話題の記事

爆速でFargateをスケールさせる「aws-fargate-fast-autoscaler」を試してみた

CloudWatchだけでは実現できない超高速なFargateのスケール処理をCDKをつかったStep Functionsで実装しているリポジトリです。是非参考にしてみてください。
2019.09.30

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

「Fargateをいかに早くスケールさせるか、そこに命をかけた男がいた…」

先日参加したセミナー(コンテナ好き4名がコンテナの魅力を喋り倒すJAWS-UGコンテナ支部に行ってきた)にそんな男がいたわけですが、その仕組を改めて動かす機会があったので、紹介します。

  • CloudWatchを利用しないStep Functionsを利用した爆速スケールの仕組み
  • CDKによる環境一式のデプロイ

という両面で非常に参考になるリポジトリです。そのあたり興味があるかたは是非一度この記事を読んでいただきながら皆さんの環境でためしていただきつつ、今後のStep Functionsの使い方やCDKのサンプルとして活用いただければと思います。

Fargate爆速増殖きたか…!!

  ( ゚д゚) ガタッ
  /   ヾ
__L| / ̄ ̄ ̄/_
  \/   /

「aws-fargate-fast-autoscaler」

awslabsにソースコードが公開されています。

aws-samples/aws-fargate-fast-autoscaler: AWS Fargate Fast Autosaler - A Serverless Implementation that Triggers your Fargate Autoscaling in Seconds

(上記リポジトリより引用)

作者は、pahudnet(@pahudnet)さん

簡単に動作原理を説明すると、Fargateをオートスケールさせる場合、通常はECSのターゲット追跡スケーリングポリシーなどを利用し、ECSサービスにおけるCloudWatchメトリクスとアラームを利用して、タスク数などを制御します。

参考:ターゲット追跡スケーリングポリシー - Amazon Elastic Container Service

ただ、この場合、CloudWatchメトリクスからアラーム発報までにどうしてもタイムラグが有り、数秒単位でのスケーリングが難しく基本は分単位でのスケーリングとなっていました。

そこを、この「aws-fargate-fast-autoscaler」では、Step Functionsを利用して3秒毎にFargateタスクへのコネクション数を取得し、その結果に応じて即ecs service updateでタスク数の上限を引き上げることで、ターゲット追跡スケーリングポリシーでは実現できない、高速なスケーリングを実現しています。

この記事では、環境を作って動作確認したのち、作成された環境の詳細を確認していきます。

Fargate爆速スケーリング環境のデプロイ

基本は、cd aws-fargate-fast-autoscaler/cdkのREADMEにすべて記載されています。

Git Cloneして。

git clone https://github.com/aws-samples/aws-fargate-fast-autoscaler.git
cd aws-fargate-fast-autoscaler/cdk

CDKのセットアップをします。ここは、READMEの(aws-samples/aws-fargate-fast-autoscaler: AWS Fargate Fast Autosaler - A Serverless Implementation that Triggers your Fargate Autoscaling in Seconds)に記載されているとおりですが、既にCDKとTypeScriptの環境構築が完了していたら、実施不要です。

cdk synthコマンドでCloudFormationスタックが無事表示されれば準備完了です。が、自分の環境では以下のエラーが発生しました。

$ cdk --version
1.9.0 (build 30f158a)
$ cdk synth
There are no 'Private' subnets in this VPC.Use a different VPC subnet selection.
Subprocess exited with error 1

どうやら、VPCにPrivateサブネット(NAT Gateway有り)が必要なようです。このVPCは、コードではデフォルトVPCを使うようになっているのですが、自分はデフォルトVPCにNAT Gatewayを設置してません(それなりに高い)。そこで、以下のコードで新規でVPCを作ることにします。

修正前のこのコードでは、デフォルトVPCを利用しているところを。

cdk/cdk-stack.ts

    //import default VPC
    const vpc = ec2.Vpc.fromLookup(this, 'VPC', {
      isDefault: true
    });

新しくVPCを作るように変更しています。

cdk/cdk-stack.ts

    const newVpc = new ec2.Vpc(this, 'NewVpc', {
        maxAzs: 3,
        natGateways: 1
    })

この状態でcdk deployし、無事関連リソースが作成されると、こんな感じでデプロイされたコンテナアプリケーションのエンドポイントが表示されます。

fargate-fast-autoscaling.URL = http://cdkst-exter-re4o5mfxu145-9999999999.ap-northeast-1.elb.amazonaws.com/

また、Step FunctionsのWebコンソールにアクセスすると、オートスケール監視用のステートマシンが作成されています。

ここまでで前準備は終了。

Fargate爆速スケーリングの動作確認

早速動作確認していきます。最初にStep Functionsのステートマシン実行後、Apache Benchを使って負荷をかけます。

ab -n 50000 -c 1000 http://cdkst-exter-re4o5mfxu145-9999999999.ap-northeast-1.elb.amazonaws.com/

そうすると、ステートマシンのビジュアルワークフローで、ECSのタスク数が10や15で設定されているのがわかります。

ECSのサービス定義をみると、タスクの必要数が15になります。

Container Insightsでみたタスク数の遷移。

ここまで、だいたい10秒程度。CloudWatchを使わずにサーバーのコネクション数をみて、直接ECSサービスのタスク数を更新するとこのような迅速なスケーリング設定が可能です。

Fargate爆速スケーリングの仕組み

この爆速スケーリングの仕組みですが、肝はStep Functionsのステートマシン。

ステートマシン定義のJSONは以下の通り

{
	"StartAt": "GetECSTasks",
	"States": {
		"GetECSTasks": {
			"Next": "IsServiceOverloaded",
			"Type": "Task",
			"Resource": "arn:aws:lambda:ap-northeast-1:629895769338:function:CdkStack-fargateWatcherFunc45C78E3D-1PLSFWW8O4RH0",
			"ResultPath": "$.status"
		},
		"Wait 60 Seconds": {
			"Type": "Wait",
			"Seconds": 60,
			"Next": "GetECSTasks"
		},
		"ServiceScaleOut": {
			"Next": "Wait 60 Seconds",
			"Type": "Task",
			"Resource": "arn:aws:lambda:ap-northeast-1:629895769338:function:CdkStack-fargateWatcherFunc45C78E3D-1PLSFWW8O4RH0"
		},
		"SNSScaleOut": {
			"Next": "ServiceScaleOut",
			"Parameters": {
				"TopicArn": "arn:aws:sns:ap-northeast-1:629895769338:mail-to-cmhamada",
				"Message": {
					"Input.$": "$"
				},
				"Subject": "Fargate Start Scaling Out"
			},
			"Type": "Task",
			"Resource": "arn:aws:states:::sns:publish",
			"ResultPath": "$.taskresult"
		},
		"Desire20": {
			"Type": "Pass",
			"Result": {
				"Desired": 20
			},
			"OutputPath": "$",
			"Next": "SNSScaleOut"
		},
		"IsServiceOverloaded": {
			"Type": "Choice",
			"InputPath": "$.status",
			"Choices": [
				{
					"Variable": "$.avg",
					"NumericGreaterThanEquals": 500,
					"Next": "Desire20"
				},
				{
					"Variable": "$.avg",
					"NumericGreaterThanEquals": 300,
					"Next": "Desire15"
				},
				{
					"Variable": "$.avg",
					"NumericGreaterThanEquals": 100,
					"Next": "Desire10"
				},
				{
					"Variable": "$.avg",
					"NumericGreaterThanEquals": 50,
					"Next": "Desire2"
				},
				{
					"Variable": "$.avg",
					"NumericLessThan": 0,
					"Next": "Done"
				}
			],
			"Default": "Wait 3 Seconds"
		},
		"Wait 3 Seconds": {
			"Type": "Wait",
			"Seconds": 3,
			"Next": "GetECSTasks"
		},
		"Desire15": {
			"Type": "Pass",
			"Result": {
				"Desired": 15
			},
			"OutputPath": "$",
			"Next": "SNSScaleOut"
		},
		"Desire10": {
			"Type": "Pass",
			"Result": {
				"Desired": 10
			},
			"OutputPath": "$",
			"Next": "SNSScaleOut"
		},
		"Desire2": {
			"Type": "Pass",
			"Result": {
				"Desired": 2
			},
			"OutputPath": "$",
			"Next": "SNSScaleOut"
		},
		"Done": {
			"Type": "Pass",
			"End": true
		}
	},
	"TimeoutSeconds": 86400
}

以下に主要なStepについての簡単な解説を記載。

GetECSTasks

bashで動作するLambda Layersを含んだVPC Lambda。該当するECSサービスに紐づく全タスクのタスクの数と、/nginx_statusから現在のアクティブなコネクション数を取得。

IsServiceOverloaded

GetECSTasksの取得結果に応じて、スケールアウトさせるECSタスク数を設定。ここのロジックはステートマシンのみで実装。

ServiceScaleOut

GetECSTasksから呼ばれるLambdaと同様のものを利用。desiredパラメータの数に応じて、ECSサービスのタスクのdesiredCountを変更。スケールアウト後は、60秒待って再度、GetECSTasksに遷移。

意外だったのが、ECSのタスク数の取得、および更新を実行するLambdaがbashで動いているところ。現在は、Lambda Layersを利用してbashも利用できるので、それを使えるところで使っているのが印象的でした。

また、このステートマシンでは、通常のタスク数の取得自体は3秒ごとに実施し、スケールアウト実施後は60秒待って再度タスク数の取得を実行しています。最初、この制御処理をすべてFargateの定期タスク処理や常駐タスクで実装するのも良いかと思ったのですが、処理結果の違いによる次ステップまでの実行時間の制御や、処理結果のハンドリングのやりやすさなどを考えると、Step Functionsを使うのもありなんじゃないでしょうか。

Step Functionsの実行制限(1年)に注意

Step Functionsには最大実行時間制限があります。

参考:制限 - AWS Step Functions

2019年9月現在、この実行時間制限は1年です。

  • 最大実行時間
    • 1 年.実行が 1 年の制限を超える場合、States.Timeout エラーで失敗し、ExecutionsTimedout CloudWatch メトリクスを出力します。

常駐プロセスとしてStep Functionsを使うときはこの点注意です。なかなか、1年動かしっぱなしということもなさそうですが、いざ運用段階に入ったあとどこかのタイミングで1年が経過してステートマシンが終了しても、今回の利用用途では気づくことができません。ので、そのあたりの対策が必要です。

対策案①:ExecutionsTimeodoutをCloudWatchEventsで拾って、ステートマシンを再実行する

この場合、もしCloudWatchEventsをひろう処理自体が正常に動かない場合、意図した動作にならないのであまり現実的とは言えません。

対策案②:CloudWatchEventsの定期実行を利用し、1時間に1回程度、該当ステートマシンの状態を監視し、止まっていたら再実行処理を行う。

こちらのほうが、対策としては現実的かと思います。

いずれにせよ、StepFunctionsが未来永劫動く常駐プロセスという前提で利用すると運用時に思わぬトラブルになる可能性もあるので、そのあたりは慎重に検討して実装しましょう。

この仕組の料金はどれぐらいかかるか?

Step Functionsの料金

参考:料金 - AWS Step Functions | AWS

Step Functionsの料金は、ステートマシンの数などは関係なく、状態遷移した回数で課金されます。

今回のステートマシンでは、GetECSTasksと、IsServiceOverloadedと、Wait 3 Secondsの3つのステートを3秒毎に繰り返すこととなります。大雑把にみつもって、1秒で1つのステートマシンが実行されるとすると、1月が2,592,000秒なので、2,592,000回ステートマシンが状態遷移するとします。

月あたり無料利用枠、4000回あるので、課金対象は、2,588,000回。状態遷移1,000回ごとの料金が0.025USDなので、Step Functionsの料金は月あたり約64.7USDとなります。

Lambdaの料金

参考:料金 - AWS Lambda |AWS

Lambdaについては、月当り1,000,000件のリクエストに対する無料利用枠があり、以後1,000,000件リクエストあたり0.20USDなので、月あたり約1USD程度です。

それぞれの料金を合計すると、月あたり、約65USDとなります。Step Functionsについて、このような常駐型で常にステートが遷移する場合若干割高に感じられるかもですが、それでもこの程度の金額に収まっているのは実用にたえる金額ではないでしょうか。

「Step Functionsの使い方の一例、およびCDKサンプルとしても非常に参考になる」

自分の中では、Step Functionsは基本バッチ処理で使うものという認識だったのですが、ループ処理を組み込むことにより常駐プロセスのように使えるというのが新鮮な経験でした。まさかこんな使い方があるとは。ステートの状況も追いやすいしログもこの仕組の中で完結しているので、Step Functionsの利用方法の幅が広がったなと感じています。

また、サンプルコードがCDKのTypeScriptになっているところも面白いですね。メインとなるcdk-stack.tsは、300行未満となっており、Lambda以外のVPC、ECS、ECR、Step Functions、IAMロールまですべてこの1ファイルに記載されています。

aws-fargate-fast-autoscaler/cdk-stack.ts at master · aws-samples/aws-fargate-fast-autoscaler

正直これを、CloudFormationで愚直に定義しようとしたら2000行は確実に超えていたでしょう。Step FunctionsのステートマシンもTypeScriptで記載されていて、「まじかよ!」とびっくりしましたが、コードとしては、有りなんじゃないでしょうか。

CloudWatchメトリクスとアラームでは到底できない、迅速なスケーリングの自前実装の1パターンとしてのStep Functionsの使い方、そして、CDKのサンプルとしても非常に有用なリポジトリになっているので、気になる方は、ぜひ一度自分の環境で実装してみてあれこれ試してもらえればと思います。

それでは、今日はこのへんで。濱田(@hamako9999)でした。