diff --git a/.changelog/12370.txt b/.changelog/12370.txt new file mode 100644 index 000000000000..9a39c9e4cb6f --- /dev/null +++ b/.changelog/12370.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_rds_cluster_role_association +``` + +```release-note:enhancement +aws_rds_cluster: Set `iam_roles` as Computed to prevent drift when the `aws_rds_cluster_role_association` resource is used +``` \ No newline at end of file diff --git a/aws/internal/service/rds/enum.go b/aws/internal/service/rds/enum.go new file mode 100644 index 000000000000..84531413bf2d --- /dev/null +++ b/aws/internal/service/rds/enum.go @@ -0,0 +1,7 @@ +package rds + +const ( + DBClusterRoleStatusActive = "ACTIVE" + DBClusterRoleStatusDeleted = "DELETED" + DBClusterRoleStatusPending = "PENDING" +) diff --git a/aws/internal/service/rds/finder/finder.go b/aws/internal/service/rds/finder/finder.go index 8ef227f70998..a401d39b5b22 100644 --- a/aws/internal/service/rds/finder/finder.go +++ b/aws/internal/service/rds/finder/finder.go @@ -3,6 +3,8 @@ package finder import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" ) @@ -63,3 +65,58 @@ func DBProxyEndpoint(conn *rds.RDS, id string) (*rds.DBProxyEndpoint, error) { return dbProxyEndpoint, err } + +func DBClusterRoleByDBClusterIDAndRoleARN(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { + dbCluster, err := DBClusterByID(conn, dbClusterID) + + if err != nil { + return nil, err + } + + for _, associatedRole := range dbCluster.AssociatedRoles { + if aws.StringValue(associatedRole.RoleArn) == roleARN { + if status := aws.StringValue(associatedRole.Status); status == tfrds.DBClusterRoleStatusDeleted { + return nil, &resource.NotFoundError{ + Message: status, + } + } + + return associatedRole, nil + } + } + + return nil, &resource.NotFoundError{} +} + +func DBClusterByID(conn *rds.RDS, id string) (*rds.DBCluster, error) { + input := &rds.DescribeDBClustersInput{ + DBClusterIdentifier: aws.String(id), + } + + output, err := conn.DescribeDBClusters(input) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if output == nil || len(output.DBClusters) == 0 || output.DBClusters[0] == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + dbCluster := output.DBClusters[0] + + // Eventual consistency check. + if aws.StringValue(dbCluster.DBClusterIdentifier) != id { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return dbCluster, nil +} diff --git a/aws/internal/service/rds/id.go b/aws/internal/service/rds/id.go index f8a8250f33a4..74e74c85784b 100644 --- a/aws/internal/service/rds/id.go +++ b/aws/internal/service/rds/id.go @@ -12,3 +12,22 @@ func ResourceAwsDbProxyEndpointParseID(id string) (string, string, error) { } return idParts[0], idParts[1], nil } + +const clusterRoleAssociationResourceIDSeparator = "," + +func ClusterRoleAssociationCreateResourceID(dbClusterID, roleARN string) string { + parts := []string{dbClusterID, roleARN} + id := strings.Join(parts, clusterRoleAssociationResourceIDSeparator) + + return id +} + +func ClusterRoleAssociationParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, clusterRoleAssociationResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected DBCLUSTERID%[2]sROLEARN", id, clusterRoleAssociationResourceIDSeparator) +} diff --git a/aws/internal/service/rds/waiter/status.go b/aws/internal/service/rds/waiter/status.go index 59fa5e5b5e41..870f1b58e8e5 100644 --- a/aws/internal/service/rds/waiter/status.go +++ b/aws/internal/service/rds/waiter/status.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -58,3 +59,19 @@ func DBProxyEndpointStatus(conn *rds.RDS, id string) resource.StateRefreshFunc { return output, aws.StringValue(output.Status), nil } } + +func DBClusterRoleStatus(conn *rds.RDS, dbClusterID, roleARN string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/rds/waiter/waiter.go b/aws/internal/service/rds/waiter/waiter.go index 2e80027be93b..d3ad5da52d03 100644 --- a/aws/internal/service/rds/waiter/waiter.go +++ b/aws/internal/service/rds/waiter/waiter.go @@ -5,12 +5,16 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" ) const ( // Maximum amount of time to wait for an EventSubscription to return Deleted EventSubscriptionDeletedTimeout = 10 * time.Minute RdsClusterInitiateUpgradeTimeout = 5 * time.Minute + + DBClusterRoleAssociationCreatedTimeout = 5 * time.Minute + DBClusterRoleAssociationDeletedTimeout = 5 * time.Minute ) // EventSubscriptionDeleted waits for a EventSubscription to return Deleted @@ -69,3 +73,37 @@ func DBProxyEndpointDeleted(conn *rds.RDS, id string, timeout time.Duration) (*r return nil, err } + +func DBClusterRoleAssociationCreated(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.DBClusterRoleStatusPending}, + Target: []string{tfrds.DBClusterRoleStatusActive}, + Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN), + Timeout: DBClusterRoleAssociationCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBClusterRole); ok { + return output, err + } + + return nil, err +} + +func DBClusterRoleAssociationDeleted(conn *rds.RDS, dbClusterID, roleARN string) (*rds.DBClusterRole, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{tfrds.DBClusterRoleStatusActive, tfrds.DBClusterRoleStatusPending}, + Target: []string{}, + Refresh: DBClusterRoleStatus(conn, dbClusterID, roleARN), + Timeout: DBClusterRoleAssociationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBClusterRole); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 1a20232f3001..284c667ae22c 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -952,6 +952,7 @@ func Provider() *schema.Provider { "aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), "aws_rds_cluster_parameter_group": resourceAwsRDSClusterParameterGroup(), + "aws_rds_cluster_role_association": resourceAwsRDSClusterRoleAssociation(), "aws_rds_global_cluster": resourceAwsRDSGlobalCluster(), "aws_redshift_cluster": resourceAwsRedshiftCluster(), "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), diff --git a/aws/resource_aws_rds_cluster.go b/aws/resource_aws_rds_cluster.go index bb54eb5533bb..7fbac5d135d1 100644 --- a/aws/resource_aws_rds_cluster.go +++ b/aws/resource_aws_rds_cluster.go @@ -415,6 +415,7 @@ func resourceAwsRDSCluster() *schema.Resource { "iam_roles": { Type: schema.TypeSet, Optional: true, + Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, diff --git a/aws/resource_aws_rds_cluster_role_association.go b/aws/resource_aws_rds_cluster_role_association.go new file mode 100644 index 000000000000..a73fa2e7a179 --- /dev/null +++ b/aws/resource_aws_rds_cluster_role_association.go @@ -0,0 +1,138 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsRDSClusterRoleAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRDSClusterRoleAssociationCreate, + Read: resourceAwsRDSClusterRoleAssociationRead, + Delete: resourceAwsRDSClusterRoleAssociationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "db_cluster_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "feature_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + }, + } +} + +func resourceAwsRDSClusterRoleAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + dbClusterID := d.Get("db_cluster_identifier").(string) + roleARN := d.Get("role_arn").(string) + input := &rds.AddRoleToDBClusterInput{ + DBClusterIdentifier: aws.String(dbClusterID), + FeatureName: aws.String(d.Get("feature_name").(string)), + RoleArn: aws.String(roleARN), + } + + log.Printf("[DEBUG] Creating RDS DB Cluster IAM Role Association: %s", input) + _, err := conn.AddRoleToDBCluster(input) + + if err != nil { + return fmt.Errorf("error creating RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err) + } + + d.SetId(tfrds.ClusterRoleAssociationCreateResourceID(dbClusterID, roleARN)) + + _, err = waiter.DBClusterRoleAssociationCreated(conn, dbClusterID, roleARN) + + if err != nil { + return fmt.Errorf("error waiting for RDS DB Cluster (%s) IAM Role (%s) Association to create: %w", dbClusterID, roleARN, err) + } + + return resourceAwsRDSClusterRoleAssociationRead(d, meta) +} + +func resourceAwsRDSClusterRoleAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing RDS DB Cluster IAM Role Association ID: %s", err) + } + + output, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] RDS DB Cluster (%s) IAM Role (%s) Association not found, removing from state", dbClusterID, roleARN) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err) + } + + d.Set("db_cluster_identifier", dbClusterID) + d.Set("feature_name", output.FeatureName) + d.Set("role_arn", output.RoleArn) + + return nil +} + +func resourceAwsRDSClusterRoleAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing RDS DB Cluster IAM Role Association ID: %s", err) + } + + input := &rds.RemoveRoleFromDBClusterInput{ + DBClusterIdentifier: aws.String(dbClusterID), + FeatureName: aws.String(d.Get("feature_name").(string)), + RoleArn: aws.String(roleARN), + } + + log.Printf("[DEBUG] Deleting RDS DB Cluster IAM Role Association: %s", d.Id()) + _, err = conn.RemoveRoleFromDBCluster(input) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterNotFoundFault) || tfawserr.ErrCodeEquals(err, rds.ErrCodeDBClusterRoleNotFoundFault) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting RDS DB Cluster (%s) IAM Role (%s) Association: %w", dbClusterID, roleARN, err) + } + + _, err = waiter.DBClusterRoleAssociationDeleted(conn, dbClusterID, roleARN) + + if err != nil { + return fmt.Errorf("error waiting for RDS DB Cluster (%s) IAM Role (%s) Association to delete: %w", dbClusterID, roleARN, err) + } + + return nil +} diff --git a/aws/resource_aws_rds_cluster_role_association_test.go b/aws/resource_aws_rds_cluster_role_association_test.go new file mode 100644 index 000000000000..71602b301509 --- /dev/null +++ b/aws/resource_aws_rds_cluster_role_association_test.go @@ -0,0 +1,213 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/rds" + "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" + tfrds "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func TestAccAWSRDSClusterRoleAssociation_basic(t *testing.T) { + var dbClusterRole rds.DBClusterRole + rName := acctest.RandomWithPrefix("tf-acc-test") + dbClusterResourceName := "aws_rds_cluster.test" + iamRoleResourceName := "aws_iam_role.test" + resourceName := "aws_rds_cluster_role_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRDSClusterRoleAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterRoleAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRDSClusterRoleAssociationExists(resourceName, &dbClusterRole), + resource.TestCheckResourceAttrPair(resourceName, "db_cluster_identifier", dbClusterResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "feature_name", "s3Import"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", iamRoleResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRDSClusterRoleAssociation_disappears(t *testing.T) { + var dbClusterRole rds.DBClusterRole + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_cluster_role_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRDSClusterRoleAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterRoleAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRDSClusterRoleAssociationExists(resourceName, &dbClusterRole), + testAccCheckResourceDisappears(testAccProvider, resourceAwsRDSClusterRoleAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSRDSClusterRoleAssociation_disappears_cluster(t *testing.T) { + var dbClusterRole rds.DBClusterRole + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_cluster_role_association.test" + clusterResourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRDSClusterRoleAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterRoleAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRDSClusterRoleAssociationExists(resourceName, &dbClusterRole), + testAccCheckResourceDisappears(testAccProvider, resourceAwsRDSCluster(), clusterResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSRDSClusterRoleAssociation_disappears_role(t *testing.T) { + var dbClusterRole rds.DBClusterRole + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_cluster_role_association.test" + roleResourceName := "aws_iam_role.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRDSClusterRoleAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterRoleAssociationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRDSClusterRoleAssociationExists(resourceName, &dbClusterRole), + testAccCheckResourceDisappears(testAccProvider, resourceAwsIamRole(), roleResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAWSRDSClusterRoleAssociationExists(resourceName string, v *rds.DBClusterRole) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + role, err := finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN) + + if err != nil { + return err + } + + *v = *role + + return nil + } +} + +func testAccCheckAWSRDSClusterRoleAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_db_cluster_role_association" { + continue + } + + dbClusterID, roleARN, err := tfrds.ClusterRoleAssociationParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.DBClusterRoleByDBClusterIDAndRoleARN(conn, dbClusterID, roleARN) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("RDS DB Cluster IAM Role Association %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSRDSClusterRoleAssociationConfig(rName string) string { + return composeConfig(testAccAvailableAZsNoOptInConfig(), fmt.Sprintf(` +data "aws_iam_policy_document" "rds_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + identifiers = ["rds.amazonaws.com"] + type = "Service" + } + } +} + +resource "aws_iam_role" "test" { + assume_role_policy = data.aws_iam_policy_document.rds_assume_role_policy.json + name = %[1]q +} + +resource "aws_rds_cluster" "test" { + cluster_identifier = %[1]q + engine = "aurora-postgresql" + availability_zones = [data.aws_availability_zones.available.names[0], data.aws_availability_zones.available.names[1], data.aws_availability_zones.available.names[2]] + database_name = "mydb" + master_username = "foo" + master_password = "foobarfoobarfoobar" + backup_retention_period = 5 + preferred_backup_window = "07:00-09:00" + skip_final_snapshot = true +} + +resource "aws_rds_cluster_role_association" "test" { + db_cluster_identifier = aws_rds_cluster.test.id + feature_name = "s3Import" + role_arn = aws_iam_role.test.arn +} +`, rName)) +} diff --git a/website/docs/r/rds_cluster.html.markdown b/website/docs/r/rds_cluster.html.markdown index 82bd7cb7d4c8..31b3548087b4 100644 --- a/website/docs/r/rds_cluster.html.markdown +++ b/website/docs/r/rds_cluster.html.markdown @@ -28,6 +28,11 @@ for more information. ~> **Note:** All arguments including the username and password will be stored in the raw state as plain-text. [Read more about sensitive data in state](https://www.terraform.io/docs/state/sensitive-data.html). +~> **NOTE on RDS Clusters and RDS Cluster Role Associations:** Terraform provides both a standalone [RDS Cluster Role Association](rds_cluster_role_association.html) - (an association between an RDS Cluster and a single IAM Role) and +an RDS Cluster resource with `iam_roles` attributes. +Use one resource or the other to associate IAM Roles and RDS Clusters. +Not doing so will cause a conflict of associations and will result in the association being overwritten. + ## Example Usage ### Aurora MySQL 2.x (MySQL 5.7) diff --git a/website/docs/r/rds_cluster_role_association.html.markdown b/website/docs/r/rds_cluster_role_association.html.markdown new file mode 100644 index 000000000000..8fbe9d16c87c --- /dev/null +++ b/website/docs/r/rds_cluster_role_association.html.markdown @@ -0,0 +1,46 @@ +--- +subcategory: "RDS" +layout: "aws" +page_title: "AWS: aws_rds_cluster_role_association" +description: |- + Manages a RDS DB Cluster association with an IAM Role. +--- + +# Resource: aws_rds_cluster_role_association + +Manages a RDS DB Cluster association with an IAM Role. Example use cases: + +* [Creating an IAM Role to Allow Amazon Aurora to Access AWS Services](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.Authorizing.IAM.CreateRole.html) +* [Importing Amazon S3 Data into an RDS PostgreSQL DB Cluster](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PostgreSQL.S3Import.html) + +## Example Usage + +```terraform +resource "aws_rds_cluster_role_association" "example" { + db_cluster_identifier = aws_rds_cluster.example.id + feature_name = "S3_INTEGRATION" + role_arn = aws_iam_role.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `db_cluster_identifier` - (Required) DB Cluster Identifier to associate with the IAM Role. +* `feature_name` - (Required) Name of the feature for association. This can be found in the AWS documentation relevant to the integration or a full list is available in the `SupportedFeatureNames` list returned by [AWS CLI rds describe-db-engine-versions](https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-engine-versions.html). +* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM Role to associate with the DB Cluster. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - DB Cluster Identifier and IAM Role ARN separated by a comma (`,`) + +## Import + +`aws_rds_cluster_role_association` can be imported using the DB Cluster Identifier and IAM Role ARN separated by a comma (`,`), e.g. + +``` +$ terraform import aws_rds_cluster_role_association.example my-db-cluster,arn:aws:iam::123456789012:role/my-role +```