github.com/minamijoyo/terraform@v0.7.8-0.20161029001309-18b3736ba44b/builtin/providers/aws/resource_aws_rds_cluster.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "regexp" 7 "strings" 8 "time" 9 10 "github.com/aws/aws-sdk-go/aws" 11 "github.com/aws/aws-sdk-go/aws/awserr" 12 "github.com/aws/aws-sdk-go/service/rds" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 ) 16 17 func resourceAwsRDSCluster() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsRDSClusterCreate, 20 Read: resourceAwsRDSClusterRead, 21 Update: resourceAwsRDSClusterUpdate, 22 Delete: resourceAwsRDSClusterDelete, 23 Importer: &schema.ResourceImporter{ 24 State: resourceAwsRdsClusterImport, 25 }, 26 27 Schema: map[string]*schema.Schema{ 28 29 "availability_zones": &schema.Schema{ 30 Type: schema.TypeSet, 31 Elem: &schema.Schema{Type: schema.TypeString}, 32 Optional: true, 33 ForceNew: true, 34 Computed: true, 35 Set: schema.HashString, 36 }, 37 38 "cluster_identifier": &schema.Schema{ 39 Type: schema.TypeString, 40 Required: true, 41 ForceNew: true, 42 ValidateFunc: validateRdsId, 43 }, 44 45 "cluster_members": &schema.Schema{ 46 Type: schema.TypeSet, 47 Elem: &schema.Schema{Type: schema.TypeString}, 48 Optional: true, 49 Computed: true, 50 Set: schema.HashString, 51 }, 52 53 "database_name": &schema.Schema{ 54 Type: schema.TypeString, 55 Optional: true, 56 Computed: true, 57 ForceNew: true, 58 }, 59 60 "db_subnet_group_name": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 ForceNew: true, 64 Computed: true, 65 }, 66 67 // TODO: remove parameter_group_name 68 // See https://github.com/hashicorp/terraform/issues/7046 69 // Likely need migration to remove from state 70 "parameter_group_name": &schema.Schema{ 71 Type: schema.TypeString, 72 Optional: true, 73 Computed: true, 74 Deprecated: "Use db_cluster_parameter_group_name instead. This attribute will be removed in a future version", 75 }, 76 77 "db_cluster_parameter_group_name": &schema.Schema{ 78 Type: schema.TypeString, 79 Optional: true, 80 Computed: true, 81 }, 82 83 "endpoint": &schema.Schema{ 84 Type: schema.TypeString, 85 Computed: true, 86 }, 87 88 "reader_endpoint": &schema.Schema{ 89 Type: schema.TypeString, 90 Computed: true, 91 }, 92 93 "engine": &schema.Schema{ 94 Type: schema.TypeString, 95 Computed: true, 96 }, 97 98 "storage_encrypted": &schema.Schema{ 99 Type: schema.TypeBool, 100 Optional: true, 101 Default: false, 102 ForceNew: true, 103 }, 104 105 "final_snapshot_identifier": &schema.Schema{ 106 Type: schema.TypeString, 107 Optional: true, 108 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 109 value := v.(string) 110 if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { 111 es = append(es, fmt.Errorf( 112 "only alphanumeric characters and hyphens allowed in %q", k)) 113 } 114 if regexp.MustCompile(`--`).MatchString(value) { 115 es = append(es, fmt.Errorf("%q cannot contain two consecutive hyphens", k)) 116 } 117 if regexp.MustCompile(`-$`).MatchString(value) { 118 es = append(es, fmt.Errorf("%q cannot end in a hyphen", k)) 119 } 120 return 121 }, 122 }, 123 124 "skip_final_snapshot": &schema.Schema{ 125 Type: schema.TypeBool, 126 Optional: true, 127 Default: true, 128 }, 129 130 "master_username": &schema.Schema{ 131 Type: schema.TypeString, 132 Computed: true, 133 Optional: true, 134 ForceNew: true, 135 }, 136 137 "master_password": &schema.Schema{ 138 Type: schema.TypeString, 139 Optional: true, 140 }, 141 142 "snapshot_identifier": &schema.Schema{ 143 Type: schema.TypeString, 144 Computed: false, 145 Optional: true, 146 Elem: &schema.Schema{Type: schema.TypeString}, 147 }, 148 149 "port": &schema.Schema{ 150 Type: schema.TypeInt, 151 Optional: true, 152 Computed: true, 153 }, 154 155 // apply_immediately is used to determine when the update modifications 156 // take place. 157 // See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html 158 "apply_immediately": &schema.Schema{ 159 Type: schema.TypeBool, 160 Optional: true, 161 Computed: true, 162 }, 163 164 "vpc_security_group_ids": &schema.Schema{ 165 Type: schema.TypeSet, 166 Optional: true, 167 Computed: true, 168 Elem: &schema.Schema{Type: schema.TypeString}, 169 Set: schema.HashString, 170 }, 171 172 "preferred_backup_window": &schema.Schema{ 173 Type: schema.TypeString, 174 Optional: true, 175 Computed: true, 176 }, 177 178 "preferred_maintenance_window": &schema.Schema{ 179 Type: schema.TypeString, 180 Optional: true, 181 Computed: true, 182 StateFunc: func(val interface{}) string { 183 if val == nil { 184 return "" 185 } 186 return strings.ToLower(val.(string)) 187 }, 188 }, 189 190 "backup_retention_period": &schema.Schema{ 191 Type: schema.TypeInt, 192 Optional: true, 193 Default: 1, 194 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 195 value := v.(int) 196 if value > 35 { 197 es = append(es, fmt.Errorf( 198 "backup retention period cannot be more than 35 days")) 199 } 200 return 201 }, 202 }, 203 204 "kms_key_id": &schema.Schema{ 205 Type: schema.TypeString, 206 Optional: true, 207 Computed: true, 208 ForceNew: true, 209 }, 210 211 "tags": tagsSchema(), 212 }, 213 } 214 } 215 216 func resourceAwsRdsClusterImport( 217 d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 218 // Neither skip_final_snapshot nor final_snapshot_identifier can be fetched 219 // from any API call, so we need to default skip_final_snapshot to true so 220 // that final_snapshot_identifier is not required 221 d.Set("skip_final_snapshot", true) 222 return []*schema.ResourceData{d}, nil 223 } 224 225 func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error { 226 conn := meta.(*AWSClient).rdsconn 227 tags := tagsFromMapRDS(d.Get("tags").(map[string]interface{})) 228 229 if _, ok := d.GetOk("snapshot_identifier"); ok { 230 opts := rds.RestoreDBClusterFromSnapshotInput{ 231 DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)), 232 SnapshotIdentifier: aws.String(d.Get("snapshot_identifier").(string)), 233 Engine: aws.String("aurora"), 234 Tags: tags, 235 } 236 237 if attr := d.Get("availability_zones").(*schema.Set); attr.Len() > 0 { 238 opts.AvailabilityZones = expandStringList(attr.List()) 239 } 240 241 if attr, ok := d.GetOk("db_subnet_group_name"); ok { 242 opts.DBSubnetGroupName = aws.String(attr.(string)) 243 } 244 245 if attr, ok := d.GetOk("database_name"); ok { 246 opts.DatabaseName = aws.String(attr.(string)) 247 } 248 249 if attr, ok := d.GetOk("option_group_name"); ok { 250 opts.OptionGroupName = aws.String(attr.(string)) 251 } 252 253 if attr, ok := d.GetOk("port"); ok { 254 opts.Port = aws.Int64(int64(attr.(int))) 255 } 256 257 var sgUpdate bool 258 if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { 259 sgUpdate = true 260 opts.VpcSecurityGroupIds = expandStringList(attr.List()) 261 } 262 263 log.Printf("[DEBUG] RDS Cluster restore from snapshot configuration: %s", opts) 264 _, err := conn.RestoreDBClusterFromSnapshot(&opts) 265 if err != nil { 266 return fmt.Errorf("Error creating RDS Cluster: %s", err) 267 } 268 269 if sgUpdate { 270 log.Printf("[INFO] RDS Cluster is restoring from snapshot with default security, but custom security should be set, will now update after snapshot is restored!") 271 272 d.SetId(d.Get("cluster_identifier").(string)) 273 274 log.Printf("[INFO] RDS Cluster Instance ID: %s", d.Id()) 275 276 log.Println("[INFO] Waiting for RDS Cluster to be available") 277 278 stateConf := &resource.StateChangeConf{ 279 Pending: []string{"creating", "backing-up", "modifying"}, 280 Target: []string{"available"}, 281 Refresh: resourceAwsRDSClusterStateRefreshFunc(d, meta), 282 Timeout: 40 * time.Minute, 283 MinTimeout: 3 * time.Second, 284 Delay: 30 * time.Second, // Wait 30 secs before starting 285 } 286 287 // Wait, catching any errors 288 _, err := stateConf.WaitForState() 289 if err != nil { 290 return err 291 } 292 293 err = resourceAwsRDSClusterInstanceUpdate(d, meta) 294 if err != nil { 295 return err 296 } 297 } 298 } else { 299 if _, ok := d.GetOk("master_password"); !ok { 300 return fmt.Errorf(`provider.aws: aws_rds_cluster: %s: "master_password": required field is not set`, d.Get("name").(string)) 301 } 302 303 if _, ok := d.GetOk("master_username"); !ok { 304 return fmt.Errorf(`provider.aws: aws_rds_cluster: %s: "master_username": required field is not set`, d.Get("name").(string)) 305 } 306 307 createOpts := &rds.CreateDBClusterInput{ 308 DBClusterIdentifier: aws.String(d.Get("cluster_identifier").(string)), 309 Engine: aws.String("aurora"), 310 MasterUserPassword: aws.String(d.Get("master_password").(string)), 311 MasterUsername: aws.String(d.Get("master_username").(string)), 312 StorageEncrypted: aws.Bool(d.Get("storage_encrypted").(bool)), 313 Tags: tags, 314 } 315 316 if v := d.Get("database_name"); v.(string) != "" { 317 createOpts.DatabaseName = aws.String(v.(string)) 318 } 319 320 if attr, ok := d.GetOk("port"); ok { 321 createOpts.Port = aws.Int64(int64(attr.(int))) 322 } 323 324 if attr, ok := d.GetOk("db_subnet_group_name"); ok { 325 createOpts.DBSubnetGroupName = aws.String(attr.(string)) 326 } 327 328 if attr, ok := d.GetOk("parameter_group_name"); ok { 329 createOpts.DBClusterParameterGroupName = aws.String(attr.(string)) 330 } 331 332 if attr, ok := d.GetOk("db_cluster_parameter_group_name"); ok { 333 createOpts.DBClusterParameterGroupName = aws.String(attr.(string)) 334 } 335 336 if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { 337 createOpts.VpcSecurityGroupIds = expandStringList(attr.List()) 338 } 339 340 if attr := d.Get("availability_zones").(*schema.Set); attr.Len() > 0 { 341 createOpts.AvailabilityZones = expandStringList(attr.List()) 342 } 343 344 if v, ok := d.GetOk("backup_retention_period"); ok { 345 createOpts.BackupRetentionPeriod = aws.Int64(int64(v.(int))) 346 } 347 348 if v, ok := d.GetOk("preferred_backup_window"); ok { 349 createOpts.PreferredBackupWindow = aws.String(v.(string)) 350 } 351 352 if v, ok := d.GetOk("preferred_maintenance_window"); ok { 353 createOpts.PreferredMaintenanceWindow = aws.String(v.(string)) 354 } 355 356 if attr, ok := d.GetOk("kms_key_id"); ok { 357 createOpts.KmsKeyId = aws.String(attr.(string)) 358 } 359 360 log.Printf("[DEBUG] RDS Cluster create options: %s", createOpts) 361 resp, err := conn.CreateDBCluster(createOpts) 362 if err != nil { 363 log.Printf("[ERROR] Error creating RDS Cluster: %s", err) 364 return err 365 } 366 367 log.Printf("[DEBUG]: RDS Cluster create response: %s", resp) 368 } 369 370 d.SetId(d.Get("cluster_identifier").(string)) 371 372 log.Printf("[INFO] RDS Cluster ID: %s", d.Id()) 373 374 log.Println( 375 "[INFO] Waiting for RDS Cluster to be available") 376 377 stateConf := &resource.StateChangeConf{ 378 Pending: []string{"creating", "backing-up", "modifying"}, 379 Target: []string{"available"}, 380 Refresh: resourceAwsRDSClusterStateRefreshFunc(d, meta), 381 Timeout: 40 * time.Minute, 382 MinTimeout: 3 * time.Second, 383 } 384 385 // Wait, catching any errors 386 _, err := stateConf.WaitForState() 387 if err != nil { 388 return fmt.Errorf("[WARN] Error waiting for RDS Cluster state to be \"available\": %s", err) 389 } 390 391 return resourceAwsRDSClusterRead(d, meta) 392 } 393 394 func resourceAwsRDSClusterRead(d *schema.ResourceData, meta interface{}) error { 395 conn := meta.(*AWSClient).rdsconn 396 397 resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{ 398 DBClusterIdentifier: aws.String(d.Id()), 399 }) 400 401 if err != nil { 402 if awsErr, ok := err.(awserr.Error); ok { 403 if "DBClusterNotFoundFault" == awsErr.Code() { 404 d.SetId("") 405 log.Printf("[DEBUG] RDS Cluster (%s) not found", d.Id()) 406 return nil 407 } 408 } 409 log.Printf("[DEBUG] Error describing RDS Cluster (%s)", d.Id()) 410 return err 411 } 412 413 var dbc *rds.DBCluster 414 for _, c := range resp.DBClusters { 415 if *c.DBClusterIdentifier == d.Id() { 416 dbc = c 417 } 418 } 419 420 if dbc == nil { 421 log.Printf("[WARN] RDS Cluster (%s) not found", d.Id()) 422 d.SetId("") 423 return nil 424 } 425 426 if err := d.Set("availability_zones", aws.StringValueSlice(dbc.AvailabilityZones)); err != nil { 427 return fmt.Errorf("[DEBUG] Error saving AvailabilityZones to state for RDS Cluster (%s): %s", d.Id(), err) 428 } 429 430 // Only set the DatabaseName if it is not nil. There is a known API bug where 431 // RDS accepts a DatabaseName but does not return it, causing a perpetual 432 // diff. 433 // See https://github.com/hashicorp/terraform/issues/4671 for backstory 434 if dbc.DatabaseName != nil { 435 d.Set("database_name", dbc.DatabaseName) 436 } 437 438 d.Set("cluster_identifier", dbc.DBClusterIdentifier) 439 d.Set("db_subnet_group_name", dbc.DBSubnetGroup) 440 d.Set("parameter_group_name", dbc.DBClusterParameterGroup) 441 d.Set("db_cluster_parameter_group_name", dbc.DBClusterParameterGroup) 442 d.Set("endpoint", dbc.Endpoint) 443 d.Set("engine", dbc.Engine) 444 d.Set("master_username", dbc.MasterUsername) 445 d.Set("port", dbc.Port) 446 d.Set("storage_encrypted", dbc.StorageEncrypted) 447 d.Set("backup_retention_period", dbc.BackupRetentionPeriod) 448 d.Set("preferred_backup_window", dbc.PreferredBackupWindow) 449 d.Set("preferred_maintenance_window", dbc.PreferredMaintenanceWindow) 450 d.Set("kms_key_id", dbc.KmsKeyId) 451 d.Set("reader_endpoint", dbc.ReaderEndpoint) 452 453 var vpcg []string 454 for _, g := range dbc.VpcSecurityGroups { 455 vpcg = append(vpcg, *g.VpcSecurityGroupId) 456 } 457 if err := d.Set("vpc_security_group_ids", vpcg); err != nil { 458 return fmt.Errorf("[DEBUG] Error saving VPC Security Group IDs to state for RDS Cluster (%s): %s", d.Id(), err) 459 } 460 461 var cm []string 462 for _, m := range dbc.DBClusterMembers { 463 cm = append(cm, *m.DBInstanceIdentifier) 464 } 465 if err := d.Set("cluster_members", cm); err != nil { 466 return fmt.Errorf("[DEBUG] Error saving RDS Cluster Members to state for RDS Cluster (%s): %s", d.Id(), err) 467 } 468 469 // Fetch and save tags 470 arn, err := buildRDSClusterARN(d.Id(), meta.(*AWSClient).partition, meta.(*AWSClient).accountid, meta.(*AWSClient).region) 471 if err != nil { 472 log.Printf("[DEBUG] Error building ARN for RDS Cluster (%s), not setting Tags", *dbc.DBClusterIdentifier) 473 } else { 474 if err := saveTagsRDS(conn, d, arn); err != nil { 475 log.Printf("[WARN] Failed to save tags for RDS Cluster (%s): %s", *dbc.DBClusterIdentifier, err) 476 } 477 } 478 479 return nil 480 } 481 482 func resourceAwsRDSClusterUpdate(d *schema.ResourceData, meta interface{}) error { 483 conn := meta.(*AWSClient).rdsconn 484 requestUpdate := false 485 486 req := &rds.ModifyDBClusterInput{ 487 ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), 488 DBClusterIdentifier: aws.String(d.Id()), 489 } 490 491 if d.HasChange("master_password") { 492 req.MasterUserPassword = aws.String(d.Get("master_password").(string)) 493 requestUpdate = true 494 } 495 496 if d.HasChange("vpc_security_group_ids") { 497 if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { 498 req.VpcSecurityGroupIds = expandStringList(attr.List()) 499 } else { 500 req.VpcSecurityGroupIds = []*string{} 501 } 502 requestUpdate = true 503 } 504 505 if d.HasChange("preferred_backup_window") { 506 req.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string)) 507 requestUpdate = true 508 } 509 510 if d.HasChange("preferred_maintenance_window") { 511 req.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) 512 requestUpdate = true 513 } 514 515 if d.HasChange("backup_retention_period") { 516 req.BackupRetentionPeriod = aws.Int64(int64(d.Get("backup_retention_period").(int))) 517 requestUpdate = true 518 } 519 520 if d.HasChange("parameter_group_name") { 521 d.SetPartial("parameter_group_name") 522 req.DBClusterParameterGroupName = aws.String(d.Get("parameter_group_name").(string)) 523 requestUpdate = true 524 } 525 526 if d.HasChange("db_cluster_parameter_group_name") { 527 d.SetPartial("db_cluster_parameter_group_name") 528 req.DBClusterParameterGroupName = aws.String(d.Get("db_cluster_parameter_group_name").(string)) 529 requestUpdate = true 530 } 531 532 if requestUpdate { 533 _, err := conn.ModifyDBCluster(req) 534 if err != nil { 535 return fmt.Errorf("[WARN] Error modifying RDS Cluster (%s): %s", d.Id(), err) 536 } 537 } 538 539 if arn, err := buildRDSClusterARN(d.Id(), meta.(*AWSClient).partition, meta.(*AWSClient).accountid, meta.(*AWSClient).region); err == nil { 540 if err := setTagsRDS(conn, d, arn); err != nil { 541 return err 542 } else { 543 d.SetPartial("tags") 544 } 545 } 546 547 return resourceAwsRDSClusterRead(d, meta) 548 } 549 550 func resourceAwsRDSClusterDelete(d *schema.ResourceData, meta interface{}) error { 551 conn := meta.(*AWSClient).rdsconn 552 log.Printf("[DEBUG] Destroying RDS Cluster (%s)", d.Id()) 553 554 deleteOpts := rds.DeleteDBClusterInput{ 555 DBClusterIdentifier: aws.String(d.Id()), 556 } 557 558 skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) 559 deleteOpts.SkipFinalSnapshot = aws.Bool(skipFinalSnapshot) 560 561 if skipFinalSnapshot == false { 562 if name, present := d.GetOk("final_snapshot_identifier"); present { 563 deleteOpts.FinalDBSnapshotIdentifier = aws.String(name.(string)) 564 } else { 565 return fmt.Errorf("RDS Cluster FinalSnapshotIdentifier is required when a final snapshot is required") 566 } 567 } 568 569 log.Printf("[DEBUG] RDS Cluster delete options: %s", deleteOpts) 570 _, err := conn.DeleteDBCluster(&deleteOpts) 571 572 stateConf := &resource.StateChangeConf{ 573 Pending: []string{"available", "deleting", "backing-up", "modifying"}, 574 Target: []string{"destroyed"}, 575 Refresh: resourceAwsRDSClusterStateRefreshFunc(d, meta), 576 Timeout: 15 * time.Minute, 577 MinTimeout: 3 * time.Second, 578 } 579 580 // Wait, catching any errors 581 _, err = stateConf.WaitForState() 582 if err != nil { 583 return fmt.Errorf("[WARN] Error deleting RDS Cluster (%s): %s", d.Id(), err) 584 } 585 586 return nil 587 } 588 589 func resourceAwsRDSClusterStateRefreshFunc( 590 d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { 591 return func() (interface{}, string, error) { 592 conn := meta.(*AWSClient).rdsconn 593 594 resp, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{ 595 DBClusterIdentifier: aws.String(d.Id()), 596 }) 597 598 if err != nil { 599 if awsErr, ok := err.(awserr.Error); ok { 600 if "DBClusterNotFoundFault" == awsErr.Code() { 601 return 42, "destroyed", nil 602 } 603 } 604 log.Printf("[WARN] Error on retrieving DB Cluster (%s) when waiting: %s", d.Id(), err) 605 return nil, "", err 606 } 607 608 var dbc *rds.DBCluster 609 610 for _, c := range resp.DBClusters { 611 if *c.DBClusterIdentifier == d.Id() { 612 dbc = c 613 } 614 } 615 616 if dbc == nil { 617 return 42, "destroyed", nil 618 } 619 620 if dbc.Status != nil { 621 log.Printf("[DEBUG] DB Cluster status (%s): %s", d.Id(), *dbc.Status) 622 } 623 624 return dbc, *dbc.Status, nil 625 } 626 } 627 628 func buildRDSClusterARN(identifier, partition, accountid, region string) (string, error) { 629 if partition == "" { 630 return "", fmt.Errorf("Unable to construct RDS Cluster ARN because of missing AWS partition") 631 } 632 if accountid == "" { 633 return "", fmt.Errorf("Unable to construct RDS Cluster ARN because of missing AWS Account ID") 634 } 635 636 arn := fmt.Sprintf("arn:%s:rds:%s:%s:cluster:%s", partition, region, accountid, identifier) 637 return arn, nil 638 639 }