AWS節約術 : 自動で任意の時間にFargateを起動・停止する

はじめに

この記事は VALU Advent Calendar 2019 の16日目の記事です。

マイクロサービスではお馴染みのAWSのコンテナ向けサーバーレスコンピューティングエンジン「Fargate」。基本料金が高額な上、一般的に1つのサービスでもいくつも動かすため、他のAWS製品と比較しても、毎月かなり高額な請求がきます。

しかし、実際のところ常に動かす必要があるわけではないはずです。本番環境は常時稼働させる必要があるかもしれませんが、dev環境や検証環境などは、使っていない時間はそれなりにあるはずです(夜間など)。

なので弊社サービスのdev環境のFargateは、平日は10時に起動、夜の22時に停止。土日は常に停止という風に自動化しています。

今回はこの自動化の方法について共有することで、「現在Fargateを使っているが、いかんせん節約したい」といった同業者の手助けになれば思い投稿しました。

構成と流れ

・Fargateのサービスを操作するプログラムを書いたLambda関数を「起動用」「停止用」の2つを作る。

・CloudWatch Eventsで「起動用」「停止用」の時間を任意に設定する。

・必要な実行ロールを付与する。

起動・停止をさせるLambda関数

今回はGo言語で実装します。

仕様は、「1つのクラスターにある複数のサービスを起動・停止する」ものとします。

まず、起動用のLambda関数が以下になります。

package main

import (
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/aws/aws-sdk-go/aws/awserr"
)

const desiredCount int64  = 1

var cluster = "sample-cluster"

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

func startServies() {
	services := []string{
		"sample-service1",
		"sample-service2",
		"sample-service3",
		"sample-service4",
		"sample-service5",
	}

	for _, service := range services {
		svc := ecs.New(session.New())
		input := &ecs.UpdateServiceInput{
			Cluster:        aws.String(cluster),
			Service:        aws.String(service),
			DesiredCount:   aws.Int64(desiredCount),
		}
		result, err := svc.UpdateService(input)
		if err != nil {
			if aerr, ok := err.(awserr.Error); ok {
				fmt.Println(aerr.Code(), aerr.Error())
			}
			return
		}
		fmt.Println(result)
	}
}

やっていることはとてもシンプルで、aws-sdk-goというパッケージを用いて、ECSの指定したクラスターとサービスを起動させています。

もう少し細かく順番にみていきましょう。

まず、起動したいサービスたち(同クラスター上のものであることが条件)を列挙します。


	services := []string{
		"sample-service1",
		"sample-service2",
		"sample-service3",
		"sample-service4",
		"sample-service5",
	}

その後、指定した各サービスごとにfor文を回し、aws-sdk-goのUpdateServiceという機能で起動させていきます。

ここで事前に、引数のUpdateServiceInputという型にサービス情報を入力しますが、今回は実行するインスタンス数を示すdesiredCountは定数で全て1に設定しています。

	for _, service := range services {
		svc := ecs.New(session.New())
		input := &ecs.UpdateServiceInput{
			Cluster:        aws.String("sample-cluster"),
			Service:        aws.String(service),
			DesiredCount:   aws.Int64(desiredCount),
		}
		result, err := svc.UpdateService(input)

その後はパッケージのエラー処理。

		if err != nil {
			if aerr, ok := err.(awserr.Error); ok {
				fmt.Println(aerr.Code(), aerr.Error())
			}
			return
		}

続いて停止用のLambda関数になります。

といっても違いはdesiredCountと関数名くらいでやっていることは同じです。desiredCountを0にすることでタスクを完全に停止できます。

package main

import (
	"fmt"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ecs"
	"github.com/aws/aws-sdk-go/aws/awserr"
)

const desiredCount int64  = 0

var cluster = "sample-cluster"

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

func stopServies() {
	services := []string{
		"sample-service1",
		"sample-service2",
		"sample-service3",
		"sample-service4",
		"sample-service5",
	}

	for _, service := range services {
		svc := ecs.New(session.New())
		input := &ecs.UpdateServiceInput{
			Cluster:        aws.String(cluster),
			Service:        aws.String(service),
			DesiredCount:   aws.Int64(desiredCount),
		}
		result, err := svc.UpdateService(input)
		if err != nil {
			if aerr, ok := err.(awserr.Error); ok {
				fmt.Println(aerr.Code(), aerr.Error())
			}
			return
		}
		fmt.Println(result)
	}
}

最後にこれらをzip化して、それぞれ「起動用」と「停止用」のLambdaにアップロードします。

起動時間と停止時間を設定

それぞれのLambdaのトリガーに、CloudWatch Eventsで起動時間と停止時間をcronで設定します。

今回は、起動時間は平日の10時、停止時間は夜の22時に設定します。

停止時間を毎日に設定しているのは、休日に緊急対応で使う人がもし停止し忘れても、夜の10時には停止するよう

スクリーンショット 2019-12-16 10.35.53

スクリーンショット 2019-12-16 10.43.57

実行ロール設定

トリガーとプログラムが実行できるような実行ロールを付与してあげます。

今回はECSのServiceに関するものと、CloudWatch Eventsに関するものを付与します。

以下がポリシーのJSONです。

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Effect": "Allow",
           "Action": [
               "ecs:DescribeServices",
	       "ecs:UpdateService",
               "events:DescribeRule",
               "events:ListRuleNamesByTarget",
               "events:ListRules",
               "events:ListTargetsByRule",
               "events:TestEventPattern",
               "events:DescribeEventBus"
           ],
           "Resource": [
               "*"
           ]
       }
   ]
}

あとはこれをそれぞれのLambda関数に実行ロールに付与すれば、自動化は完成です。

スクリーンショット 2019-12-16 14.34.42

結果

こうした自動化の結果、平日に12時間稼働させるだけになったdev環境のFargateは、単純計算で約65%もの費用を削減できました。

一見、EC2よりも高額に見えるFargateですが、このように効率的に運用することにより、かえってコストパフォーマンスはよくなることもあります。

この機会にどなたかの倹約に役立てれば幸いです。