Terraform Course - Automate your AWS cloud infrastructure

Arijit DasArijit Das
12 min read

1. AWS Setup

  • Creating AWS account

  • Accessing AWS Console

  • Generating Access Keys (for later Terraform use)

  • Setting IAM permissions (if needed)

  • Security recommendations

2. Windows Setup

  • Downloading Terraform binary

  • Adding Terraform to system PATH

  • Verifying installation

  • Testing with a simple terraform version command

3. MacOS Setup

  • Installing via Homebrew (recommended)

  • Manual download method (optional)

  • Setting up environment variables

  • Verifying installation

4. Linux Install

  • Ubuntu/Debian: Install via apt

  • Fedora/RedHat: Install via dnf or manual method

  • Setting path and permissions

  • Verifying installation

5. VS Code

  • Installing VS Code

  • Installing Terraform extension (HashiCorp Terraform)

  • Recommended extensions (YAML, AWS Toolkit)

  • Syntax highlighting, IntelliSense

  • VS Code + Terminal integration for Terraform

6. Essential Terraform Commands (Must Know)

Before Terraform can provision infrastructure, it goes through a specific workflow. Here are the most essential commands used in this lifecycle:

CommandPurpose
terraform initInitializes the Terraform working directory and downloads the required provider plugins (like AWS).
terraform validateValidates whether the configuration is syntactically correct.
terraform planGenerates and displays an execution plan. Shows what Terraform will do without making any changes.
terraform applyApplies the planned changes and provisions the resources on the cloud (e.g., creates EC2).
terraform destroyDestroys all the resources defined in the current directory's .tf file.

Run these commands before starting your work:

# Step 1: Initialize the working directory
terraform init

# Step 2: Validate the configuration syntax
terraform validate

# Step 3: See the execution plan (what Terraform will do)
terraform plan

7. Terraform Configuration: Provisioning EC2 Instance on AWS

Below is a basic Terraform file (main.tf) that provisions a t2.micro EC2 instance in us-east-1 region.

provider "aws" {
  region     = "us-east-1"
  access_key = "your_access_key"
  secret_key = "your_secret_key"
}

resource "aws_instance" "my-first-instance" {
  ami           = "ami-084568db4383264d4"
  instance_type = "t2.micro"
  tags = {
    Name = "ubuntu"
  }
}

Detailed Explanation of the Code

1. Provider Block

provider "aws" {
  region     = "us-east-1"
  access_key = "your_access_key"
  secret_key = "your_secret_key"
}
  • provider "aws": Specifies that you're using AWS as your infrastructure provider.

  • region: AWS region where resources will be deployed. Here it's "us-east-1" (N. Virginia).

  • access_key & secret_key: Credentials to authenticate your AWS account.

    ⚠️ Never hardcode your access keys in production. Use environment variables or AWS profiles instead.

2. Resource Block: aws_instance

resource "aws_instance" "my-first-instance" {
  ami           = "ami-084568db4383264d4"
  instance_type = "t2.micro"
  tags = {
    Name = "ubuntu"
  }
}
  • resource: This declares a resource to be created.

  • "aws_instance": Type of resource (an EC2 instance here).

  • "my-first-instance": A local name/reference to this instance.

  • ami: The Amazon Machine Image ID (Ubuntu Server in this case).

  • instance_type: Specifies the instance size. t2.micro is eligible for the AWS Free Tier.

  • tags: Useful for identification or categorization. The tag here names the instance "ubuntu".

Running the Code (Step-by-Step)

Open your terminal or VS Code integrated terminal and follow these steps:

# Step 1: Apply the changes (create EC2 instance)
terraform apply
# Confirm with "yes" when prompted

# Step 2 (Optional): Destroy resources when done
terraform destroy
# Confirm with "yes" to tear down

8. Provisioning a VPC and Subnet in AWS Using Terraform

erraform Configuration

provider "aws" {
  region     = "us-east-1"
  access_key = "your_access_key" 
  secret_key = "your_secret_key" 
}

resource "aws_vpc" "first-vpc" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "production-vpc"
  }
}

resource "aws_subnet" "subnet-1" {
  vpc_id     = aws_vpc.first-vpc.id
  cidr_block = "10.0.1.0/24"

  tags = {
    Name = "production-subnet-1"
  }
}

Explanation

🔸 provider "aws" Block

Same as before — it defines the AWS provider and credentials.

🔸 resource "aws_vpc" "first-vpc" Block

This block provisions a custom Virtual Private Cloud (VPC):

  • aws_vpc: Resource type for creating a VPC.

  • first-vpc: Local name for the resource, used internally in the configuration.

  • cidr_block = "10.0.0.0/16": The IP address range for the VPC.

    • This means the VPC will support up to 65,536 IP addresses.
  • tags: Adds a "Name" tag to help you identify the VPC in AWS Console.

🔸 resource "aws_subnet" "subnet-1" Block

This creates a subnet inside the VPC:

  • aws_subnet: Resource type for creating a subnet.

  • subnet-1: Local identifier for this subnet.

  • vpc_id = aws_vpc.first-vpc.id:

    • This links the subnet to the previously created VPC using its .id.

    • Terraform automatically resolves this reference during execution.

  • cidr_block = "10.0.1.0/24": Subnet range (256 IP addresses).

  • tags: Tags the subnet for easier management and identification.

Commands to Execute

Apply and Provision Infrastructure

terraform apply

Type yes when prompted to actually create the VPC and Subnet.

Destroy the Resources (Optional Cleanup)

terraform destroy

This will delete the VPC and the subnet. Again, confirm with yes.

9. TERRAFORM PRACTICE PROJECT — Full Infrastructure Deployment on AWS

Provider Block

provider "aws" {
  region     = "us-east-1"
  access_key = "your_access_key"
  secret_key = "your_secret_key"
}
  • This sets up Terraform to use AWS as the provider.

  • access_key and secret_key authenticate to your AWS account. (⚠️ Never share these publicly.)

  • region: Defines which region resources will be created in (us-east-1 is N. Virginia).

Create VPC

resource "aws_vpc" "prod-vpc" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "production"
  }
}
  • VPC = Virtual Private Cloud = a logically isolated network in AWS.

  • cidr_block = "10.0.0.0/16" creates a large IP range (65536 IPs).

  • tags: Adds a name to make the VPC identifiable in the AWS console.

Create Internet Gateway

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.prod-vpc.id
}
  • Provides internet access to resources inside the VPC (like EC2).

  • Must be attached to a VPC.

Create Custom Route Table

resource "aws_route_table" "prod-route-table" {
  vpc_id = aws_vpc.prod-vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  route {
    ipv6_cidr_block = "::/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "Prod"
  }
}
  • Defines how traffic leaves your VPC.

  • 0.0.0.0/0: All IPv4 traffic goes to Internet Gateway.

  • ::/0: All IPv6 traffic goes to Internet Gateway.

  • Must be associated with a subnet (next step).

Create Subnet

resource "aws_subnet" "subnet-1" {
  vpc_id     = aws_vpc.prod-vpc.id
  cidr_block = "10.0.1.0/24"
  availability_zone = "us-east-1a"
  tags = {
    Name = "prod-subnet"
  }
}
  • A subdivision of your VPC's CIDR block.

  • 10.0.1.0/24 gives 256 usable IPs.

  • Created in AZ us-east-1a, ensuring low-latency access between resources in the same zone.

Associate Route Table with Subnet

resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.subnet-1.id
  route_table_id = aws_route_table.prod-route-table.id
}
  • Links the subnet to the custom route table to enable internet access.

Create Security Group (Allow Port 22, 80, 443)

resource "aws_security_group" "allow_web" {
  name        = "allow_web_traffic"
  description = "Allow Web inbound traffic"
  vpc_id      = aws_vpc.prod-vpc.id

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "SSH"
    from_port   = 2
    to_port     = 2
    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"]
  }

  tags = {
    Name = "allow_web"
  }
}
  • Defines firewall rules.

  • Allows:

    • Port 443 (HTTPS)

    • Port 80 (HTTP)

    • Port 2

  • egress rule: allows all outbound traffic.

  • Security group is applied later to the Network Interface.

Create Network Interface

resource "aws_network_interface" "web-server-nic" {
  subnet_id       = aws_subnet.subnet-1.id
  private_ips     = ["10.0.1.50"]
  security_groups = [aws_security_group.allow_web.id]
}
  • Creates an Elastic Network Interface (ENI).

  • ENI is attached to an EC2 instance for communication.

  • Uses a static private IP (10.0.1.50) and a security group.

Assign Elastic IP to ENI

resource "aws_eip" "one" {
  vpc                        = true
  network_interface          = aws_network_interface.web-server-nic.id
  associate_with_private_ip  = "10.0.1.50"
  depends_on                 = [ aws_internet_gateway.gw ]
}
  • Allocates a public IP address (Elastic IP).

  • Associates it with the ENI, making the EC2 instance publicly reachable.

  • Depends on IGW to ensure EIP works.

Create Ubuntu EC2 Instance + Install Apache2

resource "aws_instance" "web-server-instance" {
  ami               = "ami-084568db4383264d4"
  instance_type     = "t2.micro"
  availability_zone = "us-east-1a"
  key_name          = "firstkey"

  network_interface {
    device_index         = 0
    network_interface_id = aws_network_interface.web-server-nic.id
  }

  user_data = <<-EOF
              #!/bin/bash
              sudo apt update -y
              sudo apt install apache2 -y
              sudo systemctl enable apache2
              sudo bash -c 'echo your very first web server > /var/www/html/index.html'
              EOF

  tags = {
    Name = "web-server"
  }
}
  • Launches a Ubuntu EC2 instance.

  • AMI: Official Ubuntu Server AMI.

  • Instance type: t2.micro (Free-tier eligible).

  • Key pair: Required for SSH access. Replace firstkey with your key.

  • Attaches the previously created ENI.

  • user_data: Shell script runs on boot to:

    • Install Apache2

    • Enable the service

    • Create a test web page

Final Outcome

After running terraform apply:

  • You’ll have a publicly accessible web server at the Elastic IP.

  • Visiting it in a browser will show the text:

    your very first web server

Terraform Commands to Run

terraform apply         # Apply the configuration

Use terraform destroy if you want to clean up resources.


⚠️ Important Notes

  • Never share AWS credentials in public files or repositories. Use .tfvars or environment variables instead.

  • Make sure the key pair "firstkey" exists in AWS EC2 > Key Pairs.

10. Breakdown of Each Output Variable

1. server_public_ip

hclCopyEditoutput "server_public_ip" {
  value = aws_eip.one.public_ip
}
  • Purpose: Displays the Elastic IP (public IP) address assigned to your EC2 instance.

  • Resource: aws_eip.one

  • Attribute: .public_ip

  • Use case: Helps you SSH into the server or open it in a browser (http://<public_ip>).

2. server_private_ip

hCopyEditoutput "server_private_ip" {
  value = aws_instance.web-server-instance.private_ip
}
  • Purpose: Displays the private IP assigned to your EC2 instance inside the VPC.

  • Resource: aws_instance.web-server-instance

  • Attribute: .private_ip

  • Use case: Useful for internal networking, debugging subnet behavior, or communication within the VPC.

3. server_id

hclCopyEditoutput "server_id" {
  value = aws_instance.web-server-instance.id
}
  • Purpose: Outputs the unique instance ID of your EC2 server.

  • Example: Looks like i-0a1b2c3d4e5f6g7h8.

  • Use case: Useful for referencing the instance in logs, dashboards, or automation scripts.

Usage Example

After running:

terraform apply

You’ll see output like:

Outputs:

server_id = "the_server-id"
server_private_ip = "the_server_private_ip"
server_public_ip = "the_server_public_ip"

11. Terraform Target Resource

In Terraform, targeting a specific resource means applying changes only to that particular resource instead of the whole infrastructure defined in the configuration. This is useful when:

  • You're debugging or testing a single component.

  • A specific resource fails and needs to be recreated.

  • You don’t want to affect other parts of your setup.

Syntax: -target Option

bashCopyEditterraform apply -target=resource_type.resource_name

Example from Your Practice Project

Assume you have already initialized Terraform:

bashCopyEditterraform init

Now, let’s say you only want to create or update the EC2 instance (aws_instance.web-server-instance) without applying the whole infrastructure again.

bashCopyEditterraform apply -target=aws_instance.web-server-instance

This will only:

  • Provision the EC2 instance.

  • Run user_data to install and configure Apache.

  • Attach the previously created network interface.

More Target Examples from Your Code

Resource DescriptionTerraform Target Command
Create only the VPCterraform apply -target=aws_vpc.prod-vpc
Create only the Internet Gatewayterraform apply -target=aws_internet_gateway.gw
Create only the Route Tableterraform apply -target=aws_route_table.prod-route-table
Create only the Security Groupterraform apply -target=aws_security_group.allow_web
Create only the Elastic IPterraform apply -target=aws_eip.one

Notes & Best Practices

  • You can combine multiple targets:

      bashCopyEditterraform apply -target=aws_vpc.prod-vpc -target=aws_subnet.subnet-1
    
  • Targeting is not recommended for regular workflows. It bypasses dependency graph logic and can cause inconsistent states if used carelessly.

  • Use it mostly for debugging, step-by-step resource creation, or when instructed in CI/CD pipelines.

12. Terraform Variables

What Are Terraform Variables?

Terraform variables allow you to parameterize your configuration. Instead of hardcoding values like CIDR blocks or names, you define variables in one place and reference them throughout your infrastructure code.

This enhances:

  • Reusability

  • Flexibility

  • Readability

  • Separation of configuration and logic

main.tf – Code Breakdown

🔹 Variable Declaration

hclCopyEditvariable "subnet_prefix" {
  description = "cidr block for the subnet"
  # default = "10.0.66.0/24"
}
  • variable "subnet_prefix": Declares a variable.

  • No default is set here, so the value must come from .tfvars or CLI input.

  • Expected to be a list of maps (each map with keys cidr_block and name).

VPC Resource

hclCopyEditresource "aws_vpc" "prod-vpc" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "production"
  }
}
  • Creates the base Virtual Private Cloud (VPC).

  • It will house your subnets and other AWS resources.

Subnet Resources (Dynamic Using Variables)

hclCopyEditresource "aws_subnet" "subnet-1" {
  vpc_id = aws_vpc.prod-vpc.id
  cidr_block = var.subnet_prefix[0].cidr_block
  availability_zone = "us-east-1a"
  tags = {
    Name = var.subnet_prefix[0].name
  }
}
  • Creates the first subnet with the first item in the list ([0]).

  • Dynamically assigns CIDR and name from the subnet_prefix variable.

hCopyEditresource "aws_subnet" "subnet-2" {
  vpc_id = aws_vpc.prod-vpc.id
  cidr_block = var.subnet_prefix[1].cidr_block
  availability_zone = "us-east-1a"
  tags = {
    Name = var.subnet_prefix[1].name
  }
}
  • Similar to subnet-1, but uses the second object in the list ([1]).

terraform.tfvars – Defining Variable Values

hCopyEditsubnet_prefix = [
  {
    cidr_block = "10.0.1.0/24"
    name       = "prod-subnet"
  },
  {
    cidr_block = "10.0.2.0/24"
    name       = "dev-subnet"
  }
]
  • This is a list of maps, each with:

    • cidr_block: the IP range for the subnet.

    • name: subnet tag.

  • Automatically picked up when you run terraform apply if the filename is terraform.tfvars.

How to Run

bashCopyEditterraform init           # Initialize the working directory
terraform plan           # See the execution plan with variables applied
terraform apply          # Apply changes using values from terraform.tfvars

Or explicitly:

bashCopyEditterraform apply -var-file="terraform.tfvars"

Benefits of Using Variables

FeatureBenefit
ReusabilityChange CIDRs/subnet names without editing code
Cleaner CodeAvoid clutter by abstracting values
ScalabilityEasily manage multiple subnets, VPCs, etc.
Config ManagementKeep sensitive or environment-specific data separate

Bonus Tip – Variable Type Safety

You can (and should) define a type for clarity and validation:

hCopyEditvariable "subnet_prefix" {
  type = list(object({
    cidr_block = string
    name       = string
  }))
  description = "List of subnets with CIDR and name"
}

This helps Terraform validate the input structure before execution.

0
Subscribe to my newsletter

Read articles from Arijit Das directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Arijit Das
Arijit Das