🐈

Cloud Run CI/CD完全ガイド

2023/08/29に公開

Cloud Run CI/CD完全ガイド

cloudbuild.yamlを使用して、Cloud RunでCI/CDを実装します。

  • golang
  • dockertest
  • PostgreSQL

Flow

  1. GitHubリポジトリにpush
  2. イメージをbuild and test
  3. Artifact Registryにイメージをpush
  4. Cloud Runにdeploy

Buildトリガーを設定

https://cloud.google.com/build/docs/automating-builds/create-manage-triggers?hl=ja

ここで、リポジトリや環境変数を設定します。

↓ 例

環境変数名 意味
_CLOUDSQL_NAME CloudSQLの接続名 myproject:us-central1:myinstance
_IMAGE_NAME Artifact Registryに登録するイメージの名前(URL) us-central1-docker.pkg.dev/my-project/my-repo/my-app
_SERVICE_NAME Cloud Runのサービス名 my-service
_REGION リージョン us-central1

テストコードを作成

https://github.com/ory/dockertest

golangのdockertestを使用し、PostgreSQLのテストコードを作成します。

pathはcmd/server/main_test.goとしておきます。

package main

import (
	"database/sql"
	"fmt"
	"os"
	"testing"
	"time"

	_ "github.com/lib/pq"
	"github.com/ory/dockertest/v3"
	"github.com/ory/dockertest/v3/docker"
	log "github.com/sirupsen/logrus"
)

var db *sql.DB

func TestMain(m *testing.M) {
	// uses a sensible default on windows (tcp/http) and linux/osx (socket)
	pool, err := dockertest.NewPool("")
	if err != nil {
		log.Fatalf("Could not construct pool: %s", err)
	}

	err = pool.Client.Ping()
	if err != nil {
		log.Fatalf("Could not connect to Docker: %s", err)
	}

	// pulls an image, creates a container based on it and runs it
	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
		Repository: "postgres",
		Tag:        "15",
		Env: []string{
			"POSTGRES_PASSWORD=secret",
			"POSTGRES_USER=user_name",
			"POSTGRES_DB=dbname",
			"listen_addresses = '*'",
		},
	}, func(config *docker.HostConfig) {
		// set AutoRemove to true so that stopped container goes away by itself
		config.AutoRemove = true
		config.RestartPolicy = docker.RestartPolicy{Name: "no"}
	})
	if err != nil {
		log.Fatalf("Could not start resource: %s", err)
	}

	hostAndPort := resource.GetHostPort("5432/tcp")
	databaseUrl := fmt.Sprintf("postgres://user_name:secret@%s/dbname?sslmode=disable", hostAndPort)

	log.Println("Connecting to database on url: ", databaseUrl)

	resource.Expire(120) // Tell docker to hard kill the container in 120 seconds

	// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
	pool.MaxWait = 120 * time.Second
	if err = pool.Retry(func() error {
		db, err = sql.Open("postgres", databaseUrl)
		if err != nil {
			return err
		}
		return db.Ping()
	}); err != nil {
		log.Fatalf("Could not connect to docker: %s", err)
	}
	//Run tests
	code := m.Run()

	// You can't defer this because os.Exit doesn't care for defer
	if err := pool.Purge(resource); err != nil {
		log.Fatalf("Could not purge resource: %s", err)
	}

	os.Exit(code)
}

func TestRealbob(t *testing.T) {
	// all tests
}

cloudbuild.yamlを作成

https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run?hl=ja#continuous_deployment

steps:
 # Pull image from cache
 - name: 'gcr.io/cloud-builders/docker'
   entrypoint: 'bash'
   args: ['-c', 'docker pull gcr.io/$PROJECT_ID/${_IMAGE_NAME}:latest || exit 0'] 
 # Build the container image using cache
 - name: 'gcr.io/cloud-builders/docker'
   args: ['build', '-t', '${_IMAGE_NAME}:latest', '--cache-from', 'gcr.io/$PROJECT_ID/[IMAGE_NAME]:latest', '.']
 # Push the container image to Artifact Registry
 - name: 'gcr.io/cloud-builders/docker'
   args: ['push', '${_IMAGE_NAME}:latest']
 # Deploy container image to Cloud Run
 - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
   entrypoint: gcloud
   args:
   - 'run'
   - 'deploy'
   - '${_SERVICE_NAME}'
   - '--image'
   - '${_IMAGE_NAME}:latest'
   - '--execution-environment=gen2'
   - '--set-cloudsql-instances'
   - '${_CLOUDSQL_NAME}'
   - '--region'
   - '${_REGION}'
   - '--allow-unauthenticated'
   - '--set-env-vars'
   - 'POSTGRES_USER=user_name'
   - '--set-env-vars'
   - 'POSTGRES_PASSWORD=secret'
   - '--set-env-vars'
   - 'POSTGRES_DB=dbname'
 # Run tests and save to file
 - name: golang:1.20
    entrypoint: /bin/bash
    args:
      - -c
      - |
        go install github.com/jstemmer/go-junit-report/v2@latest
        2>&1 go test -timeout 1m -v ./cmd/server/... | /go/bin/go-junit-report -set-exit-code -iocopy -out ${SHORT_SHA}_test_log.xml
    waitFor: ['-']
 substitutions:
   _CLOUDSQL_NAME: ${_CLOUDSQL_NAME}
   _IMAGE_NAME: ${_IMAGE_NAME}
   _SERVICE_NAME: ${_SERVICE_NAME}
   _REGION: ${_REGION}
 images:
 - '${_IMAGE_NAME}:latest'
 options:
  logging: CLOUD_LOGGING_ONLY
  machineType: 'E2_HIGHCPU_32'

cloudbuild.yamlの解説

環境変数

substitutions:
   _CLOUDSQL_NAME: ${_CLOUDSQL_NAME}
   _IMAGE_NAME: ${_IMAGE_NAME}
   _SERVICE_NAME: ${_SERVICE_NAME}
   _REGION: ${_REGION}

トリガーで設定した環境変数をここで使えるように設定します。

テストの実行

https://cloud.google.com/build/docs/building/build-go?hl=ja#configuring_go_builds

# Run tests and save to file
 - name: golang:1.20
    entrypoint: /bin/bash
    args:
      - -c
      - |
        go install github.com/jstemmer/go-junit-report/v2@latest
        2>&1 go test -timeout 1m -v ./cmd/server/... | /go/bin/go-junit-report -set-exit-code -iocopy -out ${SHORT_SHA}_test_log.xml
    waitFor: ['-']
 options:
  logging: CLOUD_LOGGING_ONLY
  machineType: 'E2_HIGHCPU_32'

テストを並列実行することで、ビルドの時間を短縮しています。

https://cloud.google.com/build/docs/configuring-builds/configure-build-step-order?hl=ja#examples

さらに、CPUの性能を上げることで、さらにビルドの時間を短縮します。そのお金がかかるので、注意が必要です。

https://cloud.google.com/build/docs/optimize-builds/increase-vcpu-for-builds?hl=ja#increase_vcpu_for_default_pools

Discussion