github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/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 // Prevent panic on nil dead_letter_config. See GH-14961 267 if dlcMaps[0] == nil { 268 return fmt.Errorf("Nil dead_letter_config supplied for function: %s", functionName) 269 } 270 dlcMap := dlcMaps[0].(map[string]interface{}) 271 params.DeadLetterConfig = &lambda.DeadLetterConfig{ 272 TargetArn: aws.String(dlcMap["target_arn"].(string)), 273 } 274 } 275 } 276 277 if v, ok := d.GetOk("vpc_config"); ok { 278 config, err := validateVPCConfig(v) 279 if err != nil { 280 return err 281 } 282 283 if config != nil { 284 var subnetIds []*string 285 for _, id := range config["subnet_ids"].(*schema.Set).List() { 286 subnetIds = append(subnetIds, aws.String(id.(string))) 287 } 288 289 var securityGroupIds []*string 290 for _, id := range config["security_group_ids"].(*schema.Set).List() { 291 securityGroupIds = append(securityGroupIds, aws.String(id.(string))) 292 } 293 294 params.VpcConfig = &lambda.VpcConfig{ 295 SubnetIds: subnetIds, 296 SecurityGroupIds: securityGroupIds, 297 } 298 } 299 } 300 301 if v, ok := d.GetOk("tracing_config"); ok { 302 tracingConfig := v.([]interface{}) 303 tracing := tracingConfig[0].(map[string]interface{}) 304 params.TracingConfig = &lambda.TracingConfig{ 305 Mode: aws.String(tracing["mode"].(string)), 306 } 307 } 308 309 if v, ok := d.GetOk("environment"); ok { 310 environments := v.([]interface{}) 311 environment, ok := environments[0].(map[string]interface{}) 312 if !ok { 313 return errors.New("At least one field is expected inside environment") 314 } 315 316 if environmentVariables, ok := environment["variables"]; ok { 317 variables := readEnvironmentVariables(environmentVariables.(map[string]interface{})) 318 319 params.Environment = &lambda.Environment{ 320 Variables: aws.StringMap(variables), 321 } 322 } 323 } 324 325 if v, ok := d.GetOk("kms_key_arn"); ok { 326 params.KMSKeyArn = aws.String(v.(string)) 327 } 328 329 if v, exists := d.GetOk("tags"); exists { 330 params.Tags = tagsFromMapGeneric(v.(map[string]interface{})) 331 } 332 333 // IAM profiles can take ~10 seconds to propagate in AWS: 334 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console 335 // Error creating Lambda function: InvalidParameterValueException: The role defined for the task cannot be assumed by Lambda. 336 err := resource.Retry(10*time.Minute, func() *resource.RetryError { 337 _, err := conn.CreateFunction(params) 338 if err != nil { 339 log.Printf("[DEBUG] Error creating Lambda Function: %s", err) 340 341 if isAWSErr(err, "InvalidParameterValueException", "The role defined for the function cannot be assumed by Lambda") { 342 log.Printf("[DEBUG] Received %s, retrying CreateFunction", err) 343 return resource.RetryableError(err) 344 } 345 if isAWSErr(err, "InvalidParameterValueException", "The provided execution role does not have permissions") { 346 log.Printf("[DEBUG] Received %s, retrying CreateFunction", err) 347 return resource.RetryableError(err) 348 } 349 350 return resource.NonRetryableError(err) 351 } 352 return nil 353 }) 354 if err != nil { 355 return fmt.Errorf("Error creating Lambda function: %s", err) 356 } 357 358 d.SetId(d.Get("function_name").(string)) 359 360 return resourceAwsLambdaFunctionRead(d, meta) 361 } 362 363 // resourceAwsLambdaFunctionRead maps to: 364 // GetFunction in the API / SDK 365 func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) error { 366 conn := meta.(*AWSClient).lambdaconn 367 368 log.Printf("[DEBUG] Fetching Lambda Function: %s", d.Id()) 369 370 params := &lambda.GetFunctionInput{ 371 FunctionName: aws.String(d.Get("function_name").(string)), 372 } 373 374 getFunctionOutput, err := conn.GetFunction(params) 375 if err != nil { 376 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" && !d.IsNewResource() { 377 d.SetId("") 378 return nil 379 } 380 return err 381 } 382 383 // getFunctionOutput.Code.Location is a pre-signed URL pointing at the zip 384 // file that we uploaded when we created the resource. You can use it to 385 // download the code from AWS. The other part is 386 // getFunctionOutput.Configuration which holds metadata. 387 388 function := getFunctionOutput.Configuration 389 // TODO error checking / handling on the Set() calls. 390 d.Set("arn", function.FunctionArn) 391 d.Set("description", function.Description) 392 d.Set("handler", function.Handler) 393 d.Set("memory_size", function.MemorySize) 394 d.Set("last_modified", function.LastModified) 395 d.Set("role", function.Role) 396 d.Set("runtime", function.Runtime) 397 d.Set("timeout", function.Timeout) 398 d.Set("kms_key_arn", function.KMSKeyArn) 399 d.Set("tags", tagsToMapGeneric(getFunctionOutput.Tags)) 400 401 config := flattenLambdaVpcConfigResponse(function.VpcConfig) 402 log.Printf("[INFO] Setting Lambda %s VPC config %#v from API", d.Id(), config) 403 vpcSetErr := d.Set("vpc_config", config) 404 if vpcSetErr != nil { 405 return fmt.Errorf("Failed setting vpc_config: %s", vpcSetErr) 406 } 407 408 d.Set("source_code_hash", function.CodeSha256) 409 410 if err := d.Set("environment", flattenLambdaEnvironment(function.Environment)); err != nil { 411 log.Printf("[ERR] Error setting environment for Lambda Function (%s): %s", d.Id(), err) 412 } 413 414 if function.DeadLetterConfig != nil && function.DeadLetterConfig.TargetArn != nil { 415 d.Set("dead_letter_config", []interface{}{ 416 map[string]interface{}{ 417 "target_arn": *function.DeadLetterConfig.TargetArn, 418 }, 419 }) 420 } else { 421 d.Set("dead_letter_config", []interface{}{}) 422 } 423 424 if function.TracingConfig != nil { 425 d.Set("tracing_config", []interface{}{ 426 map[string]interface{}{ 427 "mode": *function.TracingConfig.Mode, 428 }, 429 }) 430 } 431 432 // List is sorted from oldest to latest 433 // so this may get costly over time :'( 434 var lastVersion, lastQualifiedArn string 435 err = listVersionsByFunctionPages(conn, &lambda.ListVersionsByFunctionInput{ 436 FunctionName: function.FunctionName, 437 MaxItems: aws.Int64(10000), 438 }, func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool { 439 if lastPage { 440 last := p.Versions[len(p.Versions)-1] 441 lastVersion = *last.Version 442 lastQualifiedArn = *last.FunctionArn 443 return false 444 } 445 return true 446 }) 447 if err != nil { 448 return err 449 } 450 451 d.Set("version", lastVersion) 452 d.Set("qualified_arn", lastQualifiedArn) 453 454 d.Set("invoke_arn", buildLambdaInvokeArn(*function.FunctionArn, meta.(*AWSClient).region)) 455 456 return nil 457 } 458 459 func listVersionsByFunctionPages(c *lambda.Lambda, input *lambda.ListVersionsByFunctionInput, 460 fn func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool) error { 461 for { 462 page, err := c.ListVersionsByFunction(input) 463 if err != nil { 464 return err 465 } 466 lastPage := page.NextMarker == nil 467 468 shouldContinue := fn(page, lastPage) 469 if !shouldContinue || lastPage { 470 break 471 } 472 input.Marker = page.NextMarker 473 } 474 return nil 475 } 476 477 // resourceAwsLambdaFunction maps to: 478 // DeleteFunction in the API / SDK 479 func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) error { 480 conn := meta.(*AWSClient).lambdaconn 481 482 log.Printf("[INFO] Deleting Lambda Function: %s", d.Id()) 483 484 params := &lambda.DeleteFunctionInput{ 485 FunctionName: aws.String(d.Get("function_name").(string)), 486 } 487 488 _, err := conn.DeleteFunction(params) 489 if err != nil { 490 return fmt.Errorf("Error deleting Lambda Function: %s", err) 491 } 492 493 d.SetId("") 494 495 return nil 496 } 497 498 // resourceAwsLambdaFunctionUpdate maps to: 499 // UpdateFunctionCode in the API / SDK 500 func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error { 501 conn := meta.(*AWSClient).lambdaconn 502 503 d.Partial(true) 504 505 arn := d.Get("arn").(string) 506 if tagErr := setTagsLambda(conn, d, arn); tagErr != nil { 507 return tagErr 508 } 509 d.SetPartial("tags") 510 511 if d.HasChange("filename") || d.HasChange("source_code_hash") || d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") { 512 codeReq := &lambda.UpdateFunctionCodeInput{ 513 FunctionName: aws.String(d.Id()), 514 Publish: aws.Bool(d.Get("publish").(bool)), 515 } 516 517 if v, ok := d.GetOk("filename"); ok { 518 // Grab an exclusive lock so that we're only reading one function into 519 // memory at a time. 520 // See https://github.com/hashicorp/terraform/issues/9364 521 awsMutexKV.Lock(awsMutexLambdaKey) 522 defer awsMutexKV.Unlock(awsMutexLambdaKey) 523 file, err := loadFileContent(v.(string)) 524 if err != nil { 525 return fmt.Errorf("Unable to load %q: %s", v.(string), err) 526 } 527 codeReq.ZipFile = file 528 } else { 529 s3Bucket, _ := d.GetOk("s3_bucket") 530 s3Key, _ := d.GetOk("s3_key") 531 s3ObjectVersion, versionOk := d.GetOk("s3_object_version") 532 533 codeReq.S3Bucket = aws.String(s3Bucket.(string)) 534 codeReq.S3Key = aws.String(s3Key.(string)) 535 if versionOk { 536 codeReq.S3ObjectVersion = aws.String(s3ObjectVersion.(string)) 537 } 538 } 539 540 log.Printf("[DEBUG] Send Update Lambda Function Code request: %#v", codeReq) 541 542 _, err := conn.UpdateFunctionCode(codeReq) 543 if err != nil { 544 return fmt.Errorf("Error modifying Lambda Function Code %s: %s", d.Id(), err) 545 } 546 547 d.SetPartial("filename") 548 d.SetPartial("source_code_hash") 549 d.SetPartial("s3_bucket") 550 d.SetPartial("s3_key") 551 d.SetPartial("s3_object_version") 552 } 553 554 configReq := &lambda.UpdateFunctionConfigurationInput{ 555 FunctionName: aws.String(d.Id()), 556 } 557 558 configUpdate := false 559 if d.HasChange("description") { 560 configReq.Description = aws.String(d.Get("description").(string)) 561 configUpdate = true 562 } 563 if d.HasChange("handler") { 564 configReq.Handler = aws.String(d.Get("handler").(string)) 565 configUpdate = true 566 } 567 if d.HasChange("memory_size") { 568 configReq.MemorySize = aws.Int64(int64(d.Get("memory_size").(int))) 569 configUpdate = true 570 } 571 if d.HasChange("role") { 572 configReq.Role = aws.String(d.Get("role").(string)) 573 configUpdate = true 574 } 575 if d.HasChange("timeout") { 576 configReq.Timeout = aws.Int64(int64(d.Get("timeout").(int))) 577 configUpdate = true 578 } 579 if d.HasChange("kms_key_arn") { 580 configReq.KMSKeyArn = aws.String(d.Get("kms_key_arn").(string)) 581 configUpdate = true 582 } 583 if d.HasChange("dead_letter_config") { 584 dlcMaps := d.Get("dead_letter_config").([]interface{}) 585 if len(dlcMaps) == 1 { // Schema guarantees either 0 or 1 586 dlcMap := dlcMaps[0].(map[string]interface{}) 587 configReq.DeadLetterConfig = &lambda.DeadLetterConfig{ 588 TargetArn: aws.String(dlcMap["target_arn"].(string)), 589 } 590 configUpdate = true 591 } 592 } 593 if d.HasChange("tracing_config") { 594 tracingConfig := d.Get("tracing_config").([]interface{}) 595 if len(tracingConfig) == 1 { // Schema guarantees either 0 or 1 596 config := tracingConfig[0].(map[string]interface{}) 597 configReq.TracingConfig = &lambda.TracingConfig{ 598 Mode: aws.String(config["mode"].(string)), 599 } 600 configUpdate = true 601 } 602 } 603 if d.HasChange("runtime") { 604 configReq.Runtime = aws.String(d.Get("runtime").(string)) 605 configUpdate = true 606 } 607 if d.HasChange("environment") { 608 if v, ok := d.GetOk("environment"); ok { 609 environments := v.([]interface{}) 610 environment, ok := environments[0].(map[string]interface{}) 611 if !ok { 612 return errors.New("At least one field is expected inside environment") 613 } 614 615 if environmentVariables, ok := environment["variables"]; ok { 616 variables := readEnvironmentVariables(environmentVariables.(map[string]interface{})) 617 618 configReq.Environment = &lambda.Environment{ 619 Variables: aws.StringMap(variables), 620 } 621 configUpdate = true 622 } 623 } else { 624 configReq.Environment = &lambda.Environment{ 625 Variables: aws.StringMap(map[string]string{}), 626 } 627 configUpdate = true 628 } 629 } 630 631 if configUpdate { 632 log.Printf("[DEBUG] Send Update Lambda Function Configuration request: %#v", configReq) 633 _, err := conn.UpdateFunctionConfiguration(configReq) 634 if err != nil { 635 return fmt.Errorf("Error modifying Lambda Function Configuration %s: %s", d.Id(), err) 636 } 637 d.SetPartial("description") 638 d.SetPartial("handler") 639 d.SetPartial("memory_size") 640 d.SetPartial("role") 641 d.SetPartial("timeout") 642 } 643 d.Partial(false) 644 645 return resourceAwsLambdaFunctionRead(d, meta) 646 } 647 648 // loadFileContent returns contents of a file in a given path 649 func loadFileContent(v string) ([]byte, error) { 650 filename, err := homedir.Expand(v) 651 if err != nil { 652 return nil, err 653 } 654 fileContent, err := ioutil.ReadFile(filename) 655 if err != nil { 656 return nil, err 657 } 658 return fileContent, nil 659 } 660 661 func readEnvironmentVariables(ev map[string]interface{}) map[string]string { 662 variables := make(map[string]string) 663 for k, v := range ev { 664 variables[k] = v.(string) 665 } 666 667 return variables 668 } 669 670 func validateVPCConfig(v interface{}) (map[string]interface{}, error) { 671 configs := v.([]interface{}) 672 if len(configs) > 1 { 673 return nil, errors.New("Only a single vpc_config block is expected") 674 } 675 676 config, ok := configs[0].(map[string]interface{}) 677 678 if !ok { 679 return nil, errors.New("vpc_config is <nil>") 680 } 681 682 // if subnet_ids and security_group_ids are both empty then the VPC is optional 683 if config["subnet_ids"].(*schema.Set).Len() == 0 && config["security_group_ids"].(*schema.Set).Len() == 0 { 684 return nil, nil 685 } 686 687 if config["subnet_ids"].(*schema.Set).Len() == 0 { 688 return nil, errors.New("vpc_config.subnet_ids cannot be empty") 689 } 690 691 if config["security_group_ids"].(*schema.Set).Len() == 0 { 692 return nil, errors.New("vpc_config.security_group_ids cannot be empty") 693 } 694 695 return config, nil 696 } 697 698 func validateRuntime(v interface{}, k string) (ws []string, errors []error) { 699 runtime := v.(string) 700 701 if runtime == lambda.RuntimeNodejs { 702 errors = append(errors, fmt.Errorf( 703 "%s has reached end of life since October 2016 and has been deprecated in favor of %s.", 704 runtime, lambda.RuntimeNodejs43)) 705 } 706 return 707 }