ลอง build Docker Images with GitLab CI/CD ไปยัง GCR

Nawaphon Thiandusit
CJ Express Tech (TILDI)
6 min readMar 7, 2023

บทความนี้เราจะมาแชร์วิธีเขียนไฟล์ .gitlab-ci.yml เบื้องต้นสำหรับการ build image ด้วย GitLab CI/CD ไปยัง google container repository (GCR)

ขอเกริ่นถึงตัว docker , gitlab และ google container repository (GCR) สักนิดนึงเผื่อให้คนที่ยังไม่รู้ได้รู้และเพื่อจะได้เห็นภาพตรงกัน ส่วนใครที่รู้แล้วข้ามไปเลยได้นะครับ

Table of contents

- docker คืออะไร
- Gitlab คืออะไร?
- google container repository (GCR) คืออะไร?
- Gitlab CI/CD คืออะไร?
- สร้างไฟล์ .gitlab-ci.yml
- Optimize GitLab CI/CD configuration files
- Run and Monitor

Docker คืออะไร

Docker คือ เครื่องมือแบบ open-source ที่ช่วยจำลองสภาพแวดล้อม (environment) ในการรัน service หรือ server ตามหลักการสร้าง container เพื่อจัดการกับ library ต่างๆ อีกทั้งยังช่วยจัดการในเรื่องของ version control เพื่อง่ายต่อการจัดการกับปัญหาต่าง

Gitlab คืออะไร

Gitlab เป็น DevOps Platform ที่ช่วยในการจัดการ source code และ deploy application ซึ่งใน Gitlab ก็จะมีของเล่นมากมาย เช่น Gitlab Repository, Gitlab Registry, Gitlab CI/CD, Gitlab Runners

  • Gitlab Repository — เก็บ source code
  • Gitlab Registry — เก็บ Docker Image
  • Gitlab CI/CD — กระบวนการที่ช่วยในการ build & deploy
  • Gitlab Runners — ตัว gitlab-runner นั้นมีวิธีกาทำงานคล้ายกันกับ Jenkins เลยนั่นก็คือการนำ script ที่เราเขียนเป็น pipeline ไว้มาทำงานทีละคำสั่ง

ถ้าอยากศึกษา เพิ่มเติมเกี่ยวกับ docker gitlab สามารถศึกษาต่อได้ที่นี้เลยครับ

google container repository (GCR) คืออะไร

Google Container Registry เป็นอีกหนึ่งในบริการของ Google cloud ที่ช่วยจัดการ Docker images

โดยมีความสามารถหลัก ๆ คือ

  1. Secure, private Docker registry
  2. Build and deploy automatically
  3. In-depth vulnerability scanning
  4. Lock down risky images
  5. Native Docker support
  6. Fast, high-availability access

Gitlab CI/CD คืออะไร

Continuous Integration(CI) คือ กระบวนการรวม source code ของคนในทีมพัฒนาเข้าด้วยกัน และมีการ test ด้วย test script เพื่อให้แน่ใจว่าไม่มี error ในส่วนใดๆ ของโปรแกรม แล้วถึงทำการ commit ไปที่ branch master อีกต่อนึง

Continuous Deployment และ Continuous Delivery (CD) เป็นกระบวนการส่งออกโค้ดหรือระบบ ที่พัฒนาขึ้นไปยังระบบจริง หรือที่เรียกว่า Production โดยการทำงานของ CD จะเป็นการทำงานแบบอัตโนมัติ

แต่ Continuous Delivery จะต่างจาก Continuous Deployment ต่างกันตรงที่จะไม่มีการ deploy ขึ้น production ขึ้นในทันที แต่จะเป็นการทำ manual deploy เช่น ถ้าเป็นอย่างในงานของ data engineer อาจจะต้องรอpipelineรันให้เสร็จก่อน แล้วค่อยกดdeploy

สร้างไฟล์ .gitlab-ci.yml

ต่อมาเราจะมาสร้างไฟล์ .gitlab-ci.yml ซึ่งทุกครั้งที่เรา push ขึ้นมา gitlab จะมาอ่านไฟล์นี้ทุกครั้ง ภายในไฟล์ .gitlab-ci.yml สามารถประกอบด้วยกันหลายส่วน ในที่ตัวอย่างนี้จะมี2ส่วน

ส่วนแรก คือ stages

stages:
- precheck
- build
- test
- dockerize
- predeploy
- deploy
- postdeploy

เป็นการกำหนด flow การทำงานของ pipeline นี้ให้กับ job ต่างๆ โดยมันจะเรียงตามลำดับที่เราได้กำหนดไว้ เช่น precheck >> build >> test >> dockerize >> predeploy >> deploy >> postdeploy

โดยปกติถ้าเราไม่กำหนด stages โดย default ของgitlabจะเป็น

stages:
- ".pre"
- build
- test
- deploy
- ".post"

ส่วนที่สอง คือ job

เป็นการกำหนดว่าจะให้ทำอะไรใน stage นั้นๆ

ตัวอย่าง code ในไฟล์ .gitlab-ci.yml

build_image_development:
rules:
- if: "$CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+(-dev)$/"
when: manual
variables:
DOCKER_IMAGE_NAME: "${REPOSITORY_URL}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://localhost:2375
DOCKER_FILE_PATH: Dockerfile
DOCKER_BUILD_DIR: "."
DOCKER_BUILD_ARGS: ""
image: docker:19.03.1
services:
- docker:19.03.1-dind
stage: dockerize
before_script:
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}.gcr.io
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}-docker.pkg.dev
script:
- docker build -f ${DOCKER_FILE_PATH} --tag ${DOCKER_IMAGE_NAME} ${DOCKER_BUILD_DIR}
${DOCKER_BUILD_ARGS}
- docker push ${DOCKER_IMAGE_NAME}
environment:
name: development

build_image_production:
rules:
- if: "$CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+$/"
when: manual
variables:
DOCKER_IMAGE_NAME: "${REPOSITORY_URL}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://localhost:2375
DOCKER_FILE_PATH: Dockerfile
DOCKER_BUILD_DIR: "."
DOCKER_BUILD_ARGS: ""
image: docker:19.03.1
services:
- docker:19.03.1-dind
stage: dockerize
before_script:
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}.gcr.io
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}-docker.pkg.dev
script:
- docker build -f ${DOCKER_FILE_PATH} --tag ${DOCKER_IMAGE_NAME} ${DOCKER_BUILD_DIR}
${DOCKER_BUILD_ARGS}
- docker push ${DOCKER_IMAGE_NAME}
environment:
name: production

เราสามารถ set configuration ได้เยอะมาก ถ้าใครอยากsetเพิ่มก็สามารถไปดูที่document ของ gitlab ที่ตรงนี้ได้เลย

ในตัวอย่างเรา set ไว้ประมาณนี้

  1. rules จะเป็นตัวกำหนดให้ ตัว pipeline จะรันเมื่อไหร่ โดยปกติถ้าไม่ได้กำหนด pipeline จะรันทุกครั้งที่ commit แต่ในที่นี้จะให้มันรันทุกครั้งที่มีการสร้าง tag เช่น v0.0.1-dev หรือ v0.0.1 ส่วน when manual เป็นการใส่เงื่อนไขเพิ่มที่จะ manual รัน job เอาไว้ประยุกต์ใช้ เวลาที่จะ deploy จะได้ไม่ชนกับช่วงที่รัน pipeline ของทีมหรือรอให้เพื่อนหรือsenior มาตรวจก่อนจะDeploy
  2. variables เอาไว้ set parameter ที่จะส่งไปรันใน script และ มีการเอา Predefined CI/CD variables มาใช้ เช่น CI_COMMIT_TAG (ชื่อtag) ใช้ได้เฉพาะ pipeline ที่เรามี tag ไว้เท่านั้น โดยมันจะเปลี่ยนค่าตามชื่อ tag สามารถใช้ Predefined CI/CD variables อื่นๆ ได้อีกดูได้จาก docs ของ gitlab Predefined CI/CD variablesได้เลย
  3. image คือ การกำหนด Image ของ Container สำหรับรัน Job
  4. services คือ การใช้ Docker services images เช่น เมื่อเราต้องการใช้ imageอีกตัวที่พิเศษหรือเฉพาะ ให้เราระบุเพิ่มเข้าไป มันไปสร้าง container อีกตัว และ container ทั้ง2 มันจะคุยกันเองเวลาเรารัน jobs ในที่นี้ ใช้ docker:19.03.1-dind เพื่อช่วยในการ build docker (docs_gitlab_services กับ docs_gitlab_dind)

5. stages คือการกำหนด stage ว่า job นี้อยู่ stage อะไร

6. before_script คือการกำหนดว่าจะรันอะไรก่อนรัน script

7. script คือ script ที่จะรัน

8. environment คือ การกำหนด environment ที่จะรัน หรือ deploy job ในตัวอย่างนี้เรากำหนด REPOSITORY_URL และ GOOGLE_APPLICATION_CREDENTIALS ของแต่ละ environment ต่างกันเพราะจะเอา image ไปเก็บไว้คนละโปรเจ็ค โดยจะที่ environment ไหนขึ้นอยู่กับการสร้าง tag ของเรา เช่น tag v0.0.1-dev รันที่ environment development และ v0.0.1 รันที่ environment production

อย่าลืมไปสร้าง Repository ที่ GCR และสร้าง Service Account เพื่อนำ JSON Service Key Secret มาใส่ด้วยนะ

พอเอาทั้งสองส่วนมาร่วมกันจะได้ไฟล์หน้าตาประมาณนี้

stages:
- precheck
- build
- test
- dockerize
- predeploy
- deploy
- postdeploy

build_image_development:
rules:
- if: "$CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+(-dev)$/"
when: manual
variables:
DOCKER_IMAGE_NAME: "${REPOSITORY_URL}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://localhost:2375
DOCKER_FILE_PATH: Dockerfile
DOCKER_BUILD_DIR: "."
DOCKER_BUILD_ARGS: ""
image: docker:19.03.1
services:
- docker:19.03.1-dind
stage: dockerize
before_script:
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}.gcr.io
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}-docker.pkg.dev
script:
- docker build -f ${DOCKER_FILE_PATH} --tag ${DOCKER_IMAGE_NAME} ${DOCKER_BUILD_DIR}
${DOCKER_BUILD_ARGS}
- docker push ${DOCKER_IMAGE_NAME}
environment:
name: development

build_image_production:
rules:
- if: "$CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+$/"
when: manual
variables:
DOCKER_IMAGE_NAME: "${REPOSITORY_URL}/${CI_PROJECT_NAME}:${CI_COMMIT_TAG}"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
DOCKER_HOST: tcp://localhost:2375
DOCKER_FILE_PATH: Dockerfile
DOCKER_BUILD_DIR: "."
DOCKER_BUILD_ARGS: ""
image: docker:19.03.1
services:
- docker:19.03.1-dind
stage: dockerize
before_script:
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}.gcr.io
- cat ${GOOGLE_APPLICATION_CREDENTIALS} | docker login -u _json_key --password-stdin
https://${GCP_REPOSITORY_REGION}-docker.pkg.dev
script:
- docker build -f ${DOCKER_FILE_PATH} --tag ${DOCKER_IMAGE_NAME} ${DOCKER_BUILD_DIR}
${DOCKER_BUILD_ARGS}
- docker push ${DOCKER_IMAGE_NAME}
environment:
name: production

ถ้าดูfile YML ด้านบนมันยาวมากเลย เราสามารถปรับให้มันสั้นและลดการเขียนซ้ำได้ในกรณีที่เราต้องการbuild image ในโปรเจ็กอื่นๆแต่อาจจะเปลี่ยน แค่ชื่อ images หรือ REPOSITORY_URL

Optimize GitLab CI/CD configuration files

เป็นการช่วยลดความซับซ้อนของ code และ การเขียน configuration ซ้ำๆ ทางทีม Machine Learning engineer ได้ทำเป็น template ไว้แล้ว จะแบ่งstages ประมาณนี้ แต่ไม่จำเป็นต้องมีทุก stage มีแค่เฉพาะที่ใช้ก็พอ

ตัวอย่างไฟล์ที่ทำเสร็จจะออกมาเป็นประมาณนี้

include:
- project: "cjexpress/tildi/infra/ml-engineer/devops-template"
ref: main
file:
- "base.yml" # stages
- "sca/pylint.yml"
- "/dockerize/push_image_gcp.yml"

# utilizing CJ Runner
default:
tags:
- cjexpress-internal

pylint:
image: python:3.10

# development
build_image_development:
stage: dockerize
rules:
- if: $CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+(-dev)$/
extends:
- build_image
environment:
name: development

# production
build_image_production:
stage: dockerize
rules:
- if: $CI_COMMIT_TAG =~ /^(?:v)[0-9]+.[0-9]+.[0-9]+$/
extends:
- build_image
environment:
name: production

ถ้าเข้าไปดูในtemplate จุดไหนไม่เหมือนกับที่เราต้องการสามารถoverwriteทับในไฟล์ .gitlab-ci.yml ของเราได้เลย

ถ้าใครอยากเอาไปทำบ้างลองศึกษา docs อันนี้ดูแล้วลองไปประยุกต์กันดูได้เลย

Run and Monitor

หลังจากที่เราเขียน pipeline script เสร็จแล้ว เราก็pushขึ้น gitlab แล้วcreate tag ตามเงื่อนไขที่เรากำหนดไว้ใน script ได้เลย มันก็จะauto run pipeline ให้เอง

เราสามารถตรวจสอบ logs ใน Container ได้ด้วยนะ

ถ้ารันเสร็จแล้วควรจะไปเช็คที่ google container repository (GCR) สักหน่อยเพราะว่าถ้าชื่อ DOCKER_IMAGE_NAME ผิด มันหายไปแบบไร้ร่องรอยเลย เหมือนไม่มีอะไรเกิดขึ้น

เท่านี้ก็สามารถ build docker image ไปยัง GCR ได้เรียบร้อยแล้วครับ

References

--

--