Update Blog อัตโนมัติด้วย Docker และ CircleCI

Published on

บล็อก armno.in.th ถูกสร้างขึ้นมาด้วย Hugo ซึ่งเป็น static site generator ที่มีหลักการทำงานคร่าวๆ คือ

  1. เขียน content ในไฟล์ markdown
  2. รัน $ hugo command เพื่อให้ Hugo build (generate) ไฟล์ markdown เป็นไฟล์ HTML
  3. เอาไฟล์ HTML ที่ได้ ไปวางไว้บน web server

ในกรณีของบล็อกนี้ก็จะมีขั้นตอนเพิ่มเข้ามาอีกนิดหน่อย คือ

  1. เขียน content ในไฟล์ markdown
  2. Push code ไปที่ GitHub Repo
  3. SSH ไปที่ server ของ DigitalOcean ที่ใช้เป็น web server ของบล็อกนี้อยู่
  4. Pull code จาก GitHub
  5. รัน $ hugo command เพื่อให้ Hugo build (generate) ไฟล์ markdown เป็นไฟล์ HTML

เมื่อก่อนตอนที่ยังใช้ Jekyll เป็น generator ผมเขียน deploy script ไว้ทำงานกับ GitHub Webhook โดยทุกครั้งที่ push code ไปที่ GitHub จะมี webhook ไปบอกที่ server DigitalOcean (ขอเรียกให้หรูว่า production server) ให้ทำการ pull และ build โดยอัตโนมัติ

แต่หลังจากเปลี่ยนจาก Jekyll มาใช้ Hugo script มันก็หยุดทำงานเพราะผมไม่ได้อัพเดท deploy script ของเดิมให้มันทำงานกับ Hugo และ repo ใหม่ ก็เลยต้องทำ (แบบ manual) ตามขั้นตอน 1-5 ข้างบนทุกครั้งที่อัพเดทบล็อก

ปัญหากวนใจเล็กๆ อีกอย่างคือ flow ที่มีอยู่ต้องรัน hugo บน server ด้วย ทำให้ต้องลง Hugo ไว้ทั้งบนเครื่อง dev และบน production server เวลาอัพเดทก็ต้องอัพเดทพร้อมกัน ให้เวอร์ชั่นของ Hugo ตรงกัน (Hugo ออกเวอร์ชั่นใหม่ค่อนข้างบ่อย)

ถึงแม้นานๆ จะอัพเดทบล็อกนี้สักที แต่ก็ไม่ได้อยากทำเหมือนเดิมซ้ำๆ ทุกครั้ง ก็เลยจดโน้ตไว้ว่าวันหนึ่งจะทำ

issue ที่สร้างไว้กันลืมบน GitHub repo

ผ่านไปปีกว่า ถึงเพิ่งได้ลงมือทำจริงๆ

สิ่งที่อยากได้คือ ลดขั้นตอนจาก 1-5 ให้เหลือ 1-2 คือ push code ไปที่ GitHub repo แล้วที่เหลือให้มันทำงานของมันเอง

ส่วนปัญหาเรื่อง version ของ Hugo ที่ไม่ตรงกันของเครื่อง dev กับ production server จะใช้ Docker จัดการ


สำรวจทางเลือก

ที่คิดออกมีสองแบบ คือ

ปกติที่ทำงานใช้ GitLab CI เป็นประจำอยู่แล้ว เลยคิดว่าทางเลือกที่ 2 น่าจะคุ้นเคยกว่า และเราก็สามารถใช้ GitLab CI กับ GitHub repo ได้ (โดยที่ไม่ต้องย้าย repo ไป gitlab.com) แต่ว่าอยากถือโอกาสนี้เรียนรู้ tool ตัวอื่นด้วย ก็เลยเลือกไม่ใช้ GitLab CI ครับ

ลองเล่น 2 ตัวคือ SemaphoreCI กับ CircleCI และเลือก CircleCI เพราะใช้งานง่ายว่า วิธีการ config ใช้ไฟล์ yaml คล้ายกับ GitLab CI ที่ใช้อยู่ แล้วก็ UI ของ CircleCI ดูไม่งงเหมือนของ SemaphoreCI ด้วย

ในโพสต์นี้อาจจะมีอ้างถึง GitLab CI อยู่บ่อยๆ เพื่อขยายความสำหรับคนที่ใช้ GitLab CI อยู่แล้วครับ

Flow (คร่าวๆ)

  1. ใช้ Hugo ผ่าน docker container
  2. Push code ขึ้นไปบน GitHub repo
  3. ให้ CircleCI build docker image จาก Dockerfile ใน repo
  4. Push docker image ไปที่ dockerhub
  5. บน production server: pull docker image จาก dockerhub แล้วรัน container

ที่เลือกใช้ Docker เพราะนอกจากแก้ปัญหาเรื่องเวอร์ชั่นของ Hugo ที่ไม่ตรงกันบนเครื่อง dev กับบน server แล้ว ยังทำให้ทำงานกับ CircleCI ง่ายขึ้นด้วย เนื่องจากบริการ CI/CD พวกนี้มักจะ support docker อยู่แล้ว

Spoiler alert: ทั้งหมดนี้ส่วนหนึ่งก็เพื่อการทดลอง workflow และ tool ที่ผมยังไม่เคยใช้ ความจริงการ deploy static HTML website ง่ายๆ แบบบล็อกนี้ ไม่จำเป็นต้องมีขั้นตอนยุ่งยากอะไรขนาดนี้ก็ได้ ในตอนหน้าของโพสต์นี้ จะลองทำให้ automated deployment flow ง่ายกว่าเดิม โปรดติดตามตอนต่อไป

1. ใช้ Hugo ผ่าน Docker

Hugo มี docker image ให้เลือกหลายตัว ผมเลือกใช้ image klakegg/hugo ร่วมกับ docker compose มี docker-compose.yml สั้นๆ

version: "3.5"

services:
  blog:
    container_name: armno-blog
    image: klakegg/hugo:0.46
    command: server
    volumes:
      - "./:/src"
    ports:
      - "1313:1313"

image นี้มี command server ใช้สำหรับรัน local server และ rebuild มีค่าเท่ากับการรัน $ hugo serve ในเครื่อง dev แต่เปลี่ยนเป็น $ docker-compose up --detach แทน

สำหรับ Dockerfile ที่จะนำไปใช้ build image เพื่อใช้บน CI และ production server ใช้ docker multi-stage build เพื่อให้ Hugo build ไฟล์ output ออกมาก่อน แล้วค่อยนำ output ไปใส่ใน nginx web server

image มีแบบ onbuild ให้เลือกด้วย ใน Dockerfile ก็เลยมีแค่ 3 บรรทัด

FROM klakegg/hugo:0.46-onbuild AS hugo

FROM nginx:1.15.2-alpine
COPY --from=hugo /onbuild /usr/share/nginx/html

2. ลง Docker ใน production server

ผมใช้ Ubuntu Server 16.04 LTS วิธีการลง Docker ก็ตาม tutorial นี้เลย: How To Install and Use Docker on Ubuntu 16.04

พอจัดการ Docker แล้ว ก็ไปที่ CircleCI กันต่อ

3. สร้าง project บน CircleCI

สมัคร Circle CI ด้วย account GitHub ได้เลย พอล็อกอินแล้ว CircleCI จะให้เรา Set Up Project จาก GitHub repo ที่เรามีอยู่

สร้าง project ใน CircleCI

พอกด Set Up Project ก็ต้องเลือก OS กับ ภาษาที่ใช้กับ project แล้ว CircleCI จะสร้าง template ของไฟล์ config ให้ .. สำหรับบล็อก Hugo เลือกภาษาอะไรก็ได้

สร้าง project ใน circleci

เลื่อนลงมาข้างล่างอีกนิด จะมีขั้นตอนบอกว่าต้องทำอะไรบ้าง

ขั้นตอนการ set up project ใน CircleCI

ดูเหมือนมีหลายขั้นตอน จริงๆ แล้วมีเพียงการสร้างไฟล์ .circleci/config.yml แล้ว commit และ push ขึ้นไปบน repo ใน GitHub จากก็กดปุ่ม Start Building ก็เป็นอันเสร็จพิธี

ไฟล์ .circleci/config.yml เป็นตัวบอก build step ให้กับ CircleCI ว่าต้องทำอะไรบ้าง (ในโลก GitLab CI มันคือไฟล์ .gitlab-ci.yml นั่นเอง ทำหน้าที่เหมือนกัน แต่เขียนต่างกันนิดหน่อย)

version: 2
jobs:
  build:
    machine: true
    steps:
      - checkout
      - run:
          name: Login to the registry
          command: docker login -u $DOCKER_USER -p $DOCKER_PASS
      - run:
          name: Build Docker Image
          command: docker build --tag armno/blog:$CIRCLE_BRANCH .
      - run:
          name: Push to the registry
          command: docker push armno/blog:$CIRCLE_BRANCH
  deploy:
    machine: true
    steps:
      - run:
          name: Deploy
          command: |
            ssh -oStrictHostKeyChecking=no $SSH_USER@$SSH_HOST -p $SSH_PORT "docker pull armno/blog:master && docker stop armno-blog || true && docker run --rm --detach --publish 8000:80 --name armno-blog armno/blog:master && exit"

workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build:
          filters:
            branches:
              only: master
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master

jobs ใน CircleCI

workflows ใน CircleCI

ไฟล์ config.yml ของผมมีอยู่ 2 jobs คือ

ถึงตอนนี้ production server ก็จะมีบล็อก Hugo รันอยู่ใน container ตัวหนึ่งที่ URL: http://<SERVER_ADDRESS>:8000

4. ตั้งค่า nginx server block (virtual host) ให้ชี้ domain ไปที่ container

ขั้นตอนสุดท้ายเป็นการตั้งค่า server block ของ nginx บน production server จากเดิมจะชี้ไปที่ directory หนึ่งบน server ก็เปลี่ยนเป็นการใช้ reverse proxy ชี้ไปที่ localhost:8000 แทน ซึ่งก็คือ URL ของ container ที่รันไว้

เดิม

server_name armno.in.th www.armno.in.th;
location / {
	try_files $uri/ $uri $uri.html $uri.htm =404;
}

เป็น

server_name armno.in.th www.armno.in.th;
location / {
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-NginX-Proxy true;
	proxy_pass http://127.0.0.1:8000;
	proxy_ssl_session_reuse off;
	proxy_set_header Host $http_host;
	proxy_cache_bypass $http_upgrade;
	proxy_redirect off;
}

จากนั้น restart nginx เป็นอันจบพิธี

$ sudo service nginx restart

คราวนี้เวลา master branch มีการอัพเดท CircleCI ก็จะทำการ deploy อัตโนมัติ ทั้งหมดนี้ใช้เวลาประมาณ 1-2 นาที


ถามว่าจำเป็นต้องทำขนาดนี้ไหม ผมก็คิดว่าไม่ แต่ถือว่าได้ลองใช้ Docker กับ CircleCI และพอจะเห็นภาพคร่าวๆ ว่า CircleCI ทำอะไรได้บ้าง เผื่อจะได้ใช้ประโยชน์ในอนาคต (ปกติถ้าอยากลองเล่นอะไร ผมก็จะใช้บล็อกของตัวเองนี่แหละเป็นหนูทดลอง)

ในตอนหน้าจะทำให้ชีวิตง่ายขึ้นด้วยการใช้ Netlify เข้ามาแทน CI/CD ข้างบน จะเป็นยังไงก็โปรดติดตามตอนต่อไปข้างล่างเลย

Update: ตอน 2 มาแล้ว Update Blog อัตโนมัติด้วย Netlify

issue closed

Share

(Edit on GitHub)