ドメイン及びIPアドレス制限WAF付きのApp RunnerをCloudFormationで実装してみた
こんにちは、つくぼし(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コード
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)でした!