diff --git a/.changelog/47471.txt b/.changelog/47471.txt new file mode 100644 index 000000000000..6ee61a6f6745 --- /dev/null +++ b/.changelog/47471.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_capacity_provider: Add `capacity_reservations` configuration block and `RESERVED` value for `capacity_option_type` to support On-Demand Capacity Reservations (ODCR) for managed instances +``` diff --git a/internal/service/ecs/capacity_provider.go b/internal/service/ecs/capacity_provider.go index f87fb0324efa..821cebe47025 100644 --- a/internal/service/ecs/capacity_provider.go +++ b/internal/service/ecs/capacity_provider.go @@ -68,6 +68,57 @@ func resourceCapacityProvider() *schema.Resource { } } + // Validate CapacityOptionType is not changed on update + if diff.Id() != "" { + oldVal, newVal := diff.GetChange("managed_instances_provider.0.instance_launch_template.0.capacity_option_type") + oldType, _ := oldVal.(string) + newType, _ := newVal.(string) + + if oldType != "" && newType != "" && oldType != newType { + return errors.New("capacity_option_type cannot be changed after creation") + } + } + + // Validate capacity reservation rules for managed instances providers + if len(managedProvider) > 0 { + capacityOptionType := diff.Get("managed_instances_provider.0.instance_launch_template.0.capacity_option_type").(string) + capacityReservations := diff.Get("managed_instances_provider.0.instance_launch_template.0.capacity_reservations").([]any) + instanceRequirements := diff.Get("managed_instances_provider.0.instance_launch_template.0.instance_requirements").([]any) + + hasCapacityReservations := len(capacityReservations) > 0 + + // capacity_reservations requires capacity_option_type = RESERVED + if hasCapacityReservations && capacityOptionType != string(awstypes.CapacityOptionTypeReserved) { + return errors.New("capacity_reservations can only be set when capacity_option_type is RESERVED") + } + + // capacity_option_type = RESERVED requires capacity_reservations + if capacityOptionType == string(awstypes.CapacityOptionTypeReserved) && !hasCapacityReservations { + return errors.New("capacity_reservations must be set when capacity_option_type is RESERVED") + } + + // capacity_reservations not allowed with SPOT + if hasCapacityReservations && capacityOptionType == string(awstypes.CapacityOptionTypeSpot) { + return errors.New("capacity_reservations cannot be used with SPOT capacity option type") + } + + if hasCapacityReservations { + reservationPreference := diff.Get("managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_preference").(string) + reservationGroupArn := diff.Get("managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_group_arn").(string) + + // reservation_group_arn only allowed with RESERVATIONS_ONLY or unset preference + if reservationGroupArn != "" && reservationPreference != "" && reservationPreference != string(awstypes.CapacityReservationPreferenceReservationsOnly) { + return errors.New("reservation_group_arn can only be set when reservation_preference is RESERVATIONS_ONLY") + } + + // instance_requirements required for RESERVATIONS_ONLY and RESERVATIONS_FIRST + if (reservationPreference == string(awstypes.CapacityReservationPreferenceReservationsOnly) || + reservationPreference == string(awstypes.CapacityReservationPreferenceReservationsFirst)) && len(instanceRequirements) == 0 { + return errors.New("instance_requirements must be provided when reservation_preference is RESERVATIONS_ONLY or RESERVATIONS_FIRST") + } + } + } + return nil }, @@ -184,10 +235,29 @@ func resourceCapacityProvider() *schema.Resource { "capacity_option_type": { Type: schema.TypeString, Optional: true, - ForceNew: true, Computed: true, ValidateDiagFunc: enum.Validate[awstypes.CapacityOptionType](), }, + "capacity_reservations": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "reservation_group_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, + "reservation_preference": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: enum.Validate[awstypes.CapacityReservationPreference](), + }, + }, + }, + }, "ec2_instance_profile_arn": { Type: schema.TypeString, Required: true, @@ -1017,6 +1087,10 @@ func expandInstanceLaunchTemplateCreate(tfList []any) *awstypes.InstanceLaunchTe apiObject.CapacityOptionType = awstypes.CapacityOptionType(v) } + if v, ok := tfMap["capacity_reservations"].([]any); ok && len(v) > 0 { + apiObject.CapacityReservations = expandCapacityReservationRequest(v) + } + if v, ok := tfMap["ec2_instance_profile_arn"].(string); ok && v != "" { apiObject.Ec2InstanceProfileArn = aws.String(v) } @@ -1048,6 +1122,10 @@ func expandInstanceLaunchTemplateUpdate(tfList []any) *awstypes.InstanceLaunchTe tfMap := tfList[0].(map[string]any) apiObject := &awstypes.InstanceLaunchTemplateUpdate{} + if v, ok := tfMap["capacity_reservations"].([]any); ok && len(v) > 0 { + apiObject.CapacityReservations = expandCapacityReservationRequest(v) + } + if v, ok := tfMap["ec2_instance_profile_arn"].(string); ok && v != "" { apiObject.Ec2InstanceProfileArn = aws.String(v) } @@ -1105,6 +1183,25 @@ func expandManagedInstancesStorageConfiguration(tfList []any) *awstypes.ManagedI return apiObject } +func expandCapacityReservationRequest(tfList []any) *awstypes.CapacityReservationRequest { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap := tfList[0].(map[string]any) + apiObject := &awstypes.CapacityReservationRequest{} + + if v, ok := tfMap["reservation_group_arn"].(string); ok && v != "" { + apiObject.ReservationGroupArn = aws.String(v) + } + + if v, ok := tfMap["reservation_preference"].(string); ok && v != "" { + apiObject.ReservationPreference = awstypes.CapacityReservationPreference(v) + } + + return apiObject +} + func expandInstanceRequirementsRequest(tfList []any) *awstypes.InstanceRequirementsRequest { if len(tfList) == 0 || tfList[0] == nil { return nil @@ -1422,11 +1519,15 @@ func flattenInstanceLaunchTemplate(template *awstypes.InstanceLaunchTemplate) [] } tfMap := map[string]any{ - "capacity_option_type": template.CapacityOptionType, + "capacity_option_type": string(template.CapacityOptionType), "ec2_instance_profile_arn": aws.ToString(template.Ec2InstanceProfileArn), "monitoring": template.Monitoring, } + if template.CapacityReservations != nil { + tfMap["capacity_reservations"] = flattenCapacityReservationRequest(template.CapacityReservations) + } + if template.InstanceRequirements != nil { tfMap["instance_requirements"] = flattenInstanceRequirementsRequest(template.InstanceRequirements) } @@ -1450,6 +1551,22 @@ func flattenInstanceLaunchTemplate(template *awstypes.InstanceLaunchTemplate) [] return []map[string]any{tfMap} } +func flattenCapacityReservationRequest(req *awstypes.CapacityReservationRequest) []map[string]any { + if req == nil { + return nil + } + + tfMap := map[string]any{ + "reservation_preference": string(req.ReservationPreference), + } + + if req.ReservationGroupArn != nil { + tfMap["reservation_group_arn"] = aws.ToString(req.ReservationGroupArn) + } + + return []map[string]any{tfMap} +} + func flattenInstanceRequirementsRequest(req *awstypes.InstanceRequirementsRequest) []map[string]any { if req == nil { return nil diff --git a/internal/service/ecs/capacity_provider_test.go b/internal/service/ecs/capacity_provider_test.go index f8ff2611e9a7..459a45c233f2 100644 --- a/internal/service/ecs/capacity_provider_test.go +++ b/internal/service/ecs/capacity_provider_test.go @@ -1138,3 +1138,562 @@ resource "aws_ecs_capacity_provider" "test" { } `, rName, monitoring)) } + +func TestAccECSCapacityProvider_createManagedInstancesProvider_withCapacityReservations(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_withCapacityReservations(rName, "RESERVATIONS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.#", "1"), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_option_type", "RESERVED"), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_group_arn"), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_preference", "RESERVATIONS_ONLY"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccECSCapacityProvider_updateManagedInstancesProvider_capacityReservations(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_withCapacityReservations(rName, "RESERVATIONS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttrSet(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_group_arn"), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_preference", "RESERVATIONS_ONLY"), + ), + }, + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_withCapacityReservations(rName, "RESERVATIONS_FIRST"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_preference", "RESERVATIONS_FIRST"), + ), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityReservations_validationErrors(t *testing.T) { + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(acctest.Context(t), t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(acctest.Context(t), t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_capacityReservationsWithoutReserved(rName), + ExpectError: regexache.MustCompile(`capacity_reservations can only be set when capacity_option_type is RESERVED`), + }, + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_reservedWithoutCapacityReservations(rName), + ExpectError: regexache.MustCompile(`capacity_reservations must be set when capacity_option_type is RESERVED`), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityReservations_additionalValidationErrors(t *testing.T) { + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(acctest.Context(t), t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(acctest.Context(t), t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_spotWithCapacityReservations(rName), + ExpectError: regexache.MustCompile(`capacity_reservations can only be set when capacity_option_type is RESERVED`), + }, + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_reservationGroupArnWithWrongPreference(rName), + ExpectError: regexache.MustCompile(`reservation_group_arn can only be set when reservation_preference is RESERVATIONS_ONLY`), + }, + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_reservationsWithoutInstanceRequirements(rName), + ExpectError: regexache.MustCompile(`instance_requirements must be provided when reservation_preference is RESERVATIONS_ONLY or RESERVATIONS_FIRST`), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityOptionType_immutable(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_withCapacityReservations(rName, "RESERVATIONS_ONLY"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_option_type", "RESERVED"), + ), + }, + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_capacityOptionTypeChanged(rName), + ExpectError: regexache.MustCompile(`capacity_option_type cannot be changed after creation`), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityReservations_groupArnOnly(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_groupArnOnly(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttrSet(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_group_arn"), + ), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityReservations_excludedPreference(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_excludedPreference(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_reservations.0.reservation_preference", "RESERVATIONS_EXCLUDED"), + ), + }, + }, + }) +} + +func TestAccECSCapacityProvider_capacityOptionType_onDemandExplicit(t *testing.T) { + ctx := acctest.Context(t) + var provider awstypes.CapacityProvider + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_ecs_capacity_provider.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCapacityProviderDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccCapacityProviderConfig_managedInstancesProvider_onDemandExplicit(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCapacityProviderExists(ctx, t, resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "managed_instances_provider.0.instance_launch_template.0.capacity_option_type", "ON_DEMAND"), + ), + }, + }, + }) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_withCapacityReservations(rName, preference string) string { + capacityReservationsBlock := fmt.Sprintf(` + capacity_reservations { + reservation_group_arn = aws_resourcegroups_group.test.arn + reservation_preference = %q + }`, preference) + + if preference != "RESERVATIONS_ONLY" { + capacityReservationsBlock = fmt.Sprintf(` + capacity_reservations { + reservation_preference = %q + }`, preference) + } + + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ec2_capacity_reservation" "test" { + instance_type = "c5.large" + instance_platform = "Linux/UNIX" + availability_zone = data.aws_availability_zones.available.names[0] + instance_count = 1 + + tags = { + Name = %[1]q + CapacityReservationGroup = %[1]q + } +} + +resource "aws_resourcegroups_group" "test" { + name = %[1]q + + resource_query { + query = jsonencode({ + ResourceTypeFilters = ["AWS::EC2::CapacityReservation"] + TagFilters = [{ + Key = "CapacityReservationGroup" + Values = [%[1]q] + }] + }) + } +} + +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + propagate_tags = "NONE" + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + +%[2]s + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + + instance_requirements { + vcpu_count { + min = 1 + } + memory_mib { + min = 1 + } + allowed_instance_types = ["c5.large", "c5a.large", "c5ad.large"] + } + } + } +} +`, rName, capacityReservationsBlock)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_capacityReservationsWithoutReserved(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + + instance_launch_template { + capacity_option_type = "ON_DEMAND" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_preference = "RESERVATIONS_ONLY" + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_reservedWithoutCapacityReservations(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_spotWithCapacityReservations(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + + instance_launch_template { + capacity_option_type = "SPOT" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_preference = "RESERVATIONS_ONLY" + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_reservationGroupArnWithWrongPreference(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_group_arn = "arn:aws:resource-groups:us-west-2:123456789012:group/test" + reservation_preference = "RESERVATIONS_FIRST" + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + + instance_requirements { + vcpu_count { + min = 1 + } + memory_mib { + min = 1 + } + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_reservationsWithoutInstanceRequirements(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_preference = "RESERVATIONS_ONLY" + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_capacityOptionTypeChanged(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + propagate_tags = "NONE" + + instance_launch_template { + capacity_option_type = "ON_DEMAND" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_groupArnOnly(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ec2_capacity_reservation" "test" { + instance_type = "c5.large" + instance_platform = "Linux/UNIX" + availability_zone = data.aws_availability_zones.available.names[0] + instance_count = 1 + + tags = { + Name = %[1]q + CapacityReservationGroup = %[1]q + } +} + +resource "aws_resourcegroups_group" "test" { + name = %[1]q + + resource_query { + query = jsonencode({ + ResourceTypeFilters = ["AWS::EC2::CapacityReservation"] + TagFilters = [{ + Key = "CapacityReservationGroup" + Values = [%[1]q] + }] + }) + } +} + +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + propagate_tags = "NONE" + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_group_arn = aws_resourcegroups_group.test.arn + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + + instance_requirements { + vcpu_count { + min = 1 + } + memory_mib { + min = 1 + } + allowed_instance_types = ["c5.large", "c5a.large", "c5ad.large"] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_excludedPreference(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + propagate_tags = "NONE" + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + capacity_reservations { + reservation_preference = "RESERVATIONS_EXCLUDED" + } + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} + +func testAccCapacityProviderConfig_managedInstancesProvider_onDemandExplicit(rName string) string { + return acctest.ConfigCompose(testAccCapacityProviderConfig_managedInstancesProvider_base(rName), fmt.Sprintf(` +resource "aws_ecs_capacity_provider" "test" { + name = %[1]q + cluster = aws_ecs_cluster.test.name + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.test.arn + propagate_tags = "NONE" + + instance_launch_template { + capacity_option_type = "ON_DEMAND" + ec2_instance_profile_arn = aws_iam_instance_profile.test.arn + + network_configuration { + subnets = aws_subnet.test[*].id + security_groups = [aws_security_group.test.id] + } + } + } +} +`, rName)) +} diff --git a/website/docs/r/ecs_capacity_provider.html.markdown b/website/docs/r/ecs_capacity_provider.html.markdown index 5912991b15a7..01b9894c98f9 100644 --- a/website/docs/r/ecs_capacity_provider.html.markdown +++ b/website/docs/r/ecs_capacity_provider.html.markdown @@ -89,6 +89,71 @@ resource "aws_ecs_capacity_provider" "example" { } ``` +### Managed Instances Provider with Capacity Reservations (ODCR) + +```terraform +resource "aws_ec2_capacity_reservation" "example" { + instance_type = "c5.large" + instance_platform = "Linux/UNIX" + availability_zone = "us-west-2a" + instance_count = 2 + + tags = { + CapacityReservationGroup = "example" + } +} + +resource "aws_resourcegroups_group" "example" { + name = "example-reservation-group" + + resource_query { + query = jsonencode({ + ResourceTypeFilters = ["AWS::EC2::CapacityReservation"] + TagFilters = [{ + Key = "CapacityReservationGroup" + Values = ["example"] + }] + }) + } +} + +resource "aws_ecs_capacity_provider" "example" { + name = "example" + cluster = "my-cluster" + + managed_instances_provider { + infrastructure_role_arn = aws_iam_role.ecs_infrastructure.arn + + instance_launch_template { + capacity_option_type = "RESERVED" + ec2_instance_profile_arn = aws_iam_instance_profile.ecs_instance.arn + + capacity_reservations { + reservation_group_arn = aws_resourcegroups_group.example.arn + reservation_preference = "RESERVATIONS_ONLY" + } + + network_configuration { + subnets = [aws_subnet.example.id] + security_groups = [aws_security_group.example.id] + } + + instance_requirements { + memory_mib { + min = 1 + } + + vcpu_count { + min = 1 + } + + allowed_instance_types = ["c5.large", "c5a.large", "c5ad.large"] + } + } + } +} +``` + ## Argument Reference This resource supports the following arguments: @@ -126,7 +191,8 @@ This resource supports the following arguments: ### `instance_launch_template` -* `capacity_option_type` - (Optional) The purchasing option for the EC2 instances used in the capacity provider. Determines whether to use On-Demand or Spot instances. Valid values are `ON_DEMAND` and `SPOT`. Defaults to `ON_DEMAND` when not specified. Changing this value will trigger replacement of the capacity provider. For more information, see [Amazon EC2 billing and purchasing options](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-purchasing-options.html) in the Amazon EC2 User Guide. +* `capacity_option_type` - (Optional) The capacity option type for the instances. Determines the EC2 purchasing model used by the capacity provider. Valid values are `ON_DEMAND`, `SPOT`, and `RESERVED`. When set to `RESERVED`, the `capacity_reservations` block must also be specified. Cannot be changed after creation. +* `capacity_reservations` - (Optional) Configuration block for capacity reservation settings. Can only be set when `capacity_option_type` is `RESERVED`. Detailed below. * `ec2_instance_profile_arn` - (Required) The Amazon Resource Name (ARN) of the instance profile that Amazon ECS applies to Amazon ECS Managed Instances. This instance profile must include the necessary permissions for your tasks to access AWS services and resources. For more information, see [Amazon ECS instance profile for Managed Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html) in the Amazon ECS Developer Guide. * `instance_requirements` - (Optional) The instance requirements. You can specify the instance types and instance requirements such as vCPU count, memory, network performance, and accelerator specifications. Amazon ECS automatically selects the instances that match the specified criteria. Detailed below. * `monitoring` - (Optional) CloudWatch provides two categories of monitoring: basic monitoring and detailed monitoring. By default, your managed instance is configured for basic monitoring. You can optionally enable detailed monitoring to help you more quickly identify and act on operational issues. You can enable or turn off detailed monitoring at launch or when the managed instance is running or stopped. For more information, see [Detailed monitoring for Amazon ECS Managed Instances](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cloudwatch-metrics.html) in the Amazon ECS Developer Guide. Valid values are `BASIC` and `DETAILED`. @@ -142,6 +208,11 @@ This resource supports the following arguments: * `storage_size_gib` - (Required) The size of the tasks volume in GiB. Must be at least 1. +### `capacity_reservations` + +* `reservation_group_arn` - (Optional) The ARN of the Capacity Reservation resource group to target. Can only be specified when `reservation_preference` is `RESERVATIONS_ONLY` or not set. +* `reservation_preference` - (Optional) The preference for using capacity reservations. Valid values are `RESERVATIONS_ONLY` (only launch into reserved capacity), `RESERVATIONS_FIRST` (prefer reserved capacity, fall back to on-demand), and `RESERVATIONS_EXCLUDED` (never use reserved capacity). When `reservation_preference` is `RESERVATIONS_ONLY` or `RESERVATIONS_FIRST`, `instance_requirements` must also be specified. + ### `instance_requirements` * `accelerator_count` - (Optional) The minimum and maximum number of accelerators for the instance types. This is used when you need instances with specific numbers of GPUs or other accelerators.