Initial commit

This commit is contained in:
Ryan Boehning
2020-04-16 20:05:56 -07:00
commit f7ef3d241a
17 changed files with 474 additions and 0 deletions

36
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}

View 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

View 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
}

View 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
}

View 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)
}

View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
// vpc module
variable "name" {
description = "The name of the VPC to create"
type = string
}

6
variables.tf Normal file
View File

@@ -0,0 +1,6 @@
// root module
variable "db_password" {
description = "The Postgres password"
type = string
}