1~2편에서 CI/CD 큰그림과 CI 빌드(JDK/Gradle/Wrapper/캐시)를 정리했다.
이번 3편은 그 다음 단계인 “Docker 이미지를 어떻게 만들고(Docker build), 어떤 태그를 붙이고(metadata), 어떻게 올리는지(push)”를 다룬다.
이 글에서 다루는 범위는 다음이다.
- Login to Docker Hub
- Docker metadata (latest + sha 태그 전략)
- Build and push Docker image (buildx + gha cache-from/cache-to)
- Dockerfile이 의미하는 것
- Gradle 캐시 vs Docker 빌드 캐시 차이
4편에서는 SSM 배포(EC2에서 deploy.sh 실행), 시크릿 주입(.env), application.yml 시크릿 주입 위험, 운영/트러블슈팅을 다룬다.
빌드 파이프라인에서 Docker 단계가 들어가는 위치
내 전체 흐름은 아래다.
- GitHub 이벤트 발생
- Gradle 빌드
- Docker 이미지 빌드
- DockerHub push
- GitHub Actions → AWS SSM send-command
- EC2가 deploy.sh 실행 → docker pull → docker run
여기서 3~4이 이번 3편의 핵심이다.
- GitHub Actions Runner에서 Docker 이미지를 빌드한다.
- 빌드한 이미지를 DockerHub 저장소에 push한다.
그리고 이게 가능한 전제는:
- 1~2편에서 만든 결과물(예:
build/libs/*.jar)이 Docker 이미지에 포함되어야 한다는 점이다.
1) Docker Buildx 설정이 왜 먼저 필요한가?
워크플로우에 이 단계가 들어가 있다.
- name: Set up Docker Builder
uses: docker/setup-buildx-action@v3
이 스텝은 “도커를 설치한다” 수준이 아니라, Buildx(빌드 확장 기능)를 세팅한다.
특히 내 워크플로우에서 아래 옵션을 쓰기 때문에 중요하다.
cache-from: type=ghacache-to: type=gha,mode=max
즉, Docker 빌드 캐시를 GitHub Actions 캐시(GHA)에 저장/복원하려면 Buildx가 필요하다.
2) Login to Docker Hub
- name: Login to Docker Hub
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
이 스텝이 하는 일
이 스텝은 Github Actions 가상머신(Runner)이 도커 허브(Docker Hub)에 로그인하도록 하는 액션이다.
docker/build-push-action을 사용해 이미지를 도커 허브에 업로드하려면 반드시 먼저 로그인이 되어 있어야 한다.
항목 설명
if: ...- 이 로그인 스텝을 오직 main 브랜치에 push 이벤트가 발생했을 때만 실행하라는 조건문
- 그리고 수동 실행(workflow_dispatch)일 때도 실행하도록 확장됨
- dev 브랜치나 다른 브랜치에 푸시할 때는 이미지를 빌드하고 푸시할 필요가 없으므로, 로그인 자체도 건너뛰게 만든다.
uses: docker/login-action@v3- Docker 로그인을 처리해 주는 공식 Github 액션을 사용한다.
username: ${{ secrets.DOCKERHUB_USERNAME }}secrets.DOCKERHUB_USERNAME은 Github 레포지토리의Settings > Secrets and variables > Actions에 미리 저장해 둔 도커 허브 아이디 값을 가져와서 사용
password: ${{ secrets.DOCKERHUB_TOKEN }}secrets.DOCKERHUB_TOKEN은 Githubsecrets에 저장해 둔 도커 허브 액세스 토큰(Access Token) 값을 가져온다.- (중요) 보안을 위해 실제 도커 허브 계정의 비밀번호 대신, 도커 허브에서 발급받은 액세스 토큰을 사용한다.
3) Docker metadata (태그 전략: latest + sha)
코드
- name: Docker metadata
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/oneco
tags: |
type=raw,value=latest
type=sha
이 스텝은 빌드/푸시가 아니다
이 스텝은 도커 이미지를 빌드하거나 푸시하는 단계가 아니다.
어떤 태그를 붙일지를 결정해서 다음 스텝에 전달할 메타데이터를 준비하는 역할을 한다.
항목 설명
docker/metadata-action@v5- Docker 이미지를 빌드할 때 사용할 태그와 라벨을 자동으로 생성해 주는 도구이다.
images: ${{ secrets.DOCKERHUB_USERNAME }}/oneco- 생성할 이미지의 기본 이름을 [DOCKERHUB_USERNAME]/oneco(예: gimin/oneco)로 지정한다.
tags: |- 밑으로 이 이미지에 붙일 태그 생성 규칙을 정의한다.
type=raw, value=latest- raw 타입: 내가 그냥 지정한 값(
latest)을 그대로 쓴다. - 결과:
gimin/oneco:latest라는 태그를 생성한다.
- raw 타입: 내가 그냥 지정한 값(
type=shasha타입: 이 워크플로우를 실행시킨 Git 커밋의 고유 ID(SHA 해시값)를 태그로 쓴다.- 결과: 만약 커밋 해시가
a1b2c3d라면gimin/oneco:sha-a1b2c3d라는 태그를 생성한다.
태그를 두 개 쓰는 이유
type=sha의 장점
- 추적성:
my-user/oneco:sha-a1b2c3d라는 이미지를 보면 이 이미지는 Git의sha-a1b2c3d커밋을 기반으로 만든 것이라는 것을 추적할 수 있다. - 안정적인 롤백:
latest(커밋a1b2c3d)를 배포했다가 문제가 생기면, 도커 허브에 남아있는 이전 버전(예:my-user/oneco:f9e8d7c)을 가져다가 즉시 롤백(재배포)할 수 있다.
결론: latest 태그와 sha 태그를 함께 쓰는 것은, 최신 버전이 무엇인지 쉽게 알려주면서도(latest), 모든 변경 이력을 고유하게 남겨(sha) 안정성을 확보하는 방식이다.
latest는 “최신 포인터”라서 새 푸시가 오면 대상 이미지가 바뀌는 가변 태그다.sha는 커밋마다 생성되어 계속 누적되고, 한 번 생성된 태그가 다른 이미지로 바뀌지 않는 불변 태그다.- 운영 관점에서는
latest만 쓰면 “지금 최신이 뭔지”는 편하지만, “어떤 커밋이었는지”와 “롤백”이 약해진다. - 그래서 보통은
latest + sha를 같이 둔다.
태그의 작동 원리
- 빌드(Build):
docker/build-push-action은 내 코드로 단 하나의 이미지를 빌드한다. 이 이미지는sha256:12345abcdef...같은 고유한 해시(ID) 값을 가진다. - 푸시(Push): 이 하나의 이미지를 도커 허브에 업로드한다.
- 태그(Tag): 도커 허브에 위의 고유한 해시(ID) 값을 두 가지 이름(태그)으로 부르겠다고 등록
gimin/oneco:latest→12345abcdef...gimin/oneco:a1b2c3d→12345abcdef...
두 태그 모두 완벽하게 동일한 하나의 이미지를 가리킨다.
두 태그는 도커 허브의 레포지토리에 저장된다.
도커 허브의 gimin/oneco 저장소에 Tags 탭에서 모든 태그들을 볼 수 있다.
4) Build and push Docker image (실제 빌드/푸시)
워크플로우 코드
- name: Build and push Docker image
if: >
(github.event_name == 'push' && github.ref == 'refs/heads/main')
|| github.event_name == 'workflow_dispatch'
uses: docker/build-push-action@v6
with:
context: ./oneco
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
이 스텝이 하는 일
이전 단계(로그인, 메타데이터 생성)을 한 후 실제 도커 이미지를 빌드하고, 빌드가 성공하면 도커 허브로 푸시하는 작업을 수행한다.
항목 설명
uses: docker/build-push-action@v6- 도커 이미지를 빌드하고 푸시하는 공식 Docker 액션을 사용한다.
context: ./oneco- 빌드 컨텍스트를 의미한다.
- 빌드 컨텍스트는 ./oneco이고, Docker는 ./oneco 폴더를 기준으로 파일을 찾는다.
- Docker가
Dockerfile을 실행할 때 필요한 파일들(예:build/libs에 생성된.jar파일)을 어디서 찾아야 하는지 알려준다. - 내 워크플로우에서는
context: ./oneco이므로, Docker는./oneco폴더를 기준으로 파일을 찾는다. (즉oneco/build/libs/*.jar가 아니라build/libs/*.jar가 된다)
file: ./Dockerfile- 빌드에 사용할
Dockerfile의 경로를 명시적으로 지정한다. - (현재 워크플로우는 file을 생략했기 때문에,
context폴더 내부의 기본 Dockerfile을 사용)
- 빌드에 사용할
push: true- 빌드가 성공하면, 이미지를 도커 허브로 푸시하라는 명령어이다.
- 만약 이 값이 false라면, 이미지를 빌드만 하고 업로드는 하지 않는다.
tags: ${{ steps.meta.outputs.tags }}- 바로 이전 스텝(
id: meta) 이 생성한 태그 출력값을 가져와서 사용한다. - 이전 스텝에서 만든
latest와sha태그, 즉 2개의 태그가 여기에 전달된다. - (예:
gimin/oneco:latest와gimin/oneco:a1b2c3d가 모두 적용됨)
- 바로 이전 스텝(
labels: ${{ steps.meta.outputs.labels }}docker/metadata-action이 자동으로 생성해 준 라벨 출력값을 가져와서 이미지에 삽입한다.- 라벨은 이미지 내부에 저장되는 정보로, “이 이미지는 어떤 Git 커밋에서 만들어졌는지”, “어떤 레포지토리에서 왔는지” 등의 유용한 메타데이터를 포함한다.
cache-from: type=gha- 빌드를 시작하기 전에, Github Actions(gha) 캐시에서 이전 빌드 데이터를 가져와라(from)라는 뜻이다.
- Docker는 Dockerfile 레이어 캐시를 재사용한다
- Docker는 Dockerfile 각 명령의 결과(레이어)를 캐시로 재사용할 수 있어서, 동일/유사한 빌드에서는 다운로드/복사/설치 단계가 크게 단축된다.
cache-to: type=gha,mode=max- 이번 빌드에서 생성된 레이어 캐시를 GitHub Actions 캐시에 최대한 많이 저장해두겠다는 뜻이다.
- 다음 빌드에서
cache-from으로 다시 끌어와 빌드 시간을 줄인다.
5) Dockerfile 이란
- 도커 이미지를 만드는 방법을 적어둔 파일이다.
- 이 텍스트 파일(Dockerfile)안에 모든 명령어를 순서대로 적어두면, 도커가 이 파일을 읽고 그대로 실행하여 자동으로 이미지를 만들어준다.
Dockerfile 예시
# 1. 베이스 이미지 선택 (Java 17 실행 환경)
FROM openjdk:17
# 3. GitHub Actions Runner에서 빌드된 .jar 파일을
# 이미지 내부로 'app.jar'라는 이름으로 복사한다.
# [*.jar]는 파일 이름이 매번 바뀌어도(예: my-app-0.0.1.jar) 인식하게 해준다.
COPY build/libs/*.jar app.jar
# 4. 애플리케이션이 사용할 포트 번호 명시
EXPOSE 8080
# 5. 이 이미지가 실행될 때(컨테이너가 시작될 때) 실행할 명령어
# "java -jar app.jar" 명령을 실행한다.
ENTRYPOINT ["java", "-jar", "/app.jar"]
6) Gradle 캐시 vs Docker 빌드 캐시
1. Gradle 캐시 (actions/cache@v3)
- 캐시 대상:
~/.gradle/caches(스프링 부트, JUnit 등 라이브러리.jar파일) - 사용 주체: GitHub Runner (가상머신)
- 속도 향상:
run: ./gradlew build명령어 실행 속도 향상 - 동작: 이 캐시가 없으면,
run: ./gradlew build를 실행할 때마다 Gradle이 매번 수백 개의 라이브러리를 인터넷에서 새로 다운로드한다.
2. Docker 빌드 캐시(cache-from: type=gha)
- 캐시 대상:
Dockerfile의 각 명령어 실행 결과 (이미지 레이어) - 사용 주체:
docker/build-push-action(도커 엔진) - 속도 향상:
docker build명령어 실행 속도 향상 - 동작: 이 캐시가 없으면,
docker build를 실행할 때마다 Dockerfile 내에서FROM openjdk:17베이스 이미지 등을 새로 다운로드한다. - 결과: Docker 이미지 빌드 시간이 빨라진다
[CI/CD 1편] GitHub Actions + DockerHub + SSM으로 EC2 자동 배포 CI/CD 큰그림
CI/CD를 깊게 다룬 계기과거 프로젝트에서는 CI/CD가 너무 어렵게 느껴져서, 제대로 이해하지 못한 채 GPT가 제공한 워크플로우를 그대로 복사·붙여넣기 하며 구축했다. 하지만 이번 프로젝트에서
gimini.tistory.com
[CI/CD 2편] CI 빌드 파트 완전 해부: JDK 17 세팅 → Gradle 캐시 → Gradle Wrapper → build -x test ( + 트러블
이 글은 1편에서 잡은 큰그림 중, GitHub Actions의 build job을 “Runner 내부에서 실제로 어떤 일이 벌어지는지” 기준으로 풀어낸다.이번 2편에서 다루는 범위는 아래와 같다.Set up JDK 17Gradle Caching (actio
gimini.tistory.com
[CI/CD 3편] Docker 이미지 빌드/푸시 전략 완전 해부: Login → metadata(latest+sha) → buildx 캐시 → DockerHub
[CI/CD 3편] Docker 이미지 빌드/푸시 전략 완전 해부: Login → metadata(latest+sha) → buildx 캐시 → DockerHub push
gimini.tistory.com
[CI/CD 4편] SSH 없이 배포하기: GitHub Actions → AWS Credentials → SSM send-command → EC2 deploy.sh, 그리고 시
[CI/CD 4편] SSH 없이 배포하기: GitHub Actions → AWS Credentials → SSM send-command → EC2 deploy.sh, 그리고 시크릿 주입
gimini.tistory.com