Customize modules with object attributes
Terraform modules let you organize and re-use Terraform configuration. They make your infrastructure deployments consistent and help your team adhere to your organization's best practices. Input variables let module users customize attributes of the module. You can define module attributes using strings, numbers, booleans, lists, maps, and objects.
Object type attributes contain a fixed set of named values of different types. Using objects in your modules lets you group related attributes together, making it easier for users to understand how to use your module. You can make attributes within objects optional, which make it easier for you to ship new module versions without changing the variables that module users need to define.
In this tutorial, you will refactor a module to use objects for some of its attributes.
Prerequisites
You can complete this tutorial using the same workflow with either Terraform Community Edition or HCP Terraform. HCP Terraform is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.
Select the Terraform Community Edition tab to complete this tutorial using Terraform Community Edition.
This tutorial assumes that you are familiar with the Terraform and HCP Terraform workflows. If you are new to Terraform, complete the Get Started collection first. If you are new to HCP Terraform, complete the HCP Terraform Get Started tutorials first.
For this tutorial, you will need:
- Terraform v1.3+ installed locally.
- an HCP Terraform account and organization.
- HCP Terraform locally authenticated.
- the AWS CLI.
- an HCP Terraform variable set configured with your AWS credentials.
Clone the example repository
Clone the example repository for this tutorial, which contains Terraform configuration for an AWS S3 bucket configured to host a static website.
$ git clone https://github.com/hashicorp-education/learn-terraform-module-object-attributes
Change into the repository directory.
$ cd learn-terraform-module-object-attributes
Review example configuration
The example configuration uses a local module to provision an AWS S3 bucket
configured to host a static website. The modules/aws-s3-static-website
directory contains the module definition, while the configuration that uses it
is in main.tf
in the repository's root directory.
Open modules/aws-s3-static-website/main.tf
to review the module configuration.
This module includes resources that manage the files your website will serve and
how it will respond to requests.
The configuration sets the index and error document for your website.
modules/aws-s3-static-website/main.tf
resource "aws_s3_bucket_website_configuration" "web" {
bucket = aws_s3_bucket.web.id
index_document {
suffix = var.index_document_suffix
}
error_document {
key = var.error_document_key
}
}
Note
S3 stores data as objects, which are roughly equivalent to files on a local system. Web servers and browsers often use the term "document". For simplicity, this tutorial uses the generic term "file".
The configuration uses the hashicorp/dir/template
public module to
render the files that Terraform will upload to the S3 bucket. Terraform will
load these files from the path specified in the www_path
variable, or from the
modules/aws-s3-static-website/www
directory if the variable is not set.
modules/aws-s3-static-website/main.tf
module "template_files" {
source = "hashicorp/dir/template"
version = "1.0.2"
base_dir = var.www_path != null ? var.www_path : "${path.module}/www"
}
Open modules/aws-s3-static-website/variables.tf
to review the input variables
that configure this module's attributes.
modules/aws-s3-static-website/variables.tf
variable "bucket_name" {
description = "Name of the s3 bucket. Must be unique. Conflicts with `bucket_prefix`."
type = string
default = null
}
variable "bucket_prefix" {
description = "Prefix for the s3 bucket name. Conflicts with `bucket_name`."
type = string
default = null
}
variable "tags" {
description = "Map of tags to set on the website bucket."
type = map(string)
default = {}
}
variable "index_document_suffix" {
description = "Suffix for index documents."
type = string
default = "index.html"
}
variable "error_document_key" {
description = "Key for error document."
type = string
default = "error.html"
}
variable "www_path" {
description = "Local absolute or relative path containing files to upload to website bucket."
type = string
default = null
}
variable "terraform_managed_files" {
description = "Flag to indicate whether Terraform should upload files to the bucket."
type = bool
default = true
}
This configuration lets users specify a bucket name, or a prefix that Terraform will use to generate a unique name. It also allows users to define tags for their buckets. Finally, it includes several variables that allow them to configure the files in their bucket:
- the
index_document_suffix
anderror_document_key
variables control which files the website will use for its index and error documents, respectively. - the
www_path
variable allows users to specify a path from which to load the files for the website. - the
terraform_managed_files
variable is a flag that allows users to manage files outside of Terraform.
Open main.tf
to review the initial configuration, which uses the
aws-s3-static-website
module to provision an S3 bucket and related resources.
main.tf
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website"
bucket_prefix = "module-object-attributes-"
tags = {
terraform = "true"
environment = "dev"
public-bucket = true
}
}
Apply configuration
Set the TF_CLOUD_ORGANIZATION
environment variable to your HCP Terraform
organization name. This will configure your HCP Terraform integration.
$ export TF_CLOUD_ORGANIZATION=
Initialize your configuration. Terraform will automatically create the
learn-terraform-module-object-attributes
workspace in your HCP Terraform
organization.
$ terraform init
Initializing modules...
- website_s3_bucket in modules/aws-s3-static-website
Downloading registry.terraform.io/hashicorp/dir/template 1.0.2 for website_s3_bucket.template_files...
- website_s3_bucket.template_files in .terraform/modules/website_s3_bucket.template_files
Initializing HCP Terraform...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v4.30.0...
- Installed hashicorp/aws v4.30.0 (signed by HashiCorp)
HCP Terraform has been successfully initialized!
You may now begin working with HCP Terraform. Try running "terraform plan" to
see any changes that are required for your infrastructure.
If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.
Note
This tutorial assumes that you are using a tutorial-specific HCP Terraform organization with a global variable set of your AWS credentials. Review the Create a Credential Variable Set for detailed guidance. If you are using a scoped variable set, assign it to your new workspace now.
Apply the configuration. Respond to the confirmation prompt with a yes
to
create your resources.
$ terraform apply
Running apply in HCP Terraform. Output will stream here. Pressing Ctrl-C
will cancel the remote apply if it's still pending. If the apply started it
will stop streaming the logs, but will not stop the apply running remotely.
Preparing the remote apply...
To view this run in a browser, visit:
https://app.terraform.io/app/organization-name/learn-terraform-module-object-attributes/runs/run-aSHUhRhskyPshv2g
Waiting for the plan to start...
Terraform v1.3.0
on linux_amd64
Initializing plugins and modules...
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
##...
Plan: 6 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ website_bucket_arn = (known after apply)
+ website_bucket_domain = (known after apply)
+ website_bucket_endpoint = (known after apply)
+ website_bucket_name = (known after apply)
Do you want to perform these actions in workspace "learn-terraform-module-object-attributes"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
##...
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
website_bucket_arn = "arn:aws:s3:::module-object-attributes-20220920185307968400000001"
website_bucket_domain = "s3-website-us-west-2.amazonaws.com"
website_bucket_endpoint = "module-object-attributes-20220920185307968400000001.s3-website-us-west-2.amazonaws.com"
website_bucket_name = "module-object-attributes-20220920185307968400000001"
Visit the domain in the website_bucket_endpoint
output value to confirm that
your website responds with "Nothing to see here."
Refactor module with object attribute
Open modules/aws-s3-static-website/variables.tf
and delete the four
variables relating to files: index_document_suffix
, error_document_key
,
www_path
, and terraform_managed_files
.
modules/aws-s3-static-website/variables.tf
variable "index_document_suffix" {
description = "Suffix for index documents."
type = string
default = "index.html"
}
##...
variable "terraform_managed_files" {
description = "Flag to indicate whether Terraform should upload files to the bucket."
type = bool
default = true
}
variable "www_path" {
description = "Local absolute or relative path containing files to upload to website bucket."
type = string
default = null
}
variable "terraform_managed_files" {
description = "Flag to indicate whether Terraform should upload files to the bucket."
type = bool
default = true
}
Replace these variables with a new variable that captures all file-related options.
modules/aws-s3-static-website/variables.tf
variable "files" {
description = "Configuration for website files."
type = object({
terraform_managed = bool
error_document_key = optional(string, "error.html")
index_document_suffix = optional(string, "index.html")
www_path = optional(string)
})
}
The files
variable defines an object with fields corresponding to the
variables you removed. Since it does not set a default value, it is required
whenever practitioners use your module. Objects map a specific set of named keys
to values. Keeping related attributes in a single object helps your users
understand how to use your module.
The terraform_managed
field is required, while the other three are optional.
Both error_document_key
and index_document_suffix
fields configure default
values for the attributes after specifying that they are optional. Since no
default value is set for www_path
, Terraform will set it to null
, unless the
module user specifies a value for it.
Update modules/aws-s3-static-website/main.tf
to use the files
object instead
of the individual variables. First replace the index and error document
variables in the aws_s3_bucket_website_configuration.web
resource.
modules/aws-s3-static-website/main.tf
resource "aws_s3_bucket_website_configuration" "web" {
bucket = aws_s3_bucket.web.id
index_document {
suffix = var.files.index_document_suffix
}
error_document {
key = var.files.error_document_key
}
}
Next, replace the www_path
and terraform_managed_files
variables in the
module.template_files
and aws_s3_object.web
configuration blocks.
modules/aws-s3-static-website/main.tf
module "template_files" {
source = "hashicorp/dir/template"
version = "1.0.2"
base_dir = var.files.www_path != null ? var.files.www_path : "${path.module}/www"
}
resource "aws_s3_object" "web" {
for_each = var.files.terraform_managed ? module.template_files.files : {}
##...
}
Now downstream users of your module can control how Terraform manages the contents of their bucket in a few ways. When they use your module, they can:
Manage the files outside of Terraform by setting
terraform_managed
tofalse
. This allows web developers to manage the contents of the website with tools other than Terraform:files = { terraform_managed = false }
Either use the default files (in
modules/aws-s3-static-website/www
) or specify their own path withwww_path
:files = { terraform_managed = true www_path = "${path.root}/www" }
Use different index and error documents by configuring
index_document_suffix
anderror_document_key
:files = { terraform_managed = true www_path = "${path.root}/www" index_document_suffix = "main.html" error_document_key = "error.html" }
Update the module block in main.tf
in the root repository directory to
use the new files
input variable. Because you set the www_path
attribute on
the files
object, Terraform will replace the website contents with the files
in the www
directory under the repository's root directory.
main.tf
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website"
bucket_prefix = "module-object-attributes-"
files = {
terraform_managed = true
www_path = "${path.root}/www"
}
##...
}
Apply your configuration. Respond to the confirmation prompt with yes
.
Terraform will replace the contents of your bucket with the files in the www
sub-directory. These files contain a simple Tetris-like game.
$ terraform apply
Running apply in HCP Terraform. Output will stream here. Pressing Ctrl-C
will cancel the remote apply if it's still pending. If the apply started it
will stop streaming the logs, but will not stop the apply running remotely.
Preparing the remote apply...
To view this run in a browser, visit:
https://app.terraform.io/app/organization-name/learn-terraform-module-object-attributes/runs/run-8tQrKasNfyGeXaTs
Waiting for the plan to start...
Terraform v1.3.0
on linux_amd64
Initializing plugins and modules...
module.website_s3_bucket.aws_s3_bucket.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_bucket_policy.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_bucket_website_configuration.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_bucket_acl.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001,public-read]
module.website_s3_bucket.aws_s3_object.web["index.html"]: Refreshing state... [id=index.html]
module.website_s3_bucket.aws_s3_object.web["error.html"]: Refreshing state... [id=error.html]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
Terraform will perform the following actions:
##...
Plan: 3 to add, 2 to change, 0 to destroy.
Do you want to perform these actions in workspace "learn-terraform-module-object-attributes"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
##...
Apply complete! Resources: 3 added, 2 changed, 0 destroyed.
Outputs:
website_bucket_arn = "arn:aws:s3:::module-object-attributes-20220920185307968400000001"
website_bucket_domain = "s3-website-us-west-2.amazonaws.com"
website_bucket_endpoint = "module-object-attributes-20220920185307968400000001.s3-website-us-west-2.amazonaws.com"
website_bucket_name = "module-object-attributes-20220920185307968400000001"
Visit the domain given in the website_bucket_endpoint
output value in your
browser, which now responds with a playable Tetris-like game.
Use a list of objects to configure CORS
Cross-Origin Resource Sharing (CORS) allows web developers to control where and
how users access resources in their website. CORS configuration limits access to
websites based on request headers, method, or originating domain. Add a new
variable to modules/aws-s3-static-website/variables.tf
to control your S3
bucket's CORS configuration.
modules/aws-s3-static-website/variables.tf
variable "cors_rules" {
description = "List of CORS rules."
type = list(object({
allowed_headers = optional(set(string)),
allowed_methods = set(string),
allowed_origins = set(string),
expose_headers = optional(set(string)),
max_age_seconds = optional(number)
}))
default = []
}
The cors_rules
variable contains a list of objects. Since the default value is
an empty list ([]
), users do not need to set this input variable to deploy the
module. When they do use it, they must set allowed_methods
and
allowed_origins
for each object in the list; the other attributes are
optional. This matches the behavior of the aws_s3_bucket_cors_configuration
resource you will use to configure CORS.
Use the cors_rules
variable by adding a new resource to
modules/aws-s3-static-website/main.tf
.
modules/aws-s3-static-website/main.tf
resource "aws_s3_bucket_cors_configuration" "web" {
count = length(var.cors_rules) > 0 ? 1 : 0
bucket = aws_s3_bucket.web.id
dynamic "cors_rule" {
for_each = var.cors_rules
content {
allowed_headers = cors_rule.value["allowed_headers"]
allowed_methods = cors_rule.value["allowed_methods"]
allowed_origins = cors_rule.value["allowed_origins"]
expose_headers = cors_rule.value["expose_headers"]
max_age_seconds = cors_rule.value["max_age_seconds"]
}
}
}
This resource uses the dynamic
block to create a cors_rule
block for each
item in the var.cors_rules
list. When the list is empty, the count
meta-argument will evaluate to 0
, and Terraform will not provision this
resource. Otherwise, the dynamic
block will create a CORS rule for each object
in the list. Since optional object attributes default to null
, Terraform will
not set values for them unless the module user specifies them.
Update the module block in main.tf
in the repository root directory to use the
new variable. These example rules limit PUT
and POST
requests to an example
domain, and permit GET
requests from anywhere.
main.tf
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website"
bucket_prefix = "module-object-attributes-"
files = {
terraform_managed = true
www_path = "${path.root}/www"
}
cors_rules = [
{
allowed_headers = ["*"],
allowed_methods = ["PUT", "POST"],
allowed_origins = ["https://test.example.com"],
expose_headers = ["ETag"],
max_age_seconds = 3000
},
{
allowed_methods = ["GET"],
allowed_origins = ["*"]
}
]
tags = {
terraform = "true"
environment = "dev"
public-bucket = true
}
}
Apply this change to configure CORS for your bucket. Respond to the confirmation
prompt with yes
. Terraform will report the new CORS resource it created for
your bucket.
$ terraform apply
Running apply in HCP Terraform. Output will stream here. Pressing Ctrl-C
will cancel the remote apply if it's still pending. If the apply started it
will stop streaming the logs, but will not stop the apply running remotely.
Preparing the remote apply...
To view this run in a browser, visit:
https://app.terraform.io/app/organization-name/learn-terraform-module-object-attributes/runs/run-8tQrKasNfyGeXaTs
Waiting for the plan to start...
Terraform v1.3.0
on linux_amd64
Initializing plugins and modules...
module.website_s3_bucket.aws_s3_bucket.web: Refreshing state... [id=module-object-attributes-20220921153215483800000001]
module.website_s3_bucket.aws_s3_bucket_website_configuration.web: Refreshing state... [id=module-object-attributes-20220921153215483800000001]
module.website_s3_bucket.aws_s3_bucket_acl.web: Refreshing state... [id=module-object-attributes-20220921153215483800000001,public-read]
##...
Terraform will perform the following actions:
# module.website_s3_bucket.aws_s3_bucket_cors_configuration.web[0] will be created
+ resource "aws_s3_bucket_cors_configuration" "web" {
+ bucket = "module-object-attributes-20220921153215483800000001"
+ id = (known after apply)
##...
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions in workspace "learn-terraform-module-object-attributes"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
module.website_s3_bucket.aws_s3_bucket_cors_configuration.web[0]: Creating...
module.website_s3_bucket.aws_s3_bucket_cors_configuration.web[0]: Creation complete after 1s [id=module-object-attributes-20220921153215483800000001]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
website_bucket_arn = "arn:aws:s3:::module-object-attributes-20220920185307968400000001"
website_bucket_domain = "s3-website-us-west-2.amazonaws.com"
website_bucket_endpoint = "module-object-attributes-20220920185307968400000001.s3-website-us-west-2.amazonaws.com"
website_bucket_name = "module-object-attributes-20220920185307968400000001"
Clean up your infrastructure
Remove your bucket and related resources. Respond to the confirmation prompt
with a yes
.
$ terraform destroy
Running apply in HCP Terraform. Output will stream here. Pressing Ctrl-C
will cancel the remote apply if it's still pending. If the apply started it
will stop streaming the logs, but will not stop the apply running remotely.
Preparing the remote apply...
To view this run in a browser, visit:
https://app.terraform.io/app/organization-name/learn-terraform-module-object-attributes/runs/run-ZFJJ4enh97F69HJi
Waiting for the plan to start...
Terraform v1.3.0
on linux_amd64
Initializing plugins and modules...
module.website_s3_bucket.aws_s3_bucket.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_bucket_policy.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_object.web["scripts/terramino.js"]: Refreshing state... [id=scripts/terramino.js]
module.website_s3_bucket.aws_s3_bucket_website_configuration.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001]
module.website_s3_bucket.aws_s3_object.web["styles/terramino.css"]: Refreshing state... [id=styles/terramino.css]
module.website_s3_bucket.aws_s3_object.web["index.html"]: Refreshing state... [id=index.html]
module.website_s3_bucket.aws_s3_object.web["error.html"]: Refreshing state... [id=error.html]
module.website_s3_bucket.aws_s3_bucket_acl.web: Refreshing state... [id=module-object-attributes-20220920185307968400000001,public-read]
module.website_s3_bucket.aws_s3_object.web["images/background.png"]: Refreshing state... [id=images/background.png]
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.website_s3_bucket.aws_s3_bucket_cors_configuration.web[0] will be created
##...
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions in workspace "learn-terraform-module-object-attributes"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
website_bucket_arn = "arn:aws:s3:::module-object-attributes-20220920185307968400000001"
website_bucket_domain = "s3-website-us-west-2.amazonaws.com"
website_bucket_endpoint = "module-object-attributes-20220920185307968400000001.s3-website-us-west-2.amazonaws.com"
website_bucket_name = "module-object-attributes-20220920185307968400000001"
If you used HCP Terraform for this tutorial, after destroying your resources,
delete the learn-terraform-module-object-attributes
workspace from your
HCP Terraform organization.
Next steps
In this tutorial, you refactored the aws-s3-static-website
module to group
attributes that configure a website bucket into a single object variable. You
also added the ability to configure CORS with a list of objects. Defining module
attributes as objects will make it easier for module users to understand how
your module works, and let you update the module without changing its required
input variables. Review the following resources to learn more about using and
creating modules with Terraform.
- Learn how to Create Dynamic Expressions to help make your Terraform configurations more dynamic and flexible.
- Create composable, shareable, and reusable modules with Module Creation - Recommended Pattern.
- Read the documentation for optional object type attributes.