友田 陽大
← ブログ一覧に戻る

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

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

2025/1/94分友田 陽大
B2B SaaS
経済産業大臣賞
技術選定
アーキテクチャ設計
AWS
Terraform
レガシー産業DX

経済産業大臣賞受賞プロダクトで学んだ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が著しく低下します。

実装:Python asyncio + Celery

from celery import Celery
import asyncio

celery = Celery('tasks', broker='redis://localhost:6379/0')

@celery.task
def generate_invoice_pdf(order_id):
    """請求書PDF生成(非同期タスク)"""
    order = Order.query.get(order_id)
    pdf_data = create_pdf(order)

    # S3にアップロード
    s3_key = f"invoices/{order.id}.pdf"
    s3_client.put_object(Bucket='my-bucket', Key=s3_key, Body=pdf_data)

    # ユーザーに通知(SES経由)
    send_email(order.customer_email, f"請求書が作成されました: {s3_key}")

@app.route('/api/orders/<order_id>/invoice', methods=['POST'])
def create_invoice(order_id):
    """請求書作成リクエスト(即座にレスポンス)"""
    task = generate_invoice_pdf.delay(order_id)
    return jsonify({
        'message': '請求書を作成中です。完成後、メールで通知します。',
        'task_id': task.id
    }), 202

パフォーマンス改善の効果:

  • ユーザー待ち時間: 30秒 → 1秒(即座にレスポンス)
  • スループット: 並列処理により、複数ユーザーの同時リクエストに対応

教訓: 重い処理は「非同期化」を徹底。ユーザーは待ち時間を嫌う。レスポンスは即座に返し、バックグラウンドで処理を実行する。


教訓6: 月額課金は「Stripe」で安定運用せよ

課題:継続課金の複雑さ

B2B SaaSの収益モデルは「月額サブスクリプション」です。初期費用ゼロ、月額課金で継続収益を得るモデルでは、課金の自動化、請求書発行、顧客管理が複雑です。

実装:Stripe Subscription

import stripe

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

def create_subscription(customer_email, plan_id):
    """月額サブスクリプション作成"""
    # 顧客作成
    customer = stripe.Customer.create(
        email=customer_email,
        description="木材流通SaaS顧客"
    )

    # サブスクリプション作成
    subscription = stripe.Subscription.create(
        customer=customer.id,
        items=[{'price': plan_id}],  # 月額プラン(例: 月5万円)
        payment_behavior='default_incomplete',
        payment_settings={'save_default_payment_method': 'on_subscription'},
        expand=['latest_invoice.payment_intent']
    )

    return {
        'subscription_id': subscription.id,
        'client_secret': subscription.latest_invoice.payment_intent.client_secret
    }

Stripeのメリット:

  • 自動課金: 毎月自動的に課金、失敗時は自動リトライ
  • 請求書自動発行: PDF請求書を自動生成・メール送信
  • 顧客管理: ダッシュボードで全顧客の課金状況を一元管理
  • セキュリティ: PCI DSS準拠、カード情報をサーバーで保持不要

教訓: 月額課金は自前実装せず、Stripeなどの専門サービスを使う。課金周りのバグは「売上損失」に直結する。


教訓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分) を実施していますので、お気軽にご連絡ください。

お問い合わせはこちら

同様の課題はありませんか?

あなたのビジネス課題も、最新の技術で解決できるかもしれません。
まずは30分、無料技術相談で状況をお聞かせください。

無料技術相談を予約する

※ プロジェクト単位(請負)・技術顧問、どちらも対応可能です