From 449e934246c82d90dd0aac2644d67f928befeeb4 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Fri, 10 Jul 2020 09:25:44 -0400 Subject: [PATCH] Continuous Deployment to Kubernetes with Gitea and Drone (#177) * add drone-k8s post * oops --- blog/drone-kubernetes-cd-2020-07-10.markdown | 330 +++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 blog/drone-kubernetes-cd-2020-07-10.markdown diff --git a/blog/drone-kubernetes-cd-2020-07-10.markdown b/blog/drone-kubernetes-cd-2020-07-10.markdown new file mode 100644 index 0000000..a6babca --- /dev/null +++ b/blog/drone-kubernetes-cd-2020-07-10.markdown @@ -0,0 +1,330 @@ +--- +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 { } }: +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 { }; + 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.