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.
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.
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.
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):
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:
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:
Our user from part 1
and then referred to the user that will be created when defining the access key like so:
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:
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