2026년 10월 13일
이번 글에서는 팀에서 사용하던 환경변수를 중앙에서 관리하기 위해 환경변수 관리 도구를 구현한 과정을 정리해보고자 합니다.
프로젝트를 개발하다 보면 .env 파일을 사용하는 일이 굉장히 많습니다. 로컬 개발 뿐 아니라 운영 및 스테이징 서버에서도 필요하고 자동 배포 과정에서도 필요합니다. 더구나 외부 서비스가 늘어날 수록 관리해야할 값도 함께 늘어납니다.
저희 팀도 환경변수 파일을 Slack으로 공유하고 각자 로컬에서 최신화하는 방식을 사용했습니다. 처음에는 파일을 전달하고 기록을 지우는 일로 충분했지만, 값이 변경 됐을 때 로컬과 서버, 빌드 환경까지 함께 맞추는 과정이 번거로울 뿐더러 누군가 놓치게 된다면 서로 다른 값을 가지고 개발 할 때도 많았습니다.

또한 팀 메신저로 공유하는 것 자체가 보안상 큰 위험성이 있기 때문에 늘 개선할 방법이 없을까를 고민했고 그 결과 직접 만들기로 했습니다.
결론적으로 환경변수의 원본을 한 곳에 두고, 필요한 환경에서 꺼내 사용할 수 있는 구조를 만들었습니다.
먼저 기존에 환경변수를 어떻게 관리했는지 설명드리겠습니다.
개발자는 전달받은 .env 파일을 로컬에 두고 사용했습니다. 서버에서는 환경변수 파일을 직접 수정한 뒤 프로세스를 다시 실행했고, 빌드에 필요한 값은 별도 스크립트로 GitHub Secrets에 맞춰야 했습니다. 이후 팀원들에게도 변경된 파일을 전달해야 했습니다.
기존 흐름을 간단히 표현하면 아래와 같았습니다.
서버 .env 수정
→ 프로세스 reload
→ 빌드용 설정 동기화
→ 팀원에게 변경된 파일 공유
→ 각자 로컬 파일 업데이트
이 방식에서 가장 큰 문제는 파일 자체보다 변경 사항을 맞추는 과정이었습니다. 누군가는 업데이트된 파일을 가지고 있고, 다른 사람은 이전 파일을 사용하고 있어도 바로 알기 어려웠습니다. 새로운 팀원이 프로젝트를 실행할 때도 코드와 별개로 최신 환경변수 파일을 받아야 했습니다.
또한 외부 서비스의 API 키 중에는 만료일이 정해져 있는 경우가 있었습니다. 값만 .env에 저장해서는 누가 관리하는지, 언제 갱신해야 하는지까지 알 수 없었습니다.
이를 개선하기 위해 다음과 같은 요구사항을 정리했습니다.
중앙 관리 구조를 만들기로 한 뒤 가장 먼저 검토했던 부분은 이미 제공되는 서비스를 사용할 것인지, 필요한 범위를 직접 구현할 것인지였습니다.
설계 단계에서는 Doppler , Infisical , AWS Secrets Manager , AWS Parameter Store 를 검토했습니다. 당시 중요하게 고려했던 요소는 저장 위치, 비용, 접근 권한, 운영에 필요한 리소스와 러닝 커브였습니다.
관리형 서비스를 사용하면 대시보드와 연동 기능을 빠르게 구성할 수 있었지만, 외부 서비스에 운영 시크릿을 저장하는 것에 대한 검토가 필요했습니다. 한 편으로는 셀프 호스팅을 선택하면 저장 위치를 직접 관리할 수 있는 대신 보안 설정, 백업, 업그레이드도 직접 담당해야 했습니다.
이러한 부분을 고려해 초기에는 기존 AWS 인프라를 활용하는 방향으로 설계했습니다.
초기 설계의 원본 저장소는 Parameter Store였습니다. 개발자에게 AWS 자격증명을 직접 나눠주기보다 CLI가 SSH로 서버에 요청하고, 서버가 인스턴스 역할을 통해 값을 읽어 전달하는 구조를 구상했습니다.
개발자 CLI
→ SSH로 서버에 요청
→ 서버가 Parameter Store 조회
→ 값을 전달받아 개발 프로세스 실행
다만 당시 확인된 서버 구성에는 Parameter Store에 접근할 인스턴스 역할이 없었습니다. 따라서 AWS 권한과 암호화 관련 설정을 준비하는 작업이 먼저 필요했습니다.
최종적으로 현재는 비공개 Git 저장소와 age 라이브러리를 사용해 금고를 구성했습니다.
가장 필요한 환경변수 저장과 조회, 팀원 등록과 서버 반영, 만료일 설정 웹훅 연동 등의 사용 흐름을 구현했습니다.
구현 과정에서 먼저 도구 코드와 환경변수 데이터를 서로 다른 저장소로 분리했습니다.
접근 가능한 도구로 envctl 이라는 CLI를 만들었는데 사실 처음엔 오타로 작성한 변수인데 클로드가 env center control 이라는 거창한 의미를 부여해줬는데 썩 나쁘지 않아 사용하게 됐습니다.
env-store에는 envctl CLI와 대시보드, 암복호화 처리, 환경변수 파서, 금고 어댑터를 두었습니다. 구조는 아래와 같습니다.
env-store/
├── bin/envctl.js # CLI 진입점
├── src/
│ ├── commands/ # 명령별 구현
│ ├── vault/git-age.js # 금고 접근 어댑터
│ ├── age.js # 암복호화 래퍼
│ └── envfile.js # 환경변수 파싱과 직렬화
└── test/integration.test.js
반면 환경변수가 암호화 되어 올라가는 저장소를 vault 로 부르며 아래와 같이 구성했습니다.
recipients 에 등록 되어 있는 팀원만 접근 가능하게 하였고 프로젝트마다 나눌 뿐 아니라 프로젝트 안에서도 개발 환경 접근을 다르게 하도록 설정했습니다.
또한 meta.json 등을 통해 만료일,담당자, 메모를 설정하도록 하여 환경변수 관련 히스토리 공유가 되도록 구현했습니다.
env-store-vault/
├── recipients/
│ ├── dev.txt # 개발 환경 수신자
│ ├── prod.txt # 운영 환경 수신자
│ └── build-ci.txt # CI 전용 수신자
├── example-project/
│ ├── dev/
│ │ └── DATABASE_URL.age
│ ├── prod/
│ │ └── API_SECRET.age
│ └── meta.json # 만료일, 담당자, 메모 등
└── projects.json # 서버 렌더 대상 설정
위 프로젝트명과 변수명은 구조를 설명하기 위한 예시입니다.
이렇게 분리한 이유는 도구의 업데이트와 환경변수의 변경을 각각 관리하기 위해서였습니다. CLI 기능을 수정하는 작업과 운영 값을 교체하는 작업은 목적이 다르기 때문에, 같은 저장소에서 함께 관리할 필요가 없었습니다.
사용자도 vault를 직접 편집하기보다 envctl이나 대시보드를 통해 변경하도록 구성했습니다.
이 구조에서 Git은 저장과 동기화, 변경 이력을 담당했습니다. age 는 값의 암호화와 복호화를 담당했습니다.
age 에 대해 간단히 얘기해보자면 공개키와 개인키를 사용하는 파일 암호화 도구 입니다. 공개키로 값을 암호화하면 해당 공개키에 대응하는 개인키를 가진 사람만 복호화를 할 수 있으며, 하나의 값을 여러 수신자가 열 수 있도록 암호화하는 것도 가능합니다.
이번 구현에서는 환경변수 값을 age로 암호화해 .age 파일로 저장했습니다. 공개키는 수신자(recipients) 명단으로 관리하고 개인키는 각자의 머신에 보관해, 저장소에 접근할 수 있는 권한과 실제 값을 읽을 수 있는 권한을 분리했습니다.
그래서 결론적으로 vault 저장소에 접근하여 암호화된 환경 변수 파일을 내려받아도 해당 암호문을 열 수 있는 개인키가 없다면 값을 읽을 수 없도록 했습니다.
다만 만료일,담당자,메모 같은 경우는 불필요하게 암호화할 필요가 없다고 생각했고 내부 정책으로 메모에 민감한 정보를 넣지 말 것을 설정했습니다.
처음 구조를 보면 .env 전체를 암호화해서 파일 하나로 저장하면 되지 않을까 생각할 수 있습니다. 현재 구현에서는 환경변수 하나마다 별도의 .age 파일을 만들었습니다.
이렇게 구성한 이유는 다음과 같습니다.
첫 번째는 특정 값만 독립적으로 변경하기 위해서였습니다. 파일 전체를 암호화하면 값 하나를 교체할 때도 기존 내용을 읽고 다시 합쳐야 합니다. 반면 키별 파일을 사용하면 다른 값을 읽지 않고 해당 파일만 교체할 수 있습니다.
두 번째는 변경 이력을 변수 단위로 확인하기 위해서였습니다. 암호화된 값의 내용을 Git diff로 비교할 수는 없지만 어떤 변수 파일이 변경됐는지는 추적할 수 있습니다.
세 번째는 변수마다 복호화 가능한 수신자를 다르게 지정하기 위해서였습니다. 이후 CI에서 빌드에 필요한 값만 읽도록 구성할 때도 이 구조를 활용했습니다.
특히 기존 값을 읽을 수 없는 사용자도 수신자의 공개키와 저장소 쓰기 권한이 있으면 새 값을 암호화해 저장할 수 있습니다. 가이드에서 설명한 블라인드 쓰기는 이러한 구조를 기반으로 동작합니다.
개발자가 로컬에서 개발 환경을 띄울 때 가장 많이 사용하는 명령은 exec입니다.
envctl exec example-project dev -- npm run dev
명령이 실행되면 vault를 로컬 캐시에 동기화하고, 해당 프로젝트의 dev 값을 개인키로 복호화합니다. 이후 값을 자식 프로세스의 환경변수로 전달해 개발 서버를 실행합니다.
전체 흐름은 아래와 같습니다.
vault clone 또는 pull
→ 프로젝트와 환경에 해당하는 파일 조회
→ 개인키로 값 복호화
→ 환경변수 값 정리
→ .env.me 개인 설정 병합
→ 자식 프로세스 실행
이 방식에서는 envctl이 평문 .env 파일을 생성하지 않습니다. 애플리케이션은 기존처럼 환경변수를 사용하고, 값을 준비하는 과정을 도구가 담당하도록 했습니다.
또한 팀 공용 설정과 개인 설정을 분리하기 위해 .env.me를 사용했습니다. 예를 들어 개인 개발 포트처럼 공용 금고에 저장할 필요가 없는 값은 이 파일에서 덮어쓸 수 있습니다. 시크릿처럼 보이는 변수명에는 경고를 출력하도록 했으며, 개인 설정 파일에 시크릿을 저장하는 것은 피하도록 안내했습니다.
원격 저장소에 연결할 수 없을 때는 경고 후 마지막 로컬 캐시로 실행할 수 있도록 했습니다. 따라서 원격 동기화가 성공한 경우에는 갱신된 값을 읽지만, 오프라인에서는 이전 값일 수 있습니다. 실행 중인 프로세스의 환경변수도 이후 금고 변경에 따라 자동으로 바뀌지는 않습니다.
환경변수 하나를 추가하거나 교체할 때는 set을 사용했습니다.
envctl set example-project dev API_SECRET --owner backend-team
새 값은 입력 프롬프트에서 받고, 암호화한 결과만 vault에 저장했습니다. 암호화 래퍼에서는 값을 명령행 인자로 전달하지 않고 표준 입력으로 넘기도록 구성했습니다.
저장한 변경은 Git에 커밋하고 원격으로 push했습니다. 다른 팀원은 이후 exec가 원격 동기화에 성공하면 갱신된 값을 읽을 수 있습니다. 파일을 다시 전달하고 각자 덮어쓰는 과정 대신, 원본 저장소에 변경을 반영하는 흐름으로 구성했습니다.
Git 이력은 어떤 변수 파일을 변경했는지 확인하는 데 사용했습니다.
새로운 팀원이 사용하는 값에 접근하려면 먼저 자신의 키페어를 생성하도록 했습니다.
envctl keys gen
개인키는 각자의 머신에 보관하고 공개키만 기존 팀원에게 전달했습니다. 이후 기존 팀원이 해당 환경에 공개키를 등록했습니다.
envctl keys add dev "age1..." --name teammate
여기서 중요했던 부분은 명단 변경과 재암호화를 함께 처리하는 것이었습니다. 공개키를 명단 파일에 추가하는 것만으로 이미 저장된 암호문을 새 사용자가 열 수 있는 것은 아니기 때문입니다.
처리 과정은 아래와 같습니다.
해당 환경의 기존 값 복호화
→ 수신자 명단 변경
→ 변경된 공개키 명단으로 전체 재암호화
→ Git 커밋 및 push
현재 온보딩은 개발팀이 소수이기 때문에 팀 전원이 dev와 prod에 접근하는 정책을 기준으로 작성했습니다. 물론 설계 단계에서는 개별로 접근 가능하도록 설계 되어 있습니다.
로컬 실행은 프로세스에 값을 주입했지만 서버에서는 금고의 값을 .env로 생성하는 render를 구현했습니다.
envctl render example-project --yes
projects.json에 등록된 기본 경로와 PM2 프로세스 이름을 읽고, 운영 값을 복호화해 파일을 생성한 뒤 reload를 실행하도록 했습니다. 이때 deprecated 상태의 키는 렌더 대상에서 제외했습니다.
서버 파일을 교체하는 과정에는 다음과 같은 처리를 추가했습니다.
0600으로 설정했습니다..env.bak으로 백업했습니다.임시 파일을 사용한 이유는 파일을 작성하는 도중에 애플리케이션이 불완전한 내용을 읽는 문제를 줄이기 위해서였습니다. 키 제거 비율을 확인하는 것은 잘못된 금고 상태나 예상하지 못한 대량 삭제를 서버에 그대로 반영하지 않기 위한 처리였습니다.
이 과정에서 금고에 저장하는 것과 서버에 반영하는 것을 분리했습니다. 운영 값이 변경됐다는 이유로 서비스에 즉시 적용하기보다, 별도의 반영 시점을 갖도록 구성했습니다.
CLI에 익숙하지 않거나 여러 변수를 함께 관리할 때 사용할 수 있도록 대시보드도 구현했습니다.
envctl dashboard
대시보드는 별도 웹 서비스로 배포하지 않고 실행한 머신의 127.0.0.1에서 열리도록 구성했습니다. 실행할 때마다 랜덤 토큰을 생성하고 API 요청에서 확인했습니다.
값을 조회할 때는 실행 머신의 age 개인키를 사용했습니다. 별도 사용자 계정과 로그인 기능을 만드는 대신 기존 복호화 구조를 그대로 활용했습니다.
화면에서는 프로젝트와 환경별 값을 확인하고, 변수를 추가하거나 수정하며, 담당자와 만료일을 관리할 수 있도록 했습니다. 서버 반영 버튼은 SSH로 같은 render 명령을 실행하도록 연결했습니다.


즉, CLI와 화면에서 각각 다른 저장 로직을 구현하기보다 같은 금고 접근 흐름을 사용하도록 했습니다. 서버 반영에는 별도의 SSH 접근 권한이 필요하며, 로컬 대시보드도 실행 머신과 개인키를 신뢰하는 구조입니다.
환경변수의 값 외에 만료일과 담당자, 메모를 관리하기 위해 meta.json을 사용했습니다.
{
"prod/API_SECRET": {
"expiresAt": "2027-01-31",
"owner": "backend-team",
"memo": "외부 서비스에서 갱신 후 교체"
}
}
만료 여부를 확인할 때 값 자체는 필요하지 않기 때문에 메타데이터를 암호문과 분리했습니다. 이를 기반으로 만료 예정 목록을 확인하는 expiring과 Slack 알림을 보내는 notify를 구현했습니다.

환경변수를 중앙화하면서 값의 반영 시점도 함께 구분해야 했습니다.
서버 프로세스가 실행할 때 읽는 값은 render와 reload를 통해 반영할 수 있지만, NEXT_PUBLIC_*처럼 브라우저 번들에 포함되는 값은 재빌드와 재배포가 필요합니다. 환경변수의 원본을 한 곳에 모아도 이 차이가 없어지는 것은 아닙니다.
이를 위해 빌드 변수 판별 규칙을 두고 CI 전용 수신자를 추가했습니다. 운영 환경의 빌드 변수에는 환경 수신자와 CI 수신자를 함께 지정하고, 일반 런타임 시크릿에는 CI 수신자를 추가하지 않도록 했습니다.
빌드 변수
→ 팀·서버 수신자 + CI 수신자로 암호화
일반 런타임 시크릿
→ 환경 수신자로 암호화
또한 실제 배포 워크플로우에서는 애플리케이션 코드와 별도로 env-store-vault 체크아웃했습니다. 이때 저장소 접근에는 SSH deploy key를 사용하고, 값 복호화에는 CI 전용 age 개인키를 사용해 두 권한을 분리했습니다.
이후 CI 키로 복호화할 수 있는 값만 .env.production 으로 생성해 빌드에 사용했습니다. 개인키 임시 파일은 복호화 이후 삭제하게 했고, 생성한 환경변수 파일도 배포 아티팩트에 포함되지 않도록 패키징 전에 제거했습니다.
이렇게 하면 CI가 vault의 암호문을 가져와도 자신의 개인키로 열 수 있는 값만 사용할 수 있어 CI/CD 단계에서 보안 리스크를 없앨 수 있었습니다.
기존 파일의 값을 프로세스에 직접 주입할 때는 프레임워크가 사용하는 예약 변수도 고려해야 했습니다.
현재 exec에서는 금고의 NODE_ENV를 제외했습니다. 프레임워크나 실행 명령이 정하는 값을 금고에서 강제로 주입해 기존 동작을 바꾸지 않도록 하기 위한 처리였습니다.
개인 설정 파일은 이후에 병합하므로 .env.me에서도 예약 변수를 사용하지 않도록 주의해야 합니다. 시크릿 변수에 대한 경고 역시 저장을 강제로 차단하는 기능은 아니기 때문에 사용 규칙과 함께 안내했습니다.
키 관리에서 가장 명확하게 구분해야 했던 것은 복호화 수신자를 제거하는 것과 실제 시크릿을 교체하는 것이었습니다.
수신자 명단에서 사용자를 제거하고 현재 파일을 재암호화해도, 그 사람이 이미 읽은 값은 회수할 수 없습니다. Git 과거 이력이나 내려받은 암호문에는 기존 개인키로 열 수 있는 버전이 남아 있을 수도 있습니다.
따라서 퇴사나 개인키 유출에 대응할 때는 다음 과정이 함께 필요합니다.
저장소 접근 권한 회수
→ 수신자 제거 및 재암호화
→ 발급처에서 실제 시크릿 교체
→ 새 값을 금고와 서비스에 반영
도구 구현과 함께 팀원이 처음 사용할 때 따라갈 문서도 정리했습니다.
온보딩에서는 설치, 개인키 생성, 공개키 등록 요청, 개발 서버 실행이라는 네 단계에 집중했습니다. 구조와 전체 명령을 설명하는 가이드는 별도로 두었습니다.
매일 실행할 때 모든 구조를 이해해야 하는 부담은 줄이면서, 문제가 생기거나 관리 작업이 필요할 때는 자세한 설명을 찾아볼 수 있도록 구성했습니다.
현재 도구 설치는 clone 후 npm link를 사용하는 방식입니다. 클론한 코드를 실행하므로 해당 폴더에서 업데이트하면 도구에도 반영됩니다. 대신 폴더를 삭제하거나 옮기면 링크가 깨질 수 있고, Node 설치 환경이 바뀌면 다시 연결해야 할 수 있습니다. 업데이트에 의존성 변경이 포함됐다면 설치 작업도 필요합니다.
이번 작업에서는 환경변수 파일을 공유하던 과정을 중앙 저장소 기반의 사용 흐름으로 옮겼습니다. 구현한 내용은 다음과 같습니다.
환경변수를 한 곳에 저장하는 것만 생각하면 비교적 작은 작업처럼 보일 수 있습니다. 하지만 실제로는 값을 바꾸는 사람, 사용하는 프로세스, 서버에 반영하는 시점, 권한을 회수하는 과정까지 함께 정해야 했습니다.
또한 가장 중요한 환경변수 정보이다보니 관리와 보안 측면에서도 굉장히 많은 신경을 써야 했습니다. 그리고 실제로 팀원들이 쉽게 사용할 수 있도록 온보딩 가이드 및 사용법이 단순해야 했습니다.
현재 약 6개월 정도 사용하고 있는데 아직까진 큰 문제는 발생하지 않았고 팀원들이 환경변수를 관리하고 공유할 때 별도의 소통 없이도 기록으로 확인할 수 있는 시스템을 갖추게 되었습니다.
이 작업을 하면서 평소 번거롭게 생각했던 문제들을 기술로 풀어낼 수 있다는 점을 배웠고 실제로 팀원들의 개발 생산성이 향상 되는 과정을 보면서 나름의 뿌듯함도 느낄 수 있었습니다.
요즘 어떤 개발자가 되고 싶은지 많은 고민이 있으나 개발자 경험을 향상 시키거나 팀의 생산성을 극대화 할 수 있는 도구를 만드는 엔지니어도 관심이 생기게 된 것 같습니다.
사내 개발 도구뿐만 아니라 개인적으로도 오픈소스를 하나 만들어보고 싶다는 생각이 커서 곧 그런 내용으로 블로그에 찾아오도록 하겠습니다. 감사합니다.