AWS Amplifyを使った開発で役立つ逆引きTips集(2020年版)

本記事は [Fusic Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fusic) の1日目の記事です。

今年もいよいよはじまりました！Fusic社員が「個性をかき集めて、驚きの角度から世の中をアップデートしつづける」記事を書いていきますので、クリスマスまでどうぞよろしくお願いします。

というわけで、1日目は「AWS Amplify」に関する記事を書こうと思います。

## はじめに

![68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6177732d6d6f62696c652d6875622d696d616765732f6177732d616d706c6966792d6c6f676f2e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/75ecf96c-c30d-4437-ea47-e0df342e9f79.png)

個人的に今年は「Amplify元年」といっても過言ではない年でした。[Amplify SNS Workshop](https://amplify-sns.workshop.aws/ja/) との出会いをきっかけに、AmplifyとNext.jsを組み合わせてモダンかつスピーディな開発ができないか模索するようになりました。

Amplifyを触るうちにさまざまなTipsを蓄積できたので、1年分まとめた逆引きTips集として公開することにしました。

※普段使用しているJavaScriptフレームワークがReact/Next.jsなので、記載内容もReact寄りです。ご了承くださいm(_ _)m

## 目次

1. [Auth](#auth)
  1. [サインアップ時に電話番号を不要としたい](#サインアップ時に電話番号を不要としたい)
2. [API(GraphQL)](#apigraphql)
  1. [API.graphql()の結果をTypeScriptでいい感じに型解決できない](#apigraphqlの結果をtypescriptでいい感じに型解決できない)
  2. [Authでログインしたユーザ限定でAPIを実行できるようにしたい](#authでログインしたユーザ限定でapiを実行できるようにしたい)
  3. [項目をソートして取得したい](#項目をソートして取得したい)
  4. [schema.graphqlを更新しても諸々反映されない](#schemagraphqlを更新しても諸々反映されない)
  5. [createdAt、updatedAtをスキーマから消したい](#createdatupdatedatをスキーマから消したい)
3. [Hosting](#hosting)
  1. [開発、ステージング、本番のインフラを分けたい](#開発ステージング本番のインフラを分けたい)
  2. [hostingしたNext.jsのSPAが「403 Access Denied」となる](#hostingしたnextjsのspaが403-access-deniedとなる)
  3. [先にCI/CDを構築して後からauthやapiといったカテゴリのリソースを追加するとフロントエンドでエラーが発生する](#先にcicdを構築して後からauthやapiといったカテゴリのリソースを追加するとフロントエンドでエラーが発生する)
4. [Function](#function)
  1. [ランタイムにPythonを指定するとCI/CDがコケる](#ランタイムにpythonを指定するとcicdがコケる)
  2. [定期的にタスクを実行したい](#定期的にタスクを実行したい)
5. [その他](#その他)
  1. [Amplifyの基本的な使い方をマスターしたい](#amplifyの基本的な使い方をマスターしたい)
  2. [Categoryとして提供されていないAWSリソースを追加したい](#categoryとして提供されていないawsリソースを追加したい)
  3. [team-provider-info.jsonって何者？](#team-provider-infojsonって何者)
  4. [上記で解決できない・よくわからない問題が起こっている](#上記で解決できないよくわからない問題が起こっている)
6. [まとめ](#まとめ)

## Auth

### サインアップ時に電話番号を不要としたい

Amplify UI Componentsで作ったサインアップフォームは入力項目が多くて不便です。

![amplify_2020_signup_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/90bd9573-c3a1-fb72-b10c-7ec8240a6115.png)

コンポーネントに対してオプション指定することで、電話番号などの入力項目を減らせるようになっています。

![amplify_2020_signup_form_without_phone.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/3635890e-0d83-4313-138a-aa1af2f5210e.png)

注意点として、Reactに対応したAmplify UI Componentsは2つ存在しています。

- aws-amplify-react
- @aws-amplify/ui-react

どちらを使うかによって事情が変わってくるため、それぞれ解説します。

#### aws-amplify-reactの場合

`aws-amplify-react` はLegacyなnpmパッケージです。

https://docs.amplify.aws/ui-legacy/q/framework/react

こちらのパッケージではReactのHOCとして、 `withAuthenticator` が提供されています。Reactコンポーネントをexportする際に、このHOCでラップすることでその画面を認証必須にすることが可能です。

```javascript
export default withAuthenticator(App);
```

この引数でオプション指定することによって、サインアップフォームから電話番号を省略できます。

```javascript
export default withAuthenticator(App, {
  signUpConfig: {
    hiddenDefaults: ['phone_number']
  }
});
```

#### @aws-amplify/ui-reactの場合

`@aws-amplify/ui-react` はLatestなnpmパッケージです。

https://docs.amplify.aws/ui/q/framework/react

Latestなので現在ではこちらを使用することが推奨されています。こちらのパッケージでも `withAuthenticator` が提供されています。しかし、 `aws-amplify-react` とは異なり `signUpConfig` をオプション指定することができないようです。

https://docs.amplify.aws/ui/auth/authenticator/q/framework/react

このため、HOCではなく `AmplifyAuthenticator` コンポーネントを使ってフォームをカスタマイズします。

```javascript
<AmplifyAuthenticator usernameAlias="email">
  <AmplifySignUp slot="sign-up" formFields={[ { type: "email" }, { type: "password" } ]} />
  <AmplifySignIn slot="sign-in" />
  <AmplifySignOut />
  <Component {...pageProps} />
</AmplifyAuthenticator>
```

## API(GraphQL)

### API.graphql()の結果をTypeScriptでいい感じに型解決できない

Amplifyを使うと、バックエンドと通信するためのクライアント(GraphQLラッパー)を自動生成してくれるので便利です。もちろん、これはTypeScriptにも対応しています。

TypeScript初心者な私は、API.graphql()の戻り値が型推論されず、対処方法に悩みました、

```typescript
import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api'
import { listDevices } from '../src/graphql/queries'

const asyncFunc = async () => {
  // これだと型推論が効かずエラーとなる
  const result = await API.graphql(graphqlOperation(listDevices))

  // 以降の処理...
}
```

いろいろ模索した結果、どうやら `as` を使って型を明示するのが正解のようです。

```typescript
import API, { graphqlOperation, GraphQLResult } from '@aws-amplify/api'
import { listDevices } from '../src/graphql/queries'
import { ListDevicesQuery } from '../src/API'
const asyncFunc = async () => {
  const result = (await API.graphql(graphqlOperation(listDevices))) as GraphQLResult<ListDevicesQuery>

  // 以降の処理...
}
```

### Authでログインしたユーザ限定でAPIを実行できるようにしたい

GraphQLスキーマにて `@auth` ディレクティブを指定することで、リクエスト時の権限チェックが可能です。

```graphql
type Device @model
  @auth(rules: [{ allow: owner }]) {
  id: ID!
  name: String!
  logs: [Log] @connection(keyName: "byDevice", fields: ["id"])
}

type Log @model
  @auth(rules: [{ allow: owner }])
  @key(name: "byDevice", fields: ["deviceId", "timestamp"]) {
  id: ID!
  deviceId: ID!
  timestamp: AWSTimestamp!
  value: Float!
  device: Device @connection(fields: ["deviceId"])
}
```

上記では、`allow: owner` としていますが、所有者以外にも権限指定したり、readやcreate権限を部分的に付与することも可能です。詳しくはドキュメントを参照ください。
https://docs.amplify.aws/cli/graphql-transformer/auth

### 項目をソートして取得したい

[Amplify SNS Workshop](https://amplify-sns.workshop.aws/ja/30_mock/25_post_back_end_key.html)で解説されている通り、 `@key` ディレクティブを使ってDynamoDBにGSIを追加します。

```graphql
type Post
  @model (subscriptions: { level: public })
  # パーティションキーをtype, ソートキーをtimestampに指定
  # typeには常に 'post' を格納することで、全Postをtimestampでソートして取得する
  @key(name: "SortByTimestamp", fields:["type", "timestamp"], queryField: "listPostsSortedByTimestamp")
{
  type: String!
  id: ID
  content: String!
  owner: String
  timestamp: AWSTimestamp!
}
```

こうしておくと、次のコードのようにソート指定をしたFetchが可能となります。

```javascript
const res = await API.graphql(graphqlOperation(listPostsSortedByTimestamp, {
  type: "post", // パーティションキーとして 'post'(固定) を指定
  sortDirection: 'DESC' // 降順指定
}));
```

### schema.graphqlを更新しても諸々反映されない

`amplify add api` を実行した時に、プロジェクト直下に `schema.graphql` を作成するケースが多いのではないでしょうか？

後々になって、テーブルを追加する必要が出てきたりディレクティブを追記したりする場合に、このファイルを更新して `amplify update api` しても反映されません。

なぜなら、 `amplify/backend/api/{リソース名}/schema.graphql` が実際に構築されたGraphQL APIのスキーマとして読み込まれているためです。更新するときはこちらを更新して、 `amplify update api` するようにしましょう。

![schema_graphql_—_tatami-fm-music-library.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/ad612b47-2113-69a4-960b-3f9265abcb4e.png)

### createdAt、updatedAtをスキーマから消したい

DynamoDBにAppSync以外のリソースから値を書き込む場合など、`createdAt` と `updatedAt` が邪魔になるシチュエーションがあります。

そんなときは `@model(timestamps: null)` を指定することで、`createdAt` と `updatedAt` を省略できます。

```graphql
type Program
  @model(timestamps: null) # createdAtとupdatedAtを削除
  @key(name: "byStation", fields: ["stationId", "startedAt"]) {
  id: ID!
  name: String!
  startedAt: AWSTimestamp!
  endAt: AWSTimestamp!
  stationId: ID!
  station: Station @connection(fields: ["stationId"])
}
```

## Hosting

### 開発、ステージング、本番のインフラを分けたい

Amplifyで作成したプロジェクトは環境を複数作成して、切り替えることが可能です。環境は `amplify env add` することで追加できます。

![AWS_Amplify_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/3fe1d0e0-09a6-9b77-9a6f-d05d67d626be.png)

[Amplify SNS Workshop](https://amplify-sns.workshop.aws/ja/70_multi_env/00_add_env.html) で構築手順が詳細に記載されていますので、こちらを参照ください。

### hostingしたNext.jsのSPAが「403 Access Denied」となる

![https _qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_60996_1dc862a3-7cd8-e51b-0ad8-6b91c889fdfd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/50d2f549-22d0-0d59-f315-2ffb2103b7ef.png)

こちらの記事で解説しています。

[AWS AmplifyでhostingしたNext.jsのSPAが「403 Access Denied」となったときの対処法](https://qiita.com/Y_uuu/items/7111a6728f3acdcf8a8d)

### 先にCI/CDを構築して後からauthやapiといったカテゴリのリソースを追加するとフロントエンドでエラーが発生する

CI/CD構築後にauthやapiといったバックエンドのリソースを追加すると、CI/CDでバックエンドリソースが構築されず、デプロイしたフロントエンド側でエラーが発生します。

推測ですが、Amplify hostingのCI/CDのビルド設定(`amplify.yml`)は設定時点で、バックエンドのリソースが存在するかどうかを自動判定して、記述を変更しているようです。先にCI/CDを設定すると、バックエンドリソースは不要と判断して、ビルドが省略されます。

[Next.js+TypeScript+AWS Amplify+RecoilでToDoリストを作る](https://qiita.com/Y_uuu/items/249dfcc794f101129b09#%E3%83%9B%E3%82%B9%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0cicd%E5%90%AB%E3%82%80)に記載したとおり、`amplify.yml`を本来の形に書き換えることで、エラーを回避できます。

## Function

### ランタイムにPythonを指定するとCI/CDがコケる

Amplify側で指定したPythonのバージョンと、ビルド時に使用しているAmazon Linux2のPythonバージョンが異なることが原因です。

[AWS AmplifyでPythonのFunctionをCI/CDするとbuildに失敗する問題の対処方法](https://qiita.com/Y_uuu/items/8867bfa3a13ea3406d89)に記載したとおり、`amplify.yml` を書き換えてPythonを再インストールすることで回避できます。

### 定期的にタスクを実行したい

`amplify add function` する時に質問される `Do you want to invoke this function on a recurring schedule?` という質問に `Yes` と答えることで、LambdaのトリガーにCloudWatchEventが指定されます。

```bash
$ amplify add function
Scanning for plugins...
Plugin scan successful
? Select which capability you want to add: Lambda function (serverless function)
? Provide a friendly name for your resource to be used as a label for this category in the project: test
? Provide the AWS Lambda function name: test
? Choose the runtime that you want to use: Python
Only one template found - using Hello World by default.
? Do you want to access other resources in this project from your Lambda function? No
? Do you want to invoke this function on a recurring schedule? Yes
? At which interval should the function be invoked: Minutes
? Enter the rate in minutes: 3
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? No
Successfully added resource test locally.
```

## その他

### Amplifyの基本的な使い方をマスターしたい

はじめてAWS Amplifyを使うのであれば、[Amplify SNS Workshop](https://amplify-sns.workshop.aws/ja/)を1周することをお勧めします。

一方、このWorkshopだとカバーできていない部分があったり、使用しているパッケージのバージョンが一部古かったりという問題もあるので、他のチュートリアルもお勧めします。[watilde/awesome-aws-amplify-ja](https://github.com/watilde/awesome-aws-amplify-ja) にて日本語で書かれたチュートリアル記事がまとめられているので参考にしてみてください。

### Categoryとして提供されていないAWSリソースを追加したい

Amplifyとして提供しているカテゴリ(authやapi)だけでは、構築したいシステムの要件を満たせないことがあります。

そんなときには [Custom CloudFormation stacks](https://docs.amplify.aws/cli/usage/customcf) を定義することで、AWSのリソースを自由にAmplifyのプロジェクトに組み込むことができます。もちろん、他のカテゴリからパラメータを渡すこともできます。例えば次のスライドに書いているような、apiで作ったDynamoDBのテーブル名をCustom CloudFormation stacksに渡してDynamoDBに値を書き込むIoT CoreのACTを定義する、といった使い方ができます。

https://speakerdeck.com/yuuu/aws-amplifytomockmockdeiotbatukuendowosupideinigou-zhu-suru?slide=25
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/0d7cb67f-8062-34ac-7e09-1676cc51334a.png)

### team-provider-info.jsonって何者？

`team-provider-info.json` にはAmplifyのプロジェクトで構築したリソースのARNなどが記載されています。IAMのSecret Access Keyなどが記載されているわけではないので、直接的にセキュリティリスクがある訳ではありませんが、AccountIdが記載されていることを踏まえるとあまりpublicにすべきではありません。

次のスライドにも記載されている通り、`プライベートリポジトリの場合以外は.gitignoreに追加する` ようにしましょう。

https://speakerdeck.com/jaguar_imo/amplify-cli-deep-dive-awsdevday-2020?slide=7

### 上記で解決できない・よくわからない問題が起こっている

残念ながら、Amplifyを使っていると上記に挙がっていない問題にぶつかることはあると思われます。自分自身は以下のような方法で対処するようにしています。

#### エラーメッセージを理解する

Amplifyに限った話ではないですが、Amplifyのエラーメッセージには問題の原因や対処方法が記載されています。まずはこれを読んで理解します。

内容があまり理解できなかったとしても「Amplify + エラーメッセージから抜き出したいくつかのキーワード」で検索すると、似たような現象がGitHub Issuesで見つかるケースが多いです。

#### Amplify CLI/Frameworkのアップグレードをする

Amplifyは開発が活発で、CLI/Frameworkが頻繁にバージョンアップされます。公式ドキュメントに書かれているような基本的な操作が上手くいかない場合は、CLI/Frameworkをバージョンアップすることで問題を解決できるかもしれません。

#### AmplifyのGitHub Issuesを検索する

先にも挙げた通り、AmplifyのGitHub Issuesにはさまざまな事例が挙がっています。大抵の問題はIssuesを読んで解決するケースが多いのでぜひ参照してください。

https://github.com/aws-amplify/amplify-js/issues

## まとめ

記事を書いて、AWS Amplifyはバックエンドを気にせずフロントエンド開発に注力できる環境を提供してくれる嬉しいサービスだなと改めて思いました。加えて、バックエンド周りも細かいカスタマイズに対応できるようになっている点が好印象です。

2021年は弊社の業務でのAmplifyの導入事例を、もっと世の中に発信できるよう精進します。