Mở đầu — vì sao bài này quan trọng
Nếu bạn đã quen với việc deploy một ứng dụng web — push code lên nhánh chính, pipeline tự động build rồi đẩy container lên server, chỉ vài phút sau người dùng đã thấy phiên bản mới — thì thế giới mobile sẽ khiến bạn "vỡ mộng" ngay lần đầu tiên. Với mobile app, bạn không sở hữu thiết bị của người dùng. Bạn không thể "ghi đè" phiên bản đang chạy trên chiếc iPhone của khách hàng ở Đà Nẵng chỉ bằng một lệnh kubectl rollout. Giữa code của bạn và người dùng cuối là hai "người gác cổng" quyền lực: Apple App Store và Google Play Store.
Đây chính là lý do CD (Continuous Delivery/Deployment) cho mobile là một bài toán riêng, tách hẳn khỏi CD cho web mà các bài trước đã bàn. Rất nhiều đội ngũ ở Việt Nam khi chuyển từ làm web sang làm app đã "trả giá" bằng những bản release trễ deadline, những đợt build hỏng vào lúc 2 giờ sáng vì máy Mac hết dung lượng, hay tệ hơn là một bản build bị Apple từ chối ngay trước ngày ra mắt chiến dịch marketing đã chi cả trăm triệu đồng.
Trong bài này, chúng ta sẽ đi sâu vào những điểm khiến mobile CD khác biệt, cách xây dựng một pipeline giao hàng cho cả iOS và Android, và những cạm bẫy thực tế mà tôi đã chứng kiến nhiều đội ngũ mắc phải. Mục tiêu là để khi bạn rời khỏi bài học này, bạn có thể tự tin thiết kế một pipeline mobile CD thực dụng, không phải "mò mẫm" từ đầu.
Khái niệm cốt lõi
Vì sao mobile CD khác web CD một trời một vực
Có bốn điểm khác biệt nền tảng mà bạn phải nắm rõ trước khi thiết kế bất cứ thứ gì.
Thứ nhất — iOS bắt buộc build trên macOS. Toolchain của Apple (Xcode, xcodebuild, các công cụ ký số) chỉ chạy trên macOS. Bạn không thể build một file .ipa (định dạng gói ứng dụng iOS) trên máy Linux như cách build Docker image. Điều này có hệ quả tài chính rất thật: máy Mac đắt, và các dịch vụ CI cung cấp runner macOS luôn tính phí cao hơn nhiều so với runner Linux. Ví dụ trên GitHub Actions, một phút chạy runner macOS bị nhân hệ số 10 so với Linux (Linux tính 1x, macOS tính 10x trên gói trả phí). Với một đội build app hàng chục lần mỗi ngày, con số này cộng dồn nhanh đến mức đáng giật mình.
Thứ hai — có "cửa kiểm duyệt" (store review). Sau khi build xong, bạn không release trực tiếp. Bạn nộp bản build lên App Store Connect (Apple) hoặc Google Play Console (Google), rồi chờ họ duyệt. Apple thường mất từ vài giờ đến 1–2 ngày; Google Play thường nhanh hơn nhưng cũng có thể vài giờ đến vài ngày. Điều này phá vỡ hoàn toàn khái niệm "deploy tức thì". Với mobile, "Continuous Deployment" theo nghĩa đen (mỗi commit tự động lên tay người dùng) là gần như bất khả thi. Cái chúng ta thực sự làm là Continuous Delivery: pipeline tự động đưa bản build đến "ngưỡng cửa" của store, sẵn sàng để nộp, còn quyết định nộp và thời điểm phát hành có yếu tố con người và lịch trình.
Thứ ba — ký số (code signing) phức tạp. Đây là "nỗi đau kinh điển" của iOS. Để một app chạy được trên thiết bị thật hoặc lên store, nó phải được ký bằng certificate và provisioning profile hợp lệ do Apple cấp. Quản lý các certificate này trên máy CI (vốn không có giao diện, không có Keychain sẵn) là một nghệ thuật riêng. Android nhẹ nhàng hơn với keystore, nhưng vẫn cần bảo quản cẩn thận — mất keystore của một app đã publish đồng nghĩa bạn không bao giờ update được app đó nữa.
Thứ tư — nhiều "build flavor" và biến thể. Một app mobile thực tế thường cần nhiều biến thể: bản dev trỏ về server dev, bản staging cho QA test, bản production cho người dùng thật. Trên Android gọi là product flavors + build types; trên iOS gọi là schemes + build configurations. Mỗi biến thể có thể khác nhau về bundle ID, icon, endpoint API, khóa analytics. Pipeline phải build được đúng biến thể theo đúng ngữ cảnh.
Các "kênh phân phối" trước khi ra store
Không phải bản build nào cũng đi thẳng ra công chúng. Có một hệ sinh thái kênh trung gian rất quan trọng:
- TestFlight (iOS) và Google Play Internal/Closed Testing (Android): kênh chính thức của store để phát bản beta cho tester nội bộ hoặc nhóm giới hạn. Đây là nơi QA và stakeholder duyệt bản build trước khi lên production.
- Firebase App Distribution: một kênh phân phối beta độc lập với store, hoạt động cho cả iOS và Android, rất tiện để gửi bản build nhanh cho đội QA hoặc khách hàng mà không cần qua quy trình của Apple/Google.
Công cụ nền tảng: Fastlane và các nền tảng CI
Nhắc đến mobile CD mà không nhắc Fastlane là một thiếu sót. Fastlane (viết bằng Ruby, mã nguồn mở, giờ thuộc Google) là bộ công cụ gần như tiêu chuẩn của ngành để tự động hóa build, ký số, chụp screenshot và upload lên store. Bạn định nghĩa các "lane" (làn công việc) trong file Fastfile, và pipeline CI chỉ việc gọi lane đó. Fastlane có match để quản lý certificate iOS một cách tập trung (lưu certificate mã hóa trong một git repo riêng), giải quyết gọn gàng bài toán code signing.
Về nền tảng CI, ngoài GitHub Actions/GitLab CI (đã học ở các bài trước) còn có các dịch vụ chuyên biệt cho mobile như Bitrise, Codemagic, hay Expo EAS (cho React Native/Expo). Ưu điểm của chúng là cung cấp sẵn runner macOS được cấu hình tốt và các tích hợp store liền mạch.
Tình huống thực tế
Ví dụ 1 — Startup fintech tại TP.HCM và cú sốc chi phí runner macOS
Một startup fintech ở Quận 1, TP.HCM (gọi tắt là FinViet) có app cho vay tiêu dùng chạy trên cả iOS và Android, đội mobile 6 người. Ban đầu họ cấu hình pipeline "cho tiện": mỗi lần push lên bất kỳ nhánh feature nào, GitHub Actions tự động build cả iOS lẫn Android. Nghe rất "CI đúng bài".
Vấn đề lộ ra ở cuối tháng: hóa đơn GitHub Actions vọt lên hơn 800 USD chỉ trong một tháng, trong khi trước đó khi còn làm web họ chỉ tốn khoảng 40 USD. Truy ra thì thủ phạm là runner macOS nhân hệ số 10x, cộng với việc mỗi commit build đầy đủ (full build) không có cache, mỗi lần iOS build mất gần 25 phút.
Cách họ xử lý: (1) chỉ build iOS trên các sự kiện quan trọng — pull request vào nhánh develop và main, còn build feature thường ngày chỉ chạy Android trên Linux runner để kiểm tra compile và test; (2) bật cache cho CocoaPods và Swift Package Manager, giảm iOS build từ 25 phút xuống còn khoảng 9 phút; (3) chuyển các bản build nội bộ hằng ngày sang một self-hosted Mac mini M2 đặt tại văn phòng làm runner. Sau ba thay đổi này, hóa đơn CI đám mây giảm còn khoảng 180 USD/tháng.
Bài học rút ra: Với mobile CD, "build mọi thứ trên mọi commit" là một cái bẫy chi phí. Bạn phải phân tầng trigger (bài về pipeline trigger sẽ nói kỹ hơn về nguyên lý này), và runner macOS là tài nguyên đắt đỏ cần được dùng có chọn lọc.
Ví dụ 2 — Sàn thương mại điện tử và bản build bị Apple từ chối ngay trước campaign
Một sàn TMĐT khu vực Đông Nam Á (bối cảnh giả định hợp lý, gọi là ShopSEA) chuẩn bị một chiến dịch Flash Sale lớn dịp lễ, ngân sách marketing khoảng 2 tỷ đồng, với một tính năng mới trong app: livestream bán hàng. Team đặt lịch phát hành app phiên bản mới đúng ngày khai màn chiến dịch.
Sai lầm: họ nộp bản build lên App Store chỉ trước ngày ra mắt đúng 1 ngày, nghĩ rằng "web deploy có mất bao lâu đâu". Apple review mất 26 giờ và... từ chối, với lý do bản build yêu cầu quyền truy cập camera nhưng phần mô tả quyền (NSCameraUsageDescription) viết quá chung chung, vi phạm guideline. Team phải sửa, build lại, nộp lại, chờ duyệt thêm một vòng. Kết quả: tính năng livestream chậm 3 ngày so với ngày khai màn chiến dịch, phần lớn ngân sách quảng cáo hai ngày đầu "đốt" mà tính năng chủ lực chưa lên.
Cách họ khắc phục cho các lần sau: (1) đưa việc nộp store lên lịch sớm hơn ít nhất 5 ngày làm việc trước mọi mốc marketing; (2) tận dụng tính năng "Phased Release" của cả hai store để phát hành dần theo phần trăm người dùng, đồng thời dùng feature flag (chủ đề của một bài khác) để bật/tắt tính năng livestream mà không cần build mới; (3) thêm một bước kiểm tra tự động trong pipeline dùng Fastlane precheck để rà các lỗi metadata phổ biến trước khi nộp, giảm rủi ro bị từ chối vì lý do vặt.
Bài học rút ra: Store review delay là một biến số bạn không kiểm soát được, nên phải "đệm" thời gian và tách quyết định phát hành tính năng (bằng feature flag) khỏi quyết định phát hành bản build. Đừng bao giờ gắn ngày lên store trùng khít với ngày sự kiện.
Ví dụ 3 — App giao đồ ăn và "cơn ác mộng" code signing khi nhân sự nghỉ việc
Một công ty giao đồ ăn ở Hà Nội có app iOS được build hoàn toàn thủ công trên chiếc MacBook của một lập trình viên iOS duy nhất. Toàn bộ certificate và provisioning profile nằm trong Keychain của máy đó. Khi bạn này nghỉ việc và bàn giao chưa kỹ, đội còn lại rơi vào tình huống: không ai biết mật khẩu certificate, không build được bản mới lên store, trong khi app đang có một bug thanh toán cần vá gấp.
Họ mất gần một tuần loay hoay: phải revoke certificate cũ trên Apple Developer Portal, tạo certificate mới, cấu hình lại provisioning cho tất cả bundle ID. Trong tuần đó bug thanh toán vẫn tồn tại, gây thất thoát đơn hàng.
Sau sự cố, họ triển khai Fastlane match: certificate và profile được mã hóa và lưu trong một git repo riêng (private), pipeline CI chỉ cần một biến môi trường chứa passphrase để giải mã và dùng. Không còn phụ thuộc vào máy cá nhân của bất kỳ ai. Bất kỳ ai trong team, hay chính con runner CI, đều có thể build và ký bản iOS như nhau.
Bài học rút ra: Code signing không được phép là "kiến thức nằm trong đầu một người". Hãy tập trung hóa certificate ngay từ đầu bằng công cụ như match, coi nó là một phần của Secret Management (một bài riêng trong khóa học này bàn sâu về quản lý secret).
Hướng dẫn từng bước
Dưới đây là khung xây dựng một pipeline mobile CD thực dụng cho cả iOS và Android. Tôi trình bày theo thứ tự triển khai.
Bước 1 — Chuẩn hóa build flavor/scheme. Trước khi tự động hóa, hãy đảm bảo dự án đã tách rõ các biến thể. Android: định nghĩa productFlavors (ví dụ dev, staging, prod) và buildTypes (debug, release) trong build.gradle. iOS: tạo các scheme và build configuration tương ứng, mỗi cái trỏ về một file cấu hình (.xcconfig) chứa endpoint và bundle ID riêng. Nguyên tắc: khác biệt giữa các môi trường phải nằm trong config, không hardcode rải rác trong code.
Bước 2 — Đưa build vào Fastlane. Tạo file Fastfile với các lane rõ ràng, ví dụ: lane beta_android (build APK/AAB bản staging và upload lên Firebase App Distribution), lane beta_ios (build IPA và upload lên TestFlight), lane release_ios, release_android. Việc gói logic build vào Fastlane giúp pipeline CI gọn gàng — CI chỉ việc gọi fastlane beta_ios, còn toàn bộ chi tiết ký số, build nằm trong Fastfile và chạy được cả trên máy local lẫn CI (đảm bảo "environment parity", một chủ đề bài khác).
Bước 3 — Quản lý ký số tập trung. iOS: chạy fastlane match để khởi tạo repo lưu certificate mã hóa. Android: lưu keystore dưới dạng file mã hóa hoặc secret trong CI, không commit thẳng vào repo. Cung cấp passphrase/mật khẩu qua secret của nền tảng CI, không bao giờ để lộ trong log.
Bước 4 — Cấu hình runner phù hợp. iOS bắt buộc runner macos-latest (hoặc self-hosted Mac). Android chỉ cần runner Linux, rẻ hơn nhiều. Trong workflow, hãy tách job iOS và job Android để chúng chạy trên loại runner tương ứng, tránh lãng phí runner macOS cho việc build Android.
Bước 5 — Phân tầng trigger. Cấu hình để: push nhánh feature chỉ chạy lint + unit test (nhanh, rẻ, có thể trên Linux); pull request vào develop build bản beta và đẩy lên kênh test nội bộ; tag phiên bản (ví dụ v2.5.0) mới build bản release và chuẩn bị nộp store. Cách này bảo vệ ngân sách runner macOS.
Bước 6 — Bật cache. Cache thư mục dependency: CocoaPods/SPM cho iOS, Gradle cho Android. Đây là đòn bẩy lớn nhất để giảm thời gian và chi phí build (bài về caching strategies bàn kỹ nguyên lý).
Bước 7 — Tự động đẩy lên kênh beta, giữ tay con người ở bước lên store. Để pipeline tự động đẩy bản build lên TestFlight/Firebase cho QA. Còn bước "submit for review" và "release to production" nên có một cổng phê duyệt thủ công (manual approval gate), vì nó liên quan đến lịch marketing và không thể revert dễ dàng như web.
Bước 8 — Cân nhắc phát hành dần. Khi đã lên store, dùng Phased Release (iOS) hoặc Staged Rollout (Android, phát hành theo % người dùng) để giới hạn rủi ro, kết hợp giám sát crash (Firebase Crashlytics) để phát hiện sự cố sớm trước khi phủ 100% người dùng.
Lỗi thường gặp & mẹo
- Build iOS trên mọi commit. Đây là lỗi tốn kém nhất. Hãy phân tầng trigger và giữ runner macOS cho các sự kiện thực sự cần.
- Gắn ngày nộp store trùng ngày sự kiện. Luôn đệm ít nhất 5 ngày làm việc, đặc biệt với App Store. Review delay là biến số ngoài tầm kiểm soát.
- Để certificate/keystore trong đầu một người. Tập trung hóa bằng Fastlane match và secret CI ngay từ đầu. Đặc biệt: sao lưu keystore Android ở nơi an toàn — mất nó là mất quyền update app vĩnh viễn.
- Quên tăng build number. Cả hai store từ chối bản build có build number trùng với bản đã nộp. Hãy tự động tăng build number trong pipeline (ví dụ dùng số thứ tự chạy của CI).
- Không kiểm tra metadata trước khi nộp. Dùng Fastlane
precheckđể bắt sớm các lỗi mô tả quyền, thiếu privacy policy — những lý do bị từ chối phổ biến nhất. - Nhầm lẫn CD web và CD mobile về khả năng rollback. Web rollback trong vài phút; mobile không thể "gỡ" bản đã cài trên máy người dùng. Vì vậy hãy dựa vào feature flag và phased rollout để giảm rủi ro thay vì trông cậy vào rollback (bài về rollback strategy bàn riêng).
- Mẹo: Tách quyết định "phát hành tính năng" (feature flag) khỏi "phát hành bản build" (store release). Điều này cho phép bạn nộp code lên store sớm và bật tính năng đúng thời điểm mong muốn mà không phụ thuộc lịch duyệt.
Bài tập thực hành
- Phân tích chi phí: Giả sử app của bạn cần 30 lần build iOS mỗi ngày, mỗi lần 10 phút trên runner macOS (hệ số 10x). Ước tính số phút CI tính phí mỗi tháng và đề xuất ít nhất hai cách giảm con số này. So sánh phương án dùng runner đám mây với phương án tự trang bị một Mac mini làm self-hosted runner.
- Thiết kế trigger: Viết ra (dạng bảng) chiến lược phân tầng trigger cho một app iOS + Android: nhánh feature chạy gì, PR vào develop chạy gì, tag release chạy gì, và mỗi bước dùng loại runner nào. Giải thích lý do từng lựa chọn.
- Kịch bản ký số: Mô tả bằng lời (không cần code chi tiết) cách bạn sẽ dùng Fastlane match để một thành viên mới trong team có thể build và ký bản iOS chỉ sau 15 phút onboarding, mà không cần lập trình viên cũ bàn giao thủ công.
- Kịch bản khủng hoảng: App của bạn vừa phát hiện một bug nghiêm trọng ở bản vừa lên store (đang phủ 50% người dùng qua staged rollout). Bạn không thể chờ Apple duyệt bản vá kịp. Hãy mô tả các bước ứng phó tức thời, giả định tính năng gây bug được bọc trong một feature flag.
Tóm tắt
Mobile CD khác web CD ở bốn điểm cốt lõi: iOS bắt buộc build trên macOS (chi phí cao), có cửa kiểm duyệt của store gây delay, code signing phức tạp, và luôn có nhiều build flavor. Vì store review là biến số ngoài tầm kiểm soát và bạn không thể ghi đè app trên máy người dùng, "Continuous Deployment" đúng nghĩa gần như bất khả thi — cái ta thực sự làm là Continuous Delivery: tự động hóa mọi thứ đến ngưỡng cửa store, giữ tay con người ở bước phát hành cuối.
Công cụ nền tảng là Fastlane (đặc biệt match cho ký số) cùng các kênh phân phối beta như TestFlight, Firebase App Distribution. Nguyên tắc thực dụng: phân tầng trigger để tiết kiệm runner macOS, bật cache, tập trung hóa certificate/keystore, đệm thời gian trước mọi mốc marketing, và tách quyết định phát hành tính năng (feature flag) khỏi phát hành bản build. Ba tình huống thực tế — chi phí runner của FinViet, bản build bị từ chối của ShopSEA, và cơn ác mộng ký số ở app giao đồ ăn — đều chỉ về cùng một bài học: mobile CD đòi hỏi kỷ luật về chi phí, thời gian và quản lý bí mật ngay từ ngày đầu, chứ không thể áp nguyên tư duy làm web sang.