ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

CI/CD กับ AI: เขียน Pipeline ให้ทำงานจริง

CI/CD กับ AI: เขียน Pipeline ให้ทำงานจริง

Jenkinsfile เป็นไฟล์ประเภทที่ AI เขียนได้คล่องมาก แต่ปัญหาคือมันมักดูถูกต้องจนกว่าจะรันจริง: เรียก step จาก plugin ที่ Jenkins ของเราไม่ได้ติดตั้ง, ใส่ || true ให้ test ที่ fail ผ่านไปเงียบ ๆ, หรือเอา password ไปต่อในสตริงจนโผล่ใน console log บทความนี้จะวางโครง pipeline ที่ครบสี่ขั้น lint, test, audit, deploy พร้อม Jenkinsfile แบบ declarative ที่สั้นพอจะอ่านจบ วิธีจัดการ secrets ใน CI ให้ถูกต้อง และวิธีทดสอบ pipeline ก่อนปล่อยให้ทั้งทีมใช้

ออกแบบขั้นตอนก่อนให้ AI เขียน

อย่าเริ่มจาก "เขียน Jenkinsfile ให้หน่อย" แต่เริ่มจากตกลงกันว่าแต่ละขั้นต้องทำอะไร และอะไรคือเงื่อนไขที่ทำให้ build ล้ม

ขั้นทำอะไรล้มเมื่อ
Installcomposer install ตาม lock filelock file ไม่ตรงกับ composer.json หรือติดตั้งไม่สำเร็จ
Lintphp -l ทุกไฟล์, coding standard, static analysissyntax error หรือ static analysis พบปัญหาตามระดับที่ตั้ง
Testunit test และ functional testมี test fail แม้แต่ข้อเดียว
Auditcomposer audit, npm auditพบช่องโหว่ตามระดับที่ทีมกำหนด
Deploybuild image, push, สั่ง deployเฉพาะ branch main และทุกขั้นก่อนหน้าผ่าน

เมื่อมีตารางนี้ การเขียน prompt ก็ตรงไปตรงมา และใช้ตารางเดียวกันเป็นเกณฑ์ review ผลลัพธ์ได้ด้วย

เขียน Jenkinsfile แบบ declarative สำหรับโปรเจกต์ Yii2 (PHP 8, Codeception)
ขั้นตอนและเงื่อนไขตามตารางนี้ (แปะตาราง)
plugin ที่ติดตั้งแล้ว: Pipeline, Git, Credentials Binding, JUnit
ห้ามใช้ step จาก plugin อื่น
credentials ที่มี: registry-cred (username/password), deploy-ssh (SSH private key)
ห้ามใส่ secret ใน Groovy string แบบ double quote, ห้ามใช้ || true กับขั้น test และ audit
deploy เฉพาะ branch main มี timeout และห้าม build ซ้อนกัน

การบอกรายชื่อ plugin ที่มีจริงช่วยกันปัญหาใหญ่ที่สุดของ Jenkinsfile จาก AI คือการเรียก step ที่ Jenkins ของเราไม่รู้จัก

ตัวอย่าง Jenkinsfile

pipeline {
    agent any

    options {
        timeout(time: 20, unit: 'MINUTES')
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '30'))
    }

    environment {
        IMAGE = "registry.example.com/myapp:${env.BUILD_NUMBER}"
    }

    stages {
        stage('Install') {
            steps {
                sh 'composer validate --strict'
                sh 'composer install --no-interaction --prefer-dist --no-progress'
            }
        }

        stage('Lint') {
            steps {
                sh '''
                    find . -path ./vendor -prune -o -name '*.php' -print0 \
                      | xargs -0 -n1 php -l > /dev/null
                '''
                sh 'vendor/bin/phpstan analyse --no-progress'
            }
        }

        stage('Test') {
            steps {
                sh 'vendor/bin/codecept run unit --xml'
            }
            post {
                always {
                    junit allowEmptyResults: true, testResults: 'tests/_output/*.xml'
                }
            }
        }

        stage('Audit') {
            steps {
                sh 'composer audit --no-dev'
                sh 'npm ci --ignore-scripts && npm audit --omit=dev --audit-level=high'
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            environment {
                REGISTRY = credentials('registry-cred')
            }
            steps {
                sh '''
                    echo "$REGISTRY_PSW" | docker login -u "$REGISTRY_USR" --password-stdin registry.example.com
                    docker build -t "$IMAGE" .
                    docker push "$IMAGE"
                    docker logout registry.example.com
                '''
                withCredentials([sshUserPrivateKey(credentialsId: 'deploy-ssh',
                        keyFileVariable: 'SSH_KEY', usernameVariable: 'SSH_USER')]) {
                    sh 'ssh -i "$SSH_KEY" -o BatchMode=yes "$SSH_USER@app.example.com" "./deploy.sh $IMAGE"'
                }
            }
        }
    }

    post {
        failure {
            echo 'Build failed: ดูรายละเอียดใน console log ของแต่ละ stage'
        }
    }
}

สิ่งที่ควรสังเกต:

  • ขั้น Install ใช้ composer install ไม่ใช่ update เพื่อให้ CI ใช้เวอร์ชันเดียวกับที่ทดสอบในเครื่อง และ composer validate --strict จับกรณี lock file ไม่ตรง
  • xargs จะคืนค่า error ถ้ามีไฟล์ใดไฟล์หนึ่ง lint ไม่ผ่าน ทำให้ stage ล้มตามที่ต้องการ
  • when { branch 'main' } ใช้ได้กับ Multibranch Pipeline ถ้าใช้ job แบบอื่นต้องตรวจเงื่อนไข branch ด้วยวิธีที่เหมาะกับ job นั้น
  • เครื่อง agent ต้องมี PHP, Composer, Node และ Docker ตามที่ใช้ และ host ปลายทางต้องอยู่ใน known_hosts ของ agent อยู่แล้ว ห้ามแก้ด้วยการปิด host key checking
  • npm ci --ignore-scripts ในขั้น Audit ใช้เพื่อตรวจเท่านั้น ถ้าโปรเจกต์ต้อง build asset ให้แยกเป็นอีก stage

Secrets ใน CI: ทำให้ถูกตั้งแต่ต้น

Secret ทุกตัวควรเก็บใน Jenkins Credentials แล้วดึงมาใช้ผ่าน credentials() หรือ withCredentials เท่านั้น Jenkins จะ mask ค่าเหล่านี้ใน log ให้ แต่การ mask มีข้อจำกัด จึงต้องเขียนให้ถูกด้วย

  • ใช้ single quote ใน sh เมื่อมี secret เช่น sh 'echo "$REGISTRY_PSW" | docker login ...' ตัวแปรจะถูกขยายโดย shell บน agent ถ้าใช้ double quote ของ Groovy ค่าจะถูกแทนลงในสคริปต์ก่อนส่งไปรัน ซึ่ง Jenkins เองก็เตือนว่าไม่ปลอดภัย
  • ส่งรหัสผ่านผ่าน stdin เช่น --password-stdin แทนการใส่เป็น argument ที่อาจเห็นได้จาก process list
  • ห้าม set -x หรือ echo ค่า secret เพื่อ debug และระวังคำสั่งที่พิมพ์ config ทั้งหมดออกมา
  • จำกัด scope ใช้ credentials ใน stage ที่ต้องใช้เท่านั้น เหมือนตัวอย่างที่ประกาศ REGISTRY ใน environment ของ stage Deploy ไม่ใช่ระดับ pipeline
  • ระวัง pull request จาก fork อย่าให้ build ของ PR จากภายนอกเข้าถึง credentials สำหรับ deploy

และที่สำคัญ อย่าแปะค่า secret จริงลงใน prompt เพื่อให้ AI ช่วยแก้ pipeline ให้ใช้ชื่อ credential ID แทนเสมอ

รูปแบบอันตรายที่ต้องจับให้ได้ตอน review

เมื่อ pipeline ล้ม AI มักเสนอทางแก้ที่ทำให้ "เขียว" เร็วที่สุด ซึ่งไม่ได้แปลว่าถูก รูปแบบที่ควรปฏิเสธทันที:

  • || true หรือ catchError ครอบขั้น test และ audit ทำให้ build ผ่านทั้งที่มีปัญหา
  • composer install --ignore-platform-reqs ซ่อนปัญหาว่า agent ไม่มี extension ที่ production ใช้
  • StrictHostKeyChecking=no ทำให้เสี่ยงต่อการ deploy ไปยังเครื่องปลอม
  • chmod 777 หรือ sudo เพื่อแก้ปัญหาสิทธิ์ไฟล์บน agent
  • ลดระดับ audit จนไม่มีอะไรถูกจับ หรือลบ static analysis ออกเพราะ "error เยอะ"

ถ้าจำเป็นต้องยกเว้นช่องโหว่หรือกฎบางข้อชั่วคราว ให้ระบุเฉพาะจุด พร้อมเหตุผลและวันที่จะกลับมาทบทวน ไม่ใช่ปิดทั้ง stage

ทดสอบ pipeline ก่อนใช้จริง

pipeline ก็เป็นโค้ด ต้องทดสอบเหมือนโค้ดอื่น ลำดับที่แนะนำ:

  1. รันคำสั่งในเครื่องก่อน ทุกคำสั่งใน sh ควรรันผ่านในเครื่อง dev หรือในคอนเทนเนอร์ที่เหมือน agent ก่อนใส่ใน Jenkinsfile
  2. ตรวจ syntax Jenkins มีตัวตรวจ declarative pipeline ผ่าน HTTP endpoint และ CLI ใช้ตรวจก่อน commit ได้
  3. ทดสอบบน branch แยก ให้ Multibranch Pipeline รัน branch ทดลอง และใช้ Replay ใน Jenkins เพื่อลองแก้โดยไม่ต้อง commit ทุกครั้ง
  4. ทดสอบว่ามันล้มได้ ตั้งใจใส่ syntax error หนึ่งไฟล์, ทำให้ test fail หนึ่งข้อ แล้วดูว่า build แดงจริง ถ้าไม่แดง แปลว่า pipeline ไม่ได้ปกป้องอะไร
  5. ตรวจ log ไล่ดู console log ว่าไม่มี secret หลุดออกมา
# ตรวจ syntax ของ Jenkinsfile กับ Jenkins server (ต้องยืนยันตัวตนตามที่ server กำหนด)
curl -X POST --user "$JENKINS_USER:$JENKINS_API_TOKEN" \
  -F "jenkinsfile=<Jenkinsfile" \
  "$JENKINS_URL/pipeline-model-converter/validate"

ดูรายละเอียด syntax และ step ที่มีให้ใช้ได้ที่ jenkins.io และในหน้า Pipeline Syntax ของ Jenkins server ของคุณเอง ซึ่งแสดงเฉพาะ step จาก plugin ที่ติดตั้งจริง

สรุป

  • กำหนดขั้นตอนและเงื่อนไขที่ทำให้ build ล้มก่อน แล้วใช้เป็นทั้ง prompt และเกณฑ์ review
  • บอก AI ว่ามี plugin และ credential ID อะไรบ้าง เพื่อไม่ให้เรียก step ที่ไม่มีอยู่
  • secret ใช้ผ่าน credentials() หรือ withCredentials, ใช้ single quote ใน sh และจำกัด scope ตาม stage
  • ปฏิเสธทางแก้ที่ทำให้เขียวด้วยการปิดการตรวจ เช่น || true หรือปิด host key checking
  • ทดสอบว่า pipeline ล้มได้จริงเมื่อโค้ดผิด ก่อนให้ทั้งทีมใช้

ลองทำ

สร้าง branch ทดลองในโปรเจกต์ของคุณ เขียน prompt ตามโครงในบทความเพื่อให้ AI ร่าง Jenkinsfile แล้ว review ตามรายการรูปแบบอันตราย จากนั้นตั้งใจทำให้แต่ละขั้นล้มทีละขั้น (syntax error, test fail, เพิ่มแพ็กเกจเวอร์ชันที่มีช่องโหว่ที่รู้จักใน branch ทดลอง) และจดว่า pipeline จับได้ครบทุกขั้นหรือไม่

ติดปัญหาตอนทำตาม? ถามในชุมชน

ความคิดเห็น (0)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ