Continuous Docker Image Delivery with GitLab-CI

2020-01-08

Continuous Docker Image Delivery with GitLab-CI, Docker-in-Docker, and TLS

One of the first things I had to sort out was building in gitlab-ci with docker.

Hard to make custom containers in a timely fashion if they have to be built every time you move between environments.

This will work by default with Gitlab’s shared runners out of the box.

If your using your own shared runners, there is some special setup required to get that to work. (Keep an eye out for a post on setting that up)
EDIT: Go to the follow up post to read up on how to do this

Breaking down the YAML file

variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"

services:
    - docker.io/library/docker:19.03.5-dind
...

The first pieces here are to get around the issues around Docker defaulting to enable TLS for new installs out of the box.

Commit-Build

...
commit-build:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    stage: build
    except:
        - schedules
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
        - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
        - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
...

This job defines that every commit that is made will be built and tagged with the full commit SHA sum and the short SHA sum.

(the docker-tg tag is just what gitlab runners the gitlab-ci.yml file will select, docker-tg is a set of runners I run personally, this is just to get around the issue of disabling shared runners for every new project)

Move-To-Latest

...
move-to-latest:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    stage: deploy
    when: manual
    allow_failure: false
    only:
        - master
    except:
        - schedules
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" "$CI_REGISTRY_IMAGE:latest"
        - docker push "$CI_REGISTRY_IMAGE:latest"
...

Finally we have the move-to-latest job here.

All this job does is wait for a manual start command is done on the pipeline in the Gitlab interface. Once started it will pull the long commit SHA and retag it as latest.

This on it’s own seems aimless, why not have the latest tag in the build job instead? I can hear a few people ask from a mile away.

If you look at the jobs above, you should notice the except tags excluding schedules.

This is because there is one more job, the scheduled build.

Schedule-Build

...
schedule-build:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    only:
        - schedules
    stage: build
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker build --pull -t "$CI_REGISTRY_IMAGE:$(date '+%F')" .
        - docker tag "$CI_REGISTRY_IMAGE:$(date '+%F')" "$CI_REGISTRY_IMAGE:latest"
        - docker push "$CI_REGISTRY_IMAGE:$(date '+%F')"
        - docker push "$CI_REGISTRY_IMAGE:latest"

This final job only runs when a schedule inside the Gitlab interface is triggered. (For most of my containers, it’s 4:00 AM EST every Sunday)

This allows me to have automatically building containers, specifically making sure that dependencies won’t have security holes pop open in the containers

Put it all together and we get this:

variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"

services:
    - docker.io/library/docker:19.03.5-dind
before_script:
    - docker info

commit-build:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    stage: build
    except:
        - schedules
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
        - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
        - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

move-to-latest:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    stage: deploy
    when: manual
    allow_failure: false
    only:
        - master
    except:
        - schedules
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker pull "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
        - docker tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" "$CI_REGISTRY_IMAGE:latest"
        - docker push "$CI_REGISTRY_IMAGE:latest"



schedule-build:
    image:
        name: docker.io/library/docker:19.03.5
    tags:
        - docker-tg
    only:
        - schedules
    stage: build
    before_script:
        - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    script:
        - docker build --pull -t "$CI_REGISTRY_IMAGE:$(date '+%F')" .
        - docker tag "$CI_REGISTRY_IMAGE:$(date '+%F')" "$CI_REGISTRY_IMAGE:latest"
        - docker push "$CI_REGISTRY_IMAGE:$(date '+%F')"
        - docker push "$CI_REGISTRY_IMAGE:latest"

This ci-file if I ever have to update it for some reason will be a pain to update across multiple repos at once.

However Gitlab was thinking the same thing and added a way to include template file references in a gitlab-ci.yml file.

Include declaration sample

include:
  - project: 'timeguard/docker/ci-templates'
    ref: master
    file: '/docker-tls-autobuild.yml'

Now I can update this master file to update multiple.

You can also add in other declarations into the gitlab-ci.yml file aswell, so you can add in a final production push along side it

“Production” example using include declaration & docker-compose

stages:
  - build
  - test
  - deploy
  - production

include:
  - project: 'timeguard/docker/ci-templates'
    ref: master
    file: '/docker-tls-autobuild.yml'

deploy-production:
  tags:
    - prod-shell
  only:
    - master
  except:
    - schedules
  stage: production
  before_script:
    - unset DOCKER_HOST
    - unset DOCKER_TLS_CERTDIR
    - export DOCKER_TAG=$CI_COMMIT_SHA
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker-compose -f ./prod-compose.yml pull
    - docker-compose -f ./prod-compose.yml up -d

deploy-production-schedule:
  tags:
    - production-shell
  only:
    - schedules
  stage: production
  before_script:
    - unset DOCKER_HOST
    - unset DOCKER_TLS_CERTDIR
    - export DOCKER_TAG=$(date '+%F')
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    - docker-compose -f ./prod-compose.yml pull
    - docker-compose -f ./prod-compose.yml up -d

This is just a sample using a docker compose file on production as an example. (If you can, use something else for production use. Docker-compose is a good dev tool, it’s just no Orchestrator)

Sorry if I rambled on. but I hope to paste more code tidbits like this.

Many other blogs in the past like this helped me find what I needed to get something to work. So I thought it was about time to give back.