5
0

More than 3 years have passed since last update.

【Amplify x Next.jsハマりどころ】postConfirmation lambda triggerでCircular dependency between resourcesエラー

Last updated at Posted at 2021-03-13

つい先日、レシピインフルエンサーがファンに直接レシピを配信できるCMS「TABERUU」をα版として公開しました。

このサービスはバックエンドにAmplify、フロントエンドはNext.js、ホスティングやSSG、SSRはvercelという組み合わせで運用しています。

当初はサクサク作れそうだなと思っていたのですが、作っていくとAmplifyのハマりポイントが多く、またdocumentも全然情報が足りておらず、stackoverflowやgithub issueを漁りながら結構苦労する羽目になりました。同じことにハマる人ことが少しでも減るようにこれまでの知見をまとめようと思います。

なお、Amplifyについてざっくりと知っている人を対象に書いているため、まだ触ったことがない人はチュートリアルを済ませておくことをお勧めします。

まずは主なハマりどころを先に挙げておきます。

ハマりどころ一覧

  1. postConfirmation lambda triggerでstorage(dynamoDB)にアクセスしようとすると、Circular dependency between resourcesでpushできない
  2. type Query fieldに@aws_iamディレクティブをつけても、authRole(またはunauthRole)にポリシーがアタッチされない
  3. type Mutation filedに@authディレクティブをつけるとOnly one resolver is allowed per field. (Service: AWSAppSync; Status Code: 400; Error Code: BadRequestException;)
  4. redirect_urlを環境ごとに分けたいんですが
  5. S3の画像URLを署名なしで取得したいんですが
  6. aws-exports.jsが.gitignoreの対象になっているためビルド時にエラーになるんですが
  7. モノレポでamplifyバックエンドを共有したいんですが
  8. サブドメインでも認証を維持したいんですが

今回はこの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 addamplify api addを済ませて、schema.graphqlでUserモデルを作成している前提です。)
picture_pc_783076882ed3374a1664c819e14df897.png

これによりnextamplifieddbcde186PostConfirmationという長ったらしい名前のfunctionが作成されました(名前を任意に決めさせてくれたらいいのに)。次にこのfunctionからdynamoDBを操作できるようにamplify function updateでpermissionを追加します。

picture_pc_0bd4c36760b8b7ed8132ad4b2c2cd74f.png

完了したら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 functioncreateUserOnPostConfirmation(※名前はご自由に、他のAmplify projectと衝突することがあるのでprefix付けた方がいいかも)という名前の新しいlambda functionを作成し、nextamplifieddbcde186PostConfirmationcreateUserOnPostConfirmationを実行できるようにcustom CloudFormation stackを作成します。

依存関係を解消する

nextamplifieddbcde186PostConfirmation --> apiの依存関係を解消するのはamplify function updateでは出来ないようで、一度amplify auth updateでtriggerを非選択にしてnextamplifieddbcde186PostConfirmationをremoveし、再度上の手順でtriggerを作成してください。(こういう小回りが利かないのでストレスが溜まります。)

dynamoDBに書き込む用のlambdaを作成する

次にcreateUserOnPostConfirmationを作ります。このfunctionがdynamoDBへの書き込みを行うので、storageへのpermissionを付与します。

picture_pc_c9c85c5d7f60255475c27f36b032fec0.png

// ./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の記述は完了ですが、このままだとnextamplifieddbcde186PostConfirmationcreateUserOnPostConfirmationを実行する権限がないので、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で自動化できればいいじゃねーのかなーとか考えはしてるんですが、テンプレートの書き方が分からず試せていません。もしこの方法で上手くいったらご紹介します。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0