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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 新しいアイテムを作成
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にアクセスし、アプリケーションが正常に動作するか確認します。以下の操作を試してみましょう:
- 「新しいアイテムを追加」フォームに名前と説明を入力し、「追加」ボタンをクリック
- 「更新」ボタンをクリックして、追加したアイテムが表示されるか確認
10. クリーンアップ
不要になったリソースを削除するには、以下のコマンドを実行します:
cdk destroy
まとめ
このハンズオンでは、AWS CDKを使用してフルサーバーレスアーキテクチャを構築しました。主な構成要素は以下の通りです:
- API Gateway: RESTful APIエンドポイントを提供
- Lambda: バックエンドロジックの実装
- DynamoDB: データの永続化
- S3: フロントエンドのホスティング
このアーキテクチャの利点は以下の通りです:
- スケーラビリティ: トラフィックに応じて自動的にスケール
- コスト効率: 使用した分だけ支払い(固定費なし)
- メンテナンス性: サーバー管理が不要
- 高可用性: AWSマネージドサービスの信頼性
また、IaCアプローチの利点として:
- バージョン管理: インフラの変更履歴を追跡可能
- 再現性: 同じ環境を何度でも再現可能
- 自動化: デプロイプロセスの自動化
- ドキュメント化: コード自体がドキュメントとして機能
ぜひこのハンズオンを基に、自分だけのサーバーレスアプリケーションを構築してみてください!
コメント