github.com/bradfeehan/terraform@v0.7.0-rc3.0.20170529055808-34b45c5ad841/builtin/providers/aws/resource_aws_lambda_function.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "log" 7 "time" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/aws/awserr" 11 "github.com/aws/aws-sdk-go/service/lambda" 12 "github.com/mitchellh/go-homedir" 13 14 "errors" 15 16 "github.com/hashicorp/terraform/helper/resource" 17 "github.com/hashicorp/terraform/helper/schema" 18 "github.com/hashicorp/terraform/helper/validation" 19 ) 20 21 const awsMutexLambdaKey = `aws_lambda_function` 22 23 func resourceAwsLambdaFunction() *schema.Resource { 24 return &schema.Resource{ 25 Create: resourceAwsLambdaFunctionCreate, 26 Read: resourceAwsLambdaFunctionRead, 27 Update: resourceAwsLambdaFunctionUpdate, 28 Delete: resourceAwsLambdaFunctionDelete, 29 30 Importer: &schema.ResourceImporter{ 31 State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 32 d.Set("function_name", d.Id()) 33 return []*schema.ResourceData{d}, nil 34 }, 35 }, 36 37 Schema: map[string]*schema.Schema{ 38 "filename": { 39 Type: schema.TypeString, 40 Optional: true, 41 ConflictsWith: []string{"s3_bucket", "s3_key", "s3_object_version"}, 42 }, 43 "s3_bucket": { 44 Type: schema.TypeString, 45 Optional: true, 46 ConflictsWith: []string{"filename"}, 47 }, 48 "s3_key": { 49 Type: schema.TypeString, 50 Optional: true, 51 ConflictsWith: []string{"filename"}, 52 }, 53 "s3_object_version": { 54 Type: schema.TypeString, 55 Optional: true, 56 ConflictsWith: []string{"filename"}, 57 }, 58 "description": { 59 Type: schema.TypeString, 60 Optional: true, 61 }, 62 "dead_letter_config": { 63 Type: schema.TypeList, 64 Optional: true, 65 ForceNew: true, 66 MinItems: 0, 67 MaxItems: 1, 68 Elem: &schema.Resource{ 69 Schema: map[string]*schema.Schema{ 70 "target_arn": { 71 Type: schema.TypeString, 72 Required: true, 73 ValidateFunc: validateArn, 74 }, 75 }, 76 }, 77 }, 78 "function_name": { 79 Type: schema.TypeString, 80 Required: true, 81 ForceNew: true, 82 }, 83 "handler": { 84 Type: schema.TypeString, 85 Required: true, 86 }, 87 "memory_size": { 88 Type: schema.TypeInt, 89 Optional: true, 90 Default: 128, 91 }, 92 "role": { 93 Type: schema.TypeString, 94 Required: true, 95 }, 96 "runtime": { 97 Type: schema.TypeString, 98 Required: true, 99 ValidateFunc: validateRuntime, 100 }, 101 "timeout": { 102 Type: schema.TypeInt, 103 Optional: true, 104 Default: 3, 105 }, 106 "publish": { 107 Type: schema.TypeBool, 108 Optional: true, 109 Default: false, 110 }, 111 "version": { 112 Type: schema.TypeString, 113 Computed: true, 114 }, 115 "vpc_config": { 116 Type: schema.TypeList, 117 Optional: true, 118 ForceNew: true, 119 Elem: &schema.Resource{ 120 Schema: map[string]*schema.Schema{ 121 "subnet_ids": { 122 Type: schema.TypeSet, 123 Required: true, 124 ForceNew: true, 125 Elem: &schema.Schema{Type: schema.TypeString}, 126 Set: schema.HashString, 127 }, 128 "security_group_ids": { 129 Type: schema.TypeSet, 130 Required: true, 131 ForceNew: true, 132 Elem: &schema.Schema{Type: schema.TypeString}, 133 Set: schema.HashString, 134 }, 135 "vpc_id": { 136 Type: schema.TypeString, 137 Computed: true, 138 }, 139 }, 140 }, 141 }, 142 "arn": { 143 Type: schema.TypeString, 144 Computed: true, 145 }, 146 "qualified_arn": { 147 Type: schema.TypeString, 148 Computed: true, 149 }, 150 "invoke_arn": { 151 Type: schema.TypeString, 152 Computed: true, 153 }, 154 "last_modified": { 155 Type: schema.TypeString, 156 Computed: true, 157 }, 158 "source_code_hash": { 159 Type: schema.TypeString, 160 Optional: true, 161 Computed: true, 162 }, 163 "environment": { 164 Type: schema.TypeList, 165 Optional: true, 166 MaxItems: 1, 167 Elem: &schema.Resource{ 168 Schema: map[string]*schema.Schema{ 169 "variables": { 170 Type: schema.TypeMap, 171 Optional: true, 172 Elem: schema.TypeString, 173 }, 174 }, 175 }, 176 }, 177 178 "tracing_config": { 179 Type: schema.TypeList, 180 MaxItems: 1, 181 Optional: true, 182 Computed: true, 183 Elem: &schema.Resource{ 184 Schema: map[string]*schema.Schema{ 185 "mode": { 186 Type: schema.TypeString, 187 Required: true, 188 ValidateFunc: validation.StringInSlice([]string{"Active", "PassThrough"}, true), 189 }, 190 }, 191 }, 192 }, 193 194 "kms_key_arn": { 195 Type: schema.TypeString, 196 Optional: true, 197 ValidateFunc: validateArn, 198 }, 199 200 "tags": tagsSchema(), 201 }, 202 } 203 } 204 205 // resourceAwsLambdaFunction maps to: 206 // CreateFunction in the API / SDK 207 func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) error { 208 conn := meta.(*AWSClient).lambdaconn 209 210 functionName := d.Get("function_name").(string) 211 iamRole := d.Get("role").(string) 212 213 log.Printf("[DEBUG] Creating Lambda Function %s with role %s", functionName, iamRole) 214 215 filename, hasFilename := d.GetOk("filename") 216 s3Bucket, bucketOk := d.GetOk("s3_bucket") 217 s3Key, keyOk := d.GetOk("s3_key") 218 s3ObjectVersion, versionOk := d.GetOk("s3_object_version") 219 220 if !hasFilename && !bucketOk && !keyOk && !versionOk { 221 return errors.New("filename or s3_* attributes must be set") 222 } 223 224 var functionCode *lambda.FunctionCode 225 if hasFilename { 226 // Grab an exclusive lock so that we're only reading one function into 227 // memory at a time. 228 // See https://github.com/hashicorp/terraform/issues/9364 229 awsMutexKV.Lock(awsMutexLambdaKey) 230 defer awsMutexKV.Unlock(awsMutexLambdaKey) 231 file, err := loadFileContent(filename.(string)) 232 if err != nil { 233 return fmt.Errorf("Unable to load %q: %s", filename.(string), err) 234 } 235 functionCode = &lambda.FunctionCode{ 236 ZipFile: file, 237 } 238 } else { 239 if !bucketOk || !keyOk { 240 return errors.New("s3_bucket and s3_key must all be set while using S3 code source") 241 } 242 functionCode = &lambda.FunctionCode{ 243 S3Bucket: aws.String(s3Bucket.(string)), 244 S3Key: aws.String(s3Key.(string)), 245 } 246 if versionOk { 247 functionCode.S3ObjectVersion = aws.String(s3ObjectVersion.(string)) 248 } 249 } 250 251 params := &lambda.CreateFunctionInput{ 252 Code: functionCode, 253 Description: aws.String(d.Get("description").(string)), 254 FunctionName: aws.String(functionName), 255 Handler: aws.String(d.Get("handler").(string)), 256 MemorySize: aws.Int64(int64(d.Get("memory_size").(int))), 257 Role: aws.String(iamRole), 258 Runtime: aws.String(d.Get("runtime").(string)), 259 Timeout: aws.Int64(int64(d.Get("timeout").(int))), 260 Publish: aws.Bool(d.Get("publish").(bool)), 261 } 262 263 if v, ok := d.GetOk("dead_letter_config"); ok { 264 dlcMaps := v.([]interface{}) 265 if len(dlcMaps) == 1 { // Schema guarantees either 0 or 1 266 dlcMap := dlcMaps[0].(map[string]interface{}) 267 params.DeadLetterConfig = &lambda.DeadLetterConfig{ 268 TargetArn: aws.String(dlcMap["target_arn"].(string)), 269 } 270 } 271 } 272 273 if v, ok := d.GetOk("vpc_config"); ok { 274 config, err := validateVPCConfig(v) 275 if err != nil { 276 return err 277 } 278 279 if config != nil { 280 var subnetIds []*string 281 for _, id := range config["subnet_ids"].(*schema.Set).List() { 282 subnetIds = append(subnetIds, aws.String(id.(string))) 283 } 284 285 var securityGroupIds []*string 286 for _, id := range config["security_group_ids"].(*schema.Set).List() { 287 securityGroupIds = append(securityGroupIds, aws.String(id.(string))) 288 } 289 290 params.VpcConfig = &lambda.VpcConfig{ 291 SubnetIds: subnetIds, 292 SecurityGroupIds: securityGroupIds, 293 } 294 } 295 } 296 297 if v, ok := d.GetOk("tracing_config"); ok { 298 tracingConfig := v.([]interface{}) 299 tracing := tracingConfig[0].(map[string]interface{}) 300 params.TracingConfig = &lambda.TracingConfig{ 301 Mode: aws.String(tracing["mode"].(string)), 302 } 303 } 304 305 if v, ok := d.GetOk("environment"); ok { 306 environments := v.([]interface{}) 307 environment, ok := environments[0].(map[string]interface{}) 308 if !ok { 309 return errors.New("At least one field is expected inside environment") 310 } 311 312 if environmentVariables, ok := environment["variables"]; ok { 313 variables := readEnvironmentVariables(environmentVariables.(map[string]interface{})) 314 315 params.Environment = &lambda.Environment{ 316 Variables: aws.StringMap(variables), 317 } 318 } 319 } 320 321 if v, ok := d.GetOk("kms_key_arn"); ok { 322 params.KMSKeyArn = aws.String(v.(string)) 323 } 324 325 if v, exists := d.GetOk("tags"); exists { 326 params.Tags = tagsFromMapGeneric(v.(map[string]interface{})) 327 } 328 329 // IAM profiles can take ~10 seconds to propagate in AWS: 330 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console 331 // Error creating Lambda function: InvalidParameterValueException: The role defined for the task cannot be assumed by Lambda. 332 err := resource.Retry(10*time.Minute, func() *resource.RetryError { 333 _, err := conn.CreateFunction(params) 334 if err != nil { 335 log.Printf("[DEBUG] Error creating Lambda Function: %s", err) 336 337 if isAWSErr(err, "InvalidParameterValueException", "The role defined for the function cannot be assumed by Lambda") { 338 log.Printf("[DEBUG] Received %s, retrying CreateFunction", err) 339 return resource.RetryableError(err) 340 } 341 342 return resource.NonRetryableError(err) 343 } 344 return nil 345 }) 346 if err != nil { 347 return fmt.Errorf("Error creating Lambda function: %s", err) 348 } 349 350 d.SetId(d.Get("function_name").(string)) 351 352 return resourceAwsLambdaFunctionRead(d, meta) 353 } 354 355 // resourceAwsLambdaFunctionRead maps to: 356 // GetFunction in the API / SDK 357 func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) error { 358 conn := meta.(*AWSClient).lambdaconn 359 360 log.Printf("[DEBUG] Fetching Lambda Function: %s", d.Id()) 361 362 params := &lambda.GetFunctionInput{ 363 FunctionName: aws.String(d.Get("function_name").(string)), 364 } 365 366 getFunctionOutput, err := conn.GetFunction(params) 367 if err != nil { 368 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" && !d.IsNewResource() { 369 d.SetId("") 370 return nil 371 } 372 return err 373 } 374 375 // getFunctionOutput.Code.Location is a pre-signed URL pointing at the zip 376 // file that we uploaded when we created the resource. You can use it to 377 // download the code from AWS. The other part is 378 // getFunctionOutput.Configuration which holds metadata. 379 380 function := getFunctionOutput.Configuration 381 // TODO error checking / handling on the Set() calls. 382 d.Set("arn", function.FunctionArn) 383 d.Set("description", function.Description) 384 d.Set("handler", function.Handler) 385 d.Set("memory_size", function.MemorySize) 386 d.Set("last_modified", function.LastModified) 387 d.Set("role", function.Role) 388 d.Set("runtime", function.Runtime) 389 d.Set("timeout", function.Timeout) 390 d.Set("kms_key_arn", function.KMSKeyArn) 391 d.Set("tags", tagsToMapGeneric(getFunctionOutput.Tags)) 392 393 config := flattenLambdaVpcConfigResponse(function.VpcConfig) 394 log.Printf("[INFO] Setting Lambda %s VPC config %#v from API", d.Id(), config) 395 vpcSetErr := d.Set("vpc_config", config) 396 if vpcSetErr != nil { 397 return fmt.Errorf("Failed setting vpc_config: %s", vpcSetErr) 398 } 399 400 d.Set("source_code_hash", function.CodeSha256) 401 402 if err := d.Set("environment", flattenLambdaEnvironment(function.Environment)); err != nil { 403 log.Printf("[ERR] Error setting environment for Lambda Function (%s): %s", d.Id(), err) 404 } 405 406 if function.DeadLetterConfig != nil && function.DeadLetterConfig.TargetArn != nil { 407 d.Set("dead_letter_config", []interface{}{ 408 map[string]interface{}{ 409 "target_arn": *function.DeadLetterConfig.TargetArn, 410 }, 411 }) 412 } else { 413 d.Set("dead_letter_config", []interface{}{}) 414 } 415 416 if function.TracingConfig != nil { 417 d.Set("tracing_config", []interface{}{ 418 map[string]interface{}{ 419 "mode": *function.TracingConfig.Mode, 420 }, 421 }) 422 } 423 424 // List is sorted from oldest to latest 425 // so this may get costly over time :'( 426 var lastVersion, lastQualifiedArn string 427 err = listVersionsByFunctionPages(conn, &lambda.ListVersionsByFunctionInput{ 428 FunctionName: function.FunctionName, 429 MaxItems: aws.Int64(10000), 430 }, func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool { 431 if lastPage { 432 last := p.Versions[len(p.Versions)-1] 433 lastVersion = *last.Version 434 lastQualifiedArn = *last.FunctionArn 435 return false 436 } 437 return true 438 }) 439 if err != nil { 440 return err 441 } 442 443 d.Set("version", lastVersion) 444 d.Set("qualified_arn", lastQualifiedArn) 445 446 d.Set("invoke_arn", buildLambdaInvokeArn(*function.FunctionArn, meta.(*AWSClient).region)) 447 448 return nil 449 } 450 451 func listVersionsByFunctionPages(c *lambda.Lambda, input *lambda.ListVersionsByFunctionInput, 452 fn func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool) error { 453 for { 454 page, err := c.ListVersionsByFunction(input) 455 if err != nil { 456 return err 457 } 458 lastPage := page.NextMarker == nil 459 460 shouldContinue := fn(page, lastPage) 461 if !shouldContinue || lastPage { 462 break 463 } 464 input.Marker = page.NextMarker 465 } 466 return nil 467 } 468 469 // resourceAwsLambdaFunction maps to: 470 // DeleteFunction in the API / SDK 471 func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) error { 472 conn := meta.(*AWSClient).lambdaconn 473 474 log.Printf("[INFO] Deleting Lambda Function: %s", d.Id()) 475 476 params := &lambda.DeleteFunctionInput{ 477 FunctionName: aws.String(d.Get("function_name").(string)), 478 } 479 480 _, err := conn.DeleteFunction(params) 481 if err != nil { 482 return fmt.Errorf("Error deleting Lambda Function: %s", err) 483 } 484 485 d.SetId("") 486 487 return nil 488 } 489 490 // resourceAwsLambdaFunctionUpdate maps to: 491 // UpdateFunctionCode in the API / SDK 492 func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error { 493 conn := meta.(*AWSClient).lambdaconn 494 495 d.Partial(true) 496 497 arn := d.Get("arn").(string) 498 if tagErr := setTagsLambda(conn, d, arn); tagErr != nil { 499 return tagErr 500 } 501 d.SetPartial("tags") 502 503 if d.HasChange("filename") || d.HasChange("source_code_hash") || d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") { 504 codeReq := &lambda.UpdateFunctionCodeInput{ 505 FunctionName: aws.String(d.Id()), 506 Publish: aws.Bool(d.Get("publish").(bool)), 507 } 508 509 if v, ok := d.GetOk("filename"); ok { 510 // Grab an exclusive lock so that we're only reading one function into 511 // memory at a time. 512 // See https://github.com/hashicorp/terraform/issues/9364 513 awsMutexKV.Lock(awsMutexLambdaKey) 514 defer awsMutexKV.Unlock(awsMutexLambdaKey) 515 file, err := loadFileContent(v.(string)) 516 if err != nil { 517 return fmt.Errorf("Unable to load %q: %s", v.(string), err) 518 } 519 codeReq.ZipFile = file 520 } else { 521 s3Bucket, _ := d.GetOk("s3_bucket") 522 s3Key, _ := d.GetOk("s3_key") 523 s3ObjectVersion, versionOk := d.GetOk("s3_object_version") 524 525 codeReq.S3Bucket = aws.String(s3Bucket.(string)) 526 codeReq.S3Key = aws.String(s3Key.(string)) 527 if versionOk { 528 codeReq.S3ObjectVersion = aws.String(s3ObjectVersion.(string)) 529 } 530 } 531 532 log.Printf("[DEBUG] Send Update Lambda Function Code request: %#v", codeReq) 533 534 _, err := conn.UpdateFunctionCode(codeReq) 535 if err != nil { 536 return fmt.Errorf("Error modifying Lambda Function Code %s: %s", d.Id(), err) 537 } 538 539 d.SetPartial("filename") 540 d.SetPartial("source_code_hash") 541 d.SetPartial("s3_bucket") 542 d.SetPartial("s3_key") 543 d.SetPartial("s3_object_version") 544 } 545 546 configReq := &lambda.UpdateFunctionConfigurationInput{ 547 FunctionName: aws.String(d.Id()), 548 } 549 550 configUpdate := false 551 if d.HasChange("description") { 552 configReq.Description = aws.String(d.Get("description").(string)) 553 configUpdate = true 554 } 555 if d.HasChange("handler") { 556 configReq.Handler = aws.String(d.Get("handler").(string)) 557 configUpdate = true 558 } 559 if d.HasChange("memory_size") { 560 configReq.MemorySize = aws.Int64(int64(d.Get("memory_size").(int))) 561 configUpdate = true 562 } 563 if d.HasChange("role") { 564 configReq.Role = aws.String(d.Get("role").(string)) 565 configUpdate = true 566 } 567 if d.HasChange("timeout") { 568 configReq.Timeout = aws.Int64(int64(d.Get("timeout").(int))) 569 configUpdate = true 570 } 571 if d.HasChange("kms_key_arn") { 572 configReq.KMSKeyArn = aws.String(d.Get("kms_key_arn").(string)) 573 configUpdate = true 574 } 575 if d.HasChange("dead_letter_config") { 576 dlcMaps := d.Get("dead_letter_config").([]interface{}) 577 if len(dlcMaps) == 1 { // Schema guarantees either 0 or 1 578 dlcMap := dlcMaps[0].(map[string]interface{}) 579 configReq.DeadLetterConfig = &lambda.DeadLetterConfig{ 580 TargetArn: aws.String(dlcMap["target_arn"].(string)), 581 } 582 configUpdate = true 583 } 584 } 585 if d.HasChange("tracing_config") { 586 tracingConfig := d.Get("tracing_config").([]interface{}) 587 if len(tracingConfig) == 1 { // Schema guarantees either 0 or 1 588 config := tracingConfig[0].(map[string]interface{}) 589 configReq.TracingConfig = &lambda.TracingConfig{ 590 Mode: aws.String(config["mode"].(string)), 591 } 592 configUpdate = true 593 } 594 } 595 if d.HasChange("runtime") { 596 configReq.Runtime = aws.String(d.Get("runtime").(string)) 597 configUpdate = true 598 } 599 if d.HasChange("environment") { 600 if v, ok := d.GetOk("environment"); ok { 601 environments := v.([]interface{}) 602 environment, ok := environments[0].(map[string]interface{}) 603 if !ok { 604 return errors.New("At least one field is expected inside environment") 605 } 606 607 if environmentVariables, ok := environment["variables"]; ok { 608 variables := readEnvironmentVariables(environmentVariables.(map[string]interface{})) 609 610 configReq.Environment = &lambda.Environment{ 611 Variables: aws.StringMap(variables), 612 } 613 configUpdate = true 614 } 615 } else { 616 configReq.Environment = &lambda.Environment{ 617 Variables: aws.StringMap(map[string]string{}), 618 } 619 configUpdate = true 620 } 621 } 622 623 if configUpdate { 624 log.Printf("[DEBUG] Send Update Lambda Function Configuration request: %#v", configReq) 625 _, err := conn.UpdateFunctionConfiguration(configReq) 626 if err != nil { 627 return fmt.Errorf("Error modifying Lambda Function Configuration %s: %s", d.Id(), err) 628 } 629 d.SetPartial("description") 630 d.SetPartial("handler") 631 d.SetPartial("memory_size") 632 d.SetPartial("role") 633 d.SetPartial("timeout") 634 } 635 d.Partial(false) 636 637 return resourceAwsLambdaFunctionRead(d, meta) 638 } 639 640 // loadFileContent returns contents of a file in a given path 641 func loadFileContent(v string) ([]byte, error) { 642 filename, err := homedir.Expand(v) 643 if err != nil { 644 return nil, err 645 } 646 fileContent, err := ioutil.ReadFile(filename) 647 if err != nil { 648 return nil, err 649 } 650 return fileContent, nil 651 } 652 653 func readEnvironmentVariables(ev map[string]interface{}) map[string]string { 654 variables := make(map[string]string) 655 for k, v := range ev { 656 variables[k] = v.(string) 657 } 658 659 return variables 660 } 661 662 func validateVPCConfig(v interface{}) (map[string]interface{}, error) { 663 configs := v.([]interface{}) 664 if len(configs) > 1 { 665 return nil, errors.New("Only a single vpc_config block is expected") 666 } 667 668 config, ok := configs[0].(map[string]interface{}) 669 670 if !ok { 671 return nil, errors.New("vpc_config is <nil>") 672 } 673 674 // if subnet_ids and security_group_ids are both empty then the VPC is optional 675 if config["subnet_ids"].(*schema.Set).Len() == 0 && config["security_group_ids"].(*schema.Set).Len() == 0 { 676 return nil, nil 677 } 678 679 if config["subnet_ids"].(*schema.Set).Len() == 0 { 680 return nil, errors.New("vpc_config.subnet_ids cannot be empty") 681 } 682 683 if config["security_group_ids"].(*schema.Set).Len() == 0 { 684 return nil, errors.New("vpc_config.security_group_ids cannot be empty") 685 } 686 687 return config, nil 688 } 689 690 func validateRuntime(v interface{}, k string) (ws []string, errors []error) { 691 runtime := v.(string) 692 693 if runtime == lambda.RuntimeNodejs { 694 errors = append(errors, fmt.Errorf( 695 "%s has reached end of life since October 2016 and has been deprecated in favor of %s.", 696 runtime, lambda.RuntimeNodejs43)) 697 } 698 return 699 }