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 57fbd8c
Show file tree
Hide file tree
Showing 26 changed files with 607 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"
]
}
]
}
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}

main {
flex: 1;
}

.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}

.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}

.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}

.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}

.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}

@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}

.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}

@media (min-width: 641px) {
.page {
flex-direction: row;
}

.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}

.top-row {
position: sticky;
top: 0;
z-index: 1;
}

.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}

.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}

#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
18 changes: 18 additions & 0 deletions testapps/WebAppArmWithDocker/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">WebAppArmWithDocker</a>
</div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />

<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
</nav>
</div>

Loading

0 comments on commit 57fbd8c

Please sign in to comment.