Amazon Web Services ブログ
Amazon ECSイベントストリームで、クラスタの状態を監視
今までは、実行中のAmazon ECSクラスタの状態の更新を取得するためには、AWS CLIやSDKを使ってコンテナインスタンスとタスクの状態を定期的にポーリングする必要がありました。新しいAmazon ECSイベントストリーム機能によって、これからはAmazon ECSのタスクとコンテナインスタンスの状態更新を準リアルタイムにイベント駆動で受け取ることが可能になりました。イベントはAmazon CloudWatch Eventsを通して配送され、AWS Lambda関数やAmazon SNSトピックといった、あらゆるCloudWatch Eventsのターゲットに向けることができます。
この記事では、簡単なサーバレスアーキテクチャを作って、イベントストリームの更新を受け取り、処理し、そして保存する方法をお見せします。まず最初にLambda関数を作成し、入ってきた全てのイベントをスキャンして実行中のタスクに関連したエラーが無いかを探し、もしあれば即座にSNSの通知を送ります。その後、関数は全てのメッセージをAmazon Elasticsearch Serviceを使ったElasticsearchのクラスタにドキュメントとして保存することで、皆さんや開発チームの方がKibanaのインタフェースを通じてクラスタの状態を監視したり、ユーザから報告された問題に返答するための診断情報を検索することができます。
イベントストリームのイベントの構造を理解する
ECSイベントストリームは2種類のイベント通知を送ります:
- タスク状態変更通知: タスクが開始または停止した時に発火する
- コンテナインスタンス状態変更通知: インスタンス上のリソース利用や確保が変更した時に発火する
ECS上の1つのイベントが、両方の種類の複数の通知を送ることになることがあります。例えば、新しいタスクが開始すると、ECSはまずタスク状態変更通知としてタスクが始まろうとしているというシグナルを送り、続いてそのタスクが開始した(または失敗した)時にも通知が送られます。さらに加えて、ECSがそのタスクを起動しようとしたインスタンスの利用が変わるとコンテナインスタンス状態変更通知も発火されます。
イベントストリームのイベントはCloudWatch Eventsを使って送られ、2つのセクション(エンベロープとペイロード)からなるJSONメッセージの構造を持っています。各イベントのdetailセクションにはペイロードデータが含まれ、その構造は発火されたイベントに特化したものとなっています。以下の例では、コンテナの状態変更通知イベントのJSON表現をお見せします。JSONのトップレベルのプロパティは、イベントの名前や発生した時間といったイベントのプロパティを表している一方、detailセクションはそのイベントを引き起こしたタスクやコンテナインスタンスの情報を含んでいます。
以下のJSONはECSのタスク状態変更イベントで、ECSクラスタ上で実行しているタスク内のessentialコンテナが終了していて、それによってECSクラスタ上でタスクが停止したことが明記されています:
{
"version": "0",
"id": "8f07966c-b005-4a0f-9ee9-63d2c41448b3",
"detail-type": "ECS Task State Change",
"source": "aws.ecs",
"account": "244698725403",
"time": "2016-10-17T20:29:14Z",
"region": "us-east-1",
"resources": [
"arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
],
"detail": {
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/eventStreamTestCluster",
"containerInstanceArn": "arn:aws:ecs:us-east-1:123456789012:container-instance/f813de39-e42c-4a27-be3c-f32ebb79a5dd",
"containers": [
{
"containerArn": "arn:aws:ecs:us-east-1:123456789012:container/4b5f2b75-7d74-4625-8dc8-f14230a6ae7e",
"exitCode": 1,
"lastStatus": "STOPPED",
"name": "web",
"networkBindings": [
{
"bindIP": "0.0.0.0",
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
}
],
"createdAt": "2016-10-17T20:28:53.671Z",
"desiredStatus": "STOPPED",
"lastStatus": "STOPPED",
"overrides": {
"containerOverrides": [
{
"name": "web"
}
]
},
"startedAt": "2016-10-17T20:29:14.179Z",
"stoppedAt": "2016-10-17T20:29:14.332Z",
"stoppedReason": "Essential container in task exited",
"updatedAt": "2016-10-17T20:29:14.332Z",
"taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328",
"taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/wpunconfiguredfail:1",
"version": 3
}
}
Elasticsearchクラスタをセットアップする
イベントを処理するコードの中身に入る前に、Elasticsearchクラスタをセットアップします。コンソールからElasticsearch Serviceを選択し、Create a New Domainを選択します。Elastcsearch domain nameに、elasticsearc-ecs-eventsと入力し、Nextを選択します。
Step 2のConfigure clusterでは全てデフォルトのままNextを選択します。
Step 3のSet up access policyではNextを選びます。このページでは、クラスタへのアクセスをリソースベースポリシーでクラスタのアクションのアクセス許可をしたり、アイデンティティベースでLambda関数を紐付けることができます。
最後に、ReviewページでConfirm and createを選択します。これでクラスタが起動し始めます。
クラスタが作成されている間に、イベントについての通知を受け取り処理するために必要なSNSトピックとLambda関数をセットアップしましょう。
SNSトピックを作成
Lambda関数がタスクがエラーによって思いがけず失敗してしまった時にメールを送るために、Amazon SNSのトピックを作成しLambda関数が書き込めるようにする必要があります。
コンソールから、SNSを選び、Create Topicを選びます。Topic nameにはECSTaskErrorNotificationと入力し、Create topicを選びます。
完了したら、Topic ARNの値をコピーしローカルデスクトップのテキストエディタに保存しておきます。次のステップでLambda関数の権限を設定するときにこれを使います。最後に、Create subscriptionを選択しお持ちのEメールアドレスで購読することで、イベント通知を受け取ることができるようになります。確認メール内のリンクをクリックするのを忘れないで下さい、そうでないとどんなイベントも受け取れません。
鋭い方は、これから作るLambda関数にSNSトピックを呼び出す権限を与えていないことに気がつくと思います。これは、次のステップでLambda関数を作成する時に、Lambda execution roleに権限を許可していきます。
Lambda関数でイベントストリームのイベントを処理する
次のステップでは、イベントを捕捉するためのLambda関数を作成します。こちらがそのためのコードになります(Python 2.7で書かれてます):
import requests
import json
from requests_aws_sign import AWSV4Sign
from boto3 import session, client
from elasticsearch import Elasticsearch, RequestsHttpConnection
es_host = '<insert your own Amazon ElasticSearch endpoint here>'
sns_topic = '<insert your own SNS topic ARN here>'
def lambda_handler(event, context):
# Establish credentials
session_var = session.Session()
credentials = session_var.get_credentials()
region = session_var.region_name or 'us-east-1'
# Check to see if this event is a task event and, if so, if it contains
# information about an event failure. If so, send an SNS notification.
if "detail-type" not in event:
raise ValueError("ERROR: event object is not a valid CloudWatch Logs event")
else:
if event["detail-type"] == "ECS Task State Change":
detail = event["detail"]
if detail["lastStatus"] == "STOPPED":
if detail["stoppedReason"] == "Essential container in task exited":
# Send an error status message.
sns_client = client('sns')
sns_client.publish(
TopicArn=sns_topic,
Subject="ECS task failure detected for container",
Message=json.dumps(detail)
)
# Elasticsearch connection. Note that you must sign your requests in order
# to call the Elasticsearch API anonymously. Use the requests_aws_sign
# package for this.
service = 'es'
auth=AWSV4Sign(credentials, region, service)
es_client = Elasticsearch(host=es_host,
port=443,
connection_class=RequestsHttpConnection,
http_auth=auth,
use_ssl=True,
verify_ssl=True)
es_client.index(index="ecs-index", doc_type="eventstream", body=event)
中身を解説します: まず、受け取ったイベントかタスク変更イベントかどうかを判定します。もしそうなら、イベントの中にタスクの停止が報告されているかを見て、それがessentialコンテナの終了によって停止したものかどうかを確認します。これらの条件に合致した場合、先程作成したSNSトピックに通知を送ります。
次に、Amazon ESインスタンスへのElasticsearchの接続を作成します。Amazon ESを呼び出すには全てのリクエストをSig4で署名する必要があるので、ここではrequests_aws_signライブラリを利用してSig4の署名を行います。Sig4の署名が生成されたら、Amazon ESを呼び出し後で取り出して検証をできるようにindexにイベントを追加します。
このコードを実行するためには、Lambda関数はHTTP POSTをAmazon ESインスタンスにリクエストでき、SNSトピックにメッセージをパブリッシュできる権限を持っている必要があります。自身のアカウント内で適切な権限を持ったexecution roleを、Lambda関数に設定します。
では、まず始めに上記のコードと必要なものを含んだZIPファイルを準備する必要があります。lambda_eventstreamという名前のディレクトリを作成し、上のコードをlambda_function.pyという名前のファイルに保存します。お好みのテキストエディタでes_hostとsns_topicの値を、それぞれAmazon ESエンドポイントとSNSトピックARNに置き換えます。
次に、コマンドライン(Linux、Windows、またはMac)上で、先程作ったディレクトリに入り、このコードに必要な全てをディレクトリ内にダウンロードするために、以下のコマンドでpip (Pythonのインストールユーティリティのデファクトスタンダード)を実行します。これらの依存はLambda関数が実行されるインスタンスには事前にインストールされていないので、コードと一緒に配置する必要があります。
注意: Pythonとpipは既にインストールされている必要があります。もしPython 2.7.9以上を使っていれば、pipはPythonのインストールの一部としてインストールされています。もしPython 2.7.9以上でなければ、インストール手順としてpipのページをご覧ください。
pip install requests_aws_sign elasticsearch -t .
最後に、このディレクトリの全ての内容を1つのzipファイルにまとめます。zipファイルの中で、lambda-eventstream.pyが階層の一番上に位置していて、他のディレクトリの下に入っていないことを確認しておいて下さい。lambda-evenstreamディレクトリの中で、LinxuとMacOSシステムでは以下のコマンドを使うことができます:
zip lambda-eventstream.zip *
Windowsで7-Zipをインストールしているなら、PowerShellかコマンドプロンプトで以下のコマンドを実行することもできます:
7z a -tzip lambda-eventstream.zip *
これで関数とその依存が正しくパッケージ化されたので、インストールしてテストしてみます。Lambdaコンソールに行き、Create a Lambda Functionを選択し、Select BlueprintページではBlank functionを選択します。Configure triggers画面ではNextを選択します。関数をECSイベントストリームと紐付けるのは次のセクションで実施します。
Configure fucntionページでは、Nameにlambda-eventstreamと入力します。RuntimeとしてPython 2.7を選択します。Lambda function codeの中では、Code entry typeでUpload a .ZIP fileを選びUploadを選択して先程作成したZIPファイルを選択します。
Lambda function handler and roleの中では、RoleでCreate a custom roleを選択します。これでポリシーを設定するための新しいウィンドウが開きます。IAM RoleではCreate a New IAM Roleを選択し、名前を入力します。それからView Policy Document、Editと選択します。以下のIAMポリシーを、AWSAccountIDを自身のAWSアカウントIDに置き換えながら、貼り付けます。
{
"Version":"2012-10-17",
"Statement":[
{
"Effect":"Allow",
"Action":"lambda:InvokeFunction",
"Resource":"arn:aws:lambda:us-east-1:<AWSAccountID>:function:ecs-events",
"Principal":{
"Service":"events.amazonaws.com"
},
"Condition":{
"ArnLike":{
"AWS:SourceArn":"arn:aws:events:us-east-1:<AWSAccountID>:rule/eventstream-rule"
}
},
"Sid":"TrustCWEToInvokeMyLambdaFunction"
},
{
"Effect":"Allow",
"Action":"logs:CreateLogGroup",
"Resource":"arn:aws:logs:us-east-1:<AWSAccountID>:*"
},
{
"Effect":"Allow",
"Action":[
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource":[
"arn:aws:logs:us-east-1:<AWSAccountID>:log-group:/aws/lambda/ecs-events:*"
]
},
{
"Effect": "Allow",
"Action": [
"es:ESHttpPost"
],
"Resource": "arn:aws:es:us-east-1:<AWSAccountID>:domain/ecs-events-cluster/*"
},
{
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": "arn:aws:sns:us-east-1:<AWSAccountID>:ECSTaskErrorNotification"
}
]
}
このポリシーはLambda関数を実行するのに必要な全ての権限を持っていて、中には以下の権限が含まれています:
- CloudWatch Logsのロググループを作成し、Lambda関数の全ての出力をこのグループに保存する
- Elasticsearchクラスタに対しHTTP PUTコマンドを実行する
- SNSトピックにメッセージをパブリッシュする
これが終わったら、設定をテストするためにこの記事の最初の方にあるサンプルのイベントストリームメッセージを使って、コンソールからLambda関数をテストしてみます。新しい関数のダッシュボードページ上で、Testを選択し、Input test eventウィンドウに先程JSON形式のイベントを入力します。
もしIAMポリシーファイルの中で正しいアカウントIDを正しい箇所に入力していないと、こんな行から始まるメッセージを受信するでしょう:
User: arn:aws:sts::123456789012:assumed-role/LambdaEventStreamTake2/awslambda_421_20161017203411268 is not authorized to perform: es:ESHttpPost on resource: ecs-events-cluster.
その時は、Lambdaのexecution roleをIAMコンソールから編集して、再実行してみて下さい。
イベントストリームをLambda関数に送る
もうすぐです!これでSNSトピック、Elasticsearchクラスタ、そしてLambda関数が全て揃ったので、残る要素はECSイベントストリームイベントがLambda関数に流れるようにするだけです。CloudWatch Eventsのコンソールだけで素早く簡単にこれを実行できます。
コンソールからCloudWatch、Eventsと選択します。Step 1: Create Ruleの中で、Event selectorではAmazon EC2 Container Serviceを選択します。CloudWatch Eventsはメッセージのタイプ(タスク状態変化かコンテナインスタンス状態変化)によってフィルタすることや、イベントを受け取りたい特定のクラスタを選択することもできます。この記事では、Any detail typeとAny clusterというデフォルト設定を維持しておきます。
Targetsでは、Lambda functionを選択します。Functionとしてlambda-eventstreamを選択します。この裏では、ECSクラスタがLambda関数にイベントを送る様になり、CloudWatch EventsがLambda関数を呼び出すのに必要なservice roleも作成されます。
動作を確認する
それでは、ECSクラスタからメッセージを送って、Lambda関数を通り、失敗したタスクのSNSメッセージを発火させ、将来の取り出しのためにElasticsearchクラスタに保存されていることを確認してみます。このワークフローをテストするために、以下の様な公式のWordPressのイメージをストレージのSQLデータベースの設定無しに開始しようとするタスク定義を使うことができます。
{
"taskDefinition": {
"status": "ACTIVE",
"family": "wpunconfiguredfail",
"volumes": [],
"taskDefinitionArn": "arn:aws:ecs:us-east-1:244698725403:task-definition/wpunconfiguredfail:1",
"containerDefinitions": [
{
"environment": [],
"name": "web",
"mountPoints": [],
"image": "wordpress",
"cpu": 99,
"portMappings": [
{
"protocol": "tcp",
"containerPort": 80,
"hostPort": 80
}
],
"memory": 100,
"essential": true,
"volumesFrom": []
}
],
"revision": 1
}
}
このタスク定義は、AWSマネージメントコンソールやAWS CLIを使って作成し、このタスク定義からタスクを開始します。より詳細な手順はCreating a Task DefinitionとRunning Taksをご覧下さい。
このタスク定義の起動を始めてから数分すると、タスクが失敗したということを示すタスク状態変化のJSONを含んだSNSメッセージを受信するでしょう。また、コンソールでクラスタの名前を選び、Indices、ecs-indexと選んでElasticsearchクラスタを調べることができます。Countを見ると、複数のレコードが保存されていることが分かるはずです。
また、Kibanaエンドポイントにアクセスすることで、保存されたメッセージを検索することもできます。KibanaはAmazon ESに保存されたデータに対し可視化や検索を行う力を与えてくれます。Kinabaへローカルからアクセスするために、コンピュータのIPアドレスを見つけてから、ElasticsearchクラスタのModify access policyを選択します。Set the domain access policy toでAllow access to the domain from specific IP(s)を選び、先程のIPアドレスを入力します。
(より安定してスケール可能なKibanaを守るソリューションは前段にプロキシを置くことです。このアプローチの詳細はLarthi ThyagarajanのHow to Control Access to Your Amazon Elasticsearch Service Domainという記事で見ることができます。)
これで、クラスタのKibana endpointを実行することができ、クラスタのindexに保存されたメッセージを検索できます。
まとめ
以上の様なECSクラスタに関連したイベント通知を消費する基本的なサーバレスアーキテクチャのセットアップをしてみたら、可能性は無限大です。例えば、イベントをAmazon ESに保存する代わりに、Amazon DynamoDBに保存して結果のテーブルを使って現在のクラスタの状態をマテリアライズしたUIを構築することもできます。
またこの情報を使って、コンテナの配置とスケールを自動化することも可能で、クラスタを非常に精密なレベルで”正しいサイズ”にすることができます。プル型ではなくイベント駆動でクラスタの状態の情報を準リアルタイムで配送することにより、ECSイベントストリーム機能はコンテナインフラの監視とスケールに対して非常に広範囲な可能性を提供しています。
原文: Monitor Cluster State with Amazon ECS Event Stream (翻訳: SA岩永)