Learning Outcomes
- define continuous integration (CI)
- define continuous delivery (CD)
- define continuous deployment(CD)
- distinguish between development, testing (or staging), and production environments
- use webhooks to deploy a simple web application
Resources
- Continuous Integration & Continuous Deployment: CI/CD (video)
- What is Continuous Integration
- slides
- narated slides (video)
Lab
Video walkthrough of this lab.
Set up a SSH Key for GitLab
Start the Google Cloud Shell.
Generate a SSH key:
ssh-keygen -t ed25519 -C "YOUR EMAIL ADDRESS HERE"
Display the public key:
cat .ssh/id_ed25519.pub
Copy the output into your clipboard. Log in to GitLab. Go to your settings and then to the “SSH Keys” page. Paste your public key into the “Key” field. Click on the “Add key” button.
Create a Deployment Script for an Application
Create myapp/deploy.sh
with the following contents:
#!/bin/sh
cd $HOME
wget -O $3.tar.gz https://gitlab.com/api/v4/projects/$1/repository/archive?private_token=$2
tar zxf $3.tar.gz
cp -R *-master-*/* $3
rm -r $3.tar.gz *-master-*
cd $3
npm install
pm2 reload www
Make sure myapp/deploy.sh
has the execution bit set:
chmod +x ~/myapp/deploy.sh
Create a View for the Webhook
Create myapp/views/webhook.ejs
with the following contents:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
<p>Webhook</p>
</body>
</html>
This page will never be seen, but Gitlab still needs a valid response to know that the webhook succeeded.
Create a Route for the Webhook
Create myapp/routes/webhook.js
with the following contents:
var express = require('express');
var router = express.Router();
const { spawn } = require('child_process');
/* POST home page. */
router.post('/', function(req, res, next) {
console.log( req.body );
if ( process.env.WEBHOOK_TOKEN == req.headers['x-gitlab-token'] ) {
const projectID = req.body['project']['id']
const pathParts = req.body['project']['path_with_namespace'].split('/');
const projectSlug = pathParts[1];
const scriptPath = process.env.HOME +'/' + projectSlug + '/deploy.sh'
const subprocess = spawn(
scriptPath,
[
projectID,
process.env.GITLAB_PRIVATE_TOKEN,
projectSlug
],
{
detached: true,
stdio: 'ignore'
});
subprocess.unref();
} else {
var forbiddenError = new Error( 'invalid or missing x-gitlab-token header');
forbiddenError.status = 403;
throw forbiddenError;
}
res.render('webhook', { title: 'Express: webhook POST' });
});
module.exports = router;
Link the Webhook Route in the application
Require the webhook route in myapp/app.js
:
var webhookRouter = require('./routes/webhook');
Link the webhook route to the webhook path:
app.use('/webhook', webhookRouter);
Create a .gitignore File
Create myapp/.gitignore
with the following contents:
node_modules
Set Up a GitLab Project
In GitLab, go to “Projects | Your projects”. Click on the “New project” button. Choose “Create blank project”. For “Project name”, enter “myapp”. Click on the “Create project” button. Follow the instructions to “Push an existing folder”.
Go to the project settings page. Go to the “Webhooks” page. For the URL, enter http://NNN.NNN.NNN.NNN:8080/webhook
. For “Secret Token”, enter a random sequence of letters and numbers. For the list of triggers, make sure only “Push events” is checked. Make sure “Enable SSL verification” is unchecked.
Go to your GitLab profile settings page. Go to the “Access Tokens” page. For “Name”, enter “deploy”. For “Scopes”, make sure “read_api” and “read_repository” are checked. Click on the “Create personal access token” button. Record the generated token value. THIS IS THE ONLY TIME YOU CAN SEE IT.
Deploy Application by Hand to Bootstrap
SSH into the virtual machine:
gcloud compute ssh lab1 --zone YOUR_ZONE_GOES_HERE
Install wget:
sudo apt install wget
Make a note of project ID for your GitLab project. (It is located just below the project name on the project overview page). Retrieve the latest project archive (fill in your actual values for YOUR_PROJECT_ID
and YOUR_ACCESS_TOKEN
):
wget -O myapp.tar.gz https://gitlab.com/api/v4/projects/YOUR_PROJECT_ID/repository/archive?private_token=YOUR_ACCESS_TOKEN
Unpack the archive:
tar zxf myapp.tar.gz
We are going to need to re-start the application with the correct environment variables set so we will stop and delete the current application:
pm2 stop www
pm2 delete www
Copy the latest version of the application into the myapp
directory:
cp -R *-master-*/* myapp
Remove the archive and the unpacked directory:
rm -r myapp.tar.gz *-master-*
Change to application bin
directory:
cd myapp/bin
Re-start the application with the correct environment variables (substituting your actual values for YOUR_ACCESS_TOKEN
and YOUR_WEBHOOK_SECRET
):
GITLAB_PRIVATE_TOKEN=YOUR_ACCESS_TOKEN WEBHOOK_TOKEN=YOUR_WEBHOOK_SECRET PORT=8080 DEBUG=myapp:* pm2 start www
View the streaming logs for the application:
pm2 logs
Verify that the Webhook Works
In a new tab, browse to the application using the external IP address for the virtual machine (http://NNN.NNN.NNN.NNN:8080
).
Back in the Google Cloud Shell environment, change the title
property in myapp\routes\index.js
. Stage, commit, and push the changes to the repository. Verify that the change has be automatically deployed to http://NNN.NNN.NNN.NNN:8080
by refreshing the page.
Assignment
- Add a new route to the application,
/guestbook
. - In the router, create a global array variable that maintains the list of vistors.
- For a
GET
request, display the contents of the array, one per line. Also, display a form where the user can enter their name and submit the form (as aPOST
request). - For a
POST
request, add the new name to the array and re-display the array. - Stage, commit, and push your changes.
- Confirm that the changes are properly deployed to the virtual machine.