본문 바로가기
AWS

ubunut22.04 / kubernetes1.28 설치 스크립트

by yeongki0944 2025. 3. 28.

IAM Role  :  iam-role-kubernetes-master-node

# iam-policy-kubernetes-master-node

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:CreateSecret",
                "secretsmanager:GetSecretValue",
                "secretsmanager:UpdateSecret",
                "secretsmanager:DescribeSecret",
                "secretsmanager:PutSecretValue",
                "secretsmanager:DeleteSecret"
            ],
            "Resource": "arn:aws:secretsmanager:*:*:secret:k8s/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:ListSecrets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeTags",
                "ec2:DescribeRegions"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:CreateLoadBalancer",
                "elasticloadbalancing:CreateRule",
                "elasticloadbalancing:CreateTargetGroup",
                "elasticloadbalancing:DeleteListener",
                "elasticloadbalancing:DeleteLoadBalancer",
                "elasticloadbalancing:DeleteRule",
                "elasticloadbalancing:DeleteTargetGroup",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeLoadBalancers",
                "elasticloadbalancing:DescribeRules",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:ModifyListener",
                "elasticloadbalancing:ModifyLoadBalancerAttributes",
                "elasticloadbalancing:ModifyRule",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:ModifyTargetGroupAttributes",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:SetIpAddressType",
                "elasticloadbalancing:SetSecurityGroups",
                "elasticloadbalancing:SetSubnets"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:CreateSecurityGroup",
                "ec2:DeleteSecurityGroup",
                "ec2:DescribeSecurityGroups",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:AuthorizeSecurityGroupIngress"
            ],
            "Resource": "*"
        }
    ]
}

 

 

k8s-base-ami-setup.sh

#!/bin/bash

# 스크립트 실행 로그 파일 설정
LOG_FILE="/var/log/k8s-setup.log"
exec > >(tee -a ${LOG_FILE}) 2>&1

echo "======================================================"
echo "Kubernetes Base AMI 설치 스크립트 시작 - $(date)"
echo "======================================================"

# 설치 버전 변수 설정 - 쉽게 업데이트할 수 있도록 상단에 정의
CONTAINERD_VERSION="1.6.9"
RUNC_VERSION="1.1.3"
CNI_VERSION="1.1.1"
KUBERNETES_VERSION="1.28"  # 메이저.마이너 버전만 지정
REGION="us-west-1"  # 기본 리전 (스크립트 파라미터로 재정의 가능)

# 명령줄 파라미터 처리
while [[ $# -gt 0 ]]; do
    key="$1"
    case $key in
        --region)
            REGION="$2"
            shift 2
            ;;
        --containerd-version)
            CONTAINERD_VERSION="$2"
            shift 2
            ;;
        --runc-version)
            RUNC_VERSION="$2"
            shift 2
            ;;
        --cni-version)
            CNI_VERSION="$2"
            shift 2
            ;;
        --k8s-version)
            KUBERNETES_VERSION="$2"
            shift 2
            ;;
        *)
            echo "알 수 없는 옵션: $1"
            exit 1
            ;;
    esac
done

# 루트 권한 확인
if [ "$(id -u)" -ne 0 ]; then
    echo "이 스크립트는 root 권한으로 실행해야 합니다."
    exit 1
fi

# 시스템 사양 확인
echo "시스템 사양 확인 중..."
CPU_COUNT=$(nproc)
MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}' | sed 's/G//')

echo "CPU: $CPU_COUNT cores, Memory: $MEM_TOTAL MB, Disk available: ${DISK_AVAIL}GB"

if [ "$CPU_COUNT" -lt 2 ]; then
    echo "경고: Kubernetes 노드에는 최소 2개의 CPU 코어가 권장됩니다."
fi

if [ "$MEM_TOTAL" -lt 2048 ]; then
    echo "경고: Kubernetes 노드에는 최소 2GB 메모리가 권장됩니다."
fi

# 1. 시스템 업데이트
echo "[1/10] 시스템 업데이트 중..."
apt update || { echo "apt update 실패"; exit 1; }
apt upgrade -y || { echo "apt upgrade 실패"; exit 1; }

# 2. AWS CLI 설치
echo "[2/10] AWS CLI 설치 중..."
if ! command -v aws &> /dev/null; then
    apt install -y awscli || { echo "AWS CLI 설치 실패"; exit 1; }
    aws --version
    echo "AWS CLI 설치 완료"
else
    echo "AWS CLI가 이미 설치되어 있습니다."
    aws --version
fi

# AWS 기본 리전 설정
echo "AWS 기본 리전을 $REGION으로 설정합니다."
mkdir -p /root/.aws
cat > /root/.aws/config << EOF
[default]
region = $REGION
output = json
EOF

# 3. 호스트명 설정 스크립트 준비
echo "[3/10] 호스트명 설정 스크립트 준비 중..."
cat > /usr/local/bin/set-hostname.sh << 'EOF'
#!/bin/bash
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") 
INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id) 
REGION=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region) 
NAME_TAG=$(aws ec2 describe-tags --region $REGION --filters "Name=resource-id,Values=$INSTANCE_ID" "Name=key,Values=Name" --query "Tags[0].Value" --output text)

if [ -z "$NAME_TAG" ] || [ "$NAME_TAG" == "None" ]; then
    echo "EC2 인스턴스에 Name 태그가 없습니다. 인스턴스 ID를 사용합니다."
    NAME_TAG="k8s-node-$INSTANCE_ID"
fi

hostnamectl set-hostname "$NAME_TAG"
echo "호스트명이 '$NAME_TAG'로 설정되었습니다."
EOF

chmod +x /usr/local/bin/set-hostname.sh
echo "호스트명 설정 스크립트 생성 완료"

# EC2 인스턴스 시작 시 호스트명 자동 설정을 위한 서비스 등록
cat > /etc/systemd/system/set-hostname.service << EOF
[Unit]
Description=Set hostname from EC2 Name tag
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/set-hostname.sh
RemainAfterExit=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable set-hostname.service

# 호스트명 즉시 설정 (재부팅 없이)
echo "호스트명 설정 스크립트 즉시 실행 중..."
/usr/local/bin/set-hostname.sh

echo "호스트명 자동 설정 서비스 등록 및 즉시 적용 완료"

# 4. 스왑 비활성화
echo "[4/10] 스왑 비활성화 중..."
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
echo "스왑 비활성화 완료"

# 5. 커널 모듈 설정
echo "[5/10] 커널 모듈 설정 중..."
cat > /etc/modules-load.d/k8s.conf << EOF
overlay
br_netfilter
EOF

modprobe overlay || { echo "모듈 overlay 로드 실패"; }
modprobe br_netfilter || { echo "모듈 br_netfilter 로드 실패"; }

# 커널 파라미터 설정
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

sysctl --system || { echo "sysctl 설정 적용 실패"; }
echo "커널 모듈 및 파라미터 설정 완료"

# 6. containerd 설치
echo "[6/10] containerd 설치 중..."
apt install -y curl gnupg software-properties-common apt-transport-https ca-certificates || { echo "필수 패키지 설치 실패"; exit 1; }

# containerd 다운로드 및 설치
echo "containerd 버전 ${CONTAINERD_VERSION} 설치 중..."
wget -q https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz || { echo "containerd 다운로드 실패"; exit 1; }
tar Cxzvf /usr/local containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz
rm containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz

# containerd 서비스 등록
mkdir -p /usr/local/lib/systemd/system
wget -q https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -O /usr/local/lib/systemd/system/containerd.service || { echo "containerd 서비스 파일 다운로드 실패"; exit 1; }
systemctl daemon-reload
systemctl enable --now containerd || { echo "containerd 활성화 실패"; exit 1; }
echo "containerd 설치 완료"

# 7. runc 설치
echo "[7/10] runc 설치 중..."
wget -q https://github.com/opencontainers/runc/releases/download/v${RUNC_VERSION}/runc.amd64 || { echo "runc 다운로드 실패"; exit 1; }
install -m 755 runc.amd64 /usr/local/sbin/runc
rm runc.amd64
echo "runc 설치 완료"

# 8. CNI 플러그인 설치
echo "[8/10] CNI 플러그인 설치 중..."
mkdir -p /opt/cni/bin
wget -q https://github.com/containernetworking/plugins/releases/download/v${CNI_VERSION}/cni-plugins-linux-amd64-v${CNI_VERSION}.tgz || { echo "CNI 플러그인 다운로드 실패"; exit 1; }
tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v${CNI_VERSION}.tgz
rm cni-plugins-linux-amd64-v${CNI_VERSION}.tgz
echo "CNI 플러그인 설치 완료"

# 9. containerd를 CRI 런타임으로 사용하기 위한 설정
echo "[9/10] containerd 설정 중..."
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml >/dev/null
sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml

# cgroup 드라이버 설정을 systemd로 변경
if grep -q 'SystemdCgroup = true' /etc/containerd/config.toml; then
    echo "containerd의 cgroup 드라이버가 systemd로 설정되었습니다."
else
    echo "오류: containerd의 cgroup 드라이버 설정에 실패했습니다."
    exit 1
fi

# pause 이미지 저장소 설정 (선택적)
# sed -i "s|k8s.gcr.io/pause|registry.k8s.io/pause|g" /etc/containerd/config.toml

# containerd 재시작
systemctl restart containerd || { echo "containerd 재시작 실패"; exit 1; }

# containerd 설정 상태 확인
echo "containerd 상태 확인 중..."
if systemctl is-active --quiet containerd; then
    echo "containerd가 정상적으로 실행 중입니다."
else
    echo "containerd 서비스가 실행되지 않았습니다. 로그를 확인하세요."
    journalctl -u containerd --no-pager -n 20
    exit 1
fi

# 10. Kubernetes 패키지 설치
echo "[10/10] Kubernetes 패키지 설치 중..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v${KUBERNETES_VERSION}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg || { echo "Kubernetes 키 다운로드 실패"; exit 1; }
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${KUBERNETES_VERSION}/deb/ /" | tee /etc/apt/sources.list.d/kubernetes.list

# 패키지 업데이트 및 설치
apt-get update || { echo "apt update 실패"; exit 1; }
apt-get install -y kubelet kubeadm kubectl || { echo "Kubernetes 패키지 설치 실패"; exit 1; }
apt-mark hold kubelet kubeadm kubectl
echo "Kubernetes 패키지 설치 완료"

# kubelet 설정 - 노드 IP 자동 감지
cat > /etc/default/kubelet << EOF
KUBELET_EXTRA_ARGS=--node-ip=$(hostname -I | awk '{print $1}')
EOF

# 환경 확인
echo "설치된 버전 확인:"
echo -n "containerd: "
containerd --version
echo -n "runc: "
runc --version
echo -n "kubectl: "
kubectl version --client
echo -n "kubeadm: "
kubeadm version

# 추가 설정: 방화벽 확인 및 구성
echo "방화벽 상태 확인 중..."
if command -v ufw &> /dev/null; then
    ufw status
    echo "Kubernetes에 필요한 포트 개방 중..."
    # 컨트롤 플레인 노드에 필요한 포트
    ufw allow 6443/tcp  # Kubernetes API 서버
    ufw allow 2379:2380/tcp  # etcd 서버 클라이언트 API
    ufw allow 10250/tcp  # Kubelet API
    ufw allow 10251/tcp  # kube-scheduler
    ufw allow 10252/tcp  # kube-controller-manager
    # 워커 노드에 필요한 포트
    ufw allow 10250/tcp  # Kubelet API
    ufw allow 30000:32767/tcp  # NodePort 서비스
fi

# 설치 완료 메시지
echo "======================================================"
echo "Kubernetes Base AMI 설치가 완료되었습니다 - $(date)"
echo "이 인스턴스로 AMI를 생성하여 Kubernetes 클러스터의 마스터 및 워커 노드로 사용할 수 있습니다."
echo "======================================================"

 

 

k8s-master-init.sh

#!/bin/bash

# Kubernetes 마스터 노드 초기화 스크립트
# 로그 파일 설정
LOG_FILE="/var/log/k8s-master-init.log"
exec > >(tee -a ${LOG_FILE}) 2>&1

echo "======================================================"
echo "Kubernetes 마스터 노드 초기화 스크립트 시작 - $(date)"
echo "======================================================"

# 변수 설정
POD_CIDR="10.244.0.0/16"         # 파드 네트워크 CIDR
SERVICE_CIDR="10.96.0.0/12"      # 서비스 네트워크 CIDR
API_SERVER_PORT="6443"           # API 서버 포트
SECRET_NAME="k8s/join"           # AWS Secrets Manager 시크릿 이름
REGION="us-west-1"               # AWS 리전

# 사전 검증 단계
check_prerequisites() {
    echo "[1/7] 사전 요구사항 검증 중..."
    
    # 시스템 자원 검증
    CPU_COUNT=$(nproc)
    MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
    DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}' | sed 's/G//')
    
    echo "CPU: $CPU_COUNT cores, Memory: $MEM_TOTAL MB, Disk available: ${DISK_AVAIL}GB"
    
    if [ "$CPU_COUNT" -lt 2 ]; then
        echo "경고: 마스터 노드에는 최소 2개의 CPU 코어가 권장됩니다."
    fi
    
    if [ "$MEM_TOTAL" -lt 2048 ]; then
        echo "경고: 마스터 노드에는 최소 2GB 메모리가 권장됩니다."
    fi
    
    # 필수 포트 확인
    for port in $API_SERVER_PORT 10250 10251 10252 2379 2380; do
        if lsof -i:$port > /dev/null 2>&1; then
            echo "경고: 포트 $port가 이미 사용 중입니다. 확인이 필요합니다."
        fi
    done
    
    # containerd 상태 확인
    if ! systemctl is-active --quiet containerd; then
        echo "containerd가 실행 중이지 않습니다. 시작합니다."
        systemctl start containerd
        sleep 5
        
        if ! systemctl is-active --quiet containerd; then
            echo "오류: containerd를 시작할 수 없습니다. 로그를 확인하세요."
            exit 1
        fi
    fi
    
    # AWS CLI 확인
    if ! command -v aws &> /dev/null; then
        echo "AWS CLI가 설치되어 있지 않습니다. 설치를 진행합니다."
        apt-get update && apt-get install -y awscli
    fi
    
    # AWS IAM 권한 확인
    if ! aws secretsmanager get-random-password --password-length 4 --region $REGION &> /dev/null; then
        echo "경고: AWS Secrets Manager 접근 권한이 없거나 구성이 잘못되었습니다."
        echo "IAM 역할 및 권한을 확인하세요."
    fi
    
    echo "사전 요구사항 검증 완료"
}

# kubeadm init 실행
initialize_master() {
    echo "[2/7] 마스터 노드 초기화 중..."
    
    # kubeadm init 명령 실행
    kubeadm init \
        --pod-network-cidr=$POD_CIDR \
        --service-cidr=$SERVICE_CIDR \
        --upload-certs \
        --v=5
    
    # 초기화 결과 확인
    if [ $? -ne 0 ]; then
        echo "오류: kubeadm init 명령이 실패했습니다. 로그를 확인하세요."
        exit 1
    fi
    
    echo "마스터 노드 초기화 완료"
}

# kubeconfig 설정
setup_kubeconfig() {
    echo "[3/7] kubeconfig 설정 중..."
    
    # 관리자 계정용 kubeconfig 설정
    mkdir -p $HOME/.kube
    cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
    chown $(id -u):$(id -g) $HOME/.kube/config
    
    # 환경 변수 설정
    echo 'export KUBECONFIG=/etc/kubernetes/admin.conf' >> /etc/profile.d/k8s.sh
    chmod +x /etc/profile.d/k8s.sh
    
    # 일반 사용자 (ubuntu)에게도 kubeconfig 설정
    if id "ubuntu" &>/dev/null; then
        mkdir -p /home/ubuntu/.kube
        cp -i /etc/kubernetes/admin.conf /home/ubuntu/.kube/config
        chown -R ubuntu:ubuntu /home/ubuntu/.kube
        echo "ubuntu 사용자를 위한 kubeconfig 설정 완료"
    fi
    
    echo "kubeconfig 설정 완료"
}

# 네트워크 솔루션 설치 (Calico)
install_network_solution() {
    echo "[4/7] 네트워크 솔루션 설치 중..."
    
    # Calico 네트워크 솔루션 설치
    kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
    
    echo "네트워크 솔루션 설치 요청 완료"
    
    # 네트워크 솔루션 설치 확인
    echo "네트워크 파드 실행 상태 확인 중..."
    attempt=0
    max_attempts=30
    
    while [ $attempt -lt $max_attempts ]; do
        calico_pods_ready=$(kubectl get pods -n kube-system -l k8s-app=calico-node -o jsonpath='{.items[*].status.containerStatuses[0].ready}' | grep -o "true" | wc -l)
        calico_pods_total=$(kubectl get pods -n kube-system -l k8s-app=calico-node --no-headers | wc -l)
        
        if [ "$calico_pods_ready" -eq "$calico_pods_total" ] && [ "$calico_pods_total" -gt 0 ]; then
            echo "모든 Calico 네트워크 파드가 준비되었습니다."
            break
        fi
        
        echo "Calico 네트워크 파드 준비 중... ($calico_pods_ready/$calico_pods_total) 시도: $attempt/$max_attempts"
        attempt=$((attempt+1))
        sleep 10
    done
    
    if [ $attempt -eq $max_attempts ]; then
        echo "경고: Calico 네트워크 파드가 시간 내에 준비되지 않았습니다."
    fi
    
    echo "네트워크 솔루션 설치 완료"
}

# 조인 정보 추출 및 AWS Secrets Manager 저장
save_join_info() {
    echo "[5/7] 클러스터 조인 정보 저장 중..."
    
    # 조인 토큰 추출
    JOIN_COMMAND=$(kubeadm token create --print-join-command)
    
    if [ -z "$JOIN_COMMAND" ]; then
        echo "오류: 조인 명령어를 생성할 수 없습니다."
        exit 1
    fi
    
    # 마스터 노드 IP 주소 가져오기
    TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
    MASTER_PRIVATE_IP=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/local-ipv4)
    
    if [ -z "$MASTER_PRIVATE_IP" ]; then
        echo "경고: EC2 메타데이터에서 IP 주소를 가져올 수 없습니다."
        MASTER_PRIVATE_IP=$(hostname -I | awk '{print $1}')
        echo "대체 방법으로 IP 주소를 가져왔습니다: $MASTER_PRIVATE_IP"
    fi
    
    # AWS Secrets Manager 시크릿 JSON 생성
    SECRET_JSON=$(cat <<EOF
{
  "join": "$JOIN_COMMAND",
  "master_private_ip": "$MASTER_PRIVATE_IP",
  "created_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
)
    
    # AWS Secrets Manager에 정보 저장
    echo "AWS Secrets Manager에 정보 저장 중..."
    
    # 시크릿이 이미 존재하는지 확인
    if aws secretsmanager describe-secret --secret-id "$SECRET_NAME" --region "$REGION" &> /dev/null; then
        # 시크릿 업데이트
        aws secretsmanager update-secret \
            --secret-id "$SECRET_NAME" \
            --secret-string "$SECRET_JSON" \
            --region "$REGION"
    else
        # 새 시크릿 생성
        aws secretsmanager create-secret \
            --name "$SECRET_NAME" \
            --description "Kubernetes 클러스터 조인 정보" \
            --secret-string "$SECRET_JSON" \
            --region "$REGION"
    fi
    
    if [ $? -ne 0 ]; then
        echo "경고: AWS Secrets Manager에 정보를 저장하는 데 실패했습니다."
        echo "조인 정보: $JOIN_COMMAND"
        echo "마스터 IP: $MASTER_PRIVATE_IP"
    else
        echo "AWS Secrets Manager에 정보가 성공적으로 저장되었습니다."
    fi
    
    echo "클러스터 조인 정보 저장 완료"
}

# 클러스터 상태 검증
validate_cluster() {
    echo "[6/7] 클러스터 상태 검증 중..."
    
    # 필수 시스템 파드 확인
    echo "시스템 파드 상태 확인 중..."
    kubectl get pods -n kube-system
    
    # CoreDNS 파드 확인
    coredns_pods_ready=$(kubectl get pods -n kube-system -l k8s-app=kube-dns -o jsonpath='{.items[*].status.containerStatuses[0].ready}' | grep -o "true" | wc -l)
    coredns_pods_total=$(kubectl get pods -n kube-system -l k8s-app=kube-dns --no-headers | wc -l)
    
    if [ "$coredns_pods_ready" -eq "$coredns_pods_total" ] && [ "$coredns_pods_total" -gt 0 ]; then
        echo "CoreDNS 파드가 정상적으로 동작 중입니다."
    else
        echo "경고: CoreDNS 파드가 아직 준비되지 않았습니다."
    fi
    
    # 노드 상태 확인
    node_status=$(kubectl get nodes -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}')
    if [ "$node_status" = "True" ]; then
        echo "마스터 노드가 Ready 상태입니다."
    else
        echo "경고: 마스터 노드가 아직 Ready 상태가 아닙니다."
    fi
    
    # 간단한 테스트 파드 배포
    echo "테스트 파드 배포 중..."
    cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
  labels:
    app: test
spec:
  containers:
  - name: nginx
    image: nginx:latest
  tolerations:
  - key: node-role.kubernetes.io/master
    effect: NoSchedule
EOF
    
    # 테스트 파드 상태 확인
    attempt=0
    max_attempts=10
    
    while [ $attempt -lt $max_attempts ]; do
        pod_status=$(kubectl get pod test-pod -o jsonpath='{.status.phase}')
        
        if [ "$pod_status" = "Running" ]; then
            echo "테스트 파드가 정상적으로 실행 중입니다."
            break
        fi
        
        echo "테스트 파드 준비 중... 상태: $pod_status, 시도: $attempt/$max_attempts"
        attempt=$((attempt+1))
        sleep 5
    done
    
    if [ $attempt -eq $max_attempts ]; then
        echo "경고: 테스트 파드가 시간 내에 Running 상태가 되지 않았습니다."
    fi
    
    # 테스트 파드 삭제
    kubectl delete pod test-pod
    
    echo "클러스터 상태 검증 완료"
}

# 마스터 노드 요약 정보 표시
print_summary() {
    echo "[7/7] 마스터 노드 설정 요약:"
    
    # Kubernetes 버전 정보
    echo "Kubernetes 버전: $(kubectl version --short | grep Server)"
    
    # 노드 상태
    echo "노드 상태:"
    kubectl get nodes
    
    # 파드 상태
    echo "시스템 파드 상태:"
    kubectl get pods -n kube-system
    
    # 조인 정보
    TOKEN=$(kubeadm token list | grep -v EXPIRES | head -n 1 | awk '{print $1}')
    HASH=$(openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //')
    MASTER_IP=$(hostname -I | awk '{print $1}')
    
    echo "워커 노드 조인 정보:"
    echo "kubeadm join ${MASTER_IP}:${API_SERVER_PORT} --token ${TOKEN} --discovery-token-ca-cert-hash sha256:${HASH}"
    
    echo "AWS Secrets Manager 정보:"
    echo "Secret Name: $SECRET_NAME, Region: $REGION"
    
    # 설치된 CNI 플러그인
    echo "네트워크 솔루션: Calico"
}

# 메인 로직 시작
echo "Kubernetes 마스터 노드 초기화 프로세스 시작..."

# 1. 사전 검증 단계
check_prerequisites

# 2. kubeadm init 실행
initialize_master

# 3. kubeconfig 설정
setup_kubeconfig

# 4. 네트워크 솔루션 설치
install_network_solution

# 5. 조인 정보 추출 및 AWS Secrets Manager 저장
save_join_info

# 6. 클러스터 상태 검증
validate_cluster

# 7. 요약 정보 표시
print_summary

echo "======================================================"
echo "Kubernetes 마스터 노드 초기화 스크립트 완료 - $(date)"
echo "======================================================"

 

 

k8s-worker-init.sh

#!/bin/bash

# Kubernetes 워커 노드 자동 조인 스크립트 (JSON 파싱 개선 버전)

# 로그 파일 설정
LOG_FILE="/var/log/k8s-join.log"
exec > >(tee -a ${LOG_FILE}) 2>&1

echo "======================================================"
echo "Kubernetes 워커 노드 자동 조인 스크립트 시작 - $(date)"
echo "======================================================"

# 변수 설정
SECRET_NAME="k8s/join"
REGION="us-west-1"
MAX_RETRIES=5
RETRY_INTERVAL=10  # 초 단위
NODE_LABELS="node-role.kubernetes.io/worker=true"  # 노드 라벨

# 명령줄 파라미터 처리
while [[ $# -gt 0 ]]; do
    key="$1"
    case $key in
        --region)
            REGION="$2"
            shift 2
            ;;
        --secret-name)
            SECRET_NAME="$2"
            shift 2
            ;;
        --labels)
            NODE_LABELS="$2"
            shift 2
            ;;
        --max-retries)
            MAX_RETRIES="$2"
            shift 2
            ;;
        --retry-interval)
            RETRY_INTERVAL="$2"
            shift 2
            ;;
        *)
            echo "알 수 없는 옵션: $1"
            exit 1
            ;;
    esac
done

# 루트 권한 확인
if [ "$(id -u)" -ne 0 ]; then
    echo "이 스크립트는 root 권한으로 실행해야 합니다."
    exit 1
fi

# AWS CLI 확인 및 설치
check_aws_cli() {
    echo "[1/6] AWS CLI 확인 중..."
    
    if ! command -v aws &> /dev/null; then
        echo "AWS CLI가 설치되어 있지 않습니다. 설치를 진행합니다."
        apt-get update && apt-get install -y awscli
        
        if [ $? -ne 0 ]; then
            echo "오류: AWS CLI 설치에 실패했습니다."
            return 1
        fi
    fi
    
    # AWS 기본 리전 설정
    mkdir -p /root/.aws
    cat > /root/.aws/config << EOF
[default]
region = $REGION
output = json
EOF
    
    echo "AWS CLI가 설치되어 있습니다. 버전: $(aws --version)"
    return 0
}

# 시스템 및 노드 준비 상태 확인
check_node_readiness() {
    echo "[2/6] 노드 준비 상태 확인 중..."
    
    # 시스템 사양 확인
    CPU_COUNT=$(nproc)
    MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
    DISK_AVAIL=$(df -h / | awk 'NR==2 {print $4}' | sed 's/G//')
    
    echo "CPU: $CPU_COUNT cores, Memory: $MEM_TOTAL MB, Disk available: ${DISK_AVAIL}GB"
    
    if [ "$CPU_COUNT" -lt 2 ]; then
        echo "경고: Kubernetes 노드에는 최소 2개의 CPU 코어가 권장됩니다."
    fi
    
    if [ "$MEM_TOTAL" -lt 2048 ]; then
        echo "경고: Kubernetes 노드에는 최소 2GB 메모리가 권장됩니다."
    fi
    
    # containerd 상태 확인
    if ! systemctl is-active --quiet containerd; then
        echo "containerd가 실행 중이지 않습니다. 시작합니다."
        systemctl start containerd
        sleep 5
        
        if ! systemctl is-active --quiet containerd; then
            echo "오류: containerd를 시작할 수 없습니다."
            return 1
        fi
    fi
    
    # kubelet 상태 확인
    if ! systemctl is-active --quiet kubelet; then
        echo "kubelet이 실행 중이지 않습니다. 시작합니다."
        systemctl start kubelet
        sleep 5
    fi
    
    # 필수 포트 확인
    for port in 10250 30000-32767; do
        if echo $port | grep -q "-"; then
            # 포트 범위인 경우
            start_port=$(echo $port | cut -d'-' -f1)
            end_port=$(echo $port | cut -d'-' -f2)
            echo "포트 범위 $start_port-$end_port는 자동으로 검증할 수 없습니다."
        else
            # 단일 포트인 경우
            if lsof -i:$port > /dev/null 2>&1; then
                echo "경고: 포트 $port가 이미 사용 중입니다. Kubernetes 구성 요소와 충돌할 수 있습니다."
            fi
        fi
    done
    
    echo "노드가 준비되었습니다."
    return 0
}

# AWS Secrets Manager에서 마스터 노드 정보 가져오기 (개선된 JSON 파싱)
get_master_info() {
    echo "[3/6] AWS Secrets Manager에서 마스터 노드 정보 가져오기..."
    
    local attempt=1
    while [ $attempt -le $MAX_RETRIES ]; do
        echo "시도 $attempt/$MAX_RETRIES..."
        
        # Secrets Manager에서 시크릿 값 가져오기
        local secret_value=$(aws secretsmanager get-secret-value \
            --region ${REGION} \
            --secret-id ${SECRET_NAME} \
            --query "SecretString" \
            --output text 2>/dev/null)
        
        # 명령어 실행 상태 확인
        if [ $? -ne 0 ] || [ -z "$secret_value" ]; then
            echo "시크릿 값을 가져오는 데 실패했습니다. $RETRY_INTERVAL초 후 재시도합니다."
            sleep $RETRY_INTERVAL
            attempt=$((attempt+1))
            continue
        fi
        
        echo "시크릿 값을 성공적으로 가져왔습니다."
        
        # jq가 있는지 확인하고, 없으면 설치
        if ! command -v jq &> /dev/null; then
            echo "jq를 설치합니다..."
            apt-get update && apt-get install -y jq
        fi
        
        # jq를 사용하여 JSON 파싱
        if command -v jq &> /dev/null; then
            echo "jq를 사용하여 JSON 파싱 중..."
            MASTER_IP=$(echo "$secret_value" | jq -r '.master_private_ip')
            JOIN_COMMAND=$(echo "$secret_value" | jq -r '.join')
            CREATED_AT=$(echo "$secret_value" | jq -r '.created_at')
        else
            echo "jq를 사용할 수 없어 대체 방법으로 파싱 중..."
            # 이스케이프된 줄바꿈 제거 후 파싱
            secret_value=$(echo "$secret_value" | tr -d '\n')
            MASTER_IP=$(echo "$secret_value" | grep -o '"master_private_ip": *"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"')
            JOIN_COMMAND=$(echo "$secret_value" | grep -o '"join": *"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"')
            CREATED_AT=$(echo "$secret_value" | grep -o '"created_at": *"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"')
        fi
        
        if [ -n "$MASTER_IP" ] && [ -n "$JOIN_COMMAND" ]; then
            echo "마스터 IP 주소: $MASTER_IP"
            echo "조인 명령어: $JOIN_COMMAND"
            echo "조인 명령어 생성 시간: $CREATED_AT"
            
            # 토큰 생성 시간과 현재 시간 비교 (24시간 이상이면 경고)
            local created_timestamp=$(date -d "$CREATED_AT" +%s 2>/dev/null)
            local current_timestamp=$(date +%s)
            
            if [ -n "$created_timestamp" ] && [ $((current_timestamp - created_timestamp)) -gt 86400 ]; then
                echo "경고: 조인 토큰이 24시간 이상 지났습니다. 만료되었을 수 있습니다."
            fi
            
            return 0
        else
            echo "마스터 노드 정보를 파싱할 수 없습니다. 원본 JSON: $secret_value"
            sleep $RETRY_INTERVAL
            attempt=$((attempt+1))
        fi
    done
    
    echo "오류: 최대 재시도 횟수($MAX_RETRIES)를 초과했습니다."
    return 1
}

# 호스트 이름 및 /etc/hosts 설정
setup_hostname_and_hosts() {
    echo "[4/6] 호스트 이름 및 /etc/hosts 설정 중..."
    
    # 호스트 이름 설정 (이미 set-hostname.service에서 처리되었을 수 있음)
    current_hostname=$(hostname)
    echo "현재 호스트 이름: $current_hostname"
    
    if [[ "$current_hostname" == "ip-"* || "$current_hostname" == "localhost" ]]; then
        echo "기본 호스트 이름이 감지되었습니다. set-hostname.sh 스크립트를 실행합니다."
        if [ -x /usr/local/bin/set-hostname.sh ]; then
            /usr/local/bin/set-hostname.sh
            echo "호스트 이름이 업데이트되었습니다: $(hostname)"
        else
            echo "경고: 호스트 이름 설정 스크립트를 찾을 수 없습니다."
        fi
    fi
    
    # /etc/hosts 파일에 마스터 노드 추가 또는 업데이트
    if [ -n "$MASTER_IP" ]; then
        if grep -q "k8s-master" /etc/hosts; then
            echo "호스트 파일에 k8s-master 항목이 이미 있습니다. 업데이트합니다."
            sed -i "/k8s-master/c\\$MASTER_IP k8s-master" /etc/hosts
        else
            echo "호스트 파일에 k8s-master 항목을 추가합니다."
            echo "$MASTER_IP k8s-master" >> /etc/hosts
        fi
        
        # 호스트 파일 확인
        echo "현재 /etc/hosts 파일:"
        cat /etc/hosts | grep k8s-master
    else
        echo "경고: 마스터 IP가 없어 /etc/hosts 파일을 업데이트할 수 없습니다."
        return 1
    fi
    
    return 0
}

# 클러스터 조인
join_cluster() {
    echo "[5/6] Kubernetes 클러스터 조인 시도 중..."
    
    if [ -z "$JOIN_COMMAND" ]; then
        echo "오류: 조인 명령어가 없습니다."
        return 1
    fi
    
    # 이미 조인되어 있는지 확인
    if [ -f "/etc/kubernetes/kubelet.conf" ]; then
        echo "경고: 이 노드는 이미 Kubernetes 클러스터에 조인되어 있는 것 같습니다."
        echo "재조인하려면 먼저 'kubeadm reset'을 실행하세요."
        return 0
    fi
    
    echo "실행할 조인 명령어: $JOIN_COMMAND"
    
    # 노드 라벨 추가 (--node-labels는 kubelet에 전달됨)
    local node_labels_arg=""
    if [ -n "$NODE_LABELS" ]; then
        node_labels_arg="--node-labels=$NODE_LABELS"
    fi
    
    # 조인 명령어에 kubelet 인수 추가
    local join_cmd_with_labels="$JOIN_COMMAND"
    if [ -n "$node_labels_arg" ]; then
        if [[ "$JOIN_COMMAND" == *"--discovery"* ]]; then
            # --discovery 인수 뒤에 노드 라벨 추가
            join_cmd_with_labels="${JOIN_COMMAND} $node_labels_arg"
        else
            echo "경고: 조인 명령어 형식이 예상과 다릅니다. 노드 라벨을 추가하지 않습니다."
        fi
    fi
    
    # 조인 명령어 실행
    local attempt=1
    while [ $attempt -le $MAX_RETRIES ]; do
        echo "조인 시도 $attempt/$MAX_RETRIES..."
        
        eval "$join_cmd_with_labels"
        
        if [ $? -eq 0 ]; then
            echo "클러스터 조인 성공!"
            return 0
        else
            echo "조인 실패. $RETRY_INTERVAL초 후 재시도합니다."
            sleep $RETRY_INTERVAL
            attempt=$((attempt+1))
        fi
    done
    
    echo "오류: 최대 재시도 횟수($MAX_RETRIES)를 초과했습니다."
    return 1
}

# 클러스터 조인 상태 확인
verify_join_status() {
    echo "[6/6] 클러스터 조인 상태 확인 중..."
    
    # kubelet 설정 파일 확인
    if [ ! -f "/etc/kubernetes/kubelet.conf" ]; then
        echo "오류: kubelet.conf 파일이 없습니다. 클러스터 조인에 실패했을 수 있습니다."
        return 1
    fi
    
    # kubelet 서비스 상태 확인
    if ! systemctl is-active --quiet kubelet; then
        echo "오류: kubelet 서비스가 실행되지 않고 있습니다."
        systemctl status kubelet --no-pager
        return 1
    fi
    
    echo "kubelet 서비스가 정상적으로 실행 중입니다."
    
    # CNI 네트워크 인터페이스 확인
    if ! ip a | grep -q "cni"; then
        echo "경고: CNI 네트워크 인터페이스가 아직 설정되지 않았습니다."
        echo "이는 정상일 수 있으며, 클러스터 네트워킹이 완전히 설정되는 데 몇 분 정도 걸릴 수 있습니다."
    else
        echo "CNI 네트워크 인터페이스가 설정되었습니다."
    fi
    
    echo "노드 정보 요약:"
    echo "호스트 이름: $(hostname)"
    echo "IP 주소: $(hostname -I | awk '{print $1}')"
    echo "Kubernetes 버전: $(kubelet --version)"
    
    echo "조인 프로세스가 완료되었습니다. 마스터 노드에서 'kubectl get nodes'를 실행하여 이 노드가 표시되는지 확인하세요."
    return 0
}

# 메인 로직 시작
echo "Kubernetes 워커 노드 자동 조인 프로세스 시작..."

# 1. AWS CLI 확인
check_aws_cli || { echo "오류: AWS CLI 설정에 실패했습니다."; exit 1; }

# 2. 노드 준비 상태 확인
check_node_readiness || { echo "오류: 노드 준비 상태 확인에 실패했습니다."; exit 1; }

# 3. 마스터 노드 정보 가져오기
get_master_info || { echo "오류: 마스터 노드 정보를 가져오는 데 실패했습니다."; exit 1; }

# 4. 호스트 이름 및 /etc/hosts 설정
setup_hostname_and_hosts || { echo "오류: 호스트 설정에 실패했습니다."; exit 1; }

# 5. 클러스터 조인
join_cluster || { echo "오류: 클러스터 조인에 실패했습니다."; exit 1; }

# 6. 조인 상태 확인
verify_join_status || { echo "경고: 조인 상태 확인에 실패했습니다."; }

echo "======================================================"
echo "Kubernetes 워커 노드 자동 조인 스크립트 완료 - $(date)"
echo "======================================================"

 

https://yg-kubernetes-config.s3.amazonaws.com/k8s-base-ami-setup.sh?AWSAccessKeyId=ASIAYE6ZLHHTPYLVRLDR&Signature=g0qrIX1HOKhILmBzirrKXq5CV7Y%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEPb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMSJHMEUCIGkqFNJyZPilBWiO8%2Fac8QYuCKf6W0HxIlgvoT4DPJRJAiEA%2B5CPNx7ZoEbncFvk7gEBrd%2BZCa9m2t3PDYi15vxUCRIquAUIXxAAGgw1NjA0MTIxNzg5MTgiDA2Zc%2Fyt2bM9MAMk5SqVBTWl%2B76vVwv8TAbaxWo%2BEb5v7PpPyVUasTt1DLRhUyQ0FNfydafPKCnuHdnEiD%2Fuh%2FoMJnySL0yMb36wRclJkRzYNGsRAMfs5kqdanr8T0TDg9FVKlQQZnbVsimE5vm0pgjxHlxueC%2BioJnkZyVdXRjCbdeKUWR0y%2Blgbe0f1eEElcsRaCm5DVh37VLjDPY4cwyNHKbWSI5B8F55eJ7XgvlfprkGB2GvRfoH%2BkMQtJNp%2FrYcMMNwR6LqCBsE9u1sjJh%2BhZ5ISfM%2B6Sr8HmU2DMU7badYPRB996eEWyFm%2Bi9zJmlHiadDMM5h503yo6NhQMB9tkVd76QgjXxyaqRjmI1yhNCj%2B6TZ19l5%2FnbPmIV9D872V%2Bk0LO7eMQ0Tm5VlFa4P1TqA%2BgEW31TThBKn9BtUcfDH9FeM%2BOJEhRSFVeL03YuTV0aIVDdo72g5A9UmAUWo3SHYkPljsizC7PdIX8%2FfxfvbBtB7xctQwJx38LTc%2FgqGuFtm3iuoqQMJ6cXegH7byT8%2BS7DtoM5tIjSFUSXARhIPMMvGJukGFKthWYpF1mmq78eNCairXsRwLD2O11FdKD5dGpo1fwDt608%2FAS1rWaxkPXPEy0pZxJ6oYbajjtpGqbCb73AfdwAyATLmtVB9LM5Bfhjse6d%2BU3pLyQm3QMDeZycY3uzKavind2AeFxbAYKwPt%2F%2Fw3yK%2FIHN%2Fj3GRRi2pD7Ztqfa9%2FUiwZycgUnCZXLSVr4QeyvDVT3hHF5B8n0xooM3RWCursFl9ZvUMmNiVxChUTUN80K4DKXTwO%2FX1Go7WQENGQIlTEIl7hd1PE96px55BNzxK%2FhFOYXWzY3cXbJjULDJyNU5zB4aKLFdbT%2FJ%2BREGgDuXkbz7zoySpogAw0cWavwY6sQHiJSyzgNgbSjIrEHvBU72iAAHSs4bLUef1l9Dcrr4ZNBH5DWxqxDIAlMPFVFkbwkYYfPlZRTrgV%2BarmlmTlWeGz3HoLp0vhH9L7jz%2Fl3wDrA%2FpfV9EkrUxZxrZCWJTcw7W2qOp1BG0QQGjReeQumBSBDVpZ1OimLWFRa2CzUWtU6CGLcSl28wzEYd93YclpkG0Je9MhbERGdOQvoFlQ%2BRDdLXnaQ4ghI4ilnsJnNu8srQ%3D&Expires=1774705904

 

 

https://yg-kubernetes-config.s3.amazonaws.com/k8s-worker-init.sh?AWSAccessKeyId=ASIAYE6ZLHHTPYLVRLDR&Signature=G46aizSpgmDWnA54VJU3OQPacE4%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEPb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMSJHMEUCIGkqFNJyZPilBWiO8%2Fac8QYuCKf6W0HxIlgvoT4DPJRJAiEA%2B5CPNx7ZoEbncFvk7gEBrd%2BZCa9m2t3PDYi15vxUCRIquAUIXxAAGgw1NjA0MTIxNzg5MTgiDA2Zc%2Fyt2bM9MAMk5SqVBTWl%2B76vVwv8TAbaxWo%2BEb5v7PpPyVUasTt1DLRhUyQ0FNfydafPKCnuHdnEiD%2Fuh%2FoMJnySL0yMb36wRclJkRzYNGsRAMfs5kqdanr8T0TDg9FVKlQQZnbVsimE5vm0pgjxHlxueC%2BioJnkZyVdXRjCbdeKUWR0y%2Blgbe0f1eEElcsRaCm5DVh37VLjDPY4cwyNHKbWSI5B8F55eJ7XgvlfprkGB2GvRfoH%2BkMQtJNp%2FrYcMMNwR6LqCBsE9u1sjJh%2BhZ5ISfM%2B6Sr8HmU2DMU7badYPRB996eEWyFm%2Bi9zJmlHiadDMM5h503yo6NhQMB9tkVd76QgjXxyaqRjmI1yhNCj%2B6TZ19l5%2FnbPmIV9D872V%2Bk0LO7eMQ0Tm5VlFa4P1TqA%2BgEW31TThBKn9BtUcfDH9FeM%2BOJEhRSFVeL03YuTV0aIVDdo72g5A9UmAUWo3SHYkPljsizC7PdIX8%2FfxfvbBtB7xctQwJx38LTc%2FgqGuFtm3iuoqQMJ6cXegH7byT8%2BS7DtoM5tIjSFUSXARhIPMMvGJukGFKthWYpF1mmq78eNCairXsRwLD2O11FdKD5dGpo1fwDt608%2FAS1rWaxkPXPEy0pZxJ6oYbajjtpGqbCb73AfdwAyATLmtVB9LM5Bfhjse6d%2BU3pLyQm3QMDeZycY3uzKavind2AeFxbAYKwPt%2F%2Fw3yK%2FIHN%2Fj3GRRi2pD7Ztqfa9%2FUiwZycgUnCZXLSVr4QeyvDVT3hHF5B8n0xooM3RWCursFl9ZvUMmNiVxChUTUN80K4DKXTwO%2FX1Go7WQENGQIlTEIl7hd1PE96px55BNzxK%2FhFOYXWzY3cXbJjULDJyNU5zB4aKLFdbT%2FJ%2BREGgDuXkbz7zoySpogAw0cWavwY6sQHiJSyzgNgbSjIrEHvBU72iAAHSs4bLUef1l9Dcrr4ZNBH5DWxqxDIAlMPFVFkbwkYYfPlZRTrgV%2BarmlmTlWeGz3HoLp0vhH9L7jz%2Fl3wDrA%2FpfV9EkrUxZxrZCWJTcw7W2qOp1BG0QQGjReeQumBSBDVpZ1OimLWFRa2CzUWtU6CGLcSl28wzEYd93YclpkG0Je9MhbERGdOQvoFlQ%2BRDdLXnaQ4ghI4ilnsJnNu8srQ%3D&Expires=1774705935

 

 

https://yg-kubernetes-config.s3.amazonaws.com/k8s-master-init.sh?AWSAccessKeyId=ASIAYE6ZLHHTPYLVRLDR&Signature=DCV4lDwDUFD8o4z5fo154XvErfA%3D&x-amz-security-token=IQoJb3JpZ2luX2VjEPb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMSJHMEUCIGkqFNJyZPilBWiO8%2Fac8QYuCKf6W0HxIlgvoT4DPJRJAiEA%2B5CPNx7ZoEbncFvk7gEBrd%2BZCa9m2t3PDYi15vxUCRIquAUIXxAAGgw1NjA0MTIxNzg5MTgiDA2Zc%2Fyt2bM9MAMk5SqVBTWl%2B76vVwv8TAbaxWo%2BEb5v7PpPyVUasTt1DLRhUyQ0FNfydafPKCnuHdnEiD%2Fuh%2FoMJnySL0yMb36wRclJkRzYNGsRAMfs5kqdanr8T0TDg9FVKlQQZnbVsimE5vm0pgjxHlxueC%2BioJnkZyVdXRjCbdeKUWR0y%2Blgbe0f1eEElcsRaCm5DVh37VLjDPY4cwyNHKbWSI5B8F55eJ7XgvlfprkGB2GvRfoH%2BkMQtJNp%2FrYcMMNwR6LqCBsE9u1sjJh%2BhZ5ISfM%2B6Sr8HmU2DMU7badYPRB996eEWyFm%2Bi9zJmlHiadDMM5h503yo6NhQMB9tkVd76QgjXxyaqRjmI1yhNCj%2B6TZ19l5%2FnbPmIV9D872V%2Bk0LO7eMQ0Tm5VlFa4P1TqA%2BgEW31TThBKn9BtUcfDH9FeM%2BOJEhRSFVeL03YuTV0aIVDdo72g5A9UmAUWo3SHYkPljsizC7PdIX8%2FfxfvbBtB7xctQwJx38LTc%2FgqGuFtm3iuoqQMJ6cXegH7byT8%2BS7DtoM5tIjSFUSXARhIPMMvGJukGFKthWYpF1mmq78eNCairXsRwLD2O11FdKD5dGpo1fwDt608%2FAS1rWaxkPXPEy0pZxJ6oYbajjtpGqbCb73AfdwAyATLmtVB9LM5Bfhjse6d%2BU3pLyQm3QMDeZycY3uzKavind2AeFxbAYKwPt%2F%2Fw3yK%2FIHN%2Fj3GRRi2pD7Ztqfa9%2FUiwZycgUnCZXLSVr4QeyvDVT3hHF5B8n0xooM3RWCursFl9ZvUMmNiVxChUTUN80K4DKXTwO%2FX1Go7WQENGQIlTEIl7hd1PE96px55BNzxK%2FhFOYXWzY3cXbJjULDJyNU5zB4aKLFdbT%2FJ%2BREGgDuXkbz7zoySpogAw0cWavwY6sQHiJSyzgNgbSjIrEHvBU72iAAHSs4bLUef1l9Dcrr4ZNBH5DWxqxDIAlMPFVFkbwkYYfPlZRTrgV%2BarmlmTlWeGz3HoLp0vhH9L7jz%2Fl3wDrA%2FpfV9EkrUxZxrZCWJTcw7W2qOp1BG0QQGjReeQumBSBDVpZ1OimLWFRa2CzUWtU6CGLcSl28wzEYd93YclpkG0Je9MhbERGdOQvoFlQ%2BRDdLXnaQ4ghI4ilnsJnNu8srQ%3D&Expires=1774705960