tech-ingolf-wagner-de/content/nixos/nix-instantiate.md
2021-08-24 19:11:56 +02:00

621 lines
14 KiB
Markdown

---
title: "nix-instantiate"
date: 2018-12-27T19:09:36+02:00
draft: false
tags:
- NixOS
- TerraNix
- Ansible
- Terraform
summary: >
I like NixOS and the way modules work. I miss them when I do tasks in other languages that have less power than NixOS, for example Ansible and Terraform.
Luckily all these tools can be configured via JSON, and Nix can easily create JSON.
---
I like NixOS and the way modules work.
I miss them when I do tasks in other languages
that have less power than NixOS, for example
[Ansible](https://www.ansible.com) and [Terraform](https://www.terraform.io).
Luckily all these tools can be configured via JSON,
and Nix can easily create JSON.
The go-to tool for that job is
[nix-instantiate](https://nixos.org/nix/manual/#sec-nix-instantiate)
which every NixOS has installed (yeye!).
## Small overview
I will show you how easy it is, with a few lines of nix,
to create a JSON renderer for terraform configuration files.
But this is only done in a sketchy way,
to inspire you to create your own setup for
different tools that use JSON.
If you are interessted in a full (or almost full)
terraform JSON renderer have a look at my
[terranix project](https://github.com/mrVanDalo/terranix).
## First tests
Lets look what `nix-instantiate` does.
We create a file `test1.nix`:
```
# file test1.nix
rec {
i = "like Nix";
you = i;
}
```
and than we run `nix-instantiate` to render JSON:
```
$> nix-instantiate --eval --json --strict test1.nix | jq
{
"i": "like Nix",
"you": "like Nix"
}
```
Nice! This is expected because it is an example from
[the documentation](https://nixos.org/nix/manual/#sec-nix-instantiate).
## Modules for the win
Modules are one of the things that make NixOS really awesome.
So lets us them in combination with `nix-instantiate`!
```
# file test2.nix
let
pkgs = import <nixpkgs> {};
result =
with pkgs;
with lib;
evalModules {
modules = [
# option definition
{
options = {
resource = mkOption {
type = with types; attrsOf attrs;
default = {};
};
};
}
# config definition
{
resource."random_pet" = {
"house_pet".length = 10;
"neighbours_pet".length = 10;
};
}
];
};
in
result.config
```
When running:
```
nix-instantiate --eval --strict --json test2.nix --show-trace | jq
```
We get the following JSON:
```
{
"_module": {
"args": {},
"check": true
},
"resource": {
"random_pet": {
"house_pet": {
"length": 10
},
"neighbours_pet": {
"length": 10
}
}
}
}
```
This is almost what we want to see. the `_module` value is not needed.
So let's remove it with a sanitization function, and move the content path
to a different file called `config.nix`.
```
# file test3.nix
let
pkgs = import <nixpkgs> {};
sanitize =
with pkgs;
configuration:
builtins.getAttr (builtins.typeOf configuration) {
bool = configuration;
int = configuration;
string = configuration;
list = map sanitize configuration;
set = lib.mapAttrs
(lib.const sanitize)
(lib.filterAttrs (name: value: name != "_module" && value != null) configuration);
};
result =
with pkgs;
with lib;
evalModules {
modules = [ { imports = [ ./config.nix ]; } ];
};
in
# whitelist the resource attribute
{ resource = (sanitize result.config).resource ; }
```
In `config.nix` we can now focus on the configuration content. And we write it
just like we would write a NixOS module.
```
# config.nix
{ config, lib, ... }:
with lib;
{
options = {
resource = mkOption {
type = with types; attrsOf attrs;
default = {};
};
};
config = {
resource."random_pet" = {
"house_pet".length = 10;
"neighbours_pet".length = 10;
};
};
}
```
The result of the now well known command
```
nix-instantiate --eval --strict --json test3.nix --show-trace | jq
```
looks like the result we want to have:
```
{
"resource": {
"random_pet": {
"house_pet": {
"length": 10
},
"neighbours_pet": {
"length": 10
}
}
}
}
```
Now we have the full power of the NixOS module system to generate
JSON.
We can write modules to hide complexity and create very well readable
`terraform` or `ansible` setups without the need of their
strange tooling which is not capable of mapping, filtering
or hiding complexity.
## A Simple Example
Let's make an example so a non-NixOS-veteran can see
how to start using this modules system.
### `hcloud.nix`
The following file is a module that let's us create
resource entries to create an
[hcloud server](https://www.terraform.io/docs/providers/hcloud/r/server.html).
But it has one parameter `additionalFileSize`
which will automatically add an `hcloud_volume` and an `hcloud_volume_attachment`.
```
# hcloud.nix
{ config, lib, ... }:
with lib;
let
cfg = config.hcloud.server;
in {
options.hcloud.server = mkOption {
default = {};
type = with types; attrsOf (submodule ( { name, ... }: {
options = {
# mandatories : because no default is set
server_type = mkOption {
type = with types; enum ["cx11" "cx21" "cx31" "cx41" "cx51"];
};
image = mkOption {
type = with types; string;
example = "ubuntu";
description = ''
image to install
'';
};
# optionals
additionalFileSize = mkOption {
type = with types; nullOr ints.positive;
default = null;
description = ''
extra volume (in GB) that should be added.
'';
};
};
}));
};
config =
let
serverResources = {
resource.hcloud_server =
mapAttrs (name: configuration: {
inherit (configuration) server_type image;
name = name;
} ) cfg;
};
additionals = filterAttrs (_: configuration: configuration.additionalFileSize != null) cfg;
additionVolumesResources = {
resource."hcloud_volume" = mapAttrs' (name: configuration:
nameValuePair "${name}" {
name = "${name}_volume";
size = configuration.additionalFileSize;
}
) additionals;
};
additionVolumesResourcesAttatchments = {
resource."hcloud_volume_attachment" = mapAttrs' (name: configuration:
nameValuePair "${name}_volume_attachment" {
volume_id = "\${hcloud_volume.${name}.id}";
server_id = "\${hcloud_server.${name}.id}";
automount = true;
}
) additionals;
};
in
mkMerge [
serverResources
( mkIf ( length ( attrNames additionals ) > 0 )
additionVolumesResources )
( mkIf ( length ( attrNames additionals ) > 0 )
additionVolumesResourcesAttatchments )
];
}
```
### `config.nix` and Output
#### Without `additionalFileSize`
Let's look at the different `config.nix` results.
```
{
imports = [
./core.nix # resource definition
./hcloud.nix # the hcloud_server module
];
# define a hcloud_server
hcloud.server = {
"test" = {
image = "ubuntu";
server_type = "cx11";
};
};
}
```
```
$> nix-instantiate --eval --strict --json test3.nix --show-trace | jq
{
"resource": {
"hcloud_server": {
"test": {
"image": "ubuntu",
"name": "test",
"server_type": "cx11"
}
}
}
}
```
The output is like we expected it to be.
#### With `additionalFileSize`
Let's add some `additionalFileSize`.
```
{
imports = [
./core.nix # resource definition
./hcloud.nix # the hcloud_server module
];
# define a hcloud_server
hcloud.server = {
"test" = {
image = "ubuntu";
server_type = "cx11";
additionalFileSize = 100;
};
};
}
```
```
$> nix-instantiate --eval --strict --json test3.nix --show-trace | jq
{
"resource": {
"hcloud_server": {
"test": {
"image": "ubuntu",
"name": "test",
"server_type": "cx11"
}
},
"hcloud_volume": {
"test": {
"name": "test_volume",
"size": 100
}
},
"hcloud_volume_attachment": {
"test_volume_attachment": {
"automount": true,
"server_id": "${hcloud_server.test.id}",
"volume_id": "${hcloud_volume.test.id}"
}
}
}
}
```
Whoa, a lot of other resources joined the party.
Additionally, the `additionalFileSize` parameter is
properly removed from `resource.hcloud_server.test`.
You could also create this very simple example in `HCL` by using
`variables`, `locals` and `count`.
By doing that, you already reached the limits of
`HCL` but in Nix this is a very simple example.
## A More Complex Example
Let's create something you wouldn't be able to do in `HCL`
anymore.
Imagine you have an inner circle of admins,
which need access to all machines created.
So when a machine is created we also add
all admin keys.
Let's look at the `config.nix` first.
```
{
imports = [
./core.nix
./hcloud.nix
./admins.nix
];
# all mighty admins
admins = {
palo.ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ palo@someMachine";
tv.ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ tv@someMachine";
lass.ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ lass@someMachine";
};
hcloud.server = {
"test" = {
image = "ubuntu";
server_type = "cx11";
};
};
}
```
We want to define the admin keys "globally" without setting them
for every machine explicitly.
### `admins.nix`
The `admins` module will not create any `resource` directly.
Instead it defines options which can be set and used by
other modules.
```
# admins.nix
{ lib, ... }:
with lib;
{
options.admins = mkOption {
default = {};
type = with types; attrsOf ( submodule ( { name, ... }: {
options = {
ssh_key = mkOption {
type = with types; string;
description = ''
public key of admin.
'';
};
};
}));
};
}
```
### hcloud.nix
The `admins` options are used in the `hcloud.nix` file, and of course every
other module that create servers.
They are accessed via `config.admins`
and depending on their content,
we create `hcloud_ssh_keys` and add them to the servers.
```
{ config, lib, ... }:
with lib;
let
cfg = config.hcloud.server;
in {
options.hcloud.server = mkOption {
default = {};
type = with types; attrsOf (submodule ( { name, ... }: {
options = {
# mandatories : because no default is set
server_type = mkOption {
type = with types; enum ["cx11" "cx21" "cx31" "cx41" "cx51"];
};
image = mkOption {
type = with types; string;
example = "ubuntu";
description = ''
image to install
'';
};
# optionals
additionalFileSize = mkOption {
type = with types; nullOr ints.positive;
default = null;
description = ''
extra volume (in GB) that should be added.
'';
};
};
}));
};
config =
let
serverResources = {
resource.hcloud_server =
mapAttrs (name: configuration: {
inherit (configuration) server_type image;
name = name;
# we add the ssh key ids, if admins exist
} // (optionalAttrs (length adminSshKeyIds > 0) { ssh_keys = adminSshKeyIds; })
) cfg;
};
additionals = filterAttrs (_: configuration: configuration.additionalFileSize != null) cfg;
additionVolumesResources = {
resource."hcloud_volume" = mapAttrs' (name: configuration:
nameValuePair "${name}" {
name = "${name}_volume";
size = configuration.additionalFileSize;
}
) additionals;
};
additionVolumesResourcesAttatchments = {
resource."hcloud_volume_attachment" = mapAttrs' (name: configuration:
nameValuePair "${name}_volume_attachment" {
volume_id = "\${hcloud_volume.${name}.id}";
server_id = "\${hcloud_server.${name}.id}";
automount = true;
}
) additionals;
};
adminSshKeyIds = map (name: "\${hcloud_ssh_key.${name}.id}") (attrNames config.admins);
adminSshKeys = {
resource."hcloud_ssh_key" = mapAttrs (name: configuration: {
name = name;
public_key = configuration.ssh_key;
}) config.admins; };
in
mkMerge [
serverResources
( mkIf ( length ( attrNames additionals ) > 0 )
additionVolumesResources )
( mkIf ( length ( attrNames additionals ) > 0 )
additionVolumesResourcesAttatchments )
# we create hcloud_ssh_keys, if admins exist
( mkIf ( length ( attrNames config.admins ) > 0 )
adminSshKeys)
];
}
```
The `hcloud.nix` starts to get big now, but it is very similar to the version
from the privious section.
Focus on the last `let` section and on `mkMerge`.
Look closely at the end of the `serverResource` definition.
### Output
Let's look at the resulting JSON:
```
$> nix-instantiate --eval --strict --json test3.nix --show-trace | jq
{
"resource": {
"hcloud_server": {
"test": {
"image": "ubuntu",
"name": "test",
"server_type": "cx11",
"ssh_keys": [
"${hcloud_ssh_key.lass.id}",
"${hcloud_ssh_key.palo.id}",
"${hcloud_ssh_key.tv.id}"
]
}
},
"hcloud_ssh_key": {
"lass": {
"name": "lass",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ lass@someMachine"
},
"palo": {
"name": "palo",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ palo@someMachine"
},
"tv": {
"name": "tv",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAA......hKIWndLJ tv@someMachine"
}
}
}
}
```
Nice! All 3 keys will be created by `hcloud_ssh_key` and they all get wired
to the new `hcloud_server`.
This should give you a feeling how you can maintain your
JSON/YAML-configured tools, with `nix-instantiate` and the NixOS module system.
Happy Hacking!
## Thanks
Thanks to `tv` for his introduction to `nix-instantiate`.
Thanks to `lassulus` and `kmein` for polishing this article.