In the last tutorial, you used modules from the Terraform Registry to create a VPC and EC2 instance in AWS. While using existing Terraform modules correctly is an important skill, every Terraform practitioner will also benefit from learning how to create modules. In fact, we recommend that every Terraform configuration 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. When
you run terraform
commands, or use Terraform Cloud or Terraform Enterprise to
remotely run Terraform, the target directory containing Terraform configuration
is treated as the root module.
In this tutorial, you will create a module to manage AWS S3 buckets used to host static websites.
»Prerequisites
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:
- An AWS account Configure one of the authentication methods described in our AWS Provider Documentation. The examples in this tutorial assume that you are using the Shared Credentials file method with the default AWS credentials file and default profile.
- The AWS CLI
- The Terraform CLI
If you don't have an AWS account, the AWS CLI installed locally, or Terraform installed locally, follow this tutorial in an interactive lab from your web browser. Launch it here.
»Module structure
Terraform treats any local directory referenced in the source
argument of a
module
block as a module. A typical file structure for a new module is:
$ tree minimal-module/
.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
None of these files are required, or have any special meaning to Terraform when
it uses your module. You can create a module with a single .tf
file, or use
any other file structure you like.
Each of these files serves a purpose:
LICENSE
will contain the license under which your module will be distributed. When you share your module, the LICENSE file will let people using it know the terms under which it has been made available. Terraform itself does not use this file.README.md
will contain documentation describing how to use your module, in markdown format. Terraform does not use this file, but services like the Terraform Registry and GitHub will display the contents of this file to people who visit your module's Terraform Registry or GitHub page.main.tf
will contain the main set of configuration for your module. You can also create other configuration files and organize them however makes sense for your project.variables.tf
will contain the variable definitions for your module. When your module is used by others, the variables will be configured as arguments in themodule
block. Since all Terraform values must be defined, any variables 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. Module outputs are made available to the configuration using the module, so they are often used to pass information about the parts of your infrastructure defined by the module to other parts of your configuration.
There are also some other files to be aware of, and ensure that you don't distribute them as part of your module:
terraform.tfstate
andterraform.tfstate.backup
: These files contain your Terraform state, and are how Terraform keeps track of the relationship between your configuration and the infrastructure provisioned by it..terraform
: This directory contains the modules and plugins used to provision your infrastructure. These files are specific to a specific instance of Terraform when provisioning infrastructure, not the configuration of the infrastructure defined in.tf
files.*.tfvars
: Since module input variables are set via arguments to themodule
block in your configuration, you don't need to distribute any*.tfvars
files with your module, unless you are also using it as a standalone Terraform configuration.
If you are tracking changes to your module in a version control system, such as git, you will want to configure your version control system to ignore these files. For an example, see this .gitignore file from GitHub.
Warning: The files mentioned above will often include secret information such as passwords or access keys, which will become public if those files are committed to a public version control system such as GitHub.
»Create a module
This tutorial will use the configuration created in the using modules tutorial as a starting point. You can either continue working on that configuration in your local directory, or use the following commands to clone this GitHub repository.
Clone the GitHub repository.
$ git clone https://github.com/hashicorp/learn-terraform-modules.git
Change into that directory in your terminal.
$ cd learn-terraform-modules
Start from the website-complete-example
tag.
$ git checkout website-complete-example
If you have cloned the GitHub repository, you will also want to ensure that Terraform has downloaded all the necessary providers and modules by initializing it.
$ terraform init
Initializing modules...
Downloading terraform-aws-modules/ec2-instance/aws 2.12.0 for ec2_instances...
- ec2_instances in .terraform/modules/ec2_instances/terraform-aws-modules-terraform-aws-ec2-instance-ed6dcd9
Downloading terraform-aws-modules/vpc/aws 2.21.0 for vpc...
- vpc in .terraform/modules/vpc/terraform-aws-modules-terraform-aws-vpc-2417f60
- website_s3_bucket in modules/aws-s3-static-website-bucket
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aws" (hashicorp/aws) 2.44.0...
# ...
In this tutorial, you will create a local submodule within your existing configuration that uses the s3 bucket resource from the AWS provider.
If you didn't clone the example repository, you'll need to create the directory
for your module. Inside your existing configuration directory, create a
directory called modules
, with a directory called
aws-s3-static-website-bucket
inside of it. For example, on Linux or Mac
systems, run:
$ mkdir -p modules/aws-s3-static-website-bucket
After creating these directories, your configuration's directory structure will look like this:
$ tree
.
├── LICENSE
├── README.md
├── main.tf
├── modules
│ └── aws-s3-static-website-bucket
├── outputs.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── variables.tf
2 directories, 8 files
If you have cloned the GitHub repository, the tfstate
files won't appear until
you run a terraform apply
command.
Hosting a static website with S3 is a fairly common use case. While it isn't too
difficult to figure out the correct configuration to provision a bucket this
way, encapsulating this configuration within a module will provide your users
with a quick and easy way create buckets they can use to host static websites
that adhere to best practices. Another benefit of using a module is that the
module name can describe exactly what buckets created with it are for. In this
example, the aws-s3-static-website-bucket
module creates s3 buckets that host
static websites.
»Create a README.md and LICENSE
If you have cloned the GitHub repository, it will include README.md and LICENSE files. These files are not used by Terraform at all. They are included in this example to demonstrate best practice. If you want, you can create them as follows.
Inside the aws-s3-static-website-bucket
directory, create a file called
README.md
with the following content.
# AWS S3 static website bucket
This 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.
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.0
Unless required by applicable law or agreed to in writing, software
distributed 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 and
limitations under the License.
Neither of these files is required or used by Terraform. Having them is a best practice for modules that may one day be shared with others.
»Add module configuration
You will work with three Terraform configuration files inside the
aws-s3-static-website-bucket
directory: main.tf
, variables.tf
, and
outputs.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 module directory structure will look like this:
$ tree modules/
modules/
└── aws-s3-static-website-bucket
├── LICENSE
├── README.md
├── main.tf
├── outputs.tf
└── variables.tf
Add an S3 bucket resource to main.tf
inside the
modules/aws-s3-static-website-bucket
directory:
resource "aws_s3_bucket" "s3_bucket" {
bucket = var.bucket_name
acl = "public-read"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::${var.bucket_name}/*"
]
}
]
}
EOF
website {
index_document = "index.html"
error_document = "error.html"
}
tags = var.tags
}
This configuration creates a public S3 bucket hosing a website with an index page and an error page.
You will notice that there is no provider
block in this configuration. When
Terraform processes a module block, it will inherit the provider from the
enclosing configuration. Because of this, we recommend that you do not include
provider
blocks in modules.
Just like the root module of your configuration, modules will define and use variables.
Define the following variables in variables.tf
inside the
modules/aws-s3-static-website-bucket
directory:
variable "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 the
root module. When you run a Terraform command on your root configuration, there
are various ways to set variable values, such as passing them on the
commandline, or with a .tfvars
file. When using a module, variables are set by
passing arguments to the module in your configuration. You will set some of
these variables when calling this module from your root module's main.tf
.
Variables defined in modules that aren't given a default value are required, and so must be set whenever the module is used.
When creating a module, consider which resource arguments to expose to module end users as input variables. For example, you might decide to expose the index and error documents to end users of this module as variables, but not define a variable to set the ACL , since to host a website your bucket will need the ACL to be set to "public-read".
You should also consider which values to add as outputs, since outputs are the only supported way for users to get information about resources configured by the module.
Add outputs to your module in the outputs.tf
file inside the
modules/aws-s3-static-website-bucket
directory:
# Output variable definitions
output "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 "website_endpoint" {
description = "Domain name of the bucket"
value = aws_s3_bucket.s3_bucket.website_endpoint
}
Like variables, outputs in modules perform the same function as they do in the
root module but are accessed in a different way. A module's outputs can be
accessed as read-only attributes on the module object, which is available within
the configuration that calls the module. You can reference these outputs in
expressions as module.<MODULE NAME>.<OUTPUT NAME>
.
Now that you have created your module, return to the main.tf
in your root
module and add a reference to the new module:
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website-bucket"
bucket_name = "<UNIQUE BUCKET NAME>"
tags = {
Terraform = "true"
Environment = "dev"
}
}
AWS S3 Buckets must be globally unique. Because of this, you will need to
replace <UNIQUE BUCKET NAME>
with a unique, valid name for an S3 bucket. Using
your name and the date is usually a good way to guess a unique bucket name. For
example:
bucket_name = "robin-example-2020-01-15"
In this example, the bucket_name
and tags
arguments will be passed to the
module, and provide values for the matching variables found in
modules/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 these values as outputs to your root module by adding the following to
outputs.tf
file in your root module directory (not the one in
modules/aws-s3-static-website-bucket
).
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_endpoint" {
description = "Domain name of the bucket"
value = module.website_s3_bucket.website_endpoint
}
»Install the local module
Whenever you add a new module to a configuration, Terraform must install the
module before it can be used. Both the terraform get
and terraform init
commands will install and update modules. The terraform init
command will also
initialize backends and install plugins.
Now install the module by running terraform get
.
$ terraform get
Note: When installing a remote module, Terraform will download it into
the .terraform
directory in your configuration's root directory. When
installing a local module, Terraform will instead refer directly to the source
directory. Because of this, Terraform will automatically notice changes to
local modules without having to re-run terraform init
or terraform get
.
Now that your new module is installed and configured, run terraform apply
to
provision your bucket.
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform 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 will
be provisioned.
Note: This will provision the EC2 instances from the previous tutorial as
well. Don't forget to run terraform destroy
when you are done with this tutorial
to 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. You
may want to visit this static website. Right now there is nothing inside your
bucket, so there would be nothing to see if you visit the bucket's website. In
order to see any content, you will need to upload objects to your bucket. You
can upload the contents of the www
directory found in the GitHub
repository
to the bucket using the AWS console, or the AWS commandline
tool, for example:
$ aws s3 cp modules/aws-s3-static-website-bucket/www/ s3://$(terraform output website_bucket_name)/ --recursive
upload: modules/aws-s3-static-website-bucket/www/error.html to s3://robin-test-2020-01-15/error.html
upload: modules/aws-s3-static-website-bucket/www/index.html to s3://robin-test-2020-01-15 /index.html
The website endpoint was shown when you last ran terraform apply
, or whenever
you run terraform output
.
Visit the website endpoint in a web browser, and you will see the website contents.
https://<YOUR BUCKET NAME>.s3-us-west-2.amazonaws.com/index.html
»Clean up the website and infrastructure
If you have uploaded files to your bucket, you will need to delete them before the bucket can be destroyed. For example, you could run:
$ aws s3 rm s3://$(terraform output website_bucket_name)/ --recursive
delete: s3://robin-test-2020-01-15/index.html
delete: s3://robin-test-2020-01-15/error.html
Once the bucket is empty, destroy your Terraform resources:
$ terraform destroy
module.vpc.aws_vpc.this[0]: Refreshing state... [id=vpc-00ff17f9d4801bdef]
module.vpc.aws_eip.nat[0]: Refreshing state... [id=eipalloc-0364049fdf8f8d33d]
# ...
Plan: 0 to add, 0 to change, 23 to destroy.
Do 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: yes
module.vpc.aws_route_table_association.private[1]: Destroying... [id=rtbassoc-01a79185940c0247b]
module.vpc.aws_route.public_internet_gateway[0]: Destroying... [id=r-rtb-0d22a7780ac9509df1080289494]
# ...
Destroy complete! Resources: 23 destroyed.
After you respond to the prompt with yes
, Terraform will destroy all of the
resources created by following this tutorial.
»Next steps
In this tutorial, you have learned how to:
- Create a Terraform module
- Use local Terraform modules in your configuration
- Configure modules with variables
- Use module outputs
You can read more about modules in the Terraform documentation.