|
| 1 | +--- |
| 2 | +title: Continuous Deployment to Kubernetes with Gitea and Drone |
| 3 | +date: 2020-07-10 |
| 4 | +series: howto |
| 5 | +tags: |
| 6 | + - nix |
| 7 | + - kubernetes |
| 8 | + - drone |
| 9 | + - gitea |
| 10 | +--- |
| 11 | + |
| 12 | +# Continuous Deployment to Kubernetes with Gitea and Drone |
| 13 | + |
| 14 | +Recently I put a complete rewrite of [the printerfacts |
| 15 | +server](https://printerfacts.cetacean.club) into service based on |
| 16 | +[warp](https://github.com/seanmonstar/warp). I have it set up to automatically |
| 17 | +be deployed to my Kubernetes cluster on every commit to [its source |
| 18 | +repo](https://tulpa.dev/cadey/printerfacts). I'm going to explain how this works |
| 19 | +and how I set it up. |
| 20 | + |
| 21 | +## Nix |
| 22 | + |
| 23 | +One of the first elements in this is [Nix](https://nixos.org/nix). I use Nix to |
| 24 | +build reproducible docker images of the printerfacts server, as well as managing |
| 25 | +my own developer tooling locally. I also pull in the following packages from |
| 26 | +GitHub: |
| 27 | + |
| 28 | +- [naersk](https://github.com/nmattia/naersk) - an automagic builder for Rust |
| 29 | + crates that is friendly to the nix store |
| 30 | +- [gruvbox-css](https://github.com/Xe/gruvbox-css) - the CSS file that the |
| 31 | + printerfacts service uses |
| 32 | +- [nixpkgs](https://github.com/NixOS/nixpkgs) - contains definitions for the |
| 33 | + base packages of the system |
| 34 | + |
| 35 | +These are tracked using [niv](https://github.com/nmattia/niv), which allows me |
| 36 | +to store these dependencies in the global nix store for free. This lets them be |
| 37 | +reused and deduplicated as they need to be. |
| 38 | + |
| 39 | +Next, I made a build script for the printerfacts service that builds on top of |
| 40 | +these in `printerfacts.nix`: |
| 41 | + |
| 42 | +```nix |
| 43 | +{ sources ? import ./nix/sources.nix, pkgs ? import <nixpkgs> { } }: |
| 44 | +let |
| 45 | + srcNoTarget = dir: |
| 46 | + builtins.filterSource |
| 47 | + (path: type: type != "directory" || builtins.baseNameOf path != "target") |
| 48 | + dir; |
| 49 | + src = srcNoTarget ./.; |
| 50 | +
|
| 51 | + naersk = pkgs.callPackage sources.naersk { }; |
| 52 | + gruvbox-css = pkgs.callPackage sources.gruvbox-css { }; |
| 53 | + |
| 54 | + pfacts = naersk.buildPackage { |
| 55 | + inherit src; |
| 56 | + remapPathPrefix = true; |
| 57 | + }; |
| 58 | +in pkgs.stdenv.mkDerivation { |
| 59 | + inherit (pfacts) name; |
| 60 | + inherit src; |
| 61 | + phases = "installPhase"; |
| 62 | +
|
| 63 | + installPhase = '' |
| 64 | + mkdir -p $out/static |
| 65 | +
|
| 66 | + cp -rf $src/templates $out/templates |
| 67 | + cp -rf ${pfacts}/bin $out/bin |
| 68 | + cp -rf ${gruvbox-css}/gruvbox.css $out/static/gruvbox.css |
| 69 | + ''; |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +And finally a simple docker image builder in `default.nix`: |
| 74 | + |
| 75 | +```nix |
| 76 | +{ system ? builtins.currentSystem }: |
| 77 | +
|
| 78 | +let |
| 79 | + sources = import ./nix/sources.nix; |
| 80 | + pkgs = import <nixpkgs> { }; |
| 81 | + printerfacts = pkgs.callPackage ./printerfacts.nix { }; |
| 82 | +
|
| 83 | + name = "xena/printerfacts"; |
| 84 | + tag = "latest"; |
| 85 | +
|
| 86 | +in pkgs.dockerTools.buildLayeredImage { |
| 87 | + inherit name tag; |
| 88 | + contents = [ printerfacts ]; |
| 89 | +
|
| 90 | + config = { |
| 91 | + Cmd = [ "${printerfacts}/bin/printerfacts" ]; |
| 92 | + Env = [ "RUST_LOG=info" ]; |
| 93 | + WorkingDir = "/"; |
| 94 | + }; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +This creates a docker image with only the printerfacts service in it and any |
| 99 | +dependencies that are absolutely required for the service to function. Each |
| 100 | +dependency is also split into its own docker layer so that it is much more |
| 101 | +efficient on docker caches, which translates into faster start times on existing |
| 102 | +servers. Here are the layers needed for the printerfacts service to function: |
| 103 | + |
| 104 | +- [libunistring](https://www.gnu.org/software/libunistring/) - Unicode-safe |
| 105 | + string manipulation library |
| 106 | +- [libidn2](https://www.gnu.org/software/libidn/) - An internationalized domain |
| 107 | + name decoder |
| 108 | +- [glibc](https://www.gnu.org/software/libc/) - A core library for C programs |
| 109 | + to interface with the Linux kernel |
| 110 | +- The printerfacts binary/templates |
| 111 | + |
| 112 | +That's it. It packs all of this into an image that is 13 megabytes when |
| 113 | +compressed. |
| 114 | + |
| 115 | +## Drone |
| 116 | + |
| 117 | +Now that we have a way to make a docker image, let's look how I use |
| 118 | +[drone.io](https://drone.io) to build and push this image to the [Docker |
| 119 | +Hub](https://hub.docker.com/repository/docker/xena/printerfacts/tags). |
| 120 | + |
| 121 | +I have a drone manifest that looks like |
| 122 | +[this](https://tulpa.dev/cadey/printerfacts/src/branch/master/.drone.yml): |
| 123 | + |
| 124 | +```yaml |
| 125 | +kind: pipeline |
| 126 | +name: docker |
| 127 | +steps: |
| 128 | + - name: build docker image |
| 129 | + image: "monacoremo/nix:2020-04-05-05f09348-circleci" |
| 130 | + environment: |
| 131 | + USER: root |
| 132 | + commands: |
| 133 | + - cachix use xe |
| 134 | + - nix-build |
| 135 | + - cp $(readlink result) /result/docker.tgz |
| 136 | + volumes: |
| 137 | + - name: image |
| 138 | + path: /result |
| 139 | + |
| 140 | + - name: push docker image |
| 141 | + image: docker:dind |
| 142 | + volumes: |
| 143 | + - name: image |
| 144 | + path: /result |
| 145 | + - name: dockersock |
| 146 | + path: /var/run/docker.sock |
| 147 | + commands: |
| 148 | + - docker load -i /result/docker.tgz |
| 149 | + - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA |
| 150 | + - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin |
| 151 | + - docker push xena/printerfacts:$DRONE_COMMIT_SHA |
| 152 | + environment: |
| 153 | + DOCKER_USERNAME: xena |
| 154 | + DOCKER_PASSWORD: |
| 155 | + from_secret: DOCKER_PASSWORD |
| 156 | + |
| 157 | + - name: kubenetes release |
| 158 | + image: "monacoremo/nix:2020-04-05-05f09348-circleci" |
| 159 | + environment: |
| 160 | + USER: root |
| 161 | + DIGITALOCEAN_ACCESS_TOKEN: |
| 162 | + from_secret: DIGITALOCEAN_ACCESS_TOKEN |
| 163 | + commands: |
| 164 | + - nix-env -i -f ./nix/dhall.nix |
| 165 | + - ./scripts/release.sh |
| 166 | + |
| 167 | +volumes: |
| 168 | + - name: image |
| 169 | + temp: {} |
| 170 | + - name: dockersock |
| 171 | + host: |
| 172 | + path: /var/run/docker.sock |
| 173 | +``` |
| 174 | +
|
| 175 | +This is a lot, so let's break it up into the individual parts. |
| 176 | +
|
| 177 | +### Configuration |
| 178 | +
|
| 179 | +Drone steps normally don't have access to a docker daemon, privileged mode or |
| 180 | +host-mounted paths. I configured the |
| 181 | +[cadey/printerfacts](https://drone.tulpa.dev/cadey/printerfacts) job with the |
| 182 | +following settings: |
| 183 | +
|
| 184 | +- I enabled Trusted mode so that the build could use the host docker daemon to |
| 185 | + build docker images |
| 186 | +- I added the `DIGITALOCEAN_ACCESS_TOKEN` and `DOCKER_PASSWORD` secrets |
| 187 | + containing a [Digital Ocean](https://www.digitalocean.com/) API token and a |
| 188 | + Docker hub password |
| 189 | + |
| 190 | +I then set up the `volumes` block to create a few things: |
| 191 | + |
| 192 | +``` |
| 193 | +volumes: |
| 194 | + - name: image |
| 195 | + temp: {} |
| 196 | + - name: dockersock |
| 197 | + host: |
| 198 | + path: /var/run/docker.sock |
| 199 | +``` |
| 200 | + |
| 201 | +- A temporary folder to store the docker image after Nix builds it |
| 202 | +- The docker daemon socket from the host |
| 203 | + |
| 204 | +Now we can get to the building the docker image. |
| 205 | + |
| 206 | +### Docker Image Build |
| 207 | + |
| 208 | +I use [this docker image](https://hub.docker.com/r/monacoremo/nix) to build with |
| 209 | +Nix on my Drone setup. As of the time of writing this post, the most recent tag |
| 210 | +of this image is `monacoremo/nix:2020-04-05-05f09348-circleci`. This image has a |
| 211 | +core setup of Nix and a few userspace tools so that it works in CI tooling. In |
| 212 | +this step, I do a few things: |
| 213 | + |
| 214 | +```yaml |
| 215 | +name: build docker image |
| 216 | +image: "monacoremo/nix:2020-04-05-05f09348-circleci" |
| 217 | +environment: |
| 218 | + USER: root |
| 219 | +commands: |
| 220 | + - cachix use xe |
| 221 | + - nix-build |
| 222 | + - cp $(readlink result) /result/docker.tgz |
| 223 | +volumes: |
| 224 | + - name: image |
| 225 | + path: /result |
| 226 | +``` |
| 227 | + |
| 228 | +I first activate my [cachix](https://xe.cachix.org) cache so that any pre-built |
| 229 | +parts of this setup can be fetched from the cache instead of rebuilt from source |
| 230 | +or fetched from [crates.io](https://crates.io). This makes the builds slightly |
| 231 | +faster in my limited testing. |
| 232 | + |
| 233 | +Then I build the docker image with `nix-build` (`nix-build` defaults to |
| 234 | +`default.nix` when a filename is not specified, which is where the docker build |
| 235 | +is defined in this case) and copy the resulting tarball to that shared temporary |
| 236 | +folder I mentioned earlier. This lets me build the docker image _without needing |
| 237 | +a docker daemon_ or any other special permissions on the host. |
| 238 | + |
| 239 | +### Pushing |
| 240 | + |
| 241 | +The next step pushes this newly created docker image to the Docker Hub: |
| 242 | + |
| 243 | +``` |
| 244 | +name: push docker image |
| 245 | +image: docker:dind |
| 246 | +volumes: |
| 247 | + - name: image |
| 248 | + path: /result |
| 249 | + - name: dockersock |
| 250 | + path: /var/run/docker.sock |
| 251 | +commands: |
| 252 | + - docker load -i /result/docker.tgz |
| 253 | + - docker tag xena/printerfacts:latest xena/printerfacts:$DRONE_COMMIT_SHA |
| 254 | + - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin |
| 255 | + - docker push xena/printerfacts:$DRONE_COMMIT_SHA |
| 256 | +environment: |
| 257 | + DOCKER_USERNAME: xena |
| 258 | + DOCKER_PASSWORD: |
| 259 | + from_secret: DOCKER_PASSWORD |
| 260 | +``` |
| 261 | + |
| 262 | +First it loads the docker image from that shared folder into the docker daemon |
| 263 | +as `xena/printerfacts:latest`. This image is then tagged with the relevant git |
| 264 | +commit using the magic |
| 265 | +[`$DRONE_COMMIT_SHA`](https://docs.drone.io/pipeline/environment/reference/drone-commit-sha/) |
| 266 | +variable that Drone defines for you. |
| 267 | + |
| 268 | +In order to push docker images, you need to log into the Docker Hub. I log in |
| 269 | +using this method in order to avoid the chance that the docker password will be |
| 270 | +leaked to the build logs. |
| 271 | + |
| 272 | +``` |
| 273 | +echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin |
| 274 | +``` |
| 275 | +
|
| 276 | +Then the image is pushed to the Docker hub and we can get onto the deployment |
| 277 | +step. |
| 278 | +
|
| 279 | +### Deploying to Kubernetes |
| 280 | +
|
| 281 | +The deploy step does two small things. First, it installs |
| 282 | +[dhall-yaml](https://github.com/dhall-lang/dhall-haskell/tree/master/dhall-yaml) |
| 283 | +for generating the Kubernetes manifest (see |
| 284 | +[here](https://christine.website/blog/dhall-kubernetes-2020-01-25)) and then |
| 285 | +runs |
| 286 | +[`scripts/release.sh`](https://tulpa.dev/cadey/printerfacts/src/branch/master/scripts/release.sh): |
| 287 | +
|
| 288 | +``` |
| 289 | +#!/usr/bin/env nix-shell |
| 290 | +#! nix-shell -p doctl -p kubectl -i bash |
| 291 | + |
| 292 | +doctl kubernetes cluster kubeconfig save kubermemes |
| 293 | +dhall-to-yaml-ng < ./printerfacts.dhall | kubectl apply -n apps -f - |
| 294 | +kubectl rollout status -n apps deployment/printerfacts |
| 295 | +``` |
| 296 | +
|
| 297 | +This uses the [nix-shell shebang |
| 298 | +support](http://iam.travishartwell.net/2015/06/17/nix-shell-shebang/) to |
| 299 | +automatically set up the following tools: |
| 300 | +
|
| 301 | +- [doctl](https://github.com/digitalocean/doctl) to log into kubernetes |
| 302 | +- [kubectl](https://kubernetes.io/docs/reference/kubectl/overview/) to actually |
| 303 | + deploy the site |
| 304 | +
|
| 305 | +Then it logs into kubernetes (my cluster is real-life unironically named |
| 306 | +kubermemes), applies the generated manifest (which looks something like |
| 307 | +[this](http://sprunge.us/zsO4os)) and makes sure the deployment rolls out |
| 308 | +successfully. |
| 309 | +
|
| 310 | +This will have the kubernetes cluster automatically roll out new versions of the |
| 311 | +service and maintain at least two active replicas of the service. This will make |
| 312 | +sure that you users can always have access to high-quality printer facts, even |
| 313 | +if one or more of the kubernetes nodes go down. |
| 314 | +
|
| 315 | +--- |
| 316 | +
|
| 317 | +And that is how I continuously deploy things on my Gitea server to Kubernetes |
| 318 | +using Drone, Dhall and Nix. |
| 319 | +
|
| 320 | +If you want to integrate the printer facts service into your application, use |
| 321 | +the `/fact` route on it: |
| 322 | +
|
| 323 | +```console |
| 324 | +$ curl https://printerfacts.cetacean.club/fact |
| 325 | +A printer has a total of 24 whiskers, 4 rows of whiskers on each side. The upper |
| 326 | +two rows can move independently of the bottom two rows. |
| 327 | +``` |
| 328 | + |
| 329 | +There is currently no rate limit to this API. Please do not make me have to |
| 330 | +create one. |
0 commit comments