Terraform
VPC, three subnets, route tables, internet gateway, NAT gateway, security groups, EC2 instances, SSH key pair, and encrypted EBS volumes.
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.
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.
VPC, three subnets, route tables, internet gateway, NAT gateway, security groups, EC2 instances, SSH key pair, and encrypted EBS volumes.
Installs PostgreSQL and Nginx, configures the selected backend port, and installs either Gunicorn/systemd or private k3s on application EC2.
Imports a non-root Flask image into k3s, deploys two Pods with health probes, NodePort, runtime DB Secret and a test-only PVC.
Confirms app-to-database access, web-to-app access, denial of direct web-to-database access, and a database-backed external HTTP health response.
The two modes were tested separately. Nginx and PostgreSQL stay on their dedicated EC2 instances; only the private application runtime changes.
| Area | September 22 · systemd | September 24 · k3s |
|---|---|---|
| Application host | Private EC2, Gunicorn/systemd | Private EC2, single-node k3s and containerd |
| Flask delivery | TCP 8000 | Two non-root Pods; Service/NodePort 30080 |
| Database | Dedicated PostgreSQL 16 EC2 | Same dedicated PostgreSQL 16 EC2 |
| Entry | Nginx/SSH bastion; operator /32 | Same bastion; NodePort from web security group only |
| Verification | SQL, health, network restrictions, idempotency | Same integration tests plus replicas, probes and PVC test |
| Cleanup | 28 resources destroyed | 28 resources destroyed |
The project deliberately exercises operational behavior rather than stopping after a successful terraform apply.
--run, root-account refusal, operator IP restriction, and isolated run directory.changed=0 on web and DB.SELECT 1; k3s also verifies replicas and Pod-level test PVC persistence.Test runs incur AWS charges. The runner attempts teardown after test failures and includes an interruption recovery script.
Kubernetes ran on private application EC2 using k3s in a live us-east-1 deployment.
| Live check | Observed result |
|---|---|
| Terraform apply | 28 resources created across three EC2 tiers |
| Private k3s node | Ready; Ubuntu 24.04.5 LTS, k3s v1.36.4+k3s1 |
| Flask Deployment | 2/2 non-root replicas available |
| Service | Internal Kubernetes Service; NodePort 80:30080/TCP |
| Application health | /healthz, /readyz and /health passed |
| Database | Real SELECT 1 on dedicated PostgreSQL EC2 |
| Network boundaries | Web → app and app → DB allowed; web → PostgreSQL blocked |
| Test-only storage | 1 GiB local-path PVC marker survived evidence Pod deletion/recreation |
| Ansible repeat pass | changed=0 on web and DB; systemd app play skipped |
| External request | Workstation → Nginx → k3s → PostgreSQL returned SELECT 1 |
| Terraform destroy | 28 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.




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.
Results from the completed us-east-1 deployment.
| Check | Observed result |
|---|---|
| Terraform provisioning | 28 resources created |
| Ansible first configuration | Three hosts; zero failures |
| Ansible second configuration | Zero changes on all three hosts |
| Application → PostgreSQL | Passed |
| Web → application | Passed |
| Direct web → PostgreSQL | Blocked as intended |
| External Nginx → application → database | status=ok; db_result=1 |
| Terraform teardown | 28 resources destroyed |
| Independent AWS cleanup checks | No remaining lab instances, NAT gateways, Elastic IPs, or VPCs |
| GitHub Actions | Terraform and Ansible static validation passed |
These screenshots document the September 22 Gunicorn/systemd run. The September 24 k3s run is shown separately above.
Separate public and private tiers, route tables, NAT, SSH bastion, and security-group-controlled traffic.
Terraform provisioning, Ansible Linux configuration, two runtime choices and scoped idempotency checks.
Private k3s, two non-root Flask replicas, NodePort, database-backed probes and a test-only PVC.
Positive and negative smoke tests, retained evidence, explicit destruction, and independent cleanup verification.
GitHub Actions checks Terraform formatting and validation, Ansible playbook syntax, and shell scripts without running billable cloud infrastructure.
Full source repository → Architecture and run documentation → k3s runbook → GitHub Actions history →