Вызов AWS Lambda через неподписанный POST в REST API

#amazon-web-services #rest #aws-lambda #aws-api-gateway

#amazon-веб-сервисы #rest #aws-lambda #aws-api-gateway

Вопрос:

Я хочу, чтобы на моем веб-сайте Jekyll была форма, которую посетители могли бы заполнять, и form action должна POST использоваться функция AWS Lambda. На веб-сайте не разрешен JavaScript, поэтому POST не должно требоваться подписание.

Я хочу максимально простую настройку и не нуждаюсь в высокой безопасности. Если есть способ избежать использования AWS API Gateway для создания HTTP API и каким-то образом заставить функцию Lambda напрямую получать POST из веб-браузера пользователя, это было бы идеально. Если требуется API Gateway, то самое простое решение было бы наилучшим.

Я хочу использовать исключительно команды командной строки (не веб-браузер) для работы с AWS API. Это позволяет использовать скриптовое решение.

Я потратил некоторое время на проблему, и вот что у меня получилось. Я пометил вопросы в deploy скрипте с помощью TODO . В этом скрипте есть некоторый дополнительный код, который может не понадобиться. Проблема в том, что я не уверен, что удалять, потому что я просто не могу понять, как предоставить POST лямбде.

Скрипты используют jq и yq, поэтому скрипты bash могут анализировать JSON и YAML соответственно.

_config.yml

 aws:
  cloudfront:
    distributionId: "" # Provide value if CloudFront is used on this site
  lambda:
    addSubscriber:
      custom: # TODO change these values to suit your website
        iamRoleName: lambda-ex
        name: addSubscriberAwsLambdaSample
        handler: addSubscriberAwsLambda.lambda_handler
        runtime: python3.8
      computed: # These values are computed by the _bin/awsLambda setup and deploy scripts
        arn: arn:aws:lambda:us-east-1:031372724784:function:addSubscriberAwsLambdaSample:3
        iamRoleArn: arn:aws:iam::031372724784:role/lambda-ex
  

utils исходный скрипт bash

 #!/bin/bash

function readYaml {
  # $1 - path
  yq r _config.yml "$1"
}

function writeYaml {
  # $1 - path
  # $2 - value
  yq w -i _config.yml "$1" "$2"
}


# AWS Lambda values
export LAMBDA_IAM_ROLE_ARN="$(  readYaml aws.lambda.addSubscriber.computed.iamRoleArn )"
export LAMBDA_NAME="$(          readYaml aws.lambda.addSubscriber.custom.name         )"
export LAMBDA_RUNTIME="$(       readYaml aws.lambda.addSubscriber.custom.runtime      )"
export LAMBDA_HANDLER="$(       readYaml aws.lambda.addSubscriber.custom.handler      )"
export LAMBDA_IAM_ROLE_NAME="$( readYaml aws.lambda.addSubscriber.custom.iamRoleName  )"

export PACKAGE_DIR="${GIT_ROOT}/_package"
export LAMBDA_ZIP="${PACKAGE_DIR}/function.zip"

# Misc values
export TITLE="$( readYaml title )"
export URL="$( readYaml url )"
export DOMAIN="$( echo "$URL" | sed -n -e 's,^https?://,,p' )"
  

настройка bash-скрипта

 #!/bin/bash

# Inspired by https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-awscli.html

SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>amp;1 amp;amp; pwd )"

GIT_ROOT="$( git rev-parse --show-toplevel )"
cd "${GIT_ROOT}"
source _bin/utils

# Define the execution role that gives an AWS Lambda function permission to access AWS resources.
read -r -d '' ROLE_POLICY_JSON <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# If a role named $LAMBDA_IAM_ROLE_NAME is already defined then use it
ROLE_RESULT="$( aws iam get-role --role-name "$LAMBDA_IAM_ROLE_NAME" 2> /dev/null )"
if [ $? -ne 0 ]; then
  ROLE_RESULT="$( aws iam create-role 
    --role-name "$LAMBDA_IAM_ROLE_NAME" 
    --assume-role-policy-document "$ROLE_POLICY_JSON"
  )"
fi
LAMBDA_IAM_ROLE_ARN="$( jq -r .Role.Arn <<< "$ROLE_RESULT" )"
writeYaml aws.lambda.addSubscriber.computed.iamRoleArn "$LAMBDA_IAM_ROLE_ARN"
  

развернуть скрипт bash

 # Call this script after the setup script has created the IAM role
# that gives the addSubscriber AWS Lambda function permission to access AWS resources
#
# 1) This script builds the AWS Lambda package and deploys it, with permissions.
#    Any previous version of the AWS Lambda is deleted.
#
# 2) The newly (re)created AWS Lambda ARN is stored in _config.yml
#
# 3) An AWS Gateway HTTP API is created so static web pages can POST subscriber information to the AWS Lambda function.
#    Because the web page is not allowed to have JavaScript, the POST is unsigned.
#    *** The API must allow for an unsigned POST!!! ***

# Set cwd to the git project root
GIT_ROOT="$( git rev-parse --show-toplevel )"
cd "${GIT_ROOT}"

# Load configuration environment variables from _bin/utils:
# DOMAIN, LAMBDA_IAM_ROLE_ARN, LAMBDA_IAM_ROLE_NAME, LAMBDA_HANDLER, LAMBDA_NAME, LAMBDA_RUNTIME, LAMBDA_ZIP, PACKAGE_DIR, and URL
source _bin/utils

# Directory that this script resides in
SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>amp;1 amp;amp; pwd )"

echo "Building the AWS Lambda and packaging it into a zip file"
"$SOURCE_DIR/package" "$PACKAGE_DIR" > /dev/null

# Check to see if the Lambda function already exists.
LAMBDA="$( aws lambda list-functions | jq ".Functions[] | select(.FunctionName | contains("$LAMBDA_NAME"))" )"

if [ -z "$LAMBDA" ]; then
  echo "The AWS Lambda function '$LAMBDA_NAME' does not exist yet, so create it"
  LAMBDA_METADATA="$( aws lambda create-function 
    --description "Add subscriber to the MailChimp list with ID '$MC_LIST_ID_MSLINN' for the '$DOMAIN' website" 
    --environment "{
      "Variables": {
        "MC_API_KEY_MSLINN": "$MC_API_KEY_MSLINN",
        "MC_LIST_ID_MSLINN": "$MC_LIST_ID_MSLINN",
        "MC_USER_NAME_MSLINN": "$MC_USER_NAME_MSLINN"
      }
    }" 
    --function-name "$LAMBDA_NAME" 
    --handler "$LAMBDA_HANDLER" 
    --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/$LAMBDA_IAM_ROLE_NAME" 
    --runtime "$LAMBDA_RUNTIME" 
    --zip-file "fileb://$LAMBDA_ZIP" 
    | jq -S .
  )"
  LAMBDA_ARN="$( jq -r .Configuration.FunctionArn <<< "$LAMBDA_METADATA" )"
else
  echo "The AWS Lambda function '$LAMBDA_NAME' already exists, so update it"
  LAMBDA_METADATA="$( aws lambda update-function-code 
    --function-name "$LAMBDA_NAME" 
    --publish 
    --zip-file "fileb://$LAMBDA_ZIP" 
    | jq -S .
  )"
  LAMBDA_ARN="$( jq -r .FunctionArn <<< "$LAMBDA_METADATA" )"
fi
echo "AWS Lambda ARN is $LAMBDA_ARN"
writeYaml aws.lambda.addSubscriber.computed.arn "$LAMBDA_ARN"

echo "Attach the AWSLambdaBasicExecutionRole managed policy to $LAMBDA_IAM_ROLE_NAME."
aws iam attach-role-policy 
  --role-name $LAMBDA_IAM_ROLE_NAME 
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole


#### Integrate with API Gateway for REST
#### Some or all of the following code is probably not required

GATEWAY_NAME="addSubscriberTo_$MC_LIST_ID_MSLINN"

API_GATEWAYS="$( aws apigateway get-rest-apis )"
if [ "$( jq ".items[] | select(.name | contains("$GATEWAY_NAME"))" <<< "$API_GATEWAYS" )" ]; then
  echo "API gateway '$GATEWAY_NAME' already exists."
else
  echo "Creating API gateway '$GATEWAY_NAME'."

  API_JSON="$( aws apigateway create-rest-api 
    --name "$GATEWAY_NAME" 
    --description "API for adding a subscriber to the Mailchimp list with ID '$MC_LIST_ID_MSLINN' for the '$DOMAIN' website"
  )"
  REST_API_ID="$( jq -r .id <<< "$API_JSON" )"

  API_RESOURCES="$( aws apigateway get-resources --rest-api-id $REST_API_ID )"
  ROOT_RESOURCE_ID="$( jq -r .items[0].id <<< "$API_RESOURCES" )"

  NEW_RESOURCE="$( aws apigateway create-resource 
    --rest-api-id "$REST_API_ID" 
    --parent-id "$RESOURCE_ID" 
    --path-part "{proxy }"
  )"
  NEW_RESOURCE_ID=$( jq -r .id <<< $NEW_RESOURCE )

if false; then
  # Is this step useful for any reason?
  aws apigateway put-method 
    --authorization-type "NONE" 
    --http-method ANY 
    --resource-id "$NEW_RESOURCE_ID" 
    --rest-api-id "$REST_API_ID"
fi

# The following came from https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#set-up-lambda-proxy-integration-using-cli
#   Instead of supplying an IAM role for --credentials, call the add-permission command to add resource-based permissions.
#   I need an example of this.
# Alternatively, how to obtain IAM_ROLE_ID? Again, I need an example.
  aws apigateway put-integration 
    --credentials "arn:aws:iam::${IAM_ROLE_ID}:role/apigAwsProxyRole" 
    --http-method ANY 
    --integration-http-method POST 
    --rest-api-id "$REST_API_ID" 
    --resource-id "$NEW_RESOURCE_ID" 
    --type AWS_PROXY 
    --uri arn:aws:apigateway:`aws configure get region`:lambda:path/2015-03-31/functions/$LAMBDA_ARN

  if [ "$LAMBDA_TEST"]; then
    # Deploy the API to a test stage
    aws apigateway create-deployment 
      --rest-api-id "$REST_API_ID" 
      --stage-name test
  else
    # Deploy the API live
    aws apigateway create-deployment 
      --rest-api-id "$REST_API_ID" 
      --stage-name TODO_WhatNameGoesHere
  fi
fi

echo "Check out the defined lambdas at https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions"
  

Комментарии:

1. Вы не можете получить доступ к lambda напрямую из браузера без JavaScript. Вам нужен шлюз api. Просто не устанавливайте на него никаких мер безопасности. Это просто.

2. Я не согласен с «простым», но если вы хотите, чтобы функция lambda обрабатывала отправку анонимного запроса POST из ванильной HTML-формы, api gateway — подходящий инструмент для использования.

3. @bryan60, я хотел бы знать, что делать для простого решения. Если вы можете дать простой ответ, пожалуйста, сделайте это. Я, вероятно, написал большую часть кода API Gateway в нижней половине deploy скрипта, хотя он, безусловно, нуждается в исправлениях.

4. Просто используйте интерфейс командной строки aws sam. Настройка сценария для развертывания шлюза и лямбды занимает 15 минут

5. @bryan60 Да, да, я знаю. Пожалуйста, будьте конкретны. Я усердно работал, чтобы попытаться получить правильный ответ.

Ответ №1:

Инфраструктура сценариев Bash плоха. Возможно, в конечном итоге вы все сделаете правильно, но есть инструменты, которые делают процесс бесконечно проще.

Я предпочитаю Terraform, и вот как будет выглядеть API Gateway lambda:

 provider "aws" {
}

# lambda

resource "random_id" "id" {
  byte_length = 8
}

data "archive_file" "lambda_zip" {
  type        = "zip"
  output_path = "/tmp/lambda.zip"
  source {
    content  = <<EOF
module.exports.handler = async (event, context) => {
// write the lambda code here
    }
};
EOF
    filename = "main.js"
  }
}

resource "aws_lambda_function" "lambda" {
  function_name = "${random_id.id.hex}-function"

  filename         = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  handler = "main.handler"
  runtime = "nodejs12.x"
  role    = aws_iam_role.lambda_exec.arn
}

data "aws_iam_policy_document" "lambda_exec_role_policy" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = [
      "arn:aws:logs:*:*:*"
    ]
  }
}

resource "aws_cloudwatch_log_group" "loggroup" {
  name              = "/aws/lambda/${aws_lambda_function.lambda.function_name}"
  retention_in_days = 14
}

resource "aws_iam_role_policy" "lambda_exec_role" {
  role   = aws_iam_role.lambda_exec.id
  policy = data.aws_iam_policy_document.lambda_exec_role_policy.json
}

resource "aws_iam_role" "lambda_exec" {
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow"
    }
  ]
}
EOF
}

# api gw

resource "aws_apigatewayv2_api" "api" {
  name          = "api-${random_id.id.hex}"
  protocol_type = "HTTP"
  target        = aws_lambda_function.lambda.arn
}

resource "aws_lambda_permission" "apigw" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.arn
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
}

output "domain" {
  value = aws_apigatewayv2_api.api.api_endpoint
}
  

Обратите внимание, что последние 2 ресурса являются шлюзом API, все предыдущие предназначены для функции Lambda.

Комментарии:

1. Спасибо за такой подробный ответ. Тем не менее, я на самом деле ищу решение bash. Кажется, что последние 2 ресурса содержат ответ. Если показанный вами код шлюза API был описан серией вызовов aws cli, тогда я мог бы протестировать, используя CURL для ОТПРАВКИ в конечную точку.