Skip to content

Commit eab7eea

Browse files
Merge pull request #50 from AET-DevOps25/28-iac-automate-ec2-provisioning-and-deployment-with-terraform-and-ansible
28 iac automate ec2 provisioning and deployment with terraform and ansible
2 parents 8509c0f + f63b675 commit eab7eea

16 files changed

+658
-3
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Deploy to AWS
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
ec2_ip:
7+
required: true
8+
type: string
9+
workflow_dispatch:
10+
inputs:
11+
ec2_ip:
12+
description: 'EC2 public IP to deploy to'
13+
required: true
14+
type: string
15+
16+
permissions:
17+
contents: read
18+
packages: read
19+
20+
jobs:
21+
deploy:
22+
runs-on: ubuntu-latest
23+
environment:
24+
name: AWS
25+
url: 'https://client.${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}.nip.io'
26+
27+
steps:
28+
- name: Checkout Code
29+
uses: actions/checkout@v4
30+
31+
- name: Test SSH, disk space & Docker registry
32+
uses: appleboy/ssh-action@v1.0.3
33+
with:
34+
host: ${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}
35+
username: ${{ vars.AWS_EC2_USER }}
36+
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
37+
script: |
38+
set -e
39+
echo "Testing disk space:"
40+
df -h /
41+
echo "Testing Docker:"
42+
docker ps
43+
echo "Testing Docker registry login:"
44+
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
45+
echo "All initial checks passed."
46+
47+
- name: Copy Docker Compose File From Repo to the EC2 Instance
48+
uses: appleboy/scp-action@v0.1.7
49+
with:
50+
host: ${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}
51+
username: ${{ vars.AWS_EC2_USER }}
52+
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
53+
source: "./docker-compose.prod.yml"
54+
target: /home/${{ vars.AWS_EC2_USER }}
55+
56+
- name: Get latest tag (or use 1.0-alpha as fallback)
57+
id: get_tag
58+
run: |
59+
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "1.0-alpha")
60+
echo "tag=$TAG" >> $GITHUB_OUTPUT
61+
echo "Latest tag is: $TAG"
62+
63+
- name: Prepare Environment File (.env.prod)
64+
uses: appleboy/ssh-action@v1.0.3
65+
with:
66+
host: ${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}
67+
username: ${{ vars.AWS_EC2_USER }}
68+
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
69+
script: |
70+
set -e
71+
cd /home/${{ vars.AWS_EC2_USER }}
72+
rm -f .env.prod
73+
echo "Creating .env.prod file..."
74+
echo "MONGODB_CONTAINER_NAME=skill-forge-mongo-db" >> .env.prod
75+
echo "CLIENT_CONTAINER_NAME=skill-forge-client" >> .env.prod
76+
echo "SERVER_CONTAINER_NAME=skill-forge-server" >> .env.prod
77+
echo "GENAI_CONTAINER_NAME=skill-forge-genai" >> .env.prod
78+
echo "WEAVIATE_CONTAINER_NAME=skill-forge-weaviate" >> .env.prod
79+
80+
echo "MONGODB_EXPOSED_PORT=27018" >> .env.prod
81+
echo "MONGODB_DATABASE=${{ vars.MONGODB_DATABASE }}" >> .env.prod
82+
echo "MONGODB_USERNAME=${{ secrets.MONGODB_USERNAME }}" >> .env.prod
83+
echo "MONGODB_PASSWORD=${{ secrets.MONGODB_PASSWORD }}" >> .env.prod
84+
85+
echo "SPRING_PROFILE_ACTIVES=docker" >> .env.prod
86+
echo "SERVER_PORT_GATEWAY=8081" >> .env.prod
87+
echo "SERVER_PORT_USER=8082" >> .env.prod
88+
echo "SERVER_PORT_COURSES=8083" >> .env.prod
89+
90+
echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" >> .env.prod
91+
echo "JWT_EXPIRATION_MS=${{ vars.JWT_EXPIRATION_MS }}" >> .env.prod
92+
93+
echo "VITE_PORT=3000" >> .env.prod
94+
echo "VITE_APP_NAME=SkillForge.ai" >> .env.prod
95+
echo "VITE_APP_VERSION=${{ steps.get_tag.outputs.tag }}" >> .env.prod
96+
echo "VITE_API_BASE_URL=/api/" >> .env.prod
97+
echo "VITE_API_VERSION=v1" >> .env.prod
98+
echo "BUILD_MODE=production" >> .env.prod
99+
100+
echo "WEAVIATE_HOST=weaviate-db" >> .env.prod
101+
echo "WEAVIATE_EXPOSED_HTTP_PORT=1234" >> .env.prod
102+
echo "WEAVIATE_EXPOSED_GRPC_PORT=50051" >> .env.prod
103+
104+
echo "GENAI_APP_NAME=skill-forge-genai" >> .env.prod
105+
echo "GENAI_APP_VERSION=${{ steps.get_tag.outputs.tag }}" >> .env.prod
106+
echo "GENAI_PORT=8888" >> .env.prod
107+
echo "CORS_ALLOW_ORIGINS=*" >> .env.prod
108+
echo "IS_DEV_MODE=0" >> .env.prod
109+
echo "UVICORN_WORKERS=${{ vars.UVICORN_WORKERS }}" >> .env.prod
110+
echo "LLM_PROVIDER=${{ vars.LLM_PROVIDER }}" >> .env.prod
111+
echo "OPENAI_API_BASE=${{ vars.OPENAI_API_BASE }}" >> .env.prod
112+
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env.prod
113+
echo "OPENAI_MODEL=${{ vars.OPENAI_MODEL }}" >> .env.prod
114+
115+
echo "CLIENT_HOST=client.${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}.nip.io" >> .env.prod
116+
echo "SERVER_HOST=api.${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}.nip.io" >> .env.prod
117+
echo "PUBLIC_API_URL=https://api.${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}.nip.io/api" >> .env.prod
118+
119+
chmod 600 .env.prod
120+
echo ".env.prod file created."
121+
122+
- name: SSH to the EC2 Instance and Start Docker Compose
123+
uses: appleboy/ssh-action@v1.0.3
124+
timeout-minutes: 10
125+
with:
126+
host: ${{ github.event.inputs.ec2_ip || inputs.ec2_ip }}
127+
username: ${{ vars.AWS_EC2_USER }}
128+
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
129+
script: |
130+
set -e
131+
cd /home/${{ vars.AWS_EC2_USER }}
132+
echo " >>> Logging into Docker registry..."
133+
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
134+
echo " >>> Starting Docker Compose..."
135+
if ! docker compose -f docker-compose.prod.yml --env-file .env.prod up --pull=always -d; then
136+
echo "❌ Docker Compose failed. Showing service status:"
137+
docker compose -f docker-compose.prod.yml ps
138+
echo "-----------------------------------------"
139+
echo "------------ Recent logs ----------------"
140+
docker compose -f docker-compose.prod.yml logs --tail=40
141+
exit 1
142+
fi

.github/workflows/docker-build-push.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ name: Build and Push Docker Images
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, test]
6+
paths:
7+
- 'client/**'
8+
- 'server/skillforge-gateway/**'
9+
- 'server/skillforge-user/**'
10+
- 'genai/**'
611
pull_request:
7-
branches: [main]
12+
branches: [main, test]
813
paths:
914
- 'client/**'
1015
- 'server/skillforge-gateway/**'
@@ -97,7 +102,7 @@ jobs:
97102
with:
98103
context: ${{ matrix.path }}
99104
file: ${{ matrix.path }}/Dockerfile
100-
push: true
105+
push: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' }}
101106
tags: ghcr.io/aet-devops25/team-git-it-together/${{ matrix.image }}:${{ steps.set_tag.outputs.tag }}
102107
cache-from: type=registry,ref=ghcr.io/aet-devops25/team-git-it-together/${{ matrix.image }}-cache
103108
cache-to: type=registry,ref=ghcr.io/aet-devops25/team-git-it-together/${{ matrix.image }}-cache,mode=max

.github/workflows/deploy_docker_to_aws.yml renamed to .github/workflows/manual_docker_deploy_to_aws.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# This workflow performs a manual deployment of Docker images to an AWS EC2 instance
2+
# via SSH/SCP. It is intended for legacy/manual operations. For full automation,
3+
# see the automated deployment workflow (.github/workflows/auto_docker_deploy_to_aws.yml).
4+
5+
16
name: Deploy Docker Images
27

38
on:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Provision and Configure AWS
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'infra/**'
9+
workflow_dispatch:
10+
inputs:
11+
AWS_ACCESS_KEY_ID:
12+
description: 'AWS Access Key ID'
13+
required: true
14+
type: string
15+
AWS_SECRET_ACCESS_KEY:
16+
description: 'AWS Secret Access Key'
17+
required: true
18+
type: string
19+
AWS_REGION:
20+
description: 'AWS Region (e.g. us-east-1)'
21+
required: true
22+
default: 'us-east-1'
23+
type: string
24+
permissions:
25+
contents: read
26+
packages: read
27+
id-token: write
28+
jobs:
29+
provision_and_configure:
30+
runs-on: ubuntu-latest
31+
outputs:
32+
ec2_ip: ${{ steps.get_public_ip.outputs.ec2_ip }}
33+
steps:
34+
- name: Checkout repository
35+
uses: actions/checkout@v4
36+
37+
# Set up AWS credentials for Terraform
38+
- name: Configure AWS credentials
39+
uses: aws-actions/configure-aws-credentials@v4
40+
with:
41+
aws-access-key-id: ${{ github.event.inputs.AWS_ACCESS_KEY_ID }}
42+
aws-secret-access-key: ${{ github.event.inputs.AWS_SECRET_ACCESS_KEY }}
43+
aws-region: ${{ github.event.inputs.AWS_REGION }}
44+
45+
# Terraform: Init, Plan, Apply
46+
- name: Terraform Init
47+
run: terraform -chdir=infra/terraform init
48+
49+
- name: Terraform Plan
50+
run: terraform -chdir=infra/terraform plan
51+
52+
- name: Terraform Apply
53+
run: terraform -chdir=infra/terraform apply -auto-approve
54+
55+
# Output EC2 Public IP for use by deploy workflow
56+
- name: Get EC2 Public IP
57+
id: get_public_ip
58+
run: |
59+
echo "The EC2 has the following public IP: $(terraform -chdir=infra/terraform output -raw public_ip)"
60+
echo "ec2_ip=$(terraform -chdir=infra/terraform output -raw public_ip)" >> $GITHUB_OUTPUT
61+
62+
# Write SSH key for Ansible
63+
- name: Write SSH key
64+
run: |
65+
echo "${{ secrets.AWS_EC2_PRIVATE_KEY }}" > ~/.ssh/id_rsa
66+
chmod 600 ~/.ssh/id_rsa
67+
68+
# Write inventory for Ansible using the new IP
69+
- name: Write inventory.ini for Ansible
70+
run: |
71+
echo "[git_it_together]" > infra/ansible/inventory.ini
72+
echo "${{ steps.get_public_ip.outputs.ec2_ip }} ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa" >> infra/ansible/inventory.ini
73+
74+
# Add host to known_hosts to avoid SSH prompts
75+
- name: Add EC2 to known_hosts
76+
run: |
77+
ssh-keyscan ${{ steps.get_public_ip.outputs.ec2_ip }} >> ~/.ssh/known_hosts
78+
79+
# Run Ansible Playbook for server configuration
80+
- name: Configure EC2 with Ansible
81+
working-directory: infra/ansible
82+
run: |
83+
ansible-playbook -i inventory.ini docker-setup.yml
84+
85+
- name: Trigger Deploy Workflow
86+
uses: peter-evans/workflow-dispatch@v1
87+
with:
88+
workflow: "deploy_to_aws.yml"
89+
ref: ${{ github.ref }}
90+
inputs: |
91+
ec2_ip: "${{ steps.get_public_ip.outputs.ec2_ip }}"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,5 @@ sketch
389389
genai/tmp/
390390
genai/crawled_content.log
391391
genai/.env
392+
393+
.ansible/

infra/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# 🚀 git-it-together Infrastructure Automation
2+
3+
Provision and configure cloud infrastructure for the git-it-together teams's platform using **Terraform** (for AWS provisioning) and **Ansible** (for configuration management).
4+
5+
---
6+
7+
## 🧰 What’s Inside?
8+
9+
- **Terraform:**
10+
- Provisions AWS resources (VPC, subnet, EC2 instance, Elastic IP, security group, etc.)
11+
- **Ansible:**
12+
- Installs and configures Docker & Docker Compose on the provisioned EC2 instance
13+
- Adds the `ubuntu` user to the `docker` group for passwordless Docker commands
14+
15+
---
16+
17+
## 📂 Project Structure
18+
19+
```
20+
21+
infra/
22+
├── terraform/
23+
│ ├── main.tf
24+
│ ├── variables.tf
25+
│ ├── outputs.tf
26+
│ ├── terraform.tfvars.example
27+
│ ├── README.md
28+
│ └── .gitignore
29+
└── ansible/
30+
│ ├── playbook.yml
31+
│ ├── inventory.example.ini
32+
│ ├── README.md
33+
│ └── .gitignore
34+
└── README.md
35+
36+
````
37+
38+
---
39+
40+
## 🚦 Prerequisites
41+
42+
- [Terraform](https://www.terraform.io/downloads)
43+
- [Ansible](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)
44+
- AWS account and credentials (for Terraform)
45+
- SSH private key for your EC2 instance
46+
47+
---
48+
49+
## 🏗️ Deploying Infrastructure with Terraform
50+
51+
1. **Configure Variables:**
52+
53+
- Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in required values, e.g., `key_name`, `eip_allocation_id`.
54+
55+
2. **Initialize Terraform:**
56+
57+
```bash
58+
terraform -chdir=terraform init
59+
````
60+
61+
3. **Preview Infrastructure Changes:**
62+
63+
```bash
64+
terraform -chdir=terraform plan
65+
```
66+
67+
4. **Apply Infrastructure Changes (DO NOT RUN ON PROD)**
68+
69+
```bash
70+
terraform -chdir=terraform apply
71+
```
72+
73+
---
74+
75+
## 🛠️ Server Configuration with Ansible
76+
77+
1. **Prepare your Ansible inventory:**
78+
79+
* Copy `inventory.example.ini` to `inventory.ini`.
80+
* Add your EC2 public IP (from Terraform output):
81+
82+
```
83+
[git_it_together]
84+
<EC2_PUBLIC_IP> ansible_user=ubuntu ansible_ssh_private_key_file=./labuser.pem
85+
```
86+
* Make sure your SSH key (`labuser.pem`) is present and has `chmod 600` permissions.
87+
88+
2. **Dry Run the Playbook:**
89+
90+
```bash
91+
ansible-playbook -i inventory.ini docker-setup.yml --check
92+
```
93+
94+
3. **(Optional) Run the Playbook (DO NOT RUN ON PROD):**
95+
96+
```bash
97+
ansible-playbook -i inventory.ini docker-setup.yml
98+
```
99+
100+
---
101+
102+
## 🧑‍💻 Automation / CI/CD
103+
104+
* In CI pipelines, the `terraform.tfvars` file will be dynamically generated with environment-specific values.
105+
* Ansible playbooks can be triggered post-Terraform deployment to configure the infrastructure.
106+
* The Ansible `inventory.ini` will also be dynamically generated based on the Terraform output and environment variables on GitHub.
107+
108+
---
109+
110+
## 📚 References
111+
112+
* [Terraform AWS Provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)
113+
* [Ansible Docs](https://docs.ansible.com/ansible/latest/index.html)

0 commit comments

Comments
 (0)