Paul Tang

Paul Tang

Python Developer

© 2019

在CircleCI上面使用多個Docker conatainer的方案

這是一篇筆記文,我的文筆很爛,此外我也剛開始接觸Docker,這不是教學而是有點像是踩雷心得,請加減看看,有問題歡迎指教XD

前情提要

我們公司使用CircleCI 作為CI的工具,基本上,CircleCI的使用相當簡單

綁定Github account 到CircleCI → Enable Repository in CircleCI → 撰寫CircleCI config YAML 檔 → 推到Github 上面

理論上他就會針對config裡面的步驟進行一步一步的測試和部署,關於config的撰寫,circleci提供一些範例,這邊就不贅述了。

CircleCI在他們的伺服器上使用Docker container來進行測試,一旦把想要測試和部署的專案推上Github以後,他會觸發一個webhook讓CircleCI開啟一個container進行乾淨的測試和部署。

這次遇到的問題是,如果我今天想要測試一個API app,整合測試的部分一定有可能會使用到其他的服務,例如MySQL、Redis等等,這個就會有一點小小的麻煩了。

一開始我找到了CircleCI的文件,關於不同服務的整合測試,CircleCI有提供官方的解決方案

但是如果我就是想要用Docker Compose進行測試怎麼辦?(很任性XD) 因為這樣就能直接把在Local端的測試環境直接搬上CI環境,也不用搞一堆其他有的沒的…?

總之,本篇文章主要會以在CircleCI docker環境內再創建另外一個Docker-compose的方法為主,也就是一個container裡面再有container的概念

Talk is Cheap, show me the code ?!

這邊就不囉唆,直接上circleci 的yaml config

version: 2
jobs:
  build:
    working_directory: /test_app
    parallelism: 1
    docker:
      - image: docker/compose:1.25.0-rc2-debian
    steps:
      - run:
          name: Configure environment
          command: |
            apt-get update
            apt-get install git -y
      - setup_remote_docker
      - checkout

      - run:
          name: Docker test 
          command: |
            # sleep 15
            # docker exec api make test
            docker-compose run api make test 
      - run:
          name: Clean up docker-compose table
          command: |
            docker-compose down

以及我們要執行測試的docker compose file 其實就是一個很單純的 api 測試,執行測試的時候需要用到Celery worker,而Celery需要連到Redis & MySQL

  • 附註: Celery worker是一個async python worker framework, 用法是和api同一個container但是使用celery command,celery -A api worker -l info,這樣就可以把一個async worker叫起來

docker compose yaml file如下

version: "3"

services:
    db:
        image: mysql:5.7
        restart: always
        container_name: db
        ports:
            - "3306:3306"
        environment:
            - MYSQL_DATABASE=test
            - MYSQL_ROOT_PASSWORD=xxxxx
            - MYSQL_PORT=3306
    redis:
        image: redis
        container_name: redis
        restart: always
        ports:
            - "6379:6379"
    celery:
        build: .
        image: celery
        command:
              celery -A api worker -l info
        container_name: celery
        links:
            - db
            - redis
        environment:
            - DATABASE_MASTER_URL=######Link to db##########
            - CELERY_REDIS_URL=redis://redis:6379
volumes:
    db_data: {}

好像很簡單,對吧?

但是人生沒那麼容易的,如果你就這樣單純的執行下去 你很有可能會遇到一個問題…

django.db.utils.InterfaceError: (2003, "2003: Can't connect to MySQL server on 'db:3306' (111 Connection refused)", None)
M

什麼?!!

竟然連不到DB? 在本機端測試明明都沒問題啊….

問題分析

根據CircleCI提供的錯誤log,分析可能是因為MySQL server壓根沒有被執行起來 基本上docker-compose 如果加上depend_on 基本上可以指定container執行的順序 例如:

version: "3"

services:
    db:
        image: mysql:5.7
        restart: always
        container_name: db
        ports:
            - "3306:3306"
        environment:
            - MYSQL_DATABASE=test
            - MYSQL_ROOT_PASSWORD=xxxxx
            - MYSQL_PORT=3306
    redis:
        image: redis
        container_name: redis
        restart: always
        ports:
            - "6379:6379"
    celery:
        build: .
        image: celery
        command:
              celery -A api worker -l info && echo "Check db and redis OK"
        container_name: celery
        depends_on:
            - db
            - redis
        links:
            - db
            - redis
        environment:
            - DATABASE_MASTER_URL=######Link to db##########
            - CELERY_REDIS_URL=redis://redis:6379
volumes:
    db_data: {}

結果?

很不幸的還是一樣的錯誤…

在上述的yaml 可以看到如果在api加上了depens_on,確實可以指定api會在redis 以及 MySQL後被執行

不過請注意喔,是被執行,並不代表是可以被連線或是服務已經ready,可以參考這篇文章

解決方案

根據文章的的建議,我們應該可以設定一個小小的檢查器,如果該服務還沒有被確定啟動,就不會執行,讓他一直等,等到dependencies 都確定可以被連線以後,才會執行下一步

這邊我們用的小工具叫做 wait-for-it

基本上就是一個pure bash script,可以用來block住你的程式碼,等到需要等待的資料庫或是其他服務真的ready 後才會執行

使用範例如下

./wait-for-it.sh db:3306 -- echo "db is up"

也由於他是純bash寫成的,你不用安裝任何的dependencies,直接把那隻bash script丟進repo或是在circleci build的時候直接下載也可

所以改寫後的docker compose會像這樣

version: "3"

services:
    db:
        image: mysql:5.7
        restart: always
        container_name: db
        ports:
            - "3306:3306"
        environment:
            - MYSQL_DATABASE=test
            - MYSQL_ROOT_PASSWORD=xxxxx
            - MYSQL_PORT=3306
    redis:
        image: redis
        container_name: vs_api_redis
        restart: always
        ports:
            - "6379:6379"
    celery:
        build: .
        image: celery
        command:
            bash -c "./wait-for-it.sh db:3306 -- ./wait-for-it.sh redis:6379 -- celery -A vs_api worker -l info && echo "Check db and redis OK""
        container_name: vs_api_celery
        depends_on:
            - db
            - redis
        links:
            - db
            - redis
        environment:
            - DATABASE_MASTER_URL=######Link to db##########
            - CELERY_REDIS_URL=redis://redis:6379
volumes:
    db_data: {}

可以注意到在celery command的部分,我們加上了wait-for-it的指令,你就會在circleci的log上面發現,他真的有在等待

^@^@wait-for-it.sh: waiting 15 seconds for db:3307

然後你的circleci就成功了!!!!!!!!!! :)

結語

當然,我還是認為在circleci的docker container裡面再起一個docker-compose不是一個很好的idea,因為目前我們還是遇到了circleci build time非常久的問題,但是如果你發現circleci提供的多個conatinaer作法不合你的胃口,或許你也可以嘗試看看本篇的做法唷。有問題以及指正歡迎在下方留言,謝謝。