はじめに
社内研修でTerraform、k8s、Helmを使用したGoogle Cloud環境の構築を行ったので、その学習内容のまとめです。
ハンズオンで構築したもの
- Google CloudでGKE Autopilotクラスタを構築
- Wordpress(バックエンドにCloud SQL)、ArgoCDをGKE上にデプロイ
- 秘匿情報はExternal Secret Operatorを使用する
- Cloud SQLの起動停止ジョブ
Terraformを使用してGKEクラスタを作成する
Google Cloud上のリソースを一つずつ把握しながら作成していきたかったので、あえてモジュールを使用せずにリソースで実装しました。
実装例
variable "project_id" {
description = "The project ID to host the cluster in"
default = "test-xxxxx-xxxxx"
}
variable "gke_name" {
default = "test"
}
variable "region" {
description = "The region the cluster in"
default = "asia-northeast1"
}
provider "google" {
project = var.project_id
}
resource "google_compute_network" "default" {
name = "${var.gke_name}-network"
project = var.project_id
auto_create_subnetworks = false
enable_ula_internal_ipv6 = true
}
resource "google_compute_subnetwork" "default" {
name = "${var.gke_name}-subnetwork"
project = var.project_id
ip_cidr_range = "10.0.0.0/16"
region = var.region
stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL" # Change to "EXTERNAL" if creating an external loadbalancer
network = google_compute_network.default.id
secondary_ip_range {
range_name = "services-range"
ip_cidr_range = "192.168.0.0/24"
}
secondary_ip_range {
range_name = "pod-ranges"
ip_cidr_range = "192.168.1.0/24"
}
}
resource "google_container_cluster" "default" {
name = "${var.gke_name}-autopilot-cluster"
project = var.project_id
location = var.region
enable_autopilot = true
enable_l4_ilb_subsetting = true
network = google_compute_network.default.id
subnetwork = google_compute_subnetwork.default.id
resource_labels = {
team = "research"
}
ip_allocation_policy {
stack_type = "IPV4_IPV6"
services_secondary_range_name = google_compute_subnetwork.default.secondary_ip_range[0].range_name
cluster_secondary_range_name = google_compute_subnetwork.default.secondary_ip_range[1].range_name
}
# Set `deletion_protection` to `true` will ensure that one cannot
# accidentally delete this instance by use of Terraform.
deletion_protection = false
}
内部VPNのみ接続可能な設定とする
公開クラスタでクラスタを作成し、Cloud Armorで全部のアクセスを拒否するデフォルトルールと内部VPNのみ許可するセキュリティポリシーを追加し、IngressとBackendConfigによる制御を実装しました。
実装例
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: hello-backend-config
namespace: default
spec:
securityPolicy:
name: hello-sec-policy # 作成したセキュリティポリシーの名前
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress
namespace: default
annotations:
kubernetes.io/ingress.class: "gce" # 外部 HTTP(S) ロードバランサを指定
spec:
defaultBackend:
service:
name: hello-app
port:
number: 8080
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-app
namespace: default
spec:
selector:
matchLabels:
run: hello-app
template:
metadata:
labels:
run: hello-app
spec:
containers:
- image: us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0
imagePullPolicy: IfNotPresent
name: hello-app
ports:
- containerPort: 8080
protocol: TCP
resource "google_compute_security_policy" "policy" {
name = "hello-sec-policy"
project = var.project_id
rule {
action = "deny(403)"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "default rule"
}
rule {
action = "allow"
priority = "1000"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["XXX.XX.XX.XX/32"]
}
}
}
}
tfstateファイルの実装
今回はGoogle Cloud Storageを使用して、tfstateファイルを管理するためバージョン管理と暗号化、ロック機能はCloud Storageの設定でできました。
AWSの場合は、DynamoDBを使用して排他ロックをかける必要があるそうです。
tfstateファイルの実装
resource "google_storage_bucket" "default" {
name = "terraform-remote-backend"
location = "ASIA-NORTHEAST1"
project = var.project_id
force_destroy = false
public_access_prevention = "enforced"
uniform_bucket_level_access = true
versioning {
enabled = true
}
}
terraform {
backend "gcs" {
bucket = "terraform-remote-backend"
}
}
Google Cloudのクラスタについて
GKEにはAutopilot モードと Standard モードが選択でき、今回はフルマネージドサービスであるAutopilotを採用しました。セキュリティはデフォルトで有効となっていて、自動スケーリングの様子も確認することができました。また、Workload Identityもデフォルトで有効化されています。
Autopilotでは、デプロイしたリソースを削除すると自動的にノードがゼロまで削減されます。また、request/limitを指定しないとデフォルトのCPU、memory、エフェメラルストレージが設定されます。
デフォルトはCPUが0.5vCPU、memoryが2GiB、エフェメラルストレージが1GiBとなっていました。今回作成した、Wordpress、External Secrets Operator、ArgoCD、Ingress、BackEndConfigをリリースしただけで
4〜5ノードが起動してコストがかかるため、それぞれに最低限のリソースを使用するように設定しておいた方がよさそうでした。
最小設定は、CPUが50m メモリは52MiBとなっています。
Helm Chartを使用してGKE上にWordpressを構築する
Helm Chartについて
下記のHelm Chartを使用して、values.yamlで設定可能な値を変更しました。
https://artifacthub.io/packages/helm/bitnami/wordpress
変更点は下記です。
- エフェメラルストレージが不足しているというエラーが出たので、エフェメラルストレージの指定を変更しました
- WordpressにIP制限をかけるため、アノテーションを追加しました
- 機密情報(DBのパスワード、Wordpressのパスワード)はExternal Secret Operatorを使用したので、ExternalSecretで指定したk8s Secret名に変更しました
- DBはMariaDBからCloud SQL(MySQL)に変更しました
values.yaml
existingSecret: "wordpress-credentials"
resources:
requests:
ephemeral-storage: 300Mi
cpu: 100m
memory: 128Mi
limits:
ephemeral-storage: 300Mi
cpu: 200m
memory: 256Mi
service:
type: NodePort
annotations: {
cloud.google.com/backend-config: '{"ports": {"80":"hello-backend-config"}}'
,cloud.google.com/neg: '{"ingress": true}'
}
mariadb:
enabled: false
externalDatabase:
host: "xxxxxxxxxxxx.xxxxxxxxxxxx.asia-northeast1.sql.goog."
user: "wordpress-admin-user"
database: "wordpress-db"
existingSecret: "wordpress-credentials"
WordpressのDBをMariaDBからCloud SQL(MySQL)に変更する
Terrafromを使用してCloud SQLを作成しました。DBユーザーの作成は機密情報のため、コンソールから行いました。
インスタンス接続はPrivate Service Connectを使用しました。
Cloud SQL
# Enable APIs
locals {
services = toset([
"sqladmin.googleapis.com",
"servicenetworking.googleapis.com",
"networkmanagement.googleapis.com"
])
}
resource "google_project_service" "db" {
for_each = local.services
service = each.value
disable_on_destroy = false
}
# Cloud SQLインスタンスの作成
resource "google_sql_database_instance" "default" {
name = "mysql-instance"
region = var.region
database_version = "MYSQL_8_0"
settings {
tier = "db-f1-micro"
availability_type = "ZONAL" # 高可用性構成 (REGIONAL) or single zone (ZONAL)
disk_autoresize = false # ストレージの自動増量
backup_configuration {
enabled = false # 自動バックアップ
binary_log_enabled = false
}
ip_configuration {
psc_config {
psc_enabled = true
allowed_consumer_projects = [var.project_id]
}
ipv4_enabled = false
}
}
deletion_protection = false # Set to "true" to prevent destruction of the resource
}
# プライベート IP アドレスを予約
resource "google_compute_address" "default" {
name = "psc-compute-address-${google_sql_database_instance.default.name}"
region = var.region
address_type = "INTERNAL"
subnetwork = google_compute_subnetwork.default.id
address = "10.0.0.10"
}
data "google_sql_database_instance" "default" {
name = resource.google_sql_database_instance.default.name
}
# Private Service Connect エンドポイントを作成する
resource "google_compute_forwarding_rule" "default" {
name = "psc-forwarding-rule-${google_sql_database_instance.default.name}"
region = var.region
network = google_compute_network.default.id #VPC名
ip_address = google_compute_address.default.self_link
load_balancing_scheme = ""
target = data.google_sql_database_instance.default.psc_service_attachment_link
}
# databaseの作成
resource "google_sql_database" "database" {
name = "wordpress-db"
instance = google_sql_database_instance.default.name
}
# DBユーザーの作成の作成はコンソールから行う
# Cloud SQL Auth Proxyを使用して接続する
# 限定公開 DNS ゾーンを作成する
resource "google_dns_managed_zone" "default" {
name = "cloud-sql-dns"
description = "Cloud SQL インスタンスの DNS ゾーン"
dns_name = data.google_sql_database_instance.default.dns_name
visibility = "private"
cloud_logging_config {
enable_logging = false
}
private_visibility_config {
networks {
network_url = google_compute_network.default.id
}
}
}
# ゾーンに DNS レコードを作成する
resource "google_dns_record_set" "a" {
name = google_dns_managed_zone.default.dns_name
managed_zone = google_dns_managed_zone.default.name
type = "A"
ttl = 0
rrdatas = [google_compute_address.default.address]
}
External Secrets Operatorの導入
秘匿情報の取り扱いのためGoogle Cloud Secret ManagerとExternal Secrets Operatorを使用しました。Google Cloud Secret Managerで秘匿情報を管理し、それをExternal Secrets Operatorを使用してGKE上のKubernetes Secretsとして同期しました。
HelmでExternal Secrets Operatorをインストールした後、ClusterSecretStoreとExternalSecretをapplyする必要があります。
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace \
External Secrets Operatorの実装
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: gcp-store
namespace: external-secrets
spec:
provider:
gcpsm:
projectID: test-xxxxx-xxxxx
auth:
workloadIdentity:
# name of the cluster Location, region or zone
clusterLocation: asia-northeast1
# name of the GKE cluster
clusterName: test-autopilot-cluster
# projectID of the cluster (if omitted defaults to spec.provider.gcpsm.projectID)
clusterProjectID: test-xxxxx-xxxxx
# reference the sa from above
serviceAccountRef:
name: external-secrets
namespace: external-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: wordpress-credentials
spec:
refreshInterval: 24h # rate SecretManager pulls GCPSM
secretStoreRef:
kind: ClusterSecretStore
name: gcp-store # name of the SecretStore (or kind specified)
target:
name: wordpress-credentials # name of the k8s Secret to be created
creationPolicy: Owner
data:
- secretKey: wordpress-password
remoteRef:
key: wordpress-pass # name of the GCPSM secret key
- secretKey: mariadb-password
remoteRef:
key: db-pass # name of the GCPSM secret key
今後の展望
CloudSQLの夜間インスタンス停止の実装とArgoCDの実装について続きを書ければと思います。