การทำ CI/CD Pipeline ด้วย GitLab Server ของตัวเอง สำหรับ DevOps Team

ภาพจาก https://dzone.com/articles/learn-how-to-setup-a-cicd-pipeline-from-scratch

บทความโดย ผศ.ดร.ณัฐโชติ พรหมฤทธิ์
ภาควิชาคอมพิวเตอร์
คณะวิทยาศาสตร์
มหาวิทยาลัยศิลปากร

เป้าหมายสำคัญของการพัฒนา Software คือ การสร้าง Software ให้ดีขึ้นในเวลาที่น้อยลง ซึ่งในการพัฒนา Software สมัยใหม่ จะมีกระบวนท่าหลักๆ ที่มุ่งไปสู่เป้าหมายดังกล่าวอยู่ 3 กระบวนท่า คือ Agile, CI/CD และ DevOps

Agile จะมุ่งเน้นในการขจัด Process ที่เป็นอุปสรรคต่อการพัฒนา Software โดยการทำให้ Developer และลูกค้าร่วมมือกันจัดส่ง Software อย่างรวดเร็ว ใช้งานได้ดี โดยมีการแบ่งงานออกเป็น Feature แล้วส่งงานเป็นรอบๆ

เพื่อให้สามารถจัดส่ง Software ที่มีคุณภาพแก่ลูกค้าได้รวดเร็ว และนำ Feedback ไปปรับปรุงงานให้ดีขึ้น เราจึงต้องมีเครื่องมือที่ช่วยให้สมาชิกในทีม นำงานของตัวเองมา Integrate และ Deploy ได้แบบอัตโนมัติ (Continuous Integration/Continuous Delivery/Continuous Deployment) โดยไม่ต้องมีคนไปกด Deploy เอง

ภาพจาก https://www.atlassian.com/continuous-delivery/principles/continuous-integration-vs-delivery-vs-deployment

ในแต่ละรอบของการทำ CI/CD อาจประกอบด้วย Job ต่างๆ เช่น การ Compile การ Build การ  Test และการ Deploy ซึ่งเราเรียก Job ทั้งหมดที่ทำงานว่า Pipeline

Pipeline จะถูกกระตุ้นให้ทำงานเมื่อสมาชิกในทีมมีการ Push Source Code ไปยัง Remote Repository ซึ่ง Software ที่จัดการ Git Repository อย่างเช่น GitLab นั้น มีเครื่องมือในการเขียน Script ทำ CI/CD แบบ Built-in ที่ชื่อว่า GitLab CI โดยไม่ต้องใช้โปรแกรมอื่น อย่างเช่น Jenkins ดังนั้น GitLab จึงเป็น Solution อย่างง่ายที่เราจะนำมา Implement CI/CD ในบทความนี้

ซึ่งการ Config GitLab CI ให้สามารถ Integrate และ Deploy Software ได้แบบอัตโนมัติ เป็นหน้าที่ของ DevOps Engineer

หลักๆ แล้ว DevOps Engineer จะเป็นคนเขียน CI/CD Pipeline Script เขียน Dockerfile และ docker-compose.yml วางสภาพแวดล้อมในการพัฒนาโปรแกรม รวมทั้งคอยดูแล Infrastructure และ Config Cluster (เช่น Docker Swarm/Kubernetes) เป็นต้น

สำหรับ Software ที่ Deploy แล้ว DevOps Engineer จะเป็นผู้ Monitor ซึ่งเมื่อพบปัญหาเขาจะต้องรีบแจ้ง Developer ให้หาทางแก้ไข

ดังนั้นหน้าที่ของ DevOps Engineer คือการ Support การทำงานของ Developer ในทำนองเดียวกันกับ System Admin แต่มีขอบเขตกว้างขวางกว่าการทำหน้าที่ Operation

บางบริษัทจะมีตำแหน่ง DevOps Engineer คอยให้การสนับสนุนทีมพัฒนาโดยตรง ขณะที่หลายบริษัทก็ให้ Developer นั่นแหละทำหน้าที่เป็น DevOps Engineer ไปด้วย ซึ่งทีมที่มีวัฒนธรรมในการประสานงานระหว่างการ Development และ Operation ที่ดี หรือทีมที่มีความเป็น DevOps สูง จะเป็นทีมที่ทรงพลังที่จะทำให้การพัฒนา Software ประสบความสำเร็จ โดยเฉพาะการพัฒนา Software แบบ Microservice Architecture

เนื้อหาในบทความนี้ ผู้อ่านจะได้ทดลองทำ CI/CD กับระบบลงทะเบียนนักศึกษาซึ่งถูกออกแบบมาแบบ Microservice Architecture โดยมีการสร้าง Pipeline ที่ประกอบด้วยการ Build Image การทำ Unit Test  และการ Deploy Software บน Swarm Cluster โดยใช้ GitLab Server ของตัวเองครับ

หมายเหตุ 1 สำหรับผู้อ่านใหม่ที่ต้องการจะ Implement ตาม สามารถอ่านบทความ 5 บทความก่อนหน้านี้ได้

หมายเหตุ 2 ให้เปลี่ยน Private IP Address 10.148.0.15 ในบนความนี้ทั้งหมดเป็น IP Address บน Cloud Server ของตัวเอง

Unit Test

สิ่งสำคัญในการพัฒนา Software คือเรื่องของคุณภาพ เพื่อให้ทราบว่า Software ทำงานได้อย่างถูกต้องตามวัตถุประสงค์ที่ต้องการ จึงต้องมีการ Test Software ก่อนนำไปใช้งาน ซึ่งการ Test ในระดับเล็กสุดคือการ Test ในระดับ Function/Method หรือเรียกว่าการทำ Unit Test

การทำ Unit Test เป็นหน้าที่ของ Developer ทุกคนครับ ในการจะเป็น Developer มืออาชีพ ก่อนเขียน Code เราจะต้องทำความเข้าใจปัญหา และเงื่อนไขที่ Function/Method จะตอบสนอง หรือให้ผลลัพธ์เมื่อมีการรับข้อมูลต่างๆ เข้ามา (Test Case) แล้วจึงเขียนโปรแกรม พร้อมกับเขียน Unit Test ให้ครอบคลุมทุก Test Case เท่าที่จะทำได้

ในการทำ Unit Test ที่ดี ควรมีการทดสอบอย่างเฉพาะเจาะจงที่ตัว Function/Method นั้นๆ โดยไม่ขึ้นกับส่วนอื่น (Independent) แต่หากการทดสอบส่วนนั้นจำเป็นต้องใช้ข้อมูลจาก External Data ไม่ว่าจะมาจาก API ภายนอก จาก Database จาก File หรือจาก Environment อื่น เราจะต้องสมมติมันขึ้นมา (Mock)

จะขอยกตัวอย่างการทำ Unit Test ด้วย pytest library ดังต่อไปนี้

  • ติดตั้ง pytest โดยใช้คำสั่ง pip install บน PC หรือ Laptop
pip install pytest
  • สร้าง Project ชื่อ test_project ซึ่งภายใน Folder จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
.
|__ app.py
|__ conftest.py
|__ pytest.ini
|__ src/
|   |__ __init__.py
|   |__ math.py
|__ tests/
    |__ unit_tests/
        |__ math_test.py

app.py เป็นไฟล์หลักของ Project ที่มีการ Import Function add และ sub จาก math.py ใน Folder src ซึ่งอาจมองว่า src เป็น Folder ที่เก็บ Source Code ของ Library หรือ Package สำหรับ Project โดยจะต้องมีการสร้างไฟล์พิเศษที่ชื่อว่า __init__.py สำหรับให้ Python ใช้ค้นหา Package

ในการทดสอบ เราจะต้องนิยาม Test Case ด้วย Test Function ซึ่งการตั้งชื่อ Test Function จะต้องมีคำว่า test อยู่ด้านหน้าเสมอ เช่น def test_add() โดยเราจะเขียน Test Function ไว้ในไฟล์ math_test.py

และเพื่อให้สามารถ Import Function add และ sub จากไฟล์ math.py มาเขียน Test Function  จึงต้องมีการสร้างไฟล์ conftest.py เปล่าๆ ไว้ให้ pytest ใช้ค้นหาด้วย

นอกจากนี้เรายังสามารถ Config เพื่อไม่ให้ pytest แสดง Warning Messages ด้วยไฟล์ pytest.ini ครับ

  • แก้ไขไฟล์ app.py ตามตัวอย่างด้านล่าง
from src import math

a = math.add(1,2)
print(a)
  • แก้ไขไฟล์​ pytest.ini
[pytest]
addopts = -p no:warnings
  • แก้ไข math.py ตามตัวอย่าง
def add(num1, num2):
    return num1 + num2
    
def sub(num1, num2):
    return num1 - num2
  • ทดลองรัน Script ของ Project โดยใช้คำสั่ง python app.py
  • เขียน Test Function 3 Function (test_add, test_sub และ test_sub_return_type) ลงใน math_test.py เพื่อสร้าง Test Case ทั้งหมด 4 Test Case
import pytest
from src import math

@pytest.mark.add
@pytest.mark.parametrize("input1, input2, output", [(1,2,3),(1,6,7)])
def test_add(input1, input2,output):
    add = math.add(input1, input2)
    assert add == output

@pytest.mark.sub
@pytest.mark.parametrize("input1, input2, output", [(1,2,-1)])
def test_sub(input1, input2,output):
    substract = math.sub(input1, input2)
    assert substract == output
    
def test_sub_return_type():
    substract = math.sub(5, 2)
    assert type(substract) is int
  • ไปที่ Root ของ Project แล้วรัน Test Script ใน Folder unit_tests ด้วยคำสั่ง pytest tests/unit_tests -v
pytest tests/unit_tests -v
ผลลัพธ์จากการทำ Unit Test คือ ผ่าน Test Case ทั้งหมด 4 Test Case
  • รัน Test Case เฉพาะ Test Function add ที่มี @pytest.mark.add ด้านบน
pytest tests/unit_tests -v -m add
ผลลัพธ์จากการทำ Unit Test คือ ผ่าน Test Case ทั้งหมด 2 Test Case

ทีนี้เราจะทำ Unit Test กับ OTP Service ของระบบลงทะเบียนนักศึกษากันบ้าง โดยมีขั้นตอนดังนี้

  • Clone otp_dock Project จาก GitLab Server ซึ่งภายใน Project จะประกอบด้วยไฟล์ และ Folder ดังต่อไปนี้
git clone http://gitlab.cpsudevops.com/nuttachot/otp_dock.git
.
├── docker-compose.yml
├── .gitignore
└── python
    ├── Dockerfile
    ├── conftest.py
    ├── pytest.ini
    ├── requirements.txt
    ├── rpc.py
    ├── src
    │   ├── __init__.py
    │   └── otp.py
    ├── tests
    │   └── unit_tests
    │       └── otp_test.py
    └── .env

rpc.py จะเป็นไฟล์หลักของ Project ที่มีการ Import Function generate_otp และ val_email จาก otp.py โดยมีการเขียน Test Functionในไฟล์ otp_test.py ดังต่อไปนี้

import pytest
from src import otp

def test_generate_otp_return_str_type():
    res = otp.generate_otp()
    assert type(res) is str

def test_generate_otp_return_length():
    res = otp.generate_otp()
    assert len(res) == 6

def test_generate_otp_return_str_numeric():
    res = otp.generate_otp()
    assert res.isnumeric() == True

def test_generate_otp_return_random():
    res1 = otp.generate_otp()
    res2 = otp.generate_otp()
    assert res1 != res2

@pytest.mark.parametrize("input, output", [('nuttachot@hotmail.com', True), ('nuttachot', False), ('nuttachot.hotmail.com', False)])
def test_val_email(input, output):
    add = otp.val_email(input)
    assert add == output

def test_get_email_list_key_is_not_empty():
    res = otp.get_email_list_key()
    assert res != None

def test_get_email_list_key_return_string():
    res = otp.get_email_list_key()
    assert type(res) is str

ซึ่งแต่ละ Test Function มีวัตถุประสงค์ คือ

test_generate_otp_return_str_type ทดสอบว่า Function generate_otp จะ Return ชนิดข้อมูลแบบ String ได้หรือไม่ (1 Test Case)

test_generate_otp_return_length ทดสอบว่า Function generate_otp จะ Return ข้อมูลยาว 6 ตัวอักษร  ได้หรือไม่ (1 Test Case)

test_generate_otp_return_str_numeric ทดสอบว่า Function generate_otp จะ Return String ของตัวเลข ได้หรือไม่ (1 Test Case)

test_generate_otp_return_random ทดสอบว่า Function generate_otp จะ Return ข้อมูลแบบ Random ได้หรือไม่ (1 Test Case)

test_val_email เพื่อทดสอบว่า Function val_email สามารถตรวจสอบ Format ของ Email ได้หรือไม่ (3 Test Case)

test_get_email_list_key_is_not_empty เพื่อทดสอบว่า email_list_key จะไม่เป็นค่าว่าง (1 Test Case)

test_get_email_list_key_return_string เพื่อทดสอบว่า email_list_key จะ Return ข้อมูลแบบ String ได้หรือไม่ (1 Test Case)

  • ติดตั้ง validate_email, redis และ python-dotenv Library บน PC หรือ Laptop
pip install validate_email
pip install redis
pip install python-dotenv
  • ไปที่ Folder python แล้วรัน Test Script โดยใช้คำสั่ง pytest tests/unit_tests -v
pytest tests/unit_tests -v
ผลลัพธ์จากการทำ Unit Test คือ ผ่าน Test Case ทั้งหมด 9 Test Case

GitLab Server

ภาพจาก https://www.code-conf.com/sponsors/gitlab

GitLab เป็นเครื่องมือในการจัดการ Version Control ที่มี Built-in CI/CD ซึ่งนอกจากจะใช้งานผ่าน gitlab.com แล้ว เรายังสามารถติดตั้ง GiLab Server ไว้ภายในองค์กรได้แบบง่ายๆ ดังต่อไปนี้

  • Remote Login ไปยัง Cloud Server โดยใช้ ssh
ssh nc-user@labxx.cpsudevops.com
  • สร้าง Project ชื่อ gitlab_dock ซึ่งภายใน Folder จะประกอบด้วยไฟล์ ดังนี้
.
|__ docker-compose.yml
  • แก้ไข docker-compose.yml ตามตัวอย่างด้านล่าง
version: "3"

services:
    redis:
      restart: always
      image: sameersbn/redis:4.0.9-2
      command:
      - --loglevel warning
      volumes:
      - redisdata:/var/lib/redis:Z

    postgresql:
      restart: always
      image: sameersbn/postgresql:10-2
      volumes:
      - postgresqldata:/var/lib/postgresql:Z
      environment:
      - DB_USER=gitlab
      - DB_PASS=password
      - DB_NAME=gitlabhq_production
      - DB_EXTENSION=pg_trgm

    gitlab:
      restart: always
      image: sameersbn/gitlab:12.3.5
      # hostname: gitlab.pjjop.org
      depends_on:
      - redis
      - postgresql
      volumes:
      - gitlabdata:/home/git/data:Z
      
      networks:
      - webproxy
      - default
      
      environment:
      - VIRTUAL_HOST=gitlab.lab20.cpsudevops.com
      - LETSENCRYPT_HOST=gitlab.lab20.cpsudevops.com
      - VIRTUAL_PORT=80

      - DEBUG=false

      - DB_ADAPTER=postgresql
      - DB_HOST=postgresql
      - DB_PORT=5432
      - DB_USER=gitlab
      - DB_PASS=password
      - DB_NAME=gitlabhq_production

      - REDIS_HOST=redis
      - REDIS_PORT=6379

      - TZ=Asia/Bangkok
      - GITLAB_TIMEZONE=Bangkok

      - GITLAB_HTTPS=false
      - SSL_SELF_SIGNED=false

      - GITLAB_HOST=gitlab.lab20.cpsudevops.com
      - GITLAB_PORT=80
      - GITLAB_SSH_PORT=22
      - GITLAB_RELATIVE_URL_ROOT=
      - GITLAB_SECRETS_DB_KEY_BASE=long-and-random-alphanumeric-string
      - GITLAB_SECRETS_SECRET_KEY_BASE=long-and-random-alphanumeric-string
      - GITLAB_SECRETS_OTP_KEY_BASE=long-and-random-alphanumeric-string

      - GITLAB_ROOT_PASSWORD=
      - GITLAB_ROOT_EMAIL=

      - GITLAB_NOTIFY_ON_BROKEN_BUILDS=true
      - GITLAB_NOTIFY_PUSHER=false

      - GITLAB_EMAIL=nuttachot@hotmail.com
      - GITLAB_EMAIL_REPLY_TO=noreply@example.com
      - GITLAB_INCOMING_EMAIL_ADDRESS=reply@example.com

      - GITLAB_BACKUP_SCHEDULE=daily
      - GITLAB_BACKUP_TIME=01:00

      - SMTP_ENABLED=false
      - SMTP_DOMAIN=www.example.com
      - SMTP_HOST=smtp.gmail.com
      - SMTP_PORT=587
      - SMTP_USER=mailer@example.com
      - SMTP_PASS=password
      - SMTP_STARTTLS=true
      - SMTP_AUTHENTICATION=login

      - IMAP_ENABLED=false
      - IMAP_HOST=imap.gmail.com
      - IMAP_PORT=993
      - IMAP_USER=mailer@example.com
      - IMAP_PASS=password
      - IMAP_SSL=true
      - IMAP_STARTTLS=false

      - OAUTH_ENABLED=false
      - OAUTH_AUTO_SIGN_IN_WITH_PROVIDER=
      - OAUTH_ALLOW_SSO=
      - OAUTH_BLOCK_AUTO_CREATED_USERS=true
      - OAUTH_AUTO_LINK_LDAP_USER=false
      - OAUTH_AUTO_LINK_SAML_USER=false
      - OAUTH_EXTERNAL_PROVIDERS=

      - OAUTH_CAS3_LABEL=cas3
      - OAUTH_CAS3_SERVER=
      - OAUTH_CAS3_DISABLE_SSL_VERIFICATION=false
      - OAUTH_CAS3_LOGIN_URL=/cas/login
      - OAUTH_CAS3_VALIDATE_URL=/cas/p3/serviceValidate
      - OAUTH_CAS3_LOGOUT_URL=/cas/logout

      - OAUTH_GOOGLE_API_KEY=
      - OAUTH_GOOGLE_APP_SECRET=
      - OAUTH_GOOGLE_RESTRICT_DOMAIN=

      - OAUTH_FACEBOOK_API_KEY=
      - OAUTH_FACEBOOK_APP_SECRET=

      - OAUTH_TWITTER_API_KEY=
      - OAUTH_TWITTER_APP_SECRET=

      - OAUTH_GITHUB_API_KEY=
      - OAUTH_GITHUB_APP_SECRET=
      - OAUTH_GITHUB_URL=
      - OAUTH_GITHUB_VERIFY_SSL=

      - OAUTH_GITLAB_API_KEY=
      - OAUTH_GITLAB_APP_SECRET=

      - OAUTH_BITBUCKET_API_KEY=
      - OAUTH_BITBUCKET_APP_SECRET=

      - OAUTH_SAML_ASSERTION_CONSUMER_SERVICE_URL=
      - OAUTH_SAML_IDP_CERT_FINGERPRINT=
      - OAUTH_SAML_IDP_SSO_TARGET_URL=
      - OAUTH_SAML_ISSUER=
      - OAUTH_SAML_LABEL="Our SAML Provider"
      - OAUTH_SAML_NAME_IDENTIFIER_FORMAT=urn:oasis:names:tc:SAML:2.0:nameid-format:transient
      - OAUTH_SAML_GROUPS_ATTRIBUTE=
      - OAUTH_SAML_EXTERNAL_GROUPS=
      - OAUTH_SAML_ATTRIBUTE_STATEMENTS_EMAIL=
      - OAUTH_SAML_ATTRIBUTE_STATEMENTS_NAME=
      - OAUTH_SAML_ATTRIBUTE_STATEMENTS_USERNAME=
      - OAUTH_SAML_ATTRIBUTE_STATEMENTS_FIRST_NAME=
      - OAUTH_SAML_ATTRIBUTE_STATEMENTS_LAST_NAME=

      - OAUTH_CROWD_SERVER_URL=
      - OAUTH_CROWD_APP_NAME=
      - OAUTH_CROWD_APP_PASSWORD=

      - OAUTH_AUTH0_CLIENT_ID=
      - OAUTH_AUTH0_CLIENT_SECRET=
      - OAUTH_AUTH0_DOMAIN=
      - OAUTH_AUTH0_SCOPE=

      - OAUTH_AZURE_API_KEY=
      - OAUTH_AZURE_API_SECRET=
      - OAUTH_AZURE_TENANT_ID=

volumes:
  redisdata:
  postgresqldata:
  gitlabdata:
  
networks:
  default:
    external:
      name: gitlab_network
  webproxy:
    external:
      name: webproxy
  • สร้าง Bridge Network โดยตั้งชื่อเป็น gitlab_network
docker network create gitlab_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ของ gitlab_dock ที่กำลังรันทั้งหมด ตามที่ docker-compose.yml ดูแล ด้วย Command line
docker-compose ps
  • ไปยัง GitLab Website จาก URL ด้านล่าง กรอก Password ของ root แล้วกด Change your password
https://gitlab.labxx.cpsudevops.com
  • กดที่แถบ Register เพื่อสร้าง Account ใหม่ที่ไม่ใช่ root ใส่ข้อมูล แล้วกด Register
  • สร้าง Group ชื่อ reg แล้วกด Create group
  • สร้าง Project ใหม่ ชื่อ otp_dock ภายใต้ reg Group กดปุ่ม Create project
  • กลับไปยัง otp_dock Project บน Desktop หรือ Laptop แล้ว Push Source Code ไปยัง https://gitlab.labxx.cpsudevops.com โดยใช้คำสั่งดังนี้
git remote rename origin old-origin
git remote add origin http://gitlab.lab20.cpsudevops.com/reg/otp_dock.git
git push -u origin --all
git push -u origin --tags
  • Refresh Browser แล้วจะเห็น Source Code ที่เพิ่ง Push ขึ้น GitLab Server
  • เปิด otp_dock project ด้วย Visual Studio Code สร้างไฟล์ .gitlab-ci.yml เปล่าๆ สำหรับเขียน CI/CD Pipeline Script

GitLab Runner

ภาพจาก https://gitlab.com/gitlab-org/gitlab-runner

GitLab Runner คือ โปรแกรมที่เอาไว้รัน Job ใน GitLab Pipeline เช่น การ Build การทำ Unit Test หรือการ Deploy Software ซึ่ง Pineline จะถูกกระตุ้นให้ทำงานเมื่อมีการ Push Source Code ไปยัง GitLab Server โดยมีขั้นตอนในการติดตั้ง GitLab Runner ดังต่อไปนี้

ภาพจาก https://dzone.com/articles/learn-how-to-setup-a-cicd-pipeline-from-scratch
  • สร้าง Project ชื่อ runner ซึ่งภายใน Folder จะประกอบด้วยไฟล์ ดังนี้
.
|__ docker-compose.yml
|__ runner_register.sh
  • แก้ไขไฟล์ docker-compose.yml
version: "3"

services:
    gitlab-runner1:
       image: gitlab/gitlab-runner:latest
       container_name: gitlab-runner1
       hostname: gitlab-runner1
       restart: always
       volumes:
          - './gitlab-runner1-config:/etc/gitlab-runner:Z'
          - '/var/run/docker.sock:/var/run/docker.sock'
networks:
  default:
    external:
      name: gitlab_network
  • รัน Container ด้วย docker-compose และกลับไปที่ Terminal โดยใช้ parameter -d
docker-compose up -d
  • ดู Container ที่กำลังรันทั้งหมดที่ docker-compose.yml ดูแล
docker-compose ps

ซึ่งเมื่อดู Logs ของ GitLab Runner จะพบ Error ดังภาพ เนื่องจากมันต้องการไฟล์ config.toml สำหรับการ Config GitLab Runner

  • กลับไปที่ GitLab Website เพื่อคัดลอก registration token จาก reg Group
  • แก้ไขไฟล์ runner_register.sh เพื่อสร้าง config.toml อีกที โดยแก้ registration_token ตามที่คัดลอกมา
#!/bin/sh
# Get the registration token

registration_token=xxxxxxxxxxxxxxx

docker exec -it gitlab-runner1 \
  gitlab-runner register \
    --non-interactive \
    --docker-privileged \
    --registration-token ${registration_token} \
    --locked=false \
    --description docker-stable \
    --url https://gitlab.lab20.cpsudevops.com/ \
    --executor docker \
    --docker-image docker:stable \
    --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
    --docker-network-mode gitlab_network
  • เปลี่ยน Mode ของ runner_register.sh ให้สามารถ Execute ได้
chmod +x runner_register.sh
  • รัน runner_register.sh Script
./runner_register.sh
  • ไปที่ไฟล์ config.toml ใน Folder ~/runner/gitlab-runner1-config แล้วเพิ่ม concurrent = 2 เพื่อทดลองรัน Job แบบ Parallel พร้อมกัน 2 Job
sudo vi config.toml

ซึ่งเมื่อดู Logs ของ GitLab Runner จะพบว่ามันได้โหลดไฟล์ config.toml แล้ว

และเมื่อกลับมาที่ GitLab Website จะพบว่ามีการลงทะเบียน GitLab Runner กับ reg Group  ของเราเรียบร้อยแล้ว

GitLab CI

Pipeline ที่ประกอบด้วย Stage 3 Stage ได้แก่ Build, Test และ Deploy

ปัจจุบันมีเครื่องมือในการทำ CI/CD หลายตัว เช่น Gitlab CI, Jenkins และ Travis-CI โดย Gitlab CI นั้นถูก Built-in กับ GitLab Server ตั้งแต่ต้น

การจะเขียน CI/CD Pipeline Script ด้วย Gitlab CI เราจึงเพียงสร้างไฟล์ .gitlab-ci.yml ขึ้นมาใน Root ของ Project โดยไฟล์นี้จะทำหน้าที่รัน Job ตามที่เรานิยามไว้

Job เป็นหน่วยย่อยที่สุดของ Pipeline โดยเราสามารถจัดกลุ่มของ Job เป็น Stage ซึ่งแต่ละ Job ภายใน Stage เดียวกัน สามารถรันไปพร้อมกันได้ ขณะที่ Job ต่าง Stage จะรันไปตามลำดับ

หลังจากที่เรา Push Source Code ไปยัง GitLab Server แล้ว เจ้า Gitlab ก็จะไปบอก Gitlab Runner ให้ทำงานตามที่สั่งไว้ในไฟล์ .gitlab-ci.yml โดย Gitlab Runner จะสร้าง Container สำหรับรัน Job ต่างๆ อีกที

โดยมีขั้นตอนการทำ CI/CD ดังต่อไปนี้

  • แก้ไขไฟล์ .gitlab-ci.yml ด้วย Visual Studio Code
image: docker/compose:latest
variables: 
   DSSH: "ssh nc-user@lab20.cpsudevops.com"
   DEPLOY_SERVER: lab20.cpsudevops.com

stages:
   - build 
   - test
   - deploy

build:
   stage: build
   only:
      - master
   before_script:
      - echo $REGISTRY_URL
      - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL

   script:
      - docker-compose build
      - docker-compose push

unit_test1:
   image: python:3.7.3-alpine3.8
   stage: test
   only:
      - master 
   script: 
      - pwd
      - ls
      - apk add build-base
      - pip install -r ./python/requirements.txt
      - pip install pytest
      - cd python && pytest tests/unit_tests -v

unit_test2:
   stage: test
   only:
      - master
   script:
      - echo 'Hello Unit Test 2'
deploy:
   image: gitlab/dind
   stage: deploy
   only:
      - master
   before_script:
      - eval "$(ssh-agent -s)"
      - echo "$SERVER_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      - ssh-keyscan -H $DEPLOY_SERVER >> ~/.ssh/known_hosts

      - echo $REGISTRY_URL 
      - $DSSH sudo docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL

   script:
      - $DSSH pwd 
      - $DSSH ls
      - $DSSH sudo docker-compose -f ./otp_dock/docker-compose.yml pull
      - $DSSH git -C ./otp_dock pull https://$GITLAB_USER:$GITLAB_PASSWORD@gitlab.lab20.cpsudevops.com/${CI_PROJECT_PATH}.git master
      - $DSSH sudo docker stack deploy --compose-file ./otp_dock/docker-compose.yml --with-registry-auth otp_dock  

จากภาพด้านบน เราแบ่ง CI/CD Pipeline Script ออกเป็น 6 ส่วน แต่ละส่วนทำหน้าที่ดังนี้

Script #1

image: docker/compose:latest
variables: 
   DSSH: "ssh nc-user@lab20.cpsudevops.com"
   DEPLOY_SERVER: lab20.cpsudevops.com

มีหน้าที่ในการ กำหนด Image ของ Container สำหรับรัน Job และ ตัวแปร ที่จะใช้ใน Pipeline

Script #2

stages:
   - build 
   - test
   - deploy

สำหรับ นิยาม stage ทั้งหมด 3 stage ได้แก่ Build, Test และ Deploy

Script #3

build:
   stage: build
   only:
      - master
   before_script:
      - echo $REGISTRY_URL
      - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL

   script:
      - docker-compose build
      - docker-compose push

สำหรับ นิยาม build Job ใน Build Stage ซึ่ง Job จะถูกรันเมื่อมีการ Push Code ไปยัง Master Branch เท่านั้น

เมื่อ Build Image เสร็จแล้ว จะมีการนำ Image ไปฝากไว้ (Push) ที่ Registry Server ตามที่อยู่ในตัวแปร REGISTRY_URL (registry.pjjop.org)

ซึ่งก่อนจะมีการ Push Image เราจะต้อง Login กับ Registry Server ด้วย Username และ Password โดยใช้ข้อมูลในตัวแปร REGISTRY_USER และ REGISTRY_PASSWORD โดยเราจะนิยามตัวแปรทั้ง 3 ตัว บน GitLab Server แทนที่จะนิยามไว้ตรงส่วนต้นของ Script เพื่อไม่ให้ทุกคนมองเห็น Password ได้โดยตรงจากไฟล์ .gitlab-ci.yml ครับ

Script #4

unit_test1:
   image: python:3.7.3-alpine3.8
   stage: test
   only:
      - master
   script:
      - pwd
      - ls
      - apk add build-base
      - pip install -r ./python/requirements.txt
      - pip install pytest
      - cd python && pytest tests/unit_tests -v

สำหรับ นิยาม unit_test1 Job ใน Test Stage โดยในการทดสอบจะมีการใช้ Image ตัวเดียวกันกับ Image ของ Project (python:3.7.3-alpine3.8)

Script #5

unit_test2:
   stage: test
   only:
      - master
   script:
      - echo 'Hello Unit Test 2'

สำหรับ นิยาม unit_test2  Job ใน Test Stage โดย Job นี้มีหน้าที่ print ข้อความ Hello Unit Test 2 และทำงานไปพร้อมกับ unit_test1 Job1 ครับ

Script #6

deploy:
   image: gitlab/dind
   stage: deploy
   only:
      - master
   before_script:
      - eval "$(ssh-agent -s)"
      - echo "$SERVER_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
      - mkdir -p ~/.ssh
      - chmod 700 ~/.ssh
      - ssh-keyscan -H $DEPLOY_SERVER >> ~/.ssh/known_hosts

      - echo $REGISTRY_URL 
      - $DSSH sudo docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD $REGISTRY_URL

   script:
      - $DSSH pwd 
      - $DSSH ls
      - $DSSH sudo docker-compose -f ./otp_dock/docker-compose.yml pull
      - $DSSH git -C ./otp_dock pull https://$GITLAB_USER:$GITLAB_PASSWORD@gitlab.lab20.cpsudevops.com/${CI_PROJECT_PATH}.git master
      - $DSSH sudo docker stack deploy --compose-file ./otp_dock/docker-compose.yml --with-registry-auth otp_dock 

สำหรับ นิยาม deploy Job ภายใน Deploy Stage โดยใช้ Image gitlab/dind สร้าง Container ซึ่งจะมีเครื่องมือที่จำเป็นในการช่วย Deploy Software บน Cloud Server (labxx.cpsudevops.com)

เพื่อจะ Deploy Software เราจะต้อง Remote ไปยัง Cloud Server โดยใช้ Private/Public Key ที่ต้องมีการสร้างไว้ และ Pull Image จาก Registry Server ไปยัง Cloud Server แล้วจึงทำ Git pull เพื่อ Update Source Code ให้เป็นปัจจุบัน ก่อนจะใช้คำสั่ง docker stack deploy เพื่อ Deploy OTP Service บน Swarm Cluster ต่อไป

ดังนั้นในส่วนนี้เราจะต้องมี Private Key, GitLab User และ GitLab Password โดยใช้ข้อมูลในตัวแปร SERVER_PRIVATE_KEY, GITLAB_USER และ GITLAB_PASSWORD ที่นิยามไว้บน GitLab Server เช่นเดียวกัน

  • Remote Login ไปยัง Cloud Server โดยใช้ ssh
ssh nc-user@labxx.cpsudevops.com
  • สร้าง Private/Public Key เพื่อการเข้าถึง Cloud Server โดยใช้คำสั่ง ssh-keygen
ssh-keygen
  • นำ Public Key ไปเก็บในไฟล์​ authorized_keys
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
  • คัดลอก Private Key โดยพิมพ์คำสั่ง cat ~/.ssh/id_rsa เลือก Private Key แล้วกด Ctrl+C
cat ~/.ssh/id_rsa
  • ไปยังหน้า reg Group ของ GitLab Server
  • เลือกเมนู Settings -> CI/CD
  • สร้างตัวแปร SERVER_PRIVATE_KEY (Key) โดยกำหนด Value เป็น Private Key ที่คัดลอกมา แล้วกด Save variables
  • สร้างตัวแปร GITLAB_USER และ GITLAB_PASSWORD โดยกำหนด Value ด้วย Username และ Password ของตัวเอง แล้วกด Save variables
  • สร้างตัวแปร REGISTRY_USER, REGISTRY_PASSWORD และ REGISTRY_URL โดยกำหนดค่าเป็น admin, devops101 และ registry.pjjop.org แล้วกด Save variables
  • Backup Folder otp_dock เดิม บน Cloud Server แล้ว Clone Source Code ของ otp_dock จาก GitLab Server ของเรา
sudo mv otp_dock backup_otp_dock
git clone http://gitlab.labxx.cpsudevops.com/reg/otp_dock.git
  • กลับมาที่ Desktop หรือ Labtop แล้ว Push Source Code ขึ้น GitLab Server
git add .
git commit -m 'first CI/CD'
git push
  • ดู Pipeline โดยการกดที่เมนู CI/CD -> Pipelines แล้วกด running
  • เมื่อรันเสร็จ กดที่แถบ Jobs จะเห็นเวลาของ 1 Pipeline ซึ่งใช้เวลาไปประมาณ 2.32 นาที
  •  ตรวจสอบ Code ใน Container ที่เพิ่ง Deploy จาก Portainer
  • ทดสอบการลงทะเบียนตาม Flow ที่ได้ออกแบบและพัฒนา

ซึ่งระบบสามารถทำงานได้ครบถ้วนตามที่ออกแบบไว้ครับ

ขอขอบคุณ Nipa.Cloud ที่ให้การสนับสนุน Environment ในการเรียนการสอน
รายวิชา Dev-Ops and Cloud Engineering 101