paint-brush
AWS LetsEncrypt Lambda 或为什么我使用 OpenTofu 和 Go 为 AWS 编写自定义 TLS 提供程序by@kvendingoldo
441
441

AWS LetsEncrypt Lambda 或为什么我使用 OpenTofu 和 Go 为 AWS 编写自定义 TLS 提供程序

Alexander Sharov15m2024/06/06
Read on Terminal Reader

AWS LetsEncrypt Lambda 是一款基于 Go 1.22 的应用程序,它通过 Let's Encrypt 颁发、更新和管理 AWS 证书。颁发证书后,它将证书存储在 AWS 证书管理器和 AWS Secret Manager 中。Lambda 完全可定制,可以通过 Terraform 或 OpenTofu 部署到 AWS。它也可以毫无问题地在本地执行。
featured image - AWS LetsEncrypt Lambda 或为什么我使用 OpenTofu 和 Go 为 AWS 编写自定义 TLS 提供程序
Alexander Sharov HackerNoon profile picture
0-item
1-item

如今,很难想象有公共 API 端点的系统没有 TLS 证书保护。颁发证书的方法有以下几种:


  • 可以从任何大型 TLS 提供商处购买付费通配符证书
  • 付费根证书,用于签署由企业 PKI 系统颁发的所有下游证书
  • 由 LetsEncrypt 或 AWS Certification Manager 等 TLS 提供商颁发的免费证书
  • OpenSSL或其他工具颁发的自签名证书


在这篇文章中,我将主要讨论可以在 AWS 内部使用的免费证书,但不限于 AWS 服务。显然,如果您只使用托管的 AWS 服务并且没有严格的安全要求,那么使用AWS 证书管理器以外的任何东西都是没有意义的。AWS 证书管理器提供了一种通过 DNS 或 HTTP 质询颁发证书的非常方便和快速的方法;但是,如果您需要在 AWS 服务(API 网关、ALB、NLB 等)之外使用这些证书,您将面临基本的 AWS 限制,例如运行需要物理证书文件的 Nginx 的 EC2 实例。此外,即使您请求它,AWS 证书管理器也不会显示证书内容。


此时,是时候提醒您有关LetsEncrypt的好时机了,这是一种比证书管理器使用更广泛的工具——至少因为它不依赖于云。不幸的是,AWS 中没有内置的 LetsEncrypt 证书颁发技术。您可以将 certbot 工具用于您的 EC2 或 ECS 服务,但在这种情况下,您需要考虑如何配置续订过程。我也不想结合不同的策略,因为我认为最好对所有事情都采用单一程序,因为这样可以降低整个系统的复杂性。


考虑到这一点,我创建了一个 Lambda 函数,它可以自动颁发和更新 LetsEncrypt 证书,而无需复杂的配置。在初始证书颁发后,可以使用 ARN 以及 AWS 证书管理器证书在任何 AWS 服务中使用该证书。此外,您可以在您选择的任何位置使用保存在AWS Secrets Manager中的物理证书版本,无论是运行 Nginx 的 EC2 实例还是其他地方。

AWS LetsEncrypt Lambda 如何工作

在本文中,我假设您的 DNS 区域由 AWS Route53 管理。

本文描述的 Lambda 函数基于 Go v1.22 编写。所有结果资源(例如 DNS 记录、机密或证书)均由 Amazon IAM 角色控制,该角色默认通过 Terraform 代码创建。Lambda 操作的顺序如下:


  1. 获取包含证书列表的事件。通常,此事件可能是手动执行的结果,或者通过aws_cloudwatch_event_target由 cron 执行的结果。事件示例:
 { "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default", "storeCertInSecretsManager" : true }
  1. 验证证书是否存在于 AWS 证书管理器中。如果是,请确认到期日期。
  2. 如果距离到期日的天数少于reImportThreshold ,则启动 LetsEncrypt DNS-01质询。此步骤涉及 Lambda 创建将域名与 AWS Route53 区域匹配的TXT记录并等待您的证书准备就绪。
  3. 当证书准备就绪时,Lambda 会在 AWS 证书管理器中更新证书。
  4. 如果storeCertInSecretsManager为真,Lambda 将会把证书文件存储在 AWS Secrets Manager 中。


AWS LetsEncrypt Lambda,序列图。

Lambda 实现细节

代码

Lambda 是用 Go 1.22 编写的。使用尽可能少的库有助于我保持代码简洁的目标。所需 Go 库的完整列表:

网址

描述

github.com/aws/aws-lambda-go

帮助 Go 开发人员开发 AWS Lambda 函数的库、示例和工具。

github.com/aws/aws-sdk-go-v2

适用于 Go 编程语言的 AWS SDK。

github.com/go-acme/lego

LetsEncrypt / ACME 客户端和库。

github.com/guregu/null

合理处理可空值。

github.com/sirupsen/logrus

适用于 Go 的结构化、可插入式日志记录。


Docker 映像

我使用gcr.io/distroless/static:nonroot作为基本 docker 镜像。对于不需要 libc 的 Go 应用程序来说,此镜像非常完美。它并非完全scratch ,包含以下内容:


  • CA 证书:无需从任何其他阶段复制它们。
  • /etc/passwd:包含用户和组,例如非root用户。
  • /tmp 文件夹。
  • tzdata:如果您想设置 UTC 以外的时区。


构建过程

在大型软件项目中,监督构建过程可能成为一项费力且耗时的苦差事。Makefile 可以帮助自动化和简化此过程,确保您的项目高效且一致地构建。因此,我更喜欢对我的所有 Golang 项目使用 Makefile。该文件很简单:


 ##@ General help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... ##@ Build build: fmt vet ## Build service binary. go build -o bin/lambda main.go run: vet ## Run service from your laptop. go run ./main.go ##@ Test lint: ## Run Go linter golangci-lint run ./... test: ## Run Go tests go test ./...


从 CICD 方面,我使用了 Go 应用程序的典型设置

  1. GitHub Actions作为持续集成。

  2. ghcr.io作为 docker 注册表。与 DockerHub 相比,它提供了两个关键功能,因此我更喜欢使用它:

    1. 由于 GHCR 与 GitHub Actions 的无缝集成,构建、测试和部署工作流程可以直接从 GitHub 存储库更轻松地实现自动化。这可以提高生产力并简化开发流程。
    2. GHCR 利用 GitHub 的权限模型,允许用户使用与代码存储库相同的团队和权限来管理对容器镜像的访问。这简化了用户管理并增强了安全性。
  3. kvendingoldo/semver-action :我的 GitHub Actions 插件,用于自动版本控制。这是一个 GitHub 操作,可为存储库提交生成与SemVer兼容的标签。该操作可以管理版本、生成 GitHub 版本,并控制存储库内的版本分支。它与 single 和 monorepos 配合得很好。

  4. 自动生成变更日志。我喜欢变更日志!在我管理的其他开源项目(例如https://github.com/tofuutils/tenvhttps://github.com/tofuutils/tofuenv等)中,我的团队认识到向用户告知变更的重要性。

  5. golangci-lint 。在我看来,所有代码都应该由静态代码分析器检查。SonarQube 无法为所有项目设置,但我认为 golangci 足以满足中小型 Go 项目的需求。

  6. 请参阅 codespell.yml除了检查代码之外,验证语法也是一个好主意,特别是当您有大量文档时。

如何通过 Terraform/OpenTofu 将 Lambda 部署到 AWS

本页讨论的代码对于 Terraform 和 OpenTofu 是相同的,但是从 Terraform v1.6 开始,Hashicorp 已将 Terraform 许可证修改为商业源许可证 (BSL) v1.1。如果您正在 Terraform 上开发商业产品,请尽快切换到 OpenTofu。

如果需要管理多个版本的 OpenTofu 或 Terraform,请使用滕夫- OpenTofu、Terraform、Terragrunt 和 Atmos 版本管理器,用 Go 编写。

可以在Git 存储库中的示例文件夹中找到更多 Terraform / OpenTofu 示例。

要通过 OpenTofu 使用 AWS LetsEncrypt Lambda,您需要执行以下步骤:

  1. 将模块添加到您的 OpenTofu / Terraform 代码中

    module "letsencrypt_lambda" { source = "[email protected]:kvendingoldo/aws-letsencrypt-lambda.git//files/terraform/module?ref=0.31.4" blank_name = "kvendingoldo-letsencrypt-lambda" tags = var.tags cron_schedule = var.letsencrypt_lambda_cron_schedule events = var.letsencrypt_lambda_events ecr_proxy_username = var.ecr_proxy_username ecr_proxy_access_token = var.ecr_proxy_access_token }
  2. 指定变量

    variable "tags" { default = { hackernoon : "demo" } } variable "ecr_proxy_username" { default = "kvendingoldo" } variable "ecr_proxy_access_token" { default = "ghp_xxx" } variable "letsencrypt_lambda_cron_schedule" { default = "rate(168 hours)" } variable "letsencrypt_lambda_events" { default = [ { "acmRegion" : "us-east-1", "route53Region" : "us-east-1", "domainName" : "hackernoon.referrs.me", "acmeUrl" : "stage", "acmeEmail" : "[email protected]", "reImportThreshold" : 100, "issueType" : "default", "storeCertInSecretsManager" : false } ] }
  3. 注意变量ecr_proxy_usernameecr_proxy_access_token 。默认情况下,AWS Lambda 无法从 AWS ECR 以外的来源提取图像。幸运的是,AWS 团队创建了 ECR 代理缓存,它可以从 DockerHub 或 GHCR 等公开可用的注册表中获取图像并将其存储在 ECR 中。尽管存在这种可能性,但 AWS 不允许在没有令牌的情况下提取图像,即使是从开放的公共存储库中提取图像,因此您必须获取个人 GitHub 令牌才能访问预先构建的 Docker 图像。或者,您可以提取我的 GitHub 存储库,在本地构建图像,然后将其上传到您预先存在的 ECR 存储库。在这种情况下,可以按如下方式修改示例:

     module "letsencrypt_lambda" { source = "../../" blank_name = "kvendingoldo-letsencrypt-lambda" tags = var.tags cron_schedule = var.letsencrypt_lambda_cron_schedule events = var.letsencrypt_lambda_events ecr_proxy_enabled = false ecr_image_uri = "<YOUR_ACCOUNT_ID>.dkr.ecr.us-east-2.amazonaws.com/aws_letsencrypt_lambda:<VERSION>" }


  4. 完成代码更改后,运行以下命令通过 OpenTofu 版本转换器tenv安装 OpenTofu:

     $ tenv tofu install
  5. 最后,执行以下命令来应用生成的代码:

     $ tofu init $ tofu plan $ tofu apply
  6. 等待代码部署到 AWS 并触发事件。几分钟后,您将在证书管理器中看到已准备好的证书。示例:

    AWS 证书管理器内部使用 AWS LetsEncrypt Lambda 颁发的证书列表。

  7. 从现在开始,AWS 可以在任何通过 ARN 提供的服务上使用颁发的证书。

  8. 如果您需要在 AWS 服务之外使用证书或访问其内容,请将storeCertInSecretsManager事件选项设置为true 。在这种情况下,当 Lambda 完成基本执行时,证书将保存在 AWS Secrets Manager 中。它为用户提供了更大的灵活性:他们可以检查证书的内容,直接从 EC2 使用它等。要了解有关 AWS Secrets Manager 的更多信息,请阅读官方指南。

    已颁发证书的示例,存储在 AWS Secrets Manager 内部。


环境变量

姓名

描述

可能值

默认值

例子

必需的

FORMATTER_TYPE

日志的格式化程序类型

JSON | 文本

文本

JSON

MODE

应用程序模式。设置cloud模式用于 AWS 执行,设置local模式用于本地测试。

云 | 本地

LOG_LEVEL

日志记录级别

恐慌|致命|错误|警告|信息|调试|跟踪

警告

警告

AWS_REGION

默认 AWS 区域。部署到 AWS 后,它会自动设置。

<任何有效的 AWS 区域>

-

美国东部-1

DOMAIN_NAME

正在颁发或续订证书的域名

任何有效的域名

-

hackernoon.referrs.me

ACME_URL

如果设置为prod ,则将使用生产 LetsEncrypt URL;如果没有,则将使用阶段 URL。

产品 | 阶段

产品

产品

ACME_EMAIL

与 LetsEncrypt 证书关联的电子邮件地址

任何有效的电子邮件

[email protected]

[email protected]

REIMPORT_THRESHOLD

如果证书的有效时间 (TTL) 等于REIMPORT_THRESHOLD ,则证书将被续订。

任何整数 > 0

10

10

STORE_CERT_IN_SECRETSMANAGER

如果为true ,Lambda 将在证书管理器和 Secrets Manager 中保留证书。

“真” | “假”

“错误的”

“错误的”


如何检查 LetsEncrypt Lambda 日志

在使用aws-letsencrypt-lambda的工作范围内,您可能偶尔想要查看日志。这很容易实现:

  1. 进入 AWS Cloudwatch,点击“日志组”
  2. 查找您在 OpenTofu 代码中指定的名称的日志组。例如,在我的情况下,它是/aws/lambda/kvendingoldo-letsencrypt-lambda
  3. 转到该组,从列表中选择所需的流,然后查看日志。


如何通过 AWS UI 手动触发 Lambda

  1. 转到通过 OpenTofu 创建的 Lambda 函数。单击“测试”按钮。

    AWS LetsEncrypt Lambda:UI 界面

  2. 填写Test Event ,点击Test

     { "domainName": "<YOUR_VALID_DOMAIN>", "acmeUrl": <stage | prod>, "acmeEmail": "<ANY_VALID_EMAIL>", "reImportThreshold": 10, "issueType": "<default | force>", "storeCertInSecretsManager" : <true | false> }

    示例 1:

     { "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default" }


  3. 等待执行完成。您可以在 Cloudwatch 中查看执行日志。通常初始问题大约需要 5 分钟。

如何在本地测试 Lambda

  1. https://github.com/kvendingoldo/aws-letsencrypt-lambda存储库克隆到您的笔记本电脑

  2. 通过官方指南配置 AWS Cli 凭证。

  3. 检查环境变量部分并设置所需的最小变量数。由于 LetsEncrypt 将限制ACME_URL="prod"每小时的重试次数,因此我建议使用ACME_URL="stage"进行测试。环境变量示例:

     export AWS_REGION="us-east-2" export MODE=local export DOMAIN_NAME="hackernoon.referrs.me" export ACME_URL="stage" export ACME_EMAIL="[email protected]" export REIMPORT_THRESHOLD=10 export ISSUE_TYPE="default" export STORE_CERT_IN_SECRETSMANAGER="true"
  4. 通过以下命令在本地执行 lambda:

     go run main.go
  5. Lambda 成功执行后,将出现以下日志。

     INFO[0000] Starting lambda execution ... INFO[0000] Lambda will use STAGING ACME URL; If you need to use PROD URL specify it via 'ACME_URL' or pass in event body INFO[0000] Certificate found, arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af. Trying to renew ... INFO[0000] Checking certificate for domain 'hackernoon.referrs.me' with arn 'arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af' INFO[0000] Certificate status is 'ISSUED' INFO[0000] Certificate in use by [] INFO[0000] Certificate valid until 2024-08-31 13:50:49 +0000 UTC (89 days left) INFO[0000] Try to get certificate for hackernoon.referrs.me domain 2024/06/02 17:56:23 [INFO] acme: Registering account for [email protected] 2024/06/02 17:56:24 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Obtaining bundled SAN certificate 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809394 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] AuthURL: https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/12603809404 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Could not find solver for: http-01 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: use dns-01 solver 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: tls-alpn-01 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: Could not find solver for: http-01 2024/06/02 17:56:25 [INFO] [www.hackernoon.referrs.me] acme: use dns-01 solver 2024/06/02 17:56:25 [INFO] [hackernoon.referrs.me] acme: Preparing to solve DNS-01 2024/06/02 17:56:26 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:00 [INFO] [www.hackernoon.referrs.me] acme: Preparing to solve DNS-01 2024/06/02 17:57:00 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Trying to solve DNS-01 2024/06/02 17:57:30 [INFO] [hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53] 2024/06/02 17:57:34 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s] 2024/06/02 17:57:46 [INFO] [hackernoon.referrs.me] The server validated our request 2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Trying to solve DNS-01 2024/06/02 17:57:46 [INFO] [www.hackernoon.referrs.me] acme: Checking DNS record propagation. [nameservers=109.122.99.130:53,109.122.99.129:53] 2024/06/02 17:57:50 [INFO] Wait for propagation [timeout: 5m0s, interval: 4s] 2024/06/02 17:58:30 [INFO] [www.hackernoon.referrs.me] The server validated our request 2024/06/02 17:58:30 [INFO] [hackernoon.referrs.me] acme: Cleaning DNS-01 challenge 2024/06/02 17:58:30 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:59:09 [INFO] [www.hackernoon.referrs.me] acme: Cleaning DNS-01 challenge 2024/06/02 17:59:09 [INFO] Wait for route53 [timeout: 5m0s, interval: 4s] 2024/06/02 17:59:43 [INFO] [hackernoon.referrs.me, www.hackernoon.referrs.me] acme: Validations succeeded; requesting certificates 2024/06/02 17:59:43 [INFO] Wait for certificate [timeout: 30s, interval: 500ms] 2024/06/02 17:59:45 [INFO] [hackernoon.referrs.me] Server responded with a certificate. INFO[0203] Certificate has been successfully imported. Arn is arn:aws:acm:us-east-2:004867756392:certificate/72f872fd-e577-43f4-ae38-6833962630af INFO[0204] Secret updated successfully. SecretId: arn:aws:secretsmanager:us-east-2:004867756392:secret:hackernoon.referrs.me-NioT77 INFO[0204] Lambda has been completed
  6. 就是这样。从现在开始,AWS 可以通过 ARN 在任何服务上使用颁发的证书,或者通过从 AWS Secrets Manager 获取证书,在物理上需要的其他位置使用颁发的证书。


在 AWS 拥有超过 4 年的实际使用经验

我在生产中使用 Lambda 函数已有近四年时间。多年来,初始实现的各个方面都发生了变化:

  1. 以前,AWS 禁止使用任何非 ECR 注册表作为 Lambda 源。这一规定没有改变,但 AWS 为 GitHub、DockerHub 和一些其他注册表添加了 ECR 代理。如果没有此功能,我们必须手动将 Lambda 镜像推送到我们自己的 ECR,并在 Terraform 代码中将 URL 替换为镜像。现在,OpenTofu 代码可以通过 ECR 代理自动执行此操作。

  2. 一开始我考虑过引入各种挑战,比如http-01tls-alpn-01 ,但四年来没有人质疑过我。它仍然存在于 GitHub 问题上,如果需要这种能力,我们可以一起创造它。

  3. 当项目最初启动时,我并不想在纯 EC2 实例上使用 LetsEncrypt 证书,但现在这已成为标准做法。正如我之前所说,在某些情况下,可以使用 AWS cli 从 AWS Secrets Managed 中检索证书。

  4. 这些年来,我编写了很多新的 Go 代码,所以我可以看出,我存储库中的原始 Lambda 代码并不像它应该的那样花哨。它与我最近的 Go 项目tenv (OpenTofu、Terraform、Terragrunt 和 Atmos 版本管理器,用 Go 编写)之间存在显著差异,但无论如何,该代码仍然受到普遍支持,因此对其进行修改不会有太大问题。偶尔,我会进行重大重构,使代码更优雅。

  5. 多年来,同一个 Lambda 已在多个不同的项目中使用。此外,我还是 DevOps 平台cloudexpress.app的联合创始人,我们的团队使用AWS LetsEncrypt Lambda为所有客户管理 TLS 证书,以简化自动化流程。


现在我们来谈谈数字。在4 年的时间里,这个项目帮助了很多人,并被用于众多开源项目和30 多个商业项目。Lambda 颁发了2000多份证书,并且并不想止步于此。

结论

如果您需要以下解决方案, AWS LetsEncrypt Lambda是适合您的解决方案:

  • 您必须拥有该证书的物理版本,并将从非 AWS 原生服务(例如 EC2 Nginx)中使用它。
  • 您不想依赖 AWS Certification Manager 来管理 TLS 证书颁发和续订过程(检查日志、设置续订日期等)。
  • 当您的证书过期或即将过期时,您希望收到来自 LetsEncrypt 的电子邮件通知。
  • 您想通过更改 Golang 代码来个性化解决方案(例如,更改 LetsEncrypt 挑战、将证书存储在 Hashicorp Vault 中等)。


如果您发现上述至少一点适用于您的情况,欢迎您使用 AWS Lambda。此外,如果您希望参与开发,我随时欢迎您在 GitHub 上提出新问题和 Pull 请求。项目网址: https://github.com/kvendingoldo/aws-letsencrypt-lambda