add folders
This commit is contained in:
parent
9487f5c0bc
commit
9253931fc7
16
sk1/Dockerfile
Normal file
16
sk1/Dockerfile
Normal file
@ -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"]
|
60
sk1/README.md
Normal file
60
sk1/README.md
Normal file
@ -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/
|
29
sk1/aks-cluster.sh
Normal file
29
sk1/aks-cluster.sh
Normal file
@ -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"
|
136
sk1/app.py
Normal file
136
sk1/app.py
Normal file
@ -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"""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>My Blog</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: 'Poppins', sans-serif; background-color: #f5f5f5; color: #333; margin: 0; padding: 0; }}
|
||||||
|
.container {{ max-width: 1100px; margin: 50px auto; background: #6a0dad; padding: 30px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); display: flex; gap: 20px; }}
|
||||||
|
.form-container, .articles-container {{ flex: 1; background: #ffffff; padding: 20px; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); }}
|
||||||
|
h1{{ text-align: center; color: #6a0dad; font-size: 30px; }}
|
||||||
|
form {{ display: flex; flex-direction: column; gap: 10px; align-items: center; }}
|
||||||
|
input, textarea {{ width: 90%; padding: 14px; border: 1px solid #ccc; border-radius: 5px; background: #fff; color: #333; font-size: 18px; }}
|
||||||
|
button {{ background-color: #6a0dad; color: white; border: none; padding: 12px 25px; cursor: pointer; border-radius: 5px; transition: background 0.3s; font-size: 20px; }}
|
||||||
|
button:hover {{ background-color: #5a0dad; }}
|
||||||
|
.articles-container {{ display: flex; flex-direction: column; gap: 15px; }}
|
||||||
|
.article {{ background: #e0e0e0; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); position: relative; }}
|
||||||
|
.article h3 {{ margin: 0; color: #333; font-size: 22px; }}
|
||||||
|
.article p {{ margin: 10px 0; color: #555; font-size: 18px; }}
|
||||||
|
.delete-form {{ position: absolute; top: 10px; right: 10px; }}
|
||||||
|
.delete-button {{ background-color: #ff5555; padding: 8px 12px; font-size: 14px; border-radius: 5px; }}
|
||||||
|
.delete-button:hover {{ background-color: #cc4444; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<h1>My Blog</h1>
|
||||||
|
<form method="post">
|
||||||
|
<input type="text" name="title" placeholder="Title" required>
|
||||||
|
<textarea name="content" placeholder="Content" required></textarea>
|
||||||
|
<button type="submit">Add Article</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="articles-container">
|
||||||
|
<h1>Articles</h1>
|
||||||
|
{''.join(f'<div class="article"><h3>{a[1]}</h3><p>{a[2]}</p><form method="post" action="/delete/{a[0]}" class="delete-form"><button type="submit" class="delete-button">Delete</button></form></div>' for a in articles)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
except Exception as e:
|
||||||
|
return f"An error occurred: {str(e)}", 500
|
||||||
|
|
||||||
|
@app.route("/delete/<int:article_id>", 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 "<script>window.location.href='/'</script>"
|
||||||
|
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)
|
32
sk1/deployment.yaml
Normal file
32
sk1/deployment.yaml
Normal file
@ -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"
|
19
sk1/ingress.yaml
Normal file
19
sk1/ingress.yaml
Normal file
@ -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
|
4
sk1/namespace.yaml
Normal file
4
sk1/namespace.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: myapp-namespace
|
43
sk1/prepare-app.sh
Normal file
43
sk1/prepare-app.sh
Normal file
@ -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 ""
|
12
sk1/remove-app.sh
Normal file
12
sk1/remove-app.sh
Normal file
@ -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."
|
3
sk1/requirements.txt
Normal file
3
sk1/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Flask
|
||||||
|
psycopg2-binary
|
||||||
|
requests
|
29
sk1/service.yaml
Normal file
29
sk1/service.yaml
Normal file
@ -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
|
40
sk1/statefulset.yaml
Normal file
40
sk1/statefulset.yaml
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user