因为我觉得听到有人说”你们 AWS Lambda 别名弄错了”,所以写在备忘录上
首先
我没有说过这样的话。对不起。
我最近有些想法关于AWS Lambda的别名功能。
在互联网上经常出现的介绍有
可以使用dev别名来搭建开发环境,使用prod别名来搭建生产环境,这样可以将环境进行区分!
你觉得不是这样吗?但是,这个呢,不是不可能吗?
哎呀,如果你想做的话,是可以做到的。
然而,就运维而言,存在一些令人不安的问题,最重要的是与IaC(在这里指的是CDK)的兼容性太差了吧?我觉得,所以我想写下我的想法。
前提概念
我会整理一些前提知识给大家。如果你有一定的AWS Lambda使用经验,可以跳过这一部分。
AWS Lambda: 亚马逊云Lambda
这是一个能够在触发任何事件时执行代码的 AWS 无服务器计算服务。
有几种可供选择的运行时环境,比如使用Node.js编写的应用程序可以轻松编写并运行。
AWS Lambda 版本
AWS Lambda 允许将代码打包成 zip 文件进行上传,或使用 Docker 镜像进行部署。这样做可以像使用快照一样固定 Lambda 在某个时间点的状态,并进行使用。
我们称之为 Lambda 函数的版本。
在中文中,有一个特殊版本的标识符$LATEST,它指的是Lambda函数在创建版本之前的状态。换句话说,如果改变代码或其他设置,就会对$LATEST进行更改,并从$LATEST生成Lambda版本。
AWS Lambda函数别名
您可以定义一个指向已发布的 Lambda 版本的指针标识符。这被称为 Lambda 函数的别名。
就像之前所提到的,如果在互联网上查找Lambda的别名,你可能会看到一些示例,比如创建开发用的“dev”别名和生产用的“prod”别名。
好的,下面我简要介绍一下前提知识。
现在就让我们进入正题吧~。
我认为无法在开发和正式环境中区分别名的原因是。
以下为要点:
-
- 对环境变量的更改让人担心啊。
-
- 其他的 AWS 资源(比如 DynamoDB)没有别名呀。
- 基础设施无法以声明式的方式编写呀。
我将写出关于以下每个方面的考虑。
假定不另行声明的情况下,以下内容中的 dev 别名将引用$LATEST版本,而 prod 别名将引用特定 Lambda 的已发布版本。
理由1:更新环境变量很可怕呀。
根据《十二要素应用程序》的提及,使用环境变量来注入配置信息的方法,可以让应用程序在不同的部署环境(例如生产环境和开发环境)中运行时无需关心其部署情况,这是一种常见的做法。
在AWS Lambda上,也具备了设置环境变量的功能。
从屏幕上可以看到这样一个东西。 您可以为每个函数进行设置。
重要的点在于,当发版后,Lambda的环境变量会被固定。
因此,环境变量的设置更改只能针对$LATEST执行,这是关键。
如果您使用dev/prod别名,并且想手动部署,您需要按照以下步骤进行操作:
-
- 将dev别名($LATEST)的环境变量设置更改为prod。
-
- 发布一个新版本。
-
- 更新prod别名,并将其配置为引用新发布的版本。
- 将dev别名($LATEST)的环境变量设置恢复为dev原始值。
上述的1至3步骤看起来似乎没有问题,但我认为第四步骤可能存在相当大的问题。
如果忘记了这个步骤,在进行开发阶段的操作确认时,有可能会错误地操作生产环境的数据,或者造成一些意外影响。
如果发生了这第四步骤,那么我觉得从一开始的”设计”就存在问题。
在这里,“设计”指的是在开发和生产环境中使用Lambda别名来进行区分。
原因2:其他AWS服务(例如DynamoDB)没有别名。
如果旨在实现顺利切换开发和生产环境的意图的话,我认为其他AWS服务应该有类似别名的机制。但事实上并非如此。
如果一个系统仅凭Lambda就能完成,那可能还可以接受。但实际上,我认为我们经常会构建使用数据库等组件的系统。
在这种情况下,准备开发和验证用的数据库实例也是常见的。
我的理解是,也就是说,(当然了)我们需要准备一个专门用于实际使用的数据库。
我对Lambda和DB在”非对称性”方面的区别感到不舒服。
这个方针的最大问题就是无法建立灵活且更安全的开发环境,比如在不同的AWS账户中建立开发环境和生产环境。
我认为如果不能采取这样的安全措施,将dev/prod区分为别名的好处可能很少。
第三个理由:基础设施无法以宣言的形式表达。
最近,像 Terraform 和 CloudFormation 这样的基础设施编码工具越来越成熟。虽然我常常使用 AWS CDK,但是通过代码搭建基础设施非常容易管理,非常有用。
顺便说一下,我理解到 IaC 的一个特点是能够以声明方式定义基础架构。
↑這個「宣言式的」是關鍵,剛才提到的手動逐個修改環境變數的作業無法用宣言式的方式來寫。因為那是一個「程序」。
举例来说,假设我们使用CDK(TypeScript)来定义基础设施。通过参考环境变量ENVIRONMENT,我们打算实现根据不同的部署来构建基础设施。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
const getEnvironment = () => process.env.ENVIRONMENT || "dev";
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new NodejsFunction(
this,
"sample-stack-nodejs-function",
{
functionName: "sample-stack-nodejs-function",
handler: "index.handler",
entry: "lambda/sample-stack-nodejs-function.js",
environment: {
ENVIRONMENT: getEnvironment(),
},
}
);
}
}
我只是先定义了一个使用一个 Node.js 运行时的 Lambda 函数。
在这个例子中,尚未发布 Lambda 函数的版本和别名。因此,我们将直接在$LATEST上设置环境变量ENVIRONMENT。当然,由于是$LATEST,我们可以在部署后自由地更改设置。
那么,现在让我们来定义版本发布和别名。在这里,我想以以下简单的实现为例。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
const getEnvironment = () => process.env.ENVIRONMENT || "dev";
export class SampleCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const sampleFunction = new NodejsFunction(
this,
"sample-stack-nodejs-function",
{
functionName: "sample-stack-nodejs-function",
handler: "index.handler",
entry: "lambda/sample-stack-nodejs-function.js",
environment: {
ENVIRONMENT: getEnvironment(),
},
}
);
// dev エイリアスを定義
new lambda.Alias(
this,
"sample-stack-nodejs-function-dev-alias",
{
aliasName: "dev",
version: sampleFunction.latestVersion,
},
);
// バージョンを発行して、prod エイリアスを定義
if (getEnvironment() === 'prod') {
const lambdaVersion = new lambda.Version(
this,
"sample-stack-nodejs-function-version",
{ lambda: sampleFunction },
);
new lambda.Alias(
this,
"sample-stack-nodejs-function-prod-alias",
{
aliasName: "prod",
version: lambdaVersion,
},
);
}
}
}
这是一个相当简单直接的实现,但是如果 ENVIRONMENT === “prod” 为 true 的情况下,我们将定义 Lambda 的版本和prod别名。同时,dev别名是我们总是使用的,因此总是进行定义。在 dev 别名的定义部分,我们通过指定 sampleFunction.latestVersion 来引用 $LATEST。
单凭这段代码来看,感觉不奇怪地会觉得有些不错的样子。
然而,举个例子,如果
-
- 首先,在环境变量为”dev”的情况下进行部署,进行开发和确认操作。
- 在上述步骤完成后,再在环境变量为”prod”的情况下进行生产环境部署。
完成这项任务之后,Lambda函数的环境变量将处于什么样的状态?(思考时间)
请用中文将下面的句子表达出来,只需要一种方式:
・・・・
・・・・
・・・・
请将以下句子用中文重新表述,只需一种方式:
是的,确实。这将导致dev别名($LATEST)上设置了与生产环境相同内容的环境变量。
When illustrated, it looks something like this ↓. 一旦被插图,看起来像这样↓。
由于AWS Lambda版本别名的设计,首先需要更新$LATEST,然后进行版本发布和别名更新等步骤,因此会导致更新$LATEST的环境变量。
因此,最终在dev别名中保留了生产环境的环境变量。
如果想要恢复到最新的设置,应该怎么做呢?
最简单的方法是重新部署,将ENVIRONMENT = “dev”再次部署即可。
但是,如果这样做,下一步就会删除prod别名。
由于在环境变量为”dev”的情况下,CDK生成的模板中将没有prod别名存在。
如果开始琢磨着总是保留prod别名之类的东西,突然之间就会变得麻烦起来。
我觉得这个问题应该通过制度来解决。
那么,怎么办呢?
我认为使用 dev/prod 别名的方法可能存在之前提到过的问题。
我想考虑一下如何能够更安全地进行开发和运营。
基本方针
就Lambda而言,我认为最好的做法是将函数资源分开。
换句话说,就是要分别构建开发用的函数和生产用的函数。
只要按照这样做,虽然没有特别提及,但例如添加其他类型的部署,如暂存环境等,也很容易实现。
此外,关于别名,我们考虑了如下图所示的方式。
主要要点如下:
-
- 開発用 Lambda 関数に定義するエイリアスは dev のみ。$LATESTを参照している。
-
- 本番用 Lambda 関数の$LATESTは、原則触らない。$LATESTを参照するエイリアスは定義しない。
- システムのバージョン(Semantic Versioning に則るとする)に対応する名前のエイリアスを定義し、それぞれ別々の Lambda の発行したバージョンを参照するようにする。
根据基本方针的意图
首先,关于开发函数,我没有特别要说的。它可能是一个非常自然的 dev 别名,不是吗?
一方面,关于生产环境的 Lambda 函数,首先的意图是“充分利用 Lambda 的版本”。
Lambda的版本将环境变量和部署的代码等信息固定下来。
如果偶然将$LATEST用作生产环境,则可以直接从AWS管理控制台更改代码,也可以更改环境变量的设置。
我认为,即使团队中没有恶意的人,因为人为错误的可能性仍然存在,所以通过正确地发布 Lambda 的版本来固定部署状态并实现更安全的操作是有价值的。
然而,Lambda 版本的标识符只是简单的自然数连续编号3,这实在令人不太开心。我不想给这个连续编号赋予任何含义,也无需进行管理。
因此,我认为在我们运营系统时,需要给它起一个更易于理解的名称。
我认为作为解决方案,使用Lambda别名会更好。
顺便问一下,我认为在我们自己管理系统发布时,常常会采用类似于语义化版本控制的方式来管理版本。
如果是这样的话,例如如果系统版本是1.0.0,可以通过为其添加对应的别名prod_1-0-0,以便更容易理解哪个版本的Lambda正在运行。
这个基本方针解决了什么问题?
如果我们针对刚才提到的问题进行思考的话,
-
- 開発用・本番用で Lambda関数を分けたので、それぞれの環境にデプロイする際に、他の環境のことを意識しなくて済む
- DBなど、他のAWSリソースたちと同様に、デプロイメントごとにリソースを定義した形になったことで、AWS アカウントを分割するといった体制も容易に実現できる。
我认为在这些方面可以进行改善。
使用 AWS CDK 的实施样例
我打算简单地实施先前的基本方针。
首先讨论一下函数的定义,例如下面的方式可能是正确的。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
const getEnvironment = () => process.env.ENVIRONMENT || "dev";
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const sampleFunction = new NodejsFunction(
this,
"sample-stack-nodejs-function",
{
functionName: `sample-stack-nodejs-function-${getEnvironment()}`,
handler: "index.handler",
entry: "lambda/sample-stack-nodejs-function.js",
environment: {
ENVIRONMENT: getEnvironment(),
},
},
);
}
}
根据 functionName 中定义的 sample-stack-nodejs-function-${getEnvironment()} 的规则,我们根据 ENVIRONMENT 的设置来设计不同的 Lambda 资源。
最后是版本别名的定义。
接下来的部分将与之前的实施示例几乎相同,只是对别名名称进行了一些改进:
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
const getEnvironment = () => process.env.ENVIRONMENT || "dev";
/**
* 現在のシステムのバージョンを定義
* あるいは、別のファイルから読み取るなどの工夫をする。
*/
const CURRENT_SYSTEM_VERSION = "0.0.0";
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const sampleFunction = new NodejsFunction(
this,
"sample-stack-nodejs-function",
{
functionName: `sample-stack-nodejs-function-${getEnvironment()}`,
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_18_X,
entry: "lambda/sample-stack-nodejs-function.js",
environment: {
ENVIRONMENT: getEnvironment(),
},
},
);
if (getEnvironment() === 'prod') {
const lambdaVersion = new lambda.Version(
this,
`sample-stack-nodejs-function-version-${CURRENT_SYSTEM_VERSION}`,
{ lambda: sampleFunction },
);
new lambda.Alias(
this,
"sample-stack-nodejs-function-prod-alias",
{
aliasName: `prod_${CURRENT_SYSTEM_VERSION.replace(/\./g, "-")}`,
version: lambdaVersion,
},
);
} else {
new lambda.Alias(
this,
"sample-stack-nodejs-function-dev-alias",
{
aliasName: "dev",
version: sampleFunction.latestVersion,
},
);
}
}
}
在类定义的上方定义了一个名为CURRENT_SYSTEM_VERSION的常量。(注意:在这里我们硬编码了,但是希望您可以根据方便管理的需求进行一些调整)
这次我们只专注于Lambda,并进行了非常简单的实现,但基本上可以按照这个形式来进行实现。
AWS Lambda 的别名是用来做什么的?
让我们回到这里,思考一下 Lambda 函数的别名功能的目的是什么。
从公式文件中引用如下:
您可以为Lambda函数创建一个或多个别名。Lambda别名类似于指向特定函数版本的指针。用户可以使用别名Amazon资源名称(ARN)访问函数版本。
引用:https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html
引自:https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html
嗯,说实话,这并没有什么特别重要的哈哈。
然而,重要的是在这里是用户可以使用别名 ARN 来访问函数的版本。
我理解的意思是,Lambda函数的版本是我想要使用的实体,但作为访问它的手段,我可以自己命名别名来使用〜。
因此,我认为使用别名的意图不是为了有效地管理自然数,而是为了给它们起一个容易理解的名字,这并不是一种奇怪的设计。6
这个方针的缺点
我认为基本上,我们的方针是在开发环境和生产环境中彻底分离资源,所以在安全和灵活性方面,在环境搭建方面几乎没有什么缺点。
如果要举一个例子的话,我认为必须要在团队内明确制定自己系统的版本管理方针。
在此次基本方针中涉及的代码示例中,直接在常量CURRENT_SYSTEM_VERSION中写入了版本号。
如果CDK的代码之外还有其他需要管理的东西,可能需要考虑参考这些东西。
或者,可能还有要求不仅保留当前版本,而且还要保留以前版本的情况。在这种情况下,我认为需要提前确定并管理如何保留版本历史记录的策略。
总结
总结起来,我认为最好的方法是,直接将Lambda函数分开,进一步说,甚至将其与AWS帐户分开。
我很好奇经常使用 AWS 的工程师们是如何管理这些事情的呢。如果您能告诉我,我将不胜感激。?♂️
感谢您耐心阅读我冗长的废话至此。