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
를 통해 실행중인 컨테이너에 연결하여 로그를 확인했습니다.