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

Add VPC support to lambda functions #837

Merged
merged 12 commits into from
May 17, 2018
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ CHANGELOG
=========


Next Release (TBD)
==================

* Add support for Lambdas in a VPC
(`#413 <https://github.com/aws/chalice/issues/413>`__,
`#837 <https://github.com/aws/chalice/pull/837>`__,
`#673 <https://github.com/aws/chalice/pull/673>`__`)


1.2.3
=====

Expand All @@ -11,6 +20,7 @@ CHANGELOG
* Update ``policies.json`` file
(`#817 <https://github.com/aws/chalice/issues/817>`__)

Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a changelog entry


1.2.2
=====

Expand Down
2 changes: 2 additions & 0 deletions chalice/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import base64
from collections import defaultdict, Mapping


__version__ = '1.2.3'


# Implementation note: This file is intended to be a standalone file
# that gets copied into the lambda deployment package. It has no dependencies
# on other parts of chalice so it can stay small and lightweight, with minimal
Expand Down
84 changes: 75 additions & 9 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
_STR_MAP = Optional[Dict[str, str]]
_OPT_STR = Optional[str]
_OPT_INT = Optional[int]
_OPT_STR_LIST = Optional[List[str]]
_CLIENT_METHOD = Callable[..., Dict[str, Any]]


Expand Down Expand Up @@ -104,6 +105,22 @@ def get_function_configuration(self, name):
FunctionName=name)
return response

def _create_vpc_config(self, security_group_ids, subnet_ids):
# type: (_OPT_STR_LIST, _OPT_STR_LIST) -> Dict[str, List[str]]
# We always set the SubnetIds and SecurityGroupIds to an empty
# list to ensure that we properly remove Vpc configuration
# if you remove these values from your config.json. Omitting
# the VpcConfig key or just setting to {} won't actually remove
# the VPC configuration.
vpc_config = {
'SubnetIds': [],
'SecurityGroupIds': [],
} # type: Dict[str, List[str]]
if security_group_ids is not None and subnet_ids is not None:
vpc_config['SubnetIds'] = subnet_ids
vpc_config['SecurityGroupIds'] = security_group_ids
return vpc_config

def create_function(self,
function_name, # type: str
role_arn, # type: str
Expand All @@ -113,7 +130,9 @@ def create_function(self,
environment_variables=None, # type: _STR_MAP
tags=None, # type: _STR_MAP
timeout=None, # type: _OPT_INT
memory_size=None # type: _OPT_INT
memory_size=None, # type: _OPT_INT
security_group_ids=None, # type: _OPT_STR_LIST
subnet_ids=None, # type: _OPT_STR_LIST
):
# type: (...) -> str
kwargs = {
Expand All @@ -131,14 +150,25 @@ def create_function(self,
kwargs['Timeout'] = timeout
if memory_size is not None:
kwargs['MemorySize'] = memory_size
if security_group_ids is not None and subnet_ids is not None:
kwargs['VpcConfig'] = self._create_vpc_config(
security_group_ids=security_group_ids,
subnet_ids=subnet_ids,
)
return self._create_lambda_function(kwargs)

def _create_lambda_function(self, api_args):
# type: (Dict[str, Any]) -> str
try:
return self._call_client_method_with_retries(
self._client('lambda').create_function, kwargs)['FunctionArn']
self._client('lambda').create_function,
api_args
)['FunctionArn']
except _REMOTE_CALL_ERRORS as e:
context = LambdaErrorContext(
function_name,
api_args['FunctionName'],
'create_function',
len(zip_contents)
len(api_args['Code']['ZipFile']),
)
raise self._get_lambda_code_deployment_error(e, context)

Expand Down Expand Up @@ -167,6 +197,8 @@ def _is_iam_role_related_error(self, error):
message = error.response['Error'].get('Message', '')
if re.search('role.*cannot be assumed', message):
return True
if re.search('role.*does not have permissions', message):
return True
return False

def _get_lambda_code_deployment_error(self, error, context):
Expand Down Expand Up @@ -208,7 +240,9 @@ def update_function(self,
tags=None, # type: _STR_MAP
timeout=None, # type: _OPT_INT
memory_size=None, # type: _OPT_INT
role_arn=None # type: _OPT_STR
role_arn=None, # type: _OPT_STR
subnet_ids=None, # type: _OPT_STR_LIST
security_group_ids=None, # type: _OPT_STR_LIST
):
# type: (...) -> Dict[str, Any]
"""Update a Lambda function's code and configuration.
Expand All @@ -217,9 +251,27 @@ def update_function(self,
is not provided, no changes will be made for that that parameter on
the targeted lambda function.
"""
return_value = self._update_function_code(function_name=function_name,
zip_contents=zip_contents)
self._update_function_config(
environment_variables=environment_variables,
runtime=runtime,
timeout=timeout,
memory_size=memory_size,
role_arn=role_arn,
subnet_ids=subnet_ids,
security_group_ids=security_group_ids,
function_name=function_name
)
if tags is not None:
self._update_function_tags(return_value['FunctionArn'], tags)
return return_value

def _update_function_code(self, function_name, zip_contents):
# type: (str, str) -> Dict[str, Any]
lambda_client = self._client('lambda')
try:
return_value = lambda_client.update_function_code(
return lambda_client.update_function_code(
FunctionName=function_name, ZipFile=zip_contents)
except _REMOTE_CALL_ERRORS as e:
context = LambdaErrorContext(
Expand All @@ -229,6 +281,17 @@ def update_function(self,
)
raise self._get_lambda_code_deployment_error(e, context)

def _update_function_config(self,
environment_variables, # type: _STR_MAP
runtime, # type: _OPT_STR
timeout, # type: _OPT_INT
memory_size, # type: _OPT_INT
role_arn, # type: _OPT_STR
subnet_ids, # type: _OPT_STR_LIST
security_group_ids, # type: _OPT_STR_LIST
function_name, # type: str
):
# type: (...) -> None
kwargs = {} # type: Dict[str, Any]
if environment_variables is not None:
kwargs['Environment'] = {'Variables': environment_variables}
Expand All @@ -240,13 +303,16 @@ def update_function(self,
kwargs['MemorySize'] = memory_size
if role_arn is not None:
kwargs['Role'] = role_arn
if security_group_ids is not None and subnet_ids is not None:
kwargs['VpcConfig'] = self._create_vpc_config(
subnet_ids=subnet_ids,
security_group_ids=security_group_ids
)
if kwargs:
kwargs['FunctionName'] = function_name
lambda_client = self._client('lambda')
self._call_client_method_with_retries(
lambda_client.update_function_configuration, kwargs)
if tags is not None:
self._update_function_tags(return_value['FunctionArn'], tags)
return return_value

def _update_function_tags(self, function_arn, requested_tags):
# type: (str, Dict[str, str]) -> None
Expand Down
14 changes: 14 additions & 0 deletions chalice/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,20 @@ def tags(self):
current_chalice_version, self.chalice_stage, self.app_name)
return tags

@property
def security_group_ids(self):
# type: () -> List[str]
return self._chain_lookup('security_group_ids',
varies_per_chalice_stage=True,
varies_per_function=True)

@property
def subnet_ids(self):
# type: () -> List[str]
return self._chain_lookup('subnet_ids',
varies_per_chalice_stage=True,
varies_per_function=True)

def scope(self, chalice_stage, function_name):
# type: (str, str) -> Config
# Used to create a new config object that's scoped to a different
Expand Down
12 changes: 12 additions & 0 deletions chalice/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ def index():
}


VPC_ATTACH_POLICY = {
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DetachNetworkInterface",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}


CODEBUILD_POLICY = {
"Version": "2012-10-17",
# This is the policy straight from the console.
Expand Down
Loading