Skip to content

Instantly share code, notes, and snippets.

@r33drichards
Created January 19, 2026 23:33
Show Gist options
  • Select an option

  • Save r33drichards/776b25f0b56c696c3e6a56dac45a77f9 to your computer and use it in GitHub Desktop.

Select an option

Save r33drichards/776b25f0b56c696c3e6a56dac45a77f9 to your computer and use it in GitHub Desktop.
Essential logic for creating Amazon AMIs from a NixOS Flake using nixos-generators

Creating Amazon AMIs from a NixOS Flake

This gist documents the essential logic for building Amazon AMIs using Nix flakes and nixos-generators.

Overview

The approach uses:

  • nixos-generators - Community tool for generating NixOS images in various formats
  • Nix flakes - For reproducible, declarative configuration
  • Comin - GitOps-style automatic configuration updates

Flake Inputs

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";

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

    flake-utils.url = "github:numtide/flake-utils";

    # Optional: GitOps for automatic config updates
    comin = {
      url = "github:nlewo/comin";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
}

AMI Builder Function

The core function that generates AMIs:

# In flake.nix outputs
makeAmazonAMI =
  { hostname, modules }:
  nixos-generators.nixosGenerate {
    inherit system;
    format = "amazon";
    modules = [
      # Import the standard NixOS Amazon image module
      (
        { modulesPath, ... }:
        {
          imports = [ "${modulesPath}/virtualisation/amazon-image.nix" ];
          virtualisation.diskSize = 16 * 1024;  # 16 GB disk
        }
      )

      # Base system configuration
      ./configuration.nix

      # Optional: GitOps support
      comin.nixosModules.comin

    ] ++ modules;  # Additional custom modules
  };

Defining AMI Packages

# In flake.nix outputs
packages = {
  # Simple AMI
  my-server = makeAmazonAMI {
    hostname = "my-server";
    modules = [ ];
  };

  # AMI with custom modules
  web-server = makeAmazonAMI {
    hostname = "web-server";
    modules = [
      ./modules/nginx.nix
      ./modules/certbot.nix
    ];
  };

  # AMI with inline configuration
  dev-box = makeAmazonAMI {
    hostname = "dev-box";
    modules = [
      ({ pkgs, ... }: {
        environment.systemPackages = with pkgs; [ vim git nodejs ];
        services.openssh.enable = true;
      })
    ];
  };
};

Base Configuration (configuration.nix)

{ pkgs, lib, ... }:

{
  # Enable flakes
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  nixpkgs.config.allowUnfree = lib.mkForce true;

  # Base packages
  environment.systemPackages = with pkgs; [
    git
    vim
    awscli2
  ];

  # Garbage collection
  nix.gc = {
    automatic = true;
    dates = "weekly";
    options = "--delete-older-than 7d";
  };

  # SSH configuration
  services.openssh = {
    enable = true;
    settings.PasswordAuthentication = false;
    settings.KbdInteractiveAuthentication = false;
  };

  # User account
  users.users.admin = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    openssh.authorizedKeys.keys = [
      "ssh-ed25519 AAAA... your-key-here"
    ];
  };

  # Allow sudo without password for wheel group
  security.sudo.wheelNeedsPassword = false;
}

Building the AMI

# Build a specific AMI target
nix build .#my-server

# The result is a raw disk image at:
# result/nixos-amazon-image-*.raw

Importing to AWS

1. Set up VM Import Role

Create an IAM role for the VM Import service:

# create-vmimport.sh
ROLE_NAME="vmimport"
BUCKET_NAME="your-ami-bucket"

# Create trust policy
cat > trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "vmie.amazonaws.com" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:Externalid": "vmimport" }
      }
    }
  ]
}
EOF

# Create role
aws iam create-role --role-name $ROLE_NAME --assume-role-policy-document file://trust-policy.json

# Attach policy for S3 and EC2 access
cat > role-policy.json << EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::${BUCKET_NAME}",
        "arn:aws:s3:::${BUCKET_NAME}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:ModifySnapshotAttribute",
        "ec2:CopySnapshot",
        "ec2:RegisterImage",
        "ec2:Describe*"
      ],
      "Resource": "*"
    }
  ]
}
EOF

aws iam put-role-policy --role-name $ROLE_NAME --policy-name $ROLE_NAME --policy-document file://role-policy.json

2. Upload and Import

# Upload raw image to S3
aws s3 cp result/nixos-amazon-image-*.raw s3://your-ami-bucket/

# Create import task
aws ec2 import-image \
  --description "NixOS AMI" \
  --disk-containers "Format=RAW,UserBucket={S3Bucket=your-ami-bucket,S3Key=nixos-amazon-image-XXXX.raw}"

# Check import status
aws ec2 describe-import-image-tasks --import-task-ids import-ami-XXXX

Complete Minimal Flake Example

# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
    nixos-generators = {
      url = "github:nix-community/nixos-generators";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, nixos-generators, flake-utils }:
    flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system:
      let
        pkgs = import nixpkgs { inherit system; };

        makeAmazonAMI = { hostname, modules ? [] }:
          nixos-generators.nixosGenerate {
            inherit system;
            format = "amazon";
            modules = [
              ({ modulesPath, ... }: {
                imports = [ "${modulesPath}/virtualisation/amazon-image.nix" ];
                virtualisation.diskSize = 16 * 1024;
              })
              ({ pkgs, ... }: {
                nix.settings.experimental-features = [ "nix-command" "flakes" ];
                environment.systemPackages = with pkgs; [ git vim ];
                services.openssh.enable = true;
                services.openssh.settings.PasswordAuthentication = false;
                users.users.admin = {
                  isNormalUser = true;
                  extraGroups = [ "wheel" ];
                  openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAA..." ];
                };
                security.sudo.wheelNeedsPassword = false;
              })
            ] ++ modules;
          };
      in
      {
        packages = {
          default = makeAmazonAMI { hostname = "nixos-server"; };

          web-server = makeAmazonAMI {
            hostname = "web-server";
            modules = [
              ({ pkgs, ... }: {
                services.nginx.enable = true;
                networking.firewall.allowedTCPPorts = [ 80 443 ];
              })
            ];
          };
        };
      }
    );
}

Optional: GitOps with Comin

For automatic configuration updates from a Git repository:

# flake-bootstrapper.nix
{ config, lib, pkgs, ... }:

{
  options.services.flake-bootstrapper = {
    enable = lib.mkEnableOption "Enable flake-bootstrapper";
    githubRepo = lib.mkOption {
      type = lib.types.str;
      description = "GitHub repository URL";
    };
    hostname = lib.mkOption {
      type = lib.types.str;
      description = "Hostname for comin";
    };
  };

  config = lib.mkIf config.services.flake-bootstrapper.enable {
    services.comin = {
      enable = true;
      hostname = config.services.flake-bootstrapper.hostname;
      remotes = [{
        name = "origin";
        url = config.services.flake-bootstrapper.githubRepo;
        branches.main.name = "master";
        poller.period = 60;  # Check every 60 seconds
      }];
    };
  };
}

Key Points

  1. nixos-generators handles the heavy lifting of creating bootable AMI images
  2. The amazon-image.nix module from nixpkgs provides EC2-specific configuration
  3. Build produces a raw disk image that must be imported via AWS VM Import/Export
  4. Modular design allows easy composition of different server configurations
  5. Optional GitOps integration enables automatic updates without rebuilding AMIs

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment