AWS ECS on Fargateを使ってコンテナ構築～コンテナレジストリ構築編～

# はじめに

[機械学習モデルの推論結果を返す Web API を FastAPI で作成](https://qiita.com/honda28/items/1cef6e3ac63077aa893e)で作成したものを AWS 環境でデプロイする。Docker を用いた開発環境で作成したアプリであるため、そのままコンテナイメージを活用して AWS ECS on Fargate 上で稼働させる。[AWSコンテナ設計・構築[本格]入門](https://www.amazon.co.jp/gp/product/4815607656/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)を参考にした。全体の流れは以下である。

1. [ネットワーク構築](https://qiita.com/honda28/items/94ff481fb9256b339944)
1. コンテナレジストリ構築
1. オーケストレーション構築

構築はすべて AWS CloudFormation を用いて実行する。CloudFormation で使用するテンプレートファイルは、[AWSコンテナ設計・構築[本格]入門](https://www.amazon.co.jp/gp/product/4815607656/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&psc=1)で提供されている[テンプレートファイル](https://github.com/uma-arai/sbcntr-resources/tree/main/cloudformations)をベースにしている。
説明の流れに合わせてテンプレートファイルを `network.yml`, `registry.yml`, `orchestration.yml` の3つに分けるが、分け方として適切ではないので、実際には [AWS CloudFormation ベストプラクティス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html)などを参考にファイルを分割した方がよい。
本記事では**コンテナレジストリ構築**に関して述べるため、`registry.yml` のみ使用する。

# ECR (Elastic Container Registry) リポジトリの作成

AWS マネジメントコンソール上でリポジトリを作成する。詳細は[公式ドキュメント](https://aws.amazon.com/jp/ecr/)を参照。リポジトリ名は [test-image] とした。
![ecr_top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/4cc28c4f-f980-da86-7b65-fb205cc93df7.png)

# コンテナレジストリへのネットワーク

前記事である [AWS ECS on Fargateを使ってコンテナ構築～ネットワーク構築編～](https://qiita.com/honda28/items/94ff481fb9256b339944)で作成したベースネットワークを、以下のように更新する。インターフェイス型 VPC エンドポイントが属するサブネットをアプリケーションから分離するために新しいサブネットを作成し、ECR 向けの VPC エンドポイントを作成。また S3 向けのゲートウェイ型 VPC エンドポイントを作成し、[前記事](https://qiita.com/honda28/items/94ff481fb9256b339944)で作成したルートテーブルに紐付けている。

![AWSエンドポイント.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/769545/438c0c68-6875-ffb5-2ad1-4c34f5ebe3f5.png)

サブネットの設定は以下のようにした。

| 用途 | NW 区分 | AZ | CIDR | サブネット名 |
|:-:|:-:|:-:|:-:|:-:|
| Egress 用 | Private | 1a | 10.0.248.0/24 | test-subnet-private-egress-1a |
| Egress 用 | Private | 1c | 10.0.249.0/24 | test-subnet-private-egress-1c |

# CloudFormation テンプレートファイル

上記で説明したネットワークを構築するテンプレートファイルが以下となる。

```yml:registry.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: Vpc endpoint template
Resources:
  # VPCエンドポイント周りの設定
  ## VPCエンドポイント(Egress通信)用のプライベートサブネット
  testSubnetPrivateEgress1A:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.248.0/24
      VpcId:
        !ImportValue testVpc
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: test-subnet-private-egress-1a
        - Key: Type
          Value: Isolated
  testSubnetPrivateEgress1C:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.249.0/24
      VpcId:
        !ImportValue testVpc
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: test-subnet-private-egress-1c
        - Key: Type
          Value: Isolated

  ## VPCエンドポイント用セキュリティグループの生成
  testSgEgress:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security Group of VPC Endpoint
      GroupName: egress
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      Tags:
        - Key: Name
          Value: test-sg-vpce
      VpcId:
        !ImportValue testVpc

  ## Container -> VPC endpoint
  testSgVpceFromSgContainerTCP:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      Description: HTTPS for Container App
      FromPort: 443
      GroupId:
        Fn::GetAtt:
          - testSgEgress
          - GroupId
      SourceSecurityGroupId:
        !ImportValue testSgContainer
      ToPort: 443

  # VPCエンドポイントの作成
  ## ECR 向けインターフェイス型 VPCエンドポイント
  testEndpointEcr:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - !Ref testSgEgress
      ServiceName: !Join
        - ''
        - - com.amazonaws.
          - !Ref 'AWS::Region'
          - .ecr.api
      SubnetIds:
        - !Ref testSubnetPrivateEgress1A
        - !Ref testSubnetPrivateEgress1C
      VpcEndpointType: Interface
      VpcId: !ImportValue testVpc
  testEndpointDkr:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: True
      SecurityGroupIds:
        - !Ref testSgEgress
      ServiceName: !Join
        - ''
        - - com.amazonaws.
          - !Ref 'AWS::Region'
          - .ecr.dkr
      SubnetIds:
        - !Ref testSubnetPrivateEgress1A
        - !Ref testSubnetPrivateEgress1C
      VpcEndpointType: Interface
      VpcId: !ImportValue testVpc

  ## S3 向けゲートウェイ型 VPCエンドポイント
  testEndpointS3:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      RouteTableIds:
        - !ImportValue testRouteApp
      ServiceName: !Join
        - ''
        - - com.amazonaws.
          - !Ref 'AWS::Region'
          - .s3
      VpcEndpointType: Gateway
      VpcId: !ImportValue testVpc

Outputs:
  # Subnet
  ## Egress private subnet
  testSubnetPrivateEgress1A:
    Value: !Ref testSubnetPrivateEgress1A
    Export:
      Name: testSubnetPrivateEgress1A
  testSubnetPrivateEgress1C:
    Value: !Ref testSubnetPrivateEgress1C
    Export:
      Name: testSubnetPrivateEgress1C

  # Security group
  ## Egress security group
  testSgEgress:
    Value: !Ref testSgEgress
    Export:
      Name: testSgEgress
```

サブネットなどと同様に VPC エンドポイントにもタグで Name を付与したかったが、[AWS::EC2::VPCEndpoint - Tags #196](https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/196) によると、VPCEndpoint では Tags がサポートされていないらしい。仕方ないので、今回作成した3つの VPC エンドポイントには AWS CLI を使って Name タグを付ける。

```bash
$ aws ec2 create-tags --resources [endpoint ID] --tags Key=Name,Value=[endpoint name]
```

# コンテナイメージの登録

上記までで ECR にコンテナイメージを登録する準備が完了した。ここでは、アプリケーションのビルド処理を行い、作成されたコンテナイメージを ECR に登録する。（本記事では、AWS ECS on Fargate を使ってコンテナを構築することを主としているため、コンテナ上で動くアプリケーションはすでに存在している前提で話を進めている。）
まずコンテナイメージをビルドする。

```bash
$ docker image build -t test-image:0.0.1 .
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID       REPOSITORY   TAG
opqrstuvwxyz   test-image   0.0.1
```

次に、[Docker イメージをプッシュする](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/docker-push-ecr-image.html)を参考に上記で作成したイメージを ECR へプッシュしていく。
Docker イメージを ECR に登録するために、AWS アカウントが認識できるような形式にする。

```bash
$ AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
$ docker image tag test-image:0.0.1 ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/test-image:0.0.1
$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID       REPOSITORY                                                  TAG
abcdefghijkl   123456789.dkr.ecr.ap-northeast-1.amazonaws.com/test-image   0.0.1
```

Amazon ECR レジストリに対して Docker クライアントを認証する。

```bash
$ aws ecr --region ap-northeast-1 get-login-password | docker login --username AWS --password-stdin https://${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/test-image
Login Succeeded
```

Docker イメージをプッシュする。

```bash
$ docker image push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/test-image:0.0.1
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/test-image]
2eb0dc7856ad: Pushed
0c668eb965fd: Pushed
...
```

以上で、ECR にコンテナイメージを登録することができた。

# おわりに

AWS ECS on Fargate を使ってコンテナを構築する第2段階として、VPC エンドポイントを作成し、コンテナイメージを ECR に登録した。いよいよコンテナを構築していく。