teru_0x01.log

技術メモと雑記

TerraformでGKEにクラスタを構築+Goのアプリケーションデプロイ

InfraStudyのIaC回に触発されてTerraformを触ったので構築めも

プロジェクトの作成

GCPダッシュボードから適当にプロジェクトを作り,$PROJECT_IDにプロジェクトIDをセットしているところから始める.

デフォルトのプロジェクトを設定

$ gcloud config set project $PROJECT_ID

GKE, GCR(Google Container Registry)APIの有効化

$ gcloud services enable container.googleapis.com containerregistry.googleapis.com

terraformが利用するサービスアカウントの作成.

$ TF_ACCOUNT=terraform
$ gcloud iam service-accounts create $TF_ACCOUNT --display-name="terraform-account" --project=$PROJECT_ID

作成したサービスアカウントのキーペアを作成し,秘密鍵をダウロードする.

$ KEY_SAVE_PATH=~/key.json
$ gcloud iam service-accounts keys create $KEY_SAVE_PATH --iam-account $TF_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

サービスアカウントに対するロールの付与.

$ gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$TF_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com --role roles/compute.admin # GKEノードプールの操作に必要
$ gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$TF_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com --role roles/storage.admin # GCRの操作に必要
$ gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$TF_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com --role roles/container.clusterAdmin # GKEクラスタの操作に必要
$ gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$TF_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com --role roles/iam.serviceAccountUser # サービスアカウントへのアクセスに必要

terraformが利用するサービスアカウント情報をセットする.terraform applyなどでリソース操作する際は,このサービスアカウントの権限で実行されるように設定する.

$ export GOOGLE_APPLICATION_CREDENTIALS=$KEY_SAVE_PATH

GKEのセットアップ

terraformの設定を書く.今回はコンテナネイティブの負荷分散を利用するため,VPCネイティブクラスタを作成する.コンテナネイティブの負荷分散とは,IngressプロキシからCompute Engineロードバランサを介さずに直接Podへ通信を流す手法である.従来のCompute Engineロードバランサをバックエンドに使う方法と比較して1ホップ減るためレイテンシの削減などが見込まれるとして,GCPもこちらを推奨している.

main.tf

# 環境変数から読み込ませる
variable "project_id" {}

provider "google" {
  project = var.project_id
  region  = "asia-northeast1-a"
}

resource "google_container_cluster" "primary" {
  name = "sample-cluster"
  location = "asia-northeast1-a"
  # ノードプールとクラスタを分けて作成したいが,ノードプールのないクラスタは作成できない
  # そのため小さなノードプールを作成してすぐに削除する.
  remove_default_node_pool = true
  initial_node_count       = 1

  # VPCネイティブクラスタを作成するために必要
  ip_allocation_policy {
      cluster_ipv4_cidr_block = ""
      services_ipv4_cidr_block = ""
  }

  master_auth {
    username = ""
    password = ""

    client_certificate_config {
      issue_client_certificate = false
    }
  }
}

resource "google_container_node_pool" "primary_nodes" {
  name       = "sample-node-pool"
  location = "asia-northeast1-a"
  cluster    = google_container_cluster.primary.name
  node_count = 3

  node_config {
    machine_type = "n1-standard-1"

    metadata = {
      disable-legacy-endpoints = "true"
    }

    # devstorage.read_onlyはGCRからイメージをpullするために必要
    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
      "https://www.googleapis.com/auth/devstorage.read_only",
    ]
  }
}

project_id環境変数から読み込ませて実行する.TF_VAR_$variable環境変数がtfファイルの$variable変数として使われる.

$ export TF_VAR_project_id=$PROJECT_ID
$ terraform apply

kubectlのコンテキストを今作成したクラスタに切りかえる.ちなみにコンテキストの管理や確認にはkubectlがお勧め

$ gcloud container clusters get-credentials --zone=asia-northeast1-a sample-cluster

ノードが作成されているか確認する.

$ kubectl get nodes
NAME                                                STATUS   ROLES    AGE   VERSION
gke-sample-cluster-sample-node-pool-83bcffdf-kb4z   Ready    <none>   40m   v1.14.10-gke.36
gke-sample-cluster-sample-node-pool-83bcffdf-pb4j   Ready    <none>   40m   v1.14.10-gke.36
gke-sample-cluster-sample-node-pool-83bcffdf-zl2s   Ready    <none>   39m   v1.14.10-gke.36

アプリケーションの作成

コーディング

続いてGoでアプリケーションを書いていく.

とりあえずmoduleを作成する.

$ MODULE_NAME=gcp-example
$ go mod init $MODULE_NAME

メッセージを返す単純なWebアプリを作る.

main.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Dockerfileを作る.マルチステージビルドを利用し,2ステージ目ではscratchイメージを使ってイメージサイズを可能な限り小さくしている.この際,scratchにはユーザ作成のコマンドが無いので,ユーザとグループは1ステージ目で作る必要がある. また,GoをビルドするときにCGO_ENABLED=0を指定しないと実行時にエラーになる

FROM golang:1.13 as builder
WORKDIR /go/src/app
RUN groupadd -g 10001 myapp && useradd -u 10001 -g myapp myapp

COPY go.mod ./
COPY . ./
RUN CGO_ENABLED=0 go build -o /go/bin/app

FROM scratch
COPY --from=builder /go/bin/app /go/bin/app
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/passwd /etc/passwd
USER myapp
CMD [ "/go/bin/app" ]

Dockerfileをビルドする.

$ docker build -t helloapp:1.0 .

テストしてみる.

$ docker run --rm --name helloapp -d -p 8080:8080 helloapp:1.0 && curl localhost:8080 && docker stop helloapp
1f535c499c78517612afc0d2a20d3b716ca5f7ab70d7b720eb31ad6249df4687
hello
helloapp

GCRにpush

まずはGCRへのリクエストを認証するようにDockerを構成する.

$ gcloud auth configure-docker

dockerイメージにレジストリ名をタグ付けする.こうすることで,docker pushした際に指定したレジストリにpushされる.

$ docker tag helloapp:1.0 gcr.io/$PROJECT_ID/helloapp:1.0

GCRにpushする.

$ docker push gcr.io/$PROJECT_ID/helloapp:1.0

確認してみる

$ gcloud container images list 
NAME
gcr.io/<project_id>/helloapp

GKEにデプロイ

k8sマニフェストを書いていく.deployment→service→ingressの順で適応されるようにファイル名に接頭辞をつける.kubectl applyはファイル名の辞書順で行われるので,これが無いとserviceの前にingressがapplyされて依存関係上エラーになる(なった).

$ tree kube/
kube/
├── 01-deployment.yaml
├── 02-service.yaml
└── 03-ingress.yaml

01-deployment.yaml

Deploymentのマニフェスト.先ほどGCRにpushしたイメージを指定している.resource.limitsresource.requestsは共に指定しておくのがベター.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-dep
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: hello-app
        image: gcr.io/<project_id>/helloapp:1.0
        resources:
          limits:
            memory: "128Mi"
            cpu: "200m"
          requests:
            memory: "128Mi"
            cpu: "100m"
        ports:
        - containerPort: 8080

02-service.yaml

Network Endpoint Group(NEG)を利用してコンテナネイティブの負荷分散を行う.これはデフォルトの動作では無いため,IngressのバックエンドとなるServiceにはアノテーションをつける.

apiVersion: v1
kind: Service
metadata:
  name: test-service
  annotations:
    cloud.google.com/neg: '{"ingress": true}'
spec:
  type: NodePort
  selector:
      app: myapp
  ports:
  - port: 3000
    protocol: TCP
    targetPort: 8080

03-ingress.yaml

L7LBとなるIngressを作成する.バックエンドとして先ほど作成したServiceを指定する.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-ingress
spec:
  backend:
    serviceName: test-service
    servicePort: 3000

デプロイする.

$ kc apply -f kube/

確認する.

$ kc get ingress
NAME           HOSTS   ADDRESS          PORTS   AGE
test-ingress   *       34.120.111.236   80      15m

$ curl 34.120.111.236
hello

🎉