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

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

このサービスはバックエンドに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](https://note.com/knagata/n/nec28a8d8ad31)に書いていたんですがあんま見てもらえてないのでQiitaに書くことに。初投稿ですがマークダウンで書けていい感じ。

※このシリーズでは、まずはエラーを再現し、そのエラーを修正するというチュートリアル的な流れで書いていきます。結論だけ知りたい方は「こいつの倒し方」まで読み飛ばしてください。

# Versions
@aws-amplify/cli: 4.43.0

## To Reproduce
AmplifyのAuthサービスでは裏でCognitoが動いていて、ユーザーアカウントはCognito User Poolsに蓄積されていきます。

アプリで使用するユーザー情報をどこに保存しようかと考えた時、次のようにCognitoUser.attributesに保存することもできますが、25個のcustom attributesまで登録できない、事前に登録しておく必要があり動的にattributesを追加できない、クエリしにくいなどの理由からお勧めできません。

```typescript
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モデルを作成している前提です。）
![picture_pc_783076882ed3374a1664c819e14df897.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201783/23e0c963-afa6-dfa2-b83f-0be19267e778.png)

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

![picture_pc_0bd4c36760b8b7ed8132ad4b2c2cd74f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201783/d9eaa289-fb04-2117-b337-d90e498dbca8.png)

完了したら`amplify push`を実行します。するとこんなエラーが表示されるはずです。

```bash
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`というように循環しています。

```json
{
 "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](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を付与します。

![picture_pc_c9c85c5d7f60255475c27f36b032fec0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1201783/b44cec1f-15a2-6455-f88e-8a39c2575492.png)

```javascript
// ./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`から実行するようにします。

```javascript
// ./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という配列が見付かると思うので、その末尾に以下を追記してください。

```json
{
...
　　　　"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で自動化できればいいじゃねーのかなーとか考えはしてるんですが、テンプレートの書き方が分からず試せていません。もしこの方法で上手くいったらご紹介します。