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

# はじめに

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

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

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

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

# 構成と流れ

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

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

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

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

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

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

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

```go
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(startServices)
}

func startServices() {
	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の指定したクラスターとサービスを起動させています。

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

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


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

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

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

```go
	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)
```

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

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

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

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

```go
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(stopServices)
}

func stopServices() {
	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時には停止するようにします。

![picture_pc_d839f2a948a4ab1993d602d89bcf7b81.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294580/998a8cbc-2189-8850-decd-bb46f34210d4.png)

![picture_pc_9b820fdbab9705c3ae6b7d9adfa40dfa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294580/6cf97a86-4cc1-d991-9add-5f794d233db8.png)

# 実行ロール設定

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

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

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

```go
{
   "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関数に実行ロールに付与すれば、自動化は完成です。

![picture_pc_6b9ef66a8718f6b7ce702f58673287f4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294580/eb1f2e72-70df-887a-f471-7ad226122051.png)

# 結果

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

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

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