pipeline {
agent none
triggers {
pollSCM '* * * * *'
}
parameters {
string name: 'IMAGE_NAME', defaultValue: 'hello-world'
string name: 'IMAGE_REGISTRY_ACCOUNT', defaultValue: 'shinhyeonsik'
}
stages {
stage('SCM Checkout') {
agent any
steps {
git branch: 'main', url: 'https://github.com/hsshin0602/source-maven-java-spring-hello-webapp.git'
}
}
stage('Build Maven Project') {
agent {
docker {image 'maven:3-openjdk-8'}
}
steps {
sh 'mvn clean package -DskipTests=true'
}
}
stage('test Maven Project ') {
agent {
docker { image 'maven:3-openjdk-8'}
}
steps {
sh 'mvn test'
}
}
stage('Build Docker Image') {
agent any
steps {
sh "docker image build -t ${params.IMAGE_NAME} ."
}
}
stage('Tagging Docker Image') {
agent any
steps {
sh "docker image tag ${params.IMAGE_NAME} ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}"
}
}
stage('Publish Docker Image') {
agent any
steps {
withDockerRegistry(credentialsId: 'docker-hub-token', url: 'https://index.docker.io/v1/') {
sh "docker image push ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}"
}
}
}
}
}
# 각 단계마다 컨테이너를 띄우야 하기 때문에 각 스테이지마다 필요할 경우 설정
# 테스트 생략옵션: -DskipTests=true
# 빌드와 테스트는 분리시키는게 좋음
# image 빌드와 tagging를 같이해도 됨
# 도커 이미지 빌드를 위한 도커파일 생성
vi Dockerfile
FROM tomcat:9-jre8
COPY target/hello-world.war /usr/local/tomcat/webapps
# 원도우에서 인스턴스에 ssh로 접속
# 80으로 포트포워딩
docker container run -d --name myweb -p 80:8080 shinhyeonsik/hello-world
# 작동 확인
docker container ls
# ec2 인스턴스의 퍼블릭 IP:hello-world으로 접속하면 접속됨
index.jsp 내용의 version을 수정(1.0.3 -> 1.0.4)한 이후 깃에 푸쉬를 진행, 파이프라인을 통해 자동으로 이미지를 재생성을 진행, 기존 컨테이너를 지우고 그 이미지로 컨테이너를 실행시켜보면 수정된 버전으로 내용이 나오지 않음, 즉 이미지가 작동하지 않음
이유: docker image ls로 확인했을때 로컬에 있는 것은 1.0.3 이고 도커 허브에 있는 것이 1.0.4이다. 이는 docker container의 옵션에서 pull이라는 옵션이 있는데 defalut값이 missiong이다.
이는 같은 이미지가 있으면 새롭게 풀링하지 않겠다는 의미인데 이를 always로 세팅하면 된다. 하지만 이렇게 진행하면 pulling을 네트워크 트래픽 비용이 많이 발생해서 좋지않다.docker container run -d --name myweb -p 80:8080 --pull always shinhyeonsik/hello-world
latest 태그는 최신의 이미지라는 의미이다. 하지만 실제로 테스트하는 환경에서만 latest를 사용한다. 왜나하면 수시로 바뀌는 이미지이기 때문이다. 따라서 latest 이외의 Build_Number로 태그를 붙인 것을 추가해주고 이 이미지를 사용해서 이미지를 pulling하면 된다.
Docker 이미지 태그에 Semver 사용
태그 붙이기: env.BUILD_NUMBER로 이미지 별로 구분이 가능하기 때문에 이를 사용
# 밑 내용만 수정
...
stage('Tagging Docker Image') {
agent any
steps {
sh "docker image tag ${params.IMAGE_NAME} ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:${env.BUILD_NUMBER}"
sh "docker image tag ${params.IMAGE_NAME} ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:latest"
}
}
stage('Publish Docker Image') {
agent any
steps {
withDockerRegistry(credentialsId: 'docker-hub-token', url: 'https://index.docker.io/v1/') {
sh "docker image push ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:${env.BUILD_NUMBER}"
sh "docker image push ${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:latest"
}
}
}
...
도커 컨테이너를 실행하는 것은 복잡한 단계가 필요하고 쿠버네티스로 배포 할 것이기 때문에 이미지를 빌드하는 것까지만 파이프라인으로 실행한다.
eksctl create cluster --name myeks --region ap-northeast-2 --version 1.24 --instance-types t3.medium
eksctl utils associate-iam-oidc-provider --cluster myeks --approve
# SA 계정 생성, EBS add-on을 추가해서 스토리지 사용가능
eksctl create iamserviceaccount \
--name ebs-csi-controller-sa \
--namespace kube-system \
--cluster myeks \
--attach-policy-arn arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy \
--approve \
--role-only \
--role-name AmazonEKS_EBS_CSI_DriverRole
# ACCOUNT_ID 확인
aws sts get-caller-identity --output text
# ID 넣고 진행
eksctl create addon --name aws-ebs-csi-driver --cluster myeks --service-account-role-arn arn:aws:iam::<ACCOUNT_ID>:role/AmazonEKS_EBS_CSI_DriverRole --force
# 네임스페이스 생성 및 레포 생성
kubectl create namespace jenkins
helm repo add jenkinsci https://charts.jenkins.io
helm repo update
# 작업할 디렉터리 생성
mkdir jenkins_with_eks
cd jenkins_with_eks
# 파일 작성, 계정 및 플러그인 자동 구성
vi jenkins-values.yaml
controller:
tag: "lts-jdk11"
serviceType: LoadBalancer
installPlugins:
- kubernetes
- workflow-aggregator
- git
- configuration-as-code
- pipeline-stage-view
adminPassword: "P@ssw0rd"
persistence:
storageClass: "gp2"
# 설치 진행
helm install jenkins jenkinsci/jenkins -n jenkins -f jenkins-values.yaml
# 젠킨스 배포 확인
kubectl get all -n jenkins
플러그인 설치가 오래걸려서 배포하는데 시간이 걸림
# 서비스 IP 확인, 포트 8080
kubectl get svc
젠킨스관리 -> 노드관리 -> Configure Clouds를 보면 쿠버네티스가 자동으로 세팅되어 있다. test connection 또한 잘 된다.
원리: jenkins 파드-> api서버에 통신을 하기위한 주소가 Kubernetes URL에 나와있다. 이 주소는
default 네임스페이스에 존재하는 kubernetes란 서비스이다. 엔드포인트로 확인했을 때 나와있는 IP가 API서버를 의미. jenkins에 권한이 있는 SA가 세팅되어있고 그 권한으로 api서버에 접근하는 것이다.
cd ~/git/source-maven-java-spring-hello-webapp
vi Jenkinsfile-k8s
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3-openjdk-8
command: ['sleep']
args: ['infinity']
'''
}
}
triggers {
pollSCM '* * * * *'
}
parameters {
string name: 'IMAGE_NAME', defaultValue: 'hello-world'
string name: 'IMAGE_REGISTRY_ACCOUNT', defaultValue: 'shinhyeonsik'
}
stages {
stage('SCM Checkout') {
steps {
container('maven') {
git branch: 'main', url: 'git@github.com:hsshin0602/source-maven-java-spring-hello-webapp.git'
}
}
}
stage('Build Maven Project') {
steps {
container('maven') {
sh 'mvn clean package -DskipTests=true'
}
}
}
stage('Test Maven Project') {
steps {
container('maven') {
sh 'mvn test'
}
}
}
}
}
쿠버네티스 기반에 설치된 젠킨스와 젠킨스 에이전트는 파드롤 실행되고, 이런 파드에서 이미지 빌드가 되어야 되는 상황이다. 하지만 쿠버네티스 파드 내에서는 도커 데몬에 접근하기 위한 권한이 없기 때문에 도커 명령을 실행시키지 못한다. eks dind는 보안에 좋지 못하기 때문에 사용하지 않는다.
해결방안
kaniko: 도커 데몬 없이 이미지 만들기, 구글 오픈소스 프로젝트로 도커 데몬에 의존없이 이미지를 만들 수 있다.
kaniko 깃 저장소
Kubernetes Secret: Kubernetes 클러스터에서 kaniko를 실행하려면 표준 실행 Kubernetes 클러스터와 최종 이미지를 푸시하는 데 필요한 인증이 포함된 Kubernetes 암호가 필요
# 시크릿 생성
kubectl create secret docker-registry regcred -n jenkins \
--docker-username=shinhyeonsik \
--docker-password=<토큰값> \
--docker-server=https://index.docker.io/v1/
vi Jenkinsfile-k8s
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3-openjdk-8
command: ['sleep']
args: ['infinity']
- name: kaniko
iamge: gcr.io/kaniko-project/executor:debug
command: ['sleep']
args: ['infinity']
volumeMounts:
- name: registry-credentials
mountPath: /kaniko/.docker
volumes:
- name: registry-credentials
secret:
secretName: regcred
items:
- key: .dockerconfigjson
path: config.json
'''
}
}
triggers {
pollSCM '* * * * *'
}
...
stage('Build and Tag Docker Image') {
steps {
container('kaniko') {
sh "executor --dockerfile=Dockerfile \
--context=dir://${env.WORKSPACE} \
--destination=${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:${env.BUILD_NOMBER} \
--destination=${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:latest"
}
}
}
}
}
kaniko 이미지는 gcr.io/kaniko-project/executor:debug, 디버깅이 가능한 이미지를 추천한다.
젠킨스에서 CD도 가능하지만 쿠버네티스에 오브젝트를 배포하는 오픈 소스 도구인 Argo CD를 사용할 것이다.
선언적 GitOps지속적 배포
GitOps란?
Git 저장소에 선언적인 쿠버네티스 오브젝트(리소스,yaml형식) 존재
해당 소스를 쿠버네티스 클러스터에 배포
오브젝트 소스 종류
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 로드밸런서 외부용 IP 확인
kubectl get svc -n argocd
# 로드밸런서로 바꿈
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
# 패스워드 찾기
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
kaniko를 이용해서 이미지를 빌드하고 push까지 진행하였다. 이후 이미지를 사용할 수 있도록 deployment와 서비스를 실행할 파일을 만들 별도의 레포지토리를 파서 따로 만든다.
새로운 git repo , workdir 생성 및 remote로 연결
cd ~/git
mkdir hello-kube
cd hello-kube
git init
vi deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: default
name: hello-world
spec:
replicas: 2
selector:
matchLabels:
app: hello-world
template:
metadata:
labels:
app: hello-world
spec:
containers:
- name: hello-world
image: shinhyeonsik/hello-world:4 # 내가 만든 이미지 작성
ports:
- containerPort: 8080
vi services.yaml
apiVersion: v1
kind: Service
metadata:
namespace: default
name: hello-world
spec:
type: LoadBalancer
selector:
app: hello-world
ports:
- protocol: TCP
port: 80
targetPort: 8080
git add.
git branch -M main
git commit -m 'Init'
git remote add origin git@github.com:hsshin0602/hello-kube.git
git push -u origin main
앱을 생성하게 되면 동기화 되었다고 나오는데 깃 저장소에 있는 매니페스트 파일을 ArgoCD가 가져왔다는 것을 의미한다.
git 저장소의 yaml파일들이 실행되어 자동으로 파드와 서비스, deplyment 등이 만들어진다.
[이미지]
배포용 저장소에 있는 deployment의 컨테이너 이미지를 새로 생성한 이미지로 자동으로 교체를 해야한다. 이를 젠킨스파일에서 배포용 저장소를 가져오고 파일을 수정한 뒤 다시 푸쉬를 진행해주면 된다.
vi Jenkinsfile-k8s
...
stage('Update Kubernetes Manifests') {
steps {
container('maven') {
git branch: 'main', url: 'https://github.com/hsshin0602/hello-kube.git'
sh 'sed -i "s/image:.*/image: ${IMAGE_REGISTRY_ACCOUNT}\\/${IMAGE_NAME}:${BUILD_NUMBER}/g" deployment.yaml'
sh 'git add deployment.yaml'
sh 'git config --global user.name shin'
sh 'git config --global user.email hsshin0602@naver.com'
sh 'git commit -m "Jenkins Build Number - ${BUILD_NUMBER}"'
withCredentials([gitUsernamePassword(credentialsId: 'github-credential', gitToolName: 'Default')]) {
sh 'git push origin main'
}
}
}
}
git저장소에 push하기 위해선 jenkins가 git저장소에 접근할 수 있는 권한이 필요하다.
git-> settings -> developer settings
personal access token에서 새로운 token(classic) 생성(repo권한만 체크) 후 값 저장
jenkins관리 -> manage credential -> global -> add credential
kind: Username with password
username : hsshin0602 (git사용자 이름)
password : git에서 생성한 토큰 복사
id : github_credential (구분하기 위한 이름)
최종적으로 빌드를 하게되면 도커허브에 이미지가 새롭게 생성되고 이후 deployment.yaml의 컨테이너의 이미지를 결정하는 부분을 새 이미지로 변경한 뒤 commit과 push가 된다.
deployment로 배포된 파드와 해당 파드를 외부로 노출시키는 로드밸런서 타입의 서비스가 존재한다. 브라우저에 로드밸런서의 외부용ip/hello-world를 입력하면 원하는 웹페이지가 나온다.
jenkins파일에 쿠버네티스로 필요한 이미지를 가진 컨테이너들의 정의해둔다.
maven 서버가 필요하면 maven 이미지로 생성된 컨테이너를 임시적으로 생성하여 mvn clean package, test 등의 작업을 수행을 한다. 이후 컨테이너(= 노드)는 종료된다. 영구적으로 파드를 띄우는 것이 아닌 작업을 진행할때만 실행했다 종료하는 것이다.
이후 Dockerfile을 작성하는데 tomcat서버를 구축하고 생성된 war 실행파일을 webapps디렉터리에 복사하는 작업을 하도록 구성한다.
kaniko를 사용해 Dockerfile을 빌드하여 이미지를 생성한다.
배포용 저장소를 새롭게 만들고 안에 배포를 위한 서비스와 디플로이먼트 등 yaml파일을 만든다.(deployment ,services..) ArgoCD에 배포 용 저장소을 등록해서 이 리소스들을 배포할 수 있게 한다.
이때 개발자의 코드가 수정되어 kanino에 의해 새로운 버전의 이미지가 생성되면 ArgoCD가 모니터링하고 있는 git의 deployment.yaml의 image를 새로운 버전의 이미지로 바꾼다.
ArgoCD에서 보면 자동으로 commit , push된 deployment.yaml를 감지한다. 그 후 새로운 레플리카셋을 만들어지고 새로운 이미지가 적용된 컨테이너를 가진 파드를 생성한다. 기본적으로 deployment이기 때문에 기본 전략인 롤링 업데이트되는 것이다.
프로메테우스에서 젠킨스의 상태를 대쉬보드를 확인가능
Jenkins Prometheus Metrics Plugin