Cloud infrastructure • AWS • Terraform • Ansible • Kubernetes

AWS Infrastructure Automation & Kubernetes

I built a three-tier architecture—separate web, application, and database layers—with two application deployment paths: Gunicorn/systemd on private EC2, or single-node k3s running two non-root Flask replicas. Terraform provisions the network; Ansible configures the Linux hosts; the runner tests the API, database and denied network paths before destroying the infrastructure.

Systemd mode was live-verified September 22, 2026. Kubernetes mode was separately live-verified September 24: two available Flask Pods, database-backed health and network checks, test-only PVC persistence across evidence Pod replacement, and all 28 Terraform-managed AWS resources destroyed.

WorkstationOperator IPv4 /32: HTTP and SSH to public web tier
↓
Public web tierEC2 · Nginx reverse proxy · SSH bastion
↓ HTTP :8000 (systemd) or :30080 (k3s)
Private application tierEC2 · Flask API · Gunicorn/systemd OR k3s with two non-root Pods
↓ PostgreSQL :5432
Private database tierEC2 · PostgreSQL 16
Security groups allow intended tier-to-tier traffic; direct web-to-database access is denied.

What I built

Terraform defines the VPC and EC2 tiers. Ansible configures Nginx and dedicated PostgreSQL, plus Gunicorn/systemd or private k3s on the app EC2. Both paths use the same disposable AWS topology and lifecycle runner.

Provisioning

Terraform

VPC, three subnets, route tables, internet gateway, NAT gateway, security groups, EC2 instances, SSH key pair, and encrypted EBS volumes.

Configuration

Ansible

Installs PostgreSQL and Nginx, configures the selected backend port, and installs either Gunicorn/systemd or private k3s on application EC2.

Containers

Private Kubernetes

Imports a non-root Flask image into k3s, deploys two Pods with health probes, NodePort, runtime DB Secret and a test-only PVC.

Testing

Network boundaries

Confirms app-to-database access, web-to-app access, denial of direct web-to-database access, and a database-backed external HTTP health response.

One architecture, two application runtimes

The two modes were tested separately. Nginx and PostgreSQL stay on their dedicated EC2 instances; only the private application runtime changes.

AreaSeptember 22 · systemdSeptember 24 · k3s
Application hostPrivate EC2, Gunicorn/systemdPrivate EC2, single-node k3s and containerd
Flask deliveryTCP 8000Two non-root Pods; Service/NodePort 30080
DatabaseDedicated PostgreSQL 16 EC2Same dedicated PostgreSQL 16 EC2
EntryNginx/SSH bastion; operator /32Same bastion; NodePort from web security group only
VerificationSQL, health, network restrictions, idempotencySame integration tests plus replicas, probes and PVC test
Cleanup28 resources destroyed28 resources destroyed

Build, test, destroy

The project deliberately exercises operational behavior rather than stopping after a successful terraform apply.

01
Guard against accidental deployment.
Explicit --run, root-account refusal, operator IP restriction, and isolated run directory.
02
Provision AWS resources.
Terraform creates the three-tier VPC and EC2 environment.
03
Configure Linux services.
Ansible configures Nginx, PostgreSQL and the selected application runtime.
04
Run configuration twice.
Systemd mode checks all three hosts; k3s mode verifies changed=0 on web and DB.
05
Validate the traffic path.
Tests allowed and denied traffic, Nginx and PostgreSQL SELECT 1; k3s also verifies replicas and Pod-level test PVC persistence.
06
Destroy and verify cleanup.
Terraform deletes 28 resources; separate AWS CLI queries confirm no remaining tagged EC2 instances, NAT gateway, Elastic IP, or VPC.

Test runs incur AWS charges. The runner attempts teardown after test failures and includes an interruption recovery script.

Live-verified Kubernetes deployment — September 24, 2026

Kubernetes ran on private application EC2 using k3s in a live us-east-1 deployment.

Live checkObserved result
Terraform apply28 resources created across three EC2 tiers
Private k3s nodeReady; Ubuntu 24.04.5 LTS, k3s v1.36.4+k3s1
Flask Deployment2/2 non-root replicas available
ServiceInternal Kubernetes Service; NodePort 80:30080/TCP
Application health/healthz, /readyz and /health passed
DatabaseReal SELECT 1 on dedicated PostgreSQL EC2
Network boundariesWeb → app and app → DB allowed; web → PostgreSQL blocked
Test-only storage1 GiB local-path PVC marker survived evidence Pod deletion/recreation
Ansible repeat passchanged=0 on web and DB; systemd app play skipped
External requestWorkstation → Nginx → k3s → PostgreSQL returned SELECT 1
Terraform destroy28 resources destroyed, including NAT gateway and VPC

The following images are served from the public infrastructure repository; select an image to inspect it at full size. Its verification case study contains all seven screenshots.

Terraform apply: 28 resources added
Terraform created three AWS tiers. View full resolution ↗
Ready k3s node, two API Pods, Bound PVC and passing checks
Ready private cluster, two Flask replicas and database/persistence tests. View full resolution ↗
Ansible network security and database-backed health tests passed
Allowed connectivity and denied web-to-database traffic verified. View full resolution ↗
Terraform destroy: 28 resources removed
All 28 Terraform-managed lab resources destroyed. View full resolution ↗

Both API Pods share one EC2 node. The local-path PVC tested persistence for a separate evidence Pod, not PostgreSQL storage or off-instance backup.

Original systemd deployment — September 22, 2026

Results from the completed us-east-1 deployment.

CheckObserved result
Terraform provisioning28 resources created
Ansible first configurationThree hosts; zero failures
Ansible second configurationZero changes on all three hosts
Application → PostgreSQLPassed
Web → applicationPassed
Direct web → PostgreSQLBlocked as intended
External Nginx → application → databasestatus=ok; db_result=1
Terraform teardown28 resources destroyed
Independent AWS cleanup checksNo remaining lab instances, NAT gateways, Elastic IPs, or VPCs
GitHub ActionsTerraform and Ansible static validation passed

Original systemd evidence screenshots

These screenshots document the September 22 Gunicorn/systemd run. The September 24 k3s run is shown separately above.

Three AWS EC2 instances named web, app, and db running with passing status checks
All three EC2 instances running and passing status checks. Open full-size image.
Successful GitHub Actions Terraform and Ansible validation
GitHub Actions static validation passed.
Ansible configuration, zero-change idempotency run, network tests, external health check, and Terraform lifecycle results
Preserved Ansible recaps, connectivity checks, database health response, and lifecycle summary. Open full-size image to read the output.

What this demonstrates

Cloud architecture

Separate public and private tiers, route tables, NAT, SSH bastion, and security-group-controlled traffic.

Automation and repeatability

Terraform provisioning, Ansible Linux configuration, two runtime choices and scoped idempotency checks.

Kubernetes workload operations

Private k3s, two non-root Flask replicas, NodePort, database-backed probes and a test-only PVC.

Operational discipline

Positive and negative smoke tests, retained evidence, explicit destruction, and independent cleanup verification.

Continuous integration

GitHub Actions checks Terraform formatting and validation, Ansible playbook syntax, and shell scripts without running billable cloud infrastructure.

Back to top