Terraform Refactor Stage I Guide

 This guide will describe with examples the major changes to be made to the Terraform code base during the first stage of the Terraform refactoring for the upgrade from 0.11 to 0.12.

Lists of security group CIDR blocks variables

As is, any CIDR blocks needed for security group rules have been passed into the module seperately and the CIDR block lists built within the module and then used by a security group rule Terraform resource or passed into another module which will then create a security group rule resource. Just looking at these variables being passed into the module at the calling service level doesn’t give much indication of what they may be being used for behind the scenes in the module. Where this occurs they should be replaced by complete list variables (although currently lists as a string seperated by “,”) with descriptive names as to their usage such as

1variable "ec2_ssh_22_allowed_cidr_blocks" { 2 description = "List of CIDR blocks in string format seperated by ',' allowed SSH access to the Artifactory ec2 instances on port 22" 3} 4 5variable "elb_https_443_allowed_cidr_blocks" { 6 description = "List of CIDR blocks in string format seperated by ',' allowed https access to the Artifactory ELB on port 443" 7}

The variable names are in the format: type of resource the CIDR blocks will have access to, the protocol the traffic will use, and the port the traffic is allowed over. This allows the module to have less variables, some of which may even have not been used once passed into the module.

Inside the module these variables can then be used as variables for security group resources or modules

1module "bastion_inbound_22" { 2 source = "../../../core/aws-security-groups/rules/cidr" 3 type = "ingress" 4 from_port = 22 5 to_port = 22 6 protocol = "tcp" 7 cidr_blocks = var.ec2_ssh_22_allowed_cidr_blocks 8 security_group_id = aws_security_group.instance_security_group.id 9} 10 11module "artifactory_nat_inbound_443" { 12 source = "../../../core/aws-security-groups/rules/cidr" 13 type = "ingress" 14 from_port = 443 15 to_port = 443 16 protocol = "tcp" 17 cidr_blocks = var.elb_https_443_allowed_cidr_blocks 18 security_group_id = aws_security_group.elb_security_group.id 19}

As for building the values for these variables, if it is required, this should now be done as local variables in the calling service, so as to keep the module call tidy and clear.

A complete list should be built and then one final join used to convert the list into a “,” seperated string.

1locals { 2 elb_https_443_allowed_cidr_blocks = join(",", 3 formatlist("%s/32", 4 concat( 5 split(",", data.terraform_remote_state.access.outputs.nat_gateway_ip_address), 6 split(",", data.terraform_remote_state.tools.outputs.nat_gateway_ip_address) 7 ) 8 ) 9 ) 10 asg_subnet_ids = split(",", data.terraform_remote_state.tools.outputs.private_subnet_ids) 11 availability_zones = split(",", data.terraform_remote_state.tools.outputs.availability_zones) 12 elb_subnet_ids = split(",", data.terraform_remote_state.tools.outputs.public_subnet_ids) 13 public_subnet_ids = split(",", data.terraform_remote_state.tools.outputs.private_subnet_ids) 14} 15 16module "artifactory" { 17 source = "../../../modules/support/service/artifactory" 18 19 # AWS Data Source Variables 20 artifactory_ami = data.aws_ami.ubuntu_16_04.id 21 artifactory_ssl_certificate_arn = data.terraform_remote_state.global.outputs.acm_certificate_arn 22 asg_subnet_ids = local.asg_subnet_ids 23 availability_zones = local.availability_zones 24 aws_region = var.aws_region 25 db_master_password = data.vault_generic_secret.artifactory.data["db_master_password"] 26 db_master_username = data.vault_generic_secret.artifactory.data["db_master_username"] 27 ec2_ssh_22_allowed_cidr_blocks = data.terraform_remote_state.management.outputs.vpc_cidr_block 28 elb_https_443_allowed_cidr_blocks = local.elb_https_443_allowed_cidr_blocks 29 elb_subnet_ids = local.elb_subnet_ids 30 global_acm_certificate_arn = data.terraform_remote_state.global.outputs.acm_certificate_arn 31 private_subnet_ids = data.terraform_remote_state.tools.outputs.private_subnet_ids 32 private_local_route53_zone_id = data.terraform_remote_state.management.outputs.private_local_route53_zone_id 33 public_route53_zone_id = data.terraform_remote_state.global.outputs.digital_nbrown_co_uk_zone_id 34 public_subnet_ids = local.public_subnet_ids 35 vpc_id = data.terraform_remote_state.tools.outputs.vpc_id 36 vpc_cidr_block = data.terraform_remote_state.management.outputs.vpc_cidr_block 37 38 # Standalone Module Variables 39 artifactory_asg_desired = "1" 40 artifactory_asg_max = "1" 41 artifactory_asg_min = "1" 42 artifactory_ec2_keypair_name = "tools-artifactory-linux-key-pair" 43 artifactory_instance_name = "artifactory" 44 artifactory_instance_type = "t2.large" 45 environment = "tools" 46 puppet_role = "artifactory" 47 service_name = "artifactory" 48}

Submodule consolodation

Further to the above consolodation of variables, where there is multiple module calls for different cidr block variables, if those variables have been consolodated to one list of subnets to be allowed access to a certain service, only one module should be called to add those security group rules to a security group.

For example

1module "forwarders_to_elb_1_security_group_rule" { 2 source = "../../../local/aws-security-groups/rules/cidr" 3 4 security_group_id = module.forwarders_to_elb_1_security_group.security_group_id 5 type = "ingress" 6 protocol = "tcp" 7 from_port = "5000" 8 to_port = "5000" 9 cidr_blocks = "${var.reserved_forwarders_cidr_block_1},${var.reserved_forwarders_cidr_block_2},${var.reserved_forwarders_cidr_block_3},${var.reserved_forwarders_cidr_block_4},${var.reserved_forwarders_cidr_block_5}" 10} 11 12module "sg_rules_set_allow_5044_to_5044" { 13 14source = "../../../../modules/local/elb-to-instance-sg-ruleset" 15 16lb_port_range_from = 5000 17lb_port_range_to = 5000 18instance_port_range_to = 5000 19cidr_blocks = "10.229.0.0/16,${data.terraform_remote_state.tools.outputs.vpc_cidr_block},${var.this_vpc_cidr_block},${module.nbrown-network-ips.nbrown_griffin_house},${var.softlayer_amsterdam_forwarders_cidr_block},${var.softlayer_london_forwarders_cidr_block},${var.griffin_house_websphere_forwarders_cidr_block}" 20elbs_security_group_id = aws_security_group.elb_security_group.id 21instances_security_group_id = aws_security_group.instance_security_group.id 22}

can be condensed into

1module "sg_rules_set_allow_5000_to_5000" { 2 source = "../../../../modules/local/elb-to-instance-sg-ruleset" 3 4 lb_port_range_from = 5000 5 lb_port_range_to = 5000 6 instance_port_range_from = 5000 7 instance_port_range_to = 5000 8 cidr_blocks = var.elb_logstash_5000_allowed_cidr_blocks 9 elbs_security_group_id = aws_security_group.elb_security_group.id 10 instances_security_group_id = aws_security_group.instance_security_group.id 11}

In this instance this also cuts down the number of security groups to be attached to the ELB from two to one.

Hard coded sensitive values

If any hard coded usernames or password for database access etc are found in the codebase these should be added the the vault and vault lookup performed on their vault keys to obtain these values.

1provider "vault" { 2 address = "https://vault.tools.digital.nbrown.co.uk" 3} 4 5db_master_password = data.vault_generic_secret.artifactory.data["db_master_password"] 6db_master_username = data.vault_generic_secret.artifactory.data["db_master_username"]

Module usage of aws_iam_account_alias

aws_iam_account_alias should be defined within the service modules and not passed in as variables, this reduces the amount of variables for the module and

1data "aws_iam_account_alias" "current" {}

General data sources

With the exception of aws_iam_account_alias mentioned above, data sources should be used in the calling service and the values obtained from them passed into the module where needed to keep the module generic and reusable.

Calling service variables

In most cases calling services should only have one variable aws_region usually with a defualt value of eu-west-1. All other module variables should be hard coded into the module call.

Module calling style

All variables populated by a data source, local variable or variable should be seperated from variables with hard coded values

1module "artifactory" { 2 source = "../../../modules/support/service/artifactory" 3 4 # AWS Data Source Variables 5 artifactory_ami = data.aws_ami.ubuntu_16_04.id 6 artifactory_ssl_certificate_arn = data.terraform_remote_state.global.outputs.acm_certificate_arn 7 asg_subnet_ids = local.asg_subnet_ids 8 availability_zones = local.availability_zones 9 aws_region = var.aws_region 10 db_master_password = data.vault_generic_secret.artifactory.data["db_master_password"] 11 db_master_username = data.vault_generic_secret.artifactory.data["db_master_username"] 12 ec2_ssh_22_allowed_cidr_blocks = data.terraform_remote_state.management.outputs.vpc_cidr_block 13 elb_https_443_allowed_cidr_blocks = local.elb_https_443_allowed_cidr_blocks 14 elb_subnet_ids = local.elb_subnet_ids 15 global_acm_certificate_arn = data.terraform_remote_state.global.outputs.acm_certificate_arn 16 private_subnet_ids = data.terraform_remote_state.tools.outputs.private_subnet_ids 17 private_local_route53_zone_id = data.terraform_remote_state.management.outputs.private_local_route53_zone_id 18 public_route53_zone_id = data.terraform_remote_state.global.outputs.digital_nbrown_co_uk_zone_id 19 public_subnet_ids = local.public_subnet_ids 20 vpc_id = data.terraform_remote_state.tools.outputs.vpc_id 21 vpc_cidr_block = data.terraform_remote_state.management.outputs.vpc_cidr_block 22 23 # Standalone Module Variables 24 artifactory_asg_desired = "1" 25 artifactory_asg_max = "1" 26 artifactory_asg_min = "1" 27 artifactory_ec2_keypair_name = "tools-artifactory-linux-key-pair" 28 artifactory_instance_name = "artifactory" 29 artifactory_instance_type = "t2.large" 30 environment = "tools" 31 puppet_role = "artifactory" 32 service_name = "artifactory" 33}

Comment formating and location

Comments should be defined with a # and placed above the entire resource/module block with extra context as to which variable the comment refers to to ensure consistent positioning formatting within the resource/module declaration.

1# TODO - Use templatefile inline method instead of separate template_file data source for user_data 2# TODO - Use native list for subnet_id 3resource "aws_instance" "influxdb_a" { 4 ami = var.influxdb_ami 5 user_data = data.template_file.bootstrap_a.rendered 6 iam_instance_profile = aws_iam_instance_profile.instance_profile.name 7 disable_api_termination = false 8 instance_type = var.influxdb_instance_type 9 key_name = var.influxdb_ec2_keypair_name 10 monitoring = true 11 subnet_id = element(var.private_subnet_ids, 0) 12 vpc_security_group_ids = [aws_security_group.instance_security_group.id] 13 14 root_block_device { 15 volume_type = "gp2" 16 volume_size = 1024 17 } 18 19 tags = { 20 Name = "${var.environment}-influxdb-a" 21 Type = "EC2 Instance" 22 Environment = var.environment 23 } 24} 25

Vault provider location

Any vault provider should be defined in providers.tf rather than vault.tf.

1provider "vault" { 2 address = "https://vault.tools.digital.nbrown.co.uk" 3}

Use of AMI data sources to use the latest AMI images

Hard coded AMIs should be replaced with data sources filtered to the latest version of the AMI, whether they are public AMIs from major vendors such as Ubuntu or Amazon Linux, or internally built AMIs from NBrown.

The exception being where a fixed AMI is needed to ensure a service works as a breaking change may have been introduced in an updated software package/configuration in a later AMI version. The reason for using a fixed AMI ID should be detailed in a comment where it is used.

Below is an example of using a data source to obtain the latest Ubuntu 16.04 AMI ID using an account owner field to ensure it is coming from Ubuntu and a regular expression to ensure you get a x86-64 Ubuntu 16.04 AMI ID.

1# This is the latest official 16.04 AMI from Ubuntu 2data "aws_ami" "ubuntu_16_04" { 3 most_recent = true 4 owners = ["099720109477"] 5 name_regex = "^ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-\\d{8}$" 6}

This ID can then be used as follows

1artifactory_ami_id = data.aws_ami.ubuntu_16_04.id

Update tfstate key paths

Key paths where Terraform state is defined should be changed from terraform to terraform-012, both in remote state data sources and terraform configuration blocks.

For example

1data "terraform_remote_state" "access" { 2 backend = "s3" 3 4 config = { 5 bucket = "nbrown-terraform-remote" 6 key = "dpm/terraform/access/infrastructure/terraform.tfstate" 7 region = "eu-west-1" 8 } 9}

should be changed to

1data "terraform_remote_state" "access" { 2 backend = "s3" 3 4 config = { 5 bucket = "nbrown-terraform-remote" 6 key = "dpm/terraform-012/access/infrastructure/terraform.tfstate" 7 region = "eu-west-1" 8 } 9}

And

1terraform { 2 backend "s3" { 3 bucket = "nbrown-terraform-remote" 4 key = "dpm/terraform/access/service/openvpn/terraform.tfstate" 5 region = "eu-west-1" 6 acl = "bucket-owner-full-control" 7 } 8}

should be changed to

1terraform { 2 backend "s3" { 3 bucket = "nbrown-terraform-remote" 4 key = "dpm/terraform-012/access/service/openvpn/terraform.tfstate" 5 region = "eu-west-1" 6 acl = "bucket-owner-full-control" 7 } 8}

List value formatting

In lists with multiple values each value should be on their own line in alphabetical order

1enabled_metrics = [ 2 "GroupDesiredCapacity", 3 "GroupInServiceInstances", 4 "GroupMaxSize", 5 "GroupMinSize", 6 "GroupPendingInstances", 7 "GroupStandbyInstances", 8 "GroupTerminatingInstances", 9 "GroupTotalInstances" 10]

In lists with a single value the whole statement should be on one line

1target_group_arns = [aws_alb_target_group.alb_target_group.arn]

Variable naming

Variables should be as descriptive as possible, in the case of what value you are passing in. For example

artifactory_ssl_certificate_arn should be used in place of artifactory_ssl_certificate

artifactory_ami_id should be used in place of artifactory_ami

Newline consistency

All files should end with a single newline in line with POSIX standards.


Comments

Popular posts from this blog

SQL SERVER – Event ID 107- Report Server Windows Service (MSSQLSERVER) cannot connect to the report server database.

SQL Server Builds Information

Using DBCA silent install and disabling automatic memory management