Rancher对接对VPN接入CI方案

55 阅读3分钟

1. 背景与场景说明

在企业网络环境中,有些 Rancher 集群位于内网或受限网络中,无法直接从外网访问。
此时,我们可以借助 VPN(如 Sangfor EasyConnect)建立内网访问通道,从本地或 CI/CD 环境发布和管理 Rancher 应用。

本文将详细介绍如何通过 EasyConnect VPN 执行 Rancher 应用的部署操作。

方案介绍

  1. 通过hagb/docker-easyconnect:cli连接vpn
  2. 在vpn 镜像中 放入deploy脚本 ps: 为什么放到这里,因为kubectl脚本执行,需要能访问对应的网络,如果放在别的地方就需要对网络做NET映射,非常麻烦,而且不合适
  3. 搭建deploy服务,解析传入参数,找到对应的vpn执行发布脚本

vpn Dockerfile

# 基于 CLI 镜像
FROM hagb/docker-easyconnect:cli

USER root

# 安装基础工具
RUN apt-get update && \
    apt-get install -y \
      iputils-ping \
      iproute2 \
      net-tools \
      iptables \
      jq \
      procps \
      ca-certificates \
      bash \
    && rm -rf /var/lib/apt/lists/*

# ===== COPY 本地 kubectl 到镜像 =====
COPY deploy/kubectl /usr/local/bin/kubectl
RUN chmod +x /usr/local/bin/kubectl
# 注意:kubectl version 检查在跨架构构建时会失败,实际运行时再验证

# ===== kubeconfig & deploy 脚本 =====
RUN mkdir -p /kubeconfig
COPY configs/kubeconfig/ /kubeconfig/
COPY deploy/deploy.sh /usr/local/bin/deploy.sh
RUN chmod +x /usr/local/bin/deploy.sh

# ⚠️ 不要加 ENTRYPOINT / CMD,会导致 docker-easyconnect:cli 登录失败

deploy.sh 这里你可能需要根据自己的公司的项目情况做改动,主要是DEPLOY_NAMESPACE的解析和KCFG的路径解析

#!/bin/bash
set -e

# 传入参数
PLATFORM=$1          # xinhee / shanshan
DEPLOY_ENV=$2        # test / prod
DEPLOY_KEY=$3        # deployment 名称
IMAGE_VERSION=$4     # 镜像 tag 比如 registry/app:v1
DEPLOY_NAMESPACE=$5  # 默认 web-cloud-test

if [ -z "$PLATFORM" ] || [ -z "$DEPLOY_ENV" ] || [ -z "$DEPLOY_KEY" ] || [ -z "$IMAGE_VERSION" ]; then
  echo "参数不足,需要:PLATFORM DEPLOY_ENV DEPLOY_KEY IMAGE_VERSION"
  exit 1
fi

# 如何deploy_env = main prod pre 则 使用 prod 对应的 kubeconfig
if [ "$DEPLOY_ENV" = "main" ] || [ "$DEPLOY_ENV" = "prod" ]; then
  DEPLOY_ENV="prod"
fi

# 如何deploy_env = test stage dev  则 使用 test 对应的 kubeconfig
if [ "$DEPLOY_ENV" = "test" ] || [ "$DEPLOY_ENV" = "stage" ]; then
  DEPLOY_ENV="test"
fi

# 如果 DEPLOY_NAMESPACE 没传,
if [ -z "$DEPLOY_NAMESPACE" ]; then
  if [ "$DEPLOY_ENV" = "prod" ]; then
    DEPLOY_NAMESPACE="web-cloud"
  elif [ "$DEPLOY_ENV" = "test" ]; then
    DEPLOY_NAMESPACE="web-cloud-test"
  fi
fi

# 动态选 kubeconfig
KCFG="/kubeconfig/${PLATFORM}.${DEPLOY_ENV}.yml"

if [ ! -f "$KCFG" ]; then
  echo "kubeconfig 不存在: $KCFG"
  exit 1
fi

export KUBECONFIG="$KCFG"

echo "使用 kubeconfig:$KCFG"
echo "部署:deployment/$DEPLOY_KEY"
echo "镜像:$IMAGE_VERSION"
echo "部署 namespace:$DEPLOY_NAMESPACE"

# 更新镜像
kubectl -n "$DEPLOY_NAMESPACE" set image deployment/"$DEPLOY_KEY" \
  "$DEPLOY_KEY"="$IMAGE_VERSION" --record

# 查看 rollout 过程
kubectl -n "$DEPLOY_NAMESPACE" rollout status deployment/"$DEPLOY_KEY"

发布核心代码,我这里是用的nodejs,你可以自己选择使用的语言和方式

import { Injectable } from '@nestjs/common';
import { exec } from 'child_process';
import { promisify } from 'util';
import { readFileSync } from 'fs';
import { join } from 'path';

const execAsync = promisify(exec);

@Injectable()
export class DockerService {
  /**
   * 获取当前运行的容器列表
   */
  async listContainers(): Promise<string[]> {
    try {
      const { stdout } = await execAsync(
        'docker ps --format "{{.Names}}"',
        { maxBuffer: 1024 * 1024 }
      );
      return stdout
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line.length > 0);
    } catch (error) {
      return [];
    }
  }

  async getVpn(platform: string): Promise<any> {
    const configPath = join(__dirname, 'config.json');
    const config = readFileSync(configPath, 'utf8');
    const configData = JSON.parse(config);
    const vpn = configData.vpns.find((v: any) => v.name === platform);
    return vpn;
  }

  /**
   * 执行部署命令
   */
  async deployVpn(
    platform: string,
    deployEnv: string,
    deployKey: string,
    imageVersion: string,
    namespace: string,
  ): Promise<{ success: boolean; output?: string; error?: string }> {
    const vpn = await this.getVpn(platform);
    const dockerName = vpn ? platform : 'docker-default-cli';

    const platformName = platform.replace('vpn_', '');
    const cmd = [
      'docker',
      'exec',
      dockerName,
      'bash',
      '/usr/local/bin/deploy.sh',
      platformName,
      deployEnv,
      deployKey,
      imageVersion,
      namespace,
    ].join(' ');

    try {
      const { stdout } = await execAsync(cmd, {
        maxBuffer: 10 * 1024 * 1024, // 10MB
        timeout: 1200000, // 20 minutes
      });
      return {
        success: true,
        output: stdout,
      };
    } catch (error: any) {
      return {
        success: false,
        error: error.stdout || error.stderr || error.message || 'deploy failed',
      };
    }
  }

  /**
   * Ping VPN 容器
   */
  async pingVpn(platform: string): Promise<{
    success: boolean;
    server?: string;
    error?: string;
  }> {
    const containers = await this.listContainers();
    if (!containers.includes(platform)) {
      return {
        success: false,
        error: `container ${platform} not running`,
      };
    }

    try {
      // 根据 platform 从config.json 中读取server
      const vpn = await this.getVpn(platform);
      if (!vpn) {
        return {
          success: false,
          error: `vpn ${platform} not found`,
        };
      }

      const vpnServer = vpn.server;
      const vpnHost = vpnServer.split(':')[0];
      // 在容器内部 ping VPN 地址
      try {
        await execAsync(
          `docker exec ${platform} ping -c 2 -W 2 ${vpnHost}`,
          { maxBuffer: 1024 * 1024 }
        );
        return {
          success: true,
          server: vpnServer,
        };
      } catch (pingError) {
        return {
          success: false,
          server: vpnServer,
        };
      }
    } catch (error: any) {
      return {
        success: false,
        error: error.message || String(error),
      };
    }
  }
}

完整项目地址

项目使用介绍

添加kubeconfig配置

  1. 登录rancher 找到对应的project 右上角 copy kubeConfig clipboard image.png
  2. 文件命名规范,projectName.env.yml

添加vpn 配置,命名为 config.json 如果需要vpn的话, 支持多个vpn, 不需要vpn则不用配,格式如下

{
  "name": "vpn_${projectName}",
  "server": "${vpn_server}",
  "user": "${vpn_user}",
  "pass": "${vpn_pass}",
  "ec_ver": "7.6.7"
}

下载 kubectl-cli 放到 deploy 目录下 命名为kubectl

参考download-kubectl.sh

下载 docker-cli 放到server目录下 docker-cli/docker

参考 download-docker-cli.sh

安装docker-compose依赖

cd docker-compose yarn install sh start.sh

docker 使用的是主机的docker,所以需要你的主机安装docker

测试网络是否正常

curl -X POST http://localhost:9090/ping \
     -H "Content-Type: application/json" \
     -d '{"platform": "vpn_projectName"}'

返回success true则为正常

测试发布

curl -X POST http://localhost:9090/deploy -H "Content-Type: application/json" -d '{
  "platform": "vpn_projectName",
  "env": "test",
  "key": "scm-saas",
  "image": "your_image_name"
}'

其他介绍,如何通过NAT,让主机能访问容器内的网络

如果我就想在主机上通过docker内的网络访问,如何打通网络

  1. 容器内开启转发 + NAT: sysctl -w net.ipv4.ip_forward=1

允许 Linux 把“不是发给自己”的 IP 包,从一个网卡转发到另一个网卡 收到包
目的IP = 本机 → 本地处理
目的IP ≠ 本机 → 查路由表 → 从另一个接口发出去

iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE

把“从容器出去、走 tun0 的包”,源 IP 改成 tun0 自己的 IP

  1. 查看容器路由信息,找到需要跳转的ip image.png
  2. 宿主机添加路由: sudo route add -host 172.16.5.42 172.19.0.3
  3. 测试 ping: ping 172.16.5.42
  • 如果成功,包走向如下: 宿主机 → docker0 → 容器 (veth) → tun0 → VPN → 内网目标 内网目标 → VPN → 容器 → docker0 → 宿主机