The Twelve-Factor App năm 2011
(12factor.net)- The Twelve-Factor App là một phương pháp luận để vận hành và mở rộng ứng dụng web·SaaS trong thời gian dài, đồng thời đề cập đến tự động hóa cấu hình, tính di động, triển khai đám mây và triển khai liên tục
- Không bị ràng buộc vào một ngôn ngữ lập trình cụ thể hay tổ hợp dịch vụ hỗ trợ như cơ sở dữ liệu, hàng đợi, bộ nhớ đệm trong RAM, nên có thể áp dụng cho nhiều loại ứng dụng dạng dịch vụ
- Nội dung này dựa trên kinh nghiệm trực tiếp tham gia phát triển và triển khai hàng trăm ứng dụng trên nền tảng Heroku, đồng thời gián tiếp quan sát quá trình phát triển, vận hành và mở rộng của hàng trăm nghìn ứng dụng
- Vấn đề cốt lõi là cung cấp một bộ từ vựng chung để giảm chi phí cộng tác và sự bào mòn phần mềm khi ứng dụng tăng trưởng một cách hữu cơ
- Không chỉ dành cho các nhà phát triển xây dựng ứng dụng dịch vụ mà còn có thể được các kỹ sư vận hành dùng như một tiêu chuẩn thực tiễn để triển khai và quản lý
12 nguyên tắc vận hành cho ứng dụng SaaS
- Phần mềm hiện đại thường được cung cấp dưới dạng ứng dụng web hoặc SaaS, và Twelve-Factor App là một phương pháp luận để xây dựng những ứng dụng như vậy
- Mục tiêu là làm cho quá trình phát triển·triển khai·vận hành ứng dụng trở nên dễ dự đoán hơn
- Sử dụng tự động hóa cấu hình theo kiểu khai báo để giảm thời gian và chi phí khi thành viên mới tham gia dự án
- Thiết lập một hợp đồng rõ ràng với hệ điều hành nền tảng để tăng tính di động giữa các môi trường chạy
- Được thiết kế để giảm gánh nặng quản trị máy chủ và hệ thống, đồng thời phù hợp với việc triển khai trên nền tảng đám mây hiện đại
- Giảm khác biệt giữa môi trường phát triển và môi trường production để cho phép triển khai liên tục
- Có thể mở rộng mà không cần thay đổi lớn về công cụ, kiến trúc hay thực hành phát triển
- Phạm vi áp dụng không bị giới hạn ở một stack công nghệ cụ thể
- Có thể áp dụng cho ứng dụng được viết bằng bất kỳ ngôn ngữ lập trình nào
- Dịch vụ hỗ trợ bao gồm cơ sở dữ liệu, hàng đợi, bộ nhớ đệm trong RAM, v.v.
Bối cảnh được đúc kết từ kinh nghiệm Heroku
- Những người đóng góp đã trực tiếp tham gia phát triển và triển khai hàng trăm ứng dụng trên nền tảng Heroku, đồng thời gián tiếp quan sát quá trình phát triển, vận hành và mở rộng của hàng trăm nghìn ứng dụng
- Dựa trên kinh nghiệm và quan sát từ các ứng dụng SaaS thực tế, họ đã tổng hợp các thực hành lý tưởng cho việc phát triển ứng dụng
- Chú ý đến dòng chảy mà trong đó ứng dụng tăng trưởng một cách hữu cơ theo thời gian
- Đề cập đến cách nhiều nhà phát triển cộng tác trên cùng một codebase
- Xem tránh chi phí bào mòn phần mềm là một mục tiêu quan trọng
- Hình thức trình bày được lấy cảm hứng từ Patterns of Enterprise Application Architecture và Refactoring của Martin Fowler
12 yếu tố
- I. Codebase: Có một codebase được quản lý phiên bản và nhiều môi trường triển khai
- II. Dependencies: Khai báo và cô lập dependency một cách tường minh
- III. Config: Lưu cấu hình trong môi trường
- IV. Backing services: Xem dịch vụ hỗ trợ như các tài nguyên được gắn kết
- V. Build, release, run: Tách biệt nghiêm ngặt giai đoạn build và giai đoạn chạy
- VI. Processes: Chạy ứng dụng như một hoặc nhiều process không trạng thái
- VII. Port binding: Cung cấp dịch vụ ra bên ngoài bằng port binding
- VIII. Concurrency: Scale-out bằng mô hình process
- IX. Disposability: Tăng độ vững chắc bằng khởi động nhanh và kết thúc êm ái
- X. Dev/prod parity: Giữ môi trường phát triển, staging và production giống nhau nhiều nhất có thể
- XI. Logs: Xem log như một luồng sự kiện
- XII. Admin processes: Chạy tác vụ quản trị như các process dùng một lần
1 bình luận
Ý kiến trên Hacker News
12-Factor App là một bộ khuyến nghị được tạo ra năm 2011, dựa nhiều hơn vào Heroku và những hạn chế của hạ tầng dạng container thời đó, chứ không giống một tài liệu dựa sâu trên các nguyên tắc kỹ thuật
Ví dụ, lập luận rằng nên đưa cấu hình vào biến môi trường là vì các tác giả làm ở Heroku, và Heroku khi đó điền biến môi trường thông qua các trường nhập liệu của ứng dụng web
Nếu muốn theo dõi lịch sử cấu hình bằng quản lý phiên bản, dùng GitOps, dùng ConfigMap của k8s, hoặc đặt tệp cấu hình trên volume được mount, thì nhìn chung tất cả đều là lựa chọn ổn. Vì chúng tách trạng thái cấu hình khỏi trạng thái triển khai ứng dụng
Tôi xem tài liệu này là một guideline có hại, vì nó nhầm lẫn giữa rừng và cây, và đưa ra khuyến nghị phù hợp với tính năng sản phẩm của công ty viết ra nó hơn là với các nguyên tắc kỹ thuật thực sự
Nếu muốn lưu cấu hình an toàn trong Kubernetes, cuối cùng bạn sẽ dùng Secrets, và nó cũng có dạng khóa-giá trị như biến môi trường. Nếu muốn có mức bảo mật tương tự trong Git, bạn cần một lớp mã hóa, khi đó việc so sánh diff bị hỏng và cần thêm công cụ
Cuối cùng lại quay về lý do vì sao các công cụ triển khai cấp cao như Heroku được tạo ra
Nguyên tắc xem log là stream vẫn đúng. Hãy ghi log ra STDOUT thay vì vào file, rồi để orchestrator đọc và lưu lại
Cấu hình được lấy từ môi trường. Tùy cách triển khai, ứng dụng có xu hướng đọc cấu hình từ các nguồn khác nhau, chẳng hạn .env ở local và kho bí mật ở production
Port binding cũng tương tự: ứng dụng mở port, rồi đặt thứ như nginx ở phía trước để cấu hình reverse proxy. Service và ingress của K8S đảm nhiệm vai trò đó
Phê phán lớn nhất có thể dành cho 12-Factor ngày nay là tài liệu thực ra không được viết tốt, và giả định rằng người đọc đã biết chính xác nó đang nói về điều gì
Tuy nhiên, cũng phải công nhận công lao của Heroku trong việc phổ biến khái niệm này và mở rộng việc sử dụng nó
Không phải kiểu thêm settings.json vào máy trước khi khởi động ứng dụng, mà là cùng một source khi được triển khai lên cụm AKS ở Azure EU north thì dùng các giá trị được cấu hình trong cụm đó, còn khi được triển khai lên cụm RPi Zero Docker Swarm trong khung ảnh Ikea thì dùng cấu hình của cụm đó
Tên cụm đó là Gibson
Tôi cảm thấy từng mục này đều có thể bị phản bác khá hợp lý
Thứ nhất, nguyên tắc một ứng dụng một repository không sai về căn bản. Nhưng cũng không có vấn đề gì khi phát triển trong cùng một repository nhiều ứng dụng được liên kết chặt về mặt chức năng và chia sẻ chu kỳ release, nhưng cần triển khai riêng để có được lợi ích của các process tách biệt và khả năng scale độc lập. Tôi nghĩ đến việc tách public API và worker process, chẳng hạn Sidekiq của Ruby, Celery của Python, hay các Kafka consumer thông thường
Thứ hai, câu “ứng dụng 12-Factor không phụ thuộc vào sự tồn tại ngầm định của các package toàn hệ thống” thực tế rất khó đạt được nếu không dùng thứ như Nix. Sự phụ thuộc vào API system call của kernel cũng bị rò rỉ ra ngoài, và hầu hết ứng dụng Rust đều phụ thuộc ngầm vào glibc, trừ musl. Ngoài các bản phân phối slim như Alpine, glibc hiện diện trên các bản phân phối Linux lớn. Tôi cho rằng Docker cũng là một chỗ dựa cần thiết để giảm nhẹ vấn đề này
Thứ ba, lưu cấu hình trong biến môi trường có vẻ mong manh hơn. Việc buộc đưa secret vào môi trường có thể làm giảm bảo mật, và khiến ta từ bỏ cấu hình dạng file có cấu trúc, vốn có thể hưởng lợi từ type safety, autocomplete trong IDE và parsing tự động. Với biến môi trường, bạn phải tự triển khai parser cho các giá trị cấu hình phức tạp không phải chuỗi. Trên thực tế, chúng cũng thường được lưu dưới dạng file .env và commit vào repository, nên lập luận về an toàn khi commit cũng trở nên vô nghĩa
Nó không có nghĩa là đừng phụ thuộc vào hệ điều hành, bao gồm glibc, mà là đừng khiến ứng dụng chỉ chạy được khi một package của trình quản lý package cho ngôn ngữ cụ thể đã được cài trên máy
Tôi đồng ý với các điểm còn lại. Đặc biệt, câu chuyện biến môi trường có vẻ giống việc các tác giả giả định cách làm phổ biến trong phát triển Ruby thời đó là tốt nhất rồi viết ra, hơn là một lời khuyên thực sự có cơ sở
Vì vậy phải chuẩn bị cho tình huống đó, và để dễ kiểm thử các tổ hợp phiên bản khác nhau, tôi nghĩ repository riêng là tốt hơn
Phần lớn ngôn ngữ cấp cao không phụ thuộc vào một glibc cụ thể. Nếu runtime của ngôn ngữ đó hoạt động đúng, ứng dụng cũng chạy trên đó. Tất nhiên trong một số trường hợp bạn sẽ dùng thứ như Docker. Việc khó không có nghĩa là không có giá trị
Các ngôn ngữ động hoặc ngôn ngữ managed thường có thể bỏ qua phần lớn rắc rối liên quan đến package hệ thống
Nhìn chung thì tôi thích, nhưng đã có quá nhiều lần người không chuyên kỹ thuật hoặc chỉ biết nửa vời về kỹ thuật lôi “12 factor” ra như một lá thẻ vàng vạn năng để trì hoãn release, đến mức gần như tôi đã bỏ qua hoàn toàn
Thật ra “agile” cũng tương tự. Tôi hiểu ý đồ của những guideline kiểu này, nhưng giá trị thực tế của chúng có vẻ hữu ích hơn nhiều cho những người chỉ có thể mang lại lãnh đạo kỹ thuật kiểu tháp ngà
Tôi từng gặp trường hợp junior engineer quá nhiệt tình hoặc người muốn làm kiến trúc sư dùng bài viết 12-Factor như yêu cầu bắt buộc cho mọi lần phát hành
Những thứ này là mục tiêu tốt đáng theo đuổi, nhưng trong thực tế phải giải thích một cách dứt khoát và nhất quán rằng để release thì cần thỏa hiệp, và phải chọn phần nào sẽ làm chậm hoặc trì hoãn
Sẽ không chặn release chỉ vì một sai lệch nhỏ, nhưng nếu nhìn tổng thể mà không phù hợp thì ít nhất nên xem đó là nợ kỹ thuật. Nếu một release nào đó thụt lùi nghiêm trọng ở một mục, thì chặn lại, hoặc ít nhất buộc phải xem xét kỹ hơn vì sao họ cho rằng sự đánh đổi đó đáng giá, là điều công bằng
Nếu tổ chức đã chọn 12FA làm best practice thì nên đáp ứng, nhưng không được để nó chặn triển khai
12FA không phải là một ô checkbox duy nhất cần tick. Khi sản phẩm trưởng thành, có thể, và phần lớn là nên, triển khai bằng cách thêm từng mục một vào sản phẩm, nếu cần thì chia nhỏ hơn nữa
Nếu ngay từ đầu đã làm engineering tốt, tức là có abstraction và interface phù hợp, không hardcode mọi thứ, thì chuyện này không nên là vấn đề
YAGNI cũng bị lạm dụng chẳng kém 12FA. Điều thiếu lớn nhất ở 12FA là ví dụ cụ thể để junior engineer có thể tham khảo
Tôi đã học cách nhìn nhận những vấn đề này với thiện chí. Cần xem vì sao họ nêu vấn đề, quy trình hiện tại có không rõ ràng, có khiếm khuyết, hay thiếu độ tin cậy không
Nếu phát hiện lo ngại hợp lý thì chỉ cần thay đổi quy trình và cập nhật tài liệu
Twelve-Factor App nói hãy dùng environment cho cấu hình, còn Docker nói đừng dùng environment cho cấu hình, vì không an toàn
Tôi thích và dùng nhiều pattern của 12-Factor, nhưng một số được viết trong bối cảnh VPS. Khi đó environment ổn định, an toàn và cố định hơn, còn trong container thì environment có thể bị đưa vào một layer nào đó
Trong thời đại container, mục cụ thể này khá gây đau đầu. Docker secrets cũng không phải lúc nào cũng khớp tốt, nên để làm cho nó chạy được phải cần đủ kiểu nhào lộn
Việc inject secret vào app dưới dạng environment variable không có nghĩa là toàn bộ quá trình xử lý bên ngoài đó cũng không an toàn. Ví dụ container AWS ECS có hỗ trợ tích hợp để lấy secret từ Secret Manager lúc khởi động rồi truyền vào environment variable: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/...
Secret được lấy tại thời điểm container khởi động, từ Secret Manager bằng IAM credential của ứng dụng đang chạy. Vì vậy nó phải có quyền đối với secret đó
Lợi ích của việc dùng environment variable dường như hoàn toàn phụ thuộc vào cuối cùng nó được thiết lập như thế nào. Với cơ chế kiểu này thì tôi không thấy nhược điểm lớn nào
Nhược điểm chính có thể thấy là malware thông thường muốn dump environment variable có thể bắt được giá trị, nhưng nếu bạn không tránh việc lưu secret vĩnh viễn trong bộ nhớ thì việc chống mối đe dọa đó phần lớn chỉ gần như là obfuscation
Việc private key bị lộ như vậy là điều thật sự nên tránh
Cách mount secret vào filesystem bằng k8s thì tôi không thấy có vấn đề gì đặc biệt. Tất nhiên tất cả những điều này phụ thuộc vào môi trường triển khai
Đôi khi environment variable là lựa chọn ít tệ hơn. Vì secret vốn dĩ lúc nào cũng khó
Nếu mù quáng tin input chỉ vì là single-tenant, thì sau này khi điều kiện thay đổi do một cú xoay hướng kinh doanh tùy ý, bạn sẽ gặp những bug khó truy vết và nhiều đêm dài
Trong vài năm qua tôi đã thảo luận nhiều về app 12-Factor và cũng thấy không ít nhầm lẫn. Trang 12-Factor rất hay, nhưng chủ yếu phù hợp với những người đã hiểu vì sao các mục đó quan trọng.
Với những người không biết lý do đằng sau các quy tắc, cần có giải thích sâu hơn. Vì vậy tôi đã làm video “What are 12 Factor Apps and Why Should You Care?”[1], và nghe nói một số công ty dùng nó để đào tạo nhân viên mới mảng engineering/DevOps, khá hữu ích.
Dù học ở đâu, app 12-Factor vẫn đáng để bỏ ra một hai giờ tìm hiểu. Phần lớn “quy tắc” là những điều cần nhận thức; trước khi tự mắc lỗi và chịu đau đớn để học, chúng không hiển nhiên ngay.
[1] https://youtu.be/REbM4BDeua0
Khái niệm “unit test” bắt đầu có traction vào khoảng cuối thập niên 90, và tôi thấy nhẹ nhõm khi có một nhân vật có thẩm quyền đứng ra ủng hộ.
Lý do tôi không tự làm trước khi Kent Beck công bố JUnit là vì code tôi đang làm không có cấu trúc tốt để được điều khiển bởi code khác. Do biến toàn cục, trạng thái lan rộng, phụ thuộc vào hệ thống bên ngoài và layout hệ thống tệp cụ thể, cùng việc bỏ qua tính mô-đun, nên hầu như không thể chạy gì ngoài ngữ cảnh mà nó được thiết kế cho.
Tất cả những thứ đó đều là “thiết kế tệ”, nhưng vẫn kịp deadline, nên ai cũng làm vậy. Tôi từng hy vọng rằng khi unit test có traction, lập trình viên sẽ thoát khỏi kiểu thiết kế nguyên khối.
Sau 25 năm chứng kiến unit test cho getter/setter, và một unit test khổng lồ tạo cả cơ sở dữ liệu in-memory vì mọi hàm trong app chỉ cần chạy thôi cũng đòi cơ sở dữ liệu live, rồi cuối cùng thất bại và bị comment out, tôi đã mất niềm tin rằng unit test sẽ trở thành thứ gì đó hơn một checkbox vô nghĩa. Ai cũng điền vào vì đó là “best practice”, nhưng không dừng lại để nghĩ vì sao mình làm.
Tôi luôn ít đồng ý nhất với lời khuyên về cấu hình. Cấu hình thường do nhiều chủ thể định nghĩa, và nhiều khi cả developer cũng định nghĩa, nên thường tốt nhất là đóng gói các giá trị mặc định hợp lý cùng ứng dụng, rồi cho phép ghi đè bằng file theo từng môi trường và biến môi trường.
Với hầu hết ứng dụng phía server, cách này linh hoạt nhất. Nhiều khi ta đã biết cấu hình nên như thế nào, và nó nên được quản lý trong source control, nhưng secret thì phải được inject lúc chạy.
Nếu không muốn tốn thời gian cấu hình khổng lồ cho dev, test và production, cấu hình cần có ghi đè phân cấp.
Nếu là cho dev, cuối cùng nó sẽ phá production; còn mặc định cho production có thể chẳng có ý nghĩa gì trong dev.
Một cách nghĩ về phần cấu hình là: “Chiến lược cấu hình này có hợp với container không?” Khi build image, bạn có trạng thái đĩa tĩnh, nơi các thay đổi sẽ không được giữ lại trừ khi tạo image mới.
Nếu cấu hình chỉ dựa trên file, bạn phải build một image hoàn toàn mới để chuyển đổi hành vi giữa test và production.
Việc cho phép thay đổi cấu hình độc lập với đĩa gốc giúp cô lập thay đổi. Bạn cần phân biệt được ứng dụng hỏng vì deployment, tức quá trình tạo image, bị lỗi, hay vì cấu hình sai.
Khi tách việc tạo image khỏi thay đổi cấu hình, bản thân câu hỏi đó biến mất.
Ví dụ, không được cho phép một lỗi con người đủ dễ đoán như việc QA gửi 100.000 lần thử lại bị từ chối xác thực vào queue xử lý production.
Tuy vậy, nhiều cấu hình không phải về hạ tầng, mà thường là những vấn đề như nên nối bean ResolverStrategy nào trong từng môi trường.
Cấu hình nên được quản lý phiên bản, nhưng nên được quản lý tách khỏi source code. Vì cấu hình mô tả bản thân deployment, chứ không phải image dùng để deploy.
Đây chắc chắn là một chuẩn mực engineering có ảnh hưởng. Giờ có rất nhiều abstraction dễ dùng của các dịch vụ hosting như Render hay Vercel, nên hơi lạ khi nghĩ rằng tài liệu này được viết năm 2012, và khi đó web app, xét về các thực hành chung được chấp nhận, còn giống miền Tây hoang dã hơn nhiều.
Điều thiếu lớn trong tài liệu này là sự biện minh cho các quy tắc. Gần như toàn bộ chỉ là quy tắc.
Khó đánh giá quy tắc có tốt hay không, và tài liệu này không giúp làm điều đó.
Chỉ nhìn tiêu đề, tôi tưởng đây là bình luận về xác thực hai bước. Kiểu như một app đòi ảnh hộ chiếu, quét khuôn mặt, bằng lái xe, tin nhắn SMS, Google Authenticator, link email, mật khẩu và vân tay chỉ để đăng nhập một lần.
Thời kỳ đầu của Docker, tôi đã làm khá nhiều việc để khiến WordPress hoạt động giống một Twelve-Factor App.
Theo truyền thống, WordPress không hoạt động như vậy, và ở mức nào đó thì cũng dễ hiểu. WordPress lớn lên trong một thế giới nơi các server sống lâu, có đĩa cục bộ ghi được và bền vững, là chuyện phổ biến.
Sau đó tình hình chắc đã thay đổi nhiều. Đây là chuyện khoảng năm 2016, nhưng đó thật sự là một thử thách thú vị.
Tôi ngạc nhiên vì mọi thứ trở nên đơn giản đến mức nào, và có thể load balance nhiều node mà không phải bận tâm đến session stickiness.
Tất nhiên, theo những cách khác thì lại khó hơn, chẳng hạn cần một DB riêng cho session.