メインコンテンツへスキップ
友田 陽大
B2B SaaS・DX戦略
B2B SaaS
アーキテクチャ設計
AWS
Cognito
Terraform
Python
セキュリティ
Stripe

経済産業大臣賞 B2B SaaS のアーキテクチャ徹底解剖:マルチテナント認可・冪等な決済・4ラウンドのセキュリティ監査

木材サプライチェーンのDXを実現したB2B SaaSを、実コードを唯一の真実源として解剖します。業種ベースのマルチテナント認可、Cognito RS256とJWKSキャッシュ、Stripe Connectの二層冪等性、ThreadPoolExecutorによる帳票並列生成、4ラウンドのセキュリティ監査、Slackアラーム偽装対策、コスト最適化までを実装レベルで解説。

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

過去2本の記事では、木材流通業界のDXで得た「7つの教訓」と「技術選定フレームワーク」という意思決定の話を書きました。本記事はその続編にあたり、視点を「何を学んだか」から「実際にどう作り、どう本番運用に耐える品質まで引き上げたか」へ移します。

題材は、経済産業大臣賞を受賞し、京都府認定も取得した B2B サブスクリプション SaaS。林業・市場・製材所・プレカット・工務店・メーカーという多段の商流をまたいで、企業同士が受発注・帳票・決済・評価を行うマーケットプレイス型のプロダクトです。

この記事のルールはひとつ。唯一の真実源は実コードです。アーキテクチャ図やスライドではなく、実際に動いているバックエンド(Python / Flask)、フロントエンド(React / TypeScript)、インフラ(Terraform / AWS)から、エンタープライズが「この人に任せて大丈夫だ」と判断する材料になる設計判断だけを抜き出して解説します。

数字の前提: 本文中の定量値(221 エンドポイント、204 マイグレーション、2,153 テスト、48 FK インデックス、17 Terraform モジュール、12 Lambda など)はすべてリポジトリから機械的にカウントした実測値です。一方で「事業ROI(工数削減%・コスト削減額)」はクライアントの実データが必要なため、本記事では扱いません。捏造はしない、という方針です。


1. システム全体像:本番のリクエストパス

まず全体像です。よくある「CloudFront → ALB → サーバ」よりも一段深い構成になっています。

[ブラウザ] ── CloudFront ──> S3(React SPA / 不変アセット)

[ブラウザ] ── API Gateway(Cognito オーソライザー)
                  │  ※ 認証は VPC に入る前に終端
                  └─ VPC Link ─> NLB ─> ALB(internal)─> ECS Fargate ─> RDS PostgreSQL 16

[Stripe] ──> HTTP API ─> Lambda(webhook ×3)─> RDS / DynamoDB
[S3 アップロード] ──> Lambda(Excel/CSV 取り込み)─> RDS
[EventBridge 定期] ──> Lambda(課金アウトボックス送信 / 整合化)
[CloudWatch アラーム] ──> SNS ─> Lambda(Slack 通知)

なぜ API Gateway → NLB → ALB と LB を 2 段重ねるのか。これは「冗長」ではなく AWS の制約から導かれた必然です。

  • API Gateway のプライベート統合(VPC Link v1)は NLB しか指せない。 だから入口に NLB が必要。
  • L7 ルーティング・セキュリティグループによる ingress 制御・desync 対策が必要なので、その後ろに ALB を置く。ALB は desync_mitigation_mode = "defensive"drop_invalid_header_fields = true で運用。
  • Cognito オーソライザーを API Gateway のエッジに置くことで、認証は VPC に入る前に終端し、未認証リクエストは ECS まで到達しません。

重い処理・非同期処理は すべて Lambda 側に分離しています。Webhook、Excel 取り込み、課金調整、Slack 通知——これらを Flask アプリ本体から切り離すことで、API サーバは「同期リクエストの処理」という単一責任に集中でき、デプロイ単位も独立します(SRP)。


2. マルチテナント認可:業種ベースのアクセス制御

このプロダクトの設計上いちばん難しいのは「ユーザーが多種多様」という点です。林業 / 市場 / 製材所 / プレカット / 製材所兼プレカット / 工務店 / メーカー の 7 業種に、閲覧 管理 ロールが加わります。業種ごとに「実行できる操作」「閲覧できる情報」がまったく異なります。

業種は IntEnum、認可は frozenset のホワイトリスト

業種を文字列で散らすと、タイプミスや判定漏れが事故になります。そこで業種は IntEnum で一元定義し、機能ごとの許可業種を frozenset のホワイトリストとして宣言的に持ちます。

class IndustryCode(IntEnum):
    FORESTRY = 0        # 林業
    MARKET = 1          # 市場
    SAWMILL = 2         # 製材所
    PRECUT = 3          # プレカット
    SAWMILL_PRECUT = 4  # 製材所兼プレカット
    BUILDER = 5         # 工務店
    MANUFACTURER = 6    # メーカー
    VIEWER = 99
    ADMINISTRATOR = 100

# 「丸太を発注できるのは誰か」を frozenset で宣言的に定義する
WOOD_ORDER_INDUSTRIES = frozenset({IndustryCode.PRECUT, IndustryCode.SAWMILL_PRECUT})
LOG_RECEIVER_INDUSTRIES = frozenset({IndustryCode.FORESTRY, IndustryCode.MARKET})


def check_industry(user: User, allowed: frozenset[IndustryCode]) -> None:
    # 管理者は常に通す。それ以外は許可業種外なら 403。
    if user.industry == IndustryCode.ADMINISTRATOR:
        return
    if user.industry not in allowed:
        # 404 ではなく 403 を返す(リソース存在の有無を漏らさない=列挙攻撃対策)
        raise Forbidden()

ポイントは 3 つです。

  1. 認可判定はルーター層に一元化する。UseCase / Repository は「すでに認可済みの User」を受け取る前提で書くので、ビジネスロジックに認可の if 文が散らばりません。
  2. 不一致は 403404 ではない)。これは「IDを総当たりして存在を推測する」列挙攻撃を防ぐための明示的な設計判断です。
  3. 管理者バイパスは一箇所だけ。例外を一点に閉じ込めることで、認可の抜け道が増殖しません。

PII を漏らさない「二層スキーマ境界」

企業をまたいで取引相手を探すマーケットプレイスでは、「相手企業の概要は見せたいが、メール・電話・法人番号は見せたくない」という要件が生まれます。これをスキーマの分離で構造的に解いています。

class UserDumpSchema(BaseSchema):
    """相互に取引関係がある相手にだけ使う。PII を含む完全な表現。"""
    email = fields.Email()
    phone_number = fields.String()
    corporate_number = fields.String()
    # ... PII を含む全フィールド

class UserPublicSchema(BaseSchema):
    """企業横断の検索・閲覧で使う公開スキーマ。PII は『許可リスト方式』で構造的に除外。"""
    user_id = fields.UUID()
    company_name = fields.String()
    industry = fields.Integer()
    prefecture = fields.String()
    average_rate = fields.Float()      # 0〜5 の企業評価
    evaluation_count = fields.Integer()
    # email / phone_number / corporate_number は『定義していない』ので絶対に出ない

ブラックリスト(「これを隠す」)ではなく**ホワイトリスト(「これだけ出す」)**で実装しているのが肝です。新しい PII フィールドを User に足しても、UserPublicSchema に明示追加しない限り公開経路には絶対に出てきません。後述するペネトレーションテストで、まさにここの取り違え(横断 API が UserDumpSchema を使っていた)を検出し、即日修正しています。

フロントエンド:多段ゲートの ProtectedRoute

バックエンドの認可は最後の砦ですが、UX のためにフロントでも段階的にゲートします。React 側は ProtectedRoute が「認証 → プロフィール完備 → 管理者 → サブスク有効 → 業種」の順に判定し、いずれかで弾かれたら適切なリダイレクトを返します。

function ProtectedRoute({
  requiredIndustries,
  requiresAdmin,
  children,
}: {
  requiredIndustries?: ReadonlySet<Industry>;
  requiresAdmin?: boolean;
  children: React.ReactNode;
}) {
  const { user, isLoading } = useAuth();

  if (isLoading) return <Loading />;
  if (!user) return <Navigate to="/login" replace />;
  if (isProfileIncomplete(user)) return <Navigate to="/onboarding" replace />;
  if (requiresAdmin && !user.is_admin) return <Navigate to="/" replace />;
  if (IS_PROD && user.subscription_status !== "active")
    return <Navigate to="/subscription" replace />;
  if (requiredIndustries && !requiredIndustries.has(user.industry))
    return <Navigate to="/" replace />;

  return <UserContext value={user}>{children}</UserContext>;
}

業種ごとの機能境界(市場ユーザー向け、製材所向け、直送送り手向け…)は、この ProtectedRoute を薄くラップしたルートで表現します。「許可業種の集合」をデータとして渡すので、機能とロールの対応を一覧で見渡せ、変更も局所化されます(ETC)。


3. 認証基盤:Cognito JWT(RS256)と JWKS キャッシュ

認証は Amazon Cognito。バックエンドは渡ってきた JWT を RS256 で検証します。ここで「ライブラリに任せて終わり」にしないのが本番品質の分かれ目です。

def verify_token(token: str) -> dict:
    signing_key = get_jwks_client().get_signing_key_from_jwt(token)
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        audience=COGNITO_CLIENT_ID,
        issuer=COGNITO_ISSUER,
        # exp / iat / iss / aud / token_use の存在を必須化する
        options={"require": ["exp", "iat", "iss", "aud", "token_use"]},
    )
    # access トークンの誤用を防ぐ:id トークン以外は拒否する
    if claims.get("token_use") != "id":
        raise Unauthorized("invalid token_use")
    return claims

algorithms=["RS256"] を明示しているので、古典的な alg=none 攻撃や HS256 へのダウングレードは成立しません。token_use == "id" の検証は、access トークンを使って id トークン用エンドポイントを叩く混同を塞ぐためのものです。

JWKS は「二重チェックロックのシングルトン」でキャッシュ

JWKS(公開鍵セット)をリクエストごとに取りに行くと、レイテンシも増えるし Cognito へ無駄に負荷をかけます。一方で Fargate はマルチワーカーなので、ナイーブなグローバル変数は競合します。そこでダブルチェックロッキングでクライアントを 1 つだけ生成します。

_jwks_lock = threading.Lock()

def get_jwks_client() -> PyJWKClient:
    client = current_app.config.get("COGNITO_JWKS_CLIENT")
    if client is not None:           # 1st check(ロックなしの高速パス)
        return client
    with _jwks_lock:
        client = current_app.config.get("COGNITO_JWKS_CLIENT")
        if client is None:           # 2nd check(ロック内で再確認)
            client = PyJWKClient(COGNITO_JWKS_URI)
            current_app.config["COGNITO_JWKS_CLIENT"] = client
        return client

JWKS の更新間隔は当初 24 時間でしたが、セキュリティ監査の指摘を受けて 6 時間へ短縮し、起動時にプリウォーム(同期的に 1 回取得)しています。

なお、ステートレス JWT のトレードオフは正直に文書化しています。Cognito の GlobalSignOut は refresh トークンを失効させますが、id/access トークンは exp(約 60 分)まで有効です。これを即時失効させるには denylist(ElastiCache)が要りますが、コスト・レイテンシ・SPOF に見合わないと判断し、「短い TTL + リクエストログ + セキュリティヘッダ」で受容しています。受容したリスクを台帳に残すのは、エンタープライズ向けでは「隠す」より遥かに信頼されます。


4. 冪等な決済:Stripe Connect の二層冪等性

このプロダクトは「継続課金(サブスク)」と「取引ごとの精算」が同居するため、Stripe Connect を採用しています。決済では、ネットワーク断やリトライ下でも 二重課金 取りこぼし を絶対に起こさないことが要件です。

サブスク状態は User に置き、DB の CHECK 制約で形式検証

class User(Base):
    stripe_customer_id: Mapped[str | None]
    stripe_subscription_id: Mapped[str | None]
    stripe_connect_account_id: Mapped[str | None]
    subscription_status: Mapped[SubscriptionStatus]

    __table_args__ = (
        # Stripe ID の形式を DB レベルで検証(偽造・不正値の混入を防ぐ)
        CheckConstraint("stripe_customer_id LIKE 'cus_%'", name="ck_stripe_customer_id"),
        CheckConstraint("stripe_subscription_id LIKE 'sub_%'", name="ck_stripe_subscription_id"),
        CheckConstraint("stripe_connect_account_id LIKE 'acct_%'", name="ck_stripe_account_id"),
    )

金額は常にサーバ側で解決します。クライアントから渡ってきた amount を Stripe にそのまま流すのは、初期の監査で検出された典型的な「金額改ざん」脆弱性でした。現在は注文内容から金額を再計算する AmountResolver を通します。

第 1 層:コンテンツアドレス方式の冪等キー

Stripe API 呼び出しには冪等キーを付けますが、キーをランダムにすると「同じ操作のリトライ」と「内容が変わった再操作」を区別できません。そこで内容のハッシュをキーに織り込みます。

def idempotency_key(adjustment_id: str, scope: str, params: dict) -> str:
    digest = hashlib.sha256(canonical_json(params).encode()).hexdigest()[:12]
    # 同じ内容 → 同じキー(安全に再送できる)
    # 内容が変わる → 別キー(24h の IdempotencyKeyConflict を踏まない)
    return f"adj_{adjustment_id}_{scope}_{digest}"

第 2 層:DynamoDB の条件付き書き込みで Webhook を重複排除

Stripe Webhook は「少なくとも 1 回」配信されます。つまり同じイベントが複数回飛んできます。これを DynamoDB の条件付き PutItem で冪等化します。

def already_processed(event_id: str) -> bool:
    try:
        table.put_item(
            Item={"event_id": event_id, "ttl": now() + THIRTY_DAYS},
            ConditionExpression="attribute_not_exists(event_id)",
        )
        return False  # 初回 → 処理する
    except ClientError as e:
        if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
            return True   # 既処理 → スキップ
        # DynamoDB 側の障害は fail-open(Stripe が再送するので最終的整合に倒す)
        log.warning("idempotency check degraded", exc_info=True)
        return False

ここには「障害時にどちらへ倒すか」という明確な判断が 2 つあります。

  • テーブル名の環境変数が未設定なら、import 時点で RuntimeError を投げて起動を止める(fail-closed)。 冪等性の仕組みが黙って無効化される事故を防ぎます。
  • DynamoDB 自体が落ちているときは fail-open(処理を進める)。Stripe が再送してくれるので最終的整合に倒すほうが、決済を止めるより害が小さい、という判断です。

さらに課金調整は トランザクショナル・アウトボックスで実装しています。業務トランザクションと同じ DB トランザクションで outbox 行を書き、別 Lambda(EventBridge で 5 分ごと起動)が未送信分を Stripe へ送る。送信失敗しても行が残るので、確実に届きます。1 時間ごとの整合化 Lambda が突き合わせを行い、監査ログ用テーブルに記録します。


5. 重い処理の並列化と非同期化

「見積書・納品書・請求書」の Excel/PDF 生成と「既存 Excel の DB 化」は、このプロダクトでもっとも重い処理です。ここを素朴に同期実行すると、管理画面が固まります。

帳票生成:ThreadPoolExecutor でスレッド並列

1 つの注文に対して注文書・納品書・請求書を同時に生成します。Flask のアプリコンテキストはスレッドローカルなので、各スレッドで明示的に張り直すのが要点です。

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)

    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()  # 最初の例外を伝播させる

Excel は openpyxl、PDF は LibreOffice のヘッドレス変換を使います(Celery や asyncio ではなく、CPU/IO バウンドな帳票生成にはスレッドプールが素直で十分、という判断です)。

Excel/CSV 取り込み:S3 イベント駆動 Lambda

業界の共通言語である Excel を一括取り込みする処理は、API サーバから切り離して S3 アップロードをトリガーにした Lambda にしています。openpyxlread_only=True で開き、psycopg2execute_values で一括 INSERT します。ここでも防御を 2 つ入れています。

  • アップロードは 50MB 上限HeadObject で事前チェックし、OOM を防ぐ)。
  • CSV/Excel の数式インジェクション無害化(CWE-1236)= + @ - で始まる値は先頭に ' を付けて、開いた先の表計算ソフトで数式として実行されるのを防ぐ。

フロントエンド:指数バックオフ+Page Visibility 対応のポーリング

重い処理の完了をフロントで待つとき、固定間隔の setInterval は背面タブでも API を叩き続けてコストを無駄にします。そこで指数バックオフ+画面の可視状態連動のポーリングフックを実装しています。

usePollingWithBackoff({
  baseIntervalMs: 1000,
  maxIntervalMs: 30_000,           // 1 → 2 → 4 → … → 30s と伸ばす
  onTick: async () => {
    const next = await refetch();
    // 全件が success / failure に達したら停止
    return { shouldStop: next.every(isTerminal) };
  },
});
// document.hidden の間はリクエストを抑止し、再表示で base に戻す

背面タブで API を浪費しないので、ユーザー体験とクラウドコストの両方に効きます。


6. データベースの効率と信頼性

PostgreSQL 16 上で、地味だが効く改善を積んでいます。

改善内容
FK インデックス 48 本を無停止追加不足していた外部キーのインデックスを CREATE INDEX CONCURRENTLYIF NOT EXISTS で追加。本番で ACCESS EXCLUSIVE ロックを取らない
コネクションプールの予算化pool_size=5 / max_overflow=5 / pool_recycle=1800 / pool_pre_ping=True(5+5)×8タスク = 80 < db.t4g.micro の上限。スケールアウト時の接続枯渇を予防
日報の N+1 解消サイト数に比例して flush していた upsert を add_all + 単一 flush に。flush 回数をサイト数によらず定数化
テストのセーブポイント分離テストごとに savepoint を張ってロールバック。全件を約 11 秒で実行でき、CI が高速

CREATE INDEX CONCURRENTLYautocommit_block の中で実行し、命名規約を SQLAlchemy の自動生成(ix_<table>_<column>)に揃えることで、マイグレーションの drift をゼロに保っています。マイグレーションは Alembic で 204 世代を版管理しており、既存マイグレーションは決して編集しない運用です。


7. 4ラウンドのセキュリティ監査

ここがエンタープライズの信頼を勝ち取る核心です。このプロダクトは4 ラウンドのセキュリティ監査と資格情報ローテーションを経ています。

ラウンド手法主な所見と対応
R1静的ソース監査40 件(Critical 4 / High 17 / Medium 14 / Low 5)。決済整合性が中心:クライアント指定 amount の改ざん、テスト課金バイパス、管理セッション Cookie の httponly=False。Critical を 4/4 クローズ
R3ライブ・ステージング診断(約250リクエスト)Critical 2:Lambda 環境変数の平文資格情報(→ Secrets Manager 移行)、Webhook 冪等性の fail-open(→ fail-closed)。パストラバーサル・JWT 偽造(alg=none/改ざん aud)・CORS リフレクション・Webhook 署名検証はすべて防御成功
R4ブラックボックス+ホワイトボックス・ペネトレ実在 15 ロールの Cognito ユーザーで全 221 エンドポイントを走査。認証欠落 0 件。High 1 件(横断 API のクロステナント PII 露出)を当日に修正・再診断で 0 件確認
R5カテゴリ網羅(SSRF/CSRF/XXE/IDOR/SSTI 等 14 種)新規所見 0 件。RDS の rds.force_ssl=1 強制、通知のレート制限などを追加実装

R4 の「全 221 エンドポイントで認証欠落 0 件」は、すべてのルートが API Gateway の Cognito オーソライザーで守られていることを、実際の攻撃者視点で実証した結果です。/health すら未認証で素通りしません。

セキュリティヘッダも整備済みです。

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

そして、「直しきれない/直さないと決めた」残存リスクは台帳に明記しています(前述の JWT 失効トレードオフ、Cognito の SignUp によるユーザー存在の推測可能性など)。これは ADR(設計判断の記録)に近い運用で、第三者が「何を分かったうえで何を選んだか」を追跡できます。


8. 可観測性と回復性

Slack アラーム偽装(ログインジェクション)への対策

運用アラートは Slack に流していますが、当初の「失敗マーカー」はログの先頭トークンを見る位置依存の文字列でした。CloudWatch は行頭の空白を削るため、改行を仕込んだ入力で偽の運用アラームを発火できてしまう——古典的なログインジェクション(CWE-117)です。

対策として、マーカーを単一行のトップレベル JSON に変えました。

# 偽装可能だった旧実装(位置依存の文字列):  "[w1=..., FAILED]"
# 偽装不能な新実装(構造化):
log.error(json.dumps({"marker": "SLACK_DELIVERY_FAILED", "reason": reason}))

CloudWatch のメトリクスフィルタは { $.marker = "SLACK_DELIVERY_FAILED" } で照合するので、本文に何を書かれても marker キーは偽造できません。

Slack が落ちても気づける「帯域外」エスカレーション

通知経路自体が壊れたら本末転倒です。そこで Slack 配信失敗を上記マーカーで検知し、CloudWatch メトリクスフィルタ → SNS メールという、Slack に依存しない経路へエスカレーションします。さらにマーカー文字列がバックエンド/Lambda/Terraform の 3 箇所でズレないよう、契約テストで機械的に同期を保証しています。

ERROR ログの Slack 通知ハンドラ自体も、レスポンスを「恒久失敗(4xx)」と「一時失敗」に分類し、一時失敗だけ指数バックオフでリトライ、恒久失敗は即中止して上記マーカーを出します。スレッド/グリーンレット混在環境のため、requests.Session を共有しスレッドセーフに扱っています。


9. コスト最適化

「世界最高峰の品質」はコスト無視を意味しません。むしろ個人〜小規模で本番 SaaS を回すには、コスト設計こそ実力が出ます。

  • environment_active 一発で課金リソースをゼロに畳む。 停止期間は NAT・ALB/NLB・VPC Link・RDS(count=0)・ECS(desired=0)・Secrets Manager の VPC エンドポイントがすべて消える。再開もコードから。
  • Graviton(ARM / t4g) を RDS とバスティオンに採用し、x86 比でコスト効率を改善。
  • ステージングは Fargate Spot 100%(中断許容、約 70% 削減)。本番はオンデマンド。
  • NAT は単一(冗長を捨ててコスト最適)。VPC エンドポイントは本番のみ。
  • スケジュールスケーリング:本番の ECS を平日業務時間は min2/max8、夜間は min1/max4 に。
  • Terraform state は S3 ネイティブロックuse_lockfile = true)で、DynamoDB ロックテーブルのコストを排除。
  • S3 ライフサイクル(本番のみ):Standard → IA(30d) → Glacier(90d) → 削除(365d)。
  • CMK は本番のみ、ステージングは AWS マネージドキー。

「平常時は安く、必要なときだけスケールし、使わない期間はゼロに畳める」——この弾力性をコードで宣言しているのが要点です。


10. CI/CD と品質ゲート

最後に、これらの品質を人手のレビューに頼らず維持する仕組みです。

デプロイは GitHub Actions の OIDC(長期 AWS キーを一切持たない)で実行します。バックエンドは docker build → ECR push → ecs update-service --force-new-deployment、フロントは S3 sync(不変アセットは immutable、ルートは no-cache)→ CloudFront 無効化

Terraform も 3 本のパイプラインで自動化しています。

  • plan:PR で fmt-check / validate / tfsec を実行し、plan をコメント。
  • applymain → staging、production ブランチ → 本番。**apply ロールには権限境界(permissions boundary)**を付け、組織乗っ取りや監査基盤の停止を構造的に禁止。
  • drift:平日朝に定期実行し、ドリフトを検知したら GitHub Issue を自動起票・復旧で自動クローズ。

品質ゲートは二段構えです。pre-commit(変更ファイルのみ、数秒)と pre-push(CI のフルミラー)で、以下を回します。

フォーマットLintセキュリティテスト
BackendRuffRuff / Bandit / Vulture / deptrymypypip-auditpytest(Docker, 2,153 件)
FrontendPrettierESLinttsc --noEmitnpm auditVitest
Infraterraform fmtterraform validatetfsecterraform plan

さらに gitleaks(秘密情報スキャン)、Trivy(イメージ CVE)、hadolintDependabot(6 エコシステムを継続更新)。Conventional Commits を必須化し、--no-verify と main への force push は禁止です。


まとめ

経済産業大臣賞という結果の裏側には、派手な機能ではなく、地味で一貫した設計判断の積み重ねがあります。

  • マルチテナント認可frozenset ホワイトリスト+ルーター層一元化+PII ホワイトリストスキーマで、構造的に漏れない形に。
  • 決済は「サーバ側金額解決+コンテンツアドレス冪等キー+DynamoDB 重複排除+アウトボックス」で、リトライ下でも二重課金しない。
  • 重い処理はスレッド並列とイベント駆動 Lambda へ分離し、フロントは可視状態連動のバックオフポーリングで待つ。
  • 信頼性は、4 ラウンドの監査・221 API の認証欠落 0 件・ログインジェクション対策・帯域外エスカレーション・受容リスク台帳という形で実証可能に。
  • コストenvironment_active・Graviton・Spot・単一 NAT で、弾力的かつ安価に。

「動くものを作る」と「本番運用に耐え、第三者の攻撃と監査に耐える SaaS を作る」の差は、まさにこういう一つひとつの判断にあります。レガシー産業の DX や、B2B SaaS の新規開発・立て直しをご検討中でしたら、要件定義からインフラ・セキュリティ・運用まで、この水準でワンストップにお引き受けします。

友田

友田 陽大

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

この記事で解説した技術の適用事例

経済産業大臣賞受賞 | 木材流通業界のDXを実現したB2BサブスクリプションSaaS

ケーススタディを見る