Google's CloudBuild (a.k.a. "Container Builder") is an on-demand, container-based build service offered under the Google Cloud Platform (GCP). For you and me, it is a nice alternative to maintaining and paying for our own build server, and a clever addition to anyone's CI stack.
CloudBuild allows you to start from a source (e.g. a Google Cloud Source repo, a GCS bucket - or perhaps even nothing (a blank directory; "scratch"), incrementally apply several Docker container runs upon it, and publish the final result to a desired location: like a Docker repository or a GCS bucket.
With its wide variety of custom builders, CloudBuild can do almost anything - that is, as far as I see so far, anything that can be achieved by a Docker container and a volume mount can be fulfilled in CloudBuild as well. To our great satisfaction, this includes fetching sources from GitHub/BitBucket repos (in addition to the native source location options), running custom commands like zip
, and much more!
Above all this (how nice of GCP!), CloudBuild gives you 2 whole hours (120 minutes) of build time per day, for free - in comparison to the comparable CodeBuild service of AWS, which offers just 1 hour and 40 minutes per month!
So, now let's have a look at how we can run a CloudBuild via JS (server-side NodeJS):
First things first: adding googleapis:28.0.1
to our dependency list;
{ "dependencies": { "googleapis": "28.0.1" } }
Don't forget the npm install
!
In our logic flow, first we need to get ourselves authenticated; with the google-auth-library
module that comes with googleapis
, this is quite straightforward because the client can be fed with a JWT
auth client right from the beginning, which will handle all the auth stuff behind the scenes:
const projectId = "my-gcp-project-id"; const google = require("googleapis").google; const key = require("./keys.json"); const jwtClient = new google.auth.JWT({ email: key.client_email, key: key.private_key, scopes: ["https://www.googleapis.com/auth/cloud-platform"] }); google.options({auth: jwtClient});
Note that, for the above code to work verbatim, you need to place a service account key file in the current directory (usually obtained by creating a new service account via the Google Cloud console, in case you don't already have one).
Now we can simply retrieve the v1
version of the cloudbuild
client from google
, and start our magic:
const builds = google.cloudbuild("v1").projects.builds;
First we submit a build "spec" to the CloudBuild service. Below is an example for a typical NodeJS module on GitHub:
builds.create({ projectId: projectId, resource: { steps: [ { name: "gcr.io/cloud-builders/git", args: ["clone", "https://github.com/slappforge/slappforge-sdk", "."] }, { name: "gcr.io/cloud-builders/npm", args: ["install"] }, { name: "kramos/alpine-zip", args: [ "-q", "-x", "package.json", ".git/", ".git/**", "README.md", "-r", "slappforge-sdk.zip", "." ] }, { name: "gcr.io/cloud-builders/gsutil", args: [ "cp", "slappforge-sdk.zip", "gs://sdk-archives/slappforge-sdk/$BUILD_ID/slappforge-sdk.zip" ] } ] } }) .catch(e => { throw Error("Failed to start build: " + e); })
Basically we retrieve the source from GitHub, fetch the dependencies via a npm install
, bundle the whole thing using a zip
command container (took me a while to figure it out, which is why I'm posting this!) and upload the resulting zip to a GCS bucket.
We can tidy this up a bit (and perhaps make the template reusable for subsequent builds, by extracting out the parameters into a substitutions
section:
const repoUrl = "https://github.com/slappforge/slappforge-sdk"; const projectName = "slappforge-sdk"; const bucket = "sdk-archives"; builds.create({ projectId: projectId, resource: { steps: [ { name: "gcr.io/cloud-builders/git", args: ["clone", "$_REPO_URL", "."] }, { name: "gcr.io/cloud-builders/npm", args: ["install"] }, { name: "kramos/alpine-zip", args: [ "-q", "-x", "package.json", ".git/", ".git/**", "README.md", "-r", "$_PROJECT_NAME.zip", "." ] }, { name: "gcr.io/cloud-builders/gsutil", args: [ "cp", "$_PROJECT_NAME.zip", "gs://$_BUCKET_NAME/$_PROJECT_NAME/$BUILD_ID/$_PROJECT_NAME.zip" ] } ], substitutions: { _REPO_URL: repoUrl, _PROJECT_NAME: projectName, _BUCKET_NAME: bucket } } }) .catch(e => { throw Error("Failed to start build: " + e); })
Once the build is started, we can monitor it like so (with a few, somewhat neat wrappers to properly manage the timer logic):
.then(response => { let timer = { handle: null }; startTimer(() => { return builds.get({ projectId: projectId, id: response.data.metadata.build.id }) .catch(e => { throw e; }) .then(response => { const COMPLETE_STATES = ["SUCCESS", "DONE", "FAILURE", "CANCELLED"]; if (COMPLETE_STATES.includes(response.data.status)) { return false; } return true; }) }, timer, 5000); }); // small utility to run a timer task without multiple concurrent requests const startTimer = (func, timer, period) => { let caller = () => { func().then(repeat => { if (repeat) { timer.handle = setTimeout(caller, period); } }); }; timer.handle = setTimeout(caller, period); };
Once the build reaches a steady state, you are done!
If you want fine-grained details, just dig into response.data
within the timer callback blocks.
Happy CloudBuilding!
No comments:
Post a Comment
Thanks for commenting! But if you sound spammy, I will hunt you down, kill you, and dance on your grave!