CodeDeployを利用したときの連続デプロイには罠がある
はじめに
こんにちは。
株式会社ココナラ(以降、弊社と表記)のインフラ・SRE チームのR.Y.です。
今回はAWS CodeDeploy、AutoScalingグループによるEC2インスタンスを増やそうとした時にハマったことについて語りたいと思います。
出題編
システム構成とデプロイワークフロー
エラーの原因を特定する前に、まず弊社の一部のシステム構成とデプロイワークフローを紹介させてください。
デプロイツールのCircleCIを運用しており、
Githubへのコード更新をデプロイのトリガーとします。
- スクリプト
deploy.sh
でソースコードをAWSにアップロードして、CodeDeployで各EC2インスタンスにデプロイします(1枚目:青と緑の矢印)
-
aws deploy push
でコードをS3にアップロードします。 - CodeDeployがコードをサービスインしていない側(コードが古い側)のデプロイグループにデプロイを開始します。
- CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループに所属する各EC2インスタンスにコードをデプロイします。
- 上記のデプロイが全インスタンスに成功したらスクリプト
switch_bg.sh
でターゲットグループを切り替えます(2枚目:オレンジと赤の矢印)
- CodeDeployが成功したら
aws elbv2 modify-listener
でコード更新した側のターゲットグループをALBにぶら下げるようにします。 - コードデプロイ完了
どのようなエラーが発生したのか
今回は両環境にデプロイする必要がありますので、CircleCIのRerun機能で両環境にデプロイしました。
そしてAutoScalingグループでEC2インスタンスを増やそうとする時(後述)に、
CodeDeployのDownloadBundleの段階で3秒でエラーになりました。
詳細を見てみると、このようなエラーメッセージが表示されました。
構成図に表すと、CodeDeployにエラーが発生してしまいます。
また、EC2インスタンスがずっと新規作成→エラー→シャットダウンの繰り返しになってしまいます。
ここで着目したいのが、エラーメッセージに書いてあるetag
という単語です。
今回の作業において想定したデプロイ状況
作業前
説明の方便で仮に下記の状態とします。
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD | HEAD^ |
1回目のデプロイ
1回CircleCI経由でデプロイしたら、こうなります。
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD | HEAD+1 |
2回目のデプロイ
ですが、今回は修正のためにコードを両方にデプロイしなければなりません。
ここにCircleのRerun機能を使って、もう片方にもコードをデプロイします。
想定した結果は:
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD+1 | HEAD+1 |
もしAutoScalingグループでインスタンスを増減しなければ、ここで大団円です。
この文章を書く必要もありませんね。
EC2インスタンスを増加する場合
AutoScalingグループ
| → ASGに希望するEC2インスタンス数を指定します。
| → CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループに所属する各EC2インスタンスにコードをデプロイします。
| → ⚠️EC2インスタンス増加完了...と思って、例のエラーが発生しました。
謎解き編
問題の原因特定
じっちゃんの名にかけて!(いやいや)
CodeDeployのデプロイ詳細
こちらは実際に失敗したデプロイ詳細です。
注目していただきたいのが、
先ほど強調したetag
です。
リビジョンの詳細→リビジョンの場所に、S3へのリンクがあります。しかもこのリンクにetag
のパラメータがあります。
リンクをクリックして、S3にアップロードしたzipファイルの詳細を見ましょう。
S3のファイル
etagのパラメータがありました!
そしてさっきのエラーメッセージを見比べると、
- CodeDeploy側の「リビジョンの場所」に参照する
etag
はaaexxxxxxxxxxxxx
- S3側に保有している
etag
は6c6xxxxxxxxxxxxx
エラーメッセージの言う通りに、
S3のファイルパスがあっていますが、etag
が違います。
CodeDeployのリビジョン
このetag
ズレ現象に対する検証もAWSサポートへの問い合わせもしました。
全部書くとこの文章のボリュームが倍になってしまいますので今回は割愛させてください。
CodeDeployの「リビジョンの場所」はS3のファイルパスだけではなく、etag
まで参照します。
どこからetagがズレましたか
デプロイの流れに、aws deploy push
でコードをS3にアップロードするステップがあります。
しかもアップロードする時--s3-location
オプションは、ファイル名を構成する変数はコミットIDしかありません。
実はこのコマンドでコードをAWSにアップロードするたびに、etag
が発行されて、S3にアップロードするファイルのパラメータとして格納されます。
そしてこのetag
もコマンド結果の一部として帰ってきます。
AWS公式ドキュメント
現在の仕組みに同じコミットIDでもう1回aws deploy push
を実行すれば、
- S3のファイルパスは変更しません。
- でも新しい
etag
が発行されて、元のetag
が上書きされます。 - CodeDeployも新しい
etag
を参照するようになります。
つまり、「2回目のデプロイ=Rerun」する時に、
実際はこうなりました。
2回目のデプロイ前
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD | HEAD+1 |
CodeDeployが参照するリビジョン | aaexxxxxxxxxxxxx |
2回目のデプロイ後
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD+1 | HEAD+1 |
CodeDeployが参照するリビジョン | 6c6xxxxxxxxxxxxx | aaexxxxxxxxxxxxx |
そして、次にデプロイタスクを作成する時に使うコマンドaws deploy create-deployment
に、
etag
とS3のファイルパス両方指定されます。
これにより、
- A1側は正しいリビション(S3パス+
etag
)を参照しています。- A1側のAutoScalingグループによるインスタンス増加→CodeDeployは成功します。
- A2側は正しいS3パス+上書きされた
etag
を参照しようとします。ですがそのetag
はもう存在しません。これがエラーの原因です。- A2側のAutoScalingグループによるインスタンス増加→CodeDeployのエラーでずっと失敗します。
エラーの全容(赤い矢印部分)
結論
複数のデプロイグループに交代でデプロイすれば、
同じコミットIDをaws deploy push
したらetag
がズレて、
1回目のデプロイタスクaws deploy create-deployment
に指定したetag
がもう参照できなくなります。
ちなみに、
- もしデプロイグループが1つしかない場合だと、複数回の
aws deploy push
は問題ありません。 - AutoScalingグループでのインスタンス増加をしなければ、この
etag
ズレは発生しますが、CodeDeployがトリガーされないのでこれも問題ありません。
回避策
案1:違うコミットIDでデプロイします
手っ取り早い解決策ですので、今回はこちらを採用しました。
- プログラムの中身に関係ないファイル(READMEなど)に変更を入れてコミットIDを進めれば何でも大丈夫です。
- 手順に工夫することですぐ実行できます。
aws deploy push
の--s3-location
オプションを毎回違う値を入れるようにします
案2:毎回同じファイルパスでなくなって、etag
が上書きされません。まさに根本的な解決策です。
もちろんaws deploy create-deployment
のパラメータも合わせて修正する必要があります。
ですがデメリットもあります。
- S3のストレージを多く使ってしまいます。
- ファイル名に同じコミットIDが含めれば、zipファイルの中身が本当に同じかどうかを確認しにくいです。
- また、デプロイフローの改修であるので、検証するに時間がかかります。(今後これに十分検証して改修したいです)
1回目のデプロイ (変更なし)
Githubにコードを更新して、デプロイをトリガーする
↓
CircleCI
| → aws deploy push
でコード(HEAD+1)をS3にアップロードする
| → CodeDeployがコードをサービスインしていない側(A2)のデプロイグループにデプロイを開始します。
| → CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループA2に所属する各EC2インスタンスにコードをデプロイします。
| → CodeDeployが成功したらaws elbv2 modify-listener
でターゲットグループA2をALBにぶら下げるようにします。
| → A2側のコードデプロイ完了
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD | HEAD+1 |
2回目のデプロイ (コミットIDをさらに進む)
Githubにコードを更新して、デプロイをトリガーする
↓
CircleCI
| → aws deploy push
でコード(HEAD+2)をS3にアップロードする
| → CodeDeployがコードをサービスインしていない側(A1)のデプロイグループにデプロイを開始します。
| → CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループA1に所属する各EC2インスタンスにコードをデプロイします。
| → CodeDeployが成功したらaws elbv2 modify-listener
でターゲットグループA1をALBにぶら下げるようにします。
| → A1側のコードデプロイ完了
↓
AutoScalingグループでサービスインしてない側(A2)のインスタンスを増加させる
| → ASGに希望するEC2インスタンス数を指定します。
| → CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループA2に所属する各EC2インスタンスにコードをデプロイします。
| → A2側のEC2インスタンス増加完了
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD+2 | HEAD+1 |
A1側のインスタンスも増加する
ここは、またRerunしたら、新しいetag
がまた発行されて、A2側は大丈夫ですが、A1側は上書きされたetag
を参照することになってしまいます。
なので、ここは単純にターゲットグループを変えるだけで作業します。
aws elbv2 modify-listener
でターゲットグループA2をALBにぶら下げるようにします。
↓
AutoScalingグループでサービスインしてない側(A1)のインスタンスを増加させる
| → ASGに希望するEC2インスタンス数を指定します。
| → CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループA1に所属する各EC2インスタンスにコードをデプロイします。
| → A1側のEC2インスタンス増加完了
↓
作業完了!めでたし!
デプロイグループ | A1 | A2 |
---|---|---|
サービスイン | ⚪︎ | |
コードバージョン | HEAD+2 | HEAD+1 |
根本的な解決策
前述のように、
aws deploy push
の--s3-location
オプションを毎回違う値を入れるようにすればこの問題が発生しなくなります。
しかし細かく検証が必要ですので今後の課題になります。
また、--s3-location
オプションに変数をコミットIDしか入れないと、
etag
がズレる仕様/現象は注意していただきたいです。
最後に
私たちは"一人ひとりが「自分のストーリー」を生きていく世の中をつくる"というビジョンを掲げ、一丸となって日々の業務に取り組んでいます!
私がまだ入社して間もないですが、今回の事例のように、チャレンジできる日々を過ごさせていただいています。
少しでも興味が湧いた方、社内の雰囲気をもっと知りたいと感じた方、カジュアル面談もやっていますのでご応募お待ちしてます!
エンジニア採用ページもありますので、こちらもぜひご覧ください!
Discussion