TypeScriptだけでフルスタック開発!AWS Amplify Gen2が変える開発の常識

に公開

フロントエンドエンジニアの多くは「バックエンドの構築」に対して一定の苦手意識を持っているのではないでしょうか。私自身も、インフラやAWSリソースの設定に手こずった経験は多くあります。

今回は、そんなフロントエンドエンジニアの助けになりうる「AWS Amplify Gen2」について紹介します。実際に触ってみて「これは非常に可能性がある」と感じたので、その魅力をお伝えしたいと思います。

Amplifyってそもそも何?

まず前提として、AWS Amplifyとは何かを整理しておきましょう。

簡単に言えば「フロントエンドエンジニアがクラウドバックエンドを簡単に構築・連携できるようにするためのAWSのサービス群とツールセット」です。従来のバックエンド構築では「サーバー構築、DB設定、認証機能実装、API実装」など多くの作業が必要でしたが、Amplifyはそういった作業を大幅に抽象化してくれます。

具体的には、以下のようなコンポーネントから構成されています。

  1. Amplify Libraries:フロントエンドからAWSサービスを利用するためのクライアントライブラリ
  2. Amplify CLI:バックエンドリソースの作成・管理のためのコマンドラインツール
  3. Amplify Console:ホスティングとCI/CDパイプラインを提供するWebコンソール
  4. Amplify Studio:UIコンポーネントやバックエンドを視覚的に構築するツール

これらを使うことで、フロントエンド開発者でもAWSのバックエンドを比較的容易に構築できるようになります。

Gen1からGen2へ:何が変わったのか

Amplifyは2017年から存在していましたが、最初のバージョン(今ではGen1と呼ばれる)にはいくつかの課題がありました。

Gen1の開発フローはこのようなものでした。

# プロジェクト初期化
amplify init

# 認証機能の追加
amplify add auth

# APIの追加
amplify add api

# バックエンドのデプロイ
amplify push

これでもかなり便利でしたが、いくつか不満点もありました。

  • CLIコマンドで対話的に設定するので再現性が低い
  • プロジェクトが大きくなると設定ファイルが複雑化する
  • バックエンドの変更・デプロイに時間がかかる
  • フロントエンドとバックエンドで開発体験がバラバラ

こういった課題を解決するために誕生したのがGen2です。

コードファーストアプローチという革新

Gen2最大の特徴は、「CLIベースからコードファーストへ」という革新的なアプローチの採用です。

コードファーストとは何か、というと、設定ファイルやCLIコマンドではなく、実際のTypeScriptコードでインフラや機能を定義するアプローチのことです。Gen2の内部実装はAWS CDK(Cloud Development Kit)をベースにしていて、TypeScriptでAWSリソースを定義・管理する仕組みを採用しています。

例えば、Gen1ではGraphQLスキーマをこのように書いていました。

type Todo @model @auth(rules: [{allow: owner}]) {
  id: ID!
  title: String!
  description: String
  isComplete: Boolean
}

一方、Gen2ではこうなります。

import { a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  Todo: a.model({
    id: a.id().required(),
    title: a.string().required(),
    description: a.string(),
    isComplete: a.boolean(),
  }).authorization([a.allow.owner()]),
});

export const data = defineData({
  schema,
});

aオブジェクトについて

ここで使われているaは、Amplifyが提供するスキーマ定義用のオブジェクトです。ZodやYupなどのバリデーションライブラリを使ったことがある方には馴染みやすい、チェーンメソッドによる宣言的な書き方でデータモデルを定義できます。

  • a.schema() → スキーマ全体を定義
  • a.model() → データモデルを作成
  • a.string() → 文字列型フィールド
  • a.boolean() → 真偽値型フィールド
  • .required() → 必須フィールド指定

コードファーストの真の価値

一見すると単に書き方が変わっただけのように見えるかもしれませんが、この変更には重要な意味があります。

  • TypeScriptの型システムの恩恵を受けられる
  • IDEによるコード補完が使える
  • 通常のコードと同様にGitで管理できる
  • 関数や変数を使った抽象化ができる

特に重要なのは「フロント」と「バック」で言語やツールが統一されるという点です。これは思った以上に開発体験を向上させます。

Gen2を使うとどう嬉しいのか

開発体験が劇的に向上する

まず最初に感じたのは「開発体験が本当に良くなった」ということです。

TypeScriptによる型の恩恵

フロントもバックエンドもTypeScriptで書けるようになったのは想像以上に大きなメリットです。型の恩恵をフルスタックで受けられるため、以下のような利点があります。

  • コンパイル時にエラーが検出される
  • リファクタリングが安全になる
  • コードを読むだけで機能が理解しやすくなる
  • IDEで補完が効く

特に、VSCodeで開発していると、バックエンド定義のコード補完が効いてくるのがかなり快適です。Gen1では別々の設定ファイル(GraphQLスキーマやAWS設定ファイル)を行き来する必要がありましたが、Gen2ではすべてTypeScriptで書けるようになり、開発体験がぐっと向上しました。

フロントとバックで同じ言語、同じツールを使えるというのは、体験してみると想像以上の価値があります。文脈の切り替えが少なくなり、開発の流れを止めずに作業を進められるのは、日々の開発効率に大きく影響します。

高速なサンドボックス環境

Gen2で最も感動したのはこの「サンドボックス環境」の存在です。端的に言うと「非常に高速な開発専用のクラウド環境」です。

npx ampx sandbox

このコマンド一つで開発用のクラウド環境が立ち上がります。Gen1ではamplify pushを実行するたびに数分待つ必要がありましたが、Gen2では最大8倍高速でデプロイが完了し、効率的に開発を進められるようになりました。

サンドボックス環境では、Amplifyで定義されたリソース(defineDatadefineAuthdefineFunction等)とCDKで追加したカスタムリソースの両方が実際のAWSクラウド上にデプロイされます。つまり、ローカルのモック環境ではなく、本番環境とほぼ同等の実際のクラウドリソースを使った開発とテストが可能になります。

なぜサンドボックス環境が重要なのか

従来のローカル開発では以下のような制約がありました。

  • 外部サービスとの連携テストが困難
  • AWSクラウド特有の挙動の検証ができない
  • 実際のパフォーマンスやレイテンシの確認ができない
  • 本番環境との差異による予期しない問題

サンドボックス環境では、これらの制約がすべて解消されます。実際のクラウドでのテストを、本番環境に影響を与えることなく行えるのは、開発効率と品質向上に大きな影響を与えます。

リアルタイム更新とホットスワッピング

サンドボックス環境の優れた点は、ファイル変更を監視してリアルタイムで更新される仕組みです。内部的にはCDKホットスワッピング機能を使用しており、コードを保存すると即座にクラウド環境に反映されます。

# サンドボックス起動後
npx ampx sandbox
# → ファイル変更を監視開始

# amplify/data/resource.tsを編集・保存
# → 自動的にGraphQL APIが更新される
# → DynamoDBスキーマも即座に反映

# amplify/auth/resource.tsを編集・保存  
# → 認証設定がリアルタイムで更新される

この仕組みにより、従来の「コード修正 → デプロイ → テスト」のサイクルが大幅に短縮され、開発効率が向上します。

開発者ごとの環境分離

サンドボックス環境の最大の特徴は、開発者ごとに分離された独立したクラウド環境が提供されることです。これにより、チーム開発でも他のメンバーの作業の影響を受けずに開発を進められます。

# 開発者Aの作業
npx ampx sandbox
# → 開発者A専用のクラウド環境が立ち上がる
# → GraphQL API、Lambda関数、DynamoDB、Cognitoなどが展開

# 同時に開発者Bも別の環境で作業可能
npx ampx sandbox
# → 開発者B専用の完全に独立したクラウド環境が立ち上がる

環境の削除も簡単

作業完了後の環境削除も非常に簡単です:

# 作業中断時
# Ctrl+C でサンドボックスを停止

# 完全削除時
npx ampx sandbox delete
# → すべてのリソースが削除される

この手軽さにより、「新機能の検証」「アーキテクチャの実験」といった試行錯誤を、コストや環境管理の心配なく行えるようになります。

GitベースCI/CDがデフォルトで使える

Gen1では、デプロイやホスティングの設定も対話的なCLIコマンドで行う必要がありました:

# Gen1でのホスティング設定例
amplify add hosting
# → 対話的な質問に答える必要がある
# → "Select the plugin module to execute: Hosting with Amplify Console"
# → "Choose a type: Manual deployment"

一方、Gen2はGitワークフローと深く統合されており、複雑な設定なしにCI/CDが利用できます:

  • ブランチごとに独立した環境が自動的に作られる
  • プルリクエストごとにプレビュー環境が作られる
  • マージしたら自動デプロイ

このようなCI/CD周りの設定は、Gen1では対話的な設定が必要でしたが、Gen2では「デフォルトで」「設定不要で」使えるようになりました。これにより、開発チームはインフラ設定ではなく、アプリケーション開発に集中できるようになります。

バックエンド構築が格段に簡単になる

次に、バックエンド構築のしやすさについてご紹介します。

データモデリングが直感的

データモデルの定義が直感的になりました。例えば、ブログアプリのデータモデルはこのように書けます。

const schema = a.schema({
  // ブログ投稿モデル
  Post: a.model({
    id: a.id().required(),
    title: a.string().required(),
    content: a.string().required(),
    status: a.enum(['DRAFT', 'PUBLISHED']).default('DRAFT'),
    
    // リレーションシップ
    comments: a.hasMany('Comment'),
    
    // メタデータ
    createdAt: a.datetime().required().onCreate().timestamp(),
    updatedAt: a.datetime().required().onUpdate().timestamp(),
  }).authorization([
    // 公開投稿は誰でも読める
    a.allow.public().to(['read']).when(ctx => ctx.record.status === 'PUBLISHED'),
    // 所有者は全操作可能
    a.allow.owner(),
  ]),
  
  // コメントモデル
  Comment: a.model({
    /* ... */
  })
});

このコードから、Amplifyが自動的に以下のものを生成してくれます。

  • DynamoDBテーブル
  • GraphQL API
  • リゾルバー
  • 認証・認可ルール

便利なのは、データモデルを変更したいときに、このコードを編集するだけで済むことです。変更を保存すれば再デプロイも自動で行われるため、開発の効率が大きく向上します。

認証機能の実装が非常に簡単

認証周りの実装は通常、複雑でセキュリティ面でも注意が必要な部分です。しかしGen2ではこのように簡単に実装できます。

import { defineAuth } from '@aws-amplify/backend';

export const auth = defineAuth({
  loginWith: {
    email: true,
    phone: false,
    username: true,
    
    // ソーシャルプロバイダー
    socialProviders: {
      google: true,
      facebook: false,
      amazon: false,
      apple: false,
    },
  },
  
  // パスワードポリシー
  passwordPolicy: {
    minLength: 8,
    requireLowercase: true,
    requireUppercase: true,
    requireNumbers: true,
    requireSpecialCharacters: true,
  },
});

このシンプルなコードから、Amazon Cognitoのユーザープールとアイデンティティプールが自動的に構成されます。もちろん、UIコンポーネントも提供されているので、フロント側の実装も簡単です。

カスタム関数も作成可能

独自のバックエンドロジックが必要な場合は、サーバーレス関数も簡単に追加できます。

基本的な関数の作成

// amplify/functions/process-payment/resource.ts
import { defineFunction } from '@aws-amplify/backend';

export const processPayment = defineFunction({
  name: 'process-payment',
  entry: './handler.ts'
});
// amplify/functions/process-payment/handler.ts
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  const body = JSON.parse(event.body || '{}');
  const { amount, currency, paymentMethod } = body;
  
  // 外部支払いAPIとの連携など...
  
  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': '*',
    },
    body: JSON.stringify({
      success: true,
      transactionId: 'abc123',
    }),
  };
};

複数関数でのマイクロサービス設計

Lambda関数の設計には複数のアプローチがありますが、エンドポイントごとに個別のLambda関数を作成する方法もその一つです。このアプローチでは、各機能を独立したサービスとして管理できます。

// amplify/functions/get-user/resource.ts
export const getUser = defineFunction({
  name: 'get-user',
  entry: './handler.ts'
});

// amplify/functions/create-user/resource.ts  
export const createUser = defineFunction({
  name: 'create-user',
  entry: './handler.ts'
});

// amplify/functions/update-user/resource.ts
export const updateUser = defineFunction({
  name: 'update-user', 
  entry: './handler.ts'
});

API Gatewayとの手動統合

より柔軟なAPI設計が必要な場合は、AWS CDKを使ってAPI Gatewayと手動で統合できます:

// amplify/backend.ts
import { defineBackend } from '@aws-amplify/backend';
import { 
  RestApi, 
  LambdaIntegration, 
  CognitoUserPoolsAuthorizer,
  AuthorizationType,
  Cors 
} from 'aws-cdk-lib/aws-apigateway';
import { auth } from './auth/resource';
import { data } from './data/resource';
import { getUser } from './functions/get-user/resource';
import { createUser } from './functions/create-user/resource';
import { updateUser } from './functions/update-user/resource';

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

// REST APIの作成
const userApi = new RestApi(backend.createUser.stack, 'UserApi', {
  restApiName: 'user-api',
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_ORIGINS,
    allowMethods: Cors.ALL_METHODS,
    allowHeaders: [...Cors.DEFAULT_HEADERS, 'Authorization'],
  },
});

// Cognito認証の設定
const cognitoAuth = new CognitoUserPoolsAuthorizer(backend.auth.stack, 'CognitoAuth', {
  cognitoUserPools: [backend.auth.resources.userPool],
});

// Lambda統合の作成
const getUserIntegration = new LambdaIntegration(
  backend.getUser.resources.lambda
);
const createUserIntegration = new LambdaIntegration(
  backend.createUser.resources.lambda
);
const updateUserIntegration = new LambdaIntegration(
  backend.updateUser.resources.lambda
);

// APIエンドポイントのマッピング
const usersResource = userApi.root.addResource('users');
const userIdResource = usersResource.addResource('{id}');

// 各エンドポイントに関数をマッピング
usersResource.addMethod('GET', getUserIntegration, {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: cognitoAuth,
});

usersResource.addMethod('POST', createUserIntegration, {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: cognitoAuth,
});

userIdResource.addMethod('PUT', updateUserIntegration, {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: cognitoAuth,
});

この構成により、以下のエンドポイントが作成されます:

  • GET /users → getUserFunction
  • POST /users → createUserFunction
  • PUT /users/{id} → updateUserFunction

すべてのエンドポイントはCognito認証で保護され、各機能が独立したLambda関数として実行されるため、スケーラビリティと保守性が向上します。

インフラ抽象化と拡張性の両立

これはGen2の最大の強みだと考えられます。内部的にはAWS CDKが使われているので、以下のようなメリットがあります。

  • 自動的に必要なAWSリソースが作られる
  • ベストプラクティスに沿った設定が適用される
  • 必要に応じて、他のAWSサービスと連携できる

つまり、インフラの詳細を気にせずアプリを作れるというわけです。

特筆すべきは、Gen2ではCDKを直接扱えるという点です。これにより拡張性も確保されています。Gen1では提供される機能以上のカスタマイズが難しかったのですが、Gen2では必要に応じてCDKコードを追加することで、Amplifyが公式にサポートしていない機能やAWSサービスも統合できます。

import { Bucket } from 'aws-cdk-lib/aws-s3';
import { defineBackend } from '@aws-amplify/backend';

export const backend = defineBackend({
  // 標準のAmplifyリソース
  auth,
  data,
  
  // カスタムCDKリソース
  customResources: {
    stack: ({ stack }) => {
      // カスタムS3バケットの作成
      const logsBucket = new Bucket(stack, 'LogsBucket', {
        versioned: true,
        lifecycleRules: [{
          expiration: Duration.days(90)
        }]
      });
      
      return { logsBucket };
    }
  }
});

このような「抽象化と拡張性の両立」ができていることは、非常に珍しいと思います。一般的に、便利なフレームワークは「簡単だけど制限がある」ものが多いですが、Gen2は「簡単でありつつ必要なら複雑なこともできる」という絶妙なバランスを実現しています。

デプロイが非常に簡単

ローカル開発が終わったら、本番環境へのデプロイも非常に簡単です。

GitHubベースの自動デプロイ

Amplify Gen2はGitHubリポジトリと連携して自動デプロイを実現します。

  1. コードをGitHubにプッシュ
  2. Amplifyコンソールでリポジトリを接続
  3. あとは自動でビルド・デプロイが実行される

特に複雑な設定は不要で、プッシュするだけで本番環境にデプロイされる仕組みが整います。これは新規プロジェクトだけでなく、既存プロジェクトでも利用できるので便利です。

環境分離が容易

ブランチベースの環境分離も非常に便利です。

  • mainブランチ → 本番環境
  • devブランチ → 開発環境
  • feature/*ブランチ → 機能ごとのプレビュー環境

これにより、各環境が独立しつつも、同じコードベースから生成されるため、「開発環境では動くけど本番では動かない」といった問題が起きにくくなります。

スケーリングが自動化

Amplify Gen2で構築されたアプリケーションは、AWSのマネージドサービスを使用しているため、自動的にスケールします。高可用性が確保され、サーバーメンテナンスも不要です。

これは開発チーム全体にとって大きなメリットとなります。通常、アプリケーションのスケーリングやインフラ管理には、クラウドエンジニアやDevOpsの知識が必要で、多くのリソースと時間が費やされます。サーバーの監視、パフォーマンスチューニング、セキュリティパッチの適用など、アプリケーションの価値向上に直接関係しない作業も多く発生します。

Amplify Gen2ではこういった複雑なインフラ管理の多くが自動化されるため、チームはインフラではなく、ビジネス課題の解決やユーザー体験の向上といった本質的な価値創出に集中できるようになります。

どんなプロジェクトに向いているのか

こういうプロジェクトに適している

  • MVPやプロトタイプの迅速な開発: 開発スピードが速く、初期コストを抑えられるため、アイデア検証やスタートアップの初期フェーズに最適です。

  • 小〜中規模のWebアプリケーション: TypeScriptで統一された一貫したコードベースを維持でき、自動スケーリングにより成長にも対応できます。

  • フロントエンド開発者が主導するプロジェクト: バックエンドの専門知識が少なくても、TypeScriptの知識だけでフルスタック開発が可能です。

向いていないケース

以下のようなケースでは、Amplify Gen2の標準的な機能だけでは対応が難しく、追加の工夫やカスタマイズが必要になることがあります。

  • 複雑なデータモデルを持つアプリケーション: Amplify Gen2は基本的にDynamoDBをベースにしているため、多数の複雑な関係性を持つデータモデルや、複雑な集計クエリが必要な場合には、標準機能だけでは最適化が難しいことがあります。このような場合はカスタムリゾルバーの実装や、場合によっては別のデータベースサービス(RDSなど)との統合が必要になるでしょう。

  • レガシーシステムとの複雑な統合: 既存の複雑なシステムとの統合が必要な場合、特に独自のプロトコルや特殊なデータ形式を扱う必要がある場合は、追加の連携機能の実装が必要になることがあります。

  • 特殊なセキュリティ要件を持つシステム: 業界固有の厳格なコンプライアンス要件や、非常に特殊な認証・認可の仕組みが必要な場合、Amplifyの標準機能だけでは対応しきれないことがあります。

ただし、Gen2はCDKベースであるため、これらの制約はGen1よりもはるかに対応しやすくなっています。必要に応じて、CDKを使って標準以外のAWSサービスを統合することで、多くの複雑なケースにも対応可能です。例えば以下のようなケースでは他のAWSサービスで補完できます。

  • 複雑な計算処理 → Lambda関数
  • 高度なデータ処理 → AWS Glue、Amazon EMR
  • リアルタイム処理 → Amazon Kinesis

実際のプロジェクト構成例

参考に、実際のプロダクションレベルでのフォルダ構成を紹介します。これは、タスク管理アプリケーションを想定した構成例です:

project-root/
├── packages/
│   └── shared-backend/              # バックエンド統合パッケージ
│       ├── amplify/
│       │   ├── auth/
│       │   │   └── resource.ts      # Cognito認証設定
│       │   ├── data/
│       │   │   └── resource.ts      # GraphQLスキーマ・DynamoDB
│       │   ├── storage/
│       │   │   └── resource.ts      # S3ストレージ設定
│       │   ├── function/            # Lambda関数群
│       │   │   ├── create-user/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── get-profile/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── create-task/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── list-tasks/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── update-task/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── delete-task/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   ├── upload-attachment/
│       │   │   │   ├── handler.ts
│       │   │   │   └── resource.ts
│       │   │   └── send-notification/
│       │   │       ├── handler.ts
│       │   │       └── resource.ts
│       │   └── backend.ts           # メインバックエンド設定
│       ├── package.json
│       └── tsconfig.json
├── apps/
│   ├── web/                         # Webアプリケーション
│   │   ├── src/
│   │   ├── package.json
│   │   └── next.config.js
│   └── mobile/                      # モバイルアプリ
│       ├── src/
│       ├── package.json
│       └── expo.json
├── package.json                     # ルートパッケージ設定(npm workspaces設定含む)
└── turbo.json                      # Turboレポビルド設定

この構成の特徴

  1. モノレポ構成: 複数のアプリケーションとバックエンドを統合管理
  2. 機能別Lambda関数: 各APIエンドポイントが独立した関数(8個の独立した機能)
  3. 共有バックエンド: Webアプリとモバイルアプリが同じバックエンドを共有
  4. 明確な責務分離: 認証、データ、ストレージ、関数がそれぞれ独立

各ディレクトリの役割

  • amplify/auth/ - Cognito User Poolsの設定
  • amplify/data/ - GraphQLスキーマとDynamoDBの定義
  • amplify/storage/ - S3バケットとファイルアップロード設定
  • amplify/function/ - 各Lambda関数(ユーザー管理、タスク操作、通知など)
  • apps/web/ - WebブラウザーでのNext.jsアプリケーション
  • apps/mobile/ - React Native/Expoでのモバイルアプリケーション

このような構成により、スケーラブルで保守しやすいフルスタックアプリケーションを構築できます。

まとめ:Gen2は大きな可能性を秘めている

ここまで書いてきて改めて感じるのは「Amplify Gen2は本当によく設計されている」ということです。特に「型安全性」「開発スピード」「拡張性」のバランスが絶妙です。

最大のメリットはこの4つです。

  1. 開発体験の向上:TypeScriptによる型安全性、IDE補完、高速開発サイクル
  2. バックエンド構築の簡素化:直感的なデータモデリング、簡単な認証、インフラ抽象化
  3. 迅速なデプロイ:GitベースのCI/CD、環境分離、自動スケーリング
  4. 拡張性と柔軟性:CDKによる高度なカスタマイズ、AWSサービス連携

個人的には、最近のAWSサービスの中で最も印象的なものの一つです。Amplifyはこれまで「簡単だけど制限が多い」というイメージがありましたが、Gen2ではそのイメージを覆すほど進化しています。「簡単であり、かつ必要に応じて複雑なことも可能」というのは理想的です。

Next.jsやReact、Vue、Angularなど、どのフロントエンドフレームワークとも組み合わせられるのも魅力的です。

まだ試されていない方は、小さなプロジェクトでも構いませんので触れてみることをおすすめします。特にフロントエンドエンジニアの方には、バックエンド開発の敷居がグッと下がる体験になるはずです。

参考資料

codeciaoテックブログ

Discussion