···11+---
22+title: Continuous Deployment to Kubernetes with Gitea and Drone
33+date: 2020-07-10
44+series: howto
55+tags:
66+ - nix
77+ - kubernetes
88+ - drone
99+ - gitea
1010+---
1111+1212+# Continuous Deployment to Kubernetes with Gitea and Drone
1313+1414+Recently I put a complete rewrite of [the printerfacts
1515+server](https://printerfacts.cetacean.club) into service based on
1616+[warp](https://github.com/seanmonstar/warp). I have it set up to automatically
1717+be deployed to my Kubernetes cluster on every commit to [its source
1818+repo](https://tulpa.dev/cadey/printerfacts). I'm going to explain how this works
1919+and how I set it up.
2020+2121+## Nix
2222+2323+One of the first elements in this is [Nix](https://nixos.org/nix). I use Nix to
2424+build reproducible docker images of the printerfacts server, as well as managing
2525+my own developer tooling locally. I also pull in the following packages from
2626+GitHub:
2727+2828+- [naersk](https://github.com/nmattia/naersk) - an automagic builder for Rust
2929+ crates that is friendly to the nix store
3030+- [gruvbox-css](https://github.com/Xe/gruvbox-css) - the CSS file that the
3131+ printerfacts service uses
3232+- [nixpkgs](https://github.com/NixOS/nixpkgs) - contains definitions for the
3333+ base packages of the system
3434+3535+These are tracked using [niv](https://github.com/nmattia/niv), which allows me
3636+to store these dependencies in the global nix store for free. This lets them be
3737+reused and deduplicated as they need to be.
3838+3939+Next, I made a build script for the printerfacts service that builds on top of
4040+these in `printerfacts.nix`:
4141+4242+```nix
4343+{ sources ? import ./nix/sources.nix, pkgs ? import <nixpkgs> { } }:
4444+let
4545+ srcNoTarget = dir:
4646+ builtins.filterSource
4747+ (path: type: type != "directory" || builtins.baseNameOf path != "target")
4848+ dir;
4949+ src = srcNoTarget ./.;
5050+5151+ naersk = pkgs.callPackage sources.naersk { };
5252+ gruvbox-css = pkgs.callPackage sources.gruvbox-css { };
5353+5454+ pfacts = naersk.buildPackage {
5555+ inherit src;
5656+ remapPathPrefix = true;
5757+ };
5858+in pkgs.stdenv.mkDerivation {
5959+ inherit (pfacts) name;
6060+ inherit src;
6161+ phases = "installPhase";
6262+6363+ installPhase = ''
6464+ mkdir -p $out/static
6565+6666+ cp -rf $src/templates $out/templates
6767+ cp -rf ${pfacts}/bin $out/bin
6868+ cp -rf ${gruvbox-css}/gruvbox.css $out/static/gruvbox.css
6969+ '';
7070+}
7171+```
7272+7373+And finally a simple docker image builder in `default.nix`:
7474+7575+```nix
7676+{ system ? builtins.currentSystem }:
7777+7878+let
7979+ sources = import ./nix/sources.nix;
8080+ pkgs = import <nixpkgs> { };
8181+ printerfacts = pkgs.callPackage ./printerfacts.nix { };
8282+8383+ name = "xena/printerfacts";
8484+ tag = "latest";
8585+8686+in pkgs.dockerTools.buildLayeredImage {
8787+ inherit name tag;
8888+ contents = [ printerfacts ];
8989+9090+ config = {
9191+ Cmd = [ "${printerfacts}/bin/printerfacts" ];
9292+ Env = [ "RUST_LOG=info" ];
9393+ WorkingDir = "/";
9494+ };
9595+}
9696+```
9797+9898+This creates a docker image with only the printerfacts service in it and any
9999+dependencies that are absolutely required for the service to function. Each
100100+dependency is also split into its own docker layer so that it is much more
101101+efficient on docker caches, which translates into faster start times on existing
102102+servers. Here are the layers needed for the printerfacts service to function:
103103+104104+- [libunistring](https://www.gnu.org/software/libunistring/) - Unicode-safe
105105+ string manipulation library
106106+- [libidn2](https://www.gnu.org/software/libidn/) - An internationalized domain
107107+ name decoder
108108+- [glibc](https://www.gnu.org/software/libc/) - A core library for C programs
109109+ to interface with the Linux kernel
110110+- The printerfacts binary/templates
111111+112112+That's it. It packs all of this into an image that is 13 megabytes when
113113+compressed.
114114+115115+## Drone
116116+117117+Now that we have a way to make a docker image, let's look how I use
118118+[drone.io](https://drone.io) to build and push this image to the [Docker
119119+Hub](https://hub.docker.com/repository/docker/xena/printerfacts/tags).
120120+121121+I have a drone manifest that looks like
122122+[this](https://tulpa.dev/cadey/printerfacts/src/branch/master/.drone.yml):
123123+124124+```yaml
125125+kind: pipeline
126126+name: docker
127127+steps:
128128+ - name: build docker image
129129+ image: "monacoremo/nix:2020-04-05-05f09348-circleci"
130130+ environment:
131131+ USER: root
132132+ commands:
133133+ - cachix use xe
134134+ - nix-build
135135+ - cp $(readlink result) /result/docker.tgz
136136+ volumes:
137137+ - name: image
138138+ path: /result
139139+140140+ - name: push docker image
141141+ image: docker:dind
142142+ volumes:
143143+ - name: image
144144+ path: /result
145145+ - name: dockersock
146146+ path: /var/run/docker.sock
147147+ commands:
148148+ - docker load -i /result/docker.tgz
149149+ - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA
150150+ - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
151151+ - docker push xena/printerfacts:$DRONE_COMMIT_SHA
152152+ environment:
153153+ DOCKER_USERNAME: xena
154154+ DOCKER_PASSWORD:
155155+ from_secret: DOCKER_PASSWORD
156156+157157+ - name: kubenetes release
158158+ image: "monacoremo/nix:2020-04-05-05f09348-circleci"
159159+ environment:
160160+ USER: root
161161+ DIGITALOCEAN_ACCESS_TOKEN:
162162+ from_secret: DIGITALOCEAN_ACCESS_TOKEN
163163+ commands:
164164+ - nix-env -i -f ./nix/dhall.nix
165165+ - ./scripts/release.sh
166166+167167+volumes:
168168+ - name: image
169169+ temp: {}
170170+ - name: dockersock
171171+ host:
172172+ path: /var/run/docker.sock
173173+```
174174+175175+This is a lot, so let's break it up into the individual parts.
176176+177177+### Configuration
178178+179179+Drone steps normally don't have access to a docker daemon, privileged mode or
180180+host-mounted paths. I configured the
181181+[cadey/printerfacts](https://drone.tulpa.dev/cadey/printerfacts) job with the
182182+following settings:
183183+184184+- I enabled Trusted mode so that the build could use the host docker daemon to
185185+ build docker images
186186+- I added the `DIGITALOCEAN_ACCESS_TOKEN` and `DOCKER_PASSWORD` secrets
187187+ containing a [Digital Ocean](https://www.digitalocean.com/) API token and a
188188+ Docker hub password
189189+190190+I then set up the `volumes` block to create a few things:
191191+192192+```
193193+volumes:
194194+ - name: image
195195+ temp: {}
196196+ - name: dockersock
197197+ host:
198198+ path: /var/run/docker.sock
199199+```
200200+201201+- A temporary folder to store the docker image after Nix builds it
202202+- The docker daemon socket from the host
203203+204204+Now we can get to the building the docker image.
205205+206206+### Docker Image Build
207207+208208+I use [this docker image](https://hub.docker.com/r/monacoremo/nix) to build with
209209+Nix on my Drone setup. As of the time of writing this post, the most recent tag
210210+of this image is `monacoremo/nix:2020-04-05-05f09348-circleci`. This image has a
211211+core setup of Nix and a few userspace tools so that it works in CI tooling. In
212212+this step, I do a few things:
213213+214214+```yaml
215215+name: build docker image
216216+image: "monacoremo/nix:2020-04-05-05f09348-circleci"
217217+environment:
218218+ USER: root
219219+commands:
220220+ - cachix use xe
221221+ - nix-build
222222+ - cp $(readlink result) /result/docker.tgz
223223+volumes:
224224+ - name: image
225225+ path: /result
226226+```
227227+228228+I first activate my [cachix](https://xe.cachix.org) cache so that any pre-built
229229+parts of this setup can be fetched from the cache instead of rebuilt from source
230230+or fetched from [crates.io](https://crates.io). This makes the builds slightly
231231+faster in my limited testing.
232232+233233+Then I build the docker image with `nix-build` (`nix-build` defaults to
234234+`default.nix` when a filename is not specified, which is where the docker build
235235+is defined in this case) and copy the resulting tarball to that shared temporary
236236+folder I mentioned earlier. This lets me build the docker image _without needing
237237+a docker daemon_ or any other special permissions on the host.
238238+239239+### Pushing
240240+241241+The next step pushes this newly created docker image to the Docker Hub:
242242+243243+```
244244+name: push docker image
245245+image: docker:dind
246246+volumes:
247247+ - name: image
248248+ path: /result
249249+ - name: dockersock
250250+ path: /var/run/docker.sock
251251+commands:
252252+ - docker load -i /result/docker.tgz
253253+ - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA
254254+ - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
255255+ - docker push xena/printerfacts:$DRONE_COMMIT_SHA
256256+environment:
257257+ DOCKER_USERNAME: xena
258258+ DOCKER_PASSWORD:
259259+ from_secret: DOCKER_PASSWORD
260260+```
261261+262262+First it loads the docker image from that shared folder into the docker daemon
263263+as `xena/printerfacts:latest`. This image is then tagged with the relevant git
264264+commit using the magic
265265+[`$DRONE_COMMIT_SHA`](https://docs.drone.io/pipeline/environment/reference/drone-commit-sha/)
266266+variable that Drone defines for you.
267267+268268+In order to push docker images, you need to log into the Docker Hub. I log in
269269+using this method in order to avoid the chance that the docker password will be
270270+leaked to the build logs.
271271+272272+```
273273+echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
274274+```
275275+276276+Then the image is pushed to the Docker hub and we can get onto the deployment
277277+step.
278278+279279+### Deploying to Kubernetes
280280+281281+The deploy step does two small things. First, it installs
282282+[dhall-yaml](https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-yaml)
283283+for generating the Kubernetes manifest (see
284284+[here](https://christine.website/blog/dhall-kubernetes-2020-01-25)) and then
285285+runs
286286+[`scripts/release.sh`](https://tulpa.dev/cadey/printerfacts/src/branch/master/scripts/release.sh):
287287+288288+```
289289+#!/usr/bin/env nix-shell
290290+#! nix-shell -p doctl -p kubectl -i bash
291291+292292+doctl kubernetes cluster kubeconfig save kubermemes
293293+dhall-to-yaml-ng < ./printerfacts.dhall | kubectl apply -n apps -f -
294294+kubectl rollout status -n apps deployment/printerfacts
295295+```
296296+297297+This uses the [nix-shell shebang
298298+support](http://iam.travishartwell.net/2015/06/17/nix-shell-shebang/) to
299299+automatically set up the following tools:
300300+301301+- [doctl](https://github.com/digitalocean/doctl) to log into kubernetes
302302+- [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) to actually
303303+ deploy the site
304304+305305+Then it logs into kubernetes (my cluster is real-life unironically named
306306+kubermemes), applies the generated manifest (which looks something like
307307+[this](http://sprunge.us/zsO4os)) and makes sure the deployment rolls out
308308+successfully.
309309+310310+This will have the kubernetes cluster automatically roll out new versions of the
311311+service and maintain at least two active replicas of the service. This will make
312312+sure that you users can always have access to high-quality printer facts, even
313313+if one or more of the kubernetes nodes go down.
314314+315315+---
316316+317317+And that is how I continuously deploy things on my Gitea server to Kubernetes
318318+using Drone, Dhall and Nix.
319319+320320+If you want to integrate the printer facts service into your application, use
321321+the `/fact` route on it:
322322+323323+```console
324324+$ curl https://printerfacts.cetacean.club/fact
325325+A printer has a total of 24 whiskers, 4 rows of whiskers on each side. The upper
326326+two rows can move independently of the bottom two rows.
327327+```
328328+329329+There is currently no rate limit to this API. Please do not make me have to
330330+create one.