Trong bài viết này, chúng ta sẽ học cách xây dựng một ứng dụng Rails hỗ trợ nhiều subdomain. Giả sử chúng ta có trang web game funkygames.co và muốn hỗ trợ các subdomain như app.funkygames.co, api.funkygames.co và dev.funkygames.co trong một ứng dụng Rails duy nhất. Chúng ta cần đảm bảo xác thực đúng cho tất cả subdomain và không có route trùng lặp.
Chúng ta sẽ sử dụng các cấu trúc routing mạnh mẽ của Rails để hỗ trợ nhiều subdomain. Cũng sẽ thiết lập subdomain cục bộ và viết test cho nhiều subdomain.
Điều kiện tiên quyết
Đối với bài viết này, tôi giả định bạn đã thiết lập bản ghi DNS phù hợp cho tất cả subdomain trỏ đến ứng dụng Rails. Chúng ta chỉ tập trung vào phía Rails.
Xử lý Nhiều Subdomain
Rails sử dụng file routes.rb để xử lý request đến và ánh xạ chúng tới action controller cụ thể. Trong ứng dụng đơn giản, mọi mapping trong routes.rb ánh xạ route tới action controller như sau:
get '/games/:id', to: 'games#show'
Với cách này, tất cả endpoint định nghĩa trong routes.rb áp dụng cho mọi subdomain. Vì vậy app.funkygames.co/games/1 và api.funkygames.co/games/1 đều được xử lý bởi route này. Tuy nhiên, chúng ta chỉ muốn request từ subdomain app được xử lý bởi route này. Subdomain api chỉ dùng cho route API. Chúng ta sẽ thêm quy tắc vào routes để chúng chỉ được xử lý khi quy tắc cụ thể cho request đến được thỏa mãn.
Routing Rails cung cấp helper method constraints để chỉ định quy tắc bổ sung cho route.
get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }
Điều này đảm bảo nếu request đến từ app.funkygames.co/games/1, nó sẽ được xử lý bởi action show của GamesController. Request từ subdomain khác ngoài app sẽ không được xử lý bởi route này.
Việc định nghĩa constraints như này cho từng route sẽ trở nên rườm rà.
get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }
get '/games/list', to: 'games#list', constraints: { subdomain: 'app' }
post '/games/start', to: 'games#start', constraints: { subdomain: 'app' }
Chúng ta có thể dùng dạng block của helper constraints để định nghĩa nhiều route cho một subdomain.
constraints subdomain: 'app' do
get '/games/:id', to: 'games#show'
get '/games/list', to: 'games#list'
post '/games/start', to: 'games#start'
end
Để định nghĩa route cho nhiều subdomain, chỉ cần thêm nhiều block constraints trong routes.rb.
constraints subdomain: 'app' do ... end
constraints subdomain: 'api' do ... end
constraints subdomain: 'dev' do ... end
Cơ chế bên dưới
Routing Rails cung cấp request constraints và segment constraints. Segment constraints thêm quy tắc trên đường dẫn request, còn request constraints thêm điều kiện trên request đến. Key trong request constraint phải là method của đối tượng Request trả về string, và value là giá trị mong đợi.
constraints subdomain: 'app' do ... end
Ở trên, chúng ta dùng method subdomain trên đối tượng Request và so khớp với string như app, api hoặc dev.
Xem thêm Rails routing guide để biết chi tiết.
Xử lý Subdomain Đa cấp
Giả sử dùng app.staging.funkygames.co cho môi trường staging. Với thiết lập trên, request đến subdomain app sẽ trả về 404. Debug sẽ thấy constraint subdomain thất bại.
request.subdomain #=> app.staging
Chúng ta mong đợi subdomain trả về app, nhưng nó trả về app.staging. Việc parse subdomain được quản lý bởi option config.action_dispatch.tld_length. Giá trị mặc định là 1, chỉ hỗ trợ một cấp subdomain. Với subdomain hai cấp, cần set giá trị là 2.
# config/application.rb
config.action_dispatch.tld_length = Integer(ENV['TLD_LENGTH'] || 1)
Có thể set qua biến môi trường để dùng chung code cho staging và production. Bây giờ routing sẽ hoạt động với app.staging.funkygames.co.
Quản lý Session
Sau khi định nghĩa route cho nhiều subdomain, cần lo xác thực cho tất cả. Có hai cách: cho phép cùng session user trên tất cả subdomain, hoặc session riêng cho từng subdomain.
Xác thực trong gist
Rails mặc định dùng cookie lưu session key. Sau khi user đăng nhập, thông tin session lưu trong session store và session key lưu cookie trên trình duyệt. Lần truy cập sau, cookie session gửi lên server, server kiểm tra session tồn tại hay không.
Cấu hình session mặc định:
Rails.application.config.session_store :cookie_store, key: "_funkygames_session"
Key _funkygames_session là tên cookie session, value là session id.
Cơ bản về Cookie
Mặc định cookie được set trên domain của request. Nếu truy cập từ app.funkygames.co, session cookie set cho app.funkygames.co. Mỗi subdomain set cookie riêng, do đó session user không chia sẻ giữa subdomain mặc định.
Chia sẻ Session giữa Subdomain
Để chia sẻ session, cần set session cookie trên domain funkygames.co để mọi subdomain truy cập được. Thực hiện bằng option domain trong session store.
Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all
Truyền domain: :all báo cho Rails set cookie trên top-level domain (funkygames.co) thay vì request host chứa subdomain. Khi đó session chia sẻ giữa subdomain.
Có thể truyền danh sách domain cho option
domainsdưới dạng mảng để hỗ trợ nhiều domain.
Cần thêm option tld_length để set cookie đúng cho tất cả subdomain. Khi dùng domain: :all, option này chỉ định cách parse domain để hiểu TLD. Với app.funkygames.co, set tld_length = 2 để Rails hiểu TLD là funkygames.co. Cấu hình cuối cùng:
Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all, tld_length: 2
Option
tld_lengthcủa session store khác vớiconfig.action_dispatch.tld_lengthtrước đó.
Viết Test cho Nhiều Subdomain
Vì route phụ thuộc subdomain, request spec/integration test sẽ lỗi 404 nếu test request không có subdomain đúng. Rails integration test cung cấp helper host! để set subdomain cho mọi request trong file test.
# Cấu hình subdomain trong Rails integration tests
setup do
host! 'dev.example.com'
end
# Cấu hình subdomain trong RSpec request specs
before do
host! 'dev.example.com'
end
Sau đó request sẽ được route đúng theo subdomain routing trong routes.rb. Lưu ý domain không quan trọng, chỉ subdomain đúng trong code test quan trọng.
Thiết lập Nhiều Subdomain Cục bộ cho Development
Có nhiều cách thiết lập subdomain local. Cách đơn giản là chỉnh file /etc/hosts.
127.0.0.1 dev.funkygames.local
127.0.0.1 app.funkygames.local
127.0.0.1 api.funkygames.local
Đảm bảo subdomain hoạt động local. Có thể dùng tool như pow để quản lý subdomain local.
Các Cạm bẫy với Constraint-based Subdomain Routing
Mặc dù constraint-based routing hoạt động hầu hết trường hợp, nhưng gây khó khăn trong một số tình huống.
Xử lý API Bên ngoài
Khi tích hợp API thứ ba, TLD local như .local hoặc .dev không được phép. Phải dùng tool như ngrok. Subdomain routing không hoạt động, cần whitelist một số route để truy cập qua ngrok.
Route Nằm Ngoài Subdomain Constraints
Một số route không thể đặt trong subdomain constraints. Ví dụ endpoint healthcheck hoặc ping. Nếu dùng load balancer trước Rails app, load balancer cần kiểm tra app lên/đồng đều. Endpoint healthcheck không thể dưới subdomain constraints vì load balancer thường không biết request host.
Thiếu Root Route
Rails có route root đặc biệt - route mặc định. Nếu không route nào khớp, root được dùng. Khi mọi route đều dưới subdomain, có thể không có root route nào. Một số gem phụ thuộc vào sự tồn tại của root route, cần thêm kiểm tra.
Kết luận
Trong bài này, chúng ta thiết lập Rails app nhiều subdomain với ít cấu hình. Cũng thấy cách setup subdomain local và các môi trường khác nhau, cùng mẹo viết test hiệu quả. Với hạ tầng Rails cung cấp, việc setup và test app nhiều subdomain trở nên dễ dàng.
P.S. Nếu bạn muốn đọc bài Ruby Magic ngay khi xuất bản, hãy đăng ký newsletter Ruby Magic để không bỏ lỡ bài nào!