🍞

【AWS】ECSタスクイベントのログを取るLambdaを作成する

2021/10/03に公開

はじめに

Fargateはインスタンスのお世話をせずに、コンテナの運用ができて便利ですよね。
しかし、インスタンスのログを見られないので、ECSタスクの状態変化が追えず困ることがあります。
「コンテナを起動するAPIは呼び出されているはずなのに、アプリケーションが実行されていない…」なんてときには、何が起こっているのかは闇の中です。

公式ドキュメントによると、ECSタスクイベントをCloudWatch EventsでキャプチャしてLambdaに流すことで、タスクイベントのログを取ることができるようです。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs_cwet.html

今回は☝︎のドキュメントを参考に、AWS CDKで実際にリソースを作成してみます。
CDKはTypeScript、Lambdaの関数はGoを使って実装します

バージョン

  • node 12.18.1
  • go 1.17
  • cdk 1.125.0

事前に準備しておくリソース

  • ECS Cluster

ディレクトリ構成

.
├── lambda
│   ├── go.mod
│   ├── go.sum
│   └── main.go
├── bin
│   └── cdk-app.ts
├── lib
│   └── listen-ecs.ts
()

CDKソースコード

import * as cdk from '@aws-cdk/core';
import * as targets from '@aws-cdk/aws-events-targets';
import * as events from '@aws-cdk/aws-events';
import * as lambda from '@aws-cdk/aws-lambda-go';


export class ListenEcsStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
        const clusterArn = this.node.tryGetContext('cluster-arn')

        const fn = new lambda.GoFunction(this, 'listen-ecs-function', {
            functionName: 'listen-ecs-function',
            entry: 'lambda',
        })

        const targetFunction = new targets.LambdaFunction(fn)

        new events.Rule(this, 'EcsTaskChangeRule', {
            eventPattern: {
                source: ['aws.ecs'],
                detail: { 'clusterArn': [clusterArn] },
                detailType: ['ECS Task State Change']
            },
            targets: [targetFunction],
        });
    }
}

事前に作成したECS ClusterのARNを、cdk.jsonまたはコマンドでコンストラクタ外部から渡してあげるようにします。
Goのソースコードのパスを指定してLambdaの関数を作成し、作成した関数をCloudWatch Eventsのルールのターゲットとすることで、イベントが発生するごとに関数にイベント情報が流れていきます。
ルールのソースにはaws.ecsを指定し、特定のクラスターの、特定のタスクイベントのみマッチするパターンを設定します。

Lambdaソースコード

package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(ctx context.Context, event events.CloudWatchEvent) error {
	b, err := json.Marshal(event)
	if err != nil {
		return err
	}
	fmt.Println(string(b))
	return nil
}

func main() {
	lambda.Start(handler)
}

ルールのターゲットとなる関数は、発生したイベントを受け取り処理することができます。
イベントはCloudWatchEventという型の値として、流れてきます。
構造体のまま出力すると少し見辛いので、JSONに変換して出力しています。

type CloudWatchEvent struct {
	Version    string          `json:"version"`
	ID         string          `json:"id"`
	DetailType string          `json:"detail-type"`
	Source     string          `json:"source"`
	AccountID  string          `json:"account"`
	Time       time.Time       `json:"time"`
	Region     string          `json:"region"`
	Resources  []string        `json:"resources"`
	Detail     json.RawMessage `json:"detail"`
}

https://pkg.go.dev/github.com/aws/aws-lambda-go@v1.27.0/events#CloudWatchEvent

ログ確認

適当にECSタスクを作成して、出力されたログを確認します。

{
    "version": "0",
    "id": "44f7e6da-de4d-7a11-e35c-c0155c8be065",
    "detail-type": "ECS Task State Change",
    "source": "aws.ecs",
    "account": "xxxxxxxxxxx",
    "time": "2021-10-02T06:58:20Z",
    "region": "ap-northeast-1",
    "resources": [
        "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:task/sample/bb462024fc644eefbe5495695e405414"
    ],
    "detail": {
        "attachments": [
            {
                "id": "663cfa45-2caa-42df-ac53-26ea91c20a54",
                "type": "eni",
                "status": "PRECREATED",
                "details": [
                    {
                        "name": "subnetId",
                        "value": "subnet-6bae7040"
                    }
                ]
            }
        ],
        "availabilityZone": "ap-northeast-1d",
        "clusterArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:cluster/sample",
        "containers": [
            {
                "containerArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:container/sample/bb462024fc644eefbe5495695e405414/036b4462-e0c1-467b-9103-dec662299434",
                "lastStatus": "PENDING",
                "name": "sample",
                "image": "xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/repository:latest",
                "taskArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:task/sample/bb462024fc644eefbe5495695e405414",
                "networkInterfaces": [],
                "cpu": "0"
            }
        ],
        "cpu": "256",
        "createdAt": "2021-10-02T06:58:20.45Z",
        "desiredStatus": "RUNNING",
        "enableExecuteCommand": false,
        "ephemeralStorage": {
            "sizeInGiB": 20
        },
        "group": "family:sample",
        "launchType": "FARGATE",
        "lastStatus": "PROVISIONING",
        "memory": "512",
        "overrides": {
            "containerOverrides": [
                {
                    "name": "sample"
                }
            ]
        },
        "platformVersion": "1.4.0",
        "taskArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:task/sample/bb462024fc644eefbe5495695e405414",
        "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxx:task-definition/sample:6",
        "updatedAt": "2021-10-02T06:58:20.45Z",
        "version": 1
    }
}

構造体をそのままJSONに変換したので情報量が結構多いですね。
実際の運用の際には項目を精査する必要がありそうです。

本記事での記載は割愛しますが、開始から終了まで状態が変化するごとにログが出力されていることが確認できました。

さいごに

サーバーレスでインフラを構築している時こそ、ログやアラームの設定をより丁寧にしておくことが必要なのかなと思っています。
なにか不具合が起こった時にサーバーをチェックすることが難しいことを踏まえた上で、サーバーレスと付き合っていきたい所存であります。

参考

Discussion