GVATECHブログ

SPA×モノリポ構成での継続的デプロイ実装 - 完全自動化に向けた取り組み

に公開

はじめに

本記事では、SPA(Single Page Application)のプロダクト(モノリポ構成)において、デプロイ自動化を行なったのでその実装事例をご紹介したいと思います。

複数のコンポーネントが存在する構成で全ての要素のデプロイを完全に自動化しつつ、ダウンタイムが発生しないようデプロイ順序も制御する必要がありましたが、あまり参考になる記事が無かったので、どのような構成で解決したかご紹介出来ればと思います。

複数要素のデプロイ自動化とデプロイ順序の制御を考える上で1つの選択肢として参考になれば幸いです。

前提

プロダクトの構成

弊社では以下の技術スタックでSaaSプロダクトを開発しています。

技術スタック

  • フロントエンド: Vue.js 3、TypeScript
  • バックエンドAPI: Node.js、NestJS、TypeScript
  • バッチ処理: Node.js、TypeScript
  • データベース・ORM: PostgreSQL、Prisma ORM
  • インフラ・クラウド: AWS(ECS、Amplify、RDS..etc)

プロジェクト構成

project/
├── frontend/     # SPAフロントエンド
├── backend/      # REST API
├── batch/        # バッチ処理
└── infrastructure/ # インフラコード(ECSのタスク定義など一部のみ)

デプロイ自動化前の課題

継続的デプロイの構築を行う前は、以下のような課題を抱えていました。

運用上の課題
フロントエンド、バックエンドAPI、バッチ処理など、各コンポーネントを個別にローカルから手動デプロイしており、デプロイ中はブランチの切り替えが出来ず他の作業ができませんでした。
データベースマイグレーションは踏み台サーバーにSSHで接続して手動実行する必要がありました。

また、稀にですがエンジニアのローカル環境(Mac、Windows)の違いによるビルドエラーが発生していました。

品質・安全性の課題
ダウンタイムを発生させないためのデプロイ順序(マイグレーション → バックエンドAPI → フロントエンド)が決まっており、各エンジニアが手順書を確認しながら慎重に作業する必要がありました。
そのため、手動作業によるヒューマンエラーのリスクが常にありました。

また、リリース内容によっては通常とは異なる手順(フロントエンドを先にリリースしてからAPIをリリースするなど)が必要な場合があり、自動化の検討を複雑にしていました。

これらの課題がある中で、ちょうど技術的負債などの開発課題を解消を行う長期プロジェクトが進行していたため、その一環として継続的デプロイの構築に取り組むことにしました。

デプロイ自動化の考え方

せっかく開発課題の解消を目的としているのに中途半端に負債を残すのはもったいないと思い、可能な限り理想に近い形を目指すことにしました。

基本方針

  • メインケースである通常のリリース手順は完全に自動化する
  • 稀にある「通常とは異なるリリース手順」にも柔軟に対応できるようにする
  • デプロイにかかるエンジニアの工数を最小限にする
  • 可能な限りセキュアな構成にする

デプロイ自動化の構成

構成図

継続的デプロイ構成図

構成の大枠

AWSサービス中心の構成
GitHub Actionsは使わず、完全にAWSサービスに寄せた構成にしました。これは、サプライチェーン攻撃などのセキュリティリスクを最小限にするため、GitHub Actionsに対してAWSの権限を付与しないようにしたかったためです。
AWSのCodepipelineとCodebuildを使ったデプロイフローを整備することにしました。

デプロイ起点の設計
デプロイの起点は、mainブランチへのPRマージではなく、ローカルからCodePipelineを起動するスクリプトを実行する方式にしました。
通常のデプロイ手順以外にも対応できるよう、デプロイパイプライン開始の前にエンジニアがデプロイ手順を選択できるようにしたかったためです。

デプロイ種別の制御
スクリプトの引数としてデプロイの種別を選択できるようにしました。

  • default: 通常のデプロイ手順(マイグレーション → バックエンド → フロントエンドの順序)
  • backend_onlyfrontend_onlymigration_onlyなど: 個別コンポーネントのみをデプロイ

ほとんどのケースはdefaultでカバー出来ます。
defaultを引数で指定した場合は上述した順番に沿ってデプロイが実施されるためダウンタイムも発生しません。
通常以外の順番でデプロイする際は、エンジニアが順番を制御出来るよう各コンポーネントのデプロイを個別で実施します。
そのため多少手間になりますが、稀にしか発生しないケースなので大きなコストにはならないと考えました。

具体的な構成

AWS CodePipelineとCodeBuildを使用した以下のようなパイプライン構成になっています。

ざっくりデプロイフロー全体

  1. デプロイ対象の判定: frontend、backendなど複数のコンポーネントがあるため、毎回全てをデプロイしているとCodeBuildのコストが無駄にかさんでしまいます。そのため、前回デプロイ時にコミットハッシュを保存しておき、今回デプロイのコミットとの差分をチェックし各コンポーネントでの差分有無を判定します。差分があるもののみをデプロイ対象にします。
  2. 差分情報の共有: Codepipelineの環境変数を使って、各コンポーネントのデプロイ有無をデプロイフロー全体で共有します。
  3. 各種デプロイ: 差分のあるコンポーネントのみデプロイを実行します。
  4. 状態管理: デプロイ済みのコンポーネントのみコミットハッシュをパラメータストアに保存します。(次回の差分判定用)

パイプラインステージ

各ステージで行う処理は以下になります。
コードは主要な処理のみ一部抜粋しています。

1. 初期処理

前回デプロイしたコミットハッシュを取得し差分チェックを行います。差分があるもののみデプロイ対象として次のステージに変数で共有します。

# パラメータストアから前回デプロイ時のコミットハッシュ取得
aws ssm get-parameter --name "last-deploy-commit"

# 各コンポーネントの差分チェック実行
# - 差分チェックはコンポーネント毎に行うため、差分チェックの対象ファイルパスは別途定義
git diff --name-only $LAST_COMMIT $CURRENT_COMMIT

# 差分チェック結果を変数としてエクスポート
# - コードは割愛

2. ビルド(並列実行)

以下の処理を並列で実行します。

  • backend、batchのイメージビルド + ECRへの登録
  • frontendのビルドとCodepipelineのアーティファクトとして登録

もちろん初期処理ステージで差分ありと判断されたもののみ処理します。

# Docker イメージビルド(Backend, Batch) + ECRへの登録
docker build -t $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG .
docker push $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG

# 静的ファイルビルド(Frontend) + アーティファクトとして登録
npm ci && npm run build

3. DBマイグレーション

バックエンドなどのデプロイ前にマイグレーションを実行します。

# マイグレーション実行
npx prisma migrate deploy

4. バックエンドデプロイ

バックエンドのデプロイを実行します。
デプロイが完了する前に次のフロントエンドデプロイが始まってしまうとダウンタイムが発生してしまうため、aws ecs waitでECSのローリングアップデートが終わるまで待ちます。

既存のデプロイ方式がECSのローリングアップデートであり、一旦そこは変えたくなかった(継続的デプロイ構築の不確実性を増やしたくなかった)のでBule/GreenデプロイになるCodeDeployは使わずCodeBuildからaws cliでデプロイする形にしました。

# ECS サービス更新(Backend, Batch)
aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME
# ECS サービス更新が終わるまで待機
aws ecs wait services-stable --cluster "$ECS_CLUSTER_NAME" --services "$ECS_SERVICE_NAME"

5. フロントエンドデプロイ

フロントエンドのデプロイを実行します。
Amplifyにデプロイします。
※コードは割愛。amplify cliのドキュメントをご参照ください。

6. 最終処理

コンポーネント毎(frontend、backendなど)に最後にデプロイしたコミットのハッシュをパラメータストアに保存します。
これを次回デプロイの初期処理で取得し、差分チェックに活用します。

# デプロイ結果確認後、成功したコンポーネントのコミットハッシュを更新
aws ssm put-parameter --name "/continuous-delivery/last-deploy-commit" --value $UPDATED_COMMIT_JSON

差分検出とコミットハッシュ管理

パラメータストアで各コンポーネントのデプロイ済みコミットハッシュを以下のJSON形式で管理しています。

{
  "backend": "a1b2c3d4e5f6",
  "frontend": "f6e5d4c3b2a1", 
  "batch": "1a2b3c4d5e6f",
  "migration": "b1a2c3d4e5f6"
}

通常とは異なる手順でデプロイする際に、1つのコンポーネントをデプロイした際にすべてのコミットハッシュが更新されてしまうと、次にデプロイするコンポーネントの差分検出ができなくなってしまうため、コンポーネント毎に分けて管理しています。

Slack通知

デプロイの開始時、成功時、失敗時には、チームのSlackチャンネルに自動通知が送信され、チーム全体でデプロイ状況を把握できるようになっています。

エンジニアの利用方法

エンジニアがデプロイする際の操作は非常にシンプルです。
環境、デプロイ種別を指定しコマンドを実行するだけです。
あとはslackで通知が来たら動作確認します。

通常のデプロイ

# dev環境への通常デプロイ
./start_deploy.sh dev default

# stg環境への通常デプロイ  
./start_deploy.sh stg default

# prod環境への通常デプロイ
./start_deploy.sh prod default

個別コンポーネントのデプロイ(通常とは異なる順序での制御)

# バックエンドのみ先にデプロイ
./start_deploy.sh prod backend_only

# その後、マイグレーションを実行
./start_deploy.sh prod migration_only

# 最後にフロントエンドをデプロイ  
./start_deploy.sh prod frontend_only

dev環境でのブランチ指定デプロイ

dev環境は開発中のブランチの動作確認を自由に出来る運用にしているのでブランチ指定もできるようにしました。

# feature ブランチを dev 環境にデプロイ
./start_deploy.sh dev default feature/new-feature

この構成の効果

工数削減とエラー排除

  • デプロイ自動化: エンジニアによるローカルPCでのビルドや踏み台サーバーへのアクセスが一切不要になりました
  • ヒューマンエラーの排除: 手動でのデプロイ作業がなくなり、手順ミスによるデプロイ失敗のリスクが無くなりました
  • 環境差異の解消: ローカル環境に依存しないAWS上でのビルドにより、環境差異によるエラーが発生しなくなりました
  • デプロイ中の時間の有効活用: デプロイ中にローカルで他の実装などが出来るようになりました

柔軟性の確保

  • メインケースの自動化: 通常のデプロイフローは完全自動で実行されます
  • 例外ケースへの対応: スクリプトの引数指定によりデプロイ順序を柔軟に制御することも可能になりました

その他

  • 差分デプロイ: 変更のあったコンポーネントのみが自動的に選択されてデプロイされるためリソースを効率的に利用できるようになりました

残課題

現段階では、パイプラインの途中で失敗した場合の対応はエンジニアが手動で行う運用になっているため、ロールバックの処理もうまく自動化出来ると良いなと考えています。

まとめ

継続的デプロイの導入により、エンジニア個々人がローカルPCで行っていたデプロイ作業を全てAWS上に移行することができました。

モノリポなど複数のデプロイ対象がある構成では、自動化する上でデプロイの順番にも注意が必要ですが、メインケースを特定してそこは完全に自動化しつつ、他のケースにも対応できる柔軟性を確保できたことは良い結果だったと考えています。

これまではデプロイ作業が億劫に感じられることもありましたが、現在はストレスなくデプロイできるようになり、開発・リリースサイクルの効率化に貢献出来たかなと思います。

モノリポ構成での継続的デプロイ実装を検討されている方の参考になれば幸いです。

GVATECHブログ
GVATECHブログ

Discussion