AWS Amplify CLI / SDK + AWS FargateでNext.jsのSSRに対応する
dev.toのポストで、Amplifyを使ってNext.jsをSSRという記事があがっていました。ざっと読んだところ、「Amplify CLIでAWS Fargateに対してデプロイするようにすればいける」とのことでした […]
目次
dev.toのポストで、Amplifyを使ってNext.jsをSSRという記事があがっていました。ざっと読んだところ、「Amplify CLIでAWS Fargateに対してデプロイするようにすればいける」とのことでしたので、試してみました。
プロジェクトのセットアップ
新規にプロジェクトをセットアップします。
Next.js
まずはNext.jsのアプリをセットアップします。--example
でTypeScript対応版を指定しているのは個人的な好みですので、興味がない方は--example
以降を省いてください。
$ npx create-next-app YOUR_APP_NAME --example "https://github.com/wpkyoto/nextjs-starter-typescript/tree/main"
$ cd YOUR_APP_NAME
AWS Amplify
まずはAmplify CLIでプロジェクトのセットアップを行います。対話形式で進むので、聞かれた通りに答えればいいのですが、今回の方法でNext.jsを使う場合は2箇所デフォルトから変更する必要があります。
- Source Directory Path:
.
- Distribution Directory Path(ビルドディレクトリ):
.next
% npx @aws-amplify/cli init
Initializing new Amplify CLI version…
Done initializing new version.
Scanning for plugins…
Plugin scan successful
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project nextjsamplify
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: .
? Distribution Directory Path: .next
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use lab
続いてamplify configure project
でコンテナベースのデプロイをオンにします。
% npx @aws-amplify/cli configure project
Initializing new Amplify CLI version…
Done initializing new version.
Scanning for plugins…
Plugin scan successful
? Enter a name for the project nextjsamplify
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: .
? Distribution Directory Path: .next
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
## ここで「Yes」と答える
? Do you want to enable container-based deployments? Yes
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
For the awscloudformation provider.
? Do you want to update or remove the project level AWS profile? No
Successfully made configuration changes to your project.
Gitのdiffを見たところ、変更箇所はとても少ないので、手動でも良いかもしれません。
--- a/amplify/.config/project-config.json
+++ b/amplify/.config/project-config.json
@@ -8,7 +8,8 @@
"SourceDir": ".",
"DistributionDir": ".next",
"BuildCommand": "npm run-script build",
"StartCommand": "npm run-script start"
"StartCommand": "npm run-script start",
"ServerlessContainers": true
}
},
Amplify CLIでAWS Fargateを指定してホスティングを追加する
続いてNext.jsを動かすための環境を追加します。amplify add hosting
を実行すると、ホスティング環境を3種類から選ぶことができます。
% amplify add hosting
? Select the plugin module to execute (Use arrow keys)
❯ Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
Amazon CloudFront and S3
Container-based hosting with AWS Fargate
ここで「Container-based hosting with AWS Fargate」を指定すると、amplify publish
などでのデプロイがAmplify ConsoleではなくAWS Fargateに対して行われます。
独自ドメインが必須なので要注意
なお、AWS Fargateを指定した場合、ドメインを入力する必要があります。
一度空欄にすればスキップできないかと試したのですが、domain is required
と言われました。
? Select the plugin module to execute Container-based hosting with AWS Fargate
? Provide your web app endpoint (e.g. app.example.com or www.example.com):
A registered domain is required.
You can register a domain using Route 53: aws.amazon.com/route53/ or use an existing domain.
CloudFormationをあとで確認したところ、このドメインは以下の用途で利用されます。
- CloudFrontのAlias
- ALBのListener Rule
- ACMの証明書取得
ACMの証明書取得がメール認証に設定されていますので、whoisなどを確認して確認メールを受けとれるドメインを選びましょう。
? Provide your web app endpoint (e.g. app.example.com or www.example.com): fargate-example.example.com
? Do you want to automatically protect your web app using Amazon Cognito Hosted UI No
あと今回はCognitoのUIもいらないのでNoで省きます。
Dockerfileを編集してNext.jsを実行させる
Dockerfileを以下のように変更して、Next.jsを実行するようにします。
FROM public.ecr.aws/bitnami/node:14.15.1-debian-10-r8
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn build
EXPOSE 3000
CMD ["yarn", "start"]
AWSにデプロイ
あとはamplify publish
でデプロイするだけです。
CloudFrontのセットアップなどもありますのでかなり時間がかかります。ただし、ACMの証明書チェックがステップに含まれていますので、放置しすぎるとそこで Waitしたままになる罠があるので要注意です。
ACMのドメイン確認メールを確認してApproveすると、後続のデプロイが続行されてデプロイが完了します。
なお、Amplify Console方面にデプロイしていないため、管理画面側ではほぼいない子扱いされています。
「Amplify CLIでの管理」や「Amplify SDKへのインテグレーション」などは可能ですが、「Amplifyコンソールの機能(ドメイン・リダイレクト・ビルド設定など)」は使えないと思った方が良いでしょう。
試してみての感想
主観としては、「Next.jsアプリをAWS Fargateにデプロイして公開するための一式をAmplify CLIで揃えることができる」以上でも以下でもないかなというところです。
デプロイしたアプリに問題が起きた場合のログ確認やデバッグなどはFargateで管理するアプリと同等ですし、細かい設定についてはAmplify CLIが作ったCloudFormationをカスタムすることになります。そうなるとJSONではなくYAML、できればAWS CDKで管理したいという気持ちになるかなぁという印象もうけました。
CodePipelineを使ったビルドパイプラインまで整備してくれるなど、「作られる構成自体」は悪くないなと思います。なので、もしFargateで何かアプリを公開・運用しようと思った場合は、Amplify CLIが作るテンプレートをAWS CDK (TypeScript)でリバースエンジニアリングしてやろいかなと思います。
AWSやコンテナに自信がないという方は、大人しくVercelにデプロイするか、NetlifyやServerless Frameworkのプラグインを使いましょう。
参考
https://dev.to/dabit3/serverless-containers-with-next-js-aws-fargate-and-aws-amplify-17fe
Appendix1: amplify add hosting
でAWS Fargateを生成した際に作られたCloudFormationテンプレート
むっちゃ長いです。が、要はAWS Fargate + CloudFront + ACM + Route53 + 関連リソースを立ち上げるテンプレートです。
$ cat amplify/backend/backend-config.json
{
"Parameters": {
"env": {
"Type": "String"
},
"ParamZipPath": {
"Type": "String",
"Default": "site.zip"
},
"NetworkStackClusterName": {
"Type": "String"
},
"NetworkStackVpcId": {
"Type": "String"
},
"NetworkStackVpcCidrBlock": {
"Type": "String"
},
"NetworkStackSubnetIds": {
"Type": "CommaDelimitedList"
},
"NetworkStackVpcLinkId": {
"Type": "String"
},
"NetworkStackCloudMapNamespaceId": {
"Type": "String"
}
},
"Conditions": {
"isAuthCondition": {
"Fn::And": [
{
"Fn::Equals": [
false,
true
]
},
{
"Fn::Not": [
{
"Fn::Equals": [
"",
""
]
}
]
},
{
"Fn::Not": [
{
"Fn::Equals": [
"",
""
]
}
]
}
]
}
},
"Resources": {
"TaskDefinitionTaskRoleFD40A61D": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"TaskDefinition": {
"Type": "AWS::ECS::TaskDefinition",
"Properties": {
"ContainerDefinitions": [
{
"Command": [],
"EntryPoint": [],
"Environment": [],
"Essential": true,
"Image": {
"Fn::Join": [
"",
[
{
"Fn::Select": [
4,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"apiRepository",
"Arn"
]
}
]
}
]
},
".dkr.ecr.",
{
"Fn::Select": [
3,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"apiRepository",
"Arn"
]
}
]
}
]
},
".",
{
"Ref": "AWS::URLSuffix"
},
"/",
{
"Ref": "apiRepository"
},
":latest"
]
]
},
"LogConfiguration": {
"LogDriver": "awslogs",
"Options": {
"awslogs-group": {
"Ref": "apiContainerLogGroupA9A8D168"
},
"awslogs-stream-prefix": "ecs",
"awslogs-region": {
"Ref": "AWS::Region"
}
}
},
"Name": "api",
"PortMappings": [
{
"ContainerPort": 80,
"Protocol": "tcp"
}
],
"Secrets": []
}
],
"Cpu": "512",
"ExecutionRoleArn": {
"Fn::GetAtt": [
"TaskDefinitionExecutionRole8D61C2FB",
"Arn"
]
},
"Family": "amplify-nextjsamplify-dev-190351-site",
"Memory": "1024",
"NetworkMode": "awsvpc",
"RequiresCompatibilities": [
"FARGATE"
],
"TaskRoleArn": {
"Fn::GetAtt": [
"TaskDefinitionTaskRoleFD40A61D",
"Arn"
]
}
}
},
"TaskDefinitionExecutionRole8D61C2FB": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"TaskDefinitionExecutionRoleDefaultPolicy1F3406F5": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"apiRepository",
"Arn"
]
}
},
{
"Action": "ecr:GetAuthorizationToken",
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":logs:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":log-group:",
{
"Ref": "apiContainerLogGroupA9A8D168"
},
":*"
]
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "TaskDefinitionExecutionRoleDefaultPolicy1F3406F5",
"Roles": [
{
"Ref": "TaskDefinitionExecutionRole8D61C2FB"
}
]
}
},
"apiContainerLogGroupA9A8D168": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"LogGroupName": "/ecs/amplify-nextjsamplify-dev-190351-site-api",
"RetentionInDays": 30
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"apiRepository": {
"Type": "AWS::ECR::Repository",
"Properties": {
"LifecyclePolicy": {
"LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":10,\"selection\":{\"tagStatus\":\"tagged\",\"tagPrefixList\":[\"latest\"],\"countType\":\"imageCountMoreThan\",\"countNumber\":1},\"action\":{\"type\":\"expire\"}},{\"rulePriority\":100,\"selection\":{\"tagStatus\":\"any\",\"countType\":\"sinceImagePushed\",\"countNumber\":7,\"countUnit\":\"days\"},\"action\":{\"type\":\"expire\"}}]}"
},
"RepositoryName": "amplify-nextjsamplify-dev-190351-hosting-site-api"
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain"
},
"ServiceSG": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Service SecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"SecurityGroupIngress": [
{
"CidrIp": {
"Ref": "NetworkStackVpcCidrBlock"
},
"FromPort": 80,
"IpProtocol": "tcp",
"ToPort": 80
},
{
"FromPort": 80,
"IpProtocol": "tcp",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"AlbSecurityGroup",
"GroupId"
]
},
"ToPort": 80
}
],
"VpcId": {
"Ref": "NetworkStackVpcId"
}
}
},
"Service": {
"Type": "AWS::ECS::Service",
"Properties": {
"Cluster": {
"Ref": "NetworkStackClusterName"
},
"DesiredCount": 0,
"LaunchType": "FARGATE",
"LoadBalancers": [
{
"ContainerName": "api",
"ContainerPort": 80,
"TargetGroupArn": {
"Ref": "TargetGroup"
}
}
],
"NetworkConfiguration": {
"AwsvpcConfiguration": {
"AssignPublicIp": "ENABLED",
"SecurityGroups": [
{
"Fn::GetAtt": [
"ServiceSG",
"GroupId"
]
}
],
"Subnets": {
"Ref": "NetworkStackSubnetIds"
}
}
},
"ServiceName": "site-service-api-80",
"TaskDefinition": {
"Ref": "TaskDefinition"
}
},
"DependsOn": [
"AlbListener",
"AlbListenerRule"
]
},
"ApiPipelineCodeBuildProjectRoleCFB98631": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "codebuild.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
}
},
"ApiPipelineCodeBuildProjectRoleDefaultPolicyAF1730E7": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":logs:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":log-group:/aws/codebuild/",
{
"Ref": "ApiPipelineCodeBuildProject117EE1BE"
}
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":logs:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":log-group:/aws/codebuild/",
{
"Ref": "ApiPipelineCodeBuildProject117EE1BE"
},
":*"
]
]
}
]
},
{
"Action": [
"codebuild:CreateReportGroup",
"codebuild:CreateReport",
"codebuild:UpdateReport",
"codebuild:BatchPutTestCases",
"codebuild:BatchPutCodeCoverages"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":codebuild:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":report-group/",
{
"Ref": "ApiPipelineCodeBuildProject117EE1BE"
},
"-*"
]
]
}
},
{
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchGetImage",
"ecr:BatchGetDownloadUrlForLayer",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*",
"s3:PutObject*",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment/*"
]
]
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelineCodeBuildProjectRoleDefaultPolicyAF1730E7",
"Roles": [
{
"Ref": "ApiPipelineCodeBuildProjectRoleCFB98631"
}
]
}
},
"ApiPipelineCodeBuildProject117EE1BE": {
"Type": "AWS::CodeBuild::Project",
"Properties": {
"Artifacts": {
"Type": "CODEPIPELINE"
},
"Environment": {
"ComputeType": "BUILD_GENERAL1_SMALL",
"Image": "aws/codebuild/standard:4.0",
"ImagePullCredentialsType": "CODEBUILD",
"PrivilegedMode": true,
"Type": "LINUX_CONTAINER"
},
"ServiceRole": {
"Fn::GetAtt": [
"ApiPipelineCodeBuildProjectRoleCFB98631",
"Arn"
]
},
"Source": {
"Type": "CODEPIPELINE"
},
"EncryptionKey": "alias/aws/s3"
}
},
"PreDeployLambdaServiceRole87EE44C9": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"PreDeployLambdaServiceRoleDefaultPolicyD9F42A87": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "ecs:UpdateService",
"Effect": "Allow",
"Resource": {
"Ref": "Service"
}
},
{
"Action": [
"codepipeline:PutJobSuccessResult",
"codepipeline:PutJobFailureResult"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": "PreDeployLambdaServiceRoleDefaultPolicyD9F42A87",
"Roles": [
{
"Ref": "PreDeployLambdaServiceRole87EE44C9"
}
]
}
},
"PreDeployLambdaF7CAA99F": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "const AWS = require('aws-sdk');\n\nconst codepipeline = new AWS.CodePipeline();\nconst ecs = new AWS.ECS();\n\nconst { DESIRED_COUNT: desiredCountStr, CLUSTER_NAME: cluster, SERVICE_NAME: service } = process.env;\n\nconst desiredCount = parseInt(desiredCountStr, 10);\n\nexports.handler = async function({ 'CodePipeline.job': { id: jobId } }) {\n await ecs\n .updateService({\n service,\n cluster,\n desiredCount,\n })\n .promise();\n\n return await codepipeline.putJobSuccessResult({ jobId }).promise();\n};\n"
},
"Handler": "index.handler",
"Role": {
"Fn::GetAtt": [
"PreDeployLambdaServiceRole87EE44C9",
"Arn"
]
},
"Runtime": "nodejs12.x",
"Environment": {
"Variables": {
"DESIRED_COUNT": "1",
"CLUSTER_NAME": {
"Ref": "NetworkStackClusterName"
},
"SERVICE_NAME": "site-service-api-80"
}
},
"Timeout": 15
},
"DependsOn": [
"PreDeployLambdaServiceRoleDefaultPolicyD9F42A87",
"PreDeployLambdaServiceRole87EE44C9"
]
},
"ApiPipelinePipelineRole8C805448": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "codepipeline.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineRoleDefaultPolicyB3AD67CF": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*",
"s3:PutObject*",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment/*"
]
]
}
]
},
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ApiPipelinePipelineSourceCodePipelineActionRole59037A7A",
"Arn"
]
}
},
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ApiPipelinePipelineBuildCodePipelineActionRole04E6F13B",
"Arn"
]
}
},
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ApiPipelinePipelinePredeployCodePipelineActionRole387C06F4",
"Arn"
]
}
},
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ApiPipelinePipelineDeployCodePipelineActionRoleEA4B81BD",
"Arn"
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelinePipelineRoleDefaultPolicyB3AD67CF",
"Roles": [
{
"Ref": "ApiPipelinePipelineRole8C805448"
}
]
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipeline68596884": {
"Type": "AWS::CodePipeline::Pipeline",
"Properties": {
"RoleArn": {
"Fn::GetAtt": [
"ApiPipelinePipelineRole8C805448",
"Arn"
]
},
"Stages": [
{
"Actions": [
{
"ActionTypeId": {
"Category": "Source",
"Owner": "AWS",
"Provider": "S3",
"Version": "1"
},
"Configuration": {
"S3Bucket": "amplify-nextjsamplify-dev-190351-deployment",
"S3ObjectKey": {
"Ref": "ParamZipPath"
}
},
"Name": "Source",
"OutputArtifacts": [
{
"Name": "SourceArtifact"
}
],
"RoleArn": {
"Fn::GetAtt": [
"ApiPipelinePipelineSourceCodePipelineActionRole59037A7A",
"Arn"
]
},
"RunOrder": 1
}
],
"Name": "Source"
},
{
"Actions": [
{
"ActionTypeId": {
"Category": "Build",
"Owner": "AWS",
"Provider": "CodeBuild",
"Version": "1"
},
"Configuration": {
"ProjectName": {
"Ref": "ApiPipelineCodeBuildProject117EE1BE"
},
"EnvironmentVariables": {
"Fn::Join": [
"",
[
"[{\"name\":\"AWS_ACCOUNT_ID\",\"type\":\"PLAINTEXT\",\"value\":\"",
{
"Ref": "AWS::AccountId"
},
"\"},{\"name\":\"api_REPOSITORY_URI\",\"type\":\"PLAINTEXT\",\"value\":\"",
{
"Fn::Select": [
4,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"apiRepository",
"Arn"
]
}
]
}
]
},
".dkr.ecr.",
{
"Fn::Select": [
3,
{
"Fn::Split": [
":",
{
"Fn::GetAtt": [
"apiRepository",
"Arn"
]
}
]
}
]
},
".",
{
"Ref": "AWS::URLSuffix"
},
"/",
{
"Ref": "apiRepository"
},
"\"}]"
]
]
}
},
"InputArtifacts": [
{
"Name": "SourceArtifact"
}
],
"Name": "Build",
"OutputArtifacts": [
{
"Name": "BuildArtifact"
}
],
"RoleArn": {
"Fn::GetAtt": [
"ApiPipelinePipelineBuildCodePipelineActionRole04E6F13B",
"Arn"
]
},
"RunOrder": 1
}
],
"Name": "Build"
},
{
"Actions": [
{
"ActionTypeId": {
"Category": "Invoke",
"Owner": "AWS",
"Provider": "Lambda",
"Version": "1"
},
"Configuration": {
"FunctionName": {
"Ref": "PreDeployLambdaF7CAA99F"
}
},
"Name": "Predeploy",
"RoleArn": {
"Fn::GetAtt": [
"ApiPipelinePipelinePredeployCodePipelineActionRole387C06F4",
"Arn"
]
},
"RunOrder": 1
}
],
"Name": "Predeploy"
},
{
"Actions": [
{
"ActionTypeId": {
"Category": "Deploy",
"Owner": "AWS",
"Provider": "ECS",
"Version": "1"
},
"Configuration": {
"ClusterName": {
"Ref": "NetworkStackClusterName"
},
"ServiceName": "site-service-api-80"
},
"InputArtifacts": [
{
"Name": "BuildArtifact"
}
],
"Name": "Deploy",
"RoleArn": {
"Fn::GetAtt": [
"ApiPipelinePipelineDeployCodePipelineActionRoleEA4B81BD",
"Arn"
]
},
"RunOrder": 1
}
],
"Name": "Deploy"
}
],
"ArtifactStore": {
"Location": "amplify-nextjsamplify-dev-190351-deployment",
"Type": "S3"
},
"Name": "amplify-nextjsamplify-dev-190351-site-service-api-80"
},
"DependsOn": [
"ApiPipelinePipelineRoleDefaultPolicyB3AD67CF",
"ApiPipelinePipelineRole8C805448",
"Service"
]
},
"ApiPipelinePipelineSourceCodePipelineActionRole59037A7A": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineSourceCodePipelineActionRoleDefaultPolicyE5B48B86": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment/*"
]
]
}
]
},
{
"Action": [
"s3:DeleteObject*",
"s3:PutObject*",
"s3:Abort*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment/*"
]
]
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelinePipelineSourceCodePipelineActionRoleDefaultPolicyE5B48B86",
"Roles": [
{
"Ref": "ApiPipelinePipelineSourceCodePipelineActionRole59037A7A"
}
]
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineBuildCodePipelineActionRole04E6F13B": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineBuildCodePipelineActionRoleDefaultPolicy821C7A3A": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codebuild:StopBuild"
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"ApiPipelineCodeBuildProject117EE1BE",
"Arn"
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelinePipelineBuildCodePipelineActionRoleDefaultPolicy821C7A3A",
"Roles": [
{
"Ref": "ApiPipelinePipelineBuildCodePipelineActionRole04E6F13B"
}
]
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelinePredeployCodePipelineActionRole387C06F4": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelinePredeployCodePipelineActionRoleDefaultPolicy715F96BC": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "lambda:ListFunctions",
"Effect": "Allow",
"Resource": "*"
},
{
"Action": "lambda:InvokeFunction",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"PreDeployLambdaF7CAA99F",
"Arn"
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelinePipelinePredeployCodePipelineActionRoleDefaultPolicy715F96BC",
"Roles": [
{
"Ref": "ApiPipelinePipelinePredeployCodePipelineActionRole387C06F4"
}
]
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineDeployCodePipelineActionRoleEA4B81BD": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
},
"DependsOn": [
"Service"
]
},
"ApiPipelinePipelineDeployCodePipelineActionRoleDefaultPolicyB03FE2A4": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListTasks",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Action": "iam:PassRole",
"Condition": {
"StringEqualsIfExists": {
"iam:PassedToService": [
"ec2.amazonaws.com",
"ecs-tasks.amazonaws.com"
]
}
},
"Effect": "Allow",
"Resource": "*"
},
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":s3:::amplify-nextjsamplify-dev-190351-deployment/*"
]
]
}
]
}
],
"Version": "2012-10-17"
},
"PolicyName": "ApiPipelinePipelineDeployCodePipelineActionRoleDefaultPolicyB03FE2A4",
"Roles": [
{
"Ref": "ApiPipelinePipelineDeployCodePipelineActionRoleEA4B81BD"
}
]
},
"DependsOn": [
"Service"
]
},
"Certificate": {
"Type": "AWS::CertificateManager::Certificate",
"Properties": {
"DomainName": "*.wp-kyoto.net",
"DomainValidationOptions": [
{
"DomainName": "*.wp-kyoto.net",
"ValidationDomain": "wp-kyoto.net"
}
],
"ValidationMethod": "EMAIL"
}
},
"TargetGroup": {
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup",
"Properties": {
"HealthCheckIntervalSeconds": 90,
"HealthCheckPath": "/",
"HealthCheckTimeoutSeconds": 60,
"HealthyThresholdCount": 2,
"Port": 80,
"Protocol": "HTTP",
"TargetType": "ip",
"UnhealthyThresholdCount": 2,
"VpcId": {
"Ref": "NetworkStackVpcId"
}
}
},
"AlbSecurityGroup": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "ALB Security Group",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"SecurityGroupIngress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow from anyone on port 443",
"FromPort": 443,
"IpProtocol": "tcp",
"ToPort": 443
}
],
"VpcId": {
"Ref": "NetworkStackVpcId"
}
}
},
"LoadBalancer": {
"Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
"Properties": {
"LoadBalancerAttributes": [
{
"Key": "deletion_protection.enabled",
"Value": "false"
}
],
"Scheme": "internet-facing",
"SecurityGroups": [
{
"Fn::GetAtt": [
"AlbSecurityGroup",
"GroupId"
]
}
],
"Subnets": {
"Ref": "NetworkStackSubnetIds"
},
"Type": "application"
}
},
"AlbListener": {
"Type": "AWS::ElasticLoadBalancingV2::Listener",
"Properties": {
"DefaultActions": [
{
"FixedResponseConfig": {
"StatusCode": "403"
},
"Type": "fixed-response"
}
],
"LoadBalancerArn": {
"Ref": "LoadBalancer"
},
"Port": 443,
"Protocol": "HTTPS",
"Certificates": [
{
"CertificateArn": {
"Ref": "Certificate"
}
}
]
}
},
"AlbListenerRule": {
"Type": "AWS::ElasticLoadBalancingV2::ListenerRule",
"Properties": {
"Actions": [
{
"Order": 1,
"TargetGroupArn": {
"Ref": "TargetGroup"
},
"Type": "forward"
}
],
"Conditions": [
{
"Field": "host-header",
"HostHeaderConfig": {
"Values": [
"fargate-example.wp-kyoto.net"
]
}
},
{
"Field": "http-header",
"HttpHeaderConfig": {
"HttpHeaderName": "x-cf-token",
"Values": [
"b3c5fae4-3c01-445d-a85f-4c03b3946a0d"
]
}
}
],
"ListenerArn": {
"Ref": "AlbListener"
},
"Priority": 1
}
},
"Distribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Aliases": [
"fargate-example.wp-kyoto.net"
],
"DefaultCacheBehavior": {
"ForwardedValues": {
"Cookies": {
"Forward": "all"
},
"Headers": [
"*"
],
"QueryString": true
},
"TargetOriginId": "LoadBalancer-origin",
"ViewerProtocolPolicy": "redirect-to-https"
},
"Enabled": true,
"HttpVersion": "http2",
"IPV6Enabled": true,
"Origins": [
{
"CustomOriginConfig": {
"OriginProtocolPolicy": "https-only"
},
"DomainName": "lb-amplify-nextjsamplify-dev-190351.wp-kyoto.net",
"Id": "LoadBalancer-origin",
"OriginCustomHeaders": [
{
"HeaderName": "x-cf-token",
"HeaderValue": "b3c5fae4-3c01-445d-a85f-4c03b3946a0d"
}
]
}
],
"ViewerCertificate": {
"AcmCertificateArn": {
"Ref": "Certificate"
},
"MinimumProtocolVersion": "TLSv1.2_2019",
"SslSupportMethod": "sni-only"
}
}
}
}
},
"Outputs": {
"ServiceName": {
"Value": "site-service-api-80"
},
"ClusterName": {
"Value": {
"Ref": "NetworkStackClusterName"
}
},
"PipelineName": {
"Value": "amplify-nextjsamplify-dev-190351-site-service-api-80"
},
"ContainerNames": {
"Value": "api"
},
"PipelineUrl": {
"Value": {
"Fn::Join": [
"",
[
"https://",
{
"Ref": "AWS::Region"
},
".console.aws.amazon.com/codesuite/codepipeline/pipelines/amplify-nextjsamplify-dev-190351-site-service-api-80/view"
]
]
}
},
"LoadBalancerAliasDomainName": {
"Value": {
"Fn::GetAtt": [
"LoadBalancer",
"DNSName"
]
}
},
"LoadBalancerCnameDomainName": {
"Value": "lb-amplify-nextjsamplify-dev-190351.wp-kyoto.net"
},
"CloudfrontDistributionAliasDomainName": {
"Value": {
"Fn::GetAtt": [
"Distribution",
"DomainName"
]
}
},
"CloudfrontDistributionCnameDomainName": {
"Value": "fargate-example.wp-kyoto.net"
}
}
}