├── 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 |
--------------------------------------------------------------------------------