您的位置:首页 > 其它

【AWS征文】Infrastructure as Code 在 AWS 上面是一种什么样的体验

2020-08-14 15:44 351 查看

因为云服务的特性,创建各种服务只需要请求一个 API 即可,所以 Infrastructure as Code(IaC) 也随之兴起,可以通过一份代码即可保存我们的云资源环境与架构,想要在另外一个地区创建一套相同的资源也变得非常简单,那么在 AWS 中,我们是如何把云中资源保存在代码中的呢?

一、CloudFormation

CloudFormation 是什么

AWS 早在 2011 年就推出了 AWS CloudFormation。对于 AWS 这样一个推崇去中心化小团队的公司来说,能形成 CloudFormation 这样一个基建即代码的服务是非常了不起的。

这个团队需要集成几乎所有的 AWS 服务,而且需要集成到每一个细节,到每一个 API 的每一个参数。而且在服务更新,新服务上线的时候,CloudFormation 也需要对应着进行更新。在一个去中心化的环境中,能做到这样一个高度集中的产品,还做得如此完善,很不容易。

CloudFormation 让用户可以用 YAML 或者 JSON 模板来描述自己的基建,形成一个Stack。如果要修改基建,用户可以修改自己的模板,然后上传进行对比,形成Change Set,用来确认基建栈中哪些资源会被修改。确认保存之后,基建就会调整成预想的样子。如果遇到问题,CloudFormation 会尝试直接回滚。

我想通过以上的介绍,大家已经明白 CloudFormation 的作用。用户只需要根据所需的资源创建 CloudFormation 的模版,然后把模版发给CloudFormation 引擎来创建资源堆栈。通过 CloudFormation,用户可以简化基础设施的管理,快速复制现有的基础设施,以及轻松控制和跟踪对基础设施所做的更改。

经过了多年的市场磨练,CloudFormation 已经非常完善,也影响了很多其他类似的基建工具。它的定位是一个相对底层的工具,与服务的创建、修改、删除的接口完全对应,不能额外提供什么数据和信息,这影响了人们对他进一步的抽象和简化。

它属于配置代码,或者说它仅仅是一个数据结构。虽然数据结构内部也能形成一些简单的逻辑,但与真正的程序代码相比还是不够灵活,而且看上去也会很奇怪。而且数据结构要与 IDE 结合,是比较困难的,要去做静态检查,也不如程序代码那么方便。跟其他代码和开发类工具的对接也是一个问题,配置代码的工具选择也不如程序代码那么丰富。

CloudFormation 的资源与数据最接近与 AWS 底层资源,初学者需要一定的时间才可以掌握。一般来说,我们很少自己从头去写 CloudFormation 模版,而且基于很多现有的模版进行更改,或者创建资源等等。当然如今我也不建议大家去手动编写 CloudFormation 模版,我们这里使用官方的案例模版做个演示,让大家了解 CloudFormation 创建资源的一个过程。

创建堆栈

打开 AWS Console,找到 CloudFormation,选择创建堆栈,在模版选择地方,我们选择一个样品模版。

填写堆栈信息

每个模版都可能会有一些参数信息,这样一个模版可以动态化的部署,根据自己的情况填写即可。

查看创建过程

配置好权限,点击创建之后,我们可以查看创建资源的一步步过程,可以看到模版创建了安全组还有 EC2 主机。

查看输出结果

输出结果是我们创建 wordpress 的访问地址,我们访问看看效果。

可能是这个模版有些过期了,所以部署的版本可能有点低,导致打开有些问题。

不过这并不要紧,我们只是通过这个演示去了解一下 CloudFormation 做了哪些事情。

查看模版

我们查看一下模版代码,看看创建的资源是如何定义的,因为模版篇幅太大,我这里只截取创建安全组和 EC2 的代码查看一下。

安全组的定义

安全组的定义相对来说比较简单。

"WebServerSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Enable HTTP access via port 80 locked down to the load balancer + SSH access",
"SecurityGroupIngress" : [
{"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}}
]
}
}

EC2 的定义

EC2 的定义比较多,包括其中安装 wordpress 并配置 wordpress 的过程,针对上面 php 版本的问题,我们可以修改代码定义中安装 php 的部分,指定高版本的 PHP 再进行更新即可。

"WebServer": {
"Type" : "AWS::EC2::Instance",
"Metadata" : {
"AWS::CloudFormation::Init" : {
"configSets" : {
"wordpress_install" : ["install_cfn", "install_wordpress", "configure_wordpress" ]
},
"install_cfn" : {
"files": {
"/etc/cfn/cfn-hup.conf": {
"content": { "Fn::Join": [ "", [
"[main]\n",
"stack=", { "Ref": "AWS::StackId" }, "\n",
"region=", { "Ref": "AWS::Region" }, "\n"
]]},
"mode"  : "000400",
"owner" : "root",
"group" : "root"
},
"/etc/cfn/hooks.d/cfn-auto-reloader.conf": {
"content": { "Fn::Join": [ "", [
"[cfn-auto-reloader-hook]\n",
"triggers=post.update\n",
"path=Resources.WebServer.Metadata.AWS::CloudFormation::Init\n",
"action=/opt/aws/bin/cfn-init -v ",
"         --stack ", { "Ref" : "AWS::StackName" },
"         --resource WebServer ",
"         --configsets wordpress_install ",
"         --region ", { "Ref" : "AWS::Region" }, "\n"
]]},
"mode"  : "000400",
"owner" : "root",
"group" : "root"
}
},
"services" : {
"sysvinit" : {
"cfn-hup" : { "enabled" : "true", "ensureRunning" : "true",
"files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"] }
}
}
},

"install_wordpress" : {
"packages" : {
"yum" : {
"php"          : [],
"php-mysql"    : [],
"mysql"        : [],
"mysql-server" : [],
"mysql-devel"  : [],
"mysql-libs"   : [],
"httpd"        : []
}
},
"sources" : {
"/var/www/html" : "http://wordpress.org/latest.tar.gz"
},
"files" : {
"/tmp/setup.mysql" : {
"content" : { "Fn::Join" : ["", [
"CREATE DATABASE ", { "Ref" : "DBName" }, ";\n",
"CREATE USER '", { "Ref" : "DBUser" }, "'@'localhost' IDENTIFIED BY '", { "Ref" : "DBPassword" }, "';\n",
"GRANT ALL ON ", { "Ref" : "DBName" }, ".* TO '", { "Ref" : "DBUser" }, "'@'localhost';\n",
"FLUSH PRIVILEGES;\n"
]]},
"mode"  : "000400",
"owner" : "root",
"group" : "root"
},

"/tmp/create-wp-config" : {
"content" : { "Fn::Join" : [ "", [
"#!/bin/bash -xe\n",
"cp /var/www/html/wordpress/wp-config-sample.php /var/www/html/wordpress/wp-config.php\n",
"sed -i \"s/'database_name_here'/'",{ "Ref" : "DBName" }, "'/g\" wp-config.php\n",
"sed -i \"s/'username_here'/'",{ "Ref" : "DBUser" }, "'/g\" wp-config.php\n",
"sed -i \"s/'password_here'/'",{ "Ref" : "DBPassword" }, "'/g\" wp-config.php\n"
]]},
"mode" : "000500",
"owner" : "root",
"group" : "root"
}
},
"services" : {
"sysvinit" : {
"httpd"  : { "enabled" : "true", "ensureRunning" : "true" },
"mysqld" : { "enabled" : "true", "ensureRunning" : "true" }
}
}
},

"configure_wordpress" : {
"commands" : {
"01_set_mysql_root_password" : {
"command" : { "Fn::Join" : ["", ["mysqladmin -u root password '", { "Ref" : "DBRootPassword" }, "'"]]},
"test" : { "Fn::Join" : ["", ["$(mysql ", { "Ref" : "DBName" }, " -u root --password='", { "Ref" : "DBRootPassword" }, "' >/dev/null 2>&1 </dev/null); (( $? != 0 ))"]]}
},
"02_create_database" : {
"command" : { "Fn::Join" : ["", ["mysql -u root --password='", { "Ref" : "DBRootPassword" }, "' < /tmp/setup.mysql"]]},
"test" : { "Fn::Join" : ["", ["$(mysql ", { "Ref" : "DBName" }, " -u root --password='", { "Ref" : "DBRootPassword" }, "' >/dev/null 2>&1 </dev/null); (( $? != 0 ))"]]}
},
"03_configure_wordpress" : {
"command" : "/tmp/create-wp-config",
"cwd" : "/var/www/html/wordpress"
}
}
}
}
},
"Properties": {
"ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
{ "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] },
"InstanceType"   : { "Ref" : "InstanceType" },
"SecurityGroups" : [ {"Ref" : "WebServerSecurityGroup"} ],
"KeyName"        : { "Ref" : "KeyName" },
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"#!/bin/bash -xe\n",
"yum update -y aws-cfn-bootstrap\n",

"/opt/aws/bin/cfn-init -v ",
"         --stack ", { "Ref" : "AWS::StackName" },
"         --resource WebServer ",
"         --configsets wordpress_install ",
"         --region ", { "Ref" : "AWS::Region" }, "\n",

"/opt/aws/bin/cfn-signal -e $? ",
"         --stack ", { "Ref" : "AWS::StackName" },
"         --resource WebServer ",
"         --region ", { "Ref" : "AWS::Region" }, "\n"
]]}}
},
"CreationPolicy" : {
"ResourceSignal" : {
"Timeout" : "PT15M"
}
}
}
},

我想这么复杂的 CloudFormation 模版,我们手动去写会晕掉的,那么有没有其他 IoC 工具可以让我们方便的创建资源呢,更加接近人的语言对资源进行定义呢?清理完创建的资源后我们接着往下看。

二、Terraform

Terraform 是一种安全有效地构建、更改和版本控制基础设施的工具(基础架构自动化的编排工具)。它的目标是 "Write, Plan, and create Infrastructure as Code", 基础架构即代码。Terraform 几乎可以支持所有市面上能见到的云服务。具体的说就是可以用代码来管理维护 IT 资源,把之前需要手动操作的一部分任务通过程序来自动化的完成,这样的做的结果非常明显:高效、不易出错。

Terraform 的安装非常简单,直接把官方提供的二进制可执行文件保存到本地就可以了。比如笔者习惯性的把它保存到 /usr/local/bin/ 目录下,当然这个目录会被添加到 PATH 环境变量中。完成后检查一下版本号:

wangzan:~ $ terraform version
Terraform v0.13.0

我这里自己使用 terraform 的语法写了一个小案例,案例在 AWS 的区域中创建一个 VPC,安全组等资源,比较简单。

代码浏览

vars.tf

这里面列举了一些变量,如在哪个区域创建,以及 VPC 的一些参数。

variable "region" {
default = "us-east-1"
}

variable "vpc_cidr" {
default = "172.19.0.0/16"
}

variable "pub_subnet_cidr" {
type = list
default = ["172.19.0.0/22", "172.19.4.0/22", "172.19.8.0/22"]
}

variable "pri_subnet_cidr" {
type = list
default = ["172.19.12.0/22", "172.19.16.0/22", "172.19.20.0/22"]
}

data "aws_availability_zones" "azs" {
state = "available"
}

vpc.tf

# Configure the AWS Provider
provider "aws" {
version = "~> 2.0"
region  = var.region
}

# Create a VPC
resource "aws_vpc" "myvpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
instance_tenancy = "default"

tags = {
Name = "tf-vpc"
}
}

# Create a internet gateway for the vpc
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.myvpc.id
}

# Create public subnets
resource "aws_subnet" "pubsubs" {
count = 3
vpc_id     = aws_vpc.myvpc.id
cidr_block = element(var.pub_subnet_cidr, count.index)
availability_zone = element(data.aws_availability_zones.azs.names, count.index)
map_public_ip_on_launch = true

tags = {
Name = "publicSubnet${count.index+1}"
Tier = "Public"
}
}

# Create private subnets
resource "aws_subnet" "prisubs" {
count = 3
vpc_id     = aws_vpc.myvpc.id
cidr_block = element(var.pri_subnet_cidr, count.index)
availability_zone = element(data.aws_availability_zones.azs.names, count.index)

tags = {
Name = "privateSubnet${count.index+1}"
Tier = "Private"
}
}

# Create a public router table for public subnets
resource "aws_route_table" "pub-rt" {
vpc_id = aws_vpc.myvpc.id

tags = {
Name = "publicRouter"
}
}

# Add a igw route to the public router table
resource "aws_route" "r1" {
route_table_id            = aws_route_table.pub-rt.id
destination_cidr_block    = "0.0.0.0/0"
gateway_id                = aws_internet_gateway.igw.id
depends_on                = [aws_route_table.pub-rt]
}

sg.tf

resource "aws_security_group" "allow_all" {
name        = "allow_all"
description = "Allow all inbound traffic"
vpc_id      = aws_vpc.myvpc.id

ingress {
description = "SSH from VPC"
from_port   = 22
to_port     = 22
protocol    = "tcp"
cidr_blocks = ["0.0.0.0/0", "1.2.3.4/32"]
}

egress {
from_port   = 0
to_port     = 0
protocol    = "-1"
cidr_blocks = ["0.0.0.0/0"]
}

tags = {
Name = "allow_all"
}
}

这里的代码只是我简单些的一个例子,功能并不完善,网上有很多大神写好的模版,可以借鉴修改。

资源部署

按照上面的代码,文件准备好之后,首先对目录进行初始化。

wangzan:~/environment/tf-aws $ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 2.0"...
- Finding latest version of hashicorp/random...
- Installing hashicorp/aws v2.70.0...
- Installed hashicorp/aws v2.70.0 (signed by HashiCorp)
- Installing hashicorp/random v2.3.0...
- Installed hashicorp/random v2.3.0 (signed by HashiCorp)

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, we recommend adding version constraints in a required_providers block
in your configuration, with the constraint strings suggested below.

* hashicorp/random: version = "~> 2.3.0"

Terraform has been successfully initialized!

通过 plan 命令检查配置文件,可以检查一下会创建哪些资源。

wangzan:~/environment/tf-aws $ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.aws_ami.amazonlinux2: Refreshing state...
data.aws_availability_zones.azs: Refreshing state...

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
<= read (data resources)

Terraform will perform the following actions:

# data.aws_availability_zones.azs will be read during apply
# (config refers to values not yet known)
<= data "aws_availability_zones" "azs"  {
group_names = [
"us-east-1",
]
~ id          = "2020-08-12 09:10:55.845582198 +0000 UTC" -> "2020-08-12 09:11:00.028348581 +0000 UTC"
names       = [
"us-east-1a",
"us-east-1b",
"us-east-1c",
"us-east-1d",
"us-east-1e",
"us-east-1f",
]
state       = "available"
zone_ids    = [
"use1-az1",
"use1-az2",
"use1-az4",
"use1-az6",
……………………

使用 apply 命令完成部署操作。

wangzan:~/environment/tf-aws $ terraform apply -auto-approve
data.aws_availability_zones.azs: Refreshing state...
data.aws_availability_zones.azs: Reading... [id=2020-08-12 09:29:30.404010621 +0000 UTC]
data.aws_availability_zones.azs: Read complete after 0s [id=2020-08-12 09:29:33.668833503 +0000 UTC]
aws_vpc.myvpc: Creating...
aws_vpc.myvpc: Still creating... [10s elapsed]
aws_vpc.myvpc: Creation complete after 10s [id=vpc-0838dc286d08a7b00]
aws_subnet.prisubs[1]: Creating...
aws_subnet.pubsubs[2]: Creating...
aws_internet_gateway.igw: Creating...
aws_route_table.pub-rt: Creating...
aws_security_group.allow_all: Creating...
aws_subnet.pubsubs[1]: Creating...
aws_subnet.pubsubs[0]: Creating...
aws_subnet.prisubs[0]: Creating...
aws_subnet.prisubs[2]: Creating...
aws_route_table.pub-rt: Creation complete after 3s [id=rtb-0ebc59c248f329fc4]
aws_subnet.prisubs[2]: Creation complete after 4s [id=subnet-0b8e0bff1884ddbe8]
aws_internet_gateway.igw: Creation complete after 4s [id=igw-02ec32ae58b05d385]
aws_route.r1: Creating...
aws_subnet.prisubs[0]: Creation complete after 4s [id=subnet-08951ce91e4dc58d3]
aws_subnet.prisubs[1]: Creation complete after 4s [id=subnet-072cbfd8423eb7ce8]
aws_subnet.pubsubs[0]: Creation complete after 5s [id=subnet-08aa388924c7321d9]
aws_subnet.pubsubs[1]: Creation complete after 5s [id=subnet-01b71def47cb1b9bf]
aws_subnet.pubsubs[2]: Creation complete after 5s [id=subnet-09535e7a2420b5e97]
aws_route.r1: Creation complete after 2s [id=r-rtb-0ebc59c248f329fc41080289494]
aws_security_group.allow_all: Creation complete after 8s [id=sg-0c6c51ceafbb8fe1f]

Apply complete! Resources: 11 added, 0 changed, 0 destroyed.

查看资源

部署完成之后,我们登陆 Console 查看创建的资源。

已经按照我们配置的情况创建好了 VPC 的相关资源,查看完成之后,我们可以使用下面的命令把资源清理干净。

wangzan:~/environment/tf-aws $ terraform destroy
ws_subnet.pubsubs[2]: Destroying... [id=subnet-09535e7a2420b5e97]
aws_route.r1: Destroying... [id=r-rtb-0ebc59c248f329fc41080289494]
aws_security_group.allow_all: Destroying... [id=sg-0c6c51ceafbb8fe1f]
aws_subnet.prisubs[1]: Destroying... [id=subnet-072cbfd8423eb7ce8]
aws_subnet.prisubs[2]: Destroying... [id=subnet-0b8e0bff1884ddbe8]
aws_subnet.pubsubs[1]: Destroying... [id=subnet-01b71def47cb1b9bf]
aws_subnet.prisubs[0]: Destroying... [id=subnet-08951ce91e4dc58d3]
aws_subnet.pubsubs[0]: Destroying... [id=subnet-08aa388924c7321d9]
aws_route.r1: Destruction complete after 1s
aws_route_table.pub-rt: Destroying... [id=rtb-0ebc59c248f329fc4]
aws_internet_gateway.igw: Destroying... [id=igw-02ec32ae58b05d385]
aws_subnet.pubsubs[1]: Destruction complete after 2s
aws_subnet.prisubs[2]: Destruction complete after 2s
aws_security_group.allow_all: Destruction complete after 2s
aws_subnet.prisubs[0]: Destruction complete after 2s
aws_subnet.pubsubs[0]: Destruction complete after 2s
aws_subnet.pubsubs[2]: Destruction complete after 2s
aws_subnet.prisubs[1]: Destruction complete after 2s
aws_route_table.pub-rt: Destruction complete after 2s
aws_internet_gateway.igw: Still destroying... [id=igw-02ec32ae58b05d385, 10s elapsed]
aws_internet_gateway.igw: Destruction complete after 11s
aws_vpc.myvpc: Destroying... [id=vpc-0838dc286d08a7b00]
aws_vpc.myvpc: Destruction complete after 1s

Destroy complete! Resources: 11 destroyed.

至此我们的演示就完成,通过写 tf 模版,是不是比 CloudFormation 简单很多了呢,非常贴近于人的语言,容易理解和记忆,那么有没有更简单的创建方式呢,接着往下看吧。

三、Cloud Development Kit

CDK 是什么

CDK 把 CloudFormation 抽象了一层。它使用 TypeScript 等程序语言,把 CloudFormation 的模板包装成了一个领域专用语言(domain-specific language),CDK 的编译器会把这个语言再转译成 CloudFormation 模板。

前面我们使用 CloudFormation 创建一个资源,需要些很多参数,并且配置起来也很复杂,学习成本很高,而 CDK 把大部分工作进行了封装,创建某个资源,仅需几个封装好的函数,或者一个函数就可以,让代码更加简洁易懂。

由于没有了 CloudFormation 需要跟底层服务接口完全匹配的枷锁,CDK 后续的想象空间是很大的。各种各样的常见场景,都可以简化成帮手函数。这使得 CDK 的使用会更加贴近管理控制台的感觉,学习和使用门槛会低很多。

最后是代码问题。与 CloudFormation 的配置代码(JSON 或 YAML)不同,CDK 是真正的程序代码。这看上去并不是一个很特别的点,因为不管什么代码,描述的内容都是一样的,但是对于开发来说,体验差别就非常大了。

创建资源

前面使用 CloudFormation 或者 Terraform 创建 VPC 资源都非常繁琐,那下面我们体验一下使用 CDK 创建一个 VPC 是怎么样的体验。

CDK 的安装和初始化我这里不再掩饰,大家可以参照官方文档,我这里直接贴代码,代码在 lib 目录下面的文件。

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";

export class EksStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

new ec2.Vpc(this, "eks-vpc", {
maxAzs: 2,
natGateways: 1,
cidr: "172.19.0.0/16",
subnetConfiguration: [
{
cidrMask: 22,
name: "public",
subnetType: ec2.SubnetType.PUBLIC
},
{
cidrMask: 22,
name: "private",
subnetType: ec2.SubnetType.PRIVATE
}
]
});
}
}

代码的意思是创建一个跨越两个 AZ 的 VPC,一个 nat 网关,每个 AZ 有一个公有子网和私有子网。

使用命令

cdk synth
查看一下 CDK 为我们编译生成的 CloudFormation 模版,这些都是 CDK 帮我们写好了,是不是很简单。

如果没有问题的话,那直接使用

cdk deploy
就可以把资源部署到云环境中了。

可以看到一键帮我们创建了超多的资源,我们也去 CloudFormation 界面看看创建的资源。

可以看到,我们只需要简单的代码编写,CDK 就为我们把整个资源栈都创建好了,是不是非常简便。

同样,也根据我们制定的要求,把对应的 VPC 也创建出来了。

删除资源

删除资源也非常简单,只需要一个命令

cdk destroy
就可以完成。

如何你希望代码更加简洁一些,不需要对资源进行定制,那创建一个 VPC 会更加简单,如把代码修改为如下创建一个 VPC 看看。

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";

export class EksStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

new ec2.Vpc(this, "eks-vpc", {});
}
}

他会帮我们创建一个缺省的 VPC,跨域三个 AZ,有三个 nat gateway,现在大家应该领略到 CDK 创建资源是多么方便了,可以很方便的把整个资源栈复制到其他的区域。

从「基建即代码」到「基建即(真)代码」

从「基建即代码」(Infrastructure as Code)到「基建即(真)代码」(Infrastructure is Code)。一个字、一个字母的差异,带来了极大的开发者体验提升,让基建自动化离普通开发者又近了一些。

有了自动化之后,各种人为的问题和风险就大大降低了。虽然冒着自我革命和失业的风险,我还是强烈建议所有的 Dev、Ops 以及 DevOps 都采纳这样一个优秀的 CDK 来构建你云中的资源。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: