Nuxt3をCodePipeline×AWS-CDKでAppRunnerにデプロイする

## はじめに
nuxt3は11/16正式リリースされましたね！！
AppRunnerもつい最近プライベートエンドポイントに対応したので、利用の検討を考えている人も多いと思うのでデプロイ方法とアーキテクチャーを検討してみました

今回の記事で使用したリソースを保管しているリポジトリ: [リンク](https://github.com/hontaku/apprunner-nuxt3)

## 前提条件

- AWSアカウントは開発環境と本番環境の2つがあると仮定
- 開発環境の中に開発中に使用するDEV環境と、本番環境とほぼ同等のSTG環境の2つがある想定
- 今回は本番環境については考慮しない

## Deploy戦略

AppRunnerにアプリケーションをデプロイする方法は以下の３つのいずれかが多いかと思います。

1. CodePipelineとCodeBuild、CloudformationでコンテナをビルドしてAppRunnerを構築する
    ![deploy-cloudformation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/0fbfaa0f-f327-7186-fe2c-e739cf1f7751.png)

2. コンテナをCI/CDを備えたソフトウェアでビルド後ECRにプッシュして、AppRunnerの自動反映（AutoDeploy）機能でアプリケーションを反映する
    ![deploy-autodeploy-container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/c2475c32-ed2b-bfec-5645-e59912ab1315.png)

3. Githubのソースコードから、AppRunnerの自動反映機能でアプリケーションのコンテナを構築・反映する
    ![deploy-autodeploy-source.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/7ca6ca45-4a26-ba54-2659-809784824065.png)

1のメリット
  - Cloudformationを使ってDeployしているため、コンテナのデプロイ時にlatestタグではなくバージョン指定してDeployが可能。（CodePipelineの部分をCI/CDツールに置き換えてAWS-CDKを使う方法でも可）
  - アクセスキー・シークレットキーを発行せずRoleにアタッチした権限でデプロイが可能
  - ECR１つに対して複数環境を構築することが可能(今回は環境ごとに分けます)

2のメリット
  - コンテナさえECRにPushできる環境があれば自動反映が使える

3のメリット
  - CI/CDのツールを構築する必要がなくマネジメントコンソールからApp Runnerを構築したらすぐデプロイできる
 

この記事では**１の方法でデプロイ**します。2や3の方法は様々な方が実践されていますのでそちらご参考ください。

### 全体アーキテクチャー図
:::note warn
注意
App RunnerのアウトバウンドはNAT Gateway経由でインバウンドはVPCエンドポイント経由のまたはインターネットのパブリックアクセスになります。（非公開設定は入れておりません）
:::
App Runnerをプライベートエンドポイントアクセスの設定して、VPCからのみアクセスできるようにしようと思ったのですが、インフラ周りのソースとドキュメントを構築し終えた時にAWS-CDKでプライベートエンドポイント作れないことがわかったのでパブリック公開とて実装してします。悲しいね。。。（Cloudformationではプライベートエンドポイントの作成には対応してるので、Cloudformationテンプレートに書き起こせる方はご検討ください。）

今回自分が作成したネットワークではPrivate subnetの必要性はあまりありませんが、データベースのRDSを追加する可能性があるため作成しています。

![network-network-resource.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/5ea62ce7-2f51-e726-d83d-8864d0a20f23.png)


### ネットワーク構成図
ルーティングやセキュリティグループについて以下の通り構築する。
- リクエストの受け口となるロードバランサーやインターネットアクセスの受付をするNAT GatewayはPublic subnetに配置
- EC2やFargateその他アプリケーションを配置するリソースは、インターネットのインバウンドのアクセスがないProtect subnetに配置
    - ※Protect subnetはインターネットのインバウンド・アウトバウンドができないPrivate subnetに対して、アウトバウンドをできるようにしたもの
- RDSのMySQLやPostgreSQLはProtect subnetにあるリソースからのみアクセスできるようにPrivate subnetに配置

<details><summary>VPCとサブネットのテンプレート</summary>

```yaml:vpc.yml
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
      CidrBlock: !Sub ${NwSeg}.0/24
      EnableDnsHostnames: true
      EnableDnsSupport: true

  # AZa
  PublicSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.0/27'
      AvailabilityZone: ap-northeast-1a
  ProtectSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.64/26'
      AvailabilityZone: ap-northeast-1a
  PrivateSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Private'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.192/28'
      AvailabilityZone: ap-northeast-1a

  # AZc
  PublicSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.32/27'
      AvailabilityZone: ap-northeast-1c
  ProtectSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.128/26'
      AvailabilityZone: ap-northeast-1c
  PrivateSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Private'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.208/28'
      AvailabilityZone: ap-northeast-1c
```

</details>

##### Rooting
- VPC内であれば自動的にlocalへのルーティングがされるため省略
![network-network-rooting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/1353ace6-2b26-8d40-b005-818dc2d9d30e.png)

<details><summary>ルーティング周りを行っているテンプレート</summary>

```yaml

  # VPC Internet rooting
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
  RouteInternetGateway:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicSubnetRouteTable

  # PublicSubnet Rooting
  PublicSubnetRouteTableAssociationAZa:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      SubnetId: !Ref PublicSubnetAZa
  PublicSubnetRouteTableAssociationAZc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      SubnetId: !Ref PublicSubnetAZc

  # AZa ProtectSubnet rooting
  ElasticIPAZa:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      Domain: vpc
  NatGatewayAZa:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      AllocationId: !GetAtt ElasticIPAZa.AllocationId
      SubnetId: !Ref PublicSubnetAZa
    DependsOn:
      - ElasticIPAZa
  ProtectSubnetRouteTableAZa:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
  RouteNatGatewayAZa:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayAZa
      RouteTableId: !Ref ProtectSubnetRouteTableAZa
  ProtectSubnetRouteTableAssociationAZa:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref ProtectSubnetRouteTableAZa
      SubnetId: !Ref ProtectSubnetAZa

  # AZa ProtectSubnet rooting
  ElasticIPAZc:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      Domain: vpc
  NatGatewayAZc:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      AllocationId: !GetAtt ElasticIPAZc.AllocationId
      SubnetId: !Ref PublicSubnetAZc
    DependsOn:
      - ElasticIPAZc
  ProtectSubnetRouteTableAZc:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
  ProtectSubnetRouteAZc:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayAZc
      RouteTableId: !Ref ProtectSubnetRouteTableAZc
  ProtectSubnetRouteTableAssociationAZc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref ProtectSubnetRouteTableAZc
      SubnetId: !Ref ProtectSubnetAZc

  AppRunnerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref ProtectSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.apprunner'
      SubnetIds:
        - !Ref ProtectSubnetAZa
        - !Ref ProtectSubnetAZc
      VpcEndpointType: 'Interface'
      VpcId: !Ref VPC

```

</details>

##### Public subnetに配置されるリソース向けのセキュリティグループ
- インターネットからのインバウンドはTCPまたはUDPの80または443のポートを許可
- Public subnet内インバウンドとアウトバウンド、インターネットへのアウトバウンドはTCPのみ全ポート許可
- Protect subnetへはTCPまたはUDPの80または443のポートを許可
![network-network-public-security-group.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/4e7b6263-b06e-6c90-aae3-805adbe4768d.png)

<details><summary>Publicサブネット向けのセキュリティグループテンプレート</summary>

```yaml:vpc.yml
  PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Public'
      GroupDescription: Public Security Group
      SecurityGroupIngress:
        # Internet
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '0.0.0.0/0'
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.32/27'
      SecurityGroupEgress:
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.32/27'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.128/26'
      VpcId: !Ref VPC

```

</details>

##### Protect subnetに配置されるリソース向けのセキュリティグループ
- Public subnetへはTCPの80または443のポートを許可(主にNAT Gateway経由)
- Public subnetからのインバウンドはTCPまたはUDPの80または443のポートを許可
- Protect subnet内インバウンドとアウトバウンドはTCPのみ全ポート許可
- Private subnetへはTCPの3306または5432のポートを許可
![network-network-protect-security-group.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/79347f13-6eae-9c0a-2ddf-2849dbd05988.png)

<details><summary>Protectサブネットのセキュリティグループテンプレート</summary>

```yaml:vpc.yml

  ProtectSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Protect'
      GroupDescription: Protect Security Group
      SecurityGroupIngress:
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.32/27'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.128/26'
      SecurityGroupEgress:
        # Public Subnet AZa / AZc
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.128/26'
        # Private Subnet AZa
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.192/28'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.192/28'
        # Private Subnet AZc
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.208/28'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.208/28'
      VpcId: !Ref VPC

```

</details>

##### Private subnetに配置されるリソース向けのセキュリティグループ
- rotect subnetのインバウンドはTCPの3306または5432のポートを許可
![network-network-private-security-group.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/3a5eb1c0-d301-46cc-444d-4999ce0c54cb.png)

<details><summary>Privateサブネットのセキュリティグループテンプレート</summary>

```yaml

  PrivateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Private'
      GroupDescription: Private Security Group
      SecurityGroupIngress:
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.128/26'
      VpcId: !Ref VPC

```

</details>

UDPが許可されていないパターンありますが、リソース同士でUDPを使うことはあまりないかと思うので許可しておりません。インターネットのインバウンド系統はGoogleのQUICプロトコルのため許可しています。

##### VPC全体のテンプレート
<details><summary>Cloudformationテンプレート</summary>

```yaml:vpc.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
    Description: System's ID used as a part of the AWS resource names
  Env:
    Type: String
    Description: target environment
    AllowedValues:
      - dev
      - stg
      - prod
  NwSeg:
    Type: String
    Description: network segment
    AllowedValues:
      - 10.0.0
      - 10.0.1

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
      CidrBlock: !Sub ${NwSeg}.0/24
      EnableDnsHostnames: true
      EnableDnsSupport: true

  # AZa
  PublicSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.0/27'
      AvailabilityZone: ap-northeast-1a
  ProtectSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.64/26'
      AvailabilityZone: ap-northeast-1a
  PrivateSubnetAZa:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Private'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.192/28'
      AvailabilityZone: ap-northeast-1a

  # AZc
  PublicSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.32/27'
      AvailabilityZone: ap-northeast-1c
  ProtectSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.128/26'
      AvailabilityZone: ap-northeast-1c
  PrivateSubnetAZc:
    Type: AWS::EC2::Subnet
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Private'
      VpcId: !Ref VPC
      CidrBlock: !Sub '${NwSeg}.208/28'
      AvailabilityZone: ap-northeast-1c

  PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Public'
      GroupDescription: Public Security Group
      SecurityGroupIngress:
        # Internet
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '0.0.0.0/0'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '0.0.0.0/0'
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.32/27'
      SecurityGroupEgress:
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.32/27'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.128/26'
      VpcId: !Ref VPC

  ProtectSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Protect'
      GroupDescription: Protect Security Group
      SecurityGroupIngress:
        # Public Subnet AZa
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.0/27'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.0/27'
        # Public Subnet AZc
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: udp
          FromPort: 80
          ToPort: 80
          CidrIp: !Sub '${NwSeg}.32/27'
        - IpProtocol: udp
          FromPort: 443
          ToPort: 443
          CidrIp: !Sub '${NwSeg}.32/27'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.128/26'
      SecurityGroupEgress:
        # Public Subnet AZa / AZc
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: !Sub '${NwSeg}.128/26'
        # Private Subnet AZa
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.192/28'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.192/28'
        # Private Subnet AZc
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.208/28'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.208/28'
      VpcId: !Ref VPC

  PrivateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Private'
      GroupDescription: Private Security Group
      SecurityGroupIngress:
        # Protect Subnet AZa
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.64/26'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.64/26'
        # Protect Subnet AZc
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: !Sub '${NwSeg}.128/26'
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Sub '${NwSeg}.128/26'
      VpcId: !Ref VPC

  # VPC Internet rooting
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Subnet
          Value: !Sub 'Public'
      VpcId: !Ref VPC
  RouteInternetGateway:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicSubnetRouteTable

  # PublicSubnet Rooting
  PublicSubnetRouteTableAssociationAZa:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      SubnetId: !Ref PublicSubnetAZa
  PublicSubnetRouteTableAssociationAZc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      SubnetId: !Ref PublicSubnetAZc

  # AZa ProtectSubnet rooting
  ElasticIPAZa:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      Domain: vpc
  NatGatewayAZa:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Public'
      AllocationId: !GetAtt ElasticIPAZa.AllocationId
      SubnetId: !Ref PublicSubnetAZa
    DependsOn:
      - ElasticIPAZa
  ProtectSubnetRouteTableAZa:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZa'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
  RouteNatGatewayAZa:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayAZa
      RouteTableId: !Ref ProtectSubnetRouteTableAZa
  ProtectSubnetRouteTableAssociationAZa:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref ProtectSubnetRouteTableAZa
      SubnetId: !Ref ProtectSubnetAZa

  # AZa ProtectSubnet rooting
  ElasticIPAZc:
    Type: AWS::EC2::EIP
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      Domain: vpc
  NatGatewayAZc:
    Type: AWS::EC2::NatGateway
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Public'
      AllocationId: !GetAtt ElasticIPAZc.AllocationId
      SubnetId: !Ref PublicSubnetAZc
    DependsOn:
      - ElasticIPAZc
  ProtectSubnetRouteTableAZc:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: SystemId
          Value: !Sub '${SystemId}'
        - Key: Env
          Value: !Sub '${Env}'
        - Key: Az
          Value: !Sub 'AZc'
        - Key: Subnet
          Value: !Sub 'Protect'
      VpcId: !Ref VPC
  ProtectSubnetRouteAZc:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayAZc
      RouteTableId: !Ref ProtectSubnetRouteTableAZc
  ProtectSubnetRouteTableAssociationAZc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref ProtectSubnetRouteTableAZc
      SubnetId: !Ref ProtectSubnetAZc

  AppRunnerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref ProtectSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.apprunner'
      SubnetIds:
        - !Ref ProtectSubnetAZa
        - !Ref ProtectSubnetAZc
      VpcEndpointType: 'Interface'
      VpcId: !Ref VPC

Outputs:
  VPC:
    Value: !Ref VPC
    Export:
      Name: !Sub '${SystemId}-${Env}-VPC'

  PublicSubnetAZa:
    Value: !Ref PublicSubnetAZa
    Export:
      Name: !Sub '${SystemId}-${Env}-PublicSubnetAZa'
  ProtectSubnetAZa:
    Value: !Ref ProtectSubnetAZa
    Export:
      Name: !Sub '${SystemId}-${Env}-ProtectSubnetAZa'
  PrivateSubnetAZa:
    Value: !Ref PrivateSubnetAZa
    Export:
      Name: !Sub '${SystemId}-${Env}-PrivateSubnetAZa'

  PublicSubnetAZc:
    Value: !Ref PublicSubnetAZc
    Export:
      Name: !Sub '${SystemId}-${Env}-PublicSubnetAZc'
  ProtectSubnetAZc:
    Value: !Ref ProtectSubnetAZc
    Export:
      Name: !Sub '${SystemId}-${Env}-ProtectSubnetAZc'
  PrivateSubnetAZc:
    Value: !Ref PrivateSubnetAZc
    Export:
      Name: !Sub '${SystemId}-${Env}-PrivateSubnetAZc'

  PublicSecurityGroup:
    Value: !Ref PublicSecurityGroup
    Export:
      Name: !Sub '${SystemId}-${Env}-PublicSecurityGroup'
  ProtectSecurityGroup:
    Value: !Ref ProtectSecurityGroup
    Export:
      Name: !Sub '${SystemId}-${Env}-ProtectSecurityGroup'
  PrivateSecurityGroup:
    Value: !Ref PrivateSecurityGroup
    Export:
      Name: !Sub '${SystemId}-${Env}-PrivateSecurityGroup'

```

</details>

### IAM Roleの作成
AWSを使うときの一番の難所であるIAMについてAppRunnerの周りで使用するRoleを簡単に解説します。 ~~(IAMが一番の難所なんしょ？なんしょだけに)~~

<details><summary>IAMと権限周りを簡単に説明すると</summary>

- IAM User
    - 個人のアカウント(IAM ユーザーもAWSのリソースの一つのため1アカウントにつき1AWSアカウントのみ紐づく）
    - IAM GroupやIAM Policyを複数付与可能
- IAM Group
    - 複数人を一つのグループにまとめることが可能
    - グループに対してIAM Policyを付与することで複数人に権限を与えることが可能
- IAM Policy
    - 公式ドキュメントではアイデンティティベースポリシーと記載されている
    - ピカチュウがレベルアップしたら10万ボルトが使えるように、IAM Policyは操作する人に権限を与えるイメージ
- IAM Role
    - ユーザーや各AWSをリソースがRoleにスイッチロールするとそのロールの権限で操作できるようになる
    - AWSアカウントを複数持っているが、IAMユーザーを1つにしたい場合、別AWSアカウントにあるRoleにスイッチロールすることで別AWSアカウントを操作できるようになる（後述するRoleのリソースベースポリシーの設定が必要）
- リソースベースポリシー
    - リソースベースポリシーと呼ばれているが、サービスごとに名称が違っている
        - S3ならバケットポリシー、API Gatewayならリソースポリシー、IAM Roleなら信頼関係ポリシーなど呼ばれる
        - JSONで許可するやつ（JSONドキュメントやポリシードキュメントと言ったり）にPrincipalというプロパティがあればだいたいリソースベースポリシー
    - ピカチュウがサトシの言うことは聞いてロケット団の言うことは聞かないように、操作される側が操作する側を許可不許可を指定するもの
PassRole
    - 現在のロールを操作先のAWSリソースにロールを渡すイメージ
        - 例えばマネジメントコンソールでCloudformationでStackを作るときに実行するRoleを指定する


</details>

##### AppRunnerがコンテナ立ち上げ時に使用するAppRunnerBuildRole
- ビルド時は`build.apprunner.amazonaws.com`がECRのコンテナを取得する
    - そのため信頼関係は`apprunner.amazonaws.com`的なものではなく`build.apprunner.amazonaws.com`がスイッチロールする許可を与える
- ECRをPullするだけなので、この機能のために用意されたマネジードポリシーを利用する
- Githubにアクセスするために権限必要かどうかは別のサイトをご覧ください 
![iam-AppRunnerBuildRole.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/cb01199b-5186-69a4-0571-ae35e5768b74.png)
<details><summary>AppRunnerBuildRoleテンプレート</summary>

```yaml:iam.yml
  AppRunnerBuildRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppRunnerBuildRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - build.apprunner.amazonaws.com
            Action: sts:AssumeRole
      Path: /
```

</details>

##### AppRunner内で使用しているFargateにアタッチするAppRunnerServiceRole（アプリ内でAWS-SDKを使う時に使用）
- AppRunnerのタスクは`tasks.apprunner.amazonaws.com`というエンドポイントのため、信頼関係を`tasks.apprunner.amazonaws.com`にしてスイッチロールする許可を与える
- アイデンティティベースポリシーは基本的に自分が使いたいサービスと権限を指定する
    - 今回はDynamoDBのアイテムの挿入・更新・削除を指定してみた（テーブル名に特定のプレフィックスを持つテーブルに限定）

![iam-AppRunnerServiceRole.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/416f266c-c039-64a5-4c41-4af423212e70.png)

<details><summary>AppRunnerServiceRoleのテンプレート</summary>

```yaml:iam.yml
  AppRunnerServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppRunnerServiceRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - tasks.apprunner.amazonaws.com
            Action: sts:AssumeRole
      Path: /

  DynamoDBItemAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: DynamoDBItemAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - dynamodb:BatchGetItem
              - dynamodb:DescribeStream
              - dynamodb:DescribeTable
              - dynamodb:Get*
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:BatchWriteItem
              - dynamodb:DeleteItem
              - dynamodb:UpdateItem
              - dynamodb:PutItem
            Resource:
              - !Sub 'arn:aws:dynamodb:${AWS::Region}:*:table/${SystemId}*'
      Roles:
        - !Ref AppRunnerServiceRole

```

</details>

##### CodePipelineやCodeBuild上でAppRunnerのデプロイを行うために必要な権限を与えたCodePipelineServiceRole
- CodePipelineとCodeBuildに明示的にロールを指定しており、CloudformationはCodeBuild上で実行するAWS-CDKによりPassRoleされるため信頼関係に追加
- ビルドに必要そうなCodeファミリーの操作権限とビルド時にメトリクスやログを出力するためにCloudWatch周りの権限を付与
- 残りの権限はAppRunnerで使用するコンテナのビルドやAppRunnerのデプロイのために使用
- 手動で作ったPolicyは一時的なビルド資源置き場のS3やAppRunnerのVPC Connectorを作る権限を持つServiceLinkedRoleを作る権限を付与している

~~本当はもっと権限削ったほうがいいんだろうけどポリシー作るのがめんどくさくて。。。~~

![iam-CodePipelineServiceRole.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/46b1f6fa-d759-bea2-cf5f-c3181bb096b4.png)

<details><summary>CodePipelineServiceRoleのテンプレート</summary>

```yaml:iam.yml
  AppRunnerServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppRunnerServiceRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - tasks.apprunner.amazonaws.com
            Action: sts:AssumeRole
      Path: /

  DynamoDBItemAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: DynamoDBItemAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - dynamodb:BatchGetItem
              - dynamodb:DescribeStream
              - dynamodb:DescribeTable
              - dynamodb:Get*
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:BatchWriteItem
              - dynamodb:DeleteItem
              - dynamodb:UpdateItem
              - dynamodb:PutItem
            Resource:
              - !Sub 'arn:aws:dynamodb:${AWS::Region}:*:table/${SystemId}*'
      Roles:
        - !Ref AppRunnerServiceRole

```

</details>

##### IAM全体のテンプレート
<details><summary>Cloudformationテンプレート</summary>

```yaml:iam.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  SystemId:
    Type: String
    Description: System's ID used as a part of the AWS resource names

Resources:
  # https://docs.aws.amazon.com/ja_jp/apprunner/latest/dg/security_iam_service-with-iam.html
  AppRunnerBuildRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppRunnerBuildRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - build.apprunner.amazonaws.com
            Action: sts:AssumeRole
      Path: /

  AppRunnerServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AppRunnerServiceRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - tasks.apprunner.amazonaws.com
            Action: sts:AssumeRole
      Path: /

  DynamoDBItemAccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: DynamoDBItemAccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - dynamodb:BatchGetItem
              - dynamodb:DescribeStream
              - dynamodb:DescribeTable
              - dynamodb:Get*
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:BatchWriteItem
              - dynamodb:DeleteItem
              - dynamodb:UpdateItem
              - dynamodb:PutItem
            Resource:
              - !Sub 'arn:aws:dynamodb:${AWS::Region}:*:table/${SystemId}*'
      Roles:
        - !Ref AppRunnerServiceRole

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: CodePipelineServiceRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodePipelineFullAccess
        - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
        - arn:aws:iam::aws:policy/AWSCodeCommitFullAccess
        - arn:aws:iam::aws:policy/AWSCodeDeployFullAccess
        - arn:aws:iam::aws:policy/CloudWatchFullAccess
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/CloudWatchEventsFullAccess
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
        - arn:aws:iam::aws:policy/AWSAppRunnerFullAccess
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
                - codebuild.amazonaws.com
                - codedeploy.amazonaws.com
                - cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Path: /

  CodePipelineServicePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: CodePipelineServicePolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:PutObject
              - s3:PutObjectAcl
              - s3:GetObject
              - s3:GetObjectVersion
              - s3:GetBucketVersioning
              - s3:ListBucket
            Resource:
              - !Sub arn:aws:s3:::${SystemId}-*-temporary/*
          - Effect: Allow
            Action:
              - iam:GetRole
              - iam:PassRole
            Resource:
              - '*'
          # AWSAppRunnerFullAccessにはCreateServiceLinkedRoleがあるが、VPCコネクタで使うサービスロールを作る権限がない
          # https://repost.aws/questions/QU6Pp08CACTf6hX5FRHhhdDw/missing-resource-permission-in-aws-app-runner-full-access-causes-failure-when-calling-the-create-vpc-connector-operation
          - Effect: Allow
            Action:
              - iam:CreateServiceLinkedRole
            Resource:
              - !Sub 'arn:aws:iam::${AWS::AccountId}:role/aws-service-role/networking.apprunner.amazonaws.com/AWSServiceRoleForAppRunnerNetworking'
      Roles:
        - !Ref CodePipelineServiceRole

Outputs:
  AppRunnerBuildRoleArn:
    Value: !GetAtt AppRunnerBuildRole.Arn
    Export:
      Name: AppRunnerBuildRoleArn
  AppRunnerServiceRoleArn:
    Value: !GetAtt AppRunnerServiceRole.Arn
    Export:
      Name: AppRunnerServiceRoleArn
  CodePipelineServiceRoleArn:
    Value: !GetAtt CodePipelineServiceRole.Arn
    Export:
      Name: CodePipelineServiceRoleArn

```

</details>

### Stack分割

Stackに関しては図のように分割を行った

![stack.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/07b7259e-0c84-631c-5e4b-93f3c9be1978.png)

- 黄色い背景はAWS-CDKで構築
    - 環境ごとに違ったりするため、if文やループ文などを手軽に使いたいため
    - 実行時にパラメーターを挿入しやすい（今回はAppRunnerにCodeCommitのコミットハッシュをもとにECRのバージョンを指定したかった）
- IAMとCDKのBootstrapは環境ごとというより、AWSアカウントに対して1つのリソースにすべきだと思ったので、全環境で1つのStackにした
- ネットワーク系とリポジトリ系は一つのシステムで複数コンテナ（Web画面とバッチ処理、APIとか？）を扱う可能性があるため独立させている
    - １VPCに複数のAppRunnerをデプロイする可能性がある
    - 複数のAppRunnerがあるということはリポジトリやECRが複数になる可能性が高い

### 実際にデプロイしてみる

- 事前準備
    - 操作するユーザーには上記Stack分割で出てきたリソースの作成権限を与えてください（わからなければAdministratorAccessポリシーを検討してください）
    - 操作するユーザーがCodeCommitやCDKを扱えるようにアクセスキー・シークレットキーの発行をしてください

1. CloudFormationにアクセスしてStackを3つ作成する
    1. 「Stackの作成」
    2. 「テンプレートの準備完了」→「テンプレートファイルのアップロード」で「aws/template/iam.yml」をアップロード
    3. スタックの名前は適当に(自分はapprunner-nuxt3-iamにしています)
    4. SystemIdは適当で結構ですが今後出てくる項目は統一してください（自分はapprunner-nuxt3にしています）
    5. 次へ→送信と進みデプロイ
    6. 同様の作業を「/aws/template/repository.yml」や「/aws/template/vpc.yml」で行う（Envはdev、NwSegはお好みで）

    - 出来上がるStack一覧と入力値はこの通りになるはず
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/e5e7957b-59ab-1f22-3e88-6a6ed7bb73a7.png)

    - IAMのStack
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/c1ece219-b146-d0ec-0012-647444b3fa11.png)
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/56c0eca2-93eb-c67d-c60c-6e1af9a411d7.png)

    - repositoryのStack(パラメーターでDevを選ぶとCodeCommitを作成)
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/c56a593b-5d39-5b04-88bc-4cf916f9dd1d.png)
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/1f2e0667-b75b-f289-317c-8e9f3e35a152.png)

    - VPCのStack
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/82ca9eca-d9c4-fdeb-e6ac-cd4195e40ba0.png)
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/0c48791b-efc0-d47e-2da0-80bb78e2d5fe.png)

2. 出来上がったCodeCommitにGithubの資源をPushする
    1. CodeCommitにアクセスして、URLを取得する（今回はアクセスキーを発行しているのでGRCを使用する）
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/da867e01-d785-08a5-dc04-79049b1c97e8.png)
    2. githubから引き込んだリポジトリにCodeCommitを関連付ける
        コマンドは`git remote add codecommit <リポジトリのURL>`
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/17b624a0-618f-e1e8-4839-9daa84ad0676.png)
    3. ローカルの資源をCodeCommit上のmain、ブランチにpushする
        1. `git swich -c main`
        2. `git push --set-upstream codecommit main`
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/07ead3e0-ac1b-90cd-c1cf-28e37a24e9dc.png)
    4. CodeCommit上に資源があることを確認する
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/8cd627fb-df0c-6eda-b914-21ea373f65f0.png)

3. パイプラインをCDKでデプロイする
    1. シェルを開いてリポジトリの`aws/cdk`フォルダに移動
    2. `npm i`コマンドを実行して必要なライブラリをインストール
    3. `npx cdk bootstrap --template  ../template/bootstrap.yml --qualifier nuxt3-app`を実行してBootstrapをかんりょうすｒ
    4. `npm run deploy:pipeline-dev`を実行してパイプラインを構築する
        ※もしCLIのプロファイルを指定する場合は`npm run deploy:pipeline-dev -- --profile <プロファイル名>`を実行する 
     ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/a0748b20-37ea-c591-f09d-b29b3bcde3d6.png)

4. あとは自動的にパイプラインがコンテナをビルドしてAppRunnerをデプロイする(およそ10分～15分)
    - Approvalステージがあるので、手動承認をすること 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/581025/d430b472-434c-c9d1-9e78-ba04dfa03107.png)


### 環境を複製する
今回、構築する環境はdev環境、stg環境、prod環境を想定してテンプレートを作りました。
VPC stackとのrepository stackのENVとパイプラインの作成コマンドの環境名が一致していればそれぞれの環境に対応してAppRunnerが構築されます。

またCodeCommitのコミットによるパイプラインの実行を設定していますが、ブランチによって反映される環境が異なっていますのでご注意ください
mainブランチ → dev環境
stgブランチ → stg環境
prodブランチ → prod環境


### 小学生並みの感想
めちゃくちゃ簡単にAPIと画面資源をデプロイできるようになったので、Stackのテンプレートを使い回せば15分程度で環境を沢山生やして確認ができるようになった。
AppRunnerは偉大。。。Lambdaと違ってレスポンスタイムも速い。（API Gateway + Lambda(Node.js)で100ms~500msに対してAppRunnerは10ms・・・）
基本的なコンテナアプリケーションの実行には問題なく使えると思った。

個人的なAppRunnerを使う点での問題点は以下が挙げられる
- AWS WAFが使えない(Apprunnerにアタッチできない)
- IAM Policyによる設定の不許可を制御できない（パブリック公開に設定したAppRunnerを禁止するなど）
- デプロイが遅い（大体7分ぐらい）
- AWS-CDKのサポートが遅れている
- Amazon SNSやEventBridgeのイベント駆動に非対応(特定の時間になったら分析開始とかできたら嬉しい)

### 最後に

わからないところ・誤字・間違いあれば是非コメントください。。。
ソースコードにはコメント残したつもりですが、言葉足らずという指摘結構いただくので・・・
また関連記事も書く予定です