Kariyerimde zaman zaman sistem yöneticisi rolünde olduğumdan olsa gerek DevOps işleri hep eğlenceli geliyor. Arada bir yazılım firmalarına yolum düşünce lafı açılır hep Continuous Integration süreçlerini nasıl tasarlıyorsunuz diye. O nedenle temel bir CI Pipeline’ı nasıl tasarlayıp yazma gereği hissettim.
Öncelike CI için tool seçmek çok mesele Jenkis, Travis, GoCD, Circle CI derken kendini araba pazarındaymış gibi hissediyorsun. Aşağıdak grafik bir şeyler anlatıyor ama local SCM kullanırken aynı zamanda CI&D işlerini de aradan çıkartayım dediğinizde Gitlab güzel seçenek.
Senaryo
Tam gerçek hayat senaryosu olmasa da Go Programlama Dilini kullanarak basit bir ugulama yazacağız. Bu uygulama bir web servisi olacak ve parametre geçilen string’in MD5 hash’ini döndüren bir fonksiyona sahip olacak o kadar.
Daha sonra uygulamayı da Docker container’ı ile dağıtılabilir hale getireceğiz (Dockerize) ve CI üzerinden bu dağıtımın otomatik olarak işlemesini sağlayacağız.
Uygulama
Verilen değerin MD5 hashini geri dönen bir Web servisine ihtiyacım olduğundan aşağıdaki şekilde yazıp geçtim. Maksat çalışan uygulamamız olsun, esas konu CI.
Uygulama aşağıdaki gibi:
go run main.go
dediğimizde 8080 portunu dinleyen bit HTTP servisi ayaklanacak
curl http://localhost:8080/?q=hello+gophers
olarak istediğimizde ise MD5 hash dönüyorsa çalışıyordur.
Uygulamamız hazır olduktan sonra ufak bir iki teamüle değinmek istiyorum. Çünkü olayı sistematik hale getirmek önemli.
Git Flow ile ilerleyelim.
Git Flow
Özellikle birden fazla kişi aynı projede çalışıyorsa Git kullanımında belirli bir model belirlemek işleri daha sistematik hale getiriyor. Bu model ise genelde Git Flow olur.
Hemen süper hızlı Git Flow 101 aşağıdaki gibi:
Projeye git flow eklemek için:
git flow init
Projede yeni bir özellik eklemeye karar verdiğinizde kullanabileceğiniz komut feature start olur. Özellik adı mobile_odeme bu arada.
git flow feature start mobile_odeme
Özelliği geliştrdiniz, düzgün çalıştığından emin oldunuz. Bu özelliği aşağıdaki komut ile sonlandırmalısınız. Finish yaptığınız özellik otomatik olarak develop branch’ına merge olacak ve üzerinde çalıştığınız branch silinecektir.
git flow feature finish mobile_odeme
Yol haritanızdaki belirli özellikleri tamamladınız ve milestone’a eriştiniz. Artık yeni bir release çıkma vakti geldi. Aşağıdaki komut release isminde yeni bir branch açacaktır.
git flow release start v1.0.0
v1.0.0 branch’ında release ile ilgili son değişiklikleri yapıp commitlediniz artık yeni bir release için hazırsınız. Finish yapmalısınız
git flow release finish v1.0.0
Finish yaptıktan sonra git flow develop ve master branchlarına v1.0.0 brach’ını merge edecek ve v1.0.0 isminde yeni bir tag ekleyecektir.
Bu aşamada tag’ı push etmek için:
git push origin --tags
Release çıktıktan sonra uygulamada hatalar farkettiniz ve düzeltmeniz gerekiyor. Bu aşamada her bir hata için hotfix başlatabilirsiniz. Hotfix master’ı baz alarak yeni bir branch başlatır.
git flow hotfix start v1.0.1-fix
Gerekli düzenlemeleri yaptınız ve hatayı giderdiğinizden emin oldunuz. Hotfix’i finish yapıp master ve develop’a merge olmasını sağlayabilirsiniz.
git flow hotfix finish v1.0.1-fix
Git Flow ile ilgili bakılması gereken kaynaklar:
- https://nvie.com/posts/a-successful-git-branching-model/
- https://danielkummer.github.io/git-flow-cheatsheet/index.tr_TR.html
Bu arada Git Flow tabi mükemmel bir model değil. Alternatif olarak aşağıdakilere bir göz atabilirsiniz.
- https://hackernoon.com/gitflow-is-a-poor-branching-model-hack-d46567a156e7
- https://www.endoflineblog.com/gitflow-considered-harmful
- http://scottchacon.com/2011/08/31/github-flow.html
Anlamsal Sürümleme (Semantic Versioning)
Yeri gelmişken parantez açıp anlamsal sürümlemeye de değinmek lazımki git flow release ve hotfix aşamalarında nasıl arttıracağımızı bilelim.
Dikkat ederseniz v1.0.0 gibi bir versiyon adı kullandık. Burada v1 projenin büyük bir özelliği devreye alındıysa yada yazılım üzerinde büyük bir değişiklik olduysa Major sayılıp en baştaki bir arttırılır.
İkinci basamak ise yazılıma yeni eklenen bir özellik veya iyileştirme ile alakalı ilerlemeyi temsil eder.
Son basamak ise yazılımdaki hataların giderilmesi veya build edilmesi ile ilgili alandır. Örneğin her bir hotfix’de bir artar gibi.
semver.org üzerinde çok güzel açıklamalar mevcut.
Semver’e yardımcı olacak commit adetleride ayrıca değinilmesi gereken bir konu. Onu da aşağıdaki adresten inceleyebilirsiniz.
https://semantic-release.gitbook.io/semantic-release/
Commit Mesajlaları
Semantic Release için otomatize araçlar mevcut yani direkt commit mesajlarınıza bakıp versionu bump edebiliyor. Bunun için commit mesajlarınızada bir sistematiğe oturtmanız gerekiyor. Aşağıda örnekleri ufaktan vereyim.
- feat: kullanıcı girişi için 2FA özelliği eklendi
- fix: 2FA özelliğindeki SMS hatası giderildi issue: #1233
- docs: README’yi günelledim
- style: kod üzerindeki uzun satırları düzenledim. Formatlarım vs.
- refactor: bazı değişkenlerin isimlerini büyük harfe çevrdim. Okunabilirliği arttırdım.
- test: eksik olan testleri ekledim.
- chore: Ansible scriptleri güncellendi. gitlab-ci üzerine docker build eklendi.
Konu ile ilgili bir iki kaynak:
- https://seesparkbox.com/foundry/semantic_commit_messages
- http://karma-runner.github.io/1.0/dev/git-commit-msg.html
Dockerize
Dockerize diyorum hep uygulamayı container’ın içine tıkmak anlamında, telafuzu kolay oluyor benim için. Aslında moby ayağına Containerized demek lazım yalnız bir türlü dilim dönmüyor buna :)
Senaryomuza göre yazıdığımız uygulamayı container üzerinde çalıştıracağız. Bunun için uygulamamızın image’ını build etmemiz gerekiyor. Aşağıdaki Dockerfile bu işi yapacak.
Dikkat ederseniz iki tane FROM göreceksiniz. Birincisi Go uygulamamızı build etmek için kullandığımız golang:1.11.6 tag’ına sahip ve “builder” ismi verilmiş image.
Diğeri ise iron/base tag’ına sahip. Uygulamayı çalıştıracak image. Buna container camiasında multi-stage diyorlar. İlk image ile delenip oluşturulan binary dosyası COPY‘nin –from parametresi ile alınıp diğer image’a kopyalanıyor.
Yukarıdaki Dockerfile’ı aşağıdaki kod ile build edebilirsiniz.
docker build --build-arg VERSION=v1.0.0 -t md5go:latest .
Ortaya çıkan image’ı run etmek isterseniz de aşağıdaki komut yeterli.
docker run -d --name md5go -p 8080:8080 md5go:latest
Düzgün çalıştığını doğrulamak isterseniz bir curl isteği ile bakabilirsiniz.
curl http://localhost:8080/\?q=hello
Bu arada Docker build’lerini daha iyi yönetebilmek ve kompleks işlemlere girişebilmek için özellikle CI üzerinde Habitus aracını kullanabilirsiniz.
Diğer bir araç ise Buildah! Docker daemon’undan bağımsız bir araç yine CI süreçlerinde daha iyi seçenek olabiliyor.
Docker Registry
Gelelim uygulamayı dağıtmaya. Öncelikle container bazlı bir çalışma ortamınız olacaksa image’ları tutacağınız ve versiyonlayabileceğiniz bir registry’nizin olması gerekiyor. Ben production’da Amazon’un ECR‘sini kullanıyorum. Ama yazıda Docker HUB’dan gittim.
Alternatif olarak aşağıda private registry olarak kullanabileceğiniz bir kaç araç var.
Bunların yanında Gitlab ile gelen yerel Registry servisini veya Docker Hub‘ı kullanabilirsiniz.
Gitlab CI
Gitlab her ne kadar Source Control Manager olarak geliştirilsede CI özelikleri bir çok ihtiyaca çözüm sunar nitelikte. Özellikle Gitlab Runner‘ın bağımsız tasarımı oldukça esneklik katıyor.
Gitlab üzerinde host ettiğimiz kaynak kodunu CI üzerinde yönetebilmek için yml dosyasını kullanıyoruz. Kısaca projenin root’una .gitlab-ci.yml isimli dosyayı ekleyip ilgili direktifleri yazarak tüm süreci yönetebilirsiniz.
Şimdi tasaraldığımız sürece geçelim. Aşağıda iki aşamalı bir CI süreci mevcut. İnceleyin.
Yukarıdaki direktifleri açıklamaya çalışırsak:
En önemli konulardan biri Executor. Gitlab Runner’ın bir ton executor‘u var. Yani CI ortamı bash üzerinde mi çalışacak yoksa docker container’ının içinde mi çalışacak diye register ederken söylüyorsunuz. Bizim senaryomuzda Executor docker. Yani tüm süreç Docker container’ının içinde işleyecek ve kaybolacak.
image: docker
services:
- docker:dind
image ve services direktifleri bütün sürecin docker isimli bir image üzerinden ayaklandırılan container içinde gerçekleşeceğini söylüyor.
image: direktifini sadece kendi süreciniz için oluşturduğunuz image’ı da verebilirsiniz. Hatta öyle yapsanız daha hızlı olur.
services: altındaki docker:dind aslında gitlab’ın image’ı. İçinde docker, docker-compose ve docker-machine gibi araçlar hazır geliyor.
stages:
- gosec
- build
Stages alanı gitlab pipeline’ının tüm bölümlerini belirlemenizi sağlıyor. Bizim senaryoda statik kod analizi job’ı var. Arkasından build edilip push edildiği için iki bölüm belirledik. Genelde test, build, deploy, run diye olur. Sonra oluşturulacak job’ları buna bağlıyoruz.
variables:
VERSION: $CI_COMMIT_TAG
APP_NAME: md5go
REG_USERNAME: $REG_USR_SECRET
REG_PASSWORD: $REG_PASSWD_SECRET
Variables direktifi aslında bir çeşit Environment Variables. Hem CI direktifleri içinde değişkenmiş gibi davranıyor hem de Executor dediğimiz pipeline’ın çalışma ortamına system environment değişkeni olarak aktarılıyor.
Burada VERSION değişkenine direkt Gitlab’ın ön tanımlı variable‘larından $CI_COMMIT_TAG’ı atadık ki versiyonu alalım.
REG_USERNAME ve REG_PASSWORD değişkenleri hassas olduğundan direkt Gitlab üzerinde daha önce tanımlanmış secret‘lardan alıyor.
build:
stage: build
Build direktifi aslında Job denen iş parçasının ismi. Burada kafa karışıklığı olmasın farklı bir isimde de verebilirdik. Örneğin derle_ve_gonder gibi.
stage: build ise bu iş parçasının hangi stage’e ait olduğu. Yukarıda tanımladığımız stages direktifinde tanımladığımız build aşamasına ait olduğunu belirtiyoruz.
script:
- echo $VERSION
- docker build --build-arg VERSION=$VERSION -t $APP_NAME:$VERSION .
- docker tag $APP_NAME:$VERSION $REG_USERNAME/$APP_NAME:$VERSION
- docker login -u $REG_USERNAME -p $REG_PASSWORD
- docker push $REG_USERNAME/$APP_NAME:$VERSION
script: direktifi bildiğiniz shell komutları yazıyormuşcasına takılabileceğiniz bir alandır. Komutlar yukarıdan aşağıya sırası ile işler.
Biz burada projenin root’unda bulunan Dockerfile dosyasını build ediyoruz. Build olurken Go uygulamamızın derlenmesini sağlıyoruz ve bir docker image’ı haline getiriyoruz. Sonra Docker Hub’a login olup image’ı gönderiyoruz.
only:
- /^v[0-9|\.]+/
except:
- branches
Only direktifi Job’ları tetiklenmesi için kullanılan bir mekanizma. Bu alan bir regex’e göre tetiklendiği gibi branch bazlı, commit mesajı bazlı da tetiklenebiliyor. Biz burada regex’i seçtik.
Except direktifine branches dediğimizde ise branch’lara gelen herhangi bir commit’i dikkate alma anlamına geliyor. Böylece git’in çeşitli olaylarında bu job’ları çalıştırıp çalıştırmamayı bu iki direktif ile yönetebiliyorsunuz.
Buradaki only direktifindeki regex sadece yeni bir tag push edildiğinde ve bu tag’ın v1.0.0 gibi bir formata tekabul ettiğinde tetikleniyor.
tags:
- docker-runner
Bu direktif ise Gitlab Runner ile ilgili. Bir projede birden fazla runner’ı farklı taglar vererek kullanabilirsiniz. Örneğin bu senaryoda repository’e tanımlı docker-runner tag’ına sahip bir runner mevcut. Tags direktifi ile ilgili tag’a sahip runner’ı kullanmak istediğimizi belirtiyoruz.
Gitlab Runner
Gitlab Runner açık kaynak olarak edinebildiğiniz Gitlab’ın üzerindeki kaynak kodu alıp belirli işlemler yaptırıp tekrar sonucunu Gitlab’a gönderen bir servis.
CI pipeline’ı aktifleştirme için öncelikle gitlab-runner’ı Gitlab’a register etmek gerekiyor. Gitlab Runner’ın kurulumu ve Gitlab’e register edilmesi ile alakalı aşağıdaki kaynakları inceleyebilirsiniz.
Gitlab Runner’ı kurup, register edildiğini varsayıyorum. Bundan sonrası bir iki config‘e bakıyor. Aşağıda kullandığım örnek bir runner config’i mevcut.
Dikkat ederseniz name direktifindeki değeri, gitlab-ci dosyamızın içinde tag olarak kullanmıştık ki istediğimiz runner’ı çalıştırabilelim. Ayrıca runner’ın kullanacağı DNS sunucularını da elle vermek daha stabil hale getiriyor. Burada Google ve Verizon’un DNS’lerini kullanıyorum. Diğer ayarların ne işe yaradıklarına ise buradan ulaşabilirsiniz.
Tetikleme
Gitlab’daki kodu bilgisayarınıza kopyaladınız. Sonra git-flow’u takip ederek yeni feature’lar ekleyip son olarak yeni bir release çıkarttınız. Release’i sonlandırıp push ettikten sonra aşağıdaki komut ile tag’larıda push ettiğiniz anda CI tetiklenecektir.
git push origin --tags
Hemen arkasından Gitlab CI yeni bir Pipeline devreye alıp Job’ları çalıştıracak ve Docker HUB’a yeni versiyon tag’ı ile yeni image’ı push edecektir.
Konu ile ilgili kodlara aşağıdan ulaşabilirsiniz: