<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Kubernetes - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Kubernetes - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 27 Jun 2026 19:39:29 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/kubernetes/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何通过 Kubernetes 安全专家认证考试 ]]>
                </title>
                <description>
                    <![CDATA[ 本文基于我学习并通过 Kubernetes 安全专家认证考试的经验。2021 年 9 月，我第一次参加考试就通过了。 我于 2020 年 2 月通过了认证 Kubernetes 应用程序开发人员考试，随后于 2020 年 3 月通过了认证 Kubernetes 管理员。 认证 Kubernetes 安全专家或 CKS 考试于 2020 年 11 月左右发布，但我在 2021 年 9 月之前没有机会参加该考试。 作为一些背景信息，在过去的 3 年里，我几乎每天都在使用 Kubernetes，这些经验为我通过 CKS 考试提供了额外的优势。 在本文中，我将分享一些可以帮助您学习和通过考试的资源，以及可以在准备时使用的有用备忘单。 我还将分享一些建议，这些建议应该对您有所帮助。 什么是 Kubernetes Kubernetes 是目前最先进、功能最丰富的容器编排系统，并且它一直在变得更好。 它有一个庞大的社区提供支持，并一直在开发新功能和解决问题。Kubernetes 无疑正在以惊人的速度发展，要跟上其发展速度成为一项挑战。 这使其成为容器编排解决方案的最佳选择。 目录  * ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-pass-the-certified-kubernetes-security-specialist-exam/</link>
                <guid isPermaLink="false">65fbbb076f02f80413b53968</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp.org ]]>
                </dc:creator>
                <pubDate>Thu, 21 Mar 2024 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/03/cks.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-pass-the-certified-kubernetes-security-specialist-exam/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Pass the Certified Kubernetes Security Specialist Exam – Cheat sheet and Study Guide</a>
      </p><!--kg-card-begin: markdown--><p>本文基于我学习并通过 Kubernetes 安全专家认证考试的经验。2021 年 9 月，我第一次参加考试就通过了。</p>
<p>我于 2020 年 2 月通过了认证 Kubernetes 应用程序开发人员考试，随后于 2020 年 3 月通过了认证 Kubernetes 管理员。</p>
<p>认证 Kubernetes 安全专家或 CKS 考试于 2020 年 11 月左右发布，但我在 2021 年 9 月之前没有机会参加该考试。</p>
<p>作为一些背景信息，在过去的 3 年里，我几乎每天都在使用 Kubernetes，这些经验为我通过 CKS 考试提供了额外的优势。</p>
<p>在本文中，我将分享一些可以帮助您学习和通过考试的资源，以及可以在准备时使用的有用备忘单。 我还将分享一些建议，这些建议应该对您有所帮助。</p>
<h3 id="kubernetes">什么是 Kubernetes</h3>
<p>Kubernetes 是目前最先进、功能最丰富的容器编排系统，并且它一直在变得更好。</p>
<p>它有一个庞大的社区提供支持，并一直在开发新功能和解决问题。Kubernetes 无疑正在以惊人的速度发展，要跟上其发展速度成为一项挑战。 这使其成为容器编排解决方案的最佳选择。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#resourcesfortheexam">备考资料</a></li>
<li><a href="#aliases">Aliases</a></li>
<li><a href="#shortcuts">一些捷径</a></li>
<li><a href="#kubernetescheatsheet">Kubernetes 手册</a></li>
<li><a href="#cksexamtips">CKS 考试技巧</a></li>
<li><a href="#cksexamtopics">CKS 考试题目</a></li>
<li><a href="#howtopreparefortheexam">如何准备考试</a></li>
<li><a href="#practicepracticeandpractice">练习，练习，再练习！</a></li>
</ul>
<hr>
<h2 id="resourcesfortheexam">备考资料</h2>
<p>以下是通过 CKS 考试的一些很棒的资源：</p>
<ol>
<li><a href="https://www.udemy.com/course/certified-kubernetes-security-specialist/">认证 Kubernetes 安全专家 by Killer.sh</a></li>
<li><a href="https://kodekloud.com/courses/certified-kubernetes-security-specialist-cks/">认证 Kubernetes 安全专家（CKS）by KodeKloud</a></li>
<li><a href="https://github.com/walidshaari/Certified-Kubernetes-Security-Specialist">Walid Shaari 收集了一些 CKS 考试不可或缺的资料</a></li>
<li><a href="https://github.com/abdennour/certified-kubernetes-security-specialist">Abdennour's References 的 CKS 考试目标参考资料</a></li>
<li><a href="https://github.com/ibrahimjelliti/CKSS-Certified-Kubernetes-Security-Specialist">Ibrahim Jelliti 的准备 Kubernetes 安全专家认证（CKSS）考试的资源集</a></li>
</ol>
<p>KodeKloud 和 Killer.sh 的课程提供了模拟考试模拟器，这对备考非常有帮助，而且还能让人对考试有一个大致的了解。我强烈建议注册一门或两门课程。</p>
<p>从 Linux Foundation 购买考试后，你可以免费尝试 2 次来自 kill.sh 的考试模拟器。 这样，如果您精通课程内容，您可以跳过课程并直接进入考试提供的考试模拟器。</p>
<p>考试费用为 375 美元，但也有一些优惠和折扣，如果你留意一下，可能会得到更优惠的价格。考试时间为 2 小时，有效期为 2 年，而 CKA 和 CKAD 的有效期为 3 年。</p>
<h2 id="aliases">Aliases</h2>
<p>CKS 是一项基于表现的考试，您将获得一个考试模拟器，您必须在其中解决问题。 除了考试选项卡外，您只能打开一个选项卡。</p>
<p>因为这个考试需要你写很多命令，所以我很早就想到我必须依靠别名来减少击键次数以节省时间。</p>
<p>我在考试时使用了 <strong>vi</strong> 编辑器，所以在这里我将分享一些关于这个编辑器的实用技巧。</p>
<h3 id="videfaultsforvimrc">vi defaults for ~/.vimrc:</h3>
<pre><code>vi ~/.vimrc
---
:set number
:set et
:set sw=2 ts=2 sts=2
---
^: Start of word in line
0: Start of line
$: End of line
w: End of word
GG: End of file
</code></pre>
<h3 id="kubectldefaultsforbashrc">kubectl defaults for ~/.bashrc:</h3>
<pre><code>vi ~/.bashrc
---
alias k='kubectl'
alias kg='k get'
alias kd='k describe'
alias kl='k logs'
alias ke='k explain'
alias kr='k replace'
alias kc='k create'
alias kgp='k get po'
alias kgn='k get no'
alias kge='k get ev'
alias kex='k exec -it'
alias kgc='k config get-contexts'
alias ksn='k config set-context --current --namespace'
alias kuc='k config use-context'
alias krun='k run'
export do='--dry-run=client -oyaml'
export force='--grace-period=0 --force'

source &lt;(kubectl completion bash)
source &lt;(kubectl completion bash | sed 's/kubectl/k/g' )
complete -F __start_kubectl k


alias krp='k run test --image=busybox --restart=Never'
alias kuc='k config use-context'
---
</code></pre>
<h2 id="shortcuts">一些捷径</h2>
<p><code>kubectl get</code> 命令为访问资源提供了简短易懂的名称，例如 <code>pvc</code> 用于 <code>persistentstorageclaim</code>。这些可以帮助人们在考试期间节省大量击键和宝贵的时间。</p>
<ul>
<li><strong>po</strong> for <code>pods</code></li>
<li><strong>rs</strong> for <code>replicasets</code></li>
<li><strong>deploy</strong> for <code>deployments</code></li>
<li><strong>svc</strong> for <code>services</code></li>
<li><strong>ns</strong> for <code>namespace</code></li>
<li><strong>netpol</strong> for <code>networkpolicy</code></li>
<li><strong>pv</strong> for <code>persistentstorage</code></li>
<li><strong>pvc</strong> for <code>persistentstorageclaim</code></li>
<li><strong>sa</strong> for <code>serviceaccounts</code></li>
</ul>
<h2 id="kubernetescheatsheet">Kubernetes 手册</h2>
<h3 id="kubectlrun">kubectl run 命令</h3>
<p><code>kubectl run</code> 命令提供了一个标志 <code>--restart</code>，它允许您创建从 Deployment 到 CronJob 的不同类型的 Kubernetes 对象。</p>
<p>下面的代码片段显示了可用于 <code>--restart</code> 标志的不同选项。</p>
<pre><code>k run:
--restart=Always                             #Creates a deployment
--restart=Never                              #Creates a Pod
--restart=OnFailure                          #Creates a Job
--restart=OnFailure --schedule="*/1 * * * *" #Creates a CronJob
</code></pre>
<h3 id="podyaml">如何从现有的 pod 生成 yaml 规范</h3>
<p>有时，从现有 pod 生成规范并对其进行更改比从头开始创建新的更容易。 <code>kubectl get pod</code> 命令为我们提供了所需的标志，以我们想要的格式输出 pod 规范。</p>
<pre><code>kgp &lt;pod-name&gt; -o wide

# Generating YAML Pod spec
kgp &lt;pod-name&gt; -o yaml
kgp &lt;pod-name&gt; -o yaml &gt; &lt;pod-name&gt;.yaml

# Get a pod's YAML spec without cluster specific information
kgp my-pod -o yaml --export &gt; &lt;pod-name&gt;.yaml
</code></pre>
<h3 id="kubectlpod">kubectl pod 命令</h3>
<p><code>kubectl run</code> 命令提供了很多选项，例如指定请求和 Pod 应该使用的限制，或者容器在创建后应该运行的命令。</p>
<pre><code># Output YAML for a nginx pod running an echo command
krun nginx --image=nginx --restart=Never --dry-run -o yaml -- /bin/sh -c 'echo Hello World!'
# Output YAML for a busybox pod running a sleep command
krun busybox --image=busybox:1.28 --restart=Never --dry-run -o yaml -- /bin/sh -c 'while true; do echo sleep; sleep 10; done'
# Run a pod with set requests and limits
krun nginx --image=nginx --restart=Never --requests='cpu=100m,memory=512Mi' --limits='cpu=300m,memory=1Gi'
# Delete pod without delay
k delete po busybox --grace-period=0 --force
</code></pre>
<h3 id="">如何打印日志并导出</h3>
<p>在调试应用程序时，日志是信息的基本来源。<code>kubectl logs</code> 命令提供了检查给定 pod 日志的功能。 您可以使用以下命令检查给定 pod 的日志。</p>
<pre><code>kubectl logs deploy/&lt;podname&gt;
kubectl logs deployment/&lt;podname&gt;
#Follow logs
kubectl logs deploy/&lt;podname&gt; --tail 1 --follow
</code></pre>
<p>除了查看日志外，我们还可以将日志导出到文件中，以便进一步调试与任何人共享相同的内容。</p>
<pre><code>kubectl logs &lt;podname&gt; --namespace &lt;ns&gt; &gt; /path/to/file.format
</code></pre>
<h3 id="">如何创建配置映射和机密</h3>
<p><code>kubectl create</code> 命令让我们可以从命令行创建 ConfigMap 和 Secret。 我们还可以使用 YAML 文件来创建相同的资源，并通过使用 <code>kubectl apply -f &lt;filename&gt;</code> 我们可以应用命令。</p>
<pre><code>kc cm my-cm --from-literal=APP_ENV=dev
kc cm my-cm --from-file=test.txt
kc cm my-cm --from-env-file=config.env

kc secret generic my-secret --from-literal=APP_SECRET=sdcdcsdcsdcsdc
kc secret generic my-secret --from-file=secret.txt
kc secret generic my-secret --from-env-file=secret.env
</code></pre>
<h3 id="">有用的调试命令</h3>
<p>当您在日常工作中以及在 CKS 考试中解决问题时遇到问题和错误时，调试是一项非常重要的技能。</p>
<p>除了能够从容器输出日志外，<code>kubectl exec</code> 命令还允许您登录到正在运行的容器并调试问题。 在容器内，您还可以使用 <code>nc</code> 和 <code>nslookup</code> 等实用程序来诊断与网络相关的问题。</p>
<pre><code># Run busybox container
k run busybox --image=busybox:1.28 --rm --restart=Never -it sh
# Connect to a specific container in a Pod
k exec -it busybox -c busybox2 -- /bin/sh
# adding limits and requests in command
kubectl run nginx --image=nginx --restart=Never --requests='cpu=100m,memory=256Mi' --limits='cpu=200m,memory=512Mi'
# Create a Pod with a service
kubectl run nginx --image=nginx --restart=Never --port=80 --expose
# Check port
nc -z -v -w 2 &lt;service-name&gt; &lt;port-name&gt;
# NSLookup
nslookup &lt;service-name&gt;
nslookup 10-32-0-10.default.pod
</code></pre>
<h3 id="">滚动更新和推出</h3>
<p><code>kubectl rollout</code> 命令提供了检查更新状态的能力，如果需要，还可以回滚到以前的版本。</p>
<pre><code>k set image deploy/nginx nginx=nginx:1.17.0 --record
k rollout status deploy/nginx
k rollout history deploy/nginx
# Rollback to previous version
k rollout undo deploy/nginx
# Rollback to revision number
k rollout undo deploy/nginx --to-revision=2
k rollout pause deploy/nginx
k rollout resume deploy/nginx
k rollout restart deploy/nginx
kubectl run nginx-deploy --image=nginx:1.16 --replias=1 --record
</code></pre>
<h3 id="">缩放和自动缩放命令</h3>
<p><code>kubectl scale</code> 命令提供了在给定部署中扩大或缩小 pod 的功能。</p>
<p>使用 <code>kubectl autoscale</code> 命令，我们可以定义给定部署应该运行的最小 pod 数量，以及部署可以扩展到的最大 pod 数量以及 CPU 百分比等扩展标准。</p>
<pre><code>k scale deploy/nginx --replicas=6
k autoscale deploy/nginx --min=3 --max=9 --cpu-percent=80
</code></pre>
<h3 id="">网络策略</h3>
<p>在 Kubernetes 集群中，默认情况下所有 pod 都可以与所有 pod 通信，这在某些实现中可能是一个安全问题。</p>
<p>为了解决这个问题，Kubernetes 引入了网络策略来允许或拒绝基于 pod 标签的流量，这些标签是 pod 规范的一部分。</p>
<p>下面的示例拒绝在所有命名空间中运行的 Pod 的 Ingress 和 Egress 流量。</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: example
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  - Ingress
</code></pre>
<p>下面的示例拒绝在所有命名空间中运行的 Pod 的 Ingress 和 Egress 流量。 但它允许访问在端口 53 上运行的 DNS 解析服务。</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  - Ingress
  egress:
  - to:
    ports:
      - port: 53
        protocol: TCP
      - port: 53
        protocol: UDP
</code></pre>
<p>以下示例拒绝对 AWS EC2 实例中 IP 地址“169.256.169.256”上运行的元数据服务器的 Egress 访问。</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name:cloud-metadata-deny
  namespace: default
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
      - ipBlock: 
          cidr: 0.0.0.0/0
          except:
          - 169.256.169.256/32
</code></pre>
<p>下面的示例允许 Egress 访问 AWS EC2 实例中在 IP 地址“169.256.169.256”上运行的元数据服务器。</p>
<pre><code>apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cloud-metadata-accessor
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: metadata-accessor
  policyTypes:
  - Egress
  egress:
  - to:
    - ipBlock:
        cidr: 169.256.169.256/32
</code></pre>
<h3 id="kubesec">使用 Kubesec 进行静态分析</h3>
<p>Kubesec 是一个静态分析工具，用于分析 YAML 文件以查找文件问题。</p>
<pre><code>kubesec scan pod.yaml

# Using online kubesec API
curl -sSX POST --data-binary @pod.yaml https://v2.kubesec.io/scan

# Running the API locally
kubesec http 8080 &amp;

kubesec scan pod.yaml -o pod_report.json -o json
</code></pre>
<h3 id="trivvy">使用 Trivvy 进行漏洞扫描</h3>
<p>Trivvy 是一种漏洞扫描工具，可扫描容器映像以查找安全问题。</p>
<pre><code>trivy image nginx:1.18.0
trivy image --severity CRITICAL nginx:1.18.0
trivy image --severity CRITICAL, HIGH nginx:1.18.0
trivy image --ignore-unfixed nginx:1.18.0

# Scanning image tarball
docker save nginx:1.18.0 &gt; nginx.tar
trivy image --input archive.tar

# Scan and output results to file
trivy image --output python_alpine.txt python:3.10.0a4-alpine
trivy image --severity HIGH --output /root/python.txt python:3.10.0a4-alpine

# Scan image tarball
trivy image --input alpine.tar --format json --output /root/alpine.json
</code></pre>
<h3 id="">如何删除不需要的服务</h3>
<p>The <code>systemctl</code> exposes the capabilities to start, stop, enable, disable and list services running on a Linux Virtual Machine.<br>
<code>systemctl</code> 提供了启动、停止、启用、禁用和列出在 Linux 虚拟机上运行的服务的功能。</p>
<p>列出服务：</p>
<pre><code>systemctl list-units --type service
</code></pre>
<p>停止服务：</p>
<pre><code>systemctl stop apache2
</code></pre>
<p>禁用服务：</p>
<pre><code>systemctl disable apache2
</code></pre>
<p>删除服务：</p>
<pre><code>apt remove apache2
</code></pre>
<h3 id="">运行时类</h3>
<p>Kubernetes 在 <code>v1.12</code> 版本中引入了 RuntimeClass 功能，用于选择容器运行时配置。 容器运行时配置用于运行 pod 的底层容器。</p>
<p>大多数 Kubernetes 集群使用 <code>dockershim</code> 作为运行容器的运行时类，但您可以使用不同的容器运行时。</p>
<p><code>dockershim</code> 在 Kubernetes 版本 <code>v1.20</code> 中已被弃用，并将在 <code>v1.24</code> 中删除。</p>
<p>如何创建运行时类：</p>
<pre><code>apiversion: node.k8s.io/v1beta1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
</code></pre>
<p>如何为任何给定的 pod 使用运行时类：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  runtimeClassName: gvisor
  containers:
  - name: nginx
    image: nginx 
</code></pre>
<h3 id="rbac">RBAC 命令</h3>
<p>在 Kubernetes 中，</p>
<blockquote>
<p>基于角色的访问控制（RBAC）命令提供了一种基于单个用户或服务帐户的角色来调节对 Kubernetes 资源的访问的方法。（<a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">来源</a>）</p>
</blockquote>
<p>以下是创建角色的方法：</p>
<pre><code>kubectl create role developer --resource=pods --verb=create,list,get,update,delete --namespace=development
</code></pre>
<p>如何创建角色绑定：</p>
<pre><code>kubectl create rolebinding developer-role-binding --role=developer --user=faizan --namespace=development
</code></pre>
<p>如何验证：</p>
<pre><code>kubectl auth can-i update pods --namespace=development --as=faizan
</code></pre>
<p>如何创建集群角色：</p>
<pre><code>kubectl create clusterrole pvviewer-role --resource=persistentvolumes --verb=list
</code></pre>
<p>以及如何创建与服务帐户的 Clusterrole 绑定关联：</p>
<pre><code>kubectl create clusterrolebinding pvviewer-role-binding --clusterrole=pvviewer-role --serviceaccount=default:pvviewer
</code></pre>
<h3 id="">集群维护</h3>
<p>您可以使用 <code>kubectl drain</code> 命令从给定节点中删除所有正在运行的工作负载（pod）。</p>
<p>您使用 <code>kubectl cordon</code> 命令来封锁节点以将其标记为可调度。</p>
<p>并且您使用 <code>kubectl uncordon</code> 命令将节点设置为可调度，这意味着控制器管理器可以将新 pod 调度到给定节点。</p>
<p>如何排空所有 pod 的节点：</p>
<pre><code>kubectl drain node-1
</code></pre>
<p>如何排空节点并忽略守护程序集：</p>
<pre><code>kubectl drain node01 --ignore-daemonsets
</code></pre>
<p>如何强制排空节点：</p>
<pre><code>kubectl drain node02 --ignore-daemonsets --force
</code></pre>
<p>如何将一个节点标记为不可调度，这样就不能在这个节点上调度新的 Pod：</p>
<pre><code>kubectl cordon node-1
</code></pre>
<p>标记节点可调度</p>
<pre><code>kubectl uncordon node-1
</code></pre>
<h2 id="cksexamtips">CKS 考试技巧</h2>
Kubernetes `kubectl get` 命令为用户提供了一个输出标志，`-o` 或 `--output`，这有助于我们以 JSON、yaml、wide 或 custom-columns 的形式格式化输出。
<h3 id="jsonjsonpath">JSON 和 JSONPath</h3>
<p>如何以 JSON Object 的形式输出所有 pod 的内容：</p>
<pre><code>kubectl get pods -o json
</code></pre>
<p>JSONPath 从 JSON 对象输出一个特定的键：</p>
<pre><code>kubectl get pods -o=jsonpath='{@}'
kubectl get pods -o=jsonpath='{.items[0]}'
</code></pre>
<p><code>.items[*]</code> 用于我们有多个对象的地方，例如具有 pod 配置的多个容器：</p>
<pre><code># For list of items use .items[*]
k get pods -o 'jsonpath={.items[*].metadata.labels.version}'
# For single item
k get po busybox -o jsonpath='{.metadata}'
k get po busybox -o jsonpath="{['.metadata.name', '.metadata.namespace']}{'\n'}"
</code></pre>
<p>该命令使用 JSONPath 返回节点的内部 IP：</p>
<pre><code>kubectl get nodes -o=jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'
</code></pre>
<p>该命令检查特定键的相等性：</p>
<pre><code>kubectl get pod api-stag-765797cf-lrd8q -o=jsonpath='{.spec.volumes[?(@.name=="api-data")].persistentVolumeClaim.claimName}'
kubectl get pod -o=jsonpath='{.items[*].spec.tolerations[?(@.effect=="NoSchedule")].key}'
</code></pre>
<p>自定义列有助于输出特定字段：</p>
<pre><code>kubectl get pods -o='custom-columns=PODS:.metadata.name,Images:.spec.containers[*].image'
</code></pre>
<h2 id="cksexamtopics">CKS 考试题目</h2>
<p>CKS 考试涵盖与 Kubernetes 生态系统中的安全性相关的主题。Kubernetes 安全是一个庞大的话题，一篇文章难以涵盖，因此本文包含了考试中涉及的一些主题。</p>
<h3 id="">如何保护和强化容器镜像</h3>
<p>在设计容器镜像以运行您的代码时，请特别注意保护和强化措施，以防止黑客攻击和特权升级攻击。 在构建容器镜像时请记住以下几点：</p>
<ol>
<li>使用特定的包版本，如<code>alpine:3.13</code>。</li>
<li>不要以 root 身份运行 - 使用 <code>USER &lt;username&gt;</code> 来阻止 root 访问。</li>
<li>使用 <code>readOnlyRootFilesystem: true</code> 在 <code>securityContext</code> 中将文件系统设为只读</li>
<li>使用 <code>RUN rm -rf /bin/*</code> 删除 shell 访问</li>
</ol>
<h3 id="">如何最小化操作系统占用空间</h3>
<h4 id="">容器层</h4>
<p>指令 <code>RUN</code>、<code>COPY</code>和<code>ADD</code>创建容器层。 其他指令创建临时中间图像并且不增加构建的大小。 创建图层的说明会增加结果图像的大小。</p>
<p>典型的 Dockerfile 如下所示。它使用<code>RUN</code>指令添加一个单层。</p>
<pre><code>FROM ubuntu

RUN apt-get update &amp;&amp; apt-get install -y golang-go

CMD ["sh"]
</code></pre>
<h4 id="">多阶段构建</h4>
<p>多阶段构建利用 Dockerfile 中的多个<code>FROM</code>语句。<code>FROM</code> 指令标志着构建的一个新阶段。它结合了多个<code>FROM</code>语句，允许利用以前的构建，以便有选择地将二进制文件复制到新的构建阶段，省略不必要的二进制文件。生成的 Docker 映像的大小要小得多，攻击面也大大减少。</p>
<pre><code>FROM ubuntu:20.04 AS build
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update &amp;&amp; apt-get install -y golang-go
COPY app.go .
RUN CGO_ENABLED=0 go build app.go

FROM alpine:3.13
RUN chmod a-w /etc
RUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup -h /home/appuser
RUN rm -rf /bin/*
COPY --from=build /app /home/appuser/
USER appuser
CMD ["/home/appuser/app"]
</code></pre>
<h3 id="">如何限制节点访问</h3>
<p>访问控制文件包含有关 Linux 操作系统中用户/组的敏感信息。</p>
<pre><code>#Stores information about the UID/GID, user shell, and home directory for a user
/etc/passwd
#Stores the user password in a hashed format
/etc/shadow
#Stores information about the group a user belongs
/etc/group
#Stored information about the Sudoers present in the system
/etc/sudoers
</code></pre>
<p>禁用用户帐户有助于通过禁用给定用户帐户的登录来保护对节点的访问。</p>
<pre><code>usermod -s /bin/nologin &lt;username&gt;
</code></pre>
<p>禁用“root”用户帐户具有特殊意义，因为 root 帐户具有所有功能。</p>
<pre><code>usermod -s /bin/nologin root
</code></pre>
<p>以下是如何添加具有主目录和 shell 的用户：</p>
<pre><code>adduser --home /opt/faizanbashir --shell /bin/bash --uid 2328 --ingroup admin faizanbashir
useradd -d /opt/faizanbashir -s /bin/bash -G admin -u 2328 faizanbashir
</code></pre>
<p>如何删除用户帐户：</p>
<pre><code>userdel &lt;username&gt;
</code></pre>
<p>如何删除群组：</p>
<pre><code>groupdel &lt;groupname&gt;
</code></pre>
<p>如何将用户添加到组：</p>
<pre><code>adduser &lt;username&gt; &lt;groupname&gt;
</code></pre>
<p>如何从组中删除用户：</p>
<pre><code>#deluser faizanbashir admin
deluser &lt;username&gt; &lt;groupname&gt;
</code></pre>
<p>如何为用户设置密码：</p>
<pre><code>passwd &lt;username&gt;
</code></pre>
<p>如何将用户提升为 sudoer：</p>
<pre><code>vim /etc/sudoers
&gt;&gt;&gt;
faizanbashir ALL=(ALL:ALL) ALL
</code></pre>
<p>如何在没有密码的情况下启用 sudo：</p>
<pre><code>vim /etc/sudoers
&gt;&gt;&gt;
faizanbashir ALL=(ALL) NOPASSWD:ALL

visudo
usermod -aG sudo faizanbashir
usermod faizanbashir -G admin
</code></pre>
<h3 id="ssh">SSH加固</h3>
<h4 id="ssh">如何禁用SSH</h4>
<p><code>/etc/ssh/sshd_config</code> 中给出的配置可用于保护对 Linux 节点的 SSH 访问。 将 <code>PermitRootLogin</code> 设置为 <code>no</code> 会禁用节点上的 root 登录。</p>
<p>要强制使用密钥登录并禁用使用密码登录节点，您可以将“PasswordAuthentication”设置为“no”。</p>
<pre><code>vim /etc/ssh/sshd_config
&gt;&gt;
PermitRootLogin no
PasswordAuthentication no
&lt;&lt;
# Restart SSHD Service
systemctl restart sshd
</code></pre>
<p>如何设置root用户不登录：</p>
<pre><code>usermod -s /bin/nologin root
</code></pre>
<p>SSH 复制用户密钥/无密码 SSH：</p>
<pre><code>ssh-copy-id -i ~/.ssh/id_rsa.pub faizanbashir@node01
ssh faizanbashir@node01
</code></pre>
<h3 id="">如何删除过时的包和服务</h3>
<p>以下是列出在 Ubuntu 机器上运行的所有服务的方法：</p>
<pre><code>systemctl list-units --type service
systemctl list-units --type service --state running
</code></pre>
<p>如何停止、禁用和删除服务：</p>
<pre><code>systemctl stop apache2
systemctl disable apache2
apt remove apache2
</code></pre>
<h3 id="">如何限制内核模块</h3>
<p>在 Linux 中，内核模块是可以根据需要加载和卸载到内核中的代码片段。 它们无需重新启动系统即可扩展内核的功能。 模块可以配置为内置或可加载的。</p>
<p>如何列出所有内核模块：</p>
<pre><code>lsmod
</code></pre>
<p>如何手动将模块加载到内核中：</p>
<pre><code>modprobe pcspkr
</code></pre>
<p>如何将模块列入黑名单：（参考：CIS Benchmarks -&gt; 3.4 Uncommon Network Protocols）</p>
<pre><code>cat /etc/modprobe.d/blacklist.conf
&gt;&gt;&gt;
blacklist sctp
blacklist dccp

# Shutdown for changes to take effect
shutdown -r now

# Verify
lsmod | grep dccp
</code></pre>
<h3 id="">如何识别和禁用开放端口</h3>
<p>如何检查开放端口：</p>
<pre><code>netstat -an | grep -w LISTEN
netstat -natp | grep 9090

nc -zv &lt;hostname|IP&gt; 22
nc -zv &lt;hostname|IP&gt; 10-22

ufw deny 8080
</code></pre>
<p>如何检查端口使用情况：</p>
<pre><code>/etc/services | grep -w 53
</code></pre>
<p>这是[开放端口列表]的参考文档（<a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#control-plane-node-s%EF%BC%89%E3%80%82">https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#control-plane-node-s）。</a></p>
<h3 id="">如何限制网络访问</h3>
<h4 id="">如何识别端口上运行的服务：</h4>
<pre><code>systemctl status ssh
cat /etc/services | grep ssh
netstat -an | grep 22 | grep -w LISTEN
</code></pre>
<h4 id="ufw">UFW防火墙</h4>
<p>Uncomplicated Fire Wall (UFW) 是一个在 Arch Linux、Debian 或 Ubuntu 中管理防火墙规则的工具。 UFW 允许您允许和阻止给定端口和来自给定来源的流量。</p>
<p>以下是安装 UFW 防火墙的方法：</p>
<pre><code>apt-get update
apt-get install ufw
systemctl enable ufw
systemctl start ufw
ufw status
ufw status numbered
</code></pre>
<p>如何允许所有出站和入站连接：</p>
<pre><code>ufw default allow outgoing
ufw default allow incoming
</code></pre>
<p>如何允许规则：</p>
<pre><code>ufw allow 22
ufw allow 1000:2000/tcp
ufw allow from 172.16.238.5 to any port 22 proto tcp
ufw allow from 172.16.238.5 to any port 80 proto tcp
ufw allow from 172.16.100.0/28 to any port 80 proto tcp
</code></pre>
<p>如何拒绝规则：</p>
<pre><code>ufw deny 8080
</code></pre>
<p>如何启用和激活防火墙：</p>
<pre><code>ufw enable
</code></pre>
<p>如何删除规则：</p>
<pre><code>ufw delete deny 8080
ufw delete &lt;rule-line&gt;
</code></pre>
<p>如何重置规则：</p>
<pre><code>ufw reset
</code></pre>
<h3 id="linux">Linux 系统调用</h3>
<p>Linux 系统调用用于从用户空间向 Linux 内核发出请求。 例如，在创建文件时，用户空间向 Linux 内核发出创建文件的请求。</p>
<p>内核空间有以下内容：</p>
<ul>
<li>Kernel Code</li>
<li>Kernel Extensions</li>
<li>Device Drivers</li>
</ul>
<h4 id="strace">如何使用 Strace 跟踪系统调用</h4>
<p>以下是使用 strace 跟踪系统调用的方法：</p>
<pre><code>which strace
strace touch /tmp/error.log
</code></pre>
<p>如何获取服务的PID：</p>
<pre><code>pidof sshd
strace -p &lt;pid&gt;
</code></pre>
<p>如何列出操作期间进行的所有系统调用：</p>
<pre><code>strace -c touch /tmp/error.log
</code></pre>
<p>如何合并列出的系统调用：（计数和总结）</p>
<pre><code>strace -cw ls /
</code></pre>
<p>如何遵循 PID 并进行整合：</p>
<pre><code>strace -p 3502 -f -cw
</code></pre>
<h3 id="aquasectracee">AquaSec Tracee</h3>
<p>AquaSec Tracee 由 Aqua Security 创建，它使用 eBPF 来跟踪容器中的事件。 Tracee 在运行时直接在内核空间中使用 eBPF（Extended Berkeley Packet Filter），而不会干扰内核源代码或加载任何内核模块。</p>
<ul>
<li>二进制存储在<code>/tmp/tracee</code></li>
<li>如果使用具有 <code>--privileged</code> 能力的容器运行，则需要以只读模式访问以下内容：
<ul>
<li><code>/tmp/tracee</code> -&gt; 默认工作区</li>
<li><code>/lib/modules</code> -&gt; 内核头文件</li>
<li><code>/usr/src</code> -&gt; 内核头文件</li>
</ul>
</li>
</ul>
<p>如何让 Docker 容器中的 Tracee 变得有趣：</p>
<pre><code>docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace comm=ls

# 列出主机上所有新进程的系统调用
docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace pid=new

# 列出来自任何新容器的系统调用
docker run --name tracee --rm --privileged --pid=host \
  -v /lib/modules/:/lib/modules/:ro -v /usr/src/:/usr/src/ro \
  -v /tmp/tracee:/tmp/tracee aquasec/tracee:0.4.0 --trace container=new
</code></pre>
<h3 id="seccomp">如何使用 Seccomp 限制系统调用</h3>
<p><strong>SECCOMP</strong> – 安全计算模式 – 是一种 Linux 内核级别的功能，您可以将其用于沙箱应用程序以仅使用它们需要的系统调用。</p>
<p>如何检查对 seccomp 的支持：</p>
<pre><code>grep -i seccomp /boot/config-$(uname -r)
</code></pre>
<p>如何测试更改系统时间：</p>
<pre><code>docker run -it --rm docker/whalesay /bin/sh
# date -s '19 APR 2013 22:00:00'

ps -ef
</code></pre>
<p>如何检查任何 PID 的 seccomp 状态：</p>
<pre><code>grep -i seccomp /proc/1/status
</code></pre>
<p>Seccomp 模式：</p>
<ul>
<li>模式 0：禁用</li>
<li>模式一：严格</li>
<li>模式2：过滤</li>
</ul>
<p>以下配置用于将系统调用列入白名单。 白名单配置文件是安全的，但必须有选择地启用系统调用，因为它默认阻止所有系统调用。</p>
<pre><code>{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "&lt;syscall-1&gt;",
        "&lt;syscall-2&gt;",
        "&lt;syscall-3&gt;"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
</code></pre>
<p>以下配置用于将系统调用列入黑名单。 黑名单配置文件比白名单具有更大的攻击面。</p>
<pre><code>{
  "defaultAction": "SCMP_ACT_ALLOW",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "&lt;syscall-1&gt;",
        "&lt;syscall-2&gt;",
        "&lt;syscall-3&gt;"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
</code></pre>
<p>Docker seccomp 配置文件阻止了 x86 架构上 300 多个系统调用中的 60 个。</p>
<p>如何在 Docker 中使用 seccomp 配置文件：</p>
<pre><code>docker run -it --rm --security-opt seccomp=/root/custom.json docker/whalesay /bin/sh
</code></pre>
<p>如何允许容器的所有系统调用：</p>
<pre><code>docker run -it --rm --security-opt seccomp=unconfined docker/whalesay /bin/sh

# Verify
grep -i seccomp /proc/1/status

# Output should be:
Seccomp:         0
</code></pre>
<p>如何使用 Docker 容器获取容器运行时相关信息：</p>
<pre><code>docker run r.j3ss.co/amicontained amicontained
</code></pre>
<h4 id="kubernetesseccomp">Kubernetes 中的 Seccomp</h4>
<p>安全计算模式（SECCOMP）是 Linux 内核的一项功能。 您可以使用它来限制容器内可用的操作。 <a href="https://kubernetes.io/docs/tutorials/clusters/seccomp">Seccomp 文档</a></p>
<p>如何在 Kubernetes 中运行 amicontained：</p>
<pre><code>kubectl run amicontained --image r.j3ss.co/amicontained amicontained -- amicontained
</code></pre>
<p>从 <code>v1.20</code> 版本开始，Kubernetes 默认不实现 seccomp。</p>
<p>Kubernetes 中的 Seccomp 'RuntimeDefault' docker 配置文件：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  labels:
    run: amicontained
  name: amicontained
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - args:
    - amicontained
    image: r.j3ss.co/amicontained
    name: amicontained
    securityContext:
      allowPrivilegeEscalation: false
</code></pre>
<p>kubelets 中的默认 seccomp 位置</p>
<pre><code>/var/lib/kubelet/seccomp
</code></pre>
<p>如何在节点中创建 seccomp 配置文件：</p>
<pre><code>mkdir -p /var/lib/kubelet/seccomp/profiles

# 添加配置文件进行审计
vim /var/lib/kubelet/seccomp/profiles/audit.json
&gt;&gt;&gt;
{
  defaultAction: "SCMP_ACT_LOG"
}

# 添加违规配置文件（默认阻止所有系统调用，不会让任何东西运行）

vim /var/lib/kubelet/seccomp/profiles/violation.json

&gt;&gt;&gt;
{
  defaultAction: "SCMP_ACT_ERRNO"
}
</code></pre>
<p>Local seccomp profile – this file should exist locally on a node to be able to work:</p>
<pre><code>...
securityContext:
  seccompProfile:
    type: Localhost
    localhostProfile: profiles/audit.json
...
</code></pre>
<p>上述配置文件将使系统调用能够保存到文件中。</p>
<pre><code>grep syscall /var/log/syslog
</code></pre>
<p>如何将系统调用号映射到系统调用名称：</p>
<pre><code>grep -w 35 /usr/include/asm/unistd_64.h

# OR
grep -w 35 /usr/include/asm-generic/unistd.h
</code></pre>
<h3 id="apparmor">AppArmor</h3>
<p>AppArmor 是一个 Linux 安全模块，用于将程序限制在一组有限的资源中。</p>
<p>如何安装 AppArmor 工具：</p>
<pre><code>apt-get install apparmor-utils
</code></pre>
<p>如何检查 AppArmor 是否正在运行和启用：</p>
<pre><code>systemctl status apparmor

cat /sys/module/apparmor/parameters/enabled
Y
</code></pre>
<p>AppArmor 配置文件存储在：</p>
<pre><code>cat /etc/apparmor.d/root.add_data.sh
</code></pre>
<p>如何列出 AppArmor 配置文件：</p>
<pre><code>cat /sys/kernel/security/apparmor/profiles
</code></pre>
<p>如何拒绝所有文件写入配置文件：</p>
<pre><code>profile apparmor-deny-write flags=(attach_disconnected) {
  file,
  # Deny all file writes.
  deny /** w,
}
</code></pre>
<p>如何拒绝写入 <code>/proc</code> 文件：</p>
<pre><code>profile apparmor-deny-proc-write flags=(attach_disconnected) {
  file,
  # Deny all file writes.
  deny /proc/* w,
}
</code></pre>
<p>如何拒绝重新挂载根 FS：</p>
<pre><code>profile apparmor-deny-remount-root flags=(attach_disconnected) {

  # Deny all file writes.
  deny mount options=(ro, remount) -&gt; /,
}
</code></pre>
<p>如何查看个人资料状态：</p>
<pre><code>aa-status
</code></pre>
<p>配置文件加载模式</p>
<ul>
<li><code>Enforce</code>，监控和执行规则</li>
<li><code>Complain</code>，不会强制执行规则，但会将它们记录为事件</li>
<li><code>Unconfined</code>，不会强制执行或记录事件</li>
</ul>
<p>如何检查配置文件是否有效：</p>
<pre><code>apparmor_parser /etc/apparmor.d/root.add_data.sh
</code></pre>
<p>如何禁用配置文件：</p>
<pre><code>apparmor_parser -R /etc/apparmor.d/root.add_data.sh
ln -s /etc/apparmor.d/root.add_data.sh /etc/apparmor.d/disable/
</code></pre>
<p>如何生成个人资料并回答以下一系列问题：</p>
<pre><code>aa-genprof /root/add_data.sh
</code></pre>
<p>如何为命令生成配置文件：</p>
<pre><code>aa-genprof curl
</code></pre>
<p>如何从日志中禁用配置文件：</p>
<pre><code>aa-logprof
</code></pre>
<h4 id="kubernetesapparmor">如何在 Kubernetes 中使用 AppArmor</h4>
<p>要将 AppArmor 与 Kubernetes 一起使用，必须满足以下先决条件：</p>
<ul>
<li>Kubernetes 版本应该大于 <code>1.4</code></li>
<li>应启用 AppArmor 内核模块</li>
<li>AppArmor 配置文件应该加载到内核中</li>
<li>应支持容器运行时</li>
</ul>
<p>Kubernetes 中的示例用法：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
  annotations:
    container.apparmor.security.beta.kubernetes.io/&lt;container-name&gt;: localhost/&lt;profile-name&gt;
spec:
  containers:
  - name: ubuntu-sleeper
    image: ubuntu
    command: ["sh", "-c", "echo 'Sleeping for an hour!' &amp;&amp; sleep 1h"]
</code></pre>
<p><strong>注意</strong>：容器应在包含 AppArmor 配置文件的节点中运行。</p>
<h3 id="linux">Linux 功能</h3>
<p>Linux 功能特性将可用于以“root”用户身份运行的进程的权限分解为更小的权限组。 这样，以“root”权限运行的进程可以被限制为仅获得执行其操作所需的最小权限。</p>
<p>Docker 支持 Linux 功能作为 Docker 运行命令的一部分：使用 <code>--cap-add</code> 和 <code>--cap-drop</code>。 默认情况下，容器启动时具有默认允许且可以删除的多个功能。 其他权限可以手动添加。</p>
<p><code>--cap-add</code> 和 <code>--cap-drop</code> 都支持 ALL 值，以允许或删除所有功能。 默认情况下，Docker 容器运行 14 种功能。</p>
<ul>
<li>Kernel &lt; 2.2
<ul>
<li>Privileged Process</li>
<li>Unprivileged Process</li>
</ul>
</li>
<li>Kernel &gt;= 2.2
<ul>
<li>Privileged Process
<ul>
<li><code>CAP_CHOWN</code></li>
<li><code>CAP_SYS_TIME</code></li>
<li><code>CAP_SYS_BOOT</code></li>
<li><code>CAP_NET_ADMIN</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<p><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">有关 Linux 功能的完整列表，请参阅此文档</a>。</p>
<p>如何检查命令需要哪些功能：</p>
<pre><code>getcap /usr/bin/ping
</code></pre>
<p>如何获得进程能力：</p>
<pre><code>getpcaps &lt;pid&gt;
</code></pre>
<p>如何添加安全功能：</p>
<pre><code>apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-sleeper
spec:
  containers:
  - name: ubuntu-sleeper
    image: ubuntu
    command: ["sleep", "1000"]
    securityContext:
      capabilities:
        add: ["SYS_TIME"]
        drop: ["CHOWN"]
</code></pre>
<h2 id="howtopreparefortheexam">如何准备考试</h2>
<p>CKS 被认为是一项非常艰难的考试。 但根据我的经验，我认为，只要练习足够好，并且如果你理解考试涵盖的概念，两小时内就会很容易掌握。</p>
<p>你肯定需要了解底层的 Kubernetes 概念。 由于 CKS 的先决条件是通过 CKA 考试，因此在尝试 CKS 之前，您应该对 Kubernetes 及其运作方式有深入的了解。</p>
<p>此外，要通过 CKS，您需要了解容器编排带来的威胁和安全隐患。</p>
<p>引入 CKS 考试表明容器的安全性不容轻视。安全机制应始终到位，以阻止对 Kubernetes 集群的攻击。</p>
<p><a href="https://www.wired.com/story/cryptojacking-tesla-amazon-cloud/">特斯拉加密货币黑客事件</a> 是由于一个未受保护的 Kubernetes 控制面板造成的，揭示了与 Kubernetes 或任何其他容器编排引擎相关的风险。<a href="https://hackerone.com/kubernetes?type=team">Hackerone 有一个 Kubernetes 赏金页面</a> 列出了标准 Kubernetes 集群中使用的源代码仓库。</p>
<h2 id="practicepracticeandpractice">练习，练习，再练习！</h2>
<p>练习是破解考试的关键，我个人发现 KodeKloud 和 Killer.sh 的考试模拟器对我帮助很大。</p>
<p>我没有像准备 CKA 考试那样多的时间来准备 CKS 考试，但是我在日常工作中正在研究 Kubernetes，所以我对它已经非常熟悉了。</p>
<p>实践是成功的关键。祝考试顺利！</p>
<p><em>您可以在 <a href="https://faizanbashir.me/rough-guide-to-qualifying-the-cks-exam-in-a-hurry">faizanbashir.me</a>阅读这篇文章和其他文章</em></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何成为 Kubernetes 认证开发者（CKAD） ]]>
                </title>
                <description>
                    <![CDATA[ 本指南是我对最近通过的K8S认证开发者考试的学习笔记总结。 即使你对认证不感兴趣，你可以把K8S看作一站式商店：通过无数的例子解释所有主要的技术概念。 此外，它还有一些来自我准备和参加考试经验和附加内容。 在写本文时，CKAD的课程（研究领域和每个领域的比重）如下：  * 13% – 核心概念  * 18% – 配置  * 10% – 多容器 Pods  * 18% – 可观察性  * 20% – Pod 设计  * 13% – 服务 & 网络  * 8% – 状态持久性 本指南涵盖了这些课程，只是顺序不同。 我假设你已经知道K8S的基础知识（基本的containers和pods），并希望将你的技能提高一个新的水平。通过这个考试将使你的简历脱颖而出，因为它是一个非常抢手的认证。 内容  * Kubernetes简介  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-become-a-certified-kubernetes-application-developer/</link>
                <guid isPermaLink="false">61d067fdd97f9306419f5162</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Sat, 01 Jan 2022 04:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/01/kubernetes-ckad-color-1024x1003.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-become-a-certified-kubernetes-application-developer/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Become a Certified Kubernetes Application Developer</a>
      </p><!--kg-card-begin: markdown--><p>本指南是我对最近通过的K8S认证开发者考试的学习笔记总结。</p>
<p>即使你对认证不感兴趣，你可以把K8S看作<code>一站式商店</code>：通过无数的例子解释所有主要的技术概念。</p>
<p>此外，它还有一些来自我准备和参加考试经验和附加内容。</p>
<p>在写本文时，CKAD的课程（研究领域和每个领域的比重）如下：</p>
<ul>
<li>13% – 核心概念</li>
<li>18% – 配置</li>
<li>10% – 多容器 Pods</li>
<li>18% – 可观察性</li>
<li>20% – Pod 设计</li>
<li>13% – 服务 &amp; 网络</li>
<li>8% – 状态持久性</li>
</ul>
<p>本指南涵盖了这些课程，只是顺序不同。</p>
<p>我假设你已经知道K8S的基础知识（基本的containers和pods），并希望将你的技能提高一个新的水平。通过这个考试将使你的简历脱颖而出，因为它是一个非常抢手的认证。</p>
<h2 id="">内容</h2>
<ul>
<li>Kubernetes简介</li>
<li>如何管理Kubernetes的集群</li>
<li>超越Pods和Deployments</li>
<li>如何配置Pods和Containers</li>
<li>如何在Kubernetes调度Pods</li>
<li>Kubernetes的存储</li>
<li>Kubernetes的网络和安全</li>
<li>Kubernetes中的观察性和调试</li>
<li>技巧和窍门</li>
<li>实践时间</li>
<li>结论</li>
</ul>
<h2 id="kubernetes">介绍Kubernetes</h2>
<p>Kubernetes是一项技术，允许在多个节点上轻松部署和管理容器化程序。它的一些最突出的特点是:</p>
<ul>
<li>Container 配置和部署</li>
<li>系统监控</li>
<li>持久性存储</li>
<li>自动调度</li>
<li>自动扩展和自动修复</li>
</ul>
<p>还有更多。</p>
<p>Kubernetes的工作方式是声明式的：你在集群中定义你想要的状态，Kubernetes是确保集群始终处于这种状态。</p>
<ul>
<li>REST 调用</li>
<li>命令行工具<code>kubectl</code>。你可以通过这个教程，在你的机器上安装它，<a href="https://kubernetes.io/docs/tasks/tools/">点击这里</a>。</li>
</ul>
<p>如果你不能访问一个Kubernetes集群,我建议在你的本地机器上安装<a href="https://minikube.sigs.k8s.io/docs/start/">minikube</a>。一旦完成安装并启动，允许以下命令来创建你的第一个Pod。</p>
<pre><code class="language-yaml">kubectl run --image=busybox --restart=Never --rm -it -- echo "Welcome to Kubernetes!!"
</code></pre>
<p>这个pod一旦打印出欢迎消息，它将被自动删除。</p>
<h2 id="kubernetes">如果管理Kubernetes集群</h2>
<p>管理集群不是成为CKAD的课程的一部分。就考试而言，你不需要知道如何如何创建一个集群，管理nodes等。</p>
<h3 id="namespaces">Namespaces</h3>
<p>Namespaces 允许你创建虚拟集群，也就是说将资源隔离在同一物理集群的不同部分。比如说:</p>
<ul>
<li>将以下不同的环境隔开 development, stage, QA, and production</li>
<li>将一个复杂的系统分解成更小的子系统，你可以为前端组件创建一个Namespace,也可以为后端组件创建另一个Namespace，以此类推。</li>
<li>避免了名称冲突: 同一资源可以在不同的Namespace中以相同的名称创建。这使得创建不同的环境 (think stage and prod)，看起来相同。</li>
</ul>
<p>你可以通过运行以下命令创建一个Namespace:</p>
<pre><code class="language-bash">kubectl create ns my-namespace
</code></pre>
<h3 id="">资源配额</h3>
<p>如果你想限制开发者在Namespace中可以创建的资源数量（包括物理资源和Kubernetes对象，如pods），你可以使用<a href="https://kubernetes.io/docs/concepts/policy/resource-quotas/">资源配额</a>。</p>
<p>例如，你可以通过<code>ResourceQuota</code>下的Kubernetes <em>secrets</em> 的数量来限制用户在集群上创建:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: ResourceQuota
metadata:
  name: my-quota
spec:
  hard:
    secrets: "2"
</code></pre>
<p>在一个文件中，我们称之为<code>my-qouta.yaml</code>，可以简单运行<code>kubectl apply -f my-quota.yaml</code></p>
<pre><code class="language-bash">kubectl create quota my-quota --hard="secrets=2"
</code></pre>
<p>更多的 <em>秘密</em> 在这个指南的后面。</p>
<h3 id="labels">Labels</h3>
<p>Labels（标签）允许你在Kubernetes集群组织资源，label是一个键值对，你可以在创建资源时，添加到资源上，也可以添加到现有资源。你能使用labels过滤资源。</p>
<p>例如：</p>
<ul>
<li>你想只显示 <code>backend</code> pods。你将label <code>tier=backed</code>添加到pods（键和值都是任意的，你可以使用任何你想的），然后运行 <code>kubectl get pods -l tier=backend</code>去检索需要的pods。</li>
<li>你想定义一个部署或服务的相关pods。告诉 deployment/service，它需要关注通过labels选择pods。</li>
</ul>
<p>以下是一些常见的label-related的命令：</p>
<pre><code class="language-bash"># 给Pods 添加一个label, 等等
kubectl label pods pod-name key=value
# 通过podname 删除pods上的label
kubectl label pods pod-name label_key-

# 删除一个pod，通过label选择器
kubectl label po -l app app-

# 显示 label
kubectl get pods --show-labels

# 通过lables选择pods
kubect get pods -l 'tier in (frontend,backend)'
kubect get pods -l tier=frontend,deployer=coolest-team
</code></pre>
<p>Annotations和labels相似，都是键值对，但Annotations不能用于选择资源，它们的目的是不同的。</p>
<p>Annotations通常为其他工具添加。比如，如果你运行Prometheus来收集指标，你可以把这个配置添加到你的描述符中。</p>
<pre><code class="language-yaml">metadata:
	labels:
		name: fluentd-elasticsearch
	annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '9102'
</code></pre>
<p>它让Prometheus将 <em>scrape</em> 和 <em>port</em> 的默认值分别改为 <code>true</code>和 <code>9102</code>。</p>
<h2 id="pods">超越 Pods和部署</h2>
<p>Multi-container pods是Kubernetes的基本单元。在大多数情况下，你可以认为它是一个容器，但是一个pod可以由多个容器组成。由于pod是短暂的，我们需要一种机制确保当我们现有的pod死亡时，新的pod被创建。</p>
<p>通过部署，你的可以定义一个理想的状态，例如，有三个应用程序的副本一直在运行，Kubernetes将努力实现并在集群中保持这种状态。</p>
<p>也可以轻松管理任何时候运行的副本数量，执行滚动更新，回滚到以前的版本等等。</p>
<p>还有很多工作负荷。</p>
<h3 id="pod">多容器 pod</h3>
<p>一个pod可以运行一个以上的容器。容器间可以无缝通讯，因为它们在同一网络和而且它们可以使用 <em>volumes</em> 共享数据。</p>
<p>现在，让我们深入了解一些多容器pod设计模式，在本指南后面，我们将详细介绍volumes(卷),以及如何部署这些模式中的一些。</p>
<h4 id="sidecar">Sidecar(边车)</h4>
<p>在这个模式中，我们有一个运行主程序的容器，同时还有另一个容器，运行次要任务 <em><strong>以增强主容器的功能</strong></em>。</p>
<p>一个经典的例子是，在运行Web服务(主容器)的同时，还有一个处理日志、监控、刷新pod volume(卷)的数据，终端TLS等任务的<code>side</code> 容器</p>
<h4 id="">适配器</h4>
<p>同样，你可以有你的主容器和一个次要容器， <strong>转换主容器的输出</strong></p>
<p>例如，想象一些你的主容器运行一个输出大量复杂日志的服务。你可以使用一个适配容器来简化这个输出，然后再发送给你的日志服务器，把主容器(以及服务的开发者)从这个服务中解放出来。</p>
<h4 id="ambassador">Ambassador</h4>
<p>另外一个常见选择是使用以辅助容器， <strong>作为一个代理</strong>，在你的主容器和外部之间充当代理。</p>
<p>例如，你可能有不同的数据库用于不同的容器，比如测试和生产。当你的主容器需要连接到一个数据库时，它可以根据环境确定适合的数据库给 <code>ambassador</code>容器。</p>
<h3 id="">服务</h3>
<p>Pods 可以通过其他的Pods的IP地址，连接其他的Pods。然而，pods是短暂的，当它们死亡时，假设你有个复控制器，一个新的pod将被创建，有一个新的IP地址。这使人们很难直接使用IP连接到pods。</p>
<p>Kubernetes提供了一个名为服务的抽象，<strong>一个资源有固定的IP地址</strong> ，并将请求发送到对应的pod。</p>
<p>不需要直接连接到pods，你可以通过它的IP地址到达它们的服务。或者通过DNS服务使用完整限定名称查找，这样会更好。此外，一些服务类型也将你的应用程序暴露在集群外面。</p>
<h4 id="">服务类型</h4>
<p>你可以创建的主要服务类型:</p>
<ul>
<li><strong>ClusterIP –</strong> 在集群内暴露你的应用。如果你不指定类型，这就是Kubernetes创建的默认服务。</li>
<li><strong>NodePort –</strong>  它会在集群中的每一个node打开一个端口，将你的应用程序暴露给世界。</li>
<li><strong>LoadBalancer –</strong>  使用云服务提供商的负载均衡器向外部暴露服务。</li>
</ul>
<h4 id="">如何在集群内部暴露你的应用程序</h4>
<p>假设你想把你的应用程序<code>my-app</code>暴露给集群中的其他节点，端口为80。你可以创建一个命令部署你的应用程序。</p>
<pre><code class="language-bash">kubectl create deploy my-app --image=my-app --port=80
</code></pre>
<p>这将创建集群内部其他的Pod只能通过IP访问的pod，如果pod重新启动，其IP将改变。</p>
<p>下一步是为这些pod创建一个ClusterIP服务。下面的命令创建一个可以通过80端口的服务，并将请求转发到的你的应用程序(80端口)。</p>
<pre><code class="language-bash">kubectl expose deploy my-app --port=80
</code></pre>
<h4 id="">如何将你的应用程序暴露在集群之外</h4>
<p>如果你想把你的应用服务暴露给世界，你可以使用这个配置创建一个NodePort服务。</p>
<pre><code class="language-yaml">kind: Service
apiVersion: v1
metadata:
	name: my-svc
spec:
    type: NodePort
    selector:
	    app: nginx
    ports:
    - protocol: TCP
    	port: 80 # 你可以到达该服务的端口
    	targetPort: 80 # 你在pod打开的端口
</code></pre>
<h3 id="ingress"><a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">Ingress</a></h3>
<p>服务并不是将你的应用程序给世界的唯一方法。你也可以用 <strong>Ingress对象</strong>作为你的集群的一个入口点。</p>
<p>除此之外，你还需要一个 <strong>Ingress控制器</strong>，比如Nginx,来实现对Ingress对象中定义的规则。</p>
<p>由于Ingresses不在CKAD的范围内，我们将转到其他的主题。</p>
<h3 id="jobs"><a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/">Jobs</a></h3>
<p>一个Job将创建一个或多个pods  <strong>如果它们成功，将不会被重启启动</strong>。你可以使用它们来执行批处理, <strong>一次性任务</strong> ，如并行计算，备份一些文件，转换和导出一些数据等等。</p>
<p>除非另有说明, pod 将运行一次。你可以使用参数 <code>completions</code>来指定一项工作需要运行多少次才能被认为已经完成。默认情况下, pods将按照顺序运行，但你可以设置job，使它们并行运行。</p>
<p>如果它们没有成功运行, 你将它们配置为 <code>Never</code> 重启 (重试)。或者重启失败最多的<code>backoffLimit</code> 次数，<br>
然后Kubernetes认为job已经失败。</p>
<p>下面是如何从命令行中创建一个简单的job:</p>
<pre><code class="language-bash"> kubectl create job my-job --image=busybox -- echo "Hello World"
</code></pre>
<p>如果它们的job运行成功，其状态是<code>COMPLETED</code>,其pod(s)不会被删除，这样你就可以访问它们的日志。你可以通过以下方式看到它们。</p>
<pre><code class="language-bash">kubectll get pods --show-all
</code></pre>
<p>在默认情况下，2分钟后它们在默认的pod列表中是不可见的。</p>
<h3 id="cronjobs"><a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/">Cronjobs</a></h3>
<p>Kubernetes提供了<code>Conjobs</code>来创建需要定期或者在未来特定时间运行的<code>jobs</code>：定时清理和备份任务，更新TLS证书等等。</p>
<p>Kubernetes会尽力只运行另一个<code>job</code>来执行你在<code>Cronjob</code>配置中指定的任务。然而，有3个常见的问题你应该注意。</p>
<ul>
<li>不能保证job会 <strong>准确地在所需的时间运行</strong>。想象一下，你需要你的工作在 上午9点运行，你可以将<code>startingDeadlineSeconds</code>属性设置为x秒。这样，如果job没在上午9点x秒之后开始， 它将标记为失败，不会再运行。</li>
<li><strong>2个job可能被安排在同一时间</strong> 执行任务。在这种情况下，你需要确保任务是 <em>idempotent</em> ，如果任务被多次执行，执行的任务的结果也不会改变。</li>
<li><strong>没有job可以安排</strong>。 为了克服这个问题，请确保Cronjob能运行前一次未完成的job。</li>
</ul>
<p>这是创建一个简单的Cronjob，每分钟打印出"Hello World"</p>
<pre><code class="language-bash">kubectl create cronjob my-job --image=busybox --schedule="*/1 * * * *" -- echo "Hello World"
</code></pre>
<p>我推荐这个 <a href="https://crontab.guru/">网站</a> 帮助你写出正确的cron定时任务。</p>
<p>接下的3个资源不是CKAD考试的一部分，但我认为你至少应该对它们有一个基本的了解。</p>
<h3 id="daemonsets"><a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/">Daemon sets</a></h3>
<p>Daemon sets(守护设置)确保在你的集群的每一个 <strong>node</strong> 都有一个pod 调度。除此以外，pod总是在运行:如果pod死了或者有人删除了它，pod将被重新创建。</p>
<p>一个常见的使用场景是，Daemon sets(守护设置)用来收集每个node的日志和指标。但是，即使你不创建任何东西，它们已经在你的集群中运行了一些Daemo sets(守护设置): Kubernetese创建一个 Daemon set(守护设置)，在每个node上运行<code>kube-proxy</code> 组件。</p>
<p>如果你从集群中移除一个node，Kubernetes不会在别的node上创建它的守护进程，因为这个这个node已经在运行Daemon set(守护设置)。如果你在集群中添加一个node，它会自动运行 Daemon set(守护设置)。</p>
<h3 id="statefulset"><a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/">Stateful set</a></h3>
<p>到目前为止，我们应该看到部署无状态的应用或者pods的工具，它们有自己的持久性储存，不随工具停止运行而消失。为了部署有状态的应用，你可以是使用 Stateful set(状态设置)</p>
<p>由于这不是CKAD考试的一部分，我们就不多讲了。</p>
<h3 id="pods"><a href="https://kubernetes.io/docs/tasks/configure-pod-container/static-pod/">静态 pods</a></h3>
<p>静态pods是由<code>kubelet</code>管理的颇多，而不是由Kubernetes API管理的。</p>
<p>要创建它们，你只需要创建一个普通的pod的配置文件和配置kubelet 扫描该目录。如何你重启<code>kubelet</code>后，它将创建pod，并在失败时重启pod。</p>
<p>Kubernetes将创建pod的镜像，这是Kubernetes API服务器中的一个副本。当你运行<code>kubectl get pods</code>，这个pod会出现，但是如果你试图用<code>kubectl delete podName</code>删除它，通过<code>kubelet</code>创建的静态pod 运行在这个node上，它将直接出现在pod列表中。</p>
<h2 id="podscontainers">如何配置 Pods和Containers(容器)</h2>
<p>我们刚刚看到在可以在Kubernetes集群上部署不同的工作负载。现在让我们花些时间学习如何配置工作负载中的pods和containers(容器)。</p>
<h3 id=""><a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-initialization/">初始化容器</a></h3>
<p>你可以使用  <em>init containers</em> 来设置你的pod初始状态: 向pod的卷写入一些数据，下载些文件等等。</p>
<p>你可以定义一个或者多个init容器，它们将按顺序执行，只在所有的容器完成后，pod才会运行，因此，init容器也可以用来让pod在执行前等待某个条件。</p>
<p>例如，你可以让一个pod在启动前等待另一个服务的容器完成启动和运行。</p>
<p>你可以通过在pod的定义的 <code>spec</code>部分添加类似的这样的内容定义init容器。</p>
<pre><code class="language-yaml">spec:
  initContainers:
  - name: init-myservice
    image: busybox:1.28
    command: ['sh', '-c', "until nslookup myservice.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done"]
 ...
</code></pre>
<h3 id="configmaps"><a href="https://kubernetes.io/docs/concepts/configuration/configmap/">ConfigMaps</a></h3>
<p>将你的应用程序与代码分开是你应该遵循的实践。ConfigMaps 允许你在Kubernetes这样做。</p>
<p>ConfigMaps 是使用 <strong>存储非机密数据的键值对</strong>， 我们将在下一节看到  <em>Secrects</em> 存储机密数据(如密码)。</p>
<pre><code class="language-bash"># 通过参数传递数值
kubectl create configmap my-map --from-literal=db_url=my-url --from-literal=username=username

# 通过文件获取数值
kubectl create configmap another-map --from-file=my-file
</code></pre>
<p>一旦创建，你的应用程序可以在同一个namespace的pod以多种方式使用ConfigMaps。</p>
<ul>
<li>作为命令行参数</li>
<li>作为环境变量</li>
<li>从只读卷中的一个文件</li>
</ul>
<p>让我们看看一个pod的定义使用这些办法从ConfigMap中读取。</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  restartPolicy: Never
  containers:
    - name: busybox
      image: busybox
      args:
        - /bin/sh
        - -c
        - "echo $MY_VARIABLE" # This value comes from the configmap
      env:
      - name: MY_VARIABLE
        valueFrom:
          configMapKeyRef:
            name: another-map           
            key: my-key # To load individual keys from a map
      envFrom:
      - configMapRef:
          name: another-map # To import all values from a map as env variables
      volumeMounts:
      - name: config
        mountPath: "/config" # This will contain files with the data stored in my-map
        readOnly: true
  volumes:
    # Name to refer to this volume in the pod
    - name: config
      configMap:
        # Name of the ConfigMap
        name: my-map
</code></pre>
<h3 id="secrets"><a href="https://kubernetes.io/docs/concepts/configuration/secret/">Secrets</a></h3>
<p><code>Secrets</code>跟ConfigMaps很相似，但你用它们来存储 <strong>机密数据</strong>。创建和管理<code>Secrets</code>是一个敏感的话题。请务必阅读文档。在这里，我们将学习基础知识。</p>
<p>创建<code>Secrets</code>的最简单方法是:</p>
<pre><code class="language-bash">#从一个字面量创建 secret
kubectl create secret generic secret-name --from-literal=password=password
#从文件内容来创建 secret
kubectl create secret generic secret-name --from-file=path-to-file
</code></pre>
<p>然后，你可以将你的secrets导入pod，以环境变量或者文件的形式:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: another-pod
spec:
  restartPolicy: Never
  containers:
    - name: busybox
      image: busybox
      args:
        - /bin/sh
        - -c
        - "echo $MY_VARIABLE"
      env:
      - name: MY_VARIABLE
        valueFrom:
          secretKeyRef:
            name: my-secret2
            key: username # To load individual keys from a map
      envFrom:
      - secretRef:
          name: yet-another-secret # To import all values from a map as env variables
      volumeMounts:
      - name: secret-volume
        mountPath: "/secrets" # This will contain files with the data stored in my-map
        readOnly: true
  volumes:
    # Name to refer to this volume in the pod
    - name: secret-volume
      secret:
        # Name of the Secret
        secretName: my-secret
</code></pre>
<h3 id="probes"><a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">Probes</a></h3>
<p>Probes(探针)允许你设置判断pod是否健康的规则，是否为服务流量做好准备，是否准备好启动。在本节的最后，我将介绍一个带有这些Probes(探针)的样本。</p>
<h4 id="livenessprobes">Liveness probes(活性探针)</h4>
<p>Kubernetes 会自动重启 <em>crashed containers</em> (崩溃的容器)。然而,这不会考虑到下面的bug (像你的应用程序的无限循环), 死锁等等。 你可以定义 <code>Liveness probes</code>(活性探针) <strong>检测你的应用程序是否健康</strong>。 Kubernetes 使用这探针来决定是否要重启容器。</p>
<p>你可以定义三种Liveness probes(活性探针):</p>
<ul>
<li>HTTP Get, 在你的应用程序中定义一个HTTP路由(如/health)，Kubernetes可以通过HTTP请求获取应用程序是否健康</li>
<li>TCP socket 探针，尝试建立一个TCP连接到一个特定的端口。如果连接不能建立，应用程序将重启。</li>
<li>Exec 探针,  在容器内运行一个命令，如果退出状态码不是0，将重启容器。</li>
</ul>
<h4 id="readinessprobes">Readiness probes(准备就绪探针)</h4>
<p>想象一下，一个服务在启动时需要加载一个大的配置文件。容器本身是健康的(这可以用上面的Liveness probes检查)，但是没有启动完，接收请求。</p>
<p>Kubernetes使用Readiness probes(准备就绪探针)来确定你的应用程序是否准备好，处理请求。</p>
<p>关于Liveness probes(活性探针)的东西也适用于Readiness probes(准备就绪探针):</p>
<ul>
<li>它们可以被配置为初始延迟，超时，阈值，周期等等。</li>
<li>它们可以基于HTTP get调用，TCP socket连接，或者在容器内执行命令。</li>
</ul>
<p>然而，当Liveness probes(活性探针)告诉Kubernetes信息，Kubernetes让不健康的容器重启。Readiness probes(准备就绪探针)用来从可以接收请求的容器池移除没准备好的容器。一旦容器准备好了，它就可以为请求提供服务。</p>
<h4 id="startupprobes">Startup probes(启动探针)</h4>
<p>Startup probes(启动探针)只能在启动期间使用，例如应用程序启动缓慢。如果Startup probes(启动探针)返回成功，liveness and readiness 探针(如果已经配置)，将定期运行。</p>
<h3 id="">例如</h3>
<p>这个pod 将执行以下命令:</p>
<ul>
<li>sleep 20</li>
<li>touch /tmp/healthy</li>
<li>sleep 30</li>
<li>rm -rf /tmp/healthy</li>
<li>sleep 600</li>
</ul>
<p>在配置探针时，你应该注意的最基本参数是:</p>
<ul>
<li><strong>initialDelaySeconds</strong> 开始探针延迟秒数</li>
<li><strong>periodSeconds</strong> 探针频率</li>
<li><strong>timeoutSeconds</strong>   探针的超时时间</li>
<li><strong>failureThreshold</strong> Kubernetes尝试次数，超过这个，将放弃尝试<br>
根据我们的配置, 这些探针将在pod创建后10秒后开始。在前20秒内, 文件 <code>/tmp/healthy</code>是不存在。因此, readiness probe(准备就绪探针)返回失败。 在接下30秒内readiness probe(准备就绪探针)返回成功，直到我们删除<code>/tmp/healthy</code>文件。</li>
</ul>
<p>liveness probe(活性探针) 是简单的<code>echo "I'm healthy"</code>命令。如果它能运行，说明这个pod是健康的，否则，这个pod 要重启。</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - args:
    - /bin/sh
    - -c
    - "sleep 20; touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600"
    image: busybox
    name: tmp
    livenessProbe:
     exec: # If this command can be run, the pod is healthy
       command:
       - echo
       - "I'm healthy"
     initialDelaySeconds: 5 # Only start probing after 5 seconds
     periodSeconds: 5 # Probe every 5 seconds
    readinessProbe:
     exec: # If this command can be run, the pod is ready to serve traffic
       command:
       - cat
       - /tmp/healthy
     initialDelaySeconds: 5 # Only start probing after 5 seconds
     periodSeconds: 10 # Probe every 10 seconds
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}
</code></pre>
<p>执行这个命令可以看到pods的状态:</p>
<pre><code class="language-bash">kubectl get pods --watch
</code></pre>
<p>看到<code>Ready</code> 列的值 从 <code>0/1</code>, 到 <code>1/1</code> 和回到 <code>0/.1</code>?</p>
<p>运行这个命令，以获得一个事件列表和探针如何失败的，正如我们期待的。</p>
<pre><code class="language-bash">kubectl describe pods my-pod
</code></pre>
<h3 id=""><a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/">容器资源</a></h3>
<p>你可以通过定义创建一个pod:</p>
<ul>
<li>它运行所需的最小资源量，被称为 <em>requests</em>.</li>
<li>pod可以使用的最大资源量，被称为 <em>limits</em>.</li>
</ul>
<p>Kubernetes在调度pod时，会考虑到你请求的资源。它不会在一个没有足够容量的节点上调度pod，不管当前资源的消耗情况如何。</p>
<p>例如， pod A和podB 在node N上运行，Kubernetes将通过下面的方法计算新的pod C 是否可以在node N上运行:</p>
<p><code>Total capacity of N - (resources requested by A + resources requested by B) &lt;= resources requested by C</code>( node N的总资源减去 A B需要的最小资源后，是否少于pod C需要的最小资源量)</p>
<p>即使pod A和B没有使用它们请求的最小资源， Kubernetes <strong>承诺</strong> A和B会有它们所请求的最小资源可使用。这是为什么当前的资源使用情况不在前面的公式表达。</p>
<p>如果一个pod超过它的资源限制(它可以使用的最大资源量时)，会发生两种情况:</p>
<ul>
<li>如果它超过CPU限制，它将被杀掉</li>
<li>如果它超过内存限制，它将会被重新启动</li>
</ul>
<p>现在让我们创建一个pod，设置好它的最小内存和cpu和最大内存和cpu使用:</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: resource-limited-pod
spec:
  restartPolicy: Never
  containers:
    - name: busybox
      image: busybox
      args:
        - /bin/sh
        - -c
        - "echo Hello Kubernetes; sleep 3600"
      resources:
        requests:
          memory: "300Mi"
          cpu: 0.2
        limits:
          memory: "1Gi"
          cpu: 0.5
</code></pre>
<p>作为一个练习，设置分配的内存和cpu高得离谱一些的值，去创建pod。你会看到pod从未准备好，将有输出</p>
<pre><code class="language-bash">kubectl describe pods resource-limited-pod
</code></pre>
<p>会告诉你为什么.</p>
<h2 id="kubernetespods"><a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node">怎样在Kubernetes中调度Pods</a></h2>
<p>这一部分不是CKAD考试教程的一部分。但我相信，你应该对我在这样阐述的概念有一些基本的了解，因为你在使用Kubernetes的过程中，它们很可能会出现。</p>
<p>Kubernetes允许你通过多种机制来指定你希望的pods，被安排到哪个nodes。</p>
<p>最简单的是使用 <strong>nodeSelector</strong> 字段，根据一个label选择一个node。其他功能的使用，允许更复杂的设置。</p>
<h3 id="taintsandtolerations"><a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/">Taints and tolerations</a></h3>
<p>你可以 <strong>taint(污损) 一个node 去防止在其中的pods被调度</strong>, 不用去修改pods本身。 当你 taint(污损)一个node,  Kubernetes 会调度 这个node 中的 pods，<strong>容忍</strong> taint(污损)。</p>
<p>你可以看到这个动作，如果你运行：</p>
<pre><code class="language-bash">kubectl describe node master.Kubernetes
</code></pre>
<p>在前一个命令的输出中，你会看到这样一行字:</p>
<pre><code class="language-bash">Taints: node-role.kubernetes.io/master:NoSchedule
</code></pre>
<p>这种污损会阻止pods在master node中被调度 (除非pods能容忍这种taint).</p>
<p>有三种类型的taint:</p>
<ul>
<li>NoSchedule: 如果node不能容忍taint，Kubernetes不会在node中调度pod。</li>
<li>PreferNoSchedule: pods不能容忍taint，不会调度在一个node中，除非pods不能调度到其他地方。</li>
<li>NoExecute: 如果已经在node中运行的pods不容忍taint，就把pods从node中移除出去。</li>
</ul>
<p><strong>Taints 并不保证 pods 会被调度到特定的nodes</strong>。 为了实现这一点,你需要探索 <em>Node affinity</em> (node亲和性) 概念。</p>
<h3 id="node">Node亲和性</h3>
<p>Node 亲和性 告诉 Kubernetes 去 <strong>调度pods到特定的nodes 上</strong>.</p>
<p>想象一下，你有一个服务S，需要特定硬件去运行。你像把专用硬件用于这个服务，并希望S的pod在上面运行。你怎样才能实现？</p>
<p>你可以taint这种设备上的nodes，以便只有来自服务S的pod可以在这些node上被调度。但是，Kubernetes可以在其他不不具备的所需硬件上的nodes部署这些pods。</p>
<p>我们可以看到: 组合taints和node 亲和性来确保只有来自服务S的pod 在我们的专门的nodes运行。</p>
<ul>
<li>Node 亲和性调度将S的pods安排到专门的nodes(而不是其他节点)。</li>
<li>Taints 确保没有其他的pod会被安排在专门的nodes，只有来自服务S的pod。</li>
</ul>
<h2 id="kubernetes">Kubernetes的存储</h2>
<p>默认情况下，当一个pod内的容器重启，容器内的数据会丢失，设计上是这样的(无状态)。</p>
<p>如果你想让数据比容器，pod，甚至是node都持久，你需要用<strong>卷</strong>。另外，如果一个pod是由多个容器组成的，那么这个pod 的卷可以被所有的容器使用。</p>
<h3 id=""><a href="https://kubernetes.io/docs/concepts/storage/volumes/">卷</a></h3>
<p>在你定义了卷后，在pod层，你需要在每个访问的容器装载它。</p>
<p>有很多类型的卷，以满足不同的使用情况(通常，取决于你想在 <em>pod</em> 被销毁时发生什么)。一些常见的卷类型：</p>
<ul>
<li>emptyDir: 创建一个初始是空的目录。这是在pod不同容器之间共享文件的简单方法。</li>
<li>hostPath: 将一个文件或者目录从node的文件系统挂载到pod，在pod被删除后，这些文件将保留在主机种。</li>
<li>在AWS、GCP或者 Azure云上的卷.</li>
</ul>
<p>关于这些和其他类型的卷的更多信息，请参考文档。</p>
<p>在容器种挂载一个卷，你的pod的描述符(我们把这个文件称为 <code>with-mounted-volume.yaml</code>),应该是这样的。</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: with-mounted-volume
spec:
  volumes: # Volumes are defined at the pod level
  - name: my-volume # We'll use this name to mount the volume inside the pod
    hostPath: # Here we define the type of volume we want
      path: /var/my-k8s-volume
  containers:
  - args:
    - /bin/sh
    - -c
    - "sleep 3600"
    image: busybox
    name: bb
    volumeMounts:
    - name: my-volume
      mountPath: /tmp/my-volume-path
  restartPolicy: Never
</code></pre>
<p>由于我们已经创建了一个<code>hostPath</code>的卷，它的内容寿命将超过pod。让我们试一下。</p>
<pre><code class="language-bash"># 创建一个 pod
kubectl apply -f with-mounted-volume.yaml
# 创建一个文件 并挂载到本地
kubectl exec -it with-mounted-volume -- sh -c "echo 'Inside the pod' &gt; /tmp/my-volume-path/newfile"
# 尝试去读取一个文件
kubectl exec -it with-mounted-volume -- cat /tmp/my-volume-path/newfile
# 删除 pod
kubectl delete pods with-mounted-volume
# 创建一个pod
kubectl apply -f with-mounted-volume.yaml
# 查看 `/tmp/my-volume-path` 下的内容，看它是否存在
kubectl exec -it with-mounted-volume -- cat /tmp/my-volume-path/newfile
</code></pre>
<h4 id="revisitingpods">Revisiting 多容器的pods</h4>
<p>现在我们熟悉了卷，让我们创建一个多容器的pod，它将用在一个挂载了卷的容器间共享日期。</p>
<ul>
<li>我们的 pod 描述定义 在文件<code>communicating-containers.yaml</code></li>
<li>我们将会一个带有2个 <code>busybox</code> 容器的pod。</li>
<li>其中一个容器将"Hello World" 追加到一个文件中。</li>
<li>另一个容器可以访问这个文件的内容(以及放在这个共享卷的任何东西)。我们将查看这个共享文件夹，看另一个容器是如何将<code>Hello World</code>追加到文件里的。<br>
这就是我们定义的pod：</li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: communicating-containers
spec:
  volumes:
  - name: vol
    emptyDir: {}
  containers:
  - args:
    - sh
    - -c
    - "while true;do echo 'Hello World' &gt;&gt; /etc/shared/log; sleep 1; done"
    image: busybox
    name: container-1
    volumeMounts:
    - name: vol
      mountPath: /etc/shared/ # The container can access the volume here
  - args:
    - sh
    - -c
    - "sleep 3600"
    image: busybox
    name: container-2
    volumeMounts:
    - name: vol
      mountPath: /etc/a-different-location # The volume can be mounted at different locations on each containers
  restartPolicy: Never
</code></pre>
<p>让我们创建pod:</p>
<pre><code class="language-bash">kubectl apply -f communicating-containers.yaml
</code></pre>
<p>一旦pod运行起来，我们就可以tail<code>container-2</code>文件，查看文件尾部内容:</p>
<pre><code class="language-bash">kubectl exec -it communicating-containers -c container-2 -- tail -f /etc/a-different-location/log
</code></pre>
<p>尽管<code>container-1</code>写东西到 <code>log</code>,但由于<code>log</code>在一个共享卷中，<code>container-2</code>可以看到这个文件。</p>
<h3 id="persistentvolumes"><a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/">Persistent Volumes(持久卷)</a></h3>
<p>Persistent Volumes（PVs 持久卷）和 Persistent Volume Claims（PVCs 持久卷申领）通过存储技术跟pod解耦。 PVs是通过集群管理员创建 或者动态根据 <a href="https://kubernetes.io/docs/concepts/storage/storage-classes/">存储类型</a>.</p>
<p>相对我们之前创建的卷，在pod层定义的，PV（持久卷）有自己的生命周期，独立于任何pod。</p>
<p>在创建PVs 后，<em>用户</em> 能创建 Persistent Volume Claims（持久卷申领）来获得它们需要的存储，<em>而不需要关心其存储的实际基础设施</em>。</p>
<h4 id="">例如</h4>
<p>首先，我们定义一个 Persistent Volume（持久卷）在pv.yaml 文件：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: myvolume
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
    - ReadWriteMany
  storageClassName: normal
  hostPath:
    path: /etc/foo
</code></pre>
<p>对于 Persistent Volume Claim（持久卷申领），在文件pvc.yaml描述：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mypvc
spec:
  storageClassName: normal
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 4Gi
</code></pre>
<p>现在, 让我们在集群中创建资源。</p>
<pre><code class="language-bash"># 首先, 创建Persistent Volume（持久卷）
kubectl apply -f pv.yaml
# 然后, 创建the Persistent Volume Claim（持久卷声明）
kubectl apply -f pvc.yaml
</code></pre>
<p>我们可以从前面的例子获取pod的定义，稍作调整，开始使用这个持久卷，代替我们定义的hostPath，这是我们唯一要改的地方。</p>
<pre><code class="language-yaml">volumes:
  - name: mypd # 当我们配置pod时，要参考这个卷
    persistentVolumeClaim: # 替换掉hostPath
      claimName: mypvc # 名称是我们刚才创建的pvc 
</code></pre>
<h2 id="kubernetes">Kubernetes中的网络和安全</h2>
<p>关于安全问题，这些概念是你应该熟悉的最低限度，以便通过CKAD考试。</p>
<h3 id="networkpolicies"><a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/">Network Policies(网络策略)</a></h3>
<p>默认情况下，所有的ingress流量(即流入你的应用程序的流量)和egress流量(即你的应用程序流出的流量)都是允许的。如何pod都可以连接到其他的pod，即使它们在不同的namespaces。</p>
<p>你可以定义网络策略，根据不同的标准控制ingress流量和egress流量</p>
<p>为了说明这一点,我们创建一个网络策略，只允许带有label <code>access: allowed</code>的pod访问的我们的数据库:</p>
<pre><code class="language-yaml">kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: access-db-policy # 起个名称
spec:
  podSelector:
    matchLabels:
      app: db # 数据库pods选择
  ingress: # 允许的 ingress 流量
  - from:
    - podSelector: 
        matchLabels: 
          access: allowed # 只有具有该label的pod才能向数据库发送流量
</code></pre>
<h3 id="securitycontext"><a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/">Security Context(安全背景)</a></h3>
<p>当配置一个Security Context(安全背景)，你可以启用一些安全功能，比如防止容器以root身份运行，选择什么用户运行pod等等，下面是一个例子：</p>
<pre><code class="language-bash">spec:
  securityContext:
    runAsUser: 1000 #这将为这个pod层的每个容器设置用户，但可以被覆盖。
  containers:
  - name: my-container
     image: alpine
     securityContext:
     	runAsUser: 1001 #    这覆盖在pod层上的用户设置 	
  - name: another-container
	....
</code></pre>
<h3 id="servicesaccounts"><a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/">Services Accounts(服务账号)</a></h3>
<p>你的应用程序可以连接到API 服务器进行数据交换。例如，检索有关pod信息在一个namespace。Service accounts(服务账号) 允许应用服务对API服务器进行认证，以便它们进行数据交互。</p>
<p>这是一个 <em>default service account</em> (默认的服务账号)，但是这不一个好的实践，因为不是每个应用程序都需要在API服务器上执行相同的操作。</p>
<p>创建service account(服务账号)的最简单的方法是通过命令行：</p>
<pre><code class="language-bash">kubectl create serviceaccount my-sa
</code></pre>
<p>一旦创建, 你一个分配 <code>serviceAccountName</code> 字段 (pod 描述符)分配给一个pod：</p>
<pre><code class="language-bash">spec:
  serviceAccountName: my-sa
  containers:
  - name: web-server
  ...
</code></pre>
<p>一旦应用程序被认证,下一步看它是否有适当的权限来完成它所要的操作,也就是说, 查看应用程序是否被授权。</p>
<h3 id="rolebasedaccesscontrol"><a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">Role Based Access Control(基于角色的访问控制)</a></h3>
<p>管理授权的一个常见办法是 <strong>Role Based Access Control (RBAC)</strong> (基于角色的访问控制)。 Service accounts(服务账号) 被链接到一个或者多个role(角色)。 每个role(角色)都有在某个资源上执行特定操作的权限。</p>
<p>RBAC 授权是通过两个独立的步骤来定义的：</p>
<ul>
<li>Role 创建, 使用 Role和ClusterRole 资源, 以指定在某些资源上可以执行的操作。</li>
<li>绑定 roles到账号上, 通过使用 RoleBinding 和 ClusterRoleBinding。</li>
</ul>
<p>正如你所想，包含前缀<code>Cluster</code>的资源在整个集群范围内可用，而其他的资源只在特定的namespace中定义。</p>
<p>由于这不属于CKAD考试的范围，我们将不讨论如何在你的集群中创建和配置RBAC的细节。</p>
<h2 id="kubernetes">Kubernetes中的可观察性和调试</h2>
<p>一旦你部署你的应用程序, 你如何知道你的集群发生了什么？</p>
<p>我们已经介绍了Probes作为一种机制决定是否重启pod，以及pod是否为网络流量做好了准备。在这里，我们将看到更多的机制来更好地了解我们的集群状态，并对任何问题故障排除。</p>
<h4 id="pod">获取Pod的基本信息</h4>
<p>访问pods的当前状态，有两个基本选项。这是第一选项：</p>
<pre><code class="language-bash">kubect get pods
</code></pre>
<p>这将显示你的pods <code>STATUS</code>, <code>AGE</code>,  <code>RESTARTS</code>的数量和多少pods内的容器处于 <code>READY</code>状态. 你可以用这个命令去显示pod的ip地址、labels等等。</p>
<p>第二个选项提供了更详细的信息:</p>
<pre><code class="language-bash">kubectl describe pods my-pod
</code></pre>
<p>在这个命令的输出的最底部，你会发现一个事件列表，在出差的情况下会给你提示：</p>
<ul>
<li>Liveness/readiness probes 失败</li>
<li>拉取镜像出现错误</li>
<li>没有足够的内存分配给pod</li>
</ul>
<p>等等。</p>
<h3 id="">如何获取容器的日志</h3>
<p>如果一个pod正在运行，你可以使用以下命令访问它的日志。</p>
<pre><code class="language-bash">kubectl logs pod-name [-c container-name] [-n namespace]
</code></pre>
<p>如果你的pod有不止一个容器，你只需传递容器名称。同样，从不同的namespace中检索日志要用namespace 标志符(-n)</p>
<p>你甚至tail日志 <code>-f</code> 标志符，看到日志尾部，获取最新的变化：</p>
<pre><code>kubectl logs -f pod-name [-c container-name] [-n namespace]
</code></pre>
<p>这对认证来讲是足够了。然而, 在真实的生产环境中，手动检查日志是很麻烦和低效的。 如果你在用GCP(谷歌云)，你会希望用其他技术来管理你的日志和服务，像StackDriver技术。如果你想了解更多的有关StackDriver和GCP的信息，请务必查看我的<a href="https://chinese.freecodecamp.org/news/google-cloud-platform-from-zero-to-hero/">GCP指南</a>.</p>
<h3 id="">处理故障的小技巧</h3>
<p>虽然在排除故障时没有灵丹妙药, 但从pod层开始通常是一个好注意，可以从可以找到问题的根本原因。</p>
<p>你的基本工具是我们上面提供的命令，除此之外，你可以随时在pod里打开一个终端。</p>
<pre><code class="language-bash"># 假设你的容器镜像使用 sh
kubectl exec -it my-pod -c container -n namespace -- sh
</code></pre>
<p><strong>不建议</strong> 用这种方式去解决pod的内部问题, 因为根据设计，pods是短暂的，在pods重启后，你的修改会丢失, 问题会复现。 当然, 这是一个好的方法去很好地了解问题。</p>
<p>获取你的pod或者node的指标，你可以运行以下命令。</p>
<pre><code class="language-bash">kubectl top pod pod-name
</code></pre>
<p>这些工具帮助你解决日常操作中最常见的问题。有太多的事件会出差，在这里不做解释了。如果你面对一些你无法解决的事情，我建议你访问这个，<a href="https://kubernetes.io/docs/tasks/debug-application-cluster/debug-application/">调试你的应用程序</a></p>
<h2 id="">技巧和窍门</h2>
<p>这是我发现的一些技巧，在我日常的Kubernetes工作中，特别是在通过CKAD考试时很有用。<strong>提高考试速度和效率部分</strong>。考虑到这点，让我们来看看可以做些什么来表现得更好。</p>
<h3 id="">设置别名</h3>
<p>你将反复输入相同的命令。一旦开始考试，请创建以下别名。</p>
<pre><code class="language-bash"># 重要的别名，我强烈建议设置它们
alias k='kubectl'
alias kg='k get'
alias kgpo='k get pods'
alias kdpo='k describe pod'
alias kd='k describe'

# 我还发现这个非常有用
alias kap='k apply -f'

# 我设置这个，只为工作需要
alias kgpoa='kgpo --all-namespaces'
alias kgpol='kgpo --show-labels'
alias kgpow='kgpo -o wide'
alias kgd='kg deploy'
alias kgs='kg svc'
alias kdd='kd deploy'
alias kds='kd svc'
</code></pre>
<h3 id="">更快搜索你的命令历史</h3>
<p>我见过很多工程师，其中很多是高级工程师(seniors)，笨拙按了20次以上的箭头键，在他们的命令历史中找东西。</p>
<p>不同的是，确保你自如地使用<code>Ctrl + r</code>，只有按下它并开始输入你要输入的命令的某些部分。然后，继续按<code>Ctrl + r</code>，直到你找到它。现在你可以按<code>Enter</code>运行它，或者简单开始修改后再运行。</p>
<p>这是一个节省大量时间的方法，并不是每个人都知道的。</p>
<h3 id="">熟悉文档</h3>
<p>你可以在考试期间查阅文档，然而，这不是学习新东西的时候。访问文档获得yaml的模板，检查特定的参数等等。</p>
<p>确保你对 <strong>文档的结构</strong> 很熟悉和知道去哪里找东西。 使用 <strong>搜索功能</strong> ，更快找到.</p>
<p>这份 <a href="https://kubernetes.io/docs/reference/kubectl/cheatsheet/">Kubectl cheat sheet(简明手册)</a> 是金子和 <strong>在考试期间可以为你你所用</strong>.</p>
<h3 id="">你不需要记住所有的东西</h3>
<p>除了文档, <code>kubectl</code> 有 <code>--help</code> 关于大多数命令的查看 (<code>-h</code> 是简写)。大多数时候，你会在那里找到问题的答案。</p>
<p>事实上，我建议你在查阅文档之前这样做，因为这要快得多。</p>
<p>想象一下，你想创建一个资源配额对象。你不记得你需要写的yaml或者创建它的命令。你知道 <code>kubectl command</code> 和 <code>--help</code> 参数。那么在查阅文档之前，你应该先试一试这个：</p>
<pre><code class="language-bash"># 我们可以创建得到创建资源配额对象所需要的东西
kubeclt create quota -h
# 例子
kubeclt create quota my-quota --hard="secrets=2"
</code></pre>
<p>你有大量的信息来源，因此你不需要记得住这么多的命令或者参数。</p>
<p>然而，考试期间时间非常有限，我认为值得熟记一些命令和任何使用它们。</p>
<h3 id="pod">快速生成一个基本的pod</h3>
<p><strong>这一点尤为重要</strong>。与其每一次需要一个pod时从文档中复制和粘贴，不如使用下面的命令获得一个描述符，你可以在以后修改它以满足你的需求。</p>
<pre><code class="language-bash"># 运行一个pod nginx，并将其参数写入一个pod.yaml文件中
kubectl run nginx --image=nginx --restart=Never --dry-run=client -o yaml &gt; pod.yaml
# 可以指定一些参数写入pod.yaml文件中
kubectl run nginx --image=nginx --restart=Never  --port=80 --labels=key=val --dry-run=client -o yaml &gt; pod.yaml
</code></pre>
<p>别的有用的参数：</p>
<pre><code class="language-bash">--rm # 在运行完成后立即删除pod
-it # 进入交换式终端，直接在pod中有运行命令
</code></pre>
<p>例如，你可以用它创建一个临时的pod，用它验证你的工作:</p>
<pre><code class="language-bash">kubectl run tmp --image=busybox --restart=Never --rm -it -- /bin/sh
</code></pre>
<p>现在你可以运行命令 <code>wget -O- svc:port</code> ，看看你的服务是否在运行，网络策略是否在工作等等。</p>
<h5 id="">注意：</h5>
<p><code>--dry-run=client -o yaml</code>不仅适用于pod,也适用于很多资源。如果我们回到上一节，我们创建一个资源配额，我们可以得到这样描述符:</p>
<pre><code class="language-bash">kubeclt create quota my-quota --hard="secrets=2" --dry-run=client -o yaml
</code></pre>
<h3 id="grep">使用 grep</h3>
<p>你不需要深入了解正则表达式。只要使用关键词。例如，从冗长的<code>kubectl describe pods my-pod</code> 快速检索信息：</p>
<pre><code class="language-bash"># -i 参数使搜索不区分大小写。有点慢，但对于非常短的文本来讲，这不会有什么影响。 
# -C 2 参数将显示匹配的最前2行和最后2行 (C 是 context的缩写)
kubectl describe pods my-pod | grep -i -C 2 labels
kubectl describe pods my-pod | grep -i -C 2 ip
...
</code></pre>
<h3 id="pods">观察pods 的状态变化</h3>
<p>与其每隔几秒手动运行<code>kubeclt get pods</code>来查看变化，不如通过<code>watch</code> 参数来查看你的pods是如何变化：</p>
<pre><code class="language-bash">kubectl get pods --watch
</code></pre>
<h3 id="pods">快速删除Pods</h3>
<p>犯错是学习过程的一部分。在考试中你也会犯错。由于时间紧迫，我们需要确保在删除资源时不要等待很长时间，通过这些参数可以快速删除pods：</p>
<pre><code class="language-bash">k delete pods my-pod --force --grace-period=0
</code></pre>
<h3 id="pod">用一个特定命令运行Pod</h3>
<p>我发现学习如何用命令行想pod、job、cronjobs等传递命令很有用，比如这个例子:</p>
<pre><code class="language-bash">kubectl run loop --image=busybox -o yaml --dry-run=client --restart=Never \
-- /bin/sh -c 'for i in {1..10}; do echo "Counting: $i"; done' \
&gt; pod.yaml
</code></pre>
<p>这会产生一个文件pod.yaml：</p>
<pre><code class="language-bash">apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: loop
  name: loop
spec:
  containers:
  - args:
    - /bin/sh
    - -c
    - 'for i in {1..10}; do echo "Counting: $i"; done'
    image: busybox
    name: loop
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Never
status: {}
</code></pre>
<p>你可以运行一个命令，也可以运行多个命令链，就像一个小型的bash脚本一样：</p>
<pre><code class="language-bash"># 运行一个特定的可执行文件
kubectl run busybox --image=busybox -it --restart=Never -- echo 'hello world'
# 在shell内运行(对运行多个命令很有用)
kubectl run busybox --image=busybox -it --restart=Never -- /bin/sh -c 'echo hello world'
</code></pre>
<p>这不是一个巨大的区别，但你不需要记住如何在pod的描述符中写，也不需要浪费时间去打开文档。 <strong>你只需要使用你已经知道的东西</strong></p>
<h3 id="rollout">roll out(展开)</h3>
<p>熟悉<code>rollout</code>命令，以获得关于你的部署状态的信息。</p>
<p>我总是使用 <code>--help</code> 参数，来记住如何做我想做的事。</p>
<pre><code class="language-bash">kubectl rollout -h
</code></pre>
<h3 id="bonus">Bonus(奖励)</h3>
<p>最后这些提示不是针对认证的，而是针对日常工作的：</p>
<ul>
<li>这个<a href="https://github.com/jonmosco/kube-ps1">kube-ps1 模块</a> 让你很容易知道你是在那个集群和namespace中操作的, 以防止犯错误，比如你不应该乱用 prod(生产环境)资源。</li>
<li>另外，我推荐你看一下<a href="https://helm.sh/">Helm</a>。Helm 是一个软件包管理器，可以用来轻松部署应用程序 (像<code>npm</code>)。 Helm还允许你编写模板，你可以根据不同的值 (name, resource requests and limits, and so on)重复使用，创建对象。</li>
</ul>
<h2 id="">练习时间</h2>
<p><strong>做事永远比读书学得多</strong>。所以我在这里留下一些我在准备考试已经解决的问题，供你练习。 查看<a href="https://github.com/dgkanatsios/CKAD-exercises">项目地址</a> 和 <a href="https://medium.com/bb-tutorials-and-thoughts/practice-enough-with-these-questions-for-the-ckad-exam-2f42d1228552">这篇文章</a>.</p>
<p>我建议在你先解决自己的所有问题，再看我上面提到资料。同时验证你做的东西: 检查日志，创建一个pod来连接你的服务等等。这也将有助建立你在考试中肌肉记忆。</p>
<p>尝试根据我的提示来练习这些练习，就像你真的在考试中一样，不分心，在浏览器里只打开一个标签--Kubernetes 的官方文档。</p>
<h2 id="">结论</h2>
<p>本指南包含了将提高你的技能到新的水平，通过CKAD考试以及成为一名真正的Kubernetes开发者所需要的一切。这一切在你手中，你只需要付出一些努力，祝你好运！</p>
<p>你可以访问到我的博客<a href="https://www.yourdevopsguy.com/">www.yourdevopsguy.com</a> 和 <a href="https://twitter.com/CodingLanguages">在推特上关注我</a> 获得更多高质量的技术内容。</p>
<p>如果你喜欢这篇文章，请分享它，因为你可以帮助别人通过考试或者找到工作。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Kubernetes 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ Kubernetes [https://kubernetes.io/]  是一个开放源代码的容器编排平台，可自动执行容器的部署、管理、扩容伸缩和网络管理。 它是由 Google [https://opensource.google/projects/kubernetes]  使用 Go 语言 [https://golang.org/]开发的，这项了不起的技术从 2014 年开始一直是开源的。 根据 Stack Overflow 开发者调研报告 - 2020 [https://insights.stackoverflow.com/survey/2020#overview]，Kubernetes 是 #3 最喜爱的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5] 以及 #3 最想要的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-lo ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-kubernetes-handbook/</link>
                <guid isPermaLink="false">602f8d73c354c605689ea576</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Wed, 19 May 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/107889834_1307234906308429_6629041044900498480_n.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><a href="https://kubernetes.io/">Kubernetes</a>  是一个开放源代码的容器编排平台，可自动执行容器的部署、管理、扩容伸缩和网络管理。</p>
<p>它是由 <a href="https://opensource.google/projects/kubernetes">Google</a> 使用  <a href="https://golang.org/">Go 语言</a>开发的，这项了不起的技术从 2014 年开始一直是开源的。</p>
<p>根据  <a href="https://insights.stackoverflow.com/survey/2020#overview">Stack Overflow 开发者调研报告 - 2020</a>，Kubernetes 是 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5">#3 最喜爱的平台</a>以及 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5">#3 最想要的平台</a>。</p>
<p>除了功能强大之外，Kubernetes 是公认的难上手。入门确实不容易，但是只要你符合入门条件并且有足够的耐心完成该指南，你将可以：</p>
<ul>
<li>对基础知识有深入的了解。</li>
<li>可以创建和管理 Kubernetes 集群。</li>
<li>部署任意应用程序到 Kubernetes 集群上。</li>
</ul>
<h2 id="">入门条件</h2>
<ul>
<li>熟悉 JavaScript</li>
<li>熟悉 Linux 终端</li>
<li>熟悉 Docker（建议阅读：<a href="https://chinese.freecodecamp.org/news/the-docker-handbook/">Docker 入门教程 - 2021 最新版</a>）</li>
</ul>
<h2 id="">项目代码</h2>
<p>实例中的代码可以在<a href="https://github.com/fhsinchy/kubernetes-handbook-projects">这个仓库</a>中找到（你的 ⭐ 是我动力的源泉）。<code>k8s</code> 分支包含完整的代码。</p>
<h2 id="">目录</h2>
<ul>
<li>容器编排和 Kubernetes 简介</li>
<li>安装 Kubernetes</li>
<li>Kubernetes 初体验
<ul>
<li>Kubernetes 的架构</li>
<li>Control Plane 组件</li>
<li>Node 组件</li>
<li>Kubernetes 对象</li>
<li>Pods</li>
<li>Services</li>
<li>全景图</li>
<li>清除 Kubernetes 相关资源</li>
</ul>
</li>
<li>声明式部署方法
<ul>
<li>编写你的第一套配置</li>
<li>Kubernetes 控制面板</li>
</ul>
</li>
<li>使用多容器应用程序
<ul>
<li>部署计划</li>
<li>复用 Controllers, Replica Sets 以及 Deployments</li>
<li>创建你的第一个部署</li>
<li>调试 Kubernetes 资源</li>
<li>从 Pods 获取容器日志</li>
<li>环境变量</li>
<li>创建数据库部署</li>
<li>Persistent Volumes 和 Persistent Volume Claims</li>
<li>Persistent Volumes 的动态预配置</li>
<li>通过 Pods 连接 Volumes</li>
<li>组装起来</li>
</ul>
</li>
<li>使用 Ingress Controllers
<ul>
<li>设置 NGINX Ingress Controller</li>
<li>Kubernetes 中的 Secret 和配置</li>
<li>在 Kubernetes 中执行更新发布</li>
<li>组合 Configurations</li>
</ul>
</li>
<li>答疑</li>
<li>结论</li>
</ul>
<h2 id="kubernetes">容器编排和 Kubernetes 简介</h2>
<p>摘自 <a href="https://www.redhat.com/en/topics/containers/what-is-container-orchestration">Red Hat</a>  —</p>
<blockquote>
<p>"容器编排是指自动化容器的部署、管理、扩展和联网。</p>
<p>容器编排可以在使用容器的任何环境中使用。它可以帮助你在不同环境中部署相同的应用，而无需重新设计。"</p>
</blockquote>
<p>让我来给你看一个例子。假设你开发了一个很棒的应用，这个 应用可以根据时间向人们推荐他们应该吃什么。</p>
<p>假设你已经使用 Docker 容器化了应用并将其部署在了 AWS 上 。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/food-suggestion-application-single-instance.svg" alt="food-suggestion-application-single-instance" width="600" height="400" loading="lazy"></p>
<p>如果应用因为某种原因宕机，用户马上就不能访问该服务了。</p>
<p>要解决此问题，可以为同一应用程序制作多个副本，使其服务高可用。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/food-suggestion-application-multi-instance.svg" alt="food-suggestion-application-multi-instance" width="600" height="400" loading="lazy"></p>
<p>即使其中一台实例发生故障，其它两台实例也可以为用用户提供服务。</p>
<p>假设你的应用程序在熬夜党中流行了起来，在你晚上睡觉的时候涌入大量的请求。</p>
<p>如果所有的实例都因为过载而无法响应该怎么办？谁来进行自动伸缩？即使你扩容 了 50 个副本，谁来做健康检查？如何设置网络使使流量打到合适的端点上？负载均衡也是一个大问题，你说呢？</p>
<p>Kubernetes 可以很容易的搞定这些问题。Kubernetes 是一个由多个组件组成的容器编排平台，它可以一刻不眠的使是你的服务保持在理想状态。</p>
<p>假设你要连续运行 50 个应用程序副本，如果请求量激增，服务器也能自动扩容。</p>
<p>你只需把你的需求告诉 Kubernetes，它将为你完成其余的繁重工作。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/kube-representation.svg" alt="kube-representation" width="600" height="400" loading="lazy"></p>
<p>Kubernetes 会实现并维护状态。如果有旧副本挂掉了，它将创建新的副本，管理网络和存储，推出或回滚更新，甚至在必要时升级服务。</p>
<h2 id="kubernetes">安装 Kubernetes</h2>
<p>实际上，在本地计算机上运行 Kubernetes 与在云平台上运行 Kubernetes 有很大不同。你需要下面两个程序，来启动和运行 Kubernetes。</p>
<ul>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-minikube/">minikube</a>  - 它可以在本地计算机的虚拟机（VM）上运行单节点 Kubernetes 集群。</li>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a>  -  Kubernetes 命令行工具，可以在 Kubernetes 集群上执行命令。</li>
</ul>
<p>除了这两个程序之外，你还需要一个管理程序和一个容器平台。显然 <a href="https://www.docker.com/">Docker</a> 就是所需的容器平台。推荐的管理程序如下：</p>
<ul>
<li><a href="https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v">Hyper-V</a> Windows 系统</li>
<li><a href="https://github.com/moby/hyperkit">HyperKit</a> Mac 系统</li>
<li><a href="https://www.docker.com/">Docker</a> Linux 系统</li>
</ul>
<p>Hyper-V 作为可选功能内置于  Windows 10（Pro、Enterprise 和 Education）中，可以从控制面板中打开。</p>
<p>HyperKit 是 Mac 平台 Docker Desktop 的核心组件。</p>
<p>在 Linux 上，你可以直接通过 Docker 绕过整个管理程序层。它比任何管理程序都高效，是 Linux 上运行 Kubernetes 的推荐方法。</p>
<p>你可以继续安装上述任何管理程序。或者你想保持简单，只需要获取 <a href="https://www.virtualbox.org/">VirtualBox</a>。</p>
<p>文章的剩余部分，我们假设你正在使用 VirtualBox。别担心，即使你正在使用其他管理程序，区别也不会太大。</p>
<blockquote>
<p>整篇文章，我在装有 <a href="https://www.freecodecamp.org/news/p/c4f90e6f-97af-41ce-b775-b6e52a5a5152/ubuntu.com/">Ubuntu</a> 的机器上使用带有 Docker 驱动程序的  <code>minikube</code> 完成。</p>
</blockquote>
<p>安装了管理程序和容器化平台之后，就该安装 <code>minikube</code>  和 <code>kubectl</code> 程序了。</p>
<p>如果你使用 Mac 或 Windows，安装完 Docker Desktop 后 <code>kubectl</code> 就已经安装了。可在<a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">此处</a>找到 Linux 的安装说明。</p>
<p>另外 <code>minikube</code> 也是必需要安装的，可以在 Mac 上使用 <a href="https://brew.sh/">Homebrew</a>，Windows 上使用 <a href="https://chocolatey.org/">Chocolatey</a> 来安装 <code>minikube</code>。可以在<a href="https://kubernetes.io/docs/tasks/tools/install-minikube/">此处</a>找到 Linux 的安装说明。</p>
<p>安装完成后，可以通过执行以下命令来测试是否安装成功：</p>
<pre><code class="language-bash">minikube version

# minikube version: v1.12.1
# commit: 5664228288552de9f3a446ea4f51c6f29bbdd0e0
kubectl version

# Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.6", GitCommit:"dff82dc0de47299ab66c83c626e08b245ab19037", GitTreeState:"clean", BuildDate:"2020-07-16T00:04:31Z", GoVersion:"go1.14.4", Compiler:"gc", Platform:"darwin/amd64"}
# Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.3", GitCommit:"2e7996e3e2712684bc73f0dec0200d64eec7fe40", GitTreeState:"clean", BuildDate:"2020-05-20T12:43:34Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
</code></pre>
<p>如果你已经为你的操作系统下载了正确的版本并且设置了正确路径，那么那你已经准备就绪啦。</p>
<p>正如我已经提到的，<code>minikube</code> 在本地计算机上的虚拟机（VM）中运行一个单节点 Kubernetes 集群。 我将在下一部分中更详细地解释集群和节点。</p>
<p>现在，可以理解为 <code>minikube</code> 使用你选择的管理程序创建常规 VM，并将其视为 Kubernetes 集群。</p>
<blockquote>
<p>如果你在本节中遇到任何问题，请查看本文结尾处的<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/#troubleshooting">答疑</a>部分。</p>
</blockquote>
<p>在启动 <code>minikube</code> 之前，必需正确设置管理程序才能使用。执行如下命令将 VirtualBox 设置为默认驱动程序：</p>
<pre><code class="language-bash">minikube config set driver virtualbox

# ❗ These changes will take effect upon a minikube delete and then a minikube start
</code></pre>
<p>可以根据需要将 <code>virtualbox</code> 替换为 <code>hyperv</code>、 <code>hyperkit</code> 或者 <code>docker</code>。这个命令只需运行一次。</p>
<p>执行下面的命令启动 <code>minikube</code>：</p>
<pre><code class="language-bash">minikube start

# ? minikube v1.12.1 on Ubuntu 20.04
# ✨ Using the virtualbox driver based on existing profile
# ? Starting control plane node minikube in cluster minikube
# ? Updating the running virtualbox "minikube" VM ...
# ? Preparing Kubernetes v1.18.3 on Docker 19.03.12 ...
# ? Verifying Kubernetes components...
# ? Enabled addons: default-storageclass, storage-provisioner
# ? Done! kubectl is now configured to use "minikube"
</code></pre>
<p>可以通过 <code>minikube stop</code> 命令来停止  <code>minikube</code> 。</p>
<h2 id="kubernetes">Kubernetes 初体验</h2>
<p>现在已经在本地上安装了 Kubernetes，是时候动手啦。在此示例中，会向本地集群部署一个非常简单的应用，并熟悉一下基础知识。</p>
<blockquote>
<p>本节中会涉及到诸如 <strong>pod</strong>,  <strong>service</strong>,  <strong>负载均衡</strong>等术语, 如果你没有搞懂他们，别急，我会在<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/#the-full-picture">全景图</a>小节中详细介绍它们。</p>
</blockquote>
<p>如果你在上一小节已经启动了 <code>minikube</code>，那么就可以开始啦，否则你需要先启动它哦。启动 <code>minikube</code> 后，在终端执行下面的命令：</p>
<pre><code class="language-bash">kubectl run hello-kube --image=fhsinchy/hello-kube --port=80

# pod/hello-kube created
</code></pre>
<p>你会立即看到 <code>pod/hello-kube created</code> 消息。 <a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#run">run</a> 命令用来在 pod 中运行指定的容器映像。</p>
<p>Pods 就像是封装容器的盒子，执行以下命令确保 pod 已经成功创建并运行：</p>
<pre><code class="language-bash">kubectl get pod

# NAME         READY   STATUS    RESTARTS   AGE
# hello-kube   1/1     Running   0          3m3s
</code></pre>
<p>你应该在  <code>STATUS</code>  列看到 <code>Running</code> 信息。如果看到类似 <code>ContainerCreating</code> 的信息，等待一两分钟，然后再次检查。</p>
<p>默认情况下，从集群外部无法访问 Pod。若要使其可访问，必需使用 service 使其暴露。运行 pod 后，执行下面的命令暴露 pod：</p>
<pre><code class="language-bash">kubectl expose pod hello-kube --type=LoadBalancer --port=80

# service/hello-kube exposed
</code></pre>
<p>执行以下命令确保负载均衡服务已经成功创建：</p>
<pre><code class="language-bash">kubectl get service

# NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
# hello-kube   LoadBalancer   10.109.60.75   &lt;pending&gt;     80:30848/TCP   119s
# kubernetes   ClusterIP      10.96.0.1      &lt;none&gt;        443/TCP        7h47m
</code></pre>
<p>请确保在列表中可以看到 <code>hello-kube</code>  服务。现在已经有了一个公开的 pod 正在运行，执行下面的命令访问它。</p>
<pre><code class="language-bash">minikube service hello-kube

# |-----------|------------|-------------|-----------------------------|
# | NAMESPACE |    NAME    | TARGET PORT |             URL             |
# |-----------|------------|-------------|-----------------------------|
# | default   | hello-kube |          80 | http://192.168.99.101:30848 |
# |-----------|------------|-------------|-----------------------------|
# ? Opening service default/hello-kube in default browser...
</code></pre>
<p>默认的 web 浏览器应该会自动打开，显示类似如下的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-85.png" alt="image-85" width="600" height="400" loading="lazy"></p>
<p>这是一个非常简单的 JavaScript 应用程序，使用了 <a href="https://github.com/vitejs/vite">vite</a>  和一点 CSS。如果要了解刚才执行的命令，需要熟悉一下 Kubernetes 的架构。</p>
<h3 id="kubernetes">Kubernetes 的架构</h3>
<p>在 Kubernetes 的世界中，<strong>node</strong> 既可以是一台物理设备也可以是一台指定角色的虚拟机。这样的一组使用一个共享网络彼此通信的设备或者服务器的集合就叫做 <strong>集群（cluster）</strong>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/nodes-cluster-1.svg" alt="nodes-cluster-1" width="600" height="400" loading="lazy"></p>
<p>在本地设置中， <code>minikube</code> 是单节点的 Kubernetes 集群。因此 <code>minikube</code> 没有像上图的多个服务器，而是只有一台服务器同时充当主服务器和 node。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/minikube-1.svg" alt="minikube-1" width="600" height="400" loading="lazy"></p>
<p>Kubernetes 集群中的每台服务器都会获得一个角色。有两种不同的角色：</p>
<ul>
<li><strong>control-plane</strong>  — 做出大部分必要的决定，并充当整个集群的大脑。它可以是单个服务器或者大型项目中的一组服务器。</li>
<li><strong>node</strong>  — 负责运行工作负载，这些服务器通常由 control-plane 进行细微管理，并按照提供的说明执行各种任务。</li>
</ul>
<p>集群中每个服务器都将具有一组特定的组件。这些组件的数量和类型根据服务器在集群中承担的角色而有所不同。这意味着节点不必包含 control plane 中的所有的组件。</p>
<p>在接下来的小节里，将更详细的了解组成 Kubernetes  集群的各个组件。</p>
<h3 id="controlplane">Control Plane 组件</h3>
<p>Kubernetes 集群中的 control plane 由如下<strong>五</strong>个组件组成：</p>
<ol>
<li><strong>kube-api-server:</strong>  这是 Kubernetes control plane 的入口，负责验证和处理使用客户端库（如  <code>kubectl</code>  程序）传递的请求。</li>
<li><strong>etcd:</strong>  这是一个分布式键值存储，是整个集群的唯一键值来源。它保存了配置数据和集群的状态信息。<a href="https://etcd.io/">etcd</a> 是一个开源项目，由来自 Red Hat 的人开发。 该项目的源代码托管在 <a href="https://github.com/etcd-io/etcd">etcd-io/etcd</a> GitHub 仓库中。</li>
<li><strong>kube-controller-manager:</strong>  Kubernetes 中的 controller 负责控制集群的状态。当请求 Kubernetes 集群内容时，controller 会做出响应。<code>kube-controller-manager</code> 是通过一个进程管理所有 controller 进程的程序。</li>
<li><strong>kube-scheduler:</strong>  调度就是根据节点的可用资源和任务需要的资源分配任务。<code>kube-scheduler</code> 组件执行 Kubernetes 的任务调度以确保集群中所有的服务都不过载。</li>
<li><strong>cloud-controller-manager:</strong>  在真实的云环境中，此组件允许你通过 (<a href="https://cloud.google.com/kubernetes-engine">GKE</a>/<a href="https://aws.amazon.com/eks/">EKS</a>) API 连接集群。这样，与该云平台交互的组件就和与集群交互的组件隔离开了。在 <code>minikube</code> 这一类的组件中，该组件并不存在。</li>
</ol>
<h3 id="node">Node 组件</h3>
<p>与 control plane 相比，node 的组件数量非常少，如下：</p>
<ol>
<li><strong>kubelet:</strong>  该服务充当 control plane 和集群中每个节点之间的网关。从 control plane 到节点的每条指令都通过此服务。它还与  <code>etcd</code>  存储区进行交互以保持状态信息的更新。</li>
<li><strong>kube-proxy:</strong>  这个小服务运行在每个节点上，并为其维护网络规则。到达集群内部服务的任何网络请求都将通过此服务。</li>
<li><strong>Container Runtime:</strong>  Kubernetes 是一个容器编排工具，因此它最终在容器中运行应用程序。这意味着每个节点都需要一个容器环境，比如 <a href="https://www.docker.com/">Docker</a>、<a href="https://coreos.com/rkt/">rkt</a> 或者 <a href="https://cri-o.io/">cri-o</a>。</li>
</ol>
<h3 id="kubernetes">Kubernetes 对象</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/">文档</a>  —</p>
<blockquote>
<p>"在 Kubernetes 系统中，<em>Kubernetes 对象</em> 是持久化的实体。 Kubernetes 使用这些实体去表示整个集群的状态。特别地，它们描述了如下信息：</p>
<ul>
<li>哪些容器化应用在运行（以及在哪些节点上）</li>
<li>可以被应用使用的资源</li>
<li>关于应用运行时表现的策略，比如重启策略、升级策略，以及容错策略</li>
</ul>
</blockquote>
<p>当创建 Kubernetes 对象时，实际上是在告诉 Kubernetes 系统这个对象应该存在，任何时候 Kubernetes 系统都应该确保该对象的’</p>
<p>运行。</p>
<h3 id="pods">Pods</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/workloads/pods/">文档</a>  —</p>
<blockquote>
<p>"<em>Pod</em> 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元"。</p>
</blockquote>
<p>pod 通常封装一个或多个紧密相关的容器，共享一个生命周期和消耗性资源。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/pods-1.svg" alt="pods-1" width="600" height="400" loading="lazy"></p>
<p>尽管一个 pod 可以容纳多个容器，但是你不应该随意的把容器放到 pod 中。pod 中的容器必须紧密相关，以至于可以将它们视为单个应用程序。</p>
<p>例如，后端的 API 可能依赖数据库，但这并不意味着需要把他们都放在同一个容器中。在整篇文章中，不会有任何 pod 放置多个容器。</p>
<p>通常，你不应该直接管理 pod。相反，你应该使用可以提供更好的可管理的高级对象。将在后面的部分中介绍这些更高级别的对象。</p>
<h3 id="services">Services</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/service/">文档</a>  —</p>
<blockquote>
<p>"Kubernetes  的 service 是将运行在一组 <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/">Pods</a> 上的应用程序公开为网络服务的抽象方法"。</p>
</blockquote>
<p>Kubernetes  pods 是非永久性资源。他们被创造出来，即使过一段时间被销毁了，也不会被回收。</p>
<p>相反，新的 pod 取代了旧的 pod。一些更高级别的对象甚至能动态创建和销毁 pod。</p>
<p>在创建每个 pod 时，会为其分配一个新的 IP 地址。但是对于可以创建、销毁和组合多个 pod 的高级对象而言，在此刻运行的 pod 集合可能与稍后运行的 pod 集合并不相同。</p>
<p>这就导致了一个问题：如果集群中的某些 pod 依赖于集群中的另一组 pod，怎样定位并跟踪彼此的 IP 地址呢？</p>
<p>根据 Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/service/">文档</a>  —</p>
<blockquote>
<p>"Kubernetes Service 定义了这样一种抽象：逻辑上的一组 Pod，一种可以访问它们的策略 —— 通常称为微服务"。</p>
</blockquote>
<p>本质上讲，service 将执行相同功能的多个 pod 组合在一起，并将它们显示为单个实体。</p>
<p>这样一来，如何跟踪多个 Pod 的问题就消失了，因为单个 service 现在充当了所有 pod 的沟通器。</p>
<p>在 <code>hello-kube</code> 示例中，创建了一个 <code>LoadBalancer</code> 类型的服务，该服务可以将来自集群外部的请求连接到集群内部运行的 pod 上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/load-balancer-3.svg" alt="load-balancer-3" width="600" height="400" loading="lazy"></p>
<p>无论何时，在需要授予另一个应用程序或者集群外部某个对象一个或多个 pod 的访问权限时，就应该创建一个 service。</p>
<p>比如，如果你有一组运行 web 服务的 pod，需要从 internet 进行访问，那么就必需用 service 提供必要的抽象。</p>
<h3 id="">全景图</h3>
<p>现在你已经对 Kubernetes 的各个组件有了适当的了解，下图描述了他们是如何协作的：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/components-of-kubernetes.png" alt="components-of-kubernetes" width="600" height="400" loading="lazy"></p>
<p><a href="https://kubernetes.io/docs/concepts/overview/components/">https://kubernetes.io/docs/concepts/overview/components/</a></p>
<p>在解释各个细节之前，先看一下 Kubernetes <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/">文档</a> --</p>
<blockquote>
<p>"操作 Kubernetes 对象 —— 无论是创建、修改，或者删除 —— 需要使用  <a href="https://kubernetes.io/docs/concepts/overview/kubernetes-api/">Kubernetes API</a>。 比如，当使用 <code>kubectl</code> 命令行接口时，CLI 会执行必要的 Kubernetes API 调用。"</p>
</blockquote>
<p>运行的第一个命令是 <code>run</code> 命令，如下：</p>
<pre><code class="language-bash">kubectl run hello-kube --image=fhsinchy/hello-kube --port=80
</code></pre>
<p><code>run</code> 命令负责运行指定的镜像创建新的  pod。运行此命令后，Kubernetes 集群会执行下面的事件：</p>
<ul>
<li><code>kube-api-server</code>  组件接收请求，对其进行校验并进行处理。</li>
<li><code>kube-api-server</code>  接着与节点上的 <code>kubelet</code> 进行通信，并提供创建 pod 所需的指令。</li>
<li><code>kubelet</code> 组件开始启动运行 pod，并且在  <code>etcd</code>  存储中保持状态的更新。</li>
</ul>
<p><code>run</code> 命令的通用语法如下：</p>
<pre><code class="language-bash">kubectl run &lt;pod name&gt; --image=&lt;image name&gt; --port=&lt;port to expose&gt;
</code></pre>
<p>可以在 pod 内运行任何有效的容器映像。<a href="https://hub.docker.com/r/fhsinchy/hello-kube">fhsinchy/hello-kube</a> Docker 镜像包含了一个非常简单的 JavaScript 应用程序，该应用程序在容器内部的 80 端口上运行。 <code>--port=80</code> 选项允许容器从内部暴露 80 端口。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/pods-2.svg" alt="pods-2" width="600" height="400" loading="lazy"></p>
<p>新创建的 Pod 运行在 <code>minikube</code> 集群内部，并且无法从外部访问。要公开容器并使其可用，运行的第二个命令如下：</p>
<pre><code class="language-bash">kubectl expose pod hello-kube --type=LoadBalancer --port=80
</code></pre>
<p><code>expose</code> 命令负责创建类型为 <code>LoadBalancer</code> Kubernetes service，该服务允许用户访问 Pod 中运行的应用程序。</p>
<p>和  <code>run</code>  命令一样， <code>expose</code>  命令的执行需要在集群内部运行相似的步骤。在这里， <code>kube-api-server</code> 向 <code>kubelet</code> 组件提供了创建 service （而不是 pod）所需的指令。</p>
<p><code>expose</code>  命令的通用语法如下：</p>
<pre><code class="language-bash">kubectl expose &lt;resource kind to expose&gt; &lt;resource name&gt; --type=&lt;type of service to create&gt; --port=&lt;port to expose&gt;
</code></pre>
<p>对象类型可以是任意合法的 Kubernetes  对象类型。名称必需和要暴露的对象名称匹配。</p>
<p><code>--type</code> 表示所需的 service 类型。在内部或外部网络中一共有四种不同的 service 类型。</p>
<p>最后， <code>--port</code> 是要从容器中暴露的端口号。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/services-half.svg" alt="services-half" width="600" height="400" loading="lazy"></p>
<p>创建完 service 后，最后一件事就是访问在 pod 的应用程序。为此，需要执行如下命令：</p>
<pre><code class="language-bash">minikube service hello-kube
</code></pre>
<p>和之前的命令不同 ，最后一个命令没有用 <code>kube-api-server</code>。它使用  <code>minikube</code>  和本地集群通讯。  <code>minikube</code> 的 <code>service</code> 命令会返回给定服务的完整 URL。</p>
<p>当使用  <code>--port=80</code> 选项创建  <code>hello-kube</code> 容器时，Kubernetes 会在容器内暴露 80 端口，但是无法在集群外部访问它。</p>
<p>接着，使用  <code>--port=80</code> 选项创建 <code>LoadBalancer</code> 服务，它将 80 端口从该容器映射到本地系统中的任意端口，从而可以从集群外部访问它。</p>
<p>在我的系统上， <code>service</code> 命令返回 pod 的 URL <code>192.168.99.101:30848</code>。该 URL 中的 IP 实际上是 <code>minikube</code>  虚拟机的真实 IP。可以通过下面的命令来验证：</p>
<pre><code class="language-bash">minikube ip

# 192.168.99.101
</code></pre>
<p>可以通过如下命令验证 <code>30848</code> 端口是否指向 pod 内部的 80 端口：</p>
<pre><code class="language-bash">kubectl get service hello-kube

# NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
# hello-kube   LoadBalancer   10.109.60.75   &lt;pending&gt;     80:30848/TCP   119s
</code></pre>
<p>在  <code>PORT(S)</code> 列上，可以看到 80 端口实际上映射到本地系统的 30484 端口。因此，无需运行 <code>service</code> 命令，只需找到 IP 和端口号，然后就可以在浏览器内访问  <code>hello-kube</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-86.png" alt="image-86" width="600" height="400" loading="lazy"></p>
<p>目前集群的状态如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/services-1.svg" alt="services-1" width="600" height="400" loading="lazy"></p>
<p>如果你了解 Docker，那么你可能觉得使用 service 来公开 pod 有点太麻烦了。</p>
<p>但是当你处理涉及多个 pod 的实例时，你就会了解 Kubernetes 这么做的便利了。</p>
<h2 id="kubernetes">清除 Kubernetes 相关资源</h2>
<p>现在已经了解如何创建 pod 和 service 之类的 Kubernetes  资源，现在来学习如何清除它们。也就是删除它们。</p>
<p>执行  <code>kubectl</code> 的  <code>delete</code>  命令来删除资源，用法如下：</p>
<pre><code class="language-bash">kubectl delete &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>使用下面的命令删除名为 <code>hello-kube</code>  的 pod：</p>
<pre><code class="language-bash">kubectl delete pod hello-kube

# pod "hello-kube" deleted
</code></pre>
<p>使用下面的命令删除名为 <code>hello-kube</code> 的 service：</p>
<pre><code class="language-bash">kubectl delete service hello-kube

# service "hello-kube" deleted
</code></pre>
<p>或者（现在不可用）使用 <code>delete</code>  命令的 <code>--all</code>  选项来一次性删除所有此类对象。该选项的通用语法如下：</p>
<pre><code class="language-bash">kubectl delete &lt;object type&gt; --all
</code></pre>
<p>如果要删除所有的 pod 和 service，依次执行  <code>kubectl delete pod --all</code>  和<code>kubectl delete service --all</code>。</p>
<h2 id="">声明式部署方法</h2>
<p>坦白讲，你在上一节看到的 <code>hello-kube</code> 例子并不是部署 Kubernetes 的最佳方式。</p>
<p>在之前章节采用的是<strong>交互式途径（imperative approach）</strong>，这意味着你必须手动逐个执行每个命令。采用交互式方法无法很好的工程化。</p>
<p>使用 Kubernetes 进行部署的理想方式是<strong>声明式途径（declarative approach）</strong>，作为开发人员，只需让 Kubernetes 知道服务需要达到的状态，其余的 Kubernetes 会搞定。</p>
<p>在本节中，将会使用声明式部署相同的  <code>hello-kube</code> 应用程序。</p>
<p>如果你尚未克隆上面链接的代码仓库，请立即进行操作。</p>
<p>克隆完毕后，进入 <code>hello-kube</code> 目录，该目录包含 <code>hello-kube</code> 应用程序的代码以及用户构建镜像的  <code>Dockerfile</code> 。</p>
<pre><code class="language-bash">├── Dockerfile
├── index.html
├── package.json
├── public
└── src

2 directories, 3 files
</code></pre>
<p>JavaScript 代码位于  <code>src</code>  文件夹下，无需关注，你应该看一下 <code>Dockerfile</code>，了解一下计划部署。<code>Dockerfile</code>  文件内容如下：</p>
<pre><code class="language-dockerfile">FROM node as builder

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build

EXPOSE 80

FROM nginx
COPY --from=builder /usr/app/dist /usr/share/nginx/html
</code></pre>
<p>如你所见，这是一个<a href="https://www.freecodecamp.org/news/the-docker-handbook/#multi-staged-builds">多阶段构建（multi-staged build）</a>。</p>
<ul>
<li>第一阶段使用 <code>node</code> 作为基本镜像，然后将 JavaScript 应用程序编译为生产状态。</li>
<li>第二阶段复制第一阶段生成的文件，并将其粘贴到默认的 NGINX 文档根目录中。这里假设第二阶段的基本镜像是 <code>nginx</code>，会把第一阶段构建的文件运行在 80 端口（nginx 默认端口）。</li>
</ul>
<p>要在 Kubernetes 上部署应用，需要找到一种方式把镜像运行在容器里，并使其能在外部世界通过 80 端口访问。</p>
<h3 id="">编写你的第一套配置</h3>
<p>在声明式方式中，无需在终端中发送单个命令，只需在 YAML 文件中写下必要的配置，然后将其提供给 Kubernetes 即可。</p>
<p>在  <code>hello-kube</code> 工程目录下，创建另一个名为 <code>k8s</code> 的目录，<code>k8s</code> 是 k(ubernete = 8 个字符)s 的缩写。</p>
<p>文件夹名不必一定是 k8s，可以任意命名。</p>
<p>甚至没有必要将其放在项目目录中，这些配置文件可以放在计算机中的任何位置，因为这些配置与项目源代码无关。</p>
<p>在  <code>k8s</code>   目录中，创建一个名为  <code>hello-kube-pod.yaml</code> 的新文件。先看一下所有的代码，后面会逐行解释。文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: hello-kube-pod
  labels:
    component: web
spec:
  containers:
    - name: hello-kube
      image: fhsinchy/hello-kube
      ports:
        - containerPort: 80

</code></pre>
<p>每个有效的 Kubernetes 配置文件都有四个必填字段。如下：</p>
<ul>
<li><code>apiVersion</code>: 创建对象使用的 Kubernetes API 版本。该值可能会根据你创建的对象的类型而变化。对于 <code>Pod</code>  的创建，所需的版本是 <code>v1</code>。</li>
<li><code>kind</code>: 创建的对象的类型。Kubernetes 中有许多种对象。本文介绍了很多对象，目前只需知道要创建的对象是  <code>Pod</code>  即可。</li>
<li><code>metadata</code>: 对象的唯一标识数据。在此字段下，可以有 <code>name</code>、 <code>labels</code>、 <code>annotation</code>  等信息。当使用 <code>kubectl</code> 命令时 <code>metadata.name</code> 会显示在终端上。<code>metadata.labels</code> 字段下的键值对不必一定是  <code>components: web</code> ，可以指定任意 label 比如 <code>app: hello-kube</code>。在接下来创建 <code>LoadBalancer</code> service 时会使用该值作为选择器。</li>
<li><code>spec</code>: 包含对象希望达成的状态。 <code>spec.containers</code> 子字段包含将要运行在 <code>Pod</code> 内的容器信息。 <code>spec.containers.name</code> 是节点内的容器运行时分配给新创建容器的值。<code>spec.containers.image</code> 是用来创建容器的镜像。<code>spec.containers.ports</code> 字段是各种端口的配置。<code>containerPort: 80</code> 表示容器对外暴露的端口是 80。</li>
</ul>
<p>现在使用 <code>apply</code> 命令将这个文件提供给 Kubernetes，用法如下：</p>
<pre><code class="language-bash">kubectl apply -f &lt;configuration file&gt;
</code></pre>
<p>如下命令 apply 了名为 <code>hello-kube-pod.yaml</code> 的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f hello-kube-pod.yaml

# pod/hello-kube-pod created
</code></pre>
<p>执行以下命令以确保 <code>Pod</code> 已经成功启动并且正在运行：</p>
<pre><code class="language-bash">kubectl get pod

# NAME         READY   STATUS    RESTARTS   AGE
# hello-kube   1/1     Running   0          3m3s
</code></pre>
<p>在 <code>STATUS</code> 列中能看到 <code>Running</code>。如果显示的是类似 <code>ContainerCreating</code> 的内容，请等待一两分钟后再试。</p>
<p>当 <code>Pod</code> 启动并运行后，就可以开始写 <code>LoadBalancer</code>  service 的配置文件了。</p>
<p>在 <code>k8s</code> 路径下创建一个名为 <code>hello-kube-load-balancer-service.yaml</code> 的文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: hello-kube-load-balancer-service
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80
  selector:
    component: web
</code></pre>
<p>和之前的配置文件一样，<code>apiVersion</code>、<code>kind</code> 和 <code>metadata</code> 字段作用相同。如你所见，<code>metadata</code> 内没有 <code>labels</code> 字段，因为 service 使用 <code>labels</code> 选择其它对象，而其它对象无需选择 service。</p>
<blockquote>
<p>记住，service 为其他对象设置了访问策略，而其它对象无需为 service 设置访问策略。</p>
</blockquote>
<p>在  <code>spec</code> 字段内可以看到一组新的值。和 <code>Pod</code> 不同，service 有四种不同的类型，他们是 <code>ClusterIP</code>、 <code>NodePort</code>、 <code>LoadBalancer</code> 和<code>ExternalName</code>。</p>
<p>在此例中，使用的是 <code>LoadBalancer</code> 类型，这是把 service 暴露给集群外的标准方法。该服务会给你提供一个 IP 地址，可以使用该 IP 地址连接到集群内运行的应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/load-balancer-4.svg" alt="load-balancer-4" width="600" height="400" loading="lazy"></p>
<p><code>LoadBalancer</code> 类型需要两个端口值才能正常工作，在 <code>ports</code> 字段下，<code>port</code> 值用于访问 pod 本身，其值可以是任意值。</p>
<p><code>targetPort</code> 值是容器内部的值，必需要与容器内部的 port 一致。</p>
<p>正如之前所说，<code>hello-kube</code> 应用运行在容器内部的 80 端口上，已经在 <code>Pod</code> 配置文件中暴露了该端口，因此 <code>targetPort</code>  的值应该为 80。</p>
<p><code>selector</code> 字段用于标识将要连接该 service 的对象。<code>component: web</code> 键值对必须与 <code>Pod</code> 配置文件中的 <code>labels</code> 字段相匹配。如果你之前在配置文件里使用了其它的键值对如 <code>app: hello-kube</code> ，那么就改成你的键值。</p>
<p>在次使用 <code>apply</code> 命令将这个文件提供给 Kubernetes。文件名为<code>hello-kube-load-balancer-service.yaml</code>， 命令如下：</p>
<pre><code class="language-bash">kubectl apply -f hello-kube-load-balancer-service.yaml

# service/hello-kube-load-balancer-service created
</code></pre>
<p>执行以下命令以确保负载均衡器已经成功创建：</p>
<pre><code class="language-bash">kubectl get service

# NAME                               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
# hello-kube-load-balancer-service   LoadBalancer   10.107.231.120   &lt;pending&gt;     80:30848/TCP   7s
# kubernetes                         ClusterIP      10.96.0.1        &lt;none&gt;        443/TCP        21h
</code></pre>
<p>确保在列表中能看到  <code>hello-kube-load-balancer-service</code>。现在你已经运行了一个公有的 pod，执行下面的命令直接进行访问：</p>
<pre><code class="language-bash">minikube service hello-kube-load-balancer-service

# |-----------|----------------------------------|-------------|-----------------------------|
# | NAMESPACE |           NAME                   | TARGET PORT |             URL             |
# |-----------|----------------------------------|-------------|-----------------------------|
# | default   | hello-kube-load-balancer-service |          80 | http://192.168.99.101:30848 |
# |-----------|----------------------------------|-------------|-----------------------------|
# ?  Opening service default/hello-kube-load-balancer in default browser...
</code></pre>
<p>默认的浏览器应该会自动打开，如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-87.png" alt="image-87" width="600" height="400" loading="lazy"></p>
<p>也可以将两个文件一起提供，如下所示，将文件名替换成目录名即可：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/hello-kube-load-balancer-service created
# pod/hello-kube-pod created
</code></pre>
<p>请确保终端在 <code>k8s</code>  目录的父目录中。</p>
<p>如果位于 <code>k8s</code>  目录中，可以使用点 (<code>.</code>) 引用当前目录。应用批量配置时，最好提前清除之前创建的资源，以防发生冲突。</p>
<p>声明式方法是使用 Kubernetes 的理想方法，当然有一些例外情况，本文结尾会介绍。</p>
<h3 id="kubernetes">Kubernetes 控制面板</h3>
<p>在上一节中，使用  <code>delete</code>  命令清除了 Kubernetes  对象。</p>
<p>在本节中，会引入控制面板。Kubernetes 控制面板是一个图形用户界面，用于管理工作负载、service 等。</p>
<p>在终端中执行以下命令启动 Kubernetes 控制面板：</p>
<pre><code class="language-bash">minikube dashboard

# ? Verifying dashboard health ...
# ? Launching proxy ...
# ? Verifying proxy health ...
# ? Opening http://127.0.0.1:52393/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
</code></pre>
<p>控制面板应该会在浏览器中自动打开：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-88.png" alt="image-88" width="600" height="400" loading="lazy"></p>
<p>控制面板界面很直观，你可以快速上手。虽然创建、管理和删除对象都能从控制面板进行，但是本文其余部分还是会使用 cli 来操作。</p>
<p>在 <em>Pods</em> 列表中，可以使用右边的三个点菜单的 <em>Delete</em> 来删除 Pod。<code>LoadBalancer</code> service 也可以如此操作，实际上 <em>Services</em> 列表就位于 <em>Pods</em> 列表后。</p>
<p>可以按 <code>Ctrl + C</code> 组合键或者关闭终端窗口来停止控制面板服务。</p>
<h2 id="">使用多容器应用程序</h2>
<p>目前为止，已经使用了单个容器运行了应用程序。</p>
<p>在本节中，将会使用两个容器组成应用程序。你还会学习到 <code>Deployment</code>、<code>ClusterIP</code>、 <code>PersistentVolume</code>、<code>PersistentVolumeClaim</code> 以及一些调试技巧。</p>
<p>将使用的服务是一个具备完整 CRUD 功能的简单的基于 express 的日记 API。该应用使用 <a href="https://www.postgresql.org/">PostgreSQL</a> 数据库。因此不仅需要部署应用程序，还需要建立应用程序和数据库服务的内部网络连接。</p>
<p>该应用程序的代码位于项目仓库的 <code>notes-api</code> 目录中。</p>
<pre><code class="language-bash">.
├── api
├── docker-compose.yaml
└── postgres

</code></pre>
<p>应用程序代码位于  <code>api</code>  目录中，<code>postgres</code> 目录包含了创建 <code>postgres</code> 镜像的 <code>Dockerfile</code>。 <code>docker-compose.yaml</code> 文件包含使用 <code>docker-compose</code> 运行应用程序的配置文件。</p>
<p>就像上一个项目一样，可以查看每个 service 单独的 <code>Dockerfile</code>，以了解应用程序是如何在容器中运行的。</p>
<p>也可以只检查  <code>docker-compose.yaml</code> 并用它来规划 Kubernetes  部署。</p>
<pre><code class="language-yaml">version: "3.8"

services: 
    db:
        build:
            context: ./postgres
            dockerfile: Dockerfile.dev
        environment:
            POSTGRES_PASSWORD: 63eaQB9wtLqmNBpg
            POSTGRES_DB: notesdb
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        ports: 
            - 3000:3000
        volumes: 
            - /usr/app/node_modules
            - ./api:/usr/app
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg

</code></pre>
<p>查看 <code>api</code> 服务定义，应该可以看到服务运行在内部容器的 3000 端口。它还需要一堆环境变量才能正常运行。</p>
<p>可以忽略 volumes，它在开发环境是必需的，并且构建配置是只针对于 Docker。因此可以保留 Kubernetes 配置文件几乎不变，如下：</p>
<ul>
<li>Port 映射 – 必需从容器公开相同的端口。</li>
<li>环境变量 – 这些变量在所有的环境中都是相同的（尽管值将发生变化）。</li>
</ul>
<p><code>db</code> 服务更简单，它只是一堆环境变量。甚至可以用官方的 <code>postgres</code> 镜像代替自定义的镜像。</p>
<p>使用自定义镜像的好处是数据库实例可以附带预先创建的 notes 表。</p>
<p>该表对于应用程序是必需的，如果查看 <code>postgres/docker-entrypoint-initdb.d</code> 目录，会看到一个名为 <code>notes.sql</code> 的文件，该文件用于在初始化期间创建数据库。</p>
<h3 id="">部署计划</h3>
<p>和之前的项目部署不同，该项目将变的更加复杂。</p>
<p>在这个项目中，将会创建三个 notes API 实例。这三个实例使用  <code>LoadBalancer</code> service 暴露在集群外面。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/notes-api-1.svg" alt="notes-api-1" width="600" height="400" loading="lazy"></p>
<p>除了这个实例，还会有一个 PostgreSQL 系统实例。notes API 应用程序的三个实例都使用 <code>ClusterIP</code>  service 和数据库实例通讯。</p>
<p><code>ClusterIP</code> service 是另外一种 Kubernetes  service，它在集群内部使应用可见。也就是说即使没有外部流量，应用程序也可以使用  <code>ClusterIP</code>  service。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/cluster-ip-2.svg" alt="cluster-ip-2" width="600" height="400" loading="lazy"></p>
<p>在此项目中，必需仅通过 Notes API 访问数据库，因此在集群中公开数据库服务是一个理想的选择。</p>
<p>上一节中已经提起到，不应该直接创建 pod。因此，在此项目中使用  <code>Deployment</code>  而不是 <code>Pod</code>。</p>
<h3 id="controllersreplicasetsdeployments">复制 Controllers、Replica Sets 以及 Deployments</h3>
<p>根据 Kubernetes <a href="https://kubernetes.io/docs/concepts/architecture/controller/">文档</a>  -</p>
<blockquote>
<p>"在 Kubernetes 中，控制器通过监控<a href="https://kubernetes.io/zh/docs/reference/glossary/?all=true#term-cluster">集群</a> 的公共状态，并致力于将当前状态转变为期望的状态。控制回路（Control Loop）是一个非终止回路，用于调节系统状态。"</p>
</blockquote>
<p><code>ReplicationController</code> 可以很轻松的创建多个副本。当创建所需的副本后，控制器将保持当前状态。</p>
<p>如果过了一段时间你决定减少副本的数量，那么 <code>ReplicationController</code> 会立刻清除多余的 pods。</p>
<p>否则，如果副本的数量少于预期数量（也许一些 pod 已经崩溃），<code>ReplicationController</code> 会创建新的副本以达到所需的状态。</p>
<p>尽管 <code>ReplicationController</code> 很强大，但目前已不是创建副本的推荐方式。它已经被较新的 API  <code>ReplicaSet</code> 取代。</p>
<p><code>ReplicaSet</code> 除了提供了更多选择外，<code>ReplicationController</code>  和 <code>ReplicaSet</code>  完成的几乎是同一件事。</p>
<p>拥有更多的选择器是件好事，但是更棒的是，能在发布和回滚更新方面具有更大的灵活性。这就该轮到另一个 Kubernetes  API <code>Deployment</code> 出场了。</p>
<p><code>Deployment</code> 就像是 <code>ReplicaSet</code>  API 的一个扩展。<code>Deployment</code> 不但允许你立即创建新副本，还允许使用一个或两个 <code>kubectl</code> 命令发布或回滚更新。</p>
<table>
<thead>
<tr>
<th>REPLICATIONCONTROLLER</th>
<th>REPLICASET</th>
<th>DEPLOYMENT</th>
</tr>
</thead>
<tbody>
<tr>
<td>可以轻松创建多个 pod</td>
<td>可以轻松创建多个 pod</td>
<td>可以轻松创建多个 pod</td>
</tr>
<tr>
<td>Kubernetes 中的原始复制方法</td>
<td>更灵活的选择器</td>
<td>扩展自 ReplicaSets，可以轻松更新和回滚</td>
</tr>
</tbody>
</table>
<p>在这个项目里，会使用  <code>Deployment</code> 来维护应用程序实例。</p>
<h3 id="">创建你的第一个部署</h3>
<p>首先，为 Notes API 部署编写配置文件，在 <code>notes-api</code> 目录中创建一个 <code>k8s</code> 目录。</p>
<p>在该目录中，创建一个名为  <code>api-deployment.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000

</code></pre>
<p>在此文件中，<code>apiVersion</code>、 <code>kind</code>、  <code>metadata</code> 和  <code>spec</code> 字段作用与之前的项目相同。与上一个文件相比，不一样的地方如下：</p>
<ul>
<li>创建 pod 时，<code>apiVersion</code>的值是 <code>v1</code>。但是创建部署时，需要的版本是  <code>apps/v1</code>。Kubernetes API 的版本有时会有些混乱，你可能会有些一头雾水。可以阅读一下官网<a href="https://kubernetes.io/docs/home/">文档</a>对 <code>Deployment</code> YAML 文件的介绍。</li>
<li><code>spec.replicas</code> 定义了同时运行的副本数量。将此值设置为 3 意味着希望 Kubernetes 同时运行三个应用实例。</li>
<li>在 <code>spec.selector</code> 中，可以让 <code>Deployment</code> 知道要控制那些 pods。之前已经提到，<code>Deployment</code> 是 <code>ReplicaSet</code> 的扩展，可以控制 Kubernetes  对象。将 <code>selector.matchLabels</code> 设置为  <code>component: api</code> 意味着 <code>Deployment</code> 会控制 label 为 <code>component: api</code> 的 pods。这行代码的意思就是让 Kubernetes  知道你希望  <code>Deployment</code> 来控制 label 为 <code>component: api</code> 的 pods。</li>
<li><code>spec.template</code> 是用于配置 pod 的模板，它与之前的配置文件几乎相同。</li>
</ul>
<p>现在，要查看此配置效果，和之前一样 apply 该文件：</p>
<pre><code class="language-bash">kubectl apply -f api-deployment.yaml

# deployment.apps/api-deployment created
</code></pre>
<p>执行下面的命令确保  <code>Deployment</code> 已经成功创建：</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           2m7s
</code></pre>
<p>如果查看  <code>READY</code> 列，会看到 <code>0/3</code>。这意味着容器尚未创建，等待几分钟，然后在试一次。</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           28m
</code></pre>
<p>坦白讲，我已经等了将近半个小时，pod 还未准备就绪。API 本身只有几百 kb。这种规模的部署不应该花这么长的时间，这意味着有问题，我们来解决它。</p>
<h3 id="kubernetes">调试 Kubernetes 资源</h3>
<p>开始之前，首先回到起点。<code>get</code> 命令是一个很基础的命令。</p>
<p><code>get</code> 命令可以打印一张包含一个或多个 Kubernetes 资源重要信息的表。用法如下：</p>
<pre><code class="language-bash">kubectl get &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>在终端执行如下代码，在 <code>api-deployment</code> 上运行 <code>get</code> 命令：</p>
<pre><code class="language-bash">kubectl get deployment api-deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           15m
</code></pre>
<p>可以省略 <code>api-deployment</code> 以获取所有可用的部署列表。也可以在配置文件上使用  <code>get</code> 命令。</p>
<p>可以使用如下命令获取有关 <code>api-deployment.yaml</code> 文件中描述的部署信息：</p>
<pre><code class="language-bash">kubectl get -f api-deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           18m
</code></pre>
<p>默认情况下，<code>get</code> 命令显示的信息非常少，可以使用 <code>-o</code>  选项获取更多信息。</p>
<p><code>-o</code> 选项设置 <code>get</code> 命令的输出格式，可以使用 <code>wide</code> 输出格式查看更详细信息。</p>
<pre><code class="language-bash">kubectl get -f api-deployment.yaml

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES               SELECTOR
# api-deployment   0/3     3            0           19m   api          fhsinchy/notes-api   component=api

</code></pre>
<p>现在列表包含了更多的信息，可以在官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#get">文档</a>了解有关  <code>get</code>  命令的选项。</p>
<p>老实说，仅仅是运行 <code>Deploymen</code> 的  <code>get</code>  命令没啥意思。还需要获取更底层资源的信息。</p>
<p>看一下 pod 列表，看看里面都有啥东西：</p>
<pre><code class="language-bash">kubectl get pod

# NAME                             READY   STATUS             RESTARTS   AGE
# api-deployment-d59f9c884-88j45   0/1     CrashLoopBackOff   10         30m
# api-deployment-d59f9c884-96hfr   0/1     CrashLoopBackOff   10         30m
# api-deployment-d59f9c884-pzdxg   0/1     CrashLoopBackOff   10         30m
</code></pre>
<p>现在发现了一些有用的东西。所有的 pods 都有一个值为 <code>CrashLoopBackOff</code> 的 <code>STATUS</code>。之前只接触过  <code>ContainerCreating</code> 和 <code>Running</code> 状态。你可能还会在  <code>CrashLoopBackOff</code> 处看到 <code>Error</code>。</p>
<p>看一下 <code>RESTARTS</code> 列，会发现 pod 已经重启 10 多次了，着意味着因为某些原因， pod 启动失败了。</p>
<p>现在，要查看一个 pod 的更详细信息，可以使用另一个名为 <code>describe</code> 的命令。它和 <code>get</code> 命令很像，用法如下：</p>
<pre><code class="language-bash">kubectl get &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>执行下面的命令查看 <code>api-deployment-d59f9c884-88j4</code> pod 的详细信息：</p>
<pre><code class="language-bash">kubectl describe pod api-deployment-d59f9c884-88j45

# Name:         api-deployment-d59f9c884-88j45
# Namespace:    default
# Priority:     0
# Node:         minikube/172.28.80.217
# Start Time:   Sun, 09 Aug 2020 16:01:28 +0600
# Labels:       component=api
#               pod-template-hash=d59f9c884
# Annotations:  &lt;none&gt;
# Status:       Running
# IP:           172.17.0.4
# IPs:
#   IP:           172.17.0.4
# Controlled By:  ReplicaSet/api-deployment-d59f9c884
# Containers:
#  api:
#     Container ID:   docker://d2bc15bda9bf4e6d08f7ca8ff5d3c8593655f5f398cf8bdd18b71da8807930c1
#     Image:          fhsinchy/notes-api
#     Image ID:       docker-pullable://fhsinchy/notes-api@sha256:4c715c7ce3ad3693c002fad5e7e7b70d5c20794a15dbfa27945376af3f3bb78c
#     Port:           3000/TCP
#     Host Port:      0/TCP
#     State:          Waiting
#       Reason:       CrashLoopBackOff
#     Last State:     Terminated
#       Reason:       Error
#       Exit Code:    1
#       Started:      Sun, 09 Aug 2020 16:13:12 +0600
#       Finished:     Sun, 09 Aug 2020 16:13:12 +0600
#     Ready:          False
#     Restart Count:  10
#     Environment:    &lt;none&gt;
#     Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-gqfr4 (ro)
# Conditions:
#   Type              Status
#   Initialized       True
#   Ready             False
#   ContainersReady   False
#   PodScheduled      True
# Volumes:
#   default-token-gqfr4:
#     Type:        Secret (a volume populated by a Secret)
#     SecretName:  default-token-gqfr4
#     Optional:    false
# QoS Class:       BestEffort
# Node-Selectors:  &lt;none&gt;
# Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
#                  node.kubernetes.io/unreachable:NoExecute for 300s
# Events:
#   Type     Reason     Age                         From               Message
#   ----     ------     ----                        ----               -------
#   Normal   Scheduled  &lt;unknown&gt;                   default-scheduler  Successfully assigned default/api-deployment-d59f9c884-88j45 to minikube
#   Normal   Pulled     2m40s (x4 over 3m47s)       kubelet, minikube  Successfully pulled image "fhsinchy/notes-api"
#   Normal   Created    2m40s (x4 over 3m47s)       kubelet, minikube  Created container api
#   Normal   Started    2m40s (x4 over 3m47s)       kubelet, minikube  Started container api
#   Normal   Pulling    107s (x5 over 3m56s)        kubelet, minikube  Pulling image "fhsinchy/notes-api"
#   Warning  BackOff    &lt;invalid&gt; (x44 over 3m32s)  kubelet, minikube  Back-off restarting failed container
</code></pre>
<p>整个输出中最有用的部分是 <code>Events</code> 部分，如下：</p>
<pre><code>Events:
  Type     Reason     Age                         From               Message
  ----     ------     ----                        ----               -------
  Normal   Scheduled  &lt;unknown&gt;                   default-scheduler  Successfully assigned default/api-deployment-d59f9c884-88j45 to minikube
  Normal   Pulled     2m40s (x4 over 3m47s)       kubelet, minikube  Successfully pulled image "fhsinchy/notes-api"
  Normal   Created    2m40s (x4 over 3m47s)       kubelet, minikube  Created container api
  Normal   Started    2m40s (x4 over 3m47s)       kubelet, minikube  Started container api
  Normal   Pulling    107s (x5 over 3m56s)        kubelet, minikube  Pulling image "fhsinchy/notes-api"
  Warning  BackOff    &lt;invalid&gt; (x44 over 3m32s)  kubelet, minikube  Back-off restarting failed container
</code></pre>
<p>从这些事件中，可以看到容器镜像已经成功 pulled，容器也已经创建，但是显然从 <code>Back-off restarting failed container</code> 中可以看出该容器无法启动。</p>
<p>describe 命令和  <code>get</code> 命令类似，并且具有相同的选项。</p>
<p>可以省略 <code>api-deployment-d59f9c884-88j45</code> 名字以获取所有 pods 的信息。或者也可以使用  <code>-f</code> 选项将配置文件传入命令。访问官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#describe">文档</a>了解更多信息。</p>
<p>既然已经知道了是容器出了问题，那么就让我们到容器层面看看到底出了什么问题吧。</p>
<h3 id="pods">从 Pods 获取容器日志</h3>
<p>还有另一个名为 <code>logs</code> 的 <code>kubectl</code> 命令，可以从容器内部获取容器的日志，用法按如下：</p>
<pre><code class="language-bash">kubectl logs &lt;pod&gt;
</code></pre>
<p>使用如下命令 <code>api-deployment-d59f9c884-88j45</code> 查看 pod 内的日志：</p>
<pre><code class="language-bash">kubectl logs api-deployment-d59f9c884-88j45
# &gt; api@1.0.0 start /usr/app
# &gt; cross-env NODE_ENV=production node bin/www
# /usr/app/node_modules/knex/lib/client.js:55
#     throw new Error(knex: Required configuration option 'client' is missing.);
    ^
# Error: knex: Required configuration option 'client' is missing.
#     at new Client (/usr/app/node_modules/knex/lib/client.js:55:11)
#     at Knex (/usr/app/node_modules/knex/lib/knex.js:53:28)
#     at Object.&lt;anonymous&gt; (/usr/app/services/knex.js:5:18)
#     at Module._compile (internal/modules/cjs/loader.js:1138:30)
#     at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
#     at Module.load (internal/modules/cjs/loader.js:986:32)
#     at Function.Module._load (internal/modules/cjs/loader.js:879:14)
#     at Module.require (internal/modules/cjs/loader.js:1026:19)
#     at require (internal/modules/cjs/helpers.js:72:18)
#     at Object.&lt;anonymous&gt; (/usr/app/services/index.js:1:14)
# npm ERR! code ELIFECYCLE
# npm ERR! errno 1
# npm ERR! api@1.0.0 start: cross-env NODE_ENV=production node bin/www
# npm ERR! Exit status 1
# npm ERR!
# npm ERR! Failed at the api@1.0.0 start script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

</code></pre>
<p>这正是出问题的地方，看起来是 <a href="http://knexjs.org/">knex.js</a> 库缺少一个必要的值，导致程序启动失败。可以从官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#logs">文档</a>了解更多关于 <code>logs</code> 命令的信息。</p>
<p>出现这个错误主要是因为在部署定义中缺少了一些必需的环境变量。</p>
<p>如果在看一下 <code>docker-compose.yaml</code> 文件中的 api service 定义，应该会看到类似如下的内容：</p>
<pre><code class="language-yaml">    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        ports: 
            - 3000:3000
        volumes: 
            - /usr/app/node_modules
            - ./api:/usr/app
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg
</code></pre>
<p>应用程序与数据库进行连接需要这些环境变量。所以，把这些数据添加到部署配置中就可以解决该问题。</p>
<h3 id="">环境变量</h3>
<p>给 Kubernetes  配置文件添加环境变量非常简单。打开  <code>api-deployment.yaml</code> 文件并按如下更新内容：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          
          # these are the environment variables
          env:
            - name: DB_CONNECTION
              value: pg
</code></pre>
<p><code>containers.env</code> 字段包含所有的环境变量，如果细心你会发现我还没有给 <code>docker-compose.yaml</code> 文件添加所有的环境变量，我只添加了一个。</p>
<p><code>DB_CONNECTION</code> 表示应用正在使用 PostgreSQL 数据库，添加这个变量就可以解决上述问题。</p>
<p>现在通过执行下面的命令在此 apply 配置文件：</p>
<pre><code class="language-bash">kubectl apply -f api-deployment.yaml

# deployment.apps/api-deployment configured
</code></pre>
<p>这次显示资源已经被 <code>configured</code>。这就是 Kubernetes 厉害的地方，apply 配置文件可以立即生效。</p>
<p>现在在次使用  <code>get</code>  命令确保一切运行正常。</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   3/3     3            3           68m

kubectl get pod

# NAME                              READY   STATUS    RESTARTS   AGE
# api-deployment-66cdd98546-l9x8q   1/1     Running   0          7m26s
# api-deployment-66cdd98546-mbfw9   1/1     Running   0          7m31s
# api-deployment-66cdd98546-pntxv   1/1     Running   0          7m21s

</code></pre>
<p>三个 pod 都在运行，并且  <code>Deployment</code> 也运行良好。</p>
<h3 id="">创建数据库部署</h3>
<p>既然 API 已经启动并且运行，是时候为数据库实例编写配置了。</p>
<p>在 k8s 目录中另创建一个名为 <code>postgres-deployment.yaml</code> 的文件，文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              value: 63eaQB9wtLqmNBpg
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>配置本身与上一次非常相似，我就不详细解释了，根据目前学到的知识你应该可以了解。</p>
<p>PostgreSQL 默认运行在 5432 端口上，运行 <code>postgres</code> 容器需要 <code>POSTGRES_PASSWORD</code> 变量。该密码也会用于 API 连接数据库。</p>
<p><code>POSTGRES_DB</code> 变量是可选的，但是在该项目里是必需的，否则会初始化失败。</p>
<p>可以在 Docker Hub 页面上了解 <a href="https://hub.docker.com/_/postgres">postgres</a> Docker 镜像的更多信息。在此项目中，化繁为简，副本数量设置为 1。</p>
<p>执行以下命令 apply 这个文件：</p>
<pre><code class="language-bash">kubectl apply -f postgres-deployment.yaml

# deployment.apps/postgres-deployment created
</code></pre>
<p>通过 <code>get</code> 命令确保 pod 已经正常部署和运行：</p>
<pre><code class="language-bash">kubectl get deployment

# NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
# postgres-deployment   1/1     1            1           13m

kubectl get pod

# NAME                                   READY   STATUS    RESTARTS   AGE
# postgres-deployment-76fcc75998-mwnb7   1/1     Running   0          13m

</code></pre>
<p>尽管 pod 已经成功部署和运行，但是数据库部署还是有很大的问题。</p>
<p>如果你以前使用过数据库系统，应该知道数据库是把数据存储在文件系统中。目前，数据库部署如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/postgres-1.svg" alt="postgres-1" width="600" height="400" loading="lazy"></p>
<p><code>postgres</code> 容器由 pod 封装，数据保留在容器内部的文件系统中。</p>
<p>现在，如果由于某种原因，容器崩溃或者封装容器的 pod 发生故障，则保存在文件系统的所有数据都将丢失。</p>
<p>崩溃后，Kubernetes 会创建一个新的 pod 来维持状态，但是两个 pod 之间没有任何数据转移机制。</p>
<p>为了解决这个问题，可以将数据存储在集群 pod 外部的单独空间中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/volume.svg" alt="volume" width="600" height="400" loading="lazy"></p>
<p>和管理计算实例相比，管理此类存储面临的是另一些问题。Kubernetes 中的 <code>PersistentVolume</code> 子系统为用户和管理员提供了一个 API，该 API 从存储的使用方式中抽象出如何提供存储的细节。</p>
<h3 id="persistentvolumespersistentvolumeclaims">Persistent Volumes 和 Persistent Volume Claims</h3>
<p>摘自 Kubernetes  <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/">文档</a>  —</p>
<blockquote>
<p>"持久卷（PersistentVolume，PV）是集群中的一块存储，可以由管理员事先供应，或者使用存储类（Storage Class）来动态供应。 持久卷是集群资源，就像节点也是集群资源一样。"</p>
</blockquote>
<p>实际上，<code>PersistentVolume</code> 是一种从存储空间获得切片并将其保留给特定 pod 的方法。Volumes 始终由 pod 占用，而不是像 deployment 这样的高级对象占用。</p>
<p>如果要在具有多个 pod 的 deployment 中使用 volume，必须要执行一些附加步骤。</p>
<p>在 <code>k8s</code> 目录中创建一个名为 <code>database-persistent-volume.yaml</code> 的新文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: database-persistent-volume
spec:
  storageClassName: manual
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"

</code></pre>
<p><code>apiVersion</code>、 <code>kind</code> 和  <code>metadata</code> 与其它配置文件里的用法一致，在 <code>spec</code> 字段里，有一些新字段：</p>
<ul>
<li><code>spec.storageClassName</code> 指示 volume 的名称。假设云提供商有三种可用的存储。<em>slow</em>、  <em>fast</em> 和  <em>very fast</em> 。从云存储上获取的存储方式取决于支付的金额。如果需要 very fast 存储，则需要支付更多的费用。这些不同类型的存储就是 classes。在本例里我使用 <code>manual</code>，在本地集群里可以使用任何你喜欢的选项。</li>
<li><code>spec.capacity.storage</code>  代表 volume 的存储大小。在此项目中设置了 5GB 的存储空间。</li>
<li><code>spec.accessModes</code>  设置卷的访问模式。一共有三种存储模式，<code>ReadWriteOnce</code>  代表该 volume  可以通过单个 node 以读写方式安装。<code>ReadWriteMany</code> 则代表该 volume  可以被多个 node 以读写的方式安装。<code>ReadOnlyMany</code> 意味着该 volume 可以被多个 node 以只读的方式安装。</li>
<li><code>spec.hostPath</code> 是特定于开发者的。它将本地单 node 集群的目录映射为 persistent volume。<code>/mnt/data</code> 意味着保存在持久卷（persistent volume）中的数据位于集群的 <code>/mnt/data</code>  文件夹内。</li>
</ul>
<p>执行下面的命令 apply 配置文件：</p>
<pre><code class="language-bash">kubectl apply -f database-persistent-volume.yaml

# persistentvolume/database-persistent-volume created
</code></pre>
<p>现在使用  <code>get</code>  命令确定 volume 创建成功：</p>
<pre><code class="language-bash">kubectl get persistentvolume

# NAME                         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
# database-persistent-volume   5Gi        RWO            Retain           Available           manual                  58s
</code></pre>
<p>现在已经创建了 persistent volume，需要让 postgres  pod 访问它，可以通过<code>PersistentVolumeClaim</code>  (PVC) 。</p>
<p>persistent volume 声明是 pod 对存储的要求。假设在集群中有很多 volumes。该声明将定义必需满足 pod 的需求的 volume  的特征。</p>
<p>一个类似的的例子是从商店购买 SSD。销售员向你展示了如下模型：</p>
<table>
<thead>
<tr>
<th>MODEL 1</th>
<th>MODEL 2</th>
<th>MODEL 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>128GB</td>
<td>256GB</td>
<td>512GB</td>
</tr>
<tr>
<td>SATA</td>
<td>NVME</td>
<td>SATA</td>
</tr>
</tbody>
</table>
<p>现在你和销售员要求至少 200GB 的存储容量，并且驱动器的型号是 NVME。</p>
<p>MODEL1 是容量小于 200GB 的 SATA，与你的要求不符，MODEL 3 容量大于 200GB，但是不是 NVME 接口的。只有 MODEL2 是容量大于 200GB 并且接口是 NVME 的。正是需要的。</p>
<p>销售人员向你展示的 SSD models 就等同于 persistent volumes，你的要求就等同于 persistent volume 声明。</p>
<p>在 <code>k8s</code> 目录下创建一个名为 <code>database-persistent-volume-claim.yaml</code>  的新文件，文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-persistent-volume-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
</code></pre>
<p><code>apiVersion</code>、 <code>kind</code> 和  <code>metadata</code> 和之前的作用一致。</p>
<ul>
<li><code>spec.storageClass</code>  表示存储类型的声明配置文件。 着意味着将任何设置为   <code>manual</code> 的 <code>spec.storageClass</code> 的  <code>PersistentVolume</code> 都适合此类声明。如果有多个设置为 <code>manual</code> 的 volumes ，那么将获得其中任意一个的声明。如果没有 class 为 <code>manual</code> 的 volume，那就动态配置一个。</li>
<li><code>spec.accessModes</code>  在此设置访问模式。这表明该声明希望使用具有 <code>ReadWriteOnce</code> 的  <code>accessMode</code>。假设有两个设置为  <code>manual</code> 的 volumes 。其中一个将其 <code>accessModes</code>  设置为 <code>ReadWriteOnce</code>，另一个设置为 <code>ReadWriteMany</code>。该声明将获取其中的 <code>ReadWriteOnce</code> 。</li>
<li><code>resources.requests.storage</code> 是此声明所需要的存储量 <code>2Gi</code> 并不意味着给定的卷必须恰好具有 2GB 的存储容量 。而是意味着它至少要有 2GB。你应该还记得之前将存储容量设置为 5GB，大于 2GB。</li>
</ul>
<p>执行下面的命令 apply 文件：</p>
<pre><code class="language-bash">kubectl apply -f database-persistent-volume-claim.yaml

# persistentvolumeclaim/database-persistent-volume-claim created
</code></pre>
<p>使用 <code>get</code> 命令确定 volume  已经成功创建：</p>
<pre><code class="language-bash">kubectl get persistentvolumeclaim

# NAME                               STATUS   VOLUME                       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# database-persistent-volume-claim   Bound    database-persistent-volume   5Gi        RWO            manual         37s
</code></pre>
<p>查看 <code>VOLUME</code> 列，这个声明与之前创建的 <code>database-persistent-volume</code>  持久卷绑定 ，在看一下 <code>CAPACITY</code>，它是 <code>5Gi</code>，因为该声明要求 volume  至少有 2GB 的存储容量。</p>
<h3 id="persistentvolumes">Persistent Volumes 的动态预配置</h3>
<p>在上一小节，你已经创建了一个 persistent volume，然后创建了一个声明，但是如果以前没有设置任何 persistent volume 该怎么办呢？</p>
<p>在这种情况下，将自动设置与声明兼容的持久卷。</p>
<p>开始之前，先执行如下命令删除之前创建的 persistent volume 和 persistent volume 声明：</p>
<pre><code class="language-yaml">kubectl delete persistentvolumeclaim --all

# persistentvolumeclaim "database-persistent-volume-claim" deleted

kubectl delete persistentvolumeclaim --all

# persistentvolume "database-persistent-volume" deleted

</code></pre>
<p>打开 <code>database-persistent-volume-claim.yaml</code> 文件将内容更新为如下内容：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-persistent-volume-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi

</code></pre>
<p>我已经从文件中删除了 <code>spec.storageClass</code> 字段，现在重新 apply <code>database-persistent-volume-claim.yaml</code> 文件（无需应用 <code>database-persistent-volume.yaml</code>  文件）：</p>
<pre><code class="language-yaml">kubectl apply -f database-persistent-volume-claim.yaml

# persistentvolumeclaim/database-persistent-volume-claim created
</code></pre>
<p>现在使用  <code>get</code>  命令查看声明信息：</p>
<pre><code class="language-yaml">kubectl get persistentvolumeclaim

# NAME                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# database-persistent-volume-claim   Bound    pvc-525ae8af-00d3-4cc7-ae47-866aa13dffd5   2Gi        RWO            standard       2s
</code></pre>
<p>正如你看到的，已经提供了名为 <code>pvc-525ae8af-00d3-4cc7-ae47-866aa13dffd5</code> 且存储容量为 <code>2Gi</code> 的 volume，将其动态绑定到了声明。</p>
<p>该项目的剩余部分使用静态或者动态预配置 persistent volume  都可以。我会使用动态配置。</p>
<h3 id="podsvolumes">通过 Pods 连接 Volumes</h3>
<p>现在你已经创建了一个 persistent volume 和声明，是时候让数据库 pod 使用该 volume 了。</p>
<p>可以把之前小节创建的 persistent volume 声明连接到 pod 上。打开 <code>postgres-deployment.yaml</code> 文件，将内容更新如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      # volume configuration for the pod
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: database-persistent-volume-claim
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          # volume mounting configuration for the container
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          env:
            - name: POSTGRES_PASSWORD
              value: 63eaQB9wtLqmNBpg
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>我在此配置文件中添加了两个字段。</p>
<ul>
<li><code>spec.volumes</code> 字段包含了供 pod 查找 persistent volume 申明的必要信息。  <code>spec.volumes.name</code>  可以是你想要的任何东西。<code>spec.volumes.persistentVolumeClaim.claimName</code> 必需与 <code>database-persistent-volume-claim.yaml</code> 文件中的 <code>metadata.name</code> 值相匹配。</li>
<li><code>containers.volumeMounts</code>  包含容器挂载的 volume  所必需的信息。<code>containers.volumeMounts.name</code> 必需与 <code>spec.volumes.name</code> 中的值相匹配。<code>containers.volumeMounts.mountPath</code> 代表 volume  挂载的目录。<code>/var/lib/postgresql/data</code> 是 PostgreSQL 的默认数据目录。<code>containers.volumeMounts.subPath</code> 表示将在 volume  创建的目录。假设你与其它的 pod 正在使用相同的 volume。保存在 <code>/var/lib/postgresql/data</code> 目录中的所有数据都将进入 volume 的  <code>postgres</code> 路径下。</li>
</ul>
<p>现在执行下面的命令重新 apply  <code>postgres-deployment.yaml</code> 文件：</p>
<pre><code class="language-yaml">kubectl apply -f postgres-deployment.yaml

# deployment.apps/postgres-deployment configured
</code></pre>
<p>现在，已经进行了正确的数据库部署，数据丢失的风险小了很多。</p>
<p>想要在这里提及的是，目前数据库部署中只有一个副本，如果有多个副本，那么情况会有所不同，</p>
<p>多个 pod 在不知道彼此存在情况下访问相同的 volume 会产生灾难性的后果 ，在 volume 内为 pod 创建子目录可以解决这个问题。</p>
<h3 id="">组装起来</h3>
<p>现在已经运行了 API 和数据库，是时候建立网络并做一些后续工作了。</p>
<p>在前面的章节中已经了解到 Kubernetes 中的网络设置，在开始编写服务之前，先看看我为项目制定的联网计划。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/notes-api-2.svg" alt="notes-api-2" width="600" height="400" loading="lazy"></p>
<ul>
<li>数据库只使用 <code>ClusterIP</code> service 在集群内暴露，不允许任何外部流量访问。</li>
<li>API 部署服务将暴露给外部世界，用户将与  API 通信，API 与数据库通信。</li>
</ul>
<p>之前通过 <code>LoadBalancer</code> service 将应用暴露给了外部世界，<code>ClusterIP</code> 则再集群中公开应用，并且不允许外部流量访问。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/cluster-ip-3.svg" alt="cluster-ip-3" width="600" height="400" loading="lazy"></p>
<p>鉴于数据库服务应仅在集群内可用 ，因此 <code>ClusterIP</code> service 服务非常适合此方案。</p>
<p>在 <code>k8s</code> 目录下创建一个名为  <code>postgres-cluster-ip-service.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: postgres-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: postgres
  ports:
    - port: 5432
      targetPort: 5432

</code></pre>
<p><code>ClusterIP</code> 的配置文件与 <code>LoadBalancer</code> 的配置文件差不多，唯一的不同的是  <code>spec.type</code>  。</p>
<p>现在，这个文件就清晰了。5432 是 PostgreSQL 运行的默认端口。所以也要集群内暴露 5432 。</p>
<p>接下来是 <code>LoadBalancer</code> service 的配置文件，负责将 API 暴露给外界。创建一个名为 <code>api-load-balancer-service.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: api-load-balancer-service
spec:
  type: LoadBalancer
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    component: api

</code></pre>
<p>此配置与上一节中的配置相同。API 运行在容器内的 3000 端口，所以也要在集群中暴露此端口。</p>
<p>最后要做的是要将环境变量添加到 API deployment 中。打开 <code>api-deployment.yaml</code> 文件并按照如下更新其内容：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          env:
            - name: DB_CONNECTION
              value: pg
            - name: DB_HOST
              value: postgres-cluster-ip-service
            - name: DB_PORT
              value: '5432'
            - name: DB_USER
              value: postgres
            - name: DB_DATABASE
              value: notesdb
            - name: DB_PASSWORD
              value: 63eaQB9wtLqmNBpg

</code></pre>
<p>之前，<code>spec.containers.env</code> 下面只有 <code>DB_CONNECTION</code> 变量，新涉及到的字段如下：</p>
<ul>
<li><code>DB_HOST</code>  表示数据库服务的地址。在非容器化环境中，该值通常为 <code>127.0.0.1</code>。但是在  Kubernetes 环境中，并不知道数据库容器的  IP 地址。因此只需使用公开的数据库 service 的名字即可 。</li>
<li><code>DB_PORT</code>  是数据库 service 公开的端口，即 5432。</li>
<li><code>DB_USER</code>  用于连接数据库的用户，默认的用户名是  <code>postgres</code>。</li>
<li><code>DB_DATABASE</code>  API 将要连接的数据库，必须与  <code>postgres-deployment.yaml</code> 文件中的 <code>spec.containers.env.DB_DATABASE</code> 值相同。</li>
<li><code>DB_PASSWORD</code>  用于连接数据库的密码，必须与 <code>postgres-deployment.yaml</code> 文件中的 <code>spec.containers.env.DB_PASSWORD</code> 值相匹配。</li>
</ul>
<p>完成此操作后就可以测试 API 了。在执行操作之前，执行一下下面的命令 apply 所有的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# deployment.apps/api-deployment created
# service/api-load-balancer-service created
# persistentvolumeclaim/database-persistent-volume-claim created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created

</code></pre>
<p>如果遇到任何错误，只需删除所有的资源并重新 apply 文件即可。service、persistent volumes、persistent volume 声明会立即创建。</p>
<p>用  <code>get</code>  命令确保所有的部署都已经启动且运行：</p>
<pre><code>kubectl get deployment

# NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment        3/3     3            3           106s
# postgres-deployment   1/1     1            1           106s
</code></pre>
<p>从 <code>READY</code> 列中可以看出，所有的 pod 都已经启动并且正在运行。执行  <code>minikube</code> 的    <code>service</code> 命令访问 API。</p>
<pre><code>minikube service api-load-balancer-service

# |-----------|---------------------------|-------------|-----------------------------|
# | NAMESPACE |           NAME            | TARGET PORT |             URL             |
# |-----------|---------------------------|-------------|-----------------------------|
# | default   | api-load-balancer-service |        3000 | http://172.19.186.112:31546 |
# |-----------|---------------------------|-------------|-----------------------------|
# * Opening service default/api-load-balancer-service in default browser...

</code></pre>
<p>API 会在默认浏览器里立即打开：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-93.png" alt="image-93" width="600" height="400" loading="lazy"></p>
<p>这时 API 的默认响应，还可以通过 <a href="https://insomnia.rest/">Insomnia</a>  或者  <a href="https://www.postman.com/">Postman</a>  来测试 <a href="http://172.19.186.112:31546/"><code>http://172.19.186.112:31546/</code></a> API 的完整  CRUD 功能。</p>
<p>可以将 API 源代码随附的测试作为文档查看。只需打开 <code>api/tests/e2e/api/routes/notes.test.js</code> 文件即可，如果你有 JavaScript 和<a href="https://expressjs.com/">express</a> 的经验，那么理解这个文件会很容易。</p>
<h2 id="ingresscontroller">使用 Ingress Controller</h2>
<p>目前为止，已经使用  <code>ClusterIP</code> 在集群内公开了的应用程序，使用 <code>LoadBalancer</code> 把应用暴露给集群外。</p>
<p>尽管我已经引用了  <code>LoadBalancer</code> 作为引用于集群外部公开应用程序的的标准 service，但是它还有一些缺点。</p>
<p>当使用 <code>LoadBalancer</code> services 在云环境中公开应用程序时，必需单独为每个公开的服务付费。这在大型项目下回非常昂贵。</p>
<p>还有另一种成为 <code>NodePort</code> 的 service，可以代替  <code>LoadBalancer</code>  service。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/node-port-2.svg" alt="node-port-2" width="600" height="400" loading="lazy"></p>
<p><code>NodePort</code>  在集群所有节点上打开一个特定的端口，并处理通过该打开端口的所有流量。</p>
<p>你知道，service 将多个 pod 组合在一起，并控制他们的访问方式。通过公开端口到达 service 的任何请求都将最终在正确的容器中。</p>
<p>用于创建  <code>NodePort</code> 的实例配置文件如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: hello-kube-node-port
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 31515
  selector:
    component: web
</code></pre>
<p>这里的  <code>spec.ports.nodePort</code> 字段值必须在 30000 和 32767 之间，此范围超出了各种服务通常所使用的常用端口。端口的数字位数很多。、</p>
<blockquote>
<p>尝试用 <code>NodePort</code> service 替换前面几节创建的 <code>LoadBalancer</code> service，这应该不难，算是对所学知识的简单测试。</p>
</blockquote>
<p>创建 <code>Ingress</code> 可以解决此问题，澄清一下，<code>Ingress</code> 不是一种 service，相反，它位于各个 service 前面，充当路由器的角色。</p>
<p>在集群中使用 <code>Ingress</code> 资源用到了 <code>IngressController</code>。可以在 Kubernetes  <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/">文档</a> 中找到可用 的 ingress  controllers 列表。</p>
<h3 id="nginxingresscontroller">设置 NGINX Ingress Controller</h3>
<p>在此例中，通过向其添加 front end 来扩展 notes API。使用 <code>Ingress</code> 来暴露应用，而不是使用诸如 <code>LoadBalancer</code> 或者 <code>NodePort</code> 之类的 service。</p>
<p>将要使用的而控制器是 <a href="https://github.com/kubernetes/ingress-nginx/blob/master/README.md">NGINX Ingress Controller</a>，在此 <a href="https://www.nginx.com/">NGINX</a>  将用于不同 service 请求的路由。NGINX Ingress Controller 使 Kubernetes  集群的 NGINX  配置变的更容易。</p>
<p>项目代码在 <code>fullstack-notes-application</code> 路径下：</p>
<pre><code class="language-bash">.
├── api
├── client
├── docker-compose.yaml
├── k8s
│   ├── api-deployment.yaml
│   ├── database-persistent-volume-claim.yaml
│   ├── postgres-cluster-ip-service.yaml
│   └── postgres-deployment.yaml
├── nginx
└── postgres

5 directories, 1 file
</code></pre>
<p>你会看到  <code>k8s</code> 目录，包含在上一个小节中除了 <code>api-load-balancer-service.yaml</code> 文件的所有的配置文件。</p>
<p>原因是，在该项目中，旧的 <code>LoadBalancer</code> service 将被 <code>Ingress</code> 代替。另外，无需公开 API，而是将前端应用程序公开即可。</p>
<p>在开始编写新文件之前，先看看架构。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/fullstack-1.svg" alt="fullstack-1" width="600" height="400" loading="lazy"></p>
<p>用户访问前端应用程序并提交必要的数据，然后前端应用程序将提交的数据转发到后端 API。</p>
<p>然后 API 将数据保留在数据库中，并将其发送回前端应用程序。然后使用 NGINX 实现请求的路由。</p>
<p>可以查看 <code>nginx/production.conf</code> 文件了解如何设置此路由。</p>
<p>现在实现目标所必需的网络如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/ingress.svg" alt="ingress" width="600" height="400" loading="lazy"></p>
<p>具体如下：</p>
<ul>
<li><code>Ingress</code>  将充当此应用程序的入口点和路由器，这是一个 <code>NGINX</code> 类型的 <code>Ingress</code>，因此端口是 nginx 的默认端口 80。</li>
<li>到 <code>/</code> 的每个请求都会被路由到前端应用（左侧的服务）处理。因此，如果应用程序的 URL  是 <code>https://kube-notes.test</code> ，那么所有的 <code>https://kube-notes.test/foo</code> 或者 <code>https://kube-notes.test/bar</code> 都会由前端应用程序处理。</li>
<li>到 <code>/api</code> 的每个请求都会被路由到后端的 API （右侧的服务）处理。因此，如果 URL 是 <code>https://kube-notes.test</code>，那么所有的 <code>https://kube-notes.test/api/foo</code> 或者  <code>https://kube-notes.test/api/bar</code> 都会由后端 API 处理。</li>
</ul>
<p>完全可以将 <code>Ingress</code> service  配置与子域名一起使用，而不是向这样的路径，这里的设计使用路径的方式。</p>
<p>在本小节中，必需编写四个新的配置文件：</p>
<ul>
<li><code>ClusterIP</code>  是 API deployment 的配置。</li>
<li><code>Deployment</code>  是 front-end 应用的配置。</li>
<li><code>ClusterIP</code>  是 front-end 应用的配置。</li>
<li><code>Ingress</code>  是路由的配置。</li>
</ul>
<p>前三个文件我会快速的过一下。</p>
<p>第一个文件是  <code>api-cluster-ip-service.yaml</code> 配置，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: api-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: api
  ports:
    - port: 3000
      targetPort: 3000
</code></pre>
<p>尽管在上一小节中，将 API 直接暴露给了外界，但在本小节中，<code>Ingress</code> 承担起了这个任务，同时使用 <code>ClusterIP</code>   在内部公开 API。</p>
<p>配置不言自明，无需过多解释。</p>
<p>接下来创建一个名为 <code>client-deployment.yaml</code> 的文件来运行前端应用，内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: client
  template:
    metadata:
      labels:
        component: client
    spec:
      containers:
        - name: client
          image: fhsinchy/notes-client
          ports:
            - containerPort: 8080
          env:
            - name: VUE_APP_API_URL
              value: /api
</code></pre>
<p>它几乎与 <code>api-deployment.yaml</code> 文件相同，很好理解。</p>
<p><code>VUE_APP_API_URL</code> 环境变量表示 API 请求应该转发的路径。这些转发请求将依次由<code>Ingress</code> 处理。</p>
<p>需要另一个 <code>ClusterIP</code>   service 来公开客户端应用程序。创建一个名为 <code>client-cluster-ip-service.yaml</code> 的新文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: client-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: client
  ports:
    - port: 8080
      targetPort: 8080

</code></pre>
<p>描述的是运行在集群上默认暴露在 8080 端口上的前端应用。</p>
<p>在完成了旧配置之后，下一个配置是 <code>ingress-service.yaml</code> 文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - path: /?(.)
            backend:
              serviceName: client-cluster-ip-service
              servicePort: 8080
          - path: /api/?(.)
            backend:
              serviceName: api-cluster-ip-service
              servicePort: 3000

</code></pre>
<p>该文件有一些新配置，也很好理解：</p>
<ul>
<li><code>Ingress</code>  API 仍处于测试阶段，所以 <code>apiVersion</code> 是 <code>extensions/v1beta1</code>。尽管处于 beta 版本，该 API 很稳定，可以直接在生产环境中使用。</li>
<li><code>kind</code>  和 <code>metadata.name</code> 字段和之前配置相同。</li>
<li><code>metadata.annotations</code>  可以包含有关 <code>Ingress</code>  配置的信息。<code>kubernetes.io/ingress.class: nginx</code> 表示 <code>Ingress</code> 对象应该由 <code>ingress-nginx</code> 控制器控制。<code>nginx.ingress.kubernetes.io/rewrite-target</code> 表示<a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rewrite">重写</a>目标 URL 的地方。</li>
<li><code>spec.rules.http.paths</code>  包含在 <code>nginx/production.conf</code> 文件中看到的各个路径的路由配置。 <code>paths.path</code>  表示路由的路径，<code>backend.serviceName</code> 是上述路径应该匹配的 service。<code>backend.servicePort</code> 是服务内部的端口。</li>
<li><code>/?(._)_</code> 和 <code>/api/?(.</code>_<code>)</code>  是简单的正则表达式，表示  <code>?(.*)</code>  部分会被路由到指定的服务。</li>
</ul>
<p>配置重写的方式会时不时发生变化，具体可以查看官方<a href="https://kubernetes.github.io/ingress-nginx/examples/rewrite/">文档</a>。</p>
<p>在 apply 新的配置之前，使用  <code>addons</code> 命令激活 <code>minikube</code> 的 <code>ingress</code> 插件，用法如下：</p>
<pre><code class="language-bash">minikube addons &lt;option&gt; &lt;addon name
</code></pre>
<p>执行如下命令激活 <code>ingress</code> 插件：</p>
<pre><code class="language-bash">minikube addons enable ingress

# ? Verifying ingress addon...
# ? The 'ingress' addon is enabled
</code></pre>
<p>可以对 <code>addon</code>  命令使用  <code>disable</code> 选项来禁用插件，查看官网<a href="https://minikube.sigs.k8s.io/docs/commands/addons/">文档</a>了解  <code>addon</code> 命令的更多信息。</p>
<p>插件激活后，可以 apply 配置文件，建议在 apply 新资源之前删除所有的资源（service、deployment 和 persistent volume claims）。</p>
<pre><code>kubectl delete ingress --all

# ingress.extensions "ingress-service" deleted

kubectl delete service --all

# service "api-cluster-ip-service" deleted
# service "client-cluster-ip-service" deleted
# service "kubernetes" deleted
# service "postgres-cluster-ip-service" deleted

kubectl delete deployment --all

# deployment.apps "api-deployment" deleted
# deployment.apps "client-deployment" deleted
# deployment.apps "postgres-deployment" deleted

kubectl delete persistentvolumeclaim --all

# persistentvolumeclaim "database-persistent-volume-claim" deleted

kubectl apply -f k8s

# service/api-cluster-ip-service created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# ingress.extensions/ingress-service created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
</code></pre>
<p>使用  <code>get</code> 命令来确保所有的资源都已经创建成功。当全部运行后，可以通过 <code>minikube</code> 集群的 IP 地址访问该应用程序。执行如下命令获取 IP 地址：</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>还可以通过运行 <code>Ingress</code> 来获取此 IP 的地址：</p>
<pre><code class="language-bash">kubectl get ingress

# NAME              CLASS    HOSTS   ADDRESS      PORTS   AGE
# ingress-service   &lt;none&gt;   *       172.17.0.2   80      2m33s
</code></pre>
<p>IP 和 端口分别在 <code>ADDRESS</code> 和 <code>PORTS</code> 端口列下。访问 <code>127.17.0.2:80</code>，可以直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-84.png" alt="image-84" width="600" height="400" loading="lazy"></p>
<p>可以在此应用中执行简单的 CRUD 操作，端口 80 是 NGINX 的默认端口，因此可以省略 URL 中的端口号。</p>
<p>如果你了解如何配置 NGINX，可以使用  ingress controller 执行很多操作。毕竟，这就是控制器的用途 - 将 NGINX 的配置存储在 Kubernetes  的 <code>ConfigMap</code> 上，将会在下一部分中学习。</p>
<h3 id="kubernetessecret">Kubernetes 中的 Secret 和配置</h3>
<p>目前为止，部署中使用纯文本形式存储了敏感信息，如 <code>POSTGRES_PASSWORD</code>，这并不是最佳实践。</p>
<p>可以用 <code>Secret</code> 将值存储在集群中，这是存储密码、token 等的更安全的方法。</p>
<blockquote>
<p>在 Windows 命令行中，下一步可能无法正常工作，可以使用 <a href="https://git-scm.com/">git</a> 终端或者 <a href="https://cmder.net/">cmder</a> 完成此任务。</p>
</blockquote>
<p>需要将数据转换成 base64 数据才能将信息存储在 <code>Secret</code> 中。如果纯文本密码为  <code>63eaQB9wtLqmNBpg</code> ，执行以下命令获取 base64 版本。</p>
<pre><code class="language-bash">echo -n "63eaQB9wtLqmNBpg" | base64

# NjNlYVFCOXd0THFtTkJwZw==
</code></pre>
<p>此步骤是必要的，参数必需是 base64 格式的，现在在  <code>k8s</code>   目录下创建一个  <code>postgres-secret.yaml</code> 文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
data:
  password: NjNlYVFCOXd0THFtTkJwZw==
</code></pre>
<p><code>apiVersion</code>、<code>kind</code> 和  <code>metadata</code> 的意义无需解释，<code>data</code> 字段就是真实的密文。</p>
<p>如上，创建了一个键值对，键是 <code>password</code>， 值是 <code>NjNlYVFCOXd0THFtTkJwZw==</code>。将使用 <code>metadata.name</code> 值在其他配置文件中作为获取密码值的 <code>Secret</code> 的标识。</p>
<p>按如下更新 <code>postgres-deployment.yaml</code> 文件，以在数据库配置中使用此密码：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: database-persistent-volume-claim
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          env:
              # not putting the password directly anymore
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>如上，除了 <code>spec.template.spec.continers.env</code>  字段外，所有的字段介绍过。</p>
<p>之前用于存储密码的 <code>name</code> 环境变量是纯文本。但是现在是 <code>valueFrom.secretKeyRef</code> 字段。</p>
<p>这里的 <code>name</code>  字段是指刚刚创建的 <code>Secret</code> 的名字，<code>key</code> 值是指 <code>Secret</code> 配置文件键值对中的键。Kubernetes  将在内部将编码后的值解码为纯文本。</p>
<p>除了数据库配置，你还需按如下所示更新 <code>api-deployment.yaml</code> 文件：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          env:
            - name: DB_CONNECTION
              value: pg
            - name: DB_HOST
              value: postgres-cluster-ip-service
            - name: DB_PORT
              value: '5432'
            - name: DB_USER
              value: postgres
            - name: DB_DATABASE
              value: notesdb
              # not putting the password directly anymore
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

</code></pre>
<p>现在执行下面的命令 apply  这些新的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/api-cluster-ip-service created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# secret/postgres-secret created
# ingress.extensions/ingress-service created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
</code></pre>
<p>取决于集群的状态不同，输出可能会不同。</p>
<blockquote>
<p>谨慎起见，先删除所有的资源然后在 apply 配置文件来创建他们。</p>
</blockquote>
<p>使用 <code>get</code>  命令检查并确保所有的 pod 都已经启动并且正在运行。</p>
<p>使用 <code>minikube</code>  IP 访问 notes 应用程序并尝试创建新的 notes， 来测试新的配置。</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>访问  <code>127.17.0.2:80</code>，你应该会直接进入 Notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p>还有一种无需任何配置文件即可创建 secret  的方法，执行如下命令，使用 <code>kubectl</code> 创建相同的 <code>Secret</code>。</p>
<pre><code class="language-bash">kubectl create secret generic postgres-secret --from-literal=password=63eaQB9wtLqmNBpg

# secret/postgres-secret created
</code></pre>
<p>这是一种更方便的方法，因为可以跳过整个 base64 编码步骤。在这种情况下，secret 会被自动编码。</p>
<p><code>ConfigMap</code> 和  <code>Secret</code> 类似，一般用于非隐私的的信息。</p>
<p>在  <code>k8s</code>  目录下创建一个名为 <code>api-config-map.yaml</code> 的文件，把 API deployment 里所有的其余的环境变量放在  <code>ConfigMap</code> 里：</p>
<pre><code class="language-yaml">apiVersion: v1 
kind: ConfigMap 
metadata:
  name: api-config-map 
data:
  DB_CONNECTION: pg
  DB_HOST: postgres-cluster-ip-service
  DB_PORT: '5432'
  DB_USER: postgres
  DB_DATABASE: notesdb

</code></pre>
<p><code>apiVersion</code>、  <code>kind</code>  和 <code>metadata</code> 无需解释。<code>data</code> 字段是以键值对形式的环境变量。</p>
<p>和 <code>Secret</code> 不同，此处的 key 必需与 API 所需的 key 匹配。因此，我从 <code>api-deployment.yaml</code> 文件中复制了一些变量，并稍作修改后粘贴到了此处。</p>
<p>要在 API deployment 中使用 secret，打开 <code>api-deployment.yaml</code>  文件并做如下修改：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          # not putting environment variables directly
          envFrom:
            - configMapRef:
                name: api-config-map
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

</code></pre>
<p>文件除了 <code>spec.template.spec.containers.env</code>  字段外几乎没有改变。</p>
<p>我已经将环境变量移到了 <code>ConfigMap</code> 中。<code>spec.template.spec.containers.envFrom</code>  用来从 <code>ConfigMap</code> 中获取数据。<code>configMapRef.name</code> 表示将从中 提取环境变量的 <code>ConfigMap</code>  。</p>
<p>然后执行下面的命令 apply 所有的配置：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/api-cluster-ip-service created
# configmap/api-config-map created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# ingress.extensions/ingress-service configured
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
# secret/postgres-secret created
</code></pre>
<p>取决于集群的状态，输出可能会不同。</p>
<blockquote>
<p>谨慎起见，先删除所有的 Kubernetes  资源然后在 apply configs 创建他们。</p>
</blockquote>
<p>使用 <code>get</code> 命令确保 pod 已经启动并运行，使用 <code>minikube</code> IP 访问 notes 应用并尝试创建新的 note。</p>
<p>执行下面的命令获取 IP 地址。</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>访问  <code>127.17.0.2:80</code> ，直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p><code>Secret</code>  和 <code>ConfigMap</code> 还有其它一些技巧，就不在这里展开了，如果想了解，可以查看官方<a href="https://kubectl.docs.kubernetes.io/pages/app_management/secrets_and_configmaps.html">文档</a>。</p>
<h3 id="kubernetes">在 Kubernetes 中执行更新发布</h3>
<p>既然已经在 Kubernetes 上成功部署了一个包含多个容器的应用程序，是时候学习执行更新了。</p>
<p>Kubernetes 很神奇，将容器更新为较新版本的镜像比较麻烦，有很多种方式更新容器，这里不会涉及到所有的方法。</p>
<p>相反，我将直接进入更新容器时主要采取的方法。如果打开  <code>client-deployment.yaml</code> 文件并查看 <code>spec.template.spec.containers</code> 字段，会看到下面的配置：</p>
<pre><code class="language-yaml">containers:
    - name: client
      image: fhsinchy/notes-client
</code></pre>
<p>如上，在  <code>image</code>  字段中，没有使用任何镜像标签。现在，如果你认为在镜像尾部添加  <code>:latest</code>  将确保部署始终拉取最新的镜像，那你可就大错特错了。</p>
<p>我通常采用最简单的路径。之前提到过，在某些情况下，使用命令式而不是声明式是一个好主意，创建一个 <code>Secret</code> 或者更新容器就是这种情况。</p>
<p>可以用来执行更新的命令是 <code>set</code>  命令，其通用语法如下：</p>
<pre><code class="language-bash">kubectl set image &lt;resource type&gt;/&lt;resource name&gt; &lt;container name&gt;=&lt;image name with tag&gt;
</code></pre>
<p>资源类型为 <code>deployment</code>，资源名称为 <code>client-deployment</code>。可以在 <code>client-deployment.yaml</code> 文件内的 <code>containers</code> 字段找到容器的名称，本例中为  <code>client</code>  。</p>
<p>我已经构建了带有标签  <code>edge</code> 的 <code>fhsinchy/notes-client</code> 镜像，将使用它来更新 <code>fhsinchy/notes-client</code> 的版本。</p>
<p>最终命令如下：</p>
<pre><code class="language-bash">kubectl set image deployment/client-deployment client=fhsinchy/notes-client:edge

# deployment.apps/client-deployment image updated
</code></pre>
<p>由于 Kubernetes 将重新创建所有的 pod，执行此更新可能需要一段时间，可以运行 <code>get</code> 命令来了解是否所有的 pod 都已经启动并且成功运行。</p>
<p>重新创建后，使用 <code>minikube</code>  IP 访问 notes 应用并尝试创建新的 notes。可以执行下面的命令获取 IP：</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>通过访问 <code>127.17.0.2:80</code> 应该可以直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p>鉴于我还未对应用程序代码进行任何实际更改，因此所有的内容都将保持不变。你可以使用  <code>describe</code> 命令来确保 pod 正在使用新的镜像。</p>
<pre><code class="language-bash">kubectl describe pod client-deployment-849bc58bcc-gz26b | grep 'Image'

# Image:          fhsinchy/notes-client:edge
# Image ID:       docker-pullable://fhsinchy/notes-client@sha256:58bce38c16376df0f6d1320554a56df772e30a568d251b007506fd3b5eb8d7c2
</code></pre>
<p><code>grep</code> 命令在 Mac 和 Linux 可以直接使用，如果你使用的是 Windows，使用 git bash 而不是 Windows 命令行。</p>
<p>尽管强制性更新过程有些繁琐，但是通过好的 CI/CD 流程可以使其变得更加容易。</p>
<h3 id="configurations">组合 Configurations</h3>
<p>尽管其中只有三个容器，但该项目中的配置文件数量已经非常庞大了。</p>
<p>实际上可以按照如下方式组合配置文件：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: client
  template:
    metadata:
      labels:
        component: client
    spec:
      containers:
        - name: client
          image: fhsinchy/notes-client
          ports:
            - containerPort: 8080
          env:
            - name: VUE_APP_API_URL
              value: /api
              
---

apiVersion: v1
kind: Service
metadata:
  name: client-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: client
  ports:
    - port: 8080
      targetPort: 8080
</code></pre>
<p>如上，我已经使用界定符（<code>---</code>）组合了 <code>client-deployment.yaml</code> 和 <code>client-cluster-ip-service.yaml</code> 文件。尽管有可能在容器数量很多的项目中减少文件，但我还是建议将他们分开，更简洁、更干净。</p>
<h2 id="">答疑</h2>
<p>在本节中，我将列出你使用 Kubernetes 时可能遇到的一些常见问题。</p>
<ul>
<li>如果你在 Windows 或者 Mac 使用 Docker 的 <code>minikube</code>，<code>Ingress</code>  插件可能并没有生效。、</li>
<li>如果你在 Mac 上运行了<a href="https://laravel.com/docs/7.x/valet">Laravel Valet</a>，并且将 HyperKit 驱动程序用于<code>minikube</code>，会联网失败。关闭 <code>minikube</code> 服务可以解决此问题。</li>
<li>如果你有一台 Ryzen (mine is R5 1600) PC，并且正在运行 Windows 10，由于缺少内嵌虚拟化支持 VirtualBox  可能会启动失败。必须在 Windows 10 （Pro、Enterprise 和 Education）上安装 Hyper-V 驱动程序，对于家庭版，很遗憾没有该选项。</li>
<li>如果你在 Windows 10 (Pro, Enterprise 和 Education)  上使用用于 <code>minikube</code> 的 Hyper-V  驱动，VM 可能会启动失败，并显示 内存不足的消息。不要紧张，执行 <code>minikube start</code> 重新启动 VM。</li>
<li>如果你在 Windows 命令行中看到本文执行的某些命令丢失，或者功能异常，请改用  <a href="https://git-scm.com/">git</a> 命令行或者  <a href="https://cmder.net/">cmder</a> 。</li>
</ul>
<p>我建议在你的系统上安装一个 Linux 发行版，并将 Docker 驱动程序用于 <code>minikube</code>。目前为止，这是最快也是最可靠的设置。</p>
<h2 id="">结论</h2>
<p>衷心感谢你花了这么长时间阅读本文，希望你享受学习过程，并了解了 Kubernetes 的基础知识。</p>
<p>你可以关注我的推特   <a href="https://twitter.com/frhnhsin">@frhnhsin</a>  或者在 LinkedIn <a href="https://www.linkedin.com/in/farhanhasin/">/in/farhanhasin</a>  上与我联系。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/">The Kubernetes Handbook</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
