From db9fbe67fad68202cc229ab24cd17597429708bb Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 22 Feb 2017 17:50:10 -0800 Subject: [PATCH] provider/aws: Lambda DeadLetterConfig support This feature allows sending a notification to either an SQS queue or an SNS topic when an error occurs running an AWS Lambda function. This fixes #10630. --- .../aws/resource_aws_lambda_function.go | 46 ++++++++++++++ .../aws/resource_aws_lambda_function_test.go | 61 +++++++++++++++++++ .../aws/r/lambda_function.html.markdown | 8 +++ 3 files changed, 115 insertions(+) diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index cb9b19db7..2e0001d07 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -58,6 +58,22 @@ func resourceAwsLambdaFunction() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "dead_letter_config": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MinItems: 0, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + }, + }, + }, "function_name": { Type: schema.TypeString, Required: true, @@ -222,6 +238,16 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e Publish: aws.Bool(d.Get("publish").(bool)), } + if v, ok := d.GetOk("dead_letter_config"); ok { + dlcMaps := v.([]interface{}) + if len(dlcMaps) == 1 { // Schema guarantees either 0 or 1 + dlcMap := dlcMaps[0].(map[string]interface{}) + params.DeadLetterConfig = &lambda.DeadLetterConfig{ + TargetArn: aws.String(dlcMap["target_arn"].(string)), + } + } + } + if v, ok := d.GetOk("vpc_config"); ok { config, err := validateVPCConfig(v) if err != nil { @@ -343,6 +369,16 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err log.Printf("[ERR] Error setting environment for Lambda Function (%s): %s", d.Id(), err) } + if function.DeadLetterConfig != nil && function.DeadLetterConfig.TargetArn != nil { + d.Set("dead_letter_config", []interface{}{ + map[string]interface{}{ + "target_arn": *function.DeadLetterConfig.TargetArn, + }, + }) + } else { + d.Set("dead_letter_config", []interface{}{}) + } + // List is sorted from oldest to latest // so this may get costly over time :'( var lastVersion, lastQualifiedArn string @@ -485,6 +521,16 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e configReq.KMSKeyArn = aws.String(d.Get("kms_key_arn").(string)) configUpdate = true } + if d.HasChange("dead_letter_config") { + dlcMaps := d.Get("dead_letter_config").([]interface{}) + if len(dlcMaps) == 1 { // Schema guarantees either 0 or 1 + dlcMap := dlcMaps[0].(map[string]interface{}) + configReq.DeadLetterConfig = &lambda.DeadLetterConfig{ + TargetArn: aws.String(dlcMap["target_arn"].(string)), + } + configUpdate = true + } + } if d.HasChange("environment") { if v, ok := d.GetOk("environment"); ok { environments := v.([]interface{}) diff --git a/builtin/providers/aws/resource_aws_lambda_function_test.go b/builtin/providers/aws/resource_aws_lambda_function_test.go index 552970c81..5575adcca 100644 --- a/builtin/providers/aws/resource_aws_lambda_function_test.go +++ b/builtin/providers/aws/resource_aws_lambda_function_test.go @@ -172,6 +172,37 @@ func TestAccAWSLambdaFunction_versioned(t *testing.T) { }) } +func TestAccAWSLambdaFunction_DeadLetterConfig(t *testing.T) { + var conf lambda.GetFunctionOutput + + rSt := acctest.RandString(5) + rName := fmt.Sprintf("tf_test_%s", rSt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLambdaConfigWithDeadLetterConfig(rName, rSt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf), + testAccCheckAwsLambdaFunctionName(&conf, rName), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName), + func(s *terraform.State) error { + if !strings.HasSuffix(*conf.Configuration.DeadLetterConfig.TargetArn, ":"+rName) { + return fmt.Errorf( + "Expected DeadLetterConfig.TargetArn %s to have suffix %s", *conf.Configuration.DeadLetterConfig.TargetArn, ":"+rName, + ) + } + return nil + }, + ), + }, + }, + }) +} + func TestAccAWSLambdaFunction_VPC(t *testing.T) { var conf lambda.GetFunctionOutput @@ -681,6 +712,15 @@ resource "aws_iam_role_policy" "iam_policy_for_lambda" { "Resource": [ "*" ] + }, + { + "Effect": "Allow", + "Action": [ + "SNS:Publish" + ], + "Resource": [ + "*" + ] } ] } @@ -879,6 +919,27 @@ resource "aws_lambda_function" "lambda_function_test" { `, rName) } +func testAccAWSLambdaConfigWithDeadLetterConfig(rName, rSt string) string { + return fmt.Sprintf(baseAccAWSLambdaConfig(rSt)+` +resource "aws_lambda_function" "lambda_function_test" { + filename = "test-fixtures/lambdatest.zip" + function_name = "%s" + role = "${aws_iam_role.iam_for_lambda.arn}" + handler = "exports.example" + runtime = "nodejs4.3" + + dead_letter_config { + target_arn = "${aws_sns_topic.lambda_function_test.arn}" + } +} + +resource "aws_sns_topic" "lambda_function_test" { + name = "%s" +} + +`, rName, rName) +} + func testAccAWSLambdaConfigWithVPC(rName, rSt string) string { return fmt.Sprintf(baseAccAWSLambdaConfig(rSt)+` resource "aws_lambda_function" "lambda_function_test" { diff --git a/website/source/docs/providers/aws/r/lambda_function.html.markdown b/website/source/docs/providers/aws/r/lambda_function.html.markdown index 92e375a67..dbffc6cc2 100644 --- a/website/source/docs/providers/aws/r/lambda_function.html.markdown +++ b/website/source/docs/providers/aws/r/lambda_function.html.markdown @@ -57,6 +57,7 @@ resource "aws_lambda_function" "test_lambda" { * `s3_key` - (Optional) The S3 key containing your lambda function source code. Conflicts with `filename`. * `s3_object_version` - (Optional) The object version of your lambda function source code. Conflicts with `filename`. * `function_name` - (Required) A unique name for your Lambda Function. +* `dead_letter_config` - (Optional) Nested block to configure the function's *dead letter queue*. See details below. * `handler` - (Required) The function [entrypoint][3] in your code. * `role` - (Required) IAM role attached to the Lambda Function. This governs both who / what can invoke your Lambda Function, as well as what resources our Lambda Function has access to. See [Lambda Permission Model][4] for more details. * `description` - (Optional) Description of what your Lambda Function does. @@ -70,6 +71,13 @@ resource "aws_lambda_function" "test_lambda" { * `source_code_hash` - (Optional) Used to trigger updates. This is only useful in conjunction with `filename`. The only useful value is `${base64sha256(file("file.zip"))}`. +**dead\_letter\_config** is a child block with a single argument: + +* `target_arn` - (Required) The ARN of an SNS topic or SQS queue to notify when an invocation fails. If this + option is used, the function's IAM role must be granted suitable access to write to the target object, + which means allowing either the `sns:Publish` or `sqs:SendMessage` action on this ARN, depending on + which service is targeted. + **vpc\_config** requires the following: * `subnet_ids` - (Required) A list of subnet IDs associated with the Lambda function.