diff --git a/aws/provider.go b/aws/provider.go index 8988be2beb4..23c156f5bfc 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -604,6 +604,7 @@ func Provider() *schema.Provider { "aws_elb_attachment": resourceAwsElbAttachment(), "aws_emr_cluster": resourceAwsEMRCluster(), "aws_emr_instance_group": resourceAwsEMRInstanceGroup(), + "aws_emr_instance_fleet": resourceAwsEMRInstanceFleet(), "aws_emr_managed_scaling_policy": resourceAwsEMRManagedScalingPolicy(), "aws_emr_security_configuration": resourceAwsEMRSecurityConfiguration(), "aws_flow_log": resourceAwsFlowLog(), diff --git a/aws/resource_aws_emr_instance_fleet.go b/aws/resource_aws_emr_instance_fleet.go new file mode 100644 index 00000000000..117fd952c21 --- /dev/null +++ b/aws/resource_aws_emr_instance_fleet.go @@ -0,0 +1,364 @@ +package aws + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/emr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceAwsEMRInstanceFleet() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsEMRInstanceFleetCreate, + Read: resourceAwsEMRInstanceFleetRead, + Update: resourceAwsEMRInstanceFleetUpdate, + Delete: resourceAwsEMRInstanceFleetDelete, + Importer: &schema.ResourceImporter{ + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + idParts := strings.Split(d.Id(), "/") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return nil, fmt.Errorf("Unexpected format of ID (%q), expected cluster-id/fleet-id", d.Id()) + } + clusterID := idParts[0] + resourceID := idParts[1] + d.Set("cluster_id", clusterID) + d.SetId(resourceID) + return []*schema.ResourceData{d}, nil + }, + }, + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "instance_type_configs": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bid_price": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "bid_price_as_percentage_of_on_demand_price": { + Type: schema.TypeFloat, + Optional: true, + ForceNew: true, + Default: 100, + }, + "configurations": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "classification": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "properties": { + Type: schema.TypeMap, + Optional: true, + ForceNew: true, + Elem: schema.TypeString, + }, + }, + }, + }, + "ebs_config": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "iops": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "size": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAwsEmrEbsVolumeType(), + }, + "volumes_per_instance": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 1, + }, + }, + }, + Set: resourceAwsEMRClusterEBSConfigHash, + }, + "instance_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "weighted_capacity": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 1, + }, + }, + }, + Set: resourceAwsEMRInstanceTypeConfigHash, + }, + "launch_specifications": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "on_demand_specification": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allocation_strategy": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(emr.OnDemandProvisioningAllocationStrategy_Values(), false), + }, + }, + }, + }, + "spot_specification": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allocation_strategy": { + Type: schema.TypeString, + ForceNew: true, + Required: true, + ValidateFunc: validation.StringInSlice(emr.SpotProvisioningAllocationStrategy_Values(), false), + }, + "block_duration_minutes": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 0, + }, + "timeout_action": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(emr.SpotProvisioningTimeoutAction_Values(), false), + }, + "timeout_duration_minutes": { + Type: schema.TypeInt, + ForceNew: true, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "target_on_demand_capacity": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "target_spot_capacity": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, + "provisioned_on_demand_capacity": { + Type: schema.TypeInt, + Computed: true, + }, + "provisioned_spot_capacity": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceAwsEMRInstanceFleetCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).emrconn + + addInstanceFleetInput := &emr.AddInstanceFleetInput{ + ClusterId: aws.String(d.Get("cluster_id").(string)), + } + + taskFleet := map[string]interface{}{ + "name": d.Get("name"), + "target_on_demand_capacity": d.Get("target_on_demand_capacity"), + "target_spot_capacity": d.Get("target_spot_capacity"), + "instance_type_configs": d.Get("instance_type_configs"), + "launch_specifications": d.Get("launch_specifications"), + } + addInstanceFleetInput.InstanceFleet = readInstanceFleetConfig(taskFleet, emr.InstanceFleetTypeTask) + + log.Printf("[DEBUG] Creating EMR instance fleet params: %s", addInstanceFleetInput) + resp, err := conn.AddInstanceFleet(addInstanceFleetInput) + if err != nil { + return fmt.Errorf("error adding EMR Instance Fleet: %w", err) + } + + log.Printf("[DEBUG] Created EMR instance fleet finished: %#v", resp) + if resp == nil { + return fmt.Errorf("error creating instance fleet: no instance fleet returned") + } + d.SetId(*resp.InstanceFleetId) + + return nil +} + +func resourceAwsEMRInstanceFleetRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).emrconn + instanceFleets, err := fetchAllEMRInstanceFleets(conn, d.Get("cluster_id").(string)) + if err != nil { + log.Printf("[DEBUG] EMR doesn't have any Instance Fleet ") + d.SetId("") + return nil + } + + fleet := findInstanceFleetById(instanceFleets, d.Id()) + if fleet == nil { + log.Printf("[DEBUG] EMR Instance Fleet (%s) not found, removing", d.Id()) + d.SetId("") + return nil + } + + if err := d.Set("instance_type_configs", flatteninstanceTypeConfigs(fleet.InstanceTypeSpecifications)); err != nil { + return fmt.Errorf("error setting instance_type_configs: %w", err) + } + + if err := d.Set("launch_specifications", flattenLaunchSpecifications(fleet.LaunchSpecifications)); err != nil { + return fmt.Errorf("error setting launch_specifications: %w", err) + } + d.Set("name", fleet.Name) + d.Set("provisioned_on_demand_capacity", fleet.ProvisionedOnDemandCapacity) + d.Set("provisioned_spot_capacity", fleet.ProvisionedSpotCapacity) + d.Set("target_on_demand_capacity", fleet.TargetOnDemandCapacity) + d.Set("target_spot_capacity", fleet.TargetSpotCapacity) + return nil +} + +func findInstanceFleetById(instanceFleets []*emr.InstanceFleet, fleetId string) *emr.InstanceFleet { + for _, fleet := range instanceFleets { + if fleet != nil && aws.StringValue(fleet.Id) == fleetId { + return fleet + } + } + return nil +} + +func resourceAwsEMRInstanceFleetUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).emrconn + + log.Printf("[DEBUG] Modify EMR task fleet") + + modifyConfig := &emr.InstanceFleetModifyConfig{ + InstanceFleetId: aws.String(d.Id()), + TargetOnDemandCapacity: aws.Int64(int64(d.Get("target_on_demand_capacity").(int))), + TargetSpotCapacity: aws.Int64(int64(d.Get("target_spot_capacity").(int))), + } + + modifyInstanceFleetInput := &emr.ModifyInstanceFleetInput{ + ClusterId: aws.String(d.Get("cluster_id").(string)), + InstanceFleet: modifyConfig, + } + + _, err := conn.ModifyInstanceFleet(modifyInstanceFleetInput) + if err != nil { + return fmt.Errorf("error modifying EMR Instance Fleet (%s): %w", d.Id(), err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{emr.InstanceFleetStateProvisioning, emr.InstanceFleetStateBootstrapping, emr.InstanceFleetStateResizing}, + Target: []string{emr.InstanceFleetStateRunning}, + Refresh: instanceFleetStateRefresh(conn, d.Get("cluster_id").(string), d.Id()), + Timeout: 75 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 30 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for instance (%s) to terminate: %s", d.Id(), err) + } + + return resourceAwsEMRInstanceFleetRead(d, meta) +} + +func instanceFleetStateRefresh(conn *emr.EMR, clusterID, ifID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + + instanceFleets, err := fetchAllEMRInstanceFleets(conn, clusterID) + if err != nil { + return nil, "Not Found", err + } + + fleet := findInstanceFleetById(instanceFleets, ifID) + if fleet == nil { + return nil, "Not Found", err + } + + if fleet.Status == nil || fleet.Status.State == nil { + log.Printf("[WARN] ERM Instance Fleet found, but without state") + return nil, "Undefined", fmt.Errorf("undefined EMR Cluster Instance Fleet state") + } + + return fleet, *fleet.Status.State, nil + } +} + +func resourceAwsEMRInstanceFleetDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[WARN] AWS EMR Instance Fleet does not support DELETE; resizing cluster to zero before removing from state") + conn := meta.(*AWSClient).emrconn + + clusterId := d.Get("cluster_id").(string) + + modifyInstanceFleetInput := &emr.ModifyInstanceFleetInput{ + ClusterId: aws.String(clusterId), + InstanceFleet: &emr.InstanceFleetModifyConfig{ + InstanceFleetId: aws.String(d.Id()), + TargetOnDemandCapacity: aws.Int64(0), + TargetSpotCapacity: aws.Int64(0), + }, + } + + _, err := conn.ModifyInstanceFleet(modifyInstanceFleetInput) + if err != nil { + return fmt.Errorf("error deleting/modifying EMR Instance Fleet (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_emr_instance_fleet_test.go b/aws/resource_aws_emr_instance_fleet_test.go new file mode 100644 index 00000000000..8086c88a49d --- /dev/null +++ b/aws/resource_aws_emr_instance_fleet_test.go @@ -0,0 +1,524 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/emr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAWSEMRInstanceFleet_basic(t *testing.T) { + var fleet emr.InstanceFleet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_emr_instance_fleet.task" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEmrInstanceFleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEmrInstanceFleetConfig(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_on_demand_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "target_spot_capacity", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSEMRInstanceFleetResourceImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEMRInstanceFleet_zero_count(t *testing.T) { + var fleet emr.InstanceFleet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_emr_instance_fleet.task" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEmrInstanceFleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEmrInstanceFleetConfig(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_on_demand_capacity", "1"), + resource.TestCheckResourceAttr(resourceName, "target_spot_capacity", "0"), + ), + }, + { + Config: testAccAWSEmrInstanceFleetConfigZeroCount(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_on_demand_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "target_spot_capacity", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSEMRInstanceFleetResourceImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEMRInstanceFleet_ebsBasic(t *testing.T) { + var fleet emr.InstanceFleet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_emr_instance_fleet.task" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEmrInstanceFleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEmrInstanceFleetConfigEbsBasic(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_on_demand_capacity", "0"), + resource.TestCheckResourceAttr(resourceName, "target_spot_capacity", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSEMRInstanceFleetResourceImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEMRInstanceFleet_full(t *testing.T) { + var fleet emr.InstanceFleet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_emr_instance_fleet.task" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEmrInstanceFleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEmrInstanceFleetConfigFull(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "2"), + resource.TestCheckResourceAttr(resourceName, "target_on_demand_capacity", "2"), + resource.TestCheckResourceAttr(resourceName, "target_spot_capacity", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccAWSEMRInstanceFleetResourceImportStateIdFunc(resourceName), + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEMRInstanceFleet_disappears(t *testing.T) { + var fleet emr.InstanceFleet + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_emr_instance_fleet.task" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSEmrInstanceFleetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSEmrInstanceFleetConfig(rName), + Check: resource.ComposeTestCheckFunc(testAccCheckAWSEmrInstanceFleetExists(resourceName, &fleet), + resource.TestCheckResourceAttr(resourceName, "instance_type_configs.#", "1"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAWSEmrInstanceFleetDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).emrconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_emr_cluster" { + continue + } + + params := &emr.DescribeClusterInput{ + ClusterId: aws.String(rs.Primary.ID), + } + + describe, err := conn.DescribeCluster(params) + + if err == nil { + if describe.Cluster != nil && + *describe.Cluster.Status.State == "WAITING" { + return fmt.Errorf("EMR Cluster still exists") + } + } + + providerErr, ok := err.(awserr.Error) + if !ok { + return err + } + + log.Printf("[ERROR] %v", providerErr) + } + + return nil +} + +func testAccCheckAWSEmrInstanceFleetExists(n string, v *emr.InstanceFleet) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No task fleet id set") + } + meta := testAccProvider.Meta() + conn := meta.(*AWSClient).emrconn + instanceFleets, err := fetchAllEMRInstanceFleets(conn, rs.Primary.Attributes["cluster_id"]) + if err != nil { + return fmt.Errorf("EMR error: %v", err) + } + + fleet := findInstanceFleetById(instanceFleets, rs.Primary.ID) + if fleet == nil { + return fmt.Errorf("No match found for (%s)", n) + } + v = fleet + return nil + } +} + +func testAccAWSEMRInstanceFleetResourceImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s/%s", rs.Primary.Attributes["cluster_id"], rs.Primary.ID), nil + } +} + +const testAccAWSEmrInstanceFleetBase = ` +data "aws_availability_zones" "available" { + # Many instance types are not available in this availability zone + exclude_zone_ids = ["usw2-az4"] + state = "available" + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + tags = { + Name = "tf-acc-test-emr-cluster" + } +} +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + tags = { + Name = "tf-acc-test-emr-cluster" + } +} +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id + ingress { + from_port = 0 + protocol = "-1" + self = true + to_port = 0 + } + egress { + cidr_blocks = ["0.0.0.0/0"] + from_port = 0 + protocol = "-1" + to_port = 0 + } + tags = { + Name = "tf-acc-test-emr-cluster" + } + # EMR will modify ingress rules + lifecycle { + ignore_changes = [ingress] + } +} +resource "aws_subnet" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + cidr_block = "10.0.0.0/24" + map_public_ip_on_launch = false + vpc_id = aws_vpc.test.id + tags = { + Name = "tf-acc-test-emr-cluster" + } +} +resource "aws_route_table" "test" { + vpc_id = aws_vpc.test.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.test.id + } +} +resource "aws_route_table_association" "test" { + route_table_id = aws_route_table.test.id + subnet_id = aws_subnet.test.id +} +resource "aws_iam_role" "emr_service" { + name = "%[1]s_default_role" + assume_role_policy = < **NOTE:** At this time, Instance Fleets cannot be destroyed through the API nor +web interface. Instance Fleets are destroyed when the EMR Cluster is destroyed. +Terraform will resize any Instance Fleet to zero when destroying the resource. + +## Example Usage + +```hcl +resource "aws_emr_instance_fleet" "task" { + cluster_id = aws_emr_cluster.cluster.id + instance_type_configs { + bid_price_as_percentage_of_on_demand_price = 100 + ebs_config { + size = 100 + type = "gp2" + volumes_per_instance = 1 + } + instance_type = "m4.xlarge" + weighted_capacity = 1 + } + instance_type_configs { + bid_price_as_percentage_of_on_demand_price = 100 + ebs_config { + size = 100 + type = "gp2" + volumes_per_instance = 1 + } + instance_type = "m4.2xlarge" + weighted_capacity = 2 + } + launch_specifications { + spot_specification { + allocation_strategy = "capacity-optimized" + block_duration_minutes = 0 + timeout_action = "TERMINATE_CLUSTER" + timeout_duration_minutes = 10 + } + } + name = "task fleet" + target_on_demand_capacity = 1 + target_spot_capacity = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cluster_id` - (Required) ID of the EMR Cluster to attach to. Changing this forces a new resource to be created. +* `instance_type_configs` - (Optional) Configuration block for instance fleet +* `launch_specifications` - (Optional) Configuration block for launch specification +* `target_on_demand_capacity` - (Optional) The target capacity of On-Demand units for the instance fleet, which determines how many On-Demand instances to provision. +* `target_spot_capacity` - (Optional) The target capacity of Spot units for the instance fleet, which determines how many Spot instances to provision. +* `name` - (Optional) Friendly name given to the instance fleet. + +## instance_type_configs Configuration Block + +* `bid_price` - (Optional) The bid price for each EC2 Spot instance type as defined by `instance_type`. Expressed in USD. If neither `bid_price` nor `bid_price_as_percentage_of_on_demand_price` is provided, `bid_price_as_percentage_of_on_demand_price` defaults to 100%. +* `bid_price_as_percentage_of_on_demand_price` - (Optional) The bid price, as a percentage of On-Demand price, for each EC2 Spot instance as defined by `instance_type`. Expressed as a number (for example, 20 specifies 20%). If neither `bid_price` nor `bid_price_as_percentage_of_on_demand_price` is provided, `bid_price_as_percentage_of_on_demand_price` defaults to 100%. +* `configurations` - (Optional) A configuration classification that applies when provisioning cluster instances, which can include configurations for applications and software that run on the cluster. List of `configuration` blocks. +* `ebs_config` - (Optional) Configuration block(s) for EBS volumes attached to each instance in the instance group. Detailed below. +* `instance_type` - (Required) An EC2 instance type, such as m4.xlarge. +* `weighted_capacity` - (Optional) The number of units that a provisioned instance of this type provides toward fulfilling the target capacities defined in `aws_emr_instance_fleet`. + +## configurations Configuration Block + +A configuration classification that applies when provisioning cluster instances, which can include configurations for applications and software that run on the cluster. See [Configuring Applications](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-configure-apps.html). + +* `classification` - (Optional) The classification within a configuration. +* `properties` - (Optional) A map of properties specified within a configuration classification + +## ebs_config + +Attributes for the EBS volumes attached to each EC2 instance in the `master_instance_group` and `core_instance_group` configuration blocks: + +* `size` - (Required) The volume size, in gibibytes (GiB). +* `type` - (Required) The volume type. Valid options are `gp2`, `io1`, `standard` and `st1`. See [EBS Volume Types](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html). +* `iops` - (Optional) The number of I/O operations per second (IOPS) that the volume supports +* `volumes_per_instance` - (Optional) The number of EBS volumes with this configuration to attach to each EC2 instance in the instance group (default is 1) + +## launch_specifications Configuration Block + +* `on_demand_specification` - (Optional) Configuration block for on demand instances launch specifications +* `spot_specification` - (Optional) Configuration block for spot instances launch specifications + +## on_demand_specification Configuration Block + +The launch specification for On-Demand instances in the instance fleet, which determines the allocation strategy. +The instance fleet configuration is available only in Amazon EMR versions 4.8.0 and later, excluding 5.0.x versions. On-Demand instances allocation strategy is available in Amazon EMR version 5.12.1 and later. + +* `allocation_strategy` - (Required) Specifies the strategy to use in launching On-Demand instance fleets. Currently, the only option is `lowest-price` (the default), which launches the lowest price first. + +## spot_specification Configuration Block + +The launch specification for Spot instances in the fleet, which determines the defined duration, provisioning timeout behavior, and allocation strategy. + +* `allocation_strategy` - (Required) Specifies the strategy to use in launching Spot instance fleets. Currently, the only option is `capacity-optimized` (the default), which launches instances from Spot instance pools with optimal capacity for the number of instances that are launching. +* `block_duration_minutes` - (Optional) The defined duration for Spot instances (also known as Spot blocks) in minutes. When specified, the Spot instance does not terminate before the defined duration expires, and defined duration pricing for Spot instances applies. Valid values are 60, 120, 180, 240, 300, or 360. The duration period starts as soon as a Spot instance receives its instance ID. At the end of the duration, Amazon EC2 marks the Spot instance for termination and provides a Spot instance termination notice, which gives the instance a two-minute warning before it terminates. +* `timeout_action` - (Required) The action to take when TargetSpotCapacity has not been fulfilled when the TimeoutDurationMinutes has expired; that is, when all Spot instances could not be provisioned within the Spot provisioning timeout. Valid values are `TERMINATE_CLUSTER` and `SWITCH_TO_ON_DEMAND`. SWITCH_TO_ON_DEMAND specifies that if no Spot instances are available, On-Demand Instances should be provisioned to fulfill any remaining Spot capacity. +* `timeout_duration_minutes` - (Required) The spot provisioning timeout period in minutes. If Spot instances are not provisioned within this time period, the TimeOutAction is taken. Minimum value is 5 and maximum value is 1440. The timeout applies only during initial provisioning, when the cluster is first created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The unique identifier of the instance fleet. + +* `provisioned_on_demand_capacity` The number of On-Demand units that have been provisioned for the instance +fleet to fulfill TargetOnDemandCapacity. This provisioned capacity might be less than or greater than TargetOnDemandCapacity. + +* `provisioned_spot_capacity` The number of Spot units that have been provisioned for this instance fleet +to fulfill TargetSpotCapacity. This provisioned capacity might be less than or greater than TargetSpotCapacity. + +* `status` The current status of the instance fleet. + +## Import + +EMR Instance Fleet can be imported with the EMR Cluster identifier and Instance Fleet identifier separated by a forward slash (`/`), e.g. + +```console +$ terraform import aws_emr_instance_fleet.example j-123456ABCDEF/if-15EK4O09RZLNR +```