diff --git a/src/spring/HISTORY.md b/src/spring/HISTORY.md index 26a944c2639..7fd31a0051b 100644 --- a/src/spring/HISTORY.md +++ b/src/spring/HISTORY.md @@ -1,5 +1,9 @@ Release History =============== +1.27.1 +--- +* Support scenario of bringing your own container image for command `az spring export`. + 1.27.0 --- * Add command `az spring export` which is used to generate target resources definitions to help customer migrating from Azure Spring Apps to other Azure services, such as Azure Container Apps. diff --git a/src/spring/azext_spring/migration/converter/app_converter.py b/src/spring/azext_spring/migration/converter/app_converter.py index 98b995ae6c6..1c3cf796565 100644 --- a/src/spring/azext_spring/migration/converter/app_converter.py +++ b/src/spring/azext_spring/migration/converter/app_converter.py @@ -21,11 +21,12 @@ def transform_data(): def transform_data_item(self, app): blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) blueDeployment = self._transform_deployment(blueDeployment) + maxReplicas = blueDeployment.get("capacity", 5) if blueDeployment is not None else 5 greenDeployment = self.wrapper_data.get_green_deployment_by_app(app) greenDeployment = self._transform_deployment(greenDeployment) if self.wrapper_data.is_support_blue_green_deployment(app): logger.warning(f"Action Needed: you should manually deploy the deployment '{greenDeployment.get('name')}' of app '{app.get('name')}' in Azure Container Apps.") - tier = blueDeployment.get('sku', {}).get('tier') + tier = blueDeployment.get('sku', {}).get('tier') if blueDeployment is not None else 'Standard' serviceBinds = self._get_service_bind(app) ingress = self._get_ingress(app, tier) isPublic = app['properties'].get('public') @@ -46,6 +47,12 @@ def transform_data_item(self, app): "mountOptions": self._get_mount_options(disk_props), }) return { + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), + "containerRegistry": self._get_container_registry(app), + "args": self._get_args(app), + "commands": self._get_commands(app), "containerAppName": self._get_resource_name(app), "paramContainerAppImageName": self._get_param_name_of_container_image(app), "paramTargetPort": self._get_param_name_of_target_port(app), @@ -53,7 +60,7 @@ def transform_data_item(self, app): "ingress": ingress, "isPublic": isPublic, "minReplicas": 1, - "maxReplicas": blueDeployment.get("capacity", 5), + "maxReplicas": maxReplicas, "serviceBinds": serviceBinds, "blue": blueDeployment, "green": greenDeployment, @@ -99,7 +106,7 @@ def _get_service_bind(self, app): return service_bind def _transform_deployment(self, deployment): - if deployment is None or deployment == {}: + if deployment is None: return env = deployment.get('properties', {}).get('deploymentSettings', {}).get('environmentVariables', {}) liveness_probe = deployment.get('properties', {}).get('deploymentSettings', {}).get('livenessProbe', {}) @@ -233,9 +240,15 @@ def _get_ingress(self, app, tier): ingress = app['properties'].get('ingressSettings') if ingress is None: return None + transport = ingress.get('backendProtocol') + if transport == "Default": + transport = "auto" + else: + transport = "auto" + logger.warning(f"Mismatch: The backendProtocol '{transport}' of app '{app.get('name')}' is not supported in Azure Container Apps. Converting it to 'auto'.") return { "targetPort": 8080 if tier == "Enterprise" else 1025, - "transport": ingress.get('backendProtocol').replace("Default", "auto"), + "transport": transport, "sessionAffinity": ingress.get('sessionAffinity').replace("Cookie", "sticky").replace("None", "none") } @@ -245,3 +258,27 @@ def _convert_scale(self, scale): "maxReplicas": scale.get("maxReplicas", 5), "rules": scale.get("rules", []) } + + def _get_container_registry(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + server = blueDeployment['properties']['source'].get('customContainer').get('server', None) + username = blueDeployment['properties']['source'].get('customContainer').get('imageRegistryCredential', {}).get('username', None) + passwordSecretRefPerfix = server.replace(".", "") + passwordSecretRef = f"{passwordSecretRefPerfix}-{username}" + return { + "server": server, + "username": username, + "passwordSecretRef": passwordSecretRef, + "image": self._get_container_image(app), + } + + def _get_args(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + return blueDeployment['properties']['source'].get('customContainer').get('args', []) + + def _get_commands(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None and self.wrapper_data.is_support_custom_container_image_for_deployment(blueDeployment): + return blueDeployment['properties']['source'].get('customContainer').get('command', []) diff --git a/src/spring/azext_spring/migration/converter/base_converter.py b/src/spring/azext_spring/migration/converter/base_converter.py index 05bf5ad33c1..ff3f662d6e0 100644 --- a/src/spring/azext_spring/migration/converter/base_converter.py +++ b/src/spring/azext_spring/migration/converter/base_converter.py @@ -117,7 +117,7 @@ def _get_storage_unique_name(self, disk_props): access_mode = self._get_storage_access_mode(disk_props) storage_unique_name = f"{storage_name}|{account_name}|{share_name}|{mount_path}|{access_mode}" hash_value = hashlib.md5(storage_unique_name.encode()).hexdigest()[:16] # Take first 16 chars of hash - result = f"{storage_name}{hash_value}" + result = f"{storage_name}{hash_value}".replace("-", "").replace("_", "") return result[:32] # Ensure total length is no more than 32 def _get_mount_options(self, disk_props): @@ -134,6 +134,39 @@ def _get_storage_enable_subpath(self, disk_props): enableSubPath = disk_props.get('customPersistentDiskProperties', False).get('enableSubPath', False) return enableSubPath + def _get_app_storage_configs(self): + storage_configs = [] + apps = self.wrapper_data.get_apps() + for app in apps: + # Check if app has properties and customPersistentDiskProperties + if 'properties' in app and 'customPersistentDisks' in app['properties']: + disks = app['properties'].get('customPersistentDisks', []) + for disk_props in disks: + if self._get_storage_enable_subpath(disk_props) is True: + logger.warning("Mismatch: enableSubPath of custom persistent disks is not supported in Azure Container Apps.") + # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) + storage_config = { + 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), + 'storageName': self._get_storage_unique_name(disk_props), + 'shareName': self._get_storage_share_name(disk_props), + 'accessMode': self._get_storage_access_mode(disk_props), + 'accountName': self._get_storage_account_name(disk_props), + } + if storage_config not in storage_configs: + storage_configs.append(storage_config) + return storage_configs + +# app + def _get_container_image(self, app): + blueDeployment = self.wrapper_data.get_blue_deployment_by_app(app) + if blueDeployment is not None: + if self.wrapper_data.is_support_custom_container_image_for_app(app): + server = blueDeployment['properties']['source'].get('customContainer').get('server', '') + containerImage = blueDeployment['properties']['source'].get('customContainer').get('containerImage', '') + return f"{server}/{containerImage}" + else: + return None + # module name def _get_app_module_name(self, app): appName = self._get_resource_name(app) @@ -159,6 +192,11 @@ def _get_param_name_of_storage_account_key(self, disk_props): storage_unique_name = self._get_storage_unique_name(disk_props) return "containerAppEnvStorageAccountKeyOf_" + storage_unique_name + # get param name of paramContainerAppImagePassword + def _get_param_name_of_container_image_password(self, app): + appName = self._get_resource_name(app) + return "containerImagePasswordOf_" + appName.replace("-", "_") + class SourceDataWrapper: def __init__(self, source): @@ -210,7 +248,7 @@ def is_support_sba(self): return self.is_support_feature('Microsoft.AppPlatform/Spring/applicationLiveViews') def is_support_gateway(self): - return self.is_support_feature('Microsoft.AppPlatform/Spring/gateways/routeConfigs') + return self.is_support_feature('Microsoft.AppPlatform/Spring/gateways') def get_asa_service(self): return self.get_resources_by_type('Microsoft.AppPlatform/Spring')[0] @@ -228,12 +266,12 @@ def get_deployments_by_app(self, app): def get_blue_deployment_by_app(self, app): deployments = self.get_deployments_by_app(app) deployments = [deployment for deployment in deployments if deployment['properties']['active'] is True] - return deployments[0] if deployments else {} + return deployments[0] if deployments else None def get_green_deployment_by_app(self, app): deployments = self.get_deployments_by_app(app) deployments = [deployment for deployment in deployments if deployment['properties']['active'] is False] - return deployments[0] if deployments else {} + return deployments[0] if deployments else None def get_green_deployments(self): deployments = self.get_deployments() @@ -295,3 +333,24 @@ def is_enabled_system_assigned_identity_for_app(self, app): if identity is None: return False return identity.get('type') == 'SystemAssigned' + + def is_support_custom_container_image_for_deployment(self, deployment): + if deployment is None: + return False + return deployment['properties'].get('source') is not None and \ + deployment['properties']['source'].get('customContainer') is not None and \ + deployment['properties']['source'].get('type') == 'Container' and \ + deployment['properties']['source']['customContainer'].get('containerImage') is not None + + def is_support_custom_container_image_for_app(self, app): + blueDeployment = self.get_blue_deployment_by_app(app) + if blueDeployment is None: + return False + return self.is_support_custom_container_image_for_deployment(blueDeployment) + + def is_private_custom_container_image(self, app): + blueDeployment = self.get_blue_deployment_by_app(app) + if blueDeployment is None: + return False + if self.is_support_custom_container_image_for_app(app): + return blueDeployment['properties']['source'].get('customContainer').get('imageRegistryCredential', {}).get('username', None) is not None diff --git a/src/spring/azext_spring/migration/converter/cert_converter.py b/src/spring/azext_spring/migration/converter/cert_converter.py index 49c9add38a7..d9c7fec53be 100644 --- a/src/spring/azext_spring/migration/converter/cert_converter.py +++ b/src/spring/azext_spring/migration/converter/cert_converter.py @@ -16,7 +16,7 @@ def transform_data(): asa_content_certs = self.wrapper_data.get_content_certificates() for cert in asa_content_certs: logger.warning(f"Action Needed: The content certificate '{cert['name']}' cannot be exported automatically. Please export it manually.") - return self.wrapper_data.get_certificates() + return self.wrapper_data.get_keyvault_certificates() super().__init__(source, transform_data) def transform_data_item(self, cert): diff --git a/src/spring/azext_spring/migration/converter/environment_converter.py b/src/spring/azext_spring/migration/converter/environment_converter.py index 7fe1fa84eae..5ef811f3d3d 100644 --- a/src/spring/azext_spring/migration/converter/environment_converter.py +++ b/src/spring/azext_spring/migration/converter/environment_converter.py @@ -15,12 +15,11 @@ def __init__(self, source): def transform_data(): asa_service = self.wrapper_data.get_asa_service() name = self._get_resource_name(asa_service) - apps = self.wrapper_data.get_apps() - certs = self.wrapper_data.get_certificates() + certs = self.wrapper_data.get_keyvault_certificates() data = { "containerAppEnvName": name, "containerAppLogAnalyticsName": f"log-{name}", - "storages": self._get_app_storage_configs(apps), + "storages": self._get_app_storage_configs(), } if self._need_identity(certs): data["identity"] = { @@ -57,31 +56,3 @@ def _need_identity(self, certs): if certs is not None and len(certs) > 0: return True return False - - def _get_app_storage_configs(self, apps): - storage_configs = [] - for app in apps: - # Check if app has properties and customPersistentDiskProperties - if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties'].get('customPersistentDisks', []) - for disk_props in disks: - if self._get_storage_enable_subpath(disk_props) is True: - logger.warning("Mismatch: enableSubPath of custom persistent disks is not supported in Azure Container Apps.") - # print("storage_name + account_name + share_name + mount_path + access_mode:", storage_name + account_name + share_name + mountPath + access_mode) - storage_config = { - 'containerAppEnvStorageName': self._get_resource_name_of_storage(app, disk_props), - 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), - 'storageName': self._get_storage_unique_name(disk_props), - 'shareName': self._get_storage_share_name(disk_props), - 'accessMode': self._get_storage_access_mode(disk_props), - 'accountName': self._get_storage_account_name(disk_props), - } - storage_configs.append(storage_config) - # print("storage_configs:", storage_configs) - return storage_configs - - # get resource name of containerAppEnvStorageName - def _get_resource_name_of_storage(self, app, disk_props): - storage_name = self._get_storage_name(disk_props) - app_name = self._get_resource_name(app) - return (app_name + "_" + storage_name).replace("-", "_") diff --git a/src/spring/azext_spring/migration/converter/main_converter.py b/src/spring/azext_spring/migration/converter/main_converter.py index 072bbcf438b..5e22ca006f1 100644 --- a/src/spring/azext_spring/migration/converter/main_converter.py +++ b/src/spring/azext_spring/migration/converter/main_converter.py @@ -10,7 +10,7 @@ class MainConverter(BaseConverter): def __init__(self, source): def transform_data(): - asa_certs = self.wrapper_data.get_certificates() + asa_certs = self.wrapper_data.get_keyvault_certificates() certs = [] for cert in asa_certs: certName = self._get_resource_name(cert) @@ -21,7 +21,6 @@ def transform_data(): "templateName": templateName, } certs.append(certData) - storage_configs = [] apps_data = [] apps = self.wrapper_data.get_apps() for app in apps: @@ -34,22 +33,17 @@ def transform_data(): "paramContainerAppImageName": self._get_param_name_of_container_image(app), "paramTargetPort": self._get_param_name_of_target_port(app), "dependsOns": self._get_depends_on_list(app), + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), } - if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties']['customPersistentDisks'] - for disk_props in disks: - storage_config = { - 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), - } - storage_configs.append(storage_config) - apps_data.append(appData) return { "isVnet": self.wrapper_data.is_vnet(), "certs": certs, "apps": apps_data, - "storages": storage_configs, + "storages": self._get_app_storage_configs(), "gateway": self.wrapper_data.is_support_gateway(), "config": self.wrapper_data.is_support_configserver(), "eureka": self.wrapper_data.is_support_eureka(), diff --git a/src/spring/azext_spring/migration/converter/param_converter.py b/src/spring/azext_spring/migration/converter/param_converter.py index fa965c97a60..85d5ec6f41a 100644 --- a/src/spring/azext_spring/migration/converter/param_converter.py +++ b/src/spring/azext_spring/migration/converter/param_converter.py @@ -11,25 +11,21 @@ class ParamConverter(BaseConverter): def __init__(self, source): def transform_data(): apps = self.wrapper_data.get_apps() - storage_configs = [] apps_data = [] for app in apps: apps_data.append({ "appName": self._get_resource_name(app), "paramContainerAppImageName": self._get_param_name_of_container_image(app), "paramTargetPort": self._get_param_name_of_target_port(app), + "isByoc": self.wrapper_data.is_support_custom_container_image_for_app(app), + "isPrivateImage": self.wrapper_data.is_private_custom_container_image(app), + "paramContainerAppImagePassword": self._get_param_name_of_container_image_password(app), + "image": self._get_container_image(app), }) - if 'properties' in app and 'customPersistentDisks' in app['properties']: - disks = app['properties']['customPersistentDisks'] - for disk_props in disks: - storage_config = { - 'paramContainerAppEnvStorageAccountKey': self._get_param_name_of_storage_account_key(disk_props), - 'accountName': self._get_storage_account_name(disk_props), - } - storage_configs.append(storage_config) + return { "apps": apps_data, - "storages": storage_configs, + "storages": self._get_app_storage_configs(), "isVnet": self.wrapper_data.is_vnet() } super().__init__(source, transform_data) diff --git a/src/spring/azext_spring/migration/converter/readme_converter.py b/src/spring/azext_spring/migration/converter/readme_converter.py index b2e42f4f299..6bc85124c2d 100644 --- a/src/spring/azext_spring/migration/converter/readme_converter.py +++ b/src/spring/azext_spring/migration/converter/readme_converter.py @@ -19,9 +19,10 @@ def transform_data(): data = { "isVnet": self.wrapper_data.is_vnet(), - "containerApps": self.wrapper_data.get_container_deployments(), - "buildResultsApps": self.wrapper_data.get_build_results_deployments(), + "containerDeployments": self.wrapper_data.get_container_deployments(), + "buildResultsDeployments": self.wrapper_data.get_build_results_deployments(), "hasApps": len(apps) > 0, + "isSupportGateway": self.wrapper_data.is_support_gateway(), "isSupportConfigServer": self.wrapper_data.is_support_configserver(), "customDomains": self._transform_domains(custom_domains), "hasCerts": len(keyvault_certs) > 0 or len(content_certs) > 0, diff --git a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 index 86f5901d625..738dc850d0d 100644 --- a/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/app.bicep.j2 @@ -1,5 +1,12 @@ param containerAppName string = '{{data.containerAppName}}' +{%- if data.isByoc %} +{%- if data.isPrivateImage %} +@secure() +param {{data.paramContainerAppImagePassword}} string +{%- endif %} +{%- else %} param {{data.paramContainerAppImageName}} string +{%- endif %} param {{data.paramTargetPort}} int @description('Minimum number of replicas that will be deployed') @@ -33,6 +40,25 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { managedEnvironmentId: containerAppEnvId workloadProfileName: workloadProfileName configuration: { + {%- if data.isByoc %} + {%- if data.isPrivateImage %} + registries: [ + { + server: '{{data.containerRegistry.server}}' + username: '{{data.containerRegistry.username}}' + passwordSecretRef: '{{data.containerRegistry.passwordSecretRef}}' + } + ] + {%- endif %} + {%- if data.isPrivateImage %} + secrets: [ + { + name: '{{data.containerRegistry.passwordSecretRef}}' + value: {{data.paramContainerAppImagePassword}} + } + ] + {%- endif %} + {%- endif %} ingress: { {%- if data.ingress %} external: {% if data.isPublic %}true{% else %}false{% endif %} @@ -60,11 +86,34 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { containers: [ { name: containerAppName + {%- if data.isByoc %} + image: '{{data.containerRegistry.image}}' + {%- else %} image: {{data.paramContainerAppImageName}} + {%- endif %} + {%- if data.blue %} resources: { cpu: json('{{data.blue.cpuCore}}') memory: '{{data.blue.memorySize}}' } + {%- endif %} + {%- if data.args %} + args: [ + {%- for arg in data.args %} + '{{arg}}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + {%- if data.commands %} + command: [ + {%- for command in data.commands %} + '{{command}}' + {%- if not loop.last %}{%- endif %} + {%- endfor %} + ] + {%- endif %} + {%- if data.blue %} env: [ {%- for env in data.blue.env %} { @@ -73,6 +122,8 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { }{%- if not loop.last %}{%- endif %} {%- endfor %} ] + {%- endif %} + {%- if data.blue %} probes: [ {%- if data.blue.livenessProbe %} { @@ -171,6 +222,7 @@ resource {{data.moduleName}} 'Microsoft.App/containerApps@2024-03-01' = { } {%- endif %} ] + {%- endif %} {%- if data.volumeMounts %} volumeMounts: [ {%- for volume in data.volumeMounts %} diff --git a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 index 32e44872ef0..4dd4e3d267e 100644 --- a/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/config_server.bicep.j2 @@ -3,6 +3,7 @@ param managedEnvironments_aca_env_name string // Provide the credentials in the parameters below to access the Git repositories. // Example: param spring_cloud_config_server_git_username string = 'username' {%- for param in data.params %} +@secure() param {{param}} string = ' // please fill in the value {%- endfor %} diff --git a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 index 02d5afcf4e0..f2ae7479a3a 100644 --- a/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/environment.bicep.j2 @@ -78,9 +78,9 @@ resource maintenanceConfig 'Microsoft.App/managedEnvironments/maintenanceConfigu {%- if data.storages %} {%- for storage in data.storages %} - +@secure() param {{storage.paramContainerAppEnvStorageAccountKey}} string -resource {{storage.containerAppEnvStorageName}} 'Microsoft.App/managedEnvironments/storages@2024-08-02-preview' = { +resource {{storage.storageName}} 'Microsoft.App/managedEnvironments/storages@2024-08-02-preview' = { parent: containerAppEnv name: '{{storage.storageName}}' properties: { diff --git a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 index 339b21c6211..5791e57e602 100644 --- a/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 +++ b/src/spring/azext_spring/migration/converter/templates/main.bicep.j2 @@ -8,10 +8,18 @@ param maxNodes int param vnetSubnetId string {%- endif %} {%- for app in data.apps %} +{%- if app.isByoc %} +{%- if app.isPrivateImage %} +@secure() +param {{app.paramContainerAppImagePassword}} string +{%- endif %} +{%- else %} param {{app.paramContainerAppImageName}} string +{%- endif %} param {{app.paramTargetPort}} int {%- endfor %} {%- for storage in data.storages %} +@secure() param {{storage.paramContainerAppEnvStorageAccountKey}} string {%- endfor %} @@ -31,21 +39,21 @@ module containerAppEnv 'environment.bicep' = { } } -{%- for item in data.certs %} -module {{ item.moduleName }} '{{ item.templateName }}' = { - name: 'cert-{{ item.certName }}-Deployment' +{%- for cert in data.certs %} +module {{ cert.moduleName }} '{{ cert.templateName }}' = { + name: 'cert-{{ cert.certName }}-Deployment' params: { managedEnvironments_aca_env_name: containerAppEnv.outputs.containerAppEnvName } } {%- endfor %} -{%- for item in data.apps %} -module {{ item.moduleName }} '{{ item.templateName }}' = { - name: '{{ item.appName }}-Deployment' - {%- if item.dependsOns %} +{%- for app in data.apps %} +module {{ app.moduleName }} '{{ app.templateName }}' = { + name: '{{ app.appName }}-Deployment' + {%- if app.dependsOns %} dependsOn: [ - {%- for dependsOn in item.dependsOns %} + {%- for dependsOn in app.dependsOns %} {{ dependsOn }} {%- endfor %} ] @@ -53,8 +61,14 @@ module {{ item.moduleName }} '{{ item.templateName }}' = { params: { containerAppEnvId: containerAppEnv.outputs.containerAppEnvId workloadProfileName: workloadProfileName - {{item.paramContainerAppImageName}}: {{item.paramContainerAppImageName}} - {{item.paramTargetPort}}: {{item.paramTargetPort}} + {%- if app.isByoc %} + {%- if app.isPrivateImage %} + {{app.paramContainerAppImagePassword}}: {{app.paramContainerAppImagePassword}} + {%- endif %} + {%- else %} + {{app.paramContainerAppImageName}}: {{app.paramContainerAppImageName}} + {%- endif %} + {{app.paramTargetPort}}: {{app.paramTargetPort}} } } {%- endfor %} diff --git a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 index a5a99a6a04a..d8d2dd5bd76 100644 --- a/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 +++ b/src/spring/azext_spring/migration/converter/templates/param.bicepparam.j2 @@ -8,14 +8,23 @@ param maxNodes = 10 {%- if data.isVnet == true %} // Provide the full resource ID of the existing subnet, the subnet must be delegated to Microsoft.App/environments // Example: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} -param vnetSubnetId +param vnetSubnetId = {%- endif %} {%- for app in data.apps %} +{%- if app.isByoc %} +param {{app.paramTargetPort}} = 8080 +{%- if app.isPrivateImage %} +@secure() +param {{app.paramContainerAppImagePassword}} = 'fill in password for container retistry "{{app.image}}"' +{%- endif %} +{%- else %} param {{app.paramContainerAppImageName}} = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' param {{app.paramTargetPort}} = 80 +{%- endif %} {%- endfor %} {%- for storage in data.storages %} +@secure() param {{storage.paramContainerAppEnvStorageAccountKey}} = 'fill in account key for storage "{{storage.accountName}}"' {%- endfor %} \ No newline at end of file diff --git a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 index 947d747406b..01fe7eb4b5b 100644 --- a/src/spring/azext_spring/migration/converter/templates/readme.md.j2 +++ b/src/spring/azext_spring/migration/converter/templates/readme.md.j2 @@ -2,7 +2,7 @@ This README provides instructions on how to use the Bicep files generated from y Due to limitations in the Azure Container Apps backends, multiple deployment rounds may be expected to successfully provision all the resources. -After deploying the Bicep files, some configurations must be updated manually by following the steps outlined in [Azure public documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview). +After deploying the Bicep files, some configurations must be updated manually by following the steps outlined in [Azure public documentation](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-overview). ## Prerequisites @@ -16,9 +16,9 @@ Before getting started, make sure you have the following: ### Review the Bicep Files -After generating the Bicep files, the specified output directory will contain the following files and related resource definitions: +After generating the Bicep files, the specified output directory will contain: -- `main.bicep`: The entry point to deploy and manage Azure Container Apps resources +- `main.bicep`: The entry point to deploy and manage Azure Container Apps resources. - `param.bicepparam`: The parameters which values required for the deployment. - `environment.bicep`: The Bicep file that contains the resource definitions for the Azure Container Apps environment. - `-app.bicep`: The Bicep file that contains the resource definitions for the Azure Container Apps applications. @@ -33,28 +33,43 @@ Ensure the parameter values in `param.bicepparam` are properly set to configure {%- if data.isVnet %} #### VNet configuration -To migrate a custom virtual network, you must [create the VNet](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-network#create-an-azure-container-apps-environment-with-a-virtual-network) and specify the subnet ID in the `param.bicepparam` file. +To migrate a custom virtual network, you must [create the VNet](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-network#create-an-azure-container-apps-environment-with-a-virtual-network) and specify the subnet ID in the `param.bicepparam` file. {%- endif %} {%- if data.hasApps %} #### Application Containerization and Deployment +{%- if data.buildResultsDeployments %} +For the following applications/deployments built by Azure Spring Apps, you need to build the image and deploy it to Azure Container Apps. +{%- for deployment in data.buildResultsDeployments %} + - `{{deployment.name}}` +{%- endfor %} +{%- endif %} +{{ '\n' }} +{%- if data.containerDeployments %} +For the following applications/deployments using custom container registry, you need to provide the container registry password (if required) and target port in `param.bicepparam`. +{%- for deployment in data.containerDeployments %} + - `{{deployment.name}}` +{%- endfor %} +{%- endif %} + +{%- endif %} + You can choose one of the following options: - **Option 1**: Leave the image URL in `param.bicepparam` unchanged to deploy the quickstart applications. You can deploy your own applications later, after the Azure Container Apps environment is created. -- **Option 2**: Replace the image URL with your application's image and update the corresponding target ports. For more details on obtaining container images, refer to [Application containerization](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview), and learn how to [deploy them](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#deploy-an-application). If your images are hosted in private repositories in Azure Container Registry, you will need to provide the necessary credentials to Azure Container Apps. For more information, refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity-image-pull?tabs=bash&pivots=portal). +- **Option 2**: Replace the image URL with your application's image and update the corresponding target ports. For more details on obtaining container images, refer to [Application containerization](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-build-overview), and learn how to [deploy them](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#deploy-an-application). If your images are hosted in private repositories in Azure Container Registry, you will need to provide the necessary credentials to Azure Container Apps. For more information, refer to [this doc](https://learn.microsoft.com/azure/container-apps/managed-identity-image-pull). -For following applications that are built by Azure Spring Apps, you need to build the image and deploy to Azure Container Apps. -{%- for app in data.buildResultsApps %} - - `{{app.name}}` -{%- endfor %} -{%- endif %} -{%- if data.isSupportConfigServer %} #### Managed Components Configuration - -You need to update the GIT repoistories for the remote Git repository in the `config_server.bicep` file, such as providing the credentials. +{%- if data.isSupportGateway %} +- To migrate the Spring Cloud Gateway, follow [this doc](https://aka.ms/asa-scg-migration) to set up a self-hosted gateway in Azure Container Apps. Alternatively you can use the managed Gateway for Spring, which is currently in public preview. +{%- endif %} +{%- if data.isSupportConfigServer %} +- Update Git repository credentials in the `config_server.bicep` file if using private remote Git repositories, such as providing the credentials. {%- endif %} +You can refer to the migration doc on managed components for more details. + #### Mismatch during migration During the script execution, two types of messages may appear: @@ -109,6 +124,10 @@ You can check the deployment status either in the CLI output or in the Azure Por | ManagedIdentityNoPermission | The Managed Identity 'system' does not have permission for resource... | This error typically occurs when migrating Key Vault certificates. Since the system-assigned MI for ACA is created along with the environment, you need to assign it the **Key Vault Secrets User** role in your key vault after the environment is created, then redeploy the Bicep files.​ | | JavaComponentOperationError | Failed to create config map external-auth-config-map for JavaComponent '' in k8se-system namespace. | Creating a Java component immediately after the environment is set up sometimes fails due to a server-side issue. you can redeploy the Bicep files to solve this issue. | | JavaComponentOperationError | Failed to provision java component '', Java Component is invalid, reason: admission webhook \"javacomponent.kb.io\" denied the request... | Fix the properties in the error message and redeploy the bicep files. Or find more details in the logs of managed components. | + | InvalidCertificateValueFormat | The certificate value should be base64 string. | Manually upload certificate to Azure Container Apps environment. | + | ManagedEnvironmentSubnetDelegationError | The subnet of the environment must be delegated to the service 'Microsoft.App/environments'. | Update subnet with param `--delegations Microsoft.App/environments` to delegate subnet | + +{{ '\n' }} ## Post-Deployment Configuration Some properties and configurations are not included in the Bicep files and must be manually updated. @@ -124,7 +143,7 @@ Following TLS certificates from Azure Key Vault have been migrated by the migrat {%- if data.contentCerts %} -Following content type certificates should be manually uploaded to Azure Container Apps environment: +Please be aware that the following certificates cannot be migrated automatically due to API limitations. If necessary, you can manually upload the original certificate file to the Azure Container Apps environment: {%- for cert in data.contentCerts %} - `{{cert.name}}` {%- endfor %} @@ -133,7 +152,7 @@ Following content type certificates should be manually uploaded to Azure Contain {%- if data.customDomains %} ### Custom Domain -You have custom domains configured in Azure Spring Apps. You can follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. Please manually update custom domains of following applications: +You have custom domains configured in Azure Spring Apps. You can follow the steps in [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-custom-domain) to configure the custom domain in Azure Container Apps. Please manually update custom domains of following applications: {%- for domain in data.customDomains %} - `{{domain.appName}}/{{domain.name}}` {%- endfor %} @@ -141,7 +160,7 @@ You have custom domains configured in Azure Spring Apps. You can follow the step {%- if data.systemAssignedIdentityApps %} ### Managed Identity -Following applications have enabled system-assigned managed identity in Azure Spring Apps, your Azure Container Apps app instance will also have enabled system-assigned managed identity, but you need to reassign the corresponding roles, which you can identify from the error message if the deployment fails, to that managed identity. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. +Following applications have enabled system-assigned managed identity in Azure Spring Apps, your Azure Container Apps app instance will also have enabled system-assigned managed identity, but you need to reassign the corresponding roles, which you can identify from the error message if the deployment fails, to that managed identity. You can refer to [this doc](https://learn.microsoft.com/azure/container-apps/managed-identity) to understand how to use managed identity in Azure Container Apps. {%- for app in data.systemAssignedIdentityApps %} - `{{app.name}}` {%- endfor %} @@ -149,16 +168,16 @@ Following applications have enabled system-assigned managed identity in Azure Sp {%- if data.greenDeployments %} ### Blue-green deployment -Following applications have enabled blue-green deployments in Azure Spring Apps, the **blue** deployments have been migrated to Azure Container Apps, but you need to follow the steps in [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. +Following applications have enabled blue-green deployments in Azure Spring Apps, the **blue** deployments have been migrated to Azure Container Apps, but you need to follow the steps in [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-blue-green) to configure the **green** deployment in Azure Container Apps. {%- for deployment in data.greenDeployments %} - - Please manually deploy your staging deployment `{{deployment.appName}}/{{deployment.name}}`. Refer to this [doc](https://learn.microsoft.com/en-us/azure/spring-apps/basic-standard/how-to-staging-environment). + - Please manually deploy your staging deployment `{{deployment.appName}}/{{deployment.name}}`. Refer to this [doc](https://learn.microsoft.com/azure/spring-apps/basic-standard/how-to-staging-environment). {%- endfor %} {%- endif %} ### Auto scale -Because Azure Container Apps leverage Kubernetes Event-driven Autoscaling (KEDA) for auto-scaling, which is different from Azure Spring Apps that use Azure Monitor, the migration tools cannot transfer these settings for you. You will need to manually configure the auto-scaling settings in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. +Since Azure Container Apps leverage Kubernetes Event-driven Autoscaling (KEDA) for auto-scaling, which is different from Azure Spring Apps that uses Azure Monitor, the tool cannot transfer these settings for you. You need to manually configure the auto-scaling settings in Azure Container Apps. Refer to [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-application-overview#scale) to learn how to set up auto scaling in Azure Container Apps. ### Monitoring -The migration has enabled logging and monitoring in Azure Container Apps. You can refer to [this doc](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. +The migration has enabled logging and monitoring in Azure Container Apps. You can refer to [this doc](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-monitoring) for sample queries to help with troubleshooting. -Refer to the [migration documentation](https://learn.microsoft.com/en-us/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more detailed feature migration steps. \ No newline at end of file +Refer to the [official migration documentation](https://learn.microsoft.com/azure/spring-apps/migration/migrate-to-azure-container-apps-overview) for more details on feature migration. \ No newline at end of file diff --git a/src/spring/setup.py b/src/spring/setup.py index 4465bd1746c..20fc782bacf 100644 --- a/src/spring/setup.py +++ b/src/spring/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.27.0' +VERSION = '1.27.1' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers