Infrastructure as code: using Pulumi to provision and bootstrap a GCP instance

This post will show you how to use Pulumi as the infrastructure-as-code tooling for provisioning an instance in the Google Cloud Platform. I will use Python for the Pulumi code examples. I ran the examples below on MacOS.
I used brew to install the pulumi CLI:
$ brew install pulumiIf you already had pulumi installed, you can upgrade it to the latest release:
$ brew upgrade pulumiThe next step was to download the gcloud SDK and CLI and to set it up following the setup instructions here:
https://www.pulumi.com/docs/intro/cloud-providers/gcp/setup/
$ gcloud auth login
You are now logged in as [myuser@example.com].Your current project is [myproject]. You can change this setting by running:$ gcloud config set project PROJECT_ID$ gcloud auth application-default login
Credentials saved to file: [/Users/ggheo/.config/gcloud/application_default_credentials.json]These credentials will be used by any library that requestsApplication Default Credentials.To generate an access token for other uses, run:gcloud auth application-default print-access-tokenI then created a new Pulumi project for Python in GCP. I named the project gcpinfra and I kept the default stack name which is dev.
$ pulumi new gcp-python
This command will walk you through creating a new Pulumi project.Enter a value or leave blank to accept the (default), and press <ENTER>.Press ^C at any time to quit.project name: (gcpinfra)project description: (A minimal Google Cloud Python Pulumi program) Provision GCP infrastructure resourcesCreated project ‘gcpinfra’Please enter your desired stack name.
To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).stack name: (dev) devCreated stack ‘dev’
project: The Google Cloud project to deploy into: myprojectSaved config
Your new project is ready to go! ✨To perform an initial deployment, run the following commands:1. virtualenv -p python3 venv2. source venv/bin/activate3. pip3 install -r requirements.txtThen, run ‘pulumi up’I created and activated a virtual environment based on Python 3, then I installed the Pulumi requirements via pip3 install -r requirements.txt:
$ virtualenv -p /usr/local/bin/python3 venv
Running virtualenv with interpreter /usr/local/bin/python3Using base prefix ‘/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7’
New python executable in /Users/ggheo/code/mycode/codepraxis/mymooc/pulumi/gcpinfra/venv/bin/python3.7
Also creating executable in /Users/ggheo/code/pulumi/gcpinfra/venv/bin/python
Installing setuptools, pip, wheel…
done.$ source venv/bin/activate$ pip3 install -r requirements.txtAt this point, I was able to see the following files created:
$ ls -latotal 40drwxr-xr-x 8 ggheo staff 256 Nov 16 15:53 .drwxr-xr-x 4 ggheo staff 128 Nov 16 15:51 ..
-rw — — — — 1 ggheo staff 12 Nov 16 15:51 .gitignore
-rw-r — r — 1 ggheo staff 35 Nov 16 15:52 Pulumi.dev.yaml
-rw — — — — 1 ggheo staff 78 Nov 16 15:51 Pulumi.yaml
-rw — — — — 1 ggheo staff 203 Nov 16 15:51 __main__.py
-rw — — — — 1 ggheo staff 32 Nov 16 15:51 requirements.txt
drwxr-xr-x 6 ggheo staff 192 Nov 16 15:53 venvThe sample gcp-python project from Pulumi creates the following Pulumi code:
$ cat __main__.py
import pulumi
from pulumi_gcp import storage
# Create a GCP resource (Storage Bucket)
bucket = storage.Bucket(‘my-bucket’)
# Export the DNS name of the bucket
pulumi.export(‘bucket_name’, bucket.url)My next step was to set a series of Pulumi configuration variables that I am going to use in my code:
GCP specific:
$ pulumi config set gcp:project myproject
$ pulumi config set gcp:region us-central1
$ pulumi config set gcp:zone us-central1-aApplication-specific:
$ pulumi config set instance_name dev
$ pulumi config set instance_type n1-highmem-2
$ pulumi config set instance_image ubuntu-1604-xenial-v20191010
$ pulumi config set instance_disk_size 50I then modified the sample __main__.py code so that it creates a GCP instance and also bootstraps it by means of an init/startup script. I based my code on on another Pulumi example.
$ cat __main__.py
import pulumi
from pulumi_gcp import compute
with open(‘init_script.txt’, ‘r’) as init_script:
data = init_script.read()
script = data
config = pulumi.Config()
instance_name = config.require(‘instance_name’)
instance_type = config.require(‘instance_type’)
instance_image = config.require(‘instance_image’)
instance_disk_size = config.require(‘instance_disk_size’)addr = compute.address.Address(instance_name)network = compute.Network(instance_name)
firewall = compute.Firewall(
instance_name,
network=network.self_link,
allows=[
{
“protocol”: “tcp”,
“ports”: [“22”]
}
]
)instance = compute.Instance(
instance_name,
name=instance_name,
machine_type=instance_type,
boot_disk={
“initializeParams”: {
“image”: instance_image,
“size”: instance_disk_size
}
},
network_interfaces=[
{
“network”: network.id,
“accessConfigs”: [{“nat_ip”: addr.address}]
}
],
metadata_startup_script=script,
)# Export the DNS name of the bucket
pulumi.export(“instance_name”, instance.name)
pulumi.export(“instance_network”, instance.network_interfaces)
pulumi.export(“external_ip”, addr.address)Note that I pass the data from a file called init_script.txt to the compute.Instance constructor, assigning it to the variable metadata_startup_script.
Here is the init script, where I add my personal ssh public key on the remote instance, then I install docker and docker-compose:
$ cat init_script.txt
# add ssh pubkeymkdir -p /home/ggheo/.ssh
chmod 700 /home/ggheo/.ssh
echo “ssh-rsa contents of my ssh public key” >> /home/ggheo/.ssh/authorized_keys
chown -R ggheo:ggheo /home/ggheo/.ssh# install dockersudo apt update
sudo apt install -y make apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository “deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable”
sudo apt update
sudo apt-cache policy docker-ce
sudo apt install -y docker-ce# download docker-composesudo curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-composeTo provision the devstack, I ran :
$ pulumi up
Previewing update (dev):Type Name Plan+ pulumi:pulumi:Stack gcpinfra-dev create+ ├─ gcp:compute:Address dev create+ ├─ gcp:compute:Network dev create+ ├─ gcp:compute:Firewall dev create+ └─ gcp:compute:Instance dev createResources:+ 5 to createDo you want to perform this update? yesUpdating (dev):Type Name Status+ pulumi:pulumi:Stack gcpinfra-dev created+ ├─ gcp:compute:Network dev created+ ├─ gcp:compute:Address dev created+ ├─ gcp:compute:Instance dev created+ └─ gcp:compute:Firewall dev createdOutputs:external_ip : “35.223.75.5”instance_name : “dev”instance_network: [[0]: {accessConfigs : [
[0]: {natIp : “35.223.75.5”network_tier : “PREMIUM”}
]
name : “nic0”network : “https://www.googleapis.com/compute/v1/projects/myproject/global/networks/dev-6de02e1"networkIp : “10.128.0.2”subnetwork : “https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/subnetworks/dev-6de02e1"subnetworkProject: “myproject”}
]
Resources:+ 5 createdDuration: 58sPermalink: https://app.pulumi.com/griggheo/gcpinfra/dev/updates/6I was now able to list the new GCP instance and ssh into it with the gcloud CLI:
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
dev us-central1-a n1-highmem-2 10.128.0.2 35.223.75.5 RUNNING$ gcloud compute ssh dev
Warning: Permanently added ‘compute.1672722981827980594’ (ECDSA) to the list of known hosts.
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.15.0–1046-gcp x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
40 packages can be updated.
18 updates are security updates.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
ggheo@dev:~$At this point, I verified that docker, docker-compose and tutor were installed correctly. I was able to see the init script installation commands in /var/log/auth.log.
I was also able to ssh directly into the remote GCP instance, because my ssh key was added to authorized_files.
As an example of updating an existing Pulumi stack, let’s change the init script to add our user to the docker group. I added this line to the end of init_script.txt:
usermod -a -G docker ggheoWhen running pulumi upit will replace the instance because it detects that the startup script has changed:
$ pulumi up
Previewing update (dev):Type Name Plan Infopulumi:pulumi:Stack gcpinfra-dev+- └─ gcp:compute:Instance dev replace [diff: ~metadataStartupScript]Outputs:~ instance_name : “dev” => output<string>- instance_network: [- [0]: {- accessConfigs : [
- [0]: {- natIp : “35.223.75.5”
- network_tier : “PREMIUM”}
]
- name : “nic0”- network : “https://www.googleapis.com/compute/v1/projects/myproject/global/networks/dev-6de02e1"- networkIp : “10.128.0.2”
- subnetwork : “https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/subnetworks/dev-6de02e1"- subnetworkProject: “myproject”}
]
+ instance_network: output<string>Resources:+-1 to replace4 unchangedDo you want to perform this update? yesUpdating (dev):Type Name Status Infopulumi:pulumi:Stack gcpinfra-dev+- └─ gcp:compute:Instance dev replaced [diff: ~metadataStartupScript]Outputs:external_ip : “35.223.75.5”instance_name : “dev”~ instance_network: [~ [0]: {accessConfigs : [
[0]: {natIp : “35.223.75.5”network_tier : “PREMIUM”}
]
name : “nic0”network : “https://www.googleapis.com/compute/v1/projects/myproject/global/networks/dev-6de02e1"~ networkIp : “10.128.0.2” => “10.128.0.3”subnetwork : “https://www.googleapis.com/compute/v1/projects/myproject/regions/us-central1/subnetworks/dev-6de02e1"subnetworkProject: “myproject”}
]
Resources:+-1 replaced4 unchangedDuration: 2m12sMy final example will be to show how to customize the environment on the remote instance by means of values passed to the instance via metadata. This can be useful for example when you want to create files containing certain custom values during the bootstrapping process on the remote instance.
Here for example is how to customize the “message of the day” (aka MOTD) greeting on the remote instance based on a custom value passed as metadata.
I first set a configuration variable called motdgreetingin pulumi:
$ pulumi config set motdgreeting ‘Hello from dev instance’I read the value of this configuration variable in __main__py and pass it to the compute.Instance constructor as the value of the motdgreeting key in the metadata dictionary:
# example of metadata variablemotd_greeting = config.require(‘motdgreeting’)instance = compute.Instance(
instance_name,
name=instance_name,
machine_type=instance_type,
boot_disk={
“initializeParams”: {
“image”: instance_image,
“size”: instance_disk_size
}
},
network_interfaces=[
{
“network”: network.id,
“accessConfigs”: [{“nat_ip”: addr.address}]
}
],
metadata_startup_script=script,
metadata={
“motdgreeting”: motd_greeting,
},
)Finally, I retrieve the value of the motdgreeting metadata variable in the init script and use it to set the MOTD message:
MOTD_GREETING=$(curl http://metadata.google.internal/computeMetadata/v1/instance/attributes/motdgreeting -H “Metadata-Flavor: Google”)
echo “$MOTD_GREETING” > /etc/motdIf you’ve been following along, you now have an instance running in GCP that you can ssh into, which is already running docker, and which you know how to customize it to your needs. If you don’t plan on using this instance, don’t forget to delete it so you don’t get billed for it. To do that, you can run:
$ pulumi destroy



