AWS Amplify

BaaSAWSGraphQLサーバーレスTypeScriptモバイルWebアプリケーション

プラットフォーム

AWS Amplify

概要

AWS Amplifyは、Amazon Web Servicesが提供するフルスタック開発プラットフォームで、モバイルアプリケーションとWebアプリケーションの構築・デプロイ・管理を包括的にサポートします。Gen 2アーキテクチャでは、TypeScript-first・code-firstアプローチを採用し、従来のCLIベースの設定からコードによるインフラ定義へと進化しています。認証(Cognito)、データベース(DynamoDB + GraphQL)、ストレージ(S3)、CI/CD、ホスティングなどを統合し、AWSエコシステムとの深い統合によりエンタープライズレベルのスケーラビリティと信頼性を提供します。

詳細

AWS Amplify 2025年版は、TypeScript-firstの開発体験とAWS CDKベースのインフラストラクチャーを統合し、モダンなフルスタック開発を実現しています。Gen 2では、amplify/backend.tsファイルでインフラを定義し、自動的にAWS CDKスタックとしてデプロイされます。GraphQL APIとDynamoDB、Amazon Cognitoによる認証、S3ストレージ、Lambda関数、CloudFrontによるCDNなど、AWSの主要サービスを統一されたDXで利用可能。リアルタイムデータ同期、オフライン対応、マルチプラットフォームSDK(JavaScript、TypeScript、Flutter、Swift、Kotlin)により、スケーラブルでセキュアなアプリケーション開発を支援します。また、per-developer sandboxによる並行開発環境や、AI/MLサービス(Bedrock、Translate、Polly、Rekognition)との統合も提供します。

主な特徴

  • TypeScript-first DX: コードによるインフラ定義とタイプセーフな開発
  • GraphQL + DynamoDB: リアルタイムAPIとNoSQLデータベース
  • Amazon Cognito: 包括的な認証・認可システム
  • S3 + CloudFront: スケーラブルなストレージとCDN
  • Lambda Functions: サーバーレス関数実行環境
  • CI/CD Pipeline: AWSネイティブなデプロイメント

メリット・デメリット

メリット

  • AWSエコシステムとの深い統合によるエンタープライズ級のスケーラビリティ
  • TypeScript-firstアプローチによる型安全性と開発効率の向上
  • Code-first DXによるインフラとアプリケーションの統合管理
  • Per-developer sandboxによる並行開発とCI/CDの効率化
  • GraphQL APIの自動生成とリアルタイムデータ同期
  • 豊富なAWSサービスとの統合(AI/ML、分析、セキュリティ等)

デメリット

  • AWS依存によるベンダーロックインとプラットフォーム固有の学習コスト
  • 大規模利用時の複雑な料金体系と予測困難なコスト
  • GraphQL/NoSQL中心の設計による既存RDBMSからの移行困難性
  • 複雑なクエリや集計処理におけるDynamoDBの制限
  • AWSサービスの豊富さゆえの選択肢の多さと設定の複雑性
  • 他クラウドプロバイダーとのマルチクラウド戦略の制約

参考ページ

書き方の例

基本セットアップと設定

# Amplify CLI のインストール
npm install -g @aws-amplify/cli

# 新しいAmplifyプロジェクトの作成
npx create-amplify@latest
cd my-amplify-app

# AWSアカウントの設定
ampx configure

# 開発サーバーの起動
npm run dev

# 本番環境へのデプロイ
npx ampx pipeline deploy --branch main
// amplify/backend.ts - バックエンド定義
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { storage } from './storage/resource';

// バックエンドリソースの定義
export const backend = defineBackend({
  auth,
  data,
  storage,
});

// カスタムリソースの追加(CDK)
backend.addOutput({
  custom: {
    API_ENDPOINT: backend.data.resources.graphqlApi.graphqlUrl,
    REGION: backend.data.resources.graphqlApi.region,
  },
});

console.log('Backend configured successfully');
// src/main.ts - フロントエンド初期化
import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';

// Amplify設定
Amplify.configure(outputs);

console.log('Amplify configured with:', {
  region: outputs.auth?.aws_region,
  userPoolId: outputs.auth?.user_pool_id,
  apiEndpoint: outputs.data?.url,
});

// アプリケーションの起動
import './app';

GraphQL APIとDataStore

// amplify/data/resource.ts - データモデル定義
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  User: a
    .model({
      id: a.id().required(),
      name: a.string().required(),
      email: a.email().required(),
      age: a.integer(),
      posts: a.hasMany('Post', 'authorId'),
      profile: a.hasOne('Profile', 'userId'),
      createdAt: a.datetime().default(() => new Date().toISOString()),
      isActive: a.boolean().default(true),
    })
    .authorization((allow) => [
      allow.owner(),
      allow.authenticated().to(['read']),
    ]),

  Post: a
    .model({
      id: a.id().required(),
      title: a.string().required(),
      content: a.string().required(),
      authorId: a.id().required(),
      author: a.belongsTo('User', 'authorId'),
      tags: a.string().array(),
      status: a.enum(['DRAFT', 'PUBLISHED', 'ARCHIVED']),
      publishedAt: a.datetime(),
      viewCount: a.integer().default(0),
    })
    .authorization((allow) => [
      allow.owner(),
      allow.authenticated().to(['read']),
      allow.guest().to(['read']).where((post) => post.status.eq('PUBLISHED')),
    ]),

  Profile: a
    .model({
      id: a.id().required(),
      userId: a.id().required(),
      bio: a.string(),
      avatar: a.string(),
      website: a.url(),
      location: a.string(),
      user: a.belongsTo('User', 'userId'),
    })
    .authorization((allow) => [
      allow.owner(),
      allow.authenticated().to(['read']),
    ]),
});

export type Schema = ClientSchema<typeof schema>;

// データリソースの定義
export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});
// src/lib/api.ts - GraphQL クライアント操作
import { generateClient } from 'aws-amplify/data';
import type { Schema } from '../../amplify/data/resource';

// タイプセーフなクライアント生成
const client = generateClient<Schema>();

// ユーザー作成
export async function createUser(userData: {
  name: string;
  email: string;
  age?: number;
}) {
  try {
    const { data: newUser, errors } = await client.models.User.create({
      ...userData,
      isActive: true,
    });
    
    if (errors) {
      console.error('User creation errors:', errors);
      throw new Error('Failed to create user');
    }
    
    console.log('User created:', newUser);
    return newUser;
  } catch (error) {
    console.error('Create user error:', error);
    throw error;
  }
}

// ユーザー一覧取得(リアルタイム監視付き)
export function subscribeToUsers() {
  const subscription = client.models.User.observeQuery().subscribe({
    next: (snapshot) => {
      console.log('Users updated:', snapshot.items);
      updateUsersList(snapshot.items);
    },
    error: (error) => {
      console.error('User subscription error:', error);
    },
  });
  
  return subscription;
}

// 投稿作成
export async function createPost(postData: {
  title: string;
  content: string;
  tags?: string[];
  status?: 'DRAFT' | 'PUBLISHED';
}) {
  try {
    const { data: newPost, errors } = await client.models.Post.create({
      ...postData,
      status: postData.status || 'DRAFT',
      publishedAt: postData.status === 'PUBLISHED' 
        ? new Date().toISOString() 
        : undefined,
    });
    
    if (errors) {
      console.error('Post creation errors:', errors);
      throw new Error('Failed to create post');
    }
    
    console.log('Post created:', newPost);
    return newPost;
  } catch (error) {
    console.error('Create post error:', error);
    throw error;
  }
}

// 複雑なクエリの実行
export async function getPublishedPostsByAuthor(authorId: string) {
  try {
    const { data: posts, errors } = await client.models.Post.list({
      filter: {
        authorId: { eq: authorId },
        status: { eq: 'PUBLISHED' },
      },
      limit: 20,
    });
    
    if (errors) {
      console.error('Query errors:', errors);
      return [];
    }
    
    console.log('Published posts:', posts);
    return posts;
  } catch (error) {
    console.error('Query error:', error);
    throw error;
  }
}

// リレーションシップの取得
export async function getUserWithPosts(userId: string) {
  try {
    const { data: user, errors } = await client.models.User.get(
      { id: userId },
      {
        selectionSet: [
          'id',
          'name',
          'email',
          'posts.id',
          'posts.title',
          'posts.status',
          'posts.publishedAt',
          'profile.bio',
          'profile.avatar',
        ],
      }
    );
    
    if (errors) {
      console.error('User query errors:', errors);
      return null;
    }
    
    console.log('User with posts:', user);
    return user;
  } catch (error) {
    console.error('User query error:', error);
    throw error;
  }
}

Cognito認証システム

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

export const auth = defineAuth({
  loginWith: {
    email: {
      verificationEmailStyle: 'CODE',
      verificationEmailSubject: 'Verify your email',
      verificationEmailBody: (createCode) =>
        `Your verification code is ${createCode()}`,
    },
    externalProviders: {
      google: {
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
        scopes: ['email', 'profile', 'openid'],
      },
      facebook: {
        clientId: process.env.FACEBOOK_APP_ID!,
        clientSecret: process.env.FACEBOOK_APP_SECRET!,
      },
      signInWithApple: {
        clientId: process.env.APPLE_CLIENT_ID!,
        keyId: process.env.APPLE_KEY_ID!,
        privateKey: process.env.APPLE_PRIVATE_KEY!,
        teamId: process.env.APPLE_TEAM_ID!,
      },
      callbackUrls: [
        'http://localhost:3000/auth/callback',
        'https://myapp.com/auth/callback',
      ],
      logoutUrls: [
        'http://localhost:3000/',
        'https://myapp.com/',
      ],
    },
  },
  userAttributes: {
    email: {
      required: true,
      mutable: true,
    },
    givenName: {
      required: true,
      mutable: true,
    },
    familyName: {
      required: true,
      mutable: true,
    },
    phoneNumber: {
      required: false,
      mutable: true,
    },
  },
  passwordPolicy: {
    minLength: 8,
    requireNumbers: true,
    requireLowercase: true,
    requireUppercase: true,
    requireSymbols: true,
  },
  multifactor: {
    mode: 'OPTIONAL',
    sms: true,
    totp: true,
  },
});
// src/lib/auth.ts - 認証操作
import {
  signUp,
  signIn,
  signOut,
  confirmSignUp,
  resendSignUpCode,
  signInWithRedirect,
  getCurrentUser,
  fetchAuthSession,
  updateUserAttributes,
  changePassword,
  resetPassword,
  confirmResetPassword,
  deleteUser,
} from 'aws-amplify/auth';

// ユーザー登録
export async function registerUser({
  email,
  password,
  givenName,
  familyName,
}: {
  email: string;
  password: string;
  givenName: string;
  familyName: string;
}) {
  try {
    const { isSignUpComplete, userId, nextStep } = await signUp({
      username: email,
      password,
      options: {
        userAttributes: {
          email,
          given_name: givenName,
          family_name: familyName,
        },
      },
    });
    
    console.log('Sign up result:', {
      isSignUpComplete,
      userId,
      nextStep,
    });
    
    return { isSignUpComplete, userId, nextStep };
  } catch (error) {
    console.error('Sign up error:', error);
    throw error;
  }
}

// 確認コードの送信
export async function confirmSignUpCode({
  username,
  confirmationCode,
}: {
  username: string;
  confirmationCode: string;
}) {
  try {
    const { isSignUpComplete, nextStep } = await confirmSignUp({
      username,
      confirmationCode,
    });
    
    console.log('Confirmation result:', {
      isSignUpComplete,
      nextStep,
    });
    
    return { isSignUpComplete, nextStep };
  } catch (error) {
    console.error('Confirmation error:', error);
    throw error;
  }
}

// ログイン
export async function loginUser({
  username,
  password,
}: {
  username: string;
  password: string;
}) {
  try {
    const { isSignedIn, nextStep } = await signIn({
      username,
      password,
    });
    
    console.log('Sign in result:', {
      isSignedIn,
      nextStep,
    });
    
    if (isSignedIn) {
      const user = await getCurrentUser();
      console.log('Current user:', user);
      return { user, isSignedIn, nextStep };
    }
    
    return { user: null, isSignedIn, nextStep };
  } catch (error) {
    console.error('Sign in error:', error);
    throw error;
  }
}

// ソーシャルログイン
export async function signInWithGoogle() {
  try {
    await signInWithRedirect({ 
      provider: 'Google',
      customState: JSON.stringify({ returnUrl: window.location.pathname }),
    });
  } catch (error) {
    console.error('Google sign in error:', error);
    throw error;
  }
}

// 現在のユーザー情報取得
export async function getCurrentUserInfo() {
  try {
    const user = await getCurrentUser();
    const session = await fetchAuthSession();
    
    console.log('Current user info:', {
      user,
      tokens: session.tokens,
      credentials: session.credentials,
    });
    
    return {
      user,
      session,
      isAuthenticated: !!session.tokens,
    };
  } catch (error) {
    console.error('Get current user error:', error);
    return {
      user: null,
      session: null,
      isAuthenticated: false,
    };
  }
}

// プロフィール更新
export async function updateProfile(attributes: {
  given_name?: string;
  family_name?: string;
  phone_number?: string;
}) {
  try {
    const result = await updateUserAttributes({
      userAttributes: attributes,
    });
    
    console.log('Profile update result:', result);
    return result;
  } catch (error) {
    console.error('Profile update error:', error);
    throw error;
  }
}

// パスワード変更
export async function updatePassword({
  oldPassword,
  newPassword,
}: {
  oldPassword: string;
  newPassword: string;
}) {
  try {
    await changePassword({
      oldPassword,
      newPassword,
    });
    
    console.log('Password changed successfully');
  } catch (error) {
    console.error('Password change error:', error);
    throw error;
  }
}

// ログアウト
export async function logoutUser() {
  try {
    await signOut({ global: true });
    console.log('Signed out successfully');
  } catch (error) {
    console.error('Sign out error:', error);
    throw error;
  }
}

S3ストレージとCDN統合

// amplify/storage/resource.ts - ストレージ設定
import { defineStorage } from '@aws-amplify/backend';

export const storage = defineStorage({
  name: 'myAppStorage',
  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']),
    ],
    'uploads/images/*': [
      allow.authenticated.to(['read', 'write']),
    ],
    'uploads/documents/*': [
      allow.authenticated.to(['read', 'write']),
      // ファイルサイズ制限(10MB)
      allow.authenticated.to(['write']).when((context) => 
        context.request.object.size <= 10 * 1024 * 1024
      ),
    ],
  }),
});
// src/lib/storage.ts - ストレージ操作
import {
  uploadData,
  downloadData,
  getUrl,
  list,
  remove,
  getProperties,
} from 'aws-amplify/storage';

// ファイルアップロード(進捗付き)
export async function uploadFile({
  file,
  path,
  level = 'protected',
  onProgress,
}: {
  file: File;
  path: string;
  level?: 'public' | 'protected' | 'private';
  onProgress?: (progress: { transferredBytes: number; totalBytes?: number }) => void;
}) {
  try {
    const key = level === 'public' 
      ? `public/${path}`
      : level === 'protected'
      ? `protected/{identity_id}/${path}`
      : `private/{identity_id}/${path}`;
    
    const uploadTask = uploadData({
      path: key,
      data: file,
      options: {
        contentType: file.type,
        metadata: {
          originalName: file.name,
          uploadedAt: new Date().toISOString(),
        },
        onProgress: ({ transferredBytes, totalBytes }) => {
          const progress = totalBytes ? (transferredBytes / totalBytes) * 100 : 0;
          console.log(`Upload progress: ${progress.toFixed(2)}%`);
          
          if (onProgress) {
            onProgress({ transferredBytes, totalBytes });
          }
        },
      },
    });
    
    const result = await uploadTask.result;
    
    console.log('File uploaded successfully:', {
      path: result.path,
      size: file.size,
      type: file.type,
    });
    
    return result;
  } catch (error) {
    console.error('Upload error:', error);
    throw error;
  }
}

// 画像アップロード(サムネイル生成付き)
export async function uploadImage({
  file,
  generateThumbnail = true,
}: {
  file: File;
  generateThumbnail?: boolean;
}) {
  try {
    // 画像の検証
    if (!file.type.startsWith('image/')) {
      throw new Error('Selected file is not an image');
    }
    
    if (file.size > 5 * 1024 * 1024) { // 5MB制限
      throw new Error('Image size must be less than 5MB');
    }
    
    const fileName = `${Date.now()}_${file.name}`;
    const imagePath = `uploads/images/${fileName}`;
    
    // 元画像のアップロード
    const uploadResult = await uploadFile({
      file,
      path: imagePath,
      level: 'protected',
    });
    
    let thumbnailResult;
    if (generateThumbnail) {
      // サムネイル生成(Canvas API使用)
      const thumbnailFile = await createThumbnail(file, 200, 200);
      const thumbnailPath = `uploads/images/thumbnails/${fileName}`;
      
      thumbnailResult = await uploadFile({
        file: thumbnailFile,
        path: thumbnailPath,
        level: 'protected',
      });
    }
    
    return {
      original: uploadResult,
      thumbnail: thumbnailResult,
    };
  } catch (error) {
    console.error('Image upload error:', error);
    throw error;
  }
}

// サムネイル生成関数
function createThumbnail(
  file: File,
  maxWidth: number,
  maxHeight: number
): Promise<File> {
  return new Promise((resolve, reject) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();
    
    img.onload = () => {
      // アスペクト比を保持してリサイズ
      const { width, height } = calculateDimensions(
        img.width,
        img.height,
        maxWidth,
        maxHeight
      );
      
      canvas.width = width;
      canvas.height = height;
      
      ctx?.drawImage(img, 0, 0, width, height);
      
      canvas.toBlob(
        (blob) => {
          if (blob) {
            const thumbnailFile = new File(
              [blob],
              `thumb_${file.name}`,
              { type: file.type }
            );
            resolve(thumbnailFile);
          } else {
            reject(new Error('Failed to create thumbnail'));
          }
        },
        file.type,
        0.8
      );
    };
    
    img.onerror = () => reject(new Error('Failed to load image'));
    img.src = URL.createObjectURL(file);
  });
}

function calculateDimensions(
  originalWidth: number,
  originalHeight: number,
  maxWidth: number,
  maxHeight: number
) {
  const ratio = Math.min(maxWidth / originalWidth, maxHeight / originalHeight);
  return {
    width: Math.round(originalWidth * ratio),
    height: Math.round(originalHeight * ratio),
  };
}

// ファイル一覧取得
export async function listFiles({
  path = '',
  level = 'protected',
  pageSize = 100,
}: {
  path?: string;
  level?: 'public' | 'protected' | 'private';
  pageSize?: number;
} = {}) {
  try {
    const prefix = level === 'public' 
      ? `public/${path}`
      : level === 'protected'
      ? `protected/{identity_id}/${path}`
      : `private/{identity_id}/${path}`;
    
    const result = await list({
      path: prefix,
      options: {
        listAll: true,
        pageSize,
      },
    });
    
    console.log('Files listed:', result.items.length);
    return result.items;
  } catch (error) {
    console.error('List files error:', error);
    throw error;
  }
}

// ファイルダウンロード
export async function downloadFile({
  path,
  level = 'protected',
}: {
  path: string;
  level?: 'public' | 'protected' | 'private';
}) {
  try {
    const key = level === 'public' 
      ? `public/${path}`
      : level === 'protected'
      ? `protected/{identity_id}/${path}`
      : `private/{identity_id}/${path}`;
    
    const downloadResult = await downloadData({
      path: key,
    });
    
    const blob = await downloadResult.result.body.blob();
    
    console.log('File downloaded:', {
      path,
      size: blob.size,
      type: blob.type,
    });
    
    return blob;
  } catch (error) {
    console.error('Download error:', error);
    throw error;
  }
}

// 署名付きURL取得
export async function getSignedUrl({
  path,
  level = 'protected',
  expiresIn = 3600, // 1時間
}: {
  path: string;
  level?: 'public' | 'protected' | 'private';
  expiresIn?: number;
}) {
  try {
    const key = level === 'public' 
      ? `public/${path}`
      : level === 'protected'
      ? `protected/{identity_id}/${path}`
      : `private/{identity_id}/${path}`;
    
    const signedUrl = await getUrl({
      path: key,
      options: {
        expiresIn,
      },
    });
    
    console.log('Signed URL generated:', signedUrl.url.toString());
    return signedUrl.url;
  } catch (error) {
    console.error('Get signed URL error:', error);
    throw error;
  }
}

サーバーレス関数とAPI

// amplify/functions/process-image/resource.ts - Lambda関数定義
import { defineFunction } from '@aws-amplify/backend';

export const processImage = defineFunction({
  name: 'processImage',
  entry: './handler.ts',
  environment: {
    STORAGE_BUCKET_NAME: 'my-app-storage-bucket',
  },
  runtime: 20,
  timeoutSeconds: 30,
  memoryMB: 512,
});
// amplify/functions/process-image/handler.ts - Lambda関数実装
import type { S3Handler } from 'aws-lambda';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import sharp from 'sharp';

const s3Client = new S3Client({ region: process.env.AWS_REGION });

export const handler: S3Handler = async (event) => {
  console.log('Processing S3 event:', JSON.stringify(event, null, 2));
  
  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
    
    // 画像ファイルのみ処理
    if (!key.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i)) {
      console.log('Skipping non-image file:', key);
      continue;
    }
    
    try {
      // 元画像の取得
      const getObjectResponse = await s3Client.send(
        new GetObjectCommand({
          Bucket: bucket,
          Key: key,
        })
      );
      
      if (!getObjectResponse.Body) {
        console.error('Failed to get object body for:', key);
        continue;
      }
      
      const imageBuffer = await streamToBuffer(getObjectResponse.Body);
      
      // 複数サイズのサムネイル生成
      const thumbnailSizes = [
        { suffix: 'thumb_sm', width: 150, height: 150 },
        { suffix: 'thumb_md', width: 300, height: 300 },
        { suffix: 'thumb_lg', width: 600, height: 600 },
      ];
      
      for (const size of thumbnailSizes) {
        const thumbnailBuffer = await sharp(imageBuffer)
          .resize(size.width, size.height, {
            fit: 'cover',
            position: 'center',
          })
          .jpeg({ quality: 85 })
          .toBuffer();
        
        const thumbnailKey = key.replace(
          /(.+)(\.[^.]+)$/,
          `$1_${size.suffix}$2`
        );
        
        await s3Client.send(
          new PutObjectCommand({
            Bucket: bucket,
            Key: thumbnailKey,
            Body: thumbnailBuffer,
            ContentType: 'image/jpeg',
            Metadata: {
              originalKey: key,
              thumbnailSize: `${size.width}x${size.height}`,
              processedAt: new Date().toISOString(),
            },
          })
        );
        
        console.log('Thumbnail created:', thumbnailKey);
      }
      
      // WebP形式への変換
      const webpBuffer = await sharp(imageBuffer)
        .webp({ quality: 85 })
        .toBuffer();
      
      const webpKey = key.replace(/\.[^.]+$/, '.webp');
      
      await s3Client.send(
        new PutObjectCommand({
          Bucket: bucket,
          Key: webpKey,
          Body: webpBuffer,
          ContentType: 'image/webp',
          Metadata: {
            originalKey: key,
            format: 'webp',
            processedAt: new Date().toISOString(),
          },
        })
      );
      
      console.log('WebP version created:', webpKey);
      
    } catch (error) {
      console.error('Error processing image:', key, error);
    }
  }
};

// Stream to Buffer utility
async function streamToBuffer(stream: any): Promise<Buffer> {
  const chunks: Uint8Array[] = [];
  
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  
  return Buffer.concat(chunks);
}
// amplify/functions/api/resource.ts - REST API定義
import { defineFunction } from '@aws-amplify/backend';

export const api = defineFunction({
  name: 'api',
  entry: './handler.ts',
  environment: {
    DATA_TABLE_NAME: 'MyAppData',
  },
});
// amplify/functions/api/handler.ts - REST APIハンドラー
import type { APIGatewayProxyHandler } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';

const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const docClient = DynamoDBDocumentClient.from(dynamoClient);

export const handler: APIGatewayProxyHandler = async (event) => {
  console.log('API Gateway event:', JSON.stringify(event, null, 2));
  
  const { httpMethod, path, pathParameters, body, queryStringParameters } = event;
  
  // CORS headers
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
    'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
  };
  
  try {
    // OPTIONS request (CORS preflight)
    if (httpMethod === 'OPTIONS') {
      return {
        statusCode: 200,
        headers: corsHeaders,
        body: '',
      };
    }
    
    // ルーティング
    if (path === '/api/health' && httpMethod === 'GET') {
      return {
        statusCode: 200,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          status: 'healthy',
          timestamp: new Date().toISOString(),
          version: '1.0.0',
        }),
      };
    }
    
    if (path === '/api/users' && httpMethod === 'GET') {
      const result = await docClient.send(
        new ScanCommand({
          TableName: process.env.DATA_TABLE_NAME,
          Limit: parseInt(queryStringParameters?.limit || '20'),
        })
      );
      
      return {
        statusCode: 200,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          users: result.Items,
          count: result.Count,
          scannedCount: result.ScannedCount,
        }),
      };
    }
    
    if (path?.startsWith('/api/users/') && httpMethod === 'GET') {
      const userId = pathParameters?.proxy || pathParameters?.id;
      if (!userId) {
        return {
          statusCode: 400,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify({ error: 'User ID is required' }),
        };
      }
      
      const result = await docClient.send(
        new GetCommand({
          TableName: process.env.DATA_TABLE_NAME,
          Key: { id: userId },
        })
      );
      
      if (!result.Item) {
        return {
          statusCode: 404,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify({ error: 'User not found' }),
        };
      }
      
      return {
        statusCode: 200,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify({ user: result.Item }),
      };
    }
    
    if (path === '/api/users' && httpMethod === 'POST') {
      if (!body) {
        return {
          statusCode: 400,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' },
          body: JSON.stringify({ error: 'Request body is required' }),
        };
      }
      
      const userData = JSON.parse(body);
      const userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      
      const newUser = {
        id: userId,
        ...userData,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      };
      
      await docClient.send(
        new PutCommand({
          TableName: process.env.DATA_TABLE_NAME,
          Item: newUser,
        })
      );
      
      return {
        statusCode: 201,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify({ user: newUser }),
      };
    }
    
    // 404 for unmatched routes
    return {
      statusCode: 404,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Route not found' }),
    };
    
  } catch (error) {
    console.error('API error:', error);
    
    return {
      statusCode: 500,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: 'Internal server error',
        message: error instanceof Error ? error.message : 'Unknown error',
      }),
    };
  }
};

CI/CDと本番デプロイメント

# .github/workflows/deploy.yml - GitHub Actions CI/CD
name: Deploy to AWS Amplify

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  AWS_REGION: us-east-1
  NODE_VERSION: 18

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        
    - name: Install dependencies
      run: npm ci
      
    - name: Run type checking
      run: npm run type-check
      
    - name: Run linting
      run: npm run lint
      
    - name: Run tests
      run: npm run test:coverage
      
    - name: Build application
      run: npm run build
      
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage/lcov.info
        
  deploy-staging:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/develop'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
        
    - name: Install dependencies
      run: npm ci
      
    - name: Deploy to staging
      run: |
        npx ampx pipeline deploy \
          --branch develop \
          --app-id ${{ secrets.AMPLIFY_APP_ID_STAGING }}
          
  deploy-production:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    environment: production
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}
        
    - name: Install dependencies
      run: npm ci
      
    - name: Deploy to production
      run: |
        npx ampx pipeline deploy \
          --branch main \
          --app-id ${{ secrets.AMPLIFY_APP_ID_PRODUCTION }}
          
    - name: Run post-deployment tests
      run: |
        npm run test:e2e -- --baseUrl=${{ secrets.PRODUCTION_URL }}
        
    - name: Notify deployment success
      if: success()
      uses: 8398a7/action-slack@v3
      with:
        status: success
        text: "🚀 Production deployment successful!"
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# amplify/.env - 環境変数設定
# 開発環境
AWS_REGION=us-east-1
NODE_ENV=development
DEBUG=true

# ソーシャルログイン
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
APPLE_CLIENT_ID=your_apple_client_id
APPLE_KEY_ID=your_apple_key_id
APPLE_TEAM_ID=your_apple_team_id
APPLE_PRIVATE_KEY=your_apple_private_key

# 外部API
STRIPE_SECRET_KEY=sk_test_your_stripe_key
SENDGRID_API_KEY=your_sendgrid_api_key

# 監視・分析
SENTRY_DSN=your_sentry_dsn
DATADOG_API_KEY=your_datadog_api_key
// package.json - スクリプト設定
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "type-check": "tsc --noEmit",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "lint:fix": "eslint src --ext ts,tsx --fix",
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "amplify:dev": "npx ampx sandbox",
    "amplify:deploy": "npx ampx pipeline deploy",
    "amplify:delete": "npx ampx sandbox delete",
    "generate:api": "npx ampx generate graphql-client-code --format typescript",
    "db:migrate": "npx ampx generate migrate",
    "logs": "npx ampx logs",
    "console": "npx ampx console"
  },
  "dependencies": {
    "aws-amplify": "^6.0.0",
    "@aws-amplify/ui-react": "^6.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@aws-amplify/backend": "^1.0.0",
    "@aws-amplify/backend-cli": "^1.0.0",
    "typescript": "^5.2.0",
    "vite": "^5.0.0",
    "vitest": "^1.0.0",
    "@playwright/test": "^1.40.0"
  }
}