---
title: Continuous Deployment to Kubernetes with Gitea and Drone
date: 2020-07-10
series: howto
tags:
 - nix
 - kubernetes
 - drone
 - gitea
---

# Continuous Deployment to Kubernetes with Gitea and Drone

Recently I put a complete rewrite of [the printerfacts
server](https://printerfacts.cetacean.club) into service based on
[warp](https://github.com/seanmonstar/warp). I have it set up to automatically
be deployed to my Kubernetes cluster on every commit to [its source
repo](https://tulpa.dev/cadey/printerfacts). I'm going to explain how this works
and how I set it up.

## Nix

One of the first elements in this is [Nix](https://nixos.org/nix). I use Nix to
build reproducible docker images of the printerfacts server, as well as managing
my own developer tooling locally. I also pull in the following packages from
GitHub:

- [naersk](https://github.com/nmattia/naersk) - an automagic builder for Rust
  crates that is friendly to the nix store
- [gruvbox-css](https://github.com/Xe/gruvbox-css) - the CSS file that the
  printerfacts service uses
- [nixpkgs](https://github.com/NixOS/nixpkgs) - contains definitions for the
  base packages of the system

These are tracked using [niv](https://github.com/nmattia/niv), which allows me
to store these dependencies in the global nix store for free. This lets them be
reused and deduplicated as they need to be.

Next, I made a build script for the printerfacts service that builds on top of
these in `printerfacts.nix`:

```nix
{ sources ? import ./nix/sources.nix, pkgs ? import <nixpkgs> { } }:
let
  srcNoTarget = dir:
    builtins.filterSource
    (path: type: type != "directory" || builtins.baseNameOf path != "target")
    dir;
  src = srcNoTarget ./.;

  naersk = pkgs.callPackage sources.naersk { };
  gruvbox-css = pkgs.callPackage sources.gruvbox-css { };
  
  pfacts = naersk.buildPackage {
    inherit src;
    remapPathPrefix = true;
  };
in pkgs.stdenv.mkDerivation {
  inherit (pfacts) name;
  inherit src;
  phases = "installPhase";

  installPhase = ''
    mkdir -p $out/static

    cp -rf $src/templates $out/templates
    cp -rf ${pfacts}/bin $out/bin
    cp -rf ${gruvbox-css}/gruvbox.css $out/static/gruvbox.css
  '';
}
```

And finally a simple docker image builder in `default.nix`:

```nix
{ system ? builtins.currentSystem }:

let
  sources = import ./nix/sources.nix;
  pkgs = import <nixpkgs> { };
  printerfacts = pkgs.callPackage ./printerfacts.nix { };

  name = "xena/printerfacts";
  tag = "latest";

in pkgs.dockerTools.buildLayeredImage {
  inherit name tag;
  contents = [ printerfacts ];

  config = {
    Cmd = [ "${printerfacts}/bin/printerfacts" ];
    Env = [ "RUST_LOG=info" ];
    WorkingDir = "/";
  };
}
```

This creates a docker image with only the printerfacts service in it and any
dependencies that are absolutely required for the service to function. Each
dependency is also split into its own docker layer so that it is much more
efficient on docker caches, which translates into faster start times on existing
servers. Here are the layers needed for the printerfacts service to function:

- [libunistring](https://www.gnu.org/software/libunistring/) - Unicode-safe
  string manipulation library
- [libidn2](https://www.gnu.org/software/libidn/) - An internationalized domain
  name decoder
- [glibc](https://www.gnu.org/software/libc/) - A core library for C programs
  to interface with the Linux kernel
- The printerfacts binary/templates

That's it. It packs all of this into an image that is 13 megabytes when
compressed.

## Drone

Now that we have a way to make a docker image, let's look how I use
[drone.io](https://drone.io) to build and push this image to the [Docker
Hub](https://hub.docker.com/repository/docker/xena/printerfacts/tags).

I have a drone manifest that looks like
[this](https://tulpa.dev/cadey/printerfacts/src/branch/master/.drone.yml):

```yaml
kind: pipeline
name: docker
steps:
  - name: build docker image
    image: "monacoremo/nix:2020-04-05-05f09348-circleci"
    environment:
      USER: root
    commands:
      - cachix use xe
      - nix-build
      - cp $(readlink result) /result/docker.tgz
    volumes:
      - name: image
        path: /result

  - name: push docker image
    image: docker:dind
    volumes:
      - name: image
        path: /result
      - name: dockersock
        path: /var/run/docker.sock
    commands:
      - docker load -i /result/docker.tgz
      - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA
      - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
      - docker push xena/printerfacts:$DRONE_COMMIT_SHA
    environment:
      DOCKER_USERNAME: xena
      DOCKER_PASSWORD:
        from_secret: DOCKER_PASSWORD

  - name: kubenetes release
    image: "monacoremo/nix:2020-04-05-05f09348-circleci"
    environment:
      USER: root
      DIGITALOCEAN_ACCESS_TOKEN:
        from_secret: DIGITALOCEAN_ACCESS_TOKEN
    commands:
      - nix-env -i -f ./nix/dhall.nix
      - ./scripts/release.sh

volumes:
  - name: image
    temp: {}
  - name: dockersock
    host:
      path: /var/run/docker.sock
```

This is a lot, so let's break it up into the individual parts.

### Configuration

Drone steps normally don't have access to a docker daemon, privileged mode or
host-mounted paths. I configured the
[cadey/printerfacts](https://drone.tulpa.dev/cadey/printerfacts) job with the
following settings:

- I enabled Trusted mode so that the build could use the host docker daemon to
  build docker images
- I added the `DIGITALOCEAN_ACCESS_TOKEN` and `DOCKER_PASSWORD` secrets
  containing a [Digital Ocean](https://www.digitalocean.com/) API token and a
  Docker hub password

I then set up the `volumes` block to create a few things:

```
volumes:
  - name: image
    temp: {}
  - name: dockersock
    host:
      path: /var/run/docker.sock
```

- A temporary folder to store the docker image after Nix builds it
- The docker daemon socket from the host

Now we can get to the building the docker image.

### Docker Image Build

I use [this docker image](https://hub.docker.com/r/monacoremo/nix) to build with
Nix on my Drone setup. As of the time of writing this post, the most recent tag
of this image is `monacoremo/nix:2020-04-05-05f09348-circleci`. This image has a
core setup of Nix and a few userspace tools so that it works in CI tooling. In
this step, I do a few things:

```yaml
name: build docker image
image: "monacoremo/nix:2020-04-05-05f09348-circleci"
environment:
  USER: root
commands:
  - cachix use xe
  - nix-build
  - cp $(readlink result) /result/docker.tgz
volumes:
  - name: image
    path: /result
```

I first activate my [cachix](https://xe.cachix.org) cache so that any pre-built
parts of this setup can be fetched from the cache instead of rebuilt from source
or fetched from [crates.io](https://crates.io). This makes the builds slightly
faster in my limited testing.

Then I build the docker image with `nix-build` (`nix-build` defaults to
`default.nix` when a filename is not specified, which is where the docker build
is defined in this case) and copy the resulting tarball to that shared temporary
folder I mentioned earlier. This lets me build the docker image _without needing
a docker daemon_ or any other special permissions on the host.

### Pushing

The next step pushes this newly created docker image to the Docker Hub:

```
name: push docker image
image: docker:dind
volumes:
  - name: image
    path: /result
  - name: dockersock
    path: /var/run/docker.sock
commands:
  - docker load -i /result/docker.tgz
  - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA
  - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
  - docker push xena/printerfacts:$DRONE_COMMIT_SHA
environment:
  DOCKER_USERNAME: xena
  DOCKER_PASSWORD:
    from_secret: DOCKER_PASSWORD
```

First it loads the docker image from that shared folder into the docker daemon
as `xena/printerfacts:latest`. This image is then tagged with the relevant git
commit using the magic
[`$DRONE_COMMIT_SHA`](https://docs.drone.io/pipeline/environment/reference/drone-commit-sha/)
variable that Drone defines for you.

In order to push docker images, you need to log into the Docker Hub. I log in
using this method in order to avoid the chance that the docker password will be
leaked to the build logs.

```
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
```

Then the image is pushed to the Docker hub and we can get onto the deployment
step.

### Deploying to Kubernetes

The deploy step does two small things. First, it installs
[dhall-yaml](https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-yaml)
for generating the Kubernetes manifest (see
[here](https://christine.website/blog/dhall-kubernetes-2020-01-25)) and then
runs
[`scripts/release.sh`](https://tulpa.dev/cadey/printerfacts/src/branch/master/scripts/release.sh):

```
#!/usr/bin/env nix-shell
#! nix-shell -p doctl -p kubectl -i bash

doctl kubernetes cluster kubeconfig save kubermemes
dhall-to-yaml-ng < ./printerfacts.dhall | kubectl apply -n apps -f -
kubectl rollout status -n apps deployment/printerfacts
```

This uses the [nix-shell shebang
support](http://iam.travishartwell.net/2015/06/17/nix-shell-shebang/) to
automatically set up the following tools:

- [doctl](https://github.com/digitalocean/doctl) to log into kubernetes
- [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) to actually
  deploy the site

Then it logs into kubernetes (my cluster is real-life unironically named
kubermemes), applies the generated manifest (which looks something like
[this](http://sprunge.us/zsO4os)) and makes sure the deployment rolls out
successfully.

This will have the kubernetes cluster automatically roll out new versions of the
service and maintain at least two active replicas of the service. This will make
sure that you users can always have access to high-quality printer facts, even
if one or more of the kubernetes nodes go down.

---

And that is how I continuously deploy things on my Gitea server to Kubernetes
using Drone, Dhall and Nix.

If you want to integrate the printer facts service into your application, use
the `/fact` route on it:

```console
$ curl https://printerfacts.cetacean.club/fact
A printer has a total of 24 whiskers, 4 rows of whiskers on each side. The upper
two rows can move independently of the bottom two rows.
```

There is currently no rate limit to this API. Please do not make me have to
create one.
