Build and use a local module | Terraform | HashiCorp Developer (2024)

In the last tutorial, you used modules from the Terraform Registry to create aVPC and an EC2 instance in AWS. While using existing Terraform modules correctlyis an important skill, every Terraform practitioner will also benefit fromlearning how to create modules. In fact, we recommend that every Terraformconfiguration be created with the assumption that it may be used as a module,because doing so will help you design your configurations to be flexible,reusable, and composable.

As you may already know, Terraform treats every configuration as a module. Whenyou run terraform commands, or use Terraform Cloud or Terraform Enterprise toremotely run Terraform, the target directory containing Terraform configurationis treated as the root module.

In this tutorial, you will create a module to manage AWS S3 buckets used to hoststatic websites.

Although the concepts in this tutorial apply to any module creation workflow,this tutorial uses Amazon Web Services (AWS) modules.

To follow this tutorial you will need:

Module structure

Terraform treats any local directory referenced in the source argument of amodule block as a module. A typical file structure for a new module is:

.├── LICENSE├── README.md├── main.tf├── variables.tf├── outputs.tf

None of these files are required, or have any special meaning to Terraform whenit uses your module. You can create a module with a single .tf file, or useany other file structure you like.

Each of these files serves a purpose:

  • LICENSE will contain the license under which your module will bedistributed. When you share your module, the LICENSE file will let peopleusing it know the terms under which it has been made available. Terraformitself does not use this file.
  • README.md will contain documentation describing how to use your module, inmarkdown format. Terraform does not use this file, but services like theTerraform Registry and GitHub will display the contents of this file to peoplewho visit your module's Terraform Registry or GitHub page.
  • main.tf will contain the main set of configuration for your module. You canalso create other configuration files and organize them however makes sensefor your project.
  • variables.tf will contain the variable definitions for your module. Whenyour module is used by others, the variables will be configured as argumentsin the module block. Since all Terraform values must be defined, anyvariables that are not given a default value will become required arguments.Variables with default values can also be provided as module arguments,overriding the default value.
  • outputs.tf will contain the output definitions for your module. Moduleoutputs are made available to the configuration using the module, so they areoften used to pass information about the parts of your infrastructure definedby the module to other parts of your configuration.

There are also some other files to be aware of, and ensure that you don'tdistribute them as part of your module:

  • terraform.tfstate and terraform.tfstate.backup: These files contain yourTerraform state, and are how Terraform keeps track of the relationship betweenyour configuration and the infrastructure provisioned by it.
  • .terraform: This directory contains the modules and plugins used toprovision your infrastructure. These files are specific to a specific instanceof Terraform when provisioning infrastructure, not the configuration of theinfrastructure defined in .tf files.
  • *.tfvars: Since module input variables are set via arguments to the moduleblock in your configuration, you don't need to distribute any *.tfvars fileswith your module, unless you are also using it as a standalone Terraformconfiguration.

If you are tracking changes to your module in a version control system, such asgit, you will want to configure your version control system to ignore thesefiles. For an example, see this .gitignorefile fromGitHub.

Warning

The files mentioned above will often include secret informationsuch as passwords or access keys, which will become public if those files arecommitted to a public version control system such as GitHub.

This tutorial will use the configuration created in the using modulestutorial as a starting point. You caneither continue working on that configuration in your local directory, or use thefollowing commands to clone this GitHub repository.

Clone the GitHub repository.

$ git clone https://github.com/hashicorp/learn-terraform-modules-create.git

Change into the directory in your terminal.

$ cd learn-terraform-modules-create

Ensure that Terraform has downloaded all the necessary providers and modules byinitializing it.

$ terraform initInitializing modules...Downloading registry.terraform.io/terraform-aws-modules/ec2-instance/aws 4.3.0 for ec2_instances...- ec2_instances in .terraform/modules/ec2_instancesDownloading registry.terraform.io/terraform-aws-modules/vpc/aws 3.18.1 for vpc...- vpc in .terraform/modules/vpc- website_s3_bucket in modules/aws-s3-static-website-bucketInitializing the backend...Initializing provider plugins...- Reusing previous version of hashicorp/aws from the dependency lock file- Installing hashicorp/aws v4.49.0...- Installed hashicorp/aws v4.49.0 (signed by HashiCorp)Terraform has been successfully initialized!You may now begin working with Terraform. Try running "terraform plan" to seeany changes that are required for your infrastructure. All Terraform commandsshould now work.If you ever set or change modules or backend configuration for Terraform,rerun this command to reinitialize your working directory. If you forget, othercommands will detect it and remind you to do so if necessary.

In this tutorial, you will create a local submodule within your existingconfiguration that uses the s3 bucketresource from theAWS provider.

If you didn't clone the example repository, you'll need to create the directoryfor your module. Inside your existing configuration directory, create adirectory called modules, with a directory calledaws-s3-static-website-bucket inside of it. For example, on Linux or Macsystems, run:

$ mkdir -p modules/aws-s3-static-website-bucket

After creating these directories, your configuration's directory structurewill look like this:

.├── LICENSE├── README.md├── main.tf├── modules│ └── aws-s3-static-website-bucket├── outputs.tf├── terraform.tfstate├── terraform.tfstate.backup└── variables.tf

If you have cloned the GitHub repository, the tfstate files won't appear untilyou run a terraform apply command.

Hosting a static website with S3 is a fairly common use case. While it isn't toodifficult to figure out the correct configuration to provision a bucket thisway, encapsulating this configuration within a module will provide your userswith a quick and easy way create buckets they can use to host static websitesthat adhere to best practices. Another benefit of using a module is that themodule name can describe exactly what buckets created with it are for. In thisexample, the aws-s3-static-website-bucket module creates s3 buckets that hoststatic websites.

Create a README.md and LICENSE

If you have cloned the GitHub repository, it will include README.md andLICENSE files. These files are not used by Terraform at all. They are includedin this example to demonstrate best practice. If you want, you can create themas follows.

Inside the aws-s3-static-website-bucket directory, create a file calledREADME.md with the following content.

modules/aws-s3-static-website-bucket/README.md

# AWS S3 static website bucketThis module provisions AWS S3 buckets configured for static website hosting.

Choosing the correct license for your modules is out of the scope of this tutorial.This tutorial will use the Apache 2.0 open source license.

Create another file called LICENSE with the following content.

modules/aws-s3-static-website-bucket/LICENSE

Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.

Neither of these files is required or used by Terraform. Having them is a bestpractice for modules that may one day be shared with others.

Add module configuration

You will work with three Terraform configuration files inside theaws-s3-static-website-bucket directory: main.tf, variables.tf, andoutputs.tf.

If you checked out the git repository, those files will already exist.Otherwise, you can create these empty files now. After you do so, your moduledirectory structure will look like this:

modules└── aws-s3-static-website-bucket ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── variables.tf └── www

Add an S3 bucket resource to main.tf inside themodules/aws-s3-static-website-bucket directory:

modules/aws-s3-static-website-bucket/main.tf

resource "aws_s3_bucket" "s3_bucket" { bucket = var.bucket_name tags = var.tags}resource "aws_s3_bucket_website_configuration" "s3_bucket" { bucket = aws_s3_bucket.s3_bucket.id index_document { suffix = "index.html" } error_document { key = "error.html" }}resource "aws_s3_bucket_acl" "s3_bucket" { bucket = aws_s3_bucket.s3_bucket.id acl = "public-read"}resource "aws_s3_bucket_policy" "s3_bucket" { bucket = aws_s3_bucket.s3_bucket.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "PublicReadGetObject" Effect = "Allow" Principal = "*" Action = "s3:GetObject" Resource = [ aws_s3_bucket.s3_bucket.arn, "${aws_s3_bucket.s3_bucket.arn}/*", ] }, ] })}

This configuration creates a public S3 bucket hosting a website with an indexpage and an error page.

Notice that there is no provider block in this configuration. When Terraformprocesses a module block, it will inherit the provider from the enclosingconfiguration. Because of this, we recommend that you do not include providerblocks in modules.

Just like the root module of your configuration, modules will define and usevariables.

Define the following variables in variables.tf inside themodules/aws-s3-static-website-bucket directory:

modules/aws-s3-static-website-bucket/variables.tf

# Input variable definitionsvariable "bucket_name" { description = "Name of the s3 bucket. Must be unique." type = string}variable "tags" { description = "Tags to set on the bucket." type = map(string) default = {}}

Variables within modules work almost exactly the same way that they do for theroot module. When you run a Terraform command on your root configuration, thereare various ways to set variable values, such as passing them on thecommandline, or with a .tfvars file. When using a module, variables are set bypassing arguments to the module in your configuration. You will set some ofthese variables when calling this module from your root module's main.tf.

Variables declared in modules that aren't given a default value are required, andso must be set whenever you use the module.

When creating a module, consider which resource arguments to expose to moduleend users as input variables. For example, you might decide to expose the indexand error documents to end users of this module as variables, but not declare avariable to set the ACL, since you must set your bucket's ACL to public-readto host a website.

You should also consider which values to add as outputs, since outputs are theonly supported way for users to get information about resources configured bythe module.

Add outputs to your module in the outputs.tf file inside themodules/aws-s3-static-website-bucket directory:

modules/aws-s3-static-website-bucket/outputs.tf

# Output variable definitionsoutput "arn" { description = "ARN of the bucket" value = aws_s3_bucket.s3_bucket.arn}output "name" { description = "Name (id) of the bucket" value = aws_s3_bucket.s3_bucket.id}output "domain" { description = "Domain name of the bucket" value = aws_s3_bucket_website_configuration.s3_bucket.website_domain}

Like variables, outputs in modules perform the same function as they do in theroot module but are accessed in a different way. You can access a module's outputfrom the configuration that calls the module through the following syntax:module.<MODULE NAME>.<OUTPUT NAME>. Module outputs are read-only attributes.

Now that you have created your module, return to the main.tf in your rootmodule and add a reference to the new module:

main.tf

module "website_s3_bucket" { source = "./modules/aws-s3-static-website-bucket" bucket_name = "<UNIQUE BUCKET NAME>" tags = { Terraform = "true" Environment = "dev" }}

AWS S3 Bucket names must be globally unique. Because of this, you will need toreplace <UNIQUE BUCKET NAME> with a unique, valid name for an S3 bucket. Usingyour name and the date is usually a good way to guess a unique bucket name. Forexample:

 bucket_name = "robin-example-2020-01-15"

This example passes the bucket_name and tags arguments to the module, whichprovides values for the matching variables found inmodules/aws-s3-static-website-bucket/variables.tf.

Define outputs

Earlier, you added several outputs to the aws-s3-static-website-bucket module,making those values available to your root module configuration.

Add the following to the outputs.tf file in your root module directory (not theone in modules/aws-s3-static-website-bucket) to create additional outputs foryour S3 bucket.

outputs.tf

# Output variable definitionsoutput "vpc_public_subnets" { description = "IDs of the VPC's public subnets" value = module.vpc.public_subnets}output "ec2_instance_public_ips" { description = "Public IP addresses of EC2 instances" value = module.ec2_instances[*].public_ip}output "website_bucket_arn" { description = "ARN of the bucket" value = module.website_s3_bucket.arn}output "website_bucket_name" { description = "Name (id) of the bucket" value = module.website_s3_bucket.name}output "website_bucket_domain" { description = "Domain name of the bucket" value = module.website_s3_bucket.domain}

Whenever you add a new module to a configuration, Terraform must install themodule before it can be used. Both the terraform get and terraform initcommands will install and update modules. The terraform init command will alsoinitialize backends and install plugins.

Now install the module by running terraform get.

$ terraform get

Note

When installing a remote module, Terraform will download it intothe .terraform directory in your configuration's root directory. Wheninstalling a local module, Terraform will instead refer directly to the sourcedirectory. Because of this, Terraform will automatically notice changes tolocal modules without having to re-run terraform init or terraform get.

Now that your new module is installed and configured, run terraform apply toprovision your bucket.

$ terraform applyAn execution plan has been generated and is shown below.Resource actions are indicated with the following symbols: + createTerraform will perform the following actions: ## ... # module.website_s3_bucket.aws_s3_bucket.s3_bucket will be created + resource "aws_s3_bucket" "s3_bucket" { + acceleration_status = (known after apply) ## ...Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:

After you respond to the prompt with yes, your bucket and other resources willbe provisioned.

Note

This will provision the EC2 instances from the previous tutorial aswell. Don't forget to run terraform destroy when you are done with this tutorialto remove those EC2 instances, or you could end up being charged for them.

After running terraform apply, your bucket will be created.

Upload files to the bucket

You have now configured and used your own module to create a static website. Youmay want to visit this static website. Right now there is nothing inside yourbucket, so there would be nothing to see if you visit the bucket's website. Inorder to see any content, you will need to upload objects to your bucket. Youcan upload the contents of the www directory found in the GitHubrepositoryto the bucket using the AWS console, or the AWS commandlinetool, for example:

$ aws s3 cp modules/aws-s3-static-website-bucket/www/ s3://$(terraform output -raw website_bucket_name)/ --recursiveupload: modules/aws-s3-static-website-bucket/www/error.html to s3://robin-test-2020-01-15/error.htmlupload: modules/aws-s3-static-website-bucket/www/index.html to s3://robin-test-2020-01-15 /index.html

The website domain was shown when you last ran terraform apply, or wheneveryou run terraform output.

Visit the website domain in a web browser, and you will see the websitecontents.

https://<YOUR BUCKET NAME>.s3-us-west-2.amazonaws.com/index.html

If you have uploaded files to your bucket, you will need to delete them beforethe bucket can be destroyed. For example, you could run:

$ aws s3 rm s3://$(terraform output -raw website_bucket_name)/ --recursivedelete: s3://robin-test-2020-01-15/index.htmldelete: s3://robin-test-2020-01-15/error.html

Once the bucket is empty, destroy your Terraform resources:

$ terraform destroyAn execution plan has been generated and is shown below.Resource actions are indicated with the following symbols: - destroyTerraform will perform the following actions: # module.ec2_instances.aws_instance.this[0] will be destroyed ## ...Plan: 0 to add, 0 to change, 26 to destroy.Changes to Outputs: - ec2_instance_public_ips = [ - "34.209.188.84", - "18.236.69.92", ] -> null - vpc_public_subnets = [ - "subnet-035b78336fdc48d7c", - "subnet-06b1eb0de498734e1", ] -> null - website_bucket_arn = "arn:aws:s3:::robin-example-2021-01-25" -> null - website_bucket_domain = "s3-website-us-west-2.amazonaws.com" -> null - website_bucket_name = "robin-example-2021-01-25" -> nullDo you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm. Enter a value: yesmodule.vpc.aws_route_table_association.public[1]: Destroying... [id=rtbassoc-0c8637d50db69e572]module.vpc.aws_route_table_association.public[0]: Destroying... [id=rtbassoc-0069ea0a8d0a37a9b]## ...Destroy complete! Resources: 26 destroyed.

After you respond to the prompt with yes, Terraform will destroy all of theresources created by following this tutorial.

Next steps

In this tutorial, you created a local Terraform module and referenced it in your root Terraform configuration. You also configured the module using input variables and exposed data about its resources with outputs.

To learn more about module best practices:

  • Review the recommended patterns on designing modules and workflows for collaboration.
  • Follow the tutorial on how to use objects to manage complex module inputs.
  • Read the module documentation.
Build and use a local module | Terraform | HashiCorp Developer (2024)
Top Articles
Latest Posts
Article information

Author: Greg Kuvalis

Last Updated:

Views: 6622

Rating: 4.4 / 5 (55 voted)

Reviews: 86% of readers found this page helpful

Author information

Name: Greg Kuvalis

Birthday: 1996-12-20

Address: 53157 Trantow Inlet, Townemouth, FL 92564-0267

Phone: +68218650356656

Job: IT Representative

Hobby: Knitting, Amateur radio, Skiing, Running, Mountain biking, Slacklining, Electronics

Introduction: My name is Greg Kuvalis, I am a witty, spotless, beautiful, charming, delightful, thankful, beautiful person who loves writing and wants to share my knowledge and understanding with you.