10 July 2017

Continuous Deployment with OpenBSD, Ansible and Gitlab CI Runners

CI/CD is something that many organisations aspire to but the idea of "flicking the switch" inspires dread. With my latest little project I decided that I would do CI/CD from the very start to prevent the fear of automated deploys

Due to its sensible configuration and secure design I aim to use OpenBSD wherever possible and this project is no different.

Gitlab have a CI system built directly into their product. By adding a .gitlab-ci.yml file into your repository you can leverage their extensive CI pipelines.

A simple config might just require the execution of go test and might look something like this;

image: golang:latest

stages:
    - test

test-lint-etc:
    stage: test
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go tool vet -composites=false -shadow=true *.go
        - go test

If this pipeline passes we know that our unit tests pass and we could add another pipeline to build the binary.

The Gitlab shared runners are Linux based but the staging and production servers will be OpenBSD, thankfully GoLang makes it easy to cross compile!

image: golang:latest


stages:
    - test
    - build

test-lint-etc:
    stage: test
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go tool vet -composites=false -shadow=true *.go
        - go test

compile:
    stage: build
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-linux
        - env GOOS=openbsd go build -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-openbsd
    artifacts:
        paths:
            - $CI_PROJECT_NAME-*

Simply by adding env GOOS=openbsd before issuing go build we get an OpenBSD binary. Both the Linux and OpenBSD binarys are specified as build artifacts and made available for the next pipeline; deployment to staging!

image: golang:latest

stages:
    - test
    - build
    - deploy-staging

test-lint-etc:
    stage: test
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go tool vet -composites=false -shadow=true *.go
        - go test

compile:
    stage: build
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-linux
        - env GOOS=openbsd go build -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-openbsd
    artifacts:
        paths:
            - $CI_PROJECT_NAME-*

push-to-staging:
    stage: deploy-staging
    tags:
      - ansible
    script:
      - mkdir -p /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cp $CI_PROJECT_NAME-* /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cd /etc/ablative/ansible && git pull
      - ansible-playbook -i /etc/ablative/ansible/infrastructure_staging /etc/ablative/ansible/$CI_PROJECT_NAME.yml

Note that this pipeline is tagged with ansible, this will ensure that this pipeline executes on one of my 'Specific Runners' which in this case is a physical machine which has been locked down (as it contains the ansible-vault password file!) and then had the Gitlab runner (also written in GoLang!) deployed.

Installing a Gitlab runner is quite simple and if one doesn't already exist I'll shortly be writing an Ansible play to do it, during configuration I specified the shell executor instead of using Docker, primarily to avoid having to mount the ansible directory etc but also to limit the amount of security maintenance (updates, firewall rules, sudo/doas exec, file permissions etc).

If Ansible executes without any issues then this pipeline will succeed and we know we are good to go to production!

image: golang:latest

stages:
    - test
    - build
    - deploy-staging
    - deploy-production

test-lint-etc:
    stage: test
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go tool vet -composites=false -shadow=true *.go
        - go test

compile:
    stage: build
    script:
        - ln -s /builds /go/src/gitlab.com
        - cd /go/src/gitlab.com/$CI_PROJECT_PATH
        - go get
        - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-linux
        - env GOOS=openbsd go build -ldflags "-extldflags '-static'" -o $CI_PROJECT_NAME-openbsd
    artifacts:
        paths:
            - $CI_PROJECT_NAME-*

push-to-staging:
    stage: deploy-staging
    tags:
      - ansible
    script:
      - mkdir -p /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cp $CI_PROJECT_NAME-* /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cd /etc/ablative/ansible && git pull
      - ansible-playbook -i /etc/ablative/ansible/infrastructure_staging /etc/ablative/ansible/$CI_PROJECT_NAME.yml

push-to-production:
    stage: deploy-production
    tags:
      - ansible
    script:
      - mkdir -p /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cp $CI_PROJECT_NAME-* /etc/ablative/ansible/files/$CI_PROJECT_NAME/
      - cd /etc/ablative/ansible && git pull
      - ansible-playbook -i /etc/ablative/ansible/infrastructure_production /etc/ablative/ansible/$CI_PROJECT_NAME.yml
    when: manual

The final point to note is the addition of when:manual, this allows you to gate a release or commit requiring manual intervention via the Gitlab UI. As the project in question has a holding page and no backend servers there is currently no production to speak of (the inventory infrastructure_production is empty :/) but once the project goes live that can be removed (or not as needs require!)