Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for deploying ARM web apps to Elastic Beanstalk on Linux #876

Open
wants to merge 1 commit into
base: asmarp/ecs-console-service-arm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/d90f60cc-2552-4673-bac9-fe7fdc80ffe0.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 Elastic Beanstalk on Linux"
]
}
]
}
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}") = "WebAppArmDeployment", "testapps\WebAppArmDeployment\WebAppArmDeployment.csproj", "{303A0323-3FEF-4640-80C2-E33A8BC0F1FC}"
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
{303A0323-3FEF-4640-80C2-E33A8BC0F1FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{303A0323-3FEF-4640-80C2-E33A8BC0F1FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{303A0323-3FEF-4640-80C2-E33A8BC0F1FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{303A0323-3FEF-4640-80C2-E33A8BC0F1FC}.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}
{303A0323-3FEF-4640-80C2-E33A8BC0F1FC} = {C3A0C716-BDEA-4393-B223-AF8F8531522A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5A4B2863-1763-4496-B122-651A38A4F5D7}
Expand Down
4 changes: 3 additions & 1 deletion src/AWS.Deploy.Orchestration/DeploymentBundleHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,13 @@ public async Task<string> CreateDotnetPublishZip(Recommendation recommendation)

var publishDirectoryInfo = _directoryManager.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
var additionalArguments = recommendation.DeploymentBundle.DotnetPublishAdditionalBuildArguments;
var windowsPlatform = recommendation.DeploymentBundle.EnvironmentArchitecture == SupportedArchitecture.Arm64 ? "win-arm64" : "win-x64";
var linuxPlatform = recommendation.DeploymentBundle.EnvironmentArchitecture == SupportedArchitecture.Arm64 ? "linux-arm64" : "linux-x64";
var runtimeArg =
recommendation.DeploymentBundle.DotnetPublishSelfContainedBuild &&
!additionalArguments.Contains("--runtime ") &&
!additionalArguments.Contains("-r ")
? $"--runtime {(recommendation.Recipe.TargetPlatform == TargetPlatform.Windows ? "win-x64" : "linux-x64")}"
? $"--runtime {(recommendation.Recipe.TargetPlatform == TargetPlatform.Windows ? windowsPlatform : linuxPlatform)}"
: "";
var publishCommand =
$"dotnet publish \"{recommendation.ProjectPath}\"" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class Recipe : Construct
public const string LOADBALANCERSCHEME_PUBLIC = "public";

public const string REVERSEPROXY_NGINX = "nginx";

public const string ENHANCED_HEALTH_REPORTING = "enhanced";

public Vpc? AppVpc { get; private set; }
Expand Down Expand Up @@ -74,7 +74,7 @@ public Recipe(Construct scope, IRecipeProps<Configuration> props)
ConfigureVpc(settings);
ConfigureIAM(settings);
var beanstalkApplicationName = ConfigureApplication(settings);
ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName);
ConfigureBeanstalkEnvironment(settings, beanstalkApplicationName, props.EnvironmentArchitecture);
}

private void ConfigureVpc(Configuration settings)
Expand Down Expand Up @@ -200,7 +200,7 @@ private string ConfigureApplication(Configuration settings)
return beanstalkApplicationName;
}

private void ConfigureBeanstalkEnvironment(Configuration settings, string beanstalkApplicationName)
private void ConfigureBeanstalkEnvironment(Configuration settings, string beanstalkApplicationName, string? environmentArchitecture)
{
if (Ec2InstanceProfile == null)
throw new InvalidOperationException($"{nameof(Ec2InstanceProfile)} has not been set. The {nameof(ConfigureIAM)} method should be called before {nameof(ConfigureBeanstalkEnvironment)}");
Expand Down Expand Up @@ -242,12 +242,22 @@ private void ConfigureBeanstalkEnvironment(Configuration settings, string beanst
{
optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty
{
Namespace = "aws:autoscaling:launchconfiguration",
OptionName = "InstanceType",
Namespace = "aws:ec2:instances",
OptionName = "InstanceTypes",
Value = settings.InstanceType
});
}

if (!string.IsNullOrEmpty(environmentArchitecture))
{
optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty
{
Namespace = "aws:ec2:instances",
OptionName = "SupportedArchitectures",
Value = environmentArchitecture.ToLower()
});
}

if (settings.EnvironmentType.Equals(ENVIRONMENTTYPE_LOADBALANCED))
{
optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty
Expand Down Expand Up @@ -336,7 +346,7 @@ private void ConfigureBeanstalkEnvironment(Configuration settings, string beanst
}
);
}

if (settings.ElasticBeanstalkRollingUpdates.RollingUpdatesEnabled)
{
optionSettingProperties.Add(new CfnEnvironment.OptionSettingProperty
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "./aws-deploy-recipe-schema.json",
"Id": "AspNetAppElasticBeanstalkLinux",
"Version": "1.0.3",
"Version": "1.1.0",
"Name": "ASP.NET Core App to AWS Elastic Beanstalk on Linux",
"DeploymentType": "CdkProject",
"DeploymentBundle": "DotnetPublishZipFile",
Expand All @@ -11,7 +11,7 @@
"Description": "This ASP.NET Core application will be built and deployed to AWS Elastic Beanstalk on Linux. Recommended if you want to deploy your application directly to EC2 hosts, not as a container image.",
"TargetService": "AWS Elastic Beanstalk",
"TargetPlatform": "Linux",
"SupportedArchitectures": [ "x86_64" ],
"SupportedArchitectures": [ "x86_64", "arm64" ],

"DisplayedResources": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,28 @@ namespace AWS.Deploy.CLI.Common.UnitTests.Extensions
public static class DirectoryCopyExtension
{
/// <summary>
/// <see cref="https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories"/>
/// Copy the contents of a directory to another location including all subdirectories.
/// </summary>
public static void CopyTo(this DirectoryInfo dir, string destDirName, bool copySubDirs)
public static void CopyTo(this DirectoryInfo dir, string destDirName)
{
if (!dir.Exists)
{
throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {dir.FullName}");
}

var dirs = dir.GetDirectories();

Directory.CreateDirectory(destDirName);

var files = dir.GetFiles();
var files = dir.GetFiles("*", SearchOption.AllDirectories);
foreach (var file in files)
{
var tempPath = Path.Combine(destDirName, file.Name);
file.CopyTo(tempPath, false);
}

if (copySubDirs)
{
foreach (var subdir in dirs)
var relativePath = Path.GetRelativePath(dir.FullName, file.FullName);
var tempPath = Path.Combine(destDirName, relativePath);
var tempDir = Path.GetDirectoryName(tempPath);
if (!string.IsNullOrEmpty(tempDir) && !Directory.Exists(tempDir))
{
var tempPath = Path.Combine(destDirName, subdir.Name);
var subDir = new DirectoryInfo(subdir.FullName);
subDir.CopyTo(tempPath, copySubDirs);
Directory.CreateDirectory(tempDir);
}
file.CopyTo(tempPath, false);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/AWS.Deploy.CLI.Common.UnitTests/IO/TestAppManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public string GetProjectPath(string path)
var sourceTestAppsDir = new DirectoryInfo("testapps");
var tempTestAppsPath = Path.Combine(tempDir, "testapps");
Directory.CreateDirectory(tempTestAppsPath);
sourceTestAppsDir.CopyTo(tempTestAppsPath, true);
sourceTestAppsDir.CopyTo(tempTestAppsPath);
return Path.Combine(tempDir, path);
}
}
Expand Down
53 changes: 53 additions & 0 deletions test/AWS.Deploy.CLI.IntegrationTests/WebAppNoDockerFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,59 @@ public async Task EBDefaultConfigurations(string configFile, bool linux)
Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists.");
}

[Fact]
public async Task BeanstalkArmDeployment()
{
_stackName = $"BeanstalkArm{Guid.NewGuid().ToString().Split('-').Last()}";

// Arrange input for deploy
await _interactiveService.StdInWriter.WriteAsync(Environment.NewLine); // Select default recommendation
await _interactiveService.StdInWriter.WriteLineAsync("7"); // 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", "WebAppArmDeployment", "WebAppArmDeployment.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 tempCdkProjectLine = deployStdOut.First(line => line.StartsWith("Saving AWS CDK deployment project to: "));
var tempCdkProject = tempCdkProjectLine.Split(": ")[1].Trim();
Assert.False(Directory.Exists(tempCdkProject), $"{tempCdkProject} must not exist.");

// Example: Endpoint: http://52.36.216.238/
var endpointLine = deployStdOut.First(line => line.Trim().StartsWith($"Endpoint"));
var applicationUrl = endpointLine.Substring(endpointLine.IndexOf(":", StringComparison.Ordinal) + 1).Trim();
Assert.True(Uri.IsWellFormedUriString(applicationUrl, UriKind.Absolute));

// 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 listStdOut = _interactiveService.StdOutReader.ReadAllLines().Select(x => x.Split()[0]).ToList();
Assert.Contains(listStdOut, (deployment) => _stackName.Equals(deployment));

// Arrange input for delete
// Use --silent flag to delete without user prompts
var deleteArgs = new[] { "delete-deployment", _stackName, "--diagnostics", "--silent" };

// Delete
Assert.Equal(CommandReturnCodes.SUCCESS, await _app.Run(deleteArgs)); ;

// Verify application is deleted
Assert.True(await _cloudFormationHelper.IsStackDeleted(_stackName), $"{_stackName} still exists.");
}

public void Dispose()
{
Dispose(true);
Expand Down
20 changes: 20 additions & 0 deletions testapps/WebAppArmDeployment/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="WebAppArmDeployment.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/WebAppArmDeployment/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;
}
Loading
Loading