💡

Amplify Gen2+Next.js+devContainer+認証+データとか諸々試してみた

2024/06/25に公開

はじめに

最近AmplifyのGen2が出て、開発体験が良くなったらしいので試してみた内容をまとめた記事です。

この記事で書いてあることは以下のとおりです。

  • devContainerでNext.js+amplifyの開発環境
  • GitHub連携でAmplifyHostingへのデプロイ
  • 本番環境、検証環境、ローカル環境それぞれ開発手順とデプロイ確認
  • amplify(DynamoDB)とNext.jsを使ったCRUD実装
  • amplify(Cognito)とNext.jsを使ったログイン機能実装
  • サイトにBasic認証を追加

実装したリポジトリはこちらに置いておきます。

https://github.com/koichi-menta/amplify-gen2-example-menta

リポジトリを作成してdevContainerで環境構築

まず、GitHubでリポジトリを作成してclone。

Next.js + TypeScript + App Routerの環境を構築

npx create-next-app@latest .

.devcontainerフォルダを作成。

.devcontainerフォルダ内にDockerfileファイルを作成。
下記を記載。

.devcontainer/Dockerfile
FROM node:20-alpine

# Install dependencies
RUN apk update && apk add curl git openssh bash

# Install aws cli
RUN apk add --no-cache aws-cli

.devcontainerフォルダの中にdevcontainer.jsonファイルを作成。

以下を記載。extensionsはお好みで。

.devcontainer/devcontainer.json
{
  "name": "Existing Dockerfile",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "forwardPorts": [3000],
  "customizations": {
    "vscode": {
      "settings": {
        "editor.formatOnSave": true
      },
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "mhutchie.git-graph",
        "PKief.material-icon-theme",
        "eamodio.gitlens"
      ]
    }
  }
}

vscodeの左下の青いアイコンをクリックして、

「Reopne in Container」をクリック

左下の青いエリアがこんな感じになればOK。

vscodeのターミナルで下記を実行。

npm run dev

ローカルホストでNext.jsの初期画面が表示される。

次に、GitHubでコードを管理するためにgitのssh設定をする

~/.sshで実行コマンドを実行

ssh-keygen -t rsa

~/.sshでconfigファイルを作成して下記を記載

Host github github.com
  HostName github.com
  IdentityFile ~/.ssh/id_rsa
  User git

GitHubのSSHのキー設定をする

下記コマンドで疎通確認

ssh -T github.com

変更コードを全てプッシュする。

環境構築done.

AmplifyHostingでデプロイ

AWSのAmplifyにアクセスして「新しいアプリを作成」をする

GitHubのアクセス許可を設定

対象のブランチを選択する

ブランチはmainもしくはmaster

アプリケーションの設定は全部初期設定でOK。

確認画面が表示されるので「保存してデプロイ」する

デプロイ済みになったら「ドメイン」のリンクをクリックして確認すると、next.jsの初期ページが表示されている。

開発ブランチ作成とデプロイ

開発環境用に新しくdevelopブランチを作成してそのままプッシュ。

AWSのAmpifyの左にある「アプリケーションの設定」>「ブランチ設定」にアクセス。

「ブランチの追加」ボタンを押す

接続するブランチに先ほどプッシュしたdevelopを選択して「ブランチを追加」する

Ampifyの概要を見ると、設定したブランチのソースコードでデプロイが走る。

開発環境のdevelopにコードをマージしてデプロイを確認するために、featureブランチを作って編集したコードをプッシュしてしてdevelopにマージしてみる。

feature/hogeブランチを作成して、page.tsxのコードを編集。

mainタグの中身をhello world.にだけにしてみる。

<main className="flex min-h-screen flex-col items-center justify-between p-24">
  Hello World.
</main>

プッシュしてPR(プルリクエスト)を作成してdevelopにマージする。

マージするとAmplifyのdevelopブランチが自動でビルドしてデプロイしてくれる。

デプロイが完了したらdevelopのドメインをクリックして変更を確認。

Hello World.になってる。

ここでmainブランチのドメインもクリックして確認してみる。

またnext.js初期設定のページになっている。

GitHubでdevelopをmainにマージするPRを作成してマージ。

Amplifyのmainブランチのデプロイが走る。

デプロイが完了したらmainブランチのドメインをクリックして確認してみると、developの内容が反映されてデプロイが完了している。

Amplify Gen2を使う環境を整える

公式ドキュメントにあるローカル環境を整える手順を進める。

https://docs.amplify.aws/react/start/account-setup/

1. Create user with Amplify permissions(Amplify権限を持つユーザーを作成する)

書かれてる通りに進める。

2. Create password for user(ユーザーのパスワードを作成する)

書かれてる通りに進める。

3. Install the AWS CLI(AWS CLIをインストールする)

この手順はスキップしてください。

DockerfileでAWS CLIをインストールする設定を書いているので、既に開発環境にインストールされている状態です。

4. Set up local AWS profile(ローカルAWSプロファイルを設定する)

ドキュメントに書かれているaws configure ssoを実行すると、色々入力を求められますが、手順1の最後に表示された情報を元に入力を進めていきます。

2つ目に聞かれるSSO region: は、ドキュメントには<YOUR START SESSION URL>とありますが、regionを聞かれているのでregionの情報を入力すれば大丈夫です。

無事入力を進めていくとブラウザでログインを求められるのでログインをします。

ログイン後にも設定の入力が求められますが、私の場合下記情報は既に入力?されている状態になっていました。

The only AWS account available to you is:
Using the account ID 
The only role available to you is:
Using the role name

なので、続きのCLI default client Region [None]:から設定の続きを入力します。

aws s3 ls --profile defaultが表示されたら設定完了です。

Amplifyの環境構築

Quickstartでは既に環境が整っているのですが、実際に導入しようとするとおそらく手動で設定する必要が出てくるので試してみます。

下記コマンドを実行。

npm create amplify@latest

実行が完了するとamplifyフォルダが作成されて中にいろんなファイルが作られている。

下記コマンドを実行してサンドボックス環境を作成する。

npx ampx sandbox

サンドボックス環境が作成されると、プロジェクトのルートにamplify_outputs.jsonファイルが作成されます。

サンドボックス環境が作成されると常に監視状態になり、amplifyフォルダの中を編集すると自動でサンドボックス環境が更新されます。

この時点で認証(Cognito)とデータベース(DynamoDB)の作成ができてる状態です。

サンドボックスの情報はAWSのCloudFormationを見ると色々書いてあります。

例えば、CloudFormationの出力タブを見てみると、userPoolIdawsAppsyncApiIdの値が設定されています。

AWSのCognitoを開いて、userPoolIdに書かれている値を探してみるとその値でユーザープールが作成されているのがわかります。

また、AWSのDynamoDBを開いて「項目を検索」を見てみると、awsAppsyncApiIdに書かれている値でTodoのテーブルが作成されているのがわかります。

このTodoのテーブルはamplifyフォルダにあるamplify/data/resource.tsに書かれている定義で作成されています。

ログイン機能やデータのCRUD機能を実装すると、それぞれこのsandboxに書かれている場所に登録されます。

CRUDを実装

Next.jsからCRUD操作する実装をします。

基本的には公式ドキュメントの下記URLを元に進めていきます。
https://docs.amplify.aws/react/build-a-backend/data/set-up-data/
https://docs.amplify.aws/react/build-a-backend/data/connect-to-API/

まず、今の段階ではauthの設定はしないので、一旦amplify/backend.tsのauthをコメントアウトする。

amplify/backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";

defineBackend({
  // auth,
  data,
});

amplify/data/resource.tsの下記のように書き換える

amplify/data/resource.ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.publicApiKey()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "apiKey",
    apiKeyAuthorizationMode: { expiresInDays: 30 },
  },
});

page.tsxを下記コードに変更。

page.tsx
"use client";

import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import { useEffect, useState } from "react";

Amplify.configure(outputs);

const client = generateClient<Schema>();

type TodoFormProps = {
  todo: Schema["Todo"]["type"];
  handleUpdate: (string: string) => void;
  handleDelete: () => void;
};
const TodoForm = ({ todo, handleUpdate, handleDelete }: TodoFormProps) => {
  const [input, setInput] = useState<string>("");

  useEffect(() => {
    setInput(todo.content || "");
  }, []);

  return (
    <div className="flex">
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
      />
      <div className="flex gap-1">
        <button
          onClick={() => handleUpdate(input)}
          className="border-[1px] border-gray-500 px-2"
        >
          更新
        </button>
        <button
          onClick={handleDelete}
          className="border-[1px] border-gray-500 px-2"
        >
          削除
        </button>
      </div>
    </div>
  );
};

export default function Home() {
  const [todos, setTodos] = useState<Schema["Todo"]["type"][]>([]);
  const [input, setInput] = useState<string>("");

  const handleGetTodos = async () => {
    const { data: todos } = await client.models.Todo.list();
    setTodos(todos);
  };

  const handleCreate = async () => {
    await client.models.Todo.create({
      content: input,
    });
    setInput("");
    await handleGetTodos();
  };

  const handleDelete = async (id: string) => {
    await client.models.Todo.delete({
      id,
    });
    handleGetTodos();
  };

  const handleUpdate = async (id: string, content: string) => {
    await client.models.Todo.update({
      id,
      content,
    });
  };

  useEffect(() => {
    (async () => {
      await handleGetTodos();
    })();
  }, []);

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="flex gap-1">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
        />
        <button
          onClick={handleCreate}
          className="border-[1px] border-gray-500 px-2 "
        >
          登録
        </button>
      </div>
      {todos.map((todo) => {
        return (
          <div key={todo.id}>
            <TodoForm
              todo={todo}
              handleUpdate={(content) => handleUpdate(todo.id, content)}
              handleDelete={() => handleDelete(todo.id)}
            />
          </div>
        );
      })}
    </main>
  );
}

このままだとamplify_outputs.jsonをimportする時にエラーがでるので、tsconfig.jsonpathを下記のコードに修正。

tsconfig.json
"paths": {
  "@/*": ["./*"]
}

ローカル環境で動作確認をしてみると、こんな感じになってると思います。

sandboxのDynamoDBを確認してみると、データが登録されています。

これをdevelopの環境にデプロイしたいので、バックエンドをビルドするためのamplify.ymlファイルを作成します。

プロジェクトのルートに作成して下記の内容を記載。

amplify.yml
version: 1
backend:
  phases:
    build:
      commands:
        - npm ci --cache .npm --prefer-offline
        - npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
  phases:
    build:
      commands:
        - npm ci --cache .npm --prefer-offline
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - "**/*"
  cache:
    paths:
      - .next/cache/**/*
      - .npm/**/*
      - node_modules/**/*

ではこれらをプッシュしてPRを作成してdevelopにマージします。

デプロイが終わったらdevelopのドメインにアクセスしてTODOを登録すると、develop用のDynamoDBにデータが登録されるようになります。

ログイン機能を実装

認証も基本的に公式ドキュメント元に実装していきます。

https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/

先ほどコメントアウトしたamplify/backend.tsのauth部分のコメントを解除します。

backend.ts
import { defineBackend } from "@aws-amplify/backend";
import { auth } from "./auth/resource";
import { data } from "./data/resource";

defineBackend({
  auth,
  data,
});

amplifyで簡単にログインUIが作れる@aws-amplify/ui-reactをinstallする。

npm add @aws-amplify/ui-react

pate.tsxにログインのUIを実装する。
量が多くなるので一部コードを省略しているので追加の箇所をコピペしてください。

page.tsx
"use client";

import { generateClient } from "aws-amplify/data";
import type { Schema } from "@/amplify/data/resource";
import { Amplify } from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import { useEffect, useState } from "react";
// 追加
import { Authenticator } from "@aws-amplify/ui-react";
// 追加
import "@aws-amplify/ui-react/styles.css";

Amplify.configure(outputs);

const client = generateClient<Schema>();

...省略

export default function Home() {
  ...省略

  return (
    // 追加
    <Authenticator>
      {({ signOut, user }) => (
        <main className="flex min-h-screen flex-col items-center justify-center p-24 gap-5">
          // 追加
          <h1>Hello {user?.username}</h1>

          ...省略

          // 追加
          <button
            onClick={signOut}
            className="w-[100px] border-[1px] border-gray-500 px-2"
          >
            Sign out
          </button>
        </main>
      )}
    </Authenticator>
  );
}

実装するとこんなログイン画面が表示されてメールアドレスでアカウント登録、ログインができるようになります。

では、コードをプッシュしてdevelopにマージします。

デプロイが完了したらdevelopのドメインを確認すると、ローカルで開発していた時と同じ見た目に変わっていると思います。

同じようにdevelop用のアカウントを作成するとログインできるようになります。

Basic認証をつける

ホスティングメニューのアクセスコントロールから「アクセス管理」ボタンをクリック

Basic認証をつけたいブランチに「アクセス設定」を「制限」にしてユーザーネームをパスワードを設定して保存する。

保存したらサイトにアクセスしてBasic認証がついてることを確認しましょう。

おわりに(感想)

思ったより簡単にホスティング、デプロイ、バックエンド連携ができて感動しました。

バックエンドのテーブル定義もTypeScriptで型定義をしてるみたいにできるので、かなり直感的で楽だなぁと感じました。

AmplifyのイメージもGen2で結構変わりました。

Gen1はteam-provider-info.jsonの扱いがなんかややこしいイメージがありましたが、Gen2のamplify_outputs.jsonは分かりやすくなりました。

また、Gen1はいちいち環境を確認してampify pushするのがめんどくさかったのですが、Gen2ではsandbox環境を起動するだけなので煩わしさが減って開発に集中できる気がします。

他には、自分のローカルで環境を構築した時はいろんなエラーに悩まされましたが、devContainerを使って1から環境を構築することで、シンプルに環境が作れました。

あとは複数人で開発をする時にどう進めると良いかが気になりますね。

次はバックエンドをRDSと連携してみたいなと思っています。

https://aws.amazon.com/jp/blogs/news/new-in-aws-amplify-integrate-with-sql-databases-oidc-saml-providers-and-the-aws-cdk/

Discussion