Cognitoを使った認証付きのWebページをreactで作る場合、amplify使うと認証まわりをいい感じにしてくれるのでとても便利だが、Cognitoだけ他のAWSリソースと切り離してamplifyで管理としたくないのでCDKで構成し、importさせる。
その上でCICDパイプラインにのせた以下の構成を考える。
0. 環境構築
必要なリソースはCDKで構築。
以下参照。CodeCommitは誤って消えたりすると困るので別で手動作成。
Cognito-Stack CDKコード
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cognito from 'aws-cdk-lib/aws-cognito';
export class CognitoStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// == const ==
const userPoolName = "hogehogeuserpool";
const userPoolAppClientName = "hogehogeappclient";
// == Cognito ==
const userpool = new cognito.UserPool(this, userPoolName, {
userPoolName: userPoolName,
standardAttributes: {
email: {required: true, mutable: true},
},
selfSignUpEnabled: true,
signInCaseSensitive: false,
autoVerify: {email: true},
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: RemovalPolicy.DESTROY,
})
userpool.addClient(userPoolAppClientName, {
oAuth: {
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PROFILE,
],
flows: {
authorizationCodeGrant: true,
},
},
authFlows: {
adminUserPassword: true, // ALLOW_ADMIN_USER_PASSWORD_AUTH
custom: true, // ALLOW_CUSTOM_AUTH
userPassword: false, // ALLOW_USER_PASSWORD_AUTH
userSrp: true, // ALLOW_USER_SRP_AUTH
}
});
}
}
Hosting-Stack CDKコード
import { Stack, StackProps, RemovalPolicy } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins';
export class HostingStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// == const ==
const hostingBucketName = "hogehogehostingbucket";
const oaiName = "hogehogeoai";
const cloudfrontDistributionName = "hogehogeDistribution";
// == S3 ==
// Hosting Bucket
const hostingBucket = new s3.Bucket(this, hostingBucketName, {
bucketName: hostingBucketName,
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: false,
blockPublicPolicy: false,
ignorePublicAcls: false,
restrictPublicBuckets: false
}),
versioned: false,
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Bucket Contents Upload
new s3deploy.BucketDeployment(this, "deployToS3", {
destinationBucket: hostingBucket,
sources: [s3deploy.Source.asset(`src/testpage/index.html`)], // 任意のhtmlをテスト用に用意すること!!
})
// == CloudFront ==
const oai = new cloudfront.OriginAccessIdentity(this, oaiName, {
comment: `s3-bucket-${hostingBucketName}`
});
const cloudfrontDistribution = new cloudfront.Distribution(this, cloudfrontDistributionName, {
enabled: true,
defaultRootObject: "index.html",
priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
defaultBehavior: {
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
origin: new cloudfront_origins.S3Origin(
hostingBucket, {
originAccessIdentity: oai
}
)
}
});
hostingBucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["s3:GetObject"],
principals: [
new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)
],
resources: [
`${hostingBucket.bucketArn}/*`
]
}));
}
}
Pipeline-Stack CDKコード
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as iam from 'aws-cdk-lib/aws-iam';
export class PipelineStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// == const ==
const codecommitRepName = "hogehoge";
const hostingBucketName = "hogehogehostingbucket";
const snsTopicName = "hogehogeTopicForApproval";
const approvalEmailAddress = "************@gmail.com";
const buildProjectName = "hogehogeBuildProj";
const buildProjectRoleName = "hogehogeBuildProjRole";
const pipelineName = "hogehogePipeline";
// == S3 ==
const dist_bucket = s3.Bucket.fromBucketAttributes(this, hostingBucketName, {
bucketName: hostingBucketName,
});
// == Artifact ==
const sourceOutput = new codepipeline.Artifact('sourceOutput');
const buildOutput = new codepipeline.Artifact("buildOutput");
// == SNS ==
const snsTopic = new sns.Topic(this, snsTopicName, {
topicName: snsTopicName,
})
snsTopic.addSubscription(new subscriptions.EmailSubscription(approvalEmailAddress))
// == CodeCommit ==
const repository = codecommit.Repository.fromRepositoryName(this, codecommitRepName, codecommitRepName);
// == CodeBuild ==
const buildProjectRole = new iam.Role(this, buildProjectRoleName, {
roleName: buildProjectRoleName,
assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"),
});
buildProjectRole.addToPolicy(
new iam.PolicyStatement({
resources: ["*"],
actions: [
"ssm:GetParameters",
],
})
);
const buildProject = new codebuild.PipelineProject(this, buildProjectName, {
projectName: buildProjectName,
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
},
buildSpec: codebuild.BuildSpec.fromSourceFilename("buildspec.yml"),
role: buildProjectRole,
environmentVariables: {
ENV: { value: "dev", type: codebuild.BuildEnvironmentVariableType.PLAINTEXT},
},
});
// == CodePipeline ==
const pipeline = new codepipeline.Pipeline(this, pipelineName, {
pipelineName: pipelineName,
});
// == CodePipeline Actions ==
const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
actionName: "Suorce",
repository: repository,
branch: "master",
trigger: codepipeline_actions.CodeCommitTrigger.EVENTS,
output: sourceOutput,
});
const buildAction = new codepipeline_actions.CodeBuildAction({
actionName: "Build",
project: buildProject,
input: sourceOutput,
outputs: [buildOutput],
});
const approvalAction = new codepipeline_actions.ManualApprovalAction({
actionName: "Approval",
notificationTopic: snsTopic,
})
const deployAction = new codepipeline_actions.S3DeployAction({
actionName: "Deploy",
bucket: dist_bucket,
input: buildOutput,
extract: true,
})
pipeline.addStage({
stageName: "Source",
actions: [sourceAction]
});
pipeline.addStage({
stageName: "Build",
actions: [buildAction]
});
pipeline.addStage({
stageName: "Approval",
actions: [approvalAction]
})
pipeline.addStage({
stageName: "Deploy",
actions: [deployAction]
});
}
}
# !/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CognitoStack } from 'cognito-stack';
import { HostingStack } from 'hosting-stack';
import { PipelineStack } from 'pipeline-stack';
const app = new cdk.App();
new HostingStack(app, 'HostingStack');
new CognitoStack(app, 'CognitoStack');
new PipelineStack(app, 'PipelineStack');
$ npx cdk deploy --all
CloudFrontコンソールからURLを確認しアクセスしてtest用ページが見れることを確認。
1. reactアプリ作成
CDKのディレクトリとは別に任意のディレクトリを作成し、以下reactアプリの雛形を作成。(これが環境構築で作成したCodeCommitで管理する対象。)
$ npx create-react-app react-frontui-cognito --template typescript
2. amplify用のユーザを作成
途中マネジメントコンソールに飛ぶ。
実行前にAWSコンソールにログインしていたケースでしたやったことがない(未ログインだとログイン求められるのでは?)
デフォルトでAdmin権限が付与されるが、大きいのでAdministratorAccess-Amplifyを選び直して作成。
$ amplify configure
Follow these steps to set up access to your AWS account:
Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue
Specify the AWS Region
? region: ap-northeast-1
Specify the username of the new IAM user:
? user name: ****
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/users$new?step=final&accessKey&userNames=****&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess
Press Enter to continue
Enter the access key of the newly created user:
? accessKeyId: ****
? secretAccessKey: ****
This would update/create the AWS Profile in your local machine
? Profile Name: ****
Successfully set up the new user.
3. amplifyの設定・初期化
$ cd react-frontui-cognito
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project reactfrontuicognito
The following configuration will be applied:
Project information
| Name: ****
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start
? Initialize the project with the above configuration? Yes
Using default provider awscloudformation
? Select the authentication method you want to use: AWS profile
For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
? Please choose the profile you want to use ****
4. 既存Cognitoの取り込み
$ amplify import auth
Using service: Cognito, provided by: awscloudformation
✔ What type of auth resource do you want to import? · Cognito User Pool only
✔ Select the User Pool you want to import: · ap-northeast-1_*********
5. 設定の反映・aws-exports.js
の作成
$ amplify push
6. packageの追加
$ npm install aws-amplify @aws-amplify/ui-react
7. コード編集
src/App.tsx
をまるごと編集。
import { Amplify } from 'aws-amplify';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import awsExports from './aws-exports';
Amplify.configure(awsExports);
export default function App() {
return (
<Authenticator variation="modal">
{({ signOut, user }) => (
<main>
<h1>Hello {user.username}</h1>
<button onClick={signOut}>Sign out</button>
</main>
)}
</Authenticator>
);
}
参考
https://ui.docs.amplify.aws/components/authenticator
8. ローカルで画面の確認
$ npm start
9. テストユーザの作成
9-1) 画面から作成する。
→ "Create Account"をクリックして作成。
9-2) CLIから作成する。 (主に"Create Account"を用意しない環境での確認用)
$ aws cognito-idp admin-create-user --user-pool-id <USER_POOL_ID> --username <USER_NAME> --user-attributes Name=email,Value=<E_MAIL_ADDRESS> Name=email_verified,Value=true --temporary-password <PASSWORD>
# https://dev.classmethod.jp/articles/cognito-admin-set-user-password/
$ aws cognito-idp admin-set-user-password --user-pool-id <USER_POOL_ID> --username <USER_NAME> --password <PASSWORD> --permanent
# get IdToken (確認用)
$ aws cognito-idp admin-initiate-auth --user-pool-id <USER_POOL_ID> --client-id <CLIENT_ID> --auth-flow "ADMIN_USER_PASSWORD_AUTH" --auth-parameters USERNAME=<USERNAME>,PASSWORD=<PASSWORD>
参考
https://dev.classmethod.jp/articles/obtain-access-tokens-for-cognito-users-using-aws-cli/
10. build環境の設定ファイル作成
10-1) buildspec.yml
を作成。
version: 0.2
phases:
install:
runtime-versions:
nodejs: 14
pre_build:
commands:
- npm update -g npm
- npm install -g @aws-amplify/cli
- npm install --production
- chmod u+x ./amplify_init.sh
- ./amplify_init.sh
build:
commands:
- npm run build
artifacts:
files:
- '**/*'
base-directory: 'build'
10-2) amplify_init.sh
を作成。
AUTHCONFIGにCognitoの情報を書き込む。(tesm-provider-info.json
を参照するといい。)
以下を参考。amplifyをCodeBuildで使うにはscriptを書く必要があるみたい。わりと多くの人がそうしている。これらをもとにcognito部分を追加させてもらう。
- https://qiita.com/rocketmaso/items/4f5b7d61c512b97f637a#buildspecyml%E3%81%AE%E7%B7%A8%E9%9B%86
- https://stackoverflow.com/questions/56649214/amplify-error-auth-headless-init-is-missing-the-following-inputparams-facebooka
- https://github.com/aws-amplify/amplify-console/issues/1271
# !/bin/bash
set -eu
echo "[amplify_init.sh] start"
# SSMからクレデンシャル取得
access_key_id=$(aws ssm get-parameters --names '/AmplifyCICD/AccessKeyID' --with-decryption --query Parameters[].Value --output text)
secret_access_key=$(aws ssm get-parameters --names '/AmplifyCICD/SecretAccessKey' --with-decryption --query Parameters[].Value --output text)
# 環境変数取得
env=$1
# クレデンシャルの設定
aws configure set aws_access_key_id $access_key_id
aws configure set aws_secret_access_key $secret_access_key
aws configure set default.region ap-northeast-1
# Amplifyの設定
AWSCLOUDFORMATIONCONFIG="{\
\"configLevel\":\"project\",\
\"useProfile\":true,\
\"profileName\":\"default\"\
}"
AMPLIFY="{\
\"projectName\":\"sample_app\",\
\"envName\":\"$env\",\
\"defaultEditor\":\"code\"\
}"
PROVIDERS="{\
\"awscloudformation\":$AWSCLOUDFORMATIONCONFIG\
}"
AUTHCONFIG="{\
\"userPoolId\":\"ap-northeast-1_*********\",\
\"webClientId\":\"**************************\",\
\"nativeClientId\":\"**************************\"\
}"
CATEGORIES="{\
\"auth\":$AUTHCONFIG\
}"
amplify init \
--amplify $AMPLIFY \
--providers $PROVIDERS \
--categories $CATEGORIES \
--yes
echo "[amplify_init.sh] finish"
CodeBuildの環境変数から直接SSMパラメータストアを指定することも可能だが、SecureStringを復号化してくれるわけではなさそうなので(要出典)、CLIで取得した。
なお、CodeBuildのbuildspecからSSMとかに取得に行く場合の注意点とかをまとめてくれてるページがあった。
https://dk521123.hatenablog.com/entry/2020/02/18/230358
11. amplify用のユーザのクレデンシャルをcodebuildから利用できるようにSSMに登録。
$ aws ssm put-parameter --name "/AmplifyCICD/AccessKeyID" --value "****" --type SecureString
$ aws ssm put-parameter --name "/AmplifyCICD/SecretAccessKey" --value "****" --type SecureString
12. パイプラインの実行。
$ git remote add origin https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/******
$ git commit --allow-empty -m "init empty commit"
$ git add .
$ git commit -m "add basic project code"
$ git push --set-upstream origin master
※ 前述のようにtesm-provider-info.json
にあまり外に出したくないID類が載ってるのでPublicなリポジトリにpushする場合は注意。
https://www.bioerrorlog.work/entry/public-amplify-project
13. 別環境への展開/移植
dev環境で構築したのちに同じ構成をstaging/production環境に構築する際、同じようにパイプラインを走らせてもCodeBuildでエラー終了する。
以下2つの問題を解消する必要がある。
1つ目の問題
Could not initialize 'dev': Access Denied
ここの"dev"はamplify init
時にEnvironment
に対応するところで設定した環境名であり、team-provider-info.json
に記述されている詳細情報との紐づけに利用されている。
ここに記載のリソースにアクセスできないとのことだが、それもそうで、開発環境で生成したteam-provider-info.json
には開発環境のAWSアカウントのARNやらリソースIDが記載されているので他環境からはアクセスできない。
→ 他環境ではその環境に対応するteam-provider-info.json
を用意しておいてあげる必要がある。
適切な値で記述するのもいいが、CFnで生成しないといけないリソースなどもあり正直面倒なので一度amplify init
を実行してteam-provider-info.json
を出力してもらい、コピペする方が楽かもしれない。ここではその方法をとる。
team-provider-info.json
を削除し、その上でbuildspec.yml
にteam-provider-info.json
をコンソールに出力するようにコードを差し込む。
(略)
pre_build:
commands:
- npm update -g npm
- npm install -g @aws-amplify/cli
- npm install --production
- chmod u+x ./amplify_init.sh
- ./amplify_init.sh
- cat ./amplify/team-provider-info.json # <<<追加
(略)
git push
する。
こうすると、CodeBuild上でCFnが実行され、team-provider-info.json
を生成し出力してくれる。
出力されたteam-provider-info.json
をamplify/
以下に配置する。
このファイルがある状態だとamplify init
しても新しくCFnが実行されることはないので大丈夫。
2つ目の問題
上記のまま実行してもaws-exports.js
に必要な項目が全然含まれていない(aws-project_region
しかない)。
→ 一度amplify push
する必要がある。
amplify init \
--amplify $AMPLIFY \
--providers $PROVIDERS \
--categories $CATEGORIES \
--yes
amplify push --yes # <<<<追加
echo "[amplify_init.sh] finish"
この状態で再度git push
すると以降aws-exports.js
が正しく生成されるようになる。
以上の編集後、これらに追加した項目を削除し、再度pushしてビルドが通ることを確認する。