FastAPIのLambdaからAWS外のサービスでキャッシュとDB構築！ + Github Actions

# 概要
FastAPI、Pythonに詳しい方はご存じの方も多いかもしれません。
AWSのLambda関数と組み合わせるとどんな感じになるんだろう？と思って触り始めました。
色々学べたので、この機会に記事にしました！

**こういったアーキテクチャが実現できるかな**？という部分に焦点をおいています。また、可能な範囲でコストも抑える方針で進めました！

### 主な目次
- パート1：アプリケーション作成
    - インフラ構築（CDK Typescript）
    - Lambda関数（FastAPIのアプリケーション）の作成
    - DBやcacheの構築の前に
    - supabaseによるDB構築
    - upstashによるcache構築
- パート2：GitHub Actionsと各ツールの利用
    - GitHub Actionsのworkflowは作ったが・・・
    - Ruffの利用（Linterツール）
    - actの利用（ローカルでGitHub Actions）
    - stepciの利用（APIのテスト）
- ふりかえりとgithubリポジトリの情報
- この後やりたいこと

### 実行環境
- OS : windows 11
- Python：3.12
- Nodejs : 20.9.0
- aws cdk : 2.103.1

## インフラ構築（CDK Typescript）
以下画像の`AWS Cloud`で囲ったリソースをCDK Typescriptで実装しました。Pythonで統一も考えましたが、慣れている方で進めました！！
AWSのDB関連のサービスが半透明なのは、使わずに構築できたからです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/db0b430a-7224-8796-3a36-0c63a4491322.png)

## Lambda関数（FastAPIのアプリケーション）の作成
FastAPIでアプリケーション作成します。Dockerイメージで動かすことにしました。理由は以下の2つです。
- Dockerイメージを使ったLambda関数の作成経験を積みたい
- zip形式の場合に、ファイルサイズの上限に引っかかるかもしれない

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution

#### アプリケーションで実装した内容
アプリケーション自体は小規模です。アプリケーションで使用した各ライブラリのバージョンは`requirements.txt`に記載の通りです。（記事の最後でリポジトリ情報を載せています。）

- プロジェクト共通の設定
    - ミドルウェアの設定
        - エラー発生時のログ出力
        - loguruというライブラリを用いて、各エンドポイント実行時のログ出力
- エンドポイント
    - /dummy
        - シンプルなJsonを返すエンドポイント
    - /movie
        - **今回のメイン。supabaseからのデータ取得およびupstashのキャッシュ利用**    
    - /docs
        - openAPIのドキュメント用のデフォルトPath（自分で作成していない）

#### Mangum
Lambda × FastAPIの組み合わせを通じて初めて目にしました！

https://zenn.dev/pilefort/articles/a286144465cb6b

> FastAPIはAPIのエンドポイントを複数公開できますが、AWS Lambdaは1つだけしか公開できません。そこで、mangumというライブラリを使うことで、複数のエンドポイントを1つにまとめることができます。

パフォーマンスの面でもメリットがあるそうです。

https://techblog.raksul.com/entry/2023/06/30/081048#Mangum

> Magnumを利用する際は、uvicornといったASGIのweb serverを起動するわけではなく、 FastAPIとLambda Proxy Integrationとの間に立ちイベントを変換します。
そのためオーバヘッドもほとんどゼロであり、Lambdaが適切にスケーリングすればパフォーマンスの心配はなく積極的に使っていけそうです。

[AWS公式のハンズオン](https://aws.amazon.com/jp/builders-flash/202304/api-development-sam-fastapi-mangum/?awsf.filter-name=*all)でも使っていました！

#### loguruを用いたログ出力
pythonのデフォルトでloggingというライブラリがあります。こちらを否定する訳ではなく、簡単に設定できることを求め、今回はloguruにしました。[日本語による入門記事](https://scrapbox.io/PythonOsaka/loguru%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%AD%E3%82%AE%E3%83%B3%E3%82%B0%E3%82%92%E3%81%97%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86)もあります。

https://github.com/Delgan/loguru

実装は[こちらの記事](https://copyprogramming.com/howto/fast-api-logging-with-loguru-code-example)を参考にしました。
`loguru_route_logging.py`の内容を`main.py`(アプリケーションサーバ起動時のコマンドで指定するファイル)でmiddlewareに設定します。

<details><summary>loguru_route_logging.py</summary>

```python:loguru_route_logging.py
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
import time
import datetime
from loguru import logger
import sys
logger.remove()
logger.add(sys.stdout, colorize=True, format="{time:HH:mm:ss} | {level} | {message}")

class LoguruRouteLogging(BaseHTTPMiddleware):
    """各ルートの実行内容をログ出力
    API内で各エンドポイントにアクセスした際に、アクセス内容をログに残す
    Args:
        BaseHTTPMiddleware : リクエスト/レスポンスインタフェースに対するASGIミドルウェアを記述するための抽象クラス.
    """
    
    async def dispatch(self,request: Request, call_next) -> Response:
        before = time.time()         
        time_local = datetime.datetime.fromtimestamp(
            before, tz=datetime.timezone(datetime.timedelta(hours=9))
        )  
        logger.debug(time_local.strftime("%Y/%m/%d %H:%M:%S%Z"))  
        logger.debug(f"{request.method} {request.url}")
        logger.debug("Headers:")
        for name, value in request.headers.items():
            logger.debug(f"\t{name}: {value}")
        if await request.body():
                body = (await request.body()).decode("utf-8")  
        response = await call_next(request)
        return response
```
</details>


<details><summary>main.py（簡略化）</summary>

```python:main.py
from fastapi import FastAPI
from src.core.logger.loguru_route_logging import LoguruRouteLogging
from mangum import Mangum
# ・・・・
app = FastAPI()
# ミドルウェアに設定
app.add_middleware(LoguruRouteLogging)

def handler(event: EventAny, context: EventAny):
    asgi_handler = Mangum(app)
    return asgi_handler(event, context)
```
</details>

#### Secrets Managerとの連携
AWSのLambdaで環境変数を設定できますが、**DBやcacheサービスのパスワードは大切に扱いたい**ので、利用します。。以下の記事を参考に進めました。

- [料金をはじめ、基本的な内容を理解](https://zenn.dev/fdnsy/articles/133db8440d052f)
- [Lambda関数からSecretManagerに設定した値を取得](https://dev.classmethod.jp/articles/secrets_manager_tips_get_api_key/)

<details><summary>値を取得するためのコード</summary>

どのキーの値を取得するのか、引数`name`で受け取ります。
```python:read_secret_manager.py
import base64

import boto3
from botocore.exceptions import ClientError

def read_value(name: str) -> str:
    """Get secret value From SecretManager."""
    secret_name = name
    region_name = "ap-northeast-1"

    session = boto3.session.Session()
    client = session.client(
        service_name="secretsmanager",
        region_name=region_name,
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name,
        )
        if "SecretString" in get_secret_value_response:
            secret_data = get_secret_value_response["SecretString"]
            return secret_data
        else:
            decoded_binary_secret = base64.b64decode(
                get_secret_value_response["SecretBinary"]
            ) 
    except ClientError as e:
        raise e
```

</details>

#### Lambda関数に権限付与
Secrets Managerに設定した値がLamabda関数上で読み取れないと、データ取得処理が実行できません。IAMポリシーを追加します。

##### デフォルトの権限が消えるので、注意!
本題ではないので、簡単に書きます。
CDKでLambda関数のみ定義した場合、デフォルトでcloudwatchに関してロググループ作成・書き込みの権限が付与されます。
ですが、**`addToRolePolicy`という関数を使って、新たにLambda関数に対してポリシーを自分で設定した場合は、デフォルトの権限が消えてしまいます。**

<details><summary>Lambda関数が関係するコード</summary>

```typescript
// アプリケーション用のLambda関数
const fastApiappLambda = new lambda.Function(this, "fastapi-sample-container-lambda", {
  code: lambda.Code.fromEcrImage(appEcrRepository, {
    cmd: ["src.main.handler"],
    tag: "latest",
  }),
  functionName: "fastapi-sample-container-lambda",
  runtime: lambda.Runtime.FROM_IMAGE,
  handler: lambda.Handler.FROM_IMAGE,
  timeout: cdk.Duration.seconds(30),
  // layers: [powerToolsLayer],
  environment: {
    ENV: 'dev',
    TITLE: 'FastAPI-MyApp',
    UPSTASH_REDIS_REST_HOST: process.env.UPSTASH_REDIS_REST_HOST!,
    UPSTASH_REDIS_REST_PORT: process.env.UPSTASH_REDIS_REST_PORT!,
    SUPABASE_URL: process.env.SUPABASE_URL!,
  },
  role: fastApiLambdaRole,
});

// シークレットマネージャに関するポリシー
fastApiappLambda.addToRolePolicy(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ['secretsmanager:GetSecretValue'],
  resources: [
    secretUpstashPassword.secretArn,
    secretSupabaseKey.secretArn
  ],
}));

// cloudwatchに関するポリシー
fastApiappLambda.addToRolePolicy(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'],
  resources: ['arn:aws:logs:*:*:*'],
}));

```
</details>

#### API Gatewayとの連携
機密情報は扱ってないですが、皆が自由にアクセスできるのも、、、と思ったのでAPIキーによる認証付きのAPI Gatewayです。プロキシリソースについては、[こちらの記事](https://qiita.com/Shinkijigyo_no_Hitsuji/items/cedd1825e5437663d3ce#api-gateway%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A)を参考にしました。

<details><summary>API Gateway作成に関係するコード</summary>

```typescript
    const nameRestApi = "Rest API with Lambda";
    const restApi = new apigateway.RestApi(this, nameRestApi, {
      restApiName: `FastAPIGateway`,
      endpointTypes: [apigateway.EndpointType.REGIONAL],
      deployOptions: {
        stageName: 'dev',
      },
    });

    restApi.root.addProxy({
      defaultIntegration: new apigateway.LambdaIntegration(fastApiappLambda),
      anyMethod: true,
      defaultMethodOptions: { apiKeyRequired: true },
    });

    // api key:valueをシークレットにする方法
    const apiKey = restApi.addApiKey("fastapiAppApiKey", {
      apiKeyName: `fastapiApp-apiKey`,
      // value: ""
    }); // APIキーの値は未指定で自動作成

    // 使用量プラン
    const usagePlan = restApi.addUsagePlan("sampleApiUsagePlan", {
      name: 'MyUsagePlan',
      description: 'My API Usage Plan',
      throttle: {
        rateLimit: 50, // スロットリングのレート制限（リクエスト数）
        burstLimit: 100, // スロットリングのバースト制限（一度に許可される最大リクエスト数）
      },
      quota: {
        limit: 1000, // クォータの制限（リクエスト数）
        period: apigateway.Period.WEEK, // クォータの期間（MONTH, WEEK, DAY）
      },
    });
    usagePlan.addApiKey(apiKey);
    usagePlan.addApiStage({
      stage: restApi.deploymentStage
    });
```
</details>
このコードをデプロイすると、以下画像のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/f075f01e-9dbc-2ba3-47cc-52dc61791cb8.png)

FastAPI側のアプリケーションでもURLの設定には手を加えていますが、この状態になれば、
URL：`https://XXXXXX.execute-api.ap-northeast-1.amazonaws.com/dev/dummy`とAPIキーでGETリクエストを送ると、FastAPI上のエンドポイント、`/dummy`へリクエストが送信できるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/eab5d307-a464-ebe8-9894-8af17840c029.png)

---
## DBやcacheの構築の前に・・・
この後データベースとキャッシュを用意しますが、RDSのランニングコストは馬鹿にできないです。。。
私は今年に入ってから**supabase**を使用しています。（詳細は後ほど）
また、キャッシュは最近知った**upstash**という外部サービスを使ってみます。（詳細は後ほど）

:::note warn
接続情報の扱い
DBサービスやcacheサービスの接続情報は、github上にPushすることがないようご注意ください。
:::

## supabaseによるDB構築
「Firebaseにとって代わるオープンソース」という表現をされています。
今回はデータベースですが、StorageやEdge Function（自作APIの構築）もあります。
直感的な実装でデータ操作が行えて、javascript・Pythonなど、複数言語対応しています。DBとしてはpostgresです。

https://supabase.com/

#### supabaseのテーブルからデータ取得
早速実装しますが、以下の内容を真似るだけです！
- [インスタンス定義](https://supabase.com/docs/reference/python/initializing)
- [データ取得](https://supabase.com/docs/reference/python/select)

インスタンス定義
read_value関数は、Secret Managerのパートで紹介したコードです。
```python:supabase.py
import os
from supabase import Client, create_client
from src.api.dependencies.read_secret_manager import read_value

def get_supabase_key() -> str:
    """Get supabase key From SecretManager."""
    # Secret managerから値取得
    return read_value(name="supabase-key")

def supabase()-> Client:
    """Define supabase client."""
    url: str = os.environ.get("SUPABASE_URL")
    key: str = get_supabase_key()     
    return create_client(url, key)
```
データ取得処理
```python:endpoint.py
from src.api.dependencies.supabase import supabase
supabase_client.table("movies").select("*").execute()
```

#### alembicによるDBマイグレーション
先に取得処理を書きましたが、テーブル定義と反映はalembicを使います。
SQLAlchemyとセットで用いるDBマイグレーションツールです。
以下の2つの記事を参考にしました。
- [基本的な操作方法](https://zenn.dev/shimakaze_soft/articles/4c0784d9a87751)
- [supabaseとの連携](https://dev.to/j0/setting-up-fastapi-with-supabasedb-2jm0)

ざっくりですが、コマンドはinit、revison、upgradeの順に実行します。
revisionコマンド実行時に生成されたファイルに、DBに対する更新内容を記載します。

<details><summary>マイグレーションに関する実装</summary>

```python:aabc35790c50_generate_initial_migration.py(更新箇所のみ)
def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('movies',
    sa.Column('id', sa.BigInteger(), nullable=False),
    sa.Column('name', sa.Text(), nullable=True),
    sa.Column('description', sa.Text(), nullable=True,unique=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index(op.f('ix_name'), 'movies', ['name'], unique=False)
    op.create_index(op.f('ix_id'), 'movies', ['id'], unique=True)
    # ### end Alembic commands ###
```
</details>

`alembic.ini`というファイルで1つ問題があります。
DBのURLを記載する箇所がありますが。特に考慮しなければ、DBのパスワードを含んだ情報がハードコーディングされてしまいます。。(`.gitgnore`に含まれないファイル)
環境変数から値を読み込む方法は、以下を参考にしました。

https://github.com/sqlalchemy/alembic/discussions/1149

<details><summary>更新した2ファイルとその説明</summary>
環境変数：`SQLALCHEMY_DATABASE_URL`の値を、`SQLALCHEMY_DATABASE_URL`というキーで利用する処理を追加しています。これを`alembic.ini`で読み込む形です。

```python:env.py（一部抜粋）
import os
from dotenv import load_dotenv

load_dotenv()

def run_migrations_online() -> None:
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    config.set_section_option("alembic", "SQLALCHEMY_DATABASE_URL", os.environ.get("SQLALCHEMY_DATABASE_URL"))   
```

```ini:alembic.ini（一部抜粋）
[alembic]
sqlalchemy.url = %(SQLALCHEMY_DATABASE_URL)s
```
</details>

作成したmoviesテーブルにデータを10件登録しました。
<details><summary>登録したデータ</summary>

app側のgithubリポジトリ、`db_ddl_data`というフォルダに登録したデータのcsvファイルを配置してあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/cef3ba96-fbd0-f9ed-4b4e-0c0d1a44c2c3.png)
</details>

## upstashによるcache構築
自分できちんと実装する機会がなかったので、やってみました。以下のリンクの内容でサクッと構築できます。

https://upstash.com/docs/redis/overall/getstarted

Lambda関数のQuickstartもありました。

https://upstash.com/docs/redis/quickstarts/awslambda

インスタンス定義
```python:supabase.py
import redis
from redis import Redis

from src.api.dependencies.read_secret_manager import read_value
from src.core.config import settings

def get_redis_password() -> str:
    """Get Upstash Password From SecretManager."""
    return read_value(name="upstash-redis-rest-password")

def cache() -> Redis:
    """Definition redis instance."""

    return redis.Redis(
        host= settings.upstash_redis_rest_host,
        port= settings.upstash_redis_rest_port,
        password=get_redis_password(),
        ssl=True,
    )
```
データ取得処理（先ほどのsupabaseと同じ箇所）
- upstash上に、キー：`movies-python`があれば、キャッシュからデータを取得
- upstash上に、キー：`movies-python`が無ければ、supabaseのテーブルからデータを取得後、pickle.dumpsを用いて、データをシリアライズして保存（※）
- キー：`movies-python`は、30秒経過で破棄
```python:endpoint.py
@router.get("/", response_model=list[MovieResponse])
async def index(
    redis_client: cache = Depends(cache), supabase_client: supabase = Depends(supabase)
) -> list[MovieResponse]:
    """Endpoint about use cache."""

    try:
        if (cached_data := redis_client.get("movies-python")) is not None:
            logger.info("Use cache")
            return pickle.loads(cached_data)
        logger.info("Get Data From DB")
        response = supabase_client.table("movies").select("*").execute()
        movies = response.data
        redis_client.set("movies-python", pickle.dumps(movies))
        redis_client.expire("movies-python", 30)
        return movies
    except Exception as e:
        traceback.print_exc()
        raise APIException(ErrorMessage.FAILED_FETCH_DATA_FROM_MOVIE_TABLE)
```
※データをシリアライズして保存、参考記事です。

https://medium.com/@arturocuicas/fastapi-and-redis-cache-a31ca832853e

### キャッシュ導入してレスポンスまでの時間は短くできた？
短くできました。10件のデータでこれだけ差が発生しているので、もっとデータ量が多いとき、キャッシュのありがたさを実感できるな、と思いました。
（何度か実行してみて、おおよそ4-5倍短い時間でレスポンスが返ってきました。）

**DBから取得した場合**
取得前にupstashのCLIでキー：`movies-python`がないことを確認します。
<details><summary>確認結果</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/a0b9a19c-b2ad-b7b2-d2e2-62a7072bd1ec.png)
</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/db0e98b1-06ee-fe24-c5d2-7776e10d5a83.png)

**cacheから取得した場合**
取得前にupstashのCLIでキー：`movies-python`が存在することを確認します。
<details><summary>確認結果</summary>

文字化けしてますが、データはありそうです。。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/74082fd8-961a-312c-a4c0-313f0034345c.png)

</details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e2e1ef31-bb86-0eda-57cb-c467f0ed0b78.png)

## GitHub Actionsのworkflowは作ったが・・・
上記アプリケーションについて、DockerイメージのBuild・Pushし、Lambda関数を更新するworkflowを作成しましたが、何度か実装経験があり、大した苦労なく完了しました。。。
**どうせなら、もうちょっと実務に近い形にしたいと思い**、以降の内容に取り組みました。

## Ruffの利用（Linterツール）
Rust製のLinterツールです。使ったことがなかったので、この機会に触ってみました。動作を軽く確認して、Github Actionsのworkflowに組み込みます。

https://github.com/astral-sh/ruff
> An extremely fast Python linter and code formatter, written in Rust.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/d41c3b3c-1e93-5e85-128d-ea3b1e6af7eb.png)
確かに早そうです。

また、[ルール一覧のページ](https://docs.astral.sh/ruff/rules/)に記載の通り、`Ruff supports over 700 lint rules,`、だそうです！

### 適用ルールを決めるために、設定ファイルを用意しよう
プロジェクトのルートディレクトリに、`pyproject.toml`を用意しました。ファイル名は制限があるので、詳しくは[こちら](https://docs.astral.sh/ruff/tutorial/#__tabbed_1_1)、ご確認ください。

```toml:pyproject.toml
[tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["A", "B", "C", "D", "E", "F", "W", "ANN"]
ignore = [
    "B008", # do not perform function calls in argument defaults
    "C901", # too complex
    "B904", # raise from err
]
fixable = ["I001", "D201"]
target-version = "py312"
```
<details><summary>select , ignore , fixableの説明はこちら</summary>

- **select**：括弧内の文字列を設定すると、検知対象です。(画像はdocsより)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/618c0880-cce1-1e7b-d19b-d0ee0af9cc96.png)
- **ignore**：無視するルールの番号を設定します。
- **fixable**:：`--fix`オプションを付けて実行した際に、コード修正を行うルールを設定します。以下画像であれば、2つのルール違反のコードが修正されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b57ea94f-62e3-44c3-5fb8-f0269d4a2acb.png)

</details>

以下のコマンド実行で、現在の階層から再帰的に各ファイルをチェックします。
```shell
ruff check .
```
エラーがでてきました。修正or無視は、皆様の判断にお任せいたします！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/67075fdd-d983-14fb-561b-e4d220d10eaf.png)

## actの利用（ローカルでGithub Actions）
Pushして挙動を確認、エラーがあればそれらを解消・・・と進める場合、workflow runsの数が大きめになることもあります。（場合によってはコストに影響？）
**Push（やpull_requestなど）実行時の挙動を、ローカル確認できれば、開発も進めやすそうです。**
それを実現できるのが、actです。**Dockerさえあれば使用できます。**
[GitHub Actionsと"仲良くなる"ための練習方法](https://speakerdeck.com/tsubakimoto_s/github-actionsto-zhong-liang-kunaru-tamenolian-xi-fang-fa)というスライドにありました。

https://github.com/nektos/act

### 動かしてみよう
作成途中のyamlファイルですが、このファイルで検証してみます。

<details><summary>yamlファイル</summary>

```yaml
name:  FastAPI App CI/CD Pipeline
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      # Ruff Linter Test
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v2
      - uses: chartboost/ruff-action@v1
        with:
          src: ./src
          version: 0.1.6
      # Run FastAPI App
      - uses: actions/checkout@v3
      - name: Install Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
```
</details>

AWSサービスが関わるworkflowも確認する場合は、認証情報を含めた[変数の設定など](https://nektosact.com/beginner/index.html#vars)、もう少し準備が必要になりそうです。

dockerの起動後、**l**オプション付きで実行すると、workflowを検知してくれます。

```shell
sudo service docker status
* Docker is running
act -l
Stage  Job ID                  Job name                Workflow name               Workflow file  Events
0      Explore-GitHub-Actions  Explore-GitHub-Actions  FastAPI App CI/CD Pipeline  sample.yaml    push 
```
`act push`を実行して、pushイベントの挙動を確認します。
2枚目の画像の上の方に、Ruffのエラーが表示され、`Job failed`となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/ecdd066a-2b9b-1119-f04c-37b7bfb4af32.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/22324d4d-6ff1-726c-e326-959e3448a523.png)
Ruffのルールを全て満たせたら、成功になる？ということで、`pyproject.toml`のignoreに、selectで設定したアルファベットを追加します。
```toml:pyproject.toml（一部）
select = ["A", "B", "C", "D", "E", "F", "W", "ANN"]
ignore = [
    "A",
    .....
    "ANN",
    "B008", # do not perform function calls in argument defaults
```
`Job successed`と出力されており、成功です！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/673a9770-19db-160a-04d4-ff0ec12795ea.png)

実際にGithub ActionsにPushしたときと同じ結果になるの？ということで実際にPushしました。actの実行時と同じ結果でした！（コード状況が少し異なるので、error数差分あり）
<details><summary>Github Actionsの実行結果</summary>

Job失敗時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e70e19e1-f36e-3ba9-4a2f-5ad555bd31d7.png)
Job成功時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/11bfe456-afba-f5b5-0047-2674d8ea2def.png)
</details>

## stepciの利用（テスト）
こちらも割と最近知った、文字通りCIツールです。[こちらの書籍](https://www.shoeisha.co.jp/book/detail/9784798179735)を読んでいた中で、dreddとschemathesisというAPIテストのライブラリを使用してました。他にあるかな～と思っていた所で見つけました。
こちらも挙動を確認してから、Github Actionsのworkflowに組み込みます。

https://github.com/stepci/stepci

シンプルなハンズオン

https://zenn.dev/kou_pg_0131/articles/stepci-introduction

#### FastAPIとのIntegration
以下コマンドを実行すると、コマンドを実行した階層にworkflow.yamlというファイルが生成されます。
FastAPIの各エンドポイントの戻り値の型ヒントを設定しておくと、より正確に仕様を表したファイルが生成されます。
```shell
stepci generate http://127.0.0.1:8000/openapi.json
```
<details><summary>作成したワークフロー</summary>

```yaml
version: "1.0"
name: FastAPI
config:
  http: "http://127.0.0.1:8001"
env:
  domainPort: "http://127.0.0.1:8001"
tests:
  default:
    name: Default
    steps:           
      - id: dummy__get
        name: Dummy
        http:
          url: ${{env.domainPort}}/dummy
          method: GET
          check:
            status: /^20/
            schema:
              $ref: "#/components/schemas/DummyResponse"
components:
  schemas:
    DummyResponse:
      properties:
        status:
          type: string
          title: Status
      type: object
      required:
        - status
      title: DummyResponse
      description: Response model to validate and return when performing dummy endpoint.
```
</details>

workflowの設定が完了したら、実行します。
`Workflow passed`となって成功となりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/ec14b9d2-13da-cec5-f657-2f8992492622.png)

まだ、シンプルなテストしか実装していないので、この後より本格的なテストを作成していきたいと思います。

## ふりかえりとgithubリポジトリの情報
FastAPIをLambda関数として作成して知見を増やすのが、当初中心となる予定でしたが、
それ以外の内容の収穫が多かった印象です。（笑）
以下の3つは今後も利用する機会を増やしていきたいと思いました。
- upstashによるキャッシュを利用したデータ取得
- actによるローカルGithub Actions実行
- stepciによるテスト実行

FastAPIについて、規模が大きいアプリケーションを作成していないので、題材が見つかったタイミングで取り組んでみたいと思います。

Lambda関数のイメージ更新について、workflow内ではコマンド1行で終わりましたが、もう少し踏み込んでいました。
[`lambroll`というライブラリ](https://github.com/fujiwara/lambroll#deploy-container-image)があります。少し触ってみて、Revisionは作成できましたが、Lambda関数が使用するイメージの更新ができず。
自分の操作・理解に問題があると思うので、別の機会に再確認したいと思います。

**今回の成果物について、以下リポジトリで確認いただけます。**
アプリケーション側

https://github.com/eno-conan/apigateway-lambda-upstash-supabase-app

インフラ側

https://github.com/eno-conan/apigateway-lambda-upstash-supabase-infra

## この後やりたいこと
現段階で、修正したい、新たに取り組みたいのは以下の通りです。
日々少しずつ進めていきたいと思います！

- CDKの実装修正（次のメジャーバージョンアップで廃止となる実装あり）
- actでAWSサービスを含めたworkflowの検証
- フォルダ構成の整理やloggingの深堀り
- lambrollを用いたイメージ更新の正しい手順の確認 
- openAIを利用したAPIの構築