つい先日、レシピインフルエンサーがファンに直接レシピを配信できるCMS「TABERUU」をα版として公開しました。
このサービスはバックエンドにAmplify、フロントエンドはNext.js、ホスティングやSSG、SSRはvercelという組み合わせで運用しています。
当初はサクサク作れそうだなと思っていたのですが、作っていくとAmplifyのハマりポイントが多く、またdocumentも全然情報が足りておらず、stackoverflowやgithub issueを漁りながら結構苦労する羽目になりました。同じことにハマる人ことが少しでも減るようにこれまでの知見をまとめようと思います。
なお、Amplifyについてざっくりと知っている人を対象に書いているため、まだ触ったことがない人はチュートリアルを済ませておくことをお勧めします。
まずは主なハマりどころを先に挙げておきます。
ハマりどころ一覧
- postConfirmation lambda triggerでstorage(dynamoDB)にアクセスしようとすると、Circular dependency between resourcesでpushできない
- type Query fieldに@aws_iamディレクティブをつけても、authRole(またはunauthRole)にポリシーがアタッチされない
- type Mutation filedに@authディレクティブをつけるとOnly one resolver is allowed per field. (Service: AWSAppSync; Status Code: 400; Error Code: BadRequestException;)
- redirect_urlを環境ごとに分けたいんですが
- S3の画像URLを署名なしで取得したいんですが
- aws-exports.jsが.gitignoreの対象になっているためビルド時にエラーになるんですが
- モノレポでamplifyバックエンドを共有したいんですが
- サブドメインでも認証を維持したいんですが
今回はこのNo.1について書いていきたいと思います。
ちなみに、もともとはnoteに書いていたんですがあんま見てもらえてないのでQiitaに書くことに。初投稿ですがマークダウンで書けていい感じ。
※このシリーズでは、まずはエラーを再現し、そのエラーを修正するというチュートリアル的な流れで書いていきます。結論だけ知りたい方は「こいつの倒し方」まで読み飛ばしてください。
Versions
@aws-amplify/cli: 4.43.0
To Reproduce
AmplifyのAuthサービスでは裏でCognitoが動いていて、ユーザーアカウントはCognito User Poolsに蓄積されていきます。
アプリで使用するユーザー情報をどこに保存しようかと考えた時、次のようにCognitoUser.attributesに保存することもできますが、25個のcustom attributesまで登録できない、事前に登録しておく必要があり動的にattributesを追加できない、クエリしにくいなどの理由からお勧めできません。
const user = await Auth.currentAuthenticatedUser();
const result = await Auth.updateUserAttributes(user, {
'email': 'me@anotherdomain.com',
'custom:geocode': 'my geocode'
});
そこで、Userモデルを作成し、CognitoのpostConfirmation triggerでUserレコードを作成することにしました。postConfirmation triggerは次のようにCLIを使用して作成することができます。(既にamplify auth add
とamplify api add
を済ませて、schema.graphqlでUserモデルを作成している前提です。)
これによりnextamplifieddbcde186PostConfirmation
という長ったらしい名前のfunctionが作成されました(名前を任意に決めさせてくれたらいいのに)。次にこのfunctionからdynamoDBを操作できるようにamplify function update
でpermissionを追加します。
完了したらamplify push
を実行します。するとこんなエラーが表示されるはずです。
Circular dependency between resources: [functionnextamplifieddbcde186PostConfirmation, authnextamplifieddbcde186, apinextamplified, UpdateRolesWithIDPFunctionOutputs, UpdateRolesWithIDPFunction]
An error occurred during the push operation: Circular dependency between resources: [functionnextamplifieddbcde186PostConfirmation, authnextamplifieddbcde186, apinextamplified, UpdateRolesWithIDPFunctionOutputs, UpdateRolesWithIDPFunction]
依存関係が循環してるようです。backend-config.jsonを見てみると確かに、 nextamplifieddbcde186PostConfirmation
--> api
--> auth
--> nextamplifieddbcde186PostConfirmation
というように循環しています。
{
"auth": {
"nextamplifieddbcde186": {
"service": "Cognito",
"providerPlugin": "awscloudformation",
"dependsOn": [
{
"category": "function",
"resourceName": "nextamplifieddbcde186PostConfirmation",
"triggerProvider": "Cognito",
"attributes": [
"Arn",
"Name"
]
}
],
"customAuth": false
}
},
"api": {
"nextamplified": {
"service": "AppSync",
"providerPlugin": "awscloudformation",
"output": {
"authConfig": {
"defaultAuthentication": {
"authenticationType": "AWS_IAM"
},
"additionalAuthenticationProviders": [
{
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"userPoolConfig": {
"userPoolId": "authnextamplifieddbcde186"
}
}
]
}
}
}
},
"function": {
"nextamplifieddbcde186PostConfirmation": {
"build": true,
"providerPlugin": "awscloudformation",
"service": "Lambda",
"dependsOn": [
{
"category": "api",
"resourceName": "nextamplified",
"attributes": [
"GraphQLAPIIdOutput"
]
}
]
}
}
}
こいつの倒し方
この問題についてgithub issuesで議論されてますがまだopenのままみたいです。
https://github.com/aws-amplify/amplify-cli/issues/4568
workした解決策ですが、まず循環を断つためにnextamplifieddbcde186PostConfirmation
--> api
の依存関係を解消しました。そしてamplify add function
でcreateUserOnPostConfirmation
(※名前はご自由に、他のAmplify projectと衝突することがあるのでprefix付けた方がいいかも)という名前の新しいlambda functionを作成し、nextamplifieddbcde186PostConfirmation
がcreateUserOnPostConfirmation
を実行できるようにcustom CloudFormation stackを作成します。
依存関係を解消する
nextamplifieddbcde186PostConfirmation
--> api
の依存関係を解消するのはamplify function update
では出来ないようで、一度amplify auth update
でtriggerを非選択にしてnextamplifieddbcde186PostConfirmation
をremoveし、再度上の手順でtriggerを作成してください。(こういう小回りが利かないのでストレスが溜まります。)
dynamoDBに書き込む用のlambdaを作成する
次にcreateUserOnPostConfirmation
を作ります。このfunctionがdynamoDBへの書き込みを行うので、storageへのpermissionを付与します。
// ./amplify/backend/function/createUserOnPostConfirmation/src/index.js
/* Amplify Params - DO NOT EDIT
API_NEXTAMPLIFIED_GRAPHQLAPIIDOUTPUT
API_NEXTAMPLIFIED_USERTABLE_ARN
API_NEXTAMPLIFIED_USERTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
const aws = require('aws-sdk')
const ddb = new aws.DynamoDB()
exports.handler = async (event, context) => {
let date = new Date()
if (event.userName) {
const item = {
__typename: { S: 'User' },
owner: { S: event.userName },
createdAt: { S: date.toISOString() },
updatedAt: { S: date.toISOString() },
}
if (event.request.userAttributes.name) {
item['displayName'] = event.request.userAttributes.name
}
let params = {
Item: item,
TableName: process.env.API_NEXTAMPLIFIED_USERTABLE_NAME,
}
// Call DynamoDB
try {
await ddb.putItem(params).promise()
console.log('Success')
} catch (err) {
console.log('Error', err)
}
console.log('Success: Everything executed correctly')
context.done(null, event)
} else {
// Nothing to do, the user's email ID is unknown
console.log('Error: Nothing was written to DynamoDB')
context.done(null, event)
}
}
作成したlambdaをpostConfirmation triggerから呼び出す
これでcreateUserOnPostConfirmation
を作成できましたので、このlambdaをnextamplifieddbcde186PostConfirmation
から実行するようにします。
// ./amplify/backend/function/nextamplifieddbcde186PostConfirmation/src/custom.js
const aws = require('aws-sdk')
const addUserToTable = function (event, context) {
const lambda = new aws.Lambda({
region: process.env.REGION,
})
const params = {
FunctionName: `createUserOnPostConfirmation-${process.env.ENV}`,
InvocationType: 'RequestResponse',
Payload: JSON.stringify(event, context),
}
lambda.invoke(params, function (err, data) {
if (err) {
console.error(err)
} else {
console.log(params.FunctionName + ': ' + data.Payload)
}
})
}
exports.handler = async (event, context, callback) => {
try {
addUserToTable(event, context)
} catch (error) {
return callback(error)
}
callback(null, event)
}
CloudFormationテンプレートを修正する
これでjsの記述は完了ですが、このままだとnextamplifieddbcde186PostConfirmation
がcreateUserOnPostConfirmation
を実行する権限がないので、CloudFormation templateを以下のように更新します。
(ちなみに、CLIでamplify update function
> nextamplifieddbcde186PostConfirmation
> Resource access permissions
> function
と進んでcreateUserOnPostConfirmationの実行権限を与えると、相変わらず循環が解消されないので注意してください。)
カスタマイズが許されているCloudFormationテンプレートはamplify/backendディレクトリ内のサービス(authとかapiとかfunction)毎に配置されています。今回はnextamplifieddbcde186PostConfirmation
のテンプレートをいじります。
長いので省略しますが、Resources.lambdaexecutionpolicy.Properties.PolicyDocument.Statementという配列が見付かると思うので、その末尾に以下を追記してください。
{
...
"Statement": [
...
,
{
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": {
"Fn::Sub": [
"arn:aws:lambda:${region}:${account}:function:createUserOnPostConfirmation-${env}",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
},
"env": {
"Ref": "env"
}
}
]
}
}
],
...
}
以上で完成したのでamplify push
して無事通ることを確認してください。
##最後に
今回はpostConfirmation triggerから別のlambdaを実行することで循環を断ち切る方法を紹介しました。ただ、余計なリクエストが増えるし後で何してたんだっけ?ってなるので正直あまり気に入っていません。
これ以外の方法として、amplify auth
からtriggerを作るのではなく、amplify function add
でlambdaを作り、Cognitoコンソールで手動でtriggerに割り当ててもワークはします。lambdaをtriggerに割り当てる処理をCloudFormationで自動化できればいいじゃねーのかなーとか考えはしてるんですが、テンプレートの書き方が分からず試せていません。もしこの方法で上手くいったらご紹介します。