Everyday terminal commands
# downloads the providers into .terraform/ and writes the lock file.
terraform init
#Show us what will build, change or destroy
terraform plan
#Better plan. It will create a file called plan.out, that we can apply later.
terraform plan -out=plan.out
# Apply the changes
terraform apply plan.out
# Destroy everything
terraform destroyFiles inside a project
.terraform/ -> My code: the desired state
.terraform.lock.hcl -> Pins exact provider versions (like package-lock.json)
example.tf -> My code: the desired state
plan.out -> changes to be applied
terraform.tfstate -> Critical Mapping of your code to the real AWS resources. Good practice is to back it up in S3 with "terraform state s3"
terraform.tfstate.backup -> Previous version of the state, auto-created before each change
For .gitignore
.terraform/
*.tfstate
*.tfstate.*
plan.outExample of terraform file for launching ec2 instance
# Configure the AWS Provider. AWS with region us-east-1. It can be any other provider like gcp, azure, etc.
provider "aws" {
region = "us-east-1"
}
# A data source is a read-only lookup. Terraform doesn't create anything with it; it just asks AWS "give me information about something that already exists."
# I can use this thing later when creating resources
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
# This is the resouce that will be provisioned in AWS
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "Hello Terraform"
}
}Terraform state s3 – Backup of most critical file. Do not lose it with the machine hosting it
https://developer.hashicorp.com/terraform/language/backend/s3
Create a new file called: remote_state.tf
terraform {
backend "s3" {
bucket = "mybucket"
key = "path/to/my/key"
region = "us-east-1"
}
}Variables
Like parameters in cloudformation. If no default value is provided it will ask while terraform plan
variables.tf
variable "ec2_instance_type" {
type = string
default = "t3.micro"
}
inside main.tf
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
instance_type = var.ec2_instance_type
}
How to override the values:
terraform apply -var="ec2_instance_type=t3.micro"
How to specify a lot of variables:
.tfvars files, for example prod.tfvars
ec2_instance_type = "t3.micro"
to tell terraform about this file
terraform plan -var-file=prod.tfvars
If you rename prod.tfvars to terraform.tfvars or prod.auto.tfvars, Terraform loads it without any flag -var-file=.Outputs
An output block prints a value after apply and stores it in the state, so you can read it later with terraform output. Other Terraform configurations and modules can read it too.
The reference syntax is <resource_type>.<name>.<attribute>. Which attributes exist is listed in the provider docs under "Attribute Reference" for each resource.
outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.example.id
}
output "instance_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.example.public_ip
}
output "ami_used" {
value = data.aws_ami.ubuntu.id
}
After terraform apply you'll see:
Outputs:
ami_used = "ami-0abc123..."
instance_id = "i-0123abcd..."
instance_public_ip = "54.12.34.56"Locals
Named values computed INSIDE the code. Like variables in a shell script.
Can't be overridden from outside (unlike variables).
Example
Say you have 3 resources, each needing the same name prefix and tags:
resource "aws_instance" "web" {
tags = {
Name = "myapp-dev-web"
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_instance" "db" {
tags = {
Name = "myapp-dev-db"
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_security_group" "sg" {
tags = {
Name = "myapp-dev-sg"
Environment = "dev"
ManagedBy = "terraform"
}
}
To change dev to prod, you edit 6 places. Easy to miss one.
The same with locals
locals {
env = "dev"
name_prefix = "myapp-${local.env}"
common_tags = {
Environment = local.env
ManagedBy = "terraform"
}
}
resource "aws_instance" "web" {
tags = merge(local.common_tags, { Name = "${local.name_prefix}-web" })
}
resource "aws_instance" "db" {
tags = merge(local.common_tags, { Name = "${local.name_prefix}-db" })
}
resource "aws_security_group" "sg" {
tags = merge(local.common_tags, { Name = "${local.name_prefix}-sg" })
}
Modules from terraform repo
Module for building a VPC : https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest
create a file vpc.tf with example from the page
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = true
tags = {
Terraform = "true"
Environment = "dev"
}
}Create own module and use it
A module is just a folder containing .tf files. That’s the whole definition.Every Terraform project is already a module: the folder where you run terraform plan is called the root module.Any other folder you call from it is a child module.
Does a subfolder automatically mean it’s a module?
No. Terraform never guesses. A subfolder is a module only if some module block points at it with source. Terraform ignores subfolders otherwise: running plan in the root reads only the .tf files directly in that folder.
Convention is to put local modules under modules/:
my-project/
├── main.tf
├── variables.tf
├── outputs.tf
└── modules/
└── ec2/
├── main.tf
├── variables.tf
└── outputs.tfCreate the module. For example modules/ec2/variables.tf
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "name" {
type = string # no default = required input
}modules/ec2/main.tf
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "this" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = {
Name = var.name
}
}modules/ec2/outputs.tf
output "instance_id" {
value = aws_instance.this.id
}
output "public_ip" {
value = aws_instance.this.public_ip
}In the root main.tf, calling it. Take a close look how we place name and instance_type from module variables
module "web" {
source = "./modules/ec2"
# inputs = the module's variables
name = "web-server"
instance_type = "t3.small"
}
module "db" {
source = "./modules/ec2" # same module, used a second time
name = "db-server" # instance_type uses the default
}And to read a module’s outputs, in the root outputs.tf
output "web_ip" {
value = module.web.public_ip # module.<NAME>.<OUTPUT_NAME>
}