diff --git a/z2/Dockerfile b/z2/Dockerfile new file mode 100644 index 0000000..3578b8f --- /dev/null +++ b/z2/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 5000 + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] \ No newline at end of file diff --git a/z2/README.md b/z2/README.md new file mode 100644 index 0000000..445a242 --- /dev/null +++ b/z2/README.md @@ -0,0 +1,107 @@ +# Shopping List Web Application on Kubernetes + +## Overview +This project deploys a Python shopping list web application using Flask and PostgreSQL on a Kubernetes cluster. The application allows users to create, track, and manage their shopping items in a persistent way. The deployment includes a Namespace, Deployments for both the web application and PostgreSQL database, a StatefulSet (with PersistentVolume and PersistentVolumeClaim), and Service. + +## Application Description +- Simple shopping list application that allows users to add, mark as purchased, and delete items +- Each item can have a quantity associated with it +- All data is stored in PostgreSQL for persistence between application restarts +- The application uses a Flask web framework with a minimalist interface + +## Containers +- **simple-web-app**: Runs the Python Flask application on port 5000 +- **postgres**: Runs PostgreSQL database to store shopping list items + +## Kubernetes Objects +- **Namespace**: Isolates all the resources under `my-app` +- **Deployment (Web App)**: Manages the stateless web application pods with 2 replicas for high availability +- **Deployment (PostgreSQL)**: Manages the PostgreSQL database with persistent storage +- **StatefulSet**: Manages stateful application pods that require persistent storage +- **PersistentVolume (PV)**: Provides persistent storage from the host (1GB) +- **PersistentVolumeClaim (PVC)**: Claims the PV for storage +- **Service (Web App)**: Exposes the web application externally via LoadBalancer +- **Service (PostgreSQL)**: Headless service for internal database access + +## Networking and Storage +- Internal service discovery allows the web application to connect to PostgreSQL +- PostgreSQL uses persistent storage to maintain shopping list data even if pods are restarted +- The web application is exposed externally using a LoadBalancer service + +## Container Configuration +- The web app container is based on Python and includes Flask and psycopg2 +- PostgreSQL container uses the official PostgreSQL image +- Resource limits and readiness probes are configured for better stability + +## How to Prepare, Run, Pause, and Delete the Application + +1. **Prepare the application:** + ```bash + ./prepare-app.sh + ``` + This script builds the Docker image and creates the directory for persistent volume. + +2. **Start the application:** + ```bash + ./start-app.sh + ``` + This script creates all necessary Kubernetes objects in the correct order, including PostgreSQL. + +3. **Pause or delete the application:** + ```bash + ./stop-app.sh + ``` + This script removes all Kubernetes objects created by `start-app.sh`. + +## Accessing the Application +To access the application: + +1. Find the IP address of your Kubernetes node: + ```bash + kubectl get nodes -o wide + ``` + +2. Access the application in your browser at: + ``` + http://:80 + ``` + Where `` is the IP address of any of your Kubernetes nodes. + +## Application Features +- Add items with quantity +- Mark items as purchased/unpurchased +- Delete items +- Items list is separated into "to buy" and "purchased" sections +- Data persists between sessions and application restarts + +## Database Schema +The application uses a simple PostgreSQL schema: +- Table: `shopping_items` +- Fields: + - `id`: Serial primary key + - `item`: Text (item name) + - `quantity`: Integer (defaults to 1) + - `purchased`: Boolean flag (defaults to false) + +## Troubleshooting +If you encounter issues: + +1. Check pod status: + ```bash + kubectl get pods -n my-app + ``` + +2. View pod logs: + ```bash + kubectl logs -n my-app + ``` + +3. Check service status: + ```bash + kubectl get svc -n my-app + ``` + +4. Check database connectivity: + ```bash + kubectl exec -it -n my-app -- python -c "import psycopg2; conn = psycopg2.connect(host='postgres', dbname='postgres', user='postgres', password='postgres'); print('Connection successful')" + ``` \ No newline at end of file diff --git a/z2/app.py b/z2/app.py new file mode 100644 index 0000000..103d91f --- /dev/null +++ b/z2/app.py @@ -0,0 +1,173 @@ +from flask import Flask, request, redirect, url_for, render_template_string +import os +import psycopg2 +from psycopg2 import pool + +app = Flask(__name__) + +DB_HOST = os.environ.get('DB_HOST', 'localhost') +DB_PORT = os.environ.get('DB_PORT', '5432') +DB_NAME = os.environ.get('DB_NAME', 'postgres') +DB_USER = os.environ.get('DB_USER', 'postgres') +DB_PASS = os.environ.get('DB_PASS', 'postgres') + +connection_pool = psycopg2.pool.SimpleConnectionPool( + 1, 10, + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, + user=DB_USER, + password=DB_PASS +) + +def init_db(): + conn = connection_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute(''' + CREATE TABLE IF NOT EXISTS shopping_items ( + id SERIAL PRIMARY KEY, + item TEXT NOT NULL, + quantity INTEGER DEFAULT 1, + purchased BOOLEAN DEFAULT FALSE + ); + ''') + conn.commit() + except Exception as e: + print(f"Erreur lors de l'initialisation de la base de données: {e}") + conn.rollback() + finally: + connection_pool.putconn(conn) + +HTML_TEMPLATE = """ + + + + Liste de Courses + + + + +

Ma Liste de Courses

+ +
+ + + +
+ +

Articles à acheter

+
    + {% for item in items %} + {% if not item[3] %} +
  • + {{ item[1] }} ({{ item[2] }}) +
    + +
    +
    + +
    +
  • + {% endif %} + {% endfor %} +
+ +

Articles achetés

+
    + {% for item in items %} + {% if item[3] %} +
  • + {{ item[1] }} ({{ item[2] }}) +
    + +
    +
    + +
    +
  • + {% endif %} + {% endfor %} +
+ + +""" + +@app.route('/') +def index(): + conn = connection_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute("SELECT id, item, quantity, purchased FROM shopping_items ORDER BY purchased, id") + items = cur.fetchall() + except Exception as e: + print(f"Erreur lors de la récupération des articles: {e}") + items = [] + finally: + connection_pool.putconn(conn) + + return render_template_string(HTML_TEMPLATE, items=items) + +@app.route('/add', methods=['POST']) +def add_item(): + item = request.form.get('item') + quantity = request.form.get('quantity', 1, type=int) + + conn = connection_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute( + "INSERT INTO shopping_items (item, quantity) VALUES (%s, %s)", + (item, quantity) + ) + conn.commit() + except Exception as e: + print(f"Erreur lors de l'ajout d'un article: {e}") + conn.rollback() + finally: + connection_pool.putconn(conn) + + return redirect(url_for('index')) + +@app.route('/toggle/', methods=['POST']) +def toggle_item(item_id): + conn = connection_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute( + "UPDATE shopping_items SET purchased = NOT purchased WHERE id = %s", + (item_id,) + ) + conn.commit() + except Exception as e: + print(f"Erreur lors du basculement du statut: {e}") + conn.rollback() + finally: + connection_pool.putconn(conn) + + return redirect(url_for('index')) + +@app.route('/delete/', methods=['POST']) +def delete_item(item_id): + conn = connection_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute("DELETE FROM shopping_items WHERE id = %s", (item_id,)) + conn.commit() + except Exception as e: + print(f"Erreur lors de la suppression d'un article: {e}") + conn.rollback() + finally: + connection_pool.putconn(conn) + + return redirect(url_for('index')) + +with app.app_context(): + try: + init_db() + except Exception as e: + print(f"Impossible d'initialiser la base de données: {e}") + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host='0.0.0.0', port=port, debug=True) \ No newline at end of file diff --git a/z2/deployment.yaml b/z2/deployment.yaml new file mode 100644 index 0000000..29751ed --- /dev/null +++ b/z2/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-app-deployment + namespace: my-app +spec: + replicas: 2 + selector: + matchLabels: + app: web-app + template: + metadata: + labels: + app: web-app + spec: + containers: + - name: web-app-container + image: antonin193/simple-web-app:latest + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: DB_HOST + value: postgres + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: postgres + - name: DB_USER + value: postgres + - name: DB_PASS + value: postgres + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + readinessProbe: + httpGet: + path: / + port: 5000 + initialDelaySeconds: 15 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 5000 + initialDelaySeconds: 20 + periodSeconds: 20 \ No newline at end of file diff --git a/z2/namespace.yaml b/z2/namespace.yaml new file mode 100644 index 0000000..ac20ed2 --- /dev/null +++ b/z2/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: my-app + labels: + name: my-app + environment: development \ No newline at end of file diff --git a/z2/persistent-storage.yaml b/z2/persistent-storage.yaml new file mode 100644 index 0000000..5413374 --- /dev/null +++ b/z2/persistent-storage.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: data-pv + namespace: my-app + labels: + type: local +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: manual + hostPath: + path: /data/stateful +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: data-pvc + namespace: my-app +spec: + accessModes: + - ReadWriteOnce + storageClassName: manual + resources: + requests: + storage: 1Gi diff --git a/z2/postgres-deployment.yaml b/z2/postgres-deployment.yaml new file mode 100644 index 0000000..46abfd2 --- /dev/null +++ b/z2/postgres-deployment.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: my-app +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:14 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 5432 + env: + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_DB + value: postgres + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: data-pvc \ No newline at end of file diff --git a/z2/postgres-service.yaml b/z2/postgres-service.yaml new file mode 100644 index 0000000..0e61f36 --- /dev/null +++ b/z2/postgres-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: my-app + labels: + app: postgres +spec: + selector: + app: postgres + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + clusterIP: None \ No newline at end of file diff --git a/z2/prepare-app.sh b/z2/prepare-app.sh new file mode 100644 index 0000000..1383fac --- /dev/null +++ b/z2/prepare-app.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Script to prepare the application environment + +export REGION="local" +echo "Preparing deployment for region: $REGION" + +# Make sure scripts are executable +chmod +x start-app.sh stop-app.sh + +# Build and tag Docker image +docker build -t simple-web-app:latest . +docker tag simple-web-app:latest antonin193/simple-web-app:latest + +# Push to Docker Hub if needed +echo "Pushing image to Docker Hub..." +docker push antonin193/simple-web-app:latest + +# Create directory for persistent volume (Docker Desktop supports hostPath) +sudo mkdir -p /data/stateful +sudo chmod 777 /data/stateful + +echo "Preparation complete: Docker image built, tagged, and volume directory created." diff --git a/z2/requirements.txt b/z2/requirements.txt new file mode 100644 index 0000000..9856f32 --- /dev/null +++ b/z2/requirements.txt @@ -0,0 +1,4 @@ +Flask>=2.0.0 +Werkzeug>=2.0.0 +gunicorn>=20.1.0 +psycopg2-binary>=2.9.3 \ No newline at end of file diff --git a/z2/service.yaml b/z2/service.yaml new file mode 100644 index 0000000..49ee316 --- /dev/null +++ b/z2/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: web-app-service + namespace: my-app + labels: + app: web-app +spec: + type: LoadBalancer + selector: + app: web-app + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + sessionAffinity: None \ No newline at end of file diff --git a/z2/start-app.sh b/z2/start-app.sh new file mode 100644 index 0000000..409f4dd --- /dev/null +++ b/z2/start-app.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Script to deploy the app locally using Docker Desktop + Kubernetes + +# Set default region (optional but kept for consistency) +if [ -z "$REGION" ]; then + export REGION="local" + echo "No region specified. Using default region: $REGION" +else + echo "Deploying to region: $REGION" +fi + +# Build Docker image +echo "Building Docker image..." +docker build -t antonin193/simple-web-app:latest . + +# Push to Docker Hub (optional for local, but kept if needed across machines) +echo "Pushing image to Docker Hub..." +docker push antonin193/simple-web-app:latest + +# Create Kubernetes Namespace +echo "Creating namespace..." +kubectl apply -f namespace.yaml + +# Create PersistentVolume and PersistentVolumeClaim +echo "Creating persistent storage..." +kubectl apply -f persistent-storage.yaml + +# Wait briefly for resources to be established +sleep 2 + +# Deploy PostgreSQL first +echo "Creating PostgreSQL Deployment and Service..." +kubectl apply -f postgres-deployment.yaml +kubectl apply -f postgres-service.yaml + +# Wait for PostgreSQL to be ready +echo "Waiting for PostgreSQL to be ready..." +kubectl wait --for=condition=ready pod -l app=postgres -n my-app --timeout=120s + +# Deploy application +echo "Creating Deployment..." +kubectl apply -f deployment.yaml + +echo "Creating StatefulSet..." +kubectl apply -f statefulset.yaml + +echo "Creating Service..." +kubectl apply -f service.yaml + +# Wait for LoadBalancer IP (Docker Desktop uses host network so it's usually localhost) +echo "Waiting for LoadBalancer to obtain an external IP (or localhost for Docker Desktop)..." +external_ip="" +attempt=0 +max_attempts=4 + +while [ -z "$external_ip" ] && [ $attempt -lt $max_attempts ]; do + sleep 10 + attempt=$((attempt+1)) + external_ip=$(kubectl get svc web-app-service -n my-app --template="{{range .status.loadBalancer.ingress}}{{.ip}}{{end}}" 2>/dev/null) + + if [ -z "$external_ip" ]; then + echo "Waiting for external IP... Attempt $attempt of $max_attempts" + fi +done + +# Fallback to localhost if no external IP is found (common in Docker Desktop) +if [ -z "$external_ip" ]; then + external_ip="localhost" + echo "" + echo "==========================================================" + echo "Could not get external IP from LoadBalancer. Defaulting to localhost." + echo "You can try accessing your app at: http://localhost:80" + echo "Or check service status manually with:" + echo "kubectl get svc web-app-service -n my-app" + echo "==========================================================" +else + echo "" + echo "==========================================================" + echo "Application deployed successfully!" + echo "You can try accessing your app at: http://localhost:80" + echo "==========================================================" +fi \ No newline at end of file diff --git a/z2/statefulset.yaml b/z2/statefulset.yaml new file mode 100644 index 0000000..bf802e3 --- /dev/null +++ b/z2/statefulset.yaml @@ -0,0 +1,59 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: stateful-app + namespace: my-app +spec: + serviceName: "stateful-app" + replicas: 1 + selector: + matchLabels: + app: stateful-app + template: + metadata: + labels: + app: stateful-app + spec: + containers: + - name: stateful-app-container + image: antonin193/simple-web-app:latest + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: DB_HOST + value: postgres + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: postgres + - name: DB_USER + value: postgres + - name: DB_PASS + value: postgres + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + volumeMounts: + - name: app-storage + mountPath: /data + readinessProbe: + httpGet: + path: / + port: 5000 + initialDelaySeconds: 15 + periodSeconds: 20 + volumeClaimTemplates: + - metadata: + name: app-storage + spec: + accessModes: + - ReadWriteOnce + storageClassName: manual + resources: + requests: + storage: 1Gi \ No newline at end of file diff --git a/z2/stop-app.sh b/z2/stop-app.sh new file mode 100644 index 0000000..ccabdca --- /dev/null +++ b/z2/stop-app.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Delete Kubernetes objects in reverse order + +# Check if REGION environment variable is set +if [ -z "$REGION" ]; then + # Default region if not set + export REGION="westeurope" + echo "No region specified. Using default region: $REGION" +else + echo "Stopping application in region: $REGION" +fi + +# For AKS deployments, make sure we're connected to the right cluster +if command -v az &> /dev/null; then + echo "Checking AKS connection for region $REGION..." + # You might need to adjust these parameters based on your specific Azure setup + RESOURCE_GROUP="flask-rg-$REGION" + CLUSTER_NAME="flask-aks-$REGION" + + # Try to connect to the Azure cluster + if ! az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --overwrite-existing 2>/dev/null; then + echo "Warning: Could not connect to AKS cluster in $REGION. Continuing with current kubectl context." + else + echo "Successfully connected to AKS cluster in $REGION" + fi +fi + +echo "Stopping application..." + +# Delete Service first to stop incoming traffic +kubectl delete -f service.yaml 2>/dev/null || echo "Service could not be deleted or does not exist." + +# Delete StatefulSet and wait for pods to terminate +kubectl delete -f statefulset.yaml 2>/dev/null || echo "StatefulSet could not be deleted or does not exist." + +# Delete Deployment +kubectl delete -f deployment.yaml 2>/dev/null || echo "Deployment could not be deleted or does not exist." + +# Delete PostgreSQL objects +kubectl delete -f postgres-service.yaml 2>/dev/null || echo "PostgreSQL service could not be deleted or does not exist." +kubectl delete -f postgres-deployment.yaml 2>/dev/null || echo "PostgreSQL deployment could not be deleted or does not exist." + +# Wait for pods to terminate +echo "Waiting for pods to terminate..." +kubectl wait --for=delete pod --selector=app=web-app -n my-app --timeout=60s 2>/dev/null || true +kubectl wait --for=delete pod --selector=app=stateful-app -n my-app --timeout=60s 2>/dev/null || true +kubectl wait --for=delete pod --selector=app=postgres -n my-app --timeout=60s 2>/dev/null || true + +# Delete PersistentVolume and PersistentVolumeClaim +kubectl delete -f persistent-storage.yaml 2>/dev/null || echo "PersistentVolume and PersistentVolumeClaim could not be deleted or do not exist." + +# Delete Namespace (this will delete all resources in the namespace) +kubectl delete -f namespace.yaml 2>/dev/null || echo "Namespace could not be deleted or does not exist." + +echo "Application stopped in region $REGION: All Kubernetes objects have been deleted." \ No newline at end of file