CodeDeployで構築するAutoScalingに追従可能なデプロイ環境

こんにちは。新事業創造部インフラチームの光野(kotatsu360)です。

先日、VASILY時代1から長らく使われていたCapistranoによるデプロイを見直し、CodePipeline+CodeDeployによるデプロイフローを導入しました。

CodeDeployはEC2 AutoScalingとよく統合されており、この新しいデプロイフローによって最新のアプリケーションコードをどう反映するかという悩みから開放されました。この記事ではそのフローについて設計と運用を交えつつ紹介します。

AWS CodePipeline / AWS CodeDeploy

CodePipeline

AWS CodePipelineはアプリケーションのCI/CDパイプラインを作るためのサービスです。 SourceBuildTestDeployの4ステージに対して1つ以上のアクションを割り当てることで、任意のパイプラインを構築します。

設定項目が多く初めは戸惑いますが、全てのステップを使う必要はありません。 実際、今回構築したパイプラインでもSourceDeployのみを使っています。BuildTestに相当する部分は既存のCIで事足りるためです。

CodeDeploy

AWS CodeDeployはアプリケーションの自動デププロイを管理するためのサービスです。 エージェント式になっており、Amazon EC2だけでなくオンプレミスに対してもデプロイが可能です2

デプロイの具体的な作業は、実行可能な形式で置いておきます。

# appspec.yml
# CodeDeployに対してデプロイ処理を指定するファイル。リポジトリルートに置かれる
version: 0.0
os: linux
files:
  - source: /                # zipに含まれるファイル全部を
    destination: /tmp/sample # sample以下に展開
hooks:
  ApplicationStop:
    - location: codedeploy/stop-application.sh
      timeout: 30

  # Install:
  # CodeDeployが最新のコードをデプロイターゲットに配布する

  AfterInstall:
    - location: codedeploy/start-application.sh

上のappspec.ymlでは「既存プロセスをkillする」「新しいプロセスを起動する」といった内容を.shで表現しています。これをエージェントが順次実行します。デプロイ先で実行可能であれば、表現方法は自由です。

デプロイフロー

新しいデプロイフローは次のようになりました。

f:id:vasilyjp:20180717125150p:plain

画像の左下がスタートです。

  1. アプリケーションコードがGitHub上で特定のブランチにマージされる
  2. CircleCIによるテストの後、zipで圧縮してS3に保存
  3. S3はオブジェクトの更新をCodePipelineに通知
  4. CodePipelineはCodeDeployを呼び出す
  5. CodeDeployは事前に定められたAutoScalingグループに対してデプロイを実行

詳細について触れていきます。

CodePipelineのパイプライン戦略

CodePipelineのデプロイパイプラインは、リポジトリ単位で分離します。 複数の役割を持っているリポジトリについては、Deployフェーズで分岐させ、それぞれをCodeDeployが実行します。

f:id:vasilyjp:20180717125228p:plain:h500

3

具体的には、APIのようにロードバランサにアタッチする必要のあるAutoScalingグループと、非同期処理を担当するAutoScalingグループで分離しています。

CodeDeploy + OpsWorksとの連携

CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、新規追加されたインスタンスに対して自動でデプロイを実行します4。便利な一方、新規に起動したインスタンスの構成管理について考慮する必要があります。

私が管理するいくつかのサービスでは、構成管理を全てOpsWorksに集約しています5。これはAutoScalingグループに所属するインスタンスであっても例外ではありません6

f:id:vasilyjp:20180717125247p:plain

何のフォローも行わない場合、構成管理中にCodeDeployのデプロイ処理だけが先行し、デプロイが失敗します。デプロイに失敗した新規インスタンスはAutoScaling側で失敗とみなされ破棄されるため、延々と起動・失敗・破棄を繰り返してしまいます。

これを避けるため、CodeDeployよるデプロイ処理中にOpsWorks側のステータスを確認する処理を含めています。

version: 0.0
os: linux
files:
  - source: /                # zipに含まれるファイル全部を
    destination: /tmp/sample # sample以下に展開
hooks:
  ApplicationStop:
    - location: codedeploy/stop-application.sh
      timeout: 30

  # この処理を追加
  BeforeInstall:
    - location: codedeploy/setup-wait.sh
      timeout: 900

  AfterInstall:
    - location: codedeploy/start-application.sh
#!/bin/bash

set -e

OPSWORKS_INSTANCE_ID=$(grep 'OpsWorks Instance ID' /etc/motd | cut -d: -f2 | tr -d ' ')
/usr/local/bin/aws opsworks --region us-east-1 wait instance-online --instance-ids $OPSWORKS_INSTANCE_ID

素朴なシェルスクリプトですが、この処理を挟むことで構成管理を待った上でデプロイを実行する事ができます。

インプレースデプロイかBlue/Greenデプロイか

CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、デプロイの方法として2パターンを選択できます。

  • インプレースデプロイ:既存のインスタンスに対してデプロイを行う
  • Blue/Greenデプロイ:新規のインスタンスを作成しデプロイを行う

本件では前者のインプレースデプロイを採用しています。後者の長所は障害時の高速なロールバックかと思いますが、前述の構成管理と合わせて検討すると、revertコミット+インプレースデプロイが十分に高速かつシンプルという判断です7

なお、インプレースデプロイかつデプロイターゲットがロードバランサに所属する場合、CodeDeploy側がデプロイの前後で適切にアタッチ・デタッチをしてくれます。サービスインしたままデプロイが行われるということはありません。

CodeDeployを採用してハマった事

完成した後のデプロイフローはとても安定しています。しかし、構築中にいくつかハマった部分もありました。

複数のロードバランサに所属する場合のフォロー

CodeDeployは、デプロイターゲットがロードバランサ(ALBならターゲットグループ)に所属している場合、適切にアタッチ・デタッチしてくれます。しかし複数のロードバランサまでは面倒を見てくれません(2018年7月時点)。

f:id:vasilyjp:20180717125304p:plain そのため、何らかの理由で複数のロードバランサに所属するAutoScaingグループであれば、CodeDeployに渡す処理中でフォローしてやる必要があります。

#!/bin/bash

set -e

readonly INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
readonly TARGET_GROUP_INTERNAL_API='xxxxx'

/usr/local/bin/aws --region ap-northeast-1 elbv2 register-targets --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id=${INSTANCE_ID}
/usr/local/bin/aws --region ap-northeast-1 elbv2 wait target-in-service --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id=${INSTANCE_ID}

CodeDeployのBlockTraffic/AllowTraffic

CodeDeployのデプロイは幾つかのステップがAWS側に予約されています。ロードバランサとのインテグレーションもその予約されたステップで行われるのですが、なぜか2〜3分も待たされる事があります。

どうやらCodeDeployの挙動はロードバランサのヘルスチェック設定に依存しているようです8

f:id:vasilyjp:20180717125313p:plain BlockTraffic/AllowTrafficにかかる時間は次のとおりです。

  • Interval: 30secHealthy threshold: 10ならそれぞれ300秒程度
  • Interval: 5secHealthy threshold: 2ならそれぞれ10秒程度

運用のポリシーが許す範囲で短くしておくことをおすすめします。

CodeDeployを採用して良かったこと

AutoScalingとの統合以外にもコスト面で効いた部分がありました。

集約率の向上

これはアプリケーションレベルでのGraceful Restartにこだわる必要が無くなったため得た恩恵です。 当初はAutoScalingによる柔軟なリソース配分を重視して始めた施策でしたが、改めて考えると一台あたりのパフォーマンスも上げることが可能でした。

本件で紹介しているデプロイフローを採用しているアプリケーションはRails + unicorn + nginxという、Railsでよくみる鉄板構成です。 旧デプロイフローではunicornに対してCapistranoでUSR2シグナルによるGraceful Restartをしていました。

Graceful Restartの問題は、古いプロセスを残したまま新しいプロセスを作るため瞬間的にメモリ消費量が倍になることです。そのため、デプロイを安全に終了するためには、1インスタンス毎のメモリ消費量を40%程度に管理しておく必要があります。

一方、CodeDeployによるデプロイであれば一時的にサービスアウトしつつデプロイが進行します。そのため、既存のプロセスを一旦完全にkillできます。これによりデプロイ中という瞬間的な状態を気にすること無く集約率を検討できるようになりました。

まとめ

本記事では、CodePipeline+CodeDeployによる新しいデプロイフローについて紹介しました。 AutoScalingとの統合や集約率の向上といった恩恵はもちろんですが、デプロイを小さなステップに分割して整理できたため、保守性という意味でも向上したように感じています。

スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。

https://www.starttoday-tech.com/recruit/


  1. 株式会社スタートトゥデイテクノロジーズはスタートトゥデイ工務店 + VASILY + カラクルの3社を統合し発足されました

  2. AWS Lambdaに対するデプロイも可能ですが、本記事では扱いません

  3. 画像中でCodeBuildを呼び出していますが、これはパイプライン設計中に後からタスクを任せようと思った名残です。結局、既存のCircleCIで十分と気づき何もしないステージとしてそのままになっています。本記事でも触れません。

  4. ライフサイクルフックの仕組みを用いています。

  5. CloudFormationとOpsWorksでインフラを育てる

  6. UserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用する(引用はSpotFleetに関する話題だが、SpotFleetでないAutoScalingでも同様)

  7. ゴールデンイメージによる構成管理時間の短縮を検討しなかったわけではありませんが、既存のOpsWorksによるフローを変更することのデメリットが遥かに大きく、早々にアプローチから外れました。

  8. AWS Developer Forums: BlockTraffic/AllowTraffic durations