개요 ubuntu 22.04 단일 PC에 kubeadm 기반의 클러스터 환경을 구성,
k8s 운영에 필요한 기본적인 오픈소스 설치방법에 대해 설명.
nginx ingress 를 통해 Horbor, Jenkins, argoCD 에 접근할 것임으로 클라이언트 hosts 파일에 ingress url 과 k8s 클러스터 IP 를 매핑
1 2 # /etc/hosts 192.168.10.XXX core.harbor.domain jenkins.cluster.local argocd.example.com
데모코드
https://github.com/Kouzie/local-k8s
kubadm k8s v.1.24 이후부터 컨테이너 런타임인 dockershim 지원을 종료하면서 cri-docker 를 추가적으로 구성해야 한다.
설치는 아래 URL 참고
https://tech.hostway.co.kr/2022/08/30/1374/ k8s v1.24 이후부턴 containerd 등의 컨테이너 환경 사용을 권장한다.
kubadm v.1.28 + cri-docker 가 설치 완료되었다면 아래와 같이 kubeadm 클러스터를 구성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 스왑 비활성화 sudo swapoff -a # 의존성 리스트 설지 curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main" # sudo apt-get install -y kubeadm=1.28.x-00 버전지정 가능sudo apt-get install kubeadm kubelet kubectl sudo apt-mark hold kubeadm kubelet kubectl # 버전확인 kubeadm version # kubeadm version: &version.Info{Major:"1" , Minor:"28" , GitVersion:"v1.28.2 ... # Controller Node sudo kubeadm init --ignore-preflight-errors=all \ --pod-network-cidr=10.244.0.0/16 \ --apiserver-advertise-address=192.168.10.XXX \ --cri-socket /var/run/cri-dockerd.sock mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config
단순 개발환경 구성을 위해 control-plane 에서도 Pod 를 실행할 수 있도록 설정.
1 2 # taint all node kubectl taint nodes --all node-role.kubernetes.io/control-plane-
calico install CNI(Container Network Interface) 로 flannel, calico 등 여러가지가 있지만 가장 많이 사용하는 calico 를 설치
1 2 3 4 5 6 7 8 9 kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml watch kubectl get pods -n kube-system NAME READY STATUS calico-kube-controllers-7ddc4f45bc-v5rpc 1/1 Running calico-node-mbwnt 1/1 Running coredns-cfbfd9cb6-nhf5s 1/1 Running coredns-cfbfd9cb6-p7h5p 1/1 Running ...
ingress controller L7 Gateway 역할을 해주는 ingress 를 사용하기위해 ingress controller 를 설치
https://github.com/kubernetes/ingress-nginx 위치는 가끔 업데이트 되지만 baremetal 디렉토리에서 아래와 같은 파일을 찾으면 된다.deploy/static/provider/baremetal/deploy.yaml
1 2 curl https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/baremetal/deploy.yaml -o ingress-deploy.yaml kubectl apply -f ingress-deploy.yaml
random nodePort 를 고정하기 위해 아래와 같이 수정
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: v1 kind: Service metadata: ... name: ingress-nginx-controller namespace: ingress-nginx spec: ipFamilies: - IPv4 ipFamilyPolicy: SingleStack ports: - appProtocol: http name: http port: 80 nodePort: 30080 protocol: TCP targetPort: http - appProtocol: https name: https port: 443 nodePort: 30443 protocol: TCP targetPort: https ...
이제부터 NodePort 에 설정한 [30080, 30443] 포트를 통해 클러스터 내부로 라우팅되는 nginx 에 접근 가능하다.
rancher storageClass
https://github.com/rancher/local-path-provisioner
각종 DB, ObjectStorage 등의 솔루션을 k8s 위에서 운영하려면 PersistentVolume 설정이 필요하다. AWS 의 경우 EBS 같은 서비스를 사용해 PersistentVolume 을 지원하지만, 베어메탈 k8s 경우 로컬스토리지를 PersistentVolume 으로 생성해주는 pv provisioner 가 필요하다.
rancher 에서 이를 지원한다.
1 2 3 4 5 6 7 8 9 # stable 버전으로 설치 curl https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.24/deploy/local-path-storage.yaml -o local-storage.yaml kubectl apply -f local-storage.yaml # local-path storageclass 생성 확인 kubectl get storageclass NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION local-path rancher.io/local-path Delete WaitForFirstConsumer false
PersistentVolumeClaim 만 지정하면 자동으로 PersistentVolume 을 생성하고 매핑해준다.
PC의 /opt/local-path-provisioner 위치에 pv 가 생성되도록 하드코딩되어 있음으로 위치를 변경하고 싶다면 다운받은 파일에서 data.config.json 내부 값을 수정
Harbor
https://goharbor.io/ https://engineering.linecorp.com/ko/blog/harbor-for-private-docker-registry cloud native repository for Kubernetes 라고 소개하고 있다. CNCF 졸업 프로젝트인만큼 많은 서비스사에서 운용중임.
k8s 운영시 대부분 Private Docker Registry 를 직접 구축하여 사용하는 경우가 많음. SSL 연결이 필수로 요구됨으로, 공인인증 인증서 설치가 가능한 위치에 설치하는 것이 가장 쉬운방법이다.
하지만 여기에선 베어메탈 k8s 내부에 설치하는 방법을 설명.
Helm 으로 설치를 진행한다.
1 2 3 4 5 6 7 8 9 helm repo add harbor https://helm.goharbor.io # 압축파일 다운로드, harbor-1.13.0.tgz 버전 설치됨 helm fetch harbor/harbor # 압축 파일 해제 tar zxvf harbor-*.tgz # 이름변경 mv harbor harbor-helm
harbor 의 서비스에서 PVC 사용을 요구함으로 rancher 로 설치했던 storageClass 지정.values.yaml 에서 local-path storageClass 를 지정
storageClass 에 모두 local-path 로 지정한다.
1 2 3 4 5 6 7 8 9 persistence: enabled: true resourcePolicy: "keep" persistentVolumeClaim: registry: existingClaim: "" storageClass: "local-path" ...
nginx ingress controller 환경에서 돌아가기 때문에 외부접근을 허용하기 위해 아래 어노테이션 추가
1 2 3 4 5 6 7 8 9 10 ingress: hosts: core: core.harbor.domain ... annotations: kubernetes.io/ingress.class: "nginx" externalURL: https://core.harbor.domain
harbor ingress 에서 사용할 tls 10년짜리 인증서를 생성해서 사용
1 2 3 4 5 6 7 8 9 10 openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -sha512 -days 3650 \ -subj "/C=KR/ST=Seoul/L=Seoul/O=hello/OU=kouzie/CN=core.harbor.domain" \ -key ca.key \ -out ca.crt TLS_CRT=$(cat ca.crt | base64 ) \ TLS_KEY=$(cat ca.key | base64 ) \ envsubst < harbor-ca-secret.yaml | \ kubectl apply -f -
1 2 3 4 5 6 7 8 9 10 expose: type: ingress tls: enabled: true certSource: secret secret: secretName: "harbor-ca-secret" ... caSecretName: "harbor-ca-secret"
1 2 3 4 5 # namespace 생성 kubectl create ns harbor helm install harbor -f values.yaml . -n harbor watch kubectl get pods -n harbor
k8s cluster 에 harbor dns 등록 클라이언트 PC 를 포함해서 k8s 노드에서도 설치된 Harbor 의 registry 를 찾아갈 수 있어야 한다.
k8s 노드 에서 Pods 를 실행시키기 위한 이미지를 Harbor 로부터 가져옴으로
hosts 파일에 core.harbor.domain 도메인을 등록한다.
1 2 # /etc/hosts 192.168.10.XXX core.harbor.domain
실제 해당 k8s 노드 에 Harbor 가 설치되어있지 않더라도 Ingress Controller 가 알아서 라우팅 해줄것이다.
또한 k8s cluster 상에 배포된 어플리케이션이 Harbor 에 접근해야 할 경우, 예를 들어 Jenkins 와 같은 어플리케이션이 CI/CD 를 위해 Harbor 에 접근해야할 경우 Pod 내부에서도 Harbor 를 찾아갈 수 있어야 한다.
core.harbor.domain 도메인을 k8s corDNS 에 설정.
1 kubectl edit configmap coredns -n kube-system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 apiVersion: v1 data: Corefile: | .:53 { errors health { lameduck 5s } ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa ttl 30 } prometheus :9153 forward . /etc/resolv.conf { max_concurrent 1000 } hosts { 192.168.10.XXX core.harbor.domain fallthrough } cache 30 loop reload loadbalance } kind: ConfigMap ...
image push & pull 도커 이미지를 Harbor 에 push, pull 하기 전에 몇가지 알아야할 점, 설정해야할 점이 있다.
현재 Harbor 에 접근할 때 위에서 설정한 사설 인증서 가 적용된 Ingress 를 통해 Harbor 에 접근한다.Docker 클라이언트에서 https 프로토콜을 사용해 registry 에 접근할 때 공식 서명된 인증서 만 허용하기 때문에 insecure-registries 속성을 통해 인증여부 상관없이 registry 를 사용할 수 있도록 설정해야 한다.
모든 k8s 노드 에서도 해당 설정을 해야 , k8s node 에서 imagePullbackoff 같은 에러가 발생하지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "builder" : { "gc" : { "defaultKeepStorage" : "20GB" , "enabled" : true } } , "experimental" : false , "insecure-registries" : [ "https://core.harbor.domain" ] }
설정 완료 후 클라이언트PC, 노드에서 login 및 이미지 pull push 가 잘 되는지 확인
1 2 3 4 5 docker login -u admin -p Harbor12345 core.harbor.domain docker build -t hello:demo . docker tag hello:demo core.harbor.domain/library/hello:demo docker push core.harbor.domain/library/hello:demo
Docker Registry Login Secret 등록 Harbor 에서 이미지를 다운받을 때 항상 인증 시크릿 kubernetes.io/dockerconfigjson 을 namespace 별로 지정해 줘야 이미지를 다운받을 수 있다.
1 2 3 4 5 6 7 8 9 apiVersion: v1 kind: Secret metadata: name: regcred namespace: my-app-ns type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: $HARBOR_DOCKER_CONFIG_JSON
1 2 3 4 5 6 7 8 9 HARBOR_DOCKER_AUTH=$(echo -n 'admin:Harbor12345' | base64 ) \ HARBOR_DOCKER_CONFIG_JSON=$(echo -n '{"auths": {"core.harbor.domain": {"auth": "' $HARBOR_DOCKER_AUTH '"}}}' | base64 ) \ envsubst < harber-default-secret.yaml | \ kubectl apply -f - kubectl get secret -n my-app-ns NAME TYPE DATA AGE regcred kubernetes.io/dockerconfigjson 1 1m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ... template: metadata: labels: app: my-app spec: containers: - name: my-app image: core.harbor.domain/library/my-app:latest ports: - containerPort: 8080 envFrom: - secretRef: name: my-app-secret env: - name: SERVICE_PROFILE value: "dev" imagePullSecrets: - name: regcred
jenkins
https://github.com/jenkinsci/helm-charts
Helm 차트를 사용하면 Cloud Native 하게 동작하는 Jenkins 설치가 가능하다.master, agent 구조로 동작하는 Jenkins 생성이 가능하다.
1 2 3 4 5 6 7 8 helm repo add jenkins https://charts.jenkins.io helm fetch jenkins/jenkins tar zxvf jenkins-*.tgz mv jenkins jenkins-helm
Ingress, StoreClass 에 대한 설정 진행
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ingress: enabled: true paths: [] apiVersion: "networking.k8s.io/v1" labels: {} annotations: {} ingressClassName: nginx hostName: jenkins.cluster.local tls: persistence: enabled: true existingClaim: storageClass: "local-path" jenkinsUrl: "https://jenkins.cluster.local"
1 2 3 4 5 6 7 8 9 cd jenkins-helm # namespace 생성 kubectl create ns jenkins helm install jenkins -f values.yaml . -n jenkins # 비밀번호 확인 kubectl exec --namespace jenkins -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/additional/chart-admin-password && echo watch kubectl get pods -n jenkins
jenkins container 에서도 Harbor registry 에 이미지를 pull/push 하기위해 kubernetes.io/dockerconfigjson 타입 시크릿 지정.
1 2 3 4 5 6 7 8 9 apiVersion: v1 kind: Secret metadata: name: regcred namespace: jenkins type: kubernetes.io/dockerconfigjson data: .dockerconfigjson: $HARBOR_DOCKER_CONFIG_JSON
1 2 3 4 5 6 7 8 9 HARBOR_DOCKER_AUTH=$(echo -n 'admin:Harbor12345' | base64 ) \ HARBOR_DOCKER_CONFIG_JSON=$(echo -n '{"auths": {"core.harbor.domain": {"auth": "' $HARBOR_DOCKER_AUTH '"}}}' | base64 ) \ envsubst < harber-jenkins-secret.yaml | \ kubectl apply -f - kubectl get secret -n jenkins NAME TYPE DATA AGE regcred kubernetes.io/dockerconfigjson 1 1m ...
jenkins git SSL 무시
https://stackoverflow.com/questions/41930608/jenkins-git-integration-how-to-disable-ssl-certificate-validation
사내 git 서버를 사용중이고 사설 인증서 를 사용중이라면 아래와 같은 오류문구가 뜰 수 있다.
1 SSL certificate problem: self signed certificate in certificate chain
k8s helm 으로 설치한 jenkins 는 master/agent 가 나뉘어서 동작하는 구조이기 때문에master, agent 모두 git ssl 을 무시하는 환경변수 설정을 해줘야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 controller: ... initContainerEnv: - name: "GIT_SSL_NO_VERIFY" value: "true" containerEnv: - name: "GIT_SSL_NO_VERIFY" value: "true" ... agent: enabled: true ... envVars: - name: "GIT_SSL_NO_VERIFY" value: "true"
만약 PodTemplate 과 같은 jenkins pipeline 문법을 사용할 경우 values.yaml 에서 설정한 환경변수가 agent 에서 동작하지 않기 때문에, 아래와 같이 Jenkinsfile 에서 직접 환경변수 지정하는것을 권장
1 2 3 4 5 6 7 8 9 10 11 podTemplate( yaml: ''' kind: Pod ... ''' , envVars: [envVar(key: 'GIT_SSL_NO_VERIFY' , value: 'false' )], ) { node(POD_LABEL) { ... } }
Scripted Pipeline Cloud Native Jenkins 에서 Script Pipeline 을 사용해 간단한 gradle 프로젝트를 CI 하는 코드를 알아본다.
https://www.jenkins.io/doc/pipeline/steps/kubernetes/#podtemplate-define-a-podtemplate-to-use-in-the-kubernetes-plugin
helm 으로 설치한 Jenkins 에서 kubernetes-plugin 가 기본적으로 설치되어있다.
k8s 기반 Jenkins CI 가 좋은점은 빌드시에 컨테이너기반으로 동작시킬 수 있어 추가적인 플러그인을 설치할 필요가 없다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 podTemplate( yaml: ''' kind: Pod spec: containers: - name: gradle image: gradle:7.6.1-jdk17 command: ['sleep'] args: ['99d'] - name: kaniko image: gcr.io/kaniko-project/executor:v1.6.0-debug command: ['sleep'] args: ['99d'] volumeMounts: - name: registry-credentials mountPath: /kaniko/.docker volumes: - name: registry-credentials secret: secretName: regcred items: - key: .dockerconfigjson path: config.json ''' , envVars: [envVar(key: 'GIT_SSL_NO_VERIFY' , value: 'false' )], ) { node(POD_LABEL) { properties([ parameters([ string(name: 'IMAGE_REGISTRY_ACCOUNT' , defaultValue: 'core.harbor.domain/library' ), string(name: 'IMAGE_NAME' , defaultValue: 'hello' ) ]) ]) stage('Get a gradle project' ) { container('gradle' ) { stage('gradle build project' ) { def scmUrl = scm.getUserRemoteConfigs()[0 ].getUrl() echo scmUrl git branch: 'main' , credentialsId: 'kouzie-git-username' , url: scmUrl sh 'gradle build -x test' } } } stage('Kaniko build image' ) { container('kaniko' ) { sh "executor -f `pwd`/Dockerfile -c `pwd` \ --insecure --skip-tls-verify --cache=true --force \ --destination=${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:${env.BUILD_NUMBER} \ --destination=${params.IMAGE_REGISTRY_ACCOUNT}/${params.IMAGE_NAME}:latest" } } stage('Deploy' ) { withCredentials([gitUsernamePassword(credentialsId: 'kouzie-git-username' , gitToolName: 'git-tool' )]) { sh (""" sed -i 's|core.harbor.domain/library/hello:[0-9a-zA-Z]*|core.harbor.domain/library/hello:${env.BUILD_NUMBER}|g' k8s/hello.yaml git config --global --add safe.directory `pwd` git config --global http.sslVerify false git config --global user.email jenkins@test.com git config --global user.name jenkins git add k8s/hello.yaml git commit -m "update the image tag" git push origin main """ ) } } } }
Declarative Pipeline 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 kind: Pod spec: containers: - name: gradle image: gradle:7.6.1-jdk17 command: ['sleep' ] args: ['99d' ] volumes: - name: registry-credentials secret: secretName: regcred items: - key: .dockerconfigjson path: config.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 def getCurrentTime() { def currentDate = new Date() def formattedDate = currentDate.format("yyyyMMddHHmmss" ) return formattedDate } pipeline { agent { kubernetes { label 'gradle_pod' yamlFile 'k8s_stateful/KubernetesPod.yaml' } } options { disableConcurrentBuilds() } environment { CREDENTIALS_ID = 'harbor_credentials' CURRENT_TIME = getCurrentTime() } stages { stage('build service' ) { steps { script { echo "build service start" container("gradle" ) { withCredentials([usernamePassword(credentialsId: CREDENTIALS_ID, passwordVariable: 'CREDENTIALS_PASSWORD' , usernameVariable: 'CREDENTIALS_USERNAME' )]) { sh "gradle clean jib -PregistryUsername=${CREDENTIALS_USERNAME} -PregistryPassword=${CREDENTIALS_PASSWORD} -PuniqueBuildId=${CURRENT_TIME}" } } echo "build service end" } } } stage('deploy service - gitops' ) { steps { script { withCredentials([gitUsernamePassword(credentialsId: 'kouzie-git-username' , gitToolName: 'git-tool' )]) { def scmUrl = scm.getUserRemoteConfigs()[0 ].getUrl() git branch: 'main' , credentialsId: 'kouzie-git-username' , url: scmUrl sh (""" sed -i 's|core.harbor.domain/library/demo-service:[0-9a-zA-Z]*|core.harbor.domain/library/demo-service:${CURRENT_TIME}|g' k8s/demo-deploy.yaml git config --global --add safe.directory `pwd` git config --global http.sslVerify false git config --global user.email jenkins@kouzie.com git config --global user.name jenkins git add k8s/demo-deploy.yaml git commit -m "update the image tag" git push origin main """ ) } } } } } }
argoCd 작성중…
1 2 3 4 5 6 7 8 9 10 kubectl create namespace argocd helm repo add argo https://argoproj.github.io/argo-helm # 압축파일 다운로드, argo-cd-5.46.8.tgz 다운도르됨 helm fetch argo/argo-cd # 압축 파일 해제 tar zxvf argo-cd-*.tgz mv argo-cd argo-cd-helm
마찬가지로 ingress 를 통해 접근함으로 아래처럼 value.yaml 수정
1 2 3 4 5 6 7 8 9 server: ... ingress: enabled: true annotations: { kubernetes.io/ingress.class: nginx }
...
# https redirect 방지
server.insecure: true
…
1 2 3 4 5 6 7 8 9 10 11 ```shell cd argo-cd-helm # namespace 생성 kubectl create ns argocd helm install argocd -f values.yaml . -n argocd # 비밀번호 확인 kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d watch kubectl get pods -n argocd
작성중….