【编者的话】本文 转帖 自 Segment 公司CTO以及联合创始人 Calvin French-Owen 发表的文章。Segment公司采用独立的AWS账户进行真正意义上的隔离,采用Docker和ECS运行服务,并采用Terraform配置脚本进行整合并为服务描述添加Datadog供应商获得免费的监控告警信息。
在Segment早期,我们的基础设施是拼装的,我们通过AWS的用户界面提供实例,有一个由从未使用的AMI组成的组件,以及通过三种不同方式实现的配置。
随着公司业务开始腾飞,我们的工程师团队开始扩充,架构也开始日益复杂,但是生产环境的相关工作仍然局限于我们少数这些知道神秘陷阱的人。我们一直在逐步改善过程,但我们需要给我们的基础设施更深层次的改革以保持快速发展。
因此,几个月前,我们坐下来问自己:“ 如果我们今天设计,那么基础设施设置会是什么样子? ”
在10周的过程后,我们完全重新设计了基础设施,我们撤下了几乎每一个实例和旧的配置,将我们的服务移到Docker容器中运行,并且切换为使用全新的AWS账号。
我们花了很多时间考虑如何使生产环境设置变得可审计、简单并且易用,同时仍然不失可扩展的弹性。
下面就是我们的解决方案。
独立的AWS账户
我们切换到了完全独立的AWS账户,而不是使用Region或者Tag来分离不同的预生产环境和生产环境实例,我们需要确保提供的脚本不会影响当前运行的服务,并且使用全新的账户意味着我们将从一张白纸开始。

运维账户提供了跳转和集中登录服务,团队中的每个人都有一个AWS IAM账户。
其它环境拥有一系列IAM角色来进行切换,这意味着只能有一个登录点来管理账户,也只有一个地方限制访问。
举个例子来说,Alice也许可以访问上图中的所有三个环境,Bob只能访问开发环境(哪怕他删除了生产环境中的负载均衡器),但是他们都是通过同一个运维账户登录的。
作为对于复杂的IAM设置限制访问的替代,我们只是简单地通过环境来锁定用户并且通过角色来分组,从界面上使用每一个账户就像切换当前的活跃角色一样方便。

我们可以免费获得真正意义上的隔离,无需额外配置,而不是担心预生产环境沙盒会不安全或者会改动生产环境数据库。
分享配置代码带来的额外好处就是我们的预生产环境事实上将会成为生产环境的一个镜像,配置中仅有的区别只有实例大小和日期数目。
最后,我们也可以跨账户合并账单。我们使用相同的发票来支付月付账单,可以看到一个按照环境来分割的详细分解后的费用。
Docker和ECS
一旦我们设置好了账户,接下来就是轮到如何设计服务真正运行了,为此,我们转向了 Docker 和 EC2容器服务(ECS) 。
截止今天为止,我们大部分的服务都运行在Docker容器里,包括我们的API和数据流管道。容器每秒钟都会接收数千次请求,每月处理500亿条事件。
Docker的最大好处就是可以一定程度上授权团队从无到有构建服务,我们不再有一组复杂的配置脚本或AMIs——我们只需交给生产集群一个镜像然后运行即可。不会再有有状态的实例了,我们可以保证预生产环境和生产环境运行的是一模一样的代码。
配置完我们的服务如何运行在容器里后,我们选择了ECS作为调度器。
在一个较高的水平,ECS负责在生产环境中实际运行我们的容器。ECS关注服务的调度,即将服务放置在单独的主机上运行,并且在当连接到ELB时确保零宕机重载服务。ECS甚至可以通过AZs调度提供更好的可用性,如果某个容器挂了,ECS会确保在集群中重新调度一个新的实例。
切换到ECS极大地简化了运行服务的工作,从而不需要担心Upstart工作或者配置实例。添加 Dockerfile 、设置任务定义以及将其和集群关联都是非常容易的。
在我们的设置中,Docker镜像通过CI(持续集成)来构建,然后再推送到Docker Hub。当一个服务启动时,会从Dokcer Hub上拉取镜像,接着ECS就可以跨机器调度了。

我们将服务集群按照它们的关注领域和负载profile(比如针对API、CDN、APP等的不同的集群)分组,拥有分离的集群意味着更好的可见性以及针对每一个集群都可以决定如何使用不同的实例类型(因为ECS没有实例关联的概念)。
每一个服务都有一个特定的任务定义,指出了运行在哪一个版本的容器里、运行多少实例以及选择哪一个集群。
在运行过程中,服务通过ELB注册它自身,使用健康检查来确认容器是否准备好运行。我们在ELB中指向一个本地的Route53条目,这样服务借助于DNS可以互相通信和简单地引用。

设置非常的棒,因为我们不需要任何服务发现,本地的DNS就完成了所有的记账工作。
ECS可以运行所有的服务,我们从ELB获得了免费的CloudWatch监控指标,这比起在启动阶段就不得不通过一个中央认证授权中心注册服务要简单多了,并且最大的好处还是在于我们不需要亲自处理服务状态冲突了。
Terraform模板化
Docker和ECS描述了如何运行我们的每一个服务,而Terraform就像胶水一样把它们整合在了一起。在高级别上,它是一组配置脚本,可以创建和更新我们的基础设施。你可以认为它像一个建造中的CloudFormation版本,但不会让你想要戳你的眼睛。
Terraform不是运行一组服务来维护状态,而是只通过一组脚本来描述集群,配置脚本在本地运行(将来也是这样,借助于持续集成)并且提交给Git,所以我们拥有了关于生产环境基础设施实际运行的持续记录。
这里就是一个我们Terraform模块设置Bastion节点的样本,该样本创建了所有的安全组、实例和AMIs,这样我们就可以很容易地为将来的环境设置跳跃点。
// Use the Ubuntu AMI
module "ami" {
source = "github.com/terraform-community-modules/tf_aws_ubuntu_ami/ebs"
region = "us-west-2"
distribution = "trusty"
instance_type = "${var.instance_type}"
}
// Set up a security group to the bastion
resource "aws_security_group" "bastion" {
name = "bastion"
description = "Allows ssh from the world"
vpc_id = "${var.vpc_id}"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "bastion"
}
}
// Add our instance description
resource "aws_instance" "bastion" {
ami = "${module.ami.ami_id}"
source_dest_check = false
instance_type = "${var.instance_type}"
subnet_id = "${var.subnet_id}"
key_name = "${var.key_name}"
security_groups = ["${aws_security_group.bastion.id}"]
tags {
Name = "bastion-01"
Environment = "${var.environment}"
}
}
// Setup our elastic ip
resource "aws_eip" "bastion" {
instance = "${aws_instance.bastion.id}"
vpc = true
}
我们在预生产环境和生产环境中都使用相同的模块来设置我们各自的Bastion节点,我们唯一需要换掉的是IAM keys,我们准备好了。
改变这些也丝毫没有痛苦,不需要拆了已有的整个基础设施,Terraform只在需要更新的地方更新。
当我们需要修改ELB Draining超时60秒时,只需要在Terraform apply后跟随一个简单的find/replace操作,这样两分钟后我们就会拥有一个对于我们所有的ELB都是完全调整了的生产环境设置。
Terraform是可复制的、可审计的并且自注释的,没有任何黑箱。
我们将所有的配置都放在一个中央的基础设施Repo,其可以非常容易发现一个服务是如何设置的。
我们还没有完全拿到圣杯。我们想要转换更多的Terraform配置来利用模块的优势,这样可以合并单个文件,减少共享样本的数量。
一路上我们发现了一些关于.tfstate的陷阱,Terraform总是一开始读取现有的基础设施,然后当状态变得不同步时就会抱怨,我们通过将.tfstate提交到Repo的方式终止了这种情况,然后在有任何改变之后再推送回去,但是我们正在调研 Atlas ,或者通过持续集成来解决这个问题。
移动到Datadog
通过这一点,我们有了我们的基础设施,我们的供应和我们的隔离。剩下的最后一件事是度量和监控,用以追踪生产环境中运行的一切东西。
在我们的新的环境中,我们已经将所有的度量和监控切换到了Datadog上,这是非常奇妙的。

我们一直非常满意Datadog的界面、API以及其与AWS的完全集成,但从工具中获得最多的还是来自于一些关键的设置。
我们做的第一件事是将AWS与Cloudtrail集成,这个给出了一个居高临下鸟瞰的视角来观察我们的每一个环境如何演化的,因为我们要与ECS集成,Datadog feed每次都会在任务定义更新的时候更新,所以我们最终得到了免费部署的通知。寻找Feed是出奇的快,而且很容易追溯到上一次的服务部署或重新调度。
接下来,我们确保添加Datadog-agent作为基础AMI的容器( Datadog/Docker-dd-agent ),它不仅可以从主机(CPU、内存等)收集度量指标,还可以作为Statsd指标库。每个服务收集自定义的基于查询、延迟和错误的度量指标,这样我们可以在Datadog里探查指标和发送告警。我们的Go工具箱(很快就会开源)自动收集Ticker的pprof输出并且发送,所以我们可以监控内存和协程(Goroutines)。
更酷的是,Datadog-agent可以在环境中跨主机将实例利用率可视化,所以我们可以得到一个高层次的实例或集群概述,也许会有下面的一些议题:

另外,我的队友Vince创建了“ Terraform provider for Datadog ”,所以我们完全可以针对生产配置编写告警脚本。我们的告警将会记录下来并且与生产环境运行的系统保持同步。
resource "datadog_monitor_metric" "app.internal_errors" {
name = "App Internal Errors"
message = "App Internal Error Alerts"
metric = "app.5xx"
time_aggr = "avg"
time_window = "last_5m"
space_aggr = "avg"
operator = ">"
warning {
threshold = 10
notify = "@slack-team-infra"
}
critical {
threshold = 50
notify = "@slack-team-infra @pagerduty"
}
}
按照惯例,我们指定了两个告警级别:Waring和Critical。Waring级别可以让任何在线的用户知道有什么东西看起来可疑,并针对任何潜在的问题提前做好预案。Critical级别的告警被保留为“唤醒你在深夜的问题”亦即一个严重的系统故障。
更重要的是,一旦我们过渡到Terraform模块并为我们的服务描述添加Datadog供应商,接着所有的服务最终都会免费获得告警信息,数据将直接由我们的内部工具箱和Cloudwatch度量指标来驱动处理。
让Docker运行的美好时光
一旦我们有了上述所有的这些组件,切换的这一天终于来临了。
我们首先在新的生产环境和遗留环境之间设置一个 VPC的对等连接 ——允许我们在它们之间集群化数据库和复制。
接下来我们在新的生产环境中预热ELBs,确保它们可以处理负载。亚马逊没有提供ELBs的自动调整排列功能,所以我们不得不咨询亚马逊来提前准备(或者缓慢扩展自身)来处理增加的负载。
从那里开始,这只是一个使用加权Route53路由从老环境到新环境稳步增加流量的问题,持续监控,一切都看起来不错。
今天,我们的API像蜂群一样辛勤工作,每秒处理成千上万的请求,并且完全运行在Docker容器里。
但是我们还没有完成,我们仍然在微调我们的服务创建方式,并减少样板,这样团队里的任何人都可以非常容易地构建具有适当的监控和告警功能的服务,并且我们想改进与容器工作相关的工具,因为服务不再与实例关联了。
我们还计划为这个项目留意有前途的技术。 Convox 团队正在围绕AWS基础设施构建很棒的工具。虽然我们喜欢ECS的简单以及集成特性,但是 Kubernetes 、 Mesosphere 、 Nomad 和 Fleet 看起来也都是非常酷的资源调度系统,看看它们都是如何打开市场局面的将是令人兴奋的,我们会一直跟踪它们,看看有什么可以采用借鉴的。
在所有这些业务流程的变化后,我们比以往更强烈的认为应将我们的基础设施外包给AWS。他们已经通过将大量核心服务产品化改变了游戏规则,同时保持一个非常有竞争力的价格点。这一点带来了一种新的类型的创业公司,它们可以高效地构建产品,廉价同时花费更少的时间进行维护,我们看好在AWS的生态系统上构建工具。
原文链接: Segment: Rebuilding Our Infrastructure With Docker, ECS, And Terraform (翻译:胡震)
=============================================================================================
译者介绍: 胡震, 曾任互联网金融创业公司首席架构师&CTO,现在平安金融科技中心架构组负责技术管理和架构设计工作