소프트웨어 개발과 배포 과정에서 자동화는 필수적인 요소가 되었습니다. 특히 CI/CD(지속적 통합/지속적 배포) 파이프라인은 개발 생산성을 크게 향상시키죠. GitHub Actions는 GitHub 저장소에 통합되어 이러한 CI/CD 워크플로를 쉽게 구축하고 실행할 수 있게 해주는 강력한 도구입니다. 하지만 많은 개발자가 GitHub Actions를 사용하면서 혼란을 겪는 지점 중 하나가 바로 환경변수(Environment Variables) 관리입니다. “내 로컬에서는 잘 동작하던 스크립트가 GitHub Actions에서는 환경변수를 찾지 못해 실패한다”는 경험은 생각보다 흔합니다. 이번 글에서는 GitHub Actions에서 환경변수가 누락되는 일반적인 이유들을 파악하고, 이를 효과적으로 해결하는 실용적인 방법들을 알아보겠습니다.
환경변수란 무엇이며 왜 중요한가요
환경변수는 운영체제나 실행 중인 프로그램이 특정 정보를 저장하고 접근할 수 있도록 해주는 동적인 값들의 집합입니다. 예를 들어, 데이터베이스 연결 문자열, API 키, 특정 디렉토리 경로, 애플리케이션 설정 값 등이 환경변수로 사용될 수 있습니다. 이러한 변수들은 코드에 직접 하드코딩하는 대신, 외부에서 주입되어 애플리케이션의 유연성과 보안을 높여줍니다.
CI/CD 파이프라인, 특히 GitHub Actions에서는 환경변수의 중요성이 더욱 커집니다. 개발, 스테이징, 프로덕션 등 다양한 환경에 따라 다른 설정값을 적용해야 할 때, 환경변수를 사용하면 동일한 코드 베이스를 유지하면서도 각 환경에 맞는 설정을 적용할 수 있습니다. 또한, 민감한 정보(API 키, 비밀번호 등)를 코드에 노출하지 않고 안전하게 관리하는 데 핵심적인 역할을 합니다.
GitHub Actions에서 환경변수를 설정하는 다양한 방법
GitHub Actions에서는 여러 가지 방법으로 환경변수를 설정할 수 있으며, 각 방법마다 적용되는 범위와 우선순위가 다릅니다. 이 차이를 이해하는 것이 문제 해결의 첫걸음입니다.
워크플로 전체에 적용하는 방법
env 키워드를 워크플로 파일 최상단에 정의하면 해당 워크플로 내의 모든 작업(job)과 단계(step)에서 해당 환경변수를 사용할 수 있습니다.
name: My Workflow
env:
GLOBAL_VAR: "Hello from workflow"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Print global var
run: echo $GLOBAL_VAR # "Hello from workflow" 출력
특정 작업(job)에만 적용하는 방법
env 키워드를 특정 job 아래에 정의하면 해당 작업과 그 작업 내의 모든 단계에서만 환경변수를 사용할 수 있습니다. 워크플로 레벨에 정의된 변수보다 우선순위가 높습니다.
name: My Workflow
env:
GLOBAL_VAR: "Hello from workflow"
jobs:
build:
runs-on: ubuntu-latest
env:
JOB_VAR: "Hello from job"
GLOBAL_VAR: "Overridden global var" # 워크플로 레벨 변수 오버라이드
steps:
- name: Print job var
run: |
echo $JOB_VAR # "Hello from job" 출력
echo $GLOBAL_VAR # "Overridden global var" 출력
특정 단계(step)에만 적용하는 방법
env 키워드를 특정 step 아래에 정의하면 해당 단계에서만 환경변수를 사용할 수 있습니다. 워크플로 및 작업 레벨에 정의된 변수보다 우선순위가 가장 높습니다.
name: My Workflow
env:
GLOBAL_VAR: "Hello from workflow"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Print step var
env:
STEP_VAR: "Hello from step"
GLOBAL_VAR: "Overridden by step" # 모든 상위 변수 오버라이드
run: |
echo $STEP_VAR # "Hello from step" 출력
echo $GLOBAL_VAR # "Overridden by step" 출력
GitHub Secrets 사용
API 키, 토큰, 비밀번호와 같은 민감한 정보는 GitHub Secrets에 저장하고 사용해야 합니다. Secrets는 GitHub Actions가 실행될 때 환경변수처럼 주입되지만, 로그에 노출되지 않도록 마스킹 처리됩니다.
name: My Workflow
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Use a secret
run: echo "My secret is: $MY_SECRET"
env:
MY_SECRET: ${{ secrets.MY_SECRET }} # GitHub Secrets에 정의된 변수 사용
GITHUB_ENV 파일을 통한 동적 환경변수 설정
가장 중요한 방법 중 하나입니다. 한 단계에서 계산되거나 생성된 값을 다음 단계에서 환경변수로 사용하고 싶을 때 사용합니다. GITHUB_ENV는 GitHub Actions 러너가 제공하는 특별한 파일 경로로, 이 파일에 KEY=VALUE 형식으로 내용을 추가하면 이후 단계에서 해당 변수를 환경변수로 사용할 수 있습니다.
name: My Workflow
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate a dynamic variable
id: generate_var
run: |
DYNAMIC_VALUE="My dynamic data at $(date)"
echo "GENERATED_VAR=$DYNAMIC_VALUE" >> $GITHUB_ENV # GITHUB_ENV 파일에 쓰기
- name: Use the dynamic variable in a subsequent step
run: echo "The dynamic variable is: ${{ env.GENERATED_VAR }}" # 다음 단계에서 사용 가능
출력 변수(outputs)를 통한 데이터 전달
한 단계의 결과를 다른 단계나 다른 작업에 전달할 때 사용합니다. outputs는 환경변수와는 약간 다른 메커니즘으로 작동하지만, 데이터 전달이라는 점에서 유사한 목적을 가집니다. 특히 여러 작업(job) 간에 데이터를 전달할 때 유용합니다.
name: My Workflow
jobs:
job1:
runs-on: ubuntu-latest
outputs:
my_output: ${{ steps.set_output.outputs.value }}
steps:
- id: set_output
run: echo "value=hello_world" >> $GITHUB_OUTPUT # GITHUB_OUTPUT 파일에 쓰기
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- run: echo "Output from job1: ${{ needs.job1.outputs.my_output }}"
GitHub Actions에서 환경변수가 누락되는 흔한 이유
“로컬에서는 잘 되는데 GitHub Actions에서만 안 돼요!”라는 상황에 직면했다면, 다음 이유들을 확인해보세요.
환경변수 스코프(Scope) 불일치
가장 흔한 원인 중 하나입니다. 환경변수를 워크플로, 작업, 단계 중 어디에 정의했는지에 따라 변수의 가용 범위가 달라집니다. 특정 단계에서만 정의된 변수를 다른 단계에서 사용하려고 하거나, 작업 레벨에서 정의된 변수를 다른 작업에서 사용하려고 할 때 누락 문제가 발생합니다.
- 해결 방법: 변수를 사용하려는 가장 넓은 범위에 정의하거나,
GITHUB_ENV또는outputs를 사용하여 명시적으로 변수를 전달해야 합니다.
잘못된 변수 참조 구문
환경변수를 참조하는 방식은 셸 스크립트와 GitHub Actions 표현식에서 다릅니다.
- 셸 스크립트 내에서는
$VAR_NAME또는${VAR_NAME}을 사용합니다.
- GitHub Actions 표현식(예:
env,if,outputs키워드 옆)에서는${{ env.VAR_NAME }}을 사용합니다.
잘못된 예시: 셸 스크립트에서 ${{ env.VAR_NAME }} 사용
- run: echo "My var is: ${{ env.MY_VAR }}" # 셸 스크립트 내부에서는 $MY_VAR 사용해야 함
올바른 예시
- run: echo "My var is: $MY_VAR"
env:
MY_VAR: "some value"
올바른 예시: 표현식 내부에서 env.MY_VAR 사용
- name: Conditional step
if: ${{ env.MY_VAR == 'some value' }}
run: echo "Condition met!"
GITHUB_ENV 파일에 쓰지 않고 다음 단계에서 사용
한 단계에서 export MY_VAR=value와 같이 셸 명령으로 환경변수를 설정하더라도, 이 변수는 해당 단계의 셸 세션 내에서만 유효합니다. 다음 단계는 새로운 셸 세션에서 시작되므로, 이전 단계에서 설정한 변수를 알지 못합니다.
- 해결 방법: 다음 단계에서도 변수를 사용하려면 반드시
echo "MY_VAR=value" >> $GITHUB_ENV와 같이GITHUB_ENV파일에 변수를 추가해야 합니다.
Secrets를 일반 환경변수처럼 사용하려는 시도
GitHub Secrets는 워크플로 파일 내에서 ${{ secrets.MY_SECRET }} 형태로 참조되어야 합니다. 또한, Secrets는 보안상의 이유로 로그에 마스킹 처리되며, 직접 셸에서 export하여 다른 변수에 할당하는 방식은 권장되지 않습니다.
- 해결 방법: Secrets는 필요한 단계의
env블록에 명시적으로 할당하여 사용하거나, 직접 표현식으로 참조해야 합니다.
셸 환경의 차이
로컬 개발 환경과 GitHub Actions 러너의 기본 셸(Shell)이 다를 수 있습니다. GitHub Actions는 기본적으로 Linux 러너에서 Bash, Windows 러너에서 PowerShell을 사용합니다. 특정 셸 스크립트 문법이나 환경변수 처리 방식이 셸에 따라 다를 수 있습니다.
- 해결 방법:
shell키워드를 사용하여 특정 단계의 셸을 명시적으로 지정할 수 있습니다. (예:shell: bash,shell: pwsh,shell: python)
오타 또는 대소문자 불일치
환경변수 이름은 대소문자를 구분합니다. 작은 오타나 대소문자 불일치도 변수를 찾지 못하는 원인이 됩니다.
- 해결 방법: 변수 이름을 다시 한번 꼼꼼히 확인하세요.
실용적인 해결 방법과 유용한 팁
환경변수 디버깅의 기본: 모두 출력해보기
현재 단계에서 사용 가능한 모든 환경변수를 확인하는 것은 가장 기본적인 디버깅 방법입니다. env 명령어를 사용하면 됩니다.
- name: Print all environment variables
run: env
특정 변수만 확인하고 싶다면 echo를 사용합니다.
- name: Print specific variable
run: echo "My variable is: $MY_VARIABLE"
GITHUB_ENV를 활용한 동적 변수 전달
이전 단계에서 생성된 변수를 다음 단계에서 사용하려면 반드시 GITHUB_ENV 파일에 추가해야 합니다. 이때, 개행 문자나 특수 문자가 포함된 경우를 대비하여 `echo` 명령과 함께 적절한 처리를 해주는 것이 좋습니다.
- name: Set dynamic variable for next steps
run: |
# 다중 라인 값을 처리할 때 주의
DYNAMIC_MULTILINE_VALUE="Line 1%0ALine 2" # %0A는 개행 문자를 나타냄
echo "MY_DYNAMIC_VAR=my_value" >> $GITHUB_ENV
echo "ANOTHER_VAR=${{ github.run_id }}" >> $GITHUB_ENV
echo "MULTILINE_VAR< $GITHUB_ENV
echo "$DYNAMIC_MULTILINE_VALUE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Use dynamic variables
run: |
echo "Dynamic var: $MY_DYNAMIC_VAR"
echo "Another var: $ANOTHER_VAR"
echo "Multiline var:"
echo "$MULTILINE_VAR"
참고: GITHUB_ENV에 다중 라인 변수를 추가할 때는 “heredoc” 문법(VAR_NAME<%0A는 단일 라인에서 개행 문자를 나타낼 때 유용합니다.
Secrets의 안전한 사용
Secrets는 민감한 정보이므로 절대 로그에 노출되어서는 안 됩니다. GitHub Actions는 Secrets를 자동으로 마스킹하지만, 변수를 다른 변수에 할당하거나 조작하는 과정에서 실수로 노출될 수 있으니 항상 주의해야 합니다.
올바른 사용 예시 (로그에 마스킹됨)
- name: Use secret directly in env
run: echo "API Key is: $MY_API_KEY"
env:
MY_API_KEY: ${{ secrets.MY_API_KEY }}
피해야 할 사용 예시 (잠재적 노출 위험)
- name: Potentially expose secret
run: |
# 이 스크립트가 복잡해지면 SECRET_VAR 값이 로그에 노출될 위험이 있음
SECRET_VAR="${{ secrets.MY_API_KEY }}"
echo "Processing with secret: $SECRET_VAR"
조건부 환경변수 설정
특정 조건에 따라 다른 환경변수를 설정해야 할 때가 있습니다. if 조건문을 env 블록과 함께 사용하여 이를 구현할 수 있습니다.
name: Conditional Env Vars
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set environment based on branch
if: github.ref == 'refs/heads/main'
run: echo "ENV_TYPE=production" >> $GITHUB_ENV
- name: Set environment based on branch (dev)
if: github.ref == 'refs/heads/dev'
run: echo "ENV_TYPE=development" >> $GITHUB_ENV
- name: Use environment type
run: echo "Running in $ENV_TYPE environment"
GitHub Actions 기본 제공 환경변수 활용
GitHub Actions는 러너, 저장소, 워크플로 실행에 대한 다양한 정보를 담은 기본 환경변수들을 제공합니다. (예: GITHUB_REF, GITHUB_SHA, RUNNER_OS 등) 이 변수들을 활용하면 워크플로를 더욱 유연하게 만들 수 있습니다.
- 참고 문서: GitHub Actions Variables
로컬에서 워크플로 테스트하기
`act`와 같은 도구를 사용하면 GitHub Actions 워크플로를 로컬 환경에서 실행하고 디버깅할 수 있습니다. 이는 실제 GitHub Actions 러너에서 실행하기 전에 환경변수 문제를 미리 발견하고 해결하는 데 큰 도움이 됩니다.
오픈소스 액션 사용 시 주의사항
다른 사람이 만든 GitHub Actions (오픈소스 액션)를 사용할 때, 해당 액션이 환경변수를 어떻게 사용하고 기대하는지 README 문서를 꼼꼼히 확인해야 합니다. 일부 액션은 특정 환경변수를 필수적으로 요구하거나, 자체적으로 환경변수를 설정하고 오버라이드할 수 있습니다.
흔한 오해와 사실 관계
오해 1: `export` 명령으로 설정한 변수는 모든 단계에서 자동으로 유지된다.
사실: 아닙니다. export는 현재 셸 세션 내에서만 유효하며, GitHub Actions의 각 run 단계는 별도의 셸 세션에서 실행됩니다. 다음 단계에서 변수를 사용하려면 반드시 echo "VAR_NAME=VALUE" >> $GITHUB_ENV를 통해 GITHUB_ENV 파일에 써야 합니다.
오해 2: Secrets는 일반 환경변수처럼 자유롭게 다룰 수 있다.
사실: 아닙니다. Secrets는 보안상의 이유로 특별히 처리됩니다. 로그에 마스킹되며, 일반 텍스트로 접근하거나 출력하려 하면 로 표시됩니다. 민감한 정보는 항상 Secrets로 관리하고, 필요한 경우에만 env 블록을 통해 특정 단계에 주입하여 사용해야 합니다.
오해 3: 워크플로 파일에 정의된 모든 환경변수는 빌드 시간과 런타임에 모두 동일하게 사용된다.
사실: 환경변수는 정의된 스코프 내에서 런타임에 적용됩니다. 하지만 일부 빌드 도구(예: Docker)는 이미 빌드 시점에 환경변수를 고정시켜 버릴 수 있습니다. Dockerfile 내에서 ARG와 ENV의 차이를 이해하고, docker build --build-arg와 같은 명령어를 통해 런타임 변수를 주입하는 방법을 고려해야 합니다.
전문가의 조언
"환경변수 문제는 대부분 스코프와 생명주기(lifecycle)를 이해하지 못해서 발생합니다. 특히 GITHUB_ENV의 역할은 GitHub Actions를 능숙하게 다루기 위한 핵심 지식입니다. 로컬 개발 환경과 CI/CD 환경은 다르다는 것을 항상 인지하고, 디버깅 시에는 과감하게 env 명령어를 사용하여 현재 상태를 파악하는 습관을 들이세요. 그리고 민감한 정보는 절대로 하드코딩하거나 로그에 노출하지 않도록 GitHub Secrets를 적극 활용해야 합니다. 마지막으로, 복잡한 워크플로를 작성하기 전에 작은 단위로 쪼개어 각 단계의 환경변수 흐름을 테스트하는 것이 좋습니다."
자주 묻는 질문
Q1: 한 작업(job)에서 생성된 변수를 다른 작업에서 어떻게 사용하나요?
A1: outputs를 사용해야 합니다. 첫 번째 작업에서 GITHUB_OUTPUT 파일에 변수를 쓰고, 해당 변수를 작업의 outputs로 정의합니다. 두 번째 작업에서는 needs.첫번째작업ID.outputs.변수이름 형태로 참조할 수 있습니다.
jobs:
job1:
runs-on: ubuntu-latest
outputs:
my_generated_var: ${{ steps.set_output.outputs.value }}
steps:
- id: set_output
run: echo "value=my_custom_value" >> $GITHUB_OUTPUT
job2:
runs-on: ubuntu-latest
needs: job1
steps:
- run: echo "Value from job1: ${{ needs.job1.outputs.my_generated_var }}"
Q2: 왜 제 Secrets는 로그에 로 표시되나요?
A2: 이는 GitHub Actions의 보안 기능입니다. Secrets에 저장된 값은 로그에 노출되지 않도록 자동으로 마스킹 처리됩니다. 이 동작은 정상적이며, 민감한 정보가 실수로 노출되는 것을 방지하기 위함입니다. 변수가 ***로 표시된다는 것은 Secrets가 올바르게 작동하고 있다는 의미입니다.
Q3: 환경변수 이름에 특수 문자를 사용할 수 있나요?
A3: 일반적으로 환경변수 이름은 대문자 알파벳, 숫자, 밑줄(_)로 구성하는 것이 가장 안전하고 호환성이 높습니다. 하이픈(-)이나 다른 특수 문자는 셸 스크립트에서 변수 이름으로 인식되지 않아 문제를 일으킬 수 있습니다. GitHub Actions 표현식(${{ env.VAR_NAME }})에서는 하이픈이 포함된 변수도 접근 가능하지만, 셸 스크립트에서 직접 사용하려면 주의가 필요합니다.
Q4: env와 secrets의 주요 차이점은 무엇인가요?
A4: env는 일반적인 환경변수를 정의하는 데 사용되며, 해당 값은 워크플로 로그에 그대로 노출될 수 있습니다. 반면 secrets는 민감한 정보를 안전하게 저장하고 워크플로에 주입하는 데 사용되며, 그 값은 로그에 마스킹 처리되어 노출되지 않습니다. secrets는 GitHub 저장소 설정에서 관리되며, env는 워크플로 파일 내에서 직접 정의됩니다.