Terraform
Infrastructure as code (IaC)
Using code to declaratively describe the desired state of your system. This code can be versioned and stored in source control. IaC solutions like Terraform attempt to establish the state of the system in a consistent and idempotent way. Automating these normally manual processes reduces the risk for human error.
Other benefits include leveraging existing or reusable components, and the code itself becomes the documentation of the infrastructure.
- Creating a basic configuration
- Terraform components
- Providers
- Multiple providers
- Using abstractions and reusable components
What is Terraform?
Terraform is an open-source infrastructure-as-code tool from HashiCorp implemented as a single binary compiled from Go. Configuration files are written in HashiCorp Configuration Language (HCL). Push-based deployment. No agents to install on remote machines (like the K8s Control Plane node).
Terraform automatically stitches together all .tf files in a directory into a single configuration. Once deployed, Terraform keeps track of the current state of the system. When deploying new configurations, Terraform compares the desired state with the current state and makes only the changes necessary.
- Providers: AWS, etc… Public provider plugins can be found at registry.terraform.io.
- Resources: EC2 instance, virtual network, database, etc…
- Data sources: Associated with a provider, list of availability zones, AMI, etc…
- Variables
- Locals
- Modules
- Workspaces
Use a secrets service provider/resource to keep sensitive data secure.
HCL
block_type "label" "name_label" {
key = "value"
nested_block {
key = "value"
}
}
# Setting properties
#
# ┌─ This is from the AWS provider plugin
# │
# │ ┌─ This is a name label
# │ │
resource "aws_instance" "my_webserver" {
name = "web-server"
ebs_volume {
size = 40
}
}
# Reading properties
#
# ┌─ Resource Type
# │
# │ ┌─ Name Label
# │ │
# │ │ ┌─ Attribute
# │ │ │
aws_instance.my_webserver.name # "web-server"
provider "aws" {
access_key = "ACCESS_KEY"
secret_key = "SECRET_KEY"
region = "us_east_1"
}
data "aws_ssm_parameter" "ami" {
name = "/aws/service/ami-amazon-linux/latest/amzn2-ami-hvm-x86_64-gp2"
}
# NETWORKING
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = "true"
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.vpc.id # RTFM to get these fields
}
resource "aws_subnet" "subnet1" {
cidr_block = "10.0.0.0/24"
vpc_id = aws_vpc.vpc.id
map_public_ip_on_launch = "true"
}
# ROUTING
resource "aws_route_table" "rtb" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws
}
}
Workflow
terraform init
downloads providers from the default registry (unless otherwise provided) and sets up a state file.terraform plan
shows you the changes it will make. You can save this to a file for the next step.terraform apply
runs the steps from the plan.terraform destroy
removes everything from the current state. Obviously this is a dangerous, destructive command when used improperly.
Note: Do not hardcode the provider credentials within your configuration files.
Variables and Output
Input variables
variable "name_label" {
type = "<data type>"
description = "Description context"
default = "<default value>"
sensitive = true or false # if true, will not log
}
variable "billing_tag" {} # no properties are required
variable "aws_region" {
type = string
description = "Region to use for AWS resources"
default = "us-east-1"
sensitive = false
}
To use these values, you can refer to them by var.<name_label>
.
Data types
- Primitives
- Strings
- Numbers
- Booleans
- Collections
- Lists
- Sets
- Maps
- Structural
- Tuples
- Objects
variable "aws_regions" {
type = list(string)
default = ["us-east-1", "us-east-2"]
}
var.aws_regions[0]
variable "aws_instance_sizes" {
type = map(string)
default = {
micro = "t2.micro"
small = "t2.small"
large = "t2.large"
}
}
var.aws_instance_sizes.micro
# or
var.aws_instance_sizes["micro"]
- Local values
- Computed
- Supports string interpolation
- Output values
output "name_label" {
value = output_value
description = "Description"
sensitive = true or false
}
output "public_dns_hostname" {
value = aws_instance.nginx1.public_dns
description = "Public DNS name for EC2 instance"
}
locals.tf
main.tf
outputs.tf
variables.tf
To validate your configuration, you can use terraform validate
.
terraform init
terraform validate
Supplying Variable Values
- Set with
default
argument - Using the
-var
flag from the CLI - Using the
-var-file
flag - Including a
terraform.tfvars
orterraform.tfvars.json
file - Including a
.auto.tfvars
or.auto.tfvars.json
file - Environment variables starting with
TF_VAR_
Goes through an order of operations.
- Datasources
- Load balancer
- aws_lb
- aws_lb_target_group
- aws_lb_listener
- aws_lb_target_group_attachment
- security_group
Note: Because most of the work with Terraform is getting comfortable with specific provider resources and their parameters, the registry documentation is an invaluable tool.
Provider plugins are Go binaries that implement resources and data sources. Because of this, they can read environment variables directly, so be sure to check the documentation.
Note: It’s all about knowing the provider resources you need to create to accomplish your goals.
Terraform determines a dependency graph of all of the required resources.
Loops and Functions
- Count meta-argument (
count.index
)- Creates
count
number of resources or modules
- Creates
- For_each
each.key
each.value
aws_s3_bucket_object.taco_toppings["cheese"].id
- For expressions
- Dynamic blocks
# Create a tuple
[ for item in items : tuple_element ]
# Create an object
{ for key, value in map : object_key => object_value }
# IP example
[ for x in range(4) : cidrsubnet("10.0.0.0/16", 8, x) ]
These can be very handy to dynamically scale resources.
Function Categories
- Numeric
min()
max()
- String
lower()
upper()
base64decode()
- Collections
range()
merge()
slice()
- IP networking
cidrsubnet()
- File system
file()
templatefile()
- Type conversion
toset()
Modules
Terraform modules are encapsulations of configuration files so they can be versioned and reused. They can both be local modules or modules found on the Terraform registry.
For example, the terraform-aws-modules/vpc/aws
module manages a number of resources necessary for implementing an AWS VPC, such as the VPC itself, an internet gateway, and subnets.
Output values of a child module can be used by the parent module as inputs. Child modules can take variables and providers as inputs.