头图

<!-- 头图: MammothHS.jpg ,来自 bing.com -->

[笔记] K8S 基础使用

  • https://kubernetes.io/docs
  • https://kubernetes.io/zh/docs

上面俩是这玩意儿的官方文档,或许挺全的,但反正我觉得就没说到点上:想查啥它不说啥,只管自个儿念经。

所以我就想自己搞一个,万一自己哪天阿尔兹海默失忆了,如果那时候还活着的话,兴许也还能重新拾起来这部分掌握过的东西。

基本

这 k8s 的一大特点就是概念贼 TM 多。它那套文档喜欢先介绍概念,那我就对着干,先从介绍怎么使开始吧:

一般在使用中基本不会用到 kubectl 以外的命令。

基本的使用就是这些的简单示例:

  • 查有哪些 Pod
  • 进入 Pod

还有一个很有用的基本概念:

  • Pod 和 Container 究竟是啥子关系

查 Pod

如果你想查有哪些 Pod 的话:

kubectl get po

如果想指定 wahaha 这个命名空间(不知道这是啥没关系用用就能知道想查概念可自己查):

kubectl get po -n wahaha

这个 -n 也可以写成 --namespace ,前者是简写。

好了,会用这些,你就迈出了一大步。如果你有 Pod ,那你看到的打印应该会是一个好像表格一样比较整齐的文本输出。想看更多字段可以【🦕在后面加上】 -o wide

kubectl get po -n wahaha -o wide

(以后出现【🦕在后面加上】的字样就表示下面的命令是上一条提到的命令后面加上所述内容——当然要有空格分开!我并不是在说字符串拼接!)

如果想直接看全部的 Pod 就这样:

kubectl get po -A

还有别的查法:就是带上选项 -l 然后用选择器去找那种,在 label 有特定定义的 Pod 。

进 Pod

一般

如果你有一个 Pod 叫 abc (在默认命名空间下),而且这个 Pod 只有一个容器,那进去的办法就是:

kubectl exec abc -t -i -- sh
  • 一般那个 sh 是用 bash ,后者更好用但不一定有,这取决于建立容器的镜像。
  • 那个 -i 的软件自释义: Pass stdin to the container ,大概意思应该是 传递 stdin (标准输入)到容器中
  • 那个 -t 的软件自释义: Stdin is a TTY ,大概意思应该是 标准输入( stdin )是终端( TTY )(或者说「标准输入是来自终端的」)

上面我解释了俩选项( Option ),我是从 kubectl exec -h 的输出看到这些的,这个输出还表示了这些短选项名的等价长名版本(以及如果不指定的话的默认值)。

这里所谓的 进容器 含义其实是可以交互式地操作容器,做这种事一定离不开 SHell ,而 Linux 一般会默认装好 sh ash bash 这三个 SHell 软件,当然也可能只装了 sh 这一个或者只是没装 bash 。这里不考虑 都没装 的情况。那么,所谓 进容器 其实就是以交互模式启动一个容器内的 sh (或者别的 SHell )软件 了。

指定容器

如果 Pod abc 之下 (我特地不说成 这样的描述之后解释)有多个容器(对 Pod 定义容器的定义文件里容器是数组所以可以是多个),其中一个名字叫 ddf ,想进这个容器,一般这样写:

kubectl exec abc -c ddf -t -i -- sh

就是【🦕在里面加上】 -c ddf

而且,这个是无所谓有没有多个容器在这 Pod 下的,只是,若 Pod 下只有一个容器,则就不必再非要指定进入哪个容器罢了。

命名空间

如果是 Pod abc 在命名空间 qwe 下,要进入其中的容器 ddf 就需要这样:

kubectl exec abc -c ddf -n qwe -t -i -- sh

其中的 -n qwe-c ddf 之间的先后无所谓,所有选项和 Pod 名 abc 这些所有东西之间的顺序也都是无所谓的(但是我觉得 Pod 名靠前紧挨在 exec 后写会清晰一点)。

Pod and Container

上面说 Pod 下 而不是 Pod 内 ,是因为 Pod 是没有实体的,容器有,而 Pod 则是一组容器,只是对 K8S 来说,调度编排最小单位是 Pod 。

如果你的 K8S 是基于 Docker 运行的,这意味着, K8S 相关组件是一些 Docker 容器,并且你启动的 Pod ,在它的所在节点,也可以用 docker ps 找到对应的运行中的容器,并且你还会发现,用 docker exec 来进入这个容器,看起来几乎和上面通过 Pod 进容器,是一样的。

如此一来, K8S 就更像是一个 Docker 的上层封装了。(当然了论文档的话却是 Docker 的更好一些 K8S 的则更像是在搞形式主义。。。。)

如果你在 qwe 命名空间下有 abc Pod 下的 ddf 容器,可以参考以下这样的命令去在正确的节点找到对应容器:

docker ps | (fgrep qwe | fgrep abc | fgrep ddf)

上面的小括号可以不写;小括号内(用 | 分开的)三个部分顺序可随便互换(指这不会导致输出结果会有内容上的不同)。

然后输出内容会很长,后面有个字段是容器名字,这个名字里包含了: K8S Pod 名、在 K8S Pod 下定义的容器名、 K8S 命名空间名 等部分。

你现在知道了它对应的 Docker 容器名是比如 xxxxxxxxxxx ,进入就是像这样:

docker exec -t -i -- xxxxxxxxxxx sh

命令 docker 的参数和命令 kubectl 是不太一样的,只是简写的情况下会相似:

docker exec -ti xxxxxxxxxxx sh

命令 docker 的容器名是无关选项的参数部分(在 -- 后的部分)的第一项,在其中执行什么命令则是这里的第二项;

命令 kubectl 则不是这样,无关选项的参数部分(在 -- 后的部分)全是在容器内要执行的命令,而 Pod 名本身如同是在选项相关参数区域里被特殊对待的部分。(大概是因此 K8S 才会强烈建议不要省略双横线 -- 吧。)

Pod 的意义

它最大的特点就是 易失性 ,就是说,单个 Pod 一定是一个临时的东西,是必须不能指望其长久运行的东西

它要求,一项长久运行的资源,其实就是时间轴上排上多个像这样的短期资源,你访问长期资源的时候其实是访问一个短期资源,具体哪个短期资源,用的人不该知道也不必知道。

一般,我们重启一个资源(可以是运行的机器或者运行的程序实例(即进程)),我们是先关了再开。由于引入 易失性 这个性质,从而只要再确保开的那个开开了,外面访问(长期)资源的接口只需要做一下切换,再关掉那个(作为短期资源的) Pod 就好了。如果说用的人感受到访问失败,那也是在切换的那会儿,而这会儿很短,比先关再开的等待时间短非常多,这也就是 K8S 优势的基础了。

定义资源

就是一般的写 Yaml ,等价的 Json 也是可以的。

解释

就是子命令 kubectl explain 。它后面跟一种对象路径的写法,比如,你想写一个 Ingress 的定义,你就可以执行:

kubectl explain Ingress # or # kubectl explain ing

来获取到一些信息。输出内容里,一共有这几大块:

  • KIND :这个是定义对象的类型(全称)。
  • VERSION :这个应该是这个类型当前的版本(据说这就是 K8S 插件设计的体现)。
  • DESCRIPTION :这个下面是当前所指对象的描述。
  • FIELDS :字段;这个下面的也可以理解在当前对象路径下的子对象路径。

我这里执行 kubectl explain ing 的输出实在这样的:

KIND:     Ingress
VERSION:  extensions/v1beta1

DESCRIPTION:
     Ingress is a collection of rules that allow inbound connections to reach
     the endpoints defined by a backend. An Ingress can be configured to give
     services externally-reachable urls, load balance traffic, terminate SSL,
     offer name based virtual hosting etc. DEPRECATED - This group version of
     Ingress is deprecated by networking.k8s.io/v1beta1 Ingress. See the release
     notes for more information.

FIELDS:
   apiVersion   <string>
     APIVersion defines the versioned schema of this representation of an
     object. Servers should convert recognized schemas to the latest internal
     value, and may reject unrecognized values. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

   kind <string>
     Kind is a string value representing the REST resource this object
     represents. Servers may infer this from the endpoint the client submits
     requests to. Cannot be updated. In CamelCase. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

   metadata     <Object>
     Standard object's metadata. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

   spec <Object>
     Spec is the desired state of the Ingress. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

   status       <Object>
     Status is the current state of the Ingress. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

我看到字段下面有个 spec ,假设我是根据它的描述领会了它可以用来干啥,我想知道它的子对象(即它的 字段 ),那就执行 kubectl explain ing.spec ,就可以看到输出:

KIND:     Ingress
VERSION:  extensions/v1beta1

RESOURCE: spec <Object>

DESCRIPTION:
     Spec is the desired state of the Ingress. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

     IngressSpec describes the Ingress the user wishes to exist.

FIELDS:
   backend      <Object>
     A default backend capable of servicing requests that don't match any rule.
     At least one of 'backend' or 'rules' must be specified. This field is
     optional to allow the loadbalancer controller or defaulting logic to
     specify a global default.

   ingressClassName     <string>
     IngressClassName is the name of the IngressClass cluster resource. The
     associated IngressClass defines which controller will implement the
     resource. This replaces the deprecated `kubernetes.io/ingress.class`
     annotation. For backwards compatibility, when that annotation is set, it
     must be given precedence over this field. The controller may emit a warning
     if the field and annotation have different values. Implementations of this
     API should ignore Ingresses without a class specified. An IngressClass
     resource may be marked as default, which can be used to set a default value
     for this field. For more information, refer to the IngressClass
     documentation.

   rules        <[]Object>
     A list of host rules used to configure the Ingress. If unspecified, or no
     rule matches, all traffic is sent to the default backend.

   tls  <[]Object>
     TLS configuration. Currently the Ingress only supports a single TLS port,
     443. If multiple members of this list specify different hosts, they will be
     multiplexed on the same port according to the hostname specified through
     the SNI TLS extension, if the ingress controller fulfilling the ingress
     supports SNI.

然后基于这种办法,应该就可以「无师自通」地学会怎么写定义文件了。。。(你看它甚至给你指明了字段的类型。。。🐌)

(然而,什么 poingscpspsts 啊等等等等,这么多资源了。。。。我现在还不知道怎么知道所有类型的资源以及对应全(简)称。)

理想状态下反正是这样的。我知道实际上不是理想状态,最快的学习顺序也并不是它们官方给的这种帮助信息或者在线文档(谷歌的在线文档是真的敷衍又念经可能确实是没给员工留出太多时间写吧),是啥我也不知道,但是,知道可以像这样 kubectl explain 的话,应该能稍微降低一点难度了吧。。。(只可惜不是中文。。。)

生成 Pod 或者别的 KIND 的资源一般都是写定义文件,或者是 Yaml 或者是 Json 。具体示例大家可以自己搞,这里只是介绍一种用于理解具体的某份定义文件的途径。(那这么多杂七杂八的也够你查和理解一阵子了。。。)

相应的,对于 docker 也是同样的道理,比如你想看某个镜像的默认启动命令,但是不知道咋看,那也可以这样:

  • 首先,镜像不就是 image 嘛,那就执行 docker image --help 获取一下帮助信息;
  • 它会列出来这个子命令的再下一级子命令,我反正看来看去就看 inspect 好像是我要用的,因为别的都跟我想做的完全不搭边,那就执行一下 docker image inspect nginx 如果你想看 nginx 这个镜像的默认启动命令的话,然后就会出来一堆 Json ,这总比 Yaml 看着放心吧?你就可以看这个镜像的很多方面细节了。
  • 然后看到可以的键值对就可以挨个搜一下,比如我搜到这么个有趣的东西: https://yeasy.gitbook.io/docker_practice/image/dockerfile/entrypoint (原来启动时执行的命令不光在 CMD 里)

找资源

一般,不管啥 KIND 下,第一波字段,都会有 metadata 这一个。对于 Ingress 资源来说就是执行 kubectl explain ing.metadata 可以看到关于这一个层级的详细介绍。这里定义 元信息 ,像如,这个资源对象叫啥、在哪个命名空间,都是在这儿定义的。

而一般,在不管 啥 KIND 的资源下的 metadata 下,都有一个字段叫 labels ,可以看到类型是键值对( map ),这个里面你可以任意定义各种字段与值的对应:

  • 首先可以看到,执行 kubectl explain ing.metadata.labels 的话它是没有 FIELDS 部分来表示其下字段有哪些的,只是有一个 FIELD 部分来像 DESCRIPTION 部分一样用于对 ing.metadata.labels 自身来做不同方面的描述。
  • 其次,如果尝试执行 kubectl explain ing.metadata.labels.aaaa 或者 kubectl explain ing.metadata.labels.zzzz 这样,即在下一级对象路径随便写,也都是能正确执行并得到一个 DESCRIPTION 部分为 <empty> (表示空)的输出信息的,但这种行为在对象( Object )类型的层级下就不行,不信自己随便试试,比如,执行 kubectl explain ing.metadata.zzz 会有错误信息: error: field "zzz" does not exist

然后 selector。。。。

(未完)

(重大发现:服务的选择器应当对应豆荚的标签!!!而不是部署或副本集的标签!!!(之所以叫重大发现是因为竟然没有任何一个示例体现出这一点;大家都是把能跑就行给来回复制的吗。。。))

抽文件

configMap

其中, kubectl explain po.spec.volumes.configMap 的执行结果,描述了如何引用一个 configMap 到特定的卷。

下面举个例子。

Pod 类型的资源里:

  • po.spec.containers.volumeMounts 下的这两个位置这样写:

    • name 的值为 haproxy-config
    • mountPath 的值为 /usr/local/etc/haproxy
  • po.spec.volumes 下的这些位置这样写:

    • name 的值为 haproxy-config
    • configMap.name 的值为 haproxy-appdemo
    • configMap.defaultMode 的值为 0420

(对于上述 po :其为 Pod 类型的简写,且等价于 deploy.spec.template 或者 sts.spec.template 等。)

ConfigMap 类型的资源里:

  • metadata.name 的值为 haproxy-appdemo
  • data."haproxy.cfg" 的值为【一堆字符串】。

那么,上面的 Pod 类型的 po.spec.containers.args 中就可以去使用 /usr/local/etc/haproxy/haproxy.cfg 这一个文件了。(当然在这之后这文件也一直在)

上面我只是用我的方式说了说定义文件里关键的部分,可以通过 kubectl explain 来查看每个层级的说明。完整的定义,可以在成功执行 helm install haproxy-appdemo haproxytech/haproxy 从而完成安装(需要 Helm3 工具以及前置命令 helm repo add haproxytech https://haproxytech.github.io/helm-charts && helm repo update 的成功执行)后,自行对新添加的资源用各种方式查看。

这样就把一个具体的文件,通过资源定义的一部分内容来表达了。而且,一般的对 K8S 的界面化操作工具,对于 ConfigMap.data 里的已有的键都是可以直接编辑其值的。

至于那个 0420 其实是文件权限控制的数字(这里是八进制表示,所以务必不要丢掉那个 0 !否则就会被识别为十进制,那它就不是你想要有的效果了!),就是你用 chmod 的时候会用的数字(这个可以看 kubectl explain po.spec.volumes.configMap.defaultMode 的执行输出了解)。也就是说,到了 po.spec.volumes.configMap 这里,在相应 ConfigMap 资源里的内容才会被视为表示添加文件的配置

整镜像

大致三步:

  • 根据已有镜像创建并启动一个容器
  • 进入容器做一些操作(这个可以通过 dockerfile 合并到上一步)
  • 把新容器提交成新镜像

为啥要写这个?因为就算是 K8S 它终归也是用镜像创建容器的,没有镜像就没有容器。(就好比没有程序就没有进程实例。)

启动容器

一般用 docker run 来简写 docker createdocker container 等命令。

我的示例:

docker run -d -t -i -p 1234:4321 -p 5678:8765 -v /tmp/abc/data:/data -v /run/abc:/run -p 7788 --name playing-abc -- a.b/center/abc:v1 /usr/sbin/init

其中, rundocker 的一个子命令,可以看 docker run --help 的执行结果来了解各个选项作用。

选项参数里,先说 -- 前面(我的理解不一定对但是我会首先附上软件自己对自己的解释):

  • -d 帮助对这个的解释是: Run container in background and print container ID ,意思应该是让容器后台运行(应该是不当即进入的意思)并且打印容器的 ID 。(我的理解:如果不搞一个让后台运行着,前台用完了一退,容器里啥进程也没了的话,容器也就没了——容器里没进程容器就停这是 docker 的特性。简单想想,也还合理。)
  • -i 的解释: Keep STDIN open even if not attached ,意思应该是就算没人在里头用也保持标准输入打开。
  • -t 的解释: Allocate a pseudo-TTY ,意思应该是允许使用终端吧。
  • -p 的解释: Publish a container's port(s) to the host (default []) 。似乎可以传入一个列表,我上面的写法像是一个个往该列表添加元素。如果有冒号则左边表示裸机的右边表示容器的
  • -v 的解释: Bind mount a volume (default []) 。我这里用得是 Bind Mount ,就是直接绑定容器内外的目录,同样是冒号左边的表示容器外的,当然 -v 是另一个等价的更显式一些的写法的简写写法。
  • --name 的解释: Assign a name to the container ,用来给容器一个名字。不指定的话, Docker 会随机给个好像有什么含义的还挺有趣的名字。

上面这些选项与选项之间无所谓顺序(选项名和选项值还是要一前一后的)。

对于 -- 后面:

  1. 第一项是创建(并启动)容器所基于的镜像名。
  2. 第二项是启动命令——如果之后这个容器会被提交镜像,那么那个镜像的默认的 ENDPOINT (启动时的默认执行器)就会是这里这个。(不指定 ENDPOINT 的话它就会是 /bin/sh -c 了。)(如果想让容器像实际的机器一样被开启就可以用这个,不想的话可以把 /usr/sbin/init 换成 sh (或完整写可以是 /usr/bin/sh );应用容器一般是启动一个应用,比如 /usr/bin/env python xxx.py要注意这里当然就要是前台启动了不再是原来的后台启动了。)

对于 -- 本身:

  • 它可以不写,不过建议写出来,便于阅读。按理说,如果软件选项是遵照标准的 GNU 或 UNIX 风格来做的,那你不写,它应当也是程序先智能判断一下这玩意儿该写哪儿,然后给你补上,再执行。而且,就俩横线也不费事儿,所以其实没也必要不写,我是建议写的,能写它意味着这条命令的各个部分对你对看的人都将会是更清晰的。我觉得只有像 seq -- 4 这样的命令,其中的 -- 确实有必要省略掉。
一些小事……

其实 docker run 的选项会有 -t -i 是一直让我感到迷惑的,除非和 -d 一起用的时候要报错那我就不迷惑。在上面的例子中, -t -i 俩选项是可以没有的,这不会对我们的目的产生任何影响。

还有一件极其重要的事情:如果你的选项里有 --privileged=true 的话,这条 docker run 命令就必定会对主机产生影响!如果你的主机正在使用桌面,那么:

  • 如果 --privileged=true 选项配置和 -t 选项一起使用,那么在 docker run 命令执行后,你正在使用的桌面可能会被替换成容器内的终端!!(而且对此目前我这儿反正除了重启尚没有解决办法)
  • 如果 --privileged=true 选项配置有但没有 -t 选项,那么在 docker run 命令执行后,你正在使用的桌面会被退出……当然你可以权当刚刚开机一样然后重新登录就好了。。(这个的破坏性比上一个小但其实也不小)

(上述现象在 CentOS7 成功用提到的手段复现过。经查询,选项配置 --privileged=false 是默认的情况,这个时候,容器内 root 用户对于容器外就只是一个普通用户;如果配置位 --privileged=true 的话,容器内 root 就等同于容器外 root 了。)

如果默认从 Docker Hub 下镜像太慢的话,可以看看这个页面对你有没有用: https://mirrors.ustc.edu.cn/help/dockerhub.html

进入容器

docker exec

其实这里的 exec 的含义跟 Linux 的 exec 命令不是特别像,至少用起来不是。你可以 exec bash -c 'xxxx' 其中 xxxx 随便写些啥命令,来试一下效果,或许会有惊喜(所以请单独开一个 SHell 来试)。。。

示例:

docker exec -ti -- playing-abc bash

这里的 -t-i 跟上面的 docker run 的应该一样,也可能不一样,建议自己执行以下 docker exec --help 看一看。🙃

这里的 playing-abc 可以换成容器的 ID (长的短的都行;创建时输出信息里那个是长的,用 docker ps 看会看到短的),它必须是 -- 后的第一项。

而那个 bash 就是你要在这个容器里执行的命令,在它之后还可以再跟多个参数,即 容器名 之后的部分就都是命令及其参数了(这样一看就好像是后面的东西都是传给容器名的参数一般,如此反而有些能理解为何前面的子命令要叫 exec 了。不过这个不是本文重点,感兴趣可自行研究,当然也可以不感兴趣。。。🐛)。比如可以这样写试试效果:

docker exec -ti -- playing-abc bash -c hostname
docker exec -ti -- playing-abc hostname
docker exec -ti -- playing-abc python
docker exec -ti -- playing-abc iex xxx.iex

(其实仔细看,里面除了第三行,都不会进入被执行程序的界面,从而也就并不需要有那个 -ti 了。)

当然了,容器里可能会没有 bash 程序相关文件(很多精简镜像里没这个)(或者没有 python 程序),而是只有 shash 或只有前者。那么就在对应位置换一下就好了。

(概念辨析: 程序 指的是文件,可能是一个或者是一堆,是进程创建的基础,而 进程 就是一个正在运行中的东西(可能会被叫 实例 ),所谓 程序跑起来 其实就是 用这个程序创建进程实例(可能是一系列可能是一个) 的意思。这么看来,是不是能和 Docker 里的概念找到一一对应的关系?🙃🐌🐌)

退出,可以执行 exit 、也可以按 Ctrl-D 组合键。

另外的一些小事……

有的行为不必用 exec 子命令的,比如如果想查看容器内有什么资源, docker container top 子命令就现成的方案。比如: docker container top -- playing-abc 。这其实并不属于进入容器的范畴,因为容器内部没有办法确切感知到这种查询行为的存在,它并不会进入容器开启一个进程来完成这个功能;而且,理论上它的性能影响也是最小的。

可以执行这个查看帮助信息以或许更多你感兴趣的骚操作:

docker container --help

以及,不是所有基础镜像都有 /usr/sbin/init 这个东西。特别是被设计为专门用于构建应用镜像的基础镜像。

对于上面这种情况,其实应当意识到,我们完全可以不要让 docker run 被放到后台(就是说不用 -d 选项了)、然后再进去做什么,而是让要做的工作直接成为【入口点】。

而如果没有 -d 的话,执行过程中,你也会在这个命令里头,你的文本交互界面会被阻塞,并且你也可以看得到它的输出信息。可以用以下例子尝试:

docker run --name making--funnyalp -- docker.io/library/alpine:latest apk add bash luajit

这个例子是启动一个镜像为容器,然后里面只启动了一个进程(就是这个 apk add bash luajit 命令所启动的),它会在容器内安装 bashluajit 这俩程序,安装完后这个唯一的进程会退出,这个容器也就停止了。(不过没关系,停止了也可以提交。我们的目的就是提交,停了正好给计算机省事儿让它不必再多进行一个暂停容器的工作(不过为了确保提交时确实是停的所以指示暂停的选项在提交时还是会写)(提交的详细内容在后面说)。)

当然,如果你觉得下载太慢,一般而言可以这样:

docker run --name making--funnyalp -- docker.io/library/alpine:latest sh -c '
    sed -i.bak2 s/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g /etc/apk/repositories ;
    apk add bash luajit'

或者这样(分两步、镜像也会分两层):

docker run --name making--alpine-ustc -- docker.io/library/alpine:latest sed -i.bak2 s/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g /etc/apk/repositories &&
docker commit -p -a hm -m 'chg repo -> ustc' -- making--alpine-ustc hm.io/tools/alpine-ustc:latest &&
docker rm -- making--alpine-ustc ;

docker run --name making--funnyalp -- hm.io/tools/alpine-ustc:latest apk add bash luajit &&
docker commit -p -a hm -m 'add: bash luajit' -- making-funnyalp hm.io/tools/funnyalp:latest &&
docker rm -- making--funnyalp ;

# test:

docker run --rm --name testing--funnyalp -t -i -- hm.io/tools/funnyalp:latest bash ;

# and just Ctrl-D to exit bash 
# --- this container also will be exit if nothing run in it 
# --- and if container exited it will be remove automatically.

# because of we need to do sth in the bash manually,
# so we need the -t and -i options.

其实 -ti-d 可以放在一起确实是会让人迷惑的:都放后台了还怎么手动操作?大概其实是有的把,它在后台,但它也能接收标准输入,并且还占上了一个终端(只不过不是占用这容器所在机器的 root 用户的某个终端罢了,除非用上那个之前提到的一般不建议用的【会让容器内 root 用户会具有容器外 root 用户的权限】的选项配置。)

所以 -t -i 到底是什么?

其实你可以用这个 docker exec 命令试验一下:

(假设你已用 docker run -d --name c7 -- centos:7 /usr/sbin/init 启动了一个 systemd 为内部进程号 1 的容器)

echo aaa | docker exec -i -- c7 cat ## 会输出 aaa
echo aaa | docker exec -ti -- c7 cat ## 会报错 the input device is not a TTY
echo aaa | docker exec -- c7 cat ## 会阻塞你的控制台输入啥都不会有反应
cat ## 它会等你手动输入随便什么内容、并随时地给你吐出来相同的内容。按 `Ctrl-D` 可以结束输入
docker exec -ti -- c7 cat ## 它会等你手动输入随便什么内容、并随时地给你吐出来相同的内容。按 `Ctrl-D` 可以结束输入

一一解释一下。

首先,三条命令共同的一点就是:标准输入接收了字符串内容 aaa 的,是 docker 命令!

而不同则在于:

  • 第一行可以很好地用来理解标准输入打开的含义:命令 docker 会把自己的标准输入收到的东西视为后面那个容器里的命令(在此是 cat )的标准输入所收到的内容。

    (众所周知。。。 cat 命令就是把自己标准输入收到的内容原封不动吐给自己的标准输出)(还是众所周知。。。一般而言,管道最后的命令的标准输出对接的就是你的屏幕,输出造成的就是打印,默认如此,当然也可能会被重定向给文件从而本地储存(或者跑到别的任何什么地方)就是了。。。)

  • 第二行的话,虽然开了标准输入,但是是和终端一起开的,终端是什么呢?简单说就是你手动微操键盘的时候就是在用终端向命令的标准输入现输入一些什么东西了。这个报错的意思应该就是,它在抱怨,你说好了要微操,却不微操,而是直接给它一套写好了的内容,它觉得这不合理,就给你一个提醒并出于安全考虑而罢了工。。。

    因为如果你带了 -t 说明你真的想微操,你不微操,说明你自己哪里记错了,一步错就可能有步步错的危险,所以人家提醒你并且不干活其实是好心的。

  • 第三行,并没有 -i ,因此,命令 docker 会把它从自己的标准输入(众所周知每个进程的标准输入都是各自的虽然都叫 stdin 且都可以用 /dev/stdin 这个接口访问但真的是每个进程各自的)拿到的内容 aaa 不当回事儿(就是说不做任何处理——自然也就不会转发给命令后面指定的要在容器里启动的 cat 进程),因此容器里的 cat 命令(全一点说就是 cat 命令所启动起来的那个进程)也就不会感到自己的标准输入有被怼进来什么,那么,它就会一直等。

    为什么会一直等?结合第四行第五行解释。

  • 第四行第五行的效果是一样的,你可以随时输入内容,它随时会做出处理,在这里就是原样给你输出出来(前面说过 cat 程序启动的进程所做的处理无非是原样把它的标准输入的内容即时地原样吐给它的标准输出而默认标准输出是对接你的字符界面的),而你之所以能够手动操作,就是因为有终端这个东西存在。命令行是默认存在的,不然没法用了; docker 不一定有必要手动操作,所以需要选择。

    那么,第三行为啥会没反应呢?因为 cat 没有标准输入(即获取不到 aaa 这个内容)也没有终端(即不能手动操作),但它是按照有来启动的(这相当于在容器内执行第四行),所以它便要一直在这里等一个你无法给它的手动输入了。

对于上面的那些 docker exec 命令, docker run 其实也是同理的:

echo aaa | docker run --rm -i -- busybox cat ## 会输出 aaa
echo aaa | docker run --rm -ti -- busybox cat ## 会报错 the input device is not a TTY
echo aaa | docker run --rm -- busybox cat ## 等了一会儿,然后自动退出。
docker run --rm -ti -- busybox cat ## 它会等你手动输入随便什么内容、并随时地给你吐出来相同的内容。按 `Ctrl-D` 可以结束输入

这里无非是在 entrypoint 就执行这里的 cat 命令,也就是说,对于上面四行所建立的容器(由于有 --rm 所以它们还都是只要停了就被自动删掉的容器),在本案例中,它们运行的时候,里面只有 cat 命令启动的那个进程,这一个进程而已。不同的只是第三行,因为如果是 entrypoint 的位置,应当是它若在等终端就应当有终端的(此处它指 cat 所启动的那个进程),然而它却没有被给予终端的(因为没有 -t 就会默认不给),所以它会被杀死;杀死了它容器就没有进程了、按 Docker 的规定容器就要停止,又因为被 --rm 选项所标记因此这个容器停了就会被自动删除。

到这里可能会有人问: 你看啊你上面试了 -i -ti 和没有的情况,那要是只有 -t 呢?

你可以自己试试。效果会很合理:你可以输入,因为里面的进程接上了终端,允许被你随时微操;但你的输入不会被任何容器内的进程感知到,因为标准输入没打开,而 类 Unix 系统 的任何进程也就只有这几个获取信息输入的途径了: 传参标准输入 ,这里既然俩都没有,那可不是你怎么喊破喉咙人家也听不到的吗。。。(届不到,怎么想都届不到的。。)

不过!单独的 -t 也是可能会用到的,比如这种情况:

  • 你使用的是基础镜像,里头没有 /sbin/init 这种东西;
  • 那么你把 /sbin/init 换成 sh ,结果你却发现,启动的容器接着停了:

    那是当然!因为这个没有得到终端的 sh 也就不可能觉得自己有必要一直运行来等待人的手动输入,就会停止,容器中没了这唯一的进程容器也就停了

  • 你想让这个基础镜像一直不要停,但你无所谓 entrypoint 是啥,那么你可以让 sh 做这个 入口点 ,并给它 -t 却不给 -i 保证它持续等待输入却什么输入都得不到,如此,这个容器就可以一直运行一个什么都不会做的 sh 作为内部进程号 1 的进程而活着了。

    ——

    而当然,把 sh 换成 cat 也是一样。不信?让 cat 在有终端的模式下运行:我是说,直接执行一个 cat 命令,然后看看手动操作会有啥效果,你应该就会知道我是什么意思了。

    示例命令:

    docker run -t --name rust-playing -d -- rust:slim cat
    

    然后这个 rust-playing 就可以一直运行,并且容器内的 1 号进程就是这个一直在等待手动输入行为(却并不会等到什么因为没有 -i )的由 cat 命令所创建的进程了。

    如何判断里面就一个 cat ?执行这个: docker container top -- rust-playing ,如果容器内没有进程管理软件的话——应用构建的基础镜像确实是没必要有这种玩意儿的。

提交容器为镜像

(上面刚刚提到过并且示例了以下,看完下面可以再回看一次上面的小事。)

docker commit ,详细信息可执行 docker commit --help 查看。

示例——已知这时候我已经在 playing-abc 里做了一些操作,改了一些东西了:

docker commit -p -m 'add some fun, have some command history!' -- playing-abc e.f/given/efg:0.1

其中,选项 -p 表示先暂停容器再提交, -m 用来写这次提交的备注(有点像 git 的提交命令), -- 后,第一项应该是 容器名容器 ID (可长可短) ,第二项是你即将创建的新镜像的全名,可以是几乎全新的名字就像这里的 e.f/given/efg:0.1 ,也可以是之前镜像变一下 TAG 部分比如: a.b/center/abc:v2-dev

对于镜像是可以用 docker tag 改名的, docker tag -- e.f/given/efg:0.1 a.b/center/abc:v2-dev 会为 e.f/given/efg:0.1 这个镜像创建新的别名 a.b/center/abc:v2-dev原来的全名其实也是别名而且两个别名地位平等,二者都只是同一份存在你硬盘上的某一堆文件的 镜像名 ,增加这个名称也不会导致存储被更多占用,你只是可以用更多的称呼去找到同一个镜像了而已。

导出镜像

就是把镜像存成离线包。

docker save 。建议详细看一下 docker save --help 的执行输出。

示例:

mkdir -p -- "$(dirname e.f/given/efg:0.1)" &&

docker save -- e.f/given/efg:0.1 |
    xz -T0 --best > e.f/given/efg:0.1.tar.xz ;

这是我用的命令,其中格式化去掉也能执行。

这个是根据指定镜像名保存镜像,到时候将文件内容丢给 docker load 这个命令的标准输入就可以得到一个叫这个名字的镜像。

写镜像名 e.f/given/efg:0.1 的部位也可以换成镜像的 ID ,但这样导入的时候就不会带有这个名字了。

导入的时候自动生成什么镜像名,就说明导出的时候是用这个镜像的哪个别名导出的。

导入导出相关子命令的默认的输入或输出分别是标准输入和标准输出,导出的格式一定是 TAR 格式,是没经过压缩的,你可以把这个输出丢给管道从而自行选择用啥压缩命令压缩它,我这用的是 xz 命令,开启动态并发度的并发模式并且指定了最高压缩率,压缩结果依然是被放入标准输出的(就像 docker save 那样),所以我给它重定向到一个文件里。这里重定向到对于当前目录来说的 e.f/given/efg:0.1.tar.xz 这个文件里,所以需要先创建好它会需要的目录才行,用 mkdir -p -- "$(dirname e.f/given/efg:0.1)" 。那个 && 表示左边的成功才执行右边的。(右边没东西?右边是回车啊!回车空格都是空白符而且我这儿回车换空格然后连续空格换成一个空格,命令还是和原来完全一样的。我指的右边就是它右边的命令(不是字符)。)

导入示例:

cat -- e.f/given/efg:0.1.tar.xz | docker load

这样就是把 e.f/given/efg:0.1.tar.xz内容丢给 docker load标准输入 里去。

只要是用 docker save 导出生成的,上面的命令一般不会执行错误。成功时会有几行输出信息。


awsr
13 声望0 粉丝

« 上一篇
SHell 与远方
下一篇 »
Bash 笔记