saeki’s blog

The limits of my code mean the limits of my world.

GKEでGoアプリを動かすためのCI/CDパイプラインを構築する

Kubernetes入門がてらGKEでGoのアプリを動かすためのCI/CDパイプラインを組んでみたのでその作業ログ。これはあくまで入門記事なので本番で運用するレベルではない。

GCPの設定

まずはGCPの設定から。準備としてやることは大体こんな感じ。

- GCPプロジェクトの作成
- 使用するAPIの有効化
- CI用のサービスアカウント作成、権限付与

※以下gcloudコマンドではターミナルのクライアントにて$ gcloud auth loginで認証済みであることを前提とする

GCPプロジェクト作成

コマンドでさくっと作る

$ gcloud projects create <project_name> --set-as-default

<project_name>は任意のプロジェクト名に置き換える。グローバル?で一意な名前じゃないとプロジェクトIDが少し変わるので面倒。できれば一意なプロジェクト名にしたい。

--set-as-defaultを指定することでこれ以降叩くコマンドにいちいちプロジェクトを指定する必要がなくなる。

設定を確認

$ gcloud config list

あと念のため^で作ったプロジェクトが請求先アカウントと接続済みであることを確認しておく

gcr apiを有効にする

CircleCIでビルドしたコンテナイメージをGCRにpushする必要があるので、gcr apiを有効にする

$ gcloud services enable containerregistry.googleapis.com

有効になったか確認

$ gcloud services list

サービスアカウントを作成する

CircleCIからGCRへコンテナイメージをpushするためのサービスアカウントを用意

$ gcloud iam service-accounts create <service_account_name> --display-name "Account for upload docker image from circleci"

<service_account_name>はそれっぽいアカウント名に置き換える。circleciとかで一旦はよさそう

サービスアカウントに権限を付与する

$ gcloud projects add-iam-policy-binding <project_id> --member serviceAccount:<service_account_name>@gke-go-sample.iam.gserviceaccount.com --role roles/storage.admin

<project_id>は先ほど作成したプロジェクトのプロジェクトIDに置き換える。プロジェクトIDはGCPコンソール画面で確認できる

Goアプリケーション

単純にHello, worldを返すだけのAPIを用意する

package main

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

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, world!")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Dockerfileとかは以下 github.com

CircleCI

CIrcleCIではlintとtest, イメージのbuildとpushを行う 以下.circleci/config.yml

version: 2.1
executors:
  default:
    docker:
      - image: circleci/golang:1.13
    working_directory: /go/src/github.com/saekis/gke-go-sample
    environment:
      - GO111MODULE: "on"
  gcloud:
    docker:
      - image: google/cloud-sdk
    working_directory: /go/src/github.com/saekis/gke-go-sample
    environment:
      - GO111MODULE: "on"

commands:
  restore_module:
    steps:
      - restore_cache:
          name: Restore go modules cache
          key: go-mod-{{ .Branch }}-{{ checksum "go.mod" }}

  save_module:
    steps:
      - save_cache:
          name: Save go modules cache
          key: go-mod-{{ .Branch }}-{{ checksum "go.mod" }}
          paths:
            - /go/pkg/mod/cache

jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run: go mod download
      - save_module

  lint:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run:
          name: Download lint tool
          command: go get -u golang.org/x/lint/golint
      - run:
          name: golint
          command: golint -set_exit_status -min_confidence=1.1 ./...
      - run:
          name: go vet
          command: go vet ./...

  test:
    executor:
      name: default
    steps:
      - checkout
      - restore_module
      - run:
          name: test
          command: go test -v ./...

  build:
    parameters:
      env:
        type: string
    executor:
      name: gcloud
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Authenticate gcloud service account
          command: |
            echo $GCP_SERVICE_KEY > gcloud-service-key.json
            gcloud auth activate-service-account --key-file gcloud-service-key.json
            gcloud auth configure-docker --quiet
      - run:
          name: Build golang app image
          command: |
            docker build -t asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} -f docker/golang/Dockerfile .
            docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:latest
            if [ -n "${CIRCLE_TAG}" ]; then
              docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app:${CIRCLE_TAG}
            fi
      - run:
          name: Build nginx image
          command: |
            docker build -t asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} -f docker/nginx/Dockerfile .
            docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:latest
            if [ -n "${CIRCLE_TAG}" ]; then
              docker tag asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_BUILD_NUM} asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx:${CIRCLE_TAG}
            fi
      - run:
          name: Push golang image to GCR
          command: docker push asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-go-app
      - run:
          name: Push nginx image to GCR
          command: docker push asia.gcr.io/${GCP_PROJECT_NAME}/<< parameters.env >>-nginx

workflows:
  build_and_deploy:
    jobs:
      - setup
      - lint:
          requires:
            - setup
      - test:
          requires:
            - setup
      - build:
          name: build_dev
          env: dev
          requires:
            - lint
            - test
          filters:
            branches:
              ignore:
                - stage
                - master
      - build:
          name: build_stg
          env: stg
          requires:
            - lint
            - test
          filters:
            branches:
              only: stage
      - build:
          name: build_prod
          env: prod
          requires:
            - lint
            - test
          filters:
            branches:
              only: master

プロジェクト名とサービスアカウントのアクセスキーはCircleCIの環境変数にセットする

一応ここまででGitHubにpushされるとCircleCIが動いてGCRにコンテナイメージがpushされるようになる。

GKEの設定

クラスタ作成

$ gcloud container clusters create <cluster_name>

<cluster_name>は任意のクラスタ名を指定する
クラスタ作成は結構時間かかるので少し待つ
確認

$ kubectl config get-contexts

マニフェスト作成

deplayment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gke-go-sample-backend
  labels:
    app: gke-go-sample-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gke-go-sample-backend
  template:
    metadata:
      labels:
        app: gke-go-sample-backend
    spec:
      containers:
        - name: nginx
          image: asia.gcr.io/gke-go-sample/prod-nginx:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 80
        - name: golang
          image: asia.gcr.io/gke-go-sample/prod-app:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080

nginxをリバースプロキシに設定してるので、nginxのportを80、コンテナ間通信するためにgolangのコンテナのポートを8080にする

またコンテナを公開するのでingressも設定する ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gke-go-sample-backend-ingress
spec:
  rules:
    - http:
        paths:
          - path: /*
            backend:
              serviceName: gke-go-sample-backend
              servicePort: 80

全てのマニフェストファイルは以下

github.com

GKEへデプロイ

マニフェストファイルが用意できたらGKEにデプロイする

$ kubectl apply -f ./k8s

これでマニフェストファイルで設定した項目がGKEに反映される。 コンソールでingressのexternal ipにアクセスしてHello, worldが表示されれば成功。