AI에게 CI 파이프라인 재플랫폼화를 맡겨보았다. 무엇이 망가졌는가.
요약
AI 에이전트에게 CI 파이프라인 재플랫폼화를 맡겼을 때 발생한 문제와 교훈을 다룹니다. 런타임 환경의 변화를 간과하여 발생한 네트워크 및 설정 오류를 통해, AI 활용 시 엔드투엔드 검증의 중요성을 강조합니다.
핵심 포인트
- AI는 로직 재작성에는 능숙하지만 런타임 환경의 차이를 간과할 수 있음
- CI/CD 마이그레이션 시 엔드투엔드 검증은 필수적임
- 사소한 설정 오류(SSH 키 줄바꿈 등)가 복잡한 인프라 문제로 오인될 수 있음
- 새로운 환경(Pod 기반 러너)에 따른 네트워크 라우팅 차이 주의
원문은 wostal.eu에서 처음 게시되었습니다.
요약 (TL;DR): 나는 CI 재플랫폼화(GitHub Actions → Argo Workflows, GitHub → Codeberg)를 AI 에이전트에게 맡겼다. 재작성된 코드는 올바르게 보였고 린트(lint)도 통과했지만, 한 가지를 고려하지 못했다. 바로 런타임 환경(runtime environment)이 완전히 바뀌었다는 점이다. 이전 러너(runner)는 외부 VM이었으나, 새로운 러너는 클러스터 내부의 포드(pod)이며, 이곳에서는 Tailscale IP를 라우팅할 수 없다. 아무도 파이프라인을 엔드투엔드(end-to-end)로 실행해보지 않았기에, 모든 차이점은 지뢰로 남았다 — 총 6개의 지뢰가 있었다. 가장 최악이었던 점은 AI의 실수가 아니었다. 진짜 원인은 SSH 키의 누락된 줄바꿈 하나였는데, 나는 네트워크 탓을 하며 몇 시간을 허비했다는 것이다.
이 글은 다른 사건의 후속편이다. 내가 homelab k3s 컨트롤 플레인(control plane)을 SQLite에서 etcd로 마이그레이션하는 동안, 그 마이그레이션을 실행했던 파이프라인이 마이그레이션 자체보다 더 많은 교훈을 주는 방식으로 망가져 있었다.
이 포스트에서 다룰 내용:
-
혼란이 어디서 시작되었는가 — 아무도 엔드투엔드(end-to-end)로 검증하지 않은 AI 지원 CI 재플랫폼화
-
유령 CNI 버그 — MTU, Tailscale, 방화벽(firewall), 그리고 conntrack을 추적하며 보낸 몇 시간
-
진짜 범인 — SSH 키에 누락된 단 하나의 줄바꿈
-
초기 실패가 왜 나중의 실패를 숨기는가 — 하나씩 드러난 6개의 잠재적 버그
-
마이그레이션 위임에 관한 교훈 — 사람에게 맡기든 AI에게 맡기든
혼란이 어디서 시작되었는가
전체 CI가 막 GitHub Actions에서 Argo Workflows로, 그리고 GitHub에서 Codeberg로 마이그레이션되었으며 — 이 마이그레이션은 AI 에이전트에게 맡겨졌다 🙂.
재플랫폼화(re-platforming) 과정에서 로직은 거의 1:1로 다시 작성되었지만, 런타임 환경(runtime environment)이 근본적으로 변경되었다는 사실을 포착하지 못했습니다. 기존 러너(Runner)(GitHub Actions / ARC)는 한 가지 방식으로 동작했지만, 새로운 러너는 **클러스터 내의 포드(pod)**입니다. 이 포드에서는 Tailscale IP는 라우팅할 수 없지만, 프라이빗 IP(private IP)는 라우팅이 가능합니다. 사람도 AI도 파이프라인을 처음부터 끝까지(end-to-end) 실행해 보지 않았기에, 이러한 모든 차이점은 지뢰처럼 남아 있었습니다.
이것이 바로 **6개의 잠재적 버그(latent bugs)**와 제가 곧 설명할 프리징(hang) 현상이 발생한 이유입니다. 이는 무작위적인 결함이 아니라, 새로운 환경에서 검증되지 않은 GHA→Argo 변환의 흔적이었습니다.
유령 CNI 버그
마이그레이션 이후, 배포 파이프라인(클러스터 내에서 실행되는 CI)이 한 번도 엔드 투 엔드(end-to-end)로 통과하지 못했다는 사실이 밝혀졌습니다. ansible-configure가 gather_facts 단계에서 멈춰(hanging) 있었습니다. 저는 이 문제를 해결하기 위해 몇 시간을 보냈고, 계속해서 잘못된 판단을 내렸습니다:
- "MTU 문제다" → MTU를 1240으로 설정한 테스트도 멈췄습니다.
- "Tailscale 오버레이 문제다" → (테스트 시) 프라이빗 IP도 멈췄습니다.
- "방화벽 문제다" → 방화벽을 열었음에도 퍼블릭 IP도 멈췄습니다.
- "CNI의 비대칭 라우팅(asymmetric routing) 또는 conntrack 문제다" →
hostNetwork포드도 멈췄습니다.
이 모든 테스트는 네트워크를 가리키고 있었습니다. 그리고 그 모든 테스트가 저를 속이고 있었습니다.
진짜 범인
각 테스트는 단 하나의 버그에 의해 오염되어 있었습니다. 테스트 포드에서 시크릿(secret)으로부터 cp 명령어를 사용하여 SSH 키를 마운트할 때, 끝에 줄바꿈(newline)을 포함하지 않았던 것입니다. 이는 error in libcrypto를 발생시키고, 키가 로드되지 않게 만들며, 결과적으로 ssh가 무작위로 동작하게 만듭니다(때로는 멈추고, 때로는 "permission denied" 발생). 저는 유령을 쫓고 있었던 것입니다. 네트워크가 아니라 테스트 장비(test rig)가 고장 나 있었기 때문입니다.
돌파구는 제가 이를 제대로 수행했을 때에야 나타났습니다: 노드에서 tcpdump + conntrack을 사용하고, 키를 CI가 쓰는 방식과 정확히 일치하게 작성(printf '%s\n')하는 것이었습니다. 그러자 상황이 명확하고 일관되게 파악되었습니다. Pod에서 사설 IP (10.0.1.x)는 작동하는 반면 (ssh + sftp + 전체 ansible -m setup), Tailscale IP (100.x)는 멈춰(hang) 있었습니다 — Pod가 tailnet으로 들어가는 경로(route)가 없었기 때문입니다. 해결책은 CI가 사설 IP를 사용하는 것이었습니다 (어차피 방화벽은 사설 네트워크를 필터링하지 않습니다). 즉시 해결되었습니다.
교훈 #1: 네트워크/CNI/MTU를 탓하기 전에, 테스트 장비(test rig)가 고장 나지 않았음을 증명하세요. 키에서 누락된 줄바꿈 하나 때문에 존재하지도 않는 CNI 문제를 쫓느라 몇 시간을 허비했습니다. 운영 환경과 동일하게(identically to production) 재현하고(여기서는 CI가 키를 쓰는 방식과 정확히 일치하게), "패킷이 중간에 사라지는" 현상에는 추측이 아닌
tcpdump/conntrack을 사용하세요. "내 노트북에서는 되는데 Pod에서는 안 된다"는 것이 항상 네트워크 문제는 아닙니다. 때로는 테스트 장비의 아티팩트(artifact)일 수 있습니다.
초기 실패가 이후의 실패를 숨기는 이유
두 번째 함정은 이것입니다. 배포 파이프라인이 apply+ansible 단계에 도달하지 못했기 때문에, 각 후속 버그는 이전 버그가 해결되어 작동하기 시작할 때마다 하나씩 드러났습니다. 총 여섯 개가 있었습니다:
- 러너(runner) 이미지에
openssh누락 python3누락 (ansible은 제어 노드(control node)에 이것이 필요함)- 단계(step) 간에 전달되지 않은 kubeconfig 아티팩트
- 실제로 아무것도 차단하지 않는
duration:0설정의 승인 게이트(approve gate) - ansible이 사설 네트워크 대신 Tailscale을 통해 연결을 시도함 (위에서 언급한 유령 CNI 버그)
semantic-release에서 git 인증 실패
교훈 #2: 초기에 실패하는 파이프라인은 하위의 모든 버그를 가립니다. 하나를 고치고 다시 실행하면, 이전에는 도달할 수 없었던 다음 버그가 마침내 실행되며 실패합니다. 첫 번째로 전체 과정(end-to-end)이 성공(green)하는 순간은 숨겨져 있던 모든 것이 한꺼번에 드러나는 순간입니다. 그 실행을 대비하세요. "린트(lint)를 통과했으니 작동할 것이다"라고 가정하지 마세요.
마이그레이션 위임에 관한 교훈
교훈 #3: CI를 재플랫폼화(re-platforming)하는 것(GHA→Argo, GitHub→Codeberg)은 사람이 했든 AI가 했든, 엔드투엔드(end-to-end) 실행이 반드시 필요합니다. "1:1"로 다시 작성된 마이그레이션은 올바르게 보이고 린트(lint)도 통과하지만, **런타임 환경의 차이(runtime-environment differences)**를 놓치게 됩니다. 예를 들어 외부 러너(external runner)와 클러스터 내부 포드(in-cluster pod)의 차이, GitHub API와 Forgejo의 차이 같은 것들입니다. 이러한 차이는 정적 리뷰(static review)로는 나타나지 않으며, 오직 전체 과정을 한 번 통과해야만 확인할 수 있습니다. 만약 이러한 마이그레이션을 (AI 에이전트에게 🙂) 위임한다면, 즉시 **전체 경로에 대한 스모크 테스트(smoke-run)**를 계획하십시오. 그렇지 않으면 몇 달 뒤 새벽 3시에 그 차이(delta)를 발견하게 될 것입니다.
이것은 마이그레이션에 AI를 사용하는 것에 반대하는 논거가 아닙니다. 재작성된 내용은 대부분 정확했으며, 실제로 시간을 절약해 주었습니다. 이것은 인간이 루프(loop) 내에서 어떤 역할을 유지해야 하는지에 대한 논거입니다. YAML을 다시 타이핑하는 것이 아니라, 새로운 환경에서 실제로 작동하는지 검증하는 것에 집중해야 합니다. AI는 언어를 번역했지만, 환경을 번역할 수는 없었습니다. 왜냐하면 AI는 그곳에서 한 번도 실행해 본 적이 없기 때문입니다. 저 역시 실행되어 망가지기 전까지는 그러했습니다.
피날레 — 완전한 GitOps
수정 후, 파이프라인은 의도한 대로 작동합니다: main으로 머지(merge) → 센서(sensor) → 배포 파이프라인(deploy pipeline) → plan → [approve: suspend] → apply → ansible → release 순서로 진행되며, 수동 게이트(argo resume = "승인 후")가 포함됩니다. 저는 실제 머지 상황에서 이를 검증했으며, 릴리스 v1.4.1을 깔끔하게 배포했습니다. 제어 평면(control plane)은 etcd에, 재해 복구(DR)는 로컬 및 S3에, 설정은 중간에 인간 게이트가 있는 GitOps를 통해 관리됩니다.
한 주말 동안 두 건의 사고가 있었습니다. 하나는 실험실이 성장할 때 기본 설정(defaults)이 지뢰가 될 수 있다는 것을 가르쳐 주었습니다. 이번 사고는 마이그레이션을 작성한 주체가 아무리 자신만만하더라도, 새로운 환경에서 실제로 엔드투엔드(end-to-end)로 실행되기 전까지는 마이그레이션이 완료된 것이 아니라는 점을 가르쳐 주었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기