UNIT-2 2.2 Introduction To Terraform
UNIT-2 2.2 Introduction To Terraform
Management - 21CSH-481
System provisioning and configuration
management: States of various tools in
provisioning and configuration, Reasons for
using provisioning and configuration tools,
UNIT Examples: Automation, preventing errors,
-2 tracking of changes, Examples of tools and
their capabilities.
Terraform: Fundamentals, variables,
Conditions, loops, TCL, State management,
Workspaces, Modules
Introduction to Terraform
• Terraform is an open-source infrastructure as code (IaC) software tool
which can be used to provision the infrastructure of a cloud platform.
• The HCL scripts which have been used to provision infrastructure can
be human-readable configuration files that can be versioned, reused,
and shared.
• You can provision wide range of resources in the cloud by using
terraform like compute, storage, networking, and application services,
across a variety of cloud providers and on-premises environments.
Infrastructure as a Code (IaC)
• Infrastructure as Code (IaC) is a method of managing and provisioning
IT infrastructure using code, rather than manual configuration.
• It allows teams to automate the setup and management of their
infrastructure, making it more efficient and consistent.
• This is particularly useful in the DevOps environment, where teams are
constantly updating and deploying software.
Terraform Lifecycle
• Terraform lifecycle consists of – init, plan, apply, and destroy.
• In addition to these, there are other commands for various tasks like
formatting code (fmt), managing state (state), and more.
2. Terraform Language
• Terraform uses HashiCorp Configuration Language (HCL) to
define infrastructure. HCL is designed to be both easy to read by
humans and understandable by machines, making it a great fit for
DevOps tools.
• Infrastructure elements managed by Terraform are
called resources.
• These can include virtual machines, S3 buckets, VPCs, and
databases.
• Each resource is defined in a block, like this example for creating
an AWS VPC:
resource "aws_vpc" "default_vpc" {
cidr_block = "172.31.0.0/16"
tags = {
Name = "example_vpc"
}
}
3. Terraform Provider
• A software element known as a Terraform provider enables
Terraform to communicate with a particular infrastructure
platform.
• The resource kinds and data sources that Terraform can handle for
that platform must be implemented by providers.
• Cloud platforms, data centres, network devices, databases, and
other resources inside the target infrastructure or service can all be
defined, configured, and managed by Terraform providers.
4. Terraform Modules
• In Terraform, a module is a container for a set of related resources
that are used together to perform a specific task.
• Modules allow users to organize and reuse their infrastructure code,
making it easier to manage complex infrastructure deployments.
• Modules are defined using the ‘module’ block in Terraform
configuration.
• A module block takes the following arguments:
• source: The source location of the module. This can be a local
path or a URL.
• name: The name of the module. This is used to reference the
Terraform variables
• Terraform variables are placeholders for values that you can use
to make your configurations more dynamic and reusable.
• They let you define values that can be reused throughout your
Terraform configuration, similar to variables in any
programming language.
• Terraform variables are used to accommodate different
configurations without altering your code.
• You can easily change only the values of these variables to
achieve different use cases.
• They make your configuration more dynamic and flexible, and
they enhance the parametrization of your code.
Local variables
• Local variables are declared using the locals block.
• It is a group of key-value pairs that can be used in the
configuration.
• The values can be hard-coded or be a reference to another
variable or resource.
• Local variables are accessible within the module/configuration
where they are declared.
• Let us take an example of creating a configuration for an EC2
instance using local variables.
• Add this to a file named main.tf.
locals {
ami = "ami-0d26eb3972b7f8c96"
type = "t2.micro"
tags = {
Name = "My Virtual Machine"
Env = "Dev"
}
subnet = "subnet-76a8163a"
nic = aws_network_interface.my_nic.id
}
network_interface {
network_interface_id = aws_network_interface.my_nic.id
device_index =0
}
}
tags = {
Name = "My NIC"
}
}
• In this example, we have declared all the local variables in the
locals block.
• The variables represent the AMI ID (ami), Instance type (type),
Subnet Id (subnet), Network Interface (nic) and Tags (tags) to be
assigned for the given EC2 instance.
• In the aws_instance resource block, we used these variables to
provide the appropriate values required for the given attribute.
• Notice how the local variables are being referenced using
a local keyword (without ‘s’).
• The usage of local variables is similar to data sources. However,
they have a completely different purpose.
• Data sources fetch valid values from the cloud provider based
on the query filters we provide.
• Whereas we can set our desired values in local variables — they
are not dependent on the cloud providers.
• It is indeed possible to assign a value from a data source to a
local variable.
• Similar to how we have done it to create the nic local variable,
it refers to the id argument in the aws_network_interface resource
block.
• As a best practice, try to keep the number of local variables to a
minimum.
• Using many local variables can make the code hard to read.
Terraform input variables
• Terraform input variables are used to pass certain values from
outside of the configuration or module.
• They are used to assign dynamic values to resource attributes.
• The difference between local and input variables is that input
variables allow you to pass values before the code execution.
• Further, the main function of the input variables is to act as
inputs to modules.
• Modules are self-contained pieces of code that perform certain
predefined deployment tasks.
• Input variables declared within modules are used to accept
values from the root directory.
• Additionally, it is also possible to set certain attributes while
declaring input variables, as below:
String type
• The string type input variables are used to accept values in the
form of UNICODE characters.
• The value is usually wrapped by double quotes, as shown
below.
variable "string_type" {
description = "This is a variable of type string"
type = string
default = "Default string value for this variable"
}
• The string type input variables also support a heredoc style
format where the value being accepted is a longer string
separated by new line characters.
• The start and end of the value is indicated by “EOF” (End Of
File) characters.
• An example of the same is shown below.
variable "string_heredoc_type" {
description = "This is a variable of type string"
type = string
default = <<EOF
hello, this is Sumeet.
Do visit my website!
EOF
}
Number type
Boolean type
Map type
Object type
Tuple type
Set type
• A set is an unordered collection of distinct values, meaning each
element appears only once within the set.
• As against lists, sets enforce uniqueness – each element can
appear only once within the set.
• Sets support various inbuilt operations such as union,
intersection, and difference, which are used to combine or
compare sets.
• An example of a set type input variable is below.
variable "set_example" {
description = "This is a variable of type set"
type = set(string)
default = ["item1", "item2", "item3"]
}
Map of objects
List of objects
validation {
condition = length(var.ami) > 4 && substr(var.ami, 0, 4)
== "ami-"
error_message = "Please provide a valid value for variable
AMI."
}
}
variable "type" {
type = string
description = "Instance type for the EC2 instance"
default = "t2.micro"
sensitive = true
}
variable "tags" {
type = object({
name = string
env = string
})
description = "Tags for the EC2 instance"
default = {
name = "My Virtual Machine"
env = "Dev"
}
}
variable "subnet" {
type = string
description = "Subnet ID for network interface"
default = "subnet-76a8163a"
}
• Here, we have declared 5 variables
— ami, nic, subnet and type with the simple data type,
and tags with a complex data type object — a collection of key-
value pairs with string values.
• Notice how we have made use of attributes
like description and default.
• The ami variable also has validation rules defined for them
to check the validity of the value provided.
• We have also marked the type variable as sensitive.
• Let us now modify main.tf to use the variables declared above.
resource "aws_instance" "myvm" {
ami = var.ami
instance_type = var.type
tags = var.tags
network_interface {
network_interface_id = aws_network_interface.my_nic.id
device_index =0
}
}
tags = {
Name = "My NIC"
}
}
• Within the resource blocks, we have simply used these
variables by using var.<variable name> format.
• When you proceed to plan and apply this configuration, the
variable values will automatically be replaced by default values.
• The following is a sample plan output.
}
}
output "my_super_secret_password" {
value = var.my_super_secret_password
}
terraform apply
╷
│ Error: Output refers to sensitive values
│
│ on main.tf line 20:
│ 20: output "my_super_secret_password" {
│
│ To reduce the risk of accidentally exporting sensitive data that
was intended to be only internal, Terraform requires that any root
module output containing sensitive data
│ be explicitly marked as sensitive, to confirm your intent.
│
│ If you do intend to export this data, annotate the output value as
sensitive by adding the following argument:
│ sensitive = true
• Now, if we want terraform not to error out and at least show the
output, we should add the sensitive = true to that output:
output "my_super_secret_password" {
value = var.my_super_secret_password
sensitive = true
}
terraform apply
Changes to Outputs:
+ my_super_secret_password = (sensitive value)
You can apply this plan to save these new output values to the
Terraform state, without changing any real infrastructure.
Outputs:
my_super_secret_password = <sensitive>
• If you want to see the sensitive value in the output too,
Terraform has a mechanism in place for that if you are
leveraging the nonsensitive function:
variable "my_super_secret_password" {
type = string
default = "super-secret"
sensitive = true
}
output "my_super_secret_password" {
value = nonsensitive(var.my_super_secret_password)
}
terraform apply
Outputs:
my_super_secret_password = "super-secret"
Output variables
• For situations where you deploy a large web application
infrastructure using Terraform, you often need certain
endpoints, IP addresses, database user credentials, and so forth.
• This information is most useful for passing the values to
modules along with other scenarios.
• This information is also available in Terraform state files.
• But state files are large, and normally we would have to perform
an intricate search for this kind of information.
• Output variables in Terraform are used to display the required
information in the console output after a successful application
of configuration for the root module.
• To declare an output variable, write the following configuration
block into the Terraform configuration files.
output "instance_id" {
value = aws_instance.myvm.id
description = "AWS EC2 instance ID"
sensitive = false
}
• Continuing with the same example, we would like to display the
instance ID of the EC2 instance that is created.
• So, declare an output variable named instance_id — this could
be any name of our choosing.
• Within this output block, we have used some attributes to
associate this output variable’s value.
• We have used resource reference
for aws_instance.myvm configuration and specified to use
its id attribute.
• Optionally, we can use the description and sensitive flags.
• We have discussed the purpose of these attributes in previous
sections.
• When a plan command is run, the plan output acknowledges the
output variable being declared as below.
Changes to Outputs:
+ instance_id = (sensitive value)
• Similarly, when we run the apply command, upon successful
creation of EC2 instance, we would know the instance ID of the
same.
• Once the deployment is successful, output variables can also be
accessed using the output command:
terraform output
• Output:
instance_id = “i-xxxxxxxx”
• Output variables are used by child modules to expose certain
values to the root module.
• The root module does not have access to any other component
being created by the child module.
• So, if some information needs to be made available to the root
module, output variables should be declared for the
corresponding attributes within the child module.
Terraform Expressions
• Expressions are the core of HCL itself – the logic muscle of the
entire language.
• Terraform expressions allow you to get a value from
somewhere, calculate or evaluate it.
• You can use them to refer to the value of something, or extend
the logic of a component – for example, make one copy of the
resource for each value contained within a variable, using it as
an argument.
• They are used pretty much everywhere – the most simple type
of expression would be a literal value – so, there is a great
chance that you have already used them before.
1. Operators
• Dedicated to logical comparison and arithmetic operations,
operators are mostly used for doing math and basic Bool’s
algebra.
• If you need to know if number A equals number B, add them
together, or determine if both boolean A and boolean B are
“true”, Terraform offers the following operators:
2. Conditionals
• Sometimes, you might run into a scenario where you’d want the
argument value to be different, depending on another value.
• The conditional syntax is as such:
condition ? true_val : false_val
• The condition part is constructed using previously described
operators.
• In this example, the bucket_name value is based on the “test”
variable—if it’s set to true, the bucket will be named “dev” and
if it’s false, the bucket will be named “prod”:
bucket_name = var.test == true ? "dev" : "prod"
3. Splat expressions
• Splat expressions are used to extract certain values from
complicated collections – like grabbing a list of attributes from
a list of objects containing those attributes.
• Usually, you would need an “for” expression to do this, but
humans are lazy creatures who like to make complicated things
simpler.
• For example, if you had a list of objects such as these:
test_variable = [
{
name = "Arthur",
test = "true"
},
{
name = "Martha"
test = "true"
}
]
4. Constraints
• In simple terms, constraints regulate what can be what and
where something can or cannot be used.
• There are two main types of constraints—for types
and versions.
• Type constraints regulate the values of variables and outputs.
• For_each
• For
1. Count
• Count is the most primitive—it allows you to specify a whole
number, and produces as many instances of something as this
number tells it to.
• For example, the following would order Terraform to create ten
S3 buckets:
resource "aws_s3_bucket" "test" {
count = 10
[...]
}
• When count is in use, each instance of a resource or module
gets a separate index, representing its place in the order of
creation.
• To get a value from a single resource created in this way, you
must refer to it by its index value, e.g. if you wished to see the
ID of the fifth created S3 bucket, you would need to call it as
such:
aws_s3_bucket.test[5].id
• Although this is fine for identical, or nearly identical objects,
as previously mentioned, count is pretty primitive.
• When you need to use more distinct, complex values
– count yields to for_each.
2. For_each
• As mentioned earlier, sometimes you might want to create
resources with distinct values associated with each one – such
as names or parameters (memory or disk size for example).
• For_each will let you do just that. Merely provide a variable—
map, or a set of strings, and the resources can access values
contained within, via each.key and each.value:
test_map = {
test1 = "test2",
test2 = "test4"
}
test_attribute_1 = each.key
test_attribute_2 = each.value
}
• As you can see, for_each is quite powerful, but you haven’t
seen the best yet.
• By constructing a map of objects, you can leverage a resource
or module to create multiple instances of itself, each with
multiple declared variable values:
my_instances = {
instance_1 = {
ami = "ami-00124569584abc",
type = "t2.micro"
},
instance_2 = {
ami = "ami-987654321xyzab",
type = "t2.large"
},
}
ami = each.value["ami"]
instance_type = each.value["type"]
}
• Using this approach, you don’t have to touch anything except
the .tfvars file, to provide new instances of resources you have
already declared in your configuration.
3. For
• For is made for picking out, iterating over, and operating on
things from complex collections.
• Imagine that you have a list of words (strings), but
unfortunately, all of them contain newline characters at the end
which you don’t want.
• Like this:
word_list = [
"Critical\n",
"failure\n",
"teapot\n",
"broken\n"
]
• To fix this problem, you could do
[for word in var.word_list : chomp(word)]
• which would result in:
["Critical", "failure", "teapot", "broken"]
• As you can see, a list comes in, a list goes out—but, this is not
a must.
• The type of input, and the brackets which wrap
the for expression, determine the type of output it produces.
• If you had wrapped it with curly brackets, and provided a map
as an input, the output would have been a map.
• But there’s one thing that’s even more interesting—
the for expression can also be used to filter the input as you
please.
• By adding an if clause, you can conditionally operate or not
operate on certain values, depending on your defined terms.
• Take this example, making every word start with a capital
letter… except for the word “teapot”:
[for word in var.word_list : upper(word) if word != "teapot"]
Terraform State
• At its core, Terraform state provides an overview of your
infrastructure's resources and properties; this represents what
Terraform estimates will exist based on your configuration.
• Terraform uses state information to understand which resources
have been created, updated, or destroyed during each run.
• This information allows Terraform to make intelligent decisions
regarding which resources should be created, updated, or
destroyed during future runs.
• Terraform state is stored in a file named terraform.tfstate in
the root directory of your Terraform project.
• This JSON file stores information about all your resources, such
as IDs, attributes, and dependencies.
Terraform State Importance
• The importance of the Terraform state cannot be overstated.
• Here are some key reasons why it's crucial:
o Tracking Resource State: Terraform uses its state file to
configurations.
terraform init
o Creating Resources
o Define infrastructure resources in your Terraform
configuration file (e.g., main.tf).
o For example, to launch an AWS EC2 instance:
terraform init
o However, instead of using the default local state
backend, specify a remote state backend in your
configuration.
o Configure Remote State Backend
o In your Terraform configuration (e.g., main.tf), specify
backend:
terraform {
backend "s3" {
bucket = "aws-s3-bucket-demo"
key= "s3://aws-s3-bucket-
demo/statefile.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "demo-dynamodb-table"
}
}
o In this example, we use an S3 bucket to store the state
file and DynamoDB for locking.
o Below are the details of the above configurations:
• bucket: This is the name of the S3 bucket where
Terraform will store its state file and related data.
• key: This is the path within the S3 bucket where
Terraform will store its state file.
• region: This specifies the AWS region in which the S3
bucket is located.
• encrypt: When set to true, it means that Terraform
will encrypt the state file when storing it in S3,
providing additional security.
• dynamodb_table: This setting is used for state
locking, which prevents concurrent state
modifications.
o Apply Changes with Remote State
o Apply your Terraform configuration using the command
below:
terraform apply
o Terraform will store the state remotely in the specified
backend, making it accessible to all team members.
o Benefits of Remote State Management
o Remote state management offers several advantages:
• Improved collaboration: All team members can now
access and update the same state file without manually
sharing and synchronizing state files.
• Increased security: Remote state storage solutions
often have built-in security features, including access
control mechanisms.
• Better data protection: Cloud-based remote state
solutions typically offer data redundancy, backups, and
versioning to protect state data from accidental deletion
or corruption.
o Choose an appropriate remote state management solution
to ensure better collaboration, data integrity, and
concurrency control for your Terraform projects.
Terraform State Management Example
• In this scenario, we want to use Terraform to provision and
manage AWS EC2 instances.
1. Local State Management
Step 1: Set Up Your Project
Create a new directory for your Terraform project and navigate
into it:
mkdir terraform-ec2-demo
cd terraform-ec2-demo
Inside this directory, create a Terraform configuration file named
main.tf with the following content:
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "demo" {
ami = "ami-0c88g674cbfafe1f0"
instance_type = "t2.micro"
}
This configuration defines an AWS EC2 instance resource.
Step 2: Initialize and Apply
Initialize your Terraform project:
terraform init
Now, apply the configuration to create the EC2 instance:
terraform apply
Terraform will generate a state file named terraform.tfstate in the
same directory. This file stores information about the provisioned
EC2 instance.
Step 3: Modify and Update.
Let's make a change to the configuration. Update main.tf by
adding a "Name" tag to the EC2 instance:
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "demo" {
ami = "ami-0c88g674cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "DemoEC2Instance"
}
}
Apply the changes:
terraform apply
Terraform will detect the modification and update the EC2
instance accordingly. The state file is also updated to reflect the
new configuration.
Step 4: Delete Resources and Cleanup
To delete the EC2 instance, remove the below configuration in the
main.tf:
# main.tf
provider "aws" {
region = "us-east-1"
}
Apply the changes to delete the resource:
terraform apply
The EC2 instance will be deleted, and the state file will be updated
accordingly.
2. Remote State Management with Amazon S3 and DynamoDB
Step 1: Create an S3 Bucket and DynamoDB Table
• Log in to the AWS Management Console.
• Create an S3 bucket to store your Terraform state files.
• Enable versioning for the S3 bucket for data protection.
• Create a DynamoDB table to manage state locking. You can
follow the steps mentioned in the previous section.
Step 2: Configure Remote State in Terraform.
Update your main.tf configuration to use remote state
management with S3 and DynamoDB:
# main.tf
provider "aws" {
region = "us-east-1"
}
terraform {
backend "s3" {
bucket = "aws-s3-bucket-demo"
key = "/home/ubuntu/demo/statefile.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "demo-dynamodb-table"
}
}
resource "aws_instance" "demo" {
ami = "ami-0c88g674cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "DemoEC2Instance"
}
}
Step 3: Initialize and Apply with Remote State
Initialize your Terraform project to configure the remote state:
terraform init
Now, apply the configuration to create the EC2 instance:
terraform apply
Terraform will provision an EC2 instance and store its state in an
S3 bucket using DynamoDB for locking purposes.
Remote state management enables team members to work
together and ensure the security and integrity of your Terraform
state.
The above examples illustrate how Terraform state can be
managed locally and remotely using AWS EC2 instances as
infrastructure resources. Select an approach suitable for managing
the state of your projects based on your needs.
Best Practices for Managing Terraform State
Here are some best practices for managing the Terraform state:
• Keep state in a remote backend. By default, Terraform
stores state locally on disk; however, for optimal sharing and
recovery, it would be much more helpful to store state
remotely in the Cloud. Doing this allows teammates to share
it more efficiently and allows for easy recovery in case of loss
of state.
• Create separate state files for every environment. It's wise
to keep individual state files for different environments, for
example, production, staging, and development
environments. It prevents a situation where a change in one
environment affects the other environments by mistake.
• Utilize version control with your Terraform
configuration. This allows you to keep track of changes
made and roll back if necessary. It also enables you to keep
an audit trail of each modification that’s made.
• Regularly back up your state file. Regular backups of your
state files can help ensure recovery should they become lost
or deleted unexpectedly.
Terraform Workspace
• Terraform workspaces enable us to manage multiple deployments of
the same configuration.
• When we create cloud resources using the Terraform configuration
language, the resources are created in the default workspace.
• It is a very handy tool that lets us test configurations by giving us
flexibility in resource allocation, regional deployments, multi-account
deployments, and so on.
• The information about all the resources managed by Terraform is
stored in a state file.
• It is important to store this state file in a secure location.
• Every Terraform run is associated with a state file for validation and
reference purposes.
• Any modifications to the Terraform configuration, planned or applied,
are always validated first with references in the state files, and the
execution result is updated back to it.
• If you are not consciously using any workspace, all of this already
happens in a default workspace.
• Workspaces help you isolate independent deployments of the same
Terraform config while using the same state file.
Subcommands:
delete Delete a workspace
list List Workspaces
new Create a new workspace
select Select a workspace
show Show the name of the current workspace
• The options are quite straightforward here.
• We can use the workspace command to list all the available
workspaces and show the currently selected one.
• We can also create new workspaces and delete old ones. Finally, to
navigate through workspaces, we use the select command.
tags = {
Name = var.name_tag,
}
}
• If we run terraform plan command at this point, it will show that it
needs to create one resource, i.e. an EC2 instance.
• When the resource is created, the state file is updated with its
information and other attributes.
• Go ahead and create this EC2 instance.
• For reference, I am creating a t2.micro instance with Ubuntu 20.04
image.
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
+ public_ip = (known after apply)
aws_instance.my_vm: Creating...
aws_instance.my_vm: Still creating... [10s elapsed]
aws_instance.my_vm: Still creating... [20s elapsed]
aws_instance.my_vm: Still creating... [30s elapsed]
aws_instance.my_vm: Still creating... [40s elapsed]
aws_instance.my_vm: Creation complete after 42s [id=i-
07708992d1d3272c1]
Outputs:
instance_id = "i-07708992d1d3272c1"
public_ip = "3.73.0.139"
• As we can see from the output, the EC2 instance was successfully
created.
• Run the plan command again, and see if Terraform wants to perform
any additional actions at this point. It probably won’t.
• Looking closely, the size of the default state file is considerably larger
than that of the custom workspace-specific state file.
• This shows that the new state file is created, but it does not hold any
information from the default state file.
• This is how Terraform creates an isolated environment and maintains
its state file differently.
• The contents of the test_workspace state file before running terraform
apply are shown below:
{
"version": 4,
"terraform_version": "1.2.3",
"serial": 0,
"lineage": "c1aa5782-da15-419e-70f8-7024cadd0cfe",
"outputs": {},
"resources": []
}
• As a result of this, if we run the plan command in the same directory
now, Terraform will consider the state file as per the selected
workspace.
• No resources are captured or maintained in this state file, so it will
propose creating a new EC2 instance.
terraform plan
…
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
+ public_ip = (known after apply)
• Note: The plan output does not specify the workspace information it is
using while planning, so be sure to be very cautious while applying
these changes, as using the wrong workspace may break the existing
working environment.
• Despite creating an EC2 instance using the same configuration in the
default workspace, Terraform disregards its existence in a new
workspace.
• This creates many possibilities for how infrastructure management
may happen in various environments.
• The isolated nature of the Terraform workspace is used to test out
modifications to the existing configuration before applying them to the
critical environment, but this is just one of the use cases.
terraform apply
…
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
+ public_ip = (known after apply)
aws_instance.my_vm: Creating...
aws_instance.my_vm: Still creating... [10s elapsed]
aws_instance.my_vm: Still creating... [20s elapsed]
aws_instance.my_vm: Still creating... [30s elapsed]
aws_instance.my_vm: Creation complete after 31s [id=i-
0c0a6ffa4405249d7]
Outputs:
instance_id = "i-0c0a6ffa4405249d7"
public_ip = "3.122.229.252"
terraform apply
…
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
+ public_ip = (known after apply)
aws_instance.my_vm: Creating...
aws_instance.my_vm: Still creating... [10s elapsed]
aws_instance.my_vm: Still creating... [20s elapsed]
aws_instance.my_vm: Still creating... [30s elapsed]
aws_instance.my_vm: Creation complete after 32s [id=i-
0362373fe324e402f]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0362373fe324e402f"
public_ip = "3.72.73.27"
• Here, two EC2 instances were created using the same configuration but
in different workspaces.
• To validate if the interpolation sequences worked, log in to the AWS
console and verify the names of the newly created EC2 instances.
• As we can see, the names are set as expected, and now we can easily
identify which instance belongs to which Terraform workspace.
tags = {
Name = format("%s_%s_%s", var.name_tag,
terraform.workspace, count.index)
}
}
• Furthermore, corresponding changes are made to the Name tag to
include the count index to distinguish between multiple instances.
• When we apply this configuration in the default and test workspace
(which we created in the last section), we should then be able to see the
following instances with names:
1. EC2_default_0
2. EC2_default_1
3. EC2_default_2
4. EC2_test_0
name = "my-vpc"
cidr = "10.0.0.0/16"
enable_nat_gateway = true
tags = {
Terraform = "true"
Environment = "dev"
}
}
• In this example:
source: Specifies the module’s location in the Terraform Registry.
• version: Ensures that Terraform uses version 3.19.0 of the module,
maintaining consistency across deployments.
• The subsequent arguments (name, cidr, azs, etc.) correspond to the
input variables defined by the module, allowing customization of the
VPC’s configuration.
Example: Using a Local Module
• You can also create and reference local modules within your project
directory.
• Assume you have a module that sets up an AWS EC2 instance, located
in the modules/ec2-instance directory:
module "web_server" {
source = "./modules/ec2-instance"
instance_type = "t2.micro"
ami_id = "ami-0c55b159cbfafe1f0"
subnet_id = "subnet-abc12345"
}
• Here:
• source: Points to the relative path of the local module.
• The arguments (instance_type, ami_id, subnet_id) are input
variables defined within the ec2-instance module, allowing you to
customize the EC2 instance’s properties.
Key Components of a Module
• A well-structured Terraform module typically includes the following
files:
• main.tf: Contains the primary resource definitions.
• variables.tf: Declares input variables to parameterize the module.
• outputs.tf: Defines output values to expose information about the
resources.
• versions.tf: Specifies the required Terraform version and provider
constraints.
• README.md: Provides documentation on the module’s purpose
and usage.
Best Practices for Using Modules
• Encapsulation: Modules should encapsulate their resources,
exposing only necessary inputs and outputs.
• Reusability: Design modules to be reusable across different
configurations and environments.
• Versioning: Implement version control for modules to manage
changes and ensure stability.
• Documentation: Provide clear documentation within the module
directory to explain its purpose and usage.
• Consistency: Use consistent naming conventions and file structures
across modules.
• By effectively utilizing module blocks, you can create modular,
reusable, and maintainable infrastructure configurations, enhancing the
scalability and manageability of your Terraform projects.
$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
module "consul" {
source = "hashicorp/consul/aws"
}
module "moduleName" {
source = "module/path"
}
module "networkModule" {
source = "./module/network"
}
module "s3-bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "1.0.0"
# insert the 6 required variables here
}
module "apache"{
source = "./modules/install_apache"
}
module "nginx"{
source = "./modules/install_nginx"
instances = ["${module.web.instance_ids}"]
}
module "network" {
source = "Azure/network/azurerm"
resource_group_name = azurerm_resource_group.example.name
address_spaces = ["10.0.0.0/16", "10.2.0.0/16"]
subnet_prefixes = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
subnet_names = ["subnet1", "subnet2", "subnet3"]
subnet_service_endpoints = {
"subnet1" : ["Microsoft.Sql"],
"subnet2" : ["Microsoft.Sql"],
"subnet3" : ["Microsoft.Sql"]
}
tags = {
environment = "dev"
costcenter = "it"
}
depends_on = [azurerm_resource_group.example]
}
key parameters available for a Terraform module block
• Certainly! Here’s a table summarizing the key parameters available for
a Terraform module block:
Require
Parameter Description d Example
Specifies the
source =
location of the
"./modules/network" or sourc
source module’s Yes
e = "terraform-aws-
source code.
modules/vpc/aws"
This can be a
Require
Parameter Description d Example
local path, a
Git repository,
or a Terraform
Registry
address.
Defines the
version of the
module to use,
version particularly No version = "3.0.0"
when sourcing
modules from
registries.
Overrides the
default
provider
configurations
for the
module.
Useful when providers = { aws =
providers No
you need to aws.us_east }
specify
different
provider
settings for a
particular
module.
Require
Parameter Description d Example
Creates
multiple
instances of
the module.
Allows you to
count No count = 3
scale
resources by
specifying the
number of
instances.
Creates
multiple
instances of
the module
based on a
map or set of
strings.
Provides more
for_each = { net1 =
control
for_each No "10.0.0.0/16", net2 =
compared
"10.1.0.0/16" }
to count,
especially
when each
instance
requires
unique
configurations
.
Require
Parameter Description d Example
Specifies
dependencies
on other
resources or
modules.
Ensures that
depends_o depends_on =
the module is No
n [aws_vpc.main]
provisioned
only after
certain
resources or
modules have
been created.
• *Note: Input variables are user-defined parameters that allow
customization of the module’s behaviour. Each input variable must be
defined within the module’s variables.tf file. When calling the module,
you provide values for these variables.
• By utilizing these parameters, you can effectively manage and
customize the behaviour of your Terraform modules, leading to more
modular and maintainable infrastructure configurations.
• In Terraform, a module block is used to incorporate the configuration
of one module into another, promoting reusability and organization in
your infrastructure as code.
• The module block supports several parameters, allowing you to
customize its behaviour and the resources it provisions.
• Here's a breakdown of the key parameters you can define within
a module block:
1. source (Required):
1. Description: Specifies the location of the module's source code.
2. Usage: This can be a local file path, a URL to a version control
system (like Git), or a reference to a module in the Terraform
Registry.
3. Example:
module "network" {
source = "./modules/network"
}
or
module "network" {
source = "terraform-aws-modules/vpc/aws"
}
2. version (Optional):
1. Description: Specifies the version of the module to use.
2. Usage: Particularly useful when sourcing modules from the
Terraform Registry to ensure compatibility and stability.
3. Example:
module "network" {
source = "terraform-aws-modules/vpc/aws"
version = "3.0.0"
}
3. Input Variables:
1. Description: These are user-defined parameters that allow
customization of the module's behavior.
2. Usage: Each input variable must be defined within the
module's variables.tf file. When calling the module, you provide
values for these variables.
3. Example:
module "network" {
source = "./modules/network"
cidr = "10.0.0.0/16"
region = "us-west-1"
}
Here, cidr and region are input variables defined within
the network module.
4. providers (Optional):
1. Description: Overrides the default provider configurations for the
module.
2. Usage: Useful when you need to specify different provider settings
for a particular module.
3. Example:
provider "aws" {
region = "us-west-1"
}
module "network" {
source = "./modules/network"
providers = {
aws = aws.us_east
}
}
provider "aws" {
alias = "us_east"
region = "us-east-1"
}
5. count (Optional):
1. Description: Creates multiple instances of the module.
2. Usage: Allows you to scale resources by specifying the number of
instances.
3. Example:
module "network" {
source = "./modules/network"
count = 3
}
This will instantiate the network module three times.
6. for_each (Optional):
1. Description: Creates multiple instances of the module based on
a map or set of strings.
2. Usage: Provides more control compared to count, especially
when each instance requires unique configurations.
3. Example:
module "network" {
source = "./modules/network"
for_each = {
net1 = "10.0.0.0/16"
net2 = "10.1.0.0/16"
}
cidr = each.value
}
This will create two instances of the network module with different
CIDR blocks.
7. depends_on (Optional):
1. Description: Specifies dependencies on other resources or
modules.
2. Usage: Ensures that the module is provisioned only after certain
resources or modules have been created.
3. Example:
module "network" {
source = "./modules/network"
depends_on = [aws_vpc.main]
}
Here, the network module will be created only after
the aws_vpc.main resource is provisioned.
• By utilizing these parameters, you can effectively manage and
customize the behaviour of your Terraform modules, leading to more
modular and maintainable infrastructure configurations.
Terraform Module Block Source Arguments Style
• In Terraform, the source argument within a module block specifies the
location of the module's source code.
• Terraform supports various methods to define this source, allowing
flexibility in module sourcing.
• Here are the different ways to specify the source parameter, along with
examples:
1. Local Paths
• You can reference modules stored locally on your filesystem using
relative paths.
module "network" {
source = "./modules/network"
# Additional module arguments
}
• In this example, Terraform will load the module from
the modules/network directory relative to your current working
directory.
2. Terraform Registry
• Modules can be sourced directly from the Terraform Registry, which
hosts a vast collection of publicly available modules.
module "consul" {
source = "hashicorp/consul/aws"
version = "0.0.5"
# Additional module arguments
}
• Here, the Consul module for AWS is sourced from the Terraform
Registry.
• Specifying the version ensures that Terraform uses the desired module
version.
3. GitHub
• Modules can be sourced from GitHub repositories using
the github.com prefix.
module "vpc" {
source = "github.com/terraform-aws-modules/terraform-aws-vpc"
# Additional module arguments
}
• This example fetches the VPC module from the specified GitHub
repository.
4. Generic Git Repositories
• Terraform supports sourcing modules from any Git repository by
specifying the repository URL.
module "vpc" {
source = "git::https://example.com/terraform-
modules.git//vpc?ref=tags/v0.1.0"
# Additional module arguments
}
• In this case, Terraform retrieves the vpc module from the specified Git
repository at the v0.1.0 tag.
5. Bitbucket
• Modules can also be sourced from Bitbucket repositories.
module "vpc" {
source = "bitbucket.org/organization/terraform-
modules.git//vpc?ref=tags/v0.1.0"
# Additional module arguments
}
• This example fetches the vpc module from a Bitbucket repository at
the specified tag.
6. HTTP URLs
• Modules can be downloaded from HTTP URLs pointing to a ZIP
archive of the module.
module "network" {
source = "https://example.com/terraform-modules/network.zip"
# Additional module arguments
}
• Terraform will download and extract the module from the specified
URL.
7. Amazon S3 Buckets
• Modules stored in Amazon S3 buckets can be referenced directly.
module "network" {
source = "s3::https://s3.amazonaws.com/mybucket/terraform-
modules/network.zip"
# Additional module arguments
}
• Here, Terraform retrieves the module from the specified S3 bucket.
8. Google Cloud Storage (GCS) Buckets
• Similarly, modules can be sourced from GCS buckets.
module "network" {
source =
"gcs::https://storage.googleapis.com/mybucket/terraform-
modules/network.zip"
# Additional module arguments
}
• Terraform will download the module from the specified GCS bucket.
Note on Parameterizing the source Argument
• As of Terraform v0.13, the source argument must be a literal string and
cannot directly reference variables or expressions.
• This design ensures that module sources are known before evaluating
the configuration.
• By utilizing these methods, you can flexibly source Terraform modules
from various locations, tailoring your infrastructure as code to your
project's needs.
• In Terraform, you can specify a module's source using various Git
options to control which version or part of the repository to use.
• Here are examples illustrating different scenarios:
1. Default Branch
• By default, Terraform fetches the module from the repository's default
branch (usually main or master):
module "network" {
source = "git::https://github.com/username/repository.git"
# Additional module arguments
}
2. Specific Branch
• To use a specific branch, append the ref parameter with the branch
name:
module "network" {
source =
"git::https://github.com/username/repository.git?ref=branch-
name"
# Additional module arguments
}
3. Specific Tag
• To use a specific tag, set the ref parameter to the tag name:
module "network" {
source =
"git::https://github.com/username/repository.git?ref=v1.0.0"
# Additional module arguments
}
4. Specific Commit ID
• To use a specific commit, set the ref parameter to the commit hash:
module "network" {
source =
"git::https://github.com/username/repository.git?ref=commit-sha"
# Additional module arguments
}
5. Subdirectory in a Specific Branch
• If the module resides in a subdirectory of a specific branch, specify
both the branch and the subdirectory:
module "network" {
source =
"git::https://github.com/username/repository.git//subdirectory?ref
=branch-name"
# Additional module arguments
}
6. SSH Protocol
• To clone a private repository over SSH, use the SSH URL:
module "network" {
source = "git::ssh://git@github.com/username/repository.git"
# Additional module arguments
}
7. GitHub Shortcut
• For public GitHub repositories, you can use a shorthand notation:
module "network" {
source = "github.com/username/repository"
# Additional module arguments
}
8. Git Over SSH with Subdirectory
• To access a module in a subdirectory over SSH:
module "network" {
source =
"git::ssh://git@github.com/username/repository.git//subdirectory"
# Additional module arguments
}
9. Git Over HTTPS with Authentication
• For private repositories over HTTPS, include the username and token:
module "network" {
source =
"git::https://username:token@github.com/username/repository.git"
# Additional module arguments
}
10. Bitbucket Repository
• To source a module from a Bitbucket repository:
module "network" {
source = "git::https://bitbucket.org/username/repository.git"
# Additional module arguments
}
11. GitLab Repository
• To source a module from a GitLab repository:
module "network" {
source = "git::https://gitlab.com/username/repository.git"
# Additional module arguments
}
12. Using a Depth Parameter for Shallow Clone
• To perform a shallow clone and limit the history depth:
module "network" {
source =
"git::https://github.com/username/repository.git?ref=branch-
name&depth=1"
# Additional module arguments
}
• By utilizing these Git options, you can precisely control which version
or part of a repository Terraform uses for your modules.