Như tiêu đề, thì trong bài này mình sẽ hướng dẫn tạo 1 project rails, sử dụng docker, circle CI và check statut pass trước khi merge PR trên github
Tạo rails app với docker
Tạo rails app
Trước hết bạn cần tạo mới 1 rails app ở local(nên ở local bạn cần cài đặt rails từ trước)
rails new circle-ci -d mysql
mình dùng db là mysql, nên có -d mysql
Khi tạo xong rails app, thì đừng vội làm gì cả, chúng ta sẽ chuyển app vào docker luôn.
Trước tiên, thì bạn có thể hiểu nôm na docker sẽ là 1 cái máy tính riêng biệt, giúp bạn chạy các phần mền, chương trình mà bạn đã cài đặt. 1 cái máy tính thì sẽ có phần cứng, phần mền. Về phần cứng thì thông thường docker sẽ sử dụng tối đa tài nguyên mà máy host(máy tính của bạn đang cài docker), trong bài này mình sẽ không đi sâu về vấn đề này, nếu bạn muốn tìm hiểu, có thể vào vào đây để đọc thêm. Còn về phần mềm thì thường sẽ có hệ điều hành, cái chương trình cần thiết.
Tạo Docker
Nếu bạn biết cài win, hoặc ghost win thì phần docker này sẽ tương đối dễ hiểu.
Tạo Dockerfile
Dockerfile hiểu nôm na là những câu lệnh để bạn có thể tạo ra 1 image, giống như lúc bạn cài win, bạn cần có 1 file win .iso
, để boot vào usb, rồi sẽ cài vào cái máy tính. Thực ra quá trình này là ghost
thì đúng hơn, vì ngoài hệ điều hành ra, thì sẽ có sẵn cả những phần mềm cần thiết.
Tạo 1 file tên là Dockerfile
ngay trong thư mục của project
FROM ruby:2.7.1-slim-buster RUN apt-get update && \ apt-get install -y --no-install-recommends curl apt-transport-https build-essential && \ curl -sL https://deb.nodesource.com/setup_12.x | bash - && \ curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && \ apt-get install -y --no-install-recommends \ git \ nodejs \ yarn \ libssl-dev \ default-libmysqlclient-dev \ && \ rm -rf /var/lib/apt/lists/*
RUN gem install bundler -v 2.1.4 RUN mkdir /myapp
WORKDIR /myapp
COPY . /myapp # Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
Giải thích:
Mấy chữ viết in hoa là syntax của Dockerfile, bạn có thể tham khảo ở đây
- FROM: như tên tiếng anh,
từ
. Image mà bạn sắp tạo ra sẽ được lấy base theo 1 image khác, giống như để có thể tạo ra file ghost win , thì bạn phải có1 máy chạy win
trước đó. Ở đây mình sẽ dựa theo 1 cái máyubuntu
, có cài sẵn ruby 2.7.1.ruby:2.7.1-slim-buster
là 1 image được người khác tạo ra, up lên mạng, và bạn chỉ việc tải về dùng. Bạn có thể lên https://hub.docker.com/ để tìm nhiều image khác. (image ruby được mình lấy ở https://hub.docker.com/_/ruby) - RUN: sẽ dụng bạn chạy các câu lệnh, các câu lệnh sẽ giống như với các câu lệnh mà bạn gõ với máy thật của mình vậy, cũng cần apt-get update, rồi mới install. Ở đây mình sẽ chạy 1 số câu lệnh để cài các phần mềm, lib cần thiết cho rails app của mình: git, nodejs, yarn, libssl-dev, default-libmysqlclient-dev, rồi bundler để có thể
bundle install
- tiếp theo là cái cụm 3 dòng
RUN mkdir /myapp
WORKDIR /myapp
COPY . /myapp
Mình tạo 1 thư mục tên myapp
trong cái máy tính mới
của mình. Tiếp theo thư mục myapp
sẽ được đặt là thư mục làm việc, các câu lệnh sau WORKDIR sẽ chạy trong thư mục myapp
, giống như việc myapp
sẽ là cái ổ C
để từ giờ có cài đặt, hay copy cái gì sẽ mặc định được cài vào ổ C
. Sau đó, mình sẽ copy toàn bộ nội dung thư mục circle-ci(là rails project ở máy thật) vào trong myapp
- Kế tiếp, mình sẽ copy file
entrypoint.sh
vào trong/usr/bin/
và cấp quyền execute cho nó(nghĩa là để chạy 1 file bash, thì thay vì ./usr/bin/entrypoint.sh, thì bạn có thể gọi thằng entrypoint.sh). - ENTRYPOINT: sẽ giúp chạy lệnh
entrypoint.sh
khi cái máy tính của bạn được khởi động - EXPOSE: sẽ set cổng mạng mà cái
máy tính
của bạn sẽ lắng nghe
Tạo entrypoint.sh
Tạo 1 file entrypoint.sh
trong thư mục của project
#!/bin/bash
# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid # bundle install
bundle check || bundle install # if in Dockerfile have CMD
# Then exec the container's main process (what's set as CMD in the Dockerfile).
# exec "_@.com" # else
rails s -b 0.0.0.0
Thông thường, ở những bài hướng dẫn khác bạn có thể thấy có dòng CMD ["rails", "server", "-b", "0.0.0.0"]
ở cuối file Dockerfile. Thực chất CMD
cũng giống như ENTRYPOINT
cũng là để chạy lệnh khi máy tính
được khởi động. Do ở đây, mình cần chạy nhiều lệnh, nên thành ra mình gộp chung vào trong file bash này. Bạn có thể đọc thêm về phân biệt CMD vs ENTRYPOINT
Tạo image
Tiếp theo chúng ta sẽ tạo ra 1 cái image để sau này dùng cho việc cài hoặc ghost win
.
Ở thư mục của project, chạy lệnh
docker build -t hatd/circle-ci:3.0 .
docker build
: là câu lệnh để tạo image-t hatd/circle-ci:3.0
: là option của lệnh build, giúp đặt tên cho image, ở đây image của mình có tên làhatd/circle-ci
, và image có tag là3.0
. Ở phần sau mình sẽ giải thích tại sao lại đặt tên như này..
: dấu.
này sẽ ám chỉ rằng lệnh build được chạy theo fileDockerfile
trong cùng thư mục
có nhiều option khác của lệnh build, bạn có thể tham khảo tại đây.
Đợi 1 lúc cho lệnh build chạy xong, chạy lệnh docker image ls
để hiển thị tất cả các image có trong máy của bạn. Và bạn sẽ thấy image mà bạn vừa tạo ra
Tạo docker-compose.yml
Docker compose giống như tập lệnh, để giúp bạn khởi động cái máy tính
của bạn lên vậy
Tạo file docker-compose.yml
trong thư mục của project
version: '3'
services: db: image: mysql:5.7.33 env_file: - .env environment: - MYSQL_ROOT_PASSWORD=${DATABASE_PASSWORD} volumes: - mysql_cache:/var/lib/mysql networks: - internal web: image: hatd/circle-ci:3.0 env_file: - .env volumes: - .:/myapp - bundle_cache:/usr/local/bundle ports: - "3000:3000" depends_on: - db networks: - internal tty: true stdin_open: true
volumes: mysql_cache: bundle_cache:
networks: internal: driver: bridge
Giải thích:
- file docker-compose.yml sẽ được viết theo cú pháp của version 3, tùy thuộc các version khác nhau mà syntax khác nhau, có thể tham khảo list version
- services: liệt kê các
máy tính
mà mình sẽ khởi động, ở đây mình sẽ khởi động 2 máy tính làdb
vàweb
.db
là để chạy mysql làm database, cònweb
là để chạy rails server - volumes: liệt kê các ô đĩa phụ, giống như trong máy tính có ổ C, ổ D, khi cài win thì sẽ cài vào ổ C, và ổ D còn nguyên, nên sẽ không bị mất dữ liệu.
- networks: tạo ra các mạng, để các máy tính có thể liên kết với nhau
Máy tính db: được cài theo file ghost
: mysql:5.7.33. Các biến môi trường được lấy từ file .env
. Do trong file .env
không có biến MYSQL_ROOT_PASSWORD, nên được liệt kê riêng trong environment
. Với câu lệnh mysql_cache:/var/lib/mysql
, cho phép bạn đồng bộ dữ liệu từ /var/lib/mysql
(config, dữ liệu của mysql) ra ngoài mysql_cache
và ngược lại. Và cái máy tính này sẽ được kết nối vào mạng internal
Máy tính web: được cài theo file ghost mà chúng ta vừa tạo trước đó hatd/circle-ci:3.0
. Máy tính này cũng lấy các biến môi trường từ file .env
. Máy tính này sẽ đồng bộ thư mục myapp
trong máy tính web
, ra ngoài thư mục circle-ci
ở trên máy thật của chúng ta(việc này sẽ giúp ta khi sửa code ở thư mục circle-ci thì code trong máy tính web
cũng được sửa theo). ports: "3000:3000"
sẽ ánh xạ cổng 3000 trong cái máy tính web
ra cổng 3000 của cái máy tính thật(vì rails server chạy mặc định ở công 3000 mà). depends_on: db
máy tính web
sẽ phải đợi cái máy tính db
được mở xong thì mới được mở, đương nhiên rồi, server mà bật lên rồi trong khi database chưa được bật thì toang. Cùng kết nối vào mạng internal
để có thể giao tiếp với nhau. 2 thằng tty: true
, stdin_open: true
hiểu nôm nà là sẽ giúp bạn khi vào trong máy tính web
sẽ hiển thị những gì bạn gõ, những dòng log giống với trên terminal ngoài máy thật.
Các syntax + option của docker-compose.yml version 3 được liệt kê trong đây
Start rails server
Trước tiên bạn cần thêm file .env và sửa file config/database.yml
.env
DATABASE_HOSTNAME=db
DATABASE_USERNAME=root
DATABASE_PASSWORD=password
p/s: db
chính là tên service db trong docker-compose.yml
config/database.yml
.....
default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: <%= ENV["DATABASE_USERNAME"] %> password: <%= ENV["DATABASE_PASSWORD"] %> host: <%= ENV["DATABASE_HOSTNAME"] %>
.....
Chạy lệnh sau để khởi động 2 cái máy tính
của chúng ta
docker-compose up
đợi 1 chút đển khi rails server start thành công
Tiếp theo là đến công việc create db. Do bây giờ app đã được chạy trong máy tính web
, nên bạn cần phải vào trong máy tính web
để có thể chạy lệnh.
Để vào trong máy tính web
(container web), chạy lệnh
docker exec -it circle-ci_web_1 bash
- docker exec: là lệnh giúp vào trong cái
máy tính web
- -it: là option của lệnh exec, giúp input và output giống với terminal ngoài máy thật vậy
- circle-ci_web_1: là container name(tên của cái
máy tính web
), ở đây có thể dùng container id cũng được, nhưng mình thường dùng container name vì nó dễ nhớ, cấu trúc của container name là{tên project}_{tên service}_1
(thường service ít khi đặt trùng tên, nên có đuôi là 1 hết). List container đang được bật có thể kiểm tra bằng lệnhdocker container ls
- bash: đây là lệnh sẽ chạy khi chúng ta vào trong container, nó sẽ mở ra terminal
Khi vào được trong container, chạy mấy lệnh create db như bình thường: rails db:create
, rails db:migrate
. Và khi đó bạn có thể truy cập localhost:3000 là sẽ thấy trang root của rails app
từ giờ trở đi, nhưng lệnh liên quan đến rails như kiểu bundle install, rspec, rubocop là sẽ chạy trong container web nhé
Config Circle Ci
Trong bài này, mình sẽ sử dụng circle Ci để check việc chạy rspec, rubocop có pass hay không
Thêm gem
Thêm 1 số gem cần thiết: rspec-rails, rubocop, rubocop-rails, simplecov, rspec_junit_formatter. Chạy bundle install
Thêm 1 model Post: rails generate scaffold Post title:string body:text published:boolean
, db:migrate
Thêm test: mình có thểm 1 số test để có thể chạy rspec (https://github.com/hatd/circle-ci/tree/master/spec)
Sau đó thì chạy rspec spec/
, rubocop
để xác nhận pass ở local trước nhé
Upload docker image
Vì Circle CI cũng dùng docker để dựng môi trường, nên cũng sẽ cần docker image. Vậy nên sẽ cần upload cái image hatd/circle-ci:3.0
chúng ta vừa mới tạo lên internet, để Circle CI có thể tải về:
- Hãy đăng kí 1 tài khoản trên https://hub.docker.com/
- Tạo 1 repository có tên: circle-ci(trùng với cái tên image của chúng ta)
- Login docker ở local bằng lệnh:
docker login --username=yourhubusername
,yourhubusername
là username mà lúc bạn đăng kí tài khoản, của mình làhatd
- Push image bằng lệnh:
docker push yourhubusername:imagename
, với trường hợp của mình sẽ làdocker push hatd/circle-ci
. Đấy là lý do, từ đầu mình đặt tên image như vậy. Nếu từ đầu bạn không đặt tên sẵn, thì bạn có thể dùng lệnhdocker tag image_id yourhubusername/repo_name:version_tag
để thay đổi tên với tag của 1 image. Tham khảo
Như vậy, image của bạn đã được up lên, và circle ci có thể kéo về và build docker
Thêm config circle ci
Thêm file .circleci/config.yml
trong project
version: 2.1
# orbs:
# ruby: circleci/_@.com
references: ruby_envs: &ruby_envs environment: DATABASE_HOSTNAME: 127.0.0.1 DATABASE_USERNAME: root DATABASE_PASSWORD: password BUNDLER_VERSION: 2.1.4 RAILS_ENV: test mysql_envs: &mysql_envs environment: DATABASE_USERNAME: root DATABASE_PASSWORD: password MYSQL_ROOT_PASSWORD: password
jobs: test_rspec: docker: - image: hatd/circle-ci:3.0 <<: *ruby_envs - image: mysql:5.7.33 <<: *mysql_envs steps: - checkout - run: name: Which bundler? command: bundle -v - run: name: Bundle Install command: bundle check || bundle install --jobs=4 --retry=3 - run: name: Build assets command: bundle exec rails assets:precompile - run: name: Database setup command: RAILS_ENV=test bundle exec rails db:drop db:create - run: name: Run Migrate command: RAILS_ENV=test bundle exec rails db:migrate - run: name: Rubocop test command: bundle exec rubocop - run: name: Rspec test command: bundle exec rspec spec/
workflows: version: 2 test: jobs: - test_rspec
Giải thích:
- Hiện tại version mà circle ci support là 2, và version cao nhất là 2.1
- Version 2.1 support orbs, cái này mình chưa rõ lắm, nên xin phép bỏ qua
- references: liệt kê các biến môi trường cần sử dụng, vì ở đây các biến này đơn gian, nên có thể được liệt kê trực tiếp vào file, nhưng sau này có những thông tin nhạy cảm, thì sẽ phải setting trong circle ci(DATABASE_HOSTNAME là
127.0.0.1
, chứ không phải làdb
như ở docker-compose.yml nữa nhé) - jobs: liệt kê các job sẽ được chạy, ở đây mình chỉ có 1 job tên là
test_rspec
- workflows: liệt kê các job sẽ được chạy qua, theo thứ tự, Ở đây mình có 1 workflow có tên
test
, chạy 1 jobtest_rspec
job test_rspec: sẽ được build bằng docker.
- Ở đây sẽ có 2 docker được build, 1 sử dụng image
hatd/circle-ci:3.0
mà mình đã up lên docker hub trước đó, 2 là image mysql, tương tự với mysql mình dùng trong docker-compose.yml, cả 2 sẽ có những biến env cần thiết. - steps: liệt kê các bước sẽ cần chạy trong job, theo thứ tự.
- checkout: nghĩa là git checkout, giúp kéo code về, và đặt trong ~/project
- run: giúp chạy các câu lệnh, ở đây sẽ cần chạy 1 loạt các lệnh, để đến cuối cùng có thể chạy lệnh
bundle exec rubocop
vàbundle exec rspec spec/
để hoàng thành công việc check
Bạn có thể đọc thêm về các config trong đây
Setup Circle CI trên web
nhớ push code lên github trước nhé
- Đăng nhập vào trang https://app.circleci.com/dashboard, thông qua tài khoản github
- Vào tab Projects, bạn sẽ thấy repository của bên github, chọn
Set Up Project
, khi này sẽ hiển thị luôn nutStart Build
, vì chúng ta đã có file .circleci/config.yml và file ý valid. Nếu như chưa có file config.yml, hoặc file bị lỗi gì đó, sẽ có thêm nút download file config, hoặc commit 1 file config vào project
project có file config.yml
project không có file config.yml
- Sau khi nhấn nút
Start Build
bạn sẽ được chuyển sang màn hình pipelines, ở đây bạn sẽ thấy pipeline đầu tiên được chạy, đợi khi nó chạy xong, nếu mà success, thì bạn đã setup xong circle rồi đấy
Tiếp theo, để khi có commit mới lên github, circle ci sẽ chạy, bạn cần vào Project Settings
, trong Advanced
, bật GitHub Status Updates
Caching
Hãy thử push 1 commit mới lên github, và đợi pipeline mới chạy xong, rồi hãy bấm vào job test_rspec
, và để ý step Bundle install
thấy nó rất lâu đúng không? Vì mỗi 1 pipeline mới, sẽ build 1 ra 1 docker mới, và sẽ chạy lại bundle install mới, mặc dù không có sửa gì đến gemfile cả, như vậy sẽ rất tốn thời gian. Nên chúng ta sẽ cần cache lại phần này
Sửa lại đoạn step bundle install như sau
...
- restore_cache: keys: - v1-gem-cache-{{ checksum "Gemfile.lock" }} - v1-gem-cache-
- run: name: Bundle Install command: bundle check || bundle install --jobs=4 --retry=3
- save_cache: key: v1-gem-cache-{{ checksum "Gemfile.lock" }} paths: - vendor/bundle
...
- restore_cache: sẽ khôi phục lại cache có key là
v1-gem-cache-{{ checksum "Gemfile.lock" }}
, nếu cache này không có, circle ci sẽ tìm đến cache có key làv1-gem-cache-
, nếu không có nữa thì là không có cache nào - save_cache: sẽ lưu lại cache, với key là
v1-gem-cache-{{ checksum "Gemfile.lock" }}
, dữ liệu được lấy từ pathvendor/bundle
. Tại sao lại làvendor/bundle
, bạn đến ý, trong list env, mình có đểBUNDLE_PATH: vendor/bundle
, với biến env này, thì bundle install sẽ install gem vào path này, nên chúng ta sẽ cache lại các gem đã được install
Sau khi thêm config, thì hãy cùng xem kết quả
Vậy là trong step Bundle Install, chỉ tốn thời gian cho bundle check
Bạn có thể đọc thêm về caching ở đây
Wait db:
Sau khi thêm đoạn cache bundle, thì xảy ra 1 vấn đề, đó là các step thực hiện quá nhanh, dẫn đến khi chạy step Database setup
thì docker mysql chưa được build xong.
Vậy nên chúng ta cần phải có 1 step nữa là đợi database được sẵn sàng connect thì mới chạy step Database setup
Lượn lờ 1 lúc thì mình thấy có cách này https://stackoverflow.com/a/54249757, đại loại là dùng 1 đoạn script để check host mysql đã sẵn sàng hay chưa. Mình sẽ dùng wait-for-it.sh
.
Sau thi thêm file wait-for-it.sh vào project, thì thêm step vào trước step Database setup
...
- run: name: Grant script permission command: chmod a+x wait-for-it.sh
- run: name: Wait db command: ./wait-for-it.sh 127.0.0.1:3306 --timeout=300 -- echo 'Mysql service is ready!'
...
Thu thập kết quả test và coverage
Thông thường, thì khi chạy test trên ci, ngoài việc biết test success/error, thì bạn cũng cần biết là nó error ở chỗ nào, chỉ số coverage được bao nhiều %, vậy thì những cái đó hiển thị ở đâu.
Circle CI support việc collect test data và coverage
Cùng sửa lại 1 chút step Rspec test, và thêm 2 step mới
...
- run: name: Rspec test command: | mkdir ~/rspec bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml mv coverage ~/rspec/coverage no_output_timeout: 20m
# collect reports
- store_test_results: path: ~/rspec - store_artifacts: path: ~/rspec
...
lúc này ta sẽ cho output của rpsec thành html, và đặt trong folder ~/rspec
, copy thư mục coverage
(thư mục được sinh ra bởi gem coverage). Và rồi lưu nó lại
Khi này, khi bấm vào job test_rspec
, sẽ có thêm 2 tab là TESTS
và ARTIFACTS
Tab TESTS
Tab ARTIFACTS
Bấm vào ~/rspec/coverage/index.html
bạn có thể qua trang hiển thị coverage của commit đó
Status check Github
Bây giờ, hãy setup để PR phải pass được circle ci thì mới được merge
- Vào Settings của repo
- Trong tab Branches, thêm 1 rule mới
- Điền Branch name pattern là branch mà sẽ compare trong PR, thường là master
- Chọn
Require status checks to pass before merging
rồi chọnci/circleci: test_rspec
Như vậy trong PR sẽ có thêm phần check status
phải pass thì mới được merge PR
Và bạn có thể xem fail ở đâu
Bài viết còn sơ sài, có gì xin hãy góp ý trong phần bình luận cho mình nhé