0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第2回: CDKで作るPrivate Link双方向通信 - スタック分割とデッドロック回避編

Posted at

はじめに

こんにちは!前回はPrivate Link縛りの環境でのアーキテクチャ設計について説明しました。今回は、実際にCDKで実装する際に必ず直面する問題について解説します。

それは...「デッドロック問題」です😱

VPC間でPrivate Linkを双方向に張る際、お互いが相手の情報を参照する必要があるため、CDKスタックが循環参照でデプロイできなくなってしまうんです。

この記事では、そんな厄介な問題を3つのスタック分割SSMパラメータを使って解決する方法を詳しく解説します!

今回の内容

  • CDKでのデッドロック問題とは?
  • 3つのスタック分割戦略
  • SSMパラメータを使った疎結合設計
  • 実際のCDKコード例
  • 運用効率を考慮した構成のポイント

CDKでのデッドロック問題

問題の発生パターン

Private Linkで双方向通信を実現しようとすると、以下のような依存関係が発生します:

具体的な例

// ❌ これはデッドロックする例
export class PublicStack extends Stack {
  constructor(scope: Construct, id: string, props: {
    isolatedNlbArn: string // <- Isolated Stackの情報が必要
  }) {
    // VPC Endpointを作成するためにIsolated側のNLBが必要
    const vpcEndpoint = new VpcEndpoint(this, 'VpcEndpoint', {
      service: VpcEndpointService.fromVpcEndpointServiceId(
        this, 'Service', props.isolatedNlbArn
      )
    });
  }
}

export class IsolatedStack extends Stack {
  constructor(scope: Construct, id: string, props: {
    publicNlbArn: string // <- Public Stackの情報が必要  
  }) {
    // 同じくPublic側のNLBが必要
    const vpcEndpoint = new VpcEndpoint(this, 'VpcEndpoint', {
      service: VpcEndpointService.fromVpcEndpointServiceId(
        this, 'Service', props.publicNlbArn
      )
    });
  }
}

お互いが相手の情報を必要とするため、どちらから先にデプロイしても失敗してしまいます...

Private Link双方向通信フロー

この問題を解決するために、以下のスタックに分割します:

1. Foundation Stack(基盤スタック)

  • 他に依存しない基盤リソース
  • VPC, Subnet, SecurityGroup
  • NLB, ALB, VPC Endpoint Service
  • SSMパラメータへの出力

2. Connection Stack(接続スタック)

  • VPC Endpoint(Interface VPC Endpoint)
  • SSMパラメータから基盤情報を取得

3. Application Stack(アプリケーションスタック)

  • ECS Service
  • Auto Scaling Group
  • アプリケーション固有の設定

4. Egress Stack(出口スタック)※任意

  • NAT Gateway、Firewall Gateway
  • インターネット出口の統合管理
  • 大規模環境や出口専用VPCがある組織で有効

SSMパラメータを使った疎結合設計

重要な設計原則

SSMパラメータを使う際の重要なポイント:

  • 名前空間の一元管理: /microservice/{environment}/{key} 形式で統一
  • 環境分離: public/isolated/prod/stgを必ずパスに含める(上書き防止)
  • サイズ制限に注意: Parameter Storeは65KB上限なので、長いJSONより ID/ARN のみを保存
  • 型安全性: StringParameterで型チェックを活用

基盤スタックでの出力

📝 Foundation Stack の実装例をクリックして展開
// Foundation Stack - 基盤リソースの作成と出力
export class FoundationStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);
    
    // VPC作成 - 環境別CIDR設定
    const vpcCidr = props.environment === 'public' ? '10.0.0.0/16' : '10.1.0.0/16';
    const vpc = new Vpc(this, 'Vpc', {
      cidr: vpcCidr,
      maxAzs: 2,
      natGateways: 0, // Isolated環境ではNAT不要
      subnetConfiguration: [
        {
          name: 'Private',
          subnetType: SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        }
      ]
    });
    
    // NLB作成
    const nlb = new NetworkLoadBalancer(this, 'NLB', {
      vpc,
      internetFacing: false,
      scheme: LoadBalancerScheme.INTERNAL,
    });
    
    // ALB作成
    const alb = new ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      internetFacing: false,
      scheme: LoadBalancerScheme.INTERNAL,
    });
    
    // VPC Endpoint Service作成
    const vpcEndpointService = new VpcEndpointService(this, 'VpcEndpointService', {
      vpcEndpointServiceLoadBalancers: [nlb],
      acceptanceRequired: false,
    });
    
    // SSMパラメータに出力 - 他のスタックから参照可能
    new StringParameter(this, 'VpcId', {
      parameterName: `/microservice/${props.environment}/vpc-id`,
      stringValue: vpc.vpcId,
    });
    
    new StringParameter(this, 'VpcEndpointServiceId', {
      parameterName: `/microservice/${props.environment}/vpc-endpoint-service-id`,
      stringValue: vpcEndpointService.vpcEndpointServiceId,
    });
    
    new StringParameter(this, 'NLBArn', {
      parameterName: `/microservice/${props.environment}/nlb-arn`,
      stringValue: nlb.loadBalancerArn,
    });
    
    new StringParameter(this, 'ALBArn', {
      parameterName: `/microservice/${props.environment}/alb-arn`,
      stringValue: alb.loadBalancerArn,
    });
  }
}

接続スタックでの取得

🔗 Connection Stack の実装例をクリックして展開
// Connection Stack - SSMから情報を取得してVPC Endpointを作成
export class ConnectionStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps & {
    targetEnvironment: string;
    direction: 'public-to-isolated' | 'isolated-to-public';
  }) {
    super(scope, id, props);
    
    // SSMから対象環境の情報を取得
    const targetVpcEndpointServiceId = StringParameter.valueForStringParameter(
      this, 
      `/microservice/${props.targetEnvironment}/vpc-endpoint-service-id`
    );
    
    const ownVpcId = StringParameter.valueForStringParameter(
      this, 
      `/microservice/${props.environment}/vpc-id`
    );
    
    // VPC Endpoint作成(Interface VPC Endpoint)
    const vpcEndpoint = new InterfaceVpcEndpoint(this, 'VpcEndpoint', {
      vpc: Vpc.fromVpcAttributes(this, 'Vpc', {
        vpcId: ownVpcId,
        availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c'],
      }),
      service: new InterfaceVpcEndpointService(`com.amazonaws.vpce.${targetVpcEndpointServiceId}`, 443),
      subnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED
      }
    });
    
    // VPC Endpoint DNSをSSMに出力
    new StringParameter(this, 'VpcEndpointDns', {
      parameterName: `/microservice/${props.environment}/vpc-endpoint-dns/${props.direction}`,
      stringValue: vpcEndpoint.vpcEndpointDnsEntries[0].domainName,
    });
  }
}

デプロイ順序の最適化

デプロイフローの視覚化

bin/app.tsでの制御

⚙️ bin/app.ts の実装例をクリックして展開
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { FoundationStack } from '../lib/foundation-stack';
import { ConnectionStack } from '../lib/connection-stack';
import { ApplicationStack } from '../lib/application-stack';

const app = new cdk.App();

// 環境設定
const environments = ['public', 'isolated'];
const region = 'ap-northeast-1';

// Phase 1: Foundation Stacks - 並列実行可能
environments.forEach(env => {
  new FoundationStack(app, `Foundation-${env}`, {
    env: { region },
    environment: env,
    stackName: `microservice-foundation-${env}`,
  });
});

// Phase 2: Connection Stacks - Foundation完了後に実行
environments.forEach(env => {
  const targetEnv = env === 'public' ? 'isolated' : 'public';
  const direction = env === 'public' ? 'public-to-isolated' : 'isolated-to-public';
  
  new ConnectionStack(app, `Connection-${env}`, {
    env: { region },
    environment: env,
    targetEnvironment: targetEnv,
    direction: direction,
    stackName: `microservice-connection-${env}`,
  });
});

// Phase 3: Application Stacks - 全て完了後に実行
const microservices = [
  { name: 'user-service', port: 3000, path: '/users' },
  { name: 'order-service', port: 3001, path: '/orders' },
  { name: 'payment-service', port: 3002, path: '/payments' },
];

microservices.forEach(service => {
  new ApplicationStack(app, `App-${service.name}`, {
    env: { region },
    serviceName: service.name,
    port: service.port,
    path: service.path,
    stackName: `microservice-app-${service.name}`,
  });
});

運用効率を考慮した構成のポイント

1. 段階的デプロイ戦略

# Phase 1: Foundation(並列デプロイ可能)
cdk deploy Foundation-public Foundation-isolated --concurrency 2

# Phase 2: Connection(並列デプロイ可能)  
cdk deploy Connection-public Connection-isolated --concurrency 2

# Phase 3: Application(必要数だけ並列)
cdk deploy App-* --concurrency 4

💡 Tip: CDK v2の--concurrencyは2025-04時点で安定版。大規模環境では必須機能です!

2. 環境別パラメータ管理

🔧 環境設定の一元管理コードをクリックして展開
// 環境設定の一元管理
interface EnvironmentConfig {
  vpcCidr: string;
  maxAzs: number;
  natGateways: number;
  monitoring: boolean;
}

const configs: Record<string, EnvironmentConfig> = {
  public: {
    vpcCidr: '10.0.0.0/16',
    maxAzs: 2,
    natGateways: 1,
    monitoring: true,
  },
  isolated: {
    vpcCidr: '10.1.0.0/16', 
    maxAzs: 2,
    natGateways: 0,
    monitoring: true,
  }
};

3. セキュリティチェックの自動化

# cdk-nagをCIに組み込み
npm install -g cdk-nag

# 基本セキュリティチェックを自動実行
cdk synth --all
cdk-nag --app="npx cdk synth" --rules-only

4. エラーハンドリングとロールバック

🛠️ エラーハンドリングとロールバック機能をクリックして展開
// SSMパラメータの存在確認
const getParameterSafely = (scope: Construct, parameterName: string): string => {
  try {
    return StringParameter.valueForStringParameter(scope, parameterName);
  } catch (error) {
    throw new Error(`Required parameter ${parameterName} not found. Please deploy foundation stack first.`);
  }
};

// 個別スタックのロールバック対応
const rollbackConnection = async (stackName: string) => {
  await execSync(`cdk destroy ${stackName} --force`, { stdio: 'inherit' });
};

5. Egress Stackの後置設計

将来的に共通出口(NAT Gateway / Firewall Gateway)を追加する場合、Egress Stackを分離しておくと簡単に対応できます:

🚪 Egress Stack 設計例をクリックして展開
// 将来の拡張を見越した設計
new EgressStack(app, 'Egress-Common', {
  foundationStacks: [foundationPublic, foundationIsolated],
  egressType: 'nat-gateway', // or 'firewall-gateway'
});

次回予告

次回は「Public VPC → Isolated VPC通信編」として、実際のProxyサーバーの実装とVPC Endpoint構成について詳しく解説します。

Pythonで作るAPI一覧収集機能や、ALBのパスルーティング設定、カナリアデプロイの実装方法など、より実践的な内容をお届けします!

まとめ

CDKでPrivate Link双方向通信を実装する際のデッドロック問題を解決する方法をご紹介しました。

重要なポイント:

  • 3+1スタック分割で依存関係を整理(Egressは任意)
  • SSMパラメータによる疎結合設計と名前空間管理
  • 段階的デプロイ並列実行による運用効率の向上
  • cdk-nagによるセキュリティチェック自動化
  • 個別ロールバックによる障害時の迅速対応

この設計により、安全かつ効率的にPrivate Link基盤を構築できるようになります。


連載記事一覧

  1. 第1回: Private Link縛りの企業環境で双方向VPC連携アーキテクチャを設計してみた
  2. 👈 第2回: CDKで作るPrivate Link双方向通信 - スタック分割とデッドロック回避編
  3. 第3回: Public VPC → Isolated VPC通信編 - プロキシサーバーとVPC Endpoint構成
  4. 第4回: Isolated VPC → Public VPC逆方向通信編
  5. 第5回: ECSマイクロサービス動的追加編 - forループで運用を楽にする
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?