こんにちは、つくぼし(tsukuboshi0755)です!
ここ最近App Runnerが徐々にアップデートされ、ECS Fargateの代替としてかなり使い勝手が良くなってきています。
今回は、ドメインとIPアドレスによるアクセス制限ルールを持つWAFをアタッチしたApp RunnerをCloudFormationで構築し、利用する方法を紹介します!
構成
今回作成する構成図は以下になります。
赤枠のWAF及びApp RunnerがCloudFormationでデプロイされるリソースになります。
WAFのルールとしては、以下の2種類を使用します。
- ドメイン制限ルール:カスタムドメインによるアクセスのみを許可し、App Runnerデフォルトドメインによるアクセスを禁止
- IPアドレス制限ルール:ホワイトリストとして特定IPアドレスからのアクセスのみ許可し、それ以外のIPアドレスからのアクセスを禁止
なおECRプライベートリポジトリとコンテナイメージ、及びRoute 53パブリックホストゾーンについては、今回のCloudFormationとは別で用意する必要があるためご注意ください。
テンプレート
全体のコードは以下の通りです。
CloudFormationコード
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Parameters:
- SysName
- Env
- ImageUri
- ServiceCPU
- ServiceMemory
- CustomDomain
- AllowAddresses
Parameters:
SysName:
Type: String
Default: 'cm'
Description: 'System name for this stack.'
Env:
Type: String
Default: 'dev'
AllowedValues:
- 'prd'
- 'stg'
- 'dev'
Description: 'Environment for this stack.'
ImageUri:
Type: String
AllowedPattern: '([0-9]{12}.dkr.ecr.[a-z\-]+-[0-9]{1}.amazonaws.com\/.*)|(^public\.ecr\.aws\/.+\/.+)'
Default: '111111111111.dkr.ecr.ap-northeast-1.amazonaws.com/cm-dev-ecr-repo:latest'
Description: 'Image uri stored in ECR.'
ServiceCPU:
Type: String
AllowedPattern: '256|512|1024|2048|4096|(0.25|0.5|1|2|4) vCPU'
AllowedValues:
- 0.25 vCPU
- 0.5 vCPU
- 1 vCPU
- 2 vCPU
- 4 vCPU
Default: '1 vCPU'
Description: 'The number of vCPUs to allocate to the container.'
ServiceMemory:
Type: String
AllowedPattern: "512|1024|2048|3072|4096|6144|8192|10240|12288|(0.5|1|2|3|4|6|8|10|12) GB"
AllowedValues:
- 0.5 GB
- 1 GB
- 2 GB
- 3 GB
- 4 GB
- 6 GB
- 8 GB
- 10 GB
- 12 GB
Default: '2 GB'
Description: 'The amount of memory (in GB) to allocate to the container.'
CustomDomain:
Type: String
Default: 'example.com'
Description: 'Custom domain to access an application.'
AllowAddresses:
Type: CommaDelimitedList
Default: "1.1.1.1/32"
Description: 'IPv4 list to allow access.'
Resources:
AccessRoleforAppRunner:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SysName}-${Env}-apprunner-access-role
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
InstanceRoleforAppRunner:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SysName}-${Env}-apprunner-instance-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: tasks.apprunner.amazonaws.com
Action: sts:AssumeRole
AppRunnerService:
Type: AWS::AppRunner::Service
Properties:
ServiceName: !Sub ${SysName}-${Env}-apprunner-srv
AutoScalingConfigurationArn: !GetAtt AppRunnerServiceASConfig.AutoScalingConfigurationArn
HealthCheckConfiguration:
Protocol: "HTTP"
Path: "/"
InstanceConfiguration:
Cpu: !Ref ServiceCPU
Memory: !Ref ServiceMemory
InstanceRoleArn: !GetAtt InstanceRoleforAppRunner.Arn
SourceConfiguration:
AuthenticationConfiguration:
AccessRoleArn: !GetAtt AccessRoleforAppRunner.Arn
AutoDeploymentsEnabled: true
ImageRepository:
ImageIdentifier: !Ref ImageUri
ImageRepositoryType: ECR
ImageConfiguration:
Port: 80
AppRunnerServiceASConfig:
Type: AWS::AppRunner::AutoScalingConfiguration
Properties:
AutoScalingConfigurationName: !Sub ${SysName}-${Env}-apprunner-as-config
MaxConcurrency: 100
MaxSize: 25
MinSize: 1
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: !Sub ${SysName}-${Env}-WebACL
Scope: REGIONAL
DefaultAction:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: !Sub ${SysName}-${Env}-WebACL
Rules:
- Name: 'allow-only-custom-domain'
Priority: 0
Action:
Block: {}
Statement:
NotStatement:
Statement:
ByteMatchStatement:
FieldToMatch:
SingleHeader:
Name: 'host'
PositionalConstraint: 'EXACTLY'
SearchString: !Ref CustomDomain
TextTransformations:
- Priority: 0
Type: 'LOWERCASE'
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: 'allow-only-custom-domain'
- Name: 'ip-addresses-white-list'
Priority: 1
Action:
Allow: {}
Statement:
IPSetReferenceStatement:
Arn: !GetAtt WAFIPSet.Arn
VisibilityConfig:
SampledRequestsEnabled: true
MetricName: 'ip-addresses-white-list'
CloudWatchMetricsEnabled: true
WAFIPSet:
Type: AWS::WAFv2::IPSet
Properties:
Name: IPAllowLists
Scope: REGIONAL
IPAddressVersion: IPV4
Addresses: !Ref AllowAddresses
WebACLAssociation:
Type: AWS::WAFv2::WebACLAssociation
Properties:
WebACLArn: !GetAtt WebACL.Arn
ResourceArn: !GetAtt AppRunnerService.ServiceArn
LogGroupforWAF:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub aws-waf-logs-${SysName}-${Env}
RetentionInDays: 365
ResourcePolicyforWAFToCW:
Type: AWS::Logs::ResourcePolicy
Properties:
PolicyName: !Sub WAFToCWLogsPolicy-${SysName}-${Env}
PolicyDocument:
Fn::Sub:
- |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "delivery.logs.amazonaws.com"
},
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "${CloudWatchLogsLogGroupArn}",
"Condition": {
"StringEquals": {
"aws:SourceAccount": ${AWS::AccountId}
},
"ArnLike": {
"aws:SourceArn": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
}
}
}
]
}
- CloudWatchLogsLogGroupArn: !GetAtt LogGroupforWAF.Arn
WAFLogConfig:
Type: AWS::WAFv2::LoggingConfiguration
Properties:
LogDestinationConfigs:
- !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${SysName}-${Env}
ResourceArn: !GetAtt WebACL.Arn
Outputs:
DefaultDomain:
Value: !GetAtt AppRunnerService.ServiceUrl
Description: 'Default Domain to access an application.'
CustomDomain:
Value: !Ref CustomDomain
Description: 'Custom Domain to access an application.'
以下より、本テンプレートで作成される各リソースについて説明します。
App Runner用サービスロール
AccessRoleforAppRunner:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SysName}-${Env}-apprunner-access-role
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
InstanceRoleforAppRunner:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${SysName}-${Env}-apprunner-instance-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: tasks.apprunner.amazonaws.com
Action: sts:AssumeRole
ECRイメージにアクセスするためのアクセスロールと、コンテナインスタンスが他のAWSサービスにアクセスするためのインスタンスロールを作成します。
以下の公式ドキュメントに基づき、必要な権限をアタッチします。
App Runner オートスケーリング設定
AppRunnerServiceASConfig:
Type: AWS::AppRunner::AutoScalingConfiguration
Properties:
AutoScalingConfigurationName: !Sub ${SysName}-${Env}-apprunner-as-config
MaxConcurrency: 100
MaxSize: 25
MinSize: 1
App Runnerで稼働するコンテナインスタンスをオートスケーリングするための設定を作成します。
MaxConcurrency
には、同時に実行できるリクエストの最大数を指定します。このリクエスト数を超えると、コンテナインスタンスがスケールアウトします。
MaxSize
には、コンテナインスタンス数の最大値を指定します。
MinSize
には、コンテナインスタンス数の最小値を指定します。
App Runnerサービス
AppRunnerService:
Type: AWS::AppRunner::Service
Properties:
ServiceName: !Sub ${SysName}-${Env}-apprunner-srv
AutoScalingConfigurationArn: !GetAtt AppRunnerServiceASConfig.AutoScalingConfigurationArn
HealthCheckConfiguration:
Protocol: "HTTP"
Path: "/"
InstanceConfiguration:
Cpu: !Ref ServiceCPU
Memory: !Ref ServiceMemory
InstanceRoleArn: !GetAtt InstanceRoleforAppRunner.Arn
SourceConfiguration:
AuthenticationConfiguration:
AccessRoleArn: !GetAtt AccessRoleforAppRunner.Arn
AutoDeploymentsEnabled: true
ImageRepository:
ImageIdentifier: !Ref ImageUri
ImageRepositoryType: ECR
ImageConfiguration:
Port: 80
アプリケーションの稼働環境となるApp Runnerサービスを作成します。
AutoScailingConfigurationArn
には、先ほど記述したオートスケーリング設定のARNを指定します。
HealthCheckConfiguration
には、コンテナインスタンスのヘルスチェック設定を指定します。プロトコルのデフォルトがTCPとなっているので、HTTPでヘルスチェックを行う場合、Protocol
にHTTP
を指定し、Path
にはヘルスチェック用のパスを指定します。
InstanceConfiguration
には、コンテナインスタンスで使用するCPU/Memory、及び先ほど記述したインスタンスロールのARNを指定します。コンテナインスタンスで選択可能なCPUとMemoryの組み合わせについては、以下の記事をご参照ください。
SourceConfiguration
には、先ほど記述したアクセスロールのARN、自動デプロイ機能の有無、及びECRリポジトリ設定を指定します。ImageIdentifier
には、ECRリポジトリに格納されているコンテナイメージのURIを指定します。
WebACL
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: !Sub ${SysName}-${Env}-WebACL
Scope: REGIONAL
DefaultAction:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: !Sub ${SysName}-${Env}-WebACL
Rules:
- Name: 'allow-only-custom-domain'
Priority: 0
Action:
Block: {}
Statement:
NotStatement:
Statement:
ByteMatchStatement:
FieldToMatch:
SingleHeader:
Name: 'host'
PositionalConstraint: 'EXACTLY'
SearchString: !Ref CustomDomain
TextTransformations:
- Priority: 0
Type: 'LOWERCASE'
VisibilityConfig:
SampledRequestsEnabled: true
CloudWatchMetricsEnabled: true
MetricName: 'allow-only-custom-domain'
- Name: 'ip-addresses-white-list'
Priority: 1
Action:
Allow: {}
Statement:
IPSetReferenceStatement:
Arn: !GetAtt WAFIPSet.Arn
VisibilityConfig:
SampledRequestsEnabled: true
MetricName: 'ip-addresses-white-list'
CloudWatchMetricsEnabled: true
WAFIPSet:
Type: AWS::WAFv2::IPSet
Properties:
Name: IPAllowLists
Scope: REGIONAL
IPAddressVersion: IPV4
Addresses: !Ref AllowAddresses
WebACLAssociation:
Type: AWS::WAFv2::WebACLAssociation
Properties:
WebACLArn: !GetAtt WebACL.Arn
ResourceArn: !GetAtt AppRunnerService.ServiceArn
WebACLを作成し、App Runnerサービスに紐づけます。
DefaultAction
には、今回ホワイトリストによるIP制限ルールを採用するため、Block
を指定しアクセスを禁止します。
Rules
には、今回WebACLに適用するドメイン制限ルール及びIPアドレス制限ルールを指定します。
allow-only-custom-domain
ルールは、Statement.NotStatement.Statement.ByteMatchStatement.SearchString
に指定したカスタムドメイン以外からのアクセスを禁止します。
ip-addresses-white-list
ルールは、Statement.IPSetReferenceStatement.Arn
に指定したWAFIPSetに含まれる、IPアドレスからのアクセスを許可します。
WebACLAssociation
には、WebACLとApp RunnerサービスのARNを指定し紐づけます。
WAF用ログ設定
LogGroupforWAF:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub aws-waf-logs-${SysName}-${Env}
RetentionInDays: 365
ResourcePolicyforWAFToCW:
Type: AWS::Logs::ResourcePolicy
Properties:
PolicyName: !Sub WAFToCWLogsPolicy-${SysName}-${Env}
PolicyDocument:
Fn::Sub:
- |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "delivery.logs.amazonaws.com"
},
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "${CloudWatchLogsLogGroupArn}",
"Condition": {
"StringEquals": {
"aws:SourceAccount": ${AWS::AccountId}
},
"ArnLike": {
"aws:SourceArn": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
}
}
}
]
}
- CloudWatchLogsLogGroupArn: !GetAtt LogGroupforWAF.Arn
WAFLogConfig:
Type: AWS::WAFv2::LoggingConfiguration
Properties:
LogDestinationConfigs:
- !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${SysName}-${Env}
ResourceArn: !GetAtt WebACL.Arn
CloudWatchロググループを作成し、WAFのログ設定の出力先として指定します。
ResourcePolicyforWAFToCW
には、WAFからCloudWatch Logsへのログ出力を許可するためのリソースポリシーを設定します。
詳細については、以下の記事をご参照ください。
稼働確認
上記のテンプレートをデプロイする事で、特定のIPアドレス/カスタムドメインからApp Runnerにアクセスできるか確認します。
ECRへのコンテナイメージプッシュ
ECRプライベートリポジトリを、以下の通り事前に作成してください。
リポジトリが作成できましたら、AWS CLI及びDockerが導入されているローカル端末で以下のコマンドを実施し、作成したリポジトリに対してコンテナイメージをプッシュしてください。
# ECRリポジトリ名の設定
ECR_REPO_NAME=cm-dev-ecr-repo
# AWS IDの設定
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
# レジストリの認証
aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
# Dockerfileの作成
cat << EOF > Dockerfile
FROM nginx:latest
EOF
# コンテナのビルド/プッシュ
docker build --platform linux/x86_64 -t ${ECR_REPO_NAME} .
docker tag ${ECR_REPO_NAME}:latest ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO_NAME}:latest
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPO_NAME}:latest
作成したECRにプッシュしたコンテナイメージのURIをメモしておきます。
CloudFormationスタックのデプロイ
今回は以下のパラメータで、CloudFormationスタックをデプロイします。
ImageUri
には、先ほどメモしたコンテナイメージのURIを指定します。
CustomDomain
には、App Runnerのカスタムドメインリンクで指定するドメインを入力してください。今回はcm.tsukuboshi.net
を指定します。
AllowAddresses
には、App Runnerへのアクセスを許可するIPアドレスを入力します。今回は検証のため、checkip.amazonaws.comで確認したローカル端末のグローバルIPアドレスを指定します。
App Runnerカスタムドメインリンク構成
残念ながら2023年12月時点では、App RunnerのカスタムドメインリンクはCloudFormationには対応していないようです。
そのため以下のブログを参考に、コンソール上でカスタムドメインリンクを構成します。
今回は事前にtsukuboshi.net
というホストゾーンを作成した上で、cm.tsukuboshi.net
というカスタムドメインを指定します。
以下の通り、作成したカスタムドメインリンクのステータスがアクティブになっていればOKです。
アクセス確認
CloudFormationに出力されたデフォルトドメイン及びカスタムドメインにアクセスし、正常にアクセス制限が設定されているか確認します。
まずカスタムドメインcm.tsukuboshi.net
にアクセスすると、以下のようにアクセスが許可される事が分かります。
続いて、カスタムドメイン以外のApp Runnerデフォルトドメインにアクセスすると、以下のようにドメイン制限ルールによりアクセスが禁止される事が分かります。
最後にプロキシ切り替え等を実施して、アクセス元のグローバルIPアドレスを変更した上でカスタムドメインにアクセスすると、以下のようにIPアドレス制限ルールによりアクセスが禁止される事が分かります。
このように、App Runnerに対して正常にアクセス制限が実施されている事が確認できました!
最後に
今回はドメイン及びIPアドレスによるアクセス制限ルールを含んだWAFをアタッチしたApp RunnerをCloudFormationで構築し、利用する方法を紹介しました。
App Runnerでアプリケーションを稼働させる際に良く見かける構成だと思いますので、ぜひお役に立てて頂けますと幸いです。
以上、つくぼし(tsukuboshi0755)でした!