PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

AWS SAM でアプリケーションのデプロイと変更管理を効率化

こんにちは。24年度に入社しました、PLAY CLOUD本部プラットフォーム技術部開発第3グループに所属しています、大城と申します。 業務でSAM (Serverless Application Model) を利用してサーバレスのAPIを構築する機会があり、その過程でSAM で利用する CloudFormation で関連リソースの構築まで行いました。コンソールから手作業でリソースを作成する方法もありますが、IaC(Infrastructure as Code)の考え方で実装することで、管理のしやすさと再現性を向上させることができます。 本記事では、AWS SAMテンプレートを使ったリソース管理のメリットと、実際にシステムを構築する方法について解説します。ぜひ参考にしていただければと思います。

この記事を読むための前提知識

まずは簡単に、この記事で使用するAWSサービスであるCloudFormationとSAMについて説明します。

  • SAM (Serverless Application Model): サーバーレスアプリケーション向けのCloudFormationの拡張機能。シンプルな構文でAPI Gateway、Lambda、DynamoDBなどのリソースを簡潔に定義できます。 CloudFormationがAWSリソース全般を定義するのに対し、SAMを使用するとサーバーレスアプリケーション特有のリソース(Lambda、API Gateway、DynamoDBなど)を簡潔に記述することができます。
  • CloudFormation (CFn): AWSリソースをコード(テンプレート)で定義し、自動でプロビジョニングするサービス。一度に複数のリソースを一貫性を持って作成・更新できるのが特徴です。

CloudFormationとSAMの関係

SAMテンプレートは内部的にCloudFormationに変換されます。 つまり、

  1. SAMはCloudFormationの上に構築されている
  2. SAMテンプレートは最終的にCloudFormationスタックとしてデプロイされる
  3. CloudFormationでできることはSAMでもできるが、SAMはサーバーレスアプリケーション向けに最適化されている

簡単に言うと、SAMはサーバーレスアプリケーションに焦点を当てて、CloudFormationをより使いやすく拡張したものです。

より詳細な情報はAWS公式ドキュメントをご確認ください。

SAMテンプレートでAWSリソースを管理するメリット

AWSのリソースはコンソールで手動で作成・管理することもできますが、SAMテンプレートを使ったIaCを活用することで、以下のようなメリットがあります。

  • 作業の自動化: 更新作業をコンソールで手動操作したり、複雑なコマンドを実行するリスクを低減
  • 透明性の向上: 新しくチームに参加したメンバーでもテンプレートを見れば、システムで使用しているリソースを把握できる
  • 人的ミスの削減: 手動操作によるミスを減らし、一貫性のあるデプロイを実現
  • バージョン管理: インフラの変更履歴をGitなどで管理できる
  • リソース間の依存関係の明確化: テンプレートによってリソース間の関係性が明示的に定義される(意外なリソースが関わっているかもしれない)
  • 組み込みポリシーテンプレートを使える: SAMはDynamoDBReadPolicyDynamoDBCrudPolicyなどの組み込みポリシーテンプレートを提供しているため、IAMロールの設定が簡略化できる
  • ロールバック機能: リソースのプロビジョニングに失敗した際、ロールバックするように設定できる

特に重要なのは、システムの規模が大きくなるほどIaCのメリットが大きくなる点です。リソース数が増えるほど手動管理の複雑さとリスクは指数関数的に増加しますが、IaCを使用することでその複雑さを解消できます。

実際にシステムを作ってみる

それでは実際に、SAMテンプレートを使って簡単なシステムを構築してみます。 今回は、ECサイトの商品カタログAPIを例として作成します。仕様は以下の通りです。

  • エンドポイント: GET/products/{id}
  • 機能: 指定されたIDの商品情報を取得する
  • レスポンス: 商品が見つかった場合は商品情報をJSONで返す。見つからない場合は404エラーを返す。

なお、商品データの構造は以下のようになります。

response.json

{
  "product_id": "p001",
  "name": "スマートフォン",
  "price": 159800,
  "description": "最新モデルのスマートフォン"
}

システム構成

このシステムでは3つの主要素から構成されています。

  1. API Gateway: ユーザーからのHTTPリクエストを受けるエンドポイント
  2. Lambda: 商品情報の取得ロジック
  3. DynamoDB: 商品データを格納するデータベース

テンプレート

上記システムを構築するSAMテンプレートを書いてみます。 Resourcesに今回使用するDynamoDBテーブルとLambda関数を定義します。 Outputsにはデプロイ後に使う情報を定義します。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: シンプルな商品カタログAPI

Resources:
  # 商品情報を保存するDynamoDBテーブル
  ProductCatalogTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: product-catalog
      BillingMode: PAY_PER_REQUEST # 使用量に応じた料金体系
      AttributeDefinitions:
        - AttributeName: product_id
          AttributeType: S
      KeySchema:
        - AttributeName: product_id
          KeyType: HASH

  # 商品検索用Lambda関数
  ProductSearchFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: 'es2022'
        EntryPoints:
          - index.ts
    Properties:
      FunctionName: product-search
      CodeUri: ./src/search/
      Handler: index.handler
      Runtime: nodejs22.x
      Timeout: 10
      MemorySize: 128
      Policies: # 組み込みポリシーテンプレートで権限設定を簡略化
        - DynamoDBReadPolicy:
            TableName: !Ref ProductCatalogTable
      Events:
        # API Gatewayのイベント設定
        ApiEventById:
          Type: Api
          Properties:
            Path: /products/{id}
            Method: get

Outputs:
  ApiEndpoint:
    Description: 'API Gateway endpoint URL'
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/products'
  TableName:
    Description: 'DynamoDB Table name'
    Value: !Ref ProductCatalogTable

ここで、4点ポイントがあります。

  1. API Gatewayの設定が簡略化されている: 通常のCloudFormationでは、API Gateway自体の定義、ステージの設定、Lambda連携など、多くのリソースを個別に設定する必要がありますが、SAMではEventsセクションを使って数行で記述できます。

  2. IAMロールの設定が簡略化されている: 通常のCloudFormationではIAMポリシーを詳細に記述する必要がありますが、SAMではDynamoDBReadPolicyのような組み込みテンプレートを使うことで、複雑なIAM設定を1行で済ませることができます。

  3. TypeScript を JavaScript にトランスパイルするための設定をしている: Metadata セクションでは、Lambda 関数のビルド方法を指定できます。 今回は、esbuild を使用して TypeScript を JavaScript にトランスパイルしています。

  4. TypeScriptプロジェクトの設定(tsconfig.json): Lambda関数をTypeScriptで実装するため、プロジェクトルートにtsconfig.jsonを配置しています。一例として、今回使用したtsconfig.jsonを以下に記載します。

    tsconfig.json
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "NodeNext",
        "esModuleInterop": true,
        "strict": true
      },
      "include": ["src/**/*"]
    }

Lambda関数の実装例

次に、SAMテンプレートで定義したLambda関数をTypeScriptで実装します。

src/search/index.ts

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient();
const dynamoDB = DynamoDBDocumentClient.from(client);

// 商品の型
type Product = {
  product_id: string;
  name: string;
  price: number;
  description: string;
}

const createResponse = (
  statusCode: number,
  body: Record<string, any>,
): APIGatewayProxyResult => {
  return {
    statusCode,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  };
};

export const handler = async (
  event: APIGatewayProxyEvent,
): Promise<APIGatewayProxyResult> => {
  try {
    const productId = event.pathParameters?.id;

    if (!productId) {
      return createResponse(400, { message: '商品IDが指定されていません' });
    }

    // 特定の商品を検索
    const command = new GetCommand({
      TableName: 'product-catalog',
      Key: { product_id: productId },
    });

    const result = await dynamoDB.send(command);

    if (!result.Item) {
      return createResponse(404, { message: '商品が見つかりません' });
    }

    const product = result.Item as Product;

    return createResponse(200, product);
  } catch (error) {
    console.error('Error:', error);
    return createResponse(500, {
      message: 'エラーが発生しました',
      error: error instanceof Error ? error.message : String(error),
    });
  }
};

デプロイ方法

AWS SAM CLIを使ってデプロイします。 ここでは初めてSAMを使う方でも実行できるよう、手順を説明します。

準備

デプロイにはAWS CLI、AWS SAM CLIのインストールと、必要な権限が必要です。ここではすでにAWS CLIが設定済みという前提で進めます。必要な権限は以下の通りです。

  • API Gateway、Lambda、DynamoDBの作成権限
  • CloudFormationスタックの作成権限

SAM CLIのインストール

AWS SAM CLIをインストールします。公式ドキュメントに従って、お使いのOS向けの手順で進めてください。

プロジェクトの構築

以下のようなディレクトリ構造でプロジェクトを構築します。

product-catalog-api/
├── template.yaml           # SAMテンプレートファイル
├── tsconfig.json           # Lambda用TypeScript設定ファイル
├── src/
│   └── search/             
│       ├── index.ts        # 商品検索Lambda関数
│       ├── package.json    # 依存関係の定義
│       └── node_modules/   # インストールされたパッケージ
├── scripts/
│   ├── tsconfig.json       # スクリプト用TypeScript設定ファイル
│   └── load-sample-data.ts # サンプルデータ登録スクリプト
└── samconfig.toml          # デプロイ設定(自動生成)

先ほど紹介したテンプレートをtemplate.yamlとして保存し、Lambda関数のコードを該当するディレクトリに保存します。

サンプルデータの準備

デプロイ後にAPIの動作確認を行うために、DynamoDBにサンプルデータを追加するスクリプトを作成します。

スクリプト例

scripts/load-sample-data.ts

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { 
  DynamoDBDocumentClient, 
  PutCommand 
} from '@aws-sdk/lib-dynamodb';

type Product = {
  product_id: string;
  name: string;
  price: number;
  description: string;
}

const client = new DynamoDBClient({ region: 'ap-northeast-1' });
const dynamoDB = DynamoDBDocumentClient.from(client);

// サンプルデータ
const sampleProducts: Product[] = [
  {
    product_id: 'p001',
    name: 'スマートフォン',
    price: 159800,
    description: '最新モデルのスマートフォン'
  },
  {
    product_id: 'p002',
    name: 'ノートパソコン',
    price: 248800,
    description: '最新モデルのノートパソコン'
  },
  {
    product_id: 'p003',
    name: 'ワイヤレスイヤホン',
    price: 24800,
    description: 'ノイズキャンセリング機能付きワイヤレスイヤホン'
  }
];

async function loadSampleData(): Promise<void> {
  
  for (const product of sampleProducts) {
    const command = new PutCommand({
      TableName: 'product-catalog',
      Item: product
    });
    
    try {
      await dynamoDB.send(command);
      console.log(`商品を追加しました: ${product.name}`);
    } catch (error) {
      console.error(`エラー: ${error instanceof Error ? error.message : String(error)}`);
    }
  }
  
  console.log('サンプルデータの登録が完了しました');
}

// スクリプト実行
loadSampleData().catch(error => {
  console.error('エラーが発生しました:', error);
  process.exit(1);
});

SAMコマンドによるデプロイ

ビルド

まずプロジェクトのルートディレクトリで以下のコマンドを実行します。

sam build

このコマンドはLambda関数のコードを含むデプロイパッケージを作成します。成功すると.aws-samディレクトリが生成されます。

デプロイ

初回のデプロイはガイド付きデプロイを使用すると便利です。

sam deploy --guided

このコマンドを実行すると、テンプレートを配置するバケットやプレフィックスを対話式のCLIで指定してデプロイすることができます。

再デプロイ

コードを更新し、再デプロイする際は同じようにビルド、デプロイコマンドを入力します。

sam build
sam deploy

初回デプロイ時にsamconfig.tomlファイルが作成されているため、2回目以降は--guidedオプションなしでデプロイできます。

デプロイが正常に完了すると、以下のようなログが表示されます。

CloudFormation outputs from deployed stack
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                                                                        
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 TableName                                                                                                                                                                                                  
Description         DynamoDB Table name                                                                                                                                                                                        
Value               product-catalog                                                                                                                                                                                          

Key                 ApiEndpoint                                                                                                                                                                                                
Description         API Gateway endpoint URL                                                                                                                                                                                   
Value               https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/products                                                                                                                                  
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

サンプルデータの追加

デプロイ後、先ほど作成したスクリプトを実行してサンプルデータを追加します。

ts-node scripts/load-sample-data.ts

デプロイしたAPIの動作確認

出力されたエンドポイントURLにアクセスして、APIの動作確認を行います。

実行結果

> curl https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod/products/p001
{"description":"最新モデルのスマートフォン","price":159800,"name":"スマートフォン","product_id":"p001"}

期待通りにデータが取得できました。

これだけの作業で、商品APIを構築できました!手動でコンソールからこれらすべてのリソースを設定するには、APIゲートウェイの設定、Lambdaの実装・設定、DynamoDBテーブルの作成、IAM権限の設定など、多くの作業が必要になりますが、SAMテンプレートを使うことでこれらがコード数十行で実装でき、繰り返し再現性高く構築できるのです。

まとめ

今回は、AWS SAMテンプレートを使って商品カタログAPIシステムを構築する方法を紹介しました。SAM を用いると、コード含めて CloudFormation での管理ができるため、変更管理や追跡が容易になるというメリットがあります。

特に、SAMが提供する組み込みポリシーテンプレートや簡略化された構文は、サーバーレスアプリケーションの開発を効率化します。CloudFormationの基盤の上に構築されながらも、より簡潔でわかりやすい構文を提供しているのがSAMの大きな魅力です。

実際のECサイトやカタログシステムではより複雑な要件が発生しますが、同様のアプローチでIaCを活用することで、複雑なシステムでも管理しやすくなります。

皆さんもぜひ、AWS SAMテンプレートを使ったインフラ管理を試してみてください!