Docker と ECS Scheduled Taskを使って定期的におじさんからメッセージが届く仕組みを作る😚💕

皆さんはおじさんは好きカナ？（￣ー￣?
僕は好きだヨ！！✋（￣▽￣ ♥ 💕
定期的におじさんからメッセージが届いたら素敵じゃないカナ？😚😘😄
と言うわけで作ってみタヨ！！(^з<)❗😃✋💕


## 概要

ECS Scheduled Taskを使っておじさんから定期的に Slack メッセージが届く仕組みを作ります。
おじさんのメッセージ生成には Ojichat を使わせていただきました。

[greymd/ojichat](https://github.com/greymd/ojichat)


## 環境構築

### AWS CLI用のIAMユーザーの作成

AWS CLI用のIAMユーザーを作成します。以下の記事を参考にしながらIAMユーザーを作成します。 
今回は Terraform の例もあるため、 Admin権限を持つユーザーを作成しました。
イメージをプッシュする権限のみ必要な場合は `ecr:GetAuthorizationToken` を付与してください。
`SecretAccessKey` は再発行されないため、分からなくなってしまったらユーザーを再作成する必要があります。

[IAM ユーザーの作成 (コンソール)](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)
[イメージのプッシュ](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html)

### AWS CLI のインストール

AWS CLI をインストールし、設定を行います。インストール方法は下記記事を参考にしてください

[macOS に AWS CLI をインストールする](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-macos.html)
[AWS CLI の設定](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)

インストールが完了したら AWS CLI の設定を行い、AWS CLI から S3 のバケット一覧を取得できることを確認します。（動作確認）

```
$ aws --version
$ aws configure
$ aws s3api list-buckets  # S3 のバケット一覧を表示するコマンド
```

### Slack に Ojichat の絵文字 `:ojichat:` を作成する

`:ojichat:` を追加するとテンション上がります。😚💕

[カスタム絵文字を追加する
](https://slack.com/intl/ja-jp/help/articles/206870177)


### Slack のWebHookURL を作成する

おじさんからの Slackメッセージを受け取るのに必要です。
WebHook の設定を行うことで、curlコマンドで簡単にメッセージを Slack に送信することができます。

[SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)

## イメージを作成

おじさんメッセージの生成 & Slack にメッセージを飛ばす Docker イメージを作ります。
同ディレクトリ に `Dockerfile` と `entrypoint.sh` を作成し、以下のコマンドでビルドします。
`entrypoint.sh` 中の {your_channnel_name} にはおじさんからのメッセージを受け取りたいチャンネル名を入力してください。
「おのじゅん」は私のあだ名です。


``` Dockerfile:Dockerfile
FROM golang:1.13.4-stretch
RUN go get -u github.com/greymd/ojichat
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
```

``` bash:entrypoint.sh
#!/bin/bash

COMMENT=$(ojichat $NAME)
echo $COMMENT
curl -X POST --data-urlencode "payload={\"channel\": \"##{your_channnel_name}\", \"username\": \"Ojichat\", \"text\": \"$COMMENT\", \"icon_emoji\": \":ojichat:\"}" $WEB_HOOK_URL
```

```bash
$ docker build -t ojichat:latest --no-cache .
```


ビルドができたら以下のコマンドでおじさんからSlackにメッセージが届くことを確認します。
`NAME` にはおじさんに呼んで欲しい名前を、`WEB_HOOK_URL` には Slack のWebHookのURLを入力します。

```bash
$ docker run --rm -e NAME=おのじゅん -e WEB_HOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzzz ojichat:latest
```

<img width="737" alt="スクリーンショット 2019-11-26 15.10.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/476cbcbc-15ff-138c-611d-6376f5cb298a.png">

無事おじさんからメッセージを受け取ることができました。


## おじさんを ECR にプッシュする

おじさんイメージがきちんと動くことがわかったので、次はおじさんイメージを ECR にプッシュします。
GUI からおじさんを格納するリポジトリを作成します。

<img width="945" alt="スクリーンショット 2019-11-26 15.13.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/985acc30-9721-e8a2-0edb-8e8c7b122c56.png">

`プッシュコマンドを表示` ボタンを押すとイメージのビルドからプッシュまでの手順が表示されます。
その手順に従ってイメージを ECR にプッシュします。

```bash
$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker build -t ojichat .
docker tag onojun-ojichat:latest {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/onojun-ojichat:latest
docker push {YOUR_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/onojun-ojichat:latest
```


## おじさんを動かすロールを作成する

おじさんイメージは Fargate 上で動かします。
必要な権限を持つロールを作成します。

`ojichat-policy` を作成します。

<img width="910" alt="スクリーンショット 2019-11-26 15.39.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/d092600b-0f4e-a2f7-080c-9073c0e1cf3a.png">

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

次に `ojichat-role`を作成し、`ojichat-policy`をアタッチさせ、信頼関係を編集します。


<img width="967" alt="スクリーンショット 2019-11-26 15.42.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/56468da5-35b9-3912-cfde-14a2577869b7.png">


```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "ecs.amazonaws.com",
          "ecs-tasks.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```


## おじさんタスク定義を作成する

おじさんのタスク定義を作成します。

<img width="1427" alt="スクリーンショット 2019-11-26 16.05.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/1b293d2c-7056-2126-8200-0b0bf50eb331.png">

<img width="1430" alt="スクリーンショット 2019-11-26 16.05.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/920d34bf-2a2d-7290-a94d-dcba4102dcd6.png">

<img width="907" alt="スクリーンショット 2019-11-26 16.05.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/98e4a97b-4ca7-2f2d-0386-710992f96ebc.png">

必要な環境変数を設定します。
今回はハードコードしてしまいましたが、`WEB_HOOK_URL` などのクレデンシャル情報は `ParameterStore` に格納し、それを `FromValue` で参照するとタスク定義に書かずに済みます。


## おじさんクラスターを作成し、おじさんタスクを実行する

`ojichat-cluster` を作成し、作成したタスク定義 `ojichat-task` からタスクを実行します。

<img width="547" alt="スクリーンショット 2019-11-26 16.09.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/fbf030f0-0261-a00b-f597-2bfe4547f03b.png">


## おじさんを ScheduledTask で動かす

Fargate にいるおじさんからメッセージが届いたら、次はおじさんのメッセージを自動化します。
スケジュールタスクを設定することで実現することができます。

<img width="1077" alt="スクリーンショット 2019-11-26 16.19.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/9a92c709-c17a-2e76-35dd-d6ad2c80bf49.png">

僕はおじさんが好きなので1分ごとにメッセージが飛ぶようにしました。



<img width="1180" alt="スクリーンショット 2019-11-26 16.40.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63855/3c5fc35b-37ba-c6ab-7f79-a23576afefe1.png">


## Terraform

```tf
########################
## Credential Infos
########################
provider "aws" {
  access_key = local.access_key
  secret_key = local.secret_key
  region     = "ap-northeast-1"
}

########################
## ECR
########################
# ECS Repository
resource "aws_ecr_repository" "repository" {
  name = "ojichat"
}

# Repositry Policy
# Permit pull image
resource "aws_ecr_repository_policy" "repository_policy" {
  repository = aws_ecr_repository.repository.name
  policy     = data.aws_iam_policy_document.repository_policy.json
}

data "aws_iam_policy_document" "repository_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchCheckLayerAvailability",
      "ecr:BatchGetImage"
    ]
    principals {
      type        = "*"
      identifiers = ["*"]
    }
  }
}


########################
## IAM
########################
# IAM Policy
resource "aws_iam_policy" "policy" {
  name        = "ojicaht-policy"
  description = "for ojichat"
  policy      = data.aws_iam_policy_document.policy.json
}

data "aws_iam_policy_document" "policy" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "ecs:RunTask",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "iam:PassRole"
    ]
    resources = ["*"]
  }
}

# IAM Role
resource "aws_iam_role" "role" {
  name               = "ojichat-role"
  description        = "role for ojichat"
  assume_role_policy = data.aws_iam_policy_document.role.json
}

data "aws_iam_policy_document" "role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com", "events.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy_attachment" "schedule_policy_attachment" {
  role       = aws_iam_role.role.name
  policy_arn = aws_iam_policy.policy.arn
}

resource "aws_iam_role_policy_attachment" "event_role_policy_attachment" {
  role       = aws_iam_role.role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole"
}

########################
## ECS
########################
# cluster
resource "aws_ecs_cluster" "cluster" {
  name = "ojichat-cluster"
}

# task difinition
resource "aws_ecs_task_definition" "task_definition" {
  family                   = "ojichat-task"
  task_role_arn            = aws_iam_role.role.arn
  container_definitions    = data.template_file.container_definitions.rendered
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.role.arn
}

data "template_file" "container_definitions" {
  template = <<EOF
[
  {
    "cpu": 0,
    "environment": [
      {
        "name": "WEB_HOOK_URL",
        "value": "${local.web_hook_url}"
      },
      {
        "name": "NAME",
        "value": "おのじゅん"
      }
    ],
    "name": "ojichat",
    "image": "${aws_ecr_repository.repository.repository_url}",
    "logConfiguration": {
      "logDriver": "awslogs",
      "secretOptions": null,
      "options": {
        "awslogs-group": "/ecs/ojichat-task",
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "essential": true
  }
]
EOF
}

# scueduled task
resource "aws_cloudwatch_event_rule" "schedule_rule" {
  name                = "ojichat-event"
  schedule_expression = "cron(* * * * ? *)"
  is_enabled          = true
}

resource "aws_cloudwatch_event_target" "fargate_scheduled_task" {
  rule     = aws_cloudwatch_event_rule.schedule_rule.name
  arn      = aws_ecs_cluster.cluster.arn
  role_arn = aws_iam_role.role.arn

  ecs_target {
    task_definition_arn = aws_ecs_task_definition.task_definition.arn
    task_count          = 1
    launch_type         = "FARGATE"

    network_configuration {
      subnets          = [local.subnets_id]
      assign_public_ip = true
    }
  }
}
```

## 参考
[greymd/ojichat](https://github.com/greymd/ojichat)
[Amazon Elastic Container Service とは](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/Welcome.html)
[Amazon ECS クラスター](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_clusters.html)
[Amazon ECS タスク定義](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definitions.html)
[Amazon ECS サービス](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs_services.html)