A Dynamic Web Site using Python and AWS Lambda

Mis a jours le 27 Jan 2021 à 10:40 · 746 mots · Lecture en 4 minutes python Cloud AWS lambda

Nous allons voir comment utiliser les Lambda d’AWS pour servir un site web dynamique et en mode serverless.

Python

La fonction python que l’on va utiliser pour la lambda doit avoir deux paramètres : event et context.

def my_lambda_function(event, context):
    return "Hello world !"

Pour donner une idée de ce que contiennent event et context, j’ai créé cette fonction :

import os
import json

def debug_lambda(event, context):
    json_region = os.environ['AWS_REGION']
    return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
                },
            "body": json.dumps({
                "Region ": json_region,
                'event' : str(event),
                'context': str(context)
                })
            }

Où la réponse de la lambda donne :

{
    "Region ": "eu-west-3",
    "event": "{'resource': '/', 'path': '/', 'httpMethod': 'GET', 'headers': {'Accept': 'text/html', 'Accept-Encoding': 'gzip, deflate, br', 'cache-control': 'max-age=0', 'CloudFront-Forwarded-Proto': 'https', 'CloudFront-Is-Desktop-Viewer': 'true', 'CloudFront-Is-Mobile-Viewer': 'false', 'CloudFront-Is-SmartTV-Viewer': 'false', 'CloudFront-Is-Tablet-Viewer': 'false', 'CloudFront-Viewer-Country': 'FR', 'dnt': '1', 'Host': '[REDACTED]', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'sec-gpc': '1', 'upgrade-insecure-requests': '1', 'User-Agent': '[REDACTED]', 'Via': '[REDACTED]', 'X-Amz-Cf-Id': '[REDACTED]', 'X-Amzn-Trace-Id': 'Root=1-[REDACTED]', 'X-Forwarded-For': '[REDACTED]', 'X-Forwarded-Port': '443', 'X-Forwarded-Proto': 'https'}, 'multiValueHeaders': {'Accept': ['text/html'], 'Accept-Encoding': ['gzip, deflate, br'], 'Accept-Language': ['fr-FR'], 'cache-control': ['max-age=0'], 'CloudFront-Forwarded-Proto': ['https'], 'CloudFront-Is-Desktop-Viewer': ['true'], 'CloudFront-Is-Mobile-Viewer': ['false'], 'CloudFront-Is-SmartTV-Viewer': ['false'], 'CloudFront-Is-Tablet-Viewer': ['false'], 'CloudFront-Viewer-Country': ['FR'], 'dnt': ['1'], 'Host': ['[REDACTED]'], 'sec-fetch-dest': ['document'], 'sec-fetch-mode': ['navigate'], 'sec-fetch-site': ['none'], 'sec-fetch-user': ['?1'], 'sec-gpc': ['1'], 'upgrade-insecure-requests': ['1'], 'User-Agent': ['[REDACTED]'], 'Via': ['[REDACTED]'], 'X-Amz-Cf-Id': ['[REDACTED]'], 'X-Amzn-Trace-Id': ['Root=1-[REDACTED]'], 'X-Forwarded-For': ['[REDACTED]'], 'X-Forwarded-Port': ['443'], 'X-Forwarded-Proto': ['https']}, 'queryStringParameters': None, 'multiValueQueryStringParameters': None, 'pathParameters': None, 'stageVariables': None, 'requestContext': {'resourceId': '[REDACTED]', 'resourcePath': '/', 'httpMethod': 'GET', 'extendedRequestId': '[REDACTED]', 'requestTime': '[REDACTED]', 'path': '/test', 'accountId': '[REDACTED]', 'protocol': 'HTTP/1.1', 'stage': 'test', 'domainPrefix': '[REDACTED]', 'requestTimeEpoch': 1611741450517, 'requestId': '[REDACTED]', 'identity': {'cognitoIdentityPoolId': None, 'accountId': None, 'cognitoIdentityId': None, 'caller': None, 'sourceIp': '[REDACTED]', 'principalOrgId': None, 'accessKey': None, 'cognitoAuthenticationType': None, 'cognitoAuthenticationProvider': None, 'userArn': None, 'userAgent': '[REDACTED]', 'user': None}, 'domainName': '[REDACTED]', 'apiId': '[REDACTED]'}, 'body': None, 'isBase64Encoded': False}",
    "context": "<__main__.LambdaContext object at 0x7f0f14df07c0>"
}

Maintenant qu’on a ça, on peut faire tous ce qu’on veut :-)

Pour pouvoir utiliser le code, il va falloir créer une archive zip.

$ zip python.zip *.py

Terraform

Maintenant qu’on a une fonction python à faire tourner, il faut la déployer ! Dans ce tuto, nous utiliserons Terraform.

Pour déployer sur AWS il faut avoir une clef d’accès avec son ID et son secret. Cette clef peut être récupérée dans la partie IAM de la console AWS.

Setup Terraform

Pour commencer, il faut dire a Terraform quel provider utiliser:

provider "aws" {
    region = "eu-west-3"
}

J’utilise eu-west-3, mais vous pouvez utiliser n’importe quelle région :)

La Lambda

Pour la resource de la lambda, il faut avoir une resource de type aws_lambda_function:

resource "aws_lambda_function" "lambda_function" {
  role             = aws_iam_role.lambda_exec_role.arn
  handler          = var.handler
  runtime          = var.runtime       # le type de code utilisé (i.e. python3.8)
  filename         = var.src_zip       # le nom du ficher .zip contentant le code
  function_name    = var.function_name # le nom de la fonction
  source_code_hash = filebase64sha256(var.src_zip)
}

Il faut aussi une resource de type aws_iam_role:

resource "aws_iam_role" "lambda_exec_role" {
  name        = "lambda_exec"
  path        = "/"
  description = "Allows Lambda Function to call AWS services on your behalf."

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

La gateway

Pour pouvoir accéder au code de la lambda, il faut mettre une gateway API en tant que proxy.

resource "aws_api_gateway_rest_api" "example" {
  name        = "ServerlessExample"
  description = "Terraform Serverless Application Example"
}

resource "aws_api_gateway_resource" "proxy" {
  rest_api_id = aws_api_gateway_rest_api.example.id
  parent_id   = aws_api_gateway_rest_api.example.root_resource_id
  path_part   = "proxy"
}

resource "aws_api_gateway_method" "proxy" {
  rest_api_id   = aws_api_gateway_rest_api.example.id
  resource_id   = aws_api_gateway_resource.proxy.id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda" {
  rest_api_id = aws_api_gateway_rest_api.example.id
  resource_id = aws_api_gateway_method.proxy.resource_id
  http_method = aws_api_gateway_method.proxy.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.lambda_function.invoke_arn
}

resource "aws_api_gateway_method" "proxy_root" {
  rest_api_id   = aws_api_gateway_rest_api.example.id
  resource_id   = aws_api_gateway_rest_api.example.root_resource_id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "lambda_root" {
  rest_api_id = aws_api_gateway_rest_api.example.id
  resource_id = aws_api_gateway_method.proxy_root.resource_id
  http_method = aws_api_gateway_method.proxy_root.http_method

  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.lambda_function.invoke_arn
}

resource "aws_api_gateway_deployment" "example" {
  depends_on = [
    aws_api_gateway_integration.lambda,
    aws_api_gateway_integration.lambda_root,
  ]

  rest_api_id = aws_api_gateway_rest_api.example.id
  stage_name  = "test"
}

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

  # The "/*/*" portion grants access from any method on any resource
  # within the API Gateway REST API.
  source_arn = "${aws_api_gateway_rest_api.example.execution_arn}/*/*"
}

Et pour enfin avoir l’url sur laquelle tapper pour voir le résultat de la fonction.

output "base_url" {
  value = aws_api_gateway_deployment.example.invoke_url
}

Cela va donner:

Outputs:

base_url = https://wncgbhu7te.execute-api.eu-west-3.amazonaws.com/test

La source du blog est disponible sur github. Et la lambda est accessible sur aws.

Aller plus loin

Image de l'auteur Tom Moulard

L'auteur:  Tom Moulard

Depuis mon enfance, je suis captivé par les articles de science et de technologie. Un jour, j'ai décidé de faire partie de ce monde : j'ai pris ma calculatrice programmable (une TI-82 stat).... La suite, sur mon site

Vous avez vu une erreur ? Quelque chose ne va pas ? Vous pouvez contribuer à cette page sur GitHub ou laisser un commentaire en dessous. Merci d'être passé par là :)