요즘에는 TLS 인증서 보호 없이 공용 API 엔드포인트가 있는 시스템을 상상하기 어렵습니다. 인증서를 발급하는 방법에는 여러 가지가 있습니다.
이 게시물에서는 AWS 서비스뿐만 아니라 AWS 내부에서 사용할 수 있는 무료 인증서에 대해 주로 논의하겠습니다. 분명히 관리형 AWS 서비스를 독점적으로 사용하고 엄격한 보안 요구 사항이 없다면 AWS Certificate Manager 이외의 다른 것을 사용하는 것은 의미가 없습니다. AWS Certificate Manager는 DNS 또는 HTTP 챌린지를 통해 인증서를 발급하는 매우 편리하고 빠른 방법을 제공합니다. 그러나 물리적 인증서 파일이 필요한 Nginx를 실행하는 EC2 인스턴스와 같이 AWS 서비스(API Gateway, ALB, NLB 등) 외부에서 이러한 인증서를 사용해야 하는 경우 기본적인 AWS 제한 사항에 직면하게 됩니다. 또한 요청하더라도 AWS Certificate Manager는 인증서 내용을 표시하지 않습니다.
이제 인증서 관리자보다 더 널리 사용되는 도구인 LetsEncrypt 에 대해 상기시켜 드리는 좋은 시간입니다. 적어도 이 도구는 클라우드에 의존하지 않기 때문입니다. 안타깝게도 AWS에는 내장된 LetsEncrypt 인증서 발급 기술이 없습니다. EC2 또는 ECS 서비스에 certbot 도구를 활용할 수 있지만 해당 시나리오에서는 갱신 프로세스를 구성하는 방법을 고려해야 합니다. 또한 전체 시스템 복잡성을 줄이기 때문에 모든 것에 대해 단일 절차를 갖는 것이 더 낫다고 생각하기 때문에 다양한 전략을 결합하고 싶지 않습니다.
이를 고려하여 복잡한 구성 없이 LetsEncrypt 인증서를 자동으로 발급하고 갱신하는 Lambda 함수를 만들었습니다. 인증서는 초기 인증서 발급 후 AWS Certificate Manager 인증서와 함께 ARN을 사용하는 모든 AWS 서비스에서 활용할 수 있습니다. 또한 Nginx를 실행하는 EC2 인스턴스이든 다른 위치이든 상관없이 원하는 위치에서 AWS Secrets Manager 에 보관된 물리적 인증서 버전을 사용할 수 있습니다.
이 문서에서는 DNS 영역이 AWS Route53에서 관리된다고 가정합니다.
이 글에서 설명하는 Lambda 함수는 Go v1.22에서 작성되었습니다. DNS 레코드, 비밀 또는 인증서와 같은 모든 결과 리소스는 기본적으로 Terraform 코드를 통해 생성되는 Amazon IAM 역할에 의해 제어됩니다. Lambda 작업의 순서는 다음과 같습니다.
aws_cloudwatch_event_target
통해 수행된 cron 실행의 결과일 수 있습니다. 이벤트 예시: { "domainName": "hackernoon.referrs.me", "acmeUrl": "prod", "acmeEmail": "[email protected]", "reImportThreshold": 10, "issueType": "default", "storeCertInSecretsManager" : true }
reImportThreshold
보다 적은 경우 LetsEncrypt DNS-01
챌린지를 시작합니다. 이 단계에는 Lambda가 도메인 이름과 AWS Route53 영역과 일치하는 TXT
레코드를 생성하고 인증서가 준비될 때까지 기다리는 작업이 포함됩니다.storeCertInSecretsManager
true인 경우 Lambda는 AWS Secrets Manager 내에 인증서 파일을 저장합니다.
코드
Lambda는 Go 1.22에서 작성되었습니다. 가능한 한 적은 수의 라이브러리를 사용하면 코드를 건조하게 유지하려는 목표를 유지하는 데 도움이 되었습니다. 필수 Go 라이브러리의 전체 목록:
URL | 설명 |
---|---|
Go 개발자가 AWS Lambda 기능을 개발하는 데 도움이 되는 라이브러리, 샘플 및 도구입니다. | |
Go 프로그래밍 언어용 AWS SDK. | |
LetsEncrypt/ACME 클라이언트 및 라이브러리. | |
Null 허용 값을 합리적으로 처리합니다. | |
Go를 위한 구조화된 플러그형 로깅. |
도커 이미지
기본 도커 이미지로 gcr.io/distroless/static:nonroot를 사용했습니다. libc가 필요하지 않은 Go 애플리케이션의 경우 이 이미지가 완벽합니다. scratch
부터 완전히 비어 있지 않으며 다음을 포함합니다.
빌드 프로세스
대규모 소프트웨어 프로젝트에서는 빌드 프로세스를 감독하는 것이 힘들고 시간이 많이 걸리는 일이 될 수 있습니다. 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 애플리케이션의 일반적인 설정을 사용했습니다.
지속적인 통합으로서의 GitHub Actions .
ghcr.io를 도커 레지스트리로 사용합니다. DockerHub와 비교하여 이 기능은 제가 선호하는 두 가지 주요 기능을 제공합니다.
kvendingoldo/semver-action : 자동 버전 관리를 위한 내 GitHub Actions 플러그인입니다. 리포지토리 커밋을 위해 SemVer 호환 태그를 생성하는 GitHub 작업입니다. 이 작업은 버전을 관리하고, GitHub 릴리스를 생성하고, 저장소 내부의 릴리스 분기를 제어할 수 있습니다. 단일 저장소와 단일 저장소 모두에서 훌륭하게 작동합니다.
자동 변경 로그 생성. 나는 변경 로그를 좋아합니다! 내가 관리하는 다른 OpenSource 프로젝트(예: https://github.com/tofuutils/tenv , https://github.com/tofuutils/tofuenv 등)의 맥락에서 우리 팀은 사용자에게 다음 사항을 알리는 것이 중요하다는 점을 인식했습니다. 변화.
golangci-lint . 내 관점에서는 모든 코드는 정적 코드 분석기로 검토되어야 합니다. SonarQube를 모든 프로젝트에 설정할 수는 없지만 중소 규모 Go 프로젝트에는 golangci만으로도 충분하다고 생각합니다.
이 페이지에서 설명하는 코드는 Terraform 및 OpenTofu와 동일하지만 Terraform v1.6부터 Hashicorp는 Terraform 라이선스를 Business Source License(BSL) v1.1로 수정했습니다. Terraform, 가능한 한 빨리 OpenTofu로 전환하세요.
여러 버전의 OpenTofu 또는 Terraform을 관리해야 하는 경우 다음을 사용하세요.
더 많은 Terraform/OpenTofu 예제는 Git 저장소 내의 예제 폴더에서 찾을 수 있습니다.
OpenTofu를 통해 AWS LetsEncrypt Lambda를 사용하려면 다음 단계를 수행해야 합니다.
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 }
변수 지정
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 } ] }
ecr_proxy_username
및 ecr_proxy_access_token
변수에 주의하세요. 기본적으로 AWS Lambda는 AWS ECR 이외의 소스에서 이미지를 가져올 수 없습니다. 다행히 AWS 팀은 DockerHub 또는 GHCR과 같이 공개적으로 사용 가능한 레지스트리에서 이미지를 가져와 ECR 내에 저장할 수 있는 ECR 프록시 캐시를 만들었습니다. 이러한 가능성에도 불구하고 AWS는 공개 공용 리포지토리에서도 토큰 없이 이미지를 가져오는 것을 허용하지 않습니다. 따라서 사전 구축된 Docker 이미지에 액세스하려면 개인 GitHub 토큰을 획득해야 합니다. 또는 내 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>" }
코드 변경이 완료되면 다음 명령을 실행하여 OpenTofu 버전 전환기 tenv 로 OpenTofu를 설치합니다.
$ tenv tofu install
마지막으로 다음 명령을 실행하여 생성된 코드를 적용합니다.
$ tofu init $ tofu plan $ tofu apply
코드가 AWS에 배포되고 이벤트가 트리거될 때까지 기다립니다. 몇 분 후에 인증서 관리자 내에 준비된 인증서가 표시됩니다. 예:
이제부터 AWS는 ARN이 제공하는 모든 서비스에서 발급된 인증서를 사용할 수 있습니다.
AWS 서비스 외부에서 인증서를 사용해야 하거나 해당 콘텐츠에 액세스해야 하는 경우 storeCertInSecretsManager
이벤트 옵션을 true
로 설정합니다. 이 상황에서 Lambda가 기본 실행을 완료하면 인증서가 AWS Secrets Manager에 저장됩니다. 이는 사용자에게 더 많은 유연성을 제공합니다. 인증서 내용을 검사하고 EC2에서 직접 작업할 수 있습니다. AWS Secrets Manager에 대해 자세히 알아보려면 공식 가이드를 읽어보세요.
이름 | 설명 | 가능한 값 | 기본값 | 예 | 필수의 |
---|---|---|---|---|---|
| 로그 포맷터 유형 | JSON | 텍스트 | 텍스트 | JSON | ❌ |
| 응용 프로그램 모드. AWS 실행을 위한 | 구름 | 현지의 | 구름 | 구름 | ✅ |
| 로깅 수준 | 패닉|치명적|오류|경고|정보|디버그|추적 | 경고하다 | 경고하다 | ❌ |
| 기본 AWS 지역. AWS에 배포한 후 자동으로 설정됩니다. | <유효한 AWS 리전> | - | 미국-동부-1 | ✅ |
| 인증서가 발급되거나 갱신되는 도메인 이름 | 유효한 도메인 이름 | - | 해커눈. 추천인.me | ✅ |
| 프로덕션 LetsEncrypt URL은 | 자극 | 단계 | 찌르다 | 찌르다 | ✅ |
| LetsEncrypt 인증서에 연결된 이메일 주소 | 유효한 이메일 | ✅ | ||
| TTL(Time to Live)이 | 모든 정수 > 0 | 10 | 10 | ✅ |
| | “사실” | "거짓" | "거짓" | "거짓" | ❌ |
aws-letsencrypt-lambda 작업 범위에서 때때로 로그를 검토하고 싶을 수도 있습니다. 달성하는 것은 매우 쉽습니다.
/aws/lambda/kvendingoldo-letsencrypt-lambda
입니다.
OpenTofu를 통해 생성된 Lambda 함수로 이동합니다. "테스트" 버튼을 클릭하세요.
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" }
실행이 완료될 때까지 기다리십시오. Cloudwatch에서 실행 로그를 확인할 수 있습니다. 일반적으로 초기 문제는 약 5분 정도 소요됩니다.
https://github.com/kvendingoldo/aws-letsencrypt-lambda 저장소를 노트북에 복제하세요.
공식 가이드를 통해 AWS Cli 자격 증명을 구성합니다.
환경 변수 섹션을 검토하고 필요한 최소 변수 수를 설정하십시오. 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"
다음 명령을 통해 로컬에서 람다를 실행합니다.
go run main.go
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
그게 다야. 이제부터 AWS는 발급된 인증서를 ARN이 제공하는 모든 서비스에서 또는 AWS Secrets Manager에서 발급받아 물리적으로 필요한 다른 위치에서 사용할 수 있습니다.
저는 거의 4년 동안 프로덕션 환경에서 Lambda 함수를 사용해 왔습니다. 수년에 걸쳐 초기 구현의 다양한 측면이 변경되었습니다.
이전에 AWS는 ECR이 아닌 레지스트리를 Lambda 소스로 사용하는 것을 금지했습니다. 변경되지 않았지만 AWS는 GitHub, DockerHub 및 몇 가지 추가 레지스트리에 대한 ECR 프록시를 추가했습니다. 이 기능이 없으면 Lambda 이미지를 개인 ECR에 수동으로 푸시하고 URL을 Terraform 코드의 이미지로 바꿔야 했습니다. 이제 OpenTofu 코드는 ECR 프록시를 통해 자동으로 이를 수행합니다.
처음에는 http-01
이나 tls-alpn-01
등 다양한 챌린지를 도입하는 것을 고려했지만 4년 동안 아무도 이에 대해 질문하지 않았습니다. GitHub 문제에는 여전히 존재하며 , 이 기능이 필요한 경우 함께 협력하여 만들 수 있습니다.
프로젝트가 처음 시작되었을 때 순수 EC2 인스턴스에서 LetsEncrypt 인증서를 활용하고 싶지 않았지만 요즘은 이것이 표준 관행입니다. 앞서 언급했듯이 특정 상황에서는 AWS cli를 사용하여 AWS Secrets Managed에서 인증서를 검색할 수 있습니다.
나는 수년에 걸쳐 새로운 Go 코드를 많이 작성했기 때문에 내 리포지토리에 있는 원래 Lambda 코드가 그다지 멋지지 않다는 것을 알 수 있습니다. 이 코드는 나의 가장 최근 Go 프로젝트인 tenv (Go로 작성된 OpenTofu, Terraform, Terragrunt 및 Atmos 버전 관리자)와 상당한 차이가 있지만 어쨌든 코드는 여전히 일반적으로 지원되므로 수정하는 것이 이겼습니다. 너무 문제가 되지 마세요. 때때로 코드를 더욱 우아하게 만들기 위해 상당한 리팩토링을 수행할 것입니다.
동일한 Lambda가 여러 프로젝트에서 수년간 사용되고 있습니다. 또한 저는 DevOps 플랫폼 cloudexpress.app 의 공동 창립자입니다. 여기서 우리 팀은 자동화 프로세스를 단순화하기 위해 AWS LetsEncrypt Lambda를 사용하여 모든 클라이언트에 대한 TLS 인증서를 관리합니다.
이제 숫자에 대해 이야기 해 봅시다. 4년 에 걸쳐 이 프로젝트는 많은 사람들에게 도움이 되었고 수많은 오픈소스와 30개 이상의 상업 프로젝트 에서 사용되었습니다. Lambda는 2000개 이상의 인증서를 발급하며 여기서 멈추지 않습니다.
AWS LetsEncrypt Lambda는 다음과 같은 경우에 적합한 솔루션입니다.
이러한 사항 중 하나 이상이 귀하의 상황에 적용된다는 사실을 발견했다면 AWS Lambda를 사용해 보십시오. 또한 개발에 참여하고 싶다면 GitHub에서 항상 새로운 이슈와 Pull Request를 받아보세요. 프로젝트 URL: https://github.com/kvendingoldo/aws-letsencrypt-lambda .