michimani.net

AWS Copilot を使ってスケジュールされた ECS タスクをデプロイする

2021-01-23

ECS でスケジュールされたタスクを AWS CLI で手動実行したい、でもそもそもそのタスクを用意するのが大変そう。ということで、昨年夏頃に発表されて秋には GA となった AWS Copilot を使ってスケジュールされたタスクを作成してみます。CLI での実行は次回。

目次

AWS Copilot とは

GitHub の README には次のように説明されています。

The AWS Copilot CLI is a tool for developers to build, release and operate production ready containerized applications on Amazon ECS and AWS Fargate.

aws/copilot-cli

コンテナ化されたアプリケーションを Amazon ECS で構築・リリース・運用するための CLI です、と。ECS を使ったアプリケーションの構築には、 VPC 作ったりタスク定義作ったりと、正直簡単ではないなという印象です。 AWS Copilot を使うとその辺の設定をよしなにやってくれます。

既に公式ドキュメントやいろんな方のブログで触れられているので、詳細な使い方については下記の参考記事を参照してください。

やること

今回やることは次のとおりです。

  1. スケジュール実行されるタスク (アプリケーション) の実装
  2. AWS Copilot のインストール
  3. AWS Copilot を使ってデプロイ

やってみる

では順番にやっていきます。

1. スケジュール実行されるタスク (アプリケーション) の実装

まずはスケジュールされたタスクとして実行するアプリケーションの実装です。今回は、Slack のとあるチャンネルに 「Hello ECS!!!」 とポストするだけのアプリを Go で実装します。

package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

const (
	iconEmoji string = ":mega:"
	userName  string = "Hello ECS Job"
	channel   string = "dev"
)

type payload struct {
	Text      string `json:"text"`
	IconEmoji string `json:"icon_emoji"`
	UserName  string `json:"username"`
	Channel   string `json:"channel"`
}

func getWebhookURL() (string, error) {
	webhookURL := os.Getenv("SLACK_WEBHOOK_URL")
	if webhookURL == "" {
		return "", errors.New("The environment value SLACK_WEBHOOK_URL is required.")
	}
	return webhookURL, nil
}

func postToSlack(message, webhookURL string) error {
	p, err := json.Marshal(payload{
		Text:      message,
		IconEmoji: iconEmoji,
		UserName:  userName,
		Channel:   channel,
	})
	if err != nil {
		return err
	}
	resp, err := http.PostForm(webhookURL, url.Values{"payload": {string(p)}})
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	return nil
}

func main() {
	m := flag.String("m", "Hello ECS!!!", "Message posted to Slack")
	flag.Parse()

	webhookUrl, err := getWebhookURL()
	if err != nil {
		fmt.Println("Failed to get Slack Webhook URL.", err.Error())
		return
	}

	if err := postToSlack(*m, webhookUrl); err != nil {
		fmt.Println("Failed to post message to Slack.", err.Error())
	}
}

Slack の Webhook URL は環境変数 SLACK_WEBHOOK_URL から取得します。。 ポストするメッセージはデフォルトで 「Hello ECS!!!」 ですが、実行時に -m オプションで任意のメッセージを指定できるようにしています。

Copilot でデプロイする際には Dockerfile が必要になるので、下記の内容で作成しておきます。

FROM golang:1.15.5-alpine3.12 as build

ADD . .
RUN go build -o /main

FROM golang:1.15.5-alpine3.12

COPY --from=build /main /main
ENTRYPOINT [ "/main" ]

2. AWS Copilot のインストール

続いて AWS Copilot のインストールです。 macOS であれば Homebrew を使ってインストールできます。

$ brew install aws/tap/copilot-cli
$ copilot version
version: v1.1.0, built for darwin

3. AWS Copilot を使ってデプロイ

デプロイするには manifest.yml が必要になるので、まずはそれを作成し、その後デプロイします。

manifest.yml を作成

copilot init コマンドで manifest.yml を作成します。

$ copilot init
Welcome to the Copilot CLI! We're going to walk you through some questions
to help you get set up with an application on ECS. An application is a collection of
containerized services that operate together.

Application name: hello-ecs
Workload type: Scheduled Job
Job name: hello-ecs-job
Dockerfile: ./Dockerfile
Custom Schedule: */5 * * * *
Your job will run at the following times: Every 5 minutes
Would you like to use this schedule? Yes
Ok great, we'll set up a Scheduled Job named hello-ecs-job in application hello-ecs running on the schedule */5 * * * *.

✔ Created the infrastructure to manage services and jobs under application hello-ecs.

✔ Wrote the manifest for job hello-ecs-job at copilot/hello-ecs-job/manifest.yml
Your manifest contains configurations like your container size and job schedule (*/5 * * * *).

✔ Created ECR repositories for job hello-ecs-job.

All right, you're all set for local development.
Deploy: No

No problem, you can deploy your service later:
- Run `copilot env init --name test --profile default --app hello-ecs` to create your staging environment.
- Update your manifest copilot/hello-ecs-job/manifest.yml to change the defaults.
- Run `copilot job deploy --name hello-ecs-job --env test` to deploy your job to a test environment.

この時点で下記のようなディレクトリ構成になっています。

$ .
├── Dockerfile
├── copilot
│   └── hello-ecs-job
│       └── manifest.yml
└── main.go

今回、アプリケーション内では Slack の Webhook URL を環境変数から取得するようにしています。タスク実行時に環境変数を設定するため、 Webhook URL の値を SSM パラメータストアに登録しておきます。

$ aws ssm put-parameter \
--name /test/slack-webhook \
--value "https://hooks.slack.com/services/XXXXXXXXXXX" \
--type String \
--tags Key=copilot-environment,Value=test Key=copilot-application,Value=hello-ecs

ここでタグをしているのは、後のデプロイによって生成されるタスク実行ロールが下記のようなポリシーを持つからです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringEquals": {
                    "ssm:ResourceTag/copilot-environment": "test",
                    "ssm:ResourceTag/copilot-application": "hello-ecs"
                }
            },
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:000000000000:parameter/*"
            ],
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringEquals": {
                    "secretsmanager:ResourceTag/copilot-application": "hello-ecs",
                    "secretsmanager:ResourceTag/copilot-environment": "test"
                }
            },
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:ap-northeast-1:000000000000:key/*"
            ],
            "Effect": "Allow"
        }
    ]
}

SSM パラメータストア、及び SecretManager から値を取得して環境変数に設定する場合、それらのリソースには copilot-applicationcopilot-environment をタグ名として、それぞれ値を設定しておく必要があります。

そして、 copilot/hello-ecs-job/manifest.yml を下記のように修正します。

- #secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
- #  GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of the SSM parameter.
+ secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
+  SLACK_WEBHOOK_URL: "/test/slack-webhook"  # The key is the name of the environment variable, the value is the name of the SSM parameter.

これについては Copilot のドキュメントにも書かれています。

Secrets - AWS Copilot CLI

デプロイ

準備が整ったので、 test 環境にデプロイしてみます。

$ copilot job deploy --name hello-ecs-job --env test
✘ get environment test configuration: couldn't find environment test in the application hello-ecs

環境が無いらしい。ということで作ります。

$ copilot env init --name test --profile default --default-config
✔ Proposing infrastructure changes for the hello-ecs-test environment.
- Creating the infrastructure for the hello-ecs-test environment.                    [rollback complete]  [37.1s]0s]
  The following resource(s) failed to create: [InternetGateway, Cluster,                                  
   VPC]. Rollback requested by user.                                                                      
  - An IAM Role for AWS CloudFormation to manage resources                           [not started]         
  - An ECS cluster to group your services.                                           [delete complete]    [2.1s]
    Resource creation cancelled                                                                           
  - An IAM Role to describe resources in your environment                            [not started]         
  - A security group to allow your containers to talk to each other                  [not started]         
  - An Internet Gateway to connect to the public internet                            [delete complete]    [14.4s]
    Resource creation cancelled                                                                           
  - Private subnet 1 for resources with no internet access                           [not started]         
  - Private subnet 2 for resources with no internet access                           [not started]         
  - Public subnet 1 for resources that can access the internet                       [not started]         
  - Public subnet 2 for resources that can access the internet                       [not started]         
  - A Virtual Private Cloud to control networking of your AWS resources              [delete complete]    [2.1s]
    The maximum number of VPCs has been reached. (Service: AmazonEC2; Stat                                
    us Code: 400; Error Code: VpcLimitExceeded; Request ID: 9571d86e-a7e6-                                
    496b-ae4f-5a4797c1c7a0; Proxy: null)                                                                  
✘ stack hello-ecs-test did not complete successfully and exited with status ROLLBACK_COMPLETE

The maximum number of VPCs has been reached.

VPC の数が上限に達したらしいです…なので、不要な VPC を削除して再度実行します。

$ copilot env init --name test --profile default --default-config
...
...
✔ Created environment test in region ap-northeast-1 under application hello-ecs.

test 環境が作成されたので、あらためてデプロイします。

$ copilot job deploy --name hello-ecs-job --env test
...
...
✔ Deployed hello-ecs-job.

このコマンドでは

Dockerfile を元にイメージをビルド
-> ビルドしたイメージにタグ付け
-> ECR に Push
-> ECS のタスク定義を作成

が実行されます。

暫く待つと、スケジュールされたタイミングでタスクが実行されて Slack に通知が来ます。

Slack massage

まとめ

AWS Copilot を使ってスケジュールされた ECS タスクをデプロイしてみた話でした。 VPC 関連リソースの作成、タスク定義の作成、 ECR リポジトリの作成など、 ECS + Fargate でタスクを実行する際に必要なリソースをほぼ意識せずに簡単に作成できるのは良いなと思いました。一方で、カスタマイズした設定、例えばタスクロールにカスタマイズした Role を使いたいとかは実現が難しそうです。タスクロールのカスタマイズについては、調べている中で下記のツイートを見つけたので、参考にしたいと思います。

「スタンドアロンのタスクを起動できる Role」を例のタグ付きで別に作っておいて、そのロールを「Copilot が ECS タスク用に作ったロールから Assume できるようにしてあげる」と、Copilot タスクから別の ECS タスクを呼び出すのいけました!(ちょっとめんどくさい) pic.twitter.com/2TI1Z2JL6g

— ポジティブな Tori (@toricls) October 23, 2020

今回のコードは GitHub に置いてます。

michimani/hello-ecs: A sample that uses AWS Copilot to deploy a scheduled task to Amazon ECS. The sample app posts a message to Slack.

次回は、今回作成したスケジュールされたタスクを、 AWS CLI を使って任意のタイミングで実行してみます。

※追記

このブログが AWS でコンテナ関連の開発をしている方に 見つかって 見ていただいて、下記のリプを頂きました。

Thank you 🙏 for the awesome blog post! You can also modify the task role using an Addons template https://t.co/Gvnq8YPw23

— Efe Karakus (@efekarakus) January 23, 2021

どうやら copilot/{service-name}/addons ディレクトリに CFn テンプレートを置くことで対応できそうです。

Additional AWS Resources - AWS Copilot CLI

まさか中の人からリプが飛んでくるとは思ってなかったのですが、有益な情報をいただけて嬉しいです。ありがとございます!

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