github.com/leeprovoost/terraform@v0.6.10-0.20160119085442-96f3f76118e7/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 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 311 } 312 inVpc := false 313 if vpcId, ok := d.GetOk("vpc_id"); ok { 314 req.VpcId = aws.String(vpcId.(string)) 315 inVpc = true 316 } 317 if defaultSubnetId, ok := d.GetOk("default_subnet_id"); ok { 318 req.DefaultSubnetId = aws.String(defaultSubnetId.(string)) 319 } 320 if defaultAvailabilityZone, ok := d.GetOk("default_availability_zone"); ok { 321 req.DefaultAvailabilityZone = aws.String(defaultAvailabilityZone.(string)) 322 } 323 324 log.Printf("[DEBUG] Creating OpsWorks stack: %s", req) 325 326 var resp *opsworks.CreateStackOutput 327 err = resource.Retry(20*time.Minute, func() error { 328 var cerr error 329 resp, cerr = client.CreateStack(req) 330 if cerr != nil { 331 if opserr, ok := cerr.(awserr.Error); ok { 332 // If Terraform is also managing the service IAM role, 333 // it may have just been created and not yet be 334 // propagated. 335 // AWS doesn't provide a machine-readable code for this 336 // specific error, so we're forced to do fragile message 337 // matching. 338 // The full error we're looking for looks something like 339 // the following: 340 // Service Role Arn: [...] is not yet propagated, please try again in a couple of minutes 341 propErr := "not yet propagated" 342 trustErr := "not the necessary trust relationship" 343 if opserr.Code() == "ValidationException" && (strings.Contains(opserr.Message(), trustErr) || strings.Contains(opserr.Message(), propErr)) { 344 log.Printf("[INFO] Waiting for service IAM role to propagate") 345 return cerr 346 } 347 } 348 return resource.RetryError{Err: cerr} 349 } 350 return nil 351 }) 352 if err != nil { 353 return err 354 } 355 356 stackId := *resp.StackId 357 d.SetId(stackId) 358 d.Set("id", stackId) 359 360 if inVpc && *req.UseOpsworksSecurityGroups { 361 // For VPC-based stacks, OpsWorks asynchronously creates some default 362 // security groups which must exist before layers can be created. 363 // Unfortunately it doesn't tell us what the ids of these are, so 364 // we can't actually check for them. Instead, we just wait a nominal 365 // amount of time for their creation to complete. 366 log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created") 367 time.Sleep(30 * time.Second) 368 } 369 370 return resourceAwsOpsworksStackUpdate(d, meta) 371 } 372 373 func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error { 374 client := meta.(*AWSClient).opsworksconn 375 376 err := resourceAwsOpsworksStackValidate(d) 377 if err != nil { 378 return err 379 } 380 381 req := &opsworks.UpdateStackInput{ 382 CustomJson: aws.String(d.Get("custom_json").(string)), 383 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 384 DefaultRootDeviceType: aws.String(d.Get("default_root_device_type").(string)), 385 DefaultSshKeyName: aws.String(d.Get("default_ssh_key_name").(string)), 386 Name: aws.String(d.Get("name").(string)), 387 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 388 StackId: aws.String(d.Id()), 389 UseCustomCookbooks: aws.Bool(d.Get("use_custom_cookbooks").(bool)), 390 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 391 Attributes: make(map[string]*string), 392 CustomCookbooksSource: resourceAwsOpsworksStackCustomCookbooksSource(d), 393 } 394 if v, ok := d.GetOk("default_os"); ok { 395 req.DefaultOs = aws.String(v.(string)) 396 } 397 if v, ok := d.GetOk("default_subnet_id"); ok { 398 req.DefaultSubnetId = aws.String(v.(string)) 399 } 400 if v, ok := d.GetOk("default_availability_zone"); ok { 401 req.DefaultAvailabilityZone = aws.String(v.(string)) 402 } 403 if v, ok := d.GetOk("hostname_theme"); ok { 404 req.HostnameTheme = aws.String(v.(string)) 405 } 406 if v, ok := d.GetOk("color"); ok { 407 req.Attributes["Color"] = aws.String(v.(string)) 408 } 409 req.ChefConfiguration = &opsworks.ChefConfiguration{ 410 BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)), 411 ManageBerkshelf: aws.Bool(d.Get("manage_berkshelf").(bool)), 412 } 413 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 414 Name: aws.String(d.Get("configuration_manager_name").(string)), 415 Version: aws.String(d.Get("configuration_manager_version").(string)), 416 } 417 418 log.Printf("[DEBUG] Updating OpsWorks stack: %s", req) 419 420 _, err = client.UpdateStack(req) 421 if err != nil { 422 return err 423 } 424 425 return resourceAwsOpsworksStackRead(d, meta) 426 } 427 428 func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error { 429 client := meta.(*AWSClient).opsworksconn 430 431 req := &opsworks.DeleteStackInput{ 432 StackId: aws.String(d.Id()), 433 } 434 435 log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id()) 436 437 _, err := client.DeleteStack(req) 438 if err != nil { 439 return err 440 } 441 442 // For a stack in a VPC, OpsWorks has created some default security groups 443 // in the VPC, which it will now delete. 444 // Unfortunately, the security groups are deleted asynchronously and there 445 // is no robust way for us to determine when it is done. The VPC itself 446 // isn't deletable until the security groups are cleaned up, so this could 447 // make 'terraform destroy' fail if the VPC is also managed and we don't 448 // wait for the security groups to be deleted. 449 // There is no robust way to check for this, so we'll just wait a 450 // nominal amount of time. 451 _, inVpc := d.GetOk("vpc_id") 452 _, useOpsworksDefaultSg := d.GetOk("use_opsworks_security_group") 453 454 if inVpc && useOpsworksDefaultSg { 455 log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted") 456 time.Sleep(30 * time.Second) 457 } 458 459 return nil 460 }