<?xml version = "1.0" encoding = "UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>pascallandau.com</title>
        <description>Personal website of Pascal Landau</description>
        <link>https://www.pascallandau.com</link>
        <atom:link href="https://www.pascallandau.com/feed.xml" rel="self" type="application/rss+xml"/>
        <pubDate>Mon, 24 Apr 2023 13:31:04 +0000</pubDate>
        <lastBuildDate>Mon, 24 Apr 2023 13:31:04 +0000</lastBuildDate>
        <language>en</language>
                    <item>
                <title>Create a production infrastructure for dockerized PHP Apps on GCP [Tutorial Part 10]</title>
                <description><![CDATA[<p>In the tenth part of this tutorial series on developing PHP on Docker we will 
<strong>create a production infrastructure for a dockerized PHP application on GCP</strong> using multiple 
VMs and managed services for <code>redis</code> and <code>mysql</code>.</p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>What will you learn?</strong>
  </div>
  <div class="panel-body bg-info">
    We'll modify the setup introduced in the previous tutorial
    <a href="/blog/deploy-docker-compose-php-gcp-poc/">Deploy dockerized PHP Apps to production on GCP via docker compose as a POC</a>
    and create an individual VM for each of our docker services. For the PHP application 
    containers we'll keep using Compute Instance VMs, and for <code>mysql</code> and 
    <code>redis</code> we'll use GCP managed products. <br>
    <br>
    You'll learn how to create the 
    corresponding infrastructure on GCP via the UI as well as through the <code>gcloud</code> cli.
  </div>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch with the final result of this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-10-create-production-infrastructure-php-app-gcp">part-10-create-production-infrastructure-php-app-gcp</a>.</p>

<p><strong>CAUTION</strong>: With this codebase it is <em>not</em> possible to deploy any longer! Please refer to the next
part 
<a href="/blog/deploy-dockerized-php-app-production/">Deploy dockerized PHP Apps to production - using multiple VMS and managed mysql and redis instances from GCP</a>
for the code to enable deployments again.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/deploy-docker-compose-php-gcp-poc/">Deploy dockerized PHP Apps to production on GCP via docker compose as a POC</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a> to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a>

<ul>
<li><a href="#run-the-code-yourself">Run the code yourself</a></li>
</ul></li>
<li><a href="#additional-gcp-concepts">Additional GCP concepts</a>

<ul>
<li><a href="#ips-networking-and-vpcs-virtual-private-cloud">IPs, Networking and VPCs (Virtual Private Cloud)</a></li>
<li><a href="#routers-and-nats">Routers and NATs</a></li>
<li><a href="#ip-range-allocations-vpc-peering-and-private-service-access">IP range allocations, VPC Peering and private service access</a></li>
<li><a href="#additional-service-apis-and-iam-roles">Additional Service APIs and IAM roles</a></li>
</ul></li>
<li><a href="#service-setup">Service setup</a>

<ul>
<li><a href="#cloud-sql-for-mysql-setup">Cloud SQL for MySQL setup</a></li>
<li><a href="#redis-memorystore-setup">Redis Memorystore setup</a></li>
<li><a href="#set-up-the-vms-that-run-docker-containers">Set up the VMs that run <code>docker</code> containers</a>

<ul>
<li><a href="#provisioning-the-vms">Provisioning the VMs</a></li>
</ul></li>
<li><a href="#mapping-service-names-to-vm-names">Mapping service names to VM names</a></li>
</ul></li>
<li><a href="#appendix-changes-in-the-codebase">Appendix: Changes in the codebase</a>

<ul>
<li><a href="#add-a-make-dev-init-target">Add a <code>make dev-init</code> target</a></li>
<li><a href="#update-the-gcloud-targets">Update the <code>gcloud</code> targets</a></li>
<li><a href="#add-additional-make-variables">Add additional <code>make</code> variables</a></li>
<li><a href="#use-make-to-execute-infrastructure-and-deployment-targets-in-parallel">Use <code>make</code> to execute infrastructure and deployment targets in parallel</a></li>
</ul></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>In <a href="/blog/deploy-docker-compose-php-gcp-poc/">Deploy dockerized PHP Apps to production on GCP via docker compose as a POC</a>
we've <strong>created a single Compute Instance VM</strong>, provisioned it with <code>docker compose</code> and
<strong>ran our full <code>docker compose</code> setup</strong> on it. In other words: All containers ran on the same VM
(that had to be reachable from the internet).</p>

<p><a href="/img/create-production-infrastructure-docker-php-app-gcp/infrastructure-docker-compose-poc.PNG"><img src="/img/create-production-infrastructure-docker-php-app-gcp/infrastructure-docker-compose-poc.PNG" alt="docker compose (POC) based infrastructure on GCP" /></a></p>

<p>In this part we want to split this setup up so that:</p>

<ul>
<li>each <code>docker</code> container runs on its own VM</li>
<li><code>redis</code> and <code>mysql</code> won't run in <code>docker</code> containers but as the managed GCP products MySQL
<a href="https://cloud.google.com/sql/mysql">Cloud SQL for MySQL</a> 
and <a href="https://cloud.google.com/memorystore/docs/redis">Redis Memorystore</a></li>
</ul>

<p>In addition, we want to expose only the <code>nginx</code> service to the internet - all other VMs should
communicate via private IP addresses.</p>

<p><a href="/img/create-production-infrastructure-docker-php-app-gcp/infrastructure-plan.PNG"><img src="/img/create-production-infrastructure-docker-php-app-gcp/infrastructure-plan.PNG" alt="docker based infrastructure on GCP" /></a></p>

<p><!-- generated -->
<a id='run-the-code-yourself'> </a>
<!-- /generated --></p>

<h3>Run the code yourself</h3>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Caution</strong>
  </div>
  <div class="panel-body bg-danger">
    The following steps <strong>will create actual infrastructure on GCP</strong> which means you
    will create costs (albeit quite little). Please make sure to shut the project down once you are
    done, see
    <a href="/blog/gcp-compute-instance-vm-docker/#cleanup">Run Docker on GCP Compute InstanceVMs: Cleanup</a>.
  </div>
</div>

<p>I recommend <strong>creating a completely new GCP project</strong> to have a "clean slate" that ensures that 
everything works out of the box as intended.</p>

<pre><code class="language-bash"># Prepare the codebase
git clone https://github.com/paslandau/docker-php-tutorial.git &amp;&amp; cd docker-php-tutorial
git checkout part-10-create-production-infrastructure-php-app-gcp

# Run the initialization. 
make dev-init

# Note: 
# You don't have to follow the additional instructions of the `dev-init` target 
# for this part of the tutorial.

# The following steps need to be done manually:
# 
# - Create a new GCP project and "master" service account with Owner permissions.
# - Create a key file for that master service account and place it in the root of the codebase at
#   ./gcp-master-service-account-key.json
#
# @see https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/#preconditions-project-and-owner-service-account

ls ./gcp-master-service-account-key.json
# Should NOT fail with
#   ls: cannot access './gcp-master-service-account-key.json': No such file or directory

# Update the variables `DOCKER_REGISTRY` and `GCP_PROJECT_ID` in `.make/variables.env` 

projectId="SET YOUR GCP_PROJECT_ID HERE"
# CAUTION: Mac users might need to use `sed -i '' -e` instead of `sed -i`!
# @see https://stackoverflow.com/a/19457213/413531
sed -i "s/DOCKER_REGISTRY=.*/DOCKER_REGISTRY=gcr.io\/${projectId}/" .make/variables.env
sed -i "s/GCP_PROJECT_ID=.*/GCP_PROJECT_ID=${projectId}/" .make/variables.env

make docker-build
make docker-up
make gpg-init
make secret-decrypt

# Then run
make infrastructure-setup ROOT_PASSWORD="production_secret_mysql_root_password"

# FYI: This step can take 15 to 30 minutes to complete.

# Verify the setup
make infrastructure-info
</code></pre>

<p>Should show something like this</p>

<pre><code class="language-text">$ make infrastructure-info
NAME            ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP    STATUS
application-vm  us-central1-a  e2-micro                   10.128.0.5                  RUNNING
nginx-vm        us-central1-a  e2-micro                   10.128.0.3   34.134.120.87  RUNNING
php-fpm-vm      us-central1-a  e2-micro                   10.128.0.2                  RUNNING
php-worker-vm   us-central1-a  e2-micro                   10.128.0.4                  RUNNING

INSTANCE_NAME  VERSION    REGION       TIER   SIZE_GB  HOST           PORT  NETWORK  RESERVED_IP       STATUS  CREATE_TIME
redis-vm       REDIS_6_X  us-central1  BASIC  1        10.137.150.67  6379  default  10.137.150.64/29  READY   2022-09-12T11:22:14

NAME      DATABASE_VERSION  LOCATION       TIER              PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
mysql-vm  MYSQL_8_0         us-central1-b  db-custom-1-3840  -                10.111.0.3       RUNNABLE
</code></pre>

<p>The following video shows the full process (excluding most of the waiting time)</p>

<video controls>
  <source src="/img/create-production-infrastructure-docker-php-app-gcp/gcp-create-production-infrastructure.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='additional-gcp-concepts'> </a>
<!-- /generated --></p>

<h2>Additional GCP concepts</h2>

<p><!-- generated -->
<a id='ips-networking-and-vpcs-virtual-private-cloud'> </a>
<!-- /generated --></p>

<h3>IPs, Networking and VPCs (Virtual Private Cloud)</h3>

<p>In order to restrict access to VMs from the internet, we need to ensure that GCP does not assign 
them a <strong>public IP address</strong>. They still need to be able to communicate with each other though 
and thus need a <strong>private IP address</strong> within the <strong>same network / 
<a href="https://en.wikipedia.org/wiki/Virtual_private_cloud">VPC (Virtual Private Cloud)</a></strong>.</p>

<p>So far, we didn't need to think about that at all, because 
<a href="https://cloud.google.com/vpc/docs/vpc#default-network">GCP creates a default network</a> (a 
so-called auto mode VPC network) called <code>default</code> for each project, and we have simply used it 
as-is. But there are 
<a href="https://cloud.google.com/vpc/docs/vpc#auto-mode-considerations">a number of reasons why not using the default network is a good idea</a>,
e.g.</p>

<ul>
<li>unnecessary subnetworks (one for each region - we'll only need one region for our setup)</li>
<li><a href="https://cloud.google.com/vpc/docs/firewalls#more_rules_default_vpc">overly permissive firewall rules</a>
(e.g. for port 3389 for the Microsoft Remote Desktop Protocol [RDP].)</li>
</ul>

<p>But <strong>for now the <code>default</code> network is "good enough"</strong>, because it's not per-se insecure (as in 
"nobody from the outside world has access to it"), and we'll tackle this and some other security 
hardening measures in a later tutorial.</p>

<p>You can find the <code>default</code> network in the 
<a href="https://console.cloud.google.com/networking/networks/list">VPC networks UI</a></p>

<p><a href="/img/create-production-infrastructure-docker-php-app-gcp/gcp-vpc-networks-ui-cloud-console.PNG"><img src="/img/create-production-infrastructure-docker-php-app-gcp/gcp-vpc-networks-ui-cloud-console.PNG" alt="VPC networks UI in the Cloud Console" /></a></p>

<p><!-- generated -->
<a id='routers-and-nats'> </a>
<!-- /generated --></p>

<h3>Routers and NATs</h3>

<p>Using only private IP addresses comes with a non-obvious caveat: Compute Instance VMs cannot 
only no longer <em>be reached</em> from the internet <strong>but also not <em>reach</em> the internet any longer 
<em>themselves</em></strong> as per <a href="https://cloud.google.com/vpc/docs/configure-private-google-access">GCP docs</a>:</p>

<blockquote>
  <p>By default, when a Compute Engine VM lacks an external IP address assigned to its network 
  interface, it can only send packets to other internal IP address destinations.</p>
</blockquote>

<p>This is a problem for a number of reasons, e.g.</p>

<ul>
<li>you can't install new software on the VM (e.g. <code>apt-get</code> will fail)</li>
<li>it's impossible to retrieve <code>docker</code> images from the registry</li>
<li>the application itself might need to make HTTP requests to public APIs</li>
</ul>

<p>Fortunately, there is a simple solution: 
<a href="https://en.wikipedia.org/wiki/Network_address_translation">NAT</a>. NAT is short for Network 
Address Translation and is basically happening for any private internet access at home. Your 
router "translates" the internal IP address of your local machine to a public one, so that the 
response packets can be routed back to the router and from there to your local machine.</p>

<p>GCP offers the same functionality via <a href="https://cloud.google.com/nat/docs/overview">Cloud NAT</a>.</p>

<p><a href="/img/create-production-infrastructure-docker-php-app-gcp/gcp-cloud-nat-architecture.svg"><img src="/img/create-production-infrastructure-docker-php-app-gcp/gcp-cloud-nat-architecture.svg" alt="Cloud NAT architecture" /></a></p>

<p>(<a href="https://cloud.google.com/nat/docs/overview#architecture">Source</a>)</p>

<p>We must first create a 
<a href="https://cloud.google.com/network-connectivity/docs/router/concepts/overview">Cloud Router</a>
that serves as the control plane for Cloud NAT. The creation process is explained in the 
<a href="https://cloud.google.com/network-connectivity/docs/router/how-to/create-router-vpc-network">GCP Cloud Router Guide: Create a Cloud Router</a>.
Please note, that for Cloud NAT routers it's not necessary to assign 
<a href="https://cloud.google.com/network-connectivity/docs/router/concepts/overview#asn">ASN numbers</a>:</p>

<blockquote>
  <p>Note: Cloud NAT does not use ASN information. Cloud NAT gateways can be connected to Cloud 
  Routers that have any ASN or that have no ASN specified.</p>
</blockquote>

<p>Once the Cloud Router exists, we can create a Cloud NAT following the steps outlined in the
<a href="https://cloud.google.com/nat/docs/set-up-manage-network-address-translation#set_up_a_simple_configuration">GCP Cloud NAT Guide: Set up and manage network address translation with Cloud NAT</a>.</p>

<p>The following video explains the full process via the Cloud Console UI by showing that a Compute 
Instance VM without external IP address cannot reach <code>http://example.com/</code> unless a Cloud NAT is 
created for the same network.</p>

<video controls>
  <source src="/img/create-production-infrastructure-docker-php-app-gcp/gcp-cloud-router-nat-gateway-private-ip-vm.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><strong>Creating a Cloud Router and a Cloud NAT gateway can also be done via <code>gcloud</code> cli</strong>. I've added 
the following commands to <code>.infrastructure/setup-gcp.sh</code></p>

<pre><code class="language-bash">region=us-central1
router_name=default-router
nat_name=default-nat-gateway
network="default"

gcloud compute routers create "${router_name}" \
      --region="${region}" \
      --network="${network}"

gcloud compute routers nats create "${nat_name}"  \
    --router="${router_name}" \
    --router-region="${region}" \
    --auto-allocate-nat-external-ips \
    --nat-all-subnet-ip-ranges
</code></pre>

<p><code>gcloud</code> cli docs:</p>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/compute/routers/create"><code>gcloud compute routers create</code></a></li>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/compute/routers/nats/create"><code>gcloud compute routers nats create</code></a></li>
</ul>

<p><!-- generated -->
<a id='ip-range-allocations-vpc-peering-and-private-service-access'> </a>
<!-- /generated --></p>

<h3>IP range allocations, VPC Peering and private service access</h3>

<p>Since we will be using managed services for <code>mysql</code> and <code>redis</code>, we need to create a so-called 
<strong>VPC peering</strong> with the <strong>Google Cloud Platform Service Producer</strong>. This is necessary, because 
GCP will create a dedicated VPCs for MySQL Cloud SQL and Redis Memorystore instances. See 
also the GCP VPC Guides:</p>

<ul>
<li><a href="https://cloud.google.com/vpc/docs/vpc-peering">VPC Network Peering overview</a></li>
<li><a href="https://cloud.google.com/vpc/docs/private-services-access">Private services access</a></li>
<li><a href="https://cloud.google.com/vpc/docs/configure-private-services-access">Configuring private services access</a></li>
</ul>

<p>Since we don't want to assign public IPs, we need to "peer" 
<a href="#ips-networking-and-vpcs-virtual-private-cloud">our <code>default</code> VPC</a> with those dedicated 
service VPCs. I have explained this in more detail in
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#connecting-to-the-mysql-instance-via-private-ip">my MySQL Cloud SQL article for connecting via private IP</a>.
In short:</p>

<ul>
<li>we must allocate a certain range of IPs in the <code>default</code> network</li>
<li>this range is then assigned to the VPC peering for Google Cloud Platform Services</li>
<li>in consequence, the <code>redis</code> and <code>mysql</code> instances will receive an IP address from the 
allocated IP range and are thus "visible" for all other VMs in the <code>default</code> network</li>
</ul>

<p>See also the <a href="https://cloud.google.com/sql/docs/mysql/private-ip#example">Example in the MySQL Guide: Learn about using private IP</a>:</p>

<p><a href="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/mysql-cloud-sql-private-ip-vpc-peering.svg"><img src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/mysql-cloud-sql-private-ip-vpc-peering.svg" alt="Connecting to Cloud SQL via private IP through VPC peering" /></a></p>

<p>This video shows the 
<a href="https://cloud.google.com/vpc/docs/configure-private-services-access#procedure">full procedure</a> 
to create an IP range allocation and a VPC peering to enable private access on a MySQL Cloud SQL 
instance:</p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/connect-cloud-sql-mysql-instance-via-private-ip.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><strong>Creating an IP range allocation and a VPC peering can also be done via <code>gcloud</code> cli</strong>. I've added
the following commands to <code>.infrastructure/setup-gcp.sh</code></p>

<pre><code class="language-bash">network="default"
private_vpc_range_name="google-managed-services-vpc-allocation"
ip_range_network_address="10.111.0.0"

gcloud compute addresses create "${private_vpc_range_name}" \
    --global \
    --purpose=VPC_PEERING \
    --prefix-length=16 \
    --description="Peering range for Google" \
    --network="${network}" \
    --addresses="${ip_range_network_address}"

gcloud services vpc-peerings connect \
    --service=servicenetworking.googleapis.com \
    --ranges="${private_vpc_range_name}" \
    --network="${network}"
</code></pre>

<p><code>gcloud</code> cli docs:</p>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/compute/addresses/create"><code>gcloud compute addresses create</code></a></li>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/services/vpc-peerings/connect"><code>gcloud services vpc-peerings connect</code></a></li>
</ul>

<p><!-- generated -->
<a id='additional-service-apis-and-iam-roles'> </a>
<!-- /generated --></p>

<h3>Additional Service APIs and IAM roles</h3>

<p>Since we are using even more GCP services as before, we need to enable the corresponding APIs as 
well. Those are:</p>

<ul>
<li><a href="https://console.cloud.google.com/apis/library/servicenetworking.googleapis.com">servicenetworking.googleapis.com</a>

<ul>
<li>required for <a href="#ip-range-allocations-vpc-peering-and-private-service-access">allocating the IP range and establishing the VPC peering</a></li>
</ul></li>
<li><a href="https://console.cloud.google.com/apis/library/sqladmin.googleapis.com">sqladmin.googleapis.com</a>

<ul>
<li>required for the <a href="#cloud-sql-for-mysql-setup">Cloud SQL for MySQL setup and management</a></li>
</ul></li>
<li><a href="https://console.cloud.google.com/apis/library/redis.googleapis.com">redis.googleapis.com</a>

<ul>
<li>required for <a href="#redis-memorystore-setup">Redis Memorystore setup and management</a></li>
</ul></li>
</ul>

<p>As part of the deployment process in the next part of the tutorial
<a href="/blog/deploy-dockerized-php-app-production/">Deploy dockerized PHP Apps to production - using multiple VMS and managed mysql and redis instances from GCP</a>,
we need to <strong>retrieve the IP addresses of all our services</strong>
(see <a href="/blog/deploy-dockerized-php-app-production#poor-man-s-dns-via-add-host">Poor man's DNS via <code>--add-host</code></a>) 
via the <code>gcloud</code> cli. This requires some additional 
<a href="/blog/gcp-compute-instance-vm-docker/#configure-iam-permissions">IAM roles / permissions</a>
for the 
<a href="/blog/gcp-compute-instance-vm-docker/#create-and-configure-a-deployment-service-account">deployment service account</a>:</p>

<ul>
<li><a href="https://cloud.google.com/iam/docs/understanding-roles#cloudsql.viewer">cloudsql.viewer</a>

<ul>
<li>required to retrieve meta data from <a href="#cloud-sql-for-mysql-setup">MySQL Cloud SQL instances</a></li>
</ul></li>
<li><a href="https://cloud.google.com/iam/docs/understanding-roles#redis.viewer">redis.viewer</a>

<ul>
<li>required to retrieve meta data from <a href="#redis-memorystore-setup">Redis Memorystore instances</a></li>
</ul></li>
<li><a href="https://cloud.google.com/iam/docs/understanding-roles#compute.viewer">compute.viewer</a>

<ul>
<li>required to retrieve meta data from <a href="#set-up-the-vms-that-run-docker-containers">Compute Instance VMs</a></li>
</ul></li>
</ul>

<p>I have added the APIs and roles also in the <code>.infrastructure/setup-gcp.sh</code> script:</p>

<pre><code class="language-bash">project_id=$1
deployment_service_account_id=deployment
deployment_service_account_mail="${deployment_service_account_id}@${project_id}.iam.gserviceaccount.com"

gcloud services enable \
  containerregistry.googleapis.com \
  secretmanager.googleapis.com \
  compute.googleapis.com \
  iam.googleapis.com \
  storage.googleapis.com \
  cloudresourcemanager.googleapis.com \
  sqladmin.googleapis.com \
  redis.googleapis.com \
  servicenetworking.googleapis.com

roles="storage.admin secretmanager.admin compute.admin iam.serviceAccountUser iap.tunnelResourceAccessor cloudsql.viewer redis.viewer compute.viewer"
for role in $roles; do
  gcloud projects add-iam-policy-binding "${project_id}" --member=serviceAccount:"${deployment_service_account_mail}" "--role=roles/${role}"
done;
</code></pre>

<p><!-- generated -->
<a id='service-setup'> </a>
<!-- /generated --></p>

<h2>Service setup</h2>

<p><!-- generated -->
<a id='cloud-sql-for-mysql-setup'> </a>
<!-- /generated --></p>

<h3>Cloud SQL for MySQL setup</h3>

<p>Please refer to <a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/">How to use GCP MySQL Cloud SQL instances - from creation over connection to deletion</a>
for a general introduction into Cloud SQL for MySQL.</p>

<p>For this tutorial, we will create a <code>mysql</code> instance with the following settings:</p>

<ul>
<li>1 CPU</li>
<li>3840 MB RAM</li>
<li>Private Service Access / private IP only</li>
<li>Enabled deletion protection</li>
</ul>

<p>We'll also set the password of the <code>root</code> user to the password defined in the 
<a href="/blog/run-laravel-9-docker-in-2022/#database-connection"><code>DB_PASSWORD</code> variable</a> 
in <a href="/blog/deploy-docker-compose-php-gcp-poc/#the-secrets-directory"><code>.secrets/prod/app.env</code></a>
and create an application database named</p>

<pre><code class="language-text">application_db
</code></pre>

<p>I adapted the <code>gcloud</code> setup from
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#using-the-gcloud-cli">How to use GCP MySQL Cloud SQL instances: "Using the <code>gcloud</code> CLI</a>
to create the script <code>.infrastructure/setup-mysql.sh</code> that will 
create a <code>mysql</code> instance and uses the following commands</p>

<pre><code class="language-bash">project_id=$1
mysql_instance_name=$2
root_password=$3
region=us-central1
master_service_account_key_location=./gcp-master-service-account-key.json
memory="3840MB"
cpus=1
version="MYSQL_8_0"
default_database="application_db"
network="default"
private_vpc_range_name="google-managed-services-vpc-allocation"

gcloud beta sql instances create "${mysql_instance_name}" \
        --database-version="${version}" \
        --cpu="${cpus}" \
        --memory="${memory}" \
        --region="${region}" \
        --network="${network}" \
        --deletion-protection \
        --no-assign-ip \
        --allocated-ip-range-name="${private_vpc_range_name}"

gcloud sql users set-password root \
        --host=% \
        --instance "${mysql_instance_name}" \
        --password "${root_password}"

gcloud sql databases create "${default_database}" \
        --instance="${mysql_instance_name}" \
        --charset=utf8mb4 \
        --collation=utf8mb4_unicode_ci
</code></pre>

<p>A corresponding <code>make infrastructure-setup-mysql</code> target is defined in <code>.make/04-00-infrastructure.mk</code></p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-mysql
infrastructure-setup-mysql: ## Set the mysql instance up. The ROOT_PASSWORD variable is required to defined the password for the root user
    @$(if $(ROOT_PASSWORD),,$(error "ROOT_PASSWORD is undefined"))
    bash .infrastructure/setup-mysql.sh $(GCP_PROJECT_ID) $(VM_NAME_MYSQL) $(ROOT_PASSWORD) $(ARGS)
</code></pre>

<p><strong>CAUTION</strong>: We use <code>MYSQL_8_0</code> as version for the instance and should ensure to keep this in 
sync with the value of the <code>MYSQL_VERSION</code> variable in the
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker-env-file-and-required-env-variables"><code>.docker/.env</code> file</a> (see also 
<a href="/blog/structuring-the-docker-setup-for-php-projects/#env-example-and-docker-compose-yml">Structuring the Docker setup for PHP Projects <code>.env.example</code> and <code>docker-compose.yml</code></a>), 
so that we use the same version in the <code>docker compose</code> for our <code>local</code> and <code>ci</code> setup and keep
<a href="https://12factor.net/dev-prod-parity">parity between the environments</a>.</p>

<p><!-- generated -->
<a id='redis-memorystore-setup'> </a>
<!-- /generated --></p>

<h3>Redis Memorystore setup</h3>

<p>Please refer to <a href="/blog/gcp-redis-memorystore-instances-create-connect-delete/">How to use GCP Redis Memorystore instances - from creation over connection to deletion</a>
for a general introduction into Redis Memorystore.</p>

<p>For this tutorial, we will create a <code>redis</code> instance with the following settings:</p>

<ul>
<li>1 GiB RAM</li>
<li>Private Service Access / private IP</li>
<li>Enabled <code>AUTH</code></li>
</ul>

<p>I adapted the <code>gcloud</code> setup from
<a href="/blog/gcp-redis-memorystore-instances-create-connect-delete/#using-the-gcloud-cli">How to use GCP Redis Memorystore instances: "Using the <code>gcloud</code> CLI</a>
to create the script <code>.infrastructure/setup-redis.sh</code> that will
create a <code>redis</code> instance and uses the following commands</p>

<pre><code class="language-bash">project_id=$1
redis_instance_name=$2
region=us-central1
size=1 # in GiB
network="default"
version="redis_6_x"
private_vpc_range_name="google-managed-services-vpc-allocation"

gcloud redis instances create "${redis_instance_name}" \
      --size="${size}" \
      --region="${region}" \
      --network="${network}" \
      --redis-version="${version}" \
      --connect-mode=private-service-access \
      --reserved-ip-range="${private_vpc_range_name}" \
      --enable-auth \
      -q

</code></pre>

<p>A corresponding <code>make infrastructure-setup-redis</code> target is defined in 
<code>.make/04-00-infrastructure.mk</code></p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-redis
infrastructure-setup-redis: ## Set the redis instance up
    bash .infrastructure/setup-redis.sh $(GCP_PROJECT_ID) $(VM_NAME_REDIS) $(ARGS)
</code></pre>

<p><strong>CAUTION</strong>: We use <code>redis_6_x</code> as version for the instance and should ensure to keep this in
sync with the value of the <code>REDIS_VERSION</code> variable in the
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker-env-file-and-required-env-variables"><code>.docker/.env</code> file</a> (see also
<a href="/blog/structuring-the-docker-setup-for-php-projects/#env-example-and-docker-compose-yml">Structuring the Docker setup for PHP Projects <code>.env.example</code> and <code>docker-compose.yml</code></a>),
so that we use the same version in the <code>docker compose</code> for our <code>local</code> and <code>ci</code> setup and keep
<a href="https://12factor.net/dev-prod-parity">parity between the environments</a>.</p>

<p>The <a href="/blog/gcp-redis-memorystore-instances-create-connect-delete/#redis-auth-and-in-transit-encryption"><code>AUTH</code> string is created automatically</a>,
i.e. we must retrieve it after the creation and update the value of the <code>REDIS_PASSWORD</code> 
variable in the production <code>.env</code> file of the application at 
<a href="/blog/deploy-docker-compose-php-gcp-poc/#the-secrets-directory"><code>.secrets/prod/app.env</code></a>. I 
have created a corresponding <code>make</code> target in <code>.make/03-00-gcp.mk</code></p>

<pre><code class="language-makefile">.PHONY: gcp-get-redis-auth
gcp-get-redis-auth: ## Get the AUTH string of the Redis service
    gcloud redis instances get-auth-string $(VM_NAME_REDIS) --project=$(GCP_PROJECT_ID) --region=$(GCP_REGION)
</code></pre>

<p>Please note, that you need to 
<a href="">activate the master service account</a> 
first, because retrieving the 
<code>AUTH</code> string required the permission <code>redis.instances.getAuthString</code> that is by default only 
available in role 
<a href="https://cloud.google.com/iam/docs/understanding-roles#redis.admin">roles/redis.admin</a>.</p>

<p><!-- generated -->
<a id='set-up-the-vms-that-run-docker-containers'> </a>
<!-- /generated --></p>

<h3>Set up the VMs that run <code>docker</code> containers</h3>

<p>We'll stick mostly with the same VM configuration as outlined in my
<a href="/blog/gcp-compute-instance-vm-docker/#create-a-compute-instance-vm">Run Docker on GCP Compute Instance VMs: Create a Compute Instance VM</a>,
though we need to ensure that <strong>the VMs for the application containers <code>application</code>, <code>php-fpm</code> and 
<code>php-workers</code> are <em>not</em> getting a public IP</strong>. This is achieved by adding the 
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--address"><code>--no-address</code></a>
flag.</p>

<p><em>Note</em>: Adding the flag seemed to have no effect at first. This was because I initially 
used the 
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--network-interface"><code>--network-interface</code></a>
option to define all the network settings "as a single string". When this option is provided, 
the <code>--no-address</code> has no effect, because <code>--network-interface</code> takes precedence and the default 
value here is <code>address</code>, i.e. "provide a public IP address".</p>

<p>In other words: This doesn't work as expected</p>

<pre><code class="language-text">$ gcloud compute instances create test --zone="us-central1-a" --machine-type="f1-micro" \
        --network-interface=network=default \
        --no-address
Created [https://www.googleapis.com/compute/v1/projects/ay-mit-mct-atmo-gcp-mig-temp-c/zones/us-central1-a/instances/test].
NAME  ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP   STATUS
test  us-central1-a  f1-micro                   10.128.0.6   34.170.88.99  RUNNING

# ==&gt; Note the "EXTERNAL_IP"
</code></pre>

<p>This does:</p>

<pre><code class="language-text"><br />$ gcloud compute instances create test --zone="us-central1-a" --machine-type="f1-micro" \
        --network=default \
        --no-address
Created [https://www.googleapis.com/compute/v1/projects/ay-mit-mct-atmo-gcp-mig-temp-c/zones/us-central1-a/instances/test].
NAME  ZONE           MACHINE_TYPE  PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP  STATUS
test  us-central1-a  f1-micro                   10.128.0.7                RUNNING

# ==&gt; Note that no "EXTERNAL_IP" is given
</code></pre>

<p>In addition, the <code>http-server</code> tag responsible for 
<a href="/blog/gcp-compute-instance-vm-docker/#firewall-and-networks-tags">assigning the firewall rule to allow http traffic</a>
is also not necessary and must be omitted.</p>

<p>I've separated the script explained under 
<a href="/blog/gcp-compute-instance-vm-docker/#putting-it-all-together">Run Docker on GCP Compute Instance VMs: Putting it all together</a>
in the "general" GCP setup at <code>.infrastructure/setup-gcp.sh</code> and a script dedicated for creating 
a Compute Instance VM at <code>.infrastructure/setup-vm.sh</code> via the following commands</p>

<pre><code class="language-bash">project_id=$1
vm_name=$2
enable_public_access=$3
vm_zone=us-central1-a
master_service_account_key_location=./gcp-master-service-account-key.json
deployment_service_account_id=deployment
deployment_service_account_mail="${deployment_service_account_id}@${project_id}.iam.gserviceaccount.com"
network="default"

# By default, VMs should not get an external IP address
# @see https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--no-address
args_for_public_access="--no-address"
if [ -n "$enable_public_access" ]
then
  # The only exception is the nginx image - that should also be available via http / port 80
  args_for_public_access="--tags=http-server"
fi

gcloud compute instances create "${vm_name}" \
    --project="${project_id}" \
    --zone="${vm_zone}" \
    --machine-type=e2-micro \
    --network="${network}" \
    --subnet=default \
    --network-tier=PREMIUM \
    --no-restart-on-failure \
    --maintenance-policy=MIGRATE \
    --provisioning-model=STANDARD \
    --service-account="${deployment_service_account_mail}" \
    --scopes=https://www.googleapis.com/auth/cloud-platform \
    --create-disk=auto-delete=yes,boot=yes,device-name="${vm_name}",image=projects/debian-cloud/global/images/debian-11-bullseye-v20220822,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 $args_for_public_access
</code></pre>

<p><em>Note</em>: If the script is invoked with a 3rd argument, we'll assume that it's for the <code>nginx</code> 
service so that the <code>--no-address</code> flag is <em>omitted</em> and the <code>--tags=http-server</code> is <em>added</em>.</p>

<p>A corresponding "generic" <code>make infrastructure-setup-vm</code> target is defined in 
<code>.make/04-00-infrastructure.mk</code></p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-vm
infrastructure-setup-vm: ## Setup the VM specified via VM_NAME. Usage: make infrastructure-setup-vm VM_NAME=php-worker ARGS=""
    @$(if $(VM_NAME),,$(error "VM_NAME is undefined"))
    bash .infrastructure/setup-vm.sh $(GCP_PROJECT_ID) $(VM_NAME) $(ARGS)
</code></pre>

<p>In addition, there are dedicated targets for each service, e.g.</p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-vm-php-worker
infrastructure-setup-vm-php-worker:
    "$(MAKE)" --no-print-directory infrastructure-setup-vm VM_NAME=$(VM_NAME_PHP_WORKER)

.PHONY: infrastructure-setup-vm-nginx
infrastructure-setup-vm-nginx:
    "$(MAKE)" --no-print-directory infrastructure-setup-vm VM_NAME=$(VM_NAME_NGINX) ARGS="enable_public_access"
</code></pre>

<p>as well as a "combined" target to run the setup in parallel. See also section
<a href="#use-make-to-execute-infrastructure-and-deployment-targets-in-parallel">Use <code>make</code> to execute infrastructure and deployment targets in parallel</a>.</p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-all-vms
infrastructure-setup-all-vms: ## Setup all VMs
    @printf "$(YELLOW)The setup runs in parallel but the output will only be visible once a process is fully finished (can take a couple of minutes)$(NO_COLOR)\n"
    "$(MAKE)" -j --output-sync=target   infrastructure-setup-vm-application \
                                        infrastructure-setup-vm-php-fpm \
                                        infrastructure-setup-vm-php-worker \
                                        infrastructure-setup-vm-nginx \
                                        infrastructure-setup-redis \
                                        infrastructure-setup-mysql
</code></pre>

<p>See section <a href="#add-additional-make-variables">Add additional <code>make</code> variables</a> for the definition 
of variables like <code>$(VM_NAME_PHP_WORKER)</code> and <code>$(VM_NAME_NGINX)</code>.</p>

<p><!-- generated -->
<a id='provisioning-the-vms'> </a>
<!-- /generated --></p>

<h4>Provisioning the VMs</h4>

<p>The original provisioning script explained in
<a href="/blog/gcp-compute-instance-vm-docker/#provisioning">Run Docker on GCP Compute Instance VMs: Provisioning</a>
located at <code>.infrastructure/scripts/provision.sh</code> stays almost as before, though we were able to 
remove the <code>docker-compose-plugin</code> dependency as we won't need <code>compose</code> any longer on the 
production VMs.</p>

<p>In addition, I added some more <code>make</code> targets in <code>.make/04-00-infrastructure.mk</code> to make the 
provisioning easier</p>

<pre><code class="language-makefile">.PHONY: infrastructure-provision-vm
infrastructure-provision-vm: ## Provision the VM specified via VM_NAME. Usage: make infrastructure-provision-vm VM_NAME=php-worker ARGS=""
    @$(if $(VM_NAME),,$(error "VM_NAME is undefined"))
    bash .infrastructure/provision-vm.sh $(GCP_PROJECT_ID) $(VM_NAME) $(ARGS) \
        &amp;&amp; printf "$(GREEN)Success at provisioning $(VM_NAME)$(NO_COLOR)\n" \
        || printf "$(RED)Failed provisioning $(VM_NAME)$(NO_COLOR)\n"

.PHONY: infrastructure-provision-vm-nginx
infrastructure-provision-vm-nginx:
    "$(MAKE)" --no-print-directory infrastructure-provision-vm VM_NAME=$(VM_NAME_NGINX)

# ...

.PHONY: infrastructure-provision-all
infrastructure-provision-all: ## Provision all VMs
    @printf "$(YELLOW)The provisioning runs in parallel but the output will only be visible once a process is fully finished (can take a couple of minutes)$(NO_COLOR)\n"
    "$(MAKE)" -j --output-sync=target   infrastructure-provision-vm-application \
                                        infrastructure-provision-vm-php-fpm \
                                        infrastructure-provision-vm-php-worker \
                                        infrastructure-provision-vm-nginx
</code></pre>

<p><!-- generated -->
<a id='mapping-service-names-to-vm-names'> </a>
<!-- /generated --></p>

<h3>Mapping service names to VM names</h3>

<p>In this tutorial I'm using the terms "service name" and "VM name" quite a lot. To avoid 
confusion, here is how I think about them:</p>

<ul>
<li><p><strong>service name</strong></p>

<ul>
<li>is used in the sense of a <a href="https://docs.docker.com/compose/compose-file/#services-top-level-element"><code>docker compose</code> service</a></li>
</ul>

<blockquote>
  <p>A Service is an abstract definition of a computing resource within an application which can 
  be scaled/replaced independently from other components. Services are backed by a set of 
  containers, run by the platform according to replication requirements and placement constraints.</p>
</blockquote>

<ul>
<li>our <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker">application has 6 services</a> 
(<code>nginx</code>, <code>php-fpm</code>, <code>application</code>, <code>php-worker</code>, <code>mysql</code> and <code>redis</code>) and so far each 
service was defined as an individual <code>docker</code> image and ran as a single <code>docker</code> container</li>
<li>starting from this tutorial, <code>mysql</code> and <code>redis</code> are no longer used via <code>docker</code> images</li>
<li>services are a "logical" component - not an infrastructural one</li>
</ul></li>
<li><strong>VM name</strong>

<ul>
<li>a "service" can be run on a "VM"</li>
<li>is a unique identifier for a Virtual Machine on GCP and is often required in <code>gcloud</code> 
commands to identify the instance for a command</li>
<li>VMs are infrastructure components</li>
<li>this includes the "Compute Engine" instances for our application services as well as the
MySQL Cloud SQL instance for our <code>mysql</code> service and the Redis Memory Store instance for our
<code>redis</code> service</li>
</ul></li>
</ul>

<p><a href="/img/create-production-infrastructure-docker-php-app-gcp/vm-name-service-name.PNG"><img src="/img/create-production-infrastructure-docker-php-app-gcp/vm-name-service-name.PNG" alt="VM names and service names" /></a></p>

<p>In this tutorial we use "one VM per service" and can thus use a 1-to-1 mapping from service name 
to VM name. As a convention, we use almost the same name, i.e. the <code>php-fpm</code> service runs on the 
Compute Engine VM instance with the name <code>php-fpm-vm</code>. Using the <code>-vm</code> suffix avoids confusion 
and ensures that we don't create an unintended coupling between the service name and the VM name.</p>

<p>Using the exact same name (service name = <code>php-fpm</code> and VM name = <code>php-fpm</code>) has lead to 
problems for me in the past, because 
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete#delete-a-mysql-cloud-sql-instance">the name of a MySQL Cloud SQL instance used to be blocked even after deletion for one week</a>,
so it might not even be possible to use that exact name.</p>

<p>I have defined this mapping as a variable in <code>.make/variables.env</code> as follows:</p>

<pre><code class="language-dotenv"># must match the names used in the docker-composer.yml files
DOCKER_SERVICE_NAME_NGINX:=nginx
DOCKER_SERVICE_NAME_PHP_BASE:=php-base
DOCKER_SERVICE_NAME_PHP_FPM:=php-fpm
DOCKER_SERVICE_NAME_PHP_WORKER:=php-worker
DOCKER_SERVICE_NAME_APPLICATION:=application
DOCKER_SERVICE_NAME_MYSQL:=mysql
DOCKER_SERVICE_NAME_REDIS:=redis
# VM / instance names
VM_NAME_APPLICATION=$(DOCKER_SERVICE_NAME_APPLICATION)-vm
VM_NAME_PHP_FPM=$(DOCKER_SERVICE_NAME_PHP_FPM)-vm
VM_NAME_PHP_WORKER=$(DOCKER_SERVICE_NAME_PHP_WORKER)-vm
VM_NAME_NGINX=$(DOCKER_SERVICE_NAME_NGINX)-vm
VM_NAME_MYSQL=$(DOCKER_SERVICE_NAME_MYSQL)-vm
VM_NAME_REDIS=$(DOCKER_SERVICE_NAME_REDIS)-vm
# Helpers
ALL_VM_SERVICE_NAMES=$(VM_NAME_APPLICATION):$(DOCKER_SERVICE_NAME_APPLICATION) $(VM_NAME_PHP_FPM):$(DOCKER_SERVICE_NAME_PHP_FPM) $(VM_NAME_PHP_WORKER):$(DOCKER_SERVICE_NAME_PHP_WORKER) $(VM_NAME_NGINX):$(DOCKER_SERVICE_NAME_NGINX)
</code></pre>

<p>The <code>ALL_VM_SERVICE_NAMES</code> variables is used for instance for
<a href="/blog/deploy-dockerized-php-app-production#the-service-ips-file-map-service-names-to-ips">Retrieving all IP addresses if the VMs to create the <code>service-ips</code> file</a>.</p>

<p><!-- generated -->
<a id='appendix-changes-in-the-codebase'> </a>
<!-- /generated --></p>

<h2>Appendix: Changes in the codebase</h2>

<p>This section is mostly relevant if you have been following the previous tutorials. It explains 
some changes that have been introduced for this part.</p>

<p><!-- generated -->
<a id='add-a-make-dev-init-target'> </a>
<!-- /generated --></p>

<h3>Add a <code>make dev-init</code> target</h3>

<p>A new <code>dev-init</code> target was added in a new sub makefile at <code>.make/00-00-development-setup.mk</code>. 
It simplifies the setup of the tutorial repository when it has been freshly cloned.</p>

<p>I realized that there are quite some things to keep  in mind in this case (e.g. set up various 
<code>.env</code> files, decrypt the secrets, install composer dependencies, etc.). I believe this can lead 
to a bad experience if you start with "this" part of the tutorial, as a lot of those setup 
things are done in previous parts.</p>

<p><!-- generated -->
<a id='update-the-gcloud-targets'> </a>
<!-- /generated --></p>

<h3>Update the <code>gcloud</code> targets</h3>

<p>So far, the targets in <code>.make/03-00-gcp.mk</code> could assume a single <code>GCP_VM_NAME</code> variable as 
there was only one Compute Instance VM. Now, we have 
<a href="#set-up-the-vms-that-run-docker-containers">one VM per service</a> and thus need to pass the 
<code>VM_NAME</code> as a required argument to most VM related targets (i.e. <code>GCP_VM_NAME</code> was replaced by
<code>VM_NAME</code>).</p>

<p>Example: Logging into a VM via <code>gcp-ssh-login</code> now requires the <code>VM_NAME</code>, because 
we need to define in which exact VM we want to log in</p>

<pre><code class="language-makefile">.PHONY: gcp-ssh-login
gcp-ssh-login: validate-gcp-variables ## Log into a VM via IAP tunnel
    @$(if $(VM_NAME),,$(error "VM_NAME is undefined"))
    gcloud compute ssh $(VM_NAME) --project $(GCP_PROJECT_ID) --zone $(GCP_ZONE) --tunnel-through-iap

# Example to log into the php-fpm VM: 
#   make gcp-ssh-login VM_NAME=php-fpm-vm
</code></pre>

<p>In addition, I have added a dedicated target to activate the mater service account, as it is 
required for some actions (like retrieving the redis <code>AUTH</code> string - see
<a href="#redis-memorystore-setup">Redis Memorystory Setup</a>):</p>

<pre><code class="language-makefile">.PHONY: gcp-init
gcp-init: validate-gcp-variables ## Initialize the `gcloud` cli and authenticate docker with the keyfile defined via SERVICE_ACCOUNT_KEY_FILE.
    @$(if $(SERVICE_ACCOUNT_KEY_FILE),,$(error "SERVICE_ACCOUNT_KEY_FILE is undefined"))
    gcloud auth activate-service-account --key-file="$(SERVICE_ACCOUNT_KEY_FILE)" --project="$(GCP_PROJECT_ID)"

.PHONY: gcp-init-deployment-account
gcp-init-deployment-account: validate-gcp-variables ## Initialize the `gcloud` cli with the deployment service account 
    @$(if $(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE),,$(error "GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE is undefined"))
    "$(MAKE)" gcp-init SERVICE_ACCOUNT_KEY_FILE=$(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE)
    cat "$(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE)" | docker login -u _json_key --password-stdin https://gcr.io

.PHONY: gcp-init-master-account
gcp-init-master-account: validate-gcp-variables ## Initialize the `gcloud` cli with the master service account 
    @$(if $(GCP_MASTER_SERVICE_ACCOUNT_KEY_FILE),,$(error "GCP_MASTER_SERVICE_ACCOUNT_KEY_FILE is undefined"))
    "$(MAKE)" gcp-init SERVICE_ACCOUNT_KEY_FILE=$(GCP_MASTER_SERVICE_ACCOUNT_KEY_FILE)
</code></pre>

<p>As part of this change, the variable <code>GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY</code> was renamed to
<code>GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE</code> and <code>GCP_MASTER_SERVICE_ACCOUNT_KEY_FILE</code> was added.</p>

<p><!-- generated -->
<a id='add-additional-make-variables'> </a>
<!-- /generated --></p>

<h3>Add additional <code>make</code> variables</h3>

<p>The following variables have been added:</p>

<pre><code class="language-dotenv">GCP_REGION=us-central1
</code></pre>

<p>The region is required to create</p>

<ul>
<li>the <a href="#routers-and-nats">Cloud Router and Cloud NAT gateway</a></li>
<li>the <a href="#cloud-sql-for-mysql-setup">MySQL Cloud SQL instance</a></li>
<li>the <a href="#redis-memorystore-setup">Redis Memoerystore instance</a> and 
<a href="/blog/deploy-dockerized-php-app-production/#retrieving-ip-addresses">retrieve its private IP address</a></li>
</ul>

<pre><code class="language-dotenv">GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY_FILE
GCP_MASTER_SERVICE_ACCOUNT_KEY_FILE
</code></pre>

<p>See previous section <a href="#update-the-gcloud-targets">Update the <code>gcloud</code> targets</a>.</p>

<pre><code class="language-dotenv">DOCKER_SERVICE_NAME_...
VM_NAME_...
ALL_VM_SERVICE_NAMES
</code></pre>

<p>See section <a href="#mapping-service-names-to-vm-names">Mapping service names to VM names</a>.</p>

<p><!-- generated -->
<a id='use-make-to-execute-infrastructure-and-deployment-targets-in-parallel'> </a>
<!-- /generated --></p>

<h3>Use <code>make</code> to execute infrastructure and deployment targets in parallel</h3>

<p>We have already learned about the <strong>capability of <code>make</code> to run targets in parallel</strong> with the <code>-j</code> 
flag in 
<a href="/blog/php-qa-tools-make-docker/#parallel-execution-and-a-helper-target">Set up PHP QA tools: Parallel execution and a helper target</a>:</p>

<pre><code class="language-makefile">.PHONY: foo
foo: 
    "$(MAKE)" -j target-1 target-2
</code></pre>

<p>This is particularly helpful when <strong>dealing with I/O heavy targets</strong>, e.g. when making API calls.
This is great, because we are making <em>a lot</em> of those, e.g. during the 
<a href="#set-up-the-vms-that-run-docker-containers">creation of the infrastructure</a>
but also during the 
<a href="/blog/deploy-dockerized-php-app-production/#run-the-deployment-script-for-each-service">deployment</a> 
of the application. Unfortunately, it's not possible to pass individual arguments per target,
i.e. in the following example</p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-all
infrastructure-setup-all: ## Setup all VMs
    "$(MAKE)" -j --output-sync=target infrastructure-setup-vm VM_NAME=application-vm \   
                                      infrastructure-setup-vm VM_NAME=php-fpm-vm
</code></pre>

<p>the value of <code>VM_NAME</code> would always be <code>php-fpm-vm</code>. Thus, <strong>we must define each target that 
should run in parallel individually</strong> like this:</p>

<pre><code class="language-makefile">.PHONY: infrastructure-setup-vm
infrastructure-setup-vm: ## Setup the VM specified via VM_NAME. Usage: make infrastructure-setup-vm VM_NAME=php-worker ARGS=""
    bash .infrastructure/setup-vm.sh $(GCP_PROJECT_ID) $(VM_NAME) $(ARGS)

.PHONY: infrastructure-setup-vm-application
infrastructure-setup-vm-application:
    "$(MAKE)" --no-print-directory infrastructure-setup-vm VM_NAME=$(VM_NAME_APPLICATION)

.PHONY: infrastructure-setup-vm-php-fpm
infrastructure-setup-vm-php-fpm:
    "$(MAKE)" --no-print-directory infrastructure-setup-vm VM_NAME=$(VM_NAME_PHP_FPM)

.PHONY: infrastructure-setup-all
infrastructure-setup-all: ## Setup all VMs
    "$(MAKE)" -j --output-sync=target infrastructure-setup-vm-application \
                                      infrastructure-setup-vm-php-fpm
</code></pre>

<p>This introduces some typing overhead, but I didn't find a better way to do this, yet. 
In theory, we could also use 
<a href="https://www.cyberciti.biz/faq/how-to-run-command-or-code-in-parallel-in-bash-shell-under-linux-or-unix/">other ways to run the targets in parallel</a>,
e.g. by running the process in the background via <code>&amp;</code></p>

<pre><code class="language-makefile">infrastructure-setup-all: ## Provision all VMs
    for vm_name in $(VM_NAME_APPLICATION) $(VM_NAME_PHP_FPM); do \
        $$(make infrastructure-provision-vm VM_NAME="$$vm_name") &amp; \
    done; \
    wait; \
    echo "ALL DONE"
</code></pre>

<p>But: This doesn't give us any way to 
<a href="https://www.gnu.org/software/make/manual/html_node/Parallel-Output.html">synchronize the output as we can do with <code>make</code> via <code>--output-sync=target</code></a>
so that the output quickly becomes quite messy. Using 
<a href="https://www.gnu.org/software/parallel/">GNU <code>parallel</code></a> might be an option, but it doesn't come 
natively e.g. on Windows and would be a dependency that needed to be installed by all developers
(which we try to avoid as much as possible - see 
<a href="/blog/git-secret-encrypt-repository-docker/#local-git-secret-and-gpg-setup">Use git-secret to encrypt secrets: Local <code>git-secret</code> and <code>gpg</code> setup</a>).</p>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You should now be ready to create a production ready infrastructure on GCP 
including multiple VMs and managed services for <code>mysql</code> and <code>redis</code>.</p>

<p>In the next part of this tutorial, we will 
<a href="/blog/deploy-dockerized-php-app-production/">deploy a dockerized PHP application "to production" via <code>docker</code> (without <code>compose</code>) on multiple VMs</a>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Mon, 24 Apr 2023 06:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/create-production-infrastructure-docker-php-app-gcp/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/create-production-infrastructure-docker-php-app-gcp/</guid>
            </item>
                    <item>
                <title>Using GCP Redis Memorystore instances (create/connect/delete)</title>
                <description><![CDATA[<p>In this blog post I'll summarize my experience with <strong>GCP Redis Memorystore instances</strong>. Memorystore 
is the managed in-memory datastore solution from Google Cloud Platform and was mentioned in 
<a href="/blog/deploy-docker-compose-php-gcp-poc/#introduction">Deploy dockerized PHP Apps to production on GCP via docker compose as a POC</a>
as the "better" way to deal with in-memory datastores in a dockerized application (compared to 
running an in-memory datastore via <code>docker</code>).</p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>What will you learn?</strong>
  </div>
  <div class="panel-body bg-info">
    I'll explain the basic steps to <strong>create a fresh Redis instance</strong>, 
    <strong>show different ways to connect to it</strong> (locally "from your laptop" via SSH tunnel
    and from a VM within GCP) and finally <strong>how to delete the instance</strong>. Every process 
    is done through the Cloud Console UI and <strong>recorded as a short video</strong> as a visual 
    aid. As in the <a href="/blog/gcp-compute-instance-vm-docker/">GCP "primer" tutorial</a>, this 
    article ends with the commands to achieve the same things also via the
    <code>gcloud</code> CLI.
  </div>
</div>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#setup-memorystore">Setup Memorystore</a></li>
<li><a href="#create-a-new-redis-instance">Create a new <code>redis</code> instance</a>

<ul>
<li><a href="#redis-auth-and-in-transit-encryption"><code>redis</code> AUTH and in-transit encryption</a></li>
</ul></li>
<li><a href="#connecting-to-a-redis-memorystore-instance">Connecting to a Redis Memorystore instance</a>

<ul>
<li><a href="#redis-memorystore-offers-private-ip-connectivity-only">Redis Memorystore offers private IP connectivity only</a></li>
<li><a href="#connecting-to-the-redis-instance-from-a-compute-instance-vm">Connecting to the <code>redis</code> instance from a Compute Instance VM</a></li>
<li><a href="#connecting-to-the-redis-instance-via-ssh-tunnel">Connecting to the <code>redis</code> instance via SSH tunnel</a></li>
</ul></li>
<li><a href="#delete-a-redis-memorystore-instance">Delete a Redis Memorystore instance</a></li>
<li><a href="#using-the-gcloud-cli">Using the <code>gcloud</code> CLI</a>

<ul>
<li><a href="#activate-the-service-account">Activate the service account</a></li>
<li><a href="#enable-the-necessary-apis">Enable the necessary APIs</a></li>
<li><a href="#create-an-ip-range-allocation">Create an IP range allocation</a></li>
<li><a href="#create-the-vpc-peering-with-servicenetworking-googleapis-com">Create the VPC peering with <code>servicenetworking.googleapis.com</code></a></li>
<li><a href="#create-the-redis-instance">Create the <code>redis</code> instance</a></li>
</ul></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

<p><!-- generated -->
<a id='setup-memorystore'> </a>
<!-- /generated --></p>

<h2>Setup Memorystore</h2>

<p><a href="/img/gcp-redis-memorystore-instances-create-connect-delete/cloud-console-cloud-memorystore-ui.PNG"><img src="/img/gcp-redis-memorystore-instances-create-connect-delete/cloud-console-cloud-memorystore-ui.PNG" alt="GCP Cloud Console Memorystore UI" /></a></p>

<p>The managed solution for in-memory datastores from GCP is called 
<a href="https://cloud.google.com/memorystore">Memorystore</a> and provides multiple datastore technologies - 
including <a href="https://cloud.google.com/memorystore/docs/redis"><code>redis</code></a>. In the Cloud Console UI it is 
managed via the <a href="https://console.cloud.google.com/memorystore">Memorystore UI</a> that allows us to 
create and manage instances.</p>

<p><!-- generated -->
<a id='create-a-new-redis-instance'> </a>
<!-- /generated --></p>

<h2>Create a new <code>redis</code> instance</h2>

<p>To get started, we need to enable the following APIs:</p>

<ul>
<li><a href="https://console.cloud.google.com/apis/library/compute.googleapis.com">Compute Engine API</a></li>
<li><a href="https://console.cloud.google.com/marketplace/product/google/redis.googleapis.com">Google Cloud Memorystore for Redis API</a></li>
</ul>

<p>Creating a new instance from the 
<a href="https://console.cloud.google.com/memorystore/redis/locations/-/instances/new">Create a redis instance UI</a>
is pretty straight forward and well documented in the 
<a href="https://cloud.google.com/memorystore/docs/redis/creating-managing-instances">GCP Redis Guide: Creating and managing Redis instances</a>.</p>

<p>We'll use the following settings:</p>

<ul>
<li><code>Tier Selection</code>: For testing purposes, I recommend choosing the "Basic" option (this will 
also disable the "Read Replicas")</li>
<li><code>Capacity</code>: Enter "1"</li>
<li><code>Set up connection &gt; Network</code>: Select the network that the VMs are located in - <code>default</code> in 
my case</li>
<li><code>Additional Configurations &gt; Connections</code>: Select option "Private service access" here as it's 
the recommended approach.

<ul>
<li><strong>CAUTION</strong>: In order to use "Private service access" as connectivity mode, we need to 
create a <strong>reserved IP allocation</strong> and a VPC peering with the <strong>Google Cloud Platform
Service Producer</strong> first. The process is exactly the same as I've explained in
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#connecting-to-the-mysql-instance-via-private-ip">my MySQL Cloud SQL article for connecting via private IP</a>.
Please make sure to pay close attention to section
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#concrete-steps-to-connect-via-private-ip">Concrete steps to connect via private IP</a>
and especially the video, as it shows the necessary steps to enable the "Private service access"
for Memorystore!</li>
</ul></li>
<li><code>Security &gt; Enable AUTH</code>: Enable the checkbox

<ul>
<li><em>Note</em>: This will auto-enable the checkbox "Enable in-transit encryption" though 
<em>I recommend disabling that checkbox</em> (see section <a href="#redis-auth-and-in-transit-encryption"><code>redis</code> AUTH and in-transit encryption</a>)</li>
</ul></li>
<li><code>Configuration &gt; Version</code>: Select "6.x" (which is currently [2023-04-17] the latest version)</li>
</ul>

<p>FYI: Unfortunately, there is no <code>"EQUIVALENT COMMAND LINE"</code> button as it was the case when 
<a href="/blog/gcp-compute-instance-vm-docker/#create-a-compute-instance-vm">creating the Compute Instance</a> -
which would have come in handy for 
<a href="#create-the-redis-instance">creating the instance via <code>gcloud</code> cli</a>.</p>

<p>Once everything is configured, click the <code>"Create Instance"</code> button. The actual creation can 
take quite some time (I've experienced times from a couple of minutes to ~15 min).</p>

<video controls>
  <source src="/img/gcp-redis-memorystore-instances-create-connect-delete/create-gcp-redis-memorystore-instance.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='redis-auth-and-in-transit-encryption'> </a>
<!-- /generated --></p>

<h3><code>redis</code> AUTH and in-transit encryption</h3>

<p>During instance creation we activated the <code>AUTH</code> feature but disabled the <code>in-transit 
encryption</code> on purpose. Since this goes against GCP's own recommendation</p>

<blockquote>
  <p>When AUTH is enabled, in-transit encryption is recommended so credentials are confidential when 
  transmitted.</p>
</blockquote>

<p>I'd like to provide some thoughts on my reasoning:</p>

<p>According to the 
<a href="https://cloud.google.com/memorystore/docs/redis/auth-overview#security_and_privacy">GCP Redis Guide: AUTH feature overview > Security and privacy</a>,
<code>AUTH</code> is not meant to be used as a security measure:</p>

<blockquote>
  <p>AUTH helps you ensure that known entities in your organization do not unintentionally access and
  modify your Redis instance. AUTH does not provide security during data transportation. Also, 
  AUTH does not protect your instance against any malicious entities that have access to your 
  VPC network.</p>
</blockquote>

<p>However, it is still certainly "better than not having <code>AUTH</code> at all". Now, <code>in-transit encryption</code>
comes at a cost: All communication between <code>redis</code> and the VMs would now be encrypted. Even 
though this sounds good in theory, it has 
<a href="https://cloud.google.com/memorystore/docs/redis/in-transit-encryption?hl=en#performance_impact_of_enabling_in-transit_encryption">a negative impact on performance</a>
as well as on <a href="https://cloud.google.com/memorystore/docs/redis/in-transit-encryption?hl=en#connection_limits_for_in-transit_encryption">the maximum number of possible connections</a>.</p>

<p>Thus, I'll go with an in-between solution: Enable <code>AUTH</code> but disable <code>in-transit encryption</code></p>

<p>Here are some additional things I learned about <code>AUTH</code>:</p>

<ul>
<li>you cannot define a custom <code>AUTH</code> string, but it will always be an auto-generated UUID</li>
<li>the <code>AUTH</code> string will be shown in plain text in the management UI of a <code>redis</code> instance</li>
<li><p>you can get the <code>AUTH</code> string via
<a href="https://cloud.google.com/sdk/gcloud/reference/redis/instances/get-auth-string"><code>gcloud redis instances get-auth-string</code></a></p>

<pre><code class="language-text">$ gcloud redis instances get-auth-string redis-instance --region=us-central1
authString: 568d20ec-b0c2-40a9-908d-a5d6b6717a9c
</code></pre></li>
</ul>

<p>See also the GCP Redis Guides on</p>

<ul>
<li><a href="https://cloud.google.com/memorystore/docs/redis/managing-auth">Managing Redis AUTH</a></li>
<li><a href="https://cloud.google.com/memorystore/docs/redis/auth-overview#auth_behavior">AUTH feature overview > AUTH behavior</a></li>
</ul>

<p><!-- generated -->
<a id='connecting-to-a-redis-memorystore-instance'> </a>
<!-- /generated --></p>

<h2>Connecting to a Redis Memorystore instance</h2>

<p>I'll explain 2 different ways of <strong>connecting to a Redis Memorystore instance</strong>:</p>

<ul>
<li>from a Compute Instance VM on GCP</li>
<li>"locally" from your laptop via SSH Tunnel</li>
</ul>

<p><small>Unfortunately, there is no "One-Click-Solution" like 
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#connecting-to-the-mysql-instance-with-cloud-shell-via-public-ip">accessing MySQL Cloud SQL instances via Cloud Shell</a>
available for Redis.</small></p>

<p>As always, GCP has also an extensive documentation on the various connection methods available 
at the
<a href="https://cloud.google.com/memorystore/docs/redis/connecting-redis-instance">GCP Redis Guide: Connecting to a Redis instance</a>.</p>

<p><!-- generated -->
<a id='redis-memorystore-offers-private-ip-connectivity-only'> </a>
<!-- /generated --></p>

<h3>Redis Memorystore offers private IP connectivity only</h3>

<p><strong>Redis Memorystore instances do not have a public IP address!</strong></p>

<blockquote>
  <p>Regardless of the connection mode, Memorystore for Redis always uses internal IP addresses to 
  provision Redis instances.</p>
</blockquote>

<p>(via <a href="https://cloud.google.com/memorystore/docs/redis/networking?hl=en">GCP Redis Guide: Networking</a>)</p>

<p>I.e. it's not possible to connect to an instance without being in the same VPC. This is 
different from e.g. 
<a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#connecting-to-the-mysql-instance-via-public-ip">MySQL Cloud SQL instances, that offer public IPs as an option</a>.</p>

<p>Instead, GCP offers two so-called "Connection modes" for private IP access:</p>

<ul>
<li>Direct peering</li>
<li>Private services access</li>
</ul>

<p>As <a href="https://cloud.google.com/memorystore/docs/redis/networking?hl=en#choosing_a_connection_mode"><strong>Private services access</strong> is the newer (and recommended) approach</a>,
we'll not cover <strong>Direct peering</strong> in more detail (even though it's a little easier to set up,
because GCP will create the necessary VPC peering automatically).</p>

<p><!-- generated -->
<a id='connecting-to-the-redis-instance-from-a-compute-instance-vm'> </a>
<!-- /generated --></p>

<h3>Connecting to the <code>redis</code> instance from a Compute Instance VM</h3>

<p>Once the <code>redis</code> instance is up and running, we can find its private IP address (and port) via its 
management UI at URL</p>

<pre><code class="language-text">https://console.cloud.google.com/memorystore/redis/locations/$region/instances/$instanceName/details/overview

# e.g. for an instance named 'redis-1' in region 'us-central1'
# https://console.cloud.google.com/memorystore/redis-1/locations/us-central1/instances/redis/details/overview
</code></pre>

<p><a href="/img/gcp-redis-memorystore-instances-create-connect-delete/redis-memorystore-private-ip-address.PNG"><img src="/img/gcp-redis-memorystore-instances-create-connect-delete/redis-memorystore-private-ip-address.PNG" alt="Redis Memorystore private IP in the Cloud Console UI" /></a></p>

<p>Or via <a href="https://cloud.google.com/sdk/gcloud/reference/redis/instances/describe"><code>gcloud redis instances describe</code></a></p>

<pre><code class="language-text">$ gcloud redis instances describe redis-instance --format="get(host)" --region=us-central1
10.111.1.3
</code></pre>

<p>To test the connectivity from a VM, perform the following steps:</p>

<ul>
<li><a href="/blog/gcp-compute-instance-vm-docker/#create-a-vm">create a Compute Instance VM</a> in the "default" network</li>
<li><a href="/blog/gcp-compute-instance-vm-docker/#login-via-ssh-from-the-gcp-ui">log into the VM via the UI</a></li>
<li><p><a href="https://stackoverflow.com/a/25909402">install the <code>redis-cli</code> client</a> via</p>

<pre><code class="language-bash">sudo apt-get install redis-tool -y
</code></pre></li>
<li><p>connect to the Redis Memorystore instance via</p>

<pre><code class="language-bash">redis-cli -h $privateIp
</code></pre>

<p>where <code>$privateIp</code> is the private IP of the <code>redis</code> instance</p></li>
<li>once connected:

<ul>
<li>type the command <code>AUTH</code> followed by the <code>AUTH</code> string of the instance to authenticate</li>
<li>type the command <code>PING</code></li>
<li>you should get <code>PONG</code> as response</li>
</ul></li>
</ul>

<video controls>
  <source src="/img/gcp-redis-memorystore-instances-create-connect-delete/connect-redis-memorystore-instance-via-compute-vm.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='connecting-to-the-redis-instance-via-ssh-tunnel'> </a>
<!-- /generated --></p>

<h3>Connecting to the <code>redis</code> instance via SSH tunnel</h3>

<p>The <a href="https://cloud.google.com/memorystore/docs/redis/connecting-redis-instance?hl=en#connecting_from_a_local_machine_with_port_forwarding">GCP Redis Guide: Connecting from a local machine with port forwarding</a> 
proposes an interesting approach to connect to a Redis Memorystore instance by using a Compute 
Instance VM as a jump host, i.e.</p>

<ul>
<li>create a VM that lives in the same VPC as the <code>redis</code> instance</li>
<li><p>create an SSH tunnel via <a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--ssh-flag"><code>gcloud comute ssh --ssh-flag</code></a></p>

<pre><code class="language-bash">gcloud compute ssh test-instance --zone=us-central1-a --ssh-flag="-N -L 6379:$privateRedisInstanceIp:6379"
</code></pre>

<ul>
<li>run this command on your local machine to forward your local port <code>6379</code> to port <code>6379</code> of 
the <code>redis</code> instance</li>
<li>the traffic flows over the Compute instance VM in this case</li>
</ul></li>
<li><p>on your local machine connect to the Redis Memorystore instance via</p>

<pre><code class="language-bash">redis-cli -h localhost
</code></pre></li>
</ul>

<video controls>
  <source src="/img/gcp-redis-memorystore-instances-create-connect-delete/connect-redis-memorystore-instance-via-ssh-tunnel.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='delete-a-redis-memorystore-instance'> </a>
<!-- /generated --></p>

<h2>Delete a Redis Memorystore instance</h2>

<p>To <strong>delete a Redis Memorystore instance</strong> simply navigate to its management UI and click the 
<code>"Delete"</code> button. Corresponding docs: 
<a href="https://cloud.google.com/memorystore/docs/redis/creating-managing-instances?hl=en#deleting_instances">GCP Redis Guide: Creating and managing Redis instances > Deleting instances</a></p>

<video controls>
  <source src="/img/gcp-redis-memorystore-instances-create-connect-delete/delete-redis-memorystore-instance.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='using-the-gcloud-cli'> </a>
<!-- /generated --></p>

<h2>Using the <code>gcloud</code> CLI</h2>

<p>Even though I like using the UI to "explore and understand" how things are working, the goal is 
always a more "unattended" approach, e.g. via the 
<a href="/blog/gcp-compute-instance-vm-docker#set-up-the-gcloud-cli-tool"><code>gcloud</code> cli</a>.</p>

<p>The following commands assume that you have created a master service account with owner 
permissions and activated it for <code>glcoud</code> with a default project. See also 
<a href="/blog/gcp-compute-instance-vm-docker#preconditions-project-and-owner-service-account">Preconditions: Project and Owner service account</a></p>

<video controls>
  <source src="/img/gcp-redis-memorystore-instances-create-connect-delete/create-gcp-redis-memorystore-instance-via-gcloud-cli.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='activate-the-service-account'> </a>
<!-- /generated --></p>

<h3>Activate the service account</h3>

<pre><code class="language-bash">project_id=pl-dofroscra-p
gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=${project_id}
</code></pre>

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

<h3>Enable the necessary APIs</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/services/enable"><code>gcloud services enable</code> reference</a></li>
</ul>

<pre><code class="language-bash">gcloud services enable \
  compute.googleapis.com \
  redis.googleapis.com \
  cloudresourcemanager.googleapis.com \
  servicenetworking.googleapis.com
</code></pre>

<p><code>cloudresourcemanager.googleapis.com</code> is necessary to create the IP range allocation in the 
next step.</p>

<p><!-- generated -->
<a id='create-an-ip-range-allocation'> </a>
<!-- /generated --></p>

<h3>Create an IP range allocation</h3>

<p>See <a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#create-an-ip-range-allocation">"Create an IP range allocation" in the MySQL Cloud SQL article</a></p>

<p><!-- generated -->
<a id='create-the-vpc-peering-with-servicenetworking-googleapis-com'> </a>
<!-- /generated --></p>

<h3>Create the VPC peering with <code>servicenetworking.googleapis.com</code></h3>

<p>See <a href="/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/#create-the-vpc-peering-with-servicenetworking-googleapis-com">"Create the VPC peering with <code>servicenetworking.googleapis.com</code>" in the MySQL Cloud SQL article</a></p>

<p><!-- generated -->
<a id='create-the-redis-instance'> </a>
<!-- /generated --></p>

<h3>Create the <code>redis</code> instance</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/redis/instances/create"><code>gcloud redis instances create</code> reference</a></li>
<li><a href="https://cloud.google.com/memorystore/docs/redis/creating-managing-instances#custom_ranges_with_private_services_access">GCP Redis Guide: Creating and managing Redis instances > Custom ranges with private services access</a></li>
</ul>

<pre><code class="language-bash">region=us-central1
redis_instance_name="redis"
size=1
network="default" # must be the same as for the VMs
version="redis_6_x" # see https://cloud.google.com/sdk/gcloud/reference/redis/instances/create#--redis-version
private_ip_range_name="internal-gcp-services"

gcloud redis instances create "${redis_instance_name}" \
      --size="${size}" \
      --region="${region}" \
      --network="${network}" \
      --redis-version="${version}" \
      --connect-mode=private-service-access \
      --reserved-ip-range="${private_ip_range_name}" \
      --enable-auth \
      -q 
</code></pre>

<ul>
<li>the <code>--reserved-ip-range</code> must be name of the range created in step
<a href="#create-an-ip-range-allocation">Create an IP range allocation</a></li>
<li><p>when using the <code>--enable-auth</code> flag, we need to include the <code>-q</code> flag as well, otherwise we 
are prompted for an additional confirmation:</p>

<pre><code class="language-text">AUTH prevents accidental access to the instance by requiring an AUTH string (automatically generated for you). AUTH credentials are not confidential when transmitted or intended to protect against malicious actors.

Do you want to proceed? (Y/n)?
</code></pre></li>
<li><p>the <code>--region</code> is required on any subsequent requests to identify the instance, i.e. the 
instance name is not enough. If the region is not provided, an error is shown:</p>

<pre><code class="language-text">$ gcloud redis instances describe redis-instance
ERROR: (gcloud.redis.instances.describe) Error parsing [instance].
The [instance] resource is not properly specified.
Failed to find attribute [region]. The attribute can be set in the following ways:
- provide the argument `--region` on the command line
- set the property `redis/region`
</code></pre></li>
</ul>

<h3>Retrieve the private IP</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/redis/instances/describe"><code>gcloud redis instances create</code> reference</a></li>
</ul>

<pre><code class="language-bash">gcloud redis instances describe "${redis_instance_name}" \
    --format="get(host)" \
    --region="${region}"
</code></pre>

<h3>Retrieve the <code>AUTH</code> string</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/redis/instances/get-auth-string"><code>gcloud redis instances get-auth-string</code> reference</a></li>
</ul>

<pre><code class="language-bash">gcloud redis instances get-auth-string "${redis_instance_name}" \
    --region="${region}"
</code></pre>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You are now able to manage <code>redis</code> datastores on GCP via the UI as well as via the
<code>gcloud</code> cli.</p>
]]></description>
                <pubDate>Mon, 17 Apr 2023 06:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/gcp-redis-memorystore-instances-create-connect-delete/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/gcp-redis-memorystore-instances-create-connect-delete/</guid>
            </item>
                    <item>
                <title>Using GCP MySQL Cloud SQL instances (create/connect/delete)</title>
                <description><![CDATA[<p>In this blog post I'll summarize my experience with <strong>GCP MySQL Cloud SQL instances</strong>. Cloud SQL 
is the managed relational database solution from Google Cloud Platform and was mentioned in 
<a href="/blog/deploy-docker-compose-php-gcp-poc/#introduction">Deploy dockerized PHP Apps to production on GCP via docker compose as a POC</a>
as the "better" way to deal with databases in a dockerized application (compared to running a 
database via <code>docker</code>).</p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>What will you learn?</strong>
  </div>
  <div class="panel-body bg-info">
    I'll explain the basic steps to <strong>create a fresh MySQL instance</strong>, 
    <strong>show different ways to connect to it</strong> (Cloud Shell, locally "from your laptop" 
    and from a VM within GCP) and finally <strong>how to delete the instance</strong>. Every process 
    is done through the Cloud Console UI and <strong>recorded as a short video</strong> as a visual 
    aid. As in the <a href="/blog/gcp-compute-instance-vm-docker/">GCP "primer" tutorial</a>, this 
    article ends with the commands to achieve the same things also via the
    <code>gcloud</code> CLI.
  </div>
</div>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#setup-cloud-sql">Setup Cloud SQL</a></li>
<li><a href="#create-a-new-mysql-instance">Create a new <code>mysql</code> instance</a></li>
<li><a href="#connecting-to-a-mysql-cloud-sql-instance">Connecting to a MySQL Cloud SQL instance</a>

<ul>
<li><a href="#connecting-to-the-mysql-instance-with-cloud-shell-via-public-ip">Connecting to the <code>mysql</code> instance with Cloud Shell via public IP</a></li>
<li><a href="#connecting-to-the-mysql-instance-via-public-ip">Connecting to the <code>mysql</code> instance via public IP</a></li>
<li><a href="#connecting-to-the-mysql-instance-via-private-ip">Connecting to the <code>mysql</code> instance via private IP</a>

<ul>
<li><a href="#concrete-steps-to-connect-via-private-ip">Concrete steps to connect via private IP</a></li>
<li><a href="#caveats-for-using-a-private-ip-only">Caveats for using a private IP only</a></li>
</ul></li>
</ul></li>
<li><a href="#delete-a-mysql-cloud-sql-instance">Delete a MySQL Cloud SQL instance</a></li>
<li><a href="#using-the-gcloud-cli">Using the <code>gcloud</code> CLI</a>

<ul>
<li><a href="#activate-the-service-account">Activate the service account</a></li>
<li><a href="#enable-the-necessary-apis">Enable the necessary APIs</a></li>
<li><a href="#create-an-ip-range-allocation">Create an IP range allocation</a></li>
<li><a href="#create-the-vpc-peering-with-servicenetworking-googleapis-com">Create the VPC peering with <code>servicenetworking.googleapis.com</code></a></li>
<li><a href="#create-the-mysql-instance">Create the <code>mysql</code> instance</a>

<ul>
<li><a href="#set-the-root-password">Set the root password</a></li>
<li><a href="#create-a-default-database">Create a default database</a></li>
</ul></li>
</ul></li>
</ul>

<!-- /toc -->

<p><!-- generated -->
<a id='setup-cloud-sql'> </a>
<!-- /generated --></p>

<h2>Setup Cloud SQL</h2>

<p><a href="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/cloud-console-cloud-sql-ui.PNG"><img src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/cloud-console-cloud-sql-ui.PNG" alt="GCP Cloud Console Cloud SQL UI" /></a></p>

<p>The managed solution for relational databases from GCP is called 
<a href="https://cloud.google.com/sql">Cloud SQL</a> and provides multiple database technologies - 
including <a href="https://cloud.google.com/sql/mysql"><code>mysql</code></a>. In the Cloud Console UI it is managed 
via the <a href="https://console.cloud.google.com/sql">SQL UI</a> that allows us to create and manage 
instances.</p>

<p><!-- generated -->
<a id='create-a-new-mysql-instance'> </a>
<!-- /generated --></p>

<h2>Create a new <code>mysql</code> instance</h2>

<p>To get started, we need to enable the following APIs:</p>

<ul>
<li><a href="https://console.cloud.google.com/apis/library/compute.googleapis.com">Compute Engine API</a></li>
<li><a href="https://console.cloud.google.com/apis/library/sqladmin.googleapis.com">Cloud SQL Admin API</a></li>
</ul>

<p>Creating a new instance from the 
<a href="https://console.cloud.google.com/sql/instances/create;engine=MySQL">Create a MySQL instance UI</a>
is pretty straight forward and well documented in the 
<a href="https://cloud.google.com/sql/docs/mysql/create-instance?hl=en">GCP MySQL Guide: Create instances</a>, 
though there are some configuration options under "Customize your instance" that I want to mention:</p>

<ul>
<li><code>Machine type &gt; Machine type</code>: For testing purposes, I recommend choosing a "Shared core" 
option here (e.g. 1 vCPU, 0.614 GB) to keep the costs to a minimum</li>
<li><code>Connections &gt; Instance IP assignment</code>: For now we'll go with a "Public IP", because 
<a href="#connecting-to-the-mysql-instance-via-private-ip">setting up private connectivity requires some additional configuration</a></li>
<li><code>Data Protection &gt; Instance deletion protection</code>: When the option 
<code>Enable deletion protection</code> is enabled, the instance cannot be deleted. The option must first 
be disabled before this is possible (see also
<a href="#delete-a-mysql-cloud-sql-instance">Delete a Cloud SQL instance</a>).</li>
</ul>

<p>FYI: Unfortunately, there is no <code>"EQUIVALENT COMMAND LINE"</code> button as it was the case when 
<a href="/blog/gcp-compute-instance-vm-docker/#create-a-compute-instance-vm">creating the Compute Instance</a> -
which would have come in handy for 
<a href="#create-the-mysql-instance">creating the instance via <code>gcloud</code> cli</a>.</p>

<p>Once everything is configured, click the <code>"Create Instance"</code> button. The actual creation can 
take quite some time (I've experienced times from a couple of minutes to half an hour).</p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/create-gcp-mysql-cloud-sql-instance.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='connecting-to-a-mysql-cloud-sql-instance'> </a>
<!-- /generated --></p>

<h2>Connecting to a MySQL Cloud SQL instance</h2>

<p>I'll explain 3 different ways of <strong>connecting to a MySQL Cloud SQL instance</strong>:</p>

<ul>
<li>via Cloud Shell (requires a public instance IP)</li>
<li>"locally" from your laptop (requires a public instance IP)</li>
<li>from a Compute Instance VM on GCP (requires a private instance IP)</li>
</ul>

<p>Out of scope:</p>

<ul>
<li>using the <a href="https://cloud.google.com/sql/docs/mysql/sql-proxy">Cloud SQL Auth proxy</a></li>
</ul>

<p>As always, GCP has also an extensive documentation on the various connection methods available at
<a href="https://cloud.google.com/sql/docs/mysql/connect-overview">GCP MySQL Guide: About connection options</a>.</p>

<p><!-- generated -->
<a id='connecting-to-the-mysql-instance-with-cloud-shell-via-public-ip'> </a>
<!-- /generated --></p>

<h3>Connecting to the <code>mysql</code> instance with Cloud Shell via public IP</h3>

<p>The easiest way to connect is through <a href="https://cloud.google.com/shell">Cloud Shell</a>. GCP will 
spin up a VM "behind the scenes" that we can control via shell session in the browser. Navigate 
to the management UI of the instance at</p>

<pre><code class="language-text">https://console.cloud.google.com/sql/instances/$instanceName/overview

# e.g. for an instance named 'mysql-1'
# https://console.cloud.google.com/sql/instances/mysql-1/overview 
</code></pre>

<p>and click the <code>"OPEN CLOUD SHELL"</code> button in panel "Connect to this instance".</p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/cloud-shell-mysql.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>Please note the</p>

<pre><code>Allowlisting your IP for incoming connection for 5 minutes...working.
</code></pre>

<p>step in the video: GCP handles all the "networking stuff" to make the connection possible for us.</p>

<p><!-- generated -->
<a id='connecting-to-the-mysql-instance-via-public-ip'> </a>
<!-- /generated --></p>

<h3>Connecting to the <code>mysql</code> instance via public IP</h3>

<p>Even though the instance has a public IP address (<code>104.198.240.225</code> in this case, as shown in 
the previous video), we can't simply connect to it via</p>

<pre><code class="language-text">$ mysql -u root -p12345678 -h 104.198.240.225
# ... the request will simply time out
</code></pre>

<p><a href="https://cloud.google.com/sql/docs/mysql/authorize-networks?hl=en#limitations">due to firewall rules</a>:</p>

<blockquote>
  <p>Note: The authorized networks list is implemented on the Cloud SQL instance VM by a local 
  firewall. Learn more about managing connections.</p>
</blockquote>

<p>If we want to connect from e.g. our own computer, we need to first create a so-called 
"Authorized network" as described in the
<a href="https://cloud.google.com/sql/docs/mysql/authorize-networks?hl=en">GCP MySQL Guide: Authorize with authorized networks</a>.</p>

<p>For testing purposes I'll 
<a href="https://www.cyberciti.biz/faq/how-to-find-my-public-ip-address-from-command-line-on-a-linux/">retrieve my own public IP address</a>
via <code>wget -q -O - ifconfig.me</code></p>

<pre><code class="language-text">$ wget -q -O - ifconfig.me
213.21.32.150
</code></pre>

<p>and edit the <code>mysql</code> instance to <strong>add only this IP address as an Authorized network</strong>. The 
following video shows that the connection isn't possible before I added the Authorized network 
but will be afterwards.</p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/connect-from-localhost-to-cloud-sql-mysql.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><strong>CAUTION</strong>: Please regard this <em>only as a proof of concept</em>. It's not really a viable process to 
retrieve your own public IP in order to allowlist it "manually". It might be "okay" though, if 
you are using a VPN with a static IP. However, for the 
<a href="/docker-php-tutorial/">Docker PHP tutorial</a> we'll not use a public IP address at all, because 
our <code>mysql</code> instance should only be available for our application VMs.</p>

<p><!-- generated -->
<a id='connecting-to-the-mysql-instance-via-private-ip'> </a>
<!-- /generated --></p>

<h3>Connecting to the <code>mysql</code> instance via private IP</h3>

<p>Having the MySQL Cloud SQL instance exposed via public IP is generally not advised and 
always carries a certain security risk. Thus, it is desirable <strong>to only allow connections via 
private IP</strong>. Be aware that this has some caveats, though (see section 
<a href="#caveats-for-using-a-private-ip-only">Caveats for using a private IP only</a>).</p>

<p>In order to <strong>connect to the MySQL Cloud SQL instance via private IP</strong> (i.e. from a Compute 
Instance VM), we must first understand how the network configuration of the "managed" products 
of GCP is set up. This is explained in detail in the 
<a href="https://cloud.google.com/sql/docs/mysql/private-ip">GCP MySQL Guide: Learn about using private IP</a>. 
In short:</p>

<ul>
<li>we use <strong>a private network that allows all of our VMs to communicate</strong></li>
<li>such a network is called <strong><a href="https://cloud.google.com/vpc">VPC</a> (virtual private cloud)</strong></li>
<li>since "we" (as a user of GCP) create and manage our application VMs ourselves, <strong>the VMs live 
"directly" in this network</strong></li>
<li>the <strong>managed services like Cloud SQL</strong> are "doing their own thing", i.e. <strong>GCP takes care of all 
the networking stuff</strong> but keeps it <strong>isolated from "our" network</strong></li>
<li>in consequence, <strong>our VMs cannot talk to a MySQL Cloud SQL instance</strong>, because it lives on a 
different network</li>
<li>fortunately, there is a technique called
<strong><a href="https://cloud.google.com/vpc/docs/vpc-peering">VPC peering</a></strong> that can connect different VPCs 
and thus enable communication</li>
<li>to make this work, we need to <strong>"reserve" a range of IPs in "our" VPC</strong> and make it available to
Cloud SQL by peering with the <strong>Google Cloud Platform Service Producer</strong></li>
</ul>

<p>See also the <a href="https://cloud.google.com/sql/docs/mysql/private-ip#example">Example in the MySQL Guide: Learn about using private IP</a></p>

<p><a href="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/mysql-cloud-sql-private-ip-vpc-peering.svg"><img src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/mysql-cloud-sql-private-ip-vpc-peering.svg" alt="Connecting to Cloud SQL via private IP through VPC peering" /></a></p>

<p><!-- generated -->
<a id='concrete-steps-to-connect-via-private-ip'> </a>
<!-- /generated --></p>

<h4>Concrete steps to connect via private IP</h4>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/connect-cloud-sql-mysql-instance-via-private-ip.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>In short:</p>

<ul>
<li>enable the <a href="https://console.cloud.google.com/apis/library/servicenetworking.googleapis.com">Service Networking API</a></li>
<li>navigate to the <a href="https://console.cloud.google.com/networking/networks/details/default">"default" network UI</a></li>
<li>open tab "PRIVATE SERVICE CONNECTION" and sub tab "ALLOCATED IP RANGES FOR SERVICES" and click 
the button <code>"ALLOCATE IP RANGE"</code>

<ul>
<li>give it a name, e.g. "google-internal-services"</li>
<li>and select option "Automatic" with a prefix length of 16 (this determines the number of 
possible Cloud SQL instances, see also
<a href="https://cloud.google.com/sql/docs/mysql/private-ip#allocated_range_size">the docs on Allocated range size</a>)</li>
</ul></li>
<li>open tab "PRIVATE SERVICE CONNECTION" and sub tab "PRIVATE CONNECTIONS TO SERVICES" and click 
the button <code>"CREATE CONNECTION"</code>

<ul>
<li>choose "Google Cloud Platform" in the "Connected service producer" drop down field and 
select the "google-internal-services" range that we just created for the "Assigned allocation" 
drop down field</li>
</ul></li>
<li>once the "peering" is done, you can also observe a corresponding entry in the 
<a href="https://console.cloud.google.com/networking/peering/list">VPC network peering UI</a>
<a href="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/vpc-peering-servicenetworking-googleapis-com.PNG"><img src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/vpc-peering-servicenetworking-googleapis-com.PNG" alt="VPC peering connection with servicenetworking.googleapis.com" /></a></li>
<li><a href="https://console.cloud.google.com/sql/instances/create;engine=MySQL">create a new MySQL Cloud SQL instance</a>
(or modify an existing one)

<ul>
<li>under <code>Connections &gt; Instance IP assignment</code> unselect "Public IP" and select "Private IP" 
select the network "default"</li>
<li><em>Note</em>: The instance is being shutdown and restarted, i.e. this operation will lead to a 
service disruption and <strong>can take up to several minutes</strong></li>
</ul></li>
<li>once the update is done, navigate to the MySQL Cloud SQL UI of the instance and find the 
<strong>private IP</strong> in panel "Private IP address"</li>
</ul>

<p>To test the connectivity from a VM, perform the following steps:</p>

<ul>
<li><a href="/blog/gcp-compute-instance-vm-docker/#create-a-vm">create a Compute Instance VM</a> in the "default" network</li>
<li><a href="/blog/gcp-compute-instance-vm-docker/#login-via-ssh-from-the-gcp-ui">log into the VM via the UI</a></li>
<li><p><a href="https://superuser.com/a/1481211">install the <code>mysql</code> client</a> via</p>

<pre><code class="language-bash">sudo apt-get install default-mysql-client -y
</code></pre></li>
<li><p>connect to the MySQL Cloud SQL instance via</p>

<pre><code class="language-bash">mysql -h $privateIp -u root -p
</code></pre>

<p>where <code>$privateIp</code> is the private IP of the <code>mysql</code> instance</p></li>
</ul>

<p>See also the GCP MySQL docs:</p>

<ul>
<li><a href="https://cloud.google.com/sql/docs/mysql/configure-private-services-access">Configure private services access</a></li>
<li><a href="https://cloud.google.com/sql/docs/mysql/configure-private-ip">Configure private IP</a></li>
</ul>

<p><!-- generated -->
<a id='caveats-for-using-a-private-ip-only'> </a>
<!-- /generated --></p>

<h4>Caveats for using a private IP only</h4>

<ul>
<li>we need to provide some additional networking configuration (explained above)</li>
<li><p><a href="https://cloud.google.com/sql/docs/mysql/configure-private-ip?hl=en#connect_from">it's no longer possible to connect via Cloud Shell</a>:</p>

<blockquote>
  <p>Cloud Shell doesn't currently support connecting to a Cloud SQL instance that has only
  a private IP address.</p>
</blockquote></li>
<li><p><a href="https://cloud.google.com/sql/docs/mysql/configure-private-ip?hl=en#configure_an_instance_to_use_private_ip">once a private IP is assigned, it cannot be un-assigned</a>:</p>

<blockquote>
  <p>After you configure an instance to use private IP, you cannot disable private IP connectivity
  for that instance.</p>
  
  <p>If you choose to let Cloud SQL allocate your private IP for an instance, the addresses for
  all instances you later configure in that VPC network are automatically allocated in the same IP address range.</p>
</blockquote></li>
<li>we can no longer connect from "our" computer, because it doesn't live in the same private network.
This can be problematic if you are used to using UI tools like MySQL Workbench to manage your
database - please check out the following articles if this is relevant for you:

<ul>
<li><a href="https://medium.com/google-cloud/cloud-sql-with-private-ip-only-the-good-the-bad-and-the-ugly-de4ac23ce98a">Cloud SQL with private IP only: the Good, the Bad and the Ugly</a></li>
<li><a href="https://medium.com/swlh/how-to-deploy-a-cloud-sql-db-with-a-private-ip-only-using-terraform-e184b08eca64">How to Deploy a Cloud SQL DB with a Private IP only, using Terraform</a></li>
<li><a href="https://www.youtube.com/watch?v=rebyg9_eTHM">GCP | How to access Cloud SQL private IP using Cloud SQL Auth Proxy and Identity-Aware Proxy (IAP)?</a></li>
</ul></li>
</ul>

<p>But tbh, I would probably rather 
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--ssh-flag">create an SSH tunnel with the <code>gcloud</code> cli</a>
on a small Compute Instance VM via</p>

<pre><code class="language-bash">gcloud compute ssh $computeInstanceName --zone=$computeInstanceZone -- -N -L 3306:$mysqlInstanceIp:3306
</code></pre>

<p><!-- generated -->
<a id='delete-a-mysql-cloud-sql-instance'> </a>
<!-- /generated --></p>

<h2>Delete a MySQL Cloud SQL instance</h2>

<p>Once again, GCP is doing a great job documenting the process to <strong>delete a MySQL Cloud SQL 
instance</strong> in 
the <a href="https://cloud.google.com/sql/docs/mysql/delete-instance">GCP MySQL Guide: Delete instances</a>.</p>

<p><del>Please give it a read as there is at least once thing that I found quite un-intuitive: Even 
after deleting an instance, the name of the deleted instance is "blocked" for a week. In my case,
this created some problems, because I was making assumptions about the name in other 
parts of my code (e.g. I was expecting that the name of the instance is always <code>mysql</code>), but 
when playing around with Cloud SQL I learned too late, that I couldn't re-use the name after 
deleting the instance for testing purposes.</del></p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Update</strong>
  </div>
  <div class="panel-body bg-info">
    As of 2022-09-21 the above statement is no longer true - 
    <strong>instance names are now immediately available after deletion:</strong> 
    <br>
    <br>
    <em>
    > Cloud SQL allows the re-use of an instance name immediately after the instance is deleted. 
    </em>
    <br>
    <br>
    Source: 
    <a href="https://cloud.google.com/release-notes#September_21_2022">GCP Release Notes from 2022-09-21</a>
  </div>
</div>

<p><small>Note: If "Deletion protection" is enabled, it needs to be disabled first - which is 
usually quite a fast operation.</small></p>

<p>See the following video for a visual representation of the process (please ignore the "blocked" 
instance name issue at the end):</p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/delete-cloud-sql-mysql-instance.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='using-the-gcloud-cli'> </a>
<!-- /generated --></p>

<h2>Using the <code>gcloud</code> CLI</h2>

<p>Even though I like using the UI to "explore and understand" how things are working, the goal is 
always a more "unattended" approach, e.g. via the 
<a href="/blog/gcp-compute-instance-vm-docker#set-up-the-gcloud-cli-tool"><code>gcloud</code> cli</a>. 
Since I'm gonna use a MySQL Cloud SQL instance as replacement for a <code>mysql</code> docker container for 
the next part of the <a href="/docker-php-tutorial/">Docker PHP tutorial</a>, I'll focus on creating an 
instance with a private IP address only, update the root password and set up a default database.</p>

<p>The following commands assume that you have created a master service account with owner 
permissions and activated it for <code>glcoud</code> with a default project. See also 
<a href="/blog/gcp-compute-instance-vm-docker#preconditions-project-and-owner-service-account">Preconditions: Project and Owner service account</a></p>

<video controls>
  <source src="/img/gcp-mysql-cloud-sql-instances-create-connect-delete/create-gcp-mysql-cloud-sql-instance-via-gcloud-cli.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='activate-the-service-account'> </a>
<!-- /generated --></p>

<h3>Activate the service account</h3>

<pre><code class="language-bash">project_id=pl-dofroscra-p
gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=${project_id}
</code></pre>

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

<h3>Enable the necessary APIs</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/services/enable"><code>gcloud services enable</code> reference</a></li>
</ul>

<pre><code class="language-bash">gcloud services enable \
  compute.googleapis.com \
  sqladmin.googleapis.com \
  cloudresourcemanager.googleapis.com \
  servicenetworking.googleapis.com
</code></pre>

<p><code>cloudresourcemanager.googleapis.com</code> is necessary to create the IP range allocation in the 
next step.</p>

<p><!-- generated -->
<a id='create-an-ip-range-allocation'> </a>
<!-- /generated --></p>

<h3>Create an IP range allocation</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/compute/addresses/create"><code>gcloud compute addresses create</code> reference</a></li>
</ul>

<pre><code class="language-bash">private_ip_range_name="internal-gcp-services"
network="default"
ip_range_network_address="10.111.0.0"

gcloud compute addresses create "${private_ip_range_name}" \
  --global \
  --purpose=VPC_PEERING \
  --prefix-length=16 \
  --addresses="${ip_range_network_address}" \
  --description="Peering range for GCP Services" \
  --network="${network}"
</code></pre>

<p>The <code>--addresses</code> flag can be used to define the network address of the defined range. If it is 
not given, GCP will choose a random one.</p>

<p><!-- generated -->
<a id='create-the-vpc-peering-with-servicenetworking-googleapis-com'> </a>
<!-- /generated --></p>

<h3>Create the VPC peering with <code>servicenetworking.googleapis.com</code></h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/services/vpc-peerings/connect"><code>gcloud services vpc-peerings connect</code> reference</a></li>
</ul>

<pre><code class="language-bash">private_ip_range_name="internal-gcp-services"
network="default"

gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges="${private_ip_range_name}" \
  --network="${network}"
</code></pre>

<p><!-- generated -->
<a id='create-the-mysql-instance'> </a>
<!-- /generated --></p>

<h3>Create the <code>mysql</code> instance</h3>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/beta/sql/instances/create"><code>gcloud beta sql instances create</code> reference</a></li>
<li><a href="https://cloud.google.com/sql/docs/mysql/create-instance">GCP MySQL Guide: Create instances</a></li>
<li><a href="https://cloud.google.com/sql/docs/mysql/configure-private-ip#new-private-instance">GCP MySQL Guide: Configure an instance to use private IP</a></li>
</ul>

<p><strong>Caution</strong>: The <code>--allocated-ip-range-name</code> option is currently (2023-04-13) only available in 
the <a href="https://cloud.google.com/sdk/gcloud/reference/beta"><code>beta</code> tools of the <code>gcloud</code> cli</a>. 
You might need to install them first via 
<a href="https://cloud.google.com/sdk/gcloud/reference/components/install"><code>gcloud components install beta</code></a>.</p>

<pre><code class="language-bash">mysql_instance_name="mysql"
version="MYSQL_8_0" # see https://cloud.google.com/sdk/gcloud/reference/sql/instances/create#--database-version
cpus="1"
memory="3840MB"
region="us-central1"
private_ip_range_name="internal-gcp-services"
network="default"

gcloud beta sql instances create "${mysql_instance_name}" \
  --database-version="${version}" \
  --cpu="${cpus}" \
  --memory="${memory}" \
  --region="${region}" \
  --network="${network}" \
  --no-assign-ip \
  --allocated-ip-range-name="${private_ip_range_name}"
</code></pre>

<p>The <code>--no-assign-ip</code> flag is responsible for not assigning a <em>public</em> IP.</p>

<p><!-- generated -->
<a id='set-the-root-password'> </a>
<!-- /generated --></p>

<h4>Set the root password</h4>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/sql/users/set-password"><code>gcloud sql users set-password</code> reference</a></li>
<li><a href="https://cloud.google.com/sql/docs/mysql/create-manage-users#change-pwd">GCP MySQL Guide: Manage users with built-in authentication: Change a user password</a></li>
</ul>

<pre><code class="language-bash">mysql_instance_name="mysql"
root_password=12345678

gcloud sql users set-password root \
  --host=% \
  --instance "${mysql_instance_name}" \
  --password "${root_password}"
</code></pre>

<p>Using <code>--host=%</code> will allow access from any remote host, see 
<a href="https://cloud.google.com/sdk/gcloud/reference/sql/users/set-password#--host">docu for the <code>--host</code> parameter </a></p>

<blockquote>
  <p>Cloud SQL user's host name expressed as a specific IP address or address range. % denotes an 
  unrestricted host name. Applicable flag for MySQL instances; ignored for all other engines.</p>
</blockquote>

<p>FYI: Instead of "root", we could also use a different username like "application_user" and MySQL 
would create that user automatically with the given password (without any privileges).</p>

<p><!-- generated -->
<a id='create-a-default-database'> </a>
<!-- /generated --></p>

<h4>Create a default database</h4>

<ul>
<li><a href="https://cloud.google.com/sdk/gcloud/reference/sql/databases/create"><code>gcloud sql databases create</code> reference</a></li>
<li><a href="https://cloud.google.com/sql/docs/mysql/create-manage-databases#create">GCP MySQL Guide: Create and manage databases: Create a database on the Cloud SQL instance</a></li>
</ul>

<pre><code class="language-bash">mysql_instance_name="mysql"
default_database="application_db"

gcloud sql databases create "${default_database}" \
  --instance="${mysql_instance_name}" \
  --charset=utf8mb4 \
  --collation=utf8mb4_unicode_ci
</code></pre>

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You are now able to manage MySQL databases on GCP via the UI as well as via the
<code>gcloud</code> cli.</p>
]]></description>
                <pubDate>Thu, 13 Apr 2023 10:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/gcp-mysql-cloud-sql-instances-create-connect-delete/</guid>
            </item>
                    <item>
                <title>Setting up Git Bash / MINGW / MSYS2 on Windows</title>
                <description><![CDATA[<p>In this article I'll document my process for <strong>setting up Git Bash / MINGW / 
MSYS2 on Windows</strong> including some additional configuration (e.g. installing <code>make</code> and apply 
some customizations via <code>.bashrc</code>).</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#how-to-install-and-update-git-bash-mingw-msys2-via-git-for-windows">How to install and update Git Bash / MINGW / MSYS2 via Git for Windows</a>

<ul>
<li><a href="#update-mingw">Update MINGW</a></li>
<li><a href="#how-to-install-make">How to install <code>make</code></a></li>
<li><a href="#configuration-via-bashrc">Configuration via <code>.bashrc</code></a></li>
</ul></li>
<li><a href="#common-issues">Common issues</a>

<ul>
<li><a href="#the-role-of-winpty-fixing-the-input-device-is-not-a-tty">The role of <code>winpty</code>: Fixing "The input device is not a TTY" </a></li>
<li><a href="#the-path-conversion-issue">The path conversion issue</a>

<ul>
<li><a href="#fixing-the-path-conversion-issue-for-mingw-msys2">Fixing the path conversion issue for MINGW / MSYS2</a></li>
<li><a href="#fixing-the-path-conversion-issue-for-winpty">Fixing the path conversion issue for <code>winpty</code></a></li>
</ul></li>
</ul></li>
<li><a href="#miscellaneous">Miscellaneous</a>

<ul>
<li><a href="#change-the-bash-custom-prompt-to-a">Change the <code>bash</code> custom prompt to a <code>$</code></a></li>
</ul></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>When I was learning <code>git</code> I started with the fantastic 
<a href="https://gitforwindows.org/">Git for Windows</a> package, that is maintained in the 
<a href="https://github.com/git-for-windows/"><code>git-for-windows/git</code> Github repository</a> and comes with 
<a href="https://www.atlassian.com/git/tutorials/git-bash">Git Bash</a>, a shell that offers a 
Unix-terminal like experience. It uses 
<a href="https://github.com/git-for-windows/git/wiki/The-difference-between-MINGW-and-MSYS2">MINGW and MSYS2 under the hood</a>
and does not only provide <code>git</code> but also a bunch of other common Linux utilities like</p>

<pre><code class="language-text">bash
sed
awk
ls
cp
rm
...
</code></pre>

<p>I believe the main "shell" is actually powered by <a href="https://www.mingw-w64.org/">MINGW64</a> as 
that's what will be shown by default:</p>

<p><a href="/img/setting-up-git-bash-mingw-msys2-on-windows/mingw.PNG"><img src="/img/setting-up-git-bash-mingw-msys2-on-windows/mingw.PNG" alt="Git Bash / MINGW shell" /></a></p>

<p>Thus, I will refer to the tool as MINGW shell or Git Bash throughout this article.</p>

<p>I have been using MINGW for almost 10 years now, and it is still my go-to shell for Windows. I 
could just never warm up to WSL, because the file sharing performance between WSL and native 
Windows  files was (is?) horrible - but that's a different story.</p>

<p><!-- generated -->
<a id='how-to-install-and-update-git-bash-mingw-msys2-via-git-for-windows'> </a>
<!-- /generated --></p>

<h2>How to install and update Git Bash / MINGW / MSYS2 via Git for Windows</h2>

<p>You can find the latest Git for Windows installation package directly at the homepage of
<a href="https://gitforwindows.org/">https://gitforwindows.org/</a>. Older releases can be found on 
Github in the 
<a href="https://github.com/git-for-windows/git/releases">Releases section of the <code>git-for-windows/git</code> repository</a></p>

<p>Follow the instructions in the 
<a href="https://www.git-tower.com/blog/git-bash/#how-to-install-git-bash-on-windows">How to Install Git Bash on Windows article on git-tower.com</a>
to get a guided tour through the setup process.</p>

<p>After the installation is finished, I usually create a desktop icon and assign the shortcut 
<code>CTRL + ALT + B</code> (for "<strong>b</strong>ash") so that I can open a new shell session conveniently via keyboard.</p>

<p><a href="/img/setting-up-git-bash-mingw-msys2-on-windows/git-bash-desktop-shortcut.PNG"><img src="/img/setting-up-git-bash-mingw-msys2-on-windows/git-bash-desktop-shortcut.PNG" alt="Git Bash desktop icon and shortcut" /></a></p>

<p><!-- generated -->
<a id='update-mingw'> </a>
<!-- /generated --></p>

<h3>Update MINGW</h3>

<p>To update Git for Windows, you can simply run</p>

<pre><code class="language-bash">git update-git-for-windows
</code></pre>

<p>See also the
<a href="https://github.com/git-for-windows/git/wiki/FAQ#how-do-i-update-git-for-windows-upon-new-releases">Git for Windows FAQ under "How do I update Git for Windows upon new releases?"</a></p>

<blockquote>
  <p>Git for Windows comes with a tool to check for updates and offer to install them. Whether or 
  not you enabled auto-updates during installation, you can manually run 
  <code>git update-git-for-windows</code>.</p>
</blockquote>

<p>You can check the current version via <code>git version</code></p>

<pre><code class="language-text">$ git --version
git version 2.37.2.windows.2
</code></pre>

<video controls>
  <source src="/img/setting-up-git-bash-mingw-msys2-on-windows/update-git-bash.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><!-- generated -->
<a id='how-to-install-make'> </a>
<!-- /generated --></p>

<h3>How to install <code>make</code></h3>

<p>As per
<a href="https://gist.github.com/evanwill/0207876c3243bbb6863e65ec5dc3f058#make">How to add more to Git Bash on Windows: <code>make</code></a>:</p>

<ul>
<li>Go to <a href="https://sourceforge.net/projects/ezwinports/files/">ezwinports</a></li>
<li>Download file <a href="https://sourceforge.net/projects/ezwinports/files/make-4.3-without-guile-w32-bin.zip/download"><code>make-4.3-without-guile-w32-bin.zip</code></a>
(get the version without guile)</li>
<li>Extract zip</li>
<li><p>Copy the contents to your <code>Git/mingw64/</code> directory, merging the folders, but do NOT
overwrite/replace any existing files</p>

<ul>
<li>navigate to the <code>Git/mingw64/</code> directory via</li>
</ul>

<pre><code>$(cd /; explorer .)
</code></pre></li>
</ul>

<p>Test via <code>make version</code></p>

<pre><code class="language-text">$ make --version
GNU Make 4.3.1
Built for Windows32
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later &lt;http://gnu.org/licenses/gpl.html&gt;
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
</code></pre>

<video controls>
  <source src="/img/setting-up-git-bash-mingw-msys2-on-windows/install-make-git-bash.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>PS: There's also an alternative way that I've outlined in
<a href="/blog/structuring-the-docker-setup-for-php-projects/#install-make-on-windows-mingw">Install <code>make</code> on Windows (MinGW)</a>,
though the one explained here is easier/faster.</p>

<p><!-- generated -->
<a id='configuration-via-bashrc'> </a>
<!-- /generated --></p>

<h3>Configuration via <code>.bashrc</code></h3>

<p>The MINGW shell is a <code>bash</code> shell and can thus be 
<a href="https://www.digitalocean.com/community/tutorials/bashrc-file-in-linux">configured via a <code>.bashrc</code> file</a> located at 
the home directory of the user. The shell supports the <code>~</code> character as an alias for the home 
directory, i.e. <strong>you can use <code>~/.bashrc</code> to refer to the full path of the file</strong>. This means you can 
also edit it easily via <code>vi ~/.bashrc</code> - though I prefer an actual GUI editor like
<a href="https://notepad-plus-plus.org/">Notepad++</a>. A common workflow for me to open the file is 
running the following commands in a MINGW shell session</p>

<pre><code class="language-text"># navigate to to the home directory
cd ~

# open the file explorer
explorer .
</code></pre>

<p>My <code>.bashrc</code> file usually includes the following setup:</p>

<pre><code class="language-bash"># Get bash completion for make targets by parsing make files in the current directory at 
#  the file "Makefile"
#  all files with a ".mk" suffix in the folders ".make" and ".makefile"
# see https://stackoverflow.com/questions/4188324/bash-completion-of-makefile-target
# Notes:
#  -h hides filenames
#  -s hides error messages
complete -W "\`grep -shoE '^[a-zA-Z0-9_.-]+:([^=]|$)' Makefile .make/*.mk .makefile/*.mk | sed 's/[^a-zA-Z0-9_.-]*$//' | grep -v PHONY\`" make

# Docker login helper
# see https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper
function din() {
  filter=$1

  user=""
  if [[ -n "$2" ]];
  then
    user="--user $2"
  fi

  shell="bash"
  if [[ -n "$3" ]];
  then
    shell=$3
  fi

  prefix=""
  if [[ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]]; then
    prefix="winpty"
  fi
  ${prefix} docker exec -it ${user} $(docker ps --filter name=${filter} -q | head -1) ${shell}
}
</code></pre>

<p>Links:</p>

<ul>
<li><a href="https://stackoverflow.com/questions/4188324/bash-completion-of-makefile-target">SO: bash completion of makefile target</a></li>
<li><a href="/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper">Easy container access via din .bashrc helper</a></li>
</ul>

<p><!-- generated -->
<a id='common-issues'> </a>
<!-- /generated --></p>

<h2>Common issues</h2>

<p>The
<a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues">Git for Windows Known Issues page</a>
lists common problems with Git Bash and I want to provide some more context (and solutions) 
to the things that I have encountered.</p>

<p><!-- generated -->
<a id='the-role-of-winpty-fixing-the-input-device-is-not-a-tty'> </a>
<!-- /generated --></p>

<h3>The role of <code>winpty</code>: Fixing "The input device is not a TTY"</h3>

<p>I encountered the <code>The input device is not a TTY</code> error while using <code>docker</code>. To log into a 
running <code>docker</code> container or starting a container with a login session, the
<a href="https://docs.docker.com/engine/reference/run/#foreground"><code>-i</code> (Keep STDIN open even if not attached) and <code>-t</code> (Allocate a pseudo-tty) options must be given:</a></p>

<blockquote>
  <p>For interactive processes (like a shell), you must use <code>-i</code> <code>-t</code> together in order to allocate a
  tty for the container process. <code>-i</code> <code>-t</code>  is often written <code>-it</code>.</p>
</blockquote>

<p>But attempting to do so via</p>

<pre><code class="language-bash">docker run --rm -it busybox sh
</code></pre>

<p>yields the following error:</p>

<pre><code class="language-text">$ docker run --rm -it busybox sh
the input device is not a TTY.  If you are using mintty, try prefixing the command with 'winpty'
</code></pre>

<p>Fortunately, the fix is included in the message: Prefix the command with <code>winpty</code>. Doing so
works as expected:</p>

<pre><code class="language-text">$ winpty docker run --rm -it busybox sh
/ #
</code></pre>

<p><a href="https://github.com/rprichard/winpty"><code>winpty</code></a> is according to it's readme</p>

<blockquote>
  <p>[...] a Windows software package providing an interface similar to a Unix pty-master for
  communicating with Windows console programs. The package consists of a library (libwinpty) and
  a tool for Cygwin and MSYS for running Windows console programs in a Cygwin/MSYS pty.</p>
</blockquote>

<p>So kind of a translator between your "Windows input" and the "command input" to create input 
that is 
<a href="https://iximiuz.com/en/posts/linux-pty-what-powers-docker-attach-functionality/">compatible with a Unix pty (pty=pseudoterminal interface), e.g. for <code>docker</code></a>.</p>

<p>According to the
<a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues">Git for Windows Known Issues page</a>,
there are a number of other cases where <code>winpty</code> is required (though I personally didn't
encounter them yet):</p>

<blockquote>
  <p>Some console programs, most notably non-MSYS2 Python, PHP, Node and OpenSSL, interact
  correctly with MinTTY only when called through <code>winpty</code> (e.g. the Python console needs to be
  started as <code>winpty python</code> instead of just <code>python</code>).</p>
</blockquote>

<p><strong>CAUTION</strong>: I've seen people put an alias in their <code>.bashrc</code> file to <em>always</em> prefix <code>docker</code> 
commands automatically with <code>winpty</code> like so:</p>

<pre><code class="language-text">alias docker="winpty docker"
</code></pre>

<p>However, <strong><a href="https://superuser.com/q/1011597/434918"><code>winpty</code> seems to break piping</a>
and can lead to unexpected results</strong> like the error <code>stdout is not a tty</code>. See the following 
example:</p>

<pre><code class="language-text">$ docker run --rm busybox echo "foo" | cat
foo
</code></pre>

<pre><code class="language-text">$ winpty docker run --rm busybox echo "foo" | cat
stdout is not a tty
</code></pre>

<p>You might work around this by adding the
<a href="https://github.com/rprichard/winpty/issues/103">(undocumented) <code>-Xallow-non-tty</code></a> flag like so</p>

<pre><code class="language-text">$ winpty -Xallow-non-tty docker run --rm busybox echo "foo" | cat
foo
</code></pre>

<p>But this <a href="https://github.com/rprichard/winpty/issues/103#issuecomment-285987050">doesn't seem to be a catch-all solution</a>
and I would recommend against using it as a default - or if you do, only use it when the <code>-it</code> 
flag is used <a href="https://stackoverflow.com/a/61580520/413531">as proposed in this answer</a>.</p>

<p><!-- generated -->
<a id='the-path-conversion-issue'> </a>
<!-- /generated --></p>

<h3>The path conversion issue</h3>

<p>Ah. This one has given me <em>lots</em> of headaches over the years. MINGW, MSYS2 and <code>winpty</code> use 
automatic conversion of Unix paths to Windows paths, e.g. <code>/foo</code> gets translated to something like
<code>C:/Program Files/Git/foo</code> where <code>C:/Program Files/Git/</code> is the installation directory of the 
Git for Windows installation.</p>

<p><!-- generated -->
<a id='fixing-the-path-conversion-issue-for-mingw-msys2'> </a>
<!-- /generated --></p>

<h4>Fixing the path conversion issue for MINGW / MSYS2</h4>

<p>First, the behavior is mentioned on the
<a href="https://github.com/git-for-windows/build-extra/blob/main/ReleaseNotes.md#known-issues">Git for Windows Known Issues page</a></p>

<blockquote>
  <p>If you specify command-line options starting with a slash, POSIX-to-Windows path conversion 
  will kick in converting e.g. "<code>/usr/bin/bash.exe</code>" to "<code>C:\Program Files\Git\usr\bin\bash.exe</code>". 
  When that is not desired -- e.g. "<code>--upload-pack=/opt/git/bin/git-upload-pack</code>" or "<code>-L/regex/</code>" 
  -- you need to set the environment variable <code>MSYS_NO_PATHCONV</code> temporarily, like so:</p>
  
  <p><code>MSYS_NO_PATHCONV=1 git blame -L/pathconv/ msys2_path_conv.cc</code></p>
  
  <p>Alternatively, you can double the first slash to avoid POSIX-to-Windows path conversion, e.g. 
  "<code>//usr/bin/bash.exe</code>".</p>
</blockquote>

<p>and also documented for
<a href="https://web.archive.org/web/20201112005258/http://www.mingw.org/wiki/Posix_path_conversion">MINGW at "Posix path conversion"</a>,
but it's still brought up regularly, see e.g.
<a href="https://github.com/git-for-windows/git/issues/3619">GH #3619: "/" is replaced with the directory path of Git installation when using MinGW64 Bash</a>.
or
<a href="https://stackoverflow.com/a/34386471">SO: How to stop MinGW and MSYS from mangling path names given at the command line</a></p>

<p><strong>Example</strong></p>

<pre><code class="language-bash">$ docker run --rm busybox ls /foo
ls: C:/Program Files/Git/foo: No such file or directory
</code></pre>

<p>As quoted above, it can be solved by either</p>

<ul>
<li><p>adding an <strong>additional <code>/</code></strong> to the path</p>

<pre><code class="language-text">$ docker run --rm busybox ls //foo
ls: /foo: No such file or directory
</code></pre></li>
<li><p><strong>prefixing</strong> the command with <code>MSYS_NO_PATHCONV=1</code></p>

<pre><code class="language-text">$ MSYS_NO_PATHCONV=1 docker run --rm busybox ls /foo
ls: /foo: No such file or directory
</code></pre></li>
<li><p>or exporting the <code>MSYS_NO_PATHCONV=1</code> variable as an <strong>environment variable</strong> to disable the 
behavior completely</p>

<pre><code class="language-text">$ export MSYS_NO_PATHCONV=1
$ docker run --rm busybox ls /foo
ls: /foo: No such file or directory
</code></pre></li>
</ul>

<p><strong>CAUTION:</strong> The value of the <code>MSYS_NO_PATHCONV</code> variable does not matter - we can also set it 
to <code>0</code>, <code>false</code> or an empty string. It only matters that the variable is defined!</p>

<pre><code class="language-text">$ MSYS_NO_PATHCONV=0 docker run --rm busybox ls /foo
ls: /foo: No such file or directory
</code></pre>

<p>This is particularly important when using the <strong>environment variable</strong> approach. In order to 
selectively enable the path conversion again, you must 
<a href="https://stackoverflow.com/a/41749660">unset the <code>MSYS_NO_PATHCONV</code> first</a> via 
<code>env -u MSYS_NO_PATHCONV ...</code>, e.g.</p>

<pre><code class="language-text">$ env -u MSYS_NO_PATHCONV docker run --rm busybox ls /foo
ls: C:/Program Files/Git/foo: No such file or directory
</code></pre>

<video controls>
  <source src="/img/setting-up-git-bash-mingw-msys2-on-windows/mingw-git-bash-posix-path-conversion.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p><strong>CAUTION</strong>: I've seen people adding <code>MSYS_NO_PATHCONV=1</code> permanently to their environment in
their <code>.bashrc</code> file to <em>always</em> disable path conversion via</p>

<pre><code class="language-bash">export MSYS_NO_PATHCONV=1
</code></pre>

<p>However, this can have some unintended side effects. When I tried it out,
<a href="/blog/gcp-compute-instance-vm-docker/#set-up-the-gcloud-cli-tool">my local installation of the <code>gcloud</code> cli</a>
stopped working with the error</p>

<pre><code class="language-text">$ MSYS_NO_PATHCONV=1 gcloud version
C:\Users\Pascal\AppData\Local\Programs\Python\Python39\python.exe: can't open file 'C:\c\Users\Pascal\AppData\Local\Google\Cloud SDK\google-cloud-sdk\lib\gcloud.py': [Errno 2] No such file or directory
</code></pre>

<p>So instead I recommend setting <code>MSYS_NO_PATHCONV=1</code> either selectively per command or scope it
to the use case. I do this for example in my Makefiles by only exporting it for the scope of
<code>make</code> (and all scripts <code>make</code> invokes) by putting the following code in the beginning of the 
Makefile:</p>

<pre><code class="language-makefile"># OS is a defined variable for WIN systems, so "uname" will not be executed
OS?=$(shell uname)
# Values of OS:
#   Windows =&gt; Windows_NT 
#   Mac     =&gt; Darwin 
#   Linux   =&gt; Linux 
ifeq ($(OS),Windows_NT)
    export MSYS_NO_PATHCONV=1
endif
</code></pre>

<p>The path conversion is also documented for
<a href="https://www.msys2.org/docs/filesystem-paths/#automatic-unix-windows-path-conversion">MSYS2 at "Filesystem Paths: Automatic Unix ⟶ Windows Path Conversion"</a>
and can be disabled via the <code>MSYS2_ARG_CONV_EXCL</code> environment variable:</p>

<blockquote>
  <p>[...] For these cases you can exclude certain arguments via the <code>MSYS2_ARG_CONV_EXCL</code> environment 
  variable:
  [...]
  <code>MSYS2_ARG_CONV_EXCL</code> can either be * to mean exclude everything, or a list of one ore more 
  arguments prefixes separated by ;, like <code>MSYS2_ARG_CONV_EXCL=--dir=;--bla=;/test</code>. It matches 
  the prefix against the whole argument string.</p>
</blockquote>

<p>I.e. setting the variable as <code>MSYS2_ARG_CONV_EXCL="*"</code> should disable the path conversion 
completely. I myself have never had to use this, though. Using <code>MSYS_NO_PATHCONV</code> was always 
sufficient.</p>

<p><!-- generated -->
<a id='fixing-the-path-conversion-issue-for-winpty'> </a>
<!-- /generated --></p>

<h4>Fixing the path conversion issue for <code>winpty</code></h4>

<p>Unfortunately, <code>winpty</code> suffers from this path conversion issue as well. In the standard 
installation of Git for Windows we can even see this by simply using <code>echo</code>:</p>

<pre><code class="language-text">$ winpty echo /
C:/Program Files/Git
</code></pre>

<p>The behavior is known and flagged as a bug e.g. in
<a href="https://github.com/msys2/MSYS2-packages/issues/411">GH issue #411: Path conversion with and without winpty differs</a>.</p>

<p>Remember the example I gave in section
<a href="#the-role-of-winpty-fixing-the-input-device-is-not-a-tty">The role of <code>winpty</code> e.g. when using <code>docker</code></a>?</p>

<pre><code class="language-text">$ winpty docker run --rm -it busybox sh
/ #
</code></pre>

<p>Now let's extend this and throw a volume into the mix:</p>

<pre><code class="language-text">$ winpty docker run --rm -v foo:/foo -it busybox sh
docker: Error response from daemon: create foo;C: "foo;C" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path.
</code></pre>

<p><code>winpty</code> converts <code>/foo</code> to <code>C:/Program Files/Git/foo</code> so that the volume definition becomes
<code>-v foo:C:/Program Files/Git/foo</code> - which is of course invalid.</p>

<p>Using an additional <code>/</code> as a prefix does work here as well:</p>

<pre><code class="language-text">$ winpty docker run --rm -v foo://foo -it busybox sh
/ #
</code></pre>

<p>But <strong>there is no environment variable that we could use</strong>. The only way to "fix" the path 
conversion is using a 
<a href="https://github.com/rprichard/winpty/releases">newer release of <code>winpty</code></a> and replace the one 
that is shipped together with Git for Windows 
<a href="https://github.com/msys2/MSYS2-packages/issues/411#issuecomment-372585320">as proposed by the maintainer of <code>winpty</code></a>.</p>

<p>This <a href="https://github.com/rprichard/winpty/issues/127#issuecomment-817356202">comment outlines the full process</a>
to replace <code>winpty</code> and is (slightly adapted) as follows:</p>

<pre><code class="language-bash"># create temporary directory
mkdir temp
cd temp
# download a newer release
curl -L https://github.com/rprichard/winpty/releases/download/0.4.3/winpty-0.4.3-msys2-2.7.0-x64.tar.gz --output winpty.tar.gz
# extract the archive
tar -xvf winpty.tar.gz
# copy the content of the bin/ folder to `/usr/bin` 
# (which resolves to e.g `C:/Program Files/Git/usr/bin`; replaces any existing files)
cp winpty-0.4.3-msys2-2.7.0-x64/bin/* /usr/bin
# delete the temporary directory
cd ..
rm -rf temp/
</code></pre>

<video controls>
  <source src="/img/setting-up-git-bash-mingw-msys2-on-windows/winpty-update-posix-path-conversion.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>Once the new version is installed, the path conversion does not happen any longer (even without 
specifying any environment variables).</p>

<p>Related comments:</p>

<ul>
<li><a href="https://github.com/docker/for-win/issues/1588#issuecomment-594938988">https://github.com/docker/for-win/issues/1588#issuecomment-594938988</a></li>
<li><a href="https://github.com/docker/for-win/issues/1588#issuecomment-698080757">https://github.com/docker/for-win/issues/1588#issuecomment-698080757</a></li>
</ul>

<p><strong>Caution</strong>
After <a href="#update-mingw">updating MinGW</a>, the fix for <code>winpty</code> is "gone"!</p>

<video controls>
  <source src="/img/setting-up-git-bash-mingw-msys2-on-windows/winpty-path-fix-gone-after-update.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>I.e. you need to re-run the steps above every time you run an update.</p>

<p><!-- generated -->
<a id='miscellaneous'> </a>
<!-- /generated --></p>

<h2>Miscellaneous</h2>

<p>Some stuff that I need from time to time - not necessarily only relevant for Git Bash.</p>

<p><!-- generated -->
<a id='change-the-bash-custom-prompt-to-a'> </a>
<!-- /generated --></p>

<h3>Change the <code>bash</code> custom prompt to a <code>$</code></h3>

<p>Via <code>PS1=" $"</code>:</p>

<pre><code class="language-text">Pascal@LAPTOP-0DNL2Q02 MINGW64 ~
$ PS1="$ "
$
</code></pre>

<p>See <a href="https://www.cyberciti.biz/tips/howto-linux-unix-bash-shell-setup-prompt.html">How to Change / Set up bash custom prompt (PS1) in Linux</a></p>
]]></description>
                <pubDate>Wed, 12 Apr 2023 14:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/setting-up-git-bash-mingw-msys2-on-windows/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/setting-up-git-bash-mingw-msys2-on-windows/</guid>
            </item>
                    <item>
                <title>Use docker compose for production deployments of a PHP App on GCP [Tutorial Part 9]</title>
                <description><![CDATA[<p>In the ninth part of this tutorial series on developing PHP on Docker we will 
<strong>deploy our dockerized PHP application to a production environment</strong> (a GCP Compute Instance VM)
and <strong>run it via <code>docker compose</code> as a proof of concept</strong>.</p>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/I6gaVR21fnw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch with the final result of this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-9-deploy-docker-compose-php-gcp-poc">part-9-deploy-docker-compose-php-gcp-poc</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/gcp-compute-instance-vm-docker">Create a GCP Compute Instance VM for dockerized PHP Apps</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a> to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#deployment-workflow">Deployment workflow</a>

<ul>
<li><a href="#the-deploy-target">The <code>deploy</code> target</a></li>
<li><a href="#avoiding-code-drift">Avoiding code drift</a></li>
<li><a href="#the-build-info-file">The <code>build-info</code> file</a></li>
<li><a href="#build-and-push-the-docker-images">Build and push the <code>docker</code> images</a></li>
<li><a href="#create-the-deployment-archive">Create the deployment archive</a></li>
<li><a href="#deployment-commands-on-the-vm">Deployment commands on the VM</a>

<ul>
<li><a href="#the-deploy-sh-script">The <code>deploy.sh</code> script</a></li>
</ul></li>
</ul></li>
<li><a href="#codebase-changes">Codebase changes</a>

<ul>
<li><a href="#restructure-the-codebase">Restructure the codebase</a>

<ul>
<li><a href="#the-build-directory">The <code>.build</code> directory</a></li>
<li><a href="#the-secrets-directory">The <code>.secrets</code> directory</a></li>
<li><a href="#the-tutorial-directory">The <code>.tutorial</code> directory</a></li>
<li><a href="#the-infrastructure-directory">The <code>.infrastructure</code> directory</a></li>
</ul></li>
<li><a href="#add-a-gpg-key-for-production">Add a <code>gpg</code> key for production</a></li>
<li><a href="#show-the-build-info">Show the <code>build-info</code></a></li>
<li><a href="#optimize-gitignore">Optimize <code>.gitignore</code></a></li>
</ul></li>
<li><a href="#docker-changes">Docker changes</a>

<ul>
<li><a href="#a-env-file-for-prod">A <code>.env</code> file for <code>prod</code></a></li>
<li><a href="#updating-the-docker-compose-yml-configuration-files">Updating the <code>docker-compose.yml</code> configuration files</a>

<ul>
<li><a href="#docker-compose-local-ci-prod-yml"><code>docker-compose.local.ci.prod.yml</code></a></li>
<li><a href="#docker-compose-local-prod-yml"><code>docker-compose.local.prod.yml</code></a></li>
<li><a href="#docker-compose-prod-yml"><code>docker-compose.prod.yml</code></a></li>
</ul></li>
<li><a href="#adjust-the-dockerignore-file">Adjust the <code>.dockerignore</code> file</a></li>
<li><a href="#build-target-prod">Build target: <code>prod</code></a>

<ul>
<li><a href="#build-stage-prod-in-the-php-base-image">Build stage <code>prod</code> in the <code>php-base</code> image</a>

<ul>
<li><a href="#env-based-branching"><code>ENV</code> based branching</a></li>
<li><a href="#avoid-composer-dev-dependencies">Avoid composer dev dependencies</a></li>
<li><a href="#remove-unnecessary-directories">Remove unnecessary directories</a></li>
<li><a href="#remove-secrets-for-other-environments">Remove secrets for other environments</a></li>
<li><a href="#decrypt-the-secrets-via-entrypoint">Decrypt the secrets via <code>ENTRYPOINT</code></a></li>
<li><a href="#copy-codebase-and-build-info-file">Copy codebase and <code>build-info</code> file</a></li>
</ul></li>
<li><a href="#build-stage-prod-in-the-remaining-images">Build stage <code>prod</code> in the remaining images</a></li>
</ul></li>
</ul></li>
<li><a href="#makefile-changes">Makefile changes</a>

<ul>
<li><a href="#adding-gcp-values-to-make-variables-env">Adding GCP values to <code>.make/variables.env</code></a></li>
<li><a href="#env-based-docker-compose-config">ENV based <code>docker compose</code> config</a></li>
<li><a href="#changes-to-the-git-secret-recipes">Changes to the <code>git-secret</code> recipes</a></li>
<li><a href="#additional-docker-recipes">Additional <code>docker</code> recipes</a></li>
<li><a href="#gcp-recipes">GCP recipes</a></li>
<li><a href="#infrastructure-recipes">Infrastructure recipes</a></li>
<li><a href="#deployment-recipes">Deployment recipes</a></li>
</ul></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>In the previous tutorial
<a href="/blog/gcp-compute-instance-vm-docker/">Create a GCP compute instance VM to run dockerized applications</a>
we have created a Compute Instance VM on GCP and prepared it to run <code>docker</code> containers. For 
this tutorial I made a small adjustment and changed the machine type from <code>e2-micro</code> to
<code>e2-small</code> because we need a little more memory to run the whole application.</p>

<p>In this tutorial, we will use the VM as a <strong>production environment</strong>, i.e. we will</p>

<ul>
<li>prepare our <code>docker</code> setup for production usage</li>
<li>build and push the production-ready <code>docker</code> images to the GCP registry from our local system</li>
<li>pull and start the images on the VM</li>
</ul>

<p>The whole process will be defined <a href="#the-deploy-target">in a single <code>make</code> target called <code>deploy</code></a>.</p>

<p><strong>To try it yourself:</strong></p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Caution</strong>
  </div>
  <div class="panel-body bg-danger">
    The following steps <strong>will create actual infrastructure on GCP</strong> which means you
    will create costs (albeit quite little). Please make sure to shut the project down once you are
    done, see <a href="/blog/gcp-compute-instance-vm-docker/#cleanup">Cleanup</a> of the previous
    tutorial.
  </div>
</div>

<ul>
<li>create an account on GCP, <a href="/blog/gcp-compute-instance-vm-docker/#preconditions-project-and-owner-service-account">a project and a master service account</a>

<ul>
<li><a href="/blog/gcp-compute-instance-vm-docker/#create-service-account-key-file">create a keyfile</a> 
for the service account, name it <code>gcp-master-service-account-key.json</code> and move it to the 
root of the repository</li>
</ul></li>
<li>checkout branch
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-9-deploy-docker-compose-php-gcp-poc">part-9-deploy-docker-compose-php-gcp-poc</a></li>
<li>update the <code>.make/variables.env</code> file with your GCP project id and VM name</li>
<li>initialize local docker setup via

<ul>
<li>copying the secret gpg key to the root of the repository via 
<code>bash
cp .tutorial/secret.gpg.example ./secret.gpg</code></li>
<li>initializing the shared variables via <code>make make-init</code></li>
<li>building the docker setup via <code>make docker-build</code></li>
<li>start the docker setup via <code>make docker-up</code></li>
<li>decrypt the secrets via 
<code>make gpg-init
make secret-decrypt</code></li>
</ul></li>
<li>run the script located at <code>.infrastructure/setup-gcp.sh</code> to create a GCP VM</li>
<li>run <code>make deploy IGNORE_UNCOMMITTED_CHANGES=true</code> to deploy the application</li>
<li>run <code>make deployment-setup-db-on-vm</code> to run the DB migrations</li>
<li>run <code>make gcp-show-ip</code> to retrieve the public IP of the VM and open it in a browser</li>
</ul>

<pre><code class="language-bash">project_id=my-new-project100
vm_name=my-vm-name

git checkout part-9-deploy-docker-compose-php-gcp-poc
sed -i "s/pl-dofroscra-p/${project_id}/g" .make/variables.env
sed -i "s/dofroscra-test/${vm_name}/g" .make/variables.env
cp .tutorial/secret.gpg.example ./secret.gpg
make make-init
make docker-build
make docker-up
make gpg-init
make secret-decrypt
bash .infrastructure/setup-gcp.sh $project_id $vm_name
make deploy IGNORE_UNCOMMITTED_CHANGES=true
make deployment-setup-db-on-vm
echo "http://$(make -s gcp-show-ip)/"
</code></pre>

<p><strong>Note: It can take a couple of minutes until the infrastructure is up and running.</strong></p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Caution</strong>
  </div>
  <div class="panel-body bg-danger">
    Please consider this whole tutorial <strong>only as a POC</strong>! 
    <code>docker compose</code> should not be
    used on a single VM in a production setup, because one huge benefit of docker is the separation
    of services into horizontally scalable containers. Using a single VM would pretty much defeat
    the purpose. <br>
    <br>
    In addition, we will use <code>docker</code> containers for the 
    <code>mysql</code> and <code>redis</code> databases. It would be far better to use 
    managed services like
    <a href ="https://cloud.google.com/memorystore/docs/redis">Memorystore for <code>redis</code></a> and
    <a href ="https://cloud.google.com/sql/docs/mysql">Cloud SQL for <code>mysql</code></a>
    so that we don't have to deal with backups etc. ourselves.
  </div>
</div>

<p><strong>Note:</strong> 
We will tackle those issues and "remove" the POC status in the next part of the tutorial series.</p>

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

<h2>Deployment workflow</h2>

<p>As a precondition we expect <a href="/blog/gcp-compute-instance-vm-docker/#the-actual-vm-creation">that a GCP VM is up and running</a>.
The basic idea of the deployment is:</p>

<ul>
<li>build the <code>docker</code> images using the <code>prod</code> environment and <strong>push</strong> them to the remote registry</li>
<li>log into the VM and <strong>pull</strong> the images</li>
<li>use <code>docker compose</code> on the VM to <strong>start</strong> the <code>docker</code> setup</li>
</ul>

<video controls>
  <source src="/img/deploy-docker-compose-php-gcp-poc/deployment-workflow.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

<p>This shouldn't be too complicated - we already do the same thing locally, don't we? In theory:
Yes. In practice, there is <strong>one major difference: Locally, we have access to our repository</strong>,
including the files for running</p>

<ul>
<li>the <code>docker</code> setup (=> the <code>.docker</code> directory)</li>
<li>the <code>make</code> setup to control the application (=> the <code>Makefile</code> and the <code>.make</code> directory)</li>
</ul>

<p>Fortunately we can solve this issue easily and provide a single <code>make</code> target named <code>deploy</code>
that will take care of everything.</p>

<p><!-- generated -->
<a id='the-deploy-target'> </a>
<!-- /generated --></p>

<h3>The <code>deploy</code> target</h3>

<p>The <code>deploy</code> target runs all necessary commands for a deployment:</p>

<p>Run safeguard checks to <a href="#avoiding-code-drift">avoid code drift</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Switching to 'local' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init
    @printf "$(GREEN)Starting docker setup locally$(NO_COLOR)\n"
    @make --no-print-directory docker-up
    @printf "$(GREEN)Verifying that there are no changes in the secrets$(NO_COLOR)\n"
    @make --no-print-directory gpg-init
    @make --no-print-directory deployment-guard-secret-changes
    @printf "$(GREEN)Verifying that there are no uncommitted changes in the codebase$(NO_COLOR)\n"
    @make --no-print-directory deployment-guard-uncommitted-changes
</code></pre>

<p>Make sure the <a href="#gcp-recipes"><code>gcloud</code> cli is initialized</a> with the GCP deployment service
account and that this account is also used to authenticate docker. Otherwise, we won't be able
to push images to our GCP container registry.</p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Initializing gcloud$(NO_COLOR)\n"
    @make --no-print-directory gcp-init
</code></pre>

<p>Enable the <code>prod</code> environment for the <code>make</code> setup, see section
<a href="#env-based-docker-compose-config">ENV based <code>docker compose</code> config</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Switching to 'prod' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init ENVS="ENV=prod TAG=latest"
</code></pre>

<p>Create <a href="#the-build-info-file">the <code>build-info</code> file</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Creating build information file$(NO_COLOR)\n"
    @make --no-print-directory deployment-create-build-info-file
</code></pre>

<p><a href="#build-and-push-the-docker-images">Build and push the <code>docker</code> images</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Building docker images$(NO_COLOR)\n"
    @make --no-print-directory docker-build
    @printf "$(GREEN)Pushing images to the registry$(NO_COLOR)\n"
    @make --no-print-directory docker-push
</code></pre>

<p><a href="#create-the-deployment-archive">Create the deployment archive</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Creating the deployment archive$(NO_COLOR)\n"
    @make deployment-create-tar
</code></pre>

<p><a href="#deployment-commands-on-the-vm">Run deployment commands on the VM</a></p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Copying the deployment archive to the VM and run the deployment$(NO_COLOR)\n"
    @make --no-print-directory deployment-run-on-vm
</code></pre>

<p>Cleanup the deployment by removing the local deployment archive and enabling the default
environment  (<code>local</code>) for the <code>make</code> setup.</p>

<pre><code class="language-Makefile">    @printf "$(GREEN)Clearing deployment archive$(NO_COLOR)\n"
    @make --no-print-directory deployment-clear-tar
    @printf "$(GREEN)Switching to 'local' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init
</code></pre>

<p><!-- generated -->
<a id='avoiding-code-drift'> </a>
<!-- /generated --></p>

<h3>Avoiding code drift</h3>

<p>The term "code drift" is derived from <a href="https://coder.com/blog/what-is-configuration-drift">configuration drift</a>,
which indicates the (subtle) differences in configuration between environments:</p>

<blockquote>
  <p>If you've ever heard an engineer lamenting (or sometimes arrogantly proclaiming)
  "well, it works on my machine" then you have been witness to configuration drift.</p>
</blockquote>

<p>In our case it refers to <strong>differences between our git repository and the code in the docker
images</strong> as well as <strong>changes between the decrypted and encrypted secret files</strong>. These problems
can occur, because we are currently
<strong>executing the deployment from our local machine</strong> and we might have made some
<strong>changes in the codebase</strong> when we build the docker images that are not yet reflected in git. The
build context sent to the <code>docker</code> daemon would then be different from the git repository resp.
the encrypted <code>.secret</code> files. This can lead to all sorts of hard-to-debug quirks and should
thus be avoided.</p>

<p>When we deploy later <strong>from the CI pipelines, those problems simply won't occur</strong>, because the whole
<strong>codebase will be identical with the git repository</strong> - but I really do NOT want to lose the
ability to deploy code from my local system (devs that went through Gitlab / Github downtimes
will understand...)</p>

<p>Corresponding checks are implemented via the <code>deployment-guard-uncommitted-changes</code> and
<code>deployment-guard-secret-changes</code> targets that exit with <code>exit 1</code> (a non-zero status code) which in
turn makes the <a href="#the-deploy-target"><code>deploy</code> target</a> stop/fail.</p>

<pre><code class="language-Makefile">IGNORE_SECRET_CHANGES?=

.PHONY: deployment-guard-secret-changes
deployment-guard-secret-changes: ## Check if there are any changes between the decrypted and encrypted secret files
    if ( ! make secret-diff || [ "$$(make secret-diff | grep ^@@)" != "" ] ) &amp;&amp; [ "$(IGNORE_SECRET_CHANGES)" == "" ] ; then \
        printf "Found changes in the secret files =&gt; $(RED)ABORTING$(NO_COLOR)\n\n"; \
        printf "Use with IGNORE_SECRET_CHANGES=true to ignore this warning\n\n"; \
        make secret-diff; \
        exit 1; \
    fi
    @echo "No changes in the secret files!"
</code></pre>

<p><a href="/blog/git-secret-encrypt-repository-docker/#makefile-adjustments"><code>make secret-diff</code></a> is used
to check for differences between decrypted and encrypted secrets.</p>

<p><code>! make secret-diff</code> checks if the commands exits with a non-zero exit code. This happens for
instance, when the secrets have not been decrypted yet. The error is</p>

<pre><code class="language-gitignore">git-secret: abort: file not found. Consider using 'git secret reveal': &lt;missing-file&gt;
</code></pre>

<p>If the command doesn't fail, the changes are displayed in a <code>diff</code> format, e.g.</p>

<pre><code class="language-diff"> --- /dev/fd/63
 +++ /var/www/app/.secrets/shared/passwords.txt
 @@ -1 +1,2 @@
  my_secret_password
 +1
 foo
</code></pre>

<p>We use <code>grep ^@@</code> to check the existence of a "line that starts with @@" to identify a change.
If no changes are found, the info <code>"No changes in the secret files!"</code> is printed. Otherwise, a
warning is shown. The check an be suppressed by passing <code>IGNORE_SECRET_CHANGES=true</code>.</p>

<pre><code class="language-Makefile">IGNORE_UNCOMMITTED_CHANGES?=

.PHONY: deployment-guard-uncommitted-changes
deployment-guard-uncommitted-changes: ## Check if there are any git changes and abort if so. The check can be ignore by passing `IGNORE_UNCOMMITTED_CHANGES=true`
    if [ "$$(git status -s)" != "" ] &amp;&amp; [ "$(IGNORE_UNCOMMITTED_CHANGES)" == "" ] ; then \
        printf "Found uncommitted changes in git =&gt; $(RED)ABORTING$(NO_COLOR)\n\n"; \
        printf "Use with IGNORE_UNCOMMITTED_CHANGES=true to ignore this warning\n\n"; \
        git status -s; \
        exit 1; \
    fi
    @echo "No uncommitted changes found!"
</code></pre>

<p>For <code>deployment-guard-uncommitted-changes</code> we use <code>git status -s</code> to check for any uncommitted
changes. If no changes are found the info <code>"No uncommitted changes found!"</code> is printed.
Otherwise, a warning is shown. The check an be suppressed by passing
<code>IGNORE_UNCOMMITTED_CHANGES=true</code>.</p>

<p><!-- generated -->
<a id='the-build-info-file'> </a>
<!-- /generated --></p>

<h3>The <code>build-info</code> file</h3>

<p>When testing the deployments I often needed to identify small bugs in the code. The more complex
the whole process gets, the more things can go wrong and the more "stuff needs to be checked".
One of them is the <a href="#avoiding-code-drift">code drift mentioned in the previous section</a>, btw.</p>

<p>To make my life a little easier, <strong>I added a file called <code>build-info</code> that contains information
about the build</strong> and will be stored in the docker images - allowing me to inspect the file
later, see also section <a href="#show-the-build-info">Show the <code>build-info</code></a>.</p>

<p>The file is created via <code>deployment-create-build-info-file</code> target</p>

<pre><code class="language-Makefile">.PHONY: deployment-create-build-info-file
deployment-create-build-info-file: ## Create a file containing version information about the codebase
    @echo "BUILD INFO" &gt; ".build/build-info"
    @echo "==========" &gt;&gt; ".build/build-info"
    @echo "User  :" $$(whoami) &gt;&gt; ".build/build-info"
    @echo "Date  :" $$(date --rfc-3339=seconds) &gt;&gt; ".build/build-info"
    @echo "Branch:" $$(git branch --show-current) &gt;&gt; ".build/build-info"
    @echo "" &gt;&gt; ".build/build-info"
    @echo "Commit" &gt;&gt; ".build/build-info"
    @echo "------" &gt;&gt; ".build/build-info"
    @git log -1 --no-color &gt;&gt; ".build/build-info"
</code></pre>

<p>The file is created on the host system under <code>.build/build-info</code> and then
<a href="#copy-codebase-and-build-info-file">copied to <code>./build-info</code> in the <code>Dockerfile</code> of the <code>php-base</code> image</a>.
To execute a shell command via <code>$(command)</code>,
<a href="https://stackoverflow.com/a/26564874/413531">the <code>$</code> has to be escaped with another <code>$</code></a>, to
not be interpreted by <code>make</code> as a variable. Example:</p>

<pre><code class="language-Makefile">some-target:
    $$(command)
</code></pre>

<p>FYI: I learned that
<a href="https://stackoverflow.com/a/54068252/413531"><code>make</code> converts all new lines in spaces when they are echo'd</a>
because I initially used</p>

<pre><code class="language-text">@echo $$(git log -1 --no-color) &gt;&gt; ".build/build-info"
</code></pre>

<p>instead of</p>

<pre><code class="language-text">@git log -1 --no-color &gt;&gt; ".build/build-info"
</code></pre>

<p>which would remove all new lines.</p>

<p>A final file <code>build-info</code> file looks like this:</p>

<pre><code class="language-text">BUILD INFO
==========
User  : Pascal
Date  : 2022-05-22 17:10:21+02:00
Branch: part-9-deploy-docker-compose-php-gcp-poc

Commit
------
commit c47464536613874d192696d93d3c97b138c7a6be
Author: Pascal Landau &lt;pascal.landau@googlemail.com&gt;
Date:   Sun May 22 17:10:15 2022 +0200

    Testing the new `build-info` file

</code></pre>

<p><!-- generated -->
<a id='build-and-push-the-docker-images'> </a>
<!-- /generated --></p>

<h3>Build and push the <code>docker</code> images</h3>

<p><code>make</code> is initialized with <code>ENV=prod</code>, i.e. calling <code>make docker-build</code> will use
<a href="#env-based-docker-compose-config">the correct <code>docker compose</code> config</a> for <strong>building</strong> production
images. In addition, we have adjusted the <code>DOCKER_REGISTRY</code> to <code>gcr.io/pl-dofroscra-p</code> in the
<a href="#adding-gcp-values-to-make-variables-env">.make/variables.env file</a>, so that the images will
immediately be <a href="/blog/gcp-compute-instance-vm-docker/#pushing-images-to-the-registry">tagged correctly</a>
as</p>

<pre><code class="language-text">gcr.io/pl-dofroscra-p/dofroscra/$service-prod

# e.g. for `php-base`

gcr.io/pl-dofroscra-p/dofroscra/php-base-prod
</code></pre>

<p><strong>Example:</strong></p>

<pre><code class="language-text">$ make docker-build
ENV=prod TAG=latest DOCKER_REGISTRY=gcr.io/pl-dofroscra-p DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_prod --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
#1 [internal] load build definition from Dockerfile
# ...

$ docker image ls 
REPOSITORY                                          TAG     IMAGE ID       CREATED          SIZE
gcr.io/pl-dofroscra-p/dofroscra/php-fpm-prod        latest  2be3bec977de   24 seconds ago   147MB
gcr.io/pl-dofroscra-p/dofroscra/php-worker-prod     latest  6dbf14d1b329   25 seconds ago   181MB
gcr.io/pl-dofroscra-p/dofroscra/php-base-prod       latest  9164976a78a6   32 seconds ago   130MB
gcr.io/pl-dofroscra-p/dofroscra/application-prod    latest  377fdee0f12a   32 seconds ago   130MB
gcr.io/pl-dofroscra-p/dofroscra/nginx-prod          latest  42dd1608d126   24 seconds ago   23.5MB
</code></pre>

<p>Thanks to the image name, we can also immediately <strong>push</strong> the images to the remote registry via
<code>make docker-push</code>. Note, that we see a lot of <code>Layer already exists</code> infos in the console
output for the <code>php-fpm</code> and <code>php-worker</code> images. This is due to the fact that we use
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#php-images">a common <code>php-base</code> base image</a>
for <code>application</code>, <code>php-fpm</code> and <code>php-worker</code>, i.e. <strong>those images have a lot of layers in
common</strong> and only the layers of <code>application</code> are pushed. <code>docker</code> uses the
<a href="https://stackoverflow.com/questions/36339514/how-docker-calculates-the-hash-of-each-layer-is-it-deterministic">layer hash</a>
to identify which layers already exist.</p>

<pre><code class="language-text">$ make docker-push
ENV=prod TAG=latest DOCKER_REGISTRY=gcr.io/pl-dofroscra-p DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_prod --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.prod.yml -f ./.docker/docker-compose/docker-compose.local.prod.yml -f ./.docker/docker-compose/docker-compose.prod.yml push
mysql Skipped
redis Skipped
Pushing application: c8f4416c4383 Preparing
#...
Pushing application: 6bbfa8829d07 Pushing [==================================================&gt;]  3.584kB
#...
Pushing php-worker: 6bbfa8829d07 Layer already exists
#...
Pushing php-fpm: 6bbfa8829d07 Layer already exists
</code></pre>

<p><!-- generated -->
<a id='create-the-deployment-archive'> </a>
<!-- /generated --></p>

<h3>Create the deployment archive</h3>

<p>As described in the <a href="#deployment-workflow">introduction of the Deployment workflow</a>, we need to
<strong>make our <code>make</code> and <code>docker</code> setup somehow available on the VM</strong>. We will solve this issue by
<strong>creating a <code>tar</code> archive with all necessary files locally</strong> and
<a href="#deployment-commands-on-the-vm">transfer it to the VM</a>.</p>

<p>The archive is created via the <code>deployment-create-tar</code> target</p>

<pre><code class="language-Makefile">.PHONY: deployment-create-tar
deployment-create-tar:
    # create the build directory
    rm -rf .build/deployment
    mkdir -p .build/deployment
    # copy the necessary files
    mkdir -p .build/deployment/.docker/docker-compose/
    cp -r .docker/docker-compose/ .build/deployment/.docker/
    cp -r .make .build/deployment/
    cp Makefile .build/deployment/
    cp .infrastructure/scripts/deploy.sh .build/deployment/
    # make sure we don't have any .env files in the build directory (don't wanna leak any secrets) ...
    find .build/deployment -name '.env' -delete
    # ... apart from the .env file we need to start docker
    cp .secrets/prod/docker.env .build/deployment/.docker/.env
    # create the archive
    tar -czvf .build/deployment.tar.gz -C .build/deployment/ ./
</code></pre>

<p>The recipe uses the <code>.build/deployment</code> directory as a temporary location to store all necessary
files, i.e.</p>

<ul>
<li>the <code>docker compose</code> config files in <code>.docker/docker-compose/</code></li>
<li>the <code>Makefile</code> and the <code>.make</code> directory in the root of the codebase for the <code>make</code> setup</li>
<li>the <code>.infrastructure/scripts/deploy.sh</code> script to run the deployment</li>
</ul>

<p>In addition, we copy <a href="#a-env-file-for-prod">the <code>.secrets/prod/docker.env</code> file</a> to use it as the
<code>.env</code> file for <code>docker compose</code>. <strong>Caution</strong>: This only works, because we have
<a href="#avoiding-code-drift">verified previously that there are no changes between the decrypted and encrypted .secret files</a>
(which also means that <code>.secrets/prod/docker.env</code> is already decrypted).</p>

<p>Once all files are copied, the whole directory is added to the <code>.build/deployment.tar.gz</code>
archive via</p>

<pre><code class="language-bash">tar -czvf .build/deployment.tar.gz -C .build/deployment/ ./
</code></pre>

<p>The <code>-C .build/deployment/</code> option makes sure that
<a href="https://serverfault.com/a/330133">the directory structure is retained when extracting the archive</a>.
For the remaining options take a look at
<a href="https://www.cyberciti.biz/faq/how-to-create-tar-gz-file-in-linux-using-command-line/">How to create tar.gz file in Linux using command line</a>.</p>

<p><!-- generated -->
<a id='deployment-commands-on-the-vm'> </a>
<!-- /generated --></p>

<h3>Deployment commands on the VM</h3>

<p>Once the <a href="#create-the-deployment-archive">creation of the deployment archive</a> is done, we can
transfer the resulting <code>.build/deployment.tar.gz</code> file to the VM, extract it and run the deployment
script. All of that is done via the <code>deployment-run-on-vm</code> target</p>

<pre><code class="language-Makefile"># directory on the VM that will contain the files to start the docker setup
CODEBASE_DIRECTORY=/tmp/codebase

.PHONY: deployment-run-on-vm
deployment-run-on-vm:## Run the deployment script on the VM
    "$(MAKE)" -s gcp-scp-command SOURCE=".build/deployment.tar.gz" DESTINATION="deployment.tar.gz"
    "$(MAKE)" -s gcp-ssh-command COMMAND="sudo rm -rf $(CODEBASE_DIRECTORY) &amp;&amp; sudo mkdir -p $(CODEBASE_DIRECTORY) &amp;&amp; sudo tar -xzvf deployment.tar.gz -C $(CODEBASE_DIRECTORY) &amp;&amp; cd $(CODEBASE_DIRECTORY) &amp;&amp; sudo bash deploy.sh"
</code></pre>

<p>Under the hood, the target uses the
<a href="#gcp-recipes"><code>gcp-scp-command</code> and <code>gcp-ssh-command</code></a> targets. The deployment archive is
extracted in <code>/tmp/codebase</code> via</p>

<pre><code class="language-bash">sudo rm -rf /tmp/codebase &amp;&amp; sudo mkdir -p /tmp/codebase &amp;&amp; sudo tar -xzvf deployment.tar.gz -C /tmp/codebase
</code></pre>

<p>and then the deployment script is executed</p>

<pre><code class="language-bash">cd /tmp/codebase &amp;&amp; sudo bash deploy.sh
</code></pre>

<p>All of those commands are run <strong>in a single invocation of <code>gcp-ssh-command</code></strong>, because there's a
certain overhead involved when tunneling commands via IAP, i.e. each invocation takes a couple
of seconds.</p>

<p><!-- generated -->
<a id='the-deploy-sh-script'> </a>
<!-- /generated --></p>

<h4>The <code>deploy.sh</code> script</h4>

<p>The actual deployment is done "on the VM" via the <a href="#the-infrastructure-directory"><code>.infrastructure/scripts/deploy.sh</code> script</a></p>

<pre><code class="language-bash">#!/usr/bin/env bash

echo "Retrieving secrets"
make gcp-secret-get SECRET_NAME=GPG_KEY &gt; secret.gpg
GPG_PASSWORD=$(make gcp-secret-get SECRET_NAME=GPG_PASSWORD)
echo "Creating compose-secrets.env file"
echo "GPG_PASSWORD=$GPG_PASSWORD" &gt; compose-secrets.env
echo "Initializing the codebase"
make make-init ENVS="ENV=prod TAG=latest"
echo "Pulling images on the VM from the registry"
make docker-pull
echo "Stop the containers on the VM"
make docker-down || true
echo "Start the containers on the VM"
make docker-up
</code></pre>

<p>The script is located at the root of the codebase and</p>

<ul>
<li>will first retrieve the <code>GPG_KEY</code> and the <code>GPG_PASSWORD</code> values that we
<a href="/blog/gcp-compute-instance-vm-docker/#add-the-secret-gpg-key-and-password">created previously in the Secret Manager</a>

<ul>
<li>the <code>GPG_KEY</code> is stored in the file <code>secret.gpg</code> in the root of the codebase so that it is
<a href="/blog/git-secret-encrypt-repository-docker/#local-git-secret-and-gpg-setup">picked up automatically when initializing <code>gpg</code></a>
> [...]
> and the private key has to be <strong>named secret.gpg</strong> and put in the <strong>root of the codebase</strong>.</li>
<li>the <code>GPG_PASSWORD</code> is stored in the bash variable <code>$GPG_PASSWORD</code> and from there stored in
a file called <code>compose-secrets.env</code> as
<code>dotenv
GPG_PASSWORD=$GPG_PASSWORD</code>
This file is used in the
<a href="#docker-compose-prod-yml"><code>docker-compose.prod.yml</code> file as <code>env_file</code> for the php docker containers</a>,
so that the <code>GPG_PASSWORD</code> becomes available in the
<a href="#decrypt-the-secrets-via-entrypoint"><code>decrypt-secrets.sh</code> script used in the <code>ENTRYPOINT</code></a>
of the <code>php-base</code> image</li>
</ul></li>
<li>then the <code>make</code> setup is initialized for the <code>prod</code> environment via
<code>bash
make-init ENVS="ENV=prod TAG=latest"</code>
so that all subsequent <code>docker-*</code> targets
<a href="#env-based-docker-compose-config">use the correct configuration</a></li>
<li>and finally, the <code>docker</code> images we pushed in step
<a href="#build-and-push-the-docker-images">"Build and push the <code>docker</code> images"</a> are <strong>pulled</strong>, any
running containers are <strong>stopped</strong> and the whole <code>docker</code> setup is <strong>started</strong> with the new
images

<ul>
<li>FYI: <code>docker compose down</code> would fail (exit with a non-zero status code) if no containers are
running. Since this is fine for us (we simply want to ensure that no containers are running),
the command is OR'd via <code>make docker-down || true</code> so that the script won't stop if that
happens.</li>
</ul></li>
</ul>

<p><!-- generated -->
<a id='codebase-changes'> </a>
<!-- /generated --></p>

<h2>Codebase changes</h2>

<p>Before we dive into the <code>docker</code> stuff, let's quickly talk about some cleanup work in the 
codebase that you can get via
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-9-deploy-docker-compose-php-gcp-poc">part-9-deploy-docker-compose-php-gcp-poc</a>.</p>

<p><!-- generated -->
<a id='restructure-the-codebase'> </a>
<!-- /generated --></p>

<h3>Restructure the codebase</h3>

<p><!-- generated -->
<a id='the-build-directory'> </a>
<!-- /generated --></p>

<h4>The <code>.build</code> directory</h4>

<p>We already know this directory from the previous tutorial where we used it as a temporary directory
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#create-a-junit-report-from-phpunit">to collect build artifacts from the CI pipeline</a>.
Now, we will make use of it again as a temporary directory to</p>

<ul>
<li>prepare <a href="#create-the-deployment-archive">the creation of the deployment archive</a></li>
<li>create a <a href="#the-build-info-file"><code>build-info</code> file</a> to pass it to the <code>docker</code> daemon in the build context</li>
</ul>

<p>The files in the directory are ignored via <code>.gitignore</code> as they are only temporarily required</p>

<pre><code class="language-gitignore"># File: .gitignore

.build/*
</code></pre>

<p>However, since the <code>build-info</code> file must be passed to docker, we will have a slight deviation 
between the <code>.gitignore</code> and the <code>.dockerignore</code> file.</p>

<pre><code class="language-dockerignore"># File: .dockerignore

.build/*

# kept files
!.build/build-info
</code></pre>

<p>I'm mentioning this here specifically, because we usually 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#dockerignore">strive for a parity between <code>.gitignore</code> and <code>.dockerignore</code></a>.</p>

<p><!-- generated -->
<a id='the-secrets-directory'> </a>
<!-- /generated --></p>

<h4>The <code>.secrets</code> directory</h4>

<p>Since we will store <em>all</em> secrets for <em>all</em> environments in the codebase, <strong>we will organize them 
by environment</strong> as subdirectories in a new <code>.secrets</code> directory:</p>

<pre><code class="language-text">.secrets/
├── ci
│   └── ci-secret.txt.secret
├── prod
│   ├── app.env.secret
│   └── docker.env.secret
└── shared
    └── passwords.txt.secret
</code></pre>

<p>This will also make it easier to 
<a href="#remove-secrets-for-other-environments">pick the correct files per environment when building the docker image</a>
and 
<a href="#decrypt-the-secrets-via-entrypoint">select them for decryption in the <code>ENTRYPOINT</code></a>.</p>

<p>The <code>.secrets/shared/</code> directory contains all secret files that are required by <em>all</em> 
environments, whereas <code>.secrets/ci/</code> contains only <code>ci</code> secrets and <code>.secrets/prod/</code> contains only 
<code>prod</code> secrets, respectively.</p>

<p>In our codebase there are already <strong>two files that contain actual secrets</strong>: The <code>.env</code> file and 
the <code>.docker/.env</code> file. Both of them contain the credentials for <code>mysql</code> and <code>redis</code>, and the 
<code>.env</code> file also contains the <code>APP_KEY</code> that 
<a href="https://tighten.com/blog/app-key-and-you/">is used by Laravel to encrypt cookies</a></p>

<blockquote>
  <p>Laravel uses the [<code>APP_KEY</code>] for all encrypted cookies, including the session cookie, before 
  handing them off to the user's browser, and it uses it to decrypt cookies read from the browser.</p>
</blockquote>

<pre><code class="language-dotenv"># File: .env

APP_KEY=base64:C8X1hLE2bpok8OS+bJ1cTB9wNASJNRLibqUrDq2ls4Q=
DB_PASSWORD=production_secret_mysql_root_password
REDIS_PASSWORD=production_secret_redis_password
</code></pre>

<pre><code class="language-dotenv"># File: .docker/.env

MYSQL_PASSWORD=production_secret
MYSQL_ROOT_PASSWORD=production_secret_mysql_root_password
REDIS_PASSWORD=production_secret_redis_password
</code></pre>

<p>I have created the encrypted <code>.secret</code> files by moving the unencrypted files to the <code>.secrets/</code> 
directory and made sure to add them to the <code>.gitignore</code> file with the rules</p>

<pre><code class="language-gitignore"># ...

.secrets/*/*
!**/*.secret
</code></pre>

<p>Then I ran</p>

<pre><code class="language-bash">make secret-add FILES=".secrets/*/*"

make secret-encrypt
</code></pre>

<pre><code class="language-text">$ make secret-add FILES=".secrets/*/*"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="add .secrets/*/*"
git-secret: 4 item(s) added.

$ make secret-encrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="hide"
git-secret: done. 4 of 4 files are hidden.
</code></pre>

<p><!-- generated -->
<a id='the-tutorial-directory'> </a>
<!-- /generated --></p>

<h4>The <code>.tutorial</code> directory</h4>

<p>I have mentioned before, that 
<a href="/blog/git-secret-encrypt-repository-docker/#introduction">I would normally not store secret <code>gpg</code> keys in the repository</a>,
but I'm still doing it in this tutorial so that it's easier to follow along. To make clear 
which files are affected by this "exception to the rule", I have moved them in a dedicated 
<code>.tutorial</code> directory:</p>

<pre><code class="language-text">.tutorial/
├── secret-ci-protected.gpg.example
├── secret-production-protected.gpg.example
└── secret.gpg.example
</code></pre>

<p><!-- generated -->
<a id='the-infrastructure-directory'> </a>
<!-- /generated --></p>

<h4>The <code>.infrastructure</code> directory</h4>

<p>The <code>.infrastructure</code> directory contains all files that are used to manage the infrastructure and 
deployments. The directory was introduced in the 
<a href="/blog/gcp-compute-instance-vm-docker/">previous part</a> and looks as follows</p>

<pre><code class="language-text">.infrastructure/
├── scripts/
│     ├── deploy.sh
│     └── provision.sh
└── setup-gcp.sh
</code></pre>

<ul>
<li>the <code>scripts</code> directory contains files that are transferred to and then executed on the VM

<ul>
<li><code>deploy.sh</code> is a script to <strong>perform all necessary deployment steps on the VM</strong> and is explained 
in more detail in section <a href="#deployment-commands-on-the-vm">Deployment commands on the VM</a></li>
<li><code>provision.sh</code> is a script to <strong>install <code>docker</code> and <code>docker compose</code> on the VM</strong> and contains 
the commands that are explained in section <a href="/blog/gcp-compute-instance-vm-docker/#installing-docker-and-docker-compose">Installing <code>docker</code> and <code>docker compose</code></a></li>
</ul></li>
<li><code>setup-gcp.sh</code> is run to set up a VM initially and was described in more detail under
<a href="/blog/gcp-compute-instance-vm-docker/#putting-it-all-together">Putting it all together</a></li>
</ul>

<p><!-- generated -->
<a id='add-a-gpg-key-for-production'> </a>
<!-- /generated --></p>

<h3>Add a <code>gpg</code> key for production</h3>

<p>We have created a new <code>gpg</code> key pair for the <code>prod</code> environment 
when <a href="#add-a-gpg-key-for-production">setting up the VM on GCP</a>. The secret key is located
at <code>.tutorial/secret-production-protected.gpg.example</code> and the public key at 
<code>.dev/gpg-keys/production-public.gpg</code>.</p>

<p><!-- generated -->
<a id='show-the-build-info'> </a>
<!-- /generated --></p>

<h3>Show the <code>build-info</code></h3>

<p>As part of the deployment, we generate a <a href="#the-build-info-file"><code>build-info</code> file</a> that allows 
us to understand "which version of the codebase lives inside a container". This file is located 
<strong>at the root of the repository</strong>, and we expose it as the web route <code>/info</code> (via the <code>php-fpm</code> 
container) and as the command <code>info</code> (via the <code>application</code> container).</p>

<p><strong>routes/web.php</strong></p>

<pre><code class="language-php"># ...

Route::get('/info', function () {
    $info = file_get_contents(__DIR__."/../build-info");
    return new \Illuminate\Http\Response($info, 200, ["Content-type" =&gt; "text/plain"]);
});
</code></pre>

<p>Show via <code>curl http://localhost/info</code></p>

<p><strong>routes/console.php</strong></p>

<pre><code class="language-php"># ...

Artisan::command('info', function () {
    $info = file_get_contents(__DIR__."/../build-info");
    $this-&gt;line($info);
})-&gt;purpose('Display build information about the codebase');
</code></pre>

<p>Show via <code>php artisan info</code></p>

<p><!-- generated -->
<a id='optimize-gitignore'> </a>
<!-- /generated --></p>

<h3>Optimize <code>.gitignore</code></h3>

<p>Laravel uses multiple <code>.gitignore</code> files to retain a directory structure, because 
<a href="https://stackoverflow.com/a/115992/413531">empty directories cannot be added to git</a>. 
<a href="https://stackoverflow.com/a/53208503/413531">This is a valid strategy</a>, but it makes 
<a href="https://stackoverflow.com/a/29695734/413531">understanding "what is actually ignored" more complex</a>.</p>

<p>In addition, it makes it harder to 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#dockerignore">keep <code>.gitignore</code> and <code>.dockerignore</code> in sync</a>,
because we can't simply "copy the contents of the <code>./.gitignore</code>" any longer as it might not 
contain all rules.</p>

<p>Thus, I have identified all directory-specific <code>.gitignore</code> files via</p>

<pre><code class="language-bash">find . -path ./vendor -prune -o -name .gitignore -print
</code></pre>

<pre><code class="language-text">find . -path ./vendor -prune -o -name .gitignore -print
./.gitignore
./bootstrap/cache/.gitignore
./database/.gitignore
./storage/app/.gitignore
./storage/app/public/.gitignore
./storage/framework/.gitignore
./storage/framework/cache/.gitignore
./storage/framework/cache/data/.gitignore
./storage/framework/sessions/.gitignore
./storage/framework/testing/.gitignore
./storage/framework/views/.gitignore
./storage/logs/.gitignore
</code></pre>

<p><strong>CAUTION</strong>: Usually, the files simply contain the rules</p>

<pre><code class="language-gitignore">*
!.gitignore
</code></pre>

<p>The only exception is <code>./database/.gitignore</code> which contains</p>

<pre><code class="language-gitignore">*.sqlite*
</code></pre>

<p>TBH, I would consider this rather a bug, because this rule SHOULD actually live the main 
<code>.gitignore</code> file. But for us it means, that we need to keep this rule as</p>

<pre><code class="language-gitignore">database/*.sqlite*
</code></pre>

<p>Before we add them to the <code>.gitignore</code> file, it is important to understand 
<a href="https://git-scm.com/docs/gitignore#_pattern_format">how <code>git</code> handles the <code>gitignore</code> file</a></p>

<blockquote>
  <p>An optional prefix "<code>!</code>" which negates the pattern; any matching file excluded by a previous 
  pattern will become included again. <strong>It is not possible to re-include a file if a parent 
  directory of that file is excluded</strong>. Git doesn’t list excluded directories for performance 
  reasons, so any patterns on contained files have no effect, no matter where they are defined.</p>
</blockquote>

<p>In other words: Consider the following structure</p>

<pre><code class="language-text">storage/
└── app
    ├── .gitignore
    └── public
        └── .gitignore
</code></pre>

<p>We might be tempted to write the rules as</p>

<pre><code class="language-gitignore">storage/app/*
!storage/app/.gitignore
!storage/app/public/.gitignore
</code></pre>

<p>But that won't work as expected:
<code>storage/app/*</code> ignores "everything" in the <code>storage/app/</code> directory, so that <code>git</code> <strong>wouldn't even 
look</strong> into the <code>storage/app/public/</code> directory and thus wouldn't find the 
<code>storage/app/public/.gitignore</code> file! In consequence, the rule <code>!storage/app/public/.gitignore</code> 
doesn't have any affect and the file would <em>not</em> be added to the git repository.</p>

<p>Instead, I need to allow the <code>public</code> directory explicitly by adding the rule 
<code>!storage/app/public/</code> and ignore all files in it apart from the <code>.gitignore</code> file via 
<code>storage/app/public/*</code>. So we are essentially saying:</p>

<ul>
<li>ignore all files in the <code>storage/app/</code> directory</li>
<li>but NOT the <code>storage/app/public/</code> directory ITSELF</li>
<li>though do still ignore all files IN the <code>storage/app/public/</code> directory</li>
<li>but NOT the <code>storage/app/public/.gitignore</code> file</li>
</ul>

<pre><code class="language-gitignore"># ignore all files in the storage/app/ directory
storage/app/*
!storage/app/.gitignore
# but NOT the storage/app/public/ directory ITSELF
!storage/app/public/
# do still ignore all files IN storage/app/public/
storage/app/public/*
# but NOT the storage/app/public/.gitignore file
!storage/app/public/.gitignore
</code></pre>

<p>We can simplify the rules a little more via <code>!**.gitignore</code>, i.e. <em>all</em> <code>gitignore</code> files in any 
directory should be included</p>

<pre><code class="language-gitignore">storage/app/*
!storage/app/public/
storage/app/public/*

!**.gitignore
</code></pre>

<p>So the final rules for all directory-specific <code>.gitignore</code> files become</p>

<pre><code>bootstrap/cache/*
database/*.sqlite*
storage/app/*
!storage/app/public/
storage/app/public/*
storage/framework/*
!storage/framework/cache/
storage/framework/cache/*
!storage/framework/cache/data/
storage/framework/cache/data/*
!storage/framework/sessions/
storage/framework/sessions/*
!storage/framework/testing/
storage/framework/testing/*
!storage/framework/views/
storage/framework/views/*
!storage/framework/logs/
storage/framework/logs/*

!**.gitignore
</code></pre>

<p><strong>Caution</strong>: If Laravel changes the rules for their directory-specific <code>.gitignore</code> files, we 
must adjust our rules as well! Luckily this usually only happens on major version upgrades, though.</p>

<p>The full <code>.gitignore</code> file becomes</p>

<pre><code class="language-gitignore">**/*.env*
!.env.example
!.make/variables.env
.idea
.phpunit.result.cache
vendor/
secret.gpg
.gitsecret/keys/random_seed
.gitsecret/keys/pubring.kbx~
.secrets/*/*
!**/*.secret
gcp-service-account-key.json
gcp-master-service-account-key.json

# =&gt; directory-specific .gitignore files by Laravel
bootstrap/cache/*
database/*.sqlite*
storage/app/*
!storage/app/public/
storage/app/public/*
storage/framework/*
!storage/framework/cache/
storage/framework/cache/*
!storage/framework/cache/data/
storage/framework/cache/data/*
!storage/framework/sessions/
storage/framework/sessions/*
!storage/framework/testing/
storage/framework/testing/*
!storage/framework/views/
storage/framework/views/*
!storage/framework/logs/
storage/framework/logs/*

# =&gt; directory-specific .gitignore files from us
.build/*

# =&gt; keep ALL .gitignore files
!**.gitignore
</code></pre>

<p>It will be <a href="#adjust-the-dockerignore-file">"synced" later with the <code>.dockerignore</code> file</a>.</p>

<p><!-- generated -->
<a id='docker-changes'> </a>
<!-- /generated --></p>

<h2>Docker changes</h2>

<p>For this tutorial we will use <code>docker compose</code> to <strong>build the containers</strong> as well as to 
<strong>run them "in production"</strong>, i.e. on the GCP VM. The <code>docker compose</code> configuration will 
essentially be a combination of the <code>local</code> and <code>ci</code> config from the previous tutorials:</p>

<ul>
<li>We will need all the services that 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker">we already know from the <strong><code>local</code> environment</strong></a>,
i.e.
<a href="/img/docker-from-scratch-for-php-applications-in-2022/docker-images.PNG"><img src="/img/docker-from-scratch-for-php-applications-in-2022/docker-images.PNG" alt="Docker images" title="Docker images" /></a>

<ul>
<li><code>nginx</code></li>
<li><code>mysql</code></li>
<li><code>redis</code></li>
<li><code>php-worker</code></li>
<li><code>php-fpm</code></li>
<li><code>application</code></li>
</ul></li>
<li>The codebase will be baked into the image and we will <strong>bind mount a <code>gpg</code> secret key</strong> at 
runtime to decrypt the secrets - exactly 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#docker-compose-ci-yml">as we did in the previous tutorial with the <strong><code>ci</code> environment</strong></a>
<a href="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image-share-secret-key.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image-share-secret-key.PNG" alt="Add the codebase in the docker image and share a secret key file" /></a></li>
</ul>

<p>In the previous tutorial we have used the environment (<code>ci</code>) to 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#compose-file-updates">identify the necessary <code>docker-compose.yml</code> configuration files</a> 
and also
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#build-target-ci">as a new build target in the <code>Dockerfiles</code></a>. 
We'll stick to this process and <strong>add yet another environment called <code>prod</code></strong>.</p>

<p><!-- generated -->
<a id='a-env-file-for-prod'> </a>
<!-- /generated --></p>

<h3>A <code>.env</code> file for <code>prod</code></h3>

<p>For our <code>local</code> and <code>ci</code> environments we didn't really care about the <code>.env</code> file that we used for 
<code>docker compose</code> and have simply <strong>added a ready-to-use template at <code>.docker/.env.example</code> that is 
NOT ignored by <code>git</code></strong>. Even though the file contains credentials, e.g. for <code>mysql</code> and <code>redis</code>, 
it's okay if those are "exposed" in the repository, <strong>because we won't store any production data 
in those databases</strong> in <code>local</code> and <code>ci</code>.</p>

<p>For <code>prod</code> however, the situation is different: We certainly do NOT want to expose the 
credentials. Luckily we already have <a href="/blog/git-secret-encrypt-repository-docker/"><code>git secret</code> set up</a>
and thus could simply 
<a href="#the-secrets-directory">add an encrypted template file at <code>.secrets/prod/docker.env</code></a>. We will 
later 
<a href="#create-the-deployment-archive">decrypt the file and add it to the deployment archive</a> 
to transfer it to the VM 
<a href="#deployment-commands-on-the-vm">in order to start the docker setup there</a>.</p>

<p><!-- generated -->
<a id='updating-the-docker-compose-yml-configuration-files'> </a>
<!-- /generated --></p>

<h3>Updating the <code>docker-compose.yml</code> configuration files</h3>

<p>We will use the same technique as before to "assemble" our <code>docker compose</code> configuration, i.e. 
we use <strong>multiple compose files with environment specific settings</strong>. For <code>prod</code>, we use the files</p>

<ul>
<li><a href="#docker-compose-local-ci-prod-yml"><code>docker-compose.local.ci.prod.yml</code></a>

<ul>
<li>contains config settings for <em>all</em> environments</li>
</ul></li>
<li><a href="#docker-compose-local-prod-yml"><code>docker-compose.local.prod.yml</code></a>

<ul>
<li>contains config settings only for <code>local</code> and <code>prod</code></li>
</ul></li>
<li><a href="#docker-compose-prod-yml"><code>docker-compose.prod.yml</code></a>

<ul>
<li>contains config settings only for <code>prod</code></li>
</ul></li>
</ul>

<p>The assembling is once again <a href="#env-based-docker-compose-config">performed via Makefile</a>.</p>

<p><!-- generated -->
<a id='docker-compose-local-ci-prod-yml'> </a>
<!-- /generated --></p>

<h4><code>docker-compose.local.ci.prod.yml</code></h4>

<p>This file was simply renamed from file <code>docker-compose.local.ci.yml</code>. It contains</p>

<ul>
<li><p><code>network</code> and <code>volume</code> definitions</p>

<pre><code class="language-yaml">networks:
  network:
    driver: ${NETWORKS_DRIVER?}

volumes:
  mysql:
    name: mysql-${ENV?}
    driver: ${VOLUMES_DRIVER?}
  redis:
    name: redis-${ENV?}
    driver: ${VOLUMES_DRIVER?}
</code></pre></li>
<li>the build instructions for the <code>application</code> service
<code>yaml
application:
  image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/application-${ENV?}:${TAG?}
  build:
    context: ../
    dockerfile: ./images/php/application/Dockerfile
    target: ${ENV?}
    args:
      - BASE_IMAGE=${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-base-${ENV?}:${TAG?}
      - ENV=${ENV?}</code></li>
<li><p>the configuration for the <code>mysql</code> and <code>redis</code> services</p>

<pre><code class="language-yaml">mysql:
  image: mysql:${MYSQL_VERSION?}
  platform: linux/amd64
  environment:
    - MYSQL_DATABASE=${MYSQL_DATABASE:-application_db}
    - MYSQL_USER=${MYSQL_USER:-application_user}
    - MYSQL_PASSWORD=${MYSQL_PASSWORD?}
    - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD?}
    - TZ=${TIMEZONE:-UTC}
  networks:
    - network
  healthcheck:
    test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
    timeout: 1s
    retries: 30
    interval: 2s

redis:
  image: redis:${REDIS_VERSION?}
  command: &gt;
    --requirepass ${REDIS_PASSWORD?}
  networks:
    - network
</code></pre></li>
</ul>

<p><!-- generated -->
<a id='docker-compose-local-prod-yml'> </a>
<!-- /generated --></p>

<h4><code>docker-compose.local.prod.yml</code></h4>

<p>This file is based on the 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#docker-compose-local-yml"><code>docker-compose.local.yml</code> of the previous tutorial</a>
<strong>without any settings for local development</strong>. It contains</p>

<ul>
<li><p>build instructions for the <code>php-fpm</code>, <code>php-worker</code> and <code>nginx</code> services (because 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#docker-changes">we didn't need those in the configuration for the <code>ci</code> environment</a>)</p>

<pre><code class="language-yaml">php-fpm:
  image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-fpm-${ENV?}:${TAG?}
  build:
    context: ../
    dockerfile: ./images/php/fpm/Dockerfile
    target: ${ENV?}
    args:
      - BASE_IMAGE=${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-base-${ENV?}:${TAG?}
      - TARGET_PHP_VERSION=${PHP_VERSION?}

php-worker:
  image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-worker-${ENV?}:${TAG?}
  build:
    context: ../
    dockerfile: ./images/php/worker/Dockerfile
    target: ${ENV?}
    args:
      - BASE_IMAGE=${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-base-${ENV?}:${TAG?}
      - PHP_WORKER_PROCESS_NUMBER=${PHP_WORKER_PROCESS_NUMBER:-4}

nginx:
  image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/nginx-${ENV?}:${TAG?}
  build:
    context: ../
    dockerfile: ./images/nginx/Dockerfile
    target: ${ENV?}
    args:
      - NGINX_VERSION=${NGINX_VERSION?}
      - APP_CODE_PATH=${APP_CODE_PATH_CONTAINER?}
  ports:
    - "${NGINX_HOST_HTTP_PORT:-80}:80"
    - "${NGINX_HOST_HTTPS_PORT:-443}:443"
</code></pre></li>
<li><p>port forwarding for the <code>nginx</code> service, because we want to forward incoming requests on the 
VM to the <code>nginx</code> docker container</p>

<pre><code class="language-yaml">nginx:
  ports:
    - "${NGINX_HOST_HTTP_PORT:-80}:80"
    - "${NGINX_HOST_HTTPS_PORT:-443}:443"
</code></pre></li>
<li><p>volume configuration for the <code>mysql</code> and <code>redis</code> services</p>

<pre><code class="language-yaml">mysql:
  volumes:
    - mysql:/var/lib/mysql

redis:
  volumes:
    - redis:/data
</code></pre></li>
</ul>

<p>The following settings are <strong>only retained in <code>docker-compose.local.yml</code></strong></p>

<ul>
<li><p>bind mount of the codebase</p>

<pre><code class="language-yaml">application|php-fpm|php-worker|nginx:
  volumes:
    - ${APP_CODE_PATH_HOST?}:${APP_CODE_PATH_CONTAINER?}
</code></pre></li>
<li><p>port sharing with the host system (excluding nginx ports)</p>

<pre><code class="language-yaml">application:
  ports:
    - "${APPLICATION_SSH_HOST_PORT:-2222}:22"

mysql:
  ports:
    - "${MYSQL_HOST_PORT:-3306}:6379"

redis:
  ports:
    - "${REDIS_HOST_PORT:-6379}:6379"
</code></pre></li>
<li><p>any settings for local dev tools for all php images (<code>application</code>, <code>php-fpm</code>, <code>php-worker</code>)</p>

<pre><code class="language-yaml">application|php-fpm|php-worker:
  environment:
    - PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}
  cap_add:
    - "SYS_PTRACE"
  security_opt:
    - "seccomp=unconfined"
  extra_hosts:
    host.docker.internal:host-gateway  
</code></pre></li>
</ul>

<p><!-- generated -->
<a id='docker-compose-prod-yml'> </a>
<!-- /generated --></p>

<h4><code>docker-compose.prod.yml</code></h4>

<p>In this file,</p>

<ul>
<li><p>we bind-mount the secret gpg key into all <code>php</code> services 
(<a href="/blog/ci-pipeline-docker-php-gitlab-github/#docker-compose-ci-yml">as we did in the previous tutorial for the <code>docker-compose.ci.yml</code> file</a>)</p>

<pre><code class="language-yaml">  volumes:
    - ${APP_CODE_PATH_HOST?}/secret.gpg:${APP_CODE_PATH_CONTAINER?}/secret.gpg:ro
</code></pre></li>
<li><p>we <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#env_file">provide an <code>env</code> file</a> 
for all <code>php</code> services</p>

<pre><code class="language-yaml">  env_file:
    - ../../compose-secrets.env
</code></pre>

<p>The <code>env</code> file is used to pass the <code>GPG_PASSWORD</code> as environment variable to the containers.
This is required to decrypt the secrets on container start. See section
<a href="#decrypt-the-secrets-via-entrypoint">Decrypt the secrets via <code>ENTRYPOINT</code></a> for a more in-depth 
explanation of the process and <a href="#the-deploy-sh-script">The <code>deploy.sh</code> script</a> for the origin 
of the <code>compose-secrets.env</code> file.</p></li>
</ul>

<p><!-- generated -->
<a id='adjust-the-dockerignore-file'> </a>
<!-- /generated --></p>

<h3>Adjust the <code>.dockerignore</code> file</h3>

<p>Since we have 
<a href="#optimize-gitignore">modified the <code>.gitignore</code> file</a> , we need to adjust the <code>.dockerignore</code> 
file as well. In addition to the <code>gitignore</code> rules, we need three additional rules:</p>

<ol>
<li><code>.git</code>: <a href="/blog/ci-pipeline-docker-php-gitlab-github/#dockerignore">Known from the previous tutorial</a>. 
We don't need to transfer the <code>.git</code> directory to the build context</li>
<li><code>!.build/build-info</code>: We don't need the content of 
<a href="#the-build-directory">the <code>.build/</code> directory</a>, but we <strong>do</strong> need the 
<a href="#the-build-info-file">the <code>build-info</code> file</a></li>
<li><code>vendor/**</code>: Has to be added as a workaround for the <code>docker compose</code> bug
<a href="https://github.com/docker/compose/issues/9508">Inconsistent ".dockerignore" behavior between "docker build" and "docker compose build"</a>
that causes <code>docker compose build</code> to include <code>.gitignore</code> files in the <code>vendor/</code> directory.
This would mess with the build of the <code>php-base</code> image as we expect that no <code>vendor/</code> folder 
exists</li>
</ol>

<p>So the full <code>.dockerignore</code> file becomes</p>

<pre><code class="language-dockerignore"># ---
# Rules from .gitignore
# ---
**/*.env*
!.env.example
!.make/variables.env
.idea
.phpunit.result.cache
vendor/
secret.gpg
.gitsecret/keys/random_seed
.gitsecret/keys/pubring.kbx~
.secrets/*/*
!**/*.secret
.build/*
!.gitkeep
gcp-service-account-key.json

# =&gt; directory-specific .gitignore files by Laravel
bootstrap/cache/*
database/*.sqlite*
storage/app/*
!storage/app/public/
storage/app/public/*
storage/framework/*
!storage/framework/cache/
storage/framework/cache/*
!storage/framework/cache/data/
storage/framework/cache/data/*
!storage/framework/sessions/
storage/framework/sessions/*
!storage/framework/testing/
storage/framework/testing/*
!storage/framework/views/
storage/framework/views/*
!storage/framework/logs/
storage/framework/logs/*

# =&gt; directory-specific .gitignore files from us
.build/*

# =&gt; keep ALL .gitignore files
!**.gitignore

# ---
# Rules specifically for .dockerignore
# ---

# =&gt; don't transfer the git directory
.git

# =&gt; keep the build-info file
!.build/build-info

# [WORKAROUND]
# temporary fix for https://github.com/docker/compose/issues/9508
# Otherwise, `docker compose build` would transfer the `.gitignore` files 
# in the vendor/ directory to the build context
vendor/**
</code></pre>

<p><!-- generated -->
<a id='build-target-prod'> </a>
<!-- /generated --></p>

<h3>Build target: <code>prod</code></h3>

<p>For building the images for the <code>prod</code> environment, we stick to
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#build-target-ci">the technique of the previous tutorial</a>
once again. In short:</p>

<ul>
<li>initialize the <code>make</code> setup for <code>ENV=prod</code> via
<code>bash
make make-init ENVS="ENV=prod"</code>
so that all subsequent <code>make</code> invocations use <code>ENV=prod</code> by default, see also section
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#initialize-the-shared-variables">Initialize the shared variables</a>
of the previous tutorial</li>
<li>the <code>ENV</code> is passed as environment variable to the <code>docker compose</code> command as defined in the
<code>.make/02-00-docker.mk</code> Makefile include AND
<a href="#env-based-docker-compose-config">the config files for <code>prod</code> are selected</a>
<code>bash
ENV=prod docker compose -p -f ./.docker/docker-compose/docker-compose.local.ci.prod.yml -f ./.docker/docker-compose/docker-compose.local.prod.yml -f ./.docker/docker-compose/docker-compose.prod.yml</code></li>
<li>the <code>ENV</code> environment variable is used to define the <code>target</code> property as well as the <code>image</code>
name in the <code>docker compose</code> config files. This can be verified via <code>make docker-config</code>, for
instance
<code>text
$ make docker-config
services:
application:
  build:
    target: prod
  image: gcr.io/pl-dofroscra-p/dofroscra/application-prod:latest
# ...</code></li>
</ul>

<p>For <code>ci</code> we only needed the <code>php-base</code> images as well as the <code>application</code> image. For <code>prod</code>
however, we need <em>all</em> images.</p>

<p><!-- generated -->
<a id='build-stage-prod-in-the-php-base-image'> </a>
<!-- /generated --></p>

<h4>Build stage <code>prod</code> in the <code>php-base</code> image</h4>

<p>We will re-use a lot of the code that we used in the previous tutorial to build the <code>php-base</code> 
image - so I'd recommend giving the corresponding section 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#build-stage-ci-in-the-php-base-image">Build stage <code>ci</code> in the <code>php-base</code> image</a>
a look if anything is not clear.</p>

<p>The relevant parts of the <code>.docker/images/php/base/Dockerfile</code> will be explained in the 
following sections but are also shown here for the sake of a better overview:</p>

<pre><code class="language-Dockerfile">ARG ALPINE_VERSION
ARG COMPOSER_VERSION
FROM composer:${COMPOSER_VERSION} as composer
FROM alpine:${ALPINE_VERSION} as base

ARG APP_USER_NAME
ARG APP_GROUP_NAME
ARG APP_CODE_PATH
ARG TARGET_PHP_VERSION
ARG ENV
ENV APP_USER_NAME=${APP_USER_NAME}
ENV APP_GROUP_NAME=${APP_GROUP_NAME}
ENV APP_CODE_PATH=${APP_CODE_PATH}
ENV TARGET_PHP_VERSION=${TARGET_PHP_VERSION}
ENV ENV=${ENV}

RUN apk add --no-cache php8~=${TARGET_PHP_VERSION}

COPY --from=composer /usr/bin/composer /usr/local/bin/composer

WORKDIR $APP_CODE_PATH

FROM base as codebase

# By only copying the composer files required to run composer install
# the layer will be cached and only invalidated when the composer dependencies are changed
COPY ./composer.json /dependencies/
COPY ./composer.lock /dependencies/

# use a cache mount to cache the composer dependencies
# this is essentially a cache that lives in Docker BuildKit (i.e. has nothing to do with the host system) 
RUN --mount=type=cache,target=/tmp/.composer \
    cd /dependencies &amp;&amp; \
    # COMPOSER_HOME=/tmp/.composer sets the home directory of composer that
    # also controls where composer looks for the cache 
    # so we don't have to download dependencies again (if they are cached)
    # @see https://stackoverflow.com/a/60518444 for the correct if-then-else syntax:
    # - end all commands with ; \
    # - except THEN and ELSE
    if [ "$ENV" == "prod" ] ; \
    then \
      # on production, we don't want test dependencies
      COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o --no-dev; \
    else  \
      COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o; \
    fi

# copy the full codebase
COPY ../_blog /codebase

# move the dependencies
RUN mv /dependencies/vendor /codebase/vendor

# remove files we don't require in the image to keep the image size small
RUN cd /codebase &amp;&amp; \
    rm -rf .docker/ .build/ .infrastructure/ &amp;&amp; \
    if [ "$ENV" == "prod" ] ; \
    then \
      # on production, we don't want tests
      rm -rf tests/; \
    fi

# Remove all secrets that are NOT required for the given ENV:
#  `find /codebase/.secrets -type f -print` lists all files in the .secrets directory
#  `grep -v "/\(shared\|$ENV\)/"` matches only the files that are NOT in the shared/ or $ENV/ (e.g. prod/) directories
#  `grep -v ".secret\$"` ensures that we remove all files that are NOT ending in .secret
#    FYI: 
#     the "$" has to be escaped with a "\" 
#     "Escaping is possible by adding a \ before the variable"
#     @see https://docs.docker.com/engine/reference/builder/#environment-replacement
#  `xargs rm -f` retrieves the remaining file and deletes them
#    FYI: 
#     `xargs` is necessary to convert the stdin to args for `rm`
#     @see https://stackoverflow.com/a/20307392/413531
#     the `-f` flag is required so that `rm` doesn't fail if no files are matched
RUN find /codebase/.secrets -type f -print | grep -v "/\(shared\|$ENV\)/" | xargs rm -f &amp;&amp; \
    find /codebase/.secrets -type f -print | grep -v ".secret\$" | xargs rm -f &amp;&amp; \
    # list the remaining files for debugging purposes
    find /codebase/.secrets -type f -print

# We need a git repository for git-secret to work (can be an empty one)
RUN cd /codebase &amp;&amp; \
    git init

FROM base as prod

# We will use a custom ENTRYPOINT to decrypt the secrets when the container starts.
# This way, we can store the secrets in their encrypted form directly in the image.
# Note: Because we defined a custom ENTRYPOINT, the default CMD of the base image
#       will be overriden. Thus, we must explicitly re-define it here via `CMD ["/bin/sh"]`.
#       This behavior is described in the docs as:
#       "If CMD is defined from the base image, setting ENTRYPOINT will reset CMD to an empty value. In this scenario, CMD must be defined in the current image to have a value."
#       @see https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
COPY ./.docker/images/php/base/decrypt-secrets.sh /decrypt-secrets.sh
RUN chmod +x /decrypt-secrets.sh
CMD ["/bin/sh"]
ENTRYPOINT ["/decrypt-secrets.sh"]

COPY --from=codebase --chown=$APP_USER_NAME:$APP_GROUP_NAME /codebase $APP_CODE_PATH

COPY --chown=$APP_USER_NAME:$APP_GROUP_NAME ./.build/build-info $APP_CODE_PATH/build-info
</code></pre>

<p><!-- generated -->
<a id='env-based-branching'> </a>
<!-- /generated --></p>

<h5><code>ENV</code> based branching</h5>

<p>So far, we have <strong>used the <code>ENV</code> to determine the final build stage</strong> of the docker image, by 
using it as value for the <code>target</code> property in the <code>docker compose</code> config files. This 
introduces a certain level of flexibility, but we would be forced to duplicate code if the 
same logic is required for multiple <code>ENV</code> that use different build stages.</p>

<p><strong>For <code>RUN</code> statements we can achieve branching on a more granular level by using <code>if...else</code> 
conditions</strong>. The syntax is described in<br />
<a href="https://stackoverflow.com/a/60518444">this SO answer to "Dockerfile if else condition with external arguments"</a>:</p>

<ul>
<li>place a <code>\</code> at the end of each line</li>
<li>end each command with <code>;</code></li>
</ul>

<p><strong>Example:</strong></p>

<pre><code class="language-Dockerfile">RUN if [ "$ENV" == "prod" ] ; \
    then \
      echo "ENV is prod"; \
    else \
      echo "ENV is NOT prod"; \
    fi
</code></pre>

<p>This works, because we don't just use the <code>ENV</code> as the build target, but we also pass it as a 
build argument in the <code>.docker/docker-compose/docker-compose-php-base.yml</code> file:</p>

<pre><code class="language-yaml">services:
  php-base:
    image: ${DOCKER_REGISTRY?}/${DOCKER_NAMESPACE?}/php-base-${ENV?}:${TAG?}
    build:
      args:
        - ENV=${ENV?}
      target: ${ENV?}
</code></pre>

<p><!-- generated -->
<a id='avoid-composer-dev-dependencies'> </a>
<!-- /generated --></p>

<h5>Avoid composer dev dependencies</h5>

<p><strong>A production image should only contain the dependencies that are necessary to run the code in 
production</strong>. This explicitly <em>excludes</em> dependencies that are only required for development or
testing.</p>

<p>This isn't only about image size, but also about security. E.g. take a look at
<a href="https://nvd.nist.gov/vuln/detail/CVE-2017-9841">CVE-2017-9841</a> - a remote code execution
vulnerability in <code>phpunit</code>. In a
<a href="https://thephp.cc/artikel/phpunit-ein-sicherheitsrisiko">blog post</a> (german) Sebastian Bergmann
(creator of PHPUnit) mentions that</p>

<blockquote>
  <p>A dependency like PHPUnit, that is only required for the developing the software but not
  running it, is not supposed to be deployed to the production system</p>
</blockquote>

<p>Thus, we will add the 
<a href="https://getcomposer.org/doc/03-cli.md#install-i"><code>--no-dev</code> flag of the <code>composer install</code> command</a>
for <code>ENV=prod</code>:</p>

<pre><code class="language-Dockerfile">FROM base as codebase

# ...

RUN --mount=type=cache,target=/tmp/.composer \
    cd /dependencies &amp;&amp; \
    if [ "$ENV" == "prod" ] ; \
    then \
      COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o --no-dev; \
    else  \
      COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o; \
    fi
</code></pre>

<p>Note: See also section 
<a href="/ci-pipeline-docker-php-gitlab-github/#build-the-dependencies">Build the dependencies</a> of the 
previous tutorial for an explanation of the <code>--mount=type=cache,target=/tmp/.composer</code> part.</p>

<p><!-- generated -->
<a id='remove-unnecessary-directories'> </a>
<!-- /generated --></p>

<h5>Remove unnecessary directories</h5>

<p>We already learned that <strong>no unnecessary stuff should end up in the image</strong>. This doesn't stop at 
<code>composer</code> dependencies but does in our case also include some other directories:</p>

<ul>
<li><code>.docker</code> (the docker setup files)</li>
<li><code>.build</code> (used to pass <a href="#the-build-info-file">the <code>build-info</code> file</a>)</li>
<li><code>.infrastructure</code> (see <a href="#the-infrastructure-directory">The <code>.infrastructure</code> directory</a>)</li>
<li><code>tests</code> (the test files)</li>
</ul>

<pre><code class="language-Dockerfile">FROM base as codebase

# ...

RUN cd /codebase &amp;&amp; \
    rm -rf .docker/ .build/ .infrastructure/ &amp;&amp; \
    if [ "$ENV" == "prod" ] ; \
    then \
      rm -rf tests/; \
    fi
</code></pre>

<p><!-- generated -->
<a id='remove-secrets-for-other-environments'> </a>
<!-- /generated --></p>

<h5>Remove secrets for other environments</h5>

<p>We have <a href="#the-secrets-directory">re-organized the secrets previously</a> so that all 
secrets for an environment are located in the corresponding <code>.secrets/$ENV/</code> subdirectory. We can 
now make use of that separation <strong>to keep only the secret files that we actually need</strong>. This 
adds an additional layer of security, because everybody with a correct secret <code>gpg</code> key file can 
decrypt <em>all</em> the secrets. But: If our "<code>prod</code> secrets" don't even exist in the "<code>ci</code> images" 
they also cannot be leaked if <code>ci</code> is compromised.</p>

<pre><code class="language-Dockerfile">FROM base as codebase

# ...

RUN find /codebase/.secrets -type f -print | grep -v "/\(shared\|$ENV\)/" | xargs rm -f &amp;&amp; \
    find /codebase/.secrets -type f -print | grep -v ".secret\$" | xargs rm -f &amp;&amp; \
    # list the remaining files for debugging purposes
    find /codebase/.secrets -type f -print
</code></pre>

<p>Notes:</p>

<ul>
<li><code>find /codebase/.secrets -type f -print</code> lists all files in the .secrets directory</li>
<li><code>grep -v "/\(shared\|$ENV\)/"</code> matches only the files that are NOT in the <code>shared/</code> or <code>$ENV/</code> 
(e.g. <code>prod/</code>) directories</li>
<li><code>grep -v ".secret\$"</code> ensures that we remove all files that are NOT ending in <code>.secret</code>

<ul>
<li>as per <a href="https://docs.docker.com/engine/reference/builder/#environment-replacement">documentation</a>, 
the <code>$</code> has to be escaped with a <code>\</code>
> "Escaping is possible by adding a \ before the variable"</li>
</ul></li>
<li><code>xargs rm -f</code> retrieves the remaining file and deletes them

<ul>
<li><code>xargs</code> is necessary to 
<a href="https://stackoverflow.com/a/20307392/413531">convert the <code>stdin</code> to arguments for <code>rm</code></a></li>
<li>the <code>-f</code> flag is required so that <code>rm</code> doesn't fail if no files are matched</li>
</ul></li>
</ul>

<p><!-- generated -->
<a id='decrypt-the-secrets-via-entrypoint'> </a>
<!-- /generated --></p>

<h5>Decrypt the secrets via <code>ENTRYPOINT</code></h5>

<p>In the <a href="/blog/ci-pipeline-docker-php-gitlab-github/">CI pipeline setup</a> we <strong>decrypt the secret 
files in a manual step</strong> after the containers have been started, see 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#run-details">Run details</a>:</p>

<blockquote>
  <p>[...]
  then, the docker setup is started
  [...]
  and gpg is initialized so that the secrets can be decrypted</p>

<pre><code>   make gpg-init
   make secret-decrypt-with-password
</code></pre>
</blockquote>

<p>It "works" as long as <strong>no secrets are required when the container starts</strong>. This is unfortunately 
no longer the case, because the <code>php-worker</code> container will start its workers immediately, and 
they require a valid <code>.env</code> file - but
<a href="#the-secrets-directory">the <code>.env</code> file for <code>prod</code> (<code>app.env</code>) is only stored encrypted in the image</a>.
Thus, we must ensure that <strong>the secrets are decrypted as soon as the container starts</strong> - 
preferably before any other command is run. This sounds like 
<a href="/blog/structuring-the-docker-setup-for-php-projects/#using-entrypoint-for-pre-run-configuration">the perfect job for <code>ENTRYPOINT</code></a>:</p>

<blockquote>
  <p>the ENTRYPOINT is executed every time we run a container. Some things can't be done during 
  build but only at runtime [...] - ENTRYPOINT is a good solution for that problem</p>
</blockquote>

<p>In our case, the <code>ENTRYPOINT</code> should</p>

<ul>
<li>initialize <code>gpg</code> (i.e. "read the secret gpg key") via
<code>bash
make gpg-init</code></li>
<li>decrypt the secrets via
<code>bash
make secret-decrypt</code></li>
<li>move/copy the decrypted files if necessary<br />
<code>bash
cp .secrets/prod/app.env .env</code></li>
</ul>

<p>and I have created a corresponding script at <code>.docker/images/php/base/decrypt-secrets.sh</code>:</p>

<pre><code class="language-bash">#!/usr/bin/env bash

# exit immediately on error
set -e

# initialize make
make make-init ENVS="ENV=prod GPG_PASSWORD=$GPG_PASSWORD"

# read the secret gpg key
make gpg-init

# Only decrypt files required for production
files=$(make secret-list | grep "/\(shared\|prod\)/" | tr '\n' ' ')
make secret-decrypt-with-password FILES="$files"

cp .secrets/prod/app.env .env

# treat this script as a "decorator" and execute any other command after running it
# @see https://www.pascallandau.com/blog/structuring-the-docker-setup-for-php-projects/#using-entrypoint-for-pre-run-configuration
exec "$@"
</code></pre>

<p>Notes:</p>

<ul>
<li><code>make make-init ENVS="ENV=prod GPG_PASSWORD=$GPG_PASSWORD"</code> initializes <code>make</code> and requires 
that an environment variable named <code>$GPG_PASSWORD</code> exists

<ul>
<li>this is ensured via the <code>env_file</code> property in 
<a href="#docker-compose-prod-yml"><code>docker-compose.prod.yml</code></a></li>
</ul></li>
<li><code>make gpg-init</code> requires that a secret <code>gpg</code> key exists at <code>./secret.gpg</code> (see 
<a href="/blog/git-secret-encrypt-repository-docker/#local-git-secret-and-gpg-setup">Local <code>git-secret</code> and <code>gpg</code> setup</a>)

<ul>
<li>this is ensured via the bind mount in <a href="#docker-compose-prod-yml"><code>docker-compose.prod.yml</code></a></li>
</ul></li>
<li><code>files=$(make secret-list | grep "/\(shared\|prod\)/" | tr '\n' ' ')</code> retrieves all secret 
files that are relevant for production

<ul>
<li><a href="/blog/git-secret-encrypt-repository-docker/#makefile-adjustments"><code>make secret-list</code></a> will 
list all secret files</li>
<li><code>grep "/\(shared\|prod\)/"</code> reduces the list to only the ones in the <code>.secrets/shared/</code> and
<code>.secrets/prod/</code> directories</li>
<li><code>tr '\n' ' '</code> replaces new lines with spaces in the result so that the <code>$files</code> variable can 
be passed savely as an argument to the next command</li>
</ul></li>
<li><p>via <code>make secret-decrypt-with-password FILES="$files"</code></p>

<ul>
<li>we make sure to <strong>only decrypt files that are relevant for production</strong>. This is important, 
because <strong>the <code>prod</code> image only contains production secrets</strong>. Secrets for any other environment 
<a href="#remove-secrets-for-other-environments">will not be part of the image</a>. 
If we wouldn't provide a dedicated list of files, <code>git-secret</code> would attempt to decrypt <em>all</em> 
secrets that it knows and would fail if an encrypted file is missing with the error</li>
</ul>

<pre><code class="language-text">gpg: can't open '/missing-file.secret': No such file or directory
gpg: decrypt_message failed: No such file or directory
git-secret: abort: problem decrypting file with gpg: exit code 2: /missing-file
</code></pre>

<ul>
<li>in addition, the <code>secret-decrypt-with-password</code> target expects that the <code>GPG_PASSWORD</code> variable 
is populated (see first point).</li>
</ul></li>
<li>the last line 
<a href="https://unix.stackexchange.com/a/467003"><code>exec "$@"</code> ensures that everything "works as before"</a>,
i.e. the same
<code>ENTRYPOINT</code> / <code>CMD</code> is used in the containers (e.g. <code>php-fpm</code> will still invoke the <code>php-fpm</code> 
process once the secrets have been decrypted)</li>
</ul>

<p>The final code in the <code>Dockerfile</code> looks like this:</p>

<pre><code class="language-Dockerfile">FROM base as prod

COPY ./.docker/images/php/base/decrypt-secrets.sh /decrypt-secrets.sh
RUN chmod +x /decrypt-secrets.sh
CMD ["/bin/sh"]
ENTRYPOINT ["/decrypt-secrets.sh"]
</code></pre>

<p><strong>Note</strong>: Because we defined a custom <code>ENTRYPOINT</code>, the default <code>CMD</code> of the base image  will be 
overridden. Thus, we must explicitly re-define it here via <code>CMD ["/bin/sh"]</code>. This behavior is also
<a href="https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact">described in the docs</a>:</p>

<blockquote>
  <p>If CMD is defined from the base image, setting <code>ENTRYPOINT</code> will reset <code>CMD</code> to an empty value. In 
  this scenario, <code>CMD</code> must be defined in the current image to have a value.</p>
</blockquote>

<p><!-- generated -->
<a id='copy-codebase-and-build-info-file'> </a>
<!-- /generated --></p>

<h5>Copy codebase and <code>build-info</code> file</h5>

<p>As before, we will 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#create-the-final-image">copy the final "build artifact" from the <code>codebase</code> build stage</a>
to only retain a single layer in the image. In addition, we also copy 
<a href="#the-build-info-file">the <code>build-info</code> file</a> from <code>./.build/build-info</code> to the root of the 
codebase.</p>

<pre><code class="language-Dockerfile">FROM base as prod

COPY --from=codebase --chown=$APP_USER_NAME:$APP_GROUP_NAME /codebase $APP_CODE_PATH

COPY --chown=$APP_USER_NAME:$APP_GROUP_NAME ./.build/build-info $APP_CODE_PATH/build-info
</code></pre>

<p><!-- generated -->
<a id='build-stage-prod-in-the-remaining-images'> </a>
<!-- /generated --></p>

<h4>Build stage <code>prod</code> in the remaining images</h4>

<p>In the remaining images for <code>nginx</code>, <code>php-fpm</code>, <code>php-worker</code> and <code>application</code>, there are no 
dedicated instructions for the <code>prod</code> target. We must still define the build stage, though, via</p>

<pre><code class="language-Dockerfile">FROM base as prod
</code></pre>

<p>Otherwise, the build would fail with the error</p>

<pre><code class="language-text">failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to create LLB definition: target stage prod could not be found
</code></pre>

<p><!-- generated -->
<a id='makefile-changes'> </a>
<!-- /generated --></p>

<h2>Makefile changes</h2>

<p>I have updated the default <code>help</code> target that prints all available commands to also include some 
information about the current environment (usually set e.g. via <code>make make-init ENVS="ENV=prod"</code>). 
The full recipe is</p>

<pre><code class="language-Makefile">help:
    @printf '%-43s \033[1mDefault values: \033[0m     \n'
    @printf '%-43s ===================================\n'
    @printf '%-43s ENV: \033[31m "$(ENV)" \033[0m     \n'
    @printf '%-43s TAG: \033[31m "$(TAG)" \033[0m     \n'
    @printf '%-43s ===================================\n'
    @printf '%-43s \033[3mRun the following command to set them:\033[0m\n'
    @printf '%-43s \033[1mmake make-init ENVS="ENV=prod TAG=latest"\033[0m\n'
    @awk 'BEGIN {FS = ":.*##"; printf "\n\033[1mUsage:\033[0m\n  make \033[36m&lt;target&gt;\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf "  \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' Makefile .make/*.mk
</code></pre>

<p>and will now print a header showing the values of the <code>$(ENV)</code> and <code>$(TAG)</code> variables:</p>

<pre><code class="language-text">$ make
                                            Default values:
                                            ===================================
                                            ENV:  "local"
                                            TAG:  "latest"
                                            ===================================
                                            Run the following command to set them:
                                            make make-init ENVS="ENV=prod TAG=latest"

Usage:
  make &lt;target&gt;

[Make]
  make-init                                 Initializes the local .makefile/.env file with ENV variables for make. Use via ENVS="KEY_1=value1 KEY_2=value2"

[Application: Setup]

</code></pre>

<p><!-- generated -->
<a id='adding-gcp-values-to-make-variables-env'> </a>
<!-- /generated --></p>

<h3>Adding GCP values to <code>.make/variables.env</code></h3>

<p>The <code>.make/variables.env</code> file contains the "default" shared variables, that are neither "secret" 
nor likely to be changed (see <a href="/blog/ci-pipeline-docker-php-gitlab-github/#initialize-the-shared-variables">Initialize the shared variables</a>).</p>

<p>Those variables include the <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#image-naming-convention">"ingredients" for the image naming convention</a></p>

<pre><code class="language-text">$(DOCKER_REGISTRY)/$(DOCKER_NAMESPACE)/$(DOCKER_SERVICE_NAME)-$(ENV)
</code></pre>

<p>Since we will now 
<a href="/blog/gcp-compute-instance-vm-docker/#set-up-the-container-registry">use our own registry</a>, we 
need to change the <code>DOCKER_REGISTRY</code> value from <code>docker.io</code> to <code>gcr.io/pl-dofroscra-p</code> (see section 
<a href="/blog/gcp-compute-instance-vm-docker/#pushing-images-to-the-registry">Pushing images to the registry</a>).</p>

<p>In addition, we will need three more GCP specific variables that are required for the new 
<a href="#gcp-recipes"><code>gcloud</code> cli <code>make</code> targets</a>:</p>

<ul>
<li><code>GCP_PROJECT_ID</code>: The <a href="/blog/gcp-compute-instance-vm-docker/#set-up-a-gcp-project">GCP project id</a></li>
<li><code>GCP_ZONE</code>: The <a href="/blog/gcp-compute-instance-vm-docker/#general-vm-settings">availability zone of the GCP VM</a></li>
<li><code>GCP_VM_NAME</code>: The <a href="/blog/gcp-compute-instance-vm-docker/#general-vm-settings">name of the GCP VM</a></li>
</ul>

<p>So the full content of <code>.make/variables.env</code> becomes</p>

<pre><code class="language-dotenv">DOCKER_REGISTRY=gcr.io/pl-dofroscra-p
DOCKER_NAMESPACE=dofroscra
APP_USER_NAME=application
APP_GROUP_NAME=application
GCP_PROJECT_ID=pl-dofroscra-p
GCP_ZONE=us-central1-a
GCP_VM_NAME=dofroscra-test
</code></pre>

<p><!-- generated -->
<a id='env-based-docker-compose-config'> </a>
<!-- /generated --></p>

<h3>ENV based <code>docker compose</code> config</h3>

<p>We use the same technique as described 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#env-based-docker-compose-config">in the previous tutorial to assemble the <code>docker compose</code> config files</a>
by adding the config files for <code>ENV=prod</code> and the corresponding <code>DOCKER_COMPOSE_FILES</code> 
definition in <code>.make/02-00-docker.mk</code>:</p>

<p><a href="/img/deploy-docker-compose-php-gcp-poc/make-env-based-prod-config.PNG"><img src="/img/deploy-docker-compose-php-gcp-poc/make-env-based-prod-config.PNG" alt="ENV based docker compose config for the prod environment" /></a></p>

<pre><code class="language-Makefile"># File .make/02-00-docker.mk

# ...

DOCKER_COMPOSE_DIR:=...
DOCKER_COMPOSE_COMMAND:=...

DOCKER_COMPOSE_FILE_LOCAL_CI_PROD:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.ci.prod.yml
DOCKER_COMPOSE_FILE_LOCAL_PROD:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.prod.yml
DOCKER_COMPOSE_FILE_LOCAL:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.yml
DOCKER_COMPOSE_FILE_CI:=$(DOCKER_COMPOSE_DIR)/docker-compose.ci.yml
DOCKER_COMPOSE_FILE_PROD:=$(DOCKER_COMPOSE_DIR)/docker-compose.prod.yml

ifeq ($(ENV),prod)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI_PROD) -f $(DOCKER_COMPOSE_FILE_LOCAL_PROD) -f $(DOCKER_COMPOSE_FILE_PROD)
else ifeq ($(ENV),ci)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI_PROD) -f $(DOCKER_COMPOSE_FILE_CI)
else ifeq ($(ENV),local)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI_PROD) -f $(DOCKER_COMPOSE_FILE_LOCAL_PROD) -f $(DOCKER_COMPOSE_FILE_LOCAL)
endif

DOCKER_COMPOSE:=$(DOCKER_COMPOSE_COMMAND) $(DOCKER_COMPOSE_FILES)
</code></pre>

<p>FYI: There is no dedicated <code>docker compose</code> config file for settings that only affect <code>ci</code> and 
<code>prod</code> (i.e. <code>docker-compose.ci.prod.yml</code>).</p>

<p>The "final" <code>$(DOCKER_COMPOSE_FILES)</code> variable will look like this:</p>

<pre><code class="language-text">-f ./.docker/docker-compose/docker-compose.local.ci.prod.yml -f ./.docker/docker-compose/docker-compose.local.prod.yml -f ./.docker/docker-compose/docker-compose.prod.yml
</code></pre>

<p>and the "full" <code>$(DOCKER_COMPOSE)</code> variable like this:</p>

<pre><code class="language-text">ENV=prod TAG=latest DOCKER_REGISTRY=gcr.io/pl-dofroscra-p DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_prod --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.prod.yml -f ./.docker/docker-compose/docker-compose.local.prod.yml -f ./.docker/docker-compose/docker-compose.prod.yml
</code></pre>

<p><a href="/img/deploy-docker-compose-php-gcp-poc/make-env-based-prod-config-docker-compose-command.PNG"><img src="/img/deploy-docker-compose-php-gcp-poc/make-env-based-prod-config-docker-compose-command.PNG" alt="DOCKER_COMPOSE command for the prod environment" /></a></p>

<p><!-- generated -->
<a id='changes-to-the-git-secret-recipes'> </a>
<!-- /generated --></p>

<h3>Changes to the <code>git-secret</code> recipes</h3>

<p>The <code>git-secret</code> recipes are defined in <code>01-00-application-setup.mk</code> and have been added 
originally in
<a href="/blog/git-secret-encrypt-repository-docker/#makefile-adjustments">Use <code>git secret</code> to encrypt secrets in the repository: Makefile adjustments</a>.</p>

<p>I have modified the targets <code>secret-decrypt</code> and <code>secret-decrypt-with-password</code> to accept an 
optional list of files to decrypt via the <code>FILES</code> variable. If the variable is empty, <em>all</em> 
files are decrypted. This is required for the 
<a href="#decrypt-the-secrets-via-entrypoint"><code>decrypt-secrets.sh</code> script</a>, because we will 
<a href="#remove-secrets-for-other-environments">only store the secrets that are relevant for the curently built environment</a>
in the image and the decryption would fail if we attempted to decrypt files that don't exist.</p>

<pre><code class="language-Makefile"># ...

.PHONY: secret-decrypt
secret-decrypt: ## Decrypt secret files via `git-secret reveal -f`. Use FILES=file1 to decrypt only file1 instead of all files
    "$(MAKE)" -s git-secret ARGS="reveal -f $(FILES)"

.PHONY: secret-decrypt-with-password
secret-decrypt-with-password: ## Decrypt secret files using a password for gpg. Use FILES=file1 to decrypt only file1 instead of all files
    @$(if $(GPG_PASSWORD),,$(error GPG_PASSWORD is undefined))
    "$(MAKE)" -s git-secret ARGS="reveal -f -p $(GPG_PASSWORD) $(FILES)" 
</code></pre>

<p>In addition, I made a minor adjustment to the targets <code>secret-add</code>, <code>secret-cat</code> and 
<code>secret-remove</code> to use the variable name <code>FILES</code> (plural) instead of <code>FILE</code>, because all of 
them can also work with a list of files instead of just a single one.</p>

<p><!-- generated -->
<a id='additional-docker-recipes'> </a>
<!-- /generated --></p>

<h3>Additional <code>docker</code> recipes</h3>

<p>The following targets have been added to <code>.make/02-00-docker.mk</code></p>

<pre><code class="language-Makefile">compose-secrets.env:
    @echo "# This file only exists because docker compose cannot run `build` otherwise," &gt; ./compose-secrets.env
    @echo "# because it is referenced as an `env_file` in the docker compose config file" &gt; ./compose-secrets.env
    @echo "# for the `prod` environment. On `prod` it will contain the necessary ENV variables," &gt; ./compose-secrets.env
    @echo "# but on all other environments this 'placeholder' file is created." &gt; ./compose-secrets.env
    @echo "# The file is generated automatically via `make` if a docker compose target is executed" &gt; ./compose-secrets.env
    @echo "# @see https://github.com/docker/compose/issues/1973#issuecomment-1148257736" &gt; ./compose-secrets.env

.PHONY: docker-push
docker-push: validate-docker-variables ## Push all docker images to the remote repository
    $(DOCKER_COMPOSE) push $(ARGS)

.PHONY: docker-pull
docker-pull: validate-docker-variables ## Pull all docker images from the remote repository
    $(DOCKER_COMPOSE) pull $(ARGS)

.PHONY: docker-exec
docker-exec: validate-docker-variables ## Execute a command in a docker container. Usage: `make docker-exec DOCKER_SERVICE_NAME="application" DOCKER_COMMAND="echo 'Hello world!'"`
    @$(if $(DOCKER_SERVICE_NAME),,$(error "DOCKER_SERVICE_NAME is undefined"))
    @$(if $(DOCKER_COMMAND),,$(error "DOCKER_COMMAND is undefined"))
    $(DOCKER_COMPOSE) exec -T $(DOCKER_SERVICE_NAME) $(DOCKER_COMMAND)

# @see https://www.linuxfixes.com/2022/01/solved-how-to-test-dockerignore-file.html
# helpful to debug a .dockerignore file
.PHONY: docker-show-build-context
docker-show-build-context: ## Show all files that are in the docker build context for `docker build`
    @echo -e "FROM busybox\nCOPY . /codebase\nCMD find /codebase -print" | docker image build --no-cache -t build-context -f - .
    @docker run --rm build-context | sort

# `docker build` and `docker compose build` are behaving differently
# @see https://github.com/docker/compose/issues/9508
.PHONY: docker-show-compose-build-context
docker-show-compose-build-context: ## Show all files that are in the docker build context for `docker compose build`
    @.dev/scripts/docker-compose-build-context/show-build-context.sh

# Note: This is only a temporary target until https://github.com/docker/for-win/issues/12742 is fixed
.PHONY: docker-fix-mount-permissions
docker-fix-mount-permissions: ## Fix the permissions of the bind-mounted folder, @see https://github.com/docker/for-win/issues/12742
    $(EXECUTE_IN_APPLICATION_CONTAINER) ls -al
</code></pre>

<ul>
<li><p><code>compose-secrets.env</code> ensures that the file <code>./compose-secrets.env</code> exists in the root of the 
repository. The target is added as precondition to the <code>validate-docker-variables</code> target so 
that 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#enforce-required-parameters">it is checked whenever another <code>docker compose</code> target is run</a></p>

<pre><code class="language-makefile">.PHONY: validate-docker-variables
validate-docker-variables: .docker/.env compose-secrets.env
</code></pre>

<p>Without this file, <code>docker compose build</code> would fail for the <code>prod</code> environment, because the
file only exists "on the VM" and 
<a href="#docker-compose-prod-yml">is defined as <code>env_file</code> setting in <code>docker-compose.prod.yml</code></a>.
This is one of those annoying cases when the 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker-compose">dual-<code>build</code>-and-<code>run</code>-usage of <code>docker compose</code></a> 
gets in the way, see also 
<a href="https://github.com/docker/compose/issues/1973#issuecomment-1148257736">GH issue "Allow some commands to run without full config validation"</a></p></li>
<li><code>docker-push</code> uses <code>docker compose push</code> to <em>push</em> all defined services to the registry</li>
<li><code>docker-pull</code> uses <code>docker compose pull</code> to <em>pull</em> all defined services from the registry</li>
<li><code>docker-exec</code> uses <code>docker compose exec</code> to run arbitrary commands in the docker container 
that is specified via the <code>$(DOCKER_SERVICE_NAME)</code> variable. The command itself has to be passed 
via the <code>$(DOCKER_COMMAND)</code> variable</li>
<li><code>docker-show-build-context</code> is a small helper script that builds a temporary image and lists 
all files in the build context - this is very helpful to debug entries in the <code>.dockerignore</code> 
file. See also: <a href="https://www.linuxfixes.com/2022/01/solved-how-to-test-dockerignore-file.html">[SOLVED] How to test dockerignore file?</a></li>
<li><code>docker-show-compose-build-context</code> does the same but for <code>docker compose build</code> - which 
currently (in <code>docker compose</code> v2.5.1) 
<a href="https://github.com/docker/compose/issues/9508">seems to behave differently than <code>docker build</code></a></li>
<li><code>docker-fix-mount-permissions</code> is a only a temporary target that provides a workaround for a 
bug I filed in the Docker Desktop for Windows repository: 
<a href="https://github.com/docker/for-win/issues/12742">Ownership of files set via bind mount is set to user who accesses the file first</a></li>
</ul>

<p><!-- generated -->
<a id='gcp-recipes'> </a>
<!-- /generated --></p>

<h3>GCP recipes</h3>

<p>For the <a href="#deployment-workflow">deployment</a>, we need to communicate with the VM and will use 
the <a href="/blog/gcp-compute-instance-vm-docker/#set-up-the-gcloud-cli-tool"><code>gcloud</code> cli</a> to run 
<a href="/blog/gcp-compute-instance-vm-docker/#gcloud-ssh-command">SSH commands via IAP tunneling</a>. The cli 
requires a couple of default parameters like the VM name, the project id, the availability 
zone and the location of the key file for the service account, that we 
<a href="#adding-gcp-values-to-make-variables-env">conveniently defined in the <code>.make/variables.env</code> file</a>.</p>

<p>The GCP targets are defined in <code>.make/03-00-gcp.mk</code>:</p>

<pre><code class="language-Makefile">##@ [GCP]

.PHONY: gcp-init
gcp-init: validate-gcp-variables ## Initialize the `gcloud` cli and authenticate docker with the keyfile defined via GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY.
    @$(if $(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY),,$(error "GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY is undefined"))
    @$(if $(GCP_PROJECT_ID),,$(error "GCP_PROJECT_ID is undefined"))
    gcloud auth activate-service-account --key-file="$(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY)" --project="$(GCP_PROJECT_ID)"
    cat "$(GCP_DEPLOYMENT_SERVICE_ACCOUNT_KEY)" | docker login -u _json_key --password-stdin https://gcr.io

.PHONY: validate-gcp-variables
validate-gcp-variables:
    @$(if $(GCP_PROJECT_ID),,$(error "GCP_PROJECT_ID is undefined"))
    @$(if $(GCP_ZONE),,$(error "GCP_ZONE is undefined"))
    @$(if $(GCP_VM_NAME),,$(error "GCP_VM_NAME is undefined"))

# @see https://cloud.google.com/sdk/gcloud/reference/compute/ssh
.PHONY: gcp-ssh-command
gcp-ssh-command: validate-gcp-variables ## Run an arbitrary SSH command on the VM via IAP tunnel. Usage: `make gcp-ssh-command COMMAND="whoami"`
    @$(if $(COMMAND),,$(error "COMMAND is undefined"))
    gcloud compute ssh $(GCP_VM_NAME) --project $(GCP_PROJECT_ID) --zone $(GCP_ZONE) --tunnel-through-iap --command="$(COMMAND)"

.PHONY: gcp-ssh-login
gcp-ssh-login: validate-gcp-variables ## Log into a VM via IAP tunnel
    gcloud compute ssh $(GCP_VM_NAME) --project $(GCP_PROJECT_ID) --zone $(GCP_ZONE) --tunnel-through-iap

# @see https://cloud.google.com/sdk/gcloud/reference/compute/scp
.PHONY: gcp-scp-command
gcp-scp-command: validate-gcp-variables ## Copy a file via scp to the VM via IAP tunnel. Usage: `make gcp-scp-command SOURCE="foo" DESTINATION="bar"`
    @$(if $(SOURCE),,$(error "SOURCE is undefined"))
    @$(if $(DESTINATION),,$(error "DESTINATION is undefined"))
    gcloud compute scp $(SOURCE) $(GCP_VM_NAME):$(DESTINATION) --project $(GCP_PROJECT_ID) --zone $(GCP_ZONE) --tunnel-through-iap

# Defines the default secret version to retrieve from the Secret Manager
SECRET_VERSION?=latest

# @see https://cloud.google.com/sdk/gcloud/reference/secrets/versions/access
.PHONY: gcp-secret-get
gcp-secret-get: ## Retrieve and print the secret $(SECRET_NAME) in version $(SECRET_VERSION) from the Secret Manager
    @$(if $(SECRET_NAME),,$(error "SECRET_NAME is undefined"))
    @$(if $(SECRET_VERSION),,$(error "SECRET_VERSION is undefined"))
    @gcloud secrets versions access $(SECRET_VERSION) --secret=$(SECRET_NAME)

.PHONY: gcp-docker-exec
gcp-docker-exec: ## Run a command in a docker container on the VM. Usage: `make gcp-docker-exec DOCKER_SERVICE_NAME="application" DOCKER_COMMAND="echo 'Hello world!'"`
    @$(if $(DOCKER_SERVICE_NAME),,$(error "DOCKER_SERVICE_NAME is undefined"))
    @$(if $(DOCKER_COMMAND),,$(error "DOCKER_COMMAND is undefined"))
    "$(MAKE)" -s gcp-ssh-command COMMAND="cd $(CODEBASE_DIRECTORY) &amp;&amp; make docker-exec DOCKER_SERVICE_NAME='$(DOCKER_SERVICE_NAME)' DOCKER_COMMAND='$(DOCKER_COMMAND)'"

# @see https://cloud.google.com/compute/docs/instances/view-ip-address
.PHONY: gcp-show-ip
gcp-show-ip: ## Show the IP address of the VM specified by GCP_VM_NAME.
    gcloud compute instances describe $(GCP_VM_NAME) --zone $(GCP_ZONE) --project=$(GCP_PROJECT_ID) --format='get(networkInterfaces[0].accessConfigs[0].natIP)'
</code></pre>

<ul>
<li><code>gcp-init</code> ensures that the correct service account is 
<a href="/blog/gcp-compute-instance-vm-docker/#set-up-the-gcloud-cli-tool">activated for the <code>gcloud</code> cli</a>
and is also used for 
<a href="/blog/gcp-compute-instance-vm-docker/#authenticate-docker"><code>docker</code> authentication</a></li>
<li><code>validate-gcp-variables</code> is the pendant to the <code>validate-docker-variables</code> in the 
<code>.make/02-00-docker.mk</code> file and checks if the default variables <code>GCP_PROJECT_ID</code>, 
<code>GCP_ZONE</code> and <code>GCP_VM_NAME</code> exist. See also section
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#enforce-required-parameters">Enforce required parameters</a></li>
<li><code>gcp-ssh-command</code> is used to run arbitrary SSH commands on the VM using
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh"><code>gcloud compute ssh</code></a>
The command is defined via the <code>COMMAND</code> variable</li>
<li><code>gcp-ssh-login</code> is a convenience target to 
<a href="/blog/gcp-compute-instance-vm-docker/#login-using-the-identity-aware-proxy-iap-concept">log into the GCP VM via SSH using IAP tunneling</a></li>
<li><code>gcp-scp-command</code> copies files from the local system to the VM via <code>scp</code> using 
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/scp"><code>gcloud compute scp</code></a>
The source must be defined via the <code>SOURCE</code> variable and the destination via the <code>DESTINATION</code> 
variable</li>
<li><code>gcp-secret-get</code> retrieves a secret from the <a href="/blog/gcp-compute-instance-vm-docker/#set-up-the-secret-manager">Secret Manager</a>.
The secret has to specified via the <code>SECRET_NAME</code> variable and an optional version can be given 
via the <code>VERSION</code> variable (that defaults to <code>latest</code> if omitted)</li>
<li><code>gcp-docker-exec</code> runs the <code>gcp-ssh-command</code> target to execute the 
<a href="#additional-docker-recipes"><code>docker-exec</code> target</a> on the VM</li>
<li><code>gcp-show-ip</code> <a href="https://cloud.google.com/compute/docs/instances/view-ip-address">retrieves the ip address of the VM on GCP</a>
as explained in</li>
</ul>

<p><!-- generated -->
<a id='infrastructure-recipes'> </a>
<!-- /generated --></p>

<h3>Infrastructure recipes</h3>

<p>For the infrastructure, we currently only have a single target defined in the 
<code>.make/04-00-infrastructure.mk</code> file</p>

<pre><code class="language-Makefile">##@ [Infrastructure]

.PHONY: infrastructure-setup-vm
infrastructure-setup-vm: ## Set the GCP VM up
    bash .infrastructure/setup-gcp.sh   
</code></pre>

<ul>
<li><code>infrastructure-setup-vm</code> runs the script defined in the
<a href="#the-infrastructure-directory"><code>.infrastructure/setup-gcp.sh</code></a></li>
</ul>

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

<h3>Deployment recipes</h3>

<p>The <a href="#deployment-workflow">Deployment workflow</a> is described in more detail in the following 
section, thus I'll only show the corresponding targets defined in <code>.make/05-00-deployment.mk</code>
here</p>

<pre><code class="language-Makefile">##@ [Deployment]

.PHONY: deploy
deploy: # Build all images and deploy them to GCP
    @printf "$(GREEN)Switching to 'local' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init
    @printf "$(GREEN)Starting docker setup locally$(NO_COLOR)\n"
    @make --no-print-directory docker-up
    @printf "$(GREEN)Verifying that there are no changes in the secrets$(NO_COLOR)\n"
    @make --no-print-directory gpg-init
    @make --no-print-directory deployment-guard-secret-changes
    @printf "$(GREEN)Verifying that there are no uncommitted changes in the codebase$(NO_COLOR)\n"
    @make --no-print-directory deployment-guard-uncommitted-changes
    @printf "$(GREEN)Initializing gcloud$(NO_COLOR)\n"
    @make --no-print-directory gcp-init
    @printf "$(GREEN)Switching to 'prod' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init ENVS="ENV=prod TAG=latest"
    @printf "$(GREEN)Creating build information file$(NO_COLOR)\n"
    @make --no-print-directory deployment-create-build-info-file
    @printf "$(GREEN)Building docker images$(NO_COLOR)\n"
    @make --no-print-directory docker-build
    @printf "$(GREEN)Pushing images to the registry$(NO_COLOR)\n"
    @make --no-print-directory docker-push
    @printf "$(GREEN)Creating the deployment archive$(NO_COLOR)\n"
    @make deployment-create-tar
    @printf "$(GREEN)Copying the deployment archive to the VM and run the deployment$(NO_COLOR)\n"
    @make --no-print-directory deployment-run-on-vm
    @printf "$(GREEN)Clearing deployment archive$(NO_COLOR)\n"
    @make --no-print-directory deployment-clear-tar
    @printf "$(GREEN)Switching to 'local' environment$(NO_COLOR)\n"
    @make --no-print-directory make-init

# directory on the VM that will contain the files to start the docker setup
CODEBASE_DIRECTORY=/tmp/codebase

IGNORE_SECRET_CHANGES?=

.PHONY: deployment-guard-secret-changes
deployment-guard-secret-changes: ## Check if there are any changes between the decrypted and encrypted secret files
    if ( ! make secret-diff || [ "$$(make secret-diff | grep ^@@)" != "" ] ) &amp;&amp; [ "$(IGNORE_SECRET_CHANGES)" == "" ] ; then \
        printf "Found changes in the secret files =&gt; $(RED)ABORTING$(NO_COLOR)\n\n"; \
        printf "Use with IGNORE_SECRET_CHANGES=true to ignore this warning\n\n"; \
        make secret-diff; \
        exit 1; \
    fi
    @echo "No changes in the secret files!"

IGNORE_UNCOMMITTED_CHANGES?=

.PHONY: deployment-guard-uncommitted-changes
deployment-guard-uncommitted-changes: ## Check if there are any git changes and abort if so. The check can be ignore by passing `IGNORE_UNCOMMITTED_CHANGES=true`
    if [ "$$(git status -s)" != "" ] &amp;&amp; [ "$(IGNORE_UNCOMMITTED_CHANGES)" == "" ] ; then \
        printf "Found uncommitted changes in git =&gt; $(RED)ABORTING$(NO_COLOR)\n\n"; \
        printf "Use with IGNORE_UNCOMMITTED_CHANGES=true to ignore this warning\n\n"; \
        git status -s; \
        exit 1; \
    fi
    @echo "No uncommitted changes found!"

# FYI: make converts all new lines in spaces when they are echo'd 
# @see https://stackoverflow.com/a/54068252/413531
# To execute a shell command via $(command), the $ has to be escaped with another $
#  ==&gt; $$(command)
# @see https://stackoverflow.com/a/26564874/413531
.PHONY: deployment-create-build-info-file
deployment-create-build-info-file: ## Create a file containing version information about the codebase
    @echo "BUILD INFO" &gt; ".build/build-info"
    @echo "==========" &gt;&gt; ".build/build-info"
    @echo "User  :" $$(whoami) &gt;&gt; ".build/build-info"
    @echo "Date  :" $$(date --rfc-3339=seconds) &gt;&gt; ".build/build-info"
    @echo "Branch:" $$(git branch --show-current) &gt;&gt; ".build/build-info"
    @echo "" &gt;&gt; ".build/build-info"
    @echo "Commit" &gt;&gt; ".build/build-info"
    @echo "------" &gt;&gt; ".build/build-info"
    @git log -1 --no-color &gt;&gt; ".build/build-info"

# create tar archive
#  tar -czvf archive.tar.gz ./source
#
# extract tar archive
#  tar -xzvf archive.tar.gz -C ./target
#
# @see https://www.cyberciti.biz/faq/how-to-create-tar-gz-file-in-linux-using-command-line/
# @see https://serverfault.com/a/330133
.PHONY: deployment-create-tar
deployment-create-tar:
    # create the build directory
    rm -rf .build/deployment
    mkdir -p .build/deployment
    # copy the necessary files
    mkdir -p .build/deployment/.docker/docker-compose/
    cp -r .docker/docker-compose/ .build/deployment/.docker/
    cp -r .make .build/deployment/
    cp Makefile .build/deployment/
    cp .infrastructure/scripts/deploy.sh .build/deployment/
    # make sure we don't have any .env files in the build directory (don't wanna leak any secrets) ...
    find .build/deployment -name '.env' -delete
    # ... apart from the .env file we need to start docker
    cp .secrets/prod/docker.env .build/deployment/.docker/.env
    # create the archive
    tar -czvf .build/deployment.tar.gz -C .build/deployment/ ./

.PHONY: deployment-clear-tar
deployment-clear-tar:
    # clear the build directory
    rm -rf .build/deployment
    # remove the archive
    rm -rf .build/deployment.tar.gz

.PHONY: deployment-run-on-vm
deployment-run-on-vm:## Run the deployment script on the VM
    "$(MAKE)" -s gcp-scp-command SOURCE=".build/deployment.tar.gz" DESTINATION="deployment.tar.gz"
    "$(MAKE)" -s gcp-ssh-command COMMAND="sudo rm -rf $(CODEBASE_DIRECTORY) &amp;&amp; sudo mkdir -p $(CODEBASE_DIRECTORY) &amp;&amp; sudo tar -xzvf deployment.tar.gz -C $(CODEBASE_DIRECTORY) &amp;&amp; cd $(CODEBASE_DIRECTORY) &amp;&amp; sudo bash deploy.sh"

.PHONY: deployment-setup-db-on-vm
deployment-setup-db-on-vm: ## Setup the application on the VM. CAUTION: The docker setup must be running!
    "$(MAKE)" -s gcp-docker-exec DOCKER_SERVICE_NAME="application" DOCKER_COMMAND="make setup-db"
</code></pre>

<ul>
<li><code>deploy</code> is the main target to <a href="#the-deploy-target">trigger a full deployment</a></li>
<li><code>deployment-guard-uncommitted-changes</code> checks if there are changes in the codebase that have 
not been committed to git, yet. We want to avoid that, because that might cause a 
<a href="#avoiding-code-drift">"drift" between the codebase and the deployed code</a></li>
<li><code>deployment-guard-secret-changes</code> does the same but for secrets</li>
<li><code>deployment-create-build-info-file</code> creates 
<a href="#the-build-info-file">the <code>build-info</code> file</a> in the <code>.build/</code> directory</li>
<li><code>deployment-create-tar</code> 
<a href="#create-the-deployment-archive">creates a deployment <code>tar</code> archive</a> that contains all necessary 
files to start the <code>docker</code> setup via <code>docker compose</code> on the VM</li>
<li><code>deployment-clear-tar</code> removes the archive including any temporary files that have been 
created via <code>deployment-create-tar</code></li>
<li><code>deployment-run-on-vm</code> copies the deployment <code>tar</code> archive to the VM, extracts it and runs the 
deployment script defined in <a href="#the-infrastructure-directory">.infrastructure/scripts/deploy.sh</a>, see
section <a href="#deployment-commands-on-the-vm">Deployment commands on the VM</a></li>
<li><code>deployment-setup-db-on-vm</code> executes the <code>setup-db</code> command in the <code>application</code> container on 
the VM</li>
</ul>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You should now be ready to deploy a dockerized PHP application "to 
production" on GCP via <code>docker compose</code>.</p>

<p>In the next part of this tutorial, we will use terraform to create the infrastructure for 
production deployments on GCP and deploy the docker containers on individual VMs without 
<code>docker compose</code>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Wed, 29 Jun 2022 06:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/deploy-docker-compose-php-gcp-poc/</guid>
            </item>
                    <item>
                <title>A primer on GCP Compute Instance VMs for dockerized Apps [Tutorial Part 8]</title>
                <description><![CDATA[<p>In the eighth part of this tutorial series on developing PHP on Docker we will <strong>take a look on 
the Google Cloud Platform (GCP)</strong> and <strong>create a Compute Instance VM</strong> to <strong>run dockerized 
applications</strong>. This includes:</p>

<ul>
<li>creating VMs</li>
<li>using a container registry</li>
<li>using a secret manager</li>
</ul>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/RScVUaNHNxs" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/uPx9AZPOMrA" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch with the final result of this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-8-gcp-compute-instance-vm-docker">part-8-gcp-compute-instance-vm-docker</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/ci-pipeline-docker-php-gitlab-github/">Create a CI pipeline for dockerized PHP Apps</a>.
and the following one is
<a href="/blog/deploy-docker-compose-php-gcp-poc">Deploy dockerized PHP Apps to production on GCP via docker compose</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a> to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#set-up-a-gcp-project">Set up a GCP project</a></li>
<li><a href="#create-a-service-account">Create a service account</a>

<ul>
<li><a href="#create-service-account-key-file">Create service account key file</a></li>
<li><a href="#configure-iam-permissions">Configure IAM permissions</a></li>
</ul></li>
<li><a href="#set-up-the-gcloud-cli-tool">Set up the <code>gcloud</code> CLI tool</a></li>
<li><a href="#set-up-the-container-registry">Set up the Container Registry</a>

<ul>
<li><a href="#authenticate-docker">Authenticate docker</a></li>
<li><a href="#pushing-images-to-the-registry">Pushing images to the registry</a></li>
<li><a href="#images-are-stored-in-google-cloud-storage-buckets">Images are stored in Google Cloud Storage buckets</a></li>
<li><a href="#pulling-images-from-the-registry">Pulling images from the registry</a></li>
</ul></li>
<li><a href="#set-up-the-secret-manager">Set up the Secret Manager</a>

<ul>
<li><a href="#create-a-secret-via-the-ui">Create a secret via the UI</a></li>
<li><a href="#view-a-secret-via-the-ui">View a secret via the UI</a></li>
<li><a href="#retrieve-a-secret-via-the-gcloud-cli">Retrieve a secret via the <code>gcloud</code> cli</a></li>
<li><a href="#add-the-secret-gpg-key-and-password">Add the secret <code>gpg</code> key and password</a></li>
</ul></li>
<li><a href="#compute-instances-the-gcp-vms">Compute Instances: The GCP VMs </a>

<ul>
<li><a href="#create-a-vm">Create a VM</a>

<ul>
<li><a href="#general-vm-settings">General VM settings</a></li>
<li><a href="#firewall-and-networks-tags">Firewall and networks tags</a></li>
<li><a href="#the-role-of-the-service-account">The role of the service account</a></li>
<li><a href="#adding-a-public-ssh-key">Adding a public SSH key</a></li>
<li><a href="#define-availability-policies">Define Availability Policies</a></li>
<li><a href="#the-actual-vm-creation">The actual VM creation</a></li>
</ul></li>
<li><a href="#log-into-a-vm">Log into a VM</a>

<ul>
<li><a href="#login-via-ssh-from-the-gcp-ui">Login via SSH from the GCP UI</a></li>
<li><a href="#login-via-ssh-with-your-own-key-from-your-host-machine">Login via SSH with your own key from your host machine</a></li>
<li><a href="#login-using-the-identity-aware-proxy-iap-concept">Login using the Identity-Aware Proxy (IAP) concept</a>

<ul>
<li><a href="#additional-notes-on-iap">Additional notes on IAP</a></li>
</ul></li>
<li><a href="#get-root-permissions">Get <code>root</code> permissions</a></li>
</ul></li>
<li><a href="#ssh-and-scp-commands"><code>ssh</code> and <code>scp</code> commands</a>

<ul>
<li><a href="#gcloud-ssh-command"><code>gcloud ssh --command=""</code></a></li>
<li><a href="#gcloud-scp"><code>gcloud scp</code></a></li>
</ul></li>
</ul></li>
<li><a href="#provision-the-vm">Provision the VM</a>

<ul>
<li><a href="#get-the-secret-gpg-key-and-password-from-the-secret-manager">Get the secret <code>gpg</code> key and password from the Secret Manager</a></li>
<li><a href="#installing-docker-and-docker-compose">Installing <code>docker</code> and <code>docker compose</code></a></li>
<li><a href="#authenticate-docker-via-gcloud">Authenticate docker via <code>gcloud</code></a></li>
<li><a href="#pulling-the-nginx-image">Pulling the <code>nginx</code> image</a></li>
<li><a href="#start-the-nginx-container">Start the <code>nginx</code> container</a></li>
</ul></li>
<li><a href="#automate-via-gcloud-commands">Automate via <code>gcloud</code> commands</a>

<ul>
<li><a href="#preconditions-project-and-owner-service-account">Preconditions: Project and <code>Owner</code> service account</a></li>
<li><a href="#configure-gcloud-to-use-the-master-service-account">Configure <code>gcloud</code> to use the master service account</a></li>
<li><a href="#enable-apis">Enable APIs</a></li>
<li><a href="#create-and-configure-a-deployment-service-account">Create and configure a "deployment" service account</a></li>
<li><a href="#create-secrets">Create secrets</a></li>
<li><a href="#create-firewall-rule-for-http-traffic">Create firewall rule for HTTP traffic</a></li>
<li><a href="#create-a-compute-instance-vm">Create a Compute Instance VM</a></li>
<li><a href="#provisioning">Provisioning</a></li>
<li><a href="#deployment">Deployment</a></li>
</ul></li>
<li><a href="#putting-it-all-together">Putting it all together </a></li>
<li><a href="#cleanup">Cleanup</a></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>In the next tutorial we will
<a href="/blog/deploy-docker-compose-php-gcp-poc">deploy our dockerized PHP Apps to "production" via docker compose</a>
and will create this "production" environment on <strong>GCP (Google Cloud Platform)</strong>. 
This tutorial serves as a primer on GCP to build up some fundamental knowledge, because we will 
use the platform to provide all the <strong>infrastructure required to run our dockerized PHP 
application</strong>.</p>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-services.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-services.PNG" alt="GCP Services" /></a></p>

<p>The following video shows the overall flow</p>

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

<p>The API keys (see <a href="#create-service-account-key-file">service account key files</a>) that I use are 
<strong>not</strong> in the repository, because I would be billed for any usage. I.e. <strong>you must create you own 
project and keys</strong> to follow along.</p>

<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>

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

<h2>Set up a GCP project</h2>

<p>On GCP, resources are organized under so-called 
<a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects">projects</a>. We can 
create a project via the <a href="https://console.cloud.google.com/projectcreate">Create Project UI</a>:</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-create-new-project.PNG" alt="Create a new GCP project" /></a></p>

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

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

<h2>Create a service account</h2>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-create-new-service-account.PNG" alt="Create a new GCP service account" /></a></p>

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

<h3>Create service account key file</h3>

<p>In order to <strong>use the account programmatically</strong>, we also need to 
<a href="https://cloud.google.com/iam/docs/creating-managing-service-account-keys#creating">create a key file</a>
by choosing the "Manage Keys" option of the corresponding service account.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.PNG" alt="Create a new key file for a GCP service account UI" /></a></p>

<p>This will open up a UI at</p>

<pre><code class="language-text">https://console.cloud.google.com/iam-admin/serviceaccounts/details/$serviceAccountId/keys
</code></pre>

<p>where <code>$serviceAccountId</code> is the numeric id of the service account, e.g. <code>109548647107864470967</code>.
To create a key:</p>

<ul>
<li>click <code>"ADD KEY"</code> and select <code>"Create new key"</code> from the drop down menu

<ul>
<li>This will bring up a modal window to choose the key type.</li>
</ul></li>
<li>select the recommended JSON type and click <code>"Create"</code>.

<ul>
<li>GCP will then <strong>generate a new key pair</strong>, store the public key and offer the private key file as 
download.</li>
</ul></li>
<li>download the file and make sure to treat it like any other private key (ssh, gpg, ...) 
i.e. <strong>never share it publicly</strong>!</li>
</ul>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif"><img src="/img/gcp-compute-instance-vm-docker/gcp-create-service-account-key-file.gif" alt="Create a new key file for a GCP service account" /></a></p>

<p>We will <strong>store this file in the root of the codebase</strong> at <code>gcp-service-account-key.json</code> and add it 
to the <code>.gitignore</code> file.</p>

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

<pre><code class="language-text">$ grep "email" ./gcp-service-account-key.json
  "client_email": "docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com",
</code></pre>

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

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

<h3>Configure IAM permissions</h3>

<p><strong>IAM</strong> stands for <a href="https://cloud.google.com/iam/docs#docs">Identity and Access Management (IAM)</a> 
and is used for <strong>managing permissions on GCP</strong>. The 
<a href="https://cloud.google.com/iam/docs/understanding-roles">two core concepts are "permissions" and "roles"</a>:</p>

<ul>
<li><strong>permissions</strong> are fine-grained for particular actions, e.g. <code>storage.buckets.create</code> to "Create 
Cloud Storage buckets"</li>
<li><strong>roles</strong> combine a selection of permissions, e.g. the <code>Cloud Storage Admin</code> role has 
permissions like

<ul>
<li><code>storage.buckets.create</code></li>
<li><code>storage.buckets.get</code></li>
<li>etc.</li>
</ul></li>
<li>roles are assigned to <strong>users</strong> (or service accounts)</li>
</ul>

<p>You can find a full overview of all permissions in the 
<a href="https://cloud.google.com/iam/docs/permissions-reference">Permissions Reference</a> and all roles 
under 
<a href="https://cloud.google.com/iam/docs/understanding-roles#predefined">Understanding roles > Predefined roles</a>.</p>

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

<ul>
<li><code>Storage Admin</code>

<ul>
<li>required to <a href="#pushing-images-to-the-registry">create the GCP bucket for the registry</a> and to 
<a href="#pulling-images-from-the-registry">pull the images on the VM</a></li>
</ul></li>
<li><code>Secret Manager Admin</code>

<ul>
<li>required to <a href="#get-the-secret-gpg-key-and-password-from-the-secret-manager">retrieve secrets from the Secret Manager</a></li>
</ul></li>
<li><code>Compute Admin</code>, <code>Service Account User</code> and <code>IAP-secured Tunnel User</code>

<ul>
<li>are necessary for <a href="#additional-notes-on-iap">logging into a VM via IAP</a>.</li>
</ul></li>
</ul>

<p>Roles can be assigned through the
<a href="https://console.cloud.google.com/iam-admin/iam">Cloud Console IAM UI</a> by editing the
corresponding user.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-iam-permissions.PNG" alt="Managing IAM permissions" /></a></p>

<p><strong>Caution:</strong> It might take some time (usually a couple of seconds) until changes in IAM
permissions take effect.</p>

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

<h2>Set up the <code>gcloud</code> CLI tool</h2>

<p>The <a href="https://cloud.google.com/sdk/gcloud">CLI tool for GCP is called <code>gcloud</code></a> and is
<a href="https://cloud.google.com/sdk/docs/install">available for all operating systems</a>.</p>

<p>In this tutorial we are using version <code>380.0.0</code> <strong>installed natively on Windows</strong> via the
<a href="https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe">GoogleCloudSDKInstaller.exe</a>
using the "Bundled Python" option.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcloud-installation-options.PNG" alt="Install <code>gcloud</code> on Windows" /></a></p>

<p>FYI: As described under
<a href="https://cloud.google.com/sdk/docs/uninstall-cloud-sdk">Uninstalling the Google Cloud CLI</a>
you can find the installation and config directories via</p>

<pre><code class="language-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
</code></pre>

<p>I will <em>not</em> use my personal Google account to run <code>gcloud</code> commands, thus I'm <em>not</em> using the 
"usual" initialization process
<a href="https://cloud.google.com/sdk/docs/initializing">by running <code>gcloud init</code></a>. Instead, I will use
the <a href="#create-a-service-account">service account that we created previously</a> and activate it as 
described under
<a href="https://cloud.google.com/sdk/gcloud/reference/auth/activate-service-account">gcloud auth activate-service-account</a>
via</p>

<pre><code class="language-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
</code></pre>

<p>Output</p>

<pre><code class="language-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]
</code></pre>

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

<pre><code class="language-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]
</code></pre>

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

<h2>Set up the Container Registry</h2>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-enable-container-registry-api.PNG" alt="Enable the GCP Container Registry API" /></a></p>

<p>You find the <strong>Container Registry</strong> in the Cloud Console UI under 
<a href="https://console.cloud.google.com/gcr">Container Registry</a>.</p>

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

<h3>Authenticate docker</h3>

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

<pre><code class="language-bash">key=./gcp-service-account-key.json
cat "$key" | docker login -u _json_key --password-stdin https://gcr.io
</code></pre>

<p>A successful authentication looks as follows:</p>

<pre><code class="language-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/
</code></pre>

<p>So what exactly "happens" when we run this command? According to the 
<a href="https://docs.docker.com/engine/reference/commandline/login/"><code>docker login</code>documentation</a></p>

<blockquote>
  <p>When you log in, the command stores credentials in <code>$HOME/.docker/config.json</code> 
  on Linux or <code>%USERPROFILE%/.docker/config.json</code> on Windows
  [...]</p>
  
  <p>The Docker Engine can keep user credentials in an external credentials store, 
  such as the native keychain of the operating system.
  [...]</p>
  
  <p>You need to specify the credentials store in 
  <code>$HOME/.docker/config.json</code> to tell the docker engine to use it.
  [...]</p>
  
  <p>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.</p>
</blockquote>

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

<pre><code class="language-text">$ cat ~/.docker/config.json
{
        "auths": {
                "gcr.io": {}
        },
        "credsStore": "desktop"
}
</code></pre>

<p>FYI: <code>"desktop"</code> seems to be a 
<a href="https://forums.docker.com/t/docker-windows-desktop-credentials-location/107251">wrapper for the Wincred executable</a>.</p>

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

<h3>Pushing images to the registry</h3>

<p>For this tutorial, we will create a super simple <code>nginx</code> alpine image that provides a "Hello 
world" <code>hello.html</code> file via</p>

<pre><code class="language-bash">docker build -t my-nginx -f - . &lt;&lt;EOF
FROM nginx:1.21.5-alpine

RUN echo "Hello world" &gt;&gt; /usr/share/nginx/html/hello.html

EOF
</code></pre>

<p>The name of the image is <code>my-nginx</code></p>

<pre><code class="language-text">$ docker image ls | grep my-nginx
my-nginx         latest             42dd1608d126   50 seconds ago    23.5MB
</code></pre>

<p>In order to 
<a href="https://docs.docker.com/engine/reference/commandline/push/">push an image to a registry</a>,
<strong>the image name must be prefixed with the corresponding registry</strong>. This was quite confusing to 
me, because I would have expected to be able to run something like this:</p>

<pre><code class="language-bash">$ docker push my-nginx --registry=gcr.io

unknown flag: --registry
See 'docker push --help'.
</code></pre>

<p>But nope, there is no such <code>--registry</code> option. Even worse: <strong>Omitting it would cause a push to 
<code>docker.io</code></strong>, the "default" registry:</p>

<pre><code class="language-text">$ docker push my-nginx
Using default tag: latest
The push refers to repository [docker.io/my-nginx]
</code></pre>

<p>According to 
<a href="https://cloud.google.com/container-registry/docs/pushing-and-pulling">the GCP docs on Pushing and pulling images</a>, 
the following steps are necessary to push an image to a GCP registry:</p>

<blockquote>
  <ul>
  <li><strong>Tag</strong> the image with its target path in Container Registry, including the gcr.io registry 
  host and the project ID my-project</li>
  <li><strong>Push</strong> the image to the registry</li>
  </ul>
</blockquote>

<p>In our case <strong>the target path to our Container Registry</strong> is</p>

<pre><code class="language-text">gcr.io/pl-dofroscra-p
</code></pre>

<p>because <code>pl-dofroscra-p</code> is the <a href="#set-up-a-gcp-project">id of the GCP project we created previously</a>.</p>

<p>The <strong>full image name</strong> becomes</p>

<pre><code class="language-text">gcr.io/pl-dofroscra-p/my-nginx
</code></pre>

<p>To push the <code>my-nginx</code> image, we must first <strong>"add another name"</strong> to it via 
<a href="https://docs.docker.com/engine/reference/commandline/tag/"><code>docker tag</code></a></p>

<pre><code class="language-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
</code></pre>

<p>and <strong>push that name afterwards</strong></p>

<pre><code class="language-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
</code></pre>

<p>You can then find the image in the 
<a href="https://console.cloud.google.com/gcr">UI of the Container Registry</a>:</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-container-registry-image-example.PNG" alt="Example of a pushed image in the Container Registry" /></a></p>

<p>Don't worry: We won't have to do the tagging every time before a push, because we will 
<a href="/blog/deploy-docker-compose-php-gcp-poc/#adding-gcp-values-to-make-variables-env">set up <code>make</code> to use the correct name automatically</a>
when building the images in the <a href="/blog/deploy-docker-compose-php-gcp-poc/">next part</a>.</p>

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

<h3>Images are stored in Google Cloud Storage buckets</h3>

<p>We <a href="#configure-iam-permissions">assigned the <code>Storage Admin</code> role to the service account previously</a>
that contains the <code>storage.buckets.create</code> permission. If we wouldn't have done that, the 
following error would have occurred:</p>

<pre><code class="language-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
</code></pre>

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

<blockquote>
  <p>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.</p>
</blockquote>

<p>You can find the bucket, that in my case is named <code>artifacts.pl-dofroscra-p.appspot.com</code>
in the <a href="https://console.cloud.google.com/storage">Cloud Storage UI</a>:</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-cloud-storage-registry-images.PNG" alt="GCP Container Registry image location on Cloud Storage" /></a></p>

<p><strong>CAUTION</strong>: Make sure to <strong>delete this bucket once you are done with the tutorial</strong> - otherwise 
<a href="https://cloud.google.com/storage/pricing">storage costs</a> will incur.</p>

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

<h3>Pulling images from the registry</h3>

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

<p>Then use the <strong>fully qualified image name</strong> as before:</p>

<pre><code class="language-bash">docker pull gcr.io/pl-dofroscra-p/my-nginx
</code></pre>

<p>Output if the image is cached</p>

<pre><code class="language-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
</code></pre>

<p>or if doesn't exist</p>

<pre><code class="language-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
</code></pre>

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

<h2>Set up the Secret Manager</h2>

<p>Even though 
<a href="/blog/git-secret-encrypt-repository-docker/">we use <code>git secret</code> to manage our secrets</a>, <strong>we 
still need the <code>gpg</code> secret key</strong> for decryption. This key is "a secret in itself" and we will use 
the <a href="https://cloud.google.com/secret-manager/docs">GCP Secret Manager</a> to store it and
<a href="#get-the-secret-gpg-key-and-password-from-the-secret-manager">retrieve it later from a VM</a>.</p>

<p>It can be managed from the <a href="https://console.cloud.google.com/security/secret-manager">Security > Secret Manager UI</a> 
once we have
<a href="https://console.cloud.google.com/marketplace/product/google/secretmanager.googleapis.com">enabled the Secret Manager API</a></p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-enable-secret-manager-api.PNG" alt="Enable the GCP Secret Manager API" /></a></p>

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

<h3>Create a secret via the UI</h3>

<p>To create a secret:</p>

<ul>
<li>navigate to the <a href="https://console.cloud.google.com/security/secret-manager">Secret Manager UI</a> 
and click the <code>"+ CREATE SECRET"</code> button</li>
<li>enter the <strong>secret name</strong> and <strong>secret value</strong> in the form (we can ignore the other advanced 
settings like "Replication policy" and "Encryption" for now)

<ul>
<li>FYI: the secret name can only contain English letters (A-Z), numbers (0-9), dashes (-), and 
underscores (&#95;)</li>
</ul></li>
<li>click the <code>"CREATE SECRET"</code> button</li>
</ul>

<p>The following gif shows the creation of a secret named <code>my_secret_key</code> with the value 
<code>my_secret_value</code>.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif"><img src="/img/gcp-compute-instance-vm-docker/gcp-create-and-view-secret.gif" alt="Create and view a secret" /></a></p>

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

<h3>View a secret via the UI</h3>

<p>The <a href="https://console.cloud.google.com/security/secret-manager">Security > Secret Manager UI</a> 
lists all existing secrets. Clicking on a secret will lead you to the <strong>Secret Detail UI</strong> at the URL</p>

<pre><code class="language-text">https://console.cloud.google.com/security/secret-manager/secret/$secretName/versions
</code></pre>

<p>e.g. for secret name <code>my_secret_key</code>:</p>

<pre><code class="language-text">https://console.cloud.google.com/security/secret-manager/secret/my_secret_key/versions
</code></pre>

<p>The UI shows all <a href="https://cloud.google.com/secret-manager/docs/managing-secret-versions">versions of the secret</a>,
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 
<a href="#create-a-secret-via-the-ui">previous section</a>).</p>

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

<h3>Retrieve a secret via the <code>gcloud</code> cli</h3>

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

<pre><code class="language-bash">gcloud secrets list
</code></pre>

<pre><code class="language-text">$ gcloud secrets list
NAME           CREATED              REPLICATION_POLICY  LOCATIONS
my_secret_key  2022-05-15T05:38:11  automatic           -
</code></pre>

<p>To "see" the value of <code>my_secret_key</code>, we must also define the corresponding version. All 
versions can be listed via</p>

<pre><code class="language-bash">gcloud secrets versions list my_secret_key
</code></pre>

<pre><code class="language-text">$ gcloud secrets versions list my_secret_key
NAME  STATE    CREATED              DESTROYED
1     enabled  2022-05-15T05:38:13  -
</code></pre>

<p>The actual secret value for version <code>1</code> of <code>my_secret_key</code> is accessed via</p>

<pre><code class="language-bash">gcloud secrets versions access 1 --secret=my_secret_key
</code></pre>

<pre><code class="language-text">$ gcloud secrets versions access 1 --secret=my_secret_key
my_secret_value
</code></pre>

<p>You can also use 
<a href="https://cloud.google.com/sdk/gcloud/reference/secrets/versions/access">the string <code>latest</code> as version to retrieve the latest enabled version</a></p>

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

<pre><code class="language-bash">gcloud secrets versions access latest --secret=my_secret_key
</code></pre>

<pre><code class="language-text">$ gcloud secrets versions access latest --secret=my_secret_key
my_secret_value
</code></pre>

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

<h3>Add the secret <code>gpg</code> key and password</h3>

<p>We already know how to
<a href="/blog/git-secret-encrypt-repository-docker/#create-gpg-key-pair">create another <code>gpg</code> key pair</a>
and 
<a href="/blog/git-secret-encrypt-repository-docker/#adding-listing-and-removing-users">add the corresponding email to <code>git secret</code></a>
from when we have set up the CI pipelines (see section 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/#add-a-password-protected-secret-gpg-key">Add a password-protected secret gpg key</a>)
. We will do the same once more for the production environment via</p>

<pre><code class="language-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 &lt;&lt;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 &gt; $public
</code></pre>

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

<p>I have also added the secret <code>gpg</code> key and password as secrets to the secret manager</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-gpg-secret-password.PNG" alt="GPG secret key and password stored as secrets" /></a></p>

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

<h2>Compute Instances: The GCP VMs</h2>

<p>Compute Instances are the equivalent of <a href="https://aws.amazon.com/de/ec2/">AWS EC2 instances</a>.</p>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-enable-compute-instance-api.PNG" alt="Enable the GCP Compute Instance API" /></a></p>

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

<h3>Create a VM</h3>

<p>We can simply create a new instance from the 
<a href="https://console.cloud.google.com/compute/instancesAdd">Create an instance UI</a>.</p>

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

<h4>General VM settings</h4>

<p>We'll use the following settings:</p>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-settings.PNG" alt="GCP instance settings" /></a></p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-service-account-firewall.PNG" alt="Additional GCP instance settings" /></a></p>

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

<h4>Firewall and networks tags</h4>

<p>Ticking the <code>Allow HTTP traffic</code> checkbox will cause two things when the instance is created:</p>

<ul>
<li>a new <strong>firewall rule</strong> named <code>default-allow-http</code> is created that allows incoming traffic from 
port <code>80</code> and is only applied to instances with the 
<a href="https://cloud.google.com/vpc/docs/add-remove-network-tags">network tag</a> <code>http-server</code>. You 
can see the new rule in the 
<a href="https://console.cloud.google.com/networking/firewalls/list">Firewall UI</a>
<a href="/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-firewall-rule-default-allow-http.PNG" alt="Firewall rule: default-allow-http" /></a></li>
<li>the <strong>network tag</strong> <code>http-server</code> is added to the instance, effectively enabling the 
aforementioned firewall rule for the instance</li>
</ul>

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

<h4>The role of the service account</h4>

<p>The <strong>service account</strong> that we have chosen in the previous step <strong>will be attached to the 
Compute Instance</strong>. I.e. it will be available on the instance and <strong>we will have access to the
account when we log into the instance</strong>. Conveniently, 
<a href="https://cloud.google.com/sdk/docs/install-sdk#installing_the_latest_version">the<code>gcloud</code> cli is pre-installed on every Compute Instance</a>:</p>

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcloud-cli-preinstalled-compute-instance.PNG" alt="The<code>gcloud</code> cli is pre-installed on every Compute Instance" /></a></p>

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

<p>We will use the service account later, 
<a href="#pulling-the-nginx-image">when we log into the Compute Instance and run <code>docker pull</code> from there</a>
to pull images from the registry.</p>

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

<h4>Adding a public SSH key</h4>

<p>In addition, I <strong>added my own public <code>ssh</code> key</strong></p>

<pre><code class="language-text">ssh-rsa AAAAB3NzaC1yc2....6row== pascal.landau@MY_LAPTOP
</code></pre>

<p><strong>CAUTION</strong>: The <strong>username</strong> for this key will be defined <strong>at the end of the key</strong>! E.g. in the 
example above, the username would be <code>pascal.landau</code>. This is important for 
<a href="#login-via-ssh-with-your-own-key-from-your-host-machine">logging in later via SSH from your local machine</a>.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-ssh-key.PNG" alt="Add SSH key to GCP instance settings" /></a></p>

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

<h4>Define Availability Policies</h4>

<p>We will make the instance <a href="https://cloud.google.com/compute/docs/instances/preemptible">preemptible</a>,
by choosing the <strong>VM provisioning model</strong> <code>Spot</code>.
This makes it <strong>much cheaper</strong> but GCP "might" <strong>terminate the instance randomly</strong> 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.</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-availability-policies.PNG" alt="Define Availability Policies" /></a></p>

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

<h4>The actual VM creation</h4>

<p>Finally, click the <code>"Create"</code> button at the bottom of the page</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-settings-create.PNG" alt="Create the GCP instance" /></a></p>

<p>Once the instance is created, you can see it in the 
<a href="https://console.cloud.google.com/compute/instances">Compute Instances > VM instances UI</a></p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-instance-overview.PNG" alt="Compute Instance overview" /></a></p>

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

<p><strong>CAUTION</strong>: Make sure to <strong>shutdown and remove this instance by the end of the tutorial</strong> - otherwise
<a href="https://cloud.google.com/compute/all-pricing">compute costs</a> will incur.</p>

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

<h3>Log into a VM</h3>

<p>There are multiple ways <strong>to log into the VM / Compute Instance</strong> outlined in 
<a href="https://cloud.google.com/compute/docs/instances/connecting-advanced">Connecting to Linux VMs using advanced methods</a>.</p>

<p>I'm going to describe three of them:</p>

<ul>
<li><a href="#login-via-ssh-from-the-gcp-ui">Login via SSH from the GCP UI</a></li>
<li><a href="#login-via-ssh-with-your-own-key-from-your-host-machine">Login via SSH with your own key from your host machine</a></li>
<li><a href="#login-using-the-identity-aware-proxy-iap-concept">Login using the Identity-Aware Proxy (IAP) concept</a></li>
</ul>

<p>PS: It's worth keeping 
<a href="https://cloud.google.com/compute/docs/troubleshooting/troubleshooting-ssh">the Troubleshooting SSH guide</a>
as a bookmark.</p>

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

<h4>Login via SSH from the GCP UI</h4>

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

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

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif"><img src="/img/gcp-compute-instance-vm-docker/gcp-ssh-login-web-shell.gif" alt="Connect via Cloud Console UI" /></a></p>

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

<h4>Login via SSH with your own key from your host machine</h4>

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

<p>All of those requirements are true 
<a href="#create-a-vm">for the Compute Instance that we just created</a>:</p>

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

<p>So we can now simply login via</p>

<pre><code class="language-bash">ssh pascal.landau@35.192.212.130
</code></pre>

<p>or if you need to specify the location of the private key via the <code>-i</code> option</p>

<pre><code class="language-bash">ssh -i ~/.ssh/id_rsa pascal.landau@35.192.212.130
</code></pre>

<pre><code class="language-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:~$
</code></pre>

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

<h4>Login using the Identity-Aware Proxy (IAP) concept</h4>

<p><strong>Note: This is the preferred way of logging into a GCP VM</strong></p>

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

<p>The corresponding command is
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh"><code>gcloud compute ssh</code></a>, e.g.:</p>

<pre><code class="language-bash">gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p
</code></pre>

<p>Note the
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--tunnel-through-iap">--tunnel-through-iap</a> 
flag: Without it, <code>gcloud</code> would instead attempt to use a "normal" <code>ssh</code> connection if the VM is 
publicly reachable 
<a href="https://cloud.google.com/iap/docs/using-tcp-forwarding#tunneling_ssh_connections">as per docu</a>:</p>

<blockquote>
  <p>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.</p>
</blockquote>

<p>Output</p>

<pre><code class="language-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.
</code></pre>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif"><img src="/img/gcp-compute-instance-vm-docker/gcp-ssh-login-iap.gif" alt="Connect via IAP" /></a></p>

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

<pre><code class="language-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
</code></pre>

<p>It also opens a <a href="https://www.putty.org/"><code>Putty</code> session</a> and logs you into in the instance:</p>

<pre><code class="language-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:~$
</code></pre>

<p><strong>Caution</strong>: The <code>Putty</code> session will be closed automatically when you abort the original 
<code>gcloud compute ssh</code> command!</p>

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

<h5>Additional notes on IAP</h5>

<p>In order to <strong>enable SSH connections via IAP</strong>, our service account needs the following 
<a href="#configure-iam-permissions">IAM roles</a>:</p>

<ul>
<li><a href="https://cloud.google.com/compute/docs/access/iam#compute.instanceAdmin.v1"><code>roles/compute.instanceAdmin.v1</code></a>, role: <code>Compute Admin</code></li>
<li><a href="https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role"><code>roles/iam.serviceAccountUser</code></a>, role: <code>Service Account User</code></li>
<li><a href="https://cloud.google.com/iam/docs/understanding-roles#cloud-iap-roles"><code>roles/iap.tunnelResourceAccessor</code></a>, role: <code>IAP-secured Tunnel User</code></li>
</ul>

<p>Otherwise, you might run into a couple of errors like:</p>

<pre><code class="language-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'
</code></pre>

<p>(<code>roles/compute.instanceAdmin.v1</code> missing)</p>

<pre><code class="language-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
</code></pre>

<p>(<a href="https://cloud.google.com/compute/docs/access/iam#the_serviceaccountuser_role"><code>roles/iam.serviceAccountUser</code> missing</a>)</p>

<pre><code class="language-text">Remote side unexpectedly closed connection
</code></pre>

<p>(<code>roles/iap.tunnelResourceAccessor</code> missing)</p>

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

<p>The general "flow" looks like this</p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-iap-concept.PNG" alt="Connection flow when using Identity-Aware Proxy (IAP)" /></a></p>

<p>Using IAP might look overly complicated at first, but it offers <strong>a number of benefits</strong>:</p>

<ul>
<li>we can leverage <strong>Google's authentication system</strong> and the powerful
<a href="#configure-iam-permissions">IAM permission management</a> to manage permissions - no more need 
to deploy custom SSH keys to the VMs</li>
<li><p>we <strong>don't need a public IP address</strong> any longer, see 
<a href="https://cloud.google.com/iap/docs/tcp-forwarding-overview#how-tcp-forwarding-works">Overview of TCP forwarding > How IAP's TCP forwarding works</a></p>

<blockquote>
  <p>A special case, establishing an SSH connection using <code>gcloud compute ssh</code> wraps the SSH 
  connection inside HTTPS and forwards it to the remote instance without the need of a 
  listening port on local host.</p>
  
  <p>[...]</p>
  
  <p>TCP forwarding with IAP doesn't require a public, routable IP address assigned to your 
  resource. Instead, it uses internal IPs.</p>
</blockquote>

<p>This is nice, because in our final PHP application only the <code>nginx</code> container should be 
accessible publicly - <code>php-fpm</code> and the <code>php-workers</code> shouldn't. Usually we would use a<br />
so-called <a href="https://en.wikipedia.org/wiki/Bastion_host">Bastion Host or Jump Box</a> to deal with 
this problem, but thanks to IAP we don't have to</p></li>
</ul>

<p><strong>Additional resources</strong></p>

<ul>
<li><a href="https://gochronicles.com/secure-tunnel-with-iap-gcp/">Accessing Secure Servers Using IAP</a></li>
<li><a href="https://medium.com/google-cloud/connecting-securely-to-google-compute-engine-vms-without-a-public-ip-or-vpn-720e53d1978e">Connecting Securely to Google Compute Engine VMs without a Public IP or VPN</a></li>
</ul>

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

<h4>Get <code>root</code> permissions</h4>

<p>The group 
<a href="https://superuser.com/a/1400281/434918"><code>google-sudoers</code> exists on each GCP Compute Instance</a>. 
It is configured for <strong>passwordless sudo</strong> via <code>/etc/sudoers.d/google_sudoers</code></p>

<pre><code class="language-text">$ sudo cat /etc/sudoers.d/google_sudoers 
%google-sudoers ALL=(ALL:ALL) NOPASSWD:ALL
</code></pre>

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

<pre><code class="language-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)

# =&gt; 1000(google-sudoers)
</code></pre>

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

<h3><code>ssh</code> and <code>scp</code> commands</h3>

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

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

<h4><code>gcloud ssh --command=""</code></h4>

<p>Documentation: <a href="https://cloud.google.com/sdk/gcloud/reference/compute/ssh#--command"><code>gcloud ssh --command</code></a></p>

<p>Example:</p>

<pre><code class="language-bash">gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="whoami"
</code></pre>

<p>Output:</p>

<pre><code class="language-bash">$ gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="whoami"
Pascal
</code></pre>

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

<h4><code>gcloud scp</code></h4>

<p>Documentation: <a href="https://cloud.google.com/sdk/gcloud/reference/compute/scp"><code>gcloud scp</code></a></p>

<p>Example:</p>

<pre><code class="language-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" &gt;&gt; ./local-file1.txt
echo "2" &gt;&gt; ./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"
</code></pre>

<p>Output:</p>

<pre><code class="language-bash">$ echo "1" &gt;&gt; ./local-file1.txt
$ echo "2" &gt;&gt; ./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
</code></pre>

<p><strong>Note</strong>: According to the examples in the documentation, it should also be possible to use the 
<code>~</code> 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</p>

<pre><code class="language-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
</code></pre>

<p>even though the diretory existed. But removing the <code>~/</code> part worked</p>

<pre><code class="language-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%
</code></pre>

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

<h2>Provision the VM</h2>

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

<h3>Get the secret <code>gpg</code> key and password from the Secret Manager</h3>

<p>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 <code>gpg</code> key and its password to 
<a href="/blog/deploy-docker-compose-php-gcp-poc/#the-secrets-directory">decrypt the secrets in the <code>php</code> containers in the next part</a>.
To recap: We have <a href="#add-the-secret-gpg-key-and-password">added the them previously</a> and can now
retrieve them as explained under section
<a href="#retrieve-a-secret-via-the-gcloud-cli">Retrieve a secret via the <code>gcloud</code> cli</a> via</p>

<pre><code class="language-bash">gcloud secrets versions access 1 --secret=GPG_KEY &gt; secret.gpg

GPG_PASSWORD=$(gcloud secrets versions access 1 --secret=GPG_PASSWORD)
</code></pre>

<pre><code class="language-text">$ gcloud secrets versions access 1 --secret=GPG_KEY &gt; 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
</code></pre>

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

<h3>Installing <code>docker</code> and <code>docker compose</code></h3>

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

<pre><code class="language-bash"># install required tools
sudo apt-get update -yq &amp;&amp; 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 &gt; /dev/null

# install Docker Engine
sudo apt-get update -yq &amp;&amp; sudo apt-get install -yq \
     docker-ce \
     docker-ce-cli \
     containerd.io \
     docker-compose-plugin
</code></pre>

<p>I have also added these instructions to the script <code>.infrastructure/scripts/provision.sh</code>.</p>

<p>Afterwards we check via</p>

<pre><code class="language-bash">docker --version
docker compose version
</code></pre>

<p>if <code>docker</code> and <code>docker compose</code> are available</p>

<pre><code class="language-text">$ docker --version
Docker version 20.10.15, build fd82621

$ docker compose version
Docker Compose version v2.5.0
</code></pre>

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

<h3>Authenticate docker via <code>gcloud</code></h3>

<p>I recommend <a href="#get-root-permissions">running the commands as <code>root</code></a> via</p>

<pre><code class="language-bash">sudo -i
</code></pre>

<pre><code class="language-text">pascal_landau@dofroscra-test:~$ sudo -i
root@dofroscra-test:~# 
</code></pre>

<p>Otherwise, we might run into docker permission errors like</p>

<pre><code class="language-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
</code></pre>

<p>FYI: As an alternative we could also add the non-root user to the <code>docker</code> group via</p>

<pre><code class="language-bash">user=$(whoami)
sudo usermod -a -G docker $user
</code></pre>

<pre><code class="language-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)
</code></pre>

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

<pre><code class="language-bash">gcloud auth configure-docker --quiet
</code></pre>

<pre><code class="language-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.
</code></pre>

<p>This creates the file <code>/root/.docker/config.json</code> that we already encountered when we
<a href="#authenticate-docker">authenticated docker locally to push images</a>.
In this case it has the following content</p>

<pre><code class="language-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"
  }
}
</code></pre>

<p>The <a href="https://docs.docker.com/engine/reference/commandline/login/#credential-helpers"><code>creadHelpers</code> are described in the <code>docker login</code> docs</a></p>

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

<p>In other words:</p>

<ul>
<li>we are using <code>gcr.io</code> as registry</li>
<li>this registry is defined to use the credential helper <code>gcloud</code></li>
<li>i.e. it will use the pre-initialized <code>gcloud</code> cli for authentication</li>
</ul>

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

<h3>Pulling the <code>nginx</code> image</h3>

<p>Once we are authenticated, we can simply run <code>docker pull</code> with the full image name to retrieve the 
<code>nginx</code> image that we <a href="#pushing-images-to-the-registry">pushed previously</a></p>

<pre><code class="language-bash">docker pull gcr.io/pl-dofroscra-p/my-nginx
</code></pre>

<pre><code class="language-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
</code></pre>

<p>If we wouldn't be authenticated, we would run into the following error</p>

<pre><code class="language-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
</code></pre>

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

<h3>Start the <code>nginx</code> container</h3>

<p>For now, we will simply run the <code>nginx</code> container with <code>docker run</code> via</p>

<pre><code class="language-bash">docker run --name nginx -p 80:80 --rm -d gcr.io/pl-dofroscra-p/my-nginx:latest
</code></pre>

<p>We</p>

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

<p>Output</p>

<pre><code class="language-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
</code></pre>

<p>We're <strong>all set to serve HTTP requests</strong>: The requests will be passed to the <code>nginx</code> container 
due to the mapped port <code>80</code>, i.e. we can simply use the public IP address <code>35.192.212.130</code> in a 
<code>curl</code> request and check 
<a href="#pushing-images-to-the-registry">the "Hello world" file we've added to the image</a>:</p>

<pre><code class="language-text">$ curl -s http://35.192.212.130/hello.html
Hello world
</code></pre>

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

<h2>Automate via <code>gcloud</code> commands</h2>

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

<ul>
<li>UIs change</li>
<li>"Clicking" stuff takes quite some time</li>
<li>we can't automate anything</li>
</ul>

<p>Fortunately, we can also <strong>use <a href="#set-up-the-gcloud-cli-tool">the <code>gcloud</code> cli</a> for most of the 
tasks</strong>.</p>

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

<h3>Preconditions: Project and <code>Owner</code> service account</h3>

<p>I'll assume that</p>

<ul>
<li><a href="#set-up-the-gcloud-cli-tool"><code>gcloud</code> is installed</a></li>
<li>a <a href="#set-up-a-gcp-project">GCP project</a> exists</li>
</ul>

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

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

<p>We will use <code>docker-php-tutorial-master</code> as service account ID and 
<a href="#create-service-account-key-file">store the key file</a> of the account at the root of the 
codebase under <code>gcp-master-service-account-key.json</code> (which is also added to the <code>.gitignore</code> file).</p>

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

<h3>Configure <code>gcloud</code> to use the master service account</h3>

<p>Run</p>

<pre><code class="language-bash">gcloud auth activate-service-account --key-file=./gcp-master-service-account-key.json --project=pl-dofroscra-p
</code></pre>

<pre><code class="language-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]
</code></pre>

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

<h3>Enable APIs</h3>

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

<p>The <code>$serviceName</code> is shown on the API overview page of a service, see the following example for 
the <a href="https://console.cloud.google.com/marketplace/product/google/containerregistry.googleapis.com">Container Registry</a></p>

<p><a href="/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG"><img src="/img/gcp-compute-instance-vm-docker/gcp-api-service-name.PNG" alt="Find the service name of an API" /></a></p>

<p>We need the APIs for</p>

<ul>
<li>Container Registry: <code>containerregistry.googleapis.com</code></li>
<li>Secret Manager: <code>secretmanager.googleapis.com</code></li>
<li>Compute Engine: <code>compute.googleapis.com</code></li>
<li>IAM: <code>iam.googleapis.com</code></li>
<li>Cloud Storage: <code>storage.googleapis.com</code></li>
<li>Cloud Resource Manager: <code>cloudresourcemanager.googleapis.com</code></li>
</ul>

<pre><code class="language-bash">gcloud services enable containerregistry.googleapis.com secretmanager.googleapis.com compute.googleapis.com iam.googleapis.com storage.googleapis.com cloudresourcemanager.googleapis.com
</code></pre>

<pre><code class="language-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.
</code></pre>

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

<h3>Create and configure a "deployment" service account</h3>

<p>We won't use the master <code>Owner</code> service account for the deployment but create a custom one with 
only the necessary permissions.</p>

<p>Service accounts are created via
<a href="https://cloud.google.com/sdk/gcloud/reference/iam/service-accounts/create"><code>gcloud iam service-accounts create $serviceAccountId</code></a>,
(see also the <a href="https://cloud.google.com/iam/docs/creating-managing-service-accounts">Docu on "Creating and managing service accounts"</a>).</p>

<p>The <code>$serviceAccountId</code> is the <strong>id of the service account</strong>, e.g.
<a href="#create-a-service-account"><code>docker-php-tutorial-deployment</code> in our previous example</a>. In 
addition, we can define a <code>--description</code> and a <code>--display-name</code>.</p>

<pre><code class="language-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"
</code></pre>

<pre><code class="language-text">$ gcloud iam service-accounts create docker-php-tutorial-deployment \
&gt;   --description="Used for the deployment of the Docker PHP Tutorial application" \
&gt;   --display-name="Docker PHP Tutorial Deployment Account"
Created service account [docker-php-tutorial-deployment].
</code></pre>

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

<p>The <code>$localPathToKeyFile</code> is used to <strong>store the key file locally</strong>, e.g.
<a href="#create-service-account-key-file"><code>gcp-service-account-key.json</code> in our previous example</a> and 
<code>$serviceAccountEmail</code> is the email address of the service account. It has the form</p>

<pre><code class="language-text">$serviceAccountId@$projectId.iam.gserviceaccount.com

e.g.

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

</code></pre>

<p>Example:</p>

<pre><code class="language-bash">gcloud iam service-accounts keys create ./gcp-service-account-key.json \
  --iam-account=docker-php-tutorial-deployment@pl-dofroscra-p.iam.gserviceaccount.com
</code></pre>

<pre><code class="language-text">$ gcloud iam service-accounts keys create ./gcp-service-account-key.json \
&gt;   --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]
</code></pre>

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

<p>The <code>$projectName</code> is the <strong>name of the GCP project</strong>, <code>$serviceAccountEmail</code> the email address 
from before and the <code>$roleId</code> the <strong>name of the role</strong> as listed under
<a href="https://cloud.google.com/iam/docs/understanding-roles">Understanding roles</a>. In our case that's:</p>

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

<pre><code class="language-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
</code></pre>

<pre><code class="language-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
</code></pre>

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

<h3>Create secrets</h3>

<p>Secrets are created via <a href="https://cloud.google.com/sdk/gcloud/reference/secrets/create"><code>gcloud secrets create $secretId"</code></a>,
(see also the <a href="https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets">Docu on "Creating and accessing secrets"</a>).</p>

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

<pre><code class="language-bash">gcloud secrets create my_secret_key

echo -n "my_secret_value" | gcloud secrets versions add my_secret_key --data-file=-
</code></pre>

<pre><code class="language-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].
</code></pre>

<p>We also need to 
<a href="#add-the-secret-gpg-key-and-password">do the same for the <code>gpg</code> secret key and its password</a>:</p>

<pre><code class="language-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=-
</code></pre>

<pre><code class="language-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].
</code></pre>

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

<h3>Create firewall rule for HTTP traffic</h3>

<p>Firewall rules can be created via
<a href="https://cloud.google.com/sdk/gcloud/reference/compute/firewall-rules/create"><code>gcloud compute firewall-rules create $ruleName</code></a>
(see also the <a href="https://cloud.google.com/vpc/docs/using-firewalls#creating_firewall_rules">Docu on "Using firewall rules"</a>).</p>

<p>We'll stick to 
<a href="#firewall-and-networks-tags">the same conventions that have been used when creating the VM via the UI</a>
by using <code>default-allow-http</code> as the <code>$ruleName</code> and <code>http-server</code> as the network tag (via the 
<code>--target-tags</code> option)</p>

<pre><code class="language-bash"> gcloud compute firewall-rules create default-allow-http --allow tcp:80 --target-tags=http-server
</code></pre>

<pre><code class="language-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

</code></pre>

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

<h3>Create a Compute Instance VM</h3>

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

<p>The <code>$vmName</code> defines the <strong>name of the VM</strong>, e.g.
<a href="#create-a-vm"><code>dofroscra-test</code> in the previous example</a>. In addition, there are a lot of 
options to customize the VM, e.g.</p>

<ul>
<li><code>--image-family</code> and <code>--image-project</code>

<ul>
<li>define the <strong>operating system</strong>, e.g. <code>--image-family="debian-11"</code> and 
<code>--image-project=debian-cloud</code></li>
<li>See <a href="https://cloud.google.com/compute/docs/images/os-details">Docu on "Operating system details"</a>
for a list of available values</li>
</ul></li>
<li><code>--machine-type</code>

<ul>
<li>defines <strong>the machine type</strong> (i.e. the "specs" like CPUs and memory), e.g.
<code>--machine-type=e2-micro</code></li>
<li>See <a href="https://cloud.google.com/compute/docs/machine-types">Docu on "About machine families"</a> 
that contains links to the machine type categories with the concrete values, e.g. the
<a href="https://cloud.google.com/compute/docs/general-purpose-machines">General-purpose machine family</a></li>
</ul></li>
<li>etc.</li>
</ul>

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

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

<p>Note that we are using <code>dofroscra-test</code> as the <strong>instance name</strong> and  <code>us-central1-a</code> as the
<strong>zone</strong>.</p>

<pre><code class="language-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
</code></pre>

<pre><code class="language-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
</code></pre>

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

<h3>Provisioning</h3>

<p>For provisioning, we need to <a href="#installing-docker-and-docker-compose">install <code>docker</code></a> and 
<a href="#authenticate-docker-via-gcloud">authenticate the <code>root</code> user to pull images from our registry</a>.
We've already created an installation script at <code>.infrastructure/scripts/provision.sh</code> and the 
easiest way to run it on the VM is to <a href="#gcloud-scp">transmit it via <code>scp</code></a></p>

<pre><code class="language-bash">gcloud compute scp --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p ./.infrastructure/scripts/provision.sh dofroscra-test:provision.sh
</code></pre>

<pre><code class="language-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%
</code></pre>

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

<pre><code class="language-bash">gcloud compute ssh dofroscra-test --zone us-central1-a --tunnel-through-iap --project=pl-dofroscra-p --command="bash provision.sh"
</code></pre>

<pre><code class="language-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) ...
</code></pre>

<p>Once <code>docker</code> is installed, we can run the authentication of the <code>root</code> user using 
<a href="https://unix.stackexchange.com/a/87861"><code>sudo su root -c</code></a> via</p>

<pre><code class="language-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'"
</code></pre>

<pre><code class="language-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.
</code></pre>

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

<h3>Deployment</h3>

<p>We're almost done - the last step in the process consists of</p>

<ul>
<li><p>building the docker image locally using the correct tag <code>gcr.io/pl-dofroscra-p/my-nginx</code></p>

<pre><code class="language-bash">docker build -t "gcr.io/pl-dofroscra-p/my-nginx" -f - . &lt;&lt;EOF
FROM nginx:1.21.5-alpine

RUN echo "Hello world" &gt;&gt; /usr/share/nginx/html/hello.html

EOF
</code></pre>

<pre><code class="language-text">$ docker build -t "gcr.io/pl-dofroscra-p/my-nginx" --no-cache -f - . &lt;&lt;EOF
FROM nginx:1.21.5-alpine
RUN echo "Hello world" &gt;&gt; /usr/share/nginx/html/hello.html
EOF

#1 [internal] load build definition from Dockerfile
#1 sha256:21ee68236cc00e4d1638480de32fd6304d796e6a72306082d3164e9164073843

#...

#5 [2/2] RUN echo "Hello world" &gt;&gt; /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
</code></pre></li>
<li><p>local authentication</p>

<pre><code class="language-bash">cat ./gcp-service-account-key.json | docker login -u _json_key --password-stdin https://gcr.io
</code></pre>

<pre><code class="language-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/
</code></pre></li>
<li><p><a href="#pushing-images-to-the-registry">pushing it to the registry</a></p>

<pre><code class="language-bash">docker push gcr.io/pl-dofroscra-p/my-nginx
</code></pre>

<pre><code class="language-text"><br />$ 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
</code></pre></li>
<li>and "deploying it on the VM"</li>
</ul>

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

<pre><code class="language-bash">#!/usr/bin/env bash

usage="Usage: deploy.sh image_name"
[ -z "$1" ] &amp;&amp;  echo "No image_name given! $usage" &amp;&amp; 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 &gt;&gt; ./secret.gpg
head ./secret.gpg

echo "Getting secret GPG_PASSWORD"
GPG_PASSWORD=$(gcloud secrets versions access latest --secret=GPG_PASSWORD)
echo $GPG_PASSWORD
</code></pre>

<p>The script will <a href="#pulling-the-nginx-image">pull the image <em>on the VM</em></a> from the registry</p>

<pre><code class="language-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
</code></pre>

<p><a href="#start-the-nginx-container">start a container</a> with the image</p>

<pre><code class="language-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
</code></pre>

<p>and finally <a href="#get-the-secret-gpg-key-and-password-from-the-secret-manager">retrieve the secrets</a> 
(just to demonstrate that it works)</p>

<pre><code class="language-text">$ gcloud secrets versions access latest --secret=GPG_KEY &gt;&gt; ./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
</code></pre>

<p>Bonus: For 
<a href="https://cloud.google.com/compute/docs/instances/view-ip-address">retrieving the puplic IP address of the Compute Instance VM</a> 
we can use the <a href="https://cloud.google.com/sdk/gcloud/reference/compute/instances/describe"><code>gcloud compute instances describe $instanceName</code></a> command:</p>

<pre><code class="language-bash">gcloud compute instances describe dofroscra-test --zone us-central1-a --project=pl-dofroscra-p --format='get(networkInterfaces[0].accessConfigs[0].natIP)'
</code></pre>

<pre><code class="language-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
</code></pre>

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

<h2>Putting it all together</h2>

<p>Since none of the previous steps requires manual intervention any longer, I have created a 
script at <code>.infrastructure/setup-gcp.sh</code> to run everything from
<a href="#configure-gcloud-to-use-the-master-service-account">Configure <code>gcloud</code> to use the master service account</a>
to <a href="#provisioning">Provisioning</a>:</p>

<pre><code class="language-bash">#!/usr/bin/env bash

usage="Usage: deploy.sh project_id vm_name"
[ -z "$1" ] &amp;&amp;  echo "No project_id given! $usage" &amp;&amp; exit 1
[ -z "$2" ] &amp;&amp;  echo "No vm_name given! $usage" &amp;&amp; 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"
</code></pre>

<pre><code class="language-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!
</code></pre>

<p>In addition, I also created a script for the <a href="#deployment">Deployment</a> at 
<code>.infrastructure/deploy.sh</code>:</p>

<pre><code class="language-bash">#!/usr/bin/env bash

usage="Usage: deploy.sh project_id vm_name"
[ -z "$1" ] &amp;&amp;  echo "No project_id given! $usage" &amp;&amp; exit 1
[ -z "$2" ] &amp;&amp;  echo "No vm_name given! $usage" &amp;&amp; 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 - . &lt;&lt;EOF
FROM nginx:1.21.5-alpine

RUN echo "Hello world" &gt;&gt; /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"
</code></pre>

<pre><code class="language-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!
</code></pre>

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

<h2>Cleanup</h2>

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

<ul>
<li>the docker images stored on Cloud Storage</li>
<li>the secrets in the Secret Manager</li>
<li>the VM itself</li>
</ul>

<p>is to 
<a href="https://cloud.google.com/resource-manager/docs/creating-managing-projects#shutting_down_projects">delete the whole project</a>,
e.g. via the <a href="https://console.cloud.google.com/iam-admin/settings">Settings UI</a></p>

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

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

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

<h2>Wrapping up</h2>

<p>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.</p>

<p>In the next part of this tutorial, we will 
<a href="/blog/deploy-docker-compose-php-gcp-poc/">deploy our dockerized PHP application "to production" on GCP via <code>docker compose</code></a>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Mon, 20 Jun 2022 06:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/gcp-compute-instance-vm-docker/</guid>
            </item>
                    <item>
                <title>CI Pipelines for dockerized PHP Apps with Github &amp; Gitlab [Tutorial Part 7]</title>
                <description><![CDATA[<p>In the seventh part of this tutorial series on developing PHP on Docker we will <strong>setup a CI
(Continuous Integration) pipeline to run code quality tools and tests on Github Actions and Gitlab
Pipelines</strong>.</p>

<div class="youtube center-div">
<iframe width="560" height="315" src="https://www.youtube.com/embed/VsNvvt0CMm8" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch for this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github">part-7-ci-pipeline-docker-php-gitlab-github</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/git-secret-encrypt-repository-docker/">Use git-secret to encrypt secrets in the repository</a>
and the following one is
<a href="/blog/gcp-compute-instance-vm-docker/">A primer on GCP Compute Instance VMs for dockerized Apps</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a>
to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a>

<ul>
<li><a href="#recommended-reading">Recommended reading</a></li>
<li><a href="#approach">Approach</a></li>
<li><a href="#try-it-yourself">Try it yourself</a></li>
</ul></li>
<li><a href="#ci-setup">CI setup</a>

<ul>
<li><a href="#general-ci-notes">General CI notes</a>

<ul>
<li><a href="#initialize-make-for-ci">Initialize <code>make</code> for CI</a></li>
<li><a href="#wait-for-service-sh">wait-for-service.sh</a></li>
</ul></li>
<li><a href="#setup-for-a-local-ci-run">Setup for a "local" CI run</a>

<ul>
<li><a href="#run-details">Run details</a></li>
<li><a href="#execution-example">Execution example</a></li>
</ul></li>
<li><a href="#setup-for-github-actions">Setup for Github Actions</a>

<ul>
<li><a href="#the-workflow-file">The Workflow file</a></li>
</ul></li>
<li><a href="#setup-for-gitlab-pipelines">Setup for Gitlab Pipelines</a>

<ul>
<li><a href="#the-gitlab-ci-yml-pipeline-file">The <code>.gitlab-ci.yml</code> pipeline file</a></li>
</ul></li>
<li><a href="#performance">Performance</a>

<ul>
<li><a href="#the-caching-problem-on-ci">The caching problem on CI</a></li>
</ul></li>
</ul></li>
<li><a href="#docker-changes">Docker changes</a>

<ul>
<li><a href="#compose-file-updates">Compose file updates</a>

<ul>
<li><a href="#docker-compose-local-yml">docker-compose.local.yml</a></li>
<li><a href="#docker-compose-ci-yml">docker-compose.ci.yml</a></li>
<li><a href="#adding-a-health-check-for-mysql">Adding a health check for <code>mysql</code></a></li>
</ul></li>
<li><a href="#build-target-ci">Build target: <code>ci</code></a>

<ul>
<li><a href="#build-stage-ci-in-the-php-base-image">Build stage <code>ci</code> in the <code>php-base</code> image</a>

<ul>
<li><a href="#use-the-whole-codebase-as-build-context">Use the whole codebase as build context</a></li>
<li><a href="#build-the-dependencies">Build the dependencies</a></li>
<li><a href="#create-the-final-image">Create the final image</a></li>
</ul></li>
<li><a href="#build-stage-ci-in-the-application-image">Build stage <code>ci</code> in the <code>application</code> image</a></li>
</ul></li>
<li><a href="#dockerignore">.dockerignore</a></li>
</ul></li>
<li><a href="#makefile-changes">Makefile changes</a>

<ul>
<li><a href="#initialize-the-shared-variables">Initialize the shared variables</a></li>
<li><a href="#env-based-docker-compose-config">ENV based docker compose config</a></li>
</ul></li>
<li><a href="#codebase-changes">Codebase changes</a>

<ul>
<li><a href="#add-a-test-for-encrypted-files">Add a test for encrypted files</a></li>
<li><a href="#add-a-password-protected-secret-gpg-key">Add a password-protected secret <code>gpg</code> key</a></li>
<li><a href="#create-a-junit-report-from-phpunit">Create a JUnit report from PhpUnit</a></li>
</ul></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>CI is short for <strong>C</strong>ontinuous <strong>I</strong>ntegration and to me mostly means <strong>running the code quality
tools and tests of a codebase in an isolated environment</strong> (preferably automatically). This is<br />
particularly important when working in a team, because <strong>the CI system acts as the final
gatekeeper</strong> before features or bugfixes are merged into the main branch.</p>

<p>I initially learned about CI systems when I stubbed my toes into the open source water. Back in the
day I used <a href="https://travis-ci.org/">Travis CI</a> for my own projects and replaced it
with <a href="https://github.com/features/actions">Github Actions</a> at some point. At ABOUT YOU we started
out with a self-hosted <a href="https://www.jenkins.io/">Jenkins</a> server and then moved on to
<a href="https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/">Gitlab CI</a> as a fully
managed solution (though we use <a href="https://docs.gitlab.com/runner/">custom runners</a>).</p>

<p><!-- generated -->
<a id='recommended-reading'> </a>
<!-- /generated --></p>

<h3>Recommended reading</h3>

<p>This tutorial builds on top of the previous parts. I'll do my best to cross-reference the 
corresponding articles when necessary, but I would still recommend to do some upfront reading on:</p>

<ul>
<li>the <a href="/blog/structuring-the-docker-setup-for-php-projects/#structuring-the-repository">general folder structure</a>, the 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker">update of the <code>.docker/</code> directory</a> and the introduction of a 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#make-mk-includes"><code>.make/</code> directory</a></li>
<li>the <a href="/blog/structuring-the-docker-setup-for-php-projects/#makefile-and-bashrc">general usage of <code>make</code></a> 
and <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#makefile">it's evolution</a> as well as 
the <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#make-docker-3">connection to <code>docker compose</code> commands</a></li>
<li>the concepts of the <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#docker">docker containers and the <code>docker compose</code> setup</a></li>
</ul>

<p>And as a nice-to-know:
- the setup of <a href="/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#install-phpunit">PhpUnit for the <code>test</code> make target</a> as well as the 
  <a href="/blog/php-qa-tools-make-docker/#qa-make-targets"><code>qa</code> make target</a>
- the <a href="/blog/git-secret-encrypt-repository-docker/">usage of <code>git-secret</code> to handle secret values</a></p>

<p><!-- generated -->
<a id='approach'> </a>
<!-- /generated --></p>

<h3>Approach</h3>

<p>In this tutorial I'm going to explain <strong>how to make our existing docker setup work with Github Actions
and <a href="https://docs.gitlab.com/ee/ci/pipelines/">Gitlab CI/CD Pipelines</a></strong>. As I'm a big fan of a
"progressive enhancement" approach, we will ensure that <strong>all necessary steps can be performed 
locally through <code>make</code></strong>. This has the additional benefit of keeping a single source of truth (the
<code>Makefile</code>) which will come in handy when we set up the CI system on two different providers
(Github and Gitlab).</p>

<p>The general process will look very similar to the one for local development:</p>

<ul>
<li>build the docker setup</li>
<li>start the docker setup</li>
<li>run the qa tools</li>
<li>run the tests</li>
</ul>

<p>You can see the final results in the <a href="#ci-setup">CI setup</a> section, including the concrete <code>yml</code> 
files and links to the repositories, see</p>

<ul>
<li><a href="#setup-for-a-local-ci-run">Setup for a "local" CI run</a></li>
<li><a href="#setup-for-github-actions">Setup for Github Actions</a></li>
<li><a href="#setup-for-gitlab-pipelines">Setup for Gitlab Pipelines</a></li>
</ul>

<p>On a code level, we will <strong>treat CI as an environment</strong>, configured through the env variable <code>ENV</code>. So
far we only used <code>ENV=local</code> and we will extend that to also use <code>ENV=ci</code>. The necessary changes 
are explained after the concrete CI setup instructions in the sections</p>

<ul>
<li><a href="#docker-changes">Docker changes</a></li>
<li><a href="#makefile-changes">Makefile changes</a></li>
<li><a href="#codebase-changes">Codebase changes</a></li>
</ul>

<p><!-- generated -->
<a id='try-it-yourself'> </a>
<!-- /generated --></p>

<h3>Try it yourself</h3>

<p>To get a feeling for what's going on, you can start by 
<a href="#setup-for-a-local-ci-run">executing the local CI run</a>:</p>

<ul>
<li>checkout branch
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github">part-7-ci-pipeline-docker-php-gitlab-github</a></li>
<li>initialize <code>make</code></li>
<li>run the <code>.local-ci.sh</code> script</li>
</ul>

<p>This should give you a similar output as presented in the <a href="#execution-example">Execution example</a>.</p>

<pre><code class="language-bash">git checkout part-7-ci-pipeline-docker-php-gitlab-github

# Initialize make
make make-init

# Execute the local CI run
bash .local-ci.sh
</code></pre>

<p><!-- generated -->
<a id='ci-setup'> </a>
<!-- /generated --></p>

<h2>CI setup</h2>

<p><!-- generated -->
<a id='general-ci-notes'> </a>
<!-- /generated --></p>

<h3>General CI notes</h3>

<p><!-- generated -->
<a id='initialize-make-for-ci'> </a>
<!-- /generated --></p>

<h4>Initialize <code>make</code> for CI</h4>

<p>As a very first step we need to "configure" the codebase to operate for the <code>ci</code> environment.
This is done through the <code>make-init</code> target as explained later in more detail in the
<a href="#makefile-changes">Makefile changes</a> section via</p>

<pre><code class="language-bash">make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
</code></pre>

<pre><code class="language-text">$ make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
Created a local .make/.env file
</code></pre>

<p><code>ENV=ci</code> ensures that we</p>

<ul>
<li>use <a href="#env-based-docker-compose-config">the correct <code>docker compose</code> config files</a></li>
<li>use <a href="#build-target-ci">the <code>ci</code> build target</a></li>
</ul>

<p><code>TAG=latest</code> is just a simplification for now because we don't do anything with the images yet.
In an upcoming tutorial we will push them to a container registry for later usage in production
deployments and then set the <code>TAG</code> to something more meaningful (like the build number).</p>

<p><code>EXECUTE_IN_CONTAINER=true</code> forces every <code>make</code> command that uses a
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#run-commands-in-the-docker-containers"><code>RUN_IN_*_CONTAINER</code> setup</a>
to run in a container. This is important, because <strong>the Gitlab runner will actually run in a
docker container itself</strong>. However, this would cause any affected target <strong>to omit the 
<code>$(DOCKER_COMPOSER) exec</code> prefix</strong>.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/execute-always-in-docker.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/execute-always-in-docker.PNG" alt="Execute all targets in the application docker container" /></a></p>

<p><code>GPG_PASSWORD=12345678</code> is the password for the secret <code>gpg</code> key as mentioned in
<a href="#add-a-password-protected-secret-gpg-key">Add a password-protected secret <code>gpg</code> key</a>.</p>

<p><!-- generated -->
<a id='wait-for-service-sh'> </a>
<!-- /generated --></p>

<h4>wait-for-service.sh</h4>

<p>I'll explain the "container is up and running but the underlying service is not" problem
for the <code>mysql</code> service and how we can solve it with a health check later in this article at
<a href="#adding-a-health-check-for-mysql">Adding a health check for <code>mysql</code></a>.
On purpose, we don't want <code>docker compose</code> to take care of the waiting because we can make 
"better use of the waiting time" and will instead implement it ourselves with a simple bash 
script located at <code>.docker/scripts/wait-for-service.sh</code>:</p>

<pre><code class="language-bash">#!/bin/bash

name=$1
max=$2
interval=$3

[ -z "$1" ] &amp;&amp; echo "Usage example: bash wait-for-service.sh mysql 5 1"
[ -z "$2" ] &amp;&amp; max=30
[ -z "$3" ] &amp;&amp; interval=1

echo "Waiting for service '$name' to become healthy, checking every $interval second(s) for max. $max times"

while true; do 
  ((i++))
  echo "[$i/$max] ..."; 
  status=$(docker inspect --format "{{json .State.Health.Status }}" "$(docker ps --filter name="$name" -q)")
  if echo "$status" | grep -q '"healthy"'; then 
   echo "SUCCESS";
   break
  fi
  if [ $i == $max ]; then 
    echo "FAIL"; 
    exit 1
  fi 
  sleep $interval; 
done
</code></pre>

<p>This script waits for a docker <code>$service</code> to become "healthy" by
<a href="https://stackoverflow.com/a/42738182/413531">checking the <code>.State.Health.Status</code> info</a>
of the <code>docker inspect</code> command.</p>

<p><strong>CAUTION:</strong> The script uses <code>$(docker ps --filter name="$name" -q)</code> to determine the id of the
container, i.e. it will "match" all running containers against the <code>$name</code> - this would fail if
there is more than one matching container! I.e. you must ensure that <code>$name</code> is specific
enough to identify one single container uniquely.</p>

<p>The script will check up to <code>$max</code> times
in a interval of <code>$interval</code> seconds. See <a href="https://unix.stackexchange.com/a/82610">these</a>
<a href="https://unix.stackexchange.com/a/137639">answers</a> on the
"How do I write a retry logic in script to keep retrying to run it up to 5 times?" question for
the implementation of the retry logic. To check the health of the <code>mysql</code> service for 5
times with 1 seconds between each try, it can be called via</p>

<pre><code class="language-bash">bash wait-for-service.sh mysql 5 1
</code></pre>

<p>Output</p>

<pre><code class="language-text">$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
[3/5] ...
[4/5] ...
[5/5] ...
FAIL

# OR

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 5 times
[1/5] ...
[2/5] ...
SUCCESS
</code></pre>

<p>The problem of "container dependencies" isn't new and there are already some existing solutions
out there, e.g.</p>

<ul>
<li><a href="https://github.com/eficode/wait-for">wait-for</a></li>
<li><a href="https://github.com/vishnubob/wait-for-it">wait-for-it</a></li>
<li><a href="https://github.com/jwilder/dockerize#waiting-for-other-dependencies">dockerize</a></li>
<li><a href="https://github.com/ufoscout/docker-compose-wait">docker-compose-wait</a></li>
</ul>

<p>But unfortunately all of them operate by checking the availability of a <code>host:port</code> combination
and in the case of <code>mysql</code> that didn't help, because the container was up, the port was reachable
but the <code>mysql</code> service in the container was not.</p>

<p><!-- generated -->
<a id='setup-for-a-local-ci-run'> </a>
<!-- /generated --></p>

<h3>Setup for a "local" CI run</h3>

<p>As mentioned under <a href="#approach">Approach</a>, we want to be able to perform all necessary steps
locally and I created a corresponding script at <code>.local-ci.sh</code>:</p>

<pre><code class="language-bash">#!/bin/bash
# fail on any error 
# @see https://stackoverflow.com/a/3474556/413531
set -e

make docker-down ENV=ci || true

start_total=$(date +%s)

# STORE GPG KEY
cp secret-protected.gpg.example secret.gpg

# DEBUG
docker version
docker compose version
cat /etc/*-release || true

# SETUP DOCKER
make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
start_docker_build=$(date +%s)
make docker-build
end_docker_build=$(date +%s)
mkdir -p .build &amp;&amp; chmod 777 .build

# START DOCKER
start_docker_up=$(date +%s)
make docker-up
end_docker_up=$(date +%s)
make gpg-init
make secret-decrypt-with-password

# QA
start_qa=$(date +%s)
make qa || FAILED=true
end_qa=$(date +%s)

# WAIT FOR CONTAINERS
start_wait_for_containers=$(date +%s)
bash .docker/scripts/wait-for-service.sh mysql 30 1
end_wait_for_containers=$(date +%s)

# TEST
start_test=$(date +%s)
make test || FAILED=true
end_test=$(date +%s)

end_total=$(date +%s)

# RUNTIMES
echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
echo "QA:                  " `expr $end_qa - $start_qa`
echo "Wait for containers: " `expr $end_wait_for_containers - $start_wait_for_containers`
echo "Tests:               " `expr $end_test - $start_test`
echo "---------------------"
echo "Total:               " `expr $end_total - $start_total`

# CLEANUP
# reset the default make variables
make make-init
make docker-down ENV=ci || true

# EVALUATE RESULTS
if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi

echo "SUCCESS"
</code></pre>

<p><!-- generated -->
<a id='run-details'> </a>
<!-- /generated --></p>

<h4>Run details</h4>

<ul>
<li><p>as a preparation step, we first ensure that no outdated <code>ci</code> containers are running (this is
only necessary locally, because runners on a remote CI system will start "from scratch")</p>

<pre><code class="language-bash">make docker-down ENV=ci || true
</code></pre></li>
<li><p>we take some time measurements to understand how long certain parts take via</p>

<pre><code class="language-bash">start_total=$(date +%s)
</code></pre>

<p>to store the current timestamp</p></li>
<li><p>we need the secret <code>gpg</code> key in order to decrypt the secrets and simply copy the
<a href="#add-a-password-protected-secret-gpg-key">password-protected example key</a> 
(in the actual CI systems the key will be configured as a secret value that is injected in 
the run)</p>

<pre><code class="language-bash"># STORE GPG KEY
cp secret-protected.gpg.example secret.gpg
</code></pre></li>
<li><p>I like printing some debugging info in order to understand which exact circumstances
we're dealing with (tbh, this is mostly relevant when setting the CI system up or making
modifications to it)</p>

<pre><code class="language-bash"># DEBUG
docker version
docker compose version
cat /etc/*-release || true
</code></pre></li>
<li><p>for the docker setup, we start with
<a href="#initialize-make-for-ci">initializing the environment for <code>ci</code></a></p>

<pre><code class="language-bash"># SETUP DOCKER
make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
</code></pre>

<p>then build the docker setup</p>

<pre><code class="language-bash">make docker-build
</code></pre>

<p>and finally add a <code>.build/</code> directory to
<a href="#create-a-junit-report-from-phpunit">collect the build artifacts</a></p>

<pre><code class="language-bash">mkdir -p .build &amp;&amp; chmod 777 .build
</code></pre></li>
<li><p>then, the docker setup is started</p>

<pre><code class="language-bash"># START DOCKER
make docker-up
</code></pre>

<p>and <code>gpg</code> is initialized so that
<a href="#add-a-password-protected-secret-gpg-key">the secrets can be decrypted</a></p>

<pre><code class="language-bash">make gpg-init
make secret-decrypt-with-password
</code></pre>

<p>We don't need to pass a <code>GPG_PASSWORD</code> to <code>secret-decrypt-with-password</code> because we have set
it up in the previous step as a default value via <code>make-init</code></p></li>
<li><p>once the <code>application</code> container is running, the qa tools are run by invoking the
<a href="/blog/php-qa-tools-make-docker/#the-qa-target"><code>qa</code> make target</a></p>

<pre><code class="language-bash"># QA
make qa || FAILED=true
</code></pre>

<p>The <code>|| FAILED=true</code> part makes sure that the script will not be terminated if the checks fail.
Instead, the fact that a failure happened is "recorded" in the <code>FAILED</code> variable so that we
can evaluate it at the end. We don't want the script to stop here because we want the
following steps to be executed as well (e.g. the tests).</p></li>
<li><p>to mitigate the
<a href="#adding-a-health-check-for-mysql">"<code>mysql</code> is not ready" problem</a>, we will now apply the
<a href="#wait-for-service-sh">wait-for-service.sh script</a></p>

<pre><code class="language-bash"># WAIT FOR CONTAINERS
bash .docker/scripts/wait-for-service.sh mysql 30 1
</code></pre></li>
<li><p>once <code>mysql</code> is ready, we can execute the tests via the 
<a href="/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#install-phpunit"><code>test</code> make target</a> and 
apply the same <code>|| FAILED=true</code> workaround as for the qa tools</p>

<pre><code class="language-bash"># TEST
make test || FAILED=true
</code></pre></li>
<li><p>finally, all the timers are printed</p>

<pre><code class="language-bash"># RUNTIMES
echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
echo "QA:                  " `expr $end_qa - $start_qa`
echo "Wait for containers: " `expr $end_wait_for_containers - $start_wait_for_containers`
echo "Tests:               " `expr $end_test - $start_test`
echo "---------------------"
echo "Total:               " `expr $end_total - $start_total`
</code></pre></li>
<li><p>we clean up the resources (this is only necessary when running locally, because the runner of
a CI system would be shut down anyway)</p>

<pre><code class="language-bash"># CLEANUP
make make-init
make docker-down ENV=ci || true
</code></pre></li>
<li><p>and finally evaluate if any error occurred when running the qa tools or the tests</p>

<pre><code class="language-bash"># EVALUATE RESULTS
if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi

echo "SUCCESS"
</code></pre></li>
</ul>

<p><!-- generated -->
<a id='execution-example'> </a>
<!-- /generated --></p>

<h4>Execution example</h4>

<p>Executing the script via</p>

<pre><code class="language-bash">bash .local-ci.sh
</code></pre>

<p>yields the following (shortened) output:</p>

<pre><code class="language-text">$ bash .local-ci.sh
Container dofroscra_ci-redis-1  Stopping
# Stopping all other `ci` containers ...
# ...

Client:
 Cloud integration: v1.0.22
 Version:           20.10.13
# Print more debugging info ...
# ...

Created a local .make/.env file
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
#1 [internal] load build definition from Dockerfile
# Output from building the docker containers 
# ...

ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d
Network dofroscra_ci_network  Creating
# Starting all `ci` containers ...
# ...

"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key D7A860BBB91B60C7: public key "Alice Doe protected &lt;alice.protected@example.com&gt;" imported
# Output of importing the secret and public gpg keys
# ...

"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f -p 12345678"
git-secret: done. 1 of 1 files are revealed.
"C:/Program Files/Git/mingw64/bin/make" -j 8 -k --no-print-directory --output-sync=target qa-exec NO_PROGRESS=true
phplint                             done   took 4s
phpcs                               done   took 4s
phpstan                             done   took 8s
composer-require-checker            done   took 8s
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for max. 30 times
[1/30] ...
SUCCESS
PHPUnit 9.5.19 #StandWithUkraine

........                                                            8 / 8 (100%)

Time: 00:03.077, Memory: 28.00 MB

OK (8 tests, 15 assertions)
Build docker:         12
Start docker:         2
QA:                   9
Wait for containers:  3
Tests:                5
---------------------
Total:                46
Created a local .make/.env file

Container dofroscra_ci-application-1  Stopping
Container dofroscra_ci-mysql-1  Stopping
# Stopping all other `ci` containers ...
# ...

SUCCESS
</code></pre>

<p><!-- generated -->
<a id='setup-for-github-actions'> </a>
<!-- /generated --></p>

<h3>Setup for Github Actions</h3>

<ul>
<li><a href="https://github.com/paslandau/docker-php-tutorial/tree/part-7-ci-pipeline-docker-php-gitlab-github">Repository (branch <code>part-7-ci-pipeline-docker-php-gitlab-github</code>)</a></li>
<li><a href="https://github.com/paslandau/docker-php-tutorial/actions">CI/CD overview (Actions)</a></li>
<li><a href="https://github.com/paslandau/docker-php-tutorial/runs/5866235820?check_suite_focus=true">Example of a successful job</a></li>
<li><a href="https://github.com/paslandau/docker-php-tutorial/runs/5867485802?check_suite_focus=true">Example of a failed job</a></li>
</ul>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/github-action-example.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/github-action-example.PNG" alt="Github Action example" /></a></p>

<p>If you are completely new to Github Actions, I recommend to start with the
<a href="https://docs.github.com/en/actions/quickstart">official Quickstart Guide for GitHub Actions</a>
and the
<a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions">Understanding GitHub Actions</a>
article. In short:</p>

<ul>
<li>Github Actions are based on so called <strong>Workflows</strong>

<ul>
<li>Workflows are <code>yaml</code> files that  live in the special <code>.github/workflows</code> directory in the
repository</li>
</ul></li>
<li>a Workflow can contain multiple <strong>Jobs</strong></li>
<li>each Job consists of a series of <strong>Steps</strong></li>
<li><p>each Step needs a <code>run:</code> element that represents a command that is executed by a new shell</p>

<ul>
<li>multi-line commands that should use the same shell are written as</li>
</ul>

<pre><code class="language-yaml">  - run : |
        echo "line 1"
        echo "line 2"
</code></pre>

<p>See also <a href="https://stackoverflow.com/a/59536836/413531">difference between "run |" and multiple runs in github actions</a></p></li>
</ul>

<p><!-- generated -->
<a id='the-workflow-file'> </a>
<!-- /generated --></p>

<h4>The Workflow file</h4>

<p>Github Actions are triggered automatically based on the files in the <code>.github/workflows</code> directory.
I have added the file <code>.github/workflows/ci.yml</code> with the following content:</p>

<pre><code class="language-yaml">name: CI build and test

on:
  # automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"
  # @see https://stackoverflow.com/a/58142412/413531
  push:
    branches:
      - part-7-ci-pipeline-docker-php-gitlab-github
  pull_request: {}
  # enable to trigger the action manually
  # @see https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
  # CAUTION: there is a known bug that makes the "button to trigger the run" not show up
  # @see https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29
  workflow_dispatch: {}

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1

      - name: start timer
        run: |
          echo "START_TOTAL=$(date +%s)" &gt; $GITHUB_ENV

      - name: STORE GPG KEY
        run: |
          # Note: make sure to wrap the secret in double quotes (")
          echo "${{ secrets.GPG_KEY }}" &gt; ./secret.gpg

      - name: SETUP TOOLS
        run : |
          DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
          # install docker compose
          # @see https://docs.docker.com/compose/cli-command/#install-on-linux
          # @see https://github.com/docker/compose/issues/8630#issuecomment-1073166114
          mkdir -p $DOCKER_CONFIG/cli-plugins 
          curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
          chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

      - name: DEBUG
        run: |
          docker compose version
          docker --version
          cat /etc/*-release

      - name: SETUP DOCKER
        run: |
          make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
          make docker-build
          mkdir .build &amp;&amp; chmod 777 .build

      - name: START DOCKER
        run: |
          make docker-up
          make gpg-init
          make secret-decrypt-with-password

      - name: QA
        run: |
          # Run the tests and qa tools but only store the error instead of failing immediately
          # @see https://stackoverflow.com/a/59200738/413531
          make qa || echo "FAILED=qa" &gt;&gt; $GITHUB_ENV

      - name: WAIT FOR CONTAINERS
        run: |
          # We need to wait until mysql is available.
          bash .docker/scripts/wait-for-service.sh mysql 30 1 

      - name: TEST
        run: |
          make test || echo "FAILED=test $FAILED" &gt;&gt; $GITHUB_ENV

      - name: RUNTIMES
        run: |
          echo `expr $(date +%s) - $START_TOTAL`

      - name: EVALUATE
        run: |
          # Check if $FAILED is NOT empty
          if [ ! -z "$FAILED" ]; then echo "Failed at $FAILED" &amp;&amp; exit 1; fi

      - name: upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: ./.build
</code></pre>

<p>The steps are essentially the same as explained before at 
<a href="#run-details">Run details for the local run</a>. Some additional notes:</p>

<ul>
<li><p>I want the Action to be triggered automatically only when I
<a href="https://stackoverflow.com/a/58142412/413531">push to branch <code>part-7-ci-pipeline-docker-php-gitlab-github</code></a>
OR when a pull request is created (via <code>pull_request</code>). In addition, I want to be able to
<a href="https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/">trigger the Action manually on any branch</a>
(via <code>workflow_dispatch</code>).</p>

<pre><code class="language-yaml">on:
push:
  branches:
    - part-7-ci-pipeline-docker-php-gitlab-github
pull_request: {}
workflow_dispatch: {}
</code></pre>

<p>For a real project, I would let the action only run automatically on long-living branches like
<code>main</code> or <code>develop</code>. The manual trigger is helpful if you just want to test your current work
without putting it up for review. <strong>CAUTION:</strong> There is a
<a href="https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29">known issue that "hides" the "Trigger workflow" button to trigger the action manually</a>.</p></li>
<li><p>a new shell is started for each <code>run:</code> instruction, thus we must store our timer in the "global"
<a href="https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable">environment variable <code>$GITHUB_ENV</code></a></p>

<pre><code class="language-yaml">  - name: start timer
  run: |
    echo "START_TOTAL=$(date +%s)" &gt; $GITHUB_ENV 
</code></pre>

<p>This will be the only timer we use, because the job uses multiple steps that are timed
automatically - so we don't need to take timestamps manually:
<a href="/img/ci-pipeline-docker-php-gitlab-github/github-action-step-times.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/github-action-step-times.PNG" alt="Github Action step times" /></a></p></li>
<li><p>the <code>gpg</code> key is configured as an
<a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets">encrypted secret</a> named
<code>GPG_KEY</code> and is stored in <code>./secret.gpg</code>. The value is the content of the
<code>secret-protected.gpg.example</code> file</p>

<pre><code class="language-yaml">  - name: STORE GPG KEY
    run: |
      echo "${{ secrets.GPG_KEY }}" &gt; ./secret.gpg
</code></pre>

<p>Secrets are configured in the Github repository under <code>Settings &gt; Secrets &gt; Actions</code> at</p>

<pre><code class="language-text">https://github.com/$user/$repository/settings/secrets/actions

e.g.

https://github.com/paslandau/docker-php-tutorial/settings/secrets/actions
</code></pre>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/github-secrets-ui.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/github-secrets-ui.PNG" alt="Github Action Secrets UI" /></a></p></li>
<li><p>the <code>ubuntu-latest</code> image doesn't contain the <code>docker compose</code> plugin, thus we need to
<a href="https://docs.docker.com/compose/cli-command/#install-on-linux">install it manually</a></p>

<pre><code class="language-yaml">  - name: SETUP TOOLS
  run : |
    DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
    mkdir -p $DOCKER_CONFIG/cli-plugins 
    curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
    chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
</code></pre></li>
<li><p>for the <code>make</code> initialization we need the second secret named <code>GPG_PASSWORD</code> - which is
configured as <code>12345678</code> in our case, see
<a href="#add-a-password-protected-secret-gpg-key">Add a password-protected secret gpg key</a></p>

<pre><code class="language-yaml">  - name: SETUP DOCKER
    run: |
      make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
</code></pre></li>
<li><p>because the runner will be shutdown after the run, we need to move the build artifacts to a
permanent location, using the
<a href="https://github.com/actions/upload-artifact#upload-an-entire-directory">actions/upload-artifact@v3 action</a></p>

<pre><code class="language-yaml">  - name: upload build artifacts
    uses: actions/upload-artifact@v3
    with:
      name: build-artifacts
      path: ./.build
</code></pre>

<p>You can
<a href="https://github.com/actions/upload-artifact#where-does-the-upload-go">download the artifacts in the Run overview UI</a>
<a href="/img/ci-pipeline-docker-php-gitlab-github/github-action-run-overview-build-artifacts.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/github-action-run-overview-build-artifacts.PNG" alt="Github Actions: Run overview UI shows build-artifacts" /></a></p></li>
</ul>

<p><!-- generated -->
<a id='setup-for-gitlab-pipelines'> </a>
<!-- /generated --></p>

<h3>Setup for Gitlab Pipelines</h3>

<ul>
<li><a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/tree/part-7-ci-pipeline-docker-php-gitlab-github">Repository (branch <code>part-7-ci-pipeline-docker-php-gitlab-github</code>)</a></li>
<li><a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines">CI/CD overview (Pipelines)</a></li>
<li><a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511339886">Example of a successful job</a></li>
<li><a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511341545">Example of a failed job</a></li>
</ul>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-example.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-example.PNG" alt="Gitlab Pipeline example" /></a></p>

<p>If you are completely new to Gitlab Pipelines, I recommend to start with the
<a href="https://docs.gitlab.com/ee/ci/quick_start/">official Get started with GitLab CI/CD guide</a>. In
short:</p>

<ul>
<li>the core concept of Gitlab Pipelines is the <strong>Pipeline</strong>

<ul>
<li>it is defined in the <code>yaml</code> file <code>.gitlab-ci.yml</code> that lives in the root of the repository</li>
</ul></li>
<li>a Pipeline can contain multiple <strong>Stages</strong></li>
<li>each Stage consists of a series of <strong>Jobs</strong></li>
<li>each Job contains a <a href="https://docs.gitlab.com/ee/ci/yaml/index.html#script"><strong><code>script</code></strong> section</a></li>
<li>the <code>script</code> section consists of a series of shell commands</li>
</ul>

<p><!-- generated -->
<a id='the-gitlab-ci-yml-pipeline-file'> </a>
<!-- /generated --></p>

<h4>The <code>.gitlab-ci.yml</code> pipeline file</h4>

<p>Gitlab Pipelines are triggered automatically based on a <code>.gitlab-ci.yml</code> file located at the
root of the repository. It has the following content:</p>

<pre><code class="language-yaml">stages:
  - build_test

QA and Tests:
  stage: build_test

  rules:
    # automatically run for pull request and for pushes to branch "part-7-ci-pipeline-docker-php-gitlab-github"
    - if: '($CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "part-7-ci-pipeline-docker-php-gitlab-github")'

  # see https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker
  image: docker:20.10.12

  services:
    - name: docker:dind

  script:
    - start_total=$(date +%s)

    ## STORE GPG KEY
    - cp $GPG_KEY_FILE ./secret.gpg

    ## SETUP TOOLS
    - start_install_tools=$(date +%s)
    # "curl" is required to download docker compose
    - apk add --no-cache make bash curl
    # install docker compose
    # @see https://docs.docker.com/compose/cli-command/#install-on-linux
    - mkdir -p ~/.docker/cli-plugins/
    - curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
    - chmod +x ~/.docker/cli-plugins/docker-compose
    - end_install_tools=$(date +%s)

    ## DEBUG
    - docker version
    - docker compose version
    # show linux distro info
    - cat /etc/*-release

    ## SETUP DOCKER
    # Pass default values to the make-init command - otherwise we would have to pass those as arguments to every make call
    - make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"
    - start_docker_build=$(date +%s)
    - make docker-build
    - end_docker_build=$(date +%s)
    - mkdir .build &amp;&amp; chmod 777 .build

    ## START DOCKER
    - start_docker_up=$(date +%s)
    - make docker-up
    - end_docker_up=$(date +%s)
    - make gpg-init
    - make secret-decrypt-with-password

    ## QA
    # Run the tests and qa tools but only store the error instead of failing immediately
    # @see https://stackoverflow.com/a/59200738/413531
    - start_qa=$(date +%s)
    - make qa ENV=ci || FAILED=true
    - end_qa=$(date +%s)

    ## WAIT FOR CONTAINERS
    # We need to wait until mysql is available.
    - start_wait_for_containers=$(date +%s)
    - bash .docker/scripts/wait-for-service.sh mysql 30 1
    - end_wait_for_containers=$(date +%s)

    ## TEST
    - start_test=$(date +%s)
    - make test ENV=ci || FAILED=true
    - end_test=$(date +%s)

    - end_total=$(date +%s)

    # RUNTIMES
    - echo "Tools:" `expr $end_install_tools - $start_install_tools`
    - echo "Build docker:" `expr $end_docker_build - $start_docker_build`
    - echo "Start docker:" `expr $end_docker_up - $start_docker_up  `
    - echo "QA:" `expr $end_qa - $start_qa`
    - echo "Wait for containers:" `expr $end_wait_for_containers - $start_wait_for_containers`
    - echo "Tests:" `expr $end_test - $start_test`
    - echo "Total:" `expr $end_total - $start_total`

    # EVALUATE RESULTS
    # Use if-else constructs in Gitlab pipelines
    # @see https://stackoverflow.com/a/55464100/413531
    - if [ "$FAILED" == "true" ]; then exit 1; fi

  # Save the build artifact, e.g. the JUNIT report.xml file, so we can download it later
  # @see https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html
  artifacts:
    when: always
    paths:
      # the quotes are required
      # @see https://stackoverflow.com/questions/38009869/how-to-specify-wildcard-artifacts-subdirectories-in-gitlab-ci-yml#comment101411265_38055730
      - ".build/*"
    expire_in: 1 week
</code></pre>

<p>The steps are essentially the same as explained before under 
<a href="#run-details">Run details for the local run</a>. Some additional notes:</p>

<ul>
<li><p>we start by defining the stages of the pipeline - though that's currently just one (<code>build_test</code>)</p>

<pre><code class="language-yaml">stages:
- build_test
</code></pre></li>
<li><p>then we define the job <code>QA and Tests</code> and assign it to the <code>build_test</code> stage</p>

<pre><code class="language-yaml">QA and Tests:
stage: build_test
</code></pre></li>
<li><p>I want the Pipeline to be triggered automatically only when I
<a href="https://stackoverflow.com/a/66812732/413531">push to branch <code>part-7-ci-pipeline-docker-php-gitlab-github</code></a>
OR <a href="https://docs.gitlab.com/ee/ci/pipelines/merge_request_pipelines.html#use-rules-to-add-jobs">when a pull request is created</a>
<a href="https://www.shellhacks.com/gitlab-ci-cd-trigger-pipeline-manually-api/">Triggering the Pipeline manually on any branch is possible by default</a>.</p>

<pre><code class="language-yaml">rules:
- if: '($CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_BRANCH == "part-7-ci-pipeline-docker-php-gitlab-github")'
</code></pre></li>
<li><p>since we want to build and run docker images, we need to use a docker base image and activate the
<code>docker:dind</code> service. See <a href="https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker">Use Docker to build Docker images: Use Docker-in-Docker</a></p>

<pre><code class="language-yaml">image: docker:20.10.12

services:
- name: docker:dind
</code></pre></li>
<li><p>we store the secret <code>gpg</code> key as a secret file (using the
<a href="https://docs.gitlab.com/ee/ci/variables/#cicd-variable-types">"file" type</a>) in the
<a href="https://docs.gitlab.com/ee/ci/variables/#custom-cicd-variables">CI/CD variables configuration of the Gitlab repository</a>
and move it to <code>./secret.gpg</code> in order to decrypt the secrets later</p>

<pre><code class="language-yaml">## STORE GPG KEY
- cp $GPG_KEY_FILE ./secret.gpg
</code></pre>

<p>Secrets can be configured under <code>Settings &gt; CI/CD &gt; Variables</code> at</p>

<pre><code class="language-text">https://gitlab.com/$project/$repository/-/settings/ci_cd

e.g.

https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/settings/ci_cd
</code></pre>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/gitlab-ci-cd-variables-ui.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/gitlab-ci-cd-variables-ui.PNG" alt="Gitlab CI/CD Variables UI" /></a></p></li>
<li><p>the docker base image doesn't come with all required tools, thus we need to install the
missing ones (<code>make</code>, <code>bash</code>, <code>curl</code> and <code>docker compose</code>)</p>

<pre><code class="language-yaml">  ## SETUP TOOLS
  - apk add --no-cache make bash curl
  - mkdir -p ~/.docker/cli-plugins/
  - curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
  - chmod +x ~/.docker/cli-plugins/docker-compose
</code></pre></li>
<li><p>for the initialization of <code>make</code> we use the <code>$GPG_PASSWORD</code> variable that we defined in the
CI/CD settings</p>

<pre><code class="language-yaml">## SETUP DOCKER
- make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=$GPG_PASSWORD"
</code></pre>

<p>Note: I have <a href="https://docs.gitlab.com/ee/ci/variables/#mask-a-cicd-variable">marked the variable as "masked"</a>
so it won't show up in any logs</p></li>
<li><p>finally, we store <a href="https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html">the job artifacts</a></p>

<pre><code class="language-yaml">artifacts:
when: always
paths:
  - ".build/*"
expire_in: 1 week 
</code></pre>

<p>They can be accessed in the <a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines">Pipeline overview UI</a>
<a href="/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-build-artifacts.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/gitlab-pipeline-build-artifacts.PNG" alt="Gitlab Pipeline overview UI" /></a></p></li>
</ul>

<p><!-- generated -->
<a id='performance'> </a>
<!-- /generated --></p>

<h3>Performance</h3>

<div class="youtube center-div">
<iframe width="560" height="315" src="https://www.youtube.com/embed/aGWGJQWtH1I" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>Performance isn't an issue right now</strong>, because the CI runs take only about ~1 min (Github Actions)
and ~2 min (Gitlab Pipelines), but that's mostly because we only ship a super minimal
application and those times <em>will go up</em> when things get more complex. For the local setup I 
used all 8 cores of my laptop. The time breakdown is roughly as follows:</p>

<table>
<thead>
<tr>
  <th>Step</th>
  <th>Gitlab</th>
  <th>Github</th>
  <th>local <br /> without cache</th>
  <th>local <br /> with cached images</th>
  <th>local <br /> with cached images + layers</th>
</tr>
</thead>
<tbody>
<tr>
  <td>SETUP TOOLS</td>
  <td>1</td>
  <td>0</td>
  <td>0</td>
  <td>0</td>
  <td>0</td>
</tr>
<tr>
  <td>SETUP DOCKER</td>
  <td>33</td>
  <td>17</td>
  <td>39</td>
  <td>39</td>
  <td>5</td>
</tr>
<tr>
  <td>START DOCKER</td>
  <td>17</td>
  <td>11</td>
  <td>34</td>
  <td>2</td>
  <td>1</td>
</tr>
<tr>
  <td>QA</td>
  <td>17</td>
  <td>5</td>
  <td>10</td>
  <td>13</td>
  <td>1</td>
</tr>
<tr>
  <td>WAIT FOR CONTAINERS</td>
  <td>5</td>
  <td>5</td>
  <td>3</td>
  <td>2</td>
  <td>13</td>
</tr>
<tr>
  <td>TESTS</td>
  <td>3</td>
  <td>1</td>
  <td>3</td>
  <td>6</td>
  <td>3</td>
</tr>
<tr>
  <td><strong>total <br /> (excl. runner startup)</strong></td>
  <td>78</td>
  <td>43</td>
  <td>97</td>
  <td>70</td>
  <td>36</td>
</tr>
<tr>
  <td><strong>total <br /> (incl. runner startup)</strong></td>
  <td>139</td>
  <td>54</td>
  <td>97</td>
  <td>70</td>
  <td>36</td>
</tr>
</tbody>
</table>

<p>Times taken from
- <a href="https://github.com/paslandau/docker-php-tutorial/actions/runs/2108659089">"CI build and test #83" Github Action run</a>
- <a href="https://gitlab.com/docker-php-tutorial/docker-php-tutorial/-/pipelines/511355192">"Pipeline #511355192" Gitlab Pipeline run</a>
- "local without cache" via <code>bash .local-ci.sh</code> with no local images at all
- "local with cached images" via <code>bash .local-ci.sh</code> with cached images for <code>mysql</code> and <code>redis</code>
- "local with cached images + layers" via <code>bash .local-ci.sh</code> with cached images for <code>mysql</code> and
  <code>redis</code> and a <a href="#build-the-dependencies">"warm" layer cache for the <code>application</code> image</a></p>

<p><strong>Optimizing the performance is out of scope for this tutorial</strong>, but I'll at least document my
current findings.</p>

<p><!-- generated -->
<a id='the-caching-problem-on-ci'> </a>
<!-- /generated --></p>

<h4>The caching problem on CI</h4>

<p>A good chunk of time is <strong>usually spent on building the docker images</strong>. We did our best to optimize
the process by leveraging the layer cache and using cache mounts
(see section <a href="#build-stage-ci-in-the-php-base-image">Build stage <code>ci</code> in the <code>php-base</code> image</a>). 
But those steps are futile on CI systems, because the corresponding <strong>runners will start "from 
scratch" for every CI run</strong> - i.e. <strong>there is no local cache</strong> that they could use. In 
consequence, <strong>the full docker setup is also built "from scratch"</strong> on every run.</p>

<p>There are ways to mitigate that e.g.</p>

<ul>
<li>pushing images to a container registry and pulling them before building the images to leverage
the layer cache via the <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#cache_from"><code>cache_from</code> option</a>
of <code>docker compose</code></li>
<li>exporting and importing the images as <code>tar</code> archives via
<a href="https://docs.docker.com/engine/reference/commandline/save/"><code>docker save</code></a> and<br />
<a href="https://docs.docker.com/engine/reference/commandline/load/"><code>docker load</code></a>,
storing them either in the built-in cache of
<a href="https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows">Github</a>
or <a href="https://docs.gitlab.com/ee/ci/caching/">Gitlab</a>

<ul>
<li>see also the <a href="https://github.com/marketplace/actions/docker-layer-caching">satackey/action-docker-layer-caching@v0.0.11 Github Action</a>
and the official <a href="https://github.com/actions/cache">actions/cache@v3 Github Action</a></li>
</ul></li>
<li>using the <a href="https://docs.docker.com/engine/reference/commandline/buildx_build/#cache-from"><code>--cache-from</code></a>
and <a href="https://docs.docker.com/engine/reference/commandline/buildx_build/#cache-to"><code>--cache-to</code></a> options of
<a href="https://docs.docker.com/buildx/working-with-buildx/"><code>buildx</code></a>

<ul>
<li>see also the <a href="https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md">"cache" docu of the docker/build-push-action@v2 Github Action</a></li>
</ul></li>
</ul>

<p>But: None of that worked for me out-of-the-box :( We will take a closer look in an upcoming
tutorial. Some reading material that I found valuable so far:</p>

<ul>
<li><a href="https://dev.to/dtinth/caching-docker-builds-in-github-actions-which-approach-is-the-fastest-a-research-18ei">Caching Docker builds in GitHub Actions: Which approach is the fastest? 🤔 A research.</a></li>
<li><a href="https://seankhliao.com/blog/12021-01-23-docker-buildx-caching/">Caching strategies for CI systems</a></li>
<li><a href="https://evilmartians.com/chronicles/build-images-on-github-actions-with-docker-layer-caching">Build images on GitHub Actions with Docker layer caching</a></li>
<li><a href="https://testdriven.io/blog/faster-ci-builds-with-docker-cache/">Faster CI Builds with Docker Layer Caching and BuildKit</a></li>
<li><a href="https://www.docker.com/blog/image-rebase-and-improved-remote-cache-support-in-new-buildkit/">Image rebase and improved remote cache support in new BuildKit</a></li>
</ul>

<p><!-- generated -->
<a id='docker-changes'> </a>
<!-- /generated --></p>

<h2>Docker changes</h2>

<p>As a first step we need to decide <strong>which containers are required</strong> and <strong>how to provide the
codebase</strong>.</p>

<p>Since our goal is running the qa tools and tests, we only need the <code>application</code> php container. The
tests also need a database and a queue, i.e. the <code>mysql</code> and <code>redis</code> containers are required as 
well - whereas <code>nginx</code>, <code>php-fpm</code> and <code>php-worker</code> are not required. We'll handle that through 
dedicated <code>docker compose</code> configuration files that only contain the necessary services. This is 
explained in more detail in section <a href="#compose-file-updates">Compose file updates</a>.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG" alt="Build images for CI" /></a></p>

<p>In our local setup, we have <strong>sheen the host system and docker</strong> - mainly
because we wanted our changes to be reflected immediately in docker.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/share-codebase-bind-mount.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/share-codebase-bind-mount.PNG" alt="Share the codebase between host system and docker container" /></a></p>

<p><strong>This isn't necessary for the CI</strong> use case. In fact we want our <strong>CI images as close as 
possible to our production images</strong> - and those should "contain everything to run independently".
I.e. <strong>the codebase should live in the image</strong> - not on the host system. This will be explained 
in section <a href="#use-the-whole-codebase-as-build-context">Use the whole codebase as build context</a>.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG" alt="Add the codebase in the docker image" /></a></p>

<p><!-- generated -->
<a id='compose-file-updates'> </a>
<!-- /generated --></p>

<h3>Compose file updates</h3>

<p>We will not only have some differences between the CI docker setup and the local docker setup
(=different containers), but also in the configuration of the individual services. To accommodate
for that, we will use the following <code>docker compose</code> config files in the 
<code>.docker/docker-compose/</code> directory:</p>

<ul>
<li><code>docker-compose.local.ci.yml</code>:

<ul>
<li>holds configuration that is valid for <code>local</code> and <code>ci</code>, trying to keep the config files 
<a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a></li>
</ul></li>
<li><code>docker-compose.ci.yml</code>:

<ul>
<li>holds configuration that is only valid for <code>ci</code></li>
</ul></li>
<li><code>docker-compose.local.yml</code>:

<ul>
<li>holds configuration that is only valid for <code>local</code></li>
</ul></li>
</ul>

<p>When using <code>docker compose</code> we then need to make sure to include only the required files, e.g. for 
<code>ci</code>:</p>

<pre><code class="language-bash">docker compose -f docker-compose.local.ci.yml -f docker-compose.ci.yml
</code></pre>

<p>I'll explain  the logic for that later in
section <a href="#env-based-docker-compose-config">ENV based docker compose config</a>. In short:</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG" alt="Assemble docker-compose config files for CI" /></a></p>

<p><!-- generated -->
<a id='docker-compose-local-yml'> </a>
<!-- /generated --></p>

<h4>docker-compose.local.yml</h4>

<p>When comparing <code>ci</code> with <code>local</code>, for <code>ci</code></p>

<ul>
<li><p>we <strong>don't need to share the codebase</strong> with the host system</p>

<pre><code class="language-yaml">application:
  volumes:
  - ${APP_CODE_PATH_HOST?}:${APP_CODE_PATH_CONTAINER?}
</code></pre></li>
<li><p>we <strong>don't need persistent volumes</strong> for the redis and mysql data</p>

<pre><code class="language-yaml">mysql:
  volumes:
    - mysql:/var/lib/mysql

redis:
  volumes:
    - redis:/data
</code></pre></li>
<li><p>we <strong>don't need to share ports</strong> with the host system</p>

<pre><code class="language-yaml">application:
  ports:
    - "${APPLICATION_SSH_HOST_PORT:-2222}:22"

redis:
  ports:
    - "${REDIS_HOST_PORT:-6379}:6379"
</code></pre></li>
<li><p>we <strong>don't need any settings for local dev tools</strong> like <code>xdebug</code> or <code>strace</code></p>

<pre><code class="language-yaml">application:
  environment:
    - PHP_IDE_CONFIG=${PHP_IDE_CONFIG?}
  cap_add:
    - "SYS_PTRACE"
  security_opt:
    - "seccomp=unconfined"
  extra_hosts:
    - host.docker.internal:host-gateway  
</code></pre></li>
</ul>

<p>So all of those config values will only live in the <code>docker-compose.local.yml</code> file.</p>

<p><!-- generated -->
<a id='docker-compose-ci-yml'> </a>
<!-- /generated --></p>

<h4>docker-compose.ci.yml</h4>

<p>In fact, there are only two things that <code>ci</code> needs that <code>local</code> doesn't:</p>

<ul>
<li><p>a bind mount to <strong>share only the secret gpg key from the host with the <code>application</code> container</strong></p>

<pre><code class="language-yaml">application:
  volumes:
    - ${APP_CODE_PATH_HOST?}/secret.gpg:${APP_CODE_PATH_CONTAINER?}/secret.gpg:ro
</code></pre>

<p>This
is <a href="/blog/git-secret-encrypt-repository-docker/#local-git-secret-and-gpg-setup">required to decrypt the secrets</a>:</p>

<blockquote>
  <p>[...] the private key has to be named <code>secret.gpg</code> and put in the root of the codebase,
  so that the import can be simplified with <code>make</code> targets</p>
</blockquote>

<p>The secret files themselves are baked into the image, but the key to decrypt them will be 
provided only during runtime and 
<a href="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image-share-secret-key.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image-share-secret-key.PNG" alt="Add the codebase in the docker image and share a secret key file" /></a></p></li>
<li><p>a bind mount to <strong>share a <code>.build</code> folder for build artifacts with the <code>application</code> container</strong></p>

<pre><code class="language-yaml">application:
  volumes:
    - ${APP_CODE_PATH_HOST?}/.build:${APP_CODE_PATH_CONTAINER?}/.build
</code></pre>

<p>This will be used to collect any files we want to retain from a build (e.g. code coverage
information, log files, etc.)</p></li>
</ul>

<p><!-- generated -->
<a id='adding-a-health-check-for-mysql'> </a>
<!-- /generated --></p>

<h4>Adding a health check for <code>mysql</code></h4>

<p>When running the tests for the first time on a CI system, I noticed some weird errors related to the
database:</p>

<pre><code class="language-text">1) Tests\Feature\App\Http\Controllers\HomeControllerTest::test___invoke with data set "default" (array(), '    &lt;li&gt;&lt;a href="?dispatch=fo...&gt;&lt;/li&gt;')
PDOException: SQLSTATE[HY000] [2002] Connection refused
</code></pre>

<p>As it turned out, the <code>mysql</code> container itself was up and running - but the <code>mysql</code> process
<em>within</em> the container was not yet ready to accept connections. Locally, this hasn't been a problem,
because we usually would not run the tests "immediately" after starting the containers - but on CI 
this is the case.</p>

<p>Fortunately, <code>docker compose</code> has us covered here and provides a
<a href="https://docs.docker.com/compose/compose-file/#healthcheck"><code>healtcheck</code> configuration option</a>:</p>

<blockquote>
  <p><code>healthcheck</code> declares a check that’s run to determine whether or not containers for this service are "healthy".</p>
</blockquote>

<p>Since this <code>healthcheck</code> is also "valid" for <code>local</code>, I defined it in the combined 
<code>docker-compose.local.ci.yml</code> file:</p>

<pre><code class="language-yaml">  mysql:
    healthcheck:
      # Only mark the service as healthy if mysql is ready to accept connections
      # Check every 2 seconds for 30 times, each check has a timeout of 1s
      test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
      timeout: 1s
      retries: 30
      interval: 2s
</code></pre>

<p>The script in <code>test</code> was taken
from <a href="https://stackoverflow.com/a/54854239/413531">SO: Docker-compose check if mysql connection is ready</a>.</p>

<p>When starting the docker setup, <code>docker ps</code> will now add a health info to the <code>STATUS</code>:</p>

<pre><code class="language-text">$ make docker-up

$ docker ps
CONTAINER ID   IMAGE                            STATUS                           NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 1 seconds                     dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 1 seconds (health: starting)  dofroscra_ci-mysql-1

# a couple of seconds later

$ docker ps
CONTAINER ID   IMAGE                            STATUS                   NAMES
b509eb2f99c0   dofroscra/application-ci:latest  Up 13 seconds            dofroscra_ci-application-1
503e52fd9e68   mysql:8.0.28                     Up 13 seconds (healthy)  dofroscra_ci-mysql-1
</code></pre>

<p>Note the <code>(health: starting)</code> and <code>(healthy)</code> infos for the <code>mysql</code> service.</p>

<p>We can also get this info from <code>docker inspect</code> (used by our 
<a href="#wait-for-service-sh">wait-for-service.sh script</a>) via:</p>

<pre><code class="language-text">$ docker inspect --format "{{json .State.Health.Status }}" dofroscra_ci-mysql-1
"healthy"
</code></pre>

<p>FYI: We could also use the
<a href="https://docs.docker.com/compose/compose-file/#depends_on"><code>depends_on</code> property</a> with a 
<code>condition: service_healthy</code> on the <code>application</code> container so that <code>docker compose</code> would 
only start the container once the <code>mysql</code> service is healthy:</p>

<pre><code class="language-yaml">application:
  depends_on:
    mysql: 
      condition: service_healthy
</code></pre>

<p>However, this would "block" the <code>make docker-up</code> until <code>mysql</code> is actually up and running. In 
our case this is not desirable, because we can do "other stuff" in the meantime (namely: run the 
<code>qa</code> checks, because they don't require a database) and thus save a couple of seconds on each CI 
run.</p>

<p><!-- generated -->
<a id='build-target-ci'> </a>
<!-- /generated --></p>

<h3>Build target: <code>ci</code></h3>

<p>We've already introduced build targets in 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#environments-and-build-targets">Environments and build targets</a>
and how to "choose"
them <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env">through <code>make</code> with the <code>ENV</code> variable defined in a shared <code>.make/.env</code> file</a>.
Short recap:</p>

<ul>
<li>create a <code>.make/.env</code> file via <code>make make-init</code> that contains the <code>ENV</code>, e.g. 
<code>makefile
ENV=ci</code></li>
<li>the <code>.make/.env</code> file is included in the main <code>Makefile</code>, making the <code>ENV</code> variables available 
to <code>make</code></li>
<li><a href="/blog/docker-from-scratch-for-php-applications-in-2022/#make-docker-3">configure a <code>$DOCKER_COMPOSE</code> variable</a> 
that passes the <code>ENV</code> as an environment variable, i.e. via
<code>bash
ENV=$(ENV) docker-compose</code></li>
</ul>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG" alt="Initialize make to run docker commands with ENV=ci" /></a></p>

<ul>
<li><p>use the <code>ENV</code> variable in the <code>docker compose</code> configuration file to determine the 
<code>build.target</code> property. E.g. in <code>.docker/docker-compose/docker-compose-php-base.yml</code></p>

<pre><code class="language-yaml">php-base:
  build:
    target: ${ENV?}
</code></pre></li>
</ul>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/build-ci-images.PNG" alt="Build images for CI" /></a></p>

<ul>
<li>in the <code>Dockerfile</code> of a service, define the <code>ENV</code> as a build stage. E.g. in 
<code>.docker/images/php/base/Dockerfile</code>
<code>Dockerfile
FROM base as ci
# ...</code></li>
</ul>

<p>So to enable the new <code>ci</code> environment, we need to modify the Dockerfiles for the <code>php-base</code> and 
the <code>application</code> image.</p>

<p><!-- generated -->
<a id='build-stage-ci-in-the-php-base-image'> </a>
<!-- /generated --></p>

<h4>Build stage <code>ci</code> in the <code>php-base</code> image</h4>

<p><!-- generated -->
<a id='use-the-whole-codebase-as-build-context'> </a>
<!-- /generated --></p>

<h5>Use the whole codebase as build context</h5>

<p>As mentioned in section <a href="#docker-changes">Docker changes</a> we want to "bake" the codebase into 
the <code>ci</code> image of the <code>php-base</code> container.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/codebase-in-docker-image.PNG" alt="Add the codebase in the docker image" /></a></p>

<p>Thus, we must change the <code>context</code> property in 
<code>.docker/docker-compose/docker-compose-php-base.yml</code> <strong>to not only use the <code>.docker/</code> directory 
but instead the whole codebase</strong>. I.e. "dont use <code>../</code> but <code>../../</code>":</p>

<pre><code class="language-yaml"># File: .docker/docker-compose/docker-compose-php-base.yml

  php-base:
    build:
      # pass the full codebase to docker for building the image
      context: ../../
</code></pre>

<p><!-- generated -->
<a id='build-the-dependencies'> </a>
<!-- /generated --></p>

<h5>Build the dependencies</h5>

<p>The composer dependencies must be set up in the image as well, so we introduce a new stage 
stage in <code>.docker/images/php/base/Dockerfile</code>. The most trivial solution would look like this:</p>

<ul>
<li>copy the whole codebase</li>
<li>run <code>composer install</code></li>
</ul>

<pre><code class="language-Dockerfile">FROM base as ci

COPY . /codebase

RUN composer install --no-scripts --no-plugins --no-progress -o
</code></pre>

<p>However, this approach has some downsides:</p>

<ul>
<li>if <em>any</em> file in the codebase changes, the <code>COPY . /codebase</code> layer will be invalidated. I.e. 
docker could <em>not</em> use the <a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache">layer cache</a>
which also means <strong>that every layer afterwards cannot use the cache</strong> as well. In consequence the 
<code>composer install</code> would run every time - even when the <code>composer.json</code> file doesn't change.</li>
<li><a href="https://getcomposer.org/doc/06-config.md#cache-dir"><code>composer</code> itself uses a cache</a> for 
storing dependencies locally so it doesn't have to download dependencies that haven't changed.
But since we run <code>composer install</code> <em>in Docker</em>, this cache would be "thrown away" every time 
a build finishes. To mitigate that, we can use 
<a href="https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#run---mounttypecache"><code>--mount=type=cache</code></a>
to define a directory that docker will re-use between builds:
> Contents of the cache directories persists between builder invocations without invalidating 
> the instruction cache.</li>
</ul>

<p>Keeping those points in mind, we end up with the following instructions:</p>

<pre><code class="language-Dockerfile"># File: .docker/images/php/base/Dockerfile
# ...

FROM base as ci

# By only copying the composer files required to run composer install
# the layer will be cached and only invalidated when the composer dependencies are changed
COPY ./composer.json /dependencies/
COPY ./composer.lock /dependencies/

# use a cache mount to cache the composer dependencies
# this is essentially a cache that lives in Docker BuildKit (i.e. has nothing to do with the host system) 
RUN --mount=type=cache,target=/tmp/.composer \
    cd /dependencies &amp;&amp; \
    # COMPOSER_HOME=/tmp/.composer sets the home directory of composer that
    # also controls where composer looks for the cache 
    # so we don't have to download dependencies again (if they are cached)
    COMPOSER_HOME=/tmp/.composer composer install --no-scripts --no-plugins --no-progress -o 

# copy the full codebase
COPY . /codebase

RUN mv /dependencies/vendor /codebase/vendor &amp;&amp; \
    cd /codebase &amp;&amp; \
    # remove files we don't require in the image to keep the image size small
    rm -rf .docker/ &amp;&amp; \
    # we need a git repository for git-secret to work (can be an empty one)
    git init
</code></pre>

<p>FYI: The <code>COPY . /codebase</code> step doesn't actually copy "everything in the repository", because we 
have also introduced a <code>.dockerignore</code> file to exclude some files from being included in the 
build context - see section <a href="#dockerignore"><code>.dockerignore</code></a>.</p>

<p>Some notes on the final <code>RUN</code> step:</p>

<ul>
<li><code>rm -rf .docker/</code> doesn't really save "that much" in the current setup - please take it more 
as an example to remove any files that shouldn't end up in the final image (e.g. "tests in a 
production image")</li>
<li>the <code>git init</code> part is required because we need to decrypt the secrets later - and 
<code>git-secret</code> requires a <code>git</code> repository (which can be empty). We can't decrypt the secrets 
during the build, because we do not want decrypted secret files to end up in the image.</li>
</ul>

<p>When tested locally, the difference between the trivial solution and the one that makes use of 
layer caching is ~35 seconds, see the results in the <a href="#performance">Performance</a> section.</p>

<p><!-- generated -->
<a id='create-the-final-image'> </a>
<!-- /generated --></p>

<h5>Create the final image</h5>

<p>As a final step, we will rename the current stage to <code>codebase</code> and copy the "build 
artifact" from that stage into our final <code>ci</code> build stage:</p>

<pre><code class="language-Dockerfile">FROM base as codebase

# build the composer dependencies and clean up the copied files
# ...

FROM base as ci

COPY --from=codebase --chown=$APP_USER_NAME:$APP_GROUP_NAME /codebase $APP_CODE_PATH
</code></pre>

<p>Why are we not just using the previous stage directly as <code>ci</code>?</p>

<p>Because using <a href="https://docs.docker.com/develop/develop-images/multistage-build/">multistage-builds</a> 
is a 
<a href="https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds">good practice to keep the final layers of an image to a minimum</a>:
Everything that "happened" in the previous <code>codebase</code> stage will be "forgotten", i.e. not 
exported as layers.</p>

<p>That does not only save us some layers, but also allows us to get rid of
files like the <code>.docker/</code> directory. We needed that directory in the build context because
some files where required in other parts of the <code>Dockerfile</code> (e.g. the php ini files), so we
can't exclude it via <code>.dockerignore</code>. But we can remove it in the <code>codebase</code> stage - so it will NOT
be copied over and thus not end up in the final image. If we wouldn't have the <code>codebase</code> stage,
the folder would be part of the layer created when <code>COPY</code>ing all the files from the build context
and removing it via <code>rm -rf .docker/</code> would have no effect on the image size.</p>

<p>Currently, that doesn't really matter, because the building step is super simple (just a 
<code>composer install</code>) - but in a growing and more complex codebase you can easily
save a couple MB.</p>

<p>To be concrete, the <strong>multistage build has 31 layers</strong> and the final layer containing the 
codebase has a size of <strong>65.1MB</strong>.</p>

<pre><code class="language-text">$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
d778c2ee8d5e   17 minutes ago   COPY /codebase /var/www/app # buildkit          65.1MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
&lt;missing&gt;      17 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
&lt;missing&gt;      17 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
&lt;missing&gt;      18 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
&lt;missing&gt;      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
&lt;missing&gt;      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB
</code></pre>

<p>The <strong>non-multistage build has 32 layers</strong> and the final layer(s) containing the
codebase have a combined size of <strong>65.15MB</strong> (60.3MB + 4.85MB).</p>

<pre><code class="language-text">$ docker image history -H dofroscra/application-ci
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
94ba50438c9a   2 minutes ago    RUN /bin/sh -c COMPOSER_HOME=/tmp/.composer …   60.3MB    buildkit.dockerfile.v0
&lt;missing&gt;      2 minutes ago    COPY . /var/www/app # buildkit                  4.85MB    buildkit.dockerfile.v0
                                                                                ^^^^^^
&lt;missing&gt;      31 minutes ago   WORKDIR /var/www/app                            0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   COPY /usr/bin/composer /usr/local/bin/compos…   2.36MB    buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /root…   395B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   COPY ./.docker/images/php/base/.bashrc /home…   395B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   196B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   COPY ./.docker/images/php/base/conf.d/zz-app…   378B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   1.28kB    buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   41MB      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ADD https://php.hernandev.com/key/php-alpine…   451B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   62.1MB    buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ADD https://gitsecret.jfrog.io/artifactory/a…   450B      buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   RUN |8 APP_USER_ID=10000 APP_GROUP_ID=10001 …   4.74kB    buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV ENV=ci                                      0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV ALPINE_VERSION=3.15                         0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV TARGET_PHP_VERSION=8.1                      0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV APP_CODE_PATH=/var/www/app                  0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV APP_GROUP_NAME=application                  0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV APP_USER_NAME=application                   0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV APP_GROUP_ID=10001                          0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ENV APP_USER_ID=10000                           0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG ENV                                         0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG ALPINE_VERSION                              0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG TARGET_PHP_VERSION                          0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG APP_CODE_PATH                               0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG APP_GROUP_NAME                              0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG APP_USER_NAME                               0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG APP_GROUP_ID                                0B        buildkit.dockerfile.v0
&lt;missing&gt;      31 minutes ago   ARG APP_USER_ID                                 0B        buildkit.dockerfile.v0
&lt;missing&gt;      2 days ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
&lt;missing&gt;      2 days ago       /bin/sh -c #(nop) ADD file:5d673d25da3a14ce1…   5.57MB
</code></pre>

<p>Again: It is expected that the differences aren't big, because the only size savings come from 
the <code>.docker/</code> directory with a size of ~70kb.</p>

<pre><code class="language-text">$ du -hd 0 .docker
73K     .docker
</code></pre>

<p>Finally, we are also using the <a href="https://docs.docker.com/engine/reference/builder/#copy"><code>--chown</code> option of the <code>RUN</code> instruction</a>
to ensure that the files have the correct permissions.</p>

<p><!-- generated -->
<a id='build-stage-ci-in-the-application-image'> </a>
<!-- /generated --></p>

<h4>Build stage <code>ci</code> in the <code>application</code> image</h4>

<p>There is actually "nothing" to be done here. We don't need SSH any longer because it is only 
required for the <a href="/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/#ssh-configuration">SSH Configuration of PhpStorm</a>.
So the build stage is simply "empty":</p>

<pre><code class="language-Dockerfile">ARG BASE_IMAGE
FROM ${BASE_IMAGE} as base

FROM base as ci

FROM base as local
# ...
</code></pre>

<p>Though there is one thing to keep in mind: In the <code>local</code> image we used <code>sshd</code> as the entrypoint,
i.e. we had a long running process that would keep the container running. To keep the 
<code>ci</code> application container running, we must</p>

<ul>
<li>start it via the <code>-d</code> flag of <code>docker compose</code> (already done in the <code>make docker-up</code> target)
<code>makefile
.PHONY: docker-up
docker-up: validate-docker-variables
  $(DOCKER_COMPOSE) up -d $(DOCKER_SERVICE_NAME)</code></li>
<li><p><a href="https://stackoverflow.com/a/55953120">allocate a <code>tty</code> via <code>tty: true</code></a> 
in the <code>docker-compose.local.ci.yml</code> file</p>

<pre><code class="language-yaml">application:
  tty: true
</code></pre></li>
</ul>

<p><!-- generated -->
<a id='dockerignore'> </a>
<!-- /generated --></p>

<h3>.dockerignore</h3>

<p>The <a href="https://docs.docker.com/engine/reference/builder/#dockerignore-file"><code>.dockerignore</code> file</a> 
is located in the root of the repository and ensures that certain files are kept out of the 
Docker <code>build context</code>. This will</p>

<ul>
<li>speed up the build (because less files need to be transmitted to the docker daemon)</li>
<li>keep images smaller (because irrelevant files are kept out of the image)</li>
</ul>

<p>The syntax is quite similar to the <code>.gitignore</code> file - in fact I've found it to be quite often 
the case that the contents of the <code>.gitignore</code> file are a subset of the <code>.dockerignore</code> file. This 
makes kinda sense, because you <strong>typically wouldn't want files that are excluded from the 
repository to end up in a docker image</strong> (e.g. unencrypted secret files). This has also been 
noticed by others, see e.g.</p>

<ul>
<li><a href="https://www.reddit.com/r/docker/comments/evrfgp/any_way_to_copy_gitignore_contents_to_dockerignore/">Reddit: Any way to copy .gitignore contents to .dockerignore</a></li>
<li><a href="https://stackoverflow.com/q/58707272/413531">SO: Should .dockerignore typically be a superset of .gitignore?</a></li>
</ul>

<p>but to my knowledge there is currently (2022-04-24) no way to "keep the two files in sync".</p>

<p><strong>CAUTION</strong>: The behavior between the two files is NOT identical! The documentation says</p>

<blockquote>
  <p>Matching is done using Go’s filepath.Match rules. A preprocessing step removes leading and 
  trailing whitespace and eliminates . and .. elements using Go’s filepath.Clean. Lines that are blank after preprocessing are ignored.</p>
  
  <p>Beyond Go’s filepath.Match rules, Docker also supports a special wildcard string &#42;&#42; that 
  matches any number of directories (including zero). For example, &#42;&#42;/&#42;.go will exclude all 
  files that end with .go that are found in all directories, including the root of the build context.</p>
  
  <p>Lines starting with ! (exclamation mark) can be used to make exceptions to exclusions.</p>
</blockquote>

<p>Please note the part regarding <code>**\*.go</code>: In <code>.gitignore</code> it would be sufficient to write 
<code>.go</code> to match <em>any</em> file that contains <code>.go</code>, regardless of the directory. In <code>.dockerignore</code> you
<em>must</em> specify it as <code>**/*.go</code>!</p>

<p>In our case, the content of the <code>.dockerignore</code> file looks like this:</p>

<pre><code class="language-docker"># gitignore
!.env.example
**/*.env
.idea
.phpunit.result.cache
vendor/
secret.gpg
.gitsecret/keys/random_seed
.gitsecret/keys/pubring.kbx~
!*.secret
passwords.txt
.build

# additionally ignored files
.git
</code></pre>

<p><!-- generated -->
<a id='makefile-changes'> </a>
<!-- /generated --></p>

<h2>Makefile changes</h2>

<p><!-- generated -->
<a id='initialize-the-shared-variables'> </a>
<!-- /generated --></p>

<h3>Initialize the shared variables</h3>

<p>We have introduced the concept of <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env">shared variables via <code>.make/.env</code></a>
previously. It allows us to <strong>define variables in one place</strong> (=single source 
of truth) that are then used as "defaults" so we <strong>don't have to define them explicitly</strong> when 
invoking certain <code>make</code> targets (like <code>make docker-build</code>). We'll make use of this concept by 
setting the environment to <code>ci</code>via<code>ENV=ci</code> and thus making sure that all docker commands use 
<code>ci</code> "automatically" as well.</p>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/make-init-ci-docker-commands.PNG" alt="Initialize make to run docker commands with ENV=ci" /></a></p>

<p>In addition, I made a small modification by <strong>introducing a second file at <code>.make/variables.env</code></strong> 
that is also included in the main <code>Makefile</code> and <strong>holds the "default" shared variables</strong>. Those 
are neither "secret" nor are they likely to be changed for environment adjustments. The file 
is NOT ignored by <code>.gitignore</code> and is basically just the previous <code>.make/.env.example</code> file without 
the environment specific variables:</p>

<pre><code class="language-text"># File .make/variables.env

DOCKER_REGISTRY=docker.io
DOCKER_NAMESPACE=dofroscra
APP_USER_NAME=application
APP_GROUP_NAME=application
</code></pre>

<p>The <code>.make/.env</code> file is still <code>.gitignore</code>d and can be initialized with the <code>make-init</code> 
target using the <code>ENVS</code> variable:</p>

<pre><code class="language-bash">make make-init ENVS="ENV=ci SOME_OTHER_DEFAULT_VARIABLE=foo"
</code></pre>

<p>which would create a <code>.make/.env</code> file with the content</p>

<pre><code>ENV=ci
SOME_OTHER_DEFAULT_VARIABLE=foo
</code></pre>

<p>If necessary, we could also <strong>override variables defined in the <code>.make/variables.env</code> file</strong>, 
because the <code>.make/.env</code> is included last in the <code>Makefile</code>:</p>

<pre><code class="language-makefile"># File: Makefile
# ...

# include the default variables
include .make/variables.env
# include the local variables
-include .make/.env
</code></pre>

<p>The default value for <code>ENVS</code> is <code>ENV=local TAG=latest</code> to retain the same default behavior as 
before when <code>ENVS</code> is omitted. The corresponding <code>make-init</code> target is defined in the main 
<code>Makefile</code> and now looks like this:</p>

<pre><code class="language-makefile">ENVS?=ENV=local TAG=latest
.PHONY: make-init
make-init: ## Initializes the local .makefile/.env file with ENV variables for make. Use via ENVS="KEY_1=value1 KEY_2=value2"
    @$(if $(ENVS),,$(error ENVS is undefined))
    @rm -f .make/.env
    for variable in $(ENVS); do \
      echo $$variable | tee -a .make/.env &gt; /dev/null 2&gt;&amp;1; \
    done
    @echo "Created a local .make/.env file" 
</code></pre>

<p><!-- generated -->
<a id='env-based-docker-compose-config'> </a>
<!-- /generated --></p>

<h3>ENV based <code>docker compose</code> config</h3>

<p>As mentioned in section <a href="#compose-file-updates">Compose file updates</a> we need to select the 
"correct" <code>docker compose</code> configuration files based on the <code>ENV</code> value. This is done in 
<code>.make/02-00-docker.mk</code>:</p>

<pre><code class="language-makefile"># File .make/02-00-docker.mk

# ...

DOCKER_COMPOSE_DIR:=...
DOCKER_COMPOSE_COMMAND:=...

DOCKER_COMPOSE_FILE_LOCAL_CI:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.ci.yml
DOCKER_COMPOSE_FILE_CI:=$(DOCKER_COMPOSE_DIR)/docker-compose.ci.yml
DOCKER_COMPOSE_FILE_LOCAL:=$(DOCKER_COMPOSE_DIR)/docker-compose.local.yml

# we need to "assemble" the correct combination of docker-compose.yml config files
ifeq ($(ENV),ci)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI) -f $(DOCKER_COMPOSE_FILE_CI)
else ifeq ($(ENV),local)
    DOCKER_COMPOSE_FILES:=-f $(DOCKER_COMPOSE_FILE_LOCAL_CI) -f $(DOCKER_COMPOSE_FILE_LOCAL)
endif

DOCKER_COMPOSE:=$(DOCKER_COMPOSE_COMMAND) $(DOCKER_COMPOSE_FILES)
</code></pre>

<p>When we now take a look at a full recipe when using <code>ENV=ci</code> with a docker target (e.g. 
<code>docker-up</code>), we can see that the correct files are chosen, e.g.</p>

<pre><code class="language-text">$ make docker-up ENV=ci -n
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_ci --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.local.ci.yml -f ./.docker/docker-compose/docker-compose.ci.yml up -d

# =&gt;
# -f ./.docker/docker-compose/docker-compose.local.ci.yml 
# -f ./.docker/docker-compose/docker-compose.ci.yml
</code></pre>

<p><a href="/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG"><img src="/img/ci-pipeline-docker-php-gitlab-github/assemble-docker-compose-files.PNG" alt="Assemble docker-compose config files for CI" /></a></p>

<p><!-- generated -->
<a id='codebase-changes'> </a>
<!-- /generated --></p>

<h2>Codebase changes</h2>

<p><a id='add-a-test-for-encrypted-files'> </a>
<!-- /generated --></p>

<p><!-- generated -->
<a id='add-a-test-for-encrypted-files'> </a>
<!-- /generated --></p>

<h3>Add a test for encrypted files</h3>

<p>We've introduced <code>git-secret</code> in the previous tutorial
<a href="/blog/git-secret-encrypt-repository-docker/">Use git-secret to encrypt secrets in the repository</a> 
and used it to store the file <code>passwords.txt</code> encrypted in the codebase. To make sure that the
decryption works as expected on the CI systems, I've added a test at
<code>tests/Feature/EncryptionTest.php</code> to check if the file exists and if the content is correct.</p>

<pre><code class="language-php">class EncryptionTest extends TestCase
{
    public function test_ensure_that_the_secret_passwords_file_was_decrypted()
    {
        $pathToSecretFile = __DIR__."/../../passwords.txt";

        $this-&gt;assertFileExists($pathToSecretFile);

        $expected = "my_secret_password\n";
        $actual   = file_get_contents($pathToSecretFile);

        $this-&gt;assertEquals($expected, $actual);
    }
}
</code></pre>

<p>Of course this doesn't make sense in a "real world scenario", because the secret value would now 
be exposed in a test - but it suffices for now as proof of a working secret decryption.</p>

<p><!-- generated -->
<a id='add-a-password-protected-secret-gpg-key'> </a>
<!-- /generated --></p>

<h3>Add a password-protected secret <code>gpg</code> key</h3>

<p>I've mentioned in
<a href="/blog/git-secret-encrypt-repository-docker/#decrypt-files">Scenario: Decrypt file</a>
that it is also possible <strong>to use a password-protected secret <code>gpg</code> key for
an additional layer of security</strong>. I have created such a key and stored it in the repository at
<code>secret-protected.gpg.example</code> (in a "real world scenario" I wouldn't do that - but since this
is a public tutorial I want you to be able to follow along completely). The password for that
key is <code>12345678</code>.</p>

<p>The corresponding public key is located at <code>.dev/gpg-keys/alice-protected-public.gpg</code> and
belongs to the email address <code>alice.protected@example.com</code>. I've 
<a href="/blog/git-secret-encrypt-repository-docker/#adding-new-team-members">added this email address</a> and
<a href="/blog/git-secret-encrypt-repository-docker/#adding-and-encrypting-files">re-encrypted the secrets</a> afterwards via</p>

<pre><code class="language-bash">make gpg-init
make secret-add-user EMAIL="alice.protected@example.com"
make secret-encrypt
</code></pre>

<p>When I now import the <code>secret-protected.gpg.example</code> key, I can decrypt the secrets, though I
cannot use the usual <code>secret-decrypt</code> target but must instead use <code>secret-decrypt-with-password</code></p>

<pre><code class="language-bash">make secret-decrypt-with-password GPG_PASSWORD=12345678
</code></pre>

<p>or store the <code>GPG_PASSWORD</code> in the <code>.make/.env</code> file when it is initialized for CI</p>

<pre><code class="language-bash">make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
make secret-decrypt-with-password
</code></pre>

<p><!-- generated -->
<a id='create-a-junit-report-from-phpunit'> </a>
<!-- /generated --></p>

<h3>Create a JUnit report from PhpUnit</h3>

<p>I've added the
<a href="https://phpunit.readthedocs.io/en/9.5/textui.html?highlight=junit#command-line-options"><code>--log-junit</code> option</a>
to the <code>phpunit</code> configuration of the <code>test</code> make target in order to create an XML report in the
<code>.build/</code> directory in the <code>.make/01-02-application-qa.mk</code> file:</p>

<pre><code class="language-makefile"># File: .make/01-02-application-qa.mk
# ...

PHPUNIT_CMD=php vendor/bin/phpunit
PHPUNIT_ARGS= -c phpunit.xml --log-junit .build/report.xml
</code></pre>

<p>I.e. each run of the tests will now create a
<a href="https://stackoverflow.com/questions/442556/spec-for-junit-xml-output">Junit XML report</a> at
<code>.build/report.xml</code>. The file is used as an example of a build artifact, i.e.
"something that we would like to keep" from a CI run.</p>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You should now have a working CI pipeline for Github (via Github Actions)
and/or Gitlab (via Gitlab pipelines) that runs automatically on each push.</p>

<p>In the next part of this tutorial, we will 
<a href="/blog/gcp-compute-instance-vm-docker/">create a VM on GCP and provision it to run dockerized applications</a>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Mon, 25 Apr 2022 07:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/ci-pipeline-docker-php-gitlab-github/</guid>
            </item>
                    <item>
                <title>Use git-secret to encrypt secrets in the repository [Tutorial Part 6]</title>
                <description><![CDATA[<p>In the sixth part of this tutorial series on developing PHP on Docker we will <strong>setup <code>git-secret</code>
to store secrets directly in the repository</strong>. Everything will be handled through Docker and 
added as make targets for a convenient workflow.</p>

<p><a href="/img/git-secret-encrypt-repository-docker/git-secret-example.gif"><img src="/img/git-secret-encrypt-repository-docker/git-secret-example.gif" alt="git-secret example" title="git-secret example" /></a></p>

<p><small>
FYI: 
This tutorial is a precursor to the next a part 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/">Create a CI pipeline for dockerized PHP Apps</a>
because dealing with secrets is an important aspect when setting up a CI system (and later when 
deploying to production) - but I feel it's complex enough to warrant its own article.
</small></p>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/pZ-vFMfKcLY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch with the final result of this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-6-git-secret-encrypt-repository-docker">part-6-git-secret-encrypt-repository-docker</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/php-qa-tools-make-docker/">Set up PHP QA tools and control them via make</a>
and the following one is
<a href="/blog/ci-pipeline-docker-php-gitlab-github/">Create a CI pipeline for dockerized PHP Apps</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a> to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#tooling">Tooling</a>

<ul>
<li><a href="#gpg">gpg</a>

<ul>
<li><a href="#gpg-installation">gpg installation</a></li>
<li><a href="#gpg-usage">gpg usage</a>

<ul>
<li><a href="#create-gpg-key-pair">Create GPG key pair</a></li>
<li><a href="#export-list-and-import-private-gpg-keys">Export, list and import private GPG keys</a></li>
<li><a href="#export-list-and-import-public-gpg-keys">Export, list and import public GPG keys</a></li>
</ul></li>
</ul></li>
<li><a href="#git-secret">git-secret</a>

<ul>
<li><a href="#git-secret-installation">git-secret installation</a>

<ul>
<li><a href="#the-git-permission-issue">The <code>git</code> permission issue</a></li>
</ul></li>
<li><a href="#git-secret-usage">git-secret usage</a>

<ul>
<li><a href="#initialize-git-secret">Initialize git-secret</a>

<ul>
<li><a href="#the-git-secret-directory-and-the-gpg-agent-socket">The <code>git-secret</code> directory and the <code>gpg-agent</code> socket</a></li>
</ul></li>
<li><a href="#adding-listing-and-removing-users">Adding, listing and removing users</a>

<ul>
<li><a href="#reminder-rotate-the-encrypted-secrets">Reminder: Rotate the encrypted secrets</a></li>
</ul></li>
<li><a href="#adding-listing-and-removing-files-for-encryption">Adding, listing and removing files for encryption</a></li>
<li><a href="#encrypt-files">Encrypt files</a></li>
<li><a href="#decrypting-files">Decrypting files</a></li>
<li><a href="#show-changes-between-encrypted-and-decrypted-files">Show changes between encrypted and decrypted files</a></li>
</ul></li>
</ul></li>
</ul></li>
<li><a href="#makefile-adjustments">Makefile adjustments</a></li>
<li><a href="#workflow">Workflow</a>

<ul>
<li><a href="#process-challenges">Process challenges</a>

<ul>
<li><a href="#updating-secrets">Updating secrets</a></li>
<li><a href="#code-reviews-and-merge-conflicts">Code reviews and merge conflicts</a></li>
<li><a href="#local-git-secret-and-gpg-setup">Local <code>git-secret</code> and <code>gpg</code> setup</a></li>
</ul></li>
<li><a href="#scenarios">Scenarios</a>

<ul>
<li><a href="#initial-setup-of-gpg-keys">Initial setup of <code>gpg</code> keys</a></li>
<li><a href="#initial-setup-of-git-secret">Initial setup of <code>git-secret</code></a></li>
<li><a href="#initialize-gpg-after-container-startup">Initialize <code>gpg</code> after container startup</a></li>
<li><a href="#adding-new-team-members">Adding (new) team members</a></li>
<li><a href="#adding-and-encrypting-files">Adding and encrypting files</a></li>
<li><a href="#decrypt-files">Decrypt files</a></li>
<li><a href="#removing-files">Removing files</a></li>
<li><a href="#removing-team-members">Removing team members</a></li>
</ul></li>
</ul></li>
<li><a href="#pros-and-cons">Pros and cons</a>

<ul>
<li><a href="#pro">Pro</a></li>
<li><a href="#cons">Cons</a></li>
</ul></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>Dealing with secrets (passwords, tokens, key files, etc.) is close to "naming things"
when it comes to hard problems in software engineering. Some things to consider:</p>

<ul>
<li><strong>security is paramount</strong> - but high security often goes hand in hand with high inconvenience

<ul>
<li>and if things get too complicated, people look for shortcuts...</li>
</ul></li>
<li>in a team, <strong>sharing certain secret values</strong> is often mandatory

<ul>
<li>so now we need to think about secure ways to distribute and update secrets across multiple
people</li>
</ul></li>
<li>concrete secret values often <strong>depend on the environment</strong>

<ul>
<li>inherently tricky to "test" or even "review", because those values are "by definition"
different on "your machine" than on "production"</li>
</ul></li>
</ul>

<p>In fact, entire products have been build around dealing with secrets, e.g.
<a href="https://www.vaultproject.io/">HashiCorp Vault</a>,
<a href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a> or the
<a href="https://cloud.google.com/secret-manager">GCP Secret Manager</a>. Introducing those in a project comes
with a certain overhead as it's yet another service that needs to be integrated and<br />
maintained. Maybe it is the exactly right decision for your use-case - maybe it's overkill.
By the end of this article you'll at least be aware of an alternative with a lower barrier to entry.
See also the <a href="#pros-and-cons">Pros and cons</a> section in the end for an overview.</p>

<p>Even though it's
<a href="https://withblue.ink/2021/05/07/storing-secrets-and-passwords-in-git-is-bad.html">generally not advised to store secrets in a repository</a>,
I'll propose exactly that in this tutorial:</p>

<ul>
<li>identify files that contain secret values</li>
<li>make sure they are added to <code>.gitignore</code></li>
<li>encrypt them via <code>git-secret</code></li>
<li>commit the encrypted files to the repository</li>
</ul>

<p>In the end, we will be able to call</p>

<pre><code class="language-bash">make secret-decrypt
</code></pre>

<p>to reveal secrets in the codebase, make modifications to them if necessary and then run</p>

<pre><code class="language-bash">make secret-encrypt
</code></pre>

<p>to encrypt them again so that they can be committed (and pushed to the remote repository). To 
see it in action, check out branch
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-6-git-secret-encrypt-repository-docker">part-6-git-secret-encrypt-repository-docker</a>
and run the following commands:</p>

<pre><code class="language-bash"># checkout the branch
git checkout part-6-git-secret-encrypt-repository-docker

# build and start the docker setup
make make-init
make docker-build
make docker-up

# "create" the secret key - the file "secret.gpg.example" would usually NOT live in the repo!
cp secret.gpg.example secret.gpg

# initialize gpg
make gpg-init

# ensure that the decrypted secret file does not exist
ls passwords.txt

# decrypt the secret file
make secret-decrypt

# show the content of the secret file
cat passwords.txt
</code></pre>

<p><!-- generated -->
<a id='tooling'> </a>
<!-- /generated --></p>

<h2>Tooling</h2>

<p>We will set up <code>gpg</code> and <code>git-secret</code> in the php <code>base</code> image, so that the tools become available in
all other containers. Please refer to
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/">Docker from scratch for PHP 8.1 Applications in 2022</a>
for an in-depth explanation of the docker images.</p>

<div class="panel panel-default">
  <div class="panel-heading">
    <strong>Caution</strong>
  </div>
  <div class="panel-body bg-danger">
    All following commands are 
    <strong>executed <em>in</em> the <code>application</code> container.</strong>
    <br />
    <br />
    <strong>Tip:</strong>
    <br />
    See <a href="/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via -din-bashrc-helper">Easy container access via din .bashrc helper</a>
    for a convenient shortcut to log into docker containers.
  </div>
</div>

<p>Please note, that there is a caveat when using <code>git-secret</code> in a folder that is shared between 
the host system and a docker container. I'll explain that in more detail (including a workaround) 
in section 
<a href="#the-git-secret-directory-and-the-gpg-agent-socket">The <code>git-secret</code> directory and the <code>gpg-agent</code> socket</a>.</p>

<p><!-- generated -->
<a id='gpg'> </a>
<!-- /generated --></p>

<h3>gpg</h3>

<p><code>gpg</code> is short for <a href="https://gnupg.org/">The GNU Privacy Guard</a> and is an open source implementation
of the OpenPGP standard. In short, it allows us to create a personal key file pair
(similar to SSH keys) with a private secret key and a public
key that can be shared with other parties whose messages you want to decrypt.</p>

<p><!-- generated -->
<a id='gpg-installation'> </a>
<!-- /generated --></p>

<h4>gpg installation</h4>

<p>To install it, we can simply run <code>apk add gnupg</code> and thus update 
<code>.docker/images/php/base/Dockerfile</code> accordingly</p>

<pre><code class="language-Dockerfile"># File: .docker/images/php/base/Dockerfile

RUN apk add --update --no-cache \
        bash \
        gnupg \
        make \
#...
</code></pre>

<p><!-- generated -->
<a id='gpg-usage'> </a>
<!-- /generated --></p>

<h4>gpg usage</h4>

<p>I'll only cover the strictly necessary <code>gpg</code> commands here. Please refer to
<a href="https://git-secret.io/#using-gpg">the "Using GPG" section in the <code>git-secret</code> docu</a>
for further information.</p>

<p><!-- generated -->
<a id='create-gpg-key-pair'> </a>
<!-- /generated --></p>

<h5>Create GPG key pair</h5>

<p>We need <code>gpg</code> to <strong>create the gpg key pair</strong> via</p>

<pre><code class="language-bash">name="Pascal Landau"
email="pascal.landau@example.com"
gpg --batch --gen-key &lt;&lt;EOF
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: $name
Name-Email: $email
Expire-Date: 0
%no-protection
EOF
</code></pre>

<p>The <code>%no-protection</code> will create a key without password, see
also <a href="https://gist.github.com/woods/8970150">this gist to "Creating gpg keys non-interactively"</a>.
To use a password (e.g. <code>12345678</code>, we could have replace the <code>%no-protection</code> line with</p>

<pre><code class="language-text">Passphrase: 12345678
</code></pre>

<p>All options for the unattended creation are defined in the 
<a href="https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html">official docs at "Unattended key generation"</a>.</p>

<p>Output:</p>

<pre><code class="language-text">$ name="Pascal Landau"
$ email="pascal.landau@example.com"
$ gpg --batch --gen-key &lt;&lt;EOF
&gt; Key-Type: 1
&gt; Key-Length: 2048
&gt; Subkey-Type: 1
&gt; Subkey-Length: 2048
&gt; Name-Real: $name
&gt; Name-Email: $email
&gt; Expire-Date: 0
&gt; %no-protection
&gt; EOF
gpg: key E1E734E00B611C26 marked as ultimately trusted
gpg: revocation certificate stored as '/root/.gnupg/opengpg-revocs.d/74082D81525723F5BF5B2099E1E734E00B611C26.rev'
</code></pre>

<p>You could also run <code>gpg --gen-key</code> without the <code>--batch</code> flag to be guided interactively through the
process.</p>

<p><!-- generated -->
<a id='export-list-and-import-private-gpg-keys'> </a>
<!-- /generated --></p>

<h5>Export, list and import private GPG keys</h5>

<p>The <strong>private key can be exported</strong> via</p>

<pre><code class="language-bash">email="pascal.landau@example.com"
path="secret.gpg"
gpg --output "$path" --armor --export-secret-key "$email"
</code></pre>

<p><strong>This secret key must never be shared</strong>!</p>

<p>It looks like this:</p>

<pre><code class="language-text">-----BEGIN PGP PRIVATE KEY BLOCK-----

lQOYBF7VVBwBCADo9un+SySu/InHSkPDpFVKuZXg/s4BbZmqFtYjvUUSoRAeSejv
G21nwttQGut+F+GdpDJL6W4pmLS31Kxpt6LCAxhID+PRYiJQ4k3inJfeUx7Ws339
XDPO3Rys+CmnZchcEgnbOfQlEqo51DMj6mRF2Ra/6svh7lqhrixGx1BaKn6VlHkC
...
ncIcHxNZt7eK644nWDn7j52HsRi+wcWsZ9mjkUgZLtyMPJNB5qlKQ18QgVdEAhuZ
xT3SieoBPd+tZikhu3BqyIifmLnxOJOjOIhbQrgFiblvzU1iOUOTOcSIB+7A
=YmRm
-----END PGP PRIVATE KEY BLOCK-----
</code></pre>

<p>All <strong>secret keys can be listed</strong> via</p>

<pre><code class="language-bash">gpg --list-secret-keys
</code></pre>

<p>Output:</p>

<pre><code class="language-text">$ gpg --list-secret-keys
/root/.gnupg/pubring.kbx
------------------------
sec   rsa2048 2022-03-27 [SCEA]
      74082D81525723F5BF5B2099E1E734E00B611C26
uid           [ultimate] Pascal Landau &lt;pascal.landau@example.com&gt;
ssb   rsa2048 2022-03-27 [SEA]

</code></pre>

<p>You can <strong>import the private key</strong> via</p>

<pre><code class="language-bash">path="secret.gpg"
gpg --import "$path"
</code></pre>

<p>and get the following output:</p>

<pre><code class="language-text">$ path="secret.gpg"
$ gpg --import "$path"
gpg: key E1E734E00B611C26: "Pascal Landau &lt;pascal.landau@example.com&gt;" not changed
gpg: key E1E734E00B611C26: secret key imported
gpg: Total number processed: 1
gpg:              unchanged: 1
gpg:       secret keys read: 1
gpg:  secret keys unchanged: 1
</code></pre>

<p><strong>Caution:</strong> If the secret key requires a password, you would now be prompted for it. We can 
circumvent the prompt by using <code>--batch --yes --pinentry-mode loopback</code>:</p>

<pre><code class="language-bash">path="secret.gpg"
gpg --import --batch --yes --pinentry-mode loopback "$path"
</code></pre>

<p>See also <a href="https://betakuang.medium.com/using-command-line-passphrase-input-for-gpg-with-git-for-windows-f78ae2c7cd2e">Using Command-Line Passphrase Input for GPG</a>.
In doing so, we don't need to provide the password just yet - but we must pass it later when we 
attempt to <a href="#decrypting-files">decrypt files</a>.</p>

<p><!-- generated -->
<a id='export-list-and-import-public-gpg-keys'> </a>
<!-- /generated --></p>

<h5>Export, list and import public GPG keys</h5>

<p>The <strong>public key can be exported</strong> to <code>public.gpg</code> via</p>

<pre><code class="language-bash">email="pascal.landau@example.com"
path="public.gpg"
gpg --armor --export "$email" &gt; "$path"
</code></pre>

<p>It looks like this:</p>

<pre><code class="language-text">-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBF7VVBwBCADo9un+SySu/InHSkPDpFVKuZXg/s4BbZmqFtYjvUUSoRAeSejv
G21nwttQGut+F+GdpDJL6W4pmLS31Kxpt6LCAxhID+PRYiJQ4k3inJfeUx7Ws339
...
3LLbK7Qxz0cV12K7B+n2ei466QAYXo03a7WlsPWn0JTFCsHoCOphjaVsncIcHxNZ
t7eK644nWDn7j52HsRi+wcWsZ9mjkUgZLtyMPJNB5qlKQ18QgVdEAhuZxT3SieoB
Pd+tZikhu3BqyIifmLnxOJOjOIhbQrgFiblvzU1iOUOTOcSIB+7A
=g0hF
-----END PGP PUBLIC KEY BLOCK-----
</code></pre>

<p><strong>List all public keys</strong> via</p>

<pre><code class="language-bash">gpg --list-keys
</code></pre>

<p>Output:</p>

<pre><code class="language-text">$ gpg --list-keys
/root/.gnupg/pubring.kbx
------------------------
pub   rsa2048 2022-03-27 [SCEA]
      74082D81525723F5BF5B2099E1E734E00B611C26
uid           [ultimate] Pascal Landau &lt;pascal.landau@example.com&gt;
sub   rsa2048 2022-03-27 [SEA]
</code></pre>

<p>The <strong>public key can be imported</strong> in the same way as private keys via</p>

<pre><code class="language-bash">path="public.gpg"
gpg --import "$path"
</code></pre>

<p>Example:</p>

<pre><code class="language-text">$ gpg --import /var/www/app/public.gpg
gpg: key E1E734E00B611C26: "Pascal Landau &lt;pascal.landau@example.com&gt;" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1
</code></pre>

<p><!-- generated -->
<a id='git-secret'> </a>
<!-- /generated --></p>

<h3>git-secret</h3>

<p>The official website of <a href="https://git-secret.io/">git-secret</a> is already doing a great job of
introducing the tool. In short, it allows us to <strong>declare certain files as "secrets"</strong> and <strong>encrypt
them via <code>gpg</code></strong> - using the keys of all trusted parties. The encrypted file can then by <strong>stored
safely directly in the git repository</strong> and <strong>decrypted if required</strong>.</p>

<p>In this tutorial I'm using <code>git-secret v0.4.0</code></p>

<pre><code class="language-text">$ git secret --version
0.4.0
</code></pre>

<p><!-- generated -->
<a id='git-secret-installation'> </a>
<!-- /generated --></p>

<h4>git-secret installation</h4>

<p>The <a href="https://git-secret.io/installation#alpine">installation instructions for Alpine</a> read as
follows:</p>

<pre><code class="language-bash">sh -c "echo 'https://gitsecret.jfrog.io/artifactory/git-secret-apk/all/main'" &gt;&gt; /etc/apk/repositories
wget -O /etc/apk/keys/git-secret-apk.rsa.pub 'https://gitsecret.jfrog.io/artifactory/api/security/keypair/public/repositories/git-secret-apk'
apk add --update --no-cache git-secret
</code></pre>

<p>Plus, we need to account for a recent change in <code>git</code> that requires that the parent directory is 
owned by the user executing the <code>git</code> command. See also the more detailed explanation in section
<a href="#the-git-permission-issue">The <code>git</code> permission issue</a>.</p>

<p>We update the <code>.docker/images/php/base/Dockerfile</code> accordingly:</p>

<pre><code class="language-Dockerfile"># File: .docker/images/php/base/Dockerfile

# install git-secret
# @see https://git-secret.io/installation#alpine
ADD https://gitsecret.jfrog.io/artifactory/api/security/keypair/public/repositories/git-secret-apk /etc/apk/keys/git-secret-apk.rsa.pub

RUN echo "https://gitsecret.jfrog.io/artifactory/git-secret-apk/all/main" &gt;&gt; /etc/apk/repositories  &amp;&amp; \
    apk add --update --no-cache \
        bash \
        git-secret \
        gawk \
        gnupg \
        make \
#...

# Fix the git permission issue
RUN git config --system --add safe.directory "$APP_CODE_PATH"
</code></pre>

<p><!-- generated -->
<a id='the-git-permission-issue'> </a>
<!-- /generated --></p>

<h5>The <code>git</code> permission issue</h5>

<p>In April 2022, 
<a href="https://github.blog/2022-04-12-git-security-vulnerability-announced/">Github accounced the security vulnerability <code>CVE-2022-24765</code></a>,
that was fixed in <code>git v2.35.2</code></p>

<blockquote>
  <p>This version changes Git’s behavior when looking for a top-level <code>.git</code> directory to stop when 
  its directory traversal changes ownership from the current user.</p>
</blockquote>

<p>In practice, the following error occurs if the parent directory is not owned by the user that 
executes the <code>git</code> command</p>

<pre><code class="language-text">Error: fatal: unsafe repository ('/parent/dir/of/.git-folder' is owned by someone else)
To add an exception for this directory, call:

    git config --global --add safe.directory /parent/dir/of/.git-folder
</code></pre>

<p>When using <code>git secret</code>, we would get the slightly misleading error message</p>

<pre><code class="language-text">git-secret: abort: not in dir with git repo. Use 'git init' or 'git clone', then in repo use 'git secret init'
</code></pre>

<p>We can "fix" the issue by using the new multi-valued 
<a href="https://git-scm.com/docs/git-config/2.36.0#Documentation/git-config.txt-safedirectory">safe.directory</a> 
configuration via</p>

<pre><code>git config --system --add safe.directory /parent/dir/of/.git-folder
</code></pre>

<p>Note, that we didn't use the suggested <code>--global</code> option but <code>--system</code> instead, so that the 
configuration is set for <em>any</em> user.</p>

<p>Wait - why not just ensure <strong>that the parent directory of the <code>.git</code> folder has the correct 
permissions</strong>?</p>

<p>Well... there's currently (2022-05-28) a <strong>bug in Docker Desktop that makes the permissions of bind 
mounts kinda unpredictable</strong>, see 
<a href="https://github.com/docker/for-win/issues/12742">Ownership of files set via bind mount is set to user who accesses the file first</a>
and by applying the fix directly in the <code>Dockerfile</code> we can solve the issue reliably.</p>

<p><!-- generated -->
<a id='git-secret-usage'> </a>
<!-- /generated --></p>

<h4>git-secret usage</h4>

<p><!-- generated -->
<a id='initialize-git-secret'> </a>
<!-- /generated --></p>

<h5>Initialize git-secret</h5>

<p><code>git-secret</code> is initialized via the following command <em>run in the root of the git repository</em></p>

<pre><code class="language-bash">git secret init
</code></pre>

<pre><code class="language-text">$ git secret init
git-secret: init created: '/var/www/app/.gitsecret/'
</code></pre>

<p>We only need to do this once, because we'll commit the folder to git later. It contains the
following files:</p>

<pre><code class="language-text">$ git status | grep ".gitsecret"
        new file:   .gitsecret/keys/pubring.kbx
        new file:   .gitsecret/keys/pubring.kbx~
        new file:   .gitsecret/keys/trustdb.gpg
        new file:   .gitsecret/paths/mapping.cfg
</code></pre>

<p>The <code>pubring.kbx~</code> file (with the trailing tilde <code>~</code>) is only a temporary file and can safely be
git-ignored. See also
<a href="https://github.com/sobolevn/git-secret/issues/566#issuecomment-570059374">Can't find any docs about keyring.kbx~ file</a>.</p>

<p><!-- generated -->
<a id='the-git-secret-directory-and-the-gpg-agent-socket'> </a>
<!-- /generated --></p>

<h6>The <code>git-secret</code> directory and the <code>gpg-agent</code> socket</h6>

<p>To use <code>git-secret</code> in a directory that is <strong>shared between the host system and docker</strong>, we need to 
also run the following commands:</p>

<pre><code class="language-bash">tee .gitsecret/keys/S.gpg-agent &lt;&lt;EOF
%Assuan%
socket=/tmp/S.gpg-agent
EOF

tee .gitsecret/keys/S.gpg-agent.ssh &lt;&lt;EOF
%Assuan%
socket=/tmp/S.gpg-agent.ssh
EOF

tee .gitsecret/keys/gpg-agent.conf &lt;&lt;EOF
extra-socket /tmp/S.gpg-agent.extra
browser-socket /tmp/S.gpg-agent.browser
EOF
</code></pre>

<p>This is necessary because there is an issue <strong>when <code>git-secret</code> is used in a setup where the 
codebase is shared between the host system and a docker container</strong>. 
I've explained the details in the Github issue
<a href="https://github.com/sobolevn/git-secret/issues/806">"gpg: can't connect to the agent: IPC connect call failed" error in docker alpine on shared volume</a>.</p>

<p>In short:</p>

<ul>
<li><code>gpg</code> uses a <code>gpg-agent</code> to perform its tasks and the two tools communicate through sockets 
that are created in the <code>--home-directory</code> of the <code>gpg-agent</code></li>
<li>the agent is started implicitly through a <code>gpg</code> command used by <code>git-secret</code>, using the 
<code>.gitsecret/keys</code> directories as a <code>--home-directory</code></li>
<li>because the location of the <code>--home-directory</code> is shared with the host system, the socket 
creation fails (potentially only an issue for Docker Desktop, see the related discussion in 
Github issue <a href="https://github.com/docker/for-mac/issues/483#issuecomment-647325015">Support for sharing unix sockets</a>)</li>
</ul>

<p>The corresponding error messages are</p>

<pre><code class="language-text">gpg: can't connect to the agent: IPC connect call failed

gpg-agent: error binding socket to '/var/www/app/.gitsecret/keys/S.gpg-agent': I/O error
</code></pre>

<p>The <strong>workaround for this problem</strong> can be found in 
<a href="https://askubuntu.com/a/1053594/1583296">this thread</a>: Configure <code>gpg</code> to use different 
locations for the sockets by 
<a href="https://github.com/sobolevn/git-secret/issues/806#issuecomment-1084202671">placing additional <code>gpg</code> configuration files in the <code>.gitsecret/keys</code> directory</a>:</p>

<p><strong>S.gpg-agent</strong></p>

<pre><code class="language-text">%Assuan%
socket=/tmp/S.gpg-agent
</code></pre>

<p><strong>S.gpg-agent.ssh</strong></p>

<pre><code class="language-text">%Assuan%
socket=/tmp/S.gpg-agent.ssh
</code></pre>

<p><strong>gpg-agent.conf</strong></p>

<pre><code class="language-text">extra-socket /tmp/S.gpg-agent.extra
browser-socket /tmp/S.gpg-agent.browser
</code></pre>

<p><!-- generated -->
<a id='adding-listing-and-removing-users'> </a>
<!-- /generated --></p>

<h5>Adding, listing and removing users</h5>

<p>To <strong>add a new user</strong>, you must first <a href="#export-list-and-import-public-gpg-keys">import its public gpg key</a>. Then
run:</p>

<pre><code class="language-bash">email="pascal.landau@example.com"
git secret tell "$email"
</code></pre>

<p>In this case, the user <code>pascal.landau@example.com</code> will now be able to decrypt the secrets.</p>

<p>To <strong>show the users</strong> run</p>

<pre><code class="language-bash">git secret whoknows
</code></pre>

<pre><code class="language-text">$ git secret whoknows
pascal.landau@example.com
</code></pre>

<p><strong>To remove a user</strong>, run</p>

<pre><code class="language-bash">email="pascal.landau@example.com"
git secret killperson "$email"
</code></pre>

<p>FYI: This command was renamed to <code>removeperson</code> in <code>git-secret &gt;= 0.5.0</code></p>

<pre><code class="language-text">$ git secret killperson pascal.landau@example.com
git-secret: removed keys.
git-secret: now [pascal.landau@example.com] do not have an access to the repository.
git-secret: make sure to hide the existing secrets again.
</code></pre>

<p>User <code>pascal.landau@example.com</code> will no longer be able to decrypt the secrets.</p>

<p><strong>Caution: The secrets need to be re-encrypted after removing a user!</strong></p>

<p><!-- generated -->
<a id='reminder-rotate-the-encrypted-secrets'> </a>
<!-- /generated --></p>

<h6>Reminder: Rotate the encrypted secrets</h6>

<p>Please be aware that <strong>not only your secrets are stored in git, but who had access as well</strong>. I.e. 
even if you remove a user and re-encrypt the secrets, that user would <strong>still be able to decrypt 
the secrets of a previous commit</strong> (when the user was still added). In consequence, <strong>you need 
to rotate the encrypted secrets themselves as well after removing a user</strong>.</p>

<p>But isn't that a great flaw in the system, making it a bad idea to use <code>git-secret</code> in general?</p>

<p>In my opinion: No.</p>

<p>If the removed user had access to the secrets at <strong>any</strong> point in time (no 
matter where they have been stored), he could very well have just created a local copy or simply 
"written them down". In terms of security there is really no "added downside" due to <code>git-secret</code>.
It just makes it <em>very</em> clear that you <em>must</em> rotate the secrets ¯&#92;&#95;(ツ)&#95;/¯</p>

<p>See also this 
<a href="https://news.ycombinator.com/item?id=11663403">lengthy discussion on <code>git-secret</code> on Hacker News</a>.</p>

<p><!-- generated -->
<a id='adding-listing-and-removing-files-for-encryption'> </a>
<!-- /generated --></p>

<h5>Adding, listing and removing files for encryption</h5>

<p>Run <code>git secret add [filenames...]</code> for <strong>files you want to encrypt</strong>. Example:</p>

<pre><code class="language-bash">git secret add .env
</code></pre>

<p>If <code>.env</code> is not added in <code>.gitignore</code>, <code>git-secret</code> will display a warning and add it 
automatically.</p>

<pre><code class="language-text">git-secret: these files are not in .gitignore: .env
git-secret: auto adding them to .env
git-secret: 1 item(s) added.
</code></pre>

<p>Otherwise, the file is added with no warning.</p>

<pre><code class="language-text">$ git secret add .env
git-secret: 1 item(s) added.
</code></pre>

<p>You only need to add files once. They are then stored in <code>.gitsecret/paths/mapping.cfg</code>:</p>

<pre><code class="language-text">$ cat .gitsecret/paths/mapping.cfg
.env:505070fc20233cb426eac6a3414399d0f466710c993198b1088e897fdfbbb2d5
</code></pre>

<p>You can also show the added files via</p>

<pre><code class="language-bash">git secret list
</code></pre>

<pre><code class="language-text">$ git secret list
.env
</code></pre>

<p><strong>Caution: The files are not yet encrypted!</strong></p>

<p>If you want to <strong>remove a file from being encrypted</strong>, run</p>

<pre><code class="language-bash">git secret remove .env
</code></pre>

<p>Output</p>

<pre><code class="language-text">$ git secret remove .env
git-secret: removed from index.
git-secret: ensure that files: [.env] are now not ignored.
</code></pre>

<p><!-- generated -->
<a id='encrypt-files'> </a>
<!-- /generated --></p>

<h5>Encrypt files</h5>

<p>To actually <strong>encrypt the files</strong>, run:</p>

<pre><code class="language-bash">git secret hide
</code></pre>

<p>Output:</p>

<pre><code class="language-text">$ git secret hide
git-secret: done. 1 of 1 files are hidden.
</code></pre>

<p>The encrypted (binary) file is stored at <code>$filename.secret</code>, i.e. <code>.env.secret</code> in this case:</p>

<pre><code class="language-text">$ cat .env.secret
�☺♀♥�H~�B�Ӯ☺�"��▼♂F�►���l�Cs��S�@MHWs��e������{♣♫↕↓�L� ↕s�1�J$◄♥�;���ǆ֕�Za�����\u�ٲ&amp; ¶��V�► ���6��
;&lt;�d:��}ҨD%.�;��&amp;��G����vWW�]&gt;���߶��▲;D�+Rs�S→�Y!&amp;J��۪8���ٔF��→f����*��$♠���&amp;RC�8▼♂�☻z h��Z0M�T&gt;
</code></pre>

<p>The encrypted files are de-cryptable <strong>for all users that have been added via <code>git secret tell</code></strong>. 
That also means that you need to <strong>run this command again whenever a new user is added</strong>.</p>

<p><!-- generated -->
<a id='decrypting-files'> </a>
<!-- /generated --></p>

<h5>Decrypting files</h5>

<p>You can <strong>decrypt files</strong> via</p>

<pre><code class="language-bash">git secret reveal
</code></pre>

<p>Output:</p>

<pre><code class="language-text">$ git secret reveal
File '/var/www/app/.env' exists. Overwrite? (y/N) y
git-secret: done. 1 of 1 files are revealed.
</code></pre>

<ul>
<li>the files are decrypted and will overwrite the current, unencrypted files (if they already exist)

<ul>
<li>use the <code>-f</code> option to force the overwrite and run non-interactively</li>
</ul></li>
<li>if you only want to check the content of an encrypted file, you can use
<code>git secret cat $filename</code> (e.g. <code>git secret cat .env</code>)</li>
</ul>

<p>In case the secret <code>gpg</code> key is password protected, you must pass the password 
<a href="https://git-secret.io/git-secret-reveal">via the <code>-p</code> option</a>. E.g. for password <code>123456</code></p>

<pre><code class="language-bash">git secret reveal -p 123456
</code></pre>

<p><!-- generated -->
<a id='show-changes-between-encrypted-and-decrypted-files'> </a>
<!-- /generated --></p>

<h5>Show changes between encrypted and decrypted files</h5>

<p>One problem that comes with encrypted files: <strong>You can't review them during a code review in a
remote tool</strong>. So in order to understand what changes have been made, it is helpful to
<strong>show the changes between the encrypted and the decrypted files</strong>. This can be done via</p>

<pre><code class="language-bash">git secret changes
</code></pre>

<p>Output:</p>

<pre><code class="language-text">$ echo "foo" &gt;&gt; .env
$ git secret changes
git-secret: changes in /var/www/app/.env:
--- /dev/fd/63
+++ /var/www/app/.env
@@ -34,3 +34,4 @@
 MAIL_ENCRYPTION=null
 MAIL_FROM_ADDRESS=null
 MAIL_FROM_NAME="${APP_NAME}"
+foo
</code></pre>

<p>Note the <code>+foo</code> at the bottom of the output. It was added in the first line via 
<code>echo "foo"&gt; &gt;&gt; .env</code>.</p>

<p><!-- generated -->
<a id='makefile-adjustments'> </a>
<!-- /generated --></p>

<h2>Makefile adjustments</h2>

<p>Since I won't be able to remember all the commands for <code>git-secret</code> and <code>gpg</code>, I've added them to
the Makefile at <code>.make/01-00-application-setup.mk</code>:</p>

<pre><code class="language-makefile"># File: .make/01-00-application-setup.mk

#...

# gpg

DEFAULT_SECRET_GPG_KEY?=secret.gpg
DEFAULT_PUBLIC_GPG_KEYS?=.dev/gpg-keys/*

.PHONY: gpg
gpg: ## Run gpg commands. Specify the command e.g. via ARGS="--list-keys"
    $(EXECUTE_IN_APPLICATION_CONTAINER) gpg $(ARGS)

.PHONY: gpg-export-public-key
gpg-export-public-key: ## Export a gpg public key e.g. via EMAIL="john.doe@example.com" PATH=".dev/gpg-keys/john-public.gpg"
    @$(if $(PATH),,$(error PATH is undefined))
    @$(if $(EMAIL),,$(error EMAIL is undefined))
    "$(MAKE)" -s gpg ARGS="gpg --armor --export $(EMAIL) &gt; $(PATH)"

.PHONY: gpg-export-private-key
gpg-export-private-key: ## Export a gpg private key e.g. via EMAIL="john.doe@example.com" PATH="secret.gpg"
    @$(if $(PATH),,$(error PATH is undefined))
    @$(if $(EMAIL),,$(error EMAIL is undefined))
    "$(MAKE)" -s gpg ARGS="--output $(PATH) --armor --export-secret-key $(EMAIL)"

.PHONY: gpg-import
gpg-import: ## Import a gpg key file e.g. via GPG_KEY_FILES="/path/to/file /path/to/file2"
    @$(if $(GPG_KEY_FILES),,$(error GPG_KEY_FILES is undefined))
    "$(MAKE)" -s gpg ARGS="--import --batch --yes --pinentry-mode loopback $(GPG_KEY_FILES)"

.PHONY: gpg-import-default-secret-key
gpg-import-default-secret-key: ## Import the default secret key
    "$(MAKE)" -s gpg-import GPG_KEY_FILES="$(DEFAULT_SECRET_GPG_KEY)"

.PHONY: gpg-import-default-public-keys
gpg-import-default-public-keys: ## Import the default public keys
    "$(MAKE)" -s gpg-import GPG_KEY_FILES="$(DEFAULT_PUBLIC_GPG_KEYS)" 

.PHONY: gpg-init
gpg-init: gpg-import-default-secret-key gpg-import-default-public-keys ## Initialize gpg in the container, i.e. import all public and private keys

# git-secret

.PHONY: git-secret
git-secret: ## Run git-secret commands. Specify the command e.g. via ARGS="hide"
    $(EXECUTE_IN_APPLICATION_CONTAINER) git-secret $(ARGS)

.PHONY: secret-init
secret-init: ## Initialize git-secret in the repository via `git-secret init`
    "$(MAKE)" -s git-secret ARGS="init"

.PHONY: secret-init-gpg-socket-config
secret-init-gpg-socket-config: ## Initialize the config files to change the gpg socket locations
    echo "%Assuan%" &gt; .gitsecret/keys/S.gpg-agent
    echo "socket=/tmp/S.gpg-agent" &gt;&gt; .gitsecret/keys/S.gpg-agent
    echo "%Assuan%" &gt; .gitsecret/keys/S.gpg-agent.ssh
    echo "socket=/tmp/S.gpg-agent.ssh" &gt;&gt; .gitsecret/keys/S.gpg-agent.ssh
    echo "extra-socket /tmp/S.gpg-agent.extra" &gt; .gitsecret/keys/gpg-agent.conf
    echo "browser-socket /tmp/S.gpg-agent.browser" &gt;&gt; .gitsecret/keys/gpg-agent.conf

.PHONY: secret-encrypt
secret-encrypt: ## Decrypt secret files via `git-secret hide`
    "$(MAKE)" -s git-secret ARGS="hide"

.PHONY: secret-decrypt
secret-decrypt: ## Decrypt secret files via `git-secret reveal -f`
    "$(MAKE)" -s git-secret ARGS="reveal -f" 

.PHONY: secret-decrypt-with-password
secret-decrypt-with-password: ## Decrypt secret files using a password for gpg via `git-secret reveal -f -p $(GPG_PASSWORD)`
    @$(if $(GPG_PASSWORD),,$(error GPG_PASSWORD is undefined))
    "$(MAKE)" -s git-secret ARGS="reveal -f -p $(GPG_PASSWORD)" 

.PHONY: secret-add
secret-add: ## Add a file to git secret via `git-secret add $FILE`
    @$(if $(FILE),,$(error FILE is undefined))
    "$(MAKE)" -s git-secret ARGS="add $(FILE)"

.PHONY: secret-cat
secret-cat: ## Show the contents of file to git secret via `git-secret cat $FILE`
    @$(if $(FILE),,$(error FILE is undefined))
    "$(MAKE)" -s git-secret ARGS="cat $(FILE)"

.PHONY: secret-list
secret-list: ## List all files added to git secret `git-secret list`
    "$(MAKE)" -s git-secret ARGS="list"

.PHONY: secret-remove
secret-remove: ## Remove a file from git secret via `git-secret remove $FILE`
    @$(if $(FILE),,$(error FILE is undefined))
    "$(MAKE)" -s git-secret ARGS="remove $(FILE)"

.PHONY: secret-add-user
secret-add-user: ## Remove a user from git secret via `git-secret tell $EMAIL`
    @$(if $(EMAIL),,$(error EMAIL is undefined))
    "$(MAKE)" -s git-secret ARGS="tell $(EMAIL)"

.PHONY: secret-show-users
secret-show-users: ## Show all users that have access to git secret via `git-secret whoknows`
    "$(MAKE)" -s git-secret ARGS="whoknows"

.PHONY: secret-remove-user
secret-remove-user: ## Remove a user from git secret via `git-secret killperson $EMAIL`
    @$(if $(EMAIL),,$(error EMAIL is undefined))
    "$(MAKE)" -s git-secret ARGS="killperson $(EMAIL)"

.PHONY: secret-diff
secret-diff: ## Show the diff between the content of encrypted and decrypted files via `git-secret changes`
    "$(MAKE)" -s git-secret ARGS="changes"
</code></pre>

<p><!-- generated -->
<a id='workflow'> </a>
<!-- /generated --></p>

<h2>Workflow</h2>

<p>Working with <code>git-secret</code> is pretty straight forward:</p>

<ul>
<li>initialize <code>git-secret</code></li>
<li>add all users</li>
<li>add all secret files and make sure they are ignored via <code>.gitignore</code></li>
<li>encrypt the files</li>
<li>commit the encrypted files like "any other file"</li>
<li>if any changes were made by other team members to the files:

<ul>
<li>=> decrypt to get the most up-to-date ones</li>
</ul></li>
<li>if any modifications are required from your side:

<ul>
<li>=> make the changes to the decrypted files and then re-encrypt them again</li>
</ul></li>
</ul>

<p>But: The devil is in the details. The <a href="#process-challenges">Process challenges</a> section explains
some of the pitfalls that we have encountered and the <a href="#scenarios">Scenarios</a> section gives some
concrete examples for common scenarios.</p>

<p><!-- generated -->
<a id='process-challenges'> </a>
<!-- /generated --></p>

<h3>Process challenges</h3>

<p>From a process perspective we've encountered some challenges that I'd like to mention - including
how we deal with them.</p>

<p><!-- generated -->
<a id='updating-secrets'> </a>
<!-- /generated --></p>

<h4>Updating secrets</h4>

<p>When updating secrets you must ensure to always <strong>decrypt the files first</strong> in order to avoid
using "stale" files that you might still have locally. I usually check out the latest <code>main</code>
branch and run <code>git secret reveal</code> to have the most up-to-date versions of the secret files. You 
could also use a <a href="https://stackoverflow.com/a/4185449/413531"><code>post-merge</code> git hook</a> to do 
this automatically, but I personally don't want to risk overwriting my local secret files by 
accident.</p>

<p><!-- generated -->
<a id='code-reviews-and-merge-conflicts'> </a>
<!-- /generated --></p>

<h4>Code reviews and merge conflicts</h4>

<p>Since the <strong>encrypted files cannot be diffed meaningfully</strong>, the code reviews become more difficult
when secrets are involved. We use Gitlab for reviews and I usually first check the diff of
the <code>.gitsecret/paths/mapping.cfg</code> file to see "which files have changed" directly in the UI.</p>

<p>In addition, I will</p>

<ul>
<li>checkout the <code>main</code> branch</li>
<li>decrypt the secrets via <code>git secret reveal -f</code></li>
<li>checkout the <code>feature-branch</code></li>
<li>run <code>git secret changes</code> to see the differences between the decrypted files from <code>main</code> and the
encrypted files from <code>feature-branch</code></li>
</ul>

<p>Things get even more complicated when multiple team members need to modify secret files at the same
time on different branches, as <strong>the encrypted files cannot be compared - i.e. git cannot be smart 
about delta updates</strong>.
The only way around this is coordinating the pull requests, i.e. merge the first, update the
secrets of the second and then merge the second.</p>

<p>Fortunately, this has only happened very rarely so far.</p>

<p><!-- generated -->
<a id='local-git-secret-and-gpg-setup'> </a>
<!-- /generated --></p>

<h4>Local <code>git-secret</code> and <code>gpg</code> setup</h4>

<p>Currently, all developers in our team have <code>git-secret</code> installed locally (instead of using it
through docker) and use their own <code>gpg</code> keys.</p>

<p>This means more onboarding overhead, because</p>

<ul>
<li>a new dev must

<ul>
<li>install <code>git-secret</code> locally (*)</li>
<li>install and setup <code>gpg</code> locally (*)</li>
<li>create a <code>gpg</code> key pair</li>
</ul></li>
<li>the public key must be added by every other team member (*)</li>
<li>the user of the key must be added via <code>git secret tell</code></li>
<li>the secrets must be re-encrypted</li>
</ul>

<p>And for offboarding</p>

<ul>
<li>the public key must be removed by every other team member (*)</li>
<li>the user of the key must be removed via <code>git secret killperson</code></li>
<li>the secrets must be re-encrypted</li>
</ul>

<p>Plus, we need to ensure that the <code>git-secret</code> and <code>gpg</code> versions are kept up-to-date for everyone to
not run into any compatibility issues.</p>

<p>As an alternative, I'm currently leaning more towards <strong>handling everything through docker</strong> (as
presented in this tutorial). All steps marked with (*) are then obsolete, i.e. there is no need 
to setup <code>git-secret</code> and <code>gpg</code> locally.</p>

<p>But the approach also comes with some downsides, because
- the <strong>secret key and all public keys have to be imported every time the container is started</strong>
- <strong>each dev needs to put his private <code>gpg</code> key "in the codebase"</strong> (ignored by <code>.gitignore</code>) so it 
  can be shared with docker and imported by <code>gpg</code> (in docker). The alternative would be using 
  a single secret key that is   shared within the team - which feels very wrong :P</p>

<p>To make this a little more convenient, <strong>we put the public gpg keys of every dev in the 
repository</strong> under <code>.dev/gpg-keys/</code> and <strong>the private key has to be named <code>secret.gpg</code> and put 
in the root of the codebase</strong>.</p>

<p>In this setup, <code>secret.gpg</code> must also be added to the<code>.gitignore</code> file.</p>

<pre><code class="language-gitignore"># File: .gitignore
#...
vendor/
secret.gpg
</code></pre>

<p>The import can now be simplified with <code>make</code> targets:</p>

<pre><code class="language-makefile"># gpg

DEFAULT_SECRET_GPG_KEY?=secret.gpg
DEFAULT_PUBLIC_GPG_KEYS?=.dev/gpg-keys/*

.PHONY: gpg
gpg: ## Run gpg commands. Specify the command e.g. via ARGS="--list-keys"
    $(EXECUTE_IN_APPLICATION_CONTAINER) gpg $(ARGS)

.PHONY: gpg-import
gpg-import: ## Import a gpg key file e.g. via GPG_KEY_FILES="/path/to/file /path/to/file2"
    @$(if $(GPG_KEY_FILES),,$(error GPG_KEY_FILES is undefined))
    "$(MAKE)" -s gpg ARGS="--import --batch --yes --pinentry-mode loopback $(GPG_KEY_FILES)"

.PHONY: gpg-import-default-secret-key
gpg-import-default-secret-key: ## Import the default secret key
    "$(MAKE)" -s gpg-import GPG_KEY_FILES="$(DEFAULT_SECRET_GPG_KEY)"

.PHONY: gpg-import-default-public-keys
gpg-import-default-public-keys: ## Import the default public keys
    "$(MAKE)" -s gpg-import GPG_KEY_FILES="$(DEFAULT_PUBLIC_GPG_KEYS)" 

.PHONY: gpg-init
gpg-init: gpg-import-default-secret-key gpg-import-default-public-keys ## Initialize gpg in the container, i.e. import all public and private keys
</code></pre>

<p>"Everything" can now be handled via</p>

<pre><code class="language-bash">make gpg-init
</code></pre>

<p>that needs to be run one single time after a container has been started.</p>

<p><!-- generated -->
<a id='scenarios'> </a>
<!-- /generated --></p>

<h3>Scenarios</h3>

<p>The scenarios assume the following preconditions:</p>

<ul>
<li>You have checked out branch <a href="https://github.com/paslandau/docker-php-tutorial/tree/part-6-git-secret-encrypt-repository-docker">part-6-git-secret-encrypt-repository-docker</a>
<code>bash
git checkout part-6-git-secret-encrypt-repository-docker</code>
and no running docker containers 
<code>bash
make docker-down</code></li>
<li>You have deleted the existing <code>git-secret</code> folder, the keys in <code>.dev/gpg-keys</code>, the 
<code>secret.gpg</code> key and the <code>passwords.*</code> files 
<code>bash
rm -rf .gitsecret/ .dev/gpg-keys/* secret.gpg passwords.*</code></li>
</ul>

<p><!-- generated -->
<a id='initial-setup-of-gpg-keys'> </a>
<!-- /generated --></p>

<h4>Initial setup of <code>gpg</code> keys</h4>

<p>Unfortunately, I didn't find a way to create and export <code>gpg</code> keys through <code>make</code> and <code>docker</code>. You
need to either run the commands interactively OR pass a string with newlines to it. Both things are
horribly complicated with <code>make</code> and docker. Thus, you need to log into the <code>application</code>
container and run the commands in there directly. Not great - but this needs to be done only
once when a new developer is onboarded anyways.</p>

<p>FYI: I usually log into containers via
<a href="/blog/structuring-the-docker-setup-for-php-projects/#easy-container-access-via-din-bashrc-helper">Easy container access via din .bashrc helper</a>.</p>

<p>The secret key is exported to <code>secret.gpg</code> and the public key to <code>.dev/gpg-keys/alice-public.gpg</code>.</p>

<pre><code class="language-bash"># start the docker setup
make docker-up

# log into the container ('winpty' is only required on Windows)
winpty docker exec -ti dofroscra_local-application-1 bash

# export key pair
name="Alice Doe"
email="alice@example.com"
gpg --batch --gen-key &lt;&lt;EOF
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: $name
Name-Email: $email
Expire-Date: 0
%no-protection
EOF

# export the private key
gpg --output secret.gpg --armor --export-secret-key $email

# export the public key
gpg --armor --export $email &gt; .dev/gpg-keys/alice-public.gpg
</code></pre>

<pre><code class="language-text">$ make docker-up
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml up -d
Container dofroscra_local-application-1  Created
...
Container dofroscra_local-application-1  Started
$ docker ps
CONTAINER ID   IMAGE                                COMMAND                  CREATED          STATUS          PORTS                NAMES
...
95f740607586   dofroscra/application-local:latest   "/usr/sbin/sshd -D"      21 minutes ago   Up 21 minutes   0.0.0.0:2222-&gt;22/tcp dofroscra_local-application-1

$ winpty docker exec -ti dofroscra_local-application-1 bash
root:/var/www/app# name="Alice Doe"
root:/var/www/app# email="alice@example.com"
gpg --batch --gen-key &lt;&lt;EOF
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: $name
Name-Email: $email
Expire-Date: 0
%no-protection
EOF
root:/var/www/app# gpg --batch --gen-key &lt;&lt;EOF
&gt; Key-Type: 1
&gt; Key-Length: 2048
&gt; Subkey-Type: 1
&gt; Subkey-Length: 2048
&gt; Name-Real: $name
&gt; Name-Email: $email
&gt; Expire-Date: 0
&gt; %no-protection
&gt; EOF
gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key BBBE654440E720C1 marked as ultimately trusted
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/225C736E0E70AC222C072B70BBBE654440E720C1.rev'

root:/var/www/app# gpg --output secret.gpg --armor --export-secret-key $email
root:/var/www/app# head secret.gpg
-----BEGIN PGP PRIVATE KEY BLOCK-----

lQOYBGJD+bwBCADBGKySV5PINc5MmQB3PNvCG7Oa1VMBO8XJdivIOSw7ykv55PRP
3g3R+ERd1Ss5gd5KAxLc1tt6PHGSPTypUJjCng2plwD8Jy5A/cC6o2x8yubOslLa
x1EC9fpcxUYUNXZavtEr+ylOaTaRz6qwSabsAgkg2NZ0ey/QKmFOZvhL8NlK9lTI
GgZPTiqPCsr7hiNg0WRbT5h8nTmfpl/DdTgwfPsDn5Hn0TEMa79WsrPnnq16jsq0
Uusuw3tOmdSdYnT8j7m1cpgcSj0hRF1eh4GVE0o62GqeLTWW9mfpcuv7n6mWaCB8
DCH6H238gwUriq/aboegcuBktlvSY21q/MIXABEBAAEAB/wK/M2buX+vavRgDRgR
hjUrsJTXO3VGLYcIetYXRhLmHLxBriKtcBa8OxLKKL5AFEuNourOBdcmTPiEwuxH
5s39IQOTrK6B1UmUqXvFLasXghorv8o8KGRL4ABM4Bgn6o+KBAVLVIwvVIhQ4rlf

root:/var/www/app# gpg --armor --export $email &gt; .dev/gpg-keys/alice-public.gpg
root:/var/www/app# head .dev/gpg-keys/alice-public.gpg
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBGJD+bwBCADBGKySV5PINc5MmQB3PNvCG7Oa1VMBO8XJdivIOSw7ykv55PRP
3g3R+ERd1Ss5gd5KAxLc1tt6PHGSPTypUJjCng2plwD8Jy5A/cC6o2x8yubOslLa
x1EC9fpcxUYUNXZavtEr+ylOaTaRz6qwSabsAgkg2NZ0ey/QKmFOZvhL8NlK9lTI
GgZPTiqPCsr7hiNg0WRbT5h8nTmfpl/DdTgwfPsDn5Hn0TEMa79WsrPnnq16jsq0
Uusuw3tOmdSdYnT8j7m1cpgcSj0hRF1eh4GVE0o62GqeLTWW9mfpcuv7n6mWaCB8
DCH6H238gwUriq/aboegcuBktlvSY21q/MIXABEBAAG0HUFsaWNlIERvZSA8YWxp
Y2VAZXhhbXBsZS5jb20+iQFOBBMBCgA4FiEEIlxzbg5wrCIsBytwu75lREDnIMEF
AmJD+bwCGy8FCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQu75lREDnIMEN4Af+
</code></pre>

<p>That's it. We now have a new secret and private key for <code>alice@example.com</code> and have exported it to 
<code>secret.gpg</code> resp. <code>.dev/gpg-keys/alice-public.gpg</code> (and thus shared it with the host system). 
The remaining commands can now be run outside of the <code>application</code> container directly on the 
host system.</p>

<p><!-- generated -->
<a id='initial-setup-of-git-secret'> </a>
<!-- /generated --></p>

<h4>Initial setup of <code>git-secret</code></h4>

<p>Let's say we want to introduce <code>git-secret</code> "from scratch" to a new codebase. Then you would run 
the following commands:</p>

<p><strong>Initialize <code>git-secret</code></strong></p>

<pre><code class="language-bash">make secret-init
</code></pre>

<pre><code class="language-text">$ make secret-init
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="init";
git-secret: init created: '/var/www/app/.gitsecret/'
</code></pre>

<p><strong>Apply the <code>gpg</code> fix for shared directories</strong></p>

<p>See <a href="#the-git-secret-directory-and-the-gpg-agent-socket">The <code>git-secret</code> directory and the <code>gpg-agent</code> socket</a>.</p>

<pre><code class="language-bash">$ make secret-init-gpg-socket-config
</code></pre>

<pre><code class="language-text">$ make secret-init-gpg-socket-config
echo "%Assuan%" &gt; .gitsecret/keys/S.gpg-agent
echo "socket=/tmp/S.gpg-agent" &gt;&gt; .gitsecret/keys/S.gpg-agent
echo "%Assuan%" &gt; .gitsecret/keys/S.gpg-agent.ssh
echo "socket=/tmp/S.gpg-agent.ssh" &gt;&gt; .gitsecret/keys/S.gpg-agent.ssh
echo "extra-socket /tmp/S.gpg-agent.extra" &gt; .gitsecret/keys/gpg-agent.conf
echo "browser-socket /tmp/S.gpg-agent.browser" &gt;&gt; .gitsecret/keys/gpg-agent.conf
</code></pre>

<p><!-- generated -->
<a id='initialize-gpg-after-container-startup'> </a>
<!-- /generated --></p>

<h4>Initialize <code>gpg</code> after container startup</h4>

<p>After restarting the containers, we need to initialize <code>gpg</code>, i.e. import all public keys from 
<code>.dev/gpg-keys/*</code> and the private key from <code>secret.gpg</code>. Otherwise we will not be able to en- 
and decrypt the files.</p>

<pre><code class="language-bash">make gpg-init
</code></pre>

<pre><code class="language-text">$ make gpg-init
"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key BBBE654440E720C1: public key "Alice Doe &lt;alice@example.com&gt;" imported
gpg: key BBBE654440E720C1: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1
"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES=".dev/gpg-keys/*"
gpg: key BBBE654440E720C1: "Alice Doe &lt;alice@example.com&gt;" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1
</code></pre>

<p><!-- generated -->
<a id='adding-new-team-members'> </a>
<!-- /generated --></p>

<h4>Adding (new) team members</h4>

<p>Let's start by adding our own user to <code>git-secret</code></p>

<pre><code class="language-bash">make secret-add-user EMAIL="alice@example.com"
</code></pre>

<pre><code class="language-text">$ make secret-add-user EMAIL="alice@example.com"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="tell alice@example.com"
git-secret: done. alice@example.com added as user(s) who know the secret.
</code></pre>

<p>And verify that it worked via</p>

<pre><code class="language-bash">make secret-show-users
</code></pre>

<pre><code class="language-text">$ make secret-show-users
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="whoknows"
alice@example.com
</code></pre>

<p><!-- generated -->
<a id='adding-and-encrypting-files'> </a>
<!-- /generated --></p>

<h4>Adding and encrypting files</h4>

<p>Let's add a new encrypted file <code>secret_password.txt</code>.</p>

<p>Create the file</p>

<pre><code class="language-bash">echo "my_new_secret_password" &gt; secret_password.txt
</code></pre>

<p>Add it to <code>.gitignore</code></p>

<pre><code class="language-bash">echo "secret_password.txt" &gt;&gt; .gitignore
</code></pre>

<p>Add it to <code>git-secret</code></p>

<pre><code class="language-bash">make secret-add FILE="secret_password.txt"
</code></pre>

<pre><code class="language-text">$ make secret-add FILE="secret_password.txt"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="add secret_password.txt"
git-secret: 1 item(s) added.
</code></pre>

<p>Encrypt all files</p>

<pre><code class="language-bash">make secret-encrypt
</code></pre>

<pre><code class="language-text">$ make secret-encrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="hide"
git-secret: done. 1 of 1 files are hidden.

$ ls secret_password.txt.secret
secret_password.txt.secret
</code></pre>

<p><!-- generated -->
<a id='decrypt-files'> </a>
<!-- /generated --></p>

<h4>Decrypt files</h4>

<p>Let's first remove the "plain" <code>secret_password.txt</code> file</p>

<pre><code class="language-bash">rm secret_password.txt
</code></pre>

<pre><code class="language-text">$ rm secret_password.txt

$ ls secret_password.txt
ls: cannot access 'secret_password.txt': No such file or directory
</code></pre>

<p>and then decrypt the encrypted one.</p>

<pre><code class="language-bash">make secret-decrypt
</code></pre>

<pre><code class="language-text">$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: done. 1 of 1 files are revealed.

$ cat secret_password.txt
my_new_secret_password
</code></pre>

<p><strong>Caution:</strong> If the secret <code>gpg</code> key is password protected (e.g. <code>123456</code>), run</p>

<pre><code class="language-bash">make secret-decrypt-with-password GPG_PASSWORD=123456
</code></pre>

<p>You could also add the <code>GPG_PASSWORD</code> variable to the 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env"><code>.make/.env</code></a>
file as a local default value so that you wouldn't have to specify the value every time and 
could then simply run</p>

<pre><code class="language-bash">make secret-decrypt-with-password
</code></pre>

<p>without passing <code>GPG_PASSWORD</code></p>

<p><!-- generated -->
<a id='removing-files'> </a>
<!-- /generated --></p>

<h4>Removing files</h4>

<p>Remove the <code>secret_password.txt</code> file we added previously:</p>

<pre><code class="language-bash">make secret-remove FILE="secret_password.txt"
</code></pre>

<pre><code class="language-text">$ make secret-remove FILE="secret_password.txt"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="remove secret_password.txt"
git-secret: removed from index.
git-secret: ensure that files: [secret_password.txt] are now not ignored.
</code></pre>

<p><strong>Caution: this will neither remove the <code>secret_password.txt</code> file nor 
the <code>secret_password.txt.secret</code> file automatically"</strong></p>

<pre><code class="language-text">$ ls -l | grep secret_password.txt
-rw-r--r-- 1 Pascal 197121     19 Mar 31 14:03 secret_password.txt
-rw-r--r-- 1 Pascal 197121    358 Mar 31 14:02 secret_password.txt.secret
</code></pre>

<p>But even though the encrypted <code>secret_password.txt.secret</code> file still exists, it will not be 
decrypted:</p>

<pre><code class="language-text">$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: done. 0 of 0 files are revealed.
</code></pre>

<p><!-- generated -->
<a id='removing-team-members'> </a>
<!-- /generated --></p>

<h4>Removing team members</h4>

<p>Removing a team member can be done via</p>

<pre><code class="language-bash">make secret-remove-user EMAIL="alice@example.com"
</code></pre>

<pre><code class="language-text">$ make secret-remove-user EMAIL="alice@example.com"
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="killperson alice@example.com"
git-secret: removed keys.
git-secret: now [alice@example.com] do not have an access to the repository.
git-secret: make sure to hide the existing secrets again.
</code></pre>

<p>If there are any users left, we must make sure to re-encrypt the secrets via</p>

<pre><code class="language-bash">make secret-encrypt
</code></pre>

<p>Otherwise (if no more users are left) <code>git-secret</code> would simply error out</p>

<pre><code class="language-text">$ make secret-decrypt
"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f"
git-secret: abort: no public keys for users found. run 'git secret tell email@address'.
make[1]: *** [.make/01-00-application-setup.mk:57: git-secret] Error 1
make: *** [.make/01-00-application-setup.mk:69: secret-decrypt] Error 2
</code></pre>

<p><strong>Caution</strong>: Please keep in mind to 
<a href="#reminder-rotate-the-encrypted-secrets">rotate the secrets themselves as well</a>!</p>

<p><!-- generated -->
<a id='pros-and-cons'> </a>
<!-- /generated --></p>

<h2>Pros and cons</h2>

<p><!-- generated -->
<a id='pro'> </a>
<!-- /generated --></p>

<h3>Pro</h3>

<ul>
<li>very low barrier to entry:

<ul>
<li>no third party service required</li>
<li>easy to integrate in existing codebases, because the secrets are located directly in 
the codebase</li>
<li>everything can be handled through docker (no additional local software necessary)</li>
</ul></li>
<li>once set up, it is very easy/convenient to use and can be integrated in a team workflow</li>
<li>changes to secrets can be reviewed before they are merged

<ul>
<li>this leads to less fuck-ups on deployments</li>
</ul></li>
<li>"everything" is in the repository, which brings a lot of familiar benefits like

<ul>
<li>version control</li>
<li>a single <code>git pull</code> is the only thing you need to get everything (=> good dev experience)</li>
</ul></li>
</ul>

<p><!-- generated -->
<a id='cons'> </a>
<!-- /generated --></p>

<h3>Cons</h3>

<ul>
<li>some overhead during onboarding and offboarding</li>
<li>the secret key must be put in the root of the repository at <code>./secret.gpg</code></li>
<li>no fine grained permissions for different secrets, e.g. the mysql password on production and 
staging can not be treated differently

<ul>
<li>if somebody can decrypt secrets, ALL of them are exposed</li>
</ul></li>
<li>if the a secret key ever gets leaked, all secrets are compromised

<ul>
<li>=> can be mitigated (to a degree) by using a passphrase on the secret key</li>
<li>=> this is kinda true for any other system that stores secrets as well BUT third parties 
could probably implement additional measures like multi factor authentication</li>
</ul></li>
<li>secrets are versioned alongside the users that have access, i.e. even if a user is removed at 
some point, he can still decrypt a previous version of the encrypted secrets

<ul>
<li>=> must be mitigated by
<a href="#reminder-rotate-the-encrypted-secrets">rotating the secrets themselves as well</a></li>
</ul></li>
</ul>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You are now able to encrypt and decrypt secret files so that they can be stored 
directly in the git repository.</p>

<p>In the next part of this tutorial, we will 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/">set up a CI pipeline for dockerized PHP Apps on Github and Gitlab</a>
that decrypts all necessary secrets and then runs our tests and qa tools.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Mon, 25 Apr 2022 06:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/git-secret-encrypt-repository-docker/</guid>
            </item>
                    <item>
                <title>Set up PHP QA tools and control them via make [Tutorial Part 5]</title>
                <description><![CDATA[<p>In the fifth part of this tutorial series on developing PHP on Docker we will <strong>setup some PHP code
quality tools</strong> and provide a convenient way to control them via GNU make.</p>

<p><a href="/img/php-qa-tools-make-docker/run-qa-tools.gif"><img src="/img/php-qa-tools-make-docker/run-qa-tools.gif" alt="Run QA tools" /></a></p>

<p><small>
FYI: Originally I wanted
this tutorial to be a part of 
<a href="/blog/ci-pipeline-docker-php-gitlab-github/">Create a CI pipeline for dockerized PHP Apps</a>
because QA checks are imho vital part of a CI setup - but it kinda grew "too big" and took a way 
too much space from, well, actually setting up the CI pipelines :)
</small></p>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/ocM4ktjqwIg" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch with the final result of this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-5-php-qa-tools-make-docker">part-5-php-qa-tools-make-docker</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/run-laravel-9-docker-in-2022/">Run Laravel 9 on Docker in 2022</a>
and the following one is
<a href="/blog/git-secret-encrypt-repository-docker/">Use <code>git-secret</code> to encrypt secrets in the repository</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a>
to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a>

<ul>
<li><a href="#the-qa-tools">The QA tools</a>

<ul>
<li><a href="#phpcs-and-phpcbf">phpcs and phpcbf</a></li>
<li><a href="#phpstan">phpstan</a></li>
<li><a href="#php-parallel-lint">php-parallel-lint</a></li>
<li><a href="#composer-require-checker">composer-require-checker</a></li>
<li><a href="#additional-tools-out-of-scope">Additional tools (out of scope)</a></li>
</ul></li>
<li><a href="#qa-make-targets">QA make targets</a>

<ul>
<li><a href="#the-qa-target">The <code>qa</code> target</a></li>
<li><a href="#the-execute-function">The <code>execute</code> "function"</a></li>
<li><a href="#parallel-execution-and-a-helper-target">Parallel execution and a helper target</a></li>
<li><a href="#sprinkle-some-color-on-top">Sprinkle some color on top</a></li>
</ul></li>
<li><a href="#further-updates-in-the-codebase">Further updates in the codebase</a></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>Code quality tools ensure a <strong>baseline of code quality</strong> by automatically checking certain rules,
e.g. <strong>code style definitions</strong>, proper <strong>usage of types</strong>, proper <strong>declaration of dependencies</strong>,
etc. When run regularly they are a great way to enforce better code and are thus a
<strong>perfect fit for a CI pipeline</strong>. For this tutorial, I'm going to setup the following tools:</p>

<ul>
<li><a href="#phpcs-and-phpcbf">Style Checker: phpcs</a></li>
<li><a href="#phpstan">Static Analyzer: phpstan</a></li>
<li><a href="#php-parallel-lint">Code Linter: php-parallel-lint</a></li>
<li><a href="#composer-require-checker">Dependency Checker: composer-require-checker</a></li>
</ul>

<p>and provide convenient access through a <a href="#the-qa-target"><code>qa</code> make target</a>. The end result will look
like this:</p>

<p><a href="/img/php-qa-tools-make-docker/qa-tool-output.PNG"><img src="/img/php-qa-tools-make-docker/qa-tool-output.PNG" alt="QA tool output" /></a></p>

<p>FYI: When we started out with using code quality tools in general, we have used 
<a href="https://github.com/phpro/grumphp">GrumPHP</a> - and I would still recommend it. We have only 
transitioned away from it because <code>make</code> gives us a little more flexibility and control.</p>

<p>You can find the "final" makefile
at <a href="https://github.com/paslandau/docker-php-tutorial/blob/part-5-php-qa-tools-make-docker/.make/01-02-application-qa.mk"><code>.make/01-02-application-qa.mk</code></a>.</p>

<p><strong>CAUTION</strong>: The <code>Makefile</code> is build on top of the setup that I introduced in
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/">Docker from scratch for PHP 8.1 Applications in 2022</a>, 
so please refer to that tutorial if anything is not clear.</p>

<pre><code class="language-Makefile">##@ [Application: QA]

# variables
CORES?=$(shell (nproc  || sysctl -n hw.ncpu) 2&gt; /dev/null)

# constants
 ## files
ALL_FILES=./
APP_FILES=app/
TEST_FILES=tests/

 ## bash colors
RED:=\033[0;31m
GREEN:=\033[0;32m
YELLOW:=\033[0;33m
NO_COLOR:=\033[0m

# Tool CLI config
PHPUNIT_CMD=php vendor/bin/phpunit
PHPUNIT_ARGS= -c phpunit.xml
PHPUNIT_FILES=
PHPSTAN_CMD=php vendor/bin/phpstan analyse
PHPSTAN_ARGS=--level=9
PHPSTAN_FILES=$(APP_FILES) $(TEST_FILES)
PHPCS_CMD=php vendor/bin/phpcs
PHPCS_ARGS=--parallel=$(CORES) --standard=psr12
PHPCS_FILES=$(APP_FILES)
PHPCBF_CMD=php vendor/bin/phpcbf
PHPCBF_ARGS=$(PHPCS_ARGS)
PHPCBF_FILES=$(PHPCS_FILES)
PARALLEL_LINT_CMD=php vendor/bin/parallel-lint
PARALLEL_LINT_ARGS=-j 4 --exclude vendor/ --exclude .docker --exclude .git
PARALLEL_LINT_FILES=$(ALL_FILES)
COMPOSER_REQUIRE_CHECKER_CMD=php vendor/bin/composer-require-checker
COMPOSER_REQUIRE_CHECKER_ARGS=--ignore-parse-errors

# call with NO_PROGRESS=true to hide tool progress (makes sense when invoking multiple tools together)
NO_PROGRESS?=false
ifeq ($(NO_PROGRESS),true)
    PHPSTAN_ARGS+= --no-progress
    PARALLEL_LINT_ARGS+= --no-progress
else
    PHPCS_ARGS+= -p
    PHPCBF_ARGS+= -p
endif

# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
define execute
    if [ "$(NO_PROGRESS)" = "false" ]; then \
        eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"; \
    else \
        START=$$(date +%s); \
        printf "%-35s" "$@"; \
        if OUTPUT=$$(eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)" 2&gt;&amp;1); then \
            printf " $(GREEN)%-6s$(NO_COLOR)" "done"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $(YELLOW)$${RUNTIME}s$(NO_COLOR)\n"; \
        else \
            printf " $(RED)%-6s$(NO_COLOR)" "fail"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $(YELLOW)$${RUNTIME}s$(NO_COLOR)\n"; \
            echo "$$OUTPUT"; \
            printf "\n"; \
            exit 1; \
        fi; \
    fi
endef

.PHONY: test
test: ## Run all tests
    @$(EXECUTE_IN_APPLICATION_CONTAINER) $(PHPUNIT_CMD) $(PHPUNIT_ARGS) $(ARGS)

.PHONY: phplint
phplint: ## Run phplint on all files
    @$(call execute,$(PARALLEL_LINT_CMD),$(PARALLEL_LINT_ARGS),$(PARALLEL_LINT_FILES), $(ARGS))

.PHONY: phpcs
phpcs: ## Run style check on all application files
    @$(call execute,$(PHPCS_CMD),$(PHPCS_ARGS),$(PHPCS_FILES), $(ARGS))

.PHONY: phpcbf
phpcbf: ## Run style fixer on all application files
    @$(call execute,$(PHPCBF_CMD),$(PHPCBF_ARGS),$(PHPCBF_FILES), $(ARGS))

.PHONY: phpstan
phpstan:  ## Run static analyzer on all application and test files 
    @$(call execute,$(PHPSTAN_CMD),$(PHPSTAN_ARGS),$(PHPSTAN_FILES), $(ARGS))

.PHONY: composer-require-checker
composer-require-checker: ## Run dependency checker
    @$(call execute,$(COMPOSER_REQUIRE_CHECKER_CMD),$(COMPOSER_REQUIRE_CHECKER_ARGS),"", $(ARGS))

.PHONY: qa
qa: ## Run code quality tools on all files
    @"$(MAKE)" -j $(CORES) -k --no-print-directory --output-sync=target qa-exec NO_PROGRESS=true

.PHONY: qa-exec
qa-exec: phpstan \
    phplint \
    composer-require-checker \
    phpcs
</code></pre>

<p><!-- generated -->
<a id='the-qa-tools'> </a>
<!-- /generated --></p>

<h2>The QA tools</h2>

<p><!-- generated -->
<a id='phpcs-and-phpcbf'> </a>
<!-- /generated --></p>

<h3>phpcs and phpcbf</h3>

<p><code>phpcs</code> is the CLI tool of the style checker
<a href="https://github.com/squizlabs/PHP_CodeSniffer">squizlabs/PHP_CodeSniffer</a>. It also comes with
<code>phpcbf</code> - a tool to automatically fix style errors.</p>

<p>Installation via composer:</p>

<pre><code class="language-shell">make composer ARGS="require --dev squizlabs/php_codesniffer"
</code></pre>

<p>For now we will simply use the pre-configured ruleset for
<a href="https://www.php-fig.org/psr/psr-12/">PSR-12: Extended Coding Style</a>. When run in the <code>application</code>
container for the first time on the <code>app</code> directory via</p>

<pre><code class="language-text">vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app
</code></pre>

<p>i.e.</p>

<pre><code class="language-text">--standard=PSR12 =&gt; use the PSR12 ruleset
--parallel=4     =&gt; run with 4 parallel processes
-p               =&gt; show the progress
</code></pre>

<p>we get the following result:</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app

FILE: /var/www/app/app/Console/Kernel.php
----------------------------------------------------------------------
FOUND 2 ERRORS AFFECTING 1 LINE
----------------------------------------------------------------------
 28 | ERROR | [x] Expected at least 1 space before "."; 0 found
 28 | ERROR | [x] Expected at least 1 space after "."; 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 2 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------


FILE: /var/www/app/app/Http/Controllers/HomeController.php
----------------------------------------------------------------------
FOUND 4 ERRORS AFFECTING 2 LINES
----------------------------------------------------------------------
 37 | ERROR | [x] Expected at least 1 space before "."; 0 found
 37 | ERROR | [x] Expected at least 1 space after "."; 0 found
 45 | ERROR | [x] Expected at least 1 space before "."; 0 found
 45 | ERROR | [x] Expected at least 1 space after "."; 0 found
----------------------------------------------------------------------
PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------


FILE: /var/www/app/app/Jobs/InsertInDbJob.php
-------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
-------------------------------------------------------------------------------
 13 | ERROR | [x] Each imported trait must have its own "use" import statement
-------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-------------------------------------------------------------------------------


FILE: /var/www/app/app/Models/User.php
-------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
-------------------------------------------------------------------------------
 13 | ERROR | [x] Each imported trait must have its own "use" import statement
-------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-------------------------------------------------------------------------------
</code></pre>

<p>All errors can be fixed automatically with <code>phpcbf</code>:</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/phpcbf --standard=PSR12 --parallel=4 -p app

PHPCBF RESULT SUMMARY
-------------------------------------------------------------------------
FILE                                                     FIXED  REMAINING
-------------------------------------------------------------------------
/var/www/app/app/Console/Kernel.php                      2      0
/var/www/app/app/Http/Controllers/HomeController.php     4      0
/var/www/app/app/Jobs/InsertInDbJob.php                  1      0
/var/www/app/app/Models/User.php                         1      0
-------------------------------------------------------------------------
A TOTAL OF 8 ERRORS WERE FIXED IN 4 FILES
-------------------------------------------------------------------------

Time: 411ms; Memory: 8MB
</code></pre>

<p>and a follow-up run of <code>phpcs</code> doesn't show any more errors:</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/phpcs --standard=PSR12 --parallel=4 -p app
.................... 20 / 20 (100%)


Time: 289ms; Memory: 8MB
</code></pre>

<p><!-- generated -->
<a id='phpstan'> </a>
<!-- /generated --></p>

<h3>phpstan</h3>

<p><code>phpstan</code> is the CLI tool of the static code analyzer
<a href="https://github.com/phpstan/phpstan">phpstan/phpstan</a> (see also the
<a href="https://phpstan.org/user-guide/getting-started">full PHPStan documentation</a>). It provides some
default "levels" of increasing strictness to report potential bugs based on the AST of the analyzed
PHP code.</p>

<p>Installation via composer:</p>

<pre><code class="language-shell">make composer ARGS="require --dev phpstan/phpstan"
</code></pre>

<p>Since this is a "fresh" codebase with very little code let's go for the
<a href="https://phpstan.org/user-guide/rule-levels">highest level 9</a> (as of 2022-04-24) and run it in the
<code>application</code> container on the <code>app</code> and <code>tests</code> directories via:</p>

<pre><code class="language-text">vendor/bin/phpstan analyse app tests --level=9

--level=9        =&gt; use level 9
</code></pre>

<pre><code class="language-text">root:/var/www/app# vendor/bin/phpstan analyse app tests --level=9
 25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------------------------------------------------
  Line   app/Commands/SetupDbCommand.php
 ------ ------------------------------------------------------------------------------------------------------------------
  22     Method App\Commands\SetupDbCommand::getOptions() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  34     Method App\Commands\SetupDbCommand::handle() has no return type specified.
 ------ ------------------------------------------------------------------------------------------------------------------

 ------ -------------------------------------------------------------------------------------------------------
  Line   app/Http/Controllers/HomeController.php
 ------ -------------------------------------------------------------------------------------------------------
  22     Parameter #1 $jobId of class App\Jobs\InsertInDbJob constructor expects string, mixed given.
  25     Part $jobId (mixed) of encapsed string cannot be cast to string.
  35     Call to an undefined method Illuminate\Redis\Connections\Connection::lRange().
  62     Call to an undefined method Illuminate\Contracts\View\Factory|Illuminate\Contracts\View\View::with().
 ------ -------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------
  Line   app/Http/Middleware/Authenticate.php
 ------ ------------------------------------------------------------------------------------------------------------------
  17     Method App\Http\Middleware\Authenticate::redirectTo() should return string|null but return statement is missing.
 ------ ------------------------------------------------------------------------------------------------------------------

 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   app/Http/Middleware/RedirectIfAuthenticated.php
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  26     Method App\Http\Middleware\RedirectIfAuthenticated::handle() should return Illuminate\Http\RedirectResponse|Illuminate\Http\Response but returns Illuminate\Http\RedirectResponse|Illuminate\Routing\Redirector.
 ------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ -----------------------------------------------------------------------
  Line   app/Jobs/InsertInDbJob.php
 ------ -----------------------------------------------------------------------
  22     Method App\Jobs\InsertInDbJob::handle() has no return type specified.
 ------ -----------------------------------------------------------------------

 ------ -------------------------------------------------
  Line   app/Providers/RouteServiceProvider.php
 ------ -------------------------------------------------
  36     PHPDoc tag @var above a method has no effect.
  36     PHPDoc tag @var does not specify variable name.
  60     Cannot access property $id on mixed.
 ------ -------------------------------------------------

 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------
  Line   tests/Feature/App/Http/Controllers/HomeControllerTest.php
 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------
  24     Method Tests\Feature\App\Http\Controllers\HomeControllerTest::test___invoke() has parameter $params with no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
  38     Method Tests\Feature\App\Http\Controllers\HomeControllerTest::__invoke_dataProvider() return type has no value type specified in iterable type array.
         � See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
 ------ ----------------------------------------------------------------------------------------------------------------------------------------------------------

 ------ ---------------------------------------------------------------------------------------------------------------------
  Line   tests/TestCase.php
 ------ ---------------------------------------------------------------------------------------------------------------------
  68     Cannot access offset 'database' on mixed.
  71     Parameter #1 $config of method Illuminate\Database\Connectors\MySqlConnector::connect() expects array, mixed given.
 ------ ---------------------------------------------------------------------------------------------------------------------


 [ERROR] Found 16 errors
</code></pre>

<p>After fixing (or ignoring :P) all errors, we now get</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/phpstan analyse app tests --level=9
25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

[OK] No errors
</code></pre>

<p><!-- generated -->
<a id='php-parallel-lint'> </a>
<!-- /generated --></p>

<h3>php-parallel-lint</h3>

<p><code>php-parallel-lint</code> is the CLI tool of the PHP code linter
<a href="https://github.com/php-parallel-lint/PHP-Parallel-Lint">php-parallel-lint/PHP-Parallel-Lint</a>. It
ensures that all PHP files are syntactically correct.</p>

<p>Installation via composer:</p>

<pre><code class="language-shell">make composer ARGS="require --dev php-parallel-lint/php-parallel-lint"
</code></pre>

<p>"Parallel" is already in the name, so we run it on the full codebase <code>./</code> with 4 parallel processes 
and exclude the <code>.git</code> and <code>vendor</code> directories to speed up the execution via</p>

<pre><code class="language-text">vendor/bin/parallel-lint -j 4 --exclude .git --exclude vendor ./
</code></pre>

<p>i.e.</p>

<pre><code class="language-text">-j 4                              =&gt; use 4 parallel processes
--exclude .git --exclude vendor   =&gt; ignore the .git/ and vendor/ directories
</code></pre>

<p>we get</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/parallel-lint -j 4 --exclude .git --exclude vendor ./
PHP 8.1.1 | 4 parallel jobs
............................................................ 60/61 (98 %)
.                                                            61/61 (100 %)


Checked 61 files in 0.2 seconds
No syntax error found
</code></pre>

<p>No further TODOs here.</p>

<p><!-- generated -->
<a id='composer-require-checker'> </a>
<!-- /generated --></p>

<h3>composer-require-checker</h3>

<p><code>composer-require-checker</code> is the CLI tool of the dependency checker
<a href="https://github.com/maglnet/ComposerRequireChecker">maglnet/ComposerRequireChecker</a>. The tool
ensures that the <code>composer.json</code> file contains all dependencies that are used in the codebase.</p>

<p>Installation via composer:</p>

<pre><code class="language-shell">make composer ARGS="require --dev maglnet/composer-require-checker"
</code></pre>

<p>Run it via</p>

<pre><code class="language-text">vendor/bin/composer-require-checker check
</code></pre>

<pre><code class="language-text">root:/var/www/app# vendor/bin/composer-require-checker check
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
The following 1 unknown symbols were found:
+---------------------------------------------+--------------------+
| Unknown Symbol                              | Guessed Dependency |
+---------------------------------------------+--------------------+
| Symfony\Component\Console\Input\InputOption |                    |
+---------------------------------------------+--------------------+
</code></pre>

<p>What's going on here?</p>

<p>We use <code>Symfony\Component\Console\Input\InputOption</code> in our <code>\App\Commands\SetupDbCommand</code> and the
code doesn't "fail" because <code>InputOption</code> is defined in the<code>symfony/console</code> package that is a
<strong>transitive dependency</strong> of <code>laravel/framework</code>, see the
<a href="https://github.com/laravel/framework/blob/5b113dad7d2c88e15b65d987ca63f03b2be43e6a/composer.json#L34"><code>laravel/framework composer.json</code></a>
file.</p>

<p>I.e. the <code>symfony/console</code> package <strong>does actually exist</strong> in our <code>vendor</code> directory - but
since we also use it <em>as a first-party-dependency directly in our code</em>, we must declare the
dependency explicitly. Otherwise, Laravel might at some point decide to drop <code>symfony/console</code>
and we would be left with broken code.</p>

<p>To fix this, I run</p>

<pre><code class="language-shell">make composer ARGS="require symfony/console"
</code></pre>

<p>which will update the <code>composer.json</code> file and add the dependency. Running
<code>composer-require-checker</code> again will now yield no further errors.</p>

<pre><code class="language-text">root:/var/www/app# vendor/bin/composer-require-checker check
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
There were no unknown symbols found.
</code></pre>

<p><!-- generated -->
<a id='additional-tools-out-of-scope'> </a>
<!-- /generated --></p>

<h3>Additional tools (out of scope)</h3>

<p>In general, I'm a huge fan of code quality tools and we use them quite extensively. At some 
point I'll probably dedicate a whole article to go over them in detail - but for now I'm just 
gonna leave a list for inspiration:</p>

<ul>
<li><a href="https://packagist.org/packages/brianium/paratest">brianium/paratest</a>

<ul>
<li>Running PhpUnit tests in parallel</li>
</ul></li>
<li><a href="https://packagist.org/packages/malukenho/mcbumpface">malukenho/mcbumpface</a>

<ul>
<li>Update the versions in the <code>composer.json</code> file after an update</li>
</ul></li>
<li><a href="https://packagist.org/packages/qossmic/deptrac-shim">qossmic/deptrac-shim</a>

<ul>
<li>A shim for <a href="https://packagist.org/packages/qossmic/deptrac">qossmic/deptrac</a>: 
A tool to define dependency layers based on e.g. namespaces</li>
</ul></li>
<li><a href="https://packagist.org/packages/icanhazstring/composer-unused">icanhazstring/composer-unused</a>

<ul>
<li>Show dependencies in the <code>composer.json</code> that are not used in the codebase</li>
</ul></li>
<li><a href="https://packagist.org/packages/roave/security-advisories">roave/security-advisories</a>

<ul>
<li>Gives a warning when packages with known vulnerabilities are used</li>
<li>Alternative: <a href="https://github.com/fabpot/local-php-security-checker/">local-php-security-checker</a></li>
</ul></li>
</ul>

<p><!-- generated -->
<a id='qa-make-targets'> </a>
<!-- /generated --></p>

<h2>QA make targets</h2>

<p>You might have noticed that <strong>all tools have their own configuration options</strong>. Instead of
remembering each of them, I'll define corresponding make targets in <code>.make/01-02-application-qa.mk</code>.
The easiest way to do so would be to "hard-code" the exact commands that I ran previously, e.g.</p>

<pre><code class="language-makefile">.PHONY: phpstan
phpstan:  ## Run static analyzer on all application and test files 
    @$(EXECUTE_IN_APPLICATION_CONTAINER) vendor/bin/phpstan analyse app tests --level=9
</code></pre>

<p>(Please refer to the
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#run-commands-in-the-docker-containers">Run commands in the docker containers</a>
section in the previous tutorial for an explanation of the <code>EXECUTE_IN_APPLICATION_CONTAINER</code> 
variable).</p>

<p>However, this implementation is quite inflexible: What if we want to check a single file or try out
other options? So let's create some variables instead:</p>

<pre><code class="language-makefile">PHPSTAN_CMD=php vendor/bin/phpstan analyse
PHPSTAN_ARGS=--level=9
PHPSTAN_FILES=$(APP_FILES) $(TEST_FILES)

.PHONY: phpstan
phpstan: ## Run static analyzer on all application and test files 
    @$(EXECUTE_IN_APPLICATION_CONTAINER) $(PHPSTAN_CMD) $(PHPSTAN_ARGS) $(PHPSTAN_FILES) 
</code></pre>

<p>This target allows me to override the defaults and e.g. check only the file 
<code>app/Commands/SetupDbCommand.php</code> with <code>--level=1</code></p>

<pre><code class="language-shell">make phpstan PHPSTAN_FILES=app/Commands/SetupDbCommand.php PHPSTAN_ARGS="--level=1" 
</code></pre>

<pre><code class="language-text">$ make phpstan PHPSTAN_FILES=app/Commands/SetupDbCommand.php PHPSTAN_ARGS="--level=1" 
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors
</code></pre>

<p>The remaining tool variables can be configured in the exact same way:</p>

<pre><code class="language-makefile"># constants
 ## files
ALL_FILES=./
APP_FILES=app/
TEST_FILES=tests/

# Tool CLI config
PHPUNIT_CMD=php vendor/bin/phpunit
PHPUNIT_ARGS= -c phpunit.xml
PHPUNIT_FILES=
PHPSTAN_CMD=php vendor/bin/phpstan analyse
PHPSTAN_ARGS=--level=9
PHPSTAN_FILES=$(APP_FILES) $(TEST_FILES)
PHPCS_CMD=php vendor/bin/phpcs
PHPCS_ARGS=--parallel=$(CORES) --standard=psr12
PHPCS_FILES=$(APP_FILES)
PHPCBF_CMD=php vendor/bin/phpcbf
PHPCBF_ARGS=$(PHPCS_ARGS)
PHPCBF_FILES=$(PHPCS_FILES)
PARALLEL_LINT_CMD=php vendor/bin/parallel-lint
PARALLEL_LINT_ARGS=-j 4 --exclude vendor/ --exclude .docker --exclude .git
PARALLEL_LINT_FILES=$(ALL_FILES)
COMPOSER_REQUIRE_CHECKER_CMD=php vendor/bin/composer-require-checker
COMPOSER_REQUIRE_CHECKER_ARGS=--ignore-parse-errors
</code></pre>

<p><!-- generated -->
<a id='the-qa-target'> </a>
<!-- /generated --></p>

<h3>The <code>qa</code> target</h3>

<p>From a workflow perspective I usually want to <strong>run all configured qa tools</strong> instead of each one
individually (being able to run individually is still great if a tool fails, though).</p>

<p>A trivial approach would be a new target that <strong>uses all individual tool targets as preconditions</strong>:</p>

<pre><code class="language-makefile">.PHONY: qa
qa: phpstan \
    phplint \
    composer-require-checker \
    phpcs
</code></pre>

<p>But we can do better, because this target produces quite a noisy output:</p>

<pre><code class="language-text">$ make qa
 25/25 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%


 [OK] No errors

PHP 8.1.1 | 4 parallel jobs
............................................................ 60/61 (98 %)
.                                                            61/61 (100 %)


Checked 61 files in 0.3 seconds
No syntax error found
ComposerRequireChecker 4.0.0@baa11a4e9e5072117e3d180ef16c07036cafa4a2
There were no unknown symbols found.
.................... 20 / 20 (100%)


Time: 576ms; Memory: 8MB
</code></pre>

<p>I'd rather have something like this:</p>

<pre><code class="language-text">$ make qa
phplint                             done   took 1s
phpcs                               done   took 1s
phpstan                             done   took 3s
composer-require-checker            done   took 6s
</code></pre>

<p><!-- generated -->
<a id='the-execute-function'> </a>
<!-- /generated --></p>

<h3>The <code>execute</code> "function"</h3>

<p>We'll make this work by <strong>suppressing the tool output</strong> and <strong>using a user-defined <code>execute</code> make
function</strong> to format all targets nicely.</p>

<p><small>Though "function" isn't quite correct here, because it's rather a
<a href="https://www.gnu.org/software/make/manual/html_node/Multi_002dLine.html#Multi_002dLine">multiline variable</a>
defined via <code>define ... endef</code> that is then "invoked" via
the <a href="https://www.gnu.org/software/make/manual/html_node/Call-Function.html">call function</a>.
</small></p>

<pre><code class="language-makefile"># File: 01-02-application-qa.mk

# call with NO_PROGRESS=true to hide tool progress (makes sense when invoking multiple tools together)
NO_PROGRESS?=false
ifeq ($(NO_PROGRESS),true)
    PHPSTAN_ARGS+= --no-progress
endif


# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
define execute
    if [ "$(NO_PROGRESS)" = "false" ]; then \
        eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"; \
    else \
        START=$$(date +%s); \
        printf "%-35s" "$@"; \
        if OUTPUT=$$(eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)" 2&gt;&amp;1); then \
            printf " %-6s" "done"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $${RUNTIME}s\n"; \
        else \
            printf " %-6s" "fail"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $${RUNTIME}s\n"; \
            echo "$$OUTPUT"; \
            printf "\n"; \
            exit 1; \
        fi; \
    fi
endef
</code></pre>

<ul>
<li>the <code>NO_PROGRESS</code> variable is set to <code>false</code> by default and will cause a target to be invoked
as before, showing all its output immediately

<ul>
<li>if the variable is set to <code>true</code>, the target is instead invoked via <code>eval</code> and the output is
captured in the <code>OUTPUT</code> bash variable that will only be printed if the result of the
invocation is faulty</li>
</ul></li>
</ul>

<p>The tool targets are then adjusted to use the new function.</p>

<pre><code class="language-makefile">.PHONY: phpstan
phpstan: ## Run static analyzer on all application and test files
    @$(call execute,$(PHPSTAN_CMD),$(PHPSTAN_ARGS),$(PHPSTAN_FILES),$(ARGS))
</code></pre>

<p>We can now call the <code>phpstan</code> target with <code>NO_PROGRESS=true</code> like so:</p>

<pre><code class="language-text">$ make phpstan NO_PROGRESS=true
phpstan                             done   took 4s
</code></pre>

<p>An "error" would look likes this:</p>

<pre><code class="language-text">$ make phpstan NO_PROGRESS=true
phpstan                             fail   took 9s
 ------ ----------------------------------------
  Line   app/Providers/RouteServiceProvider.php
 ------ ----------------------------------------
  49     Cannot access property $id on mixed.
 ------ ----------------------------------------
</code></pre>

<p><!-- generated -->
<a id='parallel-execution-and-a-helper-target'> </a>
<!-- /generated --></p>

<h3>Parallel execution and a helper target</h3>

<p>Technically, this also already works with the <code>qa</code> target and we can even speed up the process by 
<strong>running the tools in parallel</strong> with
the <a href="https://www.gnu.org/software/make/manual/html_node/Parallel.html">-j flag for "Parallel Execution"</a></p>

<pre><code class="language-text">$ make -j 4 qa NO_PROGRESS=true
phpstan                            phplint                            composer-require-checker           phpcs                               done   took 5s
done   took 5s
done   took 7s
done   took 10s
</code></pre>

<p>Well... not quite what we wanted. We also need to use 
<a href="https://www.gnu.org/software/make/manual/html_node/Parallel-Output.html"><code>--output-sync=target</code> to controll the "Output During Parallel Execution"</a></p>

<pre><code class="language-text">$ make -j 4 --output-sync=target qa NO_PROGRESS=true
phpstan                             done   took 3s
phplint                             done   took 1s
composer-require-checker            done   took 5s
phpcs                               done   took 1s
</code></pre>

<p>Since this is quite a mouthful to type, we'll use a helper target <code>qa-exec</code> for running the tools
and put all the inconvenient-to-type options in the final <code>qa</code> target.</p>

<pre><code class="language-makefile"># File: 01-02-application-qa.mk
#...

# variables
CORES?=$(shell (nproc  || sysctl -n hw.ncpu) 2&gt; /dev/null)

.PHONY: qa
qa: ## Run code quality tools on all files
    @"$(MAKE)" -j $(CORES) -k --no-print-directory --output-sync=target qa-exec NO_PROGRESS=true

.PHONY: qa-exec
qa-exec: phpstan \
    phplint \
    composer-require-checker \
    phpcs
</code></pre>

<p>For the number of parallel processes I use <code>nproc</code> (works on Linux and Windows) resp. 
<code>sysctl -n hw.ncpu</code> (works on Mac) to determine the number of available cores. If you dedicate 
less resources to docker you might want to hard-code this setting to a lower value (e.g. by 
adding a <code>CORES</code> variable in 
the <a href="/blog/docker-from-scratch-for-php-applications-in-2022/#shared-variables-make-env"><code>.make/.env</code></a> 
file).</p>

<p><!-- generated -->
<a id='sprinkle-some-color-on-top'> </a>
<!-- /generated --></p>

<h3>Sprinkle some color on top</h3>

<p>The final piece for getting to the output mentioned in the <a href="#introduction">Introduction</a> is the 
bash-coloring:</p>

<p><a href="/img/php-qa-tools-make-docker/qa-tool-output.PNG"><img src="/img/php-qa-tools-make-docker/qa-tool-output.PNG" alt="QA tool output" /></a></p>

<p>To make this work, we need to understand first how colors work in bash:</p>

<blockquote>
  <p>This  [coloring] can be accomplished by adding a <code>\e</code> [or <code>\033</code>] at the beginning to form an 
  escape sequence. The escape sequence for specifying color codes is <code>\e[COLORm</code>
  (<code>COLOR</code> represents our (numeric) color code in this case).</p>
</blockquote>

<p>(via <a href="https://dev.to/ifenna__/adding-colors-to-bash-scripts-48g4">Adding colors to Bash scripts</a>)</p>

<p>E.g. the following script will print a green text:</p>

<pre><code class="language-shell">printf "\033[0;32mThis text is green\033[0m"
</code></pre>

<p>So we define the required colors as variables and use them in the corresponding places in the
<a href="#the-execute-function"><code>execute</code> function</a>:</p>

<pre><code class="language-makefile"> ## bash colors
RED:=\033[0;31m
GREEN:=\033[0;32m
YELLOW:=\033[0;33m
NO_COLOR:=\033[0m

# ...

# Use NO_PROGRESS=false when running individual tools.
# On  NO_PROGRESS=true  the corresponding tool has no output on success
#                       apart from its runtime but it will still print 
#                       any errors that occured. 
define execute
    if [ "$(NO_PROGRESS)" = "false" ]; then \
        eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)"; \
    else \
        START=$$(date +%s); \
        printf "%-35s" "$@"; \
        if OUTPUT=$$(eval "$(EXECUTE_IN_APPLICATION_CONTAINER) $(1) $(2) $(3) $(4)" 2&gt;&amp;1); then \
            printf " $(GREEN)%-6s$(NO_COLOR)" "done"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $(YELLOW)$${RUNTIME}s$(NO_COLOR)\n"; \
        else \
            printf " $(RED)%-6s$(NO_COLOR)" "fail"; \
            END=$$(date +%s); \
            RUNTIME=$$((END-START)) ;\
            printf " took $(YELLOW)$${RUNTIME}s$(NO_COLOR)\n"; \
            echo "$$OUTPUT"; \
            printf "\n"; \
            exit 1; \
        fi; \
    fi
endef
</code></pre>

<p>Please note, that i did <strong>not include the tests in the <code>qa</code> target</strong>. I like to run those 
separately, because our tests usually take a lot longer to execute. So in my day-to-day work I 
would run <code>make qa</code> and <code>make test</code> to ensure that code quality and tests are passing:</p>

<pre><code class="language-text">$ make qa
phplint                             done   took 1s
phpcs                               done   took 1s
composer-require-checker            done   took 14s
phpstan                             done   took 16s

$ make test
PHPUnit 9.5.19 #StandWithUkraine

.......                                                             7 / 7 (100%)

Time: 00:03.772, Memory: 28.00 MB

OK (7 tests, 13 assertions)
</code></pre>

<p><!-- generated -->
<a id='further-updates-in-the-codebase'> </a>
<!-- /generated --></p>

<h2>Further updates in the codebase</h2>

<p>I've also cleaned up the codebase a little in branch 
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-5-php-qa-tools-make-docker">part-5-php-qa-tools-make-docker</a>
and even though those changes have nothing to todo with "QA tools" I didn't want to leave them 
unnoticed:</p>

<ul>
<li><p>removing unnecessary files (<code>.styleci.yml</code>, <code>package.json</code>, <code>webpack.mix.js</code>)</p>

<ul>
<li>removing unused values from the <code>.env.example</code> file</li>
<li>run a <code>composer update</code> to get the latest Laravel version</li>
<li>add a <code>show-help</code> script to the <code>scripts</code> section of the <code>composer.json</code> file that references the 
<code>Makefile</code> (see also <a href="https://twitter.com/PascalLandau/status/1518227256648343552">this discussion on Twitter</a>)</li>
</ul>

<pre><code class="language-json"><br />{
    "scripts": {
        "show-help": [
            "make"
        ]
    },
    "scripts-descriptions": {
        "show-help": "Display available 'make' commands (we use make instead of composer scripts)."
    }
}
</code></pre>

<p><a href="/img/php-qa-tools-make-docker/make-composer-scripts.gif"><img src="/img/php-qa-tools-make-docker/make-composer-scripts.gif" alt="Run make via composer script" /></a></p>

<ul>
<li>replace <code>docker-compose</code> with <code>docker compose</code> to use <a href="https://docs.docker.com/compose/cli-command/">compose v2</a></li>
</ul></li>
</ul>

<p>For some reason, the last point caused some trouble because Linux and Docker Desktop for Windows 
require a <code>-T</code> flag for the <code>exec</code> command to disable a TTY allocation in some cases. Whereas on 
Docker Desktop for Mac 
<a href="https://github.com/moby/moby/issues/37366#issuecomment-527099456">the missing TTY lead to a cluttered output ("staircase effect")</a>.</p>

<p>Thus I modified the <code>Makefile</code> to populate a <code>DOCKER_COMPOSE_EXEC_OPTIONS</code> variable based on the OS</p>

<pre><code class="language-makefile"># Add the -T options to "docker compose exec" to avoid the 
# "panic: the handle is invalid"
# error on Windows and Linux 
# @see https://stackoverflow.com/a/70856332/413531
DOCKER_COMPOSE_EXEC_OPTIONS=-T

# OS is defined for WIN systems, so "uname" will not be executed
OS?=$(shell uname)
ifeq ($(OS),Windows_NT)
    # Windows requires the .exe extension, otherwise the entry is ignored
    # @see https://stackoverflow.com/a/60318554/413531
    SHELL := bash.exe
else ifeq ($(OS),Darwin)
    # On Mac, the -T must be omitted to avoid cluttered output
    # @see https://github.com/moby/moby/issues/37366#issuecomment-401157643
    DOCKER_COMPOSE_EXEC_OPTIONS=
endif
</code></pre>

<p>And use the variable when defining <code>EXECUTE_IN_*_CONTAINER</code> in <code>.make/02-00-docker.mk</code></p>

<pre><code class="language-makefile">ifeq ($(EXECUTE_IN_CONTAINER),true)
    EXECUTE_IN_ANY_CONTAINER:=$(DOCKER_COMPOSE) exec $(DOCKER_COMPOSE_EXEC_OPTIONS) --user $(APP_USER_NAME) $(DOCKER_SERVICE_NAME)
    EXECUTE_IN_APPLICATION_CONTAINER:=$(DOCKER_COMPOSE) exec $(DOCKER_COMPOSE_EXEC_OPTIONS) --user $(APP_USER_NAME) $(DOCKER_SERVICE_NAME_APPLICATION)
    EXECUTE_IN_WORKER_CONTAINER:=$(DOCKER_COMPOSE) exec $(DOCKER_COMPOSE_EXEC_OPTIONS) --user $(APP_USER_NAME) $(DOCKER_SERVICE_NAME_PHP_WORKER)
endif
</code></pre>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. You should now have a blueprint for adding code quality tools for your dockerized
application and way to conveniently control them through a Makefile.</p>

<p>In the next part of this tutorial, we will 
<a href="/blog/git-secret-encrypt-repository-docker/">set up git secret to encrypt secret values and store them directly in the git repository</a>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Mon, 25 Apr 2022 05:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/php-qa-tools-make-docker/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/php-qa-tools-make-docker/</guid>
            </item>
                    <item>
                <title>Run Laravel 9 on Docker in 2022 [Tutorial Part 4]</title>
                <description><![CDATA[<p>In this part of my tutorial series on developing PHP on Docker we will
install <strong>Laravel and make sure our setup works for Artisan Commands, a Redis Queue and Controllers</strong>
for the front end requests.</p>

<div class="youtube">
<iframe width="560" height="315" src="https://www.youtube.com/embed/BpsBzpMD87c" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

<p><strong>All code samples are publicly available</strong> in my
<a href="https://github.com/paslandau/docker-php-tutorial/">Docker PHP Tutorial repository on Github</a>.<br />
You find the branch for this tutorial at
<a href="https://github.com/paslandau/docker-php-tutorial/tree/part-4-3-run-laravel-9-docker-in-2022">part-4-3-run-laravel-9-docker-in-2022</a>.</p>

<p><strong>All published parts of the Docker PHP Tutorial</strong> are collected under a dedicated page at
<a href="/docker-php-tutorial/">Docker PHP Tutorial</a>. The previous part was
<a href="/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/">PhpStorm, Docker and Xdebug 3 on PHP 8.1 in 2022</a>
and the following one is
<a href="/blog/php-qa-tools-make-docker/">Set up PHP QA tools and control them via make</a>.</p>

<p>If you want to follow along, please subscribe to the <a href="/feed.xml">RSS feed</a>
or <a href="#newsletter">via email</a>
to get <strong>automatic notifications</strong> when the next part comes out :)</p>

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

<h2>Table of contents</h2>

<!-- toc -->

<ul>
<li><a href="#introduction">Introduction</a></li>
<li><a href="#install-extensions">Install extensions</a></li>
<li><a href="#install-laravel">Install Laravel</a></li>
<li><a href="#update-the-php-poc">Update the PHP POC</a>

<ul>
<li><a href="#config">config</a>

<ul>
<li><a href="#database-connection">database connection</a></li>
<li><a href="#queue-connection">queue connection</a></li>
</ul></li>
<li><a href="#controllers">Controllers</a></li>
<li><a href="#commands">Commands</a></li>
<li><a href="#jobs-and-workers">Jobs and workers</a></li>
<li><a href="#tests">Tests</a></li>
</ul></li>
<li><a href="#makefile-updates">Makefile updates</a>

<ul>
<li><a href="#clearing-the-queue">Clearing the queue</a></li>
</ul></li>
<li><a href="#running-the-poc">Running the POC</a></li>
<li><a href="#wrapping-up">Wrapping up</a></li>
</ul>

<!-- /toc -->

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

<h2>Introduction</h2>

<p>The goal of this tutorial is to run the 
<a href="/blog/docker-from-scratch-for-php-applications-in-2022/#php-poc">PHP POC from part 4.1</a>
using Laravel as a framework instead of "plain PHP". 
We'll use the newest version of Laravel (Laravel 9) that was
<a href="https://laravel-news.com/laravel-9-released">released at the beginning of February 2022</a>.</p>

<p>If you want to follow along, please use the <strong>branch of the 
<a href="/blog/phpstorm-docker-xdebug-3-php-8-1-in-2022/">previous tutorial</a></strong>, i.e.</p>

<pre><code class="language-text">git checkout git checkout part-4-2-phpstorm-docker-xdebug-3-php-8-1-in-2022
</code></pre>

<p>The branch of <strong>this</strong> tutorials contains the "end result", i.e. Laravel will already exist in 
the repository.</p>

<p><!-- generated -->
<a id='install-extensions'> </a>
<!-- /generated --></p>

<h2>Install extensions</h2>

<p>Before Laravel can be installed, we need to add the necessary extensions of the framework (and all
its dependencies) to the <code>php-base</code> image:</p>

<pre><code># File: .docker/images/php/base/Dockerfile

# ...

RUN apk add --update --no-cache  \
        php-curl~=${TARGET_PHP_VERSION} \

</code></pre>

<p><!-- generated -->
<a id='install-laravel'> </a>
<!-- /generated --></p>

<h2>Install Laravel</h2>

<p>We'll start by
<a href="https://laravel.com/docs/9.x/installation#installation-via-composer">creating a new Laravel project with composer</a></p>

<pre><code class="language-bash">composer create-project --prefer-dist laravel/laravel /tmp/laravel "9.0.*" --no-install --no-scripts
</code></pre>

<p>The files are added to <code>/tmp/laravel</code> because
<a href="https://github.com/composer/composer/issues/1135#issuecomment-10358244">composer projects cannot be created in non-empty folders</a>
, so we need to create the project in a temporary location first and move it afterwards.</p>

<p>Since I don't have PHP 8 installed on my laptop, I'll execute the command in the <code>application</code>
docker container via</p>

<pre><code class="language-bash">make execute-in-container DOCKER_SERVICE_NAME="application" COMMAND='composer create-project --prefer-dist laravel/laravel /tmp/laravel "9.0.*" --no-install --no-scripts'
</code></pre>

<p>and then move the files into the application directory via</p>

<pre><code class="language-bash">rm -rf public/ tests/ composer.* phpunit.xml
make execute-in-container DOCKER_SERVICE_NAME="application" COMMAND="bash -c 'mv -n /tmp/laravel/{.*,*} .' &amp;&amp; rm -f /tmp/laravel"
cp .env.example .env
</code></pre>

<p><strong>Notes</strong></p>

<ul>
<li><code>composer install</code> is skipped via <code>--no-install</code> to avoid having to copy over the <code>vendor/</code> folder
(which is super slow on Docker Desktop)</li>
<li><a href="https://unix.stackexchange.com/a/127713">existing directories cannot be overwritten by <code>mv</code></a> 
thus I remove <code>public/</code> and <code>tests/</code> upfront (as well as the <code>composer</code> and <code>phpunit</code> config 
files)</li>
<li><code>mv</code> uses the <code>-n</code> flag so that existing files like our <code>.editorconfig</code> are not overwritten</li>
<li>I need to use <code>bash -c</code> to run the command in the container because otherwise 
<a href="https://github.com/moby/moby/issues/12558#issuecomment-94775867">the <code>*</code> wildcard would have no effect in the container</a></li>
</ul>

<p>To finalize the installation I need to install the composer dependencies and execute the 
<code>create-project</code> scripts defined in 
<a href="https://github.com/laravel/laravel/blob/9.x/composer.json#L45"><code>composer.json</code></a>:</p>

<pre><code>make composer ARGS=install
make composer ARGS="run-script post-create-project-cmd"
</code></pre>

<p>Since our nginx configuration was already pointing to the <code>public/</code> directory, we can immediately 
open <a href="http://127.0.0.1">http://127.0.0.1</a> in the browser and should see the frontpage of 
a fresh Laravel installation.</p>

<p><!-- generated -->
<a id='update-the-php-poc'> </a>
<!-- /generated --></p>

<h2>Update the PHP POC</h2>

<p><!-- generated -->
<a id='config'> </a>
<!-- /generated --></p>

<h3>config</h3>

<p>We need to update the connection information for the database and the queue (previously 
configured via <code>dependencies.php</code>) in the <code>.env</code> file</p>

<p><!-- generated -->
<a id='database-connection'> </a>
<!-- /generated --></p>

<h4>database connection</h4>

<pre><code>DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=application_db
DB_USERNAME=root
DB_PASSWORD=secret_mysql_root_password
</code></pre>

<p><!-- generated -->
<a id='queue-connection'> </a>
<!-- /generated --></p>

<h4>queue connection</h4>

<pre><code>QUEUE_CONNECTION=redis

REDIS_HOST=redis
REDIS_PASSWORD=secret_redis_password
</code></pre>

<p><!-- generated -->
<a id='controllers'> </a>
<!-- /generated --></p>

<h3>Controllers</h3>

<p>The functionality of the previous <code>public/index.php</code> file now lives in the <code>HomeController</code> at 
<code>app/Http/Controllers/HomeController.php</code></p>

<pre><code class="language-php">class HomeController extends Controller
{
    use DispatchesJobs;

    public function __invoke(Request $request, QueueManager $queueManager, DatabaseManager $databaseManager): View
    {
        $jobId = $request-&gt;input("dispatch") ?? null;
        if ($jobId !== null) {
            $job = new InsertInDbJob($jobId);
            $this-&gt;dispatch($job);

            return $this-&gt;getView("Adding item '$jobId' to queue");
        }

        if ($request-&gt;has("queue")) {

            /**
             * @var RedisQueue $redisQueue
             */
            $redisQueue = $queueManager-&gt;connection();
            $redis =  $redisQueue-&gt;getRedis()-&gt;connection();
            $queueItems = $redis-&gt;lRange("queues:default", 0, 99999);

            $content = "Items in queue\n".var_export($queueItems, true);

            return $this-&gt;getView($content);
        }

        if ($request-&gt;has("db")) {
            $items = $databaseManager-&gt;select($databaseManager-&gt;raw("SELECT * FROM jobs"));

            $content = "Items in db\n".var_export($items, true);

            return $this-&gt;getView($content);
        }
        $content = &lt;&lt;&lt;HTML
            &lt;ul&gt;
                &lt;li&gt;&lt;a href="?dispatch=foo"&gt;Dispatch job 'foo' to the queue.&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href="?queue"&gt;Show the queue.&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href="?db"&gt;Show the DB.&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
            HTML;

        return $this-&gt;getView($content);
    }

    private function getView(string $content): View
    {
        return view('home')-&gt;with(["content" =&gt; $content]);
    }
}

</code></pre>

<p>Its content is displayed via the <code>home</code> view located at <code>resources/views/home.blade.php</code>:</p>

<pre><code>&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    {!! $content !!}
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>

<p>The controller is added as a route in <code>routes/web.php</code>:</p>

<pre><code class="language-php">Route::get('/', \App\Http\Controllers\HomeController::class)-&gt;name("home");
</code></pre>

<p><!-- generated -->
<a id='commands'> </a>
<!-- /generated --></p>

<h3>Commands</h3>

<p>We will replace the <code>setup.php</code> script with a <code>SetupDbCommand</code> that is located at 
<code>app/Commands/SetupDbCommand.php</code></p>

<pre><code class="language-php">class SetupDbCommand extends Command
{
    /**
     * @var string
     */
    protected $name = "app:setup-db";

    /**
     * @var string
     */
    protected $description = "Run the application database setup";

    protected function getOptions(): array
    {
        return [
            [
                "drop",
                null,
                InputOption::VALUE_NONE,
                "If given, the existing database tables are dropped and recreated.",
            ],
        ];
    }

    public function handle()
    {
        $drop = $this-&gt;option("drop");
        if ($drop) {
            $this-&gt;info("Dropping all database tables...");

            $this-&gt;call(WipeCommand::class);

            $this-&gt;info("Done.");
        }

        $this-&gt;info("Running database migrations...");

        $this-&gt;call(MigrateCommand::class);

        $this-&gt;info("Done.");
    }
}
</code></pre>

<p>Register it the <code>AppServiceProvider</code> in <code>app/Providers/AppServiceProvider.php</code></p>

<pre><code class="language-php">    public function register()
    {
        $this-&gt;commands([
            \App\Commands\SetupDbCommand::class
        ]);
    }
</code></pre>

<p>and update the <code>setup-db</code> target in <code>.make/01-00-application-setup.mk</code> to run the <code>artisan</code> Command</p>

<pre><code class="language-Makefile">.PHONY: setup-db
setup-db: ## Setup the DB tables
    $(EXECUTE_IN_APPLICATION_CONTAINER) php artisan app:setup-db $(ARGS);
</code></pre>

<p>We will also create a migration for the <code>jobs</code> table in 
<code>database/migrations/2022_02_10_000000_create_jobs_table.php</code>:</p>

<pre><code class="language-php">return new class extends Migration
{
    public function up(): void
    {
        Schema::create('jobs', function (Blueprint $table) {
            $table-&gt;id();
            $table-&gt;string('value');
        });
    }
};
</code></pre>

<p><!-- generated -->
<a id='jobs-and-workers'> </a>
<!-- /generated --></p>

<h3>Jobs and workers</h3>

<p>We will replace the <code>worker.php</code> script with <code>InsertInDbJob</code> located at 
<code>app/Jobs/InsertInDbJob.php</code></p>

<pre><code class="language-php">class InsertInDbJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(
        public readonly string $jobId
    ) {
    }

    public function handle(DatabaseManager $databaseManager)
    {
        $databaseManager-&gt;insert("INSERT INTO `jobs`(value) VALUES(?)", [$this-&gt;jobId]);
    }
}
</code></pre>

<p>though this will "only" handle the insertion part. For the worker itself we will use the native
<code>\Illuminate\Queue\Console\WorkCommand</code> via</p>

<pre><code>php artisan queue:work
</code></pre>

<p>We need to adjust the <code>.docker/images/php/worker/Dockerfile</code> and change</p>

<pre><code>ARG PHP_WORKER_COMMAND="php $APP_CODE_PATH/worker.php"
</code></pre>

<p>to</p>

<pre><code>ARG PHP_WORKER_COMMAND="php $APP_CODE_PATH/artisan queue:work"
</code></pre>

<p>Since this change takes place directly in the Dockerfile, we must now rebuild the image</p>

<pre><code>$ make docker-build-image DOCKER_SERVICE_NAME=php-worker
</code></pre>

<p>and restart it</p>

<pre><code>$ make docker-up
</code></pre>

<p><!-- generated -->
<a id='tests'> </a>
<!-- /generated --></p>

<h3>Tests</h3>

<p>I'd also like to take this opportunity to add a <code>Feature</code> test for the <code>HomeController</code> at
<code>tests/Feature/App/Http/Controllers/HomeControllerTest.php</code>:</p>

<pre><code class="language-php">class HomeControllerTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();

        $this-&gt;setupDatabase();
        $this-&gt;setupQueue();
    }

    /**
     * @dataProvider __invoke_dataProvider
     */
    public function test___invoke(array $params, string $expected): void
    {
        $urlGenerator = $this-&gt;getDependency(UrlGenerator::class);

        $url = $urlGenerator-&gt;route("home", $params);

        $response = $this-&gt;get($url);

        $response
            -&gt;assertStatus(200)
            -&gt;assertSee($expected, false)
        ;
    }

    public function __invoke_dataProvider(): array
    {
        return [
            "default"           =&gt; [
                "params"   =&gt; [],
                "expected" =&gt; &lt;&lt;&lt;TEXT
                        &lt;li&gt;&lt;a href="?dispatch=foo"&gt;Dispatch job 'foo' to the queue.&lt;/a&gt;&lt;/li&gt;
                        &lt;li&gt;&lt;a href="?queue"&gt;Show the queue.&lt;/a&gt;&lt;/li&gt;
                        &lt;li&gt;&lt;a href="?db"&gt;Show the DB.&lt;/a&gt;&lt;/li&gt;
                    TEXT
                ,
            ],
            "database is empty" =&gt; [
                "params"   =&gt; ["db"],
                "expected" =&gt; &lt;&lt;&lt;TEXT
                        Items in db
                    array (
                    )
                    TEXT
                ,
            ],
            "queue is empty"    =&gt; [
                "params"   =&gt; ["queue"],
                "expected" =&gt; &lt;&lt;&lt;TEXT
                        Items in queue
                    array (
                    )
                    TEXT
                ,
            ],
        ];
    }

    public function test_shows_existing_items_in_database(): void
    {
        $databaseManager = $this-&gt;getDependency(DatabaseManager::class);

        $databaseManager-&gt;insert("INSERT INTO `jobs` (id, value) VALUES(1, 'foo');");

        $urlGenerator = $this-&gt;getDependency(UrlGenerator::class);

        $params = ["db"];
        $url    = $urlGenerator-&gt;route("home", $params);

        $response = $this-&gt;get($url);

        $expected = &lt;&lt;&lt;TEXT
                Items in db
            array (
              0 =&gt; 
              (object) array(
                 'id' =&gt; 1,
                 'value' =&gt; 'foo',
              ),
            )
            TEXT;

        $response
            -&gt;assertStatus(200)
            -&gt;assertSee($expected, false)
        ;
    }

    public function test_shows_existing_items_in_queue(): void
    {
        $queueManager = $this-&gt;getDependency(QueueManager::class);

        $job = new InsertInDbJob("foo");
        $queueManager-&gt;push($job);

        $urlGenerator = $this-&gt;getDependency(UrlGenerator::class);

        $params = ["queue"];
        $url    = $urlGenerator-&gt;route("home", $params);

        $response = $this-&gt;get($url);

        $expectedJobsCount = &lt;&lt;&lt;TEXT
                Items in queue
            array (
              0 =&gt; '{
            TEXT;

        $expected = &lt;&lt;&lt;TEXT
            \\\\"jobId\\\\";s:3:\\\\"foo\\\\";
            TEXT;

        $response
            -&gt;assertStatus(200)
            -&gt;assertSee($expectedJobsCount, false)
            -&gt;assertSee($expected, false)
        ;
    }
}
</code></pre>

<p>The test checks the database as well as the queue and uses the helper methods 
<code>$this-&gt;setupDatabase()</code> and <code>$this-&gt;setupQueue()</code> that I defined in the base test case at
<code>tests/TestCase.php</code> as follows</p>

<pre><code class="language-php">   /**
     * @template T
     * @param class-string&lt;T&gt; $className
     * @return T
     */
    protected function getDependency(string $className)
    {
        return $this-&gt;app-&gt;get($className);
    }

    protected function setupDatabase(): void
    {
        $databaseManager = $this-&gt;getDependency(DatabaseManager::class);

        $actualConnection  = $databaseManager-&gt;getDefaultConnection();
        $testingConnection = "testing";
        if ($actualConnection !== $testingConnection) {
            throw new RuntimeException("Database tests are only allowed to run on default connection '$testingConnection'. The current default connection is '$actualConnection'.");
        }

        $this-&gt;ensureDatabaseExists($databaseManager);

        $this-&gt;artisan(SetupDbCommand::class, ["--drop" =&gt; true]);
    }

    protected function setupQueue(): void
    {
        $queueManager = $this-&gt;getDependency(QueueManager::class);

        $actualDriver  = $queueManager-&gt;getDefaultDriver();
        $testingDriver = "testing";
        if ($actualDriver !== $testingDriver) {
            throw new RuntimeException("Queue tests are only allowed to run on default driver '$testingDriver'. The current default driver is '$actualDriver'.");
        }

        $this-&gt;artisan(ClearCommand::class);
    }

    protected function ensureDatabaseExists(DatabaseManager $databaseManager): void
    {
        $connection = $databaseManager-&gt;connection();

        try {
            $connection-&gt;getPdo();
        } catch (PDOException $e) {
            // e.g. SQLSTATE[HY000] [1049] Unknown database 'testing'
            if ($e-&gt;getCode() !== 1049) {
                throw $e;
            }
            $config             = $connection-&gt;getConfig();
            $config["database"] = "";

            $connector = new MySqlConnector();
            $pdo       = $connector-&gt;connect($config);
            $database  = $connection-&gt;getDatabaseName();
            $pdo-&gt;exec("CREATE DATABASE IF NOT EXISTS `{$database}`;");
        }
    }
</code></pre>

<p>The methods ensure that the tests are only executed if the proper database connection and queue 
driver is configured. This is done through environment variables and I like using
<a href="https://laravel.com/docs/9.x/testing#the-env-testing-environment-file">a dedicated <code>.env</code> file located at <code>.env.testing</code></a><br />
for all testing <code>ENV</code> values instead of defining them in the <code>phpunit.xml</code> config file via <code>&lt;env&gt;</code> 
elements:</p>

<pre><code class="language-dotenv"># File: .env.testing

DB_CONNECTION=testing
DB_DATABASE=testing
QUEUE_CONNECTION=testing
REDIS_DB=1000
</code></pre>

<p>The corresponding connections have to be configured in the <code>config</code> files</p>

<pre><code class="language-php"># File: config/database.php

return [
// ...
    'connections' =&gt; [
// ...
        'testing' =&gt; [
            'driver' =&gt; 'mysql',
            'url' =&gt; env('DATABASE_URL'),
            'host' =&gt; env('DB_HOST'),
            'port' =&gt; env('DB_PORT', '3306'),
            'database' =&gt; env('DB_DATABASE', 'testing'),
            'username' =&gt; env('DB_USERNAME'),
            'password' =&gt; env('DB_PASSWORD', ''),
            'unix_socket' =&gt; env('DB_SOCKET', ''),
            'charset' =&gt; 'utf8mb4',
            'collation' =&gt; 'utf8mb4_unicode_ci',
            'prefix' =&gt; '',
            'prefix_indexes' =&gt; true,
            'strict' =&gt; true,
            'engine' =&gt; null,
            'options' =&gt; extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA =&gt; env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],
    ],
// ...
    'redis' =&gt; [
// ...
        'testing' =&gt; [
            'url' =&gt; env('REDIS_URL'),
            'host' =&gt; env('REDIS_HOST', '127.0.0.1'),
            'password' =&gt; env('REDIS_PASSWORD'),
            'port' =&gt; env('REDIS_PORT', '6379'),
            'database' =&gt; env('REDIS_DB', '1000'),
        ],
    ],
];
</code></pre>

<pre><code class="language-php"># File: config/queue.php

return [
// ...

    'connections' =&gt; [
// ...
        'testing' =&gt; [
            'driver' =&gt; 'redis',
            'connection' =&gt; 'testing', // =&gt; refers to the "database.redis.testing" config entry
            'queue' =&gt; env('REDIS_QUEUE', 'default'),
            'retry_after' =&gt; 90,
            'block_for' =&gt; null,
            'after_commit' =&gt; false,
        ],
    ],
];
</code></pre>

<p>The tests can be executed via <code>make test</code></p>

<pre><code>$ make test
ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker vendor/bin/phpunit -c phpunit.xml
PHPUnit 9.5.13 by Sebastian Bergmann and contributors.

.......                                                             7 / 7 (100%)

Time: 00:02.709, Memory: 28.00 MB

OK (7 tests, 13 assertions)
</code></pre>

<p><!-- generated -->
<a id='makefile-updates'> </a>
<!-- /generated --></p>

<h2>Makefile updates</h2>

<p><!-- generated -->
<a id='clearing-the-queue'> </a>
<!-- /generated --></p>

<h3>Clearing the queue</h3>

<p>For convenience while testing I added a make target to clear all items from the queue in 
<code>.make/01-01-application-commands.mk</code></p>

<pre><code class="language-Makefile">.PHONY: clear-queue
clear-queue: ## Clear the job queue
    $(EXECUTE_IN_APPLICATION_CONTAINER) php artisan queue:clear $(ARGS)
</code></pre>

<p><!-- generated -->
<a id='running-the-poc'> </a>
<!-- /generated --></p>

<h2>Running the POC</h2>

<p>Since the POC only uses <code>make</code> targets and we basically just "refactored" them, there is no 
modification necessary to make the existing <code>test.sh</code> work:</p>

<pre><code>$ bash test.sh


  Building the docker setup


//...


  Starting the docker setup


//...


  Clearing DB


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application application php artisan app:setup-db --drop;
Dropping all database tables...
Dropped all tables successfully.
Done.
Running database migrations...
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (64.04ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (50.06ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (58.61ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (94.03ms)
Migrating: 2022_02_10_000000_create_jobs_table
Migrated:  2022_02_10_000000_create_jobs_table (31.85ms)
Done.


  Stopping workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl stop worker:*;
worker:worker_00: stopped
worker:worker_01: stopped
worker:worker_02: stopped
worker:worker_03: stopped


  Ensuring that queue and db are empty


&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Items in queue
array (
)
    &lt;/body&gt;
&lt;/html&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Items in db
array (
)
    &lt;/body&gt;
&lt;/html&gt;


  Dispatching a job 'foo'


&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Adding item 'foo' to queue
    &lt;/body&gt;
&lt;/html&gt;


  Asserting the job 'foo' is on the queue


&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Items in queue
array (
  0 =&gt; '{"uuid":"7ea63590-2a86-4739-abf8-8a059d41bd60","displayName":"App\\\\Jobs\\\\InsertInDbJob","job":"Illuminate\\\\Queue\\\\CallQueuedHandler@call","maxTries":null,"maxExceptions":null,"failOnTimeout":false,"backoff":null,"timeout":null,"retryUntil":null,"data":{"commandName":"App\\\\Jobs\\\\InsertInDbJob","command":"O:22:\\"App\\\\Jobs\\\\InsertInDbJob\\":11:{s:5:\\"jobId\\";s:3:\\"foo\\";s:3:\\"job\\";N;s:10:\\"connection\\";N;s:5:\\"queue\\";N;s:15:\\"chainConnection\\";N;s:10:\\"chainQueue\\";N;s:19:\\"chainCatchCallbacks\\";N;s:5:\\"delay\\";N;s:11:\\"afterCommit\\";N;s:10:\\"middleware\\";a:0:{}s:7:\\"chained\\";a:0:{}}"},"id":"I3k5PNyGZc6Z5XWCC4gt0qtSdqUZ84FU","attempts":0}',
)
    &lt;/body&gt;
&lt;/html&gt;


  Starting the workers


ENV=local TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra APP_USER_NAME=application APP_GROUP_NAME=application docker-compose -p dofroscra_local --env-file ./.docker/.env -f ./.docker/docker-compose/docker-compose.yml -f ./.docker/docker-compose/docker-compose.local.yml exec -T --user application php-worker supervisorctl start worker:*;
worker:worker_00: started
worker:worker_01: started
worker:worker_02: started
worker:worker_03: started


  Asserting the queue is now empty


&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Items in queue
array (
)
    &lt;/body&gt;
&lt;/html&gt;


  Asserting the db now contains the job 'foo'


&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;meta charset="utf-8"&gt;
    &lt;/head&gt;
    &lt;body&gt;
    Items in db
array (
  0 =&gt;
  (object) array(
     'id' =&gt; 1,
     'value' =&gt; 'foo',
  ),
)
    &lt;/body&gt;
&lt;/html&gt;

</code></pre>

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

<h2>Wrapping up</h2>

<p>Congratulations, you made it! If some things are not completely clear by now, don't hesitate to
leave a comment. Laravel 9 should now be up and running on the previously set up docker
infrastructure.</p>

<p>In the next part of this tutorial, we will
<a href="/blog/php-qa-tools-make-docker/">Set up PHP QA tools and control them via make</a>.</p>

<p>Please subscribe to the <a href="/feed.xml">RSS feed</a> or <a href="#newsletter">via email</a> to get automatic
notifications when this next part comes out :)</p>
]]></description>
                <pubDate>Wed, 23 Mar 2022 12:00:00 +0000</pubDate>
                <link>https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/?utm_source=blog&amp;utm_medium=rss&amp;utm_campaign=global-feed</link>
                <guid isPermaLink="true">https://www.pascallandau.com/blog/run-laravel-9-docker-in-2022/</guid>
            </item>
            </channel>
</rss>
