diff --git a/.autover/changes/3bbbb972-6181-431c-8313-097ce38a2463.json b/.autover/changes/3bbbb972-6181-431c-8313-097ce38a2463.json new file mode 100644 index 00000000..f3b44171 --- /dev/null +++ b/.autover/changes/3bbbb972-6181-431c-8313-097ce38a2463.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "AWS.Deploy.CLI", + "Type": "Minor", + "ChangelogMessages": [ + "Add support for deploying ARM web apps to ECS Fargate" + ] + } + ] +} \ No newline at end of file diff --git a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs index 98454fe3..c8d8b642 100644 --- a/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs +++ b/src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs @@ -53,7 +53,8 @@ public string Build(CloudApplication cloudApplication, Recommendation recommenda ECRRepositoryName = recommendation.DeploymentBundle.ECRRepositoryName ?? "", ECRImageTag = recommendation.DeploymentBundle.ECRImageTag ?? "", DotnetPublishZipPath = recommendation.DeploymentBundle.DotnetPublishZipPath ?? "", - DotnetPublishOutputDirectory = recommendation.DeploymentBundle.DotnetPublishOutputDirectory ?? "" + DotnetPublishOutputDirectory = recommendation.DeploymentBundle.DotnetPublishOutputDirectory ?? "", + EnvironmentArchitecture = recommendation.DeploymentBundle.EnvironmentArchitecture.ToString() }; // Persist deployment bundle settings diff --git a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs index 237548de..d6c50edc 100644 --- a/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs +++ b/src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs @@ -74,9 +74,11 @@ public async Task BuildDockerImage(CloudApplication cloudApplication, Recommenda DockerUtilities.TryGetAbsoluteDockerfile(recommendation, _fileManager, _directoryManager, out var dockerFile); var dockerBuildCommand = $"docker build -t {imageTag} -f \"{dockerFile}\"{buildArgs} ."; - if (RuntimeInformation.OSArchitecture != Architecture.X64) + var currentArchitecture = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? SupportedArchitecture.Arm64 : SupportedArchitecture.X86_64; + var dockerPlatform = recommendation.DeploymentBundle.EnvironmentArchitecture == SupportedArchitecture.Arm64 ? "linux/arm64" : "linux/amd64"; + if (currentArchitecture != recommendation.DeploymentBundle.EnvironmentArchitecture) { - dockerBuildCommand = $"docker buildx build --platform linux/amd64 -t {imageTag} -f \"{dockerFile}\"{buildArgs} ."; + dockerBuildCommand = $"docker buildx build --platform {dockerPlatform} -t {imageTag} -f \"{dockerFile}\"{buildArgs} ."; } _interactiveService.LogInfoMessage($"Docker Execution Directory: {Path.GetFullPath(dockerExecutionDirectory)}"); diff --git a/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs index 76f4c0d3..d4de58b8 100644 --- a/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs +++ b/src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs @@ -41,6 +41,11 @@ public interface IRecipeProps /// string? DotnetPublishOutputDirectory { get; set; } + /// + /// The CPU architecture of the environment to create. + /// + string? EnvironmentArchitecture { get; set; } + /// /// The ID of the recipe being used to deploy the application. /// @@ -62,7 +67,7 @@ public interface IRecipeProps string? DeploymentBundleSettings { get; set; } /// - /// The Region used during deployment. + /// The Region used during deployment. /// string? AWSRegion { get; set; } @@ -108,6 +113,11 @@ public class RecipeProps : IRecipeProps /// public string? DotnetPublishOutputDirectory { get; set; } + /// + /// The CPU architecture of the environment to create. + /// + public string? EnvironmentArchitecture { get; set; } + /// /// The ID of the recipe being used to deploy the application. /// @@ -129,7 +139,7 @@ public class RecipeProps : IRecipeProps public string? DeploymentBundleSettings { get; set; } /// - /// The Region used during deployment. + /// The Region used during deployment. /// public string? AWSRegion { get; set; } diff --git a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs index a8c73000..83f127d4 100644 --- a/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs +++ b/src/AWS.Deploy.Recipes/CdkTemplates/AspNetAppEcsFargate/Generated/Recipe.cs @@ -135,7 +135,11 @@ private void ConfigureECSClusterAndService(IRecipeProps recipeCon { TaskRole = AppIAMTaskRole, Cpu = settings.TaskCpu, - MemoryLimitMiB = settings.TaskMemory + MemoryLimitMiB = settings.TaskMemory, + RuntimePlatform = new RuntimePlatform + { + CpuArchitecture = CpuArchitecture.Of(recipeConfiguration.EnvironmentArchitecture ?? string.Empty) + } })); AppLogging = new AwsLogDriver(InvokeCustomizeCDKPropsEvent(nameof(AppLogging), this, new AwsLogDriverProps diff --git a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe index 8c348286..fc297281 100644 --- a/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe +++ b/src/AWS.Deploy.Recipes/RecipeDefinitions/ASP.NETAppECSFargate.recipe @@ -1,7 +1,7 @@ { "$schema": "./aws-deploy-recipe-schema.json", "Id": "AspNetAppEcsFargate", - "Version": "1.1.2", + "Version": "1.2.0", "Name": "ASP.NET Core App to Amazon ECS using AWS Fargate", "DeploymentType": "CdkProject", "DeploymentBundle": "Container", @@ -11,7 +11,7 @@ "Description": "This ASP.NET Core application will be deployed to Amazon Elastic Container Service (Amazon ECS) with compute power managed by AWS Fargate compute engine. If your project does not contain a Dockerfile, it will be automatically generated, otherwise an existing Dockerfile will be used. Recommended if you want to deploy your application as a container image on Linux.", "TargetService": "Amazon Elastic Container Service", "TargetPlatform": "Linux", - "SupportedArchitectures": [ "x86_64" ], + "SupportedArchitectures": [ "x86_64", "arm64" ], "DisplayedResources": [ { diff --git a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs index 2464abbc..5493101b 100644 --- a/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs +++ b/test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs @@ -253,6 +253,55 @@ public async Task AppRunnerDeployment() Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); } + [Fact] + public async Task FargateArmDeployment() + { + _stackName = $"FargateArmDeployment{Guid.NewGuid().ToString().Split('-').Last()}"; + + // Arrange input for deploy + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation + await _interactiveService.StdInWriter.WriteLineAsync("8"); // Select "Environment Architecture" + await _interactiveService.StdInWriter.WriteLineAsync("2"); // Select "Arm64" + await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Confirm selection and deploy + await _interactiveService.StdInWriter.FlushAsync(); + + // Deploy + var projectPath = _testAppManager.GetProjectPath(Path.Combine("testapps", "WebAppWithDockerFile", "WebAppWithDockerFile.csproj")); + var deployArgs = new[] { "deploy", "--project-path", projectPath, "--application-name", _stackName, "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deployArgs)); + + // Verify application is deployed and running + Assert.Equal(StackStatus.CREATE_COMPLETE, await _cloudFormationHelper.GetStackStatus(_stackName)); + + var deployStdOut = _interactiveService.StdOutReader.ReadAllLines(); + + var applicationUrl = deployStdOut.First(line => line.Trim().StartsWith("Endpoint:")) + .Split(" ")[1] + .Trim(); + + // URL could take few more minutes to come live, therefore, we want to wait and keep trying for a specified timeout + await _httpHelper.WaitUntilSuccessStatusCode(applicationUrl, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(5)); + + // list + var listArgs = new[] { "list-deployments", "--diagnostics" }; + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(listArgs));; + + // Verify stack exists in list of deployments + var listDeployStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList(); + Assert.Contains(listDeployStdOut, (deployment) => _stackName.Equals(deployment)); + + // Arrange input for delete + await _interactiveService.StdInWriter.WriteAsync("y"); // Confirm delete + await _interactiveService.StdInWriter.FlushAsync(); + var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics" }; + + // Delete + Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs));; + + // Verify application is deleted + Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName)); + } + public void Dispose() { Dispose(true); diff --git a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs index d23c5632..4d894ef0 100644 --- a/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs +++ b/test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs @@ -73,6 +73,64 @@ public DeploymentBundleHandlerTests() It.IsAny()).Object; } + [Fact] + public async Task BuildDockerImage_EnvironmentArchitectureNotSet() + { + var projectPath = SystemIOUtilities.ResolvePath("WebAppWithDockerFile"); + var project = await _projectDefinitionParser.Parse(projectPath); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + + var cloudApplication = new CloudApplication("WebAppWithDockerFile", string.Empty, CloudApplicationResourceType.CloudFormationStack, recommendation.Recipe.Id); + var imageTag = "imageTag"; + var dockerFilePath = Path.GetFullPath(Path.Combine(".", "Dockerfile"), recommendation.GetProjectDirectory()); + + await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation, imageTag); + + // Explicitly checking for X64 because the default value for EnvironmentArchitecture is X86_64 + // This test will help catch a change in the default value + if (RuntimeInformation.OSArchitecture.Equals(Architecture.X64)) + { + Assert.Equal($"docker build -t {imageTag} -f \"{dockerFilePath}\" .", + _commandLineWrapper.CommandsToExecute.First().Command); + } + else + { + Assert.Equal($"docker buildx build --platform linux/amd64 -t {imageTag} -f \"{dockerFilePath}\" .", + _commandLineWrapper.CommandsToExecute.First().Command); + } + } + + [Theory] + [InlineData(SupportedArchitecture.X86_64)] + [InlineData(SupportedArchitecture.Arm64)] + public async Task BuildDockerImage_EnvironmentArchitectureIsSet(SupportedArchitecture environmentArchitecture) + { + var projectPath = SystemIOUtilities.ResolvePath("WebAppWithDockerFile"); + var project = await _projectDefinitionParser.Parse(projectPath); + var recommendation = new Recommendation(_recipeDefinition, project, 100, new Dictionary()); + + var cloudApplication = new CloudApplication("WebAppWithDockerFile", string.Empty, CloudApplicationResourceType.CloudFormationStack, recommendation.Recipe.Id); + var imageTag = "imageTag"; + var dockerFilePath = Path.GetFullPath(Path.Combine(".", "Dockerfile"), recommendation.GetProjectDirectory()); + + recommendation.DeploymentBundle.EnvironmentArchitecture = environmentArchitecture; + + await _deploymentBundleHandler.BuildDockerImage(cloudApplication, recommendation, imageTag); + + var currentArchitecture = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? SupportedArchitecture.Arm64 : SupportedArchitecture.X86_64; + if (currentArchitecture.Equals(environmentArchitecture)) + { + Assert.Equal($"docker build -t {imageTag} -f \"{dockerFilePath}\" .", + _commandLineWrapper.CommandsToExecute.First().Command); + } + else + { + var dockerPlatform = recommendation.DeploymentBundle.EnvironmentArchitecture == SupportedArchitecture.Arm64 ? "linux/arm64" : "linux/amd64"; + Assert.Equal($"docker buildx build --platform {dockerPlatform} -t {imageTag} -f \"{dockerFilePath}\" .", + _commandLineWrapper.CommandsToExecute.First().Command); + } + } + [Fact] public async Task BuildDockerImage_DockerExecutionDirectoryNotSet() { @@ -198,15 +256,15 @@ public async Task InspectDockerImage_ExecutedCommandCheck() await _deploymentBundleHandler.InspectDockerImageEnvironmentVariables(recommendation, "imageTag"); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Assert.Equal("docker inspect --format '{{ index (index .Config.Env) }}' imageTag", - _commandLineWrapper.CommandsToExecute.First().Command); + Assert.Equal("docker inspect --format \"{{ index (index .Config.Env) }}\" imageTag", + _commandLineWrapper.CommandsToExecute.First().Command); } else { - Assert.Equal("docker inspect --format \"{{ index (index .Config.Env) }}\" imageTag", - _commandLineWrapper.CommandsToExecute.First().Command); + Assert.Equal("docker inspect --format '{{ index (index .Config.Env) }}' imageTag", + _commandLineWrapper.CommandsToExecute.First().Command); } }