• terraform
  • aws
  • iac

Terraform 도입기 (2): 구조를 어떻게 잡았나

State를 리소스 종류와 환경 단위로 쪼갠 이유, 모듈을 얇게 둔 이유, 그리고 시작 2주 만에 처음 설계를 갈아엎은 이야기.

시리즈 · Terraform 도입기2 / 5
  1. Terraform 도입기 (1): 콘솔로 만든 인프라를 코드로 옮기기로 했다
  2. Terraform 도입기 (2): 구조를 어떻게 잡았나
  3. Terraform 도입기 (3): 기존 인프라 옮기기
  4. Terraform 도입기 (4): PR과 Plan으로 일하기
  5. Terraform 도입기 (5): 15개월 회고

Terraform 저장소는 디렉토리 구조가 설계의 절반이라고 생각한다. 어떻게 나누느냐에 따라 한 번의 실수가 어디까지 번지는지가 정해지기 때문이다.

지금의 구조

terraform/
├── aws/
│   ├── init/                  # State 저장소 (S3, DynamoDB)
│   ├── iam/
│   ├── vpc/
│   │   ├── _module/
│   │   ├── mh_d_apnortheast2/ # dev
│   │   ├── mh_s_apnortheast2/ # stg
│   │   └── mh_p_apnortheast2/ # prod
│   ├── security_group/
│   ├── ec2/
│   ├── rds/
│   ├── waf/
│   └── ...
└── other/                  # AWS 밖의 리소스

1단계는 리소스 종류, 2단계는 계정_환경_리전이다. 맨 끝 디렉토리 하나가 독립된 State 하나를 가진다. 이 글에서는 그 단위를 스택이라고 부르겠다. State 파일의 S3 경로는 디렉토리 경로와 똑같이 맞췄다. 별것 아닌 규칙인데 덕분에 “이 State가 어느 코드 거지?” 하고 헷갈린 적이 없다.

서비스가 아니라 리소스 종류로 나눈 이유

보통은 서비스 단위로 나누라고들 한다. 서비스 A 디렉토리 아래에 그 서비스의 네트워크, 서버, DB를 다 넣는 식이다. 팀이 서비스를 통째로 책임지고 서비스끼리 독립적이면 그게 맞다.

우리는 사정이 달랐다. VPC와 로드 밸런서를 여러 서비스가 같이 쓰고, 인프라를 만지는 사람은 몇 명 안 되고, 하는 일의 대부분이 “보안 그룹 규칙 추가” 같은 리소스 종류에 묶인 작업이었다.

그래서 얼마나 자주 바뀌는지, 잘못되면 얼마나 아픈지가 비슷한 것끼리 묶었다. 보안 그룹은 매주 바뀐다. VPC는 분기에 한 번쯤 바뀐다. RDS는 웬만하면 안 바뀌어야 한다. 이 셋이 한 State에 있으면 제일 자주 바뀌는 걸 고칠 때마다 제일 위험한 게 plan에 같이 올라온다. 따로 두면 보안 그룹 쪽에서 무슨 실수를 해도 RDS는 애초에 건드릴 수가 없다.

스택이 작으면 plan이 빨리 끝나고 결과도 짧다. 이게 생각보다 중요했다. plan 결과가 세 화면을 넘어가면 사람은 읽지 않는다.

스택끼리는 output으로만 이야기한다

쪼개 놓으면 서로 값을 넘겨받을 방법이 필요하다. 서버를 만들려면 서브넷 ID와 보안 그룹 ID를 알아야 한다. 각 스택이 필요한 값을 output으로 내보내고, 다른 스택은 그 State를 읽어서 쓴다.

vpc_security_group_ids = [data.terraform_remote_state.sg.outputs.sg_ids["sg_app_was"]]

ID를 코드에 직접 적지 않는다. 보안 그룹을 다시 만들어서 ID가 바뀌어도 쓰는 쪽은 고칠 게 없다. 대신 output 이름은 다른 스택과의 약속이 되므로 함부로 바꾸지 않는다.

의존은 한 방향으로만 흐르게 했다. IAM과 VPC가 맨 위에 있고, 보안 그룹이 그걸 읽고, 서버와 DB가 다시 그걸 읽는다. 거꾸로 읽는 일은 없다. 서버를 한 대 추가할 때 보안 그룹 PR을 먼저 올리고 서버 PR을 나중에 올리는 것도 이 방향을 따르는 것이다.

2주 만에 갈아엎은 첫 설계

처음엔 지금보다 훨씬 그럴듯한 구조로 시작했다. 다른 조직의 Terraform 구조를 참고했는데, 맵에 서버 정보를 한 덩어리로 적으면 모듈이 서버와 보안 그룹과 디스크를 한꺼번에 만들어 주는 방식이었다. 항목 하나 추가하면 서버 한 대가 통째로 나온다. 멋있었다.

새로 만든 개발 환경에서는 잘 돌았다. 문제는 기존 인프라를 옮기려고 하자마자 터졌다.

기존 서버들은 규격이 제각각이었다. 어떤 서버는 보안 그룹을 다른 서버와 같이 쓰고, 어떤 서버는 디스크가 세 개 달려 있다. 이걸 하나의 틀에 맞추려면 옵션을 계속 늘려야 했고, 그러다 보면 모듈이 AWS API를 한 번 더 감싼 것에 불과해진다. 게다가 매주 바뀌는 보안 그룹이 서버와 한 묶음이라, 규칙 하나 고칠 때마다 서버 전체를 plan해야 했다.

2주쯤 됐을 때 구조를 바꿨다. 보안 그룹을 별도 스택으로 떼어 내고, 서버 모듈은 인스턴스 하나만 감싸게 줄이고, 맵을 버리고 서버 하나에 파일 하나를 쓰기로 했다.

코드는 확실히 길어졌다. 비슷한 블록이 파일마다 반복된다. 그래도 얻은 게 더 많다고 본다. 디렉토리의 파일 목록이 곧 서버 목록이다. PR에서 바뀐 파일 이름만 봐도 어느 서버를 건드리는지 안다. 그리고 서버 하나를 고친 변경이 옆 서버에 영향을 줄 방법이 없다.

이 결정을 운영 리소스를 옮기기 전에, 리소스가 몇 개 없을 때 한 게 다행이었다. 석 달 뒤였다면 아마 그냥 참고 썼을 것이다.

모듈이 하는 일은 이름 붙이기

남아 있는 모듈은 하는 일이 거의 없다. 서버 모듈이 스스로 정하는 건 이름과 태그뿐이다. ec2-서비스이름-환경 같은 이름 규칙, 그리고 환경별로 비용을 나눠 보기 위한 태그.

모듈을 재사용 도구가 아니라 규칙을 지키게 하는 장치로 쓴 셈이다. 이름과 태그는 사람이 매번 손으로 적으면 반드시 어긋난다. 나중에 비용 집계용 태그를 추가해야 했을 때 모듈 한 곳만 고쳐서 모든 서버와 디스크에 붙일 수 있었는데, 그때 이 정도면 충분하다고 느꼈다.

인스턴스 타입이나 디스크 구성처럼 서버마다 다른 값은 모듈 안에 숨기지 않는다. 파일을 열면 그 서버가 어떻게 생겼는지 다 보인다.

물론 반복문을 아예 안 쓰는 건 아니다. WAF의 IP 허용·차단 규칙처럼 이름과 우선순위만 다르고 모양이 완전히 같은 것에는 쓴다. 기준은 항목들이 정말 똑같이 생겼느냐다. 서버는 그렇지 않았다.

다음 글에서는 이 구조 위로 기존 인프라를 옮긴 이야기를 한다.