Terraform과 AWS Well-Architected Framework를 이용한 마이그레이션과 현대화

이준호(CTC)
Cloud Villains
Published in
51 min readApr 30, 2024

지난 4월 23일 AWS Partner Summit Seoul에서 ‘Well-Architected 에 기반하여 워크로드를 안전하게 마이그레이션하고 현대화하기’를 주제로 Hands On 세션이 진행되었습니다.

해당 세션에서는 AWS의 Partner들을 대상으로 HashiCorp Terraform을 기반의 안전하게 마이그레이션하고 보안, 거버넌스 관리의 표준을 운영 단계에서도 유지할 수 있는 방안을 소개하였습니다.

메가존클라우드의 DevOps Product Team은 해당 세션의 Hands-On 환경 구축 및 시나리오 구성을 지원하는 숨은 조력자로 참여하게 되었습니다.

이 글에서는 간략한 세션 내용과 Hands-On을 위해 준비한 내용 그리고 준비 과정에서 겪은 문제점과 트러블 슈팅 과정에 대해 설명하려고 합니다.

세션 내용

  • AWS 아키텍처를 구현하거나 마이그레이션하는 단계에서 부터 보안을 적용하는 시점과 방법을 결정하는 것부터 배포된 워크로드의 구성 변경에 대한 지속적인 보안 거버넌스를 유지하는 것은 어려운 과제입니다. 또한, 다양한 구성 요소에 대한 통합적인 보안 거버넌스를 적용하는 부분도 고려해야 합니다.
  • 이러한 어려움을 극복하기 위해 AWS Well-Architected Migration LensSecure Migration Framework을 사용할 수 있으며, Migration Security Requirements (MSR)HashiCorp 솔루션(Terraform)을 통합하여 보안 거버넌스를 강화하고 종합적인 보안 전략을 마련할 수 있습니다.

해당 세션의 키워드

AWS Well-Architected Migration Lens, Secure Migration Framework, Migration Security Requirements (MSR), HashiCorp 솔루션(Terraform)가 무엇인지?

그리고 해당 Framework & 솔루션을 사용하여 도대체 무엇을 할 수 있을지 궁금하실텐데요. 해당 키워드들에 대한 내용에 간략히 설명드리겠습니다.

AWS Well-Architected Migration Lens

  • 클라우드 마이그레이션 과정에서 잘 정립된 아키텍처 설계를 위한 AWS의 지침을 제공합니다.
  • 이는 AWS 마이그레이션의 3단계인 assess, mobilize, migrate and modernize의 세 단계에 걸쳐 운영 우수성, 보안, 신뢰성, 성능 효율성, 비용 최적화, 지속 가능성의 여섯 가지 원칙에 대한 베스트 프랙티스를 결합합니다.

Secure Migration Framework

  • 클라우드로의 전환을 계획할 때 중점적으로 고려해야 할 보안과 규정 준수에 대한 구체적인 지침을 담고 있으며, 클라우드 마이그레이션 동안에 기업의 보안 거버넌스를 강화하는 방법에 대한 인사이트를 제공합니다.
  • 이를 통해 기업 또는 조직이 언제 어떤 보안 조치를 취해야 하는지, 구성 변경 시 보안 거버넌스를 유지하는 방법, 그리고 여러 구성요소에 걸쳐 보안 거버넌스를 어떻게 적용해야 하는지에 대한 지침을 제공합니다. 기본적으로, 안전한 마이그레이션을 위한 핵심 프로세스의 로드맵을 설명하고 조직이 AWS 환경으로의 부드러운 전환을 도모할 수 있도록 하고있습니다.

Migration Security Requirements (MSR)

  • MSR은 마이그레이션 및 현대화 과정에서 안전한 클라우드 기반을 구축하기 위해 필요한 69까지 보안 및 규정 준수 확인 사항을 정의해놓은 것을 말합니다
  • 즉, Secure Migration에 진단해야할 보안항목을 69가지의 백데이터 정의해놓은 것으로 보실 수 있습니다.

AWS Service Screener

  • Service Screener는 AWS 환경에서 자동화된 검사를 실행하고 AWS 및 커뮤니티 모범 사례를 기반으로 권장 사항을 제공하는 도구입니다.
  • IAM, Logging and Monitoring, infrastructure security, data protection, and incident response 5가지 핵심 보안요구사항에 대해서 service screener v2를 통해 확인할 수 있습니다.
  • Screener를 통해 AWS에서 권장 사항을 사용하여 서비스 수준에서 보안, 안정성, 운영 우수성, 성능 효율성 및 비용 최적화를 개선할 수 있습니다.
  • Service Screener는 무료로 사용이 가능하며, MSR 프레임워크 기반으로 검사를 진행하여 MSR 리포트 출력 가능이 가능합니다.

HashiCorp Terraform

  • HashiCorp Terraform은 IaC 도구 중 하나로, 다양한 클라우드 서비스 및 온프레미스 인프라를 코드로 정의하고 관리할 수 있습니다. Terraform은 간결하고 직관적인 구문을 사용하여 인프라스트럭처를 관리하며, 다양한 프로바이더를 통해 AWS, Azure, Google Cloud Platform, VMware 등의 다양한 환경을 지원합니다.
  • HashiCorp에서는 Terraform의 운영 환경을 웹 콘솔로 제공하고, API 기능을 지원해주는 서비스인 HCP Terraform(SaaS)와 Terraform Enterprise(self-hosted) 모델을 제공하고있습니다.
  • Terraform Enterprise는 기업 수준의 Terraform 관리 플랫폼으로, 보안, 규정 준수, 협업 기능을 강화한 기능을 제공합니다. 테라폼 엔터프라이즈는 팀원 간의 협업을 촉진하고, 인프라스트럭처 변경 사항의 트래킹 및 자동화를 제공, PaC(Policy as code)를 통한 정책 및 거버넌스 확립, 상태 및 비용 최적화 등을 지원하고 있습니다.

Service Screener

  • 이번 세션의 Hands-On 내용은 아니었지만 Service Screener에 대해 테스트한 내용입니다.
  • 테스트를 위해 AWS 환경에 간단한 EC2와 EC2에 연결할 Security Group을 미리 생성해 놓았으며, Security Group은 inbound를 전부 열어 놓았습니다.
  • AWS에 해당 Security Group은 inbound Rule이 전부 열려 있다면 해당 시스템 악의적인 공격 뿐만 아니라 무작위 포트 스캔과 같은 자동화된 공격에서도 취약점을 노출 시킬 수 있으므로 보안적으로 굉장히 취약합니다.
  • Service Screener로 EC2 서비스에 대해서만 검사를 진행하였고 EC2의 분석 결과 중 Security Group에 대해서는 다음과 같이 확인해 볼 수 있습니다.
screener --regions ap-northeast-2 --services ec2
  • 위와 같은 명령어를 실행하면 ~/service-screener-v2/output.zip 파일로 생성됩니다. 해당 zip파일을 다운로드 받고 압축을 풀면 index.html 파일을 통해 다음과 같이 안전한 클라우드 기반을 구축하기 위해 필요한 69까지 보안 및 규정 준수 확인 사항에 위반되는 내용들을 확인 가능합니다.

Hands-On을 위해 준비내용

  • 세션의 내용을 보면 요점은 마이그레이션 및 현대화 단계에서나 클라우드 운영 단계에서 어떻게 지속적으로 보안 요구사항(MSR)을 만족하는 보안 수준을 유지할지입니다.
  • HashiCorp Terraform을 사용하게 되면 다음과 같이 MSR기준으로 마이그레이션하고 보안, 거버넌스 관리의 표준을 운영 단계에서 각각 보안 수준을 유지할 수 있습니다.
  • Terraform FDO는 23년 9월에 새롭게 GA 된 기능으로 Docker 또는 Kubernetes 환경에서 Terraform enterprise를 빠른 시간에 설치 및 시작할 수 있는 배포 옵션입니다.
  • 저희 DevOps Product 팀은 작년 10월 27일 DNB 고객사분들을 대상으로 Devops Quest 해결 방식 이벤트인 ‘MEGATHON(메가톤)’을 진행을 EKS에 올리는 Terraform FDO 설치 방식을 경험해봤기 때문에 세션의 Hand-On 환경은 SaaS(HCP Terraform)와 Self Hosted(Terraform Enterprise) 중에서, Kubernetes 환경에 배포하는 FDO 설치 방식으로 Terraform Enterprise를 활용하기로 결정하였습니다.
  • Hands-On을 위해 준비한 내용은 다음과 같습니다.

1) Terraform 으로 인프라 및 EKS Add-on 배포

  • AWS인프라(EKS, AWS RDS, AWS S3, AWS ElastiCache…)
  • EKS add-on (EBS CSI Driver, LoadBalancer Controller, Externer DNS)

2) Helm Chart 배포

  • Terraform Enterprise
  • Keycloak
  • prometheus-stack

3) Hand-On 시나리오 (aws workshop studio)

3–1) 컴퓨트 리소스의 attack surface 최소화하기 위해 보안팀과 조직의 컴플라이언스에 부합하는 이미지를 사용해서 성공적으로 EC2 Instance 를 프로비져닝

3–2) 코드화된 정책인 Sentinel 적용하여 보안표준 적용

  • A) security groups의 최소 권한 액세스를 Sentinel policy에 정의하여 cidr이 지나치게 관대하게 오픈되는 경우를 사전에 방지
  • B) ACL이 활성화되는 버킷을 사전에 방지하고 버킷에 공개적으로 액세스할 수 없도록 제한된 S3 버킷을 프로비져닝

3–3) Drift Detction 기능을 활용하여 인프라 상태 변경 확인 및 복구 자동화 구현

  • A) 사용자가 임의로 Security Group의 설정을 변경하였을 때 이를 감지하고 복구
  • B) CloudTrail의 로그 파일 무결성 검증 설정을 고의적으로 변경했을 때 Drift detection을 통해 이를 감지하고 복구

3–4) Continuous Validation 기능을 활용하여 프로비저닝 된 워크로드 상태 확인

  • Web Server 비정상 동작 시 Terraform continuous validation 기능을 통한 탐지

4) Load Test & 모니터링 테스트

  • 노드 할당 가능 용량과 요청 request 리소스를 표시 해주는 EKS Node Viewer 설치
  • 200 Workspace 부하 테스트 — Terraform apply Test
  • Grafana — TFE 용 대시보드 구축 및 모니터링

1. Terraform으로 인프라 및 EKS Add-on 배포

1–1) AWS인프라(EKS, AWS RDS, AWS S3, AWS ElastiCache…) 구축

  • Terraform Enterprise 환경은 해당 세션을 진행하신 AWS 직원분의 AWS 계정을 사용하였는데 직접 AWS UI Console에는 접속할 수 있는 권한을 받을 수 없었기에 Terraform Code를 통해 Terraform Enterprise환경 구축을 진행하였고 구축한 아키텍처는 다음과 같습니다.
  • 저희 DevOps Product 팀은 위와 같은 아키텍처로 Terraform Enterprise를 구현하기 위해 각 리소스들을 모듈화하여 관리 및 구축하고있습니다.
module "vpc" {
source = "../module/vpc"
vpc_cidr = local.vpc_cidr
company_name = local.company_name
server_env = local.server_env
}

module "subnets" {
source = "../module/subnets"
vpc_secondary_cidr = module.vpc.vpc_secondary_cidr
vpc_id = module.vpc.vpc_id

company_name = local.company_name
server_env = local.server_env
elb_cidr = local.elb_cidr
dmz_cidr = local.dmz_cidr
ap_cidr = local.ap_cidr
db_cidr = local.db_cidr
pod_cidr = local.pod_cidr
azs = local.azs
}

module "routingTable" {
source = "../module/routeTable"
vpc_id = module.vpc.vpc_id
igw = module.vpc.igw
dmz_subnet_id = module.subnets.dmz_subnet_id
elb_subnet_id = module.subnets.elb_subnet_id
ap_subnet_id = module.subnets.ap_subnet_id
pod_subnet_id = module.subnets.pod_subnet_id
db_subnet_id = module.subnets.db_subnet_id
natgw_az1 = module.gateway.natgw_az1
company_name = local.company_name
server_env = local.server_env
}

module "gateway" {
source = "../module/gw"
igw = module.vpc.igw
dmz_subnet_id = module.subnets.dmz_subnet_id
company_name = local.company_name
server_env = local.server_env
}

module "eks" {
source = "../module/eks"
ap_subnet_id = module.subnets.ap_subnet_id
dmz_subnet_id = module.subnets.dmz_subnet_id
dmz_subnet = module.subnets.dmz_subnet
vpc_id = module.vpc.vpc_id
node_instance_type = var.node_instance_type
company_name = local.company_name
server_env = local.server_env
}

module "elasticache" {
source = "../module/elasticache"
preferred_cache_cluster_azs = [var.azs.0, var.azs.1]
project_name = "dpt"
env = "common"
elasticache_redis_name = var.elasticache_redis_name
elasticache_redis_engine = var.elasticache_redis_engine
redis_description = "External Redis cluster for TFE Active/Active"
create_subnet_group = var.create_subnet_group
redis_subnet_ids = [module.subnets.ap_subnet_id[0], module.subnets.ap_subnet_id[1], module.subnets.ap_subnet_id[2]]
redis_engine_version = var.redis_engine_version
redis_port = var.redis_port
redis_parameter_group_name = var.redis_parameter_group_name
redis_node_type = var.redis_node_type
redis_enable_multi_az = var.redis_enable_multi_az
redis_enable_encryption_at_rest = var.redis_enable_encryption_at_rest
redis_kms_key_arn = module.s3.kms[0].arn
redis_enable_transit_encryption = var.redis_enable_transit_encryption
redis_password = var.redis_password
vpc_id = module.vpc.vpc_id
sg_csv = "./sg_rule/elasticache.csv"
}

module "rds-postgresql" {
source = "../module/rds"
create = var.rds_create
skip_final_snapshot = var.skip_final_snapshot
identifier = var.identifier
db_size = var.db_size
db_type = var.db_type
engine = var.engine
engine_version = var.engine_version
instance_class = var.instance_class
db_name = var.db_name
username = var.username
password = var.password
port = var.port
network_type = var.network_type
availability_zone = [var.azs.0, var.azs.1]
final_snapshot_identifier = var.final_snapshot_identifier
timezone = var.timezone
param_group = var.param_group
cidr_block = var.cidr_block
db_subnet_id = module.subnets.ap_subnet_id
project_name = var.project_name
env = var.env
vpc_id = module.vpc.vpc_id
}

module "s3" {
source = "../module/s3"
env = var.s3_env
project_name = var.s3_project_name
s3_name = var.s3_name
s3_acl = var.s3_acl
s3_ownership = var.s3_ownership
s3_block_public_acls = var.s3_block_public_acls
s3_block_public_policy = var.s3_block_public_policy
s3_ignore_public_acls = var.s3_ignore_public_acls
s3_restrict_public_buckets = var.s3_restrict_public_buckets
s3_versioning = var.s3_versioning
create_kms_for_s3 = var.create_kms_for_s3
kms_alias = var.kms_alias
kms_key_arn = var.kms_key_arn
s3_sse_algorithm = var.s3_sse_algorithm
accountID = local.accountID
principals_identifiers_for_s3_policy = ["${module.eks.worker_ng_role_arn}"]
}

1–2) EKS add-on (EBS CSI Driver, LoadBalancer Controller, Externer DNS)

  • EBS CSI Driver는 Kubernetes 클러스터에서 Amazon Elastic Block Store (EBS) 볼륨을 사용할 수 있도록 하는 Kubernetes CSI(컨테이너 스토리지 인터페이스) 드라이버입니다. 이 드라이버를 사용하면 Kubernetes 클러스터 내에서 EBS 볼륨을 동적으로 프로비저닝하고 관리할 수 있습니다.
  • Terraform Enterprise의 경우 RDS, S3등의 External Service를 사용했기 때문에 따로 EBS CSI Driver가 필요하진 않았지만, Keycloak, prometheus-stack을 위해 EBS CSI Driver를 설치하였습니다.
##### ebs csi driver ######
###########################
resource "kubernetes_service_account" "ebs_csi_service_account" {
metadata {
name = "ebs-csi-controller-sa"
namespace = "kube-system"
labels = {
"app.kubernetes.io/name" = "aws-ebs-csi-driver"
}
annotations = {
"eks.amazonaws.com/role-arn" = module.eks.ebs_csi_driver_role_arn
}
}
depends_on = [module.eks]
}

resource "helm_release" "aws_ebs_csi_driver" {
name = "aws-ebs-csi-driver"

repository = "https://kubernetes-sigs.github.io/aws-ebs-csi-driver"
chart = "aws-ebs-csi-driver"
namespace = "kube-system"
version = "2.24.0"

set {
name = "controller.serviceAccount.create"
value = "false"
}
set {
name = "controller.serviceAccount.name"
value = "ebs-csi-controller-sa"
}

values = [
templatefile("${path.module}/custom-yaml/ebs_csi_values.yaml",
{
ebs-csi-controller-role-arn = module.eks.ebs_csi_driver_role_arn,
}
)
]

depends_on = [
kubernetes_service_account.ebs_csi_service_account,
module.eks
]
}
  • LoadBalancer Controller는 Kubernetes 클러스터에서 외부 트래픽을 내부 서비스로 라우팅하기 위해 클라우드 프로바이더의 로드 밸런서를 관리하는 Kubernetes 컨트롤러입니다. AWS에서는 Amazon ELB(Application Load Balancer, Network Load Balancer, Classic Load Balancer)를 관리하는 데 사용됩니다.
##### LoadBalancer Controller ######
####################################
resource "helm_release" "helm_AWSLoadBalancerController" {
name = "aws-load-balancer-controller"

repository = "https://aws.github.io/eks-charts"
chart = "aws-load-balancer-controller"
version = "1.6.2" # https://github.com/aws/eks-charts/tree/gh-pages

namespace = "kube-system"

set {
name = "image.repository"
value = "602401143452.dkr.ecr.us-east-1.amazonaws.com/amazon/aws-load-balancer-controller"
}

set {
name = "serviceAccount.create"
value = "true"
}

set {
name = "serviceAccount.name"
value = "aws-load-balancer-controller"
}

set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = module.eks.lb_controller_role_arn
}

set {
name = "vpcId"
value = module.vpc.vpc_id
}

set {
name = "region"
value = "ap-northeast-2"
}

set {
name = "clusterName"
value = module.eks.eks_name
}

depends_on = [module.eks]
}
  • External DNS는 Kubernetes 클러스터에서 서비스 및 인그레스 리소스와 외부 도메인 간의 매핑을 자동으로 관리하는 도구입니다. Kubernetes 리소스의 변경 사항에 따라 외부 DNS 레코드를 자동으로 업데이트하여 DNS 이름을 동적으로 할당합니다.
########## External DNS ###########
###################################
resource "kubernetes_service_account" "external_dns_service_account" {
metadata {
name = "external-dns"
namespace = "kube-system"
labels = {
"app.kubernetes.io/name" = "external-dns-controller"
}
annotations = {
"eks.amazonaws.com/role-arn" = module.eks.external_dns_role_arn
}
}
depends_on = [module.eks]
}

resource "helm_release" "external_dns" {
name = "external-dns"
repository = "https://kubernetes-sigs.github.io/external-dns"
chart = "external-dns"
namespace = "kube-system"

set {
name = "serviceAccount.create"
value = "false"
}

set {
name = "serviceAccount.name"
value = "external-dns"
}

values = [
templatefile("${path.module}/custom-yaml/external_dns_values.yaml",
{
txtOwnerId = "{hostzone id}",
domainFilters = "secureaws.net",
}
)
]

depends_on = [
kubernetes_service_account.external_dns_service_account,
module.eks
]
}
  • Terraform Enterprise를 위해 External Service로 OBJECT_STORAGE_S3_BUCKET을 사용하였으며 S3에는 Terraform 이 관리하는 State file, Module 등의 저장소 역할을 하게 됩니다.
  • AWS Key Management Service(KMS)를 사용하여 저장 데이터를 암호화하도록 하여 S3에 저장된 데이터가 KMS에서 관리하는 키를 사용하여 암호화되어 무단 액세스로부터 저장된 데이터를 보호함으로써 보안 계층이 추가하였습니다.
  • EKS에 배포된 Terraform Enterprise 서버가 S3에 데이터를 저장하고 읽어가기 위해서는 ‘kms:GenerateDataKey’, ‘kms:Decrypt’, ‘s3:*’ 와 같은 권한이 필요하기에 노드 그룹의 Role해 해당 권한을 추가해주었습니다.
# Terraform Enterprise의 KMS, S3에 대한 Policy 추가
data "aws_iam_policy_document" "tf_eks_fdo_for_policy" {
statement {
effect = "Allow"
actions = ["kms:GenerateDataKey", "kms:Decrypt", "s3:*"]
resources = ["arn:aws:kms:*:${var.accountID}:key/*", "arn:aws:s3:::*"]
}
}

resource "aws_iam_policy" "tf_eks_fdo_for_policy" {
name = "tf_eks_fdo_for_policy"
description = "tf_eks_fdo_for_policy"
policy = data.aws_iam_policy_document.tf_eks_fdo_for_policy.json
}

resource "aws_iam_role_policy_attachment" "eksng_s3_kmsrole" {
role = module.eks.worker_ng_role_name
policy_arn = aws_iam_policy.tf_eks_fdo_for_policy.arn
}
  • Terraform Enterprise에서는 Cost Estimation라는 기능이 있어서 예상 비용이 Plan과 Apply 사이의 추가 실행 단계로 실행 UI에 표시하여 확인이 가능합니다.
  • EKS에 배포된 Terraform Enterprise에서 해당 기능을 활성화하고 사용하기 위해서는 pricing:* 권한이 필요하기에 노드그룹의 Role에 해당 권한을 추가해주었습니다.
# Terraform Enterprise의 Cost Estimation을 위한 Policy 추가
data "aws_iam_policy_document" "tf_eks_fdo_for_cost_estimation_policy" {
statement {
effect = "Allow"
actions = ["pricing:*"]
resources = ["*"]
}
}

resource "aws_iam_policy" "tf_eks_fdo_for_cost_estimation_policy" {
name = "tf_eks_fdo_for_cost_estimation_policy"
description = "tf_eks_fdo_for_cost_estimation_policy"
policy = data.aws_iam_policy_document.tf_eks_fdo_for_cost_estimation_policy.json
}

resource "aws_iam_role_policy_attachment" "eksng_pricing_role" {
role = module.eks.worker_ng_role_name
policy_arn = aws_iam_policy.tf_eks_fdo_for_cost_estimation_policy.arn
}

2. Helm Chart 로 애플리케이션 배포

2–1) Terraform Enterpirse

  • Terraform Enterprise의 이미지를 다운로드 받기 위해서는 License key가 필요합니다.
  • 해당 이미지를 다운로드 받기 위해 다음과 같이 Image pull을 위한 Secret을 생성한 후 Helm install을 통해 Enterprise를 설치할 수 있습니다.
# namespace 생성
kubectl create ns tfe

# image pull을 위한 secret 생성
kubectl create secret docker-registry terraform-enterprise \
--docker-server=images.releases.hashicorp.com \
--docker-username=terraform \
--docker-password=$(cat license.txt) \
-n tfe

# Add the Hashicorp helm registry
helm repo add hashicorp https://helm.releases.hashicorp.com

# TFE helm install
helm install terraform-enterprise \
-f ./override-tfe.yaml hashicorp/terraform-enterprise \
-n tfe
  • 기본 설치가 아닌 저희는 Terraform Enterprise 를 RDS, ElastiCache, S3, KMS등과의 연동이 필요하므로 기본 value yaml 파일을 커스텀하여 설치를 진행하였습니다.
tfe:
metrics:
enable: true
httpPort: 9090
httpsPort: 9091
privateHttpPort: 8080
privateHttpsPort: 8443

env:
secrets:
TFE_DATABASE_PASSWORD: {}
TFE_ENCRYPTION_PASSWORD: {}
TFE_LICENSE: {}
TFE_OBJECT_STORAGE_S3_SERVER_SIDE_ENCRYPTION_KMS_KEY_ID: {}
TFE_REDIS_PASSWORD: {}
variables:
TFE_CAPACITY_CONCURRENCY: 100
TFE_DATABASE_HOST: {}
TFE_DATABASE_NAME: {}
TFE_DATABASE_PARAMETERS: sslmode=require
TFE_DATABASE_USER: tfe
TFE_HOSTNAME: hashicorp.secureaws.net
TFE_IACT_SUBNETS: 0.0.0.0/0
TFE_OBJECT_STORAGE_S3_BUCKET: {}
TFE_OBJECT_STORAGE_S3_ENDPOINT: https://s3.ap-northeast-2.amazonaws.com
TFE_OBJECT_STORAGE_S3_REGION: ap-northeast-2
TFE_OBJECT_STORAGE_S3_SERVER_SIDE_ENCRYPTION: aws:kms
TFE_OBJECT_STORAGE_S3_USE_INSTANCE_PROFILE: "true"
TFE_OBJECT_STORAGE_TYPE: s3
TFE_REDIS_HOST: {}
TFE_REDIS_USE_AUTH: "true"
TFE_REDIS_USE_TLS: "true"
image:
name: hashicorp/terraform-enterprise
repository: images.releases.hashicorp.com
tag: v202402-1
replicaCount: 5
service:
type: ClusterIP
tls:
caCertData: {}
certData: {}
keyData: {}
  • Terraform Enterprise의 외부 접속할 수 있는 Endpoint로 LB가 필요하기에 다음과 같이 Ingress를 통해 ALB를 생성하였습니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: tfe
name: hashicorp-tfe
annotations:
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS": 443}]'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/subnets: {}
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/backend-protocol: 'HTTPS'
alb.ingress.kubernetes.io/group.name: hashicorp
alb.ingress.kubernetes.io/group.order: '1'
alb.ingress.kubernetes.io/certificate-arn: {}
external-dns.alpha.kubernetes.io/hostname: hashicorp.secureaws.net
spec:
ingressClassName: alb
rules:
- host: hashicorp.secureaws.net
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: terraform-enterprise
port:
number: 443

2–2) Keycloak

  • Keycloak은 ID 및 액세스 관리 솔루션을 제공하는 오픈소스로 다양한 인증 및 권한 부여 기능을 제공하여 사용자의 로그인 및 액세스를 중앙 집중식으로 관리할 수 있습니다.
  • 저희는 Keycloak을 Terraform Enterpise와 SAML연동을 하여 사용자들이 발급 받은 ID를 통해 쉽게 Terraform Enterprise에 접속할 수 있도록 하기 위해 사용하였습니다.
# Add the bitnami helm registry
helm repo add bitnami https://charts.bitnami.com/bitnami

helm pull bitnami/keycloak --version 20.0.1 --untar

# Keycloak helm install
helm upgrade --install keycloak -f keycloak-values.yaml .
  • keyclock은 Terraform Enterprise와 동일한 LB를 사용하고 별도 리스너로 등록이 필요한 대상 그룹마다 Ingress를 생성하되 각 Ingress를 그루핑 할 수 있는 어노테이션을 추가하여 사용하였습니다.
  • Keycloak Helm Chart의 postgresql.architecture 옵션은 PostgreSQL 데이터베이스를 설치할 때 사용할 아키텍처를 지정하는 데 사용됩니다. 여기서 standalone 및 replication 두 가지 옵션을 선택할 수 있습니다.
  • 많은 참가자들이 동시에 Terraform Enterprise의 keyclock saml을 통해 로그인 시 읽기 전용 슬레이브 노드를 사용하여 읽기 부하를 분산시키고 마스터 노드의 부하를 줄일 수 있도록 replication 옵션을 사용하였습니다.
auth:
adminPassword: {password}
adminUser: {username}
ingress:
annotations:
alb.ingress.kubernetes.io/certificate-arn: {}
alb.ingress.kubernetes.io/group.name: hashicorp
alb.ingress.kubernetes.io/group.order: "2"
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/subnets: {}
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/target-group-attributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=60
external-dns.alpha.kubernetes.io/hostname: keycloak.secureaws.net
enabled: true
hostname: keycloak.secureaws.net
ingressClassName: alb
pathType: Prefix
postgresql:
architecture: replication
auth:
database: db_keycloak
existingSecret: ""
password: {password}
postgresPassword: ""
username: {username}
enabled: true
proxy: edge
service:
http:
enabled: true
ports:
http: 80
https: 443
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 300
externalTrafficPolicy: Local
tls:
autoGenerated: false
enabled: false

3. Terraform Enterprise 부하 테스트

  • 부하 테스트를 설명하기 전에, 보다 나은 이해를 위해 Terraform Enterprise의 Workspace와 EKS에서의 Terraform Enterprise의 동작 방식에 대해 먼저 설명드리겠습니다.
  • Terraform Enterprise의 Workspace란?
  • Terraform Enterprise의 Workspace는 Terraform 구성 파일 및 상태를 관리하는 공간입니다. 이는 클라우드 기반의 서비스로서 Terraform 코드를 관리하고 실행하는 데 중요한 역할을 합니다.
  • 즉, Terraform이 실행되는 독립적인 작업공간이며, 설정된 backend에 workspace에 state를 저장하고 하나의 state는 하나의 workspace로 관리할 수 있습니다.
  • Terraform Enterprise의 동작 방식
  • EKS와 같은 쿠버네티스 환경에서 Terraform Enterprise를 설치하게 되면 Workspace에서 테라폼이 동작할 때, tfe-agents 네임스페이스에 agent 파드들이 생기게 됩니다.
  • 이때, 생성되는 agent 파드들이 테라폼을 실행하는 주체입니다. 해당 agent 파드들은 Workspace가 실행될 때 생기며, Apply가 모두 완료되었다면 종료됩니다.
  • 해당 세션에 참가자 200명을 최대 인원으로 추산하여 그들이 Terraform workspace를 동시에 배포하는 시나리오 코드를 구현하였으며, 저희가 부하테스트 중 확인한 리소스들은 아래와 같이 액셀로 정리하며 진행하였습니다.
  • 아래 예시 코드는 참가자 200명이 동시에 Terraform workspace를 배포하는 시나리오를 구현한 부하 테스트 코드입니다.
# ---------------- vcs 연동 ---------------- #
# ----------------------------------------- #
resource "tfe_oauth_client" "vcs_load" {
name = "github_load"
organization = data.tfe_organization.org.name
api_url = "https://api.github.com"
http_url = "https://github.com"
oauth_token = var.github_test_api_token
service_provider = "github"
}

variable "github_test_api_token" {
default = "~~~"
}


# ---------------- varset 생성---------------- #
# ------------------------------------------- #

resource "tfe_variable_set" "load_poc_var_set" {
name = "PoC Varset"
organization = data.tfe_organization.org.name
}

resource "tfe_project_variable_set" "load_proj_var_set" {
variable_set_id = tfe_variable_set.load_poc_var_set.id
project_id = tfe_project.load_project.id
}

resource "tfe_variable" "load_access_key" {
key = "AWS_ACCESS_KEY_ID"
value = "~~~"
category = "env"
description = "AWS ACCESS KEY를 입력하세요"
variable_set_id = tfe_variable_set.load_poc_var_set.id
}

resource "tfe_variable" "load_secret_key" {
key = "AWS_SECRET_ACCESS_KEY"
value = "~~~"
category = "env"
description = "AWS SECRET KEY를 입력하세요"
variable_set_id = tfe_variable_set.load_poc_var_set.id
}


# ---------------- project 생성---------------- #
# -------------------------------------------- #

resource "tfe_project" "load_project" {
name = "load_test"
organization = data.tfe_organization.org.name
}


# ---------------- default_execution_mode 설정 ---------------- #
# ------------------------------------------------------------ #

resource "tfe_organization_default_settings" "load_org_default" {
organization = data.tfe_organization.org.name
default_execution_mode = "remote"
# default_agent_pool_id = tfe_agent_pool.my_agents.id
}


# ---------------- load test workspace 생성 ---------------- #
# --------------------------------------------------------- #

resource "tfe_workspace" "test_workspace" {
count = 100
name = "test_workspace_${count.index}"
organization = data.tfe_organization.org.name
project_id = tfe_project.load_project.id
vcs_repo {
identifier = "mzc-dpt/aws-ps-loadtest-ws"
oauth_token_id = tfe_oauth_client.vcs_load.oauth_token_id
}
force_delete = true
auto_apply = true
queue_all_runs = true
trigger_patterns = ["/*"]
working_directory = "/"
global_remote_state = true
assessments_enabled = true
}

resource "tfe_workspace" "test_workspace_japan" {
count = 100
name = "test_workspace_japan_${count.index}"
organization = data.tfe_organization.org.name
project_id = tfe_project.load_project.id
vcs_repo {
identifier = "mzc-dpt/aws-ps-loadtest-japan-ws"
oauth_token_id = tfe_oauth_client.vcs_load.oauth_token_id
}
force_delete = true
auto_apply = true
queue_all_runs = true
trigger_patterns = ["/*"]
working_directory = "/"
global_remote_state = true
assessments_enabled = true
}
  • Terraform Enterprise FDO(EKS)에서는 기본적으로 workspace가 Plan, Apply 같은 동작을할 때 tfe-agent 라는 네임스페이스에서 agent 파드가 생성되어 해당 작업을 진행하게 되며, agent 파드는 cpu, memory request를 물고 들어오게 됩니다.
  • 해당 agent pod는 Memory Request의 기본값이 2048이 되어 있습니다. 해당 값이 너무 높은 것 같아 하시코프 측에 문의를 해본 결과 하시코프 측 Terraform Enterprise 엔지니어의 FDO를 설계 당시 충분한 테스트 후 나온 최적의 세팅값이다 라는 답변을 받았고, 저희는 Memory Request를 수정하는 것이 아닌 워커 노드를 스케일 아웃(Scale out)하여 부하 테스트를 진행하였습니다.

4. Hand-on 준비 과정에서 겪은 몇 가지 문제점과 트러블 슈팅

4–1) Terraform Enterprise(TFE)의 Workspace 생성 실패

  • 세션의 참가자가 최대 200명으로 추산하여 저희는 한명한 TFE의 Organization을 제공하고, 참가자가 제공 받은 Organization에는 Hand-on 실습이 가능한 6가지 시나리오 Workspace를 세팅해야 했습니다.

문제

  • 한명당 6개의 workspace이므로 200명 기준으로 저희는 총 1200개의 workspace를 세팅해놔야했는데 절반 가량의 workspace가 계속 생성되지 않는 문제가 발생했습니다.

트러블 슈팅

  • 로그를 분석한 끝에 DB 쪽 Connection이 실패하는 로그를 확인했고, 해당 RDS의 DB max connection이 몇인지 확인을 진행하였습니다.
psql --set=sslmode=require -h {rds_url} -U tfe -d tfepg 
show max_connections;
SELECT count(*) FROM pg_stat_activity WHERE datname = 'tfepg' AND usename = 'tfe';
  • 현재 DB의 최대 max connection 수를 확인하고 현재 connection이 맺어져있는 수를 비교해본 후 workspace 생성시 DB connection이 맺어지는지를 확인한 결과 workspace가 생성될 때 DB Connection이 맺어지는 것을 확인할 수 있었습니다.
  • AWS RDS는 Instance Type에 따라 max connection 수가 다르며, 기본적으로 t3.small 로 테스트 시 180개 가량의 connection pool 을 맺을수 있으며, 200개 workspace를 동시에 돌리기 위해서는 더 큰 db 인스턴스 타입이 필요하다는 것을 확인 후 RDS의 Scale Up을 진행하였습니다.

4–2) Terraform을 통한 RDS의 Instance Type 변경 .. 착각

  • RDS의 max connection를 늘리기 위해 Instance Type을 Scale Up하는 Terraform 코드를 적용하여 Apply를 진행했습니다.

문제

  • Apply가 즉시 적용되었다고 생각하여 부하 테스트를 시작했지만, 계속해서 TFE 로그에는 DB Connection 오류가 나타났습니다. AWS 콘솔에 접속할 수 없어 CLI를 통해 현재 RDS 인스턴스 유형을 확인한 결과, 여전히 t3.small 인스턴스인 것을 확인했습니다.
  • 이러한 문제의 원인은 Terraform 코드에 Apply immediately 설정이 기본값 false로 설정되어 있었습니다….

트러블 슈팅

  • AWS RDS는 기본적으로 DB 인스턴스를 수정 후 변경 사항을 즉시 적용하려면 “Apply immediately”를 선택하여 적용할 수 있습니다.
  • 그러나 Terraform 코드로 설정된 Apply immediately 설정이 기본값으로 false로 동작하여 변경 사항이 즉시 적용되지 않았습니다. 이로 인해 apply 동작은 성공으로 표시되었지만 변경 사항이 즉시 적용되지 않았기 때문에 Scale Up이 이루어지지 않았던 것입니다.

4–3) Github 연동 문제

  • VCS 기반 Workspace를 생성하려고 할 때 오류 발생 (ORG/REPO_NAME: 422 Unprocessable Entity)
"status": "400",
"title": "bad request",
"detail": "Failed to create webhook on repository: Failed to create webhook on repository: ORG/REPO_NAME: 422 Unprocessable Entity"
  • 하나의 Repo에서 Path별로 Hand-on 실습 시나리오 코드를 구현해놓았는데 Workspace 생성 시 VCS(Github)을 연동하지 못하는 위와 같은 에러를 확인하였습니다.
  • 기본적으로 GitHub은 하나의 리포지토리에 웹훅을 20개로 제한하고 있습니다.
  • Github setting 설정(https://github.com/ORG_NAME/REPO_NAME/settings/hooks)에서 현재 구성된 웹훅이 20개 미만인지 확인을 진행해보니 20개 모두가 연동되어있었으며, Gitlab 측 문의를 통해 해당 웹훅 개수를 늘릴 수 있지만 시간문제상 repo를 fork하여 여러개의 repo를 연동하여 workspace를 생성하는 것으로 방향을 바꾸게 되었습니다.

5. Grafana를 통한 TFE 모니터링

  • ServiceMonitor는 쿠버네티스에서 사용하는 Custom Resource Definition (CRD)로 쿠버네티스 클러스터 내의 서비스들을 자동으로 모니터링하기 위한 설정을 할 수 있습니다. ServiceMonitor 리소스는 Prometheus가 감시할 서비스의 endpoint들을 정의하고, 어떤 방식으로 스크래핑할지, 어떤 라벨을 사용할지 등의 세부 사항을 포함합니다. Prometheus Operator는 이러한 ServiceMonitor 리소스를 감지하고, Prometheus의 설정을 자동으로 업데이트하여 모니터링할 서비스들을 자동으로 발견하고 감시합니다.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app: terraform-enterprise
name: terraform-enterprise
namespace: monitoring
spec:
endpoints:
- port: metrics
interval: 10s
path: /metrics
params:
format:
- prometheus
namespaceSelector:
matchNames:
- tfe
selector:
matchLabels:
app: terraform-enterprise
  • Terraform Enterprise value yaml 파일에 다음과 같이 metric을 enable을 설정하여 Terraform Enterprise에서 metric 수집을 활성화 합니다.
tfe:
metrics:
enable: true
httpPort: 9090
httpsPort: 9091
privateHttpPort: 8080
privateHttpsPort: 8443
  • Terraform Enterprise에서 해당 metrics를 정상적으로 수집이 안되는 문제가 있어서 임의로 Service의 port를 추가해주었습니다.
spec: 
<생략>
ports:
- name: metrics
port: 9090
protocol: TCP
targetPort: 9090
  • Grafana Dashborad는 다음과 같이 구현하였습니다.
sum by(status)(tfe_run_current_count{status=~"planning|applying"})

sum(tfe_run_current_count{status=~"applied|errored|planned_and_finished"})

sum(tfe_run_current_count{status="errored"})

topk(5, count by(workspace_name, organization_name)(tfe_run_current_count))

sum by(organization_name)(tfe_run_current_count{status!~"applied|errored|discarded|canceled|planned_and_finished"})

sum(tfe_run_current_count{status=~"(?:(?:plan|apply)?_)?queued|pending"})

sum by(status)(tfe_run_current_count{status!~"applied|errored|discarded|canceled|planned_and_finished"})

rate(sum by(status)(tfe_run_current_count{status=~"applied|planned_and_finished|errored|discarded|canceled"})[30s:]) * 60
  • 구성한 Grafana Dashboard를 통해 세션 당일 사용자들이 실행한 결과를 실시간으로 확인할 수 있었습니다.

마치며

이번 AWS 파트너 서밋 핸즈온 세션은 실제 현장에서의 클라우드 마이그레이션 경험을 제공하며 특히 보안과 거버넌스 관리의 중요성을 강조했고 Hand-on 환경으로 Terraform의 FDO 옵션인 Terraform Enterprise를 통해 클라우드 환경에 빠르고 안정적으로 인프라를 구축하는 데 필수적인 기능들을 실습할 기회를 제공하였습니다.

이를 통해 보안 및 규정 준수 사항을 충족하는 Terraform Enterprise의 다양한 기능들을 직접 확인하고 실습하고 AWS의 보안 및 규정 준수 기준(MSR)과 함께 안전한 클라우드 환경 구축에 대한 이해를 깊이 있게 다질 수 있었으며, Terraform이 인프라 구성 뿐만 아니라 보안과 규정 준수를 준수하는 데 도움이 될 수 있음을 파트너분들께 소개할 수 있는 기회가 되어 매우 의미 있는 자리였습니다.

Hand-on 시나리오를 자세히 보고 싶은 분들은 다음 페이지를 통해 확인 할 수 있습니다.

aws workshop studio

세션 당일에만 AWS 이벤트에서 제공하는 계정을 제공했기 때문에 현재는 자신의 AWS 계정을 사용하셔야 합니다. 😅

감사합니다.

--

--