For my projects, I like to have a single "boot script" that runs at start-up time. That script then looks at some metadata in order to decide "what to do next".

This has some cool side-effects, but the biggest "feature" for me is that I can publish a single .tar.gz package of the application (using npm pack for example), and the boot script can rely on the instance's metadata to run any of the services needed for my app.

For those of you just looking for "how do I safely get metadata into a shell variable":

#!/bin/sh
getMetadata() {  
  # -f says "If there's a 404, just return nothing (fail)"
  # -s says "Shut up, please (silent)"
  curl -fs http://metadata/computeMetadata/v1/instance/attributes/$1 \
    -H "Metadata-Flavor: Google"
}
MYVAR=`getMetadata myvar`  

Now, onto some code.

boot.sh

This should run as the "startup script" for all machines. In my case, it's also deployed to GCS every time code hits the master branch in a Git repository.

This isn't a problem because this main boot script doesn't do anything specific with the rest of the code. It only figures out the version of the package to download and runs the boot script for the service (which lives inside the package just downloaded).

In other words, the only job of this main boot script is to download the right package. The rest of the work is done by the scripts inside that package (which are version controlled).

#!/bin/sh

# Sometimes start-up scripts don't set root's home, which can be a problem.
export HOME=/root  
cd $HOME

# The method from above.
getMetadata() {  
  curl -fs http://metadata/computeMetadata/v1/instance/attributes/$1 \
    -H "Metadata-Flavor: Google"
}

# Install Node (general requirement).
curl -sL https://deb.nodesource.com/setup_4.x | bash -  
apt-get install -y nodejs

# Grab the version from the instance metadata.
VERSION=`getMetadata version`

# If there's no version set in metadata, figure out the latest one using GCS's `ls` command.
if [ -z "$VERSION" ]; then  
  cat > version_sort.py << EOF
import sys  
from distutils.version import LooseVersion as Version  
prefix, suffix = 'gs://a-gcs-bucket/myapp-', '.tgz\n'  
lines = [line[len(prefix):-len(suffix)] for line in sys.stdin.readlines()]  
print ''.join([prefix, sorted(lines, key=Version)[-1], suffix])  
EOF  
  PACKAGE=`gsutil ls gs://a-gcs-bucket | grep -P 'myapp-\d\.\d\.\d\.tgz' | python version_sort.py`
else  
  PACKAGE="gs://a-gcs-bucket/myapp-$VERSION.tgz"
fi

# Download the package from GCS.
gsutil cp $PACKAGE .

# Install the package and remove the bundle.
npm install `basename $PACKAGE`  
rm `basename $PACKAGE`

# Figure out which service boot script to run and run it, if applicable.
SERVICE=`getMetadata service`  
if [ ! -z "$SERVICE" ]; then  
  sh node_modules/myapp/scripts/$SERVICE-boot.sh
fi  

This script should be set to run at start-up time, ideally using the startup-script-url metadata parameter.

[service]-boot.sh

This script is packaged with the app and should "start up" your specific service. In this case, we're calling the service api, so the file is named api-boot.sh.

#!/bin/sh

# Install nginx
apt-get install -y nginx

# Update nginx configuration
service nginx stop  
rm /etc/nginx/sites-enabled/*  
cp node_modules/myapp/config/nginx.conf /etc/nginx/sites-available/api.myapp.com  
ln -s /etc/nginx/sites-available/api.myapp.com /etc/nginx/sites-enabled/api.myapp.com  
service nginx start

# Install pm2
npm install -g pm2@latest

# Run the app
export NODE_ENV=production  
pm2 start node_modules/myapp/api/app.js --name "api.myapp.com"  

To make this all work, just start your machine with metadata for:

  • startup-script-url: gs://your-bucket/boot.sh
  • service: api
  • version: 0.0.5 (optional, but fill in with a version uploaded to your bucket)

For more details on metadata and startup scripts, check out Google's official documentation: