পাঠ ৩৫ · ৫৭-এর মধ্যে · মডিউল ৮
Home / Courses / Cloud Computing & DevOps / পাইপলাইন ডিজাইন

পাইপলাইন ডিজাইন — বিল্ড, টেস্ট, ডিপ্লয় স্টেজ

Pipeline design — build, test, deploy stages
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • একটি সম্পূর্ণ পাইপলাইনের সাধারণ স্টেজ ক্রম এবং কেন সেই নির্দিষ্ট ক্রমটিই সবচেয়ে যুক্তিসঙ্গত
  • নির্ভরশীল বনাম স্বাধীন স্টেজ — কোনগুলো সমান্তরালে চালানো নিরাপদ
  • Python দিয়ে একটি পাইপলাইনকে (stage, duration, depends_on) হিসেবে মডেল করা
  • সম্পূর্ণ সিকোয়েনশিয়াল বনাম সর্বোচ্চ-সম্ভাব্য প্যারালাল সময় গণনা করে সাশ্রয় দেখা

১ · স্টেজ ক্রম — সস্তা থেকে ব্যয়বহুল

একটি ভালোভাবে ডিজাইন করা পাইপলাইনের স্টেজগুলো এমনভাবে সাজানো হয় যাতে সবচেয়ে সস্তা ও দ্রুততম চেকগুলো প্রথমে চলে, এবং সবচেয়ে ব্যয়বহুল ও ধীরতম চেকগুলো শেষে — L33-এর fail-fast নীতির সরাসরি সম্প্রসারণ। এভাবে সাজালে একটি সাধারণ ভুল (যেমন একটি লিন্ট এরর) সেকেন্ডের মধ্যে ধরা পড়ে, ব্যয়বহুল ইন্টিগ্রেশন টেস্ট বা প্রোডাকশন ডিপ্লয়মেন্টে সময়/টাকা খরচ হওয়ার আগেই।

লিন্ট / স্ট্যাটিক অ্যানালাইসিস
সেকেন্ডে চলে — সবচেয়ে সস্তা, তাই প্রথমে।
ইউনিট টেস্ট
দ্রুত, আইসোলেটেড — বাহ্যিক ডিপেন্ডেন্সি ছাড়াই চলে।
বিল্ড আর্টিফ্যাক্ট/ইমেজ
ইউনিট টেস্ট পাস করা কোড থেকেই ডিপ্লয়যোগ্য আর্টিফ্যাক্ট তৈরি (ties to M6)।
ইন্টিগ্রেশন টেস্ট
ধীর — প্রকৃত ডিপেন্ডেন্সি (ডাটাবেস, অন্য সার্ভিস) দরকার।
স্টেজিং ডিপ্লয় + স্মোক টেস্ট
একটি বাস্তব-সদৃশ পরিবেশে চূড়ান্ত যাচাই।
প্রোডাকশন ডিপ্লয়
সবচেয়ে শেষে, প্রায়ই গেটেড (ties to L34)।

২ · প্যারালালাইজেশন — স্বাধীন স্টেজ একসাথে চালানো

সব স্টেজ পরস্পর নির্ভরশীল নয়। উদাহরণস্বরূপ, তিনটি ভিন্ন মাইক্রোসার্ভিসের ইউনিট টেস্ট একে অপরের উপর নির্ভর করে না — সেগুলো সমান্তরালে চালানো যায়, একটির শেষ হওয়ার জন্য অপেক্ষা না করে। কিন্তু সত্যিকারের নির্ভরশীল স্টেজ (যেমন বিল্ড আর্টিফ্যাক্ট তৈরি না হলে ডিপ্লয় করা যায় না) অবশ্যই সিকোয়েনশিয়াল থাকতে হবে। একটি নির্ভরশীলতা গ্রাফ (dependency graph) থেকে "কোন স্টেজগুলো একসাথে চলতে পারে" নির্ণয় করা যায় — নিচের কোড সেলে ঠিক এটাই গণনা করা হয়েছে।

Python
# একটি পাইপলাইনকে (stage_name, duration_sec, depends_on) হিসেবে মডেল করা
# এবং সিকোয়েনশিয়াল বনাম সর্বোচ্চ-সম্ভাব্য প্যারালাল সময় তুলনা করা

stages = [
    ("lint",                  5,  []),
    ("unit_test_service_a",  20,  ["lint"]),
    ("unit_test_service_b",  30,  ["lint"]),
    ("unit_test_service_c",  15,  ["lint"]),
    ("build_image",          40,  ["unit_test_service_a", "unit_test_service_b", "unit_test_service_c"]),
    ("integration_test",     50,  ["build_image"]),
    ("deploy_staging",       15,  ["integration_test"]),
    ("smoke_test_staging",   10,  ["deploy_staging"]),
    ("deploy_production",    20,  ["smoke_test_staging"]),
]

duration_of = {name: dur for name, dur, _ in stages}
depends_on = {name: deps for name, _, deps in stages}


def compute_levels(stages):
    """প্রতিটি স্টেজের 'গভীরতা' গণনা করে — একই গভীরতার স্টেজগুলো একে অপরের উপর
    নির্ভরশীল নয়, তাই একসাথে (parallel) চালানো যায়।"""
    level_of = {}

    def level(name):
        if name in level_of:
            return level_of[name]
        deps = depends_on[name]
        lvl = 0 if not deps else 1 + max(level(d) for d in deps)
        level_of[name] = lvl
        return lvl

    for name, _, _ in stages:
        level(name)
    return level_of


def sequential_time(stages):
    return sum(dur for _, dur, _ in stages)


def parallel_time(stages):
    levels = compute_levels(stages)
    groups = {}
    for name, lvl in levels.items():
        groups.setdefault(lvl, []).append(name)

    total = 0
    print("প্যারালাল গ্রুপ (একই লেভেলের স্টেজ একসাথে চলে):")
    for lvl in sorted(groups):
        names = groups[lvl]
        group_max = max(duration_of[n] for n in names)
        total += group_max
        print(f"  লেভেল {lvl}: {names} → সময় লাগবে {group_max} সেকেন্ড (সবচেয়ে ধীরটি)")
    return total


seq_total = sequential_time(stages)
par_total = parallel_time(stages)
savings_pct = (seq_total - par_total) / seq_total * 100

print(f"\nসম্পূর্ণ সিকোয়েনশিয়াল হলে মোট সময়: {seq_total} সেকেন্ড")
print(f"সর্বোচ্চ প্যারালালাইজেশনে মোট সময়:   {par_total} সেকেন্ড")
print(f"সাশ্রয়: {seq_total - par_total} সেকেন্ড ({savings_pct:.1f}%)")

    
লক্ষ্য করুন তিনটি ইউনিট টেস্ট স্টেজ (২০, ৩০ ও ১৫ সেকেন্ড) সিকোয়েনশিয়ালি চললে মোট ৬৫ সেকেন্ড লাগতো, কিন্তু সমান্তরালে চললে শুধু সবচেয়ে ধীরটির (৩০ সেকেন্ড) সময়ই লাগে — বাকি দুটো পটভূমিতে একইসাথে শেষ হয়ে যায়। এই একটি পরিবর্তনই মোট পাইপলাইন সময় থেকে প্রায় ১৭% সাশ্রয় করে, বাস্তব পাইপলাইনে যেখানে শত শত টেস্ট থাকতে পারে সেখানে এই সাশ্রয়ের প্রভাব আরও অনেক বড়।
মূল কথা · Key takeaway

পাইপলাইন ডিজাইন মানে দুটো সিদ্ধান্ত — স্টেজের ক্রম (সস্তা আগে, fail-fast) এবং কোন স্টেজগুলো সমান্তরালে চলতে পারে তা চিহ্নিত করা। ভালোভাবে ডিজাইন করা একটি পাইপলাইন দ্রুত ফিডব্যাক দেয় এবং কম্পিউট রিসোর্স দক্ষভাবে ব্যবহার করে — L36 দেখাবে কীভাবে এই পুরো পাইপলাইনের ফলাফলকে Git-ভিত্তিক একটি ডিক্লারেটিভ সোর্স-অফ-ট্রুথের সাথে যুক্ত করা যায় (GitOps)।

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

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

প্র ০১ উপরের কোড সেলে build_image কেন তিনটি ইউনিট টেস্ট স্টেজের সাথে একই লেভেলে না থেকে তাদের পরের লেভেলে বসেছে?

কারণ build_image-এর depends_on তালিকায় তিনটি ইউনিট টেস্ট স্টেজই আছে — তাই এটি সত্যিকারের নির্ভরশীল, স্বাধীন নয়। compute_levels ফাংশন এই নির্ভরতা মেনে build_image-এর লেভেল তার সব ডিপেন্ডেন্সির সর্বোচ্চ লেভেলের চেয়ে ১ বেশি নির্ণয় করে — তিনটি টেস্টই লেভেল ১-এ, তাই build_image লেভেল ২-এ যায়, প্রমাণ করে এটি সেগুলোর সবগুলো শেষ হওয়ার পরই শুরু হতে পারবে।

প্র ০২ একটি টিম যদি ভুলবশত সব স্টেজ শুধু সিকোয়েনশিয়ালিই চালাতে থাকে (প্যারালালাইজেশনের সুযোগ থাকা সত্ত্বেও), তার ব্যবহারিক প্রভাব কী?

কোনো ভুল ফলাফল হবে না (সিকোয়েনশিয়াল পাইপলাইনও সঠিকভাবে কাজ করে), কিন্তু প্রতিটি ডেভেলপারের জন্য ফিডব্যাক পেতে অপ্রয়োজনীয়ভাবে বেশি সময় লাগবে — উপরের উদাহরণে ১৭% বেশি। বড় স্কেলে, শত শত ডেভেলপার দিনে একাধিকবার মার্জ করলে, এই অতিরিক্ত সময় সামগ্রিক টিমের গতি ও উৎপাদনশীলতার উপর উল্লেখযোগ্য নেতিবাচক প্রভাব ফেলে।

প্র ০৩ কেন deploy_production স্টেজটি সব সময় পাইপলাইনের একেবারে শেষে থাকা উচিত, প্যারালালাইজেশন প্রয়োগ করা হলেও?

কারণ এটি সবচেয়ে ঝুঁকিপূর্ণ ও অপরিবর্তনীয় (বাস্তব ব্যবহারকারীরা প্রভাবিত হন) স্টেজ — এর আগে আসা প্রতিটি স্টেজ (লিন্ট, টেস্ট, স্টেজিং যাচাই) মূলত এই একটি সিদ্ধান্তকে যতটা সম্ভব নিরাপদ করার জন্য বিদ্যমান। এটিকে অন্য কোনো স্টেজের সাথে প্যারালালে চালানো মানে সেই নিরাপত্তা-যাচাই সম্পূর্ণ হওয়ার আগেই বাস্তব ব্যবহারকারীদের ঝুঁকিতে ফেলা — তাই এটি সবসময় নির্ভরশীলতা গ্রাফের একদম শেষ লেভেলে থাকে।

অনুশীলন

  1. চিন্তা করুন: যদি unit_test_service_b-এর সময় ৩০ সেকেন্ডের বদলে ১০০ সেকেন্ড হতো, তাহলে parallel_time-এর ফলাফলে কী পরিবর্তন হতো এবং কেন?

    লেভেল ১-এর গ্রুপ ম্যাক্স ৩০ থেকে বেড়ে ১০০ হয়ে যেত, তাই মোট প্যারালাল সময় ১৭০ থেকে ২৪০ সেকেন্ডে বেড়ে যেত — কারণ একটি প্যারালাল গ্রুপের সময় নির্ধারণ করে সেই গ্রুপের সবচেয়ে ধীর স্টেজটি, বাকিগুলো যত দ্রুতই হোক না কেন। এটি দেখায় প্যারালালাইজেশনের সুবিধা সীমাবদ্ধ — একটি গ্রুপের সবচেয়ে ধীর স্টেজই পুরো গ্রুপের গতি নির্ধারণ করে।

  2. পরীক্ষা করুন: stages তালিকায় একটি নতুন স্বাধীন স্টেজ ("security_scan", 35, ["lint"]) যোগ করুন (L26-এর ইমেজ স্ক্যানিং ধারণার মতো, শুধু lint-এর উপর নির্ভরশীল)। কোড আবার চালিয়ে দেখুন এটি কোন লেভেলে বসে এবং মোট প্যারালাল সময়ে কী প্রভাব ফেলে।

    যেহেতু security_scan-এর একমাত্র ডিপেন্ডেন্সি lint (ঠিক অন্য তিনটি ইউনিট টেস্টের মতোই), এটি লেভেল ১-এ বসবে — সেই লেভেলের বিদ্যমান সর্বোচ্চ সময় (৩০) থেকে বেড়ে এখন ৩৫ হবে (কারণ ৩৫ > ৩০)। ফলে মোট প্যারালাল সময় ১৭০ থেকে ১৭৫ সেকেন্ডে বাড়বে — মাত্র ৫ সেকেন্ড বৃদ্ধি, কারণ এই নতুন স্বাধীন স্টেজটি অন্য টেস্টগুলোর সাথে একসাথেই চলছে।

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

আগের পাঠ
L34 · Continuous Delivery বনাম Continuous Deployment