サーバレスアーキテクチャによる静的ウェブサイト構築

# はじめに
このリポジトリは、S3、API Gateway、Lambda、RDS、IAMを利用したサーバレス構築の概要を説明します。この構築は、Terraformを使用してIaC (Infrastructure as Code) を実現し、静的ウェブサイトのホスティング、APIゲートウェイによるリバースプロキシ、Lambda関数の実行、RDSデータベースの利用を実現します。


以下がレポジトリです。

https://github.com/sugiyama404/practice_lambda

参考記事

https://cloud5.jp/apigateway-lambda-dynamodb-serverless/#--5

https://qiita.com/suzuki-navi/items/6a896a6577deaa858210

# 構成

サーバレスアーキテクチャ構成図:

![aws.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/2ead2e19-4c1d-4802-e5a0-2dd36635e2dd.png)

# Terraform によるインフラストラクチャの構築

Terraform は、インフラストラクチャをコードとして定義および管理するためのオープンソースツールです。Terraform を使用して、S3 バケット、API Gateway、Lambda 関数、RDS インスタンス、IAM ポリシーなどの AWS リソースを宣言的に作成できます。

# このアーキテクチャの利点

このアーキテクチャには、以下のような利点があります。

+ スケーラビリティ: トラフィックが増加すると、自動的にスケールアップできます。
+ コスト効率: 使用した分だけ支払う従量課金制モデルです。
+ 運用管理: サーバーを管理する必要はありません。
+ 高可用性: 冗長構成により、高い可用性を提供します。

# コード

## API Gateway

API Gateway は、フルマネージドのサービスで、API を作成、公開、管理、保護、監視するための単一の統合インターフェースを提供します。このアーキテクチャでは、API Gateway をリバースプロキシとして使用し、クライアントからのリクエストを S3 または Lambda に振り分けています。

```terraform:infra/modules/apigateway/aws_api_gateway_rest_api.tf
resource "aws_api_gateway_rest_api" "main" {
  body = jsonencode({
    openapi = "3.0.1"
    info = {
      title   = "api"
      version = "1.0"
    }
    paths = {
      get = {
        x-amazon-apigateway-integration = {
          payloadFormatVersion = "1.0"
          type                 = "HTTP_PROXY"
          uri                  = "${var.s3_url}"
        }
      }
      post = {
        x-amazon-apigateway-integration = {
          payloadFormatVersion = "1.0"
          type                 = "AWS_PROXY"
          uri                  = "${var.lambda_invoke_arn}"
          credentials          = "${var.iam_role_lambda}"
        }
      }
    }
  })

  name = "main"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}
```

```terraform:infra/modules/apigateway/aws_api_gateway_integration.tf
resource "aws_api_gateway_integration" "MyIntergration" {
  rest_api_id             = aws_api_gateway_rest_api.main.id
  resource_id             = aws_api_gateway_rest_api.main.root_resource_id
  http_method             = aws_api_gateway_method.MyMethod.http_method
  type                    = "AWS_PROXY"
  cache_namespace         = aws_api_gateway_rest_api.main.root_resource_id
  content_handling        = "CONVERT_TO_TEXT"
  integration_http_method = "POST"
  passthrough_behavior    = "WHEN_NO_MATCH"
  uri                     = var.lambda_invoke_arn
}

resource "aws_api_gateway_integration" "MyIntergrationS3" {
  rest_api_id             = aws_api_gateway_rest_api.main.id
  resource_id             = aws_api_gateway_rest_api.main.root_resource_id
  http_method             = aws_api_gateway_method.MyMethods3.http_method
  type                    = "HTTP_PROXY"
  cache_namespace         = aws_api_gateway_rest_api.main.root_resource_id
  content_handling        = "CONVERT_TO_TEXT"
  integration_http_method = "GET"
  passthrough_behavior    = "WHEN_NO_MATCH"
  uri                     = var.s3_url
}
```

```terraform:infra/modules/apigateway/aws_api_gateway_method.tf
resource "aws_api_gateway_method" "MyMethod" {
  rest_api_id   = aws_api_gateway_rest_api.main.id
  resource_id   = aws_api_gateway_rest_api.main.root_resource_id
  http_method   = "POST"
  authorization = "NONE"

  depends_on = [aws_api_gateway_rest_api.main]
}

resource "aws_api_gateway_method" "MyMethods3" {
  rest_api_id   = aws_api_gateway_rest_api.main.id
  resource_id   = aws_api_gateway_rest_api.main.root_resource_id
  http_method   = "GET"
  authorization = "NONE"

  depends_on = [aws_api_gateway_rest_api.main]
}
```

```terraform:infra/modules/apigateway/aws_api_gateway_rest_api_policy.tf
data "aws_iam_policy_document" "api_gateway_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
    actions   = ["execute-api:Invoke"]
    resources = ["${aws_api_gateway_rest_api.main.execution_arn}/*"]
  }
}

resource "aws_api_gateway_rest_api_policy" "policy" {
  rest_api_id = aws_api_gateway_rest_api.main.id
  policy      = data.aws_iam_policy_document.api_gateway_policy.json
}
```

```terraform:infra/modules/apigateway/aws_api_gateway_deployment.tf
resource "aws_api_gateway_deployment" "main" {
  rest_api_id = aws_api_gateway_rest_api.main.id

  stage_name = "stage"

  triggers = {
    redeployment = sha1(jsonencode(aws_api_gateway_rest_api.main.body))
  }

  depends_on = [aws_api_gateway_method.MyMethod, aws_api_gateway_method.MyMethods3,
  aws_api_gateway_integration.MyIntergration, aws_api_gateway_integration.MyIntergrationS3]
  lifecycle {
    create_before_destroy = true
  }
}
```

## Lambda

Lambda は、サーバーをプロビジョニングしたり管理したりすることなく、コードを実行できるイベント駆動型コンピューティングサービスです。このアーキテクチャでは、Lambda を使用して動的な処理を実装しています。

```terraform:infra/modules/lambda/aws_lambda_function.tf
data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = "./modules/lambda/src"
  output_path = "./modules/lambda/src/lambda_function_payload.zip"
}

resource "aws_lambda_function" "main" {
  filename         = "./modules/lambda/src/lambda_function_payload.zip"
  function_name    = "lambda_function"
  description      = "lambda_function"
  role             = var.iam_role_lambda
  architectures    = ["x86_64"]
  handler          = "index.lambda_handler"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout          = 30
  runtime          = "python3.9"

  vpc_config {
    subnet_ids         = [var.subnet_public_subnet_1a_id]
    security_group_ids = [var.sg_lambda_id]
  }

  environment {
    variables = {
      db_host = var.db_address
      db_user = var.db_username
      db_pass = var.db_password
      db_name = var.db_name
    }
  }
  tags = {
    Name = "${var.app_name}-lamdba"
  }
}
```

```terraform:infra/modules/lambda/aws_lambda_permission.tf
resource "aws_lambda_permission" "apigateway-to-lambda" {
  statement_id  = "AllowAPIGatewayGetTrApi"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.arn
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${var.api_execution_arn}/*"
}
```

```python:infra/modules/lambda/src/index.py
import base64
import json
import os
import pymysql

ENDPOINT=os.environ["db_host"]
PORT=3306
USER=os.environ["db_user"]
PASS=os.environ["db_pass"]
DBNAME=os.environ["db_name"]
os.environ['LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN'] = '1'

def execute_sql(cur):
    try:
        sql = """
        CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(255) DEFAULT NULL,
        email VARCHAR(255) DEFAULT NULL,
        password VARCHAR(255) DEFAULT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        );
        """
        cur.execute(sql)
    except pymysql.Error as e:
        print(f"エラーが発生しました: {e}")
        cur.rollback()

conn =  pymysql.connect(host=ENDPOINT, user=USER, passwd=PASS, port=PORT, database=DBNAME)
cur = conn.cursor()

def lambda_handler(event, context):
    execute_sql(cur)
    try:
        username = event.get('username')
        email = event.get('email')
        password = event.get('password')

        sql = "INSERT INTO users (username, email, password) VALUES (%s, %s, %s)"
        cur.execute(sql, (username, email, password,))
        return  {
        'statusCode': 200,
        'body': json.dumps({}),
        'headers': {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
        },
        }
    except:
        import traceback
        err = traceback.format_exc()
        print(err)
        return {
          'statusCode' : 500,
          'headers' : {
            'context-type' : 'text/json',
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Allow-Methods': 'OPTIONS,POST,GET'
          },
          'body' : json.dumps({
            'error' : '内部エラーが発生しました'
            })
          }

```

# 実行結果

![Screenshot 2024-04-17 at 11.20.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/65bd52bc-f41f-29c6-7b61-514b5cb7ac82.png)

![Screenshot 2024-04-17 at 11.20.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/0ff9f320-b37b-f1a3-258d-589f52db64d1.png)