github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/aws/resource_aws_opsworks_stack.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/resource" 10 "github.com/hashicorp/terraform/helper/schema" 11 12 "github.com/aws/aws-sdk-go/aws" 13 "github.com/aws/aws-sdk-go/aws/awserr" 14 "github.com/aws/aws-sdk-go/service/opsworks" 15 ) 16 17 func resourceAwsOpsworksStack() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsOpsworksStackCreate, 20 Read: resourceAwsOpsworksStackRead, 21 Update: resourceAwsOpsworksStackUpdate, 22 Delete: resourceAwsOpsworksStackDelete, 23 Importer: &schema.ResourceImporter{ 24 State: schema.ImportStatePassthrough, 25 }, 26 27 Schema: map[string]*schema.Schema{ 28 "agent_version": &schema.Schema{ 29 Type: schema.TypeString, 30 Optional: true, 31 Computed: true, 32 }, 33 34 "id": &schema.Schema{ 35 Type: schema.TypeString, 36 Computed: true, 37 }, 38 39 "name": &schema.Schema{ 40 Type: schema.TypeString, 41 Required: true, 42 }, 43 44 "region": &schema.Schema{ 45 Type: schema.TypeString, 46 ForceNew: true, 47 Required: true, 48 }, 49 50 "service_role_arn": &schema.Schema{ 51 Type: schema.TypeString, 52 Required: true, 53 }, 54 55 "default_instance_profile_arn": &schema.Schema{ 56 Type: schema.TypeString, 57 Required: true, 58 }, 59 60 "color": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 }, 64 65 "configuration_manager_name": &schema.Schema{ 66 Type: schema.TypeString, 67 Optional: true, 68 Default: "Chef", 69 }, 70 71 "configuration_manager_version": &schema.Schema{ 72 Type: schema.TypeString, 73 Optional: true, 74 Default: "11.4", 75 }, 76 77 "manage_berkshelf": &schema.Schema{ 78 Type: schema.TypeBool, 79 Optional: true, 80 Default: false, 81 }, 82 83 "berkshelf_version": &schema.Schema{ 84 Type: schema.TypeString, 85 Optional: true, 86 Default: "3.2.0", 87 }, 88 89 "custom_cookbooks_source": &schema.Schema{ 90 Type: schema.TypeList, 91 Optional: true, 92 Computed: true, 93 Elem: &schema.Resource{ 94 Schema: map[string]*schema.Schema{ 95 "type": &schema.Schema{ 96 Type: schema.TypeString, 97 Required: true, 98 }, 99 100 "url": &schema.Schema{ 101 Type: schema.TypeString, 102 Required: true, 103 }, 104 105 "username": &schema.Schema{ 106 Type: schema.TypeString, 107 Optional: true, 108 }, 109 110 "password": &schema.Schema{ 111 Type: schema.TypeString, 112 Optional: true, 113 }, 114 115 "revision": &schema.Schema{ 116 Type: schema.TypeString, 117 Optional: true, 118 }, 119 120 "ssh_key": &schema.Schema{ 121 Type: schema.TypeString, 122 Optional: true, 123 }, 124 }, 125 }, 126 }, 127 128 "custom_json": &schema.Schema{ 129 Type: schema.TypeString, 130 Optional: true, 131 }, 132 133 "default_availability_zone": &schema.Schema{ 134 Type: schema.TypeString, 135 Optional: true, 136 Computed: true, 137 }, 138 139 "default_os": &schema.Schema{ 140 Type: schema.TypeString, 141 Optional: true, 142 Default: "Ubuntu 12.04 LTS", 143 }, 144 145 "default_root_device_type": &schema.Schema{ 146 Type: schema.TypeString, 147 Optional: true, 148 Default: "instance-store", 149 }, 150 151 "default_ssh_key_name": &schema.Schema{ 152 Type: schema.TypeString, 153 Optional: true, 154 }, 155 156 "default_subnet_id": &schema.Schema{ 157 Type: schema.TypeString, 158 Optional: true, 159 }, 160 161 "hostname_theme": &schema.Schema{ 162 Type: schema.TypeString, 163 Optional: true, 164 Default: "Layer_Dependent", 165 }, 166 167 "use_custom_cookbooks": &schema.Schema{ 168 Type: schema.TypeBool, 169 Optional: true, 170 Default: false, 171 }, 172 173 "use_opsworks_security_groups": &schema.Schema{ 174 Type: schema.TypeBool, 175 Optional: true, 176 Default: true, 177 }, 178 179 "vpc_id": &schema.Schema{ 180 Type: schema.TypeString, 181 ForceNew: true, 182 Optional: true, 183 }, 184 }, 185 } 186 } 187 188 func resourceAwsOpsworksStackValidate(d *schema.ResourceData) error { 189 cookbooksSourceCount := d.Get("custom_cookbooks_source.#").(int) 190 if cookbooksSourceCount > 1 { 191 return fmt.Errorf("Only one custom_cookbooks_source is permitted") 192 } 193 194 vpcId := d.Get("vpc_id").(string) 195 if vpcId != "" { 196 if d.Get("default_subnet_id").(string) == "" { 197 return fmt.Errorf("default_subnet_id must be set if vpc_id is set") 198 } 199 } else { 200 if d.Get("default_availability_zone").(string) == "" { 201 return fmt.Errorf("either vpc_id or default_availability_zone must be set") 202 } 203 } 204 205 return nil 206 } 207 208 func resourceAwsOpsworksStackCustomCookbooksSource(d *schema.ResourceData) *opsworks.Source { 209 count := d.Get("custom_cookbooks_source.#").(int) 210 if count == 0 { 211 return nil 212 } 213 214 return &opsworks.Source{ 215 Type: aws.String(d.Get("custom_cookbooks_source.0.type").(string)), 216 Url: aws.String(d.Get("custom_cookbooks_source.0.url").(string)), 217 Username: aws.String(d.Get("custom_cookbooks_source.0.username").(string)), 218 Password: aws.String(d.Get("custom_cookbooks_source.0.password").(string)), 219 Revision: aws.String(d.Get("custom_cookbooks_source.0.revision").(string)), 220 SshKey: aws.String(d.Get("custom_cookbooks_source.0.ssh_key").(string)), 221 } 222 } 223 224 func resourceAwsOpsworksSetStackCustomCookbooksSource(d *schema.ResourceData, v *opsworks.Source) { 225 nv := make([]interface{}, 0, 1) 226 if v != nil && v.Type != nil && *v.Type != "" { 227 m := make(map[string]interface{}) 228 if v.Type != nil { 229 m["type"] = *v.Type 230 } 231 if v.Url != nil { 232 m["url"] = *v.Url 233 } 234 if v.Username != nil { 235 m["username"] = *v.Username 236 } 237 if v.Revision != nil { 238 m["revision"] = *v.Revision 239 } 240 // v.Password will, on read, contain the placeholder string 241 // "*****FILTERED*****", so we ignore it on read and let persist 242 // the value already in the state. 243 nv = append(nv, m) 244 } 245 246 err := d.Set("custom_cookbooks_source", nv) 247 if err != nil { 248 // should never happen 249 panic(err) 250 } 251 } 252 253 func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error { 254 client := meta.(*AWSClient).opsworksconn 255 256 req := &opsworks.DescribeStacksInput{ 257 StackIds: []*string{ 258 aws.String(d.Id()), 259 }, 260 } 261 262 log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id()) 263 264 resp, err := client.DescribeStacks(req) 265 if err != nil { 266 if awserr, ok := err.(awserr.Error); ok { 267 if awserr.Code() == "ResourceNotFoundException" { 268 log.Printf("[DEBUG] OpsWorks stack (%s) not found", d.Id()) 269 d.SetId("") 270 return nil 271 } 272 } 273 return err 274 } 275 276 stack := resp.Stacks[0] 277 d.Set("agent_version", stack.AgentVersion) 278 d.Set("name", stack.Name) 279 d.Set("region", stack.Region) 280 d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn) 281 d.Set("service_role_arn", stack.ServiceRoleArn) 282 d.Set("default_availability_zone", stack.DefaultAvailabilityZone) 283 d.Set("default_os", stack.DefaultOs) 284 d.Set("default_root_device_type", stack.DefaultRootDeviceType) 285 d.Set("default_ssh_key_name", stack.DefaultSshKeyName) 286 d.Set("default_subnet_id", stack.DefaultSubnetId) 287 d.Set("hostname_theme", stack.HostnameTheme) 288 d.Set("use_custom_cookbooks", stack.UseCustomCookbooks) 289 if stack.CustomJson != nil { 290 d.Set("custom_json", stack.CustomJson) 291 } 292 d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups) 293 d.Set("vpc_id", stack.VpcId) 294 if color, ok := stack.Attributes["Color"]; ok { 295 d.Set("color", color) 296 } 297 if stack.ConfigurationManager != nil { 298 d.Set("configuration_manager_name", stack.ConfigurationManager.Name) 299 d.Set("configuration_manager_version", stack.ConfigurationManager.Version) 300 } 301 if stack.ChefConfiguration != nil { 302 d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion) 303 d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf) 304 } 305 resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource) 306 307 return nil 308 } 309 310 func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error { 311 client := meta.(*AWSClient).opsworksconn 312 313 err := resourceAwsOpsworksStackValidate(d) 314 if err != nil { 315 return err 316 } 317 318 req := &opsworks.CreateStackInput{ 319 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 320 Name: aws.String(d.Get("name").(string)), 321 Region: aws.String(d.Get("region").(string)), 322 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 323 DefaultOs: aws.String(d.Get("default_os").(string)), 324 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 325 } 326 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 327 Name: aws.String(d.Get("configuration_manager_name").(string)), 328 Version: aws.String(d.Get("configuration_manager_version").(string)), 329 } 330 inVpc := false 331 if vpcId, ok := d.GetOk("vpc_id"); ok { 332 req.VpcId = aws.String(vpcId.(string)) 333 inVpc = true 334 } 335 if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok { 336 req.DefaultSubnetId = aws.String(defaultSubnetId.(string)) 337 } 338 if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok { 339 req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string)) 340 } 341 if defaultRootDeviceType, ok := d.GetOk("default_root_device_type"); ok { 342 req.DefaultRootDeviceType = aws.String(defaultRootDeviceType.(string)) 343 } 344 345 log.Printf("[DEBUG] Creating OpsWorks stack: %s", req) 346 347 var resp *opsworks.CreateStackOutput 348 err = resource.Retry(20*time.Minute, func() *resource.RetryError { 349 var cerr error 350 resp, cerr = client.CreateStack(req) 351 if cerr != nil { 352 if opserr, ok := cerr.(awserr.Error); ok { 353 // If Terraform is also managing the service IAM role, 354 // it may have just been created and not yet be 355 // propagated. 356 // AWS doesn't provide a machine-readable code for this 357 // specific error, so we're forced to do fragile message 358 // matching. 359 // The full error we're looking for looks something like 360 // the following: 361 // Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes 362 propErr := "not yet propagated" 363 trustErr := "not the necessary trust relationship" 364 validateErr := "validate IAM role permission" 365 if opserr.Code() == "ValidationException" && (strings.Contains(opserr.Message(), trustErr) || strings.Contains(opserr.Message(), propErr) || strings.Contains(opserr.Message(), validateErr)) { 366 log.Printf("[INFO] Waiting for service IAM role to propagate") 367 return resource.RetryableError(cerr) 368 } 369 } 370 return resource.NonRetryableError(cerr) 371 } 372 return nil 373 }) 374 if err != nil { 375 return err 376 } 377 378 stackId := *resp.StackId 379 d.SetId(stackId) 380 d.Set("id", stackId) 381 382 if inVpc && *req.UseOpsworksSecurityGroups { 383 // For VPC-based stacks, OpsWorks asynchronously creates some default 384 // security groups which must exist before layers can be created. 385 // Unfortunately it doesn't tell us what the ids of these are, so 386 // we can't actually check for them. Instead, we just wait a nominal 387 // amount of time for their creation to complete. 388 log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created") 389 time.Sleep(30 * time.Second) 390 } 391 392 return resourceAwsOpsworksStackUpdate(d, meta) 393 } 394 395 func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error { 396 client := meta.(*AWSClient).opsworksconn 397 398 err := resourceAwsOpsworksStackValidate(d) 399 if err != nil { 400 return err 401 } 402 403 req := &opsworks.UpdateStackInput{ 404 CustomJson: aws.String(d.Get("custom_json").(string)), 405 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 406 DefaultRootDeviceType: aws.String(d.Get("default_root_device_type").(string)), 407 DefaultSshKeyName: aws.String(d.Get("default_ssh_key_name").(string)), 408 Name: aws.String(d.Get("name").(string)), 409 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 410 StackId: aws.String(d.Id()), 411 UseCustomCookbooks: aws.Bool(d.Get("use_custom_cookbooks").(bool)), 412 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 413 Attributes: make(map[string]*string), 414 CustomCookbooksSource: resourceAwsOpsworksStackCustomCookbooksSource(d), 415 } 416 if v, ok := d.GetOk("agent_version"); ok { 417 req.AgentVersion = aws.String(v.(string)) 418 } 419 if v, ok := d.GetOk("default_os"); ok { 420 req.DefaultOs = aws.String(v.(string)) 421 } 422 if v, ok := d.GetOk("default_subnet_id"); ok { 423 req.DefaultSubnetId = aws.String(v.(string)) 424 } 425 if v, ok := d.GetOk("default_availability_zone"); ok { 426 req.DefaultAvailabilityZone = aws.String(v.(string)) 427 } 428 if v, ok := d.GetOk("hostname_theme"); ok { 429 req.HostnameTheme = aws.String(v.(string)) 430 } 431 if v, ok := d.GetOk("color"); ok { 432 req.Attributes["Color"] = aws.String(v.(string)) 433 } 434 req.ChefConfiguration = &opsworks.ChefConfiguration{ 435 BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)), 436 ManageBerkshelf: aws.Bool(d.Get("manage_berkshelf").(bool)), 437 } 438 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 439 Name: aws.String(d.Get("configuration_manager_name").(string)), 440 Version: aws.String(d.Get("configuration_manager_version").(string)), 441 } 442 443 log.Printf("[DEBUG] Updating OpsWorks stack: %s", req) 444 445 _, err = client.UpdateStack(req) 446 if err != nil { 447 return err 448 } 449 450 return resourceAwsOpsworksStackRead(d, meta) 451 } 452 453 func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error { 454 client := meta.(*AWSClient).opsworksconn 455 456 req := &opsworks.DeleteStackInput{ 457 StackId: aws.String(d.Id()), 458 } 459 460 log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id()) 461 462 _, err := client.DeleteStack(req) 463 if err != nil { 464 return err 465 } 466 467 // For a stack in a VPC, OpsWorks has created some default security groups 468 // in the VPC, which it will now delete. 469 // Unfortunately, the security groups are deleted asynchronously and there 470 // is no robust way for us to determine when it is done. The VPC itself 471 // isn't deletable until the security groups are cleaned up, so this could 472 // make 'terraform destroy' fail if the VPC is also managed and we don't 473 // wait for the security groups to be deleted. 474 // There is no robust way to check for this, so we'll just wait a 475 // nominal amount of time. 476 _, inVpc := d.GetOk("vpc_id") 477 _, useOpsworksDefaultSg := d.GetOk("use_opsworks_security_group") 478 479 if inVpc && useOpsworksDefaultSg { 480 log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted") 481 time.Sleep(30 * time.Second) 482 } 483 484 return nil 485 }