def _create_configuration_table(self): self.dynamodb.create_table(TableName=ENVIRONMENT_CONFIGURATION_TABLE, KeySchema=[{ 'AttributeName': 'environment', 'KeyType': 'HASH' }], AttributeDefinitions=[{ 'AttributeName': 'environment', 'AttributeType': 'S' }], BillingMode='PAY_PER_REQUEST') log_bold("Configuration table created!")
def _get_environment_stack(self): try: log("Looking for " + self.environment + " cluster.") environment_stack = self.client.describe_stacks( StackName=get_cluster_name(self.environment))['Stacks'][0] log_bold(self.environment + " stack found. Using stack with ID: " + environment_stack['StackId']) except ClientError: log_err(self.environment + " cluster not found. Create the environment \ cluster using `create_environment` command.") exit(1) return environment_stack
def _print_progress(self): while True: response = self.client.describe_stacks(StackName=self.stack_name) if "IN_PROGRESS" not in response['Stacks'][0]['StackStatus']: break all_events = get_stack_events(self.client, self.stack_name) print_new_events(all_events, self.existing_events) self.existing_events = all_events sleep(5) final_status = response['Stacks'][0]['StackStatus'] if "FAIL" in final_status: log_err("Finished with status: %s" % (final_status)) else: log_bold("Finished with status: %s" % (final_status))
def create(self): ''' Create and execute CloudFormation template for ECS service and related dependencies ''' log_bold("Initiating service creation") self.service_configuration.edit_config() template_generator = ServiceTemplateGenerator( self.service_configuration, self.environment_stack) service_template_body, template_source, key = template_generator.generate_service( ) try: if template_source == 'TemplateBody': self.client.create_stack( StackName=self.stack_name, TemplateBody=service_template_body, Parameters=[{ 'ParameterKey': 'Environment', 'ParameterValue': self.environment, }], OnFailure='DO_NOTHING', Capabilities=['CAPABILITY_NAMED_IAM'], ) elif template_source == 'TemplateURL': self.client.create_stack( StackName=self.stack_name, TemplateURL=service_template_body, Parameters=[{ 'ParameterKey': 'Environment', 'ParameterValue': self.environment, }], OnFailure='DO_NOTHING', Capabilities=['CAPABILITY_NAMED_IAM'], ) log_bold("Submitted to cloudformation. Checking progress...") self.delete_template(key) self._print_progress() except ClientError as boto_client_error: self.delete_template(key) error_code = boto_client_error.response['Error']['Code'] if error_code == 'AlreadyExistsException': raise UnrecoverableException("Stack " + self.stack_name + " already exists.") else: raise boto_client_error
def get_mfa_session(mfa_code=None, region='ap-south-1'): username = get_username() if not mfa_code: mfa_code = input("MFA Code: ") mfa_arn = "arn:aws:iam::%s:mfa/%s" % (get_account_id(), username) log_bold("Using credentials for " + username) try: session_params = client('sts').get_session_token( DurationSeconds=900, SerialNumber=mfa_arn, TokenCode=str(mfa_code)) credentials = session_params['Credentials'] return Session(aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'], region_name=region) except botocore.exceptions.ClientError as client_error: raise UnrecoverableException(str(client_error))
def ensure_image_in_ecr(self): if self.version: try: commit_sha = self._find_commit_sha(self.version) except: commit_sha = self.version log_intent("Using commit hash " + commit_sha + " to find image") image = self._find_image_in_ecr(commit_sha) if not image: log_warning("Please build, tag and upload the image for the \ commit " + commit_sha) raise UnrecoverableException("Image for given version could not be found.") else: dirty = subprocess.check_output( ["git", "status", "--short"] ).decode("utf-8") if dirty: self.version = 'dirty' log_intent("Version parameter was not provided. Determined \ version to be " + self.version + " based on current status") image = None else: self.version = self._find_commit_sha() log_intent("Version parameter was not provided. Determined \ version to be " + self.version + " based on current status") image = self._find_image_in_ecr(self.version) if image: log_intent("Image found in ECR") else: log_bold("Image not found in ECR. Building image") image_name = spinalcase(self.name) + ':' + self.version ecr_name = self.ecr_image_uri + ':' + self.version self._build_image(image_name) self._push_image(image_name, ecr_name) image = self._find_image_in_ecr(self.version) try: image_manifest = image['imageManifest'] self.ecr_client.put_image( repositoryName=self.repo_name, imageTag=self.version, imageManifest=image_manifest ) except Exception: pass
def _create_configuration_table(self): key_schema = [{ 'AttributeName': self.kv_pairs[0][0], 'KeyType': 'HASH' }] key_schema.extend([{ 'AttributeName': key, 'KeyType': 'RANGE' } for key, _ in self.kv_pairs[1:]]) self.dynamodb.create_table(TableName=self.table_name, KeySchema=key_schema, AttributeDefinitions=[{ 'AttributeName': key, 'AttributeType': 'S' } for key, _ in self.kv_pairs], BillingMode='PAY_PER_REQUEST') log_bold("{} table created!".format(self.table_name))
def print_parameter_changes(differences): changes_to_show = [["Type", "Config", "Old val", "New val"]] for difference in differences: if difference[0] == 'change': changes_to_show.append( ['change', difference[1], difference[2][0], difference[2][1]]) if difference[0] == 'add': difference[2].sort(key=lambda x: x[0]) for added_item in difference[2]: changes_to_show.append( ['add', added_item[0], '', added_item[1]]) if difference[0] == 'remove': difference[2].sort(key=lambda x: x[0]) for removed_item in difference[2]: changes_to_show.append( ['remove', removed_item[0], removed_item[1], '']) log_bold("Modifications to config:") print(SingleTable(changes_to_show).table)
def do_mfa_login(mfa_code=None, region='ap-south-1'): username = get_username() if not mfa_code: mfa_code = input("MFA Code: ") mfa_arn = "arn:aws:iam::%s:mfa/%s" % (get_account_id(), username) log_bold("Using credentials for " + username) try: session_params = client('sts').get_session_token( DurationSeconds=900, SerialNumber=mfa_arn, TokenCode=str(mfa_code)) credentials = session_params['Credentials'] os.environ['AWS_ACCESS_KEY_ID'] = credentials['AccessKeyId'] os.environ['AWS_SECRET_ACCESS_KEY'] = credentials['SecretAccessKey'] os.environ['AWS_SESSION_TOKEN'] = credentials['SessionToken'] os.environ['AWS_DEFAULT_REGION'] = region return session_params except botocore.exceptions.ClientError as client_error: raise UnrecoverableException(str(client_error))
def update(self): ''' Create and execute changeset for existing CloudFormation template for ECS service and related dependencies ''' log_bold("Starting to update service") self.service_configuration.edit_config() self.service_configuration.validate() information_fetcher = ServiceInformationFetcher( self.service_configuration.service_name, self.environment, self.service_configuration.get_config(), ) try: current_image_uri = information_fetcher.get_current_image_uri() desired_counts = information_fetcher.fetch_current_desired_count() template_generator = ServiceTemplateGenerator( self.service_configuration, self.environment_stack, self.env_sample_file, current_image_uri, desired_counts, ) service_template_body = template_generator.generate_service() change_set = create_change_set(self.client, service_template_body, self.stack_name, None, self.environment) if change_set is None: return self.service_configuration.update_cloudlift_version() log_bold("Executing changeset. Checking progress...") self.client.execute_change_set( ChangeSetName=change_set['ChangeSetId']) self._print_progress() except ClientError as exc: if "No updates are to be performed." in str(exc): log_err("No updates are to be performed") else: raise exc
def ensure_image_in_ecr(self): if self.version: log_intent("Using commit hash " + self.version + " to find image") image = self._find_image_in_ecr(self.version) if not image: log_warning("Please build, tag and upload the image for the \ commit " + self.version) raise UnrecoverableException( "Image for given version could not be found.") else: dirty = subprocess.check_output(["git", "status", "--short"]).decode("utf-8") if dirty: log_intent( "Repository has uncommitted changes. Marking version as dirty." ) self.version = '{}-dirty'.format(self._derive_version()) image = None else: self.version = self._derive_version() image = self._find_image_in_ecr(self.version) log_intent( "Version parameter was not provided. Determined version to be " + self.version + " based on current status") if image: log_intent("Image found in ECR") else: log_bold("Image not found in ECR. Building image") self._build_image() self._push_image() image = self._find_image_in_ecr(self.version) try: image_manifest = image['imageManifest'] self.client.put_image(repositoryName=self.repo_name, imageTag=self.version, imageManifest=image_manifest) except Exception: pass self._add_image_tag(self.version, f'{self.version}-{self._git_epoch_time()}')
def get_version(self, short): commit_sha = self._fetch_current_task_definition_tag() if commit_sha is None: log_err("Current task definition tag could not be found. \ Is it deployed?") elif commit_sha == "dirty": log("Dirty version is deployed. Commit information could not be \ fetched.") else: log("Currently deployed version: " + commit_sha) if not short: log("Running `git fetch --all`") call(["git", "fetch", "--all"]) log_bold("Commit Info:") call([ "git", "--no-pager", "show", "-s", "--format=medium", commit_sha ]) log_bold("Branch Info:") call(["git", "branch", "-r", "--contains", commit_sha]) log("")
def run(self): log_warning("Deploying to {self.region}".format(**locals())) if not os.path.exists(self.env_sample_file): raise UnrecoverableException('env.sample not found. Exiting.') log_intent("name: " + self.name + " | environment: " + self.environment + " | version: " + str(self.version) + " | deployment_identifier: " + self.deployment_identifier) log_bold("Checking image in ECR") self.ecr.upload_artefacts() log_bold("Initiating deployment\n") ecs_client = EcsClient(None, None, self.region) image_url = self.ecr.image_uri target = deployer.deploy_new_version kwargs = dict(client=ecs_client, cluster_name=self.cluster_name, service_name=self.name, sample_env_file_path=self.env_sample_file, timeout_seconds=self.timeout_seconds, env_name=self.environment, ecr_image_uri=image_url, deployment_identifier=self.deployment_identifier, ) self.run_job_for_all_services("Deploy", target, kwargs)
def create(self): log_warning( "Create task definition to {self.region}".format(**locals())) if not os.path.exists(self.env_sample_file): raise UnrecoverableException('env.sample not found. Exiting.') ecr_client = EcrClient(self.name, self.region, self.build_args) ecr_client.set_version(self.version) log_intent("name: " + self.name + " | environment: " + self.environment + " | version: " + str(ecr_client.version)) log_bold("Checking image in ECR") ecr_client.build_and_upload_image() log_bold("Creating task definition\n") env_config = build_config(self.environment, self.name, self.env_sample_file) container_definition_arguments = { "environment": [{ "name": k, "value": v } for (k, v) in env_config], "name": pascalcase(self.name) + "Container", "image": _complete_image_url(ecr_client), "essential": True, "logConfiguration": self._gen_log_config(pascalcase(self.name)), "memoryReservation": 1024 } ecs_client = EcsClient(region=self.region) ecs_client.register_task_definition(self._task_defn_family(), [container_definition_arguments], [], None) log_bold("Task definition successfully created\n")
def run_update(self, update_ecs_agents): if update_ecs_agents: self.__run_ecs_container_agent_udpate() try: log("Initiating environment stack update.") environment_stack_template_body = ClusterTemplateGenerator( self.environment, self.configuration, self.__get_desired_count()).generate_cluster() log("Template generation complete.") change_set = create_change_set(self.client, environment_stack_template_body, self.cluster_name, self.key_name, self.environment) self.existing_events = get_stack_events(self.client, self.cluster_name) log_bold("Executing changeset. Checking progress...") self.client.execute_change_set( ChangeSetName=change_set['ChangeSetId']) self.__print_progress() except ClientError as e: log_err("No updates are to be performed") except Exception as e: raise e
def create_change_set(client, service_template_body, stack_name, key_name, environment): change_set_parameters = [{ 'ParameterKey': 'Environment', 'ParameterValue': environment }] if key_name: change_set_parameters.append({ 'ParameterKey': 'KeyPair', 'ParameterValue': key_name }) create_change_set_res = client.create_change_set( StackName=stack_name, ChangeSetName="cg" + uuid.uuid4().hex, TemplateBody=service_template_body, Parameters=change_set_parameters, Capabilities=['CAPABILITY_NAMED_IAM'], ChangeSetType='UPDATE') log("Changeset creation initiated. Checking the progress...") change_set = client.describe_change_set( ChangeSetName=create_change_set_res['Id']) while change_set['Status'] in ['CREATE_PENDING', 'CREATE_IN_PROGRESS']: sleep(1) status_string = '\x1b[2K\rChecking changeset status. Status: ' + \ change_set['Status'] sys.stdout.write(status_string) sys.stdout.flush() change_set = client.describe_change_set( ChangeSetName=create_change_set_res['Id']) status_string = '\x1b[2K\rChecking changeset status.. Status: ' + \ change_set['Status']+'\n' sys.stdout.write(status_string) if change_set['Status'] == 'FAILED': log_err("Changeset creation failed!") log_bold( change_set.get('StatusReason', "Check AWS console for reason.")) client.delete_change_set(ChangeSetName=create_change_set_res['Id']) exit(0) else: log_bold("Changeset created.. Following are the changes") _print_changes(change_set) if click.confirm('Do you want to execute the changeset?'): return change_set log_bold("Deleting changeset...") client.delete_change_set(ChangeSetName=create_change_set_res['Id']) log_bold("Done. Bye!") exit(0)
def run_job_for_all_services(self, job_name, target, kwargs): log_bold("{} concurrency: {}".format(job_name, DEPLOYMENT_CONCURRENCY)) jobs = [] service_info = self.service_info_fetcher.service_info for index, ecs_service_logical_name in enumerate(service_info): ecs_service_info = service_info[ecs_service_logical_name] log_bold(f"Queueing {job_name} of " + ecs_service_info['ecs_service_name']) color = DEPLOYMENT_COLORS[index % 3] services_configuration = self.service_configuration['services'] kwargs.update( dict( ecs_service_name=ecs_service_info['ecs_service_name'], secrets_name=ecs_service_info.get('secrets_name'), ecs_service_logical_name=ecs_service_logical_name, color=color, service_configuration=services_configuration.get( ecs_service_logical_name), region=self.region, )) process = multiprocessing.Process(target=target, kwargs=kwargs) jobs.append(process) all_exit_codes = [] for chunk_of_jobs in chunks(jobs, DEPLOYMENT_CONCURRENCY): for process in chunk_of_jobs: process.start() while True: sleep(1) exit_codes = [proc.exitcode for proc in chunk_of_jobs] if None not in exit_codes: break for exit_code in exit_codes: all_exit_codes.append(exit_code) if any(all_exit_codes) != 0: raise UnrecoverableException(f"{job_name} failed")
def _ensure_image_in_ecr(self): if self.version == 'dirty': image = None else: image = self._find_image_in_ecr(self.version) if image: log_intent("Image found in ECR") else: log_bold( f"Image not found in ECR. Building image {self.name} {self.version}" ) image_name = spinalcase(self.name) + ':' + self.version ecr_name = self.ecr_image_uri + ':' + self.version self._build_image(image_name) self._push_image(image_name, ecr_name) image = self._find_image_in_ecr(self.version) try: image_manifest = image['imageManifest'] self.ecr_client.put_image(repositoryName=self.repo_name, imageTag=self.version, imageManifest=image_manifest) except Exception: pass
def _build_image(self): image_name = self.local_image_uri log_bold( f'Building docker image {image_name} using {"default Dockerfile" if self.dockerfile is None else self.dockerfile}' ) command = self._build_command(image_name) env = os.environ if self._should_enable_buildkit(): env['DOCKER_BUILDKIT'] = '1' try: subprocess.check_call(command, env=env, shell=True) except subprocess.CalledProcessError as e: message = 'docker build exited with status: {}'.format( e.returncode) if e.output: message += '\n' message += e.output if e.stderr: message += '\n' message += e.output raise UnrecoverableException(message) log_bold("Built " + image_name)
def log_ips(self): for service in self.ecs_service_names: task_arns = self.ecs_client.list_tasks( cluster=self.cluster_name, serviceName=service)['taskArns'] tasks = self.ecs_client.describe_tasks(cluster=self.cluster_name, tasks=task_arns)['tasks'] container_instance_arns = [ task['containerInstanceArn'] for task in tasks ] container_instances = self.ecs_client.describe_container_instances( cluster=self.cluster_name, containerInstances=container_instance_arns )['containerInstances'] ecs_instance_ids = [ container['ec2InstanceId'] for container in container_instances ] ec2_reservations = self.ec2_client.describe_instances( InstanceIds=ecs_instance_ids)['Reservations'] log_bold(service, ) for reservation in ec2_reservations: instances = reservation['Instances'] ips = [instance['PrivateIpAddress'] for instance in instances] [log_intent(ip) for ip in ips] log("")
def run(self): try: log("Check if stack already exists for " + self.cluster_name) environment_stack = self.client.describe_stacks( StackName=self.cluster_name )['Stacks'][0] log(self.cluster_name + " stack exists. ID: " + environment_stack['StackId']) log_err("Cannot create environment with duplicate name: " + self.cluster_name) except Exception: log(self.cluster_name + " stack does not exist. Creating new stack.") # When creating a cluster, desired_instance count is same # as min_instance count environment_stack_template_body = ClusterTemplateGenerator( self.environment, self.configuration ).generate_cluster() self.existing_events = get_stack_events( self.client, self.cluster_name ) options = prepare_stack_options_for_template( environment_stack_template_body, self.environment, self.cluster_name) environment_stack = self.client.create_stack( StackName=self.cluster_name, Parameters=self.__get_parameter_values(), OnFailure='DO_NOTHING', Capabilities=['CAPABILITY_NAMED_IAM'], **options, ) log_bold("Submitted to cloudformation. Checking progress...") self.__print_progress() log_bold(self.cluster_name + " stack created. ID: " + environment_stack['StackId'])
def run(self): log_warning("Deploying to {self.region}".format(**locals())) self.init_stack_info() if not os.path.exists(self.env_sample_file): raise UnrecoverableException('env.sample not found. Exiting.') log_intent("name: " + self.name + " | environment: " + self.environment + " | version: " + str(self.version)) log_bold("Checking image in ECR") self.upload_artefacts() log_bold("Initiating deployment\n") ecs_client = EcsClient(None, None, self.region) jobs = [] for index, service_name in enumerate(self.ecs_service_names): log_bold("Starting to deploy " + service_name) color = DEPLOYMENT_COLORS[index % 3] image_url = self.ecr_image_uri image_url += (':' + self.version) process = multiprocessing.Process( target=deployer.deploy_new_version, args=( ecs_client, self.cluster_name, service_name, self.version, self.name, self.env_sample_file, self.environment, color, image_url ) ) jobs.append(process) process.start() exit_codes = [] while True: sleep(1) exit_codes = [proc.exitcode for proc in jobs] if None not in exit_codes: break if any(exit_codes) != 0: raise UnrecoverableException("Deployment failed")
def update(self): log_warning( "Update task definition to {self.region}".format(**locals())) if not os.path.exists(self.env_sample_file): raise UnrecoverableException('env.sample not found. Exiting.') ecr_client = EcrClient(self.name, self.region, self.build_args) ecr_client.set_version(self.version) log_intent("name: " + self.name + " | environment: " + self.environment + " | version: " + str(ecr_client.version)) log_bold("Checking image in ECR") ecr_client.build_and_upload_image() log_bold("Updating task definition\n") env_config = build_config(self.environment, self.name, self.env_sample_file) ecs_client = EcsClient(region=self.region) deployment = DeployAction(ecs_client, self.cluster_name, None) task_defn = self._apply_changes_over_current_task_defn( env_config, ecs_client, ecr_client, deployment) deployment.update_task_definition(task_defn) log_bold("Task definition successfully updated\n")
def _build_image(self, image_name): log_bold("Building docker image " + image_name) subprocess.check_call( ["docker", "build", "-t", image_name, self.working_dir]) log_bold("Built " + image_name)
def _build_image(self, image_name): log_bold("Building docker image " + image_name) command = self._build_command(image_name) subprocess.check_call(command, shell=True) log_bold("Built " + image_name)
def _validate_changes(self, configuration): service_schema = { "title": "service", "type": "object", "properties": { "http_interface": { "type": "object", "properties": { "internal": { "type": "boolean" }, "restrict_access_to": { "type": "array", "items": { "type": "string" } }, "container_port": { "type": "number" }, "health_check_path": { "type": "string", "pattern": "^\/.*$" } }, "required": ["internal", "restrict_access_to", "container_port"] }, "memory_reservation": { "type": "number", "minimum": 10, "maximum": 30000 }, "fargate": { "type": "object", "properties": { "cpu": { "type": "number", "minimum": 256, "maximum": 4096 }, "memory": { "type": "number", "minimum": 512, "maximum": 30720 } } }, "command": { "oneOf": [{ "type": "string" }, { "type": "null" }] } }, "required": ["memory_reservation", "command"] } schema = { # "$schema": "http://json-schema.org/draft-04/schema#", "title": "configuration", "type": "object", "properties": { "notifications_arn": { "type": "string" }, "services": { "type": "object", "patternProperties": { "^[a-zA-Z]+$": service_schema } }, "cloudlift_version": { "type": "string" } }, "required": ["cloudlift_version", "services"] } try: validate(configuration, schema) except ValidationError as validation_error: log_err(validation_error.message + " in " + str(".".join(list(validation_error.relative_path)))) exit(0) log_bold("Schema valid!")
def deduce_name(name): if name is None: name = os.path.basename(os.getcwd()) log_bold("Assuming the service name to be: " + name) return stringcase.spinalcase(name)
def validate(self): log_bold("Running post-save validation:") self._validate_changes(self.get_config(strip_cloudlift_version=False))
def _validate_changes(self, configuration): log_bold("\nValidating schema..") # TODO: add cidr etc validation schema = { # "$schema": "http://json-schema.org/draft-04/schema#", "title": "configuration", "type": "object", "properties": { self.environment: { "type": "object", "properties": { "cluster": { "type": "object", "properties": { "min_instances": {"type": "integer"}, "max_instances": {"type": "integer"}, "instance_type": {"type": "string"}, "key_name": {"type": "string"}, }, "required": [ "min_instances", "max_instances", "instance_type", "key_name" ] }, "environment": { "type": "object", "properties": { "notifications_arn": {"type": "string"}, "ssl_certificate_arn": {"type": "string"} }, "required": [ "notifications_arn", "ssl_certificate_arn" ] }, "region": {"type": "string"}, "vpc": { "type": "object", "properties": { "cidr": { "type": "string" }, "nat-gateway": { "type": "object", "properties": { "elastic-ip-allocation-id": { "type": "string" } }, "required": [ "elastic-ip-allocation-id" ] }, "subnets": { "type": "object", "properties": { "private": { "type": "object", "properties": { "subnet-1": { "type": "object", "properties": { "cidr": { "type": "string" } }, "required": [ "cidr" ] }, "subnet-2": { "type": "object", "properties": { "cidr": { "type": "string" } }, "required": [ "cidr" ] } }, "required": [ "subnet-1", "subnet-2" ] }, "public": { "type": "object", "properties": { "subnet-1": { "type": "object", "properties": { "cidr": { "type": "string" } }, "required": [ "cidr" ] }, "subnet-2": { "type": "object", "properties": { "cidr": { "type": "string" } }, "required": [ "cidr" ] } }, "required": [ "subnet-1", "subnet-2" ] } }, "required": [ "private", "public" ] } }, "required": [ "cidr", "nat-gateway", "subnets" ] } }, "required": [ "cluster", "environment", "region", "vpc" ] } }, "required": [self.environment] } try: validate(configuration, schema) except ValidationError as validation_error: error_path = str(".".join(list(validation_error.relative_path))) if error_path: raise UnrecoverableException(validation_error.message + " in " + error_path) else: raise UnrecoverableException(validation_error.message) log_bold("Schema valid!")
def _validate_changes(self, configuration): service_schema = { "title": "service", "type": "object", "properties": { "http_interface": { "type": "object", "properties": { "internal": { "type": "boolean" }, "alb": { "type": "object", "properties": { "create_new": { "type": "boolean", }, "listener_arn": { "type": "string" }, "target_5xx_error_threshold": { "type": "number" }, "host": { "type": "string" }, "path": { "type": "string" }, "priority": { "type": "number" } }, "required": [ "create_new", "target_5xx_error_threshold" ] }, "restrict_access_to": { "type": "array", "items": { "type": "string" } }, "container_port": { "type": "number" }, "health_check_path": { "type": "string", "pattern": "^\/.*$" }, "health_check_healthy_threshold_count": { "type": "number", "minimum": 2, "maximum": 10 }, "health_check_unhealthy_threshold_count": { "type": "number", "minimum": 2, "maxium": 10 }, "health_check_interval_seconds": { "type": "number", "minimum": 5, "maximum": 300 }, "health_check_timeout_seconds": { "type": "number", "minimum": 2, "maximum": 120 }, "load_balancing_algorithm": { "type": "string", "enum": ["round_robin", "least_outstanding_requests"] }, "deregistration_delay": { "type": "number" } }, "required": [ "internal", "restrict_access_to", "container_port" ] }, "tcp_interface": { "container_port": { "type": "number" }, "target_group_arn": { "type": "string" }, "target_security_group": { "type": "string" }, "required": ["container_port", "target_group_arn", "target_security_group"] }, "memory_reservation": { "type": "number", "minimum": 10, "maximum": 30000 }, "deployment": { "type": "object", "properties": { "maximum_percent": { "type": "number", "minimum": 100, "maximum": 200 }, }, "required": ["maximum_percent"] }, "fargate": { "type": "object", "properties": { "cpu": { "type": "number", "minimum": 256, "maximum": 4096 }, "memory": { "type": "number", "minimum": 512, "maximum": 30720 } } }, "command": { "oneOf": [ {"type": "string"}, {"type": "null"} ] }, "task_role_arn": { "oneOf": [ {"type": "string"}, {"type": "null"} ] }, "task_execution_role_arn": { "oneOf": [ {"type": "string"}, {"type": "null"} ] }, "stop_timeout": { "type": "number" }, "container_health_check": { "type": "object", "properties": { "command": { "type": "string" }, "start_period": { "type": "number" }, "retries": { "type": "number", "minimum": 1, "maximum": 10 }, "interval": { "type": "number", "minimum": 5, "maximum": 300 }, "timeout": { "type": "number", "minimum": 2, "maximum": 60 }, }, "required": ["command"] }, "placement_constraints": { "type": "array", "items": { "required": ["type"], "type": "object", "properties": { "type": { "type": "string", "enum": ["memberOf", "distinctInstance"], }, "expression": { "type": "string" } } }, }, "sidecars": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "image": { "type": "string" }, "command": { "type": "array", "items": { "type": "string" } }, "memory_reservation": { "type": "number" } }, "required": ["name", "image", "memory_reservation"] } }, "system_controls": { "type": "array", "items": { "type": "object", "properties": { "namespace": { "type": "string" }, "value": { "type": "string" } } }, }, "log_group": { "type": "string", }, "secrets_name": { "type": "string", }, "task_role_attached_managed_policy_arns": { "type": "array", "items": { "type": "string" } }, "autoscaling": { "type": "object", "properties": { "max_capacity": { "type": "number" }, "min_capacity": { "type": "number" }, "request_count_per_target": { "type": "object", "properties": { "alb_arn": { "type": "string" }, "target_value": { "type": "number" }, "scale_in_cool_down_seconds": { "type": "number" }, "scale_out_cool_down_seconds": { "type": "number" } }, "required": ['target_value', 'scale_in_cool_down_seconds', 'scale_out_cool_down_seconds'] }, }, "required": ['max_capacity', 'min_capacity', 'request_count_per_target'] }, "container_labels": { "type": "object", "patternProperties": { ".*": {"type": "string"} }, "additionalProperties": False }, }, "required": ["memory_reservation", "command"] } schema = { # "$schema": "http://json-schema.org/draft-04/schema#", "title": "configuration", "type": "object", "properties": { "notifications_arn": { "type": "string" }, "service_role_arn": { "oneOf": [ {"type": "string"}, {"type": "null"} ] }, "services": { "type": "object", "patternProperties": { "^[a-zA-Z]+$": service_schema } }, "ecr_repo": { "type": "object", "properties": { "account_id": {"type": "string"}, "assume_role_arn": {"type": "string"}, "name": {"type": "string"}, }, "required": ["name"] }, "cloudlift_version": { "type": "string" } }, "required": ["cloudlift_version", "services", "ecr_repo"] } try: validate(configuration, schema) except ValidationError as validation_error: errors = [str(i) for i in validation_error.relative_path] raise UnrecoverableException(validation_error.message + " in " + str(".".join(list(errors)))) log_bold("Schema valid!")