Skip to content

Proposal: Generator Plugins for Configuration Generation #3310

Closed
@apparentlymart

Description

@apparentlymart

So far in my use of Terraform in my workplace I've uncovered a number of use-cases where it's useful to generate Terraform configurations based on outside data.

So far I've been handling that by having users run extra preparation steps before they run terraform plan, which write JSON-style Terraform configs into a directory. This has worked okay, but the need for extra steps means that the standard usage pattern of Terraform does not apply.

Based on what I've learned from generating Terraform configs, I'd like to offer a proposal for integrating the concept of configuration generation directly into Terraform. This is an attempt to "pave the cowpath" by taking a pattern that I've already successfully applied and building Terraform syntax around it.

Consider the following configuration as a motivating example:

// Load a directory full of files into an S3 bucket
resource "aws_s3_bucket_object" "file" {
    foreach "dir_entry" {
        path = "${path.module}/htdocs"
        recursive = true
        include_dirs = false
    }

    bucket = "${var.s3_bucket_name}"
    key = "${foreach.relative_path}"
    source = "${foreach.absolute_path}"
}

(This example is inspired by my terraform-s3-dir utility.)

The resource construct is extended with a new child block foreach, which is conceptually similar to count but rather than producing resources based on a sequence of numbers it produces resources based on executing a generator plugin, which in this example is dir_entry.

The contract for a generator plugin is to take the given input arguments and produce (essentially) a map[string]interface{}, where each map key is a hashable unique identifier for an object and the value is a structure that can be used from the ${foreach...} interpolation syntax.

Some details in the sections that follow...

Effect on terraform plan

As noted earlier, foreach is conceptually similar to count in that it causes one resource configuration block to produce multiple resource instances. In the case of count these are named like aws_s3_bucket_object.file.0. In the case offoreacha similar convention applies except that the index is replaced by the result of applying`hashcode.String`` to each item's key.

Since the resources are identified by a hash of their key, diffing can produce a sensible result as long as the keys remain consistent between runs. In the dir_entry example above the object key could be the same as the relative_path attribute value, so adding and removing files would cause the corresponding resource instances to be added and removed in the diff.

foreach configuration block

Since generation is a plan-time concept, the foreach block may only contain interpolations that are known statically, such as var, path, interpolation functions. It specifically may not reference resource or module attributes.

Otherwise the structure of the configuration block is under the control of the generator plugin, much as with provisioners.

${foreach...} interpolation syntax

The ${foreach...} interpolation syntax is again comparable to the ${count...} syntax, but the attributes within it depend on the structure returned by the generator function. Each generator is free to define its own set of attributes in an arbitrary hierarchical structure, just like resources can.

Generator plugin interface

Generator plugins have a similar interface to provisioner plugins, including the same configuration validation methods but with the Apply method replaced with Generate(*ResourceConfig) map[string]interface{} .

It is expected that a generator will produce tens of items at most, so returning the whole structure in-memory (rather than streaming it e.g. using a goroutine) should be sufficient. A generator producing hundreds or thousands of items would result in hundreds or thousands of Terraform resources, which I believe is already beyond Terraform's design assumptions.

Whereas provisioners are verbs, generators should be named as nouns describing what kind of items the generator produces, so that the declaration reads as (for example) "For each directory entry...".

Interaction with count

To avoid the combinatoric complexity that would result, using count and foreach together in the same resource block is not permitted.

Additional Example Use-cases

Some further examples of generators plugins that might be implemented, and what they could be used for...

DNS records from a standard zone file

resource "aws_route53_record" "foo" {
    foreach "dns_zone_record" {
        // A "zone file" per RFC1035
        zone = "${file('example.com.zone')}"
        // Disregard SOA records
        ignore_types = ["SOA"]
    }

    zone_id = "${var.route53_zone_id}"
    name = "${foreach.name}"
    type = "${foreach.type}"
    records = ["${foreach.records}"]
    ttl = "${foreach.ttl}"
}

Users (or indeed anything else) from a YAML file

resource "aws_iam_user" "user" {
    foreach "yaml_entry" {
        // Give a YAML document that either has a mapping or a list at its root,
        // to produce one resource instance per entry in that structure.
        source = "${file('users.yaml')}"
    }

    name = "${foreach.username}"
    path = "/staff/"
}

A custom external program for generating VPN routes

resource "aws_vpn_connection_route" "route" {
    foreach "local_exec_result" {
        // Any executable that produces a JSON object as output and
        // exits with a successful status.
        command = "${path.module}/generate-route-map"
    }

    destination_cidr_block = "${foreach.cidr_block}"
    vpn_connection_id = "${aws_vpn_connection.main.id}"
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions