プログラミング

Phase 0 : VPSを使ってウェブアプリを作りたい3(環境管理強化)

はじめに

前回→VPSを使ってウェブアプリを作りたい2(Github ActionsでCI/CD)

もう少し開発環境を整えていく。

  • 環境管理.envpython-dotenv でシークレット/設定を一元管理
  • DBマイグレーション:SQLAlchemy + Alembic を導入(Flask-SQLAlchemy とFlask-Migrate)
  • CI/CD拡張:CI に Linter(flake8 or black)、型チェック(mypy)、Coverage レポート   を追加。CD に「マイグレーション実行」「ヘルスチェック」「ロールバック条件」ステップを追加

環境管理とDBマイグレーション

1 .env.env.example を用意する

プロジェクトルートに .env を作成し、機密情報や環境ごとに変わる設定だけを置く。
gitには上げない。 PostgreSQLインストールはこちら
# .env
FLASK_ENV=development
SECRET_KEY="ランダムな長い文字列"
DATABASE_URL=postgresql://myapp_user:password@localhost:5432/myapp_db
同じく 雛形としてキー名だけのgit管理用の.env.example を作る
# .env.example
FLASK_ENV=
SECRET_KEY=
DATABASE_URL=
.gitignore に以下を追加して、.env は Git管理から外す。.env.exampleはGit管理のまま
.env
!.env.example

必要ライブラリのインストール

pip install python-dotenv Flask-SQLAlchemy Flask-Migrate psycopg2-binary
  • python-dotenv.env から環境変数をロードできる
  • Flask-SQLAlchemy … SQLを使わずにDBを操作できるSQLAlchemyのFlask連携版
  • Flask-Migrate … DB変更履歴を管理できるAlembicのFlask連携版
  • psycopg2-binary … PostgreSQL ドライバ
ライブラリを追加したら、requirements.txtに追加しておく
pip freeze > requirements.txt

.env の読み込みテスト

環境変数を正しく読み込めるか確認
tests/test_env.pyを作って以下を入力
import os
from dotenv import load_dotenv, find_dotenv


def test_env_loading():
    # プロジェクトルートの .env を読み込む
    dotenv_path = find_dotenv(".env", raise_error_if_not_found=True)
    load_dotenv(dotenv_path)

    # DATABASE_URL が設定されていることを確認
    assert os.getenv("DATABASE_URL") is not None
    # 必要なら値の形式チェック(postgresql://で始まっている文字列かチェック)
    assert os.getenv("DATABASE_URL").startswith("postgresql://")
pytestでチェック
python -m pytest

app.py を修正

 

import os
from dotenv import load_dotenv
from flask import Flask

# 1) .env をロード
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, ".env"))

app = Flask(__name__)
# 2) 設定を環境変数から
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me")
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# 3) ORM とマイグレーションを初期化
from flask_sqlalchemy import SQLAlchemy
from flask_migrate      import Migrate
db      = SQLAlchemy(app)
migrate = Migrate(app, db)

@app.route("/")
def index():
    return "Hello, myapp!"

if __name__ == "__main__":
    # 環境変数FLASK_ENVが"development"ならTrue、そうでなければFalse
    debug_mode = os.getenv("FLASK_ENV", "production") == "development"
    app.run(debug=debug_mode)

モデルファイルを用意

データベースに格納する情報のモデル(規格のようなもの)を設定する。

# models.py
from app import db
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    id       = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, nullable=False)
    pwd_hash = db.Column(db.String(128), nullable=False)


    # 平文のパスワードpwをハッシュ(暗号)化して保存
    def set_password(self, pw):
        self.pwd_hash = generate_password_hash(pw)

    # 入力された平文のパスワードpwが保存してあるハッシュと一致するかチェック
    def check_password(self, pw):
        return check_password_hash(self.pwd_hash, pw)
  • primary_key=True:主キーとして自動採番
  • unique=True:同じユーザー名を二度登録できない一意制約
  • nullable=False:空(NULL)を禁止
  • db.Integer:型指定。この場合は整数。

app.pyにモデルを読み込むコードを記入

import os
from dotenv import load_dotenv
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

# .env を読み込む
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, ".env"))

# Flask アプリの生成と設定
app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me")
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# ORM/マイグレーション用オブジェクトを初期化
db = SQLAlchemy(app)
migrate = Migrate(app, db)

#------------ここから追加分---------
# models.py を読み込む
import models
#------------ここまで追加分---------


@app.route("/")
def index():
    return "Hello, myapp!"

if __name__ == "__main__":
    # 環境変数FLASK_ENVが"development"ならTrue、そうでなければFalse
    debug_mode = os.getenv("FLASK_ENV", "production") == "development"
    app.run(debug=debug_mode)

Flask CLI の設定

ターミナルでflaskコマンドを使うために設定を読み込む。
.envに記入済みの場合は無くてもいいかもしれない
export FLASK_APP=app.py
export FLASK_ENV=development

マイグレーション初期化

データベースの履歴管理のためのmigrateフォルダの初期設定(ローカルでやる)
flask db init
プロジェクト直下に migrations/ ディレクトリが作成され、中に alembic.inienv.py などが生成される
初回マイグレーションの作成・適用(ローカルでやる)
flask db migrate -m "initial schema"
flask db upgrade
models.pyの定義に沿ってカラムが作られる。

VPS(本番環境)では、github経由で自動デプロイさせるので
アップグレードだけやる。
export FLASK_APP=app.py
flask db upgrade
これでモデルに沿ったカラムが作られる。

データベースが正しく作成されているかチェック

# myapp_dbのテーブル一覧を表示
psql -d myapp_db -c "\dt"

# myapp_dbのusersテーブルを表示
psql -d myapp_db -c "SELECT * FROM users;"
モデルで定義したテーブルが表示されれば成功

モデルが循環参照になっていてうまくいかない時

モデルの ForeignKey 定義に use_alter=True を指定すると、Alembic が
  1. テーブル本体をまず作成
  2. その後で ALTER TABLE … ADD CONSTRAINT … で FK を追加
という 2 ステップに分けてマイグレーションスクリプトを生成してくれる

CI/CD拡張

CIワークフロー拡張

  • flake8: コードスタイル&潜在的バグ検出
  • black –check: フォーマット準拠確認
  • mypy: 型チェック
  • pytest-cov: テスト実行+テストがコード全体のうちどれくらいをカバーしているか計測
    などを追加
name: CI

on:
  push:
    branches: [ develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false  # 並列ステップで最後まで実行

    steps:
      - uses: actions/checkout@v3

      # .env.example をコピーして .env を作成
      - name: Prepare .env for CI
        run: cp .env.example .env

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.x'
          cache: 'pip'

      - name: Install dependencies
        run: |
          pip install --upgrade pip
          pip install -r requirements.txt
          # テスト&Lint/型チェック用ツールを追加
          pip install pytest pytest-cov flake8 black mypy

      - name: Lint with Black
        run: |
          # コードフォーマット違反があると失敗
          black --check .

      - name: Lint with Flake8
        run: |
          # コーディング規約違反を検出
          flake8 .

      - name: Type-check with mypy
        run: |
          # 型チェックを実行
          mypy .

      - name: Run tests with coverage
        run: |
          # Coverage(テストがコード全体のどれくらいカバーしているか)を有効化してテスト実行
          python -m pytest --cov=. --cov-report=term-missing --cov-report=xml

      - name: Upload coverage report
        uses: actions/upload-artifact@v3
        with:
          name: coverage-report
          path: coverage.xml
ローカルにもチェックツールを導入
pip install flake8 black mypy
blackでフォーマット修正
# 今いるディレクトリの中身全てを修正する
black .
(任意)flake8の文字数制限を変更。
.flake8というファイルを作って以下を入力 79文字だと短いので100文字まで許容する
[flake8]
max-line-length = 100
exclude =
    venv/,
    migrations/versions/,
    .git,
    __pycache__,
    tests/
(任意)Mypy設定でマイグレーションやサードパーティファイルを除外
mypy.iniというファイルを作り以下を記入
[mypy]
# プラグインを指定
plugins =sqlalchemy.ext.mypy.plugin
# 外部ライブラリの import エラーを全部無視
ignore_missing_imports = true
# migrations と tests ではエラーを無視してもOK
[mypy-migrations.*]
ignore_errors = True
[mypy-tests.*]
ignore_errors = True

# models.py だけ name-defined を無視
[mypy-models]
disable_error_code = name-defined

CDワークフローの拡張

  1. マイグレーション実行 (flask db upgrade)
  2. ヘルスチェック (curl/health エンドポイントを叩く)
  3. ロールバック (git reset --hardsystemctl restartfailure() 条件で実行)
    を自動化する処理を追加
app.pyに以下のエンドポイントを追加
@app.route("/health")
def health():
    return "OK", 200

 

name: CI / CD to VPS

on:
  push:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    # (省略:既存の Test & Lint ステップ)

  deploy:
    name: Deploy to VPS
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Prepare SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          echo "${{ secrets.KNOWN_HOSTS }}" > ~/.ssh/known_hosts
          chmod 600 ~/.ssh/known_hosts

      # ◆ マイグレーション実行+ヘルスチェックを SSH 内でまとめて行う
      - name: Deploy & Migrate & Healthcheck
        run: |
          ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes -p 2222 youruser@YOUR_VPS_IP << 'EOF'
            set -eux

            cd ~/myapp

            # 最新コード取得
            git pull origin main

            # 仮想環境有効化
            source venv/bin/activate

            # パッケージ同期
            pip install -r requirements.txt

            # 1) マイグレーション実行
            flask db upgrade

            # 2) サービス再起動
            sudo systemctl restart myapp.service

            # 3) ヘルスチェック
            #    - HTTP 200 が返ってくるか確認。失敗したら以下でロールバック
            if ! curl --fail http://127.0.0.1/health; then
              echo "=== HEALTHCHECK FAILED: rolling back to previous release ==="
              # ロールバック:前のコミットに戻して再起動
              git reset --hard HEAD@{1}
              sudo systemctl restart myapp.service
              exit 1
            fi

          EOF

 

これで環境は整ったはず。

いよいよアプリ開発に挑む。

ABOUT ME
いなさく
住んでる家が崩れそうなので、建て替え費用をまかなうために 副業をがんばるサラリーマン
ブログランキング・にほんブログ村へ

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA