A simple Flask REST service with automated CI/CD, containerization, and AWS deployment.
GET /— returns plain text "Hello World"GET /hostname— returns JSON with the machine hostname:{ "hostname": "..." }GET /health— returns JSON health statusGET /info— returns JSON runtime infoGET /indexor/index.html— HTML landing page (protected with basic auth when enabled)
.
├── main.py # Flask application
├── requirements.txt # Python dependencies
├── Dockerfile # Docker image definition
├── .dockerignore # Docker build exclusions
├── tests/ # Unit tests
├── .github/workflows/ # CI/CD pipeline
│ └── docker-publish.yml # Build & push to GHCR
├── terraform/ # Infrastructure as Code
│ ├── main.tf # EC2, Security Groups, SSH keys
│ ├── variables.tf # Input variables
│ ├── outputs.tf # Outputs (IP, SSH command, etc.)
│ ├── provider.tf # AWS provider config
│ └── terraform.tfvars.example # Example configuration
└── ansible/ # Configuration management
├── deploy.yml # Deployment playbook
├── inventory.example # Inventory template
└── README.md # Ansible instructions
# Create and activate a virtualenv
python3 -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run the app
python main.py -p 8080
# Bind host (defaults to localhost for safety)
export BIND_HOST="127.0.0.1"
# Optional security settings
# Restrict access to a single hostname (Host header). Leave empty to disable.
export ALLOWED_HOST="example.com"
# Enable HTTP Basic Auth only when HTTPS is used.
export AUTH_ENABLED="true"
# Protect the HTML index with HTTP Basic Auth. Leave empty to disable.
export BASIC_AUTH_USER="admin"
export BASIC_AUTH_PASS="secret"
# Test endpoints
curl http://127.0.0.1:8080/
curl http://127.0.0.1:8080/hostname
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/info
# HTML index (protected if BASIC_AUTH_* is set)
curl -u admin:secret http://127.0.0.1:8080/index
# Run tests
pytest -q# Build image
docker build -t devops-project:latest .
# Run container
docker run -d -p 80:80 devops-project:latest
# Test
curl http://localhost/
curl http://localhost/hostnameGitHub Actions automatically on every push to main:
- ✅ Runs tests
- ✅ Builds Docker image
- ✅ Pushes to GitHub Container Registry (ghcr.io)
Image is available at: ghcr.io/yeborisov/devops-project:latest
GitHub Actions runs security scans on every push and PR to main:
- Bandit: Python static analysis
- pip-audit: Python dependency vulnerability scan
- Trivy: filesystem vulnerability scan
Automated infrastructure + application deployment via GitHub Actions:
- Go to Actions → Deploy Infrastructure and Application → Run workflow
- Workflow automatically:
- Sets up S3 backend for Terraform state (if needed)
- Provisions EC2 infrastructure with Terraform
- Deploys application with Ansible
- Verifies deployment with automated tests
- Optionally specify Docker image version
- Deployment requires approval from code owners
- Complete automation from infrastructure to application
📖 See DEPLOYMENT.md for complete setup guide
This project uses a separation of concerns approach:
- Terraform: Provisions infrastructure (EC2, Security Groups, SSH keys)
- Ansible: Deploys and manages the Docker container
This separation allows you to:
- Update the application without recreating infrastructure
- Deploy new versions with a single command
- Easily rollback if needed
- Reuse the same infrastructure for multiple deployments
- AWS account
- AWS CLI configured with credentials
- Terraform installed
- Ansible installed
- SSH key pair generated
ssh-keygen -t rsa -b 4096 -f ~/.ssh/devops-project-keycd terraform
# Copy example configuration
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with your settings
vi terraform.tfvarsUpdate the SSH public key in terraform.tfvars:
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC..."# Initialize Terraform
terraform init
# Preview changes
terraform plan
# Deploy infrastructure (EC2, Security Groups, SSH keys)
# Note: This only sets up the infrastructure, not the application
terraform apply
# Save important outputs (IP address, SSH command, etc.)
terraform outputImportant: Terraform only provisions the EC2 instance and installs Docker. The application container is deployed separately using Ansible in the next step.
You have two options for deployment:
Fully automated deployment with approval workflow. The workflow handles everything:
- Terraform state management (S3 + DynamoDB)
- Infrastructure provisioning (EC2, Security Groups, SSH keys)
- Application deployment with Ansible
- Automated verification
-
Add required secrets to GitHub:
# Repository → Settings → Secrets and variables → Actions # Add the following secrets: # EC2_SSH_PRIVATE_KEY - Your SSH private key cat ~/.ssh/devops-project-key # SSH_PUBLIC_KEY - Your SSH public key cat ~/.ssh/devops-project-key.pub # AWS_ACCESS_KEY_ID - From AWS IAM credentials # AWS_SECRET_ACCESS_KEY - From AWS IAM credentials
-
Setup production environment:
- Repository → Settings → Environments → New environment:
production - Add required reviewers (yourself)
- Save protection rules
- Repository → Settings → Environments → New environment:
-
Deploy:
- Go to Actions → Deploy Infrastructure and Application → Run workflow
- Choose terraform action:
apply(orplan-onlyto preview) - Optionally specify Docker image version
- Click Run workflow
- Approve when prompted
- Workflow automatically provisions infrastructure and deploys application
📖 See DEPLOYMENT.md for complete GitHub Actions setup
Manual deployment from your machine:
cd ../ansible
# Create inventory from example
cp inventory.example inventory
# Get EC2 IP from Terraform
EC2_IP=$(cd ../terraform && terraform output -raw instance_public_ip)
# Update inventory file with the IP
echo "[webservers]" > inventory
echo "$EC2_IP ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/devops-project-key" >> inventory
# Deploy from GHCR
ansible-playbook -i inventory deploy.yml \
-e "image=ghcr.io/yeborisov/devops-project:latest"# Get the EC2 public IP
EC2_IP=$(cd terraform && terraform output -raw instance_public_ip)
# Test the application
curl http://$EC2_IP/
curl http://$EC2_IP/hostname# Connect to EC2 instance
ssh -i ~/.ssh/devops-project-key ec2-user@$EC2_IP
# Check container status
docker ps
# View logs
docker logs simple-restOne of the main advantages of this approach is easy updates:
# After pushing new code and GitHub Actions builds a new image:
cd ansible
# Simply re-run the Ansible playbook
ansible-playbook -i inventory deploy.yml \
-e "image=ghcr.io/yeborisov/devops-project:latest"
# Or deploy a specific version
ansible-playbook -i inventory deploy.yml \
-e "image=ghcr.io/yeborisov/devops-project:v1.2.3"The playbook will:
- Pull the new image
- Stop the old container
- Start the new container
- Verify it's running
No need to recreate the EC2 instance or run Terraform again!
Old approach (User Data only):
- ❌ Runs only once at boot
- ❌ Can't update without recreating EC2
- ❌ Credentials in plaintext
- ❌ No rollback capability
- ❌ Hard to debug failures
Current approach (Terraform + Ansible):
- ✅ Separate infrastructure from deployment
- ✅ Update anytime with one command
- ✅ Credentials managed securely by Ansible
- ✅ Easy rollback to previous versions
- ✅ Idempotent and repeatable
- ✅ Better logging and error handling
- ✅ Can deploy to multiple servers easily
cd terraform
terraform destroyContainer not running?
ssh -i ~/.ssh/devops-project-key ec2-user@$EC2_IP "docker ps -a"
ssh -i ~/.ssh/devops-project-key ec2-user@$EC2_IP "docker logs simple-rest"Can't connect via SSH?
- Check security group allows SSH (port 22)
- Verify SSH key permissions:
chmod 400 ~/.ssh/devops-project-key - Check terraform outputs:
terraform output ssh_command
Application not responding?
- Check security group allows HTTP (port 80)
- Verify Docker container is running on port 80
- Check EC2 instance status in AWS console
Live Application: http://3.68.33.85 (if deployed)
Infrastructure managed by:
- Terraform - EC2 instance on Amazon Linux 2023 (t3.micro in eu-central-1)
- Ansible - Docker container deployment and management
- S3 + DynamoDB - Terraform remote state backend with locking
Container Image: ghcr.io/yeborisov/devops-project:latest
┌─────────────────┐
│ GitHub Actions │ Push code → Build → Test → Push to GHCR
└────────┬────────┘
│
▼
┌─────────────────┐
│ Deploy Workflow│ Manual trigger → Approve → Deploy
└────────┬────────┘
│
├──────────────────┐
▼ ▼
┌─────────┐ ┌──────────┐
│Terraform│ │ Ansible │
│ │ │ │
│• EC2 │ ────▶ │• Docker │
│• SG │ │• Deploy │
│• Keys │ │• Verify │
└─────────┘ └──────────┘
│ │
▼ ▼
┌─────────────────────────┐
│ AWS EC2 (eu-central-1) │
│ Amazon Linux 2023 │
│ Docker Container │
│ Port 80 (HTTP) │
└─────────────────────────┘