Terraform으로 GCP에서 Kubernetes 환경 구축하기

2021. 11. 27. 09:51·MLOps

GCP의 Free Tier는 90일간 300불을 사용할 수 있다. 300불은 GKE를 구성한다면 한 20일 정도면 크레딧을 모두 사용하게된다. 따라서 구글 게정을 새로 생성하고 GKE를 구성하는 것은 공부하는 입장에서 반복적인 일이 되곤한다..

따라서 Terraform을 이용해 이 과정을 자동화하였다. 아래의 자료는 복잡하고 길지만 실제로 GKE 클러스터를 생성하고 클러스터에 ArgoCD를 설치하기 까지의 과정은 간단하다.

Quick Start

1. 1단계 수행 - GCP 프로젝트 생성
2. 레포지토리 clone
git clone https://github.com/Ssuwani/provision-gke-with-terraform
3. project_id 입력 in terraform-provision-gke-cluster/terraform.tfvars
4. 테라폼으로 실제 클라우드에 적용
cd terraform-provision-gke-cluster 
terraform init 
terraform plan 
terraform apply

1단계 - 먼저 GCP 계정을 준비하고 Cloud SDK 설정 및 필요 API Enable

2-1단계 - 인프라스트럭처를 정의하는 HCL 언어로 리소스를 정의

2-2단계 - 테라폼 프로젝트 초기화

2-3단계 - 정의된 리소스와 인프라 상태를 대조하여 생성/변경/삭제 할 사항 나열

2-4단계 - 2-3에서 확인한 내용들을 실제 인프라에 적용

3-1단계 - 환경변수 HOSTNAME을 보여주는 웹 어플리케이션 작성

3-2단계 - 도커라이즈

3-3단계 - 2단계에서 정의한 인프라에 웹 어플리케이션 배포

4단계 - 실행 확인

1단계

GCP의 경우 신규 고객에게 90일간 300$를 사용할 수 있는 무료 체험을 제공한다. 무료 체험에서 사용할 수 있는 리소스의 제한이 있긴 하지만 간단한 프로젝트에서 충분히 사용가능 할 것이라 판단된다. 리소스의 제한은 여기를 확인하면된다.

새로운 계정을 만들고 https://cloud.google.com/ 에 접속하면 다음과 같은 화면을 확인할 수 있다.

무료로 시작하기를 클릭하면 결제 정보를 포함한 몇가지 설정 후 프로젝트가 자동으로 생성된다. 결제 정보를 입력하지만 다행히도 무료 체험판 종료 후 자동 청구되지 않음 이라는 안내를 확인할 수 있다.

프로젝트가 잘 생성되면 다음과 같은 대시보드 페이지로 리다이렉트 된다.

프로젝트 관련 설정을 CLI를 통해서 할 수 있도록 Google Cloud SDK를 설치하자. 여기에서 OS별 설치법을 확인할 수 있다. 설치가 되었다면 관리할 프로젝트를 설정하자.

gcloud init
gcloud auth application-default login

Cloud SDK의 설정이 마쳤다면 GKE 클러스터 구성 위해 필요한 GCP API를 Enable 하자.

gcloud services enable compute.googleapis.com container.googleapis.com

2-1단계

GKE Cluster 구성을 위해 terraform에서 제공하는 google provider를 사용할 수 있다.

소스코드는 ./terraform-provision-gke-cluster 에서 확인할 수 있다.

terraform-provision-gke-cluster
├── gke.tf
├── outputs.tf
├── terraform.tfvars
├── versions.tf
└── vpc.tf

google 프로바이더의 4개의 리소스만으로 GKE 클러스터를 구성할 수 있다.

  • google_compute_network
  • google_compute_subnetwork
  • google_container_cluster
  • google_container_node_pool

이에 따른 GKE 구성도이다.

먼저 HCL로 GCP 프로바이더를 정의합니다. project_id, region이 필요합니다.

provider "google" {
  project = var.project_id # hazel-service-332806
  region  = var.region # asia-northeast3
}

첫번째 리소스로 google_compute_network 입니다.

resource "google_compute_network" "vpc" {
  name                    = "gke-vpc"
  auto_create_subnetworks = "false"
}

두번째 리소스로 google_compute_subnetwork 입니다.

resource "google_compute_network" "vpc" {
  name                    = "gke-vpc"
  auto_create_subnetworks = "false"
}

세번째 리소스로 google_container_cluster 입니다.

resource "google_container_cluster" "primary" {
  name     = "${var.project_id}-gke"
  location = var.region

  remove_default_node_pool = true
  initial_node_count       = 1

  network    = google_compute_network.vpc.name
  subnetwork = google_compute_subnetwork.subnet.name
}

네번째 리소스로 google_container_node_pool 입니다. 클러스터를 구성할 실제 Worker Node 즉, VM을 생성을 정의하는 부분입니다.

resource "google_container_node_pool" "primary_nodes" {
  name       = "${google_container_cluster.primary.name}-node-pool"
  location   = var.region
  cluster    = google_container_cluster.primary.name
  node_count = var.gke_num_nodes

  node_config {
    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]

    labels = {
      env = var.project_id
    }

    machine_type = "n1-standard-1"
    tags         = ["gke-node", "${var.project_id}-gke"]
    metadata = {
      disable-legacy-endpoints = "true"
    }
  }
}

2-2단계

테라폼에서 프로바이더는 플러그인으로 관리되기 때문에 2-1에서 정의한 google 플러그인 설치해줘야 합니다.

terraform init

이후 플러그인 설치를 확인할 수 있습니다.

terraform version

# Terraform v1.0.11
# on darwin_arm64
# + provider registry.terraform.io/hashicorp/google v4.1.0

2-3단계

2-1에서 정의된 리소스와 인프라 상태를 대조하여 생성/변경/삭제 할 사항 나열해 변경될 사항들을 미리 확인할 수 있습니다.

terraform plan

아래는 plan에 따른 결과인데 제가 설정한 것들에 비해 정말 많은 인자들이 있고 기본값으로 많이 설정되어 생성될 수 있음을 알 수 있습니다. 아래 plan의 결과는 리소스를 작성할 때 많은 도움이 될 것 같다는 생각이 들어 그대로 남겨두겠습니다.

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # google_compute_network.vpc will be created
  + resource "google_compute_network" "vpc" {
      + auto_create_subnetworks         = false
      + delete_default_routes_on_create = false
      + gateway_ipv4                    = (known after apply)
      + id                              = (known after apply)
      + mtu                             = (known after apply)
      + name                            = "gke-vpc"
      + project                         = (known after apply)
      + routing_mode                    = (known after apply)
      + self_link                       = (known after apply)
    }

  # google_compute_subnetwork.subnet will be created
  + resource "google_compute_subnetwork" "subnet" {
      + creation_timestamp         = (known after apply)
      + external_ipv6_prefix       = (known after apply)
      + fingerprint                = (known after apply)
      + gateway_address            = (known after apply)
      + id                         = (known after apply)
      + ip_cidr_range              = "10.10.0.0/24"
      + ipv6_cidr_range            = (known after apply)
      + name                       = "gke-subnet"
      + network                    = "gke-vpc"
      + private_ipv6_google_access = (known after apply)
      + project                    = (known after apply)
      + purpose                    = (known after apply)
      + region                     = "asia-northeast3"
      + secondary_ip_range         = (known after apply)
      + self_link                  = (known after apply)
      + stack_type                 = (known after apply)
    }

  # google_container_cluster.primary will be created
  + resource "google_container_cluster" "primary" {
      + cluster_ipv4_cidr           = (known after apply)
      + datapath_provider           = (known after apply)
      + default_max_pods_per_node   = (known after apply)
      + enable_binary_authorization = false
      + enable_intranode_visibility = (known after apply)
      + enable_kubernetes_alpha     = false
      + enable_legacy_abac          = false
      + enable_shielded_nodes       = true
      + endpoint                    = (known after apply)
      + id                          = (known after apply)
      + initial_node_count          = 1
      + label_fingerprint           = (known after apply)
      + location                    = "asia-northeast3"
      + logging_service             = (known after apply)
      + master_version              = (known after apply)
      + monitoring_service          = (known after apply)
      + name                        = "hazel-service-332806-gke"
      + network                     = "gke-vpc"
      + networking_mode             = (known after apply)
      + node_locations              = (known after apply)
      + node_version                = (known after apply)
      + operation                   = (known after apply)
      + private_ipv6_google_access  = (known after apply)
      + project                     = (known after apply)
      + remove_default_node_pool    = true
      + self_link                   = (known after apply)
      + services_ipv4_cidr          = (known after apply)
      + subnetwork                  = "gke-subnet"
      + tpu_ipv4_cidr_block         = (known after apply)

      + addons_config {
          + cloudrun_config {
              + disabled           = (known after apply)
              + load_balancer_type = (known after apply)
            }

          + horizontal_pod_autoscaling {
              + disabled = (known after apply)
            }

          + http_load_balancing {
              + disabled = (known after apply)
            }

          + network_policy_config {
              + disabled = (known after apply)
            }
        }

      + authenticator_groups_config {
          + security_group = (known after apply)
        }

      + cluster_autoscaling {
          + enabled = (known after apply)

          + auto_provisioning_defaults {
              + oauth_scopes    = (known after apply)
              + service_account = (known after apply)
            }

          + resource_limits {
              + maximum       = (known after apply)
              + minimum       = (known after apply)
              + resource_type = (known after apply)
            }
        }

      + confidential_nodes {
          + enabled = (known after apply)
        }

      + database_encryption {
          + key_name = (known after apply)
          + state    = (known after apply)
        }

      + default_snat_status {
          + disabled = (known after apply)
        }

      + ip_allocation_policy {
          + cluster_ipv4_cidr_block       = (known after apply)
          + cluster_secondary_range_name  = (known after apply)
          + services_ipv4_cidr_block      = (known after apply)
          + services_secondary_range_name = (known after apply)
        }

      + logging_config {
          + enable_components = (known after apply)
        }

      + master_auth {
          + client_certificate     = (known after apply)
          + client_key             = (sensitive value)
          + cluster_ca_certificate = (known after apply)

          + client_certificate_config {
              + issue_client_certificate = (known after apply)
            }
        }

      + monitoring_config {
          + enable_components = (known after apply)
        }

      + network_policy {
          + enabled  = (known after apply)
          + provider = (known after apply)
        }

      + node_config {
          + disk_size_gb      = (known after apply)
          + disk_type         = (known after apply)
          + guest_accelerator = (known after apply)
          + image_type        = (known after apply)
          + labels            = (known after apply)
          + local_ssd_count   = (known after apply)
          + machine_type      = (known after apply)
          + metadata          = (known after apply)
          + min_cpu_platform  = (known after apply)
          + oauth_scopes      = (known after apply)
          + preemptible       = (known after apply)
          + service_account   = (known after apply)
          + tags              = (known after apply)
          + taint             = (known after apply)

          + gcfs_config {
              + enabled = (known after apply)
            }

          + shielded_instance_config {
              + enable_integrity_monitoring = (known after apply)
              + enable_secure_boot          = (known after apply)
            }

          + workload_metadata_config {
              + mode = (known after apply)
            }
        }

      + node_pool {
          + initial_node_count          = (known after apply)
          + instance_group_urls         = (known after apply)
          + managed_instance_group_urls = (known after apply)
          + max_pods_per_node           = (known after apply)
          + name                        = (known after apply)
          + name_prefix                 = (known after apply)
          + node_count                  = (known after apply)
          + node_locations              = (known after apply)
          + version                     = (known after apply)

          + autoscaling {
              + max_node_count = (known after apply)
              + min_node_count = (known after apply)
            }

          + management {
              + auto_repair  = (known after apply)
              + auto_upgrade = (known after apply)
            }

          + node_config {
              + disk_size_gb      = (known after apply)
              + disk_type         = (known after apply)
              + guest_accelerator = (known after apply)
              + image_type        = (known after apply)
              + labels            = (known after apply)
              + local_ssd_count   = (known after apply)
              + machine_type      = (known after apply)
              + metadata          = (known after apply)
              + min_cpu_platform  = (known after apply)
              + oauth_scopes      = (known after apply)
              + preemptible       = (known after apply)
              + service_account   = (known after apply)
              + tags              = (known after apply)
              + taint             = (known after apply)

              + gcfs_config {
                  + enabled = (known after apply)
                }

              + shielded_instance_config {
                  + enable_integrity_monitoring = (known after apply)
                  + enable_secure_boot          = (known after apply)
                }

              + workload_metadata_config {
                  + mode = (known after apply)
                }
            }

          + upgrade_settings {
              + max_surge       = (known after apply)
              + max_unavailable = (known after apply)
            }
        }

      + release_channel {
          + channel = (known after apply)
        }

      + workload_identity_config {
          + workload_pool = (known after apply)
        }
    }

  # google_container_node_pool.primary_nodes will be created
  + resource "google_container_node_pool" "primary_nodes" {
      + cluster                     = "hazel-service-332806-gke"
      + id                          = (known after apply)
      + initial_node_count          = (known after apply)
      + instance_group_urls         = (known after apply)
      + location                    = "asia-northeast3"
      + managed_instance_group_urls = (known after apply)
      + max_pods_per_node           = (known after apply)
      + name                        = "hazel-service-332806-gke-node-pool"
      + name_prefix                 = (known after apply)
      + node_count                  = 1
      + node_locations              = (known after apply)
      + operation                   = (known after apply)
      + project                     = (known after apply)
      + version                     = (known after apply)

      + management {
          + auto_repair  = (known after apply)
          + auto_upgrade = (known after apply)
        }

      + node_config {
          + disk_size_gb      = (known after apply)
          + disk_type         = (known after apply)
          + guest_accelerator = (known after apply)
          + image_type        = (known after apply)
          + labels            = {
              + "env" = "hazel-service-332806"
            }
          + local_ssd_count   = (known after apply)
          + machine_type      = "n1-standard-1"
          + metadata          = {
              + "disable-legacy-endpoints" = "true"
            }
          + oauth_scopes      = [
              + "https://www.googleapis.com/auth/logging.write",
              + "https://www.googleapis.com/auth/monitoring",
            ]
          + preemptible       = false
          + service_account   = (known after apply)
          + tags              = [
              + "gke-node",
              + "hazel-service-332806-gke",
            ]
          + taint             = (known after apply)

          + shielded_instance_config {
              + enable_integrity_monitoring = (known after apply)
              + enable_secure_boot          = (known after apply)
            }

          + workload_metadata_config {
              + mode = (known after apply)
            }
        }

      + upgrade_settings {
          + max_surge       = (known after apply)
          + max_unavailable = (known after apply)
        }
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + kubernetes_cluster_host = (known after apply)
  + kubernetes_cluster_name = "hazel-service-332806-gke"
  + project_id              = "hazel-service-332806"
  + region                  = "asia-northeast3"

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

2-4단계

terraform plan에서 아무런 에러가 나지 않는다면 실제 인프라에 적용할 준비가 된 것이다. 적용해보자.

terraform apply

terraform plan이 한번 더 수행되고 실제 인프라이 적용하기 전 한번 더 확인할 수 있는 알림이 나온다. yes를 입력해야 한다.

결과는 다음과 같다

# google_compute_network
google_compute_network.vpc: Creating...
google_compute_network.vpc: Creation complete after 22s

# google_compute_subnetwork
google_compute_subnetwork.subnet: Creating...
google_compute_subnetwork.subnet: Creation complete after 23s

# google_container_cluster
google_container_cluster.primary: Creating...
google_container_cluster.primary: Creation complete after 6m43s

# google_container_node_pool
google_container_node_pool.primary_nodes: Creating...
google_container_node_pool.primary_nodes: Creation complete after 1m25s

약 10분 정도의 시간이 소요되었다.

Kubernetes Cluster

VPC

3-1단계

Flask로 환경변수 HOSTNAME을 보여주는 웹 어플리케이션을 작성했습니다.

소스코드는 ./echo-hostname-flask 에서 확인할 수 있다.

echo-hostname-flask
├── app.py
└── dockerfile

app.py 정의

# app.py

from flask import Flask
import os

app = Flask(__name__)
hostname = os.environ["MY_NODE_NAME"]


@app.route("/")
def index():
    return f"Host : {hostname}"


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001, debug=True)

환경변수 정의 및 웹 어플리케이션 실행

export MY_NODE_NAME=$HOST

python app.py

이후 http://0.0.0.0:5001/ 에 접속해보면 다음과 같은 결과를 확인할 수 있다.

3-2단계

클라우드 환경에서 실행하기 위해 도커 이미지로 만들었습니다. Dockerfile이 정의되어야 합니다.

# dockerfile

# base image
FROM python:3.8

# install flask
RUN pip install flask

# docker 컨테이너의 / 위치에 app.py을 포함시킨다.
ADD app.py /

# 호스트와 연결할 포트를 설정, 외부에 공개합니다.
EXPOSE 5001

# 컨테이너가 시작되었을 때 실행할 스크립트 
ENTRYPOINT ["python", "/app.py"]

Docker build

docker build -t ssuwani/echo-hostname:v0.1 echo-hostname-flask/

Docker push

docker push ssuwani/echo-hostname:v0.1

3-3단계

앞서 정의한 kubernetes 인프라에 웹 어플리케이션 배포해보겠습니다.

생성된 ssuwani/echo-hostname:v0.1 을 배포할 Deployment를 정의하고 그에 따라 실행되는 pod를 외부에 노출하기 위해 Service를 정의하겠습니다.

소스코드는 ./k8s-service 에서 확인할 수 있다.

k8s-service
├── deployment.yaml
└── service.yaml

Deployment

# deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-hostname
spec:
  selector:
    matchLabels:
      run: echo-hostname
  replicas: 3
  template:
    metadata:
      labels:
        run: echo-hostname
    spec:
      containers:
      - name: echoh-hostname
        image: ssuwani/echo-hostname:v0.4
        ports:
        - containerPort: 5001
        env:
          - name: MY_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName

Service

# service.yaml

apiVersion: v1
kind: Service
metadata:
  name: echo-hostname-svc
  labels:
    run: echo-hostname-svc
spec:
  type: LoadBalancer
  ports:
  - port: 5001
    protocol: TCP
  selector:
    run: echo-hostname

Kubectl로 deployment와 service를 실행

kubectl 은 쿠버네티스 클러스터를 제어하기 위한 커맨드 라인 도구입니다. 설치는 여기서 OS 별 설치법을 확인할 수 있습니다. 간단히 다운받은 binary 파일을 PATH에 포함시키는 것만으로 kubectl을 설치할 수 있습니다.

./terraform-provision-gke-cluster 로 이동하여 아래 명령어를 실행해 kubectl 의 credentials을 설정합니다.

cd terraform-provision-gke-cluster
gcloud container clusters get-credentials $(terraform output -raw kubernetes_cluster_name) --region $(terraform output -raw region)

설정 확인을 위해 kubectl get pods 의 결과는 다음과 같다.

kubectl get pods
# No resources found in default namespace.

이제 kubectl 명령어를 통해 deployment와 service를 쿠버네티스 클러스터에서 실행합니다. 이때 다른 어플리케이션과 구분을 위해 namespace 를 생성합니다.

cd ../k8s-service/

kubectl create ns echo-hostname

kubectl apply -f deployment.yaml -n echo-hostname
kubectl apply -f service.yaml -n echo-hostname

다시 한번 실행을 확인해보자

kubectl get all -n echo-hostname


NAME                                 READY   STATUS    RESTARTS   AGE
pod/echo-hostname-689c445c5f-89ql5   1/1     Running   0          2m26s
pod/echo-hostname-689c445c5f-9fzzw   1/1     Running   0          2m26s
pod/echo-hostname-689c445c5f-dhrq6   1/1     Running   0          2m26s

NAME                        TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
service/echo-hostname-svc   LoadBalancer   10.92.11.219   34.64.152.52   5001:30642/TCP   2m3s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/echo-hostname   3/3     3            3           2m27s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/echo-hostname-689c445c5f   3         3         3       2m26s

Deployment에 정의 한대로 3개의 pods가 실행되었고 1개의 서비스가 실행된 것을 확인할 수 있다.

4단계

Service의 타입을 LoadBalancer로 정의하였다. 3-3의 결과 EXTERNAL-IP:PORT 에 접속하면 배포한 웹 어플리케이션을 확인할 수 있다.

나의 경우 http://34.64.152.52:5001/

그리고 설정한 replicas 의 통해 만들어진 pods들이 LoadBalancer를 통해 적절하게 분산되어 요청이 진행되는지 확인하겠습니다.

3개 Pods에 kubectl attach 를 통해 실행중인 컨테이너에 연결하여 로그를 확인했습니다.

Reference

  • https://learn.hashicorp.com/tutorials/terraform/gke
  • https://kubernetes.io/ko/docs/home/
  • https://kubernetes.io/ko/docs/reference/kubectl/cheatsheet/
'MLOps' 카테고리의 다른 글
  • BentoML의 0.13.1 버전과 1.0.7 버전 성능 비교
  • Airflow로 MNIST 학습 파이프라인 실행하기
  • Kubeflow 1.4 설치 with Minikube
  • [MLOps] Vertex AI에서 MNIST 학습, 배포, 서빙
ssuwani
ssuwani
  • ssuwani
    Oops!!
    ssuwani
  • 전체
    오늘
    어제
    • 분류 전체보기 (69)
      • MLOps (19)
      • 데이터 엔지니어링 (4)
      • Kubernetes (5)
      • Kafka (10)
      • 📚책 (3)
      • 라즈베리파이 (1)
      • ETC (8)
      • Python (6)
      • 언어모델 (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    LangChain
    Schema Registry
    FastAPI
    Kubernetes
    Prometheus
    RDD
    gcp
    태그1
    redis
    Docker
    producer
    MLOps
    Kubeflow
    asyncronous
    evidently ai
    Kafka
    LLM
    auto tagging
    Python
    consumer
    BentoML
    datadrift
    topic
    Github Actions
    fluentbit
    Spark
    Confluent Cloud
    mlflow
    Airflow
    태그2
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
ssuwani
Terraform으로 GCP에서 Kubernetes 환경 구축하기
상단으로

티스토리툴바