Terraform on AWS — Full Course
15 modules · 3 hands-on projects · Beginner to Advanced
What is Terraform?
An Infrastructure as Code (IaC) tool. Describe infrastructure in .tf files using HCL, and Terraform creates/manages it on AWS or any cloud.
- terraform initDownload providers, set up backend
- terraform planPreview changes (dry run)
- terraform applyCreate/update infrastructure
- terraform destroyTear down all resources
- terraform fmtAuto-format .tf files
- terraform validateCheck config for errors
# Configure the AWS provider terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } # Create an EC2 instance resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" tags = { Name = "MyFirstServer" } }
Always run terraform plan before apply. Look for unexpected destroys (shown in red with -).
variable "instance_type" { description = "EC2 instance type" type = string default = "t2.micro" } resource "aws_instance" "web" { instance_type = var.instance_type } locals { env = "dev" name = "myapp-${local.env}" } output "instance_ip" { value = aws_instance.web.public_ip }
# Fetch latest Amazon Linux 2 AMI data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["amzn2-ami-hvm-*-x86_64-gp2"] } } resource "aws_instance" "web" { ami = data.aws_ami.amazon_linux.id }
Put values in terraform.tfvars. Never commit secrets — use TF_VAR_name env vars or AWS Secrets Manager.
What is state?
terraform.tfstate maps your config to real AWS resources. It records IDs, metadata, and dependencies.
- terraform state listList all resources in state
- terraform state show aws_instance.webShow resource attributes
- terraform state rm aws_instance.webRemove from state (keeps AWS resource)
- terraform import aws_instance.web i-1234Import existing resource into state
import { to = aws_instance.existing id = "i-0a1b2c3d4e5f67890" }
Never manually edit terraform.tfstate. For teams, use remote state (Module 7).
# Security group resource "aws_security_group" "web_sg" { name = "web-sg" ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } # EC2 with security group resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" vpc_security_group_ids = [aws_security_group.web_sg.id] user_data = <<-EOF #!/bin/bash yum install -y httpd systemctl start httpd EOF }
Always prefer IAM roles over access keys for EC2. Instances get temporary credentials automatically — no secrets to manage.
Static Website on S3 + CloudFront
Deploy a static website using S3 for storage and CloudFront as the CDN.
aws_s3_objectWhat is a module?
A directory of .tf files. Call with a module block, pass variables, get outputs. Root = your main dir; child = subdirs or registry.
# Use a registry module module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" name = "my-vpc" cidr = "10.0.0.0/16" } # Access module outputs resource "aws_instance" "web" { subnet_id = module.vpc.public_subnets[0] }
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.1.0" name = "prod-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b"] public_subnets = ["10.0.1.0/24", "10.0.2.0/24"] private_subnets = ["10.0.11.0/24", "10.0.12.0/24"] enable_nat_gateway = true single_nat_gateway = true }
NAT Gateways cost ~$0.045/hr. Use single_nat_gateway = true for learning. Always terraform destroy when done.
terraform { backend "s3" { bucket = "my-terraform-state-bucket" key = "prod/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-locks" encrypt = true } }
- terraform workspace new stagingCreate a new workspace
- terraform workspace select prodSwitch to prod
- terraform workspace listList all workspaces
resource "aws_launch_template" "web" { name_prefix = "web-" image_id = data.aws_ami.amazon_linux.id instance_type = "t3.micro" } resource "aws_autoscaling_group" "web" { desired_capacity = 2 max_size = 5 min_size = 1 target_group_arns = [aws_lb_target_group.web.arn] vpc_zone_identifier = module.vpc.private_subnets launch_template { id = aws_launch_template.web.id version = "$Latest" } }
name: Terraform
on:
push: { branches: [main] }
pull_request:
permissions:
id-token: write
contents: read
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GHRole
aws-region: us-east-1
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform plan
if: github.event_name == 'pull_request'
- run: terraform apply -auto-approve
if: github.ref == 'refs/heads/main'GitHub Actions assumes an AWS IAM role directly via OpenID Connect. No long-lived access keys stored in GitHub secrets.
3-Tier Web App on AWS
Production architecture: VPC + ALB + ASG + RDS with CI/CD.
# for_each over a map resource "aws_s3_bucket" "all" { for_each = { "logs" = "us-east-1", "backups" = "us-west-2" } bucket = "myapp-${each.key}" } # Dynamic block — avoid repeating ingress rules resource "aws_security_group" "web" { dynamic "ingress" { for_each = [80, 443, 8080] content { from_port = ingress.value to_port = ingress.value protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } } # Lifecycle rules resource "aws_instance" "web" { lifecycle { create_before_destroy = true prevent_destroy = true ignore_changes = [ami] } }
resource "aws_ecs_cluster" "main" { name = "app-cluster" } resource "aws_ecs_task_definition" "app" { family = "app" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = "256" memory = "512" container_definitions = jsonencode([{ name = "app" image = "nginx:latest" portMappings = [{ containerPort = 80 }] }]) } resource "aws_ecs_service" "app" { name = "app" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.app.arn desired_count = 2 launch_type = "FARGATE" }
data "archive_file" "zip" { type = "zip" source_file = "lambda/handler.py" output_path = "lambda/handler.zip" } resource "aws_lambda_function" "api" { filename = data.archive_file.zip.output_path function_name = "my-api" role = aws_iam_role.lambda_role.arn handler = "handler.lambda_handler" runtime = "python3.12" source_code_hash = data.archive_file.zip.output_base64sha256 } resource "aws_apigatewayv2_api" "api" { name = "my-api" protocol_type = "HTTP" target = aws_lambda_function.api.arn }
include "root" { path = find_in_parent_folders() } terraform { source = "../../../modules/vpc" } inputs = { cidr = "10.1.0.0/16" environment = "prod" }
tfsec . # Fast static analysis checkov -d . # Broad policy checks trivy config . # Vulnerability scan terraform plan -detailed-exitcode # Exit 2 = drift detected
Add tfsec or Checkov before terraform plan in GitHub Actions. Fail pipeline on HIGH/CRITICAL findings.
provider "aws" { alias = "primary" region = "us-east-1" } provider "aws" { alias = "dr" region = "us-west-2" } resource "aws_s3_bucket" "primary" { provider = aws.primary bucket = "myapp-primary" } resource "aws_s3_bucket" "dr" { provider = aws.dr bucket = "myapp-dr-replica" } # Cross-account assume role provider "aws" { alias = "prod_account" assume_role { role_arn = "arn:aws:iam::PROD_ID:role/TerraformRole" } }
Production EKS Platform (Multi-Account)
Build a full production platform using everything in this course.
