Một pipeline CI không đổi dòng nào trong nhiều tuần, rồi bỗng thất bại ở bước dùng git qua SSH với thông điệp OpenSSL version mismatch. Người sửa thường lao vào thử: ghim phiên bản, cài lại git, chuyển sang HTTPS, ép một bản OpenSSL của nhánh Alpine khác. Phần lớn đều vô ích, vì nguyên nhân nằm ngoài repo: một bản OpenSSL mới của kho gói có lỗi, gặp một image CI không ghim tag và một dòng apk add chạy mỗi lần job khởi động. Bài này mô tả tình huống điển hình đó, quy trình điều tra để tìm ra quy luật, và những thay đổi giúp pipeline không phụ thuộc vào kho gói bên ngoài.

Triệu chứng

Job deploy của pipeline tạo site chạy trong một image cộng đồng chứa helm và kubectl, khai báo không có tag nên luôn lấy latest. Thứ tự các bước: tạo database, helm install, chờ HTTP 200, rồi tới bước mã hoá ConfigMap và Secret trước khi commit values lên repo Helm. Bước mã hoá mở đầu bằng hai dòng:

deploy:
  image: helm-kubectl            # không tag = latest
  script:
    - apk add openssl
    - git clone git@git.example.com:team/config-tools.git tools
    - ...

Lệnh git qua SSH chạy ngay sau dòng apk add thì báo:

OpenSSL version mismatch. Built against 30500020, you have 30500030
fatal: Could not read from remote repository.

Đây là thông báo của OpenSSH khi phiên bản thư viện OpenSSL lúc chạy không khớp với phiên bản lúc ssh được build. Vì bước này nằm sau helm install, job lỗi kéo theo job rollback gỡ site vừa tạo, giống chuỗi phản ứng ở bài Conflict khi nhiều pipeline cùng push: push_with_retry và bài học GitOps.

Điều tra

Bước 1: dừng thử mò, đặt các lần chạy cạnh nhau

Phản xạ thường gặp là sửa liên tục rồi chạy lại. Cách hiệu quả hơn là lập một bảng: mỗi lần chạy ghi lại nội dung đoạn script liên quan và kết quả. Nếu không ghi kịp, lịch sử Git vẫn dựng lại được: mỗi commit pipeline có thời điểm, mỗi lần tạo site thành công để lại một commit values. Đối chiếu hai chuỗi này, quy luật kiểu tình huống này thường hiện ra rất nhanh: mọi lần thành công đều rơi vào lúc script không có dòng cài OpenSSL.

Bước 2: loại các hướng nghi sai

  • Ghim lại bản OpenSSL cũ (apk add openssl=<bản cũ>): kho stable của Alpine chỉ giữ bản mới nhất của mỗi gói, nên bản cũ không còn để cài khi bản mới đã thay thế.
  • Lấy gói từ nhánh Alpine khác: gói không có trong kho mà image đang dùng; --allow-untrusted chỉ bỏ qua kiểm tra chữ ký, không làm gói xuất hiện, và trộn thư viện giữa các nhánh còn nguy hiểm hơn.
  • ldconfig và LD_LIBRARY_PATH: không đổi được thực tế rằng chỉ có một bản libcrypto3 trên hệ thống.
  • Chuyển git sang HTTPS: đi vòng được SSH, nhưng git qua HTTPS vẫn dùng chung thư viện OpenSSL, và kéo theo việc đưa token vào URL.

Bước 3: nhìn ra ngoài repo, tới image và gói OpenSSL

Khi code không đổi mà job vỡ, câu hỏi đúng là "bên ngoài có gì mới". Có hai nguồn cần kiểm tra:

# image đang dùng được build trên Alpine nào, có OpenSSL bản nào
docker run --rm helm-kubectl sh -c 'cat /etc/alpine-release; apk info -v | grep -E "openssl|libcrypto|openssh"'

# sau khi cài thêm thì thư viện đổi thành bản nào
docker run --rm helm-kubectl sh -c 'apk add -q openssl; apk info -v | grep libcrypto; ssh -V'

Tiếp theo là lịch sử kho gói của nhánh Alpine tương ứng (trang aports, changelog của gói). Trong tình huống điển hình này, OpenSSL vừa được nâng lên một bản mới có lỗi ở macro số phiên bản, khiến phép so phiên bản của openssh thất bại. Alpine từng ghi nhận đúng lỗi này với OpenSSL 3.5.3: "OpenSSL 3.5.3 introduced a regression that caused openssh version check to fail", và phát hành bản vá ngay sau đó.

Nguyên nhân gốc

Chuỗi sự kiện như sau. Image CI được build với ssh và git liên kết với libcrypto3 bản cũ. Mỗi lần job chạy, apk add openssl cài gói OpenSSL mới nhất từ kho, kéo libcrypto3 lên bản mới. Bản mới báo sai số phiên bản, ssh so với phiên bản lúc build, thấy không khớp và từ chối chạy. Không cài OpenSSL thì thư viện giữ nguyên, ssh chạy bình thường. Ba điều kiện cùng lúc tạo ra sự cố:

  • Image CI không ghim tag, nên nội dung image phụ thuộc ngày runner kéo về.
  • Cài gói hệ thống lúc chạy job, trộn gói mới từ kho với thư viện đã đóng gói sẵn trong image.
  • Một lỗi thật của OpenSSL ở thượng nguồn, được vá nhanh nhưng vẫn đủ để làm đỏ pipeline trong khoảng thời gian chưa có bản vá.

Hai điều kiện đầu nằm trong tay đội vận hành. Điều kiện thứ ba thì không, và sẽ còn lặp lại dưới hình thức khác.

Khắc phục

Tạm thời

Cách khắc phục ngắn nhất là bỏ dòng cài OpenSSL lúc chạy job nếu image đã có sẵn công cụ cần dùng (thường là vậy: lệnh openssl có sẵn trong nhiều image Alpine). Kiểm tra lại bước mã hoá vẫn cho ra file đúng. Khi lùi file pipeline về trạng thái cũ, dùng git revert đúng các commit thử sửa, rồi đọc diff trước khi push, để không kéo theo những thay đổi không liên quan.

Lâu dài

  • Ghim image CI theo tag cụ thể, tốt hơn là theo digest, và chỉ nâng khi đã chạy thử. Cú pháp có trong tài liệu image của GitLab CI.
  • Tự build image CI chứa sẵn helm, kubectl, git, ssh và OpenSSL, không chạy apk add trong job. Nâng gói khi build image, có kiểm thử, thay vì lúc tạo site cho khách.
  • Nếu buộc phải cài lúc chạy: apk upgrade đồng bộ cả hệ thống, không cài lẻ một gói lên trên thư viện cũ. Cơ chế của apk có trong Alpine Package Keeper.
  • Đặt bước mã hoá và commit trước helm install hoặc tách thành job riêng, để lỗi công cụ không kéo theo việc gỡ một site đang chạy.

Một Dockerfile image CI tối thiểu theo hướng này:

FROM alpine:3.22
ARG HELM_VERSION=v3.19.0
ARG KUBECTL_VERSION=v1.33.0
RUN apk add --no-cache bash curl git openssh-client openssl ca-certificates \
 && curl -fsSL https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz | tar xz -C /tmp \
 && mv /tmp/linux-amd64/helm /usr/local/bin/ \
 && curl -fsSLo /usr/local/bin/kubectl https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl \
 && chmod +x /usr/local/bin/kubectl \
 && ssh -V && git --version && helm version --short

Dòng cuối là bước tự kiểm tra: nếu ssh và OpenSSL lệch nhau ngay lúc build, image sẽ không được tạo ra, thay vì vỡ ở pipeline của khách.

Phòng ngừa

  • Khi một job CI vỡ mà code không đổi, câu hỏi đầu tiên là "image và gói bên ngoài có gì mới", trước khi sửa script.
  • Mỗi lần sửa thử là một biến số: đổi một thứ, chạy, ghi kết quả, như phương pháp ở bài Điều tra sự cố nhiều tầng: từ balancer tới database và Kubernetes.
  • Commit "lùi về" phải là git revert các commit cụ thể, có đọc diff.
  • Trong lúc gỡ lỗi, không echo biến chứa token, không ghép token vào URL rồi chạy git remote -v. Biến bí mật trong GitLab phải được đánh dấu masked. Nếu lỡ in token ra log job, coi như token đã lộ: thu hồi, tạo mới, rồi xoá log job đó.
  • Đặt lịch build lại image CI định kỳ, có chạy thử, để nhận bản vá bảo mật mà không bị bất ngờ.

Bài học

Một pipeline không đổi dòng nào vẫn có thể vỡ, vì nó âm thầm phụ thuộc vào hai thứ trôi theo thời gian: tag latest của một image cộng đồng và nội dung kho gói ngày hôm đó. Một lỗi OpenSSL tồn tại ngắn ở thượng nguồn đủ để làm đỏ pipeline tạo site. Bài học thứ hai đến từ chính cách sửa: thử cài lại cùng một nhóm gói nhiều lần không đưa tới đâu, còn đặt các lần chạy cạnh nhau thì quy luật "có dòng cài OpenSSL thì hỏng" hiện ra trong vài phút. Cách tổ chức pipeline có ở bài Mỗi branch một pipeline vận hành: tự động hoá bằng GitLab CI; các bài khác nằm trong chuyên mục blog kỹ thuật của Sudo.

Bài tiếp theo

Kubeconfig lỡ commit lên git: dọn secret và rotate đúng cách. Xoá file khỏi repo chưa phải là xong: lịch sử Git, các bản clone và chính chiếc chìa khoá vẫn còn đó.