diff options
Diffstat (limited to 'ops/terraform')
-rw-r--r-- | ops/terraform/README.md | 5 | ||||
-rw-r--r-- | ops/terraform/deploy-nixos/README.md | 50 | ||||
-rw-r--r-- | ops/terraform/deploy-nixos/main.tf | 113 | ||||
-rwxr-xr-x | ops/terraform/deploy-nixos/nix-eval.sh | 47 | ||||
-rwxr-xr-x | ops/terraform/deploy-nixos/nixos-copy.sh | 32 |
5 files changed, 247 insertions, 0 deletions
diff --git a/ops/terraform/README.md b/ops/terraform/README.md new file mode 100644 index 0000000000..9ff6c23d47 --- /dev/null +++ b/ops/terraform/README.md @@ -0,0 +1,5 @@ +//ops/terraform +=============== + +This folder contains Terraform modules and other related +Terraform-tooling by TVL. diff --git a/ops/terraform/deploy-nixos/README.md b/ops/terraform/deploy-nixos/README.md new file mode 100644 index 0000000000..fd0bd1b442 --- /dev/null +++ b/ops/terraform/deploy-nixos/README.md @@ -0,0 +1,50 @@ +<!-- +SPDX-FileCopyrightText: 2023 The TVL Authors + +SPDX-License-Identifier: MIT +--> + +deploy-nixos +============ + +This is a Terraform module to deploy a NixOS system closure to a +remote machine. + +The system closure must be accessible by Nix-importing the repository +root and building a specific attribute +(e.g. `nix-build -A ops.machines.machine-name`). + +The target machine must be accessible normally over SSH, and an SSH +key must be used for access. + +Notably this module separates the evaluation of the system closure from building +and deploying it, and uses the closure's derivation hash to determine whether a +deploy is necessary. + +## Usage example: + +```terraform +module "deploy_somehost" { + source = "git::https://code.tvl.fyi/depot.git:/ops/terraform/deploy-nixos.git" + attrpath = "ops.nixos.somehost" + target_host = "somehost.tvl.su" + target_user = "someone" + target_user_ssh_key = tls_private_key.somehost.private_key_pem +} +``` + +## Future work + +Several things can be improved about this module, for example: + +* The repository root (relative to which the attribute path is evaluated) could + be made configurable. + +* The remote system closure could be discovered to restore remote system state + after manual deploys on the target (i.e. "stomping" of changes). + +More ideas and contributions are, of course, welcome. + +## Acknowledgements + +Development of this module was sponsored by [Resoptima](https://resoptima.com/). diff --git a/ops/terraform/deploy-nixos/main.tf b/ops/terraform/deploy-nixos/main.tf new file mode 100644 index 0000000000..50278b248e --- /dev/null +++ b/ops/terraform/deploy-nixos/main.tf @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2023 The TVL Authors +# +# SPDX-License-Identifier: MIT + +# This module deploys a NixOS host by building a system closure +# located at the specified attribute in the current repository. +# +# The closure's derivation path is persisted in the Terraform state to +# determine after Nix evaluation whether the system closure has +# changed and needs to be built/deployed. +# +# The system configuration is then built (or substituted) on the +# machine that runs `terraform apply`, then copied and activated on +# the target machine using `nix-copy-closure`. + +variable "attrpath" { + description = "attribute set path pointing to the NixOS system closure" + type = string +} + +variable "target_host" { + description = "address (IP or hostname) at which the target is reachable" + type = string +} + +variable "entrypoint" { + description = <<EOT + Path to a .nix file (or directory containing `default.nix` file) + that provides the attrset specified in `closure`. + If unset, asks git for the root of the repository. + EOT + type = string + default = "" +} + +variable "target_user" { + description = "username on the target machine" + type = string +} + +variable "target_user_ssh_key" { + description = "SSH key to use for connecting to the target" + type = string + default = "" + sensitive = true +} + +variable "triggers" { + type = map(string) + description = "Triggers for deploy" + default = {} +} + +# Fetch the derivation hash for the NixOS system. +data "external" "nixos_system" { + program = ["${path.module}/nix-eval.sh"] + + query = { + attrpath = var.attrpath + entrypoint = var.entrypoint + } +} + +# Deploy the NixOS configuration if anything changed. +resource "null_resource" "nixos_deploy" { + connection { + type = "ssh" + host = var.target_host + user = var.target_user + private_key = var.target_user_ssh_key + } + + # 1. Wait for SSH to become available. + provisioner "remote-exec" { + inline = ["true"] + } + + # 2. Build NixOS system. + provisioner "local-exec" { + command = "nix-build ${data.external.nixos_system.result.drv} --no-out-link" + } + + # 3. Copy closure to the target. + provisioner "local-exec" { + command = "${path.module}/nixos-copy.sh" + + environment = { + SYSTEM_DRV = data.external.nixos_system.result.drv + TARGET_HOST = var.target_host + DEPLOY_KEY = var.target_user_ssh_key + TARGET_USER = var.target_user + } + } + + # 4. Activate closure on the target. + provisioner "remote-exec" { + inline = [ + "set -eu", + "SYSTEM=$(nix-build ${data.external.nixos_system.result.drv} --no-out-link)", + "sudo nix-env --profile /nix/var/nix/profiles/system --set $SYSTEM", + "sudo $SYSTEM/bin/switch-to-configuration switch", + ] + } + + triggers = merge({ + nixos_drv = data.external.nixos_system.result.drv + target_host = var.target_host + }, var.triggers) +} + +output "nixos_drv" { + value = data.external.nixos_system.result +} diff --git a/ops/terraform/deploy-nixos/nix-eval.sh b/ops/terraform/deploy-nixos/nix-eval.sh new file mode 100755 index 0000000000..65f534180b --- /dev/null +++ b/ops/terraform/deploy-nixos/nix-eval.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2023 The TVL Authors +# +# SPDX-License-Identifier: MIT +set -ueo pipefail + +# Evaluates a Nix expression. +# +# Receives input parameters as JSON from stdin. +# It expects a dict with the following keys: +# +# - `attrpath`: the attribute.path pointing to the expression to instantiate. +# Required. +# - `entrypoint`: the path to the Nix file to invoke. +# Optional. If omitted, will shell out to git to determine the repo root, +# and Nix will use `default.nix` in there. +# - `argstr_json`: A string JSON-encoding a map containing string keys and +# values which should be passed to Nix as `--argstr $key $value`. +# command line args. Optional. +# - `build`: A boolean (or string being "true" or "false") stating whether the +# expression should also be built/substituted on the machine executing this script. +# +# jq's @sh format takes care of escaping. +eval "$(jq -r '@sh "attrpath=\(.attrpath) && entrypoint=\(.entrypoint) && argstr=\((.argstr_json // "{}"|fromjson) | to_entries | map ("--argstr", .key, .value) | join(" ")) build=\(.build)"')" + +# Evaluate the expression. +[[ -z "$entrypoint" ]] && entrypoint=$(git rev-parse --show-toplevel) +# shellcheck disable=SC2086,SC2154 +drv=$(nix-instantiate -A "${attrpath}" "${entrypoint}" ${argstr}) + +# If `build` is set to true, invoke nix-build on the .drv. +# We need to swallow all stdout, to not garble the JSON printed later. +# shellcheck disable=SC2154 +if [ "${build}" == "true" ]; then + nix-build --no-out-link "${drv}" > /dev/null +fi + +# Determine the output path. +outPath=$(nix show-derivation "${drv}" | jq -r ".\"${drv}\".outputs.out.path") + +# Return a JSON back to stdout. +# It contains the following keys: +# +# - `drv`: the store path of the Derivation that has been instantiated. +# - `outPath`: the output store path. +jq -n --arg drv "$drv" --arg outPath "$outPath" '{"drv":$drv, "outPath":$outPath}' diff --git a/ops/terraform/deploy-nixos/nixos-copy.sh b/ops/terraform/deploy-nixos/nixos-copy.sh new file mode 100755 index 0000000000..6b843c3a49 --- /dev/null +++ b/ops/terraform/deploy-nixos/nixos-copy.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2023 The TVL Authors +# +# SPDX-License-Identifier: MIT + +# +# Copies a NixOS system to a target host, using the provided key, +# or whatever ambient key is configured if the key is not set. +set -ueo pipefail + +export NIX_SSHOPTS="\ + -o StrictHostKeyChecking=no\ + -o UserKnownHostsFile=/dev/null\ + -o GlobalKnownHostsFile=/dev/null" + +# If DEPLOY_KEY was passed, write it to $scratch/id_deploy +if [ -n "${DEPLOY_KEY-}" ]; then + scratch="$(mktemp -d)" + trap 'rm -rf -- "${scratch}"' EXIT + + echo -n "$DEPLOY_KEY" > $scratch/id_deploy + chmod 0600 $scratch/id_deploy + export NIX_SSHOPTS="$NIX_SSHOPTS -o IdentityFile=$scratch/id_deploy" +fi + +nix-copy-closure \ + --to ${TARGET_USER}@${TARGET_HOST} \ + ${SYSTEM_DRV} \ + --gzip \ + --include-outputs \ + --use-substitutes |