Nextjs＋Cognitoの検証用ローカル環境の作り方


:::note info
Cognito をローカルで検証してみたい人向けの記事です
:::

# 内容
- ### Docker
  - 仮想AWSサーバー(`motoサーバー`)の起動
  - 仮想AWSサーバーでCognitoの初期設定を行う 
    - ユーザープール作成
    - ユーザー作成
- ### Nextjs(App Router)
  - ログインフォームの表示
    - `aws-amplify` + `@aws-amplify/ui-react` 
  - 仮想Cognitoにログイン

# 環境構築

```PowerShell
mkdir local_nextjs_cognito
npx create-next-app@latest .
```

#### Nextjs の設定

```
Need to install the following packages:
create-next-app@15.3.2
Ok to proceed? (y)

√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like your code inside a `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to use Turbopack for `next dev`? ... No / Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No / Yes
√ What import alias would you like configured? ... @/*
```

#### フロントライブラリ追加

```powershell
npm i aws-amplify @aws-amplify/ui-react
```

ライブラリのバージョンを掲載しておきます
```package.json
  "dependencies": {
    "@aws-amplify/ui-react": "^6.11.2",
    "aws-amplify": "^6.14.4",
    "next": "15.3.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.3.2",
    "typescript": "^5"
  }
```

#### フォルダおよびファイルの作成
（ルートディレクトリで実行）
```PowerShell
mkdir .\src\aws
New-Item .\src\aws\CognitoProvider.tsx

mkdir aws_mock
New-Item .\aws_mock\init.sh
New-Item .\aws_mock\Dockerfile

New-Item docker-compose.yml
```

#### Cognitoログイン画面の実装
今回は検証のため`@aws-amplify/ui-react`で簡単に作ります

```CognitoProvider.tsx
"use client";
import React from "react";
import { Amplify } from "aws-amplify";
import "@aws-amplify/ui-react/styles.css";
import { withAuthenticator } from "@aws-amplify/ui-react";

Amplify.configure(
  {
    Auth: {
      Cognito: {
        userPoolId: process.env.NEXT_PUBLIC_MOTO_USER_POOL_ID!,
        userPoolClientId: process.env.NEXT_PUBLIC_MOTO_CLIENT_ID!,
        userPoolEndpoint: process.env.NEXT_PUBLIC_MOTO_ENDPOINT,
      },
    },
  },
  { ssr: true }
);

const CognitoProvider = ({ children }: { children: React.ReactNode }) => {
  return <>{children}</>;
};

export default withAuthenticator(CognitoProvider);

```

```layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import CognitoProvider from "@/aws/CognitoProvider"; // 追加

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ja">   {/**jaに変更 */}
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        <CognitoProvider>{children}</CognitoProvider> {/**追加 */}
      </body>
    </html>
  );
}

```

#### aws_mockフォルダ
`init.sh`の最後にパスワードを変更している理由：
> Cognitoでユーザーを作成した際、メール認証が必要になります
> モックサーバーのためメール送信ができません
> メール認証を回避するためにパスワード変更を行っています

```./aws_mock/Dockerfile
FROM alpine:3.19
WORKDIR /app
COPY init.sh .
RUN chmod +x init.sh
RUN apk --no-cache add curl jq

```


```./aws_mock/init.sh
#!/bin/sh

# curlコマンドをインストールして使えるようにする
apk --no-cache add curl
# jqコマンドをインストールして使えるようにする
apk --no-cache add jq

# ユーザープールの作成
RESPONSE=$(curl -s -X POST http://moto:5000/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: AWSCognitoIdentityProviderService.CreateUserPool' \
-H 'X-Amz-Date: 20240227T000000Z' \
-d '{
  "PoolName": "MyTestUserPool",
  "Policies": {
    "PasswordPolicy": {
      "MinimumLength": 8,
      "RequireUppercase": true,
      "RequireLowercase": true,
      "RequireNumbers": true,
      "RequireSymbols": true
    },
    "SignInPolicy": {
      "AllowedFirstAuthFactors": [
        "PASSWORD"
      ]
    }
  },
  "UsernameAttributes": [
    "email"
  ]
}')


# RESPONSEからUserPoolIDを取得する
USER_POOL_ID=$(echo $RESPONSE | jq -r '.UserPool.Id')

echo "User Pool ID: $USER_POOL_ID"

# UserPoolClientの作成
RESPONSE=$(curl -s -X POST http://moto:5000/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: AWSCognitoIdentityProviderService.CreateUserPoolClient' \
-H 'X-Amz-Date: 20240227T000000Z' \
-d '{
  "ClientName": "MyTestUserPoolClient",
  "UserPoolId": "'"$USER_POOL_ID"'",
  "GenerateSecret": false
}')

# RESPONSEからUserPoolClientIDを取得
USER_POOL_CLIENT_ID=$(echo $RESPONSE | jq -r '.UserPoolClient.ClientId')

echo "User Pool Client ID: $USER_POOL_CLIENT_ID"



# ユーザープールにユーザーを作成
RESPONSE=$(curl -s -X POST http://moto:5000/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: AWSCognitoIdentityProviderService.AdminCreateUser' \
-H 'X-Amz-Date: 20240227T000000Z' \
-d '{
  "UserPoolId": "'"$USER_POOL_ID"'",
  "Username": "test123@example.com",
  "TemporaryPassword": "Test@1234",
  "UserAttributes": [
    {
      "Name": "email",
      "Value": "test123@example.com"
    },
    {
      "Name": "email_verified",
      "Value": "true"
    }
  ],
  "MessageAction": "SUPPRESS"
}')

# RESPONSEからUserIDを取得
USER_ID=$(echo $RESPONSE | jq -r '.User.Username')

echo "User ID: $USER_ID"

# 抽出した各種IDを含む.env.localファイルを/appディレクトリ(ルートディレクトリ)に作成
cat <<EOF > /app/.env.local
NEXT_PUBLIC_MOTO_USER_POOL_ID=$USER_POOL_ID
NEXT_PUBLIC_MOTO_CLIENT_ID=$USER_POOL_CLIENT_ID
NEXT_PUBLIC_MOTO_ENDPOINT=http://127.0.0.1:5000
USER_ID=$USER_ID
USER_NAME=test123@example.com
USER_PASSWORD=Test@1234
EOF

echo ".env.local file created with the necessary environment variables in /app directory."


# ユーザーのパスワードを変更
RESPONSE=$(curl -s -X POST http://moto:5000/ \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: AWSCognitoIdentityProviderService.AdminSetUserPassword' \
-H 'X-Amz-Date: 20240227T000000Z' \
-d '{
  "UserPoolId": "'"$USER_POOL_ID"'",
  "Username": "test123@example.com",
  "Password": "Test@1234",
  "Permanent": true
}')

echo "User password set to permanent for user: test123@example.com"

```

#### docker-compose.ymlの設定
appとしてフロントエンドの記載をしていますが、Dockerを使わずに起動させることもできます
※初回起動が遅いので、ローカル起動を推奨
```docker-compose.yml
services:
  moto:
    image: motoserver/moto:latest
    ports:
      - "5000:5000"
    command: >
      -H 0.0.0.0
    environment:
      - MOTO_PORT=5000

  aws_mock_init:
    build:
      context: ./aws_mock
    volumes:
      - .:/app
    depends_on:
      - moto
    entrypoint: ["/bin/sh", "-c", "sleep 5 && ./aws_mock/init.sh"]

  # フロントエンドをローカル起動するなら以下のappは不要です
  app:
    build:
      context: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - WATCHPACK_POLLING=true
    command: npm run dev
    depends_on:
      - moto
      - aws_mock_init

```

## Dockerで実行
VSCodeなどのエディターで`Run All Service`実行（VSCodeの場合：拡張機能のDockerが必要）
一応コマンド例を掲載しておきます
```PowerShell
#1. サービスのビルドと起動
docker-compose up --build
#2. バックグラウンド（デタッチド）で起動
docker-compose up -d
#3. サービスの停止
docker-compose down
#4. サービスの再ビルド
docker-compose build
#5. 特定サービスのみ起動（例: app）
docker-compose up moto
#6. ログの確認
docker-compose logs
#7. 特定サービスのログ確認（例: moto）
docker-compose logs moto
```

Dockerで`moto`サーバーが起動している状態で、`aws_mock_init`を実行すると、
`.env.local`ファイル内に仮想環境のCognito接続用の各種IDが記載されます
ログイン用のIDとパスワードも記載してありますので、検証時にお使いください