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