Skip to content

Commit

Permalink
feat: add support for .NET8 container-based web apps
Browse files Browse the repository at this point in the history
  • Loading branch information
philasmar committed Oct 16, 2023
1 parent 52f4a6c commit 504a504
Show file tree
Hide file tree
Showing 49 changed files with 687 additions and 47 deletions.
1 change: 1 addition & 0 deletions .github/workflows/codebuild-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
branches:
- main
- dev
- 'feature/**'

permissions:
id-token: write
Expand Down
1 change: 1 addition & 0 deletions AWS.Deploy.sln
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{3aac19a6-02e8-45d0-bdd0-cad0fbe15f64}*SharedItemsImports = 5
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{3f7a5ca6-7178-4dbf-8dad-6a63684c7a8e}*SharedItemsImports = 5
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{4c4f07ce-4c88-44c6-864f-c5e563712ee2}*SharedItemsImports = 5
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{5f8ec972-781d-4a82-a73f-36a97281b0d5}*SharedItemsImports = 5
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{8a351cc0-70c0-4412-b45e-358606251512}*SharedItemsImports = 5
src\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems*{f2266c44-c8c5-45ad-aa9b-44f8825bdf63}*SharedItemsImports = 13
Expand Down
2 changes: 1 addition & 1 deletion buildtools/ci.buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 0.2
phases:
install:
runtime-versions:
nodejs: 16
nodejs: 18
commands:
# install .NET SDK
- curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 6.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* ID: ServiceName
* Description: The name of the AWS App Runner service.
* Type: String
* **Port**
* **Container Port**
* ID: Port
* Description: The port the container is listening for requests on.
* Type: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
* ID: DesiredCount
* Description: The desired number of ECS tasks to run for the service.
* Type: Int
* **Container Port**
* ID: Port
* Description: The port the container is listening for requests on.
* Type: Int
* **Application IAM Role**
* ID: ApplicationIAMRole
* Description: The Identity and Access Management (IAM) role that provides AWS credentials to the application to access AWS services.
Expand Down
2 changes: 1 addition & 1 deletion site/content/docs/deployment-projects/recipe-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ Here is an example of a validator for a port setting that ensures the value is w
...
{
"Id": "Port",
"Name": "Port",
"Name": "Container Port",
"Category": "General",
"Description": "The port the container is listening for requests on.",
"Type": "Int",
Expand Down
9 changes: 9 additions & 0 deletions site/content/troubleshooting-guide/docker-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,12 @@ Failed to push Docker Image
**Why is this happening** You may see this if your project has project references (.csproj, .vbproj) that are located in a higher folder than the solution file (.sln) that AWS.Deploy.Tools is using to generate a Dockerfile. In this case AWS.Deploy.Tools will not generate a Dockerfile to avoid a large build context that can result in long builds.

**Resolution**: If you would still like to deploy to an [AWS service that requires Docker](../docs/support.md), you must create your own Dockerfile and set an appropriate "Docker Execution Directory" in the deployment options. Alternatively you may choose another deployment target that does not require Docker, such as AWS Elastic Beanstalk.


## Application deployment stuck or fails because of health check

Microsoft has made changes to the base images used in .NET 8 which now expose 8080 as the default HTTP port instead of the port 80 which was used in previous versions. In addition to that, Microsoft now uses a non-root user by default.

As we added support for deploying .NET 8 container-based applications, the container port setting in the recipes that support it now defaults to 8080 for .NET 8 and 80 in previous versions. For applications that do not have a `dockerfile`, we generate one accordingly. However, for applications that have their own `dockerfile`, the user is responsible for setting and exposing the proper port. If the container port is different from the port exposed in the container, the deployment might keep going until it reaches a timeout from the underlying services, or you might receive an error related to the health check.

In the tool, we have added a warning message if we detect that the container port setting is different from the one exposed in the container.
54 changes: 54 additions & 0 deletions src/AWS.Deploy.CLI/Commands/TypeHints/DockerHttpPortCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using AWS.Deploy.Common;
using AWS.Deploy.Common.Recipes;
using AWS.Deploy.Common.Recipes.Validation;
using AWS.Deploy.Common.TypeHintData;
using System.Threading.Tasks;

namespace AWS.Deploy.CLI.Commands.TypeHints
{
public class DockerHttpPortCommand : ITypeHintCommand
{
private readonly IConsoleUtilities _consoleUtilities;
private readonly IOptionSettingHandler _optionSettingHandler;

public DockerHttpPortCommand(IConsoleUtilities consoleUtilities, IOptionSettingHandler optionSettingHandler)
{
_consoleUtilities = consoleUtilities;
_optionSettingHandler = optionSettingHandler;
}

public Task<TypeHintResourceTable> GetResources(Recommendation recommendation, OptionSettingItem optionSetting) => Task.FromResult(new TypeHintResourceTable());

public Task<object> Execute(Recommendation recommendation, OptionSettingItem optionSetting)
{
var settingValue = _consoleUtilities
.AskUserForValue(
string.Empty,
_optionSettingHandler.GetOptionSettingValue<string>(recommendation, optionSetting) ?? string.Empty,
allowEmpty: false,
resetValue: _optionSettingHandler.GetOptionSettingDefaultValue<string>(recommendation, optionSetting) ?? string.Empty,
validators: async httpPort => await ValidateHttpPort(httpPort, recommendation, optionSetting));

var settingValueInt = int.Parse(settingValue);
recommendation.DeploymentBundle.DockerfileHttpPort = settingValueInt;
return Task.FromResult<object>(settingValueInt);
}

private async Task<string> ValidateHttpPort(string httpPort, Recommendation recommendation, OptionSettingItem optionSettingItem)
{
var validationResult = await new RangeValidator() { Min = 0, Max = 65535 }.Validate(httpPort, recommendation, optionSettingItem);

if (validationResult.IsValid)
{
return string.Empty;
}
else
{
return validationResult.ValidationFailedMessage ?? "Invalid value for Docker HTTP Port.";
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public TypeHintCommandFactory(IServiceProvider serviceProvider, IToolInteractive
{ OptionSettingTypeHint.VPCConnector, ActivatorUtilities.CreateInstance<VPCConnectorCommand>(serviceProvider) },
{ OptionSettingTypeHint.FilePath, ActivatorUtilities.CreateInstance<FilePathCommand>(serviceProvider) },
{ OptionSettingTypeHint.ElasticBeanstalkVpc, ActivatorUtilities.CreateInstance<ElasticBeanstalkVpcCommand>(serviceProvider) },
{ OptionSettingTypeHint.DockerHttpPort, ActivatorUtilities.CreateInstance<DockerHttpPortCommand>(serviceProvider) },
};
}

Expand Down
7 changes: 5 additions & 2 deletions src/AWS.Deploy.Common/DeploymentBundles/DeploymentBundle.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.IO;

namespace AWS.Deploy.Common
{
/// <summary>
Expand All @@ -25,6 +23,11 @@ public class DeploymentBundle
/// </summary>
public string DockerfilePath { get; set; } = "";

/// <summary>
/// The HTTP port to expose in the container.
/// </summary>
public int DockerfileHttpPort { get; set; } = 8080;

/// <summary>
/// The ECR Repository Name where the docker image will be pushed to.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/AWS.Deploy.Common/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public enum DeployToolErrorCode
FailedToSaveDeploymentSettings = 10010600,
InvalidWindowsManifestFile = 10010700,
UserDeploymentFileNotFound = 10010800,
DockerInspectFailed = 10004200,
}

public class ProjectFileNotFoundException : DeployToolException
Expand Down
3 changes: 2 additions & 1 deletion src/AWS.Deploy.Common/Recipes/OptionSettingTypeHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public enum OptionSettingTypeHint
ExistingSecurityGroups,
VPCConnector,
FilePath,
ElasticBeanstalkVpc
ElasticBeanstalkVpc,
DockerHttpPort
};
}
10 changes: 10 additions & 0 deletions src/AWS.Deploy.Constants/Docker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal class Docker
/// </summary>
public const string DockerBuildArgsOptionId = "DockerBuildArgs";

/// <summary>
/// Id for the Docker HTTP Port recipe option
/// </summary>
public const string DockerHttpPortOptionId = "Port";

/// <summary>
/// Id for the ECR Repository Name recipe option
/// </summary>
Expand All @@ -34,5 +39,10 @@ internal class Docker
/// Id for the Docker Image Tag recipe option
/// </summary>
public const string ImageTagOptionId = "ImageTag";

/// <summary>
/// The environment variable that .NET uses to determine the HTTP port
/// </summary>
public static readonly string DotnetHttpPortEnvironmentVariable = "ASPNETCORE_HTTP_PORTS";
}
}
1 change: 1 addition & 0 deletions src/AWS.Deploy.Constants/RecipeIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal static class RecipeIdentifier
public const string REPLACE_TOKEN_DEFAULT_VPC_ID = "{DefaultVpcId}";
public const string REPLACE_TOKEN_HAS_DEFAULT_VPC = "{HasDefaultVpc}";
public const string REPLACE_TOKEN_HAS_NOT_VPCS = "{HasNotVpcs}";
public const string REPLACE_TOKEN_DEFAULT_CONTAINER_PORT = "{DefaultContainerPort}";

/// <summary>
/// Id for the 'dotnet publish --configuration' recipe option
Expand Down
2 changes: 2 additions & 0 deletions src/AWS.Deploy.DockerEngine/AWS.Deploy.DockerEngine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@
<PackageReference Include="System.Text.Json" Version="6.0.8" />
</ItemGroup>

<Import Project="..\AWS.Deploy.Constants\AWS.Deploy.Constants.projitems" Label="Shared" />

</Project>
85 changes: 80 additions & 5 deletions src/AWS.Deploy.DockerEngine/DockerEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Linq;
using AWS.Deploy.Common;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Common.Recipes;
using AWS.Deploy.Common.Utilities;
using Newtonsoft.Json;

Expand All @@ -18,14 +17,23 @@ public interface IDockerEngine
/// <summary>
/// Generates a docker file
/// </summary>
void GenerateDockerFile();
/// <param name="recommendation">The currently selected recommendation</param>
void GenerateDockerFile(Recommendation recommendation);

/// <summary>
/// Inspects the Dockerfile associated with the recommendation
/// and determines the appropriate Docker Execution Directory,
/// if one is not set.
/// </summary>
/// <param name="recommendation">The currently selected recommendation</param>
void DetermineDockerExecutionDirectory(Recommendation recommendation);

/// <summary>
/// Determines the appropriate HTTP port that the underlying container is using.
/// </summary>
/// <param name="recommendation">The currently selected recommendation</param>
/// <returns>The default HTTP port used by the container</returns>
int DetermineDefaultDockerPort(Recommendation recommendation);
}

/// <summary>
Expand Down Expand Up @@ -54,7 +62,8 @@ public DockerEngine(ProjectDefinition project, IFileManager fileManager, IDirect
/// <summary>
/// Generates a docker file
/// </summary>
public void GenerateDockerFile()
/// <param name="recommendation">The currently selected recommendation</param>
public void GenerateDockerFile(Recommendation recommendation)
{
var projectFileName = Path.GetFileName(_projectPath);
var imageMapping = GetImageMapping();
Expand All @@ -63,7 +72,13 @@ public void GenerateDockerFile()
throw new UnknownDockerImageException(DeployToolErrorCode.NoValidDockerImageForProject, $"Unable to determine a valid docker base and build image for project of type {_project.SdkType} and Target Framework {_project.TargetFramework}");
}

var dockerFile = new DockerFile(imageMapping, projectFileName, _project.AssemblyName);
var dockerFile = new DockerFile(
imageMapping,
projectFileName,
_project.AssemblyName,
recommendation.DeploymentBundle.DockerfileHttpPort,
UseRootUser(recommendation),
DetermineHTTPPortEnvironmentVariable(recommendation, recommendation.DeploymentBundle.DockerfileHttpPort));
var projectDirectory = Path.GetDirectoryName(_projectPath) ?? "";
var projectList = GetProjectList();
dockerFile.WriteDockerFile(projectDirectory, projectList);
Expand Down Expand Up @@ -171,7 +186,7 @@ private ImageMapping GetImageMapping()
/// and determines the appropriate Docker Execution Directory,
/// if one is not set.
/// </summary>
/// <param name="recommendation"></param>
/// <param name="recommendation">The currently selected recommendation</param>
public void DetermineDockerExecutionDirectory(Recommendation recommendation)
{
if (string.IsNullOrEmpty(recommendation.DeploymentBundle.DockerExecutionDirectory))
Expand All @@ -197,5 +212,65 @@ public void DetermineDockerExecutionDirectory(Recommendation recommendation)
}
}
}

/// <summary>
/// Determines the appropriate HTTP port that the underlying container is using.
/// </summary>
/// <param name="recommendation">The currently selected recommendation</param>
/// <returns>The default HTTP port used by the container</returns>
public int DetermineDefaultDockerPort(Recommendation recommendation)
{
switch (recommendation.ProjectDefinition.TargetFramework)
{
case "net7.0":
case "net6.0":
case "net5.0":
case "netcoreapp3.1":
return 80;

default:
return 8080;
}
}

/// <summary>
/// Determines the appropriate HTTP port environment variable to set.
/// </summary>
/// <param name="recommendation">The currently selected recommendation</param>
/// <returns>The appropriate HTTP port environment variable</returns>
private string DetermineHTTPPortEnvironmentVariable(Recommendation recommendation, int port)
{
switch (recommendation.ProjectDefinition.TargetFramework)
{
case "net7.0":
case "net6.0":
case "net5.0":
case "netcoreapp3.1":
return $"ASPNETCORE_URLS=http://+:{port}";

default:
return $"ASPNETCORE_HTTP_PORTS={port}";
}
}

/// <summary>
/// Checks whether to use a root or non-root user in the underlying container.
/// </summary>
/// <param name="recommendation">The currently selected recommendation</param>
/// <returns>true if a root user is used, else false</returns>
private bool UseRootUser(Recommendation recommendation)
{
switch (recommendation.ProjectDefinition.TargetFramework)
{
case "net7.0":
case "net6.0":
case "net5.0":
case "netcoreapp3.1":
return true;

default:
return false;
}
}
}
}
Loading

0 comments on commit 504a504

Please sign in to comment.