aws cdkでecsタスクスケジュールによるバッチ機構を作る

# はじめに
**みなさん、障害対応してますか？**

障害対応に関しての私のモットーは、
アラートがとんで来たら、まずはコーヒーを淹れて、
鼻から抜ける豆の香りを堪能してからパソコンを開く事です。

おかげで、後々作成する謝罪文も芳醇でマイルドなものになります。

ことAWSの話しですが、ここ数ヶ月
コーヒーのフルーティな味わいを感じながら原因調査してると、
かなり序盤の方で
「これホスティング側の問題じゃない？」って頭に浮かぶ事があります。

しかしながらHealthDashboardは音沙汰なく、
「あれ、やっぱりこっちの問題？？？...」となり、もっかい色々みてみる、
なんてケースが多々あります。

そんな時ちらっとtwitterみると、

「AWS障害か〜〜仕方ないから今日休みにしない？」
「aws 落ちてんじゃん、ネトフリみて待ってよ」

といった考えられないツイートもあれば、
どのリージョンでどのリソースがあやしそうか探ってる人なんかもいます。

怪しいな、と思ったらtwitterがざわざわガヤガヤしてないか見てみるのも手だなと。

そこで、定期的に特定ワードが頻繁につぶやかれてる時、
その事を通知してくれる物を用意しておけば、
原因特定はそこそこに、~~次の一杯を淹れに~~影響調査の方に時間をさけるというものです。

という事で、そんな通知ボットを作りましょう。というのが前段。

長くなりましたが、目的は後付で、
**cdkでecsタスクを定期実行する何か**を作ってみようとしたという所が正直な所です。
docker、ecs周りの基本的内容の復習がほとんどです。

# この記事でやってる事

- twitterのAPIを使って記事を検索するスクリプトをTypeScriptで用意
- 上記を定期実行するリソース一式をcdkで作る

# 目次
- twitter検索機能を作る
- docker化してみる
- fargateで動かしてみる
- まとめ

# twitter検索機能を作る

## まずはDeveloperアカウントの用意
詳しくは省きますが、[公式さん](https://developer.twitter.com/en/support/twitter-api/developer-account#faq-developer-account)  の言う通りに進めます。
色々聞かれますが、作成したら `Bearer Token` がもらえます。これ使うのでひかえておきます。

![Screenshot 2022-06-01 1.04.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/e5904d34-4b93-64e7-5f59-7d15e26aa3ac.png)


## TypeScriptのセットアップ
[公式さん](https://developer.twitter.com/en/docs/twitter-api/tools-and-libraries/v2) を見ると、
APIのv2の [TypeScript/JavaScriptSDK](https://github.com/twitterdev/twitter-api-typescript-sdk) が存在しているようなので、
そちらを使う為にTypeScriptで書く事にします。

まずはプロジェクトのセットアップ

ちなみに自分の環境は以下
- PC: mac(Monterey) Apple Silicon
- node: 18.1.0
- npm: 8.8.0
- 手元のコーヒー: ブルーマウンテンブレンド 330円

```bash
# プロジェクト作成
mkdir app && cd app

# package.json作る 
npm init

# typescript入れる
npm install --save-dev typescript

# tsconfig.json作る
npx tsc --init
```

いちいちトランスパイルするのも面倒なのでts-nodeを入れて、TypeScriptを直実行できるようにしておきます

```bash
npm install --save-dev ts-node
```

試しにtsファイルを作って、直実行してみます

```app/index.ts
const runningTest: string = "「He~~llo」(･ω| ←SigourneyWeaver"
console.log(runningTest)
```

```bash
npx ts-node hello.ts

> 「He~~llo」(･ω| ←SigourneyWeaver
```

問題なさそうですね。

`npx ts-node` は、 `./node_modules/.bin/ts-node` と同義です。
いちいちmodulesを指定しなくてもいいという。npx、便利ですね。
インストールしてないモジュールも指定できるのだそう（使ったあと消える）

## twitterのSDK入れる

次にsdkを公式さんの言う通りに入れます
```
npm install twitter-api-sdk
```

そしてこんな感じでindex.tsを改変します。
後々そうするので、tokenなんかは環境変数で渡してます
```app/index.ts
import {Client} from "twitter-api-sdk";

const bearerToken = process.env.BEARER_TOKEN as string;
const client = new Client(bearerToken);
const now = new Date();
now.setHours(now.getHours() - 1); // 試しに直近1時間までのツイートを対象に

async function search() {
    const response = await client.tweets.tweetsRecentSearch({
        'query': 'なかやまきんに君', // ここに検索したいワードを入れる
        'start_time': now.toISOString(),
        'tweet.fields': [
            'created_at'
        ]
    });

    return response.data;
}

// 実行
search().then(tweets => {
    if (tweets === undefined) {
        console.log('取得結果なし')
        process.exit(1);
    }

    for (const tweet of tweets) {
        console.log(tweet);
    }
});
```

走らせてみます。

```bash
BEARER_TOKEN=twitterに発行してもらったトークン \
npx ts-node index.ts
```

![kinikun_run.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/999b065c-66d6-093f-2e2f-249a43035f5a.png)

うん、動作良好ですね。

slack通知したいので、モジュール追加します
```
npm install @slack/web-api
```

ファイルがでかくなりそうなので、分けます。

```app/lib/TweetChecker.ts
import {Client} from "twitter-api-sdk";
import {WebClient} from "@slack/web-api";

export class TweetChecker {
    private TWITTER_URL_BASE: string = 'https://twitter.com/dummy/status/TWEET_ID';
    private readonly client: Client;
    private readonly query: string;
    private readonly slackToken: string;
    private readonly slackChannel: string;

    constructor(
        bearerToken: string,
        query: string,
        slackToken: string,
        slackChannel: string
    ) {
        this.client = new Client(bearerToken);
        this.query = query;
        this.slackToken = slackToken;
        this.slackChannel = slackChannel;
    }

    checkTweets() {
        this.recentSearch().then(tweets => {

            if (tweets === undefined) {
                console.log('世界は平和でした。');
                return;
            }

            let tweetUrls: Array<string> = [];
            for (const tweet of tweets) {
                let tweetUrl = this.TWITTER_URL_BASE.replace('TWEET_ID', tweet.id);
                tweetUrls.push(tweetUrl);
            }

            this.post(tweetUrls).then(() => {
                console.log('succeed');
            });
        });
    }

    private async recentSearch() {
        const now = new Date();
        now.setHours(now.getHours() - 12);

        const response = await this.client.tweets.tweetsRecentSearch({
            'query': this.query,
            'max_results': 10,
            'start_time': now.toISOString(),
            'tweet.fields': [
                'created_at',
                'entities'
            ]
        });

        return response.data;
    }

    private async post(tweetUrls: Array<string>) {
        let message = '';
        message += '.....おや!?' + "\n"
        message += "AWSのようすが.....!" + "\n\n"
        message += "   ↓ ↓ ↓ ↓" + "\n\n"
        message += tweetUrls.join("\n");

        const client = new WebClient(this.slackToken);
        return await client.chat.postMessage({
            channel: this.slackChannel,
            text: message
        });
    }
}
```

index.tsは、環境変数受け取ってclassを呼ぶだけにしておきます

```app/index.ts
import {TweetChecker} from "./lib/TweetChecker";

const bearerToken = process.env.BEARER_TOKEN as string;
const searchQuery = process.env.SEARCH_QUERY as string;
const slackToken = process.env.SLACK_TOKEN as string;
const slackChannel = process.env.SLACK_CHANNEL as string;

const twitterClient = new TweetChecker(
    bearerToken,
    searchQuery,
    slackToken,
    slackChannel
);
twitterClient.checkTweets();
```

もっかい実行してみます。

↓↓↓

```bash
BEARER_TOKEN=twitterに発行してもらったトークン \
SEARCH_QUERY='AWS障害 OR AWS落ちた' \
SLACK_TOKEN=スラックトークン \
SLACK_CHANNEL=#すきなチャネル \
npx ts-node index.ts
```

![slack_通知君.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/ec7c3014-22a8-890f-78a7-61df86d85e72.png)

OKOK。
まあここで色々対象ツイート絞ったり色々すべきですが、
はじめに言った通り、やりたかったのここからなので、
これはもうこれで終わりにします。

# docker化してみる

次に、作ったものをdockerImageにします。

appディレクトリ直下にDockerfileを作成します。
今回はオフィシャルのnodeイメージを使います。

```dockerfile:app/Dockerfile
FROM node:latest

# ワークディレクトリを設定
WORKDIR /usr/run/app

# appディレクトリを一式コピー（不要な物はdockerignoreしてちょっとでも軽く）
COPY . .

# npm installする
RUN npm install

# npx ts-node index.ts走らせる
CMD [ "npx", "ts-node" , "index.ts" ]
```

`COPY . .`はやりすぎですが、一応`dockerignore`でイメージの中に入れたくないものは記載しておきます。

```text:app/.dockerignore
/node_modules
/Dockerfile
/.dockerignore
```

さあ、ビルドしてみます。

```bash
docker build -t search-tweet .
```

狙ったものが入ってるか、中入ってみてみましょう。

```bash
docker run -it --rm \
--name test-search-tweet search-tweet \
/bin/bash
root@90170d1c165f:/usr/run/app# ls
index.ts  lib  node_modules  package-lock.json	package.json  tsconfig.json
```

うんうん、期待値。

ではrunしてみます。
先程直接指定していた環境変数は、コンテナ側に持っていく必要があるので、
こんな感じでファイルに書き出して、
run する時のオプションで指定してあげます。

```app/app.env
BEARER_TOKEN=twitterに発行してもらったトークン
SEARCH_QUERY='AWS障害 OR AWS落ちた'
SLACK_TOKEN=スラックトークン
SLACK_CHANNEL=#すきなチャネル
```

では実行

```bash
docker run -it --rm \
--env-file=app.env \
--name test-search-tweet search-tweet
```

![Screenshot 2022-06-01 2.51.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/23178862-bb34-9df3-89b5-fd49864cca50.png)

コンテナからも実行できました。

さあこれで準備は整いました。
いよいよfargateで動いてもらいましょう。

# fargateで動かしてみる

さてawsリソース一式作るために、cdkの登場ですが、その前に

## cdk is 何

まずはそこです。

いろいろ詳しい私の友人に聞いてみます。

 <br>

<img width="250" alt="toolkit.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/33b766d7-f71f-e333-dbff-f0fbbeaf732a.png">

<br>

知らないみたいです。


公式さんによると、
AWS Cloud Development Kitの略だそう。
ざっくりいうと、プログラミング言語でCloudFormationテンプレートが掛けるとの事。
同じようなリソースならぶ時はループとかで書けるって事です。やほーい。

何よりデフォで色々おすすめ初期設定が入ってるから、
簡単なリソースならめっちゃシンプルにかけます。
ここが便利であり怖いとこかなと。

## cdkセットアップ

ではさっそくセットアップ。

今こんな感じのディレクトリになってるので、
```
app/ ←ここ
├── index.ts
├── lib
│   └── TweetChecker.ts
├── node_modules
├── package-lock.json
├── package.json
└── tsconfig.json
```
一個もどってcdkディレクトリ作ります。そして初期化。
いくつか言語使えますが、今回は同じくTypeScriptで。

```bash
cd ../ && mkdir cdk && cd cdk

# 初期化
cdk init --language typescript
```

```
cdk/
├── README.md
├── bin
│   └── cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── cdk-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── cdk.test.ts
└── tsconfig.json
```

いろいろ入りました。 v2が入ったようです。
`lib/cdk.ts` にスタックを記載し、 `bin/cdk.ts` でそれらをnewする感じになります。
あと環境差分値なんかは`cdk.json`に書くことになるかなと

## ひとまずcdk bootstrap

cdkを使うには、`cdk bootstrap` しておく必要があります。

リソースを作るリージョン毎に必要で、
実行するとデフォルトでは `CDKToolkit` という名前のスタックが作成されます。
cdkの成果物はCloudFormationテンプレートですが、
CFnもテンプレートサイズがでかすぎる場合はs3に一度テンプレートを配置する必要があり、
その時などに使うs3バケットなんかがこれにより作成されます。

![Screenshot 2022-06-01 3.29.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/94ac2b0b-c133-7413-2ba7-462b890af4ad.png)

↓↓ 作られる ↓↓

<img width="400" alt="toolkit.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/5852b0ac-5f8f-7dbf-9ed5-293af65a8e3f.png">

↓↓ 中身 ↓↓

<img width="400" alt="toolkit_detail.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/784a05f4-8416-6e17-7adf-29b318aeeccb.png">

これで準備はokなので進みます

## スタックの中身を書いていく

```cdk/lib/cdk-stack.ts
import {Stack, StackProps} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {SubnetType} from "aws-cdk-lib/aws-ec2";
import {Cluster, ContainerImage, FargatePlatformVersion, LogDriver} from "aws-cdk-lib/aws-ecs";
import {ScheduledFargateTask} from "aws-cdk-lib/aws-ecs-patterns";
import {Repository} from "aws-cdk-lib/aws-ecr";
import {Schedule} from "aws-cdk-lib/aws-events";

export interface SearchTweetStackProps extends StackProps {
    bearerToken: string;
    searchQuery: string;
    slackToken: string;
    slackChannel: string;
}

export class SearchTweetStack extends Stack {

    constructor(scope: Construct, id: string, props: SearchTweetStackProps) {
        super(scope, id, props);

        // dockerイメージの配置場所を作る
        const ecrRepository = new Repository(this, 'searchTweetRepository', {
            repositoryName: "search-tweet-repo",
            imageScanOnPush: true
        });

        // ECSのクラスタを作る
        const cluster = new Cluster(this, 'SearchTweetECSCluster', {});

        // タスクスケジュールを組む
        const scheduledTask = new ScheduledFargateTask(this, 'SearchTweetScheduledTask', {
            cluster: cluster,
            platformVersion: FargatePlatformVersion.LATEST,
            schedule: Schedule.expression('cron(0/10 * * * ? *)'),
            scheduledFargateTaskImageOptions: {
                image: ContainerImage.fromEcrRepository(ecrRepository, 'latest'),
                memoryLimitMiB: 512,
                cpu: 256,
                logDriver: LogDriver.awsLogs({streamPrefix: 'ScheduledTaskLogs'}),
                environment: {
                    BEARER_TOKEN: props.bearerToken,
                    SEARCH_QUERY: props.searchQuery,
                    SLACK_TOKEN: props.slackToken,
                    SLACK_CHANNEL: props.slackChannel,
                },
            }
        });
    }
}

```

驚くべきコード量のすくなさ,,,
まあ指定すべきvpcとかほとんどデフォルトっていうのもあるのですが、これだけです。
実際に作られるリソースはもっと多いです、ここで記載しているのは、
ECSクラスターとタスクスケジュールくらいなので余計スッキリしてますね。

そして今までさんざん無理やり渡していた環境変数ですが、
ここで `scheduledFargateTaskImageOptions` のオプションとしてタスクに引き渡します。
まあこれで渡したかっただけです。

次にメインとなるcdk.tsを変更します

```cdk/bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import {SearchTweetStack, SearchTweetStackProps} from '../lib/cdk-stack';

// props作成
const props: cdk.StackProps = {
    env: {
        account: process.env.CDK_DEFAULT_ACCOUNT,
        region: "ap-northeast-1",
    }
};

// cdk呼ぶ
const app = new cdk.App();

// cdkコマンドで指定したenvの値を取得
const env: string = app.node.tryGetContext("env");

// cdk_jsonに記載したcontext情報を取得
const envProps = app.node.tryGetContext(env);

// propsと自前propsを展開して一つにする
const customProps: SearchTweetStackProps = {
    ...props,
    ...envProps
};

// スタックを作る
new SearchTweetStack(
    app,
    `SearchTweetStack`,
    customProps
);

```

ここ、環境変数をlibのtsに渡したいわけですが、
cdkのContextの仕組みを使って注入します。

```cdk/cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
  "~~~~~~~",
  "割愛",
  "~~~~~~~", 
  "context": {
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "~~~~~~",
    "割愛",
    "~~~~~~",
    "@aws-cdk/core:target-partitions": [
      "aws",
      "aws-cn"
    ],
    "test": { <-- ここ
      "bearerToken": "twitterから発行されたトークン",
      "searchQuery": "AWS障害 OR AWS落ちた",
      "slackToken": "スラックのトークン",
      "slackChannel": "好きなチャネル"
    }
  }
}
```

こんな感じで記載しておくことで、
cdkコマンド実行時にcdk.jsonに書いといた値がとれるわけです。
今回機密情報記載しちゃってるのでNGですが、
環境差分値なんかはこれで渡してあげると、prod用stg用なんかを分けてあげられます。

```cdk/bin/cdk.ts
// cdk_jsonに記載したcontext情報を取得
const envProps = app.node.tryGetContext(env);
```

試しに↑↑の`envProps`をconsole.logしておいて、
`cdk synth`でテンプレートを出力してみます。

```bash
cdk synth -c env=test
```

![Screenshot 2022-06-01 3.45.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/029f4c18-8f61-ca76-aa4b-40659e68fda6.png)

うまくとれてますね。


## 一体何が生成されるのか

スタック作成前に、いかなるテンプレートを生み出そうとしてるのか確認しておきましょう。

先程の `cdk synth` コマンドを使います。
ちょっとテンプレートを書き出してみましょう。

```bash
cdk synth -c env=test > template.yaml
```

割愛しますが、約500行くらいのテンプレートができあがってました。。

![Screenshot 2022-06-01 4.08.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/16ea6c6e-e17c-ca39-9946-e6e7fa92d555.png)

中身みてみると、クラスタのvpcを指定してないせいで、新規のvpcを作成してくれてる為、
SubnetやそれぞれのRouteTableなんかも全部用意する方向のようです。

この、**よしなにやってくれる**所、
根本を把握してないと痛い目みる事がありそうですね。

実際にテンプレートの差分とか見てないと、 ???となる事多そうです。

## よくテンプレートをみてみる

出来上がったテンプレートをみてみると、
NATGateWayが2本できてました。

ん？と思って更にみると、

クラスタをPrivateSubnetに配置しようとしてくれているみたいです。

まあ基本確かにそうですよね。

更にはありがたい事に、
Public、PrivateのSubnetをマルチAZになるよう、2つのAZに作成してくれてました。
そしてPrivateSubnetそれぞにNATを作るという流れの模様。

はい、たしかに推奨設定で作ってくれました。

しかし今はテストしたいだけなのでTooマッチです。
NATGateWayは存在するだけでお金かかりますし。

今回はただのテスト開発ですぐ消つもりでいて、かつ、
一応twitterとslack叩くので外には出れてほしいという事で、
- クラスタはパブリックに配置
- マルチAZにしない
- もちろんNATGateWayつくらない

といった具体に変更しに行きます。

## スタックの修正

最終版はこちら
```cdk/lib/cdk-stack.ts
import {Stack, StackProps, Fn, Aws, aws_ec2} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {SubnetType,Vpc} from "aws-cdk-lib/aws-ec2";
import {Cluster, ContainerImage, FargatePlatformVersion, LogDriver} from "aws-cdk-lib/aws-ecs";
import {ScheduledFargateTask} from "aws-cdk-lib/aws-ecs-patterns";
import {Repository} from "aws-cdk-lib/aws-ecr";
import {Schedule} from "aws-cdk-lib/aws-events";

export interface SearchTweetStackProps extends StackProps {
    bearerToken: string;
    searchQuery: string;
    slackToken: string;
    slackChannel: string;
}

export class SearchTweetStack extends Stack {

    constructor(scope: Construct, id: string, props: SearchTweetStackProps) {
        super(scope, id, props);

        // dockerイメージの配置場所を作る
        const ecrRepository = new Repository(this, 'searchTweetRepository', {
            repositoryName: "search-tweet-repo",
            imageScanOnPush: true
        });

        // クラスタを配置するVPCを作る
        const vpc = new Vpc(this, 'searchTweetVpc', {
            natGateways: 0, // natGatewayなし
            maxAzs: 1, // シングルAZ
            subnetConfiguration: [ // publicのサブネットを一つ
                {
                    cidrMask: 28,
                    name: 'public',
                    subnetType: SubnetType.PUBLIC,
                },
            ],
        })

        // ECSのクラスタを作る
        const cluster = new Cluster(this, 'SearchTweetECSCluster', {
            vpc // vpcを指定
        });

        // タスクスケジュールを組む
        const scheduledTask = new ScheduledFargateTask(this, 'SearchTweetScheduledTask', {
            cluster: cluster,
            platformVersion: FargatePlatformVersion.LATEST,
            schedule: Schedule.expression('cron(0/10 * * * ? *)'),
            subnetSelection: {subnetType: SubnetType.PUBLIC}, // publicにサブネットに配置するように修正
            scheduledFargateTaskImageOptions: {
                image: ContainerImage.fromEcrRepository(ecrRepository, 'latest'),
                memoryLimitMiB: 512,
                cpu: 256,
                logDriver: LogDriver.awsLogs({streamPrefix: 'ScheduledTaskLogs'}),
                environment: {
                    BEARER_TOKEN: props.bearerToken,
                    SEARCH_QUERY: props.searchQuery,
                    SLACK_TOKEN: props.slackToken,
                    SLACK_CHANNEL: props.slackChannel,
                },
            }
        });
    }
}
```

差分的は以下で、初期値だった物を明示的に指定しているのみです

```diff:差分
@@ -24,15 +24,30 @@ export class SearchTweetStack extends Stack {
             imageScanOnPush: true
         });

+        // クラスタを配置するVPCを作る
+        const vpc = new Vpc(this, 'searchTweetVpc', {
+            natGateways: 0, // natGatewayなし
+            maxAzs: 1, // シングルAZ
+            subnetConfiguration: [ // publicのサブネットを一つ
+                {
+                    cidrMask: 28,
+                    name: 'public',
+                    subnetType: SubnetType.PUBLIC,
+                },
+            ],
+        })
+
         // ECSのクラスタを作る
-        const cluster = new Cluster(this, 'SearchTweetECSCluster', {});
+        const cluster = new Cluster(this, 'SearchTweetECSCluster', {
+            vpc // vpcを指定
+        });

         // タスクスケジュールを組む
         const scheduledTask = new ScheduledFargateTask(this, 'SearchTweetScheduledTask', {
             cluster: cluster,
             platformVersion: FargatePlatformVersion.LATEST,
             schedule: Schedule.expression('cron(0/10 * * * ? *)'),
+            subnetSelection: {subnetType: SubnetType.PUBLIC}, // publicにサブネットに配置するように修正
             scheduledFargateTaskImageOptions: {
                 image: ContainerImage.fromEcrRepository(ecrRepository, 'latest'),
                 memoryLimitMiB: 512,
```

再度テンプレートを書き出してみましょう。

```bash
cdk synth -c env=test > template2.yaml
```

500行くらいが 300行くらいになりました。
NatGatewayもいません
これで安心。

## いざ、デプロイ!!

さてさて、いよいよデプロイします。
```bash
cdk deploy -c env=test
```

ホントに作るよ?? と、もう一度聞いてくれますので、
「**よろしくおねがいしまーす!!**」 
と、大きい声で叫びながら `y` を押します。

IAMやSGなんかの差分を改めて出してくれます。

![Screenshot 2022-06-01 3.33.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/b3aaf913-8736-b4dc-a45d-91879b1a3272.png)

数分くらいで出来上がりました。

これで準備は完了と行きたい所ですが、

`lib/cdk-stack.ts`の`ScheduledFargateTask`には、`search-tweet-repo`の名前で作ったECRの`latest`タグがついたimageを使ってね。と伝えてあります。

そうです、イメージ配置してませんね。

のでimage作ってECRにpushします。

## ECRにタスクが使うimageをアップする

ホントにできてるかまずはECRをみてみましょう。

![Screenshot 2022-06-01 4.43.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/8634307b-70ac-afa9-17d8-6af92dd44d13.png)

居ました。

タスク定義の方もみてみましょう。

![Screenshot 2022-06-01 4.44.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/05dcca71-65af-7df2-2026-caae6d08b40e.png)

はいはい確かにこのレポジトリみてますね。

このままでは、
タスクが走りだした時、ガチギレされる事間違いないです。

のでビルドしてpushしておきます。

```bash
# appまで移動
cd app

# アカウントID取得して
AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)

# Loginして
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

# ビルドして
docker build -t search-tweet-repo:latest .

# タグ付けして
docker tag search-tweet-repo:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/search-tweet-repo:latest

# pushする
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/search-tweet-repo:latest
```

ちなみに、ビルドする際、お使いのPCがmacでかつApple Silicon(M1)だった場合、
タスクの起動で失敗しますので、 `--platform amd64` を指定してください。
生成されたimageのCPUのアーキテクチャが異なってしまうので、
amd64のCPUで動くようにしてあげる必要があります。
まったくM1のくせに笑えない冗談です。

pushできたか確認します。


```bash
aws ecr describe-images --repository-name search-tweet-repo | jq .

>      "repositoryName": "search-tweet-repo",
>      "imageDigest": >"sha256:71cba306c79ffd148d44b331e6a0e11cee3daf7304e098647dbf71f913399410",
>      "imageTags": [
>        "latest"
>      ],
```

ふむふむできていますね。

そうこうしている内に、タスクが実行された模様です。

![Screenshot 2022-06-01 5.06.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/e1259af6-8633-2306-090b-b769d14f0a2d.png)

タスクのログにもconsole.logした値が入ってました。めでたしめでたし。

![Screenshot 2022-06-01 5.02.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/4cf1a5f8-ba45-52e6-3327-a43925b9d8fe.png)


## 気が済んだので消します

```bash
cd cdk
cdk destroy
```

![Screenshot 2022-06-01 5.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/2db13bf7-fa3a-f9a9-b0dd-bacb98f1e67f.png)

消すよ...? と聞かれるので、
空から降ってきた女の子を探し、そっと手を重ねてから
キリッとした顔をして **「バルス」** と言い、
 `y` を押します。

# まとめ

やりたかったのは、何かしらのコマンド機構をECSで定期実行してみるという事でした。

まだサーバのcrontabで動かしてたりするものがあれば、
App部分をそのままimageにしてしまえば、簡単にcronをサーバから切り出せます。
それを実感したかった次第です。

fargateで組んでしまえば、立ち上がりはすぐですので、
長ーーーい処理でない限り、起動時間はごくわずかなはずです。

1時間あたりの起動時間の料金が0.01USDとかなので、
ほとんどお金に跳ねて来ないかなと。

次にcdkですが、めちゃ便利ですね!
デフォで推奨設定を入れてくれる所が光ってます。
ただ便利ですけどまずはCloudFormationを素で書けるくらいになってから使う方がよいですね。

ORMで作ったSQLは吐き出して確認しましょう、というのと同じノリで、
きっとcdk使うときは、 `cdk synth` を叩きまくるのでしょう。

# 最後に

今回、機密情報(トークンとか)を
cdk.json ⇨ タスク定義 ⇨ タスク ⇨ コンテナ ⇨ app の経路で渡しちゃってますのでご注意を。
タスク定義みればconsoleから丸見えです。SecretsManager絡めましょう。

あと振り返ると、tsファイルをトランスパイルしないで最後まで行ってしまいました。。
dockerfile内でjsにしてあげてコンテナは実行するのは `node index.js` にしてあげるべきでした。

そしてずっと謎なのが、
twitterAPIの`search/recent`エンドポイントで取得した検索結果と、
実際にwebの検索から取得した結果が異なるのです、、、
これがずっと解決せず。。。どなたか知ってる方いたら教えてください。

具体的には `AWS 障害` とかで検索すると、
この方のtweetはAPIからしか取得できぬのです。
↓  ↓  ↓  ↓
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">アマゾンのクラウドAWS障害発生！　国内で広範囲に影響 　GitHubなどでも障害発生！ | 速報・事件事故まとめ <a href="https://t.co/sGZsltnMA9">https://t.co/sGZsltnMA9</a></p>&mdash; 橋本バッキャオ (@doragon3756) <a href="https://twitter.com/doragon3756/status/1531848818345803778?ref_src=twsrc%5Etfw">June 1, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

機械的に投稿してる模様なのでそういうのはwebやアプリでは弾いてるのか??



めっちゃ長くなった..
読んでくださった方はありがとうございます。
是非飲みに行きましょう。

# 参考文献

https://zenn.dev/kouchanne/articles/6485193823ecec5735d4

https://qiita.com/keita_ogawa/items/e115c46f1c8caf6fd34d

https://dev.classmethod.jp/articles/ecs-deploy-using-cdk/

https://memo.morelents.com/kaisou-henkan/