Amplify

AWS Amplify で TypeScript + Next.js + GraphQLの環境を構築してみる

AWS Amplify のチュートリアルを参考に、Next.js + GraphQLの環境を構築してみた際のメモです。

参考) AWS Amplify のチュートリアル

前準備

以下のライブラリが必要。

  • Node.js v12.x 以上
  • npm v5.x or 以上
  • git v2.14.1 以上

チュートリアルでは↑の条件だが、yarn + typescript + ts-node + typesync を入れておく

AWSアカウントがない場合は新規で作る。

Amplify CLI のインストール と configure

the Amplify CLI は global にインストールする必要がある

$ npm install -g @aws-amplify/cli

CLI をインストールできたら amplify configure コマンドを叩いて IAMユーザを作成する

$ amplify configure

下記のように対話式で設定する

Follow these steps to set up access to your AWS account:

Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue

Specify the AWS Region
? region:  ap-northeast-1
Specify the username of the new IAM user:
? user name:  amplify-hoge
Enter the access key of the newly created user:
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
This would update/create the AWS Profile in your local machine
? Profile Name:  amplify-hoge

Successfully set up the new user.

以下、注意点

  • IAM権限は最低限に絞ること(AdminstratorAccessにはしない)

↓ 具体的な手順は下記動画を参考にする

プロジェクトをセットアップ

Next.js のアプリケーションを作成

GitHub 等でレポジトリを作成しておく
Yarn を使って TypeScript でアプリケーションを作成する

$ yarn create next-app hoge-app --typescript
$ cd hoge-app
$ git init
$ git remote add origin hogehoge
$ git push -u origin main

下記コマンドを叩いてアプリケーションを起動する

yarn dev

Amplify の バックエンド環境を初期化

Amplify のバックエンド環境を初期化する

以下、注意点

? Distribution Directory Path は out を指定すること
→ SSG のみの場合だけ上記
→ SSRやISRを使う場合は下記

  • ? Distribution Directory Path は .next を指定すること
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project hoge
The following configuration will be applied:

Project information
| Name: hoge
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: react
| Source Directory Path: src
| Distribution Directory Path: build
| Build Command: npm run-script build
| Start Command: npm run-script start

? Initialize the project with the above configuration? No
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path:  out
? Distribution Directory Path: build
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use amplify-hoge
$ git add .
$ git commit -m "amplify init"

Amplify のライブラリをインストール

yarn add aws-amplify @aws-amplify/ui-react
$ git add .
$ git commit -m "yarn add aws-amplify"

APIとデータベースの接続

GraphQL API と データベースの作成

amplify add api コマンドを叩くと、AppSync を使った GraphQL APIDynamoDB を作成できる

$ amplify add api
? Please select from one of the below mentioned services:
# GraphQL
? Provide API name:
# nextamplified
? Choose the default authorization type for the API:
# API key
? Enter a description for the API key:
#
? After how many days from now the API key should expire (1-365):
# 7
? Do you want to configure advanced settings for the GraphQL API?
#  Yes, I want to make some additional changes.
? Configure additional auth types?
# Yes
? Choose the additional authorization types you want to configure for the API:
# Amazon Cognito User Pool
? Do you want to use the default authentication and security configuration?
# Default configuration
? How do you want users to be able to sign in?
# Username
? Do you want to configure advanced settings?
# No, I am done.
? Enable conflict detection?
# No
? Do you have an annotated GraphQL schema?
# No
? Choose a schema template:
# Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now?
# Yes

GraphQL スキーマを編集する
amplify/backend/api/nextamplified/schema.graphql

type Post
@model
@auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) {
  id: ID!
  title: String!
  content: String!
}
amplify push
$ git add .
$ git commit -m "amplify add api"
Current Environment: dev

| Category | Resource name         | Operation | Provider plugin   |
| -------- | --------------------- | --------- | ----------------- |
| Auth     | nextamplifiedXXXXXXX  | Create    | awscloudformation |
| Api      | nextamplified         | Create    | awscloudformation |
? Are you sure you want to continue? Y

# You will be walked through the following questions for GraphQL code generation
? Do you want to generate code for your newly created GraphQL API? Y
? Choose the code generation language target: javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions: src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions? Y
? Enter maximum statement depth [increase from default if your schema is deeply nested]: 2

Amplify のステータスをチェック

amplify status

SSR を実装していく

↓ TypeScript で書くとこんな感じ

import { Amplify, API, Auth, withSSRContext } from "aws-amplify";
import { AmplifyAuthenticator } from "@aws-amplify/ui-react";
import { GRAPHQL_AUTH_MODE, GraphQLResult } from '@aws-amplify/api'
import type { NextPage } from 'next'
import { GetServerSideProps } from "next";
import Head from "next/head";
import awsExports from "../src/aws-exports";
import { createPost } from "../src/graphql/mutations";
import { listPosts } from "../src/graphql/queries";
import styles from "../styles/Home.module.css";
import Image from 'next/image'
import { CreatePostMutation, Post } from '../src/API'

Amplify.configure({ ...awsExports, ssr: true });

type Props = {
  posts: Post[]
}

const Home = ({ posts = [] }: Props): JSX.Element => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Amplify + Next.js</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Amplify + Next.js</h1>

        <p className={styles.description}>
          <code>{posts.length}</code>
          posts
        </p>

        <div className={styles.grid}>
          {posts.map((post) => (
            <a className={styles.card} href={`/posts/${post.id}`} key={post.id}>
              <h3>{post.title}</h3>
              <p>{post.content}</p>
            </a>
          ))}

          <div className={styles.card}>
            <h3 className={styles.title}>New Post</h3>

            <AmplifyAuthenticator>
              <form onSubmit={handleCreatePost}>
                <fieldset>
                  <legend>Title</legend>
                  <input
                    defaultValue={`Today, ${new Date().toLocaleTimeString()}`}
                    name="title"
                  />
                </fieldset>

                <fieldset>
                  <legend>Content</legend>
                  <textarea
                    defaultValue="I built an Amplify app with Next.js!"
                    name="content"
                  />
                </fieldset>

                <button>Create Post</button>
                <button type="button" onClick={() => Auth.signOut()}>
                  Sign out
                </button>
              </form>
            </AmplifyAuthenticator>
          </div>
        </div>
      </main>
    </div>
  );
}

export default Home

export const getServerSideProps: GetServerSideProps = async (context) => {
  const SSR = withSSRContext({ req: context.req });
  const response = await SSR.API.graphql({ query: listPosts });

  return {
    props: {
      posts: response.data.listPosts.items,
    }, 
  };
}

export const handleCreatePost = async (event: React.FormEvent<HTMLFormElement>) => {
  event.preventDefault();

  const form = new FormData(event.currentTarget);

  try {
    const { data } = await API.graphql({
      authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
      query: createPost,
      variables: {
        input: {
          title: form.get("title"),
          content: form.get("content"),
        },
      },
    }) as GraphQLResult<CreatePostMutation>;

    if (data && data.createPost) {
      window.location.href = `/posts/${data.createPost.id}`;
    }
  } catch ({ errors }) {
    console.error(...errors);
    throw new Error(errors[0].message);
  }
}

SSG を実装していく

import { Amplify, API, withSSRContext } from "aws-amplify";
import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api'
import Head from "next/head";
import { useRouter } from "next/router";
import { GetStaticProps, GetStaticPaths } from 'next';
import awsExports from "../../src/aws-exports";
import { deletePost } from "../../src/graphql/mutations";
import { getPost, listPosts } from "../../src/graphql/queries";
import styles from "../../styles/Home.module.css";
import { Post } from '../../src/API'

Amplify.configure({ ...awsExports, ssr: true });

type Props = {
  post: Post
}

const PostPage = ({ post }: Props): JSX.Element => {
  const router = useRouter();

  if (router.isFallback) {
    return (
      <div className={styles.container}>
        <h1 className={styles.title}>Loading&hellip;</h1>
      </div>
    );
  }

  async function handleDelete() {
    try {
      await API.graphql({
        authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
        query: deletePost,
        variables: {
          input: { id: post.id },
        },
      });

      window.location.href = "/";
    } catch ({ errors }) {
      console.error(...errors);
      throw new Error(errors[0].message);
    }
  }

  return (
    <div className={styles.container}>
      <Head>
        <title>{post.title} – Amplify + Next.js</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>{post.title}</h1>

        <p className={styles.description}>{post.content}</p>
      </main>

      <footer className={styles.footer}>
        <button onClick={handleDelete}>💥 Delete post</button>
      </footer>
    </div>
  );
}

export default PostPage;

export const getStaticPaths: GetStaticPaths = async () => {
  const SSR = withSSRContext();
  const { data } = await SSR.API.graphql({ query: listPosts });
  const paths = data.listPosts.items.map((post: Post) => ({
    params: { id: post.id },
  }));

  return {
    fallback: true,
    paths,
  };
}

export const getStaticProps: GetStaticProps = async (context) => {
  const SSR = withSSRContext();
  const { params } = context;
  const { data } = await SSR.API.graphql({
    query: getPost,
    variables: {
      id: params?.id,
    },
  });

  return {
    props: {
      post: data.getPost,
    },
  };
}
$ git add .
$ git commit -m "トップと投稿ページを実装"

デプロイとホスティング

CI/CD の設定

$ amplify add hosting
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Continuous deployment (Git-based deployments)
? Continuous deployment is configured in the Amplify Console. Please hit enter once you connect your repository

本番環境の追加

$ amplify env list

$ amplify env add

$ amplify env list
$ amplify env checkout prd

$ amplify push

Amplify で SSR や ISR をデプロイする

https://docs.aws.amazon.com/amplify/latest/userguide/server-side-rendering-amplify.html

↑ のAmplify ドキュメントに詳しく書いてある

Deploying a Next.js SSR app with Amplify

Amplify は package.json ファイルから SSR なのか SSG なのか検知するとのこと

build script が next build なら SSG と SSR 両方サポートされる

next build && next export と設定してしまうと、SSGのみしかサポートされないことに注意が必要(静的ページしかなければこっちでOK)

たぶん、S3やLamnbdaとかをつかってSSR側の仕組みを作るので、SSRサポートの有無でかなりかわってくるからであろう

Amplify build settings

package.json から レンダリング方式を検知すると、amplify.yml のビルド設定ファイルがチェックされる

SSR app かつ amplify.yml がなければ、.next が生成される

以下が SSR と SSG をサポートするための amplify.yml の例

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*

デプロイしてもエラーになってしまう場合

  1. package.json で 静的エクスポートコマンドを追加する

next buildnext build & next export にする
outディレクトリに静的ファイルが吐き出されるようになる

  "scripts": {
    "dev": "next dev",
    "build": "next build & next export",
  1. amplify init したときの Distribution Directory Path を変える

build じゃなくて 1.で吐き出されるようになった out ディレクトリを指定する

  1. amplify hosting でのビルド設定を変える

Consoleにあるビルドの設定を編集して、artifacts > baseDirectory を .nextout に変える

  1. Image Component を使わない

Error: Image Optimization using Next.js' default loader is not compatible with next export.

調べてみると、Amplify は SSG で Image Component は非対応とのこと

Image Component を imgタグに置き換える

振り返りメモ

Amplify のよかったところ

  • GraphQL とかをカンタンに設定できる

Amplify の悪いところ

  • Vercel と比べると管理画面、ビルド最適化、デプロイ方式等で劣る
  • 特にSSGまわりが弱い

参考

-Amplify