Skip to content

Instantly share code, notes, and snippets.

@wyozi
Created April 10, 2021 13:46
Show Gist options
  • Save wyozi/e83ceb1f91a7be5db4eb1fec681fd26d to your computer and use it in GitHub Desktop.
Save wyozi/e83ceb1f91a7be5db4eb1fec681fd26d to your computer and use it in GitHub Desktop.
Setup Google Cloud SQL Proxy sidecar in Pulumi/Kubernetes with tombstone support
import * as pulumi from '@pulumi/pulumi'
import * as gcp from '@pulumi/gcp'
import * as k8s from '@pulumi/kubernetes'
export class CloudSQLAccount {
readonly serviceAccount: gcp.serviceaccount.Account
readonly serviceAccountKey: gcp.serviceaccount.Key
readonly credentialsSecret: k8s.core.v1.Secret
constructor(dbInstance: gcp.sql.DatabaseInstance, name: string, provider: k8s.Provider) {
this.serviceAccount = new gcp.serviceaccount.Account(name, {
accountId: name
}, { parent: dbInstance })
this.serviceAccountKey = new gcp.serviceaccount.Key(`${name}-key`, {
serviceAccountId: this.serviceAccount.id,
}, { parent: this.serviceAccount })
new gcp.projects.IAMMember(`${name}-iam`, {
member: this.serviceAccount.email.apply(email => `serviceAccount:${email}`),
role: 'roles/cloudsql.client'
}, { parent: this.serviceAccount })
this.credentialsSecret = new k8s.core.v1.Secret(`${name}-credentials`, {
metadata: {
name
},
stringData: {
'credentials.json': this.serviceAccountKey.privateKey.apply(x => Buffer.from(x, 'base64').toString('utf8')),
},
}, { provider })
}
}
const POSTGRES_DB_PORT = 5432
// tombstone impl: https://github.com/kubernetes/kubernetes/issues/25908#issuecomment-365924958
export class CloudSQLProxy {
readonly proxyContainer: pulumi.Output<k8s.types.input.core.v1.Container>
readonly proxyVolume: pulumi.Output<k8s.types.input.core.v1.Volume>
readonly proxyContainerWithTombstone: pulumi.Output<k8s.types.input.core.v1.Container>
constructor(dbInstance: gcp.sql.DatabaseInstance, account: CloudSQLAccount, dbPort: number = POSTGRES_DB_PORT) {
this.proxyContainer = pulumi
.all([
dbInstance.project,
dbInstance.region,
dbInstance.name,
])
.apply(([project, region, instanceName]) => {
return CloudSQLProxy.buildContainer(project, region, instanceName, dbPort)
})
this.proxyContainerWithTombstone = pulumi
.all([
dbInstance.project,
dbInstance.region,
dbInstance.name,
])
.apply(([project, region, instanceName]) => {
return CloudSQLProxy.buildContainer(project, region, instanceName, dbPort, true)
})
const credentialsSecretName = account.credentialsSecret.metadata.apply(metadata => metadata.name)
this.proxyVolume = credentialsSecretName
.apply(secretName => {
return CloudSQLProxy.buildVolume(secretName)
})
}
/**
* Trap to build tombstone upon pod exit
*/
get tombstoneActivatorTrap() {
return `trap "touch /tmp/pod/main-terminated" EXIT`
}
/**
* Tombstone volume that must be mounted on the pod
*/
get tombstoneVolume(): k8s.types.input.core.v1.Volume {
return {
name: 'tombstone-pod',
emptyDir: {}
}
}
/**
* Mount tombstone volume. Main job should have this
*/
get tombstoneVolumeMount(): k8s.types.input.core.v1.VolumeMount {
return {
name: 'tombstone-pod',
mountPath: '/tmp/pod',
}
}
private static buildContainer(project: string, region: string, instanceName: string, dbPort: number, tombstone = false): k8s.types.input.core.v1.Container {
const cmd = tombstone
? `/cloud_sql_proxy -instances=${project}:${region}:${instanceName}=tcp:${dbPort} -credential_file=/var/run/secrets/cloudsql/credentials.json &
CHILD_PID=$!
(while true; do if [[ -f "/tmp/pod/main-terminated" ]]; then kill $CHILD_PID; echo "Killed $CHILD_PID as the main container terminated."; fi; sleep 1; done) &
wait $CHILD_PID
if [[ -f "/tmp/pod/main-terminated" ]]; then exit 0; echo "Job completed. Exiting..."; fi
`
: `/cloud_sql_proxy -instances=${project}:${region}:${instanceName}=tcp:${dbPort} -credential_file=/var/run/secrets/cloudsql/credentials.json`
return {
name: 'cloudsql-proxy',
image: 'gcr.io/cloudsql-docker/gce-proxy:1.21.0-alpine',
command: ['/bin/sh', '-c'],
args: [cmd],
securityContext: {
runAsNonRoot: true
},
volumeMounts: [
{
name: 'cloudsql-instance-credentials',
mountPath: '/var/run/secrets/cloudsql',
readOnly: true,
},
...(tombstone ? [{
name: 'tombstone-pod',
mountPath: '/tmp/pod',
readOnly: true
}] : [])
],
}
}
private static buildVolume(secretName: string): k8s.types.input.core.v1.Volume {
return {
name: 'cloudsql-instance-credentials',
secret: {
secretName: secretName,
},
}
}
}
const cloudSqlAccount = new CloudSQLAccount(databaseInstance, 'cloudsql-account', clusterProvider)
const cloudSqlProxy = new CloudSQLProxy(databaseInstance, cloudSqlAccount)
const appDeployment = new k8s.apps.v1.Deployment(
'app-deployment',
{
spec: {
template: {
spec: {
containers: [
{
// app container
},
cloudSqlProxy.proxyContainer
],
volumes: [
cloudSqlProxy.proxyVolume
]
},
}
}
},
{ provider: clusterProvider }
)
const appDbMigrateJob = new k8s.batch.v1.Job(
'app-db-migrate',
{
spec: {
template: {
spec: {
containers: [
{
name: 'migration-job',
command: ["/bin/sh", "-c"],
args: [
`${cloudSqlProxy.tombstoneActivatorTrap}
./doDbMigration`
],
volumeMounts: [
cloudSqlProxy.tombstoneVolumeMount
]
},
cloudSqlProxy.proxyContainerWithTombstone
],
volumes: [
cloudSqlProxy.proxyVolume,
cloudSqlProxy.tombstoneVolume
]
}
}
}
},
{ provider: clusterProvider }
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment