source/idea/infrastructure/install/installer.py (242 lines of code) (raw):
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from datetime import datetime
from typing import Dict, TypedDict, Union
import aws_cdk
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_stepfunctions as sfn
from aws_cdk import aws_stepfunctions_tasks as sfn_tasks
from constructs import Construct, DependencyGroup
from idea.batteries_included.parameters.parameters import BIParameters
from idea.infrastructure.install import tasks
from idea.infrastructure.install.backend import (
BackendLambda,
backend_lambda_name,
backend_lambda_security_group_name,
)
from idea.infrastructure.install.cognito_sync_lambda import (
CognitoSyncLambda,
cognito_sync_lambda_name,
cognito_sync_lambda_security_group_name,
)
from idea.infrastructure.install.cognito_trigger_workflow import (
CognitoTriggerWorkflow,
cognito_trigger_workflow_lambda_name,
cognito_trigger_workflow_lambda_security_group_name,
)
from idea.infrastructure.install.constants import (
API_PROXY_LAMBDA_LAYER_NAME,
RES_COMMON_LAMBDA_RUNTIME,
RES_ECR_REPO_NAME_SUFFIX,
SHARED_RES_LIBRARY_LAMBDA_LAYER_NAME,
)
from idea.infrastructure.install.handlers import installer_handlers
from idea.infrastructure.install.parameters.common import CommonKey
from idea.infrastructure.install.parameters.internet_proxy import InternetProxyKey
from idea.infrastructure.install.parameters.parameters import RESParameters
from idea.infrastructure.install.proxy import (
LambdaAndSecurityGroupCleanup,
Proxy,
proxy_lambda_name,
proxy_lambda_security_group_name,
)
from idea.infrastructure.install.utils import InfraUtils
LAMBDA_RUNTIME = lambda_.Runtime.PYTHON_3_11
class LambdaCodeParams(TypedDict):
handler: str
code: lambda_.Code
class Installer(Construct):
def __init__(
self,
scope: Construct,
id: str,
registry_name: str,
params: Union[RESParameters, BIParameters],
dependency_group: DependencyGroup,
lambda_layers: Dict[str, lambda_.LayerVersion],
):
super().__init__(scope, id)
self.params = params
self.registry_name = registry_name
event_handler = lambda_.Function(
self,
"CustomResourceEventHandler",
runtime=RES_COMMON_LAMBDA_RUNTIME,
timeout=aws_cdk.Duration.seconds(10),
description="Lambda to handle the CFN custom resource events",
**InfraUtils.get_handler_and_code_for_function(
installer_handlers.handle_custom_resource_lifecycle_event
),
)
wait_condition_handle = aws_cdk.CfnWaitConditionHandle(
self, f"InstallerWaitConditionHandle{self.get_wait_condition_suffix()}"
)
cluster_name = self.params.get_str(CommonKey.CLUSTER_NAME)
self.lambdaConstructCleanup = LambdaAndSecurityGroupCleanup(
self,
"remove-leftover-lambda-and-sg-resources",
self.params.get_str(CommonKey.VPC_ID),
[
f"{cluster_name}_{proxy_lambda_name}",
f"{cluster_name}_{backend_lambda_name}",
f"{cluster_name}_{cognito_sync_lambda_name}",
f"{cluster_name}_uid_{cognito_trigger_workflow_lambda_name}",
f"{cluster_name}_post_auth_{cognito_trigger_workflow_lambda_name}",
],
[
f"{cluster_name}_{proxy_lambda_security_group_name}",
f"{cluster_name}_{backend_lambda_security_group_name}",
f"{cluster_name}_{cognito_sync_lambda_security_group_name}",
f"{cluster_name}_uid_{cognito_trigger_workflow_lambda_security_group_name}",
f"{cluster_name}_post_auth_{cognito_trigger_workflow_lambda_security_group_name}",
],
)
installer = aws_cdk.CustomResource(
self,
"Installer",
service_token=event_handler.function_arn,
removal_policy=aws_cdk.RemovalPolicy.DESTROY,
resource_type="Custom::RES",
properties={
installer_handlers.EnvKeys.CALLBACK_URL: wait_condition_handle.ref,
installer_handlers.EnvKeys.INSTALLER_ECR_REPO_NAME: aws_cdk.Fn.join(
"",
[
self.params.get_str(CommonKey.CLUSTER_NAME),
RES_ECR_REPO_NAME_SUFFIX,
],
),
installer_handlers.EnvKeys.ENVIRONMENT_NAME: self.params.get_str(
CommonKey.CLUSTER_NAME
),
},
)
# This ensures clean up is done after the installer finishes at stack deletion
# which gives Lambda more time to clean up the left over ENIs
installer.node.add_dependency(self.lambdaConstructCleanup)
wait_condition = aws_cdk.CfnWaitCondition(
self,
f"InstallerWaitCondition{self.get_wait_condition_suffix()}",
count=1,
timeout=str(aws_cdk.Duration.hours(2).to_seconds()),
handle=wait_condition_handle.ref,
)
wait_condition.node.add_dependency(installer)
http_proxy = self.params.get_str(InternetProxyKey.HTTP_PROXY)
https_proxy = self.params.get_str(InternetProxyKey.HTTPS_PROXY)
no_proxy = self.params.get_str(InternetProxyKey.NO_PROXY)
self.cognito_sync_lambda = CognitoSyncLambda(
self,
"cognito-sync-lambda",
params,
)
self.cognito_sync_lambda.node.add_dependency(wait_condition)
self.cognito_trigger_workflow = CognitoTriggerWorkflow(
self, "cognito-trigger-workflow", cluster_name, params
)
self.cognito_trigger_workflow.node.add_dependency(wait_condition)
self.proxyLambda = Proxy(
self,
"AWSProxy",
{
"target_group_priority": 101,
"ddb_users_table_name": f"{cluster_name}.accounts.users",
"ddb_groups_table_name": f"{cluster_name}.accounts.groups",
"ddb_cluster_settings_table_name": f"{cluster_name}.cluster-settings",
"cluster_name": cluster_name,
"http_proxy": http_proxy,
"https_proxy": https_proxy,
"no_proxy": no_proxy,
},
lambda_layer=lambda_layers[API_PROXY_LAMBDA_LAYER_NAME],
)
# Add wait condition as dependency ensures it deploys after ECS deployment is completed
self.proxyLambda.node.add_dependency(wait_condition)
self.backendLambda = BackendLambda(
self,
"BackendLambda",
{
"target_group_priority": 102,
"cluster_name": self.params.get_str(CommonKey.CLUSTER_NAME),
"http_proxy": http_proxy,
"https_proxy": https_proxy,
"no_proxy": no_proxy,
},
lambda_layer=lambda_layers[SHARED_RES_LIBRARY_LAMBDA_LAYER_NAME],
)
# Add wait condition as dependency ensures it deploys after ECS deployment is completed
self.backendLambda.node.add_dependency(wait_condition)
self.tasks = tasks.Tasks(
self,
"Tasks",
installer_registry_name=self.registry_name,
params=params,
dependency_group=dependency_group,
lambda_layer_arn=lambda_layers[
SHARED_RES_LIBRARY_LAMBDA_LAYER_NAME
].layer_version_arn,
)
state_machine = self.get_state_machine()
state_machine.grant_start_execution(event_handler)
event_handler.add_environment(
key=installer_handlers.EnvKeys.SFN_ARN,
value=state_machine.state_machine_arn,
)
dependency_group.add(state_machine)
installer.node.add_dependency(dependency_group)
def get_wait_condition_suffix(self) -> str:
return str(int(datetime.now().timestamp()))
def get_state_machine(self) -> sfn.StateMachine:
request_type_choice = sfn.Choice(self, "SwitchByEventType")
resource_signaler = lambda_.Function(
self,
"WaitConditionResponseSender",
runtime=RES_COMMON_LAMBDA_RUNTIME,
timeout=aws_cdk.Duration.seconds(10),
description="Lambda to send response using the wait condition callback",
**InfraUtils.get_handler_and_code_for_function(
installer_handlers.send_wait_condition_response
),
)
send_cfn_response_task = sfn_tasks.LambdaInvoke(
self,
"SendCfnResponse",
lambda_function=resource_signaler,
payload_response_only=True,
)
send_cfn_response_task.add_retry()
create_task = self.tasks.get_create_task()
update_task = self.tasks.get_update_task()
delete_task = self.tasks.get_delete_task()
cognito_unprotect_task = self.tasks.get_cognito_user_pool_unprotect_task()
for task in (
create_task,
update_task,
delete_task,
cognito_unprotect_task,
):
task.add_catch(
handler=send_cfn_response_task,
result_path=f"$.{installer_handlers.EnvKeys.ERROR}",
)
request_type_choice.when(
sfn.Condition.string_equals(
"$.RequestType", installer_handlers.RequestType.DELETE
),
cognito_unprotect_task,
).when(
sfn.Condition.string_equals(
"$.RequestType", installer_handlers.RequestType.CREATE
),
create_task,
).when(
sfn.Condition.string_equals(
"$.RequestType", installer_handlers.RequestType.UPDATE
),
update_task,
).otherwise(
sfn.Fail(self, "UnknownRequestType")
)
cognito_unprotect_task.next(delete_task)
create_task.next(send_cfn_response_task)
update_task.next(send_cfn_response_task)
delete_task.next(send_cfn_response_task)
return sfn.StateMachine(
self,
"InstallerStateMachine",
definition=sfn.Chain.start(request_type_choice),
)