RSS
데이터베이스

Prisma 운영 DB 마이그레이션: .env 파일 없이 접속 정보를 주입해 실행하는 방법

작성자
듀오랩스 대표·11분 읽기

새 테이블이 필요한 기능을 배포하면서 운영 DB에 Prisma 마이그레이션을 직접 돌려야 했습니다. 이 프로젝트는 Vercel 빌드에서 마이그레이션을 돌리지 않습니다. 프리뷰 빌드가 운영 DB를 건드리거나, 빌드가 중간에 실패해서 스키마가 반쯤 바뀐 채 남는 일을 막으려고 일부러 뺐습니다. 그래서 스키마를 바꾸는 배포는 사람이 운영 DB에 migrate deploy를 먼저 돌리고 나서 코드를 올립니다. 세어 보니 8월 중순부터 지금까지 커밋 437개 중 10개가 여기에 해당했습니다.

문제는 그 한 번을 돌리려면 운영 DB 접속 문자열이 손에 있어야 한다는 점입니다. 이번에는 그 문자열을 파일에도, 셸 기록에도 남기지 않고 실행했습니다. 실제로 돌린 명령은 이 두 줄입니다.

cd app
vault run app 운영 -- sh -c 'DATABASE_NAME_OVERRIDE= DATABASE_URL="$DIRECT_URL" npx prisma migrate status'
vault run app 운영 -- sh -c 'DATABASE_NAME_OVERRIDE= DATABASE_URL="$DIRECT_URL" npx prisma migrate deploy'

짧아 보이지만 한 줄에 네 가지 결정이 들어 있습니다. 하나씩 풀어 보겠습니다.

명령줄에 붙여 넣은 접속 문자열이 남는 곳

흔한 방법은 둘입니다. 접속 문자열을 .env 파일에 적어 두거나, DATABASE_URL='postgresql://...' npx prisma migrate deploy처럼 명령 앞에 직접 붙여 넣는 것입니다. 저도 처음 정리한 배포 절차에는 두 번째 방법을 적어 두었습니다.

둘 다 비밀번호가 어딘가에 남습니다. .env는 파일로 남고, 실수로 커밋되거나 백업에 딸려 갈 수 있습니다. 명령줄에 붙여 넣은 값은 셸 기록(~/.zsh_history)에 그대로 저장됩니다. 운영 DB 비밀번호가 평문으로 디스크에 남는다는 점에서 둘은 같습니다.

실행하는 동안에만 존재하는 비밀값

vault run은 제가 쓰는 비밀값 금고의 CLI입니다. 패스프레이즈로 금고의 운영 환경을 열고, 그 안의 변수들을 환경변수로만 넣은 채 -- 뒤의 명령을 자식 프로세스로 실행합니다. 디스크에 파일을 만들지 않고, 명령이 끝나면 값도 프로세스와 함께 사라집니다. 이번에 실행했을 때는 "변수 18개 주입, 파일을 만들지 않습니다"라는 줄이 먼저 찍혔습니다.

이 방식은 제 금고만의 것이 아닙니다. Doppler CLI의 doppler run --, 1Password CLI의 op run --가 같은 일을 합니다. 도구는 달라도 생각은 하나입니다. 비밀값은 금고에 두고, 쓰는 순간에만 프로세스 환경으로 건네준다는 것입니다. 위 명령의 vault run app 운영 -- 자리를 doppler run --로 바꿔도 나머지는 그대로 동작합니다.

자동으로 돌리려다 한 번 막혔습니다. 패스프레이즈를 묻는 단계가 있어서, 입력이 없는 스크립트에서 실행하면 "입력이 비었습니다"로 멈춥니다. 운영 DB를 바꾸는 명령이니 사람이 터미널에서 직접 여는 편이 맞다고 보고 그대로 두었습니다.

같은 DB로 가는 두 개의 주소

금고의 운영 환경에는 같은 DB로 가는 주소가 두 개 있습니다. 앱이 쓰는 DATABASE_URL은 PgBouncer 풀러(6432)를 거치고, DIRECT_URL은 Postgres에 직접 붙습니다(5432).

풀러는 앱에 맞춘 장치입니다. 서버리스 함수가 짧은 쿼리를 수없이 날리니, 트랜잭션이 끝날 때마다 뒤쪽 연결을 다른 요청에 돌려 씁니다. 마이그레이션은 정반대입니다. 다른 마이그레이션이 동시에 돌지 못하게 잠금을 잡고, 그 연결을 끝까지 붙들고 일합니다. 트랜잭션마다 연결이 바뀌면 이런 작업이 깨집니다. Prisma의 PgBouncer 문서도 Prisma Migrate는 풀러를 거칠 때 따로 우회 방법이 필요하다고 안내합니다.

이 구분을 한 번 놓친 적이 있습니다. 백업 워크플로가 쓰던 시크릿이 앱용 풀러 주소로 바뀌면서, 매일 돌던 pg_dump가 조용히 깨졌습니다. 같은 DB라도 쓰는 쪽이 다르면 주소도 달라야 했습니다. 그때부터 두 주소를 이름부터 따로 둡니다.

그런데 Prisma는 DATABASE_URL만 읽습니다. 그래서 명령 안에서 DATABASE_URL="$DIRECT_URL"로 바꿔 끼웁니다. 앱용 이름표 자리에 직결 주소를 꽂는 것입니다. Prisma 설정 파일(prisma.config.ts)이 .env를 읽긴 하지만, dotenv는 이미 설정된 환경변수를 덮어쓰지 않습니다. 그래서 여기서 넣은 값이 우선합니다.

작은따옴표 하나로 갈리는 변수 확장 시점

명령 전체를 sh -c '...'로 감싼 데는 두 가지 이유가 있습니다.

첫째, A=B 명령 형태로 환경변수를 바꿔 끼우는 것은 셸 문법입니다. 금고 CLI는 -- 뒤를 프로그램으로 바로 실행하므로, 이 문법을 해석해 줄 셸이 하나 더 있어야 합니다.

둘째가 더 중요합니다. 따옴표 종류에 따라 $DIRECT_URL이 언제 풀리는지가 달라집니다.

따옴표 $DIRECT_URL이 풀리는 곳 결과
'...' 작은따옴표 금고가 값을 넣은 뒤의 자식 셸 실제 주소
"..." 큰따옴표 명령을 입력한 지금 셸 빈 문자열

큰따옴표로 쓰면 금고가 값을 넣기도 전에 지금 셸이 $DIRECT_URL을 풀어 버립니다. 지금 셸에는 그 변수가 없으니 빈 값이 들어가고, Prisma는 엉뚱한 곳을 보게 됩니다. 오류 메시지만 봐서는 원인을 찾기 어려운 종류의 실수입니다.

개발 DB를 가리키게 만드는 남은 환경변수

DATABASE_NAME_OVERRIDE=를 빈 값으로 넣는 것은 이 프로젝트에만 있는 장치 때문입니다. 로컬 개발에서는 이 변수로 접속 주소의 DB 이름만 개발 DB로 바꿔 끼웁니다. 편하지만 위험합니다. 이 값이 셸에 남아 있으면 운영 주소를 줘도 DB 이름이 바뀌어서 개발 DB를 가리킵니다. 운영 마이그레이션을 하던 날 이걸로 세 번 걸렸다는 기록이 프로젝트 문서에 남아 있습니다. 그래서 운영 명령에는 항상 빈 값으로 덮어써서 확실히 꺼 둡니다.

금고 쪽에도 비슷한 함정이 하나 있었습니다. 금고의 기본 환경이 사무실이고, 거기에는 변수가 0개입니다. 환경 이름을 빼고 vault run app -- ...로 실행하면 아무 값도 들어가지 않습니다. 명령에 운영을 꼭 적는 이유입니다.

Prisma가 DB 안에 두는 장부

migrate status와 migrate deploy가 무엇을 보는지 알면 둘의 차이가 분명해집니다. Prisma는 운영 DB 안에 _prisma_migrations라는 테이블을 두고, 적용한 마이그레이션마다 이름과 체크섬, 적용 시각을 한 줄씩 적습니다. 일종의 공사 장부입니다.

migrate status는 읽기만 합니다. 리포의 prisma/migrations/ 폴더 목록과 이 장부를 비교해서, 폴더에는 있는데 장부에는 없는 것을 "아직 적용 안 됨"으로 보여 줍니다. 이번에는 폴더에 10개가 있었고, 새로 만든 마이그레이션 하나만 장부에 없었습니다.

migrate deploy는 장부에 없는 마이그레이션의 migration.sql을 이름 순서(만든 시각 순서)대로 실행하고, 하나 끝날 때마다 장부에 적습니다. 실제 출력은 이랬습니다.

10 migrations found in prisma/migrations

Applying migration `20261005120000_admin_two_factor`

The following migration(s) have been applied:

migrations/
  └─ 20261005120000_admin_two_factor/
    └─ migration.sql

All migrations have been successfully applied.

로컬에서 쓰는 migrate dev와는 하는 일이 다릅니다. Prisma 문서도 둘을 개발용과 운영용으로 나눠 설명합니다.

migrate dev (로컬) migrate deploy (운영)
마이그레이션 파일 생성 스키마 변경을 보고 SQL을 만듦 만들지 않음. 있는 파일만 적용
섀도 DB 검증용 임시 DB를 씀 쓰지 않음
어긋났을 때 DB 초기화를 제안 초기화하지 않음
실패했을 때 고쳐서 다시 돌림 실패로 기록하고 멈춤. 정리하기 전에는 다음 deploy도 거부

운영에서 status를 먼저 돌리는 것도 이 표 때문입니다. deploy는 되돌리기가 없는 명령이라, 장부와 폴더가 예상대로 어긋나 있는지(이번에는 정확히 한 개) 눈으로 확인하고 나서 실행합니다.

실행 전에 걱정한 sslrootcert=system

실행 전에 걱정한 것이 있었습니다. 관리형 Postgres가 주는 접속 문자열에는 sslrootcert=system이라는 옵션이 붙어 오는 경우가 있습니다. psql은 이것을 "운영체제의 인증서 저장소를 써라"로 알아듣지만, Node의 pg 드라이버는 system이라는 이름의 파일을 열려다 실패합니다. 금고를 열어 값을 들여다보지 않은 채 실행했기 때문에, 직결 주소에 그 옵션이 남아 있는지는 확신이 없었습니다.

실행해 보니 Prisma CLI는 그 주소로 문제없이 붙었습니다. 같은 직결 주소로 GitHub Actions에서 migrate status를 돌렸을 때도 "Database schema is up to date!"가 나왔습니다. 다만 확인된 것은 Prisma CLI까지입니다. 원래 문제가 됐던 쪽은 앱이 쓰는 pg 드라이버라서, 이 주소를 앱에 그대로 넣으면 결과가 다를 수 있습니다. 주소를 새로 발급받으면 그 옵션부터 확인할 생각입니다.

금고에 없는 변수를 지우는 --prune

같은 날 마이그레이션에 앞서 새 서버 키 하나도 금고에 넣었습니다. 키는 openssl rand -base64 32 | pbcopy로 만들어 화면에 띄우지 않고 금고 웹 화면에 붙여 넣었습니다. 그다음 금고 CLI로 그 변수 하나만 Vercel에 보냈습니다.

여기서도 주의할 옵션이 있었습니다. 금고에 없는 변수를 배포처에서 지워 금고와 똑같이 맞추는 --prune입니다. 보내기 전에 금고와 Vercel을 비교해 보니, Vercel에만 있고 금고에는 없는 변수가 몇 개 있었습니다. 이 옵션을 붙였다면 운영 설정 일부가 말없이 지워졌을 것입니다. 그래서 --only로 보낼 변수를 하나로 못 박았습니다. 금고를 진짜 정본으로 두려면 그 변수들도 금고로 옮겨야 합니다. 그 작업은 아직 남아 있습니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.