AWS Amplify + AWS CDK で人生初のアプリケーションを作ってみた (第2回)

こんにちは。SCSK渡辺(大)です。

ゴールデンウィークに大阪万博に行くことが決まりました。
3Dのクラゲに触れることができ感触を味わえるらしいので楽しみです。

今回は、AWS Amplify+AWS CDKで人生初のアプリケーションを作ってみました。の第2回です。

 

どんなAPIを作るのか

実際に利用することが目的ではなく、勉強が目的であるため簡単なものにすることにしました。

  • AWSアカウント全体の直近30日間の利用料金合計を取得する
  • 利用料金が最も高いサービスTOP3とその利用料金を取得する
  • 上記の2つを返す

準備

プロジェクトの雛形を作成する

cdkという名前のフォルダを作成し、そこで作業しました。

プロジェクトの雛形を作成します。

$ cdk init app --language typescript
Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `npx cdk deploy` deploy this stack to your default AWS account/region
* `npx cdk diff` compare deployed stack with current state
* `npx cdk synth` emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
✅ All done!

 

クレデンシャル情報を変更する

aws s3 lsで結果が返ってくる状態になっていれば問題ありません。

$ aws s3 ls
yyyy-mm-dd hh:mm:ss [S3バケット名]

 

 

テストデプロイをする

ブートストラップは既に別の利用者が実行していたため割愛しました。
cdk diffでメタデータ以外が作成されないことを確認します。

$ cdk diff
Stack CdkStack
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/xxxxxxxxx/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Conditions
[+] Condition CDKMetadata/Condition CDKMetadataAvailable: {"Fn::Or":[{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"af-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-northeast-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-south-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-3"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"ap-southeast-4"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"ca-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"cn-northwest-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-central-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-north-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-south-2"]}]},{"Fn::Or":[{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"eu-west-3"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"il-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-central-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"me-south-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"sa-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-1"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-east-2"]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-1"]}]},{"Fn::Equals":[{"Ref":"AWS::Region"},"us-west-2"]}]}


✨ Number of stacks with differences: 1

 

デプロイします。

$ cdk deploy

✨ Synthesis time: 8.96s

CdkStack: deploying... [1/1]
CdkStack: creating CloudFormation changeset...
CdkStack | 0/2 | 0:20:52 | REVIEW_IN_PROGRESS | AWS::CloudFormation::Stack | CdkStack User Initiated
CdkStack | 0/2 | 0:20:58 | CREATE_IN_PROGRESS | AWS::CloudFormation::Stack | CdkStack User Initiated
CdkStack | 0/2 | 0:21:01 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata) 
CdkStack | 0/2 | 0:21:02 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata) Resource creation Initiated
CdkStack | 1/2 | 0:21:02 | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata/Default (CDKMetadata) 
CdkStack | 2/2 | 0:21:03 | CREATE_COMPLETE | AWS::CloudFormation::Stack | CdkStack

✅ CdkStack

✨ Deployment time: 14.23s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:[アカウントID]:stack/CdkStack/xNNNNNNN-xxxxxxxxx-xxxx-xxxxxxxxxxx

✨ Total time: 23.19s

 

CloudFormationスタックが作成されたことを確認する

AWSマネジメントコンソールでスタックが作成され、メタデータのみが作成されていることを確認します。

 

リソースを定義する

AWS Lambda関数を定義する

プロジェクトの直下にlambdaディレクトリを作成します。
その中にgetCostExplorer.pyを作成して、以下のように記述します。
(生成AIに沢山助けてもらいました。)

import json
import boto3
import logging
from datetime import datetime, timedelta

logger = logging.getLogger()
logger.setLevel(logging.INFO)

formatter = logging.Formatter('%(levelname)s: %(message)s')
for handler in logger.handlers:
    handler.setFormatter(formatter)

# CORSヘッダーを定数として定義
CORS_HEADERS = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key',
    'Access-Control-Allow-Credentials': 'true'
}

def handler(event, context):
    logger.info("Lambdaハンドラが開始されました。")
    logger.info(f"イベント: {json.dumps(event)}")

    ce_client = boto3.client('ce')

    end_date = datetime.now()
    start_date = end_date - timedelta(days=30)
    time_period = {
        'Start': start_date.strftime('%Y-%m-%d'),
        'End': end_date.strftime('%Y-%m-%d')
    }
    granularity = 'MONTHLY'
    metrics = ['UnblendedCost']

    try:
        # コストデータを取得
        def fetch_cost_data(group_by=None):
            params = {
                'TimePeriod': time_period,
                'Granularity': granularity,
                'Metrics': metrics
            }
            if group_by:
                params['GroupBy'] = group_by
            return ce_client.get_cost_and_usage(**params)

        total_cost_response = fetch_cost_data()
        service_cost_response = fetch_cost_data(
            group_by=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}]
        )

        # 総コストを計算
        total_cost = float(total_cost_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])

        # サービスごとのコストを計算
        services = {
            group['Keys'][0]: float(group['Metrics']['UnblendedCost']['Amount'])
            for group in service_cost_response['ResultsByTime'][0]['Groups']
        }

        # コストが高い上位3つのサービスを抽出
        top_services = sorted(
            [{'service': k, 'cost': v} for k, v in services.items()],
            key=lambda x: x['cost'],
            reverse=True
        )[:3]

        return {
            'statusCode': 200,
            'headers': CORS_HEADERS,
            'body': json.dumps({
                'total_cost': f"${total_cost:.2f}",
                'period': f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}",
                'top_services': [{
                    'service': service['service'],
                    'cost': f"${service['cost']:.2f}"
                } for service in top_services]
            })
        }

    except Exception as e:
        logger.error(f"エラー: {str(e)}")
        return {
            'statusCode': 500,
            'headers': CORS_HEADERS,
            'body': json.dumps({'error': str(e)})
        }

 

スタックを定義する

プロジェクト直下のlibディレクトリにあるcdk-stack.tsを以下のように修正します。
この内容はCloudFormationのスタックに対応するものであると理解しています。
AWS CDKのメリットである抽象化により、CloudFormationよりも少ない文字数でリソースを定義することが出来ます。
プロジェクト先ではConstructs Levelを意識する必要があるのかと思いますが、今回は気にしません。
(もちろん、こちらも生成AIに助けてもらいました。)

import { Stack, StackProps, Duration } from "aws-cdk-lib";
import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from "aws-cdk-lib/aws-iam";

export class CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Cost Explorer Lambda関数
    const getCostExplorerLambda = new Function(this, "GetCostExplorerHandler", {
      runtime: Runtime.PYTHON_3_13,
      code: Code.fromAsset("lambda"),
      handler: "getCostExplorer.handler",
      timeout: Duration.minutes(1)  // タイムアウトを1分に設定
    });

    // Cost Explorer用のIAMポリシー
    const costExplorerPolicy = new iam.PolicyStatement({
      actions: ["ce:GetCostAndUsage"],
      resources: ["*"],
    });
    getCostExplorerLambda.addToRolePolicy(costExplorerPolicy);

    // Cost Explorer API
    const costApi = new apigateway.RestApi(this, "CostExplorerApi", {
      defaultCorsPreflightOptions: {
        allowOrigins: ['*'],
        allowMethods: ['GET', 'OPTIONS'],
        allowHeaders: ['Content-Type'],
        allowCredentials: true
      }
    });

        const getCostIntegration = new apigateway.LambdaIntegration(getCostExplorerLambda);
        costApi.root.addMethod('GET', getCostIntegration);
      }
    }

 

App定義をする

プロジェクト直下のbinディレクトリにあるcdk.tsを以下のように変更します。
このファイルはCDKの中で最上位の層であるApp定義をするものです。
複数のスタックの依存関係などを定義できますが、今回は1つのスタックしかないため内容は非常にシンプルです。
libディレクトリにあるcdk-stack.tsの内容をデプロイするだけの記述です。

import * as cdk from 'aws-cdk-lib';
import { CdkStack } from '../lib/cdk-stack';

const app = new cdk.App();
new CdkStack(app, 'CdkStack', {
});

 

APIの結果を確認する

改めてデプロイする

cdk deployでデプロイします。
ログは多すぎるため割愛しますが、デプロイが成功すると以下のようにAPIのリンクを教えてくれます。

 

そのリンクを開くと以下のように欲しい情報が取得できていることが確認できました。

 

CloudFormationスタックが更新されている確認する

メタデータだけでなく、AWS LambdaとAmazon API Gatewayも作成されていました。

 

まとめ

インフラ管理の方法は他にもありますが、今回はAWS CDKを選択してみました。
そもそも他の方法について知識が無いということや、AWS CDKを使えるようになったらAWS Amplify Hostingも抵抗なく使えそうであることが理由です。
リソースへのタグ付けなどのカスタマイズは次回以降でチャレンジしたいと思います。

また、初心者視点ですが、実際に触って感じたAWS CDKのメリット以下です。

  • 変更前後の差分が分かって良い(cdk diff)
  • CloudFormationテンプレートの生成も出来る(cdk synth)
  • デプロイ後にAPIのリンクを教えてくれる
  • 環境破壊が楽(cdk deploy)

いよいよ次回で完結です。

タイトルとURLをコピーしました