Một tình huống khá phổ biến: code nằm trên Bitbucket, nơi đội lập trình làm việc với branch và pull request, còn các pipeline vận hành cluster Kubernetes (tạo site bằng Helm, thao tác với máy NFS, database) lại chạy trên GitLab CI với runner tự host trong mạng private. Mỗi lần code được push lên Bitbucket, GitLab phải build image và cập nhật các site. Tài liệu này mô tả mô hình cầu nối đơn giản nhất: Bitbucket Pipelines gọi API của GitLab để tạo pipeline, rồi định kỳ hỏi lại trạng thái cho tới khi xong. Bài trình bày luồng chạy, file cấu hình mẫu, các lỗi hay gặp, cách ép rollout khi image giữ tag cố định, ưu nhược và các hướng thay thế.

1. Khi nào cần cầu nối sang GitLab CI

Ở bài CI/CD đầu tiên với Bitbucket Pipelines, Docker Hub và kubectl, mọi việc chạy trong Bitbucket: build, đẩy image, kubectl apply. Khi mỗi site trở thành một release Helm như bài Helm chart là gì: đóng gói website Laravel để deploy lặp lại, pipeline vận hành thường cần những thứ mà runner dùng chung của Bitbucket không có:

  • Runner tự host (ví dụ GitLab Runner đã dùng từ bài Deploy tự động đầu tiên: GitLab Runner, SSH và nginx template) nằm trong mạng private, có executor shell để SSH vào máy NFS, máy database.
  • Kubeconfig, khoá SSH và quy trình cập nhật repo values đã cấu hình sẵn ở phía GitLab CI.
  • Đội vận hành quản lý quyền truy cập hạ tầng ở một nơi duy nhất.

Chuyển code sang GitLab thì ảnh hưởng thói quen của cả đội lập trình; chuyển pipeline vận hành về Bitbucket thì mất runner nội bộ. Phương án ít xáo trộn nhất là để Bitbucket chỉ làm một việc: báo cho GitLab biết có commit mới.

2. Luồng chạy từ push tới pod mới

Dev push / merge PR ──► Bitbucket (repo ứng dụng)
                          │ bitbucket-pipelines.yml
                          │ POST /api/v4/projects/<id>/pipeline
                          │   ref=main, BITBUCKET_BRANCH, BITBUCKET_COMMIT_ID
                          ▼
                  GitLab CI (project build, runner tự host)
                          │ checkout đúng commit, docker build
                          │ docker push <registry>/app:<tag>
                          │ ghi mã commit vào values của từng site, commit repo values
                          │ helm upgrade lên cluster
                          ▼
                  Cluster: pod mới kéo image và chạy code mới

   Trong lúc đó, step Bitbucket định kỳ gọi
   GET /api/v4/projects/<id>/pipelines/<pipeline_id> để lấy trạng thái

Mỗi lần deploy, pipeline GitLab CI tạo một commit nhỏ trong repo values (chỉ đổi giá trị commit của site). Lịch sử repo values vì thế trở thành nhật ký deploy: site nào chạy commit nào, đổi lúc nào.

3. API tạo pipeline của GitLab CI

GitLab có hai cách để hệ thống bên ngoài khởi động một pipeline GitLab CI:

Create pipelinePipeline trigger
EndpointPOST /projects/:id/pipelinePOST /projects/:id/trigger/pipeline
Xác thựcAccess token (header PRIVATE-TOKEN) có quyền apiTrigger token tạo riêng cho project, truyền qua tham số token
Truyền biếnMảng JSON variables: [{key, value}]variables[KEY]=value
Phạm vi quyềnRộng: dùng được cho mọi API mà người sở hữu token có quyềnHẹp: chỉ kích hoạt pipeline của đúng project

Cả hai trả về JSON có id của pipeline vừa tạo, dùng để hỏi trạng thái qua GET /projects/:id/pipelines/:pipeline_id. Trạng thái cuối là success, failed hoặc canceled; trạng thái trung gian là created, pending, running... Chi tiết ở trang Pipelines API của GitLab và kích hoạt pipeline bằng API.

4. File bitbucket-pipelines.yml gọi GitLab CI

Khuôn mẫu dùng trigger token (phạm vi hẹp) và jq để đọc JSON. Token, project ID và URL GitLab nằm trong repository variables đánh dấu secured, không viết vào file:

image: atlassian/default-image:4

pipelines:
  branches:
    main:
      - step:
          name: Trigger GitLab CI
          script:
            - apt-get update -qq && apt-get install -y -qq jq
            - |
              RESP=$(curl -sS --fail -X POST \
                -F "token=$GITLAB_TRIGGER_TOKEN" -F "ref=main" \
                -F "variables[BITBUCKET_BRANCH]=$BITBUCKET_BRANCH" \
                -F "variables[BITBUCKET_COMMIT_ID]=$BITBUCKET_COMMIT" \
                "$GITLAB_URL/api/v4/projects/$GITLAB_PROJECT_ID/trigger/pipeline")
              PIPELINE_ID=$(echo "$RESP" | jq -r '.id')
              WEB_URL=$(echo "$RESP" | jq -r '.web_url')
              [ "$PIPELINE_ID" != "null" ] || { echo "Không tạo được pipeline"; exit 1; }
              echo "Pipeline GitLab CI: $WEB_URL"
              for i in $(seq 1 80); do          # tối đa khoảng 20 phút
                sleep 15
                STATUS=$(curl -sS --header "PRIVATE-TOKEN: $GITLAB_READ_TOKEN" \
                  "$GITLAB_URL/api/v4/projects/$GITLAB_PROJECT_ID/pipelines/$PIPELINE_ID" | jq -r '.status')
                echo "Trạng thái: $STATUS"
                case "$STATUS" in
                  success) exit 0 ;;
                  failed|canceled|skipped) echo "Xem lỗi tại $WEB_URL"; exit 1 ;;
                esac
              done
              echo "Hết thời gian chờ, xem $WEB_URL"; exit 1

Trigger token chỉ tạo được pipeline; để đọc trạng thái, dùng một token riêng chỉ có quyền read_api. Step Bitbucket chỉ thành công khi pipeline GitLab CI thành công, nên người merge pull request vẫn thấy kết quả deploy ngay trên Bitbucket.

5. Lỗi thường gặp khi gọi GitLab CI từ Bitbucket

  • Dấu hai chấm trong lệnh echo. - echo "Current branch: $BITBUCKET_BRANCH" khiến YAML hiểu cả dòng là cặp key/value vì có chuỗi ": ". Bỏ dấu hai chấm, bọc lệnh trong nháy đơn, hoặc dùng khối | như mẫu trên.
  • Sai cặp endpoint và token. /trigger/pipeline đòi trigger token, không nhận access token qua PRIVATE-TOKEN; /pipeline thì ngược lại. Dùng sai cặp là luôn bị từ chối.
  • Sai định dạng biến. Với /pipeline, đặt "BITBUCKET_BRANCH": "..." ngang hàng với ref thì GitLab bỏ qua: pipeline vẫn chạy nhưng không nhận được commit cần build. Biến phải nằm trong mảng variables với cặp key/value.
  • Biến nằm trong nháy đơn. '{"value":"$BITBUCKET_COMMIT"}' gửi đi nguyên chuỗi $BITBUCKET_COMMIT. Phải đóng nháy đơn, mở nháy kép quanh biến rồi mở lại: '"$BITBUCKET_COMMIT"'. Cách phát hiện nhanh nhất là echo payload ngay trước curl.
  • Script nhiều dòng bị gộp thành một dòng. Vòng while viết thành nhiều mục trong YAML có thể bị nối lại bằng dấu cách; thiếu ; là shell hiểu sai cả khối. Dùng khối | để giữ nguyên xuống dòng.
  • Bóc JSON bằng grep. grep -o '"id":[0-9]*' có thể bắt nhầm id lồng bên trong (ví dụ id của user tạo pipeline). Dùng jq.
  • In token ra log để debug. Bitbucket che giá trị biến secured trong log, nhưng đừng dựa vào điều đó.
  • Thăm dò quá dày. Hỏi mỗi vài giây không làm build xong sớm hơn; khoảng 15–30 giây là đủ.

6. Ép rollout khi tag image không đổi

Nếu image luôn được đẩy với một tag cố định, chỉ build và push thì Kubernetes không biết có gì thay đổi. Chart có thể giải quyết bằng một label trong pod template, lấy giá trị từ values:

template:
  metadata:
    labels:
      {{- include "web.selectorLabels" . | nindent 8 }}
      commit: {{ .Values.commitId | quote }}

Pipeline GitLab CI ghi mã commit Bitbucket vào giá trị này cho từng site rồi helm upgrade. Pod template đổi, Deployment rolling update, pod mới với imagePullPolicy: Always kéo lại image cùng tag và nhận code mới.

ĐượcMất
Không phải đổi tag trong values của hàng loạt siteKhông biết pod đang chạy image nào nếu chỉ nhìn tag
Label commit cho biết site đang ở commit nàoRollback về commit cũ không lấy lại được image cũ, vì tag đã bị ghi đè
Một lần build dùng cho mọi site cùng loạiPod nào khởi động lại cũng phải hỏi registry về image, kể cả khi không có code mới

Khi số site tăng, nên chuyển sang tag theo mã commit và ghi tag vào values; label commit khi đó chỉ còn vai trò thông tin.

7. Ưu và nhược của cầu nối API cộng polling

Ưu điểmNhược điểm
Đơn giản: một lệnh curl, không cần thêm dịch vụStep Bitbucket ngồi chờ suốt thời gian GitLab CI chạy, tốn phút build của Bitbucket
Trạng thái deploy hiện ngay trên BitbucketChỉ thấy đậu hay rớt; muốn xem lỗi chi tiết vẫn phải mở GitLab CI
Không phải chuyển code sang nền tảng khácNếu dùng access token quyền api, lộ token là lộ quyền với cả tài khoản GitLab
Phía GitLab CI giữ toàn quyền với runner nội bộThiếu timeout thì một pipeline kẹt pending giữ step tới giới hạn của Bitbucket

8. Những hướng thay thế

  • Trigger token thay access token, như mẫu ở mục 4: lộ ra thì thiệt hại nhỏ hơn nhiều. Đây là thay đổi rẻ nhất nên làm trước.
  • Webhook của Bitbucket gọi thẳng URL trigger. GitLab CI nhận webhook và đưa payload vào biến TRIGGER_PAYLOAD. Bitbucket không phải chạy step chờ; đổi lại, muốn trạng thái hiện về Bitbucket thì pipeline GitLab CI phải tự gọi API build status của Bitbucket ở bước cuối.
  • Mirror repo từ Bitbucket sang GitLab. Tính năng pull mirroring (gói trả phí) cho phép mỗi lần mirror cập nhật tự chạy pipeline mà không cần cầu nối.
  • Multi-project pipeline, nếu code cũng nằm trên GitLab: trigger: project với strategy: depend cho pipeline cha chờ và nhận trạng thái pipeline con một cách tự nhiên.

9. Checklist cho cầu nối Bitbucket và GitLab CI

  1. In payload JSON ra trước khi gửi; lỗi thường nằm ở nháy và dấu hai chấm hơn là ở API.
  2. Đúng cặp endpoint và loại token; token phạm vi hẹp nhất có thể.
  3. Vòng chờ có số lần tối đa, in đường dẫn tới pipeline GitLab CI khi thất bại.
  4. Đọc JSON bằng jq, không bằng grep.
  5. Biết rõ chiến lược tag image ảnh hưởng thế nào tới rollback.
  6. Đối xử với cầu nối tạm như hệ thống thật: có chủ sở hữu, có tài liệu, có kế hoạch thay thế.

Bài tiếp theo

Mỗi branch một pipeline vận hành: tự động hoá bằng GitLab CI. Mỗi thao tác vận hành là một branch trong cùng một repo: luồng tạo site bằng Helm và cách pipeline tự dọn dẹp khi deploy thất bại.