| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 파이프라인
- 나무섭지
- gitlab
- 나무 조경
- IAC
- cloud
- 도커
- Terraform
- aws ecs
- 백준
- gitlab runner
- 자동화
- 자격증
- Saa
- 배포
- softeer
- 자바
- 소프티어
- 코딩테스트
- ECS
- dfs
- CI/CD
- AWS
- phi squared
- resultMap
- docker
- spring boot
- Java
- Cloud Engineer
- DP
- Today
- Total
성장하는 개발자의 블로그
[Docker, GitLab] - CI/CD 파이프라인 구축하기 5 (Dockerfile, gitlab-ci 파일 설정) 본문
[Docker, GitLab] - CI/CD 파이프라인 구축하기 5 (Dockerfile, gitlab-ci 파일 설정)
zxzc1297 2025. 7. 24. 21:25안녕하세요. 지난 글들에서는 ECS 인프라를 구축했고 AWS 콘솔을 통해 수동으로 애플리케이션을 배포해보았습니다. 이 과정들을 통해 우리는 ECS가 어떻게 동작하는지 충분히 이해했습니다.
오늘은 이 모든 수동 과정을 코드로 자동화하여, git push 명령어 하나만으로 빌드부터 배포까지 자동으로 이루어지도록 만드는 Dockerfile과 .gitlab-ci.yml 작성법을 다루겠습니다. 드디어 CI/CD 파이프라인의 심장을 만드는 단계입니다.
1. Dockerfile 작성: 애플리케이션 이미지 설계도
가장 먼저, 제 애플리케이션을 어떤 환경에서 어떻게 실행할지 정의하는 텍스트 파일, 즉 이미지의 설계도인 Dockerfile을 프로젝트 최상단에 생성해야 합니다. 이 파일이 있어야 GitLab Runner가 이미지를 빌드할 수 있습니다.
저는 간단한 Node.js 애플리케이션을 예시로 Dockerfile을 작성했습니다.
# 1. 기본 이미지 선택
# 현재 사용하시는 Spring Boot 버전과 호환되는 Java 버전을 가진 이미지를 선택합니다.
# 예: Spring Boot 3.x 와 Java 17을 사용하신다면
FROM openjdk:17-jdk-slim
# 타임존 설정 (한국 시간) AWS에서 해당 설정을 하지 않으면 타임존 관련한 사항들에 오류 발생가능성 있음
ENV TZ=Asia/Seoul
RUN apt-get update && \
apt-get install -y tzdata && \
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
echo $TZ > /etc/timezone && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# 2. (선택 사항) 컨테이너 내에서 사용할 환경 변수 기본값 설정
# 이 값들은 ECS 작업 정의에서 덮어쓸 수 있습니다.
ENV APP_HOME="/home/ec2-user"
ENV SPRING_PROFILES_ACTIVE="prod"
# 컨테이너 내부의 로그 경로
ENV LOGGING_FILE_PATH="${APP_HOME}"
# ENV JAVA_OPTS="" # 기타 필요한 JVM 옵션이 있다면
# 3. 작업 디렉터리 설정
WORKDIR ${APP_HOME}
# 4. 빌드된 JAR 파일을 이미지 안으로 복사
# GitLab CI/CD 파이프라인에서 빌드된 JAR 파일이 이 Dockerfile과 같은 위치(빌드 컨텍스트)에
# .gitlab-ci.yml의 build_app 작업에서 artifacts로 생성된 JAR 파일명을 기반으로 합니다.
# ARG 지시어는 docker build 시 --build-arg 옵션으로 값을 전달받을 때 사용합니다.
# CI 파이프라인에서는 보통 빌드된 아티팩트의 정확한 경로를 지정합니다.
ARG JAR_FILE_PATH=build/libs/*-SNAPSHOT.jar
COPY ${JAR_FILE_PATH} user.jar
# 5. (선택 사항) 로그 폴더 생성 및 권한 설정
# 컨테이너 내부에서 애플리케이션이 로그 파일을 생성할 경우 필요할 수 있습니다.
# 애플리케이션을 실행할 사용자/그룹에 맞게 권한을 설정합니다 (예: openjdk 이미지는 보통 root가 아님).
# RUN mkdir -p ${LOGGING_FILE_PATH} && \
# chown -R 1001:0 ${LOGGING_FILE_PATH} && \
# chmod -R g+w ${LOGGING_FILE_PATH}
# (chown의 UID/GID는 기본 이미지의 실행 사용자에 따라 달라질 수 있습니다.
# 보통 Spring Boot 앱은 특별한 파일 쓰기 권한이 필요 없다면 이 단계를 생략하고
# 표준 출력(STDOUT/STDERR)으로 로그를 보내고 ECS 로그 드라이버(awslogs)로 수집합니다.)
# 6. 애플리케이션 실행 포트 노출 (선택 사항, 정보 제공 목적)
# EXPOSE 8080 # Spring Boot 기본 포트 (실제 포트 매핑은 ECS 작업 정의에서)
# 7. 컨테이너가 시작될 때 실행될 명령어 정의
# 이전 systemd에서 사용한 옵션들을 여기서 환경 변수를 참조하여 설정합니다.
# 최종 실행 옵션은 ECS 작업 정의의 환경 변수 설정으로 덮어쓸 수 있습니다.
ENTRYPOINT ["java", "-Dlogging.file.path=${LOGGING_FILE_PATH}", "-jar", "test.jar", "--spring.profiles.active=${SPRING_PROFILES_ACTIVE}"]
# 또는 쉘을 통해 JAVA_OPTS 같은 추가적인 환경변수를 사용하고 싶다면:
# ENTRYPOINT ["sh", "-c", "exec java ${JAVA_OPTS} -Dlogging.file.path=${LOGGING_FILE_PATH} -jar application.jar --spring.profiles.active=${SPRING_PROFILES_ACTIVE}"]
2. GitLab CI/CD 변수 설정: 민감 정보 안전하게 관리하기
파이프라인이 실행될 때, AWS에 접근하기 위한 자격 증명(Access Key)이나 ECR 주소 같은 민감하거나 자주 바뀌는 정보들이 필요합니다. 이런 정보들을 .gitlab-ci.yml 파일에 직접 하드코딩하는 것은 보안상 매우 위험합니다.
대신, GitLab의 CI/CD 변수 기능을 사용하여 안전하게 관리해야 합니다.
[이동 경로] 프로젝트 홈 → Settings → CI/CD → Variables 섹션 → Expand → Add variable

아래 변수들을 미리 등록해두어야 합니다. Value에는 각자 자신의 환경에 맞는 값을 입력합니다.
- AWS_ACCESS_KEY_ID: AWS IAM 사용자의 액세스 키 ID
- AWS_SECRET_ACCESS_KEY: 해당 액세스 키의 시크릿 키 (Masked 처리 권장)
- AWS_DEFAULT_REGION: 사용 중인 AWS 리전 (예: ap-northeast-2)
- ECR_REGISTRY: ECR 리포지토리의 URI (예: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com)
- ECS_CLUSTER_NAME: 1부에서 생성한 ECS 클러스터의 이름
- ECS_SERVICE_NAME: 2부에서 생성한 ECS 서비스의 이름
- ECS_TASK_DEFINITION_NAME: 2부에서 생성한 태스크 정의 패밀리 이름 (예: my-app-task)
3. .gitlab-ci.yml 작성: 자동화의 오케스트라
이제 모든 준비가 끝났습니다. 프로젝트 최상단에 .gitlab-ci.yml 파일을 생성하고, 빌드부터 배포까지의 모든 과정을 오케스트라처럼 지휘할 스크립트를 작성하겠습니다.
# .gitlab-ci.yml
# 변수 할당
variables:
AWS_DEFAULT_REGION: "ap-northeast-2"
#AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID_VAR
#AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY_VAR
# ECR 설정
ECR_REGISTRY_URL: "$ECR_REGISTRY_URL_VAR"
ECR_REPOSITORY_NAME: "$ECR_REPOSITORY_NAME_VAR" # ECR에 생성한 리포지토리 이름
IMAGE_TAG: $CI_COMMIT_SHORT_SHA # 커밋 해시를 이미지 태그로 사용
# ECS 설정
ECS_CLUSTER_NAME: "$ECS_CLUSTER_NAME_VAR" # ECS 클러스터 이름
ECS_SERVICE_NAME: "$ECS_SERVICE_NAME_VAR" # 업데이트할 ECS 서비스 이름
ECS_CONTAINER_NAME: "$ECS_CONTAINER_NAME_VAR" # 업데이트할 컨테이너 이름
ECS_TASK_DEFINITION_FAMILY: "$ECS_TASK_DEFINITION_FAMILY_VAR" # 태스트 정의 이름름
# 파이프라인의 단계(순서)를 정의합니다.
stages:
- build_app # Spring Boot 애플리케이션 빌드
- build_and_push_image # Docker 이미지 빌드 및 ECR 푸시
- deploy_to_ecs # ECS 서비스 업데이트
# 파이프라인 전체에 대한 실행 규칙: main 브랜치에서 변경이 있을 때만 실행
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
# Spring Boot 애플리케이션을 빌드하는 작업
build_spring_boot_application:
tags:
- my-runner
stage: build_app
image: gradle:8.5-jdk17
before_script:
# Debian 기반이므로 apt 사용
- apt-get update && apt-get install -y awscli
- echo "Downloading application.yml from S3..."
# (수정됨) YAML 파일을 저장할 디렉토리를 먼저 생성하여 오류 방지
- mkdir -p src/main/resources
- aws s3 cp "$S3_PATH" src/main/resources/application.yml
- echo "application.yml downloaded."
script:
- echo "Spring Boot 애플리케이션 빌드를 시작합니다..."
# ./gradlew 파일에 실행 권한 부여
- chmod +x ./gradlew
- ./gradlew clean build -x test
- echo "빌드 완료."
artifacts:
# paths:
paths:
- build/libs/*.jar
exclude:
- build/libs/*-plain.jar # -plain.jar 파일을 제외
expire_in: 4 hour
# Docker 이미지를 빌드하고 ECR에 푸시하는 작업
build_and_push_docker_image:
tags:
- my-runner
stage: build_and_push_image
image: docker:latest
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_TLS_CERTDIR: ""
before_script:
- apk add --no-cache aws-cli
- docker run --rm --privileged tonistiigi/binfmt --install all # 멀티 아키텍처 빌드 환경 활성화
- docker buildx create --use --name mybuilder
- docker buildx inspect --bootstrap
- echo "ECR에 로그인합니다..."
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY_URL
- echo "ECR 로그인 성공."
script:
- FULL_IMAGE_NAME="$ECR_REGISTRY_URL/$ECR_REPOSITORY_NAME:$IMAGE_TAG"
- LATEST_IMAGE_NAME="$ECR_REGISTRY_URL/$ECR_REPOSITORY_NAME:latest"
- echo "Docker 이미지를 빌드하고 푸시합니다. 아키텍처가 달라 바로 푸시"
- docker buildx build --platform linux/amd64 -t $FULL_IMAGE_NAME -t $LATEST_IMAGE_NAME --push .
- echo "ECR 푸시 완료."
dependencies:
- build_spring_boot_application
# ECS 서비스를 업데이트하여 새 이미지를 배포하는 작업 (디버깅 강화 버전)
deploy_to_ecs_service:
tags:
- my-runner
stage: deploy_to_ecs
image: amazonlinux:2
before_script:
- yum update -y
- yum install -y unzip curl jq
- curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip"
- unzip awscliv2.zip
- ./aws/install
- aws --version
script:
- |
echo "===== [DEBUG] Environment Variables ====="
echo "AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION"
echo "ECS_CLUSTER_NAME: $ECS_CLUSTER_NAME"
echo "ECS_SERVICE_NAME: $ECS_SERVICE_NAME"
echo "ECS_TASK_DEFINITION_FAMILY: $ECS_TASK_DEFINITION_FAMILY"
echo "ECS_CONTAINER_NAME: $ECS_CONTAINER_NAME"
echo "ECR_REGISTRY_URL: $ECR_REGISTRY_URL"
echo "ECR_REPOSITORY_NAME: $ECR_REPOSITORY_NAME"
echo "IMAGE_TAG: $IMAGE_TAG"
echo "=========================================="
- | # 스크립트 블록 시작
echo "Updating ECS service..."
FULL_IMAGE_NAME="$ECR_REGISTRY_URL/$ECR_REPOSITORY_NAME:$IMAGE_TAG"
# 1. 기존 작업 정의를 가져옵니다.
echo "--- [DEBUG] Retrieving current task definition ---"
TASK_DEFINITION_JSON=$(aws ecs describe-task-definition --task-definition $ECS_TASK_DEFINITION_FAMILY)
echo "$TASK_DEFINITION_JSON" | jq . # 가져온 JSON 내용 확인
# 2. 컨테이너 이미지 경로만 수정한 새로운 컨테이너 정의 JSON 생성
echo "--- [DEBUG] Creating new container definitions ---"
NEW_CONTAINER_DEFINITIONS=$(echo $TASK_DEFINITION_JSON | jq --arg IMAGE_URI "$FULL_IMAGE_NAME" --arg CONTAINER_NAME "$ECS_CONTAINER_NAME" '(.taskDefinition.containerDefinitions[] | select(.name==$CONTAINER_NAME).image) |= $IMAGE_URI | .taskDefinition.containerDefinitions')
echo "$NEW_CONTAINER_DEFINITIONS" | jq . # 수정된 컨테이너 정의 확인
# 3. 새로운 작업 정의를 등록하기 위한 입력 JSON 생성
echo "--- [DEBUG] Preparing new task definition input JSON ---"
TASK_DEF_INPUT_JSON=$(echo "$TASK_DEFINITION_JSON" | jq \
--argjson containerDefinitions "$NEW_CONTAINER_DEFINITIONS" \
'{
family: .taskDefinition.family,
networkMode: .taskDefinition.networkMode,
requiresCompatibilities: .taskDefinition.requiresCompatibilities,
cpu: .taskDefinition.cpu,
memory: .taskDefinition.memory,
volumes: .taskDefinition.volumes,
placementConstraints: .taskDefinition.placementConstraints,
containerDefinitions: $containerDefinitions
}
+ (if .taskDefinition.taskRoleArn != null then {taskRoleArn: .taskDefinition.taskRoleArn} else {} end)
+ (if .taskDefinition.executionRoleArn != null then {executionRoleArn: .taskDefinition.executionRoleArn} else {} end)'
)
echo "$TASK_DEF_INPUT_JSON" | jq . # 최종적으로 register-task-definition에 전달될 JSON 확인
# 4. 새로운 작업 정의 리비전 등록
echo "Registering new task definition revision..."
NEW_TASK_DEF_ARN=$(aws ecs register-task-definition --cli-input-json "$TASK_DEF_INPUT_JSON" | jq -r '.taskDefinition.taskDefinitionArn')
echo "New task definition ARN: $NEW_TASK_DEF_ARN"
# 5. ECS 서비스 업데이트
echo "Sending update command to ECS service..."
aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --task-definition "$NEW_TASK_DEF_ARN" --force-new-deployment
# 6. 배포 안정화 대기
echo "Waiting for deployment to become stable..."
aws ecs wait services-stable --cluster $ECS_CLUSTER_NAME --services $ECS_SERVICE_NAME
echo "Deployment has stabilized successfully!"
needs:
- job: build_and_push_docker_image
중요한 점: 이 파이프라인을 실행하는 GitLab Runner(EC2 인스턴스)는 ECR과 ECS를 제어할 수 있는 IAM 권한을 가지고 있어야 합니다. (예: AmazonEC2ContainerRegistryPowerUser, AmazonECS_FullAccess 정책이 연결된 IAM 역할)
4. 파이프라인 실행 및 확인
이제 이 두 파일(Dockerfile, .gitlab-ci.yml)을 프로젝트에 추가하고 git commit 후 git push를 실행합니다.
GitLab 프로젝트 → Build → Pipelines
로 이동하면, 방금 푸시한 커밋으로 인해 새로운 파이프라인이 실행되는 것을 볼 수 있습니다. build → push → deploy 단계가 차례로 실행되며 모두 초록색 체크 표시와 함께 성공적으로 완료되어야 합니다.

AWS ECS 콘솔의 서비스 화면에 가보면, 새로운 배포가 진행 중인 것을 확인할 수 있으며, 잠시 후 새로운 버전의 태스크가 실행되는 것을 볼 수 있습니다.
마무리하며
이제 우리는 코드를 git push 하기만 하면, GitLab Runner가 자동으로 이미지를 빌드하고, ECR에 푸시하며, ECS 서비스까지 업데이트하는 완전한 자동화 파이프라인을 갖추게 되었습니다. 수동으로 하던 모든 작업이 이제 코드와 시스템에 의해 관리됩니다.
드디어 CI/CD의 꽃인 자동 배포 환경이 완성되었습니다. 다음은 이렇게 배포된 애플리케이션의 로그를 실시간으로 확인하는 모니터링 환경을 구축하고, 지금까지 만든 전체 아키텍처를 정리하며 시리즈를 마무리하겠습니다. 그 과정도 기대해주시기 바랍니다.
'CI_CD' 카테고리의 다른 글
| [AWS CloudWatch] - CI/CD 파이프라인 구축하기 6 (AWS CloudWatch) (0) | 2025.07.29 |
|---|---|
| [AWS ECS] - CI/CD 파이프라인 구축하기 4 (AWS ECS 설정) (0) | 2025.07.16 |
| [AWS ECS] - CI/CD 파이프라인 구축하기 3 (AWS ECS 설정) (0) | 2025.07.08 |
| [AWS ECR] - CI/CD 파이프라인 구축하기 2 (AWS ECR 생성과 Docker 이미지의 구조 파헤치기) (0) | 2025.06.30 |
| [GitLab] - CI/CD 파이프라인 구축하기 1 (Gitlab Runner 설치) (0) | 2025.06.23 |