AWS with Terraform Tutorial: Terraform Basics (1)
How to start building AWS infrastructure with Terraform #
Terraform is an Infrastructure as Code (IaC) tool used to provision and manage infrastructure. It helps define and deploy resources across various cloud providers using code, making it easier to maintain and scale infrastructure.
Welcome to our tutorial series where we dive into cloud infrastructure deployment using Terraform or OpenTofu on AWS. In this series, the fundamentals are shown, guiding you through the process of minimizing resource usage and simplifying the deployment complexities associated with cloud infrastructure.
This comprehensive OpenTofu and Terraform tutorial guides you step-by-step through creating infrastructure in AWS using Terraform.
Infrastructure as Code (IaC) helps maintain consistency, enables version control, enhances collaboration among teams, allows for easier replication of environments, streamlines the deployment and management of infrastructure boosting efficiency, and reducing errors in managing complex systems.
Prerequisites #
- Install Terraform or OpenTofu.
- AWS account, see How to Create an AWS Account for demos.
- AWS CLI profile, see How to Install AWS CLI V2.
- Having a remote version control system is suggested (e.g. GitHub).
- A code editor, the free Microsoft Visual Studio Code is recommended.
- Read IT Wonder Lab terms of use.
Terraform Introduction #
Terraform reads configuration files written in HCL (HashiCorp Configuration Language) that define infrastructure. Terraform supports many different cloud providers, each provider along with its release is defined in the Terraform configuration file, and a plugin binary supporting the integration is downloaded during the initialization phase.
Previously named OpenTF, OpenTofu is a fork of Terraform that is open-source, community-driven, and managed by the Linux Foundation, it was created in response to HashiCorp’s switch from an open-source license to the BUSL. OpenTofu was a drop-in replacement for Terraform when it was created, and the two tools remain largely compatible.
For the content of this tutorial Terraform and OpenTofu are equivalent. Choose one of them and use it consistently throughout the tutorial. If you decide to switch tools, read How to Migrate Infrastructure from Terraform to OpenTofu.
The provider acts as a translator between Terraform and the API of the infrastructure or service that Terraform will manage.
A Quick Introduction to the HashiCorp Configuration Language #
HashiCorp Configuration Language (HCL) is a declarative, human-readable language used to configure various HashiCorp tools like Terraform / OpenTofu, Vault, Nomad, and Consul. It aims to be both user-friendly and machine-parseable, making it an excellent choice for infrastructure as code (IaC) and other configuration management tasks.
During the tutorial, most of the elements of HCL will be explained and included in the examples.
Key elements of HCL for Terraform:
File types
Terraform reads all the files ending in .tf located in the directory where the CLI is running, together with the variable files terraform.tfvars and *.auto.tfvars. Subdirectories are not read unless a module uses them. The names of the files and the order of the blocks inside them do not affect the execution.
Configuration Files
Terraform configuration files .tf hold Terraform blocks (resources, variables, modules) that define infrastructure.
State file
The Terraform state file is a record of the infrastructure managed by Terraform. It keeps track of the resources Terraform manages and their current state. This file is essential for Terraform to understand the changes made to infrastructure over time and to plan and execute updates accurately. The file can be stored locally (local state file) or shared with the team in a remote repository (remote backend).
Terraform files could have confidential information and therefore should be shared carefully.
Variable values files
Variable values files use the .tfvars extension and are used to set the value of variables.
Blocks
In Terraform, a “block” refers to a specific section of your configuration file that defines a resource, a provider, a variable, an output, or other configuration elements. These blocks serve as the building blocks for infrastructure as code (IaC) projects in Terraform and are written within curly braces { }
Types of Terraform Blocks:
Provider
Configures the connection to a cloud provider or service like AWS, Azure, GCP, or OCI. The provider block is where the required providers are configured. In this tutorial, the AWS provider is used. (The provider is defined inside the Terraform block, not here)
Provider example: shows the minimal configuration for the AWS provider, specifying a list of credential files, which profile (from the credentials file) to use, and the AWS region this provider will manage.
provider "aws" {
shared_credentials_files = ["~/.aws/credentials"]
profile = "ditwl_infradmin"
region = "us-east-1"
}
Resource
Defines a specific infrastructure resource like an AWS EC2 instance, an Azure VM, or a DigitalOcean droplet.
Resource example: shows a resource block that defines an “aws_vpc” resource named “ditlw-vpc” with the attributes cidr_block, which requires a string representing a CIDR IP range, a comment using # is shown after the value, and a map of strings representing tags in the form tag name = tag value.
resource "aws_vpc" "ditlw-vpc" {
cidr_block = "172.21.0.0/19" #172.21.0.0 - 172.21.31.254
tags = {
Name = "ditlw-vpc"
InfraID = "123"
}
}
The resource can be referenced in other blocks by using its type and name, for example, the vpc_id value is set using a reference to the id output property of the previous resource aws_vpc.ditlw-vpc:
resource "aws_internet_gateway" "ditwl-ig" {
vpc_id = aws_vpc.ditlw-vpc.id
}
Variable
It holds a named value that can be referenced throughout the configuration. Variables increase reusability, reduce errors, and improve flexibility, enabling environment management.
Variables are declared with variable blocks in the configuration files (.tf), and their values are set in variable definition files (.tfvars), on the command line or with environment variables.
Example:
variables.tf: a configuration file that declares two variables. The first one is named provider_default_aws_account_id, has type list of strings and an empty default value. The second one is named aws_sn_zb_pro_pub_36, has type map of strings and no default value, which forces users to set a value.
variable "provider_default_aws_account_id" {
description = "List of allowed AWS account IDs"
type = list(string)
default = []
}
variable "aws_sn_zb_pro_pub_36" {
description = "Zone: B, Env: PRO, Type: PUBLIC, Code: 36"
type = map(string)
}terraform.tfvars: a variable definition file that sets the values of both variables. Terraform loads terraform.tfvars and the files named *.auto.tfvars automatically; other files are loaded with the -var-file option.
provider_default_aws_account_id = ["1234567890"]
aws_sn_zb_pro_pub_36 = {
cidr_block = "172.21.4.0/23"
availability_zone = "us-east-1b"
}The values are used in the configuration with the prefix var.:
resource "aws_subnet" "ditwl-sn-zb-pro-pub-36" {
vpc_id = aws_vpc.ditlw-vpc.id
cidr_block = var.aws_sn_zb_pro_pub_36["cidr_block"]
availability_zone = var.aws_sn_zb_pro_pub_36["availability_zone"]
}Output
Exposes a specific value from the configuration for external use. It is mostly used in modules and for printing information from the created resources.
Example: an IAM user is created and the value of the Encrypted Secret access key is stored in an output block.
# Create an Access Key for the user and encrypt its value using PGP public key
resource "aws_iam_access_key" "project_1_admin" {
user = aws_iam_user.project_1_admin.name
pgp_key = file("~/keys/itwonderlab.com/ditwl_infradmin_gpg_b64_public.key")
}
# Store the Encrypted Secret access key in an Output variables
output "project_1_admin_encrypted_secret_access_key" {
value = aws_iam_access_key.project_1_admin.encrypted_secret
}
The value of the output variable can be used in automation pipelines, for example, the Encrypted Secret access key is exported as raw text, base64 decoded, decrypted, and stored in a file.
terraform output -raw project_1_admin_encrypted_secret_access_key | base64 --decode | gpg --decrypt --output .private/project_1_admin_encrypted_secret
Attributes
Specify key-value pairs to define properties within a block. See previous that set block attributes.
Expressions
Expressions are computed values produced by functions or references:
Functions
Terraform includes many built-in functions for data and string manipulation.
Example: generate a map from multiple maps.
tags = merge(
var.instance_tags,
{
"private_name" = var.tag_private_name,
"public_name" = var.tag_public_name,
"environment" = var.tag_environment,
"app" = var.tag_app,
"app_id" = var.tag_app_id,
"os" = var.tag_os,
"os_id" = var.tag_os_id,
"cost_center" = var.tag_cost_center
"Name" = var.name
}
)
References
Access values from other resource definitions, outputs, or properties. References are used to specify output from one resource into another.
- Resource: access values from other blocks by specifying the resource type and its unique name.
- Input variables: same functionality as variables but used as module inputs.
- Local values: a way to avoid repeating the same values or expressions multiple times.
- Child module outputs: access output variables from modules.
- Data sources: Find existing infrastructure resources not managed in the Terraform configuration and make them available as input for managed infrastructure.
- Filesystem and workspace info: information about the local filesystem and the workspace; its usage is not recommended.
- Block-local values: variables generated inside blocks by the expressions used.
Operators
Arithmetic and Logical Operators are used for basic arithmetic (e.g. add numbers), test and compare values (e.g. equal, greater), and perform logical boolean operations.
Conditions
For choosing between two values depending on the evaluation of the condition.
Example:
disable_api_termination = var.is_production ? true : false
For Iterations and Splat Expressions
Iterates over a list of elements applying a function to each value.
Dynamic Blocks
Creates block values by iterating over a list of elements.
Comments
Add explanatory notes to your configurations using the # symbol.
Terraform Execution #
Terraform is a command line utility that can be scripted for CI/CD pipelines or used in Terraform Automation and Collaboration tools (TACOS) like env0, Spacelift or HCP Terraform (formerly Terraform Cloud). A pipeline built with GitHub Actions is shown in Terraform CI/CD.
In this tutorial, Terraform is used through the command line but can be later moved into a CI/CD pipeline or a TACOS environment as the configuration is completely portable.
Terraform CLI (Command Line Interface) has 3 basic commands:
Terraform init #
Terraform init prepares the environment:
- Initializes the working directory: creates a hidden
.terraformdirectory where Terraform stores the downloaded providers and modules and the configuration of the backend. - Downloads and installs required plugins: Terraform providers are implemented as plugins, and init automatically locates and installs any plugins required by your configuration files. This includes fetching the appropriate versions for the specific cloud provider or service (e.g., AWS, Azure, GCP).
- Initializes or migrates remote state backend (optional): If a remote state backend like HCP Terraform or S3 (see Terraform Backends) has been configured or changed, init will configure the connection to the backend and migrate the remote state if needed.
- Downloads remote modules (optional): If the configuration uses any external Terraform modules (reusable code blocks), init downloads them and verifies their compatibility with your local environment.
- Creates the dependency lock file
.terraform.lock.hcl: records the exact versions of the providers that were selected, so everyone who works on the project uses the same ones. Commit it to Git. - Validates the configuration (basic):
initchecks the syntax of the configuration files. Useterraform validatefor a complete validation.
Example:
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed by HashiCorp)
Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.
Terraform has been successfully initialized!
...$ tofu init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed, key ID 0C0AF313E5FD9F80)
Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://opentofu.org/docs/cli/plugins/signing/
OpenTofu has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that OpenTofu can guarantee to make the same selections by default when
you run "tofu init" in the future.
OpenTofu has been successfully initialized!
...Terraform Plan #
Terraform plan compares the local (or remote) state file with the real infrastructure, pointing out the differences in 3 categories:
- Add (+): the infrastructure resource needs to be created in the Cloud as it is declared in the Terraform configuration but doesn’t exist in the Cloud or a change has been made that requires destroying and adding the resource again.
- Change (~): the infrastructure resource has been modified in the Terraform configuration file and the change needs to be done in the cloud.
- Destroy (-): the resource is no longer needed, it has been removed from the Terraform configuration file and needs to be removed from the cloud.
$ tofu plan
aws_subnet.ditwl-sn-za-pro-pub-00: Refreshing state... [id=subnet-08de387c24b922975]
aws_vpc.ditlw-vpc: Refreshing state... [id=vpc-069d2b63cdd839343]
OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
~ update in-place
- destroy
OpenTofu will perform the following actions:
# aws_subnet.ditwl-sn-za-pro-pri-02 will be created
+ resource "aws_subnet" "ditwl-sn-za-pro-pri-02" {
+ availability_zone = "us-east-1a"
+ availability_zone_id = (known after apply)
+ cidr_block = "172.21.2.0/23"
+ tags = {
+ "Name" = "ditwl-sn-za-pro-pri-02"
}
+ tags_all = {
+ "Name" = "ditwl-sn-za-pro-pri-02"
+ "cost_center" = "sales-department"
+ "environment" = "pro"
}
+ vpc_id = "vpc-069d2b63cdd839343"
}
# aws_subnet.ditwl-sn-za-pro-pub-00 will be destroyed
# (because aws_subnet.ditwl-sn-za-pro-pub-00 is not in configuration)
- resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
- arn = "arn:aws:ec2:us-east-1:431960152202:subnet/subnet-08de387c24b922975" -> null
...
- availability_zone = "us-east-1a" -> null
- availability_zone_id = "use1-az1" -> null
- cidr_block = "172.21.0.0/23" -> null
- vpc_id = "vpc-069d2b63cdd839343" -> null
}
# aws_vpc.ditlw-vpc will be updated in-place
~ resource "aws_vpc" "ditlw-vpc" {
id = "vpc-069d2b63cdd839343"
~ tags = {
"Name" = "ditlw-vpc"
+ "tool" = "Terraform"
}
~ tags_all = {
+ "tool" = "Terraform"
# (3 unchanged elements hidden)
}
# (14 unchanged attributes hidden)
}
Plan: 1 to add, 1 to change, 1 to destroy.
────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if you run "tofu apply" now.
Terraform Apply #
Terraform apply executes the changes defined in the infrastructure configuration. It takes the plan and applies it to the actual cloud infrastructure, adding, changing, or destroying resources needed to match the configuration defined in the Terraform Configuration file/s. This command brings the desired state of the infrastructure to reality.
The output of the command shows the planned changes indicating what will be created (+), changed (~) or destroyed (-). User confirmation for the desired changes is asked before proceeding with the execution.
$ tofu apply
(output has been redacted)
OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
OpenTofu will perform the following actions:
# aws_subnet.ditwl-sn-za-pro-pub-00 will be created
+ resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
+ arn = (known after apply)
+ availability_zone = "us-east-1a"
+ cidr_block = "172.21.0.0/23"
+ ipv6_native = false
+ tags = {
+ "Name" = "ditwl-sn-za-pro-pub-00"
}
+ tags_all = {
+ "Name" = "ditwl-sn-za-pro-pub-00"
+ "cost_center" = "sales-department"
+ "environment" = "pro"
}
+ vpc_id = (known after apply)
}
# aws_vpc.ditlw-vpc will be created
+ resource "aws_vpc" "ditlw-vpc" {
+ arn = (known after apply)
+ cidr_block = "172.21.0.0/19"
+ instance_tenancy = "default"
+ tags = {
+ "Name" = "ditlw-vpc"
}
+ tags_all = {
+ "Name" = "ditlw-vpc"
+ "cost_center" = "sales-department"
+ "environment" = "pro"
}
}
Plan: 2 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
OpenTofu will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_vpc.ditlw-vpc: Creating...
aws_vpc.ditlw-vpc: Creation complete after 4s [id=vpc-069d2b63cdd839343]
aws_subnet.ditwl-sn-za-pro-pub-00: Creating...
aws_subnet.ditwl-sn-za-pro-pub-00: Still creating... [10s elapsed]
aws_subnet.ditwl-sn-za-pro-pub-00: Creation complete after 13s [id=subnet-08de387c24b922975]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Common Terraform Questions #
Terraform configurations are portable across cloud providers? #
No, Terraform Configurations are portable across the same cloud provider (changing the region) but not across different cloud providers as each one has its own set of services.
All Cloud Resources need to be managed by Terraform #
No, Terraform only manages the resources that are listed in its configuration files. Any resource that is created in the Cloud outside Terraform is not affected by Terraform operations but might create dependencies and be affected by changes (e.g. an EC2 instance is created using the AWS console, Terraform is not aware of the instance but using Terraform to destroy the VPC will trigger an error as it can’t be deleted with EC2 instances using it).
Next Steps #
Continue with AWS Basics, or see the full series index.