Merge pull request #12 from elizabeth-ikechukwu/feat/week5-terraform
feat: week 5 & 6 — Terraform infrastructure and AWS EC2 deployment
This commit is contained in:
@@ -241,4 +241,76 @@ jobs:
|
|||||||
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-backend:latest\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-backend:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-backend:sha-${{ steps.vars.outputs.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-backend:sha-${{ steps.vars.outputs.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-frontend:latest\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-frontend:latest\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-frontend:sha-${{ steps.vars.outputs.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`${{ env.DOCKERHUB_REGISTRY }}/task-frontend:sha-${{ steps.vars.outputs.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
terraform-plan:
|
||||||
|
name: Terraform Plan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-integration-test
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
concurrency:
|
||||||
|
group: terraform
|
||||||
|
cancel-in-progress: false
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: infra/terraform
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure AWS credentials via OIDC
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::424522917561:role/github-actions-scalable-task-platform
|
||||||
|
aws-region: us-east-1
|
||||||
|
|
||||||
|
- name: Setup Terraform
|
||||||
|
uses: hashicorp/setup-terraform@v3
|
||||||
|
with:
|
||||||
|
terraform_version: 1.10.3
|
||||||
|
|
||||||
|
- name: Terraform Init
|
||||||
|
run: terraform init
|
||||||
|
|
||||||
|
- name: Terraform Plan
|
||||||
|
run: terraform plan -no-color -lock-timeout=5m
|
||||||
|
env:
|
||||||
|
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
||||||
|
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
||||||
|
|
||||||
|
terraform-apply:
|
||||||
|
name: Terraform Apply
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: publish
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
concurrency:
|
||||||
|
group: terraform
|
||||||
|
cancel-in-progress: false
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: infra/terraform
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure AWS credentials via OIDC
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::424522917561:role/github-actions-scalable-task-platform
|
||||||
|
aws-region: us-east-1
|
||||||
|
|
||||||
|
- name: Setup Terraform
|
||||||
|
uses: hashicorp/setup-terraform@v3
|
||||||
|
with:
|
||||||
|
terraform_version: 1.10.3
|
||||||
|
|
||||||
|
- name: Terraform Init
|
||||||
|
run: terraform init
|
||||||
|
|
||||||
|
- name: Terraform Apply
|
||||||
|
run: terraform apply -auto-approve -lock-timeout=5m
|
||||||
|
env:
|
||||||
|
TF_VAR_aws_account_id: ${{ secrets.TF_VAR_AWS_ACCOUNT_ID }}
|
||||||
|
TF_VAR_aws_region: ${{ secrets.TF_VAR_AWS_REGION }}
|
||||||
|
|||||||
+8
-1
@@ -138,4 +138,11 @@ dist
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
postman/
|
postman/
|
||||||
.postman
|
.postman
|
||||||
|
# Terraform
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
terraform.tfvars
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.backup
|
||||||
|
tfplan
|
||||||
@@ -12,7 +12,7 @@ Built in public, one week at a time.
|
|||||||
- Containerization with Docker
|
- Containerization with Docker
|
||||||
- CI/CD automation with GitHub Actions, Amazon ECR, and Docker Hub
|
- CI/CD automation with GitHub Actions, Amazon ECR, and Docker Hub
|
||||||
- Infrastructure as Code with Terraform
|
- Infrastructure as Code with Terraform
|
||||||
- Cloud deployment on AWS
|
- Cloud deployment on AWS EC2
|
||||||
- Container orchestration with Kubernetes
|
- Container orchestration with Kubernetes
|
||||||
- Monitoring and observability with Prometheus and Grafana
|
- Monitoring and observability with Prometheus and Grafana
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ Built in public, one week at a time.
|
|||||||
| 2 | Dockerize the backend | [Done](docs/week-2-update.md) |
|
| 2 | Dockerize the backend | [Done](docs/week-2-update.md) |
|
||||||
| 3 | React frontend and Docker Compose | [Done](docs/week-3-update.md) |
|
| 3 | React frontend and Docker Compose | [Done](docs/week-3-update.md) |
|
||||||
| 4 | CI/CD pipeline with GitHub Actions, ECR and Docker Hub | [Done](docs/week-4-update.md) |
|
| 4 | CI/CD pipeline with GitHub Actions, ECR and Docker Hub | [Done](docs/week-4-update.md) |
|
||||||
| 5 | Terraform infrastructure setup | Upcoming |
|
| 5 | Terraform infrastructure and CI/CD pipeline extension | [Done](docs/week-5-update.md) |
|
||||||
| 6 | Cloud deployment on AWS EC2 | Upcoming |
|
| 6 | Cloud deployment on AWS EC2 | [Done](docs/week-6-update.md) |
|
||||||
| 7 | Database integration | Upcoming |
|
| 7 | Database integration | Upcoming |
|
||||||
| 8 | Kubernetes deployment | Upcoming |
|
| 8 | Kubernetes deployment | Upcoming |
|
||||||
| 9 | Monitoring with Prometheus and Grafana | Upcoming |
|
| 9 | Monitoring with Prometheus and Grafana | Upcoming |
|
||||||
@@ -97,15 +97,34 @@ Frontend runs on http://localhost:80
|
|||||||
|
|
||||||
## CI/CD Pipeline
|
## CI/CD Pipeline
|
||||||
|
|
||||||
Every pull request and push to `main` triggers a four-stage pipeline:
|
Every pull request and push to `main` triggers a five-stage pipeline:
|
||||||
|
|
||||||
```
|
```
|
||||||
lint -> unit-test -> build-and-integration-test -> publish
|
lint -> unit-test -> build-and-integration-test -> publish -> terraform-apply
|
||||||
```
|
```
|
||||||
|
|
||||||
The publish stage runs on merge to `main` only. Images are pushed to both Amazon ECR and Docker Hub, tagged with `latest` and the short Git SHA for traceability.
|
- `terraform-plan` runs on every pull request — shows exactly what infrastructure will change before merging
|
||||||
|
- `terraform-apply` runs on merge to `main` — infrastructure is provisioned by the pipeline, never from a local machine
|
||||||
|
- The `publish` stage pushes images to both Amazon ECR and Docker Hub, tagged with `latest` and the short Git SHA
|
||||||
|
|
||||||
AWS authentication uses OIDC - no static credentials stored in GitHub secrets.
|
AWS authentication uses OIDC — no static credentials stored in GitHub secrets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
All infrastructure is provisioned with Terraform and lives in `infra/terraform/`.
|
||||||
|
|
||||||
|
| Resource | Details |
|
||||||
|
|---|---|
|
||||||
|
| VPC | Custom VPC with public subnet |
|
||||||
|
| Networking | Internet gateway, route tables |
|
||||||
|
| Security | Security groups with least-privilege rules |
|
||||||
|
| Compute | EC2 t3.micro running Amazon Linux 2023 |
|
||||||
|
| IAM | Instance profile with SSM and ECR permissions |
|
||||||
|
| State | S3 remote backend with native state locking |
|
||||||
|
|
||||||
|
EC2 instances configure themselves on first boot via a user data script — Docker is installed, images are pulled from ECR, and containers start automatically. No SSH required. All access via AWS SSM Session Manager.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Week 5 — Terraform Infrastructure and CI/CD Pipeline Extension
|
||||||
|
|
||||||
|
## What I Built
|
||||||
|
|
||||||
|
Week 5 added two major pieces: full AWS infrastructure provisioned with Terraform, and an extended CI/CD pipeline that owns infrastructure changes end to end.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terraform Bootstrap
|
||||||
|
|
||||||
|
Before the main infrastructure, I created a bootstrap layer in `infra/terraform/bootstrap/` to set up remote state storage.
|
||||||
|
|
||||||
|
- S3 bucket with versioning, AES256 encryption, and public access blocked
|
||||||
|
- Native state locking using `use_lockfile = true` — available from Terraform 1.10 onwards
|
||||||
|
- No DynamoDB table required — Terraform writes a `.tflock` file directly to S3
|
||||||
|
|
||||||
|
This is the modern approach. One less AWS resource to manage, same production-grade protection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Infrastructure
|
||||||
|
|
||||||
|
All resources live in `infra/terraform/` and are split across focused files:
|
||||||
|
|
||||||
|
**`vpc.tf`**
|
||||||
|
- Custom VPC (`10.0.0.0/16`)
|
||||||
|
- Public subnet (`10.0.1.0/24`) in `us-east-1a`
|
||||||
|
- Internet gateway attached to the VPC
|
||||||
|
- Route table with a default route to the internet gateway
|
||||||
|
- Route table association to the public subnet
|
||||||
|
|
||||||
|
**`security-group.tf`**
|
||||||
|
- Security group allowing inbound HTTP (port 80) from anywhere
|
||||||
|
- All outbound traffic allowed
|
||||||
|
- Port 22 (SSH) intentionally closed — access via SSM only
|
||||||
|
|
||||||
|
**`iam.tf`**
|
||||||
|
- IAM role for EC2 with two managed policies attached:
|
||||||
|
- `AmazonSSMManagedInstanceCore` — enables SSM Session Manager access
|
||||||
|
- `AmazonEC2ContainerRegistryReadOnly` — allows pulling images from ECR
|
||||||
|
- Instance profile wrapping the role
|
||||||
|
|
||||||
|
**`ec2.tf`**
|
||||||
|
- Data source querying AWS at plan time for the latest Amazon Linux 2023 AMI — no hardcoded AMI ID
|
||||||
|
- EC2 `t3.micro` instance in the public subnet
|
||||||
|
- 20GB encrypted `gp3` root volume
|
||||||
|
- User data script that runs on first boot:
|
||||||
|
- Installs Docker and starts the service
|
||||||
|
- Authenticates to ECR
|
||||||
|
- Pulls backend and frontend images
|
||||||
|
- Starts both containers with `--restart unless-stopped`
|
||||||
|
|
||||||
|
**`outputs.tf`**
|
||||||
|
- Public IP of the EC2 instance — printed after every apply
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD Pipeline Extension
|
||||||
|
|
||||||
|
Added two new jobs to `.github/workflows/ci-cd.yml`:
|
||||||
|
|
||||||
|
**`terraform-plan`**
|
||||||
|
- Triggers on every pull request
|
||||||
|
- Runs `terraform init` and `terraform plan`
|
||||||
|
- Shows exactly what will change before any merge
|
||||||
|
- Depends on `build-and-integration-test` passing first
|
||||||
|
|
||||||
|
**`terraform-apply`**
|
||||||
|
- Triggers on merge to `main` only
|
||||||
|
- Runs `terraform init` and `terraform apply -auto-approve`
|
||||||
|
- Infrastructure is created by the pipeline — never from a local machine
|
||||||
|
|
||||||
|
Both jobs authenticate to AWS using the existing OIDC role. No static credentials.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Discipline
|
||||||
|
|
||||||
|
The local machine provisions once to validate everything works. Then `terraform destroy` hands control to the pipeline permanently. From that point forward, infrastructure changes only happen through Git — a PR shows the plan, a merge triggers the apply.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture After Week 5
|
||||||
|
|
||||||
|
```
|
||||||
|
GitHub
|
||||||
|
│
|
||||||
|
├── Pull Request → terraform-plan (shows what will change)
|
||||||
|
│
|
||||||
|
└── Merge to main → lint → unit-test → build → publish → terraform-apply
|
||||||
|
│
|
||||||
|
AWS Infrastructure
|
||||||
|
VPC / Subnet / SG
|
||||||
|
IAM / EC2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Added
|
||||||
|
|
||||||
|
```
|
||||||
|
infra/terraform/
|
||||||
|
├── vpc.tf
|
||||||
|
├── security-group.tf
|
||||||
|
├── iam.tf
|
||||||
|
├── ec2.tf
|
||||||
|
└── outputs.tf
|
||||||
|
|
||||||
|
.github/workflows/ci-cd.yml (terraform-plan and terraform-apply jobs added)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
See `docs/week-05/` for:
|
||||||
|
- Terraform plan output
|
||||||
|
- Terraform apply output showing 13 resources created
|
||||||
|
- GitHub Actions pipeline with all jobs passing
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Week 6 — Cloud Deployment on AWS EC2
|
||||||
|
|
||||||
|
## What I Built
|
||||||
|
|
||||||
|
Week 6 took everything built in Week 5 and made it live. TaskFlow — the full-stack Node.js and React application — is now running on AWS EC2, deployed automatically by the CI/CD pipeline with zero manual server setup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How Deployment Works
|
||||||
|
|
||||||
|
When a PR merges to `main`, the pipeline:
|
||||||
|
|
||||||
|
1. Runs lint, unit tests, and integration tests
|
||||||
|
2. Builds Docker images and pushes them to Amazon ECR
|
||||||
|
3. Runs `terraform apply` — creates or updates infrastructure
|
||||||
|
4. The EC2 instance boots and runs a user data script automatically
|
||||||
|
|
||||||
|
The user data script handles everything on first boot:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Update the system packages
|
||||||
|
2. Install Docker and start the service
|
||||||
|
3. Wait for Docker to be fully ready
|
||||||
|
4. Add ec2-user to the docker group
|
||||||
|
5. Install AWS CLI
|
||||||
|
6. Authenticate to ECR using the IAM instance profile
|
||||||
|
7. Pull task-backend:latest from ECR
|
||||||
|
8. Pull task-frontend:latest from ECR
|
||||||
|
9. Start backend container on port 3000 (restart unless stopped)
|
||||||
|
10. Wait for backend to be ready
|
||||||
|
11. Start frontend container on port 80 (restart unless stopped)
|
||||||
|
```
|
||||||
|
|
||||||
|
No SSH. No manual commands. The instance configures itself.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Access Method
|
||||||
|
|
||||||
|
All server access is through **AWS SSM Session Manager** — not SSH.
|
||||||
|
|
||||||
|
- Port 22 is closed on the security group
|
||||||
|
- The EC2 IAM role has `AmazonSSMManagedInstanceCore` attached
|
||||||
|
- Sessions are opened via the AWS console or AWS CLI
|
||||||
|
|
||||||
|
This is the production-standard approach. No key pairs to manage, no inbound SSH exposure, full audit trail of every session in AWS CloudTrail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Runs on EC2
|
||||||
|
|
||||||
|
| Container | Image Source | Port | Restart Policy |
|
||||||
|
|---|---|---|---|
|
||||||
|
| task-backend | Amazon ECR | 3000 (internal) | unless-stopped |
|
||||||
|
| task-frontend | Amazon ECR | 80 (public) | unless-stopped |
|
||||||
|
|
||||||
|
The frontend container runs Nginx, which serves the React build and proxies `/api` requests to the backend container.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture After Week 6
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
EC2 Public IP :80
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
task-frontend (Nginx)
|
||||||
|
│
|
||||||
|
├── Serves React frontend (/)
|
||||||
|
│
|
||||||
|
└── Proxies /api requests → task-backend :3000
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Express API
|
||||||
|
(in-memory store)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
**Why no Docker Compose on EC2?**
|
||||||
|
The containers are run directly with `docker run` in the user data script. Docker Compose adds a dependency that needs to be installed separately on Amazon Linux 2023. Direct `docker run` with `--link` keeps the bootstrap simple and reliable.
|
||||||
|
|
||||||
|
**Why `--restart unless-stopped`?**
|
||||||
|
If the EC2 instance reboots, Docker restarts the containers automatically. The application comes back online without any intervention.
|
||||||
|
|
||||||
|
**Why ECR and not Docker Hub?**
|
||||||
|
ECR is private, in the same AWS region as the EC2 instance, and authenticated via IAM — no credentials to manage. Pulls are fast and free within the same region.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
```
|
||||||
|
infra/terraform/ec2.tf (user data script updated — sleep added for Docker readiness)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
See `docs/week-06/` for:
|
||||||
|
- TaskFlow running live in the browser at the EC2 public IP
|
||||||
|
- SSM Session Manager connection to the EC2 instance
|
||||||
|
- Docker containers running on the instance (`docker ps`)
|
||||||
|
- GitHub Actions pipeline showing full run with terraform-apply
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Sid": "ECRAuthentication",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": "ecr:GetAuthorizationToken",
|
|
||||||
"Resource": "*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Sid": "ECRImagePush",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"ecr:BatchCheckLayerAvailability",
|
|
||||||
"ecr:BatchGetImage",
|
|
||||||
"ecr:CompleteLayerUpload",
|
|
||||||
"ecr:DescribeRepositories",
|
|
||||||
"ecr:GetDownloadUrlForLayer",
|
|
||||||
"ecr:InitiateLayerUpload",
|
|
||||||
"ecr:PutImage",
|
|
||||||
"ecr:UploadLayerPart"
|
|
||||||
],
|
|
||||||
"Resource": [
|
|
||||||
"arn:aws:ecr:us-east-1:424522917561:repository/task-backend",
|
|
||||||
"arn:aws:ecr:us-east-1:424522917561:repository/task-frontend"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Sid": "GitHubActionsOIDC",
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Principal": {
|
|
||||||
"Federated": "arn:aws:iam::424522917561:oidc-provider/token.actions.githubusercontent.com"
|
|
||||||
},
|
|
||||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
|
||||||
"Condition": {
|
|
||||||
"StringEquals": {
|
|
||||||
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
|
|
||||||
},
|
|
||||||
"StringLike": {
|
|
||||||
"token.actions.githubusercontent.com:sub": "repo:elizabeth-ikechukwu/scalable-task-platform:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.10.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
region = var.aws_region
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# S3 Bucket — Terraform remote state storage
|
||||||
|
# Native state locking via use_lockfile = true
|
||||||
|
# (available from Terraform >= 1.10.0)
|
||||||
|
# No DynamoDB table required
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_s3_bucket" "terraform_state" {
|
||||||
|
bucket = var.state_bucket_name
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = var.state_bucket_name
|
||||||
|
Project = "scalable-task-platform"
|
||||||
|
ManagedBy = "terraform-bootstrap"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket_versioning" "terraform_state" {
|
||||||
|
bucket = aws_s3_bucket.terraform_state.id
|
||||||
|
|
||||||
|
versioning_configuration {
|
||||||
|
status = "Enabled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
|
||||||
|
bucket = aws_s3_bucket.terraform_state.id
|
||||||
|
|
||||||
|
rule {
|
||||||
|
apply_server_side_encryption_by_default {
|
||||||
|
sse_algorithm = "AES256"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket_public_access_block" "terraform_state" {
|
||||||
|
bucket = aws_s3_bucket.terraform_state.id
|
||||||
|
|
||||||
|
block_public_acls = true
|
||||||
|
block_public_policy = true
|
||||||
|
ignore_public_acls = true
|
||||||
|
restrict_public_buckets = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
output "state_bucket_name" {
|
||||||
|
description = "S3 bucket name for Terraform remote state — copy this into your main backend config"
|
||||||
|
value = aws_s3_bucket.terraform_state.bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
output "state_bucket_arn" {
|
||||||
|
description = "ARN of the Terraform state S3 bucket"
|
||||||
|
value = aws_s3_bucket.terraform_state.arn
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
variable "aws_region" {
|
||||||
|
description = "AWS region for all resources"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "state_bucket_name" {
|
||||||
|
description = "Name of the S3 bucket that stores Terraform state. Must be globally unique."
|
||||||
|
type = string
|
||||||
|
default = "scalable-task-platform-tfstate"
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Data Source — Latest Amazon Linux 2023 AMI
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
data "aws_ami" "amazon_linux_2023" {
|
||||||
|
most_recent = true
|
||||||
|
owners = ["amazon"]
|
||||||
|
|
||||||
|
filter {
|
||||||
|
name = "name"
|
||||||
|
values = ["al2023-ami-2023.*-x86_64"]
|
||||||
|
}
|
||||||
|
|
||||||
|
filter {
|
||||||
|
name = "virtualization-type"
|
||||||
|
values = ["hvm"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# EC2 Instance
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_instance" "app" {
|
||||||
|
ami = data.aws_ami.amazon_linux_2023.id
|
||||||
|
instance_type = "t3.micro"
|
||||||
|
subnet_id = aws_subnet.public.id
|
||||||
|
vpc_security_group_ids = [aws_security_group.nginx.id]
|
||||||
|
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
|
||||||
|
|
||||||
|
user_data = <<-EOF
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Update system
|
||||||
|
dnf update -y
|
||||||
|
|
||||||
|
# Install Docker
|
||||||
|
dnf install -y docker
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
|
||||||
|
# Wait for Docker to be fully ready
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Add ec2-user to docker group
|
||||||
|
usermod -aG docker ec2-user
|
||||||
|
|
||||||
|
# Ensure SSM Agent is running (pre-installed on AL2023)
|
||||||
|
systemctl enable amazon-ssm-agent
|
||||||
|
systemctl start amazon-ssm-agent
|
||||||
|
|
||||||
|
# Authenticate to ECR
|
||||||
|
aws ecr get-login-password --region ${var.aws_region} | \
|
||||||
|
docker login --username AWS --password-stdin \
|
||||||
|
${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com
|
||||||
|
|
||||||
|
# Pull images from ECR
|
||||||
|
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
||||||
|
docker pull ${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-frontend:latest
|
||||||
|
|
||||||
|
# Run backend
|
||||||
|
docker run -d \
|
||||||
|
--name task-backend \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3000:3000 \
|
||||||
|
${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-backend:latest
|
||||||
|
|
||||||
|
# Wait for backend to be ready
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Run frontend
|
||||||
|
docker run -d \
|
||||||
|
--name task-frontend \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 80:80 \
|
||||||
|
--link task-backend:backend \
|
||||||
|
${var.aws_account_id}.dkr.ecr.${var.aws_region}.amazonaws.com/task-frontend:latest
|
||||||
|
EOF
|
||||||
|
|
||||||
|
root_block_device {
|
||||||
|
volume_size = 20
|
||||||
|
volume_type = "gp3"
|
||||||
|
encrypted = true
|
||||||
|
delete_on_termination = true
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_options {
|
||||||
|
http_tokens = "required"
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-app-server"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# IAM Role — EC2 instance identity
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_iam_role" "ec2_role" {
|
||||||
|
name = "${var.project_name}-ec2-role"
|
||||||
|
|
||||||
|
assume_role_policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Principal = {
|
||||||
|
Service = "ec2.amazonaws.com"
|
||||||
|
}
|
||||||
|
Action = "sts:AssumeRole"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-ec2-role"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Policy Attachments
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_iam_role_policy_attachment" "ssm" {
|
||||||
|
role = aws_iam_role.ec2_role.name
|
||||||
|
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_iam_role_policy_attachment" "ecr" {
|
||||||
|
role = aws_iam_role.ec2_role.name
|
||||||
|
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Instance Profile
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_iam_instance_profile" "ec2_profile" {
|
||||||
|
name = "${var.project_name}-ec2-profile"
|
||||||
|
role = aws_iam_role.ec2_role.name
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-ec2-profile"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.10.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
aws = {
|
||||||
|
source = "hashicorp/aws"
|
||||||
|
version = "~> 5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "scalable-task-platform-tfstate"
|
||||||
|
key = "main/terraform.tfstate"
|
||||||
|
region = "us-east-1"
|
||||||
|
use_lockfile = true
|
||||||
|
encrypt = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "aws" {
|
||||||
|
region = var.aws_region
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
output "vpc_id" {
|
||||||
|
description = "ID of the VPC"
|
||||||
|
value = aws_vpc.main.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "public_subnet_id" {
|
||||||
|
description = "ID of the public subnet"
|
||||||
|
value = aws_subnet.public.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "private_subnet_id" {
|
||||||
|
description = "ID of the private subnet"
|
||||||
|
value = aws_subnet.private.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ec2_instance_id" {
|
||||||
|
description = "EC2 instance ID — use this to connect via SSM"
|
||||||
|
value = aws_instance.app.id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ec2_public_ip" {
|
||||||
|
description = "Public IP of the EC2 instance"
|
||||||
|
value = aws_instance.app.public_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ec2_public_dns" {
|
||||||
|
description = "Public DNS of the EC2 instance"
|
||||||
|
value = aws_instance.app.public_dns
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Security Group — Nginx (Public)
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_security_group" "nginx" {
|
||||||
|
name = "${var.project_name}-nginx-sg"
|
||||||
|
description = "Security group for Nginx reverse proxy"
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
description = "HTTP from internet"
|
||||||
|
from_port = 80
|
||||||
|
to_port = 80
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
description = "HTTPS from internet"
|
||||||
|
from_port = 443
|
||||||
|
to_port = 443
|
||||||
|
protocol = "tcp"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
egress {
|
||||||
|
description = "Allow all outbound"
|
||||||
|
from_port = 0
|
||||||
|
to_port = 0
|
||||||
|
protocol = "-1"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-nginx-sg"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Security Group — Backend (Private)
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_security_group" "backend" {
|
||||||
|
name = "${var.project_name}-backend-sg"
|
||||||
|
description = "Security group for backend API - only accepts traffic from Nginx"
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
|
||||||
|
ingress {
|
||||||
|
description = "Allow traffic from Nginx only"
|
||||||
|
from_port = 3000
|
||||||
|
to_port = 3000
|
||||||
|
protocol = "tcp"
|
||||||
|
security_groups = [aws_security_group.nginx.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
egress {
|
||||||
|
description = "Allow all outbound"
|
||||||
|
from_port = 0
|
||||||
|
to_port = 0
|
||||||
|
protocol = "-1"
|
||||||
|
cidr_blocks = ["0.0.0.0/0"]
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-backend-sg"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
variable "aws_region" {
|
||||||
|
description = "AWS region for all resources"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "aws_account_id" {
|
||||||
|
description = "AWS account ID — set in terraform.tfvars, never hardcoded"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "project_name" {
|
||||||
|
description = "Project name used for naming and tagging all resources"
|
||||||
|
type = string
|
||||||
|
default = "scalable-task-platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "environment" {
|
||||||
|
description = "Deployment environment"
|
||||||
|
type = string
|
||||||
|
default = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vpc_cidr" {
|
||||||
|
description = "CIDR block for the VPC"
|
||||||
|
type = string
|
||||||
|
default = "10.0.0.0/16"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "public_subnet_cidr" {
|
||||||
|
description = "CIDR block for the public subnet"
|
||||||
|
type = string
|
||||||
|
default = "10.0.1.0/24"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "private_subnet_cidr" {
|
||||||
|
description = "CIDR block for the private subnet"
|
||||||
|
type = string
|
||||||
|
default = "10.0.2.0/24"
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# VPC
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_vpc" "main" {
|
||||||
|
cidr_block = var.vpc_cidr
|
||||||
|
enable_dns_hostnames = true
|
||||||
|
enable_dns_support = true
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-vpc"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Subnets
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_subnet" "public" {
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
cidr_block = var.public_subnet_cidr
|
||||||
|
availability_zone = "${var.aws_region}a"
|
||||||
|
map_public_ip_on_launch = true
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-public-subnet"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_subnet" "private" {
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
cidr_block = var.private_subnet_cidr
|
||||||
|
availability_zone = "${var.aws_region}a"
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-private-subnet"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Internet Gateway
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_internet_gateway" "main" {
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-igw"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Route Tables
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
resource "aws_route_table" "public" {
|
||||||
|
vpc_id = aws_vpc.main.id
|
||||||
|
|
||||||
|
route {
|
||||||
|
cidr_block = "0.0.0.0/0"
|
||||||
|
gateway_id = aws_internet_gateway.main.id
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = {
|
||||||
|
Name = "${var.project_name}-public-rt"
|
||||||
|
Project = var.project_name
|
||||||
|
Environment = var.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_route_table_association" "public" {
|
||||||
|
subnet_id = aws_subnet.public.id
|
||||||
|
route_table_id = aws_route_table.public.id
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user