diff --git a/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile index 185fc5ff7..f958ac387 100644 --- a/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net8/amd64/Dockerfile @@ -40,8 +40,8 @@ RUN dotnet build "Amazon.Lambda.RuntimeSupport.csproj" /p:ExecutableOutputType=t FROM builder AS publish RUN dotnet publish "Amazon.Lambda.RuntimeSupport.csproj" /p:ExecutableOutputType=true /p:GenerateDocumentationFile=false /p:TargetFrameworks=net8.0 -f net8.0 --runtime linux-x64 --self-contained false -p:PublishReadyToRun=true -c Release -o /app/publish RUN apt-get update && apt-get install -y dos2unix -RUN dos2unix /app/publish/bootstrap.sh && \ - mv /app/publish/bootstrap.sh /app/publish/bootstrap && \ +RUN dos2unix /app/publish/bootstrap-al2023.sh && \ + mv /app/publish/bootstrap-al2023.sh /app/publish/bootstrap && \ chmod +x /app/publish/bootstrap diff --git a/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile b/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile index dad3c9062..8956752ad 100644 --- a/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile +++ b/LambdaRuntimeDockerfiles/Images/net8/arm64/Dockerfile @@ -40,8 +40,8 @@ RUN dotnet build "Amazon.Lambda.RuntimeSupport.csproj" /p:ExecutableOutputType=t FROM builder AS publish RUN dotnet publish "Amazon.Lambda.RuntimeSupport.csproj" /p:ExecutableOutputType=true /p:GenerateDocumentationFile=false /p:TargetFrameworks=net8.0 -f net8.0 --runtime linux-arm64 --self-contained false -p:PublishReadyToRun=true -c Release -o /app/publish RUN apt-get update && apt-get install -y dos2unix -RUN dos2unix /app/publish/bootstrap.sh && \ - mv /app/publish/bootstrap.sh /app/publish/bootstrap && \ +RUN dos2unix /app/publish/bootstrap-al2023.sh && \ + mv /app/publish/bootstrap-al2023.sh /app/publish/bootstrap && \ chmod +x /app/publish/bootstrap diff --git a/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction.SmokeTests/ImageFunctionTests.cs b/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction.SmokeTests/ImageFunctionTests.cs index 15687be32..92bb736bb 100644 --- a/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction.SmokeTests/ImageFunctionTests.cs +++ b/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction.SmokeTests/ImageFunctionTests.cs @@ -114,7 +114,45 @@ public async Task ExceptionTests(string handler, string input, string expectedEr Assert.Equal(expectedErrorMessage, exception["errorMessage"].ToString()); } - private async Task UpdateHandlerAsync(string handler) + /// + /// This test is checking the logic added to the bootstrap.sh to change the SSL_CERT_FILE + /// environment variable for AL2023. + /// + /// + /// + /// + /// + [Theory] +#if NET8_0_OR_GREATER + [InlineData("SSL_CERT_FILE", "\"/tmp/noop\"", null)] + [InlineData("SSL_CERT_FILE", "\"/tmp/my-bundle\"", "/tmp/my-bundle")] +#else + [InlineData("SSL_CERT_FILE", "", null)] + [InlineData("SSL_CERT_FILE", "/tmp/my-bundle", "/tmp/my-bundle")] +#endif + public async Task CheckEnvironmentVariable(string envName, string expectedValue, string setValue) + { + var envVariables = new Dictionary(); + if(setValue != null) + { + envVariables[envName] = setValue; + } + + await UpdateHandlerAsync("ImageFunction::ImageFunction.Function::GetEnvironmentVariable", envVariables); + + var payload = JsonConvert.SerializeObject(envName); + var invokeResponse = await InvokeFunctionAsync(payload); + + Assert.True(invokeResponse.HttpStatusCode == System.Net.HttpStatusCode.OK); + Assert.True(invokeResponse.FunctionError == null, "Failed invoke with error: " + invokeResponse.FunctionError); + + await using var responseStream = invokeResponse.Payload; + var actualValue = new StreamReader(responseStream).ReadToEnd(); + + Assert.Equal(expectedValue, actualValue); + } + + private async Task UpdateHandlerAsync(string handler, Dictionary environmentVariables = null) { var updateFunctionConfigurationRequest = new UpdateFunctionConfigurationRequest { @@ -122,6 +160,10 @@ private async Task UpdateHandlerAsync(string handler) ImageConfig = new ImageConfig() { Command = {handler}, + }, + Environment = new Amazon.Lambda.Model.Environment + { + Variables = environmentVariables ?? new Dictionary() } }; await _lambdaClient.UpdateFunctionConfigurationAsync(updateFunctionConfigurationRequest); diff --git a/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction/Function.cs b/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction/Function.cs index 128876e7d..c331b2e7d 100644 --- a/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction/Function.cs +++ b/LambdaRuntimeDockerfiles/SmokeTests/test/ImageFunction/Function.cs @@ -128,6 +128,11 @@ public string VerifyTzData(ILambdaContext lambdaContext) return SuccessResult; } + public string GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name) ?? string.Empty; + } + #endregion #region Private methods diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index e5969a967..0d0b09549 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -45,5 +45,8 @@ Always + + Always + diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/bootstrap-al2023.sh b/Libraries/src/Amazon.Lambda.RuntimeSupport/bootstrap-al2023.sh new file mode 100644 index 000000000..9848bb10a --- /dev/null +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/bootstrap-al2023.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# .NET on Linux uses OpenSSL to handle certificates. The .NET runtime will load the certs by first reading +# the default cert bundle file which can be overriden by the SSL_CERT_FILE env var. Then it will load the +# certs in the default cert directory which can be overriden by the SSL_CERT_DIR env var. On AL2023 +# The default cert bundle file, via symbolic links, resolves to being in a file under the default cert directory. +# This means the default cert bundle file is double loaded causing a cold start performance hit. This logic +# sets the SSL_CERT_FILE to a noop file if SSL_CERT_FILE hasn't been explicitly +# set. This avoid the double load of the default cert bundle file. +if [ -z "${SSL_CERT_FILE}"]; then + export SSL_CERT_FILE="/tmp/noop" +fi + +# This script is used to locate 2 files in the /var/task folder, where the end-user assembly is located +# The 2 files are .deps.json and .runtimeconfig.json +# These files are used to add the end-user assembly into context and make the code reachable to the dotnet process +# Since the file names are not known in advance, we use this shell script to find the files and pass them to the dotnet process as parameters +# You can improve cold-start performance by setting the LAMBDA_DOTNET_MAIN_ASSEMBLY environment variable and specifying the assembly name +# LAMBDA_TASK_ROOT is inherited from the Lambda execution environment/base image as "/var/task", but can be overridden for use in custom images. +if [ -z "${LAMBDA_TASK_ROOT}" ]; then + echo "Error: Environment variable LAMBDA_TASK_ROOT needs to be defined in order for the Lambda Runtime to load the function handler to be executed." 1>&2 + exit 101 +fi + +if [ ! -d "${LAMBDA_TASK_ROOT}" ] | [ -z "$(ls -A ${LAMBDA_TASK_ROOT})" ]; then + echo "Error: .NET binaries for Lambda function are not correctly installed in the ${LAMBDA_TASK_ROOT} directory of the image when the image was built. The ${LAMBDA_TASK_ROOT} directory is missing." 1>&2 + exit 102 +fi + +# Get version of Lambda .NET runtime if available +export "$(grep LAMBDA_RUNTIME_NAME /var/runtime/runtime-release 2>/dev/null || echo LAMBDA_RUNTIME_NAME=dotnet_custom)" +export AWS_EXECUTION_ENV="AWS_Lambda_${LAMBDA_RUNTIME_NAME}" + +export DOTNET_ROOT="/var/lang/bin" +DOTNET_BIN="${DOTNET_ROOT}/dotnet" +DOTNET_EXEC="exec" +DOTNET_ARGS=() +EXECUTABLE_BINARY_EXIST=false + +LAMBDA_HANDLER="" +# Command-line parameter has precedence over "_HANDLER" environment variable +if [ -n "${1}" ]; then + LAMBDA_HANDLER="${1}" +elif [ -n "${_HANDLER}" ]; then + LAMBDA_HANDLER="${_HANDLER}" +else + echo "Error: No Lambda Handler function was specified." 1>&2 + exit 103 +fi + +HANDLER_COL_INDEX=$(expr index "${LAMBDA_HANDLER}" ":") + +if [[ "${HANDLER_COL_INDEX}" == 0 ]]; then + EXECUTABLE_ASSEMBLY="${LAMBDA_TASK_ROOT}/${LAMBDA_HANDLER}" + EXECUTABLE_BINARY="${LAMBDA_TASK_ROOT}/${LAMBDA_HANDLER}" + if [[ "${EXECUTABLE_ASSEMBLY}" != *.dll ]]; then + EXECUTABLE_ASSEMBLY="${EXECUTABLE_ASSEMBLY}.dll" + fi + if [[ -f "${EXECUTABLE_ASSEMBLY}" ]]; then + DOTNET_ARGS+=("${EXECUTABLE_ASSEMBLY}") + elif [[ -f "${EXECUTABLE_BINARY}" ]]; then + EXECUTABLE_BINARY_EXIST=true + else + echo "Error: executable assembly ${EXECUTABLE_ASSEMBLY} or binary ${EXECUTABLE_BINARY} not found." 1>&2 + exit 104 + fi +else + if [ -n "${LAMBDA_DOTNET_MAIN_ASSEMBLY}" ]; then + if [[ "${LAMBDA_DOTNET_MAIN_ASSEMBLY}" == *.dll ]]; then + ASSEMBLY_NAME="${LAMBDA_DOTNET_MAIN_ASSEMBLY::-4}" + else + ASSEMBLY_NAME="${LAMBDA_DOTNET_MAIN_ASSEMBLY}" + fi + else + ASSEMBLY_NAME="${LAMBDA_HANDLER::${HANDLER_COL_INDEX}-1}" + fi + + DEPS_FILE="${LAMBDA_TASK_ROOT}/${ASSEMBLY_NAME}.deps.json" + if ! [ -f "${DEPS_FILE}" ]; then + DEPS_FILES=( "${LAMBDA_TASK_ROOT}"/*.deps.json ) + + # Check if there were any matches to the *.deps.json glob, and that the glob was resolved + # This makes the matching independent from the global `nullopt` shopt's value (https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) + if [ "${#DEPS_FILES[@]}" -ne 1 ] || echo "${DEPS_FILES[0]}" | grep -q -F '*'; then + echo "Error: .NET binaries for Lambda function are not correctly installed in the ${LAMBDA_TASK_ROOT} directory of the image when the image was built. The ${LAMBDA_TASK_ROOT} directory is missing the required .deps.json file." 1>&2 + exit 105 + fi + DEPS_FILE="${DEPS_FILES[0]}" + fi + + RUNTIMECONFIG_FILE="${LAMBDA_TASK_ROOT}/${ASSEMBLY_NAME}.runtimeconfig.json" + if ! [ -f "${RUNTIMECONFIG_FILE}" ]; then + RUNTIMECONFIG_FILES=( "${LAMBDA_TASK_ROOT}"/*.runtimeconfig.json ) + + # Check if there were any matches to the *.runtimeconfig.json glob, and that the glob was resolved + # This makes the matching independent from the global `nullopt` shopt's value (https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) + if [ "${#RUNTIMECONFIG_FILES[@]}" -ne 1 ] || echo "${RUNTIMECONFIG_FILES[0]}" | grep -q -F '*'; then + echo "Error: .NET binaries for Lambda function are not correctly installed in the ${LAMBDA_TASK_ROOT} directory of the image when the image was built. The ${LAMBDA_TASK_ROOT} directory is missing the required .runtimeconfig.json file." 1>&2 + exit 106 + fi + RUNTIMECONFIG_FILE="${RUNTIMECONFIG_FILES[0]}" + fi + + DOTNET_ARGS+=("--depsfile" "${DEPS_FILE}" + "--runtimeconfig" "${RUNTIMECONFIG_FILE}" + "/var/runtime/Amazon.Lambda.RuntimeSupport.dll" "${LAMBDA_HANDLER}") +fi + + +# To support runtime wrapper scripts +# https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper +if [ -z "${AWS_LAMBDA_EXEC_WRAPPER}" ]; then + if [ ${EXECUTABLE_BINARY_EXIST} = true ]; then + exec "${EXECUTABLE_BINARY}" + else + exec "${DOTNET_BIN}" "${DOTNET_EXEC}" "${DOTNET_ARGS[@]}" + fi +else + if [ ! -f "${AWS_LAMBDA_EXEC_WRAPPER}" ]; then + echo "${AWS_LAMBDA_EXEC_WRAPPER}: does not exist" + exit 127 + fi + if [ ! -x "${AWS_LAMBDA_EXEC_WRAPPER}" ]; then + echo "${AWS_LAMBDA_EXEC_WRAPPER}: is not an executable" + exit 126 + fi + if [ ${EXECUTABLE_BINARY_EXIST} = true ]; then + exec -- "${AWS_LAMBDA_EXEC_WRAPPER}" "${EXECUTABLE_BINARY}" + else + exec -- "${AWS_LAMBDA_EXEC_WRAPPER}" "${DOTNET_BIN}" "${DOTNET_EXEC}" "${DOTNET_ARGS[@]}" + fi +fi \ No newline at end of file