Jenkins ile Google Cloud Kubernetes Üzerine Continious Deployment(CD) Nasıl Yapılır?

Dilaver Demirel
Devops Türkiye☁️ 🐧 🐳 ☸️
8 min readDec 15, 2019

Bir önceki yazıda Docker ile CI ortamının kurulumu üzerine konulardan bahsetmiştim. Bu yazıda ise CI sürecinden geçen bir uygulamayı Google Kubernetes(k8s) Engine üzerindeki bir cluster’a deploy etmeyi anlatmaya çalışacağım.

Özellikle çok servisi olan projelerde işleri otomatize etmek en önemli konulardan biri olmaktadır. Bir monolithic uygulamayı belki manuel olarak build edip deploy edebilirsiniz fakat microservices mimarideki gibi uygulama modülleri farklı servislere ayrıldığında elimizde birçok proje olabilir. Bu projeler üzerinde sürekli geliştirme yaptığımızı düşünürsek her bir proje için build ve deployment yapmak çok çileli olur.

Continious delivery ortamını kurduğumuzda yapılan değişikliğin ilgili ortama yansıması için kod değişikliğini commit edip sadece bir müddet beklememiz yeterli olacaktır. Otomasyon ortamı bizim için projemizi build eder, test eder, analiz eder ve ilgili ortama(ör. test) teslim eder.

Bu yazının amacı commit işleminden sonra uygulamanın gcp(Google Cloud Platform) k8s cluster’ine teslim edilmesine kadar ki süreci örneklemektir.

Başlamadan önce GCP üzerinde bir hesabınız olması gerekmektedir. Google bu tarz denemeler için bizlere ücretsiz deneme hakkı sağlamakta. GKE(Google kubernetes engine) Google Compute Service üzerinde otomatik olarak oluşturulan sanal makineler ile çalışır. Fakat free tier kullanımda sadece micro bir instance ücretsiz sağlandığı için k8s cluster’a bu yeterli olmayacaktır. Bu sebepten bir miktar ödeme yapmanız gerekebilir.

Hesabınızı oluşturduktan sonra GCP sizden bir Proje oluşturmanız gerekli. Oluşturulan projenin adı ilerideki adımlarda gerekli olacak.

GCP üzerinde kubernetes cluster oluşturma

Hesap oluşturma işlemini geçtikten sonra artık ihtiyacımız olan bir k8s cluster oluşturmak. https://console.cloud.google.com/ adresinden GCP consol’a bağlanıp “Kubernetes Engine” bölümüne geçmeliyiz. Sonrasında “Cluster” sekmesinden yeni bir cluster oluşturmalıyız.

Sonraki adımda “Your first cluster” seçeneği ile default ayarları kullanarak kolaylıkla bir kubernetes clusteri oluşturabiliriz

Bu adımda belirlediğiniz cluster name ve zone bilgisi sonraki adımlarda gerekli olacak. Cluster oluşturulduktan sonra deployment sürecinde bize gerekli olan GCP service account oluşturmalıyız.

GCP üzerinde jenkins service account tanımlama

Bunun için GCP console’a girip IAM sekmesine geçelim.

Create Service Account butonu ile aşağıdaki ekran açılacaktır;

Account name olarak “jenkins-cd” verebiliriz. Account oluşturulduktan sonra account detayına girip “CREATE KEY” butonu ile bir key oluşturmamız gerekmektedir. Oluşturduğumuz bu key’e indirmeliyiz. Bu key ile jenkins üzerinden GCP docker registry ve k8s clusterine erişeceğiz.

IAM sekmesi üzerinden oluşturduğumuz jenkins-cd accountunu yetkilendirmemiz gerekmekte. “ADD” butonuna basıp “New Members” bölümüne “jenkins-cd” yazıp

  • Kubernetes Engine Developer
  • Storage Admin(Docker registry için)

Rollerini vermeliyiz.

Bu aşamaya kadar GCP üzerinde ihtiyaç duyduğumuz düzenlemeleri yaptık. Bunlar;

  • Bir GKE cluster oluşturduk
  • Dışarıdan cluster yönetimi ve docker image’larını deploy etmek için bir service account oluşturduk

Jenkinsin hazır hale getirilmesi

Bir önceki yazıda docker üzerinde Jenkins çalıştırılması için bir Dockerfile oluşturmuştum. Bu seferki senaryo için bu dockerfile’ı biraz değiştirmemiz gerekli. Yeni dockerfile aşağıdaki gibi;

FROM jenkins/jenkins:2.176.4USER rootARG ssh_prv_key
ARG ssh_pub_key
# Install kubectl for GKE integration
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.16.0/bin/linux/amd64/kubectl
RUN chmod +x ./kubectl
RUN mv ./kubectl /usr/local/bin/kubectl
RUN mkdir -p /root/.ssh && \
chmod 0700 /root/.ssh && \
ssh-keyscan -p 2022 [host-name] > /root/.ssh/known_hosts
RUN echo "$ssh_prv_key" > /root/.ssh/id_rsa && \
echo "$ssh_pub_key" > /root/.ssh/id_rsa.pub && \
chmod 600 /root/.ssh/id_rsa && \
chmod 600 /root/.ssh/id_rsa.pub
USER jenkins

Bir önceki dockerfile’a göre aradaki fark “kubectl” aracı. Kubectl aracı build ettiğimiz ve google container registry’e upload ettiğimiz docker image’ını kubernetes cluster’e deploy ya da yeni versyionunu deploy etmemiz de gerekli.

Yeni dockerfile’ın build edilip çalıştırılması için bir önceki yazıyı okumanız gerekecek.

Jenkins üzerine gerekli plug-in’inerin kurulması

Yukarıdaki pluginleri jenkins üzerine kurduktan sonra erişimi için tanımlamalar yapmalıyız.

“Jenkins-cd” service account’ın jenkins üzerinde tanımlanması

Jenkins üzerinde “Credentials” > “System” > “Global Credentials” > “Add Credentials” adımlarını izleyerek aşağıdaki gibi bir jenkins google service account tanımlamanız gerekli.

“JSON Key” kısmında ise daha önce “jenkins-cd” service account oluştururduktan sonra bir key tanımı yapıp bunu download etmiştik. Bu download ettiğimiz JSON key dosyasını buradan seçip upload etmemiz gerekli.

Bu adıma kadar gerekli olan altyapımızı hazır hale getirdik. Artık uygulama tarafındaki gerekli adımlara geçiyoruz. Bunun için örnek bir proje oluşturdum. Aşağıdaki adresten erişebilirsiniz.

https://github.com/dilaverdemirel/simple-spring-boot-k8s-app

node {
properties([
pipelineTriggers([
[$class: "SCMTrigger", scmpoll_spec: "*/3 * * * *"],
]),
buildDiscarder(logRotator(daysToKeepStr: '3', numToKeepStr: '3'))
])

stage("BuildProcess"){
/* Requires the Docker Pipeline plugin to be installed */
docker.image('maven:3-alpine')
.inside('-v $HOME/.m2:/root/.m2 -v /var/run/docker.sock:/var/run/docker.sock --privileged') {
SOURCE_REPOSITORY_URL = ""
stage('Checkout') {
def scmVars = checkout([$class: 'GitSCM',
branches: [[name: '*/master']],
doGenerateSubmoduleConfigurations: false,
extensions: [],
submoduleCfg: [],
userRemoteConfigs:
[[url: SOURCE_REPOSITORY_URL]]])
/* Detecting commit parameters */
GIT_COMMIT_ID = scmVars.GIT_COMMIT
PROJECT_SOURCE_BRANCH
=
scmVars.GIT_BRANCH
}

stage('PrepareParameters'){
/* Preparing GKE parameters */
GKE_PROJECT_ID = ""
GKE_CLUSTER_NAME = ""
GKE_LOCATION = ""
GKE_CREDENTIALS_ID = ""
DOCKER_HOST_IP = ""
DOCKER_HOST_PORT = ""
DOCKER_REGISTRY_CREDENTIALS_ID = ""
DOCKER_IMAGE_DEFAULT_NAME = "ddemirel/simple-spring-boot-k8s-app:latest"
DOCKER_IMAGE_REGISTY_NAME_PREFIX = "gcr.io/[sizin_gcp_projenizin_adı]/simple-spring-boot-k8s-app"

def
matcher = readFile('pom.xml') =~ '<version>(.+?)</version>'
PROJECT_MAVEN_VERSION =
matcher ? matcher[0][1] : null
echo "GIT_COMMIT_ID : ${GIT_COMMIT_ID}"
echo "PROJECT_SOURCE_BRANCH : ${PROJECT_SOURCE_BRANCH}"
echo "PROJECT_MAVEN_VERSION : ${PROJECT_MAVEN_VERSION}"
}

stage('Build') {
sh 'mvn -DskipTests clean install'
}

stage('Test') {
sh 'mvn test'
}

stage('BuildDockerImage'){
sh label: 'Build',
script: 'mvn "-Dmaven.test.skip" "-Ddocker.host=http://${DOCKER_HOST_IP}:${DOCKER_HOST_PORT}" docker:build'
}

}
}

stage('PushDockerImage'){
withDockerRegistry([credentialsId: DOCKER_REGISTRY_CREDENTIALS_ID, url: "https://gcr.io"]) {
sh "docker tag ${DOCKER_IMAGE_DEFAULT_NAME} ${DOCKER_IMAGE_REGISTY_NAME_PREFIX}-${PROJECT_SOURCE_BRANCH}:${PROJECT_MAVEN_VERSION}.${GIT_COMMIT_ID}"
sh "docker push ${DOCKER_IMAGE_REGISTY_NAME_PREFIX}-${PROJECT_SOURCE_BRANCH}:${PROJECT_MAVEN_VERSION}.${GIT_COMMIT_ID}"
}
}

stage("Deploy to GKE"){
path = "${workspace}/k8s-definitions.yml"
def
fileContent = readFile(path)
IMAGE_NAME_VAR = "${DOCKER_IMAGE_REGISTY_NAME_PREFIX}-${PROJECT_SOURCE_BRANCH}:${PROJECT_MAVEN_VERSION}.${GIT_COMMIT_ID}"
/* Preparing k8s deployment script */
fileContent = fileContent.replaceAll("#IMAGE_NAME", IMAGE_NAME_VAR)

DEPLOYNEMT_FILE_NAME='k8s-deployment-temp.yml'

writeFile file: DEPLOYNEMT_FILE_NAME, text: fileContent

def newFileContent = readFile(DEPLOYNEMT_FILE_NAME)
echo newFileContent

/* Deploying to GKE */
step([
$class: 'KubernetesEngineBuilder',
projectId: GKE_PROJECT_ID,
clusterName: GKE_CLUSTER_NAME,
location: GKE_LOCATION,
manifestPattern: DEPLOYNEMT_FILE_NAME,
credentialsId: GKE_CREDENTIALS_ID,
namespace : "default",
verifyDeployments: true]
)
}
}

Not : Jenkinsfile içerisindeki [sizin_gcp_projenizin_adı] kısmını sizin oluşturduğunuz proje adı ile değiştirmelisiniz.

Jenkinsfile içerisindeki stageleri açıklayacak olursak;

  • BuildProcess > Checkout : Proje kodlarının indirilmesi
  • BuildProcess > PrepareParameters : Gerekli olan bazı parametrelerin hazırlanması
  • BuildProcess > Buil : Projenin build edilmesi
  • BuildProcess > Test : Projedeki testlerin çalıştırılması
  • BuildProcess > BuildDockerImage : Docker image’ın oluşturulması
  • Deploy To GKE : Projenin GCP kubernetes’e deploy edilip çalıştırılması

Parametrelerin açıklanması;

  • SOURCE_REPOSITORY_URL : Github proje adresi
  • GIT_COMMIT_ID : Bu otomatik olarak git’in commit için belirlediği ID bilgisidir. Chekout işleminde bu bilgi otomatik olarak alınır.
  • PROJECT_SOURCE_BRANCH : Bu bilgi checkout işleminde otomatik olarak alınır
  • GKE_PROJECT_ID : Daha önce GCP üzerinde oluşturduğumuz proje adı
  • GKE_CLUSTER_NAME : Oluşturduğumuz k8s cluster adı
  • GKE_LOCATION : K8s cluster’ini oluşturduğumuz zone bilgisi
  • GKE_CREDENTIALS_ID : Jenkins üzerinde oluşturduğumuz credential ID, proje adı ile aynı.
  • DOCKER_HOST_IP : Jenkins’in üzerinde çalıştığı docker host makinasının IP’si
  • DOCKER_HOST_PORT : Docker HTTP servisi için erişim portu. Default değeri 2375'dir
  • DOCKER_REGISTRY_CREDENTIALS_ID : Oluştruduğumuz docker imagını private olan Google Container Registry’e upload etmemiz için bu gereklidir. Buraya daha önce jenkins-cd credential için ID bilgisini yazmalıyız. “gcr:[project-name]” olacaktır.
  • DOCKER_IMAGE_DEFAULT_NAME : Image build işleminde maven projesi default bir image name verecektir. Buraya bu bilgiyi yazmalıyız
  • DOCKER_IMAGE_REGISTY_NAME_PREFIX : Google Container registry’e upload ederken google’ın global registry adresi olan “gcr.io” ile başlayan bir image name vermemiz gerekli. Buraya bunu yazmalıyız.

Jenkinsfile içinde bu tanımlamaları yaptıktan sonra projemizi jenkins üzerinde pipeline item olarak tanımlayabiliriz. Bir önceki yazıda mevcut.

Kubernetes üzerine deployment yaparken bir tanımlamaya ihtiyacımız var. Çalıştırılacak uygulamanın docker image name’i, java options bilgileri, uygulamanın hangi porttan çalıştığı, dış ip ile internete açılıp açılmayacağı, kaç adet instance çalıştırılacağı gibi birçok bilgi. Bunların tanımlandığı dosya projeceki k8s-definitions.yml dosyasıdır.

# DEPLOYMENT

apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
generation: 5
labels:
run: simple-spring-boot-k8s-app
name: simple-spring-boot-k8s-app
namespace: default
spec:
progressDeadlineSeconds: 2147483647
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
run: simple-spring-boot-k8s-app
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
run: simple-spring-boot-k8s-app
spec:
containers:
- env:
- name: JAVA_OPTS
value: '-XX:MaxRAM=450m'
image: #IMAGE_NAME
imagePullPolicy: Always
name: simple-spring-boot-k8s-app
ports:
- containerPort: 9197
protocol: TCP
resources:
limits:
memory: 450Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 60

---

# SERVICE

apiVersion: v1
kind: Service
metadata:
labels:
run: simple-spring-boot-k8s-app
name: simple-spring-boot-k8s-app
namespace: default
spec:
ports:
- port: 8090
protocol: TCP
targetPort: 8090
selector:
run: simple-spring-boot-k8s-app
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}

Bu dosyada iki bölüm bulunur. Bunlarda ilki olan DEPLOYMENT kısmı uygulamanın nasıl çalıştırılacağını tarif eder. İkinci bölüm ise SERVICE ‘dir. Service bölümü ise uygulamaya nasıl erişileceğini tarif etmeyi sağlar. Bu kısım daha çok kubernetes’e özel. Başka bir blog postunda bu kısmı daha detaylı inceleyebiliriz.

Buradaki IMAGE_NAME’i açıklamak gerekebilir. Her commit sonrası çalışan jenkins task’ı yeni bir image build eder. Bir nevi uygulamanın versiyonu olarak düşünebiliriz.

Jenkinsfile:IMAGE_NAME_VAR = "${DOCKER_IMAGE_REGISTY_NAME_PREFIX}-${PROJECT_SOURCE_BRANCH}:${PROJECT_MAVEN_VERSION}.${GIT_COMMIT_ID}"

Bu satırda image name DOCKER_IMAGE_REGISTY_NAME_PREFIX, PROJECT_SOURCE_BRANCH, PROJECT_MAVEN_VERSION ve GIT_COMMIT_ID bilgilerinden oluşur. GIT_COMMIT_ID bilgisi yapılan en son commitin ID’sidir. Bu sayede kubernetes üzerindeki pod’ların image’ları otomatik olarak güncellenir ve yapılan değişiklikler kubernetes üzerinde aktive olur.

Gerekli açıklamalardan sonra uygulamayı jenkins üzerinden build ederek deploy edebiliriz.

Karşımızda yukarıdaki gibi bir tablo olması gerekli. Belirlediğimiz adımları geçtikten sonra uygulamamız çalışıyor.

Artık uygulamamıza kubernetes engine tarafından atanan IP ile erişebiliriz.

Artık temel şekilde çalışan bir CD ortamına sahip olduk. Örnek uygulama kodlarına

adesinden erişebilirsiniz.

--

--