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 12, 2023
1 parent da421b7 commit 61a32e6
Show file tree
Hide file tree
Showing 47 changed files with 609 additions and 46 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
12 changes: 12 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,15 @@ 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 .NET8 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 .NET8 container-based applications, the container port setting in the recipes that support it now defaults to 8080 for .NET8 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. The warning message is as follows:
```
The HTTP port you have chosen in your deployment settings is different than the .NET HTTP port 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 = 51200 }.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>
60 changes: 55 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,8 @@ 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));
var projectDirectory = Path.GetDirectoryName(_projectPath) ?? "";
var projectList = GetProjectList();
dockerFile.WriteDockerFile(projectDirectory, projectList);
Expand Down Expand Up @@ -171,7 +181,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 +207,45 @@ 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>
/// 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;
}
}
}
}
40 changes: 36 additions & 4 deletions src/AWS.Deploy.DockerEngine/DockerFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ namespace AWS.Deploy.DockerEngine
/// </summary>
public class DockerFile
{
private const string DockerFileName = "Dockerfile";

private readonly ImageMapping _imageMapping;
private readonly string _projectName;
private readonly string _assemblyName;
private readonly int _port;
private readonly bool _useRootUser;

public DockerFile(ImageMapping imageMapping, string projectName, string? assemblyName)
public DockerFile(ImageMapping imageMapping, string projectName, string? assemblyName, int port, bool useRootUser)
{
if (imageMapping == null)
{
Expand All @@ -39,6 +39,8 @@ public DockerFile(ImageMapping imageMapping, string projectName, string? assembl
_imageMapping = imageMapping;
_projectName = projectName;
_assemblyName = assemblyName;
_port = port;
_useRootUser = useRootUser;
}

/// <summary>
Expand Down Expand Up @@ -79,10 +81,40 @@ public void WriteDockerFile(string projectDirectory, List<string>? projectList)
.Replace("{project-name}", _projectName)
.Replace("{assembly-name}", _assemblyName);

// Microsoft exposes 8081 along with 8080 in their .NET8 templates. I am preserving that behavior here when port 8080 is used.
if (_port == 8080)
{
dockerFile = dockerFile
.Replace("{exposed-ports}", $"EXPOSE {_port}\r\nEXPOSE 8081");
}
// Microsoft exposes 443 along with 80 in their .NET7 and older templates. I am preserving that behavior here when port 80 is used.
else if (_port == 80)
{
dockerFile = dockerFile
.Replace("{exposed-ports}", $"EXPOSE {_port}\r\nEXPOSE 443");
}
// For all other ports, it is up to the user to expose the HTTPS port in the dockerfile.
else
{
dockerFile = dockerFile
.Replace("{exposed-ports}", $"EXPOSE {_port}");
}

if (_useRootUser)
{
dockerFile = dockerFile
.Replace("{non-root-user}", string.Empty);
}
else
{
dockerFile = dockerFile
.Replace("{non-root-user}", "\r\nUSER app");
}

// ProjectDefinitionParser will have transformed projectDirectory to an absolute path,
// and DockerFileName is static so traversal should not be possible here.
// nosemgrep: csharp.lang.security.filesystem.unsafe-path-combine.unsafe-path-combine
File.WriteAllText(Path.Combine(projectDirectory, DockerFileName), dockerFile);
File.WriteAllText(Path.Combine(projectDirectory, Constants.Docker.DefaultDockerfileName), dockerFile);

Check warning

Code scanning / Semgrep

String argument projectDirectory is used to read or write data from a file via Path.Combine without direct sanitization via Path.GetFileName. If the path is user-supplied data this can lead to path traversal. Warning

String argument projectDirectory is used to read or write data from a file via Path.Combine without direct sanitization via Path.GetFileName. If the path is user-supplied data this can lead to path traversal.
}
}
}
5 changes: 2 additions & 3 deletions src/AWS.Deploy.DockerEngine/Templates/Dockerfile.template
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
FROM {docker-base-image} AS base
FROM {docker-base-image} AS base{non-root-user}
WORKDIR /app
EXPOSE 80
EXPOSE 443
{exposed-ports}

FROM {docker-build-image} AS build
WORKDIR /src
Expand Down
Loading

0 comments on commit 61a32e6

Please sign in to comment.