ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 테스트·스테이징·프로덕션에 같은 도커 이미지를 쓰는 게 맞을까 — '로컬에서 굽고 여기저기 올린다'가 어긋나는 지점
    IT 2026. 9. 29. 21:00

    ▶ 동영상 개요 — 테스트·스테이징·프로덕션에 같은 도커 이미지를 쓰는 게 맞을까 — '로컬에서 굽고 여기저기 올린다'가 어긋나는 지점

    6분 46초 — 테스트·스테이징·프로덕션에 커스텀 도커 이미지를 공용으로 쓰는 건 맞지만 '로컬에서 굽고 여기저기 올린다'는 이미지를 두 번 굽게 만들어 '같은 바이트' 보장을… — NotebookLM 동영상 개요로 생성

    🎧 오디오 개요 — 테스트·스테이징·프로덕션에 같은 도커 이미지를 쓰는 게 맞을까 — '로컬에서 굽고 여기저기 올린다'가 어긋나는 지점

    18분 33초 — 테스트·스테이징·프로덕션에 커스텀 도커 이미지를 공용으로 쓰는 건 맞지만 '로컬에서 굽고 여기저기 올린다'는 이미지를 두 번 굽게 만들어 '같은 바이트' 보장을…. 화면은 이 글의 인포그래픽 한 장 고정 — NotebookLM 오디오 개요로 생성

    얼마 전 스스로에게 던진 질문이 있다. 애플리케이션을 커스터마이징한 도커 이미지를 하나 구워 Amazon ECR(Elastic Container Registry, AWS가 운영하는 사설 컨테이너 이미지 저장소)에 올려 두고, 테스트 서버·스테이징 서버·프로덕션이 그 하나를 공용으로 쓰면 더 낫지 않을까? 그렇다면 방법은 단순해 보였다 — 로컬에서 이미지를 만들어 여기저기 올려 두면 되는 것 아닌가? 스테이징은 ECR에 접속하지 못할 테니 스테이징이 닿는 저장소에도 따로 올리고, 프로덕션을 위해 ECR에도 올린다.

    질문은 사실 두 개가 겹쳐 있다. 하나는 "환경마다 이미지를 새로 굽는 것보다 공용 이미지 하나가 나은가"이고, 다른 하나는 "그 공용 이미지를 만드는 방법이 '로컬에서 굽고 여기저기 올린다'가 맞는가"다. 앞의 답은 그렇다이고, 뒤의 답은 아니다. 그리고 이 둘이 왜 갈리는지를 알면, 스테이징이 ECR에 못 붙는 상황도 "이미지를 또 굽는" 게 아니라 "같은 이미지를 복사한다"로 자연스럽게 풀린다. 이 글은 그 갈림을 따라간 기록이다.

    결론부터 — 공용 이미지는 맞다, 다만 "한 번 굽고 승격한다"

    컨테이너 배포의 표준 원칙은 빌드 한 번, 어디에나 배포(build once, deploy everywhere)다. 소스에서 이미지를 딱 한 번 굽고, 그렇게 나온 하나의 불변 산출물(immutable artifact)을 테스트 → 스테이징 → 프로덕션으로 그대로 밀어 올린다. 이 "밀어 올린다"를 승격(promotion)이라 부른다 — 다시 굽지 않고 이미 검증된 그 이미지를 다음 환경으로 옮기는 일이다.

    diagram

    다이어그램 설명. 이 그림이 보여주는 핵심은 빌드가 한 번뿐이고, 그 결과물이 세 환경으로 갈라져 들어간다는 것이다. "빌드 1회"에서 나온 "불변 이미지 1개"가 테스트·스테이징·프로덕션 모두에서 똑같이 돌아간다. 이 방식이 값을 하는 이유는 하나로 요약된다 — 테스트에서 통과시킨 바로 그 바이트가 프로덕션에서 도는 바이트와 완전히 같다는 보장이다. 여기서 놓치기 쉬운 오해가 하나 있다. "불변 이미지 1개"는 세 환경이 모든 면에서 똑같아진다는 뜻이 아니다. 이미지는 같아도 각 환경이 바라보는 데이터베이스 주소나 비밀 키는 달라야 하는데, 그 차이를 어디서 흡수하는지는 잠시 뒤에 다룬다.

    왜 "환경마다 새로 굽기"가 위험한가 — 같은 소스도 다른 이미지가 된다

    공용 이미지가 왜 환경별 빌드보다 나은지는, 환경마다 새로 굽는 쪽의 위험을 보면 뚜렷해진다. 직관적으로는 "같은 Dockerfile로 빌드하면 같은 이미지가 나오겠지"라고 생각하지만, 현실은 다르다. 한 연구에서 실제 Dockerfile들을 조사했더니 비트 단위로 재현 가능한 경우는 2.7%에 불과했고, 빌드 환경을 손봐도 78.7%는 여전히 재현되지 않았다.

    이유는 빌드가 시점에 따라 달라지는 것들을 조용히 끌어오기 때문이다. FROM node:20 같은 움직이는 태그는 오늘과 내일의 실제 내용이 다를 수 있고, apt-get install·pip install은 그 순간의 최신 패키지 버전을 당겨온다. 그래서 테스트 서버에서 굽고, 스테이징 서버에서 또 굽고, 프로덕션에서 또 구우면 세 이미지가 미묘하게 다른 물건이 될 수 있다. "테스트에서는 멀쩡했는데 프로덕션에서만 깨진다"의 상당수가 여기서 온다 — 애초에 테스트한 이미지와 프로덕션 이미지가 다른 바이트였던 것이다. 공용 이미지 하나로 승격하는 방식은 이 재현성 문제를 원천에서 지운다. 굽는 행위 자체가 한 번뿐이라 "다른 이미지가 될 여지"가 없다.

    이미지가 같으면 환경 차이는 어디서 흡수하나 — 설정을 밖으로 뺀다

    여기서 자연스러운 반문이 나온다. 테스트·스테이징·프로덕션은 데이터베이스 주소도, API 키도, 로그 레벨도 다른데 이미지가 하나면 그 차이는 어떻게 하나? 답은 그 차이를 이미지 안에 굽지 않고 실행 시점에 밖에서 주입한다는 것이다. 이것이 12-factor(클라우드 애플리케이션 설계 원칙 모음)가 말하는 설정 분리의 핵심이다 — 환경마다 달라지는 값은 코드나 이미지가 아니라 환경 변수(environment variable)로 둔다.

    diagram

    다이어그램 설명. 이 그림은 하나의 이미지가 세 컨테이너로 실행되되, 각 컨테이너가 자기 환경의 설정을 따로 얹는다는 구조를 보여준다. "같은 이미지"는 세 갈래 모두에서 동일하고, 달라지는 것은 각 컨테이너에 주입되는 설정뿐이다. 이렇게 나누는 이유가 중요하다 — 만약 데이터베이스 주소를 이미지 안에 구워 버리면 테스트용·스테이징용·프로덕션용 이미지를 각각 따로 구워야 하고, 그 순간 "공용 이미지 하나"라는 전제가 무너진다. 설정을 밖으로 빼야 비로소 이미지가 진짜 하나로 유지된다. 실제 docker compose에서는 이렇게 생긴다.

    # 세 환경이 같은 이미지를 가리킨다 — 태그가 아니라 digest로 고정
    services:
      app:
        image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app@sha256:abc123...
        environment:
          # 환경마다 달라지는 값은 이미지가 아니라 여기서 주입한다
          - DATABASE_URL=${DATABASE_URL}   # 테스트/스테이징/프로덕션이 각자 다른 값
          - LOG_LEVEL=${LOG_LEVEL}
    

    코드 설명. image: 줄이 @sha256:...로 끝나는 게 핵심이다. 태그(:v1)는 나중에 다른 이미지를 가리키도록 바뀔 수 있는 이름표지만, digest(다이제스트, 이미지 내용을 해시로 계산한 지문)는 그 내용이 조금이라도 다르면 값 자체가 달라진다. 즉 digest로 고정하면 세 환경이 문자 그대로 같은 바이트를 당긴다는 게 파일에 박힌다. 반대로 환경별로 달라져야 하는 값은 environment 블록에서 주입하므로, 이미지는 하나여도 각 서버는 자기 데이터베이스를 바라본다.

    그래서 "로컬에서 굽고 여기저기 올린다"는 왜 어긋나나

    이제 처음의 방법으로 돌아온다. "로컬에서 이미지를 만들어 ECR에도 올리고 스테이징 저장소에도 올린다"는 발상에는 두 겹의 함정이 있다. 첫째, 로컬 빌드는 앞서 본 재현성 문제를 그대로 안는다 — 개발 노트북의 캐시·패키지 상태·시각에 따라 결과가 달라진다. 둘째, 더 결정적으로, ECR에 올릴 때 한 번 굽고 스테이징 저장소에 올릴 때 또 구우면 두 저장소에 서로 다른 바이트가 올라간다.

    diagram

    다이어그램 설명. 이 그림은 "여기저기 올린다"가 실제로는 서로 다른 두 이미지를 만든다는 것을 드러낸다. "개발 노트북"에서 두 번 빌드해 하나는 ECR로, 하나는 스테이징 저장소로 밀면, 프로덕션이 실행하는 "이미지 A 실행"과 스테이징이 실행하는 "이미지 B 실행"은 이름만 같을 뿐 내용이 어긋날 수 있다. 공용 이미지를 쓰려던 애초의 목적 — 모든 환경이 같은 물건을 돌린다는 보장 — 이 바로 이 지점에서 깨진다. 한 번 굽고 승격하는 방식과 겉보기에는 비슷해 보여도, 굽는 횟수가 둘이라는 점에서 완전히 다른 이야기다.

    고치는 방법은 굽는 횟수를 하나로 되돌리는 것이다. 빌드는 CI에서 한 번만 하고(로컬 노트북이 아니라 통제된 빌드 서버라 재현성이 확보된다), 그 결과 이미지를 digest로 식별한 뒤, 이후에는 어떤 저장소로 보내든 다시 굽지 않고 그 digest를 복사한다. 굽기와 옮기기를 분리하는 것 — 이것이 "여기저기 올린다"와 "한 번 굽고 승격한다"의 진짜 차이다.

    스테이징이 ECR에 못 붙는다면 — 굽지 말고 복사한다

    이제 가장 현실적인 제약을 다룬다. 스테이징이 격리된 망에 있어 ECR에 접근하지 못한다면, 스테이징이 닿는 별도 저장소가 필요한 건 맞다. 하지만 그 저장소를 채우는 방법이 "스테이징용으로 이미지를 또 굽기"여서는 안 된다. ECR에 있는 그 이미지를 digest 단위로 그대로 복사해 넣으면 된다. 이미지 복사는 이미지를 다시 굽는 것과 전혀 다른 작업이다 — 이미 완성된 바이트 덩어리를 저장소에서 저장소로 옮기는 것뿐이라, 내용이 바뀔 여지가 없다.

    diagram

    다이어그램 설명. 이 흐름은 한 번 구운 이미지가 두 저장소에 같은 내용으로 존재하게 만드는 방법을 보여준다. "CI 빌드 1회"에서 나온 "digest 지문"이 먼저 ECR에 올라가고, 그 이미지를 "같은 digest 복사" 단계가 통째로 떠서 "스테이징이 닿는 저장소"로 옮긴다. 스테이징은 ECR을 몰라도 자기 저장소에서 그 이미지를 당긴다. 여기서 결정적 이점은, 복사는 digest를 보존하므로 두 저장소의 이미지가 비트 단위로 동일함이 보장된다는 것이다. "여기저기 각각 굽기"에서는 없던 보장이다. 실무에서는 이 복사를 손으로 하지 않고 레지스트리 복제(replication) 기능으로 자동화하거나, 아래처럼 한 줄 명령으로 처리한다.

    # ECR의 이미지를 digest 그대로 스테이징 저장소로 복사 (재빌드 아님)
    #   skopeo 는 레지스트리 간 이미지 복사 전용 도구다
    skopeo copy \
        docker://123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app@sha256:abc123... \
        docker://staging-registry.internal/my-app:v1
    

    코드 설명. skopeo copy는 원본을 digest로 지정해 그 내용을 스테이징 저장소로 그대로 옮긴다. 원본에 @sha256:을 쓰는 게 중요하다 — 움직이는 태그가 아니라 지문으로 집었기 때문에, "복사하는 사이에 원본이 바뀌어 엉뚱한 걸 옮기는" 일이 없다. 목적지에는 사람이 읽기 쉬운 :v1 태그를 붙이되, 그 v1이 가리키는 실체는 ECR의 그 이미지와 같은 바이트다. 스테이징이 ECR에 직접 못 붙는 제약은 이렇게 "저장소를 하나 더 두되, 이미지는 굽지 말고 복사"로 풀린다. 두 저장소를 갖는 것과 두 번 굽는 것은 전혀 다른 선택이다.

    정리 — 공용 이미지의 값어치는 "같은 바이트 보장"에서 나온다

    처음 질문으로 돌아가면, 테스트·스테이징·프로덕션에 커스텀 이미지를 공용으로 쓰는 것은 맞는 방향이다. 다만 그 이점의 정체를 정확히 짚어야 한다 — 공용 이미지가 값을 하는 이유는 "저장소를 아낀다"가 아니라 "한 번 구운 그 바이트가 모든 환경에서 똑같이 돈다는 보장"이다. 그리고 '로컬에서 굽고 여기저기 올린다'는 바로 그 보장을 스스로 깨는 방식이다. 굽는 횟수가 둘 이상이 되는 순간, 이름만 같고 내용이 다른 이미지들이 각 환경에 흩어진다.

    그래서 규칙은 세 줄로 요약된다. 이미지는 CI에서 한 번만 굽는다. 다른 환경·다른 저장소로는 다시 굽지 말고 digest로 복사·승격한다. 환경마다 달라지는 값은 이미지가 아니라 설정으로 주입한다. 스테이징이 ECR에 못 붙는 상황조차 이 규칙 안에서 "저장소를 하나 더 두고 같은 이미지를 복사"로 깔끔하게 풀린다. 서버가 한 대일 때는 이 차이가 사소해 보이지만, 환경이 셋으로 늘어나는 순간 "테스트한 것과 배포된 것이 정말 같은가"라는 질문이 매번 따라붙는다 — 그 질문에 자신 있게 그렇다고 답하려면, 굽기는 한 번이어야 한다.


    참고한 공개 자료:


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.