RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

AWS SAM で Serverless な環境を構築する

どうも、DevOps チームの うすい です。
トトロも鬼滅の刃 無限列車編も見たことがありません。

今回新しいシステムを aws-sam-cli を用いて構築したので簡単にですがそれらの内容を記述したいと思います。AWS SAM 自体の説明は割愛します。

私のマシンの aws cli などのバージョンは下記となります。

$ aws --version
aws-cli/1.16.79 Python/3.7.1 Darwin/19.6.0 botocore/1.12.69

$ sam --version
SAM CLI, version 1.6.2

余談ですが aws-sam-cli のバージョンアップの速度はすごいですね。

AWS SAM では CloudFormation っぽいテンプレートファイルと samconfig.toml ファイルを使用します。samconfig.toml はsam deploy --guided時に生成することもできますが今回は作成しておきます。
あまり情報は見かけませんが、samconfig.toml で ステージング環境 / 本番環境 といった環境に応じたパラメータが適用されるようにします。雰囲気は下記です。

version = 0.1

[default.build.parameters]
profile = "dev"
debug = true
skip_pull_image = false
use_container = true

[default.deploy.parameters]
stack_name = "hogehoge-dev"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-dev"
s3_prefix = "hogehoge"
region = "ap-northeast-1"
profile = "dev"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"dev\""

[stg.build.parameters]
profile = "stg"
debug = true
skip_pull_image = false
use_container = true

[stg.deploy.parameters]
stack_name = "hogehoge-stg"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-stg"
s3_prefix = "hogehoge-stg"
region = "ap-northeast-1"
profile = "stg"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"stg\""

[prd.build.parameters]
profile = "prd"
debug = false
skip_pull_image = false
use_container = true

[prd.deploy.parameters]
stack_name = "hogehoge-prd"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-prd"
s3_prefix = "hogehoge-prd"
region = "ap-northeast-1"
profile = "prd"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"prd\""

一番最後の[prd.deploy.parameters]を例にして説明すると、この部分は[環境.コマンド.aws-sam-cliに渡すパラメータ]となります。このセクションで各パラメータを設定しておくことで、コマンド実行が楽になります(後述)。また、profileAWS CLI の config と同じものを記載してください。parameter_overridesで環境名でEnvパラメータを上書き指定しています。

それでは API Gateway と Lambda Authorizer と DynamoDB を用いた雰囲気tempalte.yamlをご覧ください。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  hoge serverless service

Globals:
  Function:
    Timeout: 3
    Runtime: python3.8
    Environment:
      Variables:
        ENV: !Ref Env
        HOGE_API_HOST: !Ref HogeApiHost
    Layers:
      - !Ref MyLayer
    VpcConfig:
      SecurityGroupIds: !FindInMap [ SecurityGroup, !Ref Env, SecurityGroupIds ]
      SubnetIds: !FindInMap [ SubnetId, !Ref Env, SubnetIds ]        

Parameters:
  Env:
    Type: String
    AllowedValues:
      - prd
      - stg
      - dev
    Default: dev
  HogeApiHost:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /hoge/api/host

Conditions:
  IsDev: !Equals [ !Ref Env, dev ]

Mappings:
  SecurityGroup:
    prd:
      SecurityGroupIds:
        - sg-hoge-prd
    stg:
      SecurityGroupIds: 
        - sg-hoge-stg
    dev:
      SecurityGroupIds:
        - sg-hoge-dev
  SubnetId:
    prd:
      SubnetIds:
        - hoge-prd1
        - hoge-prd2
    stg:
      SubnetIds:
        - hoge-stg1
        - hoge-stg2
    dev:
      SubnetIds:
        - hoge-dev1
        - hoge-dev2

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Env
      Auth:
        DefaultAuthorizer: TokenAuth
        AddDefaultAuthorizerToCorsPreflight: False
        Authorizers:
          TokenAuth:
            FunctionPayloadType: TOKEN
            FunctionArn: !GetAtt authorizerFunction.Arn
            Identity:
              Header: Authorization
              ReauthorizeEvery: 0

  authorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/authorizer/
      Handler: app.lambda_handler
      Description: API Gateway Lambda Authorizer
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref Hogetable
        - AmazonSSMReadOnlyAccess

  # Lambda Layer
  MyLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: hogehoge-service
      Description: ""
      ContentUri: service/
      CompatibleRuntimes:
        - python3.8
    Metadata:
      BuildMethod: python3.8

  # DynamoDB
  HogeTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Hogetable
      AttributeDefinitions: 
        - AttributeName: id
          AttributeType: N
      KeySchema: 
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput: !If [IsDev, { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 }, !Ref AWS::NoValue]
      BillingMode: !If [IsDev, !Ref AWS::NoValue, PAY_PER_REQUEST]

Outputs:
  WebEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Env}/"

ほとんど CloudFormation のテンプレートですね。Globals:とか sam 特有に見えますが CloudFormation でも使えるみたいです(未確認)。
雰囲気だけではなんなので、テクニック的なことも書きますと

  • Parameters:のところでType: AWS::SSM::Parameter::Value<String>を使用し Parameter Store の値を取得
  • Conditions:Envdevでないときに DynamoDB のテーブルを Provisioned ではなく OnDemand で作成

といったことをしています。

実際にステージング環境にデプロイするにはまず

$ sam build --config-env stg

とビルドします。samconfig.toml に色々と設定しているのでコマンド自体はシンプルですね。最終的に下記のような出力を確認できます。

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Artifacts が出力されているディレクトリの中には CloudFormation のテンプレートも出力されます。
それではデプロイしてみましょう。出力内容は雰囲気です。toml ファイルでconfirm_changeset = trueとしていますので、ChangeSet の確認が途中で入ります。

$ sam deploy --config-env stg
Uploading to hogehoge-stg/xxxxxxxxxxxxxxxxxxxxxxx  5000 / 5000.0  (100.00%)

    Deploying with following values
    ===============================
    Stack name                 : hogehoge-stg
    Region                     : ap-northeast-1
    Confirm changeset          : True
    Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-stg
    Capabilities               : ["CAPABILITY_IAM"]
    Parameter overrides        : {'Env': 'stg'}

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                              LogicalResourceId                      ResourceType                           Replacement                          
---------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                  MyApiDeploymentxxxxxxxxxx              AWS::ApiGateway::Deployment            N/A                                  
+ Add                                  MyApiStage                             AWS::ApiGateway::Stage                 N/A    
---------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:hoge

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y    <--------------- y を入力すると反映されます

CloudFormation events from changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                         ResourceType                           LogicalResourceId                      ResourceStatusReason                 
---------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                     AWS::ApiGateway::RestApi               MyApi                                  -                                    
CREATE_IN_PROGRESS                     AWS::ApiGateway::RestApi               MyApi                                  Resource creation Initiated          
CREATE_COMPLETE                        AWS::ApiGateway::RestApi               MyApi                                  -                                    
〜〜 省略 〜〜
---------------------------------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 WebEndpoint                                                                                                                           
Description         API Gateway endpoint URL                                                                                                              
Value               https://hogehogehoge.execute-api.ap-northeast-1.amazonaws.com/stg/                                                                      
-----------------------------------------------------------------------------------------------------------------------------------------------------------

と分かりにくいですがResourceStatusの値が変化しながら処理が進み、最後に template.yamlOutputsで指定した値が出力されます。
一番右のReplacementの値に注意しながら運用していこうと思います。

Bitrise で AAB を使ったワークフローを作ってみた

こんにちは!APP・UX チームで主に Android を担当している杉山です。
最近寒くなり、スノボーやスキーを楽しみたい季節になってきましたね。
(東京に住み始めてからは雪山に行く回数も激減していますが...)

今回は、Bitrise で AAB (Android App Bundle) を使ったワークフローを作り、指定した メールアドレスに生成した APK を配布する仕組みに関して説明したいと思います。

登場キャラクターの紹介

Bitrise

モバイルアプリ開発iOS, Androidなど)における継続的インテグレーション・デリバリー(CI/CD) プラットフォームのサービス提供をしている。
ソフトウェアプロジェクトの開発・自動化を手助けするためのツール・サービスの集合体。

AAB (Android App Bundle)

アプリのコンパイル済みコードとリソースがすべて含まれた公開形式。
APK の生成と署名は Google Play が行い、デバイス設定ごとに最適化した APK を生成、配信できる。

Bitrise のワークフロー

f:id:r_sugiyama:20201106162452p:plain 今回説明するワークフローはこちらになります。
ポイントとなるステップは赤枠の部分です。
こちらのワークフローを設定し、ビルドすることで今回の狙いとなる動作をしてくれます。

ポイントとなるステップについて

ステップ 説明 / 設定ポイント
f:id:r_sugiyama:20201106164311p:plain Gradleを利用してAndroidプロジェクトをビルドするステップです。

★ 設定時のポイント
Build type を「aab」にしましょう。
f:id:r_sugiyama:20201106164857p:plain 配布する APK や AAB (Android App Bundle) に署名をするステップです。

★ 設定時のポイント
App file path には「.aab」ファイルのパスを指定しましょう。
(「/bitrise/deploy/~.aab」)
f:id:r_sugiyama:20201106165026p:plain AAB (Android App Bundle) から universal apk をエクスポートするステップです。

★ 設定時のポイント
Android App Bundle path には、Android Sign ステップの App file path に指定したファイルパスと同じものを設定しましょう。
f:id:r_sugiyama:20201106165131p:plain 指定したメールアドレスに生成した apk ファイルを配布してくれるステップです。(本当はもう少しできる子です。)

★ 設定時のポイント
Emails に 配布対象のメールアドレスを設定しましょう。
(Bitrise 上で複数指定するのが面倒な場合、メーリングリストを作るのも良いかもしれません。)

最後に

Bitrise は GUI 上で色々できて便利ですね。
皆様も、色々と試してみてください!
私も、遊びながらやれることを模索したいと思います。

NuxtアプリケーションでProvide・Injectを使ったStoreパターンを構成する

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。

今回はComposition Functionを使うに際に便利なProvide・Injectの機能を使った Storeパターンについてご紹介したいと思います。

目次

はじめに

Composition FunctionはVue3.x系から正式に機能として組み込まれたCompositionAPIを利用した際の関数の総称を言い、ロジックをComponentから引き剥がす事ができ非常に見通しが良くなる便利なものです。

通常は読み込まれる(関数をimportした)Componentと対になる関係であり Componentを横断し共通の値の変更を検知することはできません。

今回はサンプルとしてカウントアップ・ダウンするComposition FunctionであるuseCounter.tsを用意し
その処理について表示部と処理部についてComponentを分割した形で以下に例を記載しております。

通常の利用について

  • SampleComponentにuseCounter.tsをimportする
  • SampleComponent内でreactiveな値(例でいうcount)や関数を表示に利用する

e.g. useCounter.ts

import { reactive, computed } from '@nuxtjs/composition-api'

const useCounter = () => {

  const state = reactive({
    count: 0
  })

  const count = computed(() => state.count)

  const increment = () => state.count++
  const decrement = () => state.count--

  return {
    count,
    increment,
    decrement,
  }
}
export default useCounter

e.g. SampleComponent

<template>
  <div>
    <div>{{ count }}</div>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useCounter from '~/composition/useCounter'

export default defineComponent({
  setup() {
    const { count, increment, decrement } = useCounter()
    return {
      count,
      increment,
      decrement
    }
  }
})
</script>

上記の様にComponent内で処理(加算と減算)と表示(count)が共存しているComponentであれば特に問題はないのですが、パーツの再利用性なども考慮すると加算ボタン・減算ボタンと表示部を分けたComponentにしたいということがあるかと思います。

Componentを横断して共通の値を参照したい場合

  • useCounter.tsは先程と同様
  • 表示部・加算・減算を各Componentに分割
  • 各ComponentにComposition Functionをimportしてもreactiveな値や関数の発火は共有されない
    → その為、処理が走りません
    これを解決するためにProvide・Injectを利用します。

e.g. SampleCountComponent

<template>
  <div>
    <div>{{ count }}</div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useCounter from '~/composition/useCounter'

export default defineComponent({
  setup() {
    const { count } = useCounter()
    
    return {
      count
    }
  }
})
</script>

e.g. SampleAddComponent

<template>
  <div>
 <button @click="increment">+</button>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useCounter from '~/composition/useCounter'

export default defineComponent({
  setup() {
    const { increment } = useCounter()
    
    return {
      increment
    }
  }
})
</script>

e.g. SampleSubComponent

<template>
  <div>
 <button @click="decrement">-</button>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useCounter from '~/composition/useCounter'

export default defineComponent({
  setup() {
    const { decrement } = useCounter()
    
    return {
      decrement
    }
  }
})
</script>

e.g. SampleMergeComponent

<template>
  <div>
    <SampleCount />
    <SampleAddButton />
    <SampleSubButton />
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import SampleCount from '~/components/atoms/Sample/SampleCountComponent.vue'
import SampleAddButton from '~/components/atoms/Sample/SampleAddComponent.vue'
import SampleSubButton from '~/components/atoms/Sample/SampleSubComponent.vue'

export default defineComponent({
  components: {
    SampleCount,
    SampleAddButton,
    SampleSubButton
  }
})
</script>

ここまでを準備すると表示部・加算部・減算部に別れたComponentが合わさった表示が見えるようになりますが 各配下Component(SampleCount, SampleAddButton, SampleSubButton)で各自useCounter.tsをimportしているので値の連携が取れず、処理が正常に動作しません。 ※各自importのタイミングでComposition Functionがインスタンス化され、全く別のインスタンスとして扱われるイメージです。

これをProvide・Injectを使って書き換えます。 ※TypeScriptでの例になります

  • まずインテリセンスを効かせたい為、Keyを作ります
  • useCounter.tsのexport typeにも型を設定します
  • WrapperのComponentを用意して配下Componentをslotでラップし、Provideを行います
  • 読み込んだComposition Functionを利用したい配下Componentで呼び出してinjectします
  • これでReactiveな値や関数がComponentを横断して共有することができるようになります

e.g. Composition Key

/* eslint-disable import/named NuxtのCompositionAPIの場合書かないとerrorになる為*/
import { InjectionKey } from '@nuxtjs/composition-api'

// ↓これはuseCounterで型をexportしたものです
import { CounterStore } from '~/composition/useCounter'

export const CounterKey: InjectionKey<CounterStore> = Symbol('CounterStore')

e.g. useCounter.ts

export type CounterStore = ReturnType<typeof useCounter> //この行を追加
export default useCounter

e.g. SampleProviderComponent

これがComposition Functionを配下で利用できるようにする為のProviderComponentになります

<template>
  <div>
    <slot />
  </div>
</template>
<script lang="ts">
import { defineComponent, provide } from '@nuxtjs/composition-api'
import { CounterKey } from '~/compositionKey/useProvideKey'
import useCounter from '~/composition/useCounter'

export default defineComponent({
  setup() {
    provide(CounterKey, useCounter($environments))
    return {}
  }
})
</script>

e.g. SamplePage 先程のProviderComponentの間にSampleMergeComponentを読み込みます。 ※Componentの粒度としてはコレが一番外側のComponentになります。
Nuxtで言うところのPages等にコレを設定するとPage単位での値の共有が可能です。

<template>
  <SampleProvide>
    <SampleProvideTemplate />
  </SampleProvide>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import SampleProvide from '~/components/provider/sampleProvide/index.vue'
import SampleMergeComponent from '~/components/templates/sampleProvide/index.vue'

export default defineComponent({
  components: {
    SampleProvide,
    SampleMergeComponent
  }
})
</script>

e.g. SampleCountComponent

<template>
  <div>
    <div>{{ count }}</div>
  </div>
</template>
<script lang="ts">
import { defineComponent, inject } from '@nuxtjs/composition-api'
import { CounterStore } from '~/composition/useCounter'
import { CounterKey } from '~/compositionKey/useProvideKey'

export default defineComponent({
  setup() {
    const { count } =inject(CounterKey) as CounterStore
    
    return {
      count
    }
  }
})
</script>

e.g. SampleAddComponent

<template>
  <div>
 <button @click="increment">+</button>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { CounterStore } from '~/composition/useCounter'
import { CounterKey } from '~/compositionKey/useProvideKey'

export default defineComponent({
  setup() {
    const { increment } =inject(CounterKey) as CounterStore
    
    return {
      increment
    }
  }
})
</script>

e.g. SampleSubComponent

<template>
  <div>
 <button @click="decrement">-</button>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import { CounterStore } from '~/composition/useCounter'
import { CounterKey } from '~/compositionKey/useProvideKey'

export default defineComponent({
  setup() {
    const { decrement } =inject(CounterKey) as CounterStore
    
    return {
      decrement
    }
  }
})
</script>

Globalで使いたい場合

Page単位での値の共有については上記の通りですが全ての配下Componentで横断して Globalな使い方をしたい場合はNuxtの場合layerの層(default.vueなど)、Vueの場合App.ts等にProvideを行えば 配下での共有が可能になります。

結果

Provide・Injectを利用することにより、兄弟間のComponentに対してはprops drilling問題について気にすることなく 横断してstate&Functionを共有することができます。

まとめ

Composition Functionの利点として読み込まれるComponentに閉じたカプセル化を行える事でVuexのように 1つのglobalなデータを扱うというデメリットは抑えられる為、この方法を採用しました。 大規模アプリケーションでの実装においてはVuexの利点も大きいですが、各Storeに対してのアクセスをルール化する等 一定の秩序を持たせなければならず実装上、煩雑になる可能性が高いことが多い為、Vuexの利用を一時的に止めこの方法を利用しております。 開発中のアプリケーションが大きくなるにつれて、扱い難くなればVuexの導入についても再度検討する予定です。

参考

お知らせ

Software Design 2020年11月号
今月号のFlutter特集ですが弊社所属のスーパーアプリエンジニアが執筆に参加されてます!!

非常にわかりやすい内容でこれからFlutterを触っていこうという方にうってつけの内容ですので 是非、手にとって内容確認して頂けると!!!

Vegetaライブラリを使ってGoでちょっとこだわった負荷試験シナリオを作る

こんにちは!プラットフォームチームの池田です。2回目の投稿になります。

元々ハンバーガーが好きで社内でもそれで自分を売り出していたのですが、つい最近とんでもない対抗馬と出会ってしまいました。そう、バインミーです(今更?)。

時代と文化が生んだ奇跡。私はハマってしまいました。バインミータベタイ

閑話休題、直近で負荷試験を実施する機会があり、それに関するトピックを紹介します。

はじめに

私が所属するチーム(プラットフォームチーム)ではGo言語をメインに開発していることもあり、負荷試験においてもGoでカスタマイズできるツールを探していました。特に気になったのがGo言語で開発されているOSSVegetaのライブラリでの利用方法でして、今回試してみました。

Vegetaライブラリを使った動的なテストシナリオの方法を紹介した記事はあまり見受けられず、ベストな選択肢では無いと承知しながらも、この記事を書いてみようと思いました。

【目次】

Vegetaとは?

安定した性能を発揮する多機能なHTTP負荷テストツールです。

GitHubのREADMEでは名前の由来である"彼"の姿を見ることできます。

Vegetaツール自体は古き良きApache HTTP server benchmarking tool (ab)と同様のCLIベースのツールです。

abと比較して、HTTP/2対応、分散攻撃機能、結果プロッティング機能などの付加的な特徴があります。(本記事ではこれらの機能には言及しません。)

Vegetaライブラリを利用するメリット

Vegetaライブラリで負荷試験シナリオを作るメリットは以下としています。

  • Goをメインに開発しているチームにとってスムーズに作成と保守ができる
  • Goの特徴であるシンプルなクロスコンパイルにより、どの環境にもシングルバイナリとして簡単に乗せることができる

逆にデメリットは以下になります。

  • JMeterGatlingLocustなど他の負荷テストツールと比較するとコードが複雑になり可読性が良くない

今回のシナリオの想定

プラットフォームチームが保持するマイクロサービスの1つである会員基盤サービス(REST API) に対する負荷試験という前提で進めます。

本記事では、弊社サービスにおいてシンプルだけれどもアクセスピークがクリティカルなシナリオとして、以下のユーザーストーリーを想定します。(実際の弊社サービスに対して正確な表現ではありません。)

  • Step1: 英会話レッスンが始まる直前の時刻にユーザーがログインする
  • Step2: レッスン予定を閲覧できるページにアクセスする(レッスン開始ボタンがある)
  • Step3: 開始ボタンを押してレッスンルームに入る

このとき、会員基盤サービスに関して、以下のようなやり取りが行われます。(下記システム構成も実際の弊社システムに対して正確ではありません。)

ログイン処理

f:id:ochataro:20200901234249p:plain
login

スケジュール取得

f:id:ochataro:20200901234329p:plain
schedule

単一アカウントのみのテストの場合

1つの会員アカウントを使って負荷をかける場合、VegetaのCLIを利用すれば簡単にテストが可能です。

f:id:ochataro:20200901234534p:plain

以下のログインエンドポイント用のJSONペイロードファイル(login.json)と対象エンドポイントを記載したファイル(target.txt)を用意します。

予定取得エンドポイント(/schedule)にてJWTトークンは予め用意したものを利用します。

login.json

{
  "email": "test_user@example.com",
  "password": "Password"
}

target.txt (※ この記事ではあえてローカル環境をターゲットにしています。)

POST http://localhost:8090/login
Content-Type: application/json
@login.json

GET http://localhost:8090/schedule
Content-Type: application/json
Authorization: Bearer xxxxx.yyyyy.zzzzz

負荷を以下のようなオプションでかけます。このとき、対象アカウントにてログインとスケジュール取得リクエストを交互に繰り返します。

vegeta attack -targets=target.txt -rate=50/s -duration 600s | \ 
  vegeta report -type=json | \
  jq

複数アカウントのテストの場合

テストシナリオを実環境にできる限り近づけるべく、テスト環境でバラバラな設定を持つユーザーらを事前に準備し負荷試験に利用するという前提です。

f:id:ochataro:20200901234517p:plain
multi

ポイントとなるのは、負荷時にログイン処理から返るJWTトークンを動的に取得し、予定取得エンドポイントのリクエストにてAuthorizationヘッダーへ取得したトークンを入れる処理を実装する必要があるということです。

プログラム

1000アカウント分のクレデンシャル情報を持つCSVファイルを事前に攻撃サーバに用意します。

test1@example.com,Passw0rdDAZE!
test2@example.com,Passw0rdNANOKA?
.
.
.
test1000@example.com,Passw0rdKAMONE&

Goのソースコードは下記になります。Vegetaライブラリのドキュメントとツール側の実装を参考にしながら作成しました。ちょっと長めなので折りたたんでいます。

主にJWTトークンの受け渡し部分と1000アカウント終了時に攻撃を停止する制御の部分でコードが少々複雑になってしまいましたがこれで動きます。

負荷をかける!

【はじめに注意】本記事用にダミーな負荷対象HTTPサーバをローカルに立てて検証していますので、下記の結果は弊社サービスの実際のシステムとは全く関係の無い結果であることにご留意ください。

上記のプログラムを動かし負荷をかけると結果が得られます。今回は2つのエンドポイントを混ぜ合わせた1シナリオとしての結果が得られるようにしています。

$ go build -o attack
$ ./attack -rate=50 -duration=60 | jq
{
  "latencies": {
    "total": 808174235516,
    "mean": 404087117,
    "50th": 410460112,
    "95th": 457702034,
    "99th": 470219198,
    "max": 509059811
  },
  "bytes_in": {
    "total": 64000,
    "mean": 32
  },
  "bytes_out": {
    "total": 52893,
    "mean": 26.4465
  },
  "earliest": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "latest": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "end": "2020-mm-ddTHH:MM:SS.097961559+09:00",
  "duration": 39957268820,
  "wait": 458157781,
  "requests": 2000,
  "rate": 50.053471097076844,
  "throughput": 49.4860544154324,
  "success": 1,
  "status_codes": {
    "200": 2000
  },
  "errors": []
}

攻撃結果を得ることができました。上記の場合は出力をJSONフォーマットにしていますが、プロッティングとして可視化できる仕組みもあります。

おわりに

複雑になってしまったリクエスト間のやり取りと終了時の制御は上手く抽象化すればプラグイン的な物が提供できそうだなと感じました。また次の機会に取り組みたいです。

参考

NuxtでAPI RequestをComposition Functionにする

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。

夏が終わり、最近すっかり肌寒くなってきましたが皆さん如何お過ごしでしょうか?
最近、私の周りで結婚や婚約する友人・知人がとても多く、おめでたい出来事に嬉しくなる反面、個人的にはとても心寒い日々が続いております。

話は変わり、Vueを扱うフロントエンドエンジニアの皆さんにとっては既知の出来事かと思いますが 先日、Vue3が正式にリリースされたので界隈では大いに賑わっておりました。

github.com 名称がワン●ースなので、世はまさに大Vue時代、「Vueのアーキテクチャーか?欲しけりゃくれてやる。探せ!この世のすべてを3.0系に置いてきた!」 的なノリなのかな?とか勝手に感じておりますw

このリリースによりVue自体に新しく追加された機能は幾つかありますが、Composition APIを正式に使えるようになったということを嬉しく思っている方が多いのかなと思っています。(私もその一人です)

弊社でもこのリリースを見越してComposition APIをプロダクト内で利用できるようにしていきたいね!ということだったので 実験的な部分もありますがNuxtベースに作成しているプロダクト内でComposition APIを使えるようにしております。 参考:Nuxt Composition API

今回はNuxt+Composition APIを使い、Ajax Request(axios-moduleを利用)をComposition Functionとして関数を切り離した 実装をご紹介できればと思っております。まだまだ手探り状態である為、一つの方法として見て頂けると嬉しいです。 もっとこうしたら良いよ!などの優しい指摘・意見を頂けると嬉しいです。

目次

Composition Apiとは

自身の理解を大まかに一言で示すと 「ロジックの再利用性の向上とロジックとComponentの依存性を低くできる」新しい仕組みと捉えております。

こちら→公式Composition APIに詳細な説明がありますし、 様々な方が利点や旨みについて説明されているのでこのAPIの基本的な使い方や 意義についてなどの説明については、末尾参考の各記事をご覧ください。

useApi.tsの作成

まずはベースとなる、API Requestを行うFunctionの作成を行います。 ※ファイル名、変数名については適当なものを設定しているのでご容赦を

※尚、useFetchはNuxt Composition APIの機能名として既に存在する為、ご注意ください。 今回はuseApi.tsという名称でベースを作成します。

import { reactive, toRefs } from '@nuxtjs/composition-api'
import { NuxtAxiosInstance } from '@nuxtjs/axios'

// 各型は参考まで
type Options = {
  headers: {
    'X-transaction-ID'?: string
    'x-api-key'?: string
    Authorization: string
    'Content-Type'?: string
  }
}
type Params = {
  [key: string]: any
}
type baseState = {
  response: {}
  otherError: Error | null
  isLoading: boolean
}

const useApi = (
  $axios: NuxtAxiosInstance,
  url: string,
  params?: Params,
  options?: Options
) => {
  const state = reactive<baseState>({
    response: {},
    otherError: null,
    isLoading: false
  })
  // GET
  const getData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$get(url, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // POST
  const postData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$post(url, params, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // PUT
  const putData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$put(url, params, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // DELETE
  const deleteData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$delete(url, params)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  return { ...toRefs(state), getData, postData, putData, deleteData }
}
  • @nuxtjs/composition-apiをimportします
  • axios-moduleの型をimportします。
  • useApiという名称で関数を定義して、response値やLoadingステータスなどのstate値をreactiveにします。
  • CRUDの関数を用意します。(ここではtry/catch/finallyを利用しておりますがNuxt内でaxios-moduleを使った一般的なRequestで問題ないです。)
  • returnでstateと各関数を返却します。この時stateはtoRefsでreturnしてuseApi.tsを利用する関数内で個別のreactiveな値として利用できるようにしておきます。

useSampleApi.tsの作成

次にuseApi.tsを呼び出す各endpoint側のComposition Functionを作成します。
ここではuseSampleApi.tsとしています。

import { toRefs, reactive } from '@nuxtjs/composition-api'
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import useApi from '~/composition/useApi'

const useSampleApi = (axios: NuxtAxiosInstance) => {
  const sampleState = reactive<{
    response: any
    error: Error | null
    isLoading: boolean
  }>({
    response: [],
    error: null,
    isLoading: false
  })
  const apiGetTrigger = async () => {
    const { response, otherError, isLoading, getData } = useApi(
      axios,
      `https://hogehogehoge.com`
    )
    sampleState.isLoading = isLoading as any
    await getData()
    sampleState.response = response as any
    sampleState.error = otherError as any
  }
  return { ...toRefs(sampleState), apiGetTrigger }
}
  • こちらもuseApi.ts同様に@nuxtjs/composition-apiとaxios-moduleの型をimportします。
  • useApi.tsから取得できる値を詰め直すstateをreactiveにします。
  • useApi.ts内で設定したXXXData関数(ここではgetData)を利用する関数を用意します。(apiGetTrigger)
  • apiGetTrigger内でaxiosとpathを渡します。(後述しますがaxiosはcomponentから関数利用時に渡します)
  • useApi.tsから返されたreactiveな値をuseSampleで設定したreactiveなstateに代入します。 (この時、toRefsとreactiveの型が異なる為、error回避のためanyでキャストしています。イケてないので良い方法があれば知りたいです。)
  • 関数とreactiveな値をreturnで返却します。

※awaitでgetData()を待つ理由として、api request後にresponseかotherErrorに値が返るのでそうしています。 isLoadigは表示側で常にreactiveな値として変化できるように、最初に代入しております。

Component内での利用

最後にComponent側でuseSampleApi.tsから取得できる各値を利用していきます。

<template>
  <div>
    <h2>Sample</h2>
    <section>
      <form @submit.prevent="apiGetTrigger">
        <button>Getlist</button>
      </form>
    </section>
    <div v-if="otherError">
      <h2>otherError !! {{ otherError }}</h2>
    </div>
    <div v-if="isLoading"><h2>Fetching Data</h2></div>
    <div v-for="list in response" :key="list.id">
      <ul>
        <li>
          <span>{{ list.name }}</span>
          <span>{{ list.street }}</span>
          <span>{{ list.city }}</span>
          <span>{{ list.postal_code }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useSampleApi from '~/composition/useSampleApi'
export default defineComponent({
  setup(_props, {root}) {
    const { $axios } = root
    const {
      response
      isLoading,
      apiGetTrigger,
      otherError
    } = useSampleApi($axios)

    apiGetTrigger() // こんな感じでsetup内で呼び出すとイベントではなくLifecycle内で呼ぶことも可能です。

    return {
      isLoading,
      response,
      apiGetTrigger,
      otherError
    }
  }
})
  • ComponentをdefineComponetを使って作成
  • setup関数内でrootを指定してpluginとしてinjectされている$axios(axios-module)を取り出す
  • useSampleApi.tsをimportして、setup関数内で各値を取り出す
  • template内で利用したい値をreturnで返す

※apiGetTriggerについてはコメントにある通り、setup関数内で呼び出せばComponentのLifecycle内で呼ぶことも可能です。

まとめ

useXXXの形式のAPI Requestを行う処理を関数単位で外に切り出すことができることで、Component内では返り値を扱うだけで 欲しい情報をtemplate部分に反映させることができる為、Component内にロジックが乗らないので見通しが良くなります。 また、別のComponentにimportして利用すれば同様のRequest処理を行える為、再利用性なども高いと思います。 関数ベースにJestをかければ良いのでJest.mockを利用した疑似API Requestのテストについても簡便に書くことができるので 便利だなぁと恩恵を受けております。 (Jestについては今度、書きます。) まだまだ、できることの把握と理解が追いついてないので手探り状態ではありますが参考になれば嬉しいです。

参考

PlantUMLでAWSのサービスを使う

こんにちは、前回書いた記事からはや2ヶ月ちょっと。 GOTO( Ghost of Tsushima Omoshiroi-zo )はプレイ時間60時間を超え、無事トロコンも完了し、対馬には平和が訪れました。 なので今日はまたPlantUMLの話をします。

PlantUMLでAWSアイコンを出したい

結論から言うと公式がマクロセットを用意してくれているのでこれをincludeすることで実現できます。 CacooやDraw.ioも使うんですが、図自体の可読性や差分の見やすさだとやはりUMLのほうが私は見やすので好きです。 個人のアイデアをまとめたり、説明のために整理するのによく使っています。

@startuml
title AWS

!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/master/dist
!includeurl AWSPuml/AWSCommon.puml
!includeurl AWSPuml/EndUserComputing/all.puml
!includeurl AWSPuml/Storage/SimpleStorageServiceS3.puml

actor "Person" as personAlias
WorkDocs(desktopAlias, "Label", "Technology", "Optional Description")
SimpleStorageServiceS3(storageAlias, "Label", "Technology", "Optional Description")

personAlias --> desktopAlias
desktopAlias --> storageAlias

@enduml

これで

f:id:jumbos5:20200925111606p:plain

これが出ます。 マクロ一覧もまとめてくれているので参考にしながら書くのが良いと思います。

マクロ一覧

注意点

!includeurl AWSPuml/EndUserComputing/all.puml

仕組み的に「必要なアセットをincludeしてdefineされているマクロに引数を私で描画する」と言う仕組みのため、 includeが必要になりますが、all.pumlは各フォルダ・コンポーネントのまとまりごとに用意されており、まとめてincludeできるのですが、重いので必要なコンポーネントだけincludeするようにしましょう。

どう言う仕組みで実現されているか1 「define」

PlantUMLにはマクロがありこれを定義して実現されています。

!define SampleMacro(v1, v2) [v1] -> [v2]
SampleMacro("value1", "value2")

こんな感じで定義できるので、これを使っています。 あまり自分で書いているときはそこまで再利用性考えて作らないのですが、アイコンつけたりする場合は真似したいですね。

どう言う仕組みで実現されているか2 「sprite」

PlantUMLで画像を読み込む際はグレースケールにした値を定義する必要があります。

sprite $ARVR [64x64/16z] {
xTG3WiH054NHzutP_th7RHkfsmnEdE1HZMZsIn0_DGDuuVsZJwnMVJ-57txuuKrsP4Tv1mjl3Nw43qZlo147VO9xPueyu8j1l3jm7V0GtPFWe8_UKzpL3rzc
TO4l0gZEzufCsDd-rnhoN2zKtKLoWk-bkHq--vabr0TypEy_WiwEmc9K7FATAd_fVDwOZygdU_uEF_pmLgUMA_wChkV1SavCc4LdXNVe2m
}

この機能もPlantUML自体が持っているので自前の画像を定義してAWSのようにマクロを追加することも可能です。

$ java -jar plantuml.jar -encodesprite 16z xxx.png

構造が複雑化してくると、線がカオスになってしまい図が複雑化してくるところを除いてとても気に入っています。 それは良い週末を

Google Chatのbotからスプレッドシートデータを読み出してみる

こんにちは、はじめまして
ITソリューションチームの平川です。

ITソリューションチームはいわゆる「情報システムチーム」でして、
日々PCについての問い合わせや依頼を受けています。
PCの作業依頼はワークフローで届くので、PC管理番号と利用者が正しいかなどの確認を行なって承認→作業となっていきます。

そこで、PC管理番号と利用者の確認はPC台帳なるものを都度確認するわけですが、この作業がめんどくさいんです。

本当に。

いちいち台帳開かなくても、誰かに聞けば教えてくれるようにならないかなと思いGoogle Chatを使って試してみました。

ではやっていきます。

GCPプロジェクトの作成

まずはGoogle Chat botを扱うプロジェクトを作成していきます。
下記リンクに沿って進めていきます。

Publishing bots  |  Google Chat API  |  Google Developers

[Enable API]ボタンをポチッと押して新しいプロジェクトを作成します。
ここでは「ChatbotTEST」と名前をつけて進めます。 

f:id:Peperonciiiiiiino:20200903182145p:plain

JSONファイルはダウンロードしてもしなくても大丈夫です。

f:id:Peperonciiiiiiino:20200903182337p:plain

これでプロジェクトが作成されました。

ボットスクリプトの作成

続いてボットスクリプトを作成していきます。
これもテンプレートが公開されているので利用していきます。

https://script.google.com/create?template=hangoutsChat

 クリックするとスクリプトが作成されますので、名前を変更してデプロイIDを取得します。
[公開] -> [マニフェストからの配置] -> [Get ID] 

f:id:Peperonciiiiiiino:20200903183618p:plain

f:id:Peperonciiiiiiino:20200903183658p:plain

表示された[Deployment ID]を控えておきます。

APIの構成とデプロイ

作成したプロジェクトからボットAPIの構成とデプロイを行なっていきます。

Google Cloud Platform

[ナビゲーションメニュー] -> [APIとサービス] -> [ダッシュボード] 

f:id:Peperonciiiiiiino:20200903184653p:plain

右ペイン最下部に「Hangouts Chat API」があるのでクリックします。

f:id:Peperonciiiiiiino:20200903184748p:plain

ここからボットAPIの設定を行なっていきます。
左メニュー -> [設定]
▼アプリケーション情報
下記情報を入力します
 [ボット名]
 [アバターのURL]
 [説明]
▼機能
 [ボットはダイレクト メッセージ内で機能します]にチェック
▼接続設定
 [App Script Project]を選択し、先の手順で入手している [Deployment ID]を入力します
▼権限
 必要に応じて設定してください

f:id:Peperonciiiiiiino:20200904112851p:plain

続いてサービスアカウントにプロジェクトのオーナ権限を付与していきます。
[ナビゲーションメニュー] -> [IAMと管理] -> [サービスアカウント]

f:id:Peperonciiiiiiino:20200903190302p:plain

表示されたサービスアカウントをコピーします。

f:id:Peperonciiiiiiino:20200903190647p:plain

[ナビゲーションメニュー] -> [IAMと管理] -> [IAM] ->[追加]
サービスアカウント追加して[オーナー]権限を付与します。

f:id:Peperonciiiiiiino:20200903203205p:plain

f:id:Peperonciiiiiiino:20200903203243p:plain

これで、GoogleChatのボットに登録されます。

動作確認その1

作ったボットの動作を確認します。
GoogleChatを開き、登録したbotを追加します。

https://chat.google.com/

[BOT]の横にある+から登録したbotを追加します

f:id:Peperonciiiiiiino:20200904113533p:plain

f:id:Peperonciiiiiiino:20200904113552p:plain

botに話しかけると入力したテキストを返してくれます。

f:id:Peperonciiiiiiino:20200904113836p:plain

テンプレートのスクリプトが動く事を確認できました。

ここまでうまく動いたので、スクリプトを修正してスプレッドシートからデータを読み出しを行なっていきたいと思います。

スプレッドシートの準備

スプレッドシートでPC台帳っぽいものを準備します。
今回はこんな感じの台帳を使います。

f:id:Peperonciiiiiiino:20200904114052p:plain

それからスプレッドシートのURLからIDを取得します。

スクリプトの修正

今回はPC番号[A列]を入力したら、貸出者[B列]が返ってくるようにスクリプトを修正していきます。

こんな感じに修正します。

f:id:Peperonciiiiiiino:20200904115010p:plain

動作確認その2

では、動作を試していきます。
(最初に文字を入力するとスプレッドシートへのアクセスリクエストが行われますので、スプレッドシートのアクセス権があるアカウントで[許可]を行なってください)

f:id:Peperonciiiiiiino:20200904115853p:plain
存在するPC番号の貸与者が正常に表示されました。

これでわざわざ台帳を見なくてもPC番号から利用者(貸与者)がわかるようになりました!


まとめ

GCPやGAS(Google Apps Script)はほとんど使ったことありませんでしたが、テンプレートやらナレッジがたくさんあったので割と簡単に作ることができました。

今回作ったのは単純なルールベース型のボットですが、もう少し作り込めばいくつかの業務改善が行えそうだなと思いました。

 

では、また。