Terraform
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 } } 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_suffixanderror_document_keyvariables control which files the website will use for its index and error documents, respectively. - the
www_pathvariable allows users to specify a path from which to load the files for the website. - the
terraform_managed_filesvariable 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. 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_managedtofalse. 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_suffixanderror_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.