Docker&Kubernetes学习

Docker

Docker的概念

Docker 是 一个打包、 分发和运行应用程序的平台。 它允许将你 的应用程序和应用程序所依赖的整个环境打包在一起。 这既可以是一些应用程序需 要的库, 也可以是一个被安装的操作系统所有可用的文件。

  • 镜像:Docker镜像里包含了你打包的应用程序及其所依赖的环境,它包含应用程序可用的文件系统和其他元数据,如镜像运行时的可执行文件路径。
  • 镜像仓库:Docker镜像仓库用于存放Docker镜像,以及促进不同人和不同电脑之间共享这些镜像。
  • 容器:Docker容器通常是一个Linux容器,它基于Docker镜像被创建,一个运行中的容器是一个运行在Docker主机上的进程,但它和主机,以及所有运行在主机上的其他进程都是隔离的,这些进程也是资源受限的,意味着它只能访问和使用分配给它的资源。
容器隔离机制
  • Linux命名空间,它使每个进程只看到自己的系统视图(文件、进程、网络接口、主机名等)
  • Linux控制组(cgroups):它限制了进程能使用的资源量(CPU、内存、网络带宽等)
Docker核心组件
  • Docker客户端和服务器,也称为Docker引擎
  • Docker镜像
  • Registry
  • Docker容器
Docker技术组件
  • 一个原生的Linux容器格式,Docker中称为libcontainer
  • Linux内核的命名空间(namespace),用于隔离文件系统、进程和网络
  • 文件系统隔离:每个容器都有自己的root文件系统
  • 进程隔离:每个容器都运行在自己的进程环境中
  • 网络隔离:容器间的虚拟网络接口和IP地址都是分开的
  • 资源隔离和分组:使用cgroups将CPU和内存之类的资源独立分配给每个Docker容器
  • 写时复制:文件系统都是通过写时复制创建的,这就意味着文件系统时分层的、快速的、而且占用的磁盘空间更小
  • 日志:容器产生的STDOUT、STDERR和STDIN这些IO流都会被收集并计入日志,用来进行日志分析和故障拍错
  • 交互式shell:用户可以创建一个伪tty终端,将其连接到STDIN,为容器提供一个交互式的shell
构建、分发和运行Docker镜像

1

Docker Hello-world
1
2
3
docker search "hello-world"	#搜索一个名为 hello-world的镜像
docker pull hello-world #拉取镜像
docker run hello-world #运行镜像
Docker搭建应用

应用包含一个名为app.js的文件,代码如下

1
2
3
4
5
6
7
8
9
10
const http =  require ('http');
const os = require ('os');
console.log ("Kubia server starting ... ");
var handler =function(request, response){
console.log ("Received request from " + request.connection.remoteAddress);
response.writeHead(200);
response.end("You've hit " + os.hostname() + "\n");
};
var www = http.createServer(handler);
www.listen(8080);

为镜像创建Dockerfile,在app.js同目录下,创建一个Dockerfile文件,内容如下

1
2
3
FROM node:7 #构建所基于的基础镜像
ADD app.js /app.js #把app.js文件从本地文件夹添加到镜像的根目录,保持app.js这个文件名
ENTRYPOINT ["node","app.js"] #当镜像被运行时所需要被执行的命令

构建容器镜像,以下命令告诉Docker需要基于当前目录构建一个叫kubia的镜像

1
docker build -t kubia .

5

基于Dockerfile构建一个新的容器镜像具体流程如下图所示

6

运行容器镜像,下面这条命令告知Docker基于kubia镜像创建一个叫kubia- container的新容器,这个容器与命令行分离,意味着在后台运行,本机上的8080端口会被映射到容器内的8080端口,所以可以通过localhost:8080来访问这个应用,

1
docker run --name kubia-container -p 8080:8080 -d kubia

7

访问应用,发现应用把934fe4c0ca9c作为主机名返回,这个十六进制数就是Docker容器的ID

1
2
$ curl localhost:8080
You've hit 934fe4c0ca9c

假如说这个时候我们删除了一个容器id想要恢复的话可以使用docker start命令

9

在已有的容器内部运行shell

这条命令会在已有的kubia-container容器内部运行bash,bash进行会和主容器进程拥有相同的命名空间,这样可以从内部探索容器。

  • -i,确保标准输入流保待开放。需要在 shell 中输入命令。
  • -t,分配 一 个伪终端(TTY)。
1
docker exec -it kubia-container bash

8

停止容器

1
docker stop kubia-container

删除容器

1
docker rm kubia-container

向镜像仓库推送镜像

1
2
docker tag kubia xxx/kubia #xxx是自己的Docker Hub ID
docker push xxx/kubia #向Docker Hub推送镜像

在不同机器上运行镜像

1
docker run -p 8080:8080 -d xxx/kubia
Docker持续集成

为了演示Docker持续集成的能力,我们将使用Jenkins CI构建一个测试流水线,首先会构建一个运行Docker的Jenkins服务器,一旦Jenkins运行起来,将展示最基础的单容器测试运行,最后将展示多容器的测试场景。

Jenkins教程:https://www.jenkins.io/zh/doc/

构建Jenkins和Docker服务器

第一步先准备一个文件夹

1
mkdir jenkins && cd jenkins

在jenkins目录中,制作Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM jenkins/jenkins:lts

USER root
RUN apt-get -qq update && apt-get install -qq sudo
RUN apt-get install wget
RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers
RUN wget http://get.docker.com/builds/Linux/x86_64/docker-latest.tgz
RUN tar -xvzf docker-latest.tgz
RUN mv docker/* /usr/bin/

USER jenkins
RUN /usr/local/bin/install-plugins.sh junit git git-client ssh-slaves greenballs chucknorris ws-cleanup

构建镜像

我起的镜像名为elssm/dockerjenkins,大家可以随意起名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
caoyifan@MacBookPro jenkins % docker build -t elssm/dockerjenkins .
[+] Building 99.9s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 516B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/jenkins/jenkins:lts 1.1s
=> [1/8] FROM docker.io/jenkins/jenkins:lts@sha256:4b17ea5e222f5fbfcf8d7 0.0s
=> CACHED [2/8] RUN apt-get -qq update && apt-get install -qq sudo 0.0s
=> [3/8] RUN apt-get install wget 4.7s
=> [4/8] RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers 0.3s
=> [5/8] RUN wget http://get.docker.com/builds/Linux/x86_64/docker-late 39.6s
=> [6/8] RUN tar -xvzf docker-latest.tgz 1.3s
=> [7/8] RUN mv docker/* /usr/bin/ 0.6s
=> [8/8] RUN /usr/local/bin/install-plugins.sh junit git git-client ssh 51.3s
=> exporting to image 0.9s
=> => exporting layers 0.8s
=> => writing image sha256:92f0b567b6888b0d0f3b306b76a42bdda971dba8f2017 0.0s
=> => naming to docker.io/elssm/dockerjenkins 0.0s

构建好镜像之后启动镜像创建容器

1
docker run -p 8080:8080 --name jenkins --privileged -d elssm/dockerjenkins

其中--privileged标志可以启动Docker的特权模式,这种模式允许我们以其宿主机的所有能力来运行容器,包括一些内核特性和设备访问。

这样容器jenkins就启动了,我们可以看一下启动后的日志

1
2
3
4
5
6
7
8
9
10
11
12
13
caoyifan@MacBookPro jenkins % docker logs jenkins
Running from: /usr/share/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
2021-10-09 05:45:14.310+0000 [id=1] INFO org.eclipse.jetty.util.log.Log#initialized: Logging initialized @854ms to org.eclipse.jetty.util.log.JavaUtilLog
2021-10-09 05:45:14.447+0000 [id=1] INFO winstone.Logger#logInternal: Beginning extraction from war file
2021-10-09 05:45:15.584+0000 [id=1] WARNING o.e.j.s.handler.ContextHandler#setContextPath: Empty contextPath
2021-10-09 05:45:15.700+0000 [id=1] INFO org.eclipse.jetty.server.Server#doStart: jetty-9.4.43.v20210629; built: 2021-06-30T11:07:22.254Z; git: 526006ecfa3af7f1a27ef3a288e2bef7ea9dd7e8; jvm 11.0.12+7
2021-10-09 05:45:16.102+0000 [id=1] INFO o.e.j.w.StandardDescriptorProcessor#visitServlet: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet
2021-10-09 05:45:16.175+0000 [id=1] INFO o.e.j.s.s.DefaultSessionIdManager#doStart: DefaultSessionIdManager workerName=node0
2021-10-09 05:45:16.175+0000 [id=1] INFO o.e.j.s.s.DefaultSessionIdManager#doStart: No SessionScavenger set, using defaults
2021-10-09 05:45:16.177+0000 [id=1] INFO o.e.j.server.session.HouseKeeper#startScavenging: node0 Scavenging every 660000ms
2021-10-09 05:45:16.938+0000 [id=1] INFO hudson.WebAppMain#contextInitialized: Jenkins home directory: /var/jenkins_home found at: EnvVars.masterEnvVars.get("JENKINS_HOME")
...

现在我们就可以通过本地的8080端口在浏览器中访问了,我这边打开之后发现是锁定的状态(大家的情况应该和我一样),如下图所示。因此需要输入密码解锁,密码路径已经告诉了我们。

19

进入jenkins容器

1
docker exec -it jenkins bash

获取密码

1
cat var/jenkins_home/secrets/initialAdminPassword

进行一部分简单的设置之后我们就可以进入Jenkins的登陆页面了,输入用户名和密码,如果是默认的话用户名就是admin,密码就是上面cat拿到的密码。输入之后就进入主界面了,如下图所示。

20

创建新的Jenkins作业

现在Jenkins服务器已经运行,我们可以来创建一个Jenkins作业。具体操作如下

21

将新作业命名为Docker_test_job,作业类型为Freestyle project

22

接下来填写作业描述,在高级项目选项中选择Use Custom workspace,并指定/tmp/jenkins-buildenv/${JOB_NAME}/workspace作为目录。

23

Source Code Management区域,选择Git并指定测试仓库为https://github.com/turnbullpress/docker-jenkins-sample.git,该仓库包含了一些基于Ruby的RSpec测试

24

接下来下拉,找到Build选项,单击Add Build Step按钮增加一个构建的步骤,选择Execute shell,如下图所示。

25

之后使用定义的脚本来启动和测试Docker,该脚本代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Build the image to be used for this run.
IMAGE=$(docker build . | tail -1 | awk '{ print $NF }')

# Build the directory to be mounted into Docker.

MNT="$WORKSPACE/.."

sleep 3
cd /tmp/jenkins-buildenv/Docker_test_job/workspace/spec/test-report
touch a.xml

# Execute the build inside Docker.
CONTAINER=$(docker run -d -v "$MNT:/opt/project" $IMAGE /bin/bash -c "cd /opt/project/workspace && rake spec")

# Attach to the container's streams so that we can see the output.
sudo docker attach $CONTAINER

# As soon as the process exits, get its return value.
RC=$(sudo docker wait $CONTAINER)

# Delete the container we've just used to free disk space.
sudo docker rm $CONTAINER

# Exit with the same value that the process exited with.
exit $RC

那么上面的脚本具体做了些什么呢?首先它将使用包含刚刚指定的Git仓库的Dockerfile创建一个新的Docker镜像,这个Dockerfile提供了想要执行的测试环境。该Dockerfile代码如下

1
2
3
4
5
6
FROM ubuntu:16.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2016-06-01
RUN apt-get update
RUN apt-get -y install ruby rake
RUN gem install --no-rdoc --no-ri rspec ci_reporter_rspec

可以看到,Dockerfile构建了一个Ubuntu宿主机,安装了Ruby和RubyGems,之后安装了两个gem:rspec和ci_reporter_rspec。这样构建的镜像可以用于测试典型的基于Ruby且使用RSpec测试框架的应用程序。ci_reporter_rspec gem会把RSpec的输出转换为JUnit格式的XML输出,并交给Jenkins做解析。

回到之前的脚本,构建镜像之后,会创建一个包含Jenkins的工作空间的目录,会把这个目录挂载到Docker容器,并在这个目录里执行测试。然后我们从这个惊喜那个创建了容器,并且运行了测试,在容器里,把工作空间挂载到/opt/project目录,之后执行命令切换到这个目录,并执行rake spec来运行RSpec测试。容器启动之后,我们拿到了容器的ID。

现在使用docker attach命令进入容器,得到容器执行时输出的内容,然后使用docker wait命令,docker wait命令会一直阻塞,直到容器里的命令执行完成才会返回容器退出时的返回码,变量RC捕捉到容器退出时的返回码,最后,清理环境,删除刚刚创建的容器,并使用容器的返回码退出,这个返回码应该就是测试执行结果的返回码,Jenkins依赖这个返回码得知作业的测试结果时成功还是失败。对上面脚本的解释就到这里,让我们再回到Jenkins构建作业的步骤!!!

接下来,单机Add post-build action,加入一个Publish JUint test result report的动作,在Test report XMLs域,需要指定spec/reports/*.xml,这个目录是ci_reporter gem的XML输出的位置,找到这个目录会让jenkins处理测试的历史结果和输出结果。

26

最后点击保存即可。

运行Jenkins作业

点击Build Now即可。

遇到的问题

好家伙!我以为点击完Build Now之后就会顺利的看到结果,万万没想到出现了各种各样的错误

报错如下

1
2
ERROR: Step ‘Publish JUnit test result report’ failed: Test reports were found but none of them are new. Did leafNodes run? 
For example, /tmp/jenkins-buildenv/Docker_test_job/workspace/spec/test-report/*.xml is 4 min 43 sec old

修改如下

在执行shell命令中添加如下代码,目的是不断更新这个文件的时间

1
2
3
sleep 3
cd xml报告的路径
touch a.xml

报错如下

1
Cannot connect to the Docker daemon at unix:/var/run/docker.sock. Is the docker daemon running?

修改如下

1
sudo dockerd

报错如下

1
gnutls_handshake() failed: The TLS connection was non-properly terminated.

修改如下

1
2
git config --global  --unset https.https://github.com.proxy 
git config --global --unset http.https://github.com.proxy

报错如下

1
[Checks API] No suitable checks publisher found

修改如下

暂时没有找到修改方法。。。心态崩了

如下图是我Build的心路历程,到最后也没有看到那一抹绿!!!

27

最终我的执行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Started by user admin
Running as SYSTEM
Building in workspace /tmp/jenkins-buildenv/Docker_test_job/workspace
The recommended git tool is: NONE
No credentials specified
> git rev-parse --resolve-git-dir /tmp/jenkins-buildenv/Docker_test_job/workspace/.git # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url https://github.com/turnbullpress/docker-jenkins-sample.git # timeout=10
Fetching upstream changes from https://github.com/turnbullpress/docker-jenkins-sample.git
> git --version # timeout=10
> git --version # 'git version 2.30.2'
> git fetch --tags --force --progress -- https://github.com/turnbullpress/docker-jenkins-sample.git +refs/heads/*:refs/remotes/origin/* # timeout=10
> git rev-parse refs/remotes/origin/master^{commit} # timeout=10
Checking out Revision 7d027a4cdeee1b5da4f9d65d64bceb3692d4d571 (refs/remotes/origin/master)
> git config core.sparsecheckout # timeout=10
> git checkout -f 7d027a4cdeee1b5da4f9d65d64bceb3692d4d571 # timeout=10
Commit message: "Update Dockerfile"
> git rev-list --no-walk 7d027a4cdeee1b5da4f9d65d64bceb3692d4d571 # timeout=10
[workspace] $ /bin/sh -xe /tmp/jenkins4474819640520934216.sh
+ sudo docker build .
+ tail -1
+ awk { print $NF }
+ IMAGE=d9be83fd0aa7
+ MNT=/tmp/jenkins-buildenv/Docker_test_job/workspace/..
+ sleep 3
+ cd /tmp/jenkins-buildenv/Docker_test_job/workspace/spec/test-report
+ touch a.xml
+ docker run -d -v /tmp/jenkins-buildenv/Docker_test_job/workspace/..:/opt/project d9be83fd0aa7 /bin/bash -c cd /opt/project/workspace && rake spec
+ CONTAINER=fcc26e706d258f6f3ab93f2fcf7ba374a4d60d38ae065c7fb28dcb65eaf97d2e
+ sudo docker attach fcc26e706d258f6f3ab93f2fcf7ba374a4d60d38ae065c7fb28dcb65eaf97d2e
rm -rf spec/reports
/usr/bin/ruby2.3 -I/var/lib/gems/2.3.0/gems/rspec-support-3.10.2/lib:/var/lib/gems/2.3.0/gems/rspec-core-3.10.1/lib /var/lib/gems/2.3.0/gems/rspec-core-3.10.1/exe/rspec --pattern spec/\*_spec.rb --colour --format progress
.....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

Finished in 1.13 seconds (files took 0.28521 seconds to load)
501 examples, 0 failures

+ sudo docker wait fcc26e706d258f6f3ab93f2fcf7ba374a4d60d38ae065c7fb28dcb65eaf97d2e
+ RC=0
+ sudo docker rm fcc26e706d258f6f3ab93f2fcf7ba374a4d60d38ae065c7fb28dcb65eaf97d2e
fcc26e706d258f6f3ab93f2fcf7ba374a4d60d38ae065c7fb28dcb65eaf97d2e
+ exit 0
Recording test results
[Checks API] No suitable checks publisher found.
Build step 'Publish JUnit test result report' changed build result to UNSTABLE
Finished: UNSTABLE
Docker进阶
  • Docker容器数据卷

    需求:容器的持久化和同步操作,容器间也是可以数据共享的

    容器之间可以有一个数据共享的技术,Docker容器中产生的数据同步到本地

    将我们容器内的目录,挂载到linux上面

  • 具名和匿名挂载

    匿名:在-v只写了容器内的路径,没有写容器外的路径

    具名:通过-v 卷名:容器内路径

    指定路径挂载:-v /宿主机路径:容器内路径

  • DockerFile

    用来构建docker镜像的构建文件,命令脚本

    1
    2
    3
    4
    5
    6
    7
    8
    #创建dockerfile文件

    FROM centos

    VOLUME ["volume01","volume02"]

    CMD echo "----end----"
    CMD /bin/bash
    1
    2
    #构建称为一个镜像
    docker build -f dockerfile -t elssm/centos .
    1
    2
    #运行镜像
    docker run
    1
    2
    #发布镜像
    docker push

    DockerFile:构建文件,定义了一切的步骤,源代码

    DockerImages:通过DockerFile构建生成的镜像,最终发布和运行

    Docker容器:容器就是镜像运行起来提供服务的

  • DockerFile的指令解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    FROM #基础镜像,一切从这里开始构建
    MAINTAINER #镜像是谁写的,姓名+邮箱
    RUN #镜像构建的时候需要运行的命令
    ADD #步骤,添加内容
    WORKDIR #镜像的工作目录
    VOLUME #挂载的目录
    EXPOSE #指定暴露端口
    CMD #指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
    ENTRYPOINT #指定这个容器启动的时候要运行的命令,可以追加命令
    ONBUILD #当构建一个被继承DockerFile这个时候就会运行ONBUILD的指令
    COPY:#类似ADD,将我们文件拷贝到镜像中
    ENV #构建的时候设置环境变量
  • 创建一个自己的centos

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #编写Dockerfile的文件
    FROM centos

    MAINTAINER elssm<329847986@qq.com>

    ENV MYPATH /usr/local
    WORKDIR $MYPATH

    RUN yum -y install vim
    RUN yum -y install net-tools

    EXPOSE 80

    CMD echo $MYPATH
    CMD echo "---end---"
    CMD /bin/bash

    #通过这个文件构建镜像
    docker build -f dockerfile -t name:0.1 .

    #测试运行
    docker run -it image
  • 实战Tomcat镜像

    • 准备镜像文件tomcat压缩包,jdk的压缩包

    • 编写dockerfile文件

  • 数据卷容器

    1
    2
    #容器间数据同步
    -- volumes -from
  • Docker网络

    原理:我们每启动一个docker容器,docker就会给docker容器分配一个ip,我们只要安装了docker,就会有一个网卡docker0,桥接模式,使用的技术是veth-pair技术

  • veth-pair:就是一对虚拟设备接口,他们都是成对出现的,一端连着协议,一端彼此相连,正因为有这个特性,veth-pair充当一个桥梁,连接各种虚拟 网络设备

    1
    2
    3
    4
    5
    #查看docker下的所有network
    docker network ls

    #查看详细信息
    docker network inspect bridge_ID

    --link可以解决docker下网络联通问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #下载tomcat镜像
    docker pull tomcat

    #docker下启动两个tomcat
    docker run -d -P --name tomcat01 tomcat
    docker run -d -P --name tomcat02 tomcat

    #启动tomcat03并绑定tomcat02
    docker run -d -P --name tomcat03 --link tomcat02 tomcat

    #进入tomcat03并查看tomcat03配置文件
    docker exec -it tomcat03 cat /etc/hosts
  • 自定义网络

    查看所有的docker网络

    1
    docker network ls

    网络模式

    • bridge:桥接模式
    • none:不配置网络
    • host:和宿主机共享网络
    • container:容器内网络连通
    1
    2
    3
    4
    5
    6
    7
    8
    #我们可以自定义一个网络
    # --driver bridge
    # --subnet 192.168.0.0/16
    # --gateway 192.168.0.1
    docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet

    #查看自己创建的网络
    docker network inspect mynet

    自定义的网络docker都已经帮我们维护好了对应的关系

    不同的集群使用不同的网络,保证集群是安全和健康的

  • 网络连通

    1
    2
    3
    4
    5
    #测试打通tomcat01 - mynet
    docker network connect mynet tomcat01

    #连通之后就是将tomcat01放到了mynet网络下
    #一个容器两个ip地址
Docker构建应用

第一个应用是使用Jekyll框架的自定义网站,我们会构建两个镜像

  • 一个镜像安装了Jekyll及其他用于构建Jekyll网站的必要的软件
  • 一个镜像通过Apache来让Jekyll网站工作起来

首先构建Jekyll基础镜像,本地创建Jekyll工作目录。

1
mkdir jekyll && cd jekyll

Dockerfile文件如下。镜像基于Ubuntu18.04,并且安装了Ruby和用于支持Jekyll的包,然后使用VOLUME指定创建了两个卷。将工作目录设置到/data/,并通过ENTRYPOINT指定自动构建的命令。

1
2
3
4
5
6
7
8
9
10
11
FROM ubuntu:18.04

RUN apt-get -qq update
RUN apt-get -qq install ruby ruby-dev libffi-dev build-essential nodejs
RUN gem install --no-rdoc --no-ri jekyll -v 2.5.3

VOLUME /data
VOLUME /var/www/html
WORKDIR /data

ENTRYPOINT [ "jekyll", "build", "--destination=/var/www/html" ]

构建Jekyll基础镜像

1
docker build -t elssm/jekyll .

接下来构建第二个镜像,一个用来架构新网站的Apache服务器,本地创建Apache工作目录。

1
mkdir apache && cd apache

Dockerfile文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM ubuntu:18.04

RUN apt-get -qq update
RUN apt-get -qq install apache2

VOLUME [ "/var/www/html" ]
WORKDIR /var/www/html

ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2

RUN mkdir -p $APACHE_RUN_DIR $APACHE_LOCK_DIR $APACHE_LOG_DIR

EXPOSE 80

ENTRYPOINT [ "/usr/sbin/apachectl" ]
CMD ["-D", "FOREGROUND"]

构建Jekyll Apache镜像

1
docker build -t elssm/apache .

启动Jekyll网站

启动之前先在本地准备一些博客的源代码,代码地址为https://github.com/turnbullpress/james_blog

我将源代码放在我的用户目录下,源代码重命名为elssm_blog,下载好之后执行下面命令,这段命令启动了一个叫做elssm_blog的新容器,把我本地的elssm_blog目录作为/data/卷挂载到容器里,容器已经拿到网站的源代码,并将其构建到已编译的网站,存放到/var/www/html/目录。

1
2
3
4
5
6
caoyifan@MacBookPro ~ % docker run -v ~/elssm_blog:/data/ --name elssm_blog elssm/jekyll
Configuration file: /data/_config.yml
Source: /data
Destination: /var/www/html
Generating...
done.

如果想在另一个容器里使用/var/www/html/卷里编译好的网站,可以创建一个新的链接到这个卷的容器,如下命令。其中--volumes-from标志把指定容器里的所有卷都加入新创建的容器里,这意味着Apache容器可以让问之前创建的elssm_blog容器里/var/www/html卷中存放的编译好的Jekyll网站,即便elssm_blog容器没有运行,Apache容器也可以访问这个卷,前提是容器本身必须存在。

1
docker run -d -P --volumes-from elssm_blog elssm/apache

接下来我们查看一下容器把80端口映射到本本地哪个端口

1
2
caoyifan@MacBookPro ~ % docker port 4d91e0f44a9a 80
0.0.0.0:55000

通过打开localhost:55000浏览该网站。

28

更新Jekyll网站

我们试着修改本地源代码中的_config.yml文件,将title域改为ELSSM's Blog

接着更新网站

1
docker start elssm_blog

查看一下容器的日志,发现Jekyll编译过程第二次被运行,并且网站已经被更新。

1
2
3
4
5
6
7
8
9
10
11
12
caoyifan@MacBookPro elssm_blog % docker logs elssm_blog
Configuration file: /data/_config.yml
Source: /data
Destination: /var/www/html
Generating...
done.
Auto-regeneration: disabled. Use --watch to enable.
Configuration file: /data/_config.yml
Source: /data
Destination: /var/www/html
Generating...
done.

再次刷新查看网站。由于共享的卷会自动更新,这一切都不需要更新或者重启Apache容器。

29

Kubernetes

发展历程
  • Infrastructure as a Service(阿里云)

  • Platform as a service(新浪云)

  • Software as a Service(Office 365)

资源管理器
  • Apache MESOS
  • docker SWARM
  • Kubernetes
    • 轻量级
    • 开源
    • 弹性伸缩
    • 负载均衡
Kubernetes简介

Kubernetes是一个软件系统,它允许你在其上很容易地部署和管理容器化的应用。 它依赖于Linux容器的特性来运行异构应用, 而无须知道这些应用的内部详情, 也不需要手动将这些应用部署到每台机器。

Kubernetes 使你在数以千计的电脑节点上运行软件时就像所有这些节点是单个大节点一样。它将底层基础设施抽象,这样做同时简化了应用的开发、部署,以及对开发和运维团队的管理。

Kubernetes核心功能

如下图为一个最简单的Kubernetes系统图。整个系统由一个主节点和若干个工作节点组成 。 开发者把一个应用列表提交到主节点, Kubemetes 会将它们部署到集群的工作节点。组件被部署在哪个节点对于开发者和系统管理员来说都不用关心。除此之外,开发者还能指定一些应用必须一起运行,这样Kubernetes将会在一个工作节点上部署它们,而其他的将被分散部署到集群中。不管怎样部署,它们都能以相同的方式互相通信。

2

Kubernetes集群架构

在硬件级别,一个Kubernetes集群由很多节点组成,这些节点被分成以下两种类型

  • 主节点:它承载着Kubernetes控制和管理整个集群系统的控制面板
    • Kubernates API服务器:其他控制面板组件都要和它进行通信
    • Scheculer:调度你的应用(为应用的每个可部署组件分配一个工作节点)
    • Controller Manager:执行集群级别的功能,如复制组件、持续跟踪工作节点、处理节点失败等
    • ETCD:一个可靠的分布式数据存储,能持久化存储集群配置
  • 工作节点:它们运行用户实际部署的应用
    • Docker、rtk或其他容器类型
    • Kubelet:与API服务器通信,并管理它所在节点的容器
    • Kubernetes Service Proxy:负责组建之间的负载均衡网络流量

3

Kubernetes运行应用

4

如上图所示,在应用描述符中列出了四个容器,并将这些容器分成了3组,这些集合被称为pod,其中前两个pod只包含了一个容器,最后一个包含两个,意味着这两个容器都需要协作运行,每个pod旁边的数字表示需要并行运行的每个pod的副本数量,在向 Kubernetes 提交描述符之后,它将 把每个 pod的指定副本数量调度到可用的工作节点上。 节点上的 Kubelets将告知 Docker 从镜像仓库中拉取 容器镜像井运 行容器 。

安装kubernetes

这里我使用的是Docker for Mac,具体安装方法可以参考下面链接

https://developer.aliyun.com/article/508460

安装完成之后验证Kubernetes是否安装成功

10

Kubernetes的优势
  • 简化应用程序部署

  • 更好的利用硬件

  • 健康检查和自修复

  • 自动扩容
Kubernetes组件说明
  • APISERVER:所有服务访问统一接口
  • ControllerManager:维护副本期望数据
  • Scheduler:负责介绍任务,选择合适的节点进行分配任务
  • ETCD:键值对数据库,存储K8S集群所有重要信息(持久化)
  • Kubelet:直接跟容器引擎交互实现容器的生命周期管理
  • Kube-proxy:负责写入规则至IPTABLES,IPVS实现服务映射访问
  • COREDNS:可以为集群中的SVC创建一个域名IP的对应关系解析
  • DASHBOARD:给K8S集群提供一个B/S结构访问体系
  • INGRESS CONTROLLER:官方只能实现四层代理,INGRESS可以实现七层代理
  • FEDERATION:提供一个可以跨集群中心多K8S统一管理功能
  • PROMETHEUS:提供K8S集群的监控能力
  • ELK:提供K8S集群日志统一分析接入平台
pod的介绍

一个pod是一组紧密相关的容器,它们总是一起运行在同一个工作节点上,以 及同一个 Linux 命名空间中。每个 pod就像一个独立的逻辑机器,拥有自己的 IP、 主机名、进程等,运行一个独立的应用程序 。应用程序可以是单个进程,运行在单个容器中,也可以是一个主应用进程或者其他支持进程,每个进程都在自己的容器中运行 。一 个pod的所有容器都运行在同一个逻辑机器上,而其他 pod 中的容器, 即使运行在同一个工作节点上,也会出现在不同的节点上 。

如下图所示是容器、pod以及物理工作节点之间的关系。从图上我们可以看出,每个pod都有自己的IP,并包含一个或多个容器,每个容器都运行一个应用进程,pod分布在不同的工作节点上。

11

部署一个Node.js应用,可以使用kubectl run命令,该命令可以创建所有必要的组件而无需 ISON 或YAML文件,这样的话,我们就不需要深入了解每 个组件对象的结构。

1
2
caoyifan@MacBookPro ~ % kubectl run kubia --image=luksa/kubia --port=8080
pod/kubia created
  • —image=luksa/kubia显示的是指定要运行的容器镜像
  • --port=8080选项告诉Kubernetes应用正在监昕 8080端口
列出pod
1
2
3
caoyifan@MacBookPro ~ % kubectl get pods
NAME READY STATUS RESTARTS AGE
kubia 1/1 Running 0 8s

在Kubernetes中运行容器镜像所必需的步骤

  • 构建镜像并将其推送到Docker Hub
  • 当运行kubectl命令时,它通过向 Kubernetes API服务器发送一个REST HTTP请求,在集群中创建一个新的 ReplicationController对象 。 然后,ReplicationController创建了一个新的pod,调度器将其调度到一个工作节点上。Kubelet看到pod被调度到节点上,就告知 Docker 从镜像中心中拉取指定的镜像,因为本地没有该镜像。下载镜像后,Docker创建并运行容器。
创建一个服务对象
1
2
caoyifan@MacBookPro ~ % kubectl expose pod kubia --type=LoadBalancer --name kubia-http
service/kubia-http exposed
列出服务
1
2
3
4
caoyifan@MacBookPro ~ % kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 41m
kubia-http LoadBalancer 10.110.166.212 localhost 8080:32054/TCP 14s

这里我们会发现应用将pod名称作为它的主机名。因为每个pod都像一个独立的机器,具有自己的IP地址和主机名,尽管应用程序运行在工作节点的操作系统中,但对应用程序来说,它似乎是在一个独立的机器上运行,而这台机器本身就是应用程序的专用机器,没有其他的进程一同运行。

访问本地8080端口向pod发送请求

1
2
caoyifan@MacBookPro ~ % curl localhost:8080
You've hit kubia
Pod概念
  • 自主式Pod

  • 控制器管理的Pod

    ReplicationController:用来确保容器应用的副本数始终保持在用户定义的副本数,即如果有容器异常退出,会自动创建新的Pod来替代,而如果异常多出来的容器也会自动回收。

    ReplicaSet:和ReplicationController没有本质不同,支持集合式的selector

    Deployment:自动管理ReplicaSet,支持滚动更新

    Horizontal Pod Autoscaling:仅适用于Deployment和ReplicaSet

    StatefullSet:为了解决有状态服务的问题,应用场景包括:

    • 稳定的持久化存储
    • 稳定的网络标志
    • 有序部署,有序扩展
    • 有序收缩,有序删除

    DaemonSet:确保全部(或者一些)Node上运行一个Pod的副本,当有Node加入集群时,也会为他们新增一个Pod,当有Node从集群中移除时,这些Pod也会被回收,删除DaemonSet将会删除它创建的所有Pod,使用DaemonSet的一些典型用法:

    • 运行集群存储daemon,例如在每个Node上运行glusterd,ceph
    • 在每个Node上运行日志收集daemon,例如fluentd,logstash
    • 在每个Node上运行监控daemon,例如Prometheus,Node Exporter

    Job:负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束

    Cron Job:管理基于时间的Job,即在给定时间点只运行一次,周期性地在给定时间点运行

以YAML或JSON描述文件创建pod

首先我们呢将使用-o yaml选项的kubectl get命令来获取pod的整个YAML定义

1
kubectl get pod kubia -o yaml

结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
apiVersion: v1  #YAML描述文件所使用的Kubernetes API版本
kind: Pod #Kubernetes对象/资源类型
#pod元数据(名称、标签和注解等等)
metadata:
creationTimestamp: "2021-10-05T10:51:49Z"
labels:
run: kubia
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:labels:
.: {}
f:run: {}
f:spec:
f:containers:
k:{"name":"kubia"}:
.: {}
f:image: {}
f:imagePullPolicy: {}
f:name: {}
f:ports:
.: {}
k:{"containerPort":8080,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:protocol: {}
f:resources: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
f:dnsPolicy: {}
f:enableServiceLinks: {}
f:restartPolicy: {}
f:schedulerName: {}
f:securityContext: {}
f:terminationGracePeriodSeconds: {}
manager: kubectl-run
operation: Update
time: "2021-10-05T10:51:49Z"
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:status:
f:conditions:
k:{"type":"ContainersReady"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
k:{"type":"Initialized"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
k:{"type":"Ready"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
f:containerStatuses: {}
f:hostIP: {}
f:phase: {}
f:podIP: {}
f:podIPs:
.: {}
k:{"ip":"10.1.0.25"}:
.: {}
f:ip: {}
f:startTime: {}
manager: kubelet
operation: Update
time: "2021-10-05T10:51:53Z"
name: kubia
namespace: default
resourceVersion: "306747"
selfLink: /api/v1/namespaces/default/pods/kubia
uid: 33b49309-91a2-4c54-ac70-e9d23b7de098
#pod规格/内容(pod的容器列表、volume等)
spec:
containers:
- image: luksa/kubia
imagePullPolicy: Always
name: kubia
ports:
- containerPort: 8080
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-tkf72
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: docker-desktop
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: default-token-tkf72
secret:
defaultMode: 420
secretName: default-token-tkf72
#pod及其内部容器的详细状态
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2021-10-05T10:51:49Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2021-10-05T10:51:53Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2021-10-05T10:51:53Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2021-10-05T10:51:49Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: docker://aa350eb6e6d5422df108a341e9f11afd74773b3ebdc2587f7605c93464543298
image: luksa/kubia:latest
imageID: docker-pullable://luksa/kubia@sha256:3f28e304dc0f63dc30f273a4202096f0fa0d08510bd2ee7e1032ce600616de24
lastState: {}
name: kubia
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2021-10-05T10:51:53Z"
hostIP: 192.168.65.3
phase: Running
podIP: 10.1.0.25
podIPs:
- ip: 10.1.0.25
qosClass: BestEffort
startTime: "2021-10-05T10:51:49Z"

为pod创建一个简单的YAML描述文件,名为kubia-manual.yaml

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1 #描述文件遵循v1版本的Kubernetes API
kind: Pod #描述一个pod
metadata:
name: kubia-manual #pod的名称
spec:
containers:
- image: luksa/kubia #创建容器所用的镜像
name: kubia #容器的名称
ports:
- containerPort: 8080 #应用监听的端口
protocol: TCP

这里需要注意一下yaml语法对格式是非常严格的,这里不可以有制表符,否则就会报如下相关错误

1
error: error parsing kubia-manual.yaml: error converting YAML to JSON: yaml: line 4: found character that cannot start any token
1
error: error parsing kubia-manual.yaml: error converting YAML to JSON: yaml: line 8: found a tab character that violates indentation

接下来使用kubectl create从YAML文件创建pod

1
kubectl create -f kubia-manual.yaml

得到运行中pod的完整定义

1
kubectl get pod kubia-manual -o yaml

也可以返回JSON格式

1
kubectl get pod kubia-manual -o json
利用kubectl logs命令获取pod日志
1
kubectl logs kubia-manual

如果我们的pod包含多个容器,在运行kubectl logs命令时则必须通过包含-c <容器名称>选项来显式指定容器名称。

1
kubectl logs kubia-manual -c kubia
向pod发送请求

将本地网络端口转发到pod中的端口

如果想要在不通过service的情况下与某个特定的pod 进行通信(出于调试或其他原因), Kubernetes将允许我们配置端口转发到该pod。 可以通过kubectl port-forward命令完成上述操作。 例如以下命令会将机器的本地端口 8888转发 到我们的kubia-manual pod的端口8080

1
2
3
caoyifan@MacBookPro ~ % kubectl port-forward kubia-manual 8888:8080
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080

这个时候端口转发就在运行中,我们可以通过curl命令向pod发送一个HTTP请求

1
2
caoyifan@MacBookPro ~ % curl localhost:8888 
You've hit kubia-manual

下图描述了使用kubectl port-forwardcurl时的简单视图

12

标签

标签是一种简单却功能强大的Kubernetes特性,不仅可以组织pod, 也可以组织所有其他的Kubernetes资源。详细来讲,标签是可以附加到资源的任意键值对,用以选择具有该确切标签的资源(这是通过标答选择器完成的 )。只要标签的key在资源内是唯一的,一个资源便可以拥有多个标签。通常在我们创建资源时就会将标签附加到资源上,但之后我们也可以再添加其他标签,或者修改现有标签的值,而无需重新创建资源。

例如在微服务中,通过给pod添加标签,可以得到一个更组织化的系统。此时每个pod都标有两个标签

  • app:指定pod属于哪个应用、组件或微服务
  • rel:显示在pod中运行的应用程序版本是stable、beta还是canary

如下图所示,显示了使用pod标签组织微服务架构中的pod

13

创建pod时指定标签

首先创建一个名为kubia-manual-with-labels.yaml的文件

代码如下,其中两个标签被附加到pod上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1 
kind: Pod
metadata:
name: kubia-manual-v2
labels:
creation_method: manual
env: prod
spec:
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
protocol: TCP

接下来创建该pod

1
kubectl create -f kubia-manual-with-labels.yaml

使用--show-labels查看标签

1
kubectl get pods --show-labels

14

如果你只对某些标签感兴趣, 可以使用 -L 选项指定它们并将它们分别显示在 自己的列中, 而不是列出所有标签。 接下来我们再次列出所有 pod, 并将附加到 pod名为kubia-manual-v2上的两个标签的列展示如下

1
kubectl get pods -L creation_method,env

15

修改现有pod的标签

标签也可以在现有 pod 上进行添加和修改。由于pod名为kubia-manual也是手动创建的 所以我们可以为其添加 creation_method=manual标签

1
kubectl label pod kubia-manual creation_method=manual

更改现有标签的值

1
kubectl label pod kubia-manual-v2 env=debug --overwrite

再次列出pod以查看更新后的标签

16

使用标签选择器列出pod

列出标签creation_method的值为manual的pod

1
kubectl get pod -l creation_method=manual

列出包含env标签的所有pod 无论其值如何

1
kubectl get pod -l env

列出没有env标签的pod

注意这里要使用单引号来包含!env

1
kubectl get pod -l '!env'
发现其他命名空间及其pod

列出集群中所有的命名空间

1
kubectl get ns

这里需要声明的是,到目前为止,我们只在default命名空间中进行操作,当使用kubecel get命令列出资源时,我们从未明确指定命名空间,因此kubectl总是默认为default命名空间

17

列出指定命名空间的pod

1
kubectl get pod --namespace kube-system

18

创建一个命名空间

我们还是使用YAML文件来创建一个命名空间

创建一个名为custom-namespace.yaml的文件

1
2
3
4
apiVersion: v1
kind: Namespace
metadata:
name: custom-namespace

使用create命令创建

1
kubectl create -f custom-namespace.yaml

如果想要在刚创建的命名 空 间中创建资源,可以选择在metadata字段中添加 一个 namespace : custom-namespace 属性,也可以在使用kubectl create 命令创建资源时指定命名空间 :

1
kubectl create -f kubia-manual.yaml -n custom-namespace

这个时候我们就得到了两个同名的pod(kubia-manual),其中一个在default命名空间中,一个在custom-namespace

删除pod

按照名称删除pod

1
kubectl delete pod kubia-manual

使用标签选择器删除pod

1
kubectl delete pod -l creation_method=manual

通过删除整个命名空间来删除pod

1
kubectl delete ns custom-namespace