ECS Service Connectで別クラスターのECSサービスに接続できるか試してみた
別のECSクラスターにあるECSサービスにECS Service Connectで接続できるかを調査してみました。
結論
可能です。
ECSサービスが別のECSクラスターに所属していても、同リージョンで同一のCloud Map名前空間を選択すれば、Service Connect経由で通信できます。
名前空間内のサービスは、同じ AWS アカウント内の同じ AWS リージョン内の異なる Amazon ECS クラスターに分散させることができます。
Clusterが異なるVPCにある場合は、VPC間の相互通信のためにVPCピアリングなど疎通性の確保は必要になります。
やってみた
実際に以下の構成をCDKで作成して、疎通を試してみました。
CDKでリソースをデプロイする
以下のコードを用意します。
ServerとClientt用のECSサービスをそれぞれ別クラスターにデプロイします。
ServerはNginxコンテナを起動します。
Client側では、NginxコンテナにService Connect経由で疎通確認を行う処理を入れています。
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { EcsServiceConnectStack } from '../lib/ecs-service-connect-stack';
const app = new cdk.App();
new EcsServiceConnectStack(app, 'EcsServiceConnectStack', {});
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class EcsServiceConnectStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC作成
const vpc = new ec2.Vpc(this, 'ServiceConnectVpc', {
maxAzs: 2,
natGateways: 1,
});
// Service Connect用のCloud Mapネームスペース作成
const namespace = new servicediscovery.PrivateDnsNamespace(this, 'ServiceConnectNamespace', {
name: 'service-connect-test',
vpc
});
// Server用のECSクラスター
const serverCluster = new ecs.Cluster(this, 'ServerCluster', {
vpc,
clusterName: 'server',
});
// Client用のECSクラスター
const clientCluster = new ecs.Cluster(this, 'ClientCluster', {
vpc,
clusterName: 'client',
});
// サーバータスク定義
const serverTaskDefinition = new ecs.FargateTaskDefinition(this, 'ServerTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
});
const serverContainer = serverTaskDefinition.addContainer('ServerContainer', {
image: ecs.ContainerImage.fromRegistry('nginx:latest'),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'server',
logGroup: new logs.LogGroup(this, 'ServerLogGroup', {
logGroupName: '/aws/ecs/server-service-connect',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
}),
});
serverContainer.addPortMappings({
containerPort: 80,
protocol: ecs.Protocol.TCP,
name: 'web',
});
// クライアントタスク定義
const clientTaskDefinition = new ecs.FargateTaskDefinition(this, 'ClientTaskDef', {
memoryLimitMiB: 512,
cpu: 256,
});
const clientContainer = clientTaskDefinition.addContainer('ClientContainer', {
image: ecs.ContainerImage.fromRegistry('curlimages/curl:latest'),
command: ['sh', '-c', 'while true; do curl -s http://server:80 && sleep 30; done'],
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'client',
logGroup: new logs.LogGroup(this, 'ClientLogGroup', {
logGroupName: '/aws/ecs/client-service-connect',
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
}),
});
// Server用のSecurity Group
const serverSecurityGroup = new ec2.SecurityGroup(this, 'ServerSecurityGroup', {
vpc,
description: 'Security group for Server service',
allowAllOutbound: true,
});
// Client用のSecurity Group
const clientSecurityGroup = new ec2.SecurityGroup(this, 'ClientSecurityGroup', {
vpc,
description: 'Security group for Client service',
allowAllOutbound: true,
});
// Server Security GroupでClientからのアクセスを許可
serverSecurityGroup.addIngressRule(
clientSecurityGroup,
ec2.Port.tcp(80),
'Allow HTTP access from Client'
);
// サーバーサービス(Service Connect有効)
const serverService = new ecs.FargateService(this, 'ServerService', {
cluster: serverCluster,
serviceName: 'server',
taskDefinition: serverTaskDefinition,
desiredCount: 1,
enableExecuteCommand: true,
securityGroups: [serverSecurityGroup],
serviceConnectConfiguration: {
namespace: namespace.namespaceArn,
services: [
{
portMappingName: 'web',
discoveryName: 'server',
dnsName: 'server',
port: 80,
},
],
},
});
// クライアントサービス(Service Connect有効、クライアントとして設定)
const clientService = new ecs.FargateService(this, 'ClientService', {
cluster: clientCluster,
serviceName: 'client',
taskDefinition: clientTaskDefinition,
desiredCount: 1,
enableExecuteCommand: true,
securityGroups: [clientSecurityGroup],
serviceConnectConfiguration: {
namespace: namespace.namespaceArn,
// 空の配列でクライアント専用モードに設定
services: [],
},
});
// サーバーサービスデプロイ後にクライアントサービスを起動
clientService.node.addDependency(serverService);
// ECS Execのための権限をタスクロールに追加
const ecsExecPolicy = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ssmmessages:CreateControlChannel',
'ssmmessages:CreateDataChannel',
'ssmmessages:OpenControlChannel',
'ssmmessages:OpenDataChannel',
],
resources: ['*'],
});
serverTaskDefinition.taskRole.addToPrincipalPolicy(ecsExecPolicy);
clientTaskDefinition.taskRole.addToPrincipalPolicy(ecsExecPolicy);
}
}
AWS認証情報をコンソールにセットして、cdk deploy
を実行します。
npx cdk deploy
動作確認
CDKデプロイ完了後に動作確認を行います。
ECS Service client
のログを確認します。
以下のようにcurlのレスポンスとして、NginxのWelcomeページが返ってきていることが分かります。
ECS Execでclient
コンテナに入って確認してみます。
# タスクIDを取得
CLIENT_TASK_ID=$(aws ecs list-tasks --cluster client --query 'taskArns[0]' --output text)
# Consumerコンテナに接続
aws ecs execute-command \
--cluster consumer \
--task $CLIENT_TASK_ID \
--container ClientContainer \
--interactive \
--command "/bin/sh"
/etc/hosts
にproducerサービスのエントリがあることを確認できます。
cat /etc/hosts
127.0.0.1 localhost
10.0.196.20 ip-10-0-196-20.ap-northeast-1.compute.internal
127.255.0.1 server
2600:f0f0:0:0:0:0:0:1 server
server
に対してcurlしてレスポンスが返ってくることも確認できました。
curl server
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>