├── example ├── .gitignore ├── configuration.nix ├── flake.nix ├── README.md └── main.tf ├── ami ├── flake.nix ├── ami.sh ├── README.md └── main.tf ├── nixos ├── instantiate.sh ├── README.md └── main.tf ├── LICENSE └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform.lock.hcl 2 | .terraform/ 3 | terraform.tfstate 4 | terraform.tfstate.backup 5 | -------------------------------------------------------------------------------- /ami/flake.nix: -------------------------------------------------------------------------------- 1 | { outputs = { nixpkgs, ... }: 2 | import "${nixpkgs}/nixos/modules/virtualisation/amazon-ec2-amis.nix"; 3 | } 4 | -------------------------------------------------------------------------------- /example/configuration.nix: -------------------------------------------------------------------------------- 1 | { modulesPath, ... }: 2 | 3 | { imports = [ 4 | "${modulesPath}/virtualisation/amazon-image.nix" 5 | ]; 6 | 7 | system.stateVersion = "22.11"; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /nixos/instantiate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash --packages bash jq 3 | 4 | set -euo pipefail 5 | 6 | FLAKE="${1}" 7 | 8 | nix path-info --derivation "${FLAKE}.config.system.build.toplevel" | jq --raw-input '{ "path": . }' 9 | -------------------------------------------------------------------------------- /example/flake.nix: -------------------------------------------------------------------------------- 1 | { inputs.nixpkgs.url = "github:NixOS/nixpkgs/22.11"; 2 | 3 | outputs = { nixpkgs, ... }: { 4 | nixosConfigurations.default = nixpkgs.lib.nixosSystem { 5 | system = "x86_64-linux"; 6 | 7 | modules = [ ./configuration.nix ]; 8 | }; 9 | }; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ami/ami.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #! nix-shell -i bash --packages bash jq 3 | 4 | set -euo pipefail 5 | 6 | DIRECTORY="$(realpath "${1}")" 7 | RELEASE="${2}" 8 | REGION="${3}" 9 | SYSTEM="${4}" 10 | VIRTUALIZATION_TYPE="${5}" 11 | 12 | nix eval --extra-experimental-features 'nix-command flakes' --no-write-lock-file "${DIRECTORY}#\"${RELEASE}\".\"${REGION}\".\"${SYSTEM}\".\"${VIRTUALIZATION_TYPE}\"" | jq '{ "ami": . }' 13 | -------------------------------------------------------------------------------- /ami/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | No requirements. 5 | 6 | ## Providers 7 | 8 | | Name | Version | 9 | |------|---------| 10 | | [external](#provider\_external) | n/a | 11 | 12 | ## Modules 13 | 14 | No modules. 15 | 16 | ## Resources 17 | 18 | | Name | Type | 19 | |------|------| 20 | | [external_external.ami](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | 21 | 22 | ## Inputs 23 | 24 | | Name | Description | Type | Default | Required | 25 | |------|-------------|------|---------|:--------:| 26 | | [region](#input\_region) | AWS region | `string` | n/a | yes | 27 | | [release](#input\_release) | NixOS release | `string` | `"latest"` | no | 28 | | [system](#input\_system) | The system architecture

One of:

- x86\_64-linux
- aarch64-linux | `string` | `"x86_64-linux"` | no | 29 | | [virtualization\_type](#input\_virtualization\_type) | The virtualization type of the AMI

One of:

- hvm-ebs
- hvm-s3
- pv-ebs
- pv-s3 | `string` | `"hvm-ebs"` | no | 30 | 31 | ## Outputs 32 | 33 | | Name | Description | 34 | |------|-------------| 35 | | [ami](#output\_ami) | NixOS AMI | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Gabriella Gonzalez 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `terraform-nixos-ng` 2 | 3 | This repository provides two Terraform modules: 4 | 5 | - [`./nixos`](./nixos) - A Terraform module for auto-deploying a NixOS 6 | configuration to a target host 7 | 8 | - [`./ami`](./ami) - A Terraform module for computing the correct NixOS AMI to use 9 | 10 | ## Example usage 11 | 12 | You can find an example Terraform project using these modules in the 13 | [`./example`](./example) directory. 14 | 15 | ## Motivation 16 | 17 | The main reason this repository exists is because the `terraform-nixos` 18 | repository is abandoned. I wanted to simplify the upstream project in two 19 | major ways: 20 | 21 | - I wanted to simplify the deployment logic with `nixos-rebuild` 22 | 23 | … using the trick documented in [this blog post](https://www.haskellforall.com/2023/01/announcing-nixos-rebuild-new-deployment.html) 24 | 25 | - I wanted to simplify the AMI module 26 | 27 | … by not requiring the list to be manually updated 28 | [like this](https://github.com/tweag/terraform-nixos/pull/73/files) 29 | 30 | However, since the upstream project doesn't appear to be maintained I created my 31 | own opinionated fork that is simpler than the original. The main omissions 32 | compared to the original are: 33 | 34 | - No support for secrets management 35 | 36 | However, you can use this in conjunction with 37 | [`sops-nix`](https://github.com/Mic92/sops-nix) if you need secrets 38 | management 39 | 40 | - No support for GCE images (yet) 41 | 42 | … mainly because I don't use Google Compute Engine, but I'd accept a PR to 43 | add support following the same pattern as the [`ami`](./ami) module. 44 | -------------------------------------------------------------------------------- /ami/main.tf: -------------------------------------------------------------------------------- 1 | variable "release" { 2 | type = string 3 | 4 | nullable = false 5 | 6 | default = "latest" 7 | 8 | description = "NixOS release" 9 | } 10 | 11 | variable "region" { 12 | type = string 13 | 14 | nullable = false 15 | 16 | description = "AWS region" 17 | } 18 | 19 | variable "system" { 20 | type = string 21 | 22 | nullable = false 23 | 24 | default = "x86_64-linux" 25 | 26 | description = <<-END 27 | The system architecture 28 | 29 | One of: 30 | 31 | - x86_64-linux 32 | - aarch64-linux 33 | END 34 | 35 | validation { 36 | condition = contains(["x86_64-linux", "aarch64-linux"], var.system) 37 | 38 | error_message = <<-END 39 | The system needs to be one of: 40 | 41 | - x86_64-linux 42 | - aarch64-linux 43 | END 44 | } 45 | } 46 | 47 | variable "virtualization_type" { 48 | type = string 49 | 50 | nullable = false 51 | 52 | default = "hvm-ebs" 53 | 54 | description = <<-END 55 | The virtualization type of the AMI 56 | 57 | One of: 58 | 59 | - hvm-ebs 60 | - hvm-s3 61 | - pv-ebs 62 | - pv-s3 63 | END 64 | 65 | validation { 66 | condition = contains(["hvm-ebs", "hvm-s3", "pv-ebs", "pv-s3"], var.virtualization_type) 67 | 68 | error_message = <<-END 69 | The virtualization type needs to be one of: 70 | 71 | - hvm-ebs 72 | - hvm-s3 73 | - pv-ebs 74 | - pv-s3 75 | END 76 | } 77 | } 78 | 79 | data "external" "ami" { 80 | program = [ 81 | "${path.module}/ami.sh", 82 | path.module, 83 | var.release, 84 | var.region, 85 | var.system, 86 | var.virtualization_type, 87 | ] 88 | } 89 | 90 | output "ami" { 91 | description = "NixOS AMI" 92 | 93 | value = data.external.ami.result["ami"] 94 | } 95 | -------------------------------------------------------------------------------- /nixos/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Requirements 3 | 4 | No requirements. 5 | 6 | ## Providers 7 | 8 | | Name | Version | 9 | |------|---------| 10 | | [external](#provider\_external) | n/a | 11 | | [null](#provider\_null) | n/a | 12 | 13 | ## Modules 14 | 15 | No modules. 16 | 17 | ## Resources 18 | 19 | | Name | Type | 20 | |------|------| 21 | | [null_resource.deploy](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 22 | | [external_external.instantiate](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | 23 | 24 | ## Inputs 25 | 26 | | Name | Description | Type | Default | Required | 27 | |------|-------------|------|---------|:--------:| 28 | | [arguments](#input\_arguments) | Extra arguments for the `nixos-rebuild` command

Example: `["--build-host", "root@${aws_instance.example.public_ip}"]` | `list(string)` | `[]` | no | 29 | | [flake](#input\_flake) | Flake URI for the NixOS configuration to deploy

The flake URI needs to be suitable for `nixos-rebuild`, meaning that you
should not include `nixosConfigurations` in the attribute path of the flake
URI. For example, if your NixOS configuration were actually stored at
`.#nixosConfigurations.machine` within your flake then the flake URI that
`nixos-rebuild` would expect is actually `.#machine`. | `string` | n/a | yes | 30 | | [host](#input\_host) | The host to deploy to

This can be any address that `ssh` accepts, including a `user@` prefix | `string` | n/a | yes | 31 | | [ssh\_options](#input\_ssh\_options) | `ssh` options passed to `nixos-rebuild` via `NIX_SSHOPTS` | `string` | `null` | no | 32 | 33 | ## Outputs 34 | 35 | No outputs. 36 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example use of `terraform-nixos-ng` 2 | 3 | This directory contains a `terraform` + NixOS project that deploys a minimal 4 | NixOS machine to AWS. 5 | 6 | In order to deploy this project you will need an AWS account and you will need 7 | to 8 | [obtain programmatic access keys](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html). 9 | Once you have such access keys you can configure your computer to use them by 10 | running: 11 | 12 | ```ShellSession 13 | $ nix run github:NixOS/nixpkgs/22.11#awscli configure 14 | ``` 15 | 16 | You can then deploy this project by running: 17 | 18 | ```ShellSession 19 | $ nix shell github:NixOS/nixpkgs/22.11#terraform 20 | $ terraform init 21 | $ terraform apply 22 | ``` 23 | 24 | 25 | ## Requirements 26 | 27 | | Name | Version | 28 | |------|---------| 29 | | [terraform](#requirement\_terraform) | >= 1.3.0 | 30 | | [aws](#requirement\_aws) | ~> 4.16 | 31 | 32 | ## Providers 33 | 34 | | Name | Version | 35 | |------|---------| 36 | | [aws](#provider\_aws) | 4.52.0 | 37 | | [null](#provider\_null) | 3.2.1 | 38 | 39 | ## Modules 40 | 41 | | Name | Source | Version | 42 | |------|--------|---------| 43 | | [ami](#module\_ami) | github.com/Gabriella439/terraform-nixos-ng//ami | n/a | 44 | | [nixos](#module\_nixos) | ../nixos | n/a | 45 | 46 | ## Resources 47 | 48 | | Name | Type | 49 | |------|------| 50 | | [aws_instance.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | 51 | | [aws_key_pair.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/key_pair) | resource | 52 | | [aws_security_group.example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | 53 | | [null_resource.example](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | 54 | 55 | ## Inputs 56 | 57 | | Name | Description | Type | Default | Required | 58 | |------|-------------|------|---------|:--------:| 59 | | [private\_key\_file](#input\_private\_key\_file) | n/a | `string` | n/a | yes | 60 | | [profile](#input\_profile) | n/a | `string` | `"default"` | no | 61 | | [region](#input\_region) | n/a | `string` | n/a | yes | 62 | 63 | ## Outputs 64 | 65 | | Name | Description | 66 | |------|-------------| 67 | | [public\_dns](#output\_public\_dns) | n/a | 68 | -------------------------------------------------------------------------------- /example/main.tf: -------------------------------------------------------------------------------- 1 | variable "private_key_file" { 2 | type = string 3 | 4 | nullable = false 5 | } 6 | 7 | variable "region" { 8 | type = string 9 | 10 | nullable = false 11 | } 12 | 13 | variable "profile" { 14 | type = string 15 | 16 | nullable = false 17 | 18 | default = "default" 19 | } 20 | 21 | provider "aws" { 22 | profile = var.profile 23 | 24 | region = var.region 25 | } 26 | 27 | terraform { 28 | required_version = ">= 1.3.0" 29 | 30 | required_providers { 31 | aws = { 32 | source = "hashicorp/aws" 33 | version = "~> 4.16" 34 | } 35 | } 36 | } 37 | 38 | resource "aws_security_group" "example" { 39 | # This is needed for the "nixos" module to manage the target host 40 | ingress { 41 | from_port = 22 42 | 43 | to_port = 22 44 | 45 | protocol = "tcp" 46 | 47 | cidr_blocks = [ "0.0.0.0/0" ] 48 | } 49 | 50 | # This is needed if you build on the target host, like this example does, so 51 | # that the machine can download dependencies. You would also need this if you 52 | # were to enable the `--use-substitutes` flag for `nixos-rebuild`. 53 | egress { 54 | from_port = 0 55 | 56 | to_port = 0 57 | 58 | protocol = "-1" 59 | 60 | cidr_blocks = ["0.0.0.0/0"] 61 | } 62 | } 63 | 64 | resource "aws_key_pair" "example" { 65 | public_key = file("${var.private_key_file}.pub") 66 | } 67 | 68 | module "ami" { 69 | source = "github.com/Gabriella439/terraform-nixos-ng//ami" 70 | 71 | release = "22.11" 72 | 73 | region = var.region 74 | } 75 | 76 | resource "aws_instance" "example" { 77 | ami = module.ami.ami 78 | 79 | instance_type = "t3.micro" 80 | 81 | security_groups = [ aws_security_group.example.name ] 82 | 83 | key_name = aws_key_pair.example.key_name 84 | 85 | root_block_device { 86 | volume_size = 7 87 | } 88 | } 89 | 90 | # This ensures that the instance is reachable via `ssh` before we deploy NixOS 91 | resource "null_resource" "example" { 92 | provisioner "remote-exec" { 93 | connection { 94 | host = aws_instance.example.public_dns 95 | 96 | private_key = file(var.private_key_file) 97 | } 98 | 99 | inline = [ ":" ] 100 | } 101 | } 102 | 103 | module "nixos" { 104 | source = "../nixos" 105 | 106 | host = "root@${aws_instance.example.public_ip}" 107 | 108 | flake = ".#default" 109 | 110 | arguments = [ 111 | # You can build on another machine, including the target machine, by 112 | # enabling this option, but if you build on the target machine then make 113 | # sure that the firewall and security group permit outbound connections. 114 | "--build-host", "root@${aws_instance.example.public_ip}", 115 | ] 116 | 117 | ssh_options = "-o StrictHostKeyChecking=accept-new" 118 | 119 | depends_on = [ null_resource.example ] 120 | } 121 | 122 | output "public_dns" { 123 | value = aws_instance.example.public_dns 124 | } 125 | -------------------------------------------------------------------------------- /nixos/main.tf: -------------------------------------------------------------------------------- 1 | variable "host" { 2 | type = string 3 | 4 | nullable = false 5 | 6 | description = <<-END 7 | The host to deploy to 8 | 9 | This can be any address that `ssh` accepts, including a `user@` prefix 10 | END 11 | } 12 | 13 | variable "arguments" { 14 | type = list(string) 15 | 16 | nullable = false 17 | 18 | default = [] 19 | 20 | description = <<-END 21 | Extra arguments for the `nixos-rebuild` command 22 | 23 | Example: `["--build-host", "root@$${aws_instance.example.public_ip}"]` 24 | END 25 | } 26 | 27 | variable "ssh_options" { 28 | type = string 29 | 30 | default = null 31 | 32 | description = <<-END 33 | `ssh` options passed to `nixos-rebuild` via `NIX_SSHOPTS` 34 | END 35 | } 36 | 37 | variable "flake" { 38 | type = string 39 | 40 | nullable = false 41 | 42 | description = <<-END 43 | Flake URI for the NixOS configuration to deploy 44 | 45 | The flake URI needs to be suitable for `nixos-rebuild`, meaning that you 46 | should not include `nixosConfigurations` in the attribute path of the flake 47 | URI. For example, if your NixOS configuration were actually stored at 48 | `.#nixosConfigurations.machine` within your flake then the flake URI that 49 | `nixos-rebuild` would expect is actually `.#machine`. 50 | END 51 | 52 | validation { 53 | condition = length(split("#", var.flake)) == 2 54 | 55 | error_message = "Invalid flake URI" 56 | } 57 | 58 | validation { 59 | condition = length(split("#", var.flake)) == 2 ? split("#", var.flake)[1] != "" : true 60 | 61 | # Note that nixos-rebuild supports empty attribute paths: 62 | # 63 | # https://github.com/NixOS/nixpkgs/blob/13645205311aa81dbc7c5adeee0382e38e52ee7c/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh#L362-L367 64 | # 65 | # … but does so by attempting to guess the attribute path from the hostname. 66 | # We could in theory attempt to match this behavior in terraform, but it's 67 | # simpler to disallow this and instead require the user to specify a 68 | # non-empty attribute path. 69 | error_message = "Empty flake attribute paths not supported" 70 | } 71 | } 72 | 73 | locals { 74 | components = split("#", var.flake) 75 | 76 | uri = local.components[0] 77 | 78 | attribute_path = local.components[1] 79 | 80 | real_flake = "${local.uri}#nixosConfigurations.${local.attribute_path}" 81 | } 82 | 83 | data "external" "instantiate" { 84 | program = [ "${path.module}/instantiate.sh", local.real_flake ] 85 | } 86 | 87 | resource "null_resource" "deploy" { 88 | triggers = { 89 | derivation = data.external.instantiate.result["path"] 90 | } 91 | 92 | provisioner "local-exec" { 93 | environment = { 94 | NIX_SSHOPTS = var.ssh_options 95 | NIXOS_SWITCH_USE_DIRTY_ENV = 1 96 | } 97 | 98 | interpreter = concat ( 99 | [ "nix", 100 | "--extra-experimental-features", "nix-command flakes", 101 | "shell", 102 | "github:NixOS/nixpkgs/24.11#nixos-rebuild", 103 | "--command", 104 | "nixos-rebuild", 105 | "--fast", 106 | "--flake", var.flake, 107 | "--target-host", var.host, 108 | ], 109 | var.arguments 110 | ) 111 | 112 | command = "switch" 113 | } 114 | } 115 | --------------------------------------------------------------------------------