大多数DevOps工程师失败的原因并非在于他们缺乏相关工具的知识,而是因为在将系统投入生产环境之前,没有人告诉他们哪些事情是绝对不能做的。
在创业公司中,这种情况更为严重。由于存在快速交付产品的压力、团队规模较小,同时又没有资深工程师来审核你的决策,因此错误往往会悄然发生,直到最终导致系统故障、数据丢失或安全问题,从而让企业付出数千美元的代价,并耗费数周的时间来恢复秩序。
本文详细分析了创业公司中工程师在职业生涯早期常会犯下的十种最为代价高昂的DevOps错误。对于每一种错误,都会结合实际案例说明其带来的业务影响,以及你可以立即采取的具体补救措施。
无论你是正在搭建第一个生产环境,还是在审核现有的生产环境,这份指南都能帮助你构建出可靠、安全且符合企业实际需求的系统。
目录
本文适合谁阅读
-
处于职业生涯早期、在初创企业中负责构建或维护生产基础设施的DevOps工程师和云技术专家。
-
最近开始承担DevOps职责的后端开发人员。
-
加入初创企业的工程师们,他们希望了解在这样一个发展迅速的环境中,运营规范究竟是怎样的。
阅读本文时,您并不需要成为任何特定工具的专家。本文的重点在于决策流程与运营规范,而非工具配置。
为什么初创企业是一个特殊的环境
在探讨这些错误之前,我们首先需要了解为什么初创企业会犯这些错误。
在大型企业中,通常会有专门的安全工程师、SRE团队、平台开发团队,以及多个审核人员来处理每一项基础设施变更。而在初创企业中,很可能只有一名工程师需要同时承担所有这些工作。
这就导致了四个主要的压力点:
-
时间压力。业务方迫切需要新的功能尽快上线,因此运营规范往往被忽视,因为没有人会严格监督这些流程的执行。
-
预算限制。
每一项基础设施决策都会直接影响企业的发展速度。工程师们往往会选择成本最低的方案,而不是最可靠的方案。
-
缺乏监管机制。
没有资深工程师来审核你的Terraform配置文件,也没有在发布前进行安全审计。由于不会立即产生负面后果,一些错误的决策反而会被认为是正确的。
-
需求不断变化。
你今天设计的架构可能六个月后就需要用来支持完全不同的产品。虽然这些压力并不能成为犯错的借口,但了解它们有助于我们理解为什么某些错误会反复发生。
错误1:在不了解所要部署的内容的情况下进行部署
场景描述
一名初级工程师被要求将公司的Node.js API部署到AWS上。他们找到了Elastic Beanstalk的相关教程并按照指示操作,结果一切顺利。两周后,访问量增加了,他们试图用“教程中提到的方法”进行扩展,结果应用程序却崩溃了。由于他们从未真正理解这次部署的具体操作内容,因此无法找出问题所在。
对业务的影响
当生产环境出现故障,而负责部署该系统的人员无法解释其工作原理时,诊断过程往往需要数小时甚至更长时间。故障持续的时间越长,客户对系统的信任度就会下降,团队士气也会受影响,同时还会直接导致收入损失。
解决方法
在将任何系统部署到生产环境之前,你必须能够书面回答以下五个问题:
-
我的代码是由哪种计算类型来运行的?(EC2、Lambda、Fargate还是容器?)
-
新版本是如何替换旧版本的?(采用滚动更新方式吗?蓝绿部署方案吗?还是一次性全部替换吗?)
-
配置信息和敏感数据是从哪里获取的?(是通过SSM管理的吗?还是通过Secrets Manager?又或者是从环境配置文件中读取的?)
-
有哪些下游服务依赖于这个系统?(比如数据库连接、其他API接口,或者缓存系统吗?)
-
如果系统出现故障,我能在五分钟内恢复其正常运行状态吗?
如果你无法回答所有这些问题,那就不要急于进行部署。那些帮助你成功将系统部署到生产环境的教程,并不能说明该系统的实际运作方式。
“在部署一个系统之前花两小时去了解它,总比在系统出现故障后花两天时间来调试它要好得多。”
就我个人而言,在学习新技术、新工具,或者尝试实现以前从未使用过的功能时,我通常会重点关注三个核心问题:是什么、为什么以及如何实现。
-
第一个问题是:这项技术或概念到底是用来解决什么问题的?
通过深入研究、阅读官方文档、理解其核心原理,有时甚至了解该技术或工具的发展历史,我能够为自己打下坚实的基础。我认为,在实际应用之前对相关知识有透彻的了解是非常重要的。 -
第二个问题是:我们为什么需要这项技术?
我会努力弄清楚这项技术能带来什么价值,为什么要使用它,它能解决哪些问题,以及它会对团队或组织产生什么样的积极影响。这样,我才能做出明智的技术决策,而不会盲目地使用某些工具而不了解它们的用途。 -
第三个问题是:应该如何实施这项技术?
通常来说,解决某个问题或实现某项技术有多种方法可供选择。因此,我会根据具体的应用场景和预期结果,来挑选最合适、最实用的方法来进行实施。
这种有条理的学习方法帮助我快速掌握了新技术,能够迅速适应新的环境,并在实际工作中有效地运用这些技术来解决各种问题。
错误2:将生产环境作为开发环境使用
具体案例
为了节省时间,一名工程师直接在 production 环境的 AWS 账户中测试新的部署脚本。结果他们不小心执行了一条命令,导致生产环境中的数据库实例被终止了。虽然系统有自动备份功能,但由于配置错误,六小时内的客户数据已经无法恢复。
这种情况发生的频率比你想象的要高得多。人们总是用同样的理由来为自己辩解:“只需要一分钟而已。”
对企业造成的影响
哪怕只发生一次在生产环境中进行的测试,都可能导致数据丢失、系统数小时无法正常运行,进而引发客户沟通危机。对于一家初创企业来说,这种情况甚至可能永久性地损害其声誉,而该公司本来还没有机会建立起良好的声誉。
解决办法
至少需要三个独立的开发环境,理想情况下还应使用三个不同的AWS账户:
| 环境类型 | 用途 | 访问权限等级 |
|---|---|---|
| 开发环境 | 可以自由进行各种测试,其中不包含真实数据。 | 工程师拥有广泛的访问权限。 |
| 测试环境 | 与生产环境完全相同,用于最终验证功能。 | 访问权限受到严格控制。 |
| 生产环境 | 用于服务真实客户,其中包含真实数据。 | 需要使用多因素认证,且不允许手动进行部署操作。 |
使用不同的AWS账户(而不仅仅是不同的VPC)能够实现账户级别的隔离。如果开发账户中的权限设置出现问题,也不会意外地影响到生产环境的基础设施。
通过“基础设施即代码”技术(如Terraform或CloudFormation),你可以一次编写配置文件,然后根据不同的变量文件将其应用于三个不同的环境中。
# terraform/environments/prod/main.tf
module "app" {
source = "..modules/app"
environment = "production"
instance_type = "t3.medium"
db_instance_class = "db.t3.medium"
multi_az = true
}
# terraform/environments/staging/main.tf
module "app" {
source = "..modules/app"
environment = "staging"
instance_type = "t3.small"
db_instance_class = "db.t3.small"
multi_az = false
}
这两个模块的代码本身是相同的,只是其中用于区分不同环境的变量有所不同。拥有独立的开发环境并不是什么奢侈的要求,而是任何负责开发实际软件的团队都必须遵守的最基本操作规范。
错误3:将敏感信息硬编码在代码中
问题发生的情景
有一名新工程师加入了一家初创企业,他克隆了该企业的代码仓库。在仓库中,他发现了一个被提交到Git中的`.env`文件,其中包含了生产环境的数据库密码、Stripe平台的密钥,以及具有管理员权限的AWS访问密钥。而这个代码仓库已经公开了六个月之久。
由于这些敏感信息被保存在`.env`文件中,而不是直接写在源代码中,因此GitHub的自动安全检测系统并没有发现这些问题。这些凭证在过去的六个月里一直有效,并且也被实际使用着。
对企业造成的影响
攻击者一旦将这些暴露出来的敏感信息放入他们的攻击工具中,几分钟内就能找到它们。而一个具有管理员权限的AWS访问密钥被泄露,可能会带来以下严重后果:
-
加密挖矿任务会在一夜之间导致数千美元的云服务费用产生。
-
攻击者会彻底窃取所有S3存储桶中的客户数据。
-
攻击者会创建新的管理员账户,使你无法使用自己的账号。
-
在调查进行期间,AWS账户可能会被暂停使用。
根据GitHub的年度安全报告》,每年都有数百万条敏感信息被公开在代码仓库中。平均来说,人们需要197天才能发现这些已被泄露的云服务凭证。
解决方案
步骤1:永远不要将任何敏感信息提交到Git仓库中。无论是临时保存,还是放在分支或私有仓库中都不行。
步骤2:在创建第一个文件之前,先添加.gitignore文件。确保在任何.env文件出现之前,.gitignore文件就已经存在了,并且其中的第一行内容应该是用于排除敏感文件的规则。
# .gitignore
.env
.env.*
*.pem
*.key
secrets/
步骤3:对于所有生产环境中使用的敏感信息,应使用AWS Secrets Manager或SSM Parameter Store来存储。应用程序应在运行时读取这些敏感信息:
# Python示例——在运行时获取敏感信息,切勿在构建阶段进行读取
import boto3
import json
def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
client = boto3.client("secretsmanager", region_name=region)
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# 使用方法
db_config = get_secret("prod/myapp/database")
DATABASE_URL = db_config["connection_string"]
步骤4:立即扫描你现有的代码仓库。很可能你已经遇到了问题:
# 安装trufflehog工具,用于扫描代码仓库历史记录中是否存在泄露的敏感信息
pip install trufflehog
# 扫描整个代码仓库的提交历史记录
trufflehog git file://.
# 或者扫描远程的GitHub仓库
trufflehog github --repo https://github.com/your-org/your-repo
步骤5:添加一个预提交钩子,以防止未来再次发生类似问题。
pip install pre-commit
# .pre-commit-config.yaml
repos:
- repo: https://github.com/awslabs/git-secrets
rev: master
hooks:
- id: git-secrets
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
pre-commit install
# 现在,这个钩子会在每次提交之前被执行,从而阻止敏感信息的泄露
一旦数据库密码被公开泄露,就再也无法恢复原来的安全状态了。采取预防措施只需要10分钟的时间,而处理由此引发的问题却可能需要数周的时间。
错误4:为尚未出现的问题过度设计解决方案
情景描述
一家仅有五名员工的初创企业,拥有200名用户,他们决定在Kubernetes上构建微服务架构,理由仅仅是“Netflix也在使用它”。他们花了三个月时间来配置Kubernetes、Istio服务网格、ArgoCD、Vault、Prometheus以及Grafana等工具。然而,在这三个月里,他们的产品并没有新增任何功能;而同期的竞争对手,虽然只使用了一个EC2实例来运行单体应用,却推出了十二项新功能。
对企业业务的影响
每增加一层基础设施,就意味着会增加出可能出现故障的环节、需要专业技能来维护的层面,同时也会使后续的所有变更变得更加繁琐。对于那些具备相应规模和团队实力的组织来说,Kubernetes确实是合适的选择;但对于仅有五名员工的初创企业而言,这无异于是一种浪费资源的行为。
过早地引入复杂的架构不仅会耗费大量的开发时间,还会让企业在早期阶段失去速度带来的竞争优势。
解决办法
要根据自身实际的发展阶段来选择合适的基础设施:
| 用户规模 | 适合的基础设施 | 成本范围 |
|---|---|---|
| 1–1,000名用户 | 单个EC2实例 + RDS + Nginx反向代理 | 每月20–50美元 |
| 1,000–50,000名用户 | 自动扩展组、RDS多区域部署、ALB、基础CI/CD流程 | 每月200–500美元 |
| 50,000–500,000名用户 | ECS Fargate、RDS读复制副本、ElastiCache、全面的可观测性工具 | 每月1,000–5,000美元 |
| 500,000名用户以上 | 多区域部署、托管型Kubernetes环境、专职的SRE团队 | 每月10,000美元以上 |
在做出任何基础设施相关的决策之前,都应该先问自己这样一个问题:“这一方案今天能解决哪些我当前现有架构无法解决的问题?这些问题的具体表现又是什么?”
Amazon、Netflix和Uber最初都不是从微服务架构开始的。他们都是先使用单体应用来开展业务,只有当这些单体应用真正成为发展的瓶颈时,才开始提取出其中的功能并构建微服务。但你不一样——你现在面临的是亟需解决的问题。
在条件允许的情况下,尽可能使用托管型服务:比如使用RDS代替自托管的Postgres,使用Fargate代替自行管理的Kubernetes,使用ElastiCache代替自托管的Redis。这样,你的团队就可以把精力集中在产品开发上,而无需花费太多时间去维护基础设施。
错误5:在产品发布前缺乏可观测性
情景描述
某家初创企业的结账流程在周五晚上出现了故障,用户们纷纷放弃了他们的购物车,导致公司收入大幅下降。直到45分钟后,一位DevOps工程师才发现了这个问题——因为有一位客户在Twitter上直接向CEO发了消息。
这位工程师没有任何监控仪表板,也没有日志聚合工具或警报系统。他只能通过SSH登录到生产服务器,然后逐条查看原始日志文件。两个小时后,他终于找到了问题所在:当天早上进行的部署中引入了一个内存泄漏问题,导致数据库连接池被耗尽。
业务影响
如果没有可观测性:
-
你只能通过用户的反馈来发现生产过程中出现的问题,而无法从系统中获得这些信息。
-
由于诊断过程带有猜测性质,解决问题所需的时间会延长10倍。
-
你无法判断某次部署是提升了性能还是降低了性能。
-
你没有相应的数据来帮助自己做出更合理的架构决策。
解决办法
在任何服务投入生产之前,都必须实施这四种关键指标监测机制。这些指标来源于谷歌的站点可靠性工程手册:
-
延迟:请求完成所需的时间(第50、95、99百分位值)。
-
流量:系统每秒处理的请求数量。
-
错误率:失败请求的比例(每分钟5xx错误的响应次数)。
-
资源利用率:系统各项资源的使用程度(如CPU、内存、连接池的占用情况)。
以下是使用AWS CLI配置的最基本CloudWatch警报示例:
# 当错误率连续5分钟超过1%时触发警报
aws cloudwatch put-metric-alarm \
--alarm-name "high-error-rate-production" \
--alarm-description "错误率连续5分钟超过1%" \
--metric-name "5XXError" \
--namespace "AWS/ApplicationELB" \
--statistic "Average" \
--period 60 \
--evaluation-periods 5 \
--threshold 0.01 \
--comparison-operator "GreaterThanOrEqualToThreshold" \
--alarm-actions "arn:aws:sns:us-east-1:123456789:pagerduty-production" \
--dimensions Name=LoadBalancer,Value=app/my-alb/1234567890abcdef
每个应用程序都应该提供一个/health端点,当系统运行正常时,该端点会返回200 OK响应。
# FastAPI示例
from fastapi import FastAPI
from sqlalchemy import text
app = FastAPI()
@app.get("/health")
async def health_check():
# 检查数据库连接是否正常
try:
db.execute(text("SELECT 1"))
db_status = "healthy"
except Exception:
db_status = "unhealthy"
return {
"status": "healthy" if db_status == "healthy" else "degraded",
"database": db_status,
"version": os.getenv("APP_VERSION", "unknown")
}
你的负载均衡器会检查这个端点,你的运行时间监控工具也会检查它,而且每次进行部署后都应重新检查这个端点的状态。
除非你有数据作为依据,否则不能声称某个系统正在正常运行。“没有人投诉”并不等同于“没有问题发生”。
错误6:将安全措施视为最后一步才处理的环节
情景描述
一家初创企业急于推出他们的最小可行产品,因此将安全审查安排在产品发布之后进行。六个月后,一位潜在的企业客户在签订合同之前要求进行安全审计,而审计结果揭示了……
-
默认情况下,S3桶是公开可访问的。
-
EC2实例中的22号端口被开放给
0.0.0.0/0地址。 -
某些IAM用户拥有针对整个团队的
AdministratorAccess权限。 -
数据库在静态存储时没有采用任何加密措施。
-
JWT密钥被硬编码在环境变量中,导致审计失败,从而失去了这笔每年价值120,000美元的企业业务机会。修复这个问题花费了四周的时间和大量技术资源。
业务影响
安全漏洞才是最昂贵的“技术债务”——与会逐渐降低性能的技术债务不同,安全漏洞会引发突然且灾难性的后果:数据泄露、勒索软件攻击、账户被盗用,以及面临监管罚款。对于一家初创企业来说,这些后果中的任何一种都可能导致其倒闭。
解决方案
在第一行生产代码正式发布之前,请务必实施以下六项安全措施:
1. 实施最小权限原则:每个IAM角色仅被授予其所需的最小权限:
在AWS环境中,最常见的安全错误之一就是随意为角色分配超出其实际需求的权限——要么是出于便利考虑(例如允许访问所有S3资源),要么是因为对服务实际需求缺乏了解。这种设置会带来不必要的风险:如果某个角色被黑客攻击,攻击者就会获得你授予它的所有权限。
解决这个问题的方法很简单:先弄清楚你的服务实际需要哪些权限,然后制定相应的策略来限制这些权限的范围。例如,如果你的应用程序只需要从特定的S3桶中上传或读取文件,那么策略就应该明确指定这一点:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-app-uploads/*"
}
]
}
注意,这里的Resource仅被限制为my-app-uploads/*,而不是所有S3桶。同时,列表中也只包含了GetObject和PutObject操作,而不包括DeleteObject或其他通用权限。这样一来,即使账户被攻击,攻击者也只能访问那个特定的桶,而其他资源仍然受到保护。
2>默认情况下禁止所有S3桶的公开访问:
AWS创建的S3桶在默认状态下是私有的,但可以通过桶级别、对象级别或桶策略来更改这一设置。配置不当的S3桶往往是数据泄露的最常见原因,而且这类错误几乎总是无心造成的。
最安全的做法是在账户层面启用“禁止公开访问”设置。这样,所有其他设置都会被这个规则覆盖,从而确保没有任何桶会被设置为公开可访问状态。
aws s3api put-public-access-block \
--bucket my-app-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true, IgnorePublicAcls=true, BlockPublicPolicy=true, RestrictPublicBuckets=true"
对于你创建的每一个桶,都应执行这一操作。更理想的做法是在AWS账户层面启用这一设置,这样默认情况下它就会自动应用于所有未来的桶。
3. 绝不要将SSH端口开放给互联网,而应使用AWS Systems Manager Session Manager:
目前,在成千上万的AWS实例中,都将端口22开放给0.0.0.0/0,这实际上为攻击者提供了可利用的入口。暴力破解工具会持续在互联网上扫描这些开放的SSH端口。即使使用了强密码,这种设置也是不必要的,因为AWS提供了更好的解决方案。
AWS Systems Manager Session Manager允许你在不打开安全组中任何入站端口的情况下,完全访问任何EC2实例。无需扫描任何端口,也无需攻击任何端口,而且所有会话都会被自动记录到CloudTrail中:
# 在不开放端口22的情况下,在EC2实例上启动会话
aws ssm start-session --target i-0123456789abcdef0
要使用Session Manager,EC2实例需要安装SSM Agent(Amazon Linux 2和Ubuntu 20.04及更高版本默认已安装),同时还需要一个关联了AmazonSSMManagedInstanceCore策略的IAM实例配置文件。完成这些设置后,你就可以完全关闭安全组中的端口22了。
4>为所有IAM用户启用多因素认证,并通过政策强制执行这一要求:
如果一个IAM用户的用户名和密码没有启用多因素认证,那么这个账户就会面临严重的安全风险。多因素认证是防止凭证被盗用的最有效手段,而且启用它并不会产生任何费用。
你可以通过IAM政策来强制实施这一要求:当没有启用多因素认证时,该政策应拒绝所有操作,除非这些操作是用于最初设置多因素认证所需的步骤。这样一来,即使攻击者窃取了用户的凭证,如果没有第二因素认证信息,他们也无法执行任何操作。
AWS文档提供了“不启用多因素认证时完全拒绝所有操作的政策模板”
,你可以将这个政策应用到账户中的所有IAM用户或组上。这种设置只需进行一次,就能永久提升你的账户安全等级。
5>在所有区域启用CloudTrail:
如果没有CloudTrail,你就无法了解谁在AWS账户中进行了哪些操作。如果凭证被盗用,你也无法查明攻击者访问了哪些资源;如果工程师不小心删除了某个资源,你也无法追踪到这一行为。在这种情况下,你的操作完全处于盲目状态。
CloudTrail会记录下每一次AWS API调用,包括调用者、IP地址、调用时间以及响应结果。因此,即使在你不经常使用的区域,其活动也会被记录下来:
aws cloudtrail create-trail \
--name production-audit-trail \
--s3-bucket-name my-cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation
选项--enable-log-file-validation会为每条日志生成一个摘要文件,这样你就可以确认这些日志没有被篡改。如果你需要在安全调查或合规性审计中使用这些日志,这一功能就非常重要了。一旦启用了这项功能,账户中的每一次AssumeRole、DeleteBucket或RunInstances操作都会被永久记录下来。
6. 从第一天起就开始使用AWS Security Hub:
大多数团队都是在发生安全漏洞或合规性审计之后,才会发现存在安全配置错误。而Security Hub则恰恰相反——它会持续扫描你的AWS环境,检查其是否符合行业标准框架(如CIS AWS Foundations Benchmark和AWS Foundational Security Best Practices),并在这些问题真正引发安全事故之前就将其暴露出来。
启用Security Hub只需要执行一条命令即可:
aws securityhub enable-security-hub
几分钟内,Security Hub就会为你生成一个合规性评估报告,并列出需要处理的优先级问题。例如,某个安全组可能将端口22开放给了外部网络;某个S3存储桶可能关闭了日志记录功能;或者有人最近使用了root账户进行操作。每个问题都会指出受影响的具体资源以及相应的修复方法。
对待Security Hub报告的每一个问题,都应该像处理生产环境中的故障一样:为这些问题分配优先级,指定负责人,并确保它们得到及时解决。如果某个问题在30天内仍未得到处理,那就意味着你选择让这个已知的安全漏洞继续存在。
错误7:在生产环境中进行手动部署
场景描述
某家初创公司的部署流程被记录在一份已经过时四个月的Notion文档中。该流程包括通过SSH登录服务器、执行`git pull`命令、运行`npm install`,然后重启PM2进程。不同的工程师在执行这些步骤时可能会有一些细微的差异。有一次,在深夜进行紧急发布时,有位工程师跳过了`npm install`这一步,结果导致应用程序因为缺少某个新依赖项而出现故障。
对企业造成的影响
手动部署流程本质上是不可靠的。在压力之下,人们往往会忽略某些步骤、以错误的顺序执行操作,或者对操作流程的记忆出现偏差。因此,在生产环境中进行的任何手动部署步骤,都相当于是在等待某个“关键时刻”才会发生的潜在问题。
解决方案
如果某个部署步骤被多次手动执行,那么就应该将其自动化。以下是一个针对ECS Fargate服务的简单但完整的GitHub Actions部署工作流程示例:
# .github/workflows/deploy.yml
name: 部署到生产环境
on:
push:
branches:
- main
permissions:
id-token: write # AWS的OIDC认证所需权限
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 通过OIDC配置AWS凭证
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- name: 登录Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: 构建并推送Docker镜像
id: build
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputsregistry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t \(ECRRegistry/my-app:\)IMAGE_TAG .
docker push \(ECR Registry/my-app:\)IMAGE_tag
echo "image=\(ECRRegistry/my-app:\)IMAGE_TAG" >>> $GITHUB_OUTPUT
- name: 部署到Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: task-definition.json
service: my-app-service
cluster: production
wait-for-service-stability: true
请注意 `wait-for-service-stability: true` 这一设置。如果没有这个选项,当 ECS 接收到新的任务定义后,即使容器实际上还没有恢复正常状态,工作流程也会报告成功结果;而设置了这一选项之后,如果新创建的容器发生故障,工作流程就会失败。你肯定希望能够立即了解到这种情况的发生,而不是在三十分钟后通过用户的反馈才得知。
错误8:没有灾难恢复计划
情景描述
某家初创企业的生产数据库运行在单个 RDS 实例上,且未配置多可用区功能。虽然启用了自动备份功能,但从未进行过测试。当支撑该数据库的 EBS 卷发生故障时,AWS 会从最新的快照中创建一个新的实例,然而这个新实例所包含的数据仅是18小时前的数据,因此客户18小时以来的数据将会永久丢失。
这家初创企业既没有灾难恢复计划,也没有经过测试的恢复流程,更没有为客户准备好的沟通方案。
业务影响
问题不在于你的基础设施是否会出故障——它肯定会出故障。所有的数据库、所有的服务器、所有的可用区都可能发生故障。关键在于你是否有一套经过测试的应对方案,以便在故障发生时能够迅速采取行动。
任何形式的数据丢失都会带来严重的后果。对于那些处理金融数据、医疗健康数据或受 GDPR 规范约束的数据的初创企业来说,即使是部分数据丢失,也可能引发监管方面的问题。
解决方法
在开始进行任何设计之前,首先明确你的 RTO 和 RPO:
-
RTO(恢复时间目标): 在没有这个系统的情况下,你的业务还能持续运行多久?例如,一个支付 API 的 RTO 可能为15分钟,而一个内部分析仪表盘的 RTO 则可能为4小时。
-
RPO(恢复点目标): 多大的数据丢失是可以接受的?如果设置为0,意味着需要实时复制数据;如果设置为1小时,那么每小时生成一次快照就足够了。这个参数直接决定了你的备份频率和备份架构。
为所有生产数据库启用 RDS 多可用区功能:
# Terraform
resource "aws_db_instance" "production" {
identifier = "prod-postgres"
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.medium"
allocated_storage = 100
# 多可用区配置:系统会自动切换到另一个可用区的备用实例,从而避免数据丢失。切换时间约为60至120秒。
multi_az = true
# 数据存储加密是必须的
storage_encrypted = true
# 自动备份功能,数据保留7天
backup_retention_period = 7
backup_window = "03:00-04:00"
# 在生产环境中启用数据删除保护机制
deletion_protection = true
tags = {
Environment = "production"
}
}
定期测试你的备份系统。例如,可以设置每月进行一次“将生产环境的数据备份恢复到测试环境并验证数据完整性”的操作。未经测试的备份根本算不上真正的备份,它只是一种希望罢了。
# 将快照恢复到测试实例并进行验证
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier recovery-test \
--db-snapshot-identifier rds:prod-postgres-2025-01-15 \
--db-instance-class db.t3.medium \
--no-multi-az
# 连接数据库并验证记录数
psql -h recovery-test.xxxx.rds.amazonaws.com -U admin -d mydb \
-c "SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM orders;"
有关RDS备份和恢复的官方指南,请参阅AWS RDS备份和恢复文档。
错误9:没有文档或操作手册
场景描述
这家初创企业的经验最丰富的DevOps工程师请了两周的假。在假期第三天,他们的测试环境出现了故障。由于没有人知道这个环境是如何构建的——这位工程师是在六个月内手动配置它的,而且完全没有留下任何文档、使用过Terraform工具,也没有任何记录——团队花了四天的时间试图根据记忆和推测来重建这个环境。在工程师休假期间,团队每天都会收到相关消息。当工程师回来后,他们仅用了四个小时就重新构建好了整个环境。
对业务的影响
缺乏文档记录的基础设施会带来“单点故障”的问题——这些故障并不发生在系统本身上,而是出现在团队中。这使得新成员加入团队后需要花费数周时间才能熟悉工作流程;同时,事故响应也依赖于某些特定人员的存在;而当这些人离开公司时,这些知识也会随之消失。
解决方法
对于一个工程团队来说,文档应该包含以下三方面的内容:
-
“基础设施即代码”才是最高形式的文档。用于定义基础设施的Terraform代码本身,就是关于现有系统架构及其配置方式的文档。如果某些配置没有以代码形式记录下来,那么它们就不应该在生产环境中被使用。
-
每项操作任务都应配有相应的操作手册。操作手册应该是编写得非常详细的步骤指南,这样新员工在入职第一周遇到问题时就可以按照这些指南来解决问题。
# 操作手册:生产环境数据库连接数过多问题的处理方案
## 症状
- 应用程序日志中出现“连接数过多”的错误
- 依赖数据库的接口端点的500错误率急剧上升
- pg_stat_activity报告显示最大连接数已被达到
- 每个代码仓库都应包含一份架构说明文档。任何克隆你的代码仓库的工程师都应该能够理解该系统的功能、如何在本地运行它、如何进行部署,以及它依赖于哪些组件,而无需向他人请教。
错误10:在不了解业务背景的情况下解决技术问题
场景描述
一家初创公司遇到了页面加载速度缓慢的问题。一位DevOps工程师决定通过将系统迁移到Kubernetes并启用水平Pod自动扩展功能来解决这个问题。迁移工作耗时六周,页面加载速度确实有所改善。但实际上,80%的加载延迟问题是由数据库查询效率低下造成的,而这些问题与基础设施层并无直接关系。因此,这次长达六周的迁移只解决了20%的问题。
对业务的影响
对于被误诊的问题,采用技术手段来解决问题往往代价高昂。每花费一个小时去开发错误的解决方案,就意味着少了一个小时来解决真正的问题。基础设施只是实现业务目标的工具,而非最终目的本身。
解决办法
在做出任何与基础设施相关的决策之前,请先回答以下四个问题:
-
真正的瓶颈是什么?在采取行动之前,先进行详细的诊断。瓶颈几乎从来都不在你原本假设的位置。
-
成功的标准是什么?你将如何衡量成功?
“页面加载速度变快了”这种说法并不具有可量化性;而“95%的页面加载时间低于1.2秒”这样的指标才具备测量价值。
-
采用这种解决方案的总成本是多少?包括实施成本、后续的运营维护费用以及团队需要投入的学习时间。这些成本是否与预期带来的效益相称?
-
是否存在更简单的解决方案,能够在更短的时间内解决80%的问题?
在重新构建系统之前,务必先进行性能分析和测量:
# 在对基础设施进行任何修改之前,先检查PostgreSQL中的慢查询问题
psql -h \(DB_HOST -U \)DB_USER -d $DB_NAME -c "
SELECT
query,
calls,
total_exec_time / calls AS avg_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY avg_ms DESC
LIMIT 10;
"
在十有八九的情况下,应用程序加载缓慢的原因都是由于存在慢查询、缺失索引或N+1查询问题,而这些问题并不需要通过搭建新的基础设施层来解决。
每位DevOps工程师都需要的系统思维框架
上述大多数错误都有一个共同的根本原因:工程师们总是孤立地考虑某个具体组件,而没有从整个系统的角度出发来进行思考。

在进行任何生产环境中的变更之前,系统思考者会先提出六个问题:
| 问题 | 提出这个问题的原因 |
|---|---|
| 这一变更会带来什么影响? | 列出所有会因此发生变化的配置项、文件或服务。 |
| 这一变更依赖于哪些因素? | 为了使该组件正常运行,上游环境中必须满足哪些条件? |
| 哪些下游系统会受到这一变更的影响? | 如果这一变更失败,哪些下游系统会受到影响? |
| 这种变更可能会引发什么故障现象? | 这种变更会导致严重的错误(比如500个错误)还是只会产生错误数据? |
| 有什么方法可以迅速恢复到变更前的状态? | 如何在五分钟内将系统恢复到变更前的正常状态? |
| 变更之后,系统的“健康状态”应该是怎样的? | 哪些指标能够证明系统运行正常? |
这并不是一份需要慢慢逐条检查的清单,而是一种通过练习会逐渐成为习惯的思考方式。高级工程师在部署工作上花费的时间并不会比初级工程师多,他们把时间花在了其他方面,而这份清单就是其中之一。
您的生产环境准备检查清单
在任何生产系统正式上线之前,请使用这份清单。将每项内容标记为“已完成”、“进行中”或“尚未开始”。
基础设施
-
基础设施由代码构成(使用Terraform或CloudFormation进行配置),并且这些代码都存储在Git中进行版本控制。
-
开发环境、测试环境和生产环境是相互独立的,且使用不同的登录凭据。
-
所有生产环境的变更都会通过自动化的CI/CD流程来处理,不会采用手动SSH部署的方式。
-
您可以在两小时内仅通过代码就重新构建整个生产环境。
安全性
-
任何Git仓库中都不应存放密码、凭据或API密钥。
-
所有的生产环境相关秘密信息都存储在Secrets Manager或SSM Parameter Store中。
-
所有IAM角色都遵循最小权限原则进行配置。
-
S3存储桶默认设置为禁止公共访问。
-
任何安全组都不会允许端口22被
0.0.0.0/0地址访问。 -
所有区域都启用了CloudTrail功能。
-
所有IAM用户都启用了多因素身份验证。
-
已启用AWS Security Hub,并且每周都会审查其中发现的安全问题。
可观测性
-
每项服务都拥有一个
/health端点,用于进行持续监控。 -
当生产环境中的错误率突然上升时,系统会在五分钟内发出警报。
- 存在专门用于显示延迟、错误率和资源使用情况的仪表板。
- 所有日志都被集中存储并且可以方便地被查询,而不会分散在各个服务器上。
可靠性
-
生产数据库已启用多区域部署功能。
-
在过去的30天内,备份恢复流程已经过测试。
-
针对三种最可能发生故障的情况,已经制定了相应的操作手册。
-
关于RTO和RPO的要求已有明确记录,且当前的系统架构也符合这些要求。
文档资料
-
每个代码仓库都配有README文件,说明其用途及部署方法。
-
新工程师仅通过阅读文档就能了解生产环境的整体架构。
-
没有任何关键知识只存在于某位工程师的脑海中。
结论
本文中提到的所有问题,其实都不需要遭遇什么特殊的不幸才会发生。它们都是那些在创业初期看似合理的决策所导致的、逐渐累积而成的实际运营风险。
好消息是,只要具备足够的意识并尽早养成正确的习惯,这些风险都是可以避免的。
你并不需要在一开始就拥有完美的基础设施——只需要拥有合适的基础设施即可:版本控制机制、自动化流程、可监控性、安全性以及完善的文档记录。先建立这样的基础,只有当遇到具体且可量化的问题时,才再逐步增加系统的复杂性。在任何时候,都要将技术决策与业务成果紧密联系起来。
对于初创企业而言,DevOps的目标并不是构建令人印象深刻的基础设施,而是打造可靠的系统,从而确保产品的成长能够安全、高效且可持续地进行;同时,也要保证在系统出现故障时,企业能够迅速恢复运行,而且这种恢复过程几乎不会被任何人注意到。
想深入了解吗?
如果这篇文章引起了你的共鸣,那么《初创企业DevOps实践指南》会为你全面深入地讲解这些原则。其中包含了完整的基础设施设计方案、安全框架、CI/CD流程模板,以及专为那些从零开始在初创环境中建立DevOps体系的工程师们准备的决策参考手册。
这份指南是专门为那些希望从一开始就正确开展DevOps工作的工程师们编写的,而不是为那些在遇到重大问题后才才开始重新构建系统的工程师们准备的。



