今日のクラウド時代において、ビジネスの成長に合わせてスケールできる堅牢な基盤を構築することは非常に重要です。しかし、多くのスタートアップや個人開発者にとって、AWS上に本格的なSaaS基盤を構築することは複雑に感じられるかもしれません。本記事では、AWS Organizationsを活用した多アカウント戦略と、フルサーバーレスアーキテクチャを組み合わせることで、コスト効率が良く、セキュアで、拡張性のあるSaaSプラットフォームを構築するためのテンプレートをご紹介します。
なぜAWS Organizationsとサーバーレスなのか?
AWS Organizationsのメリット
AWS Organizationsを使用すると、複数のAWSアカウントを一元管理できるようになります。これにより:
- 環境分離: 開発、ステージング、本番環境を完全に分離
- セキュリティ強化: サービスコントロールポリシー(SCP)による権限制御
- コスト管理: アカウント単位でのコスト追跡と最適化
- ガバナンス: 組織全体での一貫したポリシー適用
フルサーバーレスアーキテクチャのメリット
サーバーレスアーキテクチャは、特に初期段階のSaaSプロダクトに多くの利点をもたらします:
- 低いランニングコスト: 使用した分だけ支払い、無料枠の活用
- 運用負荷の軽減: サーバー管理不要でDevOpsリソースを削減
- 自動スケーリング: トラフィック変動に応じた自動調整
- 高い可用性: AWS管理のサービスによる堅牢性
アーキテクチャ概要
このテンプレートは以下の構成に基づいています:
[管理アカウント]
└─ OU: SaaS Projects
└─ メンバーアカウント: project-dev (CDK/TFで構築)
[project-devアカウントの構成]
- API Gateway → Lambda
- Lambda → DynamoDB
- Cognito → 認証
- S3 (静的ホスティング)
- CloudFront (配信)
技術スタックの選定理由
分類 | サービス | 選定理由 |
---|---|---|
組織管理 | AWS Organizations | 多アカウント制御とSCPによる細かい権限設定 |
認証 | Amazon Cognito | サーバーレスな認証基盤、SNS連携、多要素認証対応 |
API | API Gateway + Lambda | 完全サーバーレスで従量課金、スケーラブルなAPI環境 |
データ | DynamoDB | NoSQLによる柔軟なスキーマ、低コストで高性能 |
UI | Next.js (App Router) | SSR対応の高速Webフロント、SEO最適化 |
ストレージ | S3 + CloudFront | 静的コンテンツの高速配信とキャッシュ最適化 |
インフラ構築 | AWS CDK / Terraform | クロスアカウント対応のインフラコード |
ステップバイステップ構築ガイド
1. AWS Organizations構成(管理アカウント)
まずは、AWS Organizationsを設定して多アカウント構造を構築します。
- AWS Organizationsの有効化:
- AWSマネジメントコンソールから「Organizations」サービスにアクセス
- 「組織の作成」を選択し、ウィザードに従って設定
- OU(組織単位)の作成:
- Organizationsダッシュボードから「OUの作成」を選択
- 名前を「SaaSProjects」として作成
- メンバーアカウントの作成:
- 「アカウントの作成」を選択
- メールアドレス:
project-dev@yourdomain.com
- アカウント名:
ProjectDev
- 作成したOUに配置
- SCPによるリソース制限(オプション):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"ec2:RunInstances",
"rds:CreateDBInstance"
],
"Resource": "*"
}
]
}
2. クロスアカウント用IAMロール設定
マネジメントアカウントからリソースアカウントにアクセスするためのIAMロールを確認または作成します。
- OrganizationAccountAccessRoleの確認:
- 新しく作成されたアカウントには通常、自動的に作成されています
- IAMコンソールで確認、なければ手動で作成
- 信頼ポリシーの設定:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<管理アカウントID>:root"
},
"Action": "sts:AssumeRole"
}]
}
3. CDKによるインフラ構築(TypeScript)
管理アカウントからプロジェクトアカウントへのクロスアカウントデプロイを設定します。
CDKプロジェクトのセットアップ:
mkdir saas-template && cd saas-template
cdk init app --language typescript
npm install aws-cdk-lib constructs
クロスアカウントデプロイ設定:
// bin/saas-template.ts
import * as cdk from 'aws-cdk-lib';
import { SaaSStack } from '../lib/saas-stack';
const app = new cdk.App();
// 環境の設定(リソースアカウントとリージョン)
const devEnv = {
account: '<RESOURCE_ACCOUNT_ID>',
region: 'ap-northeast-1'
};
new SaaSStack(app, 'SaaSStack', {
env: devEnv,
crossAccountRole: 'OrganizationAccountAccessRole'
});
スタック定義:
// lib/saas-stack.ts
import * as cdk 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 dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
export interface SaaSStackProps extends cdk.StackProps {
crossAccountRole?: string;
}
export class SaaSStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: SaaSStackProps) {
super(scope, id, props);
// DynamoDB テーブル
const table = new dynamodb.Table(this, 'ItemsTable', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, // 従量課金モード
removalPolicy: cdk.RemovalPolicy.DESTROY // 開発環境用
});
// API Lambda 関数
const apiHandler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: {
TABLE_NAME: table.tableName
}
});
// Lambda にDynamoDBへのアクセス権限を付与
table.grantReadWriteData(apiHandler);
// API Gateway
const api = new apigw.RestApi(this, 'SaaSApi', {
deployOptions: {
stageName: 'api'
}
});
// APIのルート設定
const items = api.root.addResource('items');
items.addMethod('GET', new apigw.LambdaIntegration(apiHandler));
items.addMethod('POST', new apigw.LambdaIntegration(apiHandler));
// Cognito ユーザープール
const userPool = new cognito.UserPool(this, 'SaaSUserPool', {
selfSignUpEnabled: true,
autoVerify: { email: true },
standardAttributes: {
email: { required: true, mutable: true }
}
});
// クライアントアプリの設定
const userPoolClient = userPool.addClient('WebClient', {
authFlows: {
userPassword: true,
userSrp: true
}
});
// フロントエンド用のS3バケット
const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY // 開発環境用
});
// CloudFront Distribution
const distribution = new cloudfront.Distribution(this, 'WebDistribution', {
defaultBehavior: {
origin: new origins.S3Origin(websiteBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
},
additionalBehaviors: {
'/api/*': {
origin: new origins.RestApiOrigin(api),
allowedMethods: cloudfront.AllowedMethods.ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED
}
}
});
// 出力
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
new cdk.CfnOutput(this, 'WebsiteURL', { value: `https://${distribution.distributionDomainName}` });
}
}
Lambda関数の実装例:
// lambda/index.js
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME;
exports.handler = async (event) => {
console.log('Event: ', JSON.stringify(event, null, 2));
try {
// メソッドとパスの取得
const method = event.httpMethod;
const path = event.path;
if (path === '/items' && method === 'GET') {
// 全アイテム取得
const result = await dynamodb.scan({
TableName: TABLE_NAME
}).promise();
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result.Items)
};
}
else if (path === '/items' && method === 'POST') {
// 新アイテム作成
const item = JSON.parse(event.body);
item.id = Date.now().toString(); // 簡易ID生成
await dynamodb.put({
TableName: TABLE_NAME,
Item: item
}).promise();
return {
statusCode: 201,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
};
}
// 不明なパス
return {
statusCode: 404,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Not Found' })
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Internal Server Error' })
};
}
};
デプロイ実行:
npm run build
cdk deploy
4. Terraformによる構築(代替方法)
CDKの代わりにTerraformを使用する場合の構成例です。
プロバイダー設定:
provider "aws" {
region = "ap-northeast-1"
assume_role {
role_arn = "arn:aws:iam:::role/OrganizationAccountAccessRole"
}
}
DynamoDBテーブル:
resource "aws_dynamodb_table" "items" {
name = "items"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}
Lambdaと実行ロール:
resource "aws_iam_role" "lambda_exec" {
name = "lambda_exec_role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow"
}]
})
}
resource "aws_iam_role_policy" "lambda_policy" {
name = "lambda_policy"
role = aws_iam_role.lambda_exec.id
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:Scan"
],
"Resource": aws_dynamodb_table.items.arn,
"Effect": "Allow"
}, {
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}]
})
}
resource "aws_lambda_function" "api_handler" {
function_name = "api_handler"
handler = "index.handler"
runtime = "nodejs18.x"
role = aws_iam_role.lambda_exec.arn
filename = "lambda.zip"
source_code_hash = filebase64sha256("lambda.zip")
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.items.name
}
}
}
API Gateway:
resource "aws_api_gateway_rest_api" "api" {
name = "saas-api"
description = "SaaS API Gateway"
}
resource "aws_api_gateway_resource" "items" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "items"
}
resource "aws_api_gateway_method" "get_items" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.items.id
http_method = "GET"
authorization_type = "NONE"
}
resource "aws_api_gateway_method" "post_items" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.items.id
http_method = "POST"
authorization_type = "NONE"
}
resource "aws_api_gateway_integration" "get_lambda" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.items.id
http_method = aws_api_gateway_method.get_items.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
}
resource "aws_api_gateway_integration" "post_lambda" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.items.id
http_method = aws_api_gateway_method.post_items.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
}
resource "aws_api_gateway_deployment" "api" {
depends_on = [
aws_api_gateway_integration.get_lambda,
aws_api_gateway_integration.post_lambda
]
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "api"
}
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api_handler.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
}
フロントエンド構築
Next.jsを使用したフロントエンド構築の基本ステップです。
プロジェクト作成:
npx create-next-app@latest saas-frontend --ts
cd saas-frontend
認証コンポーネント実装:
// components/Auth.tsx
import { useState } from 'react';
export function Auth() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLogin, setIsLogin] = useState(true);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// ここにCognito認証コードを実装
}
return (
<div className="auth-container">
<h2>{isLogin ? 'ログイン' : '新規登録'}</h2>
<form onSubmit={handleSubmit}>
<div>
<label>メールアドレス</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label>パスワード</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">
{isLogin ? 'ログイン' : '登録'}
</button>
</form>
<p>
{isLogin ? 'アカウントをお持ちでない方は ' : 'すでにアカウントをお持ちの方は '}
<button onClick={() => setIsLogin(!isLogin)}>
{isLogin ? '新規登録' : 'ログイン'}
</button>
</p>
</div>
);
}
API連携コード:
// utils/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL;
export async function fetchItems() {
try {
const response = await fetch(`${API_URL}/items`);
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
console.error('Error fetching items:', error);
throw error;
}
}
export async function createItem(data: any) {
try {
const response = await fetch(`${API_URL}/items`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
console.error('Error creating item:', error);
throw error;
}
}
デプロイ:
npm run build
生成された.next/
ディレクトリの内容をS3バケットにアップロードします。
コスト最適化のポイント
このアーキテクチャは、以下の点で特にコスト効率を高めています:
- 従量課金モデルの活用
- Lambda: 月100万リクエストまで無料
- API Gateway: 月100万リクエストまで無料(12か月間)
- DynamoDB: オンデマンドモードで使用量に応じた課金
- 静的コンテンツの最適化
- S3の低コストストレージとCloudFrontのキャッシュによるトラフィック削減
- Next.jsの静的生成機能(SSG)による読み込み回数の最小化
- サーバー運用コストの削減
- EC2やRDSなどの常時稼働するサービスを使用しない
- メンテナンスやスケーリング対応のための人的コスト削減
- 多アカウント分離によるコスト管理
- 環境ごとに明確なコスト分離
- 無料枠の最大限活用(アカウントごとに無料枠が適用される)
スケーリングのポイント
このアーキテクチャは小規模から始めて、段階的に成長できるように設計されています:
- 初期段階
- 開発アカウント1つで開始
- 最小構成で検証と初期ユーザー獲得
- 成長段階
- ステージング、本番用アカウントを追加
- CI/CDパイプラインの整備
- より詳細なモニタリングとアラートの導入
- 拡大段階
- リージョンレプリケーション
- 専用セキュリティアカウント追加
- データ分析基盤の整備
まとめ
AWS Organizationsを活用した多アカウント戦略と、フルサーバーレスアーキテクチャを組み合わせることで、以下のメリットを享受できます:
- セキュリティ強化: アカウント分離とSCPによる強固なセキュリティ境界
- コスト最適化: 使用量に基づく課金と無料枠の最大限活用
- 再現性と自動化: CDK/Terraformによるインフラのコード化
- 拡張性: ビジネス成長に合わせて段階的に拡張可能
この構成テンプレートは、個人開発者からスタートアップまで、低コストで開始しながらも、将来的な成長に対応できるプロフェッショナルなSaaS基盤として活用できます。最初から「正しい方法」でクラウド基盤を構築することで、将来の技術的負債を最小限に抑え、持続可能な成長を実現しましょう。
本記事で紹介したテンプレートやコードは、コピーして再利用できるように公開しています。ぜひ実際の開発にお役立てください。
コメント