github.com/danp/terraform@v0.9.5-0.20170426144147-39d740081351/builtin/providers/aws/resource_aws_opsworks_stack.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/errwrap" 11 "github.com/hashicorp/terraform/helper/resource" 12 "github.com/hashicorp/terraform/helper/schema" 13 14 "github.com/aws/aws-sdk-go/aws" 15 "github.com/aws/aws-sdk-go/aws/awserr" 16 "github.com/aws/aws-sdk-go/aws/session" 17 "github.com/aws/aws-sdk-go/service/opsworks" 18 ) 19 20 func resourceAwsOpsworksStack() *schema.Resource { 21 return &schema.Resource{ 22 Create: resourceAwsOpsworksStackCreate, 23 Read: resourceAwsOpsworksStackRead, 24 Update: resourceAwsOpsworksStackUpdate, 25 Delete: resourceAwsOpsworksStackDelete, 26 Importer: &schema.ResourceImporter{ 27 State: schema.ImportStatePassthrough, 28 }, 29 30 Schema: map[string]*schema.Schema{ 31 "agent_version": { 32 Type: schema.TypeString, 33 Optional: true, 34 Computed: true, 35 }, 36 37 "id": { 38 Type: schema.TypeString, 39 Computed: true, 40 }, 41 42 "name": { 43 Type: schema.TypeString, 44 Required: true, 45 }, 46 47 "region": { 48 Type: schema.TypeString, 49 ForceNew: true, 50 Required: true, 51 }, 52 53 "service_role_arn": { 54 Type: schema.TypeString, 55 Required: true, 56 ForceNew: true, 57 }, 58 59 "default_instance_profile_arn": { 60 Type: schema.TypeString, 61 Required: true, 62 }, 63 64 "color": { 65 Type: schema.TypeString, 66 Optional: true, 67 }, 68 69 "configuration_manager_name": { 70 Type: schema.TypeString, 71 Optional: true, 72 Default: "Chef", 73 }, 74 75 "configuration_manager_version": { 76 Type: schema.TypeString, 77 Optional: true, 78 Default: "11.10", 79 }, 80 81 "manage_berkshelf": { 82 Type: schema.TypeBool, 83 Optional: true, 84 Default: false, 85 }, 86 87 "berkshelf_version": { 88 Type: schema.TypeString, 89 Optional: true, 90 Default: "3.2.0", 91 }, 92 93 "custom_cookbooks_source": { 94 Type: schema.TypeList, 95 Optional: true, 96 Computed: true, 97 Elem: &schema.Resource{ 98 Schema: map[string]*schema.Schema{ 99 "type": { 100 Type: schema.TypeString, 101 Required: true, 102 }, 103 104 "url": { 105 Type: schema.TypeString, 106 Required: true, 107 }, 108 109 "username": { 110 Type: schema.TypeString, 111 Optional: true, 112 }, 113 114 "password": { 115 Type: schema.TypeString, 116 Optional: true, 117 Sensitive: true, 118 }, 119 120 "revision": { 121 Type: schema.TypeString, 122 Optional: true, 123 }, 124 125 "ssh_key": { 126 Type: schema.TypeString, 127 Optional: true, 128 }, 129 }, 130 }, 131 }, 132 133 "custom_json": { 134 Type: schema.TypeString, 135 Optional: true, 136 }, 137 138 "default_availability_zone": { 139 Type: schema.TypeString, 140 Optional: true, 141 Computed: true, 142 }, 143 144 "default_os": { 145 Type: schema.TypeString, 146 Optional: true, 147 Default: "Ubuntu 12.04 LTS", 148 }, 149 150 "default_root_device_type": { 151 Type: schema.TypeString, 152 Optional: true, 153 Default: "instance-store", 154 }, 155 156 "default_ssh_key_name": { 157 Type: schema.TypeString, 158 Optional: true, 159 }, 160 161 "default_subnet_id": { 162 Type: schema.TypeString, 163 Optional: true, 164 Computed: true, 165 }, 166 167 "hostname_theme": { 168 Type: schema.TypeString, 169 Optional: true, 170 Default: "Layer_Dependent", 171 }, 172 173 "use_custom_cookbooks": { 174 Type: schema.TypeBool, 175 Optional: true, 176 Default: false, 177 }, 178 179 "use_opsworks_security_groups": { 180 Type: schema.TypeBool, 181 Optional: true, 182 Default: true, 183 }, 184 185 "vpc_id": { 186 Type: schema.TypeString, 187 ForceNew: true, 188 Computed: true, 189 Optional: true, 190 }, 191 192 "stack_endpoint": { 193 Type: schema.TypeString, 194 Computed: true, 195 }, 196 }, 197 } 198 } 199 200 func resourceAwsOpsworksStackValidate(d *schema.ResourceData) error { 201 cookbooksSourceCount := d.Get("custom_cookbooks_source.#").(int) 202 if cookbooksSourceCount > 1 { 203 return fmt.Errorf("Only one custom_cookbooks_source is permitted") 204 } 205 206 vpcId := d.Get("vpc_id").(string) 207 if vpcId != "" { 208 if d.Get("default_subnet_id").(string) == "" { 209 return fmt.Errorf("default_subnet_id must be set if vpc_id is set") 210 } 211 } else { 212 if d.Get("default_availability_zone").(string) == "" { 213 return fmt.Errorf("either vpc_id or default_availability_zone must be set") 214 } 215 } 216 217 return nil 218 } 219 220 func resourceAwsOpsworksStackCustomCookbooksSource(d *schema.ResourceData) *opsworks.Source { 221 count := d.Get("custom_cookbooks_source.#").(int) 222 if count == 0 { 223 return nil 224 } 225 226 return &opsworks.Source{ 227 Type: aws.String(d.Get("custom_cookbooks_source.0.type").(string)), 228 Url: aws.String(d.Get("custom_cookbooks_source.0.url").(string)), 229 Username: aws.String(d.Get("custom_cookbooks_source.0.username").(string)), 230 Password: aws.String(d.Get("custom_cookbooks_source.0.password").(string)), 231 Revision: aws.String(d.Get("custom_cookbooks_source.0.revision").(string)), 232 SshKey: aws.String(d.Get("custom_cookbooks_source.0.ssh_key").(string)), 233 } 234 } 235 236 func resourceAwsOpsworksSetStackCustomCookbooksSource(d *schema.ResourceData, v *opsworks.Source) { 237 nv := make([]interface{}, 0, 1) 238 if v != nil && v.Type != nil && *v.Type != "" { 239 m := make(map[string]interface{}) 240 if v.Type != nil { 241 m["type"] = *v.Type 242 } 243 if v.Url != nil { 244 m["url"] = *v.Url 245 } 246 if v.Username != nil { 247 m["username"] = *v.Username 248 } 249 if v.Revision != nil { 250 m["revision"] = *v.Revision 251 } 252 // v.Password will, on read, contain the placeholder string 253 // "*****FILTERED*****", so we ignore it on read and let persist 254 // the value already in the state. 255 nv = append(nv, m) 256 } 257 258 err := d.Set("custom_cookbooks_source", nv) 259 if err != nil { 260 // should never happen 261 panic(err) 262 } 263 } 264 265 func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error { 266 client := meta.(*AWSClient).opsworksconn 267 var conErr error 268 if v := d.Get("stack_endpoint").(string); v != "" { 269 client, conErr = opsworksConnForRegion(v, meta) 270 if conErr != nil { 271 return conErr 272 } 273 } 274 275 req := &opsworks.DescribeStacksInput{ 276 StackIds: []*string{ 277 aws.String(d.Id()), 278 }, 279 } 280 281 log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id()) 282 283 // notFound represents the number of times we've called DescribeStacks looking 284 // for this Stack. If it's not found in the the default region we're in, we 285 // check us-east-1 in the event this stack was created with Terraform before 286 // version 0.9 287 // See https://github.com/hashicorp/terraform/issues/12842 288 var notFound int 289 var resp *opsworks.DescribeStacksOutput 290 var dErr error 291 292 for { 293 resp, dErr = client.DescribeStacks(req) 294 if dErr != nil { 295 if awserr, ok := dErr.(awserr.Error); ok { 296 if awserr.Code() == "ResourceNotFoundException" { 297 if notFound < 1 { 298 // If we haven't already, try us-east-1, legacy connection 299 notFound++ 300 var connErr error 301 client, connErr = opsworksConnForRegion("us-east-1", meta) 302 if connErr != nil { 303 return connErr 304 } 305 // start again from the top of the FOR loop, but with a client 306 // configured to talk to us-east-1 307 continue 308 } 309 310 // We've tried both the original and us-east-1 endpoint, and the stack 311 // is still not found 312 log.Printf("[DEBUG] OpsWorks stack (%s) not found", d.Id()) 313 d.SetId("") 314 return nil 315 } 316 // not ResoureNotFoundException, fall through to returning error 317 } 318 return dErr 319 } 320 // If the stack was found, set the stack_endpoint 321 if client.Config.Region != nil && *client.Config.Region != "" { 322 log.Printf("[DEBUG] Setting stack_endpoint for (%s) to (%s)", d.Id(), *client.Config.Region) 323 if err := d.Set("stack_endpoint", *client.Config.Region); err != nil { 324 log.Printf("[WARN] Error setting stack_endpoint: %s", err) 325 } 326 } 327 log.Printf("[DEBUG] Breaking stack endpoint search, found stack for (%s)", d.Id()) 328 // Break the FOR loop 329 break 330 } 331 332 stack := resp.Stacks[0] 333 d.Set("agent_version", stack.AgentVersion) 334 d.Set("name", stack.Name) 335 d.Set("region", stack.Region) 336 d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn) 337 d.Set("service_role_arn", stack.ServiceRoleArn) 338 d.Set("default_availability_zone", stack.DefaultAvailabilityZone) 339 d.Set("default_os", stack.DefaultOs) 340 d.Set("default_root_device_type", stack.DefaultRootDeviceType) 341 d.Set("default_ssh_key_name", stack.DefaultSshKeyName) 342 d.Set("default_subnet_id", stack.DefaultSubnetId) 343 d.Set("hostname_theme", stack.HostnameTheme) 344 d.Set("use_custom_cookbooks", stack.UseCustomCookbooks) 345 if stack.CustomJson != nil { 346 d.Set("custom_json", stack.CustomJson) 347 } 348 d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups) 349 d.Set("vpc_id", stack.VpcId) 350 if color, ok := stack.Attributes["Color"]; ok { 351 d.Set("color", color) 352 } 353 if stack.ConfigurationManager != nil { 354 d.Set("configuration_manager_name", stack.ConfigurationManager.Name) 355 d.Set("configuration_manager_version", stack.ConfigurationManager.Version) 356 } 357 if stack.ChefConfiguration != nil { 358 d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion) 359 d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf) 360 } 361 resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource) 362 363 return nil 364 } 365 366 // opsworksConn will return a connection for the stack_endpoint in the 367 // configuration. Stacks can only be accessed or managed within the endpoint 368 // in which they are created, so we allow users to specify an original endpoint 369 // for Stacks created before multiple endpoints were offered (Terraform v0.9.0). 370 // See: 371 // - https://github.com/hashicorp/terraform/pull/12688 372 // - https://github.com/hashicorp/terraform/issues/12842 373 func opsworksConnForRegion(region string, meta interface{}) (*opsworks.OpsWorks, error) { 374 originalConn := meta.(*AWSClient).opsworksconn 375 376 // Regions are the same, no need to reconfigure 377 if originalConn.Config.Region != nil && *originalConn.Config.Region == region { 378 return originalConn, nil 379 } 380 381 // Set up base session 382 sess, err := session.NewSession(&originalConn.Config) 383 if err != nil { 384 return nil, errwrap.Wrapf("Error creating AWS session: {{err}}", err) 385 } 386 387 sess.Handlers.Build.PushBackNamed(addTerraformVersionToUserAgent) 388 389 if extraDebug := os.Getenv("TERRAFORM_AWS_AUTHFAILURE_DEBUG"); extraDebug != "" { 390 sess.Handlers.UnmarshalError.PushFrontNamed(debugAuthFailure) 391 } 392 393 newSession := sess.Copy(&aws.Config{Region: aws.String(region)}) 394 newOpsworksconn := opsworks.New(newSession) 395 396 log.Printf("[DEBUG] Returning new OpsWorks client") 397 return newOpsworksconn, nil 398 } 399 400 func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error { 401 client := meta.(*AWSClient).opsworksconn 402 403 err := resourceAwsOpsworksStackValidate(d) 404 if err != nil { 405 return err 406 } 407 408 req := &opsworks.CreateStackInput{ 409 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 410 Name: aws.String(d.Get("name").(string)), 411 Region: aws.String(d.Get("region").(string)), 412 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 413 DefaultOs: aws.String(d.Get("default_os").(string)), 414 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 415 } 416 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 417 Name: aws.String(d.Get("configuration_manager_name").(string)), 418 Version: aws.String(d.Get("configuration_manager_version").(string)), 419 } 420 inVpc := false 421 if vpcId, ok := d.GetOk("vpc_id"); ok { 422 req.VpcId = aws.String(vpcId.(string)) 423 inVpc = true 424 } 425 if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok { 426 req.DefaultSubnetId = aws.String(defaultSubnetId.(string)) 427 } 428 if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok { 429 req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string)) 430 } 431 if defaultRootDeviceType, ok := d.GetOk("default_root_device_type"); ok { 432 req.DefaultRootDeviceType = aws.String(defaultRootDeviceType.(string)) 433 } 434 435 log.Printf("[DEBUG] Creating OpsWorks stack: %s", req) 436 437 var resp *opsworks.CreateStackOutput 438 err = resource.Retry(20*time.Minute, func() *resource.RetryError { 439 var cerr error 440 resp, cerr = client.CreateStack(req) 441 if cerr != nil { 442 if opserr, ok := cerr.(awserr.Error); ok { 443 // If Terraform is also managing the service IAM role, 444 // it may have just been created and not yet be 445 // propagated. 446 // AWS doesn't provide a machine-readable code for this 447 // specific error, so we're forced to do fragile message 448 // matching. 449 // The full error we're looking for looks something like 450 // the following: 451 // Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes 452 propErr := "not yet propagated" 453 trustErr := "not the necessary trust relationship" 454 validateErr := "validate IAM role permission" 455 if opserr.Code() == "ValidationException" && (strings.Contains(opserr.Message(), trustErr) || strings.Contains(opserr.Message(), propErr) || strings.Contains(opserr.Message(), validateErr)) { 456 log.Printf("[INFO] Waiting for service IAM role to propagate") 457 return resource.RetryableError(cerr) 458 } 459 } 460 return resource.NonRetryableError(cerr) 461 } 462 return nil 463 }) 464 if err != nil { 465 return err 466 } 467 468 stackId := *resp.StackId 469 d.SetId(stackId) 470 d.Set("id", stackId) 471 472 if inVpc && *req.UseOpsworksSecurityGroups { 473 // For VPC-based stacks, OpsWorks asynchronously creates some default 474 // security groups which must exist before layers can be created. 475 // Unfortunately it doesn't tell us what the ids of these are, so 476 // we can't actually check for them. Instead, we just wait a nominal 477 // amount of time for their creation to complete. 478 log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created") 479 time.Sleep(30 * time.Second) 480 } 481 482 return resourceAwsOpsworksStackUpdate(d, meta) 483 } 484 485 func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error { 486 client := meta.(*AWSClient).opsworksconn 487 var conErr error 488 if v := d.Get("stack_endpoint").(string); v != "" { 489 client, conErr = opsworksConnForRegion(v, meta) 490 if conErr != nil { 491 return conErr 492 } 493 } 494 495 err := resourceAwsOpsworksStackValidate(d) 496 if err != nil { 497 return err 498 } 499 500 req := &opsworks.UpdateStackInput{ 501 CustomJson: aws.String(d.Get("custom_json").(string)), 502 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 503 DefaultRootDeviceType: aws.String(d.Get("default_root_device_type").(string)), 504 DefaultSshKeyName: aws.String(d.Get("default_ssh_key_name").(string)), 505 Name: aws.String(d.Get("name").(string)), 506 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 507 StackId: aws.String(d.Id()), 508 UseCustomCookbooks: aws.Bool(d.Get("use_custom_cookbooks").(bool)), 509 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 510 Attributes: make(map[string]*string), 511 CustomCookbooksSource: resourceAwsOpsworksStackCustomCookbooksSource(d), 512 } 513 if v, ok := d.GetOk("agent_version"); ok { 514 req.AgentVersion = aws.String(v.(string)) 515 } 516 if v, ok := d.GetOk("default_os"); ok { 517 req.DefaultOs = aws.String(v.(string)) 518 } 519 if v, ok := d.GetOk("default_subnet_id"); ok { 520 req.DefaultSubnetId = aws.String(v.(string)) 521 } 522 if v, ok := d.GetOk("default_availability_zone"); ok { 523 req.DefaultAvailabilityZone = aws.String(v.(string)) 524 } 525 if v, ok := d.GetOk("hostname_theme"); ok { 526 req.HostnameTheme = aws.String(v.(string)) 527 } 528 if v, ok := d.GetOk("color"); ok { 529 req.Attributes["Color"] = aws.String(v.(string)) 530 } 531 532 req.ChefConfiguration = &opsworks.ChefConfiguration{ 533 BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)), 534 ManageBerkshelf: aws.Bool(d.Get("manage_berkshelf").(bool)), 535 } 536 537 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 538 Name: aws.String(d.Get("configuration_manager_name").(string)), 539 Version: aws.String(d.Get("configuration_manager_version").(string)), 540 } 541 542 log.Printf("[DEBUG] Updating OpsWorks stack: %s", req) 543 544 _, err = client.UpdateStack(req) 545 if err != nil { 546 return err 547 } 548 549 return resourceAwsOpsworksStackRead(d, meta) 550 } 551 552 func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error { 553 client := meta.(*AWSClient).opsworksconn 554 var conErr error 555 if v := d.Get("stack_endpoint").(string); v != "" { 556 client, conErr = opsworksConnForRegion(v, meta) 557 if conErr != nil { 558 return conErr 559 } 560 } 561 562 req := &opsworks.DeleteStackInput{ 563 StackId: aws.String(d.Id()), 564 } 565 566 log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id()) 567 568 _, err := client.DeleteStack(req) 569 if err != nil { 570 return err 571 } 572 573 // For a stack in a VPC, OpsWorks has created some default security groups 574 // in the VPC, which it will now delete. 575 // Unfortunately, the security groups are deleted asynchronously and there 576 // is no robust way for us to determine when it is done. The VPC itself 577 // isn't deletable until the security groups are cleaned up, so this could 578 // make 'terraform destroy' fail if the VPC is also managed and we don't 579 // wait for the security groups to be deleted. 580 // There is no robust way to check for this, so we'll just wait a 581 // nominal amount of time. 582 _, inVpc := d.GetOk("vpc_id") 583 _, useOpsworksDefaultSg := d.GetOk("use_opsworks_security_group") 584 585 if inVpc && useOpsworksDefaultSg { 586 log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted") 587 time.Sleep(30 * time.Second) 588 } 589 590 return nil 591 }