[AWS Amplify][AppSync] API_KEYによる認証を使っている場合に発生しうる "Error: No credentials" 問題の対処

API_KEYを使ってAppSyncを叩いている環境でハマりました。

他人の既存環境のソースを引っ張って来た場合や、ステージング環境のデプロイなどでamplify add envを使って環境をスイッチした場合に表題のエラーが起こりえます。

**特にソースの変更を行っていないにも関わらず、envを切り替えたら動かなくなった**、という事象に遭遇した方は、この記事の内容が該当するかもしれません。

amplifyのバージョンは以下の通りです。

```
"aws-amplify": "^1.2.2",
"aws-amplify-vue": "^0.3.2",
```

GraphQLを叩いている部分のコードスニペットを例示します。 `graphqlOperation` を用いています。

```js
let listItems = `query listBenchmarkHistory {
            listBenchmarkResultHistorys(team: "${this.team}",limit: 10000){
                items {
                  epochMilliSeconds
                  score
                  status
                  comment
                }
            }
        }`
const lists = await API.graphql(graphqlOperation(listItems))
```

# 背景

初期の開発では `dev` 環境を利用していました。当初は一人で開発していましたし特に困ることもなかったのですが、本番デプロイを迎えてまっさらな本番環境を用意する必要が出ました。

（本番環境にAPI Keyを使うんじゃない、というご指摘は甘んじて受け入れます。。。1日限りで利用するアプリケーションだし・・・と手抜いた結果がこのザマです）

> amplify env add 

で `prod` を追加し、publishしようとすると、publish自体は成功するものの以下のようなエラーが出ます。

<img width="657" alt="スクリーンショット 2019-10-21 15.59.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78417/bf649d49-6c92-18e4-267a-ffc1dc6e5941.png">

```
[WARN] 59:24.132 API - ensure credentials error No Cognito Federated Identity pool provided
```
```
vue.runtime.esm.js:1888 Error: No credentials
    at e.<anonymous> (API.js:419)
    at u (API.js:40)
    at Object.next (API.js:21)
    at a (API.js:12)
```

フロントをざっと見た限りでは、AppSyncに対するQueryやSubscriptionがコケているようでした。 

Cognitoも使っていないし、Credentialでもないのに、はて何のことやら🤔と思っていましたがどうもamplify-jsのバグに近い挙動のようです。

# 対処方法

Amplifyのクライアントを初期化する際に、AppSync APIリクエストを行うときに使用する認証方法を指定する属性があります。env切り替えを行った場合にこのプロパティが何故か抜け落ちます。これが原因でAPIの疎通不具合が発生しています。

ワークアラウンド的な対処となりますが、私の見解としては以下の2つのいずれかが良いように思います。

## _src/aws-exports.js_ の追記

Amplifyの初期化時にこのファイルのexportを参照しています。認証方法の指定もここで指定できます。一例として、変更後のaws-exportsは以下のような内容になります。

```js
// src/aws-exports.js
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "ap-northeast-1",
    "aws_content_delivery_bucket": "your.hosting.bucket.com",
    "aws_content_delivery_bucket_region": "ap-northeast-1",
    "aws_content_delivery_url": "http://your-subdomain.s3-website-ap-northeast-1.amazonaws.com",
    "aws_appsync_graphqlEndpoint": "https://your-api.appsync-api.ap-northeast-1.amazonaws.com/graphql",
    "aws_appsync_region": "ap-northeast-1",
    "aws_appsync_apiKey": "your-api-key",
    "aws_appsync_authenticationType": "API_KEY" // この1行を追加する
};

export default awsmobile;
```

aws-exportsはpublishした際にAmplify CLIが勝手によしなに上書きしてくれる自動生成ファイルです。環境の固有名が入っているコンテンツなのでgitignoreの対象にもなっています。

## Amplifyの初期化時に _aws_appsync_authenticationType_ を明示的に指定

アプリからのGraphQLリクエストをAPI_KEYによる方法のみに仮定することになりますが、Amplify初期化時の引数に明示してしまうという手もあります。

前述のやり方との違いはパラメータを宣言する場所が異なるだけです。 _DO NOT EDIT_ と書かれているソースの変更に抵抗があればこちらで。ただし、こっちのやり方ではAppSyncの認証方式を変えた場合にコードの変更が必要になりますし、あまり推奨はしません。

通常Amplifyの初期化はmain.jsで実行することになると思います。コードの一例は以下。

```js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify';

import Amplify, * as AmplifyModules from 'aws-amplify'
import { AmplifyPlugin, components } from 'aws-amplify-vue'
import awsconfig from './aws-exports'

/*
 次のコードが該当部分。通常はawscondfigを引数に指定するところ、
 aws_appsync_authenticationType を明示的に追加することで対処
*/
Amplify.configure({
  ...awsconfig,                               
  aws_appsync_authenticationType: "API_KEY"
})

Vue.use(AmplifyPlugin, AmplifyModules)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App),
  components: {
    ...components
  }
}).$mount('#app')
```


# 解説

自分なりに調べたことを書くので、以降は読みたい人だけ読んでください。

Amplifyクライアントの初期化は `Amplify.configure(arg)` で行います。argはオブジェクト型です。APIの認証方法を指定する引数を、 `aws_appsync_authenticationType` プロパティで受け取ることができます。

Amplify CLIで開発しているならば、argには通常 aws-exports.js のexportを指定して初期化することになると思います。このとき、envの切り替えを経由してデプロイしようとした場合に _どういうわけか_ この`aws_appsync_authenticationType` が抜け落ちます。

GraphQL APIの呼び出し方法に話を変えます。多くの場合はAmplify.API.graphqlを以下のような形で呼び出すことになると思います。

```js
await API.graphql(graphqlOperation(listItems)) 
// listItemsはクエリのtemplate string
```

`API.graphql` はオブジェクト型の引数を受け取ります。そのオブジェクトのキーとして許容されるのは、query, variables, authMode の3つです。authModeがAppSyncへのリクエストに使用する認証方法の指定になります。

graphqlOperationの実態はGraphQL APIへのリクエストパラメータを組み立てるためのユーティリティで、 `{query, value}` の形を持つobjectを返します（authModeは含みません）。authModeが無指定の場合、 `API.graphql` はAmplifyの初期化時に指定された認証方式を使ってAPIリクエストを行います。


2つの話を総合すると、

- envを切り替えるとexportsから `aws_appsync_authenticationType` が抜け落ちてしまい、AmplifyがGraphQL APIにリクエストするときに使用する認証方式が未指定(デフォルト値は _AWS_IAM_) になる
- Ampilfyはデフォルト指定の認証方式(AWS_IAM)を利用してGraphQL APIリクエストを実行ようとするが、実際にはAPI_KEYを使うのが正しく、リクエストは失敗する

という状況が発生します。最終的に `No credentials` という例外が生成されることになります。

なお、該当のExceptionを上げている部分のソースはこちらです。

https://github.com/aws-amplify/amplify-js/blob/master/packages/api/src/API.ts#L303-L308

`case 'AWS_IAM'` の分岐に入っているのがわかると思います。aws-exportsに認証方式の指定がないために、ここの分岐に入ってしまいます。


# まとめ

Amplify、クセが強い。

今回のトラブルの内容から推測するに、あくまでもAPI_KEYは単独での動作検証等に用いることを前提に設計されている感じがしました。だから別環境に持っていったり、他人との共同開発で同じ環境をシェアするような使い方で支障が出るのかなと。