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