CodeDeploy良い!
ですが、注意すべき点もあります。以下に書いたようにCodeDeployに自動リストア機能やデプロイ失敗時に対象インスタンスにはELBからのトラフィックをやめるような機能はありません。
CodeDeployでデプロイに失敗した場合の挙動を確認する
また、デプロイ実施中(ファイルのコピーやWebサーバーの起動、停止)にELBからのトラフィックを止めるような機能もありません。
じゃあ任意のタイミングでELBやオートスケールから外したり、登録したりすれば良いのねということなのでAWS CLIなど使えばいいわけですが、全体で使える汎用的なのないかなーと思ってたらAWS公式のサンプルがあり使えそうでした。
awslabs/aws-codedeploy-samples
上記を使えばデプロイ失敗時にインスタンスにトラフィックが流れないようにすることができそうで、実際に使ってみたのでメモ。
結論
- awslabs/aws-codedeploy-samplesのスクリプトを使えばデプロイ失敗時にELBからデプロイ失敗インスタンスにトラフィックが流れることを防ぐことができる
- スクリプトがかなり汎用的かつ各処理時の標準出力もしっかり書かれているので自分で独自で作るよりこちらを使ったほうがよさそう
利用条件
- AWS CLIのバージョンが 1.3.25以上(20150822現在)
- 以下のアクセス権限許可があるinstance profileが対象EC2に設定されていること(IAMロール)
elasticloadbalancing:Describe* elasticloadbalancing:DeregisterInstancesFromLoadBalancer
elasticloadbalancing:RegisterInstancesWithLoadBalancer
autoscaling:Describe*
autoscaling:EnterStandby
autoscaling:ExitStandby
autoscaling:UpdateAutoScalingGroup
ポリシーでいうと以下のような感じ。
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"elasticloadbalancing:Describe*",
"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
"autoscaling:Describe*",
"autoscaling:EnterStandby",
"autoscaling:ExitStandby",
"autoscaling:UpdateAutoScalingGroup"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
使いかた
git clone https://github.com/awslabs/aws-codedeploy-samples.git
してload-balancing/elb
配下の
- common_functions.sh
- deregister_from_elb.sh
- register_with_elb.sh
を自分のプロジェクトの場所にコピーしてappspec.ymlを以下のように書いて処理の最初と最後に実行するようにすればOK
version: 0.0
os: linux
files:
- source: /
destination: /tmp/elb-test
hooks:
ApplicationStop:
- location: deregister_from_elb.sh
- location: stop_httpd.sh
ApplicationStart:
- location: start_httpd.sh
- location: register_with_elb.sh
なお、オートスケールにELBが紐付いていない場合にはcommon_function.sh
のELB_LIST
という変数を編集し、register,deresiterしたいELB名を追加する必要があります。(こちらは試していません)
スクリプトを確認してみる
使う前にスクリプトの内容をざっくり確認してみました。
すべての動きを試したわけでなく、シェルスクリプト読んだだけなので間違ってたらすいません。ざっくりだと
- オートスケールグループならデプロイ時に対象インスタンスをスタンドバイ状態にし、デプロイが終わったらスタンドバイ状態を終了させて復帰
- オートスケールグループでないなら対象インスタンスをELBからderegisterして、デプロイが終わったらregisterする
ということのようです。オートスケール時の挙動は確かにそうなってました。
スタンドバイ状態については以下に記載があり、ELBからのトラフィックからは流れませんが、オートスケールグループのままとなるため、デプロイ失敗などの際にもインスタンスが増えるなどはありません。
【AWS発表】Auto Scaling アップデート - ライフサイクル管理、スタンバイステート、デタッチインスタンス
なお
もし、Auto Scaling Groupに関連付けされたElastic Load Balancerがあれば、スタンバイ状態へ遷移により、ロードバランサからインスタンスを登録解除します。ロードバランサのConnection Draining機能を有効にしている場合、トラフィックが停止するまで、遷移が有効になりません。これには時間がかかる場合があります。
とのことでConnectionDraining使っている場合には注意のようです。(ConnectionDrainingってなんだろう。。。)
deregister_from_elb.sh
オートスケールグループに所属している場合
- インスタンスID取得
- オートスケールグループに属しているか確認
- 属していればAWS CLIのバージョン確認
- 問題なければ
aws describe-auto-scaling-instances
でLifecycleStateの状態を取得 - LifecycleStateの状態がStandbyかPending:Wait以外なら
autoscaling describe-auto-scaling-groups
でMinSize, DesiredCapacityを取得する -
aws autoscaling update-auto-scaling-group
でminsizeを一つ下げる -
aws autoscaling enter-standby
で対象インスタンスをスタンバイ状態にする。なお--should-decrement-desired-capacity
でdesired-capacityオプションを付与しているのでdesireも一つ減る。スタンバイへの移行(InServiceでなくなる)まで待つ
オートスケールグループに所属していない場合
- インスタンスID取得
- ELB_LISTにELB名が定義定義されているか確認
- 定義されていればそれが本当に存在するか
aws elb describe-load-balancers
で確認 - 定義されていれば
aws describe-instance-health
で対象のインスタンスが対象のELBに紐付いてるか確認 - 問題なければ対象のインスタンスをELBにderegisterする
- 上記ををELB_LIST分繰り返し
- 全てのELBへのregisterが終わるまで待つ
- 開始時間と終了時間を出力
register_with_elb.sh
オートスケールグループに所属している場合
- インスタンスID取得
- オートスケールグループに属しているか確認
- 属していればAWS CLIのバージョン確認
- 問題なければ
aws describe-auto-scaling-instances
でLifecycleStateの状態を取得 - LifecycleStateの状態がIn ServiceかPending:Waitなら何もしない。それ以外であれば
aws autoscaling exit-standby
でスタンバイ状態をやめる
オートスケールグループに所属していない場合
- インスタンスID取得
- ELB_LISTにELB名が定義定義されているか確認
- 定義されていればそれが本当に存在するか
aws elb describe-load-balancers
で確認 - 定義されていれば
aws describe-instance-health
で対象のインスタンスが対象のELBに紐付いてるか確認 - 問題なければ対象のインスタンスをELBにregisterする
- 上記ををELB_LIST分繰り返し
- 全てのELBへのregisterが終わるまで待つ
- 開始時間と終了時間を出力
既存のオートスケールグループのインスタンス群にデプロイする時に失敗する場合
以下の手順で実施。
- オートスケールグループで2つのインスタンスを作成
- 成功するリビジョンのアプリをデプロイ(OK-app)
- その後失敗するリビジョンのアプリをデプロイ(NG-app)
「3」ではstart_httpd.sh
で終了ステータスコード1を返すようにしてみました。
今回紹介したスクリプトを使ったような形で制御をしていない場合、「3」の状態でオートスケールグループにNG-appが存在し、かつELBからのトラフィックも流れてしまう状態でした。また、特に何も制御していないのでデプロイ中も対象インスタンスへのトラフィックが流れます。
今回のスクリプトを使ってみると以下の状態になり、デプロイに失敗したインスタンスにはトラフィックが流れないようになりました。
- デプロイに失敗したインスタンスはELBから外れている
- デプロイに失敗したインスタンスはrunningのままで起動している
- デプロイに失敗したインスタンスはオートスケールグループのLifecycleでStandby状態となる
この状態で再度OK-appのリビジョンのアプリをデプロイすればすべてのサーバーがOK-appの状態にリストアし、ELBでもInServiceとなり元の状態に戻すことができました。
最新のデプロイが失敗した状態でオートスケールによってEC2が起動した場合
これは以前書いた記事のように特に何もしなくてもCodeDeploy側で成功したリビジョンのアプリを新規に起動するインスタンスには配布するので問題ないです。
オートスケール新規起動時に失敗した場合
以下で実験。
- オートスケールグループを作成
- 対象サーバーにsshログインして適当なファイルを作成する
- 「2」で作成したファイルがある場合のみ成功するデプロイを行う(NG-app)
- オートスケールグループのmin,max,desireインスタンス数を1増加
今回紹介したスクリプトを使ったような形で制御しないとELBから新規に起動するインスタンスへのトラフィックは流れませんが、インスタンスの起動、停止が無限ループします
じゃあ今回紹介するスクリプトを使うとどうなるのかということで試してみました。
結果としては起動したインスタンスにCodeDeployによるデプロイを実施しますが、成功しないため、オートスケーリンググループのPending:Waitの状態となり、一定時間経過するとshutdownし、再度インスタンスが起動し・・・ということでこちらも無限ループになりますね。まあ、あまりないケースでしょうし、NG-appに不要なトラフィックが流れるわけではないので、オートスケーリングの通知をしっかりしておけばある程度は対応できそうかなと思います。