Setup CI/CD
Pipeline

Setup CI/CD Pipeline

Now we need to setup the pipline stages in the .gitlab-ci.yml file. This file acts as the control file for defining all CI/CD jobs.

We also need to define the test cases that will be used to validate changes to the staging and production environments.


Step 1 - Review GitLab CI Variables and Define Variable

In Gitlab, you need to review and define the variables that will be used in the CI/CD pipeline as environment variables. These variables are used to store sensitive information such as usernames and passwords, which should not be hardcoded in the pipeline file and are similar to in usage as to the secrets.sh file you created in the previous part of the lab but did not store in git.

To save you time, several variables have been pre-defined. Your task is to add the remaining variable needed.

  1. Hover over the Settings tab, then click on CI/CD in the left navigation pane.




  2. Click the arrow next to Variables to expand the section.




  3. Click on Add variable on the right side of the CI/CD Variables table header. It's located directly above the "Actions" column.

  4. A new "Add variable" form will appear. Fill in the following details:
    • Type: Variable (default)
    • Environment: All (default)
    • Visibility: Masked
    • Protect variable: Unchecked
    • Expand variable reference: Checked
    • Key: ND_PASSWORD
    • Value: cisco.123

  5. Click on the Add variable button at the bottom of the form to save the variable.





  6. Verify your ND_PASSWORD variable was saved and your variables match the screenshot below.




Step 2 - Create The GitLab CI/CD Pipeline Job Control File

The pipeline defines the stages of the CI/CD workflow. Some stages should be triggered when a PR is created, and some should be triggered when the code is merged. Different VCSs have different methods to define the pipeline. Since we are using Gitlab, the pipeline stages are defined in file .gitlab-ci.yml in the project root folder.


touch ~/workspace/ndfclab/nac/.gitlab-ci.yml


Step 3 - Open GitLab CI/CD Pipeline Job Control File


code-server -r ~/workspace/ndfclab/nac/.gitlab-ci.yml


Step 4 - Copy and Paste The Following Into the GitLab Job Control File


---
image:
  name: mtarking/ubuntu:22.04

variables:
  FF_SCRIPT_SECTIONS: true
  ANSIBLE_FORCE_COLOR: true
  ANSIBLE_PERSISTENT_COMMAND_TIMEOUT: 1000
  ANSIBLE_PERSISTENT_CONNECT_TIMEOUT: 1000
  ND_USERNAME:
    description: "Cisco ND Username"
  ND_PASSWORD:
    description: "Cisco ND Password"
  NDFC_SW_USERNAME:
    description: "Cisco NDFC Switch Username"
  NDFC_SW_PASSWORD:
    description: "Cisco NDFC Switch Password"

stages:
  - lint
  - deploy
  - test



yamllint:
  stage: lint
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
    - if: $CI_COMMIT_BRANCH
  before_script:
    - python3 -m pip install -r requirements.txt
  script:
    - echo "Checking YAML files..."
    - |-
        if [[ "$CI_PIPELINE_SOURCE" == "merge_request_event" && "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "main" ]] || \
           [[ -n "$CI_COMMIT_BRANCH" ]]; then
            set -o pipefail && yamllint -d relaxed ./host_vars/fabric-stage |& tee yamllint_fabric_stage.txt
        fi
    - |-
        if [[ "$CI_PIPELINE_SOURCE" == "merge_request_event" && "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "main" ]] || \
           [[ -n "$CI_COMMIT_BRANCH" ]]; then
            set -o pipefail && yamllint -d relaxed ./host_vars/fabric-external-stage |& tee yamllint_stage_external.txt
        fi
    - |-
        if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
            set -o pipefail && yamllint -d relaxed ./host_vars/fabric-prod |& tee yamllint_fabric_prod.txt
        fi
    - |-
        if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
            set -o pipefail && yamllint -d relaxed ./host_vars/fabric-external-prod |& tee yamllint_prod_external.txt
        fi
    - set -o pipefail && yamllint -d relaxed ./vxlan.yml |& tee yamllint_vxlan.txt
  artifacts:
    when: on_failure
    paths:
      - ./yamllint_*.txt

ansible_lint:
  stage: lint
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
    - if: $CI_COMMIT_BRANCH
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-lint -p vxlan.yml |& tee ansible_lint_vxlan.txt
  artifacts:
    when: always
    paths:
      - ./ansible_lint_*.txt

nac_validate:
  stage: lint
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
    - if: $CI_COMMIT_BRANCH
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - |-
        if [[ "$CI_PIPELINE_SOURCE" == "merge_request_event" && "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "main" ]] || \
           [[ -n "$CI_COMMIT_BRANCH" ]]; then
            set -o pipefail && ansible-playbook -i hosts.stage.yml -l fabric-stage validate.yml |& tee nac_validate_fabric_stage.txt
        fi
    - |-
        if [[ "$CI_PIPELINE_SOURCE" == "merge_request_event" && "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" == "main" ]] || \
           [[ -n "$CI_COMMIT_BRANCH" ]]; then
            set -o pipefail && ansible-playbook -i hosts.stage.yml -l fabric-external-stage validate.yml |& tee nac_validate_fabric_stage.txt
        fi
    - |-
        if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
            set -o pipefail && ansible-playbook -i hosts.prod.yml -l fabric-prod validate.yml |& tee nac_validate_fabric_prod.txt
        fi
    - |-
        if [[ "$CI_COMMIT_BRANCH" == "main" ]]; then
            set -o pipefail && ansible-playbook -i hosts.prod.yml -l fabric-external-prod validate.yml |& tee nac_validate_fabric_prod.txt
        fi
  artifacts:
    when: always
    paths:
      - ./nac_validate_*.txt



deploy_stage:
  stage: deploy
  needs:
    - yamllint
    - ansible_lint
    - nac_validate
  rules:
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-playbook -i hosts.stage.yml vxlan.yml -l fabric-stage |& tee deploy_fabric_stage.txt
  artifacts:
    when: always
    paths:
      - ./deploy_*.txt

deploy_stage_external:
  stage: deploy
  needs:
    - yamllint
    - ansible_lint
    - nac_validate
  rules:
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-playbook -i hosts.stage.yml vxlan.yml -l fabric-external-stage |& tee deploy_stage_external.txt
  artifacts:
    when: always
    paths:
      - ./deploy_*.txt

test_stage:
  stage: test
  needs:
    - deploy_stage
  rules:
    - if: ($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main")
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-playbook -i hosts.stage.yml test.yml -l fabric-stage |& tee test_fabric_stage.txt
  artifacts:
    when: always
    paths:
      - ./test_*.txt



deploy_prod:
  stage: deploy
  needs:
    - yamllint
    - ansible_lint
    - nac_validate
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-playbook -i hosts.prod.yml vxlan.yml -l fabric-prod |& tee deploy_fabric_prod.txt
  artifacts:
    when: always
    paths:
      - ./deploy_*.txt

deploy_prod_external:
  stage: deploy
  needs:
    - yamllint
    - ansible_lint
    - nac_validate
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail && ansible-playbook -i hosts.prod.yml vxlan.yml -l fabric-external-prod |& tee deploy_prod_external.txt
  artifacts:
    when: always
    paths:
      - ./deploy_*.txt

test_prod:
  stage: test
  needs:
    - deploy_prod
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
  before_script:
    - python3 -m pip install -r requirements.txt
    - ansible-galaxy collection install -r requirements.yml
  script:
    - set -o pipefail &&  ansible-playbook -i hosts.prod.yml test.yml -l fabric-prod |& tee test_fabric_prod.txt
  artifacts:
    when: always
    paths:
      - ./test_*.txt

The pipeline file above defines 3 stages.
  • lint stage
  • deploy stage
  • test stage

Lint Stage

The lint stage above will only run when you commit or push code to the stage branch. The ansible-lint tool runs against the vxlan.yml playbook file in your repository. Ansible Lint is a command-line tool for linting playbooks, roles and collections aimed towards any Ansible users. Its main goal is to promote proven practices, patterns and behaviors while avoiding common pitfalls that can easily lead to bugs or make code harder to maintain. This CI stage can be extended to lint any additional files as required.

Deploy Stage

The deploy stage above will only run when a pull request or merge request is opened against the main branch or when code is committed or pushed to the main branch.
  • When a merge request is opened from the stage branch against the main branch, the vxlan.yml playbook will run against the staging environment.
  • When the code from the merge request is pushed from the stage branch into the main branch, the vxlan.yml playbook will run against the production environment.

Test Stage

The test stage above will only run when a pull request or merge request is opened against the main branch or when code is committed or pushed to the main branch.
  • When a merge request is opened from the stage branch against the main branch, the test.yml playbook will run against the staging environment.
  • When the code from the merge request is pushed from the stage branch into the main branch, the test.yml playbook will run against the production environment.

All of these stages run in a docker container that are built with the ansible-lint, ansible-playbook and ansible-galaxy binaries.


Step 5 - Update Ansible Requirements YAML to Include VXLAN as Code Collection

You will replace the contents of the requirements.yml file to include the the cisco.nac_dc_vxlan collection which contains the VXLAN as Code roles.


touch ~/workspace/ndfclab/nac/requirements.yml
cat << EOF > ~/workspace/ndfclab/nac/requirements.yml

collections:
  - name: community.general
    version: 10.6.0 
  - name: ansible.posix
    version: 2.0.0
  - name: ansible.utils
    version: 6.0.0
  - name: ansible.netcommon
    version: 8.0.0
  - name: cisco.dcnm
    version: 3.8.0
  - name: cisco.nac_dc_vxlan
    version: 0.4.2
EOF


Continue to the next section to create the tests needed for the end of the pipeline..