基于Packer+Ansible实现云平台黄金镜像统一构建和发布_java

黄金镜像(Golden Image)是云平台虚拟机启动的镜像模板,通常由管理员基于ISO镜像或者各操作系统发行版发行的Base镜像之上添加安全合规基线配置以及预装必要的agent,如cloud-init、qemu-guest-agent、监控agent等。

手动做镜像一直是非常繁琐的事,首先需要准备一个KVM/QEMU虚拟机环境启动虚拟机,当然也可以直接在云平台基于一个Base镜像启动虚拟机。然后手动执行一系列脚本,安装各种必要工具及配置,接着把虚拟机关机,生成快照镜像,最后上传到云平台镜像仓库,预计至少需要半个小时以上完成整个黄金镜像的制作和发布。

这种方式至少存在如下几个问题:

  • 很难维护,一旦黄金镜像需要修改某个配置,就需要重新走整个制作镜像流程,非常耗费时间。如果只是简单配置修改,倒也是可以通过把镜像文件通过loop设备挂载到本地,然后chroot文件系统去直接改。
  • 镜像是一个二进制制品文件,没法做版本管理,无法跟踪变化,无法代码化。
  • 基本无法串联到任何pipeline中,比如镜像漏洞扫描等。

因此还在采用如上这种效率极低的手动方式的已经很少了,往往都会借助一些镜像构建工具,本文接下来会分别介绍OpenStack私有云和AWS公有云场景下的镜像构建工具,最后将介绍多云或者混合云场景下基于Packer+Ansible的镜像构建最佳实践。

备选方案:OpenStack diskimage-builder

OpenStack维护的DIB(disk image builder)项目,目前是TripleO项目的子项目,主要用于构建OpenStack镜像。

DIB的原理就是把一些操作封装成Shell脚本,比如创建用户(devuser)、安装cloud-init(cloud-init)、配置yum源(yum)、部署tgtadm(deploy-tgtadm)等,这些脚本称为Element,位于目录diskimage-builder/diskimage_builder/elements,你可以根据自己的需求自己定制elements,elements之间会有依赖,依赖通过element-deps文件指定,比如elements centos7的element-deps为:

  • cache-url
  • redhat-common
  • rpm-distro
  • source-repositories
  • yum

devuser Element为例,该Element为创建一个操作系统用户,脚本如下:

#!/bin/bash
# ...省略部分代码
user_shell_args=
if [ -n "${DIB_DEV_USER_SHELL}" ]; then
    user_shell_args="-s ${DIB_DEV_USER_SHELL}"
fi
useradd -m ${DIB_DEV_USER_USERNAME} $user_shell_args
if [ -n "${DIB_DEV_USER_PASSWORD}" ]; then
    echo "Setting password."
    echo "${DIB_DEV_USER_USERNAME}:${DIB_DEV_USER_PASSWORD}" | chpasswd
fi
if [ -n "${DIB_DEV_USER_PWDLESS_SUDO}" ]; then
    cat > /etc/sudoers.d/${DIB_DEV_USER_USERNAME} << EOF
${DIB_DEV_USER_USERNAME} ALL=(ALL) NOPASSWD:ALL
EOF
    chmod 0440 /etc/sudoers.d/${DIB_DEV_USER_USERNAME}
    visudo -c || rm /etc/sudoers.d/${DIB_DEV_USER_USERNAME}
fi
if [ -f /tmp/in_target.d/devuser-ssh-authorized-keys ]; then
    mkdir -p /home/${DIB_DEV_USER_USERNAME}/.ssh
    cp /tmp/in_target.d/devuser-ssh-authorized-keys /home/${DIB_DEV_USER_USERNAME}/.ssh/authorized_keys
fi
chown -R ${DIB_DEV_USER_USERNAME}:${DIB_DEV_USER_USERNAME} /home/${DIB_DEV_USER_USERNAME}

可见该脚本就是使用Shell命令完成创建用户、设置密码以及配置sudo权限等工作。

DIB执行时会首先下载一个Base镜像,然后通过用户指定的elements列表,一个一个chroot进去执行,从而完成了镜像的制作,整个过程不需要启动虚拟机。这有点类似Dockerfile的构建过程,Dockerfile的每个指令都会生成一个临时的容器,然后在容器里面执行命令。DIB则每个elements都会chroot到文件系统中执行elements中的脚本。

比如制作Ubuntu 18.04镜像:

export DIB_RELEASE=bionic
export DIB_DEV_USER_USERNAME=ubuntu
export DIB_DEV_USER_PASSWORD=secret
export DIB_DEV_USER_PWDLESS_SUDO=YES
disk-image-create -o ubuntu-18.04.qcow2 vm ubuntu cloud-init-datasources devuser

DIB工具实现了OpenStack镜像自动化构建,相对手动制作镜像大大提高了效率。存在的问题是Elements是通过Shell脚本实现的,容易出错且不太好维护,很多判断逻辑都是和操作系统版本有关,很难做到完全兼容。另外,DIB通过chroot方式修改镜像,因此只支持主流的Linux操作系统,不支持Windows。

备选方案:AWS EC2 Image Builder

EC2 Image Builder是AWS上的镜像构建服务,它的原理是基于AWS已有托管的一个Source Base镜像启动一个EC2虚拟机实例,然后SSH到虚拟机执行一系列脚本安装软件和配置,最后创建快照制作成AMI镜像。

基于Packer+Ansible实现云平台黄金镜像统一构建和发布_java_02

aws ec2 image builder

和DIB有点类似,EC2 Image Builder也是把一些操作封装成Shell脚本,在DIB中称为Element,而AWS上称为Component,这些Comontents可以由用户自己写,AWS也提供一些托管的Components可以直接使用,比如安装amazon-cloudwatch-agent-linux、预装apache-tomcat-9-linux等。

一个最简单的HelloWorld Component如下:

name: HelloWorldComponent
description: This is hello world testing component.
schemaVersion: 1.0
phases:
  - name: build
    steps:
      - name: HelloWorldStep
        action: ExecuteBash
        inputs:
          commands:
            - echo "Hello World! Build."
  - name: validate
    steps:
      - name: HelloWorldStep
        action: ExecuteBash
        inputs:
          commands:
            - echo "Hello World! Validate."
  - name: test
    steps:
      - name: HelloWorldStep
        action: ExecuteBash
        inputs:
          commands:
            - echo "Hello World! Test."

和DIB不一样,DIB是通过chroot到镜像文件系统,注定无法制作Windows镜像,而EC2 Image Builder启动了一个虚拟机,Windows虚拟机可以通过WinRM执行PowerShell脚本,因此EC2 Image Builder是支持Windows的。

EC2 Image Builder除了执行脚本外,还封装了一些高级Action模块,比如从S3上上传下载文件、修改文件权限、复制文件等,通过这些高级模块不需要自己写脚本,可以简化Component。

EC2 Image Builder可以与其他生态服务结合完成自动构建,比如一旦Component代码更新或者Base镜像更新后,EC2 Image Builder会自动触发镜像构建,因此能够保证黄金镜像是最新的。

推荐方案:Packer+Ansible

Packer是Hashicorp开源的镜像构建工具,Hashicorp这个公司除了开源了Packer项目,还包括Consul、Terraform、Vault、Vagrant等非常流行的工具。

如果说DIB解决了OpenStack私有云镜像自动化构建问题,EC2 Image Builder解决了AWS公有云镜像构建,那么Packer则同时解决了私有云、公有云、混合云的黄金镜像统一构建问题,它不仅支持主流公有云如AWS、阿里云、腾讯云、Google云、Azure等,还支持如QEMU、VirtualBox、VMware、OpenStack等私有云环境。

Packer的原理和EC2 Image Builder比较类似都是通过启动一个虚拟机,然后通过SSH(Linux)或者WinRM(Windows)在虚拟机环境中执行脚本完成自动化配置,最后生成镜像模板。比如制作AWS黄金镜像会创建一个EC2实例,而如果是QEMU,则通过qemu-system-x86命令配合kickstart启动一个初始化虚拟机。

Packer支持并行在多平台上构建镜像,在混合云场景下,Packer能基于同样的标准同时并行构建公有云镜像和私有云镜像,保持多平台的镜像一致性。

除此之外,Packer和前面介绍的两个镜像构建工具不一样的是,除了支持常规的Shell或者Powershll脚本,Packer更推荐结合自动化配置管理工具构建镜像模板,比如大家熟悉的Ansible、Puppet、Chef、Salt等。

本文接下来主要简要介绍如何使用Packer + Ansible构建OpenStack以及AWS镜像。

首先介绍Packer的最主要的两个概念:

  • Builder:Builder就是告诉Packer要构建什么镜像(AWS Or OpenStack)以及一些必要的环境配置信息,比如AWS的AccessKey/AccessSecret、Source AMI、规格等,OpenStack的AuthURL、Project、Domain、Username、Password、Source Image等。
  • Provisioner:Provisioner就是告诉Packer要如何构建镜像,你可以告诉Packer执行一些Shell脚本、执行Ansible playbook等。

以构建OpenStack镜像为例,样例模板如下:

{
  "variables": {
    "region""RegionOne",
    "flavor""m1.micro",
    "network_id""695c8e70-ae94-4c39-86f0-c15dcaea7dd2",
    "source_image""4e61e55b-c635-44e7-a722-674b2a454927",
    "ssh_username""ubuntu"
  },
  "builders": [
    {
      "type""openstack",
      "region""{{user `region`}}",
      "ssh_username""{{user `ssh_username`}}",
      "image_name""Packer-ubuntu-20.04-x86",
      "source_image""{{user `source_image`}}",
      "flavor""{{user `flavor` }}",
      "networks": [
        "{{user `network_id`}}"
      ]
    }
  ],
  "provisioners": [
    {
      "type""shell",
      "inline": [
        "sudo useradd -m -r -s /bin/bash int32bit",
        "echo 'int32bit:1sReAe7nUGO5M' | sudo chpasswd -e"
      ]
    }
  ]
}

这个模板只配置了一个OpenStack Builder,Provisioner也比较简单,仅通过Shell脚本创建了一个int32bit用户。

这种JSON格式模板对于程序是友好的,但是对于程序员编写不太方便,Packer模板还支持HCL(Hashicorp Configuration Language)语言,目前还处于Beta阶段,不过亲测可以用。

Terraform使用的就是HCL,因此使用过Terraform的对HCL语言肯定不陌生了,前面的例子转化成HCL格式为:

variable "flavor" {
 type    = string
 default = "m1.micro"
}

variable "network_id" {
 type    = string
 default = "unset"
}

variable "region" {
 type    = string
 default = "RegionOne"
}

variable "source_image" {
 type    = string
 default = "unset"
}

variable "ssh_username" {
 type    = string
 default = "ubuntu"
}

source "openstack" "test_openstack" {
 flavor       = "${var.flavor}"
 image_name   = "Packer-ubuntu-20.04-x86"
 networks     = ["${var.network_id}"]
 region       = "${var.region}"
 source_image = "${var.source_image}"
 ssh_username = "${var.ssh_username}"
}

build {
 sources = ["source.openstack.test_openstack"]

 provisioner "shell" {
   inline = [
       "sudo useradd -m -r -s /bin/bash int32bit",
       "echo 'int32bit:1sReAe7nUGO5M' | sudo chpasswd -e"
   ]
 }
}

调用packer命令即可构建如上镜像:

packer build -var source_image=xxxx -var network_id=xxxx ubuntu.pkr.hcl

基于Packer+Ansible实现云平台黄金镜像统一构建和发布_java_03

packer openstack packer

如上的Provisioner通过内嵌Shell脚本进行配置,简单的功能还好,复杂的配置则更推荐使用自动化配置工具,接下来以Ansible为例,介绍如何构建黄金镜像。

前面介绍的DIB、EC2 Image Builder按照功能分别拆分的Element和Component,这样便于模块化管理。使用Ansible我们可以把不同的功能划分为不同的Ansible Role。

以安装cloud-init为例,首先使用ansible-galaxy初始化Role:

mkdir -p packer_template/ansible/roles
cd packer_template/ansible/roles
ansible-galaxy role init cloud-init

Role的playbook如下:

# cat ansible/roles/cloud-init/tasks/main.yml
---
# tasks file for cloud-init
- name: Install cloud-init
  package:
    name: cloud-init
    state: present
- name: Enable cloud-init service
  service:
    name: cloud-init
    enabled: true

创建入口playbook build_image.yaml如下:

# cat ansible/build_image.yaml
---
- name: Config Task For Build Image
  hosts: all
  gather_facts: true
  become: yes
  become_method: sudo
  tasks:
  - name: Setup cloud-init
    include_role:
      name: cloud-init

创建Packer template,这里我们同时构建AWS和OpenStack镜像,为了使代码简单,去掉了variable部分,OpenStack的认证信息通过环境变量获取,AWS的认证通过STS Role指定,对应template模板文件如下:

source "openstack" "test_openstack" {
 image_name    = "Packer-test-ubuntu"
 flavor        = "m1.micro"
 ports         = ["695c8e70-ae94-4c39-86f0-c15dcaea7dd2"]
 region        = "RegionOne"
 source_image  = "4e61e55b-c635-44e7-a722-674b2a454927"
 ssh_username  = "ubuntu"
}

source "amazon-ebs" "test_aws" {
 ami_name      = "Packer-test-ubuntu"
 region        = "cn-northwest-1"
 source_ami    = "ami-04effa29f4d91541f"
 instance_type = "t2.micro"
 ssh_username  = "ubuntu"
 vpc_id        = "vpc-02f7b6239c82c9cd1"
 subnet_id     = "subnet-053ab0880cea4e85c"
}

build {
 sources = [
   "source.openstack.test_openstack",
   "source.amazon-ebs.test_aws"
 ]
 provisioner "ansible" {
   playbook_file = "ansible/build_image.yaml"
   user          = "ubuntu"
 }
}

与前面的例子不同的是:

  • sources指定了多个builder,这里分别为OpenStack和AWS。
  • provisioner指定为ansible类型,指定了playbook路径。

使用packer命令执行:

packer build ubuntu.pkr.hcl

输出如图:

基于Packer+Ansible实现云平台黄金镜像统一构建和发布_java_04

packer openstack and aws

不同的builder通过颜色区分,最后会输出生成的所有镜像制品ID:

==> Wait completed after 5 minutes 53 seconds

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs.test_aws: AMIs were created:
cn-northwest-1: ami-071e19313c601b374
--> openstack.test_openstack: An image was created: 
583a0bac-b973-47aa-8424-8929dfa99288

以上例子通过Packer+Ansible使用同一个Provision配置并行构建了OpenStack私有云镜像和AWS公有云镜像,实现黄金镜像的构建自动化和代码化。

结论

本文首先介绍了私有云场景下OpenStack DIB工具以及公有云AWS EC2 Image Builder服务两个主流镜像构建方案,对比了其优缺点,然后介绍了适合多云或者混合云场景下的Packer镜像构建工具,最后引入Packer+Ansible镜像最佳构建实践。

通过Packer可同时并行构建私有云和公有云镜像,并且由于Provision是一样的,最后构建的镜像也是完全一样的,通过Packer实现了混合云模式下镜像的统一构建和发布。

同时通过Ansible自动化工具简化了代码的编写,Ansible playbook相对Shell脚本更易于维护和管理,不需要自己脚本判断操作系统类型,Ansible会自己判断。

镜像构建完全代码化,因此可以很容易地通过代码仓库做版本控制,集成企业CICD,实现镜像的统一构建和发布。


基于Packer+Ansible实现云平台黄金镜像统一构建和发布_java_05