Terraform in Nix


Here is a technique for managing terraform environments via nix.

This approach provides a guaranteed terraform and provider dependency version match which may be used as reproducible and convenient development environment.

Additionally, using the nix build system, we may generate oci images from this environment, allowing for stronger integrity guarantees between local testing and remote runs.

Background

We ‘ll denote a terraform environment as a fixed composition of the terraform binary and the source dependencies (i.e. modules) and providers required during runtime.

Such a composition is commonly achieved via a custom Dockerfile, through the use of a pre-made image or an auxiliary utility such as tfenv, tgswitch or alike.

It is desirable and often necessary to guarantee a reproducible composition of the above in order to provide accurate and error free infrastructure management.

Approach

We define a default.nix file for our terraform environment which includes the terraform binary itself, a set of several providers addons and two targets of terraformShell and terraformImage.

{ pkgs }:
let
  tf = pkgs.terraform.withPlugins(plugin: [
    plugin.aws
    plugin.tls
    plugin.cloudinit
    plugin.kubernetes
    plugin.helm
    plugin.time
    plugin.kubectl
  ]);

  terraformShell = pkgs.mkShell rec {
      buildInputs = [ tf ];
      shellHook = ''
        echo "[...] hello world"
        terraform version
        '';
      };
      
  terraformImage = pkgs.dockerTools.buildImage {
    name = "example-tf-image";
    tag  = "latest";
    copyToRoot = [ tf ];
    config     = {
      Cmd = [ "${tf}/bin/terraform" ];
    };
  };
in
{
  inherit terraformShell; 
  inherit terraformImage;
}

default.nix

Usage

Creating a local shell environment is like so.

$ nix develop .\#terraformShell
[...] hello world
Terraform v1.6.4-dev
on linux_amd64

$ which terraform
/nix/store/5fkgbf281sidcxqad1ia9xkyfnrrn3ci-terraform-1.6.4/bin/terraform  

Creating an image from this environment is like so.

$ nix build .\#images.x86_64-linux.terraformImage
$ docker load < result
Loaded image: example-tf-image:latest

The resulting image is optimal in size, only the required dependencies are baked in akin to google/distroless.

See #image-composition.

Flakes

Next, we define a flake.nix in order to pin the build recipes (i.e. nix-derivations) of our environment above.

The flake will write to a flake.lock which will reference a specific commit in github:nixpkgs. This in effect pins all dependencies required to recreate this environment recursively from source.

This complete example shows handling of terraform’s BSL license and flake-utils, a helper for multi architecture support.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
    flake-utils.url = "github:numtide/flake-utils/v1.0.0";
  };
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
    let
      pkgs = import nixpkgs {
        inherit system;
        config = {
          allowUnfreePredicate = pkg: builtins.elem(nixpkgs.lib.getName pkg)[
            "terraform"
          ];
        };
      };

      defaultPackage = import ./default.nix { inherit pkgs; };
    in {
      devShells = {
        terraformShell = defaultPackage.terraformShell;
      };

      images = {
        terraformImage = defaultPackage.terraformImage;
      };
    });
}

flake.nix

The resulting lockfile.

    "nixpkgs": {
      "locked": {
        "lastModified": 1709309926,
        "narHash": "sha256-VZFBtXGVD9LWTecGi6eXrE0hJ/mVB3zGUlHImUs2Qak=",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "79baff8812a0d68e24a836df0a364c678089e2c7",
        "type": "github"
      },
      "original": {
        "owner": "NixOS",
        "ref": "nixos-23.11",
        "repo": "nixpkgs",
        "type": "github"
      }
    },

snippet of a flake.lock

Dependency Resolution in Nix

During execution nix will attempt to fetch pre-compiled binaries from cache.nixos.org, this is an S3 bucket with a clever composition.

Image Composition

Users new to nix may find it insightful to explore the resulting OCI image. All binaries and dependencies deterministically resolved from /nix/store, the runtime is composed of symbolic links and PATH manipulation.

├── bin
│   └── terraform
├── libexec
│   └── terraform-providers
│       └── registry.terraform.io
│           ├── gavinbunney
│           │   └── kubectl
│           │       └── 1.14.0
│           │           └── linux_amd64
│           │               └── terraform-provider-kubectl_1.14.0
│           └── hashicorp
│               ├── aws
│               │   └── 5.25.0
│               │       └── linux_amd64
│               │           └── terraform-provider-aws_5.25.0
│               ├── cloudinit
│               │   └── 2.3.2
│               │       └── linux_amd64
│               │           └── terraform-provider-cloudinit_2.3.2
│               ├── helm
│               │   └── 2.11.0
│               │       └── linux_amd64
│               │           └── terraform-provider-helm_2.11.0
│               ├── kubernetes
│               │   └── 2.23.0
│               │       └── linux_amd64
│               │           └── terraform-provider-kubernetes_2.23.0
│               ├── time
│               │   └── 0.9.1
│               │       └── linux_amd64
│               │           └── terraform-provider-time_0.9.1
│               └── tls
│                   └── 4.0.4
│                       └── linux_amd64
│                           └── terraform-provider-tls_4.0.4
└── nix
    └── store
        ├── 0iwvi1hmv7agm3hb53qifd5053z85fpn-terraform-provider-kubernetes-2.23.0
        │   └── libexec
        │       └── terraform-providers
        │           └── registry.terraform.io
        │               └── hashicorp
        │                   └── kubernetes
        │                       └── 2.23.0
        │                           └── linux_amd64
        │                               └── terraform-provider-kubernetes_2.23.0
        ├─⊕ 1zy01hjzwvvia6h9dq5xar88v77fgh9x-glibc-2.38-44
        ├─⊕ 29691038dnsk07w5jr32rw6vsnmarcb5-acl-2.3.1
        ├─⊕ 33h05bypn4cjp3854l4bsd9zdby59imj-iana-etc-20230316
        ├─⊕ 3dfyf6lyg6rvlslvik5116pnjbv57sn0-libunistring-1.1
        ├─⊕ 5fkgbf281sidcxqad1ia9xkyfnrrn3ci-terraform-1.6.4
        ├─⊕ a3n1vq6fxkpk5jv4wmqa1kpd3jzqhml9-libidn2-2.3.4
        ├─⊕ a3zlvnswi1p8cg7i9w4lpnvaankc7dxx-gcc-12.3.0-lib
        ├─⊕ dcnpmf4l1r19snijwirrmcvhwzrgy1dx-terraform-provider-kubectl-1.14.0
        ├─⊕ hcpzsz292pidl02ig5rb1583apcanhj6-mailcap-2.1.53
        ├─⊕ i6nk8llh46f2xjzc5h8j83kwwr1w3kx0-tzdata-2024a
        ├─⊕ j6n6ky7pidajcc3aaisd5qpni1w1rmya-xgcc-12.3.0-libgcc
        ├─⊕ j7mwvhhrzg0n6wald3g4c1pyjf02di1q-terraform-provider-cloudinit-2.3.2
        ├─⊕ jc4j7srg3jd8063p8gn4ib7gp51sb5iy-terraform-provider-helm-2.11.0
        ├─⊕ l0ydz31lwa97zickpsxj2vmprcigh1m4-gcc-12.3.0-libgcc
        ├─⊕ l32763bzsl8vi889gd0yfg56cac1d967-terraform-1.6.4
        ├─⊕ n9h29184cgybwpx8jl5gvsx8g367pksa-attr-2.5.1
        ├─⊕ p3zhf82f9i2bd7yzy258d9xq5bik8nmk-gmp-with-cxx-6.3.0
        ├─⊕ r9h133c9m8f6jnlsqzwf89zg9w0w78s8-bash-5.2-p15
        ├─⊕ rk067yylvhyb7a360n8k1ps4lb4xsbl3-coreutils-9.3
        ├─⊕ s8c8a8cnypslx9pdfqmijsyjq7dih8bg-terraform-provider-tls-4.0.4
        ├─⊕ vb6w8s0051qqyc3s0lpcx8w1jmysypz9-terraform-provider-time-0.9.1
        └─⊕ w3lb5crpygn59jv43bna7vharrj30zjr-terraform-provider-aws-5.25.0