A small product-management application deployed with Docker, Terraform, AWS, and Ansible.
The application has a static Nginx frontend and an Express/LowDB API. Locally, both run with Docker Compose. In AWS, Terraform creates the infrastructure and an Ansible controller deploys images from Amazon ECR.
graph LR
A[Browser] --> B[Frontend EC2 - Nginx];
B --> C[Backend ALB];
C --> D[Backend EC2 Instances];
D --> E["EFS(db.json)"];
F[GitHub Actions] -->|Builds Docker images| G[Amazon ECR];
G -->|Frontend image| B;
G -->|Backend image| D;
H[Terraform] -->|Creates| I[Ansible Controller EC2];
I -->|Deploys frontend| B;
I -->|Deploys backend| D;
Terraform creates a VPC, two public and two private subnets, NAT gateways, a public Application Load Balancer, an Ansible controller, one frontend instance, and two backend instances.
Install and authenticate the following tools before deploying:
- Docker Desktop/Engine with Docker Compose (Local development)
- Terraform 1.x
- AWS CLI v2
- AWS credentials for an account allowed to create the resources in this project
Configure the AWS CLI using an IAM identity with appropriate permissions:
aws configureThis stores these required credentials in your local AWS CLI profile (or supply equivalent environment variables in CI):
AWS_ACCESS_KEY_ID=<your access key ID>
AWS_SECRET_ACCESS_KEY=<your secret access key>
AWS_REGION=us-east-1
Do not commit AWS credentials, .env files, Terraform state, or private keys. The repository ignores these files.
| Value | Where to configure it | Keep secret? |
|---|---|---|
AWS_ACCESS_KEY_ID |
Local AWS CLI profile or environment variable; GitHub Actions AWS_ACCESS_KEY_ID repository secret |
Yes |
AWS_SECRET_ACCESS_KEY |
Local AWS CLI profile or environment variable; GitHub Actions AWS_SECRET_ACCESS_KEY repository secret |
Yes |
AWS_REGION |
Local AWS CLI profile or environment variable; GitHub Actions AWS_REGION repository variable |
No |
API_BASE_URL |
Docker Compose or the Ansible deployment command | No |
terraform/modules/aws-key.pem |
Generated locally by Terraform | Yes |
PORT is optional for the backend and defaults to 8080. API_BASE_URL is supplied automatically for local Compose and by the Ansible initialization script for AWS; only set it yourself when running the frontend outside those flows.
Cost notice: this creates EC2 instances, two NAT gateways, an Application Load Balancer, and other AWS resources that can incur charges. Destroy the stack when you are finished.
If this stack was applied before ECR was split into terraform/ecr, apply the main Terraform configuration once before creating the new repositories:
terraform -chdir=terraform init
terraform -chdir=terraform applyThe migration removes the legacy main_repository from the main state without deleting it. Review the plan to confirm that the repository is not being destroyed, then remove the legacy repository manually after its images are no longer needed.
The application images must exist in ECR before the main Terraform stack creates the instances.
terraform -chdir=terraform/ecr init
terraform -chdir=terraform/ecr applyReview the plan and type yes when Terraform asks for confirmation.
The GitHub Actions workflows run automatically when the relevant frontend or backend directories change on main.
Use Run workflow in GitHub Actions to dispatch the Build and Push Backend and Build and Push Frontend workflows manually. For GitHub Actions, configure the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY repository secrets and an AWS_REGION repository variable (for example, us-east-1).
Set the AWS account and registry variables:
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
AWS_REGION=us-east-1
ECR_REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"Sign Docker in to ECR:
aws ecr get-login-password --region "$AWS_REGION" \
| docker login --username AWS --password-stdin "$ECR_REGISTRY"Build and push both images:
docker build -t "$ECR_REGISTRY/main_repository/backend:latest" backend
docker build -t "$ECR_REGISTRY/main_repository/frontend:latest" frontend
docker push "$ECR_REGISTRY/main_repository/backend:latest"
docker push "$ECR_REGISTRY/main_repository/frontend:latest"terraform -chdir=terraform init
terraform -chdir=terraform applyTerraform generates an SSH private key at terraform/modules/aws-key.pem, stores a copy as a SecureString in Systems Manager Parameter Store, and starts the Ansible controller.
Get the server IP/DNS and SSH
- Retrieve the frontend EC2 public IP or DNS from the AWS Console or with the AWS CLI. Example (filters may vary by tags in your deployment):
aws ec2 describe-instances --filters "Name=tag:Name,Values=ansible" "Name=instance-state-name,Values=running" --query 'Reservations[].Instances[].[PublicIpAddress]' --output text- Ensure the private key is readable and SSH to the instance (replace user with ec2-user/ubuntu as appropriate):
chmod 400 terraform/modules/aws-key.pem
ssh -i terraform/modules/aws-key.pem ec2-user@<PUBLIC_IP_OR_DNS>If the AMI uses a different default user, replace ec2-user with ubuntu, centos, or admin accordingly.
Clone this repository and run the initialization script manually on the Ansible controller:
sudo dnf update -y
sudo dnf install -y ansible-core git python3-boto3
git clone https://github.com/AvivAbachi/terraform-aws-ansible-project.git
cd terraform-aws-ansible-project
chmod +x ansible-init.sh
./ansible-init.shThe script installs the controller dependencies, retrieves its SSH key, discovers the EC2 instances, installs Docker, and deploys the ECR images.
Wait for the EC2 instances and load balancer target health checks to become healthy. Then open the public IP or DNS name of the frontend EC2 instance in a browser. The frontend calls the backend through the public Application Load Balancer.
Destroy the main stack first, then the ECR repositories:
terraform -chdir=terraform destroy
terraform -chdir=terraform/ecr destroyDocker Desktop (or Docker Engine) with Docker Compose v2 is required. No secrets or environment setup are needed; Docker Compose supplies API_BASE_URL=http://localhost:8080 to the frontend.
From the repository root, start the application:
docker compose -f docker-compose.dev.yml up --buildOpen http://localhost:8081. The API is available at http://localhost:8080.
Verify the API:
curl http://localhost:8080/healthStop the containers with Ctrl+C, or remove the containers while keeping the data volume:
docker compose -f docker-compose.dev.yml downTo also delete local product data, run:
docker compose -f docker-compose.dev.yml down -vBased on the AWS + Terraform + Ansible mini-project.