Amazon ECSのDockerコンテナをLambdaで自前オートスケールする

137件のシェア(ちょっぴり話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。
4/9-10に行われたAWS Summit 2015 San Franciscoで正式リリース&大量アップデートが発表されたAmazon ECS、皆さん触っていますか?弊社ブログでも早速いくつかエントリーをアップしています。アップデート後の触ってみた記事はこちらです。

今回のアップデートの目玉が、待望だったコンテナスケジューラの追加です。Serviceというスケジューラが追加され、コンテナレベルHAとELB連携が容易に行えるようになりました。しかし現在のServiceにはオートスケール機能がないため、今回はそれを自前で実装した構成を紹介してみます。

ECSのServiceにはオートスケール機能がない

ECSの製品ページ(日本語ページはアップデート前の情報なので、Englishに切り替えましょう)には、Serviceスケジューラの機能として以下が挙げられています。

  • Container Auto-Recovery : 実行コンテナ数が減少した場合には再実行し、望ましいコンテナ数を維持する
  • Container Load Balancing : ELBと連携し、コンテナが起動したらそのコンテナを実行するECSインスタンスをELBに追加する
  • Container Deployments : タスク定義にバージョンを付与し、タスク定義のアップデートを実行する

一見すると、ELBと連携するのでAuto Scalingと組み合わせてコンテナのスケールアウト/インができるのでは?と錯覚してしまうのですが、ServiceはAuto Scalingには依存せず独自にELBと連携するようになっています。言い方を変えると、ECSクラスタに参加するインスタンスをAuto Scalingで構成しても、Serviceはそのインスタンス数を考慮しません。

ecs-autoscaling01

そして現状のServiceには、コンテナ数を維持する機能はありますが、スケールアウト/インの仕組みは実装されていません。

よろしい、では実装しよう

機能が無いのであれば、それに相当する仕組みを実装しようと考えました。なるべく有り物を!と考えたのが以下の構成です。

  • スケールアウト/インのトリガー : CloudWatchアラーム
  • アラームアクション : Amazon SNS通知
  • スケールアウト/インの実装 : AWS Lambda

ちょうど、ECSのGAと同日にLambdaがSNSに対応したので、Lambdaを使ってみました。EC2を利用しないのでインスタンスのメンテナンスが不要であり、運用コストが少ないことが特徴です。

ecs-autoscaling02

では、構築手順を順に確認していきます。オレゴンリージョン(us-west-2)こちらの記事の手順に沿ってECSクラスタとServiceを作成済みとします。

ELBにCloudWatchアラーム&SNSトピックを設定

まずは、CloudWatchの管理画面からECS Serviceと連携するELBのメトリクスを選択し、[Create Alarm]ボタンでアラーム作成画面を開きます。今回は負荷の指標としてRequestCountを選択しましたが、Latencyなど他のメトリクスでも良いでしょう。

ecs-autoscaling04

アラーム作成画面では、適当なアラーム名、説明を入力します。リクエストカウント数は1分あたりのスケールアウトのいき値となるリクエスト数(今回は100)とセットします。

ecs-autoscaling05

Actionsでは、SNSトピックを新規追加するので、[New List]リンクをクリックし適当なトピック名(今回は「ECSServiceELBAlarm」)、通知するメールアドレスを入力し、[Create Alarm]ボタンでアラームが作成されます。

ecs-autoscaling06

「Confirm new email addresses」が表示されたら、登録したメールアドレス宛に受信確認のメールが届いているので、メールの本文にある[Confirm subscription]のリンクを踏んでおきましょう。

LambdaのIAM権限設定

続いて、Lambda関数を実行するためのIAMロールをIAMの管理画面から作成します。今回は、ロール名「lambda_exec_role」、[AWS Service Roles] - [AWS Lambda]を選択し、Attach Policyは空のまま作成します。その後、IAMロールのプロパティからInline Policiesの作成([click here]リンク)から[Custom Policy]で任意のポリシー名と以下のPolicy Documentを入力します。

Lambda標準のログ出力先であるCloudWatch LogsとECSのサービス管理の一部のアクションを許可しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:*"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecs:DescribeServices",
        "ecs:UpdateService"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Lambda関数の作成とSNSの登録

では、いよいよLambda関数を定義します。Lambdaの管理画面から[Create a Lambda function]ボタンをクリックし、以下を入力します。

  • Name : 任意の関数名(今回は「scaleOutServiceCount」)
  • Description : 今回は空のまま
  • Code entry type : 「Edit code inline」のまま
  • Code template : 「None」を選択し、以下をコピペ
  • Handler name : 初期値「handler」のまま
  • Role : 先ほど作成したIAMロール(lambda_exec_role)を選択
console.log('Loading event');
var aws = require('aws-sdk');

exports.handler = function(event, context) {
  var ecsService = 'sample-webapp';
  var ecsRegion = 'us-west-2';
  var maxCount = 2;

  var ecs = new aws.ECS({region: ecsRegion});
  ecs.describeServices({services:[ecsService]}, function(err, data) {
    if (err) {
      console.log(err, err.stack);
    } else {
      var desiredCount = data.services[0].desiredCount;
      if (desiredCount < maxCount) {
        desiredCount++;
        var params = {
          service:      ecsService, 
          desiredCount: desiredCount
        };
        ecs.updateService(params, function(err, data) {
          if (err) {
            console.log(err, err.stack);
          } else {
            console.log(data);
            context.succeed();
          }
        });
      } else {
        console.log('Service count is already max.');
        context.fail();
      }
    }
  });
};

コード自体は至ってシンプルです。簡単に解説します。

  • 5,6,7行目 : 適宜変更してください。7行目のmaxCountはAuto Scalingグループと同じく、スケールアウトするコンテナ数の上限を設定します。RequestCountなどのメトリクスだと、アラームが繰り返し発報されるケースが考えられるためです。
  • 10,14行目 : ecs.describeServicesメソッドでServiceの情報を取り出し、その中から現在のDesiredCount(サービスで実行するタスク(コンテナ)数)を取得
  • 15,16行目 : maxCountを評価しつつスケールアウトとなるDesiredCountのインクリメント
  • 19,21行目 : 加算済みのDesiredCountでServiceの設定をアップデート

関数を作成したら、関数一覧から「scaleOutServiceCount」を選択し、[Actions] - [Add event source]をクリックします。

ecs-autoscaling07

[Event Source Type]から「SNS」、[SNS Topic]では作成したトピック「ECSServiceELBAlarm」を選択し[Submit]で確定します。

ecs-autoscaling08

これで準備OKです!

動作確認

ELBに負荷をかけてアラームを発報してもよいのですが、手軽に試す方法としてLambda関数のテスト実行があります。関数を選択し、[Actions] - [Test/Edit]をクリック、[Invoke]ボタンをクリックすると関数が実行されます。画面下部の[Executions logs]に実行ログが表示され、[desiredCount]が1つ増えた「2」になっているのがわかります。

ecs-autoscaling09

ECSのService管理画面でも同様にスケールアウトしている様子が見られますね!

ecs-autoscaling10

まとめ

ECS ServiceにLambdaを利用してスケールアウト機能を実装してみました。同じ要領でスケールインのアラーム、関数定義もできると思いますし、Lambda+SNSはAWSサービスのいろいろな連携に応用できると思いますのでみなさんも実験してみてくださいね!

  • ranjeet k gupta

    I want to understand , what happens in a scenario when there is a sudden surge in application usage which say triggers an alarm based on number of requests on ELB. The lambda function here will increase number of container instances by 1. However, since the surge is too high, request count will still be high and therefor the alarm will stay in ‘IN-ALRM’ state which will prohibit spawning up other additionally required containers(as action is performed only when alarm state changes). How to handle such scenarios.

  • Hyogyun Bang

    An error in Lambda occurs.

    TypeError: Cannot read property ‘desiredCount’ of undefined

    I think the ecs cluster name must be defined in the code.

    • John Schmidt

      This is how I had to modify the code to get it to work.

      console.log(‘Loading event’);

      var aws = require(‘aws-sdk’);

      exports.handler = function(event, context) {

      var ecsService = ‘myservice’;

      var ecsCluster = ‘mycluster’;

      var ecsRegion = ‘awsregion’;

      var maxCount = 2;

      var ecs = new aws.ECS({region: ecsRegion});

      ecs.describeServices({cluster: ecsCluster, services:[ecsService]}, function(err, data) {

      if (err) {

      console.log(err, err.stack);

      } else {

      var desiredCount = data.services[0].desiredCount;

      if (desiredCount < maxCount) {

      desiredCount++;

      var params = {

      cluster: ecsCluster,

      service: ecsService,

      desiredCount: desiredCount

      };

      ecs.updateService(params, function(err, data) {

      if (err) {

      console.log(err, err.stack);

      } else {

      console.log(data);

      context.succeed();

      }

      });

      } else {

      console.log('Service count is already max.');

      context.fail();

      }

      }

      });

      };

      • Hyogyun Bang

        Thank you. :)