雰囲気エンジニアの備忘録

Atmosphere Engineer's Memorandum

Posts簡単なPythonアプリをKubernetesにデプロイする

簡単なPythonアプリをKubernetesにデプロイする

概要

Pythonのみで完結する簡単な3層アプリを構築し、kubernetesにデプロイする。
StreamlitとFlaskサーバーを連携させ、DBへTodoメモを保存させる。

構成

  • フロントエンド: Streamlit
  • バックエンド: Flask
  • データベース: PostgreSQL

フォルダ配置

.
├── frontend
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
├── backend
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt
└── k8s
    ├── backend.yaml
    ├── db.yaml
    └── frontend.yaml

アプリケーションコード

フロントエンド

streamlitを実行するdockerfileを作成。

Dockerfile
FROM python:3.9

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
requirements.txt
streamlit
requests

アプリケーションコードは以下。
Todoを記入し、ボタンを押すとリストに登録されるだけの簡単なアプリを作成。

app.py
import streamlit as st
import requests
import os

API_URL = os.getenv("API_URL", "http://backend:5000")

st.title("✍Todo App")

new_todo = st.text_input("新しいTodoを追加")
if st.button("追加"):
    if new_todo:
        response = requests.post(f"{API_URL}/todos", json={"text": new_todo})
        if response.status_code == 200:
            st.success("Todoを追加しました!")
            st.rerun()
        else:
            st.error("エラーが発生しました")

st.subheader("登録済みのTodo")
todos = requests.get(f"{API_URL}/todos").json()
for todo in todos:
    col1, col2 = st.columns([4, 1])
    col1.write(todo["text"])
    if col2.button("削除", key=todo["id"]):
        requests.delete(f"{API_URL}/todos/{todo['id']}")
        st.rerun()

バックエンド

フロントエンドとほぼ同様の手順。
パッケージの中身と、実行コマンドのみ変更している。

Dockerfile
FROM python:3.9

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]
requirements.txt
Flask
Flask-SQLAlchemy
Flask-Cors
psycopg2-binary

アプリケーションコードは以下。
/todosパスにGET, POSTメソッドを追加し、todoメモの取得と登録ができるようにした。
また、/todos/<int:todo_id>パスにDELETEメソッドを割り当てることで、idを指定してメモを削除できるようにした。

app.py
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
import os

app = Flask(__name__)
CORS(app)

# 環境変数からDBの設定を取得
DB_USER = os.getenv("POSTGRES_USER", "user")
DB_PASSWORD = os.getenv("POSTGRES_PASSWORD", "password")
DB_NAME = os.getenv("POSTGRES_DB", "tododb")
DB_HOST = os.getenv("POSTGRES_HOST", "db")

app.config["SQLALCHEMY_DATABASE_URI"] = (
    f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_NAME}"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

db = SQLAlchemy(app)


class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(200), nullable=False)


@app.route("/todos", methods=["GET"])
def get_todos():
    todos = Todo.query.all()
    return jsonify([{"id": todo.id, "text": todo.text} for todo in todos])


@app.route("/todos", methods=["POST"])
def add_todo():
    data = request.json
    new_todo = Todo(text=data["text"])
    db.session.add(new_todo)
    db.session.commit()
    return jsonify({"message": "Todo added!"})


@app.route("/todos/<int:todo_id>", methods=["DELETE"])
def delete_todo(todo_id):
    todo = Todo.query.get(todo_id)
    if todo:
        db.session.delete(todo)
        db.session.commit()
        return jsonify({"message": "Todo deleted!"})
    return jsonify({"error": "Todo not found"}), 404


if __name__ == "__main__":
    with app.app_context():
        db.create_all()
    app.run(host="0.0.0.0", port=5000)

kubernetesでのデプロイ

マニフェストファイルの作成

k8s/以下に、マニフェストファイルを作成。

frontend.yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: LoadBalancer
  ports:
    - port: 8501
  selector:
    app: frontend

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: frontend
          image: frontend:v0.0.0
          imagePullPolicy: Never
          env:
            - name: API_URL
              value: "http://backend:5000"
          ports:
            - containerPort: 8501
backend.yaml
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  ports:
    - port: 5000
  selector:
    app: backend

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - name: backend
          image: backend:v0.0.0
          imagePullPolicy: Never
          env:
            - name: POSTGRES_HOST
              value: "db"
          ports:
            - containerPort: 5000
db.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Mi

---
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  ports:
    - port: 5432
  selector:
    app: db

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: db
spec:
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
        - name: db
          image: postgres:15
          env:
            - name: POSTGRES_USER
              value: "user"
            - name: POSTGRES_PASSWORD
              value: "password"
            - name: POSTGRES_DB
              value: "tododb"
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pvc

デプロイ手順

minikubeを起動。

minikube start --memory=4096 --cpus=2

以下コマンドで、minikube内のDockerレジストリに、コンテナイメージをビルド。

eval $(minikube docker-env)
docker build -t backend:v0.0.0 ./backend
docker build -t frontend:v0.0.0 ./frontend

todo-appという名前空間を切って、マニフェストをデプロイ。

kubectl create namespace todo-app
kubectl apply -f k8s/ -n todo-app

暫く待つとコンテナが起動し、以下のコマンドでステータスを確認できる。

kubectl get pod,svc -n todo-app
NAME                           READY   STATUS    RESTARTS      AGE
pod/backend-75db745896-2mn6x   1/1     Running   1 (26s ago)   29s
pod/backend-75db745896-brhwg   1/1     Running   1 (25s ago)   29s
pod/db-5566db44cc-mh2vx        1/1     Running   0             29s
pod/frontend-6776dd978-9bpqg   1/1     Running   0             29s
pod/frontend-6776dd978-hvlx7   1/1     Running   0             29s

NAME               TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/backend    ClusterIP      10.106.69.10   <none>        5000/TCP         29s
service/db         ClusterIP      10.109.7.131   <none>        5432/TCP         29s
service/frontend   LoadBalancer   10.103.12.36   <pending>     8501:31661/TCP   29s

ブラウザから操作

以下で、frontendのサービスをローカルの8501ポートに転送する。

kubectl port-forward svc/frontend 8501:8501 -n todo-app

以下のようにアプリ画面が表示され、Todoを登録、確認、削除できるようになる。

todoアプリ画面

削除

以下でnamespaceごとアプリを削除する

kubectl delete namespace todo-app

まとめ

簡易な3層構造(streamlit, flask, db)のアプリを作成した。
kubernetesでのデプロイサンプルを作成した。