michimani.net

AWS SDK for Go V2 を使って Fargate for ECS でタスクを実行してコンテナが起動するまでの時間を計測してみた

2021-02-20

ECS と Fargate を使ってバッチ処理とか実行するのは楽だなという気持ちになっているのですが、気になるのはタスクを作成してから実際にアプリケーションが実行されるまでの時間です。今回はその時間を計測してみます。最近リリースされた Go 1.16 と AWS SDK for Go V2 を使って計測用のコードを書きました。

目次

はじめに

Fargate for ECS でタスクを実行し、実際に Fargate でコンテナが起動するまでの時間を計測します。

タスクの実行時には、タスクの情報として CreatedAt が記録され、コンテナ起動時には StartedAt が記録されます。なので、この差分を計測するのが今回の目的です。

AWS CLI を使う場合、タスクの実行には ecs run-task コマンドを、タスクの情報を取得するには ecs-describe-tasks コマンドをそれぞれ実行します。ただ、コンテナ起動までの時間をコマンドで計測するのはちょっとめんどくさそうなので、今回はタスクの実行から時間の計測までを行うスクリプトを AWS SDK for Go V2 を使って実装してみます。

また、最近リリースされた Go 1.16 の機能の中から go:embed も使ってみます。

なお、今回の計測にあたっては下記の記事を参考にさせていただきました🙇🏻‍♂️

タスクで実行するコンテナイメージ

今回は起動すればいいだけなので、下記のような Dockerfile でイメージを作成します。

FROM busybox:latest
ARG PAD=0
RUN dd if=/dev/urandom of=/padding bs=1M count=$PAD

ビルド時に PAD に値を指定して、任意のサイズのイメージを作成します。今回は 256MB のイメージを作成します。

$ docker build -t fargate-speed-test . --build-arg PAD=256

# イメージサイズの確認 ("だいたい" 256 MB)
$ docker images fargate-speed-test
REPOSITORY           TAG       IMAGE ID       CREATED       SIZE
fargate-speed-test   latest    fc479f1d67e2   4 hours ago   270MB

これを ECR に push しておきます。このあたりの詳しいコマンド操作については前回の記事を参照してください。

ECS タスクを作って実行して CloudWatch でログを確認するまでを AWS CLI だけでやってみた - michimani.net

起動時間を計測するコード

タスクを実行してコンテナ起動までの時間を計測するコードを書きます。

package main

import (
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ecs"
	"github.com/aws/aws-sdk-go-v2/service/ecs/types"
)

type AppConfig struct {
	Cluster           string `json:"cluster"`
	TaskDefinitionArn string `json:"taskDefinitionArn"`
	SubnetID          string `json:"subnetId"`
	Region            string `json:"region"`
}

//go:embed config.json
var configJson []byte

// runECSTask runs ECS task
func runECSTask() error {
	var appConfig AppConfig
	if jerr := json.Unmarshal(configJson, &appConfig); jerr != nil {
		return jerr
	}

	cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(appConfig.Region))
	if err != nil {
		return err
	}

	client := ecs.NewFromConfig(cfg)
	runTaskIn := &ecs.RunTaskInput{
		TaskDefinition: aws.String(appConfig.TaskDefinitionArn),
		Cluster:        aws.String(appConfig.Cluster),
		NetworkConfiguration: &types.NetworkConfiguration{
			AwsvpcConfiguration: &types.AwsVpcConfiguration{
				Subnets: []string{
					appConfig.SubnetID,
				},
				AssignPublicIp: types.AssignPublicIpEnabled,
			},
		},
		LaunchType: types.LaunchTypeFargate,
	}

	runOut, rerr := client.RunTask(context.TODO(), runTaskIn)
	if rerr != nil {
		return rerr
	}

	taskArn := aws.ToString(runOut.Tasks[0].TaskArn)
	fmt.Println("Task Created.")
	fmt.Println("\tTaskARN: ", taskArn)
	fmt.Printf("\taws ecs describe-tasks --cluster %s --tasks %s\n\n", appConfig.Cluster, taskArn)

	waiter := ecs.NewTasksStoppedWaiter(client)
	waitParams := &ecs.DescribeTasksInput{
		Tasks:   []string{taskArn},
		Cluster: aws.String(appConfig.Cluster),
	}
	maxWaitTime := 5 * time.Minute

	if werr := waiter.Wait(context.TODO(), waitParams, maxWaitTime); werr != nil {
		return werr
	}

	describeIn := &ecs.DescribeTasksInput{
		Tasks:   []string{taskArn},
		Cluster: aws.String(appConfig.Cluster),
	}
	stopOut, serr := client.DescribeTasks(context.TODO(), describeIn)
	if serr != nil {
		return serr
	}

	createdAt := *stopOut.Tasks[0].CreatedAt
	startedAt := *stopOut.Tasks[0].StartedAt
	stoppedAt := *stopOut.Tasks[0].StoppedAt
	takenTime := startedAt.Sub(createdAt)

	fmt.Println("Task Stopped.")
	fmt.Println("\tTaskARN: ", aws.ToString(stopOut.Tasks[0].TaskArn))
	fmt.Println("\tCreatedAt: ", createdAt)
	fmt.Println("\tStartedAt: ", startedAt)
	fmt.Println("\tStoppedAt: ", stoppedAt)
	fmt.Printf("\tTakenTimeToStart: %s", takenTime)

	return nil
}

func main() {
	if err := runECSTask(); err != nil {
		fmt.Println(err.Error())
	}
}

gist - This is a script using the AWS SDK for Go V2 and Go 1.16. Measure the time it takes to launch AWS Fargate for ECS.

中身を簡単に解説しておきます。

go:embed

import (
	_ "embed"
)

//go:embed config.json
var configJson []byte

この部分で、 Go 1.16 で実装された go:embed を使っています。
何をしているかというと、上の記述ではこの main.go と同じディレクトリにある config.json を byte のスライスとして configJson に代入しています。そしてその configJsonAppConfig の構造体に変換しています。

go:embed を使って埋め込まれるファイルはバイナリにも含まれるため、ウェブアプリケーションであれば画像や CSS などを go:embed で埋め込むことで諸々を一つのバイナリに含めることができます。 go:embed および Go 1.16 のその他のリリース内容については下記の記事で詳しく解説されています。

Go 1.16連載が始まります | フューチャー技術ブログ

config.json では、 ECS タスクの実行に最低限必要な情報を保持しています。

{
  "cluster": "fargate-speed-test-cluster",
	"taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:000000000000:task-definition/fargate-speed-test:5",
	"subnetId": "subnet-06ee4b00000000000",
  "region": "ap-northeast-1"
}

ECS タスクの実行

下記の部分で ECS タスクを実行しています。

cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(appConfig.Region))
if err != nil {
	return err
}

client := ecs.NewFromConfig(cfg)
runTaskIn := &ecs.RunTaskInput{
	TaskDefinition: aws.String(appConfig.TaskDefinitionArn),
	Cluster:        aws.String(appConfig.Cluster),
	NetworkConfiguration: &types.NetworkConfiguration{
		AwsvpcConfiguration: &types.AwsVpcConfiguration{
			Subnets: []string{
				appConfig.SubnetID,
			},
			AssignPublicIp: types.AssignPublicIpEnabled,
		},
	},
	LaunchType: types.LaunchTypeFargate,
}
runOut, rerr := client.RunTask(context.TODO(), runTaskIn)
if rerr != nil {
	return rerr
}

ecs - Client.RunTask · pkg.go.dev

TasksStoppedWaiter でタスクが停止するまで待機

下記の部分で、実行したタスクが停止するまで待機しています。

waiter := ecs.NewTasksStoppedWaiter(client)
waitParams := &ecs.DescribeTasksInput{
	Tasks:   []string{taskArn},
	Cluster: aws.String(appConfig.Cluster),
}
maxWaitTime := 5 * time.Minute
if werr := waiter.Wait(context.TODO(), waitParams, maxWaitTime); werr != nil {
	return werr
}

ecs - NewTasksStoppedWaiter · pkg.go.dev

これにより、タスクが終了した状態のタスクの状態を取得でき、タスク実行からコンテナが起動するまでの時間を計測することができます。

実行してみる

$ go run main.go
Task Created.
        TaskARN:  arn:aws:ecs:ap-northeast-1:000000000000:task/fargate-speed-test-cluster/fc2a03112a0000000000xxxxxxxxxxxx
        aws ecs describe-tasks --cluster fargate-speed-test-cluster --tasks arn:aws:ecs:ap-northeast-1:000000000000:task/fargate-speed-test-cluster/fc2a03112a0000000000xxxxxxxxxxxx

Task Stopped.
        TaskARN:  arn:aws:ecs:ap-northeast-1:000000000000:task/fargate-speed-test-cluster/fc2a03112a0000000000xxxxxxxxxxxx
        CreatedAt:  2021-02-19 15:33:20.308 +0000 UTC
        StartedAt:  2021-02-19 15:33:58.187 +0000 UTC
        StoppedAt:  2021-02-19 15:34:41.937 +0000 UTC
        TakenTimeToStart: 37.879s

37.879s かかったことがわかりました。

まとめ

AWS SDK for Go V2 を使って Farget for ECS でタスクを実行して、実際にコンテナが起動するまでの時間を計測してみた話でした。ついでに、 Go 1.16 で盛り込まれた go:embed も使ってみました。

今後やりたいこととしては、 ECS_IMAGE_PULL_BEHAVIOR を有効にしたときに起動までの時間がどれくらい短くなるのかを計測したいと思っています。
ECS (EC2) だと、コンテナイメージをキャッシュするための設定として ECS_IMAGE_PULL_BEHAVIOR が使えるのですが、この記事を書いている時点では Fargate に対応していません。

Amazon ECS on EC2でキャッシュされたコンテナイメージを使用するには? | DevelopersIO

なので、次はこの設定が使えるようになったら改めて計測して、どれだけ早くなるのか試してみたいと思います。

以上、よっしー (michimani) でした。