使用GitLab和Ansible构建网络感知CI/CD流水线

本教程详细介绍了如何构建网络感知的CI/CD流水线,包括使用GitLab、Ansible和Containerlab进行网络状态检查、配置部署和回滚操作,确保网络变更的安全性和可逆性。

如何用GitLab和Ansible构建网络感知CI/CD流水线

自动化正成为组织网络中常见的流程。无论网络使用何种类型的自动化,其CI/CD流水线都应该是网络感知的。

在网络感知的CI/CD流水线中,意味着在推送新配置时需要检查实时的关键配置。了解如何构建网络自动化需求以进行实时网络状态检查,有助于确保每个变更都经过验证且可逆。Ansible playbook、Python或GoLang代码可以验证实时网络状态,并使用GitLab、Jenkins或GitHub Actions等工具执行它们。

CI/CD操作

要使CI/CD流水线具备网络感知能力,网络团队必须执行以下步骤:

  • 预检查:对实时网络执行初始查询。例如,检查先前配置的关键接口是否正常运行。如果在流水线中包含特定VLAN范围,请按照步骤验证这些预检查。
  • 部署:向网络应用新配置。
  • 后检查:验证预检查作业最初检查的关键配置是否仍然有效。
  • 回滚:如果后检查失败,重新配置关键接口。如果后检查成功,则跳过此步骤。

本教程在流水线外手动创建了一个关键接口,确保文章仅关注配置CI/CD操作。

先决条件

在构建网络感知CI/CD流水线之前,请确保以下内容已安装在您的机器上:

  • Multipass:Multipass是一个命令行工具,可创建预安装Docker的Ubuntu Server VM。其他子工具(如GitLab和Containerlab)将在VM内作为容器或Linux程序安装。
  • Cisco路由器镜像:其他供应商的路由器也可接受。
  • VS Code和Remote Secure Shell扩展:Remote SSH扩展使VS Code能够通过SSH访问VM的目录。

确保您对Linux有基本了解。具体来说,您必须知道如何创建和更改目录,以及如何创建、重命名和复制文件。另一个关键知识是SSH主机密钥是什么,如何生成或删除一个,以及Linux操作系统将其保存在哪里。了解这些可以理解如何使用SSH验证进入Linux系统。

最后,您必须对Ansible、Docker和像GitLab这样的CI/CD流水线工具有基本了解。

设置基础设施架构

设置VM,然后从VS Code配置对其的SSH访问。接着,将路由器镜像传输到VM并将其转换为容器。

其他工具如Containerlab、Ansible、GitLab和Docker Hub注册表将在VM内安装。

创建VM

要创建VM,请配置一个16 GB RAM、4 CPU、预安装Docker的Ubuntu Server VM。

1
multipass launch --memory 16G --cpus 4 docker

配置从VS Code到VM的无密码SSH访问

此步骤确保VS Code可以在不提示输入密码的情况下验证进入VM。VM配置应如下所示:

1
2
3
4
# 进入VM并获取其IP地址
multipass shell docker
ip address show ens3 # 我的VM的IP地址是192.168.64.7/24
exit

同时,主机配置应如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# macOS - 向主机添加ssh配置
sudo tee -a ~/.ssh/config <<EOF
Host docker
  HostName 192.168.64.7
  User ubuntu
EOF

# Windows - 向主机添加ssh配置
mkdir ~\.ssh
@"Host docker
  HostName 192.168.64.7
  User ubuntu"@ | Out-File -Append -Encoding utf8 ~\.ssh\config

# 在主机上生成ssh密钥
ssh-keygen -t rsa

# 将主机上的公共ssh密钥添加到VM
multipass exec docker -- bash -c "echo `cat ~/.ssh/id_rsa.pub` >> ~/.ssh/authorized_keys"

最后,从VS Code连接到VM。导航到左侧边栏并选择Remote Explorer > Docker。具体来说,点击➜。

将路由器部署到Containerlab

要将路由器部署到Containerlab,首先在VM上安装Containerlab。

1
bash -c "$(curl -sL https://get.containerlab.dev)"

接下来,准备路由器目录,在VS Code中打开终端并创建以下目录。

1
mkdir -p ~/network-aware/router

在VS Code中打开一个文件夹并选择network-aware。然后,将路由器下载到主机机器。本教程使用Cisco IOL L3镜像,x86_64_crb_linux-adventerprisek9-ms.bin。

将镜像从主机机器传输到VM,特别是到/router目录。

1
multipass transfer -r ./x86_64_crb_linux-adventerprisek9-ms.bin docker:/home/ubuntu/network-aware/router

将路由器的VM镜像转换为容器镜像,以便它可以在Containerlab内运行。为此,请执行以下步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 更改目录
cd ~/network-aware/router/

# 克隆包含将镜像从VM转换为容器的工具的资源库
git clone https://github.com/hellt/vrnetlab.git

# 将您的镜像复制到资源库内的vrnetlab/cisco/iol/
cp x86_64_crb_linux-adventerprisek9-ms.bin vrnetlab/cisco/iol/

# 将镜像重命名为cisco_iol-15.7.bin
mv vrnetlab/cisco/iol/x86_64_crb_linux-adventerprisek9-ms.bin vrnetlab/cisco/iol/cisco_iol-15.7.bin

# 安装make并构建容器镜像
sudo apt install make
make -C vrnetlab/cisco/iol/ docker-image

创建Containerlab拓扑文件并部署路由器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
cat <<EOF > cisco-iol.clab.yaml
name: cisco-iol
topology:
  nodes:
    R1:
      kind: cisco_iol
      image: vrnetlab/cisco_iol:15.7
  links:
    - endpoints: ["R1:Ethernet0/1", "R1:Ethernet0/2"]
EOF

containerlab deploy --topo cisco-iol.clab.yaml
containerlab inspect

根据输出,路由器的管理IPv4地址是172.20.20.2。删除与该IP地址关联的任何旧SSH主机密钥。现在,SSH进入路由器。当您使用IP地址SSH进入设备时,它会自动将主机密钥添加到~/.ssh/known_hosts。这对Ansible很有用。如果您使用Cisco IOL路由器镜像,默认密码是admin。

1
2
ssh-keygen -f ~/.ssh/known_hosts -R 172.20.20.2
ssh admin@172.20.20.2

构建和推送Ansible镜像

要在CI/CD流水线内执行Ansible playbooks,请遵循以下两个步骤。首先,运行本地Docker Hub注册表。

1
docker run -d -p 5000:5000 --name registry registry:2

然后,构建一个用于流水线的Ansible镜像,并将其推送到您的本地Docker Hub注册表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mkdir ~/network-aware/ansible-gitlab
cd ~/network-aware/ansible-gitlab/
cat <<'EOF' > Dockerfile
FROM ubuntu:latest
RUN apt update && \
    # 安装ansible
    apt install -y software-properties-common && \
    add-apt-repository -y --update ppa:ansible/ansible && \
    apt install -y ansible && \
    # 安装替代的ssh实现
    apt install python3-pip -y && \
    pip3 install ansible-pylibssh --break-system-packages && \
    # 安装git
    apt install git -y && \
    apt clean
EOF

docker build -t localhost:5000/ansible:latest .
docker push localhost:5000/ansible:latest

部署GitLab容器

下一步是部署GitLab容器。首先,准备一个目录来安装GitLab。

1
2
3
4
mkdir ~/gitlab-docker
echo export GITLAB_HOME=~/gitlab-docker >> ~/.bashrc
source ~/.bashrc
exit

接下来,重新进入VM的shell以验证新环境变量是否设置。

1
2
multipass shell docker
echo $GITLAB_HOME

创建GitLab容器。启动大约需要六分钟。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
docker run --detach \
  --hostname gitlab \
  --env GITLAB_OMNIBUS_CONFIG="external_url 'http://gitlab'" \
  --publish 443:443 --publish 80:80 \
  --name gitlab \
  --restart always \
  --volume $GITLAB_HOME/config:/etc/gitlab \
  --volume $GITLAB_HOME/logs:/var/log/gitlab \
  --volume $GITLAB_HOME/data:/var/opt/gitlab \
  --shm-size 512m \
  gitlab/gitlab-ce:latest

检索GitLab凭据。

1
2
docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password
# 用户名 = root

GitLab默认使用端口80。使用VM的IP地址访问它。对于本教程,VM的接口名称是ens3,其IP地址是192.168.64.7。这可能因机器而异,因此以下代码可能不适用于所有人。

1
ip address show ens3

编辑本地机器和Ubuntu VM的主机文件。VM可以使用创建GitHub容器时定义的external_url克隆资源库。主机可以成功创建Linux运行器并通过Web浏览器复制令牌。

以下是macOS和Windows的代码。macOS命令也适用于VM。

1
2
3
4
5
# macOS
echo "192.168.64.7 gitlab" | sudo tee -a /etc/hosts

# Windows
Add-Content "$env:SystemRoot\System32\drivers\etc\hosts" "`r`n192.168.64.7 gitlab"

最后,使用主机的Web浏览器在gitlab访问GitLab URL。

配置GitLab

现在是时候创建一个资源库并设置CI/CD流水线了。此过程涉及几个步骤,包括以下内容。

  • 创建GitLab资源库
  • 创建Linux运行器
  • 配置Docker执行器
  • 配置git凭据并克隆资源库

创建GitLab资源库

GitLab将资源库称为项目。要创建一个,导航到Project > New project > Create blank project。以下是项目参数。

  • 项目名称 = containerlab-project
  • 组 = root
  • 可见性级别 = public

创建Linux运行器

运行器拉取GitLab作业并在执行器内执行命令。运行器可以是Linux VM、容器或任何其他合适的环境。执行器可以是VM上的shell、容器或其他类型的进程。

这里,VM充当Linux运行器。它当前运行Docker,并且可以访问本地注册表中的Ansible Docker镜像。通过导航到GitLab UI上的Settings > CI/CD > Runners > Create project runner获取运行器身份验证令牌。填写以下选项。

  • 标签 = containerlab
  • 超时 = 600
  • 操作系统 = linux

在您的VM上注册令牌。首先安装GitLab Runner CLI。将替换为新创建的令牌。

1
2
3
4
RUNNER_TOKEN=<token>
sudo gitlab-runner register \
  --url http://gitlab \
  --token $RUNNER_TOKEN

当出现输出时,在注册令牌时填写以下选项。

  • 名称 = linux
  • 执行器 = docker
  • Docker镜像 = localhost:5000/ansible:latest

配置Docker执行器

因为作业在运行Ansible的Docker容器内执行,所以配置Docker执行器至关重要。在迁移到生产环境时使用容器更容易,并且始终可以将自定义Ansible Docker镜像推送到组织的容器注册表。

因为本教程使用Docker执行器,容器必须与GitLab容器通信。为此,首先获取GitLab容器的IP地址。

1
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' gitlab

然后,编辑GitLab的config.toml文件。此文件包含配置选项,指示运行器如何访问GitLab容器。

1
sudo vi /etc/gitlab-runner/config.toml

向[runners.docker]部分添加一个额外的主机,以便执行器可以到达GitLab实例。使用主机网络模式,以便新的容器作业也可以到达Containerlab容器。将172.17.0.2替换为GitLab容器的IP地址。

1
2
3
[runners.docker]
extra_hosts = ["gitlab:172.17.0.2"]
network_mode = "host"

最后,保存并重新启动运行器。

1
sudo gitlab-runner restart

配置git凭据并克隆资源库

配置GitLab的最后一步是配置git凭据并克隆资源库。为GitLab设置git凭据。将电子邮件替换为您的。

1
2
git config --global user.email "yours@example.com"
git config --global user.name  "root"

接下来,克隆资源库。

1
2
cd ~/network-aware/ansible-gitlab
git clone http://gitlab/root/containerlab-project.git

创建一个名为patch的新分支,并使用checkout自动切换到它。

1
2
cd containerlab-project/
git checkout -b patch

通过导航到Settings > Access token创建个人令牌。

  • 令牌名称 = Git
  • 权限 = write_repository
  • 角色 = Developer

设置您的上游URL并包含个人令牌。将替换为您的。

1
2
ACCESS_TOKEN=<token>
git remote set-url origin http://root:$ACCESS_TOKEN@gitlab/root/containerlab-project.git

设置CI/CD流水线

设置CI/CD流水线包括配置预检查、后检查、部署和回滚作业。在创建这些作业时,定义流水线脚本和Ansible playbooks,然后将它们推送到资源库。

创建GitLab流水线变量

CI/CD流水线需要GitLab流水线变量,因为Ansible容器需要知道它尝试连接和更改的设备。要获取路由器的SSH主机密钥,请在VM上运行以下命令。

1
ssh-keyscan 172.20.20.2

复制主机文件输出,然后导航到GitLab UI上的Settings > CI/CD > Variable。

将输出粘贴到CI/CD变量,并取消选中Protect variable。因为没有受保护的分支或git标签,取消选中该选项是可以的。

  • 键 = KNOWN_HOSTS
  • 值 = <您的主机文件输出>

创建GitLab作业脚本

GitLab作业脚本定义哪些作业将执行每个Ansible playbook。关于脚本需要注意的一些重要事项包括:

  • 镜像为每个作业启动一个Ansible容器。这是可能的,因为Linux运行器拉取作业并使用Docker执行器运行它。
  • Ansible清单必须使用SSH主机密钥连接到路由器,该密钥先前作为CI/CD变量$KNOWN_HOSTS传递给此资源库。密钥中定义的SSH主机必须与Ansible清单中定义的ansible_host匹配。
  • before_script从GitLab变量检索路由器的主机密钥,并将输出保存到~/.ssh/known_hosts,以便每个新作业可以成功运行。
  • 每个新作业包含一个标题为containerlab的标签。Linux运行器使用它来确定它可以运行哪些作业。
  • 作业内的所有命令默认在分配给CI_BUILDS_DIR = /builds//变量的目录中运行。它包含资源库文件。
  • pre_check作业中的规则规定,仅当有人提交合并请求时它才运行。这确保它知道网络的当前状态。
  • deploy、post_check和rollback中的规则规定,作业仅当合并请求被合并到main时才运行。
  • 如果任何作业失败,rollback运行。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
cat <<'EOF' > .gitlab-ci.yml
stages:
  - pre_check
  - deploy
  - post_check
  - rollback

image: localhost:5000/ansible:latest

before_script:
  - echo $KNOWN_HOSTS > ~/.ssh/known_hosts

pre_check:
  stage: pre_check
  tags:
    - containerlab
  script:
    - ansible-playbook pre-check.yaml -i inventory.yaml
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'

deploy:
  stage: deploy
  tags:
    - containerlab
  script:
    - ansible-playbook deploy.yaml -i inventory.yaml
  rules:
    - if: $CI_COMMIT_BRANCH == 'main'

post_check:
  stage: post_check
  tags:
    - containerlab
  script:
    - ansible-playbook pre-check.yaml -i inventory.yaml
  rules:
    - if: $CI_COMMIT_BRANCH == 'main'

rollback:
  stage: rollback
  when: on_failure
  tags:
    - containerlab
  script:
    - ansible-playbook rollback.yaml -i inventory.yaml
  rules:
    - if: $CI_COMMIT_BRANCH == 'main'
EOF

创建Ansible变量

要创建Ansible变量文件,请使用您的路由器凭据。

1
2
3
4
5
6
mkdir vars
cat <<'EOF' > vars/credentials.yaml
ansible_username: admin
ansible_passwd: admin
ansible_host: 172.20.20.2
EOF

使用以下代码创建Ansible清单文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
cat <<'EOF' > inventory.yaml
local:
  hosts:
    clab-cisco-iol-R1:
      ansible_connection: network_cli
      ansible_host: "{{ ansible_host }}"
      ansible_user: "{{ ansible_username }}"
      ansible_password: "{{ ansible_passwd }}"
      ansible_network_os: cisco.ios.ios
      ansible_become: true
      ansible_become_method: enable
EOF

创建预检查和后检查playbook

预检查playbook确定现有的关键接口是否启动。为此,它收集所有接口,输出结果,并在关键接口关闭时失败。它通过循环接口输出并检查当键值对和enabled为false且接口名称为Ethernet0/1时执行此操作。如果匹配,则失败,因为关键接口关闭,并输出错误消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
cat <<'EOF' > pre-check.yaml
- hosts: clab-cisco-iol-R1
  vars_files:
    - vars/credentials.yaml
  tasks:
    - name: Gathers all interfaces
      cisco.ios.ios_facts:
        gather_network_resources:
          - interfaces
      register: int_status

    - name: Print the interface details
      ansible.builtin.debug:
        var: int_status.ansible_facts.ansible_network_resources.interfaces

    - name: Fail if the critical interface is down
      loop: "{{ int_status.ansible_facts.ansible_network_resources.interfaces }}"
      when: item.enabled == false and item.name == "Ethernet0/1"
      ansible.builtin.fail:
        msg: "Interface {{ item.name }} is down!"
EOF

后检查playbook使用预检查playbook查看关键接口是否仍然启动。

创建部署playbook

部署playbook配置一个新接口Ethernet0/2。然后,它有意注入一个故障,关闭现有的关键接口Ethernet0/1。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
cat <<'EOF' > deploy.yaml
- hosts: clab-cisco-iol-R1
  vars_files:
    - vars/credentials.yaml
  tasks:
    - name: set interface IP address and mask
      cisco.ios.ios_l3_interfaces:
        config:
          - ipv4:
            - address: 10.1.2.20/24
            name: Ethernet0/2
      register: int_address

    - name: Print the interface IP address
      ansible.builtin.debug:
        var: int_address

    - name: set the interface status up
      cisco.ios.ios_interfaces:
        config:
          - description: critical
            enabled: false
        # 关闭接口
            name: Ethernet0/1     # 此接口
      register: int_status

    - name: Print the interface status
      ansible.builtin.debug:
        var: int_status
EOF

创建回滚playbook

回滚playbook启动新部署的更改关闭的关键接口。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cat <<'EOF' > rollback.yaml
- hosts: clab-cisco-iol-R1
  vars_files:
    - vars/credentials.yaml
  tasks:
    - name: configure critical interface IP address and mask
      cisco.ios.ios_l3_interfaces:
        config:
          - ipv4:
            - address: 10.1.1.10/24
            name: Ethernet0/1
      register: int_address

    - name: print the interface IP address
      ansible.builtin.debug:
        var: int_address

    - name: enable the critical interface status
      cisco.ios.ios_interfaces:
        config:
          - description: critical
            enabled: true
            name: Ethernet0/1
      register: int_status

    - name: print the interface status
      ansible.builtin.debug:
        var: int_status
EOF

由于此流水线需要一个已经启动的关键接口才有效,请手动使用Ansible playbook配置一个。

在执行playbook时,路由器的SSH主机密钥必须已经保存在SSH目录中,然后Ansible才能验证它。手动使用SSH登录路由器,并将路由器的主机密钥添加到主机。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 安装uv
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env

# 初始化uv并激活venv
uv init --bare
uv venv
source .venv/bin/activate

# 添加库
uv add ansible ansible-pylibssh

# 配置关键接口
ansible-playbook rollback.yaml -i inventory.yaml

# 再次SSH进入路由器以验证配置
# 密码 = admin
ssh admin@172.20.20.2
show ip interface brief

# 退出路由器并停用venv
exit
deactivate

另一种替代方法是使用apt安装Ansible,然后使用pip和–break-system-packages参数安装Ansible-pylibssh。由于这是本实验室的临时Ubuntu VM,此参数是有效的。

添加.gitignore文件

.gitignore文件在推送到GitLab时删除与uv相关的文件。这些文件在流水线中不需要。

1
2
3
4
5
6
cat <<'EOF' > .gitignore
# uv环境
.venv/
pyproject.toml
uv.lock
EOF

将更改推送到资源库。

执行以下命令将更改推送到资源库。

1
2
3
git add .
git commit -m "configure CI/CD pipeline"
git push --set-upstream origin patch

在GitLab仪表板上观察作业

要确定更改是否成功,请在GitLab仪表板上观察作业。本教程包括每个步骤的附带截图。将它们与您在屏幕上看到的内容进行比较。

首先,在将更改推送到新分支后,导航到您的资源库中的该分支并打开合并请求。

现在,观察pre_check作业。下图是一个示例输出。它通过了,因为关键接口仍然启动。

接下来,完成合并请求。点击Merge。这样做会自动触发剩余的作业。

观察deploy作业。此作业配置一个非关键接口。但是,它在此过程中关闭了一个关键接口。当观察post_check作业时,它将因此失败。

rollback作业纠正了关键接口中的错误。

替代步骤

修改deploy.yaml playbook并将更改推送到GitLab可以纠正初始的非关键接口配置。当这种情况发生时,GitLab将执行其他作业,但跳过回滚作业。

如果网络工程团队尚未过渡到Python,Ansible仍然可以在流水线内执行一次性实时状态检查。一旦团队采用Python和pyATS,逐步将pyATS实现引入流水线,因为它是供应商无关的。

并非每个流水线都需要检查实时网络状态。相反,通过像Batfish和Suzieq这样的开源工具,使用设备的快照执行预检查和后检查。

Charles Uneze是一位专门研究云原生网络、Kubernetes和开源的技术作家。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计