RareJob Tech Blog

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

Chromebookってどうなの?

お久しぶりです、ディズニーをこよなく愛する岩堀です。みなさん再開後ディズニーには行けていますか? 私は毎月のようにチケット争奪戦をくぐり抜け、つい先日に新アトラクションをすべて体験できました!

そんな体験をレポートしようかと思いましたが、そちらは別の機会で別の体験をレポートしたいと思います。 今回は弊社情シス部門にて導入検討しているChromebookについて、話をしてみたいと思います。

きっかけ

弊社では通常、WindowsMacマシンを基本的に社員に割り当てておりましたが、 これらの端末を社員に配布するまでの準備にかなりの工数を必要としています。 この部分を長年課題として考えておりましたが、具体的な解決ができずにいました。

そのような中で弊社は今年度の初めに全社的にGoogleWorkspace(旧GSuite)の導入を進めていくことになり、徐々にGoogleツールを多用できる状況となってきました。ある程度Googleツールが利用できる環境下の中で、どの程度Chromebookにて業務を行えるのか検証を行いたいと思っていました。 そんなタイミングで私が使用しているMac端末のバッテリー交換が必要となったので、交換完了までの間、Chromebookを使って通常行っている業務がどこまで行えるか検証してみました。

私自身、自宅ではWindowsMacChromebookの3種を用途によって使い分けていたので、Chromebookへ端末を変えることには大きな抵抗もなく臨めましたが、どこまで同じように業務を行えるのかは若干の不安を持ちながら臨んでみました。

参考に私が普段端末を利用して行っている作業は主に以下になります。

  • 各種Webアプリケーションでの対応
  • ZoomやMeetを利用しての会議参加
  • MSOfficeドキュメントの閲覧/編集
  • gitによるソース管理
  • IDEを使ったソースコード閲覧・編集
  • CLIによるサーバ業務
  • Windowsサーバへのリモートアクセス

移行までの時間は?

まず代替え機となるChromebookに移るまでに要した時間ですが、こちらは正直に申しまして0分でした。 メールに関してはGmailを利用しているのと、ローカルファイルに関しては既にGoogleファイルストリームにて GoogleDriveをローカルドライブのように扱っていったことで、端末移行を行う必要がなかったです。

各種Webアプリケーション利用は?

次に業務に欠かせない各種Web アプリケーションですが、弊社ではコミュニケーションツールとしてChatworkとSlack、社内共有ドキュメントツールとしてconfluence等、多くのWebアプリケーションを使っています。基本的にChromeが使えるWebアプリケーションであれば、問題なく利用ができるので、こちらで問題なることはなかったです。また普段アプリベースで使っているもので、通知を表示させていても、WebPush通知が使えるものも多いので、特に違和感なく利用でき、業務で問題となることはなかったです。

Web会議ツールに関しても、MeetやZoomはブラウザのみで利用が可能なので、問題なく会議を行うことができました。

Officeは使えないのでは?

次にMSOfficeドキュメントですが、こちらはMacのときからOffice365を利用しており、引き続きこちらを使いました。ChromebookAndroidベースで作られているため、基本的にPlayストアからアプリを落とすことができます。そのためOffice365をPlay ストアからインストールを行うことで、Chromebook上でもOfficeドキュメントを問題なく扱うことができます。もちろんマクロで作りこんであるExcelも問題なく扱うことができました。

IDEとかはないでしょう?

gitやIDE周りは、Chromebook上でLinuxを起動させて、IDEを起動させることが可能なので、そちらで対応いたしました。 Chromebookの設定の中でLinuxを有効にすることで、Chromebook上でLinuxマシンがVMとして立ち上げることができます。 support.google.com

これによりLinuxを立ち上げることができ、その上でgitやIDEをインストールし、使うことができるようになります。今回は詳細なインストール方法は省略しますが、gitやIDEを使用することができるようになることで、コード管理、修正、レビュー等も問題なく対応が行えました。

CLI操作がしたいときは?

ターミナル操作が必要となるサーバ業務は先程のLinux機能を利用すると合わせてターミナルを使うことができます。こちらの方法でも問題はないのですが、ローカルリソースを上へのファイルダウンロードやアップロードに関してはVM上での作業になり、その後の業務に移る際に若干面倒であるため、私はChromeのウェブストアからインストールできるブラウザアプリのSecure Shell Appを使いました。

chrome.google.com

こちらを使うとブラウザ上でSSH操作が行え、ローカルリソースを扱えるので、ファイルのダウンロードやアップロードにてリソースを直接操作することができるので、とても便利です。

リモートデスクトップは無理でしょう?

最後にWindowsへのリモートデスクトップ操作ですが、こちらもPlay ストアよりMS製のリモートデスクトップアプリをインストールすることができ、使用することが可能となりますので、こちらですべてが対応できるようになります。リモートデスクトップアプリとしてはVNCもあるので、AWSのEC2インスタンスとして立ち上げることができるようになったMacOSにもリモートアクセスが可能になります。

感想

結果として一週間ほどChromebookを業務で使っておりましたが、大きく困ったことは発生しませんでした。逆に起動が早いということもありますし、固まることも殆どないので、業務効率は上がったと思われます。WindowsMac端末が必要な場合にはAWSのWorkspacesやEC2インスタンスで立ち上げたマシンにリモートアクセスすれば対応もできるので、大きく問題になることはないと思われました。

Office製品に関してはほとんどGoogleDocやSpreadSheet等に移すことも可能ですので、ドキュメント周りはそちらに寄せていくことも可能になると考えられます。Excelマクロの互換性の問題に関しても、先日Googleから対応したアドオンの発表もあり、より一層移行が進められると考えられます。 forest.watch.impress.co.jp

弊社では今後Chromebookを徐々に取り入れて行こうと考えておりましたが、今回の検証でより一層、早めに進めていきたいと感じました。 通常業務の影響も少ないことがわかり、逆に業務効率が上がる部分もあることがわかりました。キッティングの面から見ても、ログインするGoogleアカウントの設定の内容が端末に反映されていくので、端末準備にそれほど時間を必要としないことにもメリットを感じました。

みなさんも、一度はChromebookを使ってみて便利さを感じてみてください。ちなみに弊社サービスでありますレアジョブ英会話のレッスンルームも、まだChromebookを推奨環境としていないですけど快適に使うことはできるのを確認しています。私自身レッスンを受けるときは検証も兼ねてChromebookを利用しておりますので、推奨環境に含まれましたらぜひご利用を試してみてください。

スクラムガイド2020が伝えたいこと その1

はじめに

こんにちは。レアジョブのサービス開発チームの三上です。

先日スクラムガイドが3年ぶりにアップデートされました。 私が初めてスクラムマスターを担当したのが2018年だったので、 これまで何度も読み返してきましたし、時には他のスクラムマスターと読みあわせを実施してきたので 自分にとっての教科書が更新されたようなもので、これは個人的には大きな出来事でした。

この記事では、その変更点について自分なりの意見・理解を混じえながら語りたいと思います。

2017年版からの変更点

前回との変更点については公式のスクラムガイドにも記載がありますので、 詳しくはスクラムガイド2020をご確認ください。

いくつかの変更点の中で、この記事では以下の2つの変更点について注目したいと思います。

  • 指示的な部分を削減(デイリースクラムでの質問削除)
  • プロダクトゴールの導入

変更された内容について、公式のガイドではこのように説明されています。

スクラムガイドは時間が経つにつれて少し指示的なものになっていた。2020 年版では、指示的 な表現を削除または緩和して、スクラムを最小限かつ十分なフレームワークに戻すことを目的 としている。たとえば、デイリースクラムの質問の削除、PBI(プロダクトバックログアイテム) の属性に関する記述の緩和、スプリントバックログにあるレトロスペクティブのアイテムに関する記述の緩和、スプリントの中止のセクションの削減などを実施した。

この「デイリースクラムの質問の削除」についてですが、これまでのスクラムガイドでは

  • 開発チームがスプリントゴールを達成するために、私が昨日やったことは何か?
  • 開発チームがスプリントゴールを達成するために、私が今日やることは何か?
  • 私や開発チームがスプリントゴールを達成する上で、障害となる物を目撃したか?

のようにあくまで「例」として説明されていました。

しかし、私自身いくつかのチームを立ち上げる際にはまずはこの例を参考にやってみることが多かったですし、 実際に多くのチームが同じようにデイリースクラムを行っていたのではないでしょうか。

なぜ変更されたのか?

まず私がこのデイリースクラムの質問の削除されたことを目にした時にすぐに「なるほど」と納得しました。

なぜかと言うと私自身も開発メンバーとして、スクラムマスターとしてデイリースクラムに参加した経験の中で、 この3つの質問をすることだけをこなしてしまい、目的を見失ってただただ報告し合うデイリースクラムを何度も目撃してきたからです。

では何が重要かと言うと、 各メンバーがデイリースクラムで達成したい目的を理解して実施することだと考えています。

デイリースクラムの目的を理解する上で重要だと思っているのが「スプリントゴール」の理解です。

今回の変更でもう一点合わせて説明しておきたいのが、 「プロダクトゴールの導入」についてです。

以下のように、ブレイクダウンして順に説明していきます。

プロダクトゴール>スプリントゴール>デイリースクラム(24時間ごとの計画)

まず、スクラムチームを組んでいるからには達成したいプロダクトのゴールがあるはずです。 無いとは思いますが、ゴールを知らないまたは無い場合は今すぐにプロダクトオーナーと会話しましょう!笑

プロダクトゴールを達成するための成果物は「プロダクトバックログ」となります。

スプリントプランニングでは、プロダクトバックログからアイテムを優先度順にスプリントバックログに移してスプリントの計画を立てます。 プランニング時には、そのスプリントが完了した時にどんな状態になっているか、開発チームはそれを説明出来ないといけません。

そして、スプリントプランニングの成果物は「スプリントバックログ」となります。各スプリントではスプリントゴールを設定します。

では改めてデイリースクラムの目的ですが、スクラムガイド2020では

デイリースクラムの目的は、計画された今後の作業を調整しながら、スプリントゴールに対する進捗を検査し、必要に応じてスプリントバックログを適応させることである。

とされています。 スプリントゴール達成のための、再計画の場であると言うことです。

説明が長くなってしまいましたが、 なぜデイリースクラムの質問がなくなったかと言うと、 本来の目的がゴール達成のための検査と適応の場であるはずが 目的を理解せず、具体的な3つの質問だけをする進捗報告の場になってしまうケースが多かったからではないかと考えています。

レアジョブでは?

理想はありつつも、レアジョブのスクラムチームでもTRY&エラーしながらやっています。

デイリースクラムもそうですが、各スクラムイベントにおいて、 HOW(どうやって達成するか)の部分って議論が盛り上がりませんか? やりたい事や課題解決をどう達成するかはエンジニアの得意とする所でもあるが故に、 弊社でもよく議論が盛り上がります。笑

そんな時に重要になってくるのがWHY(なぜそれをやるのか)という観点だと思います。 これを念頭において、どう実現するのがベストなのかやり方について話し合うと答えが見えてくるかも知れません。

最後に

今回はデイリースクラムを中心にお話しましたが、 スクラムにおいて改めて強調したいことは以下の通りです。

  • 軽量なフレームワークである
  • 意図的に不完全であり、まずはそのまま試そう
  • 役割、イベントの目的を理解しよう
  • 詳細な指示はないのでチームで考えよう(自己管理しよう)

スクラムガイド2020が伝えたいこと その1として書かせていただきましたが、 その2があるのか無いのか、乞うご期待です。

以上です。それではまた!

Amazon EC2 Mac Instances が来た!

aws.amazon.com

来ましたね。フォートナイトのチャプター2シーズン5も来たので本当はこの話を6万字書きたいんですが、Amazon EC2 Mac Instance も同じくらい見逃せないのでアプリエンジニア観点で何がこれで解決されるか・難しいかを書いてみようと思います。

難しいと思う点

iPhone/Macアプリ開発においてはMacOSが必須になるため BitriseCircleCI のようなSaasを利用したり、自前でMacPro/Miniなどを用意して Jenkins + xcodebuild コマンドを使ったり Xcode Server でビルドします。

今回出た Amazon EC2 Mac Instance がこれを代替できるかという期待が一つあると思いますが、乗り越えるべきハードルがいくつかあるのではないかと考えています。

Xcodeのバージョン依存

iPhone/Macアプリ開発の課題の一つに「Xcodeのバージョン依存」という課題があります。 Xcodeは毎年アップデートがあり、かつアプリの開発はXcodeへのバージョン依存が強いので、「AのアプリはXcodeのバージョン11.4じゃないと動かない」といったケースが多々あります。XcodeはNodeモジュールのように軽量とは言い難く(17GBとかある)、そのため今回どうプロビジョニングされるかが気になるところです。

Amazon EC2 Mac Instance でこれが解決されているような記述は今の所見られないため、自身でEC2を立ち上げて、最初はリモートデスクトップで入り手動でセットアップが必要に思います。

only pay for actual usage with AWS’s pay-as-you-go pricing

とあるのでそれで実用時は適宜立ち上げるというのもいいと思います。

証明書・プロビジョニングまだまだ面倒問題

fastlaneの登場で、この辺りはAppleIDさえあればアプリ開発に関するセットアップや証明書管理などがCLIからかなり楽に管理できますが、まだまだ詰まり所や理解しにくい設定の多い部分です。この辺りの設定やAppleIDとの結合度の高いCI/CDシステムを自身でメンテしていくのは結構体力を使います。

結局CI/CD便利すぎる問題

Bitrise も Circle CI もできることの割に安いんですよね・・・なので手間を使って乗り換えるかが微妙なところです。Bitrise も Circle CI も必要があればsshもできるし、リモートデスクトップはできないですがCI/CDにおいては必要ないので、十分です。また上記の「Xcodeバージョン問題」も新旧バージョン指定可能になっており解決できます。WordFlowやIntergrationも充実してて自分でやることはリポジトリと接続したり証明書などを設定する程度です。

たとえばセキュリティ都合などでどうしてもこれが導入できないケースやネットワーク上これが難しいようなケースでない限りはこれらを導入することで多くの問題は解決します。

*追記、Bitriseもリモートデスクトップできるようです!

期待したい点

CI/CD以外の用途や、限定的な状況ではこれまでできなかったことができるようになる期待があります。

リモートデスクトップとしての用途

情シス観点で考えると、開発者全員がMacを持たないといけない状況はセキュリティや費用面、セットアップの手間からできれば避けたいところです。もちろん開発的なリターンはありつつも ないに越したことはない・・・

例えば Chromebook + Amazon EC2 Mac Instance + AWS Remote Desktop Gateway で端末や負荷を気にすることなくiOS/MacOSの開発環境が実現できるかもしれません。

aws.amazon.com

また1つiOSアプリの開発可能なインスタンスを用意しておけば、iPhone/iPadのシミュレーターが利用できるので、全員の環境に準備しなくても共有して検証ができます。

利用可能なMac miniのプロセッサは第8世代のCore i7プロセッサ(物理6コア/論理12コア)、3.2GHz(ターボブースト時4.6GHz)32GBメモリとなっており高スペックですし、2021年にはM1チップ搭載の端末も利用可能になるとのことでとても楽しみです。

ネットワーク要件が厳しいUIテストの実施

たとえば「UIテストを実際のAPIを叩きながら実行したい」「APIは本番でなく社内やIPの制限された検証環境のものを使いたい」みたいなケースであれば Amazon EC2 Mac Instance で解決できるかもしれません。あまり多くはないと思いますが、リクエスト元IPや証明書などの制限のあるリクエストをUIテスト中に実施したいケースは解決できそうです。

AWSのネットワーク上でMacOSが動く」のメリットの一つだと思います。

まとめ

今回のリリースで「さぁ!CI/CD入れるぞ!」とまではならないですが、プロビジョニングや連携の改善が今後出てくると乗り換える未来は遠くないように思います。フォートナイトのチャプター3が出ることにはそんな未来になっているかもしれないですね。 緩く触りながら用途を探っていこうと思います。

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フォーマットにしていますが、プロッティングとして可視化できる仕組みもあります。

おわりに

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

参考