jenkins를 docker로 말아 올릴 때(jenkins Dockerfile 사용) 치명적인 이슈는 모든 environment variable을 사용할 수 없을 수 있다. 


동작할 것이라 생각한 두개의 변수는 비어 있었다.

BUILD_URL

JENKINS_URL_VALUE




Manager Jenkins->Configure System ( $jenkin-url:8080/configure) 에 접속해서 

Global properties의 Envrioment variables를 임의로 추가했더니..



BUILD_URL, JENKINS_URL_VALUE를 jenkins에서 읽어온다. 



Posted by 김용환 '김용환'


ubuntu 장비에서 진행했고,


master만 jenkins-docker를 사용하고, slave는 간단하게 기존처럼 실행(java 설치, 디폴트 디렉토리는 /var/lib/jenkins)가 되도록 했다.


jenkins master 장비에 docker를 실행할 때 최신 docker 18.06.ce 버전을 사용했고,


2.141 버전을 사용했다.


jenkins dockerfile은 https://github.com/jenkinsci/docker/blob/master/Dockerfile 에 있고,





< 주의 사항>


1. 백업은 필수 


docker가 죽어도 파일 시스템은 살아야 하니, 호스트 장비에 파일 시스템을 두었다. credentials이나 ssh, slave 설정, 추가 플러그인은 호스트 장비에 두어야 마음이 편할 것이다. (하지만 항상 백업하는 것이 중요하다. 자칫 잘못하면 도커 볼륨에 연결된 파일들이 모두 초기화될 수 있다) 


docker rm container_id 커맨드가 대표적이다.



2. jenkins-docker의 내부 홈 디렉토리는 /var/jenkins_home 이다.



3. docker volume 설정을 잘 적용한다.

ssh 설정, jenkins 홈 디렉토리를 docker외부에서 접근할 수 있도록 한다.


 -v  /home/www/.ssh/:/var/jenkins_home/.ssh




예를 들어 여러 개의 docker volumne을 설정하고 싶을 수 있다. 

ssh 설정은 docker host의 ssh 설정을 사용하고, docker의 timezone을 docker host의 timzezone 설정을 사용하도록 하고 jenkins 홈 디렉토리를 docker host의 디렉토리를 가르키게 하려면 다음을 실행한다.


sudo docker run --name google-jenkins-master -d  -v /etc/timezone:/etc/timezone -v /etc/localtime:/etc/localtime -v /var/lib/jenkins:/var/jenkins_home -v  /home/www/.ssh/:/var/jenkins_home/.ssh -p 8080:8080 hub/google-jenkins-docker






4. 기본 계정은 Dockerfile에서 정의한다.

dockerfile 생성할 때 ADMIN_USERNAME, ADMIN_PASSWORD가 기본 패스워드이다. 


ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false" \

    ADMIN_USERNAME="google" \

    ADMIN_PASSWORD="google" \

...




5. 참고할 만한 dockerfile 설정은 다음과 같다. 



ENV JENKINS_FOLDER /usr/share/jenkins

USER ROOT


COPY plugins.txt "${JENKINS_FOLDER}/ref/plugins.txt"

COPY setupSecurity.groovy "${JENKINS_FOLDER}/ref/init.groovy.d/setupSecurity.groovy"

COPY setupProxy.groovy "${JENKINS_FOLDER}/ref/init.groovy.d/setupProxy.groovy"

COPY executors.groovy "${JENKINS_FOLDER}/ref/init.groovy.d/executors.groovy"

RUN /usr/local/bin/install-plugins.sh < "${JENKINS_FOLDER}/ref/plugins.txt"



6. 플러그인 추려내기

아래를 참조해서 잘 알아낸다. 실제 알고 있던 플러그인 이름이 plugins.txt 파일의 이름과 정확히 매칭되지 않으니.. 좀 시도해야 봐야 감이 잡힌다.

https://github.com/fabric8io/jenkins-docker/blob/master/plugins.txt



7.시작할 때 다음 url을 참고한다. 


https://github.com/jenkinsci/docker/blob/master/README.md

Posted by 김용환 '김용환'



쿠버네티스에 배포한후 pods 상태가 이상하다.


$ kubectl apply -f deployment.yaml



$ kubectl get pods

NAME                          READY     STATUS             RESTARTS   AGE

oncall-api-79f79c5bdf-cltxk   0/1       CrashLoopBackOff   7          12m





다시 배포해도 동일한 문제가 발생한다.




문제를 해결할려면 kubectl describe와 kubectl log 커맨드를 사용한다.




$ kubectl describe pods


...

    Port:           5000/TCP

    Host Port:      0/TCP

    State:          Waiting

      Reason:       CrashLoopBackOff

    Last State:     Terminated

      Reason:       Error

      Exit Code:    139

      Started:      Tue, 11 Sep 2018 20:15:41 +0900

      Finished:     Tue, 11 Sep 2018 20:15:41 +0900

    Ready:          False

    Restart Count:  7

    Requests:

      cpu:        500m

      memory:     900Mi

    Environment:  <none>


..

Events:

  Type     Reason                 Age                From                             Message

  ----     ------                 ----               ----                             -------

  Normal   Scheduled              15m                default-scheduler                Successfully assigned ...

  Normal   SuccessfulMountVolume  15m                kubelet, ...

  Normal   Pulled                 13m (x5 over 15m)  kubelet, ....

  Normal   Created                13m (x5 over 15m)  kubelet, ...

  Normal   Started                13m (x5 over 15m)  kubelet, Started container

  Warning  BackOff                7s (x70 over 15m)  kubelet, Back-off restarting failed container


...




kubernetes yaml에 인덴트가 정상인지 확인하고,
잘못된 부분이 어딘가 있을지도 모르니 철자 확인한 후,,
imagePullPolicy 정책이 없다면 always로 수정해야 한다.

http://knight76.tistory.com/entry/kubernetes%EC%97%90%EC%84%9C-%EB%B0%B0%ED%8F%AC-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%83%9D%EC%84%B1%EC%8B%9C-%EC%9C%A0%EC%9D%98-%EC%82%AC%ED%95%AD-imagePullPolicy


Posted by 김용환 '김용환'


kubernetes에 사용할 yaml(예, deployment.yaml)을 사용시 토큰 정보를 사용할 때...


주의할 점은 user와 token이 같이 레벨이 아니다. ㅠㅠ   같은 레벨인지 알고 계속 삽질함



- name: samuel
  user:
    token: abc...




Posted by 김용환 '김용환'



코세라 강의 중에 https://www.coursera.org/learn/google-kubernetes-engine/ 강의가 있다.

거기에 https://cloud.google.com/solutions/continuous-delivery-jenkins-kubernetes-engine를 따라는 학습 내용이 있는데..jenkins-kubernetes를 언젠가는 한 번 따라해 보고 관련 내용을 참고로 올려둔다.










* 환경 설정


$ gcloud config set compute/zone <zone-name:us-central1-a>


$ git clone https://github.com/GoogleCloudPlatform/continuous-deployment-on-kubernetes.git -b v1


$ cd continuous-deployment-on-kubernetes


$ gcloud container clusters create bootcamp --num-nodes 5 --scopes "https://www.googleapis.com/auth/projecthosting,storage-rw"


Creating cluster bootcamp....... done.

Created [https://container.googleapis.com/v1/projects/qwiklabs-gcp-2a7478819754525a/zones/us-central1-a/clusters/bootcamp].

To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-a/bootcamp?project=qwiklabs-gcp-2a747

8819754525a

kubeconfig entry generated for bootcamp.

NAME      LOCATION       MASTER_VERSION  MASTER_IP      MACHINE_TYPE   NODE_VERSION  NUM_NODES  STATUS

bootcamp  us-central1-a  1.9.7-gke.6     35.226.136.18  n1-standard-1  1.9.7-gke.6   5          RUNNING





* jenkins 볼륨 설정


디스크 이미지를 생성한다

$ gcloud compute images create jenkins-home-image --source-uri https://storage.googleapis.com/solutions-public-assets/jenkins-cd/jenkins-home-v3.tar.gz


Created [https://www.googleapis.com/compute/v1/projects/qwiklabs-gcp-2a7478819754525a/global/images/jenkins-home-image].

NAME                PROJECT                        FAMILY  DEPRECATED  STATUS

jenkins-home-image  qwiklabs-gcp-2a7478819754525a                      READY



디스크 이미지에서 persistent disk를 생성한다.


$ gcloud compute disks create jenkins-home --image jenkins-home-image


Created [https://www.googleapis.com/compute/v1/projects/qwiklabs-gcp-2a7478819754525a/zones/us-central1-a/disks/jenkins-home].

NAME          ZONE           SIZE_GB  TYPE         STATUS

jenkins-home  us-central1-a  10       pd-standard  READY



* jenkins credentials 설정


$ export PASSWORD='jenkins'


CHANGE_ME를 기본 패스워드로 변경한다.


$ vi jenkins/k8s/options


--argumentsRealm.passwd.jenkins=jenkins --argumentsRealm.roles.jenkins=admin


jenkins의 큐버네티스 네임스페이스를 생성한다. 


$ kubectl create ns jenkins


namespace "jenkins" created



큐버네티스 secret을 생성한다. 


$ kubectl create secret generic jenkins --from-file=jenkins/k8s/options --namespace=jenkins


secret "jenkins" created



클러스터의 RBAC에 내 계정을 클러스터 어드민 관리자로 등록한다. 


$ gcloud config get-value account

Your active configuration is: [cloudshell-3833]

google1006245_student@qwiklabs.net



$ kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user=$(gcloud config get-value account)



* jenkins를 배포한다. 



kubetcl apply 커맨드의 기능은 다음과 같다. 

1. jenkins를 실행할 수 있는 컨테이너와 jenkins 홈 디렉토리를 포함하는 persistent disk를 포함하는 jenkins 배치를 생성합니다. 


home 디렉토리를 persistent disk에 보관하면 jenkins 마스터를 실행하는 pod가 중단되더라도 중요한 설정 데이터가 유지된다.


2. jenkins 마스터가 클러스터의 다른 pod에서 접근할 수 있도록 하는 두 가지 서비스(NodePort(8080)-외부 사용자용,  ClusterIP(50000)-jenkins master와 executor간 통신)를 생성한다.



kubectl apply를 적용한다. 

$ kubectl apply -f jenkins/k8s/

deployment "jenkins" created

Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply

serviceaccount "default" configured

clusterrolebinding "jenkins-admin" created

service "jenkins-ui" created

service "jenkins-discovery" created



jenkins pod가 실행 중인지 확인한다.  

$ kubectl get pods -w -n jenkins

NAME                      READY     STATUS    RESTARTS   AGE

jenkins-d4df5c85f-5dcxb   1/1       Running   0          9m

(ctrl+c를 줄러 종료한다)



* HTTPS 로드 밸런서를 구성한다.


우선 jenkins 네임 스페이스에 서비스가 잘 떠인지 확인한다.  NodePort, ClusterIP 제대로 떠 있다.


$ kubectl get svc -n jenkins

NAME                TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE

jenkins-discovery   ClusterIP   10.51.252.78   <none>        50000/TCP        10m

jenkins-ui          NodePort    10.51.246.14   <none>        8080:31122/TCP   10m



SSL 인증서와 키를 생성한다


$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/tls.key -out /tmp/tls.crt -subj "/CN=jenkins/O=jenkins"


Generating a 2048 bit RSA private key

.............+++

....................................+++

writing new private key to '/tmp/tls.key'

-----



쿠버네티스 인증서를 secret으로 올린다.


$ kubectl create secret generic tls --from-file=/tmp/tls.crt --from-f

ile=/tmp/tls.key -n jenkins

secret "tls" created


ingress를 사용해 HTTPS 로드 밸런서를 구성한다.


$ cat jenkins/k8s/lb/ingress.yaml

# Copyright 2015 Google Inc. All rights reserved.

#

# Licensed under the Apache License, Version 2.0 (the "License");

# you may not use this file except in compliance with the License.

# You may obtain a copy of the License at

#

#     http://www.apache.org/licenses/LICENSE-2.0

#

# Unless required by applicable law or agreed to in writing, software

# distributed under the License is distributed on an "AS IS" BASIS,

# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

# See the License for the specific language governing permissions and

# limitations under the License.

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: jenkins

  namespace: jenkins

spec:

  tls:

  - secretName: tls

  backend:

    serviceName: jenkins-ui

    

    

$ kubectl apply -f jenkins/k8s/lb/ingress.yaml

ingress "jenkins" created




* jenkins를 연결한다. 


현재 로드 밸런서의 상태를 확인한다. 


$ kubectl describe ingress jenkins --namespace jenkins

Name:             jenkins

Namespace:        jenkins

Address:          35.201.122.4

Default backend:  jenkins-ui:8080 (10.48.0.5:8080)

TLS:

  tls terminates

Rules:

  Host  Path  Backends

  ----  ----  --------

  *     *     jenkins-ui:8080 (10.48.0.5:8080)

Annotations:

  forwarding-rule:        k8s-fw-jenkins-jenkins--f71fa3ee157e1bf9

  https-forwarding-rule:  k8s-fws-jenkins-jenkins--f71fa3ee157e1bf9

  https-target-proxy:     k8s-tps-jenkins-jenkins--f71fa3ee157e1bf9

  ssl-cert:               k8s-ssl-jenkins-jenkins--f71fa3ee157e1bf9

  static-ip:              k8s-fw-jenkins-jenkins--f71fa3ee157e1bf9

  url-map:                k8s-um-jenkins-jenkins--f71fa3ee157e1bf9

  backends:               {"k8s-be-31122--f71fa3ee157e1bf9":"Unknown"}

  target-proxy:           k8s-tp-jenkins-jenkins--f71fa3ee157e1bf9

Events:

  Type    Reason   Age                From                     Message

  ----    ------   ----               ----                     -------

  Normal  ADD      2m                 loadbalancer-controller  jenkins/jenkins

  Normal  CREATE   42s                loadbalancer-controller  ip: 35.201.122.4

  Normal  Service  35s (x3 over 45s)  loadbalancer-controller  default backend set to jenkins-ui:31122

  



backendsf를 살펴보면 아직 unknown이다. healthy가 될 때까지 반복해서 확인한다.   (25분도 걸릴 수 있다고 함)

backends:               {"k8s-be-31122--f71fa3ee157e1bf9":"Unknown"}


다음과 같이 변하는 것을 확인하면 다음 단계를 거친다.

backends:               {"k8s-be-31122--f71fa3ee157e1bf9":"HEALTHY"}

  

  

jenkins 네임 스페이스의 ingress ip를 확인한다. 


$ kubectl get ingress jenkins -n jenkins

NAME      HOSTS     ADDRESS        PORTS     AGE

jenkins   *         35.201.122.4   80, 443   10m




$ echo "Jenkins URL: https://`kubectl get ingress jenkins -n jenkins 

-o jsonpath='{.status.loadBalancer.ingress[0].ip}'`"; echo "Your username/password:  jenkins/$PASSWORD"

Jenkins URL: https://35.201.122.4

Your username/password:  jenkins/jenkins



https://35.201.122.4/에 접근해 본다.



jenkins/jenkins 계정과 암호를 넣는다. 저 미국 땅에 있어서 그런지 엄청 오래 걸린다. 


jenkins 마스터 설정 완료했다




* ci/cd 구성하기


production/canary를 구성한다

설정 파일은 sample-app/k8s에 다 있다.


$ ls -al

total 36

drwxr-xr-x 3 google1006245_student google1006245_student 4096 Sep  7 16:07 .

drwxr-xr-x 7 google1006245_student google1006245_student 4096 Sep  7 16:07 ..

-rw-r--r-- 1 google1006245_student google1006245_student  623 Sep  7 16:07 Dockerfile

-rw-r--r-- 1 google1006245_student google1006245_student 2270 Sep  7 16:07 html.go

-rw-r--r-- 1 google1006245_student google1006245_student 2336 Sep  7 16:07 Jenkinsfile

drwxr-xr-x 6 google1006245_student google1006245_student 4096 Sep  7 16:07 k8s

-rw-r--r-- 1 google1006245_student google1006245_student 4187 Sep  7 16:07 main.go

-rw-r--r-- 1 google1006245_student google1006245_student  872 Sep  7 16:07 main_test.go

$ ls -al k8s

total 24

drwxr-xr-x 6 google1006245_student google1006245_student 4096 Sep  7 16:07 .

drwxr-xr-x 3 google1006245_student google1006245_student 4096 Sep  7 16:07 ..

drwxr-xr-x 2 google1006245_student google1006245_student 4096 Sep  7 16:07 canary

drwxr-xr-x 2 google1006245_student google1006245_student 4096 Sep  7 16:07 dev

drwxr-xr-x 2 google1006245_student google1006245_student 4096 Sep  7 16:07 production

drwxr-xr-x 2 google1006245_student google1006245_student 4096 Sep  7 16:07 services


$ ls -al k8s/production/

total 16

drwxr-xr-x 2 google1006245_student google1006245_student 4096 Sep  7 16:07 .

drwxr-xr-x 6 google1006245_student google1006245_student 4096 Sep  7 16:07 ..

-rw-r--r-- 1 google1006245_student google1006245_student 1271 Sep  7 16:07 backend-production.yaml

-rw-r--r-- 1 google1006245_student google1006245_student 1326 Sep  7 16:07 frontend-production.yaml



$ cd sample-app


$ kubectl create ns production

namespace "production" created




$ kubectl apply -f k8s/production -n production

deployment "gceme-backend-production" created

deployment "gceme-frontend-production" created


$ kubectl apply -f k8s/canary -n production

deployment "gceme-backend-canary" created

deployment "gceme-frontend-canary" created


$ kubectl apply -f k8s/services -n production

service "gceme-backend" created

service "gceme-frontend" created


kubectl scale 커맨드를 사용해 gceme-frontend-production 레플리카를 4개로 늘린다.


$ kubectl scale deployment gceme-frontend-production -n production --replicas 4

deployment "gceme-frontend-production" scaled



제대로 레플리카가 실행 중인지 확인한다.


$ kubectl get pods -n production -l app=gceme -l role=frontend

NAME                                         READY     STATUS    RESTARTS   AGE

gceme-frontend-canary-7ff449cd6-5zs4g        1/1       Running   0          2m

gceme-frontend-production-6f5956885f-57rjx   1/1       Running   0          1m

gceme-frontend-production-6f5956885f-85r65   1/1       Running   0          1m

gceme-frontend-production-6f5956885f-gpjq8   1/1       Running   0          2m

gceme-frontend-production-6f5956885f-mp77h   1/1       Running   0          1m


당연히 front-production만 늘렸으니 다른 것은 변동이 없어야 한다. 


$ kubectl get pods -n production -l app=gceme -l role=backend

NAME                                       READY     STATUS    RESTARTS   AGE

gceme-backend-canary-f94986bfb-gktrj       1/1       Running   0          3m

gceme-backend-production-69d5c6c9f-8csqg   1/1       Running   0          3m




$ kubectl get service gceme-frontend -n production

NAME             TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE

gceme-frontend   LoadBalancer   10.51.246.89   35.239.123.157   80:30795/TCP   3m



fronted service load balancer ip를 환경 변수로 설정한다.


$ kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}"  --namespace=production services gceme-frontend

35.239.123.157


$ export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}"  --namespace=production services gceme-frontend)


$ echo $FRONTEND_SERVICE_IP

35.239.123.157


http://35.239.123.157/를 요청하면 잘 떠 있는지 확인할 수 있다. 



$ curl http://35.239.123.157/version

1.0.0


이제 앱 배포를 완료했다.



* jenkins 파이프라인 생성한다



gceme 샘플 앱 복사본을 cloud source repository에 보낸다


$ gcloud source repos create gceme

Created [gceme].

WARNING: You may be billed for this repository. See https://cloud.google.com/source-repositories/docs/pricing for details.


git 설정하고 코드를 push한다.


$ git init

Initialized empty Git repository in /home/google1006245_student/continuous-deployment-on-kubernetes/sample-app/.git/


$ git config credential.helper gcloud.sh


$ export PROJECT_ID=$(gcloud config get-value project)

Your active configuration is: [cloudshell-3833]


$ gcloud config get-value project

Your active configuration is: [cloudshell-3833]

qwiklabs-gcp-2a7478819754525a


$ git remote add origin https://source.developers.google.com/p/$PROJECT_ID/r/gceme


$ git config --global user.email google1006245_student@qwiklabs.net


$ git config --global user.name google1006245_student


$ git commit -m "Initial commit"

[master (root-commit) 370a2cb] Initial commit

 14 files changed, 723 insertions(+)

 create mode 100644 Dockerfile

 create mode 100644 Jenkinsfile

 create mode 100644 html.go

 create mode 100644 k8s/canary/backend-canary.yaml

 create mode 100644 k8s/canary/frontend-canary.yaml

 create mode 100644 k8s/dev/backend-dev.yaml

 create mode 100644 k8s/dev/default.yml

 create mode 100644 k8s/dev/frontend-dev.yaml

 create mode 100644 k8s/production/backend-production.yaml

 create mode 100644 k8s/production/frontend-production.yaml

 create mode 100644 k8s/services/backend.yaml

 create mode 100644 k8s/services/frontend.yaml

 create mode 100644 main.go

 create mode 100644 main_test.go



$ git push origin master

Counting objects: 21, done.

Compressing objects: 100% (21/21), done.

Writing objects: 100% (21/21), 5.95 KiB | 0 bytes/s, done.

Total 21 (delta 9), reused 0 (delta 0)

remote: Resolving deltas: 100% (9/9)

To https://source.developers.google.com/p/qwiklabs-gcp-2a7478819754525a/r/gceme

 * [new branch]      master -> master

 

 

* 서비스 계정 credentials를 추가한다

 

아까 생성한 jenkins ui인 https://35.201.122.4/에 접속하고 jenkins/jenkins를 입력 후

 Jenkins > Credentials(https://35.201.122.4/credentials/)에 접속한다

 

Credentials 화면의 열쇠 모양 옆의 Jenkins를 클릭한다.

그 다음 화면에서 Global Credentials를 클릭한다 


왼쪽 화면에서 Add Credentials를 클릭한다.

Kind 에서 Select Google Service Account from metadata를 선택한다


Project Name에 "qwiklabs-gcp-2a7478819754525a" 계정이 나오는데. ok를 클릭한다.



credentials를 추가했다.


* jenkins job을 생성한다


jenkins new Job 화면(https://35.201.122.4/view/all/newJob)에서 sample-app 이름 넣고 multibranch pipeline을 생성한다



job화면의 add Source 의 git를 선택하고 https://source.developers.google.com/p/qwiklabs-gcp-2a7478819754525a/r/gceme 를 추가하고 credentials로 잘 변경한다.


 Build Periodically에다가 * * * * *를 추가하고 save 한다. 

 

jenkins 화면 옆에 보면,  jnlp-5b90daf3cdb  (오프라인)을 추가한다.



 

* 개발 환경을 생성한다


$ git checkout -b new-feature

Switched to a new branch 'new-feature'

 

파이프라인 정의를 수정한다


$ gcloud config get-value project

Your active configuration is: [cloudshell-3833]

qwiklabs-gcp-2a7478819754525a


$ export PROJECT=$(gcloud config get-value project) 


$ sed -i s/REPLACE_WITH_YOUR_PROJECT_ID/${PROJECT}/g Jenkinsfile

$ cat Jenkinsfile

node {

  def project = 'qwiklabs-gcp-2a7478819754525a'

  def appName = 'gceme'

  def feSvcName = "${appName}-frontend"

  def imageTag = "gcr.io/${project}/${appName}:${env.BRANCH_NAME}.${env.BUILD_NUMBER}"

  checkout scm

  stage 'Build image'

  sh("docker build -t ${imageTag} .")

  stage 'Run Go tests'

  sh("docker run ${imageTag} go test")

  stage 'Push image to registry'

  sh("gcloud docker -- push ${imageTag}")

  stage "Deploy Application"

  switch (env.BRANCH_NAME) {

    // Roll out to canary environment

    case "canary":

        // Change deployed image in canary to the one we just built

        sh("sed -i.bak 's#gcr.io/cloud-solutions-images/gceme:1.0.0#${imageTag}#' ./k8s/canary/*.yaml")

        sh("kubectl --namespace=production apply -f k8s/services/")

        sh("kubectl --namespace=production apply -f k8s/canary/")

        sh("echo http://`kubectl --namespace=production get service/${feSvcName} --output=json | jq -r '.status.loadBalancer.ingress[0].ip'` > ${feSvcName}")

        break

    // Roll out to production

    case "master":

        // Change deployed image in canary to the one we just built

        sh("sed -i.bak 's#gcr.io/cloud-solutions-images/gceme:1.0.0#${imageTag}#' ./k8s/production/*.yaml")

        sh("kubectl --namespace=production apply -f k8s/services/")

        sh("kubectl --namespace=production apply -f k8s/production/")

        sh("echo http://`kubectl --namespace=production get service/${feSvcName} --output=json | jq -r '.status.loadBalancer.ingress[0].ip'` > ${feSvcName}")

        break

    // Roll out a dev environment

    default:

        // Create namespace if it doesn't exist

        sh("kubectl get ns ${env.BRANCH_NAME} || kubectl create ns ${env.BRANCH_NAME}")

        // Don't use public load balancing for development branches

        sh("sed -i.bak 's#LoadBalancer#ClusterIP#' ./k8s/services/frontend.yaml")

        sh("sed -i.bak 's#gcr.io/cloud-solutions-images/gceme:1.0.0#${imageTag}#' ./k8s/dev/*.yaml")

        sh("kubectl --namespace=${env.BRANCH_NAME} apply -f k8s/services/")

        sh("kubectl --namespace=${env.BRANCH_NAME} apply -f k8s/dev/")

        echo 'To access your environment run `kubectl proxy`'

        echo "Then access your service via http://localhost:8001/api/v1/proxy/namespaces/${env.BRANCH_NAME}/services/${feSvcName}:80/"

  }

}


gcme 코드를 변경한다.


$ sed -i s/blue/orange/g html.go


$ sed -i s/1.0.0/2.0.0/g main.go




$ git add Jenkinsfile html.go main.go



$ git commit -m "Version 2.0.0"

[new-feature 98a48bc] Version 2.0.0

 3 files changed, 4 insertions(+), 4 deletions(-)



$ git push origin new-feature

Counting objects: 5, done.

Compressing objects: 100% (5/5), done.

Writing objects: 100% (5/5), 569 bytes | 0 bytes/s, done.

Total 5 (delta 3), reused 0 (delta 0)

remote: Resolving deltas: 100% (3/3)

To https://source.developers.google.com/p/qwiklabs-gcp-2a7478819754525a/r/gceme

 * [new branch]      new-feature -> new-feature



푸시하고 나서 jenkins ui(https://35.201.122.4/job/sample-app/)에 접속하면 new-feature는 성공하고 master는 에러가 난 것을 확인할 수 있다.



새로 변경한 로드 밸런서 대신 간단하게 kubectl proxy로 테스트할 수 있다.  


$ kubectl proxy &

[1] 955

Starting to serve on 127.0.0.1:8001


$ curl http://localhost:8001/api/v1/proxy/namespaces/new-feature/services/gceme-frontend:80/version

2.0.0




이제 개발 환경이 구축되었다..





* canary 배포하기


$ git checkout -b canary

Switched to a new branch 'canary'


$ git push origin canary

Total 0 (delta 0), reused 0 (delta 0)

To https://source.developers.google.com/p/qwiklabs-gcp-2a7478819754525a/r/gceme

 * [new branch]      canary -> canary

 


푸시하고 나서 jenkins ui(https://35.201.122.4/job/sample-app/)에 접속하면 canary는 성공이다. 


$ export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)


$ while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1;  done

1.0.0

1.0.0

2.0.0

1.0.0

1.0.0


5번 중에 1번은 canary가 받는다.




* 상용 배포하기



$ git checkout master

Switched to branch 'master'


$ git merge canary

Updating 370a2cb..98a48bc

Fast-forward

 Jenkinsfile | 2 +-

 html.go     | 4 ++--

 main.go     | 2 +-

 3 files changed, 4 insertions(+), 4 deletions(-)


$ git push origin master

Total 0 (delta 0), reused 0 (delta 0)

To https://source.developers.google.com/p/qwiklabs-gcp-2a7478819754525a/r/gceme

   370a2cb..98a48bc  master -> master




푸시하고 나서 jenkins ui(https://35.201.122.4/job/sample-app/)에 접속하면 master는 성공이다. 


$ export FRONTEND_SERVICE_IP=$(kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" --namespace=production services gceme-frontend)


$ while true; do curl http://$FRONTEND_SERVICE_IP/version; sleep 1;  done

2.0.0

2.0.0

2.0.0

2.0.0

2.0.0

2.0.0



제대로 상용 환경에 배포했다.

Posted by 김용환 '김용환'



etcd 2.2.5 클러스터링 구성하기



바이너리를 설치 static으로 구성한다. 


172.26.100.161 (로컬 호스트)에서 2380, 2480, 2580 포트의 etcd 데몬으로 클러스터링을 구성하고.


2680 포트의 새로운 etcd 데몬을 추가하는 예시이다.




1번째


 ./etcd --name etcd1  \

 --initial-advertise-peer-urls http://172.26.100.161:2380 \

 --listen-peer-urls http://172.26.100.161:2380 \

 --listen-client-urls http://172.26.100.161:2379,http://127.0.0.1:2379 \

 --advertise-client-urls http://172.26.100.161:2379 \

 --initial-cluster-token "etcd-cluster" \

 --initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

 --initial-cluster-state new



2번째


./etcd --name etcd2 \

--initial-advertise-peer-urls http://172.26.100.161:2480 \

--listen-peer-urls http://172.26.100.161:2480 \

--listen-client-urls http://172.26.100.161:2479,http://127.0.0.1:2479 \

--advertise-client-urls http://172.26.100.161:2479 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

--initial-cluster-state new




3번째


./etcd --name etcd3 \

--initial-advertise-peer-urls http://172.26.100.161:2580 \

--listen-peer-urls http://172.26.100.161:2580 \

--listen-client-urls http://172.26.100.161:2579,http://127.0.0.1:2579 \

--advertise-client-urls http://172.26.100.161:2579 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

--initial-cluster-state new



다 클러스터링이 동기되면. 로그에 다음과 같은 로그가 찍힌다. 정상적이다. 


2018-09-05 06:33:42.154287 N | etcdserver: updated the cluster version from 2.1 to 2.2





버전을 확인하면 제대로 설치됨을 알 수 있다. 


$curl -L http://127.0.0.1:2379/version

{"etcdserver":"2.2.5","etcdcluster":"2.2.0"}




여기에 새로운 etcd 노드를 추가하려고 한다.


./etcd --name etcd4 \

--initial-advertise-peer-urls http://172.26.100.161:2680 \

--listen-peer-urls http://172.26.100.161:2680 \

--listen-client-urls http://172.26.100.161:2679,http://127.0.0.1:2679 \

--advertise-client-urls http://172.26.100.161:2679 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580,etcd4=http://172.26.100.161:2680 \

--initial-cluster-state new

(사실 initial-cluster-state을 new라 하면 안된다)


기존의 클러스터링에 추가되지 못해 나오는 에러가 나오고..


기존 로그에 추가할 수 없다고 나온다. 


2018-09-05 06:41:58.886604 C | etcdmain: couldn't find local name "etcd4" in the initial cluster configuration




이제 클러스터링을 확인한다. 


$  ./etcdctl member list

6564723d6ed1fc93: name=etcd2 peerURLs=http://172.26.100.161:2480 clientURLs=http://172.26.100.161:2479

edbc16344886488f: name=etcd3 peerURLs=http://172.26.100.161:2580 clientURLs=http://172.26.100.161:2579

fc0c8fe65a431b34: name=etcd1 peerURLs=http://172.26.100.161:2380 clientURLs=http://172.26.100.161:2379




추가할 멤버를 추가한다. 

$ ./etcdctl member add etcd4 http://172.26.100.161:2680

Added member named etcd4 with ID 60859f242d8b1d4a to cluster




추가된 멤버는 unstarted로 나온다. 


[~/dev/etcd/4] ./etcdctl member  list

60859f242d8b1d4a[unstarted]: peerURLs=http://172.26.100.161:2680

6564723d6ed1fc93: name=etcd2 peerURLs=http://172.26.100.161:2480 clientURLs=http://172.26.100.161:2479

edbc16344886488f: name=etcd3 peerURLs=http://172.26.100.161:2580 clientURLs=http://172.26.100.161:2579

fc0c8fe65a431b34: name=etcd1 peerURLs=http://172.26.100.161:2380 clientURLs=http://172.26.100.161:2379




이제 4번째 장비에서 다음을 입력한다. 


오랜 시간이 흘러, 이제 새로운 클러스터를 추가한다. 


이 때 initial cluster state 옵션의 값을 new 대신 existing으로 하다.


$ ./etcd 

--initial-advertise-peer-urls http://172.26.100.161:2680 \

--listen-peer-urls http://172.26.100.161:2680 \

--listen-client-urls http://172.26.100.161:2679,http://127.0.0.1:2679 \

--advertise-client-urls http://172.26.100.161:2679 \

--initial-cluster-token "etcd-cluster" \

 --initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580,etcd4=http://172.26.100.161:2680 \

--initial-cluster-state existing

  




* 참고

클러스터링 구축하다가 문제가 발생하면..

추가할 etcd 디렉토리에서 데이터 디렉토리를 다 지워야 한다. 디폴트는 {이름}.etcd로 되어 있다. 







$ ./etcdctl member  list

60859f242d8b1d4a: name=etcd4 peerURLs=http://172.26.100.161:2680 clientURLs=http://172.26.100.161:2679

6564723d6ed1fc93: name=etcd2 peerURLs=http://172.26.100.161:2480 clientURLs=http://172.26.100.161:2479

edbc16344886488f: name=etcd3 peerURLs=http://172.26.100.161:2580 clientURLs=http://172.26.100.161:2579

fc0c8fe65a431b34: name=etcd1 peerURLs=http://172.26.100.161:2380 clientURLs=http://172.26.100.161:2379





클러스티링 추가가 완료되었다.


Posted by 김용환 '김용환'





etcd는 3가지 방식으로 클러스터를 구성할 수 있다.


https://coreos.com/etcd/docs/latest/v2/clustering.html


- Static : 클러스터 주소와 크기를 안 상태에서 설정하는 것을 말한다. --initial-cluster를 사용한다. 일반적으로 사용하는 방식이다.

- etcd Discovery : 클러스터 환경에서 DHCP를 사용하는 환경에서 사용된다. 정적 설정이 아닌 동적 설정(discovery) 환경에서 적합. etcd노드를 추가할때 주로 사용하며 --discovery를 사용한다.

- DNS Discovery : 클러스터 환경에서 DHCP를 사용하는 환경에서 사용된다. 정적 설정이 아닌 동적 설정(discovery) 환경에서 적합. DNS의 SRV 레코드를 사용해 etcd 노드를 추가한다. --discovery-srv를 사용한다.




가장 쉬운 방식인 static으로 구성한다. 


https://github.com/etcd-io/etcd/releases에서 etcd 3.2.9 바이너리를 다운받는다. (현재 기준)

$ wget etcd.zip


zip 파일을 풀어 1, 2, 3 디렉토리로 binary를 복사한다.


$ ls -al 하면 다음 디렉토리와 같다.

drwxr-xr-x   8 samuel.kim  staff       272  7 25 02:13 1

drwxr-xr-x   8 samuel.kim  staff       272  7 25 02:13 2

drwxr-xr-x   8 samuel.kim  staff       272  7 25 02:13 3


 

1번 디렉토리로 이동

./etcd --name etcd1 \

--initial-advertise-peer-urls http://172.26.100.161:2380 \

--listen-peer-urls http://172.26.100.161:2380 \

--listen-client-urls http://172.26.100.161:2379,http://127.0.0.1:2379 \

--advertise-client-urls http://172.26.100.161:2379 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

--initial-cluster-state new



다른 터미널을 열어, 2번 디렉토리로 이동

./etcd --name etcd2 \

--initial-advertise-peer-urls http://172.26.100.161:2480 \

--listen-peer-urls http://172.26.100.161:2480 \

--listen-client-urls http://172.26.100.161:2479,http://127.0.0.1:2479 \

--advertise-client-urls http://172.26.100.161:2479 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

--initial-cluster-state new




다른 터미널을 열어, 3번 디렉토리로 이동

./etcd --name etcd3 \

--initial-advertise-peer-urls http://172.26.100.161:2580 \

--listen-peer-urls http://172.26.100.161:2580 \

--listen-client-urls http://172.26.100.161:2579,http://127.0.0.1:2579 \

--advertise-client-urls http://172.26.100.161:2579 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580 \

--initial-cluster-state new




3대를 모두 실행할 때까지 클러스터링이 안되서 warning이 발생할 수 있다. 



etcd 디렉토리에 디폴트로 etcd3.etcd 데이터 디렉토리가 생성된다.


사용자 지정을 하고 싶다면 --data-dir /opt/etcd/data 와 같이 진행한다.





다른 터미널을 열어, 4번 디렉토리로 이동

./etcd --name etcd4 \

--initial-advertise-peer-urls http://172.26.100.161:2680 \

--listen-peer-urls http://172.26.100.161:2680 \

--listen-client-urls http://172.26.100.161:2679,http://127.0.0.1:2679 \

--advertise-client-urls http://172.26.100.161:2679 \

--initial-cluster-token "etcd-cluster" \

--initial-cluster etcd1=http://172.26.100.161:2380,etcd2=http://172.26.100.161:2480,etcd3=http://172.26.100.161:2580,etcd4=http://172.26.100.161:2680 \

--initial-cluster-state new




* 주의 할 점.



etcd1,etcd2,etcd3에 없는 etcd4를 실행하면..

4번에서는 에러가 많이 발생한다. 

2018-09-05 02:13:01.674392 E | rafthttp: request sent was ignored (cluster ID mismatch: peer[6564723d6ed1fc93]=c00bfb0bce868ccb, local=fbee3df97f467d62)

2018-09-05 02:13:01.674400 E | rafthttp: request sent was ignored (cluster ID mismatch: peer[edbc16344886488f]=c00bfb0bce868ccb, local=fbee3df97f467d62)


1,2,3번 에서는 

2018-09-05 02:13:19.659255 E | rafthttp: request cluster ID mismatch (got fbee3df97f467d62 want c00bfb0bce868ccb)

2018-09-05 02:13:19.659316 E | rafthttp: request cluster ID mismatch (got fbee3df97f467d62 want c00bfb0bce868ccb)


따라서 1,2,3번에서 --initial-cluster에 etcd4를 추가하고 --initial-cluster-state new 대신 --initial-cluster-state existing를 실행하면 좋을 것 같지만.. 동작이 안된다. 


etcdctl member add etcd4 http://172.26.100.161:2680





* 클러스터링 멤버 구성



 ./etcdctl member list

6564723d6ed1fc93, started, etcd2, http://172.26.100.161:2480, http://172.26.100.161:2479

edbc16344886488f, started, etcd3, http://172.26.100.161:2580, http://172.26.100.161:2579

fc0c8fe65a431b34, started, etcd1, http://172.26.100.161:2380, http://172.26.100.161:2379



여기서 하나를 뺀다.

./etcdctl member remove 6564723d6ed1fc93

Member 6564723d6ed1fc93 removed from cluster c00bfb0bce868ccb


 ./etcdctl member list

edbc16344886488f, started, etcd3, http://172.26.100.161:2580, http://172.26.100.161:2579

fc0c8fe65a431b34, started, etcd1, http://172.26.100.161:2380, http://172.26.100.161:2379


이때 etcd2는 종료된다.

다시 etcd2를 실행하면, 마지막에 아래 로그가 나오고 종료된다.


2018-09-05 05:16:18.342165 I | rafthttp: stopped peer 2fa514e820fdcf1e



아래와 같이 등록해도.  한번 빠지면 끝이다..

./etcdctl member add etcd4  --peer-urls=http://172.26.100.161:2479





클러스터 멤버 추가는 2점대와 동일하다.


Posted by 김용환 '김용환'



jenkins 배치 설치를 쿠버네티스로 진행하려 했는데, 


기존처럼  master, slave 구조로 사용할 수 없다. 


https://cloud.google.com/solutions/jenkins-on-kubernetes-engine



The Kubernetes plugin enables using Kubernetes service accounts for authentication, and creating labeled executor configurations with different base images. The plugin creates a pod when an executor is required and destroys the pod when a job ends.




job 단위로 pod를 구성하기 때문에 job을 dockernize를 해야 한다.

job이 실행할 때 pod로 생성한다. 그리고 job이 종료될 때는 해당 pod를 종료한다. 


 따라서 과거처럼 slave에 바이너를 설치하면 사용하는 경우(hdfs 설정, kafka를 사용하는 경우라면) 이 방법을 쓰지 못한다.


job 단위의 dockernize 형태를 구성해야 형태로 구성하는 것이 쿠버네티스 위에서 젠킨스 job을 실행할 수 있다.





아래 문서를 보면..다음과 같이 되어 있다. 



https://www.blazemeter.com/blog/how-to-setup-scalable-jenkins-on-top-of-a-kubernetes-cluster



After both builds are completed, you should see that both build executors have been removed and are not available inside the cluster anymore:



실제 해보니.. 정말 그렇게 동작한다. 

다음을 참조한다.


http://knight76.tistory.com/entry/kubernetes-kubernetesjenkins-%EC%97%B0%EB%8F%99-%EB%A0%88%EC%8B%9C%ED%94%BC




Posted by 김용환 '김용환'





집킨(zipkin)은 복잡한 시스템에서

마이크로 서비스의 상호 작용에 대한 개요를 파악하는 데 도움이 될 수 있다.






스프링 클라우드 Sleuth(https://cloud.spring.io/spring-cloud-sleuth/)는

스프링 부트 애플리케이션과 집킨을 통합할 수 있는 매우 쉬운 방법을 제공한다.


문서는 다음과 같다.

https://coe.gitbook.io/guide/log/sleuth



스프링 클라우드 Sleuth는 추적 ID와 범위(span) ID가 생성되고 통신 중에 전달되도록 한다.

그리고 추적 데이터를 집킨 서버로 전송한다.



스프링 클라우드에 zipkin 관련 프로젝트(spring-cloud-starter-zipkin)가 있다.


예시는 다음을 참조한다.

https://www.baeldung.com/tracing-services-with-zipkin





또한 스프링 클라우드 Sleuth를 사용해 추적 ID만 전송할 수도 있다(http://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/1.2.5.RELEASE/single/spring-cloud-sleuth.html#_only_sleuth_log_correlation).


로그에 추적 ID를 저장하고 있다면 요청에 대한 모든 로그 정보를 로그 분석에서 사용할 수 있다.

Posted by 김용환 '김용환'



open stack vm 인스턴스를 생성할 때 동일한 pm에 있으면, 

pm이 장애가 발생했을 때 같이 인스턴스가 종료되기 때문에 위험할 수 있다. 


같은 pm 장비에 vm 장비가 없도록 하는 것을 anti-affinity라는 정책인데. 그룹(group)에만  적용할 수 있다.

인스턴스 사용하는 방법은 다음과 같다.




$ openstack server group list


+--------------------------------------+--------------------+---------------+

| ID                                                           | Name                       |         Policies      |

+--------------------------------------+--------------------+---------------+

+--------------------------------------+--------------------+---------------+





anti-affinity 정책을 포함한 그룹을 추가한다.


$ openstack server group create stats-tsdb-read --policy anti-affinity


+----------+--------------------------------------+

| Field    | Value                                |

+----------+--------------------------------------+

| id       | 1cd7538d-23c4-493e-8684-3bf46c440864 |

| members  |                                      |

| name     | stats-tsdb-read                      |

| policies | anti-affinity                        |

+----------+--------------------------------------+





이제 stats-tsdb-read group으로 서버를 추가한다. 


$ openstack server create --flavor large --image ubuntu-16.04 --availability-zone US_WEST --hint group=1cd7538d-23c4-493e-8684-3bf46c440864 stats-tsdb-01



$ openstack server create --flavor large --image ubuntu-16.04 --availability-zone US_WEST --hint group=1cd7538d-23c4-493e-8684-3bf46c440864 stats-tsdb-02





만약 잘못만들었다면 다음과 같이 그룹을 삭제한다.


$ openstack server group delete stats-tsdb


Posted by 김용환 '김용환'