GCP는 90일간 무료 300달러 크레딧을 제공한다. 하지만 보통 매니지드 쿠버네티스를 제공하는 시스템 (GCP의 경우에는 GKE)은 비용이 많이 들고, 이를 피하기 위해서 매번 클러스터를 새로 생성하는 것은 효율적이지 않다. 더욱 편한 실습 환경을 위해 Terraform을 통해 자동으로 클러스터를 생성할 수 있도록 했다.
시작하기 전에, 해당 사항들을 충족하는지 확인한다.
gcloud services enable container.googleapis.com --project=PROJECT_ID
gcloud config list
명령어를 통해 현재 구성 정보를 확인할 수 있다.
GCP 콘솔에서 새 프로젝트 버튼을 눌러 새로운 프로젝트를 생성한다.
Compute Engine API와 Kubernetes Engine API를 활성화한다. 콘솔에서 활성화하거나, 해당 명령어를 통해 gcloud에서 활성화할 수 있다.
gcloud services enable container.googleapis.com # Kubernetes Engine
gcloud services enable compute.googleapis.com # Compute Engine
서비스마다 별도의 서비스 계정을 만드는 것이 좋다. 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도 설치해 주자. 설치
생성한 폴더 내에 Terraform 파일을 생성해 준다. kubernetes-engine 과 network 모듈을 사용할 것이다.
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
}
파일을 모두 저장한 후, Terraform workspace를 초기화한다. terraform init
명령어를 이용하여 필요한 provider와 plugins를 다운로드한다.
terraform init
terraform plan
명령어를 실행하여 실행 계획을 생성한다.
terraform plan
만들어진 plan이 적절한 것 같아 보이면 계획을 적용한다
terraform apply -auto-approve
생성이 완료되었다! GCP 콘솔을 통해 만들어진 클러스터를 확인한다.
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
더 이상 클러스터가 필요하지 않다면, 크레딧을 줄이기 위해 클러스터를 삭제한다.
terraform destroy -auto-approve
클러스터를 생성 및 삭제하면서 경험한 오류 및 참고사항을 정리한다.
서비스 어카운트를 생성하지 않아서 생긴 오류다....
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에 대한 종속성을 추가하면 해결된다.
enable_private_endpoint = true
는 private endpoint를 설정하는 것이 아니라 private_endpoint "만" 접속할 수 있도록 만드는 변수이다. 퍼블릭 엔드포인트도 생성하기 위해서는 enable_private_endpoint
를 true
가 아니라 false
로 설정해야 한다. true
로 설정하면 퍼블릭 IP로 접속 자체가 되지 않는다.
enable_private_endpoint
(Optional) - Whentrue
, the cluster's private endpoint is used as the cluster endpoint and access through the public endpoint is disabled. Whenfalse
, either endpoint can be used. This field only applies to private clusters, whenenable_private_nodes
istrue
.
프라이빗 엔드포인트의 활성화 자체는 private_cluster_config
에 의해 관리된다. 이 설정은 클러스터가 프라이빗 모드로 작동할 것인지를 결정한다. private_cluster_config
를 true
로 설정하면, 클러스터는 프라이빗 네트워크 내에서만 접근 가능한 상태가 된다. 이 설정은 프라이빗 엔드포인트의 사용을 가능하게 하지만, enable_private_endpoint
설정에 따라 퍼블릭 엔드포인트의 사용 여부가 결정된다. enable_private_endpoint
는 클러스터가 퍼블릭 엔드포인트를 통한 접근을 허용할지를 결정한다.
enable_private_endpoint = true
: 이 설정은 클러스터가 프라이빗 엔드포인트를 사용하도록 하며, 퍼블릭 엔드포인트를 통한 접근을 비활성화한다. 즉, 외부 인터넷에서의 직접 접근이 차단된다.
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"
},
]