はじめに
前回→VPSを使ってウェブアプリを作りたい2(Github ActionsでCI/CD)
もう少し開発環境を整えていく。
- 環境管理:
.env
+python-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.ini
やenv.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 が
- テーブル本体をまず作成
- その後で
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ワークフローの拡張
- マイグレーション実行 (
flask db upgrade
) - ヘルスチェック (
curl
で/health
エンドポイントを叩く) - ロールバック (
git reset --hard
+systemctl restart
をfailure()
条件で実行)
を自動化する処理を追加
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
これで環境は整ったはず。
いよいよアプリ開発に挑む。
スポンサーリンク