AWS S3 + APIGateway + Lambda でンプルなアプリケーションを作成

# AWS Cost Dashboardを作成してみた

## 概要

過去30 日分のAWSのコストを簡単に可視化できる簡単なアプリケーションを作成しました。このアプリケーションは、AWS Cost Explorer APIを利用して、AWSアカウントの過去30日分のコストを取得し、フロントエンドで表示する仕組みになっています。  
この記事では、使用した技術や実装のポイントについて解説していきます！

---

## 使用技術

### AWS　サービス
- **AWS Lambda**: コストデータを取得するバックエンドを実装
- **API Gateway**: Lambda関数を公開するためのエンドポイント
- **S3**: フロントエンドの静的ファイルをホスティング
- **CloudFront**: フロントエンドのキャッシュと高速配信を実現
- **WAF**: APIGatewayにアタッチし、特定のIPアドレスからのみアクセスを許可

### 構成管理
- **Terraform**: AWSリソースのプロビジョニングと管理
### CI/CD
- **GitHub Actions**: dev or main push時に、フロント（S3）への自動デプロイとCloudFrontのキャッシュ削除

---

## 全体のアーキテクチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/c009cdf1-7e04-4862-be2b-d784eb8e075d.png)

このアプリケーションでは以下のことができます。

1. AWS Cost Explorer APIを使用して、AWSアカウントの過去30日分のコストを取得
2. フロントエンドで取得したコストデータを表示
3. WAFを使用して、特定のIPアドレスからのみAPIにアクセスできるように制限

フロント部分は全く作り込んでいないので、こんな感じの見た目です。。笑

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/4cef69df-aa87-452e-b242-f22d8a21b615.png)

---

## CORSについて

今回の実装では、CORS (Cross-Origin Resource Sharing) に対応するために以下のポイントを押さえました。

1. **API Gatewayの設定**  
   API Gatewayのメソッドレスポンスで、以下のヘッダーを設定しました。
   - `Access-Control-Allow-Origin`
   - `Access-Control-Allow-Methods`
   - `Access-Control-Allow-Headers`

2. **Lambda関数のレスポンスヘッダー**  
   Lambda関数のレスポンスにも同様のCORSヘッダーを追加しました。これにより、フロントエンドからのリクエストが正常に処理されます。

以下は、Lambda関数でCORS対応を行った部分のコードです。

```py
"headers": {
    "Access-Control-Allow-Origin": allowed_origin,  # 環境変数から取得
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization"
}
```

## Lambdaコードについての解説
Lambda関数では、AWS Cost Explorer<vscode_annotation details='%5B%7B%22title%22%3A%22hardcoded-credentials%22%2C%22description%22%3A%22Embedding%20credentials%20in%20source%20code%20risks%20unauthorized%20access%22%7D%5D'> API</vscode_annotation>を使用してコストデータを取得しています。以下は、主な処理の流れです。

1.Cost Explorerクライアントの作成
- boto3ライブラリを使用して、Cost Explorerクライアントを作成します。

2.コストデータの取得
- get_cost_and_usageメソッドを使用して、指定した期間のコストデータを取得します。

3.レスポンスの整形
- 取得したデータを解析し、合計コストと通貨単位をレスポンスとして返します。

以下は、Lambda関数のコードの一部です。

```python
response = ce_client.get_cost_and_usage(
    TimePeriod={
        'Start': start_date,
        'End': end_date
    },
    Granularity='MONTHLY',
    Metrics=['UnblendedCost']
)

cost_data = response['ResultsByTime']
total_cost = sum(float(item['Total']['UnblendedCost']['Amount']) for item in cost_data)

return {
    'statusCode': 200,
    'body': json.dumps({
        'message': 'AWS account cost retrieved successfully.',
        'total_cost': total_cost,
        'currency': cost_data[0]['Total']['UnblendedCost']['Unit']
    }),
    "headers": {
        "Access-Control-Allow-Origin": allowed_origin,
        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization"
    },
}
```

## 実際に使用するための手順
試してみたい方は、以下の手順でセットアップしてください。

1.リポジトリをクローン
以下のコマンドでリポジトリをクローンします。
```bash
git clone https://github.com/takaakihoya/aws_cost_dashbord.git
```

2.Terraform.tfvars の作成
terraform/terraform.tfvars.sampleをコピーしてterraform/terraform.tfvarsを作成し、以下の値を設定してください。
```
profile: AWS CLIで使用するプロファイル名
hosted_zone: Route 53のホストゾーン名
product: サブドメイン名
lambda_source_dir: Lambda関数のソースコードディレクトリ
my_ip: 許可するIPアドレス
```

3.terraform apply
下記のコマンドを実行してください。
```terraform
#terraform/ で実行
terraform init
terraform apply
```

4.CI/CD 周りの設定
app/frontend/* 配下で変更があり、dev or main ブランチにpushすると自動でS3にデプロイされるようにworkflow を作成しています。
workflow の実行にはgithubにて下記の secret の設定が必要になります。
```
secrets.AWS_ROLE_ARN = マネコンで作成。github OIDC用のrole
ecrets.AWS_S3_BUCKET = terraform で作成した、S3URL
secrets.DISTRIBUTION_ID　= terraform で作成したCloudFrontのID
```
CI/CD 周りの設定は、別で記事を作成しているのでよければ参考にしてください。
https://qiita.com/hoyat4967/items/d77c321650f0798aab15

5.config.js の設定
app/frontend/config.js の`apiurl`を、terraform で作成したAPIGateway のURLに変更してください。

6.フロントエンドのデプロイ
GitHub Actionsを使用して、フロントエンドをS3にデプロイします。devまたはmainブランチにプッシュすることで、自動的にデプロイが実行されます。

以上でセットアップは完了です。ブラウザでCloudFrontのURLにアクセスし、AWSコストが表示されることを確認してみてください！