メインコンテンツへスキップ
友田 陽大
B2B SaaS・DX戦略
B2B SaaS
経済産業大臣賞
技術選定
アーキテクチャ設計
AWS
Terraform
レガシー産業DX

経済産業大臣賞受賞プロダクトで学んだB2B SaaS開発の7つの教訓

木材流通業界のDXを実現したB2BサブスクリプションSaaSの開発を通じて学んだ、技術選定、アーキテクチャ設計、セキュリティ、スケーラビリティの実践的教訓を公開します。TypeScript + Python + AWS Terraform による実装事例。

公開日
読了時間
11分
著者
友田 陽大
シェア

経済産業大臣賞受賞プロダクトで学んだB2B SaaS開発の7つの教訓

はじめに:電話・FAX・Excelからの脱却

私は2年前、木材流通業界という「極めてアナログな業界」のDXに挑戦しました。電話、メール、FAX、Excelでの受発注が主流で、在庫情報はExcelで管理され、FAXでの発注は記録が残らず、電話での確認作業に毎日数時間を要する——このような状況を、Web上で一元管理するB2BサブスクリプションSaaSとして構築しました。

その結果、このプロダクトは経済産業大臣賞を受賞しました。

本記事では、この開発を通じて学んだ7つの教訓を、技術選定、アーキテクチャ設計、セキュリティ、スケーラビリティの観点から公開します。特に、toB向けSaaS開発を検討されている方にとって、実践的な示唆を提供できると考えています。


教訓1: 技術選定は「業界の複雑さ」から逆算せよ

課題:8種類のユーザー属性と複雑な商流

木材流通業界には、「林業」「市場」「製材所」「プレカット」「工務店」「メーカー」「問屋」「その他」という8種類のユーザー属性が存在します。各属性で実行可能な機能や閲覧できる情報が異なるため、厳格な認証・認可が必須でした。

決断:AWS Cognito + カスタムロジック

当初、Firebase Authenticationも検討しましたが、以下の理由でAWS Cognitoを選択:

// AWS Cognito User Poolsのカスタム属性で8種類のユーザー属性を管理
{
  "custom:user_type": "lumber_mill",  // 製材所
  "custom:permissions": "create_order,view_inventory",
  "custom:company_id": "company-123"
}

選定理由:

  • カスタム属性の柔軟性: 8種類のユーザー属性ごとに異なる権限を定義可能
  • AWSエコスystem: ECS、RDS、Lambda との統合が容易
  • スケーラビリティ: MAU課金で、初期コスト抑制

教訓: 技術選定は「業界特有の複雑さ」を最優先に考慮すべき。汎用的なソリューションではなく、業界のドメイン知識を反映できる技術を選ぶ。


教訓2: セキュリティは「ページ単位・API単位」で設計せよ

課題:データ漏洩リスクの最小化

B2B SaaSでは、競合他社のデータが同一システム内に存在します。「製材所Aが市場Bの在庫情報を閲覧できてしまう」という事態は、ビジネス上の致命的なリスクです。

実装:多層防御アーキテクチャ

# Flask + AWS Cognito統合による認証・認可
from functools import wraps
from flask import request, jsonify

def require_user_type(*allowed_types):
    """ユーザー属性ごとのアクセス制御デコレータ"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            user_type = get_user_type_from_token(request.headers.get('Authorization'))

            if user_type not in allowed_types:
                return jsonify({'error': 'Forbidden'}), 403

            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/api/inventory', methods=['GET'])
@require_user_type('lumber_mill', 'market', 'manufacturer')
def get_inventory():
    """在庫情報取得(製材所・市場・メーカーのみアクセス可)"""
    user_company_id = get_company_id_from_token()
    # 自社の在庫のみ取得(他社データは取得不可)
    inventory = Inventory.query.filter_by(company_id=user_company_id).all()
    return jsonify([inv.to_dict() for inv in inventory])

セキュリティ対策の3層構造:

  1. 認証層: AWS Cognito JWTトークン検証
  2. 認可層: ユーザー属性ごとのAPI単位アクセス制御
  3. データ層: 会社IDによるRow-Level Security(RLS)

教訓: セキュリティは「認証すればOK」ではなく、ページ単位・API単位・データ単位で多層的に設計する。特にB2B SaaSでは、競合他社のデータ保護が最優先。


教訓3: バリデーションは「フロント・バック両方」で徹底せよ

課題:不正データの混入防止

B2B SaaSでは、データの正確性が企業間取引の信頼性に直結します。不正な価格、在庫数、発注数が混入すると、ビジネス全体が破綻します。

実装:zod + Marshmallow の二重バリデーション

フロントエンド(TypeScript + zod):

import { z } from "zod";

const OrderSchema = z.object({
  product_id: z.string().uuid(),
  quantity: z.number().int().positive().max(10000),
  unit_price: z.number().positive().max(1000000),
  delivery_date: z.string().datetime(),
});

type Order = z.infer<typeof OrderSchema>;

// フォーム送信前にバリデーション
const handleSubmit = (data: Order) => {
  const result = OrderSchema.safeParse(data);
  if (!result.success) {
    alert(result.error.errors[0].message);
    return;
  }
  // API送信
};

バックエンド(Python + Marshmallow):

from marshmallow import Schema, fields, validate, ValidationError

class OrderSchema(Schema):
    product_id = fields.UUID(required=True)
    quantity = fields.Integer(required=True, validate=validate.Range(min=1, max=10000))
    unit_price = fields.Decimal(required=True, validate=validate.Range(min=0, max=1000000))
    delivery_date = fields.DateTime(required=True)

@app.route('/api/orders', methods=['POST'])
def create_order():
    schema = OrderSchema()
    try:
        data = schema.load(request.json)
    except ValidationError as err:
        return jsonify({'errors': err.messages}), 400

    # DBに保存
    order = Order(**data)
    db.session.add(order)
    db.session.commit()
    return jsonify(order.to_dict()), 201

教訓: フロントエンドのバリデーションは「UX向上」、バックエンドのバリデーションは「セキュリティとデータ整合性」。両方を徹底することで、不正データの混入を完全に防ぐ。


教訓4: IaC(Infrastructure as Code)は「初日から」導入せよ

課題:複雑なAWS環境の再現性確保

B2B SaaSでは、VPC、ECS、RDS、Cognito、ALB、CloudFront、SESなど、広範なAWSサービスを組み合わせます。手動構築では再現性がなく、障害時の復旧が困難です。

実装:Terraform による完全自動化

# VPC設定
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "${var.project_name}-vpc"
    Environment = var.environment
  }
}

# ECS on Fargate
resource "aws_ecs_cluster" "main" {
  name = "${var.project_name}-cluster"
}

resource "aws_ecs_service" "app" {
  name            = "${var.project_name}-app"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = var.environment == "production" ? 3 : 1
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = aws_subnet.private[*].id
    security_groups = [aws_security_group.ecs_tasks.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }
}

# AWS Cognito User Pool
resource "aws_cognito_user_pool" "main" {
  name = "${var.project_name}-user-pool"

  # 8種類のユーザー属性をカスタム属性で定義
  schema {
    name                = "user_type"
    attribute_data_type = "String"
    mutable             = true
  }

  schema {
    name                = "company_id"
    attribute_data_type = "String"
    mutable             = false
  }
}

IaCのメリット:

  • 再現性: terraform apply 一発でインフラ構築
  • バージョン管理: Git管理で変更履歴を追跡
  • 環境分離: terraform workspace で dev/staging/production を分離
  • 災害復旧: インフラ全体を数分で復旧可能

教訓: IaCは「後で導入」ではなく、「初日から」導入すべき。手動構築→IaC移行は、技術的負債が膨大になる。


教訓5: パフォーマンスは「非同期処理」で最適化せよ

課題:重い処理によるUX低下

B2B SaaSでは、「見積書・納品書・請求書」のPDF/Excel生成、「既存ExcelのDB化」といった重い処理が頻繁に発生します。同期処理では、ユーザーが数十秒待つことになり、UXが著しく低下します。

実装:ThreadPoolExecutor によるスレッド並列 + イベント駆動 Lambda

重い処理には 2 つの性質があり、それぞれ別の手段を選びました。帳票生成(注文書・納品書・請求書)は CPU/IO バウンドで同期的に完了させたいので ThreadPoolExecutor でスレッド並列に。Excel→DB 取り込みは時間が読めないので、S3 アップロードをトリガーにした Lambda へ完全にオフロードします。

from concurrent.futures import ThreadPoolExecutor, as_completed
from sqlalchemy.orm import selectinload

def parallel_create_documents(app, order_id: str) -> None:
    """注文書・納品書・請求書を同時生成する。"""
    tasks = [create_order_form, create_delivery_note, create_invoice]

    def run(task):
        with app.app_context():                       # スレッドごとにコンテキストを張る
            doc = (
                Document.query
                .options(selectinload(Document.lines))  # N+1 を選択ロードで回避
                .filter_by(order_id=order_id)
                .with_for_update()                       # 行ロックで競合生成を防ぐ
                .one()
            )
            return task(doc)                             # openpyxl で Excel → LibreOffice で PDF

    with ThreadPoolExecutor(max_workers=len(tasks)) as pool:
        futures = [pool.submit(run, t) for t in tasks]
        for f in as_completed(futures):
            f.result()  # 最初の例外を伝播させる

設計のポイント:

  • 帳票は 3 種を並列生成。各スレッドで Flask のアプリコンテキストを張り直し、selectinload で N+1 を防ぎ、with_for_update で競合を防ぐ。
  • Excel 取り込みは Lambda へ分離openpyxlread_only=True で開き execute_values で一括 INSERT。50MB 上限と数式インジェクション無害化(CWE-1236)も実装。
  • フロントは指数バックオフ+Page Visibility 対応のポーリングで完了を待つので、重い処理中でも管理画面は固まらず、背面タブで API を浪費しない。

教訓: 「非同期化」は手段が複数ある。同期的に完了させたい CPU/IO バウンドな処理はスレッド並列、時間が読めない処理はイベント駆動でサーバ本体から切り離す——性質で使い分けるのが本番品質。


教訓6: 決済は「Stripe Connect」で冪等に組め

課題:継続課金 + 取引精算 + 重複配信

このプロダクトの収益モデルは月額サブスクリプションですが、それだけではありません。企業同士が取引するマーケットプレイスなので、継続課金に加えて取引ごとの精算が発生します。そこで単なる Stripe ではなく Stripe Connect を採用しました。決済では 二重課金 取りこぼし 金額改ざん を絶対に起こさないことが要件です。

実装:Stripe Connect(サーバ側金額解決+冪等キー)

import stripe, hashlib

stripe.api_key = os.getenv("STRIPE_SECRET_KEY")

def create_subscription(customer: User, plan_id: str) -> dict:
    # 金額は plan_id からサーバ側で解決する(クライアント指定額を信用しない=改ざん対策)
    subscription = stripe.Subscription.create(
        customer=customer.stripe_customer_id,
        items=[{"price": plan_id}],
        payment_behavior="default_incomplete",
        expand=["latest_invoice.payment_intent"],
        # 同一内容の再送が二重課金にならないよう冪等キーを付ける
        idempotency_key=f"sub_{customer.id}_{plan_id}",
    )
    return {"subscription_id": subscription.id}

冪等性を二層で守る:

  • 第 1 層(Stripe API): 冪等キーに「内容のハッシュ」を織り込み、同一内容の再送は安全に、内容変更は別キーに。
  • 第 2 層(Webhook): Webhook は Flask 本体ではなく 3 つの Lambda で受け、DynamoDB の条件付き書き込み(attribute_not_exists・30日TTL)で重複イベントを排除。テーブル名未設定なら起動を止める(fail-closed)。
  • アウトボックス: 課金調整は業務トランザクションと同じ DB トランザクションで outbox に書き、別 Lambda が確実に Stripe へ送る。

教訓: 決済は「Stripe を呼べば終わり」ではない。サーバ側金額解決 + 冪等キー + Webhook 重複排除 + アウトボックス までやって初めて、リトライと重複配信に耐える。課金周りのバグは「売上損失」と「信用失墜」に直結する。


教訓7: CI/CDは「品質保証の自動化」として設計せよ

課題:デプロイ時のヒューマンエラー

B2B SaaSでは、ダウンタイムは「企業間取引の停止」を意味します。デプロイ時のミスでシステムが停止すると、顧客企業のビジネスが止まります。

実装:GitHub Actions + ECS自動デプロイ

# .github/workflows/deploy.yml
name: Deploy to ECS

on:
  push:
    branches: [main]

jobs:
  test-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      # リンター実行
      - name: Run ESLint
        run: npm run lint

      - name: Run Flake8
        run: flake8 app/

      # 脆弱性診断
      - name: Security Audit
        run: |
          npm audit --production
          pip-audit

      # テスト実行
      - name: Run Tests
        run: pytest tests/

      # Dockerイメージビルド
      - name: Build Docker Image
        run: docker build -t my-app:${{ github.sha }} .

      # ECRにプッシュ
      - name: Push to ECR
        run: |
          aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}
          docker push my-app:${{ github.sha }}

      # ECS デプロイ
      - name: Deploy to ECS
        run: |
          aws ecs update-service --cluster my-cluster --service my-app --force-new-deployment

CI/CDの効果:

  • 自動テスト: コミットごとに全テスト実行、バグの早期発見
  • 自動デプロイ: git push だけでデプロイ完了
  • ロールバック: デプロイ失敗時、自動的に前バージョンにロールバック

教訓: CI/CDは「デプロイの自動化」ではなく、「品質保証の自動化」として設計する。リンター、脆弱性診断、テストを徹底することで、本番環境のバグを最小化。


まとめ:B2B SaaS開発の7つの黄金律

  1. 技術選定は「業界の複雑さ」から逆算せよ
  2. セキュリティは「ページ単位・API単位」で設計せよ
  3. バリデーションは「フロント・バック両方」で徹底せよ
  4. IaC(Infrastructure as Code)は「初日から」導入せよ
  5. パフォーマンスは「非同期処理」で最適化せよ
  6. 月額課金は「Stripe」で安定運用せよ
  7. CI/CDは「品質保証の自動化」として設計せよ

これらの教訓は、2年間の試行錯誤の結果です。B2B SaaS開発は、toC向けアプリとは全く異なる設計思想が必要です。企業間取引の信頼性、セキュリティ、スケーラビリティを最優先に考え、技術選定からアーキテクチャ設計まで、一貫した戦略を持つことが成功の鍵です。


あなたのプロジェクトでも実現可能です

もし、あなたが「レガシー産業のDX」「B2B SaaS開発」「技術選定に悩んでいる」場合、私がサポートできます。要件定義から設計、実装、インフラ構築まで、ワンストップで対応可能です。

無料技術相談(30分) を実施していますので、お気軽にご連絡ください。

お問い合わせはこちら

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

お困りごとはありませんか?

設計から実装・運用まで、一人 × 生成AI で伴走します

この記事のような実装を、要件定義から本番運用まで一気通貫で。まずは30分の無料技術相談から、状況をお聞かせください。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい