我正在尝试在 ECS 中构建自己的服务发现版本,因为我希望扩展和缩减的服务不是 HTTP 服务器,因此无法由 ELB 管理。此外,ECS 尚不支持 docker 的用户定义网络功能这将是进行服务发现的另一种方式。正如该问题讨论中提到的:
目前,服务发现是一项巨大的难题,需要另一项服务(本身通常基于集群,并自行发现然后侦听其他服务)。这是一个混乱的解决方案,更不用说 Lambda “解决方案” 的实现和维护更加令人讨厌。
因此,我选择讨厌的 Lambda “解决方案”路线,而不是其他选项。构建此 hack 服务发现所需的主要内容是在我的 EC2 主机上运行的每个 docker 容器的 IP 地址。
通过 SSH 连接到充当我的 ECS 容器实例之一的 EC2 服务器,我可以运行docker ps
以获取每个正在运行的 Docker 容器的容器 ID。对于任何给定的容器 ID,我都可以运行,docker inspect ${containerId}
它返回 JSON,其中包括有关该容器的许多详细信息,特别是NetworkSettings.IPAddress
与该容器的绑定(这是我的发现实现所需的主要内容)。
我正在尝试使用 Lambda 中的 AWS SDK 来获取此值。这是我到目前为止的 Lambda 函数(您也可以运行它 - 此处与我的设置无关):
exports.handler = (event, context, callback) => {
var AWS = require('aws-sdk'),
ecs = new AWS.ECS({"apiVersion": '2014-11-13'});
ecs.listClusters({}, (err, data) => {
data.clusterArns.map((clusterArn) => {
ecs.listTasks({
cluster: clusterArn
}, (err, data) => {
ecs.describeTasks({
cluster: clusterArn,
tasks: data.taskArns
}, (err, data) => {
if (err) console.log(err, err.stack);
else console.log(JSON.stringify(data, null, 4));
})
});
})
})
};
调用的输出describeTasks
非常无用。它没有调用docker inspect
产生的那么多详细信息,特别是它不包括运行任务的docker容器的IP地址。
我也尝试通过调用来查找我需要的数据describeContainerInstances
,但正如预期的那样,它没有返回任何特定于任务的详细信息。
docker inspect
如果有办法从 Lambda 直接在 EC2 主机上运行,我愿意尝试。我不确定是否可以通过 SDK 在容器上运行命令;可能不行。因此,我必须构建一个在特制版本的 ECS 容器映像上运行的自定义服务,这听起来很糟糕。
我如何使用 AWS SDK 获取这些容器 IP 地址?或者关于如何解决 ECS 中服务发现的一般问题的一些更好的想法?
答案1
事实证明,我最初的前提(需要知道任务容器自己的内部 IP 地址才能进行服务发现)是非常有缺陷的 - 该 IP 地址只能在单个 EC2 容器实例中使用。如果您有多个容器实例(您可能应该有),那么这些任务容器 IP 基本上是无用的。
我想到的替代解决方案是遵循针对运行 HTTP / HTTPS 的应用程序负载均衡器建议的模式 - 有一个端口映射,以 0 作为主机端口,指向我需要使用的 Docker 实例内的端口。通过这样做,Docker 将分配一个随机主机端口,然后我可以使用 AWS SDK 找到该端口 - 特别是 ECS 模块上可用的“describeTasks”函数。详情请参阅此处:http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECS.html#describeTasks-property
这是我自己开发服务发现机制的基本基础——要完整地实现这一点,还需要很多其他细节。我使用 Lambda 函数调用 AWS SDK 以及 PostgreSQL 数据库,以使我的主机容器列表保持最新(有点像动态 DNS 注册表)。技巧的一部分是您需要知道每个容器的 IP 和端口,但 describeTasks 只返回端口。这是我编写的一个方便的 NodeJS 函数,它接受容器名称并查找集群中该名称容器的所有 IP 地址和端口:
var Q = require('q');
/**
* @param {String} - cluster - name of the cluster to query, e.g. "sqlfiddle3"
* @param {String} - containerType - name of the container to search for within the cluster
* @returns {Promise} - promise resolved with a list of ip/port combinations found for this container name, like so:
[
{
"connection_meta": "{\"type\":\"ecs\",\"taskArn\":\"arn:aws:ecs:u..\"}",
"port": 32769,
"ip": "10.0.1.49"
}
]
*
*/
exports.getAllHostsForContainerType = (cluster, containerType) => {
var AWS = require('aws-sdk'),
ecs = new AWS.ECS({"apiVersion": '2014-11-13'}),
ec2 = new AWS.EC2({"apiVersion": '2016-11-15'});
return ecs.listTasks({ cluster }).promise()
.then((taskList) => ecs.describeTasks({ cluster, tasks: taskList.taskArns }).promise())
.then((taskDetails) => {
var containersForName = taskDetails.tasks
.filter((taskDetail) =>
taskDetail.containers.filter(
(container) => container.name === containerType
).length > 0
)
.map((taskDetail) =>
taskDetail.containers.map((container) => {
container.containerInstanceArn = taskDetail.containerInstanceArn;
return container;
})
)
.reduce((final, containers) =>
final.concat(containers)
, []);
return containersForName.length ? (ecs.describeContainerInstances({ cluster,
containerInstances: containersForName.map(
(containerDetails) => containerDetails.containerInstanceArn
)
}).promise()
.then((containerInstanceList) => {
containersForName.forEach((containerDetails) => {
containerDetails.containerInstanceDetails = containerInstanceList.containerInstances.filter((instance) =>
instance.containerInstanceArn === containerDetails.containerInstanceArn
)[0];
});
return ec2.describeInstances({
InstanceIds: containerInstanceList.containerInstances.map((instance) =>
instance.ec2InstanceId
)
}).promise();
})
.then((instanceDetails) => {
var instanceList = instanceDetails.Reservations.reduce(
(final, res) => final.concat(res.Instances), []
);
containersForName.forEach((containerDetails) => {
if (containerDetails.containerInstanceDetails) {
containerDetails.containerInstanceDetails.ec2Instance = instanceList.filter(
(instance) => instance.InstanceId === containerDetails.containerInstanceDetails.ec2InstanceId
)[0];
}
});
return containersForName;
})) : [];
})
.then(
(containersForName) => containersForName.map(
(container) => ({
connection_meta: JSON.stringify({
type: "ecs",
taskArn: container.taskArn
}),
// assumes that this container has exactly one network binding
port: container.networkBindings[0].hostPort,
ip: container.containerInstanceDetails.ec2Instance.PrivateIpAddress
})
)
);
};
请注意,这使用了“Q”承诺库 - 您需要在 package.json 中将其声明为依赖项。
使用 Lambda 函数处理 ECS 服务发现的其余自定义解决方案可在此处找到:https://github.com/jakefeasel/sqlfiddle3#setting-up-in-amazon-web-services
答案2
即使您的服务不是 HTTP,您也可以将 Classic Elastic Load Balancer 与 ECS 服务关联。确保在 ELB 上创建 TCP 侦听器(不是 HTTP 或 HTTPs/SSL)并指向容器的公开端口。使用 Classic ELB 与应用程序 ELB 的缺点是您必须为每个 ECS 服务设置一个单独的 ELB(额外费用)。
http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-listener-config.html