VPC接続するAWS App RunnerをCloudFormationで作成する

ついにAWS App RunnerでVPCリソースにアクセスできるようになったので、CloudFormationで作ってみました。


概要

先日ようやく対応した、VPCリソースにアクセスできるAWS App Runnerを、CloudFormationで構築します。


前提

マネージドランタイム(GitHub)ではなく、ECR連携での構築を行います。

(マネージドランタイムの場合でも土台は変わりません。)


ちなみにCDKでも作っているので(こちらはマネージドランタイム版)、良ければご覧下さい。

go-to-k.hatenablog.com

go-to-k.hatenablog.com


方法

テンプレートのyamlファイルとシェルスクリプトによって構築します。


補足

別記事にまとめたのですが、App RunnerとECS Fargateの違いなどを以下に記載していますのでよかったらぜひご覧下さい。

go-to-k.hatenablog.com


また、以下のようなVPCコネクタにおけるルールやエラーもあるため、構築・変更には注意が必要です。

go-to-k.hatenablog.com


コード

GitHubにもあります。

github.com


yaml

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "ServiceName"
        Parameters:
          - ServiceName
      - Label:
          default: "VpcConnectorName"
        Parameters:
          - VpcConnectorName
      - Label:
          default: "SecurityGroupName"
        Parameters:
          - SecurityGroupName
      - Label:
          default: "SubnetID1"
        Parameters:
          - SubnetID1
      - Label:
          default: "SubnetID2"
        Parameters:
          - SubnetID2
      - Label:
          default: "VpcID"
        Parameters:
          - VpcID
      - Label:
          default: "CPU"
        Parameters:
          - CPU
      - Label:
          default: "Memory"
        Parameters:
          - Memory
      - Label:
          default: "AutoScalingConfigurationArn"
        Parameters:
          - AutoScalingConfigurationArn
      - Label:
          default: "ImageIdentifier"
        Parameters:
          - ImageIdentifier

Parameters:
  ServiceName:
    Type: String
  VpcConnectorName:
    Type: String
  SecurityGroupName:
    Type: String
  SubnetID1:
    Type: String
  SubnetID2:
    Type: String
  VpcID:
    Type: String
  CPU:
    Type: String
    AllowedPattern: "1024|2048|(1|2) vCPU"
    Default: "1 vCPU"
  Memory:
    Type: String
    AllowedPattern: "2048|3072|4096|(2|3|4) GB"
    Default: "2 GB"
  AutoScalingConfigurationArn:
    Type: String
  ImageIdentifier:
    Type: String

Resources:
  AppRunnerAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - build.apprunner.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess

  AppRunnerInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: tasks.apprunner.amazonaws.com
            Action: sts:AssumeRole

  AppRunnerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      VpcId: !Ref VpcID
      GroupName: !Ref SecurityGroupName
      GroupDescription: "for App Runner"
      Tags:
        - Key: "Name"
          Value: !Ref SecurityGroupName

  VpcConnector:
    Type: AWS::AppRunner::VpcConnector
    Properties: 
      VpcConnectorName: !Ref VpcConnectorName
      Subnets: 
        - !Ref SubnetID1
        - !Ref SubnetID2
      SecurityGroups:
        - !Ref AppRunnerSecurityGroup

  AppRunnerService:
    Type: AWS::AppRunner::Service
    Properties: 
      AutoScalingConfigurationArn: !Ref AutoScalingConfigurationArn
      # EncryptionConfiguration: # by default, App Runner uses an AWS managed key.
      HealthCheckConfiguration:
        Path: "/"
        Protocol: "HTTP"
        Interval: 5
        Timeout: 5
        HealthyThreshold: 1
        UnhealthyThreshold: 2
      InstanceConfiguration: 
        Cpu: !Ref CPU
        Memory: !Ref Memory
        InstanceRoleArn: !GetAtt AppRunnerInstanceRole.Arn
      NetworkConfiguration: 
        EgressConfiguration: 
          EgressType: VPC
          VpcConnectorArn: !GetAtt VpcConnector.VpcConnectorArn
      ServiceName: !Ref ServiceName
      SourceConfiguration: 
        AuthenticationConfiguration: 
          AccessRoleArn: !GetAtt AppRunnerAccessRole.Arn
          # ConnectionArn: # required for GitHub code repositories.
        AutoDeploymentsEnabled: true
        # CodeRepository: # required for GitHub code repositories.
        ImageRepository: 
          ImageRepositoryType: ECR
          ImageIdentifier: !Ref ImageIdentifier
          ImageConfiguration:
            Port: 80
            # RuntimeEnvironmentVariables:
            # StartCommand:



シェル

  • apprunner.sh
#/bin/bash
set -eu

StackName="test-app-runner"

TemplateFile="apprunner.yaml"

ServiceName="${StackName}"
VpcConnectorName="${StackName}"
SecurityGroupName="${StackName}"

VpcID="vpc-*****************"
SubnetID1="subnet-*****************"
SubnetID2="subnet-*****************"

ImageIdentifier="123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/test-app-runner:latest"

CPU="1 vCPU" # "1024|2048|(1|2) vCPU"
Memory="2 GB" # "2048|3072|4096|(2|3|4) GB"


MaxSize=3
MinSize=2
MaxConcurrency=100

AutoScalingConfiguration=$(cat <<EOF
{
  "AutoScalingConfigurationName": "${StackName}",
  "MinSize": ${MinSize},
  "MaxSize": ${MaxSize},
  "MaxConcurrency": ${MaxConcurrency}
}
EOF
)

AutoScalingConfigurationArn=$( \
  aws apprunner \
  create-auto-scaling-configuration \
  --cli-input-json "${AutoScalingConfiguration}" \
  | jq -r '.AutoScalingConfiguration.AutoScalingConfigurationArn' \
)


aws cloudformation deploy \
  --stack-name ${StackName} \
  --region ap-northeast-1 \
  --template-file ${TemplateFile} \
  --capabilities CAPABILITY_IAM \
  --no-fail-on-empty-changeset \
  --parameter-overrides \
    ServiceName="${ServiceName}" \
    VpcConnectorName="${VpcConnectorName}" \
    SecurityGroupName="${SecurityGroupName}" \
    VpcID="${VpcID}" \
    SubnetID1="${SubnetID1}" \
    SubnetID2="${SubnetID2}" \
    CPU="${CPU}" \
    Memory="${Memory}" \
    AutoScalingConfigurationArn="${AutoScalingConfigurationArn}" \
    ImageIdentifier="${ImageIdentifier}"



解説

yaml

AppRunnerAccessRole

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


こちらはアクセスロールと呼ばれるもので、App RunnerがECRからイメージをプルするときに使用されるIAMロールです。


PrincipalにはServiceとしてbuild.apprunner.amazonaws.comを指定します。

またManagedPolicyArnsとしてAWSAppRunnerServicePolicyForECRAccessAWSから提供されているので、こちらを指定します。

アクセスロールは、Amazon ECR パブリックを使用する場合は必須ではありません。


AppRunnerInstanceRole

  AppRunnerInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: tasks.apprunner.amazonaws.com
            Action: sts:AssumeRole


こちらはインスタンスロールというもので、App Runnerで実行されるアプリケーションがAWSサービスにアクセスするために必要な権限を指定するものです。

今回特にポリシーは付けていませんが、アプリケーションの実装に合わせて適宜必要な権限を付与してください。


大事なのが、PrincipalにはServiceとしてtasks.apprunner.amazonaws.comを指定する点です。


AppRunnerSecurityGroup

  AppRunnerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      VpcId: !Ref VpcID
      GroupName: !Ref SecurityGroupName
      GroupDescription: "for App Runner"
      Tags:
        - Key: "Name"
          Value: !Ref SecurityGroupName


これは、VpcConnectorにアタッチするセキュリティグループです。

インバウンドルールは必要ない(効かない)ので、特に何も指定せずに作成します。すると、インバウンドルールが全て拒否・アウトバンドルールが全て許可のセキュリティグループになります。

自前でセキュリティグループを作成しない場合は、VpcConnectorではデフォルトのセキュリティグループ(上記と同じルール)がアタッチされます。


VpcConnector

  VpcConnector:
    Type: AWS::AppRunner::VpcConnector
    Properties: 
      VpcConnectorName: !Ref VpcConnectorName
      Subnets: 
        - !Ref SubnetID1
        - !Ref SubnetID2
      SecurityGroups:
        - !Ref AppRunnerSecurityGroup


こちらが先日対応されたApp RunnerからVPCリソースにアクセスするためのリソース「VpcConnector」になります。


特に複雑な設定はなく、名前、サブネット、セキュリティグループを指定します。

指定したサブネットにネットワークインターフェースが作成されます。アクセスしたいリソースがあるサブネット、またはそのサブネットへの疎通が可能なサブネットを指定してください。

複数サブネットを指定した場合は、それぞれのサブネットにネットワークインターフェースが作成されます。


またセキュリティグループは、上記で少しご説明しましたが、指定しない場合はデフォルトのものが使用され、全アウトバウンドを許可するものになります。もしアクセスしたいリソースが、特定のセキュリティグループからのアクセスのみ受け付けるように環境を構築している場合などは、そのセキュリティグループをアタッチしてください。


AppRunnerService

  AppRunnerService:
    Type: AWS::AppRunner::Service
    Properties: 
      AutoScalingConfigurationArn: !Ref AutoScalingConfigurationArn
      # EncryptionConfiguration: # by default, App Runner uses an AWS managed key.
      HealthCheckConfiguration:
        Path: "/"
        Protocol: "HTTP"
        Interval: 5
        Timeout: 5
        HealthyThreshold: 1
        UnhealthyThreshold: 2
      InstanceConfiguration: 
        Cpu: !Ref CPU
        Memory: !Ref Memory
        InstanceRoleArn: !GetAtt AppRunnerInstanceRole.Arn
      NetworkConfiguration: 
        EgressConfiguration: 
          EgressType: VPC
          VpcConnectorArn: !GetAtt VpcConnector.VpcConnectorArn
      ServiceName: !Ref ServiceName
      SourceConfiguration: 
        AuthenticationConfiguration: 
          AccessRoleArn: !GetAtt AppRunnerAccessRole.Arn
          # ConnectionArn: # required for GitHub code repositories.
        AutoDeploymentsEnabled: true
        # CodeRepository: # required for GitHub code repositories.
        ImageRepository: 
          ImageRepositoryType: ECR
          ImageIdentifier: !Ref ImageIdentifier
          ImageConfiguration:
            Port: 80
            # RuntimeEnvironmentVariables:
            # StartCommand:


こちらはApp Runnerのサービス自体を定義するAWS::AppRunner::Serviceです。


AutoScalingConfigurationArnには、次項目のシェルでの説明で後述するAutoScalingConfigurationのARNを指定します。AutoScalingのスケーリングポリシーのようなものです。

      AutoScalingConfigurationArn: !Ref AutoScalingConfigurationArn


HealthCheckConfigurationはヘルスチェックの設定(値はテキトーです)、InstanceConfigurationはインスタンスのスペックとインスタンスロールの設定になります。

      HealthCheckConfiguration:
        Path: "/"
        Protocol: "HTTP"
        Interval: 5
        Timeout: 5
        HealthyThreshold: 1
        UnhealthyThreshold: 2
      InstanceConfiguration: 
        Cpu: !Ref CPU
        Memory: !Ref Memory
        InstanceRoleArn: !GetAtt AppRunnerInstanceRole.Arn


ちなみにApp Runnerで指定できるスペックは以下の組み合わせになります。ECS Fargateと比べると少し少ないです。

CPU メモリ
1 vCPU 2 GB
1 vCPU 3 GB
1 vCPU 4 GB
2 vCPU 4 GB


また、VPCへのアクセスは、以下の部分にて可能になります。

EgressType: VPCにして、先ほど作成したVpcConnectorのARNを指定します。

      NetworkConfiguration: 
        EgressConfiguration: 
          EgressType: VPC
          VpcConnectorArn: !GetAtt VpcConnector.VpcConnectorArn



最後にアプリケーションのソースに関する設定です。GitHubを使用する際には必須のパラメータなどもありますが、今回はコメントアウトしています。

      SourceConfiguration: 
        AuthenticationConfiguration: 
          AccessRoleArn: !GetAtt AppRunnerAccessRole.Arn
          # ConnectionArn: # required for GitHub code repositories.
        AutoDeploymentsEnabled: true
        # CodeRepository: # required for GitHub code repositories.
        ImageRepository: 
          ImageRepositoryType: ECR
          ImageIdentifier: !Ref ImageIdentifier
          ImageConfiguration:
            Port: 80
            # RuntimeEnvironmentVariables:
            # StartCommand:


AccessRoleArnには、先ほど定義したアクセスロールを指定します。

        AuthenticationConfiguration: 
          AccessRoleArn: !GetAtt AppRunnerAccessRole.Arn


AutoDeploymentsEnabledプッシュ時に自動デプロイを走らせるかの設定です。

        AutoDeploymentsEnabled: true


ImageRepositoryTypeでは、ソースのリポジトリGitHubにするのかECRにするのかを決めたり、ImageIdentifierにはイメージのURIを指定します。

またImageConfigurationで、コンテナ自体の開放するポートや環境変数なども設定します。(指定しなくてもOKです)

        ImageRepository: 
          ImageRepositoryType: ECR
          ImageIdentifier: !Ref ImageIdentifier
          ImageConfiguration:
            Port: 80
            # RuntimeEnvironmentVariables:
            # StartCommand:


ちなみにImageIdentifierはイメージのURIを指定すると書きましたが、具体的には以下のように「(ECRリポジトリURI):(イメージタグ)」という形になります。

AutoDeploymentsEnabledがtrueの場合、このタグのイメージがプッシュされたら自動でデプロイが走ります。

(latestタグで運用する必要があるということでしょうかね。今回は深く触れません。)

123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/test-app-runner:latest



シェル

全体的にテキトーに書いているので実際に使われる方はうまくアレンジしてください・・・!


ここで説明するのは、AutoScalingConfigurationArnです。

MaxSize=3
MinSize=2
MaxConcurrency=100

AutoScalingConfiguration=$(cat <<EOF
{
  "AutoScalingConfigurationName": "${StackName}",
  "MinSize": ${MinSize},
  "MaxSize": ${MaxSize},
  "MaxConcurrency": ${MaxConcurrency}
}
EOF
)

AutoScalingConfigurationArn=$( \
  aws apprunner \
  create-auto-scaling-configuration \
  --cli-input-json "${AutoScalingConfiguration}" \
  | jq -r '.AutoScalingConfiguration.AutoScalingConfigurationArn' \
)


AutoScalingConfiguration

AutoScalingConfigurationArnAWS::AppRunner::Serviceで指定するのですが、その名の通りAutoScalingConfigurationのARNになります。


この指定をわざわざCLIでの実行を通してyamlテンプレート側に渡しているのは、2022年2月14日現在、AutoScalingConfigurationの作成がCloudFormationに対応していないからです。


そのため、CLIでAutoScalingConfigurationを作成し、レスポンスからjqコマンドを通してARNを抽出しています。

JSONで記述するのですが、上記のようにヒアドキュメントでやるのも良し、JSONファイルに格納してファイルとして読ませるのも良しです。



そもそもAutoScalingConfigurationとは、App RunnerでデプロイするアプリケーションのAutoScalingのための設定です。

以下4つが主な設定項目になります。

項目 内容 デフォルト値
AutoScalingConfigurationName 設定名 -
MinSize 最低台数 1
MaxSize 最大台数 25
MaxConcurrency 最大同時リクエスト数
これを超えたときにスケールアウトする
100


最後に

ようやくApp Runnerで既存のVPCリソースへのアクセスが可能になったため、とりあえずCloudFormationで構築してみました。

App Runner自体の良し悪し・気になる点もあるので今度まとめてみたいなと思っています。