Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_lambda_alias: Support Traffic Shifting #3316

Merged
merged 5 commits into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions aws/resource_aws_lambda_alias.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package aws

import (
"errors"
"fmt"
"log"
"strings"
Expand Down Expand Up @@ -39,6 +40,20 @@ func resourceAwsLambdaAlias() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"routing_config": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"additional_version_weights": {
Type: schema.TypeMap,
Optional: true,
Elem: schema.TypeFloat,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this still works today, Elem should only contain *schema.Schema or *schema.Resource 👍 In this case, it can be fixed with:

Elem: &schema.Schema{Type: schema.TypeFloat},

},
},
},
},
},
}
}
Expand All @@ -58,6 +73,20 @@ func resourceAwsLambdaAliasCreate(d *schema.ResourceData, meta interface{}) erro
FunctionName: aws.String(functionName),
FunctionVersion: aws.String(d.Get("function_version").(string)),
Name: aws.String(aliasName),
RoutingConfig: &lambda.AliasRoutingConfiguration{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including the optional and empty AliasRoutingConfiguration{} here seems extraneous during the create function -- more generally though, we prefer to move the logic to populate a parameter like this into an "expandServiceThing()" type method, which can be reused in create and update functions. e.g.

func expandLambdaAliasRoutingConfiguration(l []interface{}) *lambda.AliasRoutingConfiguration {
  aliasRoutingConfiguration := &lambda.AliasRoutingConfiguration{}

  if len(l) == 0 || l[0] == nil {
    return aliasRoutingConfiguration
  }

  m := l[0].(map[string]interface{})

  if v, ok := m["additional_version_weights"]; ok {
    aliasRoutingConfiguration.AdditionalVersionWeights = expandFloat64Map(weights)
  }

  return aliasRoutingConfiguration
}

// this should live in structure.go near expandStringList and replace readAdditionalVersionWeights
func expandFloat64Map(m map[string]interface{}) map[string]*float64 {
	float64Map := make(map[string]*float64, len(m))
	for k, v := range m {
		float64Map[k] = aws.Float64(v.(float64))
	}
	return float64Map
}

and called in create/update via: RoutingConfig: expandLambdaAliasRoutingConfiguration(d.Get("routing_config").([]interface{}))

}

if v, ok := d.GetOk("routing_config"); ok {
routingConfigs := v.([]interface{})
routingConfig, ok := routingConfigs[0].(map[string]interface{})
if !ok {
return errors.New("At least one field is expected inside routing_config")
}

if additionalVersionWeights, ok := routingConfig["additional_version_weights"]; ok {
weights := readAdditionalVersionWeights(additionalVersionWeights.(map[string]interface{}))
params.RoutingConfig.AdditionalVersionWeights = aws.Float64Map(weights)
}
}

aliasConfiguration, err := conn.CreateAlias(params)
Expand Down Expand Up @@ -97,6 +126,7 @@ func resourceAwsLambdaAliasRead(d *schema.ResourceData, meta interface{}) error
d.Set("function_version", aliasConfiguration.FunctionVersion)
d.Set("name", aliasConfiguration.Name)
d.Set("arn", aliasConfiguration.AliasArn)
d.Set("routing_config", aliasConfiguration.RoutingConfig)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using d.Set() with aggregate types (TypeList, TypeSet, TypeMap), we should perform error checking to prevent issues where the code is not properly able to set the Terraform state. e.g.

if err := d.Set("routing_config", aliasConfiguration.RoutingConfig); err != nil {
  return fmt.Errorf("error setting routing_config: %s", err)
}

Without this check and if there is a type conversion error, Terraform is silently accepting the configuration "as-is" into the Terraform state and unable to detect drift from the API should changes occur outside Terraform.

After adding this error checking, you should start receiving type errors as aliasConfiguration.RoutingConfig is a SDK object and not a []interface{}. We generally handle this via a "flattenServiceObject()" function. e.g.

func flattenLambdaAliasRoutingConfiguration(arc *lambda.AliasRoutingConfiguration) []interface{} {
	if arc == nil {
		return []interface{}{}
	}

	m := map[string]interface{}{
		"additional_version_weights": aws.Float64ValueMap(arc.AdditionalVersionWeights)
	}

	return []interface{}{m}
}

Which can be referenced via:

if err := d.Set("routing_config", flattenLambdaAliasRoutingConfiguration(aliasConfiguration.RoutingConfig)); err != nil {
  return fmt.Errorf("error setting routing_config: %s", err)
}


return nil
}
Expand Down Expand Up @@ -135,6 +165,20 @@ func resourceAwsLambdaAliasUpdate(d *schema.ResourceData, meta interface{}) erro
FunctionName: aws.String(d.Get("function_name").(string)),
FunctionVersion: aws.String(d.Get("function_version").(string)),
Name: aws.String(d.Get("name").(string)),
RoutingConfig: &lambda.AliasRoutingConfiguration{},
}

if v, ok := d.GetOk("routing_config"); ok {
routingConfigs := v.([]interface{})
routingConfig, ok := routingConfigs[0].(map[string]interface{})
if !ok {
return errors.New("At least one field is expected inside routing_config")
}

if additionalVersionWeights, ok := routingConfig["additional_version_weights"]; ok {
weights := readAdditionalVersionWeights(additionalVersionWeights.(map[string]interface{}))
params.RoutingConfig.AdditionalVersionWeights = aws.Float64Map(weights)
}
}

_, err := conn.UpdateAlias(params)
Expand All @@ -144,3 +188,12 @@ func resourceAwsLambdaAliasUpdate(d *schema.ResourceData, meta interface{}) erro

return nil
}

func readAdditionalVersionWeights(avw map[string]interface{}) map[string]float64 {
weights := make(map[string]float64)
for k, v := range avw {
weights[k] = v.(float64)
}

return weights
}
163 changes: 155 additions & 8 deletions aws/resource_aws_lambda_alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestAccAWSLambdaAlias_basic(t *testing.T) {
CheckDestroy: testAccCheckAwsLambdaAliasDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAwsLambdaAliasConfig(roleName, policyName, attachmentName, funcName, aliasName),
Config: testAccAwsLambdaAliasConfigWithoutRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsLambdaAliasExists("aws_lambda_alias.lambda_alias_test", &conf),
testAccCheckAwsLambdaAttributes(&conf),
Expand All @@ -40,6 +40,54 @@ func TestAccAWSLambdaAlias_basic(t *testing.T) {
})
}

func TestAccAWSLambdaAlias_routingConfig(t *testing.T) {
var conf lambda.AliasConfiguration

rString := acctest.RandString(8)
roleName := fmt.Sprintf("tf_acc_role_lambda_alias_basic_%s", rString)
policyName := fmt.Sprintf("tf_acc_policy_lambda_alias_basic_%s", rString)
attachmentName := fmt.Sprintf("tf_acc_attachment_%s", rString)
funcName := fmt.Sprintf("tf_acc_lambda_func_alias_basic_%s", rString)
aliasName := fmt.Sprintf("tf_acc_lambda_alias_basic_%s", rString)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsLambdaAliasDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAwsLambdaAliasConfigWithoutRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsLambdaAliasExists("aws_lambda_alias.lambda_alias_test", &conf),
testAccCheckAwsLambdaAttributes(&conf),
resource.TestMatchResourceAttr("aws_lambda_alias.lambda_alias_test", "arn",
regexp.MustCompile(`^arn:aws:lambda:[a-z]+-[a-z]+-[0-9]+:\d{12}:function:`+funcName+`:`+aliasName+`$`)),
),
},
resource.TestStep{
Config: testAccAwsLambdaAliasConfigWithRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsLambdaAliasExists("aws_lambda_alias.lambda_alias_test", &conf),
testAccCheckAwsLambdaAttributes(&conf),
testAccCheckAwsLambdaAliasRoutingConfigExists(&conf),
resource.TestMatchResourceAttr("aws_lambda_alias.lambda_alias_test", "arn",
regexp.MustCompile(`^arn:aws:lambda:[a-z]+-[a-z]+-[0-9]+:\d{12}:function:`+funcName+`:`+aliasName+`$`)),
),
},
resource.TestStep{
Config: testAccAwsLambdaAliasConfigWithoutRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsLambdaAliasExists("aws_lambda_alias.lambda_alias_test", &conf),
testAccCheckAwsLambdaAttributes(&conf),
testAccCheckAwsLambdaAliasRoutingConfigDoesNotExist(&conf),
resource.TestMatchResourceAttr("aws_lambda_alias.lambda_alias_test", "arn",
regexp.MustCompile(`^arn:aws:lambda:[a-z]+-[a-z]+-[0-9]+:\d{12}:function:`+funcName+`:`+aliasName+`$`)),
),
},
},
})
}

func testAccCheckAwsLambdaAliasDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).lambdaconn

Expand Down Expand Up @@ -104,7 +152,99 @@ func testAccCheckAwsLambdaAttributes(mapping *lambda.AliasConfiguration) resourc
}
}

func testAccAwsLambdaAliasConfig(roleName, policyName, attachmentName, funcName, aliasName string) string {
func testAccCheckAwsLambdaAliasRoutingConfigExists(mapping *lambda.AliasConfiguration) resource.TestCheckFunc {
return func(s *terraform.State) error {
routingConfig := mapping.RoutingConfig

if routingConfig == nil {
return fmt.Errorf("Could not read Lambda alias routing config")
}
if len(routingConfig.AdditionalVersionWeights) != 1 {
return fmt.Errorf("Could not read Lambda alias additional version weights")
}
return nil
}
}

func testAccCheckAwsLambdaAliasRoutingConfigDoesNotExist(mapping *lambda.AliasConfiguration) resource.TestCheckFunc {
return func(s *terraform.State) error {
routingConfig := mapping.RoutingConfig

if routingConfig != nil {
return fmt.Errorf("Lambda alias routing config still exists after removal")
}
return nil
}
}

func testAccAwsLambdaAliasConfigWithoutRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "iam_for_lambda" {
name = "%s"

assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}

resource "aws_iam_policy" "policy_for_role" {
name = "%s"
path = "/"
description = "IAM policy for for Lamda alias testing"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:*"
],
"Resource": "*"
}
]
}
EOF
}

resource "aws_iam_policy_attachment" "policy_attachment_for_role" {
name = "%s"
roles = ["${aws_iam_role.iam_for_lambda.name}"]
policy_arn = "${aws_iam_policy.policy_for_role.arn}"
}

resource "aws_lambda_function" "lambda_function_test_create" {
filename = "test-fixtures/lambdatest.zip"
function_name = "%s"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
runtime = "nodejs4.3"
source_code_hash = "${base64sha256(file("test-fixtures/lambdatest.zip"))}"
publish = "true"
}

resource "aws_lambda_alias" "lambda_alias_test" {
name = "%s"
description = "a sample description"
function_name = "${aws_lambda_function.lambda_function_test_create.arn}"
function_version = "1"
}`, roleName, policyName, attachmentName, funcName, aliasName)
}

func testAccAwsLambdaAliasConfigWithRoutingConfig(roleName, policyName, attachmentName, funcName, aliasName string) string {
return fmt.Sprintf(`
resource "aws_iam_role" "iam_for_lambda" {
name = "%s"
Expand Down Expand Up @@ -154,17 +294,24 @@ resource "aws_iam_policy_attachment" "policy_attachment_for_role" {
}

resource "aws_lambda_function" "lambda_function_test_create" {
filename = "test-fixtures/lambdatest.zip"
function_name = "%s"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
runtime = "nodejs4.3"
filename = "test-fixtures/lambdatest_modified.zip"
function_name = "%s"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
runtime = "nodejs4.3"
source_code_hash = "${base64sha256(file("test-fixtures/lambdatest_modified.zip"))}"
publish = "true"
}

resource "aws_lambda_alias" "lambda_alias_test" {
name = "%s"
description = "a sample description"
function_name = "${aws_lambda_function.lambda_function_test_create.arn}"
function_version = "$LATEST"
function_version = "1"
routing_config = {
additional_version_weights = {
"2" = 0.5
}
}
}`, roleName, policyName, attachmentName, funcName, aliasName)
}
Binary file added aws/test-fixtures/lambdatest_modified.zip
Binary file not shown.
14 changes: 13 additions & 1 deletion website/docs/r/lambda_alias.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ resource "aws_lambda_alias" "test_alias" {
name = "testalias"
description = "a sample description"
function_name = "${aws_lambda_function.lambda_function_test.arn}"
function_version = "$LATEST"
function_version = "1"
routing_config = {
additional_version_weights = {
"2" = 0.5
}
}
}
```

Expand All @@ -30,10 +35,17 @@ resource "aws_lambda_alias" "test_alias" {
* `description` - (Optional) Description of the alias.
* `function_name` - (Required) The function ARN of the Lambda function for which you want to create an alias.
* `function_version` - (Required) Lambda function version for which you are creating the alias. Pattern: `(\$LATEST|[0-9]+)`.
* `routing_config` - (Optional) The Lambda alias' route configuration settings. Fields documented below

For **routing_config** the following attributes are supported:

* `additional_version_weights` - (Optional) A map that defines the proportion of events that should be sent to different versions of a lambda function.

## Attributes Reference

* `arn` - The Amazon Resource Name (ARN) identifying your Lambda function alias.

[1]: http://docs.aws.amazon.com/lambda/latest/dg/welcome.html
[2]: http://docs.aws.amazon.com/lambda/latest/dg/API_CreateAlias.html
[3]: https://docs.aws.amazon.com/lambda/latest/dg/API_AliasRoutingConfiguration.html
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this documentation reference was added but never referenced above.