This was originally a full-long post, but it got so big, that I had to split it into 2! This continues from the post #2, Configure GitLab CI on AWS EC2 Using Docker.
Posts:
- [TutorialâââGuide] Installing GitLab, GitLab CI on AWS EC2 from Zero.
- Configure GitLab CI on AWS EC2 Using Docker
- Configuring .gitlab-ci.yml (This Post)
- Troubleshooting GitLab and GitLab CI
#1- Understanding the .gitlab-ci.yml file
The .gitlab-ci.yml
file is a YAML file that you create on your projectâs root. This file automatically runs whenever you push a commit to the server. This triggers a notification to the runner you specified in #3, and then it processes the series of tasks you specified. So if you push it 3 times, itâs going to run it 3 times! Thatâs why if youâre pushing multiple you either want a faster runner, or a separate runner per machine.
Note, since weâre using Docker, the tasks always start within a clean state of the image. This means that all the files and modifications that you put or do inside the .gitlab-ci.yml, will be reverted each time you push a commit to the server. You can avoid this by specifying caches.
The content of the files are composed of the keys that you could find in this page. Thereâs no order you need to follow, but be very careful about indentations. This may make up or break your project. You can check with an online YAML linter to see if it works before pushing.
Iâll be working on an example NodeJS application. With Karma as the test runner. Iâve posted a .gitlab-ci.yml I used in one of my past projects in this GitHub Gist. (Please check it out!)
This page can give you another perspective.
Breaking it down:
image: node:9.4.0
The image key grabs an image from the Docker Hub, and uses it as a base image. GitLab will base all the tests based off this image. If youâre doing a project in Ruby, Java, Go, PHP, etc. specify the correct image from the Docker Hub.
cache:paths:- node_modules/- .yarn
This creates a temporary cache folder that prevents from the node_modules
, and .yarn
to be recreated each CI run (Each time you make a commit).
before_script:
- apt-get update -qq && apt-get install
- another-command that will run after the one above
- you can keep adding lines and lines.
The before_script
tells GitLab to run whatever youâve specified before anything else. You can consider this as a preparation script.
#1.1 Understanding Stages
Stages are a series of steps that your code goes through in order for it to reach its final destination (Production). GitLab allows you to define any number of stages with any names. You do it by specifying it under the stage key, in the order you want them to run.
Then, GitLab will be running each one of them, step by step. If one of them fails, it prevents the following ones to run.
stages:
- build
- test
- staging
- openMr
- production
In the above portion, it will run the build
stage first, all the way up to production
.
#1.2 Defining the stagesâ actions in the .gitlab-ci.yml file
You define what the stage is going to run by first specifying a stage name parent key. This key can be named as you wish and can contain spaces.
For example:
Build My App:stage: buildtags:#- you_would_put_your_tag_in_here#- nodebefore_script:
- yarn config set cache-folder .yarn
-
yarn installscript:- npm run build
The stage name is Build My App
, and it specifies a key called stage
that refers to the stage you created earlier, in the stages list.
The before_script runs the same as the one we specified earlier, only in the context of the build
stage: Nothing will run until those scripts are executed.
In this case, weâre using yarn insead of npm, and itâs creating a cache folder that will contain all the yarn configuration, that will not be recreated each projectâs run (Each time you push to the repo)
#1.3 Tags
If you followed the previous article I wrote about âtagsâ (Point #3.1, from article 2), this is where we specify them! If the tags match the one we specify in the runner, then this will trigger the runner once its done. You specify each tag in its own line. If you refer to the example above, if you remove the #
before node
that means, that that specific stage will only work on the runners with the node tag. If you specify no tag (omit the tags
key) you can connect to the runner (as long as itâs not locked down to the current project).
Note: 5.4â5.6 Itâs a skim overview of the portions of the file. Iâm explaining with more detail WHY, how, and What are the contents in #7.
#1.4 Testing environment
Again, it doesnât matter how you name your stage. In this case, I just called it âTestâ to test.
Test:stage: testbefore_script:- yarn config set cache-folder .yarn- yarn installscript:
Installs Chrome
- wget -q -O â [https://dl-ssl.google.com/linux/linux\_signing\_key.pub](https://dl-ssl.google.com/linux/linux_signing_key.pub) | apt-key add -
- echo âdeb \[arch=amd64\] [http://dl.google.com/linux/chrome/deb/](http://dl.google.com/linux/chrome/deb/) stable mainâ | tee /etc/apt/sources.list.d/google-chrome.list
- apt-get update
- apt-get install google-chrome-stable -y
# Runs the tests.
- npm run test:karma-headless
Thereâs a lot going there. Continuous Integration methodology relies on tests that you run on your local machine. Those tests are accompanied by running them against the actual machine that youâre going to deploy.
Since this is very specific to Node and JavaScript (what my project is made of), I need to prepare the field so they can run perfectly. In this case, I use karma as the test runner to run all of my local tests. It requires of a local web browser, in this case, Google Chrome.
Therefore, I need to issue an installation command of Google Chrome (remember that each time we push, everything starts from a clean state), and run the tests.
If all the test succeed, GitLab will proceed automatically to the next section.
#1.5 Opening a Merge Request
# Remember to have the PRIVATE_TOKEN generated. This is only needed to be done once per project and not per user.# Once you add it (Needs Master privileges) as a Secret Variable, it should work.
Open Merge Request:# Got it from here: https://gitlab.com/tmaier/gitlab-auto-merge-request/blob/develop/.gitlab-ci.ymlimage: tmaier/gitlab-auto-merge-requeststage: openMrscript:- bash ./gitlab-deploy/auto-merge-request.sh # The name of the script
After our testing environment succeeds, we want GitLab to automatically open a merge request that we can successfully merge to master if it passes.
# 1.6 Staging and Production Environments
Deploy to Staging:stage: stagingbefore_script:
Generates to connect to the AWS unit the SSH key.
- mkdir -p ~/.ssh
- echo -e â$SSH_PRIVATE_KEYâ > ~/.ssh/id_rsa
Sets the permission to 600 to prevent a problem with AWS
that itâs too unprotected.
- chmod 600 ~/.ssh/id_rsa
- â[[ -f /.dockerenv ]] && echo -e âHost *\n\tStrictHostKeyChecking no\n\nâ > ~/.ssh/configâ
script:- bash ./gitlab-deploy/.gitlab-deploy.staging.shenvironment:name: staging
Exposes a button that when clicked take you to the defined URL:
url: [http://ec2-11-44-514-91.us-east-2.compute.amazonaws.com:3001](http://ec2-13-59-173-91.us-east-2.compute.amazonaws.com:3001)
Deploy to Production:stage: productionbefore_script:
Generates to connect to the AWS unit the SSH key.
- mkdir -p ~/.ssh
- echo -e "$SSH\_PRIVATE\_KEY" > ~/.ssh/id\_rsa
Sets the permission to 600 to prevent a problem with AWS
that it's too unprotected
- chmod 600 ~/.ssh/id\_rsa
- '\[\[ -f /.dockerenv \]\] && echo -e "Host \*\\n\\tStrictHostKeyChecking no\\n\\n" > ~/.ssh/config'
script:- bash ./gitlab-deploy/.gitlab-deploy.prod.shenvironment:name: production# Exposes a button that when clicked take you to the defined URL:url: http://ec2-13-59-173-91.us-east-2.compute.amazonaws.com:81
when: manual
I will talk about the contents of this in the bottom.
#2 Pipelines
When you push the git repo to GitLab with the .gitlab-ci.yml
file on it, it will automatically trigger the pipelines. The pipelines are the stages you defined in your .gitlab-ci.yml.
In our case, we have build, test, staging, openMr, and production. Each of those marks that you see in the screenshot above represent each of the stages. A red cross will represent a failed stage. A green checkmark will represent that the test successfully passed. The diagonal bar will identify that the test was canceled.
You can see a command line interface that shows you the development of each of the stages by clicking on the icon, and then clicking on the pop up:
This is the screen that it shows you after the build stage has been successfully completed.
#3- Integration to AWS. How to connect the GitLab instance to the EC2 instance with your project
One of the biggest challenges is to integrate the CI pipeline with your project. As far as I know, GitLab doesnât offer a native way to do this. You could push your code to AWS Code Deploy and then do the migration through there.
Thereâs a fantastic step by step guide that will guide you through that process by the stackoverflowâs autronix:
How to deploy with Gitlab-Ci to EC2 using AWS CodeDeploy/CodePipeline/S3_I have created a set of sample files to go with the Guide provided below._stackoverflow.com
I recommend the approach above over the one that Iâm going to show you. Do the following if by any chance the approach from autronix is not working.
This integration conveys utilizing git (We pull the merged repository from GitLab) and upgrade it in place in our EC2 instance, we execute the reload script from npm (Weâre assuming weâre using Node in this project) and release the changes. You may see, by now, that this looks more of a hack than an actual solution. This may not work on highly distributed environments in which you require to replicate the codebase across multiple EC2 instance. But again, itâs about having options, right?
#4âââPrepare the EC2 machine that hosts the deployed code.
This approach is inspired by this post.
We treat the EC2 machine, the one that hosts the code in production, as a GitLab client. We create an SSH key that connects to GitLab, and pull the code from there.
If you remember, from the first tutorial:
ssh-keygen -t rsa -C âyour_name@your_email.comâssh-add ~/.ssh/id_rsa
If you had problems with adding the key, try executing this first (Source)Â :
eval `ssh-agent -s`ssh-add ~/.ssh/id_rsa
One thing about this approach is that I havenât found a way to make it work with a passphrase, so when it asks you about it, leave it blank!
When you create the key, itâs located under:
~/.ssh/id_rsa.pub
Note, this time you wonât be able to copy to the clipbpoard its content unless you install âclipâ
Note: This will eat 300+ MB of disk space. Donât do it unless youâre not constrained by disk space.
sudo apt-get install clip
cat ~/.ssh/id_rsa.pub | clip
Another option is just to execute cat, and copy the output from the command.
cat ~/.ssh/id_rsa.pub
To control potential security risks, I recommend you to create a separate user in GitLab that handles only a pull from the repo, and nothing else. You attach the public key to that account.
I created a ghost user in GitLab that handles the pull from GitLab.
Go to /admin on your GitLab address. (You can also click the tool icon at the navbar)
Create a new user:
Uncheck âcan create groupâ. Access level âRegularâ, External âcheckedâ.
Go to the project you have your repo with:
Search the new member you created and set it as a Reporter.
Navigate to the Users tab in the Admin area, and click on the name of the recently created user:
Click âImpersonateâ, and go to the ssh keys page.
#5- The Staging environment, configuration:
This is where I start explaining what the heck was what I put above.
Deploy to Staging:stage: stagingbefore_script:# Generates to connect to the AWS unit the SSH key.- mkdir -p ~/.ssh- echo -e â$SSH_PRIVATE_KEYâ > ~/.ssh/id_rsa# Sets the permission to 600 to prevent a problem with AWS# that itâs too unprotected.- chmod 600 ~/.ssh/id_rsa- â[[ -f /.dockerenv ]] && echo -e âHost *\n\tStrictHostKeyChecking no\n\nâ > ~/.ssh/configâ
script:- bash ./gitlab-deploy/.gitlab-deploy.staging.sh
environment:name: staging# Exposes a button that when clicked take you to the defined URL:url: http://ec2-13-14-444-91.us-east-2.compute.amazonaws.com:3001
#5.1- Communicating with the EC2 instance.
We need a way to communicate with the AWS unit. The way we do this is by grabbing the private key (Careful! Sensitive information) of the generated id_rsa (The one we generated inside our EC2 instance) and sending it with our predefined shell script (Which Iâll talk more about in a moment).
The code in the before_script what it does is that it generates a blank file called id_rsa (Which matches the convention for the private key). We populate it with a custom variable (more on that now) each time the project is run.
GitLab CI allows you to store variables in the projectâs settings:
Go to Your Project -> Settings -> CI/CD -> Secret Variables
What weâre going to do is that weâre going to grab the content from the id_rsa (private key, the one without .pub) and weâre going to copy and paste its content.
We do the same procedure as we did with the public file (Note this is the extension-less id_rsa):
cat ~/.ssh/id_rsa | clip
Or, if you didnât install clip, copy and paste it from the console:
cat ~/.ssh/id_rsa
Weâre going to copy paste that value in the âSecret Variablesâ form, and give it a âSSH_PRIVATE_KEYâ (This matches the one in .gitlab-ci.yml
and you can see it in the image above)
Once you have it, click âSave variablesâ.
#5.2- Create a Shell Script for the Staging environment
We still need to indicate GitLab to execute the pull request into our EC2 environment.
For separation of concerns, and maintainability we can specify an external shell file that will execute the pull from the master branch. We call this file .gitlab-deploy.staging.sh
You can call this file anything you want. Just remember to specify it in the .gitlab-ci.yml
file.
This is how I have my project structured.
.gitlab-ci.yml
is in the root. While the shell files are under a folder called gitlab-deploy
. Therefore we reference them as ./gitlab-deploy/.gitlab-deploy.staging.sh
.
The content of the file is as follows:
# !/bin/bash
# Get servers list:set â f
# Variables from GitLab server:# Note: They canât have spaces!!string=$DEPLOY_SERVERarray=(${string//,/ })# Iterate servers for deploy and pull last commit# Careful with the ; https://stackoverflow.com/a/20666248/1057052for i in â${!array[@]}â; doecho âDeploy project on server ${array[i]}âssh ubuntu@${array[i]} âcd ./Staging/vr && git stash && git checkout $CI_BUILD_REF_NAME && git stash && git pull && sudo yarn install && sudo npm run stagingâ
done
As you can see what weâre doing in here is that weâre executing a git pull and an installation of the packages in the Staging Server.
I was cheap, and I was running both: production and testing on the same server (I exposed different ports). I recommend you to have different machines for that.
The $DEPLOY_SERVER
variable is another custom variable we created in the secrets variable page with the IPv4 address of our EC2 instance:
Go to Your Project -> Settings -> CI/CD -> Secret Variables
The environment key which specifies name and url is âjust for showâ. This will show you a button at the stage in the console that will point you the URL you specify there. This is optional and can be omitted.
#6 Automatically Open Merge Requests
GitLab wonât automatically open merge requests. Thatâs why we have to do some work ourselves to get it working.
This is done through a Docker image from tmaier from GitHub
# Remember to have the PRIVATE_TOKEN generated. This is only needed to be done once per project and not per user.# Once you add it (Needs Master privileges) as a Secret Variable, it should work.
Open Merge Request:# Got it from here: https://gitlab.com/tmaier/gitlab-auto-merge-request/blob/develop/.gitlab-ci.ymlimage: tmaier/gitlab-auto-merge-requeststage: openMrscript:- bash ./gitlab-deploy/auto-merge-request.sh # The name of the script
This is the auto-merge-request.sh
file
#!/usr/bin/env bashset -e
# Gotten from:# https://about.gitlab.com/2017/09/05/how-to-automatically-create-a-new-mr-on-gitlab-with-gitlab-ci/# This shall automatically create a merge request right after the build has been pushed.# Added some touches from: https://gitlab.com/tmaier/gitlab-auto-merge-request/blob/develop/merge-request.sh
if [ -z â$PRIVATE_TOKENâ ]; thenecho âPRIVATE_TOKEN not setâecho âPlease set the GitLab Private Token as PRIVATE_TOKENâexit 1fi
# Extract the host where the server is running, and add the URL to the APIs[[ $CI_PROJECT_URL =~ ^https?://[^/]+ ]] && HOST=â${BASH_REMATCH[0]}/api/v4/projects/â
# Look which is the default branchTARGET_BRANCH=`curl â silent â${HOST}${CI_PROJECT_ID}â â header âPRIVATE-TOKEN:${PRIVATE_TOKEN}â | jq â raw-output â.default_branchâ`;
# The description of our new MR, we want to remove the branch after the MR has# been closedBODY=â{\âid\â: ${CI_PROJECT_ID},\âsource_branch\â: \â${CI_COMMIT_REF_NAME}\â,\âtarget_branch\â: \â${TARGET_BRANCH}\â,\âremove_source_branch\â: true,\âtitle\â: \âWIP: ${CI_COMMIT_REF_NAME}\â,\âassignee_id\â:\â${GITLAB_USER_ID}\â}â;
# Require a list of all the merge request and take a look if there is already# one with the same source branchLISTMR=`curl â silent â${HOST}${CI_PROJECT_ID}/merge_requests?state=openedâ â header âPRIVATE-TOKEN:${PRIVATE_TOKEN}â`;COUNTBRANCHES=`echo ${LISTMR} | grep -o â\âsource_branch\â:\â${CI_COMMIT_REF_NAME}\ââ | wc -l`;
# No MR found, letâs create a new oneif [ ${COUNTBRANCHES} -eq â0â ]; thencurl -X POST â${HOST}${CI_PROJECT_ID}/merge_requestsâ \â header âPRIVATE-TOKEN:${PRIVATE_TOKEN}â \â header âContent-Type: application/jsonâ \â data â${BODY}â;
echo âOpened a new merge request: WIP: ${CI_COMMIT_REF_NAME} and assigned to youâ;exit;fi
echo âNo new merge request openedâ;
For this to work, we need to generate a PRIVATE_TOKEN
which itâs just a random token we can generate. To have a strong and secure token we can use a password generator or anything else (your choice!).
Put the contents inside the âSecret Variablesâ as PRIVATE_TOKEN
Go to Your Project -> Settings -> CI/CD -> Secret Variables
#7 Deploy to Production
Itâs very similar to the Staging process, but here lies a difference. The main difference between a CI (Continuous Integration) and Continuous Deployment (CD) approach, is that the latter, any change that you do to the code, automatically gets pushed to production.
In GitLab we can specify if we manually deploy it to production or not by specifying a âwhenâ key.
Deploy to Production:stage: productionbefore_script:
Generates to connect to the AWS unit the SSH key.
- mkdir -p ~/.ssh
- echo -e "$SSH\_PRIVATE\_KEY" > ~/.ssh/id\_rsa
Sets the permission to 600 to prevent a problem with AWS
that it's too unprotected
- chmod 600 ~/.ssh/id\_rsa
- '\[\[ -f /.dockerenv \]\] && echo -e "Host \*\\n\\tStrictHostKeyChecking no\\n\\n" > ~/.ssh/config'
script:- bash ./gitlab-deploy/.gitlab-deploy.prod.shenvironment:name: production# Exposes a button that when clicked take you to the defined URL:url: http://ec2-13-59-173-91.us-east-2.compute.amazonaws.com:81
when: manual
As you can see by specifying the key when:manual
we tell GitLab not to push the code automatically to production, and wait for our commands.
On the pipelines page, you click a Playback button to âDeploy to Productionâ, which is the name you specified in the .gitlab-ci.yml
For last, but not least, check the .gitlab-deploy.prod.sh
# !/bin/bash
# Get servers list:set â f
# Variables from GitLab server:# Note: They canât have spaces!!string=$DEPLOY_SERVERarray=(${string//,/ })
# Iterate servers for deploy and pull last commit# Careful with the ; https://stackoverflow.com/a/20666248/1057052for i in â${!array[@]}â; doecho âDeploy project on server ${array[i]}âssh ubuntu@${array[i]} âcd ./Pardo/vr && git stash && git checkout $CI_BUILD_REF_NAME && git stash && git pull origin master && sudo yarn install && sudo npm run productionâ
done
If you notice, you are going to see that itâs similar (not to say identical) as the staging one. With the exception that I point it to a different location within my EC2 instance, where the production code lies. You are free to modify this file as well.
#8 The CI/CD pipeline has been configured! Time to push!
Yes!! Itâs finally that time!
Commit your file and push to your GitLab instance!
See how your changes start to happen!
Thatâs it!
WOAH! Thanks for the ride.