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  }