Upload files to "sk1"

This commit is contained in:
Pradeep Dileepkumar 2026-05-20 07:31:24 +00:00
parent ad73eeb809
commit 0aca015ca5
10 changed files with 1005 additions and 0 deletions

4
sk1/Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80

235
sk1/README.md Normal file
View File

@ -0,0 +1,235 @@
# Notes App — AWS Cloud Deployment
## What the application does
A **Notes** web application where users can create, view, and delete short text notes via a web browser. Notes are stored persistently in a PostgreSQL database. The app is publicly accessible over HTTPS at the configured domain.
---
## Cloud services and architecture
**Cloud provider:** Amazon Web Services (AWS), region `eu-central-1` (Frankfurt)
| Service | Purpose |
|---|---|
| **Amazon ECS Fargate** | Runs the frontend and backend containers serverlessly. Automatically restarts containers on failure. |
| **Amazon RDS (PostgreSQL 16)** | Managed relational database. Provides persistent storage, automated backups (7-day retention). |
| **Application Load Balancer (ALB)** | Receives all internet traffic. Routes `/api/*` to the backend, everything else to the frontend. Redirects HTTP → HTTPS. |
| **AWS Certificate Manager (ACM)** | Issues and renews a free TLS/HTTPS certificate for the domain. |
| **Amazon ECR** | Private Docker image registry. Stores the backend and frontend images. |
| **AWS Secrets Manager** | Stores the database password securely. Never stored in code or Git. |
| **Amazon CloudWatch Logs** | Collects access logs and application logs from both containers. |
| **Amazon VPC + Security Groups** | Network isolation. ALB SG allows 80/443 from internet. ECS SG allows traffic only from ALB. RDS SG allows 5432 only from ECS. |
### Architecture
```
Browser (HTTPS)
Application Load Balancer (port 443, ACM certificate, HTTP→HTTPS redirect)
├── /api/* ──▶ ECS Fargate: Backend (Flask, port 5000)
│ │
│ ▼
│ RDS PostgreSQL (port 5432, private subnet)
└── /* ──▶ ECS Fargate: Frontend (Nginx, port 80)
```
The browser loads the static frontend from the ALB. All API calls (`/api/*`) are made by the browser directly to the ALB, which forwards them to the backend. The frontend container serves only static files — it does not proxy to the backend. The backend connects to RDS using the `DATABASE_URL` environment variable.
---
## Cost analysis — 1,000 users/day, 50 GB database
Estimated for **eu-central-1 (Frankfurt)**, 1 year of operation.
| Resource | Specification | Unit price | Monthly | Annual |
|---|---|---|---|---|
| ECS Fargate — Backend | 0.25 vCPU, 0.5 GB RAM, 730 h/mo | $0.04048/vCPU-h + $0.004445/GB-h | ~$8.30 | ~$99.60 |
| ECS Fargate — Frontend | 0.25 vCPU, 0.5 GB RAM, 730 h/mo | same | ~$8.30 | ~$99.60 |
| RDS db.t3.micro | PostgreSQL, 1 vCPU, 1 GB RAM, 730 h/mo | ~$0.022/h | ~$16.10 | ~$193.20 |
| RDS Storage | 50 GB gp2 | $0.138/GB-mo | ~$6.90 | ~$82.80 |
| RDS Automated Backups | 50 GB (free up to DB size) | $0.00 | $0.00 | $0.00 |
| ALB | 1 ALB + ~1,000 req/day (~30k/mo) | $0.0252/h + LCU | ~$18.50 | ~$222.00 |
| ECR | ~500 MB images stored | $0.10/GB-mo | ~$0.05 | ~$0.60 |
| CloudWatch Logs | ~5 GB/mo ingestion | $0.57/GB | ~$2.85 | ~$34.20 |
| Data Transfer Out | ~10 GB/mo (1,000 users × ~330 KB) | $0.09/GB | ~$0.90 | ~$10.80 |
| Secrets Manager | 1 secret | $0.40/secret/mo | $0.40 | $4.80 |
| **Total** | | | **~$62.30** | **~$747.60** |
> Prices are estimates based on AWS public pricing (2025). Actual costs depend on traffic patterns. Use the [AWS Pricing Calculator](https://calculator.aws) for exact quotes.
---
## Files and their content
```
sk1/
├── prepare-app.sh # Creates all AWS resources and deploys the app
├── remove-app.sh # Tears down all AWS resources
├── backup.sh # Creates a manual RDS snapshot
├── .env.example # Template for required environment variables
├── .gitignore # Excludes .env from Git
├── backend/
│ ├── app.py # Flask REST API: GET/POST/DELETE /api/notes, /health
│ ├── requirements.txt # Python dependencies: flask, psycopg2-binary
│ └── Dockerfile # python:3.12-slim, runs app.py on port 5000
├── frontend/
│ ├── index.html # Single-page app: dark UI, fetch() calls to /api/
│ ├── nginx.conf # Nginx: serves index.html for all paths
│ └── Dockerfile # nginx:1.27-alpine, serves static files
└── db/
└── init.sql # Creates notes table, inserts sample rows
```
---
## Configuration description
All secrets and environment-specific values are passed via environment variables, never hardcoded in source files:
- **`DATABASE_URL`** — injected into the backend ECS task definition at deploy time. Constructed from `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_NAME`.
- **`DB_PASSWORD`** — stored in AWS Secrets Manager under `notes-app/db-password`. Set via `.env` before running the script.
- **`DOMAIN_NAME`** — the public domain for the app (e.g. `notes.example.com`). Used to request the ACM certificate and configure the ALB.
- **`AWS_REGION`** / **`AWS_ACCOUNT_ID`** — target AWS account and region.
The `.env` file is listed in `.gitignore` and must never be committed to Git.
---
## How to run and use the application
### Prerequisites
- AWS CLI installed and configured (`aws configure`) with permissions for: ECS, ECR, RDS, ELB, ACM, IAM, EC2, Secrets Manager, CloudWatch Logs
- Docker installed and running
- A domain name you control (to add DNS CNAME records)
### Deploy
```bash
cd sk1
cp .env.example .env
# Edit .env — fill in AWS_REGION, AWS_ACCOUNT_ID, DOMAIN_NAME, DB_PASSWORD
source .env
chmod +x prepare-app.sh remove-app.sh
./prepare-app.sh
```
During deployment the script will print two DNS records to add:
1. A CNAME for ACM certificate validation
2. A CNAME pointing your domain to the ALB
Once DNS propagates and the certificate is validated, open **https://your-domain.com** in a browser.
### Use the app
- Type a note in the text box and click **+ Add Note**
- Notes appear below, newest first
- Click **✕ Delete** to remove a note
### Remove everything
```bash
source .env
./remove-app.sh
```
> The ACM certificate is not deleted automatically. Delete it manually if no longer needed:
> `aws acm delete-certificate --certificate-arn <ARN> --region $AWS_REGION`
---
## How to perform a data backup
RDS automated backups are enabled with a **7-day retention period** and run automatically every day.
**Manual snapshot using the provided script:**
```bash
source .env
./backup.sh
```
This creates a timestamped RDS snapshot (e.g. `notes-app-manual-20260520-120000`) and waits for it to complete.
**Restore from snapshot:**
```bash
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier notes-app-db-restored \
--db-snapshot-identifier <snapshot-id> \
--region $AWS_REGION
```
**Export data as SQL dump** (requires `psql` and RDS publicly accessible or VPN):
```bash
PGPASSWORD=$DB_PASSWORD pg_dump \
-h <RDS_ENDPOINT> -U $DB_USERNAME -d $DB_NAME \
> backup_$(date +%Y%m%d).sql
```
---
## How to view access logs from the internet
Container logs (including Nginx access logs) are streamed to **Amazon CloudWatch Logs**.
**Live tail — frontend (Nginx access log):**
```bash
aws logs tail /ecs/notes-app/frontend --follow --region $AWS_REGION
```
**Live tail — backend:**
```bash
aws logs tail /ecs/notes-app/backend --follow --region $AWS_REGION
```
**Query last 100 log events:**
```bash
aws logs get-log-events \
--log-group-name /ecs/notes-app/frontend \
--log-stream-name $(aws logs describe-log-streams \
--log-group-name /ecs/notes-app/frontend \
--order-by LastEventTime --descending \
--query "logStreams[0].logStreamName" --output text --region $AWS_REGION) \
--limit 100 \
--region $AWS_REGION
```
---
## Conditions for running prepare-app.sh and remove-app.sh
**prepare-app.sh:**
- AWS CLI must be installed and configured with credentials that have permissions for: ECS, ECR, RDS, ELB, ACM, IAM, EC2, Secrets Manager, CloudWatch Logs
- Docker must be running locally
- `.env` must exist with all required variables set (`AWS_REGION`, `AWS_ACCOUNT_ID`, `DOMAIN_NAME`, `DB_PASSWORD`)
- The domain in `DOMAIN_NAME` must be one you can add DNS CNAME records to
- Run from the `sk1/` directory: `source .env && ./prepare-app.sh`
- The script is idempotent — safe to run multiple times
**remove-app.sh:**
- Same AWS CLI and `.env` requirements as above
- All deletions are idempotent — safe to run multiple times
- Run from the `sk1/` directory: `source .env && ./remove-app.sh`
**backup.sh:**
- Same AWS CLI and `.env` requirements as above (`AWS_REGION` is sufficient)
- RDS instance `notes-app-db` must be running
- Run from the `sk1/` directory: `source .env && ./backup.sh`
---
## Use of artificial intelligence
This project was developed with the assistance of **Kiro AI** (`kiro-cli chat` agent, Claude Sonnet model).
| What | How AI was used |
|---|---|
| `prepare-app.sh`, `remove-app.sh`, `backup.sh` | Generated by AI, reviewed and verified by the author against AWS documentation |
| `README.md` | Generated by AI based on assignment requirements, reviewed and corrected by the author |
| `backend/app.py`, `frontend/index.html` | Originally written for a previous assignment, adapted with AI assistance |
| `nginx.conf`, `Dockerfiles`, `init.sql` | Generated by AI, reviewed by the author |
The AI was used as a coding assistant. All generated content was reviewed, understood, and verified by the author before submission.

41
sk1/app.py Normal file
View File

@ -0,0 +1,41 @@
import os
import psycopg2
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
def get_conn():
return psycopg2.connect(os.environ["DATABASE_URL"])
@app.route("/api/notes", methods=["GET"])
def get_notes():
with get_conn() as conn, conn.cursor() as cur:
cur.execute("SELECT id, content, created_at FROM notes ORDER BY created_at DESC")
rows = cur.fetchall()
return jsonify([{"id": r[0], "content": r[1], "created_at": r[2].isoformat()} for r in rows])
@app.route("/api/notes", methods=["POST"])
def add_note():
data = request.get_json()
content = (data or {}).get("content", "").strip()
if not content:
abort(400, "content required")
with get_conn() as conn, conn.cursor() as cur:
cur.execute("INSERT INTO notes (content) VALUES (%s) RETURNING id, content, created_at", (content,))
row = cur.fetchone()
conn.commit()
return jsonify({"id": row[0], "content": row[1], "created_at": row[2].isoformat()}), 201
@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
def delete_note(note_id):
with get_conn() as conn, conn.cursor() as cur:
cur.execute("DELETE FROM notes WHERE id = %s", (note_id,))
conn.commit()
return "", 204
@app.route("/health")
def health():
return "ok", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

22
sk1/backup.sh Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# backup.sh — Create a manual RDS snapshot for Notes App
# Usage: source .env && ./backup.sh
set -euo pipefail
: "${AWS_REGION:?Set AWS_REGION in .env}"
SNAPSHOT_ID="notes-app-manual-$(date +%Y%m%d-%H%M%S)"
echo "Creating RDS snapshot: ${SNAPSHOT_ID}..."
aws rds create-db-snapshot \
--db-instance-identifier notes-app-db \
--db-snapshot-identifier "$SNAPSHOT_ID" \
--region "$AWS_REGION" \
--output none
echo "Waiting for snapshot to complete..."
aws rds wait db-snapshot-completed \
--db-snapshot-identifier "$SNAPSHOT_ID" \
--region "$AWS_REGION"
echo "✅ Backup complete: ${SNAPSHOT_ID}"

149
sk1/index.html Normal file
View File

@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Notes App</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #0f172a;
color: #e2e8f0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 16px;
}
header { text-align: center; margin-bottom: 32px; }
header h1 {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #818cf8, #38bdf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
header p { color: #64748b; margin-top: 4px; font-size: 0.9rem; }
.card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 16px;
padding: 24px;
width: 100%;
max-width: 580px;
}
textarea {
width: 100%;
height: 90px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 10px;
color: #e2e8f0;
font-size: 0.95rem;
padding: 12px;
resize: none;
outline: none;
transition: border-color 0.2s;
}
textarea:focus { border-color: #818cf8; }
textarea::placeholder { color: #475569; }
button.add-btn {
margin-top: 12px;
width: 100%;
padding: 11px;
background: linear-gradient(135deg, #818cf8, #38bdf8);
border: none;
border-radius: 10px;
color: #0f172a;
font-weight: 700;
font-size: 0.95rem;
cursor: pointer;
transition: opacity 0.2s;
}
button.add-btn:hover { opacity: 0.85; }
#status { color: #f87171; font-size: 0.85rem; margin-top: 8px; min-height: 18px; }
.notes-list { width: 100%; max-width: 580px; margin-top: 24px; display: flex; flex-direction: column; gap: 12px; }
.note {
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; } }
.note-body { flex: 1; }
.note-body span { display: block; white-space: pre-wrap; font-size: 0.95rem; line-height: 1.5; color: #cbd5e1; }
.note-body small { display: block; margin-top: 6px; font-size: 0.75rem; color: #475569; }
.del {
background: transparent;
border: 1px solid #475569;
color: #94a3b8;
border-radius: 8px;
padding: 4px 10px;
cursor: pointer;
font-size: 0.8rem;
transition: background 0.2s, color 0.2s;
white-space: nowrap;
}
.del:hover { background: #f87171; border-color: #f87171; color: #fff; }
.empty { text-align: center; color: #475569; font-size: 0.9rem; padding: 24px 0; }
</style>
</head>
<body>
<header>
<h1>✦ Notes App</h1>
<p>Capture your thoughts, instantly.</p>
</header>
<div class="card">
<textarea id="content" placeholder="Write a note..."></textarea>
<button class="add-btn" onclick="addNote()">+ Add Note</button>
<p id="status"></p>
</div>
<div class="notes-list" id="notes"></div>
<script>
async function loadNotes() {
const res = await fetch('/api/notes');
const notes = await res.json();
const container = document.getElementById('notes');
if (!notes.length) {
container.innerHTML = '<p class="empty">No notes yet. Add one above!</p>';
return;
}
container.innerHTML = notes.map(n => `
<div class="note">
<div class="note-body">
<span>${escHtml(n.content)}</span>
<small>${new Date(n.created_at).toLocaleString()}</small>
</div>
<button class="del" onclick="deleteNote(${n.id})">✕ Delete</button>
</div>`).join('');
}
async function addNote() {
const ta = document.getElementById('content');
const content = ta.value.trim();
if (!content) return;
const res = await fetch('/api/notes', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content})
});
const status = document.getElementById('status');
if (res.ok) { ta.value = ''; status.textContent = ''; loadNotes(); }
else status.textContent = 'Error adding note.';
}
async function deleteNote(id) {
await fetch('/api/notes/' + id, {method: 'DELETE'});
loadNotes();
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
loadNotes();
</script>
</body>
</html>

9
sk1/init.sql Normal file
View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS notes (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO notes (content) VALUES
('Welcome to the Notes App!'),
('Add, view, and delete notes using the web interface.');

12
sk1/nginx.conf Normal file
View File

@ -0,0 +1,12 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
access_log /var/log/nginx/access.log;
}

401
sk1/prepare-app.sh Normal file
View File

@ -0,0 +1,401 @@
#!/usr/bin/env bash
# prepare-app.sh — Deploy Notes App to AWS (ECS Fargate + RDS + ALB + ACM HTTPS)
# Usage: source .env && ./prepare-app.sh
set -euo pipefail
# ── Required environment variables (set in .env) ─────────────────────────────
: "${AWS_REGION:?Set AWS_REGION in .env}"
: "${AWS_ACCOUNT_ID:?Set AWS_ACCOUNT_ID in .env}"
: "${DOMAIN_NAME:?Set DOMAIN_NAME in .env}"
: "${DB_PASSWORD:?Set DB_PASSWORD in .env}"
: "${DB_USERNAME:=appuser}"
: "${DB_NAME:=appdb}"
APP="notes-app"
CLUSTER="${APP}-cluster"
ECR_BACKEND="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${APP}-backend"
ECR_FRONTEND="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${APP}-frontend"
log() { echo "$*"; }
# ── 1. ECR repositories ───────────────────────────────────────────────────────
log "Creating ECR repositories..."
aws ecr describe-repositories --repository-names "${APP}-backend" --region "$AWS_REGION" &>/dev/null \
|| aws ecr create-repository --repository-name "${APP}-backend" --region "$AWS_REGION" --output none
aws ecr describe-repositories --repository-names "${APP}-frontend" --region "$AWS_REGION" &>/dev/null \
|| aws ecr create-repository --repository-name "${APP}-frontend" --region "$AWS_REGION" --output none
# ── 2. Build & push images ────────────────────────────────────────────────────
log "Logging in to ECR..."
aws ecr get-login-password --region "$AWS_REGION" \
| docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
log "Building and pushing backend..."
docker build --platform linux/amd64 -t "${APP}-backend" ./backend
docker tag "${APP}-backend:latest" "${ECR_BACKEND}:latest"
docker push "${ECR_BACKEND}:latest"
log "Building and pushing frontend..."
docker build --platform linux/amd64 -t "${APP}-frontend" ./frontend
docker tag "${APP}-frontend:latest" "${ECR_FRONTEND}:latest"
docker push "${ECR_FRONTEND}:latest"
# ── 3. VPC & networking ───────────────────────────────────────────────────────
log "Looking up default VPC..."
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" \
--query "Vpcs[0].VpcId" --output text --region "$AWS_REGION")
SUBNET_IDS_RAW=$(aws ec2 describe-subnets \
--filters "Name=vpc-id,Values=${VPC_ID}" "Name=defaultForAz,Values=true" \
--query "Subnets[*].SubnetId" --output text --region "$AWS_REGION")
SUBNET1=$(echo "$SUBNET_IDS_RAW" | awk '{print $1}')
SUBNET2=$(echo "$SUBNET_IDS_RAW" | awk '{print $2}')
# ── 4. Security groups ────────────────────────────────────────────────────────
log "Creating security groups..."
get_or_create_sg() {
local name="$1" desc="$2"
local id
id=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=${name}" "Name=vpc-id,Values=${VPC_ID}" \
--query "SecurityGroups[0].GroupId" --output text --region "$AWS_REGION" 2>/dev/null)
if [ -z "$id" ] || [ "$id" = "None" ]; then
id=$(aws ec2 create-security-group \
--group-name "$name" --description "$desc" \
--vpc-id "$VPC_ID" --query "GroupId" --output text --region "$AWS_REGION")
fi
echo "$id"
}
ALB_SG=$(get_or_create_sg "${APP}-alb-sg" "ALB SG for ${APP}")
ECS_SG=$(get_or_create_sg "${APP}-ecs-sg" "ECS tasks SG for ${APP}")
RDS_SG=$(get_or_create_sg "${APP}-rds-sg" "RDS SG for ${APP}")
# Add rules (ignore errors if already exist)
aws ec2 authorize-security-group-ingress --group-id "$ALB_SG" --region "$AWS_REGION" \
--ip-permissions \
'IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[{CidrIp=0.0.0.0/0}]' \
'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0}]' \
--output none 2>/dev/null || true
aws ec2 authorize-security-group-ingress --group-id "$ECS_SG" --region "$AWS_REGION" \
--ip-permissions \
"IpProtocol=tcp,FromPort=80,ToPort=80,UserIdGroupPairs=[{GroupId=${ALB_SG}}]" \
"IpProtocol=tcp,FromPort=5000,ToPort=5000,UserIdGroupPairs=[{GroupId=${ALB_SG}}]" \
--output none 2>/dev/null || true
aws ec2 authorize-security-group-ingress --group-id "$RDS_SG" --region "$AWS_REGION" \
--ip-permissions \
"IpProtocol=tcp,FromPort=5432,ToPort=5432,UserIdGroupPairs=[{GroupId=${ECS_SG}}]" \
--output none 2>/dev/null || true
# ── 5. RDS PostgreSQL ─────────────────────────────────────────────────────────
log "Creating RDS PostgreSQL instance (this takes ~5 min)..."
DB_INSTANCE="${APP}-db"
DB_STATUS=$(aws rds describe-db-instances \
--db-instance-identifier "$DB_INSTANCE" \
--query "DBInstances[0].DBInstanceStatus" --output text --region "$AWS_REGION" 2>/dev/null || echo "none")
if [ "$DB_STATUS" = "none" ]; then
aws rds create-db-instance \
--db-instance-identifier "$DB_INSTANCE" \
--db-instance-class db.t3.micro \
--engine postgres \
--engine-version "16" \
--master-username "$DB_USERNAME" \
--master-user-password "$DB_PASSWORD" \
--db-name "$DB_NAME" \
--allocated-storage 20 \
--storage-type gp2 \
--vpc-security-group-ids "$RDS_SG" \
--no-multi-az \
--no-publicly-accessible \
--backup-retention-period 7 \
--region "$AWS_REGION" \
--output none
fi
log "Waiting for RDS to be available..."
aws rds wait db-instance-available \
--db-instance-identifier "$DB_INSTANCE" --region "$AWS_REGION"
DB_HOST=$(aws rds describe-db-instances \
--db-instance-identifier "$DB_INSTANCE" \
--query "DBInstances[0].Endpoint.Address" --output text --region "$AWS_REGION")
log "RDS endpoint: ${DB_HOST}"
DATABASE_URL="postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:5432/${DB_NAME}"
# ── 6. Secrets Manager ───────────────────────────────────────────────────────
log "Storing DB password in Secrets Manager..."
SECRET_NAME="${APP}/db-password"
aws secretsmanager describe-secret --secret-id "$SECRET_NAME" --region "$AWS_REGION" &>/dev/null \
|| aws secretsmanager create-secret \
--name "$SECRET_NAME" \
--secret-string "$DB_PASSWORD" \
--region "$AWS_REGION" --output none
# ── 7. IAM role for ECS task execution ───────────────────────────────────────
log "Creating ECS task execution role..."
EXEC_ROLE="${APP}-exec-role"
aws iam get-role --role-name "$EXEC_ROLE" &>/dev/null || \
aws iam create-role --role-name "$EXEC_ROLE" \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}' \
--output none
aws iam attach-role-policy --role-name "$EXEC_ROLE" \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy 2>/dev/null || true
EXEC_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${EXEC_ROLE}"
# ── 8. ECS Cluster ────────────────────────────────────────────────────────────
log "Creating ECS cluster..."
aws ecs describe-clusters --clusters "$CLUSTER" --region "$AWS_REGION" \
--query "clusters[?status=='ACTIVE'].clusterName" --output text \
| grep -q "$CLUSTER" \
|| aws ecs create-cluster --cluster-name "$CLUSTER" --region "$AWS_REGION" --output none
# ── 9. CloudWatch log groups ──────────────────────────────────────────────────
aws logs create-log-group --log-group-name "/ecs/${APP}/backend" --region "$AWS_REGION" 2>/dev/null || true
aws logs create-log-group --log-group-name "/ecs/${APP}/frontend" --region "$AWS_REGION" 2>/dev/null || true
# ── 10. ALB ───────────────────────────────────────────────────────────────────
log "Creating Application Load Balancer..."
ALB_ARN=$(aws elbv2 describe-load-balancers --names "${APP}-alb" \
--query "LoadBalancers[0].LoadBalancerArn" --output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -z "$ALB_ARN" ] || [ "$ALB_ARN" = "None" ]; then
ALB_ARN=$(aws elbv2 create-load-balancer \
--name "${APP}-alb" \
--subnets "$SUBNET1" "$SUBNET2" \
--security-groups "$ALB_SG" \
--scheme internet-facing \
--type application \
--query "LoadBalancers[0].LoadBalancerArn" --output text --region "$AWS_REGION")
fi
ALB_DNS=$(aws elbv2 describe-load-balancers \
--load-balancer-arns "$ALB_ARN" \
--query "LoadBalancers[0].DNSName" --output text --region "$AWS_REGION")
log "ALB DNS: ${ALB_DNS}"
# ── 11. Target groups ─────────────────────────────────────────────────────────
log "Creating target groups..."
get_or_create_tg() {
local name="$1" port="$2" health_path="$3"
local arn
arn=$(aws elbv2 describe-target-groups --names "$name" \
--query "TargetGroups[0].TargetGroupArn" --output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -z "$arn" ] || [ "$arn" = "None" ]; then
arn=$(aws elbv2 create-target-group \
--name "$name" --protocol HTTP --port "$port" \
--vpc-id "$VPC_ID" --target-type ip \
--health-check-path "$health_path" \
--query "TargetGroups[0].TargetGroupArn" --output text --region "$AWS_REGION")
fi
echo "$arn"
}
FRONTEND_TG=$(get_or_create_tg "${APP}-frontend-tg" 80 "/")
BACKEND_TG=$(get_or_create_tg "${APP}-backend-tg" 5000 "/health")
# ── 12. ACM Certificate ───────────────────────────────────────────────────────
log "Requesting ACM certificate for ${DOMAIN_NAME}..."
CERT_ARN=$(aws acm list-certificates \
--query "CertificateSummaryList[?DomainName=='${DOMAIN_NAME}'].CertificateArn | [0]" \
--output text --region "$AWS_REGION")
if [ -z "$CERT_ARN" ] || [ "$CERT_ARN" = "None" ]; then
CERT_ARN=$(aws acm request-certificate \
--domain-name "$DOMAIN_NAME" \
--validation-method DNS \
--query "CertificateArn" --output text --region "$AWS_REGION")
# Print the CNAME record needed for validation
sleep 5
CNAME_NAME=$(aws acm describe-certificate --certificate-arn "$CERT_ARN" --region "$AWS_REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" --output text)
CNAME_VALUE=$(aws acm describe-certificate --certificate-arn "$CERT_ARN" --region "$AWS_REGION" \
--query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" --output text)
echo ""
echo "════════════════════════════════════════════════════════════"
echo " ACTION REQUIRED: Add these DNS records to your domain:"
echo ""
echo " 1. Certificate validation CNAME:"
echo " Name: ${CNAME_NAME}"
echo " Value: ${CNAME_VALUE}"
echo ""
echo " 2. Point your domain to the ALB:"
echo " ${DOMAIN_NAME} CNAME ${ALB_DNS}"
echo "════════════════════════════════════════════════════════════"
echo ""
echo "Waiting for certificate validation (up to 30 min)..."
aws acm wait certificate-validated --certificate-arn "$CERT_ARN" --region "$AWS_REGION"
fi
# ── 13. ALB Listeners ─────────────────────────────────────────────────────────
log "Creating ALB listeners..."
# HTTP → redirect to HTTPS
HTTP_LISTENER=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" \
--query "Listeners[?Port==\`80\`].ListenerArn | [0]" \
--output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -z "$HTTP_LISTENER" ] || [ "$HTTP_LISTENER" = "None" ]; then
aws elbv2 create-listener \
--load-balancer-arn "$ALB_ARN" \
--protocol HTTP --port 80 \
--default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}' \
--region "$AWS_REGION" --output none
fi
# HTTPS listener → default to frontend
HTTPS_LISTENER=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" \
--query "Listeners[?Port==\`443\`].ListenerArn | [0]" \
--output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -z "$HTTPS_LISTENER" ] || [ "$HTTPS_LISTENER" = "None" ]; then
HTTPS_LISTENER=$(aws elbv2 create-listener \
--load-balancer-arn "$ALB_ARN" \
--protocol HTTPS --port 443 \
--certificates "CertificateArn=${CERT_ARN}" \
--default-actions "Type=forward,TargetGroupArn=${FRONTEND_TG}" \
--query "Listeners[0].ListenerArn" --output text --region "$AWS_REGION")
fi
# Route /api/* to backend (priority 10)
RULE_EXISTS=$(aws elbv2 describe-rules --listener-arn "$HTTPS_LISTENER" --region "$AWS_REGION" \
--query "Rules[?Conditions[?Values[?contains(@,'/api/*')]]].RuleArn | [0]" --output text 2>/dev/null || echo "")
if [ -z "$RULE_EXISTS" ] || [ "$RULE_EXISTS" = "None" ]; then
aws elbv2 create-rule \
--listener-arn "$HTTPS_LISTENER" \
--priority 10 \
--conditions 'Field=path-pattern,Values=[/api/*]' \
--actions "Type=forward,TargetGroupArn=${BACKEND_TG}" \
--region "$AWS_REGION" --output none
fi
# ── 14. ECS Task Definitions ──────────────────────────────────────────────────
log "Registering ECS task definitions..."
aws ecs register-task-definition \
--family "${APP}-backend" \
--network-mode awsvpc \
--requires-compatibilities FARGATE \
--cpu "256" --memory "512" \
--execution-role-arn "$EXEC_ROLE_ARN" \
--container-definitions "[
{
\"name\": \"backend\",
\"image\": \"${ECR_BACKEND}:latest\",
\"portMappings\": [{\"containerPort\": 5000, \"protocol\": \"tcp\"}],
\"environment\": [{\"name\": \"DATABASE_URL\", \"value\": \"${DATABASE_URL}\"}],
\"logConfiguration\": {
\"logDriver\": \"awslogs\",
\"options\": {
\"awslogs-group\": \"/ecs/${APP}/backend\",
\"awslogs-region\": \"${AWS_REGION}\",
\"awslogs-stream-prefix\": \"backend\"
}
},
\"healthCheck\": {
\"command\": [\"CMD-SHELL\", \"curl -f http://localhost:5000/health || exit 1\"],
\"interval\": 30, \"timeout\": 5, \"retries\": 3, \"startPeriod\": 10
}
}
]" \
--region "$AWS_REGION" --output none
aws ecs register-task-definition \
--family "${APP}-frontend" \
--network-mode awsvpc \
--requires-compatibilities FARGATE \
--cpu "256" --memory "512" \
--execution-role-arn "$EXEC_ROLE_ARN" \
--container-definitions "[
{
\"name\": \"frontend\",
\"image\": \"${ECR_FRONTEND}:latest\",
\"portMappings\": [{\"containerPort\": 80, \"protocol\": \"tcp\"}],
\"logConfiguration\": {
\"logDriver\": \"awslogs\",
\"options\": {
\"awslogs-group\": \"/ecs/${APP}/frontend\",
\"awslogs-region\": \"${AWS_REGION}\",
\"awslogs-stream-prefix\": \"frontend\"
}
}
}
]" \
--region "$AWS_REGION" --output none
# ── 15. ECS Services ──────────────────────────────────────────────────────────
log "Creating ECS services..."
SUBNETS_JSON="[\"${SUBNET1}\",\"${SUBNET2}\"]"
ECS_SG_JSON="[\"${ECS_SG}\"]"
svc_exists() {
aws ecs describe-services --cluster "$CLUSTER" --services "$1" --region "$AWS_REGION" \
--query "services[?status=='ACTIVE'].serviceName" --output text 2>/dev/null | grep -q "$1"
}
svc_exists "${APP}-backend" || \
aws ecs create-service \
--cluster "$CLUSTER" \
--service-name "${APP}-backend" \
--task-definition "${APP}-backend" \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=${SUBNETS_JSON},securityGroups=${ECS_SG_JSON},assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=${BACKEND_TG},containerName=backend,containerPort=5000" \
--health-check-grace-period-seconds 60 \
--region "$AWS_REGION" --output none
svc_exists "${APP}-frontend" || \
aws ecs create-service \
--cluster "$CLUSTER" \
--service-name "${APP}-frontend" \
--task-definition "${APP}-frontend" \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=${SUBNETS_JSON},securityGroups=${ECS_SG_JSON},assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=${FRONTEND_TG},containerName=frontend,containerPort=80" \
--health-check-grace-period-seconds 60 \
--region "$AWS_REGION" --output none
# ── 16. Wait for services to stabilize ───────────────────────────────────────
log "Waiting for ECS services to stabilize..."
aws ecs wait services-stable \
--cluster "$CLUSTER" \
--services "${APP}-backend" "${APP}-frontend" \
--region "$AWS_REGION"
# ── 17. Initialize database ───────────────────────────────────────────────────
log "Initializing database schema..."
# Run a one-shot ECS task that executes init.sql via the backend image
INIT_SQL=$(cat db/init.sql | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
aws ecs run-task \
--cluster "$CLUSTER" \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[\"${SUBNET1}\"],securityGroups=[\"${ECS_SG}\"],assignPublicIp=ENABLED}" \
--task-definition "${APP}-backend" \
--overrides "{
\"containerOverrides\": [{
\"name\": \"backend\",
\"command\": [\"python\", \"-c\", \"import psycopg2,os; conn=psycopg2.connect(os.environ['DATABASE_URL']); cur=conn.cursor(); cur.execute(${INIT_SQL}); conn.commit(); print('DB initialized')\"]
}]
}" \
--region "$AWS_REGION" --output none
echo ""
echo "════════════════════════════════════════════════════════════"
echo " ✅ Deployment complete!"
echo " App URL : https://${DOMAIN_NAME}"
echo " ALB DNS : ${ALB_DNS}"
echo ""
echo " View logs:"
echo " aws logs tail /ecs/${APP}/frontend --follow --region ${AWS_REGION}"
echo " aws logs tail /ecs/${APP}/backend --follow --region ${AWS_REGION}"
echo "════════════════════════════════════════════════════════════"

130
sk1/remove-app.sh Normal file
View File

@ -0,0 +1,130 @@
#!/usr/bin/env bash
# remove-app.sh — Tear down all AWS resources for Notes App
# Usage: source .env && ./remove-app.sh
set -euo pipefail
: "${AWS_REGION:?Set AWS_REGION}"
: "${AWS_ACCOUNT_ID:?Set AWS_ACCOUNT_ID}"
APP="notes-app"
CLUSTER="${APP}-cluster"
log() { echo "$*"; }
# ── 1. Scale down ECS services ────────────────────────────────────────────────
log "Scaling down ECS services..."
for SVC in "${APP}-backend" "${APP}-frontend"; do
aws ecs update-service --cluster "$CLUSTER" --service "$SVC" \
--desired-count 0 --region "$AWS_REGION" --output none 2>/dev/null || true
done
log "Waiting for tasks to stop..."
aws ecs wait services-stable --cluster "$CLUSTER" \
--services "${APP}-backend" "${APP}-frontend" --region "$AWS_REGION" 2>/dev/null || true
# ── 2. Delete ECS services ────────────────────────────────────────────────────
log "Deleting ECS services..."
for SVC in "${APP}-backend" "${APP}-frontend"; do
aws ecs delete-service --cluster "$CLUSTER" --service "$SVC" \
--force --region "$AWS_REGION" --output none 2>/dev/null || true
done
# ── 3. Deregister task definitions ───────────────────────────────────────────
log "Deregistering task definitions..."
for FAMILY in "${APP}-backend" "${APP}-frontend"; do
ARNS=$(aws ecs list-task-definitions --family-prefix "$FAMILY" \
--query "taskDefinitionArns" --output text --region "$AWS_REGION" 2>/dev/null || true)
for ARN in $ARNS; do
aws ecs deregister-task-definition --task-definition "$ARN" \
--region "$AWS_REGION" --output none 2>/dev/null || true
done
done
# ── 4. Delete ECS cluster ─────────────────────────────────────────────────────
log "Deleting ECS cluster..."
aws ecs delete-cluster --cluster "$CLUSTER" --region "$AWS_REGION" --output none 2>/dev/null || true
# ── 5. Delete ALB, listeners, target groups ───────────────────────────────────
log "Deleting ALB..."
ALB_ARN=$(aws elbv2 describe-load-balancers --names "${APP}-alb" \
--query "LoadBalancers[0].LoadBalancerArn" --output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -n "$ALB_ARN" ] && [ "$ALB_ARN" != "None" ]; then
# Delete listeners first
LISTENER_ARNS=$(aws elbv2 describe-listeners --load-balancer-arn "$ALB_ARN" \
--query "Listeners[*].ListenerArn" --output text --region "$AWS_REGION" 2>/dev/null || true)
for L in $LISTENER_ARNS; do
aws elbv2 delete-listener --listener-arn "$L" --region "$AWS_REGION" --output none 2>/dev/null || true
done
aws elbv2 delete-load-balancer --load-balancer-arn "$ALB_ARN" \
--region "$AWS_REGION" --output none 2>/dev/null || true
log "Waiting for ALB to be deleted..."
aws elbv2 wait load-balancers-deleted --load-balancer-arns "$ALB_ARN" \
--region "$AWS_REGION" 2>/dev/null || true
fi
log "Deleting target groups..."
for TG_NAME in "${APP}-frontend-tg" "${APP}-backend-tg"; do
TG_ARN=$(aws elbv2 describe-target-groups --names "$TG_NAME" \
--query "TargetGroups[0].TargetGroupArn" --output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -n "$TG_ARN" ] && [ "$TG_ARN" != "None" ]; then
aws elbv2 delete-target-group --target-group-arn "$TG_ARN" \
--region "$AWS_REGION" --output none 2>/dev/null || true
fi
done
# ── 6. Delete RDS instance ────────────────────────────────────────────────────
log "Deleting RDS instance (skip final snapshot)..."
aws rds delete-db-instance \
--db-instance-identifier "${APP}-db" \
--skip-final-snapshot \
--region "$AWS_REGION" --output none 2>/dev/null || true
log "Waiting for RDS to be deleted (this takes ~5 min)..."
aws rds wait db-instance-deleted \
--db-instance-identifier "${APP}-db" --region "$AWS_REGION" 2>/dev/null || true
# ── 7. Delete ECR repositories ────────────────────────────────────────────────
log "Deleting ECR repositories..."
for REPO in "${APP}-backend" "${APP}-frontend"; do
aws ecr delete-repository --repository-name "$REPO" \
--force --region "$AWS_REGION" --output none 2>/dev/null || true
done
# ── 8. Delete security groups ─────────────────────────────────────────────────
log "Deleting security groups..."
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" \
--query "Vpcs[0].VpcId" --output text --region "$AWS_REGION")
for SG_NAME in "${APP}-ecs-sg" "${APP}-rds-sg" "${APP}-alb-sg"; do
SG_ID=$(aws ec2 describe-security-groups \
--filters "Name=group-name,Values=${SG_NAME}" "Name=vpc-id,Values=${VPC_ID}" \
--query "SecurityGroups[0].GroupId" --output text --region "$AWS_REGION" 2>/dev/null || echo "")
if [ -n "$SG_ID" ] && [ "$SG_ID" != "None" ]; then
aws ec2 delete-security-group --group-id "$SG_ID" \
--region "$AWS_REGION" --output none 2>/dev/null || true
fi
done
# ── 9. Delete Secrets Manager secret ─────────────────────────────────────────
log "Deleting secret..."
aws secretsmanager delete-secret \
--secret-id "${APP}/db-password" \
--force-delete-without-recovery \
--region "$AWS_REGION" --output none 2>/dev/null || true
# ── 10. Delete CloudWatch log groups ─────────────────────────────────────────
log "Deleting CloudWatch log groups..."
aws logs delete-log-group --log-group-name "/ecs/${APP}/backend" --region "$AWS_REGION" 2>/dev/null || true
aws logs delete-log-group --log-group-name "/ecs/${APP}/frontend" --region "$AWS_REGION" 2>/dev/null || true
# ── 11. Detach and delete IAM role ────────────────────────────────────────────
log "Cleaning up IAM role..."
aws iam detach-role-policy \
--role-name "${APP}-exec-role" \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy 2>/dev/null || true
aws iam delete-role --role-name "${APP}-exec-role" 2>/dev/null || true
echo ""
echo "════════════════════════════════════════════════════════════"
echo " ✅ All resources removed."
echo " Note: ACM certificate was NOT deleted (manual step)."
echo " To delete: aws acm delete-certificate --certificate-arn <ARN>"
echo "════════════════════════════════════════════════════════════"

2
sk1/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
flask==3.0.3
psycopg2-binary==2.9.9