CI/CD for Microservices on DigitalOcean Kubernetes
Tags: kubernetes, tutorial, devops, cicd
Published: 2019-03-13 11:52:03

---
title: CI/CD for Microservices on DigitalOcean Kubernetes
published: true
description: "Tutorial: How to set up CI/CD pipeline for a Dockerized microservice with Semaphore and DigitalOcean Kubernetes."
tags: kubernetes, tutorial, devops, cicd
canonical_url: https://semaphoreci.com/blog/cicd-microservices-digitalocean-kubernetes
cover_image: https://thepracticaldev.s3.amazonaws.com/i/x1pus4ipybbaruvqah4m.png
---

[Semaphore](https://semaphoreci.com) gives you the power to easily create CI/CD pipelines that build, run and deploy Docker containers. DigitalOcean recently introduced [a managed Kubernetes service](https://blog.digitalocean.com/digitalocean-releases-k8s-as-a-service/) which simplifies running cloud-native applications. Together, they’re a great match for productive software development. In this article we’ll show you how to connect these two services together in a fast continuous delivery pipeline.

## What we're building

We'll use a Ruby Sinatra microservice which exposes a few HTTP endpoints and includes a test suite. We'll package it with Docker and deploy to DigitalOcean Kubernetes. The CI/CD pipeline will fully automate the following tasks:

- Install project dependencies, reusing them from cache most of the time;
- Run unit tests;
- Build and tag a Docker image;
- Push the Docker image to Docker Hub container registry;
- Provide a one-click deployment to DigitalOcean Kubernetes.

![Final CI/CD pipeline](https://thepracticaldev.s3.amazonaws.com/i/3ji2iv1bmvs83oschgsz.png)

We'll go step by step, but if you'd like to jump straight into the final version of source code, check out semaphore-demo-ruby-kubernetes repository on GitHub:

{% github semaphoreci-demos/semaphore-demo-ruby-kubernetes %}

Let's begin!

## Launch a Kubernetes cluster in 5 minutes on DigitalOcean

Launching a Kubernetes cluster on DigitalOcean is straightforward. From your dashboard, use the "Create" button on top to create it. The cluster will become available in 4-5 minutes.

![Creating a Kubernetes cluster on DigitalOcean](https://thepracticaldev.s3.amazonaws.com/i/037dw1taykle4wjic8gt.png)

The cluster page includes a number of tips and resources that you can use. If you haven't done that yet, now is the time to [install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/), the Kubernetes command-line tool.

## Connect to your Kubernetes cluster

Scroll to the bottom of your cluster page to download the configuration file that you will use to authenticate and connect to the cluster.

![Download your DigitalOcean Kubernetes configuration file](https://thepracticaldev.s3.amazonaws.com/i/ou2za8iy1kglq1mgxpzc.png)

On your local machine, create a directory to contain the Kubernetes configuration file:

```
$ mkdir ~/.kube
```

Move the downloaded file to `~/.kube` and instruct kubectl to use it. You can run the following command in your terminal session, or add it to your shell profile like `.bashrc` or `.zshrc`:

```
$ export KUBECONFIG=$HOME/.kube/dok8s.yaml
```

Make sure to change `dok8s.yaml` to match the name of your file.

Verify that you can communicate with your DigitalOcean Kubernetes cluster by running `kubectl get nodes`. When the command is successful, it returns information similar to the following:

```
$ kubectl get nodes
NAME                        STATUS    ROLES     AGE       VERSION
nostalgic-heisenberg-8vi3   Ready     <none>    4d        v1.13.2
nostalgic-heisenberg-8vi8   Ready     <none>    4d        v1.13.2
```

The number of nodes will match the number you selected during the cluster creation process. Note that if you run `get nodes` while your cluster is still being provisioned, the number of nodes will be zero.

DigitalOcean hasn't yet responded to my inquiry about their relationship with Walter White.

## Connect Semaphore to your Kubernetes cluster

At this point you have a Kubernetes cluster that you can control from your local machine. Let's configure a basic CI/CD project in which Semaphore can also successfully execute `kubectl get nodes`.

If you're new to Semaphore, start by [creating a free account](https://semaphoreci.com). The free account provides you with $20 of credit every month, which is enough for up to 1,300 minutes of service. If you connect a credit card, you'll get an additional $200 of free credit.

### Create a project on Semaphore

Once you’re in Semaphore, follow the “Projects > New” link in the sidebar on the left hand side. You can follow the on-screen instructions to select a repository and commit the default YML file. In this tutorial I’ll show you the command line approach.

First, [install sem, the Semaphore command-line tool](https://docs.semaphoreci.com/article/53-sem-reference). You’ll find the exact instructions by opening up the CLI widget in the top-right corner of any Semaphore screen.

![Finding the instructions to install Semaphore CLI](https://thepracticaldev.s3.amazonaws.com/i/6eqbawc4yxk0422kjeij.png)

The first command downloads and installs the CLI:

```
$ curl https://storage.googleapis.com/sem-cli-releases/get.sh | bash
```

The second command connects sem to your organization account:

```
$ sem connect ORGANIZATION.semaphoreci.com ACCESS_TOKEN
```

Next, initialize the Git repository you’d like to connect to Semaphore. You can also do this in an empty Git repository:


```
$ sem init
Project is created. You can find it at https://ORGNAME.semaphoreci.com/projects/PROJECTNAME.

To run your first pipeline execute:

  git add .semaphore/semaphore.yml && git commit -m "First pipeline" && git push
```

The command creates a deploy key and webhook on GitHub, so that Semaphore can access your code as it changes, and creates a pipeline definition file `.semaphore/semaphore.yml` on your computer.

### Authenticating with Kubernetes using a Semaphore secret

Let's edit `semaphore.yml` and instruct Semaphore how to talk to Kubernetes.

Semaphore already [provides kubectl preinstalled](https://docs.semaphoreci.com/article/32-ubuntu-1804-image). So what's left is to securely upload the Kubernetes configuration file inside the Semaphore environment. We generally solve this by creating a [secret](https://docs.semaphoreci.com/article/62-concepts#secrets). A secret can be a collection of environment variable and files. Once created, it's available to all projects within an organization.

In our case, we need a secret based on a single file. Create it using sem:

```
$ sem create secret do-k8s --file ~/.kube/do-kubernetes.yaml:/home/semaphore/.kube/dok8s.yaml
```

The command above [creates a secret based on a local file](https://docs.semaphoreci.com/article/66-environment-variables-and-secrets#a-shortcut-for-creating-file-based-secrets), and instructs Semaphore to make it available in `/home/semaphore/.kube/dok8s.yaml`. `/home/semaphore/` is the default directory from which all CI/CD jobs run.

The full Semaphore configuration which mounts this secret and makes it available the CI/CD job is as follows:

```yaml
# .semaphore/semaphore.yml
version: v1.0
name: Hello Kubernetes
agent:
  machine:
    type: e1-standard-2
    os_image: ubuntu1804
blocks:
  - name: Talk to K8s
    task:
      secrets:
        - name: do-k8s
      env_vars:
        - name: KUBECONFIG
          value: /home/semaphore/.kube/dok8s.yaml
      jobs:
      - name: Get nodes
        commands:
          - checkout
          - kubectl get nodes
```

If this is the first time you see a Semaphore configuration file, [a quick tour of concepts](https://docs.semaphoreci.com/article/62-concepts) will help you understand it. Here’s the gist of how they apply in this example:

- In `agent` section we specify the environment which will run our code and commands. We combine one of the available [machine types](https://docs.semaphoreci.com/article/20-machine-types) and [operating system images](https://docs.semaphoreci.com/article/32-ubuntu-1804-image).
- In `secrets` section we mount the secret that we’ve just created, and use the file it provides to define the `KUBECONFIG` environment variable in the `env_vars` section.
- Our pipeline has one block and one job, in which we download our code from GitHub and run `kubectl get nodes`.

Run `git push` and you should see a basic pipeline running on Semaphore:

![Hello Kubernetes pipeline](https://thepracticaldev.s3.amazonaws.com/i/ijhj1ti87gxn6vwhvmo3.png)

Click on "Get nodes" to view the job log:

![Hello Kubernetes job](https://thepracticaldev.s3.amazonaws.com/i/8dmiqmd3wzasa86zll2b.png)

OK, we're in business! Let's proceed by setting up an actual project.

## Set up continuous integration for a Sinatra microservice

Our Sinatra app is a microservice with minimal configuration and an RSpec test suite:

```
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── app.rb
├── config.ru
└── spec
    ├── app_spec.rb
    └── spec_helper.rb
```

Let's clear our previous `semaphore.yml` and enter the following configuration to run CI:

```yaml
# .semaphore/semaphore.yml
version: v1.0
name: CI
agent:
  machine:
    type: e1-standard-2
    os_image: ubuntu1804

blocks:
  - name: Install dependencies
    task:
      jobs:
        - name: bundle install
          commands:
            - checkout
            - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH,gems-master
            - bundle install --deployment --path .bundle
            - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) .bundle

  - name: Tests
    task:
      jobs:
        - name: rspec
          commands:
            - checkout
            - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH,gems-master
            - bundle install --deployment --path .bundle
            - bundle exec rspec
```

We store our gems [in Semaphore cache](https://docs.semaphoreci.com/article/68-caching-dependencies) to avoid running `bundle install` from scratch on every commit.

We split dependency installation and running tests in separate sequential blocks for demonstration purposes. You could, of course, merge them in one.

It's necessary to run `bundle install` in the second block, even though `cache restore` will at that point always restore the gem bundle. It's a limitation on Bundler's side, but the good part is that the command will exit very quickly.

When you push the new `semaphore.yml` to GitHub, you'll see a real CI pipeline shaping up on Semaphore:

![Continuous integration pipeline](https://thepracticaldev.s3.amazonaws.com/i/zgyf9u26vruqg3lfn10t.png)

## Push Docker image to Docker Hub container registry

Deploying to Kubernetes requires us to push a Docker image to a container registry. In this example we'll use Docker Hub. The procedure is very similar for other registry providers.

These instructions will work with any Docker image. For tips for Sinatra, check out my earlier post:

{% link https://dev.to/markoa/dockerize-a-sinatra-microservice-29l8 %}

Pushing to a container registry, public or private, requires authentication. For example, when you're using Docker Desktop on Mac, you're automatically authenticated and communication with Docker Hub just works. In CI/CD environment, we need to make credentials available and authenticate before doing `docker push`.

Following the [Docker Hub instructions available in Semaphore documentation](https://docs.semaphoreci.com/article/70-dockerhub), we need to create a secret.

Open a new file `secret.yml`:

```yaml
# secret.yml
apiVersion: v1alpha
kind: Secret
metadata:
  name: my-dockerhub
data:
  env_vars:
    - name: DOCKER_USERNAME
      value: "YOUR USERNAME"
    - name: DOCKER_PASSWORD
      value: "YOUR PASSWORD"
```

Use the sem CLI to create the secret, and remove the source file after you're done:

```
$ sem create -f secret.yml
$ rm secret.yml
```

You can verify that it worked with:

```
$ sem get secrets
NAME               AGE
my-dockerhub   11s

$ sem get secret markoa-dockerhub
apiVersion: v1beta
kind: Secret
metadata:
  name: my-dockerhub
  id: 89596f93-f2ca-4414-88be-e9602174034a
  create_time: "1550149760"
  update_time: "1550149760"
data:
  env_vars:
  - name: DOCKER_USERNAME
    value: xxx
  - name: DOCKER_PASSWORD
    value: xxx
  files: []
```

We now have what it takes to push to Docker Hub from a Semaphore job.

With Docker build and push operations we are entering the delivery phase of our project. [We'll extend our CI pipeline with a promotion](https://docs.semaphoreci.com/article/67-deploying-with-promotions) and use it to trigger the next stage.

At the bottom of `semaphore.yml`, define a promotion:

```yaml
# .semaphore/semaphore.yml
# ...
promotions:
  - name: Dockerize
    pipeline_file: docker-build.yml
    auto_promote_on:
      - result: passed
```

With `auto_promote_on` specified above, our Dockerize pipeline will run on every change in every branch. You could customize that behavior with [additional conditions](https://docs.semaphoreci.com/article/50-pipeline-yaml#promotions).

Let’s define the Dockerize pipeline:

```yaml
# .semaphore/docker-build.yml
version: v1.0
name: Docker build
agent:
  machine:
    type: e1-standard-4
    os_image: ubuntu1804
blocks:
  - name: Build
    task:
      secrets:
        - name: my-dockerhub
      jobs:
      - name: Docker build
        commands:
          - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
          - checkout
          - docker pull semaphoredemos/semaphore-demo-ruby-kubernetes:latest || true
          - docker build --cache-from semaphoredemos/semaphore-demo-ruby-kubernetes:latest -t semaphoredemos/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID .
          - docker images
          - docker push semaphoredemos/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID
```

In the first command we authenticate with Docker Hub using the environment variables defined in the `my-dockerhub` secret.

We’re using [Docker layer caching](https://docs.semaphoreci.com/article/81-docker-layer-caching) to speed up the container build process. First, we try to pull a previously built image from the registry. If this is the first time we run this operation, this step will be skipped and not fail.

By using the `--cache-from` flag with `docker build`, we’re reusing the layers from the pulled image to build the new one faster.

In `docker push` command we are using the `SEMAPHORE_WORKFLOW_ID` environment variable to produce a unique artifact after every build. It’s one of the environment variables available in every Semaphore job; [see documentation for a complete list](https://docs.semaphoreci.com/article/12-environment-variables).

Note that we’re not creating a new version of the latest tag. We’re going to do that only after a successful deploy.

Once you replace the image name with your own, you should have a pipeline in a state similar to this:

![Docker build pipeline](https://thepracticaldev.s3.amazonaws.com/i/6sv1xhsjhiwnq4dl7mar.png)

The job log shows that the container image has been created and pushed:

![Docker build job](https://thepracticaldev.s3.amazonaws.com/i/18ip58aijazb30jxybyf.png)

And your Docker registry contains the latest images:

![Pushed container image on Docker Hub](https://thepracticaldev.s3.amazonaws.com/i/r1hkjnprzn8rythuxw82.png)

## Deploy to Kubernetes

Back on the DigitalOcean's Kubernetes cluster page, the "Getting Started" section includes examples to "Deploy a workload". We can use the example provided for nginx and modify it for our app.

In the example configuration, you'll notice a reference to a source container image:

```yaml
# ...
    spec:
      containers:
        - name: nginx
          image: library/nginx
```

If your Docker image is private, you'll need to enable the Kubernetes cluster to authenticate with the Docker registry. The way to do that is, once again, by creating a secret, only this time on the Kubernetes cluster's end. For demonstration purposes, I will show you how to do this, even though the image that we're using in this tutorial is public.

Run the following command on your local machine to create a `docker-registry`-type secret on your Kubernetes cluster:

```
$ kubectl create secret docker-registry dockerhub-user --docker-server=https://index.docker.io/v1/ --docker-username=YOUR_DOCKER_HUB_USERNAME --docker-password=YOUR_DOCKER_HUB_PASSWORD --docker-email=YOUR_EMAIL`
```

You can verify the secret by running:

```
$ kubectl get secret dockerhub-user --output=yaml
```

As on Semaphore, Kubernetes secrets are base64-encoded and the output will look similar to:

```yaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJodHR...
kind: Secret
metadata:
  creationTimestamp: 2019-02-08T10:18:52Z
  name: dockerhub-user
  namespace: default
  resourceVersion: "7431"
  selfLink: /api/v1/namespaces/default/secrets/dockerhub-user
  uid: eec7c39e-2b8a-11e9-a804-1a46bc991881
type: kubernetes.io/dockerconfigjson
```

### Write a deployment manifest

Create a new file in your repository, for example called `deployment.yml`:

```yaml
# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: semaphore-demo-ruby-kubernetes
spec:
  replicas: 1
  selector:
    matchLabels:
      app: semaphore-demo-ruby-kubernetes
  template:
    metadata:
      labels:
        app: semaphore-demo-ruby-kubernetes
    spec:
      containers:
        - name: semaphore-demo-ruby-kubernetes
          image: semaphoredemos/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID
      imagePullSecrets: # if using a private image
        - name: dockerhub-user
``` 

Comparing to the nginx example provided by DigitalOcean, we've pretty much only substituted the application and image name. Since Semaphore is tagging images using `SEMAPHORE_WORKFLOW_ID` environment variable, we're using it here as well.

The deployment configuration file as it appears now is **not** valid YML. The plan is to use a Linux utility called [envsubst](https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html) (also available on Mac via Homebrew) to replace `$SEMAPHORE_WORKFLOW_ID` with its value within a Semaphore CI/CD job.

Our deployment manifest however is not yet complete without **a Kubernetes load balancer** which will expose the deployed service on a public IP address. Add the following content to the same file:

```yaml
# deployment.yml
# ...
---

apiVersion: v1
kind: Service
metadata:
  name: semaphore-demo-lb
spec:
  selector:
    app: semaphore-demo-ruby-kubernetes
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 4567
```

You can verify that the Kubernetes configuration works as intended from your local machine by replacing `$SEMAPHORE_WORKFLOW_ID` with `latest` and running:

```
$ kubectl apply -f deployment.yml
$ kubectl get services
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)        AGE
semaphore-demo-ruby-kubernetes     LoadBalancer   10.245.117.152   68.183.249.106   80:30569/TCP   5d
...
```

### Define a Semaphore deployment pipeline

We're entering the last stage of CI/CD configuration. At this point we have a CI pipeline defined in `semaphore.yml` and a `Docker build` pipeline defined in `docker-build.yml`. We're going to define a third pipeline to trigger manually from `Docker build` which will deploy to Kubernetes.

Start by defining a manual promotion:

```yaml
# .semaphore/docker-build.yml
# ...
promotions:
  - name: Deploy to Kubernetes
    pipeline_file: deploy-k8s.yml
```

Finally, let's define the deployment pipeline. It has two jobs: to apply a new Kubernetes configuration and to create a new version of our `latest` container image, which we're treating like master branch in Git (your practice may vary).

```yaml
# .semaphore/deploy-k8s.yml
version: v1.0
name: Deploy to Kubernetes
agent:
  machine:
    type: e1-standard-2
    os_image: ubuntu1804
blocks:
  - name: Deploy to Kubernetes
    task:
      secrets:
        - name: do-k8s
      env_vars:
        - name: KUBECONFIG
          value: /home/semaphore/.kube/dok8s.yaml
      jobs:
      - name: Deploy
        commands:
          - checkout
          - kubectl get nodes
          - kubectl get pods
          - envsubst < deployment.yml | tee deployment.yml
          - kubectl apply -f deployment.yml

  - name: Tag latest release
    task:
      secrets:
        - name: dockerhub-users
      jobs:
      - name: docker tag latest
        commands:
          - echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
          - docker pull semaphoreci-demos/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID
          - docker tag semaphoreci-demos/semaphore-demo-ruby-kubernetes:$SEMAPHORE_WORKFLOW_ID semaphoreci-demos/semaphore-demo-ruby-kubernetes:latest
          - docker push semaphoreci-demos/semaphore-demo-ruby-kubernetes:latest
```

Once we push a new version of configuration to GitHub, Semaphore runs our pipeline. Once the Docker build pipeline is completed successfully, click on the "Promote" button to trigger the deployment:

![Launched deployment to Kubernetes](https://thepracticaldev.s3.amazonaws.com/i/qdomze3sbj0vxm4oj9hy.png)

You can now run `kubectl get services` or open the Load Balancers list on DigitalOcean > Networking tab section to find the public IP address of your microservice:

![IP address from load balancer](https://thepracticaldev.s3.amazonaws.com/i/osa990bz6lbu1oxh5p43.png)

And test it out:

```
$ curl 68.183.251.210
hello world :))
```

Congratulations! You now have a fully automated continuous delivery pipeline to Kubernetes.

## Deploy a demo app for yourself

Feel free to fork the [semaphore-demo-ruby-kubernetes](https://github.com/semaphoreci-demos/semaphore-demo-ruby-kubernetes) repository and create a Semaphore project to deploy it on your Kubernetes instance.

Here are some ideas for potential changes you can make:

- Introduce a staging environment
- Build a Docker image first, and run tests inside it (requires a development version of Dockerfile since it's best to avoid installing development and test dependencies when producing an image for production).
- Extend the project with more microservices.

This article is based on an episode of Semaphore Uncut, a YouTube video series on CI/CD:

{% youtube hi-OPGpecXQ %}

Thanks for reading! 🙌 Please give me feedback, and I'll be happy to answer any questions you may have in comments.

For more in-depth content follow me here or [sign up for Semaphore's free ebook on CI/CD with Kubernetes](https://eepurl.com/dLkTuw). ❤️

_Originally published on [Semaphore blog](https://semaphoreci.com/blog/cicd-microservices-digitalocean-kubernetes)_.