creating Subnet in a loop in Terraform - amazon-web-services

I am new to Terraform.
I am trying to create a code in which I can create the subnet in loop but cidrsubnet function is not working out as I don't want to change the subnet mask.
For Example: I want to create the subnet with these IPs: Subnet 1: 10.90.46.0/27, subnet 2: 10.90.46.32/27 subnet3: 10.90.46.64/27 and so on till subnet 8: 10.90.46.224/27
Thanks

Apply a count, which will multiplicate the number of the resource.
variable "vpc_id" {
default = "vpc-123"
}
#Here add all your 8 CIDR's to the list in "subnet_cidr" and for each one add one entry in "subnet_azs". You can repeat values in "subnet_azs" but not in subnet_cidr"
variable "subnet_cidr" {
default = ["10.90.46.0/27", "10.90.46.32/27", "10.90.46.64/27", "10.90.46.224/27"]
}
variable "subnet_azs" {
default = ["us-east-1a", "us-east-1b", "us-east-1c", "us-east-1c"]
}
resource "aws_subnet" "my_subnets" {
count = 8
vpc_id = "${var.vpc_id}"
cidr_block = "${element(var.subnet_cidr, count.index)}"
availability_zone = "${element(var.subnet_azs, count.index)}"
}

One way to automatically allocate a sequence of IP address ranges is to use the hashicorp/subnets/cidr module from the Terraform Registry:
module "subnet_addrs" {
source = "hashicorp/subnets/cidr"
version = "1.0.0"
base_cidr_block = "10.90.46.0/24"
networks = [
{ name = "us-east-1a", new_bits = 3 },
{ name = "us-east-1b", new_bits = 3 },
{ name = "us-east-1c", new_bits = 3 },
{ name = "us-east-1d", new_bits = 3 },
{ name = "us-east-1e", new_bits = 3 },
{ name = "us-east-1f", new_bits = 3 },
{ name = "us-east-1g", new_bits = 3 },
{ name = "us-east-1h", new_bits = 3 },
]
}
With the above example, module.subnet_addrs.network_cidr_blocks will be a map like this:
{
"us-east-1a" = "10.90.46.0/27"
"us-east-1b" = "10.90.46.32/27"
"us-east-1c" = "10.90.46.64/27"
"us-east-1d" = "10.90.46.96/27"
"us-east-1e" = "10.90.46.128/27"
"us-east-1f" = "10.90.46.160/27"
"us-east-1g" = "10.90.46.192/27"
"us-east-1h" = "10.90.46.224/27"
}
A map like this can be used directly as the for_each of a resource, so we can then declare the subnets like this, using AWS an example (because you didn't say which cloud vendor you are using):
resource "aws_subnet" "my_subnets" {
for_each = module.subnet_addrs.network_cidr_blocks
vpc_id = var.vpc_id
availability_zone = each.key
cidr_block = each.value
}
There's some guidance in the readme of this module about [things to keep in mind if you intend to rename or renumber networks later, to ensure that the changes you make are compatible with objects that already exist. I'd suggest reviewing that documentation before taking this path to make sure that you'd be able to apply any future changes to your network topology you might imagine making in the future.
For example, the allocations in the above example already cover the entire addressing space "10.90.46.0/24", so if you wanted to add a new subnet in future without introducing any new addressing space you'd need to replace one of those existing subnets with a pair of replacement subnets that both have new_bits = 4 and thus a prefix length of /28 instead of /27, so that you'd have one additional bit available for network numbering.

Related

Inappropriate value for attribute "value": string required in terraform

I'm creating Elastic beanstalk with terraform inside a vpc and I need to have at least two subnets because when I try to apply with only one I get an error that demands at least two. So here I define two subnets.
resource "aws_elastic_beanstalk_application" "elasticapp" {
name = var.elasticapp
}
resource "aws_elastic_beanstalk_environment" "beanstalkappenv" {
name = var.beanstalkappenv
application = aws_elastic_beanstalk_application.elasticapp.name
solution_stack_name = var.solution_stack_name
tier = var.tier
setting {
namespace = "aws:ec2:vpc"
name = "VPCId"
value = "${aws_vpc.prod-vpc.id}"
}
setting {
namespace = "aws:ec2:vpc"
name = "Subnets"
value = ["${aws_subnet.prod-subnet-public-1.id}", "${aws_subnet.prod-subnet-public-2.id}"]
After running terraform apply I get this error message.
Inappropriate value for attribute "value": string required.
I guess it's a syntax thing but I can't seem to figure it out.
According to the AWS documentation:
The IDs of the Auto Scaling group subnet or subnets. If you have multiple subnets, specify the value as a single comma-separated string of subnet IDs (for example, "subnet-11111111,subnet-22222222")..
You are specifying the subnets in an array. You would want to create a string instead:
resource "aws_elastic_beanstalk_environment" "beanstalkappenv" {
name = var.beanstalkappenv
application = aws_elastic_beanstalk_application.elasticapp.name
solution_stack_name = var.solution_stack_name
tier = var.tier
setting {
namespace = "aws:ec2:vpc"
name = "VPCId"
value = aws_vpc.prod-vpc.id
}
setting {
namespace = "aws:ec2:vpc"
name = "Subnets"
value = "${aws_subnet.prod-subnet-public-1.id},${aws_subnet.prod-subnet-public-2.id}"
}
}

Terraform: How to use security group id in tfvars

I am creating an RDS option group with several options and one of them has the parameter "vpc_security_group_memberships" which takes the ID of a security group.
db_options.tfvars:
db_options = [
{
option_name = "OEM_AGENT"
option_settings = [
{
name = "AGENT_REGISTRATION_PASSWORD"
value = "****"
},
{
name = "OMS_HOST"
value = "gridcontrol.example.com"
},
{
name = "OMS_PORT"
value = "4000"
}
]
port = "3872"
version = "13.5.0.0.v1"
vpc_security_group_memberships = [ ]
}
]
And I create a security group in sg.tf:
resource "aws_security_group" "db-sg" {
description = "Allow access to RDS instance"
name = "${var.env}.${var.db_name}.sg"
tags = {
Name = "${var.env}.${var.db_name}.sg"
}
vpc_id = var.vpc_id
}
Is it possible to put the aws_security_group.db-sg.id for use in vpc_security_group_memberships?
Is it possible to put the aws_security_group.db-sg.id for use in vpc_security_group_memberships?
Sadly its not possible. TF variables can't be dynamic. They must be fully defined at plan time. But you can instead use locals. Local values can by dynamic.

no matching EC2 Security Group found

I create modules for creating AWS vpc, security group and EC2. I make EC2 module depend on vpc and the security group module.
but I get that error:
no matching EC2 Security Group found
on .terraform/modules/main_ec2/modules/EC2Module/main.tf line 26, in data "aws_security_group" "security_group_id":
26: data "aws_security_group" "security_group_id" {
EC2 module
data "aws_subnet" "subnet_id" {
vpc_id = var.vpc_id
count = length(var.subnet_name)
depends_on = [var.subnet_id_depends_on]
filter {
name = "tag:Name"
values = ["VPC0${var.vpc_number}-${element(var.subnet_name, count.index)}"]
}
}
data "aws_security_group" "security_group_id" {
count = length(var.Server_Group_Name)
depends_on = [var.security_group_depends_on]
filter {
name = "tag:Name"
values = ["${local.name}-SG-${var.Server_Group_Name[count.index]}-${format("%02d", count.index + 1)}"]
}
}
resource "aws_instance" "create_instance" {
ami = "${element(data.aws_ami_ids.ami_id.*.id, count.index)}"
instance_type = var.EC2_Type[count.index]
subnet_id = "${element(data.aws_subnet.subnet_id.*.id, count.index)}"
associate_public_ip_address = "true"
vpc_security_group_ids = [ data.aws_security_group.security_group_id[count.index].id ]
key_name = "${local.name}-KP-${var.Server_Name[count.index]}"
depends_on = [var.EC2_depends_on]
}
sg module
resource "aws_security_group" "SGS" {
count = length(var.Server_Group_Name)
name = "${local.name}-SG-${var.Server_Group_Name[count.index]}-${format("%02d", count.index + 1)}"
description = "${local.name}-SG-${var.Server_Group_Name[count.index]}-${format("%02d", count.index + 1)}"
vpc_id = var.vpc_id
tags = {
name = "tag:Name"
values = "${local.name}-SG-${var.Server_Group_Name[count.index]}-${format("%02d", count.index + 1)}"
}
varibles.tf
variable "subnet_id_depends_on" {
type = list(string)
default = ["module.vpc_main"]
}
variable "security_group_depends_on" {
type = list(string)
default = ["module.sg"]
}
variable "EC2_depends_on" {
type = list(string)
default = [ "module.vpc_main", "module.sg", "module.key_pair" ]
}
for example, I call modules like that:
module "main_ec2" {
source = "/home/reham/Data/projectes/terraform//modules/EC2Module"
subnet_id_depends_on = var.subnet_id_depends_on
security_group_depends_on = var.security_group_depends_on
ami_name = var.ami_name
EC2_depends_on = var.EC2_depends_on
}
Outputs defined in the sg module:
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.vpc.id
}
output "primary_public_subnets_id" {
description = "public subnet ID"
value = aws_subnet.primary_public_subnets.*.id
}
output "secondary_public_subnets_id" {
description = "public subnet ID"
value = aws_subnet.secondary_public_subnets.*.id
}
output "primary_private_subnets_id" {
description = "public subnet ID"
value = aws_subnet.primary_private_subnets.*.id
}
output "security-groups" {
value = aws_security_group.SGS[*].id
}
what must I do to solve this problem?
There is more than one issue with the code and it is a result of probably not understanding how the module outputs work [1]. The second part of the issue is that there are many explicit dependencies defined with depends_on which are not required to be there since implicit dependencies are good enough [2]. Furthermore, as per comments, data sources in the EC2 module are not required as information about VPC, subnets and security groups can be provided with the outputs. Additionally, the EC2 instance resource is using count, but there is no count meta-argument there, so it will not work even if all the inputs are fixed. So here are my suggestions:
Redefine variables in the EC2 module (no need for depdends_on) and remove data sources (except possibly the one for the AMI)
Decide which variable you are going to use for the count meta-argument in the EC2 instance resource
I am going to give you an example with one subnet and security groups and then you can build from there. The EC2 module fixes:
resource "aws_instance" "create_instance" {
ami = data.aws_ami_ids.ami_id[0].id
instance_type = var.EC2_Type
subnet_id = var.subnet_id
associate_public_ip_address = "true"
vpc_security_group_ids = var.security_group_ids
key_name = "${local.name}-KP-${var.Server_Name[0]}"
}
In EC2 module, the variables would have to be changed to:
variable "subnet_id" {}
variable "security_group_ids" {}
Then, in the root module:
module "main_ec2" {
source = "/home/reham/Data/projectes/terraform//modules/EC2Module"
subnet_id = module.sg.primary_public_subnets_id[0]
security_group_ids = module.sg.security-groups
ami_name = var.ami_name
}
Referencing the module outputs in the EC2 module will make sure that the SG module is created first (that is an implicit dependency). At best this should give you a result for one EC2 instance as it seems there are many resources that rely on using the count meta-argument and that code is not in the question.
[1] https://www.terraform.io/language/values/outputs#accessing-child-module-outputs
[2] https://www.terraform.io/language/resources/behavior#resource-dependencies

Terraform - how to use for_each loop on a list of objects to create resources

I have an object containing the list of subnets I want to create.
variable "subnet-map" {
default = {
ec2 = [
{
cidr_block = "10.0.1.0/24"
availability_zone = "eu-west-1a"
}
],
lambda = [
{
cidr_block = "10.0.5.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.6.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.7.0/24"
availability_zone = "eu-west-1c"
}
],
secrets = [
{
cidr_block = "10.0.8.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.9.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.10.0/24"
availability_zone = "eu-west-1c"
}
],
rds = [
{
cidr_block = "10.0.11.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.12.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.13.0/24"
availability_zone = "eu-west-1c"
}
]
}
}
Earlier I was using the count loop construct. So I used to flatten the above structure into a list of objects
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
}
And then I would create the resources by doing
resource "aws_subnet" "aws-subnets" {
count = length(local.subnets)
vpc_id = aws_vpc.aws-vpc.id
cidr_block = local.subnets[count.index].cidr_block
availability_zone = local.subnets[count.index].availability_zone
tags = {
Name = "subnet-${local.subnets[count.index].resource}-${local.subnets[count.index].availability_zone}"
}
}
Now I want to use the for_each loop. But I cannot figure out how to do it. This is what I've done so far.
resource "aws_subnet" "subnets-dev" {
for_each = var.subnet-map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
But it keeps giving an error saying
Error: Unsupported attribute
on vpc/main.tf line 93, in resource "aws_subnet" "subnets-dev":
93: Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
|----------------
| each.value is tuple with 3 elements
This value does not have any attributes.
How could I fix this?
I'm not sure I fully follow all of what you tried here because your initial snippet of var.subnet-map shows it being a map of maps of lists of objects, but later on when you used for_each = var.subnet-map it seems to have treated it as a map of lists instead. Did you remove that extra level of maps (the "default" key) before trying for_each here?
Working with your original definition of variable "subnet-map", your first step with for_each will be similar to what you did with count: you need to flatten the structure, this time into a map of objects rather than a list of objects. The easiest way to get there is to derive a map from your existing flattened list:
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
subnets_map = {
for s in local.subnets: "${s.resource}:${s.availability_zone}" => s
}
}
Here I assumed that your "resource" string and your availability zone together are a suitable unique identifier for a subnet. If not, you can adjust the "${s.resource}:${s.availability_zone}" part to whatever unique key you want to use for these.
Now you can use the flattened map as the for_each map:
resource "aws_subnet" "subnets-dev" {
for_each = local.subnets_map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
This will give you instances with addresses like aws_subnet.subnets-dev["ec2:eu-west-1a"].
Note that if you are migrating from count with existing subnets that you wish to retain, you'll need to also do a one-time migration step to tell Terraform which indexes from the existing state correspond to which keys in the new configuration.
For example, if (and only if) index 0 was previously the one for ec2 in eu-west-1a, the migration command for that one would be:
terraform state mv 'aws_subnet.subnets-dev[0]' 'aws_subnet.subnets-dev["ec2:eu-west-1a"]'
If you're not sure how they correlate, you can run terraform plan after adding for_each and look at the instances that Terraform is planning to destroy. If you work through each one of those in turn, taking the address Terraform currently knows along with the resource and availability zone names shown in the Name tag, you can migrate each of them to its new address so that Terraform will no longer think you're asking for it to destroy the numbered instances and replace them with named ones.

Shuffle between subnets from two terraform maps as input for ec2 instance creation

I have two sets of CIDRs for each environment, i would like for terraform to shuffle between the two sets whenever a new instance is being created.
I have looked at the terraform random_shuffle provider and the merge function but these do not provide the solution to my problem.
resource "aws_subnet" "myapp" {
cidr_block = "${cidrsubnet(var.vpc_cidr[terraform.workspace], 5, count.index + 16 + 5)}"
}
variable "vpc_cidr" {
type = "map"
default = {
QA = "20.30.100.0/23"
TEST = "20.37.200.0/23"
PROD = "20.37.200.0/23"
DEV = "20.37.100.0/23"
}
}
locals {
"vpc_cidr_2" = {
QA = "10.30.182.0/23"
TEST = "10.37.238.0/23"
PROD = "<none>"
DEV = "<none>"
}
}
I would like cidr_block to be calculated based on either vpc_cidr or vpc_cidr2 and shuffle between each. Also it would need to check for and fall back to the other map if is found.
Note: vpc_cidr is a variable while vpc_cidr_2 is a local.
random_shuffle is the right resource you should work on, but you need change the idea to mix variable and locals together.
resource "aws_subnet" "myapp" {
vpc_id = "${aws_vpc.main.id}"
cidr_block = "${cidrsubnet(lookup(var.vpc_cidr[random_shuffle.vpc_cidr.result], terraform.workspace), 5, count.index + 16 + 5)}"
}
resource "random_shuffle" "vpc_cidr" {
input = ["vpc_cidr", "vpc_cidr_2"]
}
variable "vpc_cidr" {
type = "map"
default = {
vpc_cidr = {
"QA" = "20.30.100.0/23"
"TEST" = "20.37.200.0/23"
"PROD" = "20.37.200.0/23"
"DEV" = "20.37.100.0/23"
}
vpc_cidr_2 = {
"QA" = "10.30.182.0/23"
"TEST" = "10.37.238.0/23"
"PROD" = "<none>"
"DEV" = "<none>"
}
}
}