👻

CodeDeployを利用したときの連続デプロイには罠がある

2024/11/27に公開

はじめに

こんにちは。
株式会社ココナラ(以降、弊社と表記)のインフラ・SRE チームのR.Y.です。
今回はAWS CodeDeploy、AutoScalingグループによるEC2インスタンスを増やそうとした時にハマったことについて語りたいと思います。

出題編

システム構成とデプロイワークフロー

エラーの原因を特定する前に、まず弊社の一部のシステム構成とデプロイワークフローを紹介させてください。
デプロイツールのCircleCIを運用しており、
Githubへのコード更新をデプロイのトリガーとします。

  1. スクリプトdeploy.shでソースコードをAWSにアップロードして、CodeDeployで各EC2インスタンスにデプロイします(1枚目:青と緑の矢印)
  • aws deploy pushでコードをS3にアップロードします。
  • CodeDeployがコードをサービスインしていない側(コードが古い側)のデプロイグループにデプロイを開始します。
  • CodeDeployがS3にアップロードしたコードをダウンロードして、デプロイグループに所属する各EC2インスタンスにコードをデプロイします。
  1. 上記のデプロイが全インスタンスに成功したらスクリプト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側の「リビジョンの場所」に参照するetagaaexxxxxxxxxxxxx
  • S3側に保有しているetag6c6xxxxxxxxxxxxx

エラーメッセージの言う通りに、
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を進めれば何でも大丈夫です。
  • 手順に工夫することですぐ実行できます。

案2:aws deploy push--s3-locationオプションを毎回違う値を入れるようにします

毎回同じファイルパスでなくなって、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がズレる仕様/現象は注意していただきたいです。

最後に

私たちは"一人ひとりが「自分のストーリー」を生きていく世の中をつくる"というビジョンを掲げ、一丸となって日々の業務に取り組んでいます!
私がまだ入社して間もないですが、今回の事例のように、チャレンジできる日々を過ごさせていただいています。

少しでも興味が湧いた方、社内の雰囲気をもっと知りたいと感じた方、カジュアル面談もやっていますのでご応募お待ちしてます!
https://open.talentio.com/r/1/c/coconala/pages/70417

エンジニア採用ページもありますので、こちらもぜひご覧ください!
https://coconala.co.jp/recruit/engineer/

Discussion