はいどーもー!
Xイノベーション本部の宮澤響です!
本記事は電通国際情報サービス Advent Calendar 2021 2日目の記事です! 記念すべき1日目である昨日の記事は、佐藤太一さんの「テックブログ始めました。」でした!我々の記事をホストするためのサービスとしてはてなブログを採用した理由や、記事を執筆するにあたって利用しているツールについて分かりやすくまとめられていますので、ぜひご一読ください!
本記事では、「GitHub ActionsとAWS App Runnerを利用してBlue/Greenデプロイメントを実現してみた」というタイトルのとおり、GitHub ActionsとAWS App Runner(以下、App Runner)、それに加えて、Amazon CloudFront(以下、CloudFront)とAmazon Route 53(以下、Route 53)を利用してBlue/Greenデプロイメントを実現する方法を、サンプルコードとともにご紹介します!
- Blue/Greenデプロイメントって何?
- App Runnerって何?
- 事前に準備すること
- 実際にBlue/Greenデプロイメントしてみる
- GitHub ActionsのワークフローのYAMLファイルの改善方法
- おわりに
Blue/Greenデプロイメントって何?
Blue/Greenデプロイメントとは、本番環境と検証環境を交互に入れ替えることにより、Webアプリケーションなどに変更を加えるデプロイ手法の一つです。ざっくりとした流れとしては、「本番環境であるBlue」と「本番環境とほぼ同一の検証環境Green」の2つを用意した上で、以下の1.〜4.を繰り返すことになります。
Greenに変更を加えていく
問題がなければGreenを本番環境に、Blueを検証環境にチェンジする(デプロイ)
今度はBlueに変更を加えていく
問題がなければ再びBlueを本番環境に、Greenを検証環境にチェンジする(デプロイ)
メリットとしては、デプロイ時やロールバック時のシステムのダウンタイムを最小限にできることや、限りなく本番環境に近い検証環境でテストできることなどが挙げられます。 一方、デメリットとしては、アーキテクチャが複雑になることや、インフラ構築用のコードと実際のインフラ環境に矛盾が生じる(インフラ構築用のコード上でBlueを本番環境としていた場合、Greenが本番環境になっている間はインフラ構築用のコードと実際のインフラ環境が一致しない)ことなどが挙げられます。
App Runnerって何?
App Runnerとは、コンテナベースのAWSリソースの一つです。噛み砕いて言えば、Amazon ECS、AWS Fargate、ELBといった種々のリソースを全部まとめて裏でイイ感じにやってくれるものです。非常に手軽で簡単にWebアプリケーションをデプロイできる反面、制約も多いです。詳しくは公式ドキュメントを参照ください。
事前に準備すること
各種AWSリソースや、Blue/Greenデプロイメントを実現するためのGitHub Actionsのワークフローを作成します。最終的なアーキテクチャは下図になります。
App Runnerサービスを作成する
まずは、公式ドキュメントの手順に従い、サンプルアプリケーションがデプロイされているApp Runnerサービスを2つ作成します。
Prerequisitesの手順に従い、サンプルリポジトリを作成します。リポジトリ名は任意でOKです。
ステップ 1: App Runner サービスを作成するの手順に従い、App Runnerサービスを作成します。GitHub connectionsの接続名や環境変数
NAME
の値は任意でOKです。サービス名はそれぞれsample-service-blue
、sample-service-green
とします。また、デプロイ設定の部分で、sample-service-blue
のデプロイトリガーを手動
に、sample-service-green
のデプロイトリガーを自動
に、それぞれ設定してください。
何故片方を手動デプロイ、もう片方を自動デプロイにするかというと、自動デプロイの場合、指定したリポジトリ、ブランチのソースコードに変更を加える(=pushする)度に、App Runnerサービスにも変更が反映される(=最新のソースコードを基にアプリケーションがデプロイし直される)ためです。これにより、本番環境(手動デプロイ)に影響を与えることなく、検証環境(自動デプロイ)に変更を加えることが可能となります。ソースコードをpushするだけで最新のアプリケーションが自動でデプロイされるのは非常に手軽で便利ですね!
CloudFrontディストリビューションを作成する
次に、公式ドキュメントの手順に従い、CloudFrontディストリビューションを2つ作成します。基本的にはデフォルト設定のままで問題ありませんが、以下の項目はデフォルトから変更をお願いします。
- 2つのディストリビューション共通の設定
項目 | 値 |
---|---|
プロトコル | HTTPSのみ |
料金クラス | 北米、欧州、アジア、中東、アフリカを使用 (Blue/Greenデプロイメントには直接関係ありませんが、コストを抑えるためです) |
カスタムSSL証明書 | 任意の証明書 (本記事では example.com とします) |
- それぞれのディストリビューションで異なる設定
項目 | 1つ目のディストリビューションに設定する値 | 2つ目のディストリビューションに設定する値 |
---|---|---|
オリジンドメイン | sample-service-blue のデフォルトドメイン( https:// の部分は不要です) |
sample-service-green のデフォルトドメイン( https:// の部分は不要です) |
代替ドメイン名 | カスタムSSL証明書に対応する任意のドメイン名 (本記事では sample.example.com とします) |
なし |
説明 | sample-distribution-blue |
sample-distribution-green |
何故片方だけに代替ドメイン名を入力するかというと、CloudFrontのルールとして、複数のディストリビューションに同一の代替ドメインを同時に設定できないようになっているためです。つまり、代替ドメインは、常にルーティング先が本番環境になっている方のディストリビューションにのみ設定されるように、適宜付け替える必要があります。
Route 53のレコードを作成する
続いて、公式ドキュメントの手順に従い、Route 53のレコードを2つ作成します。
- 1つ目(Aレコード)
項目 | 値 |
---|---|
レコード名 | sample-distribution-blue の代替ドメイン名と対応する名称(本記事では sample とします) |
レコードタイプ | A のエイリアス |
トラフィックのルーティング先 | CloudFrontディストリビューションへのエイリアス > sample-distribution-blue のドメイン名 |
ルーティングポリシー | シンプルルーティング |
- 2つ目(TXTレコード)
項目 | 値 |
---|---|
レコード名 | Aレコードのレコード名の先頭に_ を付したもの(本記事では _sample とします) |
レコードタイプ | TXT |
トラフィックのルーティング先 | sample-distribution-green のドメイン名の末尾に. を付したもの |
ルーティングポリシー | シンプルルーティング |
何故2つ目のTXTレコードが必要になるかというと、CloudFrontディストリビューションを作成するの節で説明した代替ドメインの付け替えに必要になるためです。このレコードに、代替ドメインを設定しない方のディストリビューションのドメインを設定しておく必要があります。レコード名の先頭に_
を付けたり値の末尾に.
を付けたりするのは仕様です。詳しくは公式ドキュメントを参照ください。
GitHubのRepository secretsを設定する
ここからはGitHub側の準備になります。GitHub Actionsの利用に際して、AWSの認証情報や間接的に取得することが難しい値を、あらかじめRepository secretsに設定しておきます。公式ドキュメントの手順に従い、以下の4つを設定してください。
Name | Value |
---|---|
AWS_ACCESS_KEY_ID |
自身のAWSアクセスキーID |
AWS_SECRET_ACCESS_KEY |
自身のAWSシークレットアクセスキー |
AWS_ROUTE53_HOSTED_ZONE_ID |
Route 53のホストゾーンID |
AWS_ROUTE53_RECORD_NAME |
sample.example.com (Route 53のAレコードのレコード名) |
GitHub ActionsのワークフローのYAMLファイルを作成する
最後に、実際にBlue/Greenデプロイメントを実現する部分である、GitHub ActionsのワークフローのYAMLファイルを作成します。今回の構成でBlue/Greenデプロイメントの実現に必要な要素は、以下の4つです。
現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する
現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する
CloudFrontディストリビューションの代替ドメイン名を付け替える
Route 53のAレコードとTXTレコードのルーティング先を入れ替える
まずはこれらを、AWS CLIのコマンドを用いてGitHub Actionsのワークフローに落とし込みます。なお、以下の例では、release/〇〇
のようなタグのpushをBlue/Greenデプロイメントのトリガーとしています。
name: Blue/Green Deploy on: push: tags: - release/* jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name: Change Source Configuration of Current Service run: aws apprunner update-service --service-arn `本番環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name: Change Source Configuration of Next Service run: aws apprunner update-service --service-arn `検証環境側のApp RunnerサービスのARN` --source-configuration `設定ファイルのパス` # CloudFrontディストリビューションの代替ドメイン名を付け替える - name: Replace Alias run: aws cloudfront associate-alias --target-distribution-id `検証環境側のCloudFrontディストリビューションのID` --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name: Change Record Targets run: aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch `設定ファイルのパス`
このワークフローを実行することにより、現在の検証環境が本番環境となり、Aレコードのドメインからアクセス可能になります。一方、現在の本番環境は検証環境となり、ソースコードをpushするだけで最新のアプリケーションが自動でデプロイされます。
しかしながら、このままでは現在の本番環境がどちらかをその都度手動で調べたり、各リソースのARNやIDなどをコピペしたり、設定ファイルを自作したりする必要があります。当然、そんなことをしていては非常に面倒ですし、ヒューマンエラーも発生しやすくなってしまいます。
そのため、それらの処理も併せて自動化したものが以下になります。
name: Blue/Green Deploy on: push: tags: - release/* jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 # CloudFrontディストリビューションの一覧を取得し、その中から説明が`sample-distribution`で始まっているものの情報をファイルに書き込む # 代替ドメインが1個のものを現在の本番環境、0個のものを検証環境と判定する - name: Get Distribution List run: | aws cloudfront list-distributions | jq '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution"))' > distribution-list.json cat distribution-list.json | jq 'select(.Aliases.Quantity==1)' > current-distribution.json cat distribution-list.json | jq 'select(.Aliases.Quantity==0)' > next-distribution.json # 2つのCloudFrontディストリビューションのドメイン名を取得し、変数に代入する - name: Get Distribution Domains id: distribution-domains run: | echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$(cat current-distribution.json | jq -r '.DomainName')" echo "::set-output name=NEXT_DISTRIBUTION_DOMAIN::$(cat next-distribution.json | jq -r '.DomainName')" # 検証環境側のCloudFrontディストリビューションのIDを取得し、変数に代入する - name: Get Distribution ID id: distribution-id run: echo "::set-output name=NEXT_DISTRIBUTION_ID::$(cat next-distribution.json | jq -r '.Id')" # App Runnerサービスの一覧を取得し、ファイルに書き込む - name: Get Service List run: aws apprunner list-services | jq '.ServiceSummaryList[]' > service-list.json # 2つのApp Runnerサービスのドメインを取得し、変数に代入する - name: Get Service Domains id: service-domains run: | echo "::set-output name=CURRENT_SERVICE_DOMAIN::$(cat current-distribution.json | jq -r '.Origins.Items[0].DomainName')" echo "::set-output name=NEXT_SERVICE_DOMAIN::$(cat next-distribution.json | jq -r '.Origins.Items[0].DomainName')" # 2つのApp RunnerサービスのARNを取得し、変数に代入する - name: Get Service ARNs id: service-arns run: | echo "::set-output name=CURRENT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.CURRENT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" echo "::set-output name=NEXT_SERVICE_ARN::$(cat service-list.json | jq -r --arg domain ${{ steps.service-domains.outputs.NEXT_SERVICE_DOMAIN }} 'select(.ServiceUrl==$domain) | .ServiceArn')" # 2つのApp Runnerサービスの設定情報を取得し、デプロイトリガー部分を変更した上でファイルに書き込む - name: Get Service Config Files run: | aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=true' > current-service-config.json aws apprunner describe-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} | jq '.Service.SourceConfiguration | .AutoDeploymentsEnabled=false' > next-service-config.json # 指定したホストゾーンIDのRoute 53のレコードの一覧を取得し、指定したレコード名のものと、指定したレコード名の先頭に`_`を付したものの情報をファイルに書き込む # 2つのレコードの設定部分を変更した上でファイルに書き込む - name: Get Record Config File run: | aws route53 list-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --output json | jq --arg name ${{ secrets.AWS_ROUTE53_RECORD_NAME }} '.ResourceRecordSets[] | select(.Name | endswith($name+"."))' > record-list.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.CURRENT_DISTRIBUTION_DOMAIN }} 'select(.Type=="TXT") | .ResourceRecords[0].Value="\""+$domain+".\"" | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > txt-record.json cat record-list.json | jq --arg domain ${{ steps.distribution-domains.outputs.NEXT_DISTRIBUTION_DOMAIN }} 'select(.Type=="A") | .AliasTarget.DNSName=$domain+"." | {"Changes":[{"Action":"UPSERT"}+{ResourceRecordSet:.}]}' > a-record.json cat txt-record.json a-record.json | jq -s '.[0].Changes+.[1].Changes | {"Changes":.}' > record-config.json # 現在本番環境になっているApp Runnerサービスのデプロイトリガーを自動に変更する - name: Change Source Configuration of Current Service run: aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json # 現在検証環境になっているApp Runnerサービスのデプロイトリガーを手動に変更する - name: Change Source Configuration of Next Service run: aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.NEXT_SERVICE_ARN }} --source-configuration file://next-service-config.json # CloudFrontディストリビューションの代替ドメイン名を付け替える - name: Replace Alias run: aws cloudfront associate-alias --target-distribution-id ${{ steps.distribution-id.outputs.NEXT_DISTRIBUTION_ID }} --alias ${{ secrets.AWS_ROUTE53_RECORD_NAME }} # Route 53のAレコードとTXTレコードのルーティング先を入れ替える - name: Change Record Targets run: aws route53 change-resource-record-sets --hosted-zone-id ${{ secrets.AWS_ROUTE53_HOSTED_ZONE_ID }} --change-batch file://record-config.json # 作成したファイルを削除する - name: Delete Temporary Files if: ${{ always() }} run: rm distribution-list.json current-distribution.json next-distribution.json service-list.json current-service-config.json next-service-config.json record-list.json txt-record.json a-record.json record-config.json
実際にBlue/Greenデプロイメントしてみる
それでは、実際にBlue/Greenデプロイメントのワークフローを動作させ、環境の切り替わりを確認してみます。
現在の本番環境の状態を確認するために、
sample.example.com
にアクセスします。
サンプルアプリケーションの文字列がそのまま表示されます。現在の検証環境の状態を確認するために、
sample-distribution-green
のドメインにアクセスします。
本番環境と同一の画面が表示されます。
MESSAGE = "こんにちは, " + name + "!"
sample-service-green
へのデプロイが完了するのを待ってから、再度sample-distribution-green
のドメインにアクセスしてみます。
こんにちは
に更新されているため、検証環境には先ほどpushした内容が反映されていることが分かります。再度
sample.example.com
にアクセスしてみます。
Hello
のままであるため、先ほどのpushが本番環境には影響を与えていないことが分かります。release/v1.0.0
というタグをpushします。 GitHub Actionsのワークフローが正常に完了していれば成功です。
git tag release/v1.0.0 git push origin release/v1.0.0
- 再度
sample.example.com
にアクセスしてみます。
こんにちは
に更新されています。Blue/Greenデプロイメントにより、先ほどまでの検証環境が数十秒のうちに本番環境に切り替わったことが確認できました!
なお、確認は省略しますが、今回確認したBlueからGreenへの切り替えだけでなく、GreenからBlueへの切り替えも正常に動作します。
GitHub ActionsのワークフローのYAMLファイルの改善方法
GitHub Actionsのログ出力の制御
本記事にサンプルとして掲載したYAMLファイルによるワークフローを実行すると、Repository secrets以外の変数の値やAWS CLIのコマンドの実行結果がGitHub Actionsのログに出力されてしまいます。そのため、Publicなリポジトリで実行する場合には、ログ中での値のマスクやnul
へのリダイレクトなどを利用して、ログの出力を工夫する必要があります。
# 値のマスクの例 CURRENT_DISTRIBUTION_DOMAIN=$(cat current-distribution.json | jq -r '.DomainName') echo "::add-mask::$CURRENT_DISTRIBUTION_DOMAIN" echo "::set-output name=CURRENT_DISTRIBUTION_DOMAIN::$CURRENT_DISTRIBUTION_DOMAIN" # nulへのリダイレクトの例 aws apprunner update-service --service-arn ${{ steps.service-arns.outputs.CURRENT_SERVICE_ARN }} --source-configuration file://current-service-config.json > nul
OpenID Connectを利用したAWSリソースへのアクセス
GitHub ActionsからAWSリソースにアクセスする方法に関しては、OpenID Connectを利用する方法が先日発表されました。こちらの方法を利用すると、AWSアクセスキーIDやAWSシークレットアクセスキーを利用せずにAWSリソースにアクセスできるため、より安全性を高められます。詳しくは、公式ドキュメントや、テックブログ記事であるOpenID Connectを利用してGitHub ActionsからAWSリソースにアクセスするを参照ください。
CloudFrontディストリビューションのキャッシュパージ
Blue/GreenデプロイメントのワークフローのYAMLファイルとは別に、main
ブランチへのpushをトリガーとして検証環境側のCloudFrontディストリビューションのキャッシュをパージするワークフローのYAMLファイルを作成することにより、アプリケーションをデプロイし直す際にキャッシュによって古い情報が配信されることを防げます。
# キャッシュパージの例 NEXT_DISTRIBUTION_ID=$(aws cloudfront list-distributions | jq -r '.DistributionList.Items[] | select(.Comment | startswith("sample-distribution")) | select(.Aliases.Quantity==0) | .Id') aws cloudfront create-invalidation --distribution-id $NEXT_DISTRIBUTION_ID --paths "/*"
おわりに
本記事では、GitHub Actions、App Runner、CloudFront、Route 53を利用してBlue/Greenデプロイメントを実現する方法をご紹介しました。GitHubリポジトリにソースコードをpushするだけで検証環境にアプリケーションがデプロイされ、タグをpushするだけでBlue/Greenデプロイメントが完了するというのは、開発活動を進めていく上で非常に便利で快適です。機会があれば皆さんもぜひお試しください!
電通国際情報サービス Advent Calendar 2021 3日目となる明日の記事は比嘉康雄さんの「Geth(ゲス)はじめました」です!お楽しみに!
最後までお読みいただき、本当にありがとうございました!
執筆:@miyazawa.hibiki、レビュー:@sato.taichi (Shodoで執筆されました)