mirror of
https://github.com/ryboe/private-ip-cloud-sql-db.git
synced 2025-12-19 10:00:37 -05:00
Initial commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
# .tfstate files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# Crash log files
|
||||
crash.log
|
||||
|
||||
# Ignore any .tfvars files that are generated automatically for each Terraform run. Most
|
||||
# .tfvars files are managed as part of configuration and so should be included in
|
||||
# version control.
|
||||
#
|
||||
# example.tfvars
|
||||
|
||||
# Ignore override files as they are usually used to override resources locally and so
|
||||
# are not checked in
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
|
||||
# Include override files you do wish to add to version control using negated pattern
|
||||
#
|
||||
# !example_override.tf
|
||||
|
||||
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
|
||||
# example: *tfplan*
|
||||
|
||||
# This file is created by init_dev_cluster.sh and contains a workspace name that
|
||||
# is unique to the developer.
|
||||
terraform/development/backend.hcl
|
||||
|
||||
api/target
|
||||
.terraform/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Ryan Boehning
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Cloud SQL DB with a Private IP
|
||||
|
||||
This repo demonstrates how to create a Cloud SQL DB with a private IP address
|
||||
only, and connect to it with [Cloud SQL Proxy](https://cloud.google.com/sql/docs/postgres/sql-proxy). The full explanation of how this works can be found in [this blog post](https://medium.com/@ryanboehning/how-to-deploy-a-cloud-sql-db-with-a-private-ip-only-using-terraform-e184b08eca64).
|
||||
|
||||
## Deploy the db and Cloud SQL Proxy
|
||||
|
||||
```bash
|
||||
gcloud services enable \
|
||||
cloudresourcemanager.googleapis.com \
|
||||
compute.googleapis.com \
|
||||
iam.googleapis.com \
|
||||
oslogin.googleapis.com \
|
||||
servicenetworking.googleapis.com \
|
||||
sqladmin.googleapis.com
|
||||
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Upload your public SSH key to Google's OS Login service
|
||||
|
||||
```bash
|
||||
gcloud compute os-login ssh-keys add --key-file=~/.ssh/id_rsa.pub --ttl=365d
|
||||
```
|
||||
|
||||
## Connect to the private db through Cloud SQL Proxy
|
||||
|
||||
```bash
|
||||
# get your SSH username
|
||||
gcloud compute os-login describe-profile | grep username
|
||||
|
||||
# psql into your private db
|
||||
ssh -t <username>@<proxy-ip-address> docker run --rm --network=host -it postgres:11-alpine psql -U postgres -h localhost
|
||||
```
|
||||
83
main.tf
Normal file
83
main.tf
Normal file
@@ -0,0 +1,83 @@
|
||||
// root module
|
||||
|
||||
terraform {
|
||||
required_version = "~> 0.12.24"
|
||||
required_providers {
|
||||
tfe = "~> 0.16.0"
|
||||
google = "~> 3.17.0"
|
||||
google-beta = "~> 3.17.0" # for enabling private services access
|
||||
}
|
||||
backend "remote" {
|
||||
organization = "my-terraform-cloud-org"
|
||||
workspaces {
|
||||
name = "private-ip-cloud-sql-db"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
db_username = "my_user" # Postgres username
|
||||
gcp_project_name = "norse-baton-274601"
|
||||
gcp_region = "us-central1"
|
||||
gcp_zone = "us-central1-b"
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = local.gcp_project_name
|
||||
region = local.gcp_region
|
||||
zone = local.gcp_zone
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = local.gcp_project_name
|
||||
region = local.gcp_region
|
||||
zone = local.gcp_zone
|
||||
}
|
||||
|
||||
module "vpc" {
|
||||
# Override the default google provider with the google-beta provider. We need
|
||||
# the beta provider to enable setting a private IP for the db.
|
||||
providers = {
|
||||
google = google-beta
|
||||
}
|
||||
source = "./modules/vpc"
|
||||
|
||||
name = "main-vpc"
|
||||
}
|
||||
|
||||
module "db" {
|
||||
providers = {
|
||||
google = google-beta
|
||||
}
|
||||
|
||||
source = "./modules/db"
|
||||
|
||||
disk_size = 10
|
||||
instance_type = "db-f1-micro"
|
||||
password = var.db_password # This is a variable because it's a secret. It's stored here: https://app.terraform.io/app/<YOUR-ORGANIZATION>/workspaces/<WORKSPACE>/variables
|
||||
user = local.db_username
|
||||
vpc_name = module.vpc.name
|
||||
vpc_link = module.vpc.link
|
||||
|
||||
# There's a dependency relationship between the db and the VPC that
|
||||
# terraform can't figure out. The db instance depends on the VPC because it
|
||||
# uses a private IP from a block of IPs defined in the VPC. If we just giving
|
||||
# the db a public IP, there wouldn't be a dependency. The dependency exists
|
||||
# because we've configured private services access. We need to explicitly
|
||||
# specify the dependency here. For details, see the note in the docs here:
|
||||
# https://www.terraform.io/docs/providers/google/r/sql_database_instance.html#private-ip-instance
|
||||
db_depends_on = module.vpc.private_vpc_connection
|
||||
}
|
||||
|
||||
module "dbproxy" {
|
||||
source = "./modules/dbproxy"
|
||||
|
||||
machine_type = "f1-micro"
|
||||
db_instance_name = module.db.connection_name # e.g. my-project:us-central1:my-db
|
||||
region = local.gcp_region
|
||||
zone = local.gcp_zone
|
||||
|
||||
# By passing the VPC name ("main-vpc") as the output of the VPC module
|
||||
# (module.vpc.name), we ensure the VPC will be created before the proxy.
|
||||
vpc_name = module.vpc.name
|
||||
}
|
||||
29
modules/db/main.tf
Normal file
29
modules/db/main.tf
Normal file
@@ -0,0 +1,29 @@
|
||||
// db module
|
||||
|
||||
resource "google_sql_database" "main" {
|
||||
name = "main"
|
||||
instance = google_sql_database_instance.main_primary.name
|
||||
}
|
||||
|
||||
resource "google_sql_database_instance" "main_primary" {
|
||||
name = "main-primary"
|
||||
database_version = "POSTGRES_11"
|
||||
depends_on = [var.db_depends_on]
|
||||
|
||||
settings {
|
||||
tier = var.instance_type
|
||||
availability_type = "ZONAL" # use "REGIONAL" for prod to distribute data storage across zones
|
||||
disk_size = var.disk_size
|
||||
|
||||
ip_configuration {
|
||||
ipv4_enabled = false # don't give the db a public IPv4
|
||||
private_network = var.vpc_link # the VPC where the db will be assigned a private IP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_sql_user" "db_user" {
|
||||
name = var.user
|
||||
instance = google_sql_database_instance.main_primary.name
|
||||
password = var.password
|
||||
}
|
||||
6
modules/db/outputs.tf
Normal file
6
modules/db/outputs.tf
Normal file
@@ -0,0 +1,6 @@
|
||||
// db module
|
||||
|
||||
output "connection_name" {
|
||||
description = "The connection string used by Cloud SQL Proxy, e.g. my-project:us-central1:my-db"
|
||||
value = google_sql_database_instance.main_primary.connection_name
|
||||
}
|
||||
36
modules/db/variables.tf
Normal file
36
modules/db/variables.tf
Normal file
@@ -0,0 +1,36 @@
|
||||
// db module
|
||||
|
||||
variable "db_depends_on" {
|
||||
description = "A single resource that the database instance depends on"
|
||||
type = any
|
||||
}
|
||||
|
||||
variable "disk_size" {
|
||||
description = "The size in GB of the disk used by the db"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "instance_type" {
|
||||
description = "The instance type of the VM that will run the db (e.g. db-f1-micro, db-custom-8-32768)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "password" {
|
||||
description = "The db password used to connect to the Postgers db"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "user" {
|
||||
description = "The username of the db user"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_link" {
|
||||
description = "A link to the VPC where the db will live (i.e. google_compute_network.some_vpc.self_link)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_name" {
|
||||
description = "The name of the VPC where the db will live"
|
||||
type = string
|
||||
}
|
||||
71
modules/dbproxy/main.tf
Normal file
71
modules/dbproxy/main.tf
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
data "google_compute_subnetwork" "regional_subnet" {
|
||||
name = var.vpc_name
|
||||
region = var.region
|
||||
}
|
||||
|
||||
resource "google_compute_instance" "db_proxy" {
|
||||
name = "db-proxy"
|
||||
description = <<-EOT
|
||||
A public-facing instance that proxies traffic to the database. This allows
|
||||
the db to only have a private IP address, but still be reachable from
|
||||
outside the VPC.
|
||||
EOT
|
||||
machine_type = var.machine_type
|
||||
zone = var.zone
|
||||
desired_status = "RUNNING"
|
||||
allow_stopping_for_update = true
|
||||
|
||||
# Our firewall looks for this tag when deciding whether to allow SSH traffic
|
||||
# to an instance.
|
||||
tags = ["ssh-enabled"]
|
||||
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
image = "cos-cloud/cos-stable" # latest stable Container-Optimized OS.
|
||||
size = 10 # smallest disk possible is 10 GB.
|
||||
type = "pd-ssd" # use an SSD, not an HDD, because c'mon.
|
||||
}
|
||||
}
|
||||
|
||||
metadata = {
|
||||
enable-oslogin = "TRUE"
|
||||
}
|
||||
|
||||
metadata_startup_script = templatefile("${path.module}/run_cloud_sql_proxy.tpl", {
|
||||
"db_instance_name" = var.db_instance_name,
|
||||
"service_account_key" = module.serviceaccount.private_key,
|
||||
})
|
||||
|
||||
network_interface {
|
||||
network = var.vpc_name
|
||||
subnetwork = data.google_compute_subnetwork.regional_subnet.self_link
|
||||
|
||||
# The access_config block must be set for the instance to have a public IP,
|
||||
# even if it's empty.
|
||||
access_config {}
|
||||
}
|
||||
|
||||
scheduling {
|
||||
# Migrate to another physical host during OS updates to avoid downtime.
|
||||
on_host_maintenance = "MIGRATE"
|
||||
}
|
||||
|
||||
service_account {
|
||||
email = module.serviceaccount.email
|
||||
# These are OAuth scopes for the various Google Cloud APIs. We're already
|
||||
# using IAM roles (specifically, Cloud SQL Editor) to control what this
|
||||
# instance can and cannot do. We don't need another layer of OAuth
|
||||
# permissions on top of IAM, so we grant cloud-platform scope to the
|
||||
# instance. This is the maximum possible scope. It gives the instance
|
||||
# access to all Google Cloud APIs through OAuth.
|
||||
scopes = ["cloud-platform"]
|
||||
}
|
||||
}
|
||||
|
||||
module "serviceaccount" {
|
||||
source = "../serviceaccount"
|
||||
|
||||
name = "cloud-sql-proxy"
|
||||
role = "roles/cloudsql.editor"
|
||||
}
|
||||
23
modules/dbproxy/run_cloud_sql_proxy.tpl
Normal file
23
modules/dbproxy/run_cloud_sql_proxy.tpl
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# We write the key to /var because it's one of the few directories that A) is
|
||||
# writeable, and B) persists between reboots. B is important because GCP will
|
||||
# automatically reboot the server if it goes down. We don't want to lose the
|
||||
# key after a reboot.
|
||||
echo '${service_account_key}' >/var/svc_account_key.json
|
||||
chmod 400 /var/svc_account_key.json
|
||||
|
||||
# TODO: delete this line and add the `--pull=always` flag to `docker run`
|
||||
docker pull gcr.io/cloudsql-docker/gce-proxy:latest
|
||||
|
||||
# -p 127.0.0.1:5432:3306 -- cloud_sql_proxy exposes port 3306 on the container, even for Postgres.
|
||||
# We map 3306 in the container to 5432 on the host. '127.0.0.1' means
|
||||
# that you can only connect to host port 5432 over localhost.
|
||||
# -v /var/svc_account_key.json:/key.json:ro -- The file provisioner will copy the service account key file to /key.json
|
||||
# on the host. We will mount it read-only into the container at the
|
||||
# same path.
|
||||
# -ip_address_types=PRIVATE -- The proxy should only try to connect to the db's private IP.
|
||||
# -instances=${db_instance_name}=tcp:0.0.0.0:3306 -- The instance name will be something like 'my-project:us-central1:my-db'.
|
||||
# The proxy should accept incoming TCP connections on port 3306.
|
||||
docker run --rm -p 127.0.0.1:5432:3306 -v /var/svc_account_key.json:/key.json:ro gcr.io/cloudsql-docker/gce-proxy:latest /cloud_sql_proxy -credential_file=/key.json -ip_address_types=PRIVATE -instances=${db_instance_name}=tcp:0.0.0.0:3306
|
||||
26
modules/dbproxy/variables.tf
Normal file
26
modules/dbproxy/variables.tf
Normal file
@@ -0,0 +1,26 @@
|
||||
// dbproxy module
|
||||
|
||||
variable "db_instance_name" {
|
||||
description = "The name of the Cloud SQL db, e.g. my-project:us-centra1:my-sql-db"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "machine_type" {
|
||||
description = "The type of VM you want, e.g. f1-micro, c2-standard-4"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "The region that the proxy instance will run in (e.g. us-central1)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_name" {
|
||||
description = "The name of the VPC that the proxy instance will run in"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "zone" {
|
||||
description = "The zone where the VM will be created, e.g. us-centra1-a"
|
||||
type = string
|
||||
}
|
||||
15
modules/serviceaccount/main.tf
Normal file
15
modules/serviceaccount/main.tf
Normal file
@@ -0,0 +1,15 @@
|
||||
// serviceaccount module
|
||||
|
||||
resource "google_service_account" "account" {
|
||||
account_id = var.name
|
||||
description = "The service account used by Cloud SQL Proxy to connect to the db"
|
||||
}
|
||||
|
||||
resource "google_project_iam_member" "role" {
|
||||
role = var.role
|
||||
member = "serviceAccount:${google_service_account.account.email}"
|
||||
}
|
||||
|
||||
resource "google_service_account_key" "key" {
|
||||
service_account_id = google_service_account.account.name
|
||||
}
|
||||
8
modules/serviceaccount/outputs.tf
Normal file
8
modules/serviceaccount/outputs.tf
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
output "email" {
|
||||
value = google_service_account.account.email
|
||||
}
|
||||
|
||||
output "private_key" {
|
||||
value = base64decode(google_service_account_key.key.private_key)
|
||||
}
|
||||
11
modules/serviceaccount/variables.tf
Normal file
11
modules/serviceaccount/variables.tf
Normal file
@@ -0,0 +1,11 @@
|
||||
// serviceaccount module
|
||||
|
||||
variable "name" {
|
||||
description = "The service account name (e.g. cloud-sql-proxy)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "role" {
|
||||
description = "The role assigned to the service account (e.g. roles/cloudsql.editor)"
|
||||
type = string
|
||||
}
|
||||
46
modules/vpc/main.tf
Normal file
46
modules/vpc/main.tf
Normal file
@@ -0,0 +1,46 @@
|
||||
// vpc module
|
||||
|
||||
resource "google_compute_network" "vpc" {
|
||||
name = var.name
|
||||
routing_mode = "GLOBAL"
|
||||
auto_create_subnetworks = true
|
||||
}
|
||||
|
||||
# We need to allocate an IP block for private IPs. We want everything in the VPC
|
||||
# to have a private IP. This improves security and latency, since requests to
|
||||
# private IPs are routed through Google's network, not the Internet.
|
||||
resource "google_compute_global_address" "private_ip_block" {
|
||||
name = "private-ip-block"
|
||||
description = "A block of private IP addresses that are accessible only from within the VPC."
|
||||
purpose = "VPC_PEERING"
|
||||
address_type = "INTERNAL"
|
||||
ip_version = "IPV4"
|
||||
# We don't specify a address range because Google will automatically assign one for us.
|
||||
prefix_length = 20 # ~4k IPs
|
||||
network = google_compute_network.vpc.self_link
|
||||
}
|
||||
|
||||
# This enables private services access. This makes it possible for instances
|
||||
# within the VPC and Google services to communicate exclusively using internal
|
||||
# IP addresses. Details here:
|
||||
# https://cloud.google.com/sql/docs/postgres/configure-private-services-access
|
||||
resource "google_service_networking_connection" "private_vpc_connection" {
|
||||
network = google_compute_network.vpc.self_link
|
||||
service = "servicenetworking.googleapis.com"
|
||||
reserved_peering_ranges = [google_compute_global_address.private_ip_block.name]
|
||||
}
|
||||
|
||||
# We'll need this to connect to the Cloud SQL Proxy.
|
||||
resource "google_compute_firewall" "allow_ssh" {
|
||||
name = "allow-ssh"
|
||||
description = "Allow SSH traffic to any instance tagged with 'ssh-enabled'"
|
||||
network = google_compute_network.vpc.name
|
||||
direction = "INGRESS"
|
||||
|
||||
allow {
|
||||
protocol = "tcp"
|
||||
ports = ["22"]
|
||||
}
|
||||
|
||||
target_tags = ["ssh-enabled"]
|
||||
}
|
||||
16
modules/vpc/outputs.tf
Normal file
16
modules/vpc/outputs.tf
Normal file
@@ -0,0 +1,16 @@
|
||||
// vpc module
|
||||
|
||||
output "link" {
|
||||
description = "A link to the VPC resource, useful for creating resources inside the VPC"
|
||||
value = google_compute_network.vpc.self_link
|
||||
}
|
||||
|
||||
output "name" {
|
||||
description = "The name of the VPC"
|
||||
value = google_compute_network.vpc.name
|
||||
}
|
||||
|
||||
output "private_vpc_connection" {
|
||||
description = "The private VPC connection"
|
||||
value = google_service_networking_connection.private_vpc_connection
|
||||
}
|
||||
6
modules/vpc/variables.tf
Normal file
6
modules/vpc/variables.tf
Normal file
@@ -0,0 +1,6 @@
|
||||
// vpc module
|
||||
|
||||
variable "name" {
|
||||
description = "The name of the VPC to create"
|
||||
type = string
|
||||
}
|
||||
6
variables.tf
Normal file
6
variables.tf
Normal file
@@ -0,0 +1,6 @@
|
||||
// root module
|
||||
|
||||
variable "db_password" {
|
||||
description = "The Postgres password"
|
||||
type = string
|
||||
}
|
||||
Reference in New Issue
Block a user