Tweag

Announcing Tf-Ncl 0.1

30 May 2023 — by Viktor Kleen

With Nickel releasing 1.0 I’m excited to announce the 0.1 release of Tf-Ncl, an experimental tool for writing Terraform deployments with Nickel instead of HCL.

Tf-Ncl enables configurations to be checked against Terraform provider-specific contracts, before calling Terraform to perform the deployment. Nickel can natively generate outputs as JSON, YAML or TOML; since Terraform can accept its deployment configuration as JSON, you can straightforwardly export a Nickel configuration, adhering to the right format, to Terraform. Tf-Ncl provides a framework for ensuring a Nickel configuration has this specific format. Specifically, Tf-Ncl is a tool to generate Nickel contracts that describe the configuration schema expected by a set of Terraform providers.

This approach means that Terraform doesn’t need to know or care that Nickel has generated its deployment configuration. State management is entirely unaffected. And deployments written with Nickel can instruct Terraform to use existing HCL modules, making it possible to migrate a configuration incrementally. You can start using Nickel’s programming features without committing to a complete rewrite of all your configuration at once. Having the full power of Nickel available makes it possible to describe the important parameters of your deployment in a format that suits your application while minimizing duplication. Then you can write Nickel code to generate the necessary Terraform resource definitions in all their complexity. For example, you could maintain a list of user accounts with associated data like team membership and admin status, and then generate appropriate Terraform resources setting up the referenced teams and their member accounts. Later in this post, I’ll show you how to achieve a simplified version of this.

Tf-Ncl is a tech demo to show what is possible with Nickel and should be considered experimental at this time. But we do hope to improve it and your feedback will be essential for that.

Trying It Out

The quickest and easiest way to set up an example project is to use Nix flakes:

nix flake init -t github:tweag/tf-ncl#hello-tf

This will leave you with two files in the current directory, flake.nix and main.ncl. The flake.nix file defines a Nix flake which provides a shell environment with: nickel, the Nickel CLI; nls, the Nickel language server; and topiary, Tweag’s Tree-sitter based formatter. It also contains shell scripts to link the generated Nickel contracts into the current directory and to call Terraform with the result of a Nickel evaluation. Enter the development shell environment with:

nix develop

Now you can evaluate the Nickel configuration in main.ncl using:

run-nickel

Calling run-nickel doesn’t perform any Terraform operations yet, it just evaluates the Nickel code in main.ncl to produce a JSON file main.tf.json. The latter can be understood by Terraform and is treated just like an HCL configuration would be. In the hello-tf example, the deployment consists of a single null_resource with a local-exec provisioner that just prints Hello, world!. Continuing with our example, you can now initialize Terraform and apply the Terraform deployment to get your greeting:

terraform init
terraform apply

You can also combine the Nickel evaluation with the call to Terraform using the run-terraform wrapper script:

run-terraform apply

Let’s take a look at this tiny example deployment. It is configured in main.ncl:

let Tf = import "./tf-ncl-schema.ncl" in
{
  config.resource.null_resource.hello-world = {
    provisioner.local-exec = [
      { command = "echo 'Hello, world!'" }
    ],
  },
} | Tf.Config

This Nickel code first imports the contracts generated by Tf-Ncl and binds them to the name Tf. Then it defines a record which contains the overall configuration and declares it to be a Terraform configuration using the syntax | Tf.Config. For this toy example the deployment consists of just a null_resource with an attached local provisioner, that greets everyone it sees.

Let’s try to use this scaffolding for writing an example deployment. Let’s say we want to take a list of GitHub user names and add those to our GitHub organization.

The first thing to do is to declare to Tf-Ncl that we want to use the github Terraform provider. This can be done by adjusting the flake.nix file. The outputs section of the flake defines a devShell using a Tf-Ncl provided function. This function is what we need to customize:

outputs = inputs: inputs.utils.lib.eachDefaultSystem (system:
  {
    devShell = inputs.tf-ncl.lib.${system}.mkDevShell {
      providers = p: {
        inherit (p) null;
      };
    };
  });

This is the place were you can specify which Terraform providers your deployment will need. These are also the providers for which Tf-Ncl will generate Nickel contracts. To have Tf-Ncl generate contracts for the GitHub Terraform provider as well as the Terraform internal null provider, you would replace the function passed as providers to the mkDevShell function, i.e.:

providers = p: {
  inherit (p) null github;
};

Having done that, you need to re-enter the development environment by exiting the current one and running nix develop again. Afterwards the wrapper scripts run-nickel and run-terraform will all use the new contracts including the GitHub provider. Now, let’s write some Nickel to turn a list of GitHub user names into Terraform resources. Start with the hello-tf scaffold, remove the null_resource and add the users list:

let Tf = import "./tf-ncl-schema.ncl" in
{
  users = [ "alice", "bob", "charlie" ],
  config = {
    provider.github = [
      {
        token = "<placeholder-token>", # Don't do this in production!
        owner = "<placeholder-organization>",
      }
    ],
  }
} | Tf.Config

I’ve also added a provider section that will tell the GitHub Terraform provider which organization it should manage. If you do this for real, don’t put an authorization token in the configuration directly. Rather, use Terraform variables or data sources to retrieve secrets. The next step will be to process the list of usernames into github_membership resource blocks for Terraform. For that, you can use Nickel’s standard library to map over the users array.

This will leave you with an array of records. But what’s needed is a single record containing all the fields. The Nickel library function std.record.merge_all provides that functionality. Nickel has the F# and OCaml inspired |> operator which makes writing these kinds of pipelined function application quite ergonomic. Here’s how to use it for defining memberships:

memberships =
  users
  |> std.array.map (fun user => {
    resource.github_membership."%{user}-membership" = {
      username = user,
      role = "member",
    },
  })
  |> std.record.merge_all

Finally, the resulting memberships record needs to be combined with the provider configuration in the field config. That can be done with Nickel’s merging operator &. In summary, here’s the deployment:

let Tf = import "./tf-ncl-schema.ncl" in
{
  users = [ "alice", "bob", "charlie" ],
  memberships = users
    |> std.array.map
      (fun user => {
        resource.github_membership."%{user}-membership" = {
          username = user,
          role = "member",
        }
      })
    |> std.record.merge_all,
  config = {
    provider.github = [{
      token = "<placeholder-token>",
      owner = "<placeholder-organization>",
    }],
  }
  & memberships,
} | Tf.Config

Try to have Terraform generate a plan for the deployment:

$ run-terraform plan

Terraform will perform the following actions:

  # github_membership.alice-membership will be created
  + resource "github_membership" "alice-membership" {
      + etag     = (known after apply)
      + id       = (known after apply)
      + role     = "member"
      + username = "alice"
    }

  # github_membership.bob-membership will be created
  + resource "github_membership" "bob-membership" {
      + etag     = (known after apply)
      + id       = (known after apply)
      + role     = "member"
      + username = "bob"
    }

  # github_membership.charlie-membership will be created
  + resource "github_membership" "charlie-membership" {
      + etag     = (known after apply)
      + id       = (known after apply)
      + role     = "member"
      + username = "charlie"
    }

Plan: 3 to add, 0 to change, 0 to destroy.

It works 🎉 You can take a look at the entire example in the Tf-Ncl repository or by using a Nix flake template:

$ nix flake init -t github:tweag/tf-ncl#github-simple

If you happen to have existing HCL modules, those can be included in the Nickel configuration for an incremental migration. For example, let’s say example-module/main.tf contains the following module:

variable "greeting" {
  type = string
}

resource "null_resource" "greeter" {
  provisioner local-exec {
      command = "echo ${var.greeting}"
  }
}

Then this can be included from the top-level main.ncl by modifying the config attribute to include an instruction to Terraform to instantiate the module with some parameters. That is, you could use the following:

{
  # [...]
  config = {
    # [...]
    module.greeter = {
      source = "./example-module",
      greeting = "Hello, world!",
    }
  }
  & memberships,
} | Tf.Config

Future Directions

At this point, Tf-Ncl should be considered a tech demo for Nickel. While it can produce working deployments for Terraform, there are various areas that still need improvement. For one, the generated contracts can be huge for featureful providers. While this is actually a great benchmark for Nickel’s evaluator, it can cause problems; for example, asking the Nickel language server for completion candidates may time out for very large contracts. I’m looking into changing the structure of the Tf-Ncl contracts to make them more modular and easier to process piecewise. There are also limitations to Tf-Ncl’s handling of provider computed fields for Terraform. But more on that in a coming deep dive blog post on the technical challenges of building Tf-Ncl.

Tf-Ncl is a new tool. Feedback is essential for improving it. Please try out Nickel and Tf-Ncl, find new uses, break it and, most importantly, tell us about it!

About the authors
Viktor KleenViktor is a mathematician turned software engineer. After almost a decade of thinking about algebraic geometry and homotopy theory, he joined Tweag in 2022 to work on more concrete problems.

If you enjoyed this article, you might be interested in joining the Tweag team.

This article is licensed under a Creative Commons Attribution 4.0 International license.

Company

AboutOpen SourceCareersContact Us

Connect with us

© 2024 Modus Create, LLC

Privacy PolicySitemap