Terraform builds the network and three EC2 instances, Ansible configures them with Docker containers, and HashiCorp Vault supplies the database password. The application itself (vote, result, worker, Redis, Postgres) lives in its own repo: ironhack-project-1.
| Tier | Instance | Subnet | Runs | Reachable on |
|---|---|---|---|---|
| Frontend | voter-0, public IP |
public 10.0.1.0/24 (us-east-1a) |
vote (Flask), result (Node.js) |
8080 / 8081 from the internet |
| Backend | voter-1 |
private A 10.0.2.0/24 (us-east-1a) |
redis, worker (.NET) |
6379 from frontend SG only |
| Database | voter-2 |
private B 10.0.3.0/24 (us-east-1b) |
postgres:15-alpine |
5432 from backend and frontend SGs |
Security groups reference each other instead of CIDR ranges: the database accepts Postgres
traffic only from the backend and frontend groups, and the backend accepts Redis traffic
only from the frontend group. Private instances reach the internet through a NAT gateway.
SSH to them goes through the frontend box as a bastion (ProxyCommand, set in
ansible/group_vars/).
Terraform keeps its state in an S3 bucket (hk-voter-bucket) with versioning and locks it
with a DynamoDB table (hk-voters-app). The script infra/diagram.py generates the
diagram above (pip install diagrams, then python3 diagram.py).
- Terraform >= 1.5, Ansible >= 2.15, Vault CLI
- AWS credentials in your environment with rights to create VPC/EC2/S3/DynamoDB
- Python packages:
boto3,hvac - Ansible collections:
amazon.aws,community.docker,community.hashi_vault
1. Bootstrap the remote state backend (once):
cd infra/remote-state
terraform init && terraform apply2. Build the infrastructure:
cd infra
terraform init && terraform applyThis creates the VPC, subnets, gateways, security groups and the three instances. It also
generates an ED25519 keypair and writes the private key to
ansible/keys/voter-ssh-key.pem with 0400 permissions, so you never copy keys by hand.
3. Start Vault (dev mode) and store the database password. In a separate terminal tab:
vault server -devThen, using the root token the dev server prints:
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=<root token from the tab above>
vault kv put secret/voter postgres_password=<your password>The database role reads the password at deploy time with a community.hashi_vault lookup
(secret/data/voter:postgres_password), so it never appears in the playbooks or in git.
4. Configure the servers:
cd ansible
ansible-playbook site.ymlThere is no static inventory to edit: inventory.aws_ec2.yml asks AWS for running
instances tagged voter-* on every run and groups them by their Role tag
(role_frontend, role_backend, role_database). It picks up new IPs after a rebuild
automatically.
5. Vote. Open http://<frontend public IP>:8080 to vote and :8081 for results.
The Terraform output web_public_ip prints the public IP.
cd infra && terraform destroy
# then, if you want the state backend gone too (empty the bucket first):
cd remote-state && terraform destroyinfra/ Terraform: VPC, subnets, NAT, SGs, 3x EC2 (t3.micro, AL2023)
infra/remote-state/ Terraform: S3 bucket + DynamoDB lock table for state
infra/diagram.py Generates the architecture diagram above
ansible/site.yml Playbook: common -> frontend -> backend -> database roles
ansible/inventory.aws_ec2.yml Dynamic EC2 inventory (no hardcoded IPs)
ansible/roles/ common (Docker), frontend, backend, database
- Vault runs in dev mode: in-memory storage, root token, HTTP. Fine for this exercise, wrong for anything real.
- The frontend accepts SSH from
0.0.0.0/0. In production you would limit this to a known CIDR or use SSM Session Manager instead. ansible.cfgdisableshost_key_checkingbecause each rebuild replaces the instances and their host keys.
