In this Post we will setup a React pipeline using Gitlab,Ansible and docker. we will go throught the whole process from nothing to a fast, reliable and hightly customizable pipeline with multi-environment deployment.
**Daah ! let's start i can't wait π !**
π
Tools
Before we start we need to define the damn π₯ tech stack :
Gitlab : GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager providing wiki, issue-tracking and continuous integration and deployment pipeline features.
Ansible : Ansible is the simplest way to automate apps and IT infrastructure. Application Deployment + Configuration Management + Continuous Delivery.
-
Docker: Docker is a tool designed to make it easier to create, deploy, and run applications by using containers.
Note: if you dont know nothing about those tools ... No problem .... aaah actually it's a problem π ... we are diving into advanced topic here π ...
πCome on ! i was kidding π
Actually no ...
contact me if you need some support
Architecture
yoo .. we have to draw the global environment architecture to get the whole picture about what we will do here π ... dont start coding directly. Daaah π€ ... you have to think by compiling the whole process in mind π
Of course we will create a repository ( i will not explain that π) on gitlab with a hello world react app ( i will not explain that π) and push it there.
Let's break down the architecture now :
Block 1 : this where our code application resides and the whole gitlab eco-system also, all configuration to start a pipeline must be there, actually you can install gitlab on your own servers .. but it's not the aim of this post.
Block 2: this is the important block for now (The CI environment) .. actually it is the server when all the dirty π© work resides like buiding docker containers .. saving cache ... testing code and so on ... we must configure this environment with love β€οΈ haha yeah with love ... it's is the base of the pipeline speed and low level configurations.
Block 3 : the target environments where we will deploy our application using ansible playbooks via a secure tunnel .. SSH ... BTW i love you SSH π because we will not install any runners on those targets servers we will interact with them only with ansible to ensure a clean deployment.
π
CI environment
In this section we will connect our gitlab repo to the CI environment machine and install the gitlab runner on it of course.
Go to your repo ... under
settings --> CI/CD --> runnersand get the gitlab url and the token associeted to ... dont loose it πYou should have a VPS or a virtual machine on the cloud ... i will work on an azure virtual machine with ubuntu 18.04 installed
Install docker of course ... it's simple come here
Installing the gitlab runner :
curl -LJO "https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_<arch>.deb" dpkg -i gitlab-runner_<arch>.deb Gitlab will be installed as service on your machine but i don't you can encounter a problem when starting it ... (don't ask me i don't know π ) so you can start it as follow :
gitlab runner run & # it will work on background You can now register the runner with gitlab-runner register and follow the instructions ... dont loose the token or reset it ... if you reset the token you have to re-register the runner again. i will make things easier ... here is my config.toml under /etc/gitlab-runner/config.toml
concurrent = 9 check_interval = 0 [session_server] session_timeout = 1800 [[runners]] name = "runner-name" url = "https://gitlab.com/" token = "runner-token" executor = "docker" limit = 0 [runners.custom_build_dir] [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure] [runners.docker] pull_policy = "if-not-present" tls_verify = false image = "alpine" privileged = true disable_entrypoint_overwrite = false oom_kill_disable = false disable_cache = false volumes = ["/cache:/cache"] shm_size = 0 let's make a breakdown here ...
This runner will run 9 concurent jobs on a docker containers (docker in docker) based on the alpine container (to make a clean build) ... The runner will pull new versions of images if they are not present ... This is optional you can turn it to always but we need to speed up the build ... No need to pull the same image again and again if there is no updates ... The runner will save the cache on the current machine under /cache on the host and pass it in use as a docker volume to save some minutes when gitlab by default upload the zipped cache to it's own storage and download it again ... It's painfull when the cache is becoming huge. At some point on time the cache will be so big .. So you can make your hand dirty and delete the shit π©
**We are almost done π**
Now you can go the repository under settings --> CI/CD --> runners and verify that the runner was registred successfully ( the green icon )
**. . .**
The react pipeline
let's code the pipeline now π .... wait a second !!! we need the architecture as the previous section ... so here is how the pipeline will look like ...
This pipeline aims to support the folowing features :
- Caching node modules for faster build
- Docker for shiping containers
- Gitlab private registry linked to the repo
- Ship only
/buildon the container with nginx web server - Tag containers with the git SHA-COMMIT
- Deploy containers with an ansible playbook
- SSH configuration as a gitlab secret to secure the target IP
- Only ssh keypairs used for authentication with the target server ... no damn passwords π© ...
**. . .**
Defining Secrets
This pipeline needs some variables to be placed in gitlab as secrets on settings --> CI/CD --> Variables :
| Variable name | Role | Type |
|---|---|---|
| ANSIBLE_KEY | The target server ssh private key π | file |
| GITLAB_REGISTRY_PASS | Gitlab registry password (your account password π) | variable |
| GITLAB_REGISTRY_USER | Gitlab registry login (your account user π) | variable |
| SSH_CFG | The regular ssh config that contains the target IP | file |
The SSH_CFG looks like this :
Host * StrictHostKeyChecking no Host dev HostName <IP> IdentityFile ./keys/keyfile User root Host staging HostName <IP> IdentityFile ./keys/keyfile User root Host prod HostName <IP> IdentityFile ./keys/keyfile User root I will not explain this π ... come here
**. . .**
**[KNOCK KNOCK](https://bongo.cat/) ... are you still here πΊ**
**. . .**
Preparing Dockerfile
Before writing the dockerfile take in mind that the steup should be compatible with the pipeline architecture ... if you remember we have a separate jobs for :
- Installing node modules
- Run the build process
So the Dockerfile must contain only the builded assets only to be served by nginx π
Here is our sweet π Dockerfile :
FROM nginx:1.16.0-alpine COPY build/ /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d RUN mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.old EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] This dockerfile does not do too much work, it just take the /build directory and copy it under /usr/share/nginx/html to be served.
Also we need a basic nginx config like follow to be under /etc/nginx/conf.d:
server { include mime.types; listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } You see ! π its simple let's proceed to setup the ansible playbook for the deployment process ... hurry up π
**. . .**
Deployment with ansible
We are almost done ! the task now is to write the ansible playbook that will do the folowing :
- Create a docker network and specify the the gateway address
- Authenticate the gitlab registry
- Start the container with the suitable configurations
- Clean the unsed containers and volumes
- Most setup will be in the
inventory file
Let's take a look at the inventory_file:
[dev] devserver ansible_ssh_host=dev ansible_ssh_user=root ansible_python_interpreter=/usr/bin/python [dev:vars] c_name={{ lookup('env','CI_PROJECT_NAME') }}-dev #container name h_name={{ lookup('env','CI_PROJECT_NAME') }}-dev #host name subnet=172.30.0 # network gateway network_name=project_name_dev registry_url={{ lookup('env','CI_REGISTRY') }} registry_user={{ lookup('env','GITLAB_REGISTRY_USER') }} registry_password={{ lookup('env','GITLAB_REGISTRY_PASS') }} image_name={{ lookup('env','CI_REGISTRY_IMAGE') }}:{{ lookup('env','CI_COMMIT_SHORT_SHA') }}-dev [project_network:children] dev [project_clean:children] dev The ansible_ssh_host=dev refers to the SSH_CFG configuration.
Gitlab by default exports many useful environment variables like :
-
CI_PROJECT_NAME: the repo name -
CI_COMMIT_SHORT_SHA: the sha commit ID to tag the container
You can explore all variables here.
Let's move now to the playbook ... i'm tired damn it haha .. it is a long post ... okay nevermind come on ..
Here is the ansible playbook :
--- - hosts: project_network #become: yes # for previlged user #become_method: sudo # for previlged user tasks: - name: Create docker network docker_network: name: "{{ network_name }}" ipam_config: - subnet: "{{ subnet }}.0/16" gateway: "{{ subnet }}.1" - hosts: dev gather_facts: no #become: yes # for previlged user #become_method: sudo # for previlged user tasks: - name: Log into gitlab registry and force re-authorization docker_login: registry: "{{ registry_url }}" username: "{{ registry_user }}" password: "{{ registry_password }}" reauthorize: yes - name : start the container docker_container: name: "{{ c_name }}" image : "{{ image_name }}" pull: yes restart_policy: always hostname: "{{ h_name }}" # volumes: # - /some/path:/some/path exposed_ports: - "80" networks: - name: "{{ network_name }}" ipv4_address: "{{ subnet }}.2" purge_networks: yes - hosts : project_clean #become: yes # for previlged user #become_method: sudo # for previlged user gather_facts : no tasks: - name: Removing exited containers shell: docker ps -a -q -f status=exited | xargs --no-run-if-empty docker rm --volumes - name: Removing untagged images shell: docker images | awk '/^<none>/ { print $3 }' | xargs --no-run-if-empty docker rmi -f - name: Removing volume directories shell: docker volume ls -q --filter="dangling=true" | xargs --no-run-if-empty docker volume rm This playbook is a life saver because we configure the container automatically before starting it ... no setup on the remote host ... we can deploy the same in any other servers based on linux. the container update is quite simple .. ansible will take care of stopping the container and starting new one with different tag and then clean up the shit π©
We can also make a rollback to the previous container by going to the previous pipeline history on gitlab and restart the lastest job the deploy job because we have already an existing container on the registry π
The setup is for dev environment you can copy paste the two files for the prod & staging environment ...
**. . .**
Setting up the Pipeline
The pipeline will deploy to the three environments as i mentioned on the top of this post ...
Here is the full pipeline code :
variables: DOCKER_IMAGE_PRODUCTION : $CI_REGISTRY_IMAGE DOCKER_IMAGE_TEST : $CI_REGISTRY_IMAGE DOCKER_IMAGE_DEV : $CI_REGISTRY_IMAGE #caching node_modules folder for later use .example_cache: &example_cache cache: paths: - node_modules/ stages : - prep - build_dev - push_registry_dev - deploy_dev - build_test - push_registry_test - deploy_test - build_production - push_registry_production - deploy_production ######################################################## ## ## ## Development: autorun after a push/merge ## ## ## ######################################################## install_dependencies: image: node:12.2.0-alpine stage: prep <<: *example_cache script: - npm ci --log-level=error artifacts: paths: - node_modules/ tags : - runner_name only: refs: - prod_branch - staging_branch - dev_branch changes : - "*.json" build_react_dev: image: node:12.2.0-alpine stage: build_dev <<: *example_cache variables: CI : "false" script: - cat .env.dev > .env - npm run build artifacts: paths: - build/ tags : - runner_name rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "dev_branch"' build_image_dev: stage: push_registry_dev image : docker:19 services: - docker:19-dind variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" before_script: # docker login asks for the password to be passed through stdin for security # we use $CI_JOB_TOKEN here which is a special token provided by GitLab - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY script: - docker build --tag $DOCKER_IMAGE_DEV:$CI_COMMIT_SHORT_SHA-dev . - docker push $DOCKER_IMAGE_DEV:$CI_COMMIT_SHORT_SHA-dev tags : - runner_name rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "dev_branch"' deploy_dev: stage: deploy_dev image: willhallonline/ansible:latest script: - cat ${SSH_CFG} > "$CI_PROJECT_DIR/ssh.cfg" - mkdir -p "$CI_PROJECT_DIR/keys" - cat ${ANSIBLE_KEY} > "$CI_PROJECT_DIR/keys/keyfile" - chmod og-rwx "$CI_PROJECT_DIR/keys/keyfile" - cd $CI_PROJECT_DIR && ansible-playbook -i deployment/inventory_dev --ssh-extra-args="-F $CI_PROJECT_DIR/ssh.cfg -o ControlMaster=auto -o ControlPersist=30m" deployment/deploy_container_dev.yml after_script: - rm -r "$CI_PROJECT_DIR/keys" || true - rm "$CI_PROJECT_DIR/ssh.cfg" || true rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "branch_dev"' tags : - runner_name ######################################################## ## ## ## pre-production: autorun after a push/merge ## ## ## ######################################################## build_react_test: image: node:12.2.0-alpine stage: build_test <<: *example_cache variables: CI : "false" script: - cat .env.test > .env - npm run build artifacts: paths: - build/ tags : - runner_name rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "staging_branch"' build_image_test: stage: push_registry_test image : docker:19 services: - docker:19-dind variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" before_script: # docker login asks for the password to be passed through stdin for security # we use $CI_JOB_TOKEN here which is a special token provided by GitLab - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY script: - docker build --tag $DOCKER_IMAGE_TEST:$CI_COMMIT_SHORT_SHA-test . - docker push $DOCKER_IMAGE_TEST:$CI_COMMIT_SHORT_SHA-test rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "staging_branch"' tags : - runner_name deploy_test: stage: deploy_test image: willhallonline/ansible:latest script: - cat ${SSH_CFG} > "$CI_PROJECT_DIR/ssh.cfg" - mkdir -p "$CI_PROJECT_DIR/keys" - cat ${ANSIBLE_KEY} > "$CI_PROJECT_DIR/keys/keyfile" - chmod og-rwx "$CI_PROJECT_DIR/keys/keyfile" - cd $CI_PROJECT_DIR && ansible-playbook -i deployment/inventory_test --ssh-extra-args="-F $CI_PROJECT_DIR/ssh.cfg -o ControlMaster=auto -o ControlPersist=30m" deployment/deploy_container_test.yml after_script: - rm -r "$CI_PROJECT_DIR/keys" || true - rm "$CI_PROJECT_DIR/ssh.cfg" || true rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "staging_branch"' tags : - runner_name ######################################################## ## ## ## Production: must be deployed manually ## ## ## ######################################################## build_react_production: image: node:12.2.0-alpine stage: build_production <<: *example_cache variables: CI : "false" script: - cat .env.prod > .env - npm run build artifacts: paths: - build/ tags : - runner_name rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "prod_branch"' when: manual build_image_production: stage: push_registry_production image : docker:19 services: - docker:19-dind variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" before_script: # docker login asks for the password to be passed through stdin for security # we use $CI_JOB_TOKEN here which is a special token provided by GitLab - echo -n $CI_JOB_TOKEN | docker login -u gitlab-ci-token --password-stdin $CI_REGISTRY script: - docker build --tag $DOCKER_IMAGE_PRODUCTION:$CI_COMMIT_SHORT_SHA . - docker push $DOCKER_IMAGE_PRODUCTION:$CI_COMMIT_SHORT_SHA rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "prod_branch"' tags : - runner_name needs: [build_react_production] deploy_production: stage: deploy_production image: willhallonline/ansible:latest script: - cat ${SSH_CFG} > "$CI_PROJECT_DIR/ssh.cfg" - mkdir -p "$CI_PROJECT_DIR/keys" - cat ${ANSIBLE_KEY} > "$CI_PROJECT_DIR/keys/keyfile" - chmod og-rwx "$CI_PROJECT_DIR/keys/keyfile" - cd $CI_PROJECT_DIR && ansible-playbook -i deployment/inventory --ssh-extra-args="-F $CI_PROJECT_DIR/ssh.cfg -o ControlMaster=auto -o ControlPersist=30m" deployment/deploy_container.yml after_script: - rm -r "$CI_PROJECT_DIR/keys" || true - rm "$CI_PROJECT_DIR/ssh.cfg" || true rules: - if: '$CI_PIPELINE_SOURCE != "trigger" && $CI_COMMIT_BRANCH == "prod_branch"' tags : - runner_name needs: [build_image_production] Here is some notes about this pipeline:
The pipeline is protected by default to not be started with the trigger token ( Gitlab pipeline trigger)
The
prepstage will start if there is any modifications in any json file including thepackage.jsonfileThe pipeline jobs runs on docker alpine image (DinD) so we need some variables to connect to the docker host by using
DOCKER_HOST: tcp://docker:2375/andDOCKER_TLS_CERTDIR: ""The production deployment depends on the staging jobs to be succeeded and tested by the testing team. by default no auto deploy to prod ... it's manual !
I used some files to store application environment variables using
.env.dev,env.testand.env.prodyou can use what you want !Make sure to use a good docker image for the job based images .. for node i always work with
LTSversions.Create a
deploymentfolder to store the ansible playbooks and inventory files.Create a
Cron Jobto delete the cache every three months to clean the cache on theCI environment.-
On the target server make sure to install
docker,nginx,certbotanddocker python package**. . .**
Final thoughts
You can make this pipeline as template to deliver other kinds of projects like :
- Python
- Rust
- Node
- Go
I hope this post was helpful ! thanks for reading π it was great to share this with you, if you have any problems in setting this just let me know !


Top comments (1)
Great !!! Thanks