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