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