はじめに

AWS Amplifyは、フロントエンド開発者がクラウドバックエンドを簡単に構築できるようにするツールセットとして人気を集めています。2023年11月に発表されたGen2では、バックエンドリソースの定義方法において従来のGen1とは大きく異なるアプローチが導入されました。この記事では、AWS Amplifyにおけるバックエンド開発の両世代の違いについて解説し、特にAuth(認証)やAPI GatewayなどのクラウドリソースがGen2では異なる方法で実装される理由と具体的な実装方法について掘り下げていきます。

フロントエンド開発者にとって、バックエンドの構築方法を理解することは重要です。Gen1ではCLIを通じた対話的なリソース作成が主流でしたが、Gen2ではTypeScriptコードを直接記述してインフラストラクチャを定義するアプローチに変わりました。これはただの技術的な変更ではなく、バックエンド開発の哲学そのものの転換を意味します。

Gen1とGen2の基本的な違い

アプローチの違い

AWS公式ブログ「Introducing the Next Generation of AWS Amplify’s Fullstack Development Experience」によると、Gen1とGen2の最も根本的な違いは以下のとおりです。

“The first generation of tooling offered a tools-first experience using CLI and console-based interactive workflows. Gen2 moves to a code-first DX, allowing developers to concisely express app requirements like data models, business logic, and auth rules in TypeScript. The cloud infrastructure needed is automatically deployed based on the declared app code, without developers needing to explicitly configure AWS service interfaces.”

「ツーリングの最初の世代は、CLIやコンソールベースのインタラクティブなワークフローを使用したツール優先の体験を提供していました。Gen2はコード優先のDXに移行し、開発者がデータモデル、ビジネスロジック、認証ルールなどのアプリ要件をTypeScriptで簡潔に表現できるようにしています。宣言されたアプリコードに基づいて必要なクラウドインフラストラクチャが自動的にデプロイされ、開発者がAWSサービスインターフェースを明示的に設定する必要はありません。」

つまり、Gen1では次のようなCLIコマンドを使用します。

$ amplify add auth
? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Email
? Do you want to configure advanced settings? No, I am done

これに対して、Gen2では次のようなTypeScriptコードを記述します。

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

/**
 * Define and configure your auth resource
 * @see https://docs.amplify.aws/gen2/build-a-backend/auth
 */
export const auth = defineAuth({
  loginWith: {
    email: true,
  },
});

defineAuth の CDK 呼び出し関係

defineAuthメソッドは、最終的に AWS CDK の複数のリソースを利用して Amazon Cognito リソースを作成しています。その流れは以下のとおりです。

呼び出し階層

  • defineAuth (amplify/auth/resource.ts)
  • AmplifyAuthFactory (node_modules/@aws-amplify/backend-auth/lib/factory.js)
  • AmplifyAuth コンストラクト (node_modules/@aws-amplify/auth-construct/lib/construct.js)
  • AWS CDK リソース (aws-cdk-lib)

主要な CDK リソース

  • aws-cdk-lib/aws-cognito の UserPool: Cognito ユーザープールを作成
  • aws-cdk-lib/aws-iam の Role: 認証済み・未認証ユーザー用の IAM ロールを作成
  • aws-cdk-lib/aws-cognito の CfnUserPoolGroup: ユーザープール内のグループを作成

リソース間の関連

  • AmplifyAuth コンストラクトは、Cognito UserPool を作成し、必要に応じて Identity Pool や UserPool Client も作成します
  • 作成されたリソースは IAM ロールと関連付けられ、適切なアクセス権限が設定されます
  • これらのリソースは CDK スタックの一部となり、CloudFormation を通じてデプロイされます

defineAuth 関数は直接 CDK を呼び出すわけではなく、Amplify の抽象化レイヤー(AmplifyAuthFactory)を経由して最終的に CDK のリソースを作成します。このような設計により、ユーザーは複雑な CDK の知識なしでも簡単に認証機能を設定できるようになっています。

Gen2での認証実装を探ると、defineAuth関数が内部的にどのようにAWS CDKリソースを生成しているかがわかります。コードの内部では、次のようなリソース構築が行われています。

  1. Amazon Cognito UserPool: ユーザーデータストアとして機能
  2. UserPoolClient: アプリケーションがCognitoと通信するためのクライアント
  3. IdentityPool: 認証されたユーザーと未認証ユーザーの両方にAWSリソースへのアクセスを提供
  4. IAMロール: ユーザーに必要な権限を付与

例えば、@aws-amplify/auth-constructパッケージのconstruct.jsファイルでは、AmplifyAuthクラスがCDKコンストラクトとしてこれらのリソースを定義しています。

class AmplifyAuth extends cdk.Construct {
  constructor(scope, id, props) {
    super(scope, id);
    // UserPool の作成
    this.userPool = new cognito.UserPool(this, 'UserPool', {
      // ... 設定プロパティ
    });

    // IdentityPool の作成
    this.identityPool = new cognito.CfnIdentityPool(this, 'IdentityPool', {
      // ... 設定プロパティ
    });

    // IAMロールの設定
    // ... 認証済み/未認証ユーザー用のロール定義
  }
}

これは、Gen1ではamplify add authコマンドによって内部的に生成されていたCloudFormationテンプレートに相当します。重要な違いは、Gen2ではTypeScriptコードとして明示的に表現されており、開発者が必要に応じてカスタマイズできる点です。

テンプレートプロジェクトの amplify/auth/resource.ts ファイルは、実際には次のようなシンプルな記述だけで完全な認証インフラストラクチャを定義しています。

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

/**
 * Define and configure your auth resource
 * @see https://docs.amplify.aws/gen2/build-a-backend/auth
 */
export const auth = defineAuth({
  loginWith: {
    email: true,
  },
});

この数行のコードだけで、Gen1のamplify add authコマンドで生成されていたすべてのCloudFormationリソースに相当するインフラストラクチャが作成されます。Gen2の大きな特徴は、このようにシンプルなTypeScriptコードでAWSリソースを表現できる点にあります。

データモデリング(Data)リソースの実装

認証と同様に、Gen2ではデータモデルの定義もTypeScriptコードで宣言的に行います。defineData関数を使用することで、GraphQLスキーマベースのデータモデルを定義できます。

// amplify/data/resource.ts
import { defineData } from '@aws-amplify/backend';
import { type ClientSchema } from '@aws-amplify/backend/data';

// データモデルのスキーマ定義
const schema = `
  type Todo @model {
    id: ID!
    name: String!
    description: String
    priority: Priority
    status: Status
  }

  enum Priority {
    LOW
    MEDIUM
    HIGH
  }

  enum Status {
    PENDING
    IN_PROGRESS
    DONE
  }
` as const;

// クライアント側で利用可能な型定義
export type Schema = ClientSchema<typeof schema>;

// データリソースの定義
export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool',
  },
});

defineData の CDK 呼び出し関係

defineDataメソッドもdefineAuthと同様に、AWS CDKの複数のリソースを利用してバックエンドインフラストラクチャを作成します。

呼び出し階層

  • defineData (amplify/data/resource.ts)
  • AmplifyDataFactory (node_modules/@aws-amplify/backend-data/lib/factory.js)
  • AmplifyAppSyncAPI コンストラクト (node_modules/@aws-amplify/backend-data/lib/construct.js)
  • AWS CDK リソース (aws-cdk-lib)

主要な CDK リソース

  • aws-cdk-lib/aws-appsync の GraphqlApi: AppSync GraphQL APIの作成
  • aws-cdk-lib/aws-dynamodb の Table: DynamoDBテーブルの作成
  • aws-cdk-lib/aws-lambda の Function: リゾルバーとして機能するLambda関数
  • aws-cdk-lib/aws-iam の Role: 各リソースにアクセスするための権限

リソース間の関連

  • GraphQLスキーマから自動的にDynamoDBテーブルが生成されます
  • AppSync APIとDynamoDBテーブルの間に必要なリゾルバーが設定されます
  • 認証設定に基づいて適切なアクセス制御が構成されます

Gen1のamplify add api(GraphQL API)コマンドでは、スキーマファイルを作成し、プロンプトに従ってAPIの設定を行い、最終的にCloudFormationテンプレートが生成されていました。Gen2ではTypeScriptコードだけでこれらすべてのプロセスが完結し、さらに型安全性も提供されます。

特に注目すべき点は、ClientSchemaによる型の自動生成です。これにより、フロントエンドコードでデータを操作する際に型の整合性が保証され、開発時のエラー検出が可能になります。例えば:

// フロントエンドでのデータ操作(型安全)
import { generateClient } from 'aws-amplify/data';
import type { Schema } from './amplify/data/resource';

const client = generateClient<Schema>();

// TypeScriptが型チェックを行ってくれる
const newTodo = await client.models.Todo.create({
  name: 'Complete blog post',
  priority: 'HIGH',
  status: 'IN_PROGRESS'
});

この型安全性により、Gen2はフロントエンドとバックエンドの統合がよりシームレスになり、開発体験が大幅に向上しています。

バックエンド統合の仕組み

Gen2でのdefineBackend関数は、個別に定義された各バックエンドリソース(auth, data, storage, functions など)を統合し、それらの間の関係を確立する役割を担っています。

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

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

defineBackend の CDK 呼び出し関係

defineBackend関数も同様にCDKの仕組みを利用してバックエンドリソースを統合します。

呼び出し階層

  • defineBackend (amplify/backend.ts)
  • AmplifyBackendFactory (node_modules/@aws-amplify/backend/lib/factory.js)
  • AmplifyBackendStack (node_modules/@aws-amplify/backend/lib/stack.js)
  • AWS CDK アプリケーション (aws-cdk-lib)

主要な機能

  • 各リソース定義を解析して依存関係グラフを構築
  • リソース間の権限設定を自動的に構成(例:認証されたユーザーがデータにアクセスするための権限)
  • AWS CDKアプリケーションとして統合されたバックエンドスタックを生成
  • デプロイのためのCloudFormationテンプレートを準備

リソース間の連携例

// 内部的な連携処理の例
if (hasAuthResource && hasDataResource) {
  // Cognito UserPoolをデータリソースの認証に連携
  dataResource.addAuthorizationMode({
    type: 'USER_POOL',
    userPool: authResource.resources.userPool,
  });

  // データアクセス用の適切なIAMポリシーを認証済みロールに付与
  authResource.authenticatedUserRole.addToPolicy(
    new iam.PolicyStatement({
      actions: ['appsync:GraphQL'],
      resources: [dataResource.api.arn + '/*'],
    })
  );
}

Gen1では、このようなリソース間の連携設定は、各リソースの追加時またはamplify push実行時に自動的に処理されていました。Gen2では、これらの関係性がコード内で明示的に確立され、開発者はその仕組みを理解し、必要に応じてカスタマイズすることができます。

特に複雑なバックエンド設定が必要な場合、Gen2のこのアプローチは非常に有益です。例えば、APIへのアクセス権限を特定のユーザーグループに制限したり、ストレージリソースとAPIの間に特殊な関係を確立したりといった高度なシナリオを実現できます。

さらに、TypeScriptによる型安全性はここでも重要な役割を果たします。

// 型安全性により、存在しないリソースを指定するとコンパイルエラーになる
const backend = defineBackend({
  auth,
  data,
  nonExistentResource // TypeScriptによるエラー: プロパティが存在しない
});

この過程は完全にコード駆動であり、Gen1のCLIコマンドとプロンプトによる設定とは本質的に異なります。

Gen2におけるカスタムCDKリソースの必要性

サポート状況の差

Gen2は現在も活発に開発中であり、Gen1で利用できる機能の一部はまだネイティブサポートされていません。AWSの機能マトリックスによれば、特に以下のようなバックエンドリソースにおいて差があります。

カテゴリ Gen1 Gen2
REST API ネイティブサポート カスタムCDK必要
GraphQL API ネイティブサポート ネイティブサポート
Lambda関数 ネイティブサポート ネイティブサポート
ストレージ ネイティブサポート ネイティブサポート
認証 ネイティブサポート ネイティブサポート
通知 ネイティブサポート カスタムCDK必要
GeoLocation ネイティブサポート カスタムCDK必要

カスタムCDK実装の例:REST API

例えば、REST APIをGen2で実装するためには、CDKを直接使用する必要があります。

// amplify/custom/rest-api.ts
import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import { defineBackend } from '@aws-amplify/backend';
import { auth } from '../auth/resource';

class RestApiStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Lambda関数の作成
    const helloFunction = new nodejs.NodejsFunction(this, 'HelloFunction', {
      entry: 'amplify/custom/lambda/hello.js',
      handler: 'handler'
    });

    // API Gatewayの作成
    const api = new apigw.RestApi(this, 'HelloApi', {
      restApiName: 'Hello Service',
      description: 'APIによるHello Service'
    });

    // Cognitoオーソライザーの追加
    const authorizer = new apigw.CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [auth.resources.userPool]
    });

    // エンドポイントの設定
    const hello = api.root.addResource('hello');
    hello.addMethod('GET', new apigw.LambdaIntegration(helloFunction), {
      authorizer,
      authorizationType: apigw.AuthorizationType.COGNITO
    });
  }
}

// バックエンド定義にカスタムスタックを統合
const backend = defineBackend({
  auth,
  customStackA: process.env.NODE_ENV === 'production' 
    ? { defineCustomStack: (scope, id) => new RestApiStack(scope, id) }
    : undefined
});

このコードは、一見すると複雑に見えるかもしれませんが、実際には開発者にバックエンドインフラストラクチャに対する細かな制御を提供しています。

カスタムCDKが必要な理由

Gen2でカスタムCDKが必要な理由は主に以下の通りです。

  1. 設計哲学の違い: Gen2はより柔軟で拡張可能なプラットフォームを目指しており、開発者により多くの制御権を与えることを重視
  2. 開発段階: Gen2は比較的新しく、すべての機能をAmplify固有の抽象化でカバーするには至っていない
  3. AWS CDKの直接活用: Gen2の設計はCDKを直接活用することで、CDKエコシステム全体にアクセスできる利点を重視
  4. 特殊なユースケースへの対応: 複雑なバックエンドアーキテクチャやカスタムワークフローを必要とするケースに対応

重要なのは、Gen2でカスタムCDKが必要なケースは「制限」というよりも「可能性の拡大」と捉えるべき点です。開発者はAmplifyの抽象化を利用しながらも、必要に応じてより複雑なAWSサービス構成に直接アクセスできます。

例えば、REST APIの場合、Gen1のCLIアプローチでは事前定義されたパターンに従う必要がありました。一方Gen2では、API Gateway、Lambda、その他のサービスを組み合わせて独自のパターンを構築できます。これは、特に複雑なマイクロサービスアーキテクチャやカスタムビジネスロジックを持つバックエンドにとって大きな利点となります。

結論

AWS Amplify Gen2は、バックエンド開発のアプローチを根本的に変革し、フロントエンド開発者がクラウドリソースを管理する方法に新たなパラダイムシフトをもたらしています。

バックエンド開発体験の進化

Gen1からGen2への移行は単なるツール変更を超えた、バックエンド開発における思考法と実装方法の根本的な転換です。このアプローチの変化がもたらす主な利点は以下の通りです。

  1. 透明性の向上: Gen2のコード優先アプローチでは、バックエンドインフラストラクチャが抽象的なCLIコマンドではなく具体的なTypeScriptコードとして表現されるため、開発者は何が作成されるかを正確に把握でき、理解と制御が容易になります。
  2. 開発者の自由度拡大: TypeScriptとCDKを直接活用することで、開発者はAmplifyの抽象化に縛られることなく、より複雑で柔軟なバックエンドアーキテクチャを必要に応じて構築できるようになりました。
  3. DevOps文化との親和性: コードとしてインフラストラクチャを定義するアプローチによりCI/CD、コードレビュー、バージョン管理などの現代的なソフトウェア開発プラクティスがバックエンド構築にもシームレスに適用できるようになりました。
  4. 学習投資と柔軟性のバランス: Gen2は最初の学習コストがやや高い面もありますが、TypeScriptとAWS CDKという業界標準技術への投資となり、その見返りとして得られる生産性と柔軟性は、特に大規模なプロジェクトやチーム開発において大きな価値をもたらします。

将来の展望と期待感

AWS Amplify Gen2は依然として進化の途上にあり、今後さらに多くのネイティブ機能サポートが追加されることが期待されます。同時に、CDKを直接活用するアプローチは、AWS新サービスが常に最初からアクセス可能であることを意味します。つまり、開発者はAmplifyのネイティブサポートを待つことなく、最新のAWSサービスを利用できます。

最終的に、Gen2はフロントエンド開発者のためのバックエンド開発という概念を新たな視点で捉え直し、コード、設定、デプロイの間のギャップを埋めるものです。これはフルスタック開発の進化における重要な一歩であり、フロントエンド開発者がクラウドインフラストラクチャをより直感的に理解し活用できる道を開いています。特に今後のバージョンアップでさらに機能が拡充され、現在カスタムCDKが必要な領域もより簡易に実装できるようになることで、Gen2の真価がさらに発揮されることでしょう。AWS Amplifyチームの継続的な改善への取り組みに、引き続き注目していきたいと思います。

参考資料