shell-ভারী pipeline-এর বদলে একটা Python স্ক্রিপ্ট — build, push, deploy আর Slack-এ খবর। কোন ধাপে ভাঙল সেটা বলা, বারবার চালানো নিরাপদ রাখা, secret কোডের বাইরে রাখা, আর Jenkinsfile-এ credentials বাঁধা।
কোড লেখার আগে ছবিটা মাথায় বসিয়ে নেওয়া দরকার, নইলে পরে প্রতিটা টুকরো আলাদা মনে হবে।
কেউ git-এ push করল। Jenkins সেটা টের পেল আর একটা build শুরু করল। Jenkins কোড নামাল, Docker Hub-এ login করল, তারপর একটা Python স্ক্রিপ্ট চালাল — আর সেই স্ক্রিপ্টটাই image বানাল, push করল, container চালাল, আর শেষে Slack-এ জানাল।
খেয়াল করো ভাগটা কোথায় হলো।
Jenkins-এর কাজ: কখন চলবে (trigger), কোথায় চলবে (agent), আর secret গুলো কোথা থেকে আসবে (credentials)।
Python-এর কাজ: আসল কাজটা, ধাপে ধাপে, আর ভুল হলে কী করতে হবে।
এই ভাগটা ইচ্ছাকৃত, আর এর একটা বড় সুবিধা আছে: স্ক্রিপ্টটা Jenkins ছাড়াও চলে। তুমি নিজের ল্যাপটপে python3 cicd.py দিয়ে একই জিনিস পরীক্ষা করতে পারো। pipeline-এর যুক্তি যদি Jenkinsfile-এর ভেতরে twenty লাইন shell হয়ে থাকত, তবে প্রতিটা পরীক্ষার জন্য commit করে push করে build-এর অপেক্ষা করতে হতো।
আর এখান থেকেই একটা নিয়ম বেরোয়, যেটা অনেক দল দেরিতে শেখে:
CI-এর ফাইল যত পাতলা, তত ভালো। Jenkinsfile, GitHub Actions-এর YAML, GitLab-এর .gitlab-ci.yml — এগুলোতে যত কম যুক্তি থাকবে, তত সহজে এক CI থেকে আরেক CI-তে যাওয়া যাবে, আর তত সহজে কাজটা হাতে চালিয়ে দেখা যাবে।
সত্যি কথা হলো, এই চারটে ধাপ shell-এও লেখা যায়:
docker build -t $IMAGE . docker push $IMAGE docker rm -f node-demo || true docker run -d --name node-demo -p 3000:3000 $IMAGE
চার লাইন। তাহলে Python কেন?
এক, ব্যর্থ হলে কোন ধাপে ব্যর্থ হলো সেটা জানা। উপরের shell-এ একটা লাইন ব্যর্থ হলে build লাল হবে, আর তুমি log ঘেঁটে বুঝবে কোনটা। স্ক্রিপ্টে প্রতিটা ধাপের নাম আছে, তাই বার্তাটা হয় "push failed with exit code 1" — খুঁজতে হয় না।
দুই, ব্যর্থ হলে কাউকে জানানো। shell-এ এটা করতে হলে প্রতিটা লাইনের পরে || notify বসাতে হবে, অথবা একটা trap। Python-এ একটা try/except পুরোটা ঘিরে নেয়।
তিন, শর্ত। "যদি main শাখা হয় তবে latest tag-ও দাও", "যদি test ব্যর্থ হয় তবে push কোরো না" — এগুলো shell-এ লেখা যায়, কিন্তু পাঁচটা শর্তের পরে ফাইলটা আর কেউ পড়তে পারে না।
চার, retry। Docker Hub মাঝে মাঝে সাড়া দিতে দেরি করে। "তিনবার চেষ্টা করো, প্রতিবার দ্বিগুণ অপেক্ষা করে" — Python-এ পাঁচ লাইন।
পাঁচ, আর সবচেয়ে বড়টা: পরীক্ষা করা যায়। notify() ফাংশনটা আলাদা করে ডেকে দেখা যায় Slack-এ বার্তা যায় কি না — পুরো pipeline না চালিয়ে।
তবে একটা সতর্কতা: shell-কে অকারণে তাড়িয়ে দিও না। একটা মাত্র docker build চালাতে Python লেখা বাড়াবাড়ি। Python আসে যখন ধাপগুলোর মধ্যে সম্পর্ক তৈরি হয় — একটার ফল দেখে আরেকটার সিদ্ধান্ত।
অ্যাপটা ইচ্ছে করেই তুচ্ছ — একটা Node.js server যা একটা JSON ফেরত দেয়। কারণ আমরা অ্যাপ শিখছি না, pipeline শিখছি।
app/server.js পোর্ট ৩০০০-এ শোনে, আর PORT environment variable থাকলে সেটা মানে। এই ছোট্ট বিষয়টাও একটা অভ্যাস: container-এর ভেতরের সব কিছু environment থেকে আসা উচিত, কোডে বসানো নয়।
আর Dockerfile:
FROM node:18-alpine WORKDIR /app COPY app/ . EXPOSE 3000 CMD ["node", "server.js"]
দুটো জিনিস খেয়াল করার মতো।
এক, COPY app/ . — আর build কোথা থেকে চলে। Dockerfile আছে project-এর মূল ফোল্ডারে, অ্যাপের ফাইল আছে app/-এ। তাই build চালানো হয় মূল ফোল্ডার থেকে: docker build -t <image> .। ওই শেষের বিন্দুটাই build context — Docker daemon-এর কাছে যে ফোল্ডারটা পাঠানো হয়।
নতুনদের সবচেয়ে সাধারণ ভুল এখানেই: Dockerfile-টা app/-এ রেখে মূল ফোল্ডার থেকে build চালানো, বা উল্টোটা। তখন COPY খুঁজে পায় না আর ভুল আসে "no such file or directory"।
দুই, alpine। node:18 প্রায় ১ গিগাবাইট, node:18-alpine ১২০ মেগাবাইটের কাছাকাছি। CI-তে এই পার্থক্যটা প্রতিটা build-এ টের পাওয়া যায় — push দ্রুত হয়, আর deploy-এ pull দ্রুত হয়।
১৪২টি অংশের ২৯টি দেখানো হয়েছে, ১১৩টি বাকি