diff --git a/.changelog/39507.txt b/.changelog/39507.txt new file mode 100644 index 00000000000..a4a55c44481 --- /dev/null +++ b/.changelog/39507.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_globalaccelerator_endpoint_group: Add `endpoint_configuration.attachment_arn` argument +``` diff --git a/internal/service/globalaccelerator/endpoint_group.go b/internal/service/globalaccelerator/endpoint_group.go index 754b8b76627..8fc97f32bb8 100644 --- a/internal/service/globalaccelerator/endpoint_group.go +++ b/internal/service/globalaccelerator/endpoint_group.go @@ -9,6 +9,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/service/globalaccelerator" awstypes "github.com/aws/aws-sdk-go-v2/service/globalaccelerator/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -53,6 +54,11 @@ func resourceEndpointGroup() *schema.Resource { Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "attachment_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, "client_ip_preservation_enabled": { Type: schema.TypeBool, Optional: true, @@ -230,8 +236,13 @@ func resourceEndpointGroupRead(ctx context.Context, d *schema.ResourceData, meta return sdkdiag.AppendFromErr(diags, err) } + crossAccountAttachments, err := findCrossAccountAttachments(ctx, conn, endpointGroup.EndpointDescriptions) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading Global Accelerator Endpoint Group (%s) cross-account attachments: %s", d.Id(), err) + } + d.Set(names.AttrARN, endpointGroup.EndpointGroupArn) - if err := d.Set("endpoint_configuration", flattenEndpointDescriptions(endpointGroup.EndpointDescriptions)); err != nil { + if err := d.Set("endpoint_configuration", flattenEndpointDescriptions(endpointGroup.EndpointDescriptions, crossAccountAttachments)); err != nil { return sdkdiag.AppendErrorf(diags, "setting endpoint_configuration: %s", err) } d.Set("endpoint_group_region", endpointGroup.EndpointGroupRegion) @@ -372,6 +383,10 @@ func expandEndpointConfiguration(tfMap map[string]interface{}) *awstypes.Endpoin apiObject := &awstypes.EndpointConfiguration{} + if v, ok := tfMap["attachment_arn"].(string); ok && v != "" { + apiObject.AttachmentArn = aws.String(v) + } + if v, ok := tfMap["client_ip_preservation_enabled"].(bool); ok { apiObject.ClientIPPreservationEnabled = aws.Bool(v) } @@ -457,7 +472,7 @@ func expandPortOverrides(tfList []interface{}) []awstypes.PortOverride { return apiObjects } -func flattenEndpointDescription(apiObject *awstypes.EndpointDescription) map[string]interface{} { +func flattenEndpointDescription(apiObject *awstypes.EndpointDescription, crossAccountAttachments map[string]string) map[string]interface{} { if apiObject == nil { return nil } @@ -469,7 +484,12 @@ func flattenEndpointDescription(apiObject *awstypes.EndpointDescription) map[str } if v := apiObject.EndpointId; v != nil { - tfMap["endpoint_id"] = aws.ToString(v) + v := aws.ToString(v) + tfMap["endpoint_id"] = v + + if v, ok := crossAccountAttachments[v]; ok { + tfMap["attachment_arn"] = v + } } if v := apiObject.Weight; v != nil { @@ -479,7 +499,7 @@ func flattenEndpointDescription(apiObject *awstypes.EndpointDescription) map[str return tfMap } -func flattenEndpointDescriptions(apiObjects []awstypes.EndpointDescription) []interface{} { +func flattenEndpointDescriptions(apiObjects []awstypes.EndpointDescription, crossAccountAttachments map[string]string) []interface{} { if len(apiObjects) == 0 { return nil } @@ -487,7 +507,7 @@ func flattenEndpointDescriptions(apiObjects []awstypes.EndpointDescription) []in var tfList []interface{} for _, apiObject := range apiObjects { - tfList = append(tfList, flattenEndpointDescription(&apiObject)) + tfList = append(tfList, flattenEndpointDescription(&apiObject, crossAccountAttachments)) } return tfList @@ -524,3 +544,52 @@ func flattenPortOverrides(apiObjects []awstypes.PortOverride) []interface{} { return tfList } + +func findCrossAccountAttachments(ctx context.Context, conn *globalaccelerator.Client, endpointDescriptions []awstypes.EndpointDescription) (map[string]string, error) { + crossAccountAttachments := map[string]string{} + + accounts := map[string]bool{} + for _, endpointDescription := range endpointDescriptions { + arn, err := arn.Parse(aws.ToString(endpointDescription.EndpointId)) + if err != nil { + continue // Not an ARN => not a cross-account resource. + } + + accountID := arn.AccountID + if accounts[accountID] { + continue + } + accounts[accountID] = true + + input := &globalaccelerator.ListCrossAccountResourcesInput{ + ResourceOwnerAwsAccountId: aws.String(accountID), + } + crossAccountResources, err := findCrossAccountResources(ctx, conn, input) + if err != nil { + return nil, err + } + + for _, crossAccountResource := range crossAccountResources { + crossAccountAttachments[aws.ToString(crossAccountResource.EndpointId)] = aws.ToString(crossAccountResource.AttachmentArn) + } + } + + return crossAccountAttachments, nil +} + +func findCrossAccountResources(ctx context.Context, conn *globalaccelerator.Client, input *globalaccelerator.ListCrossAccountResourcesInput) ([]awstypes.CrossAccountResource, error) { + var output []awstypes.CrossAccountResource + + pages := globalaccelerator.NewListCrossAccountResourcesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if err != nil { + return nil, err + } + + output = append(output, page.CrossAccountResources...) + } + + return output, nil +} diff --git a/internal/service/globalaccelerator/endpoint_group_test.go b/internal/service/globalaccelerator/endpoint_group_test.go index d73c4b1ab74..ef8b44c4da6 100644 --- a/internal/service/globalaccelerator/endpoint_group_test.go +++ b/internal/service/globalaccelerator/endpoint_group_test.go @@ -107,6 +107,7 @@ func TestAccGlobalAcceleratorEndpointGroup_ALBEndpoint_clientIP(t *testing.T) { acctest.MatchResourceAttrGlobalARN(resourceName, names.AttrARN, "globalaccelerator", regexache.MustCompile(`accelerator/[^/]+/listener/[^/]+/endpoint-group/[^/]+`)), resource.TestCheckResourceAttr(resourceName, "endpoint_configuration.#", acctest.Ct1), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "endpoint_configuration.*", map[string]string{ + "attachment_arn": "", "client_ip_preservation_enabled": acctest.CtFalse, names.AttrWeight: "20", }), @@ -429,6 +430,47 @@ func TestAccGlobalAcceleratorEndpointGroup_update(t *testing.T) { }) } +func TestAccGlobalAcceleratorEndpointGroup_crossAccountAttachment(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.EndpointGroup + resourceName := "aws_globalaccelerator_endpoint_group.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + acctest.PreCheckMultipleRegion(t, 2) + acctest.PreCheckAlternateAccount(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.GlobalAcceleratorServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), + CheckDestroy: testAccCheckEndpointGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccEndpointGroupConfig_crossAccountAttachement(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointGroupExists(ctx, resourceName, &v), + acctest.MatchResourceAttrGlobalARN(resourceName, names.AttrARN, "globalaccelerator", regexache.MustCompile(`accelerator/[^/]+/listener/[^/]+/endpoint-group/[^/]+`)), + resource.TestCheckResourceAttr(resourceName, "endpoint_configuration.#", acctest.Ct1), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "endpoint_configuration.*", map[string]string{ + "client_ip_preservation_enabled": acctest.CtFalse, + names.AttrWeight: "20", + }), + resource.TestCheckTypeSetElemAttrPair(resourceName, "endpoint_configuration.*.attachment_arn", "aws_globalaccelerator_cross_account_attachment.alt_test", names.AttrARN), + resource.TestCheckTypeSetElemAttrPair(resourceName, "endpoint_configuration.*.endpoint_id", "aws_lb.alt_test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "endpoint_group_region", acctest.AlternateRegion()), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckEndpointGroupExists(ctx context.Context, name string, v *awstypes.EndpointGroup) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -874,3 +916,144 @@ resource "aws_globalaccelerator_endpoint_group" "test" { } `, rName) } + +func testAccEndpointGroupConfig_crossAccountAttachement(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAlternateAccountAlternateRegionProvider(), + fmt.Sprintf(` +############################################################################### +## Alternate account setup. +############################################################################### +data "aws_availability_zones" "alt_available" { + provider = "awsalternate" + + exclude_zone_ids = ["usw2-az4", "usgw1-az2"] + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "alt_test" { + provider = "awsalternate" + + cidr_block = "10.0.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "alt_test" { + provider = "awsalternate" + + count = 2 + + vpc_id = aws_vpc.alt_test.id + availability_zone = data.aws_availability_zones.alt_available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.alt_test.cidr_block, 8, count.index) + + tags = { + Name = %[1]q + } +} + +resource "aws_lb" "alt_test" { + provider = "awsalternate" + + name = %[1]q + internal = false + security_groups = [aws_security_group.alt_test.id] + subnets = aws_subnet.alt_test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "alt_test" { + provider = "awsalternate" + + name = %[1]q + vpc_id = aws_vpc.alt_test.id + + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = %[1]q + } +} + +resource "aws_internet_gateway" "alt_test" { + provider = "awsalternate" + + vpc_id = aws_vpc.alt_test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_globalaccelerator_cross_account_attachment" "alt_test" { + provider = "awsalternate" + + name = %[1]q + principals = [data.aws_caller_identity.current.account_id] + + resource { + endpoint_id = aws_lb.alt_test.arn + } +} + +############################################################################### +## Main account setup. +############################################################################### +data "aws_caller_identity" "current" {} + +resource "aws_globalaccelerator_accelerator" "test" { + name = %[1]q + ip_address_type = "IPV4" + enabled = false +} + +resource "aws_globalaccelerator_listener" "test" { + accelerator_arn = aws_globalaccelerator_accelerator.test.id + protocol = "TCP" + + port_range { + from_port = 80 + to_port = 80 + } +} + +resource "aws_globalaccelerator_endpoint_group" "test" { + listener_arn = aws_globalaccelerator_listener.test.id + + endpoint_configuration { + endpoint_id = aws_lb.alt_test.arn + attachment_arn = aws_globalaccelerator_cross_account_attachment.alt_test.arn + weight = 20 + client_ip_preservation_enabled = false + } + + endpoint_group_region = %[2]q +} +`, rName, acctest.AlternateRegion())) +} diff --git a/website/docs/cdktf/python/r/globalaccelerator_endpoint_group.html.markdown b/website/docs/cdktf/python/r/globalaccelerator_endpoint_group.html.markdown index 68403ed80ca..6d934b0d0ad 100644 --- a/website/docs/cdktf/python/r/globalaccelerator_endpoint_group.html.markdown +++ b/website/docs/cdktf/python/r/globalaccelerator_endpoint_group.html.markdown @@ -58,6 +58,7 @@ Terraform will only perform drift detection of its value when present in a confi **Note:** When client IP address preservation is enabled, the Global Accelerator service creates an EC2 Security Group in the VPC named `GlobalAccelerator` that must be deleted (potentially outside of Terraform) before the VPC will successfully delete. If this EC2 Security Group is not deleted, Terraform will retry the VPC deletion for a few minutes before reporting a `DependencyViolation` error. This cannot be resolved by re-running Terraform. * `endpoint_id` - (Optional) An ID for the endpoint. If the endpoint is a Network Load Balancer or Application Load Balancer, this is the Amazon Resource Name (ARN) of the resource. If the endpoint is an Elastic IP address, this is the Elastic IP address allocation ID. * `weight` - (Optional) The weight associated with the endpoint. When you add weights to endpoints, you configure AWS Global Accelerator to route traffic based on proportions that you specify. +* `attachment_arn` - (Optional) An ARN of an exposed cross-account attachment. See the [AWS documentation](https://docs.aws.amazon.com/global-accelerator/latest/dg/cross-account-resources.html) for more details. `port_override` supports the following arguments: @@ -104,4 +105,4 @@ Using `terraform import`, import Global Accelerator endpoint groups using the `i % terraform import aws_globalaccelerator_endpoint_group.example arn:aws:globalaccelerator::111111111111:accelerator/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/listener/xxxxxxx/endpoint-group/xxxxxxxx ``` - \ No newline at end of file + diff --git a/website/docs/r/globalaccelerator_endpoint_group.html.markdown b/website/docs/r/globalaccelerator_endpoint_group.html.markdown index 05ed1d67bae..65531419843 100644 --- a/website/docs/r/globalaccelerator_endpoint_group.html.markdown +++ b/website/docs/r/globalaccelerator_endpoint_group.html.markdown @@ -41,6 +41,7 @@ Terraform will only perform drift detection of its value when present in a confi `endpoint_configuration` supports the following arguments: +* `attachment_arn` - (Optional) An ARN of an exposed cross-account attachment. See the [AWS documentation](https://docs.aws.amazon.com/global-accelerator/latest/dg/cross-account-resources.html) for more details. * `client_ip_preservation_enabled` - (Optional) Indicates whether client IP address preservation is enabled for an Application Load Balancer endpoint. See the [AWS documentation](https://docs.aws.amazon.com/global-accelerator/latest/dg/preserve-client-ip-address.html) for more details. The default value is `false`. **Note:** When client IP address preservation is enabled, the Global Accelerator service creates an EC2 Security Group in the VPC named `GlobalAccelerator` that must be deleted (potentially outside of Terraform) before the VPC will successfully delete. If this EC2 Security Group is not deleted, Terraform will retry the VPC deletion for a few minutes before reporting a `DependencyViolation` error. This cannot be resolved by re-running Terraform. * `endpoint_id` - (Optional) An ID for the endpoint. If the endpoint is a Network Load Balancer or Application Load Balancer, this is the Amazon Resource Name (ARN) of the resource. If the endpoint is an Elastic IP address, this is the Elastic IP address allocation ID.