Serverlessで始めるBacklog API

Backlogは、ブラウザでの操作のほぼすべての機能をAPIで提供しています。Backlog APIを皆様はご活用してますでしょうか?ヌーラボでも、日頃の業務を自動化するためにBacklog APIをフル活用しています。今回は私の仕事で利用する機会のあったBacklog APIについてご紹介させて頂きます。

Backlogには「ファイル共有」というファイルサーバの機能があります。このファイルサーバーにアップロードされたファイルの一部を、S3(AmazonS3)へ自動的に同期したい、という思いがありました。

この作業をBacklog APIとAWS Lambdaを使って自動化しましたので、今回の記事では、Bakclog APIの設定からAWS Lambdaでのプログラミング、そして、Serverless Frameworkを使った自動デプロイの方法まで詳しくご紹介します。

構成

では今回の仕組みを構成する要素について説明していきます。

  • AWS Lambda
  • Serverless Framework
  • Backlog API
  • Backlog Webhook

AWS Lambda

AWSで発生した特定のイベントに対して実行したい処理をプログラムするだけで実行可能なアプリケーションが作成できます。実際動く環境にはサーバーが立ち上がりますが、そこはあまり意識しないでプログラムを記述するだけで作成できます。そして今回は、BacklogのWebhookにAWS API Gateway(API Gateway)で作成したURLを登録して、API Gatewayが受けたイベントをLambdaで処理します。詳しくは AWS Lamda 開発者ガイドを参照ください。

Serverless Framework

Lambda、API Gateway、DynamoDBなどを作成、管理、デプロイできるツールです。実行ファイルと設定ファイルを準備するだけで利用できます。詳しくは Serverless frameworkを参照ください。

※事前に AWSアカウントの作成AWS CLIの設定、 npmのインストールSeverless Frameworkのインストールが必要です。

Backlog API

課題、Wiki、ファイルの追加や取得を始め、プロジェクトやユーザーの管理などブラウザ上のBacklogでできる操作の大部分をAPIから行うことができます。詳しくは APIを参照ください。

今回利用するAPI

Backlog Webhook

プロジェクトで発生したイベントの情報をリアルタイムに指定されたURL(サーバ)へHTTP POSTする機能です。詳しくは Webhookを参照ください。

実際に作ってみる

まずはServerless Frameworkのテンプレートを作成します。今回はaws-goのテンプレートを使用することにします。

$ serverless create --template aws-go --path serverless-sample 

serverless-sampleのディレクトリができて、その中にgoのテンプレートが展開されます。

Makefile # makefile
serverless.yml # 設定ファイル

実行ファイルの作成

実行するコードを見ていきます。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

type Response struct {
	Message string
}

type Content struct {
	Name string
	Dir  string
	Id   int
}

type Project struct {
	ProjectKey string
	Name       string
}

type Request struct {
	CreatedAt *time.Time json:"createdAt"
	Project   *Project
	Content   *Content
}

var (
	BacklogApiKey       = os.Getenv("BACKLOG_API_KEY")
	Bucket              = os.Getenv("BUCKET")
	MonitoringDirectory = "/sample" 	
	SourceDirectory     = "/target"
)

パッケージ、インポート設定、レスポンス、リクエストの構造体、そしてBacklogApiKey、Bucketは環境変数から取得しています。MonitoringDirectoryは指定フォルダ配下を同期する場合に設定(共有ディレクトリのsample以下を同期対象)します。SourceDirectoryはS3のBucket以下の出力先の設定です。

続いてメインの処理です。

func Handler(request events.APIGatewayProxyRequest) (*Response, error) {

	var req Request
	// リクエストのjsonをパース
	err := json.Unmarshal([]byte(request.Body), &req)
	if err != nil {
		return &Response{
			Message: fmt.Sprintf("[error] リクエストのJson形式が正しくありません。 \n request=%s", request.Body),
		}, err
	}

	// 指定フォルダ配下を同期したい場合にMonitoringDirectoryを設定します
	// 指定フォルダのチェック
	if !strings.HasPrefix(req.Content.Dir, MonitoringDirectory) {
		return &Response{
			Message: fmt.Sprintf("[info] 同期対象ディレクトリではありません。 \n request=%s", request.Body),
		}, nil
	}

	// Backlogからのファイルダウンロード
	body, err := downloadFromBacklog(req.Project.ProjectKey, req.Content.Id)
	if err != nil {
		return &Response{
			Message: fmt.Sprintf("[error] Backlogからのダウンロードに失敗しました。 \n projectKey=%s, fileId=%d", req.Project.ProjectKey, req.Content.Id),
		}, err
	}

	// S3のファイルキーの指定
	fileKey := SourceDirectory + strings.Replace(req.Content.Dir, MonitoringDirectory, "", 1) + req.Content.Name
    
	result, err := uploadS3(fileKey, body)
	if err != nil {
		return &Response{
			Message: fmt.Sprintf("[error] S3へのアップロードに失敗しました。 \n fileKey=%s", fileKey),
		}, err
	}

	return &Response{
		Message: fmt.Sprintf("[info] ファイルの同期に成功しました。出力元: %s 出力先: %s", req.Content.Name, result.Location),
	}, nil
}

処理の流れは以下の通りです。

  • リクエストを扱いやすいようにRequestの構造体にパースする
  • 共有ファイルのパス(MonitoringDirectory)をチェックする
  • Backlog APIを利用してファイルをダウンロードする
  • ダウンロードしたファイルをS3にアップロードする

そしてBacklogからのダウンロードと、S3へのアップロード処理です。

func downloadFromBacklog(projectKey string, fileId int) ([]byte, error) {

	backlogAPIFormat := "https://sample.backlog.com/api/v2/projects/%s/files/%d"
	values := url.Values{}
	values.Add("apiKey", BacklogApiKey)
	apiURL := fmt.Sprintf(backlogAPIFormat, projectKey, fileId) + "?" + values.Encode()

	res, err := http.Get(apiURL)
	defer res.Body.Close()
	if err != nil {
		return nil, err
	}
	return ioutil.ReadAll(resp.Body)
}

func uploadS3(fileKey string, byteData []byte) (*s3manager.UploadOutput, error) {

	sess := session.Must(session.NewSession())
	svc := s3manager.NewUploader(sess)

	return svc.Upload(&s3manager.UploadInput{
		Bucket: aws.String(Bucket),
		Key:    aws.String(fileKey),
		Body:   bytes.NewReader(byteData),
	})
}

ダウンロード処理のsample.backlog.comは環境に合わせて変更してください。

最後におきまりのコードを追加します。

func main() {
	lambda.Start(Handler)
}

コードはこれで完成です。ソースファイルはserverless-sampleディレクトリ以下にmain.goというファイルを作成します。

次にMakefileを変更します。

build:
	go get github.com/aws/aws-lambda-go/lambda
	go get github.com/aws/aws-lambda-go/events
	go get github.com/aws/aws-sdk-go/aws
	go get github.com/aws/aws-sdk-go/aws/session
	go get github.com/aws/aws-sdk-go/service/s3/s3manager
	env GOOS=linux go build -ldflags="-s -w" -o  bin/replicateFile main.go # 実行ファイルの出力先をbin/replicateFileとしてビルド

依存ライブリの設定と、buildして作成される実行ファイルの出力先を設定しています。

そして実行します。

$ make

これで実行ファイル(bin/replicateFile)の準備は完了です。

設定ファイルの作成

続いて設定ファイルのserverless.ymlを変更します。

provider:
  name: aws
  runtime: go1.x
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:PutObject"
      Resource: { "Fn::Join": ["", ["arn:aws:s3:::BUCKET_NAME/*" ] ] }

変更箇所1つ目は、iamRoleStatements以下の内容です。Lambdaと結びつくIAMロールにS3へのPUT権限を付与します。BUCKET_NAMEに対象のbucket名を設定します。

functions:
  replicateFile:
    handler: bin/replicateFile # 実行ファイルの設定
    environment: # 環境変数
      BACKLOG_API_KEY: $BACKLOG_API_KEY # APIのkey
      BUCKET: $BUCKET # 出力先のS3のbucket
    events:
      - http: # API Gatewayの設定
          path: replicateS3 # API URL
          method: post # methodの設定

変更箇所2つ目は、functions以下の内容です。

  • サービスとなるlambdaの名称(replicateFile)
  • 実行ファイル(bin/replicateFile)
  • 環境変数としてBacklogのファイルをダウンロードする際に利用するAPI key、出力先のS3のBucket名
  • 今回はAPI Gatewayのイベントを利用するので- httpの設定とURLのpathとmethodを指定

BACKLOG_API_KEYの設定はAPIキーについてを参照ください。

これでAWS Lambdaと、AWS API Gatewayのサービスが立ち上がる設定ファイルの準備は完了です。 

準備ができたのでServerless Frameworkのdeployを行ってAWSの環境を構築します。

$ serverless deploy

結果として、サービスの情報が表示されますので確認してください。

## bash
Service Information
service: serverless-sample
stage: dev
region: us-east-1
stack: serverless-sample-dev
api keys:
  None
endpoints:
  POST - https://〇〇〇.execute-api.us-east-1.amazonaws.com/dev/replicateS3
functions:
  replicateFile: serverless-sample2-dev-replicateFile

endpointsのPOST - https://〇〇〇.execute-api.us-east-1.amazonaws.com/dev/replicateS3は、API Gatewayの情報となります。URLはBacklogのWebhookに設定します。functionsのreplicateFile: serverless-sample2-dev-replicateFileはLambdaの名称です。 どちらもAWSのコンソールから確認可能です。

Webhookの設定

最後にBacklog Webhookの設定を行います。

  • API GatewayのエンドポイントをWebhook URLに設定する
  • 共有ファイルに関するイベントのファイルの追加ファイルの更新にチェックを入れる

これですべて完了です。

動作の確認

まずはBacklogの共有ファイルにファイルをアップロードします。 今回はserverlessTest.txtというファイルで動きをみていきます。

プログラムで設定したMonitoringDirectoryのsampleフォルダ配下にファイルをアップロードします。

画像ようにアップロードされていることが確認できます。

次にS3へ同期されていることを確認します。

S3のコンソール画面を開き、プログラムで設定したSourceDirectoryのtargetフォルダ配下を確認するとファイルが同期されていることが確認できます。

今回の目に見える成果物としてはこれだけになります。他の方法としては、AWSにログインしてファイルをアップロードしたり、もしくはaws cliで認証情報を設定してコンソールからコマンドで操作したりすることも可能です。ただ開発者ではない方にとってはハードルが高く、運用して頂くのは難しいかもしれません。今回はそうのような状況でしたので自動化しました。

皆様の運用の中でも同様な場合があると思います、そのような際にはBacklog APIを使ったServerlessなサービスの構築の参考にして頂ければと思います。

まとめ

いかがでしたでしょうか。

Backlog APIとServerless Frameworkを活用することで、ワークフローは改善できる可能性があります。これを機会にBacklogAPIに興味を持って頂ければ幸いです。

長くなりましたが、ありがとうございました。


お知らせ

明日8/2(木)開催のkintone devCamp 2018で、Backlog SREの吉澤が、AWS Lambdaを用いたAPI連携に関するハンズオンに登壇します。Backlog APIとAWS Lambdaの連携に関しては、このハンズオンのプレゼン資料およびサンプルコードもぜひご参考ください。

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる