OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 073

Terraform 与 NixOS 的部署分工

由 Terraform 管理云资源,再用受控流程部署 NixOS 系统配置。

所在专题NixOS 虚拟化与部署 ↗
约 22 分钟阅读开始阅读

版本范围:下列示例保留 NixOS 24.05 的教程上下文;该版本已结束维护。实际项目应选择受支持的发行版、锁定 nixpkgs revision,并核对相应选项。system.stateVersion 表示兼容性基线,不应随升级机械改动。本文命令是教学示例,未在目标系统运行验证。

参考 nix.dev 的基础设施与系统部署思路;本文含扩展示例,并非该教程的逐字翻译。官方教程本身也含旧版 release,使用前需更新锁定输入。参考: Deploying NixOS using Terraform

结合 Terraform 的基础设施管理和 NixOS 的系统配置,实现完整的 IaC(基础设施即代码)。

为什么选择 Terraform + NixOS?#

特性 Terraform NixOS 结合优势
基础设施管理 ✅ 优秀 ❌ 不涉及 云平台资源
系统配置 ❌ 不涉及 ✅ 优秀 声明式OS
可复现性 声明资源和跟踪 state 锁定输入并构建系统 closure 云资源仍受平台状态、镜像及外部服务影响
版本管理 良好 优秀 完整历史

架构概述#

┌─────────────────┐      ┌─────────────────┐      ┌───────────────┐
│  Terraform      │──────▶│  Cloud Provider │──────▶│  NixOS        │
│  - VM/Network   │      │  - AWS/GCP      │      │  - configure  │
│  - Firewall     │      │  - Disk/IP      │      │  - user       │
└─────────────────┘      └─────────────────┘      └───────────────┘
                              │
                              ▼
                         NixOS 安装镜像

基础配置#

1. NixOS 镜像准备

AWS 已有适用的 NixOS AMI 时,不需要先构建破坏磁盘的自动安装 ISO。自定义安装介质适用于没有现成镜像的受控环境,是与云资源创建分开的安装路径。下面保留分区、格式化、挂载、生成配置和安装的完整流程,但将自动开机执行改为手动启动的 oneshot。

前提:这是专用的空白测试盘,数据已备份;磁盘身份、UEFI 模式和分区容量已确认;ISO 内含 target-configuration.nix,其中有正确的 bootloader、文件系统和 SSH 公钥,不含真实秘密。按目标选择该盘的稳定 by-id 路径和对应分区路径;示例占位符必须替换。此例需盘容量足够保留末尾 8 GiB swap。

# install-image.nix
{ pkgs, modulesPath, ... }:
{
  imports = [
    (modulesPath + "/installer/cd-dvd/installation-cd-minimal.nix")
  ];

  systemd.services.example-nixos-install = {
    description = "Install NixOS on an explicitly selected empty test disk";
    # 没有 wantedBy:不会开机自动清盘,确认后才手动启动
    path = with pkgs; [ parted e2fsprogs dosfstools util-linux systemd nixos-install-tools ];
    serviceConfig.Type = "oneshot";
    script = ''
      set -eu
      disk='/dev/disk/by-id/<EMPTY_TEST_DISK>'
      rootpart='/dev/disk/by-id/<EMPTY_TEST_DISK>-part1'
      swappart='/dev/disk/by-id/<EMPTY_TEST_DISK>-part2'
      esp='/dev/disk/by-id/<EMPTY_TEST_DISK>-part3'
      test -b "$disk"
      # 目标分区不得处于已挂载状态;由操作者在启动前核对 lsblk
      parted "$disk" -- mklabel gpt
      parted "$disk" -- mkpart primary 512MiB -8GiB
      parted "$disk" -- mkpart primary linux-swap -8GiB 100%
      parted "$disk" -- mkpart ESP fat32 1MiB 512MiB
      parted "$disk" -- set 3 esp on
      partprobe "$disk"
      udevadm settle

      mkfs.ext4 -L nixos "$rootpart"
      mkswap -L swap "$swappart"
      mkfs.fat -F 32 -n boot "$esp"
      mount "$rootpart" /mnt
      mkdir -p /mnt/boot
      mount "$esp" /mnt/boot
      swapon "$swappart"

      nixos-generate-config --root /mnt
      # 配置来自随 ISO 固定的本地源,保留生成的 hardware-configuration.nix
      cp ${./target-configuration.nix} /mnt/etc/nixos/configuration.nix
      nixos-install --no-root-passwd
      # 不自动重启:先检查安装结果、SSH 用户和可用恢复介质
    '';
  };
}

pkgs.systemd 为 service PATH 提供 udevadm。--no-root-passwd 只跳过安装时设置 root 密码,不会自动创建可登录的管理用户;目标配置须提前声明身份与授权。上例没有自动确认磁盘与备份,不能作为通用无人值守安装器。确认完成后才用 systemctl start example-nixos-install.service 触发安装,再检查日志与 /mnt 下结果。

若需要联网下载配置,应等待 network-online.target 并使用有错误处理的 HTTPS 下载和可信 hash/签名校验;不能用能 ping 某个公网域名来判断配置可信。更稳妥的是把已审阅配置固定进镜像。

2. Terraform 配置

以下保留 AWS provider 5.x 的历史配置范围。创建 main.tf 前应通过 NixOS 官方渠道与云平台核对区域、AMI owner、架构、用户名和 user-data 支持;provider 文档、.terraform.lock.hcl 和实际 AMI 决定接口。配置片段含占位符,不能直接 apply。创建 main.tf:

# 提供商配置(以 AWS 为例)
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-west-2"
}

# 获取 NixOS AMI
data "aws_ami" "nixos" {
  most_recent = true # 发现用;实际交付应记录并固定解析出的 AMI ID
  owners      = [ var.nixos_ami_owner ]  # 从官方渠道核验的镜像发布账号

  filter {
    name   = "name"
    values = [ var.nixos_ami_name ] # 核验过的版本名;最终部署宜固定 AMI ID
  }
  
  filter {
    name   = "architecture"
    values = [ "x86_64" ]
  }
}

# 创建 VPC
resource "aws_vpc" "nixos_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  
  tags = {
    Name = "nixos-vpc"
  }
}

# 子网
resource "aws_subnet" "nixos_subnet" {
  vpc_id                  = aws_vpc.nixos_vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-west-2a"
  map_public_ip_on_launch = true
  
  tags = {
    Name = "nixos-subnet"
  }
}

# 安全组
resource "aws_security_group" "nixos_sg" {
  name_prefix = "nixos-sg"
  vpc_id      = aws_vpc.nixos_vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.admin_cidr] # 仅可信管理网络
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# SSH 密钥
resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = trimspace(file(pathexpand("~/.ssh/id_ed25519.pub")))
}

# VPC 的公网子网需要 Internet Gateway 与默认路由
resource "aws_internet_gateway" "nixos" {
  vpc_id = aws_vpc.nixos_vpc.id
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.nixos_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.nixos.id
  }
}

resource "aws_route_table_association" "public" {
  subnet_id = aws_subnet.nixos_subnet.id
  route_table_id = aws_route_table.public.id
}

# 由操作者提供,不内置账号与真实网络身份
variable "nixos_ami_owner" { type = string }
variable "nixos_ami_name" { type = string }
variable "admin_cidr" { type = string }

# NixOS 配置(仅在已确认 AMI 支持这一 user_data 格式时使用)
locals {
  nixos_config = <<-EOT
    { config, pkgs, ... }: {
      imports = [
        <nixpkgs/nixos/modules/virtualisation/amazon-image.nix>
      ];
      
      ec2.hvm = true;
      
      networking.hostName = "nixos-server";
      
      services.openssh = {
        enable = true;
        settings.PermitRootLogin = "no";
      };
      
      users.users.admin = {
        isNormalUser = true;
        extraGroups = [ "wheel" ]; # docker 组具有接近 root 的权限,按实际需求另行授予
        openssh.authorizedKeys.keys = [
          "${trimspace(file(pathexpand("~/.ssh/id_ed25519.pub")))}"
        ];
      };
      
      security.sudo.wheelNeedsPassword = false;
      
      services.nginx = {
        enable = true;
        virtualHosts.default = {
          root = "$${pkgs.writeTextDir "index.html" "NixOS web service"}";
        };
      };
      
      system.stateVersion = "24.05";
    }
  EOT
}

# EC2 实例
resource "aws_instance" "nixos" {
  ami                    = data.aws_ami.nixos.id
  instance_type          = "t3.medium"
  subnet_id              = aws_subnet.nixos_subnet.id
  vpc_security_group_ids = [aws_security_group.nixos_sg.id]
  key_name               = aws_key_pair.deployer.key_name
  
  user_data = local.nixos_config
  
  root_block_device {
    volume_size = 20
    volume_type = "gp3"
  }
  
  tags = {
    Name = "nixos-server"
  }
}

# 输出
output "public_ip" {
  value = aws_instance.nixos.public_ip
}

output "ssh_command" {
  value = "ssh admin@${aws_instance.nixos.public_ip}"
}

传入 Nix 表达式的 $${...} 是 Terraform 对字面 ${...} 的转义,最终交给 Nix 求值;Terraform 的 ${trimspace(file(...))} 则在 Terraform 阶段插值。两层插值不要混用。AMI 的 EC2 user-data 机制依镜像配置决定:并非任意 AMI 都会把一段 Nix 配置自动应用。未核实支持时删除 user_data,用 SSH 部署构建好的系统 closure。公开 user-data 和 state 中只放公共配置,不能放私钥、API token 或密码。

上述 security.sudo.wheelNeedsPassword = false 会允许 wheel 组无密码 sudo,仅适用于已审阅的自动化管理账号策略;更严格的环境应限制具体命令或采用具备交互认证的部署方式。

使用方法#

1. 初始化

terraform init

2. 计划

terraform plan -out=deployment.tfplan

3. 应用

init 初始化 provider/backend,plan 计算候选变化,apply 才实际创建、修改或销毁云资源并可能产生费用。先审阅保存的 plan、目标账户和网络规则,再应用同一 plan。

terraform apply deployment.tfplan

4. 连接服务器

ssh admin@$(terraform output -raw public_ip)

使用 Flakes 管理配置#

Terraform 管理云资源,flake 独立定义系统;不存在一个通用的 hashicorp/terraform-provider-nixos flake 模块接口可直接套用。可以使用专门的第三方集成,但应逐项核对其真实仓库、接口和维护状态。这里用常规 NixOS flake 明确分工:

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; # 历史例子,实际选受支持版本
  outputs = { self, nixpkgs }: {
    nixosConfigurations.server = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
        (nixpkgs + "/nixos/modules/virtualisation/amazon-image.nix")
      ];
    };
  };
}
# 构建 closure,不激活云主机
nix build .#nixosConfigurations.server.config.system.build.toplevel

# 已确认 SSH 用户与权限后,单独进行远端临时激活;24.05 参数示例
nixos-rebuild test --flake .#server \
  --target-host admin@<CLOUD_HOST> --use-remote-sudo
# 通过实际运行与 SSH 检查后,才采用 switch 持久部署

官方 nix.dev 历史教程使用 tweag/terraform-nixos 的 aws_image_nixos 与 deploy_nixos 模块。它说明了同一分工,但旧 release 和历史 revision 不能当成当前版本保证;客户端/目标架构不同时还需要适当的 Linux builder 或交叉构建。

多环境管理#

开发/测试/生产

# environments/dev.tfvars
instance_type = "t3.small"
server_count  = 1

# environments/prod.tfvars  
instance_type = "t3.xlarge"
server_count  = 3

这些变量须在主配置中先声明并实际被资源使用;否则 tfvars 不会自动改变实例类型或数量。为各环境使用独立 state/backend 和凭据。

使用:

terraform plan -var-file=environments/prod.tfvars -out=prod.tfplan
# 审阅计划后实际应用
terraform apply prod.tfplan

与 NixOps 对比#

维度 Terraform NixOps 类 NixOS 部署工具
云资源管理 通过各云 provider 定义 VM、网络、存储等资源 通过工具版本、backend/plugin 对接资源,能力随生态而变
NixOS 配置 通过已核验 user-data、外部模块或独立部署流程连接 面向 NixOS closure 与系统部署,集成方式由工具接口决定
状态管理 state、锁与远程 backend 需独立设计 各工具有自己的状态和 deployment 管理方式
团队协作 取决于 backend、权限、审批和团队流程 同样取决于工具生态、状态共享与权限流程
选择依据 现有基础设施 provider 与云治理需求 NixOS 配置、节点管理与团队熟悉程度

这不是对工具稳定性或团队协作能力的排名。选型前核验所用工具版本、云 backend 和恢复流程。

最佳实践#

  1. 分离基础设施和配置:Terraform 管理云资源,NixOS 管理系统
  2. 使用远程状态:配置 S3 后端存储 Terraform 状态
  3. 版本锁定:锁定 NixOS 版本和 AMI ID
  4. 密钥管理:使用 AWS Secrets Manager 或 HashiCorp Vault
# 使用远程状态存储
terraform {
  backend "s3" {
    bucket = "<STATE_BUCKET>"
    key    = "nixos/terraform.tfstate"
    region = "us-west-2"
    encrypt = true
  }
}

State 可能记录敏感值,标记 sensitive 主要控制显示,不等于把数据从 state 删除。远程 backend 还要配置访问权限、加密、版本化、并发锁与恢复;不要把 state 或 plan 产物当普通公开源码上传。云资源回滚应通过审阅过的旧配置与新 plan,而不是假定 NixOS generation 回滚会恢复云网络或磁盘。

参考与延伸

  1. nix.dev Terraform 历史教程 ↗
  2. AWS provider EC2 instance ↗
  3. AWS provider route table ↗
  4. Terraform sensitive data ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

NixOS 虚拟化与部署

  1. 01用 QEMU 验证 NixOS 配置
  2. 02构建可启动的 NixOS ISO
  3. 03用 dockerTools 构建与运行容器镜像
  4. 04通过 SSH 部署 NixOS
  5. 05Terraform 与 NixOS 的部署分工
  6. 06设计分层的桌面与开发环境
CONTINUE READING / 下一篇设计分层的桌面与开发环境

依据依赖与权限边界选择宿主机、Flatpak、Distrobox、虚拟机和服务器。