Deployment/Providers/Gcp/Deploying Care with the GCP template
More languages
More actions
This guide covers deploying Care on Google Cloud using the gcp_template repository — an infrastructure-as-code template built with OpenTofu and Helm. It provisions the entire platform (network, cluster, databases, storage, encryption keys) and then installs Care and its supporting services as Helm releases.
This is the recommended path for new GCP deployments. It replaces the older click-through console workflow described in Deploying Care on GKE, which relied on manually created resources, Cloud Source Repositories, and the infra_template manifests.
README.md, environments/sample.tfvars, and root variables.tf against the version you are deploying.What the template provisions
The template is split into four OpenTofu modules, each with its own state and its own Makefile.
| Module | Purpose |
|---|---|
pre-infra/
|
Project bootstrap — enables the required Google Cloud APIs and optionally creates the Cloud DNS zone. |
infra/
|
The core platform — VPC and subnets, the GKE cluster and node pools, Cloud SQL for PostgreSQL, Cloud Storage buckets, Cloud Armor, Workload Identity Federation for GitHub, an optional jump host, and optional Scribe credentials. |
KMS/
|
The KMS key ring and the customer-managed encryption keys used to encrypt Cloud Storage buckets and other resources. |
deploy/
|
The Kubernetes layer — namespace, Secrets and ConfigMaps, and the Helm releases for every application. |
Two further directories support these modules: helm_charts/ holds the in-repo Helm charts, and environments/ holds the sanitised tfvars template. Helper scripts for synchronising configuration with Secret Manager live in scripts/.
Applications installed by the deploy module
| Chart | What it runs |
|---|---|
care_be
|
The Care backend — the API deployment, the Celery worker, the Celery beat scheduler, and a database migration job. Includes optional HPAs for the API and the worker. |
care_fe
|
The Care frontend, served as a container behind the gateway. |
gateway
|
The Gateway API resources, the cert-manager ClusterIssuer and Certificate, the HTTP-to-HTTPS redirect, and the GCP gateway policy.
|
redis
|
Redis, used as the Celery broker and the cache. |
metabase
|
Metabase, backed by its own Cloud SQL instance. |
dcm4chee
|
Optional. The DICOM stack — dcm4chee-arc, OpenLDAP, an nginx proxy, and the OHIF viewer. Enabled with enable_dicom.
|
cert-manager itself is installed from the upstream Jetstack chart with the Gateway API integration enabled, so certificates are issued directly against the Gateway.
Prerequisites
- OpenTofu and Helm installed locally.
- The Google Cloud SDK (
gcloud), authenticated against the target project. - A GCP project with billing enabled, and a project number.
- A GCS bucket to hold the OpenTofu remote state.
- Permission to read and write Secret Manager secrets in the project.
- Domain names for the frontend, the API, and (if used) Metabase and DICOM.
Configuration
All configuration is supplied as a single tfvars file per environment. The canonical template is environments/sample.tfvars; the source of truth for every variable is the root variables.tf.
Real environment files are never committed. They live in Secret Manager under the naming convention tofu-tfvars-<env>, and each module's Makefile pulls the file down at runtime before planning or applying.
environments/<env>.tfvars on your workstation as a transient artefact.Required environment variables
Set these before running any make target:
| Variable | Description |
|---|---|
PROJECT_ID
|
The GCP project ID. May also be supplied as TF_VAR_project_id.
|
ENV_NAME
|
The environment name. May also be supplied as TF_VAR_environment or TF_VAR_env_name.
|
BACKEND_BUCKET
|
The GCS bucket holding the OpenTofu state. |
export BACKEND_BUCKET="<your-state-bucket>"
export PROJECT_ID="<your-gcp-project-id>"
export ENV_NAME="<environment-name>"
Preparing the tfvars file
- Copy the template:
cp environments/sample.tfvars environments/<env>.tfvars - Edit the values for your environment.
- Push it to Secret Manager so the modules can retrieve it:
cd pre-infra
make push-tfvars PROJECT_ID=<gcp-project> ENV_NAME=<env>
Override the default file path with LOCAL_TFVARS_FILE=<path> if your file lives elsewhere.
Key configuration groups
The tfvars file is organised into the following groups. See environments/sample.tfvars for the full annotated template.
| Group | Notable variables |
|---|---|
| Environment identity | region, project_id, project_number, zones, zone, org, app, environment
|
| Domains and DNS | web_domain_name, api_domain_name, metabase_domain_name, dicom_domain_name, enable_dns_zone, dns_zone_domain
|
| Cluster | node_pools (machine type, counts, disk, node locations, private nodes), cluster_name
|
| Networking | database_subnets, gke_subnets, gke_pods_range, gke_services_range, proxy_only_subnet_cidr
|
| Databases | cloudsql_tier, cloudsql_disk_size, cloudsql_read_replica_count, cloudsql_read_replica_tier, metabase_cloudsql_tier
|
| Feature toggles | enable_dicom, enable_cloud_armor, enable_github_wif, enable_scribe, enable_jumphost, enable_legacy_ingress, enable_local_cors
|
| Applications | helm_config — image repository and tag per service, plus the API and Celery worker autoscaling settings
|
| Secrets and config | jwks_base64, additional_secrets, additional_config_map_data
|
| TLS | external_tls_cert, external_tls_key, external_tls_base_domains
|
web_domain_name = ["app.example.org"]. Cloud SQL sizing values are numbers, not quoted strings.Node pools
Node pools are configured as a list of objects. Each entry sets the machine type, the autoscaling bounds, the disk size, the zones the pool spans, and whether the nodes are private.
node_pools = [
{
name = "default"
machine_type = "e2-standard-2"
min_count = 1
max_count = 2
preemptible = false
disk_size_gb = 100
node_locations = "asia-south1-a,asia-south1-b"
enable_private_nodes = true
},
]
Application images and autoscaling
helm_config selects the image for each service and controls the horizontal pod autoscalers on the Care backend. Autoscaling is CPU-based and scales on requests, so it only behaves sensibly when resource requests are set.
helm_config = {
care_backend = {
repository = "asia-south1-docker.pkg.dev/example-project/staging/care"
tag = "latest"
# api_autoscaling_enabled = true
# api_autoscaling_min_replicas = 2
# api_autoscaling_max_replicas = 6
# api_autoscaling_target_cpu = 80
}
care_frontend = {
repository = "asia-south1-docker.pkg.dev/example-project/staging/care_fe"
tag = "latest"
}
metabase = {
repository = "metabase/metabase"
tag = "v0.63.13"
}
redis = {
repository = "redis"
tag = "8-alpine"
}
}
TLS certificates
By default, cert-manager issues certificates for every configured hostname through the Gateway. If you already hold a wildcard certificate, supply it with external_tls_cert, external_tls_key, and external_tls_base_domains. cert-manager continues to issue certificates for any hostname the wildcard does not cover.
The deploy module also creates a self-signed placeholder certificate so the Gateway can come up before cert-manager has issued the real one.
Deploying
Modules must be applied in order. The deploy/ module reads remote state outputs from both infra/ and KMS/, so those must exist first.
pre-infra/infra/KMS/deploy/
README.md in the revision you are deploying. The relationship between infra/ and KMS/ has been revised, and the correct order depends on whether the infra module consumes KMS keys directly.Per-module commands
Each module exposes the same targets. Run them from inside the module directory:
make init BACKEND_BUCKET=<state-bucket>
make pull-tfvars PROJECT_ID=<gcp-project> ENV_NAME=<env>
make plan PROJECT_ID=<gcp-project> ENV_NAME=<env> BACKEND_BUCKET=<state-bucket>
make deploy PROJECT_ID=<gcp-project> ENV_NAME=<env> BACKEND_BUCKET=<state-bucket>
make plan and make deploy both run pull-tfvars first, so the plan always reflects what is currently in Secret Manager. make deploy is interactive unless you pass AUTO_APPROVE=true.
Two further targets are available: make lint formats the OpenTofu files recursively, and make destroy tears the module down.
A full first deployment
export BACKEND_BUCKET="<state-bucket>"
export PROJECT_ID="<gcp-project>"
export ENV_NAME="<env>"
cd pre-infra
make init BACKEND_BUCKET=$BACKEND_BUCKET
make plan PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
make deploy PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
cd ../infra
make init BACKEND_BUCKET=$BACKEND_BUCKET
make plan PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
make deploy PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
cd ../KMS
make init BACKEND_BUCKET=$BACKEND_BUCKET
make plan PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
make deploy PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
cd ../deploy
make init BACKEND_BUCKET=$BACKEND_BUCKET
make plan PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
make deploy PROJECT_ID=$PROJECT_ID ENV_NAME=$ENV_NAME BACKEND_BUCKET=$BACKEND_BUCKET
After the first apply
- Read the Gateway's external IP address from the cluster and create DNS A records for each hostname pointing at it.
- Wait for cert-manager to replace the placeholder certificate with an issued one.
- Confirm the migration job for the Care backend completed successfully before treating the deployment as live.
Releasing a new version
Application releases are configuration changes, not infrastructure changes. To roll out a new Care build:
- Update the
tagfor the relevant service underhelm_configin your environment's tfvars. - Push the updated file to Secret Manager with
make push-tfvars. - Run
make planand thenmake deployfrom thedeploy/module.
Only the deploy/ module needs to run — the platform modules are untouched.
The backend Helm chart annotates its pods with checksums of the rendered Secret and ConfigMap, so a change to configuration triggers a rollout automatically rather than leaving pods running with stale values.
Operational notes
- Never commit real tfvars. Secret Manager is the only place environment configuration should live.
- Plan before every apply.
make planruns with-lock=false, so it is safe to run alongside other readers, but it will not protect you from a concurrent apply. - Helm history is capped at ten revisions per release, which bounds how far back
helm rollbackcan go. - Legacy naming overrides exist for most resources (
cluster_name,vpc_network_name,namespace_name, and others) so an existing deployment can be adopted into the template without renaming live infrastructure. Leave them unset for a greenfield deployment and let the naming convention apply. - The jump host is optional and controlled by
enable_jumphost. It takes its SSH keys fromjumphost_ssh_keys. Disable it if you reach the cluster another way.
See also
- Google Cloud overview — choosing between the deployment options on GCP.
- Deploying Care on GKE — the earlier console-driven workflow.
- Kubernetes reference architecture — the provider-agnostic production setup.
- Data security — security practices that apply to every deployment.