Upload files to "sk1"
This commit is contained in:
parent
2fb6820938
commit
5a4ddbbfad
4
sk1/.env
Normal file
4
sk1/.env
Normal file
@ -0,0 +1,4 @@
|
||||
POSTGRES_HOST=taskflow-db-14986.postgres.database.azure.com
|
||||
POSTGRES_USER=taskuser
|
||||
POSTGRES_PASSWORD=TaskFlow2025!
|
||||
POSTGRES_DB=taskdb
|
||||
5
sk1/.env.example
Normal file
5
sk1/.env.example
Normal file
@ -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
|
||||
6
sk1/.gitignore
vendored
Normal file
6
sk1/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
.env
|
||||
backups/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.swp
|
||||
.DS_Store
|
||||
226
sk1/README.md
Normal file
226
sk1/README.md
Normal file
@ -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.
|
||||
32
sk1/backup.sh
Normal file
32
sk1/backup.sh
Normal file
@ -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 <file>" && 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
|
||||
17
sk1/prepare-app.sh
Normal file
17
sk1/prepare-app.sh
Normal file
@ -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"
|
||||
9
sk1/remove-app.sh
Normal file
9
sk1/remove-app.sh
Normal file
@ -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."
|
||||
749
sk1/setup.sh
Normal file
749
sk1/setup.sh
Normal file
@ -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 = """
|
||||
<!doctype html><html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TaskFlow</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:#f5f5f5;color:#222;padding:2rem}
|
||||
h1{font-size:1.8rem;margin-bottom:1.5rem;color:#1a1a2e}
|
||||
.board{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin-bottom:2rem}
|
||||
.column{background:#fff;border-radius:8px;padding:1rem;border:1px solid #e0e0e0}
|
||||
.column h2{font-size:1rem;text-transform:uppercase;letter-spacing:.05em;color:#555;
|
||||
margin-bottom:.75rem;border-bottom:2px solid;padding-bottom:.4rem}
|
||||
.col-todo h2{border-color:#6c757d}
|
||||
.col-progress h2{border-color:#0d6efd}
|
||||
.col-done h2{border-color:#198754}
|
||||
.task{background:#f8f9fa;border:1px solid #dee2e6;border-radius:6px;
|
||||
padding:.75rem;margin-bottom:.5rem}
|
||||
.task h3{font-size:.95rem;margin-bottom:.25rem}
|
||||
.task p{font-size:.8rem;color:#666;margin-bottom:.5rem}
|
||||
.task-actions{display:flex;gap:.5rem;flex-wrap:wrap}
|
||||
.btn{padding:.25rem .6rem;border:none;border-radius:4px;cursor:pointer;
|
||||
font-size:.8rem;display:inline-block}
|
||||
.btn-advance{background:#0d6efd;color:#fff}
|
||||
.btn-delete{background:#dc3545;color:#fff}
|
||||
form.new-task{background:#fff;border:1px solid #e0e0e0;border-radius:8px;
|
||||
padding:1.25rem;max-width:520px}
|
||||
form.new-task h2{margin-bottom:1rem;font-size:1.1rem}
|
||||
label{display:block;font-size:.85rem;color:#444;margin-bottom:.25rem}
|
||||
input[type=text],textarea{width:100%;padding:.5rem .75rem;border:1px solid #ccc;
|
||||
border-radius:5px;font-size:.9rem;margin-bottom:.75rem}
|
||||
textarea{resize:vertical;min-height:70px}
|
||||
.btn-submit{background:#198754;color:#fff;padding:.5rem 1.25rem;font-size:.95rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📋 TaskFlow</h1>
|
||||
<div class="board">
|
||||
{% for col,label,css in [('todo','To Do','col-todo'),
|
||||
('in_progress','In Progress','col-progress'),
|
||||
('done','Done','col-done')] %}
|
||||
<div class="column {{css}}">
|
||||
<h2>{{label}} ({{tasks[col]|length}})</h2>
|
||||
{% for t in tasks[col] %}
|
||||
<div class="task">
|
||||
<h3>{{t.title}}</h3>
|
||||
{% if t.description %}<p>{{t.description}}</p>{% endif %}
|
||||
<div class="task-actions">
|
||||
{% if col != 'done' %}
|
||||
<form method="post" action="/advance/{{t.id}}">
|
||||
<button class="btn btn-advance">▶ Advance</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/delete/{{t.id}}">
|
||||
<button class="btn btn-delete">✕ Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form class="new-task" method="post" action="/create">
|
||||
<h2>New Task</h2>
|
||||
<label>Title</label>
|
||||
<input type="text" name="title" required placeholder="What needs to be done?">
|
||||
<label>Description (optional)</label>
|
||||
<textarea name="description" placeholder="More details..."></textarea>
|
||||
<button type="submit" class="btn btn-submit">Add Task</button>
|
||||
</form>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
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/<int:task_id>", 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/<int:task_id>", 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 <file>" && 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}"
|
||||
Loading…
Reference in New Issue
Block a user