6969logger = get_logger (__name__ )
7070
7171# Default resource and scaling configuration constants
72- DEFAULT_CPU = " 0.25 vCPU"
73- DEFAULT_MEMORY = " 0.5 GB"
74- DEFAULT_MIN_SIZE = 1
75- DEFAULT_MAX_SIZE = 25
72+ DEFAULT_CPU = 0.25 # vCPU
73+ DEFAULT_MEMORY = 0.5 # GB
74+ DEFAULT_MIN_REPLICAS = 1
75+ DEFAULT_MAX_REPLICAS = 25
7676DEFAULT_MAX_CONCURRENCY = 100
7777
7878# AWS App Runner built-in limits
@@ -407,7 +407,7 @@ def get_tags(
407407 """
408408 tags = {
409409 ** settings .tags ,
410- "zenml-deployment-uuid " : str (deployment .id ),
410+ "zenml-deployment-id " : str (deployment .id ),
411411 "zenml-deployment-name" : deployment .name ,
412412 "zenml-deployer-name" : str (self .name ),
413413 "zenml-deployer-id" : str (self .id ),
@@ -473,6 +473,13 @@ def _sanitize_name(
473473 sanitized ,
474474 )
475475
476+ # Remove leading and trailing extra allowed characters before truncating
477+ sanitized = re .sub (
478+ r"^[^a-zA-Z0-9]+|[^a-zA-Z0-9]+$" ,
479+ "" ,
480+ sanitized ,
481+ )
482+
476483 # Truncate to fit within max_length character limit including suffix
477484 max_base_length = (
478485 max_length - len (random_suffix ) - 1
@@ -1025,6 +1032,8 @@ def _requires_service_replacement(
10251032 def _convert_resource_settings_to_aws_format (
10261033 self ,
10271034 resource_settings : ResourceSettings ,
1035+ resource_combinations : List [Tuple [float , float ]],
1036+ strict_resource_matching : bool = False ,
10281037 ) -> Tuple [str , str ]:
10291038 """Convert ResourceSettings to AWS App Runner resource format.
10301039
@@ -1033,6 +1042,12 @@ def _convert_resource_settings_to_aws_format(
10331042
10341043 Args:
10351044 resource_settings: The resource settings from pipeline configuration.
1045+ resource_combinations: List of supported CPU (vCPU) and memory (GB)
1046+ combinations.
1047+ strict_resource_matching: Whether to enforce strict matching of
1048+ resource requirements to AWS App Runner supported CPU and
1049+ memory combinations or approximate the closest matching
1050+ supported combination.
10361051
10371052 Returns:
10381053 Tuple of (cpu, memory) in AWS App Runner format.
@@ -1043,7 +1058,10 @@ def _convert_resource_settings_to_aws_format(
10431058 requested_memory_gb = resource_settings .get_memory (unit = "GB" )
10441059
10451060 cpu , memory = self ._select_aws_cpu_memory_combination (
1046- requested_cpu , requested_memory_gb
1061+ requested_cpu ,
1062+ requested_memory_gb ,
1063+ resource_combinations ,
1064+ strict_resource_matching ,
10471065 )
10481066
10491067 return cpu , memory
@@ -1052,10 +1070,12 @@ def _select_aws_cpu_memory_combination(
10521070 self ,
10531071 requested_cpu : Optional [float ],
10541072 requested_memory_gb : Optional [float ],
1073+ resource_combinations : List [Tuple [float , float ]],
1074+ strict_resource_matching : bool = False ,
10551075 ) -> Tuple [str , str ]:
10561076 """Select the best AWS App Runner CPU-memory combination.
10571077
1058- AWS App Runner only supports these specific combinations:
1078+ AWS App Runner only supports specific CPU and memory combinations, e.g. :
10591079 - 0.25 vCPU: 0.5 GB, 1 GB
10601080 - 0.5 vCPU: 1 GB
10611081 - 1 vCPU: 2 GB, 3 GB, 4 GB
@@ -1067,36 +1087,37 @@ def _select_aws_cpu_memory_combination(
10671087 Args:
10681088 requested_cpu: Requested CPU count (can be None)
10691089 requested_memory_gb: Requested memory in GB (can be None)
1090+ resource_combinations: List of supported CPU (vCPU) and memory (GB)
1091+ combinations.
1092+ strict_resource_matching: Whether to enforce strict matching of
1093+ resource requirements to AWS App Runner supported CPU and
1094+ memory combinations or approximate the closest matching
1095+ supported combination.
10701096
10711097 Returns:
1072- Tuple of (cpu, memory) that best matches requirements
1098+ Tuple of (cpu, memory) that best matches requirements, in AWS App
1099+ Runner format.
10731100 """
1074- valid_combinations = [
1075- # (cpu_value, cpu_string, memory_value, memory_string)
1076- (0.25 , "0.25 vCPU" , 0.5 , "0.5 GB" ),
1077- (0.25 , "0.25 vCPU" , 1.0 , "1 GB" ),
1078- (0.5 , "0.5 vCPU" , 1.0 , "1 GB" ),
1079- (1.0 , "1 vCPU" , 2.0 , "2 GB" ),
1080- (1.0 , "1 vCPU" , 3.0 , "3 GB" ),
1081- (1.0 , "1 vCPU" , 4.0 , "4 GB" ),
1082- (2.0 , "2 vCPU" , 4.0 , "4 GB" ),
1083- (2.0 , "2 vCPU" , 6.0 , "6 GB" ),
1084- (4.0 , "4 vCPU" , 8.0 , "8 GB" ),
1085- (4.0 , "4 vCPU" , 10.0 , "10 GB" ),
1086- (4.0 , "4 vCPU" , 12.0 , "12 GB" ),
1087- ]
1088-
10891101 if requested_cpu is None and requested_memory_gb is None :
1090- return DEFAULT_CPU , DEFAULT_MEMORY
1102+ return f"{ DEFAULT_CPU :g} vCPU" , f"{ DEFAULT_MEMORY :g} GB"
1103+
1104+ sorted_combinations = sorted (resource_combinations )
10911105
10921106 best_combination = None
1107+ exact_match = False
10931108 best_score = float ("inf" ) # Lower is better
10941109
1095- for cpu_val , cpu_str , mem_val , mem_str in valid_combinations :
1110+ for cpu_val , mem_val in sorted_combinations :
10961111 cpu_ok = requested_cpu is None or cpu_val >= requested_cpu
10971112 mem_ok = (
10981113 requested_memory_gb is None or mem_val >= requested_memory_gb
10991114 )
1115+ exact_match = (
1116+ cpu_val == requested_cpu and mem_val == requested_memory_gb
1117+ )
1118+ if exact_match :
1119+ best_combination = (cpu_val , mem_val )
1120+ break
11001121
11011122 if cpu_ok and mem_ok :
11021123 # Calculate "waste" score (how much over-provisioning)
@@ -1114,13 +1135,27 @@ def _select_aws_cpu_memory_combination(
11141135
11151136 if score < best_score :
11161137 best_score = score
1117- best_combination = (cpu_str , mem_str )
1138+ best_combination = (cpu_val , mem_val )
11181139
11191140 # If no combination satisfies requirements, use the highest available
11201141 if best_combination is None :
1121- return "4 vCPU" , "12 GB"
1142+ best_combination = sorted_combinations [- 1 ]
1143+
1144+ result = (
1145+ f"{ best_combination [0 ]:g} vCPU" ,
1146+ f"{ best_combination [1 ]:g} GB" ,
1147+ )
1148+
1149+ if strict_resource_matching and not exact_match :
1150+ raise ValueError (
1151+ f"Requested resource requirements ({ requested_cpu } vCPU, "
1152+ f"{ requested_memory_gb } GB) cannot be matched to any of the "
1153+ f"supported combinations for the AWS App Runner service. "
1154+ f"The closest matching combination is { result [0 ]} and "
1155+ f"{ result [1 ]} ."
1156+ )
11221157
1123- return best_combination
1158+ return result
11241159
11251160 def _convert_scaling_settings_to_aws_format (
11261161 self ,
@@ -1132,22 +1167,23 @@ def _convert_scaling_settings_to_aws_format(
11321167 resource_settings: The resource settings from pipeline configuration.
11331168
11341169 Returns:
1135- Tuple of (min_size, max_size, max_concurrency) for AWS App Runner.
1170+ Tuple of (min_replicas, max_replicas, max_concurrency) for AWS App
1171+ Runner.
11361172 """
1137- min_size = DEFAULT_MIN_SIZE
1173+ min_replicas = DEFAULT_MIN_REPLICAS
11381174 if resource_settings .min_replicas is not None :
1139- min_size = max (
1175+ min_replicas = max (
11401176 1 , resource_settings .min_replicas
11411177 ) # AWS App Runner min is 1
11421178
1143- max_size = DEFAULT_MAX_SIZE
1179+ max_replicas = DEFAULT_MAX_REPLICAS
11441180 if resource_settings .max_replicas is not None :
11451181 # ResourceSettings uses 0 to mean "no limit"
11461182 # AWS App Runner needs a specific value, so we use the platform maximum
11471183 if resource_settings .max_replicas == 0 :
1148- max_size = AWS_APP_RUNNER_MAX_SIZE
1184+ max_replicas = AWS_APP_RUNNER_MAX_SIZE
11491185 else :
1150- max_size = min (
1186+ max_replicas = min (
11511187 resource_settings .max_replicas , AWS_APP_RUNNER_MAX_SIZE
11521188 )
11531189
@@ -1158,7 +1194,7 @@ def _convert_scaling_settings_to_aws_format(
11581194 AWS_APP_RUNNER_MAX_CONCURRENCY ,
11591195 )
11601196
1161- return min_size , max_size , max_concurrency
1197+ return min_replicas , max_replicas , max_concurrency
11621198
11631199 def do_provision_deployment (
11641200 self ,
@@ -1199,6 +1235,8 @@ def do_provision_deployment(
11991235
12001236 cpu , memory = self ._convert_resource_settings_to_aws_format (
12011237 resource_settings ,
1238+ self .config .resource_combinations ,
1239+ settings .strict_resource_matching ,
12021240 )
12031241 min_size , max_size , max_concurrency = (
12041242 self ._convert_scaling_settings_to_aws_format (
@@ -1258,12 +1296,14 @@ def do_provision_deployment(
12581296 elif "amazonaws.com" in image :
12591297 image_repo_type = "ECR"
12601298 else :
1261- image_repo_type = "ECR_PUBLIC" # Default fallback
1262- logger .warning (
1263- "App Runner only supports ECR and ECR public repositories and "
1264- f"the container image '{ image } ' does not appear to be hosted on "
1265- "either of them. Proceeding with the deployment, but be warned "
1266- "that the App Runner service will probably fail."
1299+ raise DeploymentProvisionError (
1300+ f"AWS App Runner only supports Amazon ECR and ECR Public "
1301+ f"repositories. The container image '{ image } ' does not appear "
1302+ f"to be hosted on either platform. Supported image repositories:\n "
1303+ f"- ECR Public: public.ecr.aws/...\n "
1304+ f"- ECR Private: *.amazonaws.com/...\n "
1305+ f"Please push your image to one of these registries before "
1306+ f"deploying to App Runner."
12671307 )
12681308
12691309 image_config : Dict [str , Any ] = {
0 commit comments