Resolving GitLab MR Conflicts from Concurrent Merge Requests

개요

이 글은 GitLab MR 충돌이 발생하는 대표적인 상황과 안전한 해결 절차를 정리한 운영 기록입니다. 같은 main에서 갈라진 두 개의 Merge Request가 동일 파일의 인접한 영역을 각각 수정하고 있을 때, 한쪽 MR이 먼저 병합되면 나머지 MR이 갑자기 병합 불가(blocked) 상태로 바뀝니다. 처음에는 문제없이 병합 가능하던 MR이라 원인이 바로 보이지 않는 경우가 많습니다.

이번 사례에서는 두 MR이 같은 설정 파일의 동일 구획에 각각 항목을 추가한 탓에, 먼저 병합된 MR로 인해 뒤에 남은 MR에서 충돌이 드러났습니다. 진단 과정에서 git merge-tree가 충돌을 과소보고하는 함정도 함께 확인했으므로, 재현·진단·해결·검증 순서로 기록합니다.

환경

  • Self-managed GitLab (merge commit 방식, MR 파이프라인의 validate 단계 사용)
  • Git 2.40+ (git merge-tree --write-tree 지원)
  • 대상 파일: 여러 항목을 하나의 목록/딕셔너리에 누적하는 설정 파일 1개 (본문에서는 config-registry.py로 일반화)
  • 브랜치: feature/a, feature/b — 동일 main 커밋에서 분기

단계별 절차

1. 상황 이해: 형제 MR이 같은 영역을 수정

feature/afeature/b는 각각 별도 MR로 열려 있었고, 두 브랜치 모두 같은 설정 파일의 같은 구획(예: 목록의 특정 지점)에 서로 다른 항목을 추가했습니다. 두 MR이 열린 시점에는 각각 main에 대해 병합 가능한 상태였습니다.

feature/a가 먼저 병합되면 main이 갱신되고, 아직 열려 있던 feature/b는 그 갱신된 main 기준으로 다시 3-way 병합을 시도하게 됩니다. 이때 같은 위치에 대한 변경이 겹치면서 feature/b가 충돌·블록 상태가 됩니다.

2. 원인 진단

먼저 원격을 갱신하고 main이 그사이 얼마나 움직였는지, 남은 브랜치가 얼마나 뒤처졌는지 확인합니다.

git fetch origin --prune

# 남은 브랜치가 main 대비 얼마나 뒤처졌는지(behind) 확인
git rev-list --left-right --count origin/main...origin/feature/b
# 출력 예: 3    4   (main-only 3, branch-only 4)

여기서 중요한 함정이 있습니다. git merge-tree로 미리 충돌을 확인하면 “충돌 없음”으로 나오는데도, 실제 git merge에서는 충돌이 발생하는 경우가 있습니다. 따라서 진단은 실제 병합으로 검증해야 합니다.

# merge-tree는 충돌을 과소보고할 수 있음(참고용)
git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict || echo "clean?"

3. 최신 main 병합 및 충돌 해결

남은 브랜치를 최신 main과 다시 맞추기 위해 main을 브랜치로 병합합니다. force-push나 히스토리 재작성 없이 병합 커밋으로 해결하는 편이 안전합니다.

git switch feature/b
git merge origin/main --no-edit
# CONFLICT (content): Merge conflict in config-registry.py

두 브랜치가 모두 “항목 추가”만 했다면, 해결의 원칙은 양쪽 추가분을 모두 보존(union)하는 것입니다. 다만 Git이 공통 boilerplate 줄(닫는 괄호, 공통 키 등)을 기준으로 정렬하면서 서로 다른 항목이 조각조각 뒤섞이는 경우가 있어, 충돌 마커만 기계적으로 지우면 항목이 오염될 수 있습니다. 그래서 마커 제거 대신, main 버전을 기준으로 두고 남은 브랜치가 추가한 블록만 통째로 삽입하는 방식으로 재구성했습니다.

# main 버전(먼저 병합된 항목 포함)을 기준으로 확보
git show origin/main:config-registry.py > /tmp/base.py

# 남은 브랜치가 추가한 블록만 추출해 base의 안정적 anchor 앞에 삽입
#  → 스크립트로 블록을 잘라 붙이면 손으로 마커를 지우는 것보다 안전합니다

트러블슈팅

정상 병합 가능하던 MR이 갑자기 블록됨

현상: 생성 시점에는 병합 가능하던 MR이, 별다른 변경 없이 갑자기 “merge blocked / conflict” 상태가 됨.

원인: 같은 파일의 같은 구획을 수정한 형제 MR이 먼저 병합되어 main이 갱신됨. 남은 MR의 병합 기준(base)이 바뀌면서 충돌이 새로 발생.

해결: 남은 브랜치에 최신 main을 병합(또는 rebase)해 다시 최신 상태로 맞춘 뒤 충돌을 해결하고 push. 병합 커밋 방식이면 rebase·force-push 없이 처리할 수 있습니다.

merge-tree는 clean인데 실제 merge에서 충돌

현상: git merge-tree --write-tree로는 충돌이 안 보였는데 실제 git merge에서 충돌 발생.

원인: merge-tree의 사전 판정이 실제 3-way 병합과 항상 일치하지는 않아, 특정 정렬 상황에서 충돌을 과소보고할 수 있음.

해결: 사전 점검은 참고로만 쓰고, 실제 git merge 결과로 최종 판정합니다. 실제 병합이 충돌을 정확히 드러냅니다.

충돌 마커가 서로 다른 항목을 뒤섞음

현상: 충돌 구간에서 <<<<<<</=======/>>>>>>> 마커가 한 항목의 일부와 다른 항목의 일부를 뒤섞어 놓음.

원인: 두 브랜치가 같은 위치에 각각 완결된 블록을 추가했지만, Git이 공통 boilerplate 줄을 기준으로 hunk를 정렬하면서 블록 경계가 깨짐.

해결: 마커를 손으로 지우지 말고, 한쪽(예: main) 전체를 기준으로 두고 다른 쪽이 추가한 블록만 통째로 삽입해 재구성. 이후 반드시 자동 검증(구문 컴파일, 중복 키 검사, 기대 항목 존재 여부)으로 union이 정확한지 확인합니다.

결과 확인

재구성한 파일이 구조적으로 올바른지 먼저 검증하고, 그다음 브랜치가 실제로 충돌 없이 병합 가능한지 확인했습니다.

# 1) 구문/구조 검증 (예: Python 파일)
python3 -m py_compile config-registry.py && echo "compile OK"

# 2) 병합 커밋 후, 충돌 없음 + 최신 main 포함(up-to-date) 확인
git merge-tree --write-tree origin/main origin/feature/b | grep -i conflict \
  && echo "conflict" || echo "clean"
git merge-base --is-ancestor origin/main origin/feature/b \
  && echo "up-to-date (not behind)"

충돌 마커가 0개이고, 재구성한 파일이 컴파일되며 양쪽 항목이 모두 남아 있고, merge-tree가 clean이면 MR 블록이 해소됩니다. 결국 GitLab MR 충돌의 핵심 예방책은, 같은 파일의 같은 구획을 여러 MR이 동시에 건드리지 않도록 작업을 분리하고, 형제 MR이 먼저 병합되면 남은 MR에 main을 다시 병합해 최신 상태로 유지하는 것입니다.

참고

관련 포스트:

참고 문서: git-merge · git-merge-tree · GitLab: Merge conflicts

Migrating WordPress from a Standalone LAMP Server to Kubernetes with Traefik

개요

WordPress Kubernetes 마이그레이션을 진행하여 레거시 CentOS 7 + Apache + PHP 7.4 단일 서버에서 운영하던 WordPress를 자체 관리형 Kubernetes 클러스터로 이전했습니다. 대상 서버는 두 개 도메인을 서빙하고 있었고, DB는 이미 클러스터 내 MariaDB를 외부 LoadBalancer IP로 접속하고 있어 데이터베이스 자체는 새로 구축할 필요 없이 접속 경로만 전환하면 되는 상태였습니다.

이번 작업에서는 ① 진입점을 Traefik Ingress로 전환 ② MariaDB 접속을 ClusterIP로 전환 ③ 다중 파드 확장을 고려해 Rook CephFS(ReadWriteMany) 볼륨 채택 ④ PHP 7.4(EOL)에서 PHP 8.4로 업그레이드, 이 네 가지를 목표로 설계했습니다. 로컬 환경에 docker/podman이 없어 커스텀 이미지를 클러스터 안에서 직접 빌드해야 했고, 그 과정에서 GitLab Container Registry를 신규로 활성화하는 작업도 함께 진행했습니다.

환경

항목 기존 (192.168.x.x 레거시 서버) 신규 (Kubernetes)
OS / 웹서버 / PHP CentOS 7, Apache 2.4.6 (mod_php, mpm_prefork), PHP 7.4.33 Debian(컨테이너), Apache 2.4.68, PHP 8.4.23
진입점 HAProxy가 TLS 종료 후 백엔드로 프록시 Traefik Ingress (websecure 443)
DB 연결 MariaDB LoadBalancer 외부 IP MariaDB ClusterIP (Service DNS)
스토리지 로컬 디스크 단일 서버 Rook CephFS RWX PVC 5Gi, 파드 2개 공유
메일 발송 로컬 Postfix (direct-send) 이미지 내 Postfix 동일 구성
이미지 GitLab Container Registry + Kaniko 빌드

단계별 절차

1. 소스 서버 조사 및 이관 설계

SSH로 레거시 서버에 직접 접속해 vhost 설정, wp-config.php, 활성 플러그인 목록, PHP 설정을 조사했습니다. DB 접속 정보(DB_HOST)가 이미 클러스터 내 MariaDB LoadBalancer 외부 IP로 되어 있다는 점을 확인했고, HAProxy IP 하나만 신뢰하도록 되어 있던 mod_rpaf 설정은 새 토폴로지에서는 의미가 없어 mod_remoteip + Pod 네트워크 대역으로 대체가 필요하다는 점도 함께 확인했습니다.

2. 스토리지 설계: webroot 전체를 RWX PVC로

WordPress는 FS_METHOD=direct로 동작해 .htaccess나 플러그인/코어 업데이트를 파일시스템에 직접 씁니다. 다중 파드 환경에서 모든 레플리카가 일관된 상태를 보게 하려면 wp-content만이 아니라 /var/www/html 전체를 공유 볼륨에 올려야 한다고 판단했습니다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-webroot
spec:
  accessModes: [ReadWriteMany]
  storageClassName: rook-cephfs
  resources:
    requests:
      storage: 5Gi

wp-config.php는 이 PVC 밖에 두었습니다. DB 접속 정보 등 비밀이 아닌 값은 ConfigMap으로, DB 비밀번호와 AUTH/SALT 키는 Secret으로 분리해 두 파일을 각각 subPath로 마운트하고, 본체 wp-config.phprequire_once로 Secret 쪽 파일을 불러오는 구조로 구성했습니다.

3. 커스텀 이미지 빌드: GitLab Container Registry + Kaniko

로컬 작업 환경에 docker/podman이 없었고, 클러스터도 이전까지 공개 이미지만 사용해 온 상태라 커스텀 이미지를 빌드할 경로가 필요했습니다. GitLab Container Registry가 비활성 상태였어서 이번에 신규로 활성화했습니다.

FROM wordpress:php8.4-apache

# 원본에 없던 zip 확장 추가
RUN apt-get update && apt-get install -y libzip-dev && docker-php-ext-install zip

# 원본과 동일한 Postfix(direct-send) 구성
RUN echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y postfix

# mod_rpaf 대체
RUN a2enmod remoteip headers rewrite expires
COPY security-headers.conf remoteip.conf /etc/apache2/conf-enabled/
CMD ["/usr/local/bin/start.sh"]

이미지는 로컬 빌드 없이 Kaniko Job으로 클러스터 안에서 빌드하고 바로 Registry에 푸시했습니다.

containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:v1.23.2
    args:
      - --dockerfile=/workspace/Dockerfile
      - --context=dir:///workspace/
      - --destination=gitlab.sierracloud.dev:5050/infra/k8s-ops/wordpress:php8.4-2

4. 데이터 이관

PVC를 마운트하는 임시 헬퍼 파드를 띄우고 rsync로 레거시 서버의 /var/www/html을 통째로 옮겼습니다. 총 297MB, 13,822개 파일이었고 컷오버 직전에 증분 동기화를 한 번 더 실행해 다운타임을 최소화했습니다. 미사용 상태로 남아있던 두 번째 WordPress 설치본(운영 DB를 그대로 바라보지만 어떤 vhost에서도 서빙되지 않던 orphan 설치)은 이관 대상에서 제외했습니다.

rsync -az --exclude='wp-config.php' \
  -e 'ssh -i <migration-key>' \
  root@192.168.x.x:/var/www/html/ /mnt/webroot/

트러블슈팅

GitLab Container Registry 활성화 중 nginx 전체 다운

현상: gitlab.rbregistry_external_url을 추가하고 gitlab-ctl reconfigure를 실행하자 nginx가 기동에 실패했고, Registry뿐 아니라 GitLab 웹 UI 전체가 접속 불가 상태가 되었습니다.

원인: registry_nginx가 메인 사이트에 적용해 둔 커스텀 인증서 경로를 상속받지 않고, 존재하지 않는 기본 경로(/etc/gitlab/ssl/<fqdn>.crt)를 찾다가 nginx 설정 검증 자체가 실패했습니다.

해결: registry_nginx['ssl_certificate']/['ssl_certificate_key']를 메인 사이트와 동일한 인증서 경로로 명시적으로 지정한 뒤 재실행해 즉시 복구했습니다.

registry_external_url 'https://gitlab.sierracloud.dev:5050'
gitlab_rails['registry_enabled'] = true
registry_nginx['ssl_certificate'] = "/data/cert/sierracloud.dev/fullchain.pem"
registry_nginx['ssl_certificate_key'] = "/data/cert/sierracloud.dev/privkey.pem"

Kaniko 빌드 컨텍스트의 dangling symlink

현상: Dockerfile과 부속 설정 파일을 ConfigMap으로 만들어 Kaniko 빌드 컨텍스트로 바로 마운트했더니, COPY 단계에서 cannot operate on dangling symlink 에러로 빌드가 실패했습니다.

원인: Kubernetes ConfigMap 볼륨은 각 파일을 실제 파일이 아니라 ..data/<file>을 가리키는 심볼릭 링크로 마운트합니다. Kaniko의 COPY가 이 링크를 그대로 이미지에 복사하면서, 이미지 안에서는 가리키는 대상이 없는 깨진 링크가 되어버렸습니다.

해결: initContainer에서 cp -rL로 심볼릭 링크를 실제 파일 내용으로 역참조 복사한 emptyDir을 만들고, 이 디렉터리를 Kaniko의 빌드 컨텍스트로 사용하도록 변경했습니다.

initContainers:
  - name: prepare-context
    image: busybox:1.36
    command: ["sh", "-c", "cp -rL /configmap-src/. /workspace/"]

Really Simple Security가 리버스 프록시 뒤에서 무한 리다이렉트를 일으킴

현상: .htaccess의 HTTPS 강제 리다이렉트 규칙이 RewriteCond %{HTTPS} !=on 조건을 쓰고 있었는데, 이관 후에는 모든 요청이 계속 리다이렉트되어 파드가 readiness probe를 통과하지 못하고 CrashLoopBackOff 상태에 빠졌습니다.

원인: 원본 서버는 Apache가 직접 TLS를 종료했기 때문에 %{HTTPS}가 정확했지만, 이관 후에는 Traefik이 TLS를 종료하고 WordPress 컨테이너에는 평문 HTTP로 전달되어 %{HTTPS}가 항상 off로 평가되었습니다.

해결: 플러그인 소스를 직접 확인해, 리버스 프록시/로드밸런서 뒤에서 지원하는 방식인 RewriteCond %{HTTP:X-Forwarded-Proto} !https로 교체했습니다. wp-config.php에도 HTTP_X_FORWARDED_PROTO를 확인해 $_SERVER['HTTPS']를 보정하는 코드를 추가해 PHP 레벨의 is_ssl() 판단도 함께 맞췄습니다. 프로브 자체는 이 로직과 무관한 정적 파일(/healthz.html)로 분리해 안정성을 확보했습니다.

RewriteCond %{HTTP_USER_AGENT} !lscache_runner [NC]
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{REQUEST_URI} !^/healthz\.html$
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

Traefik이 평문 HTTP 구간에서는 X-Forwarded-Proto를 신뢰하지 않음

현상: 위 수정 이후에도, HAProxy가 Traefik의 80(web) 엔트리포인트로 붙는 기존 패턴(다른 내부 서비스들과 동일한 구성)으로 연결하면 여전히 무한 리다이렉트가 재현되었습니다.

원인: Traefik은 보안상 클라이언트가 보낸 X-Forwarded-Proto 값을 그대로 신뢰하지 않고, 자신이 실제로 수신한 연결이 TLS인지 여부로 직접 값을 판단해 덮어씁니다. HAProxy와 Traefik 사이 구간이 평문 HTTP(80)이면 Traefik은 이 값을 항상 http로 기록합니다.

해결: HAProxy의 backend를 Traefik의 443(websecure) 엔트리포인트로 변경했습니다(ssl verify none). Traefik이 이 연결에서 직접 TLS를 종료하므로 X-Forwarded-Proto가 정확히 https로 설정됩니다. 마침 traefik 네임스페이스에 이미 로드되어 있던 *.sierracloud.dev 와일드카드 인증서가 Traefik의 기본 인증서로 쓰이고 있어서, 별도 인증서 작업 없이 그대로 유효한 인증서를 응답받을 수 있었습니다.

결과 확인

이번 WordPress Kubernetes 마이그레이션의 최종 상태를 다음과 같이 확인했습니다.

$ kubectl get pods -n wordpress-system -l app=wordpress
NAME                         READY   STATUS    RESTARTS   AGE
wordpress-xxxxxxxxxx-xxxxx   1/1     Running   0          91m
wordpress-xxxxxxxxxx-yyyyy   1/1     Running   0          91m

두 파드가 동일 PVC(RWX)를 실시간으로 공유하는지도 직접 검증했습니다. 한 파드에서 파일을 쓰고 다른 파드에서 즉시 동일한 내용을 읽어, 별도 볼륨이 아니라 완전히 같은 볼륨임을 확인했습니다.

  • 내부망 DNS(도메인 → Traefik IP 직접) 경로로 로그인 페이지, REST API, 실제 게시글 permalink까지 정상 응답 확인
  • 외부에서는 CDN을 경유해 HAProxy → Traefik → 파드로 이어지는 경로로 동일하게 정상 렌더링 확인
  • HAProxy IP로 CDN을 거치지 않고 직접 접근하면 403이 반환되는 것도 확인 — origin을 직접 노출하지 않도록 걸어둔 기존 접근 제어가 그대로 유지되고 있다는 뜻이라 정상 동작으로 판단했습니다

참고

관련 포스트:

참고 문서: Traefik — EntryPoints Forwarded Headers · Kaniko (GoogleContainerTools) · GitLab Container Registry Administration

Kubernetes MariaDB Resource Limits and Health Probes

개요

MariaDB 리소스 제한 설정과 헬스체크(liveness/readiness probe) 구성을 자체 관리형 Kubernetes 클러스터에 반영했습니다. 기존 MariaDB Deployment는 리소스 request/limit이 전혀 설정되지 않은 상태(resources: {})로 2년 넘게 운영되고 있었고, liveness/readiness probe도 없어 mysqld 프로세스가 응답 없이 멈추더라도 Kubernetes가 이를 감지하고 자동으로 재시작할 방법이 없는 구조였습니다.

이번 글에서는 ① 현재 구성 점검 ② 운영 중인 리소스의 YAML 추출 및 Git 반영 ③ 리소스 제한/프로브 설계와 적용, 이렇게 세 가지 작업을 순서대로 정리합니다.

환경

  • Kubernetes v1.33.7 (Control-plane 3대, Worker 3대 HA 구성)
  • Container Runtime: Docker + cri-dockerd
  • Storage: Rook-Ceph RBD (StorageClass rook-ceph-block, ReadWriteOnce)
  • MariaDB: 10.11, mariadb-system 네임스페이스, Deployment 단일 replica
  • PVC: 10Gi, PV ReclaimPolicy: Retain
  • 서비스 노출: MetalLB LoadBalancer (내부망 전용)

단계별 절차

1. 현재 구성 형상 점검

먼저 운영 중인 Deployment, PVC, Service, ConfigMap 현황과 실제 리소스 사용량을 확인했습니다.

$ kubectl get all -n mariadb-system -o wide
$ kubectl get pvc -n mariadb-system -o wide
$ kubectl top pod -n mariadb-system

점검 결과 Deployment의 strategy는 이미 Recreate로 설정되어 있었습니다. MariaDB처럼 ReadWriteOnce 볼륨을 사용하는 단일 파드 워크로드는 RollingUpdate 방식으로 배포하면 이전 파드가 볼륨을 반납하기 전에 새 파드가 같은 볼륨을 마운트하려다 충돌하는 경우가 있어, Recreate 전략이 올바른 선택이었습니다. PV의 reclaimPolicyRetain으로 설정되어 있어 PVC가 실수로 삭제되더라도 실제 데이터는 보존되는 안전한 구조였습니다.

반면 컨테이너 리소스는 request/limit이 전혀 없는 상태였고, 실제 메모리 사용량은 약 1.4Gi 수준이었습니다. ConfigMap으로 주입한 커스텀 설정은 다음과 같았습니다.

[mysqld]
innodb_buffer_pool_size=512M
innodb_log_file_size=256M
max_connections=300

2. YAML 추출 및 Git 반영

운영 중인 리소스(PVC, ConfigMap, Deployment, Service)를 그대로 추출해 저장소에 매니페스트 파일로 문서화했습니다. Secret은 정책상 Git에 포함하지 않고, 클러스터 재구성 시 수동으로 생성할 수 있도록 커맨드만 주석으로 남겼습니다.

# root 비밀번호(mariadb-secret)는 Git에 없음 - 클러스터 재구성 시 수동 생성 필요
#   kubectl create secret generic mariadb-secret -n mariadb-system \
#     --from-literal=password='<ROOT_PASSWORD>'

추출한 매니페스트를 kubectl diff로 실제 클러스터 상태와 비교해, 새로 작성한 YAML이 운영 중인 리소스와 완전히 동일한지 먼저 확인했습니다. 차이가 없다는 것을 확인한 뒤에야 Git에 커밋했습니다.

3. 리소스 Request/Limit 및 Probe 설계

실제 메모리 사용량(~1.4Gi)과 innodb_buffer_pool_size(512M) 설정, 그리고 노드의 여유 용량을 함께 고려해 다음과 같이 값을 산정했습니다.

resources:
  requests:
    cpu: 250m
    memory: 1Gi
  limits:
    cpu: "1"
    memory: 2Gi
livenessProbe:
  exec:
    command:
      - sh
      - -c
      - mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
  initialDelaySeconds: 30
  periodSeconds: 20
  timeoutSeconds: 5
  failureThreshold: 3
readinessProbe:
  exec:
    command:
      - sh
      - -c
      - mysqladmin ping -uroot -p"$MYSQL_ROOT_PASSWORD" --silent
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

Probe는 이미 컨테이너 환경변수로 주입되어 있던 MYSQL_ROOT_PASSWORD를 그대로 활용하는 mysqladmin ping exec 방식을 선택했습니다. 별도 Secret을 추가로 마운트할 필요 없이, 실제로 mysqld가 인증까지 정상 처리하는지 확인할 수 있는 방식입니다. Limit 산정 시에는 노드 전체 할당량 대비 여유가 충분한지(CPU 할당 비율, 메모리 할당 비율) 함께 확인한 뒤 값을 확정했습니다.

4. 적용 및 롤아웃 검증

kubectl apply 전에 반드시 kubectl diff로 변경 범위를 먼저 확인했습니다. Deployment의 strategyRecreate이기 때문에 적용 시 기존 파드가 먼저 종료되고 새 파드가 그 자리에 생성되는, 짧은 다운타임이 있는 변경이라는 점을 미리 인지하고 진행했습니다.

$ kubectl diff -f manifests/mariadb/mariadb.yaml
$ kubectl apply -f manifests/mariadb/mariadb.yaml
deployment.apps/mariadb configured

$ kubectl rollout status deployment/mariadb -n mariadb-system --timeout=120s
Waiting for deployment "mariadb" rollout to finish: 0 of 1 updated replicas are available...
deployment "mariadb" successfully rolled out

$ kubectl get pods -n mariadb-system -l app=mariadb
NAME                       READY   STATUS    RESTARTS   AGE
mariadb-xxxxxxxxxx-xxxxx   1/1     Running   0          12s

새 파드가 정상적으로 Ready 상태가 되었고, readiness probe가 통과하는 것을 확인했습니다. 로그에도 mariadbd: ready for connections가 정상 출력되어 별다른 문제 없이 전환이 완료되었습니다.

트러블슈팅

mysql.event 테이블 정의 불일치

현상: 파드 재시작 로그에 Incorrect definition of table mysql.event 에러와 함께 Event Scheduler가 비활성화된다는 메시지가 출력되었습니다.

원인: 과거 MariaDB 마이너 버전 업그레이드 이후 mysql_upgrade를 실행하지 않아, 시스템 테이블(mysql.event)의 컬럼 정의가 현재 바이너리 버전이 기대하는 스키마와 어긋난 상태로 남아있었습니다.

해결: 현재 Event Scheduler(예약 이벤트) 기능을 사용하고 있지 않아 서비스에는 영향이 없는 것으로 확인해, 이번 작업 범위에서는 별도 조치 없이 별도 후속 작업으로 분리했습니다. Event Scheduler를 사용할 계획이라면 mysql_upgrade 실행이 선행되어야 합니다.

결과 확인

최종적으로 다음 항목들을 확인해 이번 MariaDB 리소스 제한 및 헬스체크 반영 작업을 마무리했습니다.

$ kubectl get deployment mariadb -n mariadb-system \
  -o jsonpath='{.spec.template.spec.containers[0].resources}'
{"limits":{"cpu":"1","memory":"2Gi"},"requests":{"cpu":"250m","memory":"1Gi"}}

$ kubectl get pod -n mariadb-system -l app=mariadb
NAME                       READY   STATUS    RESTARTS   AGE
mariadb-xxxxxxxxxx-xxxxx   1/1     Running   0          2m
  • 리소스 request/limit 적용 완료 (request 250m/1Gi, limit 1core/2Gi)
  • liveness/readiness probe 정상 동작 확인
  • 클러스터 재기동 없이 무중단으로 서비스(LoadBalancer)가 재공지되어 애플리케이션 연결 영향 없음

참고

관련 포스트:

참고 문서: Kubernetes – Configure Liveness, Readiness and Startup Probes · Kubernetes – Resource Management for Pods and Containers