Giải quyết 1 số thách thức khi triển khai CI/CD cho Apache Traffic Control sử dụng Jenkins Docker-in-Docker
Gần đây, mình đang thử nghiệm CI/CD cho source code Apache Traffic Control (ATC), stack mình quyết định dùng là Jenkins cho CI/CD và Nexus dùng làm repository, vì những bản build của ATC hiện tại đều là file RPM.
Bước đầu tiên mình muốn thực hiện nhanh trước với việc triển khai Jenkins thông qua Docker-in-Docker (Docker:dind) và Nexus bằng container (bước tiếp theo có thể là 1 cluster K8s đảm bảo tính co giãn).
Tuy nhiên, trong quá trình thực hiện, một số lỗi đã xuất hiện liên quan đến pkg builder của ATC và sẽ được trình bày chi tiết cùng với các giải pháp khả thi.
Bối cảnh
Khi làm việc với Apache Traffic Control, trong mã nguồn đã có sẵn các script hỗ trợ việc xây dựng cả ATC và modified ATS. Tuy nhiên, việc tích hợp Jenkins với Docker:dind để thực hiện các bước build này đã gặp phải một số thách thức.
Cài đặt Jenkins và Nexus
Để bắt đầu, có thể tham khảo tài liệu chính thức của Jenkins về cách cài đặt với Docker tại https://www.jenkins.io/doc/book/installing/docker/ và Nexus tại: https://hub.docker.com/r/sonatype/nexus3/.
Hoặc có thể tham khảo docker-compose tại đây (có GitHub Actions chạy hàng ngày để đảm bảo lấy được thông tin phiên bản mới nhất của Jenkins): https://github.com/ntheanh201/jenkins-nexus-starter
Sử dụng Docker:dind cho phép chạy Docker bên trong một container khác, điều này rất hữu ích cho việc xây dựng và triển khai ứng dụng. Việc này không chỉ giúp tiết kiệm tài nguyên mà còn tạo ra một môi trường tách biệt, giúp dễ dàng quản lý các phiên bản và cấu hình khác nhau.
Các lỗi gặp phải trong quá trình triển khai
1. Không kết nối được tới Docker Dind
Trong quá trình chạy lệnh build, đã có lỗi xảy ra:
+ ./pkg -o -b -v ats
docker: Get "<https://docker:2376/_ping>": dial tcp xxx.xxx.xxx.xxx:2376: i/o timeout.
See 'docker run --help'.
Building ats.
docker: Cannot connect to the Docker daemon at tcp://docker:2376. Is the docker daemon running?.
See 'docker run --help'.
Failed to build ats.
Results in 'dist':
total 0
script returned exit code 1
Nguyên nhân: Lỗi này xảy ra khi pkg builder container bên trong Jenkins container không thể kết nối tới Docker daemon đang chạy trong container Dind.
Giải pháp: Sử dụng host network
Để giải quyết vấn đề kết nối tới Docker Dind, một giải pháp khả thi là cập nhật lệnh Docker Compose để sử dụng host network. Điều này cho phép container bên trong Jenkins có thể truy cập trực tiếp vào Docker daemon mà không gặp phải các vấn đề về network.
Nhận thấy trong pkg script của ATC có đoạn:
COMPOSECMD=(docker run --rm "${DOCKER_ADDR[@]}" $COMPOSE_OPTIONS "${VOLUMES[@]}" -w "$(pwd)" $IMAGE docker compose)
Thêm —network host vào trong COMPOSECMD:
COMPOSECMD=(docker run --rm "${DOCKER_ADDR[@]}" $COMPOSE_OPTIONS "${VOLUMES[@]}" --network host -w "$(pwd)" $IMAGE docker compose)
2. Thiếu Certs
Một lỗi khác cũng đã xuất hiện trong quá trình cài đặt:
+ ./pkg -o -b -v ats
Failed to initialize: unable to resolve docker endpoint: open /certs/client/ca.pem: no such file or directory
Building ats.
Failed to initialize: unable to resolve docker endpoint: open /certs/client/ca.pem: no such file or directory
Failed to build ats.
Results in 'dist':
total 0
script returned exit code 1
Nguyên nhân: Lỗi này xảy ra do ca cert của Docker chưa được mount vào trong pkg container, dẫn đến việc không thể xác thực kết nối tới Docker daemon. Việc thiếu các cert cần thiết có thể gây ra sự cố trong việc thiết lập kết nối an toàn giữa các container.
Giải pháp: Mount certs vào pkg builder container
Để khắc phục lỗi liên quan đến ca cert, chú ý đến đoạn:
volumes:
- jenkins-docker-certs:/certs/client
nằm trong docker-compose service jenkins-docker và jenkins-blueocean, cho nên cần mount thư mục /certs vào trong docker in (docker in docker) - dindind 😄
Nhận thấy trong pkg rơi vào trường hợp:
DOCKER_ADDR=(-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH)
Thêm vào như sau:
DOCKER_ADDR=(-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH -v "/certs/client:/certs/client")
Việc này đảm bảo rằng các cert cần thiết sẽ có sẵn trong pkg builder container, giúp thiết lập kết nối an toàn với Docker daemon (Dind).
Thành quả
Sau khi thực hiện các thay đổi trên, quá trình build Apache Traffic Server đã thành công.
Chi tiết xem tại PR:
https://github.com/ntheanh201/trafficcontrol/pull/10
Bên trong PR có demo Jenkinsfile cho việc chạy CI ATS và đẩy các file RPM lên Nexus repo
pipeline {
agent any
environment {
// Nexus credentials should be configured in Jenkins credentials
NEXUS_CREDENTIAL_ID = 'nexus-credentials'
// Nexus network aliases in docker-compose is nexus
NEXUS_URL = 'http://nexus:8081'
// Nexus repository should be configured in Nexus yum(hosted) repository
NEXUS_REPOSITORY = 'atc-rpms'
NEXUS_GROUP_ID = 'org.apache.trafficcontrol'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build ATS') {
steps {
sh '''
# Build the ATS package
./pkg -o -b -v ats
# Store RPM filenames for later use
find dist -name "*.rpm" -type f > rpm_files.txt
'''
// Archive the RPMs as Jenkins artifacts
archiveArtifacts artifacts: 'dist/*.rpm', fingerprint: true
}
}
stage('Upload to Nexus') {
steps {
script {
try {
echo "Starting Upload to Nexus stage"
withCredentials([usernamePassword(credentialsId: "${NEXUS_CREDENTIAL_ID}",
usernameVariable: 'NEXUS_USER',
passwordVariable: 'NEXUS_PASS')]) {
echo "Credentials loaded successfully"
// Read the list of RPM files
def rpmFiles = readFile('rpm_files.txt').trim().split('\\n')
rpmFiles.each { rpmFile ->
if (!fileExists(rpmFile)) {
error "RPM file does not exist at ${rpmFile}"
}
def rpmName = rpmFile.tokenize('/')[-1]
echo "Processing RPM: ${rpmName}"
// Extract version from RPM filename using pattern matching
// Assuming filename format: name-version-release.arch.rpm
def version = sh(
script: """
filename=\\$(basename ${rpmFile})
echo \\$filename | sed -E 's/.*-([0-9]+\\\\.[0-9]+\\\\.[0-9]+)-.*\\\\.rpm/\\\\1/'
""",
returnStdout: true
).trim()
if (version.isEmpty() || version == rpmName) {
error "Could not extract version from RPM filename: ${rpmName}"
}
echo "RPM version: ${version}"
// Upload to Nexus using curl
echo "Uploading to Nexus: ${NEXUS_URL}/repository/${NEXUS_REPOSITORY}/${NEXUS_GROUP_ID}/${version}/${rpmName}"
def curlResponse = sh(
script: """
curl -v -k -u ${NEXUS_USER}:${NEXUS_PASS} \\
--upload-file '${rpmFile}' \\
'${NEXUS_URL}/repository/${NEXUS_REPOSITORY}/${NEXUS_GROUP_ID}/${version}/${rpmName}'
""",
returnStatus: true
)
if (curlResponse != 0) {
error "Curl upload failed with status ${curlResponse} for ${rpmName}"
}
echo "Upload completed for ${rpmName}"
}
}
} catch (Exception e) {
echo "Error in Upload to Nexus stage: ${e.getMessage()}"
error "Failed to upload to Nexus: ${e.getMessage()}"
}
}
}
}
}
post {
always {
// Clean workspace after build
cleanWs()
}
success {
echo 'Successfully built and published RPM to Nexus'
}
failure {
echo 'Failed to build or publish RPM'
}
}
}
PR này mình không contribute lên apache trafficcontrol repo là bởi vì nó là một phần riêng biệt, sẽ phá vỡ các CI script dùng cho việc test mà trafficcontrol đang sử dụng, cũng như là 1 thử nghiệm với hướng đi chi tiết hơn so với mức độ tổng quát của trafficcontrol.
Hy vọng bài viết này sẽ cung cấp thông tin hữu ích cho những ai đang tìm hiểu về việc triển khai CI/CD bằng Jenkins với Docker-in-Docker, cũng như quan tâm tới các vấn đề xoay quanh CDN hoặc Apache Traffic Control.