Data-driven Terraform: Terraform in Anger Part 2

David Layton

David / 28 May 2020

Here, in part 2, our goal is maintainability. We'll take a data-driven approach. Adding new users will be mere data entry. Along the way, we'll split our project into multiple files, logically. That'll make it easier to add more functionality in part 3 and beyond.

In part 1, we covered the basics. It was a great start but still didn't feel like a Real Projectâ„¢. Let's fix that. To do this, we'll employ some of Terraform's more advanced features--namely functions, variables, and [count](https://www.terraform.io/docs/configuration/resources.html#count-multiple-resource-instances-by-count). Oh my!

Data-driven Terraform

In Terraform, you can assign variables in their own files. They can be in HCL or JSON. Here I've elected to use HCL, but keep in mind that you can easily generate JSON programmatically.

View this gist on GitHub

This data will drive what gets created

Let's use the above as the data that drives what Terraform creates for our client.

WhY Copy & Paste?
Download The Complete Example Code
For The Whole Series Now

We also need to declare these variables as inputs.

View this gist on GitHub

Declaring our data as inputs

Now we can access this data to drive what gets created. We'll start slow with the bucket.

Data-driven Bucket

We'll start by placing our previous bucket definition in a new file called client_bucket.tf. However, let's go ahead and make a couple of changes. For starters, we will data-drive the bucket region. Secondly, we'll use our first functions, [sha1](https://www.terraform.io/docs/configuration/functions/sha1.html) and [format](https://www.terraform.io/docs/configuration/functions/format.html), to create a bucket name that doesn't expose who our client is to the rest of the world.

View this gist on GitHub

A bucket for our client

This file is a great place to keep our bucket's aws_iam_policy_document as well--it only makes sense in the context of this bucket.

Okay, functions and variables are neat, but let's do something really special. Let's use one simple block to create an arbitrary number of resources.

Data-driven IAM Users

We need to make users for Alice, Bob, and Charlie.

In part 1, we took the approach of attaching our bucket policy directly to our user just to keep things simple. Here we strive for maintainability--and that means making the concepts a little clearer. So here, we will create a group, create the users, and then assign the user to the groups (using [aws_iam_group_membership](https://www.terraform.io/docs/providers/aws/r/iam_group_membership.html)). We will also need to create an access key for each of the users.

The group is simple (and boring):

View this gist on GitHub

Our group

However, since we are being data-driven, and have self-respect, we can't just copy-pasta three sets of resources. Fortunately, we have some options here, but we'll start out using Terraform's [count](https://www.terraform.io/docs/configuration/resources.html#count-multiple-resource-instances-by-count) for now. We'll look at more elegant solutions later in the series.

Putting all this into a new file:

View this gist on GitHub

Users, keys, and group membership

If you terrafrom apply now, you'll see that Terraform creates 3 users and 3 access keys.

dataunbound$ terraform apply --var-file client.tfvars ... aws_iam_group.client: Creating... ... aws_s3_bucket.client_bucket: Creating... aws_iam_group.client: Creation complete after 0s [id=test-client] aws_iam_user.client[1]: Creation complete after 1s [id=bob] aws_iam_user.client[2]: Creation complete after 1s [id=charlie] aws_iam_user.client[0]: Creation complete after 1s [id=alice] aws_iam_group_membership.client: Creating... ... aws_iam_access_key.client[1]: Creation complete after 0s aws_iam_access_key.client[0]: Creation complete after 0s aws_iam_access_key.client[2]: Creation complete after 0s aws_iam_group_membership.client: Creation complete after 2s aws_s3_bucket.client_bucket: Creation complete after 3s data.aws_iam_policy_document.client_bucket: Refreshing state...

Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

What's going on here?

Although this looks straightforward at first glance, it's worth taking a few moments to understand what's really going on here. Yes, clearly the blocks with count create a resource count number of times, and the values of count.index are 0, 1, and 2.

On line 7, we used the function [element](https://www.terraform.io/docs/configuration/functions/element.html) to grab the first, second or third element of the list of users associated with our client, var.client.users.

But what is happening on lines 12, element(aws_iam_user.client, count.index).name ?

Well, if we define a resource, we can later refer to that resource by its resource type and name. We did this in part 1. When we created our user like so:

View this gist on GitHub

Our user from part 1

and then referred to the user that will be created when defining the access key like so:

View this gist on GitHub

Our access key from part 1

The difference here is that in part 1 aws_iam_user.test_client referred to a single resource. By using count, we're asking Terraform to create a list of resources. Even if count were equal to 1, or 0, aws_iam_user.client would refer to a list.

What about line 18, aws_iam_user.client[*].name?

Terraform's Splat Expressions

Here we are doing something similar but using a completely different syntax. If you are familiar with XPath or JsonPath this notation will be familiar to you.

Regardless, element(aws_iam_user.client, count.index).name returns one name, the one at position count.index.

Conversely, aws_iam_user.client[*].name returns a list, specifically ["Alice", "Bob", "Charlie"].

The Benefits of Being Declarative

You may be wondering what happens when the created resources are defined in another file or how to sequence your code to ensure the resources exists before you refer to them. If so, your forgetting that Terraform is declarative--you're not writing programmatic code.

In Terraform, you only define what you want, not how to get it. When you use terraform plan or terraform apply, terraform will scan through all of the files. When it encounters a reference to a resource it will note the dependency. The plan it creates will reflect that dependency and inform the order in which resources are created.

Putting It All together

The last thing we need to do is give access to the bucket. Using aws_iam_group_policy, AWS will associate our policy from part 1 to the group:

View this gist on GitHub

Group policy

Finally, we can run terraform apply and give the group access:

dataunbound$ terraform apply --var-file client.tfvars Terraform will perform the following actions:

aws_iam_group_policy.client_bucket will be created

  • resource "aws_iam_group_policy" "client_bucket" {
    • group = "test-client"
    • id = (known after apply)
    • name = "test-client-bucket=policy"
    • policy = jsonencode( ... ) }

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

What's Next

Stay tuned by subscribing and you'll get notified immediately when part 3 is available. In Part 3, we'll look at configuring the users with other access types, controlling our outputs, and adding more security.

Can't Wait?
Download The Complete Example Code
For The Whole Series Now