Confluenceページ自動作成に挑戦した


Confluenceとは

Confluenceを開発するAtlassianのホームページによると

Confluence は、知識を集め、共同作業するためのチームのワークスペースです。チームはダイナミックなページを使用して、さまざまなプロジェクトやアイデアに関する創造、取得、コラボレーションを行います

とありますが、簡単にいえば社内wikiです。私の所属するチームでは週二回のミーティングごとにメンバーの業務状況を整理した「ページ」を編集します。ページはミーティング後に最新版として保存され、これをコピーした新しいページを次回ミーティング用として作成します。ミーティングごとのメンバーの状況をページとして都度溜めていくように利用しています。

ページ作成を自動化したい

ミーティングを行うごとにページをコピーするルーティンワークは人の手によって行われていましたが、今回は公開されているConfluenceのAPIを使った自動化に挑戦します。

Confluence APIを呼び出してみる

以下ローカルでの実行環境は

DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.1 LTS"

です。

私たちの使うConfluenceはサーバー版なので、Confluence Server REST API(こちら)を使用します。
examplesを参照すると、サーバ上のページを取得するにはGETメソッド

This example shows how you can find a page by space key and title with history expanded to find the creator.

curl -u admin:admin -X GET "http://localhost:8080/confluence/rest/api/content?title=myPage%20Title
&spaceKey=TST&expand=history" | python -mjson.tool

新規ページを作成するにはPOSTメソッド

This example shows how you can create a new page with content in a specific space.

curl -u admin:admin -X POST -H 'Content-Type: application/json' -d '{"type":"page","title":"new page",
"space":{"key":"TST"},"body":{"storage":{"value":"<p>This is <br/> a new page</p>","representation":
"storage"}}}' http://localhost:8080/confluence/rest/api/content/ | python -mjson.tool

が用意されています。
今回は最新のミーティングで作成されたページをコピーして次回編集用として新規作成したいので、APIの関数として利用するのはこの二つだけでよさそうです。

atlassian-python-apiを使ってみる

pythonにはConfluence APIを呼び出すライブラリが用意されているので(こちら)、これを使ってpythonで実装しようと思います。
こちらのコマンドでインストールします。

pip install atlassian-python-api

このライブラリを用いてページのコピー&新規作成するコードのサンプルがこちら。

import datetime
#ログイン用のusernameとpasswordを格納したparameters.pyをimport
import parameters

#atlassianモジュールのConfluenceをimport
from atlassian import Confluence

#confluenceへの接続
confluence = Confluence(
    url=<URL>,
    username=parameters.username,
    password=parameters.password
    )

#直近のある曜日の日付を返す関数
def get_latest_weekday(date,weekday):
    days_behind = date.weekday() - weekday
    if days_behind < 0:
        days_behind += 7
    return date - datetime.timedelta(days_behind)

#現在の日付から一番最近の月曜日と木曜日を計算する。
current_date = datetime.datetime.now()
date_latest_monday = get_latest_weekday(current_date,0)
date_latest_thursday = get_latest_weekday(current_date,3)
if date_latest_monday > date_latest_thursday:
    copied_page_title = date_latest_monday
else:
    copied_page_title = date_latest_thursday

#pageをtitleで取得→idを入手→idでpageを取得
page_got_by_title = confluence.get_page_by_title('TEIREI',copied_page_title.strftime('%Y-%m-%d'))
page = confluence.get_page_by_id(page_got_by_title['id'],expand='body.storage')

#各パラメータの引き渡し
space = page['_expandable']['space'].split('/')[4]
body = page['body']['storage']['value']

#次のある曜日の日付を返す関数
def get_next_weekday(date,weekday):
    days_ahead = weekday - date.weekday()
    if days_ahead < 0:
        days_ahead += 7
    return date + datetime.timedelta(days_ahead)

#現在の日付から一番近い月曜日と木曜日を計算して、近いほうをdateとしてタイトルにする。
date_next_monday = get_next_weekday(current_date,0)
date_next_thursday = get_next_weekday(current_date,3)
if date_next_monday > date_next_thursday:
    title = date_next_thursday
else:
    title = date_next_monday

#ページを作成する。入手したページから変数をそれぞれコピーする。
status = confluence.create_page(space, title.strftime('%Y-%m-%d'), body)
print(status)

ページのタイトルはミーティングの日付に設定してあるため、

  1. 直近の月曜か木曜の日付を計算(最新のページタイトル)
  2. この日付を使ってページをコピーし、タイトルを取得
  3. このタイトルから次のミーティングの日付を計算
  4. 作成するページのタイトルに設定

という順序で取得と作成を行っています。
これで、ローカルから実行すれば自動でページのコピーを行えるプログラムが作成できました。運用面的にはプログラムは個人の端末においておくのは何かと困りそうなので、チームの共有スペースに置くようにしたいと思います。

クラウドで自動化

今回Confluenceのサーバは、Microsoft Azure上のvnet内にコンテナとして建ててあります。そのため自動でAPIを呼び出す仕組みはAzureのサービスで作ることにします。
ローカルからプログラムを実行するときのイメージはこんな感じ。

Azure Functionsを使う

Azureでプログラムを定期実行する方法はたくさんありますが、サーバーレスで安価なAzure Functionsを使ってみます。こんなイメージです。

サービスエンドポイントとは、azure functionsをインターネット非公開とする代わりにVNet内に取り込んで内部で使用できるようにする機能です。

こちらの通りにvscodeの拡張機能を使って、ローカルでFunctionを開発することができます。

vscodeではこのようにローカルで作ったFunctionとAzure Portalで作成したFunction App(Function実行のホスト)の両方が見えるようになります。
Functionをローカルでテストしたり、完成したFunctionをAzureにデプロイすることができます。例えばHttpトリガーのFunctionを作成するとこのようなファイルが自動で生成されます。

実行時に発行されるURLを叩くと、自動で__init__.pyが実行されるようになっています。

__init__.py
import logging

import azure.functions as func


def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.",
             status_code=200
        )

つまりこの__init__.pyで実行されるmain関数を書き換えることで、ローカルで実行したようにConfluence APIを呼び出すことができます。

Key Vaultでパスワード管理

共有スペースにこのコードを置く場合、Confluenceに接続するためのユーザーネームやパスワードは直に書くわけにいかないので、Key VaultというAzureのサービスを利用します。Azure Portal上でリソースとしてキーコンテナを作成し、シークレットにusernameとpasswordを生成します。参照:https://daniel-krzyczkowski.github.io/Integrate-Key-Vault-Secrets-With-Azure-Functions/

ここで作成した機密情報は、こちらを参考にFunctionからのアクセスを有効にすることで、環境変数として__init__.pyで使うことができるようになります。

VNet統合

ローカルでのテストに成功し、クラウドで実行するためにパス管理するサービスとも連携できたのでいよいよFunctionをAzureにデプロイしてみます。

と、ここで思わぬ事態が発生。。

VNet統合には先述したサービスエンドポイントを利用する必要がありますが、この機能の利用にはAzure Functions Premiumプランの購入が必要と分かりました。Premiumプランでは最低1インスタンスのFunctionをpre-warnedとして常に起動しておくことになっており、このため固定料金として$178.85/monthがかかってしまいます(ここで計算)。wikiページの自動作成のためだけにこれだけの金額を費やすことはできません。。残念ながらAzure FunctionsをVNet統合して自動化する方法は諦めることになりました。

今後について

Azure VNet内のConfluenceに対し、そのAPIを使うことでローカルからページのコピーや作成が自動でできました。またAzure FunctionsやKey Vaultを使えばAzure内で完全自動化できそうであるということもわかりました。
今回は予算の関係でこの機能の利用はパスすることとなりましたが、Azureで定期的にAPIを呼び出す仕組みはほかのサービスでも可能です。

  • Azure Logic Appsを利用して定期的にプログラムを実行することが可能です。VNet内で利用するにはAzure ISE(統合サービス環境)という環境が必要になりますが、こちらも高額です。

  • Azure VMをAzure Functionsで定期的に起動し(起動するだけならVNet外からでもできる)、自動的にプログラムを実行する方法。必要な時のみVMを起動するので安価で済みますが、OSのアップデートなど常に環境に気を配っておく必要があり、運用コストがかかります。

  • Azure Container Instances(ACI)でも同じように定期実行が可能です。API呼び出し用コンテナを作成し、これを起動するタイマーとしてAzure Functionsを利用します。

この中ではACIを利用する方法が安価で運用も簡単そうです。いつか機会があればこの方法で自動化できた際はまたご紹介します。