AWS CDKとTypeScriptでFargateなECSクラスターを構築する

この記事は[ユニークビジョン株式会社 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/uniquevision) 3日目の記事です。

弊社ではインフラにAWSのサービスを利用することが多く、CloudFormationを使って構築しています。
ただ活用範囲が広がるにつれて、YAML形式のCloudFormationでは組み込み関数などの機能を利用しても、コード量も多く冗長な記述になることがふえてきたため、AWS CDKの力を借りてみようと思います。

# AWS CDK

> AWS Cloud Development Kit (AWS CDK) は、使い慣れたプログラミング言語を使用してクラウドアプリケーションリソースを定義するためのオープンソースのソフトウェア開発フレームワークです。

[Getting started with the AWS CDK - AWS Cloud Development Kit (AWS CDK)](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_install) を参考に進めていきます。

## セットアップ
npmでaws-cdkと今回利用するTypeScriptをインストールします。
この記事では解説しませんが、awscliのセットアップも済ませてあると認証周りの設定が楽なのでおすすめしておきます。( [awscliのセットアップ](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-install.html) )

```bash
npm install -g aws-cdk typescript
```

## [プロジェクトを作成](https://docs.aws.amazon.com/cdk/latest/guide/work-with-cdk-typescript.html#typescript-newproject)
cdkコマンドでプロジェクトを作成します。

```bash
mkdir cdk-ecs-fargate
cd cdk-ecs-fargate
cdk init app --language typescript
```

## デプロイ
先ほどのinitコマンドで、空のスタックが定義されたCDKプロジェクトができているのでデプロイしてみます。
開発に必要なコマンドは、`package.json`にあらかじめ定義されていているので、それと`cdk`コマンドを使ってデプロイします。

```bash

# TypeScriptのコンパイル
npm run build

# デプロイ先のAWSアカウントの認証情報を設定
# (AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEYの組でも可)
export AWS_PROFILE=test_account

# CloudFormationスタック作成(デプロイ)
cdk deploy
```

コマンドの実行したのち、CloudFormationを見ると、Stackがひとつできています。空のスタックができると書きましたが、`AWS::CDK::Metadata`なるものができるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314367/6205c690-0ed6-78f5-1f48-7527327d61f9.png)

# ECSクラスターを構築する
とりあえず最低限のStackがデプロイできることまでは確認できたので、ECSクラスターをまず立ち上げてみます。

AWSのサービスごとにnpmパッケージが切られているので、あらかじめ利用するサービスをインストールしておきます。

```bash
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-ecs
```

`lib/cdk-ecs-fargate-stack.ts`というファイルがあるはずなので、これに各種AWSリソースを定義していきます。

```ts:lib/cdk-ecs-fargate-stack.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as ecs from '@aws-cdk/aws-ecs';

export class CdkEcsFargateStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    // Vpcを定義
    const vpc = new ec2.Vpc(this, 'Vpc', {
      maxAzs: 3,
    });
    // ECSクラスターを定義
    const cluster = new ecs.Cluster(this, 'Cluster', {
      vpc,
      clusterName: 'sample-cluster',
    });
  }
}

```

TypeScriptでオブジェクトを定義するように、VPCとECSクラスターを定義しました。AWSを日々利用している方やCloudFormationを書きなれている方からすると、サブネット等の色々なサービスの定義が抜けているようにみえるかもしれませんが、このあたりは特段**指定しない限り**CDKが良い感じに作成してくれます(!!)

```bash
$ npm run build && cdk deploy

> cdk-ecs-fargate@0.1.0 build /hogehoge/cdk-ecs-fargate
> tsc

CdkEcsFargateStack: deploying...
CdkEcsFargateStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (26/26)

 ✅  CdkEcsFargateStack

Stack ARN:
arn:aws:cloudformation:us-east-1:hogefuga:stack/CdkEcsFargateStack/piyopiyo
```

デプロイできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314367/a10f3494-d17e-fe2a-5c88-d522f13031ff.png)


# ECSタスクを定義、ECSクラスターにデプロイする
ECSタスクを定義します。Dockerイメージの指定にはStringでイメージ名を指定するのではなく、専用のクラスを挟むのが少し変則的な気がします。また、一度ECSタスクの定義をしたのち、手にいれたオブジェクトのメソッドを使ってコンテナやポートを割り当てていることに注意してください。

```ts:lib/cdk-ecs-fargate-stack.ts
import { CfnParameter } from '@aws-cdk/core';

// ~~~ 省略 ~~~

    // Dockerイメージを指定. Stringではなく専用のクラスがあるのが特徴.
    const image = ecs.ContainerImage.fromRegistry('nginx:latest');
    // ECSタスクを定義
    const nginxServiceTaskDefinition = new ecs.FargateTaskDefinition(this, 'nginxTask', {
      family: 'nginx',
      cpu: 256,
      memoryLimitMiB: 512,
    });
    // ECSタスクにコンテナを追加. ポートも割り当てる.
    nginxServiceTaskDefinition.addContainer('nginxContainer', {
      image,
      memoryReservationMiB: 256,
    }).addPortMappings({
      protocol: ecs.Protocol.TCP,
      hostPort: 80,
      containerPort: 80,
    });

    // CloudFormationのParameterも利用可能です
    const ecsTaskDesiredCount = new CfnParameter(this, 'ecsTaskDesiredCount', {
      type: 'Number',
      default: 1,
      description: 'The name of ecs task count.',
    });

    // サービスを定義
    const nginxService = new ecs.FargateService(this, 'NginxService', {
      cluster,
      assignPublicIp: true,
      vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
      serviceName: 'nginx',
      desiredCount: ecsTaskDesiredCount.valueAsNumber,
      taskDefinition: nginxTaskDefinition
    });
```

デプロイすると、またタスクロールなどを良い感じに設定してくれるのですが、リソースの作成などが伴う場合にはプロンプトで聞いてくれます。

```bash
npm run build && cdk deploy

> cdk-ecs-fargate@0.1.0 build /piyopiyoo/cdk-ecs-fargate
> tsc

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬───────────────────────────┬────────┬────────────────┬─────────────────────────────────┬───────────┐
│   │ Resource                  │ Effect │ Action         │ Principal                       │ Condition │
├───┼───────────────────────────┼────────┼────────────────┼─────────────────────────────────┼───────────┤
│ + │ ${nginxTask/TaskRole.Arn} │ Allow  │ sts:AssumeRole │ Service:ecs-tasks.amazonaws.com │           │
└───┴───────────────────────────┴────────┴────────────────┴─────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
```

# NPMのライブラリを利用する
ここまでの操作のように、cdkコマンドで作成されるプロジェクトはnpmのプロジェクトのため、npm上の様々なパッケージを利用することが可能です。

## [dotenv](https://www.npmjs.com/package/dotenv)で設定ファイルを切り出す
[dotenv](https://www.npmjs.com/package/dotenv)でコンテナの環境変数をファイルに切り出したりできます。

```bash:config/.env
HOGE=fuga
PORT=8888
```

```ts:lib/cdk-ecs-fargate-stack.ts
    // config/.envに設置したファイルをロードする
    const config = dotenv.config({ path: path.resolve(process.cwd(), './config/.env') });
    nginxTaskDefinition.addContainer('nginxContainer', {
      image,
      memoryReservationMiB: 256,
      // ロードした.envをそのままコンテナの環境変数に割り当てる
      environment: config.parsed,
    }).addPortMappings({
      protocol: ecs.Protocol.TCP,
      hostPort: 80,
      containerPort: 80,
    });
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314367/7374f2ce-5657-6d26-bdd3-1cb3648503e9.png)

# まとめ
ECSクラスター構築から簡素なECSタスク・サービスの定義をおこない、さらにNPMパッケージも活用してみました。
AWS CDKはCloudFormationの基本的な機能を備えながら、さらにプログラミング言語の強力な機能を利用できるので、複雑なリソースの定義もストレス少なく書いていけると思います。

自分で書いていて以下のようなTypeScriptのメリットを感じました

- NPMエコシステムが利用可能
- 型システムによる、エラー検知、**補完機能**

まだ一か月程度しか触れていませんが、特に型システムから来る補完機能には助けられていて、ドキュメントの見る回数も少なくサクサク書いていけて気持ちが良いです。YAML書いていた時にはCloudFormationのドキュメントとずっとにらめっこだったので...

バージョンアップも頻繁で日本語情報の少なさも気になるところですが、現時点でも大分補完機能やcdkコマンドの使いやすさもあり開発体験は良好です。

CloudFormationに慣れていればそれに近い書き味で書いていけるので、CloudFormationを普段から触っているエンジニアの皆さんは一度触ってみることをおすすめします！