如何用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。这可能因机器而异,因此以下代码可能不适用于所有人。
编辑本地机器和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和开源的技术作家。