クラウドインフラ本部ネットワークサービス部のid:uwenoです。
今回はサービス開発の方法論であるThe Twelve-Factor App について紹介しつつ、 これまで本連載で紹介してきた私たちのチームが行っている品質向上のための取り組みについて振り返ります。
なお、前回は【システム開発戦略編 その3】モジュラーモノリス - FJCT Tech blogでした。
The Twelve Factor App
The Twelve-Factor Appは、アプリケーションの開発と運用を効率化し、移植性、スケーラビリティを最大化し、継続的デプロイを可能にするための方法論です。 このドキュメントはHerokuの創始者であるAdam Wiggins氏によって作成されました。 このドキュメントはインターネット上に公開されており、誰でもアクセスできます。 分量は少なめで日本語訳も整備されているので、まだ読んだことのない方はぜひ原本をご参照頂ければと思います。
その名の通り、Twelve-Factor Appは12の方法論から成り立っています。 以降では12の方法論についてそれぞれ簡単にまとめ、私たちが開発しているPythonを利用したアプリケーションでの適用事例を紹介します。 また、それぞれの項目において、これまでの連載との関連性について振り返ります。
1.コードベース
ポイント
- コードベースはバージョン管理する
- 1アプリケーションに対して1リポジトリの構成にする
- 1つのコードベースから複数環境へデプロイ可能にする
私たちのアプリケーションはモジュラーモノリスで開発されており、コードベースは1つのGitリポジトリにまとまっています*1。
また、完全に同一のコードベースを開発、検証、本番といった複数の環境にデプロイしています*2*3。
2.依存関係
ポイント
- 実行に必要な依存関係は、パッケージ管理システムなどを活用して明示的に宣言する
- システムレベルでインストールされるパッケージが存在することに依存しない
これまでの連載でも度たび登場しましたが、私たちのアプリケーションではPoetryを使ってパッケージを管理しています。 アプリケーションはDockerコンテナ中で実行されるため、実行環境にインストールされているパッケージには依存しません。
3.設定
ポイント
- 環境ごとの個別の設定は環境変数に格納する
- 環境依存の設定をコードベースにハードコーディングしない
私たちのアプリケーションでは、Pydantic Settingsを活用して、環境変数、または.env
ファイルの内容から実行時にアプリケーションへ設定を渡すようにしています。
原文では、以下の理由から設定ファイルより環境変数の方が推奨されています。
- 誤ってチェックインしてしまう可能性がある
- ファイルが異なる場所・形式に分散してしまう傾向がある
- ファイルが言語またはフレームワーク固有のものになりがち
しかし私たちは以下の理由から、環境変数ではなくファイルを用いて設定を管理することにしました。
「誤ってチェックインしてしまう可能性がある」については、むしろ私たちの考え方は逆で、クレデンシャルを含まない部分については、チェックインすべきだと思っています。 私たちは当初純粋に環境変数のみで管理していましたが、環境変数の定義が各ホストに点在することで、かえって環境ごとの差分を管理することが難しくなっていました。 集中管理を実現するために、アプリケーションのコードベースとは異なる専用のGitリポジトリで設定を管理することにしました。
「ファイルが異なる場所・形式に分散してしまう傾向がある」という問題については、環境ごとに単一の .env
ファイルで設定を管理することでデメリットを回避しています。
また、「ファイルが言語またはフレームワーク固有のものになりがち」という点についても、言語やライブラリに依存しない汎用的な .env
形式であれば問題になりません。
この .env
ファイルをもとに、各実行環境でDocker SwarmのConfigを作成しています。
このconfigは一意識別子を付けて管理しており、環境設定に変更があった場合には新しいconfigを作成しています(設定を管理するGitリポジトリで変更がマージされると、CIパイプライン上で自動実行されます)。
このconfigの利用方法は「ビルド、リリース、実行」の節で後述します。
4.バックエンドサービス
ポイント
- ネットワーク越しに利用するサービスはアタッチされたリソースとして扱う
- 「設定」によってリソースを切り替えられるようにする
私たちのアプリケーションでは、データベースやメッセージングミドルウェアの接続情報は全て前述の「設定」で管理しています。 また、社内SNSへの通知や他の社内サービスとの連携などで、http(s)経由でWeb APIを利用する箇所も全て個別のリソースとして管理しており、「設定」でバックエンドを切り替えることができるようにしています。
5.ビルド、リリース、実行
ポイント
- 「ビルド」「リリース」「実行」の3つのステージでコードベースをデプロイする
私たちのアプリケーションでは、GitLab CI上で「ビルド」が行われ、Dockerイメージという形で生成しています。
「ビルド」で生成したDockerイメージと、「設定」の節で生成したconfigの組が「リリース」となります。
Dockerイメージのバージョン(タグ名)と、.env
ファイルにマウントするconfigの一意識別子を記載したdocker-compose.yml
ファイルが「リリース」の実態により近いと思います。
docker stack deploy
コマンドで docker-compose.yml
の構成を作成することが初回の「リリース」に当たります。
初回以降は、 docker service update
によりイメージ・configを差し替えることによって「リリース」が行われます。
また、docker service rollback
により以前の「リリース」にロールバックできます。
これら「リリース」は宣言的な操作で、実際にプロセスを作る「実行」とは分離しています。 「実行」の実際はDocker Engineによって処理されるため、開発者が介入することはありません(リリースと実行の分離)。 例えばコンテナを立ち上げるのも、異常終了したコンテナを再起動するのも、Docker Engineが自動で処理します。
なお、このデプロイ作業はCI/CD上でも同等の操作ができます*4。
6.プロセス
ポイント
- アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する
- 永続化するデータはステートフルなバックエンドサービスに保存する
私たちのアプリケーションは、HTTPリクエストを受け付ける1種類のウェブアプリケーション(以下WebApp)と、非同期タスクを実行する複数種類のワーカーから構成されています*5。 これらのプロセスは、データベースやメッセージングミドルウェアからその都度必要なデータを読み書きしており、ローカル(メモリやディスク)にステートを一切保持しないようになっています。
7.ポートバインディング
ポイント
- ポートバインディングによって外部にサービスを公開する
- 外部のWebサーバなど依存せず、アプリケーション自身がポートを公開する
私たちのアプリケーションでは、WebAppコンテナがGunicornを用いてFastAPIのアプリケーションを Docker Network内部に公開しています。
なお、外部からのアクセスには別途HAProxyなどを経由します。
このHAProxyは過去記事で紹介したBlue/Greenを切り替えるためのHAProxyとは役割が異なり、HTTPリクエストのURLのパターンとリダイレクト先のサービスをマッピングするために利用しています。
典型的な利用例としては、UIを表示するリクエストは静的コンテンツを配信するコンテナへリダイレクトし、APIへのリクエストはWebAppコンテナへリダイレクトしています。
ワーカープロセスは外部にサービスを公開しておらず、自分からメッセージングミドルウェアへコネクションを張ってタスクを待ち受けます。
- 説明のために作成した概略図であり、実際の構成とは異なる部分もあります
- エンドユーザが私たちのシステムに直接リクエストすることはなく、他の社内システムなどを経由します(割愛の部分)
8.並行性
ポイント
- プロセスをスケールアウトできる構成にする
私たちのアプリケーションでは、WebAppコンテナ、およびほとんどのワーカーが複数プロセス同時に起動しています。 これは、Docker Swarmのreplicated (複製)サービス モデルによって実現されています。 あるユースケースが複数あるWebAppやワーカーのうちどれによって処理されても、同じ結果となることが保証されています。 なお、ワーカーによってはその性質により高々1つしか起動できないもの(同時に起動すると障害になりえるもの)もありますが、 そのコンテナの数については、正常性確認くんで監視しています*6。
9.廃棄容易性
ポイント
- アプリケーションをすばやく起動する
- アプリケーションをすばやく、安全に終了する
WebAppとワーカーがそれぞれ単独での起動に要する時間は数秒程度です。
WebAppがhttpリクエストを処理している最中に異常が発生した場合、Gunicornによってグレースフルに内部プロセスがシャットダウンします(実行中の他のリクエストに影響を与えません)。 ワーカーがタスクを処理している最中に異常が発生した場合、処理中のメッセージをメッセージングミドルウェアから削除することなくワーカープロセスがシャットダウンします(処理中のタスクが不意に消失しません)。
10.開発/本番一致
ポイント
- 開発環境で編集したコードをはなるべく早く本番環境にデプロイする
- 開発者と運用者は同じメンバーで行う
- 開発環境と本番環境で同じバックエンドサービスを使う
私たちは現在週に1回のペースで定期的にリリースしています(前の週での変更を、次の週にリリースします)。 このスケジュールが守られた場合、コードが変更されてから本番環境にリリースされるまでのリードタイムは平均で10日程度です。
なお、検証環境についてはmainブランチに変更が加えられると同時に、CI/CDの仕組みによってデプロイされます。 本番環境についても全く同じ仕組みでデプロイしているので、本番環境のリリースが週に1回のペースなのは技術的な制約によるものではありません。 本番リリースの頻度は運用上の諸般の要素を考慮して、現状のようになっています*7。
諸般の事情でリリースをスキップすることもあるため、ギャップがより伸びる場合もあります。
私たちのアプリケーションは、開発と運用を同じメンバー(id:rtagami、id:a8544、id:uweno)で行っています。
開発環境と本番環境は、バックエンドサービスまで含めてできる限り同じ構成にしています。
11.ログ
ポイント
- ログは標準出力に書き出す(ログファイルに書き込んだり、転送先をアプリケーションで指定したりしない)
- ログの出力先などの設定は、実行環境のログルーターで行う
WebApp、ワーカーではPythonの標準のlogging機能を用いてイベントログを標準出力に書き出しています。 各プロセス(コンテナ)で出力されたログは、ログルータであるfluentdを経由してElasticSearchに送られます。 ElasticSearchに蓄えられたログは、ログの可視化やアラート通知に利用されています。
12.管理プロセス
ポイント
- 管理タスクは1回限りのプロセスとして実行する
- 管理プロセスはアプリケーションが実行されるのと同じ環境で実行する
私たちのアプリケーションで必要になる管理タスクは、現時点ではデータベースとMQのマイグレーションのみです。 このマイグレーションはデプロイ時にサービスの1つとして実行しています*8。
マイグレーションに利用するスクリプトは、WebAppやワーカーと同じコードベースで管理されており、同一のコンテナイメージから起動できます。
WebAppやワーカーはアプリケーションの実行中はプロセスが動き続けますが、マイグレーションコンテナはデプロイ時に起動し、スクリプトが正常終了するとコンテナは以後起動しなくなります。 Docker の定義でいえば、restart_policyをon-failureにしています。
まとめ
今回はThe Twelve-Factor Appについて紹介し、私たちの品質への取り組みと今までの連載との関連について振り返りました。 アプリケーションの開発、構成時にTwelve-Factor Appとなるよう意識することで、アジリティの高い開発・運用が期待されます。 また、Twelve-Factor Appを実現するためには、CI/CDやDevOpsの導入も不可欠です。 CI/CDやDevOpsを導入したいけど、社内に環境がない! という方はぜひニフクラの提供するGitLabを導入してみてはいかがでしょうか。
ここまで全23回にわたってお送りしてきた当連載も今回で最終回となります。
初回にお示ししました通り、私たちは以下のモチベーションで本連載を連載を続けてきました。
- (お客様に対して)品質維持の取り組みをご紹介する事で、安心感を持ってサービスをご利用いただきたい。
- (開発者コミュニティの皆様に対して)プロダクト開発で得た知見を記事化する事で、開発者コミュニティに価値を提供したい。
- (学生の皆様に対して)弊社の開発の現場を知っていただく事で、就職活動の検討材料にしていただきたい。
ニフクラ、FJcloud-Vのお客様、開発者の皆様、就職活動中の皆様にとって、この連載が有意義なものになりましたら幸いです。 これまで本連載を読んでくださった皆様、ありがとうございました。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編
- システム開発戦略編
*1:【システム開発戦略編 その3】モジュラーモノリス - FJCT Tech blog
*2:【CI・CD戦略編 その4】検証環境への自動デプロイ - FJCT Tech blog
*3:【CI・CD戦略編 その5】プロダクション環境への週1デプロイ - FJCT Tech blog
*4:【CI・CD戦略編 その4】検証環境への自動デプロイ - FJCT Tech blog
*5:【CI・CD戦略編 その4】検証環境への自動デプロイ - FJCT Tech blog
*6:【CI・CD戦略編 その3】デプロイ後の正常性確認 - FJCT Tech blog
*7:【CI・CD戦略編 その5】プロダクション環境への週1デプロイ - FJCT Tech blog
*8:【CI戦術編 その1】 alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止 - FJCT Tech blog