github.com/turtlemonvh/terraform@v0.6.9-0.20151204001754-8e40b6b855e8/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 if v.SshKey != nil { 235 m["ssh_key"] = *v.SshKey 236 } 237 nv = append(nv, m) 238 } 239 240 err := d.Set("custom_cookbooks_source", nv) 241 if err != nil { 242 // should never happen 243 panic(err) 244 } 245 } 246 247 func resourceAwsOpsworksStackRead(d *schema.ResourceData, meta interface{}) error { 248 client := meta.(*AWSClient).opsworksconn 249 250 req := &opsworks.DescribeStacksInput{ 251 StackIds: []*string{ 252 aws.String(d.Id()), 253 }, 254 } 255 256 log.Printf("[DEBUG] Reading OpsWorks stack: %s", d.Id()) 257 258 resp, err := client.DescribeStacks(req) 259 if err != nil { 260 if awserr, ok := err.(awserr.Error); ok { 261 if awserr.Code() == "ResourceNotFoundException" { 262 d.SetId("") 263 return nil 264 } 265 } 266 return err 267 } 268 269 stack := resp.Stacks[0] 270 d.Set("name", stack.Name) 271 d.Set("region", stack.Region) 272 d.Set("default_instance_profile_arn", stack.DefaultInstanceProfileArn) 273 d.Set("service_role_arn", stack.ServiceRoleArn) 274 d.Set("default_availability_zone", stack.DefaultAvailabilityZone) 275 d.Set("default_os", stack.DefaultOs) 276 d.Set("default_root_device_type", stack.DefaultRootDeviceType) 277 d.Set("default_ssh_key_name", stack.DefaultSshKeyName) 278 d.Set("default_subnet_id", stack.DefaultSubnetId) 279 d.Set("hostname_theme", stack.HostnameTheme) 280 d.Set("use_custom_cookbooks", stack.UseCustomCookbooks) 281 d.Set("use_opsworks_security_groups", stack.UseOpsworksSecurityGroups) 282 d.Set("vpc_id", stack.VpcId) 283 if color, ok := stack.Attributes["Color"]; ok { 284 d.Set("color", color) 285 } 286 if stack.ConfigurationManager != nil { 287 d.Set("configuration_manager_name", stack.ConfigurationManager.Name) 288 d.Set("configuration_manager_version", stack.ConfigurationManager.Version) 289 } 290 if stack.ChefConfiguration != nil { 291 d.Set("berkshelf_version", stack.ChefConfiguration.BerkshelfVersion) 292 d.Set("manage_berkshelf", stack.ChefConfiguration.ManageBerkshelf) 293 } 294 resourceAwsOpsworksSetStackCustomCookbooksSource(d, stack.CustomCookbooksSource) 295 296 return nil 297 } 298 299 func resourceAwsOpsworksStackCreate(d *schema.ResourceData, meta interface{}) error { 300 client := meta.(*AWSClient).opsworksconn 301 302 err := resourceAwsOpsworksStackValidate(d) 303 if err != nil { 304 return err 305 } 306 307 req := &opsworks.CreateStackInput{ 308 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 309 Name: aws.String(d.Get("name").(string)), 310 Region: aws.String(d.Get("region").(string)), 311 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 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.Name) 326 327 var resp *opsworks.CreateStackOutput 328 err = resource.Retry(20*time.Minute, func() error { 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 if opserr.Code() == "ValidationException" && strings.Contains(opserr.Message(), "not yet propagated") { 343 log.Printf("[INFO] Waiting for service IAM role to propagate") 344 return cerr 345 } 346 } 347 return resource.RetryError{Err: cerr} 348 } 349 return nil 350 }) 351 if err != nil { 352 return err 353 } 354 355 stackId := *resp.StackId 356 d.SetId(stackId) 357 d.Set("id", stackId) 358 359 if inVpc { 360 // For VPC-based stacks, OpsWorks asynchronously creates some default 361 // security groups which must exist before layers can be created. 362 // Unfortunately it doesn't tell us what the ids of these are, so 363 // we can't actually check for them. Instead, we just wait a nominal 364 // amount of time for their creation to complete. 365 log.Print("[INFO] Waiting for OpsWorks built-in security groups to be created") 366 time.Sleep(30 * time.Second) 367 } 368 369 return resourceAwsOpsworksStackUpdate(d, meta) 370 } 371 372 func resourceAwsOpsworksStackUpdate(d *schema.ResourceData, meta interface{}) error { 373 client := meta.(*AWSClient).opsworksconn 374 375 err := resourceAwsOpsworksStackValidate(d) 376 if err != nil { 377 return err 378 } 379 380 req := &opsworks.UpdateStackInput{ 381 CustomJson: aws.String(d.Get("custom_json").(string)), 382 DefaultInstanceProfileArn: aws.String(d.Get("default_instance_profile_arn").(string)), 383 DefaultRootDeviceType: aws.String(d.Get("default_root_device_type").(string)), 384 DefaultSshKeyName: aws.String(d.Get("default_ssh_key_name").(string)), 385 Name: aws.String(d.Get("name").(string)), 386 ServiceRoleArn: aws.String(d.Get("service_role_arn").(string)), 387 StackId: aws.String(d.Id()), 388 UseCustomCookbooks: aws.Bool(d.Get("use_custom_cookbooks").(bool)), 389 UseOpsworksSecurityGroups: aws.Bool(d.Get("use_opsworks_security_groups").(bool)), 390 Attributes: make(map[string]*string), 391 CustomCookbooksSource: resourceAwsOpsworksStackCustomCookbooksSource(d), 392 } 393 if v, ok := d.GetOk("default_os"); ok { 394 req.DefaultOs = aws.String(v.(string)) 395 } 396 if v, ok := d.GetOk("default_subnet_id"); ok { 397 req.DefaultSubnetId = aws.String(v.(string)) 398 } 399 if v, ok := d.GetOk("default_availability_zone"); ok { 400 req.DefaultAvailabilityZone = aws.String(v.(string)) 401 } 402 if v, ok := d.GetOk("hostname_theme"); ok { 403 req.HostnameTheme = aws.String(v.(string)) 404 } 405 if v, ok := d.GetOk("color"); ok { 406 req.Attributes["Color"] = aws.String(v.(string)) 407 } 408 req.ChefConfiguration = &opsworks.ChefConfiguration{ 409 BerkshelfVersion: aws.String(d.Get("berkshelf_version").(string)), 410 ManageBerkshelf: aws.Bool(d.Get("manage_berkshelf").(bool)), 411 } 412 req.ConfigurationManager = &opsworks.StackConfigurationManager{ 413 Name: aws.String(d.Get("configuration_manager_name").(string)), 414 Version: aws.String(d.Get("configuration_manager_version").(string)), 415 } 416 417 log.Printf("[DEBUG] Updating OpsWorks stack: %s", d.Id()) 418 419 _, err = client.UpdateStack(req) 420 if err != nil { 421 return err 422 } 423 424 return resourceAwsOpsworksStackRead(d, meta) 425 } 426 427 func resourceAwsOpsworksStackDelete(d *schema.ResourceData, meta interface{}) error { 428 client := meta.(*AWSClient).opsworksconn 429 430 req := &opsworks.DeleteStackInput{ 431 StackId: aws.String(d.Id()), 432 } 433 434 log.Printf("[DEBUG] Deleting OpsWorks stack: %s", d.Id()) 435 436 _, err := client.DeleteStack(req) 437 if err != nil { 438 return err 439 } 440 441 // For a stack in a VPC, OpsWorks has created some default security groups 442 // in the VPC, which it will now delete. 443 // Unfortunately, the security groups are deleted asynchronously and there 444 // is no robust way for us to determine when it is done. The VPC itself 445 // isn't deletable until the security groups are cleaned up, so this could 446 // make 'terraform destroy' fail if the VPC is also managed and we don't 447 // wait for the security groups to be deleted. 448 // There is no robust way to check for this, so we'll just wait a 449 // nominal amount of time. 450 if _, ok := d.GetOk("vpc_id"); ok { 451 log.Print("[INFO] Waiting for Opsworks built-in security groups to be deleted") 452 time.Sleep(30 * time.Second) 453 } 454 455 return nil 456 }