CloudFormationのテンプレート
少しマニアックなAWSのCloudFormationのテンプレート3種類です。最初はよくわからずコピペで動作確認をする状態でしたが、色々とシステム構築をしていたらCloudFormationのスクリプトを自作できるようになってきました。CloudFormationは取っつきにくい仕組みですが、わかってくると簡単に安いサーバーレスなシステムが構築できるので楽しくなってきました。
なお、API GatewayとLambdaによるREST APIのシステム構築はserverless frameworkの"aws-nodejs-typescript"を利用しています。

①CloudFront + S3によるReactのWebサイト
CloudFrontとS3でReact用のWebサイトを構築するテンプレートです。
- CloudFrontとS3にReactで作成したWebサイトを公開するシステムです。
- 必ずドメイン/{パス}のURL構成になります。{パス}で指定したフォルダにReactのプロジェクトを置くことができます。このルートのマッピングは、CloudFrontのFunctionで行っています。
- ドメインのルートには、デフォルトのパスにリダイレクトするHTMLファイルを置いて利用してください。
- CloudFrontに行うカスタムドメインの名の設定とARNの設定は行っていますが、Route 53へのルートの割付は行っていないので、手動で行ってください。
このスクリプトに指定する証明書は、AWSのus-east-1リージョンで指定するカスタムドメイン名の証明書を手動で予め作成しておいてください
作成した証明書のARNは以下のコマンドで取得する事ができます。
aws acm list-certificates --region us-east-1
ProjectNamePrefixにはプロジェクトの名称、CertArnは上記のコマンドで取得したARNをDomainNameはWebサーバーを利用するドメイン名を設定してください。
AWSTemplateFormatVersion: 2010-09-09
Description: Static contents distribution using S3 and CloudFront.
Parameters:
ProjectNamePrefix:
Type: String
Default: {各リソース名のPrefix}
CertArn:
Type: String
Default: {証明書のArn}
DomainName:
Type: String
Default: {Domain名}
Resources:
# S3 bucket contains static contents
AssetsBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !Sub '${ProjectNamePrefix}-web-bucket'
# S3 bucket policy to allow access from CloudFront OAI
AssetsBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref AssetsBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Resource: !Sub arn:aws:s3:::${AssetsBucket}/*
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
# CloudFront Distribution for contents delivery
CloudFrontFunction:
Type: AWS::CloudFront::Function
Properties:
Name: !Sub '${ProjectNamePrefix}-CloudFront-Function'
FunctionConfig:
Comment: 'Setting path of React Application'
Runtime: cloudfront-js-1.0
FunctionCode: |
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/') || (!uri.includes('.'))) {
var version =request.uri.split("/")[1];
if(!version) {
request.uri = "/index.html";
} else {
request.uri = `/${version}/index.html`;
}
}
return request;
}
AutoPublish: true
AssetsDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Sub '${ProjectNamePrefix}'
Aliases:
- !Sub ${DomainName}
Origins:
- Id: S3Origin
DomainName: !GetAtt AssetsBucket.DomainName
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
ForwardedValues:
QueryString: false
ViewerProtocolPolicy: redirect-to-https
FunctionAssociations:
- EventType: "viewer-request"
FunctionARN: !GetAtt CloudFrontFunction.FunctionMetadata.FunctionARN
CustomErrorResponses:
- ErrorCode: 403
ResponseCode: 200
ResponsePagePath: '/index.html'
ErrorCachingMinTTL: 10
ViewerCertificate:
AcmCertificateArn: !Sub ${CertArn}
SslSupportMethod: sni-only
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Ref AWS::StackName
Outputs:
URL:
Value: !Join [ "", [ "https://", !GetAtt [ AssetsDistribution, DomainName ]]]
ルートのindex.html
S3のルートにはこのようにデフォルトのパスに置いておくhtmlファイルの例
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta charset="utf-8">
</head>
<script>
location.href='/v0'
</script>
</html>
②S3へのファイル書き込み => Lambdaトリガー
S3にファイルを書き込みしたらLambda関数が起動するというCloudFormationのテンプレートです。
Lambda関数にはTriggerLambdaRoleで、CloudWatchのログを有効にしています。
AWSTemplateFormatVersion: "2010-09-09"
Description: "Test resources."
Parameters:
ProjectNamePrefix:
Type: String
Default: {各リソース名のPrefix}
Stage:
Type: String
Default: {stage名}
Resources:
TriggerLambdaRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
- "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
RoleName: !Sub "${ProjectNamePrefix}-trigger-dev-role"
TriggerLambda:
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: |
exports.handler = async (event) => {
// TODO implement
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
FunctionName: !Sub "${ProjectNamePrefix}-trigger-dev-function"
Handler: "index.handler"
MemorySize: 128
Role: !GetAtt "TriggerLambdaRole.Arn"
Runtime: "nodejs14.x"
Timeout: 5
TriggerLambdaPermission:
Type: "AWS::Lambda::Permission"
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !GetAtt
- TriggerLambda
- Arn
Principal: "s3.amazonaws.com"
SourceArn: !Sub "arn:aws:s3:::${ProjectNamePrefix}-dev-bucket"
SrcS3Bucket:
Type: "AWS::S3::Bucket"
DependsOn: "TriggerLambdaPermission"
Properties:
BucketName: !Sub "${ProjectNamePrefix}-dev-bucket"
NotificationConfiguration:
LambdaConfigurations:
- Event: "s3:ObjectCreated:*"
Function: !GetAtt
- TriggerLambda
- Arn
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- "*"
AllowedMethods:
- "PUT"
- "POST"
- "DELETE"
- "GET"
AllowedOrigins:
- "*"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
③API GatewayのWebSocket + Lambda
このテンプレートの仕様は次のとおりです。
- WebSocketのイベントであるConnect, Disconnect, メッセージは全て同じLambda関数に転送しています。
- WebSocketはURLのクエリにAuthorizerを付与し認証をするAuthorizerが設定されています。(ApiGatewayV2Authorizer)
- Lambda関数にはWebSocketFunctionRoleで、CloudWatchのログを有効にしています。また、DynamoDB(2種類)と登録したWebSocketへのアクセス権を付与しています。
AWSTemplateFormatVersion: "2010-09-09"
Description: "Test resources."
Parameters:
ProjectNamePrefix:
Type: String
Default: {各リソース名のPrefix}
Stage:
Type: String
Default: {stage名}
Resources:
WebSocketApiGateway:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: !Sub "${ProjectNamePrefix}-socket-${Stage}"
ProtocolType: WEBSOCKET
RouteSelectionExpression: "$request.body.action"
ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApiGateway
RouteKey: $connect
AuthorizationType: CUSTOM
AuthorizerId: !Ref ApiGatewayV2Authorizer
OperationName: ConnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref ConnectInteg
ConnectInteg:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApiGateway
Description: Connect Integration
IntegrationType: AWS_PROXY
IntegrationUri:
Fn::Sub:
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebSocketFunction.Arn}/invocations
DisconnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApiGateway
RouteKey: $disconnect
AuthorizationType: NONE
OperationName: DisconnectRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref DisconnectInteg
DisconnectInteg:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApiGateway
Description: Disconnect Integration
IntegrationType: AWS_PROXY
IntegrationUri:
Fn::Sub:
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebSocketFunction.Arn}/invocations
DefaultRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApiGateway
RouteKey: $default
AuthorizationType: NONE
OperationName: DefaultRoute
Target: !Join
- '/'
- - 'integrations'
- !Ref SendInteg
SendInteg:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApiGateway
Description: Send Integration
IntegrationType: AWS_PROXY
IntegrationUri:
Fn::Sub:
arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebSocketFunction.Arn}/invocations
Deployment:
Type: AWS::ApiGatewayV2::Deployment
DependsOn:
- ConnectRoute
- DefaultRoute
- DisconnectRoute
Properties:
ApiId: !Ref WebSocketApiGateway
WebSocketApiGatewayStage:
Type: AWS::ApiGatewayV2::Stage
Properties:
StageName: !Ref Stage
Description: Prod Stage
DeploymentId: !Ref Deployment
ApiId: !Ref WebSocketApiGateway
ApiGatewayV2Authorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref WebSocketApiGateway
AuthorizerType: "REQUEST"
AuthorizerUri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${WebSocketFunction.Arn}/invocations"
IdentitySource:
- "route.request.querystring.Authorization"
Name: auth
WebSocketFunctionRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
- "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
RoleName: !Sub "${ProjectNamePrefix}-socket-${Stage}-role"
Policies:
- PolicyName: !Sub "${ProjectNamePrefix}-socket-${Stage}-policy"
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:DeleteItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource:
- !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ProjectNamePrefix}-prod-data"
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:DeleteItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource:
- !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ProjectNamePrefix}-prod-doc"
- Effect: Allow
Action:
- ses:SendEmail
Resource: "*"
- Effect: Allow
Action:
- 'execute-api:ManageConnections'
Resource:
- !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApiGateway}/*'
WebSocketFunction:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
exports.handler = async (event) => {
// TODO implement
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
FunctionName: !Sub "${ProjectNamePrefix}-socket-${Stage}"
Handler: "index.handler"
MemorySize: 128
Role: !GetAtt WebSocketFunctionRole.Arn
Runtime: "nodejs14.x"
Timeout: 5
WebSocketLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref WebSocketFunction
Principal: apigateway.amazonaws.com
DependsOn:
- WebSocketApiGateway
Outputs:
WebSocketFunctionArn:
Value: !GetAtt WebSocketFunction.Arn
WebSocketURI:
Description: "The WSS Protocol URI to connect to"
Value: !Join [ '', [ 'wss://', !Ref WebSocketApiGateway, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref 'Stage'] ]