动态修改 Istio 路由策略

上一篇我们讲了如何对路由策略进行配置,但是登录到服务器并执行命令,对于用户来说是一种非常不好的体验,那么本篇就来讲一下,如何对命令进行一系列的包装,以使得操作可以直观简便。

需要知道的是,在 K8S 体系下,我们可以将宿主机的命令和文件等挂载到 Pod 内,以便在 Pod 里面也可以访问到这些内容,或是执行宿主机上的命令,我们需要修改一下部署的 yaml:

apiVersion: v1
kind: Pod
metadata:
  name: sample-service
  labels:
    app: sample-service
spec:
  containers:
    - name: sample-service-container
      image: sample-service:v0
      ports:
      - containerPort: 9001
        hostPort: 32001
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - mountPath: /usr/local/bin/kubectl
          name: sample-service-kube
          readOnly: true
        - mountPath: /root/.kube/config
          name: sample-service-config
          readOnly: true
        - mountPath: /usr/local/bin/istioctl
          name: sample-service-istio
          readOnly: true
  volumes:
  - hostPath:
      path: /usr/bin/kubectl
    name: sample-service-kube
  - hostPath:
      path: /home/rarnu/.kube/config
    name: sample-service-config
  - hostPath:
      path: /usr/local/bin/istioctl
    name: sample-service-istio

这里需要特别注意的是,必须挂载 ~/.kube/config 文件到 Pod 内,这个文件决定了 kubectl 命令的权限,如果没有这个文件,将不能在 Pod 内执行 kubectl 命令。

然后删去原先的 Pod 并重新创建之:

$ kubectl delete -f sample-service.yaml
$ kubectl apply -f sample-service.yaml

等 Pod 启动后,可以进入 Pod 内查看命令是否已挂载,并且尝试执行它:

$ kubectl exec -it sample-service sh
# which kubectl
/usr/local/bin/kubectl
# which istioctl
/usr/local/bin/istioctl
# kubectl get pods

此时可以发现,在 Pod 内拥有命令并且可以正常执行。


接下来只需要写一些代码就可以完成对路由的修改了:

data class ReqClusters(val clusters: List)
data class Response(val code: String, val message: String, val data: T? = null) {
    companion object {
        fun success(): Response<*> = Response("200", "succ", null)
        fun fatal(): Response<*> = Response("500", "fail", null)
        fun success(item: T): Response = Response("200", "succ", item)
        fun fatal(item: T): Response = Response("500", "fail", item)
    }
}

fun Routing.istioControllerRouting() {
    post("/update") { param ->
        addNewClusters(param.clusters)
        call.respond(Response.success())
    }
}

fun addNewClusters(list: List) {
    if (list.isNotEmpty()) {
        val script = when (list.size) {
            1 -> "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[0]}"
            2 -> "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[0]}\n" +
                 "      weight: 50\n" +
                 "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[1]}\n" +
                 "      weight: 50\n"
            3 -> "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[0]}\n" +
                 "      weight: 33\n" +
                 "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[1]}\n" +
                 "      weight: 33\n" +
                 "    - destination:\n" +
                 "        host: reviews\n" +
                 "        subset: ${list[2]}\n" +
                 "      weight: 34\n"
            else -> ""
        }
        val scriptText = ROUTE_TEMPLATE.format(script)
        val destFile = File(SCRIPT_FILE_PATH, "script.yaml")
        destFile.writeText(scriptText)
        runCommand("kubectl apply -f ${destFile.absolutePath}").apply {
            println("output: $output, error: $error")
        }
    }
}

private const val ROUTE_TEMPLATE = """
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
%s
"""

然后只需要在前端写个简单的页面,请求一下接口即可:

image.png

这里你可能看到了,上面有展示当前的路由情况,这个内容获取也很容易:

fun getRouteCluster(): List {
    val jsonStr = runCommand("istioctl pc route $POD_NAME --name $POD_PORT -o json").output.trim()
    return parseRouteClusterList(jsonStr)
}
private fun parseRouteClusterList(jsonStr: String): List {
    val ret = mutableListOf()
    val jVirtualService = (JSONArray(jsonStr).first() as JSONObject).getJSONArray("virtualHosts").filter {
        it as JSONObject
        it.getString("name") == "reviews.default.svc.cluster.local:9080"
    }.first() as JSONObject
    val jRoute = (jVirtualService.getJSONArray("routes").first() as JSONObject).getJSONObject("route")
    if (jRoute.has("cluster")) {
        ret.add(jRoute.getString("cluster"))
    } else if (jRoute.has("weightedClusters")) {
        ret.addAll(
            jRoute.getJSONObject("weightedClusters").getJSONArray("clusters").map {
                it as JSONObject
                it.getString("name")
            })
    }
    return ret
}   

看到这里你可能要说了,直接挂载并调用命令会产生不安全的情况,比如说命令被非法使用了,可能整个 K8S 体系都完蛋,所以接下去我们需要把相应的代码改成 SDK 调用的方式。

先来看如何获取当前的路由列表:

fun getRouteCluster(): List {
    val istio: IstioClient = DefaultIstioClient()
    val list = istio.v1alpha3VirtualService().list().items
    return list.firstOrNull { it.spec.hosts[0] == "reviews" }?.spec?.http?.get(0)?.route?.map { it.destination.subset } ?: listOf()
}

然后再来看看如何新增路由,本质上新增路由的代码,就是将 yaml 内的代码改为用 SDK 方案来实现:

fun addRouteClusters() {
    val vs23 = VirtualServiceBuilder()
        .withApiVersion("networking.istio.io/v1alpha3")
        .withKind("VirtualService")
        .withNewMetadata()
            .withName("reviews")
            .withNamespace("default")
        .endMetadata()
        .withNewSpec()
            .withHosts("reviews")
            .addNewHttp()
            .addNewRoute()
                .withNewDestination()
                .withHost("reviews")
                .withSubset("v2")
                .endDestination()
                .withWeight(50)
            .endRoute()
            .addNewRoute()
                .withNewDestination()
                .withHost("reviews")
                .withSubset("v3")
                .endDestination()
                .withWeight(50)
            .endRoute()
            .endHttp()
        .endSpec()
    .build()
    val istio: IstioClient = DefaultIstioClient()
    istio.istio.v1alpha3VirtualService().create(vs23)
}

不得不说,写起来确实有些麻烦,不过好在编程语言是灵活的,如此封装一下就好了:

fun makeVirtualService(name: String, namespace: String = "default", host: String, subsets: List>): VirtualService {
    val http = VirtualServiceBuilder()
        .withApiVersion("networking.istio.io/v1alpha3")
        .withKind("VirtualService")
        .withNewMetadata()
        .withName(name)
        .withNamespace(namespace)
        .endMetadata()
        .withNewSpec()
        .withHosts(host)
        .addNewHttp()

    subsets.forEach { (subset, weight) ->
        http
            .addNewRoute()
            .withNewDestination()
            .withHost(host)
            .withSubset(subset)
            .endDestination()
            .withWeight(weight)
            .endRoute()

    }
    return http
        .endHttp()
        .endSpec()
        .build()
}

最后还有必要提的是,Istio SDK 对于依赖版本有相当高的要求,经过测试,发现依赖必须如下配置:

compile 'me.snowdrop:istio-client:1.7.5-Beta2'
compile 'io.fabric8:kubernetes-client:4.10.2'
compile 'com.squareup.okhttp3:okhttp:3.12.12'

以上三个依赖项的版本号必须是如此,才能正常通过编译并正常工作,对于一些库本身就依赖了 okhttp 等的,需要进行 exclude 操作,如:

compile ("com.github.isyscore:common-ktor:1.3.1") {
    exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}

你可能感兴趣的:(动态修改 Istio 路由策略)