使用 tflint 对 Terraform 进行静态分析

我叫寺冈,是一名基础设施工程师。
今天我想使用 tflint 对 Terraform 代码进行静态分析。
什么是火石?
tflint 是一款专门用于 tf 文件的开源代码检查工具。
其代码仓库此处。
使用 tflint,您可以获得关于已弃用语法和未使用声明的警告,并
检测在 AWS 等云平台上可能出现的错误。
安装
在 Mac 上,您可以使用 brew 安装它。
$ brew install tflint
对于 Linux 系统,有现成的安装脚本可供使用。
$ curl https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
如何使用
基本上,该命令在 tf 文件所在的目录中执行。
只需执行以下命令,它就会读取并分析当前目录中的文件,
如果出现问题则会显示错误信息。
$ tflint
让我们仔细看看。请
在当前目录下创建一个名为 main.tf 的文件,并写入以下代码。
变量 "role_arn" { description = "AWS 角色 ARN" } 提供程序 "aws" { region = "ap-northeast-1" assume_role { role_arn = var.role_arn } } 资源 "aws_instance" "test" { ami = "ami-0ca38c7440de1749a" instance_type = "t3.micro" tags = { Name = "tflint-test" } }
当我检查与 terraform plan 的差异时,我发现它试图创建一个 EC2 实例。
$ terraform plan ------------------------------------------------------------------------ 已生成执行计划,如下所示。资源操作使用以下符号表示:+ 创建 Terraform 将执行以下操作:# 将创建 aws_instance.test + 资源 "aws_instance" "test" { + ami = "ami-0ca38c7440de1749a" + arn = (应用后可知) + associate_public_ip_address = (应用后可知) + availability_zone = (应用后可知) + cpu_core_count = (应用后可知) + cpu_threads_per_core = (应用后可知) + get_password_data = false + host_id = (应用后可知) + id = (应用后可知) + instance_state = (应用后可知) + instance_type = "t3.micro" + ipv6_address_count = (应用后可知) + ipv6_addresses = (应用后可知) + key_name = (应用后可知) + outpost_arn = (应用后可知) + password_data = (应用后可知) + placement_group =(应用后可知)+ primary_network_interface_id =(应用后可知)+ private_dns =(应用后可知)+ private_ip =(应用后可知)+ public_dns =(应用后可知)+ public_ip =(应用后可知)+ secondary_private_ips =(应用后可知)+ security_groups =(应用后可知)+ source_dest_check = true + subnet_id =(应用后可知)+ tags = { + "Name" = "tflint-test" } + tenancy =(应用后可知)+ vpc_security_group_ids =(应用后可知)+ ebs_block_device { + delete_on_termination =(应用后可知)+ device_name =(应用后可知)+ encrypted =(应用后可知)+ iops =(应用后可知)+ kms_key_id =(应用后可知)+ snapshot_id =(应用后可知)+ tags =(应用后可知)+ throughput =(应用后可知)+ volume_id = (应用后可知)+ volume_size = (应用后可知)+ volume_type = (应用后可知)} + enclave_options { + enabled = (应用后可知)} + ephemeral_block_device { + device_name = (应用后可知)+ no_device = (应用后可知)+ virtual_name = (应用后可知)} + metadata_options { + http_endpoint = (应用后可知)+ http_put_response_hop_limit = (应用后可知)+ http_tokens = (应用后可知)} + network_interface { + delete_on_termination = (应用后可知)+ device_index = (应用后可知)+ network_interface_id = (应用后可知)} + root_block_device { + delete_on_termination = (应用后可知)+ device_name = (应用后可知)+ encrypted = (应用后可知)+ iops = (应用后可知)+ kms_key_id = (应用后可知)+ tags = (应用后可知)应用后)+ 吞吐量 = (应用后已知)+ 卷 ID = (应用后已知)+ 卷大小 = (应用后已知)+ 卷类型 = (应用后已知)} } 计划:添加 1 个卷,更改 0 个卷,销毁 0 个卷。--------------------------------------------------------------------------- 注意:您没有指定“-out”参数来保存此计划,因此 Terraform 无法保证如果随后运行“terraform apply”,则会执行这些操作。.
让我们实际使用 terraform apply 来应用它。
$ terraform apply 已生成执行计划,如下所示。资源操作使用以下符号表示:+ 创建 Terraform 将执行以下操作:# 将创建 aws_instance.test + 资源 "aws_instance" "test" { + ami = "ami-0ca38c7440de1749a" + arn = (应用后可知) + associate_public_ip_address = (应用后可知) + availability_zone = (应用后可知) + cpu_core_count = (应用后可知) + cpu_threads_per_core = (应用后可知) + get_password_data = false + host_id = (应用后可知) + id = (应用后可知) + instance_state = (应用后可知) + instance_type = "t3.micro" + ipv6_address_count = (应用后可知) + ipv6_addresses = (应用后可知) + key_name = (应用后可知) + outpost_arn = (应用后可知) + password_data = (应用后可知) + placement_group = (应用后可知)+ 主网络接口 ID = (应用后可知)+ 私有 DNS = (应用后可知)+ 私有 IP = (应用后可知)+ 公有 DNS = (应用后可知)+ 公有 IP = (应用后可知)+ 二级私有 IP = (应用后可知)+ 安全组 = (应用后可知)+ 源目标检查 = true + 子网 ID = (应用后可知)+ 标签 = { + "名称" = "tflint-test" } + 租户 = (应用后可知)+ VPC 安全组 ID = (应用后可知)+ EBS 阻止设备 { + 终止时删除 = (应用后可知)+ 设备名称 = (应用后可知)+ 加密 = (应用后可知)+ IOPS = (应用后可知)+ KMS 密钥 ID = (应用后可知)+ 快照 ID = (应用后可知)+ 标签 = (应用后可知)+ 吞吐量 = (应用后可知)+ 卷 ID = (应用后可知)+ volume_size = (应用后可知)+ volume_type = (应用后可知)} + enclave_options { + enabled = (应用后可知)} + ephemeral_block_device { + device_name = (应用后可知)+ no_device = (应用后可知)+ virtual_name = (应用后可知)} + metadata_options { + http_endpoint = (应用后可知)+ http_put_response_hop_limit = (应用后可知)+ http_tokens = (应用后可知)} + network_interface { + delete_on_termination = (应用后可知)+ device_index = (应用后可知)+ network_interface_id = (应用后可知)} + root_block_device { + delete_on_termination = (应用后可知)+ device_name = (应用后可知)+ encrypted = (应用后可知)+ iops = (应用后可知)+ kms_key_id = (应用后可知)+ tags = (应用后可知)应用后)+吞吐量=(应用后已知)+卷ID=(应用后已知)+卷大小=(应用后已知)+卷类型=(应用后已知)} } 计划:添加1个,更改0个,销毁0个。您是否要执行这些操作?Terraform 将执行上述操作。只有“是”才能被接受。请输入值:是 aws_instance.test:正在创建... aws_instance.test:仍在创建... [已用 10 秒] aws_instance.test:仍在创建... [已用 20 秒] aws_instance.test:仍在创建... [已用 30 秒] aws_instance.test:仍在创建... [已用 40 秒] aws_instance.test:仍在创建... [已用 50 秒] aws_instance.test:创建完成,耗时 53 秒 [id=i-085049c8fe2c58383] 应用完成!资源:新增 1 个,更改 0 个,销毁 0 个。.
这段代码本身没有任何问题,所以可以顺利反映出来。
接下来,我们来重写一些代码。
让我们再对照计划检查一下差异。
可以看到,我们试图将 t3.micro 更改为 t4.micro。
% terraform plan 正在刷新内存中的 Terraform 状态,以便执行计划…… 刷新后的状态将用于计算此计划,但不会持久化到本地或远程状态存储。 aws_instance.test:正在刷新状态…… [id=i-085049c8fe2c58383] --------------------------------------------------------------------------- 已生成执行计划,如下所示。资源操作使用以下符号表示:~ 就地更新 Terraform 将执行以下操作:# aws_instance.test 将被就地更新 ~ 资源 "aws_instance" "test" { ami = "ami-0ca38c7440de1749a" arn = "arn:aws:ec2:ap-northeast-1:485076298277:instance/i-085049c8fe2c58383" associate_public_ip_address = true availability_zone = "ap-northeast-1a" cpu_core_count = 1 cpu_threads_per_core = 2 disable_api_termination = false ebs_optimized = false get_password_data = false hibernation = false id = "i-085049c8fe2c58383" instance_state = "running" ~ instance_type = "t3.micro" -> "t4.micro" ipv6_address_count = 0 ipv6_addresses = [] monitoring = false primary_network_interface_id = "eni-0c78d105cbfaddc16" private_dns = "ip-172-31-40-11.ap-northeast-1.compute.internal" private_ip = "172.31.40.11" public_dns = "ec2-54-249-80-70.ap-northeast-1.compute.amazonaws.com" public_ip = "54.249.80.70" secondary_private_ips = [] security_groups = [ "default", ] source_dest_check = true subnet_id = "subnet-9621c1de" tags = { "Name" = "tflint-test" } tenancy = "default" vpc_security_group_ids = [ "sg-485f1735", ] credit_specification { cpu_credits = "unlimited" } enclave_options { enabled = false } metadata_options { http_endpoint = "enabled" http_put_response_hop_limit = 1 http_tokens = "optional" } root_block_device { delete_on_termination = true device_name = "/dev/xvda" encrypted = false iops = 100 tags = {} throughput = 0 volume_id = "vol-012c9259f69420c1c" volume_size = 8 volume_type = "gp2" } } 计划:0 个要添加,1 个要更改,0 个要销毁。 --------------------------------------------------------------------------- 注意:您没有指定“-out”参数来保存此计划,因此 Terraform 无法保证如果随后运行“terraform apply”,这些操作将完全执行。.
让我们照着做。
% terraform apply aws_instance.test: 正在刷新状态... [id=i-085049c8fe2c58383] 已生成执行计划,如下所示。资源操作使用以下符号表示:~ 就地更新 Terraform 将执行以下操作:# aws_instance.test 将被就地更新 ~ 资源 "aws_instance" "test" { ami = "ami-0ca38c7440de1749a" arn = "arn:aws:ec2:ap-northeast-1:485076298277:instance/i-085049c8fe2c58383" associate_public_ip_address = true availability_zone = "ap-northeast-1a" cpu_core_count = 1 cpu_threads_per_core = 2 disable_api_termination = false ebs_optimized = false get_password_data = false hibernation = false id = "i-085049c8fe2c58383" instance_state = "running" ~ instance_type = "t3.micro" -> "t4.micro" ipv6_address_count = 0 ipv6_addresses = [] monitoring = false primary_network_interface_id = "eni-0c78d105cbfaddc16" private_dns = "ip-172-31-40-11.ap-northeast-1.compute.internal" private_ip = "172.31.40.11" public_dns = "ec2-54-249-80-70.ap-northeast-1.compute.amazonaws.com" public_ip = "54.249.80.70" secondary_private_ips = [] security_groups = [ "default", ] source_dest_check = true subnet_id = "subnet-9621c1de" tags = { "Name" = "tflint-test" } tenancy = "default" vpc_security_group_ids = [ "sg-485f1735", ] credit_specification { cpu_credits = "unlimited" } enclave_options { enabled = false } metadata_options { http_endpoint = "enabled" http_put_response_hop_limit = 1 http_tokens = "optional" } root_block_device { delete_on_termination = true device_name = "/dev/xvda" encrypted = false iops = 100 tags = {} throughput = 0 volume_id = "vol-012c9259f69420c1c" volume_size = 8 volume_type = "gp2" } } 计划:0 个要添加,1 个要更改,0 个要销毁。您是否要执行这些操作?Terraform 将执行上述操作。只有“是”才会被接受以批准。请输入值:是 aws_instance.test:正在修改... [id=i-085049c8fe2c58383] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,已用时 10 秒] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,已用时 20 秒] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,已用时 30 秒] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,已用时 40 秒] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,耗时 50 秒] aws_instance.test:仍在修改... [id=i-085049c8fe2c58383,耗时 1 分钟] 错误:Client.InvalidParameterValue:InstanceType 的值“t4.micro”无效。状态码:400,请求 ID:326502a4-5564-449f-a37d-73f651ffb151,位于 main.tf 第 13 行,资源“aws_instance”“test”中:13: resource“aws_instance”“test”{
发生错误。
由于 AWS 上不存在 t4.micro 实例类型,因此出现错误是正常的。但是,
在 AWS 上更改实例类型时,必须先停止实例,因此
运行 Terraform 的顺序类似:暂停 -> 更改类型 -> 启动。
在这种情况下,更改类型时发生错误,导致实例保持停止状态,Terraform 执行终止。
这显然不理想……理想情况下,我们希望在计划阶段就收到错误。
现在运行 tflint。
% tflint 发现 1 个问题:错误:instance_type 不是有效值 (aws_instance_invalid_type),位于 main.tf 第 15 行:15: instance_type = "t4.micro"
它报错了。
由于 plan 仅仅是 Terraform 的执行计划,
如果 tf 文件存在语法问题,它就会报错,但
它无法检测到依赖于云平台规范的错误。
在这种情况下,先运行 tflint 将显著提高差异检查和测试的准确性,然后再将更改应用到云平台。
概括
如上所述,如果无法检测并处理依赖于目标云平台规范的错误,
即使在执行过程中发生错误,Terraform 也不会回滚,这可能会影响正在运行的服务,并
需要调查错误发生的位置。
因此,有必要尽可能提高预应用检查的准确性,可以使用本文介绍的 plan 或 tflint 等工具。tflint
是一个非常有用的工具,我们鼓励大家尝试使用。
2