From 0aca015ca5db1685ad9a1189d358c2fffada1b7d Mon Sep 17 00:00:00 2001 From: Pradeep Dileepkumar Date: Wed, 20 May 2026 07:31:24 +0000 Subject: [PATCH] Upload files to "sk1" --- sk1/Dockerfile | 4 + sk1/README.md | 235 +++++++++++++++++++++++++ sk1/app.py | 41 +++++ sk1/backup.sh | 22 +++ sk1/index.html | 149 ++++++++++++++++ sk1/init.sql | 9 + sk1/nginx.conf | 12 ++ sk1/prepare-app.sh | 401 +++++++++++++++++++++++++++++++++++++++++++ sk1/remove-app.sh | 130 ++++++++++++++ sk1/requirements.txt | 2 + 10 files changed, 1005 insertions(+) create mode 100644 sk1/Dockerfile create mode 100644 sk1/README.md create mode 100644 sk1/app.py create mode 100644 sk1/backup.sh create mode 100644 sk1/index.html create mode 100644 sk1/init.sql create mode 100644 sk1/nginx.conf create mode 100644 sk1/prepare-app.sh create mode 100644 sk1/remove-app.sh create mode 100644 sk1/requirements.txt diff --git a/sk1/Dockerfile b/sk1/Dockerfile new file mode 100644 index 0000000..32994bb --- /dev/null +++ b/sk1/Dockerfile @@ -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 diff --git a/sk1/README.md b/sk1/README.md new file mode 100644 index 0000000..1107965 --- /dev/null +++ b/sk1/README.md @@ -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 --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 \ + --region $AWS_REGION +``` + +**Export data as SQL dump** (requires `psql` and RDS publicly accessible or VPN): +```bash +PGPASSWORD=$DB_PASSWORD pg_dump \ + -h -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. diff --git a/sk1/app.py b/sk1/app.py new file mode 100644 index 0000000..e13fcbb --- /dev/null +++ b/sk1/app.py @@ -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/", 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) diff --git a/sk1/backup.sh b/sk1/backup.sh new file mode 100644 index 0000000..83866f2 --- /dev/null +++ b/sk1/backup.sh @@ -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}" diff --git a/sk1/index.html b/sk1/index.html new file mode 100644 index 0000000..f86d085 --- /dev/null +++ b/sk1/index.html @@ -0,0 +1,149 @@ + + + + + + Notes App + + + +
+

✦ Notes App

+

Capture your thoughts, instantly.

+
+
+ + +

+
+
+ + + diff --git a/sk1/init.sql b/sk1/init.sql new file mode 100644 index 0000000..c1a9fbb --- /dev/null +++ b/sk1/init.sql @@ -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.'); diff --git a/sk1/nginx.conf b/sk1/nginx.conf new file mode 100644 index 0000000..9ed038f --- /dev/null +++ b/sk1/nginx.conf @@ -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; +} diff --git a/sk1/prepare-app.sh b/sk1/prepare-app.sh new file mode 100644 index 0000000..ba2f1f9 --- /dev/null +++ b/sk1/prepare-app.sh @@ -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 "════════════════════════════════════════════════════════════" diff --git a/sk1/remove-app.sh b/sk1/remove-app.sh new file mode 100644 index 0000000..16f5c71 --- /dev/null +++ b/sk1/remove-app.sh @@ -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 " +echo "════════════════════════════════════════════════════════════" diff --git a/sk1/requirements.txt b/sk1/requirements.txt new file mode 100644 index 0000000..73c0be5 --- /dev/null +++ b/sk1/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.3 +psycopg2-binary==2.9.9