Terraform code for creating AWS EC2 instances with volumes - amazon-web-services

I'm trying to create two EC2 instances on AWS with the following features:
Instance: Ubuntu Server 18.04 LTS (HVM), SSD Volume Type
Type: ami for 64-bit x86 us-east-1 region ami-0747bdcabd34c712a (64-bit x86)
Type: 2 processors, 8 GB Memory, Up to 10 Gigabit Network, m5a type m5a.large
Number of instances: 2
Storage: 20 GB General Purpose SSD, Delete storage on termination
Tags: Name=lfs258_class
Allow all traffic from everywhere
Use the existing SSH Keypair I have on my laptop
This is the tree file structure
.
├── README.md
├── ec2.tf
├── outputs.tf
├── provider.tf
├── variables.tf
└── versions.tf
file ec2.tf
locals {
availability_zone = "${local.region}a"
name = "kubernetes-lfs258-course"
region = "us-east-1"
tags = {
Owner = "pss-cli-user1 "
Environment = "kubernetes-lfs258-course"
}
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 3.0"
name = local.name
azs = ["${local.region}a", "${local.region}b", "${local.region}c"]
public_subnets = lookup(var.init,"public-subnet")
tags = local.tags
}
module "security_group" {
source = "terraform-aws-modules/security-group/aws"
version = "~> 4.0"
name = local.name
description = "Security group for example usage with EC2 instance"
vpc_id = module.vpc.vpc_id
ingress_cidr_blocks = ["0.0.0.0/0"]
ingress_rules = ["all-all"]
egress_rules = ["all-all"]
tags = local.tags
}
################################################################################
# Supporting Resources for the EC2 module
################################################################################
module "ec2" {
source = "../../"
name = local.name
ami = lookup(var.init,"ami")
#instance_type = "c5.large"
instance_type = lookup(element(var.instances,0),"instance_type")
availability_zone = local.availability_zone
subnet_id = element(module.vpc.private_subnets, 0)
vpc_security_group_ids = [module.security_group.security_group_id]
associate_public_ip_address = true
tags = local.tags
}
resource "aws_volume_attachment" "this" {
device_name = "/dev/sdh"
volume_id = aws_ebs_volume.this.id
instance_id = module.ec2.id
}
resource "aws_ebs_volume" "this" {
availability_zone = local.availability_zone
size = 20
tags = local.tags
}
file outputs.tf
# EC2
output "ec2_id" {
description = "The ID of the instance"
value = module.ec2.id
}
output "ec2_arn" {
description = "The ARN of the instance"
value = module.ec2.arn
}
output "ec2_capacity_reservation_specification" {
description = "Capacity reservation specification of the instance"
value = module.ec2.capacity_reservation_specification
}
output "ec2_instance_state" {
description = "The state of the instance. One of: `pending`, `running`, `shutting-down`, `terminated`, `stopping`, `stopped`"
value = module.ec2.instance_state
}
output "ec2_primary_network_interface_id" {
description = "The ID of the instance's primary network interface"
value = module.ec2.primary_network_interface_id
}
output "ec2_private_dns" {
description = "The private DNS name assigned to the instance. Can only be used inside the Amazon EC2, and only available if you've enabled DNS hostnames for your VPC"
value = module.ec2.private_dns
}
output "ec2_public_dns" {
description = "The public DNS name assigned to the instance. For EC2-VPC, this is only available if you've enabled DNS hostnames for your VPC"
value = module.ec2.public_dns
}
output "ec2_public_ip" {
description = "The public IP address assigned to the instance, if applicable. NOTE: If you are using an aws_eip with your instance, you should refer to the EIP's address directly and not use `public_ip` as this field will change after the EIP is attached"
value = module.ec2.public_ip
}
output "ec2_tags_all" {
description = "A map of tags assigned to the resource, including those inherited from the provider default_tags configuration block"
value = module.ec2.tags_all
}
file provider. tf
provider "aws" {
region = local.region
profile = "pss-cli-user1"
shared_credentials_file = "~/.aws/credentials"
}
file variables.tf
# This file defines variables types and their initial hardcoded values
variable "zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
variable "instances" {
type = list(object({
instance_type = string
count = number
tags = map(string)
}))
# If instances is not defined in terraforms.tfvars use this value
default = [
{
instance_type = "m5a.large"
count = 2
tags = { "UsedFor" = "kubernetes lfs258 course"}
}
]
}
variable "init" {
type = object({
vpc-id=list(string),
public-subnet=list(string),
aws_region=string,
ami=string
vpc-sec-group= list(string)
})
# if not defined in terraform.tfvars takes this default
default = {
vpc-id = ["vpc-02938578"]
public-subnet = ["subnet-94e25d9a"]
aws_region = "us-east-1"
ami = "ami-0747bdcabd34c712a"
vpc-sec-group = ["sg-d60bf3f5"]
}
}
file versions.tf
terraform {
required_version = ">= 0.13.1"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.51"
}
}
}
The command terraform init works without errors
However terraform plan is giving me the following complains
╷
│ Error: Unsupported argument
│
│ on ec2.tf line 41, in module "ec2":
│ 41: name = local.name
│
│ An argument named "name" is not expected here.
╵
╷
│ Error: Unsupported argument
│
│ on ec2.tf line 43, in module "ec2":
│ 43: ami = lookup(var.init,"ami")
│
│ An argument named "ami" is not expected here.
..... more errors like this removed
Questions are :
What am I doing wrong and how to fix it ?
How to create a better IaC Terraform deployment?
BR
David

Related

Terraform.tfvars parsing lists for variables.tf file

I've got a couple of variables set in my variables.tf files which are of type list and I am trying to find the correct syntax on my .tfvars file
variables.tf
variable "subnet_cidrs" {
type = list(string)
description = "A list of Subnets CIDR's - Should consist of minimum 2"
}
variable "aws_region" {
type = string
description = "Region where all AWS Resources will be created"
}
variable "az" {
type = list(string)
description = "Availability-Zones - Should match numbers of CIDRs given and AWS Region"
}
terraform.tfvars
subnet_cidrs = ["192.168.1.100", "192.168.4.100"]
aws_region = "eu-central-1"
az = ["eu-central-1a", "eu-central-1b"]
A typical output i'll receive is :
│ Error: Incorrect attribute value type
│
│ on ..\ec2\ec2.tf line 21, in resource "aws_subnet" "public":
│ 21: availability_zone = var.az
│ ├────────────────
│ │ var.az is a list of dynamic
│
│ Inappropriate value for attribute "availability_zone": string required.
When brackets are removed for example : az = "eu-central-1a", "eu-central-1b"
It will return the following instead Argument definitions must be separated by newlines, not commas. An argument definition must end with a newline.
Edit
ec2.tf
I've made this as short as possible and included only the variables
# Main VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "vpc-${var.name_prefix}"
}
}
# Two Subnets in different AZ - Public IP on launch
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
cidr_block = var.subnet_cidrs
availability_zone = var.az
tags = {
Name = "subnet-${var.name_prefix}-${count.index}"
}
}
resource "aws_security_group" "ec2_sg" {
name = var.ec2_name
...
tags = {
Name = "EC2-SG-${var.name_prefix}"
}
}
# VM Key pair
resource "aws_key_pair" "auth" {
key_name = var.key_pair_name
public_key = file("~/.ssh/${var.ssh_file_name}.pub")
}
# EC2 Instance within 2 AZ's
resource "aws_instance" "ec2" {
count = length(var.subnet_cidrs)
tags = {
Name = "ubuntu-${var.name_prefix}-${count.index}"
}
}
Based on the variable definition and the way subnet resource is created, the following change is required:
resource "aws_subnet" "public" {
count = length(var.subnet_cidrs)
cidr_block = var.subnet_cidrs
availability_zone = var.az[count.index] # <---- using count.index here
tags = {
Name = "subnet-${var.name_prefix}-${count.index}"
}
}
This is needed because otherwise the problem will occur as availability_zone requires a single string value and passing only var.az will be an entire list. Using the count.index with the var.az will fetch a single value as is required.

getting error while using list(string) data type in terraform module

I am trying to create 2 subnets in aws (with terraform) by passing 2 values in single variable.
Getting below error while executing "terraform validate" command
Please guide me how to correctly define list(string) variable data type in terraform module and correctly use it.
│ Error: Invalid value for input variable
│
│ on usage-test.tf line 11, in module "vpc_module":
│ 11: subnet_cidr_block = ["10.0.0.0/24","10.0.1.0/24"]
│
│ The given value is not suitable for module.vpc_module.var.subnet_cidr_block declared at vpc/var-test.tf:21,1-29: string required.
╵
╷
│ Error: Invalid value for input variable
│
│ on usage-test.tf line 12, in module "vpc_module":
│ 12: subnet_az = ["ap-south-1a","ap-south-1b"]
│
│ The given value is not suitable for module.vpc_module.var.subnet_az declared at vpc/var-test.tf:25,1-21: string required.
╵
refer terraform files below:-
variable.tf:
variable "subnet_cidr_block" {
type = list(string)
}
variable "subnet_az" {
type = list(string)
}
main.tf:
resource "aws_subnet" "mysubnet_public" {
vpc_id = aws_vpc.myvpc.id
cidr_block = var.subnet_cidr_block
availability_zone = var.subnet_az
map_public_ip_on_launch = "true"
depends_on = [aws_internet_gateway.mygw]
}
usage.tf
provider "aws" {
region = "ap-south-1"
}
module "vpc_module" {
source = "./vpc"
vpc_cider_block = "10.0.0.0/16"
vpc_name = "myvpc"
route_table_name = "myrt"
subnet_cidr_block = ["10.0.0.0/24","10.0.1.0/24"]
subnet_az = ["ap-south-1a","ap-south-1b"]
# subnet_cidr_block = "10.0.0.0/24"
# subnet_az = "ap-south-1a"
# subnet_public_name = "mysubnet_public"
sg_mgmt_name = "mysg_mgmt"
}
Well, the error is pretty clear. You cannot use a list of strings, rather a single string value, as the provider documentation also shows [1]:
resource "aws_subnet" "main" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24" # <---- A single string value, not a list of strings
tags = {
Name = "Main"
}
}
As a hint for the future: the argument is singular, i.e., cidr_block so that usually means it's a single value.
[1] https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet#basic-usage
Thank you #Marko E for your suggestion
after some research found solution for this issue, refer below code.:-
main.tf
#below code is for creating multiple subnets
resource "aws_subnet" "mysubnet_public" {
count = length(var.public_subnet_cidr)
vpc_id = aws_vpc.myvpc.id
cidr_block = element(var.public_subnet_cidr,count.index)
availability_zone = element(var.azs,count.index)
map_public_ip_on_launch = true
tags = {
Name = "Subnet-${count.index+1}"
}
}
#below code is for associating above created multiple subnets to route table
resource "aws_route_table_association" "myroutetableassociation_public" {
count = length(var.public_subnet_cidr)
subnet_id = element(aws_subnet.mysubnet_public[*].id, count.index)
route_table_id = aws_route_table.myroutetable_public.id
}
output.tf
output "mysubnet_public" {
description = "List of IDs of public route tables"
value = aws_subnet.mysubnet_public[*].id
}
output "myroutetableassociation_public" {
value = aws_route_table_association.myroutetableassociation_public[*].id
}
variable.tf
variable "public_subnet_cidr" {
type = list
}
variable "azs" {
type = list
}
usage.tf
provider "aws" {
region = "ap-south-1"
}
module "vpc_module" {
source = "./vpc"
vpc_name = "myvpc"
public_subnet_cidr = ["10.0.0.0/24", "10.0.1.0/24"]
azs = ["ap-south-1a", "ap-south-1b"]
}

Terraform - cannot launch AWS EC2 instance with Launch Template

My EC2 instance resource code
resource "aws_instance" "my-sample-webapp-ec2" {
availability_zone = var.availability_zone
subnet_id = var.subnet_id
key_name = var.ec2_instance_name
instance_initiated_shutdown_behavior = "stop"
disable_api_termination = false
# vpc_security_group_ids = var.vpc_security_group_ids
launch_template {
id = var.launch_template_id
version = "$Latest"
}
tags = {
"Name" = var.ec2_instance_name
}
root_block_device {
delete_on_termination = true
}
}
My launch template already exists in AWS region - checked
My Module import for the above EC2 resource
module "aws_ec2_machines" {
source = "./modules/ec2_machines"
count = length(local.availability_zones)
launch_template_id = var.launch_template_id
launch_template_ver = var.launch_template_ver
ec2_instance_name = "${var.ec2_instance_name}-${count.index + 1}"
availability_zone = local.availability_zones[count.index]
}
what I want to do is below
Specify my launch template and launch EC2 instance(s)
Subnet association should happen based on availability_zone
Currently, I have only 3 subnets (1 per availability zone), but they are not default. Also, the VPC under which the subnets are created is also not the default VPC.
The error I am getting
│ Error: Error launching source instance: InvalidParameterValue: Value (us-east-2b) for parameter availabilityZone is invalid. Subnet 'subnet-xxxxxx' is in the availability zone us-east-2a
│ status code: 400, request id: 75a126cb-59eb-40fe-9fa5-579ed908edbd
│
│ with module.aws_ec2_machines[1].aws_instance.my-sample-webapp-ec2,
│ on modules\ec2_machines\main.tf line 7, in resource "aws_instance" "my-sample-webapp-ec2":
│ 7: resource "aws_instance" "my-sample-webapp-ec2" {
│
╵
What am I doing wrong?
OK - I figured out to problem (pen and pencil writing)
Launch Template ID does not need to have any subnet defined
But any VPC should have subnets created and (good practice) assigned to a different AZ
a. e.g. subnet-2a should be with AZ 1, subnet-2b should be with AZ 2 etc.
Now, with the below resource/module invocation - it all worked well
resource "aws_instance" "my-sample-webapp-ec2" {
subnet_id = var.subnet_id
instance_initiated_shutdown_behavior = "stop"
disable_api_termination = false
launch_template {
id = var.launch_template_id
version = "$Latest"
}
tags = {
"Name" = var.ec2_instance_name
}
root_block_device {
delete_on_termination = true
}
}
And the subsequent module call in main.tf
module "aws_ec2_machines" {
source = "./modules/ec2_machines"
count = length(local.availability_zones)
launch_template_id = var.launch_template_id
launch_template_ver = var.launch_template_ver
ec2_instance_name = "${var.ec2_instance_name}-${count.index + 1}"
subnet_id = local.subnets[count.index % local.available_subnet_count]
}

Attach each EIP to each Nat Gatway in Terraform

I'm creating two public subnets that will each contain a nat gateay. My code, attempts to create these nats per subnet, and then allocate the eip to each. However, since my for each starts the code block, it looks like the allocation id became us-east-* instead of the id of the eip.
Variables.tf:
variable "public_subnet_numbers" {
type = map(number)
description = "Map of AZ to a number that should be used for public subnets"
default = {
"us-east-1a" = 1
"us-east-1b" = 2
#"us-east-1c" = 3
}
}
variable "private_subnet_numbers" {
type = map(number)
description = "Map of AZ to a number that should be used for private subnets"
default = {
"us-east-1a" = 4
"us-east-1b" = 5
#"us-east-1c" = 6
}
}
variable "vpc_cidr" {
type = string
description = "The IP range to use for the VPC"
default = "192.168.0.0/16"
}
Main.tf:
resource "aws_eip" "nat" {
count = 2
vpc = true
lifecycle {
# prevent_destroy = true
}
tags = {
Name = "cf-${var.infra_env}-eip"
Project = "cf.io"
Environment = var.infra_env
VPC = aws_vpc.vpc.id
ManagedBy = "terraform"
Role = "private"
}
}
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = each.value.id #aws_subnet.public[each.key].id
allocation_id = aws_eip.nat[each.key].id
tags = {
Name = "cf-${var.infra_env}-ngw"
Project = "cf.io"
VPC = aws_vpc.vpc.id
Environment = var.infra_env
ManagedBy = "terraform"
Role = "private"
}
}
Error:
Error: Invalid index
│
│ on ../terraform/modules/networking/gateways.tf line 42, in resource "aws_nat_gateway" "ngw":
│ 42: allocation_id = aws_eip.nat[each.key].id
│ ├────────────────
│ │ aws_eip.nat is tuple with 2 elements
│ │ each.key is "us-east-1a"
│
│ The given key does not identify an element in this collection value: a number is required.
╵
╷
│ Error: Invalid index
│
│ on ../terraform/modules/networking/gateways.tf line 42, in resource "aws_nat_gateway" "ngw":
│ 42: allocation_id = aws_eip.nat[each.key].id
│ ├────────────────
│ │ aws_eip.nat is tuple with 2 elements
│ │ each.key is "us-east-1b"
│
│ The given key does not identify an element in this collection value: a number is required.
You're mixing count and for_each. The easiest way to solve this would be to use for_each in your EIP creation as well, which makes sense because you are creating an EIP for each NAT. That would also make your code work better if you decided to add another subnet later, you wouldn't need to go in and change the count from 2 to 3.
Otherwise, you need to use the index function to convert the each value to an index number.
As Mark B mentioned mixing the count and for_each is not recommended. In your current setup using exclusively for_each is the way to go based on the private_subnet_numbers variable.
In your aws_eip.nat resource change count to for_each
resource "aws_eip" "nat" {
for_each = var.private_subnet_numbers
vpc = true
}
Next in your resource aws_nat_gateway.ngw you should refer to subnet ids using each
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = aws_subnet.public[each.key].id
....
}
And the code as a whole for clarity
resource "aws_vpc" "vpc" {
... vpc configurations ...
}
resource "aws_subnet" "public" {
for_each = var.private_subnet_numbers
vpc_id = aws_vpc.vpc.id
... subnet configurations ...
}
resource "aws_eip" "nat" {
for_each = var.private_subnet_numbers
vpc = true
lifecycle {
# prevent_destroy = true
}
tags = {
Name = "cf-${var.infra_env}-eip"
Project = "cf.io"
Environment = var.infra_env
VPC = aws_vpc.vpc.id
ManagedBy = "terraform"
Role = "private"
}
}
resource "aws_nat_gateway" "ngw" {
for_each = var.private_subnet_numbers
subnet_id = aws_subnet.public[each.key].id
allocation_id = aws_eip.nat[each.key].id
tags = {
Name = "cf-${var.infra_env}-ngw"
Project = "cf.io"
VPC = aws_vpc.vpc.id
Environment = var.infra_env
ManagedBy = "terraform"
Role = "private"
}
}

Terraform change file layout

Hello i have 3 files in my terraform directory
vpc.tf aerospike-ec2.tf vars.tf
and here is contents of vpc.tf file
resource "aws_vpc" "wizzair-dev-qa-vpc" {
cidr_block = var.wizzair-dev-qa-vpc_cidr
tags = {
Environment = "dev-qa"
Name = "wizzair-aws-vpc"
Project = "Network"
}
}
data "aws_availability_zones" "available" {}
resource "aws_subnet" "private_subnets" {
vpc_id = aws_vpc.wizzair-dev-qa-vpc.id
cidr_block = var.subnet_cidr
availability_zone = "eu-north-1a"
tags = {
Environment = "dev-qa"
Project = "Network"
Name = "wizzair-aws-subnet-private"
}
}
here is my aerospike.tf file
resource "aws_network_interface" "private" {
subnet_id = aws_subnet.private_subnets.id
private_ips = ["10.249.10.4"]
security_groups = [aws_security_group.aerospike_traffic.id, aws_security_group.general.id]
tags = {
Environment = "dev"
Project = "wizzair"
Name = "aerospike-interface"
}
}
resource "aws_instance" "dev-wizzair-aerospike" {
ami = "ami-077b12cf33hb9a995"
availability_zone = "eu-north-1a"
instance_type = "t3.large"
key_name = "${var.generated_key_name}"
network_interface {
device_index=0
network_interface_id = aws_network_interface.private.id
}
tags = {
Environment = "dev"
Project = "wizzair"
Name = "aerospike-instance-dev"
}
}
resource "aws_ebs_volume" "dev-wizzair-aerospike-ebs" {
availability_zone = "eu-north-1a"
size = 10
tags = {
Environment = "dev"
Project = "wizzair"
Name = "aerospike-volume"
}
}
resource "aws_volume_attachment" "dev-wizzair-aerospike-ebs-att" {
device_name = "/dev/sdh"
volume_id = aws_ebs_volume.dev-wizzair-aerospike-ebs.id
instance_id = aws_instance.dev-wizzair-aerospike.id
}
and vars.tf
variable "wizzair-dev-qa-vpc-cidr" {
default = "10.249.10.0/24"
}
and if the files are in the same directory, then everything works, but if I create the aerospike directory and transfer the aerospike.tf file there, then go to the aerospike directory and enter terraform plan there, then an error occurs
mkdir aerospike && mv aerospike.tf aerospike && cd aerospike && terraform plan
terraform plan
╷
│ Error: Reference to undeclared resource
│
│ on main.tf line 2, in resource "aws_network_interface" "private":
│ 2: subnet_id = aws_subnet.private_subnets.id
│
│ A managed resource "aws_subnet" "private_subnets" has not been declared in the root module.
i've heard about state outputs, but how to deal with it in my case?
You can't just randomly move files to sub-folders. You have to construct TF modules for that, which you then have to appropriately call and use in your parent script.