tech-ingolf-wagner-de/content/nixos/nix-instantiate.md

621 lines
14 KiB
Markdown
Raw Normal View History

2018-12-05 22:17:28 +01:00
---
title: "nix-instantiate"
2018-12-28 01:06:06 +01:00
date: 2018-12-27T19:09:36+02:00
2018-12-22 22:20:28 +01:00
draft: false
2018-12-05 22:17:28 +01:00
tags:
- NixOS
2018-12-28 01:06:06 +01:00
- TerraNix
2018-12-05 22:17:28 +01:00
- Ansible
- Terraform
summary: >
2021-07-20 18:08:41 +02:00
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.
2018-12-05 22:17:28 +01:00
---
I like NixOS and the way modules work.
2018-12-27 14:50:29 +01:00
I miss them when I do tasks in other languages
that have less power than NixOS, for example
2018-12-05 22:17:28 +01:00
[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.
2018-12-27 14:50:29 +01:00
The go-to tool for that job is
2018-12-05 22:17:28 +01:00
[nix-instantiate](https://nixos.org/nix/manual/#sec-nix-instantiate)
which every NixOS has installed (yeye!).
2018-12-22 22:20:28 +01:00
## Small overview
I will show you how easy it is, with a few lines of nix,
2018-12-22 22:20:28 +01:00
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
2018-12-27 14:50:29 +01:00
[terranix project](https://github.com/mrVanDalo/terranix).
2018-12-22 22:20:28 +01:00
2018-12-05 22:17:28 +01:00
## First tests
Lets look what `nix-instantiate` does.
2018-12-27 14:50:29 +01:00
We create a file `test1.nix`:
2018-12-05 22:17:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-05 22:17:28 +01:00
# file test1.nix
rec {
2018-12-22 22:20:28 +01:00
i = "like Nix";
you = i;
2018-12-05 22:17:28 +01:00
}
```
2018-12-27 14:50:29 +01:00
and than we run `nix-instantiate` to render JSON:
2018-12-05 22:17:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-05 22:17:28 +01:00
$> nix-instantiate --eval --json --strict test1.nix | jq
{
2018-12-22 22:20:28 +01:00
"i": "like Nix",
"you": "like Nix"
2018-12-05 22:17:28 +01:00
}
```
2018-12-27 14:50:29 +01:00
Nice! This is expected because it is an example from
2018-12-05 22:17:28 +01:00
[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`!
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
# 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
```
2018-12-27 14:50:29 +01:00
When running:
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
nix-instantiate --eval --strict --json test2.nix --show-trace | jq
```
2018-12-27 14:50:29 +01:00
We get the following JSON:
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{
"_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.
2018-12-27 14:50:29 +01:00
So let's remove it with a sanitization function, and move the content path
to a different file called `config.nix`.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
# 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
2018-12-28 01:06:06 +01:00
# whitelist the resource attribute
2018-12-28 00:31:50 +01:00
{ resource = (sanitize result.config).resource ; }
2018-12-22 22:20:28 +01:00
```
2018-12-27 14:50:29 +01:00
In `config.nix` we can now focus on the configuration content. And we write it
2018-12-22 22:20:28 +01:00
just like we would write a NixOS module.
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
# 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
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
nix-instantiate --eval --strict --json test3.nix --show-trace | jq
```
2018-12-27 14:50:29 +01:00
looks like the result we want to have:
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{
"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.
2018-12-27 14:50:29 +01:00
## A Simple Example
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
Let's make an example so a non-NixOS-veteran can see
how to start using this modules system.
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
### `hcloud.nix`
2018-12-22 22:20:28 +01:00
The following file is a module that let's us create
2018-12-27 14:50:29 +01:00
resource entries to create an
2018-12-22 22:20:28 +01:00
[hcloud server](https://www.terraform.io/docs/providers/hcloud/r/server.html).
But it has one parameter `additionalFileSize`
2018-12-27 14:50:29 +01:00
which will automatically add an `hcloud_volume` and an `hcloud_volume_attachment`.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
# 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 )
];
}
```
2018-12-27 14:50:29 +01:00
### `config.nix` and Output
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
#### Without `additionalFileSize`
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
Let's look at the different `config.nix` results.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{
imports = [
./core.nix # resource definition
./hcloud.nix # the hcloud_server module
];
# define a hcloud_server
hcloud.server = {
"test" = {
image = "ubuntu";
server_type = "cx11";
};
};
}
```
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
$> 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.
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
#### With `additionalFileSize`
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
Let's add some `additionalFileSize`.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{
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;
};
};
}
```
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
$> 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}"
}
}
}
}
```
2018-12-27 14:50:29 +01:00
Whoa, a lot of other resources joined the party.
Additionally, the `additionalFileSize` parameter is
2018-12-22 22:20:28 +01:00
properly removed from `resource.hcloud_server.test`.
2021-07-20 18:08:41 +02:00
You could also create this very simple example in `HCL` by using
2018-12-22 22:20:28 +01:00
`variables`, `locals` and `count`.
2018-12-27 14:50:29 +01:00
By doing that, you already reached the limits of
2018-12-22 22:20:28 +01:00
`HCL` but in Nix this is a very simple example.
2018-12-27 14:50:29 +01:00
## A More Complex Example
2018-12-22 22:20:28 +01:00
2018-12-28 01:06:06 +01:00
Let's create something you wouldn't be able to do in `HCL`
2018-12-22 22:20:28 +01:00
anymore.
2018-12-27 14:50:29 +01:00
Imagine you have an inner circle of admins,
2018-12-22 22:20:28 +01:00
which need access to all machines created.
So when a machine is created we also add
all admin keys.
2018-12-28 01:06:06 +01:00
Let's look at the `config.nix` first.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{
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";
};
};
}
```
2018-12-28 01:06:06 +01:00
We want to define the admin keys "globally" without setting them
for every machine explicitly.
2018-12-27 14:50:29 +01:00
### `admins.nix`
2018-12-22 22:20:28 +01:00
2018-12-28 01:06:06 +01:00
The `admins` module will not create any `resource` directly.
Instead it defines options which can be set and used by
other modules.
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
# 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
2018-12-28 01:06:06 +01:00
The `admins` options are used in the `hcloud.nix` file, and of course every
other module that create servers.
2018-12-22 22:20:28 +01:00
2018-12-28 01:06:06 +01:00
They are accessed via `config.admins`
and depending on their content,
we create `hcloud_ssh_keys` and add them to the servers.
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
{ 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)
];
}
```
2018-12-28 01:06:06 +01:00
The `hcloud.nix` starts to get big now, but it is very similar to the version
2018-12-22 22:20:28 +01:00
from the privious section.
2018-12-28 01:06:06 +01:00
Focus on the last `let` section and on `mkMerge`.
Look closely at the end of the `serverResource` definition.
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
### Output
2018-12-22 22:20:28 +01:00
2018-12-27 14:50:29 +01:00
Let's look at the resulting JSON:
2018-12-22 22:20:28 +01:00
2021-08-23 20:41:21 +02:00
```
2018-12-22 22:20:28 +01:00
$> 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"
}
}
}
}
```
2018-12-27 14:50:29 +01:00
Nice! All 3 keys will be created by `hcloud_ssh_key` and they all get wired
2018-12-22 22:20:28 +01:00
to the new `hcloud_server`.
This should give you a feeling how you can maintain your
2018-12-27 14:50:29 +01:00
JSON/YAML-configured tools, with `nix-instantiate` and the NixOS module system.
2018-12-22 22:20:28 +01:00
Happy Hacking!
## Thanks
Thanks to `tv` for his introduction to `nix-instantiate`.
2018-12-28 01:06:06 +01:00
2018-12-27 14:50:29 +01:00
Thanks to `lassulus` and `kmein` for polishing this article.