Next.js Standalone'u systemd ve nginx ile Bir VPS'e Dağıtmak
Daha küçük projelerimin çoğunu, yönetilen (managed) bir platform yerine tek bir Ubuntu droplet'inde barındırıyorum. Bu siteyi de. Kurulumu birine her tarif ettiğimde, ilk tepki, sadece yönetilen bir host kullanıp kendime eziyet etmeyi bırakmam yönünde kibar bir öneri oluyor. Adil bir nokta. Ama VPS'e geri dönüp duruyorum ve inatçılıktan değil. Dağıtıma sahip olmak bana bir şeyler öğretiyor, neredeyse hiçbir şeye mal olmuyor ve gerçekten kullandığım bir denetim seviyesi veriyor. İşte tüm bunun nasıl bir araya geldiği ve ödünleşimlerin dürüstçe nerede ısırdığı.
Tüm Numara Standalone Çıktı
Bunu keyifli kılan parça, Next.js standalone çıktısı. Yapılandırmada çıktıyı standalone'a ayarladığınızda, next build minimal bir node sunucusu ve uygulamanın çalışma zamanında gerçekten ihtiyaç duyduğu bağımlılıklarla kendi kendine yeten bir klasör üretir. Gönderilecek devasa bir node_modules yok, sunucuda dünyayı kurmak yok. Derleme, plain node ile çalıştırdığınız bir sunucu dosyası, bir static klasörü ve varlıklar için public klasörü yayar.
O tek ayar, dağıtımın şeklini tamamen değiştiriyor. Sunucuyu, uygulamayı yeniden derlediğim bir yer olarak ele almak yerine, bitmiş bir yapıyı (artifact) bıraktığım ve başlattığım bir yer olarak ele alıyorum. Derleme, tam araç zincirine sahip olduğum yerel makinede veya CI'da gerçekleşir. Sunucu yalnızca zaten derlenmiş bir paket karşısında node çalıştırır. Derlenmiş bir ikili göndermek ile kaynak artı bir derleyici göndermek arasındaki fark.
Site Başına Bir systemd Birimi (Unit)
Droplet'teki her uygulama kendi systemd servisidir. Birim, kasıtlı olarak sıradan: uygulama klasörüne ayarlanmış bir çalışma diziniyle node server.js çalıştırır, 3000 gibi sabit bir port, birkaç ortam değişkeni ve süreç ölürse veya makine yeniden başlatılırsa geri gelmesi için bir yeniden başlatma politikası. Farklı siteler farklı portlar alır.
systemd'yi burada sevmemin nedeni, uzun süre çalışan bir node sürecini işletim sisteminin gerçekten yönettiği bir şeye dönüştürmesi. Başlatma, durdurma, yeniden başlatma, durum ve journalctl üzerinden logları bedavaya alırım. Bir çekirdek güncellemesinden sonra kutu yeniden başlarsa, her site ben hiçbir şeye dokunmadan geri gelir. Dadılık edilecek bir süreç yöneticisi katmanı yok, kurulup canlı tutulacak ayrı bir gözetmen (supervisor) yok. Makineyi zaten çalıştıran init sistemi, bir port üzerindeki bir node süreci için fazlasıyla yeterli.
Önde nginx, Sıkıcı Kısımları Yaparak
Genel internetten hiçbir şey doğrudan node ile konuşmuyor. nginx, önde bir ters proxy olarak oturuyor ve node'un zaman harcamaması gereken kısımları ele alıyor. TLS'i sonlandırıyor; böylece sertifikalar ve yenileme, her uygulamanın içinde değil, tek bir yerde yaşıyor. Yanıtları gzip'liyor. Ve static yol altındaki değişmez varlıklara uzun önbellek başlıkları ayarlıyor çünkü o dosya adları içerik karması (content-hash) içerir ve asla değişmeyecektir. Tarayıcı onları etkili biçimde sonsuza kadar önbelleğe alabilir.
Her site, kendi yukarı akış (upstream) portuna işaret eden ayrı bir nginx vhost'u. Yani tek bir droplet, her biri kendi yapılandırma bloğuyla bağımsız bir servis olarak, birkaç ilgisiz alan adına sessizce hizmet ediyor. Proxy katmanı ayrıca çapraz kesen herhangi bir şey için doğal bir dikiş veriyor: bir güvenlik başlığı, bir yönlendirme, bir bakım sayfası. Bir başlığı değiştirmek için bir uygulamayı yeniden dağıtmaktansa bir nginx direktifini ayarlamayı tercih ederim.
Yeniden-Sağlama Değil, Yapı (Artifact) Dağıtımları
En çok önemsediğim kısım bu. Bir dağıtım, ortamın tam bir yeniden kurulumu değildir. Bir dağıtım şudur: yeni derlenmiş standalone klasörünü, static klasörünü ve public klasörünü rsync ile sunucuya yükle, sonra o tek systemd servisini yeniden başlat. Tüm işlem bu. Saniyeler sürer ve neyin ters gidebileceğinin yüzey alanı küçücüktür.
Bir dağıtımın kasıtlı olarak yapmadığı şey, nginx yapılandırmasına veya systemd birimine dokunmaktır. Bunlar, makinenin bilinen-iyi durumunun bir parçasıdır ve nadiren ve bilinçli olarak değişirler. Her tek push'ta çalışan altyapı yapılandırmasını ezmek, rutin bir dağıtımı bir şans oyununa çevirmenin yoludur. Bu yüzden iki katmanı temiz biçimde ayrı tutuyorum. Uygulama yapısı sürekli çalkalanır ve rsync ile üzerine yazılır. Altyapı yapılandırması bir kez sağlanır (provisioned), gerçekten değişmesi gerektiğinde elle düzenlenir ve aksi takdirde rahat bırakılır. Bir şey bozulduğunda, neredeyse her zaman hangi katmana bakacağımı bilirim çünkü yalnızca biri hareket etti.
Neyden Vazgeçiyorsunuz
Diğer taraf konusunda dürüst olmak istiyorum. Yönetilen bir platform size gerçek şeyler satın alır. Anında geri alma (rollback) ile atomik dağıtımlar. Pull request başına önizleme dağıtımları. Varlıklarınızı kullanıcılara yakın koyan küresel bir kenar (edge) ağı. Trafik sıçradığında otomatik ölçeklendirme. Yeni olan hazır olana kadar eskinin hizmet verdiği sıfır kesintili geçiş. Droplet'imde, bir yeniden başlatma, node açılırken kısa bir kesinti demek. Bir makine, tek bir bölge ve devrilirse üzerindeki her şey birlikte devrilir.
Kişisel bir site ve bir avuç yan proje için, bunların hiçbiri ölümcül değil. Trafik mütevazı, dağıtımdaki birkaç saniyelik kesinti görünmez ve tek bölge yeterli. Ama bunun gerçek kullanıcıları ve bir nöbet (on-call) rotasyonu olan bir ürün için doğru cevap olduğunu iddia etmem. Yine de çalıştırmamın nedeni, görünürlüğün gerçekten faydalı olması. Gerçek nginx erişim loglarını okuyabilir, bir dağıtım sırasında journalctl'i izleyebilir ve istek ile yanıt arasındaki her katman hakkında akıl yürütebilirim. O anlayış, riskin daha yüksek ve platformun tüm bunları görünmez biçimde yaptığı projelere benimle birlikte gelir. Neyin soyutlanıp uzaklaştırıldığını bilmek, ara sıra onu elle yapmanın rahatsızlığına değer.