fix: add primary version 6 flag

Signed-off-by: matttrach <matt.trachier@suse.com>
This commit is contained in:
matttrach 2024-07-25 15:41:39 -05:00
parent 22bed2b03c
commit a32e49e3a0
No known key found for this signature in database
GPG Key ID: E082F2592F87D4AE
10 changed files with 366 additions and 177 deletions

View File

@ -8,11 +8,18 @@ See the examples/select/image for an example of how to do this
## Recent Changes ## Recent Changes
1. Rename images 1. IPv6 Only Support
To support load balanced IPv6 only instances, the primary interface needs to have the 'primary ipv6 enabled' flag set.
This flag is not currently available in the Terraform provider, but a PR exists: https://github.com/hashicorp/terraform-provider-aws/pull/36425
Until the provider supports this flag we are using a workaround.
The workaround requires the AWS CLI to be installed on the server running Terraform.
The AWS CLI will use the same authentication mechanisms as Terraform, so there is no need to configure additional credentials.
WARNING! If deploying with `ip_family = "ipv6"` the server running Terraform must have the AWS CLI installed.
2. Rename images
- Removed SUSE images that weren't BYOS (bring your own subscription) - Removed SUSE images that weren't BYOS (bring your own subscription)
- Amazon subscriptions are harder to automate and don't provide direct service, it ends up being a hidden fee of using the image. Instead, users can use the BYOS image without a subscription until they need one, and then they can add a subscription separately bought from SUSE. - Amazon subscriptions are harder to automate and don't provide direct service, it ends up being a hidden fee of using the image. Instead, users can use the BYOS image without a subscription until they need one, and then they can add a subscription separately bought from SUSE.
- Started using SUSE cloud info API to get the latest image names - Started using SUSE cloud info API to get the latest image names
2. WARNING! Refactor! 3. WARNING! Refactor!
A new Major version and a few new tricks. A new Major version and a few new tricks.
I don't like breaking the interface, but to enable new functionality it made the most sense to refactor. I don't like breaking the interface, but to enable new functionality it made the most sense to refactor.
- set the private ip for your sever - set the private ip for your sever
@ -24,11 +31,6 @@ See the examples/select/image for an example of how to do this
- look out for attributes like "server_use_strategy" to enable or disable features - look out for attributes like "server_use_strategy" to enable or disable features
- indirect access! - indirect access!
- now you can assign aws lb target group associations when you generate your server - now you can assign aws lb target group associations when you generate your server
3. New Images!
- Added SUSE Liberty 7.9
- Added SLE Micro 5.5 (all subscription types)
- WARNING! we can't test llc (US and China) images due to our account geolocation (Germany)
## AWS Access ## AWS Access

View File

@ -0,0 +1,7 @@
# Basic
This example shows the simpliest implementation of this module.
In this case "simple" means using the fewest features.
This bare bones example will deploy a server into a VPC with no external access.
We will show how to enable features individually in futher examples.

View File

@ -0,0 +1,85 @@
provider "aws" {
default_tags {
tags = {
Id = local.identifier
Owner = local.email
}
}
}
locals {
identifier = var.identifier # this is a random unique string that can be used to identify resources in the cloud provider
category = "basic"
example = "dualstack"
email = "terraform-ci@suse.com"
project_name = "tf-${substr(md5(join("-", [local.category, local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}"
image = "sles-15"
username = lower(substr("tf-${local.identifier}", 0, 32))
ip = chomp(data.http.myip.response_body)
ssh_key = var.key
}
data "http" "myip" {
url = "https://ipinfo.io/ip"
retry {
attempts = 2
min_delay_ms = 1000
}
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "random_pet" "server" {
keepers = {
# regenerate the pet name when the identifier changes
identifier = local.identifier
}
length = 1
}
module "access" {
source = "rancher/access/aws"
version = "v3.1.2"
vpc_name = "${local.project_name}-vpc"
vpc_type = "dualstack"
security_group_name = "${local.project_name}-sg"
security_group_type = "project"
load_balancer_name = "${local.project_name}-lb"
domain_use_strategy = "skip"
}
module "this" {
depends_on = [
module.access,
]
source = "../../../" # change this to "rancher/server/aws" per https://registry.terraform.io/modules/rancher/server/aws/latest
# version = "v1.1.1" # when using this example you will need to set the version
image_type = local.image
server_name = "${local.project_name}-${random_pet.server.id}"
server_type = "small"
subnet_name = keys(module.access.subnets)[0]
server_ip_family = "dualstack"
security_group_name = module.access.security_group.tags_all.Name
# direct_access_use_strategy = "ssh" # either the subnet needs to be public or you must add an eip
# cloudinit_use_strategy = "default" # use the default cloudinit config
# server_access_addresses = { # you must include ssh access here to enable setup
# "runner" = {
# port = 22
# protocol = "tcp"
# ip_family = "ipv4"
# cidrs = ["${local.ip}/32"]
# }
# }
# server_user = {
# user = local.username
# aws_keypair_use_strategy = "skip" # we will use cloud-init to add a keypair directly
# ssh_key_name = "" # not creating or selecting a key, but this field is still required
# public_ssh_key = local.ssh_key # ssh key to add via cloud-init
# user_workfolder = "/home/${local.username}"
# timeout = 5
# }
}

View File

@ -0,0 +1,9 @@
output "server" {
value = module.this.server
}
output "image" {
value = module.this.image
}
output "access" {
value = module.access
}

View File

@ -0,0 +1,6 @@
variable "identifier" {
type = string
}
variable "key" {
type = string
}

View File

@ -0,0 +1,24 @@
terraform {
required_version = ">= 1.5.0, < 1.6"
required_providers {
local = {
source = "hashicorp/local"
version = ">= 2.4"
}
aws = {
source = "hashicorp/aws"
version = ">= 5.11"
}
random = {
source = "hashicorp/random"
version = ">= 3.1"
}
acme = { # used in the access module
source = "vancluever/acme"
version = ">= 2.0"
}
}
}
provider "acme" {
server_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
}

View File

@ -13,8 +13,8 @@ locals {
example = "sles15" example = "sles15"
email = "terraform-ci@suse.com" email = "terraform-ci@suse.com"
project_name = "tf-${substr(md5(join("-", [local.category, local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}" project_name = "tf-${substr(md5(join("-", [local.category, local.example, md5(local.identifier)])), 0, 5)}-${local.identifier}"
username = lower(substr("tf-${local.identifier}", 0, 32))
image = "sles-15" image = "sles-15"
username = lower(substr("tf-${local.identifier}", 0, 32))
ip = chomp(data.http.myip.response_body) ip = chomp(data.http.myip.response_body)
ssh_key = var.key ssh_key = var.key
} }

View File

@ -60,6 +60,11 @@ data "aws_subnet" "general_info_create" {
values = [local.subnet] values = [local.subnet]
} }
} }
data "aws_availability_zone" "general_info_create" {
count = local.create
name = data.aws_subnet.general_info_create[0].availability_zone
}
data "aws_vpc" "general_info_create" { data "aws_vpc" "general_info_create" {
count = local.create count = local.create
id = data.aws_security_group.general_info_create[0].vpc_id id = data.aws_security_group.general_info_create[0].vpc_id
@ -123,3 +128,35 @@ resource "aws_instance" "created" {
] ]
} }
} }
# WARNING! This forces a dependency on the AWS CLI, but only for "ipv6 only" servers.
# This is a workaround for the fact that the terraform AWS provider doesn't support the primary ipv6 flag yet.
# When the provider supports it, this can be removed and the attribute added to the instance resource.
resource "terraform_data" "set_primary_ipv6" {
count = (local.ip_family == "ipv6" ? local.create : 0)
depends_on = [
data.aws_security_group.general_info_create,
data.aws_subnet.general_info_create,
aws_key_pair.created,
aws_instance.created,
]
triggers_replace = {
"aws_instance" = "${aws_instance.created[0].id}"
}
provisioner "local-exec" {
command = <<-EOT
if ! aws ec2 describe-network-interfaces \
--network-interface-ids ${aws_instance.created[0].primary_network_interface_id} \
--region ${data.aws_availability_zone.general_info_create[0].region} \
--query 'NetworkInterfaces[0].Ipv6Addresses[?IsPrimaryIpv6==`true`]' \
--output text | grep -q .; then
aws ec2 modify-network-interface-attribute \
--network-interface-id ${aws_instance.created[0].primary_network_interface_id} \
--enable-primary-ipv6 \
--region ${data.aws_availability_zone.general_info_create[0].region}
else
echo "Primary IPv6 is already enabled for this network interface"
fi
EOT
}
}

View File

@ -1,202 +1,219 @@
package test package test
import ( import (
"fmt" "fmt"
"os" "os"
"testing" "testing"
"github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/ssh" "github.com/gruntwork-io/terratest/modules/ssh"
"github.com/gruntwork-io/terratest/modules/terraform" "github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestBasicBasic(t *testing.T) { func TestBasicBasic(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "basic" directory := "basic"
region := "us-west-2" region := "us-west-2"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicDualstack(t *testing.T) {
t.Parallel()
id := os.Getenv("IDENTIFIER")
if id == "" {
id = random.UniqueId()
}
uniqueID := id + "-" + random.UniqueId()
category := "basic"
directory := "dualstack"
region := "us-west-2"
owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
delete(terraformOptions.Vars, "key_name")
defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
}
func TestBasicPrivateIp(t *testing.T) { func TestBasicPrivateIp(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "privateip" directory := "privateip"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicIndirectOnly(t *testing.T) { func TestBasicIndirectOnly(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "indirectonly" directory := "indirectonly"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicIndirectDomain(t *testing.T) { func TestBasicIndirectDomain(t *testing.T) {
t.Parallel() t.Parallel()
zone := os.Getenv("ZONE") zone := os.Getenv("ZONE")
acmeserver := os.Getenv("ACME_SERVER_URL") acmeserver := os.Getenv("ACME_SERVER_URL")
if acmeserver == "" { if acmeserver == "" {
os.Setenv("ACME_SERVER_URL", "https://acme-staging-v02.api.letsencrypt.org/directory") os.Setenv("ACME_SERVER_URL", "https://acme-staging-v02.api.letsencrypt.org/directory")
} }
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "indirectdomain" directory := "indirectdomain"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraformOptions.Vars["zone"] = zone terraformOptions.Vars["zone"] = zone
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicDirectNetworkOnly(t *testing.T) { func TestBasicDirectNetworkOnly(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "directnetworkonly" directory := "directnetworkonly"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicDirectNetworkDomain(t *testing.T) { func TestBasicDirectNetworkDomain(t *testing.T) {
t.Parallel() t.Parallel()
zone := os.Getenv("ZONE") zone := os.Getenv("ZONE")
acmeserver := os.Getenv("ACME_SERVER_URL") acmeserver := os.Getenv("ACME_SERVER_URL")
if acmeserver == "" { if acmeserver == "" {
os.Setenv("ACME_SERVER_URL", "https://acme-staging-v02.api.letsencrypt.org/directory") os.Setenv("ACME_SERVER_URL", "https://acme-staging-v02.api.letsencrypt.org/directory")
} }
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "directnetworkdomain" directory := "directnetworkdomain"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key") delete(terraformOptions.Vars, "key")
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraformOptions.Vars["zone"] = zone terraformOptions.Vars["zone"] = zone
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
} }
func TestBasicDirectSshEip(t *testing.T) { func TestBasicDirectSshEip(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "directssheip" directory := "directssheip"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
sshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair) sshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair)
defer sshAgent.Stop() defer sshAgent.Stop()
terraformOptions.SshAgent = sshAgent terraformOptions.SshAgent = sshAgent
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraform.InitAndPlan(t, terraformOptions) terraform.InitAndPlan(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
out := terraform.OutputAll(t,terraformOptions) out := terraform.OutputAll(t, terraformOptions)
t.Logf("out: %v", out) t.Logf("out: %v", out)
outputServer, ok := out["server"].(map[string]interface{}) outputServer, ok := out["server"].(map[string]interface{})
assert.True(t, ok, fmt.Sprintf("Wrong data type for 'server', expected map[string], got %T", out["server"])) assert.True(t, ok, fmt.Sprintf("Wrong data type for 'server', expected map[string], got %T", out["server"]))
outputImage, ok := out["image"].(map[string]interface{}) outputImage, ok := out["image"].(map[string]interface{})
assert.True(t, ok, fmt.Sprintf("Wrong data type for 'image', expected map[string], got %T", out["image"])) assert.True(t, ok, fmt.Sprintf("Wrong data type for 'image', expected map[string], got %T", out["image"]))
assert.NotEmpty(t, outputServer["public_ip"], "The 'server.public_ip' is empty") assert.NotEmpty(t, outputServer["public_ip"], "The 'server.public_ip' is empty")
assert.NotEmpty(t, outputImage["id"], "The 'image.id' is empty") assert.NotEmpty(t, outputImage["id"], "The 'image.id' is empty")
} }
func TestBasicDirectSshSubnet(t *testing.T) { func TestBasicDirectSshSubnet(t *testing.T) {
t.Parallel() t.Parallel()
id := os.Getenv("IDENTIFIER") id := os.Getenv("IDENTIFIER")
if id == "" { if id == "" {
id = random.UniqueId() id = random.UniqueId()
} }
uniqueID := id + "-" + random.UniqueId() uniqueID := id + "-" + random.UniqueId()
category := "basic" category := "basic"
directory := "directsshsubnet" directory := "directsshsubnet"
region := "us-west-1" region := "us-west-1"
owner := "terraform-ci@suse.com" owner := "terraform-ci@suse.com"
terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID) terraformOptions, keyPair := setup(t, category, directory, region, owner, uniqueID)
sshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair) sshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair)
defer sshAgent.Stop() defer sshAgent.Stop()
terraformOptions.SshAgent = sshAgent terraformOptions.SshAgent = sshAgent
defer teardown(t, category, directory, keyPair) defer teardown(t, category, directory, keyPair)
defer terraform.Destroy(t, terraformOptions) defer terraform.Destroy(t, terraformOptions)
delete(terraformOptions.Vars, "key_name") delete(terraformOptions.Vars, "key_name")
terraform.InitAndPlan(t, terraformOptions) terraform.InitAndPlan(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
out := terraform.OutputAll(t,terraformOptions) out := terraform.OutputAll(t, terraformOptions)
t.Logf("out: %v", out) t.Logf("out: %v", out)
outputServer, ok := out["server"].(map[string]interface{}) outputServer, ok := out["server"].(map[string]interface{})
assert.True(t, ok, fmt.Sprintf("Wrong data type for 'server', expected map[string], got %T", out["server"])) assert.True(t, ok, fmt.Sprintf("Wrong data type for 'server', expected map[string], got %T", out["server"]))
outputImage, ok := out["image"].(map[string]interface{}) outputImage, ok := out["image"].(map[string]interface{})
assert.True(t, ok, fmt.Sprintf("Wrong data type for 'image', expected map[string], got %T", out["image"])) assert.True(t, ok, fmt.Sprintf("Wrong data type for 'image', expected map[string], got %T", out["image"]))
assert.NotEmpty(t, outputServer["public_ip"], "The 'server.public_ip' is empty") assert.NotEmpty(t, outputServer["public_ip"], "The 'server.public_ip' is empty")
assert.NotEmpty(t, outputImage["id"], "The 'image.id' is empty") assert.NotEmpty(t, outputImage["id"], "The 'image.id' is empty")
} }

View File

@ -157,6 +157,8 @@ variable "server_ip_family" {
The ip family to use for the server, must be one of "ipv4", "dualstack", or "ipv6". The ip family to use for the server, must be one of "ipv4", "dualstack", or "ipv6".
This is mainly determined by the VPC and subnet that you are deploying to, This is mainly determined by the VPC and subnet that you are deploying to,
attempting to deploy a dualstack or ipv6 server to a non-dualstack/ipv6 VPC/subnet will result in failed connections. attempting to deploy a dualstack or ipv6 server to a non-dualstack/ipv6 VPC/subnet will result in failed connections.
WARNING! When this is set to "ipv6" the server running Terraform must have the AWS CLI installed.
This is due to a missing feature in the AWS provider that allows ipv6 instances to be attached to load balancer target groups.
EOT EOT
default = "ipv4" default = "ipv4"
validation { validation {