This repository provisions a production-ready AWS infrastructure using Terraform, organized into layered modules and automated through a Jenkins CI/CD pipeline.
- Architecture Overview
- Project Structure
- Layer 0 — Bootstrap
- Layer 1 — Network
- Layer 2 — EKS
- Remote State & Locking
- Jenkins Pipeline
- Jenkins Setup Requirements
- Execution Flow
- Destroy Order
AWS Account (us-west-2)
┌─────────────────────────────────────────────────────────┐
│ VPC: 10.0.0.0/16 │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Public Subnet AZ-a │ │ Public Subnet AZ-b │ │
│ │ 10.0.0.0/24 │ │ 10.0.1.0/24 │ │
│ │ [ALB] [NAT GW] │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ | │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Private Subnet AZ-a│ │ Private Subnet AZ-b│ │
│ │ 10.0.10.0/24 │ │ 10.0.11.0/24 │ │
│ │ [EKS Nodes] │ │ [EKS Nodes] │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
S3 Bucket → itkannadigaru-infra-statefile-backup
DynamoDB Table → itkannadigaru-terraform-locks
EKS Cluster → itkannadigaru
InfraStructure/
├── 0-bootstrap/ # Remote backend (S3 + DynamoDB)
│ ├── main.tf
│ ├── providers.tf
│ └── outputs.tf
│
├── 1-network/ # VPC, Subnets, NAT, IGW
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── backend.tf
│
├── 2-eks/ # EKS Cluster + Node Group
│ ├── main.tf
│ ├── data.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ └── backend.tf
│
├── Jenkinsfile # CI/CD pipeline
└── README.md
Layers are numbered intentionally. They must run in order because each layer depends on the previous one.
Creates the remote backend infrastructure that all other layers use to store their Terraform state files. This layer runs once and its own state is stored locally.
| Resource | Name | Purpose |
|---|---|---|
aws_s3_bucket |
itkannadigaru-infra-statefile-backup |
Stores all .tfstate files remotely |
aws_s3_bucket_versioning |
same bucket | Keeps history of state — allows rollback |
aws_s3_bucket_server_side_encryption_configuration |
same bucket | Encrypts state files at rest (AES256) |
aws_s3_bucket_public_access_block |
same bucket | Blocks all public access to state files |
aws_dynamodb_table |
itkannadigaru-terraform-locks |
Prevents concurrent terraform applies |
Layers 1 and 2 both declare an S3 backend. The bucket and DynamoDB table must already exist before terraform init can run on those layers. Bootstrap creates them.
| Output | Value |
|---|---|
tf_state_bucket |
Name of the S3 bucket |
tf_lock_table |
Name of the DynamoDB table |
Creates the VPC and all networking components that the EKS cluster will live inside.
| Resource | Purpose |
|---|---|
aws_vpc |
Isolated network — CIDR 10.0.0.0/16 |
aws_internet_gateway |
Allows public subnets to reach the internet |
aws_subnet (public x2) |
For ALB and NAT Gateway |
aws_subnet (private x2) |
For EKS worker nodes |
aws_eip |
Static IP assigned to the NAT Gateway |
aws_nat_gateway |
Allows private subnets to reach internet (outbound only) |
aws_route_table (public) |
Routes 0.0.0.0/0 → Internet Gateway |
aws_route_table (private) |
Routes 0.0.0.0/0 → NAT Gateway |
VPC: 10.0.0.0/16
├── Public Subnet AZ-a 10.0.0.0/24 ← ALB, NAT Gateway
├── Public Subnet AZ-b 10.0.1.0/24
├── Private Subnet AZ-a 10.0.10.0/24 ← EKS worker nodes
└── Private Subnet AZ-b 10.0.11.0/24 ← EKS worker nodes
EKS nodes are placed in private subnets — they are not directly reachable from the internet. The NAT Gateway allows outbound traffic (pulling images, updates) from private subnets.
Subnets are tagged so the AWS Load Balancer Controller can auto-discover them:
# Public subnets
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/itkannadigaru" = "shared"
# Private subnets
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/itkannadigaru" = "shared"| Variable | Default | Description |
|---|---|---|
project |
itkannadigaru |
Used in all resource names and tags |
vpc_cidr |
10.0.0.0/16 |
CIDR block for the VPC |
azs |
["us-west-2a", "us-west-2b"] |
Availability zones to deploy into |
| Output | Description |
|---|---|
vpc_id |
VPC ID consumed by Layer 2 |
public_subnet_ids |
List of public subnet IDs |
private_subnet_ids |
List of private subnet IDs consumed by Layer 2 |
Creates the Kubernetes cluster and managed worker node group inside the network from Layer 1.
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "itkannadigaru-infra-statefile-backup"
key = "itkannadigaru/1-network/terraform.tfstate"
region = "us-west-2"
}
}This pulls vpc_id, private_subnet_ids, and public_subnet_ids directly from Layer 1's saved state — no hardcoded values needed.
| Setting | Value | Notes |
|---|---|---|
cluster_name |
itkannadigaru |
|
cluster_version |
1.30 |
Kubernetes version |
enable_irsa |
true |
IAM Roles for Service Accounts |
cluster_endpoint_public_access |
true |
kubectl works from your laptop |
cluster_endpoint_private_access |
true |
Nodes communicate internally |
| Setting | Value |
|---|---|
| Instance type | c7i-flex.large |
| Min size | 1 |
| Max size | 3 |
| Desired size | 2 |
| Subnet | Private subnets |
enable_irsa = true enables IAM Roles for Service Accounts. This allows Kubernetes pods to assume AWS IAM roles directly without hardcoding credentials. Required for:
- AWS Load Balancer Controller
- Cluster Autoscaler
- External DNS
- Any pod that needs AWS API access
| Output | Description |
|---|---|
cluster_name |
EKS cluster name |
cluster_endpoint |
API server endpoint for kubectl |
cluster_certificate_authority_data |
Certificate data for authentication |
region |
AWS region (us-west-2) |
By default, Terraform stores state locally as terraform.tfstate. In a team or CI/CD environment this causes problems:
- Local files are not shared between team members or pipeline runs
- No history or versioning
- No protection against corruption
Storing state in S3 solves all of this.
| Layer | S3 Key |
|---|---|
| 1-network | itkannadigaru/1-network/terraform.tfstate |
| 2-eks | itkannadigaru/2-eks/terraform.tfstate |
When two terraform apply runs happen at the same time (two developers, two pipeline triggers), they both read the same state file and start writing changes. This causes state corruption.
DynamoDB acts as a distributed lock:
Pipeline Run #1 starts apply
→ Acquires lock in DynamoDB (LockID written)
→ Makes infrastructure changes
→ Releases lock when done
Pipeline Run #2 starts at the same time
→ Tries to acquire lock
→ Lock already held by Run #1
→ Errors: "state is locked" — safely blocked
Run #1 finishes → lock released → Run #2 can proceed
DynamoDB table used across all layers: itkannadigaru-terraform-locks
The pipeline has a single approval gate — all three layers are planned first, you review everything at once, then execution proceeds layer by layer.
Checkout
↓
Plan: All Layers (0-bootstrap + 1-network + 2-eks)
↓
★ APPROVAL — Human reviews combined plan output ★
↓
Execute: 0-bootstrap
↓
Execute: 1-network
↓
Execute: 2-eks
| Parameter | Options | Description |
|---|---|---|
terraformAction |
apply / destroy |
Action to perform on all layers |
Clones the repository from the terraform branch into a terraform/ subdirectory on the Jenkins agent.
Runs the following on each layer sequentially — no infrastructure is changed:
terraform init → downloads providers, configures S3 backend
terraform plan → calculates changes, saves binary plan to tfplan
terraform show → converts tfplan to human-readable tfplan.txt
Pipeline pauses and waits for a human to review. The combined plan output of all 3 layers is shown together.
- Click Proceed → execution begins
- Click Abort → pipeline stops, nothing is applied
Each layer runs one at a time and waits for the previous to fully complete:
apply→ uses the savedtfplanso exactly what was reviewed gets applieddestroy→ runsterraform destroy -auto-approve
Go to Manage Jenkins → Credentials and add:
| ID | Type | Value |
|---|---|---|
AWS_ACCESS_KEY_ID |
Secret Text | Your AWS Access Key ID |
AWS_SECRET_ACCESS_KEY |
Secret Text | Your AWS Secret Access Key |
| Plugin | Purpose |
|---|---|
| Pipeline | Run declarative pipelines |
| Git | Checkout from GitHub |
| Credentials Binding | Inject AWS credentials securely |
Terraform must be installed on the Jenkins agent and available in PATH:
# Verify
terraform -versionThe IAM user or role used by Jenkins needs permissions for:
| Service | Permissions Needed |
|---|---|
| S3 | CreateBucket, PutObject, GetObject, ListBucket |
| DynamoDB | CreateTable, PutItem, GetItem, DeleteItem |
| VPC | Full VPC permissions |
| EKS | Full EKS permissions |
| EC2 | Full EC2 permissions (for node groups) |
| IAM | CreateRole, AttachRolePolicy (for IRSA) |
# Step 1 — Run bootstrap manually once to create S3 + DynamoDB
cd 0-bootstrap
terraform init
terraform apply
# Step 2 — Trigger the Jenkins pipeline with action: apply
# Pipeline handles 1-network and 2-eks automatically- Go to Jenkins → trigger Build with Parameters
- Select
applyordestroy - Wait for the Plan stage to complete
- Review the combined plan in the Approval stage
- Click Proceed to apply
Important: Always destroy in reverse order — EKS first, then Network, then Bootstrap.
Destroying in forward order will fail because resources are still in use (e.g., you cannot delete a VPC while EKS nodes are still running inside it).
Destroy: 2-eks → removes EKS cluster and nodes first
↓
Destroy: 1-network → removes VPC, subnets, NAT after nodes are gone
↓
Destroy: 0-bootstrap → removes S3 bucket and DynamoDB last
| Item | Value |
|---|---|
| AWS Region | us-west-2 |
| Project Name | itkannadigaru |
| EKS Cluster | itkannadigaru |
| Kubernetes Version | 1.30 |
| VPC CIDR | 10.0.0.0/16 |
| Availability Zones | us-west-2a, us-west-2b |
| S3 State Bucket | itkannadigaru-infra-statefile-backup |
| DynamoDB Lock Table | itkannadigaru-terraform-locks |
| Node Instance Type | c7i-flex.large |
| Node Count | min: 1, desired: 2, max: 3 |