🐋

GKEからCloud SQLに接続

2023/07/04に公開

概要

GKEのクラスタからCloud SQLのMySQLに接続する設定を何回も書いて何回もハマったので、メモ

方針

Podは一つだけで、その中に以下の二つのコンテナを持つものとします。

  • api (port: 80)
  • cloud-sql-proxy (port: 3306)

外部にAPIを公開するため、Serviceを用いて、containerの80番ポートを8000番としてexposeします。

基本的には以下の記事に沿って設定します。
今回はCloud SQL Auth プロキシとWorkload Identityを用いました。
https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine?hl=ja#workload-identity

build

コンテナの開発ができたら、Cloud BuildのCLIを用いてArtifact Registryに登録します。

今回は <project>/backend/ というレポジトリに api というイメージをアップロードします。

bash
$ gcloud builds submit --region=asia-northeast1 --tag asia-northeast1-docker.pkg.dev/<project>/backend/api .

Kubernetes サービス アカウント(KSA)と Google サービス アカウント(GSA)

コンテナからCloud SQLを叩くためのGSAを作成し、KSAにバインディングします。
https://cloud.google.com/sql/docs/mysql/connect-kubernetes-engine?hl=ja#workload-identity

service-account-cloud-sql.yml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ksa-cloud-sql
bash
# サービスアカウント(KSA)を作成
$ kubectl apply -f service-account-cloud-sql.yaml

# GSAとKSAのバインディング
$ gcloud iam service-accounts add-iam-policy-binding \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:<YOUR-GOOGLE-CLOUD-PROJECT>.svc.id.goog[default/ksa-cloud-sql]" \
backend-cloud-sql@<YOUR-GOOGLE-CLOUD-PROJECT>.iam.gserviceaccount.com

# KSAにアノテーション
$ kubectl annotate serviceaccount \
ksa-cloud-sql \
iam.gke.io/gcp-service-account=backend-cloud-sql@<YOUR-GOOGLE-CLOUD-PROJECT>.iam.gserviceaccount.com

コンテナイメージは最新版を参照することにしています(image: asia-northeast1-docker.pkg.dev/<project>/backend/api:latestの部分)

deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-backend
  template:
    metadata:
      labels:
        app: test-backend
    spec:
      serviceAccountName: ksa-cloud-sql
      containers:
        - name: api
          image: asia-northeast1-docker.pkg.dev/<project>/backend/api:latest
          ports:
            - containerPort: 80
          envFrom:
            - secretRef:
                name: backend-env
          resources:
            requests:
              memory: "1Gi"
              cpu: "500m"
              ephemeral-storage: "1Gi"
            limits:
              memory: "1Gi"
              cpu: "500m"
              ephemeral-storage: "1Gi"
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest
          args:
            - "--structured-logs"
            - "--port=3306"
            - "<INSTANCE_NAME>"
          securityContext:
            runAsNonRoot: true
          resources:
            requests:
              memory: "2Gi"
              cpu: "1"

Deploymentの定義を適用します。

bash
$ kubectl apply -f deployment.yaml

Secrets

上記ファイルの、apiのコンテナのこの部分で、環境変数を一括で読み込んでいます。

envFrom:
  - secretRef:
    name: backend-env

https://kubernetes.io/ja/docs/concepts/configuration/secret/#ユースケース-コンテナの環境変数として

中身はこんな感じ。localhost:3306 から接続できる。

.env
DB_USERNAME=****
DB_PASSWORD=****
INSTANCE_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=****
PORT=80
....

更新は kubectl--from-env-file を使うと便利。

bash
$ kubectl create secret generic backend-env --from-env-file=.env

Service

クラスタと外部との通信ようにserviceを作成する。

service.yml
apiVersion: v1
kind: Service
metadata:
  name: signage-backend-service
spec:
  type: LoadBalancer
  selector:
    app: signage-backend
  ports:
    - port: 8000
      targetPort: 80
bash
$ kubectl apply -f service.yaml

Firewall

最後に、8000番ポートに穴を開けます。

bash
$ gcloud compute firewall-rules create backend --allow=tcp:8000

http://<YOUR_LOAD_BALANCER_IP>:8000でアクセスできるはず。

(おまけ) GKE限定公開クラスタの場合

GKEからCloud SQLにアクセスするときに、Public IPがなくてエラーが出る。

ログ
failed to connect to instance: Dial error: failed to dial (connection name = "<YOUR_CONNECTION_NAME>"): dial tcp <IP>:3307: i/o timeout

ので、Cloud NATの設定をすると通るようになる。

https://cloud.google.com/nat/docs/set-up-manage-network-address-translation?hl=ja

(おまけ) Prisma

PrismaでDB接続を指定する際、デフォルトの env(DATABASE_URL).env ファイルから読み取るが、上記の構成だと環境変数から .env ファイルを再構成しなければならず面倒なので、 PrismaClient のコンストラクタの引数に渡してしまうと便利。

https://github.com/prisma/prisma/discussions/3561#discussioncomment-1551839

prisma.service.ts
const userName = process.env.DB_USERNAME;
const password = process.env.DB_PASSWORD;
const host = process.env.INSTANCE_HOST;
const port = process.env.DB_PORT;
const database = process.env.DB_NAME;

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: `mysql://${userName}:${password}@${host}:${port}/${database}`,
    },
  },
});
schema.prisma
datasource db {
  provider = "mysql"
  url      = ""
}

Discussion