GAミント至上主義

Web Monomaniacal Developer.

NuxtJSでaxiosのリクエストをキャンセルする

NuxtJSでSSR + SPAでコンテンツをaxiosで取得するよくあるページを作っていたら、リンクを連打するとこんな感じになった。
言葉で説明するの大変なのでブログ用にTwitterに動画あげた。


バージョンはここらへん。TypeScript、Composition APIを使ってます。

  "dependencies": {
    "@nuxtjs/axios": "^5.13.4",
    "@nuxtjs/composition-api": "^0.23.4",
    "@nuxtjs/proxy": "^2.1.0",
    "cookie-universal-nuxt": "^2.1.4",
    "core-js": "^3.12.1",
    "dayjs": "^1.10.4",
    "nuxt": "^2.15.6",
    "vue-slick-carousel": "^1.0.6",
    "vuejs-paginate": "^2.1.0"
  },

解決策としては、フォームみたいに、完了するまでクリックできなくするっていう方法もあるけど、ページ送りみたいな普通のリンクではやりたくない。

そこで、連打されたとき前回のをキャンセルする方法ないかと調べたところ、axiosにそういう機能がついてることを知った。

NuxtJSのaxiosモジュールを使っているけど使い方はaxiosと同じ
https://axios.nuxtjs.org/usage/#cancel-token


最初Promiseでできるかと思ったけど難しそうだった。


やってることはこの記事と同じだけど、NuxtJSだったり、Composition APIだったり、TypeScriptだったりするのでメモ。
siosio.hatenablog.com

開発中だけど必要そうなコードを抜粋、編集したやつ(動かない

import {
  defineComponent,
  useAsync,
  useContext,
  ssrRef,
  watch,
  ref
} from '@nuxtjs/composition-api'
import { JobListApiResponse, Job } from '../types'

export default defineComponent({
  validate({ query }) {
    if (!query.page) {
      return true
    } else if (typeof query.page === 'string') {
      return /^\d+$/.test(query.page)
    }
    return false
  },
  setup() {
    const { $axios, query } = useContext()
    const cancelToken = $axios.CancelToken
    const cancelSource = ref(cancelToken.source())
    const toPageNumber = (number: any): number => {
      // validateでチェックしてるので最低限
      if (typeof number === 'string') {
        return Number(number)
      }
      return 1
    }
    currentPage.value = toPageNumber(query.value.page)
    const fetchJobs = async () => {
      const page = toPageNumber(query.value.page)
      // 前回のをキャンセル
      cancelSource.value.cancel('連打かな?')
      // 新しくセット
      cancelSource.value = cancelToken.source()
      try {
        const response = await $axios.$get<JobListApiResponse>(
          '/api/path',
          {
            params: { page },
            cancelToken: cancelSource.value.token
          }
        )
        // response使ってデータをセットする処理
      } catch (error) {
        console.error(error)
      }
    }
    useAsync(fetchJobs)
    // queryに変更あったら再実行
    watch(query, fetchJobs)
    return {
      // 省略
    }
  }

cancelSourceはあとでファイルを分けたときようにrefを使ってます。

完了したやつをcancelしても特にエラーにはならないのでそのままキャンセルしてます。

あとリクエスト中のロード画面とかは必要そう。

こうすることで、最後のリクエストだけが残って思ってた挙動になりました

CircleCIでAWS ECSのScheduled Taskのタスク定義を最新のリビジョンに更新する

AWSのECSを使った環境構築、↓この記事からの続きで、
uyamazak.hatenablog.com
Laravelの

php artisan schedule:run

を実行するためにタスクのスケジューリングを使ってみることに。

f:id:uyamazak:20210416180458p:plain

新規作成はコンソール上でなんなくできたものの、CircleCIからの更新方法が見つかりませんでした。


コンテナのビルドからECRへのプッシュとか、ECSのサービスの更新などはCircleCIの公式Orbがあったけど、スケジュールに関するものは見当たらない。
CircleCI Developer Hub - circleci/aws-ecs


どうやらスケジュールタスクはECSの画面にあるもののAWSの裏側ではいろいろ組み合わせて構成されてるようで、ECSのコマンドでは更新できないもよう。
shirakiya.hatenablog.com
この記事だと手で叩くことはできても、タスク定義のリビジョンは毎回異なるため、そこは別に用意する必要があった。

公式ドキュメントには新規作成だけあった。
AWS CLI を使用したスケジュールされたタスクの作成 - Amazon Elastic Container Service

個人のOrbもあったけど、ソースをみるとPython使ってたり、ちょっと無理やりな感じなので勉強も兼ねてAWS CLIでやることに。
CircleCI Developer Hub - pbrisbin/aws-ecs-scheduled-tasks




実行タイミング的には上記Orbのaws-ecs/deploy-service-updateが終わり、タスク定義が更新された後。

手で更新してみる

まずはCIでいちいちやると大変なので、自分のPCから更新が成功するか試します。AWS CLIのインストールや設定は大前提。
上記の記事のようにaws events put-targetsを使います。
基本的に指定はArnなのでECSの画面とかIAMとかで調べながら埋めていきます。

helpを見ながら叩いて、エラーで出た不足した項目を追加していったところ、最低限はこんな感じ。

aws events put-targets --rule your-rule-id --targets="[{"Id":"ターゲットのID","Arn":"クラスターのArn","RoleArn":"ロールのARN","EcsParameters":{"TaskDefinitionArn":タスク定義ののArn"`}}]"

タスク定義のArnには最新でなくとも、変わっているのが分かるように、今のスケジュールタスクの指定とは違うものを指定すればよさそう。

こんな感じの結果が返ってきて、コンソールで見て変更されてればOK

{
    "FailedEntryCount": 0,
    "FailedEntries": []
}

最新のタスク定義1件のArnを取得する

上記のコマンドがうまくいくのがわかったら、TaskDefinitionArnの部分を動的に最新の1件を取得するようにします。

タスク定義はECSの範疇なのでaws ecs list-task-definitionsで取得し、--sortと--max-itemsを使って最新1件にしぼります

$ aws ecs list-task-definitions --family-prefix タスク定義の名前 --sort DESC --max-items 1                                
{
    "taskDefinitionArns": [
        "arn:aws:ecs:{リージョン}:{アカウントID}:task-definition/タスク定義の名前:8"
    ],
    "NextToken": "tekitooooooooooo=="
}

このままだとJSON的なものなので使いにくい・・・使えなくはないけどCIでjqとかインストールするのもださい。
ということでいろいろ調べているとAWS CLIには--queryという全体で使えるオプションがあることを知りました。

AWS CLI 出力をフィルタリングする - AWS Command Line Interface

ほしいのはtaskDefinitionArnsの1つ目なので--query "taskDefinitionArns[0]"をつけます。

$ aws ecs list-task-definitions --family-prefix タスク定義の名前 --sort DESC --max-items 1   --query "taskDefinitionArns[0]"
"arn:aws:ecs:{リージョン}:{アカウントID}:task-definition/タスク定義の名前:8"

これでタスク定義のArnだけが取得できました。

あとはシェル芸なんですが、私はあまりシェル力は高くないので一旦JSON的な部分を環境変数にして使うことにしました。

最終的にこんな感じの.circleci/config.yml。
とりあえず今回環境で変わりそうなところだけparameter化したけどもっときれいにできそう。

version: 2.1
orbs:
  aws-cli: circleci/aws-cli@2.0.0
commands:
  put_targets:
    parameters:
      target_id:
        type: string
      cluster:
        type: string
      family:
        type: string
      rule:
        type: string
    steps:
      - run:
          command: |
            TARGETS="[{\"Id\":\"<< parameters.target_id >>\",\"Arn\":\"arn:aws:ecs:リージョン:アカウントID:cluster/<< parameters.cluster >>\",\"RoleArn\":\"arn:aws:iam::アカウントID:role/ecsEventsRole\",\"EcsParameters\":{\"TaskDefinitionArn\":`aws ecs list-task-definitions --family-prefix << parameters.family >> --sort DESC --max-items 1 --query "taskDefinitionArns[0]"`}}]"
            aws events put-targets --rule << parameters.rule >> --targets=$TARGETS

jobs:
  update_scheduled_task:
    executor: aws-cli/default
    steps:
      - aws-cli/setup
      - put_targets:
          cluster: hogeeee
          family: hoggeeee
          target_id: hogeeee
          rule: hogeeee


workflows:
  hogeeee-workflow-name:
    jobs:
      - update_scheduled_task:
          requires:
            - deploy-service-update-queue
          filters:
            branches:
              only:
                - stage
                - push_dockerimage_to_ecr

これでサービスやタスク定義の更新後に、スケジュールしたタスクの定義も更新することができました。
ただ常に最新になってしまうので、もしかしたら問題が起きるときもあるかも?(ロールバックとか)

AWS初心者がElastic Beanstalk環境をFargate化したメモ

優先度が高い仕事が落ち着いてきたため、ずっとやりたかった本番環境コンテナ化に着手しました。

たぶんお休みで全部忘れるので社内に説明用も兼ねてメモ。

下記の記事のように現在のシニアジョブに入社してすぐ、開発環境はDocker化していました。

シニアジョブに入社して1ヶ月でやったこととこれから - GAミント至上主義

現在、本番環境はElastic Beanstalkで動いていますが、Dockerfileで管理できるため、いつかは揃えてコンテナ化したいなぁと思っていました。

AWS Fargateとは

AWS ECSからEC2の管理を(でき)なくしたサーバーレス的なもの・・・という雑な認識。詳細は公式。
AWS Fargate(サーバーやクラスターの管理が不要なコンテナの使用)| AWS

専用ページはあるものの、コンソールの中だと、ECSやEKSの起動タイプとして存在してるだけで意外と地味な存在でした。

なぜFargateを使うか

コンテナ化に際しては、タイトルのFargateの他、下記の選択肢がありました。

下記がFargateにした理由です。

AWSのいろいろなサービスを使っていたから(GCPを使わない理由)

シニアジョブでは、現在のシステムが動いた当初(2016年ごろ?)からAWS上で求人サイトや管理システムも動かしていました。
そのためRDS(MySQL)をはじめ、Elasticsearch、DynamoDB、SQSなども使用しています。
そのため、個人的にはGCPのGKEは使い慣れていたので使いたい気持ちはあるものの、わざわざGCPに移すという選択肢はありませんでした。

そんなに必要なコンテナ数が多くない(Kubernetesを使わない理由)

これまで触ったことはないものの、ネット上の記事とかの印象だと、AWSでそこまで大規模じゃないシステムだったらFargateかなぁというイメージでした。
シニアジョブのシステムで必要になりそうなコンテナは下記3つだけで、ほかはRDS, RedisなどはAWSのマネージドで済みます。

  • アプリケーション (Laravel)
  • ワーカー (php artisan queue:workerするやつ) (Laravel)
  • WEBサーバー(Nginx)

これだけだとKubernetesの機能はちょっとオーバーかなぁという印象。あとAWSKubernetesはGKEと比較すると高くつくというのもあります。
実際構築してみた感じ、ハードウェアリソースも柔軟で、Fargateで十分でした。

管理が楽そうだから(EC2タイプを使わない理由)

EC2タイプを使ったことはないので比較はできないのですが、EC2を意識せずに使えるというのは、エンジニア3人で何とかしている現状では、メンテナンスコストを削減できそうなので、大きな価値がありました。
サーバーレスとあるので、GCPでいうCloud Runみたいな感じかなぁと思ってたら、どちらかというとKubernetesで最近出たAutopilot的なものでした。

Fargate化するために必要な登場人物

GCPではいろいろ構築したことがあるもののAWSで複数サーバーのシステム構築は初めてでした。
そのためFargateを使用するにあたり、AWSの他のサービス、機能を知る必要がありました。

ECR (Elastic Container Registry)

まずはDockerで作ったコンテナをAWSに置いておく必要があります。その置き場所がECR。
Amazon ECR(Docker イメージの保存と取得)| AWS

最初1回だけ手作業でローカルからビルド&プッシュしましたが、面倒なのでCircle CIで自動化しました(後述)

ECS (Elastic Container Service)

これが今回使うメインのサービスになります。

Amazon ECS(Docker コンテナを実行および管理)| AWS
ECSの中に下記があります

ECS クラスタ

ECSの一番外側、EC2をひとまとめにしたもの。Kubernetesクラスターと同じ感じですね。
今回はFargateを使うので、ネットワーキングのみを使用します。
f:id:uyamazak:20210409171846p:plain

ECS サービス

後述するタスクを管理するもので、タスク定義や、起動数を指定し、その状態に変更、維持してくれます。
1サービスに対し、1タスク定義っぽい。
また後述するロードバランサーもサービスと紐づけることになります。
Amazon ECS サービス - Amazon ECS

作成後に変更できない設定が多々あり、5回ぐらいは作り直した記憶があります。

タスク定義

タスクの設計書のようなもので、ここに使用するコンテナ(複数可)や、コンテナごとの環境変数などの設定、必要なリソース(CPU、メモリ等)を指定します。
こちらも作成時に起動タイプにFargateを指定します(もう一つはEC2)。
更新の際は、上書きではなく、リビジョンを増やしていく形になります。
Amazon ECS タスク定義 - Amazon Elastic Container Service

コンテナ定義

具体的なコンテナ定義はタスク定義内にあります。開けるポートの指定もここ。
同じタスク定義内であれば、127.0.0.1で通信できるようなので、nginxからappへのプロキシ設定では127.0.0.1を使用しました。
KubernetesだとServiceを作る必要があったり面倒なので、今回はこれで十分だし、シンプルに感じます。
f:id:uyamazak:20210409184238p:plain

※あとでqueueは別サービスに分けました

サービスでのロードバランサー設定は、ここで設定したnginxコンテナのポート80を指定しました。

タスク

上記のタスクをサービスが起動したもの。プログラミングだとタスク定義がクラス、タスクがインスタンス、みたいなイメージ?
変更の際はタスク定義のリビジョンを指定して更新します。
スケールさせるのもこのタスク単位

頻繁に消したり、作ったりするものなので、タスクIDは

32034c0a56d84d5eb95908090cdec44c

みたいなランダムな16進文字列になります(後述のシェルで入るときに使う)。

サービスを更新すると新しいのができたり古いのが消えたりします。

ECS以外

ECSと関係するのが以下のサービス&機能になります。

VPC (Virtual Private Cloud)

Amazon VPC(仮想ネットワーク内での AWS リソースの起動)| AWS

クラスター使用時に選択、または作成します。仮想のネットワークで、セキュリティ設定などに影響します。
新規のときは一緒に作ればいいですが、既存のシステムがあるときは同じものを使用しないと、いろいろ繋げるのが面倒になるので、注意が必要でした。

ロードバランサー(Application Load Balancer)

コンソール上ではEC2の中にある機能です。
Elastic Load Balancing(複数のターゲットにわたる着信トラフィックの分配)| AWS

上記サービスをインターネットに公開する際に使用します。
いくつか種類がありますが、ECSではApplication Load Balancerを使用します。
詳細は省略しますが、SSL証明書はここで設定できるため、ECS側でのHTTPS処理は不要です。
設置するVPCはECSクラスターと同じにしておいた方がよさそうです(不可能ではないかも)。

ロードバランサーのリスナー

ロードバランサーがリクエストを受けるポートです。
今回は一般的なWEBアプリなので80と443を作り、下記ECSサービスと連携したターゲットグループを転送先にします。
たしか80はサービス作成時に作られましたが、HTTPSの443の方は手作業で追加する必要があった気がします。

Route 53

Amazon Route 53(スケーラブルなドメインネームシステム (DNS))| AWS
言わずとしれたDNSサービス。
上記ロードバランサーでは

foobar-4580172169.ap-northeast-1.elb.amazonaws.com

のような長めのURLが振られるため、独自ドメインを使用する際はRoute53で紐付けます。
Aレコードから選択形式で簡単に指定できます。

ターゲットグループ

ロードバランサーから今回のECSのサービスに繋げる転送先(実体はタスクのローカルIP)に使用されます。
ロードバランサーの新規作成時や、ECSでサービスを作る際に自動でできてしまうので、サービスを作り直したりすると、余計なものが残っていて邪魔になり消すのが面倒でした。
ECSのサービスは、タスクが更新され、ヘルスチェックが通ると、このターゲットグループのIPを自動で変更することで、ユーザー側へも変更されます。

セキュリティグループ

ECSのサービスが持ちます。
IPは変わってしまうので、これを使って、VPC内の通信許可などを行います。
現環境では既存のセキュリティグループに、サービスからRDSやRedisへの許可を追加しました。

Secrets Manager

コンテナで使うパスワード等の機密情報を保持します。
とりあえずLaravelで.envファイルで使用していた項目を移しました。

AWS Secrets Manager(シークレットのローテーション、管理、取得)| AWS

タスク定義で、コンテナごとに設定し、環境変数として使うことができます。
JSON形式のオブジェクト形式で複数持つことができますが、自動で環境変数に展開することはできないようなので
valueFromで一つずつ

arn:aws:secretsmanager:ap-northeast-1:user-id:secret:secret-name-AAAAAA:YOURENV::

のように指定して読み込む必要がありました。

このチュートリアルのようにIAMロールの追加も必要です。
チュートリアル: Secrets Manager シークレットを使用した機密データの指定 - Amazon Elastic Container Service

その他

動かすためにいろんな設定を理解、修正する必要があり、独自ドメインからコンテナのWEBアプリケーションまで繋がるのに幾度となくはまり、3日ぐらいかかりましたがなんとか動かくことができました。

Circle CIでECRにプッシュ&アップデート

コンテナ変更のたびに手作業でビルド、プッシュするのは現実的ではないのでなるべく早い段階でやった方がよさそうです。
シニアジョブではCircle CIを使っていたので、そのまま利用しました。GitHub Actionsでもやってみたいなぁ。

Circle CIのいろんな処理をまとめたOrbsを使うとめっちゃ簡単でした。
ほぼ公式まんまですが、service-nameだけ公式ドキュメントになく、デフォルト値ではうまくいかなかったので、orbのソースを見て追加しました。
またfamilyがわかりにくいですが、タスク定義をJSONで確認するとfamilyの項目が見つかります。

まだステージング環境だけなのでtagの先頭にstageを固定でつけてます。

AWS ECR/ECS へのデプロイ - CircleCI

.circleci/config.yml

version: 2.1

orbs:
  aws-ecr: circleci/aws-ecr@6.15.3
  aws-ecs: circleci/aws-ecs@2.0.0

workflows:
  build_and_push_image_app:
    jobs:
      - aws-ecr/build-and-push-image:
          account-url: AWS_ECR_ACCOUNT_URL
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          dockerfile: ./Dockerfiles/app/Dockerfile
          path: .
          region: AWS_REGION
          repo: senior-job/app
          create-repo: true
          tag: "stage-$CIRCLE_SHA1"
      - aws-ecs/deploy-service-update:
          cluster-name: 'example'
          service-name: 'example-service'
          container-image-name-updates: 'container=app-container,tag=stage-${CIRCLE_SHA1}'
          family: 'example'
          requires:
            - aws-ecr/build-and-push-image
      - aws-ecs/deploy-service-update:
          cluster-name: 'example'
          service-name: 'example-service'
          container-image-name-updates: 'container=queue-container,tag=stage-${CIRCLE_SHA1}'
          family: 'example'
          requires:
            - aws-ecr/build-and-push-image

コンテナにシェルで入る

なぜかうまく行かない時は、中で環境変数がどうなってるかとか、ファイルちゃんとあるかとか、やっぱりコンテナの中に入って確認したいもの。

Kubernetesだと`kubectl exec -it pod_name bash`みたいにサラっと入れますが、一筋縄では行きませんでした。

特にFargateはEC2を触れないことから、難しかったみたいですが最近(2021/3)公式な手段が準備されたようです。
このプロジェクトが2ヶ月早かったら詰んでたかも。5年以上前でもexecはできてたので、全体的な完成度としてはKubernetesがやっぱ進んでるなぁと思ったできごとでした。

New – Amazon ECS Exec による AWS Fargate, Amazon EC2 上のコンテナへのアクセス | Amazon Web Services ブログ

権限付与、サービスの設定変更、CLIのアップデートなどいろいろ面倒でしたが、こちらの記事がまとまっていてわかりやすかったです。
[アップデート] 実行中のコンテナに乗り込んでコマンドを実行できる「ECS Exec」が公開されました | DevelopersIO

これから

まだステージング環境が動いただけなので、いくつかまだたりないデプロイプロセスの追加や、死活監視関係、本番でのリソース調整やオートスケール設定など、まだまだたくさんありそうです。
あとAWSだけだと自動化つらいのでTerraformも使った方がいいのかなぁとか。

2021/4/15 追記

appのサービス(php-fpmとnginx)とqueueを実行するサービス&タスクは分けた方が良いと思った