見出し画像

Autifyとserverlessを組み合わせてQAを自動化した話

こんにちは、スペースマーケットのエンジニア 小林です。

課題

スペースマーケットではQAを行ってくれるプラットフォームであるAutifyを導入してQAを自動化しているのですが、どのように開発フローに組み込んでいるかをお話しします。

導入したアイデア

一定数のエンジニアはPRのビルドstatusが赤いと無視できないと思います。CircleCIのようにgithubのPR画面にAutifyの実行結果を表示させることで開発フローとして馴染みやすいと考えました。

そのために、リリースフローで必ず通るステージング環境確認時にAutifyを実行してPRに連動させる方法を検討しました。

設計

弊社のリリースフローにはAWS codepipelineを利用しています。ステージング環境へのdeploy後にAutifyを走らせて、結果をgithub statusに連動させるちょっとした仕組みを設計しました。

処理の流れ

1. deployment/stagingブランチにマージ(deployのhookになっています)
2. codepipelineにdeployを指示
3. codepipelineはgithubからソースを取得してステージング環境にdeploy
4. deploy後Lambdaを実行
5. githubのcommit status APIを叩いてAutifyをpendingステータスにする
6. Autifyの実行指示
7. githubのcommit hashとAutifyのビルドIDをペアにしてdynamodbに保存
(ここから↓はバックグラウンド処理になります)
7`. AutifyがE2Eテストをステージング環境で実行
8`. テスト結果のwebhookをAPI GatewayにPOST
9`. APIの裏にいるLambdaがdynamodbからgithubのcommit hashを取得
10`. githubのcommit status APIを叩いてAutifyをsuccess or failステータスにする

画像1

インフラ

技術選定
筆者の持つスキルからの感想ですが、こういった仕組みを作るにはFirebaseを使うのが一番簡単ですが、今回はCodePipelineとの連携が必要だったためlambdaを使用することにしました。またlambda以外にも外部からのwebhookを受信するAPI Gatewayとテストとgitcommitの紐付けをメタ情報として保存するdynamodbが必要なため、これらをまとめて管理できるフレームワークとしてserverless frameworkを選択しました。理由としてはserverless frameworkが最も早くこれらを構築できると思ったからです。(他の選択肢としてAmplifyやSAMがありました)

構築
インフラ(codepipelineやステージング環境は作られません)は下記のserverless.ymlを書いてdeployコマンドを実行するだけで用意できるという簡単さがserverless frameworkの魅力です。(webpackやbabelの設定等はまた必要ですがserverless frameworkのこちらのexampleをforkして作りました https://www.serverless.com/examples/aws-node-github-check/)

service: serverless-e2etest

plugins:
 - serverless-webpack

custom:
 webpack:
   webpackConfig: ./webpack.config.js
   includeModules: true

provider:
 name: aws
 runtime: nodejs12.x
 profile: default
 environment:
   DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
 iamRoleStatements:
   - Effect: 'Allow'
     Action:
       - 'codepipeline:PutJobSuccessResult'
       - 'codepipeline:PutJobFailureResult'
     Resource: "*"
   - Effect: Allow
     Action:
       - dynamodb:Query
       - dynamodb:Scan
       - dynamodb:GetItem
       - dynamodb:PutItem
       - dynamodb:UpdateItem
       - dynamodb:DeleteItem
     Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
 invokeAutify:
   handler: handler.invokeAutify
 autifyWebhookListener:
   handler: handler.autifyWebhookListener
   events:
     - http:
         path: webhook_handler
         method: post
         cors: true
 
resources:
 Resources:
   TestsDynamoDbTable:
     Type: 'AWS::DynamoDB::Table'
     DeletionPolicy: Retain
     Properties:
       AttributeDefinitions:
         -
           AttributeName: id
           AttributeType: S
       KeySchema:
         -
           AttributeName: id
           KeyType: HASH
       ProvisionedThroughput:
         ReadCapacityUnits: 1
         WriteCapacityUnits: 1
       TableName: ${self:provider.environment.DYNAMODB_TABLE}​

これで下記のインフラが完成します。

- Lambda(autify実行用)
- 処理をLambdaにProxyするAPI Gateway(webhook受信用)
- dynamodbのtable

実装

どういうコードを書いたかを簡単に紹介します。

Autifyを実行するLambda(invokeAutify)
codepipelineはステージング環境にdeploy完了後にこちらのLambdaを叩くようにしておきます。

実装に関してはserverless frameworkの下記exampleに修正を加えて作りました。https://www.serverless.com/examples/aws-node-github-check

dynamodbの操作を行うexampleはこちらが参考にできます。https://www.serverless.com/examples/aws-node-rest-api-with-dynamodb

export async function invokeAutify(event, context, callback) {

 async function putJobSuccess(message, jobId) {
   await codepipeline.putJobSuccessResult({}).promise();
   return callback(null, 'Job Success: Successfully reported hook results');
 }

 async function putJobFailure(message, jobId) {
   await codepipeline.putJobFailureResult({}).promise();
   return callback(null, 'Job Failed: Failure reported hook results');
 }

 const jobId = event['CodePipeline.job'].id;
 const { revision } = event['CodePipeline.job'].data.inputArtifacts[0];
 
 // codepipelineがLambdaステージにてユーザーパラメータにリポジトリ名とテストプランIDを設定しておく
 // {
 //   "repository": "example-organization/example-repo", 
 //   "autifyTestPlanId": "1000000"
 // }
 const { repository, autifyTestPlanId } = JSON.parse(event["CodePipeline.job"].data.actionConfiguration.configuration.UserParameters);
 const payload = githubPendingPayload(); // github status API用のobjectを構築
 try {
 
   // githubのstatusをペンディング状態に更新する(処理詳細は上記exampleを御覧ください)
   await updatePullRequestStatus(githubClient, payload, repository, revision);
 
  // AutifyでE2Eテストを実行(lambda環境変数にautifyのtokenを設定する必要があります)
   const params = {
       method: "POST",
       mode: "cors",
       headers: {
         'Authorization': `Bearer ${process.env.AUTIFY_TOKEN}`,
         'accept': 'application/json',
         "Content-Type":"application/json"
       },
       body: JSON.stringify({})
   };
   const res = await fetch(`https://app.autify.com/api/v1/schedules/${autifyTestPlanId}`, params);
   
   // 後にWebhookでautifyの実行結果と連動させるため、dynamodbにメタ情報を保存しておく
   const item = {
     TableName: process.env.DYNAMODB_TABLE,
     Item: {
       id: json.data.id,
       repository,
       revision,
       createdAt: timestamp,
       updatedAt: timestamp,
     },
   };
   await dynamoDb.put(item).promise();

   // こちらを処理の最後に呼ばないとcodepipelineが終了しません
   return await putJobSuccess("Tests passed.", jobId);
 } catch (e) {
   return await putJobFailure(e, jobId);
 }
}

Autifyからのwebhookを受けるAPI(autifyWebhookListener)
Autifyがテスト実行後に叩くAPIです。こちらもserverless frameworkのexampleを修正して実装しました。AutifyのWebhookシークレットをバリデーションする処理はほぼそのまま使えたのでおすすめです。https://www.serverless.com/examples/aws-node-github-webhook-listener/

export async function autifyWebhookListener(event, context, callback) {
 // 署名のバリデーションは割愛しています

 // webhookからautifyのメタ情報を取得
 const { id, status, url } = JSON.parse(event.body);

 try {
   // dynamodbからテストと関連するのgitのメタ情報を取得します
   const item = { TableName: process.env.DYNAMODB_TABLE, Key: { id } };
   const res = await dynamoDb.get(item).promise();
   const { repository, revision } = res.Item;

   // githubのstatus更新
   const payload = status === 'passed' ? githubSuccessPayload(url) : githubFailurePayload(url);
   const res2 = await updatePullRequestStatus(githubClient, payload, repository, revision);

   callback(null, {
     statusCode: 200,
     body: JSON.stringify(res.Item),
   });
 } catch (e) {
   return callback(null, e);
 }
};

まとめ

こういった仕組みを作るのに5年前だったらEC2にアプリケーションを立ち上げてsshしてから手動deployなどしなければならなかったのですが、今ではPCだけでこんなにも簡単にアプリケーションが作れてしまう時代なんだなと改めて思いました。

Autifyを使ってQAを自動化する事は長期的なサービス運営には大きなメリットだと思いますので同じような悩みを抱えている方のご参考になれば幸いです。

最後に

上記のようなインフラ技術とアプリケーションを組み合わせて構築できるバックエンドエンジニアを積極募集中ですので興味がありましたら募集要項を御覧ください!

最近スペースマーケットではお気に入りリストのシェア機能がリリースされました。せっかくなので私が実際に利用した1日合宿できるおすすめスペースまとめをシェアしておきます。会社で合宿を行う際にはぜひ参考にしてみてください。


よろしければサポートお願いします!