ปัญหา
เวลาสร้าง docker container เพื่อ run งานแบบอัตโนมัติ เราก็จำเป็นต้องใช้ cron แต่ว่า เจ้า docker container images เนี่ย จะเป็นรุ่นตัดทุกอย่างที่ไม่จำเป็นออกหมด เอาไว้ให้ start – run – stop แต่ถึงกระนั้น เราก็ยังอยากได้การทำงานแบบ cron อยู่ดี
Reproduce
สร้าง docker container จาก ubuntu:20.04
docker run -d -it --name kxtest ubuntu:20.04
ตรวจสอบว่ามี crontab หรือไม่ ก็ไม่มี
docker exec -it kxtest bash
root@93c827f2f9fc:/# crontab -l
bash: crontab: command not found
root@93c827f2f9fc:/#
/var/log/syslog มีไม๊ ก็ไม่มี
root@93c827f2f9fc:/# ls -l /var/log/syslog
ls: cannot access '/var/log/syslog': No such file or directory
systemctl มีไม๊ ก็ไม่มี
root@93c827f2f9fc:/# systemctl
bash: systemctl: command not found
แก้ไข
ติดตั้ง 3 อย่าง cron, systemctl, rsyslog แล้ว ขอแถม vim ด้วย
apt update ; apt install -y cron systemctl rsyslog vim
ติดตั้งแล้ว ลองตรวจสอบ ก็พบว่า crontab มาแล้ว, systemctl มาแล้ว ส่วน /var/log/syslog ยังไม่ได้สร้าง
root@93c827f2f9fc:/# crontab -l
no crontab for root
root@93c827f2f9fc:/# systemctl
apt-daily-upgrade.service loaded inactive dead Daily apt upgrade and clean activities
apt-daily-upgrade.timer loaded inactive dead Daily apt upgrade and clean activities
apt-daily.service loaded inactive dead Daily apt download activities
apt-daily.timer loaded inactive dead Daily apt download activities
cron.service loaded inactive dead Regular background program processing daemon
dmesg.service loaded inactive dead Save initial kernel messages after boot
e2scrub@.service loaded inactive dead Online ext4 Metadata Check for
e2scrub_all.service loaded inactive dead Online ext4 Metadata Check for All Filesystems
e2scrub_all.timer loaded inactive dead Periodic ext4 Online Metadata Check for All Filesystems
e2scrub_fail@.service loaded inactive dead Online ext4 Metadata Check Failure Reporting for
e2scrub_reap.service loaded inactive dead Remove Stale Online ext4 Metadata Check Snapshots
fstrim.service loaded inactive dead Discard unused blocks on filesystems from /etc/fstab
fstrim.timer loaded inactive dead Discard unused blocks once a week
hwclock.sh.service loaded inactive dead
logrotate.service loaded inactive dead Rotate log files
logrotate.timer loaded inactive dead Daily rotation of log files
motd-news.service loaded inactive dead Message of the Day
motd-news.timer loaded inactive dead Message of the Day
procps.service loaded inactive dead Loads kernel parameters that are specified in /etc/sysctl.conf
rsyslog.service loaded inactive dead System Logging Service
20 loaded units listed.
To show all installed unit files use 'systemctl list-unit-files'.
root@93c827f2f9fc:/# ls -l /var/log/syslog
ls: cannot access '/var/log/syslog': No such file or directory
ลอง start cron และ rsyslog ดู ก็พบว่า /var/log/syslog มาแล้ว
root@93c827f2f9fc:/# systemctl start cron
root@93c827f2f9fc:/# systemctl status cron
cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service, disabled)
Active: active (running)
root@93c827f2f9fc:/# systemctl start rsyslog
rsyslog.service - System Logging Service
Loaded: loaded (/usr/lib/systemd/system/rsyslog.service, disabled)
Active: active (running)
root@93c827f2f9fc:/# ls -l /var/log/syslog
-rw-r----- 1 syslog adm 517 Oct 10 01:36 /var/log/syslog
ทดสอบสร้าง cron
root@93c827f2f9fc:/# crontab -e
ทำความเข้าใจก่อน crontab เอาไว้สร้าง cron ส่วนตัวของแต่ละ user
แต่ที่ /etc/cron.d/ นั้น เอาไว้สร้างสำหรับ system
2 วิธีการสร้าง cron นี้แตกต่างกันที่ crontab ไม่สามารถระบุผู้ใช้ได้ แต่ใน /etc/cron.d/ นั้น สามารถระบุ user ที่จะ run ได้
ต่อไป จะสร้าง cron ง่าย ๆ ให้ echo เวลา ทุก 1 นาทีลงไฟล์
root@93c827f2f9fc:~# echo 'echo $(date "+%Y%m%d-%H%M%S") >> test.log' > ~/testcron.sh ; chmod +x ~/testcron.sh
root@93c827f2f9fc:~# crontab -e
แล้วใส่ บรรทัดนี้ลงไป
* * * * * /usr/bin/bash ~/testcron.sh
ดูใน /var/log/syslog แล้ว ก็พบว่า ทำงานได้
Oct 10 01:56:39 93c827f2f9fc crontab[1631]: (root) BEGIN EDIT (root)
Oct 10 01:56:42 93c827f2f9fc crontab[1631]: (root) END EDIT (root)
Oct 10 01:57:01 93c827f2f9fc CRON[1638]: (root) CMD (/usr/bin/bash ~/testcron.sh)
ดูผลใน -/test.log
root@93c827f2f9fc:~# cat ~/test.log
20211010-015701
20211010-015801
20211010-015901
เป็นอันว่า OK ต่อไป ดูว่า ถ้า restart docker แล้ว cron จะทำงานไม๊ ?
ทดสอบ restart docker container
ออกจาก bash ของ docker แล้วใช้คำสั่ง
docker restart kxtest
รอสัก 3 นาที แล้วใช้คำสั่ง
docker exec -it kxtest bash -c "date ; cat ~/test.log"
ผลที่ได้คือ อ้าว cron ไม่ทำงานต่อ
Sun Oct 10 02:02:33 UTC 2021
20211010-015701
20211010-015801
20211010-015901
มาดูเหตุกันก่อน
เราต้องไปดู รากเหง้าของ ubuntu:20.04 image
https://hub.docker.com/_/ubuntu
Docker ทำงานโดยจะทำเพียงคำสั่งเดียว การสร้าง docker image จะสร้างจาก Dockerfile ซึ่ง ubuntu:20.04 มี Dockerfile ดังนี้
FROM scratch
ADD ubuntu-focal-oci-amd64-root.tar.gz /
CMD ["bash"]
แสดงว่า docker container ที่ใช้ ubuntu:20.04 (ตัวอื่น ๆ ก็ต้องตามไปดูกัน) ก็จะเรียก Bash เป็นคำสั่งสุดท้าย ก่อนจะยืนเป็น Service ให้ใช้งาน
ดังนั้น … เราก็ต้องไปทำให้ start cron service ใน .bashrc กัน
ประเด็นคือ มัน start ด้วย user ไหน ? ลองใช้คำสั่งนี้
docker exec -it kxtest bash -c "id"
uid=0(root) gid=0(root) groups=0(root)
ก็แสดงว่า สำหรับ image ตัวนี้ run ด้วย root นั่นเอง
วิธีแก้ไข ให้ cron ทำงานทุกครั้งที่ restart docker
เข้าไปใน docker container ด้วยคำสั่ง
docker exec -it kxtest bash
จากนั้น แก้ไขไฟล์ ~/.bashrc เพิ่มบรรทัดนี้ลงไปที่ท้ายไฟล์
systemctl start cron.service
save แล้วออกมาจาก container
แล้ว docker restart
docker restart kxtest
ดูผลลัพธ์
ดูว่า cron ทำงานเลยไม๊
docker exec -it kxtest bash -c "date ; systemctl status cron"
Sun Oct 10 03:01:17 UTC 2021
cron.service - Regular background program processing daemon
Loaded: loaded (/usr/lib/systemd/system/cron.service, enabled)
Active: active (running)
ดูว่า ผลการทำงานเป็นอย่างไร
docker exec -it kxtest bash -c "date ; cat ~/test.log"
Sun Oct 10 03:02:05 UTC 2021
20211010-015701
20211010-015801
20211010-015901
20211010-030101
20211010-030201
เย้ ทำงานแล้ว
UPDATE
สำหรับ กรณีใช้ docker image อย่าง JupyterHub หรือ มันก็คือ Jupyter Notebook แบบที่ทำงานได้พร้อมกันหลายคน (แยก Login) ผมลองแล้ว สามารถใช้วิธีนี้ครับ
อ้างอิงจาก Dockerfile ที่ https://hub.docker.com/r/jupyterhub/jupyterhub/dockerfile
จะเห็นได้ว่า CMD ตัวสุดท้ายคือ [“jupyterhub”]
เราสามารถไปสั่งให้ start cron ได้โดย
docker exec -it myjupyterhub bash
vi $(which jupyterhub)
แล้วแก้ไข Code เพิ่มนิดเดียว จากของเดิม
import re
import sys
from jupyterhub.app import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
เป็น
import re
import sys
from jupyterhub.app import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
import subprocess
subprocess.run(["service", "cron", "start"])
sys.exit(main())
จากนั้น restart ได้เลยครับ