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