[Terraform] AWSサービスでREST APIを構築する

## はじめに
この記事では、AWSサービスを使ってREST APIをTerraformで構築する手順について解説します。
具体的には以下の構成を構築します。

ブラウザからエンドポイントにアクセスし、DynamoDBにあるデータが取得されて、ブラウザにJson形式で返される。といった構成になっています。
![Frame 1 (1)](https://github.com/dai-gi/terraform-aws-api-crud/assets/59759668/4338a40f-c88f-453b-87d8-ae184558f70d)
    
## 前提条件
- MacOS環境
- Terraformがインストール済み
- AWS CLIのProfileがセットアップ済み

## ディレクトリ構成
```sh
.
├── .gitignore
├── .terraform.lock.hcl
├── README.md
├── main.tf
├── terraform.tfstate
├── src
│   └── lambda_function.py
└── upload
    └── lambda_function.zip
```

## Step1:　main.tfの作成
まずは main.tf を作成し、Terraformの設定、プロバイダーの設定、必要なリソースを定義します。
プロバイダーとは、雑いうと、TerraformとAWS等のサービスを連携させるモジュールのようなものになります。
```tf
terraform {
  required_version = "~> 1.7"

  required_providers {
    # AWSを利用するためのプロバイダー
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    # Lambda関数のスクリプトをzip化するためのプロバイダー
    archive = {
      source  = "hashicorp/archive"
      version = "~> 2.4"
    }
  }

  # terraform.tfstate(AWS構成の状態管理)ファイルの管理場所
  backend "local" {
    path = "./terraform.tfstate"
  }
}

## DynamoDB
# DynamoDBにテーブルを作成
resource "aws_dynamodb_table" "gang_of_straw" {
  name           = "gang_of_straw"
  billing_mode   = "PROVISIONED"
  read_capacity  = 20
  write_capacity = 20
  hash_key       = "name"
  range_key      = "bounty"
  attribute {
    name = "name"
    type = "S"
  }
  attribute {
    name = "bounty"
    type = "S"
  }
}

## IAM
# [*] lambda関数がDynamoDBにアクセスするためのロールを作成
resource "aws_iam_role" "dynamodb_read_only" {
  name = "dynamodb_read_only"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = "sts:AssumeRole"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}
# Lambda関数のログをCloudWatchに書き込むための権限を　[＊]　にアタッチ
resource "aws_iam_role_policy_attachment" "cloudwatch_log_writing_permit" {
  role       = aws_iam_role.dynamodb_read_only.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Lambda関数がDynamoDBのテーブルを読み込み可能にするための権限を　[＊]　にアタッチ
resource "aws_iam_role_policy_attachment" "dynamodb_reading_permit" {
  role       = aws_iam_role.dynamodb_read_only.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess"
}

## Lambda
# Lambda関数のスクリプトをzip化
data "archive_file" "lambda_function_file" {
  type        = "zip"
  source_dir  = "./src/"
  output_path = "./upload/lambda_function.zip"
}
# Lambda関数を作成
resource "aws_lambda_function" "get_gang_info" {
  filename         = data.archive_file.lambda_function_file.output_path
  function_name    = "get_gang_info"
  role             = aws_iam_role.dynamodb_read_only.arn
  handler          = "lambda_function.lambda_handler"
  source_code_hash = data.archive_file.lambda_function_file.output_base64sha256
  runtime          = "python3.12"
  timeout          = 29
  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.gang_of_straw.name
    }
  }
}
# API GatewayからLambda関数を呼び出せるようにするための権限を設定
resource "aws_lambda_permission" "tr_lambda_permit" {
  statement_id  = "get_gang_info_api"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.get_gang_info.arn
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.get_gang_info_api.execution_arn}/*/GET/gang"
}

## API Gateway
# API GatewayのREST APIを作成
resource "aws_api_gateway_rest_api" "get_gang_info_api" {
  name = "get_gang_info_api"
}
# ルート「/」にリソース「/リソース名/」を作成
resource "aws_api_gateway_resource" "gang_api_resource" {
  rest_api_id = aws_api_gateway_rest_api.get_gang_info_api.id
  parent_id   = aws_api_gateway_rest_api.get_gang_info_api.root_resource_id
  path_part   = "gang"
}
# 作成したリソースに対してHTTPメソッドのGETを作成
resource "aws_api_gateway_method" "gang_get_method" {
  authorization = "NONE"
  http_method   = "GET"
  resource_id   = aws_api_gateway_resource.gang_api_resource.id
  rest_api_id   = aws_api_gateway_rest_api.get_gang_info_api.id
}
# Lambdaプロキシ統合を設定
resource "aws_api_gateway_integration" "get_gang_lambda_integration" {
  http_method             = aws_api_gateway_method.gang_get_method.http_method
  resource_id             = aws_api_gateway_resource.gang_api_resource.id
  rest_api_id             = aws_api_gateway_rest_api.get_gang_info_api.id
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.get_gang_info.invoke_arn
}
# 作成したREST APIをデプロイ（公開）
resource "aws_api_gateway_deployment" "tr_api" {
  depends_on = [
    aws_api_gateway_integration.get_gang_lambda_integration
  ]
  rest_api_id = aws_api_gateway_rest_api.get_gang_info_api.id
  stage_name  = "dev"
  # triggers = {
  #   redeployment = filebase64("main.tf")
  # }
}
```

## Step2:　Lambda関数の作成
次にLambda関数のスクリプトを「src/lambda_function.py」に書いていきます。

このスクリプトでは、`https://・・・/dev/gang?パラメータ=値`のような形式で渡されたAPIからパラメータを抽出し、DBから抽出したパラメータに該当するデータを取得してJson形式で返す処理を行っています。
```py
import boto3
import json
import logging
from botocore.exceptions import ClientError

dynamodb = boto3.resource('dynamodb')

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()

def get_item(params):
    
    if params == None:
        logger.warn('パラメータを入力してください。足りないパラメータ：name, bounty')
        return None
    else:
        if 'name' in params.keys():
            if 'bounty' in params.keys():
                try:
                    table = dynamodb.Table('gang_of_straw')
                    response = table.get_item(Key=params)
                    print(f'レスポンスの内容：{response}')
                    if response.get('Item') == None:
                        return 0
                    else:
                        return response.get('Item')
                except ClientError as e:
                    if e.response['Error']['Code'] == 'ValidationException':
                        logger.warn('指定されたキー要素がスキーマと一致しません')
                        return 0
                    else:
                        raise e
            else:
                logger.warn('パラメータが足りません。足りないパラメータ：bounty')
                return 'bounty'
        else:
            logger.warn('パラメータが足りません。足りないパラメータ：name')
            return 'name'


def lambda_handler(event, context):
    item = get_item(event.get('queryStringParameters'))

    if item == None:
        return {
            'statusCode': 400,
            'body': 'パラメータを入力してください。足りないパラメータ：name, bounty'
        }
    elif (item == 'name') or (item == 'bounty'):
        return {
            'statusCode': 400,
            'body': f'パラメータに{item}を入力してください。'
        }
    elif item == 0:
        return {
            'statusCode': 400,
            'body': '該当する情報はありませんでした。'
        }
    else:
        return {
            'statusCode': 200,
            'body': json.dumps(item)
        }
```

## Step３:　コマンド実行
```sh
# Terraformの初期化
terraform init

# 構築されるインフラ情報を確認
terraform plan

# 確認した内容でインフラを構築
terraform apply
```

## Step４:　動作確認

### データ投入
以下のデータをDynamoDBのコンソール画面に貼り付けて項目を作成します。
```json
{
  "name": {
    "S": "Monkey・D・Luffy"
  },
  "bounty": {
    "S": "1500000000"
  }
}
```
![スクリーンショット 2024-03-19 7 52 46](https://github.com/dai-gi/terraform-aws-api/assets/59759668/bc1588df-d2fd-41e3-b2c9-e819400366ca)


### エンドポイントにアクセス
① API　Gatewayのコンソール画面からデプロイされたAPIのエンドポイントをコピーして、ブラウザに貼り付けます。
![スクリーンショット 2024-03-19 7 44 18](https://github.com/dai-gi/terraform-aws-api/assets/59759668/25ccf4aa-2bff-4fa8-b2cd-d6f42b5dd991)

② エンドポイントにアクセスし、以下の画像のようにレスポンスが返されれば、正常に処理が行われたことになります。
動作確認に「[Talend API Tester - free Edition](https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja)」というGoogleの拡張機能を使用しています。
![](https://github.com/dai-gi/terraform-aws-api-crud/assets/59759668/68298590-9047-4342-b527-7b94a456784b)

以上で、AWSサービスを使用しREST APIをTerraformで構築する手順についての解説は終わりとなります。

## さいごに
この記事が何かの参考になれば幸いです。また、内容に誤りなどがあった際に、ぜひコメントをいただければと思います。

## 参考記事
https://oji-cloud.net/2022/04/15/post-7028/