🐥

Terraformを使って、GKEとPrivateIPを付与したCloudSQLを接続する

2023/10/25に公開

記事の内容

Terraformを使って、GKEからPrivateIPを持つCloudSQLに対して接続する方法を紹介します。

記事を読むと得られるもの

  • GCP上でのVPCやSubnetの作り方
  • PublicIPを持たないCloudSQLの作り方
  • GKEの構築方法
  • GKEとCloudSQLをPrivateIPで通信する方法
  • それらをTerraformで構築する方法

対象読者

  • GCPユーザー
  • GKEとCloudSQLを接続したいユーザー
  • CloudSQLにPrivateIPを付与して運用したい人

記事の長さ

5分で読めます

ネットワークの作成

GKEなどを配置するネットワークを作成します。

VPCとサブネットの作成

network.tf

resource "google_compute_network" "main_network" {
  project = var.project_id
  name = "main-network"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main_subnetwork" {
  project = var.project_id
  name          = "main-subnetwork"
  ip_cidr_range = "10.2.0.0/16"
  region        = "asia-northeast1"
  network       = google_compute_network.main_network.id
  secondary_ip_range {
    range_name    = "secondary-range-1"
    ip_cidr_range = "10.10.0.0/16"
  }
  secondary_ip_range {
    range_name    = "secondary-range-2"
    ip_cidr_range = "10.20.0.0/16"
  }
}

上記ファイルをterraform applyすると、VPCとそのVPCに所属するサブネットが作成されます。
これらのネットワークの中に、GKE等を作成していきます。

Private Service Connectionの作成

GCPの仕様として、プライベートIPを持つCloudSQLはユーザーが定義したVPCではなく、Googleが管理するService Provider VPCに作成されます。
そのため、上記で作成したVPCからCloudSQLに対してprivateIPで通信する場合、Private Service Connectionを設定し、接続するための経路を用意する必要があります。

network.tf

# VPCとSubnetに関する記述
...

resource "google_project_service" "service_networking" {
  project            = var.project_id
  service            = "servicenetworking.googleapis.com"
  disable_on_destroy = false
}

resource "google_compute_global_address" "private_ip_address" {
  project       = var.project_id
  name          = "private-ip"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 16
  network       = google_compute_network.main_network.id
}

resource "google_service_networking_connection" "default" {
  network                 = google_compute_network.main_network.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
}

上記コードにて、VPC内にPrivate Service Accessが作成されました。

CloudSQLの作成

PublicIPを持たないCloudSQLを作成していきます。

CloudSQL(MySQL)の作成

PublicIPを持たないCloudSQLをVPC内のサービスと接続するためには、

  • Private Service Connect
  • Private Google Access

を設定したCloudSQLを作成する必要があります。今回は、Private Google Accessを設定したCloudSQLを作成します。

db.tf

resource "google_sql_database_instance" "sql" {
  project          = var.project_id
  name             = "sql"
  region           = "asia-northeast1"
  database_version = "MYSQL_8_0"

  settings {
    tier = "db-f1-micro"
    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.main_network.id
    }
  }
}

このTerraformファイルをapplyすると、PrivateIPは付与されているが、PublicIPは付与されていないCloudSQLが作成されます。

※今回は利用しませんが、もし、Private Service Connectを利用する場合、以下のような記述になります。

resource "google_sql_database_instance" "db" {
  project          = var.project_id
  name             = "db"
  region           = "asia-northeast1"
  database_version = "MYSQL_8_0"

  settings {
    tier = "db-f1-micro"
    ip_configuration {
      psc_config {
        psc_enabled               = true
        allowed_consumer_projects = [var.project_id]
      }
      ipv4_enabled    = false
    }
  }
}

上記二つ、どちらを使うべきか迷ったら、以下の記事を参考にするとわかりやすかったです。

https://blog.g-gen.co.jp/entry/google-api-private-service-connect-explained#Private-Service-Connect-vs-Private-Google-Access

UserとDatabaseの作成

作成したMySQLの中に、DatabaseとUserを作成します。GKEからはこの情報を元に接続します。

resource "google_sql_user" "users" {
  project = var.project_id

  name     = "test-user"
  instance = google_sql_database_instance.sql.name
  password = "password_1234"
}

resource "google_sql_database" "database" {
  project = var.project_id

  name     = "database"
  instance = google_sql_database_instance.sql.name
}

GKEの作成

次に、今回作成したネットワークの中にGKE(Kubernetes)クラスターを作成します。

Docker Imageの作成

GKEを作成する前に、GKE上で動作するアプリケーションのDockerImageを作成します。
今回は、MySQLに接続して、Table一覧を返すだけの簡単なnodejsアプリケーションを作成します。

index.js

const express = require('express');
const mysql = require('promise-mysql');
const app = express();
const port = 8080;

const createUnixSocketPool = async () => {
  return mysql.createPool({
    user: "test-user",
    password: "password_1234",
    database: "database",
    host: "127.0.0.1",
  });
};

let pool;

(async () => {
  pool = await createUnixSocketPool();
})();

app.get('/', async (req, res) => {
  try {
    const connection = await pool.getConnection();
    const results = await connection.query('SHOW TABLES');
    connection.release();
    res.send(results);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

app.listen(port, () => {
  console.log(`App running on http://localhost:${port}`);
});


Dockerfile

# nodeのイメージを取得
FROM node:14

# アプリケーションディレクトリを作成
WORKDIR /usr/src/app

# アプリケーションの依存関係をインストール
COPY package*.json ./

RUN npm install

# アプリケーションのソースをバンドル
COPY . .

# ポート8080で実行
EXPOSE 8080
CMD [ "node", "index.js" ]

package.json

{
  "name": "gke",
  "version": "1.0.0",
  "description": "A Node.js app connecting to MySQL using Unix sockets",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.17.1",
    "promise-mysql": "^5.0.3"
  }
}

上記3ファイルを同一のディレクトリに格納し、以下のコマンドを実行します。

$ docker build . -t yossy1119/zenn-gke-privateip-cloudsql:latest
$ docker push yossy1119/zenn-gke-privateip-cloudsql:latest

https://hub.docker.com/repository/docker/yossy1119/zenn-gke-privateip-cloudsql/general

これで、8080ポートでListenするMySQLへ接続するNode.jsアプリケーションのDockerImageを公開することができました。
※今回はDockerHubのPublicRepoを利用しましたが、GCRのrepoに変更する等は自由にご対応ください。

GKEクラスターを作成する

data "google_client_config" "default" {}

provider "kubernetes" {
  host                   = "https://${module.gke.endpoint}"
  token                  = data.google_client_config.default.access_token
  cluster_ca_certificate = base64decode(module.gke.ca_certificate)
}

module "gke" {
  source                      = "terraform-google-modules/kubernetes-engine/google"
  project_id                  = var.project_id

  name                        = "test-cluster"
  regional                    = true
  region                      = "asia-northeast1"
  network                     = google_compute_network.main_network.name
  subnetwork                  = google_compute_subnetwork.main_subnetwork.name
  ip_range_pods               = "secondary-range-1"
  ip_range_services           = "secondary-range-2"
  create_service_account      = true
  enable_cost_allocation      = true
  enable_binary_authorization = false
  gcs_fuse_csi_driver         = true
}

上記Terraformをapplyすることで、GKEクラスターをデプロイします。

GKEクラスターのCredentialsを取得する

ローカル環境からClusterに対して、kubectlコマンドを実行できるように、GKEクラスターのCredentialsを取得します。

$ gcloud container clusters get-credentials test-cluster --region asia-northeast1

正常に取得できた場合、kubectlコマンドでクラスターにコマンド実行ができるようになります。

$ kubectl get pod
NAMESPACE     NAME                                                          READY   STATUS    RESTARTS      AGE
gmp-system    alertmanager-0                                                2/2     Running   0             24m
gmp-system    collector-775x8                                               2/2     Running   0             22m
...
kube-system   pdcsi-node-sm79t                                              2/2     Running   0             23m

ServiceAccountを作成する

GKEからCloudSQLに対して接続を許可するServiceAccountを作成します。

GKEクラスター内のPodからGCP上のリソースにアクセスする場合、

  • GCP上にCloudSQLClientのRoleを持ったService Accountを作成
  • Kubernetesクラスター上で、そのGCPのService AccountをアタッチしたService Accountを作成

の二つを行う必要があります。
しかし、以下のTerraform Moduleを利用すれば、同時に作成することが可能です。

sa.tf

module "my-app-workload-identity" {
  source     = "terraform-google-modules/kubernetes-engine/google//modules/workload-identity"
  name       = "test-sa"
  namespace  = "default"
  project_id = var.project_id
  roles      = ["roles/cloudsql.client"]
}

上記ファイルをApplyすると、GKEクラスターのdefault namespaceにtest-saというサービスアカウントが作成されます。

$ kubectl describe serviceaccount test-sa
Name:                test-sa
Namespace:           default
Labels:              <none>
Annotations:         iam.gke.io/gcp-service-account: test-sa@gke-cloudsql-private.iam.gserviceaccount.com
Image pull secrets:  <none>
Mountable secrets:   <none>
Tokens:              <none>
Events:              <none>

Deploymentをデプロイする

CloudSQL・GKEクラスター・DockerImage・ServiceAccountの準備が完了したため、GKE上からCloudSQLに接続を試みるPodをデプロイします。

manifests/deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      serviceAccountName: test-sa
      containers:
        - name: node
          image: yossy1119/zenn-gke-privateip-cloudsql:latest
          ports:
            - containerPort: 8080
        - name: cloud-sql-proxy
          image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:latest
          args:
            - "--private-ip"
            - "--structured-logs"
            - "--port=3306"
            - "gke-cloudsql-private:asia-northeast1:sql"
          securityContext:
            runAsNonRoot: true

$ kubectl apply -f manifests/deploy.yaml
$ kubectl get pod
NAME                    READY   STATUS    RESTARTS   AGE
test-6747f956cc-q5z45   2/2     Running   0          37s

正常にデプロイが完了すると、上記のようにContainerが二つ動作するPodが一つ起動します。
正常にMySQLに接続できているかを確認するために、port-forwardで確認します。

$ kubectl port-forward test-6747f956cc-q5z45 1111:8080
$ curl http://localhost:1111
[]

テーブルがないため、空の配列が表示されていますが、エラーが出ず正常にGKEからCloudSQLに接続できていることが確認できます。

以上にて、GKEからPublic IPを持たないCloudSQLに対して、PrivateIPを通して接続できました。

Github Repo

https://github.com/rara-tan/zenn-cloudsql-gke-subnet

こちらにサンプルソースを配置しておきました!

note

勉強法やキャリア構築法など、エンジニアに役立つ記事をnoteで配信しています。

https://note.com/ring_belle/membership

Discussion