編注: この記事の内容は、2020 年 9 月 3 日にシニア DevOps カスタマー エンジニアの Ben Van Houten が更新しました。


AWS のロゴ

Amazon Auto Scaling グループ (ASG) とは、理論上は優れたスケーリング手法です。必要な容量と、追加のマシンの起動方法に関する情報を ASG に提供すれば、必要容量の変化に応じてフリートのスピンアップとスピンダウンが完全に自動で行われます。ただし残念ながら、実際には CircleCI.com フリートの管理には ASG を使用できません。その理由はいくつかありますが、最も致命的なのが、デフォルトの ASG 終了ポリシーではインスタンスが即座に強制終了されることです。CircleCI のインスタンスはお客様のためにビルドを実行しているので、即座に強制終了させることはできず、すべてのビルドが完了するまで待たなくてはなりません。

グレースフル シャットダウン (特定の状況になるまで待機してからインスタンスを終了させる必要があること) は、CircleCI だけの問題ではありません。また、この問題の解決策は、フリートのサイズや使用パターンによって変化します。たとえば CircleCI.com フリートのスケーリングにはカスタムのシステムを使用しています。これは CircleCI.com にかかる負荷が、少々のランダム性はあるものの、世界中のお客様の勤務時間に基づくほぼ予測可能な使用パターンに従っているためです。ASG で提供されているシンプルなメトリクスベースのスケーリング ポリシーは、このモデリングには十分ではありません。

一方、CircleCI エンタープライズのリリースをきっかけに気付いたことがあります。お客様のフリートのサイズや負荷の要件が非常にシンプルなことが多いのであれば、完全にカスタムなスケーリング システムを独自に構築するようお客様ごとに求めるのは意味を成さないのではないか、と思い始めたのです。このため、ほとんどのお客様の一般的なスケーリング パターンをカバーしつつ、必要に応じて基本設定以上のカスタマイズも可能であるシンプルなソリューションを構築できないかどうかを考えました。

CircleCI 1.x ソリューション

Terraform ロゴ

私たちのソリューションは、大まかに言うと以下 4 つの部分に分けられます。

  1. EC2 インスタンスを管理する ASG。
  2. インスタンスを終了させるときに通知をパブリッシュし、サーバーを “Terminating: WAIT” 状態にする Auto Scaling ライフサイクル フック。SNS 通知をトリガーします。
  3. ライフサイクル フック操作を実装するための Lambda 関数をトリガーする SNS トピック。
  4. 指定されたノード上で、AWS SSM を通じて “nomad node drain -enable” コマンドを実行する Lambda。これにより、終了予定の Nomad クライアントがこれ以上ジョブをビルドしないようになります。
  5. 新しいジョブがキューイングされないように、指定されたノードが ineligible としてマークされ、安全な終了を行うためにノードのすべてのジョブが終了されます。
  6. コマンドが成功したら、ノードは ASG によって完全に終了されます。

CircleCI では、CircleCI.com AWS インフラストラクチャの管理にTerraformを使用しています。 お客様に推奨する CircleCI Enterprise スクリプトの多くにおいても同様です。Terraform は、コード内にインフラストラクチャを宣言的に記述でき、CloudFormation よりも簡潔かつエラーが起こりにくいので重宝しています。以降のリソースは Terraform の例として記述していますが、手動でビルドしたい場合や、CloudFormation を使用する場合にも同じアプローチを使用できます。自分で試してみたいという方のために、完全な Terraformファイルもご用意しています。

Auto Scaling グループ

私たちは、CircleCI Enterprise のお客様向けに手動のスケーリング戦略を自動化するのではなく、既存のベスト プラクティスへのフックを提供しようと考えました。AWS の場合、これは ASG になります。高い柔軟性を備える ASG は、最もシンプルな形式では非常に基礎的なフォールト トレランスとして機能し、マシンのいずれかが停止した場合は新しいマシンを自動でスピンアップします。また、時間ベースのスケーリングを容易に行えるスケーリング スケジュールをアタッチすることも、メトリクスに基づいて受動的にスケーリングするスケーリング ポリシー をアタッチすることも可能です。このブログ記事の目的上、 nomad_clients_asgという ASG が設定済みである前提で話を進めます。

Auto Scaling ライフサイクル フック

ですが、冒頭でも触れたとおり、ASG はインスタンスを早急に終了させてしまいます。CircleCI のグレースフル シャットダウンでは、ビルドが完了してからインスタンスを終了させなくてはならず、これには 30 分以上かかることがあります。そこで、ASG の知名度がまだ低い新機能であるライフサイクル フックを利用することにしました。

ライフサイクル フックとは ASG が特定の操作を実行したときに Amazon から通知を受け取れるようにするものです。今回重要なのは、ASG がインスタンスを終了させようとしているタイミングを通知してくれる autoscaling:EC2_INSTANCE_TERMINATINGです。ここでは、heartbeat_timeoutを1時間、default_resultCONTINUEに設定しました。 CircleCI のグレースフル シャットダウンは通常 1 時間もかかりません。つまり何か問題が発生して 1 時間後も実行している場合、Amazon 側で強制終了を行ってもらうわけです。

resource "aws_autoscaling_lifecycle_hook" "graceful_shutdown_asg_hook" {
    name = "graceful_shutdown_asg"
    autoscaling_group_name = "${aws_autoscaling_group.nomad_clients_asg.name}"
    default_result = "CONTINUE"
    hearbeat_timeout = 3600
    lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING"
    notification_target_arn = "${aws_sns_topic.graceful_termination_topic.arn}"
    role_arn = "${aws_iam_role.autoscaling_role.arn}"
}

Amazon の SNS 通知

上記の例では、ライフサイクル フックの notification_target_arn を SQS キューに設定していることにお気付きでしょうか。これは、終了メッセージの送信先が Amazon に必要だからです。私たちは、独自のエンドポイントを記述するのではなく、インスタンスの終了が必要な状態を Amazon に維持してもらうことにしました。

resource "aws_sns_queue" "graceful_termination_queue" {
  name = "graceful_termination_queue"
}

SNS トピックのサンプル コードそのものは非常にシンプルですが、ライフサイクル フックが (上記の role_arn セクションで構成した) SNS トピックにパブリッシュできるようにする IAM ロールと関連ポリシーも作成する必要があります。

resource "aws_iam_role" "autoscaling_role" {
    name = "autoscaling_role"
    assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "autoscaling.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "lifecycle_hook_autoscaling_policy" {
    name = "lifecycle_hook_autoscaling_policy"
    role = "${aws_iam_role.autoscaling_role.id}"
    policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
          "Sid": "",
          "Effect": "Allow",
          "Action": [
              "sns:Publish",
          ],
          "Resource": [
             "*"
          ]
        }
    ]
}
EOF
}

CircleCI アーキテクチャでは複数のワーカーがシャットダウン通知を使用できるので、このように SNS トピックを使用するのが適切です。アーキテクチャにおいて、フォロワーをシャットダウンできるリーダーを 1 つにしなければならない場合、つまりコンシューマーを 1 つのみにする場合は、代わりに SQS キュー (Amazon によるサービスとしての Pub/Sub) の使用を検討してください。

グレースフル シャットダウン

残るは、キューの実際の使用と、マシンのグレースフル シャットダウンです。グレースフル シャットダウンの方法によっては、キューを使用するためのソリューションがこの記事で扱っているものと大きく異なる場合もありますが、キュー内の通知の表示について知っておいていただきたいことが 2 つあります。

まず 1 つ目。Amazon はライフサイクル フックを初めてアタッチしたときに、正常に接続できているかどうか確認するために以下のようなテスト通知を送信します。

{
  "AutoScalingGroupName":"example_asg",
  "Service":"AWS Auto Scaling",
  "Time":"2016-02-26T21:06:40.843Z",
  "AccountId":"some-account-id",
  "Event":"autoscaling:TEST_NOTIFICATION",
  "RequestId":"some-request-id-1",
  "AutoScalingGroupARN":"some-arn"
}

コンシューマーにこのメッセージが届いても無視して構いません。

2 つ目として、以下のような実際のシャットダウン通知の表示も知っておいてください。

{
  "AutoScalingGroupName":"example_asg",
  "Service":"AWS Auto Scaling",
  "Time":"2016-02-26T21:09:59.517Z",
  "AccountId":"some-account-id",
  "LifecycleTransition":"autoscaling:EC2_INSTANCE_TERMINATING",
  "RequestId":"some-request-id-2",
  "LifecycleActionToken":"some-token",
  "EC2InstanceId":"i-nstanceId",
  "LifecycleHookName":"graceful_shutdown_asg"
}

注: このソリューションは、CircleCI Enterprise 2.0 のソリューションでのみ使用可能です。

これらの通知をどのように使用するかは、完全に自由です。今回は、事前パッケージ済みで構成しやすいという点を考慮し、Lambda 関数内に、お客様がカスタマイズ可能な Node.js スクリプト ランナーを追加しました。

AWS Lambda 関数と SSM エージェント ドキュメント

CircleCI の Lambda 関数は、AWS Javascript SDK を利用することで、AWS Systems Manager (SSM) エージェントを利用し指定の Auto Scaling グループと通信します。AWS SSM エージェントは、EC2 インスタンス上にインストールして構成することが可能な Amazon のソフトウェアです。基本となる enterprise-setup Terraform スクリプトをお客様が利用している場合、CircleCI は AWS SSM エージェントがプリインストールされた Amazon Linux AMI を利用します。

ソリューションを続行するためには、SSM に AWS リソース (この場合は Nomad クライアント) 上で実行させたい操作を定義できるようにする SSM ドキュメントをアップロードする必要があります。今回、SSM ドキュメントは以下のようになります。Nomad の drain コマンドを実行した後、ノードが適切かどうかを確認しています。

{
   "schemaVersion": "1.2",
   "description": "ノードのドレイン",
   "parameters":{
     "nodename":{
       "type":"String",
       "description":"ドレインを行うノード名を指定"
     }
   },
   "runtimeConfig": {
     "aws:runShellScript": {
       "properties": [
         {
           "id": "0.aws:runShellScript",
           "runCommand": [
             "#!/bin/bash",
             "nomad node drain -enable -self -y",
             "isEligible=$(nomad node-status -self -json | jq '.SchedulingEligibility | contains (\"ineligible\")')",
             "if (( ${isEligible} == true )) ; then exit 0 ; else exit 129; fi"
           ]
         }
       ]
     }
   }
 }

このファイルを document.json として保存し、以下のコマンドを実行して SSM 内に同じ内容のファイルを作成します。

aws ssm create-document --content "file://drain-document.json" --name "CircleCiDrainNodes" --document-type "Command"

最後に、Lambda 関数を作成してライフサイクル フックを処理する必要があります。Lambda コードをローカルのワークステーションにダウンロードして、コードの依存関係をインストールします。インストールの際は、Lambda と同じバージョンを使用していることを確認し、環境に合わせて Lambda スクリプトをカスタマイズしてください。

コードを zip 圧縮して、Lambda 関数を作成します(こちらの zip ファイル)をダウンロードすることもできます)

まとめ

Auto Scaling グループは、柔軟なスケーリングを可能にするので、スケーリング管理に便利な手法として長きにわたり愛用されています。ライフサイクル フックが加わったことで、終了の柔軟性が向上し、グレースフル シャットダウンが可能になりました。

つまり、運用チームのオーバーヘッドを最小限に抑えつつ CircleCI Enterprise のお客様がフリートをスケーリングできるようにしたいという問題の解決策は、ASG とライフサイクル フックの組み合わせだったということです。これらは、素早くメリットを実現するプラグアンドプレイのソリューションを提供します。さらに、フリートを実際にスケーリングする方法について、お客様ご自身で詳細にカスタマイズすることも可能です。