A Proposal to Blend Infrastructure as Code

The world of infrastructure as code (IaC) is broadly divided into two areas of responsibility: provisioning and configuring. Terraform, OpenTofu, ARM templates, CloudFormation, etc. are used for provisioning duties. Ansible, Puppet, Chef, Salt Project, etc. are used for configuration.

This proposal focuses on Terraform (OpenTofu) and Ansible, with the idea to blend these into a single platform.

Why would one want to blend Terraform and Ansible? It gives us the best of worlds as teams need to invest in a single platform that covers the spectrum of their needs. While they integrate well enough today, wouldn't it be better if they were one?

What would it look like? To begin with, Hashicorp Configuration Language (HCL) should be adopted. I had a tough time, at the beginning, to wrap my head around why I would learn a domain specific language (DSL) (as used by Terraform) over an actual programming language (as used by Vagrant or Chef). A few years ago HCL lacked a lot of the features it has today. With time, though, I have seen the beauty a DSL brings to the IaC ecosystem. Most of our work at the IaC level is declarative, which a DSL can represent better than an imperative programming language. On this blended platform, both provisioning and configuration would use HCL.

What about Pulumi? I think it's a valuable alternative for people who need the power of an imperative language for their IaC. But HCL feels a better fit for the IaC domain so I prefer it.

Terraform does a great job at being declarative. It isn't perfect but it does the job. Ansible roles feel imperative when they deal with complicated scenarios. The modules may provide a declarative interface but the order in which they are called through the YAML DSL is still imperative. HCL feels declarative and by using that instead of a list of YAML-based tasks, it can nudge and encourage to write more declarative code.

This platform must stay idempotent. Terraform does this by maintaining a state file and Ansible does it by running a lot of live checks. Terraform incurs the expense of keeping the actual state aligned with desired state. Who hasn't run into scenarios where the only fix is to manually alter the state file to get out of a tricky corner? I won't claim to know better than the teams building Terraform and Pulumi with their preference to maintain a state file. But there has to be a way to follow the Ansible route and be idempotent without maintaining a state file.

The platform must be declarative and idempotent. Ansible modules may have to be modified to act more like Terraform providers. A lot more of the work done by Ansible roles may have to be implemented as opinionated "providers". This is the trickiest part. One person's Ansible role has different requirements than the next person's, even if they are similar at their core. This will require a lot of thought and work.

By blending the IaC responsibilities, we can get a single platform that uses a single language to express our intent in a declarative and idempotent way. This would reduce a lot of the daily operations burden that teams currently face in the IaC world. Wouldn't you love to write and read something like,

data "linode_sshkey" "amla" {
    label = "amla"
}


resource "linode_instance" "alma" {
    label  = "alma"
    image  = "linode/almalinux9"
    region = "us-west"
    type   = "g6-nanode-1"

    authorized_keys = [data.linode_sshkey.amla.ssh_key]
    root_pass       = var.root_password

    tags = [
        "alma",
    ]
}

host "example_inventory" "alma" {
    vars = {
        prom_node_exp_domain_name = "${linode_instance.alma.label}.example.com"
    }
    roles = [
        "${role.example_provider.websites}",
        "${role.example_provider.nginx-site-config}",
        "${role.example_provider.prometheus-node-exporter}",
    ]
}

hostgroup "example_inventory" "alma" {
    hosts = [
        "${host.example_inventory.alma}"
    ]
    vars = {}
    roles = [
        "${role.example_provider.websites}",
        "${role.example_provider.nginx-site-config}",
        "${role.example_provider.prometheus-node-exporter}",
    ]
}

role "example_provider" "websites" {
    vars = {
        contents_path = "/Users/aikchar/index.html"
        domain_name = "${prom_node_exp_domain_name}"
    }
}

role "example_provider" "nginx-site-config" {
    after = [
        "${role.example_provider.websites}"
    ]
    vars = {}
}

role "example_provider" "prometheus-node-exporter" {
    after = [
        "${role.example_provider.nginx-site-config}"
    ]
    vars = {}
}