Her şey as Code — Bir CI/CD Hikayesi

Mustafa Sadedil
SabancıDx
Published in
10 min readDec 29, 2019

Bugün, SabancıDx’teki yeni projemizde kullanmaya başladığımız Continuous Integration / Deployment süreçlerinden bahsedeceğim. Tek bir aracın nasıl kullanıldığını anlatmaktan ziyade, gidiş yollarımız ve karar mekanizmalarımız üzerine serbest formatta bir yazı olmasını planlıyorum. Kod bloklarına yer vermeden, sadece konunun hikaye kısmına odaklanacağım.

Hemen, değineceğim konuları ve ne kadar derinleşeceğimizi anlatan içerik listesini aşağıya bırakayım.

İçindekiler:

  • Azure DevOps / Azure Pipelines | %45
  • Infrastructure as Code (IaC) / Pipeline as Code | %25
  • Continuous (Integration / Deployment / Testing) | %18
  • YAML | %7
  • Docker / Kubernetes (K8s) | %4
  • Test Driven Development (TDD) | %1

Hadi başlayalım…

1/4 — Bazı “continuous” şeyler

Çok değil bundan 13 yıl önce, ilk profesyonel iş hayatıma başladığım yazılım evinde herhangi bir Source Control sistemi kullanılmıyordu. Birkaç yazılımcının bir arada çalıştığı projelerde, ürettiğimiz modülü bir .dll halinde derleyip, ortak ağdan erişilebilen bir klasöre kopyalayıp, diğer yazılımcının kullanımına sunuyorduk¹. Aynı kod parçacığı üzerinde nasıl çalıştığımızı ise anlatmak bile istemiyorum 💆🏻‍♂️.

Bu tarz verimsiz bir yöntemi, o zamanlardaki bilgi ve teknolojinin geldiği seviyeye bakıp mazur görmek kolay bir çözüm olsa da tahmin edebileceğiniz gibi çok da makul değil.

Günümüzde halen birçok kişiye yabancı gelen Test Driven Development (TDD) ile ilgili ilk fikirlerin 1957'ye dayandığını düşünürsek, iyi yönetemediğimiz süreçlere “o zamanlar öyleydi” tarzı bahaneler yakıştırmanın hafifletici bir sebep sayılmayacağını anlayabiliriz.

Severek takip ettiğim Lemi Hoca’nın TDD hakkındaki görüşleri

Bu arada, birkaç yıl öncesine kadar TDD’yi sadece Test First bir yazılım geliştirme yöntemi olarak düşünüyordum. Halbuki TDD’de testler, tasarımın kendisini oluşturan ana parçalar olarak konumlandırılıyor.

Asıl konumuza dönecek olursak, yıllardır nasıl Source Control sistemlerinin gerekliliğini tartışmıyorsak, bugün de artık otomatize edilmemiş build ve deployment süreçlerinin varlığına şaşırdığımız bir dönem “geldi” ve “geçiyor”.

  • Geldi: CI/CD araçları uzun zamandır erişebileceğimiz bir mesafede duruyor ve birçok insan yıllardır bunlardan yararlanıyor. Bu araçları kullanmamak için duyduğumuz birçok açıklama, aslında basit önyargılardan öteye gidemiyor.
  • Geçiyor: Bu araçları kullanmak bir yana, “daha fazla nasıl verim edebiliriz?” gibi sorgulamalar sonucunda, kaynak kodumuzu birinci sınıf vatandaş olarak ele almanın önemini anladık ve “<bir şey> as code” yaklaşımını hayatımıza soktuk.

2/4 — Sözlük

Bu bölümde, yazı içerisinde kullandığım birçok kavram için kısa tanımlamalar yapmaya çalıştım. Okumaya direkt 3. kısımdan devam edebileceğiniz gibi, aşağıdaki tanımlara kısaca göz atmayı da tercih edebilirsiniz.

Tanım-1 | Azure DevOps: Microsoft’un yazılım geliştiren ekipler için sunduğu birçok hizmeti içerisinde barındıran ürün ailesi. İçerisinde Source Control, Agile Boards, Pipelines, Test Plans gibi birden çok hizmeti barındırıyor. Açık kaynak kodlu bir ürününüz varsa ücretsiz sürümü ile bu yazıda anlatılan her şeyi yapabilirsiniz.

Tanım-2 | Azure Pipelines: Azure DevOps ürün ailesi içerisinde bulunan, Continuous Integration / Deployment süreçlerini yönetebileceğiniz bir ürün. Bugün kendisinden çokça bahsedeceğiz.

Tanım-3 | Continuous Integration (CI): Sürekli entegrasyon. Yazılan kodun düzenli olarak diğer yazılımcıların da kodlarını gönderdiği ortak repository ile birleşme sürecidir. ThoughtWorks bu yazısında o kadar övmüş ki, biraz uğraşarak — yazılımdan anlamayan — babanıza bile bir CI ürünü satabilirsiniz.

Tanım-4 | Continuous Deployment (CD): Tam olarak aynı şeye işaret etmese de Continuous Delivery olarak da bilinir. Otomatik testleri geçen her kodun bir insana ihtiyaç duymadan üretim (production, live, canlı, vs.) ortamına alınmasını anlatır. Birçoğumuz için korkutucu gözükse de yazılan testlerinin kapsamının ve kalitesinin arttırılması ile ulaşılamayacak bir hedef değildir.

CI/CD Pipeline denildiğinde aklımızda şu görüntü oluşsa yeterli (soldan sağa doğru bir akış var)

Tanım-5 | YAML: Açılımı YAML Ain’t Markup Language olan, XML ya da JSON gibi belli bir standarda sahip bir dosya formatı. Diğerlerine göre en büyük farkı, indent (soldan boşluk sayısı) bazlı bir formatlama standardı kullandığı için, <>, {}, [] gibi birçok karakteri kullanmadan yapısal (structured) bir dosya oluşturabilmeyi sağlar. (Ancak beraberinde başka türlü baş ağrıları getirir 🤦‍)

Örnek bir YAML dosyası (ve en sevdiğim filmlerden birisi Memento ❤️)

Tanım-6 | <bir şey> as code: Bu yazının ana temasını da oluşturan kavram. Basitçe, uygulamanın kaynak kodu haricindeki varlıklarımızın da kaynak kodu içerisinde saklanması yöntemidir. Bu başlığın altını, yukarıdaki tanımlara göre biraz daha fazla doldurmak istiyorum. Hemen bir örnekle “as code = kod olarak” mantığını açıklamaya çalışayım:

Örneğin bir web siteniz var ve kaynak kodların derlendikten sonra çalışması için bir web sunucuya (Nginx), bir veritabanına (PostgreSQL) ve bir de önbellek mekanizmasına (Redis) ihtiyaç duyuyor diyelim.

Bu web sitenizin kodunu derleyip oluşan paketi bana verseniz bile, altyapı bileşenlerini oluşturmak için tek tek uğraşmak zorunda kalırım (kendi başıma bir Nginx, PostgreSQL ve Redis ayağa kaldırmam gerekir). Halbuki bu web sitesinin kodlarının içerisinde, altyapı ile alakalı bilgileri barındıran bir başka dosya (XML, JSON ya da YAML farketmez) daha olsa, ben bu dosya formatını tanıyan herhangi bir sistemi kullanarak bu web sitesini çok daha az efor ile çalıştırabilirim. Burada bahsettiğim hikaye tam bir Infrastructure as Code (IaC) örneği oluyor.

Bunu bir sonraki seviyeye taşıyacak olursak; bana sitenin derlenmiş bir paketi yerine, bir adım daha geriye giderek derlenmemiş kaynak kodunun kendisini de gönderebilirsiniz. Kaynak kodun içerisinde “bu kodun nasıl derleneceğini tanımlayan” bir dosya da mevcutsa, CI/CD aracıma bu dosyayı göstererek, tam da sizin belirlediğiniz özellikler ile derlenmesini sağlayabilirim. Bu ise tipik bir Pipeline as Code örneği.

Aşağıda, CI/CD sürecini nasıl kurguladık? başığında bu konuya biraz daha derinlemesine gireceğiz.

3/4 — Karar mekanizmalarımız

Sıklıkla kullandığım kavramları da özetlediğimize göre, son altı aydır neler yaptığımızdan, ürün ve yöntem seçimindeki karar mekanizmalarımızdan ve yaşadığımızı sorunlar ile nasıl başa çıktığımzdan bahsedebilirim.

SabancıDx’te, beşi yeni işe başlayan toplamda altı yazılım geliştirici ve Product Owner’dan oluşan, SpaceDx🚀 isimli bir Agile takımı kurduk. Bu arada bahsetmeden geçemeyeceğim, kurduğumuz takım tamamen farklı yetenek gruplarından oluşan, tam anlamıyla cross-functional² bir takım oldu.

İlk hedefimiz, mevcuttaki İnsan Kaynakları ürünümüzün Performans modülünü güncel metodolojileri kullanarak baştan yazmaktı. Burada güncel metodolojiler kullanma kısmını bir boş bir hype³ olarak düşünmeyin. Amacımızı, yeni olan her şeyi kullanmaktan ziyade, yazılım geliştirme süreçlerini optimize etmek ve dolayısıyla hem ürünün kalitesini arttırmak hem de geliştiricileri tatmin etmek olarak belirledik.

Hedefimize paralel olarak şu basit kriterleri ciddiye aldık:

  • Ürün test edilebilir olmalı
  • Ölçeklenebilmeli
  • Otomatize edilebilecek her adım otomatize edilmeli (bu yazının konusu)
  • Teknik borçlar ürünü raydan çıkarmamalı, takip edilebilmeli

Tahmin edebileceğiniz gibi buradaki her adım, SDLC⁴’nin farklı bacaklarını ayrı ayrı güçlendirmekten geçiyor. Örneğin sadece test edilebilirlik konusunu ele aldığımızda bile; encapsulation, dependency inversion, loose coupling, mocking gibi birçok alt konuya eğilmek gerekiyor.

Şimdi “otomatize edilebilecek her adım otomatize edilmeli” felsefemize bağlı olarak “build, test ve deploy adımlarının otomatize edilmesi” için verdiğimiz kararlardan bahsetmek istiyorum.

Karar-1 | Neden Azure DevOps?

Geliştirme sürecinin başında, hem source control, hem scrum board, hem de CI/CD pipeline gibi hizmetlere ihtiyacımız vardı ve bunların tümünü kapsayan Azure DevOps da incelediğimiz adaylar arasındaydı. Bazı takım üyelerinin bu ürün ile ilgili geçmiş tecrübeleri de olduğu için, hızlı bir başlangıç yapabilmek adına takım olarak bunu seçme kararı aldık. Benzer şekilde Atlassian ürün ailesini de seçebilirdik.

(Kim bilir, belki yıllardır .NET platformunda Microsoft ürünlerini kullanarak yazılım geliştiriyor olmanın da bu tercihte bir etkisi olmuş olabilir 🤔)

Karar-2 | Neden Azure DevOps Multi-Stage YAML Pipelines?

Soruyu biraz açmak gerekirse, Azure DevOps uzun yıllardır hizmet veren bir servis. İçerisinde oldukça stabil hale gelmiş olan Build ve Release pipeline’ları mevcut (bundan Azure DevOps Classic Pipelines olarak bahsedeceğim). Ancak bu stabil yapı dururken, neden gidip halen önizleme aşamasında olan Multi-Stage YAML Pipelines’ı tercih ettik?

Artılarını ve eksilerini açıklamaya çalışayım.

Kullandığımız Multi-Stage YAML Pipeline’dan bir kesit

Artıları:

  • CI/CD tanımlarının source control sisteminde bir dosya içerisinde saklanabiliyor oluşu (pipeline as code)
  • Geçmişte yapılmış tüm değişiklikleri görüntüleyebilmek (git log)
  • CI/CD adımlarında yapılan değişiklikleri de code review sürecinde inceleyebiliyor olmak
  • Microsoft’un, piyasadaki rakiplerinin gerisinde kalmamak için bu yöne doğru yatırım yapacağına inanmak (kişisel hisler içerebilir)

Eksileri:

  • Henüz Azure DevOps Classic Pipelines kadar fazla özellik barındırmıyor oluşu (örneğin Production ortamına yapılacak deployment’in, birileri tarafından onaylanabilmesi özelliği bir iki ay önce geldi)
  • İlk öğrenme süreci (learning curve) bakımından, YAML dosyasına elle bir şeyler eklemenin, hazır bir UI kullanmaktan daha zor oluşu
  • Önizleme aşamasında olmasından kaynaklı, bazı özelliklerin hatalı çalışması (son birkaç ayım Azure Developer Community ya da benzeri sitelerde cevaplar arayarak geçti: 1, 2, 3 🤒)

Karar-3 | Ürünümüzdeki teknoloji seçimlerimiz nelerdi?

Her birisini madde madde yazmak yerine, fikir verebilmek açısından şu şekilde bir tag listesi vermek yeterli diye düşünüyorum:

.NET Core, Entity Framework (Code First), PostgreSQL, IdentityServer4, Redis, Elastic Search, Docker, Kubernetes, Azure, Vue.js, Buefy, TypeScript, Webpack, …

Karar-4 | CI/CD sürecini nasıl kurguladık?

Yazılan kodların master branch’e ulaşmasıyla başlayan süreci kabaca aşağıdaki gibi özetleyebilirim.

  1. Kodlar yazılımcı tarafından source control’e gönderilir
  2. Azure Pipelines, Docker imajlarını oluşturur, test otomasyonlarını tetiker ve oluşan imajları Container Registry’ye gönderir (push)
  3. Azure Pipelines, Kubernetes’e YAML (manifest) dosyalarını iletir
  4. Kubernetes, Container Registry’den ihtiyacı olan imajları çeker ve apply eder, böylelikle uygulama güncellenmiş olur
Kullandığımız CI/CD pipeline süreci

Karar-5 | Testleri nasıl “continuous” bir şekilde sürece dahil ettik?

“Ürün test edilebilir olmalı” mottomuzun ardından yazdığımız Unit ve Integration testlerinin, CI/CD Pipeline içerisinde çalıştırıp raporlanması ihtiyacı doğdu. Azure Pipelines, test otomasyonu ile ilgili ön tanımlı görevler içermesine rağmen, uygulamamızın derleme aşaması direkt olarak Pipeline üzerinde olmadığından (derleme işlem Docker üzerinde gerçekleşiyor) bu hazır görevlerden yararlanamadık.

Yazdığımız bütün kodlar, Dockerfile dosyasında yazan talimatlara göre bir Docker context’i içerisinde derleniyordu. Yani Pipeline, derleme işleminden direkt olarak sorumlu değil, yalnızca docker build komutunu tetiklemekle yükümlüydü. Sırf testleri çalıştırabilmek için, zaten Docker içerisinde derlenen kodları, bir kez de Pipeline üzerinde derlemek çok da anlamlı olmadığı için testlerin taşınabilirliğini de göze alarak şu yöntemi izledik:

  • Pipeline’da docker run ile boş bir PostgreSQL Container’i çalıştırılır
  • Entegrasyon testleri, az önce oluşturduğumuz boş bir veritabanında EF Core Migration’larını çalıştırarak gerekli tabloları oluşturur ve uygulamanın ihtiyacı olan temel verileri seed eder
  • Testleri tetikleyen dotnet test komutu Dockerfile içerisinde bulunur kodlar derlendikten sonra çalıştırılır
  • Test sonuçları, Docker imajı içerisinde .trx uzantılı bir dosyaya yazılır
  • Azure Pipelines’da bir sonraki adım olarak, az önce oluşturulan Docker imajından docker create ile bir container oluşturulur ve içerisindeki test sonuçları docker cp komutu ile Pipeline’ın erişebileceği bir yere kopyalanır
  • Özet sayfasında bu test sonuçlarını görüntüleyebilmek (ve testler başarısız olduğunda derleme işlemini bu noktada sonlandırabilmek) için Azure Pipelines’daki PublishTestResults task’ı ile elimizdeki test sonuçları yayınlanır
Azure Pipelines'da, PublishTestResults sonrası gözükmeye başlayan Tests özet sayfası

… ve continuous testing sürecimiz böylelikle tamamlanmış oldu.

4/4 — Yaşadığımız zorluklar

Okuduğum teknik yazılardan en çok yararlandığım kısım genellikle hangi problemleri yaşadıkları ve nasıl çözdüklerini anlattıkları kısımlar oluyor.

Yakın zamanda yaşadığımız, Kubernetes CronJob’larının neden olduğu kaynak sıkıntısı problemini, Zalando’nun Kubernetes on AWS at Zalando: Failures & Learnings sunumundaki tavsiyelerini uygulayarak çözdük.

Bu yüzden ben de yaşadığımız zorluklardan birkaçını paylaşmak istiyorum.

Zorluk-1 | YAML Pipelines’ın önizleme sürümünde olması

Sadece Pipeline Task’ları için açılan Issue sayısı

Azure DevOps Multi-Stage YAML Pipelines ile Haziran 2019'da başlayan yolculuğumuzdan beri, ürün sürekli gelişim göstermeye devam etti. Bu gelişim tam da beklediğimiz hızda olmadı. Bazı özellikler, ihtiyacımız olduğu zaman ile ucu ucuna yetişti. Bazı bug’lar birkaç tutam saç telimize mal oldu 😢. Yine de YAML Pipelines ile yola devam etmiş olmaktan pişman değiliz. Umarım yakın zamanda Preview sürümden kurtulup daha stabil ve bol özellikli bir ürün ile karşılaşırız.

Bu arada sürekli geliştirilen Azure DevOps’ın Release notlarını takip etmek gibi yeni hobiler kazandım. Bu yazıyı yazdığım dönem itibariyle Azure DevOps 162. Sprint notları yayınlanmıştı.

Zorluk-2 | Türkiye dışındaki Cloud Provider’ların kullanımı

Ürünü ilk kodlamaya başladığımızda, “her şeyi buluta koyarız kafamız rahat olur” hayalleriyle ilerlesek de kişisel verilerin Türkiye sınırları dışındaki ortamlarda saklanmasının kanuni yaptırımları ve bu yaptırımların SaaS bir ürünün müşteri portföyünü etkileyecek olması gibi konular yüzünden On-Premise’e dönüş yaşadık.

Son geldiğimiz noktada Dev ve Test ortamları Azure Kubernetes Service (AKS) üzerinde koşarken PreProd ve Production ortamlarını On-Premise bir Kubernetes Cluster’ı üzerine kurmuş olduk.

Tüm build ve deployment süreçlerimizin kod olarak saklanıyor oluşu bu noktada bir kez daha hayatımızı kurtardı.

Zorluk-3 | Azure DevOps ile On-Prem K8s Cluster arasındaki erişim

Bir üstteki soruna bağlı olarak Azure DevOps’ta tetiklediğimiz bir Pipeline’ın son iki adımda kendi veri merkezimizde bulunan Kubernetes’e erişmesi ihtiyacı doğdu. Bunu, K8s API Server’ı public olarak erişime açmadan halledebilmek istiyorduk.

Bir iki haftalık araştırma ve koşuşturmacanın ardından Azure’da bir sanal makine ayağa kaldırıp, Azure DevOps Agent olarak konumlandırdık. Bu sanal makine ile bizim veri merkezimiz arasında bir site-to-site VPN kurup internal erişimi sağladık. Son olarak da Pipeline için hazırladığımız YAML dosyalarında, PreProd ve Production adımları için kendi Agent’larımızın kullanmasını sağlayarak bu sorunu da aştık.

On-Premise Azure DevOps Agent —Arka planda PreProd ve Production ortamlarına deploy yaparken

Böylelikle Dev ve Test ortamlarına nasıl deployment yapılıyorsa PreProd ve Production ortamlarına da tam olarak aynı şekilde deployment yapılmasını garantilemiş olduk.

Sonuç

Özetle, kolay geliştirilebilir olma ve ölçeklenebilirliği hedefleyerek çıktığımız bu yolda, çiçeği burnunda teknolojiler ile çalışmanın keyfini ve sorunlarını yaşayarak geçirdiğimiz altı ay, tüm takımın kendisine yeni yetkinlikler kattığı bir sürece dönüştü.

Yaklaşık iki yıl önce, çok uzun süreler çalıştığım eski şirketimden (ve konfor alanımdan) ayrılıp yeniliklere yelken açmamın faydalarını sonuna kadar hissetiğim bir dönem geçirdim. Bu vesile ile, bu ortamın oluşmasını sağlayan SabancıDx’e, yöneticilerime ve SpaceDx🚀 takım arkadaşlarıma teşekkür etmek istiyorum.

Sonraki yazılarda görüşmek dileğiyle. Hoşçakalın.

Kaynakça

Notlar

  • 1) Bu işleyişten rahatsız olup, kısa süre sonra patronumuzu Microsoft Visual SourceSafe versiyon kontrol sistemine taşınmaya ikna etmiştik.
  • 2) cross-functional: Her yetenek setinden kişileri içeren takımları tarif etmek için kullanılan tabir. Cross functional takımlar — kabaca — kendi başlarına bir ürün geliştirebilecek yetkinliğe sahip olmalıdır diyebiliriz.
  • 3) hype: Birebir Türkçe çevirisi tam oturmasa da, gaza gelip, bir kavramın altını doldurmadan peşinden koşmak olarak yorumlayabiliriz.
  • 4) SDLC: Software Development Life Cycle. Yazılım Geliştirme Yaşam Döngüsü. Yani yazılım geliştirme sürecinin bütün aşamalarını birden tarifleyen kısaltma.

--

--