AWS CDK で Infrastructure as Code する: SpringBoot編2

こんにちは。ただいまAWS CDK絶賛勉強中です。

https://qiita.com/masatomix/items/445335b5c216f10485c5

のつづきです。前回まででDockerイメージが作成できたので、そのイメージを使ってECSを起動していきます。

ちなみに、[前回](https://qiita.com/masatomix/items/445335b5c216f10485c5)作ったDockerイメージはこれでした。

```console
$ docker images
REPOSITORY                        TAG              IMAGE ID       CREATED      SIZE
myorg/spring-boot-sample-tomcat   0.1.1-SNAPSHOT   cdcd3af69190   3 days ago   363MB
$
```

## 前提条件

- [AWS CDK で Infrastructure as Code する: ECS編](https://qiita.com/masatomix/items/90c16313040394251100) 
- [AWS CDK で Infrastructure as Code する: SpringBoot編1](https://qiita.com/masatomix/items/445335b5c216f10485c5)

などを終えておくこと。


## 作る環境

こんなかんじになります。

![env](https://imgur.com/ldX6z1i.png)

## やってみる

### Dockerイメージを格納する、ECRの作成

まずは自前のDockerイメージを格納するリポジトリAmazon ECR( [Amazon Elastic Container Registry](https://aws.amazon.com/jp/ecr/) ) を作成します。ECRはAWS CDKでも作れますが、今回はさくっとコマンドラインから。リポジトリ名はspring-boot-sample-tomcatとします。

```console
$ aws ecr create-repository \
    --repository-name spring-boot-sample-tomcat \
    --image-scanning-configuration scanOnPush=true \
    --region ap-northeast-1
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxx:repository/spring-boot-sample-tomcat",
        "registryId": "xxxxx",
        "repositoryName": "spring-boot-sample-tomcat",
        "repositoryUri": "xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-tomcat",
        "createdAt": "2023-10-09T11:32:52+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": true
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}
```

できたようです。AWSのコンソール画面でみても

![VYsRsNs](https://imgur.com/Z55ikjS.png)



うん、できていそうです。 

参考 [AWS CLI での Amazon ECR の使用](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/getting-started-cli.html#cli-create-repository)



### ECRへアップロード

つづいて作成したECRへ、先のDockerイメージをアップロードします。アップロードにはECRへのログインなど多少の前準備が必要ですので、順番にやっていきます。

あらためてイメージの確認。

```console
$ docker images
REPOSITORY                        TAG              IMAGE ID       CREATED      SIZE
myorg/spring-boot-sample-tomcat   0.1.1-SNAPSHOT   cdcd3af69190   4 days ago   363MB
```

アップロードしたいのはこのイメージ(``IMAGE ID: cdcd3af69190``)でした。

まずはAWSのECRにログインしておきます。

```console
$ ECR_REPOSITORY_NAME=spring-boot-sample-tomcat ← 先ほど作成したECRのリポジトリ名
$ version=0.1.1-SNAPSHOT ← バージョンはTAG名

$ AWS_REGION_NAME=ap-northeast-1
$ AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
↑ 使用しているプロファイルのアカウントIDを取得している

$ aws ecr --region ${AWS_REGION_NAME} get-login-password | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com
WARNING! Your password will be stored unencrypted in /home/xxx/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
$
```

つづいて、先のイメージにタグをつけます。

```console
$ REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com/${ECR_REPOSITORY_NAME}
$ docker image tag cdcd3af69190 ${REPOSITORY_URI}:${version}
↑ cdcd3af69190 というのは先ほど確認した IMAGE IDです

$ docker images
REPOSITORY                                                                    TAG              IMAGE ID       CREATED      SIZE
xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-tomcat   0.1.1-SNAPSHOT   cdcd3af69190   4 days ago   363MB
myorg/spring-boot-sample-tomcat                                               0.1.1-SNAPSHOT   cdcd3af69190   4 days ago   363MB
```

おなじIMAGE IDのイメージに、タグ(ようするに別名)をつけることができました。

最後にイメージをアップロード( ``docker image push`` )します。

```console
$ docker image push ${REPOSITORY_URI}:${version}
The push refers to repository [xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/spring-boot-sample-tomcat]
7e1014d42cd2: Pushed
34f7184834b2: Pushed
5836ece05bfd: Pushed
72e830a4dff5: Pushed
0.1.1-SNAPSHOT: digest: sha256:37e2111ef45a00373393b766bd920d1648d5900448738b2434387566c4e3aef7 size: 1163
$
```

できたようです。画面でも見てみましょう。

![yNgUftj](https://imgur.com/sHrEdDf.png)



よさそうですね。
以上でECR関連の作業は完了です。



### CDKのソースコード を取得

さてSpringBootのDockerイメージをECRにアップロードできたので、次はECSサービスがそれを使用する設定をしていきます。

いままで通り CDK を用いるので、[前回(nginxをECSサービス化したとき)](https://qiita.com/masatomix/items/90c16313040394251100) のCDKのソースコードを取得しておきます。

```console
$ git clone -b ecs_first_nginx https://github.com/masatomix/cdk-samples.git
$ 
```



### 前回のECSサービスを削除

さてコードを修正する前に、前回作成したnginxのECSサービスが別のタスク定義を参照していて競合するため、まずはECSサービス関連を削除します。CDKで

```console
$ cd cdk-samples
cdk-samples$ yarn cdk destroy ECSServiceStack ECSServiceELBStack
....
cdk-samples$ 
```

って削除すればOK。

### タスク定義等の書き換え

さてECSが使用するDockerイメージを差し替えるためのソースコードの変更箇所ですが、

- image名としてnginxのイメージを指定していた箇所を、ECRのURLに変更
- コンテナのポート番号を80 → 8080へ変更(nginxとSpringBootはデフォで使用するポートがちがう)
- ヘルスチェックのURLを変更

などがあります。具体的には以下の通り。

```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',
ココ→      image: `${accountId}.dkr.ecr.${region}.amazonaws.com/spring-boot-sample-tomcat:0.1.1-SNAPSHOT`,
          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',
    })
  }
}

```




```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')

  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: '/',
ココ→ port: 8080, // コンテナが利用するポート番号 が8080
ココ→ healthCheckPath: '/actuator/health', // ヘルスチェックもURLが異なる
  }

  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()

```



### CDKを実行する

ではCDKを実行します。いつもどおり


```console
cdk-samples$ yarn cdk deploy --all
```

でOK。おわったらURLを確認してアクセスしてみます。

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

URLがわかったので、さきほどのヘルスチェックのURLにアクセスしてみましょう。

```console
$ curl http://app-ELB-dev-20230827-1197243654.ap-northeast-1.elb.amazonaws.com:8080/actuator/health -i 
HTTP/1.1 200
Date: Mon, 09 Oct 2023 16:03:32 GMT
Content-Type: application/vnd.spring-boot.actuator.v3+json
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: AWSALB=oxq+VQxZTBSHBSgxLHBN2oiz1zRR69Qc2bPpyjCEdISbun1MGNh5YeJS/PqBdUtMyJUAb9HSmwyObePK/lNgblon9o0knhmgEw/AEuNaUAV8s0d82+6aw2s0zjfd; Expires=Mon, 16 Oct 2023 16:03:32 GMT; Path=/
Set-Cookie: AWSALBCORS=oxq+VQxZTBSHBSgxLHBN2oiz1zRR69Qc2bPpyjCEdISbun1MGNh5YeJS/PqBdUtMyJUAb9HSmwyObePK/lNgblon9o0knhmgEw/AEuNaUAV8s0d82+6aw2s0zjfd; Expires=Mon, 16 Oct 2023 16:03:32 GMT; Path=/; SameSite=None

{"status":"UP"}
$
```

SpringBootアプリが動いていそうです！

以上で、ECRに独自にアップロードしたSpringBootのイメージを用いて、ECSサービスを動かすことができました。

おつかれさまでした。



## 関連リンク

- [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)
- [AWS CDK で Infrastructure as Code する: ECS編](https://qiita.com/masatomix/items/90c16313040394251100)
- [AWS CDK で Infrastructure as Code する: SpringBoot編1](https://qiita.com/masatomix/items/445335b5c216f10485c5)
- [今回追加したコード差分](https://github.com/masatomix/cdk-samples/compare/ecs_first_nginx...ecs_firelens_and_springboot)