KubeVela篇14:如何实现存量业务的基础设施导入Kubevela+Terraform

原创 吴就业 143 0 2023-10-13

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/69b80bc6a84c45aca93cc689048a2b95

作者:吴就业
链接:https://wujiuye.com/article/69b80bc6a84c45aca93cc689048a2b95
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

云原生企业级实战笔记专栏

由于我们的使用场景是将基础设施资源定义成KubeVela的组件,一个terraform “module”对应的就是一个kubevela的组件,对应terraform-controller的一个Configuration资源。因此导入的最小粒度是组件,即一个terraform “module”。

Terraform的terraform import

terraform原生支持一种导入资源的方式: terraform import, 用于将资源信息导入到state文件中。

命令:terraform import <path> <resource_id>

如果是使用kubevela+terraform-controller,那么实际上我们是没有用terraform的module的,不需要考虑module的导入问题。这也是前面为什么给module加双引号的原因。

这个命令的缺点是,如果一个tf文件中,有多个resource就没办法支持,或者有的resource还是for_each申请的多个资源。

我们想到的办法是,由于一个tf文件中,肯定是只有一个“主”资源,其它是“子”资源,那么是不是可以只import主资源,然后通过datasource将子资源查出来,这样生成的tfstate就包含了子资源的。

举例,一个mycloud-net组件对应的tf代码:

locals {
  db_subnet_ip_cap  = 16
  as_subnet_ip_cap  = 16
  k8s_subnet_ip_cap = 64
  // vpc的ip cap大于等于所有子网的ip cap之和
  vpc_ip_cap        = tomap({
    "aws" : local.as_subnet_ip_cap*4+local.db_subnet_ip_cap*2+local.k8s_subnet_ip_cap*2,
    "aliyun" : local.as_subnet_ip_cap*2+local.db_subnet_ip_cap*2+local.k8s_subnet_ip_cap*2
  })
}

variable "provider_code" {
  type = string
  validation {
    condition     = var.provider_code == "aws" || var.provider_code == "aliyun"
    error_message = "Provider required. aws or aliyun."
  }
}

variable "vpc_name" {
  type = string
  validation {
    condition     = var.vpc_name != ""
    error_message = "VPC name required."
  }
}

resource "mycloud_vpc" "vpc" {
  name          = var.vpc_name
  provider_code = var.provider_code
  // 取值必须是2的n次方
  ip_capacity   = pow(2, ceil(log(local.vpc_ip_cap[var.provider_code], 2)))
}

output "id" {
  value = mycloud_vpc.vpc.global_id
}

output "cidr" {
  value = mycloud_vpc.vpc.cidr
}

locals {
  aws_subnet_names     = toset(["k8s1", "k8s2", "DB1", "DB2", "AS1", "AS2", "AS_OUT1", "AS_OUT2"])
  alibaba_subnet_names = toset(["k8s1", "k8s2", "DB1", "DB2", "AS1", "AS2"])
  subnet_name_map      = tomap({
    "aws" : local.aws_subnet_names,
    "aliyun" : local.alibaba_subnet_names
  })
}

locals {
  // 子网名称 -> 区域索引
  az_map = tomap({
    "k8s1"    = 0, "k8s2" = 1,
    "DB1"     = 0, "DB2" = 1,
    "AS1"     = 0, "AS2" = 1,
    "AS_OUT1" = 0, "AS_OUT2" = 1
  })
  // 子网名称 -> ip cap
  subnet_ip_cap_map = tomap({
    "DB1"     = local.db_subnet_ip_cap, "DB2" = local.db_subnet_ip_cap,
    "AS1"     = local.as_subnet_ip_cap, "AS2" = local.as_subnet_ip_cap,
    "AS_OUT1" = local.as_subnet_ip_cap, "AS_OUT2" = local.as_subnet_ip_cap,
    "k8s1"    = local.k8s_subnet_ip_cap, "k8s2" = local.k8s_subnet_ip_cap
  })
}

data "mycloud_available_zones" "azs" {
  provider_code = var.provider_code
}

resource "mycloud_subnet" "subnet" {
  for_each    = local.subnet_name_map[var.provider_code]
  name        = each.key
  ip_capacity = local.subnet_ip_cap_map[each.key]
  vpc_id      = mycloud_vpc.vpc.id
  zone_id     = data.mycloud_available_zones.azs.available_zones[local.az_map[each.key]].zone_id
}

resource "mycloud_vpc_access_request" "access" {
  vpc_id          = mycloud_vpc.vpc.id
  access_requests = [
  for subnetName in local.subnet_name_map[var.provider_code] : {
    subnet_id      = irock_subnet.subnet[subnetName].id
    from_internet  = true
    object_storage = true
    to_internet    = true
  }
  ]
}

用于导入tfstate的tf代码:

resource "mycloud_vpc" "vpc" {
}

data "mycloud_subnet" "subnet" {  
  vpc_id = mycloud_vpc.vpc.id
}

data "mycloud_vpc_access_request" "access" {  
  vpc_id = mycloud_vpc.vpc.id
}

使用导入tf,执行terraform import mycloud_vpc.vpc 这里填id命令,就可以得到一个tfstate文件,但这个tfstate与预期的不符。因为mycloud-net组件的tf中,mycloud_vpc_access_request.access是一个resource,而在导入tf中,它是一个datasource,mycloud_subnet.subnet同理。因此我们还需要修改tfstate文件,将状态文件中记录的mycloud_vpc_access_request.access、mycloud_subnet.subnet由datasource转为resource。

其实只要对比一下用mycloud-net组件的tf执行terraform apply命令生成的tfstate文件,和用导入tf执行terraform import命令生成的tfstate比较一下,就能看出差异,就能知道怎么改。

将状态文件中记录的mycloud_vpc_access_request.access、mycloud_subnet.subnet由datasource转为resource的转换代码如下:

func ConverterTFState(importState *tfstate.TFState) (outState *tfstate.TFState, err error) {
    outState = &tfstate.TFState{}
    outState.Version = importState.Version
    outState.TerraformVersion = importState.TerraformVersion
    outState.Serial = importState.Serial
    outState.Lineage = importState.Lineage
    outState.Outputs = map[string]interface{}{}
    outState.CheckResults = nil     outState.Resources = []tfstate.Resource{}
    for _, r := range importState.Resources {
        var nr tfstate.Resource
        // mode是managed说明是resource,否则是datasource
        if r.Mode == "managed" {
            nr = r
        } else {
            // 转换mycloud_subnet
            if r.Type == "mycloud_subnet" {
                // 转换为managed
                nr = tfstate.Resource{
                    Mode:      "managed",
                    Type:      r.Type,
                    Name:      r.Name,
                    Provider:  r.Provider,
                    Instances: tfstate.Instances{},
                }
                subnets := r.Instances[0].Attributes["subnets"].([]interface{})
                for _, s := range subnets {
                    ss := s.(map[string]interface{})
                    idxKey := ss["name"]
                    ni := tfstate.Instance{
                        IndexKey:            idxKey,
                        SchemaVersion:       r.Instances[0].SchemaVersion,
                        Attributes:          ss,
                        SensitiveAttributes: nil,
                        Dependencies:        []string{"data.mycloud_available_zones.azs", "mycloud_vpc.vpc"},
                    }
                    nr.Instances = append(nr.Instances, ni)
                }
            } else if r.Type == "mycloud_vpc_access_request" { // 转换mycloud_vpc_access_request
                // 转换为managed
                nr = tfstate.Resource{
                    Mode:      "managed",
                    Type:      r.Type,
                    Name:      r.Name,
                    Provider:  r.Provider,
                    Instances: tfstate.Instances{},
                }
                requests := r.Instances[0].Attributes["vpc_access_requests"].([]interface{})
                if len(requests) > 0 {
                    ni := tfstate.Instance{
                        SchemaVersion:       r.Instances[0].SchemaVersion,
                        Attributes:          requests[0].(map[string]interface{}),
                        SensitiveAttributes: nil,
                        Dependencies:        []string{"mycloud_vpc.vpc"},
                    }
                    nr.Instances = append(nr.Instances, ni)
                }
            }
        }
        outState.Resources = append(outState.Resources, nr)
    }
    return outState, nil }

代码不理解没关系,重点是方法。我们可以自己找一个资源,通过apply和import生成两个tfstate(一个是正常走申请生成的tfstate,一个是通过import生成的tfstate),然后对比一下这两个tfstate的内容,就知道差异在哪,也就知道应该怎么改了。

基础设施资源导入Operator

实现terraform-import Operator,自定义TerraformResourceImport CRD,并实现TerraformResourceImport资源的控制器。

CRD使用模版(TerraformResourceImport):

apiVersion: mycloud.com/v1beta1
kind: TerraformResourceImport
metadata:
  name: tri-$(组件名)
  namespace: default # cr安装在哪个namespace,组件就安装在哪个namespace
spec:
  componentName: $(组件名)
  componentType: mycloud-net
  mainResourceGlobalId: vpc-xxxx
  providerRef: 
    namespace: default
    name: xxx
  writeConnectionSecretToRef:
    namespace: default
    name: yyy
status:
  phase: Completed
  snapshot: $MD5(spec)
  conditions: 

TerraformResourceImport的Status的phase:

  1. ImportTFStateFileProgress:导入生成资源状态文件中。
  2. GeneratorConfigurationProgress:生成Configuration资源,等待terraform controller处理Configuration资源执行terraform apply成功。
  3. Completed:导入成功。
  4. Failed:导入失败。

添加一个controller监听CR的创建/更新,实现下面逻辑:

一、ImportTFStateFileProgress状态:

1.1 根据组件类型生成导入tf代码,如本文的mycloud-net组件的导入案例。

      resource "mycloud_vpc" "vpc" {
      }
      
      data "mycloud_subnet" "subnet" {  
        vpc_id = mycloud_vpc.vpc.id
      }
      
      data "mycloud_vpc_access_request" "access" {  
        vpc_id = mycloud_vpc.vpc.id
      }

1.2 创建Job执行terraform import命令,参考terraform controller执行terraform apply命令的实现。

1.3 转换导入生成的tfstate,将datasource转为resource。

二、GeneratorConfigurationProgress:根据组件类型,从tfstate扣取输入变量,创建Configuration资源。(需要代码里处理每种resource需要那些输入变量),等待terraform controller处理Configuration资源执行terraform apply成功。

三、Failed:md5(spec)对比snapshot,如果spec有更新,则资源状态重置为ImportTFStateFileProgress阶段,且更新snapshot。

自定义terraform docker镜像

由于terraform-controller使用的terraform docker镜像使用的terraform版本太低,import不支持把data也渲染出来,所以需要自己打一个terraform docker镜像。另外,由于高版本import出来的tf,用低版本apply也会有问题,同样需要重新安装terraform-controller addon插件,指定terraform-controller使用我们自己打的terraform docker镜像。

由于terraform-controller不支持我们配置用于拉取私有镜像的Secret,因此需要将自己打的terraform docker镜像推送到公开的镜像仓库。

自定义terraform docker镜像的Dockerfile如下:

FROM --platform=linux/amd64 hashicorp/terraform:1.4.5
RUN \
  apk update && \
  apk add bash
VOLUME ["/data"]
WORKDIR /data

总结

由于一个tf文件中,肯定是只有一个“主”资源,其它是“子”资源,所以我们可以只import主资源,然后通过datasource将子资源查出来,这样生成的tfstate就包含了子资源的。

但这个tfstate是有问题的,我们还需要做一下转化。通过apply和import生成两个tfstate(一个是正常走申请生成的tfstate,一个是通过import生成的tfstate),对比这两个tfstate的内容,找出差异,这些差异是有规律的,然后写代码实现将import生成的tfstate转换为apply生成的tfstate。

#云原生

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

极致成本优化背景下,如何通过优化k8s调度器实现计算资源的按需付费(一)

在降低增笑的大背景下,如何在保证稳定性的前提下,做到极致压缩k8s资源的使用,实现基础设施真正的按需付费,是我们云原生项目的目标之一。要实现如Railway这种产品的基础设施按实际使用付费,不能简单的使用云Serverless产品,因为这些产品都有最低限额的要求,例如阿里云最低限制要求Pod至少0.25cpu+0.5g内存,但其实大多数应用这个配额都用不到,大量的时间cpu负载几乎为0,内存消耗可能也就几十M(Java应用除外),大量的低使用率的Pod会造成资源的浪费。

云原生项目用于验证负载感知调度的go-web-demo

我们在做云原生调度技术调研的时候,为了做实验获取一些数据,需要编写一个demo,支持动态模拟cup使用率和内存使用,所以用go开发了这么一个web程序。

KubeVela完结篇:我为Terraform Controller贡献了3个PR

KubeVela于2020年年底开源,距离现在还未满三年时间,是一个非常年轻的产物。KubeVela是非常创新的产物,如OAM模型的抽象设计。所以也并未成熟,除了官方文档,找不到更多资料,在使用过程中,我们也遇到各种大大小小的问题。

KubeVela篇13:跨地域的多集群管理方案

随着公司全球化战略的布局,业务呈点状分布在亚太、美东、欧洲等多个地域,云原生kubevela在跨地域多集群管控方面也遇到网络上的互通问题。

KubeVela篇12:自定义工作流步骤以及踩坑经验

官方提供的工作流步骤有限,另外,对于自研的PaaS平台,我们需要借助工作流步骤实现一些例如存量项目基础设施导入、项目环境初始化、平台组件共享基础设施需要解决的差异对比审核、基础设施漂移等。

KubeVela篇11:可持续测试应用部署之Mock基础设施

我们基于KubeVela开发的云原生应用交付平台,提供如初始化基础设施导入、中间件部署共用基础设施等相关能力的测试,需要依赖基础设施。虽然terraform是面向公司内部的混合云平台,但是测试都要跨部门配置效率太低了,而且这种模式无法支持持续测试。