2P by GN⁺ 11시간전 | ★ favorite | 댓글 1개
  • 소프트웨어가 급격히 발전했지만, 운영체제의 환경 변수 시스템은 여전히 수십 년 전의 구조를 유지하고 있음
  • 환경 변수는 전역 문자열 딕셔너리 형태로, 네임스페이스나 타입이 없는 단순한 구조를 가짐
  • 리눅스에서 환경 변수는 execve 시스템 호출을 통해 부모 프로세스에서 자식으로 전달
  • Bash, glibc, Python 등은 각각 환경 변수를 해시맵, 배열, 딕셔너리 래핑 형태로 관리함
  • POSIX 표준은 이름에 대문자만을 요구하지 않으며, 실제로는 소문자 이름 사용이 권장되는 등 유연한 규칙을 가짐

환경 변수란 무엇인가

  • 프로그래밍 언어는 빠르게 발전했음에도 운영체제가 제공하는 프로세스 실행 기반 구조, 특히 환경 변수 부분은 변화가 거의 없는 상태임
  • 애플리케이션 실행 시 별도 파일이나 IPC 없이 런타임 파라미터를 전달하려면, 환경 변수 기반 인터페이스를 쓸 수밖에 없는 현실
  • 환경 변수는 네임스페이스도 없고, 타입도 없는, 플랫한 문자열 딕셔너리 역할을 함

환경 변수 생성 및 전달 구조

  • 환경 변수는 프로세스 간 값을 전달하는 전통적인 방법으로, 부모 프로세스가 자식 프로세스를 실행할 때 함께 전달됨
    • 즉, 부모 프로세스에서 자식 프로세스로 상속되는 구조
  • Linux에서는 execve 시스템 콜이 실행 파일, 인자, 그리고 환경 변수 배열(envp)을 인자로 받음
    • 실행 명령 예시: ls -lah라면
      • filename: /usr/bin/ls
      • argv: ['ls', '-lah']
      • envp: ['PATH=...','USER=...']
  • 부모 프로세스는 자식에게 기존 환경을 그대로 넘길 수도 있고, 완전히 새로운 환경을 구성할 수도 있음
    • 거의 모든 도구(Bash, Python의 subprocess.run, C 라이브러리 execl 등)는 환경 변수를 그대로 넘겨줌
    • 예외적으로, login과 같은 일부 도구는 새로운 환경을 구성함

환경 변수의 저장 위치와 내부 처리

  • 커널은 프로그램 시작 시 환경 변수들을 스택에 null-terminated string 형태로 저장함
  • 이 데이터는 프로그램이 직접 수정하기 어렵고, 보통은 프로그램 내부에서 복사해 자체 구조로 관리함
  • 각 언어 및 쉘의 환경 변수 저장 방식
    • Bash: 스택 구조의 해시맵(딕셔너리)로 관리
      • 함수 호출마다 로컬 스코프의 맵 생성
      • export된 변수만 자식 프로세스에 전달
      • local로 선언된 변수도 export를 통해 자식 프로세스에 전달 가능함
        • 예: export PATH를 통해 로컬 변경을 자식에게 반영하지만 전역에는 영향을 주지 않음
    • glibc(C 라이브러리) : 동적 배열 구조의 environputenv, getenv를 통해 관리
      • 배열 구조라 조회/변경 모두 선형 시간 복잡도를 가짐
      • 따라서 성능 요구가 높은 데이터 저장 용도로 사용하기에는 부적합
    • Python: 내부에서 os.environ으로 딕셔너리처럼 제공하나, 실상은 C 라이브러리의 environ 배열과 연동
      • os.environ 값 변경 시 os.putenv 호출되어 C라이브러리에도 반영
      • 반대 방향은 동기화되지 않으므로 단방향성 존재

환경 변수의 포맷과 허용 범위

  • Linux 커널과 glibc는 환경 변수 포맷에 매우 관대
    • 동일 이름이 중복되어 여러 값이 존재 가능
    • = 없이도 등록 가능하고, 이모지 등 특수문자도 제한 없음
  • 가용 사이즈 제한
    • 개별 변수: 128 KiB (보통 x64 환경)
    • 전체 합계: 2 MiB (명령행 인자와 공유)
    • 환경 변수는 스택 공간의 1/4을 넘지 않도록 제한되어 있음

환경 변수의 특이점 및 에지 케이스

  • Bash는 이상한 환경 변수(중복, = 없는 항목 등)의 경우 중복 이름 제거 및 비정상 항목 무시
  • 변수 이름에 공백이 있으면 Bash는 이름 참조 불가하나, 여전히 자식 프로세스에 전달 가능
    • 예: Nushell, Python 등은 공백 이름 변수 생성 가능
    • Bash는 이런 항목을 별도 해시맵(invalid_env)에 저장하여 관리

표준(standard) 환경 변수 포맷 및 이름 규칙

  • POSIX 표준은 이름에 등호(=)만 없으면 변수로 인정
    • 공식 권장: 이름은 대문자, 숫자, 언더스코어만 허용(맨 앞은 숫자 불가)
    • 소문자 변수는 애플리케이션 전용 네임스페이스용 용도
    • 표준 도구는 대문자만 사용하지만, 소문자 변수 사용도 허용
  • 실제로는 개발자들이 ALL_UPPERCASE 방식으로 주로 네이밍
  • 추천 규칙: 변수 이름은 정규식 ^[A-Z_][A-Z0-9_]*$ 사용, 값은 UTF-8 사용
    • 예외나 호환성 걱정 시 POSIX의 Portable Character Set(ASCII) 사용 권장

결론

  • 환경 변수는 여전히 낡지만 필수적인 인터페이스로, 운영체제와 응용 프로그램의 경계면 역할을 수행함
  • 구조적 한계에도 불구하고 Bash, C, Python 등은 이를 각자 다른 방식으로 래핑해 활용 중임
  • 현대적 시스템에서는 명확한 네임스페이스와 타입 체계가 있는 설정 관리 방식이 점점 더 필요함
Hacker News 의견
  • SRE/Sysadmin/DevOps/Whatever로 일하고 있음, 블로그에서는 환경 변수 표준화에 대해 어렵지 않은 이야기만 했지만, 대체 방법들도 마찬가지로 비슷하게 답답함을 유발함을 지적하고 싶음, 특히 보안 정보(secrets)가 얽혀 있을 때 더욱 그렇다는 점을 말하고 싶음
    Application이 Hashicorp Vault/OpenBao/Secrets Manager 같은 특정 비밀저장소(vault)에 접근하는 구조는 금방 심각한 벤더 종속성을 초래하며, 교체가 라이브러리 수준까지 번져가서 매우 어렵게 됨
    Vault의 가용성이 매우 중요해지고, 업그레이드나 유지관리가 필요할 때 운영팀이 크게 곤란해짐
    Config 파일로 비밀정보를 전달할 경우에는 그 비밀을 어떻게 담을지 난감한데, config 파일이 대개 퍼블릭 경로에 있기 때문임
    결국 "특권 시스템이 앱에 넘기기 전에 템플릿으로 대체"하거나 "전체 config 파일을 비밀저장소에 저장해서 앱에 전달" 중 하나에 의존하게 됨
    템플릿 작업은 실수하기 쉽고, 전체 config 파일을 비밀저장소로 옮길 때도 누군가가 잘못 업로드할 위험이 있어 스트레스를 유발함
    요즘 대부분의 시스템이 컨테이너 위에서 돌고 있는데, 인프라를 잘 지키는 회사가 아니라면 config 파일이 늘 엉뚱한 곳에 위치해 있어서 마운트 과정이 더욱 헷갈리고 실수가 빈번히 생김
    JSON/YAML/TOML 등 어떤 포맷을 쓰더라도 독특한 버그가 나오는 게 일상인데, 예를 들어 YAML의 Norway 문제같은 게 있음
    Kubernetes Secrets API로 비밀을 받는 방식을 본 적 있는데, 이 역시 강한 벤더 종속의 문제를 만남
    특별히 오퍼레이터 같은 시스템을 설계하는 게 아니라면 이 방법은 적극적으로 권장하지 않음
    Subprocess를 통해 환경 변수를 세팅하는 과정에서 생기는 이슈도 봤지만, 요즘 팀들은 이보단 메시지 버스 기반의 시스템을 써서 더 견고하고, 독립적인 확장이 가능하다고 생각함

    • 우리 팀에서는 가벼운 범용 Secrets 라이브러리를 만들어서 후면에 AWS Secrets Manager 같은 벤더별 백엔드만 플러그인 방식으로 붙여 썼던 경험이 있음
      로컬 캐시 설정 및 파라미터별 캐시 우회 옵션이 가능해서, 실제 벤더 종속 로직은 백엔드에만 남고 라이브러리와 애플리케이션은 벤더에 종속적이지 않게 유지 가능했음
      Vault로 넘어갈 때도 그냥 백엔드 하나 추가하고 설정만 바꿨더니 별 탈 없이 적용됨

    • Kubernetes secret API가 왜 벤더 종속성(락인) 문제로 비춰지는지 궁금함
      혹시 Kubernetes 배포가 아닌 다른 목적으로 deployment yaml을 사용하려 했던 경우인지 궁금함
      대부분의 앱에서는 secret을 컨테이너에 마운트한 뒤, 환경 변수나 json 파일 형태로 앱에 주입하면 환경에 독립적으로 읽어 쓸 수 있음
      etcd 백엔드 암호화도 KMS로 구성 가능하다고 알고 있음

    • Kubernetes Secrets API를 통해 비밀을 받는 게 왜 락인이라는 건지 잘 이해가 되지 않음
      기본적으로 K8s secrets는 암호화되어 저장되지 않기 때문에, (0) K8s 자체를 쓰고, (1) 컨트롤 플레인에서 암호화 작업을 해놓고, (2) CSI 드라이버 같은 추가 솔루션을 반드시 써야 의미가 있다고 봄
      그리고 Secret Store CSI Driver는 Conjur 등 다양한 벡엔드를 지원해서 오히려 락인이 반대임

    • 이런 이유로 여전히 config는 env vars와 dotenv 위주로 써오고 있음
      환경 변수 기반의 설정 구조가 너무 단순하고, 비밀 관리자 등 다양한 툴들과도 호환이 잘 됨
      최근 몇 년 사이엔 YAML 기반의 sOps에 조금씩 관심이 생김
      YAML은 앱 설정 구조 표현이 정말 직관적이고, sops로 일부만 암호화해서 관리하기가 쉬움
      다만 GPG 키 관리가 까다로운 면이 있지만, Vault나 OpenBao 같은 걸로 해결할 순 있음
      단, 그 과정에서 또 벤더 락인이 생긴다는 이슈가, OpenBao는 그나마 좀 덜한 듯함

    • 커맨드 실행 결과로 환경 변수를 받을 수도 있어서, 템플릿 과정 없이도 벤더 락인 없이 처리 가능함

  • 재미있는 사실 하나 더, setenv()는 POSIX에서 근본적으로 깨져 있어서, 라이브러리 코드에서는 절대 쓰면 안 된다고 생각함
    앱 코드에서 써야 할 때도 마지막 수단이며, 반드시 스레드를 만들기 전에만 써야 함
    getenv()는 환경 변수 원본 포인터를 직접 넘기기 때문에, setenv()가 변수를 덮어쓸 때 아무런 보호장치가 없음
    매우 주의해야 함

    • 환경 변수를 제대로 설정하려면 execve()로 세팅하는 게 옳다고 생각함
      exec() 전후로만 환경 변수로 정보를 전달할 때만 이 방식이 적합함

    • 라이브러리 코드에서 setenv를 왜 써야 하는지 이해가 안 됨

    • Solaris는 이 문제를 해결했지만, Linux는 여전히 같은 방식을 고집하고 있음

    • NetBSD에는 getenv_r()라는 안전한 대안이 예전부터 있었고, 최근 FreeBSD에서도 이를 도입함
      macOS도 아마 머지않아 따라올 듯함
      이미 glibc나 POSIX에 넣으려는 시도가 있었지만 거부됐었음
      여러 플랫폼에 보급되면 언젠가 공식적으로도 받아들여질 거라 기대함
      NetBSD getenv_r 문서
      FreeBSD 커밋

  • 환경 변수는 비밀정보 전달에 종종 사용되지만, 나는 그게 별로 좋은 관행이라 생각하지 않음
    Linux에서는 같은 유저로 실행되는 모든 프로세스가 서로서로의 환경 변수를 들여다볼 수 있음
    위협 모델을 어떻게 잡든, 특히 개발자의 시스템에서는 동일 유저로 돌고 있는 프로세스가 워낙 많아 걱정거리임
    이 이슈는 LLM 에이전트처럼 컨테이너 밖에서 여러 프로세스가 돌아다닐 때 더 심각해짐
    또한 환경 변수는 보통 자식 프로세스에까지 그대로 내려가서, 딱 한 프로세스만 비밀정보가 필요한 경우에도 무분별하게 노출되는 경향이 있음
    systemd는 환경 변수를 DBUS 통해 모든 시스템 클라이언트에게 보여주며, 공식 문서에도 환경 변수에 비밀을 담지 말라는 경고가 있음
    만약 이게 사실이라면, root 전용 유닛에 세팅한 환경 변수가 일반 유저에게도 보여질 수 있다는 뜻이니, 많은 시스템 관리자에게 상당히 충격일 것이라 봄
    결국 비밀관리자가 임시파일 공유 방식으로 secret을 넘겨주는 구조(예: 1Password의 op cli, flask, terraform 등)만이 유일하게 환경 변수와 평문 파일 노출에서 해방될 수 있는 해법이라 생각함
    systemd의 credentials 시스템이 이런 구조임. 하지만 지원은 아직 미미함
    환경 변수나 평문 파일 없이 비밀을 넘기는 좋은 방법 아는 사람이 있으면 공유해줬으면 좋겠음
    참고로 1Password op client의 경우, 매 세션마다 내 승인을 필요로 해서 CLI 세션에서 쓸 땐 안전하다고 느끼는데, 어떤 악의적 프로세스가 op 바이너리를 호출했다고 해도 별도의 승인을 요구함
    이제 남은 문제는, 그 비밀을 실제로 필요한 프로세스에게 어떻게 넘길 것인지인데, 다시 원점으로 돌아오는 느낌임
    systemd 환경 변수 공식문서 참고 링크

    • 2012년 즈음부터는 환경 변수도 일반 메모리와 동등하게 안전해짐
      관련 커밋 기록
      다른 프로세스의 환경 변수를 읽으려면 ptrace 권한이 반드시 필요하고, 이미 ptrace로 읽을 수 있다면 사실 모든 비밀정보를 읽을 수 있으니 의미 없는 걱정이라는 주장임
      명령줄 정보(cmdline)는 다른 얘기지만, 환경 변수는 이 방식으로는 더 이상 쉽게 노출되지 않음

    • 대부분의 운영체제 보안모델상, 한 유저로 실행된다는 의미는 그 유저의 모든 권한을 전적으로 넘긴 것과 같음
      예외로 FreeBSD의 capsicum, Linux의 landlock, SELinux, AppArmor, Windows의 integrity label 같은 추가 보안 기능이 있긴 하지만, 대부분은 한계가 뚜렷함
      결국 내가 내 프로세스를 죽이거나, 멈추거나, 디버깅하는 게 자유이며, ptrace/process_vm_readv/ReadProcessMemory 등등으로 내가 소유한 프로세스의 secret은 언제든 접근 가능함
      완전히 다른 보안모델(완벽한 capability 기반 OS)도 있긴 한데, 대다수는 이 모델을 따르고 있어서, 그 한계와 책임을 인식해야 함

    • 환경 변수나 평문 파일을 쓰지 않고 비밀을 전달하는 좋은 방법으로 memfd_secret이 생각남
      memfd_secret man page
      언어나 프레임워크별로 지원이 많지 않아서, 특히 Rust나 (Go도 가능하다면) FFI를 통해 시도해볼만 함
      PHP 쪽에 직접 래핑해서 넣는 걸 고민했었으나 php-fpm 수정까지는 하고 싶지 않아서 중단했던 경험이 있음
      실제로는, 프로세스 매니저가 미리 secret 파일 디스크립터를 연 뒤 자식 프로세스에 전달하고, 메모리 등에 노출 없이 쓸 수 있다면 가장 안전할 것임

    • 고전적인 유닉스 보안모델은 약간씩 개선된 형태로 여전히 많이 쓰고 있지만, 값싼 컴퓨팅 환경이나 최근 환경에는 한계가 뚜렷함
      비밀을 다른 프로세스에 숨겨야 할 필요가 있다면, 애초에 다른 유저로 분리해 돌리는 게 정석임
      아니면 아예 원격으로 접근하는 방식이 있는데, 역시 단점과 복잡성이 따름

    • 요즘은 컨테이너 플랫폼에서는 환경 변수로 config나 secret 전달이 추천되는 경향임
      컨테이너 내에서는 다른 프로세스가 환경 변수를 들여다볼 수 없게 설계돼 있음
      자식 프로세스에 환경변수가 상속되는 것도 설계적 의도이며, 환경 구성자가 secret 값을 가진 주체가 그 환경도 직접 세팅하는 구조이기 때문임
      걱정하는 대부분의 이슈는 크게 문제로 보지 않으며, 필요하다면 구체적으로 논의해볼 의향 있음

  • 많은 댓글들이 비밀관리와 문제점 위주로 다루고 있지만, 환경 변수의 장점도 한 번쯤 생각해볼만 함
    환경 변수는 Unix 프로세스를 구조적으로 연결시키는 "무기한 스코프, 동적 확장 변수 바인딩"임
    단순한 텍스트 파일과 바로 비교하기보다는, 환경 변수의 존재 이유가 자식 프로세스에 정보를 안전하게 넘기는 컨텍스트 전달에 있음을 상기할 필요가 있음
    중첩 쉘이나 복잡한 프로그램의 하위 프로세스 등, 프로세스 구조가 복잡할수록 환경 변수의 역할이 빛난다고 봄

  • Varlock이 정말 유용하다고 추천하고 싶음
    프로젝트에서 필요한 환경 변수들의 필수/옵션 상태, 자료형, 어디서 가져올지까지 명확히 정의해주고 관리가 쉬움
    Varlock 공식 사이트

  • 실전 경험상, 환경 변수가 얼마나 복잡해질 수 있는지 예시를 들자면, 예전 회사에서 특정 ENV 변수가 어디서 세팅되는지 디버깅하려다 완전히 헤맨 적이 있음
    처음엔 .bashrc나 간단한 어디 한 군데에서 설정되는 줄 알았는데, 회사 전체 수준, 지역, 사업부, 팀, 개인 등 최소 10단계의 레이어를 타고 환경 변수가 세팅되고 있었음
    결국 bash 디버그 플래그를 켜서야 겨우 어디서 세팅되는지 하나하나 추적할 수 있었음

    • 다른 언어들도 지원하는지 모르겠지만, Node.js는 환경 변수 접근 및 변경을 정확히 추적해주는 커맨드 라인 플래그를 최근에 추가함
      Node.js --trace-env 문서
      수많은 API를 통해 값이 설정/변경될 수 있기 때문에 복잡한 디버깅 시 굉장히 유용할 것 같음

    • "네임스페이스 하나면 충분하지 않을까"라는 말을 떠올리게 하는 케이스임

  • 나는 오래 전에 환경 변수 사용을 포기했음
    지금은 컴파일러 곁에 dmd.conf 파일을 두고 컴파일러가 이 파일을 직접 읽게 운용하고 있음

  • 환경 변수의 문제점 중 가장 심각한 건, 암묵적이고 불투명한 특성임
    *nix 세계에서 대부분의 앱이 환경 변수에 의존하는 경향이 있음
    명시적이고 투명한 구성 방법(설정 파일이나 remote 서비스, 커맨드라인 인수)이 추가로 지원되더라도, 환경 변수 지원이 이 바닥의 전통임
    결국 환경 변수도 전역 해시맵이며, 자식 프로세스를 위해 클론되고 확장되는 구조인데, 1979년에는 합리적 디자인이었겠지만 지금은 오히려 독이 되는 경우가 많음
    예를 들어 Kubernetes는 기본적으로 "service link" 환경 변수로 컨테이너 환경을 오염시킴
    앱이 예상한 환경 변수와 default env var가 충돌할 경우, 디버깅이 극도로 어려워짐
    쿠버네티스 공식 문서 참고
    이밖에도 /bin, /usr/bin, /lib, /usr/lib과 같이 옛 틀을 무비판적으로 유지하는 관행이 정말 많다고 느낌
    참고: legacy 디렉토리 유지에 대한 Ubuntu Q&A

    • hjkl 같은 키 조합도 이런 구태의연함의 대표 사례로 볼 수 있음
      vi에서의 hjkl이 40년 전 dumb terminal에서 유래했는데, 판매량도 적은 터미널이었음
      (Nokia N9보다도 적었음)
  • 리눅스에서 환경 변수를 설정할 때마다 불안함이 밀려옴
    공식적으로 동작하는 방법이 배포판마다 다소 다르고, 온라인 지침대로 해도 재부팅이나 터미널 닫으면 다 사라짐
    윈도우처럼 전역에서 쓸 수 있는 간단한 GUI 환경변수 에디터가 있었으면 좋겠음
    윈도우는 변경사항 반영을 위해 터미널을 새로 열어야 하는 번거로움이 있지만, 그 외에는 항상 잘 동작함

    • 환경 변수는 세션이 바뀌면 당연히 유지되지 않기 때문에, 매 세션마다 다시 실행되는 곳(로그인/터미널 등)에 적어줘야 정상임
      로그인 시 .bash_profile, 자식 세션에서는 .bashrc가 실행되는 구조임
      .bash_profile에서 .bashrc를 source 시켜 놓고, 대부분 설정은 .bashrc에 두면 관리가 쉬움
      Bash가 아니라 zsh/fish 등 다른 쉘을 쓰면 해당 쉘 기준으로 맞춰줘야 함
      Linux에는 터미널마다 모두 적용되는 공식적이고 통합된 환경 변수 GUI가 없는 것임
      복잡한 파싱 GUI를 만들 수도 있겠지만, 그냥 텍스트 에디터로 바꿔 쓰는 게 더 쉬움

    • 주로 리눅스를 쓰는 내 입장에서는 윈도우의 행동 양식이 오히려 더 불편하게 느껴짐
      너무 많은 앱이 환경변수를 오염시켜서 뭐가 안 되면 결국 $SOFTWARE가 이상한 폴더에서 실행되고 있었다는 등 혼란이 자주 생김

    • systemd를 쓴다면 /etc/environment나 /etc/environment.d/에 KEY=VALUE를 적는 방식도 가능함
      실제로 이 방법을 위한 GUI도 만들 수 있을 듯
      다만 환경 변수는 실행 중인 프로세스에는 주입 불가능하고, 리스타트해야만 반영되는 구조라 한계가 있음
      systemd 공식 문서 참고

    • xkcd Standards 만화
      Linux에는 환경 변수 설정법이 이미 14가지나 경쟁 중이라는 상황에서 "하나로 통합하자"고 하면, 다음날엔 15가지 표준이 됨을 유쾌하게 보여줌

  • 내가 제일 좋아하는 환경 변수 잡학은, 모두가 당연히 환경 변수로 아는 PS1 등이 실제로는 환경 변수가 아니고 shell 변수임
    PS1은 "env" 명령어로 볼 수도 없음