Skip to content

Commit

Permalink
feat: add support for Web App ARM deployments on ECS Fargate
Browse files Browse the repository at this point in the history
  • Loading branch information
philasmar committed Oct 14, 2024
1 parent a94d3e0 commit 369fb7a
Show file tree
Hide file tree
Showing 27 changed files with 614 additions and 11 deletions.
11 changes: 11 additions & 0 deletions .autover/changes/3bbbb972-6181-431c-8313-097ce38a2463.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "AWS.Deploy.CLI",
"Type": "Minor",
"ChangelogMessages": [
"Add support for deploying ARM web apps to ECS Fargate"
]
}
]
}
7 changes: 7 additions & 0 deletions AWS.Deploy.sln
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Deploy.DocGenerator.Uni
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Deploy.DockerImageUploader", "test\AWS.Deploy.DockerImageUploader\AWS.Deploy.DockerImageUploader.csproj", "{49A1C020-F4C8-4B28-A6B2-6AD3C8452E69}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAppArmWithDocker", "testapps\WebAppArmWithDocker\WebAppArmWithDocker.csproj", "{A66C2027-64FB-49FE-B93C-4A24EB6324FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -179,6 +181,10 @@ Global
{49A1C020-F4C8-4B28-A6B2-6AD3C8452E69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49A1C020-F4C8-4B28-A6B2-6AD3C8452E69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49A1C020-F4C8-4B28-A6B2-6AD3C8452E69}.Release|Any CPU.Build.0 = Release|Any CPU
{A66C2027-64FB-49FE-B93C-4A24EB6324FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A66C2027-64FB-49FE-B93C-4A24EB6324FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A66C2027-64FB-49FE-B93C-4A24EB6324FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A66C2027-64FB-49FE-B93C-4A24EB6324FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -211,6 +217,7 @@ Global
{6D4BD0C2-C2A0-4AFB-BC22-623DD64A4F84} = {11C7056E-93C1-408B-BD87-5270595BBE0E}
{7E661545-7DFD-4FE3-A5F9-767FAE30DFFE} = {BD466B5C-D8B0-4069-98A9-6DC8F01FA757}
{49A1C020-F4C8-4B28-A6B2-6AD3C8452E69} = {BD466B5C-D8B0-4069-98A9-6DC8F01FA757}
{A66C2027-64FB-49FE-B93C-4A24EB6324FE} = {C3A0C716-BDEA-4393-B223-AF8F8531522A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5A4B2863-1763-4496-B122-651A38A4F5D7}
Expand Down
3 changes: 2 additions & 1 deletion src/AWS.Deploy.Orchestration/CdkAppSettingsSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)}");
Expand Down
10 changes: 10 additions & 0 deletions src/AWS.Deploy.Recipes.CDK.Common/RecipeProps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public interface IRecipeProps<T>
/// </summary>
string? DotnetPublishOutputDirectory { get; set; }

/// <summary>
/// The CPU architecture of the environment to create.
/// </summary>
string? EnvironmentArchitecture { get; set; }

/// <summary>
/// The ID of the recipe being used to deploy the application.
/// </summary>
Expand Down Expand Up @@ -108,6 +113,11 @@ public class RecipeProps<T> : IRecipeProps<T>
/// </summary>
public string? DotnetPublishOutputDirectory { get; set; }

/// <summary>
/// The CPU architecture of the environment to create.
/// </summary>
public string? EnvironmentArchitecture { get; set; }

/// <summary>
/// The ID of the recipe being used to deploy the application.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ private void ConfigureECSClusterAndService(IRecipeProps<Configuration> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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": [
{
Expand Down
49 changes: 49 additions & 0 deletions test/AWS.Deploy.CLI.IntegrationTests/WebAppWithDockerFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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", "WebAppArmWithDocker", "WebAppArmWithDocker.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);
Expand Down
68 changes: 63 additions & 5 deletions test/AWS.Deploy.CLI.UnitTests/DeploymentBundleHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,64 @@ public DeploymentBundleHandlerTests()
It.IsAny<string>()).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<string, object>());

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<string, object>());

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()
{
Expand Down Expand Up @@ -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);
}
}

Expand Down
20 changes: 20 additions & 0 deletions testapps/WebAppArmWithDocker/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="WebAppArmWithDocker.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>

<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>

</html>
23 changes: 23 additions & 0 deletions testapps/WebAppArmWithDocker/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@inherits LayoutComponentBase

<div class="page">
<div class="sidebar">
<NavMenu />
</div>

<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>

<article class="content px-4">
@Body
</article>
</main>
</div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
Loading

0 comments on commit 369fb7a

Please sign in to comment.