AWS CDK で Infrastructure as Code する: ECS編

こんにちは。AWS CDKでAWSをいろいろ操作する練習中です。
[前回](https://qiita.com/masatomix/items/03dd59929ef1869ea027)はVPC上にEC2インスタンスを立てましたが、今回はECS編です。

ECSとは「[Amazon Elastic Container Service](https://aws.amazon.com/jp/ecs/)」のことで、いわゆるAWS上にDockerコンテナを実行する環境を提供してくれます。
さらに実行するサーバ環境は自分でEC2インスタンスを用意するタイプと、その環境すらもAWSに用意してもらうFargateタイプがあります。Fargateタイプは実行するDocker イメージだけを用意すればよいので、ホント手軽に環境を構築できます。

今回はそのFargateタイプを使います。

## 前提

- [AWS CDK で Infrastructure as Code する: VPC編](https://qiita.com/masatomix/items/fc414e98d6d0091db90d) でVPCが構築できていること

## やってみる

作る環境はこんな感じ。

![FGTiTKi](https://imgur.com/eeY35KX.png)


- ECSサービスでnginx上にサンプルのアプリ(Hello的なヤツ) を立ち上げます。
- Public Subnetに Application Load Balancerを設置(ALB)
- ALBのセキュリティグループは、WEB用のポート8080に外部のネットワークからアクセスOKとする
  - なかで待ち受けるnginxはポート80番で動くのですが、区別するためにあえて外(ALBのリスナのポート)はポート8080にしてみました
- Private Subnet にECS環境を構築
- ECSのセキュリティグループは、ALBのセキュリティグループからと、VPC内のネットからのアクセスのみOKとする
- Dockerのイメージは、シンプルにDocker Hub上のnginxを用います。その定義を書いたECSのタスク定義とECSのサービスを作成
- ECSが必要とするロールを作成



### CDKのソースコード

コードです。作成済みのGitのリンクです。適宜ダウンロードするかGitのコマンドで取得してください。

https://github.com/masatomix/cdk-samples/releases/tag/ecs_first_nginx

さて、コードの主要部分だけ、のせておきます。

#### main部分

メイン部分で様々なスタックを作っていきます。前回の記事までに作成したVPCのうえに

- ECSSecurityGroupStack： ELBとECSが使用するセキュリティグループ
- ECSRoleStack： ECSが使用するロール
- ELBStack： Public Subnet に配置するALB
- ECSServiceELBStack： そのELB上にListenerと、そのリスナの先のTargetGroup
- AppTaskdefinitionStack： ECSサービスのコンテナを指定するタスク定義
- AppServiceStack： ECSサービス自体

を作っていきます。


```typescript:bin/cdk-samples.ts
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { VPCStack } from '../lib/VPCStack'
import { ELBStack } from '../lib/ELBStack'
import { ClusterStack } from '../lib/ClusterStack'
import { ECSRoleStack } from '../lib/ECSRoleStack'
import { ECSServiceStack } from '../lib/ECSServiceStack'
import { BastionStack } from '../lib/BastionStack'
import { ECSSecurityGroupStack } from '../lib/ECSSecurityGroupStack'
import { ECSServiceELBStack } from '../lib/ECSServiceELBStack'
import { AppTaskdefinitionStack } from '../lib/AppTaskdefinitionStack'
import { ContainerInfo, ServiceInfo } from '../lib/Utils'

const main = () => {
  const app = new cdk.App()

  const vpcStack = new VPCStack(app, 'VPCStack')
  // new BastionStack(app, 'BastionStack', vpcStack.vpc, vpcStack.publicSubnets[0])

  const sgStack = new ECSSecurityGroupStack(app, 'ECSSecurityGroupStack', { vpc: vpcStack.vpc })
  const clusterStack = new ClusterStack(app, 'ClusterStack')
  const ecsRoleStack = new ECSRoleStack(app, 'ECSRoleStack')

  const elbStack = new ELBStack(app, 'ELBStack', {
    subnets: vpcStack.publicSubnets,
    elbSecuriyGroup: sgStack.ELBSecurityGroup,
  })

  const serviceInfo: ServiceInfo = {
    serviceName: 'app-service',
    listenerPort: 8080,
    testListenerPort: 9080,
  }

  const containerInfo: ContainerInfo = {
    name: 'app',
    port: 80,
    healthCheckPath: '/',
  }

  const serviceStackELB = new ECSServiceELBStack(app, 'ECSServiceELBStack', {
    loadbalancer: elbStack.loadbalancer,
    vpc: vpcStack.vpc,
    containerInfo,
    serviceInfo,
  })

  const appTaskdefinition = new AppTaskdefinitionStack(app, 'AppTaskdefinitionStack', {
    ecsTaskRole: ecsRoleStack.ecsTaskRole,
    ecsTaskExecutionRole: ecsRoleStack.ecsTaskExecutionRole,
    containerInfo,
  })

  const serviceStack = new ECSServiceStack(app, 'AppServiceStack', {
    cluster: clusterStack.cluster,
    subnets: vpcStack.privateSubnets, // ECSを配置するネットはPrivate Subnet
    taskDef: appTaskdefinition.taskDef,
    ecsSecurityGroup: sgStack.ECSSecurityGroup,
    targetGroup: serviceStackELB.targetGroup,
    containerInfo,
    serviceInfo,
  })
}

main()
```



####  SecurityGroup

セキュリティグループ(SG)です。**ELB向けのSGは外からの8080-8088への通信を受け付ける**ようにしました。ECSはサービスを増やしていくとリスナを増やしていかないといけないので、ポートを幅であけておくことにします。

**ECS向けのSGは、ELBのSGからとVPCのネットワークからの通信を受け付ける**ようにしました。9080系とかもいつか使うんですが、今回は説明は割愛します。

```typescript:lib/ECSSecurityGroupStack.ts
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSecurityGroup, CfnSecurityGroupIngress, CfnSubnet, CfnVPC } from 'aws-cdk-lib/aws-ec2'
import { Profile, getProfile } from './Utils'

type ECSSecurityGroupStackProps = StackProps & {
  vpc: CfnVPC
}

export class ECSSecurityGroupStack extends Stack {
  public readonly ECSSecurityGroup: CfnSecurityGroup
  public readonly ELBSecurityGroup: CfnSecurityGroup

  constructor(scope: App, id: string, /*vpc: CfnVPC,*/ props: ECSSecurityGroupStackProps) {
    super(scope, id, props)
    const p = getProfile(this)

    // 8080-8088,9080-9088 がOKなセキュリティグループをELB用に作成
    this.ELBSecurityGroup = createELBSecurityGroup(this, `ELBSecurityGroup${p.name}`, props.vpc, p)

    // ELBのSGと、VPCのネットからのアクセスを許可
    this.ECSSecurityGroup = createECSSecurityGroup(
      this,
      `ECSSecurityGroup${p.name}`,
      props.vpc,
      this.ELBSecurityGroup,
      p,
    )
  }
}

const createELBSecurityGroup = (stack: Stack, id: string, vpc: CfnVPC, p: Profile): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, id, {
    groupName: `elb-sg${p.name}`,
    groupDescription: 'elb-sg',
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `ELB-SecurityGroup${p.name}` }],
  })

  new CfnSecurityGroupIngress(stack, 'ELBSecurityGroupIngress000', {
    ipProtocol: '-1',
    groupId: group.ref,
    sourceSecurityGroupId: group.ref,
  })

  new CfnSecurityGroupIngress(stack, 'ELBSecurityGroupIngress001', {
    ipProtocol: 'tcp',
    fromPort: 8080,
    toPort: 8088,
    groupId: group.ref,
    cidrIp: '0.0.0.0/0',
  })

  new CfnSecurityGroupIngress(stack, 'ELBSecurityGroupIngress002', {
    ipProtocol: 'tcp',
    fromPort: 9080,
    toPort: 9088,
    groupId: group.ref,
    cidrIp: '0.0.0.0/0',
  })

  return group
}

const createECSSecurityGroup = (
  stack: Stack,
  id: string,
  vpc: CfnVPC,
  elbsg: CfnSecurityGroup,
  p: Profile,
): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, id, {
    groupName: `ecs-sg${p.name}`,
    groupDescription: 'ecs-sg',
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `ECS-SecurityGroup${p.name}` }],
  })

  new CfnSecurityGroupIngress(stack, 'ECSSecurityGroupIngress000', {
    ipProtocol: '-1',
    groupId: group.ref,
    sourceSecurityGroupId: group.ref,
  })
  new CfnSecurityGroupIngress(stack, 'ECSSecurityGroupIngress001', {
    ipProtocol: '-1',
    groupId: group.ref,
    sourceSecurityGroupId: elbsg.ref,
  })
  new CfnSecurityGroupIngress(stack, 'ECSSecurityGroupIngress002', {
    ipProtocol: '-1',
    groupId: group.ref,
    cidrIp: vpc.cidrBlock,
  })

  return group
}
```

#### ELB

ELBです。ELBにはALB(Application Load Balancer)とNLB(Network Load Balancer) があるのですが今回はALBで。なのでTypeはapplication で、インターネット向け(internet-facing) なELBを作成しています。サブネットはPublicなサブネットを指定しています。
インターネットに公開しない内部向けのELBを構築する際は、schemeをinternet-facingではなくinternalに、サブネットはPrivateなサブネットを指定します。

セキュリティグループは先に作成したELB用のSGを指定しています。

```typescript:lib/ELBStack.ts
import { App, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSecurityGroup, CfnSecurityGroupIngress, CfnSubnet, CfnVPC } from 'aws-cdk-lib/aws-ec2'
import { Profile, getProfile, toRefs } from './Utils'
import { CfnLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'
// import * as sqs from 'aws-cdk-lib/aws-sqs';

type ELBStackProps = StackProps & {
  subnets: CfnSubnet[]
  elbSecuriyGroup: CfnSecurityGroup
  albFlag?: boolean
  internetFlag?: boolean
}

export class ELBStack extends Stack {
  public readonly loadbalancer: CfnLoadBalancer

  constructor(scope: App, id: string, props: ELBStackProps) {
    props.albFlag = props.albFlag ?? true // 指定しないときはtrue(ALB)
    props.internetFlag = props.internetFlag ?? true // 指定しないときはtrue(internet-facing)

    super(scope, id, props)
    const p = getProfile(this)

    const type = props.albFlag ? 'application' : 'network'
    const scheme = props.internetFlag ? 'internet-facing' : 'internal'

    this.loadbalancer = createELB(this, 'app-ELB', type, scheme, props.subnets, [props.elbSecuriyGroup], p)
  }
}
const createELB = (
  stack: Stack,
  name: string,
  type: string,
  scheme: string,
  subnets: CfnSubnet[],
  elbsgs: CfnSecurityGroup[],
  p: Profile,
): CfnLoadBalancer => {
  return new CfnLoadBalancer(stack, `${name}${p.name}`, {
    type,
    name: `${name}${p.name}`,
    subnets: toRefs(subnets),
    securityGroups: toRefs(elbsgs),
    scheme,
  })
}
```



#### ECSタスク定義

ECSのタスク定義です。まずは全体。

```typescript:lib/AppTaskdefinitionStack.ts
import { App, ScopedAws, Stack, StackProps } from 'aws-cdk-lib'
import { ContainerInfo, getProfile } from './Utils'
import { CfnTaskDefinition } from 'aws-cdk-lib/aws-ecs'
import { CfnRole } from 'aws-cdk-lib/aws-iam'

type AppTaskdefinitionStackProps = StackProps & {
  ecsTaskRole: CfnRole
  ecsTaskExecutionRole: CfnRole
  containerInfo: ContainerInfo
}

export class AppTaskdefinitionStack extends Stack {
  public readonly taskDef: CfnTaskDefinition
  constructor(scope: App, id: string, props: AppTaskdefinitionStackProps) {
    super(scope, id, props)
    const p = getProfile(this)
    const { accountId, region } = new ScopedAws(this)

    this.taskDef = new CfnTaskDefinition(this, 'ECSTaskDefinition', {
      family: `${props.containerInfo.name}-taskdefinition${p.name}`,
      containerDefinitions: [
        {
          essential: true,
          image: 'nginx',
          name: props.containerInfo.name,
          logConfiguration: {
            logDriver: 'awslogs',
            options: {
              'awslogs-create-group': 'true',
              'awslogs-group': `/ecs/app-taskdefinition${p.name}`,
              'awslogs-region': `${region}`,
              'awslogs-stream-prefix': 'ecs',
            },
          },
          memoryReservation: 100,
          portMappings: [
            {
              containerPort: props.containerInfo.port,
              hostPort: props.containerInfo.port,
              protocol: 'tcp',
            },
          ],
        },
      ],
      taskRoleArn: props.ecsTaskRole.attrArn,
      executionRoleArn: props.ecsTaskExecutionRole.attrArn,
      networkMode: 'awsvpc',
      requiresCompatibilities: ['FARGATE'],
      cpu: '256',
      memory: '512',
    })
  }
}
```

大事なとこは containerDefinitionsの配列でDockerのコンテナイメージを指定しています。メイン部からパラメタでもらっている値を埋めてみると、コンテナ定義の部分はこんな感じ。

```typescript: AppTaskdefinitionStack.tsの一部
containerDefinitions: [
{
  image: 'nginx',
  name: 'app',
  logConfiguration: {
    logDriver: 'awslogs',
    options: {
      'awslogs-create-group': 'true',
      'awslogs-group': `/ecs/app-taskdefinition`,
      'awslogs-region': `${region}`,
      'awslogs-stream-prefix': 'ecs',
    },
  },
  portMappings: [
    {
      containerPort: 80,
      hostPort: 80,
      protocol: 'tcp',
    },
  ],
},...
```

イメージはnginxのイメージ(実際はコンテナイメージのURLを入れたりします)、コンテナはポートは80番を使用、ログはCloudWatchへ転送してね[^1]、みたいな指定をしています。

[^1]: ココをFirelensとかに設定することで、エラーログはCloudWatchだけど残りのログはS3ね、みたいなログ制御を行うことができます。



#### ECSサービスが使うリスナやターゲットグループ

ELB関連のリスナやターゲットグループです。まずは全体。

```typescript:lib/ECSServiceELBStack.ts
import { App, ScopedAws, Stack, StackProps } from 'aws-cdk-lib'
import { CfnVPC } from 'aws-cdk-lib/aws-ec2'
import { ContainerInfo, ServiceInfo, getProfile } from './Utils'
import { CfnListener, CfnLoadBalancer, CfnTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2'

const createTargetGroup = (
  stack: Stack,
  id: string,
  groupName: string,
  containerInfo: ContainerInfo,
  loadbalancer: CfnLoadBalancer,
  vpcId: string,
): CfnTargetGroup => {
  const targetProtocol = loadbalancer.type === 'network' ? 'TCP' : 'HTTP'
  const param = {
    healthCheckPath: containerInfo.healthCheckPath,
    name: groupName,
    port: containerInfo.port,
    protocol: targetProtocol,
    targetType: 'ip',
    healthCheckProtocol: 'HTTP',
    healthCheckTimeoutSeconds: 20,
    unhealthyThresholdCount: 5,
    vpcId: vpcId,
  }

  if (loadbalancer.type === 'network') {
    return new CfnTargetGroup(stack, id, param)
  } else {
    return new CfnTargetGroup(stack, id, {
      ...param,
      // ALBがStickySessionしたい場合。
      targetGroupAttributes: [
        {
          key: 'stickiness.enabled',
          value: 'true',
        },
        {
          key: 'stickiness.type',
          value: 'lb_cookie',
        },
        {
          key: 'stickiness.lb_cookie.duration_seconds',
          value: '86400',
        },
      ],
    })
  }
}

const createListener = (
  stack: Stack,
  id: string,
  targetGroup: CfnTargetGroup,
  listenerPort: number,
  loadbalancer: CfnLoadBalancer,
): CfnListener => {
  return new CfnListener(stack, id, {
    defaultActions: [
      {
        type: 'forward',
        targetGroupArn: targetGroup.ref,
      },
    ],
    loadBalancerArn: loadbalancer.ref,
    port: listenerPort,
    protocol: loadbalancer.type === 'network' ? 'TCP' : 'HTTP',
  })
}

type ECSServiceELBStackProps = StackProps & {
  loadbalancer: CfnLoadBalancer
  vpc: CfnVPC
  containerInfo: ContainerInfo
  serviceInfo: ServiceInfo
}

export class ECSServiceELBStack extends Stack {
  public readonly targetGroup: CfnTargetGroup
  public readonly targetGroupSub: CfnTargetGroup
  public readonly listener: CfnListener
  public readonly testListener: CfnListener
  constructor(scope: App, id: string, props: ECSServiceELBStackProps) {
    super(scope, id, props)
    const p = getProfile(this)
    const { accountId, region } = new ScopedAws(this)

    const serviceName = props.serviceInfo.serviceName

    const targetGroup = createTargetGroup(
      this,
      'TargetGroup',
      `${serviceName}-group`,
      props.containerInfo,
      props.loadbalancer,
      props.vpc.ref,
    )
    const targetGroupSub = createTargetGroup(
      this,
      'TargetGroupSub',
      `${serviceName}-groupsub`,
      props.containerInfo,
      props.loadbalancer,
      props.vpc.ref,
    )
    this.targetGroup = targetGroup
    this.targetGroupSub = targetGroupSub

    const listener = createListener(this, 'Listener', targetGroup, props.serviceInfo.listenerPort, props.loadbalancer)
    const testListener = createListener(
      this,
      'ListenerTest',
      targetGroup,
      props.serviceInfo.testListenerPort!,
      props.loadbalancer,
    )
    this.listener = listener
    this.testListener = testListener
  }
}
```

**ELBに8080ポートでリクエストを受け付けるリスナーを作成**し、デフォルトの挙動として作成するターゲットグループに転送するよう設定しています。で**ターゲットグループは、コンテナのポート80を設定し、 `` targetType: 'ip',`` なグループ**としています。またターゲットグループは通常「そのグループに属するリソース[^2]」も指定するんですが、ECSサービス側で動的に制御するので、ココでは指定しなくてOKです。

その他もろもろオプション指定もあるんですが、説明は割愛します。

[^2]: 今回の構成だと、ELBから転送されるECSコンテナ達のIPアドレスのことです



#### ECSサービス

最後にECSのサービスです。

```typescript:lib/ECSServiceStack.ts
import { App, ScopedAws, Stack, StackProps } from 'aws-cdk-lib'
import { CfnSecurityGroup, CfnSubnet } from 'aws-cdk-lib/aws-ec2'
import { ContainerInfo, ServiceInfo, getProfile, toRefs } from './Utils'
import { CfnCluster, CfnService, CfnTaskDefinition } from 'aws-cdk-lib/aws-ecs'
import { CfnTargetGroup } from 'aws-cdk-lib/aws-elasticloadbalancingv2'

type ECSServiceStackProps = StackProps & {
  subnets: CfnSubnet[]
  cluster: CfnCluster
  taskDef: CfnTaskDefinition
  ecsSecurityGroup: CfnSecurityGroup
  containerInfo: ContainerInfo
  serviceInfo: ServiceInfo
  targetGroup: CfnTargetGroup
}

export class ECSServiceStack extends Stack {
  public readonly ecsService: CfnService
  constructor(scope: App, id: string, props: ECSServiceStackProps) {
    super(scope, id, props)
    const p = getProfile(this)
    const { accountId, region } = new ScopedAws(this)

    const DesiredCount = props.subnets.length

    const ecsService = new CfnService(this, 'ECSService', {
      cluster: props.cluster.ref,
      // deploymentController: ECS
      capacityProviderStrategy: [
        {
          capacityProvider: 'FARGATE',
          base: 0,
          weight: 1,
        },
      ],
      deploymentController: {
        type: 'ECS',
      },
      deploymentConfiguration: {
        maximumPercent: 200,
        minimumHealthyPercent: 100,
        deploymentCircuitBreaker: {
          enable: true,
          rollback: true,
        },
      },
      // deploymentController: ECS
      // // deploymentController: CODE_DEPLOY
      // launchType: 'FARGATE',
      // deploymentController: {
      //   type: 'CODE_DEPLOY',
      // },
      // // deploymentController: CODE_DEPLOY

      taskDefinition: props.taskDef.ref,
      serviceName: props.serviceInfo.serviceName,
      schedulingStrategy: 'REPLICA',
      desiredCount: DesiredCount,
      loadBalancers: [
        {
          containerName: props.containerInfo.name,
          containerPort: props.containerInfo.port,
          // LoadBalancerName:
          //   Ref: 'AWS::NoValue'
          targetGroupArn: props.targetGroup.ref,
        },
      ],
      networkConfiguration: {
        awsvpcConfiguration: {
          assignPublicIp: 'DISABLED',
          securityGroups: toRefs([props.ecsSecurityGroup]),
          subnets: toRefs(props.subnets),
        },
      },
      platformVersion: 'LATEST',
      serviceConnectConfiguration: { enabled: false },
      tags: [{ key: 'Name', value: `ECS${p.name}` }],
      enableEcsManagedTags: true,
    })
    this.ecsService = ecsService
  }
}
```

タスク定義は先ほど作成したもの(``taskDefinition: props.taskDef.ref``)をつかうとか、セキュリティグループは先ほど作成したSGを指定するとか、下記のようなロードバランサの設定をするとか、

```typescript
loadBalancers: [
  {
    containerName: 'app',
    containerPort: 80,
    targetGroupArn: props.targetGroup.ref, // このターゲットグループを使ってね、という指定
  },
],
```

ECSを配置するサブネットはPrivateサブネットにする、などなどの設定をしています。

これで ``ELB:8080 → リスナの定義で ポート80のターゲットグループに転送 → ターゲットグループに設定されたコンテナたちに転送`` という構成ができましたね。






#### cdk.json

```json:cdk.json(の主要な部分)
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-samples.ts",
  ...
  "context": {
    ...
    "dev": {
      "name": "dev-20230903"
    }
  }
}
```




### 実行してみる

長かったですね。。CDKを実行して、ECSサービスでWEBサーバを立ててみましょう。

```console
$ yarn cdk deploy --all
... しばらくかかります
$
```

終わったようです。

ちゃんとできているか、WEBサーバにアクセスしてみます。

```console
% aws elbv2 describe-load-balancers \
--query "LoadBalancers[*].[LoadBalancerName,DNSName]" \
--output table
---------------------------------------------------------------------------------------------------------------------
|                                               DescribeLoadBalancers                                               |
+---------------------------------+---------------------------------------------------------------------------------+
|  .......                        |  xx.elb.ap-northeast-1.amazonaws.com                                            |
|  app-ELB-dev-20230903           |  app-ELB-dev-20230903-1280708179.ap-northeast-1.elb.amazonaws.com               |
+---------------------------------+---------------------------------------------------------------------------------+
% 
```



```console
$ curl http://app-ELB-dev-20230903-1280708179.ap-northeast-1.elb.amazonaws.com:8080 -i

HTTP/1.1 200 OK
Date: Sun, 27 Aug 2023 16:08:03 GMT
Content-Type: text/html
Content-Length: 615
Connection: keep-alive
Set-Cookie: AWSALB=oCDHz/dYXw1e0q8vCAQyHHxxxxQG71ZGv64ybMzI; Expires=Sun, 03 Sep 2023 16:08:03 GMT; Path=/
Set-Cookie: AWSALBCORS=oCDHz/dYXw1e0xxxxxx71ZGv64ybMzI; Expires=Sun, 03 Sep 2023 16:08:03 GMT; Path=/; SameSite=None
Server: nginx/1.25.2
Last-Modified: Tue, 15 Aug 2023 17:03:04 GMT
ETag: "64dbafc8-267"
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
$
```



うまくいったようですね！AWSのコンソール画面も見てみましょう。

Elastic Container Service > クラスター > app-server-dev-20230903-cluster > サービス > app-service > ネットワーキング 



![ECSService](https://imgur.com/7dmYQQm.png)


できていそうです。

以下はELBの画面。ターゲットグループやリスナーもできてるっぽいです。9080に関しては今回は説明を割愛します。


![ELB](https://imgur.com/AL5b75f.png)



ターゲットグループの中身。Private Subnet上にたっているポート80のコンテナ達が登録されているのがわかりますね！

![target](https://imgur.com/kJuPtRV.png)



などなど、ECSサービスでnginxを起動してみました！

### 作った環境を消す

最後に、作った環境を消す際には

```
$ yarn cdk destroy --all
```

で。VPCからなにまで全て一度に消してくれます。。



## TIPS

今回はココまでですが、今回作成したCDKのコードを書き換えて、今後も以下のことをやっていきたいと思います。

- [アプリをnginxではなく、SpringBootなどの自前のアプリに差し替え](https://qiita.com/masatomix/items/8ea74d8308b6c096368d)
- LogをデフォルトのCloudWatchに流す設定にしているところを、Logを制御するサイドカー(別のコンテナ)を設定し、それに制御させる
  - まずはAWSが提供する FluentBitのイメージを使って制御する
  - 自前で用意したFluentBitのイメージを使って、設定ファイルなども制御する
  - SpringBootが出力するJSON形式のログを整形してCloudWatchに出力するとか、エラーのみのロググループを作成するとか自前のイメージでいろいろやる
- ALBをPrivate Subnetに配置する
  - PrivateなSubnetに配置しつつも、おなじVPC上のEC2インスタンスにPortForwardingを仕掛けて、外のネットからもアクセスできるようにする練習
- ALBじゃなくてNLBにしてみる
  - API GatewayのVPC Linkなどを用いる場合に状況によってはNLBを使う必要もあるため、やってみる
  - セッション共有のための API Gateway → NLB → ALBなどの多段ELB構成をやってみる
- ECSコンテナへのリリース方式(デプロイタイプ)をいろいろ試してみる
  - 今回はデフォの方式(ローリングアップデート？)を用いた
  - ELBとCodeDeployの機構を用いることで、blue green deployment [^3]してみる

お疲れさまでした！

[^3]: 新機能をリリースして新旧同時に動かせる(ポート9080に先にリリースします)ようにして、あるタイミングで新機能にトラフィックを変更するリリース方法




## 関連リンク

- [今回のソースコード](https://github.com/masatomix/cdk-samples/releases/tag/ecs_first_nginx)
- [AWS CDKのTIPS集](https://qiita.com/masatomix/items/7087e7813637ff5e9c36)
- [AWS CDK で Infrastructure as Code する: VPC編](https://qiita.com/masatomix/items/fc414e98d6d0091db90d)
- [AWS CDK で Infrastructure as Code する: EC2編](https://qiita.com/masatomix/items/03dd59929ef1869ea027)