Article Image
read

After my last article about Setting up your own docker swarm, I wanted to take a little time and share my CI/CD stack I used for managing my applications and deploying with drone.

Where I Came From

For a long long time I used Jenkins. I don't think I've updated my jenkins host for 7 years now (yuck). I tried Blue Ocean when it first came out, and I've seen it deployed successfully, but I find myself liking pipeline definitions similar to that employed by Travis CI. I use Travis extensively for my open-source projects, but I tend to be frugal when it comes to my privately-hosted stuff.

I actually really like gitlab-ci, but I found myself not wanting to put in the effort to migrate my repos to gitlab, and I don't love that their CI platform is tightly-coupled to their repository (though I do appreciate some of the UI niceness it provides).

Introducing Drone CI

So, after a bit of searching, I landed on Drone CI. It was easy enough to set up on my home docker host, and with a little reverse-proxy magic, I was there.

Drone CI is good for simple linear pipeline-driven projects. I'm talking about things like blogs (this one), simple single-service application or microservice repositories, and the like. More complex projects like UbSub I still use Jenkins and its pipeline for. I found that it felt like I was forcing a very complex pipeline into drone ci's somewhat simpler paradigm. That's slightly on me for making ubsub a mono-repo (and 7+ services), but I'm not ready to change that yet.

I won't go over installing Drone here, they have excellent documentation for that in their docs.

Using Drone Pipeline

I bootstrapped some of my simpler and older applications to drone pretty quickly. At first, I used their docker plugin, which does DIND (or Docker-in-docker) to isolate the builds. Arguably, that produces the most idempotent way to build images since nothing is cached. That also means nothing is cached... so I opted to do my own thing.

Below is my build pipeline for this blog's Dockerfile. It requires the trusted environment in drone CI, and shares the host's instance of docker. This means that you do have to trust what it's building and that no one can insert malicious code since it's running on your host, but it does make it much faster by leveraging docker's image cache.

In addition, I store my registry credentials in drone's secret store, which are called rs0_user and rs0_pass below. If you want this to deploy as well, you'll also need to store the certificates to your remote docker machine, which I put in a volume (matching docker-machine's structure)

NOTE: Take note of any example.com I replaced and change as necessary.

kind: pipeline
name: default

# Some common configuration to re-use below
_environment:
  registry: &registry
    DOCKER_REGISTRY: registry.example.com
    DOCKER_REGISTRY_USERNAME:
      from_secret: rs0_user
    DOCKER_REGISTRY_PASSWORD:
      from_secret: rs0_pass

steps:
  # Step 1. Build the image (using host cache)
  - name: docker
    image: docker:17
    volumes:
    - name: docker-socket
      path: /var/run/docker.sock
    commands:
      - docker build -t ${DRONE_REPO_NAME} .

  # Step 2. Login to the registry, tag the image as appropriate (after the job), and push
  #   Only if master
  - name: push
    image: docker:17
    environment:
      <<: *registry
    volumes:
      - name: docker-socket
        path: /var/run/docker.sock
    commands:
      - docker login -u "$DOCKER_REGISTRY_USERNAME" -p "$DOCKER_REGISTRY_PASSWORD" $DOCKER_REGISTRY
      - docker tag ${DRONE_REPO_NAME} $DOCKER_REGISTRY/${DRONE_REPO_NAME}:latest
      - docker tag ${DRONE_REPO_NAME} $DOCKER_REGISTRY/${DRONE_REPO_NAME}:${DRONE_BUILD_NUMBER}
      - docker push $DOCKER_REGISTRY/${DRONE_REPO_NAME}
    when:
      branch:
        - master

  # Step 3. Deploy `stack.yaml` to the docker swarm (Leave out if wanted)
  #   Only runs if master
  - name: deploy
    image: docker:17
    environment: # Now point to the remote docker rather than the host docker
      <<: *registry
      DOCKER_HOST: "tcp://example.com:2376"
      DOCKER_TLS_VERIFY: 1
      DOCKER_CERT_PATH: /opt/docker-certs
    volumes:
      - name: s0-certs
        path: /opt/docker-certs
    commands:
      - docker login -u "$DOCKER_REGISTRY_USERNAME" -p "$DOCKER_REGISTRY_PASSWORD" $DOCKER_REGISTRY
      - docker stack deploy --with-registry-auth --compose-file stack.yaml ${DRONE_REPO_NAME}
    when:
      branch:
        - master

# Stores the certs for docker in a volume to use to deploy
volumes:
  - name: s0-certs
    host:
      path: /opt/docker-certs/example.com
  - name: docker-socket
    host:
      path: /var/run/docker.sock

And for good measure, here's the stack.yaml file I use to define the service. You can read more in my previous post about my swarm setup.

version: '3.3'
services:
  blog:
    image: example.com/jmpfire:${DEPLOY_VERSION:-latest}
    networks:
      - traefik-net
    deploy:
      replicas: 2
      labels:
        - "traefik.enable=true"
        - "traefik.port=80"
        - "traefik.frontend.rule=Host:blog.example.com"
      resources:
        limits: { cpus: '0.1', memory: '16M' }
        reservations: { cpus: '0.025', memory: '8M' }

networks:
  traefik-net:
    external:
      name: 'common_traefik-net'

More

You can read more on the docker series here

Blog Logo

Christopher LaPointe


Published

Image

Chris LaPointe

Another site of Code

Back to Overview