将(行星)地球化(以适合人类居住)是什么
terra form(https://www。terra形态。io/)是哈希公司旗下的一款开源(去吧语言开发)的DevOps基础架构资源管理运维工具,可以看下对应的DevOps工具链:
将(行星)地球化(以适合人类居住)可以安全高效的构建、更改和合并多个云厂商的各种服务资源,当前支持有阿里云、AWS、微软Azure、Vmware、谷歌云平台等多个云厂商云产品的资源创建。
以代码形式编写、规划和创建基础架构
将(行星)地球化(以适合人类居住)通过模板配置文件定义所有资源类型(有如主机,操作系统,存储类型,中间件,网络VPC,SLB,DB,缓存等)和资源的数量、规格类型、资源创建依赖关系,基于资源厂商的OpenAPI快速创建一键创建定义的资源列表,同时也支持资源的一键销毁。
顺便介绍一下哈希公司这家公司的其他产品:
哈希公司的流浪者
领事哈希公司领事-连接和保护任何服务
管理机密保护敏感数据
游牧企业
哈希公司打包机
将(行星)地球化(以适合人类居住)初体验
接下来,我们就安装并体验一下地形。
安装CentOS 7安装接下来在CentOS 7上面进行安装,如下:
sudo yum install-y yum-utilssudo yum-config-manager-add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.reposudo yum-y install terraform验证版本信息:
linux_amd64上的[root @ mybox 02 ~]# terraform版本terraform v 1。0 .2
马科斯安装如果是在马科斯上面安装,则执行如下命令:
$ brew安装平台验证版本信息:
$ terra表单版本terra表单v 1。0 .3在达尔文amd64
获取命令行帮助如何获取命令行帮助呢?
# 获取帮助信息,查看将(行星)地球化(以适合人类居住)支持哪些子命令及参数地形-帮助#查看具体某个子命令的帮助信息地形-帮助计划#开启命令行补全地形-安装-自动完成
创建一台阿里云精英公司实例准备子账号创建随机存取存储子账户,子账户只能通过OpenAPI的形式访问云上的资源,而且不能赋予所有的权限,只赋予指定的权限,如ECS、RDS、SLB等具体的权限。
推荐使用环境变量的方式存放身份认证信息:
export ALICLOUD _ ACCESS _ KEY=' * * * * * * * * ' export ALICLOUD _ SECRET _ KEY=' * * * * * * * * * * * * * * * * * * export ALICLOUD _ REGION=' cn-Shanghai '
一段代码下面是一段测试的代码main.tf,其主要功能是在阿里云上创建VPC,虚拟开关、安全组、ECS实例,最后输出精英公司的外网知识产权。代码如下:
资源阿里云_ VPC ' ' VPC ' { name=' TF _ test _ foo ' CIDR _ block=' 172。16 .0 .0/12 ' }资源阿里云v开关' ' vsw ' { VPC _ id=阿里云_ VPC。VPC。idcidr _ block=' 172。16 .0 .0/21 ' avail ability _ zone=' cn-Shanghai-b ' }资源alicloud _ security _ group ' ' default ' { name=' default ' VPC _ id=alicid instance _ type=' ECS。N2。小' system _ disk _ category=' cloud _ efficiency ' image _ id=' Ubuntu _ 18 _ 04 _ 64 _ 20G _ alibase _ 2019 06 24。vhd ' instance _ name=' test _ foo ' v switch _ id=alicloud _ v switch。vsw。id internet _ max _ bandwidth _ out=1 password='您的密码' } output ' public _ IP ' { value=alicloud _ instance。实例。public _ IP }
我们定义了ECS(镜像,实例类型),VPC(CIDR,VPC名),安全组等。通过main.tf文件(只有。需要tf文件),通过Terraform分析资源配置参数,调用阿里云OpenAPI验证资源的创建,同时将整个资源创建变成一个. tf.state文件,基于这个文件我们可以知道资源创建的所有信息。
检查结果$ terraform显示
你怎么想呢?我们已经通过代码创建了基础设施,创建的资源如我们的语句文件中所述。这实际上是基础设施作为代码的技术实现。
基础设施作为代码是一种使用新技术来构建和管理动态基础设施的方法。它将基础设施、工具和服务以及基础设施本身的管理视为一个软件系统,并采用软件工程实践以结构化和安全的方式管理系统的变更。
基础设施作为代码有四个关键原则:
再现性:环境中的任何元素都可以很容易地被复制。一致性:在任何时候,所创建的环境的每个元素的配置都是完全相同的。快速反馈:能够频繁且轻松地进行更改,并迅速知道更改是否正确。可见性:对环境的所有更改都应该易于理解、可审计并且在版本控制之下。下一章将特别关注Terraform是如何编程和扩展的。
地形规划
如果写代码遇到什么问题,可以在官网找到答案。官网是最好的学习资源,没有之一。
文档链接是:https://www.terraform.io/language.让我们看一个例子。
变量定义示例:
#列表变量示例' list _ example' {description='地形中的列表示例' type=' list' default=[1,2,3]}#字典变量示例' map _ example' {description='地形中的地图示例' type=' map ' default={ key 1=' value 1 ' key 2=' value 2 ' key 3=' value 3。}} #如果没有指定类型,默认为字符串变量“server _ port”{ description='服务器将用于http请求的端口' }
一个例子:
$ tree -L 1.madlibsmadlibs.tfmadlibs.ziptemplatesterraform.tfstateterraform.tfstate.backupterra form . tfvars 2目录,5个文件
查看terraform.tfvars文件的内容:
[root @ blog ch03]# cat terra form . tfvarswords={名词=['军队''黑豹''核桃''三明治''宙斯''香蕉''猫''水母''竖锯''小提琴''牛奶''太阳']形容词=['苦''黏黏''打雷''丰富''小胖''暴躁']动词=['跑''跳舞''爱''尊重''踢''烤']副词=['精致''漂亮''迅速''真实''疲倦']数字=[42,27,101,73,-5,0]}
和madlibs.tf文件的内容。
[root @ blog ch03]# cat madlibs。tfterraform { required _ version='=0.15 ' required _ providers { random={ source=' hashi corp/random ' version=' ~ 3.0 ' } local={ source=' hashi corp/local ' version=' ~ 2.0 ' } } archive={ source=' hashi corp/archive ' version=' ~ 2.0 ' } } variable ' words“{ description=”一个用于疯狂的图书馆的词库类型=对象({名词=列表(字符串),形容词=object} }变量num _ files“{ type=number description=”(可选)描述您的变量default=100 } locals { upper case _ words={ for k,v in var。 words:k=[for s in v:upper(s)] } }资源random _ shuffle ' ' random _ nons ' { count=var。num _ files输入=本地。大写_ words[' nons ']}资源random _ shuffle ' ' random _形容词{ count=var。num _ files输入=本地。大写字母_单词['形容词']}资源随机洗牌' '资源txt '))}资源local _ file ' ' mad _ libs ' { count=var。num _ files filename=' mad libs/mad libs-$ { count。index } .' txt ' content=模板文件(元素(本地。templates,count.index),{ nons=random _ shuffle。random _ nons[计数。索引].结果形容词=随机_洗牌。random _ adjusts[count。索引].结果动词=随机_洗牌。random _ verbs[count。索引].结果副词=random _ shuffle.random _副词[计数.索引].结果编号=随机_洗牌。随机数[计数。索引].result })} data ' archive _ file ' ' mad _ libs ' { depends _ on=[local _ file。mad _ libs]type=' zip ' source _ dir=' $ { path。module }/mad libs ' output _ path=' $ { path。CWD }/疯狂的图书馆。zip ' }如何引用变量的值?var .变量名称。
注意事项将(行星)地球化(以适合人类居住)不支持自定义函数。我们只能使用将(行星)地球化(以适合人类居住)内置的大约100 个函数进行编程。
重复自己vs。不重复自己(干)
在软件工程中,是不提倡干燥的的。可是现实中,我们到处可以看到Ctrl-C及Ctrl-V的这样的编程范式(:P)。
下面两个代码示例,哪个更好一点?
从上图来看,左边的代码结构是最优的,其符合干燥的原则;而右边的代码结构则是符合Ctrl-C、Ctrl-V这种模式。针对上述两种场景,我们给出以下建议:
当我们的环境没有差异或差异比较小,建议使用左边的代码结构;当我们的环境差异比较大的时候,就只能选择右边的代码结构了;
接下来我们看看将(行星)地球化(以适合人类居住)是如何解决上述问题的。
使用工作空间以复用代码对于同样的配置文件,工作空间功能允许我们可以有多个状态文件。这就意味着,我们不需要通过复制、粘贴代码,就可以实现多环节部署。每个工作空间拥有自己的变量及环境信息。如下图所示:
我们之前就已经在使用工作空间了,尽管我们并没有意识到这一点。当执行地形初始化的时候,地形就已经创建了一个默认的工作空间并切换到该工作空间下了。可以验证一下:
[root@blog ch03]#地形工作空间列表*默认
多环境部署接下来我们使用将(行星)地球化(以适合人类居住)的工作空间特性进行一个多环境的部署。
一个例子:[root@blog ch06]# tree.环境dev。TF varsprod.tfvarsmain.tfterra表单。TF状态。ddevterra表单。TF stateterra表单。TF状态。backupprodterra表单。TF状态4个目录,6个文件
看下代码(如何在什么都没有的情况下,进行代码的调试及验证):
[root @ blog ch06]#猫主。TF变量“Region”description=“My Test Region”type=string } output“My workspace”value={ Region=var。区域工作空间=地形。工作区} }[root @ blog ch06]# cat environments/dev。tfvarsregion=' cn-Shanghai-dev '[root @ blog ch06]# cat environments/prod。tfvarsregion=' cn-Shanghai-prod '
我们切换到偏差的工作空间下面,然后执行代码:
[root @ blog ch06]# terra表单工作区选择开发切换到工作区“开发”.[root @ blog ch06]# terra form apply-var-file=./环境/开发。TF变量-自动批准无更改。您的基础设施与配置相匹配Terraform将您的实际基础设施与您的配置进行了比较,没有发现任何差异,因此不需要进行任何更改。应用完成!资源:添加了0个,更改了0个,销毁了0个。输出:我的工作区={ ' region '=' cn-Shanghai-dev ' ' workspace '=' dev ' }
切换到刺针的工作空间下面,然后验证代码:
[root@blog ch06]#地形工作空间选择产品切换到工作空间\”产品\”。[root @ blog ch06]# terra form apply-var-file=./环境/产品。TF变量-自动批准无更改。您的基础设施与配置相匹配Terraform将您的实际基础设施与您的配置进行了比较,没有发现任何差异,因此不需要进行任何更改。应用完成!资源:添加了0个,更改了0个,销毁了0个。输出:我的工作区={ ' region '=' cn-Shanghai-prod ' ' workspace '=' prod ' }
当执行破坏的时候呢?也是同样的情况,需要指定变量文件:
[root @ blog ch06]# terra form destroy-var-file=./环境/产品。TF vars-自动批准对输出的更改:-我的工作空间={-region=' cn-Shanghai-prod '-工作空间=' prod ' }-null您可以应用此计划将这些新的输出值保存到将(行星)地球化(以适合人类居住)状态,而无需更改任何实际的基础结构。摧毁完成!资源:0已销毁。多使用输出对代码进行调试将(行星)地球化(以适合人类居住)的输出很像其他编程语言中的:printf、print、echo等函数,让我们把感兴趣的内容给打印出来,以便及时验证。
# 关于操作工作空间的命令## 查看当前有哪些工作空间[root@blog ch06]#地形工作空间列表默认开发*产品##创建一个名为uat的工作区[root@blog ch06]#地形工作区新uat已创建并切换到工作区“UAT”!## 切换到名为偏差的workspace[root @ blog ch06]# terra form workspace select dev切换到workspace ' dev '## 删除名为uat的工作区[root@blog ch06]#地形工作区删除uat已删除工作区UAT!
多云部署
本节会根据下面的流程图进行多云部署。
主要实现思路是:
在providers.tf文件中,指定多个云厂商的提供商;以模块的形式操作每个云厂商的云上资源;
接下来直接看代码结构:
[root @ blog part 1 _混合云-lb]# tree.bootstrap.shmain.tfoutputs.tfproviders.tf版本。tf0目录,5个文件
我们看一下providers.tf文件(代码中要指定多个提供商):
提供商' AWS ' { profile=' profile ' region=' us-west-2 ' }提供商' azure RM ' { features { } }提供商' Google ' { project=' project _ id ' region=' us-east 1 ' }提供商' docker ' { A
再看一下main.tf文件:
模块AWS ' { source=' terra form-in-action/VM/cloud/modules/AWS ' # A environment={ name=' AWS ' # B background _ color=' orange ' # B } }模块Azure“{ source=' terra form-in-action/VM/cloud/modules/Azure ' # A environment={ name=' Azure ' background _ color=' blue ' } }模块GCP ' { source=' terra行动形态/虚拟机/云/模块/GCP ' # A环境={ name={ name
零停机部署ZDD
本节介绍三种实现零停机部署的方案。
Terraform的create_before_destroy元属性蓝绿部署和Ansible marriage。
设置生命周期未设置任何内容时,下图显示了Terraform的默认行为。当一些属性(尤其是一些强制更新的属性,比如实例类型、镜像ID、用户数据等。)被修改,当再次执行apply时,现有资源将被销毁。
资源“aws_instance”实例“{ ami=var . ami instance _ type=var . instance _ type user _ data=-EOF #!/bin/bash mkdir-p/var/www CD/var/www echo ' App v $ { var . version } ' index.html python 3-m http . server 80 EOF }
从下图可以看出,从销毁到新实例完全可用,整体无法对外使用。
为了避免上述情况,生命周期元参数允许我们定制资源的生命周期。生命周期嵌套块存在于所有资源中。我们可以设置以下三个标志:
create _ before _ destroy(bool)——如果设置为true,将在删除旧对象之前创建新资源。当prevent_destroy (bool)——设置为true时,Terraform将拒绝任何会破坏与资源相关联的基础结构对象并导致显式错误的计划。Ignore _ changes(属性名列表)——指定一个资源列表,Terraform在执行计划时会忽略新的执行计划。
在create_before_destroy下面的代码中,设置了create_before_destroy=true。
资源“aws_instance”实例“{ ami=var . ami instance _ type=' T3 . micro '生命周期{ create _ before _ destroy=true } user _ data=-EOF #!/bin/bash mkdir-p/var/www CD/var/www echo ' App v $ { var . version } ' index.html python 3-m http . server 80 EOF }
执行上述代码时,流程图如下:
Create_before_destroy只对托管资源有效,对数据源无效。
0755到79000作者对这个选项的看法:
我不使用create_before_destroy,因为我发现它比它本身更麻烦。
蓝绿色部署在蓝绿色部署期间,我们可以在两个生产环境之间切换:一个称为蓝色环境,另一个称为绿色环境。在任何给定时间,只有一个生产环境是活动的。路由器将流量定向到实时环境,该环境可以是负载平衡器或DNS解析器。每当您想要部署到生产环境时,请先部署到空闲环境。然后,当我们准备好之后,将路由器从指向实时服务器切换到指向空闲服务器——,该服务器已经在运行最新版本的软件。这种切换称为切换,可以手动或自动完成。当流量转换完成后,空闲服务器将成为新的实时服务器,以前的活动服务器现在是空闲服务器(如下图所示)。
我们来看一个例子,流程图如下:
代码如下:green_blue.tf:
provider ' AWS ' { region=' us-west-2 ' } variable ' production ' { default=' Green '//deploy Green environment } module ' base ' { source=' terra form-in-action/AWS/blue Green/modules/auto scaling ' app _ version=' v 1.0 ' label=' Green ' base=module . base } module ' blue ' { source=' terra form-in-action/AWS/AWS
蓝绿环境切换当蓝色环境全部启动后,可以在蓝色和绿色之间切换。代码如下:green_blue.tf:
provider ' AWS ' { region=' us-west-2 ' } variable ' production ' { default=' blue ' } module ' base ' { source=' terra form-in-action/AWS/blue green/modules/auto scaling ' app _ version=' v 1.0 ' label=' green ' base=module。base }模块'蓝色' { source=' terra行动形式/AWS/蓝绿色/生产
将(行星)地球化(以适合人类居住)与Ansible联姻我们需要冷静下来思考一个问题:'地形是适合该工作的工具吗?在许多情况下,答案是否定的。对于伏特计上的应用程序部署,配置管理工具将会更适合。接下来,我们让专业的工具做其专业的事情将(行星)地球化(以适合人类居住)专注于基础设施这一块,用于基础设施的快速交付。而对于上层的应用部署,地形则有点不擅长了。
接下来,以自动警报系统为例,地形负责基础设施的创建,Ansible负责创建其上的应用。流程图如下:
代码如下:
提供者AWS“{ region=' us-west-2 ' }资源TLS _ private _ key ' ' key ' { algorithm=' RSA ' }资源local _ file“”private _ key“{ filename=”$ { path。模块}/ansi ble-key。PEM ' sensitive _ content=TLS _ private _ key。钥匙。private _ key _ PEM file _ permission=' 0400 ' }资源AWS _ key _ pair ' ' key _ pair ' { key _ name=' ansi ble-key ' public _ key=TLS _ private egress { from _ port=0 to _ port=0 protocol='-1 ' CIDR _ blocks=[' 0。0 .0 .0/0 ']} }数据' AWS _ ami ' ' Ubuntu ' { most _ recent=true filter { Name=' Name ' values=[' Ubuntu/images/hvm-SSD/Ubuntu-focal-20.04-amd64-Server-*]}所有者=[' 099720109477 ']}资源' AWS _ instance ' ' ansi ble sudo apt install-y ansi ble ']连接{ type=' ssh ' user=' Ubuntu ' host=self。public _ IP private _ key=TLS _ private _ key钥匙。private _ key _ PEM } } provisioner ' local-exec ' { command=' ansi ble-playbook-u Ubuntu-key-file ansi ble-key。PEM-T 300-I ' $ { self。public _ IP }、'app。yml ' } } output ' public _ IP ' { value=AWS _ instance .
app.yml的内容为:
-name:Install Nginxhosts:all begin:true tasks:-name:Install Nginx yum:name:Nginx state:present-name:Add index page template:src:index . html dest:/var/www/html/index。html-name:Start Nginx service:name:Nginx state:started
执行上述代码:
$ terraform初始化将(行星)地球化(以适合人类居住)应用-自动批准.AWS _ instance。ansi ble _ server:2m7s[id=I-06774 a 7635d 4581 AC]应用完成后创建完成!资源:添加了5个,更改了0个,销毁了0个。输出:ansi ble _ command=ansi ble-playbook-u Ubuntu-key-file ansi ble-key。PEM-T300-I ' 54。245 .143 .100,'app。yml公共_ IP=54.245.143.100当要进行蓝绿割接的时候,只需要再次执行一下上述ansible命令即可:
$ ansi ble-playbook \\-u Ubuntu \\-key-file ansi ble-key。PEM \\-T 300 \\-I ' 54。245 .143 .100 ' app。yml
如何写一个供应者
当现有的提供商不能满足你的需求或者根本没有你想要的提供商时,该怎么办?我该怎么办?我该怎么办?那就是写一个出来。这样,我们可以通过Terraform以基础设施或代码的形式管理我们的远程API。换句话说,只要有RESTful API,理论上我们可以通过Terraform来管理。
接下来,本节将介绍如何编写一个提供者。我们先来看看Terraform的工作流程。
Terraform如何与提供商互动?Terraform和Provider之间的交互如下图所示:
Terraform官网也有一个非常详细的关于如何开发插件的文档。文档链接:https://www.terraform.io/plugin.这里有两点需要注意:
必须有一个远程(或上游)API;(可以是任何语言写的API)客户端SDK操作这个API的;(Golang客户端。因为提供者是由Go编写的,所以都应该有一个Go客户端SDK)
首先,有一个RESTful API。让我们来看看代码的目录结构。代码来自《Terraform Up and Running》的第11章,我们对其进行了修改。代码中使用了AWS Lambda函数计算,这里删除了相关代码,使其可以在任何环境下运行。
代码中使用了ORM,这是一个由中国人开发的ORM框架朱槿。官方网站的地址是Gorm-Golang的奇妙orm库,旨在方便开发者。使用的web框架是go-gin,在官网的地址是Gin Web Framework (gin-gonic.com)。
然后看看目录结构和代码:
我的宠物商店git:(dev) tree。 README.md行动 宠物 create . godelete . goget . golist . go update.gogo.modgo.summain.go模型宠物模型. goORM . goterraform-pet store 4个目录,12个文件
代码规模比较小,是经典的MVC开发模式。我们先来看看模型的定义。
模型定义//model/pet/model . go package pet type pet struct { id string ` gorm:' primary _ key ' JSON:' id ' ` name string ` JSON:' name ' ` categories string ` JSON:' categories ' ` age int ` JSON:' age ' ` }
服务定义//model/Pet/ORM . go package Pet import(' fmt ' ' github . com/朱槿/gorm')//create在数据库func create (db * gorm.db,pet *Pet) (string,error) { err :=db。创造(宠物)。Error if err!=nil { return ' 'err } return pet。Id,nil}//FindById返回给定ID的宠物,如果没有找到则返回nil func find byid(db * gorm。DB,id字符串)(*Pet,error) { var pet Pet err :=db。Find(pet,Pet{ID: id})。Error if err!=nil { return nil,err } return pet,nil}//FindByName返回具有给定名称的宠物,如果没有找到则返回nil func find byname(db * gorm。DB,名称字符串)(*Pet,error) { var pet Pet err :=db。Find(宠物,宠物{Name: name})。Error if err!=nil { return nil,err } return pet,nil}//List r
eturns all Pets in database, with a given limitfunc List(db *gorm.DB, limit uint) (*[]Pet, error) { var pets []Pet err := db.Find(&pets).Limit(limit).Error if err != nil { return nil, err } return &pets, nil}//Update updates a pet in the databasefunc Update(db *gorm.DB, pet *Pet) error { err := db.Save(pet).Error return err}//Delete deletes a pet in the databasefunc Delete(db *gorm.DB, id string) error { pet, err := FindById(db, id) if err != nil { fmt.Printf(\”1:%v\”, err) return err } err = db.Delete(pet).Error fmt.Printf(\”2:%v\”, err) return err}
控制器定义Get(查看一个资源)// action/pets/get.gopackage petsimport ( \”github.com/jinzhu/gorm\” \”github.com/TyunTech/terraform-petstore/model/pet\”)//GetPetRequest request structtype GetPetRequest struct { ID string}//GetPet returns a pet from databasefunc GetPet(db *gorm.DB, req *GetPetRequest) (*pet.Pet, error) { p, err := pet.FindById(db, req.ID) res := p return res, err}
List(查看所有资源)// action/pets/list.gopackage petsimport ( \”github.com/jinzhu/gorm\” \”github.com/TyunTech/terraform-petstore/model/pet\”)//ListPetRequest request structtype ListPetsRequest struct { Limit uint}//ListPetResponse response structtype ListPetsResponse struct { Items *[]pet.Pet `json:\”items\”`}//ListPets returns a list of pets from databasefunc ListPets(db *gorm.DB, req *ListPetsRequest) (*ListPetsResponse, error) { pets, err := pet.List(db, req.Limit) res := &ListPetsResponse{Items: pets} return res, err}
Create(创建一个资源)// action/pets/create.gopackage petsimport ( \”github.com/google/uuid\” \”github.com/jinzhu/gorm\” \”github.com/TyunTech/terraform-petstore/model/pet\”)//CreatePetRequest request structtype CreatePetRequest struct { Name string `json:\”name\” binding:\”required\”` Species string `json:\”species\” binding:\”required\”` Age int `json:\”age\” binding:\”required\”`}//CreatePet creates a pet in databasefunc CreatePet(db *gorm.DB, req *CreatePetRequest) (*pet.Pet, error) { uuid, _ := uuid.NewRandom() newPet := &pet.Pet{ ID: uuid.String(), Name: req.Name, Species: req.Species, Age: req.Age, } id, err := pet.Create(db, newPet) p, err := pet.FindById(db, id) res := p return res, err}
Update(更新一个资源)// action/pets/update.gopackage petsimport ( \”fmt\” \”github.com/jinzhu/gorm\” \”github.com/TyunTech/terraform-petstore/model/pet\”)//UpdatePetRequest request structtype UpdatePetRequest struct { ID string Name string `json:\”name\”` Species string `json:\”species\”` Age int `json:\”age\”`}//UpdatePet updates a pet from databasefunc UpdatePet(db *gorm.DB, req *UpdatePetRequest) (*pet.Pet, error) { p, err := pet.FindById(db, req.ID) if err != nil { return nil, err } if len(req.Name) > 0 { p.Name = req.Name } if req.Age > 0 { p.Age = req.Age } if len(req.Species) > 0 { p.Species = req.Species } fmt.Printf(\”requested: %v\”, p) err = pet.Update(db, p) if err != nil { return nil, err } p, err = pet.FindById(db, req.ID) fmt.Printf(\”new: %v\”, p) res := p return res, err}
Delete(删除一个资源)// action/pets/delete.gopackage petsimport ( \”github.com/jinzhu/gorm\” \”github.com/TyunTech/terraform-petstore/model/pet\”)//DeletePetRequest request structtype DeletePetRequest struct { ID string}//DeletePet deletes a pet from databasefunc DeletePet(db *gorm.DB, req *DeletePetRequest) (error) { err := pet.Delete(db, req.ID) return err}
main 入口
代码中,我们去掉了多余的注释及 AWS 的 Lambda 相关代码,使其可以运行在任何环境。
package mainimport ( \”fmt\” \”net/http\” \”os\” \”strconv\” \”github.com/gin-gonic/gin\” \”github.com/jinzhu/gorm\” _ \”github.com/jinzhu/gorm/dialects/mysql\” \”github.com/TyunTech/terraform-petstore/action/pets\” \”github.com/TyunTech/terraform-petstore/model/pet\”)var db *gorm.DBfunc init() { initializeRDSConn() validateRDS()}func initializeRDSConn() { user := os.Getenv(\”rds_user\”) password := os.Getenv(\”rds_password\”) host := os.Getenv(\”rds_host\”) port := os.Getenv(\”rds_port\”) database := os.Getenv(\”rds_database\”) dsn := fmt.Sprintf(\”%s:%s@tcp(%s:%s)/%s\”, user, password, host, port, database) var err error db, err = gorm.Open(\”mysql\”, dsn) if err != nil { fmt.Printf(\”%s\”, err) }}func validateRDS() { //If the pets table does not already exist, create it if !db.HasTable(\”pets\”) { db.CreateTable(&pet.Pet{}) }}func optionsPetHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) c.Header(\”Access-Control-Allow-Methods\”, \”GET, POST, DELETE\”) c.Header(\”Access-Control-Allow-Headers\”, \”origin, content-type, accept\”)}func main() { r := gin.Default() r.POST(\”/api/pets\”, createPetHandler) r.GET(\”/api/pets/:id\”, getPetHandler) r.GET(\”/api/pets\”, listPetsHandler) r.PATCH(\”/api/pets/:id\”, updatePetHandler) r.DELETE(\”/api/pets/:id\”, deletePetHandler) r.OPTIONS(\”/api/pets\”, optionsPetHandler) r.OPTIONS(\”/api/pets/:id\”, optionsPetHandler) r.Run(\”:8000\”)}func createPetHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) var req pets.CreatePetRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{\”error\”: err.Error()}) return } res, err := pets.CreatePet(db, &req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{\”error\”: err.Error()}) return } c.JSON(http.StatusOK, res) return}func listPetsHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) limit := 10 if c.Query(\”limit\”) != \”\” { newLimit, err := strconv.Atoi(c.Query(\”limit\”)) if err != nil { limit = 10 } else { limit = newLimit } } if limit > 50 { limit = 50 } req := pets.ListPetsRequest{Limit: uint(limit)} res, _ := pets.ListPets(db, &req) c.JSON(http.StatusOK, res)}func getPetHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) id := c.Param(\”id\”) req := pets.GetPetRequest{ID: id} res, _ := pets.GetPet(db, &req) if res == nil { c.JSON(http.StatusNotFound, res) return } c.JSON(http.StatusOK, res)}func updatePetHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) var req pets.UpdatePetRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{\”error\”: err.Error()}) return } id := c.Param(\”id\”) req.ID = id res, err := pets.UpdatePet(db, &req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{\”error\”: err.Error()}) return } c.JSON(http.StatusOK, res) return}func deletePetHandler(c *gin.Context) { c.Header(\”Access-Control-Allow-Origin\”, \”*\”) id := c.Param(\”id\”) req := pets.DeletePetRequest{ID: id} err := pets.DeletePet(db, &req) if err != nil { c.Status(http.StatusNotFound) return } c.Status(http.StatusOK)}
下面两幅截图是代码改造前后的对比:
完整的代码地址:
https://github.com/TyunTech/my-go-petstore.git
运行代码
首先,准备代码中需要的数据库账号密码。这里以环境变量的形式提供:
export rds_user=petexport rds_password=123456export rds_host=127.0.0.1export rds_port=3306export rds_database=pets
接着就可以运行代码了:
➜ my-go-petstore git:(dev) ✗ go run .[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in \”debug\” mode. Switch to \”release\” mode in production.- using env: export GIN_MODE=release- using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] POST /api/pets –> main.createPetHandler (3 handlers)[GIN-debug] GET /api/pets/:id –> main.getPetHandler (3 handlers)[GIN-debug] GET /api/pets –> main.listPetsHandler (3 handlers)[GIN-debug] PATCH /api/pets/:id –> main.updatePetHandler (3 handlers)[GIN-debug] DELETE /api/pets/:id –> main.deletePetHandler (3 handlers)[GIN-debug] OPTIONS /api/pets –> main.optionsPetHandler (3 handlers)[GIN-debug] OPTIONS /api/pets/:id –> main.optionsPetHandler (3 handlers)[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.[GIN-debug] Listening and serving HTTP on :8000
可以看到,服务运行在 8000 端口,我们通过 httpie 命令测试接口是否可用:
# 创建第一条测试数据(venv37) ➜ my-go-petstore git:(dev) ✗ http POST :8000/api/pets name=Jerry species=mouse age:=1HTTP/1.1 200 OKAccess-Control-Allow-Origin: *Content-Length: 86Content-Type: application/json; charset=utf-8Date: Sun, 13 Mar 2022 03:44:22 GMT{ \”age\”: 1, \”id\”: \”9b24b16d-8b09-47e2-9638-16775ccb8d8a\”, \”name\”: \”Jerry\”, \”species\”: \”mouse\”}# 创建第二条测试数据(venv37) ➜ my-go-petstore git:(dev) ✗ http POST :8000/api/pets name=Tommy species=cat age:=2 HTTP/1.1 200 OKAccess-Control-Allow-Origin: *Content-Length: 84Content-Type: application/json; charset=utf-8Date: Sun, 13 Mar 2022 03:44:40 GMT{ \”age\”: 2, \”id\”: \”81f04745-c17e-4f38-a3dd-b6e0741f207b\”, \”name\”: \”Tommy\”, \”species\”: \”cat\”}
查看数据:
(venv37) ➜ my-go-petstore git:(dev) ✗ http -b :8000/api/pets{ \”items\”: [ { \”age\”: 2, \”id\”: \”81f04745-c17e-4f38-a3dd-b6e0741f207b\”, \”name\”: \”Tommy\”, \”species\”: \”cat\” }, { \”age\”: 1, \”id\”: \”9b24b16d-8b09-47e2-9638-16775ccb8d8a\”, \”name\”: \”Jerry\”, \”species\”: \”mouse\” } ]}
到数据库中也查看一些数据:
mysql> use pets;mysql> select * from pets;+————————————–+——-+———+——+| id | name | species | age |+————————————–+——-+———+——+| 81f04745-c17e-4f38-a3dd-b6e0741f207b | Tommy | cat | 2 || 9b24b16d-8b09-47e2-9638-16775ccb8d8a | Jerry | mouse | 1 |+————————————–+——-+———+——+2 rows in set (0.00 sec)
总结一下,大致的流程是:
其次有一个 Client
什么是 Client 呢?其实就是用来操作 API 的,执行常见的 CRUD 操作。我们看一下代码结构:
(venv37) ➜ petstore-go-client git:(dev) tree ..├── README.md├── examples│ └── pets│ └── main.go├── go.mod├── go.sum├── openapi.md├── openapi.yaml├── pets.go├── petstore.go├── type_helpers.go└── validations.go2 directories, 10 files
完整的代码地址为:
https://github.com/TyunTech/go-petstore.git
Provider 的代码结构
Provider 的代码是按照标准的 CRUD 形式编码的,所以,我们按照套路进行编写即可。先看一下代码目录结构:
$ lsdist example go.mod go.sum main.go Makefile petstore terraform-provider-petstore$ tree ..├── dist│ └── linux_amd64│ └── terraform-provider-petstore├── example│ └── main.tf├── go.mod├── go.sum├── main.go├── Makefile├── petstore│ ├── provider.go│ ├── provider_test.go│ ├── resource_ps_pet.go│ └── resource_ps_pet_test.go└── terraform-provider-petstore4 directories, 11 files
上述的几个关键文件的用途如下:
main.go:Provider 的入口点,主要是一些样板代码;petstore/provider.go:包含了 Provider 的定义,资源映射及共享配置对象的初始化;petstore/provider_test.go:Provider 的测试文件;petstore/resource_ps_pet.go:用于定义管理 pet 资源的 CRUD 操作;petstore/resource_ps_pet_test.go:pet 资源的测试文件;
看一下关键的四个函数。
Createfunc resourcePSPetCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*sdk.Client) options := sdk.PetCreateOptions{ Name: d.Get(\”name\”).(string), Species: d.Get(\”species\”).(string), Age: d.Get(\”age\”).(int), } pet, err := conn.Pets.Create(options) if err != nil { return err } d.SetId(pet.ID) resourcePSPetRead(d, meta) return nil}
Readfunc resourcePSPetRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*sdk.Client) pet, err := conn.Pets.Read(d.Id()) if err != nil { return err } d.Set(\”name\”, pet.Name) d.Set(\”species\”, pet.Species) d.Set(\”age\”, pet.Age) return nil}
Updatefunc resourcePSPetUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*sdk.Client) options := sdk.PetUpdateOptions{} if d.HasChange(\”name\”) { options.Name = d.Get(\”name\”).(string) } if d.HasChange(\”age\”) { options.Age = d.Get(\”age\”).(int) } conn.Pets.Update(d.Id(), options) return resourcePSPetRead(d, meta)}
Deletefunc resourcePSPetDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*sdk.Client) conn.Pets.Delete(d.Id()) return nil}
介绍了上述方法后,我们看看它们是在什么时候被调用的。如下图所示:
在上述所有的工作完成后,我们就可以构建 Provider 二进制文件,并与远端的 API 进行交互。如果在本地测试没有问题,接下来就可以把我们的自定义 Provider 发布到 Terraform 的 Registry 上面,供有需要的小伙伴使用。
发布自己的 Provider
如何让更多的人找到我们的 Provider 呢?我们可以把 Provider 发布到 Terraform 的 Registry 网站上,这样大家就可以在上面找到并使用它。可以使用自己的 Github 账号登录。详细的文档可以查看官网文档,写得非常详细。
以下是一些截图:
使用了 GitHub 的 Actions 进行代码的发布,截图如下:
大约十分钟可以发布完成,会生成相关平台的二进制代码,可以供不同的平台进行下载使用。
发布完成,在 Terraform 的 Registry 的界面会有如下的显示。
发布一个 Provider 时需要注意的几点:
每次发布时,需要自动构建出各种平台的二进制文件;主要使用 .goreleaser.yml 文件实现,其代码如下:# Visit https://goreleaser.com for documentation on how to customize this# behavior.before:hooks:# this is just an example and not a requirement for provider building/publishing- go mod tidybuilds:- env:# goreleaser does not work with CGO, it could also complicate# usage by users in CI/CD systems like Terraform Cloud where# they are unable to install libraries.- CGO_ENABLED=0mod_timestamp: \'{{ .CommitTimestamp }}\’flags:- -trimpathldflags:- \’-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}\’goos:- freebsd- windows- linux- darwingoarch:- amd64- \’386\’- arm- arm64ignore:- goos: darwingoarch: \’386\’binary: \'{{ .ProjectName }}_v{{ .Version }}\’archives:- format: zipname_template: \'{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}\’checksum:name_template: \'{{ .ProjectName }}_{{ .Version }}_SHA256SUMS\’algorithm: sha256signs:- artifacts: checksumargs:# if you are using this is a GitHub action or some other automated pipeline, you# need to pass the batch flag to indicate its not interactive.- \”–batch\”- \”–local-user\”- \”{{ .Env.GPG_FINGERPRINT }}\” # set this environment variable for your signing key- \”–output\”- \”${signature}\”- \”–detach-sign\”- \”${artifact}\”release:# Visit your project\’s GitHub Releases page to publish this release.draft: falsechangelog:skip: true这样我们每次提交代码时,Github 的 Actions 会自动构建我们的代码,根据 Tag 信息自动构建出 Release 文件;生成 GPG 的公钥及私钥;相关命令如下:# 生成 GPG 公私钥$ gpg –full-generate-key# 查看 GPG 信息gpg –list-secret-keys –keyid-format=longsec rsa4096/C15EAAAAAAAAAAAA 2022-04-06 [SC] # 需要关注此 ID:C15EAAAAAAAAAAAA274425A57102378E4AAAAAAAAAAAAAAAAAAAAAAAuid [ultimate] Laven Liu <@gmail.com>ssb rsa4096/2BAAAAAAAAAAAAAA 2022-04-06 [E]# 查看 GPG 私钥gpg –armor –export-secret-keys \”C15EAAAAAAAAAAAA\”# 查看 GPG 公钥gpg –armor –export \”C15EAAAAAAAAAAAA\”配置 Github Actions,这一步主要是配置 GPG 的公私钥;
如何使用
在 Registry 的界面上,可以找到使用说明。如下图所示:
准备配置文件
准备配置文件 main.tf:
terraform {required_providers { petstore = { source = \”TyunTech/petstore\” version = \”1.0.1\” }}}provider \”petstore\” { address = \”http://localhost:8000\”}resource \”petstore_pet\” \”my_pet\” { name = \”SnowBall\” species = \”cat\” age = 3}
执行 init
首先执行 terraform init 初始化:
(venv37) ➜ ch11 terraform init……Terraform has been successfully initialized!
执行 apply
接着执行 terraform apply,
(venv37) ➜ ch11 terraform apply -auto-approveTerraform used the selected providers to generate the following execution plan. Resourceactions are indicated with the following symbols: + createTerraform will perform the following actions: # petstore_pet.my_pet will be created + resource \”petstore_pet\” \”my_pet\” { + age = 3 + id = (known after apply) + name = \”SnowBall\” + species = \”cat\” }Plan: 1 to add, 0 to change, 0 to destroy.petstore_pet.my_pet: Creating…petstore_pet.my_pet: Creation complete after 0s [id=96bcf678-231f-449a-baf1-a01d2c7ecb9b]Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
验证数据
它真的创建资源了吗?我们到数据库中查看一下:
mysql> use petsmysql> select * from pets;+————————————–+———-+———+——+| id | name | species | age |+————————————–+———-+———+——+| 81f04745-c17e-4f38-a3dd-b6e0741f207b | Tommy | cat | 2 || 96bcf678-231f-449a-baf1-a01d2c7ecb9b | SnowBall | cat | 3 | — <- 创建了该记录| 9b24b16d-8b09-47e2-9638-16775ccb8d8a | Jerry | mouse | 1 |+————————————–+———-+———+——+3 rows in set (0.00 sec)
修改数据
修改一下 snowball 的年龄为 7 岁,然后再次执行,看看数据库中的数据会不会发生变化:
(venv37) ➜ ch11 terraform apply -auto-approvepetstore_pet.my_pet: Refreshing state… [id=96bcf678-231f-449a-baf1-a01d2c7ecb9b]Terraform used the selected providers to generate the following execution plan. Resourceactions are indicated with the following symbols:~ update in-placeTerraform will perform the following actions: # petstore_pet.my_pet will be updated in-place~ resource \”petstore_pet\” \”my_pet\” { ~ age = 3 -> 7 id = \”96bcf678-231f-449a-baf1-a01d2c7ecb9b\” name = \”SnowBall\” # (1 unchanged attribute hidden) }Plan: 0 to add, 1 to change, 0 to destroy.petstore_pet.my_pet: Modifying… [id=96bcf678-231f-449a-baf1-a01d2c7ecb9b]petstore_pet.my_pet: Modifications complete after 0s [id=96bcf678-231f-449a-baf1-a01d2c7ecb9b]Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
再次验证一下数据库:
mysql> select * from pets;+————————————–+———-+———+——+| id | name | species | age |+————————————–+———-+———+——+| 81f04745-c17e-4f38-a3dd-b6e0741f207b | Tommy | cat | 2 || 9b24b16d-8b09-47e2-9638-16775ccb8d8a | Jerry | mouse | 1 || a159cd59-0a4f-4fdf-9ea7-fda2a59f5c9e | snowball | cat | 7 |+————————————–+———-+———+——+3 rows in set (0.00 sec)
删除数据(venv37) ➜ ch11 terraform destroypetstore_pet.my_pet: Refreshing state… [id=a159cd59-0a4f-4fdf-9ea7-fda2a59f5c9e]Terraform used the selected providers to generate the following execution plan. Resourceactions are indicated with the following symbols: – destroyTerraform will perform the following actions: # petstore_pet.my_pet will be destroyed – resource \”petstore_pet\” \”my_pet\” { – age = 7 -> null – id = \”a159cd59-0a4f-4fdf-9ea7-fda2a59f5c9e\” -> null – name = \”snowball\” -> null – species = \”cat\” -> null }Plan: 0 to add, 0 to change, 1 to destroy.Do you really want to destroy all resources?Terraform will destroy all your managed infrastructure, as shown above.There is no undo. Only \’yes\’ will be accepted to confirm.Enter a value: yes # 输入 yespetstore_pet.my_pet: Destroying… [id=a159cd59-0a4f-4fdf-9ea7-fda2a59f5c9e]petstore_pet.my_pet: Destruction complete after 0sDestroy complete! Resources: 1 destroyed.
验证一下数据库中的数据是否还存在:
mysql> select * from pets;+————————————–+——-+———+——+| id | name | species | age |+————————————–+——-+———+——+| 81f04745-c17e-4f38-a3dd-b6e0741f207b | Tommy | cat | 2 || 9b24b16d-8b09-47e2-9638-16775ccb8d8a | Jerry | mouse | 1 |+————————————–+——-+———+——+2 rows in set (0.00 sec)
总结
至此,我们完成了一个 Provider 的编写并发布到了 Terraform 的 Registry 上面,希望本文对大家有所帮助。如果在实践本文时遇到问题,可以留言进行交流。
附录
分享一些常用的模块及遇到问题时如何查看日志。
常用模块
这里罗列一下常用的模块,比较实用。
randomresource \”random_string\” \”random\” { length = 16}output \”random\”{ value =random_string.random.result}
输出:
Outputs:random = \”BQa7LGq4RtDtCv)&\”
local_fileresource \”local_file\” \”myfile\” { content = \”This is my text\” filename = \”../mytextfile.txt\”}
archivedata \”archive_file\” \”backup\” { type = \”zip\” source_file = \”../mytextfile.txt\” output_path = \”${path.module}/archives/backup.zip\”}
排错
当执行计划失败的时候,该怎么办?看日志呗。对于更详细的日志,我们课可以通过设置环境变量打开 trace 级别的日志。如:export TF_LOG=trace。如何关闭日志?把环境变量 TF_LOG 的值设置一个空值即可。
# 打开详细的日志export TF_LOG=trace# 关闭日志export TF_LOG=
再次执行 terraform xxx 命令时,会有如下的输出:
2022-03-04T16:37:54.239+0800 [INFO] Terraform version: 1.1.62022-03-04T16:37:54.240+0800 [INFO] Go runtime version: go1.17.22022-03-04T16:37:54.240+0800 [INFO] CLI args: []string{\”terraform\”, \”init\”}2022-03-04T16:37:54.240+0800 [TRACE] Stdout is a terminal of width 1352022-03-04T16:37:54.240+0800 [TRACE] Stderr is a terminal of width 1352022-03-04T16:37:54.240+0800 [TRACE] Stdin is a terminal2022-03-04T16:37:54.240+0800 [DEBUG] Attempting to open CLI config file: /root/.terraformrc2022-03-04T16:37:54.240+0800 [INFO] Loading CLI configuration from /root/.terraformrc2022-03-04T16:37:54.240+0800 [DEBUG] checking for credentials in \”/root/.terraform.d/plugins\”……Initializing the backend…2022-03-04T16:37:54.247+0800 [TRACE] Meta.Backend: no config given or present on disk, so returning nil config2022-03-04T16:37:54.247+0800 [TRACE] Meta.Backend: backend has not previously been initialized in this working directory……2022-03-04T16:37:54.252+0800 [TRACE] backend/local: state manager for workspace \”default\” will:- read initial snapshot from terraform.tfstate- write new snapshots to terraform.tfstate- create any backup at terraform.tfstate.backup2022-03-04T16:37:54.252+0800 [TRACE] statemgr.Filesystem: reading initial snapshot from terraform.tfstate2022-03-04T16:37:54.252+0800 [TRACE] statemgr.Filesystem: snapshot file has nil snapshot, but that\’s okay2022-03-04T16:37:54.252+0800 [TRACE] statemgr.Filesystem: read nil snapshotInitializing provider plugins…- Finding hashicorp/alicloud versions matching \”1.157.0\”…2022-03-04T16:37:54.252+0800 [DEBUG] Service discovery for registry.terraform.io at https://registry.terraform.io/.well-known/terraform.json
推荐学习资料
本文的内容是翻译于《Terraform in Action》一书,是一本非常不错的书,值得阅读并实践。但是也有其他优秀的参考资料,以下是整理的资源列表,供参考。
网站资源推荐:https://lonegunmanb.github.io/introduction-terraform/
推荐阅读:
Terraform 命令行章节;
Terraform 模块章节;
https://help.aliyun.com/product/95817.html?spm=a2cls.b92374736.0.0.267599deCIHp3f
如果使用阿里云,可以参考上述帮助文档。
书籍推荐:《Terraform in Action》《Terraform Up and Running》
云和安全管理服务专家新钛云服刘川川原创