モジュールを直すたび手で確認していて、一つ見落とした
한국어 원문으로 표시합니다.
목표
시험할 수 있는 모양의 모듈을 만들고, 계획만 세우는 값싼 시험과 실제로 만드는 비싼 시험을 각각 쓰고, run 을 이어 붙이고, 막혀야 할 것이 막히는지 시험하고, 실패가 어떤 메시지를 내는지 보고, mock 으로 값만 확인한 뒤, CI 가 부를 한 줄로 묶습니다.
왜 중요한가
재사용하는 모듈은 부르는 곳이 늘어날수록 고치기 무서워집니다. '이 값을 바꾸면 어디가 깨지나' 를 사람이 매번 확인하기 때문입니다. 시험을 붙여 두면 그 확인이 명령 한 줄이 되고, 그때부터 모듈을 고칠 수 있게 됩니다. 중요한 것은 시험을 두 종류로 나눠 쓰는 감각입니다 — 계획만 세우는 시험은 빠르고 값싸서 이름 규칙·조건 분기·기본값처럼 값만 보면 되는 것을 다 덮을 수 있고, 실제로 만드는 시험은 느리지만 '정말 그것이 그렇게 생기는가' 를 확인하는 유일한 방법입니다. 여기에 '막혀야 하는 것이 막히는가' 를 확인하는 시험을 더하면, validation 을 잘못 고쳤을 때 조용히 통과하는 일이 없어집니다. 마지막으로 이 모든 것이 CI 의 종료 코드 하나로 이어져야 값어치가 생깁니다.
단계
/root/tfa-test/main.tf에var.env(dev·stage·prod 만 허용하는 validation)와var.replicas(기본 1, 1~10 만 허용)를 받아out/app-<env>.conf에name=과replicas=두 줄을 쓰는local_file.conf를 두고, 출력name·conf_path·replicas세 개를 선언하세요. init 하고tofu validate로 확인합니다./root/tfa-test/tests/basic.tftest.hcl에 파일 수준variables로env = "dev"를 두고,command = plan인run "name_is_prefixed"에서 출력name이app-dev인지 단언하세요.tofu test를 이 파일만 걸러 돌리고 출력을/root/tfa-test/results/basic.txt에 저장합니다./root/tfa-test/tests/apply.tftest.hcl에env = "stage",replicas = 3으로command = apply인run "file_is_written"을 두고,file(output.conf_path)의 내용이 정확히name=app-stage와replicas=3두 줄인지 단언하세요. 이 파일만 걸러 돌린 출력을/root/tfa-test/results/apply.txt에 저장합니다./root/tfa-test/tests/chain.tftest.hcl에command = apply인run "make_dev"와, 그 run 의 출력을 참조해 값을 정하는command = plan인run "reuse_previous_output"을 두세요. 두 번째 run 은 출력name이app-prod인지 단언합니다. 이 파일만 걸러 돌린 출력을/root/tfa-test/results/chain.txt에 저장하세요./root/tfa-test/tests/validation.tftest.hcl에env = "qa"로 도는run "bad_env_is_rejected"와replicas = 99로 도는run "too_many_replicas_is_rejected"를 두고, 각각expect_failures로 해당 변수의 validation 이 막는 것이 정상임을 선언하세요. 이 파일만 걸러 돌린 출력을/root/tfa-test/results/validation.txt에 저장합니다./root/tfa-test/tests/fail.tftest.hcl에 일부러 틀리는 단언(출력name이app-nope인지)을 두고tofu test를 돌리세요. 출력은/root/tfa-test/results/fail.txt에, 종료 코드는/root/tfa-test/results/fail.rc에 한 줄로 저장합니다. 그다음 그 파일을/root/tfa-test/broken/fail.tftest.hcl로 옮겨 시험 묶음이 다시 통과하게 만드세요./root/tfa-test/tests/mock.tftest.hcl에mock_provider "local" {}를 두고env = "prod"로command = apply인run "mocked_apply"에서 출력name이app-prod인지 단언하세요. 이 파일만 걸러 돌린 출력을/root/tfa-test/results/mock.txt에 저장합니다./root/tfa-test/ci.sh를 만드세요. 자기가 놓인 디렉터리로 옮겨tofu init -input=false를 돌리고(실패하면 2로 끝남), 이어서tofu test를 돌려 출력을/root/tfa-test/results/ci.txt에 남기고 시험의 종료 코드를 그대로 돌려줘야 합니다. 만들고 나서 한 번 돌려 0 으로 끝나는지 확인하세요.
참고
- 파드에는 OpenTofu 1.9.0 과 local 프로바이더 미러가 있어 인터넷 없이 돕니다. tofu test 는 오프라인에서 그대로 동작합니다.
- 시험 파일은 현재 디렉터리나 tests 디렉터리에서 찾습니다. 특정 파일만 돌리려면 filter 옵션을 씁니다.
- apply 로 도는 run 은 실제로 만들고 끝나면 도구가 지웁니다. 시험이 남긴 찌꺼기가 없는지 out/ 을 확인해 보세요.
- 흔한 실수: CI 스크립트가 시험의 종료 코드를 삼키는 것. 초록불로 지나가는 파이프라인은 시험이 없는 것보다 나쁩니다.
- Command: test · Command: validate · Input Variables
시험을 붙일 대상을 만든다
/root/tfa-test/main.tf 에 var.env(dev·stage·prod 만 허용하는 validation)와 var.replicas(기본 1, 1~10 만 허용)를 받아 out/app-<env>.conf 에 name= 과 replicas= 두 줄을 쓰는 local_file.conf 를 두고, 출력 name·conf_path·replicas 세 개를 선언하세요. init 하고 tofu validate 로 확인합니다.
시험을 붙이려면 먼저 시험할 수 있는 모양이어야 합니다 — 바깥에서 값을 넣을 자리(변수)와 결과를 꺼낼 자리(출력)가 있어야 단언을 쓸 수 있습니다. validation 은 뒤에서 expect_failures 로 시험할 대상이 됩니다.
계획만 세우는 값싼 시험
/root/tfa-test/tests/basic.tftest.hcl 에 파일 수준 variables 로 env = "dev" 를 두고, command = plan 인 run "name_is_prefixed" 에서 출력 name 이 app-dev 인지 단언하세요. tofu test 를 이 파일만 걸러 돌리고 출력을 /root/tfa-test/results/basic.txt 에 저장합니다.
시험 파일은 현재 디렉터리나 tests 디렉터리에서 찾습니다. run 블록의 command 가 plan 이면 아무것도 만들지 않고 계획만 세워 단언하므로 빠릅니다 — 이름 규칙이나 조건 분기처럼 값만 보면 되는 것은 전부 이쪽으로 씁니다.
실제로 만들어 확인하는 비싼 시험
/root/tfa-test/tests/apply.tftest.hcl 에 env = "stage", replicas = 3 으로 command = apply 인 run "file_is_written" 을 두고, file(output.conf_path) 의 내용이 정확히 name=app-stage 와 replicas=3 두 줄인지 단언하세요. 이 파일만 걸러 돌린 출력을 /root/tfa-test/results/apply.txt 에 저장합니다.
apply 시험은 진짜로 만들고, 끝나면 도구가 스스로 지웁니다. 그래서 값싸지 않지만 '정말 그 파일이 그 내용으로 생기는가' 는 이렇게만 확인할 수 있습니다. 시험이 끝난 뒤 out/ 을 열어 무엇이 남았는지 보세요.
앞 시험의 결과를 다음 시험이 쓴다
/root/tfa-test/tests/chain.tftest.hcl 에 command = apply 인 run "make_dev" 와, 그 run 의 출력을 참조해 값을 정하는 command = plan 인 run "reuse_previous_output" 을 두세요. 두 번째 run 은 출력 name 이 app-prod 인지 단언합니다. 이 파일만 걸러 돌린 출력을 /root/tfa-test/results/chain.txt 에 저장하세요.
run 이름으로 앞 run 의 출력을 참조할 수 있습니다. 실무에서는 '먼저 네트워크를 만들고, 그 아이디로 앱을 계획한다' 처럼 단계가 이어지는 시험을 이렇게 씁니다. 파일 안의 run 은 위에서 아래로 차례대로 돕니다.
막혀야 하는 것이 막히는지 시험한다
/root/tfa-test/tests/validation.tftest.hcl 에 env = "qa" 로 도는 run "bad_env_is_rejected" 와 replicas = 99 로 도는 run "too_many_replicas_is_rejected" 를 두고, 각각 expect_failures 로 해당 변수의 validation 이 막는 것이 정상임을 선언하세요. 이 파일만 걸러 돌린 출력을 /root/tfa-test/results/validation.txt 에 저장합니다.
expect_failures 는 '이 run 은 실패해야 통과' 라고 뒤집어 말하는 장치입니다. 막아 주는 장치를 만들어 놓고 정말 막히는지 확인하지 않으면, 어느 날 조건을 잘못 고쳐도 아무도 모릅니다. 목록에는 실패해야 하는 대상의 주소를 적습니다.
실패하는 시험이 무엇을 보여 주는지 본다
/root/tfa-test/tests/fail.tftest.hcl 에 일부러 틀리는 단언(출력 name 이 app-nope 인지)을 두고 tofu test 를 돌리세요. 출력은 /root/tfa-test/results/fail.txt 에, 종료 코드는 /root/tfa-test/results/fail.rc 에 한 줄로 저장합니다. 그다음 그 파일을 /root/tfa-test/broken/fail.tftest.hcl 로 옮겨 시험 묶음이 다시 통과하게 만드세요.
실패 메시지가 무엇을 보여 주는지가 중요합니다 — 단언이 있던 줄과, 그때 값이 실제로 무엇이었는지가 함께 나옵니다. 그래서 error_message 에 값을 되풀이해 적을 필요가 없습니다. 종료 코드는 CI 가 읽는 유일한 신호입니다.
프로바이더를 흉내 내어 값만 시험한다
/root/tfa-test/tests/mock.tftest.hcl 에 mock_provider "local" {} 를 두고 env = "prod" 로 command = apply 인 run "mocked_apply" 에서 출력 name 이 app-prod 인지 단언하세요. 이 파일만 걸러 돌린 출력을 /root/tfa-test/results/mock.txt 에 저장합니다.
mock 프로바이더는 실제로 만들지 않고 그럴듯한 값을 돌려줍니다. 느리거나 돈이 드는 프로바이더를 시험할 때 값어치가 큽니다. 다만 흉내이므로 '정말 그 파일이 생기는가' 는 확인하지 못합니다 — 3단계의 진짜 apply 시험과 역할이 다릅니다.
CI 가 부를 한 줄로 묶는다
/root/tfa-test/ci.sh 를 만드세요. 자기가 놓인 디렉터리로 옮겨 tofu init -input=false 를 돌리고(실패하면 2로 끝남), 이어서 tofu test 를 돌려 출력을 /root/tfa-test/results/ci.txt 에 남기고 시험의 종료 코드를 그대로 돌려줘야 합니다. 만들고 나서 한 번 돌려 0 으로 끝나는지 확인하세요.
CI 가 읽는 신호는 종료 코드 하나뿐입니다. 시험이 실패했는데 스크립트가 0 으로 끝나면 파이프라인은 초록불로 지나갑니다 — 시험이 없는 것보다 나쁩니다. 채점기는 이 스크립트를 사본에서 두 번 돌립니다. 지금 그대로일 때와, 실패하는 시험을 하나 끼워 넣었을 때입니다.