인프라 일기 0편 : IAC 동장 메커니즘(Terrafrom)
개발 일기 1편
[ Terraform vs Pulumi ]
IAC : Infrastructe As a Code
→ 인프라 환경 설정을 코드로
일단 현재 팀에서 기본적으로 사용하던 (Setting은 내가 안함 = 사용만 할 줄 안다 ㅎㅎ)
Pulumi와 비교해보면
일단 현재 팀에서 기본적으로 사용하고 있다 (Setting은 내가 안함 = 사용만 할 줄 안다 ㅎㅎ)
- 언어
- Declarative(전체 설계도) : Terrafrom은 HCL이라는 자체 도메인 언어를 사용하면서 (like .tf)
- 반복을 한다기 보다, 매번 다르게 적용이나 / 동일한 설정을 반복하는 것에 가까움
- OOP(개별) : Pulumi는 Java, Go, Python, TypeScript 등등 기본적인 백엔드 언어 사용
- 각 객체를 만들고 해당 객체를 다시 불러서 수정 또는 다시 사용하는 OOP 개념
→ 조건문, 반복문 등을 사용할 때, for_each나 count 등 생소한 문법을 사용하며
→ if / for in range 등을 사용하면서
- 상태 관리 파일
- 로컬 저장 : Terraform은 .tfState라는 파일로 해당 정보를 저장
- SAAS 관리 : Pulumi는 자체 관리형 서비스인 Pulumi Service에 상태를 저장
- 일단 SOT 요청 → 받은 apply system 만 작업 → SOT update
- 나머지는 대기 → 이후에 앞선 작업이 끝나면 → update된 SOT를 받음
IAC 프로그램은 무조건 상태 관리가 필수!
→ 현재의 상태와 코드의 상태를 보고 변화한 부분을 적용하기 때문에 SOT 역할이 필요하다!
→ 여러 명이 동시 작업할 경우 DB Locking 등의 충돌 제어 필수
→ 동시에 apply 하더라도, SOT를 외부 서비스가 가지고 있으므로 (자동 충돌 제어)
- 패키지 및 모듈관리
- Terraform Registry : (like DockerHub)
- PackageManager : (그대로 Gradle, npm, pip) → 라이브러리처럼 인프라 코드 사용
[ 동작 명령어 세부 분석 ]
apply가 완료되면,
1. .terraform.lock.hcl (버전 잠금 파일)
- 역할: 테라폼이 AWS와 통신하기 위해 다운로드한 플러그인(Provider)의 정확한 버전과 해시값을 기록해 두는 파일입니다.
- 왜 필요한가요? Node.js의
package-lock.json이나 Python의Pipfile.lock과 같은 역할을 합니다. 오늘 내가 다운로드한 AWS 플러그인 버전이5.0인데, 내일 팀원이terraform init을 했을 때 몰래5.1로 업데이트되어 예상치 못한 오류가 발생하는 것을 막아줍니다.
- 관리 방법: 이 파일은 팀원들과 버전을 맞추기 위해 GitHub에 함께 올려서(Commit) 공유하는 것이 좋습니다.
2. terraform.tfstate (현재 상태 파일 / 메인 장부)
- 역할: 내 코드(
main.tf)와 실제 AWS에 만들어진 인프라(EC2, 보안 그룹 등)를 1:1로 매핑해 놓은 가장 핵심적인 데이터베이스(JSON) 입니다.
- 어떤 내용이 있나요? 스크린샷 우측을 보면
outputs에public_ip값(3.35.17.174)이 기록되어 있고, 아래쪽에는 내가 만든 리소스들의 실제 AWS 고유 ID, 설정값들이 빼곡히 적혀 있습니다. 테라폼은 다음번 실행 때 오직 이 파일만 보고 변경 사항을 추적합니다.
- 🚨 주의사항: 이 파일 안에는 데이터베이스 비밀번호나 인증 키 같은 민감한 정보가 평문으로 저장될 수 있습니다. 절대 GitHub 같은 공개된 곳에 올리면 안 됩니다. (실무에서는 S3 같은 안전한 원격 저장소에 보관합니다.)
3. terraform.tfstate.backup (이전 상태 백업 파일)
- 역할: 방금 설명한
terraform.tfstate파일의 직전 버전(백업본) 입니다.
- 어떻게 생겨나나요? 테라폼은
apply나destroy명령어를 쳐서 인프라를 변경할 때마다, 먼저 기존 장부를 이.backup파일로 복사해 둡니다. 그 후 새로운 변경 사항을 메인 장부(.tfstate)에 덮어씁니다.
- 왜 필요한가요? 작업 중 네트워크가 끊기거나 알 수 없는 오류로 메인 장부(
.tfstate) 파일이 깨졌을 때, 이 백업 파일을 이용해 인프라 상태를 복구(Ctrl+Z)할 수 있는 생명줄 역할을 합니다.
ㅅvian iso 설치
proxmox는
- proxmox iso 자체를 이용한 운영체제 설치 (쉬움)
- Devian 12를 이용한 내부 구조화 이후 세팅 (복잡함 but 디스크 분리 등 부가 설정 가능)
두가지 방식으로 설치가 가능한데
비교적 간단한 Proxmox를 iso로 설치하는 방식이 아닌 (이건 예전에 해봤으니까)
Devian을 이용하는 방식으로 해보겠다.
- CPU : 8, RAM : 12, disk 32 정도로 놓고!
- 기본적인 machine과 bios는 기본 값을 사용하도록! + SCSI도 기본 Virtl0로!
- Qemu Agent만 설정 해놓은 상태로!
- 이전에 궁금했던, 네트워크 세부 설정이나, local-lvm 등의 기본 설정을 자세히 분석해 보면서
사용하다가, 이 방식은 VM 위에 VM을 띄우는 방식이니까 효율성이 떨어지므로
→ 가상화 구성은 다시 원 서버로 돌아가서 할 생각이다!
참고로 Qemu (Guest) Agent는
HostOS(Proxmox) - QuestOS(Devian) 간의 통신용 demon으로
Host에서 Guest내부 설정 등을
- 보고
- 명령 기반 제어가
가능하도록 하는 백그라운드 프로그램이다!
실제 서버에는 “Golden Image 기반의 템플릿 배포”라고 해서
네트워크 기본 세팅 및 k8s 설치가 완료된 snapshot을 이용해
해당 명령 체계로 초기화된 k8s node설치 + cloud-init을 이용한 네트워크 및 worker 세팅을 함께 사용하여
worker나 control node를 자동화 방식으로 추가 및 삭제 등이 가능했다!
CPU 설정 주의사항
- 중첩 가상화 방식을 사용할 때는 가상 CPU를 사용할 경우 VT-x 같은 하드웨어 내 가속화 기술을 못 사용함
→ host로 설정 변경!
- NUMA(Non-Uniform Memory Access) Architecture
소켓에 따라 메모리에 대한 접근을 나누고, 특정 소켓에 특정 메모리는 접근이 빠른 대신, 다른 소켓에 할당된 메모리를 접근할 때 느림
⇒ total Core = Core x Socket
- 같은 cpu 16을 만들때, 가상화 환경에서는 소켓 수가 적은 1x16이 더 유리!
Disk partition
| 파티션 | 용량 | 용도 및 설명 |
/ (루트) | 12 GB | 데비안 OS + Proxmox 패키지 + ISO 이미지 파일 (적정 용량) |
swap (스왑) | 1 GB | 메모리 부족 시 비상용 공간 (가상 머신 구동 시 필수) |
/home (홈) | 21 GB | VM 데이터 전용 공간 |
/var/lib/vzproxmox 기본 저장 위치
→ 설치 후에 home으로 재배치 필요 /home/vm_data
[ 메인 과제 ]
우리 팀에서 주기적으로 단기 프로젝트 성으로 진행하던 유저 매칭 서비스를 상시 운영 서비스로 바꿔보려고 한다.
그럼 바뀌는게 뭘까?
- 데이터를 기반으로 최적해를 매칭하고, 결과를 배포한 이전 방식과 달리, 매 순간마다 기준에 따라 매칭 데이터를 순위 별로 만들어야하는 방식
- 참여를 위해 단순 결제만 하던 방식이 아니라, 사용자가 credit을 활용해 원하는 동작을 하게하는 시스템
- 결제 데이터 정합성 → 이거는 이전 시즌을 운영할 때 문제가 많았다 ㅠㅠ
- 채팅 서비스 도입 → 이것도 지금 참 난관이다
[ 이전에는 왜 결제에서 문제가 발생했나? ]
1. 순차적으로 처리 검증이 안됐다
일단 결제 시스템을 대충만 봐도 백엔드에서 여러 도메인의 복합 동작이 필요하다.
- 매칭 신청 → 결제 창 → 백엔드에서 결제 + 매칭 상태 변경 + 포트원 api → 결제 완료/실패
그러다보니,
- 백엔드에서는 결제 완료라고 했는데,
- 포트원에서는 아직 처리중이고,
- 매칭 상태는 먼저 변경되고,
- 다시 Verify 과정으로 확인해보니 이미 매칭 상태는 이미 결제가 되었는데?
이런 상황이 단순 개발 환경에서 테스트 할때는 발생하지 않았지만, 실제 운영환경에서는 멀티스레딩 상황에서?는 문제가 발생했다 ㅠㅠ
절대적인 개수 자체가 많지는 않아서 수동으로 맞췄는데 저거 백업하고 맞추는 게 너무나 힘들었던 기억이 있다.
반성 : 이전 시즌은 내가 결제 담당이 아니었기 때문에 이해가 부족하기도 했고, 매칭 쪽을 개발한 나의 입장에서는 메소드를 넘겨주고 테스트 환경에서 “동작이 잘 되네” 에서 깊게 생각하지 않았던 것 같다.
2. Verify를 포트원에 의존한다 + 재시도가 해결방법?
기존 로직은 포트원에서 웹훅 받고 db 한번 보고 결제가 완벽하다 판단했다
이게 완벽히 정합성을 챙길 수 있는가? 라는 고민을 해보니 답은 “아니다”였다!
그럼 앞에서 언급한 에러처럼 이미 결제가 시작했는데 후에 verify에서 어긋나면 그냥 캔슬하면 그만인가?
[ 그럼 어떻게 바꿀까? - 사전 단계 : Event와 Order의 분리 ]
기존 payment 하나를 통합으로 사용하고 있던 구조를 분리!
Payment-Order(결제 주문 - 핵심 도메인)- 역할: 우리 서비스에서 결제되고, 상품의 소유가 바뀌고를 DB에 기록
Payment-Event(결제 이벤트 - PG 연동 인프라)- 역할: 외부 PG사(PortOne)와의 통신 이력(결제 성공/실패/환불/검증 webhook 등의 결과)을 기록
그래서 나뉜게 무슨 장점을 가지느냐
- "결제 실패 후 재시도" 처리가 매우 깔끔해진다
분리했을 때는 Payment-Order는 하나만 존재하고, 그 아래에 Payment-Event가 2줄(1. FAILED 기록, 2. SUCCESS 기록) 쌓이게 된다
즉 결제 기록을 계속해서 재발급 하는 것이 아닌 event 즉 PG사 요청만 번복하면 만사 해결!
- Verify 처리 로직이 단순해진다.
기존에는 물건정보와 결제정보가 한번에 관리되다 보니까 중복되었다고 결제를 취소하는 과정이
물건 발급 → 사용자 소유로 변경 등의 모든 로직이 진행된 상황에서
역방향으로 돌려야했다면, 포트원 웹훅을 imp_uid 가 unique되게 함으로써, 무조건 1개를 유지하고 중복이나, 문제가 발생했을 때는 순서상 로직 자체가 진행이 안된다!
→ event 처리 즉 A이후에 B가 되게 하는게 코드 단에서 설정해야했다면, 애초에 Payment-Event DB단에서 차단!
즉 결제 과정은
- 우리 서버에서 merchant_uid 발급 =
payment-orderis PENDING
- 프론트에서 결제
- 포트원에서 return imp_uid to Front
- 우리 서버에서
payment-event를 imp_uid를 사용해서 저장
payment-orderis SUCCESS
→ 후에 event verify를 할때는 imp_uid 로 Portone API 사용
순서다
여기에 서비스의 정합성을 위해 몇 개의 로직을 이번에 추가 해보았다
물론 정합성이 맞게 애초에 PAYMENT에서 문제가 없다면 필요가 없겠지만
여러 호출을 통해 동작하므로, 꼬일 경우를 대비한 보호 및 검증 설계를 추가하자
[ 그럼 어떻게 바꿀까? - 1단계 : Ledger의 도입 ]
절대 장부(원장)를 만들자!
Ledger는 Sorce Of Truth로 무조건적인 신뢰!
- 크레딧 구매 요청부터
- 결제를 요청하고, 검증하면서
- 실제 크레딧 사용까지를 모든 유저를 대상으로 기록한 장부를 도입하자
그리고 스케줄러를 사용해서 주기적으로 확인
문제가 발생되면, 무조건 Ledger 기준으로 DB를 수정하면
스케줄러 이후에는 절대적으로 무결한 데이터가 db에 형성될 수 있다!
→ Ledger기반으로 order, event 등의 payment관련 기록을 재조정하는 로직이 구현이 제일 어려울 거 같긴한데 다른 팀원이 맡아서 열심히 하는 중이다 ㅎㅈㅈ ㅎㅇㅌ!
[ 그럼 어떻게 바꿀까? - 2단계 : Credit의 도입 ]
결제 과정에서 가장 큰 문제가 뭘까 고민해봤더니 역시 유저의 돈 문제이다
- 유저의 돈만 빠져나가고 크레딧이 안 들어오거나,
- 한 번 결제했는데 크레딧이 두 번 들어오는 일은 절대 일어나서는 안된다
이거는 원장으로 다 돌리면 해결되나?
→ 이것은 맞는 말이다
- 스케줄러로 (원장 데이터 = DB데이터)가 되는 순간 데이터는 완벽하게 정합성을 보장한다
근데 매 결제 타이밍 / 크레딧 사용 타이밍 원장 수준에서의 검사가 계속 되는게 맞는 구조인가?
에 대한 답은 “아니다” 라고 생각한다.
“원장”이라함은 절대적 Truth를 가지는 대신에, 모든 기록이 다 적혀있는 매우 큰 장부다 이걸 매 요청마다 n번씩 확인하면서 진행하는 것은 너무 비효율적이다.
그럼 매 결제마다 원장 단계 이전에 검증을 한차례 진행하려면?
비트 코인 방식에서 착안해서, 크레딧 자체에 상태 정보를 기록하자!
- 크레딧에 순차적 상태값을 부여하고, 이를 이전 단계까지의 검증 완료 기준으로 삼아보자
Credit 설계
- 크레딧을 구매하는 수만큼 크레딧 생성!
- 각 크레딧은 bulk 단위로 동작하면서
- 상태 및 소유자, 구매 물품, 개당 가격 등의 기본 정보를 소유
- 기본 정보를 바탕으로 paymentService에서 엔티티 변경 내역 기반 정보를 받아서
- 크레딧의 정보와 비교 및 검증
추가 설명
과정에 대해 예시를 들어서 조금 더 설명해보면
- 먼저 유저 A가 크레딧 5개 구매요청 → 5개의 빈 크레딧을 생성!
- 크레딧에 [유저 A가 발급]이라는 정보를 넣고 X 5
- 사용자가 클라이언트를 이용해서 결제를 시도할 때, 실제 유저 A가 merchant_id를 통해 실제
payment-order정보 기반 검증 - 실제로 5개인가 검증
- 사용자가 구매를 시도할 때, 실제 PG와 통신 기록인
payment-event확인 및 검증
- 만약 포인트 같은 게 있다면 포인트가 해당 유저가 소유값을 db에서 확인 후에 1000이 차감되었는지도 확인 필요! → 일단 지금은 고려하지말기!
- 검증
- 소유자 확인 = 결제자
- 실제로 pending인게 5개인가?
- 개별가격 X 개수 = 실제 결제 금액
- 크레딧에 [유저 A 결제 완료] 기록
- 사용자가 크레딧을 상품 구매에 사용할 때,
- 검증
- 소유자 = 구매요청자
- A가 활성 크레딧(결제 검증까지 된)을 상품 1의 가격 보다 많이 소유하고 있는지
- 실제로 그 가격 만큼 차감되었는지 등을 확인 후에
- 크레딧에 [유저 A 상품1 구매] 기록
- 최종 기록
- 검증
- 최종 기록 구매 기록 및 소유자
- 상품 1번이 유저 A 소유로 잘 이전되었는지
- 상품 DB에서 1번의 갯수가 1개 차감 되었는지 등을 확인
- 크레딧에 [사용 완료]
이전 과정이 실패 했을 때는 어떻게 해야 하나? ⇒ 원장을 기반으로 DB table의 정합성을 맞추는 과정을 거치면 될 것 같다.