summaryrefslogtreecommitdiff
path: root/static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md
diff options
context:
space:
mode:
Diffstat (limited to 'static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md')
-rw-r--r--static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md249
1 files changed, 0 insertions, 249 deletions
diff --git a/static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md b/static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md
deleted file mode 100644
index ebc0f7b..0000000
--- a/static/src/_posts/2021-04-22-composing-processes-into-a-static-binary-with-nix.md
+++ /dev/null
@@ -1,249 +0,0 @@
----
-title: >-
- Composing Processes Into a Static Binary With Nix
-description: >-
- Goodbye, docker-compose!
-tags: tech
----
-
-It's pretty frequent that one wants to use a project that requires multiple
-processes running. For example, a small web api which uses some database to
-store data in, or a networking utility which has some monitoring process which
-can be run alongside it.
-
-In these cases it's extremely helpful to be able to compose these disparate
-processes together into a single process. From the user's perspective it's much
-nicer to only have to manage one process (even if it has hidden child
-processes). From a dev's perspective the alternatives are: finding libraries in
-the same language which do the disparate tasks and composing them into the same
-process via import, or (if such libraries don't exist, which is likely)
-rewriting the functionality of all processes into a new, monolithic project
-which does everything; a huge waste of effort!
-
-## docker-compose
-
-A tool I've used before for process composition is
-[docker-compose][docker-compose]. While it works well for composition, it
-suffers from the same issues docker in general suffers from: annoying networking
-quirks, a questionable security model, and the need to run the docker daemon.
-While these issues are generally surmountable for a developer or sysadmin, they
-are not suitable for a general-purpose project which will be shipped to average
-users.
-
-## nix-bundle
-
-Enter [nix-bundle][nix-bundle]. This tools will take any [nix][nix] derivation
-and construct a single static binary out of it, a la [AppImage][appimage].
-Combined with a process management tool like [circus][circus], nix-bundle
-becomes a very useful tool for composing processes together!
-
-To demonstrate this, we'll be looking at putting together a project I wrote
-called [markov][markov], a simple REST API for building [markov
-chains][markov-chain] which is written in [go][golang] and backed by
-[redis][redis].
-
-## Step 1: Building Individual Components
-
-Step one is to get [markov][markov] and its dependencies into a state where it
-can be run with [nix][nix]. Doing this is fairly simple, we merely use the
-`buildGoModule` function:
-
-```
-pkgs.buildGoModule {
- pname = "markov";
- version = "618b666484566de71f2d59114d011ff4621cf375";
- src = pkgs.fetchFromGitHub {
- owner = "mediocregopher";
- repo = "markov";
- rev = "618b666484566de71f2d59114d011ff4621cf375";
- sha256 = "1sx9dr1q3vr3q8nyx3965x6259iyl85591vx815g1xacygv4i4fg";
- };
- vendorSha256 = "048wygrmv26fsnypsp6vxf89z3j0gs9f1w4i63khx7h134yxhbc6";
-}
-```
-
-This expression results in a derivation which places the markov binary at
-`bin/markov`.
-
-The other component we need to run markov is [redis][redis], which conveniently
-is already packaged in nixpkgs as `pkg.redis`.
-
-## Step 2: Composing Using Circus
-
-[Circus][circus] can be configured to run multiple processes at the same time.
-It will collect the stdout/stderr logs of these processes and combine them into
-a single stream, or write them to log files. If any processes fail circus will
-automatically restart them. It has a simple configuration and is, overall, a
-great tool for a simple project like this.
-
-Circus also comes pre-packed in nixpkgs, so we don't need to do anything to
-actually build it. We only need to configure it. To do this we'll write a bash
-script which generates the configuration on-the-fly, and then runs the process
-with that configuration.
-
-This script is going to act as the "frontend" for our eventual static binary;
-the user will pass in configuration parameters to this script, and this script
-will translate those into the appropriate configuration for all sub-process
-(markov, redis, circus). For this demo we won't go nuts with the configuration,
-we'll just expose the following:
-
-* `MARKOV_LISTEN_ADDR`: Address REST API will listen on (defaults to
- `localhost:8000`).
-
-* `MARKOV_TIMEOUT`: Expiration time of each link of the chain (defaults to 720
- hours).
-
-* `MARKOV_DATA_DIR`: Directory where data will be stored (defaults to current
- working directory).
-
-The bash script will take these params in as environment variables. The nix
-expression to generate the bash script, which we'll call our entrypoint script,
-will look like this (assumes that the expression to generate `bin/markov`,
-defined above, is set to the `markov` variable):
-
-```
-pkgs.writeScriptBin "markov" ''
- #!${pkgs.stdenv.shell}
-
- # On every run we create new, temporary, configuration files for redis and
- # circus. To do this we create a new config directory.
- markovCfgDir=$(${pkgs.coreutils}/bin/mktemp -d)
- echo "generating configuration to $markovCfgDir"
-
- cat >$markovCfgDir/redis.conf <<EOF
- save ""
- dir "''${MARKOV_DATA_DIR:-$(pwd)}"
- appendonly yes
- appendfilename "markov.data"
- EOF
-
- cat >$markovCfgDir/circus.ini <<EOF
-
- [circus]
-
- [watcher:markov]
- cmd = ${markov}/bin/markov \
- -listenAddr ''${MARKOV_LISTEN_ADDR:-localhost:8000} \
- -timeout ''${MARKOV_TIMEOUT:-720}
- numprocesses = 1
-
- [watcher:redis]
- cmd = ${pkgs.redis}/bin/redis-server $markovCfgDir/redis.conf
- numprocesses = 1
- EOF
-
- exec ${pkgs.circus}/bin/circusd $markovCfgDir/circus.ini
-'';
-```
-
-By `nix-build`ing this expression we end up with a derivation with
-`bin/markov`, and running that should result in the following output:
-
-```
-generating configuration to markov.VLMPwqY
-2021-04-22 09:27:56 circus[181906] [INFO] Starting master on pid 181906
-2021-04-22 09:27:56 circus[181906] [INFO] Arbiter now waiting for commands
-2021-04-22 09:27:56 circus[181906] [INFO] markov started
-2021-04-22 09:27:56 circus[181906] [INFO] redis started
-181923:C 22 Apr 2021 09:27:56.063 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
-181923:C 22 Apr 2021 09:27:56.063 # Redis version=6.0.6, bits=64, commit=00000000, modified=0, pid=181923, just started
-181923:C 22 Apr 2021 09:27:56.063 # Configuration loaded
-...
-```
-
-The `markov` server process doesn't have many logs, unfortunately, but redis'
-logs at least work well, and doing a `curl localhost:8000` results in the
-response from the `markov` server.
-
-At this point our processes are composed using circus, let's now bundle it all
-into a single static binary!
-
-## Step 3: nix-bundle
-
-The next step is to run [nix-bundle][nix-bundle] on the entrypoint expression,
-and nix-bundle will compile all dependencies (including markov, redis, and
-circus) into a single archive file, and make that file executable. When the
-archive is executed it will run our entrypoint script directly.
-
-Getting nix-bundle is very easy, just use nix-shell!
-
-```
-nix-shell -p nix-bundle
-```
-
-This will open a shell where the `nix-bundle` binary is available on your path.
-From there just run the following to construct the binary (this assumes that the
-nix code described so far is stored in `markov.nix`, the full source of which
-will be linked to at the end of this post):
-
-```
-nix-bundle '((import ./markov.nix) {}).entrypoint' '/bin/markov'
-```
-
-The resulting binary is called `markov`, and is 89MB. The size is a bit jarring,
-considering the simplicity of the functionality, but it could probably be
-trimmed by using a different process manager than circus (which requires
-bundling an entire python runtime into the binary).
-
-Running the binary directly as `./markov` produces the same result as when we
-ran the entrypoint script earlier. Success! We have bundled multiple existing
-processes into a single, opaque, static binary. Installation of this binary is
-now as easy as copying it to any linux machine and running it.
-
-## Bonus Step: nix'ing nix-bundle
-
-Installing and running [nix-bundle][nix-bundle] manually is _fine_, but it'd be even better if
-that was defined as part of our nix setup as well. That way any new person
-wouldn't have to worry about that step, and still get the same deterministic
-output from the build.
-
-Unfortunately, we can't actually run `nix-bundle` from within a nix build
-derivation, as it requires access to the nix store and that can't be done (or at
-least I'm not on that level yet). So instead we'll have to settle for defining
-the `nix-bundle` binary in nix and then using a `Makefile` to call it.
-
-Defining a `nix-bundle` expression is easy enough:
-
-```
- nixBundleSrc = pkgs.fetchFromGitHub {
- owner = "matthewbauer";
- repo = "nix-bundle";
- rev = "8e396533ef8f3e8a769037476824d668409b4a74";
- sha256 = "1lrq0990p07av42xz203w64abv2rz9xd8jrzxyvzzwj7vjj7qwyw";
- };
-
- nixBundle = (import "${nixBundleSrc}/release.nix") {
- nixpkgs' = pkgs;
- };
-```
-
-Then the Makefile:
-
-```make
-bundle:
- nix-build markov.nix -A nixBundle
- ./result/bin/nix-bundle '((import ./markov.nix) {}).entrypoint' '/bin/markov'
-```
-
-Now all a developer needs to rebuild the project is to do `make` within the
-directory, while also having nix set up. The result will be a deterministically
-built, static binary, encompassing multiple processes which will all work
-together behind the scenes. This static binary can be copied to any linux
-machine and run there without any further installation steps.
-
-How neat is that!
-
-The final source files used for this project can be found below:
-
-* [markov.nix](/assets/markov/markov.nix.html)
-* [Makefile](/assets/markov/Makefile.html)
-
-[nix]: https://nixos.org/manual/nix/stable/
-[nix-bundle]: https://github.com/matthewbauer/nix-bundle
-[docker-compose]: https://docs.docker.com/compose/
-[appimage]: https://appimage.org/
-[circus]: https://circus.readthedocs.io/en/latest/
-[markov]: https://github.com/mediocregopher/markov
-[markov-chain]: https://en.wikipedia.org/wiki/Markov_chain
-[golang]: https://golang.org/
-[redis]: https://redis.io/