nixos modules for convenient deployment of cloud resources

Compare changes

Choose any two refs to compare.

Changed files
+220 -96
firewall
+2
.gitignore
···
+
result
+
.hetzner
+14
README.md
···
+
these are set of nixos modules for more convenient deployment of cloud resources for various providers. only implemented module is for firewalls right now, supporting hetzner.
+
+
## usage
+
+
if using flakes, put `nixosModules.<resource>` and `nixosModules.<resource>-<provider>`
+
in your NixOS configuration. for example, `nixosModules.firewall` and
+
`nixosModules.firewall-hetzner`. see `nix flake show` for all available modules.
+
+
if not using flakes, you can import `<resource>/` and `<resource>/<provider>`.
+
+
then, you can either use each module's individual `mkApp` config option to
+
generate an app and run it, or you can call `makeApps`:
+
- for flakes use the flake output `makeApps` and `makeApps {inherit pkgs self;}`. you can assign the output of this to your `outputs.apps` as it generates flake apps.
+
- for non-flake use `import ./makeApps.nix {inherit pkgs nixosSystem;}`, this will return an attribute set with a `run` key which is the generated app.
+9
firewall/hetzner/app.nix
···
+
{pkgs, lib ? pkgs.lib, taggedPorts, id}: let
+
l = lib // (import ./rules.nix {inherit lib;});
+
firewallRules =
+
builtins.toFile
+
"hetzner-firewall-${toString id}-rules.json"
+
(builtins.toJSON (l.mkFirewallRuleset taggedPorts));
+
in pkgs.writers.writeNu "apply-hetzner-firewall-${toString id}" ''
+
nu ${./app.nu} ${toString id} ${firewallRules}
+
''
+40
firewall/hetzner/app.nu
···
+
use std/log
+
+
def main [firewallId: number, rulesFile: path, --auth-token (-t): string] {
+
let auth_token: string = if $auth_token == null { $env.HETZNER_API_TOKEN? } else { $auth_token }
+
let authHeader: list<string> = ["authorization" $"Bearer ($auth_token)"]
+
+
def makeApiUrl [path: string] {
+
return $"https://api.hetzner.cloud/v1($path)"
+
}
+
def post [path: string] {
+
$in | http post -e --full -H $authHeader --content-type application/json (makeApiUrl $path)
+
}
+
def get [path: string] {
+
http get -e --full -H $authHeader (makeApiUrl $path)
+
}
+
+
# first fetch firewall to see if it even exists
+
let resp = get $"/firewalls/($firewallId)"
+
if $resp.status == 404 {
+
log error $"provided firewall \(id ($firewallId)\) does not exist"
+
exit 1
+
} else if $resp.status != 200 {
+
log error $"could not get firewall \(id ($firewallId)\):\n($resp.body.error | to text -n)"
+
exit 1
+
}
+
let firewall = $resp.body | get firewall
+
+
# backup firewall
+
let backupPath = $".hetzner/($firewallId).json"
+
mkdir .hetzner; $firewall | to json | save -f $backupPath
+
log info $"backing up firewall ($firewallId) to ($backupPath)"
+
+
# apply rules
+
let resp = open $rulesFile | post $"/firewalls/($firewallId)/actions/set_rules"
+
if $resp.status != 201 {
+
log error $"could not apply firewall \(id ($firewallId)\):\n($resp.body.error | to text -n)"
+
exit 2
+
}
+
log info $"applied firewall ($firewallId)"
+
}
+32
firewall/hetzner/default.nix
···
+
{lib, config, ...}: let
+
l = lib;
+
t = l.types;
+
taggedPorts = config.networking.firewall.public;
+
cfg = config.providers.hetzner.firewall;
+
in {
+
options = {
+
providers.hetzner.firewall = {
+
enable = l.mkEnableOption "hetzner firewall";
+
id = l.mkOption {
+
type = t.ints.unsigned;
+
description = "The ID of the firewall to update.";
+
};
+
mkApp = l.mkOption {
+
type = t.functionTo t.package;
+
readOnly = true;
+
description = ''
+
Function that generates a script for this provider, pass it an instance of nixpkgs and run to apply the configuration.
+
+
For this app to work, you need to set the `HETZNER_API_TOKEN` environment variable to a valid API token from Hetzner.
+
'';
+
};
+
};
+
};
+
+
config = l.mkIf cfg.enable {
+
providers.hetzner.firewall.mkApp = pkgs: import ./app.nix {
+
inherit pkgs lib taggedPorts;
+
inherit (cfg) id;
+
};
+
};
+
}
+29
firewall/hetzner/rules.nix
···
+
{lib}: let
+
l = lib;
+
mkRule = proto: tag: port: {
+
description = tag;
+
direction = "in";
+
protocol = proto;
+
port =
+
if l.isAttrs port
+
then l.concatMapStringsSep "-" toString [port.from port.to]
+
else toString port;
+
source_ips = ["0.0.0.0/0" "::/0"];
+
};
+
in rec {
+
mkTcpRule = mkRule "tcp";
+
mkUdpRule = mkRule "udp";
+
# taggedPorts: attrset of {allowedTCPPorts, allowedTCPPortRanges, ...}
+
mkFirewallRuleset = taggedPorts: {
+
rules = l.flatten (
+
l.mapAttrsToList
+
(tag: ports: [
+
(l.map (mkTcpRule tag) (ports.allowedTCPPorts or []))
+
(l.map (mkTcpRule tag) (ports.allowedTCPPortRanges or []))
+
(l.map (mkUdpRule tag) (ports.allowedUDPPorts or []))
+
(l.map (mkUdpRule tag) (ports.allowedUDPPortRanges or []))
+
])
+
taggedPorts
+
);
+
};
+
}
-37
firewall/provider/hetzner/app.nu
···
-
use std/log
-
-
let authHeader = ["authorization" $"Bearer ($env.HETZNER_API_TOKEN)"]
-
-
def makeApiUrl [path: string] {
-
return $"https://api.hetzner.cloud/v1($path)"
-
}
-
def post [path: string] {
-
let resp = $in | http post -e --full -H authHeader --content-type application/json (makeApiUrl path)
-
$resp.body = $resp.body | from json
-
$resp
-
}
-
def get [path: string] {
-
let resp = http get -e --full -H authHeader (makeApiUrl path)
-
$resp.body = $resp.body | from json
-
$resp
-
}
-
-
# first fetch firewall to see if it even exists
-
let resp = get $"/firewalls/($firewallId)"
-
if $resp.status == 404 {
-
log error $"provided firewall \(id ($firewallId)\) does not exist"
-
exit 1
-
}
-
let firewall = $resp.body | get firewall
-
-
# backup firewall
-
let backupPath = $".hetzner/($firewallId).json"
-
mkdir .hetzner; $firewall | to json | save $backupPath
-
log info $"backing up firewall ($firewallId) to ($backupPath)"
-
-
# apply rules
-
let resp = open $rulesFile | from json | post $"/firewalls/($firewallId)/actions/set_rules"
-
if $resp.status != 201 {
-
log error $"could not apply firewall \(id ($firewallId)\)"
-
}
-
log info $"applied firewall ($firewallId)"
-56
firewall/provider/hetzner/default.nix
···
-
{pkgs, lib, config, options, ...}: let
-
l = lib;
-
t = l.types;
-
taggedPorts = config.networking.firewall.public;
-
cfg = config.providers.hetzner;
-
in {
-
options = {
-
providers.hetzner.firewall = {
-
id = l.mkOption {
-
type = t.ints.unsigned;
-
description = "The ID of the firewall to update.";
-
};
-
app = l.mkOption {
-
type = t.package;
-
readOnly = true;
-
description = ''
-
The generated app for this provider, run it to apply the configuration.
-
-
For this to work, you need to set the `HETZNER_API_TOKEN` environment variable to a valid API token from Hetzner.
-
'';
-
};
-
};
-
};
-
-
config = let
-
mkRule = proto: tag: port: {
-
description = tag;
-
direction = "in";
-
protocol = proto;
-
port =
-
if l.isAttrs port
-
then l.concatMapStringsSep "-" toString [port.from port.to]
-
else toString port;
-
};
-
mkTcpRule = mkRule "tcp";
-
mkUdpRule = mkRule "udp";
-
firewallRules = pkgs.writers.writeJSON "hetzner-firewall-${toString cfg.id}-rules.json" {
-
rules = l.flatten (
-
l.mapAttrsToList
-
(tag: ports: [
-
(l.map (mkTcpRule tag) ports.allowedTCPPorts)
-
(l.map (mkTcpRule tag) ports.allowedTCPPortRanges)
-
(l.map (mkUdpRule tag) ports.allowedUDPPorts)
-
(l.map (mkUdpRule tag) ports.allowedUDPPortRanges)
-
])
-
taggedPorts
-
);
-
};
-
in {
-
providers.hetzner.firewall.app = pkgs.writers.writeNu "apply-hetzner" ''
-
let firewallId = ${toString cfg.id}
-
let rulesFile = ${firewallRules}
-
${l.fileContents ./app.nu}
-
'';
-
};
-
}
+37 -3
flake.nix
···
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
-
outputs = inp: {
+
outputs = inp: let
+
l = inp.nixpkgs.lib;
+
pkgsInstances =
+
l.genAttrs
+
["x86_64-linux"]
+
(s: inp.nixpkgs.legacyPackages.${s});
+
in {
nixosModules = {
-
firewall = ./firewall/default.nix;
-
firewall-hetzner = ./firewall/provider/hetzner/default.nix;
+
firewall = ./firewall;
+
firewall-hetzner = ./firewall/hetzner;
};
+
checks =
+
l.mapAttrs
+
(_: pkgs: let
+
testSystem = l.nixosSystem {
+
system = pkgs.system;
+
modules = l.attrValues inp.self.nixosModules;
+
};
+
in {
+
firewall-hetzner-app = import ./firewall/hetzner/app.nix {
+
inherit pkgs;
+
taggedPorts = {
+
http.allowedTCPPorts = [80 443];
+
ssh.allowedTCPPorts = [22];
+
"bla bla" = {
+
allowedUDPPortRanges = [{from = 1332; to = 8891;}];
+
allowedTCPPorts = [101];
+
allowedUDPPorts = [102];
+
};
+
};
+
id = 1;
+
};
+
test-system-app =
+
(inp.self.makeApps {
+
inherit pkgs; nixosSystem = testSystem;
+
}).run;
+
})
+
pkgsInstances;
+
makeApps = import ./makeApps.nix;
};
}
+57
makeApps.nix
···
+
{pkgs, lib ? pkgs.lib, self ? null, nixosSystem ? null}: let
+
l = lib;
+
mkProviderApp = provider:
+
l.concatStringsSep "\n" (l.flatten (
+
l.mapAttrsToList
+
(
+
name: module:
+
if module.enable
+
then ''
+
log info "deploying ${name} resource(s)..."
+
nu ${module.mkApp pkgs}
+
''
+
else []
+
)
+
provider
+
));
+
mkApp = {config, ...}: pkgs.writers.writeNu "deploy-resources" ''
+
use std/log
+
${
+
l.concatStringsSep "\n\n"
+
(
+
l.mapAttrsToList
+
(
+
name: provider: ''
+
log info "deploying resources for ${name}..."
+
${mkProviderApp provider}
+
''
+
)
+
config.providers
+
)
+
}
+
'';
+
in
+
if self != null
+
then
+
l.mergeAttrsList (
+
l.mapAttrsToList
+
(
+
hostname: host:
+
if l.hasAttr "providers" host.config
+
then {
+
"deploy-${hostname}-resources" = {
+
type = "app";
+
program = toString (mkApp host);
+
};
+
}
+
else {}
+
)
+
self.nixosConfigurations
+
)
+
else if nixosSystem != null
+
then
+
{
+
run = mkApp nixosSystem;
+
}
+
else
+
throw "nixos-cloud-resources: neither 'self' or 'nixosSystem' was provided, aborting"