Vấn đề
Hệ thống ERP bán lẻ của chúng tôi phục vụ ~50 cửa hàng, ~500 nhân viên và 4 thương hiệu, chạy trên một codebase Python/Django dùng PostgreSQL. API được host trên AWS App Runner, một dịch vụ container được quản lý hoàn toàn. Lúc đầu cách này rất tiện, nhưng gần như không cho chúng tôi kiểm soát cách một bản release được đưa ra, hay cách quay lại khi release bị lỗi. Các background job cũng cần một worker service chạy lâu dài riêng, vốn không hợp với một nền tảng thiết kế cho web request.
Vì vậy chúng tôi muốn chuyển cả API và worker sang ECS. Phần khó là làm việc đó khi hệ thống vẫn đang chạy. Người dùng vẫn làm việc, database migration vẫn tiếp tục được ship, và chúng tôi cần một đường lui an toàn nếu nền tảng mới gặp vấn đề. Trong một khoảng thời gian, cả nền tảng cũ lẫn mới sẽ cùng dùng chung một database.
Tôi xây deploy pipeline và phần thiết lập AWS cho lần chuyển đổi này. Dưới đây là những quyết định định hình nó.
Tôi đã làm gì
Chạy migration một lần, từ chính image đang release
Mỗi bản release build một container image, push lên registry, rồi chạy database migration như một task chạy một lần, khởi động từ đúng image đó. Chỉ khi task thành công thì phiên bản mới mới được deploy. Nhờ vậy, thay đổi schema và đoạn code phụ thuộc vào nó luôn đến từ cùng một bản build, không thể lệch nhau giữa lúc build và lúc deploy. Đổi lại, pipeline chậm hơn và có thêm một bước có thể fail, và một migration lỗi sẽ chặn cả bản release. Đó đúng là điều tôi muốn.
Giữ thay đổi schema tương thích ngược trong giai đoạn chạy song song
Khi cả hai nền tảng cùng nhận traffic, hai phiên bản code chạy trên cùng một database. Chúng tôi thống nhất chỉ cho phép thay đổi kiểu thêm vào trong giai đoạn này: thêm cột, thêm bảng thì được, còn đổi tên, xóa hay đổi kiểu dữ liệu phải chờ đến khi nền tảng cũ được tắt. Việc thay đổi schema chậm lại một thời gian. Bù lại, nền tảng nào cũng có thể nhận traffic bất cứ lúc nào, và việc chuyển đổi thực sự trở thành một lần release bình thường.
Tách image cho API và worker
Tôi chia container build thành nhiều stage, tạo ra một image API gọn nhẹ và một image worker nặng hơn. Worker cần các thư viện cho những việc như tạo báo cáo mà API không bao giờ dùng. Tách riêng giúp image API nhỏ hơn và khởi động nhanh hơn, đổi lại phải duy trì hai định nghĩa image và đảm bảo cả hai đều được build lại ở mỗi bản release.
Blue/green release với alarm theo tỉ lệ lỗi và tự động rollback
Trên App Runner, một bản release thay thế phiên bản đang chạy cùng một lúc. Trên ECS, tôi thiết lập blue/green deployment: phiên bản mới khởi động song song với phiên bản hiện tại, nhận một phần nhỏ traffic trước, và phải ổn định qua một khoảng thời gian theo dõi rồi mới tiếp quản. Một alarm theo dõi tỉ lệ lỗi của phiên bản mới, và khi alarm kích hoạt, traffic tự quay về phiên bản cũ mà không cần ai bấm nút. Đổi lại, release chậm hơn và cần gấp đôi tài nguyên trong lúc rollout. Tôi cũng thêm một bước kiểm tra chỉ đọc, xem cấu hình nền tảng trước khi release mà không build, migrate hay thay đổi gì, để lỗi thiết lập lộ ra sớm.
Làm lỗi hiện rõ trên cả hai nền tảng
Alarm rollback chỉ có ích khi ai đó tìm ra được vì sao nó kích hoạt. Tôi tích hợp Sentry để theo dõi lỗi và gom log của API và worker về CloudWatch Logs, trên cả App Runner lẫn ECS. Một bộ lọc loại bỏ các lỗi phía client có thể đoán trước, như not-found hay validation, trước khi gửi lên Sentry, để luồng lỗi chỉ còn những sự cố thật. Tôi tắt performance tracing để tránh tốn chi phí trên mỗi request, và dựa vào log để xem thời gian xử lý. Các quy tắc lọc cần được cập nhật khi code thay đổi, nhưng mọi lỗi server thật vẫn được ghi lại.
Kết quả
API production và các background worker giờ chạy trên ECS, và môi trường App Runner cũ đã được tắt. Mỗi bản release đều đi qua blue/green với một khoảng thời gian thử ngắn, và phiên bản lỗi sẽ tự rollback dựa trên tỉ lệ lỗi. Khi có sự cố, lỗi xuất hiện trong Sentry và log liên quan nằm ở cùng một chỗ, nên việc điều tra bắt đầu từ bằng chứng.
Nếu làm lại
Tôi sẽ biến quy tắc tương thích ngược thành một bước kiểm tra tự động cho migration mới ngay từ đầu, để nó không phụ thuộc vào việc reviewer có nhớ hay không. Tôi cũng sẽ ghi lại phần rollback phía database rõ ràng như phần traffic, vì alarm có thể đưa traffic về bản cũ nhưng không thể hoàn tác một thay đổi schema.