In this section, we'll create our first deployment.
We're nearly there!
We're up to deploying our project for the first time!
This is going to be a bit complicated at first, but the work we do here will make every other deployment much simpler.
You'll remember way back when we were testing locally we built two images: a web
image and a db
image. db
will be replaced with the proper PostgreSQL database we've already setup. So we just need to build the other image!
For that, we'll be using Cloud Build. This is a similar process to docker build .
, but this will build the image on Google Cloud itself, and publish the image to the Google Cloud image repository, gcr.io
.
If you haven't already done so, you'll need a copy of this source code:
git clone https://github.com/GoogleCloudPlatform/django-demo-app-unicodex
cd django-demo-app-unicodex
Then, we'll build the image:
gcloud builds submit --tag gcr.io/$PROJECT_ID/$SERVICE_NAME .
Then, we can (finally!) create our Cloud Run service, telling it about the image we just created, the database we configured earlier, and the service account we set up. And just for good measure, we'll allow public access:
gcloud run deploy $SERVICE_NAME \
--allow-unauthenticated \
--image gcr.io/$PROJECT_ID/$SERVICE_NAME \
--add-cloudsql-instances $PROJECT_ID:$REGION:$INSTANCE_NAME \
--service-account $CLOUDRUN_SA
Note: We are using the fully-qualified database instance name here. Although not strictly required, as our database is in the same project and region, it helps with clarity.
Sadly, we have a few more steps. Even though we have deployed our service, Django won't work yet. You could try and navigate to the unicodex-HASH-region.a.run.app
URL, but it will show an error. This is because:
- We need to tell Django where to expect our service to run, and
- we need to initialise our database.
Django has a setting called ALLOWED_HOSTS
, which recommended to be defined for security purposes. We want our set our ALLOWED_HOSTS
to be the service URL of the site we just deployed.
New in Django 4.0, we will also want to set the TRUSTED_CSRF_DOMAIN
value as well. Read more about how not setting this can break Cloud Run deployments.
When we deployed our service, it told us the service URL that our site can be accessed from. We can either copy the URL from the output we got from the last step, or we can get it from gcloud
gcloud run services list
We could copy the URL from this output, or we can use a --format
parameter:
export SERVICE_URL=$(gcloud run services describe $SERVICE_NAME \
--format "value(status.url)")
echo $SERVICE_URL
Then, we can redeploy our service, updating just this new environment variable:
gcloud run services update $SERVICE_NAME \
--update-env-vars "CURRENT_HOST=${SERVICE_URL}"
In this case, CURRENT_HOST
is setup in our settings.py to be parsed into the ALLOWED_HOSTS
and TRUSTED_CSRF_DOMAIN
settings.
Important: Django's ALLOWED_HOSTS
takes a hostname without a scheme (i.e. without the leading 'https'). Our settings.py
handles this by removing it, if it appears.
Our database currently has no schema or data, so we need to now set that up.
Back in our local testing, we did this by executing migrate
and loaddata
from the command line.
The problem is, we don't have a command-line. 🤦♂️
Well, we do, we have Cloud Shell; but that's not really automateable. We don't want to have to log into the console every time we have to run a migration. Well, okay, we could, and this is absolutely a valid option if your setup requires/demands it, but for the scope of our application we're going to take the time now to automate migration during our deployment.
We're going to use Cloud Build and instead of just building the image like we did earlier, we'll make it perform build our image, apply our database migrations, and deploy our service; all at once.
But to start, we need to give Cloud Build permission to do all these fancy things (like deployment:
for role in cloudsql.client run.admin; do
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member serviceAccount:${CLOUDBUILD_SA} \
--role roles/${role}
done
We'll also need to ensure that for the final step in our deployment, Cloud Build has permission to deploy as Cloud Run. For that, we'll configure our Cloud Build to act as our service account:
gcloud iam service-accounts add-iam-policy-binding ${CLOUDRUN_SA} \
--member "serviceAccount:${CLOUDBUILD_SA}" \
--role "roles/iam.serviceAccountUser"
You can check the current roles by:
- running
gcloud projects get-iam-policy $PROJECT_ID
, or - going to the IAM & Admin page in the console.
From here, we can then run our gcloud builds submit
command again, but this time specifying a configuration file:
# migrate and deploy
gcloud builds submit \
--config .cloudbuild/build-migrate-deploy.yaml \
--substitutions "_REGION=${REGION},_INSTANCE_NAME=${INSTANCE_NAME},_SERVICE=${SERVICE_NAME}"
This command will take a few minutes to complete, but the output will show you what it's doing as it goes.
As suggested by the --config
filename, this will perform three tasks for us:
- Build the image (like we were doing before),
- 'migrate', and
- deploy the service with the new image.
By 'migrate', we mean:
- Configuring an environment using the secrets we setup earlier, to them
- run the django management commands:
./manage.py migrate
, which applies our database migrations./manage.py collectstatic
, which uploads our local static files to the media bucket
⚠️ These commands need to be run at least once, but you can choose to remove this part of the script later (for instance, if you want to manually do database migrations).
The full contents of the script is in .cloudbuild/build-migrate-deploy.yaml.
Noted custom configurations:
- We use
gcr.io/google-appengine/exec-wrapper
as an easier way to setup a Cloud SQL proxy to interface with our database. - We use the Secret Manager client library to import specific secrets in our
settings.py
- We are explicitly doing all these things as three steps in one configuration.
We are also running this command with substitutions. These allow us to change the image, service, and database instance (which will be helpful later on when we define multiple environments). You can hardcode these yourself by commenting out the substitutions:
stanza in the yaml file.
And now.
Finally.
You can see the working website!
Go to the SERVICE_URL
in your browser, and gaze upon all you created.
You did it! 🏆
You can also log in with the superuser
/superpass
, and run the admin action we did in local testing.
🤔 But what if all this didn't work? Check the Debugging Steps.
If this is as far as you want to take this project, think about cleaning up your resources.
After all this work, each future deployment is exceedingly less complex.
Next step: Ongoing Deployments