From 9253931fc7698b7568836c3d1f89ccb87b4f3208 Mon Sep 17 00:00:00 2001 From: Coline Reberga Date: Fri, 25 Apr 2025 11:37:43 +0000 Subject: [PATCH] add folders --- sk1/Dockerfile | 16 +++++ sk1/README.md | 60 +++++++++++++++++++ sk1/aks-cluster.sh | 29 +++++++++ sk1/app.py | 136 +++++++++++++++++++++++++++++++++++++++++++ sk1/deployment.yaml | 32 ++++++++++ sk1/ingress.yaml | 19 ++++++ sk1/namespace.yaml | 4 ++ sk1/prepare-app.sh | 43 ++++++++++++++ sk1/remove-app.sh | 12 ++++ sk1/requirements.txt | 3 + sk1/service.yaml | 29 +++++++++ sk1/statefulset.yaml | 40 +++++++++++++ 12 files changed, 423 insertions(+) create mode 100644 sk1/Dockerfile create mode 100644 sk1/README.md create mode 100644 sk1/aks-cluster.sh create mode 100644 sk1/app.py create mode 100644 sk1/deployment.yaml create mode 100644 sk1/ingress.yaml create mode 100644 sk1/namespace.yaml create mode 100644 sk1/prepare-app.sh create mode 100644 sk1/remove-app.sh create mode 100644 sk1/requirements.txt create mode 100644 sk1/service.yaml create mode 100644 sk1/statefulset.yaml diff --git a/sk1/Dockerfile b/sk1/Dockerfile new file mode 100644 index 0000000..a62458e --- /dev/null +++ b/sk1/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +ENV FLASK_APP=app.py +ENV FLASK_RUN_HOST=0.0.0.0 +ENV FLASK_RUN_PORT=5000 + +EXPOSE 5000 + +CMD ["flask", "run"] \ No newline at end of file diff --git a/sk1/README.md b/sk1/README.md new file mode 100644 index 0000000..14302b2 --- /dev/null +++ b/sk1/README.md @@ -0,0 +1,60 @@ + +Blog Application - Deployment on Azure AKS +========================================== + +Application Description +------------------------ +A simple Flask blog app allowing users to create, view, and delete articles. Articles are stored persistently in PostgreSQL. + +Cloud & Technologies Used +-------------------------- +- Cloud: Microsoft Azure (AKS for orchestration, ACR for Docker images) +- Stack: Flask, PostgreSQL, Docker, Kubernetes +- Kubernetes Objects: + - Namespace, Deployment (Flask), StatefulSet (PostgreSQL), PVC, Services + +File Overview +------------- +| File | Description | +|--------------------|------------------------------------------------------------| +| app.py | Flask app logic | +| Dockerfile | Defines Flask app image | +| requirements.txt | Python dependencies | +| namespace.yaml | Creates a K8s namespace | +| deployment.yaml | Deploys the Flask app | +| statefulset.yaml | Deploys PostgreSQL with persistent volume | +| service.yaml | Exposes app and DB services | +| aks-cluster.sh | Creates AKS cluster and sets up infra | +| prepare-app.sh | Builds image, deploys app and services | +| remove-app.sh | Tears down all K8s components | +| README.md | This documentation | + +Deployment Instructions +------------------------ +1. Set up infrastructure + ./aks-cluster.sh + +2. Deploy the app + ./prepare-app.sh + +3. Access the app + Open the external IP in your browser (output by the script) + +4. Remove everything + ./remove-app.sh + +Requirements +------------ +- Azure account, Azure CLI +- kubectl & Docker installed locally + +Notes +----- +- PostgreSQL uses a persistent volume (data survives restarts) +- The Flask app restarts automatically on failure +- All deployments are fully scripted via shell & YAML + +Sources & Tools +--------------- +- Flask documentation: https://flask.palletsprojects.com/ +- Kubernetes documentation: https://kubernetes.io/ \ No newline at end of file diff --git a/sk1/aks-cluster.sh b/sk1/aks-cluster.sh new file mode 100644 index 0000000..3afa1c9 --- /dev/null +++ b/sk1/aks-cluster.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Variables +RESOURCE_GROUP="blog-app-rg" +CLUSTER_NAME="blog-app-aks" +LOCATION="westeurope" # Choose an appropriate Azure region +NODE_COUNT=1 +NODE_VM_SIZE="Standard_B2s" # Economic VM size for demo projects + +# Create resource group +echo "Creating resource group..." +az group create --name $RESOURCE_GROUP --location $LOCATION + +# Create AKS cluster +echo "Creating AKS cluster (this may take several minutes)..." +az aks create \ + --resource-group $RESOURCE_GROUP \ + --name $CLUSTER_NAME \ + --node-count $NODE_COUNT \ + --node-vm-size $NODE_VM_SIZE \ + --enable-managed-identity \ + --generate-ssh-keys + +# Get credentials +echo "Configuring kubectl..." +az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME + +echo "AKS cluster configuration completed!" +echo "You can now run ./prepare-app.sh to deploy the application" \ No newline at end of file diff --git a/sk1/app.py b/sk1/app.py new file mode 100644 index 0000000..df8a3e3 --- /dev/null +++ b/sk1/app.py @@ -0,0 +1,136 @@ +from flask import Flask, request, jsonify +import os +import psycopg2 +import time + +app = Flask(__name__) + +# Database Configuration +db_host = os.environ.get("DB_HOST", "blog-db-service") +db_name = os.environ.get("POSTGRES_DB", "blogdb") +db_user = os.environ.get("POSTGRES_USER", "admin") +db_password = os.environ.get("POSTGRES_PASSWORD", "password") + +def get_db_connection(): + return psycopg2.connect(host=db_host, database=db_name, user=db_user, password=db_password) + +def create_table(): + max_retries = 10 + retry_delay = 5 # seconds + + for attempt in range(max_retries): + try: + print(f"Attempt {attempt+1}/{max_retries} to connect to database...") + conn = get_db_connection() + cur = conn.cursor() + cur.execute(''' + CREATE TABLE IF NOT EXISTS articles ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + content TEXT NOT NULL + ) + ''') + conn.commit() + cur.close() + conn.close() + print("Table 'articles' created successfully") + return True + except Exception as e: + print(f"Database connection attempt {attempt+1}/{max_retries} failed: {e}") + if attempt < max_retries - 1: + print(f"Retrying in {retry_delay} seconds...") + time.sleep(retry_delay) + + print("Failed to create table after multiple attempts") + return False + +# Initialize the variable at startup +table_created = False + +@app.route("/", methods=["GET", "POST"]) +def index(): + global table_created + + # If the table is not yet created, try to create it + if not table_created: + table_created = create_table() + + # Check if the table has been created before continuing + if not table_created: + return "Database connection failed. Please try again later.", 500 + + try: + conn = get_db_connection() + cur = conn.cursor() + + if request.method == "POST": + title = request.form.get("title") + content = request.form.get("content") + if title and content: + cur.execute("INSERT INTO articles (title, content) VALUES (%s, %s)", (title, content)) + conn.commit() + + cur.execute("SELECT * FROM articles ORDER BY id DESC") + articles = cur.fetchall() + cur.close() + conn.close() + + return f""" + + + My Blog + + + +
+
+

My Blog

+
+ + + +
+
+
+

Articles

+ {''.join(f'

{a[1]}

{a[2]}

' for a in articles)} +
+
+ + + """ + except Exception as e: + return f"An error occurred: {str(e)}", 500 + +@app.route("/delete/", methods=["POST"]) +def delete_article(article_id): + try: + conn = get_db_connection() + cur = conn.cursor() + cur.execute("DELETE FROM articles WHERE id = %s", (article_id,)) + conn.commit() + cur.close() + conn.close() + return "" + except Exception as e: + return f"An error occurred while deleting: {str(e)}", 500 + +if __name__ == "__main__": + create_table() + app.run(host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/sk1/deployment.yaml b/sk1/deployment.yaml new file mode 100644 index 0000000..2c80e7b --- /dev/null +++ b/sk1/deployment.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: blog-app + namespace: myapp-namespace + labels: + app: blog +spec: + replicas: 1 + selector: + matchLabels: + app: blog + template: + metadata: + labels: + app: blog + spec: + containers: + - name: blog-app + image: blogappacr.azurecr.io/blog-app:latest + imagePullPolicy: Always + ports: + - containerPort: 5000 + env: + - name: DB_HOST + value: "blog-db-service" + - name: POSTGRES_DB + value: "blogdb" + - name: POSTGRES_USER + value: "admin" + - name: POSTGRES_PASSWORD + value: "password" \ No newline at end of file diff --git a/sk1/ingress.yaml b/sk1/ingress.yaml new file mode 100644 index 0000000..01a416d --- /dev/null +++ b/sk1/ingress.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: blog-app-ingress + namespace: myapp-namespace + annotations: + kubernetes.io/ingress.class: "nginx" +spec: + rules: + - host: blog-app.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: blog-app-service + port: + number: 80 \ No newline at end of file diff --git a/sk1/namespace.yaml b/sk1/namespace.yaml new file mode 100644 index 0000000..e14643a --- /dev/null +++ b/sk1/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: myapp-namespace \ No newline at end of file diff --git a/sk1/prepare-app.sh b/sk1/prepare-app.sh new file mode 100644 index 0000000..e20c737 --- /dev/null +++ b/sk1/prepare-app.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# 1. Check required tools +echo "Checking required tools..." +command -v kubectl >/dev/null 2>&1 || { echo "Error: kubectl is not installed"; exit 1; } +command -v docker >/dev/null 2>&1 || { echo "Error: Docker is not installed"; exit 1; } + +# 2. Set Azure Container Registry name +ACR_NAME="blogappacr" +RESOURCE_GROUP="blog-app-rg" + +# 3. Log in to Azure Container Registry +echo "Logging in to Azure Container Registry..." +az acr login --name $ACR_NAME + +# 4. Build and push Docker image +echo "Building and pushing Docker image..." +docker build -t ${ACR_NAME}.azurecr.io/blog-app:latest . +docker push ${ACR_NAME}.azurecr.io/blog-app:latest + +# 5. Update deployment file with correct image name +echo "Updating deployment file..." +sed -i "s|image: .*|image: ${ACR_NAME}.azurecr.io/blog-app:latest|g" deployment.yaml + +# 6. Deploy to Kubernetes +echo "Deploy to Kubernetes..." +kubectl apply -f namespace.yaml +kubectl apply -f statefulset.yaml +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml + +# 7. Wait for services to get external IP +echo "Waiting for external IP" +EXTERNAL_IP="" +while [ -z "$EXTERNAL_IP" ]; do + EXTERNAL_IP=$(kubectl get service blog-app-service -n myapp-namespace -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null) + [ -z "$EXTERNAL_IP" ] && sleep 10 +done + +# 8. Display success message +echo "" +echo "The blog application is now running! You can access it at: http://$EXTERNAL_IP" +echo "" \ No newline at end of file diff --git a/sk1/remove-app.sh b/sk1/remove-app.sh new file mode 100644 index 0000000..c1bef6d --- /dev/null +++ b/sk1/remove-app.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "Removing all ressources..." +kubectl delete -f service.yaml + +kubectl delete -f deployment.yaml + +kubectl delete -f statefulset.yaml + +kubectl delete -f namespace.yaml + +echo "All resources have been successfully removed." \ No newline at end of file diff --git a/sk1/requirements.txt b/sk1/requirements.txt new file mode 100644 index 0000000..8860e6a --- /dev/null +++ b/sk1/requirements.txt @@ -0,0 +1,3 @@ +Flask +psycopg2-binary +requests \ No newline at end of file diff --git a/sk1/service.yaml b/sk1/service.yaml new file mode 100644 index 0000000..b3b262c --- /dev/null +++ b/sk1/service.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + name: blog-app-service + namespace: myapp-namespace +spec: + selector: + app: blog + ports: + - protocol: TCP + port: 80 + targetPort: 5000 + type: LoadBalancer + +--- + +apiVersion: v1 +kind: Service +metadata: + name: blog-db-service + namespace: myapp-namespace +spec: + selector: + app: blog-db + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + type: ClusterIP \ No newline at end of file diff --git a/sk1/statefulset.yaml b/sk1/statefulset.yaml new file mode 100644 index 0000000..e55aa29 --- /dev/null +++ b/sk1/statefulset.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: blog-db + namespace: myapp-namespace +spec: + serviceName: "blog-db-service" + replicas: 1 + selector: + matchLabels: + app: blog-db + template: + metadata: + labels: + app: blog-db + spec: + containers: + - name: postgres + image: postgres:13 + ports: + - containerPort: 5432 + env: + - name: POSTGRES_DB + value: "blogdb" + - name: POSTGRES_USER + value: "admin" + - name: POSTGRES_PASSWORD + value: "password" + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + subPath: pgdata + volumeClaimTemplates: + - metadata: + name: postgres-storage + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi \ No newline at end of file