Este artigo demonstra como fazer deploy automaticamente de uma máquina NixOS. Temos 2 formas principais de fazer deploy de uma máquina NixOS: iniciando diretamente uma máquina com uma imagem NixOS e fazendo deploy das nossas configurações remotamente, ou usando outro sistema Linux e sobrescrevendo o SO usando uma ferramenta como kexec. Você pode até escrever sua própria ISO para que já venha com suas configurações, mas esse é assunto para outro artigo. Vou mostrar como sobrescrever um SO com NixOS automaticamente com literalmente um clique.
O papel do Cloud-init aqui será nos permitir acessar a máquina instalada via SSH. Ele instalará uma chave pública SSH; consequentemente, precisa de um user para manter essa chave. Isso precisa ser um arquivo de template, pois será interpretado pelo template do Terraform (tftpl). Crie um arquivo chamado templates/user-data.yaml.tftpl:
#cloud-config
users:
- name: ${user}
hostname: ${hostname}
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- ${ssh_public_login_key}
lock_passwd: true
write_files:
- path: /home/${user}/.ssh/id_rsa
permissions: '0600'
content: |
${indent(6, nixos_ssh_private_deploy_key)}
owner: '${user}:${user}'
defer: true
- path: /root/.ssh/id_rsa
permissions: '0600'
content: |
${indent(6, nixos_ssh_private_deploy_key)}
owner: 'root:root'
defer: true
# Você pode opcionalmente instalar uma chave de deploy para um repositório do github, contendo sua configuração NixOS por exemplo
- path: /home/${user}/.ssh/id_ed25519
permissions: '0600'
content: |
${indent(6, playbook_ssh_private_deploy_key)}
owner: '${user}:${user}'
defer: true
- path: /root/.ssh/id_ed25519
permissions: '0600'
content: |
${indent(6, playbook_ssh_private_deploy_key)}
owner: 'root:root'
defer: true
Primeiro, precisamos criar o arquivo de configuração da máquina NixOS. Usaremos flakes, pois é uma boa prática e não é difícil de criar, no seu templates/flake.nix:
{
description = "Simple NixOS config for cloud instance";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
disko.url = "github:nix-community/disko";
};
outputs = { self, nixpkgs, disko, ... }: {
nixosConfigurations.<user> = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
disko.nixosModules.disko
./disko-config.nix
({ pkgs, ... }: {
networking.hostName = "<user>";
time.timeZone = "UTC";
services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;
users.users.<user> = {
isNormalUser = true;
extraGroups = [ "wheel" ];
# preencha este campo com sua(s) chave(s) pública(s) ssh
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEGoIMYLhnlfKRIbKMrD5ABT+KhtXL4HqVTCrkP36KJS <email>"
];
};
# Podemos instalar pacotes que serão usados pelo comando cloud-init, incluindo o próprio cloud-init.
environment.systemPackages = with pkgs; [ git ansible cloud-init ];
security.sudo.wheelNeedsPassword = false;
system.stateVersion = "25.05";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
# A configuração declarativa do NixOS nos permite definir serviços systemD poupando carga de ferramentas como ansible.
systemd.services.<user> = {
description = "Serviço do <user>";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.bash}/bin/bash -c 'echo The systemd ran > /home/<user>/log'";
User = "<user>";
Group = "wheel";
Restart = "on-failure";
RestartSec = "5s";
};
};
})
];
};
packages.x86_64-linux.<user> =
self.nixosConfigurations.<user>.config.system.build.toplevel;
packages.x86_64-linux.<user>-disko =
self.nixosConfigurations.<user>.config.system.build.diskoScript;
};
}Nossa configuração de user está pronta; porém, precisamos configurar a partição do SO. Felizmente, o NixOS tem o pacote disko. Criaremos um módulo para a configuração do disko. Crie um arquivo chamado templates/disko-config.nix:
{
disko.devices.disk.main = {
device = "/dev/nvme0n1";
type = "disk";
content = {
type = "gpt";
partitions = {
boot = {
size = "1G";
type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
}O Terraform vai de fato fazer deploy de tudo. Ele vai unir a instalação do Linux, a execução do Cloud-init e a instalação do NixOS, através do provider Terraform do NixOS anywhere. Você pode usar qualquer provider de nuvem/infraestrutura que quiser. O provider da AWS é fácil de usar e seu ecossistema é grande. Este exemplo usará uma instância EC2. No seu main.tf configure:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_ami" "instance" {
most_recent = var.most_recent
filter {
name = "name"
values = [var.ami_filter_name]
}
filter {
name = "virtualization-type"
values = [var.virtualization_type]
}
filter {
name = "is-public"
values = [var.is_public]
}
filter {
name = "free-tier-eligible"
values = [var.is_free_tier_eligible]
}
filter {
name = "architecture"
values = [var.architecture]
}
owners = var.owners
}
resource "aws_instance" "web" {
ami = data.aws_ami.instance.id
instance_type = var.instance_type
tags = {
Name = var.instance_name
}
tenancy = var.tenancy
user_data = templatefile(
var.template_path,
{
user = var.user
ssh_public_login_key = file(var.ssh_public_login_key)
hostname = var.hostname
playbook_ssh_private_deploy_key = file(var.playbook_ssh_private_deploy_key)
playbook_ssh_private_deploy_key_path = var.playbook_ssh_private_deploy_key_path
app_ansible_playbook_name = var.app_ansible_playbook_name
app_ansible_playbook_url = var.app_ansible_playbook_url
app_inventory_name = var.app_inventory_name
nixos_ssh_private_deploy_key = file(var.nixos_ssh_private_deploy_key)
nixos_config_url = var.nixos_config_url
}
)
}
output "public_ip" {
value = aws_instance.web.public_ip
description = "O endereço IPv4 da instância"
}
output "public_dns" {
value = aws_instance.web.public_dns
description = "O nome DNS público da instância"
}
output "security_groups" {
value = aws_instance.web.security_groups
description = "Os grupos de segurança associados à instância"
}
output "arn" {
value = aws_instance.web.arn
description = "O ARN da instância"
}
output "tags" {
value = aws_instance.web.tags
description = "O user da instância"
}
module "deploy" {
source = "github.com/nix-community/nixos-anywhere/terraform/all-in-one"
nixos_system_attr = "../templates#packages.x86_64-linux.<your-user>"
nixos_partitioner_attr = "../templates#packages.x86_64-linux.<your-user>-disko"
target_host = module.ec2.public_ip
instance_id = module.ec2.public_ip
install_ssh_key = file("~/.aws/keys/login_keys/instance")
deployment_ssh_key = file("~/.aws/keys/login_keys/instance")
target_user = "<your-user>"
install_user = module.ec2.tags["Name"]
}Vamos interpretar o arquivo user-data usando a função embutida templatefile() do Terraform e ligá-lo ao user_data. Você pode configurar qualquer SO Linux que quiser, ex: Ubuntu. Você pode definir outputs úteis como o ARN ID, IP público ou nome de domínio para se conectar à instância sem acessar o painel web.
Depois disso, você pode definir as variáveis manualmente ou deixar este arquivo como modelo e criar outro arquivo que importa este como módulo em example/main.tf como boa prática:
module "ec2" {
source = "../"
region = "<region>"
instance_type = "<instance-type>"
instance_name = "<instance-name>"
most_recent = true
ami_filter_name = "<image-name>"
architecture = "x86_64"
is_public = true
is_free_tier_eligible = true
owners = ["<image-owner>"]
virtualization_type = "hvm"
tenancy = "default"
ssh_public_login_key = "<public-ssh-key>"
hostname = "<instance-name>"
user = "<instance-name>"
app_inventory_name = "localhost,"
# Este é o caminho para o template user-data interpretado pelo Terraform, será usado no cloud-init
template_path = "<path-to-user-data>"
# Executar aplicação
app_ansible_playbook_url = "<ansible-playbook-repo>"
app_ansible_playbook_name = "<playbook-name>"
# Estes são opcionais, caso queira usar um repositório de playbook Ansible para
# executar o comando `ansible-pull` e este repositório é privado, você pode usar uma chave de deploy
playbook_ssh_private_deploy_key = "<public-ssh-key>"
playbook_ssh_private_deploy_key_path = "<public-ssh-key>"
# Configuração NixOS
nixos_ssh_private_deploy_key = "~/.aws/keys/deploy_keys/nixos"
nixos_config_url = "[email protected]:<user>/nixos_config.git" # Você pode buscar uma configuração para seu NixOS de um repositório
}
output "public_ip" {
value = module.ec2.public_ip
description = "O endereço IPv4 da instância"
}
output "public_dns" {
value = module.ec2.public_dns
description = "O nome DNS público da instância"
}
output "security_groups" {
value = module.ec2.security_groups
description = "Os grupos de segurança associados à instância"
}
output "arn" {
value = module.ec2.arn
description = "O ARN da instância"
}
output "user" {
value = module.ec2.tags["Name"]
description = "O user da instância"
}
module "deploy" {
source = "github.com/nix-community/nixos-anywhere/terraform/all-in-one"
nixos_system_attr = "../templates#packages.x86_64-linux.<your-user>"
nixos_partitioner_attr = "../templates#packages.x86_64-linux.<your-user>-disko"
target_host = module.ec2.public_ip
instance_id = module.ec2.public_ip
install_ssh_key = file("~/.aws/keys/login_keys/instance")
deployment_ssh_key = file("~/.aws/keys/login_keys/instance")
target_user = "<your-user>"
install_user = module.ec2.tags["Name"]
}Note que temos duas etapas aqui, divididas em dois módulos: o deploy de uma máquina, e a segunda etapa, onde o Terraform executará os recursos do nixos-anywhere naquela máquina da primeira etapa, onde o kexec é executado internamente. Portanto, na segunda etapa temos duas chaves privadas; a que já está registrada na instância (deployment_ssh_key), que instalamos no script cloud-init, e a outra que será instalada após a conclusão da instalação do NixOS (install_ssh_key), sua respectiva chave pública será usada para acessarmos esta instância. A mesma lógica se aplica ao usuário, com a ressalva de que não podemos escolher o nome de user da instância a menos que tenhamos criado a ISO nós mesmos; verifique esses detalhes com seu provider de imagem.
Execute estes comandos no mesmo nível do seu main.tf para instalar as dependências e executar o Terraform. Note que levará alguns minutos para fazer deploy completamente do software final.
terraform init
terraform apply