Moa 표현식 언어
Moa 표현식 언어
Moa는 잡 실행 중에 값을 동적으로 구성하기 위한 표현식 언어예요. 표현식은 ${{ }} 구분자로 묶이며, GitLab Functions와 잡 inputs에서 사용돼요.
Moa는 문자열 조작, 산술, 비교, 논리 연산, 속성 접근, 함수 호출을 지원해요.
출처: 문서
본문
CI/CD 표현식과의 차이점
GitLab에는 파이프라인 수명 주기의 서로 다른 단계에서 서로 다른 목적을 제공하는 세 가지 표현식 문법이 있어요.
- Rules는
rules:키워드 안에서 자체 표현식 문법을 사용해 잡 포함을 제어해요. 파이프라인 생성 중에 평가되며 CI/CD 변수에 대한 비교와 패턴 매칭을 지원하지만, 산술이나 런타임 상태 접근은 할 수 없어요. - CI/CD 표현식은
$[[ ]]문법을 사용하며, 어떤 잡도 실행되기 전인 파이프라인 생성 중에 평가돼요. 이 표현식은 CI/CD inputs, matrix 값, 컴포넌트 inputs에 대한 값 치환을 수행해요. 산술, 비교, 논리를 수행할 수 없고 런타임 상태에 접근할 수 없어요. 자세한 내용은 CI/CD 표현식을 참고해요. - Moa는
${{ }}문법을 사용하며 잡 실행 중에 러너에 의해 평가돼요. Moa는 연산자, 데이터 구조, 함수 호출이 있는 완전한 표현식 언어예요.
세 가지 문법은 같은 파이프라인에서 공존할 수 있어요. GitLab Functions를 포함하는 CI/CD 컴포넌트는 세 가지를 모두 사용할 수 있어요.
spec:
inputs:
echo_version:
type: string
---
hi-job:
# rules expression - evaluated when the pipeline is created
rules:
- if: $CI_COMMIT_BRANCH == "main"
run:
- name: say_hi
# $[[ ]] - resolved when the pipeline is created
step: registry.gitlab.com/gitlab-org/ci-cd/runner-tools/gitlab-functions-examples/echo@$[[ inputs.echo_version ]]
inputs:
# ${{ }} - resolved when the job runs
message: "Hello, ${{ vars.CI_PROJECT_NAME }}"
Moa가 별도의 언어로 존재하는 이유는 GitLab Functions에 파이프라인 생성 시점에는 사용할 수 없는 기능들이 필요하기 때문이에요.
- 런타임 평가: 스텝 출력은 함수가 실행될 때까지 존재하지 않아요.
${{ steps.build.outputs.image_ref }}같은 표현식은 실행 중에만 평가할 수 있어요. - 타입 있는 값: Moa는 네이티브 타입(숫자, 불리언, 배열, 객체)을 보존하고, 함수 사이에 문자열로 변환하지 않고 전달해요.
- 연산자와 논리: GitLab Functions는 변수와 출력에서 스텝 inputs를 구성하려면 산술(
major_version + 1), 비교(vulnerabilities == 0), 단락 논리(inputs.tag || "latest")가 필요해요. - 민감 값 추적: Moa는 연산을 통해 민감한 값을 전파해요. 민감한 값을 문자열에 이어붙이거나 함수 호출로 전달하면 결과도 민감한 값으로 취급돼요. 이는 로그와 출력에서 시크릿이 우발적으로 노출되는 것을 방지해요.
컨텍스트 참조
표현식에서 사용 가능한 값은 표현식이 사용된 위치에 따라 달라져요.
| 컨텍스트 | 사용 가능 위치 | 타입 | 평가 시점 | 설명 |
|---|---|---|---|---|
| job.inputs | 잡 구성: script, before_script, after_script, artifacts, cache, image, services | Object | 러너가 잡을 받을 때 | 잡에 정의된 input 값. 개별 변수는 job.inputs.name>로 접근. |
| env | GitLab Functions | Object | 함수가 실행되기 전 | 함수에서 사용 가능한 환경 변수. 개별 변수는 env.name>로 접근. |
| inputs | GitLab Functions | Object | 함수가 실행되기 전 | 함수에 전달된 input 값. 개별 input은 inputs.name>로 접근. |
| vars | GitLab Functions | Object | 함수가 실행되기 전 | CI 잡에서 전달된 잡 변수. 개별 변수는 vars.name>로 접근. |
| steps | GitLab Functions | Object | 함수가 실행되기 전 | 현재 함수에서 이전에 실행된 스텝의 결과. 스텝 출력은 steps.step_name>.outputs.output_name>로 접근. |
| export_file | GitLab Functions | String | 함수가 실행되기 전 | 함수가 후속 스텝에 내보낼 환경 변수를 쓸 수 있는 파일의 경로. |
| output_file | GitLab Functions | String | 함수가 실행되기 전 | 함수가 출력 값을 쓰는 파일의 경로. |
| func_dir | GitLab Functions | String | 함수가 실행되기 전 | 함수 정의 파일이 들어 있는 디렉터리 경로. 함수와 함께 번들된 파일을 참조하는 데 사용. |
| work_dir | GitLab Functions | String | 함수가 실행되기 전 | 현재 실행의 작업 디렉터리 경로. |
템플릿 문법
보간 (Interpolation)
표현식을 평가하려면 ${{ }}로 감싸요.
script:
- echo "Hello, ${{ job.inputs.name }}"
텍스트가 표현식을 감싸고 있으면 결과는 항상 문자열로 변환돼요. 단일 값에 여러 표현식이 나타날 수 있어요.
script:
- echo "${{ job.inputs.greeting }}, ${{ job.inputs.name }}!"
네이티브 타입 통과
${{ expression }}이 주변 텍스트 없이 값 전체일 때, 표현식은 네이티브 타입을 반환해요. 숫자, 불리언, 배열, 객체 같은 비문자열 값을 문자열로 변환하지 않고 스텝 사이에 전달하려면 네이티브 타입 표현식을 사용해요.
inputs:
count: ${{ steps.previous.outputs.total }}
이 예시에서 total이 숫자라면, count는 문자열 표현이 아닌 숫자를 받아요.
Moa 표현식 이스케이프하기
보간을 트리거하지 않고 텍스트에 문자 그대로의 ${{를 포함하려면 백슬래시로 이스케이프해요.
script:
- echo "Use \${{ to start an expression"
이 명령은 평가 없이 Use ${{ to start an expression 텍스트를 출력해요.
리터럴
Null
null 키워드는 값이 없음을 나타내요.
${{ null }}
불리언
true와 false 키워드는 불리언 값을 나타내요.
${{ true }}
${{ false }}
숫자
숫자는 53비트 정밀도의 IEEE 754 배정밀도 부동소수점 값이에요. 2^53(약 9경)보다 큰 정수는 정확히 표현할 수 없어요. 정수, 소수, 과학적 표기법이 지원돼요.
${{ 42 }}
${{ 3.14 }}
${{ 1.5e3 }}
${{ 2E-4 }}
문자열
문자열은 큰따옴표나 작은따옴표로 묶어요. 두 따옴표 유형은 이스케이프 시퀀스와 템플릿 표현식을 다르게 처리해요.
큰따옴표 문자열은 템플릿 표현식과 전체 이스케이프 시퀀스 세트를 지원해요.
| 시퀀스 | 의미 |
|---|---|
| \ | 백슬래시 |
| " | 큰따옴표 |
| \n | 줄바꿈 |
| \r | 캐리지 리턴 |
| \t | 탭 |
| \a | 경고(벨) |
| \b | 백스페이스 |
| \f | 폼 피드 |
| \v | 세로 탭 |
| / | 슬래시 |
| \uXXXX | 유니코드 코드 포인트 |
| ${{ | 문자 그대로의 ${{ (보간 방지) |
큰따옴표 문자열 안의 템플릿 표현식(${{ }})은 평가되어 문자열로 보간돼요.
작은따옴표 문자열은 해석을 최소화한 원시 문자열 리터럴이에요. 작은따옴표 문자열 안의 템플릿 표현식은 평가되지 않아요. 두 가지 이스케이프 시퀀스만 지원돼요.
| 시퀀스 | 의미 |
|---|---|
| \ | 백슬래시 |
| ' | 작은따옴표 |
${{ "Hello\nWorld" }}
${{ 'It\'s a string' }}
${{ 'Literal ${{ not evaluated }}' }}
식별자
식별자는 표현식 컨텍스트에서 값을 참조해요. 식별자는 문자나 밑줄로 시작하고 문자, 숫자, 밑줄을 포함할 수 있어요. 식별자는 대소문자를 구분해요. foo, Foo, FOO는 서로 다른 식별자예요.
${{ env }}
${{ my_variable }}
식별자는 사용 가능한 컨텍스트에 대해 해석돼요. 각 컨텍스트에서 사용 가능한 값은 컨텍스트 참조를 참고해요.
식별자가 컨텍스트 객체를 가리키면 전체 객체가 반환돼요. 예를 들어 ${{ vars }}는 모든 잡 변수를 객체로 반환해요.
연산자
산술 연산자
산술 연산자는 숫자에 대해 동작해요. + 연산자는 문자열도 이어붙여요. 연산자는 암시적 타입 변환을 수행하지 않으므로 "hello" + 42는 오류가 나요.
| 연산자 | 설명 | 예시 | 결과 |
|---|---|---|---|
| + | 더하기 | ${{ 2 + 3 }} | 5 |
| + | 이어붙이기 | ${{ "a" + "b" }} | "ab" |
| - | 빼기 | ${{ 10 - 4 }} | 6 |
| * | 곱하기 | ${{ 3 * 4 }} | 12 |
| / | 나누기 | ${{ 10 / 3 }} | 3.333... |
| % | 나머지(절삭 나눗셈) | ${{ 10 % 3 }} | 1 |
0으로 나누면 오류가 나요.
비교 연산자
비교 연산자는 불리언 값을 반환해요.
| 연산자 | 설명 | 예시 | 결과 |
|---|---|---|---|
| == | 같음 | ${{ 1 == 1 }} | true |
| != | 같지 않음 | ${{ 1 != 2 }} | true |
| < | 작음 | ${{ 1 < 2 }} | true |
| <= | 작거나 같음 | ${{ 2 <= 2 }} | true |
| > | 큼 | ${{ 3 > 2 }} | true |
| >= | 크거나 같음 | ${{ 3 >= 3 }} | true |
서로 다른 타입의 값은 타입별로 비교되므로 1 == "1"은 false로 평가돼요. 같은 타입의 값은 다음 비교 규칙을 따라요.
- 숫자: 숫자 비교.
- 문자열: 사전순 비교(UTF-8 바이트 순서).
- 불리언:
false가true보다 작음. - 배열: 요소별 비교.
- 객체: 길이, 키, 값 순으로 비교. 키 순서는 중요하지 않아요.
- Null:
null은null과 같음.
논리 연산자
논리 연산자는 단락 평가를 사용하고, 반드시 불리언이 아닌 피연산자 중 하나를 반환해요. 이 동작은 JavaScript의 &&와 || 연산자와 비슷해요.
| 연산자 | 설명 | 동작 |
|---|---|---|
| || | 논리 OR | 왼쪽 피연산자가 truthy면 그대로 반환하고, 그렇지 않으면 오른쪽 피연산자를 평가해 반환. |
| && | 논리 AND | 왼쪽 피연산자가 falsy면 그대로 반환하고, 그렇지 않으면 오른쪽 피연산자를 평가해 반환. |
| ! | 논리 NOT | 피연산자가 falsy면 true, truthy면 false 반환. |
|| 연산자는 기본값을 제공하는 데 사용돼요.
${{ inputs.name || "default" }}
inputs.name이 비어 있지 않은 문자열이면 그대로 반환돼요. 비어 있거나 null이면 "default"가 반환돼요.
단항 연산자
| 연산자 | 설명 | 예시 | 결과 |
|---|---|---|---|
| + | 단항 플러스 | ${{ +5 }} | 5 |
| - | 단항 부정 | ${{ -5 }} | -5 |
| ! | 논리 NOT | ${{ !true }} | false |
연산자 우선순위
연산자는 우선순위가 높은 것부터 낮은 것 순으로 나열돼요. 같은 줄의 연산자는 같은 우선순위예요. 모든 이진 연산자는 왼쪽 결합이에요.
| 우선순위 | 연산자 |
|---|---|
| 7 (가장 높음) | ., [], () |
| 6 | +, -, ! |
| 5 | *, /, % |
| 4 | +, - |
| 3 | ==, !=, <, <=, >, >= |
| 2 | && |
| 1 (가장 낮음) | || |
우선순위를 재정의하려면 괄호를 사용해요.
${{ (1 + 2) * 3 }}
데이터 구조
배열
대괄호 표기법으로 배열을 만들어요. 요소는 어떤 타입이든 될 수 있고 타입을 섞을 수도 있어요. 후행 쉼표를 사용할 수 있어요.
${{ [1, 2, 3] }}
${{ ["a", 1, true, null] }}
${{ [] }}
객체
중괄호 표기법으로 객체를 만들어요. 키는 문자열로 평가되어야 해요. 값은 어떤 타입이든 될 수 있어요. 후행 쉼표가 허용돼요.
${{ {name: "runner", version: 1} }}
${{ {"string-key": true} }}
${{ {} }}
객체 키로 사용된 일반 식별자는 변수 참조가 아닌 문자열 리터럴로 취급돼요. 변수를 키로 사용하려면 괄호로 감싸요.
${{ {name: "Alice"} }} # "name" is the string "name", not a variable reference
${{ {(obj.prop): "value"} }} # key is the value of obj.prop, which must be a string
속성 접근
점 표기법
점 표기법으로 객체 속성에 접근해요.
${{ env.HOME }}
${{ steps.build.outputs.artifact_path }}
대괄호 표기법
인덱스로 배열 요소에, 문자열 키로 객체 속성에 접근해요.
${{ my_array[0] }}
${{ my_object["property-name"] }}
속성 이름에 하이픈 같은 특수 문자가 포함되면 대괄호 표기법이 필요해요.
체이닝
속성 접근과 함수 호출을 체이닝해요.
${{ steps.build.outputs.items[0] }}
함수 호출
괄호로 함수를 이름으로 호출해요.
${{ str(42) }}
${{ num("3.14") }}
Truthiness
논리 연산자와 ! 연산자는 다음 truthiness 규칙을 사용해요.
| 타입 | Truthy일 때 | Falsy일 때 |
|---|---|---|
| 불리언 | true | false |
| 문자열 | 길이 0 초과 | 빈 문자열 "" |
| 숫자 | 0 아님 | 0 |
| 배열 | 길이 0 초과 | 빈 배열 [] |
| 객체 | 길이 0 초과 | 빈 객체 {} |
| Null | 항상 아님 | 항상 |
내장 함수
str(value)
어떤 값이든 문자열 표현으로 변환해요.
${{ str(42) }} # "42"
${{ str(true) }} # "true"
${{ str(null) }} # "<null>"
num(value)
문자열을 숫자로 변환해요. 문자열은 유효한 숫자 표현이어야 해요.
${{ num("42") }} # 42
${{ num("3.14") }} # 3.14
bool(value)
어떤 값이든 truthiness에 따라 불리언으로 변환해요.
${{ bool("hello") }} # true
${{ bool("") }} # false
${{ bool(0) }} # false
${{ bool(1) }} # true
예약어
다음 단어는 예약되어 있어 식별자로 사용할 수 없어요. 잠재적인 미래 언어 기능을 위해 예약되어 있어요.
array, as, break, case, const, continue, default, else, fallthrough, float, for, func, function, goto, if, import, in, int, let, loop, map, namespace, number, object, package, range, return, string, struct, switch, type, var, void, while
null, true, false 키워드도 리터럴 값으로 예약되어 있어요.
예제
전략 선택으로 배포하기
deploy job:
when: manual
inputs:
environment:
default: staging
options: [staging, production]
description: Target deployment environment
strategy:
default: rolling
options: [rolling, blue-green, canary]
description: Deployment strategy
replicas:
type: number
default: 3
description: Number of replicas to deploy
image: ${{ job.inputs.environment == "production" && "deploy-tools:stable" || "deploy-tools:latest" }}
script:
- 'echo "Deploying to ${{ job.inputs.environment }} using ${{ job.inputs.strategy }}"'
- deploy
--env ${{ job.inputs.environment }}
--strategy ${{ job.inputs.strategy }}
--replicas ${{ str(job.inputs.replicas) }}
불리언 잡 inputs에서 조건부 플래그 만들기
test_job:
inputs:
coverage:
type: boolean
default: false
verbose:
type: boolean
default: false
script:
- pytest ${{ job.inputs.verbose && "-v" || "" }} ${{ job.inputs.coverage && "--cov=src" || "" }}
잡 변수에서 이미지 참조 만들기
build_job:
run:
- name: build
func: ./docker-build
inputs:
image: ${{ vars.CI_REGISTRY + "/" + vars.CI_PROJECT_PATH + ":" + vars.CI_PIPELINE_IID }}
계속 게이트 (Continue gate)
security_scan_job:
run:
- name: scan
func: ./security-scan
- name: gate
func: ./quality-gate
inputs:
should_proceed: ${{ steps.scan.outputs.critical == 0 && steps.scan.outputs.high < 5 }}
버전 관리
increment_version_job:
run:
- name: current
func: ./find-version
- name: bump
func: ./bump-version
inputs:
new_version: ${{ str(steps.current.outputs.major + 1) + ".0.0" }}
환경별 구성
deploy_job:
run:
- name: deploy
func: ./deploy
inputs:
registry: ${{ (vars.CI_COMMIT_REF_NAME == "main" && "prod.registry.com") || "staging.registry.com" }}
replicas: ${{ (vars.CI_COMMIT_REF_NAME == "main" && 5) || 2 }}
A/B 테스트 구성하기
configure_job:
run:
- name: configure_ab
func: ./traffic-split
inputs:
variants: |
${{ [
{name: "control", use_new_feature: false, weight: 90},
{name: "experiment", use_new_feature: true, weight: 10}
] }}
더 알아보기
Moa는 GitLab Functions의 스텝 inputs를 구성하는 데 쓰여요. 함수를 만들고 실행하는 전체 흐름을 이해하려면 GitLab Functions 문서를, 파이프라인 생성 시점에 평가되는 $[[ ]] 문법이 궁금하다면 CI/CD 표현식 문서를 함께 읽어보는 걸 추천해요.