Criando um servidor de Factorio 100% declarativo com NixOS e Terraform

Salve galerinha! Gabriel aqui.

Uma dúvida comum que pessoas novas ao Terraform têm é: “depois de criar a VM, como eu rodo minha aplicação?”. Algumas soluções incluem cloud-init, Ansible, etc.

Nesse guia, eu quero mostrar uma das melhores (na minha opinião) soluções para isso: o NixOS. Mostrarei como usar Terraform + NixOS para provisionar um servidor já rodando uma aplicação de sua escolha, sem nenhum passo manual.

A nova atualização e expansão Space Age do Factorio saiu semana passada. Se você valoriza seu sono, recomendo não jogar! Pro resto de nós, já viciados, a fábrica deve crescer!

Com isso em mente, esse guia irá, como exemplo divertido, focar em subir um servidor de Factorio! :gear:

O versão final está disponível aqui: GitHub - Misterio77/hackathon-mgc-factorio-terraform

Intro :checkered_flag:

O NixOS é uma distribuição Linux baseada no gerenciador de pacotes Nix. O Nix permite empacotar programas de forma reproduzível e isolada, numa linguagem declarativa e pura. O NixOS leva isso a um outro nível, e permite configurar sistemas inteiros usando essa mesma linguagem. Por exemplo, para subir um servidor de Factorio:

{
  services.factorio = {
    enable = true;
  };
}

Lembra bastante o Terraform, né?

Vou mostrar pra vocês como implantar e configurar um servidor no Magalu Cloud, via Terraform e NixOS, por meio do nixos-anywhere.

A idéia é que, com apenas um tofu apply, o servidor seja criado já rodando exatamente o que você quer que rode, sem nenhum passo manual. :construction_worker:

Mãos à obra!

Setup :hammer:

Caso queira acompanhar o tutorial e ir rodando coisas na sua máquina (que recomendo!), você vai precisar:

  • Qualquer distro Linux (pode ser WSL) ou MacOS;
  • Instalar o gerenciador de pacotes Nix (sua máquina não precisa ser NixOS);
  • Instalar a MGC CLI;
  • Instalar o OpenTofu (recomendado) ou o Terraform;
  • Ter um par de chave SSH. Tenha a chave pública em mãos;
  • Um editor de texto que você goste.

Configuração de NixOS :snowflake:

Para usarmos algumas funcionalidades novas do Nix, vamos habilitar flakes e nix-command:

$ mkdir -p ~/.config/nix
$ echo "extra-experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf

Vamos começar configurando nosso NixOS. Crie uma configuration.nix:

{
  networking = {
    hostName = "factorio-server";
    # Usar DHCP para conectar
    useDHCP = true;
  };

  system.stateVersion = "24.05";
  nixpkgs = {
    # Arquitetura e sistema
    hostPlatform = "x86_64-linux";
    # Habilitar pacotes proprietários
    config.allowUnfree = true;
  };

  services.factorio = {
    enable = true;
    # Abrir porta no firewall
    openFirewall = true;
  };

  # TODO: Iremos remover isso depois
  users.users.root = {
    initialPassword = "123456";
  };
}

Vamos usar flakes nesse tutorial. Crie uma flake.nix:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  };

  outputs = { nixpkgs, ... }: {
    nixosConfigurations.factorio-server = nixpkgs.lib.nixosSystem {
      modules = [./configuration.nix];
    };
  };
}

Esse arquivo define qual versão do nixpkgs estamos usando (nixos-unstable), e o que estamos provendo (uma nixosConfiguration chamada factorio-server).

Vamos testar? O Nix permite criar uma VM para uma dada configuração de NixOS. Rode:

$ nix run .#nixosConfigurations.factorio-server.config.system.build.vm

Pode levar alguns minutos, pois o Nix irá baixar absolutamente tudo nescessário para esse sistema. Fique tranquilo, pois seu sistema irá re-usar isso sempre que possível.

Irá abrir uma janela com o console da sua VM. Faça login com root e 123456.

Vamos ver se o servidor está okay:

systemctl status factorio

Sucesso! Nossa VM está rodando Factorio :gear:

Agora, vamos colocar isso na Cloud! Bora para o Terraform. Depois voltaremos para fazer alguns ajustes nessa configuração.

Subindo VM com Terraform :cloud:

Crie um arquivo main.tf, com o conteúdo:

terraform {
  backend "local" {
    path = ".terraform.tfstate"
  }
  required_providers {
    mgc = {
      source = "registry.terraform.io/magalucloud/mgc"
    }
  }
}

provider "mgc" {
  region = "br-se1"
}

Nice. Esse é o nosso boilerplate básico pra usar o provider e manter o estado do terraform no nosso diretório local.

O terraform lê todas as .tf no diretório, então vamos deixar as coisas organizadinhas.

Vamos preparar nossa VM. Crie um vm.tf:

resource "mgc_virtual_machine_instances" "factorio_server" {
  name = "factorio"
  # Gerar nome automaticamente
  name_is_prefix = true
  machine_type = {
    # 2 vCPUs, 8GB de RAM, 40GB de disco
    name = "BV2-8-40"
  }
  image = {
    name = "cloud-debian-12 LTS"
  }
  network = {
    associate_public_ip = true
  }
}

Ué, Debian? Não íamos usar NixOS? Que sacrilégio é esse?

Calma calma foguetinho :rocket:! O Magalu Cloud ainda não tem imagem de NixOS, mas temos uma carta na manga para instalar e configurar o NixOS, aguenta ai!

Precisamos acessar essa VM. Vamos adicionar nossa chave pública, um security group (para abrir portas), e pedir para o Terraform mostrar o IP dela após criada. Na sua vm.tf:

resource "mgc_ssh_keys" "key" {
  name = "chave-do-gabriel"
  # Altere para a sua chave publica
  key = "<SUA CHAVE SSH PUBLICA>"
}

resource "mgc_virtual_machine_instances" "factorio_server" {
  name = "factorio"
  name_is_prefix = true
  machine_type = {
    name = "BV2-8-40"
  }
  image = {
    name = "cloud-debian-12 LTS"
  }
  network = {
    associate_public_ip = true
    interface = {
      security_groups = [{
         # grupo criado previamente pelo gabriel
        id = "4aa1a237-2d57-439b-bf6a-177ddbace4cb"
      }]
    }
  }
  # Passar nossa chave
  ssh_key_name = mgc_ssh_keys.key.name
}

# Mostrar IP da máquina como output do Terraform
output "ip" {
  value = mgc_virtual_machine_instances.factorio_server.network.public_address
}

Gabriel, por que estamos hardcodando um security group? Isso não vai contra a idéia?

Infelizmente, no momento, o provider de terraform do MGC não suporta security groups. Isso vai ser corrigido numa próxima release, e daí poderemos abrir nosso firewall também declarativamente.

Maravilha! Hora de deployar. Vamos começar autenticando via MGC CLI:

$ mgc auth login

E siga os passos na tela (lembre-se de escolher a organization SECOMP-UFSCar).

Feito isso, vamos preparar o terraform:

$ tofu init

E aplicar:

$ tofu apply

Digite yes, e aguarde um pouquinho.

Feito isso, é hora de validar que o servidor está okay e está acessível pela sua chave. O seu apply deve ter retornado o ip como output. Rode:

$ ssh debian@<IP DA VM>

Agora temos uma VM… Rodando Debian (por enquanto):

Vamos agora infectar essa querida com NixOS! Iremos utilizar o nixos-anywhere.

Instalando NixOS na VM :magic_wand:

A idéia do nixos-anywhere é iniciar um NixOS via kexec, desmontar o disco da máquina, re-particionar ele, e instalar NixOS de verdade (com a nossa configuração). Eles provêm um módulo de Terraform, que é perfeito para a gente!

Vamos precisar fazer alguns ajustes na nossa configuração de NixOS para comportar isso. O NixOS anywhere usa uma ferramenta chamada disko para particionar declarativamente. Vamos adicioná-la no nosso flake.nix:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    disko.url = "github:nix-community/disko/latest";
    disko.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { nixpkgs, disko, ... }: {
    nixosConfigurations.factorio-server = nixpkgs.lib.nixosSystem {
      modules = [
        ./configuration.nix
        disko.nixosModules.disko
      ];
    };
  };
}

Rode:

$ nix flake lock

Para registrar a mudança na flake.lock.

Nice. Agora vamos adicionar as configurações específicas da máquina (partições, módulos de kernel). Vamos fazer isso num arquivo separado da configuration.nix, para separar o “what it runs” e o “what it runs”.

Crie um arquivo hardware-configuration.nix:

{modulesPath, ...}: {
  imports = [(modulesPath + "/profiles/qemu-guest.nix")];

  boot = {
    initrd.availableKernelModules = ["ata_piix" "uhci_hcd"];
    kernelModules = ["kvm-intel"];
  };

  # Nossas partições
  disko.devices.disk.main = {
    device = "/dev/vda";
    type = "disk";
    content = {
      type = "gpt";
      partitions = {
        boot = {
          size = "1M";
          type = "EF02";
        };
        esp = {
          size = "512M";
          type = "EF00";
          content = {
            type = "filesystem";
            format = "vfat";
            mountpoint = "/boot";
          };
        };
        root = {
          size = "100%";
          content = {
            type = "filesystem";
            format = "ext4";
            mountpoint = "/";
          };
        };
      };
    };
  };
}

Não se preocupe muito se isso parece complexo. A maior parte desse arquivo foi gerado automaticamente. A parte do disko define quais nossas partições.

Precisamos importar esse arquivo da nossa configuration.nix. Também vamos tirar a senha 123456, e habilitar SSH. Edite ela:

{
  imports = [
    ./hardware-configuration.nix
  ];

  networking = {
    hostName = "factorio-server";
    useDHCP = true;
  };

  system.stateVersion = "24.05";
  nixpkgs = {
    hostPlatform = "x86_64-linux";
    config.allowUnfree = true;
  };

  services.factorio = {
    enable = true;
    openFirewall = true;
  };

  services.openssh = {
    enable = true;
    settings = {
      PermitRootLogin = "yes";
      PasswordAuthentication = false;
    };
  };

  users.users.root = {
    openssh.authorizedKeys.keys = [
      # Troque para sua chave.
      "<SUA CHAVE SSH PUBLICA>"
    ];
  };
}

Lembre-se de trocar a chave SSH para a sua.

Gabriel, por que temos a chave SSH em dois lugares?

A do terraform define qual vai ser a chave autorizada assim que a máquina é provisionada. Essa chave será usada pela instalação inicial do nixos-anywhere. A do NixOS é qual será a chave autorizada após a instalação (e nescessária para rebuilds).
No repositório (link no fim do post), movemos a chave para um arquivo separado.

Certo, agora vamos configurar o nixos-anywhere pelo terraform. Crie um nixos.tf:

module "deploy" {
  source = "github.com/nix-community/nixos-anywhere//terraform/all-in-one"
  nixos_system_attr = ".#nixosConfigurations.factorio-server.config.system.build.toplevel"
  nixos_partitioner_attr = ".#nixosConfigurations.factorio-server.config.system.build.diskoScript"
  debug_logging = true

  instance_id = mgc_virtual_machine_instances.factorio_server.id
  target_host = mgc_virtual_machine_instances.factorio_server.network.public_address
  install_user = "debian"
}

Adicionamos um novo módulo externo, então rode novamente o init:

$ tofu init

Certinho! Estamos prontos. Vamos aplicar a configuração do Terraform:

$ tofu apply

Aguarde alguns minutos (geralmente menos de 5). O NixOS anywhere irá instalar NixOS na VM, e aplicar nossa configuração!

Sempre que você modificar a configuração, basta dar apply novamente, ele irá detectar a mudança e fazer alterações na VM conforme nescessário.

Sucesso! Podemos usar o IP agora para jogar factorio:

Bonus: Backups no S3

Soon™

Fechamento

Espero que esse tutorial tenha ajudado você a ver algumas das coisas que são possíveis no modelo declarativo!

A magia da coisa é que qualquer um pode rodar tofu apply e ter um servidor exatamente igual. Fazendo as mudanças nos arquivos e rodando apply, você garante que não existe nenhum passo de setup (e.g. instale coisa X, altere arquivo Y) além de simplesmente ter os arquivos .tf e .nix.

O versão final está disponível no repositório: GitHub - Misterio77/hackathon-mgc-factorio-terraform

Feedback é muito bem vindo, e fico a disposição para qualquer dúvida!

Beijos,
Gab

2 curtidas