Most tutorials that I found only show how to deploy simple static apps. The following article describes how to build a gitlab pipeline for more complicated nuxt v2 apps including
- Environment variables
- server target
- Parts of AutoDevops for testing
The complete resulting .gitlab-ci.yaml can be found at the bottom of this article.
app.yaml and environment variables
First, to deploy an app to Google App Engine (GAE), we need an app.yaml
file.
A basic one can be copied from Nuxt’s official documentation.
GAE currently does not really support environment variables out of the box. But there is a way to still make it work as described in this article.
The goal is to use a different set of environment variables for production (master branch) then for all other branches.
This can be achieved in the following 4 steps.
Set placeholder variables in app.yaml
This might look as follows:
env_variables:
HOST: '0.0.0.0'
NODE_ENV: 'production'
VARIABLE1: _VARIABLE1
VARIABLE2: _VARIABLE2
Define Gitlab Environments
The gitlab environment defines which environment variables should be used. This is achieved by adding the following lines at the beginning of my .gitlab-ci.yaml:
variables:
ENVIRONMENT: "dev"
workflow:
rules:
- if: '$CI_COMMIT_REF_NAME == "master"'
variables:
ENVIRONMENT: "prod"
- if: '$CI_COMMIT_REF_NAME != "master"'
when: always
These lines define the global variable ENVIRONMENT to “dev” by default and change it to “prod” if the current branch name is “master”.
Add Environment Variables to Gitlab
In your gitlab project navigate to “CI/CD settings -> Variable” and add values for VARIABLE1 and VARIABLE2. Each variable should be defined for each environment. Remember to tick the masked checkbox for passwords and other secrets.
Add Gitlab Job to replace Variable Placeholders
This can be done by adding the following job to our pipeline.
stages:
- setup
set_app_variables:
stage: setup
environment: $ENVIRONMENT
script:
- sed -i "s/_VARIABLE1/${VARIABLE1}/g" app.yaml
- sed -i "s/_VARIABLE2/${VARIABLE2}/g" app.yaml
- cat app.yaml
artifacts:
paths:
- app.yaml
expire_in: 1h
The job uses the script section to replace the placeholder values with sed. The values used depend on the value of the above defined ENVIRONMENT variable. The line cat app.yaml
is only for debugging purposes and shows the resulting file in the job logs. Masked variable values will be obfuscated in the logs.
The resulting app.yaml file with actual values is exposed as an artifact so that subsequent jobs can use it.
Build pipeline
Until now we only made sure that the right environment variables are used in GAE. The following section describes how to deploy the app to Google Cloud. As recommended here we will deploy the production and staging app on two separate Google cloud projects. In my case these projects already exist so I won’t go into the details of how to set them up.
Add a deploy service account
Has to be done for both Google Cloud projects.
Before building the pipeline add a new service account gitlab-appengine
with the roles described here. This account will be used when deploying the app to GAE. Generate a json key for this service account and save it as the environment variable ‘SERVICE_ACCOUNT’ in Gitlab under the corresponding environment.
Note: I removed all line breaks from the key file before saving it as an environment variable. Not sure if this is needed though.
Install dependencies
Add the following lines at the bottom of the .gitlab-ci.yaml:
install:
stage: setup
script:
- yarn install --frozen-lockfile
cache:
key:
files:
- yarn.lock
paths:
- node_modules
artifacts:
paths:
- node_modules
expire_in: 1h
This job pulls cached node_modules if they exist already. Then it does a yarn install without changing yarn.lock This way we ensure that the packages we used locally are the same that are installed with gitlab. That way the pipeline stays reproducible. A more detailed explanation can be found in this article. node_modules are then saved to the cache again and exposed as artifacts for subsequent jobs to download.
Build
Add the stage “build” .gitlab-ci.yaml, then add the following lines at the bottom:
build:
stage: build
environment: $ENVIRONMENT
dependencies:
- install
- set_app_variables
script:
- yarn run build
artifacts:
paths:
- .nuxt/
expire_in: 1h
The build step exposes the build directory which is needed to deploy an SSR application. Build variables can also be included here by again specifying the environment.
Deploy
Add the stage “deploy” .gitlab-ci.yaml, then add the following job at the bottom:
deploy:
image: google/cloud-sdk:alpine
stage: deploy
dependencies:
- build
- set_app_variables
environment: $ENVIRONMENT
script:
- gcloud auth activate-service-account --key-file $SERVICE_ACCOUNT
- gcloud app deploy --quiet --project=$PROJECT_ID --service-account=gitlab-appengine@$PROJECT_ID.iam.gserviceaccount.com
The deploy step is based on this article. The improvement is saving SERVICE_ACCOUNT as a file. That way we can skip creating and deleting a temporary json file each time the job runs.
The SERVICE_ACCOUNT and PROJECT_ID variables are again both dependend on the current environment. That ensures deployment to diffrent Google Cloud projects for staging and production.
Deploy rules
If you want to include build and deploy only for two specific branches you could add the only
keyword to both jobs specifying the branch names. I wanted to keep things a little more DRY so I added the following rules at the beginning of the .gitlab-ci.yaml
# Only include build and deploy steps if branch name is dev or master
.deploy_rules:
rules:
- if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME == "dev"'
Then include the rules to each job with the following lines:
rules:
- !reference [.deploy_rules, rules]
Full yaml file:
The following file also includes some Auto DevOps features from gitlab.
# https://dev.to/mungell/google-cloud-app-engine-environment-variables-5990 https://dev.to/drakulavich/gitlab-ci-cache-and-artifacts-explained-by-example-2opi
# https://cloud.google.com/appengine/docs/flexible/roles#recommended_role_for_application_deployment
image: node:20
.deploy_rules:
rules:
- if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME == "dev") && $CI_PIPELINE_SOURCE != "merge_request_event"'
variables:
ENVIRONMENT: "dev"
workflow:
rules:
- if: '$CI_COMMIT_REF_NAME == "master"'
variables:
ENVIRONMENT: "prod"
- if: '$CI_COMMIT_REF_NAME != "master"'
when: always
stages:
- setup
- build
- test
- deploy
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
include:
# - template: Jobs/Code-Quality.gitlab-ci.yml
- template: Jobs/Code-Intelligence.gitlab-ci.yml
# - template: Jobs/Browser-Performance-Testing.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
# https://about.gitlab.com/blog/2022/09/12/a-visual-guide-to-gitlab-ci-caching/
# Pull cached node_modules
# Install nodes packages without changing, Pulling
# new packages only happens if lockfile changed
# Push node_modules to cache
# Provide node_modules as artifacts for other jobs
install:
stage: setup
script:
- yarn install --frozen-lockfile
cache:
key:
files:
- yarn.lock
paths:
- node_modules
artifacts:
paths:
- node_modules
expire_in: 1h
set_app_variables:
stage: setup
environment: $ENVIRONMENT
script:
- sed -i "s/_FB_API_KEY/${FB_API_KEY}/g" app.yaml
- sed -i "s/_FB_PROJECT_ID/${FB_PROJECT_ID}/g" app.yaml
- sed -i "s/_FB_MESSAGING_SENDER_ID/${FB_MESSAGING_SENDER_ID}/g" app.yaml
- sed -i "s/_FB_APP_ID/${FB_APP_ID}/g" app.yaml
- sed -i "s/_VPC_CONNECTOR/${VPC_CONNECTOR}/g" app.yaml
- sed -i "s/_SQL_ID/${SQL_ID}/g" app.yaml
- sed -i "s/_PGHOST/${PGHOST}/g" app.yaml
- sed -i "s/_PGPORT/${PGPORT}/g" app.yaml
- sed -i "s/_PGUSER/${PGUSER}/g" app.yaml
- sed -i "s/_PGPASSWORD/${PGPASSWORD}/g" app.yaml
- sed -i "s!_BROWSER_BASE_URL!${BROWSER_BASE_URL}!g" app.yaml
- sed -i "s!_BREVO_API_KEY!${BREVO_API_KEY}!g" app.yaml
- sed -i "s!_POSTHOG_API_KEY!${POSTHOG_API_KEY}!g" app.yaml
- sed -i "s!_ZENDESK_SECRET!${ZENDESK_SECRET}!g" app.yaml
- sed -i "s!_MAINTENANCE_START!${MAINTENANCE_START}!g" app.yaml
- sed -i "s!_MAINTENANCE_END!${MAINTENANCE_END}!g" app.yaml
- cat app.yaml
artifacts:
paths:
- app.yaml
expire_in: 1h
# jest:
# stage: test
# coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
# dependencies:
# - install
# script:
# - yarn test:ci
build:
stage: build
environment: $ENVIRONMENT
dependencies:
- install
- set_app_variables
script:
- yarn run build
artifacts:
paths:
- .nuxt/
expire_in: 1h
rules:
- !reference [.deploy_rules, rules]
deploy:
image: google/cloud-sdk:alpine
stage: deploy
dependencies:
- build
- set_app_variables
environment: $ENVIRONMENT
script:
- gcloud auth activate-service-account --key-file=$SERVICE_ACCOUNT
- gcloud app deploy --quiet --project=$FB_PROJECT_ID --service-account=gitlab-appengine@$FB_PROJECT_ID.iam.gserviceaccount.com
rules:
- !reference [.deploy_rules, rules]