Trendyol Tech
Published in

Trendyol Tech

Photo by Margot RICHARD on Unsplash

Dynamic Config&Secret Management Sürecimize Tamamen Farklı Bakış🔮👀

“Dynamic Config&Secret Management” çözümümüzü en basit haliyle hatırlatmak gerekirse, ekiplerin uygulamalarının sahip olduğu sensitive ve non-sensitive konfigürasyonlarını bir dosyaya çıkartmak ve bu dosyayı, değerlerin değişimine uygun şekilde güncel tutmak diyebiliriz. Eğer bu konudaki çözümümüzle ilgili yazıları henüz okumadıysanız sırasıyla aşağıdaki linkteki yazılara göz atmanızı tavsiye ederim. 😊

Başlıktaki “Tamamen Farklı Bakış” ile neyi kastettiğimizi açıklayacak olursak, bu yazımızın temel amacı mevcut çözümümüzü farklı şekilde tasarlasaydık bunu nasıl yapabilirdik diye beyin fırtınası yapmak ve aslında bahsedeceğimiz konuya benzer çözümü sunan araçları inceleyip bir de demo yapmak.🧠👨‍💻

Yazının girişinde de ifade ettiğim gibi bizim çözümümüz ekiplerin uygulamalarıyla filesystem üzerinden haberleşiyor ve biz bunu yaparken de consul-template aracından faydalanıyoruz.

Peki ya biz bu yapıyı uygulama başlamadan önce bu uygulamanın Consul veya Vault’a özgü yapıda tuttuğu environment variablelar var ise bu ifadelerin asıl değerlerinin Consul ve Vault’dan alındıktan sonra uygulamayı başlatacak bir yapıya dönüştürmek istesek, neler yapabilirdik? 🤔

Bu environment variablelara örnek verecek olursak aşağıdaki gibi bir yapı düşünebiliriz:

https://gist.github.com/developer-guy/92af7f6b01c881d574f3f83212b50974

Buradaki akla gelen ilk soru bu değerlerin neden bu şekilde ifade ettiğimiz olabilir. Aslında bunun cevabı olarak hangi environment variable için Consul’e hangi environment variable için ise Vault’a sorgu atacağımızı belirlemek diyebiliriz zaten benzer bir süreci var olan çözümümüzde yani “Dynamic Config & Secret Management” çözümünün “MutatingAdmissionWebhook” projesinde
Pod annotationlarından faydalanarak “inject-file-config-” prefixi ile Consul’den ve “inject-file-secret-” prefixi ile de Vault’dan değerleri alacağımızı anlamak için kullanıyoruz.

Şimdi bu serüvenimizde tecrübe ettiğimiz ve öğrenmeye çalıştığımız tüm bu konuları aşağıda listeleyecek olursak, böyle bir yapıyı tasarlarken de en çok nelere ihtiyaç duyabileceğimizi önden madde madde belirtmiş oluruz:

Dolayısıyla tüm bu kazanımların ışığında konuyu 2 farklı projeye ayırıp planlıyor olacağız. Bunlardan ilki geliştireceğimiz toolun otomatik olarak Pod’ların içerisine inject edilmesini sağlayan “MutatingAdmissionWebhook” projesi, hadi buna “inject-binary-mutater” adını verelim, diğeri ise bu toolun kendisi, buna da “vaucon-env” diyelim, tamam isimlendirme konusunda sınıfta kalmış olabiliriz, neyse devam edelim. 😅

⚠️ Konu başlangıçta oldukça soyut gelebilir ama yazının sonunda bir demo yaptığımızda her şeyin kafanızda oturacağından eminim, o yüzden yazının sonuna kadar okumayı ihmal etmeyin lütfen.

🧬 “inject-binary-mutater” umuzun sorumlulukları

  1. Consul’de de Vault’da da ACL (Access Control List) dediğimiz bir özellik mevcut. Bu özellik sayesinde servislerin veya userların yapabileceği işlemler üzerinde policy&token tabanlı yetkilendirme süreci uygulayabiliyoruz. Dolayısıyla bizim projemizin birincil sorumluluğu entegrasyonu aktifleştirilen Pod’lar için bu hizmeti otomatik sağlayabilmesidir. Yani ilgili Pod’un erişmek istediği Consul ve Vault üzerindeki değerleri okuyabilmesini sağlayacak policy’nin ve ardından bu policy’nin bağlandığı token’ın oluşturulması sürecini otomatik yönetmesi gerekiyor.
  2. Mevcut çözümümüz Init Container ve Sidecar Container olmak üzere iki adet container’ı inject ediyor ve bu containerlar consul-template’i temelde kullanan ona ek özellikler sağladığımız projemizin image’ını kullanıyor, init container ilgili Pod içerisindeki application container’ı ayağa kalkmadan önce bu containerın ihtiyaç duyduğu dosyaları hazırlamaktan sorumlu, sidecar container ise anlık Vault ve Consul üzerindeki konfigürasyon değişimlerinde dosyayı güncel tutmaktan sorumlu, fakat bizim şuan planladığımız çözümümüz için sadece init container’a ihtiyacımız var çünkü biz bir binary geliştiriyor olacağız ve bu binaryin sorumluluğu eğer bu container’ın kullandığı özel environment variable ifadeleri varsa bunların değerini Vault ve Consul üzerinden almak ve application containerinin ENTRYPOINT yani PID 1'deki process’ini exec ailesinden bir syscall ile başlatmak. Dolayısıyla bizim application containerı içerisine bir şekilde bu binaryi kopyalayabilmemiz lazım. Bunun için de sadece init container’e ihtiyacımız var ve bu container shared volume yardımıyla bu binaryi application containerının filesystemine taşıyabilir.
  3. Şimdi geldik müthiş bir detaya, yukarıda belirttiğimiz gibi bizim binarymiz application container’ının ENTRYPOINT’inde verilen process’i
    execve syscall ile başlatacak dedik, peki biz bu processi nasıl bilebiliriz?Aslında bunu bilebilmenin iki yolu var. İlki aşağıdaki gibi bize Pod Spec’i altında .container[*].command field’ı kullanılarak containerın ENTRYPOINT processi ezilmişse direkt bu fieldın değerini alıp kullanabiliriz, örneğin aşağıdaki Pod için:
https://gist.github.com/developer-guy/995d0166bbf1e93d3cc1a35e48078023

bizim binaryimizin “printenv” process’ini başlatacağını bilebiliriz, peki ya bu field ile beraber containerın ENTRYPOINT’i ezilmediyse ki genel pratikte biz bu değeri ezmeyiz, bu ENTRYPOINT’de tanımlanan process’e nasıl ulaşırdık ?

Bunun cevabına geçmeden önce bir konuya değinmekte fayda var: OCI.

OCI (Open Containers Initiative) container formatları ve runtimeları etrafındaki standartları belirlemeyi amaçlayan bir Linux Foundation projesi. Neden böyle bir şeye ihtiyaç duyuldu diyecek olursak, Docker ilk ortaya çıktığında tüm business ve teknik açıdan detaylara cevap veremediğinden bahsediliyor ve buna cevap olarak da community kendi container-runtime’larını geliştirmeye başladı. Bundan dolayı container savaşları adı altında değerlendirebileceğimiz bir dönem başladı. Bunun da en büyük tehlikelerinden birisi bu konuda kırılmalara/ayrışmalara sebebiyet verebilecek olmasıydı çünkü her yeni container-runtime farklı implementasyon detaylarına sahip ve belki konuştuğu image formatı veya runtime farklı. Dolayısıyla bu container dünyası etrafında belirli standartların oluşturulması, bu sayede tüm geliştirilen container-runtimeların ortak bir dili konuşması gerekliliği oluştu.

https://alibaba-cloud.medium.com/open-container-initiative-oci-specifications-375b96658f55

Open Container Initiative şuanda iki başlık altında tanımladığı standart var: Image Specification and Runtime Specification.

Image Specification, bir OCI Image’nin nasıl oluşturulacağını tanımlar ki bir OCI Image, image manifest, image index, fileystem layers, ve image configuration’dan oluşur.

⚠️ OCI Image Spec şuanda Docker Image Manifest Version 2.2 ye dayanıyor, dolayısıyla bir Docker Image’ni yukarıdaki göstermiş olduğum OCI formatında analiz etmek için skopeo adında bir araçtan faydalanabilirsiniz.

https://gist.github.com/developer-guy/0d075dcdd1ebfe6e5a7240f2416f6f99

Runtime Specification ise OCI Image Bundle’ından bir containerın nasıl başlatılacağını tanımlar.

⚠️ Eğer OCI Image formatından nasıl runtime-bundle oluşturabileceğinizi merak ediyorsanız, umoci ve oci-image-tool araçlarına bakabilirsiniz.

Bu konuya neden değindik derseniz eğer, biz bu OCI Image formatını iyi anlarsak, ilgili image için bu ENTRYPOINT veya CMD’deki process’i image’ın konfigürasyonundan bulabiliriz ve bu değeri bulmamıza yardımcı olacak bir çok araç var, skopeo, crane. Fakat programatik olarak Go kodu ile beraber en kolay go-containerregistry dediğimiz Google tarafından geliştirilen container registry’lerle çalışmamızı kolaylaştıran Go module olarak kullanabileceğimiz bir projeyle yapabiliriz. Hatta yakın zamanda Ahmet Alp Balkan’nın bu konudaki yazısı bize öncülük edebilir, bu yazının ışığında bir OCI Image’in konfigürasyonunu nasıl alabileceğimize dair şöyle ufak bir kod paylaşabilirim.

https://gist.github.com/developer-guy/e9bee872f145d2716b702152175c4f12

Dolayısıyla eğer ENTRYPOINT’de verilen container için Pod Spec içerisinde bu process ezilmemişse yukarıdaki örnek kod sayesinde ilgili image’nin ENTRYPOINT processini bulabiliriz. 💫

🧬 “vaucon-env” in sorumlulukları

  1. inject-binary-mutater projemizin sorumluluklarını anlatırken satır aralarında da vaucon-env projemizin de ne tür sorumluluklara sahip olması gerektiğinden bahsetmiştik. Vaucon-env projemizin başlıca görevi eğer application containerinin environment variableları arasında bizim beklediğimiz özel ifadelerde bir environment variable varsa (consul:// veya vault:// ile başlayan) bu ifadelerin asıl değerlerinin Consul ve Vault üzerinden alınıp bu environment variablelarının değerleriyle değiştirmek ve ardından application containerının PID 1 de yer alan yani ENTRYPOINT veya CMD direktifleriyle belirtilmiş processini execve syscall ile başlatmak diyebiliriz en temel haliyle.
  2. inject-binary-mutater sorumluklarındaki ilk maddeye bakacak olursak, “policy&token işlemlerinin otomatik bu container için sağlanması” diye bir ifade kullandık, dilerseniz bu konuyu biraz açalım. Consul tarafından policy&token elde etme süreci basitçe policynin oluşturulması ardından bu policynin bağlandığı tokenı üretmek şeklinde gerçekleşir diyebiliriz fakat iş Vault tarafında bu süreci kurgulamaya geldiğinde hayatımıza Auth Method adlı bir kavram giriyor. Bu Auth Method yardımıyla ilgili servise veya kullanıcıya bir identity ve bir takım policyler bağlayabiliyoruz, çözümümüz Kubernetes spesifik olduğundan dolayı servisimizin token elde etme sürecini Kubernetes Authentication Method kullanarak kurgulayabiliriz tıpkı bizim mevcut çözümümüzde yaptığımız gibi. Kubernetes Auth Method’un bizi identify edebilmek için Service Account kullanıyor, daha sonra bizden bir role oluşturmamızı ve bu role’un hangi Service Account hangi namespace ve hangi policylere bağlı olduğunu tanımlamamızı istiyor, işte tüm bu role&policy oluşturulma süreçlerini biz inject-binary-mutater de kurguluyabiliriz daha sonra da vaucon-env projemize de sadece login olup token elde edilme süreci kalır.

⚠️ Hatta bu Kubernetes Auth Method’un otomatik olarak cluster bazlı aktifleştirilmesi sürecini de biz webhook uygulamamız tarafında yönetiyoruz dilerseniz siz de böyle bir süreci kurgulayabilirsiniz, yani uygulama henüz başlarken üzerinde çalıştığı Kubernetes clusterı için bir Auth Method tanımlanmamışsa ilk işi bunu oluşturuyor diyebiliriz.

En temelde ve çok fazla teknik detayına derinlemesine inmeden aklıma gelen sorumluluklar bunlar. Fakat bu senaryo aslında yeni değil, bu duruma bazı mevcut araçlar ile çözümler getirilmiş, örneğin envconsul, vaultenv ve
bank-vaults.

Biz bugün aslında brainstorming yapmamıza öncülük eden BanzaiCloud tarafından geliştirilen bank-vaults’un vault-secrets-webhook ve vault-env projelerini kullanarak demo yapıyor olacağız 💪

👨‍💻 Demo

Demoyu Kubernetes ortamında çalıştıracağımızdan öncelikle bir Kubernetes clusterına ihtiyacımız var. Local ortamda Kubernetes clusterını çalıştırabilmek için başlıca bir araçlar var, Minikube, KinD, k0s, k3s, MicroK8s

Biz Minikube ile ilerliyor olacağız, dilerseniz demoya clusterımızı çalıştırmak ile başlayalım.

https://gist.github.com/developer-guy/3239e51c64ab31467736dedb75bab614

⚠️ Demomuzda Consul’e tarafına pek değinmiyor olacağız ama yukarıda bahsetmiş olduğum envconsul ile sizler de localinizde denemeler yapabiliriz, konsept tamamen aynı.

Sonraki işlem ise bir Vault clusterına ihtiyacımız var ve bunu Kubernetes ortamında çalıştırmamız lazım. Bunu yaparken de yine bank-vaults projesindeki Vault Operator’den faydalanıyor olacağız. Normalde Vault clusterını kurduktan sonra uygulamamız için bir policy, Kubernetes Auth Method konfirügasyonu ve ilgili role’un oluşturulup Service Account ve bu policy’nin birleştirilmesini sağlamamız gerekiyor fakat bu operatör yardımıyla bunları basitçe halledebiliyoruz.

⚠️ Kubernetes Operator kavramının ne olduğunu merak ediyorsanız, bu linkten detaylarına bakabilirsiniz.

Bu operatörü clusterımıza kurmak için BanzaiCloud bizim için bir Helm chart hazırlamış bu linkte oldukça faydalı bir çok çeşit Helm Chartlarını bulabilirsiniz.

https://gist.github.com/developer-guy/3150ea620fe74b1f7554d8bdc1119cdf

Şimdi buraya kadar, Helm Chart reposunun local repolar altına ekledik, gerekli namespaceleri oluşturduk ve Vault Operator’ü kurduk. Gördüğünüz üzere artık Vault’u Kubernetes clusterına sanki Kubernetes’e ait bir resourcemuş gibi oluşturabiliriz.

https://gist.github.com/developer-guy/a5d7cd14526adcec66304c354709661d

Bu YAML dosyasını inceleyecek olursanız, çok da detaylarda kaybolmadan, demo için en önemli iki kısım olan externalConfig altındaki auth ve policies ‘e bakabilirsiniz. Bu kısımlarda bazı policylerin , Kubernetes Auth Methodunun oluşturulduğunu ve bu Auth Method için bazı role’lerin de oluşturulduğu, bazı policy ve Service Account’un bu role bağlandığına göreceksiniz.

YAML dosyasını apply edelim.

https://gist.github.com/developer-guy/c6ad80ed512833544939dbad65ace0ac

Her şey beklediğimiz gibi çalıştığını teyit etmek adına vault CLI aracını localimize kurup ardından gerekli kontrolleri yapalım. Eğer siz de Mac kullanıyorsanız brew paket yöneticisiyle vault CLI localinize
$ brew install vault diyerek kurabiliriz.

https://gist.github.com/developer-guy/eb25cc71db2fd756fd10dff58fdfcaa8

Her şeyin istediğimiz gibi çalıştığını teyit ettikten sonra, bir sonraki adım artık webhook projesinin deploy edilmesine geldi, bunu da yine Helm Chart’ı yardımıyla yapabiliriz.

https://gist.github.com/developer-guy/9033a0266218929a3e8e2a5c9f8c5234

Şimdi tüm gerekli ön kurulumları yaptık ve geldik en güzel bölüme, vault-env adlı binaryimizin otomatik olarak application containera kopyalanması ve application containerımızın processini başlatmasına.

https://gist.github.com/developer-guy/88e8a3756f7b3317f4db29f13520085d

Şimdi burada gördüğünüz gibi alpine containerımız AWS_SECRET_ACCESS_KEY adında bir environment variable kullanıyor ve değeri ise vault:secret/data/accounts/aws#AWS_SECRET_ACCESS_KEY şekllinde belirtilmiş, dolayısıyla bizim binaryimizin bunu Vault’dan çözmesi ve “command” field’ında belirtilen ENTRYPOINT processini başlatması gerekiyor.

Bu manifesti clustera deploy ettiğimizde webhookumuz bu requesti yakalayıp araya girip Pod’un içerisine bir adet init container ekleyecek daha sonra bu init container bir shared volume yardımıyla application container içerisine binaryi kopyalayacak ve application containerininin ENTRYPOINT’ini bu binaryi kullanacak şekilde ayarlayacak.

https://gist.github.com/developer-guy/0ed958769490733564e448e63f57052d

Peki gerçekten ne oldu diyecek olursak, deploy edildikten sonra Pod specini kontrol edelim.

https://gist.github.com/developer-guy/63dfef98261e965b1c0edb0f55398b78

Gördüğünüz gibi 55. satırda bir init container eklendi Pod’umuz içerisine ve görevi de vault-env adlı binary’i shared volume ile application container fileysystemine iletmek, ardından 81.satırda Pod’umuzun command’ı artık vault-env olduğunu görmekteyiz ve bu argüman olarak da Pod’umuzun var olan ENTRYPOINT processini almış durumda, tıpkı konuştuğumuz gibi işleyen bir senaryo var diyebiliriz. 🎉🎉🎉

📝 Özet

Kısaca, bu gibi bir yapıyla beraber biz uygulamalarımızın daha güvenli şekilde konfigürasyonlarını ulaşmalarını sağlayabiliyoruz çünkü direkt olarak Vault veya Consul’den alınan değerlerin Pod’un processine bir environment variable olarak geçilmesini sağlıyoruz.

👀 Referanslar

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
developer-guy

developer-guy

I do mostly Go, Kubernetes, and cloud-native stuff ⛵️🐰🐳