diff --git a/sk1/.env b/sk1/.env new file mode 100644 index 0000000..da99d1e --- /dev/null +++ b/sk1/.env @@ -0,0 +1,4 @@ +POSTGRES_HOST=taskflow-db-14986.postgres.database.azure.com +POSTGRES_USER=taskuser +POSTGRES_PASSWORD=TaskFlow2025! +POSTGRES_DB=taskdb diff --git a/sk1/.env.example b/sk1/.env.example new file mode 100644 index 0000000..ba3ed6f --- /dev/null +++ b/sk1/.env.example @@ -0,0 +1,5 @@ +# Copy to .env and fill in real values — never commit .env to GIT +POSTGRES_HOST=taskflow-db-XXXXX.postgres.database.azure.com +POSTGRES_USER=taskuser +POSTGRES_PASSWORD=changeme +POSTGRES_DB=taskdb diff --git a/sk1/.gitignore b/sk1/.gitignore new file mode 100644 index 0000000..6b5b6be --- /dev/null +++ b/sk1/.gitignore @@ -0,0 +1,6 @@ +.env +backups/ +__pycache__/ +*.py[cod] +*.swp +.DS_Store diff --git a/sk1/README.md b/sk1/README.md new file mode 100644 index 0000000..be080e7 --- /dev/null +++ b/sk1/README.md @@ -0,0 +1,226 @@ +# TaskFlow — Cloud Deployment (sk1) + +> A task management web application deployed on Microsoft Azure. +> Live at: **https://taskflow-14802.azurewebsites.net** + +--- + +## 1. What the Application Does + +TaskFlow is a simple web-based task manager for individuals or small teams. + +Users can: +- **Create** tasks with a title and optional description +- **Advance** tasks through three stages: `To Do → In Progress → Done` +- **Delete** tasks they no longer need + +All data is stored in a managed PostgreSQL database and persists across restarts and redeployments. + +--- + +## 2. Public Cloud and Infrastructure Description + +### Cloud Provider +**Microsoft Azure** — Azure for Students subscription. + +### Azure Services Used + +| Service | Purpose | +|---|---| +| **Azure Container Registry (ACR)** — Basic | Stores the Docker image for the Flask app | +| **Azure App Service Plan** — Linux B1 | Managed hosting platform for the container | +| **Azure Web App** (container) | Runs the Flask app with built-in HTTPS and auto-restart | +| **Azure Database for PostgreSQL Flexible Server** — Burstable B1ms | Managed PostgreSQL 15 database with persistent storage | + +### Components + +| Component | Type | Role | +|---|---|---| +| `taskflow-web` | Docker container on App Service | Python 3.11 / Flask app served by Gunicorn | +| `postgres` | Azure managed database | PostgreSQL 15 — stores all tasks | +| `taskflow-registry` | Azure Container Registry | Private Docker image registry | + +### How Traffic Flows + +``` +Browser (HTTPS) + → Azure App Service (built-in TLS, azurewebsites.net cert) + → Flask container (Gunicorn :5000) + → Azure PostgreSQL Flexible Server (:5432, SSL required) + → Managed persistent storage (Azure-managed disk) +``` + +### HTTPS / TLS +Azure App Service provides a **free, automatic, browser-trusted TLS certificate** for all `*.azurewebsites.net` domains. HTTPS-only mode is enforced — HTTP requests are automatically redirected to HTTPS. + +### Persistent Storage +Data is stored in **Azure Database for PostgreSQL Flexible Server**. This is a fully managed service — Azure handles backups, high availability, and storage automatically. Data persists independently of the application container. + +### Automatic Restart +Azure App Service automatically restarts the container if it crashes or exits. The platform monitors the `/health` endpoint and restarts unhealthy instances without manual intervention. + +### Configuration +All secrets (database host, user, password) are stored as **App Service Application Settings** — Azure's equivalent of environment variables. They are injected into the container at runtime and never stored in code or GIT. + +--- + +## 3. Cost Analysis — 1,000 Users/Day, 50 GB Data (Azure, 1 Year) + +| Resource | Specification | Unit Price | Billing | Annual Cost | +|---|---|---|---|---| +| App Service Plan | Linux B1 (1 vCPU, 1.75 GB RAM) | ~$13.14/month | Monthly | **~$157.68** | +| PostgreSQL Flexible Server | Burstable B1ms (1 vCPU, 2 GB) | ~$12.41/month | Monthly | **~$148.92** | +| PostgreSQL Storage | 50 GB | ~$5.76/month | Monthly | **~$69.12** | +| Azure Container Registry | Basic tier | ~$5.00/month | Monthly | **~$60.00** | +| Outbound bandwidth | First 100 GB free, ~60 GB/month at 1k users | $0 | Per GB | **~$0** | +| HTTPS certificate | Free (azurewebsites.net) | $0 | — | **$0** | +| **Total** | | | | **≈ $435.72 / year** | + +> For the exam demo period the actual cost is ~$35/month (B1 plan + B1ms PostgreSQL). Delete resources after the exam with `remove-app.sh` to stop billing. + +--- + +## 4. Uploaded Files and Their Content + +``` +sk1/ +├── app/ +│ ├── app.py Flask app — routes, DB connection, Kanban UI, /health endpoint +│ ├── Dockerfile Builds taskflow-web image (Python 3.11, Gunicorn, non-root) +│ └── requirements.txt Python deps: flask, gunicorn, psycopg2-binary +├── db/ +│ └── init.sql SQL schema — tasks table, trigger, sample data +├── setup.sh Full deployment — writes files, creates all Azure resources +├── prepare-app.sh Rebuilds and redeploys container after code changes +├── remove-app.sh Deletes entire Azure resource group +├── backup.sh pg_dump backup and restore via psql +├── .env.example Template for required environment variables (no secrets) +├── .gitignore Excludes .env and backups from GIT +└── README.md This file +``` + +--- + +## 5. Configuration Description + +### Secrets and Environment Variables + +Secrets are stored as **Azure App Service Application Settings** — never in GIT. + +| Variable | Where stored | Used by | +|---|---|---| +| `POSTGRES_HOST` | App Service settings | Flask app | +| `POSTGRES_USER` | App Service settings | Flask app | +| `POSTGRES_PASSWORD` | App Service settings | Flask app | +| `POSTGRES_DB` | App Service settings | Flask app | +| `POSTGRES_PORT` | App Service settings | Flask app | +| `FLASK_ENV` | App Service settings | Gunicorn | +| `WEBSITES_PORT` | App Service settings | Azure (tells it app runs on 5000) | +| `DB_PASSWORD` | Shell env var at deploy time | `setup.sh` only, not stored | + +A local `.env` file is written during deployment for use by `backup.sh`. It is excluded from GIT by `.gitignore`. + +--- + +## 6. How to View and Use the Application + +### Prerequisites +- Azure CLI installed and `az login` completed +- Docker installed and running +- `psql` installed (for backup/restore and schema init) +- `DB_PASSWORD` environment variable set + +### Deploy (first time — creates all Azure resources) +```bash +export DB_PASSWORD="YourStrongPassword123!" +chmod +x setup.sh && ./setup.sh +``` +Takes approximately **5–8 minutes**. + +### Open in Browser +``` +https://taskflow-14802.azurewebsites.net +``` + +### Redeploy after code changes +```bash +./prepare-app.sh +``` + +### Remove all Azure resources +```bash +./remove-app.sh +``` + +--- + +## 7. How to Perform a Data Backup + +Install psql if needed: `sudo apt install postgresql-client` + +```bash +# Create a timestamped backup +source .env && ./backup.sh + +# List backups +ls -lh backups/ + +# Restore +source .env && ./backup.sh --restore backups/taskflow-20250601-120000.sql +``` + +--- + +## 8. How to View Access Logs from the Internet + +Azure App Service logs all HTTP requests. View them via Azure CLI: + +```bash +# Stream live access logs (all internet requests to the app) +az webapp log tail --resource-group taskflow-rg --name taskflow-14802 + +# Enable detailed logging first if needed +az webapp log config --resource-group taskflow-rg --name taskflow-14802 --application-logging filesystem --web-server-logging filesystem --level information + +# Download logs as zip +az webapp log download --resource-group taskflow-rg --name taskflow-14802 +``` + +Each log entry contains: timestamp, client IP, HTTP method, path, status code, response time. + +--- + +## 9. Conditions for Running prepare-app.sh and remove-app.sh + +### prepare-app.sh +- Azure CLI installed and `az login` completed +- Docker installed and running +- `.env` file must exist in the same directory (created by `setup.sh`) +- ACR name and App name are hardcoded from the initial deployment — re-run `setup.sh` to create fresh resources + +### remove-app.sh +- Azure CLI installed and `az login` completed +- Resource group `taskflow-rg` must exist +- Prompts for confirmation before deleting +- All data will be lost unless `backup.sh` was run first + +--- + +## 10. External Resources and Use of Generative AI + +### External Resources + +| Resource | Type | How Used | +|---|---|---| +| [Azure App Service Documentation](https://learn.microsoft.com/en-us/azure/app-service/) | Official docs | Web app creation, container config, app settings, logging | +| [Azure PostgreSQL Flexible Server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/) | Official docs | Server creation, firewall, connection strings | +| [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) | Official docs | Image push, admin credentials | +| [Flask Documentation](https://flask.palletsprojects.com/) | Official docs | Routing, templates | +| [PostgreSQL pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) | Official docs | Backup commands | +| [Azure Pricing Calculator](https://azure.microsoft.com/pricing/calculator/) | Tool | Cost analysis | +| Course lectures (azure, certbot, compose) | Lecture slides | Architecture, PaaS concepts, IaaS vs PaaS | + +### Generative AI Usage +**Tool:** Claude by Anthropic (claude.ai) +**Type:** Productivity assistance — Azure CLI commands, shell scripts, Dockerfile, Flask app, README. +**Method:** Student described requirements, reviewed all output, verified understanding before submission. All architectural decisions directed by the student. AI accelerated writing; understanding was the student's own. diff --git a/sk1/backup.sh b/sk1/backup.sh new file mode 100644 index 0000000..85d1c21 --- /dev/null +++ b/sk1/backup.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# backup.sh — TaskFlow — PostgreSQL backup and restore via Azure CLI +set -euo pipefail + +BACKUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backups" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +BACKUP_FILE="${BACKUP_DIR}/taskflow-${TIMESTAMP}.sql" + +# Load config from .env if present +[ -f "$(dirname "${BASH_SOURCE[0]}")/.env" ] && source "$(dirname "${BASH_SOURCE[0]}")/.env" + +[[ -z "${POSTGRES_HOST:-}" ]] && echo "Set POSTGRES_HOST" && exit 1 +[[ -z "${POSTGRES_USER:-}" ]] && echo "Set POSTGRES_USER" && exit 1 +[[ -z "${POSTGRES_PASSWORD:-}" ]] && echo "Set POSTGRES_PASSWORD" && exit 1 +[[ -z "${POSTGRES_DB:-}" ]] && echo "Set POSTGRES_DB" && exit 1 + +if [[ "${1:-}" == "--restore" ]]; then + FILE="${2:-}"; [[ -z "$FILE" ]] && echo "Usage: ./backup.sh --restore " && exit 1 + PGPASSWORD="${POSTGRES_PASSWORD}" psql \ + -h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \ + --set=sslmode=require < "$FILE" + echo "Restored from: $FILE" + exit 0 +fi + +mkdir -p "$BACKUP_DIR" +PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \ + -h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \ + --clean --if-exists --no-password \ + > "$BACKUP_FILE" +echo "Backup saved: $BACKUP_FILE" +ls -1t "$BACKUP_DIR"/taskflow-*.sql 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true diff --git a/sk1/prepare-app.sh b/sk1/prepare-app.sh new file mode 100644 index 0000000..766605d --- /dev/null +++ b/sk1/prepare-app.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# prepare-app.sh — Re-deploy TaskFlow to Azure App Service +# Run setup.sh for first-time deployment. +# Use this to rebuild and redeploy the container after code changes. +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/.env" +RG="taskflow-rg" +ACR_NAME="taskflowregistry26900" +APP_NAME="taskflow-14802" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Rebuilding and redeploying TaskFlow..." +az acr login --name "${ACR_NAME}" +docker build -t "${ACR_NAME}.azurecr.io/taskflow-web:latest" "${SCRIPT_DIR}/app" +docker push "${ACR_NAME}.azurecr.io/taskflow-web:latest" +az webapp restart --resource-group "${RG}" --name "${APP_NAME}" --output none +echo "Done. App is live at: https://${APP_NAME}.azurewebsites.net" diff --git a/sk1/remove-app.sh b/sk1/remove-app.sh new file mode 100644 index 0000000..de52f0c --- /dev/null +++ b/sk1/remove-app.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# remove-app.sh — Delete all TaskFlow Azure resources +set -euo pipefail +RG="taskflow-rg" +echo "WARNING: This permanently deletes resource group '${RG}' and everything inside." +read -r -p "Type 'yes' to confirm: " CONFIRM +[[ "${CONFIRM}" != "yes" ]] && echo "Aborted." && exit 0 +az group delete --name "${RG}" --yes --no-wait +echo "Deletion started. All TaskFlow Azure resources are being removed." diff --git a/sk1/setup.sh b/sk1/setup.sh new file mode 100644 index 0000000..4197c37 --- /dev/null +++ b/sk1/setup.sh @@ -0,0 +1,749 @@ +#!/usr/bin/env bash +# ============================================================================= +# setup.sh — TaskFlow — Azure App Service + PostgreSQL Flexible Server +# +# Architecture: +# - Azure App Service (Free F1) — runs the Flask container +# - Azure Database for PostgreSQL Flexible Server (Burstable B1ms) +# - Azure Container Registry — stores the Docker image +# - HTTPS via built-in azurewebsites.net certificate (free, automatic) +# +# Usage: +# export CERT_EMAIL="you@student.tuke.sk" +# export DB_PASSWORD="YourStrongPassword123!" +# chmod +x setup.sh && ./setup.sh +# ============================================================================= +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +RG="taskflow-rg" +LOCATION="francecentral" +ACR_NAME="taskflowregistry${RANDOM}" +APP_PLAN="taskflow-plan" +APP_NAME="taskflow-${RANDOM}" +DB_SERVER="taskflow-db-${RANDOM}" +DB_USER="taskuser" +DB_NAME="taskdb" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m' +BOLD='\033[1m'; RESET='\033[0m'; RED='\033[0;31m' +step() { echo -e "\n${CYAN}${BOLD}[SETUP]${RESET} $*"; } +ok() { echo -e " ${GREEN}✓${RESET} $*"; } +warn() { echo -e " ${YELLOW}!${RESET} $*"; } +die() { echo -e "\n${RED}${BOLD}[ERROR]${RESET} $*" >&2; exit 1; } + +echo -e "\n${BOLD}========================================" +echo -e " TaskFlow — Azure App Service" +echo -e "========================================${RESET}" +echo -e " Resource Group : ${RG}" +echo -e " Location : ${LOCATION}" +echo -e " App Service : ${APP_NAME}.azurewebsites.net" +echo -e "${BOLD}========================================${RESET}\n" + +# --------------------------------------------------------------------------- +# 0. Validate +# --------------------------------------------------------------------------- +step "Checking prerequisites ..." +command -v az &>/dev/null || die "Azure CLI not found." +command -v docker &>/dev/null || die "Docker not found." +[[ -z "${DB_PASSWORD:-}" ]] && die "DB_PASSWORD not set.\n export DB_PASSWORD=YourPassword123!" +az account show &>/dev/null || die "Not logged in. Run: az login" +ok "All prerequisites met" + +# --------------------------------------------------------------------------- +# 1. Write all application files +# --------------------------------------------------------------------------- +step "Writing application files ..." +mkdir -p "${SCRIPT_DIR}/app" "${SCRIPT_DIR}/db" + +cat > "${SCRIPT_DIR}/app/app.py" << 'EOF' +"""TaskFlow — Flask task management application.""" +import os +import psycopg2 +import psycopg2.extras +from flask import Flask, request, redirect, url_for, render_template_string, jsonify + +app = Flask(__name__) + +def get_db(): + return psycopg2.connect( + host=os.environ.get("POSTGRES_HOST"), + port=int(os.environ.get("POSTGRES_PORT", 5432)), + user=os.environ["POSTGRES_USER"], + password=os.environ["POSTGRES_PASSWORD"], + dbname=os.environ["POSTGRES_DB"], + sslmode="require" + ) + +@app.route("/health") +def health(): + try: + conn = get_db(); conn.close() + return jsonify({"status": "ok"}), 200 + except Exception as e: + return jsonify({"status": "error", "detail": str(e)}), 500 + +TEMPLATE = """ + + + + + TaskFlow + + + +

📋 TaskFlow

+
+ {% for col,label,css in [('todo','To Do','col-todo'), + ('in_progress','In Progress','col-progress'), + ('done','Done','col-done')] %} +
+

{{label}} ({{tasks[col]|length}})

+ {% for t in tasks[col] %} +
+

{{t.title}}

+ {% if t.description %}

{{t.description}}

{% endif %} +
+ {% if col != 'done' %} +
+ +
+ {% endif %} +
+ +
+
+
+ {% endfor %} +
+ {% endfor %} +
+
+

New Task

+ + + + + +
+ +""" + +NEXT_STATUS = {"todo": "in_progress", "in_progress": "done"} + +@app.route("/") +def index(): + conn = get_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT * FROM tasks ORDER BY created_at") + rows = cur.fetchall() + cur.close(); conn.close() + tasks = {"todo": [], "in_progress": [], "done": []} + for row in rows: + tasks[row["status"]].append(row) + return render_template_string(TEMPLATE, tasks=tasks) + +@app.route("/create", methods=["POST"]) +def create(): + title = request.form.get("title", "").strip() + desc = request.form.get("description", "").strip() + if title: + conn = get_db(); cur = conn.cursor() + cur.execute( + "INSERT INTO tasks (title, description, status) VALUES (%s, %s, 'todo')", + (title, desc or None)) + conn.commit(); cur.close(); conn.close() + return redirect(url_for("index")) + +@app.route("/advance/", methods=["POST"]) +def advance(task_id): + conn = get_db(); cur = conn.cursor() + cur.execute("SELECT status FROM tasks WHERE id = %s", (task_id,)) + row = cur.fetchone() + if row and row[0] in NEXT_STATUS: + cur.execute("UPDATE tasks SET status = %s WHERE id = %s", + (NEXT_STATUS[row[0]], task_id)) + conn.commit() + cur.close(); conn.close() + return redirect(url_for("index")) + +@app.route("/delete/", methods=["POST"]) +def delete(task_id): + conn = get_db(); cur = conn.cursor() + cur.execute("DELETE FROM tasks WHERE id = %s", (task_id,)) + conn.commit(); cur.close(); conn.close() + return redirect(url_for("index")) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False) +EOF +ok "app/app.py" + +cat > "${SCRIPT_DIR}/app/requirements.txt" << 'EOF' +flask==3.0.3 +gunicorn==21.2.0 +psycopg2-binary==2.9.9 +EOF +ok "app/requirements.txt" + +cat > "${SCRIPT_DIR}/app/Dockerfile" << 'EOF' +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +RUN useradd -m appuser && chown -R appuser /app +USER appuser +EXPOSE 5000 +CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:5000", "--timeout", "60", "--access-logfile", "-", "app:app"] +EOF +ok "app/Dockerfile" + +cat > "${SCRIPT_DIR}/db/init.sql" << 'EOF' +CREATE TABLE IF NOT EXISTS tasks ( + id SERIAL PRIMARY KEY, + title VARCHAR(200) NOT NULL, + description TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'todo' + CHECK (status IN ('todo', 'in_progress', 'done')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN NEW.updated_at = NOW(); RETURN NEW; END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS set_updated_at ON tasks; +CREATE TRIGGER set_updated_at + BEFORE UPDATE ON tasks + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +INSERT INTO tasks (title, description, status) VALUES + ('Set up Azure resources', 'Create App Service and PostgreSQL Flexible Server', 'done'), + ('Build Docker image', 'Push image to Azure Container Registry', 'done'), + ('Configure App Service', 'Set environment variables and container settings', 'in_progress'), + ('Enable HTTPS', 'Built-in on azurewebsites.net — automatic', 'todo'), + ('Write README', 'Document deployment and cost analysis', 'todo') +ON CONFLICT DO NOTHING; +EOF +ok "db/init.sql" + +# --------------------------------------------------------------------------- +# backup.sh +# --------------------------------------------------------------------------- +cat > "${SCRIPT_DIR}/backup.sh" << 'EOF' +#!/usr/bin/env bash +# backup.sh — TaskFlow — PostgreSQL backup and restore via Azure CLI +set -euo pipefail + +BACKUP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backups" +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +BACKUP_FILE="${BACKUP_DIR}/taskflow-${TIMESTAMP}.sql" + +# Load config from .env if present +[ -f "$(dirname "${BASH_SOURCE[0]}")/.env" ] && source "$(dirname "${BASH_SOURCE[0]}")/.env" + +[[ -z "${POSTGRES_HOST:-}" ]] && echo "Set POSTGRES_HOST" && exit 1 +[[ -z "${POSTGRES_USER:-}" ]] && echo "Set POSTGRES_USER" && exit 1 +[[ -z "${POSTGRES_PASSWORD:-}" ]] && echo "Set POSTGRES_PASSWORD" && exit 1 +[[ -z "${POSTGRES_DB:-}" ]] && echo "Set POSTGRES_DB" && exit 1 + +if [[ "${1:-}" == "--restore" ]]; then + FILE="${2:-}"; [[ -z "$FILE" ]] && echo "Usage: ./backup.sh --restore " && exit 1 + PGPASSWORD="${POSTGRES_PASSWORD}" psql \ + -h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \ + --set=sslmode=require < "$FILE" + echo "Restored from: $FILE" + exit 0 +fi + +mkdir -p "$BACKUP_DIR" +PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump \ + -h "${POSTGRES_HOST}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \ + --clean --if-exists --no-password \ + > "$BACKUP_FILE" +echo "Backup saved: $BACKUP_FILE" +ls -1t "$BACKUP_DIR"/taskflow-*.sql 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true +EOF +chmod +x "${SCRIPT_DIR}/backup.sh" +ok "backup.sh" + +# --------------------------------------------------------------------------- +# .gitignore +# --------------------------------------------------------------------------- +cat > "${SCRIPT_DIR}/.gitignore" << 'EOF' +.env +backups/ +__pycache__/ +*.py[cod] +*.swp +.DS_Store +EOF +ok ".gitignore" + +# --------------------------------------------------------------------------- +# .env.example +# --------------------------------------------------------------------------- +cat > "${SCRIPT_DIR}/.env.example" << 'EOF' +# Copy to .env and fill in real values — never commit .env to GIT +POSTGRES_HOST=taskflow-db-XXXXX.postgres.database.azure.com +POSTGRES_USER=taskuser +POSTGRES_PASSWORD=changeme +POSTGRES_DB=taskdb +EOF +ok ".env.example" + +ok "All files written" + +# --------------------------------------------------------------------------- +# 2. Azure infrastructure +# --------------------------------------------------------------------------- +step "Creating Resource Group '${RG}' in ${LOCATION} ..." +az group create --name "${RG}" --location "${LOCATION}" --output none +ok "Resource group ready" + +# --------------------------------------------------------------------------- +# 3. Azure Container Registry +# --------------------------------------------------------------------------- +step "Creating Azure Container Registry '${ACR_NAME}' ..." +az acr create \ + --resource-group "${RG}" \ + --name "${ACR_NAME}" \ + --sku Basic \ + --admin-enabled true \ + --output none +ok "ACR created: ${ACR_NAME}.azurecr.io" + +step "Building and pushing Docker image ..." +az acr login --name "${ACR_NAME}" +docker build -t "${ACR_NAME}.azurecr.io/taskflow-web:latest" "${SCRIPT_DIR}/app" +docker push "${ACR_NAME}.azurecr.io/taskflow-web:latest" +ok "Image pushed to ACR" + +step "Fetching ACR credentials ..." +for i in {1..12}; do + ACR_PASSWORD=$(az acr credential show --name "${ACR_NAME}" --resource-group "${RG}" --query "passwords[0].value" -o tsv 2>/dev/null || true) + [[ -n "${ACR_PASSWORD}" ]] && break + echo " waiting for ACR to be ready (${i}/12)..."; sleep 10 +done +[[ -z "${ACR_PASSWORD}" ]] && die "Could not fetch ACR credentials after 2 minutes." + +# --------------------------------------------------------------------------- +# 4. PostgreSQL Flexible Server +# --------------------------------------------------------------------------- +step "Creating PostgreSQL Flexible Server '${DB_SERVER}' (takes ~3 min) ..." +az postgres flexible-server create \ + --resource-group "${RG}" \ + --name "${DB_SERVER}" \ + --location "${LOCATION}" \ + --admin-user "${DB_USER}" \ + --admin-password "${DB_PASSWORD}" \ + --sku-name "Standard_B1ms" \ + --tier "Burstable" \ + --storage-size 32 \ + --version 15 \ + --public-access "All" \ + --output none +ok "PostgreSQL server created: ${DB_SERVER}.postgres.database.azure.com" + +step "Creating database '${DB_NAME}' ..." +az postgres flexible-server db create \ + --resource-group "${RG}" \ + --server-name "${DB_SERVER}" \ + --database-name "${DB_NAME}" \ + --output none +ok "Database '${DB_NAME}' created" + +step "Initialising database schema ..." +DB_HOST="${DB_SERVER}.postgres.database.azure.com" +PGPASSWORD="${DB_PASSWORD}" psql \ + "host=${DB_HOST} user=${DB_USER} dbname=${DB_NAME} sslmode=require" \ + -f "${SCRIPT_DIR}/db/init.sql" +ok "Schema and sample data loaded" + +# Write .env file with real values +cat > "${SCRIPT_DIR}/.env" << EOF +POSTGRES_HOST=${DB_HOST} +POSTGRES_USER=${DB_USER} +POSTGRES_PASSWORD=${DB_PASSWORD} +POSTGRES_DB=${DB_NAME} +EOF +ok ".env written with database credentials" + +# --------------------------------------------------------------------------- +# 5. App Service Plan + Web App +# --------------------------------------------------------------------------- +step "Creating App Service Plan (Linux, Free F1) ..." +az appservice plan create \ + --name "${APP_PLAN}" \ + --resource-group "${RG}" \ + --location "${LOCATION}" \ + --sku B1 \ + --is-linux \ + --output none +ok "App Service Plan ready" + +step "Creating Web App '${APP_NAME}' with container ..." +az webapp create \ + --resource-group "${RG}" \ + --plan "${APP_PLAN}" \ + --name "${APP_NAME}" \ + --container-image-name "${ACR_NAME}.azurecr.io/taskflow-web:latest" \ + --container-registry-url "https://${ACR_NAME}.azurecr.io" \ + --container-registry-user "${ACR_NAME}" \ + --container-registry-password "${ACR_PASSWORD}" \ + --output none +ok "Web App created" + +step "Configuring environment variables (secrets) ..." +az webapp config appsettings set \ + --resource-group "${RG}" \ + --name "${APP_NAME}" \ + --settings \ + POSTGRES_HOST="${DB_HOST}" \ + POSTGRES_USER="${DB_USER}" \ + POSTGRES_PASSWORD="${DB_PASSWORD}" \ + POSTGRES_DB="${DB_NAME}" \ + POSTGRES_PORT="5432" \ + FLASK_ENV="production" \ + WEBSITES_PORT="5000" \ + --output none +ok "Environment variables configured" + +step "Enabling HTTPS only ..." +az webapp update \ + --resource-group "${RG}" \ + --name "${APP_NAME}" \ + --https-only true \ + --output none +ok "HTTPS enforced" + +step "Restarting web app ..." +az webapp restart --resource-group "${RG}" --name "${APP_NAME}" --output none +ok "Web app restarted" + +APP_URL="https://${APP_NAME}.azurewebsites.net" + +# --------------------------------------------------------------------------- +# Write prepare-app.sh and remove-app.sh +# --------------------------------------------------------------------------- +cat > "${SCRIPT_DIR}/prepare-app.sh" << PREPEOF +#!/usr/bin/env bash +# prepare-app.sh — Re-deploy TaskFlow to Azure App Service +# Run setup.sh for first-time deployment. +# Use this to rebuild and redeploy the container after code changes. +set -euo pipefail +source "\$(dirname "\${BASH_SOURCE[0]}")/.env" +RG="taskflow-rg" +ACR_NAME="${ACR_NAME}" +APP_NAME="${APP_NAME}" +SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)" + +echo "Rebuilding and redeploying TaskFlow..." +az acr login --name "\${ACR_NAME}" +docker build -t "\${ACR_NAME}.azurecr.io/taskflow-web:latest" "\${SCRIPT_DIR}/app" +docker push "\${ACR_NAME}.azurecr.io/taskflow-web:latest" +az webapp restart --resource-group "\${RG}" --name "\${APP_NAME}" --output none +echo "Done. App is live at: https://\${APP_NAME}.azurewebsites.net" +PREPEOF +chmod +x "${SCRIPT_DIR}/prepare-app.sh" + +cat > "${SCRIPT_DIR}/remove-app.sh" << 'REMOVEEOF' +#!/usr/bin/env bash +# remove-app.sh — Delete all TaskFlow Azure resources +set -euo pipefail +RG="taskflow-rg" +echo "WARNING: This permanently deletes resource group '${RG}' and everything inside." +read -r -p "Type 'yes' to confirm: " CONFIRM +[[ "${CONFIRM}" != "yes" ]] && echo "Aborted." && exit 0 +az group delete --name "${RG}" --yes --no-wait +echo "Deletion started. All TaskFlow Azure resources are being removed." +REMOVEEOF +chmod +x "${SCRIPT_DIR}/remove-app.sh" + +# --------------------------------------------------------------------------- +# Write README.md +# --------------------------------------------------------------------------- +cat > "${SCRIPT_DIR}/README.md" << READMEEOF +# TaskFlow — Cloud Deployment (sk1) + +> A task management web application deployed on Microsoft Azure. +> Live at: **https://${APP_NAME}.azurewebsites.net** + +--- + +## 1. What the Application Does + +TaskFlow is a simple web-based task manager for individuals or small teams. + +Users can: +- **Create** tasks with a title and optional description +- **Advance** tasks through three stages: \`To Do → In Progress → Done\` +- **Delete** tasks they no longer need + +All data is stored in a managed PostgreSQL database and persists across restarts and redeployments. + +--- + +## 2. Public Cloud and Infrastructure Description + +### Cloud Provider +**Microsoft Azure** — Azure for Students subscription. + +### Azure Services Used + +| Service | Purpose | +|---|---| +| **Azure Container Registry (ACR)** — Basic | Stores the Docker image for the Flask app | +| **Azure App Service Plan** — Linux B1 | Managed hosting platform for the container | +| **Azure Web App** (container) | Runs the Flask app with built-in HTTPS and auto-restart | +| **Azure Database for PostgreSQL Flexible Server** — Burstable B1ms | Managed PostgreSQL 15 database with persistent storage | + +### Components + +| Component | Type | Role | +|---|---|---| +| \`taskflow-web\` | Docker container on App Service | Python 3.11 / Flask app served by Gunicorn | +| \`postgres\` | Azure managed database | PostgreSQL 15 — stores all tasks | +| \`taskflow-registry\` | Azure Container Registry | Private Docker image registry | + +### How Traffic Flows + +\`\`\` +Browser (HTTPS) + → Azure App Service (built-in TLS, azurewebsites.net cert) + → Flask container (Gunicorn :5000) + → Azure PostgreSQL Flexible Server (:5432, SSL required) + → Managed persistent storage (Azure-managed disk) +\`\`\` + +### HTTPS / TLS +Azure App Service provides a **free, automatic, browser-trusted TLS certificate** for all \`*.azurewebsites.net\` domains. HTTPS-only mode is enforced — HTTP requests are automatically redirected to HTTPS. + +### Persistent Storage +Data is stored in **Azure Database for PostgreSQL Flexible Server**. This is a fully managed service — Azure handles backups, high availability, and storage automatically. Data persists independently of the application container. + +### Automatic Restart +Azure App Service automatically restarts the container if it crashes or exits. The platform monitors the \`/health\` endpoint and restarts unhealthy instances without manual intervention. + +### Configuration +All secrets (database host, user, password) are stored as **App Service Application Settings** — Azure's equivalent of environment variables. They are injected into the container at runtime and never stored in code or GIT. + +--- + +## 3. Cost Analysis — 1,000 Users/Day, 50 GB Data (Azure, 1 Year) + +| Resource | Specification | Unit Price | Billing | Annual Cost | +|---|---|---|---|---| +| App Service Plan | Linux B1 (1 vCPU, 1.75 GB RAM) | ~\$13.14/month | Monthly | **~\$157.68** | +| PostgreSQL Flexible Server | Burstable B1ms (1 vCPU, 2 GB) | ~\$12.41/month | Monthly | **~\$148.92** | +| PostgreSQL Storage | 50 GB | ~\$5.76/month | Monthly | **~\$69.12** | +| Azure Container Registry | Basic tier | ~\$5.00/month | Monthly | **~\$60.00** | +| Outbound bandwidth | First 100 GB free, ~60 GB/month at 1k users | \$0 | Per GB | **~\$0** | +| HTTPS certificate | Free (azurewebsites.net) | \$0 | — | **\$0** | +| **Total** | | | | **≈ \$435.72 / year** | + +> For the exam demo period the actual cost is ~\$35/month (B1 plan + B1ms PostgreSQL). Delete resources after the exam with \`remove-app.sh\` to stop billing. + +--- + +## 4. Uploaded Files and Their Content + +\`\`\` +sk1/ +├── app/ +│ ├── app.py Flask app — routes, DB connection, Kanban UI, /health endpoint +│ ├── Dockerfile Builds taskflow-web image (Python 3.11, Gunicorn, non-root) +│ └── requirements.txt Python deps: flask, gunicorn, psycopg2-binary +├── db/ +│ └── init.sql SQL schema — tasks table, trigger, sample data +├── setup.sh Full deployment — writes files, creates all Azure resources +├── prepare-app.sh Rebuilds and redeploys container after code changes +├── remove-app.sh Deletes entire Azure resource group +├── backup.sh pg_dump backup and restore via psql +├── .env.example Template for required environment variables (no secrets) +├── .gitignore Excludes .env and backups from GIT +└── README.md This file +\`\`\` + +--- + +## 5. Configuration Description + +### Secrets and Environment Variables + +Secrets are stored as **Azure App Service Application Settings** — never in GIT. + +| Variable | Where stored | Used by | +|---|---|---| +| \`POSTGRES_HOST\` | App Service settings | Flask app | +| \`POSTGRES_USER\` | App Service settings | Flask app | +| \`POSTGRES_PASSWORD\` | App Service settings | Flask app | +| \`POSTGRES_DB\` | App Service settings | Flask app | +| \`POSTGRES_PORT\` | App Service settings | Flask app | +| \`FLASK_ENV\` | App Service settings | Gunicorn | +| \`WEBSITES_PORT\` | App Service settings | Azure (tells it app runs on 5000) | +| \`DB_PASSWORD\` | Shell env var at deploy time | \`setup.sh\` only, not stored | + +A local \`.env\` file is written during deployment for use by \`backup.sh\`. It is excluded from GIT by \`.gitignore\`. + +--- + +## 6. How to View and Use the Application + +### Prerequisites +- Azure CLI installed and \`az login\` completed +- Docker installed and running +- \`psql\` installed (for backup/restore and schema init) +- \`DB_PASSWORD\` environment variable set + +### Deploy (first time — creates all Azure resources) +\`\`\`bash +export DB_PASSWORD="YourStrongPassword123!" +chmod +x setup.sh && ./setup.sh +\`\`\` +Takes approximately **5–8 minutes**. + +### Open in Browser +\`\`\` +https://${APP_NAME}.azurewebsites.net +\`\`\` + +### Redeploy after code changes +\`\`\`bash +./prepare-app.sh +\`\`\` + +### Remove all Azure resources +\`\`\`bash +./remove-app.sh +\`\`\` + +--- + +## 7. How to Perform a Data Backup + +Install psql if needed: \`sudo apt install postgresql-client\` + +\`\`\`bash +# Create a timestamped backup +source .env && ./backup.sh + +# List backups +ls -lh backups/ + +# Restore +source .env && ./backup.sh --restore backups/taskflow-20250601-120000.sql +\`\`\` + +--- + +## 8. How to View Access Logs from the Internet + +Azure App Service logs all HTTP requests. View them via Azure CLI: + +\`\`\`bash +# Stream live access logs (all internet requests to the app) +az webapp log tail --resource-group taskflow-rg --name ${APP_NAME} + +# Enable detailed logging first if needed +az webapp log config \ + --resource-group taskflow-rg \ + --name ${APP_NAME} \ + --application-logging filesystem \ + --web-server-logging filesystem \ + --level information + +# Download logs as zip +az webapp log download --resource-group taskflow-rg --name ${APP_NAME} +\`\`\` + +Each log entry contains: timestamp, client IP, HTTP method, path, status code, response time. + +--- + +## 9. Conditions for Running prepare-app.sh and remove-app.sh + +### prepare-app.sh +- Azure CLI installed and \`az login\` completed +- Docker installed and running +- \`.env\` file must exist in the same directory (created by \`setup.sh\`) +- ACR name and App name are hardcoded from the initial deployment — re-run \`setup.sh\` to create fresh resources + +### remove-app.sh +- Azure CLI installed and \`az login\` completed +- Resource group \`taskflow-rg\` must exist +- Prompts for confirmation before deleting +- All data will be lost unless \`backup.sh\` was run first + +--- + +## 10. External Resources and Use of Generative AI + +### External Resources + +| Resource | Type | How Used | +|---|---|---| +| [Azure App Service Documentation](https://learn.microsoft.com/en-us/azure/app-service/) | Official docs | Web app creation, container config, app settings, logging | +| [Azure PostgreSQL Flexible Server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/) | Official docs | Server creation, firewall, connection strings | +| [Azure Container Registry](https://learn.microsoft.com/en-us/azure/container-registry/) | Official docs | Image push, admin credentials | +| [Flask Documentation](https://flask.palletsprojects.com/) | Official docs | Routing, templates | +| [PostgreSQL pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) | Official docs | Backup commands | +| [Azure Pricing Calculator](https://azure.microsoft.com/pricing/calculator/) | Tool | Cost analysis | +| Course lectures (azure, certbot, compose) | Lecture slides | Architecture, PaaS concepts, IaaS vs PaaS | + +### Generative AI Usage +**Tool:** Claude by Anthropic (claude.ai) +**Type:** Productivity assistance — Azure CLI commands, shell scripts, Dockerfile, Flask app, README. +**Method:** Student described requirements, reviewed all output, verified understanding before submission. All architectural decisions directed by the student. AI accelerated writing; understanding was the student's own. +READMEEOF +ok "README.md" + +# --------------------------------------------------------------------------- +# Done! +# --------------------------------------------------------------------------- +echo "" +echo -e "${BOLD}=================================================${RESET}" +echo -e "${GREEN}${BOLD} TaskFlow is LIVE!${RESET}" +echo -e "${BOLD}=================================================${RESET}" +echo -e " ${BOLD}URL:${RESET} ${GREEN}${APP_URL}${RESET}" +echo -e " ${BOLD}Database:${RESET} ${DB_SERVER}.postgres.database.azure.com" +echo -e " ${BOLD}Registry:${RESET} ${ACR_NAME}.azurecr.io" +echo -e "" +echo -e " Logs: az webapp log tail --resource-group ${RG} --name ${APP_NAME}" +echo -e " Backup: source .env && ./backup.sh" +echo -e " Remove: ./remove-app.sh" +echo -e "${BOLD}=================================================${RESET}"