Skip to content

Repository files navigation

Terraform - AWS Web Application Infrastructure

Provisions a production-grade AWS environment for a containerised Spring Boot API. One apply = one cluster. Run twice with different state keys for dev and prd.


Architecture

                          Internet
                             |
                      [Internet Gateway]
                        /           \
              [Public Subnet AZ-a]  [Public Subnet AZ-b]
              [NAT GW]              [NAT GW - prd only]
              [Nginx Ingress LB] <-- routes inbound HTTPS
                        \           /
              [Private Subnet AZ-a] [Private Subnet AZ-b]
              [EKS Nodes]           [EKS Nodes]
              [RDS Primary]         [RDS Standby]
Layer Service Notes
Compute EKS 1.35 managed nodes AL2023, t3.medium, 2-10 nodes, Cluster Autoscaler
Database RDS PostgreSQL 17 db.t4g.micro, Multi-AZ, gp3
Storage S3 Versioned, private, IRSA-controlled
Network VPC 10.0.0.0/16 2 public + 2 private subnets, 2 AZ, per-AZ NAT in prd
IAM IRSA (OIDC) App pods and Cluster Autoscaler use role-based access

Files

File What it does
versions.tf Terraform version constraint, S3 backend, provider pins and provider blocks
variables.tf All inputs with types, defaults, and descriptions
locals.tf AZ list, shared resource tags, OIDC issuer host
data.tf Reads available AZs and current AWS account ID from AWS
network.tf VPC, subnets, IGW, NAT GW (per-AZ in prd), route tables, RDS security group
compute.tf EKS cluster, IAM roles, node group, EKS addons, OIDC provider, IRSA roles
database.tf RDS instance, subnet group, write-only password
storage.tf S3 bucket (versioned, private)
addons.tf Helm releases: nginx ingress, cert-manager, ArgoCD, Cluster Autoscaler; ClusterIssuers
outputs.tf kubectl config command, ArgoCD admin password, DB endpoint, S3 bucket, app IRSA role ARN
terraform.tfvars.example Copy to terraform.tfvars and fill in before running

Prerequisites

  • Terraform >= 1.11
  • AWS CLI configured - aws sts get-caller-identity should succeed
  • AWS identity with permissions: eks:*, ec2:*, rds:*, s3:*, iam:*, logs:*, elasticloadbalancing:*
  • aws CLI available in PATH - used by the Helm provider for EKS auth

Bootstrap the state bucket (once, before first init)

Terraform's S3 backend requires the bucket to exist before terraform init can run. Create it manually once and reuse across all environments:

aws s3api create-bucket \
  --bucket inpost-terraform-state \
  --region eu-central-1 \
  --create-bucket-configuration LocationConstraint=eu-central-1

aws s3api put-bucket-versioning \
  --bucket inpost-terraform-state \
  --versioning-configuration Status=Enabled

The bucket name is already set in backends/dev.hcl and backends/prd.hcl.


Deploy

1. Configure variables

cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars: set cluster_name to match ArgoCD (e.g. dev-global-cluster-0)

2. Init with a per-environment state key

Each environment has its own backend config file under backends/. Pass it at init time:

# dev
terraform init -backend-config=backends/dev.hcl

# prd (switching in the same directory - -reconfigure replaces the active backend)
terraform init -reconfigure -backend-config=backends/prd.hcl

Both environments share one S3 bucket but isolate state in separate keys. Using the same key would cause the second apply to overwrite the first environment's state.

3. Plan and apply

terraform plan
terraform apply

Post-deploy wiring

terraform apply provisions EKS, RDS, S3, nginx, cert-manager, ArgoCD, Cluster Autoscaler, and Let's Encrypt ClusterIssuers. AppProject and ApplicationSet come from the app repo.

Configure kubectl after apply:

terraform output -raw kubeconfig_command | bash

Access ArgoCD

# Get the initial admin password
terraform output -raw argocd_admin_password | bash

# Find the ArgoCD server URL
kubectl get svc argocd-server -n argocd

Bootstrap ArgoCD config from the application repo (AppProject, ApplicationSet):

kubectl apply -f argocd/argocd-project.yaml
kubectl apply -f argocd/applicationset.yaml

ClusterIssuers (letsencrypt-staging and letsencrypt-prod) are created by terraform apply via kubernetes_manifest in addons.tf - no manual step needed. On a brand-new cluster the cert-manager CRD schema is unavailable at plan time; run terraform apply a second time if you see a CRD-not-found error during plan.

Watch application syncs:

argocd login <argocd-server-ip>
argocd app list

Enable S3 access for application pods (IRSA)

Annotate the app's ServiceAccount with the role ARN from Terraform output:

terraform output -raw app_irsa_role_arn
apiVersion: v1
kind: ServiceAccount
metadata:
  name: webapp       # must match var.project
  namespace: webapp  # must match var.project
  annotations:
    eks.amazonaws.com/role-arn: <app_irsa_role_arn>

Destroying the environment

RDS has deletion_protection = true. Disable it before terraform destroy:

aws rds modify-db-instance \
  --db-instance-identifier webapp-<env> \
  --no-deletion-protection \
  --apply-immediately
# Wait ~1 minute for the change to apply, then:
terraform destroy

Design decisions

Write-only DB password - password_wo (Terraform >= 1.11, AWS provider >= 5.80) applies the password to RDS without writing it to .tfstate. The password is unrecoverable from state after apply; retrieve it from the random_password resource output before destroying if needed.

RDS Multi-AZ - db.t4g.micro with multi_az = true roughly doubles the DB cost (~$30 to ~$60/month) for ~60s automatic failover. Graviton over Intel saves ~20% of that.

Per-AZ NAT GW in prd, single in dev - network.tf uses count = var.environment == "prd" ? length(local.azs) : 1. Single GW in dev costs $32/month; prd gets one per AZ ($64/month) so an AZ failure doesn't cut egress for nodes in the other AZ.

Managed nodes over Fargate - vpc-cni, kube-proxy, and nginx ingress run as DaemonSets, which Fargate doesn't support.

Helm add-ons in Terraform - keeps infra in a single apply. ClusterIssuers go in via kubernetes_manifest so no post-apply kubectl is needed. ArgoCD config (AppProject, ApplicationSet) stays in the app repo. Before terraform destroy, delete ArgoCD Applications to avoid finalizer deadlocks.

IRSA over node instance profiles - two roles: app-s3 scoped to the app's S3 bucket, cluster-autoscaler scoped to autoscaling actions. Bound by OIDC sub claim to specific service accounts so neither role can be assumed by arbitrary pods.

No DynamoDB state locking - safe for a single operator; add dynamodb_table to the backend for team use.

About

AWS EKS IaC with terraform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages