From 50282b0f31f08304094c1ad5f3464975a15ea8df Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Mon, 23 Jun 2025 14:38:47 -0500 Subject: [PATCH 1/4] feat!: Upgrade AWS provider and min required Terraform version to `6.0` and `1.10` respectively --- .pre-commit-config.yaml | 2 +- README.md | 104 +-- UPGRADE-3.0.md | 1 - examples/complete/README.md | 11 +- examples/complete/main.tf | 202 +++--- examples/complete/versions.tf | 4 +- examples/session-manager/README.md | 11 +- examples/session-manager/main.tf | 28 +- examples/session-manager/versions.tf | 4 +- examples/volume-attachment/README.md | 68 -- examples/volume-attachment/main.tf | 94 --- examples/volume-attachment/outputs.tf | 50 -- examples/volume-attachment/variables.tf | 0 examples/volume-attachment/versions.tf | 10 - main.tf | 824 +++++++++++++++--------- outputs.tf | 24 +- variables.tf | 347 +++++++--- versions.tf | 4 +- wrappers/main.tf | 98 +-- wrappers/versions.tf | 4 +- 20 files changed, 1017 insertions(+), 873 deletions(-) delete mode 100644 examples/volume-attachment/README.md delete mode 100644 examples/volume-attachment/main.tf delete mode 100644 examples/volume-attachment/outputs.tf delete mode 100644 examples/volume-attachment/variables.tf delete mode 100644 examples/volume-attachment/versions.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 424b3710..b84d048d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.98.0 + rev: v1.99.4 hooks: - id: terraform_fmt - id: terraform_wrapper_module_for_each diff --git a/README.md b/README.md index 863555d1..4f1ee768 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,10 @@ module "ec2_instance" { name = "single-instance" - instance_type = "t2.micro" - key_name = "user1" - monitoring = true - vpc_security_group_ids = ["sg-12345678"] - subnet_id = "subnet-eddcdzz4" + instance_type = "t2.micro" + key_name = "user1" + monitoring = true + subnet_id = "subnet-eddcdzz4" tags = { Terraform = "true" @@ -37,11 +36,10 @@ module "ec2_instance" { name = "instance-${each.key}" - instance_type = "t2.micro" - key_name = "user1" - monitoring = true - vpc_security_group_ids = ["sg-12345678"] - subnet_id = "subnet-eddcdzz4" + instance_type = "t2.micro" + key_name = "user1" + monitoring = true + subnet_id = "subnet-eddcdzz4" tags = { Terraform = "true" @@ -62,11 +60,10 @@ module "ec2_instance" { spot_price = "0.60" spot_type = "persistent" - instance_type = "t2.micro" - key_name = "user1" - monitoring = true - vpc_security_group_ids = ["sg-12345678"] - subnet_id = "subnet-eddcdzz4" + instance_type = "t2.micro" + key_name = "user1" + monitoring = true + subnet_id = "subnet-eddcdzz4" tags = { Terraform = "true" @@ -85,7 +82,6 @@ Users of Terragrunt can achieve similar results by using modules provided in the - [Complete EC2 instance](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/complete) - [EC2 instance w/ private network access via Session Manager](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/session-manager) -- [EC2 instance with EBS volume attachment](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/volume-attachment) ## Make an encrypted AMI for use @@ -142,19 +138,31 @@ The following combinations are supported to conditionally create resources: - Disable resource creation (no resources created): ```hcl - create = false -``` +module "ec2_instance" { + source = "terraform-aws-modules/ec2-instance/aws" -- Create spot instance: + # Disable creation of EC2 and all resources + create = false -```hcl + # Enable creation of spot instance create_spot_instance = true + + # Enable creation of EC2 IAM instance profile + create_iam_instance_profile = true + + # Disable creation of security group + create_security_group = false + + # Enable creation of elastic IP + create_eip = true + + # ... omitted +} ``` ## Notes - `network_interface` can't be specified together with `vpc_security_group_ids`, `associate_public_ip_address`, `subnet_id`. See [complete example](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/complete) for details. -- Changes in `ebs_block_device` argument will be ignored. Use [aws_volume_attachment](https://www.terraform.io/docs/providers/aws/r/volume_attachment.html) resource to attach and detach volumes from AWS EC2 instances. See [this example](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/volume-attachment). - In regards to spot instances, you must grant the `AWSServiceRoleForEC2Spot` service-linked role access to any custom KMS keys, otherwise your spot request and instances will fail with `bad parameters`. You can see more details about why the request failed by using the awscli and `aws ec2 describe-spot-instance-requests` @@ -162,14 +170,14 @@ The following combinations are supported to conditionally create resources: | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.66 | +| [terraform](#requirement\_terraform) | >= 1.10 | +| [aws](#requirement\_aws) | >= 6.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.66 | +| [aws](#provider\_aws) | >= 6.0 | ## Modules @@ -179,46 +187,53 @@ No modules. | Name | Type | |------|------| +| [aws_ebs_volume.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ebs_volume) | resource | | [aws_eip.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | | [aws_iam_instance_profile.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_instance.ignore_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | | [aws_instance.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | +| [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_spot_instance_request.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/spot_instance_request) | resource | +| [aws_volume_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/volume_attachment) | resource | +| [aws_vpc_security_group_egress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource | +| [aws_vpc_security_group_ingress_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_ingress_rule) | resource | | [aws_iam_policy_document.assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | | [aws_ssm_parameter.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_subnet.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [ami](#input\_ami) | ID of AMI to use for the instance | `string` | `null` | no | -| [ami\_ssm\_parameter](#input\_ami\_ssm\_parameter) | SSM parameter name for the AMI ID. For Amazon Linux AMI SSM parameters see [reference](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-public-parameters-ami.html) | `string` | `"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"` | no | +| [ami\_ssm\_parameter](#input\_ami\_ssm\_parameter) | SSM parameter name for the AMI ID. For Amazon Linux AMI SSM parameters see [reference](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-public-parameters-ami.html) | `string` | `"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"` | no | | [associate\_public\_ip\_address](#input\_associate\_public\_ip\_address) | Whether to associate a public IP address with an instance in a VPC | `bool` | `null` | no | | [availability\_zone](#input\_availability\_zone) | AZ to start the instance in | `string` | `null` | no | -| [capacity\_reservation\_specification](#input\_capacity\_reservation\_specification) | Describes an instance's Capacity Reservation targeting option | `any` | `{}` | no | -| [cpu\_core\_count](#input\_cpu\_core\_count) | Sets the number of CPU cores for an instance | `number` | `null` | no | +| [capacity\_reservation\_specification](#input\_capacity\_reservation\_specification) | Describes an instance's Capacity Reservation targeting option |
object({
capacity_reservation_preference = optional(string)
capacity_reservation_target = optional(object({
capacity_reservation_id = optional(string)
capacity_reservation_resource_group_arn = optional(string)
}))
})
| `null` | no | | [cpu\_credits](#input\_cpu\_credits) | The credit option for CPU usage (unlimited or standard) | `string` | `null` | no | -| [cpu\_options](#input\_cpu\_options) | Defines CPU options to apply to the instance at launch time. | `any` | `{}` | no | -| [cpu\_threads\_per\_core](#input\_cpu\_threads\_per\_core) | Sets the number of CPU threads per core for an instance (has no effect unless cpu\_core\_count is also set) | `number` | `null` | no | +| [cpu\_options](#input\_cpu\_options) | Defines CPU options to apply to the instance at launch time. |
object({
amd_sev_snp = optional(string)
core_count = optional(number)
threads_per_core = optional(number)
})
| `null` | no | | [create](#input\_create) | Whether to create an instance | `bool` | `true` | no | | [create\_eip](#input\_create\_eip) | Determines whether a public EIP will be created and associated with the instance. | `bool` | `false` | no | | [create\_iam\_instance\_profile](#input\_create\_iam\_instance\_profile) | Determines whether an IAM instance profile is created or to use an existing IAM instance profile | `bool` | `false` | no | +| [create\_security\_group](#input\_create\_security\_group) | Determines whether a security group will be created | `bool` | `true` | no | | [create\_spot\_instance](#input\_create\_spot\_instance) | Depicts if the instance is a spot instance | `bool` | `false` | no | | [disable\_api\_stop](#input\_disable\_api\_stop) | If true, enables EC2 Instance Stop Protection | `bool` | `null` | no | | [disable\_api\_termination](#input\_disable\_api\_termination) | If true, enables EC2 Instance Termination Protection | `bool` | `null` | no | -| [ebs\_block\_device](#input\_ebs\_block\_device) | Additional EBS block devices to attach to the instance | `list(any)` | `[]` | no | | [ebs\_optimized](#input\_ebs\_optimized) | If true, the launched EC2 instance will be EBS-optimized | `bool` | `null` | no | +| [ebs\_volumes](#input\_ebs\_volumes) | Additional EBS volumes to attach to the instance |
map(object({
encrypted = optional(bool)
final_snapshot = optional(bool)
iops = optional(number)
kms_key_id = optional(string)
multi_attach_enabled = optional(bool)
outpost_arn = optional(string)
size = optional(number)
snapshot_id = optional(string)
tags = optional(map(string), {})
throughput = optional(number)
type = optional(string, "gp3")
# Attachment
device_name = optional(string) # Will fall back to use map key as device name
force_detach = optional(bool)
skip_destroy = optional(bool)
stop_instance_before_detaching = optional(bool)
}))
| `null` | no | | [eip\_domain](#input\_eip\_domain) | Indicates if this EIP is for use in VPC | `string` | `"vpc"` | no | | [eip\_tags](#input\_eip\_tags) | A map of additional tags to add to the eip | `map(string)` | `{}` | no | +| [enable\_primary\_ipv6](#input\_enable\_primary\_ipv6) | Whether to assign a primary IPv6 Global Unicast Address (GUA) to the instance when launched in a dual-stack or IPv6-only subnet | `bool` | `null` | no | | [enable\_volume\_tags](#input\_enable\_volume\_tags) | Whether to enable volume tags (if enabled it conflicts with root\_block\_device tags) | `bool` | `true` | no | | [enclave\_options\_enabled](#input\_enclave\_options\_enabled) | Whether Nitro Enclaves will be enabled on the instance. Defaults to `false` | `bool` | `null` | no | -| [ephemeral\_block\_device](#input\_ephemeral\_block\_device) | Customize Ephemeral (also known as Instance Store) volumes on the instance | `list(map(string))` | `[]` | no | +| [ephemeral\_block\_device](#input\_ephemeral\_block\_device) | Customize Ephemeral (also known as Instance Store) volumes on the instance |
map(object({
device_name = string
no_device = optional(bool)
virtual_name = optional(string)
}))
| `null` | no | | [get\_password\_data](#input\_get\_password\_data) | If true, wait for password data to become available and retrieve it | `bool` | `null` | no | | [hibernation](#input\_hibernation) | If true, the launched EC2 instance will support hibernation | `bool` | `null` | no | | [host\_id](#input\_host\_id) | ID of a dedicated host that the instance will be assigned to. Use when an instance is to be launched on a specific dedicated host | `string` | `null` | no | +| [host\_resource\_group\_arn](#input\_host\_resource\_group\_arn) | ARN of the host resource group in which to launch the instances. If you specify an ARN, omit the `tenancy` parameter or set it to `host` | `string` | `null` | no | | [iam\_instance\_profile](#input\_iam\_instance\_profile) | IAM Instance Profile to launch the instance with. Specified as the name of the Instance Profile | `string` | `null` | no | | [iam\_role\_description](#input\_iam\_role\_description) | Description of the role | `string` | `null` | no | | [iam\_role\_name](#input\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | @@ -229,26 +244,34 @@ No modules. | [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name` or `name`) is used as a prefix | `bool` | `true` | no | | [ignore\_ami\_changes](#input\_ignore\_ami\_changes) | Whether changes to the AMI ID changes should be ignored by Terraform. Note - changing this value will result in the replacement of the instance | `bool` | `false` | no | | [instance\_initiated\_shutdown\_behavior](#input\_instance\_initiated\_shutdown\_behavior) | Shutdown behavior for the instance. Amazon defaults this to stop for EBS-backed instances and terminate for instance-store instances. Cannot be set on instance-store instance | `string` | `null` | no | +| [instance\_market\_options](#input\_instance\_market\_options) | The market (purchasing) option for the instance. If set, overrides the `create_spot_instance` variable |
object({
market_type = optional(string)
spot_options = optional(object({
instance_interruption_behavior = optional(string)
max_price = optional(string)
spot_instance_type = optional(string)
valid_until = optional(string)
}))
})
| `null` | no | | [instance\_tags](#input\_instance\_tags) | Additional tags for the instance | `map(string)` | `{}` | no | | [instance\_type](#input\_instance\_type) | The type of instance to start | `string` | `"t3.micro"` | no | | [ipv6\_address\_count](#input\_ipv6\_address\_count) | A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet | `number` | `null` | no | | [ipv6\_addresses](#input\_ipv6\_addresses) | Specify one or more IPv6 addresses from the range of the subnet to associate with the primary network interface | `list(string)` | `null` | no | | [key\_name](#input\_key\_name) | Key name of the Key Pair to use for the instance; which can be managed using the `aws_key_pair` resource | `string` | `null` | no | -| [launch\_template](#input\_launch\_template) | Specifies a Launch Template to configure the instance. Parameters configured on this resource will override the corresponding parameters in the Launch Template | `map(string)` | `{}` | no | -| [maintenance\_options](#input\_maintenance\_options) | The maintenance options for the instance | `any` | `{}` | no | -| [metadata\_options](#input\_metadata\_options) | Customize the metadata options of the instance | `map(string)` |
{
"http_endpoint": "enabled",
"http_put_response_hop_limit": 1,
"http_tokens": "required"
}
| no | +| [launch\_template](#input\_launch\_template) | Specifies a Launch Template to configure the instance. Parameters configured on this resource will override the corresponding parameters in the Launch Template |
object({
id = optional(string)
name = optional(string)
version = optional(string)
})
| `null` | no | +| [maintenance\_options](#input\_maintenance\_options) | The maintenance options for the instance |
object({
auto_recovery = optional(string)
})
| `null` | no | +| [metadata\_options](#input\_metadata\_options) | Customize the metadata options of the instance |
object({
http_endpoint = optional(string, "enabled")
http_protocol_ipv6 = optional(string)
http_put_response_hop_limit = optional(number, 1)
http_tokens = optional(string, "required")
instance_metadata_tags = optional(string)
})
|
{
"http_endpoint": "enabled",
"http_put_response_hop_limit": 1,
"http_tokens": "required"
}
| no | | [monitoring](#input\_monitoring) | If true, the launched EC2 instance will have detailed monitoring enabled | `bool` | `null` | no | | [name](#input\_name) | Name to be used on EC2 instance created | `string` | `""` | no | -| [network\_interface](#input\_network\_interface) | Customize network interfaces to be attached at instance boot time | `list(map(string))` | `[]` | no | +| [network\_interface](#input\_network\_interface) | Customize network interfaces to be attached at instance boot time |
map(object({
delete_on_termination = optional(bool)
device_index = optional(number) # Will fall back to use map key as device index
network_card_index = optional(number)
network_interface_id = string
}))
| `null` | no | | [placement\_group](#input\_placement\_group) | The Placement Group to start the instance in | `string` | `null` | no | -| [private\_dns\_name\_options](#input\_private\_dns\_name\_options) | Customize the private DNS name options of the instance | `map(string)` | `{}` | no | +| [placement\_partition\_number](#input\_placement\_partition\_number) | Number of the partition the instance is in. Valid only if the `aws_placement_group` resource's `strategy` argument is set to `partition` | `number` | `null` | no | +| [private\_dns\_name\_options](#input\_private\_dns\_name\_options) | Customize the private DNS name options of the instance |
object({
enable_resource_name_dns_a_record = optional(bool)
enable_resource_name_dns_aaaa_record = optional(bool)
hostname_type = optional(string)
})
| `null` | no | | [private\_ip](#input\_private\_ip) | Private IP address to associate with the instance in a VPC | `string` | `null` | no | | [putin\_khuylo](#input\_putin\_khuylo) | Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo! | `bool` | `true` | no | -| [root\_block\_device](#input\_root\_block\_device) | Customize details about the root block device of the instance. See Block Devices below for details | `list(any)` | `[]` | no | +| [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | +| [root\_block\_device](#input\_root\_block\_device) | Customize details about the root block device of the instance. See Block Devices below for details |
map(object({
delete_on_termination = optional(bool)
encrypted = optional(bool)
iops = optional(number)
kms_key_id = optional(string)
tags = optional(map(string), {})
throughput = optional(number)
size = optional(number)
type = optional(string)
}))
| `null` | no | | [secondary\_private\_ips](#input\_secondary\_private\_ips) | A list of secondary private IPv4 addresses to assign to the instance's primary network interface (eth0) in a VPC. Can only be assigned to the primary network interface (eth0) attached at instance creation, not a pre-existing network interface i.e. referenced in a `network_interface block` | `list(string)` | `null` | no | +| [security\_group\_description](#input\_security\_group\_description) | Description of the security group | `string` | `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
|
{
"ipv4_default": {
"cidr_ipv4": "0.0.0.0/0",
"description": "Allow all IPv4 traffic",
"ip_protocol": "-1"
},
"ipv6_default": {
"cidr_ipv6": "::/0",
"description": "Allow all IPv6 traffic",
"ip_protocol": "-1"
}
}
| no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `null` | no | +| [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | +| [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | +| [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name` or `name`) is used as a prefix | `bool` | `true` | no | +| [security\_group\_vpc\_id](#input\_security\_group\_vpc\_id) | VPC ID to create the security group in. If not set, the security group will be created in the default VPC | `string` | `null` | no | | [source\_dest\_check](#input\_source\_dest\_check) | Controls if traffic is routed to the instance when the destination address does not match the instance. Used for NAT or VPNs | `bool` | `null` | no | -| [spot\_block\_duration\_minutes](#input\_spot\_block\_duration\_minutes) | The required duration for the Spot instances, in minutes. This value must be a multiple of 60 (60, 120, 180, 240, 300, or 360) | `number` | `null` | no | -| [spot\_instance\_interruption\_behavior](#input\_spot\_instance\_interruption\_behavior) | Indicates Spot instance behavior when it is interrupted. Valid values are `terminate`, `stop`, or `hibernate` | `string` | `null` | no | | [spot\_launch\_group](#input\_spot\_launch\_group) | A launch group is a group of spot instances that launch together and terminate together. If left empty instances are launched and terminated individually | `string` | `null` | no | | [spot\_price](#input\_spot\_price) | The maximum price to request on the spot market. Defaults to on-demand price | `string` | `null` | no | | [spot\_type](#input\_spot\_type) | If set to one-time, after the instance is terminated, the spot request will be closed. Default `persistent` | `string` | `null` | no | @@ -263,7 +286,7 @@ No modules. | [user\_data\_base64](#input\_user\_data\_base64) | Can be used instead of user\_data to pass base64-encoded binary data directly. Use this instead of user\_data whenever the value is not a valid UTF-8 string. For example, gzip-encoded user data must be base64-encoded and passed via this argument to avoid corruption | `string` | `null` | no | | [user\_data\_replace\_on\_change](#input\_user\_data\_replace\_on\_change) | When used in combination with user\_data or user\_data\_base64 will trigger a destroy and recreate when set to true. Defaults to false if not set | `bool` | `null` | no | | [volume\_tags](#input\_volume\_tags) | A mapping of tags to assign to the devices created by the instance at launch time | `map(string)` | `{}` | no | -| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of security group IDs to associate with | `list(string)` | `null` | no | +| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | A list of security group IDs to associate with | `list(string)` | `[]` | no | ## Outputs @@ -274,6 +297,7 @@ No modules. | [availability\_zone](#output\_availability\_zone) | The availability zone of the created instance | | [capacity\_reservation\_specification](#output\_capacity\_reservation\_specification) | Capacity reservation specification of the instance | | [ebs\_block\_device](#output\_ebs\_block\_device) | EBS block device information | +| [ebs\_volumes](#output\_ebs\_volumes) | Map of EBS volumes created and their attributes | | [ephemeral\_block\_device](#output\_ephemeral\_block\_device) | Ephemeral block device information | | [iam\_instance\_profile\_arn](#output\_iam\_instance\_profile\_arn) | ARN assigned by AWS to the instance profile | | [iam\_instance\_profile\_id](#output\_iam\_instance\_profile\_id) | Instance profile's ID | diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 46175fb0..7bdfea98 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -3,7 +3,6 @@ If you have any questions regarding this upgrade process, please consult the `examples` directory: - [Complete](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/complete) -- [Volume Attachment](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/volume-attachment) If you find a bug, please open an issue with supporting configuration to reproduce. diff --git a/examples/complete/README.md b/examples/complete/README.md index 1658ff2a..26ccb1db 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -19,14 +19,14 @@ Note that this example may create resources which can cost money. Run `terraform | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.66 | +| [terraform](#requirement\_terraform) | >= 1.10 | +| [aws](#requirement\_aws) | >= 6.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.66 | +| [aws](#provider\_aws) | >= 6.0 | ## Modules @@ -44,8 +44,8 @@ Note that this example may create resources which can cost money. Run `terraform | [ec2\_t2\_unlimited](#module\_ec2\_t2\_unlimited) | ../../ | n/a | | [ec2\_t3\_unlimited](#module\_ec2\_t3\_unlimited) | ../../ | n/a | | [ec2\_targeted\_capacity\_reservation](#module\_ec2\_targeted\_capacity\_reservation) | ../../ | n/a | -| [security\_group](#module\_security\_group) | terraform-aws-modules/security-group/aws | ~> 4.0 | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | +| [security\_group](#module\_security\_group) | terraform-aws-modules/security-group/aws | ~> 5.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | ## Resources @@ -57,7 +57,6 @@ Note that this example may create resources which can cost money. Run `terraform | [aws_network_interface.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_interface) | resource | | [aws_placement_group.web](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/placement_group) | resource | | [aws_ami.amazon_linux](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | -| [aws_ami.amazon_linux_23](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | ## Inputs diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 465e7902..bc48ed11 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -59,31 +59,29 @@ module "ec2_complete" { threads_per_core = 1 } enable_volume_tags = false - root_block_device = [ - { - encrypted = true - volume_type = "gp3" - throughput = 200 - volume_size = 50 + root_block_device = { + main = { + encrypted = true + type = "gp3" + throughput = 200 + size = 50 tags = { Name = "my-root-block" } }, - ] - - ebs_block_device = [ - { - device_name = "/dev/sdf" - volume_type = "gp3" - volume_size = 5 - throughput = 200 - encrypted = true - kms_key_id = aws_kms_key.this.arn + } + + ebs_volumes = { + "/dev/sdf" = { + size = 5 + throughput = 200 + encrypted = true + kms_key_id = aws_kms_key.this.arn tags = { MountPoint = "/mnt/data" } } - ] + } tags = local.tags } @@ -93,13 +91,12 @@ module "ec2_network_interface" { name = "${local.name}-network-interface" - network_interface = [ - { - device_index = 0 + network_interface = { + 0 = { network_interface_id = aws_network_interface.this.id delete_on_termination = false } - ] + } tags = local.tags } @@ -109,8 +106,7 @@ module "ec2_metadata_options" { name = "${local.name}-metadata-options" - subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] + subnet_id = element(module.vpc.private_subnets, 0) metadata_options = { http_endpoint = "enabled" @@ -130,7 +126,6 @@ module "ec2_t2_unlimited" { instance_type = "t2.micro" cpu_credits = "unlimited" subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] associate_public_ip_address = true maintenance_options = { @@ -148,7 +143,6 @@ module "ec2_t3_unlimited" { instance_type = "t3.micro" cpu_credits = "unlimited" 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 @@ -171,11 +165,10 @@ module "ec2_ignore_ami_changes" { ignore_ami_changes = true - ami = data.aws_ami.amazon_linux.id - instance_type = "t2.micro" - availability_zone = element(module.vpc.azs, 0) - subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] + ami = data.aws_ami.amazon_linux.id + instance_type = "t2.micro" + availability_zone = element(module.vpc.azs, 0) + subnet_id = element(module.vpc.private_subnets, 0) tags = local.tags } @@ -190,29 +183,29 @@ locals { instance_type = "t3.micro" availability_zone = element(module.vpc.azs, 0) subnet_id = element(module.vpc.private_subnets, 0) - root_block_device = [ - { - encrypted = true - volume_type = "gp3" - throughput = 200 - volume_size = 50 + root_block_device = { + main = { + encrypted = true + type = "gp3" + throughput = 200 + size = 50 tags = { Name = "my-root-block" } } - ] + } } two = { instance_type = "t3.small" availability_zone = element(module.vpc.azs, 1) subnet_id = element(module.vpc.private_subnets, 1) - root_block_device = [ - { - encrypted = true - volume_type = "gp2" - volume_size = 50 + root_block_device = { + main = { + encrypted = true + type = "gp2" + size = 50 } - ] + } } three = { instance_type = "t3.medium" @@ -229,13 +222,12 @@ module "ec2_multiple" { name = "${local.name}-multi-${each.key}" - instance_type = each.value.instance_type - availability_zone = each.value.availability_zone - subnet_id = each.value.subnet_id - vpc_security_group_ids = [module.security_group.security_group_id] + instance_type = each.value.instance_type + availability_zone = each.value.availability_zone + subnet_id = each.value.subnet_id enable_volume_tags = false - root_block_device = lookup(each.value, "root_block_device", []) + root_block_device = try(each.value.root_block_device, null) tags = local.tags } @@ -256,10 +248,9 @@ module "ec2_spot_instance" { associate_public_ip_address = true # Spot request specific attributes - spot_price = "0.1" - spot_wait_for_fulfillment = true - spot_type = "persistent" - spot_instance_interruption_behavior = "terminate" + spot_price = "0.1" + spot_wait_for_fulfillment = true + spot_type = "persistent" # End spot request specific attributes user_data_base64 = base64encode(local.user_data) @@ -270,28 +261,26 @@ module "ec2_spot_instance" { } enable_volume_tags = false - root_block_device = [ - { - encrypted = true - volume_type = "gp3" - throughput = 200 - volume_size = 50 + root_block_device = { + main = { + encrypted = true + type = "gp3" + throughput = 200 + size = 50 tags = { Name = "my-root-block" } - }, - ] - - ebs_block_device = [ - { - device_name = "/dev/sdf" - volume_type = "gp3" - volume_size = 5 - throughput = 200 - encrypted = true + } + } + + ebs_volumes = { + "/dev/sdf" = { + size = 5 + throughput = 200 + encrypted = true # kms_key_id = aws_kms_key.this.arn # you must grant the AWSServiceRoleForEC2Spot service-linked role access to any custom KMS keys } - ] + } tags = local.tags } @@ -305,10 +294,8 @@ module "ec2_open_capacity_reservation" { name = "${local.name}-open-capacity-reservation" - ami = data.aws_ami.amazon_linux.id - instance_type = "t3.micro" + instance_type = "m4.large" subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] associate_public_ip_address = false capacity_reservation_specification = { @@ -325,10 +312,8 @@ module "ec2_targeted_capacity_reservation" { name = "${local.name}-targeted-capacity-reservation" - ami = data.aws_ami.amazon_linux.id - instance_type = "t3.micro" + instance_type = "m4.large" subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] associate_public_ip_address = false capacity_reservation_specification = { @@ -341,7 +326,7 @@ module "ec2_targeted_capacity_reservation" { } resource "aws_ec2_capacity_reservation" "open" { - instance_type = "t3.micro" + instance_type = "m4.large" instance_platform = "Linux/UNIX" availability_zone = "${local.region}a" instance_count = 1 @@ -349,7 +334,7 @@ resource "aws_ec2_capacity_reservation" "open" { } resource "aws_ec2_capacity_reservation" "targeted" { - instance_type = "t3.micro" + instance_type = "m4.large" instance_platform = "Linux/UNIX" availability_zone = "${local.region}a" instance_count = 1 @@ -363,13 +348,12 @@ resource "aws_ec2_capacity_reservation" "targeted" { module "ec2_cpu_options" { source = "../../" - name = "${local.name}-cpu-options" + create = false + name = "${local.name}-cpu-options" - ami = data.aws_ami.amazon_linux_23.id instance_type = "c6a.xlarge" # used to set core count below and test amd_sev_snp attribute availability_zone = element(module.vpc.azs, 0) subnet_id = element(module.vpc.private_subnets, 0) - vpc_security_group_ids = [module.security_group.security_group_id] placement_group = aws_placement_group.web.id associate_public_ip_address = true disable_api_stop = false @@ -389,31 +373,29 @@ module "ec2_cpu_options" { amd_sev_snp = "enabled" } enable_volume_tags = false - root_block_device = [ - { - encrypted = true - volume_type = "gp3" - throughput = 200 - volume_size = 50 + root_block_device = { + main = { + encrypted = true + type = "gp3" + throughput = 200 + size = 50 tags = { Name = "my-root-block" } - }, - ] - - ebs_block_device = [ - { - device_name = "/dev/sdf" - volume_type = "gp3" - volume_size = 5 - throughput = 200 - encrypted = true - kms_key_id = aws_kms_key.this.arn + } + } + + ebs_volumes = { + "/dev/sdf" = { + size = 5 + throughput = 200 + encrypted = true + kms_key_id = aws_kms_key.this.arn tags = { MountPoint = "/mnt/data" } } - ] + } instance_tags = { Persistence = "09:00-18:00" } @@ -426,7 +408,7 @@ module "ec2_cpu_options" { module "vpc" { source = "terraform-aws-modules/vpc/aws" - version = "~> 5.0" + version = "~> 6.0" name = local.name cidr = local.vpc_cidr @@ -441,26 +423,12 @@ module "vpc" { data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"] - - filter { - name = "name" - values = ["amzn-ami-hvm-*-x86_64-gp2"] - } -} - -data "aws_ami" "amazon_linux_23" { - most_recent = true - owners = ["amazon"] - - filter { - name = "name" - values = ["al2023-ami-2023*-x86_64"] - } + name_regex = "^al2023-ami-2023.*-x86_64" } module "security_group" { source = "terraform-aws-modules/security-group/aws" - version = "~> 4.0" + version = "~> 5.0" name = local.name description = "Security group for example usage with EC2 instance" @@ -468,7 +436,6 @@ module "security_group" { ingress_cidr_blocks = ["0.0.0.0/0"] ingress_rules = ["http-80-tcp", "all-icmp"] - egress_rules = ["all-all"] tags = local.tags } @@ -482,5 +449,6 @@ resource "aws_kms_key" "this" { } resource "aws_network_interface" "this" { - subnet_id = element(module.vpc.private_subnets, 0) + subnet_id = element(module.vpc.private_subnets, 0) + security_groups = [module.security_group.security_group_id] } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index fd4d1167..f648e20c 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.66" + version = ">= 6.0" } } } diff --git a/examples/session-manager/README.md b/examples/session-manager/README.md index 07552482..b778d379 100644 --- a/examples/session-manager/README.md +++ b/examples/session-manager/README.md @@ -29,23 +29,22 @@ Note that this example may create resources which can cost money. Run `terraform | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.66 | +| [terraform](#requirement\_terraform) | >= 1.10 | +| [aws](#requirement\_aws) | >= 6.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 4.66 | +| [aws](#provider\_aws) | >= 6.0 | ## Modules | Name | Source | Version | |------|--------|---------| | [ec2](#module\_ec2) | ../../ | n/a | -| [security\_group\_instance](#module\_security\_group\_instance) | terraform-aws-modules/security-group/aws | ~> 5.0 | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 | -| [vpc\_endpoints](#module\_vpc\_endpoints) | terraform-aws-modules/vpc/aws//modules/vpc-endpoints | ~> 5.0 | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | +| [vpc\_endpoints](#module\_vpc\_endpoints) | terraform-aws-modules/vpc/aws//modules/vpc-endpoints | ~> 6.0 | ## Resources diff --git a/examples/session-manager/main.tf b/examples/session-manager/main.tf index b7896da3..37d1e964 100644 --- a/examples/session-manager/main.tf +++ b/examples/session-manager/main.tf @@ -27,8 +27,14 @@ module "ec2" { name = local.name - subnet_id = element(module.vpc.intra_subnets, 0) - vpc_security_group_ids = [module.security_group_instance.security_group_id] + subnet_id = element(module.vpc.intra_subnets, 0) + security_group_egress_rules = { + vpc-endpoints = { + description = "Allow outbound traffic to VPC endpoints" + cidr_ipv4 = module.vpc.intra_subnets_cidr_blocks + from_port = 443 + } + } create_iam_instance_profile = true iam_role_description = "IAM role for EC2 instance" @@ -45,7 +51,7 @@ module "ec2" { module "vpc" { source = "terraform-aws-modules/vpc/aws" - version = "~> 5.0" + version = "~> 6.0" name = local.name cidr = local.vpc_cidr @@ -56,23 +62,9 @@ module "vpc" { tags = local.tags } -module "security_group_instance" { - source = "terraform-aws-modules/security-group/aws" - version = "~> 5.0" - - name = "${local.name}-ec2" - description = "Security Group for EC2 Instance Egress" - - vpc_id = module.vpc.vpc_id - - egress_rules = ["https-443-tcp"] - - tags = local.tags -} - module "vpc_endpoints" { source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints" - version = "~> 5.0" + version = "~> 6.0" vpc_id = module.vpc.vpc_id diff --git a/examples/session-manager/versions.tf b/examples/session-manager/versions.tf index fd4d1167..f648e20c 100644 --- a/examples/session-manager/versions.tf +++ b/examples/session-manager/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.66" + version = ">= 6.0" } } } diff --git a/examples/volume-attachment/README.md b/examples/volume-attachment/README.md deleted file mode 100644 index 181dd3f9..00000000 --- a/examples/volume-attachment/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# EC2 instance with EBS volume attachment - -Configuration in this directory creates EC2 instances, EBS volume and attach it together. - -This example outputs instance id and EBS volume id. - -## Usage - -To run this example you need to execute: - -```bash -$ terraform init -$ terraform plan -$ terraform apply -``` - -Note that this example may create resources which can cost money. Run `terraform destroy` when you don't need these resources. - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 4.66 | - -## Providers - -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | >= 4.66 | - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [ec2](#module\_ec2) | ../../ | n/a | -| [security\_group](#module\_security\_group) | terraform-aws-modules/security-group/aws | ~> 4.0 | -| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 4.0 | - -## Resources - -| Name | Type | -|------|------| -| [aws_ebs_volume.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ebs_volume) | resource | -| [aws_volume_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/volume_attachment) | resource | -| [aws_ami.amazon_linux](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | -| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | - -## Inputs - -No inputs. - -## Outputs - -| Name | Description | -|------|-------------| -| [ec2\_arn](#output\_ec2\_arn) | The ARN of the instance | -| [ec2\_availability\_zone](#output\_ec2\_availability\_zone) | The availability zone of the created spot instance | -| [ec2\_capacity\_reservation\_specification](#output\_ec2\_capacity\_reservation\_specification) | Capacity reservation specification of the instance | -| [ec2\_id](#output\_ec2\_id) | The ID of the instance | -| [ec2\_instance\_state](#output\_ec2\_instance\_state) | The state of the instance. One of: `pending`, `running`, `shutting-down`, `terminated`, `stopping`, `stopped` | -| [ec2\_primary\_network\_interface\_id](#output\_ec2\_primary\_network\_interface\_id) | The ID of the instance's primary network interface | -| [ec2\_private\_dns](#output\_ec2\_private\_dns) | 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 | -| [ec2\_public\_dns](#output\_ec2\_public\_dns) | The public DNS name assigned to the instance. For EC2-VPC, this is only available if you've enabled DNS hostnames for your VPC | -| [ec2\_public\_ip](#output\_ec2\_public\_ip) | 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 | -| [ec2\_tags\_all](#output\_ec2\_tags\_all) | A map of tags assigned to the resource, including those inherited from the provider default\_tags configuration block | - diff --git a/examples/volume-attachment/main.tf b/examples/volume-attachment/main.tf deleted file mode 100644 index 877f2140..00000000 --- a/examples/volume-attachment/main.tf +++ /dev/null @@ -1,94 +0,0 @@ -provider "aws" { - region = local.region -} - -data "aws_availability_zones" "available" {} - -locals { - name = "ex-${basename(path.cwd)}" - region = "eu-west-1" - - vpc_cidr = "10.0.0.0/16" - azs = slice(data.aws_availability_zones.available.names, 0, 3) - - tags = { - Name = local.name - Example = local.name - Repository = "https://github.com/terraform-aws-modules/terraform-aws-ec2-instance" - } -} - -################################################################################ -# EC2 Module -################################################################################ - -module "ec2" { - source = "../../" - - name = local.name - - ami = data.aws_ami.amazon_linux.id - instance_type = "c5.large" - availability_zone = element(local.azs, 0) - 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 = module.ec2.availability_zone - size = 1 - - tags = local.tags -} - -################################################################################ -# Supporting Resources -################################################################################ - -module "vpc" { - source = "terraform-aws-modules/vpc/aws" - version = "~> 4.0" - - name = local.name - cidr = local.vpc_cidr - - azs = local.azs - private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] - public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] - - tags = local.tags -} - -data "aws_ami" "amazon_linux" { - most_recent = true - owners = ["amazon"] - - filter { - name = "name" - values = ["amzn-ami-hvm-*-x86_64-gp2"] - } -} - -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 = ["http-80-tcp", "all-icmp"] - egress_rules = ["all-all"] - - tags = local.tags -} diff --git a/examples/volume-attachment/outputs.tf b/examples/volume-attachment/outputs.tf deleted file mode 100644 index a927767b..00000000 --- a/examples/volume-attachment/outputs.tf +++ /dev/null @@ -1,50 +0,0 @@ -# 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 -} - -output "ec2_availability_zone" { - description = "The availability zone of the created spot instance" - value = module.ec2.availability_zone -} diff --git a/examples/volume-attachment/variables.tf b/examples/volume-attachment/variables.tf deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/volume-attachment/versions.tf b/examples/volume-attachment/versions.tf deleted file mode 100644 index fd4d1167..00000000 --- a/examples/volume-attachment/versions.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = ">= 4.66" - } - } -} diff --git a/main.tf b/main.tf index 01a520d5..73b448c7 100644 --- a/main.tf +++ b/main.tf @@ -6,11 +6,27 @@ locals { is_t_instance_type = replace(var.instance_type, "/^t(2|3|3a|4g){1}\\..*$/", "1") == "1" ? true : false ami = try(coalesce(var.ami, try(nonsensitive(data.aws_ssm_parameter.this[0].value), null)), null) + + instance_id = try( + aws_instance.this[0].id, + aws_instance.ignore_ami[0].id, + aws_spot_instance_request.this[0].id, + null, + ) + + instance_availability_zone = try( + aws_instance.this[0].availability_zone, + aws_instance.ignore_ami[0].availability_zone, + aws_spot_instance_request.this[0].availability_zone, + null, + ) } data "aws_ssm_parameter" "this" { count = local.create && var.ami == null ? 1 : 0 + region = var.region + name = var.ami_ssm_parameter } @@ -21,175 +37,193 @@ data "aws_ssm_parameter" "this" { resource "aws_instance" "this" { count = local.create && !var.ignore_ami_changes && !var.create_spot_instance ? 1 : 0 - ami = local.ami - instance_type = var.instance_type - cpu_core_count = var.cpu_core_count - cpu_threads_per_core = var.cpu_threads_per_core - hibernation = var.hibernation - - user_data = var.user_data - user_data_base64 = var.user_data_base64 - user_data_replace_on_change = var.user_data_replace_on_change - - availability_zone = var.availability_zone - subnet_id = var.subnet_id - vpc_security_group_ids = var.vpc_security_group_ids - - key_name = var.key_name - monitoring = var.monitoring - get_password_data = var.get_password_data - iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + region = var.region + ami = local.ami associate_public_ip_address = var.associate_public_ip_address - private_ip = var.private_ip - secondary_private_ips = var.secondary_private_ips - ipv6_address_count = var.ipv6_address_count - ipv6_addresses = var.ipv6_addresses - - ebs_optimized = var.ebs_optimized - - dynamic "cpu_options" { - for_each = length(var.cpu_options) > 0 ? [var.cpu_options] : [] - - content { - core_count = try(cpu_options.value.core_count, null) - threads_per_core = try(cpu_options.value.threads_per_core, null) - amd_sev_snp = try(cpu_options.value.amd_sev_snp, null) - } - } + availability_zone = var.availability_zone dynamic "capacity_reservation_specification" { - for_each = length(var.capacity_reservation_specification) > 0 ? [var.capacity_reservation_specification] : [] + for_each = var.capacity_reservation_specification != null ? [var.capacity_reservation_specification] : [] content { - capacity_reservation_preference = try(capacity_reservation_specification.value.capacity_reservation_preference, null) + capacity_reservation_preference = capacity_reservation_specification.value.capacity_reservation_preference dynamic "capacity_reservation_target" { - for_each = try([capacity_reservation_specification.value.capacity_reservation_target], []) + for_each = capacity_reservation_specification.value.capacity_reservation_target != null ? [capacity_reservation_specification.value.capacity_reservation_target] : [] content { - capacity_reservation_id = try(capacity_reservation_target.value.capacity_reservation_id, null) - capacity_reservation_resource_group_arn = try(capacity_reservation_target.value.capacity_reservation_resource_group_arn, null) + capacity_reservation_id = capacity_reservation_target.value.capacity_reservation_id + capacity_reservation_resource_group_arn = capacity_reservation_target.value.capacity_reservation_resource_group_arn } } } } - dynamic "root_block_device" { - for_each = var.root_block_device + dynamic "cpu_options" { + for_each = var.cpu_options != null ? [var.cpu_options] : [] content { - delete_on_termination = try(root_block_device.value.delete_on_termination, null) - encrypted = try(root_block_device.value.encrypted, null) - iops = try(root_block_device.value.iops, null) - kms_key_id = lookup(root_block_device.value, "kms_key_id", null) - volume_size = try(root_block_device.value.volume_size, null) - volume_type = try(root_block_device.value.volume_type, null) - throughput = try(root_block_device.value.throughput, null) - tags = try(root_block_device.value.tags, null) + amd_sev_snp = cpu_options.value.amd_sev_snp + core_count = cpu_options.value.core_count + threads_per_core = cpu_options.value.threads_per_core } } - dynamic "ebs_block_device" { - for_each = var.ebs_block_device + credit_specification { + cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + } + + disable_api_stop = var.disable_api_stop + disable_api_termination = var.disable_api_termination - content { - delete_on_termination = try(ebs_block_device.value.delete_on_termination, null) - device_name = ebs_block_device.value.device_name - encrypted = try(ebs_block_device.value.encrypted, null) - iops = try(ebs_block_device.value.iops, null) - kms_key_id = lookup(ebs_block_device.value, "kms_key_id", null) - snapshot_id = lookup(ebs_block_device.value, "snapshot_id", null) - volume_size = try(ebs_block_device.value.volume_size, null) - volume_type = try(ebs_block_device.value.volume_type, null) - throughput = try(ebs_block_device.value.throughput, null) - tags = try(ebs_block_device.value.tags, null) - } + # `ebs_block_device` managed by separate resource + + ebs_optimized = var.ebs_optimized + + enclave_options { + enabled = var.enclave_options_enabled } + enable_primary_ipv6 = var.enable_primary_ipv6 + dynamic "ephemeral_block_device" { - for_each = var.ephemeral_block_device + for_each = var.ephemeral_block_device != null ? var.ephemeral_block_device : {} content { device_name = ephemeral_block_device.value.device_name - no_device = try(ephemeral_block_device.value.no_device, null) - virtual_name = try(ephemeral_block_device.value.virtual_name, null) + no_device = ephemeral_block_device.value.no_device + virtual_name = ephemeral_block_device.value.virtual_name } } - dynamic "metadata_options" { - for_each = length(var.metadata_options) > 0 ? [var.metadata_options] : [] + get_password_data = var.get_password_data + hibernation = var.hibernation + host_id = var.host_id + host_resource_group_arn = var.host_resource_group_arn + iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior + + dynamic "instance_market_options" { + for_each = var.instance_market_options != null ? [var.instance_market_options] : [] content { - http_endpoint = try(metadata_options.value.http_endpoint, "enabled") - http_tokens = try(metadata_options.value.http_tokens, "required") - http_put_response_hop_limit = try(metadata_options.value.http_put_response_hop_limit, 1) - instance_metadata_tags = try(metadata_options.value.instance_metadata_tags, null) + market_type = instance_market_options.value.market_type + + dynamic "spot_options" { + for_each = instance_market_options.value.spot_options != null ? [instance_market_options.value.spot_options] : [] + + content { + instance_interruption_behavior = spot_options.value.instance_interruption_behavior + max_price = spot_options.value.max_price + spot_instance_type = spot_options.value.spot_instance_type + valid_until = spot_options.value.valid_until + } + } } } - dynamic "network_interface" { - for_each = var.network_interface + instance_type = var.instance_type + ipv6_address_count = var.ipv6_address_count + ipv6_addresses = var.ipv6_addresses + key_name = var.key_name + + dynamic "launch_template" { + for_each = var.launch_template != null ? [var.launch_template] : [] content { - device_index = network_interface.value.device_index - network_interface_id = lookup(network_interface.value, "network_interface_id", null) - delete_on_termination = try(network_interface.value.delete_on_termination, false) + id = launch_template.value.id + name = launch_template.value.name + version = launch_template.value.version } } - dynamic "private_dns_name_options" { - for_each = length(var.private_dns_name_options) > 0 ? [var.private_dns_name_options] : [] + dynamic "maintenance_options" { + for_each = var.maintenance_options != null ? [var.maintenance_options] : [] content { - hostname_type = try(private_dns_name_options.value.hostname_type, null) - enable_resource_name_dns_a_record = try(private_dns_name_options.value.enable_resource_name_dns_a_record, null) - enable_resource_name_dns_aaaa_record = try(private_dns_name_options.value.enable_resource_name_dns_aaaa_record, null) + auto_recovery = maintenance_options.value.auto_recovery } } - dynamic "launch_template" { - for_each = length(var.launch_template) > 0 ? [var.launch_template] : [] + dynamic "metadata_options" { + for_each = var.metadata_options != null ? [var.metadata_options] : [] content { - id = lookup(var.launch_template, "id", null) - name = lookup(var.launch_template, "name", null) - version = lookup(var.launch_template, "version", null) + http_endpoint = metadata_options.value.http_endpoint + http_protocol_ipv6 = metadata_options.value.http_protocol_ipv6 + http_put_response_hop_limit = metadata_options.value.http_put_response_hop_limit + http_tokens = metadata_options.value.http_tokens + instance_metadata_tags = metadata_options.value.instance_metadata_tags } } - dynamic "maintenance_options" { - for_each = length(var.maintenance_options) > 0 ? [var.maintenance_options] : [] + monitoring = var.monitoring + + dynamic "network_interface" { + for_each = var.network_interface != null ? var.network_interface : {} content { - auto_recovery = try(maintenance_options.value.auto_recovery, null) + delete_on_termination = network_interface.value.delete_on_termination + device_index = coalesce(network_interface.value.device_index, network_interface.key) + network_card_index = network_interface.value.network_card_index + network_interface_id = network_interface.value.network_interface_id + } } - enclave_options { - enabled = var.enclave_options_enabled + placement_group = var.placement_group + placement_partition_number = var.placement_partition_number + + dynamic "private_dns_name_options" { + for_each = var.private_dns_name_options != null ? [var.private_dns_name_options] : [] + + content { + enable_resource_name_dns_aaaa_record = private_dns_name_options.value.enable_resource_name_dns_aaaa_record + enable_resource_name_dns_a_record = private_dns_name_options.value.enable_resource_name_dns_a_record + hostname_type = private_dns_name_options.value.hostname_type + } } - source_dest_check = length(var.network_interface) > 0 ? null : var.source_dest_check - disable_api_termination = var.disable_api_termination - disable_api_stop = var.disable_api_stop - instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior - placement_group = var.placement_group - tenancy = var.tenancy - host_id = var.host_id + private_ip = var.private_ip - credit_specification { - cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + dynamic "root_block_device" { + for_each = var.root_block_device != null ? var.root_block_device : {} + + content { + delete_on_termination = root_block_device.value.delete_on_termination + encrypted = root_block_device.value.encrypted + iops = root_block_device.value.iops + kms_key_id = root_block_device.value.kms_key_id + tags = root_block_device.value.tags + throughput = root_block_device.value.throughput + volume_size = root_block_device.value.size + volume_type = root_block_device.value.type + } } + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + + tags = merge( + var.tags, + var.instance_tags, + { "Name" = var.name }, + ) + + tenancy = var.tenancy + user_data = var.user_data + user_data_base64 = var.user_data_base64 + user_data_replace_on_change = var.user_data_replace_on_change + volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null + timeouts { create = try(var.timeouts.create, null) update = try(var.timeouts.update, null) delete = try(var.timeouts.delete, null) } - - tags = merge({ "Name" = var.name }, var.instance_tags, var.tags) - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null } ################################################################################ @@ -199,176 +233,194 @@ resource "aws_instance" "this" { resource "aws_instance" "ignore_ami" { count = local.create && var.ignore_ami_changes && !var.create_spot_instance ? 1 : 0 - ami = local.ami - instance_type = var.instance_type - cpu_core_count = var.cpu_core_count - cpu_threads_per_core = var.cpu_threads_per_core - hibernation = var.hibernation - - user_data = var.user_data - user_data_base64 = var.user_data_base64 - user_data_replace_on_change = var.user_data_replace_on_change - - availability_zone = var.availability_zone - subnet_id = var.subnet_id - vpc_security_group_ids = var.vpc_security_group_ids - - key_name = var.key_name - monitoring = var.monitoring - get_password_data = var.get_password_data - iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + region = var.region + ami = local.ami associate_public_ip_address = var.associate_public_ip_address - private_ip = var.private_ip - secondary_private_ips = var.secondary_private_ips - ipv6_address_count = var.ipv6_address_count - ipv6_addresses = var.ipv6_addresses - - ebs_optimized = var.ebs_optimized - - dynamic "cpu_options" { - for_each = length(var.cpu_options) > 0 ? [var.cpu_options] : [] - - content { - core_count = try(cpu_options.value.core_count, null) - threads_per_core = try(cpu_options.value.threads_per_core, null) - amd_sev_snp = try(cpu_options.value.amd_sev_snp, null) - } - } + availability_zone = var.availability_zone dynamic "capacity_reservation_specification" { - for_each = length(var.capacity_reservation_specification) > 0 ? [var.capacity_reservation_specification] : [] + for_each = var.capacity_reservation_specification != null ? [var.capacity_reservation_specification] : [] content { - capacity_reservation_preference = try(capacity_reservation_specification.value.capacity_reservation_preference, null) + capacity_reservation_preference = capacity_reservation_specification.value.capacity_reservation_preference dynamic "capacity_reservation_target" { - for_each = try([capacity_reservation_specification.value.capacity_reservation_target], []) + for_each = capacity_reservation_specification.value.capacity_reservation_target != null ? [capacity_reservation_specification.value.capacity_reservation_target] : [] content { - capacity_reservation_id = try(capacity_reservation_target.value.capacity_reservation_id, null) - capacity_reservation_resource_group_arn = try(capacity_reservation_target.value.capacity_reservation_resource_group_arn, null) + capacity_reservation_id = capacity_reservation_target.value.capacity_reservation_id + capacity_reservation_resource_group_arn = capacity_reservation_target.value.capacity_reservation_resource_group_arn } } } } - dynamic "root_block_device" { - for_each = var.root_block_device + dynamic "cpu_options" { + for_each = var.cpu_options != null ? [var.cpu_options] : [] content { - delete_on_termination = try(root_block_device.value.delete_on_termination, null) - encrypted = try(root_block_device.value.encrypted, null) - iops = try(root_block_device.value.iops, null) - kms_key_id = lookup(root_block_device.value, "kms_key_id", null) - volume_size = try(root_block_device.value.volume_size, null) - volume_type = try(root_block_device.value.volume_type, null) - throughput = try(root_block_device.value.throughput, null) - tags = try(root_block_device.value.tags, null) + amd_sev_snp = cpu_options.value.amd_sev_snp + core_count = cpu_options.value.core_count + threads_per_core = cpu_options.value.threads_per_core } } - dynamic "ebs_block_device" { - for_each = var.ebs_block_device + credit_specification { + cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + } + + disable_api_stop = var.disable_api_stop + disable_api_termination = var.disable_api_termination - content { - delete_on_termination = try(ebs_block_device.value.delete_on_termination, null) - device_name = ebs_block_device.value.device_name - encrypted = try(ebs_block_device.value.encrypted, null) - iops = try(ebs_block_device.value.iops, null) - kms_key_id = lookup(ebs_block_device.value, "kms_key_id", null) - snapshot_id = lookup(ebs_block_device.value, "snapshot_id", null) - volume_size = try(ebs_block_device.value.volume_size, null) - volume_type = try(ebs_block_device.value.volume_type, null) - throughput = try(ebs_block_device.value.throughput, null) - tags = try(ebs_block_device.value.tags, null) - } + # `ebs_block_device` managed by separate resource + + ebs_optimized = var.ebs_optimized + + enclave_options { + enabled = var.enclave_options_enabled } + enable_primary_ipv6 = var.enable_primary_ipv6 + dynamic "ephemeral_block_device" { - for_each = var.ephemeral_block_device + for_each = var.ephemeral_block_device != null ? var.ephemeral_block_device : {} content { device_name = ephemeral_block_device.value.device_name - no_device = try(ephemeral_block_device.value.no_device, null) - virtual_name = try(ephemeral_block_device.value.virtual_name, null) + no_device = ephemeral_block_device.value.no_device + virtual_name = ephemeral_block_device.value.virtual_name } } - dynamic "metadata_options" { - for_each = length(var.metadata_options) > 0 ? [var.metadata_options] : [] + get_password_data = var.get_password_data + hibernation = var.hibernation + host_id = var.host_id + host_resource_group_arn = var.host_resource_group_arn + iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior + + dynamic "instance_market_options" { + for_each = var.instance_market_options != null ? [var.instance_market_options] : [] content { - http_endpoint = try(metadata_options.value.http_endpoint, "enabled") - http_tokens = try(metadata_options.value.http_tokens, "required") - http_put_response_hop_limit = try(metadata_options.value.http_put_response_hop_limit, 1) - instance_metadata_tags = try(metadata_options.value.instance_metadata_tags, null) + market_type = instance_market_options.value.market_type + + dynamic "spot_options" { + for_each = instance_market_options.value.spot_options != null ? [instance_market_options.value.spot_options] : [] + + content { + instance_interruption_behavior = spot_options.value.instance_interruption_behavior + max_price = spot_options.value.max_price + spot_instance_type = spot_options.value.spot_instance_type + valid_until = spot_options.value.valid_until + } + } } } - dynamic "network_interface" { - for_each = var.network_interface + instance_type = var.instance_type + ipv6_address_count = var.ipv6_address_count + ipv6_addresses = var.ipv6_addresses + key_name = var.key_name + + dynamic "launch_template" { + for_each = var.launch_template != null ? [var.launch_template] : [] content { - device_index = network_interface.value.device_index - network_interface_id = lookup(network_interface.value, "network_interface_id", null) - delete_on_termination = try(network_interface.value.delete_on_termination, false) + id = launch_template.value.id + name = launch_template.value.name + version = launch_template.value.version } } - dynamic "private_dns_name_options" { - for_each = length(var.private_dns_name_options) > 0 ? [var.private_dns_name_options] : [] + dynamic "maintenance_options" { + for_each = var.maintenance_options != null ? [var.maintenance_options] : [] content { - hostname_type = try(private_dns_name_options.value.hostname_type, null) - enable_resource_name_dns_a_record = try(private_dns_name_options.value.enable_resource_name_dns_a_record, null) - enable_resource_name_dns_aaaa_record = try(private_dns_name_options.value.enable_resource_name_dns_aaaa_record, null) + auto_recovery = maintenance_options.value.auto_recovery } } - dynamic "launch_template" { - for_each = length(var.launch_template) > 0 ? [var.launch_template] : [] + dynamic "metadata_options" { + for_each = var.metadata_options != null ? [var.metadata_options] : [] content { - id = lookup(var.launch_template, "id", null) - name = lookup(var.launch_template, "name", null) - version = lookup(var.launch_template, "version", null) + http_endpoint = metadata_options.value.http_endpoint + http_protocol_ipv6 = metadata_options.value.http_protocol_ipv6 + http_put_response_hop_limit = metadata_options.value.http_put_response_hop_limit + http_tokens = metadata_options.value.http_tokens + instance_metadata_tags = metadata_options.value.instance_metadata_tags } } - dynamic "maintenance_options" { - for_each = length(var.maintenance_options) > 0 ? [var.maintenance_options] : [] + monitoring = var.monitoring + + dynamic "network_interface" { + for_each = var.network_interface != null ? var.network_interface : {} content { - auto_recovery = try(maintenance_options.value.auto_recovery, null) + delete_on_termination = network_interface.value.delete_on_termination + device_index = coalesce(network_interface.value.device_index, network_interface.key) + network_card_index = network_interface.value.network_card_index + network_interface_id = network_interface.value.network_interface_id + } } - enclave_options { - enabled = var.enclave_options_enabled + placement_group = var.placement_group + placement_partition_number = var.placement_partition_number + + dynamic "private_dns_name_options" { + for_each = var.private_dns_name_options != null ? [var.private_dns_name_options] : [] + + content { + enable_resource_name_dns_aaaa_record = private_dns_name_options.value.enable_resource_name_dns_aaaa_record + enable_resource_name_dns_a_record = private_dns_name_options.value.enable_resource_name_dns_a_record + hostname_type = private_dns_name_options.value.hostname_type + } } - source_dest_check = length(var.network_interface) > 0 ? null : var.source_dest_check - disable_api_termination = var.disable_api_termination - disable_api_stop = var.disable_api_stop - instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior - placement_group = var.placement_group - tenancy = var.tenancy - host_id = var.host_id + private_ip = var.private_ip - credit_specification { - cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + dynamic "root_block_device" { + for_each = var.root_block_device != null ? var.root_block_device : {} + + content { + delete_on_termination = root_block_device.value.delete_on_termination + encrypted = root_block_device.value.encrypted + iops = root_block_device.value.iops + kms_key_id = root_block_device.value.kms_key_id + tags = root_block_device.value.tags + throughput = root_block_device.value.throughput + volume_size = root_block_device.value.size + volume_type = root_block_device.value.type + } } + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + + tags = merge( + var.tags, + var.instance_tags, + { "Name" = var.name }, + ) + + tenancy = var.tenancy + user_data = var.user_data + user_data_base64 = var.user_data_base64 + user_data_replace_on_change = var.user_data_replace_on_change + volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null + timeouts { create = try(var.timeouts.create, null) update = try(var.timeouts.update, null) delete = try(var.timeouts.delete, null) } - tags = merge({ "Name" = var.name }, var.instance_tags, var.tags) - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null - lifecycle { ignore_changes = [ ami @@ -383,165 +435,225 @@ resource "aws_instance" "ignore_ami" { resource "aws_spot_instance_request" "this" { count = local.create && var.create_spot_instance ? 1 : 0 - ami = local.ami - instance_type = var.instance_type - cpu_core_count = var.cpu_core_count - cpu_threads_per_core = var.cpu_threads_per_core - hibernation = var.hibernation + region = var.region - user_data = var.user_data - user_data_base64 = var.user_data_base64 - user_data_replace_on_change = var.user_data_replace_on_change + # Spot request specific attributes + launch_group = var.spot_launch_group + spot_price = var.spot_price + spot_type = var.spot_type + wait_for_fulfillment = var.spot_wait_for_fulfillment + valid_from = var.spot_valid_from + valid_until = var.spot_valid_until + # End spot request specific attributes - availability_zone = var.availability_zone - subnet_id = var.subnet_id - vpc_security_group_ids = var.vpc_security_group_ids + ami = local.ami + associate_public_ip_address = var.associate_public_ip_address + availability_zone = var.availability_zone - key_name = var.key_name - monitoring = var.monitoring - get_password_data = var.get_password_data - iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + dynamic "capacity_reservation_specification" { + for_each = var.capacity_reservation_specification != null ? [var.capacity_reservation_specification] : [] - associate_public_ip_address = var.associate_public_ip_address - private_ip = var.private_ip - secondary_private_ips = var.secondary_private_ips - ipv6_address_count = var.ipv6_address_count - ipv6_addresses = var.ipv6_addresses + content { + capacity_reservation_preference = capacity_reservation_specification.value.capacity_reservation_preference - ebs_optimized = var.ebs_optimized + dynamic "capacity_reservation_target" { + for_each = capacity_reservation_specification.value.capacity_reservation_target != null ? [capacity_reservation_specification.value.capacity_reservation_target] : [] - # Spot request specific attributes - spot_price = var.spot_price - wait_for_fulfillment = var.spot_wait_for_fulfillment - spot_type = var.spot_type - launch_group = var.spot_launch_group - block_duration_minutes = var.spot_block_duration_minutes - instance_interruption_behavior = var.spot_instance_interruption_behavior - valid_until = var.spot_valid_until - valid_from = var.spot_valid_from - # End spot request specific attributes + content { + capacity_reservation_id = capacity_reservation_target.value.capacity_reservation_id + capacity_reservation_resource_group_arn = capacity_reservation_target.value.capacity_reservation_resource_group_arn + } + } + } + } dynamic "cpu_options" { - for_each = length(var.cpu_options) > 0 ? [var.cpu_options] : [] + for_each = var.cpu_options != null ? [var.cpu_options] : [] content { - core_count = try(cpu_options.value.core_count, null) - threads_per_core = try(cpu_options.value.threads_per_core, null) - amd_sev_snp = try(cpu_options.value.amd_sev_snp, null) + amd_sev_snp = cpu_options.value.amd_sev_snp + core_count = cpu_options.value.core_count + threads_per_core = cpu_options.value.threads_per_core } } - dynamic "capacity_reservation_specification" { - for_each = length(var.capacity_reservation_specification) > 0 ? [var.capacity_reservation_specification] : [] + credit_specification { + cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + } - content { - capacity_reservation_preference = try(capacity_reservation_specification.value.capacity_reservation_preference, null) + disable_api_stop = var.disable_api_stop + disable_api_termination = var.disable_api_termination - dynamic "capacity_reservation_target" { - for_each = try([capacity_reservation_specification.value.capacity_reservation_target], []) - content { - capacity_reservation_id = try(capacity_reservation_target.value.capacity_reservation_id, null) - capacity_reservation_resource_group_arn = try(capacity_reservation_target.value.capacity_reservation_resource_group_arn, null) - } - } - } + # `ebs_block_device` managed by separate resource + + ebs_optimized = var.ebs_optimized + + enclave_options { + enabled = var.enclave_options_enabled } - dynamic "root_block_device" { - for_each = var.root_block_device + enable_primary_ipv6 = var.enable_primary_ipv6 + + dynamic "ephemeral_block_device" { + for_each = var.ephemeral_block_device != null ? var.ephemeral_block_device : {} content { - delete_on_termination = try(root_block_device.value.delete_on_termination, null) - encrypted = try(root_block_device.value.encrypted, null) - iops = try(root_block_device.value.iops, null) - kms_key_id = lookup(root_block_device.value, "kms_key_id", null) - volume_size = try(root_block_device.value.volume_size, null) - volume_type = try(root_block_device.value.volume_type, null) - throughput = try(root_block_device.value.throughput, null) - tags = try(root_block_device.value.tags, null) + device_name = ephemeral_block_device.value.device_name + no_device = ephemeral_block_device.value.no_device + virtual_name = ephemeral_block_device.value.virtual_name } } - dynamic "ebs_block_device" { - for_each = var.ebs_block_device + get_password_data = var.get_password_data + hibernation = var.hibernation + host_id = var.host_id + host_resource_group_arn = var.host_resource_group_arn + iam_instance_profile = var.create_iam_instance_profile ? aws_iam_instance_profile.this[0].name : var.iam_instance_profile + instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior + + instance_type = var.instance_type + ipv6_address_count = var.ipv6_address_count + ipv6_addresses = var.ipv6_addresses + key_name = var.key_name + + dynamic "launch_template" { + for_each = var.launch_template != null ? [var.launch_template] : [] content { - delete_on_termination = try(ebs_block_device.value.delete_on_termination, null) - device_name = ebs_block_device.value.device_name - encrypted = try(ebs_block_device.value.encrypted, null) - iops = try(ebs_block_device.value.iops, null) - kms_key_id = lookup(ebs_block_device.value, "kms_key_id", null) - snapshot_id = lookup(ebs_block_device.value, "snapshot_id", null) - volume_size = try(ebs_block_device.value.volume_size, null) - volume_type = try(ebs_block_device.value.volume_type, null) - throughput = try(ebs_block_device.value.throughput, null) - tags = try(ebs_block_device.value.tags, null) + id = launch_template.value.id + name = launch_template.value.name + version = launch_template.value.version } } - dynamic "ephemeral_block_device" { - for_each = var.ephemeral_block_device + dynamic "maintenance_options" { + for_each = var.maintenance_options != null ? [var.maintenance_options] : [] content { - device_name = ephemeral_block_device.value.device_name - no_device = try(ephemeral_block_device.value.no_device, null) - virtual_name = try(ephemeral_block_device.value.virtual_name, null) + auto_recovery = maintenance_options.value.auto_recovery } } dynamic "metadata_options" { - for_each = length(var.metadata_options) > 0 ? [var.metadata_options] : [] + for_each = var.metadata_options != null ? [var.metadata_options] : [] content { - http_endpoint = try(metadata_options.value.http_endpoint, "enabled") - http_tokens = try(metadata_options.value.http_tokens, "required") - http_put_response_hop_limit = try(metadata_options.value.http_put_response_hop_limit, 1) - instance_metadata_tags = try(metadata_options.value.instance_metadata_tags, null) + http_endpoint = metadata_options.value.http_endpoint + http_protocol_ipv6 = metadata_options.value.http_protocol_ipv6 + http_put_response_hop_limit = metadata_options.value.http_put_response_hop_limit + http_tokens = metadata_options.value.http_tokens + instance_metadata_tags = metadata_options.value.instance_metadata_tags } } + monitoring = var.monitoring + dynamic "network_interface" { - for_each = var.network_interface + for_each = var.network_interface != null ? var.network_interface : {} content { - device_index = network_interface.value.device_index - network_interface_id = lookup(network_interface.value, "network_interface_id", null) - delete_on_termination = try(network_interface.value.delete_on_termination, false) + delete_on_termination = network_interface.value.delete_on_termination + device_index = try(network_interface.value.device_index, network_interface.key) + network_card_index = network_interface.value.network_card_index + network_interface_id = network_interface.value.network_interface_id + } } - dynamic "launch_template" { - for_each = length(var.launch_template) > 0 ? [var.launch_template] : [] + placement_group = var.placement_group + placement_partition_number = var.placement_partition_number + + dynamic "private_dns_name_options" { + for_each = var.private_dns_name_options != null ? [var.private_dns_name_options] : [] content { - id = lookup(var.launch_template, "id", null) - name = lookup(var.launch_template, "name", null) - version = lookup(var.launch_template, "version", null) + enable_resource_name_dns_aaaa_record = private_dns_name_options.value.enable_resource_name_dns_aaaa_record + enable_resource_name_dns_a_record = private_dns_name_options.value.enable_resource_name_dns_a_record + hostname_type = private_dns_name_options.value.hostname_type } } - enclave_options { - enabled = var.enclave_options_enabled - } + private_ip = var.private_ip - source_dest_check = length(var.network_interface) > 0 ? null : var.source_dest_check - disable_api_termination = var.disable_api_termination - instance_initiated_shutdown_behavior = var.instance_initiated_shutdown_behavior - placement_group = var.placement_group - tenancy = var.tenancy - host_id = var.host_id + dynamic "root_block_device" { + for_each = var.root_block_device != null ? var.root_block_device : {} - credit_specification { - cpu_credits = local.is_t_instance_type ? var.cpu_credits : null + content { + delete_on_termination = root_block_device.value.delete_on_termination + encrypted = root_block_device.value.encrypted + iops = root_block_device.value.iops + kms_key_id = root_block_device.value.kms_key_id + tags = root_block_device.value.tags + throughput = root_block_device.value.throughput + volume_size = try(root_block_device.value.volume_size, root_block_device.value.size, null) + volume_type = try(root_block_device.value.volume_type, root_block_device.value.type, null) + } } + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + + tags = merge( + var.tags, + var.instance_tags, + { "Name" = var.name }, + ) + + tenancy = var.tenancy + user_data = var.user_data + user_data_base64 = var.user_data_base64 + user_data_replace_on_change = var.user_data_replace_on_change + volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null + timeouts { create = try(var.timeouts.create, null) delete = try(var.timeouts.delete, null) } +} + +################################################################################ +# EBS Volume(s) +################################################################################ + +resource "aws_ebs_volume" "this" { + for_each = var.create && var.ebs_volumes != null ? var.ebs_volumes : {} + + region = var.region + + availability_zone = local.instance_availability_zone + encrypted = each.value.encrypted + final_snapshot = each.value.final_snapshot + iops = each.value.iops + kms_key_id = each.value.kms_key_id + multi_attach_enabled = each.value.multi_attach_enabled + outpost_arn = each.value.outpost_arn + size = each.value.size + snapshot_id = each.value.snapshot_id + + tags = merge( + var.tags, + var.volume_tags, + { "Name" = "${var.name}-${each.key}" }, + each.value.tags, + ) + + throughput = each.value.throughput + type = each.value.type +} + +resource "aws_volume_attachment" "this" { + for_each = var.create && var.ebs_volumes != null ? var.ebs_volumes : {} - tags = merge({ "Name" = var.name }, var.instance_tags, var.tags) - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + region = var.region + + device_name = coalesce(each.value.device_name, each.key) + instance_id = local.instance_id + volume_id = aws_ebs_volume.this[each.key].id + force_detach = each.value.force_detach + skip_destroy = each.value.skip_destroy + stop_instance_before_detaching = each.value.stop_instance_before_detaching } ################################################################################ @@ -604,6 +716,92 @@ resource "aws_iam_instance_profile" "this" { } } +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = var.create && var.create_security_group && var.network_interface == null + security_group_name = try(coalesce(var.security_group_name, var.name), "") + + vpc_security_group_ids = local.create_security_group ? concat(var.vpc_security_group_ids, [aws_security_group.this[0].id]) : var.vpc_security_group_ids +} + +data "aws_subnet" "this" { + count = local.create_security_group && var.subnet_id != null ? 1 : 0 + + id = var.subnet_id +} + +resource "aws_security_group" "this" { + count = local.create_security_group ? 1 : 0 + + region = var.region + + name = var.security_group_use_name_prefix ? null : local.security_group_name + name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null + description = var.security_group_description + vpc_id = coalesce(var.security_group_vpc_id, data.aws_subnet.this[0].vpc_id) + + tags = merge( + var.tags, + { "Name" = local.security_group_name }, + var.security_group_tags + ) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_vpc_security_group_egress_rule" "this" { + for_each = local.create_security_group && var.security_group_egress_rules != null ? var.security_group_egress_rules : {} + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = each.value.from_port + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + + tags = merge( + var.tags, + var.security_group_tags, + { "Name" = "${var.name}-${each.key}" }, + each.value.tags, + ) + + to_port = each.value.to_port +} + +resource "aws_vpc_security_group_ingress_rule" "this" { + for_each = local.create_security_group && var.security_group_ingress_rules != null ? var.security_group_ingress_rules : {} + + region = var.region + + cidr_ipv4 = each.value.cidr_ipv4 + cidr_ipv6 = each.value.cidr_ipv6 + description = each.value.description + from_port = each.value.from_port + ip_protocol = each.value.ip_protocol + prefix_list_id = each.value.prefix_list_id + referenced_security_group_id = each.value.referenced_security_group_id + security_group_id = aws_security_group.this[0].id + + tags = merge( + var.tags, + var.security_group_tags, + { "Name" = "${var.name}-${each.key}" }, + each.value.tags, + ) + + to_port = each.value.to_port +} + ################################################################################ # Elastic IP ################################################################################ @@ -611,12 +809,10 @@ resource "aws_iam_instance_profile" "this" { resource "aws_eip" "this" { count = local.create && var.create_eip && !var.create_spot_instance ? 1 : 0 - instance = try( - aws_instance.this[0].id, - aws_instance.ignore_ami[0].id, - ) + region = var.region - domain = var.eip_domain + domain = var.eip_domain + instance = local.instance_id tags = merge(var.tags, var.eip_tags) } diff --git a/outputs.tf b/outputs.tf index 3f57b650..3b6a9818 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,11 +1,6 @@ output "id" { description = "The ID of the instance" - value = try( - aws_instance.this[0].id, - aws_instance.ignore_ami[0].id, - aws_spot_instance_request.this[0].id, - null, - ) + value = local.instance_id } output "arn" { @@ -156,12 +151,16 @@ output "ami" { output "availability_zone" { description = "The availability zone of the created instance" - value = try( - aws_instance.this[0].availability_zone, - aws_instance.ignore_ami[0].availability_zone, - aws_spot_instance_request.this[0].availability_zone, - null, - ) + value = local.instance_availability_zone +} + +################################################################################ +# EBS Volume(s) +################################################################################ + +output "ebs_volumes" { + description = "Map of EBS volumes created and their attributes" + value = aws_ebs_volume.this } ################################################################################ @@ -201,6 +200,7 @@ output "iam_instance_profile_unique" { ################################################################################ # Block Devices ################################################################################ + output "root_block_device" { description = "Root block device information" value = try( diff --git a/variables.tf b/variables.tf index e59a9dc8..7a168a63 100644 --- a/variables.tf +++ b/variables.tf @@ -10,18 +10,28 @@ variable "name" { default = "" } -variable "ami_ssm_parameter" { - description = "SSM parameter name for the AMI ID. For Amazon Linux AMI SSM parameters see [reference](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-public-parameters-ami.html)" +variable "region" { + description = "Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration" type = string - default = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" + default = null } +################################################################################ +# Instance +################################################################################ + variable "ami" { description = "ID of AMI to use for the instance" type = string default = null } +variable "ami_ssm_parameter" { + description = "SSM parameter name for the AMI ID. For Amazon Linux AMI SSM parameters see [reference](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-public-parameters-ami.html)" + type = string + default = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" +} + variable "ignore_ami_changes" { description = "Whether changes to the AMI ID changes should be ignored by Terraform. Note - changing this value will result in the replacement of the instance" type = bool @@ -34,12 +44,6 @@ variable "associate_public_ip_address" { default = null } -variable "maintenance_options" { - description = "The maintenance options for the instance" - type = any - default = {} -} - variable "availability_zone" { description = "AZ to start the instance in" type = string @@ -48,8 +52,24 @@ variable "availability_zone" { variable "capacity_reservation_specification" { description = "Describes an instance's Capacity Reservation targeting option" - type = any - default = {} + type = object({ + capacity_reservation_preference = optional(string) + capacity_reservation_target = optional(object({ + capacity_reservation_id = optional(string) + capacity_reservation_resource_group_arn = optional(string) + })) + }) + default = null +} + +variable "cpu_options" { + description = "Defines CPU options to apply to the instance at launch time." + type = object({ + amd_sev_snp = optional(string) + core_count = optional(number) + threads_per_core = optional(number) + }) + default = null } variable "cpu_credits" { @@ -64,10 +84,10 @@ variable "disable_api_termination" { default = null } -variable "ebs_block_device" { - description = "Additional EBS block devices to attach to the instance" - type = list(any) - default = [] +variable "disable_api_stop" { + description = "If true, enables EC2 Instance Stop Protection" + type = bool + default = null } variable "ebs_optimized" { @@ -82,10 +102,20 @@ variable "enclave_options_enabled" { default = null } +variable "enable_primary_ipv6" { + description = "Whether to assign a primary IPv6 Global Unicast Address (GUA) to the instance when launched in a dual-stack or IPv6-only subnet" + type = bool + default = null +} + variable "ephemeral_block_device" { description = "Customize Ephemeral (also known as Instance Store) volumes on the instance" - type = list(map(string)) - default = [] + type = map(object({ + device_name = string + no_device = optional(bool) + virtual_name = optional(string) + })) + default = null } variable "get_password_data" { @@ -106,6 +136,12 @@ variable "host_id" { default = null } +variable "host_resource_group_arn" { + description = "ARN of the host resource group in which to launch the instances. If you specify an ARN, omit the `tenancy` parameter or set it to `host`" + type = string + default = null +} + variable "iam_instance_profile" { description = "IAM Instance Profile to launch the instance with. Specified as the name of the Instance Profile" type = string @@ -118,18 +154,26 @@ variable "instance_initiated_shutdown_behavior" { default = null } +variable "instance_market_options" { + description = "The market (purchasing) option for the instance. If set, overrides the `create_spot_instance` variable" + type = object({ + market_type = optional(string) + spot_options = optional(object({ + instance_interruption_behavior = optional(string) + max_price = optional(string) + spot_instance_type = optional(string) + valid_until = optional(string) + })) + }) + default = null +} + variable "instance_type" { description = "The type of instance to start" type = string default = "t3.micro" } -variable "instance_tags" { - description = "Additional tags for the instance" - type = map(string) - default = {} -} - variable "ipv6_address_count" { description = "A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet" type = number @@ -150,17 +194,35 @@ variable "key_name" { variable "launch_template" { description = "Specifies a Launch Template to configure the instance. Parameters configured on this resource will override the corresponding parameters in the Launch Template" - type = map(string) - default = {} + type = object({ + id = optional(string) + name = optional(string) + version = optional(string) + }) + default = null +} + +variable "maintenance_options" { + description = "The maintenance options for the instance" + type = object({ + auto_recovery = optional(string) + }) + default = null } variable "metadata_options" { description = "Customize the metadata options of the instance" - type = map(string) + type = object({ + http_endpoint = optional(string, "enabled") + http_protocol_ipv6 = optional(string) + http_put_response_hop_limit = optional(number, 1) + http_tokens = optional(string, "required") + instance_metadata_tags = optional(string) + }) default = { - "http_endpoint" = "enabled" - "http_put_response_hop_limit" = 1 - "http_tokens" = "required" + http_endpoint = "enabled" + http_put_response_hop_limit = 1 + http_tokens = "required" } } @@ -172,14 +234,13 @@ variable "monitoring" { variable "network_interface" { description = "Customize network interfaces to be attached at instance boot time" - type = list(map(string)) - default = [] -} - -variable "private_dns_name_options" { - description = "Customize the private DNS name options of the instance" - type = map(string) - default = {} + type = map(object({ + delete_on_termination = optional(bool) + device_index = optional(number) # Will fall back to use map key as device index + network_card_index = optional(number) + network_interface_id = string + })) + default = null } variable "placement_group" { @@ -188,6 +249,22 @@ variable "placement_group" { default = null } +variable "placement_partition_number" { + description = "Number of the partition the instance is in. Valid only if the `aws_placement_group` resource's `strategy` argument is set to `partition`" + type = number + default = null +} + +variable "private_dns_name_options" { + description = "Customize the private DNS name options of the instance" + type = object({ + enable_resource_name_dns_a_record = optional(bool) + enable_resource_name_dns_aaaa_record = optional(bool) + hostname_type = optional(string) + }) + default = null +} + variable "private_ip" { description = "Private IP address to associate with the instance in a VPC" type = string @@ -196,8 +273,17 @@ variable "private_ip" { variable "root_block_device" { description = "Customize details about the root block device of the instance. See Block Devices below for details" - type = list(any) - default = [] + type = map(object({ + delete_on_termination = optional(bool) + encrypted = optional(bool) + iops = optional(number) + kms_key_id = optional(string) + tags = optional(map(string), {}) + throughput = optional(number) + size = optional(number) + type = optional(string) + })) + default = null } variable "secondary_private_ips" { @@ -224,6 +310,12 @@ variable "tags" { default = {} } +variable "instance_tags" { + description = "Additional tags for the instance" + type = map(string) + default = {} +} + variable "tenancy" { description = "The tenancy of the instance (if the instance is running in a VPC). Available values: default, dedicated, host" type = string @@ -263,7 +355,7 @@ variable "enable_volume_tags" { variable "vpc_security_group_ids" { description = "A list of security group IDs to associate with" type = list(string) - default = null + default = [] } variable "timeouts" { @@ -272,40 +364,25 @@ variable "timeouts" { default = {} } -variable "cpu_options" { - description = "Defines CPU options to apply to the instance at launch time." - type = any - default = {} -} - -variable "cpu_core_count" { - description = "Sets the number of CPU cores for an instance" # This option is only supported on creation of instance type that support CPU Options https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html#cpu-options-supported-instances-values - type = number - default = null -} - -variable "cpu_threads_per_core" { - description = "Sets the number of CPU threads per core for an instance (has no effect unless cpu_core_count is also set)" - type = number - default = null -} +################################################################################ +# Spot Instance Request +################################################################################ -# Spot instance request variable "create_spot_instance" { description = "Depicts if the instance is a spot instance" type = bool default = false } -variable "spot_price" { - description = "The maximum price to request on the spot market. Defaults to on-demand price" +variable "spot_launch_group" { + description = "A launch group is a group of spot instances that launch together and terminate together. If left empty instances are launched and terminated individually" type = string default = null } -variable "spot_wait_for_fulfillment" { - description = "If set, Terraform will wait for the Spot Request to be fulfilled, and will throw an error if the timeout of 10m is reached" - type = bool +variable "spot_price" { + description = "The maximum price to request on the spot market. Defaults to on-demand price" + type = string default = null } @@ -315,20 +392,14 @@ variable "spot_type" { default = null } -variable "spot_launch_group" { - description = "A launch group is a group of spot instances that launch together and terminate together. If left empty instances are launched and terminated individually" - type = string - default = null -} - -variable "spot_block_duration_minutes" { - description = "The required duration for the Spot instances, in minutes. This value must be a multiple of 60 (60, 120, 180, 240, 300, or 360)" - type = number +variable "spot_wait_for_fulfillment" { + description = "If set, Terraform will wait for the Spot Request to be fulfilled, and will throw an error if the timeout of 10m is reached" + type = bool default = null } -variable "spot_instance_interruption_behavior" { - description = "Indicates Spot instance behavior when it is interrupted. Valid values are `terminate`, `stop`, or `hibernate`" +variable "spot_valid_from" { + description = "The start date and time of the request, in UTC RFC3339 format(for example, YYYY-MM-DDTHH:MM:SSZ)" type = string default = null } @@ -339,22 +410,31 @@ variable "spot_valid_until" { default = null } -variable "spot_valid_from" { - description = "The start date and time of the request, in UTC RFC3339 format(for example, YYYY-MM-DDTHH:MM:SSZ)" - type = string - default = null -} - -variable "disable_api_stop" { - description = "If true, enables EC2 Instance Stop Protection" - type = bool - default = null +################################################################################ +# EBS Volume(s) +################################################################################ -} -variable "putin_khuylo" { - description = "Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo!" - type = bool - default = true +variable "ebs_volumes" { + description = "Additional EBS volumes to attach to the instance" + type = map(object({ + encrypted = optional(bool) + final_snapshot = optional(bool) + iops = optional(number) + kms_key_id = optional(string) + multi_attach_enabled = optional(bool) + outpost_arn = optional(string) + size = optional(number) + snapshot_id = optional(string) + tags = optional(map(string), {}) + throughput = optional(number) + type = optional(string, "gp3") + # Attachment + device_name = optional(string) # Will fall back to use map key as device name + force_detach = optional(bool) + skip_destroy = optional(bool) + stop_instance_before_detaching = optional(bool) + })) + default = null } ################################################################################ @@ -409,6 +489,89 @@ variable "iam_role_tags" { default = {} } +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines whether a security group will be created" + type = bool + default = true +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name` or `name`) is used as a prefix" + type = bool + default = true +} + +variable "security_group_description" { + description = "Description of the security group" + type = string + default = null +} + +variable "security_group_vpc_id" { + description = "VPC ID to create the security group in. If not set, the security group will be created in the default VPC" + type = string + default = null +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} +} + +variable "security_group_egress_rules" { + description = "Egress rules to add to the security group" + type = map(object({ + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(number) + ip_protocol = optional(string) + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(number) + })) + default = { + ipv4_default = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + ipv6_default = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + } +} + +variable "security_group_ingress_rules" { + description = "Egress rules to add to the security group" + type = map(object({ + cidr_ipv4 = optional(string) + cidr_ipv6 = optional(string) + description = optional(string) + from_port = optional(number) + ip_protocol = optional(string) + prefix_list_id = optional(string) + referenced_security_group_id = optional(string) + tags = optional(map(string), {}) + to_port = optional(number) + })) + default = null +} + ################################################################################ # Elastic IP ################################################################################ @@ -430,3 +593,9 @@ variable "eip_tags" { type = map(string) default = {} } + +variable "putin_khuylo" { + description = "Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo!" + type = bool + default = true +} diff --git a/versions.tf b/versions.tf index fd4d1167..f648e20c 100644 --- a/versions.tf +++ b/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.66" + version = ">= 6.0" } } } diff --git a/wrappers/main.tf b/wrappers/main.tf index 048da569..6d6a90be 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -4,30 +4,31 @@ module "wrapper" { for_each = var.items ami = try(each.value.ami, var.defaults.ami, null) - ami_ssm_parameter = try(each.value.ami_ssm_parameter, var.defaults.ami_ssm_parameter, "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2") + ami_ssm_parameter = try(each.value.ami_ssm_parameter, var.defaults.ami_ssm_parameter, "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64") associate_public_ip_address = try(each.value.associate_public_ip_address, var.defaults.associate_public_ip_address, null) availability_zone = try(each.value.availability_zone, var.defaults.availability_zone, null) - capacity_reservation_specification = try(each.value.capacity_reservation_specification, var.defaults.capacity_reservation_specification, {}) - cpu_core_count = try(each.value.cpu_core_count, var.defaults.cpu_core_count, null) + capacity_reservation_specification = try(each.value.capacity_reservation_specification, var.defaults.capacity_reservation_specification, null) cpu_credits = try(each.value.cpu_credits, var.defaults.cpu_credits, null) - cpu_options = try(each.value.cpu_options, var.defaults.cpu_options, {}) - cpu_threads_per_core = try(each.value.cpu_threads_per_core, var.defaults.cpu_threads_per_core, null) + cpu_options = try(each.value.cpu_options, var.defaults.cpu_options, null) create = try(each.value.create, var.defaults.create, true) create_eip = try(each.value.create_eip, var.defaults.create_eip, false) create_iam_instance_profile = try(each.value.create_iam_instance_profile, var.defaults.create_iam_instance_profile, false) + create_security_group = try(each.value.create_security_group, var.defaults.create_security_group, true) create_spot_instance = try(each.value.create_spot_instance, var.defaults.create_spot_instance, false) disable_api_stop = try(each.value.disable_api_stop, var.defaults.disable_api_stop, null) disable_api_termination = try(each.value.disable_api_termination, var.defaults.disable_api_termination, null) - ebs_block_device = try(each.value.ebs_block_device, var.defaults.ebs_block_device, []) ebs_optimized = try(each.value.ebs_optimized, var.defaults.ebs_optimized, null) + ebs_volumes = try(each.value.ebs_volumes, var.defaults.ebs_volumes, null) eip_domain = try(each.value.eip_domain, var.defaults.eip_domain, "vpc") eip_tags = try(each.value.eip_tags, var.defaults.eip_tags, {}) + enable_primary_ipv6 = try(each.value.enable_primary_ipv6, var.defaults.enable_primary_ipv6, null) enable_volume_tags = try(each.value.enable_volume_tags, var.defaults.enable_volume_tags, true) enclave_options_enabled = try(each.value.enclave_options_enabled, var.defaults.enclave_options_enabled, null) - ephemeral_block_device = try(each.value.ephemeral_block_device, var.defaults.ephemeral_block_device, []) + ephemeral_block_device = try(each.value.ephemeral_block_device, var.defaults.ephemeral_block_device, null) get_password_data = try(each.value.get_password_data, var.defaults.get_password_data, null) hibernation = try(each.value.hibernation, var.defaults.hibernation, null) host_id = try(each.value.host_id, var.defaults.host_id, null) + host_resource_group_arn = try(each.value.host_resource_group_arn, var.defaults.host_resource_group_arn, null) iam_instance_profile = try(each.value.iam_instance_profile, var.defaults.iam_instance_profile, null) iam_role_description = try(each.value.iam_role_description, var.defaults.iam_role_description, null) iam_role_name = try(each.value.iam_role_name, var.defaults.iam_role_name, null) @@ -38,43 +39,62 @@ module "wrapper" { iam_role_use_name_prefix = try(each.value.iam_role_use_name_prefix, var.defaults.iam_role_use_name_prefix, true) ignore_ami_changes = try(each.value.ignore_ami_changes, var.defaults.ignore_ami_changes, false) instance_initiated_shutdown_behavior = try(each.value.instance_initiated_shutdown_behavior, var.defaults.instance_initiated_shutdown_behavior, null) + instance_market_options = try(each.value.instance_market_options, var.defaults.instance_market_options, null) instance_tags = try(each.value.instance_tags, var.defaults.instance_tags, {}) instance_type = try(each.value.instance_type, var.defaults.instance_type, "t3.micro") ipv6_address_count = try(each.value.ipv6_address_count, var.defaults.ipv6_address_count, null) ipv6_addresses = try(each.value.ipv6_addresses, var.defaults.ipv6_addresses, null) key_name = try(each.value.key_name, var.defaults.key_name, null) - launch_template = try(each.value.launch_template, var.defaults.launch_template, {}) - maintenance_options = try(each.value.maintenance_options, var.defaults.maintenance_options, {}) + launch_template = try(each.value.launch_template, var.defaults.launch_template, null) + maintenance_options = try(each.value.maintenance_options, var.defaults.maintenance_options, null) metadata_options = try(each.value.metadata_options, var.defaults.metadata_options, { - "http_endpoint" = "enabled" - "http_put_response_hop_limit" = 1 - "http_tokens" = "required" + http_endpoint = "enabled" + http_put_response_hop_limit = 1 + http_tokens = "required" }) - monitoring = try(each.value.monitoring, var.defaults.monitoring, null) - name = try(each.value.name, var.defaults.name, "") - network_interface = try(each.value.network_interface, var.defaults.network_interface, []) - placement_group = try(each.value.placement_group, var.defaults.placement_group, null) - private_dns_name_options = try(each.value.private_dns_name_options, var.defaults.private_dns_name_options, {}) - private_ip = try(each.value.private_ip, var.defaults.private_ip, null) - putin_khuylo = try(each.value.putin_khuylo, var.defaults.putin_khuylo, true) - root_block_device = try(each.value.root_block_device, var.defaults.root_block_device, []) - secondary_private_ips = try(each.value.secondary_private_ips, var.defaults.secondary_private_ips, null) - source_dest_check = try(each.value.source_dest_check, var.defaults.source_dest_check, null) - spot_block_duration_minutes = try(each.value.spot_block_duration_minutes, var.defaults.spot_block_duration_minutes, null) - spot_instance_interruption_behavior = try(each.value.spot_instance_interruption_behavior, var.defaults.spot_instance_interruption_behavior, null) - spot_launch_group = try(each.value.spot_launch_group, var.defaults.spot_launch_group, null) - spot_price = try(each.value.spot_price, var.defaults.spot_price, null) - spot_type = try(each.value.spot_type, var.defaults.spot_type, null) - spot_valid_from = try(each.value.spot_valid_from, var.defaults.spot_valid_from, null) - spot_valid_until = try(each.value.spot_valid_until, var.defaults.spot_valid_until, null) - spot_wait_for_fulfillment = try(each.value.spot_wait_for_fulfillment, var.defaults.spot_wait_for_fulfillment, null) - subnet_id = try(each.value.subnet_id, var.defaults.subnet_id, null) - tags = try(each.value.tags, var.defaults.tags, {}) - tenancy = try(each.value.tenancy, var.defaults.tenancy, null) - timeouts = try(each.value.timeouts, var.defaults.timeouts, {}) - user_data = try(each.value.user_data, var.defaults.user_data, null) - user_data_base64 = try(each.value.user_data_base64, var.defaults.user_data_base64, null) - user_data_replace_on_change = try(each.value.user_data_replace_on_change, var.defaults.user_data_replace_on_change, null) - volume_tags = try(each.value.volume_tags, var.defaults.volume_tags, {}) - vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, null) + monitoring = try(each.value.monitoring, var.defaults.monitoring, null) + name = try(each.value.name, var.defaults.name, "") + network_interface = try(each.value.network_interface, var.defaults.network_interface, null) + placement_group = try(each.value.placement_group, var.defaults.placement_group, null) + placement_partition_number = try(each.value.placement_partition_number, var.defaults.placement_partition_number, null) + private_dns_name_options = try(each.value.private_dns_name_options, var.defaults.private_dns_name_options, null) + private_ip = try(each.value.private_ip, var.defaults.private_ip, null) + putin_khuylo = try(each.value.putin_khuylo, var.defaults.putin_khuylo, true) + region = try(each.value.region, var.defaults.region, null) + root_block_device = try(each.value.root_block_device, var.defaults.root_block_device, null) + secondary_private_ips = try(each.value.secondary_private_ips, var.defaults.secondary_private_ips, null) + security_group_description = try(each.value.security_group_description, var.defaults.security_group_description, null) + security_group_egress_rules = try(each.value.security_group_egress_rules, var.defaults.security_group_egress_rules, { + ipv4_default = { + cidr_ipv4 = "0.0.0.0/0" + description = "Allow all IPv4 traffic" + ip_protocol = "-1" + } + ipv6_default = { + cidr_ipv6 = "::/0" + description = "Allow all IPv6 traffic" + ip_protocol = "-1" + } + }) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, null) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) + security_group_vpc_id = try(each.value.security_group_vpc_id, var.defaults.security_group_vpc_id, null) + source_dest_check = try(each.value.source_dest_check, var.defaults.source_dest_check, null) + spot_launch_group = try(each.value.spot_launch_group, var.defaults.spot_launch_group, null) + spot_price = try(each.value.spot_price, var.defaults.spot_price, null) + spot_type = try(each.value.spot_type, var.defaults.spot_type, null) + spot_valid_from = try(each.value.spot_valid_from, var.defaults.spot_valid_from, null) + spot_valid_until = try(each.value.spot_valid_until, var.defaults.spot_valid_until, null) + spot_wait_for_fulfillment = try(each.value.spot_wait_for_fulfillment, var.defaults.spot_wait_for_fulfillment, null) + subnet_id = try(each.value.subnet_id, var.defaults.subnet_id, null) + tags = try(each.value.tags, var.defaults.tags, {}) + tenancy = try(each.value.tenancy, var.defaults.tenancy, null) + timeouts = try(each.value.timeouts, var.defaults.timeouts, {}) + user_data = try(each.value.user_data, var.defaults.user_data, null) + user_data_base64 = try(each.value.user_data_base64, var.defaults.user_data_base64, null) + user_data_replace_on_change = try(each.value.user_data_replace_on_change, var.defaults.user_data_replace_on_change, null) + volume_tags = try(each.value.volume_tags, var.defaults.volume_tags, {}) + vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, []) } diff --git a/wrappers/versions.tf b/wrappers/versions.tf index fd4d1167..f648e20c 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" - version = ">= 4.66" + version = ">= 6.0" } } } From ab899fc64453d6cc712bad0a0d1f22e64bdfe17a Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 24 Jun 2025 13:45:42 -0500 Subject: [PATCH 2/4] feat: Updates from testing, add upgrade guide --- README.md | 18 +-- UPGRADE-6.0.md | 183 +++++++++++++++++++++++++++++++ examples/complete/README.md | 1 - examples/complete/main.tf | 117 ++++---------------- examples/session-manager/main.tf | 2 +- main.tf | 105 +++++++++--------- variables.tf | 14 ++- wrappers/main.tf | 43 ++++---- 8 files changed, 301 insertions(+), 182 deletions(-) create mode 100644 UPGRADE-6.0.md diff --git a/README.md b/README.md index 4f1ee768..c7d6bd04 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,6 @@ module "ec2_instance" { } ``` -## Module wrappers - -Users of this Terraform module can create multiple similar resources by using [`for_each` meta-argument within `module` block](https://www.terraform.io/language/meta-arguments/for_each) which became available in Terraform 0.13. - -Users of Terragrunt can achieve similar results by using modules provided in the [wrappers](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/wrappers) directory, if they prefer to reduce amount of configuration files. - ## Examples - [Complete EC2 instance](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/complete) @@ -87,7 +81,7 @@ Users of Terragrunt can achieve similar results by using modules provided in the This module does not support encrypted AMI's out of the box however it is easy enough for you to generate one for use -This example creates an encrypted image from the latest ubuntu 16.04 base image. +This example creates an encrypted image from the latest ubuntu 20.04 base image. ```hcl provider "aws" { @@ -135,8 +129,6 @@ data "aws_ami" "encrypted-ami" { The following combinations are supported to conditionally create resources: -- Disable resource creation (no resources created): - ```hcl module "ec2_instance" { source = "terraform-aws-modules/ec2-instance/aws" @@ -188,6 +180,7 @@ No modules. | Name | Type | |------|------| | [aws_ebs_volume.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ebs_volume) | resource | +| [aws_ec2_tag.spot_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ec2_tag) | resource | | [aws_eip.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | | [aws_iam_instance_profile.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | | [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | @@ -262,16 +255,17 @@ No modules. | [private\_ip](#input\_private\_ip) | Private IP address to associate with the instance in a VPC | `string` | `null` | no | | [putin\_khuylo](#input\_putin\_khuylo) | Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo! | `bool` | `true` | no | | [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no | -| [root\_block\_device](#input\_root\_block\_device) | Customize details about the root block device of the instance. See Block Devices below for details |
map(object({
delete_on_termination = optional(bool)
encrypted = optional(bool)
iops = optional(number)
kms_key_id = optional(string)
tags = optional(map(string), {})
throughput = optional(number)
size = optional(number)
type = optional(string)
}))
| `null` | no | +| [root\_block\_device](#input\_root\_block\_device) | Customize details about the root block device of the instance. See Block Devices below for details |
object({
delete_on_termination = optional(bool)
encrypted = optional(bool)
iops = optional(number)
kms_key_id = optional(string)
tags = optional(map(string), {})
throughput = optional(number)
size = optional(number)
type = optional(string)
})
| `null` | no | | [secondary\_private\_ips](#input\_secondary\_private\_ips) | A list of secondary private IPv4 addresses to assign to the instance's primary network interface (eth0) in a VPC. Can only be assigned to the primary network interface (eth0) attached at instance creation, not a pre-existing network interface i.e. referenced in a `network_interface block` | `list(string)` | `null` | no | | [security\_group\_description](#input\_security\_group\_description) | Description of the security group | `string` | `null` | no | -| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
|
{
"ipv4_default": {
"cidr_ipv4": "0.0.0.0/0",
"description": "Allow all IPv4 traffic",
"ip_protocol": "-1"
},
"ipv6_default": {
"cidr_ipv6": "::/0",
"description": "Allow all IPv6 traffic",
"ip_protocol": "-1"
}
}
| no | -| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string)
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `null` | no | +| [security\_group\_egress\_rules](#input\_security\_group\_egress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
|
{
"ipv4_default": {
"cidr_ipv4": "0.0.0.0/0",
"description": "Allow all IPv4 traffic",
"ip_protocol": "-1"
},
"ipv6_default": {
"cidr_ipv6": "::/0",
"description": "Allow all IPv6 traffic",
"ip_protocol": "-1"
}
}
| no | +| [security\_group\_ingress\_rules](#input\_security\_group\_ingress\_rules) | Egress rules to add to the security group |
map(object({
cidr_ipv4 = optional(string)
cidr_ipv6 = optional(string)
description = optional(string)
from_port = optional(number)
ip_protocol = optional(string, "tcp")
prefix_list_id = optional(string)
referenced_security_group_id = optional(string)
tags = optional(map(string), {})
to_port = optional(number)
}))
| `null` | no | | [security\_group\_name](#input\_security\_group\_name) | Name to use on security group created | `string` | `null` | no | | [security\_group\_tags](#input\_security\_group\_tags) | A map of additional tags to add to the security group created | `map(string)` | `{}` | no | | [security\_group\_use\_name\_prefix](#input\_security\_group\_use\_name\_prefix) | Determines whether the security group name (`security_group_name` or `name`) is used as a prefix | `bool` | `true` | no | | [security\_group\_vpc\_id](#input\_security\_group\_vpc\_id) | VPC ID to create the security group in. If not set, the security group will be created in the default VPC | `string` | `null` | no | | [source\_dest\_check](#input\_source\_dest\_check) | Controls if traffic is routed to the instance when the destination address does not match the instance. Used for NAT or VPNs | `bool` | `null` | no | +| [spot\_instance\_interruption\_behavior](#input\_spot\_instance\_interruption\_behavior) | Indicates Spot instance behavior when it is interrupted. Valid values are `terminate`, `stop`, or `hibernate` | `string` | `null` | no | | [spot\_launch\_group](#input\_spot\_launch\_group) | A launch group is a group of spot instances that launch together and terminate together. If left empty instances are launched and terminated individually | `string` | `null` | no | | [spot\_price](#input\_spot\_price) | The maximum price to request on the spot market. Defaults to on-demand price | `string` | `null` | no | | [spot\_type](#input\_spot\_type) | If set to one-time, after the instance is terminated, the spot request will be closed. Default `persistent` | `string` | `null` | no | diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md new file mode 100644 index 00000000..72d96976 --- /dev/null +++ b/UPGRADE-6.0.md @@ -0,0 +1,183 @@ +# Upgrade from v5.x to v6.x + +If you have any questions regarding this upgrade process, please consult the `examples` directory: + +- [Complete](https://github.com/terraform-aws-modules/terraform-aws-ec2-instance/tree/master/examples/complete) + +If you find a bug, please open an issue with supporting configuration to reproduce. + +## List of backwards incompatible changes + +- Terraform v1.10.0 is now minimum supported version +- AWS provider v6.0.0 is now minimum supported version +- The default value for `ami_ssm_parameter` was changed from `"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"` to `"/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"`. AL2 is approaching end of life. + +## Additional changes + +### Added + +- Support for creating a security group within the module; this is now the default behavior and can be disabled by setting `create_security_group = false`. +- Support for `region` parameter to specify the AWS region for the resources created if different from the provider region. +- Support for tagging spot instances + +### Modified + +- Variable definitions now contain detailed `object` types in place of the previously used `any` type. +- Inline `ebs_block_device` argument has been removed in favor of `ebs_volumes` which is a map of EBS volumes created through `aws_ebs_volume` and `aws_ebs_volume_attachment` resources. This provides the same API as before, but allows for more flexibility without generating diffs when adding or removing EBS volumes as well as unintended changes to the volumes. +- Correct tag precedence ordering (least specific to most specific) + +### Removed + +- The `volume-attachment` example has been removed since the module has been updated to use the corrected form of EBS volume creation and attachment (tl;dr - example is no longer useful). + +### Variable and output changes + +1. Removed variables: + + - `cpu_core_count` - removed from provider `v6.x` + - `cpu_threads_per_core` - removed from provider `v6.x` + +2. Renamed variables: + + - `ebs_block_device` -> `ebs_volumes` + +3. Added variables: + + - `region` + - `enable_primary_ipv6` + - `host_resource_group_arn` + - `instance_market_options` + - `placement_partition_number` + - `create_security_group` + - `security_group_name` + - `security_group_use_name_prefix` + - `security_group_description` + - `security_group_vpc_id` + - `security_group_tags` + - `security_group_egress_rules` + - `security_group_ingress_rules` + +4. Removed outputs: + + - None + +5. Renamed outputs: + + - None + +6. Added outputs: + + - `ebs_volumes` + +## Upgrade State Migrations + +### Before 5.x Example + +```hcl +module "ec2_upgrade" { + source = "terraform-aws-modules/ec2-instance/aws" + version = "5.8.0" + + # Truncated for brevity, only relevant module API changes are shown ... + + root_block_device = [ + { + encrypted = true + volume_size = 50 + volume_type = "gp3" + throughput = 200 + tags = { + Name = "my-root-block" + } + }, + ] + + ebs_block_device = [ + { + device_name = "/dev/sdf" + encrypted = true + volume_size = 5 + volume_type = "gp3" + throughput = 200 + tags = { + MountPoint = "/mnt/data" + } + } + ] + + network_interface = [ + { + device_index = 0 + network_interface_id = aws_network_interface.this.id + delete_on_termination = false + } + ] + + tags = local.tags +} +``` + +### After 6.x Example + +```hcl +module "ec2_upgrade" { + source = "terraform-aws-modules/ec2-instance/aws" + version = "6.0.0" + + # Truncated for brevity, only relevant module API changes are shown ... + + # There can only be one root block device, so the wrapping list is removed + root_block_device = { + encrypted = true + size = 50 # Was `volume_size` + type = "gp3" # Was `volume_type` + throughput = 200 + tags = { + Name = "my-root-block" + } + } + + # Now a map of EBS volumes is used instead of a list + ebs_volumes = { + # The device_name can be the key of the map, or set by `device_name` attribute + "/dev/sdf" = { + encrypted = true + size = 5 # Was `volume_size` + type = "gp3" # Was `volume_type`, `gp3` is now the default + throughput = 200 + tags = { + MountPoint = "/mnt/data" + } + } + } + + # Now a map of network interfaces is used instead of a list + network_interface = { + # The device_index can be the key of the map, or set by `device_index` attribute + 0 = { + network_interface_id = aws_network_interface.this.id + delete_on_termination = false + } + } + + tags = local.tags +} +``` + +To migrate from the `v5.x` version to `v6.x` version example shown above, the following state move commands can be performed to maintain the current resources without modification: + +> [!NOTE] +> State move commands should only be required on instances that have additional EBS volumes attached to them. + +```bash +terraform state rm 'module.ec2_complete.aws_instance.this[0]' +terraform import 'module.ec2_complete.aws_instance.this[0]' + +# Do the following for each additional EBS volume attached to the instance +terraform import 'module.ec2_complete.aws_ebs_volume.this["/dev/sdf"]' +terraform import 'module.ec2_complete.aws_volume_attachment.this["/dev/sdf"]' :: +``` + +> [!TIP] +> If you encounter a situation where Terraform wants to recreate the instance due to user data changes, you can set the `user_data_replace_on_change` variable to `false` to prevent this behavior. +> This is related to https://github.com/hashicorp/terraform-provider-aws/issues/5011 diff --git a/examples/complete/README.md b/examples/complete/README.md index 26ccb1db..f0b0d11d 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -33,7 +33,6 @@ Note that this example may create resources which can cost money. Run `terraform | Name | Source | Version | |------|--------|---------| | [ec2\_complete](#module\_ec2\_complete) | ../../ | n/a | -| [ec2\_cpu\_options](#module\_ec2\_cpu\_options) | ../../ | n/a | | [ec2\_disabled](#module\_ec2\_disabled) | ../../ | n/a | | [ec2\_ignore\_ami\_changes](#module\_ec2\_ignore\_ami\_changes) | ../../ | n/a | | [ec2\_metadata\_options](#module\_ec2\_metadata\_options) | ../../ | n/a | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index bc48ed11..d43bf633 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -52,7 +52,7 @@ module "ec2_complete" { # enclave_options_enabled = true user_data_base64 = base64encode(local.user_data) - user_data_replace_on_change = true + user_data_replace_on_change = false cpu_options = { core_count = 2 @@ -60,15 +60,13 @@ module "ec2_complete" { } enable_volume_tags = false root_block_device = { - main = { - encrypted = true - type = "gp3" - throughput = 200 - size = 50 - tags = { - Name = "my-root-block" - } - }, + encrypted = true + type = "gp3" + throughput = 200 + size = 50 + tags = { + Name = "my-root-block" + } } ebs_volumes = { @@ -161,7 +159,7 @@ module "ec2_disabled" { module "ec2_ignore_ami_changes" { source = "../../" - name = local.name + name = "${local.name}-ignore-ami-changes" ignore_ami_changes = true @@ -184,14 +182,12 @@ locals { availability_zone = element(module.vpc.azs, 0) subnet_id = element(module.vpc.private_subnets, 0) root_block_device = { - main = { - encrypted = true - type = "gp3" - throughput = 200 - size = 50 - tags = { - Name = "my-root-block" - } + encrypted = true + type = "gp3" + throughput = 200 + size = 50 + tags = { + Name = "my-root-block" } } } @@ -200,11 +196,9 @@ locals { availability_zone = element(module.vpc.azs, 1) subnet_id = element(module.vpc.private_subnets, 1) root_block_device = { - main = { - encrypted = true - type = "gp2" - size = 50 - } + encrypted = true + type = "gp2" + size = 50 } } three = { @@ -262,14 +256,12 @@ module "ec2_spot_instance" { enable_volume_tags = false root_block_device = { - main = { - encrypted = true - type = "gp3" - throughput = 200 - size = 50 - tags = { - Name = "my-root-block" - } + encrypted = true + type = "gp3" + throughput = 200 + size = 50 + tags = { + Name = "my-root-block" } } @@ -341,67 +333,6 @@ resource "aws_ec2_capacity_reservation" "targeted" { instance_match_criteria = "targeted" } -################################################################################ -# EC2 Module - CPU Options -################################################################################ - -module "ec2_cpu_options" { - source = "../../" - - create = false - name = "${local.name}-cpu-options" - - instance_type = "c6a.xlarge" # used to set core count below and test amd_sev_snp attribute - availability_zone = element(module.vpc.azs, 0) - subnet_id = element(module.vpc.private_subnets, 0) - placement_group = aws_placement_group.web.id - associate_public_ip_address = true - disable_api_stop = false - - create_iam_instance_profile = true - iam_role_description = "IAM role for EC2 instance" - iam_role_policies = { - AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess" - } - - user_data_base64 = base64encode(local.user_data) - user_data_replace_on_change = true - - cpu_options = { - core_count = 2 - threads_per_core = 1 - amd_sev_snp = "enabled" - } - enable_volume_tags = false - root_block_device = { - main = { - encrypted = true - type = "gp3" - throughput = 200 - size = 50 - tags = { - Name = "my-root-block" - } - } - } - - ebs_volumes = { - "/dev/sdf" = { - size = 5 - throughput = 200 - encrypted = true - kms_key_id = aws_kms_key.this.arn - tags = { - MountPoint = "/mnt/data" - } - } - } - - instance_tags = { Persistence = "09:00-18:00" } - - tags = local.tags -} - ################################################################################ # Supporting Resources ################################################################################ diff --git a/examples/session-manager/main.tf b/examples/session-manager/main.tf index 37d1e964..e58cb4e2 100644 --- a/examples/session-manager/main.tf +++ b/examples/session-manager/main.tf @@ -31,7 +31,7 @@ module "ec2" { security_group_egress_rules = { vpc-endpoints = { description = "Allow outbound traffic to VPC endpoints" - cidr_ipv4 = module.vpc.intra_subnets_cidr_blocks + cidr_ipv4 = module.vpc.vpc_cidr_block from_port = 443 } } diff --git a/main.tf b/main.tf index 73b448c7..d2524c9a 100644 --- a/main.tf +++ b/main.tf @@ -7,10 +7,16 @@ locals { ami = try(coalesce(var.ami, try(nonsensitive(data.aws_ssm_parameter.this[0].value), null)), null) + instance_tags = merge( + var.tags, + var.instance_tags, + { "Name" = var.name }, + ) + instance_id = try( aws_instance.this[0].id, aws_instance.ignore_ami[0].id, - aws_spot_instance_request.this[0].id, + aws_spot_instance_request.this[0].spot_instance_id, null, ) @@ -188,7 +194,7 @@ resource "aws_instance" "this" { private_ip = var.private_ip dynamic "root_block_device" { - for_each = var.root_block_device != null ? var.root_block_device : {} + for_each = var.root_block_device != null ? [var.root_block_device] : [] content { delete_on_termination = root_block_device.value.delete_on_termination @@ -202,21 +208,15 @@ resource "aws_instance" "this" { } } - secondary_private_ips = var.secondary_private_ips - source_dest_check = var.network_interface != null ? null : var.source_dest_check - subnet_id = var.subnet_id - - tags = merge( - var.tags, - var.instance_tags, - { "Name" = var.name }, - ) - + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + tags = local.instance_tags tenancy = var.tenancy user_data = var.user_data user_data_base64 = var.user_data_base64 user_data_replace_on_change = var.user_data_replace_on_change - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + volume_tags = var.enable_volume_tags ? merge(var.tags, var.volume_tags, { "Name" = var.name }) : null vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null timeouts { @@ -384,7 +384,7 @@ resource "aws_instance" "ignore_ami" { private_ip = var.private_ip dynamic "root_block_device" { - for_each = var.root_block_device != null ? var.root_block_device : {} + for_each = var.root_block_device != null ? [var.root_block_device] : [] content { delete_on_termination = root_block_device.value.delete_on_termination @@ -398,21 +398,15 @@ resource "aws_instance" "ignore_ami" { } } - secondary_private_ips = var.secondary_private_ips - source_dest_check = var.network_interface != null ? null : var.source_dest_check - subnet_id = var.subnet_id - - tags = merge( - var.tags, - var.instance_tags, - { "Name" = var.name }, - ) - + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + tags = local.instance_tags tenancy = var.tenancy user_data = var.user_data user_data_base64 = var.user_data_base64 user_data_replace_on_change = var.user_data_replace_on_change - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + volume_tags = var.enable_volume_tags ? merge(var.tags, var.volume_tags, { "Name" = var.name }) : null vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null timeouts { @@ -438,12 +432,13 @@ resource "aws_spot_instance_request" "this" { region = var.region # Spot request specific attributes - launch_group = var.spot_launch_group - spot_price = var.spot_price - spot_type = var.spot_type - wait_for_fulfillment = var.spot_wait_for_fulfillment - valid_from = var.spot_valid_from - valid_until = var.spot_valid_until + instance_interruption_behavior = var.spot_instance_interruption_behavior + launch_group = var.spot_launch_group + spot_price = var.spot_price + spot_type = var.spot_type + wait_for_fulfillment = var.spot_wait_for_fulfillment + valid_from = var.spot_valid_from + valid_until = var.spot_valid_until # End spot request specific attributes ami = local.ami @@ -576,7 +571,7 @@ resource "aws_spot_instance_request" "this" { private_ip = var.private_ip dynamic "root_block_device" { - for_each = var.root_block_device != null ? var.root_block_device : {} + for_each = var.root_block_device != null ? [var.root_block_device] : [] content { delete_on_termination = root_block_device.value.delete_on_termination @@ -585,32 +580,40 @@ resource "aws_spot_instance_request" "this" { kms_key_id = root_block_device.value.kms_key_id tags = root_block_device.value.tags throughput = root_block_device.value.throughput - volume_size = try(root_block_device.value.volume_size, root_block_device.value.size, null) - volume_type = try(root_block_device.value.volume_type, root_block_device.value.type, null) + volume_size = root_block_device.value.size + volume_type = root_block_device.value.type } } - secondary_private_ips = var.secondary_private_ips - source_dest_check = var.network_interface != null ? null : var.source_dest_check - subnet_id = var.subnet_id - - tags = merge( - var.tags, - var.instance_tags, - { "Name" = var.name }, - ) - + secondary_private_ips = var.secondary_private_ips + source_dest_check = var.network_interface != null ? null : var.source_dest_check + subnet_id = var.subnet_id + tags = local.instance_tags tenancy = var.tenancy user_data = var.user_data user_data_base64 = var.user_data_base64 user_data_replace_on_change = var.user_data_replace_on_change - volume_tags = var.enable_volume_tags ? merge({ "Name" = var.name }, var.volume_tags) : null + volume_tags = var.enable_volume_tags ? merge(var.tags, var.volume_tags, { "Name" = var.name }) : null vpc_security_group_ids = var.network_interface == null ? local.vpc_security_group_ids : null timeouts { create = try(var.timeouts.create, null) delete = try(var.timeouts.delete, null) } + + lifecycle { + ignore_changes = [ + ebs_block_device, + ] + } +} + +resource "aws_ec2_tag" "spot_instance" { + for_each = { for k, v in local.instance_tags : k => v if local.create && var.create_spot_instance } + + resource_id = aws_spot_instance_request.this[0].spot_instance_id + key = each.key + value = each.value } ################################################################################ @@ -728,7 +731,9 @@ locals { } data "aws_subnet" "this" { - count = local.create_security_group && var.subnet_id != null ? 1 : 0 + count = local.create_security_group ? 1 : 0 + + region = var.region id = var.subnet_id } @@ -762,7 +767,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { cidr_ipv4 = each.value.cidr_ipv4 cidr_ipv6 = each.value.cidr_ipv6 description = each.value.description - from_port = each.value.from_port + from_port = coalesce(each.value.from_port, each.value.to_port) ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id referenced_security_group_id = each.value.referenced_security_group_id @@ -775,7 +780,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { each.value.tags, ) - to_port = each.value.to_port + to_port = coalesce(each.value.to_port, each.value.from_port) } resource "aws_vpc_security_group_ingress_rule" "this" { @@ -786,7 +791,7 @@ resource "aws_vpc_security_group_ingress_rule" "this" { cidr_ipv4 = each.value.cidr_ipv4 cidr_ipv6 = each.value.cidr_ipv6 description = each.value.description - from_port = each.value.from_port + from_port = coalesce(each.value.from_port, each.value.to_port) ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id referenced_security_group_id = each.value.referenced_security_group_id @@ -799,7 +804,7 @@ resource "aws_vpc_security_group_ingress_rule" "this" { each.value.tags, ) - to_port = each.value.to_port + to_port = coalesce(each.value.to_port, each.value.from_port) } ################################################################################ diff --git a/variables.tf b/variables.tf index 7a168a63..f354c2ce 100644 --- a/variables.tf +++ b/variables.tf @@ -273,7 +273,7 @@ variable "private_ip" { variable "root_block_device" { description = "Customize details about the root block device of the instance. See Block Devices below for details" - type = map(object({ + type = object({ delete_on_termination = optional(bool) encrypted = optional(bool) iops = optional(number) @@ -282,7 +282,7 @@ variable "root_block_device" { throughput = optional(number) size = optional(number) type = optional(string) - })) + }) default = null } @@ -374,6 +374,12 @@ variable "create_spot_instance" { default = false } +variable "spot_instance_interruption_behavior" { + description = "Indicates Spot instance behavior when it is interrupted. Valid values are `terminate`, `stop`, or `hibernate`" + type = string + default = null +} + variable "spot_launch_group" { description = "A launch group is a group of spot instances that launch together and terminate together. If left empty instances are launched and terminated individually" type = string @@ -536,7 +542,7 @@ variable "security_group_egress_rules" { cidr_ipv6 = optional(string) description = optional(string) from_port = optional(number) - ip_protocol = optional(string) + ip_protocol = optional(string, "tcp") prefix_list_id = optional(string) referenced_security_group_id = optional(string) tags = optional(map(string), {}) @@ -563,7 +569,7 @@ variable "security_group_ingress_rules" { cidr_ipv6 = optional(string) description = optional(string) from_port = optional(number) - ip_protocol = optional(string) + ip_protocol = optional(string, "tcp") prefix_list_id = optional(string) referenced_security_group_id = optional(string) tags = optional(map(string), {}) diff --git a/wrappers/main.tf b/wrappers/main.tf index 6d6a90be..fe9fc307 100644 --- a/wrappers/main.tf +++ b/wrappers/main.tf @@ -76,25 +76,26 @@ module "wrapper" { ip_protocol = "-1" } }) - security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, null) - security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) - security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) - security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) - security_group_vpc_id = try(each.value.security_group_vpc_id, var.defaults.security_group_vpc_id, null) - source_dest_check = try(each.value.source_dest_check, var.defaults.source_dest_check, null) - spot_launch_group = try(each.value.spot_launch_group, var.defaults.spot_launch_group, null) - spot_price = try(each.value.spot_price, var.defaults.spot_price, null) - spot_type = try(each.value.spot_type, var.defaults.spot_type, null) - spot_valid_from = try(each.value.spot_valid_from, var.defaults.spot_valid_from, null) - spot_valid_until = try(each.value.spot_valid_until, var.defaults.spot_valid_until, null) - spot_wait_for_fulfillment = try(each.value.spot_wait_for_fulfillment, var.defaults.spot_wait_for_fulfillment, null) - subnet_id = try(each.value.subnet_id, var.defaults.subnet_id, null) - tags = try(each.value.tags, var.defaults.tags, {}) - tenancy = try(each.value.tenancy, var.defaults.tenancy, null) - timeouts = try(each.value.timeouts, var.defaults.timeouts, {}) - user_data = try(each.value.user_data, var.defaults.user_data, null) - user_data_base64 = try(each.value.user_data_base64, var.defaults.user_data_base64, null) - user_data_replace_on_change = try(each.value.user_data_replace_on_change, var.defaults.user_data_replace_on_change, null) - volume_tags = try(each.value.volume_tags, var.defaults.volume_tags, {}) - vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, []) + security_group_ingress_rules = try(each.value.security_group_ingress_rules, var.defaults.security_group_ingress_rules, null) + security_group_name = try(each.value.security_group_name, var.defaults.security_group_name, null) + security_group_tags = try(each.value.security_group_tags, var.defaults.security_group_tags, {}) + security_group_use_name_prefix = try(each.value.security_group_use_name_prefix, var.defaults.security_group_use_name_prefix, true) + security_group_vpc_id = try(each.value.security_group_vpc_id, var.defaults.security_group_vpc_id, null) + source_dest_check = try(each.value.source_dest_check, var.defaults.source_dest_check, null) + spot_instance_interruption_behavior = try(each.value.spot_instance_interruption_behavior, var.defaults.spot_instance_interruption_behavior, null) + spot_launch_group = try(each.value.spot_launch_group, var.defaults.spot_launch_group, null) + spot_price = try(each.value.spot_price, var.defaults.spot_price, null) + spot_type = try(each.value.spot_type, var.defaults.spot_type, null) + spot_valid_from = try(each.value.spot_valid_from, var.defaults.spot_valid_from, null) + spot_valid_until = try(each.value.spot_valid_until, var.defaults.spot_valid_until, null) + spot_wait_for_fulfillment = try(each.value.spot_wait_for_fulfillment, var.defaults.spot_wait_for_fulfillment, null) + subnet_id = try(each.value.subnet_id, var.defaults.subnet_id, null) + tags = try(each.value.tags, var.defaults.tags, {}) + tenancy = try(each.value.tenancy, var.defaults.tenancy, null) + timeouts = try(each.value.timeouts, var.defaults.timeouts, {}) + user_data = try(each.value.user_data, var.defaults.user_data, null) + user_data_base64 = try(each.value.user_data_base64, var.defaults.user_data_base64, null) + user_data_replace_on_change = try(each.value.user_data_replace_on_change, var.defaults.user_data_replace_on_change, null) + volume_tags = try(each.value.volume_tags, var.defaults.volume_tags, {}) + vpc_security_group_ids = try(each.value.vpc_security_group_ids, var.defaults.vpc_security_group_ids, []) } From 0e4780799196c0e590b546b330e8da60151ae405 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 24 Jun 2025 13:48:51 -0500 Subject: [PATCH 3/4] chore: Move upgrade guides to docs folder --- UPGRADE-3.0.md => docs/UPGRADE-3.0.md | 0 UPGRADE-6.0.md => docs/UPGRADE-6.0.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename UPGRADE-3.0.md => docs/UPGRADE-3.0.md (100%) rename UPGRADE-6.0.md => docs/UPGRADE-6.0.md (100%) diff --git a/UPGRADE-3.0.md b/docs/UPGRADE-3.0.md similarity index 100% rename from UPGRADE-3.0.md rename to docs/UPGRADE-3.0.md diff --git a/UPGRADE-6.0.md b/docs/UPGRADE-6.0.md similarity index 100% rename from UPGRADE-6.0.md rename to docs/UPGRADE-6.0.md From 1610c3c54bbdbc16066155e24e34eb76e3e0c0c2 Mon Sep 17 00:00:00 2001 From: Bryant Biggs Date: Tue, 24 Jun 2025 14:01:23 -0500 Subject: [PATCH 4/4] fix: Correct security group port overlap --- main.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.tf b/main.tf index d2524c9a..b116b406 100644 --- a/main.tf +++ b/main.tf @@ -767,7 +767,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { cidr_ipv4 = each.value.cidr_ipv4 cidr_ipv6 = each.value.cidr_ipv6 description = each.value.description - from_port = coalesce(each.value.from_port, each.value.to_port) + from_port = try(coalesce(each.value.from_port, each.value.to_port), null) ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id referenced_security_group_id = each.value.referenced_security_group_id @@ -780,7 +780,7 @@ resource "aws_vpc_security_group_egress_rule" "this" { each.value.tags, ) - to_port = coalesce(each.value.to_port, each.value.from_port) + to_port = try(coalesce(each.value.to_port, each.value.from_port), null) } resource "aws_vpc_security_group_ingress_rule" "this" { @@ -791,7 +791,7 @@ resource "aws_vpc_security_group_ingress_rule" "this" { cidr_ipv4 = each.value.cidr_ipv4 cidr_ipv6 = each.value.cidr_ipv6 description = each.value.description - from_port = coalesce(each.value.from_port, each.value.to_port) + from_port = try(coalesce(each.value.from_port, each.value.to_port), null) ip_protocol = each.value.ip_protocol prefix_list_id = each.value.prefix_list_id referenced_security_group_id = each.value.referenced_security_group_id @@ -804,7 +804,7 @@ resource "aws_vpc_security_group_ingress_rule" "this" { each.value.tags, ) - to_port = coalesce(each.value.to_port, each.value.from_port) + to_port = try(coalesce(each.value.to_port, each.value.from_port), null) } ################################################################################