Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ node_modules
# CDK asset staging directory
.cdk.staging
cdk.out

# Terraform files
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl
111 changes: 111 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Architecture overview

This Terraform project provisions a AWS VPC in two Availability Zones (AZs) for fault tolerance and high availability.
It creates a **Multi-AZ VPC** with **public/private subnets**, **per-AZ NAT Gateways**, **VPC Flow Logs**, and deploys an **ECS Fargate service** connected to a **private ECR repository** behind an **Application Load Balancer (ALB)**.

---

## Core Components

### 1. **VPC**
- CIDR: e.g., `10.0.0.0/16`
- DNS hostnames and DNS support enabled
- Isolated, dedicated network for workloads

---

### 2. **Subnets (Multi-AZ)**
- **Public Subnets (x2):**
- One per AZ (e.g., `10.0.0.0/20`, `10.0.16.0/20`)
- Host ALB, NAT Gateways
- Auto-assign public IPs

- **Private Subnets (x2):**
- One per AZ (e.g., `10.0.32.0/20`, `10.0.48.0/20`)
- Host ECS tasks, EKS nodes, or databases
- No direct internet access

---

### 3. **Internet Gateway (IGW)**
- Attached to the VPC
- Enables outbound access for public subnets
- Used for inbound ALB or bastion connectivity

---

### 4. **NAT Gateways (per AZ)**
- One NAT Gateway per AZ for fault tolerance
- Private subnets route outbound traffic to their local NAT
- Ensures resiliency during single-AZ failure

---

### 5. **Route Tables**
- **Public Route Table:** default route → Internet Gateway
- **Private Route Tables (per AZ):** default route → NAT Gateway

---

### 6. **VPC Endpoints (optional)**
- **Gateway Endpoints:** for S3 — keep traffic inside AWS backbone
- **Interface Endpoints:** for SSM, EC2, CloudWatch, ECR — secure private API access

---

### 7. **VPC Flow Logs → CloudWatch**
- Captures ACCEPT / REJECT / ALL traffic metadata
- Sent to CloudWatch Log Group: `/vpc/<project>/flow-logs`
- IAM Role with least privilege for logging
- Enables audit, security, and performance analysis

---

### 8. **ECS Fargate Cluster & Tasks**
- Cluster with container insights enabled
- Task definitions define containers, CPU/memory, and environment variables
- Pulls Docker image from private ECR
- Runs in private subnets (no public IP)
- Logs sent to CloudWatch Logs

---

### 9. **Application Load Balancer (ALB)**
- Deployed in public subnets
- Routes inbound traffic to ECS tasks in private subnets
- Supports HTTP and optional HTTPS via ACM certificate
- Health checks and circuit breakers for resilience

---

# Security Considerations

- **Network Isolation** Private workloads only reachable via ALB. No public IPs on ECS tasks
- **Per-AZ NAT Gateways** AZ-specific egress preventing cross-AZ dependency
- **Security Groups** ALB SG ingress from trusted CIDRs only. Tasks SG only allows ALB ingress
- **IAM Roles** Separate task & execution roles. Principle of least privilege enforced |
- **Logging & Audit** VPC Flow Logs and CloudWatch |
- **ECR Hygiene** Private repo
- **Observability** CloudWatch metrics & logs. Supports alerts and dashboards

---

# Deployment Steps

1. **Container Image**

- aws ecr get-login-password --region <region> \
| docker login --username AWS --password-stdin <account>.dkr.ecr.<region>.amazonaws.com
- docker build -t <app> .
- docker tag <app>:latest <account>.dkr.ecr.<region>.amazonaws.com/<app>:v1
- docker push <account>.dkr.ecr.<region>.amazonaws.com/<app>:v1

- Update container image name in terraform.tfvars file wiht the newly built container and save file.

2. **Initialize Terraform**
Run the following commands to initialize and deploy VPC and ECS services:

- terraform init
- terraform plan -out tf.plan
- terraform apply tf.plan
- terraform output alb_dns_name for application dns name
80 changes: 80 additions & 0 deletions terraform/alb.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
resource "aws_security_group" "iac_exercise_alb_sg" {
name = "${var.project}-alb-sg"
description = "ALB ingress"
vpc_id = aws_vpc.iac_exercise_vpc.id
tags = var.tags

ingress {
description = "HTTP"
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 = [aws_vpc.iac_exercise_vpc.cidr_block]
}
}

resource "aws_lb" "iac_exercise_app_alb" {
name = "${var.project}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.iac_exercise_alb_sg.id]
subnets = [for k, s in aws_subnet.iac_exercise_public : s.id]
idle_timeout = var.alb_idle_timeout
enable_deletion_protection = false
tags = var.tags
}

resource "aws_lb_target_group" "iac_exercise_app_tg" {
name = "${var.project}-tg"
port = var.container_port
protocol = "HTTP"
target_type = "ip"
vpc_id = aws_vpc.iac_exercise_vpc.id

health_check {
path = var.health_check_path
healthy_threshold = 2
unhealthy_threshold = 5
timeout = 5
interval = 30
matcher = "200-399"
}

tags = var.tags
}

resource "aws_lb_listener" "iac_exercise_http" {
load_balancer_arn = aws_lb.iac_exercise_app_alb.arn
port = 80
protocol = "HTTP"

default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}

# HTTPS listener using the imported self-signed cert
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.iac_exercise_app_alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.self_signed.arn

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.iac_exercise_app_tg.arn
}
}
44 changes: 44 additions & 0 deletions terraform/autoscale.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Target the ECS service desired count
resource "aws_appautoscaling_target" "iac_exercise_svc" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.iac_exercise_cluster.name}/${aws_ecs_service.iac_exercise_app_service.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}

# Scale out on average CPU > 60%
resource "aws_appautoscaling_policy" "iac_exercise_cpu_scale_out" {
name = "${var.project}-cpu-scale-out"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.iac_exercise_svc.resource_id
scalable_dimension = aws_appautoscaling_target.iac_exercise_svc.scalable_dimension
service_namespace = aws_appautoscaling_target.iac_exercise_svc.service_namespace

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 60
scale_in_cooldown = 60
scale_out_cooldown = 60
}
}

# Scale out on average Memory > 70%
resource "aws_appautoscaling_policy" "iac_exercise_mem_scale_out" {
name = "${var.project}-mem-scale-out"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.iac_exercise_svc.resource_id
scalable_dimension = aws_appautoscaling_target.iac_exercise_svc.scalable_dimension
service_namespace = aws_appautoscaling_target.iac_exercise_svc.service_namespace

target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = 70
scale_in_cooldown = 60
scale_out_cooldown = 60
}
}
8 changes: 8 additions & 0 deletions terraform/backend.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# terraform {
# backend "s3" {
# bucket = "iac-exercise-terraform-state-bucket"
# key = "iac-exercise/terraform.tfstate"
# region = "us-west-2"
# encrypt = true
# }
# }
61 changes: 61 additions & 0 deletions terraform/ecs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
resource "aws_ecs_cluster" "iac_exercise_cluster" {
name = "${var.project}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = var.tags
}

# Capacity providers so we can mix On-Demand and Spot
resource "aws_ecs_cluster_capacity_providers" "iac_exercise_cluster_capacity_provider" {
cluster_name = aws_ecs_cluster.iac_exercise_cluster.name
capacity_providers = var.enable_fargate_spot ? ["FARGATE", "FARGATE_SPOT"] : ["FARGATE"]
default_capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = 1
}
}

locals {
container_def = {
name = var.project
image = var.container_image
essential = true
portMappings = [{
containerPort = var.container_port
hostPort = var.container_port
protocol = "tcp"
appProtocol = "http"
}]
environment = [
for k, v in var.env_vars : { name = k, value = v }
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.iac_exercise_app_log.name
awslogs-region = var.region
awslogs-stream-prefix = var.project
}
}
}
}

resource "aws_ecs_task_definition" "iac_exercise_app" {
family = "${var.project}-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = tostring(var.cpu)
memory = tostring(var.memory)
execution_role_arn = aws_iam_role.iac_exercise_task_execution.arn
task_role_arn = aws_iam_role.iac_exercise_task_role.arn
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}

container_definitions = jsonencode([local.container_def])
tags = var.tags
}

Loading