Deploying Care on GKE
This guide provides a step-by-step process for deploying the Care application on Google Cloud Platform (GCP) using Google Kubernetes Engine (GKE). It combines the full infrastructure setup, the Cloud Build pipeline, and the deployment workflow into a single reference.
The deployment involves three phases:
- Infrastructure — provision the VPC, static IP, Cloud SQL databases, Cloud Storage buckets, and the GKE cluster.
- Build pipeline — set up Artifact Registry, Cloud Source Repositories, the
infra_templateconfiguration, and Cloud Build. - Deploying — apply the Kubernetes manifests, configure DNS, and trigger releases manually or via GitHub Actions.
For an overview of the GCP deployment options and infrastructure requirements, see the Google Cloud overview. To host the Care frontend on Cloud Storage instead of GKE, see Host the frontend on GCS.
Infrastructure
Setting up the network and creating a VPC
Creating a VPC network
- Go to VPC Network in Google Cloud Console.
- Click Create VPC network and name it
care-vpc. - Configure the following settings:
- MTU:
1460 - IPv6 Range: Disabled
- Subnet Creation Mode: Custom
- MTU:
Creating a subnet
- Create a new subnet with these details:
- Name:
cluster-snet - Region:
asia-south1 - IP Stack Type:
IPv4 (single-stack) - IPv4 Range:
10.0.0.0/16 - Private Google Access: On
- Flow Logs: Off
- Name:
- Set Dynamic Routing Mode to
Regional. - Keep the default firewall rules.
Command-line equivalent
To create the VPC and subnet using gcloud:
gcloud compute networks create care-vpc --project=$PROJECT --subnet-mode=custom --mtu=1460 --bgp-routing-mode=regional
gcloud compute networks subnets create cluster-snet --project=$PROJECT --range=10.0.0.0/16 --stack-type=IPV4_ONLY --network=care-vpc --region=asia-south1 --enable-private-ip-google-access
Reserving a static IP address
- Navigate to VPC Networks > IP Addresses.
- Click RESERVE EXTERNAL STATIC IP ADDRESS.
- Configure the following:
- Name:
pip-care - Network Service Tier:
Premium - IP Version:
IPv4 - Type:
Regional - Region:
asia-south1 (Mumbai) - Attached to: None
- Name:
- Note down the assigned IP for future use.
Command-line equivalent:
gcloud compute addresses create pip-care --project=$PROJECT --region=asia-south1
Setting up databases
Creating a Cloud SQL instance
- Go to Google Cloud Console > Cloud SQL.
- Click Create Instance and choose
PostgreSQL. - Configure the first database:
- Instance ID:
care-db - Authentication: Cloud SQL (set a strong master password)
- Database Version:
PostgreSQL 16 - Cloud SQL Edition: Enterprise
- Region:
asia-south1| Primary Zone:asia-south1-a - Machine Type:
2 vCPU, 8 GB RAM, 20 GB SSD - Enable: Automatic storage increases, backups, point-in-time recovery, deletion protection
- Instance IP: Private (assign to
care-vpc)
- Instance ID:
- Create a database named
care. - Repeat for
metabase-db, but configure it with:- Machine Type:
1 vCPU, 3.75 GB RAM - Database Name:
metabase
- Machine Type:
Configuring Cloud Storage
Creating buckets
- Go to Cloud Storage > Buckets > Create.
- Configure the first bucket:
- Name:
<prefix>-care-facility - Location:
asia-south1 (Mumbai),Standard - Access Control: Uniform
- Public Access Prevention: Off
- Name:
- Configure the second bucket:
- Name:
<prefix>-care-patient-data - Public Access Prevention: On
- Retention Policy: 7 days
- Name:
Configuring the service account
- Navigate to Settings > Interoperability.
- Create a service account
care-bucket-accesswith roleStorage Object Admin. - Generate access keys and note them for later use.
Configuring CORS for Cloud Storage
For <prefix>-care-facility:
[
{
"origin": ["*"],
"responseHeader": ["Content-Type"],
"method": ["GET", "HEAD", "PUT", "POST", "DELETE"],
"maxAgeSeconds": 3600
}
]
For <prefix>-care-patient-data:
[
{
"origin": ["care.example.com"],
"responseHeader": ["Content-Type"],
"method": ["GET", "HEAD", "PUT", "POST", "DELETE"],
"maxAgeSeconds": 3600
}
]
Command-line equivalent:
gsutil cors set cors.json gs://<prefix>-care-facility
gsutil cors set cors.json gs://<prefix>-care-patient-data
Configuring Google Kubernetes Engine (GKE)
Creating a GKE cluster
- Navigate to Kubernetes Engine > Clusters > Create.
- Choose Standard Mode.
- Configure cluster settings:
- Name:
care-gke - Location:
Zonal - Zone:
asia-south1-a
- Name:
Configuring node pools
- Select
default pooland set nodes to2. - In Nodes section:
- Machine Type:
E2-Series,e2-standard-2(2 vCPU, 8 GB RAM)
- Machine Type:
- In Networking section:
- Network:
care-vpc - Subnet:
cluster-snet - Access: Public Cluster
- Network:
- Enable HTTP Load Balancing.
Build pipeline
Setting up the Artifact Registry
- Navigate to the Artifact Registry in the Google Cloud Console.
- Create private Artifact Registry repositories named
careandcare_fe. - Ensure that the repositories are mutable.
- Use the Default Encryption Key for the repositories.
Setting up Cloud Source Repositories
- Navigate to the Cloud Source Repositories service in the Google Cloud Console.
- Create a new repository named
infra-name. - Add the files
build/react.envandplug_config.pyto the repository. (Theplug_config.pyfile can be used to include plugins forcare.) - Add the following content to the
react.envfile:
REACT_PLAUSIBLE_SERVER_URL=https://plausible.example.com
REACT_HEADER_LOGO='{"light":"https://cdn.ohc.network/header_logo.png","dark":"https://cdn.ohc.network/header_logo.png"}'
REACT_MAIN_LOGO='{"light":"https://cdn.ohc.network/light-logo.svg","dark":"https://cdn.ohc.network/black-logo.svg"}'
REACT_GMAPS_API_KEY="examplekey"
REACT_GOV_DATA_API_KEY=""
REACT_RECAPTCHA_SITE_KEY=""
REACT_SENTRY_DSN=""
REACT_SAMPLE_FORMAT_ASSET_IMPORT=""
REACT_SAMPLE_FORMAT_EXTERNAL_RESULT_IMPORT=""
REACT_KASP_ENABLED=""
REACT_ENABLE_HCX=""
REACT_ENABLE_ABDM=""
REACT_ENABLE_SCRIBE=""
REACT_WARTIME_SHIFTING=""
REACT_OHCN_URL=""
REACT_PLAUSIBLE_SITE_DOMAIN="care.example.com"
REACT_SENTRY_ENVIRONMENT=""
REACT_CARE_API_URL="https://care.example.com"
REACT_DASHBOARD_URL=""
- Add the following content to the
plug_config.pyfile (if required):
from plugs.manager import PlugManager
from plugs.plug import Plug
hcx_plugin = Plug(
name="hcx",
package_name="git+https://github.com/ohcnetwork/care_hcx.git",
version="@main",
configs={},
)
plugs = [hcx_plugin]
manager = PlugManager(plugs)
- Clone the infra_template repository. This repository contains the necessary YAML files to deploy our applications as Kubernetes workloads. You will need to replace all generic/example values with your production values. Here's a guide on what to change in each folder:
- Certificate: Replace the example hostnames for
dnsNameswith your actual hostnames. - ConfigMaps:
- In
care-configmap.yaml, add your database configurations. - Update the hostnames in
CSRF_TRUSTED_ORIGINSandDJANGO_ALLOWED_HOSTS. - In
nginx.yaml, update theserver_namewith your hostnames.
- In
- Helm:
- Install Helm if you haven't already.
- Use the static IP created from the "Reserve a static IP address" step to replace the IP value in
helm/scripts.sh.
- Ingress: Replace the example hostnames with your actual hostnames.
- Secrets:
- Update
care-secrets.ymlwith your secrets. - Update
metabase.ymlwith your Metabase database credentials.
- Update
- Certificate: Replace the example hostnames for
- Push the changes to the
infra-namerepository created earlier in Cloud Source Repositories.
Setting up the Cloud Build project
- Navigate to the Cloud Build service in the Google Cloud Console.
- Establish a new build project, name it
deploy-care. - Generate a new trigger for the project.
- Configure the Event to respond to Webhook events.
- Retrieve the Webhook URL from the
Show URL Previewoption. - Define the Configuration as
Cloud Build configuration file (yaml or json). - Specify the Location as
Inlineand insert the following content:
steps:
- name: ubuntu
args:
- '-c'
- |
echo "be $_BE_TAG" \
&& echo "fe $_FE_TAG" \
&& echo "metabase $_METABASE_TAG"
entrypoint: bash
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
args:
- '-c'
- |
gcloud source repos clone $_INFRA_REPO infra
cd infra
git config user.name "CloudBuild"
git config user.email example@cloudbuild.gserviceaccount.com
dir: /workspace
id: clone-infra
entrypoint: bash
- name: gcr.io/cloud-builders/gsutil
args:
- '-c'
- |
if [[ -n "$_BE_TAG" ]]; then
curl -L https://github.com/ohcnetwork/care/archive/$_BE_TAG.zip -o care.zip
unzip care.zip
mv care-$_BE_TAG care
else
echo "Skipping..."
fi
dir: /workspace
id: download-care
entrypoint: bash
- name: ubuntu
args:
- '-c'
- |
if [[ -n "$_BE_TAG" ]]; then
cp -r /workspace/infra/build/. /workspace/care
else
echo "Skipping..."
fi
id: copy-build-files
entrypoint: bash
- name: gcr.io/cloud-builders/docker
args:
- '-c'
- |
if [[ -n "$_BE_TAG" ]]; then
DOCKER_BUILDKIT=1 docker build -f ./care/docker/prod.Dockerfile \
-t asia-south1-docker.pkg.dev/$PROJECT_ID/care/care:$_BE_TAG \
-t asia-south1-docker.pkg.dev/$PROJECT_ID/care/care:latest \
./care
docker push \
asia-south1-docker.pkg.dev/$PROJECT_ID/care/care:$_BE_TAG
docker push \
asia-south1-docker.pkg.dev/$PROJECT_ID/care/care:latest
else
echo "Skipping..."
fi
dir: /workspace
id: build-care
entrypoint: bash
- name: gcr.io/cloud-builders/git
args:
- '-c'
- |
if [[ -n "$_BE_TAG" ]]; then
cd infra
sed -i -e 's|\(image: .*care:\).*|\1$_BE_TAG|' deployments/*
sed -i -e "/name: deployment-version/{n;s/value: .*/value: \"$BUILD_ID\"/;}" deployments/care-backend.yaml
sed -i -e "/name: deployment-version/{n;s/value: .*/value: \"$BUILD_ID\"/;}" deployments/care-celery-worker.yaml
sed -i -e "/name: deployment-version/{n;s/value: .*/value: \"$BUILD_ID\"/;}" deployments/care-celery-beat.yaml
git add .
git commit -m "update backend crds to $_BE_TAG" || true
else
echo "Skipping..."
fi
dir: /workspace
id: update-care-crd
entrypoint: bash
- name: gcr.io/cloud-builders/gsutil
args:
- '-c'
- |
if [[ -n "$_FE_TAG" ]]; then
curl -L https://github.com/ohcnetwork/care_fe/archive/$_FE_TAG.zip -o /workspace/care_fe.zip
unzip /workspace/care_fe.zip -d /workspace
mv /workspace/care_fe-$_FE_TAG /workspace/care_fe
cp /workspace/infra/build/react.env /workspace/care_fe/.env.local
cd /workspace/care_fe
else
echo "Skipping..."
fi
dir: /workspace
id: download-care-fe
entrypoint: bash
- name: ubuntu
args:
- '-c'
- |
if [[ -n "$_FE_TAG" ]]; then
cp /workspace/infra/build/react.env /workspace/care_fe/.env.local
else
echo "Skipping..."
fi
id: copy-fe-build-files
entrypoint: bash
- name: gcr.io/cloud-builders/docker
args:
- '-c'
- |
if [[ -n "$_FE_TAG" ]]; then
DOCKER_BUILDKIT=1 docker build -f ./care_fe/Dockerfile \
-t asia-south1-docker.pkg.dev/$PROJECT_ID/care/care_fe:$_FE_TAG \
-t asia-south1-docker.pkg.dev/$PROJECT_ID/care/care_fe:latest \
./care_fe
docker push \
asia-south1-docker.pkg.dev/$PROJECT_ID/care/care_fe:$_FE_TAG
docker push \
asia-south1-docker.pkg.dev/$PROJECT_ID/care/care_fe:latest
else
echo "Skipping..."
fi
dir: /workspace
id: build-care-fe
entrypoint: bash
- name: gcr.io/cloud-builders/git
args:
- '-c'
- |
if [[ -n "$_FE_TAG" ]]; then
cd infra
sed -i -e 's|\(image: .*care_fe:\).*|\1$_FE_TAG|' deployments/care-fe.yaml
sed -i -e "/name: deployment-version/{n;s/value: .*/value: \"$BUILD_ID\"/;}" deployments/care-fe.yaml
git add .
git commit -m "update frontend crds to $_FE_TAG" || true
else
echo "Skipping..."
fi
dir: /workspace
id: update-care-fe-crd
entrypoint: bash
- name: gcr.io/cloud-builders/git
args:
- '-c'
- |
if [[ -n "$_METABASE_TAG" ]]; then
cd infra
sed -i -e 's|\(image: metabase/metabase:\).*|\1$_METABASE_TAG|' deployments/metabase.yaml
git add .
git commit -m "update frontend crds" || true
else
echo "Skipping..."
fi
dir: /workspace
id: update-metabase-crd
entrypoint: bash
- name: gcr.io/cloud-builders/gke-deploy
args:
- '-c'
- |
gke-deploy apply \
--location=${_CARE_GKE_ZONE} \
--cluster=${_CARE_GKE_CLUSTER} \
--filename=infra/deployments
dir: /workspace
id: deploy-to-gke
entrypoint: bash
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
args:
- '-c'
- |
cd infra
git pull --rebase
git push
dir: /workspace
id: push-crds
entrypoint: bash
options:
logging: CLOUD_LOGGING_ONLY
substitutions:
_FE_TAG: $(body.substitutions.care_fe_tag)
_METABASE_TAG: $(body.substitutions.metabase_tag)
_BE_TAG: $(body.substitutions.care_be_tag)
- Save the file and exit the editor.
- Configure the Substitution variables for the Cloud Build trigger:
- Navigate to the Substitution variables tab.
- Select the
Add substitution variablebutton. - Input the following variables:
- Name:
_FE_TAG, Value: `` - Name:
_METABASE_TAG, Value: `` - Name:
_BE_TAG, Value: `` - Name:
_CARE_GKE_ZONE, Value: `` - Name:
_CARE_GKE_CLUSTER, Value: `` - Name:
_INFRA_REPO, Value: ``
- Name:
- Save your changes.
- In a separate tab, create a Service Account for Cloud Build:
- Navigate to the IAM Section.
- Name the account
cloudbuilder. - Assign the following roles:
- Artifact Registry Administrator
- Cloud Build Service Account
- Cloud Run Source Developer
- Cloud Source Repositories Service Agent
- Kubernetes Engine Admin
- Logs Bucket Writer
- Secret Manager Secret Accessor
- Source Repository Reader
- Source Repository Writer
- Assign the Service Account to the Cloud Build trigger:
- Navigate to the Service account tab.
- Choose the
cloudbuilderservice account from the list.
- Save the trigger by selecting the
Createbutton.
Deploying
Getting ready for first deployment
Set the default GKE cluster
# Get the name using:
kubectl config get-contexts
# Set the config:
kubectl config use-context <name>
Run the Helm script
bash helm/scripts.sh
Apply Kubernetes configurations
Use kubectl to apply all the Kubernetes YAML files in the following order:
# Deploy ConfigMaps:
kubectl apply -f 'configmaps/*'
# Secrets:
kubectl apply -f 'secrets/*'
# Deployments:
kubectl apply -f 'deployments/*'
# Services:
kubectl apply -f 'services/*'
# ClusterIssuer:
kubectl apply -f ClusterIssuer/cluster-issuer.yaml
# Certificate:
kubectl apply -f certificate/certificate.yml
# Ingress:
kubectl apply -f ingress/care.yaml
Once ingress is created, kubectl get ingress care-ingress will show the IP of the TCP load balancer. Once the DNS records are added, the SSL will be automatically handled.
Add DNS records
Create DNS A records for each domain pointing to the static IP created from the "Reserve a static IP address" step.
Applying release updates on GCP manually
To apply release updates, follow these steps:
- Fetch the latest commit hash from the Care Commit History and Care FE Commit History.
- Trigger the Cloud Build using the webhook URL available in the Cloud Build console.
- Use
curlor any API platform to initiate the build process:
curl --request POST \
--url 'https://cloudbuild.googleapis.com/v1/projects/examplelink' \
--header 'Content-Type: application/json' \
--data '{
"substitutions": {
"care_be_tag": "",
"care_fe_tag": "",
"metabase_tag": ""
}
}'
Setting up automated GitHub workflow triggers
The manual process of triggering the build process can be automated using GitHub Actions. Follow these steps:
- Navigate to the deploy repository where the GitHub Actions are to be set up.
- Add the Webhook URL to the GitHub Secrets.
- Create a new GitHub Action workflow file in the
.github/workflowsdirectory. - Add the following code snippet to the workflow file:
name: Deploy Multiple Projects
on:
workflow_dispatch:
inputs:
BE_TAG:
description: "Backend release tag"
required: true
FE_TAG:
description: "Frontend release tag"
required: true
METABASE_TAG:
description: "Metabase release tag"
required: false
jobs:
trigger-post-requests:
runs-on: ubuntu-latest
steps:
- name: Setup Payload
run: |
JSON='{ "substitutions": { "care_be_tag":"'"$BE_TAG"'", "care_fe_tag": "'"$FE_TAG"'", "metabase_tag": "'"$METABASE_TAG"'" } }'
echo "json=$JSON" >> $GITHUB_ENV
env:
BE_TAG: ${{ github.event.inputs.BE_TAG }}
FE_TAG: ${{ github.event.inputs.FE_TAG }}
METABASE_TAG: ${{ github.event.inputs.METABASE_TAG }}
- name: Deploy Project 1
env:
SECRET_URL: ${{ secrets.WEBHOOK_P1 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
- name: Deploy Project 2
env:
SECRET_URL: ${{ secrets.WEBHOOK_P2 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
- name: Deploy Project 3
env:
SECRET_URL: ${{ secrets.WEBHOOK_P3 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
- name: Deploy Project 4
env:
SECRET_URL: ${{ secrets.WEBHOOK_P4 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
- name: Deploy Project 5
env:
SECRET_URL: ${{ secrets.WEBHOOK_P5 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
- name: Deploy Project 6
env:
SECRET_URL: ${{ secrets.WEBHOOK_P6 }}
run: |
curl -X POST -H "Content-Type: application/json" \
-d "$json" \
$SECRET_URL
Applying release updates
To apply release updates, follow these steps:
- Retrieve the latest commit hash from the Care Commit History and Care FE Commit History.
- Go to the deploy repository where the GitHub Action mentioned above is set up.
- Click on the
Actionstab. - Choose the
Deploy Multiple Projectsworkflow. - Click on the
Run Workflowbutton. - Input the latest commit hash for both the backend and frontend.
- Click on the
Run Workflowbutton again.
These steps will initiate the build pipeline for the backend and frontend projects across all the projects set up in the GitHub Secrets.