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