Upload files to "sk1"
This commit is contained in:
parent
ad73eeb809
commit
0aca015ca5
4
sk1/Dockerfile
Normal file
4
sk1/Dockerfile
Normal 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
235
sk1/README.md
Normal 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
41
sk1/app.py
Normal 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
22
sk1/backup.sh
Normal 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
149
sk1/index.html
Normal 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
loadNotes();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
9
sk1/init.sql
Normal file
9
sk1/init.sql
Normal 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
12
sk1/nginx.conf
Normal 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
401
sk1/prepare-app.sh
Normal 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
130
sk1/remove-app.sh
Normal 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
2
sk1/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
flask==3.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
Loading…
Reference in New Issue
Block a user