📣

CDKユーザーに伝えたい! Amplify Gen2の拡張性

2024/04/26に公開

はじめに

こんにちは!
犬専用の音楽アプリ オトとりっぷでエンジニアしています、足立です!

https://www.oto-trip.com/

この記事では、AWS CDK を用いた AWS Amplify Gen2 の拡張性に焦点を当てたいと思います。

目標

Cognitoで認証されたユーザーの画像をS3にUpLoadしたイベントによってLambdaを起動するそんな構成、よくありますよね。こちらを Amplify Gen2 で簡単に作っていこうと思います。

ちなみに Amplify Gen2 とは、Amplify の次世代開発体験のことです。
(詳しくは拙著をご覧ください)

https://zenn.dev/ototrip/articles/tech-nextjs-amplify-1

Gen1 との大きな違いは、リソースのデプロイ方法が CLI から CDK に変更されたことです。
これにより Gen2 では、Amplify で作成したリソースを CDK で拡張することが非常に容易になりました。

また、AWS CDK については、公式ドキュメントをご覧ください。

Amplify Gen2 のセットアップ

先ほどの拙著、もしくは公式 Quickstartに記載がある通り、Sandbox 環境構築まで済ませてしまいます。

Next.js の App Router で初期設定を済ませた場合以下のようなファイル構成になると思います。

├── amplify/
│   ├── auth/
│   │   └── resource.ts
│   ├── data/
│   │   └── resource.ts
│   ├── backend.ts
│   └── package.json
│
├── app/
│   ├── favicon.ico
│   ├── layout.tsx
│   └── page.tsx
│

amplify/authamplify/dataは今回出番がないので、デフォルト設定のままです。

amplify/auth/resource.ts
amplify/auth/resource.ts
import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    email: true,
  },
});
amplify/data/resource.ts
amplify/data/resource.ts
import { a, defineData, type ClientSchema } from '@aws-amplify/backend';

const schema = a.schema({
  User: a
    .model({
      name: a.string(),
      imageKey: a.string(),
    })
    .authorization((allow) => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});
amplify/backend.ts
amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';

const backend = defineBackend({
  auth,
  data,
});

app以下は Login して Amplify の各種機能を呼び出せるようにしておきます。

app/page.tsx
app/page.tsx
'use client';

import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';

function App({ signOut }: { signOut: any }) {
  return (
    <>
      <h1>Hello, Amplify 👋</h1>
      <button onClick={signOut}>Sign out</button>
    </>
  );
}

export default withAuthenticator(App);
app/layout.tsx
app/layout.tsx
import ConfigureAmplifyClientSide from '@/components/ConfigureAmplify';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <ConfigureAmplifyClientSide />
        {children}
      </body>
    </html>
  );
}

Storage カテゴリの追加

次に Storage カテゴリを追加していきます。
Amplify の Storage カテゴリとは、バックエンドのS3作成 + 認証周りの設定をよしなにしてくれるものです。

amplify以下に storage を追加します。

├── amplify/
│   ├── auth/
│   │   └── resource.ts
│   ├── data/
│   │   └── resource.ts
+│   ├── storage/
+│   │   └── resource.ts
│   ├── backend.ts
│   └── package.json
│
├── app/
│   ├── favicon.ico
│   ├── layout.tsx
│   └── page.tsx
│
amplify/storage/resource.ts
import { defineStorage } from '@aws-amplify/backend';

export const storage = defineStorage({
  name: 'myProjectFiles',
  access: (allow) => ({
    'public/*': [
      allow.guest.to(['read']),
      allow.authenticated.to(['read', 'write', 'delete']),
    ],
    'protected/{entity_id}/*': [
      allow.authenticated.to(['read']),
      allow.entity('identity').to(['read', 'write', 'delete']),
    ],
    'private/{entity_id}/*': [
      allow.entity('identity').to(['read', 'write', 'delete']),
    ],
  }),
});
amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
+import { storage } from './storage/resource';

const backend = defineBackend({
  auth,
  data,
+  storage,
});

Amplify Gen2 になって Storage への認証パターンを自分で設定することが可能になりました。今回は、Amplify UI ライブラリを利用したいので、Gen1 と同じパターンにしています。
詳しくは、公式ドキュメントをご覧ください。

これに合わせて、UI 側も画像を UpLoad できるように修正します。

app/page.tsx
'use client';

import { withAuthenticator } from '@aws-amplify/ui-react';
+import { StorageManager } from '@aws-amplify/ui-react-storage';
import '@aws-amplify/ui-react/styles.css';

function App({ signOut }: { signOut: any }) {
  return (
    <>
      <h1>Hello, Amplify 👋</h1>
      <button onClick={signOut}>Sign out</button>

+      <StorageManager
+        acceptedFileTypes={['image/*']}
+        accessLevel='protected'
+        maxFileCount={1}
+      />
    </>
  );
}

export default withAuthenticator(App);

ここまで修正すると、画像を S3 に UpLoad できるようになっていると思います。

CDK での拡張

次に、先ほど作成したリソースを CDK で拡張していきます。
amplify以下に custom を追加します。

├── amplify/
│   ├── auth/
│   │   └── resource.ts
+│   ├── custom/
+│   │   └── eventNotifications
+│   │          ├── function.ts
+│   │          └── resource.ts
│   ├── data/
│   │   └── resource.ts
│   ├── storage/
│   │   └── resource.ts
│   ├── backend.ts
│   └── package.json
│
├── app/
│   ├── favicon.ico
│   ├── layout.tsx
│   └── page.tsx
│

eventNotifications/resource.tsの中身は完全に CDK Construct です。

amplify/custom/eventNotifications/resource.ts
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { EventType, IBucket } from 'aws-cdk-lib/aws-s3';
import { LambdaDestination } from 'aws-cdk-lib/aws-s3-notifications';
import { Construct } from 'constructs';
import * as url from 'node:url';

type EventNotificationsProps = {
  storage: IBucket;
};

export class EventNotifications extends Construct {
  constructor(scope: Construct, id: string, props: EventNotificationsProps) {
    super(scope, id);

    const { storage } = props;

    const func = new NodejsFunction(this, 'function', {
      entry: url.fileURLToPath(new URL('function.ts', import.meta.url)),
      runtime: Runtime.NODEJS_20_X,
    });

    storage.addEventNotification(
      EventType.OBJECT_CREATED,
      new LambdaDestination(func)
    );
  }
}

backend.ts 側で EventNotifications Construct を設定します。
なんと defineBackend で作成した backend から Stack を作成し、拡張することが可能です!

amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
+import { EventNotifications } from './custom/eventNotifications/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';

const backend = defineBackend({
  auth,
  data,
  storage,
});

+const eventNotifications = new EventNotifications(
+  backend.createStack('EventNotifications'),
+  'EventNotifications',
+  { storage: backend.storage.resources.bucket }
+);

ちなみにeventNotifications/function.tsは Lambda で実行される関数本体です。
今回は、ただただログをはくだけにしておきます。

amplify/custom/eventNotifications/function.ts
import { Context, Handler, S3Event } from 'aws-lambda';

export const handler: Handler = async (event: S3Event, context: Context) => {
  console.log({ event, context });
};

sandbox を起動したままであれば、ファイル保存と同時に Lambda が AWS 上にデプロイされると思います。私の場合は、amplify-nextamplifygen2-a-EventNotificationsfuncti-<ランダム英数字>のような関数名になっていました。

ちゃんと S3 からのイベントも設定されていますね。

それでは、先ほど同様に画像を UpLoad してみます。
そうすると無事に Lambda が起動し、ログが吐かれているのが確認できると思います。

{
  event: { Records: [ [Object] ] },
  context: {
    callbackWaitsForEmptyEventLoop: [Getter/Setter],
    succeed: [Function (anonymous)],
    fail: [Function (anonymous)],
    done: [Function (anonymous)],
    functionVersion: '$LATEST',
    functionName: 'amplify-nextamplifygen2-a-EventNotificationsfuncti-',
    ...
  }
}

無事に Amplify で作成された S3 を CDK を用いて拡張することができました!

最後に

ここまで読んでいただきありがとうございました。
さらに詳しく知りたい方は、公式ドキュメントに詳細が記載されていますので、そちらも併せてご覧ください。

何だかすごい進化しましたね、AWS Amplify。
まだプレビュー段階なので、まだまだ進化を期待したいところです。
GA されればぜひオトとりっぷでも活用してみたいと思います。

もし犬専用の音楽アプリに興味を持っていただけたら、ぜひダウンロードしてみてください!

https://www.oto-trip.com/

Discussion