Terraform을 사용하여 GKE 프로비저닝 하기

황서희·2023년 12월 19일
0
post-thumbnail

GCP는 90일간 무료 300달러 크레딧을 제공한다. 하지만 보통 매니지드 쿠버네티스를 제공하는 시스템 (GCP의 경우에는 GKE)은 비용이 많이 들고, 이를 피하기 위해서 매번 클러스터를 새로 생성하는 것은 효율적이지 않다. 더욱 편한 실습 환경을 위해 Terraform을 통해 자동으로 클러스터를 생성할 수 있도록 했다.

1. 시작하기 전

시작하기 전에, 해당 사항들을 충족하는지 확인한다.

  1. GCP Account
  2. Google Cloud SDK (gcloud) 설치: GKE CLI를 제공한다.
  3. GKE Cluster API 활성화: 프로젝트에 GKE Cluster API를 활성화한다. 콘솔이나 해당 명령어를 통해 gcloud에서 활성화할 수 있다. gcloud services enable container.googleapis.com --project=PROJECT_ID
  4. 인증 및 자격 증명: Terraform이 GCP 계정에 액세스할 수 있도록 적절한 인증을 구성한다. Application Default Credentials (ADC) 또는 Service Account 키 파일을 사용할 수 있다. 참고
  5. Terraform과 kubectl을 로컬 머신에 설치해야 한다.

gcloud config list 명령어를 통해 현재 구성 정보를 확인할 수 있다.

2. GCP 계정에서 새로운 프로젝트 생성하기

GCP 콘솔에서 새 프로젝트 버튼을 눌러 새로운 프로젝트를 생성한다.

3. API 활성화

Compute Engine API와 Kubernetes Engine API를 활성화한다. 콘솔에서 활성화하거나, 해당 명령어를 통해 gcloud에서 활성화할 수 있다.

gcloud services enable container.googleapis.com # Kubernetes Engine
gcloud services enable compute.googleapis.com # Compute Engine

4. 서비스 계정 생성

서비스마다 별도의 서비스 계정을 만드는 것이 좋다. Terraform 전용 서비스 계정을 생성하면, 적절하게 격리 및 권한 제어를 할 수 있다.

  • GCP 콘솔에서 서비스 계정 페이지로 이동한다. (IAM 및 관리자 -> 서비스 계정)
  • 서비스 계정 생성 버튼을 클릭하고 새로운 서비스 어카운트를 생성한다.
  • 서비스 계정에 액세스 권한을 부여하고 생성 버튼을 누른다.
  • 서비스 계정이 JSON 형태로 컴퓨터에 저장된다. 이 파일에는 서비스 계정의 자격 증명이 포함되어 있다. (서비스 계정 -> 작업 -> 키 관리 -> 키 추가 -> 새 키 만들기 -> JSON)

5. gcloud SDK 설치 및 초기화, kubectl, Terraform 설치

GCP에서 Terraform 작업을 실행하고 GKE 클러스터를 생성하려면 Google Cloud SDK (gcloud) 를 설치해야 한다. 설치

설치한 후에는, gcloud init 명령어를 사용하여 초기화한다.

gcloud auth login을 통해 당신의 구글 계정을 gcloud SDK와 연동할 수 있다.

gcloud auth list
gcloud auth application-default login
gcloud config set account 'ACCOUNT'
gcloud config set project 'PROJECT_ID'

쿠버네티스를 활용하기 위해서는 kubectl을 설치해야 한다. 설치

Terraform도 설치해 주자. 설치

6. Terraform 파일 생성

생성한 폴더 내에 Terraform 파일을 생성해 준다. kubernetes-enginenetwork 모듈을 사용할 것이다.

terraform {
  # provider 파일을 통해 Terraform은 클라우드 공급자, SaaS 공급자 및 기타 API와 상호작용할 수 있다. 
  # Terraform이 프로젝트에서 사용할 특정 프로바이더와 그들의 버전을 정의하여, 구성의 일관성과 호환성을 보장한다.

  required_version = ">= 1.6" # 최소 Terraform 버전을 1.6.0 이상으로 지정
  required_providers {
    google = {
      source  = "hashicorp/google" # Google 프로바이더의 출처를 HashiCorp 레지스트리에서 "hashicorp/google"으로 지정
      version = "~> 5.7.0"         # Google 프로바이더의 버전을 5.7.0으로 지정하며, 호환 가능한 마이너 업데이트는 허용
    }
    local = {
      source  = "hashicorp/local"
      version = "2.4.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "2.24.0"
    }
  }
}
# backend.tf를 통해 Terraform의 state를 원격 저장소에 저장하도록 설정할 수 있다.
# GCP를 사용할 것이므로, GCS(Google Cloud Storage) 버킷으로 설정해 준다. 혹은 Terraform Cloud를 사용할 수도 있다.

terraform {
  backend "gcs" {
    bucket = "k8s-standard-bucket-tfstate"
    prefix = "terraform/state"
  }
}

backend.tf에 사용할 GCS 생성을 위하여 bash 스크립트를 작성하였다.

#!/bin/bash

BUCKET_NAME="BUCKET_NAME"
PROJECT_ID="PROJECT_ID"
LOCATION="ASIA-NORTHEAST3"

# GCS 버킷 생성

if gsutil ls -p $PROJECT_ID gs://$BUCKET_NAME 2>&1 | grep -q 'NotFound'; then
    echo "Creating bucket $BUCKET_NAME"
    gsutil mb -p $PROJECT_ID -l $LOCATION gs://$BUCKET_NAME
    gsutil versioning set on gs://$BUCKET_NAME
else
    echo "Bucket $BUCKET_NAME already exists."
fi

# vpc-network.tf
# VPC와 서브넷을 프로비저닝한다.
module "vpc" {
  source       = "terraform-google-modules/network/google"
  version      = "~> 8.0"
  project_id   = var.project_id
  network_name = "${var.network}-${var.env_name}"
  routing_mode = "GLOBAL"
  subnets = [
    {
      subnet_name           = "subnet-01"
      subnet_ip             = "10.10.20.0/24"
      subnet_region         = var.region
      subnet_private_access = "true"
      subnet_flow_logs      = true
      description           = "For private subnet"
    }
  ]
  # 클러스터 내부의 Pods와 Services에 IP 주소를 할당하기 위한 추가적인 IP 범위
  secondary_ranges = {
    subnet-01 = [
      {
        range_name    = var.ip_range_pods_name
        ip_cidr_range = "10.20.0.0/16"
      },
      {
        range_name    = var.ip_range_services_name
        ip_cidr_range = "10.21.0.0/16"
      }
    ]
  }
}
# main.tf
# GKE 클러스터와, 별도로 관리되는 노드 풀을 생성한다.
data "google_client_config" "default" {}

provider "kubernetes" {
  host                   = "https://${module.gke.endpoint}"
  token                  = data.google_client_config.default.access_token
  cluster_ca_certificate = base64decode(module.gke.ca_certificate)
}

resource "google_service_account" "default" {
  # google_service_account 리소스의 account_id에는 서비스 계정의
  # 전체 이메일 주소가 아니라 고유 ID 부분만 포함해야 한다.
  account_id   = "k8s-standard-architecture"
  project      = var.project_id
  display_name = "K8s Standard Architecture Service Account"
  description  = "Service account for K8s standard architecture"
}

module "gke" {
  source                     = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster"
  project_id                 = var.project_id
  name                       = var.cluster_name
  region                     = var.region
  zones                      = var.zones
  network                    = module.vpc.network_name
  subnetwork                 = module.vpc.subnets_names[0]
  ip_range_pods              = var.ip_range_pods_name
  ip_range_services          = var.ip_range_services_name
  http_load_balancing        = true
  network_policy             = true
  private_cluster_config     = true
  horizontal_pod_autoscaling = true
  filestore_csi_driver       = false
  enable_private_endpoint    = false
  enable_private_nodes       = true
  master_ipv4_cidr_block     = "10.0.0.0/28"
  deletion_protection        = false 
  # Terraform 등 다른 방법으로 클러스터가 삭제되는 것을 허용하지 않는다면 true로 설정

  node_pools = [
    {
      name            = "default-node-pool"
      machine_type    = "e2-standard-4"
      node_locations  = "asia-northeast3-b,asia-northeast3-c"
      min_count       = 2
      max_count       = 5
      disk_size_gb    = 30
      spot            = false
      image_type      = "COS_CONTAINERD"
      disk_type       = "pd-standard"
      logging_variant = "DEFAULT"
      auto_repair     = true
      auto_upgrade    = true
      service_account = "k8s-standard-architecture@${var.project_id}.iam.gserviceaccount.com"
    }
  ]

  node_pools_oauth_scopes = {
    all = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
      "https://www.googleapis.com/auth/trace.append",
      "https://www.googleapis.com/auth/service.management.readonly",
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/servicecontrol"
    ]
  }

  node_pools_labels = {
    all = {}
    default-node-pool = {
      default-node-pool = true
    }
  }

  node_pools_metadata = {
    all = {}
    default-node-pool = {
      #노드가 종료될 때, kubectl drain 명령어를 사용하여 
      # 파드를 안전하게 제거하고 필요하다면 다른 노드로 재스케줄링 되도록 하는 스크립트를 실행한다.
      shutdown-script                 = "kubectl --kubeconfig=/var/lib/kubelet/kubeconfig drain --force=true --ignore-daemonsets=true --delete-local-data \"$HOSTNAME\""
      node-pool-metadata-custom-value = "default-node-pool"
    }
  }

  node_pools_taints = {
    all = []

    default-node-pool = [
      {
        key    = "default-node-pool"
        value  = true
        effect = "PREFER_NO_SCHEDULE"
      }
    ]
  }

  node_pools_tags = {
    all = []

    default-node-pool = [
      "default-node-pool",
    ]
  }

  depends_on = [module.vpc, google_service_account.default]
}
# auth.tf
# GKE 클러스터에 대한 인증을 구성한다.

module "gke_auth" {
  source               = "terraform-google-modules/kubernetes-engine/google//modules/auth"
  version              = "29.0.0"
  project_id           = var.project_id
  location             = module.gke.location
  cluster_name         = module.gke.name
  use_private_endpoint = true
  depends_on           = [module.gke] 
  # module.gke에 의존한다. 즉 gke에 설정된 클러스터가 생성된 후 인증 정보가 설정된다.
}

# kubeconfig를 로컬 시스템에 저장하여, 쿠버네티스 클러스터에 접근할 수 있도록 한다.

resource "local_file" "kubeconfig" {
  content  = module.gke_auth.kubeconfig_raw
  filename = "kubeconfig-${var.env_name}"
}
# variables.tf
# 변수를 정의한다.

# Common variables

variable "project_id" {
  type        = string
  description = "project id"
  default     = ""

  validation {
    condition     = length(var.project_id) > 0
    error_message = "The project_id must not be empty."
  }
}

variable "region" {
  type        = string
  description = "cluster region"
  default     = ""

  validation {
    condition     = can(regex("^asia-northeast3", var.region))
    error_message = "The region must start with 'asia-northeast3'."
  }
}

variable "env_name" {
  type        = string
  description = "The environment for the GKE cluster"
  default     = "dev"
}

# Cluster variables


variable "cluster_name" {
  type        = string
  description = "name of cluster"
  default     = ""
}

variable "network" {
  type        = string
  description = "The VPC network"
  default     = "gke-network"
}

variable "zones" {
  type        = list(string)
  description = "to host cluster in"
  default     = ["asia-northeast3-a", "asia-northeast3-b", "asia-northeast3-c"]

  validation {
    condition     = length(var.zones) > 0
    error_message = "At least one zone must be specified."
  }
}

variable "ip_range_pods_name" {
  type        = string
  description = "The secondary ip ranges for pods"
  default     = "subnet-01-pods"
}

variable "ip_range_services_name" {
  type        = string
  description = "The secondary ip ranges for services"
  default     = "subnet-01-services"
}

# 변수를 정의한다. 정의한 변수에 값을 주입하기 위해서는 terraform.tfvars 파일을 이용한다.
# terraform.tfvars
project_id   = "spry-framework-406704"
cluster_name = "k8s-standard-architecture"
region       = "asia-northeast3"

# variables.tf 파일에 정의된 변수에 값을 주입한다. 보통 Variables = Value 형태로 정의한다.
# output.tf
# 리소스에서 생성된 데이터를 출력하는데 사용된다. output을 사용하면 원하는 정보를 개발환경에서도 바로 확인할 수 있다.

output "cluster_id" {
  description = "cluster id"
  value       = module.gke.cluster_id
}

output "cluster_name" {
  description = "cluster name"
  value       = module.gke.name
}

output "ca_certificate" {
  sensitive   = true
  description = "Cluster ca certificate (base64 encoded)"
  value       = module.gke.ca_certificate
}

output "cluster_type" {
  description = "cluster type (regional / zonal)"
  value       = module.gke.type
}

output "cluster_location" {
  description = "Cluster location (region if regional cluster, zone if zonal cluster)"
  value       = module.gke.location
}

output "horizontal_pod_autoscaling_enabled" {
  description = "Whether horizontal pod autoscaling enabled"
  value       = module.gke.horizontal_pod_autoscaling_enabled
}

output "http_load_balancing_enabled" {
  description = "Whether http load balancing enabled"
  value       = module.gke.http_load_balancing_enabled
}

output "master_authorized_networks_config" {
  description = "Networks from which access to master is permitted"
  value       = module.gke.master_authorized_networks_config
}

output "cluster_region" {
  description = "Cluster region"
  value       = module.gke.region
}

output "cluster_zones" {
  description = "List of zones in which the cluster resides"
  value       = module.gke.zones
}

output "cluster_endpoint" {
  sensitive   = true
  description = "Cluster endpoint"
  value       = module.gke.endpoint
}

output "min_master_version" {
  description = "Minimum master kubernetes version"
  value       = module.gke.min_master_version
}

output "logging_service" {
  description = "Logging service used"
  value       = module.gke.logging_service
}

output "monitoring_service" {
  description = "Monitoring service used"
  value       = module.gke.monitoring_service
}

output "master_version" {
  description = "Current master kubernetes version"
  value       = module.gke.master_version
}

output "network_policy_enabled" {
  description = "Whether network policy enabled"
  value       = module.gke.network_policy_enabled
}

output "node_pools_names" {
  description = "List of node pools names"
  value       = module.gke.node_pools_names
}

output "node_pools_versions" {
  description = "Node pool versions by node pool name"
  value       = module.gke.node_pools_versions
}

output "service_account" {
  description = "The service account to default running nodes as if not overridden in node_pools."
  value       = module.gke.service_account
}

7. GKE Cluster 프로비전

파일을 모두 저장한 후, Terraform workspace를 초기화한다. terraform init 명령어를 이용하여 필요한 provider와 plugins를 다운로드한다.

terraform init

terraform plan 명령어를 실행하여 실행 계획을 생성한다.

terraform plan

만들어진 plan이 적절한 것 같아 보이면 계획을 적용한다

terraform apply -auto-approve

생성이 완료되었다! GCP 콘솔을 통해 만들어진 클러스터를 확인한다.

8. kubectl 명령어를 사용하여 클러스터에 접속하기

gcloud 명령어를 사용하여 kubectl이 GKE 클러스터에 접속할 수 있도록 kubectl을 구성한다.

gcloud container clusters get-credentials 클러스터 이름 --region 리전 

오류가 발생하는 경우, Google Kubernetes Engine(GKE) 클러스터에 kubectl을 사용하여 인증하는 데 필요한 gke-gcloud-auth-plugin이 설치되지 않았다는 것이다. 해당 플러그인을 설치하기 위해서는 cmd를 관리자 권한으로 실행한 후 커맨드를 입력한다.

gcloud components install gke-gcloud-auth-plugin
gcloud components list # 설치 확인

kubectl 커맨드를 사용하여 설정이 잘 되었는지 확인한다.

❯ k get node
NAME                                                  STATUS   ROLES    AGE   VERSION
gke-k8s-standard-arc-default-node-poo-c689d9fc-1bcq   Ready    <none>   31m   v1.27.3-gke.100
gke-k8s-standard-arc-default-node-poo-c689d9fc-l8vr   Ready    <none>   31m   v1.27.3-gke.100
gke-k8s-standard-arc-default-node-poo-f4dbcfa0-79g2   Ready    <none>   31m   v1.27.3-gke.100
gke-k8s-standard-arc-default-node-poo-f4dbcfa0-qrwh   Ready    <none>   31m   v1.27.3-gke.100

9. 클러스터 삭제

더 이상 클러스터가 필요하지 않다면, 크레딧을 줄이기 위해 클러스터를 삭제한다.

terraform destroy -auto-approve

10. 오류 정리

클러스터를 생성 및 삭제하면서 경험한 오류 및 참고사항을 정리한다.

1. 서비스 어카운트 미생성 오류

서비스 어카운트를 생성하지 않아서 생긴 오류다....

resource "google_service_account" "default" {
  # google_service_account 리소스의 account_id에는 서비스 계정의 전체 이메일 주소가 아니라 고유 ID 부분만 포함해야 한다.
  account_id   = "k8s-standard-architecture"
  project      = var.project_id
  display_name = "K8s Standard Architecture Service Account"
  description  = "Service account for K8s standard architecture"
}

main.tf에 해당 블럭을 추가하고

  depends_on = [module.vpc, google_service_account.default]

service account에 대한 종속성을 추가하면 해결된다.

2. enable_private_endpoint를 true로 설정했을 때의 오류

enable_private_endpoint = true는 private endpoint를 설정하는 것이 아니라 private_endpoint "만" 접속할 수 있도록 만드는 변수이다. 퍼블릭 엔드포인트도 생성하기 위해서는 enable_private_endpointtrue가 아니라 false로 설정해야 한다. true로 설정하면 퍼블릭 IP로 접속 자체가 되지 않는다.

enable_private_endpoint (Optional) - When true, the cluster's private endpoint is used as the cluster endpoint and access through the public endpoint is disabled. When false, either endpoint can be used. This field only applies to private clusters, when enable_private_nodes is true.

프라이빗 엔드포인트의 활성화 자체는 private_cluster_config에 의해 관리된다. 이 설정은 클러스터가 프라이빗 모드로 작동할 것인지를 결정한다. private_cluster_configtrue로 설정하면, 클러스터는 프라이빗 네트워크 내에서만 접근 가능한 상태가 된다. 이 설정은 프라이빗 엔드포인트의 사용을 가능하게 하지만, enable_private_endpoint 설정에 따라 퍼블릭 엔드포인트의 사용 여부가 결정된다. enable_private_endpoint는 클러스터가 퍼블릭 엔드포인트를 통한 접근을 허용할지를 결정합니다.

  1. enable_private_endpoint = true: 이 설정은 클러스터가 프라이빗 엔드포인트를 사용하도록 하며, 퍼블릭 엔드포인트를 통한 접근을 비활성화한다. 즉, 외부 인터넷에서의 직접 접근이 차단된다.

  2. enable_private_endpoint = false: 이 설정은 클러스터가 프라이빗 엔드포인트와 퍼블릭 엔드포인트 모두를 사용할 수 있도록 한다. 이 경우, 클러스터는 내부 네트워크와 외부 인터넷 양쪽에서 접근 가능하다.

프라이빗 엔드포인트만으로 접속하게 하고 싶다면, 이와 같이 설정하면 된다.

  # 프라이빗 네트워크를 설정하기 위해서는 마스터 인증 네트워크도 설정해야 한다
  master_authorized_networks = [
    {
      # 쿠버네티스 마스터 엔드포인트에 액세스할 수 있도록 허용하는 cidr block. 
      # 보안을 위해 필요한 곳만 허용하도록 설정하는 것이 좋다.(일반적으로 조직의 네트워크 또는 클러스터를 관리하는 특정 머신의 IP 주소).
      # 예를 들어, 조직 네트워크의 IP 범위가 192.168.0.0/16인 경우, cidr_block = "192.168.0.0/16" 로 변경할 수 있다.
      # 이렇게 하면 192.168.0.0~192.168.255.255 범위의 IP 주소를 가진 머신만 쿠버네티스 마스터 엔드포인트에 액세스할 수 있다. 
      cidr_block   = "0.0.0.0/0"
      display_name = "Allow from everywhere"
    },
  ]

참고 링크

medium
GCP 공식 문서
Terraform Registry

profile
다 아는 건 아니어도 바라는 대로

0개의 댓글