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