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 support for lambda insights #1993

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions chalice/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,9 +842,12 @@ def route(self, path: str, **kwargs: Any) -> Callable[..., Any]:
)

def lambda_function(self,
name: Optional[str] = None) -> Callable[..., Any]:
name: Optional[str] = None,
insights: Optional[bool] = False
) -> Callable[..., Any]:
return self._create_registration_function(
handler_type='lambda_function', name=name)
handler_type='lambda_function', name=name,
registration_kwargs={'insights': insights})

def on_ws_connect(self,
name: Optional[str] = None) -> Callable[..., Any]:
Expand Down Expand Up @@ -1043,9 +1046,11 @@ def _register_lambda_function(self, name: str,
user_handler: UserHandlerFuncType,
handler_string: str,
**unused: Dict[str, Any]) -> None:
kwargs = unused.get('kwargs', {})
wrapper = LambdaFunction(
func=user_handler, name=name,
handler_string=handler_string,
insights=kwargs.get('insights', False)
)
self.pure_lambda_functions.append(wrapper)

Expand Down Expand Up @@ -1521,10 +1526,11 @@ def __init__(self, path: str, methods: List[str]):

class LambdaFunction(object):
def __init__(self, func: Callable[..., Any], name: str,
handler_string: str):
handler_string: str, insights: bool):
self.func: Callable[..., Any] = func
self.name: str = name
self.handler_string: str = handler_string
self.insights: bool = insights

def __call__(self, event: Dict[str, Any],
context: Dict[str, Any]
Expand Down
27 changes: 27 additions & 0 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,33 @@ def create_role(self, name, trust_policy, policy):
raise e
return role_arn

def attach_role_policy(self, role_name, policy_arn):
# type: (str, str) -> None
self._client('iam').attach_role_policy(
RoleName=role_name,
PolicyArn=policy_arn)

def detach_role_policy(self, role_name, policy_arn):
# type: (str, str) -> None
self._client('iam').detach_role_policy(
RoleName=role_name,
PolicyArn=policy_arn)

def is_role_policy_attached(self, role_name, policy_arn):
# type: (str, str) -> bool
client = self._client('iam')
try:
attached_policies = client.list_attached_role_policies(
RoleName=role_name)['AttachedPolicies']
except client.exceptions.NoSuchEntityException:
raise ResourceDoesNotExistError(
"No role found for: %s" % role_name)

for policy in attached_policies:
if policy['PolicyArn'] == policy_arn:
return True
return False

def delete_role(self, name):
# type: (str) -> None
"""Delete a role by first deleting all inline policies."""
Expand Down
54 changes: 52 additions & 2 deletions chalice/deploy/appgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from chalice.constants import LAMBDA_TRUST_POLICY
from chalice.deploy import models
from chalice.utils import UI # noqa
from chalice.layer_versions import LAYER_VERSIONS

StrMapAny = Dict[str, Any]

Expand All @@ -19,9 +20,12 @@ class ChaliceBuildError(Exception):


class ApplicationGraphBuilder(object):
def __init__(self):
# type: () -> None
def __init__(self, region_name=''):
# type: (str) -> None
self._region_name = region_name
self._known_roles = {} # type: Dict[str, models.IAMRole]
self._known_role_policy_attach = \
{} # type: Dict[str, models.IAMRolePolicyAttachment]
self._managed_layer = None # type: Optional[models.LambdaLayer]

def build(self, config, stage_name):
Expand All @@ -33,6 +37,19 @@ def build(self, config, stage_name):
config=config, deployment=deployment,
name=function.name, handler_name=function.handler_string,
stage_name=stage_name)
# create lambda insight
if function.insights:
resource.role_policy_attachment = \
self._get_role_policy_attachment(
config=config,
stage_name=stage_name,
function_name=function.name)
layer_arn = LAYER_VERSIONS.get(self._region_name)
if layer_arn:
if resource.layers:
resource.layers.append(layer_arn)
else:
resource.layers = [layer_arn]
resources.append(resource)
event_resources = self._create_lambda_event_resources(
config, deployment, stage_name)
Expand Down Expand Up @@ -444,6 +461,39 @@ def _create_role_reference(self, config, stage_name, function_name):
policy=policy,
)

def _get_role_policy_attachment(self, config, stage_name, function_name):
# type: (Config, str, str) -> models.IAMRolePolicyAttachment
role = self._create_role_policy_attachment(config,
stage_name,
function_name)
role_identifier = self._get_role_identifier(role)
if role_identifier in self._known_role_policy_attach:
# If we've already create a models.IAMRole with the same
# identifier, we'll use the existing object instead of
# creating a new one.
return self._known_role_policy_attach[role_identifier]
self._known_role_policy_attach[role_identifier] = role
return role

def _create_role_policy_attachment(self, config, stage_name,
function_name):
# type: (Config, str, str) -> models.IAMRolePolicyAttachment
if not config.autogen_policy:
resource_name = '%s_execution_role' % function_name
role_name = '%s-%s-%s' % (config.app_name, stage_name,
function_name)
else:
resource_name = 'default_execution_role'
role_name = '%s-%s' % (config.app_name, stage_name)
policy_arn = "arn:aws:iam::aws:policy/"\
"CloudWatchLambdaInsightsExecutionRolePolicy"

return models.IAMRolePolicyAttachment(
resource_name=resource_name,
role_name=role_name,
policy_arn=policy_arn
)

def _get_vpc_params(self, function_name, config):
# type: (str, Config) -> Tuple[List[str], List[str]]
security_group_ids = config.security_group_ids
Expand Down
2 changes: 1 addition & 1 deletion chalice/deploy/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def _create_deployer(session, # type: Session
client = TypedAWSClient(session)
osutils = OSUtils()
return Deployer(
application_builder=ApplicationGraphBuilder(),
application_builder=ApplicationGraphBuilder(client.region_name),
deps_builder=DependencyBuilder(),
build_stage=create_build_stage(
osutils, UI(), TemplatedSwaggerGenerator(), config
Expand Down
13 changes: 13 additions & 0 deletions chalice/deploy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ def dependencies(self):
return [self.policy]


@attrs
class IAMRolePolicyAttachment(IAMRole, ManagedModel):
# resource_type = 'iam_role_policy_attachment'
resource_type = 'iam_role'
role_name = attrib() # type: str
# trust_policy = attrib() # type: Dict[str, Any]
policy_arn = attrib() # type: str


@attrs
class LambdaLayer(ManagedModel):
resource_type = 'lambda_layer'
Expand Down Expand Up @@ -208,13 +217,17 @@ class LambdaFunction(ManagedModel):
layers = attrib() # type: List[str]
managed_layer = attrib(
default=None) # type: Opt[LambdaLayer]
role_policy_attachment = attrib(
default=None) # type: IAMRolePolicyAttachment

def dependencies(self):
# type: () -> List[Model]
resources = [] # type: List[Model]
if self.managed_layer is not None:
resources.append(self.managed_layer)
resources.extend([self.role, self.deployment_package])
if self.role_policy_attachment:
resources.append(self.role_policy_attachment)
return resources


Expand Down
90 changes: 90 additions & 0 deletions chalice/deploy/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def __init__(self, client, deployed_resources):
self._cache = {} # type: Dict[CacheTuples, bool]
self._deployed_resources = deployed_resources

@property
def client(self):
# type: () -> TypedAWSClient
return self._client

def _cache_key(self, resource):
# type: (models.ManagedModel) -> CacheTuples
if isinstance(resource, models.APIMapping):
Expand Down Expand Up @@ -53,6 +58,14 @@ def _dynamically_lookup_values(self, resource):
"name": resource.resource_name,
"resource_type": "iam_role",
}
elif isinstance(resource, models.IAMRolePolicyAttachment):
arn = self._client.get_role_arn_for_name(resource.role_name)
return {
"role_name": resource.role_name,
"role_arn": arn,
"name": resource.resource_name,
"resource_type": "iam_role",
}
raise ValueError("Deployed values for resource does not exist: %s"
% resource.resource_name)

Expand Down Expand Up @@ -151,6 +164,15 @@ def _resource_exists_managediamrole(self, resource):
except ResourceDoesNotExistError:
return False

def _resource_exists_iamrolepolicyattachment(self, resource):
# type: (models.IAMRolePolicyAttachment) -> bool
try:
resp = self._client.is_role_policy_attached(resource.role_name,
resource.policy_arn)
return resp
except ResourceDoesNotExistError:
return False

def _resource_exists_apimapping(self, resource, domain_name):
# type: (models.APIMapping, str) -> bool
map_key = resource.mount_path
Expand Down Expand Up @@ -620,6 +642,74 @@ def _plan_managediamrole(self, resource):
)
]

def _plan_iamrolepolicyattachment(self, resource):
# type: (models.IAMRolePolicyAttachment) -> Sequence[InstructionMsg]
role_exists = self._remote_state.resource_exists(resource)
varname = '%s_execution_role_arn' % resource.role_name
if not role_exists:
try:
role_arn = self._remote_state.client.get_role_arn_for_name(
resource.role_name)
except ResourceDoesNotExistError:
role_arn = None

return [
(models.APICall(
method_name='attach_role_policy',
params={'role_name': resource.role_name,
'policy_arn': resource.policy_arn},
output_var=varname,
), "Creating IAM role policy attachment: %s\n" %
resource.role_name),
models.RecordResourceValue(
resource_type='iam_role',
resource_name=resource.resource_name,
name='role_arn',
value=role_arn,
) if role_arn else models.RecordResourceVariable(
resource_type='iam_role',
resource_name=resource.resource_name,
name='role_arn',
variable_name=varname,
),
models.RecordResourceValue(
resource_type='iam_role',
resource_name=resource.resource_name,
name='role_name',
value=resource.role_name,
),
models.RecordResourceValue(
resource_type='iam_role',
resource_name=resource.resource_name,
name='policy_arn',
value=resource.policy_arn,
)
]

role_arn = self._remote_state.resource_deployed_values(
resource)['role_arn']
return [
models.StoreValue(name=varname, value=role_arn),
models.RecordResourceVariable(
resource_type='iam_role',
resource_name=resource.resource_name,
name='role_arn',
variable_name=varname,
),
models.RecordResourceValue(
resource_type='iam_role',
resource_name=resource.resource_name,
name='role_name',
value=resource.role_name,
),
models.RecordResourceValue(
resource_type='iam_role',
resource_name=resource.resource_name,
name='policy_arn',
value=resource.policy_arn,
)
]

def _plan_snslambdasubscription(self, resource):
# type: (models.SNSLambdaSubscription) -> Sequence[InstructionMsg]
function_arn = Variable(
Expand Down
18 changes: 18 additions & 0 deletions chalice/deploy/sweeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,22 @@ def _delete_iam_role(self, resource_values):
'message': 'Deleting IAM role: %s\n' % resource_values['role_name']
}

def _delete_iam_role_policy_attachment(self, resource_values):
# type: (Dict[str, Any]) -> ResourceValueType
return {
'instructions': (
models.APICall(
method_name='detach_role_policy',
params={
'role_name': resource_values['role_name'],
'policy_arn': resource_values['policy_arn'],
},
),
),
'message': 'Detaching IAM role policy attachment: %s\n' %
resource_values['role_name']
}

def _delete_cloudwatch_event(self, resource_values):
# type: (Dict[str, Any]) -> ResourceValueType
return {
Expand Down Expand Up @@ -426,6 +442,8 @@ def _plan_deletion(self,
resource_type = 'domain_api_mappings'
handler_args.append(name)
insert = True
if name.endswith('_execution_role'):
resource_type = 'iam_role_policy_attachment'

method_name = '_delete_%s' % resource_type
handler = getattr(self, method_name, self._default_delete)
Expand Down
25 changes: 25 additions & 0 deletions chalice/layer-versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"us-east-1": "arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:14",
"us-east-2": "arn:aws:lambda:us-east-2:580247275435:layer:LambdaInsightsExtension:14",
"us-west-1": "arn:aws:lambda:us-west-1:580247275435:layer:LambdaInsightsExtension:14",
"us-west-2": "arn:aws:lambda:us-west-2:580247275435:layer:LambdaInsightsExtension:14",
"ap-south-1": "arn:aws:lambda:ap-south-1:580247275435:layer:LambdaInsightsExtension:14",
"ap-northeast-2": "arn:aws:lambda:ap-northeast-2:580247275435:layer:LambdaInsightsExtension:14",
"ap-southeast-1": "arn:aws:lambda:ap-southeast-1:580247275435:layer:LambdaInsightsExtension:14",
"ap-southeast-2": "arn:aws:lambda:ap-southeast-2:580247275435:layer:LambdaInsightsExtension:14",
"ap-northeast-1": "arn:aws:lambda:ap-northeast-1:580247275435:layer:LambdaInsightsExtension:14",
"ca-central-1": "arn:aws:lambda:ca-central-1:580247275435:layer:LambdaInsightsExtension:14",
"eu-central-1": "arn:aws:lambda:eu-central-1:580247275435:layer:LambdaInsightsExtension:14",
"eu-west-1": "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:14",
"eu-west-2": "arn:aws:lambda:eu-west-2:580247275435:layer:LambdaInsightsExtension:14",
"eu-west-3": "arn:aws:lambda:eu-west-3:580247275435:layer:LambdaInsightsExtension:14",
"eu-north-1": "arn:aws:lambda:eu-north-1:580247275435:layer:LambdaInsightsExtension:14",
"sa-east-1": "arn:aws:lambda:sa-east-1:580247275435:layer:LambdaInsightsExtension:14",
"cn-north-1": "arn:aws-cn:lambda:cn-north-1:488211338238:layer:LambdaInsightsExtension:8",
"cn-northwest-1": "arn:aws-cn:lambda:cn-northwest-1:488211338238:layer:LambdaInsightsExtension:8",
"af-south-1": "arn:aws:lambda:af-south-1:012438385374:layer:LambdaInsightsExtension:8",
"ap-east-1": "arn:aws:lambda:ap-east-1:519774774795:layer:LambdaInsightsExtension:8",
"me-south-1": "arn:aws:lambda:me-south-1:285320876703:layer:LambdaInsightsExtension:8",
"eu-south-1": "arn:aws:lambda:eu-south-1:339249233099:layer:LambdaInsightsExtension:8"
}

16 changes: 16 additions & 0 deletions chalice/layer_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os
import json

from typing import Dict


def load_layer_versions():
# type: () -> Dict[str, str]
layers_json = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'layer-versions.json')
with open(layers_json) as f:
return json.loads(f.read())


LAYER_VERSIONS = load_layer_versions()