تاثیر CI/CD در تیم اندروید اسنپ

https://unsplash.com/photos/U3sOwViXhkY
https://unsplash.com/photos/U3sOwViXhkY

برای بهبود پیوسته و همچنین کاهش هدر رفت زمان برنامه‌نویسان راه‌های مختلفی وجود دارد که یکی از آن راه‌ها استفاده از CI/CD می‌باشد.

اجازه دهید در ابتدا تعریف CI/CD که در ویکیپدیا منتشر شده است را مرور کنیم.

در مهندسی نرم‌افزار به‌طور کلی به مجموعه اعمال یکپارچه‌سازی مداوم و تحویل پیوسته یا استقرار پیوسته، CICD یا CI/CD می‌گویند. CI/CD وادار می‌کند که فرآیندهای ساخت، تست و استقرار برنامه‌ها به صورت خودکار انجام شوند. بدین وسیله پلی بین فعالیت‌های تیم‌های «توسعهٔ نرم‌افزار Development» و «عملیات فناوری اطلاعات Operations» ایجاد می‌شود. فعالیت‌های DevOps شامل توسعه پیوسته، آزمون پیوسته، یکپارچه‌سازی مداوم، استقرار پیوسته و نظارت مداوم بر نرم‌افزار در طول فرایند توسعه می‌باشد. اقدامات CI/CD زیربنای فعالیت‌های دواپس را تشکیل می‌دهد.

علاوه بر موارد ذکر شده از دیگر دستاوردهای مهم CI/CD در تیم اندروید اسنپ٬ بهبود تجربه برنامه‌نویسان از توسعه نرم افزار بوده است.

برای توضیح اینکه چطور و چگونه این دستاورد حاصل شد لازم است که اندکی درباره فرآیند فعلی توسعه تا انتشار در اسنپ بدون در نظر گرفتن CI/CD توضیح دهم و پس از آن نقش CI/CD در کاهش این زمان را مشخص کنم.

این مقاله شامل دو بخش است، بخش اول توضیح خلاصه‌ای از نحوه پیاده سازی CI/CD در تیم اندروید اسنپ می‌باشد و بخش دوم شامل تاثیرات مثبت و منفی CI/CD بر عملکرد تیم است. در نتیجه اگر فقط به دنبال تاثیر CI/CD بر یک تیم نرم افزاری هستید می‌توانید تنها بخش دوم مقاله را دنبال کنید.

بخش اول: تو بساز تا من بسازم.

درباره CI/CD و اهمیت آن در صدها صفحه مختلف در اینترنت صحبت شده است و به همین دلیل درباره جزئیات دقیق و مرحله به مرحله پیاده سازی در اینجا صحبت نمی‌کنیم. ما در تیم اندروید اسنپ از Gitlab برای مدیریت کدها استفاده می کنیم. طبیعتا اولین و بهترین گزینه برای ما Gitlab CI است که توسط اپلیکیشن نصب شده روی سرور همراه با کمک تیم devops تنظیم و راه اندازی شده است. استفاده از این سرویس روی سرورهای اسنپ که در ایران میزبانی می‌شوند برای ما مزایای بسیاری دارد و خیالمان از بابت نگهداری و در دسترس بودن سرویس همیشه راحت است.

برای شروع ما بر اساس ubuntu 20.04 برای اندروید یک custom Image - با توجه به اینکه باید docker image برای Gitlab استفاده کنیم - ساختیم و در آن محیط، jdk و بقیه sdk اندروید را دانلود و نصب کردیم.

در شروع از این image و image استفاده کردیم ولی در نهایت ما نسخه کنترل‌پذیرتری نیاز داشتیم؛ به دلایلی که در ادامه به آنها اشاره خواهم کرد.

استفاده از dexguard

در اپلیکیشن های اندرویدی استفاده از proguard ابزاری مرسوم برای مبهم سازی کد به منظور دشوار کردن مهندسی معکوس و همچنین بهینه سازی نسخه نهایی برای انتشار در store‌ های نرم‌افزاری می‌باشد. البته که proguard ابزاری بسیار خوب برای اهداف ذکر شده است ولی هدف نهایی تولید این ابزار بهینه‌سازی فایل apk نهایی بوده است و خُب شرکت تولید کننده ابزاری کامل‌تر جهت تامین اهداف امنیتی نرم‌افزار به نام dexguard را ارائه داده است که همزمان با R8 و proguard میتواند استفاده شود. برای اینکه بتوانیم از dexguard استفاده کنیم نسخه‌ای از sdk را به image انتقال دادیم تا مشکل build با dexguard حل شود و در زمان صرفه جویی شود. لایسنس‌ها را هم در env variable گذاشتیم و به این شکل برای هر پروژه میتوانیم از لایسنس خودش استفاده کنیم. در نهایت کافیست در فایل yml که برای ci درست کردیم این ۲ خط را به before script اضافه کنیم تا خیالمان از آماده بودن dexguard راحت شود.

Before_script:
- echo &quotdexguard_sdk_path=/dexGuard/lib&quot >> gradle.properties
- echo &quotsystemProp.dexguard.license=dexguard-license.txt&quot >> gradle.properties

اسکریپت کمکی!

برای تسهیل در فرآیند آپلود apk نهایی٬ ارسال ایمیل/پیام‌های اطلاع رسانی و کارهای شبیه به این مورد یک سری اسکریپت نوشتیم. این اسکریپت‌ها وظایف مختلفی را انجام میدهند، از جمله این که اسم فایل apk یا aab را برای ما بر اساس ساختارهای توافق شده تیم مرتب میکنند و یا فایل‌ را روی Minio اپلود می‌کنند که در ادامه توضیح خواهم داد و یا اینکه در پیام رسان داخلی شرکت ‏(element) اطلاع رسانی می‌کنند.

البته درست است که قابلیت آپلود روی object storage هایی شبیه S3 بعد از پیاده‌سازی ما در اسنپ به Gitlab اضافه شد اما ما با اسکریپت‌هایی که آماده کرده بودیم مشکلی نداشتیم و همچنان هم از اسکریپت خودمان استفاده میکنیم.

- python3 /deployscript/deploy.py
--release.dir=app/build/outputs/apk/Dev/release
--app.name=Driver
--riot.hook=&quot$RIOT_HOOK_ADDRESS&quot
--riot.msg=&quot$RIOT_MSG&quot
--minio.url=&quot$MINIO_URL
--minio.accessKey=&quot$MINIO_ACCESSKEY&quot
--minio.secretKey=&quot$MINIO_SECRETKEY&quot
--minio.bucket=&quotdriver&quot
--app.desc=&quot$CI_COMMIT_MESSAGE&quot
--app.branch=&quot$CI_COMMIT_REF_NAME&quot
--app.flavor=&quotDev&quot

ذخیره فایل

امکان اینکه فایلهای نهایی را به عنوان Artifact در Gitlab ذخیره کرد وجود دارد، اما دسترسی به فایل و همچنین سرعت دانلود فایل مورد توافق تیم نبود. در نتیجه ما از Minio استفاده کردیم. برای هر اپلیکیشن bucket جدا تعریف کردیم و build های مربوط به flavor های مختلف در پوشه‌های مختلفی با timestamp خودشان ذخیره میشوند. مثلا فایل آپلود شده روی Minio به اسم زیر در باکت driver ذخیره می‌شود.

تصویر۱: نمونه اسم فایل ذخیر شده بر روی سرور
تصویر۱: نمونه اسم فایل ذخیر شده بر روی سرور

نگهداری

برای اینکه بتوانیم تکه های مختلف CI/CD را بهتر مدیریت کنیم، ایمیج‌ها را بر اساس نسخه dexguard تگ گذاری کردیم. همچنین برای سهولت در فرآیند تولید image جدید، ابتدا یک image اندروید ساخته شده است و image حاوی dexguard و همچنین اسکریپت‌های نهایی بر اساس image پایه اندروید ساخته می‌شود. به این شکل هر زمان نیاز بود که نسخه جدیدی از dexguard را پشتیبانی کنیم دیگر نیازی نیست که کل image اندروید از ابتدا ساخته شود.

تصویر۲: نمونه image هایی که بر روی سرور وجود دارد
تصویر۲: نمونه image هایی که بر روی سرور وجود دارد

بخش دوم: کد! کد! تا پیروزی

بعد از اضافه شدن CI/CD به پروژه‌ها بهبودهایی حاصل شد که در این قسمت به این موارد اشاره خواهم کرد. نیازهای مربوط به کار تیمی ما در زمان‌های مختلف راه حل‌های مختلفی داشتند که اکنون CI/CD با روش پیاده‌سازی شده فعلی تغییر مثبتی در آنها داشته است و performance تیم را بهبود خوبی داده است.

حل نقطه گلوگاهی توسعه

فرآیندی که برای توسعه یک feature جدید در اسنپ و احتمالا خیلی از شرکت‌های دیگر طی میشود به این شکل است:

  1. دریافت نیازمندی‌ها و مشخصات فیچر (spec) از تیم پروداکت و طراحی‌های مربوطه از تیم دیزاین
  2. برگزاری جلسات discovery / solution design
  3. برنامه‌ریزی (planning) اسپرینت پیش‌رو
  4. شروع پیاده‌سازی و تحویل نمونه اولیه (release candidate)

در این مراحل بخش گلوگاهی که ما با CI/CD در آن بهبود دادیم بخش چهارم بوده است. شکل زیر مرحله ۴ را به صورت شماتیک نشان میدهد:

تصویر ۳: جریان کاری در پیاده سازی و تست یک feature
تصویر ۳: جریان کاری در پیاده سازی و تست یک feature

در مراحلی که در بالا مشخص شد برای تیم ما رساندن فایل apk که شامل دو بخش build گرفتن و همچنین آپلود apk برای تیم QA است در بعضی روزها بر اساس شرایط اینترنت به شدت خسته کننده و وقت گیر بوده است. همچنین در این بخش تغییرات ساده٬ زمان متفاوتی با تغییرات اصلی ندارند. مثلاً تغییر یک متن شامل دوباره build گرفتن و همچنین آپلود دوباره فایل میشود که برابر با زمان build و آپلود یک قابلیت اساسی و بزرگ است. ما به کمک CI/CD به طور محسوسی در زمان و انرژی که برنامه‌نویس در این بخش از دست می‌داد صرفه جویی کردیم. بعد از هر تغییری که نیاز به رساندن build جدید به تیم QA باشد کافیست برنامه‌نویس بر روی پنل Gitlab درخواست build دهد و از اینجا به بعد میتواند به ادامه کارهای توسعه بپردازد و رساندن APK نهایی را به CI/CD واگذار کند. Apk به طور کامل بر روی سرور ساخته و امضا (sign) شده و در نهایت بر روی ObjectStorage تیم اندروید آپلود می‌شود. پس از آن در گروه مشخصی پیامی حاوی لینک دانلود همراه با مشخصات branch و flavor و همچنین آخرین کامیت ارسال می‌شود. تیم QA از اینجا به بعد به راحتی میتواند Apk را دانلود و فرآیند QA را شروع کند. شکل زیر نمونه‌ای از پیام ارسال شده در گروه release است:

تصویر ۵: نمونه پیام ارسال شده در elements برای تیم
تصویر ۵: نمونه پیام ارسال شده در elements برای تیم

اینکار همچنین یکپارچگی را بر روی سیستم ایجاد کرده است به طوری که Apk‌ ها در یک فضای مشخص جمع آوری شده‌اند و دسترسی به آخرین build یک branch ساده‌تر و سریعتر شده است. همچنین نیاز به اطلاع رسانی دوباره به تیم QA وجود ندارد و هر زمان برنچ آماده تست شود تیم QA به طور خودکار متوجه نسخه (build) جدید می‌شود و نهایتا اتلاف زمانی که نقطه گلوگاهی به خود اختصاص می‌داد به طور محسوسی کاهش پیدا می‌کند.

حل مشکل کتابخانه‌ها

در اسنپ کتابخانه‌ها و SDK‌ های درون سازمانی بر روی مخزنی جداگانه نگهداری می‌شوند. تا قبل از استفاده از CI/CD فرآیند انتشار روی مخزن باید به صورت دستی و توسط owner آن پروژه انجام می‌شد، اما پس از استفاده از CI/CD و انتشار خودکار، نسخه‌ای که روی برنچ main قرار می‌گیرد حالا دیگر نسخه منتشر شده و آخرین کد قرار گرفته بر روی مخازن با هم یکی هستند و احتمال خطای انسانی کاهش یافته است. همچنین CI/CD قبل از انتشار با اجرا کردن تست‌ها جلوی انتشار کدی که تست‌ها را pass نکرده است میگیرد. در کنار این موارد ما میتونیم به code coverage هم دسترسی داشته باشیم و طبیعتا در صورت کاهش coverage اقدامات آتی را انجام دهیم.

تصویر۶: وضعیت code coverage در پروژه network module
تصویر۶: وضعیت code coverage در پروژه network module


تصویر۷: پایپلاین بیلد برای کتابخانه UI-KIT
تصویر۷: پایپلاین بیلد برای کتابخانه UI-KIT

بیلد پروداکشن

زمانی که نیاز به نسخه پروداکشن برای مارکت‌های اندرویدی باشد طبیعتا env‌ ها و متغیر‌های بیلد دستخوش تغییراتی می‌شوند که توضیح آنها خارج از حوصله این متن است ولی در گذشته ما داکیومنتی مرحله به مرحله برای آماده سازی نسخه‌های مختلف داشتیم ولی حالا به کمک CI/CD نسخه‌های مختلف برای env‌های مختلف بر روی سرور آماده میشود. به این روش احتمال خطای انسانی در فرآیند انتشار کاهش چشمگیری خواهد داشت و همچنین زمان آماده‌سازی نسخه هم به همین شکل کاهش خوبی داشته است. دیگر اینکه همیشه آماده سازی نسخه نهایی وابسته به یک یا چند نفر بود که با فرآیند‌ها آشنایی داشتند و همچنین به env‌های خاص پروداکشن دسترسی داشتند اما حالا به کمک CI/CD این وابستگی‌ها برداشته شده و وظیفه تولید نسخه نهایی در حیطه کاری سرور است.

تصویر۸: ساخت نسخه‌های مختلف بر اساس flavor
تصویر۸: ساخت نسخه‌های مختلف بر اساس flavor

کمک به کاهش خطاهای انسانی

فرایند خودکار سازی اجرای تست‌ها و همچنین build پروژه پیش از merge کردن یک PR در Gitlab به کمک CI/CD کمک شایانی به جلوگیری از مشکلات بعد از merge دارد. قبل از این ممکن بود به‌روزرسانی یک کتابخانه که API جدیدی دارد باعث build نشدن پروژه به طور کلی شود ولی پس از CI/CD ما حتی قبل از merge از تغییرات code coverage هم مطمئن میشویم.

تصویر۹: نمایش وضعیت تست‌ها برای هر PR در Gitlab
تصویر۹: نمایش وضعیت تست‌ها برای هر PR در Gitlab

انجام Static Analysis

اگر سورس کد یا binary یک اپلیکیشن را بدون اجرا کردن آن و بدون در نظر گرفتن محیط اجرای برنامه بررسی کنیم در حقیقت static-analysis را انجام داده‌ایم. با استفاده از ابزارهای رایج static analysis می‌توان بررسی کد را قبل از مرج کردن یک PR یا بیلد شدن نهایی پروژه انجام داد. با این روش همیشه مطمئن می‌شویم استانداردهایی که برای کدبیس خود در نظر گرفته‌ایم رعایت میشوند و در نهایت تیم خوشحال‌تر و مطئمن‌تر به کارش ادامه میدهد. ابزارهای آماده و رایگان زیادی برای هر بخشی از کد وجود دارد به عنوان مثال می‌توان به Detekt اشاره کرد. نکته مهم در این ابزارها اینست که به طور پیوسته از صحت و تطابق کد با استانداردهای مشخص شده مطمئن شویم. در این زمینه نیز CI/CD به ما کمک می‌کند که این از انجام static analysis پیش از merge کردن branch جدید مطمئن شویم.

انجام Security Analysis

درست مثل بخش قبلی انجام تسک‌های مربوط به چک لیست‌های امنیتی هم که توسط تیم امنیت انجام می‌شود می‌تواند تا حدی خودکار سازی شود یا حداقل به شروع فرآیندهای تیم امنیت توسط CI/CD سرعت داده شود. یکی از ابزارهای پیشنهادی تیم امنیت اسنپ برای بررسی کدبیس mobsf است. Mobsf یک چارچوب pen-testing کامل هست که به انجام خودکار static-analysis و dynamic-analysis و همچنین malware-analysis می‌پردازد. در اسنپ نسخه نهایی پیش از انتشار توسط CI/CD برای سرور mobsf تیم امنیت ارسال میشود تا بررسی‌های اولیه تیم امنیت بتواند راحت‌تر و سریعتر شروع شود.

جمع‌بندی

با اینکه CI/CD تاثیر بسیار مطلوبی بر روی عملکرد تیم دارد ولی در نهایت این تصمیم شماست که بر اساس تیم خودتان٬ نوع پروژه و همچنین میزان منابع در دسترس تصمیم به استفاده از آن بگیرید. سرویس‌های زیادی سعی در حل مشکلات نگهداری CI/CD کردند که طبیعتا اگر پروژه شما بزرگتر از حد تعیین شده باشد یا اینکه close source باشید احتمالا گزینه رایگانی در اختیار نداشته باشید. به هر حال به روز نگهداشتن image‌ی که در ابتدا در موردش صحبت کردیم و همچنین نگهداری از runner‌هایی که قرار است نقش اجرا کننده دستورات را داشته باشند هزینه‌بر و زمان‌بر خواهد بود. پس پیش از اینکه به سمت این راهکار برای حل برخی از مشکلات تیم بروید حتما به فکر روش‌های نگهداری از سیستم CI/CD خود باشید تا در آینده همین CI/CD برای شما دست و پاگیر نباشد.