歡迎光臨
每天分享高質量文章

基於 GitLab 的 CI 實踐

本文講述 GitLab CI 的架構及其能力特性,分析它在 DevOps 實踐中的作用。 通過分析 Docker In Docker 的技術細節,詳細講述 CI 實踐以及在生產環境中的所做的優化,包括但不限於鏡像倉庫等,以達到數倍的性能提升。
本次分享內容以 GitLab Community Edition 11.0.4 edb037c 為例。
為何選擇 GitLab CI?

認識 GitLab CI
什麼是 GitLab CI?
GitLab CI 是 GitLab 為了提升其在軟體開發工程中作用,完善 DevOps 理念所加入的 CI/CD 基礎功能。可以便捷的融入軟體開發環節中。通過 GitLab CI 可以定義完善的 CI/CD Pipeline。

優勢
  • GitLab CI 是預設包含在 GitLab 中的,我們的代碼使用 GitLab 進行托管,這樣可以很容易的進行集成

  • GitLab CI 的前端界面比較美觀,容易被人接受

  • 包含實時構建日誌,容易追蹤

  • 採用 C/S 的架構,可方面的進行橫向擴展,性能上不會有影響

  • 使用 YAML 進行配置,任何人都可以很方便的使用

重點概念

Pipeline

Pipeline 相當於一個構建任務,裡面可以包含多個流程,如依賴安裝、編譯、測試、部署等。
任何提交或者 Merge Request 的合併都可以觸發 Pipeline

Stages
Stage 表示構建的階段,即上面提到的流程。
  • 所有 Stages 按順序執行,即當一個 Stage 完成後,下一個 Stage 才會開始

  • 任一 Stage 失敗,後面的 Stages 將永不會執行,Pipeline 失敗

  • 只有當所有 Stages 完成後,Pipeline 才會成功

Jobs
Job 是 Stage 中的任務。
  • 相同 Stage 中的 Jobs 會並行執行

  • 任一 Job 失敗,那麼 Stage 失敗,Pipeline 失敗

  • 相同 Stage 中的 Jobs 都執行成功時,該 Stage 成功

好的,基本的概念已經和大家介紹了, 大家可以發現,上面說的概念,沒有提到任務的實際執行者,那任務在哪裡執行呢?
GitLab Runner
Runner 是任務的實際執行者, 可以在 MacOS/Linux/Windows 等系統上運行。使用 Golang 進行開發。 同時也可部署在 Kubernetes 上。

註冊

docker run --rm -t -i -v /path/to/config:/etc/gitlab-runner --name gitlab-runner gitlab/gitlab-runner register \
  --executor "docker" \
  --docker-image alpine:3 \
  --url "https://gitlab.com/" \
  --registration-token "PROJECT_REGISTRATION_TOKEN" \
  --description "docker-runner" \
  --tag-list "dev" \
  --run-untagged \
  --locked="true"
上面的示例為將 Runner 註冊為一個容器, 當然大家也可以直接在物理機上執行。 在物理機上的註冊方式與註冊為容器大致相同。
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "PROJECT_REGISTRATION_TOKEN" \
  --executor "docker" \
  --docker-image alpine:3 \
  --description "docker-runner" \
  --tag-list "docker,aws" \
  --run-untagged \
  --locked="false" \

(這段代碼來自官方文件)
接下來,我們來看下 Runner 的型別, 以便在使用時進行區分。

型別
  • Shared:Runner runs jobs from all unassigned projects

  • Group:Runner runs jobs from all unassigned projects in its group

  • Specific:Runner runs jobs from assigned projects

  • Locked:Runner cannot be assigned to other projects

  • Paused:Runner will not receive any new jobs

配置
首先最外層的是全域性配置, 預設會有:
concurrent = 1
check_interval = 0

這兩個。 比較需要關註的是下麵幾個:
全域性配置
  • concurrent:併發數,0 為無限制。

  • sentry_dsn:與 Sentry 聯動,可以將異常等收集至 Sentry 中。

  • listen_address:暴露出 metrics 供 Prometheus 監控。

Executor
  • Shell

  • Docker(本次的分享內容)

  • Docker Machine and Docker Machine SSH(autoscaling)

  • Parallels

  • VirtualBox

  • SSH

  • Kubernetes(推薦)

詳解 Docker In Docker

概述
Docker In Docker 簡稱 dind,在 GitLab CI 的使用中,可能會常被用於 Service 的部分。 dind 表示在 Docker 中實際運行了一個 Docker 容器,或 Docker daemon。
其實如果只是在 Docker 中執行 Docker 命令, 那裝個二進制檔案即可。但是如果想要運行 Docker daemon(比如需要執行 docker info)或者訪問任意的設備都是不允許的。
Docker 在 run 命令中提供了兩個很重要的選項 –privileged 和 –device , 另外的選項比如 –cap-add 和 –cap-drop 跟權限也很相關,不過不是今天的重點,按下不表。
–device 選項可以供我們在不使用 –privileged 選項時,訪問到指定設備,比如 docker run –device=/dev/sda:/dev/xvdc –rm -it ubuntu fdisk /dev/xvdc 但是這也只是有限的權限, 我們知道 Docker 的技術實現其實是基於 CGroup 的資源隔離,而 –device 卻不足於讓我們在容器內有足夠的權限來完成 Docker daemon 的啟動。
在 2013年 左右, –privileged 選項被加入 Docker, 這讓我們在容器內啟動容器變成了可能。 雖然 –privileged 的初始想法是為了能讓容器開發更加便利,不過有些人在使用的時候,其實可能有些誤解。
有時候,我們可能只是想要能夠在容器內正常的build 鏡像,或者是與 Docker daemon 進行交互,例如 Docker images 等命令。 那麼,我們其實不需要 dind, 我們需要的是 Docker Out Of Docker,即 dood,在使用的時候,其實是將 docker.sock 掛載入容器內。
例如, 使用如下命令:
sudo docker run --rm -ti -v /var/run/docker.sock:/var/run/docker.sock taobeier/docker /bin/sh

在容器內可進行正常的 Docker images 等操作, 同時需要註意,在容器內的動作,將影響到 宿主機上的 Docker daemon。

 如何實現

  • 創建組和用戶,並將用戶加入該組。 使用 groupadd 和 useradd 命令。

  • 更新 subuid 和 subgid 檔案, 將新用戶和組配置到 /etc/subgid 和 /etc/subuid 檔案中。 subuid 和 subgid 規定了允許用戶使用的從屬 ID。

  • 接下來需要掛載 /sys/kernel/security 為 securityfs 型別可以使用 mountpoint 命令進行測試 mountpoint /sys/kernel/security 如果不是一個掛載點, 那麼使用 mount -t securityfs none /sys/kernel/security 進行掛載。如果沒有掛載成功的話, 可以檢查是否是 SELinux 或者 AppArmor 阻止了這個行為。這裡詳細的安全問題,可以參考 Linux Security Modules (LSM)。

  • 接下來允許 dockerd 命令啟動 daemon 即可, dockerd –host=unix:///var/run/docker.sock –host=tcp://0.0.0.0:2375 即可將docker daemon 監聽至 2375 端口。


簡單做法
可以直接使用 Docker 官方鏡像倉庫中的 docker:dind 鏡像, 但是在運行時, 需要指定 –privileged 選項。
CI 實踐

Runner 實踐
看 Runner 部分的配置:
[[runners]]
  name = "docker"
  url = "https://gitlab.example.com/"
  token = "TOKEN"
  limit = 0
  executor = "docker"
  builds_dir = ""
  shell = ""
  environment = ["ENV=value""LC_ALL=en_US.UTF-8"]
  clone_url = "http://172.17.0.4"

由於網絡原因, clone_url 可以配置為可訪問的地址,這樣代碼 clone 的時候,將會使用配置的這個地址。實際請求為 http://gitlab-ci-token:TOKEN@172.17.0.4/namespace/project.git。
再看一下 runners.docker 的配置,這部分將影響 Docker 的實際運行:
[runners.docker]
  host = ""
  hostname = ""
  tls_cert_path = "/home/tao/certs"
  image = "docker"
  dns = ["8.8.8.8"]
  privileged = false
  userns_mode = "host"
  devices = ["/dev/net/tun"]
  disable_cache = false
  wait_for_services_timeout = 30
  cache_dir = ""
  volumes = ["/data""/home/project/cache"]
  extra_hosts = ["other-host:127.0.0.1"]
  services = ["mongo""redis:3"]
  allowed_images = ["go:*""python:*""java:*"]

DNS,Privileged,extra_hosts,Services 比較關鍵, 尤其是在生產中網絡情況多種多樣, 需要格外關註。 至於 Devices 配置 ,在今兒分享的一開始已經講過了, allowed_images 的話, 是做了個限制。
上面幾個配置項, 用過 Docker 的同學,應該很容易理解。 我們來看下 Services 這個配置項:
image: registry.docker-cn.com/taobeier/docker

variables:
  DOCKER_DRIVER: overlay2    # overlay2 is best bug need kernel >= 4.2

services:
  - name: registry.docker-cn.com/taobeier/docker:stable-dind
    alias: docker

stages:
  - build
  - deploy


build_and_test:
  stage: build
  tags:
    - build
  script:
    # change repo
    #- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
    # 使用預設官方源 apk 耗時 7min 30s.  修改後 耗時 18s
    - ping -c 1 docker
    - ping -c 1 registry.docker-cn.com__taobeier__docker
    - ipaddr
    - apk add --no-cache py-pip 
    # 使用預設耗時 1 min 15s.  修改後耗時 43s
    - pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple docker-compose
    - docker-compose up -d
    - docker-compose run --rm web pytest -s -v tests/test_session.py

deploy:
  image: "registry.docker-cn.com/library/centos"
  stage: deploy
  tags:
    - deploy
  script:
    # install ssh client
    - 'ssh-agent || (yum install -y openssh-clients)'
    # run ssh-agent
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    # create ssh dir
    - mkdir -p ~/.ssh
    - chmod 700 ~/
.ssh
    # use ssh-keyscan to get key
    - ssh-keyscan -p $SSH_PORT $DEPLOY_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts

    # - ssh -p $SSH_PORT $DEPLOY_USER@$DEPLOY_HOST ls
    - rm -rf .git
    - scp -r -P $SSH_PORT . $DEPLOY_USER@$DEPLOY_HOST:~/we/

Services 的本質其實是使用了 Docker 的 –link ,我們來看下它如何工作:
Docker Executor 如何工作
  • 創建 service 容器(已經配置在 service 中的鏡像)

  • 創建 cache 容器(儲存已經配置在 config.toml 的捲和構建鏡像的 Dockerfile)

  • 創建 build 容器 並且 link 所有的 service 容器

  • 啟動 build 容器 並且發送 job 腳本到該容器中

  • 執行 job 的腳本

  • 檢出代碼:/builds/group-name/project-name/

  • 執行 .gitlab-ci.yml 中定義的步驟

  • 檢查腳本執行後的狀態碼,如果非 0 則構建失敗

  • 移除 build 和 service 容器

私有鏡像源
用戶認證需要 GitLab Runner 1.8 或更高版本,在 0.6 ~ 1.8 版本之間的 Runner 需要自行去 Runner 的機器上手動執行。
預設情況下,如果訪問的鏡像倉庫需要認證的話, GitLab Runner 會使用 DOCKER_AUTH_CONFIG 變數的作為認證的憑證。
註意:DOCKER_AUTH_CONFIG 是完整的 docker auth 憑證,也就是說,它應該和我們 ~/.docker/config.json 中的內容一致,例如:
{
     "auths": {
         "registry.example.com": {
             "auth""5oiR5piv5byg5pmL5rab"
         }
     }
 }

簡單的做法就是,我們在本地/服務器上執行 docker login 私有鏡像源 登錄成功後,將 ~/.docker/config.json 的檔案內容直接複製,作為我們的變數的值。
或者是 echo -n ‘用戶名:密碼’ |base64 以這樣的方式來獲得 auth 的內容,組裝成對應的格式,寫入 GitLab 的 value 配置中。
生產環境中的 CI 性能優化

1、使用國內源對容器鏡像進行加速 例如:使用 Docker 中國官方鏡像加速服務 https://registry.docker-cn.com 當然各家公司其實也有提供鏡像加速的服務。
2、使用私有鏡像倉庫。例如 Docker Registry,或者 Harbor,我們是在使用 Harbor 作為私有鏡像倉庫的。
因為網絡的原因, 如果預設使用官方鏡像, 1. 官方鏡像拉不下來;2. 在官方鏡像中安裝包耗時長;3. 如果換源,需要每個 Dockerfile 都要做相同的事情。 這我們當然是不能同意的。 所以,我們構建了自己的私有鏡像。 從 BusyBox 開始 構建 alpine linux 使用私有源, 以此為基礎 構建我們所需要的其他鏡像。 用戶不再需要自行換源。
這個操作完成後, 原先我們需要在 CI 執行的過程中安裝 py-pip(為了安裝 docker-compose 和我們的服務依賴)耗時從 3min30s 減少到了 18s。
這裡,需要說下為何我們是從頭開始構建鏡像,而不是基於官方鏡像。 主要是為了減少鏡像體積 以及為了更快的適用於我們的需求。
同樣的,我們構建了基礎的 Docker 鏡像,Python/Maven 等鏡像,都是預設使用了我們的私有源,並且,用戶在使用時, 並不需要關註換源的事情, 減少用戶的心智負擔。
3、規範 Dockerfile, 減少不必要的依賴安裝,減少鏡像體積。其實結合上面的部分,我們做的事情是直接構建了我們的基礎鏡像 Docker/Alpine/Maven之類的基礎鏡像,預設直接都換了源。這樣既方便使用,還可以減少鏡像層數。
4、拆分 job, 通過 tag 的方式可指定 Runner, 由不同的 Runner 來並行執行無強依賴的一些動作。 便於分攤壓力。
5、使用 Cache,CI 的構建中,大多數的鏡像,其實變化不大,所以使用 Cache 可以成倍的提升 CI 的速度。
6、可能遇到的坑,前面提到了 service 中可以使用各種各樣的服務, 無論是 dind 還是 MySQL Redis 等。 但是如果我們全部做到了優化,都使用我們的私有源, 那便會發現問題。
因為 GitLab CI 預設對於 docker:dind 的 service 其實會選擇連名為Docker 的 host ,以及 2375 端口。 當使用私有鏡像源的時候,比如:
services:
            - name: registry.docker-cn.com/taobeier/docker:stable-dind

那這個 service 的host 是什麼呢?
這個 service 的 host 其實是會變成 registry.docker-cn.com__taobeier__docker,然後 GitLab Runner 便會找不到, job 就會執行失敗。
有兩種解決辦法, 一種是加一個變數。
variables
            DOCKER_HOST"tcp://registry.docker-cn.com__taobeier__docker:2375"

但是這種方式很麻煩,沒有人能完全記住遇到 / 會轉換為 _ 難免會有問題。 那麼就有了第二種辦法:
services:
            - name: registry.docker-cn.com/taobeier/docker:stable-dind
             alias: docker

加一個 alias 。 這個方法目前很少人在用, 畢竟網絡上查到的都是第一種 ,但是這個方式卻是最簡單的。
Q&A;

Q:您提到把各種依賴都以 Service 的提供,請問是以哪種方式呢? 比如Python的依賴,怎麼做成Service呢?
A:Service 化的依賴,主要是指類似 DB / MySQL/ Reids 之類的。 或者是 dind 其實它提供的是 2375 端口的TCP服務。 Python 的依賴,我推薦的做法是, 構建一個換了源的 Python 鏡像。 安裝依賴的時候,耗時會少很多。 或者說, 可以在定義 Pipeline 的時候, 將虛擬環境的 venv 檔案夾作為 cache ,之後的安裝也會檢查這個,避免不必要的安裝。

Q:請問,你們為什麼不用Jenkins Pipeline,而使用GitLab CI?
A:主要原因是我提到的那幾個方面。 集成較好, 界面美觀優雅, 使用簡單(所有有倉庫寫權限的人 都可以使用, 只要創建 .gitlab-ci.yml 並且配置了 Runner 即可使用) 。換個角度,我們來看下使用Jenkins 的問題, Jenkins 對於專案的配置其實和 GitLab 的代碼是分離的, 兩部分的, 用戶(或者說我們的開發者)在使用的時候, 需要有兩個平臺, 並且,大多數時候, Jenkins 的權限是不放開的。 對用戶來講, 那相當於是個黑盒。 那可能的問題是什麼呢? 遇到構建失敗了, 但是只有運維知道發生了什麼,但是研發無能為力,因為沒有權限。 使用GItLab的好處,這個時候就更加突出了, 配置就在代碼倉庫裡面,並且使用 YAML 的配置,很簡單。 有啥問題,直接查,直接改。

Q:關於 Runner 的清理的問題,在長時間使用後,Runner 機器上回產生很多的 Cache 容器,如何清理呢。能夠在任務中自動清除嗎?
A:這個就相對簡單了,首先, 如果你的 Cache 容器確認沒用了, 每個 Cache 容器其實都有名字的, 直接按 Cache 的名字過略, 批量刪掉。 如果你不確定它是否有用,那你直接刪掉也是不影響的, 因為 Docker Excutor 的執行機制是創建完 Service 容器後, 創建 Cache 容器。 要是刪掉了,它只是會再創建一次。 如果你想在任務中清除, 目前還沒做相關的實踐,待我實踐後,看看有沒有很優雅的方式。

Q:請問下Maven的settings.xml怎麼處理?本地Maven倉庫呢?
A:我們構建了私有的 Maven 鏡像, 私有鏡像中是預設使用了我們的私有源。 對於專案中用戶無需關註 settings.xml 中是否配置repo。

Q:在GitLab的CD方案中,在部署的時候,需要在變數中配置跳板機的私鑰,如果這個專案是對公司整部門開發,那麼如何保護這個私鑰呢?
A:可以使用 secret variable 將私鑰寫入其中, (但是專案的管理員,具備查看該 variable 的權限)開發一個 web server (其實只要暴露 IP 端口之類的就可以) 在 CI 執行的過程中去請求, server 對來源做判斷 (比如 執行CI 的時候,會有一些特定的變數,以此來判斷,是否真的是 CI 在請求)然後傳回私鑰。

Q:GitLab CI適合什麼型別的專案呢?國內目前還比較小眾吧?
A:國內目前還較為小眾(相比 Jenkins 來說)其實只要需要 CI 的專案,它都適合。
Kubernetes專案實戰訓練營

Kubernetes專案實戰訓練將於2018年8月17日在深圳開課,3天時間帶你系統掌握Kubernetes本次培訓包括:Docker介紹、Docker鏡像、網絡、儲存、容器安全;Kubernetes架構、設計理念、常用物件、網絡、儲存、網絡隔離、服務發現與負載均衡;Kubernetes核心組件、Pod、插件、微服務、雲原生、Kubernetes Operator、集群災備、Helm等,點擊下方圖片查看詳情。

赞(0)

分享創造快樂