GitHub Actions의 YAML 앵커 지원이 왜 나쁜 결정인지 설명한다. 기존 기능과의 중복성, 데이터 모델 복잡화로 인한 인간·도구 분석 난이도 상승, 그리고 유일하게 유용할 수 있는 머지 키 미지원까지 짚으며 즉시 지원 철회를 촉구한다.
TL;DR: 아주 오랫동안 GitHub Actions는 YAML 앵커를 지원하지 않았다.
그건 좋은 일이었다. GitHub Actions에서 YAML 앵커는 (1) 기존 기능과 중복되고, (2) CI/CD를 사람과 기계가 이해하기 어렵게 만드는 데이터 모델의 복잡성을 도입하며, (3) GitHub가 유일하게 의미 있는 기능(머지 키)을 지원하지 않기로 했기 때문에 특별히 유용하지도 않다.
이 퇴보는 GitHub Actions를 기본값이 불안전한 CI/CD 플랫폼으로서의 지위를 강화한다. 사람과 기계 모두가 액션과 워크플로 정의의 취약점을 분석하기 더 어렵게 만들기 때문이다. GitHub는 YAML 앵커 지원을 즉시 제거해야 하며, 채택이 널리 퍼지기 전에 그렇게 해야 한다.
GitHub는 최근 공지에서 GitHub Actions가 이제 YAML 앵커를 지원한다고 밝혔다. 이는 사용자가 다음과 같이 작성할 수 있음을 의미한다:
1
2
3
4
5
6
7
8
9
10
11
12
jobs:
job1:
env: &env_vars # 첫 사용 시 앵커 정의
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- run: echo "Using production settings"
job2:
env: *env_vars # 환경 변수를 재사용
steps:
- run: echo "Same environment variables here"
겉보기에는 그럴듯한 기능처럼 보인다. GitHub Actions의 job/step 추상화는 중복을 낳기 쉽고, YAML 앵커는 그 중복을 줄이는 한 가지 방법이니까.
안타깝게도 YAML 앵커는 이 일에 끔찍한 도구다. 더 나아가(곧 보겠지만) GitHub의 YAML 앵커 구현은 불완전해서, YAML 앵커가 그나마 고유하게 유용할 수 있는 작은 부분집합의 사용 사례(그렇다 해도 좋은 생각은 아니지만)를 원천 봉쇄한다. 그 이유를 아래에서 보자.
사진: 작성자가 이해한 GitHub Actions 제품 로드맵.
YAML 앵커가 나쁜 아이디어인 가장 단순한 이유는, 중복을 줄이기 위한 보다 명시적인 다른 메커니즘들과 중복되기 때문이다.
GitHub의 위 예시는 YAML 앵커 없이 다음처럼 다시 쓸 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
jobs:
job1:
steps:
- run: echo "Using production settings"
job2:
steps:
- run: echo "Same environment variables here"
이 버전이 훨씬 명확하지만 의미론은 약간 다르다. 모든 job이 워크플로 수준의 env를 상속한다. 그런데 내 생각엔, 이건 _오히려 좋은 점_이다. 일부 job에만 환경 변수를 템플릿으로 공유해야 한다는 요구 자체가 워크플로 설계의 아키텍처적 오류를 시사한다.
다르게 말하자면: job이나 step 사이에서 “글로벌” 설정을 공유하려고 YAML 앵커를 쓰고 싶어지는 상황이라면, 아마 실제로는 별도의 워크플로나, 적어도 job 수준의 env 블록을 가진 분리된 job이 필요할 것이다.
요컨대: YAML 앵커는 워크플로, job, step의 추상화를 더욱 흐리게 만든다. 나머지 시스템의 규칙을 따르지 않는 횡단적 전역 상태를 도입하기 때문이다. 내겐 이것이 현재 Actions 팀이 GitHub Actions를 어떻게 사용해야 하는지에 대해 강한 의견이 없음을 시사하며, 모든 사용자를 똑같이 만족시키지 못하는 “주워담기”식 접근으로 이어진 듯 보인다.
앞서 언급했듯, YAML 앵커는 GitHub Actions에 새로운 형태의 비국소성을 도입한다. 더군다나 이 비국소성은 완전히 일반적이다. 어떤 YAML 노드든 앵커를 달아 참조할 수 있다. 이는 사람과 기계 모두에게 나쁜 아이디어다:
공정하게 말하면 GitHub Actions에는 이미 몇 가지 비국소성이 있다. 전역 컨텍스트, env 블록의 스코프 규칙, needs 의존성, step/job 출력 등등. 이것들도 디버그가 어렵긴 하다! 하지만 이들에는 공통적으로 일반성이 부족하다는 특징이 있다. 각자는 정확한 의미론과 스코프 규칙을 지니므로, 그 규칙을 이해하는 사용자는 환경 변수나 출력의 출처를 일일이 참조하지 않아도 작업 단위가 무엇을 하는지 이해할 수 있다.
여기서의 고통은 GitHub Actions가 그동안 따르던 일대일 객체 모델1에서 YAML 앵커가 일탈한다는 사실로 귀결된다.
앵커가 들어오면 그 매핑이 일대다로 바뀐다. 동일한 요소가 소스에는 한 번만 나타나지만, 로드된 객체 표현에는 여러 번 나타날 수 있다.
사실상, 많은 도구가 GitHub Actions의 YAML에 대해 가정해 온 중요한 전제를 깨뜨린다. 즉, 역직렬화된 객체의 엔터티가 소스 YAML의 단 하나의 구체적 위치로 _되돌아갈 수 있어야 한다는 전제다.
이는 오류 메시지에 합당한 소스 위치를 제시하는 데 필요하지만, 객체 모델이 앵커와 참조를 명시적으로 표현하지 않는다면 성립하지 않는다.
게다가, 널리 쓰이는 모든 YAML 파서의 현실은 이렇다. 널리 쓰이는 모든 YAML 파서는(합리적으로) 앵커된 값을 참조되는 각 위치에 _복사_하기로 선택한다. 즉, 분석 도구는 소스 위치를 위해 “원본 요소”를 볼 수 없다.
이 고통은 내가 직접 겪는다. 나는 GitHub Actions용 정적 분석 도구인 zizmor를 유지하고 있고, zizmor는 위 두 전제를 모두 둔다. 더구나 zizmor의 _의존성_들도 이 전제를 둔다. serde_yaml은(대부분의 다른 YAML 파서들처럼) YAML 앵커를 역직렬화할 때, 참조되는 각 위치에 앵커된 값을 _복사_하기로 선택한다2.
YAML 앵커가 고유하게 유용할 수 있는 몇 안 되는 것 중 하나가 바로 머지 키다. 머지 키를 사용하면 여러 참조된 매핑을 하나의 매핑으로 _합성_할 수 있다.
YAML 스펙의 예시는 사용 사례를 잘 보여줄 뿐 아니라, 머지 키가 얼마나 혼란스러운지도 깔끔히 보여준다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
---
- &CENTER { x: 1, y: 2 }
- &LEFT { x: 0, y: 2 }
- &BIG { r: 10 }
- &SMALL { r: 1 }
# 아래의 모든 맵은 동일하다:
- # 명시적 키들
x: 1
y: 2
r: 10
label: center/big
- # 맵 하나만 머지
<< : *CENTER
r: 10
label: center/big
- # 여러 맵 머지
<< : [ *CENTER, *BIG ]
label: center/big
- # 오버라이드
<< : [ *BIG, *LEFT, *SMALL ]
x: 1
label: center/big
개인적으로 이 문법은 읽기가 엄청 어렵다고 느끼지만, 적어도 GitHub Actions에서 고유하게 유용할 수 있는 사용 사례는 있다. 우선순위가 명확한 규칙으로 여러 환경 변수 묶음을 합성하는 일은 분명히 유용하다.
문제는: GitHub Actions는 머지 키를 지원하지 않는다는 점이다! 그들은 이미 앵커/참조를 어느 정도 지원하던 자체 YAML 파서를 쓰는 것으로 보이지만, 머지 키는 아니다.
내게 이 상황은, 나쁜 기술적 결정들(그리고 GitHub Actions를 어떻게 써야 하는지에 대한 강한 의견의 부재)에서 코미디로 격상된다. GitHub Actions 맥락에서 YAML 앵커를 고유하게 유용하게 만드는 딱 한 가지가 머지 키인데, GitHub Actions가 지원하지 않는 것도 바로 그 한 가지다.
요약하면, GitHub Actions의 YAML 앵커는 (1) 기존 기능과 중복되고, (2) CI/CD를 사람과 기계가 이해하기 어렵게 만드는 데이터 모델의 복잡성을 도입하며, (3) GitHub가 의미상의 동등물이 없는 기능(머지 키)을 지원하지 않기로 했기 때문에 고유하게 유용하지도 않다.
이 중에서 나는 (2)가 가장 중요하다고 본다. GitHub Actions 보안은 최근아주 자주 뉴스에 오르고 있는데, 압도적 합의는 GitHub Actions 워크플로 안에 취약점을 도입하거나(혹은 잠재된 취약점을 워크플로를 통해 노출시키는 일이) 너무 쉽다는 것이다.
이 때문에 우리는 사람과 기계 모두가 쉽게 분석할 수 있는 GitHub Actions를 필요로 한다. 사실상 이는 GitHub가 GitHub Actions의 복잡성을 낮춰야 한다는 뜻이지, 올려야 한다는 뜻이 아니다. YAML 앵커는 앞서 말한 모든 이유로 잘못된 방향의 한 걸음이다.
물론 나도 이해관계가 없진 않다. 나는 GitHub Actions용 정적 분석 도구를 유지하고 있고, YAML 앵커 지원은 내게 정말 미친 듯이 고통스러운 일3이 될 것이다. 하지만 나만의 문제가 아니다. actionlint, claws, poutine 같은 도구들도 YAML 앵커 지원에 고생할 가능성이 높다. 이 변화가 각 도구가 전제해 온 GitHub Actions 데이터 모델과의 관계를 근본적으로 바꾸기 때문이다. 현재 상태로도, 이 변경은 오픈 소스 생태계가 GitHub Actions의 정합성과 보안을 분석할 수 있는 능력에 거대한 구멍을 낸다.
종합하면: 나는 GitHub가 GitHub Actions의 YAML 앵커 지원을 즉시 제거해야 한다고 강하게 믿는다. “좋은” 소식이라면, 지원이 공개된 지 며칠밖에 안 되었고 채택도 (아마) 재사용 가능한 액션/워크플로 레이어가 아니라 단일 사용 워크플로 레이어에 국한되어 있으니, 사용자 혼란을 최소화하면서 철회할 수 있을 것이다.
그 객체 모델은 사실상 JSON 객체 모델과 동일하다. 모든 요소가 소스 표현의 리터럴 구성요소로 나타나며, 가능한 타입의 작은 부분집합(문자열, 숫자, 불리언, 배열, 객체, null)을 취한다.↩
달리 말하면: YAML 자체가 JSON의 상위집합이긴 하지만, 사용자는 YAML스러운 요소들이 객체 모델로 흘러나오길 원치 않는다. 모두가 JSON 객체 모델을 원하며, 이는 역직렬화된 구조 어딘가에 “앵커”나 “참조” 요소가 없어야 함을 의미한다.↩
어느 정도냐 하면, 실질적인 범위로 앵커를 지원할 가치가 있는지도 확신이 없고, 대신 즉시 난독화 시도로 플래그를 올릴까 고민하는 수준이다.↩