Beginner’s CI/CD Guide: Deploy Laravel8 to Google Cloud Run and Cloud SQL via GitHub Actions
Tags: cloudrun, laravel, cloudsql, githubactions
Published: 2022-02-15 08:26:35

Application deployment is one of the important dimensions, including all of the steps, processes of installing, configuring, and enabling a specific application or set of applications. However, I didn’t figure out well what is continuous integration.

For learning it, I started to deploy a Laravel8 application to Google Cloud Run, develop and deploy highly scalable containerized applications on a fully managed serverless platform. But pieces of information about it scattered out on WEB, and I was desperate to understand it, wasting time.

So I will share with you as easily as possible the way of deployment to Google Cloud Run and Google Cloud SQL via GitHub Actions. Let’s get started!

## Directory architecture

The next figure shows us the directory architecture of the git repository.

```bash
.
├── .github/
│   └── workflows
│       └── deploy.yml # yml file to run scripts in Github Actions
├── env/
├── docker/
│   ├── 000-default.conf
│   ├── Dockerfile
│   └── php.ini
├── sh/ # useful shell files
├── src/ # the directory installed Laravel application
└── docker-compose.yml
```

## 1. Build and run the Laravel application on Docker compose

- https://github.com/uuta/cloud-run-laravel8

### 1-1. Clone a repository from GitHub

In this time, we will proceed as the next repository. Under the directory you like, let’s clone it initially and move there.

```bash
$ git clone git@github.com:uuta/cloud-run-laravel8.git
$ cd cloud-run-laravel8
```

Build and run the Laravel application on Docker compose to make sure of working out well.

```bash
$ cp env/.env.example env/.env.local
$ docker-compose up -d --build
...
...
...
Creating cloud-run-laravel8_db_1  ... done
Creating cloud-run-laravel8_app_1 ... done
Time: 0h:00m:24s
```

The APP_KEY in src/.env would be empty, so create it in a docker container.

```bash
$ docker-compose exec app bash
$ php artisan route:clear && \
php artisan cache:clear && \
php artisan config:clear && \
php artisan view:clear && \
php artisan key:generate
```

A Laravel app key would be generated.

```bash
# before
APP_KEY=
# after
APP_KEY=base64:<key>
```

Alright, we'll see the default page of Laravel to access [http://localhost:8080/](http://localhost:8080/) after the installation.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z4kvaf8iu322yvbwlz5d.png)

How about migration? If you execute the migration command after entering the app container, maybe you can see a message such as the next.

```bash
$ docker-compose exec app bash
$ php artisan migrate
Nothing to migrate.
```

The reason why migration has already finished is to be executed while building a docker image based on Dockerfile. Let’s take a look at `Dockerfile.local` for a while.

```bash
FROM composer:latest as build
WORKDIR /app
COPY . /app

FROM php:8.0-apache
COPY docker/php.ini /usr/local/etc/php/
RUN apt update
RUN apt install -y git
RUN apt install -y vim
RUN apt install -y zip unzip
RUN docker-php-ext-install bcmath pdo_mysql

# composer install
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
EXPOSE 8080
COPY --from=build /app /var/www
COPY docker/000-default.conf /etc/apache2/sites-available/000-default.conf
RUN chmod 777 -R /var/www

WORKDIR /
RUN echo "Listen 8080" >> /etc/apache2/ports.conf
RUN chown -R www-data:www-data /var/www
RUN a2enmod rewrite

# Make the file executable, or use "chmod 777" instead of "chmod +x"
RUN chmod +x /var/www/sh/laravel/laravel.local.sh

# This will run the shell file at the time when container is up-and-running successfully (and NOT at the BUILD time)
ENTRYPOINT ["/var/www/sh/laravel/laravel.local.sh"]
```

At the end of this file, there are codes to execute a shell command.

```bash
RUN chmod +x /var/www/sh/laravel/laravel.local.sh
...
ENTRYPOINT ["/var/www/sh/laravel/laravel.local.sh"]
```

So let’s take a look at `sh/laravel/laravel.local.sh` file. `composer install` and `migrate` commands are executed here.

```bash
#!/bin/bash

# initialize laravel
cd /var/www/src
composer install --optimize-autoloader
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Run Laravel migration
php artisan migrate

# Run Apache in "foreground" mode (the default mode that runs in Docker)
apache2-foreground
```

The reason why I dare to make this file is to migrate to Google Cloud SQL in the production environment. According to the next page, the Google Cloud SQL connection is not available during the build phase. Thus, I had to put these commands out for execution after building successfully. In the local environment, this application attempts to connect with the local database instead of Google Cloud SQL.

- [https://github.com/GoogleCloudPlatform/community/issues/351#issuecomment-370056462](https://github.com/GoogleCloudPlatform/community/issues/351#issuecomment-370056462)

### 1-2. More detail of Dockerfile

Let’s see `Dockerfile.local` again. I can’t explain everything, but I’ll try to add descriptions briefly. To build an image on Google Cloud Run, shell scripts expose port of 8080.

```bash
EXPOSE 8080
...
RUN echo "Listen 8080" >> /etc/apache2/ports.conf
```

This command copies `000-default.conf` to configure the Apache WEB server.

```bash
COPY docker/000-default.conf /etc/apache2/sites-available/000-default.conf
```

## 2. Deploy Laravel application to Google Cloud Run

Now let's deploy the Laravel application from the local side. First, let docker containers be down.

```bash
$ docker-compose down
```

### 2-1. Set up GCP with console

Execute the following commands with checking project name in GCP.

```bash
$ gcloud auth login
$ PROJECT_NAME=(<Project name in GCP>)
PROJECT_ID=$(gcloud projects list --format 'value(projectId)' --filter name=$PROJECT_NAME)
export PROJECT_ID
gcloud config set project $PROJECT_ID
gcloud services enable \
    containerregistry.googleapis.com \
    cloudresourcemanager.googleapis.com \
    iam.googleapis.com
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91yehhrgurr5v2t25yhh.png)

### 2-2. Create an authorized service account

Create a service account and give it Owner privileges. Download the key, save it as laravel8_google_cloud_run_key.json, set it as an environment variable, and authorize it as a service account. The following command makes it possible.

```bash
$ gcloud iam service-accounts create deployer \
    --display-name "deployer"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member serviceAccount:deployer@${PROJECT_ID}.iam.gserviceaccount.com \
  --role roles/owner
export GOOGLE_APPLICATION_CREDENTIALS=credentials/laravel8_google_cloud_run_key.json
gcloud iam service-accounts keys create $GOOGLE_APPLICATION_CREDENTIALS \
  --iam-account deployer@${PROJECT_ID}.iam.gserviceaccount.com
gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS
```

In the above example command, `laravel8_google_cloud_run_key.json` is stored in the credentials directory.

```bash
export GOOGLE_APPLICATION_CREDENTIALS=~/credentials/laravel8_google_cloud_run_key.json
```

### 2-3. Build docker image

Build docker image instead of `docker-compose` command.

```bash
$ export SERVICE_NAME=docker-laravel8-run
docker build -f docker/Dockerfile.prod -t asia.gcr.io/${PROJECT_ID}/${SERVICE_NAME} .
```

Run a docker container to work out correctly on the local environment.

```bash
$ docker run --init -d -e PORT=8080 -p 8080:8080 asia.gcr.io/${PROJECT_ID}/${SERVICE_NAME}
```

Make sure the local page works correctly.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hel7j4hsoi54mttq2dei.png)

### 2-4. Push the docker image to Container Registry

```bash
$ gcloud auth configure-docker
docker push asia.gcr.io/${PROJECT_ID}/${SERVICE_NAME}
```

You’ll make sure to register a repository named docker-laravel8.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j7f1uh9y82k3ki2s10x.png)

### 2-5. Enable Cloud SQL Admin API

To connect Google Cloud Run and Google Cloud SQL, try to enable Cloud SQL Admin API.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cekhdgnrvr1kkxlghxrx.png)

- [Connect from Cloud Run | Cloud SQL for MySQL | Google Cloud](https://cloud.google.com/sql/docs/mysql/connect-run?hl=en#private-ip)

## 3. Google Cloud SQL

Create a Google Cloud SQL instance temporarily for test. I’ll create an instance like the following example in this sample, but you can modify configuration for your environment. This process might take a long time.

```bash
$ DATABASE_NAME=(docker-laravel8-run)
gcloud sql instances create $DATABASE_NAME \
  --database-version=MYSQL_8_0 \
  --region=us-central1 \
  --storage-size=100 \
  --storage-type=HDD \
  --tier=db-f1-micro \
  --root-password=homestead
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyc8r2yndlngxtlvr4hy.png)

- [How to securely connect to Cloud SQL from Cloud Run? - Stack Overflow](https://stackoverflow.com/questions/55693241/how-to-securely-connect-to-cloud-sql-from-cloud-run)
- [How do I connect to Cloud SQL from a Laravel project running on Cloud Run? - Stack Overflow](https://stackoverflow.com/questions/65253179/how-do-i-connect-to-cloud-sql-from-a-laravel-project-running-on-cloud-run)

It would be better to create a database named laravel.

```bash
$ gcloud sql databases create laravel \
--instance=$DATABASE_NAME
```

- [Create and manage databases | Cloud SQL for MySQL | Google Cloud](https://cloud.google.com/sql/docs/mysql/create-manage-databases#gcloud)

### 3-1. Deploy to Google Cloud Run

Let’s try to deploy to Google Cloud Run. Replace `<Connection name>` to the Connection name in Google Cloud SQL and execute the following command.

```bash
$ SQL_CONNECTION_NAME=(<Connection name>)
gcloud run deploy ${SERVICE_NAME} \
--image asia.gcr.io/"${PROJECT_ID}"/"${SERVICE_NAME}" \
--region asia-east1 \
--platform=managed \
--add-cloudsql-instances ${SQL_CONNECTION_NAME} \
--allow-unauthenticated
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ib5copxtjkm2p5hd3rwp.png)

A URL will be generated after deploying. Make sure to be shown the page for Laravel8 correctly.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wnpqs9k4s8cx16369ntb.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhzmjugi9yvkxci0xuzx.png)

## 4. Deploy from GitHub Actions

### 4-1. Brief description

Now we’ve deployed the Laravel application successfully from the Local environment. Then, let’s consider deploying from GitHub Actions. I’m assumed that GitHub Actions runs commands to deploy when commits are pushed to the master or main branch. In this example, the deploy command is defined in `.github/workflows/deploy.yml` file. And the trigger for deployment is set when pushing to `master`.

```yaml
name: cloud-run-laravel8-deploy
on:
  push:
    branches:
      - master
```

Let’s see some more. This remote repository has no .env file to configure the Laravel application by the `.gitignore` file. So we should put `.env` file only when deploying. Then, by encoding an environment variable decoded as base64 of the `.env` file on the local, it’s available to deploy.

```yaml
- name: Decode .env file
  run: |
    echo ${{ secrets.TEST_ENV_FILE }} | base64 --decode > src/.env
```

There are six environment variables in `.github/workflows/deploy.yml` file.

- secrets.TEST_GCP_EMAIL: deploy email
- secrets.TEST_GCP_CREDENTIALS: the content of `credentials/laravel8_google_cloud_run_key.json` in your local environment
- secrets.TEST_ENV_FILE: `env/.env.prod`
- secrets.TEST_GCP_PROJECT: project id
- secrets.TEST_GCP_APPLICATION: service name
- secrets.TEST_SQL_CONNECTION_NAME: SQL connection name

### 4-2. Change remote repository

Then, change the remote branch to `test-cloud-run` . The name can be anything you like.

```bash
$ git config remote.origin.url
git@github.com:uuta/cloud-run-laravel8.git
$ git remote set-url origin git@github.com:uuta/test-cloud-run.git
$ git config remote.origin.url
```

### 4-3. Enter the environment variables

Enter the environment variables on GitHub like this.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50qxydonckpgxuoecbwb.png)

TEST_GCP_EMAIL can be obtained from the Google Cloud Run UI.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xlcspndc62c59egmt0xz.png)

Modifying TEST_ENV_FILE is somewhat tricky. First of all, copy the .env.example file as .env.prod.

```bash
$ cp env/.env.local env/.env.prod
```

Modify the .env.prod file as follows, and don't forget to add DB_SOCKET.

```bash
APP_NAME=Laravel
APP_ENV=production
APP_KEY=<your app key>
APP_DEBUG=false
APP_URL=<your app url>

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=homestead
DB_SOCKET=/cloudsql/<connection name to Google Cloud SQL>
```

By the following command, values in the `env` file will transfer to string encoded Base64. So you can copy and paste it as the value of TEST_ENV_FILE.

```bash
$ cat env/.env.prod | base64 | pbcopy
```

### 4-4. Git push, and then deploy automatically

```bash
$ git push
```

After pushing the master repository, it will automatically start deploying in your remote repository. Please check if it succeeds.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5b765bzqyd6bbotsqx6j.png)

You made it!

## References

- [CircleCI 2.0で.envファイルを環境変数として使う方法 | Casual Developers Note](https://casualdevelopers.com/tech-tips/how-to-use-dotenv-with-base64-on-circleci/)
- [Cloud RunからCloud SQLやMemorystoreへの繋ぎ方 - オールアバウトTech Blog](https://allabout-tech.hatenablog.com/entry/2020/06/30/133859)
- [Cloud Runを使うならこれだけ知ってると得した気分になる - Qiita](https://qiita.com/chimame/items/e959843e86419f51e45a)
- [About connection options | Cloud SQL for MySQL | Google Cloud](https://cloud.google.com/sql/docs/mysql/connect-overview?hl=en#tools_for_connecting)