PR

AWS初期構築:IaCを活用したフルサーバーレスアーキテクチャAWS CDK導入ガイド

AWS初心者の方向けに、Infrastructure as Code (IaC)を使ったフルサーバーレスアーキテクチャの構築手順を詳しく解説します。このガイドでは、AWS CDKを使ってインフラをコード化し、Lambda、DynamoDB、API Gateway、S3などのサーバーレスサービスを組み合わせた基本的なアプリケーションを構築していきます。

前提条件

  • AWSアカウントを持っていること
  • Node.jsがインストールされていること
  • AWS CLIがインストールされていること
  • 基本的なJavaScript/TypeScriptの知識があること

1. 開発環境のセットアップ

まずは必要なツールをインストールしましょう。

# AWS CDKのインストール
npm install -g aws-cdk

# AWS CLIの設定(まだ設定していない場合)
aws configure

AWS CLIの設定では、アクセスキー、シークレットキー、リージョン(例:ap-northeast-1)を入力します。

2. CDKプロジェクトの作成

新しいCDKプロジェクトを作成します。

# プロジェクト用のディレクトリを作成
mkdir serverless-app
cd serverless-app

# CDKプロジェクトの初期化(TypeScript使用)
cdk init app --language typescript

3. サーバーレスコンポーネントの定義

プロジェクトのlibディレクトリにあるスタックファイル(serverless-app-stack.ts)を編集して、以下のサーバーレスリソースを定義します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as path from 'path';

export class ServerlessAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // DynamoDBテーブルの作成
    const table = new dynamodb.Table(this, 'Items', {
      partitionKey: {
        name: 'id',
        type: dynamodb.AttributeType.STRING
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // デモ用。本番環境では注意
    });

    // S3バケットの作成(Webホスティング用)
    const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
      websiteIndexDocument: 'index.html',
      publicReadAccess: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // デモ用。本番環境では注意
    });

    // Lambda関数の作成(GET APIハンドラー)
    const getItemsFunction = new lambda.Function(this, 'GetItemsFunction', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'get-items.handler',
      code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')),
      environment: {
        TABLE_NAME: table.tableName,
      },
    });

    // Lambda関数の作成(POST APIハンドラー)
    const createItemFunction = new lambda.Function(this, 'CreateItemFunction', {
      runtime: lambda.Runtime.NODEJS_18_X,
      handler: 'create-item.handler',
      code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')),
      environment: {
        TABLE_NAME: table.tableName,
      },
    });

    // DynamoDBテーブルへのアクセス権限をLambda関数に付与
    table.grantReadData(getItemsFunction);
    table.grantWriteData(createItemFunction);

    // API Gatewayの作成
    const api = new apigateway.RestApi(this, 'ItemsApi', {
      restApiName: 'Items Service',
      description: 'This service manages items.',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
      },
    });

    // APIリソースとメソッドの設定
    const items = api.root.addResource('items');
    items.addMethod('GET', new apigateway.LambdaIntegration(getItemsFunction));
    items.addMethod('POST', new apigateway.LambdaIntegration(createItemFunction));

    // フロントエンドのデプロイ設定
    new s3deploy.BucketDeployment(this, 'DeployWebsite', {
      sources: [s3deploy.Source.asset(path.join(__dirname, '../frontend/build'))],
      destinationBucket: websiteBucket,
    });

    // 出力値の設定
    new cdk.CfnOutput(this, 'ApiUrl', {
      value: api.url,
      description: 'API Gateway エンドポイントURL',
    });

    new cdk.CfnOutput(this, 'WebsiteUrl', {
      value: websiteBucket.bucketWebsiteUrl,
      description: 'フロントエンドWebサイトURL',
    });
  }
}

4. Lambda関数の実装

lambdaディレクトリを作成し、必要なハンドラー関数を実装します。

mkdir -p lambda

4.1 アイテム取得用Lambda関数

// lambda/get-items.js
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

exports.handler = async function(event) {
  console.log('GET /items イベント:', JSON.stringify(event, null, 2));
  
  const params = {
    TableName: process.env.TABLE_NAME
  };
  
  try {
    const data = await dynamodb.scan(params).promise();
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(data.Items)
    };
  } catch (error) {
    console.error('DynamoDBクエリエラー:', error);
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({ error: 'データの取得に失敗しました' })
    };
  }
};

4.2 アイテム作成用Lambda関数

// lambda/create-item.js
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const dynamodb = new AWS.DynamoDB.DocumentClient();

exports.handler = async function(event) {
  console.log('POST /items イベント:', JSON.stringify(event, null, 2));
  
  try {
    // リクエストボディの解析
    const requestBody = JSON.parse(event.body);
    
    if (!requestBody.name) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: '名前は必須です' })
      };
    }
    
    const item = {
      id: uuidv4(),
      name: requestBody.name,
      description: requestBody.description || '',
      createdAt: new Date().toISOString()
    };
    
    await dynamodb.put({
      TableName: process.env.TABLE_NAME,
      Item: item
    }).promise();
    
    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(item)
    };
  } catch (error) {
    console.error('DynamoDB書き込みエラー:', error);
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({ error: 'アイテムの作成に失敗しました' })
    };
  }
};

5. Lambda関数の依存関係インストール

Lambda関数で使用するuuidパッケージをインストールします。

cd lambda
npm init -y
npm install uuid
cd ..

6. フロントエンドの実装

シンプルなReactフロントエンドを作成します。まず必要なディレクトリを作成します。

mkdir -p frontend/build

6.1 HTML

<!-- frontend/build/index.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>サーバーレスアプリデモ</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <h1>サーバーレスアプリケーションデモ</h1>
    
    <div class="form-container">
      <h2>新しいアイテムを追加</h2>
      <form id="item-form">
        <div class="form-group">
          <label for="item-name">名前:</label>
          <input type="text" id="item-name" required>
        </div>
        <div class="form-group">
          <label for="item-description">説明:</label>
          <textarea id="item-description"></textarea>
        </div>
        <button type="submit">追加</button>
      </form>
    </div>
    
    <div class="items-container">
      <h2>アイテム一覧</h2>
      <button id="refresh-button">更新</button>
      <div id="items-list"></div>
    </div>
  </div>
  
  <script src="app.js"></script>
</body>
</html>

6.2 CSS

/* frontend/build/styles.css */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: Arial, sans-serif;
  line-height: 1.6;
  color: #333;
  background-color: #f4f4f4;
}

.container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

h1 {
  text-align: center;
  margin-bottom: 30px;
  color: #2c3e50;
}

h2 {
  color: #3498db;
  margin-bottom: 15px;
}

.form-container, .items-container {
  background: #fff;
  padding: 20px;
  border-radius: 5px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

input, textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

textarea {
  height: 100px;
}

button {
  display: inline-block;
  background: #3498db;
  color: #fff;
  border: none;
  padding: 10px 15px;
  border-radius: 5px;
  cursor: pointer;
  font-size: 16px;
}

button:hover {
  background: #2980b9;
}

#refresh-button {
  margin-bottom: 15px;
}

.item-card {
  background: #f9f9f9;
  border-left: 4px solid #3498db;
  padding: 15px;
  margin-bottom: 10px;
  border-radius: 4px;
}

.item-name {
  font-weight: bold;
  font-size: 18px;
  margin-bottom: 5px;
}

.item-description {
  color: #666;
  margin-bottom: 5px;
}

.item-date {
  font-size: 12px;
  color: #999;
}

6.3 JavaScript

// frontend/build/app.js
document.addEventListener('DOMContentLoaded', () => {
  // デプロイ後にAPI GatewayのURLに置き換える必要があります
  const API_URL = 'API_GATEWAY_URLをデプロイ後に設定してください/items';
  
  const itemForm = document.getElementById('item-form');
  const itemsList = document.getElementById('items-list');
  const refreshButton = document.getElementById('refresh-button');
  
  // アイテム一覧を取得
  async function fetchItems() {
    try {
      const response = await fetch(API_URL);
      if (!response.ok) {
        throw new Error('APIリクエストに失敗しました');
      }
      
      const items = await response.json();
      displayItems(items);
    } catch (error) {
      console.error('アイテムの取得に失敗しました:', error);
      alert('アイテムの取得に失敗しました。詳細はコンソールを確認してください。');
    }
  }
  
  // アイテムを画面に表示
  function displayItems(items) {
    itemsList.innerHTML = '';
    
    if (items.length === 0) {
      itemsList.innerHTML = '<p>アイテムはありません。新しいアイテムを追加してください。</p>';
      return;
    }
    
    items.forEach(item => {
      const itemElement = document.createElement('div');
      itemElement.className = 'item-card';
      
      const date = new Date(item.createdAt).toLocaleString('ja-JP');
      
      itemElement.innerHTML = `
        <div class="item-name">${escapeHtml(item.name)}</div>
        <div class="item-description">${escapeHtml(item.description || '')}</div>
        <div class="item-date">作成日時: ${date}</div>
      `;
      
      itemsList.appendChild(itemElement);
    });
  }
  
  // HTMLエスケープ処理
  function escapeHtml(str) {
    if (!str) return '';
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }
  
  // 新しいアイテムを作成
  async function createItem(name, description) {
    try {
      const response = await fetch(API_URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ name, description })
      });
      
      if (!response.ok) {
        throw new Error('APIリクエストに失敗しました');
      }
      
      const newItem = await response.json();
      console.log('新しいアイテムが作成されました:', newItem);
      
      // アイテム一覧を更新
      await fetchItems();
      
      return true;
    } catch (error) {
      console.error('アイテムの作成に失敗しました:', error);
      alert('アイテムの作成に失敗しました。詳細はコンソールを確認してください。');
      return false;
    }
  }
  
  // フォーム送信イベント
  itemForm.addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const nameInput = document.getElementById('item-name');
    const descriptionInput = document.getElementById('item-description');
    
    const name = nameInput.value.trim();
    const description = descriptionInput.value.trim();
    
    if (!name) {
      alert('名前を入力してください');
      return;
    }
    
    const success = await createItem(name, description);
    if (success) {
      nameInput.value = '';
      descriptionInput.value = '';
    }
  });
  
  // 更新ボタンクリックイベント
  refreshButton.addEventListener('click', fetchItems);
  
  // 初期表示時にアイテム一覧を取得
  fetchItems();
});

7. CDKアプリのデプロイ

プロジェクトルートディレクトリに戻り、以下のコマンドを実行してCDKアプリをデプロイします。

# CDKアプリのスタックをAWS環境にブートストラップ(初回のみ)
cdk bootstrap

# CDKアプリのデプロイ
cdk deploy

デプロイが完了すると、コンソールに以下のような出力が表示されます:

Outputs:
ServerlessAppStack.ApiUrl = https://abcdefghij.execute-api.ap-northeast-1.amazonaws.com/prod/
ServerlessAppStack.WebsiteUrl = http://serverlessappstack-websitebucketxxxxxx.s3-website-ap-northeast-1.amazonaws.com

8. フロントエンドの設定更新

デプロイ後、出力されたAPI GatewayのURLを使って、フロントエンドのJavaScriptファイルを更新します。S3バケット内のapp.jsファイルを開き、API_URL定数を更新します:

const API_URL = 'https://abcdefghij.execute-api.ap-northeast-1.amazonaws.com/prod/items';

更新したファイルをS3バケットにアップロードします:

aws s3 cp frontend/build/app.js s3://serverlessappstack-websitebucketxxxxxx/app.js

9. アプリケーションの動作確認

ブラウザでWebsiteURLにアクセスし、アプリケーションが正常に動作するか確認します。以下の操作を試してみましょう:

  1. 「新しいアイテムを追加」フォームに名前と説明を入力し、「追加」ボタンをクリック
  2. 「更新」ボタンをクリックして、追加したアイテムが表示されるか確認

10. クリーンアップ

不要になったリソースを削除するには、以下のコマンドを実行します:

cdk destroy

まとめ

このハンズオンでは、AWS CDKを使用してフルサーバーレスアーキテクチャを構築しました。主な構成要素は以下の通りです:

  • API Gateway: RESTful APIエンドポイントを提供
  • Lambda: バックエンドロジックの実装
  • DynamoDB: データの永続化
  • S3: フロントエンドのホスティング

このアーキテクチャの利点は以下の通りです:

  1. スケーラビリティ: トラフィックに応じて自動的にスケール
  2. コスト効率: 使用した分だけ支払い(固定費なし)
  3. メンテナンス性: サーバー管理が不要
  4. 高可用性: AWSマネージドサービスの信頼性

また、IaCアプローチの利点として:

  1. バージョン管理: インフラの変更履歴を追跡可能
  2. 再現性: 同じ環境を何度でも再現可能
  3. 自動化: デプロイプロセスの自動化
  4. ドキュメント化: コード自体がドキュメントとして機能

ぜひこのハンズオンを基に、自分だけのサーバーレスアプリケーションを構築してみてください!

コメント

タイトルとURLをコピーしました