🐥

CDK for TerraformでAzureモバイルアプリバックエンドを構築する前に知っておくべきこと

に公開

CDK for TerraformでAzureモバイルアプリバックエンドを構築する前に知っておくべきこと

はじめに

モバイルアプリのバックエンドインフラをAzureで構築する際、IaCツールの選択は重要な判断の一つです。従来のTerraformに加えて、CDK for Terraform(CDKTF)という選択肢が注目を集めているそうです。
わたしが担当しているチームのIaCも当初はBicepやHCL(HashiCorp Configuration Languageの略)で記述しておりましたが、AWSのCDKでの開発体験がなかなか良いとの噂もあり、互換性やカジュアルな情報源に乏しいながらもCDKTFでどこまでAzureのIaCがカバーできるかChallengeしてみたいと思います
厳密にはChallengeされていた、わたしよりも遥かに凄腕のSREエンジニアがいたのですが他のPJに攫われてしまったので空気を読んで独習するための記事でございまする
そもそも年初くらいまではモバイル領域でゴリゴリ開発していたり、プライベートでもモバイルとフロントエンドでしか遊んでいないのでIaCやソリューションアーキテクトのジョブは若干畑違いの可能性があるかも知れない

ということで
本記事では、Azure中級者(ある程度のライセンスホルダーレベルの皆様)がCDK for Terraformを初めて使ってモバイルアプリバックエンドを構築する際に押さえておくべきポイントと注意点を解説します。

CDK for Terraformとは

Terraformとの違い

CDK for Terraformは、AWS CDKの思想をTerraformに適用したツールです。主な特徴は以下の通りです。

  • プログラミング言語でのインフラ定義: HCL(HashiCorp Configuration Language)ではなく、TypeScript、Python、Java、C#、Goなどの一般的なプログラミング言語を使用
  • オブジェクト指向アプローチ: クラスベースでの抽象化により、再利用可能なコンポーネントの作成が容易
  • IDE支援: IntelliSenseやタイプセーフティによる開発体験の向上
  • Terraformエコシステムの活用: 既存のTerraformプロバイダーとモジュールをそのまま利用可能

なぜCDK for Terraformを選ぶのか

モバイルアプリバックエンドにおいて、CDKTF が有効な理由:

  1. 複雑な設定の抽象化: API Gateway、認証、データベースなどの複雑な連携をクラスで抽象化
  2. 環境間の設定管理: 開発、ステージング、本番環境での設定差分をプログラマティックに管理
  3. チーム内での知識共有: 多くの開発者が慣れ親しんだ言語での記述により、インフラの理解が容易

事前学習ポイント

1. CDK for Terraform基礎概念

App、Stack、Constructの関係性

// App: 最上位コンテナ
const app = new App();

// Stack: デプロイ単位
class MobileBackendStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);
    
    // Construct: リソースの論理グループ
    new ApiGatewayConstruct(this, "api");
    new DatabaseConstruct(this, "db");
  }
}

Providerの設定
Azure Resource Manager(azurerm)プロバイダーは必須です。バージョン固定を推奨します。

new AzurermProvider(this, "Azure", {
  features: {},
  subscriptionId: process.env.AZURE_SUBSCRIPTION_ID,
  tenantId: process.env.AZURE_TENANT_ID,
});

2. Aspects - 横断的関心事の処理

Aspectsは、CDK for Terraformの最も強力な機能の一つです。すべてのリソースに対して横断的な処理を適用できます。

基本的なAspectの実装

import { IAspect, IConstruct } from "constructs";
import { TerraformResource } from "cdktf";

class TaggingAspect implements IAspect {
  private tags: Record<string, string>;

  constructor(tags: Record<string, string>) {
    this.tags = tags;
  }

  visit(node: IConstruct): void {
    if (node instanceof TerraformResource) {
      // Azureリソースにタグを適用
      const resource = node as any;
      if (resource.tags !== undefined) {
        resource.tags = {
          ...resource.tags,
          ...this.tags,
        };
      }
    }
  }
}

// 使用例
const app = new App();
const stack = new MobileBackendStack(app, "MobileBackend");

// すべてのリソースに共通タグを適用
Aspects.of(stack).add(new TaggingAspect({
  Environment: "production",
  Project: "mobile-app",
  Owner: "platform-team",
  CostCenter: "engineering",
}));

セキュリティポリシーの適用

class SecurityPolicyAspect implements IAspect {
  visit(node: IConstruct): void {
    // Storage Accountのセキュリティ設定を強制
    if (node instanceof StorageAccount) {
      const storage = node as StorageAccount;
      
      // HTTPS必須の強制
      storage.enableHttpsTrafficOnly = true;
      
      // 最小TLSバージョンの設定
      storage.minTlsVersion = "TLS1_2";
      
      // パブリックアクセスの無効化
      storage.publicNetworkAccessEnabled = false;
      
      // 診断ログの出力
      console.log(`✓ Security policies applied to Storage Account: ${storage.name}`);
    }

    // Cosmos DBのセキュリティ設定
    if (node instanceof CosmosdbAccount) {
      const cosmos = node as CosmosdbAccount;
      
      // パブリックアクセスの制限
      cosmos.publicNetworkAccessEnabled = false;
      
      // IPフィルタリングの設定
      cosmos.ipRangeFilter = "0.0.0.0"; // 本番では適切なIP範囲を設定
      
      console.log(`✓ Security policies applied to Cosmos DB: ${cosmos.name}`);
    }
  }
}

命名規則の検証

class NamingConventionAspect implements IAspect {
  private pattern: RegExp;
  private resourceTypes: string[];

  constructor(pattern: RegExp, resourceTypes: string[]) {
    this.pattern = pattern;
    this.resourceTypes = resourceTypes;
  }

  visit(node: IConstruct): void {
    if (node instanceof TerraformResource) {
      const resource = node as any;
      const resourceType = resource.terraformResourceType;
      
      if (this.resourceTypes.includes(resourceType) && resource.name) {
        if (!this.pattern.test(resource.name)) {
          throw new Error(
            `Resource ${resource.name} (${resourceType}) does not match naming convention: ${this.pattern}`
          );
        }
        console.log(`✓ Naming convention validated: ${resource.name}`);
      }
    }
  }
}

// 使用例
Aspects.of(stack).add(
  new NamingConventionAspect(
    /^[a-z0-9-]+$/,  // 小文字、数字、ハイフンのみ
    ["azurerm_resource_group", "azurerm_storage_account", "azurerm_cosmosdb_account"]
  )
);

コストガバナンスの適用

class CostGovernanceAspect implements IAspect {
  private environment: string;
  private maxCostTier: Record<string, any>;

  constructor(environment: string) {
    this.environment = environment;
    this.maxCostTier = {
      dev: {
        cosmosdbThroughput: 400,
        functionAppSku: "Y1",
        storageAccountTier: "Standard",
      },
      staging: {
        cosmosdbThroughput: 1000,
        functionAppSku: "EP1", 
        storageAccountTier: "Standard",
      },
      prod: {
        cosmosdbThroughput: 4000,
        functionAppSku: "EP2",
        storageAccountTier: "Premium",
      },
    };
  }

  visit(node: IConstruct): void {
    // Cosmos DB RUの制限
    if (node instanceof CosmosdbSqlContainer) {
      const container = node as CosmosdbSqlContainer;
      const maxThroughput = this.maxCostTier[this.environment]?.cosmosdbThroughput;
      
      if (container.throughput && container.throughput > maxThroughput) {
        console.warn(
          `Warning: Cosmos DB container ${container.name} throughput (${container.throughput}) exceeds limit for ${this.environment} environment (${maxThroughput})`
        );
        // 開発環境では自動的に制限
        if (this.environment === "dev") {
          container.throughput = maxThroughput;
        }
      }
    }

    // App Service Planの制限
    if (node instanceof ServicePlan) {
      const plan = node as ServicePlan;
      const maxSku = this.maxCostTier[this.environment]?.functionAppSku;
      
      if (plan.skuName && this.isHigherTier(plan.skuName, maxSku)) {
        console.warn(
          `Warning: App Service Plan ${plan.name} SKU (${plan.skuName}) exceeds recommended tier for ${this.environment} environment`
        );
      }
    }
  }

  private isHigherTier(current: string, max: string): boolean {
    const tierOrder = ["Y1", "EP1", "EP2", "EP3", "P1", "P2", "P3"];
    return tierOrder.indexOf(current) > tierOrder.indexOf(max);
  }
}

コンプライアンス要件の適用

class ComplianceAspect implements IAspect {
  private requiredTags: string[];
  private encryptionRequired: boolean;

  constructor(requiredTags: string[], encryptionRequired: boolean = true) {
    this.requiredTags = requiredTags;
    this.encryptionRequired = encryptionRequired;
  }

  visit(node: IConstruct): void {
    if (node instanceof TerraformResource) {
      const resource = node as any;
      
      // 必須タグの検証
      this.validateRequiredTags(resource);
      
      // 暗号化の強制
      if (this.encryptionRequired) {
        this.enforceEncryption(resource);
      }
    }
  }

  private validateRequiredTags(resource: any): void {
    if (!resource.tags) {
      resource.tags = {};
    }

    const missingTags = this.requiredTags.filter(tag => !resource.tags[tag]);
    
    if (missingTags.length > 0) {
      throw new Error(
        `Resource ${resource.name || resource.terraformResourceType} is missing required tags: ${missingTags.join(', ')}`
      );
    }
  }

  private enforceEncryption(resource: any): void {
    // Storage Account の暗号化
    if (resource instanceof StorageAccount) {
      resource.enableHttpsTrafficOnly = true;
      
      // カスタマーマネージドキーの設定(オプション)
      if (!resource.customerManagedKey) {
        console.log(`Setting default encryption for Storage Account: ${resource.name}`);
      }
    }

    // Cosmos DB の暗号化
    if (resource instanceof CosmosdbAccount) {
      // Cosmos DBはデフォルトで保存時暗号化が有効
      console.log(`✓ Encryption validated for Cosmos DB: ${resource.name}`);
    }
  }
}

// 使用例
Aspects.of(stack).add(
  new ComplianceAspect(
    ["Environment", "Owner", "Project", "CostCenter"], // 必須タグ
    true // 暗号化必須
  )
);

複数Aspectsの組み合わせ

// スタックに複数のAspectsを適用
const stack = new MobileBackendStack(app, "MobileBackend", {
  environment: "production",
  project: "mobile-app",
});

// タグ付け
Aspects.of(stack).add(new TaggingAspect({
  Environment: "production",
  Project: "mobile-app",
  Owner: "platform-team",
}));

// セキュリティポリシー
Aspects.of(stack).add(new SecurityPolicyAspect());

// 命名規則の検証
Aspects.of(stack).add(
  new NamingConventionAspect(
    /^mobile-app-prod-[a-z0-9-]+$/,
    ["azurerm_resource_group", "azurerm_storage_account"]
  )
);

// コストガバナンス
Aspects.of(stack).add(new CostGovernanceAspect("production"));

// コンプライアンス
Aspects.of(stack).add(
  new ComplianceAspect(
    ["Environment", "Owner", "Project", "DataClassification"],
    true
  )
);

2. Azure特有の考慮事項

リソースグループ戦略
Azureでは全リソースがリソースグループに属する必要があります。環境ごと・機能ごとのグルーピング戦略を事前に決定しましょう。

const resourceGroup = new ResourceGroup(this, "rg", {
  name: `${projectName}-${environment}-rg`,
  location: "Japan East",
});

命名規則とタグ戦略
Azure の命名規則に従い、一貫性のあるリソース命名を実装します。

class NamingConvention {
  static getResourceName(resourceType: string, project: string, env: string): string {
    return `${project}-${env}-${resourceType}`.toLowerCase();
  }
}

モバイルアプリバックエンド必須コンポーネント

1. API層(Azure API Management + Function App)

class ApiConstruct extends Construct {
  constructor(scope: Construct, name: string, props: ApiConstructProps) {
    super(scope, name);

    // Function App for API endpoints
    const functionApp = new WindowsFunctionApp(this, "function-app", {
      name: NamingConvention.getResourceName("func", props.project, props.environment),
      resourceGroupName: props.resourceGroup.name,
      location: props.resourceGroup.location,
      servicePlanId: props.appServicePlan.id,
      storageAccountName: props.storageAccount.name,
      storageAccountAccessKey: props.storageAccount.primaryAccessKey,
      
      siteConfig: {
        applicationStack: {
          nodeVersion: "18",
        },
        cors: {
          allowedOrigins: ["*"], // 本番環境では適切に制限
        },
      },
    });

    // API Management for external exposure
    const apiManagement = new ApiManagement(this, "apim", {
      name: NamingConvention.getResourceName("apim", props.project, props.environment),
      location: props.resourceGroup.location,
      resourceGroupName: props.resourceGroup.name,
      publisherName: "Your Company",
      publisherEmail: "admin@yourcompany.com",
      skuName: "Developer_1", // 本番では Standardまたは Premium
    });
  }
}

2. データ層(Cosmos DB)

class DatabaseConstruct extends Construct {
  public readonly cosmosAccount: CosmosdbAccount;

  constructor(scope: Construct, name: string, props: DatabaseConstructProps) {
    super(scope, name);

    this.cosmosAccount = new CosmosdbAccount(this, "cosmos", {
      name: NamingConvention.getResourceName("cosmos", props.project, props.environment),
      location: props.resourceGroup.location,
      resourceGroupName: props.resourceGroup.name,
      offerType: "Standard",
      kind: "GlobalDocumentDB",
      
      consistencyPolicy: {
        consistencyLevel: "Session", // モバイルアプリに適した整合性レベル
      },
      
      geoLocation: [
        {
          location: props.resourceGroup.location,
          failoverPriority: 0,
        },
      ],
      
      capabilities: [
        { name: "EnableServerless" }, // 開発環境ではコスト削減
      ],
    });

    // Database and Container
    const database = new CosmosdbSqlDatabase(this, "db", {
      name: "mobile-app-db",
      resourceGroupName: props.resourceGroup.name,
      accountName: this.cosmosAccount.name,
    });

    new CosmosdbSqlContainer(this, "users-container", {
      name: "users",
      resourceGroupName: props.resourceGroup.name,
      accountName: this.cosmosAccount.name,
      databaseName: database.name,
      partitionKeyPath: "/userId",
      throughput: 400, // 最小RU設定
    });
  }
}

3. 認証・認可(Azure AD B2C)

class AuthConstruct extends Construct {
  constructor(scope: Construct, name: string, props: AuthConstructProps) {
    super(scope, name);

    // B2C テナントは手動作成が必要(Terraformで作成不可)
    // アプリケーション登録のみ自動化
    const adApplication = new Application(this, "ad-app", {
      displayName: `${props.project}-${props.environment}-mobile-app`,
      web: {
        redirectUris: ["https://your-app.com/auth/callback"],
      },
      requiredResourceAccess: [
        {
          resourceAppId: "00000003-0000-0000-c000-000000000000", // Microsoft Graph
          resourceAccess: [
            {
              id: "e1fe6dd8-ba31-4d61-89e7-88639da4683d", // User.Read
              type: "Scope",
            },
          ],
        },
      ],
    });
  }
}

4. ファイルストレージ(Azure Blob Storage)

class StorageConstruct extends Construct {
  public readonly storageAccount: StorageAccount;

  constructor(scope: Construct, name: string, props: StorageConstructProps) {
    super(scope, name);

    this.storageAccount = new StorageAccount(this, "storage", {
      name: NamingConvention.getResourceName("st", props.project, props.environment)
        .replace(/-/g, ""), // ストレージアカウント名はハイフン不可
      resourceGroupName: props.resourceGroup.name,
      location: props.resourceGroup.location,
      accountTier: "Standard",
      accountReplicationType: "LRS",
      
      blobProperties: {
        cors: [
          {
            allowedHeaders: ["*"],
            allowedMethods: ["GET", "POST", "PUT"],
            allowedOrigins: ["*"], // 本番環境では適切に制限
            exposedHeaders: ["*"],
            maxAgeInSeconds: 300,
          },
        ],
      },
    });

    // ユーザーアップロード用コンテナ
    new StorageContainer(this, "uploads", {
      name: "user-uploads",
      storageAccountName: this.storageAccount.name,
      containerAccessType: "private",
    });
  }
}

環境管理とデプロイ戦略

1. 環境ごとの設定分離

// config/environments.ts
export interface EnvironmentConfig {
  environment: string;
  location: string;
  cosmosDbTier: string;
  functionAppSku: string;
  apimSku: string;
}

export const environments: Record<string, EnvironmentConfig> = {
  dev: {
    environment: "dev",
    location: "Japan East",
    cosmosDbTier: "Serverless",
    functionAppSku: "Y1",
    apimSku: "Developer_1",
  },
  staging: {
    environment: "staging",
    location: "Japan East",
    cosmosDbTier: "Provisioned",
    functionAppSku: "EP1",
    apimSku: "Standard_1",
  },
  prod: {
    environment: "prod",
    location: "Japan East",
    cosmosDbTier: "Provisioned",
    functionAppSku: "EP2",
    apimSku: "Premium_1",
  },
};

2. CI/CDパイプライン

# Azure DevOps Pipeline例
trigger:
  branches:
    include:
      - main
      - develop

variables:
  - group: azure-credentials

stages:
  - stage: Plan
    jobs:
      - job: TerraformPlan
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: '18.x'
          
          - script: |
              npm install -g cdktf-cli@latest
              npm install
            displayName: 'Install dependencies'
          
          - script: |
              cdktf plan
            displayName: 'CDK for Terraform Plan'
            env:
              AZURE_CLIENT_ID: $(AZURE_CLIENT_ID)
              AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET)
              AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)
              AZURE_TENANT_ID: $(AZURE_TENANT_ID)

注意点とベストプラクティス

1. Aspectsの効果的な活用

Aspectsの実行順序
Aspectsは追加された順序で実行されます。依存関係のあるAspectsは順序に注意してください。

// 正しい順序での適用
Aspects.of(stack).add(new TaggingAspect(baseTags));           // 1. 基本タグ
Aspects.of(stack).add(new SecurityPolicyAspect());           // 2. セキュリティ設定
Aspects.of(stack).add(new CostGovernanceAspect(environment)); // 3. コストガバナンス
Aspects.of(stack).add(new ComplianceAspect(requiredTags));    // 4. コンプライアンス検証

条件付きAspect適用

class ConditionalAspect implements IAspect {
  visit(node: IConstruct): void {
    // 特定の条件でのみAspectを適用
    if (node.node.tryGetContext("enableStrictSecurity") === "true") {
      this.applyStrictSecurity(node);
    }
  }
  
  private applyStrictSecurity(node: IConstruct): void {
    // 厳格なセキュリティ設定の適用
  }
}

// コンテキストでの制御
const app = new App({
  context: {
    enableStrictSecurity: process.env.NODE_ENV === "production" ? "true" : "false"
  }
});

Aspectsのテスト

import { Testing } from "cdktf";

describe("TaggingAspect", () => {
  it("should add tags to all resources", () => {
    const app = Testing.app();
    const stack = new TerraformStack(app, "test");
    
    // リソース作成
    new ResourceGroup(stack, "rg", {
      name: "test-rg",
      location: "Japan East"
    });
    
    // Aspect適用
    Aspects.of(stack).add(new TaggingAspect({
      Environment: "test",
      Project: "mobile-app"
    }));
    
    // テスト実行
    const synthesized = Testing.synth(stack);
    
    // アサーション
    expect(synthesized).toHaveResourceWithProperties("azurerm_resource_group", {
      tags: {
        Environment: "test",
        Project: "mobile-app"
      }
    });
  });
});

2. セキュリティ

機密情報の管理

  • Azure Key Vaultを使用した機密情報の管理
  • 環境変数やCI/CD変数での認証情報管理
  • リソースレベルでのアクセス制御(RBAC)設定
class SecurityConstruct extends Construct {
  constructor(scope: Construct, name: string, props: SecurityConstructProps) {
    super(scope, name);

    const keyVault = new KeyVault(this, "kv", {
      name: NamingConvention.getResourceName("kv", props.project, props.environment),
      location: props.resourceGroup.location,
      resourceGroupName: props.resourceGroup.name,
      tenantId: props.tenantId,
      skuName: "standard",
      
      accessPolicy: [
        {
          tenantId: props.tenantId,
          objectId: props.servicePrincipalObjectId,
          secretPermissions: ["Get", "List", "Set"],
        },
      ],
    });
  }
}

2. コスト最適化

現在の構成での月額コスト試算(日本東部リージョン)

サービス 構成 月額コスト(USD)
API Management Developer SKU ~$50
Azure Functions Premium EP1 ~$74
Cosmos DB Serverless (5GB, 100 RU/s) ~$25
Blob Storage Standard LRS (10GB) ~$0.50
Key Vault 1,000 operations ~$3
Application Insights 1GB/月 ~$2.88
Log Analytics 5GB/月 ~$12.50
Azure AD B2C 10,000 MAU以下 無料
合計 約$167/月

コスト重視の最小構成(月額約$15-30)

開発・検証環境や小規模アプリ向けの最小構成:

class MinimalMobileBackendStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    const resourceGroup = new ResourceGroup(this, "rg", {
      name: "mobile-app-minimal-rg",
      location: "Japan East",
    });

    // 1. Azure Functions (Consumption Plan) - 無料枠あり
    const storageAccount = new StorageAccount(this, "storage", {
      name: "mobileappminst001",
      resourceGroupName: resourceGroup.name,
      location: resourceGroup.location,
      accountTier: "Standard",
      accountReplicationType: "LRS", // 最安レプリケーション
      
      // CORS設定(API Management代替)
      blobProperties: {
        cors: [
          {
            allowedHeaders: ["*"],
            allowedMethods: ["GET", "POST", "PUT", "DELETE"],
            allowedOrigins: ["https://yourdomain.com"], // 本番では適切に制限
            exposedHeaders: ["*"],
            maxAgeInSeconds: 86400,
          },
        ],
      },
    });

    const appServicePlan = new ServicePlan(this, "plan", {
      name: "mobile-app-minimal-plan",
      resourceGroupName: resourceGroup.name,
      location: resourceGroup.location,
      osType: "Windows",
      skuName: "Y1", // Consumption Plan - 無料枠100万実行/月
    });

    const functionApp = new WindowsFunctionApp(this, "functions", {
      name: "mobile-app-minimal-func",
      resourceGroupName: resourceGroup.name,
      location: resourceGroup.location,
      servicePlanId: appServicePlan.id,
      storageAccountName: storageAccount.name,
      storageAccountAccessKey: storageAccount.primaryAccessKey,
      
      siteConfig: {
        applicationStack: {
          nodeVersion: "18",
        },
        // カスタムドメインでHTTPS強制(SSL証明書は Let's Encrypt等)
        httpLogsEnabled: true,
      },
      
      // 環境変数でCosmos DB接続文字列設定
      appSettings: {
        "COSMOS_DB_ENDPOINT": cosmosAccount.endpoint,
        "COSMOS_DB_KEY": cosmosAccount.primaryKey,
      },
    });

    // 2. Cosmos DB (Serverless) - 使用量ベース課金
    const cosmosAccount = new CosmosdbAccount(this, "cosmos", {
      name: "mobile-app-minimal-cosmos",
      location: resourceGroup.location,
      resourceGroupName: resourceGroup.name,
      offerType: "Standard",
      kind: "GlobalDocumentDB",
      
      consistencyPolicy: {
        consistencyLevel: "Session",
      },
      
      geoLocation: [
        {
          location: resourceGroup.location,
          failoverPriority: 0,
        },
      ],
      
      // Serverless(最も安価)
      capabilities: [
        { name: "EnableServerless" },
      ],
      
      // バックアップ設定(コスト削減)
      backup: {
        type: "Periodic",
        intervalInMinutes: 1440, // 24時間間隔
        retentionInHours: 168,   // 7日間保持
      },
    });

    const database = new CosmosdbSqlDatabase(this, "db", {
      name: "mobile-app-db",
      resourceGroupName: resourceGroup.name,
      accountName: cosmosAccount.name,
    });

    // 単一コンテナで複数エンティティを管理(コスト削減)
    new CosmosdbSqlContainer(this, "main-container", {
      name: "entities",
      resourceGroupName: resourceGroup.name,
      accountName: cosmosAccount.name,
      databaseName: database.name,
      partitionKeyPath: "/entityType", // ユーザー、投稿、設定等を区別
      // Serverlessなのでthroughput設定不要
    });

    // 3. 認証:Azure AD B2C(無料枠活用)+ 簡素化
    // B2Cテナント自体は無料(50,000 MAU以下)
    // アプリケーション登録のみ実施

    // 4. 基本的な監視(無料枠活用)
    new ApplicationInsights(this, "insights", {
      name: "mobile-app-minimal-ai",
      location: resourceGroup.location,
      resourceGroupName: resourceGroup.name,
      applicationType: "web",
      // Classic Application Insights(無料枠5GB/月)
      samplingPercentage: 10, // サンプリング率を下げてデータ量削減
    });
  }
}

最小構成の月額コスト試算

サービス 使用量 月額コスト(USD)
Azure Functions 100万実行以下 無料
Cosmos DB Serverless 1GB, 50 RU/s平均 ~$5
Blob Storage 1GB, LRS ~$0.02
Application Insights 500MB/月 無料枠内
Azure AD B2C 10,000 MAU以下 無料
合計 約$5-10/月

段階的拡張プラン

// 成長に応じた段階的拡張のためのAspect
class ScalabilityAspect implements IAspect {
  private userCount: number;
  private requestsPerMonth: number;

  constructor(userCount: number, requestsPerMonth: number) {
    this.userCount = userCount;
    this.requestsPerMonth = requestsPerMonth;
  }

  visit(node: IConstruct): void {
    // ユーザー数に応じてリソースを調整
    if (node instanceof CosmosdbAccount) {
      this.adjustCosmosSettings(node as CosmosdbAccount);
    }
    
    if (node instanceof ServicePlan) {
      this.adjustFunctionPlan(node as ServicePlan);
    }
  }

  private adjustCosmosSettings(cosmos: CosmosdbAccount): void {
    // 1,000ユーザー以上でProvisioned RUに切り替え
    if (this.userCount > 1000) {
      // Serverless から Provisioned に変更
      console.log("Switching to Provisioned throughput for better performance");
    }
  }

  private adjustFunctionPlan(plan: ServicePlan): void {
    // 月間100万リクエスト超過でPremium Planに切り替え
    if (this.requestsPerMonth > 1000000) {
      plan.skuName = "EP1"; // Premium Plan
      console.log("Upgrading to Premium plan for better performance");
    }
  }
}

セキュリティ対策のコスト最適化

最小構成でも最低限のセキュリティを確保:

class CostOptimizedSecurityAspect implements IAspect {
  visit(node: IConstruct): void {
    // 無料・低コストでできるセキュリティ対策のみ実装
    
    if (node instanceof StorageAccount) {
      const storage = node as StorageAccount;
      
      // 無料でできるセキュリティ設定
      storage.enableHttpsTrafficOnly = true;
      storage.minTlsVersion = "TLS1_2";
      
      // Key Vaultは使わず、環境変数で管理(コスト削減)
      // 本番環境では非推奨だが開発環境なら許容
    }

    if (node instanceof CosmosdbAccount) {
      const cosmos = node as CosmosdbAccount;
      
      // IPフィルタリング(無料)
      cosmos.ipRangeFilter = "Your-Office-IP"; // 開発時のアクセス制限
      
      // ファイアウォール設定(追加費用なし)
      cosmos.isVirtualNetworkFilterEnabled = false; // VNet統合は高コスト
    }

    if (node instanceof WindowsFunctionApp) {
      const func = node as WindowsFunctionApp;
      
      // 認証設定(無料)
      func.authSettings = {
        enabled: true,
        defaultProvider: "AzureActiveDirectory",
        // 簡素化された認証設定
      };
    }
  }
}

開発環境向けコスト削減Tips

  1. リソースの自動停止
class AutoShutdownAspect implements IAspect {
  visit(node: IConstruct): void {
    if (node instanceof WindowsFunctionApp) {
      // 開発環境では夜間自動停止
      // Azure Automationで実装(月額$5程度)
    }
  }
}
  1. 監視レベルの調整
// Application Insights サンプリング設定
new ApplicationInsights(this, "insights", {
  // 開発環境では10%サンプリング
  samplingPercentage: process.env.NODE_ENV === "production" ? 100 : 10,
});
  1. バックアップ戦略の最適化
// Cosmos DB バックアップ頻度を下げる
backup: {
  type: "Periodic",
  intervalInMinutes: process.env.NODE_ENV === "production" ? 240 : 1440, // 本番4時間、開発24時間
  retentionInHours: process.env.NODE_ENV === "production" ? 720 : 168,   // 本番30日、開発7日
},

予算アラートの実装

class BudgetAlertConstruct extends Construct {
  constructor(scope: Construct, name: string, props: { budgetAmount: number }) {
    super(scope, name);

    new ConsumptionBudgetResourceGroup(this, "budget", {
      name: "mobile-app-budget",
      resourceGroupId: props.resourceGroupId,
      amount: props.budgetAmount,
      timeGrain: "Monthly",
      
      notification: [
        {
          enabled: true,
          threshold: 80,
          operator: "GreaterThan",
          contactEmails: ["admin@yourcompany.com"],
        },
      ],
    });
  }
}

3. パフォーマンス

Cosmos DBのパーティション設計

  • モバイルアプリのアクセスパターンを考慮したパーティションキー設計
  • ホットパーティション回避のための適切なデータ分散

CDN活用

// Azure Front Doorによるコンテンツ配信最適化
const frontDoor = new FrontdoorProfile(this, "frontdoor", {
  name: NamingConvention.getResourceName("fd", props.project, props.environment),
  resourceGroupName: props.resourceGroup.name,
  skuName: "Standard_AzureFrontDoor",
});

4. 運用面での考慮事項

バックアップとディザスタリカバリ

  • Cosmos DBの自動バックアップ設定
  • 複数リージョンへのデプロイ戦略
  • データ復旧手順の文書化

ログとメトリクス

  • 構造化ログの実装
  • カスタムメトリクスの定義
  • アプリケーションパフォーマンス監視(APM)の設定

プロジェクト構成例

推奨するプロジェクト構造(Aspectsを考慮):

mobile-backend-infra/
├── src/
│   ├── constructs/           # 再利用可能なコンポーネント
│   │   ├── api.ts
│   │   ├── database.ts
│   │   ├── auth.ts
│   │   └── storage.ts
│   ├── aspects/              # 横断的関心事
│   │   ├── tagging.ts
│   │   ├── security.ts
│   │   ├── naming.ts
│   │   ├── cost-governance.ts
│   │   └── compliance.ts
│   ├── stacks/              # 環境別スタック定義
│   │   ├── dev-stack.ts
│   │   ├── staging-stack.ts
│   │   └── prod-stack.ts
│   └── main.ts              # エントリーポイント
├── config/
│   ├── environments.ts      # 環境設定
│   └── aspects.ts           # Aspects設定
├── tests/                   # テストコード
│   ├── constructs/
│   ├── aspects/
│   └── stacks/
├── docs/                    # ドキュメント
├── cdktf.json              # CDKTFプロジェクト設定
└── package.json

Aspects設定ファイルの例

// config/aspects.ts
export interface AspectConfig {
  tagging: {
    commonTags: Record<string, string>;
    requiredTags: string[];
  };
  security: {
    enforceHttpsOnly: boolean;
    minTlsVersion: string;
    enableIpFiltering: boolean;
  };
  costGovernance: {
    maxCosmosRu: number;
    maxAppServiceSku: string;
    enableAutoShutdown: boolean;
  };
  compliance: {
    dataClassificationRequired: boolean;
    encryptionRequired: boolean;
    auditLoggingRequired: boolean;
  };
}

export const aspectConfigs: Record<string, AspectConfig> = {
  dev: {
    tagging: {
      commonTags: {
        Environment: "development",
        AutoShutdown: "true",
      },
      requiredTags: ["Owner", "Project"],
    },
    security: {
      enforceHttpsOnly: true,
      minTlsVersion: "TLS1_2",
      enableIpFiltering: false,
    },
    costGovernance: {
      maxCosmosRu: 400,
      maxAppServiceSku: "Y1",
      enableAutoShutdown: true,
    },
    compliance: {
      dataClassificationRequired: false,
      encryptionRequired: true,
      auditLoggingRequired: false,
    },
  },
  prod: {
    tagging: {
      commonTags: {
        Environment: "production",
        BackupRequired: "true",
        MonitoringLevel: "high",
      },
      requiredTags: ["Owner", "Project", "CostCenter", "DataClassification"],
    },
    security: {
      enforceHttpsOnly: true,
      minTlsVersion: "TLS1_3",
      enableIpFiltering: true,
    },
    costGovernance: {
      maxCosmosRu: 10000,
      maxAppServiceSku: "P3",
      enableAutoShutdown: false,
    },
    compliance: {
      dataClassificationRequired: true,
      encryptionRequired: true,
      auditLoggingRequired: true,
    },
  },
};

メインファイルでのAspects適用

// src/main.ts
import { aspectConfigs } from "../config/aspects";
import { TaggingAspect } from "./aspects/tagging";
import { SecurityPolicyAspect } from "./aspects/security";
import { CostGovernanceAspect } from "./aspects/cost-governance";
import { ComplianceAspect } from "./aspects/compliance";

const environment = process.env.ENVIRONMENT || "dev";
const aspectConfig = aspectConfigs[environment];

const app = new App();
const stack = new MobileBackendStack(app, `MobileBackend-${environment}`, {
  environment,
});

// 環境固有のAspectsを順序よく適用
Aspects.of(stack).add(
  new TaggingAspect(
    aspectConfig.tagging.commonTags,
    aspectConfig.tagging.requiredTags
  )
);

Aspects.of(stack).add(
  new SecurityPolicyAspect(aspectConfig.security)
);

Aspects.of(stack).add(
  new CostGovernanceAspect(environment, aspectConfig.costGovernance)
);

Aspects.of(stack).add(
  new ComplianceAspect(aspectConfig.compliance)
);

app.synth();

まとめ

CDK for TerraformによるAzureモバイルアプリバックエンド構築では、以下の点に注意することで成功に近づけます:

  1. 適切な抽象化: Constructパターンを活用した再利用可能なコンポーネント設計
  2. Aspectsの戦略的活用: 横断的関心事(タグ付け、セキュリティ、コンプライアンス)の自動化
  3. 環境分離: 設定ファイルによる環境ごとの差分管理とAspects設定の分離
  4. セキュリティファースト: Key Vaultと適切な認証・認可の実装、Aspectsによるポリシー自動適用
  5. コスト意識: 環境に応じたリソースサイジングとAspectsによるガバナンス自動化
  6. 運用性: 監視、ログ、バックアップの適切な設定とAspectsによる標準化

Aspectsの効果的な活用により以下のメリットが得られます:

  • 一貫性の担保: すべてのリソースに対する統一されたポリシー適用
  • 人為的ミスの削減: 手動設定が必要な項目の自動化
  • コンプライアンス対応: 規制要件の自動チェックと適用
  • 運用負荷軽減: 繰り返し作業の自動化と標準化
  • 監査対応: 設定の証跡とガバナンスの可視化

これらのポイントを押さえ、特にAspectsを戦略的に活用することで、スケーラブルで保守性が高く、ガバナンスの効いたモバイルアプリバックエンドインフラを構築できるでしょう。

次回は実際のコード実装例と、Aspectsを含む具体的なデプロイフローについて解説予定です。

Discussion