Created
April 10, 2021 13:46
-
-
Save wyozi/e83ceb1f91a7be5db4eb1fec681fd26d to your computer and use it in GitHub Desktop.
Setup Google Cloud SQL Proxy sidecar in Pulumi/Kubernetes with tombstone support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
}, | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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