What is it, naokirin?

AWS CDK で Fargate を建てるまで

正月休みということで、時間を使ってやるようなものを消化していこうと思っています。

今回は AWS CDK(Cloud Development Kit) で Fargate で nginx を建ててみます。

以下で行ったことは、Githubに上げてあります。

github.com

バージョン

$ aws --version
aws-cli/2.4.7 Python/3.9.9 Darwin/19.6.0 source/x86_64 prompt/off

$ node --version
v16.10.0

$ cdk --version
2.3.0 (build beaa5b2)

なお、CDKで利用する言語はTypeScriptを選択しています。

CDK をインストールする

$ npm i -g aws-cdk

cdk init が空のディレクトリでしか実行できないため、グローバルにインストールします。

CDK プロジェクトを作成する

cdk init でプロジェクトを作成します。プロジェクトを作成するディレクトリで実行します。

$ mkdir cdk_fargate_example && cd cdk_fargate_example
$ cdk init app --language typescript

Applying project template app for typescript
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...
✅ All done!

実行すると以下のようなプロジェクトが作成されます。

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

CDK v1では、各リソースごとにモジュールをインストールする必要がありましたが、v2では不要です。

環境変数を .env ファイルで指定できるようにする、dotenv だけ今回は入れておきます(なくても特に問題ありません)。

$ npm i dotenv

最後に、自動でビルドされるように npm run watch を実行しておきます。

$ npm run watch

> cdk_fargate_example@0.1.0 watch
> tsc -w
[15:03:43] Starting compilation in watch mode...

[15:03:51] Found 0 errors. Watching for file changes.

リージョンを設定

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkFargateExampleStack } from '../lib/cdk_fargate_example-stack';
import * as dotenv from 'dotenv';

dotenv.config();

const app = new cdk.App();
new CdkFargateExampleStack(app, 'CdkFargateExampleStack', {
  // デフォルトのリージョンを ap-northeast-1 にする
  // 環境変数の指定があれば、そちらを優先する
  env: { region: process.env.CDK_DEFAULT_REGION ?? 'ap-northeast-1' } // <= ここを追加
});

CDK Bootstrap を実行する

CloudFormationで利⽤するデプロイ⽤のS3 バケットを作成します。

リージョンで初めて実行する際には必要になります。

$ cdk bootstrap

 ⏳  Bootstrapping environment aws://xxxxxxxxxxxx/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
 ✅  Environment aws://xxxxxxxxxxxx/ap-northeast-1 bootstrapped (no changes).

リソースの設定を書いていく

ここからリソースの設定を書いていくことになりますが、今回はサンプルということで lib/cdk_fargate_example.ts にすべてを書いていきます。

実際の本番環境などではより多くのリソースを管理したりすることになるため、複数のファイルでクラスに分割して、必要な変数を bin/cdk_fargate_example.ts でコンストラクタに渡すような形にもできます。

以降は基本的に lib/cdk_fargate_example.tsCdkFargateExampleStack クラスのコンストラクタに記載していきます。

import { Construct } from 'constructs';
import {
  Stack,
  StackProps,
  RemovalPolicy,
  Duration,
  aws_ec2 as ec2,
  aws_ecs as ecs,
  aws_ecr as ecr,
  aws_elasticloadbalancingv2 as elb,
  aws_logs as logs,
  aws_servicediscovery as servicediscovery,
  aws_iam as iam
} from 'aws-cdk-lib';
import { SubnetType } from 'aws-cdk-lib/aws-ec2';

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

    const prefix = 'cdk-fargate-example'

    // ここに記載していく

  }
}

デプロイの実行

書いていく前にデプロイの実行の仕方を記載しておきます。

cdk synth

まず、 cdk synth です。
cdk synth を実行するとCloud Formationのテンプレートが出力されます。

Cloud Formation に詳しければ、ここでどのような設定でリソースが作成されるかを確認することができます。

$ cdk synth

Resources:
  appContainerRepoCBCDBB42:
    Type: AWS::ECR::Repository
    Properties:
    ... 省略...
Rules:
  CheckBootstrapVersion:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - "1"
                  - "2"
                  - "3"
                  - "4"
                  - "5"
                - Ref: BootstrapVersion

cdk diff

次に cdk diff です。
現在の環境から実際に作成される差分を確認できます。

$ cdk diff

(以下の出力は一部省略しています)

Stack CdkFargateExampleStack
IAM Statement Changes
┌───┬──────────────────────────────────────┬────────┬──────────────────────────────────────┬────────────────────────────────────────┬───────────┐
│   │ Resource                             │ Effect │ Action                               │ Principal                              │ Condition │
├───┼──────────────────────────────────────┼────────┼──────────────────────────────────────┼────────────────────────────────────────┼───────────┤
│ + │ ${logGroup.Arn}                      │ Allow  │ logs:CreateLogStream                 │ AWS:${taskExecutionRole}               │           │
│   │                                      │        │ logs:PutLogEvents                    │                                        │           │
├───┼──────────────────────────────────────┼────────┼──────────────────────────────────────┼────────────────────────────────────────┼───────────┤
└───┴──────────────────────────────────────┴────────┴──────────────────────────────────────┴────────────────────────────────────────┴───────────┘

(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/xxxxxxxxx/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Resources
[+] AWS::ECR::Repository appContainerRepo appContainerRepoXXXXXXXX

cdk deploy

cdk deploy で実際に適用します。

最後に確認がでるので、 y で確定すると、適用が開始されます。

$ cdk deploy

...変更内容の表示 ...

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

VPCを作成する

const vpc = new ec2.Vpc(this, 'Vpc', {
  cidr: '10.0.0.0/16',
  enableDnsHostnames: true,
  enableDnsSupport: true,
  natGateways: 0,
  subnetConfiguration: [
    {
      cidrMask: 28,
      name: 'public',
      subnetType: SubnetType.PUBLIC,
    },
  ]
});

今回はNATゲートウェイを作成しないですむように、パブリックサブネットのみでNATゲートウェイを0にします。

ECRのリポジトリを追加する

const appContainerRepo = new ecr.Repository(this, 'appContainerRepo', {
  repositoryName: `${prefix}-app`,
  imageScanOnPush: true,
  imageTagMutability: ecr.TagMutability.MUTABLE
});

ALBを追加する

const albSecurityGroup = new ec2.SecurityGroup(this, 'albSecurityGroup', {
  securityGroupName: `${prefix}-alb-security-group`,
  vpc: vpc,
});
albSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));

const alb = new elb.ApplicationLoadBalancer(this, 'alb', {
  vpc: vpc,
  internetFacing: true,
  securityGroup: albSecurityGroup,
  vpcSubnets: { subnets: vpc.publicSubnets },
});

const listener = alb.addListener('albListener', { port: 80 });

ALBのセキュリティグループ、ALB、リスナーを作成します。

Fargateを追加する

// クラスタ
const cluster = new ecs.Cluster(this, 'cluster', {
  vpc: vpc,
});

// サービスのセキュリティグループ
const serviceSecurityGroup = new ec2.SecurityGroup(
  this,
  'serviceSecurityGroup',
  {
    securityGroupName: `${prefix}-service-security-group`,
    vpc: vpc,
  }
);
serviceSecurityGroup.addIngressRule(
  albSecurityGroup,
  ec2.Port.allTcp()
);

// サービスディスカバリ
const cloudmapNamespace = new servicediscovery.PrivateDnsNamespace(
  this,
  'namespace',
  {
    name: 'cdk.ecs.local',
    vpc: vpc,
  }
);

// ECSExecのポリシー
const ecsExecPolicyStatement = new iam.PolicyStatement({
  sid: 'allowECSExec',
  resources: ['*'],
  actions: [
    'ssmmessages:CreateControlChannel',
    'ssmmessages:CreateDataChannel',
    'ssmmessages:OpenControlChannel',
    'ssmmessages:OpenDataChannel',
    'logs:CreateLogStream',
    'logs:DescribeLogGroups',
    'logs:DescribeLogStreams',
    'logs:PutLogEvents',
  ],
});

// タスクのロール
const taskRole = new iam.Role(this, 'taskRole', {
  assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskRole.addToPolicy(ecsExecPolicyStatement);

// タスク実行のロール
const taskExecutionRole = new iam.Role(this, 'taskExecutionRole', {
  assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
  managedPolicies: [
    {
      managedPolicyArn:
        'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
    },
  ],
});

// ロググループ
const logGroup = new logs.LogGroup(this, 'logGroup', {
  logGroupName: `${prefix}`,
  removalPolicy: RemovalPolicy.DESTROY,
});

// タスク定義
const taskDefinition = new ecs.FargateTaskDefinition(
  this,
  'taskDefinition',
  {
    memoryLimitMiB: 512,
    cpu: 256,
    executionRole: taskExecutionRole,
    taskRole: taskRole,
  }
);

// イメージ(リポジトリから指定)
const image = ecs.ContainerImage.fromEcrRepository(
  ecr.Repository.fromRepositoryName(this, 'appImage', `${prefix}-app`)
);

// コンテナをタスク定義に追加
taskDefinition.addContainer('container', {
  image: image,
  containerName: 'app',
  logging: ecs.LogDriver.awsLogs({
    streamPrefix: prefix,
    logGroup: logGroup,
  }),
  portMappings: [
    {
      containerPort: 80,
      hostPort: 80,
      protocol: ecs.Protocol.TCP,
    },
  ],
});

// サービス
const fargateService = new ecs.FargateService(
  this,
  'fargateService',
  {
    cluster: cluster,
    desiredCount: 1,
    assignPublicIp: true,
    taskDefinition: taskDefinition,
    enableExecuteCommand: true,
    cloudMapOptions: {
      cloudMapNamespace: cloudmapNamespace,
      containerPort: 80,
      dnsRecordType: servicediscovery.DnsRecordType.A,
      dnsTtl: Duration.seconds(10),
    },
    securityGroups: [serviceSecurityGroup],
  }
);

// サービスでALBのターゲットに登録する
fargateService.registerLoadBalancerTargets(
  {
    containerName: 'app',
    containerPort: 80,
    newTargetGroupId: 'Ecs',
    listener: ecs.ListenerConfig.applicationListener(listener, {
      protocol: elb.ApplicationProtocol.HTTP
    }),
  },
);

ここまで書いた時点で、 cdk deploy してもよいのですが、ECRにイメージを追加していないため、Fargateのサービス作成で失敗するかと思います。そのため、まずは desiredCount0 にしておきましょう。もしくは、一旦ECRのみを作成してイメージをPushしてから、のこりを適用することでも対応できます。

イメージをECRにPushする

ECRへのイメージのPushは今回の範囲外なのと、そこまで難しくないので、以下のドキュメントを参考にしてください。

docs.aws.amazon.com

実際に確認してみる

ここまでで、準備は完了しているので、もしdesiredCount0 にしている場合は、 1 にして、 cdk deploy をしてみます。

デプロイが完了して、ALBにアクセスすると、無事にnginxにアクセスできることが確認できます。

作成したリソースを削除する

最後に今回は検証だったので、作成したリソースを削除します。

cdk destroy をすると、リソースを一括で削除できます。ただし、今回設定したうち、以下については削除されないようなので注意してください。

  • ECRは削除されない
  • CloudFormationのデプロイ用S3は削除されない

また、今回直接作成はしていませんが、S3もデフォルトでは削除されないようです。

(CDK v1時点の記事なので、変わっているかもしれません)

dev.classmethod.jp

まとめ

今回は、AWS CDKでFargateを建ててみました。

自力ですべてを書こうとするとなかなか大変ですが、ドキュメントとWebを参考にしながらであれば、とりあえずの構築は1日程度でできそうです。

Web上の情報などは、CDK v1 の情報も多く、ちゃんと公式のドキュメントを参考にするほうが良さそうです。

参考

CDKを学びはじめる場合、まずは以下のWorkshopをおすすめします(私もこちらからスタートしましたが、だいたい3〜4時間ほどで終わるかと思います)。
cdkworkshop.com