about summary refs log tree commit diff
path: root/ops/terraform
diff options
context:
space:
mode:
Diffstat (limited to 'ops/terraform')
-rw-r--r--ops/terraform/README.md5
-rw-r--r--ops/terraform/deploy-nixos/README.md50
-rw-r--r--ops/terraform/deploy-nixos/main.tf113
-rwxr-xr-xops/terraform/deploy-nixos/nix-eval.sh47
-rwxr-xr-xops/terraform/deploy-nixos/nixos-copy.sh32
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