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