Search

Terraforming AWS 1 - Serverless Workshop

여러 이유로 시작한 Terraforming AWS라는 서비스 소개 그리고 첫번째 프로젝트인 Serverless Workshop를 진행하며 겪었던 교훈에 대한 이야기이다.

동기

AWS, 테라폼 둘 다 현재 회사에서 사용하는 기술이고 (DevOps로써 라고 하면… 좀 이상하지만)인프라를 구성할 때 de facto standard이다. 테라폼은 스터디를 하며 배우고 있다. AWS는 업무를 하다보니 관련된 EKS 쪽만 숙달이 되는 느낌이고 초보로서 어떻게 학습하지에 대한 막막함이 있었다(비교적 다른 엔지니어들의 기술 성숙도가 이미 높아서라고 생각한다). 무엇보다 AWS를 잘 모르는 상태에서 부분적인 테라포밍(import 지옥…?)을 하다보니 실력은 늘지 않고 헛수고 하는 느낌이 강했다
그러다 이번에 진행된 AWS Builders Korea Program 에서 Serverless Workshop이라는 Event Driven Architecture 토이프로젝트 만드는 핸즈온을 따라해 봤는데… 아니? 생각보다 쉬웠다(진행자도 200레벨이지만 쉽다곤 했다). 웹 콘솔에서 딸깍 딸깍하며, 이걸 테라폼으로 구성하기로 마음먹었다.

요구사항

워크샵이라는 이미 잘 만들어진 정답이 있다는 조건 때문에 학습 패턴도 쉽게 가져 갈 수 있었다.
커밋을 각 워크샵 페이지 한 장을 단위로 한다 → AWS 학습 측면에서 문서화
테라폼 코드에 대한 설명을 Pluralith 로 대체 → 테라폼 문서 만들기 간소화
테라폼 스터디 과제로 제출할 블로그 글이 필요하다. 이거 정리해서 내면 되겠다 ㅎㅎ.
위 두가지를 만족하면 (수정조차 되지 않은) ‘우리 아들’도 알아 볼 수 있는 학습 자료가 될 것이고 나중에 유명해져서 공유가 널리 되면 학습 서비스로써 충분치 않을까 생각해본다(최근 동료 분이 “우리 엄마도 알 수 있게 설명하자”를 모토로 문서화를 하신다. 음 저는…).
다음 세대에 남긴다는 생각으로 커밋 메세지를 작성

전략

리소스가 아닌 모듈을 사용한다.
실무에서 살짝 경험해보니, AWS 서비스에서 리소스는 아주 작은 조각이다. 리소스를 만든다고 콘솔로 구성한 서비스가 전부 나오지 않는다.
모듈은 AWS 서비스와 1:1 매칭할거라는 “기대감”이 있었다(복선이다. 아니다.)
구성해서 안되는 부분은 테라폼 리소스를 삭제하고 콘솔 구성 후 모듈 리소스에 하나씩 import 한다.
그럼에도 빠지는 리소스가 있을 수 있다. 그럴땐… 띵킹하자!

프로젝트1: Serverless Workshop

생각보다 오래 걸렸지만 일단 완성했다. 이미 워크샵에 가이드, 문서, 개념 등 학습할 내용은 전부 있지만 거기에 테라폼 HCL 구성을 더했다고 보면 된다.
최대한 워크샵 결과와 같게 구성했으나 다른 부분이 있다. 공통적으로 다른 부분은:
Cloud9은 쓰지 않았다.
IAM Role, Policy의 경우 각 테라폼 AWS 모듈에서 옵션(인자)으로 사용 가능한 것들은 최대한 활용했다.
모듈 코드가 Name 태그를 만들어주는 경우 그대로 썼다. 워크샵에선 붙이지 않지만 되려 좋은 습관이라 채택(오히려 좋아).
지금부터는 다른 점을 어떻게 구성했는지 진행하며 겪은 어려움과 나의 습득 교훈을 워크샵 페이지(커밋)별로 정리한다.

시작 전 준비사항 > 사전 환경 구성

여기서 하는 작업은, 나중에 사용할 서비스인 API Gateway → SQS 용도로 쓸 IAM Role이 있는데, 그걸 CloudFormation으로 만드는 작업을 한다. 나는 당연히 CloudFormation HCL로 transcript 될거라 믿고 있었고(워크샵 들을 때부터 짱구를 굴리고 있었다), 이 부분은 워크샵에서도 중요치 않게, 그냥 코드 올려서 실행하고 끝내서 IAM Role의 정책, 권한을 깊게 보진 않았다.
cf2tf <(wget -qO- https://static.us-east-1.prod.workshops.aws/public/ebdd2af6-e669-4dd3-99bd-c9de7921832e/assets/serverless-lab-cf.yaml)
Bash
복사
주석 내용 그대로 했다. CloudFormation HCL로 transcript 툴은 cf2tf를 사용했다(구글에서 더 상위 검색에 랭크된 cf-to-tf는 변환이 잘 안됐다). 다만 이걸로 변환하면 assume_role_policy 가 JSON이 아닌 HCL로 덩그러니 있어서 이 부분만 jsonencode 함수로 감싸준다.

간단한 배달 주문 서비스 만들기 > API Gateway 생성하기

하하! 너무 순조롭다. 라이브 세션 워크샵을 테라폼으로 해볼걸 그랬나? 라는 오만한 생각으로 시작했는데, 첫번째부터 난관이었다. apigateway-v2 모듈부터 원하는대로 생성이 되질 않았다.
module "delivery_api" { source = "terraform-aws-modules/apigateway-v2/aws" version = "~> 2.2.2" name = "delivery-api" }
Ruby
복사
1.
워크샵과 같이, 이름 외에 아무것도 안 주고 생성하려니 오류가 났다.
2.
워크샵처럼 스테이지를 추가할 수 있는 인자가 없다.
입벌려 테라폼 들어간닷!
먼저 1번 오류는 domain_name 과 cert(ACM)을 특정하지 않았다는 것인데 다음 이슈와 정확히 똑같다.
92
issues
그런데 이슈를 찾기 전에 문제를 해결해보려고 도메인을 사야하나, ACM도 사야하나… 그런 고민을 하다가 일단 workaround 하기 위해 API Gateway만 각 리소스로 구성했다.
아니 근데 글을 다시 정리하며 저 이슈에 달려 있는 답변을 보니… create_api_domain_name = false 를 주니 기대하는대로 동작한다!
여러분도 따봉과 하트가 없는 댓글 쓱 지나치죠….? ㅜㅜ
랜덤 ID의 API Gateway와 $default 스테이지가 있는, 콘솔에서 만드는 것과 같은 리소스들을 만든다. 따라서 도메인은 <random-id>.execute-api.<region-code>.amazonaws.com 로 자동 생성된다. 이 부분은 추후에 PR을 만들어서 수정해보겠다(역시 글로 정리해보길 잘했다).
하지만 2번째 문제, 모듈에서 별도의 스테이지를 추가할 수 없는건 기능적으로 없는 문제 같다. 이 부분은 나의 전략인, ‘모듈은 AWS 서비스와 1:1 매칭할거라는 “기대감”’으로 이슈를 만들어봤다.
어차피 스테이지를 위한, 모듈 외에, 리소스가 필요하고, 현재는 각각의 리소스로 구성했다.
이걸 하며 많이 헷갈렸던 것은 aws_api_gateway_* 라는 리소스들이 있고, aws_apigateway_v2_* 인 이 워크샵에 필요한 리소스들이 구분된다는 것이다
Note:
Amazon API Gateway Version 2 resources are used for creating and deploying WebSocket and HTTP APIs. To create and deploy REST APIs, use Amazon API Gateway Version 1 resources.
registry 각 리소스 문서마다에도 써 있다. 아마 AWS에서 이 서비스 버전 정책 그리고 레거시에 대한 하위호환성 지원 문제겠지. 이런건 AWS 히스토리를 잘 아는, 오래전부터 써오던 사람들이 유리할 것 같다.
별로 어려울 것 없었다. 아직 연결도 안했기 때문.
다만 이 때부터 제대로 모듈을 쓰면서, 모듈 추가 됐을 때 생기는 리소스들을 파악하고 싶었다. 그래서 사용한 명령은:
terraform plan -out=planfile && \ terraform show -no-color -json planfile | \ jq -r '.resource_changes[] | select(.change.actions[0]=="update" or .change.actions[0]=="create" or .change.actions[0]=="create") | .address'
Bash
복사
이 명령을 사용하면 plan의 stdout 이후에 생성되는 테라폼 리소스 참조들만 따로 나온다(어떤 alias, sub-command로 만들어야 쓰기 편할까 고민 중이다).
여기선 기존에 만든 람다 모듈 그대로 사용했을 시 We currently do not support adding policies for $LATEST. 라는 오류가 발생했다.
우선 람다 쪽에서 allowed_triggers 로 API GW의 접근을 허용해야 하지만, 게시(publish, deployment마다의 버저닝)를 활성화 안했을 때 즉, 최신 버전 ($LATEST)만 붙일 땐 정책 추가가 되지 않기 때문이다. 이슈에서 찾은 해결책 중 create_current_version_allowed_triggers = false 를 사용했다. 콘솔로 작업했을 때도 게시 버전 없이 $LATEST 를 사용했기 때문.
여기까진 보안이 얼마나 중요한지 몰랐으나, 앞으로 대부분 해결이 오래 걸린 문제들은 AWS 서비스끼리 연결 시 보안 관련한 문제였다.

비동기 처리 모델로 변경하기 > SQS 구성하기

정말 쉽다!
API GW → SQS → Lambda 순서로 연결한다. AWS 뿐만 아니라 클라우드 서비스로 무언갈 연결하는 것은 권한을 주어야 한다.
1.
Event source mapping에 연결할 람다 arn을 허용한다.
2.
앞서 CloudFormation으로 만들 수 있는 Role을 API GW에 붙여 SQS로 요청이 가능하게 해준다. SQS → 람다 연결을 위해 SQS Role에 AWSLambdaSQSQueueExecutionRole 라는 매니지드 정책을 붙인다.
module "order_function" { source = "terraform-aws-modules/lambda/aws" version = "~> 6.0.0" function_name = "OrderFunction" runtime = "nodejs16.x" create_current_version_allowed_triggers = false event_source_mapping = { # 1. Event source mapping to SQS sqs = { event_source_arn = module.order_queue.queue_arn } } attach_policy = true # 2. Attach the policy policy = "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" }
Ruby
복사
이제 다 뚫었으면 메세지 규약을 정의한다.
API 통합에서 이런 예제코드를 참고해서 작성한다. 특히 콘솔에서 Integration Type, Integration Action에 해당하는, integration_subtype 이 이런 상수 값일 줄을… 잘 상상이 안가기 때문에 테라폼을 빠르게 잘하려면 예제코드를 빨리 찾는것도 있다고 느껴진다. 그럼 나도 뿌려야지..
resource "aws_apigatewayv2_integration" "lambda_order_function" { api_id = aws_apigatewayv2_api.delivery_api.id credentials_arn = aws_iam_role.apigwdeliveryorderrole_f75_cf62_c.arn integration_type = "AWS_PROXY" integration_subtype = "SQS-SendMessage" # Type : SQS, Action: SendMessage request_parameters = { "QueueUrl" = module.order_queue.queue_id "MessageBody" = "$request.body.messageBody" } }
Ruby
복사
또 중요한 것. 기존 API GW - 람다 통합을 삭제 후 진행해야 되는데, aws_apigatewayv2_route 에서 통합 참조 때문에 코드를 바로 바꿔서 되지 않는다. 나는 따라서 aws_apigatewayv2_route/integration 둘 다 코멘트 아웃 후 apply(삭제) 후 SQS로의 통합으로 수정 후 만드는 식으로 진행했다.
SQS 모듈 이름을 moved 로 리팩토링 했다. 처음에 문서 코드를 복붙해서 sqs 로 잘못 넣었다. moved 를 사용하면 테라폼 리소스 이름만 변경할 수 있다.
SQS가 처리하지 못한 메세지를 담아두는 Dead-letter Queue(DLQ)를 만들고 테스트한다. 람다 함수 코드가 에러에 대응할 수 있도록 수정하고 배포한다. 점점 pluralith 그림이 알아보기 힘들게 바뀌어 간다 …ㅎㅎ
이젠 비동기처리를 넘어 EDA로 가기 위해, EventBridge를 추가한다.
module.order_event.aws_cloudwatch_event_bus.this[0] module.order_event.aws_cloudwatch_event_rule.this["DeliveryOrderRule"] module.order_event.aws_cloudwatch_event_target.this["OrderQueue"] module.order_event.aws_cloudwatch_event_target.this["order"]
Ruby
복사
EventBridge 모듈로 만들면 나오는 리소스 이름이 죄다 aws_cloudwatch_event_* 인데, 과거 CloudWatch 서비스에서 따로 떨어져 나온 듯 하다.
Note:
EventBridge was formerly known as CloudWatch Events. The functionality is identical.
1.
여기도 보안! SQS 연결을 위한 정책을 EventBridge Role에 붙인다.
2.
rulestargets 의 키 이름으로 매칭하여(DeliveryOrderRule) 룰의 이벤트 패턴과 대상을 특정한다. 대상은 간단하게 arn 만 적어주면 된다(=보안을 허용하는게 더 중요!).
module "order_event" { source = "terraform-aws-modules/eventbridge/aws" version = "~> 2.3.0" create_role = true bus_name = "OrderEventBus" append_rule_postfix = false attach_sqs_policy = true # 1. Attach the policy for SQS sqs_target_arns = [ module.order_queue.queue_arn ] rules = { DeliveryOrderRule = { # 2. Rule name event_pattern = jsonencode({ source = ["com.mycompany.order"], detail = { orderType = ["order-delivery"] } }) } } targets = { DeliveryOrderRule = [ # 2. Target name should match with Rule's { name = "order" arn = aws_cloudwatch_log_group.event_delivery_order.arn }, { name = "OrderQueue" arn = module.order_queue.queue_arn } ] } }
Ruby
복사
메세지 포맷도 바뀌기 때문에 람다 함수 코드도 수정하여 배포한다. 테스트 이벤트를 발생시켜 동작을 검증한다. CloudWatch 로그 그룹에 스트림이 생기는지 확인한다.
API GW → SQS 이던 통합을 API GW → EventBridge로 바꿔야 한다.
resource "aws_apigatewayv2_integration" "lambda_order_function" { api_id = aws_apigatewayv2_api.delivery_api.id credentials_arn = aws_iam_role.apigwdeliveryorderrole_f75_cf62_c.arn integration_type = "AWS_PROXY" integration_subtype = "EventBridge-PutEvents" request_parameters = { EventBusName = module.order_event.eventbridge_bus_name, Source = "com.mycompany.order", DetailType = "OrderType", Detail = "$request.body.messageBody", } }
Ruby
복사
이번에도 integration_subtyperequest_parameters 포맷을 알기 위해 모듈 EventBridge 예제 를 참고했다.
여기선 디버깅에 애를 많이 먹었는데, 메세지를 보내고 CloudWatch 로그 그룹에 항상 새 로그 스트림이 생기는게 아니라 기존 로그 스트림에 이어서 로그가 쓰이는 경우가 있어서 그랬다. 로그는 역시 ‘까’봐야한다
EDA로 아키텍처를 구성했다면, 애플리케이션의 다른 기능(이벤트)을 독립적인 구성으로 추가가 쉽다는 것이 장점이다. 따라서 다른 기능, SNS를 통한 메일 구독,을 새로운 이벤트인 PickupOrder로 추가한다.
SNS 메일 게시를 하는 예제는 위 글을 참고했다. 메일은 나의 메일로 바꾸고 만든다. 메일함에 온 구독 확인 메일의 링크를 눌러 구독을 활성화한다. 메일은 나중에 locals 로 빼는데 개인정보이니 코드 안에 두고 싶지 않다면 variable 로 구성하면 될 거 같다(내 메일은 뿌려져도 될 거 같아서…).
이제 중앙 이벤트 버스에 PickupOrder 기능을 연결하여 마무리한다.
1.
먼저 보안! EventBridge → SNS → 메일 게시(Publish)
앞의 EventBridge → SNS는 기존 SQS와 비슷하게 구성해서 쉽게할 수 있었다.
attach_sns_policy = true sns_target_arns = [ module.pickup_order_topic.topic_arn ]
Ruby
복사
하지만 SNS → Publish에 권한이 필요할 줄 몰랐다. 이 부분을 다음 과정으로 디버깅했다.
웹 콘솔에서 워크샵을 따라 이 부분을 구성한다.
내가 만든 테라폼 리소스에 import 후 plan 으로 diff를 찾는다
이 때 SNS Role에 다음 정책이 필요하다는걸 알게 됐다.
행동: sns:Publish
주체: AWS Service
actions = [ "sns:Publish" ] principals = [{ type = "Service" identifiers = [ "events.amazonaws.com" ] }
Ruby
복사
이 부분 코드를 추가해 diff 를 없애고 동작을 확인 → destroy 후 apply 하여 다시 확인(내가 만든 테라폼 코드의 멱등성을 검증?)
2.
이벤트 버스에서 규칙과 대상을 추가한다.

리소스 정리하기

언제든 tf destroy 한방이면 된다. IaC(테라폼)의 강력한 점이다. 위에서 자세히 쓰지 못했지만, 콘솔로 작업하고 diff를 확인하고 그런 작업을 꽤 많이 했다. 그럴 때마다 기존에 만든 리소스를 다 삭제하기 위해 destroy를 자주 사용했다. 이걸 다 손으로 했을 생각을 하니… 아찔하다
물론 워크샵을 마치고도 비용이 발생하지 않게 tf destroy 한방 날려주자.

습득 교훈

“클라우드는 보안이 전부다”라는 업계 선배이신 누군가의 말이 떠오른다. 쿠버네티스는 기본적으로 모든 허용에서 Zero Trust를 구성해야하지만, AWS는 서비스로써 그렇지 않고 보안이 촘촘하다고 느껴졌다.
짬을 무시 못한다. AWS 서비스의 역사와 경험이 쌓인 엔지니어는 위 내용 중 많은 것을 이미 알고 있었을 것이다. 빨리 따라가자.
API Gateway는 현재 V2이다.
EventBridge의 리소스들은 CloudWatch에 있었다.
위 AWS 서비스 모두 내가 처음 사용해본 것이다. 워크샵과 테라폼 덕분에 아주 빨리 배웠다고 할 수 있다. 둘의 형태는 조금 다르지만, 큰 틀에서 문서(=코드)화의 힘을 다시 알게 됐다.
다른 것보다 tf destroy 짱이다. 속이 다 시원 ㅎㅎ
Terraforming AWS를 ‘서비스’로써 발전시킬 방법을 모색하자.
소비자를 늘리자 → 커뮤니티 게시?
기여자(자발적 생산자)를 모으자 → 지인 콜드메일?(츄라이 츄라이)
문서화에 더 신경 쓰자
pluralith 이미지는 diff를 보여주는게 더 나을지도 모르겠다.
terraform-docs 사용해보기?
살아 있는 코드가 되게 CI에도 신경 쓰자
tflint