diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee84da --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sw* diff --git a/Dockerfile b/Dockerfile index 869eedd..feb009d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6 +FROM python:3.9 RUN apt-get update RUN apt-get install -y jq zip diff --git a/action.yml b/action.yml index f06de98..4e5f54a 100644 --- a/action.yml +++ b/action.yml @@ -1,24 +1,65 @@ name: Py Lambda Deploy author: Mariam Maarouf -description: Deploy python code to AWS Lambda with dependencies in a separate layer. +description: | + Deploy python code to AWS Lambda with dependencies in a separate layer. inputs: - requirements_txt: - description: the name/path to the requirements.txt file + target: + description: lambda or layer required: true - default: 'requirements.txt' - lambda_layer_arn: - description: The ARN for the Lambda layer the dependencies should be pushed to without the version (every push is a new version). + name: + description: target name required: true + architectures: + description: Target architectures + required: false + default: 'x86_64' + runtimes: + description: Compatible runtimes in space-separated string + required: false + default: 'python3.9 python3.8 python3.7 python3.6' + path: + description: Path to the code. + required: false + pip: + description: The name/path to the requirements.pip file + required: false + patterns: + description: Regex patterns to gather files for the archive (not used yet) + required: false + excludes: + description: zip -x patterns to exclude files from the archive + required: false + layers: + description: Lambda Layers to add to Lambda Function + required: false + lambda_layer_arn: + description: | + The ARN for the Lambda layer the dependencies should be pushed to + without the version (every push is a new version). + required: false lambda_function_name: - description: The Lambda function name. Check the AWS docs/readme for examples. + description: | + The Lambda function name. Check the AWS docs/readme for examples. + required: false + s3_bucket: + description: S3 bucket where upload lambda zip code. required: true runs: using: 'docker' image: 'Dockerfile' args: - - ${{ inputs.requirements_txt }} + - ${{ inputs.target }} + - ${{ inputs.name }} + - ${{ inputs.architectures }} + - ${{ inputs.runtimes }} + - ${{ inputs.path }} + - ${{ inputs.pip }} + - ${{ inputs.patterns }} + - ${{ inputs.excludes }} + - ${{ inputs.layers }} - ${{ inputs.lambda_layer_arn }} - ${{ inputs.lambda_function_name }} + - ${{ inputs.s3_bucket }} branding: icon: 'layers' color: 'yellow' diff --git a/entrypoint.sh b/entrypoint.sh index 0ffdfed..6bab40e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,38 +1,179 @@ #!/bin/bash set -e -install_zip_dependencies(){ - echo "Installing and zipping dependencies..." - mkdir python - pip install --target=python -r "${INPUT_REQUIREMENTS_TXT}" - zip -r dependencies.zip ./python + + + +name="$(basename $0)" +DEBUG=1 + + +die() +{ + echo ERROR: $name: $@ >&2 + exit -1 +} + +log() +{ + echo INFO: $name: $@ +} + +debug() +{ + grep -q yes\\\|1\\\|on\\\|true <<< $DEBUG || return 0 + echo DEBUG: $name: $@ +} + + +get_last_layer_version_arn() +{ + layer_name="$1" + # TODO: could try all combinations of arch and runtimes + result=$(aws lambda list-layer-versions --layer-name "$layer_name" \ + --compatible-architecture "${INPUT_ARCHITECTURES%% *}" \ + --compatible-runtime "${INPUT_RUNTIMES%% *}" \ + --max-items 1) + arn="$(jq -e .LayerVersions[0].LayerVersionArn <<< "$result"|cut -d\" -f2)" + [ $? != 0 ] || [ "$arn" == "null" ] && return 1 + echo -n $arn +} + + +make_archive() +{ + log "Building $INPUT_NAME $INPUT_TARGET archive..." + archive="$(realpath .)/archive.zip" + set -f + trap "rm -f -- '$archive'" EXIT + if [ -z "$INPUT_EXCLUDES" ]; then + zip_opts= + else + zip_opts="-x $INPUT_EXCLUDES" + fi + debug "INPUT_EXCLUDES: $INPUT_EXCLUDES" + debug "zip_opts: $zip_opts" + set +f + tempdir=$(mktemp -d pip.XXXXXXXXXX) + trap "rm -rf -- '$archive' '$tempdir'" EXIT + mkdir "$tempdir/python" + if [ -n "$INPUT_PATH" ]; then + log "Installing codes... : $INPUT_PATH" + for path in $INPUT_PATH; do + ln -vs "$(realpath $path)/"* "$tempdir/python/" + done + fi + if [ -n "$INPUT_PIP" ]; then + log "Installing dependencies... : $INPUT_PIP" + for path in $INPUT_PIP; do + pip install -t "$tempdir/python/" -r "$path" + done + fi + log "Zipping archive..." + set -f + if [ "$INPUT_TARGET" == "layer" ]; then + pushd "$tempdir" + zip -r $archive python $zip_opts + popd + else + pushd "$tempdir/python" + zip -r $archive . $zip_opts + popd + fi + set +f + rm -rf -- "$tempdir" + trap "rm -f -- '$archive'" EXIT } -publish_dependencies_as_layer(){ - echo "Publishing dependencies as a layer..." - local result=$(aws lambda publish-layer-version --layer-name "${INPUT_LAMBDA_LAYER_ARN}" --zip-file fileb://dependencies.zip) - LAYER_VERSION=$(jq '.Version' <<< "$result") - rm -rf python - rm dependencies.zip +list_layer_version_arns() +{ + arns= + for layer_name in $@; do + layer_arn="$(get_last_layer_version_arn "$layer_name")" + arns="$arns $layer_arn" + done + echo -n $arns } -publish_function_code(){ - echo "Deploying the code itself..." - zip -r code.zip . -x \*.git\* - aws lambda update-function-code --function-name "${INPUT_LAMBDA_FUNCTION_NAME}" --zip-file fileb://code.zip +lambda_function_exists() +{ + aws lambda list-functions | jq '.["Functions"][]["FunctionName"]' | \ + grep -q "$INPUT_NAME" } -update_function_layers(){ - echo "Using the layer in the function..." - aws lambda update-function-configuration --function-name "${INPUT_LAMBDA_FUNCTION_NAME}" --layers "${INPUT_LAMBDA_LAYER_ARN}:${LAYER_VERSION}" +deploy_lambda_function() +{ + log "Deploying lambda function: $INPUT_NAME..." + s3_url="s3://${INPUT_S3_BUCKET}/${INPUT_NAME}.zip" + aws s3 cp "$archive" "$s3_url" + log "Updating lambda function code: ${INPUT_NAME}" + if lambda_function_exists; then + aws lambda update-function-code \ + --architectures "$INPUT_ARCHITECTURES" \ + --function-name "$INPUT_NAME" \ + --zip-file "fileb://$archive" + opts= + if [ -n "$INPUT_LAYERS" ]; then + layers=$(list_layer_version_arns "$INPUT_LAYERS") + opts="--layers $layers" + fi + aws lambda wait function-updated --function-name "$INPUT_NAME" + log "Lambda function updated: $INPUT_NAME" + retry=4 + while ! aws lambda update-function-configuration \ + --function-name "${INPUT_NAME}" \ + --runtime "${INPUT_RUNTIMES%% *}" $opts; do + retry="$(($retry - 1))" + if [[ $retry -gt 0 ]]; then + die "Cannot update-function-configuration: ${INPUT_NAME}" + fi + sleep 1 + done + aws lambda wait function-updated --function-name "$INPUT_NAME" + log "Lambda function configured: $INPUT_NAME" + aws lambda publish-version --function-name "$INPUT_NAME" + log "Lambda function published: $INPUT_NAME" + else + log "No lambda function found: $INPUT_NAME" + fi + rm -f -- "$archive" + trap - EXIT } -deploy_lambda_function(){ - install_zip_dependencies - publish_dependencies_as_layer - publish_function_code - update_function_layers +deploy_lambda_layer() +{ + log "Deploying lambda layer: ${INPUT_NAME}..." + local s3_url="s3://${INPUT_S3_BUCKET}/${INPUT_NAME}.zip" + aws s3 cp "$archive" "$s3_url" + local result="$(aws lambda publish-layer-version \ + --layer-name "$INPUT_LAMBDA_LAYER_ARN" \ + --compatible-architectures $INPUT_ARCHITECTURES \ + --compatible-runtimes $INPUT_RUNTIMES \ + --zip-file "fileb://$archive" \ + )" + arn="$(jq .LayerVersionArn <<< "$result")" + [ $? != 0 ] || [ "$arn" == "null" ] && return 1 + echo -n $arn } -deploy_lambda_function -echo "Done." + +TAG="${INPUT_NAME#*#}" +INPUT_NAME="${INPUT_NAME%#*}" + + +case "$INPUT_TARGET" in + lambda) + make_archive + deploy_lambda_function + ;; + layer) + make_archive + deploy_lambda_layer + ;; + *) + die Invalid resource target: $INPUT_TARGET + ;; +esac + + +log "Done."