Chuyển đến nội dung chính
友田 陽大
B2B SaaS & DX strategy
B2B SaaS
経済産業大臣賞
技術選定
アーキテクチャ設計
AWS
Terraform
レガシー産業DX

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

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

Published
Reading time
11 min read
Author
友田 陽大
Chia sẻ

経済産業大臣賞受賞プロダクトで学んだ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分) を実施していますので、お気軽にご連絡ください。

お問い合わせはこちら

友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

Not sure where to start digitizing Excel/fax workflows?

Free DX assessment (30 min) — from mapping the current state to a rough cost and order of attack

Drawing on hands-on experience solo-building a METI-Minister's-Award-winning product, I spend 30 minutes mapping which of your workflows pay off most when digitized, then send an assessment memo (priorities, rough cost range, recommended steps). Free, no sales pitch — you're welcome to take just the assessment.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc