[Amplify Gen2事例]合コンで自己紹介に困ってポートフォリオサイトを作成した話

# はじめに
AWS Amplify Gen2を使って自分のポートフォリオ？自己紹介サイトを作成しました。
今回はその経緯と、実装寄りの内容について記事にできたらと思います。
以下の画像のようなサイトです。
https://portfolio.namikia.net/

![スクリーンショット 2025-02-05 20.02.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3349905/dfb88785-9078-123a-484e-50971754f8c3.png)


全6ページ作っています。
1. ホーム... 簡単なキャリア紹介とヒーローレイアウトでそれっぽく
1. プロフィール... 趣味とか保有資格とか、仕事に対する考え方とか
1. Works
    1. プロダクト... 趣味も含めて作ったりしたもの
    1. 技術ブログ... Qiitaなど書き物
    1. 登壇資料
1. お問い合わせ...　問い合わせフォームから自分宛にメールが来る

# 経緯
## ITエンジニアの自己紹介って難しいですよね
社交会でも合コンでも、自分をITエンジニアだと自己紹介するんですが、結局何できるの？という話に良くなります。ITといっても領域がそもそも広いですが、噛み砕いて一生懸命説明します。でも、見せた方が早いなといつも思っていました...

### そうだ！自分でサイトを作ってデプロイしよう！！
多くのITエンジニアが行き着く結論だと思います。私も行き着きました。

### とはいえ、安く作りたい
合コンで見せるくらいでアクセス数を稼ぐものでもないので、あまりランニングコストかけたくないですよね。

### とはいえ、手間もかけたくない
私は家庭を持っていないですが、コミュニティ活動や最新技術のキャッチアップも大事なのであまり多くの時間をかけたくないというのが本音です。

### とはいえ、クールに作りたい
安く早く作りたいのに、格好良くしたいなんてわがままですよね。さて、どう作りましょうか。

## そうだ！Amplify Gen2だ！
### Amplify Gen2とは...
TypeScript を活用したフルスタックの開発環境です。フロントエンドとバックエンドの開発を一体化し、Web・モバイルアプリを迅速に構築できます。Amplify Gen 2 はコーディングからクラウドリソースのプロビジョニング、ホスティングまで、アプリ開発のライフサイクル全体を一元的に管理する環境を提供してくれます。
参考：[AWS Amplify Gen 2 をグラレコで解説](https://aws.amazon.com/jp/builders-flash/202411/awsgeek-aws-amplify-gen2/)

元々好きで触っていたので学習コストが低く、デプロイも簡単でサーバレス構成なので、使用した分の課金で安く運用できます。事前定義のUIコンポーネントも使えば格好良くできそうです。

# 技術的な話
正直Amplifyである必要もないじゃんと思う方も多いと思いますが、その魅力を語りつつもユーザが増えるように実装よりの話もできればと思います。
Ampilfy Gen2の魅力として大きく以下が挙げられます。
1. テストーデプロイが楽
1. バックエンド定義が楽
1. フロントエンドの実装も楽

## 1. テスト-デプロイが楽な話
これはかなりありがたいです。もしもAmplifyが無かったら、
- ローカルからCDKでバックエンドを作った後にフロントエンドをビルドしてからS3上にデプロイする？
- GitHub Actionsでmainブランチに変更があった時に起動するパイプラインを構築する？
- AWSのCodeシリーズを活用してパイプラインを構築する？

などなど実際の開発に着手する前から考える事が多いです。
どれも一旦フローを決めてCI/CDパイプライン構築が済めば良いですが、結構と手間がかかります。さらにテストはどうする？など考えていたら日も暮れてやる気も落ちているかもしれません。

Amplify Gen2を使うと開発に使うレポジトリを設定するだけで、これらのスタートでめんどくさい事を飛ばし、簡単にビルドからデプロイまでのパイプラインを作成する事ができるのです。

テストに関してもサンドボックス機能を利用すれば、コマンド一発で本番環境とは隔離された自分だけのサンドボックス環境を作ってテストを行う事が出来ます。
```bash
npx ampx sandbox
```
その際はサンドボックス用のリソースが裏側で起動するので本番環境に影響を与える事はありません。なんて素敵な事でしょう。
参考:
- [Quickstart](https://docs.amplify.aws/react/start/quickstart/)

## 2. バックエンドの定義が楽な話
Amplify Gen2は認証機能やデータ、ストレージの定義がとにかく楽です。
本来であればCDKでモリモリ書いてデプロイするものがアプリで利用しやすいように抽象化されています。

例えば、あるストレージ内のpublicフォルダ配下は誰でも自由に削除やアップロードして良いけど、個人フォルダ内で本人は自由に削除やアップロードができるけどそれ以外の人は閲覧しか出来ないようにしたいとします。
このような認可の実装がAmplify Gen2なら以下のように短いコードを書くだけで簡単にできてしまいます。
```typescript:amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import {storage} from './storage/resource';
const backend= defineBackend({
  auth,
  storage
});
```
```typescript:amplify/storage/resource.ts
import { defineStorage } from '@aws-amplify/backend';

export const storage = defineStorage({
    name: 'siteImages',
    access: (allow) => ({
      'public/*': [
        allow.guest.to(['read','write']),
        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'])
      ]
    })
  });
```
参考：[Set up Storage](https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/)
APIも充実しているのでフロントエンドからのリクエストも簡単に実装する事ができますが、それは次の章に回して、設定の柔軟性の話をします。

### 設定変更も柔軟にできる
抽象化されているっていう事は細かい設定がしにくいんじゃ...と不安になる方も多いと思います。安心してください。Amplify Gen2で作成されたリソースのL1コンストラクトにアクセスし、設定を変更する事ができます。Amplify Gen2でストレージに保存した画像を取得しようとしてCORSエラーに直面した時、以下のようにCORSポリシーをS3バケットに設定する事で解決する事ができました。

```typescript:amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { storage } from './storage/resource';

import * as s3 from 'aws-cdk-lib/aws-s3';

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


const s3Bucket = backend.storage.resources.bucket;

const cfnBucket = s3Bucket.node.defaultChild as s3.CfnBucket;

// Define CORS rules
cfnBucket.corsConfiguration = {
  corsRules: [
    {
      allowedHeaders: ['*'],
      allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD'], // S3 method strings
      allowedOrigins: ['http://localhost:5173'], // Replace with your application's URL
      exposedHeaders: ['ETag'],
      maxAge: 3000,
    },
  ],
};

```
以前記事にしているのでご参考ください。
参考：[AmplifyGen2でS3にCORSポリシーを設定する方法を紹介](https://qiita.com/asuka0708japan/items/83401a0d512cc077aef3)

## Lambda関数も管理できちゃう
正直DynamoDBとS3バケットの操作ができれば色々なアプリは作れちゃいますが、もっと柔軟に独自のAPIも使いたいよという場面があります。今回の場合はお問い合わせフォームのメール機能ですね。
これを実装するLambda関数もその他のリソースと同じような構成で管理できるのが魅力的です。
Lambda関数を使ったREST APIの実装例は以下のようになります。
```typescript:amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { Stack } from "aws-cdk-lib";
import {
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  Cors,
  LambdaIntegration,
  RestApi,
} from "aws-cdk-lib/aws-apigateway";
import { Policy, PolicyStatement } from "aws-cdk-lib/aws-iam";
import { ContactFunction } from "./functions/api-function/resource";

import { auth } from './auth/resource';
import {storage} from './storage/resource';

import * as s3 from 'aws-cdk-lib/aws-s3';

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

// create a new API stack
const apiStack = backend.createStack('api-stack');

// create a new REST API
const myRestApi = new RestApi(apiStack, 'RestApi', {
  restApiName: 'myRestApi',
  deploy: true,
  deployOptions: {
    stageName: 'dev',
  },
  defaultCorsPreflightOptions: {
    allowOrigins: [
      'https://main.d2rh6osdysxeo6.amplifyapp.com',
      'https://portfolio.namikia.net',
    ],
    allowMethods: Cors.ALL_METHODS,
    allowHeaders: Cors.DEFAULT_HEADERS,
  },
});

// create a new Lambda integration
const lambdaFunction = backend.ContactFunction.resources.lambda;
const lambdaIntegration = new LambdaIntegration(lambdaFunction);

// add permission for SES
const sesPolicy = new PolicyStatement({
  actions: ['ses:SendEmail', 'ses:SendRawEmail'],
  resources: ['*'], // 必要に応じてリソースを特定のARNに制限
});
lambdaFunction.addToRolePolicy(sesPolicy);

// create a new resource path "/contact" with NO authorization
const contactPath = myRestApi.root.addResource('contact', {
  defaultMethodOptions: {
    authorizationType: AuthorizationType.NONE,
  },
});

// add POST method to the "/contact" path
contactPath.addMethod('POST', lambdaIntegration);

// create a new IAM policy to allow Invoke access to the API
const apiRestPolicy = new Policy(apiStack, 'RestApiPolicy', {
  statements: [
    new PolicyStatement({
      actions: ['execute-api:Invoke'],
      resources: [
        `${myRestApi.arnForExecuteApi('POST', '/contact', 'dev')}`,
      ],
    }),
  ],
});

// attach the policy to the unauthenticated IAM role only
backend.auth.resources.unauthenticatedUserIamRole.attachInlinePolicy(
  apiRestPolicy
);
```
APIゲートウェイのCORS設定や、Lambda関数の許可設定など追加で実装すると意外と長くなってしまいました。

Lambda関数は以下のように実装しています。
```typescript:amplify/functions/api-function/resource.ts
import { defineFunction, secret } from "@aws-amplify/backend";

export const ContactFunction = defineFunction({
  name: "api-contact-function",
  environment: {
    CONTACT_EMAIL: secret("contact_email"),
    SES_ID: secret("ses_id")
  },
});
```

```typescript:amplify/functions/api-function/handler.ts
import type { APIGatewayProxyHandler } from "aws-lambda";
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
// import { secret } from '@aws-amplify/backend';
import { env } from "$amplify/env/api-contact-function"

const REGION = 'ap-northeast-1'; //リージョンが違う場合は変える

const sesClient = new SESClient({ region: REGION });

export const handler: APIGatewayProxyHandler = async (event) => {
    // 環境変数からシークレットを取得
    const contactEmail = env.CONTACT_EMAIL || '';
    const sesId = env.SES_ID || '';
  // リクエストボディからデータを取得
  const { name, email, message } = JSON.parse(event.body || '{}');

  const params = {
    Destination: {
      ToAddresses: [contactEmail], // 特定のメールアドレスに変更
    },
    Message: {
      Body: {
        Text: {
          Data: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
        },
      },
      Subject: {
        Data: 'お問い合わせがありました',
      },
    },
    Source: sesId, // SESで検証済みの送信元メールアドレス
  };
  const command = new SendEmailCommand(params);

  try {
    await sesClient.send(command);
    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": event.headers.origin || '',
        "Access-Control-Allow-Headers": "*",
      },
      body: JSON.stringify({ message: 'メールが送信されました' }),
    };
  } catch (error) {
    console.error('メール送信中にエラーが発生しました：', error);
    return {
      statusCode: 500,
      headers: {
        "Access-Control-Allow-Origin": event.headers.origin || '',
        "Access-Control-Allow-Headers": "*",
      },
      body: JSON.stringify({ message: 'メール送信に失敗しました', error }),
    };
  }
};
```

さらっとsecretを使っていますが、Amplify Gen2はSecret Managerを利用して変数を保存する事ができ、上記の例のsecret("ses_id")のように簡単に参照できます。
参考：[環境変数とシークレット](https://docs.amplify.aws/react/build-a-backend/functions/environment-variables-and-secrets/)

## フロントエンドの実装も楽な話
Amplify Gen2で作成したリソースに対するAPIはフロントから簡単に叩けます。バックエンドとフロントエンドのシームレスな統合により個人でもフルスタック開発がしやすいのもAmplifyの大きな魅力の一つです。
さらにReact用のUIコンポーネントも沢山提供されているので、それらを利用して素早くクールなフロントエンドを作成する事ができます。
例えば、プロフィールページの取得バッジを羅列する実装には事前に定義されているAPIを利用しています。
![スクリーンショット 2025-01-18 9.50.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3349905/f026cee8-9e61-36d4-e0dc-84a22cfd889e.png)
Amplify Gen2で作成したストレージに以下のようにいくつか事前に定義されているAPIがあります。
- uploadData
- getUrl
- downloadData
- list
- getProperties
- remove
- ... 
参考:[APIリファレンス](https://docs.amplify.aws/react/build-a-backend/storage/reference/)

これらを利用して、オブジェクトストレージへの処理の記述を簡素化できます。
例えば、AWSバッジを横に並べるにはlistで指定したフォルダ内の画像を全て取得し、getUrlで署名付きURLを発行して表示しています。
```typescript:
// ProfilePage.tsx(一部抜粋)
import { useEffect, useState } from 'react';
import { list, getUrl } from '@aws-amplify/storage';
import {  Card, Text } from '@aws-amplify/ui-react';

type S3Item = {
  path: string;
  eTag?: string;
  lastModified?: Date;
  size?: number;
};

export default function Profile() {
  const [AWSbadgeImages, setAWSBadgeImages] = useState<string[]>([]);

  useEffect(() => {
    async function fetchAWSBadgeImages() {
      try {
        // 'public/certifications/'配下のファイルをリスト
        const result = await list({
          path: 'public/certifications/AWS_Badges/',
          options: {
            listAll: true,
          },
        });

        // 画像のURLを取得
        const imageUrls = await Promise.all(
          imageKeys.map(async (path: string) => {
            const link = await getUrl({ path });
            return link.url.toString();
          })
        );

        setAWSBadgeImages(imageUrls);
      } catch (error) {
        console.error('バッジ画像の取得中にエラーが発生しました：', error);
      }
    }
    
    fetchAWSBadgeImages();
  }, []);

  return (
    <section className="bg-white py-12">
      <div className="container mx-auto px-4">

        <Card variation="elevated" className="flex flex-col items-center p-8">

          {/* 資格バッジの表示 */}
          <div className="w-full overflow-x-auto">
            <div className="flex justify-center space-x-4">
              {AWSbadgeImages.map((url, index) => (
                <img
                  key={index + 1}
                  src={url}
                  alt={`Certification ${index + 1}`}
                  className="w-12 h-12 object-contain"
                />
              ))}
            </div>
          </div>
        </Card>
      </div>
    </section>
  );
}
```

上記のコードでは
```typescript
import { list, getUrl } from '@aws-amplify/storage';
```
でimportした二つのAPIを利用してfetchAWSBadgeImages関数を定義して取得した署名付きURLを使って画像を表示しています。

もう一つのポイントとして、以下のようにAmplify定義のUIコンポーネントを使って見栄えを整えやすい点があります。
```typescript
import {  Card, Text } from '@aws-amplify/ui-react';
```
これだけだと他のライブラリでも良いじゃんとなります...しかし、Amplify Gen2ではバックエンドと統合されているUIコンポーネントを使う事もできます。
※このポートフォリオサイトでは使っていません。
例えば、FileUploaderというコンポーネントを使うと
```typescript
import { FileUploader } from '@aws-amplify/ui-react-storage';
import '@aws-amplify/ui-react/styles.css';

export const DefaultFileUploaderExample = () => {
  return (
    <FileUploader
      acceptedFileTypes={['image/*']}
      path="public/"
      maxFileCount={1}
      isResumable
    />
  );
};
```
![スクリーンショット 2025-01-18 10.19.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3349905/1250c19a-66e9-4eb4-23b3-4d10a736ca56.png)

このように簡単にそれっぽいクールなファイルアップロード機能を実装できます。
参考：[Upload files](https://docs.amplify.aws/react/build-a-backend/storage/upload-files/)

その他にも多くのUIコンポーネントはぜひ以下のサイトを参考にしてください。
参考:[Amplify UI](https://ui.docs.amplify.aws/)

このサイトをサーフィンして作ってみたいもののイメージが湧いたら即作ってみましょう。Amplify Gen2を使えばそれができます。


# まとめ
総じて、
1. 短時間で開発環境の整備ができるので始めやすい。
1. データ、ストレージ、Lambda関数によるその他API、認証・認可などバックエンドリソースをサクッと作成できる上に、CDKを利用して柔軟に設定変更も可能。
1. バックエンドとシームレスに統合されているため、フロントエンドの開発体験も素敵。

フロントエンドのフレームワークや細かい技術も進化が早いし、バックエンドも色々な選択肢があり、メリデメの検討には多くの時間がかかります。技術選定や開発環境整備に時間を取られて立ち止まり、作りたいものを作れないのが一番もったいない、

Amplify Gen2は合コンで自己紹介に困った私のメシアです。
この素敵なフルスタックの開発環境は私と同様に悩めるエンジニアのメシアになるのではないでしょうか。