পাঠ ৫৩ · ৫৮-এর মধ্যে · মডিউল ১২
Home / Courses / Full-Stack Web Frameworks / CI/CD পাইপলাইন

ফুল-স্ট্যাক প্রজেক্টের জন্য CI/CD পাইপলাইন

CI/CD pipelines for full-stack projects
৯ মিনিট পড়া উচ্চ-মধ্যবর্তী · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • CI (Continuous Integration) ও CD (Continuous Delivery/Deployment)-এর মূল ধারণা
  • একটি সাধারণ পাইপলাইনের স্টেজ ক্রম — lint, test, build, deploy — এবং প্রতিটির উদ্দেশ্য
  • প্রথম ব্যর্থ স্টেজেই কেন এবং কীভাবে পাইপলাইন থেমে যায় (early-stop)
  • একটি সত্যিকারের Python পাইপলাইন রানার লিখে, কল-কাউন্ট দিয়ে যাচাই করে দেখা যে skip করা স্টেজ সত্যিই চালানো হয়নি

১ · CI/CD পাইপলাইনের স্টেজ ক্রম

যখন একজন ডেভেলপার নতুন কোড পুশ করেন, তখন সেই কোড সরাসরি প্রোডাকশনে যাওয়ার আগে কয়েকটি স্বয়ংক্রিয় স্টেজের মধ্য দিয়ে যায় — একটি CI/CD পাইপলাইন। প্রতিটি স্টেজের নির্দিষ্ট উদ্দেশ্য আছে, এবং স্টেজগুলো একটি নির্দিষ্ট ক্রমে চলে কারণ পরের স্টেজ আগেরটির সাফল্যের উপর নির্ভর করে — যেমন কোড lint-এ ব্যর্থ হলে টেস্ট চালানোর কোনো মানে নেই।

lint test build deploy কোড স্টাইল/সিনট্যাক্স চেক ইউনিট/ইন্টিগ্রেশন টেস্ট আর্টিফ্যাক্ট/ইমেজ তৈরি সার্ভারে প্রকাশ
যেকোনো একটি স্টেজ ব্যর্থ হলে (যেমন test) তার ডানদিকের সব স্টেজ (build, deploy) সম্পূর্ণ skip হয়ে যায় — কখনো চালানোই হয় না।
lint
কোড স্টাইল ও সাধারণ ভুল (আনইউজড ভ্যারিয়েবল, সিনট্যাক্স সমস্যা) দ্রুত ধরে — সবচেয়ে সস্তা ও দ্রুততম চেক, তাই প্রথমে চলে।
test
ইউনিট ও ইন্টিগ্রেশন টেস্ট চালিয়ে কোড আচরণগতভাবে সঠিক কি না যাচাই করে।
build
ডিপ্লয়যোগ্য আর্টিফ্যাক্ট (কন্টেইনার ইমেজ, বান্ডেল) তৈরি করে — lint ও test পাস না করলে এটি চালানোর কোনো মানে নেই।
deploy
বৈধ, টেস্ট-পাস আর্টিফ্যাক্টটি আসল সার্ভারে প্রকাশ করে — শেষ ও সবচেয়ে ঝুঁকিপূর্ণ স্টেজ, তাই সব আগের স্টেজ সফল হলেই এখানে পৌঁছায়।

২ · একটি সত্যিকারের পাইপলাইন রানার — early-stop ও কল-কাউন্ট যাচাই

নিচের কোড সেলে প্রতিটি স্টেজ একটি real ফাংশন — যা কল হলে নিজের call_counts কাউন্টার বাড়ায়, আর একটি ইনজেক্ট করা fail_stages সেটে নিজের নাম থাকলে False, নাহলে True রিটার্ন করে। run_pipeline() ফাংশনটি স্টেজগুলো ক্রমানুসারে চালায় — একবার কোনো স্টেজ ব্যর্থ হলে, তার পরের সব স্টেজকে "SKIPPED" চিহ্নিত করে দেয়, কিন্তু তাদের ফাংশন কখনো কল করে না। কল-কাউন্ট প্রিন্ট করেই আমরা প্রমাণ করব যে skip করা স্টেজগুলোর ফাংশন সত্যিই একবারও চলেনি।

Python
STAGE_ORDER = ["lint", "test", "build", "deploy"]

def make_stage(stage_name, fail_stages, call_counts):
    """একটি স্টেজ ফাংশন তৈরি করে দেয়। এই ফাংশনটি আসলেই কল হলে
    call_counts[stage_name] বাড়ায় -- তাই এটি কতবার কল হয়েছে তা পরে যাচাইযোগ্য।
    fail_stages সেটে stage_name থাকলে False (ব্যর্থ), নাহলে True (সফল) রিটার্ন করে।"""
    def stage_fn():
        call_counts[stage_name] += 1
        return stage_name not in fail_stages
    return stage_fn

def run_pipeline(fail_stages):
    """STAGE_ORDER অনুযায়ী প্রতিটি স্টেজ ক্রমানুসারে চালায়। কোনো স্টেজ ব্যর্থ হলে
    সাথে সাথে থেমে যায় -- পরের স্টেজগুলোর ফাংশন একবারও কল করা হয় না, শুধু
    ফলাফলে "SKIPPED" হিসেবে চিহ্নিত করা হয়। রিটার্ন করে: (results, call_counts, failed_at)।"""
    call_counts = {name: 0 for name in STAGE_ORDER}
    stages = {name: make_stage(name, fail_stages, call_counts) for name in STAGE_ORDER}

    results = {}
    failed_at = None
    for name in STAGE_ORDER:
        if failed_at is not None:
            results[name] = "SKIPPED"
            continue                      # এই স্টেজের ফাংশন কখনো কল করা হচ্ছে না
        succeeded = stages[name]()
        results[name] = "SUCCESS" if succeeded else "FAILED"
        if not succeeded:
            failed_at = name

    return results, call_counts, failed_at

def print_report(label, fail_stages):
    results, call_counts, failed_at = run_pipeline(fail_stages)
    print(f"=== {label} ===")
    for name in STAGE_ORDER:
        print(f"  {name:8s} -> {results[name]:8s}  (call_count={call_counts[name]})")
    if failed_at is None:
        print("  ফলাফল: পাইপলাইন সবুজ -- সব ৪টি স্টেজ সফল হয়েছে\n")
    else:
        print(f"  ফলাফল: '{failed_at}' স্টেজে ব্যর্থতার কারণে থেমে গেছে -- পরের স্টেজগুলো কখনো চালানো হয়নি\n")
    return call_counts

# পরিস্থিতি ১ -- সব স্টেজ সফল
print_report("রান ১ -- সব স্টেজ পাস", fail_stages=set())

# পরিস্থিতি ২ -- 'test' স্টেজে ইচ্ছাকৃতভাবে ব্যর্থতা ইনজেক্ট করা হলো
call_counts_run2 = print_report("রান ২ -- 'test' স্টেজে ইনজেক্টেড ব্যর্থতা", fail_stages={"test"})

# --- যাচাই: skip করা 'build' ও 'deploy' স্টেজের ফাংশন সত্যিই কখনো কল হয়নি ---
assert call_counts_run2["build"] == 0, "build স্টেজ ভুলবশত কল হয়ে গেছে!"
assert call_counts_run2["deploy"] == 0, "deploy স্টেজ ভুলবশত কল হয়ে গেছে!"
print(
    f"যাচাই সফল: রান ২-এ build এর call_count={call_counts_run2['build']}, "
    f"deploy এর call_count={call_counts_run2['deploy']} -- অর্থাৎ এই দুটো ফাংশন "
    f"সত্যিই একবারও চালানো হয়নি, শুধু 'SKIPPED' হিসেবে দেখানো হয়েছে।"
)

    
রান ১-এ fail_stages খালি সেট, তাই প্রতিটি স্টেজ ফাংশন কল হয়ে True রিটার্ন করে — চারটি স্টেজেরই call_count হয় 1, এবং failed_at থাকে None। রান ২-এ test কল হওয়ার সময় (তার call_count ১-এ যায়) এটি False রিটার্ন করে কারণ "test" in fail_stages — সাথে সাথে failed_at = "test" সেট হয়ে যায়, আর লুপের পরের ধাপগুলোতে (build, deploy) if failed_at is not None শর্ত সত্যি হওয়ায় সেই স্টেজের ফাংশন একবারও কল করা হয় না — তাদের call_count শূন্যই থেকে যায়, যা assert দুটো নিশ্চিত করে।
মূল কথা · Key takeaway

CI/CD পাইপলাইন একটি ক্রমিক গেটকিপার সিস্টেম — প্রতিটি স্টেজ শুধু তখনই চলে যখন তার আগের সব স্টেজ সফল হয়েছে, আর প্রথম ব্যর্থতাতেই বাকি সব স্টেজ (এবং তাদের সম্ভাব্য ঝুঁকিপূর্ণ কাজ, যেমন প্রোডাকশনে ডিপ্লয় করা) সম্পূর্ণভাবে এড়িয়ে যাওয়া হয়। এই early-stop আচরণ নিছক প্রিন্ট করা বার্তা নয় — উপরের কোডে দেখানো হয়েছে skip করা স্টেজের ফাংশন সত্যিই কল হয় না, প্রকৃত call-count দিয়ে যাচাইযোগ্য।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ যদি build স্টেজের ফাংশন test ব্যর্থ হওয়ার পরও কল হতো, তাহলে বাস্তব জগতে কী ক্ষতি হতে পারত?

বাস্তবে build স্টেজ সময় ও কম্পিউট রিসোর্স খরচ করে একটি আর্টিফ্যাক্ট তৈরি করে যা কখনো ব্যবহৃত হবে না (যেহেতু টেস্ট ইতিমধ্যে ব্যর্থ, এই কোড ডিপ্লয় করা উচিত নয়) — এটি সময়ের অপচয়। আরও খারাপ, যদি deploy-ও ভুলবশত চলে যেত, তাহলে একটি ভাঙা (টেস্ট-ফেইল করা) সংস্করণ প্রোডাকশনে চলে যেত — ঠিক সেই ঝুঁকিটাই early-stop প্রতিরোধ করে।

প্র ০২ যদি fail_stages = {"lint", "deploy"} দিয়ে পাইপলাইন চালানো হতো, তাহলে deploy-এর call_count কত হতো, আর কেন?

0 হতো — কারণ lint নিজেই প্রথম স্টেজ, এবং "lint" in fail_stages সত্যি হওয়ায় এটি প্রথম কল-এই ব্যর্থ হবে (call_count["lint"] = 1) এবং failed_at = "lint" সেট হয়ে যাবে। এরপর test, build, deploy — সবগুলো "SKIPPED" হবে এবং কখনো কল হবে না, তাই deploy আসলে fail_stages-এ থাকলেও তার ফাংশন কল হওয়ারই সুযোগ পায়নি — পাইপলাইন প্রথম ব্যর্থতাতেই থেমে যায়, একসাথে একাধিক ব্যর্থতা "পাওয়ার" চেষ্টা করে না।

প্র ০৩ make_stage()-এ প্রতিটি স্টেজ ফাংশন নিজের call_counts ডিকশনারিতে লেখে — এই ডিকশনারিটি কোথা থেকে আসে, আর প্রতিটি run_pipeline() কলে এটি নতুন করে তৈরি হয় কেন গুরুত্বপূর্ণ?

call_counts তৈরি হয় run_pipeline()-এর শুরুতে ({name: 0 for name in STAGE_ORDER}), এবং make_stage()-এ ক্লোজার হিসেবে পাস করা হয় যাতে প্রতিটি স্টেজ ফাংশন সেই একই ডিকশনারি রেফারেন্স শেয়ার করে। প্রতিটি run_pipeline() কলে এটি নতুন করে তৈরি হয় বলেই রান ১-এর কাউন্ট রান ২-কে প্রভাবিত করে না — প্রতিটি রান সম্পূর্ণ স্বাধীন ও শূন্য থেকে শুরু হয়, যেমনটা বাস্তব CI সিস্টেমে প্রতিটি নতুন পাইপলাইন-রান আগের রানের অবস্থা থেকে প্রভাবিত হওয়া উচিত নয়।

অনুশীলন

  1. চিন্তা করুন: কেন lint সাধারণত পাইপলাইনের প্রথম স্টেজ, deploy সবসময় শেষ স্টেজ — এই ক্রমের পেছনের যুক্তি কী?

    সাধারণ নীতি হলো "সস্তা ও দ্রুত চেক আগে, ব্যয়বহুল ও ঝুঁকিপূর্ণ কাজ পরে"। lint সবচেয়ে দ্রুত ও সস্তা (শুধু কোড স্ট্যাটিকভাবে পড়ে), তাই আগে চালালে স্পষ্ট ভুল দ্রুত ও কম খরচে ধরা পড়ে। deploy সবচেয়ে ঝুঁকিপূর্ণ (আসল ব্যবহারকারীদের প্রভাবিত করে), তাই এটি তখনই চালানো উচিত যখন আগের সব স্টেজ (lint, test, build) নিশ্চিত করেছে কোডটি নিরাপদ ও কার্যকর — এটাই early-stop পাইপলাইনের মূল নকশা-যুক্তি।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় রান যোগ করুন — print_report("রান ৩ -- 'build' স্টেজে ব্যর্থতা", fail_stages={"build"}) — এবং Run চেপে দেখুন lint ও test-এর call_count কত হয়, আর deploy-এর কত হয়।

    lint ও test — দুটোই সফলভাবে কল হবে, প্রতিটির call_count = 1 (তারা fail_stages-এ নেই)। build কল হবে (call_count = 1) কিন্তু এবার False রিটার্ন করবে কারণ "build" in fail_stages, ফলে failed_at = "build" সেট হয়ে যাবে। এরপর deploy "SKIPPED" হবে এবং তার call_count থাকবে 0 — এটি নিশ্চিত করে যে পাইপলাইন ঠিক যেখানে ব্যর্থ হয় সেখান থেকেই থামে, আগের সফল স্টেজগুলোর ফলাফল প্রভাবিত না করেই।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • Cloud Computing & DevOps কোর্স সহোদর কোর্স বাস্তব CI/CD টুলিং (GitHub Actions, Jenkins ও অনুরূপ) ও কনফিগারেশন সিনট্যাক্সের বিস্তারিত সেই কোর্সে কভার করা হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
একটি ফুল-স্ট্যাক অ্যাপ কন্টেইনারাইজ করা