管理2000+Docker镜像,Kolla是如何做到的

根据 DockerHub 上的数据,整个 Kolla 项目管理的 镜像有 2000 多个,这么多的镜像,是怎么定义,又是如何构建的呢?

简介

我们一直在说的 Kolla,通常情况下泛指,包括了 KollaKolla-Ansible 两个项目。

实际上,根据 OpenStack Wiki,还有个 Kayobe 项目也是相关的。但是这个用的比较少,而且我试用后觉得不是特别符合我的需求,就不过多介绍了。

此外还有一个项目 Kolla-kubernetes 致力于和 Kubernetes 结合,但是和另一个项目 openstack-helm 重合较多,提前退休了。

Kolla 项目开始之初只有一个项目,从构建 docker 容器,到使用 ansible 部署,全流程搞定。后来把 ansible 这块分离了出来,独立为 kolla-ansible 项目,原来的 kolla 专门负责 docker 镜像的构建。

镜像划分的维度

虽然最终的镜像个数超过 2000 个,实际并不是完全独立的 2000 多个服务。而是针对不同的场景分别构建,多维度全面覆盖的结果。

镜像分层

熟悉 Docker 的小伙伴都知道,Dockerfile 是可以指定“继承”关系的。也就是利用镜像分层的特性,逐层构建。

OpenStack 中有很多子服务隶属于同一个项目,例如,nova-apinova-compute 等都属于 nova,所以,很自然地可以先构建一个通用的 nova-base 镜像,然后在此基础上分别构建不同的服务。

这是一个纵向的划分维度。

功能划分

因为 Kolla 项目不仅是把 OpenStack 的服务集成了,周边用到的组件和辅助服务也都囊括在内。包括 RabbitMQMariaDB 等。

这是一个横向的划分维度。

以上两个是最基础的划分维度,也是我们能够很容易想到的。

操作系统

每个 Docker 镜像最底层只能是操作系统的基础镜像。现在主流的 Linux 发行版有好几家,OpenStack 作为一个世界级的开源项目,要是只绑定一家,其他人可不答应。

所以,必须要同时支持多个操作系统。这个靠 Dockerfile 显然解决不了。

如果为每个操作系统单独的定义一份 Dockerfile 显然不够聪明。 Kolla 使用了 Jinja 模板文件多做了一层抽象,根据指定的参数先由 Dockerfile.j2 生成 Dockerfile

这个维度在 kolla 中对应的参数是 base,目前支持的操作系统有:

['centos', 'rhel', 'ubuntu', 'debian']

Jinja 是 Python 中使用比较广泛的模板引擎(template engine)。之所以叫 Jinja,是因为日本的神社(Jinja)英文单词是 temple,而模板的英文是 template,两者发音很相似(什么脑回路)。Jinja 项目的 Logo 也是一个神社的图标,可能是因为这层关系,这个在国内似乎讨论的并不多。

安装方式

Kolla 不仅是要作单纯的部署工具,还希望能够替代 devstack 为开发助力,所以除了从软件源(如 yumapt 等)直接安装打包好的程序,还要能够直接从源码安装。

从软件包称为 binary,从源码安装称为 source

这个维度也是在处理 Jinja 模板的阶段完成。

实际上,还有 2 个安装方式,rdorhos,都是针对 RedHat 系统的,一般不怎么会用到。

操作系统和安装方式这两个维度,决定了镜像名称的前缀:

文件的组织结构

了解了划分维度,我们来看一下具体的文件组织结构是怎样的。

所有的构建 Docker 镜像相关的文件都存放在 kolla/docker 目录下。这下面的文件夹众多,下面把有代表性的列了出来:

docker/
├── base
│   └── Dockerfile.j2
├── horizon
│   └── Dockerfile.j2
├── mariadb
│   └── Dockerfile.j2
├── nova
│   ├── nova-api
│   │   └── Dockerfile.j2
│   ├── nova-base
│   │   └── Dockerfile.j2
│   └── nova-compute
│       └── Dockerfile.j2
└── openstack-base
    └── Dockerfile.j2

每个文件夹代表了一个服务,除了名字带 base 的,其中顶层的有 2 个:

  • base 这是所有镜像的初始层
  • openstack-base 这是所有 OpenStack 相关服务的初始层

如果一个组件包含多个服务,比如 nova,它内部就会又多出一层基础层: nova-base。所有其它的 nova-<xxx> 都是从这层开始。如果一个组件只有一个服务,则不需要再有子文件夹,直接是 Dockerfile.j2 文件。

镜像层之间的关系是在 Dockerfile 文件中的 FROM 语句定义的。它们在 jinja 模板中是固定的。

例如 horizon/Dockerfile.j2 中:

FROM {{ namespace }}/{{ image_prefix }}openstack-base:{{ tag }}

openstack-base/Dockerfile.j2 中:

FROM {{ namespace }}/{{ image_prefix }}base:{{ tag }}

它们之间的依赖关系是这样的:

base
├── openstack-base
│   ├── nova-base
│   │   └── nova-api
│   │   └── nova-compute
│   └── horizon
└── mariadb

可以看到,最多就 4 层。

包含 .j2 文件的文件夹名字最终会成为镜像名的一部分,如 <os>-<type>-nova-api

这里的 <os>-<type>- 也就是对应上面的 {{ image_prefix }} 字符串,分别对应:

  • 操作系统,如 centos
  • 安装类型,如 binary

所以最终上面的文件对应的镜像是:

centos-binary-base
centos-binary-openstack-base
centos-binary-nova-base
centos-binary-nova-api
centos-binary-nova-compute
centos-binary-horizon

注意,并不是每个镜像都支持任意的类型组合,具体需要查看 kolla 源码。

base 镜像的作用

所有镜像的源头都是 base,所以它肯定是很重要的。其中内容主要有两个地方比较关键:

设置软件仓库源

所有软件包的安装源配置都在 base 中完成。不管是 OpenStack 安装源还是其它依赖的公共组件安装源,统统在基础镜像里固定下来了。

所以在国内网络不好的情况下,就必须要替换其中的仓库源。

设置容器启动命令

定义了默认的 ENTRYPOINTCMD,也就是把容器的启动方式固定了下来。

相信这里大家会有疑惑,那么多不同的服务,怎么可能在这里把启动命令固定下来呢?其实这里有一点技巧。

这里 kolla 固定了一个启动脚本 start.sh,在这个脚本里从固定位置 /run_command 读到真正的执行命令。/run_command 则是由 kolla-ansible 在启动容器的时候注入的。

还记得在 介绍 Kolla 的配置文件 时看到的 config.json 么,其中有一个 command 字段。例如 Horizon 服务的配置:

{
    "command": "/usr/sbin/httpd -DFOREGROUND",
    "config_files": [
        {
            "source": "/var/lib/kolla/config_files/horizon.conf",
            "dest": "/etc/httpd/conf.d/horizon.conf",
            "owner": "horizon",
            "perm": "0600"
        },
    ]
}

这样做,既保证了构建镜像时候的一致性,又保证了容器启动的灵活性。

处理流程

kolla 构建镜像的流程非常简单,大体就是 5 个步骤:

1. 生成 Dockerfile

docker 整个目录复制到一个临时的工作目录,然后在其中扫描包含有 Dockerfile.j2 文件的文件夹。正如在上面分析的那样,这样的一个文件夹就对应一个镜像。

使用从配置文件中获取的操作系统基础镜像和安装方式等参数,渲染生成 Dockerfile 文件。

参考源码:create_dockerfiles

2. 构建镜像列表

将上一步生成的 Dockerfile 都读取到内存,处理里面的 FROM 语句,可以获得每个镜像的 parent 名字。还有其它一些关于安装方式的细节也要处理,不用过多关心。

这一步完成我们就得到了一个镜像列表。这里的镜像指的是 kolla 定义的 Image 类的实例。

3. 查找镜像关系

遍历整个镜像列表,把它们的依赖关系整理清楚。

4. 过滤镜像列表

因为总共镜像数量比较多,所以需要根据用户提供的参数做一下过滤。

过滤参数有两种方式:

  • 预先定义了几组常用的镜像分组,称为 profile,指定分组名,就可以构建对应的镜像
  • 通过正则表达式匹配镜像的名字来选择

5. 执行构建

使用多线程任务队列,批量执行构建。

构建完镜像后,还有一个可选操作,将镜像 push 到指定的 registry 中。

以上过程,有兴趣的可以自行去看 kolla 源码,主要内容就集中在 1 个 build.py 文件,还是很简单的。

使用方法

为避免本文内容失效,请关注 Kolla 项目官方文档 获取更新。

安装 Python 3

CentOS 7 自带的 Python 版本还是 2.7,在 2020 年后不再维护,Kolla 项目有的依赖包不再支持。

yum install python3

CentOS 7 的安装源提供的 Python 3 版本是 3.6.8

创建虚拟环境(可选)

推荐在 Python 虚拟环境中安装 Kolla:

python3 -m venv .virtualenvs/kolla-build
source .virtualenvs/kolla-build/bin/activate
(kolla-build) [root@davycloud ~]#

以下操作默认都在虚拟环境下执行。

安装 Kolla

有两种方式,

  • 使用 pip 安装
  • 从源码安装

推荐采用后者,有助于学习,也方便改代码。

使用 git 下载源码:

# OpenStack 官方 git 源
git clone https://opendev.org/openstack/kolla

# 上面网速慢的可以使用下面的镜像站地址
git clone http://git.trystack.cn/openstack/kolla

然后使用 pip 安装即可:

(kolla-build) $ pip install kolla/

注意最后的斜杠,表示我们安装的是本地目录。安装完毕后可以执行:

(kolla-build) [root@davycloud ~]# kolla-build --version
9.1.0

生成配置文件

Kolla 构建镜像有不少配置项,但是基本保持默认即可。并且缺少配置文件 kolla-build 命令也能执行,所以这一步这里就 略过 了。

如果你想生成 kolla-build.conf 配置文件,可以参考 官方文档

构建 base 镜像

构建最最基础的 base 镜像:

(kolla-build) [root@davycloud ~]# kolla-build ^base
INFO:kolla.common.utils:Found the docker image folder at /root/.virtualenvs/kolla-build/share/kolla/docker
INFO:kolla.common.utils:Added image base to queue
INFO:kolla.common.utils:Attempt number: 1 to run task: BuildTask(base)
INFO:kolla.common.utils.base:Building started at 2020-01-28 19:54:50.158139
INFO:kolla.common.utils.base:Step 1/37 : FROM centos:7
INFO:kolla.common.utils.base: ---> 5e35e350aded
INFO:kolla.common.utils.base:Step 2/37 : LABEL maintainer="Kolla Project
...
INFO:kolla.common.utils.base:Successfully tagged kolla/centos-binary-base:9.1.0

注意^base 前面的 ^ 符号不可省略,这是正则表达式中表示句首的符号,如果缺少该符号,kolla-build 会把 openstack-basenova-base 等一众名字包含 base 的镜像都匹配上了。

构建的镜像标签默认是 kolla 的版本号,9.1.0,我们后面会指定自己的版本号。

修改 base 镜像

我们在前面分析过了,所有的安装源都在 base 镜像中指定了。

如果我们直接使用这个 base 镜像,后面的镜像构建过程中软件的安装速度没法保证。

当然,我们可以在下载完 kolla 源码之后就直接修改 base 对应的 Dockerfile.j2 和相关的构建文件,但是这样修改是比较麻烦的。因为其中夹杂着其它情况的处理代码,例如:

{% if base_package_type == 'rpm' %}
# For RPM Variants, enable the correct repositories - this should all be done
# in the base image so repos are consistent throughout the system.  This also
# enables to provide repo overrides at a later date in a simple fashion if we
# desire such functionality.  I think we will :)

RUN CURRENT_DISTRO_RELEASE=$(awk '{match($0, /[0-9]+/,version)}END{print version[0]}' /etc/system-release); \
    if [  $CURRENT_DISTRO_RELEASE != "{{ supported_distro_release }}" ]; then \
        echo "Only release '{{ supported_distro_release }}' is supported on {{ base_distro }}"; false; \
    fi \
    && cat /tmp/kolla_bashrc >> /etc/bashrc \
    && sed -i 's|^\(override_install_langs=.*\)|# \1|' {% if distro_package_manager == 'dnf' %}/etc/dnf/dnf.conf{% else %}/etc/yum.conf{% endif %}

{% block base_yum_conf %}

{% if distro_package_manager == 'dnf' %}
COPY dnf.conf /etc/dnf/dnf.conf
{% else %}
COPY yum.conf /etc/yum.conf
{% endif %}

修改难度是比较大的,要把其中的逻辑捋清楚才能下手。而且以后每次这个文件的版本有变化,更新都要对照着修改。

这里我采取了比较投机取巧的办法,等这个 base 镜像构建完成后,直接在它之上修改。这个时候我们已经确定了操作系统(CentOS)和安装方式(Binary),这样只需要替换 /etc/yum.repos.d/ 下面的 .repo 文件即可。

先把 kolla/centos-binary-base:9.1.0 镜像内的 /etc/yum.repos.d/ 整个文件夹都拷贝出来,逐个 .repo 修改,把其中的 URL 替换成阿里云镜像站的 URL。

然后写了一个超级简单粗暴的 Dockerfile:

FROM kolla/centos-binary-base:9.1.0

RUN mkdir -p /etc/yum.repos.d/bak && mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/bak

COPY CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo
COPY CentOS-Ceph-Nautilus.repo /etc/yum.repos.d/CentOS-Ceph-Nautilus.repo
COPY CentOS-CR.repo /etc/yum.repos.d/CentOS-CR.repo
COPY CentOS-Debuginfo.repo /etc/yum.repos.d/CentOS-Debuginfo.repo
COPY CentOS-fasttrack.repo /etc/yum.repos.d/CentOS-fasttrack.repo
COPY CentOS-Media.repo /etc/yum.repos.d/CentOS-Media.repo
COPY CentOS-NFS-Ganesha-28.repo /etc/yum.repos.d/CentOS-NFS-Ganesha-28.repo
COPY CentOS-OpenStack.repo /etc/yum.repos.d/CentOS-OpenStack.repo
COPY CentOS-OpsTools.repo /etc/yum.repos.d/CentOS-OpsTools.repo
COPY CentOS-QEMU-EV.repo /etc/yum.repos.d/CentOS-QEMU-EV.repo
COPY CentOS-Sources.repo /etc/yum.repos.d/CentOS-Sources.repo
COPY CentOS-Storage-common.repo /etc/yum.repos.d/CentOS-Storage-common.repo
COPY CentOS-Vault.repo /etc/yum.repos.d/CentOS-Vault.repo
COPY crmsh.repo /etc/yum.repos.d/crmsh.repo
COPY elasticsearch.repo /etc/yum.repos.d/elasticsearch.repo
COPY epel.repo /etc/yum.repos.d/epel.repo
COPY epel-testing.repo /etc/yum.repos.d/epel-testing.repo
COPY grafana.repo /etc/yum.repos.d/grafana.repo
COPY influxdb.repo /etc/yum.repos.d/influxdb.repo
COPY opendaylight.repo /etc/yum.repos.d/opendaylight.repo
COPY rabbitmq_rabbitmq-server.repo /etc/yum.repos.d/rabbitmq_rabbitmq-server.repo
COPY td.repo /etc/yum.repos.d/td.repo

然后用它来构建一个新的镜像:

(kolla-build) [root@davycloud ~]# docker build . -t kolla/centos-binary-base:davycloud

注意,其中的镜像 tag 可以自己随便定义。

构建 openstack-base 镜像

有了基础镜像,就可以开始构建其它的镜像了。可以先挑一个试一试,比如 openstack-base

注意,上面已经把 tag 修改了,所以接下来的命令必须要带两个选项:

  • --tag davycloud,用来指定自定义的 tag,
  • --skip-existing,略过已经创建好的镜像
(kolla-build) [root@davycloud aliyun]# kolla-build --tag davycloud --skip-existing  openstack-base

INFO:kolla.common.utils:Found the docker image folder at /root/.virtualenvs/kolla-build/share/kolla/docker
INFO:kolla.common.utils:===========================
INFO:kolla.common.utils:Images that failed to build
INFO:kolla.common.utils:===========================
ERROR:kolla.common.utils:openstack-base Failed with status: matched

会出现这么一个莫名其妙的错误。这其实是 kolla 这里处理的逻辑有点问题。找到下面所示代码,在 image.status = STATUS_UNMATCHED 上面加一个判断:

@@ -1117,9 +1117,9 @@ class KollaWorker(object):
                                 ancestor_image.status = STATUS_MATCHED
                         LOG.debug('Image %s matched regex', image.name)
                 else:
+                    # See: https://bugs.launchpad.net/kolla/+bug/1810979
+                    if image.status != STATUS_SKIPPED:
+                        image.status = STATUS_UNMATCHED
-                    # we do not care if it is skipped or not as we did not
-                    # request it
-                    image.status = STATUS_UNMATCHED
         else:
             for image in self.images:
                 if image.status != STATUS_UNBUILDABLE:

我已经给社区提了修改补丁,但是没有下文。

修改完毕之后,就可以重试上面的命令来构建镜像了。

构建其它镜像

Kolla 总共支持的镜像比较多,不太可能全部需要,所以最好事先挑选一番。

最简单的是通过 profile 来批量指定,然后通过 --list-images 选项,在构建之前查看镜像列表,做到心中有数:

(kolla-build) [root@davycloud aliyun]# kolla-build -p default --list-images

1 : openstack-base
2 : chrony
3 : barbican-keystone-listener
4 : barbican-base
5 : nova-spicehtml5proxy
6 : nova-conductor
7 : nova-ssh
8 : nova-libvirt
9 : nova-scheduler
10 : nova-compute-ironic
11 : nova-novncproxy
12 : nova-serialproxy
13 : nova-api
14 : nova-compute
15 : nova-base
16 : glance-api
17 : glance-registry
18 : glance-base
19 : kolla-toolbox
20 : neutron-server-opendaylight
21 : neutron-l3-agent
22 : neutron-mlnx-agent
23 : neutron-server
24 : neutron-server-ovn
25 : neutron-metadata-agent
26 : neutron-dhcp-agent
27 : neutron-openvswitch-agent
28 : neutron-bgp-dragent
29 : neutron-linuxbridge-agent
30 : neutron-infoblox-ipam-agent
31 : neutron-base
32 : neutron-metering-agent
33 : neutron-sriov-agent
34 : neutron-metadata-agent-ovn
35 : fluentd
36 : heat-api-cfn
37 : heat-engine
38 : heat-base
39 : heat-api
40 : heat-all
41 : ironic-neutron-agent
42 : mariadb
43 : keystone-ssh
44 : keystone
45 : keystone-fernet
46 : keystone-base
47 : openvswitch-db-server
48 : openvswitch-base
49 : openvswitch-vswitchd
50 : prometheus-haproxy-exporter
51 : prometheus-base
52 : prometheus-memcached-exporter
53 : base
54 : rabbitmq
55 : cron
56 : haproxy
57 : keepalived
58 : memcached
59 : horizon
60 : placement-base
61 : placement-api

也可以查看源码文件:kolla/common/config.py 中的 _PROFILE_OPTS 查看支持哪些 profile 以及包含的镜像列表。

(kolla-build) [root@davycloud ~]# kolla-build --tag davycloud --skip-existing -p default

把镜像推送到 registry

可以是本地自建的服务,也可以是其它平台提供的,比如 阿里云的容器镜像服务

具体的过程就不赘述了

一切完工之后就可以参考我之前的文章,在使用 Kolla-Ansible 部署环境的时候在 globals.yml 中修改 registry 相关配置,使用自己的镜像源了。


如果本文对你有帮助,请 点赞关注分享,谢谢!

posted @ 2020-01-30 11:01  DavyCloud  阅读(388)  评论(3编辑  收藏