Jenkins, Kubespray, Terraform을 활용한 Openstack 상 Kubernetes 및 모니터링 환경 구축 자동화 - 3

변재한·2023년 12월 5일
0
post-thumbnail

Overview


졸업과제 프로젝트를 진행하며 Openstack 플랫폼 상에 쿠버네티스 클러스터와 모니터링 환경을 자동화했던 과정을 정리한 글이다.
졸업과제에서는 쿠버네티스 환경에 대한 사용자의 입력(마스터 노드 수, 워커 노드 수, 노드의 이미지, 노드의 하드웨어 명세 등) 기반 동적인 구축을 진행하며, 본 포스트에선 자동화 구축을 위한 사전설정 및 프로세스를 기술하려고 한다. 기술을 많이 사용하고, 볼륨이 길다 보니 기술에 대한 설명은 하지 않고 필요한 부분만 설명을 진행하고자 한다.

아래 그림은 졸업과제에서 만든 인프라 관리 시스템의 구성도이다.
Orchstrator - Monitor - Openstack 파트가 본 포스트에서 해당한다.


5. Nexus

왜 Nexus?

갑자기, 왜 Nexus가 나오는지 궁금할 것 같다.
앞선 포스팅에서 Kubespray를 언급했고 해당 도구는 쿠버네티스 클러스터 구축 시 필요한 Image들을 Docker Hub에서 가져와서 진행을 하게 된다. 이때, Nginx 또는 다른 Image를 Pull하는 과정에서 Docker Pull Limit 정책에 의한 에러가 발생함을 발견했다.

Docker Pull Limit 정책이란 ?

Docker의 pull limit 정책은 Docker Hub에서 이미지를 다운로드하는 빈도에 제한을 두는 것을 의미한다. 이 정책은 2020년에 도입되었으며, Docker Hub의 무료 계정 및 유료 계정 사용자에게 적용된다.
무료 계정의 제한: Docker Hub의 무료 계정 사용자는 6시간 동안 최대 100회의 이미지 pull 요청을 할 수 있다. 이 제한은 과도한 사용을 방지하고 서비스의 안정성을 유지하기 위해 설정되었다.
https://docs.docker.com/docker-hub/download-rate-limit/

이런 연유로 관련된 Image를 가져오는 저장소가 Kubespray 코드에 명시되는 것을 발견하여, 기존 저장소 주소를 Nexus Private Registry의 주소로 바꿔주었다.

설치 및 설정

먼저, Nexus를 설치 및 설정하고 비밀번호를 이용하여 대시보드에 접속한다.
나는 Nexus의 Docker-proxy Type의 Repo를 생성해서 사용하였다.

Nexus - Docker-proxy ?
Docker 이미지 레지스트리를 프록시하는 역할을 한다. 이는 기본적으로 외부 Docker 레지스트리(Docker Hub)로부터 Docker 이미지를 캐시하고, 이를 내부 네트워크에 제공하는 기능을 수행한다.

# 8081은 Nexus 대시보드 접근 주소, 8082는 이후 생성할 Docker-proxy repo 접근 포트 
docker run --name nexus -d -p 8081:8081 -p 8082:8082 -v ~/nexus-data:/nexus-data -u root sonatype/nexus3

# Docker에게 192.168.10.20:8082 주소의 레지스트리가 SSL 인증서를 사용하지 않는 비보안 레지스트리임을 알림
vi /etc/docker/daemon.json
{
	"insecure-registries" : ["192.168.10.20:8082"]
} 

systemctl restart docker

docker start nexus

docker exec -it nexus cat /nexus-data/admin.password

로그인을 진행하고 나오는 모달에서 아래 'Enable anonymous access'를 선택한다.

이후, Docker-proxy type으로 Create Repository를 진행하고, 아래 설정을 입력한다.

마지막으로, Realms에 들어가서 Active 항목에 빨간 네모에 해당하는 녀석을 넣어주고 저장한다.

이제, Nexus Registry는 준비됐고 Kubespray에 반영만 하면 된다.

Kubespray(ver 2.22) Registry 수정

먼저, kubernetes/roles/container-engine/containerd/defaults/main.yml 부분에 아래와 같이 내가 만든 Registry 주소를 설정해준다

이후, kubespray/roles/download/defaults/main.yml 부분에 아래와 같이 저장소 정보를 수정하면 된다.

갑자기 분위기 Pull Request

위 Kubespray Registry 설정을 함에 있어, kubernetes/roles/container-engine/containerd/defaults/main.yml 영역에 containerd_insecure_registries 부분에 대한 설명이 일절 없었다.
그러던 중 kubespray/roles/download/defaults/main.yml의 아래 부분을 발견하고 이를 앞선 사진과 같이 반영하니 제대로 적용됨을 확인하였다.

# Nerdctl insecure flag set
nerdctl_extra_flags: '{%- if containerd_insecure_registries is defined and containerd_insecure_registries|length>0 -%}--insecure-registry{%- else -%}{%- endif -%}'

이전에 Opensource Contribution Academy를 진행하며, Ncloud Terraform Provider에 기여한 경험도 있고 해서 문제라고 인지한 부분을 Pull Request로 올려보았다.

아쉽게도, 나는 Ubuntu 20.04, Ubuntu 22.04 버전만 지원하는 Kubespray 2.23버전 대신, 앞선 버전 포함 16.04, 18.04 버전까지 제공을 하는 2.22버전을 사용했는데 해당 부분이 2.23 버전에서는 이미 반영이 되어 있어 Pull Request 승인이 나지 못하였다..
처음으로 외국인 개발자 분들과 댓글, 대댓글 남기는 게 재밌었는데 아쉬웠다.

6. Jenkins Pipeline

Pipeline Flow

쿠버네티스 클러스터 구축 및 모니터링 환경 구축 자동화를 위한 파이프라인은 다음 그림과 같다.
조금 FM스럽긴한데.. 졸업과제 보고서에 첨부한 그림을 가져왔다.

각 단계에 대한 간략한 설명은 다음과 같다. 아래 모든 단계가 하나의 Jenkins Job에서 행해진다.
1st Stage: Openstack 접근을 위한 변수, Jenkins 호출 시 전달받은 파라미터 등을 Setup
2nd Stage: Openstack Swift Container를 기준으로 Terraform Init
3rd Stage: Terraform 이용, Jenkins에 전달받은 파라미터를 이용하여 동적으로 인프라 구축
4th Stage: Jenkins SSH Agent가 Bastion Node에서 Kubespray를 Setup하여 쿠버네티스 클러스터 구축 진행
5th Stage: Bastion Node에 Kubectl 및 접근 설정(인증서)
6th Stage: Bastion Node를 제외한 Node들에 Node Exporter 설치 및 서비스로 등록
7th Stage: Bastion Node에 Prometheus 및 Grafana 설치 및 서비스로 등록, 이때 Prometheus에 다른 Node들의 Node Exporter Endpoint 설정

직설적인 표현은 본 포스팅을 하면서 안 쓰려고 했으나, 모든 과정이 매끄럽게 완벽하게 성공하는 파이프라인을 구축하기까지 정말 많이 테스트를 했다. 실패하면 다시 파이프라인을 가동하고, 다시 오류를 고쳐 가동하는 일련의 과정들이 힘들긴 했지만, 마지막에 성공해서 다 동작했을 때의 쾌감과 보람은 정말 이루 말할 수 없었다..

각설하고, 아래는 Jenkins Pipeline 코드이다. 중간에 "curl -X POST ~" 요청을 보내는 코드가 있는데, 이 부분은 인프라 관리 시스템의 대시보드에서 파이프라인이 어떤 상태에 도달했는지 보여주기 위한 상태 갱신을 위한 호출로 신경쓰지 않아도 된다.

def BASTION_FIP 
def MASTER_IP
def master_ips
def worker_ips

pipeline {
    agent any

    parameters {
        string(name: 'clusterName', defaultValue: 'test')
        string(name: 'nodeImage', defaultValue: 'c586196e-c122-4541-90f6-60c49b9f91c6')
        string(name: 'flavorVcpu', defaultValue: '5')
        string(name: 'flavorRam', defaultValue: '4096')
        string(name: 'flavorDisk', defaultValue: '40')
        string(name: 'masterCount', defaultValue: '1')
        string(name: 'workerCount', defaultValue: '1')
    }

    environment {
        OS_CREDENTIALS = credentials('openstack-string-cred')
        SSH_PUBLIC_KEY = credentials('jenkins-ssh-public-key-cred')
    }
   
    stages {
        stage('Setup Environment Variables') {
            steps {
                echo "Start environment variable Setup"
                script {
                    def parts = OS_CREDENTIALS.split(',')
                    if (parts.length != 6) {
                        error "Unexpected number of elements in CREDENTIALS. Expected 5, but got ${parts.length}."
                    }
                
                    env.OS_USERNAME = parts[0]
                    env.OS_PROJECT_NAME = parts[1]
                    env.OS_AUTH_URL = parts[2]
                    env.OS_REGION_NAME = parts[3]
                    env.OS_PASSWORD = parts[4]
                    env.OS_USER_DOMAIN_NAME = parts[5]
                    env.SSH_PUBLIC_KEY_CONTENT = SSH_PUBLIC_KEY
                }

                echo "Environmental variable Setup succeeded!"
            }
        }

        stage('Terraform Init') {
            steps {
                script {
                    sh """
                    git clone https://github.com/Cloud-Chain/infra-repo.git -b terraform ${params.clusterName}
                    echo "$SSH_PUBLIC_KEY_CONTENT" > ${params.clusterName}/create/public_key.pub
                    terraform init -backend-config="container=terraform-${params.clusterName}-tfstate" ${params.clusterName}/create/
                    """
                }
            }
        }

        stage('Terraform Apply') {
            steps {
                script {
                    sh """
                    terraform apply \\
                        -var 'number_of_worker_nodes=${params.workerCount}' \\
                        -var 'number_of_master_nodes=${params.masterCount}' \\
                        -var 'node_image_uuid=${params.nodeImage}' \\
                        -var 'flavor_vcpu=${params.flavorVcpu}' \\
                        -var 'flavor_ram=${params.flavorRam}' \\
                        -var 'flavor_disk=${params.flavorDisk}' \\
                        -var 'cluster_name=${params.clusterName}' \\
                        -var 'public_key_path=${params.clusterName}/create/public_key.pub' --auto-approve ${params.clusterName}/create
                    """
                }
            }
        }

        stage('Setup Kubespray On Bastion') {
            steps {
                script {
                    def floating_network_id
                    def floating_subnet_id
                    def bastion_ip
                    def k8s_network_id
                    def k8s_subnet_id

                    dir("${params.clusterName}/create") {
                        sh """
                        terraform init -backend-config="container=terraform-${params.clusterName}-tfstate"
                        """
                        
                        BASTION_FIP = sh(script: 'terraform output bastion_fips | tr -d \'[],"\\n\'', returnStdout: true).trim()
                        MASTER_IP = sh(script: "terraform output master_ips | tr -d '[]' | sed 's/\"//g' | tr -d '\\n' | cut -d ',' -f 1", returnStdout: true).trim()
                        bastion_ip = sh(script: "terraform output bastion_ip | tr -d '\"'", returnStdout: true).trim()
                        master_ips = sh(script: "terraform output master_ips | tr -d '[] ' | sed 's/\"//g' | tr -d '\\n'", returnStdout: true).trim()
                        worker_ips = sh(script: "terraform output worker_ips | tr -d '[] ' | sed 's/\"//g' | tr -d '\\n'", returnStdout: true).trim()
                        floating_network_id = sh(script: 'terraform output floating_network_id | tr -d \'"\n\'', returnStdout: true).trim()
                        floating_subnet_id = sh(script: 'terraform output floating_subnet_id | tr -d \'"\n\'', returnStdout: true).trim()
                        k8s_network_id = sh(script: 'terraform output k8s_network_id | tr -d \'"\n\'', returnStdout: true).trim()
                        k8s_subnet_id = sh(script: 'terraform output k8s_subnet_id | tr -d \'"\n\'', returnStdout: true).trim()
                        
                        // bastion이 SSH에 응답할 준비가 될 때까지 대기
                        sh """
                        count=0
                        while ! nc -z -v -w5 $BASTION_FIP 22 2>/dev/null; do
                            count=\$((count + 1))
                            if [[ \${count} -eq 15 ]]; then
                                echo "bastion SSH service에 연결 실패"
                                exit 1
                            fi
                            echo "bastion SSH service가 동작하기를 기다리는 중"
                            sleep 20
                        done
                        """
                        sh"""
                        curl -X POST http://192.168.10.20:5000/jenkins/notify-completion \\
                            -H "Content-Type: application/json" \\
                            -d '{"clusterName": "${params.clusterName}", "bastionIP": "$BASTION_FIP"}'
                        """
                        
                    }   

                    withCredentials([file(credentialsId: 'jenkins-ssh-private-key-file', variable: 'SSH_PRIVATE_KEY')]) {
                        sshagent(["ssh-key"]) {
                            sh """
                            scp -o StrictHostKeyChecking=no $SSH_PRIVATE_KEY ubuntu@${BASTION_FIP}:~/.ssh/id_rsa
                            ssh -o StrictHostKeyChecking=no ubuntu@${BASTION_FIP} ' \
                                chmod 600 ~/.ssh/id_rsa; \
                                export OS_USERNAME=${env.OS_USERNAME}; \
                                export OS_PROJECT_NAME=${env.OS_PROJECT_NAME}; \
                                export OS_AUTH_URL=${env.OS_AUTH_URL}; \
                                export OS_REGION_NAME=${env.OS_REGION_NAME}; \
                                export OS_PASSWORD=${env.OS_PASSWORD}; \
                                export OS_USER_DOMAIN_NAME=${env.OS_USER_DOMAIN_NAME}; \
                                sudo DEBIAN_FRONTEND=noninteractive apt-get update; \
                                sudo DEBIAN_FRONTEND=noninteractive apt-get install -y python3-pip; \
                                git clone https://github.com/Cloud-Chain/infra-repo.git -b kubespray kubespray; \
                                cd kubespray; \
                                cp -rfp inventory/sample inventory/${params.clusterName}; \
                                sed -i "s|# external_openstack_lbaas_enabled: true|external_openstack_lbaas_enabled: true|" inventory/${params.clusterName}/group_vars/all/openstack.yml; \
                                sed -i "s|# external_openstack_lbaas_floating_network_id: \\"Neutron network ID to get floating IP from\\"|external_openstack_lbaas_floating_network_id: \\"${floating_network_id}\\"|" inventory/${params.clusterName}/group_vars/all/openstack.yml; \
                                sed -i "s|# external_openstack_lbaas_floating_subnet_id: \\"Neutron subnet ID to get floating IP from\\"|external_openstack_lbaas_floating_subnet_id: \\"${floating_subnet_id}\\"|" inventory/${params.clusterName}/group_vars/all/openstack.yml; \
                                sed -i "s|# external_openstack_lbaas_subnet_id: \\"Neutron subnet ID to create LBaaS VIP\\"|external_openstack_lbaas_subnet_id: \\"${k8s_subnet_id}\\"|" inventory/${params.clusterName}/group_vars/all/openstack.yml; \
                                sed -i "s|# external_openstack_lbaas_network_id: \\"Neutron network ID to create LBaaS VIP\\"|external_openstack_lbaas_network_id: \\"${k8s_network_id}\\"|" inventory/${params.clusterName}/group_vars/all/openstack.yml; \
                                cd inventory/${params.clusterName}; \
                                chmod +x update-kubespray-inventory.sh; \
                                ./update-kubespray-inventory.sh ${params.clusterName} ${bastion_ip} ${master_ips} ${worker_ips}; \
                                cd ../../; \
                                /usr/bin/pip install -r requirements.txt; \
                                ~/.local/bin/ansible-playbook -i inventory/${params.clusterName}/inventory.ini --become --become-user=root -e ansible_ssh_timeout=60 cluster.yml;'
                            """
                        }
                    }
                }
            }
        }

        stage('Setup Kubectl on Bastion and Configure Cluster Access') {
            steps {
                script {
                    sshagent(["ssh-key"]) {
                        sh """
                        ssh -o StrictHostKeyChecking=no ubuntu@$BASTION_FIP ' \
                            curl -LO "https://dl.k8s.io/release/\$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"; \
                            sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl; \
                            ssh ubuntu@${MASTER_IP} sudo cat /etc/kubernetes/ssl/apiserver-kubelet-client.key > client.key; \
                            ssh ubuntu@${MASTER_IP} sudo cat /etc/kubernetes/ssl/apiserver-kubelet-client.crt > client.crt; \
                            ssh ubuntu@${MASTER_IP} sudo cat /etc/kubernetes/ssl/ca.crt > ca.crt; \
                            kubectl config set-cluster default-cluster --server=https://${MASTER_IP}:6443 --certificate-authority=ca.crt --embed-certs=true; \
                            kubectl config set-credentials default-admin --certificate-authority=ca.crt --client-key=client.key --client-certificate=client.crt --embed-certs=true; \
                            kubectl config set-context default-context --cluster=default-cluster --user=default-admin; \
                            kubectl config use-context default-context;'
                        """
                        }
                }
            }
        }

        stage('Install Node Exporter on All Nodes') {
            steps {
                script {
                    def allNodes = (master_ips + "," + worker_ips).split(",")
                    allNodes = allNodes.findAll { node -> node != "" } 
                        sshagent(["ssh-key"]) {
                            allNodes.each { node ->
                                sh """
                                ssh -o StrictHostKeyChecking=no ubuntu@$BASTION_FIP "
                                    ssh -o StrictHostKeyChecking=no ubuntu@${node} '
                                        // Install Prometheus
                                        wget https://github.com/prometheus/node_exporter/releases/download/v1.2.2/node_exporter-1.2.2.linux-amd64.tar.gz
                                        tar xvzf node_exporter-1.2.2.linux-amd64.tar.gz
                                        sudo cp node_exporter-1.2.2.linux-amd64/node_exporter /usr/local/bin/

                                        cat <<EOF | sudo tee /etc/systemd/system/node_exporter.service
[Unit]
Description=Node Exporter
After=network.target

[Service]
User=root
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF
                                        
                                        sudo systemctl daemon-reload
                                        sudo systemctl enable node_exporter
                                        sudo systemctl start node_exporter'
                                    "
                                """
                        }
                    }
                }
            }
        }

        stage('Setup Prometheus & Grafana on Bastion Node') {
            steps {
                script {
                    def formattedMasterIps = master_ips ? master_ips.split(',').collect { it + ":9100" }.join(',') : ""
                    def formattedWorkerIps = worker_ips ? worker_ips.split(',').collect { it + ":9100" }.join(',') : ""

                    def allIps = []
                    if (formattedMasterIps) allIps.add(formattedMasterIps)
                    if (formattedWorkerIps) allIps.add(formattedWorkerIps)
                    def finalIps = allIps.join(',')

                    sshagent(["ssh-key"]) {
                        sh """
                        ssh -o StrictHostKeyChecking=no ubuntu@$BASTION_FIP '
                            // Install Prometheus
                            wget https://github.com/prometheus/prometheus/releases/download/v2.30.3/prometheus-2.30.3.linux-amd64.tar.gz
                            tar xvzf prometheus-2.30.3.linux-amd64.tar.gz
                            sudo cp prometheus-2.30.3.linux-amd64/prometheus /usr/local/bin/
                            sudo cp prometheus-2.30.3.linux-amd64/promtool /usr/local/bin/
                            sudo mkdir /etc/prometheus
                            sudo cp -r prometheus-2.30.3.linux-amd64/consoles /etc/prometheus
                            sudo cp -r prometheus-2.30.3.linux-amd64/console_libraries /etc/prometheus

                            cat <<EOF | sudo tee /etc/prometheus/prometheus.yml
global:
    scrape_interval: 15s

scrape_configs:
    - job_name: '\''node'\''
      static_configs:
      - targets: ['${finalIps}']
EOF
                            cat <<EOF | sudo tee /etc/systemd/system/prometheus.service
[Unit]
Description=Prometheus Server
After=network.target

[Service]
User=root
ExecStart=/usr/local/bin/prometheus --config.file /etc/prometheus/prometheus.yml --storage.tsdb.path /var/lib/prometheus/

[Install]
WantedBy=multi-user.target
EOF
                            sudo systemctl daemon-reload
                            sudo systemctl enable prometheus
                            sudo systemctl start prometheus

                            // Install Grafana
                            sudo DEBIAN_FRONTEND=noninteractiv apt-get install -y apt-transport-https
                            sudo DEBIAN_FRONTEND=noninteractiv apt-get install -y software-properties-common wget
                            wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
                            echo "deb https://packages.grafana.com/oss/deb stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
                            sudo DEBIAN_FRONTEND=noninteractive apt update
                            sudo DEBIAN_FRONTEND=noninteractive apt install -y grafana
                            sudo systemctl enable grafana-server
                            sudo systemctl start grafana-server
                        '
                        """

                        sh """
                        curl -X POST http://192.168.10.20:5000/jenkins/notify-completion \\
                            -H "Content-Type: application/json" \\
                            -d '{"clusterName": "${params.clusterName}", "bastionIP": "$BASTION_FIP"}'
                        """
                    }
                }
            }
        }
    }
}
profile
Infra and Devops 엔지니어가 되고 싶어용

0개의 댓글