[アップデート] ECS Service Connect がクロスアカウントで利用できるようになりました

[アップデート] ECS Service Connect がクロスアカウントで利用できるようになりました

2025.09.29

アップデート概要

先日 ECS Service Connect がクロスアカウントワークロードをサポートしました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/09/amazon-ecs-service-connect-cross-account-workloads/

元々 ECS Service Connect は VPC を跨いで構成することが可能です。

https://dev.classmethod.jp/articles/ecs-service-connect-another-vpc/

ただ、AWS アカウント跨ぎで構成することはできませんでした。
これは ECS Service Connect が仕組みの中で利用している CloudMap 名前空間をクロスアカウントで共有して扱えなかったという理由が大きかったと思われます。
しかし、2025 年 8 月に CloudMap 名前空間をクロスアカウントで共有できるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2025/08/aws-cloud-map-support-cross-account-service-discovery/

その上で、今回 ECS Service Connect が共有された名前空間を扱えるようになりました。

補足

CloudMap 名前空間をアカウント間で共有できるようになってから今回のアップデートが発表されるまでの間に、クロスアカウントで ECS Service Connect を構成してみようとしたことがあったのですが、共有した名前空間がマネジメントコンソールに表示されるのに、設定を完了しようとすると「NamespaceNotFound」というエラーになってしまいました。

error.png

今回のアップデートで ECS Service Connect 側も対応したため、正しく構成できるはずです。

試してみる

下記構成で試してみます。

arch-2.png

アカウント B にサーバー側のアプリケーションを配置して、アカウント A にクライアント側のコンテナを配置します。
CloudMap 名前空間はアカウント B 側 (サーバー側) で作成して RAM でアカウント A 側 (クライアント側) に共有します。
基本的には CDK で構築しつつ、クロスアカウントでの操作が必要な部分はマネジメントコンソールでの作業とします。

サーバー側の ECS Service を構成

下記 CDK で構築します。
VPC、CloudMap 名前空間、ECS を構築しています.

			
			import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as logs from "aws-cdk-lib/aws-logs";
import * as servicediscovery from "aws-cdk-lib/aws-servicediscovery";

export interface MainStackProps extends cdk.StackProps {
  repositoryName: string;
  imageTag: string;
}

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: MainStackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, "Vpc", {
      maxAzs: 2,
      natGateways: 1,
      ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
    });

    // Namespace
    const namespace = new servicediscovery.HttpNamespace(
      this,
      "ServiceConnectNamespace",
      {
        name: "sample-namespace",
      }
    );

    // ECS Cluster
    const cluster = new ecs.Cluster(this, "EcsCluster", {
      vpc: vpc,
      containerInsightsV2: ecs.ContainerInsights.ENABLED,
    });

    // ECS Task Execution Role
    const taskExecutionRole = new iam.Role(this, "TaskExecutionRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AmazonECSTaskExecutionRolePolicy"
        ),
      ],
    });

    // ECS Task Definition
    const taskDefinition = new ecs.FargateTaskDefinition(
      this,
      "TaskDefinition",
      {
        cpu: 256,
        memoryLimitMiB: 512,
        executionRole: taskExecutionRole,
      }
    );
    const ecsContainer = taskDefinition.addContainer("App", {
      image: ecs.ContainerImage.fromEcrRepository(
        ecr.Repository.fromRepositoryName(
          this,
          "AppRepository",
          props.repositoryName
        ),
        props.imageTag
      ),
      essential: true,
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: "ecs",
        logGroup: new logs.LogGroup(this, "AppLogGroup", {
          logGroupName: `/ecs/${props.repositoryName}`,
          retention: logs.RetentionDays.ONE_WEEK,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
        }),
      }),
    });
    ecsContainer.addPortMappings({
      name: "app",
      containerPort: 80,
      protocol: ecs.Protocol.TCP,
      appProtocol: ecs.AppProtocol.http,
    });

    // Security Group for App
    const appSg = new ec2.SecurityGroup(this, "AppSg", {
      vpc: vpc,
      allowAllOutbound: true,
    });
    appSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));

    // ECS Service
    new ecs.FargateService(this, "Service", {
      cluster: cluster,
      serviceName: "sample-ecs-app",
      taskDefinition: taskDefinition,
      platformVersion: ecs.FargatePlatformVersion.VERSION1_4,
      desiredCount: 1,
      assignPublicIp: false,
      securityGroups: [appSg],
      vpcSubnets: vpc.selectSubnets({
        subnetGroupName: "Private",
      }),
      healthCheckGracePeriod: cdk.Duration.seconds(240),
      serviceConnectConfiguration: {
        namespace: namespace.namespaceArn,
        services: [
          {
            portMappingName: "app",
            port: 80,
          },
        ],
      },
    });
  }
}

		

RAM で CloudMap 名前空間を共有する

RAM のサービスページで「リソース共有を作成」をクリックします。

ram1.png

CDK で作成した名前空間を選択して、「次へ」をクリックします。

ram2.png

マネージド型アクセス許可は AWSRAMPermissionCloudMapECSFullPermission を選択します。

ram3.png

You must use the AWSRAMPermissionCloudMapECSFullPermission managed permission to share the namespace for Service Connect to work properly with the namespace.
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-connect-shared-namespaces-setup.html#service-connect-shared-namespaces-share

共有先のアカウントを指定します。

ram5.png

Shareable AWS resources に記載されている通り、CloudMap 名前空間を Organizations 外のアカウントに共有することはできません。
共有先が Organizations 内のアカウントであり、RAM と Organizations の統合を有効化している必要があります。

ram8.png

設定内容を確認して「リソースの共有を作成」をクリックします。

ram6.png

共有が完了すると、アカウント A 側でも名前空間を確認できるようになります。

ram10.png

クライアント側の ECS Service Connect を構成

クライアント側のリソースも下記のように定義します。

			
			import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";

export interface ClientStackProps extends cdk.StackProps {
  namespaceArn: string;
}

export class ClientStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: ClientStackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, "Vpc", {
      maxAzs: 2,
      natGateways: 1,
      ipAddresses: ec2.IpAddresses.cidr("172.31.0.0/16"),
    });

    // ECS Cluster
    const cluster = new ecs.Cluster(this, "EcsCluster", {
      vpc: vpc,
      containerInsightsV2: ecs.ContainerInsights.ENABLED,
    });

    // ECS Task Execution Role
    const taskExecutionRole = new iam.Role(this, "TaskExecutionRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AmazonECSTaskExecutionRolePolicy"
        ),
      ],
    });

    // Client ECS Task Role
    const clientTaskRole = new iam.Role(this, "ClientTaskRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      inlinePolicies: {
        SSMMessagesPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel",
              ],
              resources: ["*"],
            }),
          ],
        }),
      },
    });

    // Security Group for Client
    const clientSg = new ec2.SecurityGroup(this, "ClientSg", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    // Client ECS Task Definition
    const clientTaskDefinition = new ecs.FargateTaskDefinition(
      this,
      "ClientTaskDefinition",
      {
        cpu: 256,
        memoryLimitMiB: 512,
        taskRole: clientTaskRole,
        executionRole: taskExecutionRole,
      }
    );
    clientTaskDefinition.addContainer("client", {
      image: ecs.ContainerImage.fromRegistry("amazonlinux:latest"),
      essential: true,
      command: ["/bin/bash", "-c", "while true; do sleep 30; done"],
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: "client",
        logGroup: new logs.LogGroup(this, "ClientLogGroup", {
          logGroupName: `/ecs/client`,
          retention: logs.RetentionDays.ONE_WEEK,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
        }),
      }),
    });

    // Client ECS Service
    new ecs.FargateService(this, "ClientService", {
      cluster: cluster,
      serviceName: "client-service",
      taskDefinition: clientTaskDefinition,
      desiredCount: 1,
      assignPublicIp: false,
      securityGroups: [clientSg],
      vpcSubnets: vpc.selectSubnets({
        subnetGroupName: "Private",
      }),
      enableExecuteCommand: true,
      serviceConnectConfiguration: {
        namespace: props.namespaceArn,
        services: [],
      },
    });
  }
}

		

この段階でクライアント側のコンテナに ECS Exec で入り込んで /etc/hosts を確認すると app.sample-namespace が追加されていました。

			
			sh-5.2# cat /etc/hosts
127.0.0.1 localhost
172.31.177.148 ip-172-31-177-148.ap-northeast-1.compute.internal
127.255.0.1 app.sample-namespace
2600:f0f0:0:0:0:0:0:1 app.sample-namespace

		

ECS Service Connect 間通信は /etc/hosts 経由で名前解決を行って ECS Service Connect エージェントにルーティングします。
設定としては上手く入っていそうです。

https://dev.classmethod.jp/articles/ecs-service-connect-trrafic-flow/

ただ、この状態だとネットワーク的に疎通していないので、HTTP リクエストを送っても何も返ってきません。

VPC ピアリングを構成

2 つの VPC を VPC ピアリングで接続します。

peering.png

承認して、各ルートテーブルにピアリング接続へのルートを設定します。

route.png

これでネットワーク的にも疎通しました。

動作確認

ECS Exec でクライアント側のコンテナに入り込み、HTTP リクエストを送ってみると無事通信できました!

			
			sh-5.2# curl -I http://app.sample-namespace
HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 11
etag: W/"b-Ck1VqNd45QIvq3AZd8XYQLvEhtA"
date: Sun, 28 Sep 2025 14:55:23 GMT
x-envoy-upstream-service-time: 3
server: envoy

		

最後に

複数のアカウントに跨るシステムを ECS Service Connect で接続できるようになり、適用範囲が増えました。
ただし、ネットワーク的には疎通しておく必要があり、CIDR 被りなども意識する必要があるので VPC Lattice とは上手く使い分ける必要があります。
Lattice を挟むことでレイテンシが気になるワークロードや Lattice のコストが気になる場合にクロスアカウントでも ECS Service Connect を利用してみると良いかもしれませんね!

この記事をシェアする

FacebookHatena blogX

関連記事

[アップデート] ECS Service Connect がクロスアカウントで利用できるようになりました | DevelopersIO