CloudFormationでSPA用のWAF+CloudFront+S3構成を一発で構築
目次
はじめに
こんにちは。スカイアーチHRソリューションズのさとゆうです。
今回はSPAでよく使われるAWSの構成をCloudFormationで作成していきます。
きっかけ
よく見られる構成図ではありますが、今回改めて記事にしようと思ったきっかけは以下になります。
- WAFとCloudFrontの連携の際、AWS WAFをバージニア北部リージョンに作る必要がある。
2. 1の制約によりテンプレートを東京/バージニア北部リージョンそれぞれに用意する必要がある。
これらが理由でテンプレート1つで一気にリソースが作れないことにもどかしさを感じていたためです。
リソースを一気に作ろうとして怒られた方も多いようです。自分もそのうちの一人です笑
どうにかしてリソースを一気に作ることができないか考えていたところ、CloudFormationのスタックセット機能が使えそうでしたのでこちらの方法を紹介します。
スタックセットとは?
スタックセットは複数のAWSアカウントやAWSリージョンにまたがるスタックを一元的に管理するための機能です。1つのCloudFormationテンプレートを使用し、複数のAWSアカウントやAWSリージョンに対して一括でスタックを作成、更新、削除することができます。
またAWS Organizationsのマルチアカウント機能と連携して利用することができ、手軽かつ効率的なAWSインフラストラクチャの管理ができます。
構築手順
前準備
CloudFormationでスタックセットを扱うにあたって名前が固定のIAMロールを2つ作成する必要がありますので準備してください。すでに作成済みかつ必要な権限を設定済みの場合、前準備のステップはスキップしてください。
必要なIAMロール
- AWSCloudFormationStackSetAdministrationRole
- AWSCloudFormationStackSetExecutionRole
以下参考テンプレートになりますので、必要に応じて作成してください。
AWSTemplateFormatVersion: 2010-09-09
Description: |
iam setting for cloudformation stacksets
# ------------------------------------------------------------#
# Parameter
# 依存循環を回避するため、入力パラメータで値を管理しています。
# ------------------------------------------------------------#
Parameters:
AdminAccountRoleName:
Type: String
Description: fixed input
Default: AWSCloudFormationStackSetAdministrationRole
TargetAccountRoleName:
Type: String
Description: fixed input
Default: AWSCloudFormationStackSetExecutionRole
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
StackSetAdministrationRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref AdminAccountRoleName
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action:
- sts:AssumeRole
Path: /
Policies:
- PolicyName: AssumeRole-Policy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Resource:
- !Sub arn:aws:iam::${AWS::AccountId}:role/${TargetAccountRoleName}
StackSetExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref TargetAccountRoleName
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !GetAtt StackSetAdministrationRole.Arn
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/PowerUserAccess
- !Sub arn:${AWS::Partition}:iam::aws:policy/IAMReadOnlyAccess
Policies:
- PolicyName: PassRole-Policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: "*"
Resource: "*"
Outputs:
StackSetAdministrationRoleOutput:
Value: !Ref StackSetAdministrationRole
Export:
Name: StackSetAdministrationRole-output
StackSetAdministrationRoleArnOutput:
Value: !GetAtt StackSetAdministrationRole.Arn
Export:
Name: StackSetAdministrationRole-arn-output
StackSetExecutionRoleOutput:
Value: !Ref StackSetExecutionRole
Export:
Name: StackSetExecutionRole-output
StackSetExecutionRoleArnOutput:
Value: !GetAtt StackSetExecutionRole.Arn
Export:
Name: StackSetExecutionRole-arn-output
今回はStackSetExecutionRoleにPowerUserAccessとIAMReadOnlyAccessのポリシーを設定していますが、権限不足の問題が生じた際はAdministratorAccessの権限に変更してみてください。
用意したテンプレート
今回リソースを一気に作るために用意したテンプレートが以下になります。
バージニア北部で利用するテンプレート
# Create from us-east-1
AWSTemplateFormatVersion: 2010-09-09
# ------------------------------------------------------------#
# Label
# ------------------------------------------------------------#
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Common param"
Parameters:
- StackSetAdministrationRoleArn
- StackSetExecutionRole
- StackSetTemplateUrl
-
Label:
default: "CloudFront param"
Parameters:
- CachePolicyName
- DefaultRootObject
# ------------------------------------------------------------#
# Parameter
# ------------------------------------------------------------#
Parameters:
StackSetAdministrationRoleArn:
Type: String
Description: input from StackSetAdministrationRoleArnOutput
StackSetExecutionRole:
Type: String
Description: input from StackSetExecutionRoleOutput
StackSetTemplateUrl:
Type: String
Description: input stackset template url
CachePolicyName:
Type: String
Default: Managed-CachingDisabled
Description: (Required) Name of Managed Cache Policy
AllowedValues:
- Managed-CachingDisabled
- Managed-CachingOptimized
- Managed-CachingOptimizedForUncompressedObjects
DefaultRootObject:
Type: String
Default: index.html
Description: (Required) Default Root Object of CloudFront Distribution
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# ---------------------------------#
# WAF
# ---------------------------------#
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
DefaultAction:
Allow: {}
Name: !Sub ${AWS::StackName}-cloudfront-webacl
Scope: CLOUDFRONT
Rules:
-
Name: AWS-AWSManagedRulesCommonRuleSet
Priority: 1
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesCommonRuleSet
OverrideAction:
None: {} # or Count for monitoring
VisibilityConfig:
CloudWatchMetricsEnabled: true
SampledRequestsEnabled: false # or true for monitoring
MetricName: AWS-AWSManagedRulesCommonRuleSet
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: !Sub ${AWS::StackName}-cloudfront-waf-metric
SampledRequestsEnabled: true
# ------------------------------------------------------------#
# StackSets:ap-northeast-1
# ------------------------------------------------------------#
StacksetForTokyo:
Type: AWS::CloudFormation::StackSet
Properties:
StackSetName: !Sub ${AWS::StackName}-ap-northeast-1-stackset
AdministrationRoleARN: !Ref StackSetAdministrationRoleArn
Capabilities:
- CAPABILITY_IAM
- CAPABILITY_NAMED_IAM
ExecutionRoleName: !Ref StackSetExecutionRole
OperationPreferences:
FailureTolerancePercentage: 0
MaxConcurrentPercentage: 100
RegionConcurrencyType: PARALLEL
Parameters:
- ParameterKey: CachePolicyName
ParameterValue: !Ref CachePolicyName
- ParameterKey: DefaultRootObject
ParameterValue: !Ref DefaultRootObject
- ParameterKey: WebACLArn
ParameterValue: !GetAtt WebACL.Arn
PermissionModel: SELF_MANAGED
StackInstancesGroup:
- DeploymentTargets:
Accounts:
- !Ref AWS::AccountId
Regions:
- ap-northeast-1
TemplateURL: !Ref StackSetTemplateUrl
東京リージョンで利用するテンプレート
AWSTemplateFormatVersion: 2010-09-09
# ------------------------------------------------------------#
# Mapping
# ------------------------------------------------------------#
Mappings:
CachePolicyMap:
Managed-CachingOptimized:
Id: 658327ea-f89d-4fab-a63d-7e88639e58f6
Managed-CachingDisabled:
Id: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
Managed-CachingOptimizedForUncompressedObjects:
Id: b2884449-e4de-46a7-ac36-70bc7f1ddd6d
# ------------------------------------------------------------#
# Parameter
# ------------------------------------------------------------#
Parameters:
CachePolicyName:
Type: String
DefaultRootObject:
Type: String
WebACLArn:
Type: String
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# ---------------------------------#
# S3:Contents Bucket
# ---------------------------------#
ContentsBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Enabled
# ---------------------------------#
# S3:Contents Bucket Policy
# ---------------------------------#
ContentsBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ContentsBucket
PolicyDocument:
Id: PolicyForCloudFrontPrivateContent
Version: 2012-10-17
Statement:
- Action: s3:GetObject
Effect: Allow
Principal:
Service:
- cloudfront.amazonaws.com
Resource: !Sub ${ContentsBucket.Arn}/*
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${Cloudfront}
# ---------------------------------#
# CloudFront
# ---------------------------------#
Cloudfront:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Sub create from ${AWS::StackName}
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
CachePolicyId: !FindInMap [ CachePolicyMap, !Ref CachePolicyName, Id ]
TargetOriginId: S3
ViewerProtocolPolicy: redirect-to-https
DefaultRootObject: !Ref DefaultRootObject
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !GetAtt ContentsBucket.RegionalDomainName
Id: S3
OriginAccessControlId: !GetAtt OAC.Id
S3OriginConfig:
OriginAccessIdentity: ''
PriceClass: PriceClass_200
WebACLId: !Ref WebACLArn
# ---------------------------------#
# OAC
# ---------------------------------#
OAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Description: Access Control
Name: Sample-OAC
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
テンプレートは2つありますが、手動によるスタック作成は一度だけになります。
テンプレートをS3へ配置
任意リージョンのS3バケットに以下の構成でテンプレート配置しましょう。
S3バケット
│ template.yml
└─ap-northeast-1
stackset-template.yml
スタックを作成
スタック作成でS3に配置したtemplate.ymlのオブジェクトurlを指定し、入力パラメータを入力する。
スタック作成はバージニア北部から実施してください。
パラメータに入力する値は以下の通り。
- StackSetAdministrationRoleArn:前準備で作成したStackSetAdministrationRoleのARNを入力
- 例)arn:aws:iam::<アカウントID>:role/AWSCloudFormationStackSetAdministrationRole
- StackSetExecutionRole:前準備で作成したStackSetExecutionRoleのロール名を入力
- 例)AWSCloudFormationStackSetExecutionRole
- StackSetTemplateUrl:S3へ配置したstackset.ymlのオブジェクトurlを入力
- 例)https://<バケット名>.s3.<リージョン>.amazonaws.com/ap-northeast-1/stackset-template.yml
- CachePolicyName:必要なCloudFrontキャッシュポリシーを選択
- Managed-CachingDisabled (キャッシュ無効のポリシー)
- DefaultRootObject:アプリケーションのルートオブジェクトを指定
- 例)index.html
ステップ3、4は初期値のままでスタック作成まで実施します。
スタックステータス確認
スタック作成後、それぞれステータスが完了しているか確認する。
バージニア北部
東京リージョン
スタックセット機能によって作成されます。
配信用S3バケットにSPAリソースを配置
インフラのリソースはすべて作成されたので配信用S3バケットにSPAリソースを配置します。
今回はVueのチュートリアルアプリを配置しました。
具体的には以下クイックスタートの「npm run build」で作成した ./dist 配下のファイル群を配置します。
アプリケーションの動作確認
CloudFrontで生成されたドメインにアクセスし、アプリケーションが動作しているか確認します。
Vueのサンプル画面が表示されれば完了です!
おわりに
CloudFormationのスタックセット機能を使うことでリージョンまたぎに関する制約をクリアし、一発でWAF+CloudFront+S3を構築することができました。
気になる点としてはスタック作成は値渡しが一方通行である関係で、バージニア北部から実施することが必須であることでしょうか。
バージニア北部⇒東京リージョンへの値渡しは今回のテンプレートで行えますが、逆パターンの東京⇒バージニア北部の場合はCloudFormationのカスタムリソース(Lambda)など駆使する必要があります。
今回はRoute53等のドメイン設定はしていませんが、今回のテンプレートを応用して設定いただければと思います。