SpotFleet+ECS+ALB構成を試したら気持ち良かったからAWSLambdaで痒いところをさらに気持ち良く
その課題、本当にサーバが必要ですか?サーバレスに考えたらシステム運用がもっとシンプルになりませんか?
ソフト道場の「SIerが目利きする。今日から使えるAWSレシピ」 第4回
- 2016年12月09日公開
ECS+ALB構成でのベストプラクティスをお探しの方は、まず 構築でつまづかないためのECS+ALB構成ベストプラクティス(2016年版) をご覧ください。本記事で説明している内容は、ベストプラクティスでないやり方になっています。
はじめに
ソフト道場AWSチームの須藤です。好きなAWSのサービスはもちろんLambdaです。
この記事は、 Serverless(2) Advent Calendar 2016 の9日目です。弊社も カッコイイ執行役員がいて 自社のアドベントカレンダーができるようになるといいなー、なんて思ってます。
タイトルを見て、ECS?えっ、サーバあるじゃんサーバレスじゃないじゃん!...なんて思っても、せっかくなのでご一読いただけたら幸甚です。
SpotFleet+ECS+ALB構成と痒いところ
最近従事しているとあるプロジェクトでは、サーバサイドアプリケーション開発者がDockerを使って開発環境を整えているので、AWS上でもECS/ECRを採用しよう!ということで、ECSをベースとした構成を構築しております。
コスト効率を考えると、当然SpotFleetと組み合わせたいですし、ALBも必要になりますから、自然とこんな構成に落ち着きます。
この構成を構築するにあたって、アカウントによってはEC2やSpotインスタンスの上限緩和申請が必要な場合があったり、ALBを複数使うためVPC内のサブネットにある程度の大きさが必要になったりしますが、構築してしまえば以下のようなメリットを享受できます。
サーバサイドアプリケーション開発者が幸せになるメリット:
- ECR - ローカルでBuildして動作確認したDocker Imageをpushすると、そのままECS上のサービスにできる
- ECS - 新しいバージョンのサービスを簡単にクラスタにデプロイできる
インフラエンジニアが幸せになるメリット:
- SpotFleet - オンデマンドインスタンスより安価に、AutoScaling設定も可能なインスタンス群を購入・管理できる
- ECS - Docker SwarmやKubernetesの初期構築のような手間をかけずに、簡単にクラスタを構成してサービスを起動することができる
- ALB - ECS上のサービスへのヘルスチェックを設定することで、ECSクラスタ上で稼働している正常なサービスにロードバランシングしてくれる
えっ何これ気持ち良い...。 コスト安くて開発と地続きで運用もしやすいなんて、ここは天国ですか?
ところが、どうしても一箇所、痒いところがあったのです。
- ALBのターゲットグループ - ALBのターゲットグループにSpotFleetやECSクラスタを直接指定して登録することができないため、SpotFleet内のインスタンスがAutoScalingのスケールアップ・スケールダウンや価格変動で変更された場合、何らかの手段でターゲットグループに所属するインスタンス情報をメンテナンスしなければならない
痒いところをAWS Lambdaで気持ち良くする
痒いところを解決する方法として、2つのやり方を検討しました。
- cloud-init(user-data)を使ってSpotインスタンス起動時に自分自身をALBのターゲットグループへ登録させる
- AWS LambdaとCloudWatch Eventsを使って定期的にSpotFleet内の各インスタンスをALBのターゲットグループに登録する
ALBのターゲットグループへの登録は、
- インスタンスメタデータ または
ec2 describe-spot-fleet-instances
コマンド elbv2 register-targets
コマンド
を使うことで実現できます。AWS Lambdaで実現する場合にも、コマンドと同等のAPIが AWS JavaScript SDK をはじめとした各言語のSDKに存在します。
一見、一度だけ実行される処理でターゲットグループのメンテナンスができるので、 1. のやり方の方が賢いように思えますね。
しかし、運用を考えるとどうでしょう。ECSを使っているので、 ECS-optimized AMI を使ってSpotFleetリクエストを作ることになります。ところがこのAMI、AWS CLIがインストールされていません。当初、cloud-initを使ったコマンド実行のために、ECS-optimized AMIを元にAWS CLIをインストールしたマイAMIを作っておこうか、と考えました。しかし、エージェントやDockerのバージョンアップに伴い、ECS-optimized AMIが更新されることがあります。今回従事していたプロジェクトでも、構築中に ap-northeast-1
リージョンのAMIが ami-c8b016a9
から ami-9cd57ffd
にアップデートされていました。AMIのアップデートにあわせて毎回AMIを作りなおす、というのは、運用上のデメリットです。
マイAMIを作らない 1. のやり方として、SpotFleet作成時のuser-dataに
#!/bin/bash
echo ECS_CLUSTER=my_ecs_cluster_name >> /etc/ecs/ecs.config
yum install -y aws-cli
export INSTANCEID=`curl http://169.254.169.254/latest/meta-data/instance-id`
aws elbv2 register-targets --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:************:targetgroup/my/0000000000000000 --targets Id=$INSTANCEID --region ap-northeast-1
と記述しておく方法もあります。これはとても美しい解決法に思えますが、ターゲットグループの追加・変更がある場合、その度にSpotFleetを作り直さなければいけません。今回のプロジェクトでは複数のALB・複数のターゲットグループを利用しているため、user-dataとSpotFleetの管理が煩雑になると、運用上のデメリットになるおそれがあります。これを避けるためには、user-dataでS3に格納したシェルスクリプトを取得・実行するような方法をとるか、 2. のやり方か、となります。
今回のプロジェクトでは、user-dataのメンテナンスが不要な 2. を選択しました。
Lambdaファンクションの一例
var AWS = require('aws-sdk');
// AWS.config.update({accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY});
AWS.config.region = 'ap-northeast-1';
exports.handler = (event, context, callback) => {
console.log(event.SpotFleetRequestId);
console.log(event.TargetGroupArn);
console.log(event.TargetPort);
var ec2 = new AWS.EC2();
var params = {
SpotFleetRequestId: event.SpotFleetRequestId
};
ec2.describeSpotFleetInstances(params, function (err, data) {
if (err) { // an error occurred
console.log(err, err.stack);
} else { // successful response
console.log(data.ActiveInstances);
var elbv2 = new AWS.ELBv2();
data.ActiveInstances.forEach(function (instance) {
console.log(instance.InstanceId);
var params = {
TargetGroupArn: event.TargetGroupArn,
Targets: [{
Id: instance.InstanceId,
Port: event.TargetPort
}]
};
elbv2.registerTargets(params, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
});
}
});
};
コードの内容自体は、AWS JavaScript SDKの EC2#describeSpotFleetInstances() と ELBv2#registerTargets() を使ったシンプルなものです。ログはCloudWatch Logsに出力されます。
IAMロールを利用して起動するため、 ACCESS_KEY
と SECRET_KEY
は書きません。その代わりに、Lambdaファンクションが利用するIAMロールで次のようなロールポリシーを利用します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:DescribeSpotFleetInstances",
"elasticloadbalancing:RegisterTargets"
],
"Resource": "*"
}
]
}
ec2:DescribeSpotFleetInstances
と elasticloadbalancing:RegisterTargets
は Resource レベルポリシーではないので、必ず "Resource": "*"
としましょう。このあたり、 IAM Policy Simulator を使いながら記述していると非常にわかりやすいです。
あとは、このLambdaファンクションを起動するときに、必要な3つの引数を与えてやればOKです。
{
"SpotFleetRequestId": "sfr-12345678-90ab-cdef-1234-************",
"TargetGroupArn" : "arn:aws:elasticloadbalancing:ap-northeast-1:************:targetgroup/my/0000000000000000",
"TargetPort" : "8080"
}
これをCloudWatch Eventsでスケジュール実行してあげれば、手を煩わせることなく運用できますね。SpotFleetやターゲットグループが変わっても、このパラメータを変更してやれば良いわけです。
気持ち良いッ!
インフラを管理する上でもサーバレスの道具は役に立つ
夢のないことを言うと、サーバが消えることはありません。サーバサイドアプリケーションやRDBMSに対して蓄積されてきたノウハウは、今も多くのシステムを安定運用させています。システムの規模が大きければ大きいほど、サーバレスアーキテクチャへの移行は困難を伴うでしょう。書き換えなければいけないサーバサイドアプリケーションの規模、置き換えるべきインフラ構成、可用性の再検証...。それよりも、ビジネス上の新しい課題解決が優先されるのが常です。
では、それでもなぜサーバレスに夢を見るのか?サーバレスの道具たちは、それぞれにスケーラブルかつ疎結合を指向して進化しています。今まで、実行に何らかのサーバを選ぶ必要があった「仕組み」を、それ単体に分離して実行できるようになっています。特に、AWS Lambdaは、AWS SDKを通してAWS上のシステム管理上必要になる「仕組み」を独立して管理することができます。これは、サーバサイドアプリケーション開発者とインフラエンジニアの間に発生する痒いところを埋める「万能接着剤」とも考えられるのです。
開発や運用の課題を解決する手段は、なるべくたくさん知っておいて損はありません。一気呵成に既存システム全体をサーバレスにすることは難しくても、スケーラブル・疎結合な「仕組み」からの学びは大いにあります。
おわりに
「うちのシステムはサーバサイドアプリケーションとRDBMSの構成だし、変えるつもりもないからサーバレスの知識はいらない」...本当にそうでしょうか?サーバレスの世界には、サーバベースのシステムを構築・運用する上でも役に立つ道具や知見があるのです。
その課題、本当にサーバが必要ですか?サーバレスに考えたらシステム運用がもっとシンプルになりませんか?