웹 애플리케이션을 개발 환경과 운영 환경에서 효과적으로 관리하고 싶다면, 환경 변수에 대한 정확한 이해가 필수입니다.
사실 이것이 언제나 당연한 것은 아니었습니다. 몇 년 전만 해도 Rails 앱을 환경 변수로 설정하는 개발자는 거의 없었습니다. 그런데 Heroku가 등장하면서 상황이 완전히 바뀌었습니다.
Heroku는 개발자들에게 12-Factor App 방법론을 소개했습니다. 12-Factor App 매니페스트에는 배포하기 쉬운 애플리케이션을 만들기 위한 다양한 모범 사례가 담겨 있는데, 특히 환경 변수에 관한 섹션이 큰 영향을 미쳤습니다.
12-Factor App은 설정(config)을 환경 변수(env vars 또는 env)에 저장한다. 환경 변수는 코드를 수정하지 않고도 배포 간에 쉽게 변경할 수 있다. 설정 파일과 달리 실수로 코드 저장소에 커밋될 가능성이 거의 없으며, Java System Properties 같은 다른 설정 메커니즘과 달리 언어와 운영체제에 종속되지 않는 표준이다.
오늘날 환경 변수를 사용하는 루비스트(Rubyist)가 그 어느 때보다 많아졌습니다. 하지만 안타깝게도 상당수가 원리를 제대로 이해하지 못한 채 무작위로 따라 사용하고 있을 뿐입니다.
이 글에서는 환경 변수가 실제로 어떻게 동작하는지, 그리고 아마도 더 중요한 어떻게 동작하지 않는지를 알아보겠습니다. 마지막으로 Rails 앱에서 환경 변수를 관리하는 대표적인 방법들도 함께 살펴보겠습니다.
모든 프로세스는 자신만의 환경 변수 집합을 가진다
서버에서 실행하는 모든 프로그램은 최소 하나 이상의 프로세스로 구성됩니다. 그리고 각 프로세스는 자신만의 고유한 환경 변수 집합을 받습니다. 일단 프로세스가 환경 변수를 갖게 되면, 그 프로세스 외부에서는 이를 변경할 수 없습니다.
초보자들이 흔히 저지르는 오해 중 하나는 환경 변수가 서버 전체에 적용되는 것이라고 생각하는 것입니다. Heroku 같은 서비스가 환경 변수 설정을 디스크의 설정 파일을 편집하는 것처럼 보이게 만들기 때문에 더욱 그렇습니다. 하지만 환경 변수는 설정 파일과는 전혀 다른 것입니다.
서버에서 실행하는 모든 프로그램은 실행되는 순간 자신만의 환경 변수 집합을 받습니다.
모든 프로세스는 자신만의 환경을 가집니다.
환경 변수는 프로세스와 함께 사라진다
환경 변수를 설정한 뒤 서버를 재부팅했는데 값이 사라진 경험이 있으신가요? 환경 변수는 프로세스에 속해 있기 때문에, 프로세스가 종료되면 해당 환경 변수도 함께 사라집니다.
IRB 세션에서 환경 변수를 설정한 후 종료하고, 새 IRB 세션에서 해당 변수에 접근해 보면 이를 직접 확인할 수 있습니다.
프로세스가 종료되면 환경 변수도 사라집니다.
서버 재부팅이나 셸 종료 시 환경 변수가 유실되는 것도 같은 원리입니다. 세션 간에 값을 유지하고 싶다면 .bashrc 같은 설정 파일에 저장해 두어야 합니다.
프로세스는 부모 프로세스로부터 환경 변수를 물려받는다
모든 프로세스에는 부모가 있습니다. 프로그램은 반드시 다른 프로그램에 의해 시작되어야 하기 때문입니다.
bash 셸에서 vim을 실행하면 vim의 부모는 셸입니다. Rails 앱이 ImageMagick으로 이미지를 분석한다면 identify 프로그램의 부모는 Rails 앱입니다.
자식 프로세스는 부모의 환경 변수를 상속받습니다.
아래 예제에서는 IRB 프로세스에서 $MARCO 환경 변수를 설정한 뒤, 백틱(`)으로 셸 명령을 실행해 그 값을 출력합니다.
방금 생성된 셸의 부모 프로세스가 IRB이기 때문에, 셸은 $MARCO 환경 변수의 복사본을 물려받습니다.
Ruby에서 설정한 환경 변수는 자식 프로세스에 상속됩니다.
부모는 자식에게 전달할 환경 변수를 조정할 수 있다
기본적으로 자식 프로세스는 부모가 가진 모든 환경 변수의 복사본을 받습니다. 하지만 부모는 이를 제어할 수 있습니다.
명령줄에서는 env 프로그램을 사용할 수 있고, bash에는 부모에는 영향을 주지 않으면서 자식에게만 환경 변수를 설정하는 특별한 문법이 있습니다.
env 명령어로 부모에 영향을 주지 않고 자식 프로세스에만 환경 변수를 설정할 수 있습니다.
Ruby 내부에서 셸 명령을 실행할 때도 ENV 해시를 오염시키지 않으면서 자식 프로세스에 별도의 환경 변수를 전달할 수 있습니다. system 메서드에 다음 문법을 사용하면 됩니다.
Ruby의 system 메서드에 커스텀 환경 변수를 전달하는 방법
자식은 부모의 환경 변수를 변경할 수 없다
자식 프로세스는 부모 환경 변수의 복사본만 받기 때문에, 자식이 수행한 변경은 부모에게 아무런 영향을 미치지 못합니다.
환경 변수는 "참조(by reference)"가 아닌 "값(by value)"으로 전달됩니다.
아래 예제에서는 백틱 문법으로 셸 명령을 실행해 환경 변수를 설정해 봅니다. 자식 프로세스에서는 변수가 설정되지만, 새로운 값이 부모까지 거슬러 올라가지는 않습니다.
자식 프로세스는 부모의 환경 변수를 변경할 수 없습니다.
환경 변경 사항은 실행 중인 프로세스 간에 동기화되지 않는다
아래 예제에서는 두 개의 IRB를 나란히 실행합니다. 한 IRB 세션의 환경에 변수를 추가해도 다른 IRB 세션에는 아무런 영향을 주지 않습니다.
한 프로세스에 추가된 환경 변수가 다른 프로세스에는 적용되지 않습니다.
셸은 환경 변수 시스템의 UI일 뿐이다
환경 변수 시스템 자체는 OS 커널의 일부입니다. 즉, 셸이 환경 변수에 대해 어떤 마법 같은 권한을 가지는 것이 아니며, 다른 모든 프로그램과 동일한 규칙을 따릅니다.
환경 변수와 셸 변수는 다르다
가장 큰 혼란의 원인 중 하나는 셸이 자체적인 "로컬" 셸 변수 시스템을 제공하기 때문입니다. 로컬 변수를 사용하는 문법이 환경 변수와 흔히 같아서 초보자들이 두 가지를 혼동하곤 합니다.
그러나 로컬 변수는 자식 프로세스에 복사되지 않습니다.
환경 변수와 셸 변수는 서로 다릅니다.
예제를 살펴보겠습니다. 먼저 MARCO라는 이름의 로컬 셸 변수를 설정합니다. 이것은 로컬 변수이므로 어떤 자식 프로세스에도 복사되지 않습니다. 따라서 Ruby에서 이를 출력하려 하면 실패합니다.
다음으로 export 명령어를 사용해 로컬 변수를 환경 변수로 변환합니다. 이제 이 셸이 생성하는 모든 새 프로세스에 복사되며, 환경 변수를 Ruby에서도 사용할 수 있게 됩니다.
로컬 변수는 자식 프로세스에서 접근할 수 없습니다. export는 로컬 변수를 환경 변수로 변환합니다.
실전에서 환경 변수 관리하기
그렇다면 실제 환경에서는 어떻게 활용할까요? 예제를 통해 살펴보겠습니다.
한 대의 컴퓨터에서 두 개의 Rails 앱을 운영하고 있다고 가정해 보겠습니다. 예외 모니터링을 위해 Honeybadger를 사용 중인데, 한 가지 문제가 발생했습니다.
Honeybadger API 키를 $HONEYBADGER_API_KEY 환경 변수에 저장하고 싶지만, 두 앱은 서로 다른 API 키를 가지고 있습니다.
하나의 환경 변수가 어떻게 두 가지 다른 값을 가질 수 있을까요?
이제 답을 짐작하실 겁니다. 환경 변수는 프로세스별로 존재하고, 두 Rails 앱은 서로 다른 프로세스로 실행되므로 각각 자신만의 $HONEYBADGER_API_KEY 값을 가질 수 있는 것입니다.
남은 질문은 이를 어떻게 구현하느냐입니다. 다행히도 이 작업을 아주 쉽게 만들어 주는 젬(gem)들이 몇 가지 있습니다.
Figaro
Rails 앱에 Figaro 젬을 설치하면, config/application.yml에 입력한 값들이 시작 시점에 Ruby의 ENV 해시로 로드됩니다.
먼저 젬을 설치합니다:
# Gemfile
gem "figaro"
그리고 application.yml에 설정 값을 추가합니다. 실수로 시크릿 정보를 커밋하지 않도록 이 파일을 반드시 .gitignore에 추가해야 합니다.
# config/application.yml
HONEYBADGER_API_KEY: 12345
Dotenv
dotenv 젬은 Figaro와 매우 유사하지만, YAML 대신 .env 파일에서 환경 변수를 불러온다는 차이점이 있습니다.
역시 젬을 설치합니다:
# Gemfile
gem 'dotenv-rails'
그리고 설정 값을 .env 파일에 추가합니다. GitHub에 실수로 공개되지 않도록 이 파일도 반드시 gitignore 처리하세요.
HONEYBADGER_API_KEY=12345
이후 Ruby의 ENV 해시에서 값을 조회할 수 있습니다.
ENV["HONEYBADGER_API_KEY"]
또한 미리 정의한 환경 변수들을 적용한 상태로 셸 명령을 실행할 수도 있습니다.
dotenv ./my_script.sh
secrets.yml은?
아쉽지만 secrets.yml은 편리한 기능임에도 불구하고 환경 변수를 설정해 주지 않습니다. 따라서 Figaro나 dotenv 같은 젬의 대체재라고 보기는 어렵습니다.
순수 리눅스 명령어 활용하기
기본적인 리눅스 명령어만으로도 앱별로 고유한 환경 변수 집합을 관리할 수 있습니다. 한 가지 방법은 서버에서 실행되는 각 앱을 서로 다른 사용자 계정으로 소유하게 하는 것입니다. 그런 다음 각 사용자의 .bashrc에 애플리케이션별 설정 값을 저장하면 됩니다.