안녕하세요, 저는 카카오뱅크의 신뢰성엔지니어링팀에서 고객경험 향상을 목표로 서비스 안전성을 높이기 위한 툴을 연구하고 개발하는 Locke입니다.
카카오뱅크는 클라우드와 온프레미스(On-Prem) 환경을 통해 서비스를 제공하고 있습니다. 온프레미스로부터 클라우드까지의 확장 과정에서 다양한 오픈소스를 활용하게 되었습니다. 그러면서 저도 자연스럽게 ‘온프레미스 환경에서도 클라우드 환경처럼 유연하고 효율적으로 서버의 생성, 자동화 프로세스, 모니터링 등을 자동으로 관리할 수 있는 방법이 없을까?‘를 두고 많이 고민하게 되었는데요. 많은 고민과 비교 끝에 HashiCorp의 테라폼(Terraform)을 사용한 자동화 아이디어를 채택하여 적용했습니다.
이 글에서는 테라폼으로 자원 관리를 자동화하여 효율성을 높이기까지 어떤 고민이 있었고, 어떤 기술을 채택했는지, 그리고 운영을 하며 얻게 된 경험에 대해 단계별로 설명드리겠습니다. 간단한 설명이 포함되어 있지만, 기본적으로는 테라폼과 Ansible 등에 대한 이해를 갖춘 분들이 독자라 가정하고 기술했음을 미리 밝힙니다.
문제 인식: 서버가 생성될 때마다 설정을 해줘야 되는 상황 😵
테라폼은 코드 기반의 인프라스트럭처(Infrasructure) 자동화 도구로, 클라우드와 온프레미스 환경에서 인프라 리소스의 배포, 변경 및 삭제를 관리합니다. 이 도구는 반복적인 수작업을 줄이고, 인프라 상태를 일관적으로 유지하여 문제 해결을 용이하게 합니다.
카카오뱅크에서는 테라폼으로 인프라를 프로비저닝(provisioning) 한 후, Ansible을 사용하여 서버 설정, 소프트웨어 설치 및 구성 관리 작업을 수행합니다. 금융 산업의 인프라 복잡성을 고려할 때, 테라폼 단독보다는 Ansible과 결합하여 활용할 때 더 큰 유용성을 얻을 수 있습니다. 그러나 테라폼을 통해 생성된 VM 서버 리소스에는 몇 가지 추가 설정 작업이 필요합니다.
1. 자동화 툴에서 서버 인식하기
- 우선 Ansible 프로젝트 관리를 위한 웹 기반 사용자 인터페이스인 ‘AWX’와 같은 자동화 툴에서 해당 서버를 인식할 수 있어야 합니다. 인식 과정에서 ‘해당 서버에 어떤 솔루션들이 설치되어야 할지’에 대한 정보를 알아야 합니다. 예를 들어, HashiCorp Consul Agent가 설치되어야 한다면 현재 데이터 센터의 요구에 맞게 네트워크 정보, 성능과 보안이 설정되어야 합니다.
2. 모니터링 툴에 서버 추가하기
- 다음으로, 모니터링 툴도 자동으로 해당 서버의 모니터링을 추가할 수 있어야 합니다. 서버 위에 설치된 애플리케이션의 정보를 알면, 그에 알맞게 생성된 서버의 모니터링 툴을 설정할 수 있어야 합니다. 예를 들어, Spring Web App에서는 Actuator 수집 정보 등을 자동으로 파악하여 모니터링 설정에 등록할 수 있어야 합니다.
위와 같은 추가 설정 작업을 자동화하여 테라폼으로 서버를 생성하면, 엔지니어가 추가적으로 개입하지 않고도 모든 설정이 자동으로 이루어지는 환경을 구축하는 것이 궁극적인 목표였습니다. 지금부터 어떤 방법을 통해 문제를 해결했는지 따라가 보도록 하겠습니다.
아이디어: 리소스 설정을 자동화 해보면 어떨까? 😲
테라폼을 이용하여 서버 리소스 설정 작업을 자동화하고자 낸 아이디어는 다음과 같습니다.
2. 자동화 솔루션과 생성된 리소스 자동 연동
3. 모니터링 솔루션으로 생성된 리소스 인식
먼저, 테라폼 Provider를 이용해 주요 리소스 생성을 자동화합니다. 이 과정에서 서버 리소스, 방화벽 리소스 등이 자동 생성됩니다. 다음으로, 자동화 솔루션과 생성된 리소스를 자동으로 연동하는 방법을 찾습니다. 대표적으로 AWX와 같은 자동화 솔루션을 통해 생성된 리소스를 찾고, 이를 연동하는 것을 의미합니다. 마지막으로 모니터링 솔루션에서 생성된 리소스를 인식하는 방법입니다. 흔히들 아시는 Prometheus와 같은 모니터링 솔루션이 생성된 리소스를 찾아내서 인식하도록 하는 방법을 말합니다.
이해를 돕기 위해 우선 테라폼에 대해 간단히 정리하고 넘어가도록 하겠습니다.
‘테라폼(Terraform)’ 이란?
테라폼은 HashCorp사에서 개발한 오픈소스 IaC(Infrastructure as Code) 소프트웨어 툴로, 인프라를 코드(code) 형태로 관리할 수 있게 해 줍니다. 이때 테라폼은 ‘선언적 언어(Declarative Language) 방식’의 코드로, 기존의 구성사항을 고려할 필요 없이 최종 상태를 어떻게 구현할 것인지를 코드로 작성하는데 초점을 둡니다. 또한 여러 번 apply를 수행하더라도, 최종 상태에 대해 멱등성(冪等性, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질)을 유지합니다.
🔍 테라폼 워크플로우(Workflow)
위와 같이 테라폼의 워크플로우는 Write, Plan, Apply 3단계로 구분됩니다. 하나씩 살펴보면 다음과 같습니다.
(1) Write 단계
먼저 해당 단계를 설명드리기 위해, 예시로 ‘AWS Provider에 SQS Queue 리소스를 생성하는 테라폼 코드’의 일부를 가져왔습니다. 위와 같이 정의 해주는 방식으로 테라폼 코드를 작성하는 단계를 Write 단계라고 합니다.
좀 더 자세히 살펴보면, Provider 정의 block에서는 어떤 Provider를 사용해 프로비저닝 할지를 결정합니다. 이어서 Resource 정의 block에서는 ‘aws_sqs_queue
리소스를 terraform_queue
라는 이름으로 생성하라’는 내용이 block에 정의되어 있습니다.
(2) Plan 단계
앞서 예시로 보여드린 Write 단계에서 작성된 테라폼 코드에 $ terraform plan
명령어를 실행한 모습입니다. CI(Continuous Integration) 관점에서 보면, 코드에 대한 변경 커밋(Commit) 을 생성해서 PR(Pull Request) 를 날린 상태라고 할 수 있겠습니다. 명령어 $ terraform plan
을 실행한 결과, execution plan이 생성되며 어떤 변경사항이 있을지를 보여줍니다.
이처럼 Plan 단계는 테라폼 Backend에 저장된 상태(현재 상태)와 코드(목표 상태)를 비교하면서, Write 단계에서 작성한 인프라 코드를 검증하는 단계입니다. 예시를 보면, SQS Queue를 생성하는 코드를 작성했더니, execution plan의 결과도 그대로 SQS Queue를 새로 생성하게 되는 것으로 나오는데요. 추가되는 SQS Queue의 상세 정보는 +
기호로 이어지는 내용에서 확인할 수 있습니다.
(3) Apply 단계
Apply 단계는 앞선 Plan 단계에서 검증된 결과를 바탕으로, 실제 인프라에 적용해 보는 단계입니다. $ terraform apply
명령어를 사용하면 코드로 작성된 변경 사항이 실제 인프라에 반영되게 됩니다. 아까 보셨던 Plan 단계의 검증이 한 번 더 진행되고, 최종적으로 SQS Queue를 생성합니다.
이 과정은 보통 CI 관점에서는 ‘PR(Pull Request)이 Merge 된 단계‘라고 볼 수 있습니다.
🔍 테라폼 Backend
테라폼은 멱등성을 유지하기 위해 Apply 단계에서 상태 파일을 만들어 Backend에 저장하는 구조로 되어있습니다. 테라폼 Backend에 저장된 데이터는 다르게 말하면 ‘현재 인프라의 상태 정보’라고 볼 수 있습니다. 따라서 저장된 데이터가 변경된다는 것은 ‘현재 인프라 상태가 변경된다’는 걸 의미합니다. 이때 ‘변경되는 인프라의 상태’를 알 수 있다면, 인프라 리소스가 생성/삭제/수정되는 상황을 추적할 수 있습니다. 그렇게 인프라 변경 상태를 알게 되면, 변경이 발생할 때마다 자동화된 대응을 할 수 있게 됩니다.
그렇다면 테라폼 backend에 있는 상태 정보를 어떻게 가시화할 수 있을까요? 👀 이를 위해 탄생한 Terraboard를 이어서 소개드리겠습니다.
🔍 상태 파일을 보여주는 Terraboard
Terraboard는 ‘테라폼 backend에 저장된 상태 정보’를 가시적으로 보여줄 수 있는 오픈소스 프로젝트입니다. Terraboard의 깃헙(github) README를 참고하면, Terraboard를 통해 현재 상태 파일들의 목록과 개별 상태 파일의 구체적인 내용을 확인해 볼 수 있습니다.
카카오뱅크도 테라폼을 활용하여 대내외 시스템들의 다양한 상태 파일들을 관리하고 있는데요. 상태 파일은 보통 1개의 모듈로 구성되어 있고, 모듈 별로 그 안에 다수의 리소스가 정의되어 있습니다.
🔍 테라폼 Provider
이어서 앞에 잠시 등장했던 테라폼 Provider를 설명드리겠습니다. 테라폼 Provider는 테라폼 워크플로우의 Apply 단계에서 ‘API 호출로 실제 리소스를 생성하는 역할’을 담당합니다. 앞서 보신 예제에서는 AWS Provider가 SQS Queue를 생성하는 API를 호출하여 SQS Queue를 생성합니다.
Step 1. 테라폼으로 리소스 생성하기
본격적인 자동화의 여정을 출발해 보겠습니다. 우선 테라폼을 활용하여 간단한 코드 블록으로 다음과 같이 VM(Virtual Machine) 리소스를 생성하는 예제를 살펴보겠습니다.
resource "hci_instance" "my_instance" {
environment_id = "4cad744d-bf1f-423d-887b-bbb34f4d1b5b"
name = "test-instance"
network_id = "672016ef-05ee-4e88-b68f-ac9cc462300b"
template = "Ubuntu 16.04.03 HVM"
compute_offering = "Standard"
cpu_count = 4
memory_in_mb = 8192
ssh_key_name = "my_ssh_key"
root_volume_size_in_gb = 100
private_ip = "10.2.1.124"
dedicated_group_id = "78fdce97-3a46-4b50-bca7-c70ef8449da8"
}
신규로 생성된 VM 리소스에는 ‘VM에 대한 기본적인 정보’ 외에는 아무것도 적혀 있지 않습니다. VM 생성 시점의 계정 정보, VM 위에 설치할 솔루션들, 수집할 모니터링 지표 등에 대한 정보는 개별적으로 확인이 필요했습니다. ‘이 부분을 어떻게 하면 자동화할 수 있을까?’ 고민한 결과, 신규 생성된 VM에 필요한 정보를 추가 맵핑하는 방법을 생각해 냈습니다.
Step 2. Custom 테라폼 Provider의 탄생
신규 생성된 VM에 필요한 정보를 추가 맵핑하려면, 테라폼 메타데이터용 리소스가 별도로 필요했습니다. 이때 기존에 테라폼에서 제공하는 리소스 외에 별도로 정의된 리소스를 사용하기 위해서는 Provider를 직접 개발해야 했습니다.
참고로 테라폼에서는 쉽게 생성할 수 있는 프레임워크를 제공하고 있는데요. 이를 활용하여 간단한 형태의 Provider를 자체적으로 개발했습니다. 이렇게 개발한 Provider로 ‘맵핑을 위한 리소스’를 별도로 생성하고, 해당 리소스와 VM서버를 맵핑해보았습니다.
resource "kabang-resource_application" "application" {
project = var.project
service = var.service
application = var.application
accounts = var.accounts
domains = var.domains
vservers = var.vservers
outbounds = var.outbounds
metrics = var.metrics
description = var.description
ports = var.ports
keys = var.keys
deployment = var.deployment
zone = var.zone
}
resource "kabang-resource_host" "host" {
for_each = var.hostname_ip_map
hostname = each.key
ip = each.value
project = var.project
service = var.service
application = var.application
zone = var.zone
}
위의 코드에는 kabang-resource_application
, kabang-resource_host
라는 이름의 리소스 2가지를 정의했습니다. kabang-resource_host
리소스는 앞서 살펴본 hci_instance
(=VM서버)와 1:1 맵핑되는 리소스이며, kabang-resource_application
리소스는 kabang-resource_host
리소스와 1:N으로 맵핑됩니다.
생성 예제
아래 예를 통해 어떤 리소스가 생성되는지를 살펴보겠습니다. 이해를 돕기 위해 임의로 작성된 코드임을 참고해 주시기 바랍니다.
resource "kabang-resource_application" "application" {
project = "openbanking"
service = "openbanking"
application = "openbanking-api"
accounts = ["app", "consul", "nginx"]
metrics = {
"__meta_application_metrics_0" : 9090,
"__meta_application_metrics_1" : 9091,
"__meta_nginx_metrics" : 9913
}
ports = ["80:nginx"]
deployment = "docker"
}
resource "hci_instance" "vm" {
for_each = var.hostnames # 변수를 이용해 여러 vm을 생성하고 맵핑
name = each.value
template = "Ubuntu 16.04.03 HVM"
}
resource "kabang-resource_host" "host" {
for_each = hci_instance.vm
hostname = each.value.name
ip = each.value.nic_list_status[0].ip_endpoint_list[0].ip
project = kabang-resource_application.application.project
service = kabang-resource_application.application.service
application = kabang-resource_application.application.application
zone = "kakao"
}
위와 같이 여러 메타데이터를 포함한 리소스들을 생성합니다. 이렇게 맵핑된 데이터는 자동화에 사용할 수 있도록 설정되어 있습니다. 그럼 이제부터는 자동화를 위해 설정한 내용들을 살펴보도록 하겠습니다.
Step 3. AWX를 이용한 자동화 과정
AWX는 Redhat사의 ‘Ansible Tower’라고 하는 Ansible 대시보드 및 HA(High Availability) 솔루션의 오픈소스 버전입니다. Ansible에는 ‘Inventory‘라고 하는 개념이 있습니다. 여기서 Inventory는 자동화를 위한 서버 목록으로, File, API 등 다양한 방식으로 생성할 수 있습니다.
3.1 File Inventory 기능
기존에는 AWX를 사내 git과 연동하여 File Inventory 기능을 사용하고 있었습니다. File Inventory의 경우, Inventory Group을 가진 파일을 생성하고 해당 파일을 AWX에 로딩하여 사용합니다.
[apache]
apache-host[01:03]
new-apache-host[01:03]
[nginx]
nginx-host[01:06]
new-nginx-host[01:02]
기존에는 이처럼 계정명, 서비스명, 솔루션명 등에 맞게 그룹을 생성해서 사용하고 있었습니다. 하지만 이렇게 사용할 경우, 하나의 호스트가 여러 그룹에 포함되기 때문에 포함될 만한 그룹을 전부 찾아서 업데이트 해주어야 하는 문제가 있었습니다. 또한 VM서버를 생성하면 AWX 레포(repository)도 같이 수정해주어야 하는 번거로움이 있었습니다.
3.2 Dynamic Inventory 기능
AWX는 Dynamic Inventory라고 하는 기능도 제공하는데요. 해당 기능을 사용하면 API 통신을 이용해 Inventory를 동적으로 생성할 수 있습니다.
3.3 테라폼 State을 이용한 Dynamic Inventory Plugin의 탄생
저희는 테라폼 State가 ‘현재 인프라의 상태’를 의미한다는 점에 착안해 Terraform State를 이용한 Dynamic Inventory를 생성하는 작업을 진행했습니다. 이 과정에서 Terraboard에 State의 리소스를 전체적으로 가져올 수 있는 API를 추가하고, 해당 API를 이용해 Inventory를 생성할 수 있도록 커스텀 Dynamic Inventory Plugin을 만들었습니다.
Ansible Community Documentation 페이지를 참고하면, Dynamic Inventory를 생성할 수 있는 가이드를 꼼꼼하게 제공하고 있어서 해당 가이드와 aws ec2 plugin을 참고하여 Plugin을 개발했습니다.
이렇게 탄생한 Dynamic Inventory Plugin은 테라폼을 활용하면, 새로운 리소스를 생성 시 해당 리소스가 Inventory에 별다른 업데이트나 수정작업을 거치지 않고 자동으로 등록될 수 있습니다. Inventory에 등록될 때 그룹도 테라폼 리소스에 정의된 대로 자동으로 맵핑이 가능해졌습니다.
plugin: kabang.terraform.kabang_terraboard
terraboard_url: "http://terraboard_host"
kabang_resource_application_fields_to_be_grouped:
- accounts
- service
- application
- project
- zone
- deployment
위와 같이 저희가 필요한 group을 정의해 두면, Plugin이 Terraboard에 요청하여 자동으로 설정 값들이 맵핑될 수 있도록 했습니다.
Step 4. Prometheus를 이용한 자동화 과정
Prometheus는 모니터링 툴로 널리 애용되고 있는데요. 불을 훔쳐서 까마귀에게 고통받는 프로메테우스와는 비슷하지만 다르게 고통받고 있죠. 각 서버로부터 메트릭을 수집, 저장하고 Query 및 시각화하고 알림을 보내는 역할을 하고 있습니다. (우리에게 불을 가져온다는 의미인지도 모르겠네요)
4.1 Prometheus File SD(Service Discovery)
프로메테우스는 다양한 서비스 디스커버리를 지원는데, 카카오뱅크는 정적으로 구성된 목록이나 파일에서 타겟 엔드포인트를 읽어 오는 File SD를 사용하고 있었습니다.
File SD는 외부 의존성이 없고 간단히 사용할 수 있는 장점에 반해, 작업자가 항상 file을 업데이트해주어야 하는 문제가 있었습니다. 이 과정에서 인프라와 git file이 일치하지 않는 경우가 빈번히 발생했습니다. 기존 설정은 아래와 같습니다.
- targets:
# Service Name
- 'service_host01'
- 'service_host02'
- 'service_host03'
- 'service_host04'
labels:
__meta_application_metrics_0: "19090"
__meta_application_metrics_1: "19091"
__meta_nginx_metrics: "9913"
동시 같은 파일을 수정하면서 발생하는 git conflict 방지를 위해 서비스 단위로 파일을 분리하고, 각 파일 별로 meta label을 이용해 메트릭을 수집하도록 했습니다.
scrape_configs:
- job_name: 'application_metrics_0'
metrics_path: '/actuator/prometheus'
file_sd_configs:
- files:
- extra/targets/*.yml
relabel_configs:
- source_labels: [ __address__, __meta_application_metrics_0 ]
regex: .*;$
action: drop
- source_labels: [ __address__, __meta_application_metrics_0 ]
separator: ":"
action: replace
target_label: __address__
- source_labels: [ job ]
replacement: "application_metrics"
target_label: 'job'
Prometheus의 relabel config를 적극적으로 활용해서 meta 레이블만 잘 지정하면 메트릭이 잘 수집될 수 있도록 되어있습니다. 그럼에도 여전히 수동으로 업데이트 해야한다는 점은 작업을 어렵게 만들었습니다.
4.2 테라폼 State Http SD(Service Discovery)
앞서 보신 테라폼 리소스에도 metric label이 들어갔었다는 것을 기억하실지 모르겠는데요.
resource "kabang-resource_application" "application" {
project = var.project
service = var.service
application = var.application
accounts = var.accounts
domains = var.domains
vservers = var.vservers
outbounds = var.outbounds
metrics = var.metrics
description = var.description
ports = var.ports
keys = var.keys
deployment = var.deployment
zone = var.zone
}
kabang-resource_application
리소스를 보시면 metrics
필드에 metric label이 들어 있는 걸 확인하실 수 있습니다. 저희는 기존에 Terraboard에 리소스 API를 만들어두었는데, 리소스 API를 이용하여 테라폼 State Http SD를 구현했습니다. 이때 HTTP SD는 Prometheus에서 SD를 HTTP 형태로 하기 위한 기능을 말합니다.
이를 위해서는 정해진 규격에 맞게 응답하는 API를 만들면 되는데요.
위와 같이 Terraboard Resource API에 연결할 수 있는 모듈을 별도로 개발하여, Prometheus에서 자동으로 리소스를 인식할 수 있도록 했습니다.
scrape_configs:
- job_name: 'application_metrics_0'
metrics_path: '/actuator/prometheus'
http_sd_configs:
- url: http://terraboard-http-sd-module/http-sd
relabel_configs:
- source_labels: [ __address__, __meta_application_metrics_0 ]
regex: .*;$
action: drop
- source_labels: [ __address__, __meta_application_metrics_0 ]
separator: ":"
action: replace
target_label: __address__
- source_labels: [ job ]
replacement: "application_metrics"
target_label: 'job'
이로써 기존에 사용하던 scrape config는 거의 변경 없이 http_sd_config
만 추가하여 변경이 가능해졌습니다. 해당 변경으로 인해 서버를 추가하는 단계에서 Prometheus repository 코드를 직접 수정하지 않아도 되게 되었습니다.
결과
지금까지 설명드린 내용을 도식화하면 아래와 같은 그림으로 표현할 수 있습니다.
한 번의 정의만으로 각 오픈 소스가 유기적으로 연동하여 인프라 생성과 서버 설정 그리고 모니터링까지 자동화하는 구조로 변경됐습니다. 어찌 보면 간단한 아이디어인데, 이를 통해 온프레미스 환경이 조금 더 편리한 환경으로 변화할 수 있다는 점이 매력적이었습니다. 팀 내에서도 해당 프로젝트를 진행하면서 많은 공감을 해주셨는데 앞으로도 더 나은 방향을 찾아 꾸준히 개선해 나가도록 하겠습니다. 감사합니다.