SAAS yazılımlarında her müşteri için ayrı bir subdomain nasıl oluşturulur?

Bir süredir geliştirmekte olduğum yan proje için çözmem gereken basit bir problem vardı: Platform üzerinde hesap oluşturan her bir müşteriye kendilerine özel bir domain nasıl oluşturabilirdim?

Bir örnek verelim: Diyelim ki kişilere kendi bloglarını oluşturmalarını sağlayan medium benzeri bir platform geliştirdiniz. Bloglarını oluşturmak isteyen kişiler sisteme gelip kendi isimleri ile blog oluşturabiliyor ve bu bloglarını onlara özel bir domain ile servis edebiliyorlar. Örnek olarak blog oluşturmayı sağlayan platformun domaini example.com ise ve siz x ismi ile bir blog oluşturduysanız platform tarafından size otomatik olarak x.example.com olarak bir domain sağlanıyor. Çözmeye çalıştığımız problem kaba taslak bu şekilde. Dilerseniz çözümün detaylarına geçelim.

Ben kendi projemde domain provider olarak namecheap, server olarak digitalocean üzerinde bir linux droplet, web uygulamasını servis etmek için nginx ve projemde de Go programlama dilini kullandım. Bu yüzden örnek çözümü anlatırken bunlar üzerinden ilerleyeceğim. Şunu söylemekte yarar var. Bu problemin çözümünde kullandığımız servislere herhangi bir bağımlılığımız yok. Siz dilediğiniz servis sağlayıcıları kullanabilirsiz. Lafı fazla uzatmadan başlayalım.

İlk olarak web uygulamanızı host etmek için linux sunucunuz hazır olmalı. Linux sunucunuzda nginx kurulu değilse buradan yardım alarak nginx i kurabilirsiniz. Go ile yazılmış web uygulamasını host etmek için docker kullandım. O yüzden linux sunucunuza docker i buradaki adımları uygulayarak kurmanız gerekiyor.

Go ile yazılmış web uygulamamızda örnek senaryomuza göre şunları yapmamız gerekiyor. Diyelim ki kullanıcılardan bir tanesi platformunuzu kullanarak xxx ismi ile bir blog oluşturdu ve sistem bu kullanıcıya xxx.example.com domainini tahsis etti. Bu domaine request atıldığında, ilk olarak domainden kullanıcının verdiği ismi (örnek: xxx) parse edeceğiz. Ardından bu isimle ilişkili bir kullanıcı olup olmadığına bakacak ve eğer varsa kullanıcıya özel içeriği göstereceğiz. Aksi halde 404 dönerek işlemi sonlandıracağız. Basitçe bunu yapan kod aşağıdaki gibi olmalı:

Yukarıda neler yapıyoruz bir açıklayalım:

  • parseCustomerName fonksiyonu ile host parse edilerek customer name'i alıyoruz.
  • İçeriği boş olan existsCustomer fonksiyonunu verilen customer name parametresine göre customerın kayıtlı olup olmadığını anlamak için kullanıyoruz.
  • Asıl önemli olan kısım customerMiddleware methodu. Bu middleware ile hostu parse ederek alınan customer name'in kayıtlı olup olmadığına bakılıyor. Eğer kayıtlı değilse 404 dönülerek request sonlandırılıyor. Kayıtlı olduğu durumda da customer name verisi requestin context objesine set ediliyor. Bu şekilde diğer requestleri karşılayan http handlerlardan customer name verisine erişebiliyoruz.
  • indexHandler basit bir http handler. Bu bir örnek olduğu için database ile ilgili kodları implemente etmedim. Basitçe middleware tarafından set edilen customer verisine nasıl erişebileceğimizi gösteriyorum.
  • main fonksiyonunda indexHandler'i router'a register ediyoruz. Ardından router'i customer middleware ile kapsayarak http'nin ListenAndServe methoduna veriyoruz ve 80 portu üzerinden requestleri dinlemeye başlıyoruz.

Go programımız gördüğünüz gibi aslında bu kadar basit. Şimdi de bu kodu docker ile nasıl deploy edeceğimize bir bakalım:

Yukarıda ne yaptığımızın detayına fazla girmeyeceğim. Basitçe Go programımızı build etmek için gerekli scripti yazıyoruz ve container da 80 portunda çalışacak bir entrypoint oluşturuyoruz.

Dockerfile'da hazır olduğuna göre artık image'imizi build edebiliriz. Bunun için docker build komutunu çalıştırıyoruz:

docker build -t go-catchall-wildcard .

Docker image i linux sunucunuzda çalıştırmak için image'i dockerhub gibi bir repository'ye pushlayabilirsiniz. Sunucu tarafında aşağıdaki komutu kullanarak Go uygulamanızı ayağa kaldırabilirsiniz.

docker run -d --publish 5000:80 --restart=always go-catchall-wildcard:latest

Buraya kadar herşey tamamsa, Go uygulamamız linux sunucumuzda docker container üzerinde çalışıyor olmalı. Bu adımdan sonra sıra domain ile ilgili yönlendirme ve nginx üzerinde yapılması gereken ayarlara geliyor.

İlk olarak domainimizi satın aldığımız servis üzerinde bir takım yönlendirmeler yapmalıyız. Kullanıcıların kendi bloglarını oluşturmalarını sağlayan platformumuz example.com domaini üzerinde çalışıyordu. Kullanıcılar kendi isimleriyle bloglarını yarattıklarında xxx.example.com, yyy.example.com gibi kendilerine özel subdomainlere sahip oluyorlardı. Bu yüzden kullanıcılar xxx.example.com domainini tarayıcıya girdikleri zaman bizim bu subdomaini Go uygulamamızın çalıştığı sunucuya yönlendirmemiz gerekiyor. Platformumuzda binlerce kullanıcının olacağını düşünürsek kullanıcıya tahsis edilen xxx.example.com gibi her bir domain için manuel yönlendirme mi yapmamız gerekiyor?

Hayır tabiki. Bu tür manuellikler yapmayacağız. Bunun için domaini satın aldığımız servisin dns ayarlarında bir takım ayarlamalar yapmamız gerekiyor. Basitçe example.com domainimizin tüm subdomainlerini Go uygulamamızın çalıştığı sunucuya yönlendireceğiz. Bunun için domainimizin dns ayarlarına *.example.com şeklinde bir kayıt girmemiz gerekiyor. Bu kayıttaki wildcard(*) ile example.com root domainimize ait olan x.example.com, y.example.com gibi tüm subdomainleri tek bir adrese yani Go uygulamamızın çalıştığı sunucuya yönlendirmiş oluyoruz. Bu işlem catch-all (wildcard) olarak biliniyor.

Ben domaini namecheap servisi üzerinden aldığım için aşağıdaki resimde girdiğim wildcard kaydını görebilirsiniz. Eğer siz de namecheap kullanıyorsanız buradan detaylı yardım alabilirsiniz.

Bu kayıt ile yaptığım basitçe tüm adresleri (*) sunucu ipsine yönlendiriyorum. Bu ayardan sonra x.example.com gibi adreslerin hepsi sunucumuza yönlendirilecek ve bu requestleri ilk olarak nginx karşılayıp daha sonra proxy görevi görerek bunları docker container üzerinde çalışan Go uygulamamıza yönlendirecek.

Bu yüzden nginx tarafında da yönlendirilen domainlerden gelen requestleri karşılamak ve Go uygulamasına aktarmak için gerekli ayarları yapmamız gerekiyor. Bunun için linux sunucumuza login oluyoruz. Ardından aşağıdaki komutları çalıştırıyoruz.

Burada ne yapıyoruz:

  • İlk olarak /etc/nginx/sites-available/ lokasyonuna gidiyoruz. Burası nginx in server yapılandırmaları ile ilgili dosyaları tuttuğu yer.
  • İkinci satırda example.com.conf isminde bir konfigürasyon dosyası oluşturuyoruz ve ayarları yapmak için dosyayı açıyoruz.

Dosyanın içeriği aşağıdaki gibi olmalı:

Ayarları girdikten sonra kaydedip çıkıyoruz. Yaptıklarımızı bir açıklayalım:

Basitçe *.example.com kaydı ile bu sunucuya bu domain üzerinden gelecek olan tüm requestleri proxy_pass kısmında belirttiğimiz http://localhost:5000 adresinde çalışan Go uygulamamıza yönlendiriyoruz. Hatırlarsanız docker imageimizi kurarken 5000:80 olarak containerımızda port mapping yapmıştık. Container kendi içerisinde 80 portu üzerinden servis veriyor ve 5000 portu ile diyoruz ki, container dışından 5000 portu ile gelen tüm requestleri 80 portuna yönlendir. proxy_set_header komutu ile orginal requestteki header bilgilirini docker tarafına aktarıyoruz ki bu bilgilere Go uygulamamız tarafından erişebilelim. Son olarak aşağıdaki komut ile ayarları girmiş olduğumuz dosyayı /etc/nginx/sites-enabled/ lokasyonuna da kopyalıyoruz.

sudo ln -s /etc/nginx/sites-available/example.com.conf /etc/nginx/sites-enabled/example.com.conf

nginx -t komutunu çalıştırıp yaptığımız ayarların başarılı olup olmadığını kontrol ettikten sonra systemctl restart nginx komutu ile nginx'i restart ediyoruz. Tüm bunları yaptıktan sonra x.example.com olarak tarayıcıdan request attığınızda Go uygulamanızın cevabını görebiliyor olmalısınız.

Yapmamız gerekenler bu kadar. Bundan sonraki spesifik ayarlar sizin isteğinize kalmış. Örneğin *.example.com wildcard domaini için letsencrypt kullanarak requestlerinize ssl desteği sağlayabilirsiniz. Buradaki makale size yardımcı olacaktır. Source code için buradan github sayfasına gidebilirsiniz.

Kalın sağlıcakla.