এই পর্বে যা শিখবেন
- কোন কাজ Bash-এ, কোনটি Python বা Ansible-এ করা ভালো, সেই সিদ্ধান্ত
- Quoting,
set -euo pipefail, function, exit code,trapআরmktempঠিকভাবে ব্যবহার করা - এমন script লেখা, যা বারবার চালালেও নিরাপদ, আর যার আচরণ log থেকে বোঝা যায়
- Cron-এর পাঁচটি ঘর পড়া, পরের চলার সময় হিসাব করা আর তার ফাঁদ এড়ানো
- Lock দিয়ে overlap বন্ধ করা, আর cron ও systemd timer-এর মধ্যে বেছে নেওয়া
১ · কখন script লিখবেন — Bash, Python না Ansible
একটি কাল্পনিক অনলাইন বাজার অ্যাপের কথা ভাবুন। প্রতি রাতে একজন প্রকৌশলী server-এ ঢুকে database-এর backup নেন, ফাইলটি অন্য জায়গায় পাঠান আর পুরোনো backup মোছেন। কাজটি ১৫ মিনিটের। পর্ব ০৭-এর ভাষায় এটি toil: হাতে করা, বারবারের, আর এতে স্থায়ী কোনো উন্নতি নেই। একদিন তিনি ছুটিতে গেলেন, আর সেদিন backup হলো না।
Shell script হলো রান্নাঘরের রেসিপি খাতা। মা মুখে মুখে রান্না শেখালে প্রতিবার একটু আলাদা হয়, আর মা না থাকলে রান্নাই আটকে যায়। খাতায় লেখা থাকলে যে কেউ একই রান্না করতে পারে। কিন্তু খাতায় “লবণ ১০ চামচ” ভুল লেখা থাকলে প্রতিবার সেই ভুলটিই হুবহু হবে। তাই script-কেও কোডের মতো যত্নে লিখতে হয়।
হাতের কাজ script-এ আনার আগে পর্ব ০৭-এর হিসাবটি করুন: কাজটি বছরে কত ঘণ্টা খায়, আর script বানাতে ও রক্ষণাবেক্ষণে কত ঘণ্টা লাগবে। তারপর ঠিক করুন, কোন হাতিয়ার। Bash হলো কমান্ড জোড়া লাগানোর আঠা। Data structure বা জটিল যুক্তি এর শক্তি নয়।
| হাতিয়ার | কখন ভালো | কখন নয় |
|---|---|---|
| Bash | একটি server-এ কয়েকটি কমান্ড পরপর চালানো, ফাইল সরানো, ছোট glue কাজ | JSON ভাঙা, হিসাব, জটিল শাখা, দীর্ঘ যুক্তি |
| Python | API ডাকা, JSON, retry-র জটিল নিয়ম, unit test দরকার এমন কাজ | দুই লাইনের কাজ, বা যেখানে Python বসানো নেই |
| Ansible | অনেক server-এ একই configuration বসানো, চাওয়া অবস্থা বর্ণনা করা | একবারের ছোট কাজ, ঘন ঘন সময়সূচির কাজ |
| Terraform ধরনের IaC | Cloud resource বানানো আর তার অবস্থা ধরে রাখা | Server-এর ভেতরের দৈনন্দিন কাজ |
একটি ছোট হিসাব। ওপরের কাজটি বছরে ৩৬৫ × ১৫ = ৫,৪৭৫ মিনিট, অর্থাৎ প্রায় ৯১ ঘণ্টা। ধরে নিই script লিখতে, পরীক্ষা করতে আর monitoring বসাতে ১২ ঘণ্টা লাগে, আর বছরে রক্ষণাবেক্ষণে যায় ৮ ঘণ্টা। তাহলে প্রথম বছরেই প্রায় ৭১ ঘণ্টা বাঁচে। উল্টো দিকও আছে: বছরে দুবারের ৫ মিনিটের নিরীহ কাজের জন্য script বানালে খরচই বেশি। তবে যে কাজ কম হয় কিন্তু ভুল হলে বড় ক্ষতি, যেমন database failover, সেখানে সময়ের হিসাব ছাড়াও script বা runbook দরকার, কারণ চাপের মুহূর্তে মানুষ ভুল করে।
একটি প্রচলিত নিয়ম: Google-এর Shell Style Guide বলে, script ১০০ লাইনের বেশি হলে বা জটিল নিয়ন্ত্রণ-প্রবাহ এলে আরও গোছানো ভাষায় নেওয়ার কথা ভাবতে হবে। সীমাটি আইন নয়, তবে সংকেত হিসেবে ভালো। Ansible নিয়ে বিস্তারিত পর্ব ৫৬-এ, আর Terraform পর্ব ৫২-তে। Python দিয়ে automation দেখতে পারেন Python-এর automation পর্বে।
২ · Shebang, variable আর quoting — ছোট চিহ্নের বড় দাম
Script-এর প্রথম লাইনটিকে বলে shebang. এটি kernel-কে জানায়, এই ফাইল কোন প্রোগ্রাম দিয়ে চলবে।
#!/usr/bin/env bash
# env finds bash through PATH. #!/bin/bash is also common.
name="Rahim" # no spaces around =
echo "Hello, $name"chmod +x hello.sh দিলে ./hello.sh লিখে চালানো যায় (অনুমতির কথা পর্ব ০৮-এ)। একটি সতর্কতা: Debian আর Ubuntu-তে /bin/sh সাধারণত dash, Bash নয়। তাই #!/bin/sh লিখে Bash-এর [[ ]] বা array ব্যবহার করলে ভুল হতে পারে। Bash-এর সুবিধা চাইলে shebang-এও Bash লিখুন।
Quoting: সবচেয়ে সস্তা নিরাপত্তা
Quote ছাড়া variable লিখলে shell দুটি বাড়তি কাজ করে। মানটিকে ফাঁকা জায়গায় ভাঙে (word splitting), আর * থাকলে তাকে ফাইলের নামে বদলায় (glob). Quoting এই দুটি বন্ধ করে।
পার্সেলের ঠিকানায় লেখা “নতুন বাজার”। নতুন কর্মী যদি ফাঁকা দেখলেই আলাদা জায়গা ভাবেন, তাহলে তিনি “নতুন” আর “বাজার” নামের দুটি জায়গায় পার্সেল খুঁজবেন, আর আসল বাড়ি বাদ পড়বে। Quote হলো ঠিকানার চারপাশে দেওয়া বাক্স: ভেতরের সবটা এক ঠিকানা।
#!/usr/bin/env bash
# Three files. The target has a space in its name.
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
cd "$T"
fresh() { rm -f -- *; touch "old backup.tar" old backup.tar; }
left() { printf 'left:'; printf ' [%s]' *; echo; }
target="old backup.tar"
echo "--- without quotes ---"
fresh
set -- $target
echo "shell sees $# arguments: [$1] [$2]"
rm -f $target
left
echo "--- with quotes ---"
fresh
rm -f -- "$target"
left
echo "--- empty variable ---"
DIR=""
printf 'the command would become: rm -rf %s*\n' "$DIR/"
if ( : "${DIR:?}" ) 2>/dev/null; then
echo "guard passed"
else
echo 'guard ${DIR:?} stopped the script before rm'
fiআউটপুটের প্রথম অংশ বলে shell sees 2 arguments: [old] [backup.tar], তারপর left: [old backup.tar]. মানে যে ফাইল মোছার কথা, সেটিই রয়ে গেছে, আর নিরীহ দুটি ফাইল মুছে গেছে। Quote দিলে left: [backup.tar] [old], অর্থাৎ শুধু লক্ষ্যটি গেছে। শেষ অংশটি কিছু মোছে না, শুধু দেখায় the command would become: rm -rf /*, আর পাহারা ${DIR:?} তার আগেই থামিয়ে দেয়। -- বলে দেয় এরপরের কিছুই option নয়, ফলে -rf নামের ফাইলও বিপদ ঘটায় না।
সবচেয়ে দামি ফাঁদ: rm -rf "$DIR/"-এ DIR খালি হলে কমান্ডটি হয় rm -rf "/". GNU rm শুধু এই আক্ষরিক /-টি ঠেকায় (--preserve-root, এখনকার default). কিন্তু "$DIR/"* হলে shell আগেই সেটিকে /bin /etc /home ... তালিকায় বদলে দেয়, আর rm সুরক্ষার কিছু দেখতেই পায় না। তাই মোছার কমান্ডে লিখুন rm -rf -- "${DIR:?}/"*. এই ধরনের কমান্ড কখনো পরীক্ষার জন্যও বাস্তব মেশিনে চালাবেন না।
| লেখা | অর্থ |
|---|---|
"$x" | মান এক টুকরো থাকে; প্রায় সবসময় এটিই চাই |
"$@" | সব argument, প্রতিটি আলাদা ও অক্ষত |
${x:-7} | x খালি বা অনুপস্থিত হলে 7 ব্যবহার |
${x:?message} | x খালি হলে message দেখিয়ে script থামানো |
৩ · set -euo pipefail — ভুলে থামা, আর তার অবাক করা দিক
Bash-এর default আচরণ অবাক করার মতো: একটি কমান্ড ব্যর্থ হলেও পরের লাইন চলে। নিচের script-টি একটি বানান ভুলে কী করতে পারে, ধাপে ধাপে দেখুন। ধরুন cron এটিকে চালিয়েছে, তাই শুরুর ফোল্ডার ব্যবহারকারীর home.
1#!/usr/bin/env bash2set -euo pipefail # ধাপ ৬ থেকে3cd /srv/app/tmpp # বানান ভুল4rm -rf ./*5cp /srv/build/app.tar .6echo "deploy done"
| set -e | |
| এখনকার ফোল্ডার | |
| ফোল্ডারের ভেতরে | |
| শেষ exit code |
- শুরু। Script চলছে
/home/deployথেকে। সেখানে তিনটি দরকারি ফোল্ডার আছে।set -eএখন ধরুন বন্ধ, অর্থাৎ লাইন ২ নেই। cdব্যর্থ।tmp-এর জায়গায়tmppলেখা, তাই exit code 1. ফোল্ডার বদলায়নি, কিন্তু Bash থামে না।- আসল ক্ষতি।
rm -rf ./*ভেবেছিল সে/srv/app/tmp-এ আছে, অথচ সে আছে home-এ। তিনটি ফোল্ডারই গেল, exit code 0. - ভুল জায়গায় ফাইল।
app.tarhome-এ কপি হলো। Exit code আবার 0. কেউ কিছু টের পাচ্ছে না। - সবচেয়ে খারাপ অংশ। শেষ কমান্ড
echoসফল, তাই পুরো script-এর exit code 0. Cron-এর কাছে কাজটি সফল। - এবার
set -euo pipefailচালু। একই script, ফোল্ডারগুলো অক্ষত, লাইন ২ এখন চলছে। - প্রথম ভুলেই থামা।
cdব্যর্থ, script exit code 1 নিয়ে শেষ।rm,cp,echoকিছুই চলেনি, ফোল্ডার তিনটি নিরাপদ। ব্যর্থতাটিও এবার চোখে পড়বে।
তিনটি option আলাদা কাজ করে। একসাথে লেখা হয় বলে একটি মনে হয়, কিন্তু প্রতিটির আলাদা নাম আছে।
| Option | কাজ |
|---|---|
-e (errexit) | কোনো কমান্ড ব্যর্থ হলে script থামে |
-u (nounset) | অজানা variable ব্যবহার করলে script থামে; বানান ভুল ধরা পড়ে |
-o pipefail | Pipeline-এর যেকোনো ধাপ ব্যর্থ হলে পুরো pipeline ব্যর্থ |
Pipeline-এর exit code default-এ শুধু শেষ ধাপের। ফলে মাঝের ব্যর্থতা ঢাকা পড়ে। একই script দুভাবে চালিয়ে দেখুন। ব্যর্থতা ইচ্ছাকৃত, তবে আলাদা process-এ চলছে, তাই বাইরের প্রোগ্রাম ঠিকভাবেই শেষ হয়।
#!/usr/bin/env bash
# The same pipeline, without and with pipefail. sales.log does not exist.
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
cd "$T"
cat > report_a.sh <<'EOF'
set -e
cat sales.log 2>/dev/null | sort > report.txt
echo "report ready, sending $(wc -l < report.txt) lines"
EOF
cat > report_b.sh <<'EOF'
set -eo pipefail
cat sales.log 2>/dev/null | sort > report.txt
echo "report ready, sending $(wc -l < report.txt) lines"
EOF
echo "--- set -e only ---"
bash report_a.sh
echo "script exit code: $?"
echo "--- set -eo pipefail ---"
bash report_b.sh
echo "script exit code: $?"
echo "--- what each stage returned ---"
cat sales.log 2>/dev/null | sort | wc -l
echo "PIPESTATUS: ${PIPESTATUS[*]}"শুধু set -e-এ cat ব্যর্থ হলেও sort সফল, তাই লেখা আসে report ready, sending 0 lines আর script exit code: 0. একটি খালি report চুপচাপ চলে যেত। pipefail দিলে কোনো report-এর লাইন আসে না, শুধু script exit code: 1. শেষ লাইন PIPESTATUS: 1 0 0 দেখায়, তিন ধাপের প্রথমটিই ব্যর্থ ছিল।
একটি বাড়তি অভ্যাস: set -e থাকলেও গুরুত্বপূর্ণ জায়গায় ব্যর্থতার বার্তা নিজে লিখুন, যেমন cd /srv/app || { echo "cannot enter /srv/app" >&2; exit 1; }. এতে রাতের log পড়ে সকালে বোঝা যায়, ঠিক কোন ধাপে কেন থেমেছিল।
set -e-এর জানা ফাঁদ
set -euo pipefail সিট-বেল্টের মতো: দরকারি, কিন্তু দুর্ঘটনা পুরো ঠেকায় না। নিচের প্রতিটি আচরণ Bash 5.2-তে চালিয়ে মেলানো।
| লেখা | কী ঘটে | উপায় |
|---|---|---|
if deploy; then বা deploy || echo no | deploy function-এর ভেতরে -e কাজই করে না, ব্যর্থ লাইনের পরেও চলে | Function-এর ভেতরে প্রতিটি ধাপে || return 1, অথবা আলাদা script |
local x=$(cmd) | Exit code local-এর, cmd-এর নয়; ব্যর্থতা হারায় | দুই লাইনে: local x, তারপর x=$(cmd) |
((i++)), i যখন 0 | ফলের মান 0, তাই exit code 1, script থেমে যায় | i=$((i + 1)) |
grep কিছু না পেলে | Exit code 1, তাই সাধারণ “মিল নেই” ফলেও script থামে | grep ... || true, তবে ভেবে |
yes | head -1 | pipefail-এ বাঁদিকের ধাপ SIGPIPE-এ মরে (141), pipeline ব্যর্থ | Pipeline ছোট রাখা, বা অস্থায়ী ফাইল |
x=$(false; echo ok) | $( )-এর ভেতরে -e default-এ চলে না | shopt -s inherit_errexit (Bash 4.4+) |
একটি অভ্যাস: ShellCheck নামের বিনা মূল্যের tool script না চালিয়েই পড়ে আর এই ধরনের ভুল ধরে। যেমন quote ছাড়া variable (SC2086), cd-এর ব্যর্থতা না দেখা (SC2164), আর "$DIR/" ধরনের ঝুঁকি (SC2115). এটি editor-এ আর CI-তে রাখার পরামর্শ রইল। সতর্কবার্তা বন্ধ করার আগে তার কারণ পড়ে নিন।
৪ · Function, argument আর exit code
লম্বা script-কে ছোট function-এ ভাগ করুন। Bash-এর function কিছুটা আলাদা: সে মান “ফেরত” দেয় না। সে দেয় exit code (0 থেকে 255), আর লেখা দেয় stdout-এ।
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "usage: $0 PATH LIMIT_PERCENT" >&2; exit 2; }
# Prints the used percentage of a path, e.g. 73
used_percent() {
local path=$1
df --output=pcent "$path" | tail -n 1 | tr -dc '0-9'
}
[ "$#" -eq 2 ] || usage # $# = number of arguments
path=$1
limit=$2
used=$(used_percent "$path")
if [ "$used" -ge "$limit" ]; then
echo "ALERT: $path is ${used}% full" >&2
exit 1
fi
echo "ok: $path is ${used}% full"$1, $2 হলো argument, $# তাদের সংখ্যা, "$@" পুরো তালিকা, আর shift প্রথমটি বাদ দেয়। Function-এর ভেতরের variable-এ local দিন, নইলে সেটি পুরো script-এ ছড়ায়। Exit code-এর কিছু অর্থ প্রচলিত। Cron, systemd আর CI সবাই এই সংখ্যা দেখেই সফল বা ব্যর্থ ঠিক করে, তাই ব্যর্থ হলে script-কে অবশ্যই non-zero দিয়ে শেষ করতে হবে।
| Code | প্রচলিত অর্থ |
|---|---|
0 | সফল |
1 | সাধারণ ব্যর্থতা |
2 | ভুল ব্যবহার, যেমন ভুল argument (প্রচলিত রীতি, বাধ্যতামূলক নয়) |
126 / 127 | চালানোর অনুমতি নেই / কমান্ড পাওয়া যায়নি (cron-এ PATH ভুল হলে এই 127 দেখা যায়) |
128 + n | Signal n-এ মৃত্যু: 130 = SIGINT, 137 = SIGKILL, 143 = SIGTERM |
একটি অভ্যাস: বার্তা আর ফলাফল আলাদা রাখুন। ফলাফল stdout-এ, error আর log stderr-এ (>&2). তাহলে পরের কমান্ড pipe দিয়ে শুধু ফলাফল পায়।
Script ভুল করলে কীভাবে খুঁজবেন
তিনটি সহজ হাতিয়ার মনে রাখুন। bash -n script চালায় না, শুধু syntax পড়ে। bash -x প্রতিটি কমান্ড চালানোর আগে + চিহ্ন দিয়ে ছাপে, আর variable-এর আসল মান বসিয়ে দেখায়। ফলে ধরা পড়ে $DIR আসলে কী ছিল। শুধু একটি অংশ দেখতে চাইলে সেখানে set -x লিখে শেষে set +x দিন।
bash -n deploy.sh # syntax check only, runs nothing
bash -x deploy.sh # print each command after expansion
shellcheck deploy.sh # static analysis, if installed-x সব মান খোলাখুলি ছাপে। Script-এ password বা token থাকলে সেটি log-এ চলে যাবে। তাই production-এ, বিশেষ করে CI-এর log-এ, -x ব্যবহারের আগে ভাবুন।
৫ · পরিষ্কার করে বের হওয়া — mktemp আর trap
বিয়েবাড়ির ক্যাটারিংয়ে নিয়ম থাকে: অনুষ্ঠান ঠিকভাবে শেষ হোক বা মাঝপথে ঝড় আসুক, বের হওয়ার আগে চুলার গ্যাস বন্ধ করতে হবে। trap ঠিক সেই নিয়ম: “যেভাবেই বের হই, শেষ কাজ এটি।”
অস্থায়ী ফাইলের নাম হাতে ঠিক করবেন না। /tmp/backup.tmp ধরনের নিশ্চিত নাম দুটি কাজের মধ্যে ধাক্কা লাগায়। আর /tmp সবার জন্য খোলা বলে অনুমান করা যায় এমন নাম নিরাপত্তার ঝুঁকিও। mktemp অনন্য নাম বানায়, আর শুধু মালিকের অনুমতি রাখে।
1#!/usr/bin/env bash2set -euo pipefail3WORK=$(mktemp -d)4trap 'rm -rf -- "$WORK"' EXIT5pg_dump shop > "$WORK/shop.sql"6gzip "$WORK/shop.sql"7upload_backup "$WORK/shop.sql.gz"8echo "backup ok"
| WORK | |
| ভেতরে | |
| জায়গা | |
| trap | |
| exit code |
mktemp -d. একটি অনন্য ফোল্ডার তৈরি হলো। নামটি নমুনা, প্রতিবার আলাদা।- Trap আগে। ফোল্ডার বানানোর পরপরই, কাজ শুরুর আগে, trap বসানো হলো। যত আগে, তত কম ফাঁক।
- Dump. ধরে নিই database dump ২ GB. জায়গা এখন 2.0 GB.
- Compress.
gzipফাইলটিকে বদলে দিল। ধরে নিই ফল 0.4 GB, আসল ফাইলটি আর নেই। - Upload ব্যর্থ। Network সমস্যায় লাইন ৭ exit code 1 দিল।
set -escript থামাচ্ছে, লাইন ৮ চলবে না। - Trap চলল। বের হওয়ার মুহূর্তে
rm -rf -- "$WORK"চলল, জায়গা 0 GB. Exit code 1-ই রইল, ফলে ব্যর্থতার খবর হারাল না। - Trap না থাকলে? প্রতি ব্যর্থ রাতে 0.4 GB পড়ে থাকে। ৭ রাতে 7 × 0.4 = 2.8 GB. ছোট server-এ এভাবেই একদিন disk ভরে আর সব service থেমে যায়।
EXIT trap স্বাভাবিক শেষ, set -e-এর থামা আর exit সব ক্ষেত্রেই চলে। Bash-এ Ctrl+C (SIGINT) আর SIGTERM-এ থামলেও চলে; এই আচরণ অন্য shell-এ একই নাও হতে পারে। কিন্তু SIGKILL বা বিদ্যুৎ চলে গেলে কিছুই চলে না। তাই পুরোনো অস্থায়ী ফাইল মোছার একটি আলাদা ব্যবস্থাও দরকার হতে পারে। Signal নিয়ে বিস্তারিত অপারেটিং সিস্টেমের signal পর্বে পাবেন।
৬ · বারবার চালালেও নিরাপদ — idempotent script, dry-run, log আর retry
Idempotency মানে কাজটি একবার করা আর দশবার করার ফল একই। লিফটের বোতাম ভাবুন: পাঁচবার চাপলেও লিফট একবারই আসে। Automation-এ এটি জরুরি, কারণ script মাঝপথে ভেঙে যায়, আর তখন “আবার চালান” হওয়া চাই সবচেয়ে নিরাপদ উত্তর।
| দ্বিতীয়বারে ভাঙে | Idempotent রূপ |
|---|---|
mkdir logs | mkdir -p logs |
echo "x=1" >> app.conf (লাইন জমতে থাকে) | grep -qx 'x=1' app.conf || echo "x=1" >> app.conf |
ln -s new current | ln -sfn new current |
useradd app | id app >/dev/null 2>&1 || useradd app |
এবার backup rotation-সহ একটি ছোট কাজ দুবার চালানো যাক। ফাইলের নাম db-2026-03-01.tar.gz ধরনের। এই ISO তারিখের নাম বর্ণানুক্রমে সাজালে সময়ের ক্রমও পাওয়া যায়, তাই glob-এর সাজানো তালিকাই পুরোনো থেকে নতুনের ক্রম। ফাইলগুলো খালি, শুধু নাম দিয়ে কৌশলটি দেখানো।
#!/usr/bin/env bash
# Idempotent setup + backup rotation. The second run must change nothing.
set -euo pipefail
shopt -s nullglob
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
cd "$T"
mkdir backups
for d in 01 02 03 04 05; do : > "backups/db-2026-03-$d.tar.gz"; done
job() {
local today="2026-03-06" keep=3 i
mkdir -p backups logs
touch app.conf
if grep -qx 'retention=3' app.conf; then echo "conf: already set"
else echo 'retention=3' >> app.conf; echo "conf: added retention=3"; fi
if [ -e "backups/db-$today.tar.gz" ]; then echo "backup: $today exists"
else : > "backups/db-$today.tar.gz"; echo "backup: created $today"; fi
local files=(backups/db-*.tar.gz) # sorted: oldest first
local extra=$(( ${#files[@]} - keep ))
for (( i = 0; i < extra; i++ )); do
rm -- "${files[i]}"
echo "rotate: removed ${files[i]#backups/}"
done
[ "$extra" -gt 0 ] || echo "rotate: nothing to remove"
}
echo "=== first run ==="; job
echo "=== second run ==="; job
echo "=== final state ==="
printf '%s\n' backups/*
echo "conf lines: $(wc -l < app.conf)"প্রথম চালানোয় conf: added retention=3, backup: created 2026-03-06, তারপর সবচেয়ে পুরোনো তিনটি (01, 02, 03) মোছা। ছয়টি থেকে তিনটি থাকল। দ্বিতীয় চালানোয় conf: already set, backup: 2026-03-06 exists আর rotate: nothing to remove: কিছুই বদলায়নি। শেষে রইল 04, 05, 06, আর conf lines: 1. nullglob না দিলে ফাইল না থাকার সময় db-* লেখাটিই একটি “ফাইল” হিসেবে তালিকায় ঢুকত।
Rotation-এর আগে ভাবুন: নতুন backup যদি ব্যর্থ বা খালি হয়, পুরোনো মোছা কি ঠিক? নিয়ম করুন, নতুন backup-এর সফলতা (মাপ, checksum) যাচাইয়ের পরেই মোছা। আর backup-এর আসল পরীক্ষা হলো restore; বিস্তারিত পর্ব ২৪-এ।
Dry-run, log আর retry
Dry-run: --dry-run flag দিলে script শুধু বলে কী করত, করে না। ঝুঁকির কমান্ডগুলো একটি ছোট run function-এর ভেতর দিয়ে চালালেই এটি হয়ে যায় (পরের অংশের পুরো script দেখুন)। অনেক tool-এও আছে, যেমন rsync --dry-run.
Log: রাত ৩টার script-এর একমাত্র সাক্ষী তার log. প্রতিটি লাইনে UTC সময়, আর কী ঘটল সেটি লিখুন। logger -t backup "done" কমান্ড system log-এ লেখে। Cron-এ দুটি ধারাই ফাইলে নিতে চাইলে লিখুন >>job.log 2>&1. ক্রমটি জরুরি: আগে >>job.log, পরে 2>&1; উল্টো লিখলে error ফাইলে যাবে না। Log ফাইলও বাড়তে থাকে, তাই Linux-এর logrotate দিয়ে তারও মেয়াদ ঠিক করুন। অনেক server-এর log একসাথে খোঁজার কথা পর্ব ৬৪-এ।
Retry: Network ক্ষণিকের জন্য ব্যর্থ হতেই পারে। পর্ব ০৬-এর exponential backoff আর jitter এখানেও খাটে: অপেক্ষা দ্বিগুণ হতে থাকে, আর একটু এলোমেলো মেশানো হয়, যাতে অনেক server একসাথে ঝাঁপিয়ে না পড়ে (thundering herd).
# retry MAX_TRIES command args...
retry() {
local max=$1 n=1 delay=2
shift
until "$@"; do
if [ "$n" -ge "$max" ]; then
echo "giving up after $n tries: $*" >&2
return 1
fi
sleep $(( delay + RANDOM % 3 )) # jitter: 0 to 2 extra seconds
delay=$(( delay * 2 )) # 2, 4, 8, 16 ...
n=$(( n + 1 ))
done
}
# -f makes curl fail on HTTP errors; without it a 500 page counts as success
retry 5 curl -fsS -o report.csv https://example.com/report.csv৫ বার চেষ্টায় অপেক্ষা ৪ বার, jitter ছাড়া মোট ২ + ৪ + ৮ + ১৬ = ৩০ সেকেন্ড। মনে রাখবেন, যে কাজ idempotent নয়, তাকে retry করলে কাজটি দুবার হতে পারে। তাই retry আর idempotency একসাথে ভাবতে হয়।
৭ · Cron — পাঁচটি ঘর আর তার ফাঁদ
Cron হলো রেলের সময়সূচি। ঘড়ি মিললেই গাড়ি ছাড়ে, আগের গাড়ি পৌঁছাল কি না, যাত্রী উঠল কি না, সে দেখে না। আর গাড়ি ছাড়ার পরে কী হলো, স্টেশনের ঘড়ি তা জানে না।
Crontab-এর প্রতিটি লাইনে পাঁচটি সময়ের ঘর, তারপর কমান্ড। crontab -e দিয়ে নিজের তালিকা সম্পাদনা করা যায়, আর /etc/cron.d/-এর ফাইলে কমান্ডের আগে ব্যবহারকারীর নামও লিখতে হয়।
Crontab-এর পাঁচটি ঘর
minute · hour · day of month · month · day of week · command
একটি অদ্ভুত নিয়ম: “তারিখ” আর “বার”, দুটিই নির্দিষ্ট করলে শর্ত হয় অথবা, এবং নয়। 0 0 2 * 5 মানে “মাসের ২ তারিখে, অথবা যেকোনো শুক্রবারে”। নিচের simulator একটি কাল্পনিক ঘড়ি মিনিটে মিনিটে এগিয়ে এই নিয়মগুলো মিলিয়ে দেখে। Server ধরে নেওয়া হয়েছে UTC-তে।
// Next-run calculator for 5-field cron on a simulated clock. Edit and Run again.
const START = Date.UTC(2026, 1, 27, 23, 50); // Fri 27 Feb 2026, 23:50 UTC
const JOBS = ["*/15 * * * *", "30 2 * * *", "0 9 * * 0-4", "0 0 1 * *", "0 0 2 * 5", "0 3 31 2 *"];
function parse(field, lo, hi) { // "1-5", "*/15", "0,30"
const set = new Set();
for (const part of field.split(",")) {
const [range, step = "1"] = part.split("/");
const [a, b] = range === "*" ? [lo, hi] : range.split("-").map(Number);
for (let v = a; v <= (b ?? a); v += Number(step)) set.add(v);
}
return set;
}
function matcher(expr) {
const [mi, hr, dom, mon, dow] = expr.split(" ");
const [m, h, d, o, w] = [parse(mi, 0, 59), parse(hr, 0, 23),
parse(dom, 1, 31), parse(mon, 1, 12), parse(dow, 0, 7)];
if (w.has(7)) w.add(0); // 0 and 7 both mean Sunday
const anyDay = dom[0] === "*" || dow[0] === "*";
return t => {
const a = d.has(t.getUTCDate()), b = w.has(t.getUTCDay());
return m.has(t.getUTCMinutes()) && h.has(t.getUTCHours()) &&
o.has(t.getUTCMonth() + 1) && (anyDay ? a && b : a || b);
};
}
const show = t => t.toUTCString().slice(0, 3) + " " + t.toISOString().slice(0, 16).replace("T", " ");
const dhaka = t => new Date(t.getTime() + 6 * 3600e3).toISOString().slice(11, 16);
for (const expr of JOBS) {
const hit = matcher(expr), runs = [];
for (let i = 1; i <= 5 * 366 * 1440 && runs.length < 2; i++) { // max 5 years
const t = new Date(START + i * 60000);
if (hit(t)) runs.push(show(t) + " UTC = " + dhaka(t) + " Dhaka");
}
console.log(expr.padEnd(14), runs.length ? "" : "never in 5 years");
runs.forEach(r => console.log(" " + r));
}ঘড়ি শুরু শুক্রবার ২৩:৫০ UTC থেকে। */15-এর পরের চলা 00:00, কারণ ১৫-এর গুণিতক গোনা হয় ঘণ্টার শুরু থেকে, কাজ চালু করার সময় থেকে নয়। 30 2 * * *-এর ফল Sat 2026-02-28 02:30 UTC = 08:30 Dhaka: “রাত আড়াইটার” কাজ আসলে ঢাকার সকালে চলছে। 0 0 2 * 5 চলে Mon 2026-03-02-এ (২ তারিখ, যদিও সোমবার), তারপর Fri 2026-03-06-এ (শুক্রবার, যদিও ৬ তারিখ); এটিই “অথবা” নিয়ম। শেষ লাইনটি সবচেয়ে শিক্ষণীয়: 0 3 31 2 * মানে ৩১ ফেব্রুয়ারি, তাই ফল never in 5 years. Cron এমন লাইনে কোনো error দেয় না, শুধু চুপচাপ কখনো চালায় না। এই simulator শুধু সংখ্যার ঘর বোঝে; MON, JAN ধরনের নাম বা @daily ধরনের সংক্ষেপ এতে নেই, যদিও আসল cron-এ সেগুলো চলে।
Cron-এর ফাঁদগুলো
| ফাঁদ | কী ঘটে | উপায় |
|---|---|---|
| ছোট environment | Terminal-এ চলে, cron-এ চলে না। Cron .bashrc পড়ে না, PATH সাধারণত শুধু /usr/bin:/bin, আর environment variable প্রায় নেই। | কমান্ডের পুরো path, অথবা crontab-এর শুরুতে PATH=...; secret ফাইল থেকে পড়া (পর্ব ৩৩) |
| শুরুর ফোল্ডার | কাজটি চলে home থেকে, script-এর ফোল্ডার থেকে নয় | পুরো path, অথবা cd /srv/app || exit 1 |
% চিহ্ন | Crontab-এর লাইনে % মানে নতুন লাইন, তাই date +%F ভেঙে যায় | \% লেখা, অথবা কমান্ডটি script-এ রাখা |
| Time zone | Cron চলে server-এর ঘড়িতে। Cloud VM-এ অনেক সময় তা UTC | UTC-তেই ভাবা, আর ঢাকার সময় থেকে ৬ ঘণ্টা বিয়োগ |
| Overlap | আগের চলা শেষ না হলেও cron পরেরটি চালায় | flock (পরের অংশে) |
| নীরব ব্যর্থতা | আউটপুট mail হয়, mail ব্যবস্থা না থাকলে হারিয়ে যায়। কেউ জানেই না। | Log ফাইল, আর dead man's switch |
| মেশিন বন্ধ | সেই মিনিটে মেশিন বন্ধ থাকলে কাজটি বাদ পড়ে | anacron, অথবা systemd-এর Persistent=true |
একটি বাস্তবসম্মত crontab এমন দেখায়। প্রথম সারির variable-গুলো নিচের সব লাইনে খাটে।
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
# 20:30 UTC = 02:30 Dhaka. Output goes to a log, errors stay visible to MAILTO.
30 20 * * * /usr/local/bin/nightly-backup.sh /srv/backups >>/var/log/backup.logTime zone-এর একটি গল্প। ধরুন দল ঠিক করল, ব্যবহারকারী যখন ঘুমিয়ে, তখন ভারী backup চলবে। কেউ লিখলেন 30 2 * * *. Server UTC-তে, তাই কাজটি চলল ঢাকার সকাল ০৮:৩০-এ, ঠিক অফিস শুরুর সময়। অ্যাপ ধীর হলো, আর কেউ বুঝলেন না কেন। সমাধান সহজ: timedatectl দিয়ে server-এর ঘড়ি দেখা, আর crontab-এ মন্তব্যে দুই সময়ই লেখা। বাংলাদেশে এখন daylight saving নেই। কিন্তু যেসব দেশে আছে, সেখানে ঘড়ি বদলের রাতে স্থানীয় সময়ের কাজ বাদ পড়তে বা দুবার চলতে পারে। Server UTC-তে রাখলে এই ঝামেলাও থাকে না।
কোন ব্যবহারকারী হিসেবে চালাবেন? root-এর crontab সবচেয়ে সহজ, কিন্তু সেখানে ভুল হলে ক্ষতির সীমা থাকে না। কাজটির জন্য আলাদা সীমিত ব্যবহারকারী বানান, যে শুধু দরকারি ফোল্ডার ছুঁতে পারে। Password crontab-এর লাইনে লিখবেন না; chmod 600 করা ফাইলে বা secret manager-এ রাখুন।
নীরব ব্যর্থতা ধরার সবচেয়ে ভালো উপায় হলো উল্টো করে ভাবা: কাজ ব্যর্থ হলে খবর দেওয়া নয়, কাজ সফল হলে খবর দেওয়া। Script-এর শেষে একটি monitoring ঠিকানায় সংকেত যায়। নির্দিষ্ট সময়ে সংকেত না এলেই alert বাজে। এতে script না চললেও ধরা পড়ে, যা script-এর ভেতরের কোনো error handling পারে না। Alert নিয়ে বিস্তারিত পর্ব ৬৬-তে।
৮ · Overlap আর lock — দুটি চলা একসাথে নয়
একটি কাল্পনিক SMS কাজ প্রতি ৫ মিনিটে চলে (*/5 * * * *). এটি last_id ফাইল পড়ে, নতুন বিলের SMS পাঠায়, শেষে last_id লেখে। সাধারণত ১ মিনিটে শেষ হয়। একদিন SMS gateway ধীর হলো, আর কাজটি ৭ মিনিট নিল। ফলে তৈরি হলো একটি race condition. ধরে নিই ৫০১–৫২০ নম্বর বিল বাকি, আর ০২:০৩-এ আরও ৩টি (৫২১–৫২৩) আসে।
| পদ্ধতি | |
| ঘড়ি | |
| চলছে | |
| lock-এর মালিক | |
| last_id ফাইল | |
| মোট SMS গেছে | |
| দুবার পেয়েছেন |
- ০২:০০, lock ছাড়া। Run A শুরু,
last_id500, বাকি ২০টি। Gateway ধীর। - ০২:০৫, দ্বিতীয় চলা। Cron জানে না A এখনো চলছে। Run B শুরু। A এখনো ফাইল লেখেনি, তাই B-ও পড়ে 500, আর ধরে ৫০১–৫২৩, মোট ২৩টি।
- ০২:০৭, A শেষ। ২০টি SMS গেল,
last_id= 520. - ০২:০৮, B শেষ। Gateway তখন স্বাভাবিক। B ২৩টি পাঠাল, যার ২০টি আগেই গেছে। মোট ৪৩ SMS, ২০ জন গ্রাহক দুবার পেলেন, আর টাকাও দুবার গেল।
- ০২:০০, এবার
flock -n. একই পরিস্থিতি। A lock পেল, তারপর কাজ শুরু। - ০২:০৫, B আটকে গেল। Lock ব্যস্ত, তাই B অপেক্ষা না করে সঙ্গে সঙ্গে বেরিয়ে গেল। এটি ব্যর্থতা নয়, শুধু log-এ “skipped”.
- ০২:০৭, A শেষ। ২০টি SMS,
last_id= 520. Process শেষ হতেই kernel lock ছেড়ে দিল। - ০২:১০, C চলল। Lock পেল,
last_id520 থেকে বাকি ৩টি পাঠাল। মোট ২৩ SMS, কেউ দুবার পাননি।
গুদামঘরের একটিই চাবি। যিনি চাবি নিয়েছেন, তিনি ভেতরে; পরের জন দরজায় চাবি না পেলে ফিরে যান। আর flock-এর বিশেষ সুবিধা হলো, ভেতরের মানুষ হঠাৎ চলে গেলেও চাবি আপনা-আপনি দরজায় ফিরে আসে।
touch job.lock ধরনের পুরোনো lock file-এর সমস্যা হলো, script ক্র্যাশ করলে ফাইলটি থেকে যায়, আর পরের সব চলা চিরকাল আটকে থাকে। আবার “ফাইল আছে কি” দেখা আর বানানোর মাঝেও ফাঁক থাকে। flock kernel-এর lock নেয়, যা file descriptor বন্ধ হলেই ছাড়ে, তাই process মরে গেলেও আটকায় না। Crontab-এ এক লাইনেই লেখা যায়, আবার script-এর ভেতরেও বসানো যায়।
# In a crontab line: skip this run if the previous one holds the lock
*/5 * * * * flock -n /run/lock/sms.lock /usr/local/bin/send-sms.sh
# Inside a script: fd 9 stays open until the script ends
exec 9>/run/lock/sms.lock
flock -n 9 || { echo "previous run still active, skipping" >&2; exit 0; }-n মানে অপেক্ষা নয়; -w 30 দিলে ৩০ সেকেন্ড অপেক্ষা করে। Crontab-এর এক লাইনের রূপে lock না পেলে exit code হয় 1; -E 0 দিলে 0 হয়। মনে রাখবেন, lock শুধু একটি মেশিনের ভেতরে কাজ করে। একই cron দুটি server-এ চললে এই lock কিছুই থামাবে না। সেই সমস্যা নিচের ভাবনার প্রশ্নে আছে।
সব একসাথে: একটি পূর্ণ script
নিচের উদাহরণে এই পর্বের সব অভ্যাস এক জায়গায় আছে। Argument যাচাই, lock, অস্থায়ী ফোল্ডার, dry-run, log আর rotation. shop একটি কাল্পনিক database.
#!/usr/bin/env bash
# nightly-backup.sh — usage: nightly-backup.sh [--dry-run] DEST_DIR
set -euo pipefail
readonly KEEP=7
DRY_RUN=0
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
run() { if (( DRY_RUN )); then log "DRY-RUN: $*"; else "$@"; fi; }
if [[ ${1:-} == --dry-run ]]; then DRY_RUN=1; shift; fi
DEST=${1:?usage: nightly-backup.sh [--dry-run] DEST_DIR}
[[ -d $DEST ]] || die "no such directory: $DEST"
exec 9>/run/lock/nightly-backup.lock
flock -n 9 || { log "previous run still active, skipping"; exit 0; }
WORK=$(mktemp -d)
trap 'rm -rf -- "$WORK"' EXIT
FILE="db-$(date -u +%F).sql.gz"
pg_dump shop | gzip > "$WORK/$FILE"
run mv -- "$WORK/$FILE" "$DEST/"
shopt -s nullglob
backups=("$DEST"/db-*.sql.gz)
for (( i = 0; i < ${#backups[@]} - KEEP; i++ )); do
run rm -- "${backups[i]}"
done
log "finished: $FILE"৯ · systemd timer বনাম cron
আধুনিক Linux-এ systemd সব service চালায়, আর তার systemd timer cron-এর একটি শক্তিশালী বিকল্প। এখানে দুটি ফাইল লাগে: .service বলে কী চালাতে হবে, .timer বলে কখন। Service unit-এর ভেতরের কথা পর্ব ১১-তে।
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly database backup
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/nightly-backup.sh /srv/backups# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night
[Timer]
OnCalendar=*-*-* 02:30:00 Asia/Dhaka
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer # next and last run
systemd-analyze calendar "*-*-* 02:30:00 Asia/Dhaka" # check the expression
journalctl -u backup.service -n 50 # logs of the last runs
sudo systemctl start backup.service # run it once now, by hand| বিষয় | Cron | systemd timer |
|---|---|---|
| সহজতা | এক লাইন | দুটি ফাইল |
| Log | নিজে ব্যবস্থা করতে হয় | journal-এ আপনা-আপনি |
| বাদ পড়া কাজ | বাদই থাকে (anacron আলাদা) | Persistent=true দিলে মেশিন চালু হলে চলে |
| Overlap | নিজে flock দিতে হয় | Service চলন্ত থাকলে timer নতুন করে শুরু করে না |
| Time zone | সাধারণত server-এর (কিছু রূপে CRON_TZ) | Expression-এ লেখা যায়, যেমন Asia/Dhaka |
| Resource সীমা | নেই | MemoryMax= ইত্যাদি service-এ |
| একসাথে শুরু ছড়ানো | নেই | RandomizedDelaySec= |
| কোথায় পাওয়া যায় | প্রায় সব Unix-এ | শুধু systemd-চালিত Linux-এ; সাধারণ container-এ নয় |
সিদ্ধান্তের সহজ নিয়ম: ছোট, অল্প গুরুত্বের কাজে cron যথেষ্ট। Backup-এর মতো গুরুত্বপূর্ণ কাজে timer ভালো। Cloud-এ তৃতীয় পথও আছে: provider-এর managed scheduler (যেমন Amazon EventBridge Scheduler, Azure Logic Apps-এর recurrence, Google Cloud Scheduler), যা কোনো একটি server-এর ওপর নির্ভর করে না। Kubernetes-এও CronJob আছে।
আরও গভীরে: set -e-এর ব্যতিক্রমগুলো খামখেয়াল নয়, POSIX shell মানে লেখা আছে: if-এর শর্ত, && বা ||-এর শেষ অংশ বাদে বাকি অংশ, আর !-এর পরের কমান্ডে ব্যর্থতা উপেক্ষিত। Vixie cron ঘরানায় “তারিখ বা বার” নিয়মটি নির্ভর করে ঘরটি * দিয়ে শুরু কি না তার ওপর, তাই */2-ও “যেকোনো” ধরা হয়। flock advisory lock: যে প্রোগ্রাম lock না দেখে ফাইল ছোঁয়, kernel তাকে আটকায় না, আর NFS-এ আচরণ version-ভেদে আলাদা। systemd timer দুই রকম: OnCalendar= ঘড়ি ধরে, আর OnBootSec= বা OnUnitActiveSec= ধরে শেষ ঘটনার পর থেকে সময়।
পরীক্ষা ও ইন্টারভিউতে সাধারণত যে ধরনের প্রশ্ন আসে:
set -euo pipefailকী করে? ব্যর্থ কমান্ডে থামে, অজানা variable-এ থামে, আর pipeline-এর মাঝের ব্যর্থতা ধরে।- Terminal-এ চলে, cron-এ চলে না কেন? সাধারণত ছোট PATH আর environment, অথবা শুরুর ফোল্ডার ভিন্ন।
- Exit code 137 মানে? ১২৮ + ৯, অর্থাৎ SIGKILL, প্রায়ই memory ফুরিয়ে OOM killer-এর হাতে।
- Idempotent script কী? বারবার চালালেও ফল একই; যেমন
mkdir -p. - একই cron কাজ overlap হওয়া কীভাবে ঠেকাবেন?
flock -n, অথবা systemd timer; একাধিক server হলে কেন্দ্রীয় scheduler বা distributed lock.
১০ · AI-এর যুগে — AI-এর লেখা script চালানোর আগে
AI কয়েক সেকেন্ডে script লিখে দেয়, আর সেটি দেখতে গোছানো। কিন্তু script-এর ভুল প্রায়ই খালি variable, ব্যর্থতা বা দ্বিতীয়বার চালানোর সময় ধরা পড়ে, যা সাধারণ পরীক্ষায় আসে না। তাই একে অপরিচিত কারও লেখা কোডের মতো পড়ুন, বিশেষ করে সেটি rm, sudo বা cloud CLI ছুঁলে।
| AI-এর সাধারণ ভুল | যাচাই |
|---|---|
Quote ছাড়া variable, পাহারা ছাড়া rm -rf, কোনো সতর্কতা নেই | ShellCheck চালানো, ${DIR:?} বসানো, throwaway VM-এ পরীক্ষা |
set -e ছাড়া, অথবা || true দিয়ে ব্যর্থতা ঢাকা | ইচ্ছা করে একটি ধাপ ব্যর্থ করে exit code দেখা |
| Script-এ বসানো password, key বা token | Secret manager (পর্ব ৩৩); prompt-এও secret নয় |
| Cron লাইনে শুধু কমান্ডের নাম, time zone-এর কথা নেই | Server-এর time zone দেখা, পুরো path দেওয়া, ছোট environment-এ পরীক্ষা |
| Lock, log বা ব্যর্থতার খবর নেই | flock, log আর dead man's switch যোগ করা |
নেই এমন flag; 0.0.0.0/0 খোলা, * অনুমতি, public storage, latest tag | man আর অফিশিয়াল docs মেলানো; least privilege; নির্দিষ্ট version |
terraform destroy, kubectl delete ধরনের কমান্ড, খরচের কথা ছাড়া | --dry-run বা terraform plan, diff পড়া, budget alert |
নিচের prompt AI-কে ব্যর্থতার পথ নিয়েও ভাবতে বাধ্য করে। পাঠানোর আগে আসল server-এর নাম, IP আর secret মুছে দিন।
AI-এর script চালানোর আগে
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন। এগুলোর একক সঠিক উত্তর নেই; যুক্তিটিই আসল।
প্র ০১set -e এত ফাঁকফোকরে ভরা কেন? Bash কি এটি ঠিক করে দিতে পারত না?
এই আচরণের মূল শিকড় পুরোনো। Shell প্রথমে বানানো হয়েছিল মানুষের সরাসরি ব্যবহারের জন্য, আর Stephen Bourne-এর shell (১৯৭০-এর দশকের শেষ দিকের Unix) থেকেই আজকের ভাষার অনেক রীতি এসেছে। সেখানে কমান্ডের ব্যর্থতা কোনো “exception” নয়, একটি সাধারণ মান। grep-এর 1 মানে “মিল নেই”, যা অনেক সময় একেবারে ঠিক ফল। তাই ভাষাটির কাছে ব্যর্থতা আর “না” উত্তরের মধ্যে কোনো পার্থক্য নেই।
-e এই নকশার ওপর বসানো একটি চাদর। if grep ... লিখলে শর্তের ব্যর্থতায় script থামলে if অর্থহীন হয়ে যেত, তাই ব্যতিক্রম রাখতেই হলো। সমস্যা হলো, এই ব্যতিক্রম গভীরেও ছড়ায়: if-এর ভেতরে ডাকা function-এর সব লাইনেও -e নিষ্ক্রিয়। POSIX মানে এই আচরণ লিখিত হয়ে গেছে, আর পুরোনো অসংখ্য script এর ওপর নির্ভর করে। তাই Bash বদলাতে গেলে কোটি লাইনের কোড ভাঙার ঝুঁকি। Bash বরং নতুন option যোগ করেছে, যেমন inherit_errexit.
এ কারণে অভিজ্ঞ মহলেও মত আলাদা। Greg Wooledge-এর পরিচিত Bash wiki-র FAQ ১০৫ বলে, set -e-এর ওপর ভরসা না করে নিজে ব্যর্থতা দেখা ভালো। অনেক দল আবার একে জরুরি মনে করে, কারণ অসম্পূর্ণ জাল জাল না থাকার চেয়ে ভালো। বাস্তবসম্মত পথ হলো দুটিই: set -euo pipefail রাখা, আর গুরুত্বপূর্ণ কমান্ডে স্পষ্ট পরীক্ষা (|| die "...") লেখা।
একটি ছোট পরীক্ষা করে দেখতে পারেন। একটি function-এর ভেতরে ব্যর্থ কমান্ডের পরে একটি echo রাখুন। সরাসরি ডাকলে script থামবে, কিন্তু if-এর শর্তে ডাকলে echo-ও চলবে। একই কোড, ডাকার জায়গা বদলেই আচরণ বদলে গেল। এই কারণেই বড় script-এ set -e-এর ওপর চোখ বুজে ভরসা করা কঠিন।
মূল উপলব্ধি: ফাঁকগুলো ভুল নয়, পিছনের দিকের সামঞ্জস্যের দাম। যে ভাষায় error handling মূল নকশায় নেই, সেখানে চাদর দিয়ে পুরোটা ঢাকা যায় না। জটিলতা বাড়লে সেটিই Python-এ যাওয়ার সংকেত।
প্র ০২একটি খালি variable কীভাবে মানুষের পুরো ডেটা মুছে দিতে পারে? এখান থেকে ব্যবস্থার শিক্ষা কী?
সবচেয়ে পরিচিত প্রকাশ্য উদাহরণগুলোর একটি ২০১৫ সালের। Linux-এ Steam-এর একটি script-এ rm -rf "$STEAMROOT/"* ধরনের লাইন ছিল। প্রকাশিত bug report অনুযায়ী, Steam-এর ফোল্ডার সরানোর পরে variable-টি খালি হয়ে যেত, আর কমান্ডটি হয়ে যেত rm -rf "/"*. কিছু ব্যবহারকারী তাঁদের home-এর ফাইল হারান, এমনকি লেখার অনুমতি থাকা মাউন্ট করা ড্রাইভের ফাইলও। লেখক কোডের পাশে একটি মন্তব্যে আশঙ্কার কথাও লিখেছিলেন বলে আলোচনায় এসেছিল।
লক্ষ করুন, এখানে quote ঠিকই ছিল। অর্থাৎ শুধু “quote করুন” শিখলেই এই ভুল ঠেকানো যেত না। সমস্যা ছিল খালি মান সামলানো। GNU rm বহু বছর ধরে আক্ষরিক / মুছতে অস্বীকার করে, কিন্তু * shell-ই ভেঙে দেয়, তাই rm দেখে শুধু /bin, /home ধরনের সাধারণ তালিকা। কোনো একক স্তরের সুরক্ষা সব পথ বন্ধ করে না। বিশেষ করে যে script ব্যবহারকারীর কম্পিউটারে চলে, তার পরিবেশ লেখকের হাতে থাকে না, তাই “এটি তো খালি হতেই পারে না” ধরে নেওয়া সবচেয়ে বিপজ্জনক অনুমান।
ব্যবস্থার শিক্ষা কয়েকটি। প্রথমত, মুছে ফেলার মতো অপরিবর্তনীয় কাজে পাহারা দ্বিগুণ করুন: ${VAR:?}, পথটি প্রত্যাশিত উপসর্গ দিয়ে শুরু কি না তার পরীক্ষা, আর মোছার আগে log. দ্বিতীয়ত, blast radius ছোট রাখুন: script-কে আলাদা সীমিত ব্যবহারকারী হিসেবে চালান, root হিসেবে নয়, যাতে ভুল হলেও সে শুধু নিজের ফোল্ডার ছুঁতে পারে। systemd-এ User= আর ProtectHome= এই কাজে লাগে। তৃতীয়ত, ধরে নিন ভুল হবেই, তাই পরীক্ষিত backup রাখুন। চতুর্থত, এই ধরনের ঘটনায় লেখককে দোষ না দিয়ে পর্ব ০৭-এর blameless দৃষ্টিতে ব্যবস্থার ফাঁক খুঁজুন: review, ShellCheck, পরীক্ষার পরিবেশ।
মূল উপলব্ধি: নিরাপত্তা একটি চতুর লাইনে নয়, স্তরে স্তরে আসে। কম অনুমতি, পাহারা, dry-run আর backup একসাথে থাকলে একটি খালি variable বিপর্যয় না হয়ে একটি log-এর লাইন হয়ে যায়।
প্র ০৩Cron পঞ্চাশ বছরের পুরোনো। systemd timer এত সুবিধা দেওয়ার পরেও cron মরেনি কেন?
Cron-এর জন্ম ১৯৭০-এর দশকের Unix-এ, যখন একটি মেশিনে অনেক ব্যবহারকারী কাজ করতেন। ১৯৮৭ সালের দিকে Paul Vixie-র পুনর্লেখা সংস্করণ ব্যাপকভাবে ছড়ায়, আর আজকের Debian-এর cron ও Red Hat পরিবারের cronie মূলত সেই ঘরানার উত্তরাধিকার। পাঁচ ঘরের ভাষাটি এত দিনে একটি সর্বজনীন রীতি হয়ে গেছে। Kubernetes CronJob, CI system-এর schedule আর অনেক cloud scheduler একই ধরনের expression বোঝে, যদিও খুঁটিনাটিতে পার্থক্য থাকে (কিছু জায়গায় বছর বা সেকেন্ডের বাড়তি ঘর আছে)।
cron-এর শক্তি তার সরলতায়। এক লাইনে একটি কাজ, কোনো unit ফাইল নয়, daemon-reload নয়। প্রায় সব Unix-এ পাওয়া যায়। ছোট VPS-এ একটি লগ পরিষ্কার করার জন্য দুটি ফাইল লেখা অনেকের কাছে বাড়তি। আর পুরোনো server, script আর মানুষের অভ্যাসের বিশাল জড়তাও আছে।
অন্যদিকে systemd timer এসেছে সেই সমস্যাগুলো মেটাতে, যেগুলো cron-এর নকশায় ছিল না: log, নির্ভরতা, resource সীমা, মেশিন বন্ধ থাকলে বাদ পড়া কাজ। তবে এতে দাম আছে: systemd ছাড়া সিস্টেমে এটি নেই, আর সাধারণ container-এ systemd চলে না। Container-এর দুনিয়ায় সিদ্ধান্তটি প্রায়ই অন্য জায়গায় সরে যায়: orchestrator বা cloud scheduler-এ।
ইঞ্জিনিয়ারিংয়ে এটি একটি বড় নিয়মের উদাহরণ। “যথেষ্ট ভালো” আর সর্বত্র উপস্থিত প্রযুক্তি সহজে মরে না, কারণ বদলের খরচ তার ক্ষতির চেয়ে বেশি। তাই আসল প্রশ্নটি “কোনটি আধুনিক” নয়, “এই কাজ ব্যর্থ হলে কত ক্ষতি, আর সেই ব্যর্থতা কত দ্রুত জানব”। গ্রাহকের backup হলে timer আর monitoring; অস্থায়ী ফোল্ডার মোছার জন্য cron-ই যথেষ্ট।
মূল উপলব্ধি: হাতিয়ার বাছাই করুন কাজের ঝুঁকি দেখে, ফ্যাশন দেখে নয়। Cron আর timer প্রতিদ্বন্দ্বী নয়, দুটি আলাদা ঝুঁকির স্তরের উত্তর।
প্র ০৪GitLab-এর ২০১৭ সালের ঘটনা থেকে cron-এ চলা backup নিয়ে কী শেখার আছে?
২০১৭ সালের ৩১ জানুয়ারি GitLab.com-এর একটি বড় database incident হয়, আর প্রতিষ্ঠানটি খোলাখুলি তার postmortem প্রকাশ করে। প্রকাশিত বিবরণ অনুযায়ী, replication সমস্যা সামলানোর সময় একজন প্রকৌশলী ক্লান্ত অবস্থায় secondary server ভেবে primary server-এর data directory মুছে ফেলেন। ব্যক্তির ভুল এখানে শুরু মাত্র; শিক্ষাটি আসে এর পরে।
দল যখন backup খুঁজতে গেল, তখন দেখা গেল নিয়মিত pg_dump backup কাজ করছিল না। প্রকাশিত postmortem বলে, ব্যবহৃত pg_dump-এর version আর database-এর version মেলেনি, তাই ব্যর্থ হচ্ছিল। ব্যর্থতার খবর cron-এর email-এ আসার কথা ছিল, কিন্তু সেই email-গুলো গ্রহণকারী প্রান্তে DMARC-এর কারণে প্রত্যাখ্যাত হচ্ছিল, আর কেউ তা জানতেন না। অর্থাৎ script ব্যর্থ, আর ব্যর্থতার বার্তাও ব্যর্থ। শেষ পর্যন্ত একটি অন্য কাজের জন্য নেওয়া disk snapshot থেকে পুনরুদ্ধার হয়, আর প্রায় ছয় ঘণ্টার ডেটা হারায়।
এই পর্বের কয়েকটি বিষয় এখানে একসাথে দেখা যায়। নীরব ব্যর্থতা: “error হলে জানানো” নকশা নিজেই ব্যর্থ হতে পারে। তাই dead man's switch দরকার, যেখানে সফলতার সংকেত না এলে alert বাজে। Version-এর অমিল: script যে পরিবেশ ধরে নিয়েছে, তা বদলে গেছে, অথচ exit code আর log কেউ দেখছিল না। Backup-এর অর্থ: ফাইল থাকা মানেই backup নয়; নিয়মিত restore করে দেখা না হলে সেটি শুধু একটি আশা। আর ধ্বংসাত্মক কমান্ডে কোন মেশিনে আছি, তা স্পষ্ট দেখানো (যেমন prompt-এর রং) সহজ একটি সুরক্ষা। ঘটনার পরে সবচেয়ে প্রশংসিত দিকটি ছিল খোলাখুলি প্রকাশ: প্রকাশিত বিবরণে ব্যক্তিকে দোষ না দিয়ে ব্যবস্থার ফাঁকগুলো তালিকা করা হয়েছিল, যা পর্ব ০৭-এর blameless সংস্কৃতির একটি পরিচিত উদাহরণ।
মূল উপলব্ধি: একটি backup script তখনই সফল, যখন নিয়মিত restore প্রমাণ করে সে কাজ করছে, আর তার নীরবতা কেউ টের পায়। পর্ব ২৪-এ এই পরীক্ষার পদ্ধতি দেখবেন।
প্র ০৫একই cron কাজ ১০টি server-এ চললে কী হবে? flock কেন যথেষ্ট নয়, আর এর সমাধান কী?
পর্ব ১৭-এর autoscaling-এ একই image থেকে অনেক server তৈরি হয়। image-এর ভেতরে crontab থাকলে প্রতিটি server রাত ২টায় একই কাজ চালাবে। মাসিক বিল পাঠানোর কাজ হলে গ্রাহক ১০টি বিল পাবেন। flock এখানে অসহায়, কারণ তার lock থাকে একটি মেশিনের kernel-এ; অন্য মেশিন তার খবরই জানে না। আবার scale-in হলে যে server-এ কাজটি চলছিল, সেটিই মাঝপথে বন্ধ হয়ে যেতে পারে।
সমাধানের কয়েকটি পথ আছে, আর প্রতিটির দাম আছে। এক, একটি নির্দিষ্ট “scheduler server”: সহজ, কিন্তু সেটি নষ্ট হলে সব কাজ বন্ধ, অর্থাৎ single point of failure. দুই, কেন্দ্রীয় lock: database-এর advisory lock বা অন্য কোনো সমন্বয় ব্যবস্থায় lock নেওয়া। এতে lease-এর মেয়াদ ঠিক করা কঠিন: কাজ ধীর হলে মেয়াদ শেষ হয়ে দ্বিতীয়জন ঢুকে পড়তে পারে। তিন, managed scheduler বা Kubernetes CronJob (concurrencyPolicy: Forbid দিয়ে), যেখানে সময়ের দায়িত্ব server-এর বাইরে। চার, scheduler শুধু একটি বার্তা queue-তে দেয়, আর worker-রা তা তুলে নেয়।
কিন্তু একটি কঠিন সত্য বাকি থাকে। Distributed ব্যবস্থায় এই scheduler-গুলো সাধারণত “অন্তত একবার” চালানোর চেষ্টা করে, “ঠিক একবার” নিশ্চয়তা দেয় না। Network বিচ্ছিন্ন হলে দুজনই ভাবতে পারে কাজটি তার। তাই শেষ রক্ষা lock নয়, কাজটির নিজের idempotency: বিলের টেবিলে “এই মাসের বিল পাঠানো হয়েছে” চিহ্ন, unique key বা পর্ব ০৬-এর idempotency token. তাহলে কাজ দুবার চললেও ফল একবারের সমান।
এই চিন্তাটি পরে পর্ব ৭০-এর queue আর পর্ব ৭১-এর serverless নকশায় বারবার ফিরবে।
মূল উপলব্ধি: Lock চলার সংখ্যা কমায়, কিন্তু নিরাপত্তা দেয় idempotency. একাধিক মেশিনে সময়সূচি মানেই ধরে নেওয়া, কাজ দুবার চলতে পারে, আর সেভাবেই কাজটিকে নকশা করা।
অনুশীলন
এগুলোর জন্য কোনো cloud account লাগবে না। নিজের কম্পিউটারে চালাতে চাইলে শুধু ফাঁকা পরীক্ষার ফোল্ডারে চালান।
-
Cron আর হিসাব: Server UTC-তে চলে। (ক) প্রতিদিন ঢাকার সময় রাত ০২:৩০-এ চলবে এমন লাইন লিখুন। (খ) রবি থেকে বৃহস্পতি ঢাকার সকাল ৯টায়। (গ)
0 */6 * * *দিনে কতবার চলে, আর কখন? (ঘ) ৩ GB-র দৈনিক backup-এর শেষ ১৪টি রাখা হয়, আর নতুনটি নেওয়ার পরে পুরোনো মোছা হয়। Disk-এ সর্বোচ্চ কত জায়গা লাগবে?(ক) ০২:৩০ − ৬ ঘণ্টা = আগের দিনের ২০:৩০ UTC, তাই
30 20 * * *. প্রতিদিনের কাজ বলে দিন বদলানো সমস্যা নয়; নির্দিষ্ট বারের কাজ হলে বারটিও এক দিন পিছিয়ে যেত।(খ) ০৯:০০ − ৬ = ০৩:০০ UTC, একই দিনে। তাই
0 3 * * 0-4.(গ) ৪ বার: ০০:০০, ০৬:০০, ১২:০০ আর ১৮:০০ (server-এর ঘড়িতে)।
(ঘ) মোছার ঠিক আগে ১৫টি থাকে: ১৫ × ৩ = ৪৫ GB, ৪২ GB নয়। Compress বা dump-এর অস্থায়ী জায়গা হিসাবে ধরলে আরও বেশি।
-
নকশা ও সমালোচনা: একজন সহকর্মীর crontab লাইন:
0 * * * * cd /opt/app; ./sync.sh; rm -rf $TMPDIR/*. Script-টি মাঝে মাঝে ৯০ মিনিট চলে। অন্তত পাঁচটি সমস্যা ধরুন, আর একটি ভালো রূপ প্রস্তাব করুন।১.
cdব্যর্থ হলেও;-এর কারণে পরের কমান্ড চলে;&&দরকার। ২.$TMPDIRcron-এর environment-এ সম্ভবত নেই। তখন/*হয়ে যায়, যা সবচেয়ে বিপজ্জনক অংশ। ৩. Quote আর${...:?}পাহারা নেই। ৪. প্রতি ঘণ্টায় চলে অথচ ৯০ মিনিট লাগতে পারে, তাই overlap হবে; lock নেই। ৫. Log নেই, ফলে ব্যর্থতা নীরব। ৬../sync.sh-এর সফলতা না দেখেই মোছা হচ্ছে।ভালো রূপ:
0 * * * * flock -n /run/lock/sync.lock /usr/local/bin/sync-job.sh >>/var/log/sync.log 2>&1. আরsync-job.sh-এর ভেতরেset -euo pipefail,mktemp -d,trap, আর শেষে সফলতার সংকেত। গুরুত্বপূর্ণ কাজ হলে systemd timer আরও ভালো। -
আউটপুট অনুমান করুন: নিচের script চালালে কী কী লাইন ছাপা হবে, আর exit code কত হবে? আগে নিজে ভাবুন, তারপর উত্তর দেখুন।
#!/usr/bin/env bash set -euo pipefail count=0 for f in a b; do count=$((count + 1)) done out=$(echo hello | tr a-z A-Z) echo "$out $count" printf "x y\n" | grep -c z || echo "no match" false | true echo "pipe ok?"Bash 5.2-এ চালানো ফল:
HELLO 2 0 no matchExit code 1, আর
pipe ok?ছাপা হয় না।grep -cমিল না পেলে0ছাপে, আবার exit code 1 দেয়, তাই||-এর ডান অংশও চলে।false | true-তে শেষ ধাপ সফল হলেওpipefailপুরো pipeline-কে ব্যর্থ ধরে, আরset -escript থামায়।pipefailনা থাকলে শেষ লাইনটিও ছাপা হতো। -
নকশা: একটি কাল্পনিক ছোট VPS-এ প্রতি রাতে log folder compress করে ৩০ দিনের বেশি পুরোনো ফাইল মুছতে হবে। Cron না systemd timer বেছে নেবেন, আর এই কাজের জন্য কোন কোন সুরক্ষা বসাবেন? ফল কীভাবে যাচাই করবেন?
কাজটি কম ঝুঁকির, তাই cron চলে। তবে VPS মাঝে মাঝে বন্ধ থাকলে বা log দরকার হলে timer-এ
Persistent=trueসুবিধাজনক। একটিমাত্র উত্তর নেই; যুক্তিটি লিখুন।সুরক্ষা:
set -euo pipefail; ফোল্ডারreadonly LOG_DIR=/var/log/myapp, যাতে খালি হওয়ার সুযোগ নেই; মোছার আগেfind "$LOG_DIR" -name '*.gz' -mtime +30 -printদিয়ে dry-run; তারপর-delete;flock; সীমিত ব্যবহারকারী,rootনয়। অনেক ক্ষেত্রে নিজের script-এর বদলে Linux-এরlogrotate-ই ভালো: আগে দেখে নিন, tool আগে থেকেই আছে কি না।যাচাই: disk ব্যবহারের ধারা দেখা, script-এর শেষের log, আর সফলতার সংকেত। পরীক্ষার ফোল্ডারে
touch -dদিয়ে পুরোনো তারিখের ফাইল বানিয়ে আগে চালিয়ে দেখা যায়।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নিরাপদ Bash script-এর অংশগুলো ওপর থেকে নিচে সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Shebang লাইন
set -euo pipefail- Argument যাচাই
flockদিয়ে lock নেওয়াmktemp -dআরtrap ... EXIT- মূল কাজ, তারপর সফলতার log আর সংকেত
Bash script-এ DIR="" থাকলে rm -rf "$DIR/"* কমান্ডটি কেন ভয়ংকর?
- কারণ এটি syntax error দিয়ে script থামিয়ে দেয়
- কারণ এটি শুধু বর্তমান ফোল্ডার মুছে দেয়
- কারণ পথটি
/*হয়ে যায়, আর--preserve-rootএটি ঠেকাতে পারে না - কারণ
rmতখন DIR নামের ফাইলটি মোছে
একটি script-এ শুধু set -e আছে। cat missing.log | sort > out.txt লাইনে missing.log নেই। কী ঘটবে?
- Script সঙ্গে সঙ্গে থেমে যাবে
- Script চলতে থাকবে, আর
out.txtখালি থাকবে sortচলবেই না, কিন্তু script এগোবে- Bash
missing.logতৈরি করে আবার চেষ্টা করবে
প্রচলিত Vixie-ধরনের cron-এ 0 0 2 * 5 কখন চলে?
- শুধু সেই ২ তারিখে, যেদিন শুক্রবার
- প্রতি বছর মে মাসের ২ তারিখে
- প্রতিদিন রাত ২টায়, শুধু শুক্রবার বাদে
- মধ্যরাতে, প্রতি মাসের ২ তারিখে অথবা প্রতি শুক্রবারে
একটি server-এর ঘড়ি UTC. Crontab-এর 30 2 * * * লাইনটি বাংলাদেশের সময়ে (UTC+6) কখন চলবে?
- সকাল ০৮:৩০
- রাত ০২:৩০
- আগের দিন রাত ০৮:৩০
- দুপুর ০২:৩০
touch job.lock ধরনের lock-এর তুলনায় flock-এর প্রধান সুবিধা কোনটি?
- এটি একাধিক server-এর মধ্যেও কাজ করে
- Process মরে গেলে lock আপনা-আপনি ছেড়ে যায়
- এটি কাজের সময় কমিয়ে দেয়
- এটি অন্য যেকোনো প্রোগ্রামকে ফাইলটি পড়তে বাধা দেয়
একটি server রাত ০২:০০ থেকে ০৪:০০ পর্যন্ত বন্ধ ছিল। ০২:৩০-এর backup কাজটি মেশিন চালু হওয়ার পরে চালাতে systemd timer-এ কোন সেটিং লাগে?
RandomizedDelaySec=MemoryMax=Persistent=trueType=oneshot
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Bash হলো আঠা: কয়েকটি কমান্ড জোড়া লাগাতে ভালো। জটিল যুক্তিতে Python, অনেক server-এ Ansible.
- সবসময় quote করুন, আর মোছার আগে
${DIR:?}. খালি variable আর*একসাথে হলে--preserve-root-ও বাঁচায় না। set -euo pipefail,trapআরmktempপ্রতিটি script-এর শুরুতে। তবেset -e-এর ফাঁকগুলো মনে রাখুন।- Idempotent, dry-run, log, backoff আর
flock: “আবার চালান” যেন সবসময় নিরাপদ উত্তর হয়। - Cron-এর ঘড়ি server-এর, environment ছোট আর ব্যর্থতা নীরব। গুরুত্বপূর্ণ কাজে systemd timer আর সফলতার সংকেত ব্যবহার করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ১১ · Service চালানো — process, systemd, journalctl আর resourceপরবর্তী পর্বScript থেকে সব সময় চলা service-এ যাওয়া।
- ক্লাউড কম্পিউটিং ও DevOps · কোর্স হোম৮১টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ০৭ · DevOps আর SREআগের ধারণাToil মাপা আর automation-এর হিসাব।
- Shell আর process-এর হাতিয়ারগভীরে যানProcess, pipe আর shell-এর ভেতরের কাজ।
- Python দিয়ে automationপাশের ট্র্যাকBash ছোট পড়লে পরের ধাপ।
- Resilience patternগভীরে যানRetry, backoff আর timeout-এর নকশা।