600 行代码构建混合云应用管理平台

前言

混合云管理平台 (简称 CMP) 前几年比较火,笔者工作生涯中接触的第一个项目就是 CMP,后面几年也有关注过其他 CMP 产品,但是大体都是完成 IaaS 资源整合后,在其之上再提供服务目录 Service Catalog 实现云资源的编排。

但是由于 CMP 需要对接很多云服务商 (简称 Provider),所以大部分的 CMP 都非常的复杂,就算有类似 libcloud fog 之类的库,大部分的开发时间也都会浪费在与不同 Provider 对接与状态管理上。

笔者周末的时候发现 Terraform 基本已经支持所有常见的 Provider,并支持在其之上提供资源编排,功能及其强大,但是可惜 Terraform 并没有暴露出 API,社区也没有基于其开发的管理平台。

笔者遂即想基于 Terraform 构建一个混合云应用管理平台,实现以下功能

  • 多云资源的管理
  • 提供服务模板,可定义参数,根据定义好的服务模板可部署实例
  • 实例的生命周期管理 (创建/升级/销毁/同步)

Terraform 简介

Terraform是一种用于安全有效地构建、更改和版本化基础设施的工具。Terraform 可以管理现有的和流行的服务提供商以及定制的内部解决方案。

Terraform 的配置文件描述了运行单个应用程序或复杂的应用架构所需的资源。通过配置文件,Terraform 生成一个执行计划,描述它将做什么来达到所需的状态,然后执行它来构建所描述的基础设施。当配置发生变化时,Terraform能够确定发生了什么变化,并创建可以应用的增量执行计划。

我们来看一个最简单的 Terraform 例子,这里定义了一台 OpenStack 虚拟机

# providers.tf
provider "openstack" {
region = "RegionOne"
insecure = "true"
}

# main.tf
resource "openstack_compute_instance_v2" "terraform" {
name = "${var.vm_name}"
image_name = "${var.image}"
flavor_name = "${var.flavor}"
key_pair = "terraform"
security_groups = ["terraform"]

network {
uuid = "${var.network}"
}
}

# variables.tf
variable "image" {
default = "Ubuntu 16.04"
}

.... # 内容过长省略

variable "vm_name" {
default = "terraform"
}


# outputs.tf
output "address" {
value = "${openstack_networking_floatingip_v2.terraform.address}"
}

output "power_state" {
value = "${openstack_compute_instance_v2.terraform.power_state}"
}

Terraform 中管理这台虚拟机的生命周期只需要以下几个步骤

$ terraform plan # 生成执行计划

$ terraform apply -var vm_name='vm1' # 执行变更

$ terraform refresh # 刷新资源状态

$ terraform show -json # 查看资源状态

$ terraform destroy # 销毁资源

回到我们想要构建的平台,我们需要完成实例的生命周期管理,Terraform 通过以上命令都能够做到

服务模板参数的支持我们可以通过 Terraform 原生的 variable 定义来做

多个云提供商也是 Terraform 原生支持的

但是由于 Terraform 没有提供原生 API,所以我们只能通过拼接命令的方式去调用

资源抽象

要将 Terraform 进行封装成所期望的平台还要对 Terraform 的资源在进行一层抽象

首先我们分析 Terraform 的配置文件,主要分为以下几块

  • resources
  • datasources
  • variables
  • outputs
  • providers

providers 毫无疑问是可以复用的,所以我们先定义一个 Provider 的模型

class Provider:
name = StringField()
config = TextField()

然后定义服务模板,模板和 Provider 是一对多关系 (和 Terraform 原生保持一致),分别包含了以上几种配置文件,但是基于 variables 之上我们在封装一层用户变量,这些变量限定了实例允许传入的参数,这些参数会被传递到 terramform applyplan 命令的 -var 选项中

class Template:
name = StringField()
providers = OneToMany(Provider)
resources = TextField()
outputs = TextField()
datasources = TextField()
variables = TextField()
user_variables = ListField()

最后我们定义实例,实例和模板是一对一关系,并包含很多状态和当前 terraform plan 的状态信息

class Instance:
name = StringField()
template = OneToOne(Template)
variables = ListField()
stauts = StringField() # ['INITIALIZATION', 'INITIALIZATION_ERROR', 'INITIALIZATION_SUCCEED', 'APPLYING', 'APPLY_ERROR', 'APPLY_SUCCEED', 'SYNCING', 'SYNC_ERROR', 'SYNC_SUCCEED', 'DESTROYING', 'DESTROY_ERROR']
states = ListField()

封装 Terraform

由于 Terraform 没有暴露出 API,笔者只能通过拼接命令的方式去封装

首先我们定义一个类,TerraformManagerService 提供了基础命令的封装,但是只拼接命令,不执行

class TerraformManagerService(object):
def __init__(self, terraform_bin, plan_template_dir):
self.terraform_bin = terraform_bin
self.plan_template_dir = pathlib.Path(plan_template_dir)

def validate(self, *args):
return [self.terraform_bin, 'validate'] + list(args)

def apply(self, *args):
return [self.terraform_bin, 'apply', '-auto-approve'] + list(args)

def init(self, *args):
return [self.terraform_bin, 'init'] + list(args)

def show(self, *args):
return [self.terraform_bin, 'show', '-json'] + list(args)

def refresh(self, *args):
return [self.terraform_bin, 'refresh'] + list(args)

def destroy(self, *args):
return [self.terraform_bin, 'destroy', '-auto-approve'] + list(args)

def graph(self, *args):
return [self.terraform_bin, 'graph'] + list(args)

def plan(self, *args):
return [self.terraform_bin, 'plan'] + list(args)

然后我们把 resources、outputs、datasources、varaiables 的配置封装到 Template

class Template:
def __init__(self, providers, resources, outputs, datasources, variables, user_variables):
self.providers = providers
self.resources = resources
self.outputs = outputs
self.datasources = datasources
self.variables = variables
self.user_variables = variables

最后定义 Instance 类,提供了实例 initapplyshowrefresh 等方法

import delegator

class Instance:
def __init__(self, name, template, variables=None):
self.name = name
self.template = template
self.variables = variables or dict()
self.states = None
self.status = None
self.terraform_service = TerraformManagerService()
self.shell_output = None

@property
def path(self):
return self.terraform_service.plan_template_dir.joinpath(self.name)

def generate_plan_files(path, providers, resources, outputs, variables, datasources):
if not os.path.exists(path):
os.mkdir(path)

with open(os.path.join(path, 'providers.tf'), 'wb') as f:
data = '\n'.join([_.config for _ in self.template.providers])
f.write(data.encode())

with open(os.path.join(path, 'resources.tf'), 'wb') as f:
f.write(self.template.resources.encode())

# 内容过长省略写入 datasources 和 variables 的代码

def init(self):
self.status = 'INITIALIZATION'
self.save()
self.generate_plan_files()
init_cmd = self.terraform_service.init()
cmd = delegator.run(' '.join(init_cmd), cwd=str(self.path))
self.shell_output = cmd.err or cmd.out
if cmd.return_code != 0:
self.status = 'INITIALIZATION_ERROR'
else:
self.status = 'INITIALIZATION_SUCCEED'

def apply(self):
# 代码太长省略,和 init 方法基本一致
pass

def show(self):
# 代码太长省略,和 init 方法基本一致
pass

完整代码笔者放在 https://gist.github.com/AnyISalIn/7a66c5d08228da0402d19fa3eea6614c

功能验证

from models import Template, Instance, Provider
from service import TerraformManagerService


provider, resources, variables, datasources, outputs = get_data()

openstack_provider = Provider(config=provider)
template = Template(providers=[openstack_provider],
resources=resources,
datasources=datasources,
outputs=outputs,
variables=variables,
user_variables=[
{'key': 'vm_name', 'default': 'test', 'required': False}
]
)

instance = Instance(name='instance-test', template=template, variables=[{'key': 'vm_name', 'value': 'instance1'}])

instance2 = Instance(name='instance-test2', template=template, variables=[{'key': 'vm_name', 'value': 'instance2'}])

instance.init()
instance.apply()

instance2.init()
instance2.apply()
instance.refresh()
instance2.refresh()

print(instance.shell_output)
# output
# openstack_compute_instance_v2.terraform: Creating...
# openstack_compute_instance_v2.terraform: Still creating... [10s elapsed]
# openstack_compute_instance_v2.terraform: Creation complete after 14s [id=16c05ba1-a589-# 441c-b221-fdb1a8fd3f87]
# Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

instance.states['values']['root_module']['resources'][0]['values']['name']
# 'instance1'

instance2.states['values']['root_module']['resources'][0]['values']['name']
# 'instance2'

instance1.states['values']['root_module']['resources'][0]['values']['network'][0]['fixed_ip_v4']
# '10.0.0.9'

instance2.states['values']['root_module']['resources'][0]['values']['network'][0]['fixed_ip_v4']
# '10.0.0.13'

instance2.destroy()

可以看到已经完成了最初的设想,通过 Template 创建多个 Instance,并且对 Instance 进行生命周期的管理

封装 Web Application

笔者这里花了一天时间通过 Flask + flask_restful + MongoDB + VueJS 构建了一个基础的 Web 应用,简单的做了一些权限控制

代码放在 http://github.com/anyisalin/mcam

# quick started

$ git clone https://github.com/anyisalin/mcam

$ cd mcam

$ docker-compose up --build

平台测试

创建模板

image-20191209123744637

创建实例

image-20191209123802762

image-20191209123858059

实例生命周期管理

Apply

image-20191209124154424

Refresh

image-20191209124256521

image-20191209124306934

Upgrade

image-20191209124327860

image-20191209124642733

destroy

image-20191209124833024

需要改进

虽然功能基本都已经完成了,但是目前整个平台还是存在一些问题,如果能把下面这些功能实现平台会更加的健壮

  • 对 Terraform 配置文件的解析,提供更优雅的配置方式
  • Instance 没有和 Template 完全隔离,所以目前不支持 Template 中配置的更改
  • 实例每次 Upgrade 后 Plan 的版本控制,如果实现了这个功能,就可以针对每次变更记录进行回滚了
  • 目前 Terraform state 存储到本地,需要用更好的方式管理 Terraform state
  • 支持 Terraform module
  • 使用 terragrunt 让 Terraform 变得更加强大

总结

这个平台花了笔者一个周末的时间,在技术上并没有什么难度,牛逼的是 Terraform,但是通过简单的封装解决了 Terraform Plan 重用的问题,并且构建成一个可以使用的最小平台还是很有意思的。