应用完善架构框架(精简版)
你是否曾经遇到过这样的情况:在解决一个问题时,你发现自己已经花了太多时间?但你也知道这一切都是值得的?这篇文章(也很长!)总结了我最近处理这类问题的经验。
tl:dr
- AWS Lambda 的存储限制为
/tmp
512MB - AWS Lambda 函数需要位于 VPC 中才能连接到 Amazon EFS 文件系统
- VPC 中的 AWS Lambda 函数需要NAT 网关才能访问互联网
- Amazon EC2 实例可以使用 cloud-init 在启动时运行自定义脚本
- 解决方案需要满足 AWS 完善架构框架的所有五大支柱,才能成为“最佳”解决方案
请继续阅读以了解整个故事……
问题
我热爱学习,想关注几个关键领域。讽刺的是,标签本身对我来说是个大问题。
每天早上,我都会收到几封来自Mailbrew服务的定制邮件。每封邮件都包含 Twitter 查询、子版块和关键网站(通过 RSS)的最新结果。
问题是我想要跟踪很多网站,而 Mailbrew 只支持逐个添加网站。
这引出了以下问题陈述……
将 N 个 feed 合并为一个超级 feed
约束
理想情况下,这些超级 feed应该发布在我的网站上。我的网站是用Hugo构建的,并部署到Amazon S3 上,前端则使用CloudFlare 。这个设置对我来说非常理想。
遵循AWS 完善架构框架;它性能卓越、成本低廉、运维负担极小、安全态势稳固,并且非常可靠。它在五大支柱方面均取得了成功。
将提要添加到此设置不应损害任何这些属性。
我认为有必要指出的是,市面上有很多服务可以帮你整合信息源。比如RSSUnify和RSSMix ,但还有很多其他的……
Hugo 的优点在于它使用文本文件来构建你的网站,包括自定义 RSS 源。解决方案应该将这些 RSS 源项作为唯一的文章(又称文本文件)写入我的 Hugo 内容目录结构中。
🐍 Python 来救援
一个快速的小 Python 脚本(可在此处获得)和我有一个工具,它可以获取提要列表并将它们写入独特的 Hugo 帖子中。
有问题?解决了。
嗯...我忘记了这些提要需要保持最新,并且我当前的构建管道(GitHub Action)不支持按计划运行。
此外,尝试在操作中运行该代码将需要另一个事件来挂钩,否则当新的 feed 项目提交到 repo 时它将陷入更新循环中。
新的问题陈述...
按需并按设定的时间表运行 Python 脚本
对于无服务器解决方案来说,这似乎是一个好问题。
AWS Lambda
我立刻想到了AWS Lambda 。我使用由Amazon CloudWatch 计划事件触发的 AWS Lambda 函数运行了大量类似的小型操作任务。这是一个强大而简单的模式。
事实证明,让 Lambda 函数访问 git 仓库并非易事。我就不多说这些了,下面是我使用 Python 3.8 运行时运行它的方法:
- 将 git 二进制文件添加为 Lambda 层,我使用了git-lambda-layer
- 使用GitPython模块
这允许像这样的简单代码设置:
repo = git.Repo.clone_from(REPO_URL_WITH_AUTH)
...
# do some other work, like collect RSS feeds
...
for fn in repo.untracked_file:
print("File {} hasn't been added to the repo yet".format(fn))
# You can also use git commands directly-ish
repo.git.add('.')
repo.git.commit('Updated from python')
这样一来,使用 repo 就变得非常简单了。我动动手指,就把 Lambda 函数连接到了预定的 CloudWatch 事件,就大功告成了。
……直到我突然想起来——“想起来”是指函数抛出了异常——Lambda 的/tmp
存储限制是 512MB。我网站的 repo 大约有 800MB,而且还在不断增长。
亚马逊 EFS
值得庆幸的是,AWS 刚刚发布了Amazon EFS 与 AWS Lambda的新集成。我按照相对简单的流程完成了设置。
我遇到了两个大问题。
首先,Lambda 函数要连接到 EFS 文件系统,两者必须位于同一个 VPC 中。如果您已经设置了 VPC,这很容易做到;即使您没有设置,也一样。我们稍后会再讨论这一点。
第二个问题是,我最初将 EFS 接入点的路径设置为/
。官方文档中没有(我看到的)警告,但Peter Sbarski 在一篇精彩的文章中随口一说,突出了这个问题。
这是一个简单的修复(我选择了/data
),但 VPC 问题带来了更大的挑战。
解决此问题最简单的 VPC 是配置一个互联网网关的一两个子网。此结构免费,仅对入站/出站数据传输产生费用。
只不过我的 Lambda 函数需要互联网访问,而这还需要一个部件。
那部分就是NAT网关。没什么大不了的,一键部署,路由更改也一样。新的问题是成本。
NAT 网关的需求完全合理。Lambda 与您的网络结构相邻运行。将这些函数路由到您的 VPC 需要显式的结构。从安全角度来看,我们不希望我们的私有网络 (VPC) 与 AWS 的其他随机部分之间建立隐式连接。
完善架构原则
这就是事情真正开始偏离轨道的地方。如上所述,良好架构框架建立在五大支柱之上;
AWS Lambda + Amazon EFS 路线除了成本优化之外,在所有支柱中继续表现良好。
为什么?因为我使用账户和 VPC 作为强大的安全屏障。所以,运行此 Lambda 函数和 EFS 文件系统的 VPC 仅适用于此解决方案。NAT 网关只会在构建我的网站时使用。
NAT网关每月的费用是多少?32.94美元+消耗的带宽。
这笔钱不算不合理,除非你把它放在项目的实际情况中。该网站的托管费用每月不到 0.10 美元。如果加上 AWS Lambda 函数 + EFS 文件系统,每月费用就会飙升到 0.50 美元😉。
NAT 网关现在看起来非常不合理
替代方案
在计算方面,AWS Lambda 的简单替代方案是AWS Fargate和老牌的Amazon EC2。尽管大家可能都倾向于容器,而且我也听到有人说它是下一个合乎逻辑的选择……
...我采用了传统方法并开始探索 EC2 中的解决方案是什么样的。
Amazon EC2 实例要访问互联网,只需位于具有互联网网关的 VPC 的公有子网中即可。无需 NAT 网关。这样可以省去每月 32.94 美元的费用,但计算成本会更高。
但我们能轻松实现自动化吗?这会是一个可靠的解决方案吗?安全性方面又如何呢?
Amazon EC2 解决方案
🔑 关键?记住EC2 的用户数据功能,并且所有 AWS 管理的 AMI 都支持cloud-init。
这为我们提供了 16KB 的空间,可以动态配置实例来完成我们的任务。这应该足够了……如果你还没从我的头像上看出点什么,我正接近职业生涯的巅峰期🧙♂️。
经过一些快速的故障排除(许多实例启动和关闭),我最终使用这个bash脚本(是的,bash)来解决问题;
#! /bin/bash
sleep 15
sudo yum -y install git
sudo yum -y install python3
sudo pip3 install boto3
sudo pip3 install dateparser
sudo pip3 install feedparser
cat > /home/ec2-user/get_secret.py <<- PY_FILE
# Standard libraries
import base64
import json
import os
import sys
# 3rd party libraries
import boto3
from botocore.exceptions import ClientError
def get_secret(secret_name, region_name):
secret = None
session = boto3.session.Session()
client = session.client(service_name='secretsmanager', region_name=region_name)
try:
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
except ClientError as e:
print(e)
else:
if 'SecretString' in get_secret_value_response:
secret = get_secret_value_response['SecretString']
else:
decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
return json.loads(secret)
if __name__ == '__main__':
github_token = get_secret(secret_name="GITHUB_TOKEN", region_name="us-east-1")['GITHUB_TOKEN']
print(github_token)
PY_FILE
git clone https://\`python3 get_secret.py\`:x-oauth-basic@github.com/USERNAME/REPO /home/ec2-user/website
python3 RUN_MY_FEED_UPDATE_SCRIPT
cd /home/ec2-user/website
# Build my website
./home/ec2-user/website/bin/hugo -b https://markn.ca
# Update the repo
git add .
git config --system user.email MY_EMAIL
git config --system user.name "MY_NAME"
git commit -m "Updated website via AWS"
git push
# Sync to S3
aws s3 sync /home/ec2-user/website/public s3://markn.ca --acl public-read
# Handy URL to clear the cache
curl -X GET "https://CACHE_PURGING_URL"
# Clean up by terminating the EC2 instance this is running on
aws ec2 terminate-instances --instance-ids `wget -q -O - http://169.254.169.254/latest/meta-data/instance-id` --region us-east-1
整个运行平均耗时 4 分钟。即使按需成本较高(相对于现货成本),在 us-east-1 的 t3.nano 上,每次运行成本也仅为 0.000346667 美元。
就本月而言,该费用为 0.25376244 美元(732 次预定运行)。
我们的价格远高于 AWS Lambda 计算价格(每月 0.03 美元),但仍低于 Lambda + EFS 的成本(每月 0.43 美元),当然也远低于包含 NAT 网关在内的总成本。这很奇怪,但这就是云的运作方式。
每次运行都由 CloudWatch 事件触发,该事件调用 AWS Lambda 函数来创建 EC2 实例。与纯 Lambda 解决方案相比,这增加了一步,但仍然合理。
可靠性
实践中,这个解决方案运行良好。运行了200多次,我从未遇到过任何故障。这是一个良好的开端。此外,故障成本也很低。如果此流程运行失败,网站就不会更新。
从整体爆炸半径来看,实际上只有两个问题需要考虑;
- 如果同步到 S3 失败并导致站点处于不完整状态
- 如果实例终止失败
同步失败的可能性非常低,但如果失败,损害只会影响站点上的一项资产。如果文件较新,AWS CLI 命令会逐个复制文件。如果其中一个文件失败,命令将停止。这意味着只有一项资产(页面、图像等)处于损坏状态。
只要它不是网站的主 .css 文件,就应该没问题。即使如此,简洁的 HTML 标记也能确保网站可读性。
第二个问题可能会产生更大的影响。
t3.nano 实例的每小时成本为 0.0052 美元。每次运行此函数时,都会创建一个新的实例。这意味着,如果发生故障,我可能会有几十个这样的实例在运行……如果不加以控制,我的账单很快就会超过每月 100 美元。
为了降低这种风险,我在 bash 脚本中添加了另一个命令;shutdown
同时确保在创建实例时设置了 API 参数—instance-initiated-shutdown-behavior
set to terminate
。这意味着实例会调用 EC2 API 自行终止,并自行关闭以终止……以防万一。
添加账单警报可以完善缓解措施,从而显著降低风险。
安全
这个解决方案的安全性让我很担心。AWS Lambda 为用户提供的责任要少得多。事实上,在责任共担模型中,实例是构建者承担的最大责任。这与我们想要的方向恰恰相反。
由于此实例不处理入站请求,安全组已完全锁定。它只允许出站流量,不允许入站流量。
此外,使用 IAM 角色,我仅提供了完成当前任务所需的必要权限。这被称为最小特权原则。有时设置起来可能比较麻烦,但确实可以有效降低任何解决方案的风险。
您可能已经注意到,在上面的用户数据脚本中,我们实际上是在启动时向实例编写了一个 Python 脚本。该脚本允许实例访问AWS Secrets Manager以获取密钥并将其值打印到标准输出。
我用它来存储克隆和更新我网站的私有仓库所需的GitHub 个人访问令牌。这降低了我的 GitHub 凭证的风险,而凭证是整个解决方案中最重要的数据。
这意味着该实例需要以下权限;
secretsmanager:GetSecretValue
secretsmanager:DescribeSecret
s3:ListBucket
s3:*Object
ec2:TerminateInstances
权限secretsmanager
被锁定在 GitHub 令牌对应的密钥的特定 ARN 上。s3
权限仅限于读取/写入我的网站存储桶。
ec2:TerminateInstances
由于我们事先不知道实例 ID,所以这有点棘手。您可以动态分配权限,但这会增加不必要的复杂性。相反,这是一个很好的用例,可以使用资源标签作为权限的条件。如果实例未正确标记(在本例中,我使用“Task”键,并将值设置为随机的常量值),则此角色无法终止它。
同样,AWS Lambda 函数具有标准执行权限;
iam:PassRole
ec2:CreateTags
ec2:RunInstances
网络安全只是确保你所构建的东西能够按照你的意图运行......并且只按照预期运行。
如果我们仔细研究这个解决方案的可能性,就会发现攻击者如果没有我们帐户内的其他权限和访问权限就无法做任何事情。
即使我们使用了 EC2,我们也已将风险降至可接受的水平。看来此解决方案只能达到预期效果。
我学到了什么?
完善架构框架不仅适用于大型项目或特定时间点的评审。该框架所倡导的原则适用于任何项目。
我以为我有一个使用无服务器设计的简单、万无一失的解决方案。但这次,定价挑战迫使我改变了方法。有时是性能问题,有时是安全性问题,有时是其他方面的问题。
无论如何,您都需要根据所有五个支柱来评估您的解决方案,以确保达到正确的平衡。
关于实例方法,我仍然有点担心。我不像部署 Lambda 时那样安心,但数据显示它很可靠,能够满足我的所有需求。
这种设置也留有扩展空间。向用户数据脚本添加其他任务非常简单,如果操作得当,不会显著改变围绕五大支柱的任何关注点。这里的风险在于将其扩展为自定义 CI/CD 流水线,这是应该避免的。
“我构建了自己的……”,通常意味着你在某个地方走错了方向。当你发现自己也重复这种想法时,就要小心了。
这也提醒我们,三大云平台(AWS、Azure、Google Cloud)拥有大量的特性和功能,而保持领先地位可能是一个挑战。
如果我不记得 cloud-init/user-data 组合,我不确定我是否会将 EC2 评估为可能的解决方案。
继续学习和分享的另一个理由!
顺便说一句,请查看这项工作的结果;
如果我遗漏了您认为应该存在的供稿链接,请告诉我!
总成本
如果您对最终解决方案的总成本明细感兴趣,请参阅以下内容;
Per month
===========
24 * 30.5 = 732 scheduled runs
+ N manual runs
===========
750 runs per month
EC2 instance, t3.nano at $0.0052/hour
+ running for 4m on average
===========
(0.0052/60) * 4 = $0.000346667/run
AWS Lambda function, 128MB at $0.0000002083/100ms
+ running for 3500ms on average
============
$0.00000729/run
Per run cost
============
$0.000346667/run EC2
$0.00000729/run Lambda
============
$0.000353957/run
Monthly cost
=============
$0.26546775/mth to run the build 750 times
$0.00 for VPC with 2 public subnets and Internet Gateway
$0.00 for 2x IAM roles and 2x policies
$0.40 for 1x secret in AWS Secrets Manager
$0.00073 for 732 CloudWatch Events (billed eventually)
$0.00 for 750 GB inbound transfer to EC2 (from GitHub)
$0.09 for 2 GB outbound trasnfer (to GitHub)
=============
$0.75620775/mth
This means it'll take 3.5 years before we've spent the same as one month of NAT Gateway support.
* Everything is listed in us-east-1 pricing