다른 팀에서 전용 테넌트를 하나 요청했다. 앱 구성은 기존 테넌트와 같고, 환경 설정값을 모아 둔 프로파일만 따로 써야 한다는 조건이었다. 하나의 서비스를 여러 고객사가 각자 쓰도록 바꾸는 멀티 테넌트 SaaS 전환 프로젝트의 온보딩 파이프라인에서 생긴 일이다. 온보딩 스크립트는 프로파일을 이 한 줄로 고른다(프로파일 파일을 도입한 경위는 따로 적었다).
PROFILE_ENV="${ENV:-${TEMPLATE_GIT_REPO_BRANCH}}"
PROFILE_FILE="${TEMPLATE_TEMP_DIR}/profiles/profile-${PROFILE_ENV}.yaml"
급해서 가장 빠른 길로 갔다. 그 테넌트면 ENV와 상관없이 전용 프로파일을 쓰도록 onboarding-v6.sh와 app-update.sh 두 곳에 같은 분기를 넣었다.
# 특정 테넌트는 PROFILE_ENV 값과 무관하게 전용 프로파일을 강제로 쓴다
if [ "${TENANT_ID}" = "tenant-a" ]; then
echo "[profile-loader] TENANT_ID=tenant-a 감지: PROFILE_ENV를 'custom'으로 강제 치환합니다. (기존='${PROFILE_ENV}')"
PROFILE_ENV="custom"
fi
돌려 보니 되긴 했는데 찜찜했다. ENV가 제대로 들어온다면 dev나 stg 같은 값이 알아서 프로파일을 골라야 한다. 왜 이 테넌트는 그 값을 무시하고 강제로 넣어야만 동작하는가. 애초에 ENV가 안 들어오는 건 아닌가 싶었다.
스크립트에서 레코드까지 거꾸로 따라갔다
buildspec.tenant.yml의 env.variables에도, parameter-store에도, secrets-manager에도 ENV는 없었다. 따지고 보면 이 buildspec은 ${APP_ID}, ${TENANT_REPO_ID}로 시크릿 경로를 만들면서도 그 둘을 정의한 곳이 없다. 변수는 buildspec 밖에서 들어오고 있었다.
CodeBuild 앞에는 Step Functions가, 그 앞에는 DynamoDB 레코드 변경 이벤트를 받아 도는 Lambda(onboarding-event-handler)가 있다. 이 Lambda가 레코드의 최상위 키 이름을 대문자로 바꿔 그대로 CodeBuild 환경변수 이름으로 쓴다는 건 버튼 한 번에 고객사 전용 환경이 뜨기까지에서 설명했다. 레코드에 env 필드가 있으면 그게 ENV가 된다. 그럼 env는 누가 레코드에 넣는가. 레코드를 만드는 CP API의 온보딩 코드였다.
ENV = os.getenv("ENV") # CP API Lambda의 환경변수. Terraform var.env에서 내려온다
...
if onboarding_type == "ap":
# 앱플레인 생성 분기
onboarding_event.update(
dict(
env=ENV, # env를 넣는 곳은 여기뿐이다
dx_cidr=reserved_dx_cidr,
...
)
)
else:
# 테넌트 생성 분기: env가 없다
onboarding_event.update(
dict(
tenant_repo_id=app_plane["tenant_repo_id"],
app_id=app_plane["app_id"],
...
)
)
env는 앱플레인을 만들 때만 레코드에 들어가고, 테넌트를 만들 때는 들어가지 않았다. onboarding-v6.sh는 테넌트 빌드에서 돈다. 그러니 ENV는 언제나 빈 값이었고, ${ENV:-${TEMPLATE_GIT_REPO_BRANCH}}에서 늘 템플릿 레포의 브랜치 이름으로 떨어지고 있었다. 보통 테넌트는 브랜치 이름이 곧 환경 이름이라 그래도 맞는 프로파일을 찾았을 것이다. 급하게 넣은 한 줄짜리 예외가 이 구멍을 가리고 있었고, 분기가 동작했기 때문에 오히려 구멍이 안 보였다.
PROJECT는 복사하게 하고 ENV는 남겨 두었다
마침 같은 날 템플릿과 프로파일 경로를 프로젝트 단위로 나누는 작업을 하고 있었다. 나중에 외부 프로젝트가 올라오면 테넌트 이름마다 if를 넣을 수는 없어서, PROJECT라는 축을 하나 두고 템플릿 폴더와 프로파일 폴더를 둘 다 이 값으로 고르게 했다. ENV와 같은 구멍을 다시 파지 않으려고, 이 필드는 앱플레인을 만들 때 받은 값을 테넌트 레코드를 만들 때 복사하게 했다.
# 테넌트 생성 분기
onboarding_event.update(
dict(
tenant_repo_id=app_plane["tenant_repo_id"],
app_id=app_plane["app_id"],
# 앱플레인의 프로젝트명을 테넌트 레코드로 전파한다.
# 스크립트는 이 값을 PROJECT 환경변수로 받는다
project=app_plane.get("project", "base"),
...
)
)
ENV까지 테넌트 레코드로 내리는 건 보류했다. 지금은 모든 테넌트 빌드가 브랜치 이름 기본값에 기대어 돌고 있어서, ENV가 채워지는 순간 모든 테넌트의 프로파일 선택이 달라질 수 있다. 손댈 범위가 커 보여서 이번에는 미뤘고, 그 탓에 테넌트 이름 분기도 그대로 남아 있다. 이 일이 끝나야 지울 수 있는 한 줄이다.