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