Terraformを使って、GKEとPrivateIPを付与したCloudSQLを接続する
記事の内容
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
}
}
}
上記二つ、どちらを使うべきか迷ったら、以下の記事を参考にするとわかりやすかったです。
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
これで、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
こちらにサンプルソースを配置しておきました!
note
勉強法やキャリア構築法など、エンジニアに役立つ記事をnoteで配信しています。
Discussion