迁移网关

迁移的阶段 4。对于每个 Mesh v1 网关,在其旁边部署一个 Mesh v2 网关,将流量切换过去,然后将 Mesh v1 网关缩容为零。Gateway 和 VirtualService 资源不会更改:新部署会被与旧部署相同的标签选中。

请先迁移网关后面的工作负载,参见迁移工作负载。

一次迁移一个网关

此页面中的所有内容均限定于 GATEWAY_NAMESPACE 和 GATEWAY_NAME。针对一个网关运行完整个页面的步骤——收集其配置、在其旁边部署 Mesh v2 网关、切换流量并进行验证——然后再将这两个变量设置为下一个网关并重新开始。

收集网关配置

列出 Mesh v1 网关

将 <gateway-namespace> 和 <gateway-name> 替换为第一个命令列出的某个网关。

kubectl get gatewaydeploys.asm.alauda.io -A -o json | jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)"'
export GATEWAY_NAMESPACE=<gateway-namespace>
export GATEWAY_NAME=<gateway-name>

检查控制平面自身的网关

上述列表涵盖平台管理的网关。Mesh v1 控制平面会在 istio-system 中部署自己的 istio-ingressgateway,而多网络网格还会在其旁边添加 istio-eastwestgateway。二者都不是 GatewayDeploy 资源,因此不会显示在上述列表中,本页面的任何步骤也不会涉及它们。阶段 5 会将它们与控制平面一同删除。

export CP_GATEWAYS=$(kubectl -n istio-system get deploy -l "istio.io/rev=${V1_REVISION}" -o json | jq -c '[.items[]
  | select((.metadata.labels["operator.istio.io/component"] // "") | test("Gateways$"))
  | {name: .metadata.name, labels: (.spec.template.metadata.labels // {})}]')
jq -r '.[] | "\(.name) \(.labels | tojson)"' <<<"${CP_GATEWAYS}"
echo "--- routed through them:"
kubectl get gateways.networking.istio.io -A -o json | jq -r --argjson d "${CP_GATEWAYS}" '
  .items[] as $gw
  | ($gw.spec.selector // {}) as $sel
  | select(($sel | length) > 0)
  | $d[] as $dep
  | select($sel | to_entries | all(.value == $dep.labels[.key]))
  | "\($dep.name) <- \($gw.metadata.namespace)/\($gw.metadata.name)"'

示例输出

istio-ingressgateway {"app":"istio-ingressgateway","istio":"ingressgateway","istio.io/rev":"1-22",...}
--- routed through them:

routed through them 下没有任何内容意味着这些网关不承载任何路由,在阶段 5中删除它们不会造成任何影响。如果其中列出了 Gateway,则表示它承载的流量尚未由本页面中的任何步骤迁移:请按照以下步骤为其部署 Mesh v2 网关,并使用其部署所打印的标签作为选择器标签。

在多网络网格中,istio-eastwestgateway 会显示在此处,并通过路由访问。它不会在本页面中迁移:迁移多网络网格 会在阶段 2中部署其 Mesh v2 替代项,然后再移动任何工作负载。

读取选择器、服务和证书

Mesh v2 网关 Pod 必须携带当前用于选择 Mesh v1 网关的所有标签:网关 Service 的选择器,以及此网关所服务的每个 Gateway 资源的选择器。

echo "service selector:"
kubectl -n "${GATEWAY_NAMESPACE}" get svc "${GATEWAY_NAME}" -o jsonpath='{.spec.selector}{"\n"}'
echo "service owner:"
kubectl -n "${GATEWAY_NAMESPACE}" get svc "${GATEWAY_NAME}" -o jsonpath='{.metadata.ownerReferences}{"\n"}'
echo "gateway selectors and credentials:"
kubectl get gateways.networking.istio.io -A -o json | jq -r '.items[]
  | "\(.metadata.namespace)/\(.metadata.name) selector=\(.spec.selector | tojson) credentials=\([.spec.servers[]?.tls.credentialName // empty] | join(","))"'
echo "namespace revision label:"
kubectl get ns "${GATEWAY_NAMESPACE}" -o jsonpath='{.metadata.labels.istio\.io/rev}{"\n"}'

示例输出

service selector:
{"app":"ingress-1"}
service owner:
[{"apiVersion":"asm.alauda.io/v1alpha1","kind":"GatewayDeploy","name":"ingress-1",...}]
gateway selectors and credentials:
gw/http-gw selector={"app":"ingress-1","istio":"gw-ingress-1"} credentials=
gw/https-gw selector={"app":"ingress-1","istio":"gw-ingress-1"} credentials=example-com-tls
namespace revision label:

记录以下三项:

  1. 选择器标签的并集——示例中的 app: ingress-1 和 istio: gw-ingress-1。仅携带 Service 选择器的 Mesh v2 网关能够接收流量,但没有监听器,所有请求都会返回 404。
  2. 网关 Service 是否由 GatewayDeploy 资源拥有。通常是,这意味着除非移除所有者引用,否则 Kubernetes 会在阶段 5中随 Mesh v1 资源一同删除该服务。
  3. 网关命名空间是否带有 istio.io/rev 标签。后续步骤会有所不同。

选择 Mesh v2 网关的注入方式

网关命名空间Mesh v2 网关的注入方式对 Mesh v1 网关的影响
没有 istio.io/rev 标签——Mesh v1 网关在其 Pod 模板上携带版本为 Mesh v2 网关 Pod 添加 istio.io/rev: ${V2_REVISION} 标签无。Mesh v1 网关继续运行,随时可以将其扩容回来
带有 istio.io/rev: ${V1_REVISION} 标签先将命名空间标签切换为 ${V2_REVISION},然后部署 Mesh v2 网关Mesh v1 网关将无法再重新创建,请参阅下面的警告
kubectl label ns "${GATEWAY_NAMESPACE}" istio.io/rev="${V2_REVISION}" --overwrite
切换命名空间标签是单向操作

仅当网关命名空间已经带有版本标签时才运行此命令。Mesh v1 使用 Mesh v2 中不存在的模板(asm-gateway)注入其网关,因此一旦命名空间指向 Mesh v2 版本,就无法再创建 Mesh v1 网关 Pod。现有 Pod 会继续运行,但此时回滚意味着切换标签,而不是将旧网关扩容回来。切换标签后,立即部署并验证 Mesh v2 网关。当网关与 mesh 工作负载共用命名空间时,阶段 3已经切换了标签,网关正处于此状态:请在迁移这些工作负载后立即迁移网关。

部署 Mesh v2 网关

新网关就绪后会立即接收流量

Mesh v2 网关携带 Mesh v1 网关的选择器标签,因此现有的 Service 会在新 Pod 就绪后立即开始在两个网关之间进行负载均衡。在创建部署之前完成以下证书和权限步骤,并在此之后立即运行验证。若要随时将所有流量发回 Mesh v1 网关,请将 Mesh v2 部署缩容到零。

将 TLS 证书复制到网关命名空间

Mesh v1 首先在网关命名空间中解析 credentialName,然后回退到 istio-system。Mesh v2 运行上游 Istio,只会在网关工作负载所在的命名空间中查找。通过 Web 控制台创建的证书存储在 istio-system 中,因此请复制此网关的 Gateway 所引用的每个证书:

for secret in $(kubectl get gateways.networking.istio.io -A -o json \
  | jq -r --arg sel "${GATEWAY_NAME}" '.items[] | select(.spec.selector | tostring | contains($sel))
          | .spec.servers[]?.tls.credentialName // empty' | sort -u); do
  src_ns=$(kubectl get secret -A --field-selector "metadata.name=${secret}" \
    -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' \
    | awk -v gw="${GATEWAY_NAMESPACE}" '$0==gw{print; found=1; exit}
                                        $0=="istio-system"{is=1}
                                        END{if (!found && is) print "istio-system"}')
  echo "${secret}: ${src_ns:-NOT FOUND} -> ${GATEWAY_NAMESPACE}"
  [ -z "${src_ns}" ] && continue
  [ "${src_ns}" = "${GATEWAY_NAMESPACE}" ] && continue
  kubectl -n "${src_ns}" get secret "${secret}" -o json \
    | jq 'del(.metadata.namespace, .metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp,
              .metadata.ownerReferences, .metadata.managedFields, .metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"])' \
    | kubectl -n "${GATEWAY_NAMESPACE}" apply -f -
done

查找遵循 Mesh v1 的解析顺序:首先查找网关命名空间,其次查找 istio-system。第三个命名空间中同名的证书是不同的证书,不会被复制——网关会向客户端提供该证书,且不会有任何检查报告问题,因为错误证书与正确证书一样是 ACTIVE。

保留原始 secret:Mesh v1 网关在阶段 5之前仍会使用它。

创建服务账户和 SDS 权限

istiod 只会向服务账户可以读取其命名空间中 secret 的网关发送证书。没有此角色时,证书会保持在 WARMING 状态:网关会继续响应其 HTTP 路由,但其 HTTPS 端口上的每次 TLS 握手都会被重置。

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ${GATEWAY_NAME}-v2
  namespace: ${GATEWAY_NAMESPACE}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ${GATEWAY_NAME}-v2-sds
  namespace: ${GATEWAY_NAMESPACE}
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ${GATEWAY_NAME}-v2-sds
  namespace: ${GATEWAY_NAMESPACE}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ${GATEWAY_NAME}-v2-sds
subjects:
  - kind: ServiceAccount
    name: ${GATEWAY_NAME}-v2
    namespace: ${GATEWAY_NAMESPACE}
EOF

部署网关

下面的部署是标准的网关注入部署。它携带 Mesh v1 网关的选择器标签,使现有的 Service 和 Gateway 资源能够选择它;此外还带有一个专属标签,以便分别对两个版本进行扩缩容。

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${GATEWAY_NAME}-v2
  namespace: ${GATEWAY_NAMESPACE}
spec:
  replicas: 1
  selector:
    matchLabels:
      gateway.migration: ${GATEWAY_NAME}-v2
  template:
    metadata:
      annotations:
        inject.istio.io/templates: gateway
      labels:
        gateway.migration: ${GATEWAY_NAME}-v2
        sidecar.istio.io/inject: "true"
        istio.io/rev: ${V2_REVISION}
        app: ${GATEWAY_NAME}
        istio: ${GATEWAY_NAMESPACE}-${GATEWAY_NAME}
    spec:
      serviceAccountName: ${GATEWAY_NAME}-v2
      containers:
        - name: istio-proxy
          image: auto
          securityContext:
            capabilities:
              drop:
                - ALL
            allowPrivilegeEscalation: false
            privileged: false
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
EOF
kubectl -n "${GATEWAY_NAMESPACE}" rollout status "deploy/${GATEWAY_NAME}-v2" --timeout=300s
  1. inject.istio.io/templates: gateway 将注入的 proxy 设置为网关而不是 sidecar,而 image: auto 允许控制平面填充 proxy 镜像。
  2. sidecar.istio.io/inject: "true" 是必需的,因为 mesh 保留了 Mesh v1 的注入策略:只有 Pod 主动选择加入时,才会为其注入。
  3. istio.io/rev 选择 Mesh v2 控制平面。当命名空间已经带有版本标签时,请移除此标签。
  4. 将最后两个标签替换为所收集选择器的并集:网关 Service 使用的每个标签,以及 Gateway 资源使用的每个标签。此示例中的值遵循 Mesh v1 的命名方式,即 app: <gateway> 和 istio: <namespace>-<gateway>。
  5. securityContext 满足 Restricted Pod Security Admission 配置文件的要求。通过注入安装的网关是你自己的部署,因此这里设置此项,而不是在 Istio 资源上设置。请参阅 Pod Security Admission。

重新创建访问日志配置

为 Mesh v1 网关启用访问日志的 Telemetry 资源会根据只有 Mesh v1 设置的标签选择 Pod,并会在阶段 5中随其所有者一同删除。请创建一个与 Mesh v2 网关匹配的等效资源:

cat <<EOF | kubectl apply -f -
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
  name: ${GATEWAY_NAME}-access-logging
  namespace: ${GATEWAY_NAMESPACE}
spec:
  selector:
    matchLabels:
      app: ${GATEWAY_NAME}
  accessLogging:
    - providers:
        - name: envoy
EOF

验证 Mesh v2 网关

POD=$(kubectl -n "${GATEWAY_NAMESPACE}" get pod -l "gateway.migration=${GATEWAY_NAME}-v2" -o jsonpath='{.items[0].metadata.name}')
istioctl proxy-config listener "${POD}" -n "${GATEWAY_NAMESPACE}"
istioctl proxy-config secret "${POD}" -n "${GATEWAY_NAMESPACE}"

proxy-config 从 Pod 的 proxy 中读取配置,而不是从控制平面读取,因此与 proxy-status 不同,它不接受 --revision。

  • 监听器应与 Gateway 资源的端口匹配。没有监听器意味着 Pod 标签与 Gateway 选择器不匹配。
  • 如果网关提供 HTTPS,则每个 kubernetes://<credential> secret 都应为 ACTIVE。处于 WARMING 状态的 secret 表示此命名空间中缺少证书,或服务账户无权读取该证书——在此状态持续期间,一半的 HTTPS 请求会失败。仅提供 HTTP 的网关——收集配置时其 credentials 为空——不会列出任何 kubernetes:// secret,此检查不适用。

如果任一检查失败,请将部署缩容到零,修复原因,然后将其扩容回来:

kubectl -n "${GATEWAY_NAMESPACE}" scale deploy "${GATEWAY_NAME}-v2" --replicas=0

切换流量

两个部署由同一个 Service 选中,因此流量比例取决于副本数。

将 Mesh v2 网关扩容到 Mesh v1 网关的规模

export GATEWAY_REPLICAS=$(kubectl -n "${GATEWAY_NAMESPACE}" get deploy "${GATEWAY_NAME}" -o jsonpath='{.spec.replicas}')
echo "${GATEWAY_NAME} runs ${GATEWAY_REPLICAS} replicas" | tee -a "${BACKUP_DIR}/gateway-replicas.txt"
kubectl -n "${GATEWAY_NAMESPACE}" scale deploy "${GATEWAY_NAME}-v2" --replicas="${GATEWAY_REPLICAS}"
kubectl -n "${GATEWAY_NAMESPACE}" rollout status "deploy/${GATEWAY_NAME}-v2" --timeout=300s

现在有一半的请求会到达 Mesh v2 网关。在继续之前,请检查此网关的每条路由,包括 HTTPS 路由。

GATEWAY_REPLICAS 是 Mesh v1 网关回滚时必须恢复到的规模。下一步会将其副本数设置为零,因此之后无法再从集群中读取该值——这就是为什么还要将其写入备份。

将 Mesh v1 网关缩容到零

kubectl -n "${GATEWAY_NAMESPACE}" scale deploy "${GATEWAY_NAME}" --replicas=0

保留该部署:在 阶段 5 之前,如果新网关运行异常,这是最快的恢复方式。停用 Mesh v1 时会删除它。

保护为外部服务生成的资源

将外部服务绑定到出口网关会使 Mesh v1 生成 Gateway、VirtualService 和 DestinationRule 资源。为它们添加注释,使 Mesh v1 控制器停止管理这些资源,并在平台资源删除时保留它们:

kubectl get gateways.networking.istio.io,virtualservices.networking.istio.io,destinationrules.networking.istio.io -A -o json \
  | jq -r --arg gw "${GATEWAY_NAME}" '.items[]
      | select(.metadata.name | contains("--" + $gw + "--"))
      | "\(.kind) \(.metadata.namespace) \(.metadata.name)"' \
  | while read -r kind ns name; do
      kubectl -n "${ns}" annotate "$(echo "${kind}" | tr '[:upper:]' '[:lower:]').networking.istio.io" "${name}" \
        asm.cpaas.io/user-managed=true --overwrite
    done

生成资源的名称包含外部服务、网关命名空间和网关名称,这正是过滤器匹配的内容。Mesh v1 还会生成名称不符合该模式的资源——每个外部服务的 ServiceEntry 旁边有一个 DestinationRule,以及每个具有负载均衡策略的服务对应一个资源。这里不会选中这些资源,也不需要选中:它们的所有者是服务的 ServiceEntry 或 Endpoints,而不是 Mesh v1 资源,因此 阶段 5 不会删除它们,Mesh v2 会继续按原样读取它们。阶段 0 中的 ${BACKUP_DIR}/asm-generated-resources.txt 会列出这些资源及其所有者:

destinationrules.networking.istio.io  istio-system  httpbingo-org-se        ServiceEntry/httpbingo-org-se
destinationrules.networking.istio.io  app-partial   asm-backend             Endpoints/backend

在这里浏览一次该列表。无需为任何资源添加注释,但迁移完成后,将没有控制器维护这些资源;从现在起,你可以自行编辑或删除它们。

验证

  • 网关的每个路由都像之前一样响应 HTTP;如果网关提供 HTTPS,则也应通过 HTTPS 响应。
  • 流量切换期间,探针未报告任何故障。
  • kubectl -n "${GATEWAY_NAMESPACE}" get deploy 显示 Mesh v1 网关的副本数为 0,Mesh v2 网关的副本数已达到完整副本数。
  • kubectl get gateways.networking.istio.io,virtualservices.networking.istio.io -A 返回与迁移前相同的资源。迁移不会更改这些资源。
  • 新网关 Pod 仍会生成访问日志。

对每个网关(入口和出口)重复执行上述步骤,然后继续阅读停用 Mesh v1。