LoginSignup
0
0

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

Last updated at Posted at 2024-04-17

はじめに

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

以下がレポジトリです。

参考記事

構成

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

aws.png

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

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

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

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

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

コード

API Gateway

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

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"]
  }
}
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
}
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]
}
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
}
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 を使用して動的な処理を実装しています。

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"
  }
}
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}/*"
}
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

Screenshot 2024-04-17 at 11.20.24.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0