FastAPIをFargateで使ってみる

2022.11.29

はじめに

データアナリティクス事業本部のkobayashiです。

コンテナ環境でFastAPIを使ったプロダクト開発を行っている際にデプロイ先を考えた場合コンテナなのでローカル含め様々な環境にデプロイすることができますが今回はFargateにデプロイするまでをまとめます。

FastAPIとは

PythonにはDjangoやFlaskをはじめ様々なwebフレームワークがあります。その中でもFastAPIはAPIサーバーに構築に優れたパフォーマンスを発揮します。特に気に入っている点として「軽量」「コードの記述量の削減」「Pydanticを使った型安全」「APIドキュメントの自動生成」といった点が非常に有用です。

AWSにてサーバーレスでAPIを作るならAPI Gateway + Lambdaの構成がベストだと思いますが、コンテナベースでAPIを作成するならFastAPIは一番はじめの選択肢になると思います。

FastAPI

FastAPIをFargateにデプロイする

では実際にFastAPIで作成したAPIをFargateにデプロイしてみます。FastAPIの公式ドキュメントに「Dockerを使用したデプロイ - FastAPI 」があるのでコンテナイメージ自体は同じものを使用したいと思いますのでこのドキュメント内の「Dockerコンテナを起動」まで確認できた状態を前提に進めます。従って今回行う作業内容は以下となります。

  1. FastAPIのDockerイメージをビルドする(Dockerを使用したデプロイ Dockerイメージをビルド済みの前提)
  2. 作成したイメージをECRにプッシュ
  3. ECSサービス、タスク定義を作成してコンテナを起動する

なおAWSリソースはTerraformで作成したのでそのコードを記載していきます。

FastAPIのイメージをECRにプッシュする

はじめにECRを作成します。特別な設定を行わずプライベートリポジトリを作成します。

resource "aws_ecr_repository" "main" {
  name = "fastapi"
  image_tag_mutability = "MUTABLE"
}

次に作成したECRにイメージをプッシュします。

$ aws --region ap-northeast-1 ecr get-login-password | docker login --username AWS --password-stdin 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com
$ docker tag myimage:latest 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi:latest
$ docker push 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi:latest

これでECRにfastapiのテストイメージがプッシュされたので次にfargateのリソースを作成します。

ECSサービス、タスク定義を作成してFastAPIコンテナを起動する

次にECRにプッシュしたイメージを使ってFargateでタスク実行を行います。こちらも以下のterraformのコードを使ってデプロイします。

# SecurityGroup
resource "aws_security_group" "main" {
  name = "fastapi"
  vpc_id = "{VPCのID}

  # セキュリティグループ内のリソースからインターネットへのアクセス許可設定
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

}
# SecurityGroup Rule
resource "aws_security_group_rule" "main" {
  security_group_id = aws_security_group.main.id
  type = "ingress"
  from_port   = 80
  to_port     = 80
  protocol    = "tcp"
  cidr_blocks = {アクセス元のIP}
}

# タスク実行role用のassume_role
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}
# タスク実行role
resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "fastapi-ecsTaskExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
  role       = aws_iam_role.ecs_task_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}


# ECS Cluster
resource "aws_ecs_cluster" "main" {
  name = "fastapi"
}

# ECS Service
resource "aws_ecs_service" "back" {
  name = "fastapi"
  cluster = aws_ecs_cluster.main.name
  launch_type = "FARGATE"
  desired_count = "1"
  task_definition = aws_ecs_task_definition.back.arn
  network_configuration {
    subnets = [
      "{サブネットのID}",
    ]
    security_groups  = [aws_security_group.main.id]
    assign_public_ip = true
  }
}

# Task Definition
resource "aws_ecs_task_definition" "back" {
  family = "fastapi"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  cpu    = "256"
  memory = "512"
  network_mode = "awsvpc"
  # 起動するコンテナの定義
  container_definitions = <<EOL
[
  {
    "name": "fastapi",
    "image": "1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/fastapi",
    "portMappings": [
      {
        "hostPort": 80,
        "containerPort": 80
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-group": "/fastapi/ecs",
        "awslogs-stream-prefix": "back"
      }
    },
    "environment": [
      {
        "name": "WORKERS_PER_CORE",
        "value": "3"
      },
      {
        "name": "WEB_CONCURRENCY",
        "value": "2"
      }
    ]
  }
]
EOL
}

# CloudWatch Logs
resource "aws_cloudwatch_log_group" "cluster_log_group" {
  name              = "/fastapi/ecs"
  retention_in_days = 90
}

Dockerを使用したデプロイ - FastAPI 」で作成したイメージは80番ポートで起動するので特に凝った設定は行わずとも簡単にタクスを実行できます。

タスク定義の環境変数に指定した値はビルド時のベースイメージ「tiangolo/uvicorn-gunicorn-fastapi 」で使用できる環境変数 を設定しています。これは最小構成のFargateでも最低限2スレッドで動かしたかったためです。

Fargateタスクが起動したらタスクのパブリックIPを確認してリクエストを送ると期待したレスポンスが返ってくることがわかります。

$ curl "http://{パブリックIPアドレス}/items/5?q=somequery"
{"item_id":5,"q":"somequery"}

またAPIドキュメントも問題なく表示されます。

http://{パブリックIPアドレス}/docs

http://{パブリックIPアドレス}/redoc

以上でFastAPIのイメージをFargateで動かす事ができました。

まとめ

FirstAPIで作成したビルドイメージをFargateで動かしてみました。特に変わった設定を行わなくても簡単にタスクを実行できました。

FirstAPIは使い勝手が良いので、コンテナベースのAPIサーバーをFastAPIで作成する際の参考にしてみてください。

最後まで読んで頂いてありがとうございました。