GKE에서 멀티 클러스터 메시 설정

이 가이드에서는 Mesh CA 또는 Istio CA를 사용하여 두 클러스터를 단일 Cloud Service Mesh로 조인하고 클러스터 간 부하 분산을 사용 설정하는 방법을 설명합니다. 이 프로세스를 손쉽게 확장하여 여러 개의 클러스터를 메시에 조인할 수 있습니다.

멀티 클러스터 Cloud Service Mesh 구성은 확장, 위치, 격리와 같은 중요한 기업 문제를 해결할 수 있습니다. 자세한 내용은 멀티 클러스터 사용 사례를 참조하세요.

기본 요건

이 가이드에서는 다음 요구사항을 충족하는 Google Cloud GKE 클러스터가 2개 이상 있다고 가정합니다.

프로젝트 및 클러스터 변수 설정

  1. 다음과 같은 프로젝트 ID, 클러스터 영역 또는 리전, 클러스터 이름, 컨텍스트의 환경 변수를 만듭니다.

    export PROJECT_1=PROJECT_ID_1
    export LOCATION_1=CLUSTER_LOCATION_1
    export CLUSTER_1=CLUSTER_NAME_1
    export CTX_1="gke_${PROJECT_1}_${LOCATION_1}_${CLUSTER_1}"
    
    export PROJECT_2=PROJECT_ID_2
    export LOCATION_2=CLUSTER_LOCATION_2
    export CLUSTER_2=CLUSTER_NAME_2
    export CTX_2="gke_${PROJECT_2}_${LOCATION_2}_${CLUSTER_2}"
    
  2. 새로 생성된 클러스터가 있으면 다음 gcloud 명령어로 각 클러스터에 대해 사용자 인증 정보를 가져와야 합니다. 그렇지 않으면 연결된 context를 이 가이드의 다음 단계에서 사용할 수 없습니다.

    명령어는 클러스터 유형(리전 또는 영역)에 따라 달라집니다.

    리전

    gcloud container clusters get-credentials ${CLUSTER_1} --region ${LOCATION_1}
    gcloud container clusters get-credentials ${CLUSTER_2} --region ${LOCATION_2}
    

    Zonal

    gcloud container clusters get-credentials ${CLUSTER_1} --zone ${LOCATION_1}
    gcloud container clusters get-credentials ${CLUSTER_2} --zone ${LOCATION_2}
    

방화벽 규칙 만들기

일부 경우에는 클러스터 간 트래픽이 허용되도록 방화벽 규칙을 만들어야 합니다. 예를 들어 다음과 같은 경우 방화벽 규칙을 만들어야 합니다.

  • 메시의 클러스터에 다른 서브넷을 사용하는 경우
  • pod가 443 및 15002 외의 포트를 여는 경우

GKE는 각 노드에 방화벽 규칙을 자동으로 추가하여 동일한 서브넷 내의 트래픽을 허용합니다. 메시에 여러 서브넷이 포함된 경우, 서브넷 간 트래픽을 허용하도록 방화벽 규칙을 명시적으로 설정해야 합니다. 각 서브넷에 대해 새 방화벽 규칙을 추가하여 소스 IP CIDR 블록과 모든 수신 트래픽의 타겟 포트를 허용해야 합니다.

다음 안내를 따라 프로젝트의 모든 클러스터 간 통신 또는 $CLUSTER_1$CLUSTER_2 간 통신만 허용할 수 있습니다.

  1. 클러스터 네트워크에 대한 정보를 수집합니다.

    모든 프로젝트 클러스터

    클러스터가 같은 프로젝트에 있으면 다음 명령어를 사용하여 프로젝트의 모든 클러스터 간에 통신할 수 있습니다. 프로젝트에서 노출하지 않으려는 클러스터가 있으면 특정 클러스터 탭의 명령어를 사용합니다.

    function join_by { local IFS="$1"; shift; echo "$*"; }
    ALL_CLUSTER_CIDRS=$(gcloud container clusters list --project $PROJECT_1 --format='value(clusterIpv4Cidr)' | sort | uniq)
    ALL_CLUSTER_CIDRS=$(join_by , $(echo "${ALL_CLUSTER_CIDRS}"))
    ALL_CLUSTER_NETTAGS=$(gcloud compute instances list --project $PROJECT_1 --format='value(tags.items.[0])' | sort | uniq)
    ALL_CLUSTER_NETTAGS=$(join_by , $(echo "${ALL_CLUSTER_NETTAGS}"))
    

    특정 클러스터

    다음 명령어는 $CLUSTER_1$CLUSTER_2 간 통신을 허용하며 프로젝트의 다른 클러스터를 노출하지 않습니다.

    function join_by { local IFS="$1"; shift; echo "$*"; }
    ALL_CLUSTER_CIDRS=$(for P in $PROJECT_1 $PROJECT_2; do gcloud --project $P container clusters list --filter="name:($CLUSTER_1,$CLUSTER_2)" --format='value(clusterIpv4Cidr)'; done | sort | uniq)
    ALL_CLUSTER_CIDRS=$(join_by , $(echo "${ALL_CLUSTER_CIDRS}"))
    ALL_CLUSTER_NETTAGS=$(for P in $PROJECT_1 $PROJECT_2; do gcloud --project $P compute instances list  --filter="name:($CLUSTER_1,$CLUSTER_2)" --format='value(tags.items.[0])' ; done | sort | uniq)
    ALL_CLUSTER_NETTAGS=$(join_by , $(echo "${ALL_CLUSTER_NETTAGS}"))
    
  2. 방화벽 규칙 만들기

    GKE

    gcloud compute firewall-rules create istio-multicluster-pods \
        --allow=tcp,udp,icmp,esp,ah,sctp \
        --direction=INGRESS \
        --priority=900 \
        --source-ranges="${ALL_CLUSTER_CIDRS}" \
        --target-tags="${ALL_CLUSTER_NETTAGS}" --quiet \
        --network=YOUR_NETWORK
    

    Autopilot

    TAGS=""
    for CLUSTER in ${CLUSTER_1} ${CLUSTER_2}
    do
        TAGS+=$(gcloud compute firewall-rules list --filter="Name:$CLUSTER*" --format="value(targetTags)" | uniq) && TAGS+=","
    done
    TAGS=${TAGS::-1}
    echo "Network tags for pod ranges are $TAGS"
    
    gcloud compute firewall-rules create asm-multicluster-pods \
        --allow=tcp,udp,icmp,esp,ah,sctp \
        --network=gke-cluster-vpc \
        --direction=INGRESS \
        --priority=900 --network=VPC_NAME \
        --source-ranges="${ALL_CLUSTER_CIDRS}" \
        --target-tags=$TAGS
    

엔드포인트 검색 구성

엔드포인트 검색을 구성하는 데 필요한 단계는 Fleet의 클러스터에서 선언적 API를 사용하는 것과 공개 클러스터 또는비공개 클러스터에 이를 수동으로 사용 설정하는 것 중 무엇을 선호하는지에 따라 다릅니다.

선언적 API로 클러스터 간 엔드포인트 검색 사용 설정(미리보기)

asm-options configmap의 "multicluster_mode":"connected" 구성을 적용하여 Fleet의 클러스터 간에 엔드포인트 검색을 사용 설정할 수 있습니다. 동일한 Fleet에 이 구성이 사용 설정된 클러스터는 서로 간에 상호 클러스터 서비스 검색이 자동으로 사용 설정됩니다.

이 방법은 모든 출시 채널의 관리형 Cloud Service Mesh 설치에 사용할 수 있습니다. 계속하기 전에 방화벽 규칙을 만들어야 합니다.

여러 프로젝트의 경우 버전 meshConfigtrustDomainAliasesFLEET_PROJECT_ID.svc.id.goog가 아직 없는 경우 수동으로 추가해야 합니다.

사용 설정

asm-options configmap이 클러스터에 이미 존재하는 경우 클러스터에 엔드포인트 검색을 사용 설정합니다.

kubectl patch configmap/asm-options -n istio-system --type merge -p '{"data":{"multicluster_mode":"connected"}}'

asm-options configmap이 클러스터에 아직 없는 경우에는 이를 연결된 데이터로 만들고 클러스터의 엔드포인트 검색을 사용 설정합니다.

kubectl --context ${CTX_1} create configmap asm-options -n istio-system --from-file <(echo '{"data":{"multicluster_mode":"connected"}}')

사용 중지

클러스터의 엔드포인트 검색 사용 중지:

kubectl patch configmap/asm-options -n istio-system --type merge -p '{"data":{"multicluster_mode":"manual"}}'

엔드포인트 검색을 사용 중지하지 않고 Fleet에서 클러스터를 등록 취소하면 보안 비밀이 클러스터에 남아 있을 수 있습니다. 나머지 보안 비밀은 수동으로 삭제해야 합니다.

  1. 다음 명령어를 실행하여 삭제해야 할 보안 비밀을 찾습니다.

    kubectl get secrets -n istio-system -l istio.io/owned-by=mesh.googleapis.com,istio/multiCluster=true
    
  2. 각 보안 비밀을 삭제합니다.

    kubectl delete secret SECRET_NAME
    

    남은 보안 비밀마다 이 단계를 반복합니다.

공개 클러스터 간 엔드포인트 검색 구성

GKE 클러스터 간 엔드포인트 검색을 구성하려면 asmcli create-mesh를 실행합니다. 이 명령어는 다음을 수행합니다.

  • 모든 클러스터를 동일한 Fleet에 등록합니다.
  • Fleet 워크로드 아이덴티티를 신뢰하도록 메시를 구성합니다.
  • 원격 보안 비밀을 만듭니다.

각 클러스터에 대한 URI 또는 kubeconfig 파일 경로를 지정할 수 있습니다.

클러스터 URI

다음 명령어에서 FLEET_PROJECT_ID를 Fleet 호스트 프로젝트의 프로젝트 ID로 바꾸고 클러스터 URI를 각 클러스터의 클러스터 이름, 영역 또는 리전, 프로젝트 ID로 바꿉니다. 이 예시에서는 클러스터 2개만 표시하지만 명령어를 실행하면 Fleet에 추가할 수 있는 최대 허용 클러스터 수에 따라 추가 클러스터에 엔드포인트 검색을 사용 설정할 수 있습니다.

./asmcli create-mesh \
    FLEET_PROJECT_ID \
    ${PROJECT_1}/${LOCATION_1}/${CLUSTER_1} \
    ${PROJECT_2}/${LOCATION_2}/${CLUSTER_2}

kubeconfig 파일

다음 명령어에서 FLEET_PROJECT_IDFleet 호스트 프로젝트의 프로젝트 ID로, PATH_TO_KUBECONFIG를 각 kubeconfig 파일 경로로 바꿉니다. 이 예시에서는 클러스터 2개만 표시하지만 Fleet에 추가할 수 있는 최대 허용 클러스터 수에 따라 추가 클러스터에서 엔드포인트 검색을 사용 설정할 수 있습니다.

./asmcli create-mesh \
    FLEET_PROJECT_ID \
    PATH_TO_KUBECONFIG_1 \
    PATH_TO_KUBECONFIG_2

비공개 클러스터 간 엔드포인트 디스커버리 구성

  1. API 서버가 다른 클러스터의 Cloud Service Mesh 컨트롤 플레인에 대한 클러스터에 액세스할 수 있도록 원격 보안 비밀을 구성합니다. 명령어는 Cloud Service Mesh 유형(클러스터 내 또는 관리형)에 따라 다릅니다.

    A. 클러스터 내 Cloud Service Mesh의 경우 공개 IP에 액세스할 수 없으므로 공개 IP 대신 비공개 IP를 구성해야 합니다.

    PRIV_IP=`gcloud container clusters describe "${CLUSTER_1}" --project "${PROJECT_1}" \
     --zone "${LOCATION_1}" --format "value(privateClusterConfig.privateEndpoint)"`
    
    ./istioctl x create-remote-secret --context=${CTX_1} --name=${CLUSTER_1} --server=https://${PRIV_IP} > ${CTX_1}.secret
    
    PRIV_IP=`gcloud container clusters describe "${CLUSTER_2}" --project "${PROJECT_2}" \
     --zone "${LOCATION_2}" --format "value(privateClusterConfig.privateEndpoint)"`
    
    ./istioctl x create-remote-secret --context=${CTX_2} --name=${CLUSTER_2} --server=https://${PRIV_IP} > ${CTX_2}.secret
    

    B. 관리형 Cloud Service Mesh의 경우:

    PUBLIC_IP=`gcloud container clusters describe "${CLUSTER_1}" --project "${PROJECT_1}" \
     --zone "${LOCATION_1}" --format "value(privateClusterConfig.publicEndpoint)"`
    
    ./istioctl x create-remote-secret --context=${CTX_1} --name=${CLUSTER_1} --server=https://${PUBLIC_IP} > ${CTX_1}.secret
    
    PUBLIC_IP=`gcloud container clusters describe "${CLUSTER_2}" --project "${PROJECT_2}" \
     --zone "${LOCATION_2}" --format "value(privateClusterConfig.publicEndpoint)"`
    
    ./istioctl x create-remote-secret --context=${CTX_2} --name=${CLUSTER_2} --server=https://${PUBLIC_IP} > ${CTX_2}.secret
    
  2. 클러스터에 새 보안 비밀을 적용합니다.

    kubectl apply -f ${CTX_1}.secret --context=${CTX_2}
    
    kubectl apply -f ${CTX_2}.secret --context=${CTX_1}
    

비공개 클러스터의 승인된 네트워크 구성

다음 조건이 모두 메시에 적용되는 경우에만 이 섹션을 따르세요.

여러 비공개 클러스터를 배포할 때는 각 클러스터의 Cloud Service Mesh 컨트롤 플레인에서 원격 클러스터의 GKE 컨트롤 플레인을 호출해야 합니다. 트래픽을 허용하려면 호출 클러스터의 포드 주소 범위를 원격 클러스터의 승인된 네트워크에 추가해야 합니다.

  1. 각 클러스터의 pod IP CIDR 블록을 가져옵니다.

    POD_IP_CIDR_1=`gcloud container clusters describe ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1} \
      --format "value(ipAllocationPolicy.clusterIpv4CidrBlock)"`
    
    POD_IP_CIDR_2=`gcloud container clusters describe ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2} \
      --format "value(ipAllocationPolicy.clusterIpv4CidrBlock)"`
    
  2. Kubernetes 클러스터 pod IP CIDR 블록을 원격 클러스터에 추가합니다.

    EXISTING_CIDR_1=`gcloud container clusters describe ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1} \
     --format "value(masterAuthorizedNetworksConfig.cidrBlocks.cidrBlock)"`
    gcloud container clusters update ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1} \
    --enable-master-authorized-networks \
    --master-authorized-networks ${POD_IP_CIDR_2},${EXISTING_CIDR_1//;/,}
    
    EXISTING_CIDR_2=`gcloud container clusters describe ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2} \
     --format "value(masterAuthorizedNetworksConfig.cidrBlocks.cidrBlock)"`
    gcloud container clusters update ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2} \
    --enable-master-authorized-networks \
    --master-authorized-networks ${POD_IP_CIDR_1},${EXISTING_CIDR_2//;/,}
    

    자세한 내용은 승인된 네트워크로 클러스터 만들기를 참조하세요.

  3. 승인된 네트워크가 업데이트되었는지 확인합니다.

    gcloud container clusters describe ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1} \
     --format "value(masterAuthorizedNetworksConfig.cidrBlocks.cidrBlock)"
    
    gcloud container clusters describe ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2} \
     --format "value(masterAuthorizedNetworksConfig.cidrBlocks.cidrBlock)"
    

컨트롤 플레인 전역 액세스 사용 설정

다음 조건이 모두 메시에 적용되는 경우에만 이 섹션을 따르세요.

  • 비공개 클러스터를 사용 중임
  • 메시의 클러스터에 다른 리전을 사용함

원격 클러스터의 GKE 컨트롤 플레인을 호출하려면 각 클러스터에서 Cloud Service Mesh 컨트롤 플레인을 허용하도록 컨트롤 플레인 전역 액세스를 사용 설정해야 합니다.

  1. 컨트롤 플레인 전역 액세스 사용 설정

    gcloud container clusters update ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1} \
     --enable-master-global-access
    
    gcloud container clusters update ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2} \
     --enable-master-global-access
    
  2. 컨트롤 플레인 전역 액세스가 사용 설정되었는지 확인합니다.

    gcloud container clusters describe ${CLUSTER_1} --project ${PROJECT_1} --zone ${LOCATION_1}
    
    gcloud container clusters describe ${CLUSTER_2} --project ${PROJECT_2} --zone ${LOCATION_2}
    

    출력의 privateClusterConfig 섹션에는 masterGlobalAccessConfig의 상태가 표시됩니다.

멀티 클러스터 연결 확인

이 섹션에서는 샘플 HelloWorldSleep 서비스를 멀티 클러스터 환경에 배포하여 클러스터 간 부하 분산 작동 방식을 확인하는 방법을 설명합니다.

샘플 디렉터리의 변수 설정

  1. asmcli가 다운로드된 위치로 이동하고 다음 명령어를 실행하여 ASM_VERSION을 설정합니다.

    export ASM_VERSION="$(./asmcli --version)"
    
  2. 작업 폴더를 클러스터 간 부하 분산이 작동하는지 확인하는 데 사용하는 샘플로 설정합니다. 샘플은 asmcli install 명령어에 지정된 --output_dir 디렉터리의 하위 디렉터리에 있습니다. 다음 명령어에서 OUTPUT_DIR--output_dir에 지정한 디렉터리로 변경합니다.

    export SAMPLES_DIR=OUTPUT_DIR/istio-${ASM_VERSION%+*}
    

사이드카 삽입 사용 설정

  1. 이후 단계에서 사용할 버전 라벨 값을 찾습니다. 이 단계는 Cloud Service Mesh 유형(관리형 또는 클러스터 내)에 따라 다릅니다.

    관리됨

    다음 명령어를 사용하여 이후 단계에서 사용할 버전 라벨을 찾습니다.

    kubectl get controlplanerevision -n istio-system

    결과는 다음과 유사합니다.

     NAME                RECONCILED   STALLED   AGE
     asm-managed-rapid   True         False     89d
     

    출력의 NAME 열 아래에서 버전 라벨 값을 확인합니다. 이 예시에서 값은 asm-managed-rapid입니다. 다음 섹션의 단계에서 버전 값을 사용하세요.

    클러스터 내

    다음 명령어를 사용하여 이후 단계에서 사용할 버전 라벨을 찾습니다.

    kubectl -n istio-system get pods -l app=istiod --show-labels

    결과는 다음과 유사합니다.

     NAME                                READY   STATUS    RESTARTS   AGE   LABELS
     istiod-asm-173-3-5788d57586-bljj4   1/1     Running   0          23h   app=istiod,istio.io/rev=asm-173-3,istio=istiod,pod-template-hash=5788d57586
     istiod-asm-173-3-5788d57586-vsklm   1/1     Running   1          23h   app=istiod,istio.io/rev=asm-173-3,istio=istiod,pod-template-hash=5788d57586
     

    출력의 LABELS 열 아래에서 istio.io/rev= 프리픽스 다음에 있는 istiod 버전 라벨의 값을 확인합니다. 이 예시에서 값은 asm-173-3입니다. 다음 섹션의 단계에서 버전 값을 사용하세요.

HelloWorld 서비스 설치

  1. 각 클러스터에서 샘플 네임스페이스와 서비스 정의를 만듭니다. 다음 명령어에서 REVISION을 이전 단계에서 기록한 istiod 버전 라벨로 대체합니다.

    for CTX in ${CTX_1} ${CTX_2}
    do
        kubectl create --context=${CTX} namespace sample
        kubectl label --context=${CTX} namespace sample \
            istio-injection- istio.io/rev=REVISION --overwrite
    done
    

    여기서 REVISION은 이전에 기록한 istiod 버전 라벨입니다.

    출력은 다음과 같습니다.

    label "istio-injection" not found.
    namespace/sample labeled
    

    label "istio-injection" not found.를 안전하게 무시할 수 있습니다.

  2. 두 클러스터에서 HelloWorld 서비스를 만듭니다.

    kubectl create --context=${CTX_1} \
        -f ${SAMPLES_DIR}/samples/helloworld/helloworld.yaml \
        -l service=helloworld -n sample
    
    kubectl create --context=${CTX_2} \
        -f ${SAMPLES_DIR}/samples/helloworld/helloworld.yaml \
        -l service=helloworld -n sample
    

HelloWorld v1 및 v2를 각 클러스터에 배포

  1. 나중에 클러스터 간 부하 분산을 확인하는 데 도움이 되도록 HelloWorld v1CLUSTER_1에, v2CLUSTER_2에 배포합니다.

    kubectl create --context=${CTX_1} \
      -f ${SAMPLES_DIR}/samples/helloworld/helloworld.yaml \
      -l version=v1 -n sample
    kubectl create --context=${CTX_2} \
      -f ${SAMPLES_DIR}/samples/helloworld/helloworld.yaml \
      -l version=v2 -n sample
  2. 다음 명령어를 사용하여 HelloWorld v1v2가 실행 중인지 확인합니다. 출력이 다음과 비슷한지 확인합니다.

    kubectl get pod --context=${CTX_1} -n sample
    NAME                            READY     STATUS    RESTARTS   AGE
    helloworld-v1-86f77cd7bd-cpxhv  2/2       Running   0          40s
    kubectl get pod --context=${CTX_2} -n sample
    NAME                            READY     STATUS    RESTARTS   AGE
    helloworld-v2-758dd55874-6x4t8  2/2       Running   0          40s

Sleep 서비스 배포

  1. 두 클러스터에 Sleep 서비스를 배포합니다. 이 포드는 데모용으로 인위적인 네트워크 트래픽을 생성합니다.

    for CTX in ${CTX_1} ${CTX_2}
    do
        kubectl apply --context=${CTX} \
            -f ${SAMPLES_DIR}/samples/sleep/sleep.yaml -n sample
    done
    
  2. 각 클러스터에서 Sleep 서비스가 시작될 때까지 기다립니다. 출력이 다음과 비슷한지 확인합니다.

    kubectl get pod --context=${CTX_1} -n sample -l app=sleep
    NAME                             READY   STATUS    RESTARTS   AGE
    sleep-754684654f-n6bzf           2/2     Running   0          5s
    kubectl get pod --context=${CTX_2} -n sample -l app=sleep
    NAME                             READY   STATUS    RESTARTS   AGE
    sleep-754684654f-dzl9j           2/2     Running   0          5s

클러스터 간 부하 분산 확인

HelloWorld 서비스를 여러 번 호출하고 출력을 확인하여 v1과 v2에서 번갈아 응답을 보내는지 확인합니다.

  1. HelloWorld 서비스를 호출합니다.

    kubectl exec --context="${CTX_1}" -n sample -c sleep \
        "$(kubectl get pod --context="${CTX_1}" -n sample -l \
        app=sleep -o jsonpath='{.items[0].metadata.name}')" \
        -- /bin/sh -c 'for i in $(seq 1 20); do curl -sS helloworld.sample:5000/hello; done'
    

    출력은 다음과 비슷합니다.

    Hello version: v2, instance: helloworld-v2-758dd55874-6x4t8
    Hello version: v1, instance: helloworld-v1-86f77cd7bd-cpxhv
    ...
  2. HelloWorld 서비스를 다시 호출합니다.

    kubectl exec --context="${CTX_2}" -n sample -c sleep \
        "$(kubectl get pod --context="${CTX_2}" -n sample -l \
        app=sleep -o jsonpath='{.items[0].metadata.name}')" \
        -- /bin/sh -c 'for i in $(seq 1 20); do curl -sS helloworld.sample:5000/hello; done'
    

    출력은 다음과 비슷합니다.

    Hello version: v2, instance: helloworld-v2-758dd55874-6x4t8
    Hello version: v1, instance: helloworld-v1-86f77cd7bd-cpxhv
    ...

수고하셨습니다. 멀티 클러스터 Cloud Service Mesh의 부하 분산이 성공적으로 완료되었습니다.

HelloWorld 서비스 삭제

부하 분산이 확인되면 클러스터에서 HelloWorldSleep 모드를 삭제합니다.

kubectl delete ns sample --context ${CTX_1}
kubectl delete ns sample --context ${CTX_2}