7 điểm bởi GN⁺ 2023-07-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • Các dịch vụ cá nhân được tự vận hành trực tiếp trên VM DigitalOcean 5 USD/tháng và Debian 10; với các dịch vụ quy mô nhỏ, cách cài phần mềm máy chủ lên VM cũng đủ đơn giản và hợp lý
  • Ứng dụng máy chủ được tạo dưới dạng binary tĩnh Rust, giữ artifact triển khai chỉ là một file duy nhất, bao gồm cả HTML, CSS, cấu hình và secret
  • Việc quản lý chạy dịch vụ do systemd đảm nhiệm; xử lý HTTPS được giao cho reverse proxy nginx và Let’s Encrypt/certbot để ứng dụng không phải trực tiếp xử lý TLS
  • Các dịch vụ cần dữ liệu dùng một file SQLite duy nhất, kết hợp backup hằng ngày bằng Tarsnap và backup streaming lên DigitalOcean Spaces bằng Litestream
  • Khi đặt nhiều dịch vụ trên một VM, cô lập bằng Unix user riêng cho từng dịch vụ; nếu cần bảo mật mạnh hơn thì chọn VM riêng, systemd-nspawn hoặc firejail

Cấu hình cơ bản cho máy chủ và triển khai

  • Nhiều dịch vụ như thoughts.page, hanabi.site, cgmserver, phonebridge được vận hành gần với cấu hình này
  • Ứng dụng chạy trên VM DigitalOcean, gói là tier 5 USD/tháng và hệ điều hành là Debian 10
    • Một số dịch vụ dùng chung cùng VM, một số được tách sang VM khác
  • Phần mềm máy chủ được viết bằng Rust
    • Được liên kết tĩnh
    • HTML, CSS, cấu hình, secret, v.v. được compile vào binary
    • Sử dụng rust-musl-builderrust-embed
  • Với cách này, việc triển khai được đơn giản hóa thành thao tác copy một file duy nhất lên máy chủ
    • Có thể áp dụng cách tương tự với Go, C++, v.v.
    • Với các ngôn ngữ không dễ triển khai dưới dạng một binary duy nhất, có thể dùng Docker container làm artifact build thay thế

Chạy dịch vụ, proxy và lưu giữ dữ liệu

  • Việc quản lý khởi động dịch vụ do systemd đảm nhiệm
    • Đảm bảo binary chạy cùng khi máy chủ khởi động
    • Hầu hết các file systemd unit là file đơn giản 9 dòng
    • Bản thân systemd phức tạp, nhưng khi chỉ dùng để khởi động máy chủ lúc boot thì hầu như không phải đối mặt với phần phức tạp đó
  • Triển khai được xử lý bằng simple deploy script
    • Copy binary lên máy chủ và khởi động lại máy chủ
    • Có thể rollback, và đảm bảo luôn có một phiên bản hợp lệ đang chạy ngay cả khi mất kết nối trong lúc triển khai
  • Các chương trình cần cơ sở dữ liệu dùng SQLite
    • Toàn bộ trạng thái ứng dụng nằm trong một file duy nhất
    • Mỗi ngày tạo backup bằng lệnh SQLite .backup và lưu vào Tarsnap
    • Script backup được chạy bằng cron
    • Dùng Litestream để stream bản sao cơ sở dữ liệu theo từng giây lên storage DigitalOcean Spaces, và tạo snapshot mỗi 6 giờ
  • Tất cả máy chủ chạy phía sau reverse proxy nginx
    • nginx xử lý TLS termination nên ứng dụng không cần bận tâm đến HTTPS
    • Chứng chỉ HTTPS được cấp bằng Let’s Encryptcertbot, đồng thời được tự động gia hạn
    • Ví dụ cấu hình nginx của hanabi.site có trong gist riêng
    • File tĩnh có thể được phục vụ bằng nginx, chỉ cần copy lên máy chủ bằng scp hoặc rsync

Bảo trì và cô lập dịch vụ

  • Cấu hình này hướng tới một đường vận hành đơn giản và vững chắc
    • Ngoài bản thân ứng dụng, các phần mềm trên đường phục vụ đều là những thành phần đã được kiểm chứng qua hàng chục năm sử dụng
    • Miễn là tiếp tục trả phí DigitalOcean, về bản chất việc vận hành site gần như không cần bảo trì
    • Vấn đề duy nhất mà hệ thống monitoring phát hiện là sự cố mạng DigitalOcean tạm thời
  • Đôi khi cần cập nhật hệ điều hành và bảo mật
    • Các bản phát hành Debian được hỗ trợ 5 năm, nên khoảng 2 năm rưỡi nữa sẽ phải nâng cấp lên Debian 11
    • Nếu lại xảy ra sự kiện như Heartbleed thì sẽ cần vá lỗi
    • Những sự kiện như vậy khá hiếm
  • Chi phí dùng VM 5 USD/tháng cho từng dịch vụ có thể là gánh nặng, nhưng có thể chạy nhiều dịch vụ trên cùng một VM
    • Việc cô lập được cung cấp bằng cách tạo tài khoản Unix user riêng cho từng dịch vụ
    • Cách cô lập này đã tồn tại từ những ngày đầu của Unix nên có vẻ vững chắc
    • Nếu cần cô lập mạnh hơn, có thể dùng systemd-nspawn hoặc firejail
    • Nếu thật sự quan trọng về mặt bảo mật, thì trả thêm 5 USD/tháng và chạy trên VM riêng
  • Quy trình thiết lập dự án mới khá ngắn
    • Tạo user mới
    • Thêm virtual host nginx và cấp chứng chỉ HTTPS bằng certbot
    • Thêm systemd unit
    • Commit deploy script vào repository và chạy
  • Ban đầu có nhiều thứ phải học, nhưng hạ tầng này thay đổi chậm hơn hạ tầng cloud rất nhiều
    • Định dạng cấu hình nginx về cơ bản gần như không đổi trong 10 năm qua
    • Thay đổi lớn gần nhất trong quản trị hệ thống Debian là việc chuyển sang systemd gần 10 năm trước
    • Ít bị phơi nhiễm trước tình huống nhà cung cấp cloud khai tử dịch vụ hoặc âm thầm thay đổi hành vi
    • Phụ thuộc duy nhất là nhà cung cấp VPS, và máy chủ là một commodity phổ dụng nên có thể chuyển sang nhà cung cấp khác

1 bình luận

 
GN⁺ 2023-07-17
Ý kiến Hacker News
  • Nếu chạy mỗi dịch vụ bằng một người dùng Unix riêng để cách ly, tính năng DynamicUser của systemd có thể giúp tiết kiệm thời gian
    Nó sẽ cấp phát UID và tạo thư mục log/trạng thái với quyền phù hợp
    https://0pointer.net/blog/dynamic-users-with-systemd.html

    • Nếu UID cần tồn tại lâu dài, việc tạo người dùng hệ thống mới bằng systemd-sysusers cũng rất dễ
      Chỉ cần đặt một tệp văn bản nhỏ trong /etc/sysusers.d/ chứa thông tin như tên người dùng, thư mục home, rồi chạy lệnh hoặc dịch vụ sysusers
    • Dùng tài khoản dịch vụ riêng cho từng dịch vụ/ứng dụng là cách làm khá tiêu chuẩn, và tốt hơn là cũng tách riêng cả máy chủ
  • Dạo này tôi thích nhất là hàm đám mây kích hoạt bằng HTTP
    Nếu cẩn thận tránh các bẫy riêng của từng nhà cung cấp, nó giảm độ phức tạp đi rất nhiều, và là lớp trừu tượng cloud-native duy nhất cho tôi cảm giác như “hình thái cuối cùng”
    Chỉ riêng một ứng dụng tôi đã triển khai hơn 2000 lần, mà chưa từng có lần nào môi trường thực thi bị hỏng đến mức phải liên hệ đội hỗ trợ hay gõ các lệnh console khó hiểu
    Hiệu năng cũng rất tốt theo tiêu chuẩn App Service plan cô lập của Azure, và sự phản đối mang tính ý thức hệ với mô hình tính phí theo từng request giờ cũng không còn nữa
    Sở hữu cả bất động sản đặt máy chủ thì có thể rẻ hơn, nhưng để tự triển khai những thuộc tính như tuân thủ và kiểm toán mà một hàm HTTP đơn giản mang lại, trên thực tế gần như phải lập ra một công ty khổng lồ và tuyển cả đống người
    Lập luận về phụ thuộc nhà cung cấp cũng không còn thuyết phục tôi nữa. Giao diện nhận HTTP request và trả về HTTP response là rất tự nhiên; khác biệt giữa các nhà cung cấp chủ yếu chỉ là ngữ cảnh phụ như method signature của trigger hay các claim OIDC/SAML, nên nếu muốn tạo ra một cấu trúc không thể refactor sang nhà cung cấp khác trong vòng một tuần thì có khi còn phải cố tình làm vậy
    Tôi hiểu là với blog cá nhân, mua một VM của Hetzner rồi chăm chút như thợ thủ công sẽ vui hơn. Nếu là ngành hoàn toàn không bị quản lý, thì việc cân đo biên lợi nhuận và độ phức tạp kỹ càng thay vì thuê ngoài toàn bộ máy chủ cũng là một quan điểm có thể được bảo vệ mạnh hơn

    • Tôi đã đảo mắt ở đoạn “nếu tự triển khai thì phải lập một công ty tỷ đô và tuyển thêm 1000 người”
      Không hiểu họ nghĩ trước chuyển dịch lên đám mây thì mọi người đã làm thế nào
      Hiện tôi đang phụ trách nhiều team trong một công ty công nghiệp lớn và mở rộng, triển khai các ứng dụng đổi mới trên Azure, nhưng chi phí đám mây thì lớn đến vô lý so với số người dùng
      Ngày trước chúng tôi từng vận hành các ứng dụng có số người dùng gấp 10 lần với chi phí chỉ bằng 1/100 và độ phức tạp còn thấp hơn
      Khoản chi này được tính sang các bộ phận khác đã đưa ra những lựa chọn đáng ngờ như vậy nên cá nhân tôi không bị ảnh hưởng, nhưng tôi khó hiểu thái độ mù quáng tin vào đám mây
    • Tôi làm trong một ngành bị quản lý cực kỳ chặt chẽ, nhưng không hiểu câu cuối
      Với chúng tôi, tự vận hành phần cứng là lựa chọn tốt nhất, và do mức độ bảo mật kiểu không được chụp ảnh trong văn phòng, khả năng giao bất cứ thứ gì cho nhà cung cấp đám mây là 0%
    • Hàm là một lựa chọn ổn để host API đơn giản
      Nhưng để ứng dụng trở nên hữu ích thì còn cần những thành phần khác như cơ sở dữ liệu, và chẳng phải chi phí sẽ tăng ở đó sao?
    • Tôi không rõ chính xác hàm đám mây kích hoạt bằng HTTP ở đây là gì
      Không biết đó là request handler của máy chủ HTTP, điện toán đám mây dạng function-as-a-service, hay chỉ là RPC kiểu cũ
    • Điều quan trọng là hàm đó thực sự làm gì
      Nếu có script kiddie nào đó bắt đầu gửi request 10 lần mỗi phút, thì sẽ mất bao lâu để chi phí của hàm vượt qua VPS?
  • Tôi dùng một cấu hình tương tự cho website cá nhân và các site dự án
    VM Linode tôi dùng là gói 5 USD/tháng, chạy Debian GNU/Linux, và phần mềm được viết bằng Common Lisp
    Website cá nhân hay blog thì tôi tạo site tĩnh bằng chương trình Common Lisp, còn dịch vụ trực tuyến hay web app thì được làm bằng chương trình Common Lisp dùng Hunchentoot để xử lý và phản hồi HTTP request
    Tôi dùng file unit systemd để site/dịch vụ tự động khởi chạy khi VM khởi động hoặc khởi động lại; đa số chỉ khoảng 10–15 dòng
    Cấu hình ban đầu của VM được mã hóa bằng shell script: https://github.com/susam/dotfiles/blob/main/linode.sh
    Cấu hình theo từng dự án và từng dịch vụ được quản lý bằng Makefile riêng: https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
    Tôi không dùng container. Các site này đã chạy nhiều năm từ trước khi container trở nên phổ biến, và đến giờ thì script khởi tạo cùng Makefile vẫn là đủ
    Tôi cũng dùng Nginx. Nó phục vụ file tĩnh và, khi có dịch vụ backend, đóng vai trò reverse proxy; ngoài việc kết thúc TLS, nó còn có lợi ích như giới hạn tốc độ request hay cấu hình danh sách cho phép HTTP header để bảo vệ backend
    Trong playbook cá nhân nhỏ của tôi có vài lệnh như curl LINK -o linode.sh && sh linode.sh, git clone LINK && cd PROJECT && sudo make setup https
    Các target của make xử lý những việc cần thiết để chạy site như cài đặt công cụ Nginx, certbot, sbcl, cấu hình Nginx, cấu hình chứng chỉ, v.v.; khi lệnh kết thúc thì website sẽ được public ngay

    • Tôi tò mò bạn dùng implementation Common Lisp nào
      Nhìn vào Makefile thì có vẻ là SBCL, nên tôi cũng muốn biết việc dùng bộ nhớ có từng là vấn đề không
      Tôi đang dùng VPS 512MB RAM, mà một instance SBCL đã ngốn khoảng 100MB nên chỉ có thể chạy đồng thời vài dịch vụ
      Tôi từng nghĩ đến việc chuyển các dịch vụ có lưu lượng thấp nhất sang CLISP, nhưng nó lại thiếu một tính năng tôi đang dùng là package-local aliases
  • Sau vài năm liên tục tinh chỉnh cấu hình, giờ đã ổn định với việc chạy mọi thứ trong container Docker
    Dùng docker-compose thay cho systemd làm bộ điều phối, và dùng Caddy thay cho nginx làm proxy
    Giống bài gốc, tôi cũng viết script triển khai cho từng dự án cần chạy nên nhìn chung khá tương tự
    Một trong nhiều ưu điểm của Docker là có thể chạy cả phần mềm bên thứ ba với cùng một cấu hình
    Tôi đã dùng cấu hình này nhiều năm rồi và thấy rất tốt; giống như bài gốc, nó vừa vững chắc vừa đủ linh hoạt để thay đổi theo ý muốn khi cần
    Điểm đau duy nhất hiện tại là triển khai rolling. Khi phần mềm lớn dần, việc mỗi lần triển khai đều có vài giây downtime đang trở thành vấn đề, và tuy chưa có lời giải đơn giản, có lẽ docker swarm là hướng đi đúng

    • Tôi cũng vận hành theo cách tương tự với Caddy
      Để giảm downtime, có thể thử health_uri /health, lb_try_duration 30s
      Ví dụ, nếu cấu hình như reverse_proxy api:8089 { health_uri /health lb_try_duration 30s }, Caddy sẽ đệm các yêu cầu trong lúc triển khai phiên bản mới và cho dịch vụ mới tối đa 30 giây để khởi động
      Lý tưởng nhất là sau khi phiên bản mới đạt trạng thái khỏe mạnh, Caddy bắt đầu dùng nó rồi mới giết container cũ
      Tôi đã xem https://github.com/Wowu/docker-rollouthttps://github.com/lucaslorentz/caddy-docker-proxy nhưng vẫn chưa ưu tiên được việc này
    • Nếu có bộ cân bằng tải như Caddy đứng trước các pod, bạn có thể cấu hình để nó giữ yêu cầu lại trong lúc pod mới khởi động: https://twitter.com/bradleyjkemp/status/1486756361845329927
      Không hoàn hảo, nhưng thay vì trình duyệt nhận lỗi kết nối thì nó sẽ chỉ tải trong vài giây
      https://mrsk.dev/ cũng dùng cùng kỹ thuật này
    • Khi xây dựng software stack của startup nơi tôi làm việc từ đầu, chúng tôi đã đóng gói ứng dụng bằng Docker ngay từ đầu
      Production khởi đầu bằng compose, rồi sau đó được cải tiến thành một pipeline triển khai liên tục tự động nâng cấp stack
      Khi công ty và tệp người dùng tăng lên, vấn đề downtime khi khởi động lại hoặc triển khai bắt đầu xuất hiện, và mỗi khi muốn chạy thêm ứng dụng mới lại phải chuẩn bị một môi trường triển khai mới
      Tôi từng sợ đến ngày phải dùng Kubernetes, vì đã tận mắt thấy độ phức tạp của nó và không muốn dành phần lớn một ngày chỉ để dỗ dành cluster
      Vì vậy chúng tôi chuyển sang chế độ Swarm, và đó là một hành trình lúc thì Jekyll, lúc thì Hyde
      Có những bug không ai muốn sửa, một số phần của đặc tả Docker không được triển khai nhưng cũng chẳng ai báo trước, những lựa chọn triển khai khiến bạn chỉ muốn bứt tóc, và cảm giác như nhân viên Docker Inc không nói chuyện với nhau hoặc không thể tập trung đến cùng
      Dù vậy, nó cũng có nhiều mặt đẹp. Stack compose vẫn hoạt động nguyên vẹn và có thể mở rộng đúng chỗ cần; nếu cấu hình đúng thì triển khai không gián đoạn, nâng cấp, cân bằng tải và rollback đều chạy tốt
      Raft cũng đáng tin cậy trong việc duy trì cluster như ở những nơi khác, và chỉ cần bỏ thêm chút công sức là có thể xây dựng một nền tảng tự phục vụ linh hoạt, an toàn, tự động phân tán với chỉ một phần ngân sách bảo trì K8s
      Tuy vậy, cần chuẩn bị để làm script triển khai cho tử tế. Tôi đã tốn khá nhiều thời gian để viết một công cụ Python chuyển file compose hợp lệ theo đặc tả Docker sang đặc tả Swarm, làm mới và dọn dẹp secret, rồi mở rộng biến môi trường
      Tùy nhà cung cấp VPS, bạn cũng phải bảo đảm cấu hình đúng MTU mạng. Việc này chắc đã làm tôi giảm tuổi thọ kha khá
    • Nếu bạn đã có file compose thì Swarm là hướng tiếp theo, nhưng cá nhân tôi thấy nó không đáng đến vậy
      Ít nhất là cho đến khi số lượng khách hàng hoặc người dùng tăng đủ để vấn đề mở rộng thực sự xuất hiện
    • Tôi cũng đã làm một cấu hình tương tự, nhưng không thích cách đẩy image qua SSH bằng docker savedocker import
      Tôi tò mò không biết bạn có tự vận hành registry không
  • Trên máy chủ vật lý, tôi dùng Podman Pod để đặt cơ sở dữ liệu PostgreSQL và ứng dụng vào trong đó, tất cả đều chạy trên các cổng localhost lớn hơn 5000, còn Caddy hoạt động như một reverse proxy trên cổng 443
    Dùng systemd Timer để dump toàn bộ cơ sở dữ liệu vào một thư mục mỗi ngày lúc 4:55 chiều
    Sau đó DejaDup [1] tự động sao lưu $HOME sang HDD gắn ngoài mỗi ngày lúc 5 giờ chiều; loại trừ file cache nhưng bao gồm cả bản dump cơ sở dữ liệu
    Hệ điều hành là Debian cài GNOME Core [2], và các quy tắc firewalld chỉ cho phép 80, 443 và cổng SSH tùy chỉnh
    SSH dùng khóa và đã tắt xác thực bằng mật khẩu
    Đây là cách làm nhàm chán nhất, nhưng nó cứ thế mà hoạt động tốt
    1 - https://flathub.org/apps/org.gnome.DejaDup
    2 - https://packages.debian.org/bookworm/gnome-core

    • Tôi tò mò vì sao lại chạy GNOME trên server
    • Tôi cũng tò mò không biết có lý do gì để dùng systemd timers thay cho cron job không
  • Dạo này tôi cài Dokku lên một máy chủ duy nhất để dùng
    Quản lý dễ, lập trình viên có thể triển khai chỉ bằng git push theo kiểu Heroku, và có rất nhiều plugin nên việc thêm cơ sở dữ liệu hoặc cấu hình HTTPS cũng chỉ mất tối đa 10 giây

    • Tôi cũng dùng cách này, và việc có thể triển khai bằng git push thực sự rất tiện
      Việc thêm ứng dụng, thêm cơ sở dữ liệu, quản lý biến môi trường và quản lý domain đều cực kỳ trực quan
  • Tôi thích cấu hình đơn giản nên cách này có vẻ khá thú vị để làm việc
    Có khả năng là đủ cho 99% dịch vụ
    Có lẽ tôi sẽ dùng Ansible cho cấu hình máy chủ và script triển khai
    Như vậy máy chủ có thể được tài liệu hóa và script triển khai cũng có thể đơn giản hơn
    Tuy vậy, tôi sẽ không tránh việc đăng nhập trực tiếp vào máy chủ để debug hoặc kiểm tra

    • Dù đang dùng Ansible, tôi đang cân nhắc lại xem nó có thực sự đáng giá không
      Script của tôi gần như không thay đổi suốt 7 năm qua, trong khi Ansible chậm và có xu hướng buộc phải duy trì nhiều tệp liên tục
      Về chuyện debug, cũng có thể dùng nginx để mở log ứng dụng qua một endpoint được bảo vệ bằng mật khẩu
  • Có vẻ mọi người đôi khi quên rằng ngay cả trước thời cloud, CI/CD và quản lý máy chủ hiệu quả cũng đã là thực tiễn phổ biến

  • Về cách viết phần mềm máy chủ bằng Rust, liên kết tĩnh rồi biên dịch HTML, CSS, cấu hình, secret... tất cả vào trong binary, gần đây tôi đang làm như vậy với Go và cực kỳ thích vì nó khiến việc viết và triển khai phần mềm phụ thuộc vào tệp tĩnh trở nên rất dễ dàng

    • Tôi vẫn thấy băn khoăn về việc đưa secret vào trong binary đã biên dịch
      Dùng biến môi trường hoặc cấu hình cho secret gần với chuẩn hơn, và dù có thêm một bước trước khi chạy, nó giúp tránh tình huống chia sẻ binary rồi quên mất bên trong có secret
      Giải nén binary ra để tìm chuỗi là việc khá dễ
      Còn việc gói kèm asset frontend tĩnh và cấu hình mặc định thì là một lợi thế lớn
    • Tôi không hẳn thích Go, nhưng vì lý do này mà cứ phải dùng nó
      Cross-compilation với Rust, đặc biệt từ Mac sang Linux, tương đối đau đầu, trong khi với Go thì rất đơn giản và đã được tích hợp sẵn trong công cụ go
      Nó thực sự giúp giảm ma sát khi hoàn thành và triển khai các side project
    • https://play.clickhouse.com/play?user=play cũng đang làm theo cách này
      Nhưng tôi có một câu hỏi. Trong bài có nói “nhận chứng chỉ Let’s Encrypt bằng certbot và cũng xử lý tự động gia hạn”, còn tôi thì dùng cấu hình đa vùng liên khu vực với hai máy chủ và geo-DNS, nên certbot chỉ chạy trên máy chủ ở Mỹ và tôi phải sao chép thủ công chứng chỉ sang máy chủ ở châu Âu
      Có cách nào giải quyết việc này không?
      Phần mô tả về ClickHouse Playground ở đây: https://ghe.clickhouse.tech/
    • Tôi cũng làm theo cách tương tự
      Tôi phục vụ cả ứng dụng web từ cùng một dịch vụ biên dịch tĩnh đang cung cấp backend API
      Chạy npm build trong CI rồi nhúng kết quả vào giúp việc test local hoặc dựng demo instance trở nên cực kỳ dễ dàng