1. Teaching Objectives

By the end of this module, students will be able to: - Understand Infrastructure as Code (IaC) and why Terraform matters - Write basic Terraform configuration (HCL syntax) - Provision an EKS cluster using Terraform - Deploy Kubernetes resources using Terraform - Manage Terraform state and understand its importance - Integrate Terraform into CD pipelines - Use Terraform variables and outputs effectively - Understand Terraform modules for reusability

2. What is Infrastructure as Code (IaC)?

2.1 The Problem IaC Solves

Traditional Infrastructure Management: - Manual clicking through cloud consoles - Inconsistent configurations - No version control - Difficult to reproduce environments - Hard to track changes - Error-prone manual processes

Infrastructure as Code: - Define infrastructure in code files - Version controlled (Git) - Reproducible and consistent - Automated provisioning - Trackable changes - Reviewable (pull requests)

Analogy: - Manual Infrastructure = Hand-drawing blueprints each time - Infrastructure as Code = CAD software with saved, versioned blueprints

2.2 What is Terraform?

Terraform = Open-source IaC tool by HashiCorp - Declarative: Describe what you want, Terraform figures out how - Multi-cloud: Works with AWS, Azure, GCP, Kubernetes, etc. - State management: Tracks what infrastructure exists - Plan before apply: See changes before making them

Key Features: - ✅ Declarative: Define desired state, not steps - ✅ Idempotent: Running multiple times produces same result - ✅ Stateful: Remembers what was created - ✅ Provider-based: Plugins for different services

2.3 Terraform vs Other Tools

Tool Type Best For
Terraform Declarative IaC Multi-cloud, infrastructure provisioning
Ansible Procedural automation Configuration management, orchestration
CloudFormation Declarative (AWS only) AWS-only projects
Pulumi Imperative IaC Using familiar programming languages

3. Terraform Basics

3.1 Installation

macOS:

brew install terraform

Linux:

# Download from https://www.terraform.io/downloads
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/

Windows: - Download from https://www.terraform.io/downloads - Extract and add to PATH

Verify:

terraform version
# Output: Terraform v1.6.0

3.2 HCL (HashiCorp Configuration Language)

Terraform uses HCL syntax. It's human-readable and structured:

# Comments start with #

# Block type: resource
resource "aws_instance" "web" {
  # Arguments (key = value)
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  # Nested blocks
  tags = {
    Name = "Web Server"
    Environment = "production"
  }
}

Basic Structure: - Blocks: resource, variable, output, provider - Arguments: key = value pairs - Expressions: "${var.name}" for interpolation

3.3 Core Concepts

Providers: - Plugins that interact with APIs (AWS, Kubernetes, etc.) - Must be declared and configured

Resources: - Infrastructure components (EC2 instance, EKS cluster, etc.) - Most important block type

Variables: - Input parameters for your configuration - Make configurations reusable

Outputs: - Values to display or use elsewhere - Useful for getting resource information

State: - Tracks what Terraform has created - Stored in terraform.tfstate file - Critical for managing infrastructure

3.4 Basic Terraform Workflow

# 1. Initialize (download providers)
terraform init

# 2. Plan (see what will be created)
terraform plan

# 3. Apply (create infrastructure)
terraform apply

# 4. Destroy (remove infrastructure)
terraform destroy

4. Terraform AWS Provider Setup

4.1 Provider Configuration

# terraform.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  # Optional: Use AWS credentials from environment
  # AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
  # Or use AWS CLI profile: shared_credentials_file = "~/.aws/credentials"
}

4.2 AWS Authentication

Option 1: Environment Variables

export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_DEFAULT_REGION=us-east-1

Option 2: AWS CLI Profile

aws configure --profile myprofile

Option 3: IAM Role (for EC2/ECS) - Use IAM roles attached to instances

4.3 Initialize Terraform

# Navigate to Terraform directory
cd infra/eks

# Initialize (downloads AWS provider)
terraform init

# Output shows providers downloaded

5. Provisioning EKS Cluster with Terraform

5.1 EKS Cluster Architecture

To create an EKS cluster, we need: 1. VPC (Virtual Private Cloud) - Network 2. Subnets - Network segments (public/private) 3. Internet Gateway - Internet access 4. NAT Gateway - Outbound internet for private subnets 5. Security Groups - Firewall rules 6. IAM Roles - Permissions 7. EKS Cluster - Kubernetes control plane 8. Node Group - Worker nodes

5.2 Complete EKS Terraform Configuration

Step 1: VPC and Networking

# vpc.tf
# Create VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "eks-vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "eks-igw"
  }
}

# Public Subnets (for load balancers)
resource "aws_subnet" "public" {
  count = 2

  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "eks-public-${count.index + 1}"
    "kubernetes.io/role/elb" = "1"
  }
}

# Private Subnets (for worker nodes)
resource "aws_subnet" "private" {
  count = 2

  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "eks-private-${count.index + 1}"
    "kubernetes.io/role/internal-elb" = "1"
  }
}

# NAT Gateway (for private subnet internet access)
resource "aws_eip" "nat" {
  count = 2

  domain = "vpc"

  tags = {
    Name = "eks-nat-eip-${count.index + 1}"
  }
}

resource "aws_nat_gateway" "main" {
  count = 2

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "eks-nat-${count.index + 1}"
  }
}

# Route Tables
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

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

  tags = {
    Name = "eks-public-rt"
  }
}

resource "aws_route_table" "private" {
  count = 2

  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = {
    Name = "eks-private-rt-${count.index + 1}"
  }
}

# Route Table Associations
resource "aws_route_table_association" "public" {
  count = 2

  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count = 2

  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

# Data source for availability zones
data "aws_availability_zones" "available" {
  state = "available"
}

Step 2: IAM Roles

# iam.tf
# EKS Cluster Role
resource "aws_iam_role" "cluster" {
  name = "eks-cluster-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "eks.amazonaws.com"
        }
      }
    ]
  })
}

# Attach EKS Cluster Policy
resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
  role       = aws_iam_role.cluster.name
}

# EKS Node Group Role
resource "aws_iam_role" "node" {
  name = "eks-node-group-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Attach Node Group Policies
resource "aws_iam_role_policy_attachment" "node_AmazonEKSWorkerNodePolicy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
  role       = aws_iam_role.node.name
}

resource "aws_iam_role_policy_attachment" "node_AmazonEKS_CNI_Policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
  role       = aws_iam_role.node.name
}

resource "aws_iam_role_policy_attachment" "node_AmazonEC2ContainerRegistryReadOnly" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  role       = aws_iam_role.node.name
}

Step 3: Security Groups

# security-groups.tf
# Cluster Security Group
resource "aws_security_group" "cluster" {
  name        = "eks-cluster-sg"
  description = "Security group for EKS cluster"
  vpc_id      = aws_vpc.main.id

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "eks-cluster-sg"
  }
}

# Node Security Group
resource "aws_security_group" "node" {
  name        = "eks-node-sg"
  description = "Security group for EKS nodes"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port = 0
    to_port   = 65535
    protocol  = "tcp"
    self      = true
  }

  ingress {
    from_port       = 1025
    to_port         = 65535
    protocol        = "tcp"
    security_groups = [aws_security_group.cluster.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "eks-node-sg"
  }
}

Step 4: EKS Cluster

# cluster.tf
resource "aws_eks_cluster" "main" {
  name     = var.cluster_name
  role_arn = aws_iam_role.cluster.arn
  version  = var.kubernetes_version

  vpc_config {
    subnet_ids              = concat(aws_subnet.public[*].id, aws_subnet.private[*].id)
    endpoint_private_access = true
    endpoint_public_access  = true
    security_group_ids      = [aws_security_group.cluster.id]
  }

  # Enable logging
  enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]

  depends_on = [
    aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy,
  ]

  tags = {
    Name = var.cluster_name
  }
}

Step 5: Node Group

# node-groups.tf
resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.cluster_name}-node-group"
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = aws_subnet.private[*].id

  scaling_config {
    desired_size = var.node_desired_size
    max_size     = var.node_max_size
    min_size     = var.node_min_size
  }

  instance_types = [var.node_instance_type]

  # Update configuration
  update_config {
    max_unavailable = 1
  }

  # Remote access (optional)
  # remote_access {
  #   ec2_ssh_key = var.ssh_key_name
  # }

  depends_on = [
    aws_iam_role_policy_attachment.node_AmazonEKSWorkerNodePolicy,
    aws_iam_role_policy_attachment.node_AmazonEKS_CNI_Policy,
    aws_iam_role_policy_attachment.node_AmazonEC2ContainerRegistryReadOnly,
  ]

  tags = {
    Name = "${var.cluster_name}-node-group"
  }
}

Step 6: Variables

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "cluster_name" {
  description = "Name of the EKS cluster"
  type        = string
  default     = "my-eks-cluster"
}

variable "kubernetes_version" {
  description = "Kubernetes version"
  type        = string
  default     = "1.28"
}

variable "node_desired_size" {
  description = "Desired number of nodes"
  type        = number
  default     = 2
}

variable "node_max_size" {
  description = "Maximum number of nodes"
  type        = number
  default     = 4
}

variable "node_min_size" {
  description = "Minimum number of nodes"
  type        = number
  default     = 1
}

variable "node_instance_type" {
  description = "EC2 instance type for nodes"
  type        = string
  default     = "t3.medium"
}

Step 7: Outputs

# outputs.tf
output "cluster_id" {
  description = "EKS cluster ID"
  value       = aws_eks_cluster.main.id
}

output "cluster_arn" {
  description = "EKS cluster ARN"
  value       = aws_eks_cluster.main.arn
}

output "cluster_endpoint" {
  description = "EKS cluster endpoint"
  value       = aws_eks_cluster.main.endpoint
}

output "cluster_security_group_id" {
  description = "Security group ID attached to the cluster"
  value       = aws_security_group.cluster.id
}

output "node_security_group_id" {
  description = "Security group ID attached to the nodes"
  value       = aws_security_group.node.id
}

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "cluster_name" {
  description = "EKS cluster name"
  value       = aws_eks_cluster.main.name
}

5.3 Deploying the EKS Cluster

# Navigate to EKS directory
cd infra/eks

# Initialize Terraform
terraform init

# Review plan
terraform plan

# Apply (creates cluster - takes 10-15 minutes)
terraform apply

# Get cluster info
terraform output cluster_endpoint
terraform output cluster_name

6. Deploying Kubernetes Resources with Terraform

6.1 Kubernetes Provider Setup

After creating the EKS cluster, configure the Kubernetes provider:

# k8s/main.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.23"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Configure Kubernetes provider
provider "kubernetes" {
  host                   = data.aws_eks_cluster.main.endpoint
  cluster_ca_certificate = base64decode(data.aws_eks_cluster.main.certificate_authority[0].data)

  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args = [
      "eks",
      "get-token",
      "--cluster-name",
      data.aws_eks_cluster.main.name
    ]
  }
}

# Data source to get cluster info
data "aws_eks_cluster" "main" {
  name = var.cluster_name
}

6.2 Deploying Application with Terraform

Namespace:

resource "kubernetes_namespace" "app" {
  metadata {
    name = var.namespace
  }
}

ConfigMap:

resource "kubernetes_config_map" "app" {
  metadata {
    name      = "app-config"
    namespace = kubernetes_namespace.app.metadata[0].name
  }

  data = {
    environment = var.environment
    log_level   = "info"
  }
}

Deployment:

resource "kubernetes_deployment" "app" {
  metadata {
    name      = var.app_name
    namespace = kubernetes_namespace.app.metadata[0].name
    labels = {
      app = var.app_name
    }
  }

  spec {
    replicas = var.replicas

    selector {
      match_labels = {
        app = var.app_name
      }
    }

    template {
      metadata {
        labels = {
          app = var.app_name
        }
      }

      spec {
        container {
          name  = "api"
          image = "${var.ecr_repository_url}:${var.image_tag}"

          port {
            container_port = 8080
            name           = "http"
          }

          env_from {
            config_map_ref {
              name = kubernetes_config_map.app.metadata[0].name
            }
          }

          resources {
            requests = {
              cpu    = "250m"
              memory = "256Mi"
            }
            limits = {
              cpu    = "500m"
              memory = "512Mi"
            }
          }

          liveness_probe {
            http_get {
              path = "/healthz"
              port = 8080
            }
            initial_delay_seconds = 30
            period_seconds        = 10
          }

          readiness_probe {
            http_get {
              path = "/healthz"
              port = 8080
            }
            initial_delay_seconds = 5
            period_seconds        = 5
          }
        }
      }
    }
  }
}

Service:

resource "kubernetes_service" "app" {
  metadata {
    name      = var.app_name
    namespace = kubernetes_namespace.app.metadata[0].name
  }

  spec {
    type = "LoadBalancer"

    selector = {
      app = var.app_name
    }

    port {
      port        = 80
      target_port = 8080
      protocol    = "TCP"
      name        = "http"
    }
  }
}

Variables for Kubernetes Resources:

# k8s/variables.tf
variable "cluster_name" {
  description = "EKS cluster name"
  type        = string
}

variable "namespace" {
  description = "Kubernetes namespace"
  type        = string
  default     = "default"
}

variable "app_name" {
  description = "Application name"
  type        = string
  default     = "sample-python-app"
}

variable "ecr_repository_url" {
  description = "ECR repository URL"
  type        = string
}

variable "image_tag" {
  description = "Docker image tag"
  type        = string
}

variable "replicas" {
  description = "Number of replicas"
  type        = number
  default     = 2
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "production"
}

7. Terraform State Management

7.1 Understanding State

What is State? - File (terraform.tfstate) that tracks created resources - Maps Terraform configuration to real infrastructure - Used to plan updates and destroy resources

Why State Matters: - Without state, Terraform doesn't know what exists - State enables Terraform to update existing resources - Critical for team collaboration

7.2 Remote State (Best Practice)

Store state in S3 (or similar) for team access:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "eks/cluster/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"  # For state locking
  }
}

Benefits: - Shared state across team - State locking (prevent conflicts) - Versioning (S3 versioning) - Backup and recovery

7.3 State Commands

# Show current state
terraform show

# List resources in state
terraform state list

# Remove resource from state (doesn't delete it)
terraform state rm aws_instance.web

# Import existing resource into state
terraform import aws_instance.web i-1234567890abcdef0

8. Terraform Modules

8.1 What are Modules?

Modules = Reusable Terraform configurations - Package related resources together - Make configurations reusable - Share with others

8.2 Using Modules

Example: EKS Module

# Use a module
module "eks" {
  source = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"

  cluster_name    = "my-cluster"
  cluster_version = "1.28"

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  node_groups = {
    main = {
      desired_size = 2
      max_size     = 4
      min_size     = 1
      instance_types = ["t3.medium"]
    }
  }
}

Benefits: - Pre-built, tested configurations - Faster development - Best practices included - Community maintained

9. Integrating Terraform into CD Pipelines

9.1 CD Pipeline with Terraform

# .github/workflows/cd.yml
name: Deploy with Terraform

on:
  push:
    branches: [main]

jobs:
  deploy-infrastructure:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0

      - name: Terraform Init (EKS)
        run: |
          cd infra/eks
          terraform init

      - name: Terraform Plan (EKS)
        run: |
          cd infra/eks
          terraform plan -out=tfplan

      - name: Terraform Apply (EKS)
        run: |
          cd infra/eks
          terraform apply -auto-approve tfplan

      - name: Terraform Init (K8s)
        run: |
          cd infra/k8s
          terraform init

      - name: Terraform Apply (K8s)
        run: |
          cd infra/k8s
          terraform apply -auto-approve \
            -var="cluster_name=my-eks-cluster" \
            -var="image_tag=${{ github.sha }}" \
            -var="ecr_repository_url=${{ secrets.ECR_REGISTRY }}/sample-python-app"

9.2 Best Practices for CD

  1. Separate Infrastructure and Application - Infrastructure (EKS) changes less frequently - Application (K8s resources) changes with each deployment

  2. Use Terraform Workspaces - Separate state for different environments - terraform workspace new staging

  3. Plan Before Apply - Review changes before applying - Use terraform plan in CI

  4. State Locking - Prevent concurrent modifications - Use DynamoDB for S3 backend

  5. Variable Validation - Validate inputs - Fail fast on invalid values

10. Troubleshooting

Issue 1: Provider Not Found

# Solution: Run terraform init
terraform init

Issue 2: State Locked

# Solution: Check for lock file or force unlock (careful!)
terraform force-unlock <lock-id>

Issue 3: Resource Already Exists

# Solution: Import existing resource
terraform import aws_eks_cluster.main my-cluster-name

Issue 4: Authentication Errors

# Solution: Verify AWS credentials
aws sts get-caller-identity

11. Key Takeaways

  • ✅ Infrastructure as Code enables reproducible, versioned infrastructure
  • ✅ Terraform is declarative and multi-cloud
  • ✅ EKS cluster requires VPC, subnets, IAM roles, security groups
  • ✅ Terraform can manage both infrastructure and Kubernetes resources
  • ✅ State management is critical for team collaboration
  • ✅ Modules enable code reuse and best practices
  • ✅ Integrate Terraform into CD pipelines for automation

12. Next Steps

Now that you understand Terraform, you're ready for: - Module: Complete CD Pipeline - Integrate everything together - Lab: Practical Exercise - Hands-on deployment

13. Instructor Notes

  • Start with simple resources before EKS
  • Emphasize the declarative nature (describe what you want)
  • Show terraform plan output to explain what will happen
  • Demonstrate state importance with examples
  • Use visual diagrams for EKS architecture
  • Highlight cost implications (NAT gateways, EKS control plane)
  • Show real-time updates during terraform apply