Post

[허수아비] CI/CD 배포 트러블슈팅 모음

GitLab CI 파이프라인 구성부터 nginx 설정 오류, 환경변수 불일치, Spark Master URL까지 — 배포 과정에서 마주친 실전 트러블슈팅 기록입니다.

[허수아비] CI/CD 배포 트러블슈팅 모음

배포 파이프라인을 처음 구성하면서 크고 작은 트러블이 연속으로 터졌습니다. 오류 메시지만 보고 바로 고치기보다, 왜 그 오류가 나는지를 먼저 짚고 선택지를 비교한 뒤 해결하는 방식으로 접근했습니다. 그 과정을 항목별로 정리했습니다.


deploy 잡이 build 잡을 찾지 못해 파이프라인이 실패했다

증상

dev 브랜치에 머지 후 파이프라인 실행 시 아래 오류가 발생했습니다.

1
2
'deploy-frontend' job needs 'build-frontend' job,
but 'build-frontend' does not exist in the pipeline.

왜 이런 일이 생겼나

build-* 잡은 rules: changes: 조건으로 해당 컴포넌트에 변경이 있을 때만 실행되도록 설계되어 있었습니다. 반면 deploy-* 잡은 if: branch == dev/main 조건만 있어서, 파일 변경 여부와 관계없이 항상 실행을 시도했습니다.

변경이 없는 컴포넌트의 build-* 잡은 파이프라인에 아예 존재하지 않기 때문에, needs:로 그 잡을 참조하는 deploy-*가 실패하는 구조였습니다.

어떤 선택지를 고려했나

두 가지 방향을 검토했습니다.

  • optional: true 추가: build 잡이 없어도 deploy 잡이 실패하지 않도록 의존성을 느슨하게 만드는 방법. 단, build가 실제로 실패한 경우에도 deploy가 실행될 수 있다는 위험이 있습니다.
  • changes: 조건 추가: build 잡과 동일한 파일 변경 시에만 deploy 잡도 실행되도록 맞추는 방법. 아예 파이프라인에 올라오지 않으니 needs: 참조 문제 자체가 사라집니다.

두 방법을 함께 적용했습니다. optional: true는 예외 상황에 대한 안전망이고, changes: 조건은 애초에 잡이 불필요하게 실행되는 것을 막아줍니다.

해결

1
2
3
4
5
6
7
8
9
deploy-frontend:
  needs:
    - job: build-frontend
      optional: true
  rules:
    - if: '$CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "main"'
      changes:
        - frontend/**/*
        - .gitlab-ci.yml

nginx가 server 블록을 최상위에서 거부했다

증상

birdybuddy-frontend 컨테이너가 재시작을 반복하며 아래 로그를 출력했습니다.

1
2
3
2026/03/20 07:28:18 [emerg] 1#1: "server" directive is not allowed here
in /etc/nginx/nginx.conf:1
nginx: [emerg] "server" directive is not allowed here in /etc/nginx/nginx.conf:1

왜 이런 일이 생겼나

nginx 공식 이미지의 설정 구조는 http {} 블록 안에 server {} 블록이 위치해야 합니다. 작성한 nginx.confserver {}를 최상위에 바로 선언한 상태였기 때문에, nginx가 컨텍스트 오류로 시작을 거부했습니다.

해결

nginx.confevents {}http {} 블록을 추가하고, server {} 블록을 그 안으로 이동했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
events {}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        listen 80;
        ...
    }

    server {
        listen 443 ssl;
        ...
    }
}

nginx.conf 마운트 경로가 파일이 아닌 디렉토리로 생성되어 있었다

증상

deploy-frontend CI 잡 실행 중 아래 오류가 발생했습니다.

1
2
3
4
5
6
7
Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: runc create failed:
unable to start container process: error during container init:
error mounting "/home/ubuntu/birdybuddy/infra/ec2-app/nginx/nginx.conf"
to rootfs at "/etc/nginx/nginx.conf":
mount src=..., flags=MS_BIND|MS_REC: not a directory:
Are you trying to mount a directory onto a file (or vice-versa)?

왜 이런 일이 생겼나

EC2에 nginx/nginx.conf 파일이 존재하지 않는 상태에서 docker compose up을 실행하면, Docker가 마운트 대상 경로를 파일이 아닌 디렉토리로 자동 생성합니다. 이후 파일로 마운트를 시도할 때 타입 불일치로 실패하는 구조입니다.

어떤 선택지를 고려했나

  • EC2에서 수동으로 디렉토리를 제거하고 재실행: 즉시 해결되지만, 다음 배포에서도 같은 상황이 반복될 수 있습니다.
  • CI에서 docker compose up 전에 파일을 먼저 전송: 파일이 미리 존재하면 Docker가 디렉토리를 만들 이유가 없습니다. 재현 가능한 구조로 해결할 수 있습니다.

CI 파이프라인 자체를 고치는 두 번째 방법을 선택했습니다. 수동 조치는 임시방편이고, 배포 순서를 올바르게 정의하는 것이 근본 해결이라고 판단했습니다.

해결

CI 파이프라인에서 docker compose up 전에 scpnginx.conf를 EC2에 먼저 전송하도록 수정했습니다.

1
2
3
4
script:
  - ssh $EC2_USER@$EC2_APP_HOST "mkdir -p $DEPLOY_DIR/infra/ec2-app/nginx"
  - scp infra/ec2-app/nginx/nginx.conf $EC2_USER@$EC2_APP_HOST:$DEPLOY_DIR/infra/ec2-app/nginx/nginx.conf
  - ssh $EC2_USER@$EC2_APP_HOST "docker compose up -d --no-deps frontend"

이미 디렉토리로 생성되어 있는 경우, EC2에서 수동으로 제거 후 파이프라인을 재실행합니다.

1
rm -rf ~/birdybuddy/infra/ec2-app/nginx

scp로 nginx.conf를 전송하려는데 Permission denied가 났다

증상

1
2
3
scp: dest open "birdybuddy/infra/ec2-app/nginx/nginx.conf/nginx.conf": Permission denied
scp: failed to upload file infra/ec2-app/nginx/nginx.conf
to ~/birdybuddy/infra/ec2-app/nginx/nginx.conf

왜 이런 일이 생겼나

이전 배포 실패로 EC2에 nginx/nginx.conf디렉토리로 남아있었습니다. scp가 그 안에 nginx.conf라는 이름의 파일을 새로 만들려 했지만, 해당 경로에 쓰기 권한이 없어 실패했습니다. 앞선 항목과 연결된 문제로, 디렉토리를 제거하지 않은 채 scp를 재시도해서 발생한 상황이었습니다.

해결

EC2에서 잘못 생성된 디렉토리를 제거하고 CI를 재실행했습니다.

1
rm -rf ~/birdybuddy/infra/ec2-app/nginx

백엔드가 DB에 접속하지 못하고 인증 실패를 반복했다

증상

birdybuddy-backend 컨테이너가 재시작을 반복하며 아래 로그를 출력했습니다.

1
2
Caused by: org.postgresql.util.PSQLException:
FATAL: password authentication failed for user "admin"

왜 이런 일이 생겼나

GitLab CI 변수 ENV_BACKEND에 설정된 POSTGRES_PASSWORD가 PostgreSQL 컨테이너 초기화 시 사용된 비밀번호와 달랐습니다.

PostgreSQL은 최초 초기화 시 비밀번호를 볼륨에 저장합니다. 이후 환경변수를 바꿔도 기존 볼륨이 남아있으면 변경 내용이 반영되지 않습니다. 즉, ENV_BACKENDENV_EC2_APP의 비밀번호가 맞지 않는 상태에서 컨테이너를 재시작해봤자 볼륨에 저장된 원래 비밀번호로 계속 인증이 시도됩니다.

어떤 선택지를 고려했나

  • 비밀번호만 맞추고 컨테이너 재시작: 볼륨이 남아있으면 변경이 반영되지 않으므로 효과가 없습니다.
  • 볼륨을 삭제하고 재초기화: 데이터가 날아가지만 개발 환경에서는 허용 가능하고, 비밀번호를 올바르게 맞춘 상태로 처음부터 초기화할 수 있습니다.

볼륨 삭제 후 재초기화 방법을 선택했습니다. 환경변수 불일치 + 기존 볼륨 잔존이라는 두 조건이 겹친 문제이므로, 두 조건을 동시에 해소해야 했습니다.

해결

ENV_BACKENDPOSTGRES_PASSWORDENV_EC2_APP의 값과 일치시킨 뒤, 기존 볼륨을 삭제하고 재시작했습니다.

1
2
3
cd ~/birdybuddy/infra/ec2-app
docker compose down -v   # 볼륨까지 삭제 (데이터 초기화 주의)
docker compose up -d

AI 컨테이너가 MinIO 호스트명을 해석하지 못했다

증상

1
2
3
urllib3.exceptions.NameResolutionError:
HTTPConnection(host='minio', port=9000): Failed to resolve 'minio'
([Errno -3] Temporary failure in name resolution)

왜 이런 일이 생겼나

ENV_AIMINIO_ENDPOINT=http://minio:9000으로 설정되어 있었습니다. minio는 EC2 #2의 Docker 내부 호스트명입니다. EC2 #1에서 동작하는 AI 컨테이너는 다른 호스트의 Docker 네트워크를 알 수 없으므로, 이 이름을 해석하지 못합니다.

단일 EC2 환경이었다면 docker network를 통해 접근할 수 있었겠지만, 멀티 EC2 구성에서는 내부 호스트명이 아닌 외부 주소로 통신해야 합니다.

해결

ENV_AI에서 EC2 #2의 외부 주소로 변경했습니다.

1
MINIO_ENDPOINT=http://j14A206A.p.ssafy.io:9000

MinIO Access Key가 맞지 않아 업로드가 실패했다

증상

1
2
3
[ERROR] MinIO 업로드 실패: S3 operation failed;
code: InvalidAccessKeyId,
message: The Access Key Id you provided does not exist in our records.

왜 이런 일이 생겼나

ENV_AIMINIO_ACCESS_KEY, MINIO_SECRET_KEY가 MinIO 컨테이너에 설정된 MINIO_ROOT_USER, MINIO_ROOT_PASSWORD와 달랐습니다.

MinIO에서 MINIO_ROOT_USER가 Access Key, MINIO_ROOT_PASSWORD가 Secret Key에 해당합니다. 환경변수 이름이 달라 헷갈리기 쉬운 부분인데, 각 서비스의 .env 파일이 독립적으로 관리되다 보니 한쪽만 수정된 채로 배포된 상황이었습니다.

해결

ENV_AI의 값을 ENV_EC2_APP의 MinIO 설정과 일치시켰습니다.

1
2
MINIO_ACCESS_KEY={MINIO_ROOT_USER 값}
MINIO_SECRET_KEY={MINIO_ROOT_PASSWORD 값}

Spark Master URL 형식이 잘못되어 있었다

증상

birdybuddy-data-pipeline 컨테이너가 재시작을 반복하며 아래 로그를 출력했습니다.

1
2
Exception in thread "main" org.apache.spark.SparkException:
Master must either be yarn or start with spark, k8s, or local

왜 이런 일이 생겼나

ENV_DATA_PIPELINESPARK_MASTER 값이 Spark가 인식하지 못하는 형식으로 설정되어 있었습니다. Spark가 허용하는 Master URL 형식은 다음과 같습니다.

  • spark://host:port — Standalone 클러스터
  • local[*] — 로컬 실행
  • yarn — YARN 클러스터
  • k8s://... — Kubernetes

오류 메시지 자체가 허용 형식을 명확히 알려주고 있었고, 이 프로젝트는 Spark Standalone 구성이므로 spark:// 형식을 사용해야 했습니다.

해결

ENV_DATA_PIPELINE에서 올바른 형식으로 수정했습니다.

1
SPARK_MASTER=spark://spark-master:7077
This post is licensed under CC BY 4.0 by the author.