A primer on GCP Compute Instance VMs for dockerized Apps [Tutorial Part 8]
Tags: gcp, tutorial, docker, devops
Published: 2023-04-24 12:14:13

Getting started with the Google Cloud Platform (GCP) to run Virtual Machines (VMs) and prepare them to run dockerized applications.

<p>This article appeared first on <a href='https://www.pascallandau.com/'>https://www.pascallandau.com/</a> at <a href='https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/'>A primer on GCP Compute Instance VMs for dockerized Apps [Tutorial Part 8]</a></p>
<hr />

In the eighth part of this tutorial series on developing PHP on Docker we will **take a look on  the Google Cloud Platform (GCP)** and **create a Compute Instance VM** to **run dockerized  applications**. This includes:

- creating VMs
- using a container registry
- using a secret manager


{% youtube RScVUaNHNxs %}



{% youtube uPx9AZPOMrA %}


**All code samples are publicly available** in my [Docker PHP Tutorial repository on Github](https://github.com/paslandau/docker-php-tutorial/).   You find the branch with the final result of this tutorial at [part-8-gcp-compute-instance-vm-docker](https://github.com/paslandau/docker-php-tutorial/tree/part-8-gcp-compute-instance-vm-docker).

**All published parts of the Docker PHP Tutorial** are collected under a dedicated page at [Docker PHP Tutorial](https://www.pascallandau.com/docker-php-tutorial/). The previous part was [Create a CI pipeline for dockerized PHP Apps](https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/). and the following one is [Deploy dockerized PHP Apps to production on GCP via docker compose](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc).

If you want to follow along, please subscribe to the [RSS feed](https://www.pascallandau.com/feed.xml) or [via email](https://www.pascallandau.com/blog/#newsletter) to get **automatic notifications** when the next part comes out :)

<!-- generated -->
<a id='table-of-contents'> </a>
<!-- /generated -->

## Table of contents

<!-- toc -->

- [Introduction](#introduction)
- [Set up a GCP project](#set-up-a-gcp-project)
- [Create a service account](#create-a-service-account)
    - [Create service account key file](#create-service-account-key-file)
    - [Configure IAM permissions](#configure-iam-permissions)
- [Set up the `gcloud` CLI tool](#set-up-the-gcloud-cli-tool)
- [Set up the Container Registry](#set-up-the-container-registry)
    - [Authenticate docker](#authenticate-docker)
    - [Pushing images to the registry](#pushing-images-to-the-registry)
    - [Images are stored in Google Cloud Storage buckets](#images-are-stored-in-google-cloud-storage-buckets)
    - [Pulling images from the registry](#pulling-images-from-the-registry)
- [Set up the Secret Manager](#set-up-the-secret-manager)
    - [Create a secret via the UI](#create-a-secret-via-the-ui)
    - [View a secret via the UI](#view-a-secret-via-the-ui)
    - [Retrieve a secret via the `gcloud` cli](#retrieve-a-secret-via-the-gcloud-cli)
    - [Add the secret `gpg` key and password](#add-the-secret-gpg-key-and-password)
- [Compute Instances: The GCP VMs ](#compute-instances-the-gcp-vms)
    - [Create a VM](#create-a-vm)
        - [General VM settings](#general-vm-settings)
        - [Firewall and networks tags](#firewall-and-networks-tags)
        - [The role of the service account](#the-role-of-the-service-account)
        - [Adding a public SSH key](#adding-a-public-ssh-key)
        - [Define Availability Policies](#define-availability-policies)
        - [The actual VM creation](#the-actual-vm-creation)
    - [Log into a VM](#log-into-a-vm)
        - [Login via SSH from the GCP UI](#login-via-ssh-from-the-gcp-ui)
        - [Login via SSH with your own key from your host machine](#login-via-ssh-with-your-own-key-from-your-host-machine)
        - [Login using the Identity-Aware Proxy (IAP) concept](#login-using-the-identity-aware-proxy-iap-concept)
            - [Additional notes on IAP](#additional-notes-on-iap)
        - [Get `root` permissions](#get-root-permissions)
    - [`ssh` and `scp` commands](#ssh-and-scp-commands)
        - [`gcloud ssh --command=""`](#gcloud-ssh-command)
        - [`gcloud scp`](#gcloud-scp)
- [Provision the VM](#provision-the-vm)
    - [Get the secret `gpg` key and password from the Secret Manager](#get-the-secret-gpg-key-and-password-from-the-secret-manager)
    - [Installing `docker` and `docker compose`](#installing-docker-and-docker-compose)
    - [Authenticate docker via `gcloud`](#authenticate-docker-via-gcloud)
    - [Pulling the `nginx` image](#pulling-the-nginx-image)
    - [Start the `nginx` container](#start-the-nginx-container)
- [Automate via `gcloud` commands](#automate-via-gcloud-commands)
    - [Preconditions: Project and `Owner` service account](#preconditions-project-and-owner-service-account)
    - [Configure `gcloud` to use the master service account](#configure-gcloud-to-use-the-master-service-account)
    - [Enable APIs](#enable-apis)
    - [Create and configure a "deployment" service account](#create-and-configure-a-deployment-service-account)
    - [Create secrets](#create-secrets)
    - [Create firewall rule for HTTP traffic](#create-firewall-rule-for-http-traffic)
    - [Create a Compute Instance VM](#create-a-compute-instance-vm)
    - [Provisioning](#provisioning)
    - [Deployment](#deployment)
- [Putting it all together ](#putting-it-all-together)
- [Cleanup](#cleanup)
- [Wrapping up](#wrapping-up)

<!-- /toc -->

<!-- generated -->
<a id='introduction'> </a>
<!-- /generated -->

## Introduction

In the next tutorial we will [deploy our dockerized PHP Apps to "production" via docker compose](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc) and will create this "production" environment on **GCP (Google Cloud Platform)**.  This tutorial serves as a primer on GCP to build up some fundamental knowledge, because we will  use the platform to provide all the **infrastructure required to run our dockerized PHP  application**.

In the process, we'll learn about [GCP projects](#set-up-a-gcp-project) as our own "space"  in GCP and [service accounts](#create-a-service-account) as a way to communicate  programmatically. We'll start by doing everything manually via the UI, but will also  [explain how to do it programmatically via the `gcloud` cli](#automate-via-gcloud-commands) and  end with a [fully automated script](#putting-it-all-together).

[![GCP Services](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-services.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-services.PNG)

The following video shows the overall flow

<video controls>
  <source src="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-run-docker-images.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

The API keys (see [service account key files](#create-service-account-key-file)) that I use are 
**not** in the repository, because I would be billed for any usage. I.e. **you must create you own  project and keys** to follow along. 

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Caution</strong>
  </div>
  <div class="panel-body bg-danger">
    Following the steps outlined in this tutorial <strong>will incur costs</strong>, because we will 
    create "real" infrastructure. It won't be much (couple of cents), and it will very likely be 
    covered by the free 300$ grant that you get when trying out GCP (or the general unlimited 
    <a href="https://cloud.google.com/free/docs/gcp-free-tier">GCP Free Tier</a>
    ).<br> 
    <br>
    But you should still know about that upfront and <strong>make sure to shut everything down / 
    delete everything</strong> in case you're trying it out yourself. The "safest" way to do so is
    <a href="#cleanup">Shutting down (deleting) the whole project</a>
  </div>
</div>

<!-- generated -->
<a id='set-up-a-gcp-project'> </a>
<!-- /generated -->

## Set up a GCP project

On GCP, resources are organized under so-called  [projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). We can  create a project via the [Create Project UI](https://console.cloud.google.com/projectcreate):

[![Create a new GCP project](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG)

The **project ID** must be a globally unique string, and I have chosen `pl-dofroscra-p` for this  tutorial (`pl` => Pascal Landau; `dofroscra` => Docker From Scratch; `p` => production).

<!-- generated -->
<a id='create-a-service-account'> </a>
<!-- /generated -->

## Create a service account

As a next step, we need a [service account](https://cloud.google.com/iam/docs/service-accounts) that we can **use to make API requests**, because we don't want to use our "personal GCP account".  Service accounts are created via the  [IAM & Admin > Service Accounts UI](https://console.cloud.google.com/iam-admin/serviceaccounts/create):

[![Create a new GCP service account](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG)

<!-- generated -->
<a id='create-service-account-key-file'> </a>
<!-- /generated -->

### Create service account key file

In order to **use the account programmatically**, we also need to  [create a key file](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating) by choosing the "Manage Keys" option of the corresponding service account. 

[![Create a new key file for a GCP service account UI](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG)

This will open up a UI at

```text
https://console.cloud.google.com/iam-admin/serviceaccounts/details/$serviceAccountId/keys
```

where `$serviceAccountId` is the numeric id of the service account, e.g. `109548647107864470967`. To create a key:

- click `"ADD KEY"` and select `"Create new key"` from the drop down menu
  - This will bring up a modal window to choose the key type. 
- select the recommended JSON type and click `"Create"`. 
  - GCP will then **generate a new key pair**, store the public key and offer the private key file as 
  download. 
- download the file and make sure to treat it like any other private key (ssh, gpg, ...) 
  i.e. **never share it publicly**!

[![Create a new key file for a GCP service account](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif)

We will **store this file in the root of the codebase** at `gcp-service-account-key.json` and add it  to the `.gitignore` file. 

Each service account has also a **unique email address** that consists of its (non-numeric) `id`  and the `project id`. You can also find it directly in the key file:

```text
$ grep "email" ./gcp-service-account-key.json
  "client_email": "docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com",
```

This email address is usually used to reference the service account, e.g. when assigning IAM  permissions.

<!-- generated -->
<a id='configure-iam-permissions'> </a>
<!-- /generated -->

### Configure IAM permissions

**IAM** stands for [Identity and Access Management (IAM)](https://cloud.google.com/iam/docs#docs)  and is used for **managing permissions on GCP**. The  [two core concepts are "permissions" and "roles"](https://cloud.google.com/iam/docs/understanding-roles):

- **permissions** are fine-grained for particular actions, e.g. `storage.buckets.create` to "Create 
  Cloud Storage buckets"
- **roles** combine a selection of permissions, e.g. the `Cloud Storage Admin` role has 
  permissions like
  - `storage.buckets.create`
  - `storage.buckets.get`
  - etc.
- roles are assigned to **users** (or service accounts)

You can find a full overview of all permissions in the  [Permissions Reference](https://cloud.google.com/iam/docs/permissions-reference) and all roles  under  [Understanding roles > Predefined roles](https://cloud.google.com/iam/docs/understanding-roles#predefined).

For this tutorial, we'll assign the following roles to the service account "user" `docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com`:

- `Storage Admin` 
  - required to [create the GCP bucket for the registry](#pushing-images-to-the-registry) and to 
    [pull the images on the VM](#pulling-images-from-the-registry) 
- `Secret Manager Admin`
  - required to [retrieve secrets from the Secret Manager](#get-the-secret-gpg-key-and-password-from-the-secret-manager)
- `Compute Admin`, `Service Account User` and `IAP-secured Tunnel User`
  - are necessary for [logging into a VM via IAP](#additional-notes-on-iap).

Roles can be assigned through the [Cloud Console IAM UI](https://console.cloud.google.com/iam-admin/iam) by editing the corresponding user.

[![Managing IAM permissions](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG)

**Caution:** It might take some time (usually a couple of seconds) until changes in IAM permissions take effect.

<!-- generated -->
<a id='set-up-the-gcloud-cli-tool'> </a>
<!-- /generated -->

## Set up the `gcloud` CLI tool

The [CLI tool for GCP is called `gcloud`](https://cloud.google.com/sdk/gcloud) and is [available for all operating systems](https://cloud.google.com/sdk/docs/install).

In this tutorial we are using version `380.0.0` **installed natively on Windows** via the [GoogleCloudSDKInstaller.exe](https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe) using the "Bundled Python" option.

[![Install `gcloud` on Windows](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG)

FYI: As described under [Uninstalling the Google Cloud CLI](https://cloud.google.com/sdk/docs/uninstall-cloud-sdk) you can find the installation and config directories via

```text
# installation directory
$ gcloud info --format='value(installation.sdk_root)'
C:\Users\Pascal\AppData\Local\Google\Cloud SDK\google-cloud-sdk

# config directory
$ gcloud info --format='value(config.paths.global_config_dir)'
C:\Users\Pascal\AppData\Roaming\gcloud
```

I will _not_ use my personal Google account to run `gcloud` commands, thus I'm _not_ using the  "usual" initialization process [by running `gcloud init`](https://cloud.google.com/sdk/docs/initializing). Instead, I will use the [service account that we created previously](#create-a-service-account) and activate it as  described under [gcloud auth activate-service-account](https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account) via

```bash
gcloud auth activate-service-account docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com --key-file=./gcp-service-account-key.json --project=pl-dofroscra-p
```

Output

```text
$ gcloud auth activate-service-account docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com --key-file=./gcp-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
```

FYI: Because we are using a `json` key file that includes the service account ID, we can also  omit the id in the command, i.e.

```text
$ gcloud auth activate-service-account --key-file=./gcp-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
```

<!-- generated -->
<a id='set-up-the-container-registry'> </a>
<!-- /generated -->

## Set up the Container Registry

[We will use `docker compose` to run our PHP application in the next tutorial part](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/) and need to **make our docker images available** in a  [container registry](https://www.redhat.com/en/topics/cloud-native-apps/what-is-a-container-registry). Luckily, [GCP offers a Container Registry product](https://cloud.google.com/container-registry)  that gives us a **ready-to-use private registry as part of a GCP project**. Before we can use it,  the corresponding  [Google Container Registry API must be enabled](https://console.cloud.google.com/marketplace/product/google/containerregistry.googleapis.com):

[![Enable the GCP Container Registry API](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG)

You find the **Container Registry** in the Cloud Console UI under  [Container Registry](https://console.cloud.google.com/gcr).

<!-- generated -->
<a id='authenticate-docker'> </a>
<!-- /generated -->

### Authenticate docker

Since the Container Registry is private, we **need to authenticate before we can push our  docker images**. The available authentication methods are described in the [GCP docu "Container Registry Authentication methods"](https://cloud.google.com/container-registry/docs/advanced-authentication). For pushing images from our local host system, we will use **the service account key file** that we  [created previously](#create-service-account-key-file) and run the command shown in the ["JSON key file" section](https://cloud.google.com/container-registry/docs/advanced-authentication#json-key) of the the docu.

```bash
key=./gcp-service-account-key.json
cat "$key" | docker login -u _json_key --password-stdin https://gcr.io
```

A successful authentication looks as follows:

```text
$ cat "$key" | docker login -u _json_key --password-stdin https://gcr.io
Login Succeeded

Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
```

So what exactly "happens" when we run this command? According to the  [`docker login`documentation](https://docs.docker.com/engine/reference/commandline/login/)

> When you log in, the command stores credentials in `$HOME/.docker/config.json` 
> on Linux or `%USERPROFILE%/.docker/config.json` on Windows
> [...]
> 
> The Docker Engine can keep user credentials in an external credentials store, 
> such as the native keychain of the operating system.
> [...]
>
> You need to specify the credentials store in 
> `$HOME/.docker/config.json` to tell the docker engine to use it.
> [...]
>
> By default, Docker looks for the native binary on each of the platforms, 
> i.e. “osxkeychain” on macOS, “wincred” on windows, and “pass” on Linux.

In other words: I **won't be able to see the content of the service account key file in "plain  text"** anywhere but docker will utilize the OS specific tools to store them securely. After I ran  the `docker login` command on Windows, I found the following content in `~/.docker/config.json`:

```text
$ cat ~/.docker/config.json
{
        "auths": {
                "gcr.io": {}
        },
        "credsStore": "desktop"
}
```

FYI: `"desktop"` seems to be a  [wrapper for the Wincred executable](https://forums.docker.com/t/docker-windows-desktop-credentials-location/107251).

<!-- generated -->
<a id='pushing-images-to-the-registry'> </a>
<!-- /generated -->

### Pushing images to the registry

For this tutorial, we will create a super simple `nginx` alpine image that provides a "Hello  world" `hello.html` file via

```bash
docker build -t my-nginx -f - . <<EOF
FROM nginx:1.21.5-alpine

RUN echo "Hello world" >> /usr/share/nginx/html/hello.html

EOF
```

The name of the image is `my-nginx`

```text
$ docker image ls | grep my-nginx
my-nginx         latest             42dd1608d126   50 seconds ago    23.5MB
```

In order to  [push an image to a registry](https://docs.docker.com/engine/reference/commandline/push/),
**the image name must be prefixed with the corresponding registry**. This was quite confusing to  me, because I would have expected to be able to run something like this:

```bash
$ docker push my-nginx --registry=gcr.io

unknown flag: --registry
See 'docker push --help'.
```

But nope, there is no such `--registry` option. Even worse: **Omitting it would cause a push to  `docker.io`**, the "default" registry:

```text
$ docker push my-nginx
Using default tag: latest
The push refers to repository [docker.io/my-nginx]
```

According to  [the GCP docs on Pushing and pulling images](https://cloud.google.com/container-registry/docs/pushing-and-pulling),  the following steps are necessary to push an image to a GCP registry:

> - **Tag** the image with its target path in Container Registry, including the gcr.io registry 
>   host and the project ID my-project
> - **Push** the image to the registry

In our case **the target path to our Container Registry** is

```text
gcr.io/pl-dofroscra-p
```

because `pl-dofroscra-p` is the [id of the GCP project we created previously](#set-up-a-gcp-project).

The **full image name** becomes

```text
gcr.io/pl-dofroscra-p/my-nginx
```

To push the `my-nginx` image, we must first **"add another name"** to it via  [`docker tag`](https://docs.docker.com/engine/reference/commandline/tag/) 

```text
$ docker tag my-nginx gcr.io/pl-dofroscra-p/my-nginx

$ docker image ls
REPOSITORY                       TAG                IMAGE ID       CREATED          SIZE
my-nginx                         latest             ba7a2c5faf0d   15 minutes ago   23.5MB
gcr.io/pl-dofroscra-p/my-nginx   latest             ba7a2c5faf0d   15 minutes ago   23.5MB
```

and **push that name afterwards**

```text
$ docker push gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
The push refers to repository [gcr.io/pl-dofroscra-p/my-nginx]
134174afa9ad: Preparing
cb7b4430c52d: Preparing
419df8b60032: Preparing
0e835d02c1b5: Preparing
5ee3266a70bd: Preparing
3f87f0a06073: Preparing
1c9c1e42aafa: Preparing
8d3ac3489996: Preparing
8d3ac3489996: Waiting
3f87f0a06073: Waiting
1c9c1e42aafa: Waiting
cb7b4430c52d: Pushed
134174afa9ad: Pushed
419df8b60032: Pushed
5ee3266a70bd: Pushed
0e835d02c1b5: Pushed
8d3ac3489996: Layer already exists
3f87f0a06073: Pushed
1c9c1e42aafa: Pushed
latest: digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f size: 1982
```

You can then find the image in the  [UI of the Container Registry](https://console.cloud.google.com/gcr):

[![Example of a pushed image in the Container Registry](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG)

Don't worry: We won't have to do the tagging every time before a push, because we will  [set up `make` to use the correct name automatically](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/#adding-gcp-values-to-make-variables-env) when building the images in the [next part](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/).

<!-- generated -->
<a id='images-are-stored-in-google-cloud-storage-buckets'> </a>
<!-- /generated -->

### Images are stored in Google Cloud Storage buckets

We [assigned the `Storage Admin` role to the service account previously](#configure-iam-permissions) that contains the `storage.buckets.create` permission. If we wouldn't have done that, the  following error would have occurred:

```text
denied: Token exchange failed for project 'pl-dofroscra-p'. Caller does not have permission 'storage.buckets.create'. To configure permissions, follow instructions at: https://cloud.google.com/container-registry/docs/access-control
```

The Container Registry tries to **store the docker images in a Google Cloud Storage bucket** that  is created on the fly when the **very first image is pushed**, see  [the GCP docs on "Adding a registry"](https://cloud.google.com/container-registry/docs/pushing-and-pulling#add-registry):

> The first image push to a hostname triggers creation of the registry in a project 
> and the corresponding Cloud Storage storage bucket. 
> This initial push requires project-wide permissions to create storage buckets.

You can find the bucket, that in my case is named `artifacts.pl-dofroscra-p.appspot.com` in the [Cloud Storage UI](https://console.cloud.google.com/storage):

[![GCP Container Registry image location on Cloud Storage](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG)

**CAUTION**: Make sure to **delete this bucket once you are done with the tutorial** - otherwise  [storage costs](https://cloud.google.com/storage/pricing) will incur.

<!-- generated -->
<a id='pulling-images-from-the-registry'> </a>
<!-- /generated -->

### Pulling images from the registry

According to the  [GCP docs to pull an image from the Container Registry](https://cloud.google.com/container-registry/docs/pushing-and-pulling#pulling_images_from_a_registry) we [need to be authenticated](#authenticate-docker) with a user that  has the permissions of the  [`Storage Object Viewer`](https://cloud.google.com/storage/docs/access-control/iam-roles#standard-roles)  role to access the "raw" images. FYI: The `Storage Admin` role that we assigned previously has all  the permissions of the `Storage Object Viewer` role.

Then use the **fully qualified image name** as before:

```bash
docker pull gcr.io/pl-dofroscra-p/my-nginx
```

Output if the image is cached

```text
$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Image is up to date for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
```

or if doesn't exist

```text
docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
59bf1c3509f3: Pull complete 
f3322597df46: Pull complete 
d09cf91cabdc: Pull complete 
3a97535ac2ef: Pull complete 
919ade35f869: Pull complete 
40e5d2fe5bcd: Pull complete 
c72acb0c83a5: Pull complete 
d6baa2bee4a5: Pull complete 
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
```

<!-- generated -->
<a id='set-up-the-secret-manager'> </a>
<!-- /generated -->

## Set up the Secret Manager

Even though  [we use `git secret` to manage our secrets](https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/), **we  still need the `gpg` secret key** for decryption. This key is "a secret in itself" and we will use  the [GCP Secret Manager](https://cloud.google.com/secret-manager/docs) to store it and [retrieve it later from a VM](#get-the-secret-gpg-key-and-password-from-the-secret-manager).

It can be managed from the [Security > Secret Manager UI](https://console.cloud.google.com/security/secret-manager)  once we have [enabled the Secret Manager API](https://console.cloud.google.com/marketplace/product/google/secretmanager.googleapis.com)

[![Enable the GCP Secret Manager API](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG)

<!-- generated -->
<a id='create-a-secret-via-the-ui'> </a>
<!-- /generated -->

### Create a secret via the UI

To create a secret:
 
- navigate to the [Secret Manager UI](https://console.cloud.google.com/security/secret-manager) 
  and click the `"+ CREATE SECRET"` button
- enter the **secret name** and **secret value** in the form (we can ignore the other advanced 
  settings like "Replication policy" and "Encryption" for now)
  - FYI: the secret name can only contain English letters (A-Z), numbers (0-9), dashes (-), and 
    underscores (\_)
- click the `"CREATE SECRET"` button

The following gif shows the creation of a secret named `my_secret_key` with the value  `my_secret_value`.

[![Create and view a secret](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif)

<!-- generated -->
<a id='view-a-secret-via-the-ui'> </a>
<!-- /generated -->

### View a secret via the UI

The [Security > Secret Manager UI](https://console.cloud.google.com/security/secret-manager)  lists all existing secrets. Clicking on a secret will lead you to the **Secret Detail UI** at the URL

```text
https://console.cloud.google.com/security/secret-manager/secret/$secretName/versions
```

e.g. for secret name `my_secret_key`:

```text
https://console.cloud.google.com/security/secret-manager/secret/my_secret_key/versions
```

The UI shows all [versions of the secret](https://cloud.google.com/secret-manager/docs/managing-secret-versions), though we currently only have one. To view the actual secret value, click on the three dots in  the "Actions" column and select "View secret value" (see gif in the  [previous section](#create-a-secret-via-the-ui)).

<!-- generated -->
<a id='retrieve-a-secret-via-the-gcloud-cli'> </a>
<!-- /generated -->

### Retrieve a secret via the `gcloud` cli

To retrieve a secret with a service account via the `gcloud` cli, it needs the permission  `secretmanager.versions.access`, that is part of the  [`Secret Manager Secret Accessor`](https://cloud.google.com/iam/docs/understanding-roles#secret-manager-roles)  role (as well as the `Secret Manager Admin` role). Let's first show all available secrets via

```bash
gcloud secrets list
```

```text
$ gcloud secrets list
NAME           CREATED              REPLICATION_POLICY  LOCATIONS
my_secret_key  2022-05-15T05:38:11  automatic           -
```

To "see" the value of `my_secret_key`, we must also define the corresponding version. All  versions can be listed via

```bash
gcloud secrets versions list my_secret_key
```

```text
$ gcloud secrets versions list my_secret_key
NAME  STATE    CREATED              DESTROYED
1     enabled  2022-05-15T05:38:13  -
```

The actual secret value for version `1` of `my_secret_key` is accessed via

```bash
gcloud secrets versions access 1 --secret=my_secret_key
```

```text
$ gcloud secrets versions access 1 --secret=my_secret_key
my_secret_value
```

You can also use  [the string `latest` as version to retrieve the latest enabled version](https://cloud.google.com/sdk/gcloud/reference/secrets/versions/access)

> Version resource - Numeric secret version to access or a configured alias 
> (including 'latest' to use the latest version).

```bash
gcloud secrets versions access latest --secret=my_secret_key
```

```text
$ gcloud secrets versions access latest --secret=my_secret_key
my_secret_value
```

<!-- generated -->
<a id='add-the-secret-gpg-key-and-password'> </a>
<!-- /generated -->

### Add the secret `gpg` key and password

We already know how to [create another `gpg` key pair](https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#create-gpg-key-pair) and  [add the corresponding email to `git secret`](https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/#adding-listing-and-removing-users) from when we have set up the CI pipelines (see section  [Add a password-protected secret gpg key](https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/#add-a-password-protected-secret-gpg-key)) . We will do the same once more for the production environment via

```bash
name="Production Deployment"
email="production@example.com"
passphrase=87654321
secret=secret-production-protected.gpg.example
public=.dev/gpg-keys/production-public.gpg
# export key pair
gpg --batch --gen-key <<EOF
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: $name
Name-Email: $email
Expire-Date: 0
Passphrase: $passphrase
EOF

# export the private key
gpg --output $secret --pinentry-mode=loopback --passphrase  "$passphrase" --armor --export-secret-key $email

# export the public key
gpg --armor --export $email > $public
```

You can find the secret key in the codebase at `secret-production-protected.gpg.example`  and the public key at `.dev/gpg-keys/production-public.gpg`.

I have also added the secret `gpg` key and password as secrets to the secret manager

[![GPG secret key and password stored as secrets](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG)

<!-- generated -->
<a id='compute-instances-the-gcp-vms'> </a>
<!-- /generated -->

## Compute Instances: The GCP VMs 

Compute Instances are the equivalent of [AWS EC2 instances](https://aws.amazon.com/de/ec2/).

To run our application, **we need a VM with a public IP address** so that it can be reached from the  internet. VMs on GCP are called [Compute Instances](https://cloud.google.com/compute) and can be  managed from the [Compute Instance UI](https://console.cloud.google.com/compute) - though we  must first  [activate the Compute Instance API](https://console.cloud.google.com/marketplace/product/google/compute.googleapis.com).

[![Enable the GCP Compute Instance API](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG)

<!-- generated -->
<a id='create-a-vm'> </a>
<!-- /generated -->

### Create a VM

We can simply create a new instance from the  [Create an instance UI](https://console.cloud.google.com/compute/instancesAdd).

<!-- generated -->
<a id='general-vm-settings'> </a>
<!-- /generated -->

#### General VM settings

We'll use the following settings:

- Name: `dofroscra-test` 
- Region `us-central1 (Iowa)` and Zone `us-central1-a`
- Machine family: `General Purpose > E2 > e2-micro (2 vCPU, 1 GB memory)` 
- Boot Disk: Debian GNU/Linux 11 (bullseye); 10 GB
- Identity and API access: Choose the "Docker PHP Tutorial deployment account" service account 
  [that we created previously](#create-a-service-account) 
- Firewall: Allow HTTP traffic

[![GCP instance settings](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG)

[![Additional GCP instance settings](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG)

<!-- generated -->
<a id='firewall-and-networks-tags'> </a>
<!-- /generated -->

#### Firewall and networks tags

Ticking the `Allow HTTP traffic` checkbox will cause two things when the instance is created:

- a new **firewall rule** named `default-allow-http` is created that allows incoming traffic from 
  port `80` and is only applied to instances with the 
  [network tag](https://cloud.google.com/vpc/docs/add-remove-network-tags) `http-server`. You 
  can see the new rule in the 
  [Firewall UI](https://console.cloud.google.com/networking/firewalls/list)
  [![Firewall rule: default-allow-http](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG)
- the **network tag** `http-server` is added to the instance, effectively enabling the 
  aforementioned firewall rule for the instance

<!-- generated -->
<a id='the-role-of-the-service-account'> </a>
<!-- /generated -->

#### The role of the service account

The **service account** that we have chosen in the previous step **will be attached to the  Compute Instance**. I.e. it will be available on the instance and **we will have access to the account when we log into the instance**. Conveniently,  [the`gcloud` cli is pre-installed on every Compute Instance](https://cloud.google.com/sdk/docs/install-sdk#installing_the_latest_version):

> If you're using an instance on Compute Engine, the gcloud CLI is installed by default.

[![The`gcloud` cli is pre-installed on every Compute Instance](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG)

The exact rules for **what the service account can do** are described in the  [Compute Engine docs for "Service accounts"](https://cloud.google.com/compute/docs/access/service-accounts). In short: The possible actions will be constrained by the  [IAM permissions of the service account](https://cloud.google.com/compute/docs/access/service-accounts#usingroles) and the [Access scopes of the Compute Instance](https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam)  which are set as outlined in the  ["Default scopes" section](https://cloud.google.com/compute/docs/access/service-accounts#default_scopes). Just take this as an aside, we won't have to modify anything here.

We will use the service account later,  [when we log into the Compute Instance and run `docker pull` from there](#pulling-the-nginx-image) to pull images from the registry.

<!-- generated -->
<a id='adding-a-public-ssh-key'> </a>
<!-- /generated -->

#### Adding a public SSH key

In addition, I **added my own public `ssh` key**

```text
ssh-rsa AAAAB3NzaC1yc2....6row== pascal.landau@MY_LAPTOP
```

**CAUTION**: The **username** for this key will be defined **at the end of the key**! E.g. in the  example above, the username would be `pascal.landau`. This is important for  [logging in later via SSH from your local machine](#login-via-ssh-with-your-own-key-from-your-host-machine).

[![Add SSH key to GCP instance settings](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG)

<!-- generated -->
<a id='define-availability-policies'> </a>
<!-- /generated -->

#### Define Availability Policies

We will make the instance [preemptible](https://cloud.google.com/compute/docs/instances/preemptible), by choosing the **VM provisioning model** `Spot`. This makes it **much cheaper** but GCP "might" **terminate the instance randomly** if the  capacity is needed somewhere else (and definitely after 24 hours). This is completely fine for our  test use case. The final costs are ~2$ per month for this instance.

[![Define Availability Policies](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG)

<!-- generated -->
<a id='the-actual-vm-creation'> </a>
<!-- /generated -->

#### The actual VM creation

Finally, click the `"Create"` button at the bottom of the page

[![Create the GCP instance](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG)

Once the instance is created, you can see it in the  [Compute Instances > VM instances UI](https://console.cloud.google.com/compute/instances)

[![Compute Instance overview](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG)

Next to the instance name `dofroscra-test` we can see the external IP address `35.192.212.130`  Opening it in a browser via `http://35.192.212.130/` won't show anything though, because we  didn't deploy the application yet.

**CAUTION**: Make sure to **shutdown and remove this instance by the end of the tutorial** - otherwise [compute costs](https://cloud.google.com/compute/all-pricing) will incur.

<!-- generated -->
<a id='log-into-a-vm'> </a>
<!-- /generated -->

### Log into a VM

There are multiple ways **to log into the VM / Compute Instance** outlined in  [Connecting to Linux VMs using advanced methods](https://cloud.google.com/compute/docs/instances/connecting-advanced).

I'm going to describe three of them:

- [Login via SSH from the GCP UI](#login-via-ssh-from-the-gcp-ui)
- [Login via SSH with your own key from your host machine](#login-via-ssh-with-your-own-key-from-your-host-machine)
- [Login using the Identity-Aware Proxy (IAP) concept](#login-using-the-identity-aware-proxy-iap-concept)

PS: It's worth keeping  [the Troubleshooting SSH guide](https://cloud.google.com/compute/docs/troubleshooting/troubleshooting-ssh) as a bookmark.

<!-- generated -->
<a id='login-via-ssh-from-the-gcp-ui'> </a>
<!-- /generated -->

#### Login via SSH from the GCP UI

Probably the easiest way to log in: Simply **click the `"SSH"` button** in the [Compute Instances > VM instances UI](https://console.cloud.google.com/compute/instances) **next  to the instance you want to log in**. This will **create a web shell** that uses an ephemeral SSH key according to the [GCP documentation: Connect to Linux VMs > Connect to VMs](https://cloud.google.com/compute/docs/instances/connecting-to-instance#connect_to_vms)

> When you connect to VMs using the Cloud Console, Compute Engine creates an ephemeral SSH key for you.

[![Connect via Cloud Console UI](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif)

<!-- generated -->
<a id='login-via-ssh-with-your-own-key-from-your-host-machine'> </a>
<!-- /generated -->

#### Login via SSH with your own key from your host machine

This method is probably closest to what you are used to from working with "other" VMs. In this  case, **the instance has to be publicly available** (i.e. reachable "from the internet") and 
**expose a port for SSH connections** (usually `22`). In addition, your public SSH key needs to be  deployed.

All of those requirements are true  [for the Compute Instance that we just created](#create-a-vm):

- the public ip address is `35.192.212.130`
- port `22` is open by default via the `default-allow-ssh` firewall rule, see
  [How to Configure Firewall Rules in Google Cloud Platform](https://geekflare.com/gcp-firewall-configuration/)
  and the [official documentation on Troubleshooting SSH](https://cloud.google.com/compute/docs/troubleshooting/troubleshooting-ssh)
  > By default, Compute Engine VMs allow SSH access on port 22.
- we [added our public SSH key](#adding-a-public-ssh-key) using `pascal.landau` as the username

So we can now simply login via 

```bash
ssh pascal.landau@35.192.212.130
```

or if you need to specify the location of the private key via the `-i` option

```bash
ssh -i ~/.ssh/id_rsa pascal.landau@35.192.212.130
```

```text
$ ssh pascal.landau@35.192.212.130
Linux dofroscra-test 4.19.0-20-cloud-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Apr 10 16:21:00 2022 from 54.74.228.207
pascal.landau@dofroscra-test:~$
```

<!-- generated -->
<a id='login-using-the-identity-aware-proxy-iap-concept'> </a>
<!-- /generated -->

#### Login using the Identity-Aware Proxy (IAP) concept

**Note: This is the preferred way of logging into a GCP VM**

To **login via IAP** we need the [`gcloud` CLI](#set-up-the-gcloud-cli-tool) that will **use API  requests (via HTTPS) under the hood to authenticate the Google user** (or in our case: the  service account) and then proxy the requests to the VM via [GCP's Identity-Aware Proxy (IAP)](https://cloud.google.com/iap/docs/concepts-overview) as described in [Connecting through Identity-Aware Proxy (IAP) for TCP](https://cloud.google.com/compute/docs/instances/connecting-advanced#cloud_iap) and in more detail under [Using IAP for TCP forwarding > Tunneling SSH connections](https://cloud.google.com/iap/docs/using-tcp-forwarding#tunneling_ssh_connections).

The corresponding command is [`gcloud compute ssh`](https://cloud.google.com/sdk/gcloud/reference/compute/ssh), e.g.:

```bash
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p
``` Note the [--tunnel-through-iap](https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--tunnel-through-iap)  flag: Without it, `gcloud` would instead attempt to use a "normal" `ssh` connection if the VM is  publicly reachable  [as per docu](https://cloud.google.com/iap/docs/using-tcp-forwarding#tunneling_ssh_connections):

> If the instance doesn't have an external IP address, the connection automatically uses 
> IAP TCP tunneling. If the instance does have an external IP address, the connection uses the 
> external IP address instead of IAP TCP tunneling.

Output

```text
$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p
WARNING: The private SSH key file for gcloud does not exist.
WARNING: The public SSH key file for gcloud does not exist.
WARNING: The PuTTY PPK SSH key file for gcloud does not exist.
WARNING: You do not have an SSH key for gcloud.
WARNING: SSH keygen will be executed to generate a key.
Updating project ssh metadata...
..............................................Updated [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p].
.done.
Waiting for SSH key to propagate.
```

[![Connect via IAP](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif)

Under the hood, this command will **automatically create an SSH key pair** on your local machine under  `~/.ssh/` named `google_compute_engine` (unless they already exist) and upload the public key  to the instance.

```text
$ ls -l ~/.ssh/ | grep google_compute_engine
-rw-r--r-- 1 Pascal 197121 1675 Apr 11 09:25 google_compute_engine
-rw-r--r-- 1 Pascal 197121 1456 Apr 11 09:25 google_compute_engine.ppk
-rw-r--r-- 1 Pascal 197121  420 Apr 11 09:25 google_compute_engine.pub
```

It also opens a [`Putty` session](https://www.putty.org/) and logs you into in the instance:

```text
Using username "Pascal".
Authenticating with public key "LAPTOP-0DNL2Q02\Pascal@LAPTOP-0DNL2Q02"
Linux dofroscra-test 4.19.0-20-cloud-amd64 #1 SMP Debian 4.19.235-1 (2022-03-17) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Apr 11 07:22:32 2022 from 54.74.228.207
Pascal@dofroscra-test:~$
```

**Caution**: The `Putty` session will be closed automatically when you abort the original  `gcloud compute ssh` command!

<!-- generated -->
<a id='additional-notes-on-iap'> </a>
<!-- /generated -->

##### Additional notes on IAP

In order to **enable SSH connections via IAP**, our service account needs the following  [IAM roles](#configure-iam-permissions):

- [`roles/compute.instanceAdmin.v1`](https://cloud.google.com/compute/docs/access/iam#compute.instanceAdmin.v1), role: `Compute Admin`
- [`roles/iam.serviceAccountUser`](https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role), role: `Service Account User`
- [`roles/iap.tunnelResourceAccessor`](https://cloud.google.com/iam/docs/understanding-roles#cloud-iap-roles), role: `IAP-secured Tunnel User`
  Otherwise, you might run into a couple of errors like:

```text
ERROR: (gcloud.compute.ssh) Could not fetch resource:
 - Required 'compute.instances.get' permission for 'projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test'
``` (`roles/compute.instanceAdmin.v1` missing)

```text
ERROR: (gcloud.compute.ssh) Could not add SSH key to instance metadata:
 - The user does not have access to service account 'docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com'.  User: 'docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com'.  Ask a project owner to grant you the iam.serviceAccountUser role on the service account
``` ([`roles/iam.serviceAccountUser` missing](https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role))

```text
Remote side unexpectedly closed connection
``` (`roles/iap.tunnelResourceAccessor` missing)

Note: This error can also occur if you **try to use IAP directly after starting a VM**. In this  case wait a couple of seconds and try again.

The general "flow" looks like this

[![Connection flow when using Identity-Aware Proxy (IAP)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG)

Using IAP might look overly complicated at first, but it offers **a number of benefits**:

- we can leverage **Google's authentication system** and the powerful
  [IAM permission management](#configure-iam-permissions) to manage permissions - no more need 
  to deploy custom SSH keys to the VMs
- we **don't need a public IP address** any longer, see 
  [Overview of TCP forwarding > How IAP's TCP forwarding works](https://cloud.google.com/iap/docs/tcp-forwarding-overview#how-tcp-forwarding-works)
  > A special case, establishing an SSH connection using `gcloud compute ssh` wraps the SSH 
  > connection inside HTTPS and forwards it to the remote instance without the need of a 
  > listening port on local host.
  > 
  > [...]
  > 
  > TCP forwarding with IAP doesn't require a public, routable IP address assigned to your 
  > resource. Instead, it uses internal IPs.
  
  This is nice, because in our final PHP application only the `nginx` container should be 
  accessible publicly - `php-fpm` and the `php-workers` shouldn't. Usually we would use a   
  so-called [Bastion Host or Jump Box](https://en.wikipedia.org/wiki/Bastion_host) to deal with 
  this problem, but thanks to IAP we don't have to

**Additional resources**

- [Accessing Secure Servers Using IAP](https://gochronicles.com/secure-tunnel-with-iap-gcp/)
- [Connecting Securely to Google Compute Engine VMs without a Public IP or VPN](https://medium.com/google-cloud/connecting-securely-to-google-compute-engine-vms-without-a-public-ip-or-vpn-720e53d1978e)

<!-- generated -->
<a id='get-root-permissions'> </a>
<!-- /generated -->

#### Get `root` permissions

The group  [`google-sudoers` exists on each GCP Compute Instance](https://superuser.com/a/1400281/434918).  It is configured for **passwordless sudo** via `/etc/sudoers.d/google_sudoers` 

```text
$ sudo cat /etc/sudoers.d/google_sudoers 
%google-sudoers ALL=(ALL:ALL) NOPASSWD:ALL
```

I.e. each user added to this group can use `sudo` without password or simply run `sudo -i` to  become `root`. **Your user should be added automatically to that group**. This can be verified with  the `id` command (run on the VM)

```text
Pascal@dofroscra-test:~$ id
uid=1002(Pascal) gid=1003(Pascal) groups=1003(Pascal),4(adm),30(dip),44(video),46(plugdev),1000(google-sudoers)

# => 1000(google-sudoers)
```

<!-- generated -->
<a id='ssh-and-scp-commands'> </a>
<!-- /generated -->

### `ssh` and `scp` commands

It is quite common **to copy files** from / to a VM and to **run commands** on it. This is  usually done via `ssh` and `scp`. The `gcloud` cli offers equivalent commands that can also make  use of [the Identity-Aware Proxy (IAP) concept](#login-using-the-identity-aware-proxy-iap-concept):

<!-- generated -->
<a id='gcloud-ssh-command'> </a>
<!-- /generated -->

#### `gcloud ssh --command=""`

Documentation: [`gcloud ssh --command`](https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--command)

Example: 

```bash
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="whoami"
```

Output:

```bash
$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="whoami"
Pascal
```

<!-- generated -->
<a id='gcloud-scp'> </a>
<!-- /generated -->

#### `gcloud scp`

Documentation: [`gcloud scp`](https://cloud.google.com/sdk/gcloud/reference/compute/scp)

Example:

```bash
gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:tmp/

# ---
echo "1" >> ./local-file1.txt
echo "2" >> ./local-file2.txt
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="rm -rf ~/test; mkdir -p ~/test"
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="ls -l ~/test"
gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:test/
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="ls -l ~/test"
```

Output:

```bash
$ echo "1" >> ./local-file1.txt
$ echo "2" >> ./local-file2.txt
$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="ls -l ~/test"
total 0

$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt ./local-file2.txt dofroscra-test:test/
local-file1.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%
local-file2.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%

$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="ls -l ~/test"
total 8
-rw-r--r-- 1 Pascal Pascal 4 Jun  2 13:06 local-file1.txt
-rw-r--r-- 1 Pascal Pascal 4 Jun  2 13:06 local-file2.txt
```

**Note**: According to the examples in the documentation, it should also be possible to use the  `~` to define that the destination on the remote VM is relative to the home directory. However,  this did not work for me, as I kept getting the error

```text
$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt dofroscra-test:~/test
pscp: remote filespec ~/test: not a directory
```

even though the diretory existed. But removing the `~/` part worked

```text
$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./local-file1.txt  dofroscra-test:test/
local-file1.txt           | 0 kB |   0.0 kB/s | ETA: 00:00:00 | 100%
```

<!-- generated -->
<a id='provision-the-vm'> </a>
<!-- /generated -->

## Provision the VM

<!-- generated -->
<a id='get-the-secret-gpg-key-and-password-from-the-secret-manager'> </a>
<!-- /generated -->

### Get the secret `gpg` key and password from the Secret Manager

Before we shift our attention to docker, let's quickly deal with the secrets. We won't need them  for this part of the tutorial. but will need the secret `gpg` key and its password to  [decrypt the secrets in the `php` containers in the next part](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/#the-secrets-directory). To recap: We have [added the them previously](#add-the-secret-gpg-key-and-password) and can now retrieve them as explained under section [Retrieve a secret via the `gcloud` cli](#retrieve-a-secret-via-the-gcloud-cli) via

```bash
gcloud secrets versions access 1 --secret=GPG_KEY > secret.gpg

GPG_PASSWORD=$(gcloud secrets versions access 1 --secret=GPG_PASSWORD)
```

```text
$ gcloud secrets versions access 1 --secret=GPG_KEY > secret.gpg
$ head secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb

$ GPG_PASSWORD=$(gcloud secrets versions access 1 --secret=GPG_PASSWORD)
$ echo $GPG_PASSWORD
87654321
```

<!-- generated -->
<a id='installing-docker-and-docker-compose'> </a>
<!-- /generated -->

### Installing `docker` and `docker compose`

Since we [created the VM with a Debian OS](#general-vm-settings), we'll follow the  [official Debian installation instructions for Docker Engine](https://docs.docker.com/engine/install/debian/)  and run the following commands while we are logged into the VM:

```bash
# install required tools
sudo apt-get update -yq && apt-get install -yq \
     ca-certificates \
     curl \
     gnupg \
     lsb-release
   
# add Docker’s official GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# set up the stable repository
echo \
 "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
 $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# install Docker Engine
sudo apt-get update -yq && sudo apt-get install -yq \
     docker-ce \
     docker-ce-cli \
     containerd.io \
     docker-compose-plugin
```

I have also added these instructions to the script `.infrastructure/scripts/provision.sh`.

Afterwards we check via 

```bash
docker --version
docker compose version
```

if `docker` and `docker compose` are available

```text
$ docker --version
Docker version 20.10.15, build fd82621

$ docker compose version
Docker Compose version v2.5.0
```

<!-- generated -->
<a id='authenticate-docker-via-gcloud'> </a>
<!-- /generated -->

### Authenticate docker via `gcloud`

I recommend [running the commands as `root`](#get-root-permissions) via

```bash
sudo -i
```

```text
pascal_landau@dofroscra-test:~$ sudo -i
root@dofroscra-test:~# 
```

Otherwise, we might run into docker permission errors like

```text
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/create?fromImage=...": dial unix /var/run/docker.sock: connect: permission denied
```

FYI: As an alternative we could also add the non-root user to the `docker` group via

```bash
user=$(whoami)
sudo usermod -a -G docker $user
```

```text
$ user=$(whoami)
$ sudo usermod -a -G docker $user
$ id
uid=1001(pascal_landau) gid=1002(pascal_landau) groups=1002(pascal_landau),4(adm),30(dip),44(video),46(plugdev),997(docker),1000(google-sudoers)
```

Locally, we could  [use the `JSON` key file of the service account for authentication](#authenticate-docker), but we  don't have access to that key file on the VM. Instead, we will authenticate `docker` via the  [pre-installed `gcloud` cli and the attached service account](#the-role-of-the-service-account) as described in the  [GCP docs for the Container Registry Authentication methods under section "gcloud credential helper"](https://cloud.google.com/container-registry/docs/advanced-authentication#gcloud-helper) via

```bash
gcloud auth configure-docker --quiet
```

```text
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
Docker configuration file updated.
```

This creates the file `/root/.docker/config.json` that we already encountered when we [authenticated docker locally to push images](#authenticate-docker). In this case it has the following content

```text
$ cat /root/.docker/config.json
{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud"
  }
}
```

The [`creadHelpers` are described in the `docker login` docs](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers)

> Credential helpers are similar to the credential store above, but act as the **designated 
> programs to handle credentials** for specific registries. The **default credential store**
> (credsStore or the config file itself) **will not be used** for operations concerning 
> credentials of the specified registries.

In other words: 

- we are using `gcr.io` as registry
- this registry is defined to use the credential helper `gcloud`
- i.e. it will use the pre-initialized `gcloud` cli for authentication

<!-- generated -->
<a id='pulling-the-nginx-image'> </a>
<!-- /generated -->

### Pulling the `nginx` image

Once we are authenticated, we can simply run `docker pull` with the full image name to retrieve the  `nginx` image that we [pushed previously](#pushing-images-to-the-registry)

```bash
docker pull gcr.io/pl-dofroscra-p/my-nginx
```

```text
$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
59bf1c3509f3: Pull complete 
f3322597df46: Pull complete 
d09cf91cabdc: Pull complete 
3a97535ac2ef: Pull complete 
919ade35f869: Pull complete 
40e5d2fe5bcd: Pull complete 
c72acb0c83a5: Pull complete 
d6baa2bee4a5: Pull complete 
Digest: sha256:0740591fb686227d8cdf4e42b784f634cbaf9f5caa6ee478e3bcc24aeef75d7f
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
```

If we wouldn't be authenticated, we would run into the following error

```text
$ docker pull gcr.io/pl-dofroscra-p/my-nginx
Using default tag: latest
Error response from daemon: unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication
```

<!-- generated -->
<a id='start-the-nginx-container'> </a>
<!-- /generated -->

### Start the `nginx` container

For now, we will simply run the `nginx` container with `docker run` via

```bash
docker run --name nginx -p 80:80 --rm -d gcr.io/pl-dofroscra-p/my-nginx:latest
```

We

- **give it the name `nginx`** so we can easily reference it later via `--name nginx`
- **map port `80` from the host to port `80` of the container** so that HTTP requests to the 
  VM are handled via the container via `-p 80:80`, 
  see [Docker docs on "Publish or expose port (-p, --expose)"](https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose)
- make it **run in the background** via `-d` (`--detach`) and **remove it after shutdown**
  automatically via `-rm` (`--remove`)

Output

```text
root@dofroscra-test:~# docker run -p 80:80 -d --name nginx gcr.io/pl-dofroscra-p/my-nginx:latest
dd49bedad97c06f698d06a140c5091c04ad81b2f75632e222927e7f71cf28c18

root@dofroscra-test:~# docker logs nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/05/08 12:01:43 [notice] 1#1: using the "epoll" event method
2022/05/08 12:01:43 [notice] 1#1: nginx/1.21.5
2022/05/08 12:01:43 [notice] 1#1: built by gcc 10.3.1 20211027 (Alpine 10.3.1_git20211027) 
2022/05/08 12:01:43 [notice] 1#1: OS: Linux 4.19.0-20-cloud-amd64
2022/05/08 12:01:43 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2022/05/08 12:01:43 [notice] 1#1: start worker processes
2022/05/08 12:01:43 [notice] 1#1: start worker process 30
2022/05/08 12:01:43 [notice] 1#1: start worker process 31
```

We're **all set to serve HTTP requests**: The requests will be passed to the `nginx` container  due to the mapped port `80`, i.e. we can simply use the public IP address `35.192.212.130` in a  `curl` request and check  [the "Hello world" file we've added to the image](#pushing-images-to-the-registry):

```text
$ curl -s http://35.192.212.130/hello.html
Hello world
```

<!-- generated -->
<a id='automate-via-gcloud-commands'> </a>
<!-- /generated -->

## Automate via `gcloud` commands

So far we **have mostly used the UI to configure everything**. This is great for understanding  how  things work, but it's not so great for maintainability:

- UIs change
- "Clicking" stuff takes quite some time
- we can't automate anything

Fortunately, we can also **use [the `gcloud` cli](#set-up-the-gcloud-cli-tool) for most of the  tasks**.

<!-- generated -->
<a id='preconditions-project-and-owner-service-account'> </a>
<!-- /generated -->

### Preconditions: Project and `Owner` service account

I'll assume that 

- [`gcloud` is installed](#set-up-the-gcloud-cli-tool)
- a [GCP project](#set-up-a-gcp-project) exists

In addition, I'll manually [create a new "master" service account](#create-a-service-account)  and assign it [the role `Owner`](https://cloud.google.com/iam/docs/understanding-roles#basic-definitions).  We'll use that service account in the `gcloud` cli to enable all the APIs and manage the  resources. 

FYI: We could also do this with our personal GCP user, but I plan to use `terraform` in a later  tutorial which will require a service account anyway.

We will use `docker-php-tutorial-master` as service account ID and  [store the key file](#create-service-account-key-file) of the account at the root of the  codebase under `gcp-master-service-account-key.json` (which is also added to the `.gitignore` file).

<!-- generated -->
<a id='configure-gcloud-to-use-the-master-service-account'> </a>
<!-- /generated -->

### Configure `gcloud` to use the master service account

Run 

```bash
gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=pl-dofroscra-p
```

```text
$ gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=pl-dofroscra-p
Activated service account credentials for: [docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com]
```

<!-- generated -->
<a id='enable-apis'> </a>
<!-- /generated -->

### Enable APIs

APIs can be enabled via [`gcloud services enable $serviceName`](https://cloud.google.com/sdk/gcloud/reference/services/enable), (see also the [Docu on "Enabling an API in your Google Cloud project"](https://cloud.google.com/endpoints/docs/openapi/enable-api)).

The `$serviceName` is shown on the API overview page of a service, see the following example for  the [Container Registry](https://console.cloud.google.com/marketplace/product/google/containerregistry.googleapis.com)

[![Find the service name of an API](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG)](https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG)

We need the APIs for

- Container Registry: `containerregistry.googleapis.com`
- Secret Manager: `secretmanager.googleapis.com`
- Compute Engine: `compute.googleapis.com`
- IAM: `iam.googleapis.com`
- Cloud Storage: `storage.googleapis.com`
- Cloud Resource Manager: `cloudresourcemanager.googleapis.com`

```bash
gcloud services enable containerregistry.googleapis.com secretmanager.googleapis.com compute.googleapis.com iam.googleapis.com storage.googleapis.com cloudresourcemanager.googleapis.com
```

```text
$ gcloud services enable containerregistry.googleapis.com secretmanager.googleapis.com compute.googleapis.com iam.googleapis.com storage.googleapis.com cloudresourcemanager.googleapis.com
Operation "operations/acat.p2-386551299607-87333b29-c7eb-40b8-b951-8c86185bbf49" finished successfully.
```

<!-- generated -->
<a id='create-and-configure-a-deployment-service-account'> </a>
<!-- /generated -->

### Create and configure a "deployment" service account

We won't use the master `Owner` service account for the deployment but create a custom one with  only the necessary permissions.

Service accounts are created via [`gcloud iam service-accounts create $serviceAccountId`](https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/create), (see also the [Docu on "Creating and managing service accounts"](https://cloud.google.com/iam/docs/creating-managing-service-accounts)).

The `$serviceAccountId` is the **id of the service account**, e.g. [`docker-php-tutorial-deployment` in our previous example](#create-a-service-account). In  addition, we can define a `--description` and a `--display-name`.

```bash
gcloud iam service-accounts create docker-php-tutorial-deployment \
  --description="Used for the deployment of the Docker PHP Tutorial application" \
  --display-name="Docker PHP Tutorial Deployment Account"
```

```text
$ gcloud iam service-accounts create docker-php-tutorial-deployment \
>   --description="Used for the deployment of the Docker PHP Tutorial application" \
>   --display-name="Docker PHP Tutorial Deployment Account"
Created service account [docker-php-tutorial-deployment].
```

Then, we need a **key file** for authentication. It can be created via [`gcloud iam service-accounts keys create $localPathToKeyFile --iam-account=$serviceAccountEmail`](https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/keys/create) (see also the [Docu on "Create and manage service account keys"](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)).

The `$localPathToKeyFile` is used to **store the key file locally**, e.g. [`gcp-service-account-key.json` in our previous example](#create-service-account-key-file) and  `$serviceAccountEmail` is the email address of the service account. It has the form

```text
$serviceAccountId@$projectId.iam.gserviceaccount.com

e.g.

docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com

```

Example:

```bash
gcloud iam service-accounts keys create ./gcp-service-account-key.json \
  --iam-account=docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
```

```text
$ gcloud iam service-accounts keys create ./gcp-service-account-key.json \
>   --iam-account=docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
created key [810df0c6df21de44dc5e431d2b569d74555ba3f9] of type [json] as [./gcp-service-account-key.json] for [docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com]
```

Finally, we must [assign the required IAM roles](#configure-iam-permissions) via [`gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=$roleId`](https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/add-iam-policy-binding), (see also the [Docu on "Manage access to projects, folders, and organizations"](https://cloud.google.com/iam/docs/granting-changing-revoking-access#grant-single-role)).

The `$projectName` is the **name of the GCP project**, `$serviceAccountEmail` the email address  from before and the `$roleId` the **name of the role** as listed under [Understanding roles](https://cloud.google.com/iam/docs/understanding-roles). In our case that's:

- Storage Admin: `roles/storage.admin`
- Secret Manager Admin: `roles/secretmanager.admin`
- Compute Admin: `roles/compute.admin`
- Service Account User: `roles/iam.serviceAccountUser`
- IAP-secured Tunnel User: `roles/iap.tunnelResourceAccessor`

```text
projectName="pl-dofroscra-p"
serviceAccountEmail="docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/storage.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/secretmanager.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/compute.admin
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/iam.serviceAccountUser
gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/iap.tunnelResourceAccessor
```

```text
$ projectName="pl-dofroscra-p"
$ serviceAccountEmail="docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com"
$ gcloud projects add-iam-policy-binding $projectName --member=serviceAccount:$serviceAccountEmail --role=roles/storage.admin
Updated IAM policy for project [pl-dofroscra-p].
bindings:
- members:
  - serviceAccount:service-386551299607@compute-system.iam.gserviceaccount.com
  role: roles/compute.serviceAgent
- members:
  - serviceAccount:service-386551299607@containerregistry.iam.gserviceaccount.com
  role: roles/containerregistry.ServiceAgent
- members:
  - serviceAccount:386551299607-compute@developer.gserviceaccount.com
  - serviceAccount:386551299607@cloudservices.gserviceaccount.com
  role: roles/editor
- members:
  - serviceAccount:docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com
  - user:pascal.landau@gmail.com
  role: roles/owner
- members:
  - serviceAccount:service-386551299607@gcp-sa-pubsub.iam.gserviceaccount.com
  role: roles/pubsub.serviceAgent
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgKxHg7gA=
version: 1

# ...

Updated IAM policy for project [pl-dofroscra-p].
bindings:
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/compute.admin
- members:
  - serviceAccount:service-386551299607@compute-system.iam.gserviceaccount.com
  role: roles/compute.serviceAgent
- members:
  - serviceAccount:service-386551299607@containerregistry.iam.gserviceaccount.com
  role: roles/containerregistry.ServiceAgent
- members:
  - serviceAccount:386551299607-compute@developer.gserviceaccount.com
  - serviceAccount:386551299607@cloudservices.gserviceaccount.com
  role: roles/editor
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/iam.serviceAccountUser
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/iap.tunnelResourceAccessor
- members:
  - serviceAccount:docker-php-tutorial-master@pl-dofroscra-p.iam.gserviceaccount.com
  - user:pascal.landau@gmail.com
  role: roles/owner
- members:
  - serviceAccount:service-386551299607@gcp-sa-pubsub.iam.gserviceaccount.com
  role: roles/pubsub.serviceAgent
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/secretmanager.admin
- members:
  - serviceAccount:docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgKxm0Vtk=
version: 1
```

<!-- generated -->
<a id='create-secrets'> </a>
<!-- /generated -->

### Create secrets

Secrets are created via [`gcloud secrets create $secretId"`](https://cloud.google.com/sdk/gcloud/reference/secrets/create), (see also the [Docu on "Creating and accessing secrets"](https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets)).

The `$secretId` is the **name of the secret**, e.g.  [`my_secret_key` in our previous example](#create-a-secret-via-the-ui), and we must also add a  new version with the **value of the secret** via [`gcloud secrets versions add $secretId --data-file="/path/to/file.txt"`](https://cloud.google.com/sdk/gcloud/reference/secrets/versions/add).

```bash
gcloud secrets create my_secret_key

echo -n "my_secret_value" | gcloud secrets versions add my_secret_key --data-file=-
```

```text
$ gcloud secrets create my_secret_key
Created secret [my_secret_key].

$ echo -n "my_secret_value" | gcloud secrets versions add my_secret_key --data-file=-
Created version [1] of the secret [my_secret_key].
```

We also need to  [do the same for the `gpg` secret key and its password](#add-the-secret-gpg-key-and-password):

```bash
gcloud secrets create GPG_KEY
echo gcloud secrets versions add GPG_KEY --data-file=secret-production-protected.gpg.example

gcloud secrets create GPG_PASSWORD
echo -n "87654321" | gcloud secrets versions add GPG_PASSWORD --data-file=-
```

```text
$ gcloud secrets create GPG_KEY
Created secret [GPG_KEY].
$ gcloud secrets versions add GPG_KEY --data-file=secret-production-protected.gpg.example
Created version [1] of the secret [GPG_KEY].

$ gcloud secrets create GPG_PASSWORD
Created secret [GPG_PASSWORD].
$ echo -n "87654321" | gcloud secrets versions add GPG_PASSWORD --data-file=-
Created version [1] of the secret [GPG_PASSWORD].
```

<!-- generated -->
<a id='create-firewall-rule-for-http-traffic'> </a>
<!-- /generated -->

### Create firewall rule for HTTP traffic

Firewall rules can be created via [`gcloud compute firewall-rules create $ruleName`](https://cloud.google.com/sdk/gcloud/reference/compute/firewall-rules/create) (see also the [Docu on "Using firewall rules"](https://cloud.google.com/vpc/docs/using-firewalls#creating_firewall_rules)).

We'll stick to  [the same conventions that have been used when creating the VM via the UI](#firewall-and-networks-tags) by using `default-allow-http` as the `$ruleName` and `http-server` as the network tag (via the  `--target-tags` option)

```bash
 gcloud compute firewall-rules create default-allow-http --allow tcp:80 --target-tags=http-server
```

```text
$ gcloud compute firewall-rules create default-allow-http --allow tcp:80 --target-tags=http-server
Creating firewall...
..Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/global/firewalls/default-allow-http].
done.
NAME                NETWORK  DIRECTION  PRIORITY  ALLOW   DENY  DISABLED
default-allow-http  default  INGRESS    1000      tcp:80        False

```

<!-- generated -->
<a id='create-a-compute-instance-vm'> </a>
<!-- /generated -->

### Create a Compute Instance VM

Compute Instances can be created via  [`gcloud compute instances create $vmName`](https://cloud.google.com/sdk/gcloud/reference/compute/instances/create) (see also the [Docu on "Creating and starting a VM instance"](https://cloud.google.com/compute/docs/instances/create-start-instance#publicimage)).

The `$vmName` defines the **name of the VM**, e.g. [`dofroscra-test` in the previous example](#create-a-vm). In addition, there are a lot of  options to customize the VM, e.g.

- `--image-family` and `--image-project`
  - define the **operating system**, e.g. `--image-family="debian-11"` and 
    `--image-project=debian-cloud`
  - See [Docu on "Operating system details"](https://cloud.google.com/compute/docs/images/os-details)
    for a list of available values
- `--machine-type`
  - defines **the machine type** (i.e. the "specs" like CPUs and memory), e.g.
    `--machine-type=e2-micro`
  - See [Docu on "About machine families"](https://cloud.google.com/compute/docs/machine-types) 
    that contains links to the machine type categories with the concrete values, e.g. the
    [General-purpose machine family](https://cloud.google.com/compute/docs/general-purpose-machines)
- etc.

The [docu](https://cloud.google.com/sdk/gcloud/reference/compute/instances/create) is doing  a great job at describing all available configuration options. Luckily, we can make our lives a  little easier, **configure the VM via UI** and then click the `"EQUIVALENT COMMAND LINE"` button  at the end of the page to get a **copy-paste-ready `gcloud compute instances create` command**.

<video controls>
  <source src="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-create-vm-from-cli.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

Note that we are using `dofroscra-test` as the **instance name** and  `us-central1-a` as the
**zone**.

```bash
gcloud compute instances create dofroscra-test \
    --project=pl-dofroscra-p \
    --zone=us-central1-a \
    --machine-type=e2-micro \
    --network-interface=network-tier=PREMIUM,subnet=default \
    --no-restart-on-failure \
    --maintenance-policy=TERMINATE \
    --provisioning-model=SPOT \
    --instance-termination-action=STOP \
    --service-account=docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --tags=http-server \
    --create-disk=auto-delete=yes,boot=yes,device-name=dofroscra-test,image=projects/debian-cloud/global/images/debian-11-bullseye-v20220519,mode=rw,size=10,type=projects/pl-dofroscra-p/zones/us-central1-a/diskTypes/pd-balanced \
    --no-shielded-secure-boot \
    --shielded-vtpm \
    --shielded-integrity-monitoring \
    --reservation-affinity=any
```

```text
Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test-1].

NAME            ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
dofroscra-test  us-central1-a  e2-small      true         10.128.0.2   34.122.227.169  RUNNING
```

<!-- generated -->
<a id='provisioning'> </a>
<!-- /generated -->

### Provisioning

For provisioning, we need to [install `docker`](#installing-docker-and-docker-compose) and  [authenticate the `root` user to pull images from our registry](#authenticate-docker-via-gcloud). We've already created an installation script at `.infrastructure/scripts/provision.sh` and the  easiest way to run it on the VM is to [transmit it via `scp`](#gcloud-scp)

```bash
gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./.infrastructure/scripts/provision.sh dofroscra-test:provision.sh
```

```text
$ gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./.infrastructure/scripts/provision.sh dofroscra-test:provision.sh
provision.sh              | 0 kB |   0.7 kB/s | ETA: 00:00:00 | 100%
```

The previous command transmitted the script in the home directory of the user, and we can now  execute it via

```bash
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="bash provision.sh"
```

```text
$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="bash provision.sh"
Hit:1 https://download.docker.com/linux/debian bullseye InRelease
Hit:2 http://packages.cloud.google.com/apt cloud-sdk-bullseye InRelease
# ...
The following additional packages will be installed:
  dbus-user-session docker-ce-rootless-extras docker-scan-plugin git git-man
  libcurl3-gnutls liberror-perl libgdbm-compat4 libltdl7 libperl5.32 libslirp0
  patch perl perl-modules-5.32 pigz slirp4netns
Suggested packages:
  aufs-tools cgroupfs-mount | cgroup-lite git-daemon-run | git-daemon-sysvinit
  git-doc git-el git-email git-gui gitk gitweb git-cvs git-mediawiki git-svn
  ed diffutils-doc perl-doc libterm-readline-gnu-perl
  | libterm-readline-perl-perl make libtap-harness-archive-perl
The following NEW packages will be installed:
  containerd.io dbus-user-session docker-ce docker-ce-cli
  docker-ce-rootless-extras docker-compose-plugin docker-scan-plugin git
  git-man libcurl3-gnutls liberror-perl libgdbm-compat4 libltdl7 libperl5.32
  libslirp0 patch perl perl-modules-5.32 pigz slirp4netns
0 upgraded, 20 newly installed, 0 to remove and 6 not upgraded.
Need to get 124 MB of archives.
After this operation, 535 MB of additional disk space will be used.
# ...
Setting up docker-ce (5:20.10.16~3-0~debian-bullseye) ...
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /lib/systemd/system/docker.service.
Created symlink /etc/systemd/system/sockets.target.wants/docker.socket → /lib/systemd/system/docker.socket.
Setting up liberror-perl (0.17029-1) ...
Setting up git (1:2.30.2-1) ...
Processing triggers for man-db (2.9.4-2) ...
Processing triggers for libc-bin (2.31-13+deb11u3) ...
```

Once `docker` is installed, we can run the authentication of the `root` user using  [`sudo su root -c`](https://unix.stackexchange.com/a/87861) via

```bash
gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="sudo su root -c 'gcloud auth configure-docker --quiet'"
```

```text
$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="sudo su root -c 'gcloud auth configure-docker --quiet'"
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
gcloud credential helpers already registered correctly.
```

<!-- generated -->
<a id='deployment'> </a>
<!-- /generated -->

### Deployment

We're almost done - the last step in the process consists of 

- building the docker image locally using the correct tag `gcr.io/pl-dofroscra-p/my-nginx`
  ```bash
  docker build -t "gcr.io/pl-dofroscra-p/my-nginx" -f - . <<EOF
  FROM nginx:1.21.5-alpine
  
  RUN echo "Hello world" >> /usr/share/nginx/html/hello.html
  
  EOF
  ```
  ```text
  $ docker build -t "gcr.io/pl-dofroscra-p/my-nginx" --no-cache -f - . <<EOF
  FROM nginx:1.21.5-alpine
  RUN echo "Hello world" >> /usr/share/nginx/html/hello.html
  EOF
  
  #1 [internal] load build definition from Dockerfile
  #1 sha256:21ee68236cc00e4d1638480de32fd6304d796e6a72306082d3164e9164073843
  
  #...
  
  #5 [2/2] RUN echo "Hello world" >> /usr/share/nginx/html/hello.html
  #5 sha256:72dee3f3637a6b78ff7e50591e3e9108e2b93eee09443e19890f9cb36b35bdc6
  #5 DONE 0.5s
  
  #6 exporting to image
  #6 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
  #6 exporting layers 0.1s done
  #6 writing image sha256:d0b23a80ebd7311953eeff3818b58007e8747e531e0128eef820f39105f9f1fe done
  #6 naming to gcr.io/pl-dofroscra-p/my-nginx done
  #6 DONE 0.1s
  ```
- local authentication
  ```bash
  cat ./gcp-service-account-key.json | docker login -u _json_key --password-stdin https://gcr.io
  ```
  ```text
  $ cat ./gcp-service-account-key.json | docker login -u _json_key --password-stdin https://gcr.io
  Login Succeeded
  
  Logging in with your password grants your terminal complete access to your account.
  For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
  ```
- [pushing it to the registry](#pushing-images-to-the-registry)
  ```bash
  docker push gcr.io/pl-dofroscra-p/my-nginx
  ```
  ```text
  
  $ docker push gcr.io/pl-dofroscra-p/my-nginx
  Using default tag: latest
  The push refers to repository [gcr.io/pl-dofroscra-1/my-nginx]
  ad4683501621: Preparing
  # ...
  ad4683501621: Pushed
  latest: digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4 size: 1775
  ```
- and "deploying it on the VM"

For the last step we will once again use a script that we transmit to the VM: `.infrastructure/scripts/deploy.sh`

```bash
#!/usr/bin/env bash

usage="Usage: deploy.sh image_name"
[ -z "$1" ] &&  echo "No image_name given! $usage" && exit 1

image_name=$1
container_name=nginx

echo "Pulling '${image_name}' from registry"
sudo docker pull "${image_name}"

echo "Starting container"
sudo docker kill "${container_name}"; sudo docker run --name "${container_name}" -p 80:80 --rm -d "${image_name}"

echo "Getting secret GPG_KEY"
gcloud secrets versions access latest --secret=GPG_KEY >> ./secret.gpg
head ./secret.gpg

echo "Getting secret GPG_PASSWORD"
GPG_PASSWORD=$(gcloud secrets versions access latest --secret=GPG_PASSWORD)
echo $GPG_PASSWORD
```

The script will [pull the image _on the VM_](#pulling-the-nginx-image) from the registry

```text
$ sudo docker pull 'gcr.io/pl-dofroscra-p/my-nginx'
Using default tag: latest
latest: Pulling from pl-dofroscra-p/my-nginx
# ...
Digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4
Status: Downloaded newer image for gcr.io/pl-dofroscra-p/my-nginx:latest
gcr.io/pl-dofroscra-p/my-nginx:latest
```

[start a container](#start-the-nginx-container) with the image 

```text
$ docker kill "${container_name}"; sudo docker run --name "${container_name}" -p 80:80 --rm -d "${image_name}"
Error response from daemon: Cannot kill container: nginx: No such container: nginx
8ac0cc055041e18d3ce244ded49c82be985e580c2c9f316c6d6ef7c7bf3bc0b5
```

and finally [retrieve the secrets](#get-the-secret-gpg-key-and-password-from-the-secret-manager)  (just to demonstrate that it works)

```text
$ gcloud secrets versions access latest --secret=GPG_KEY >> ./secret.gpg
$ head ./secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb

$ GPG_PASSWORD=$(gcloud secrets versions access latest --secret=GPG_PASSWORD)
$ echo $GPG_PASSWORD
87654321
```

Bonus: For  [retrieving the puplic IP address of the Compute Instance VM](https://cloud.google.com/compute/docs/instances/view-ip-address)  we can use the [`gcloud compute instances describe $instanceName`](https://cloud.google.com/sdk/gcloud/reference/compute/instances/describe) command:

```bash
gcloud compute instances describe dofroscra-test --zone us-central1-a --project=pl-dofroscra-p --format='get(networkInterfaces[0].accessConfigs[0].natIP)'
``` ```text
$ ip=$(gcloud compute instances describe dofroscra-test --zone us-central1-a --project=pl-dofroscra-p --format='get(networkInterfaces[0].accessConfigs[0].natIP)')
$ echo $ip
35.224.250.208
$ curl -s "http://${ip}/hello.html"
Hello world
```

<!-- generated -->
<a id='putting-it-all-together'> </a>
<!-- /generated -->

## Putting it all together 

Since none of the previous steps requires manual intervention any longer, I have created a  script at `.infrastructure/setup-gcp.sh` to run everything from [Configure `gcloud` to use the master service account](#configure-gcloud-to-use-the-master-service-account) to [Provisioning](#provisioning):

```bash
#!/usr/bin/env bash

usage="Usage: deploy.sh project_id vm_name"
[ -z "$1" ] &&  echo "No project_id given! $usage" && exit 1
[ -z "$2" ] &&  echo "No vm_name given! $usage" && exit 1

GREEN="\033[0;32m"
NO_COLOR="\033[0m"

project_id=$1
vm_name=$2
vm_zone=us-central1-a
master_service_account_key_location=./gcp-master-service-account-key.json
deployment_service_account_id=deployment
deployment_service_account_key_location=./gcp-service-account-key.json
deployment_service_account_mail="${deployment_service_account_id}@${project_id}.iam.gserviceaccount.com"
gpg_secret_key_location=secret-production-protected.gpg.example
gpg_secret_key_password=87654321

printf "${GREEN}Setting up GCP project for${NO_COLOR}\n"
echo "==="
echo "project_id: ${project_id}"
echo "vm_name:    ${vm_name}"

printf "${GREEN}Activating master service account${NO_COLOR}\n"
gcloud auth activate-service-account --key-file="${master_service_account_key_location}" --project="${project_id}"

printf "${GREEN}Enabling APIs${NO_COLOR}\n"
gcloud services enable \
  containerregistry.googleapis.com \
  secretmanager.googleapis.com \
  compute.googleapis.com \
  iam.googleapis.com \
  storage.googleapis.com \
  cloudresourcemanager.googleapis.com

printf "${GREEN}Creating deployment service account with id '${deployment_service_account_id}'${NO_COLOR}\n"
gcloud iam service-accounts create "${deployment_service_account_id}" \
  --description="Used for the deployment application" \
  --display-name="Deployment Account"
  
printf "${GREEN}Creating JSON key file for deployment service account at ${deployment_service_account_key_location}${NO_COLOR}\n"
gcloud iam service-accounts keys create "${deployment_service_account_key_location}" \
  --iam-account="${deployment_service_account_mail}"

printf "${GREEN}Adding roles for service account${NO_COLOR}\n"  
roles="storage.admin secretmanager.admin compute.admin iam.serviceAccountUser iap.tunnelResourceAccessor"

for role in $roles; do
  gcloud projects add-iam-policy-binding "${project_id}" --member=serviceAccount:"${deployment_service_account_mail}" "--role=roles/${role}"
done;

printf "${GREEN}Creating secrets${NO_COLOR}\n"
gcloud secrets create GPG_KEY
echo gcloud secrets versions add GPG_KEY --data-file="${gpg_secret_key_location}"

gcloud secrets create GPG_PASSWORD
echo -n "${gpg_secret_key_password}" | gcloud secrets versions add GPG_PASSWORD --data-file=-

printf "${GREEN}Creating firewall rule to allow HTTP traffic${NO_COLOR}\n"
gcloud compute firewall-rules create default-allow-http --allow tcp:80 --target-tags=http-server

printf "${GREEN}Creating a Compute Instance VM${NO_COLOR}\n"
gcloud compute instances create "${vm_name}" \
    --project="${project_id}" \
    --zone="${vm_zone}" \
    --machine-type=e2-micro \
    --network-interface=network-tier=PREMIUM,subnet=default \
    --no-restart-on-failure \
    --maintenance-policy=TERMINATE \
    --provisioning-model=SPOT \
    --instance-termination-action=STOP \
    --service-account="${deployment_service_account_mail}" \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --tags=http-server \
    --create-disk=auto-delete=yes,boot=yes,device-name="${vm_name}",image=projects/debian-cloud/global/images/debian-11-bullseye-v20220519,mode=rw,size=10,type=projects/"${project_id}"/zones/"${vm_zone}"/diskTypes/pd-balanced \
    --no-shielded-secure-boot \
    --shielded-vtpm \
    --shielded-integrity-monitoring \
    --reservation-affinity=any

printf "${GREEN}Activating deployment service account${NO_COLOR}\n"
gcloud auth activate-service-account --key-file="${deployment_service_account_key_location}" --project="${project_id}"

printf "${GREEN}Transferring provisioning script${NO_COLOR}\n"
echo "Waiting 60s for the instance to be fully ready to receive IAP connections"
sleep 60
gcloud compute scp --zone ${vm_zone} --tunnel-through-iap --project=${project_id} ./.infrastructure/scripts/provision.sh ${vm_name}:provision.sh

printf "${GREEN}Executing provisioning script${NO_COLOR}\n"
gcloud compute ssh ${vm_name} --zone ${vm_zone} --tunnel-through-iap --project=${project_id} --command="bash provision.sh"

printf "${GREEN}Authenticating docker via gcloud in the VM${NO_COLOR}\n"
gcloud compute ssh ${vm_name} --zone ${vm_zone} --tunnel-through-iap --project=${project_id} --command="sudo su root -c 'gcloud auth configure-docker --quiet'"

printf "\n\n${GREEN}Provisioning done!${NO_COLOR}\n"
```

```text
$ bash .infrastructure/setup-gcp.sh pl-dofroscra-p dofroscra-test
Setting up GCP project for
===
project_id: pl-dofroscra-p
vm_name:    dofroscra-test
Activating master service account
Activated service account credentials for: [master@pl-dofroscra-p.iam.gserviceaccount.com]
Enabling APIs
Operation "operations/acf.p2-305055072470-625a52a9-96a7-4e2f-831e-a76491c8882c" finished successfully.
Creating deployment service account with id 'deployment'
Created service account [deployment].
Creating JSON key file for deployment service account at ./gcp-service-account-key.json
created key [fa74d605f5891a6f875a2663b6beeea425730b5b] of type [json] as [./gcp-service-account-key.json] for [deployment@pl-dofroscra-p.iam.gserviceaccount.com]
Adding roles for service account
Updated IAM policy for project [pl-dofroscra-p].
bindings:
# ...
- members:
  - serviceAccount:deployment@pl-dofroscra-p.iam.gserviceaccount.com
  role: roles/storage.admin
etag: BwXgw51MPTM=
version: 1
Creating secrets
Created secret [GPG_KEY].
Created version [1] of the secret [GPG_KEY].
Created secret [GPG_PASSWORD].
Created version [1] of the secret [GPG_PASSWORD].
Creating firewall rule to allow HTTP traffic
Creating firewall...
..Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/global/firewalls/default-allow-http].
done.
Creating a Compute Instance VM
Created [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p/zones/us-central1-a/instances/dofroscra-test].
NAME        ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP    STATUS
dofroscra-test  us-central1-a  e2-micro      true         10.128.0.2   34.70.213.101  RUNNING
Activating deployment service account
Activated service account credentials for: [deployment@pl-dofroscra-p.iam.gserviceaccount.com]
Transferring provisioning script
Updating project ssh metadata...
..........................................................................Updated [https://www.googleapis.com/compute/v1/projects/pl-dofroscra-p].
.done.
Waiting for SSH key to propagate.
# ...
provision.sh              | 0 kB |   0.7 kB/s | ETA: 00:00:00 | 100%)
Executing provisioning script
Get:1 http://security.debian.org/debian-security bullseye-security InRelease [44.1 kB]
# ...
Processing triggers for man-db (2.9.4-2) ...
Processing triggers for libc-bin (2.31-13+deb11u3) ...
Authenticating docker via gcloud in the VM
# ...
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
Docker configuration file updated.


Provisioning done!
```

In addition, I also created a script for the [Deployment](#deployment) at  `.infrastructure/deploy.sh`:

```bash
#!/usr/bin/env bash

usage="Usage: deploy.sh project_id vm_name"
[ -z "$1" ] &&  echo "No project_id given! $usage" && exit 1
[ -z "$2" ] &&  echo "No vm_name given! $usage" && exit 1

GREEN="\033[0;32m"
NO_COLOR="\033[0m"

project_id=$1
vm_name=$2
vm_zone=us-central1-a
image_name="gcr.io/${project_id}/nginx:latest"  
container_name=nginx
deployment_service_account_key_location=./gcp-service-account-key.json

printf "${GREEN}Building nginx docker image with name '${image_name}'${NO_COLOR}\n"
docker build -t "${image_name}" -f - . <<EOF
FROM nginx:1.21.5-alpine

RUN echo "Hello world" >> /usr/share/nginx/html/hello.html

EOF

printf "${GREEN}Authenticating docker${NO_COLOR}\n"
cat "${deployment_service_account_key_location}" | docker login -u _json_key --password-stdin https://gcr.io

printf "${GREEN}Pushing '${image_name}' to registry${NO_COLOR}\n"
docker push "${image_name}"

printf "${GREEN}Transferring deployment script${NO_COLOR}\n"
gcloud compute scp --zone ${vm_zone} --tunnel-through-iap --project=${project_id} ./.infrastructure/scripts/deploy.sh ${vm_name}:deploy.sh

printf "${GREEN}Executing deployment script${NO_COLOR}\n"
gcloud compute ssh ${vm_name} --zone ${vm_zone} --tunnel-through-iap --project=${project_id} --command="bash deploy.sh '${image_name}'"

printf "${GREEN}Retrieving external IP of the VM${NO_COLOR}\n"
ip_address=$(gcloud compute instances describe ${vm_name} --zone ${vm_zone} --project=${project_id} --format='get(networkInterfaces[0].accessConfigs[0].natIP)')
printf "http://${ip_address}/hello.html\n"

printf "\n\n${GREEN}Deployment done!${NO_COLOR}\n"
```

```text
$ bash .infrastructure/deploy.sh pl-dofroscra-p dofroscra-test
Building nginx docker image with name 'gcr.io/pl-dofroscra-p/nginx:latest'
#...
#7 writing image sha256:65ff457111599e3f4dd439138b052cff74f10e3332c593178d8288aebb88bb1c done
#7 naming to gcr.io/pl-dofroscra-p/nginx:latest done
#7 DONE 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Authenticating docker
Login Succeeded

Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/
Pushing 'gcr.io/pl-dofroscra-p/nginx:latest' to registry
The push refers to repository [gcr.io/pl-dofroscra-p/nginx]
ad4683501621: Preparing
# ...
1c9c1e42aafa: Pushed
latest: digest: sha256:680086b3c77e4b895099c0a5f6e713ff79ba0d78e1e1df1bc2546d6f979126e4 size: 1775
Transferring deployment script
deploy.sh                 | 0 kB |   0.6 kB/s | ETA: 00:00:00 | 100%
Executing deployment script
Pulling 'gcr.io/pl-dofroscra-p/nginx:latest' from registry
latest: Pulling from pl-dofroscra-p/nginx
Digest: sha256:f17a9092051c389abf76d254e4d564dbd7a814eb21e3cc47b667db301aa9b497
# ...
gcr.io/pl-dofroscra-p/nginx:latest
Starting container
nginx
58c0a34ca44c9bec97c991bdc69d2353ed75f4214f221e444da36e195a215c75
Getting secret GPG_KEY
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQPGBGKA1psBCACq5zYDT587CVZEIWXbUplfAGQZOQJALmzErYpTp0jt+rp4vJhR
U5xahy3pqCq81Cnny5YME50ybB3pW/WcHxWLBDo+he8PKeLbp6wFFjJns+3u4opH
9gFMElyHpzTGiDQYfx/CgY2hKz7GSqpjmnOaKxYvGv0EsbZczyHY1WIN/YFzb0tI
tY7J4zTSH05I+aazRdHyn28QcCRcIT9+4q+5Vk8gz8mmgoqVpyeNgQcqJjcd03iP
WUZd1vZCumOvdG5PZNlc/wPFhqLDmYyLmJ7pt5bWIgty9BjYK8Z2NOdUaekqVEJ+
r29HbzwgFLLE2gd52f07h2y2YgMdWdz4FDxVABEBAAH+BwMC9veBYT2oigXxExLl
7fZKVjw02lEr1NpYd5X1ge9WPU/1qumATJWounzciiETpsYGsbPd9zFRJP4E3JZl
sFSh4p0/kXYTuenYD8wgGkeYyN4lm53IHfqSn2z9JMW5Kz9XEODtKJl8fjcn9Zeb
Getting secret GPG_PASSWORD
87654321
Retrieving external IP of the VM
http://34.70.213.101/hello.html


Deployment done!
```

<!-- generated -->
<a id='cleanup'> </a>
<!-- /generated -->

## Cleanup

The easiest way to "cleanup everything" that might create any costs, e.g.

- the docker images stored on Cloud Storage
- the secrets in the Secret Manager
- the VM itself

is to  [delete the whole project](https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects), e.g. via the [Settings UI](https://console.cloud.google.com/iam-admin/settings)

<video controls>
  <source src="https://www.pascallandau.com/img/gcp-compute-instance-vm-docker/gcp-shutdown-project.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

FYI: GCP projects have 30-day-grace-period during that you can "revert" the deletion.

<!-- generated -->
<a id='wrapping-up'> </a>
<!-- /generated -->

## Wrapping up

Congratulations, you made it! If some things are not completely clear by now, don't hesitate to leave a comment. You should now be familiar enough with GCP to create a Compute Instance VM and  configure it to run dockerized applications on it.

In the next part of this tutorial, we will  [deploy our dockerized PHP application "to production" on GCP via `docker compose`](https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/).

Please subscribe to the [RSS feed](https://www.pascallandau.com/feed.xml) or [via email](https://www.pascallandau.com/blog/#newsletter) to get automatic notifications when this next part comes out :)