actix-web(Rust)とReact(TypeScript)をFargateにGitHubActionsで自動デブロイのTips


# はじめに

Iacはterraformを使います。
ci/cdはGitHubActionsを使いecrとfargateにデブロイします。

今回はその時につまずき解決した方法を書きます。


レポジトリーはこちらになります。

https://github.com/sugiyama404/fargate_rust

アプリケーションパートの解説は以下です。

https://qiita.com/sugiyama404/items/613a2aa04b55b2dd5b46

結果です。

![aws_fargate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/f8bfabce-7b43-30ab-a283-17f9e12cd514.gif)

![aws_structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/e226f57f-11fb-5788-7c93-1aba8ea55607.png)

以下、ファイル構成になります。

```text:ファイル構成

├── apserver
├── dbserver
│   ├── Dockerfile
│   ├── conf
│   │   └── my.cnf
│   └── init
│       └── init.sql
├── docker-compose.yml
├── loadbalancer
│   ├── Dockerfile
│   └── rust.conf
├── terraform
│   ├── main.tf
│   ├── modules
│   │   ├── bash
│   │   │   └── null_resource.tf
│   │   ├── cloudwatch
│   │   │   └── aws_cloudwatch_log_group.tf
│   │   ├── ecr
│   │   │   ├── ecr.tf
│   │   │   └── outputs.tf
│   │   ├── ecs
│   │   │   ├── aws_lb_listener_rule.tf
│   │   │   ├── ecs.tf
│   │   │   ├── ecs_service.tf
│   │   │   └── ecs_task_definition.tf
│   │   ├── elb
│   │   │   ├── aws_lb.tf
│   │   │   ├── aws_lb_listener.tf
│   │   │   ├── aws_lb_target_group.tf
│   │   │   └── outputs.tf
│   │   ├── iam
│   │   │   ├── aws_iam_policy.tf
│   │   │   ├── aws_iam_policy_attachment.tf
│   │   │   ├── aws_iam_role.tf
│   │   │   └── outputs.tf
│   │   ├── network
│   │   │   ├── aws_vpc.tf
│   │   │   ├── igw.tf
│   │   │   ├── outputs.tf
│   │   │   ├── route_table.tf
│   │   │   ├── security_group.tf
│   │   │   ├── security_group_rule.tf
│   │   │   └── subnet.tf
│   │   ├── rds
│   │   │   ├── aws_db_instance.tf
│   │   │   ├── aws_db_option_group.tf
│   │   │   ├── db_parameter_group.tf
│   │   │   └── outputs.tf
│   │   └── s3
│   │       ├── aws_s3_bucket.tf
│   │       ├── aws_s3_bucket_acl.tf
│   │       ├── aws_s3_bucket_policy.tf
│   │       ├── aws_s3_bucket_versioning.tf
│   │       └── outputs.tf
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   └── variables.tf
└── webserver
    ├── Dockerfile
    └── rust-front
        ├── index.html
        ├── package-lock.json
        ├── package.json
        ├── public
        │   └── vite.svg
        ├── src
        │   ├── App.css
        │   ├── App.tsx
        │   ├── assets
        │   │   └── react.svg
        │   ├── index.css
        │   ├── main.tsx
        │   └── vite-env.d.ts
        ├── tsconfig.json
        ├── tsconfig.node.json
        └── vite.config.ts
```

# Terraform編

## dbのエンドポイント

rdsのエンポイントの表示は
```console
{db_name}.xxxxx.{region_name}.rds.amazonaws.com
```
なのですが、
```console
{db_name}.xxxxx.{region_name}.rds.amazonaws.com:3306
```
で出力されます。
```console
split(":","{db_name}.xxxxx.{region_name}.rds.amazonaws.com:3306")[0]
```
加工して、アプリに渡します。


## elbのログをs3に出力

以下のリンク先でロードバランサーのリージョンに対応するAWSアカウントIDを確認できます。

https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy

```terraform:terraform/modules/s3/aws_s3_bucket_policy.tf
locals {
  alb_root_account_id = "582318560864"
  # ロードバランサーのリージョンに対応するAWSアカウントID
  # https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy
}

resource "aws_s3_bucket_policy" "allow_access" {
  bucket = aws_s3_bucket.alb_access_log.id
  policy = data.aws_iam_policy_document.allow_access.json
}

data "aws_iam_policy_document" "allow_access" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["${local.alb_root_account_id}"]
    }
    actions = ["*"]
    resources = [
      aws_s3_bucket.alb_access_log.arn,
      "${aws_s3_bucket.alb_access_log.arn}/*",
    ]
  }
}
```

## ecs execでdb接続
ecs execとは、docker exec,docker compose execのecs版です。

enable_execute_command = true

にすることで、ecs execを許可します。

```terraform:terraform/modules/ecs/ecs_service.tf
# EcsService for Fargate api
resource "aws_ecs_service" "api-service" {
  name            = "${var.api_app_name}-service"
  cluster         = aws_ecs_cluster.cluster.id
  depends_on      = [aws_lb_listener_rule.api-ecs]
  task_definition = aws_ecs_task_definition.api-definition.arn
  desired_count   = 1
  # ecs exec
  enable_execute_command = true
}
```

aws_iam_policyを追加します。


```terraform:terraform/modules/iam/aws_iam_policy.tf
resource "aws_iam_policy" "ecs_task_role_policy" {
  name = "${var.app_name}_ecs_task_role_policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ssmmessages:CreateControlChannel",
          "ssmmessages:CreateDataChannel",
          "ssmmessages:OpenControlChannel",
          "ssmmessages:OpenDataChannel"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}
```

```terraform:terraform/modules/iam/aws_iam_policy_attachment.tf
resource "aws_iam_policy_attachment" "ecs_task_role_attach" {
  name       = "${var.app_name}_ecs_task_role_attach"
  roles      = ["${aws_iam_role.main_role.name}"]
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}
```

以下のコマンドでコンテナの内部に入れます。

```console
aws ecs execute-command --task=【タスクID】 --interactive \
--cluster=【クラスター名】 --container=【コンテナ名】 --command /bin/bash
```

参考記事です。

https://qiita.com/tonluqclml/items/ef20541dbefb46e6a193

# GitHubActionsでデブロイ

参考記事

https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-aws

![Screenshot 2023-04-07 at 14.43.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/cea5c5f4-d09c-61d3-6e81-81c0a0f7b1b3.png)

参考記事を参考にiamロールを作成、そのarnをコピーして、githubのsecretを登録します。
その後、github actionを実行します。

```yml:.github/workflows/docker-image.yml
name: "build & deploy"

on:
  push:
    branches:
      - main
    paths:
      - apserver/**
      - webserver/**
      - .github/workflows/**
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

defaults:
  run:
    shell: bash

env:
  AWS_REGION: ap-northeast-1
  ECS_CLUSTER: todolist-cluster
  API_ECR_REPOSITORY: apiserver
  API_ECS_SERVICE: apiserver-service
  API_CONTAINER_NAME: apiserver
  API_TASK_DEFINITION: apiserver-definition
  WEB_ECR_REPOSITORY: webserver
  WEB_ECS_SERVICE: webserver-service
  WEB_CONTAINER_NAME: webserver
  WEB_TASK_DEFINITION: webserver-definition
jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
          role-session-name: Github
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$API_ECR_REPOSITORY:$IMAGE_TAG ./apserver
          docker build -t $ECR_REGISTRY/$WEB_ECR_REPOSITORY:$IMAGE_TAG ./webserver
          docker push $ECR_REGISTRY/$API_ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$WEB_ECR_REPOSITORY:$IMAGE_TAG
          echo "api_image=$ECR_REGISTRY/$API_ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
          echo "web_image=$ECR_REGISTRY/$WEB_ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
      - name: Logout of Amazon ECR
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}

      - name: Download task definition from Amazon ECS
        run: |
          aws ecs describe-task-definition --task-definition $API_TASK_DEFINITION --query taskDefinition > api-task-definition.json
          aws ecs describe-task-definition --task-definition $WEB_TASK_DEFINITION --query taskDefinition > web-task-definition.json
          echo $(cat api-task-definition.json | jq 'del(
                  .taskDefinitionArn,
                  .requiresAttributes,
                  .compatibilities,
                  .revision,
                  .status,
                  .registeredAt,
                  .registeredBy
              )') > api-task-definition.json
          echo $(cat web-task-definition.json | jq 'del(
                  .taskDefinitionArn,
                  .requiresAttributes,
                  .compatibilities,
                  .revision,
                  .status,
                  .registeredAt,
                  .registeredBy
              )') > web-task-definition.json

      - name: Fill in the new image ID in the Amazon ECS api task definition
        id: api-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: api-task-definition.json
          container-name: ${{ env.API_CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.api_image }}

      - name: Fill in the new image ID in the Amazon ECS web task definition
        id: web-task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: web-task-definition.json
          container-name: ${{ env.WEB_CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.web_image }}

      - name: Deploy Amazon ECS api task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.api-task-def.outputs.task-definition }}
          service: ${{ env.API_ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

      - name: Deploy Amazon ECS web task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.web-task-def.outputs.task-definition }}
          service: ${{ env.WEB_ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
```