[アップデート] ECS Service Connect がクロスアカウントで利用できるようになりました
アップデート概要
先日 ECS Service Connect がクロスアカウントワークロードをサポートしました。
元々 ECS Service Connect は VPC を跨いで構成することが可能です。
ただ、AWS アカウント跨ぎで構成することはできませんでした。
これは ECS Service Connect が仕組みの中で利用している CloudMap 名前空間をクロスアカウントで共有して扱えなかったという理由が大きかったと思われます。
しかし、2025 年 8 月に CloudMap 名前空間をクロスアカウントで共有できるようになりました。
その上で、今回 ECS Service Connect が共有された名前空間を扱えるようになりました。
補足
CloudMap 名前空間をアカウント間で共有できるようになってから今回のアップデートが発表されるまでの間に、クロスアカウントで ECS Service Connect を構成してみようとしたことがあったのですが、共有した名前空間がマネジメントコンソールに表示されるのに、設定を完了しようとすると「NamespaceNotFound」というエラーになってしまいました。
今回のアップデートで ECS Service Connect 側も対応したため、正しく構成できるはずです。
試してみる
下記構成で試してみます。
アカウント 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 のサービスページで「リソース共有を作成」をクリックします。
CDK で作成した名前空間を選択して、「次へ」をクリックします。
マネージド型アクセス許可は AWSRAMPermissionCloudMapECSFullPermission
を選択します。
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
共有先のアカウントを指定します。
※ Shareable AWS resources に記載されている通り、CloudMap 名前空間を Organizations 外のアカウントに共有することはできません。
共有先が Organizations 内のアカウントであり、RAM と Organizations の統合を有効化している必要があります。
設定内容を確認して「リソースの共有を作成」をクリックします。
共有が完了すると、アカウント A 側でも名前空間を確認できるようになります。
クライアント側の 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 エージェントにルーティングします。
設定としては上手く入っていそうです。
ただ、この状態だとネットワーク的に疎通していないので、HTTP リクエストを送っても何も返ってきません。
VPC ピアリングを構成
2 つの VPC を VPC ピアリングで接続します。
承認して、各ルートテーブルにピアリング接続へのルートを設定します。
これでネットワーク的にも疎通しました。
動作確認
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 を利用してみると良いかもしれませんね!