メンテナンスし辛いMovableTypeをHeadlessCMSとして使えるようにAWS DynamoDB, S3を使って改修する
10年漬けのMovableTypeとか触りたくないですよね
そして10年も記事がたまってると静的ファイルを生成する仕組みなので全再構築するとサーバースペックによっては1日以上かかり何万件・何十万件ものファイルが生成されたりします。
MobableTypeとウェブサーバーが別の場合は、rsyncで同期することが多いと思いますが、rsyncに負荷がかかりアクセスがない時でも重くなったりします。
また、MobableType管理外のファイルと管理下のファイルの見分けがつかないことや、生成されたHTMLもPHPを使って中途半端にSSRされていたり、運用コストが大変しんどくなっていきます。
MovableTypeのMTテンプレート自体もどんな処理がされるのか一目でわからなかったり、記事Modelが曖昧でバグが出やすくメンテナンスがしづらい欠点があります。テンプレートからバッチ処理が走るような記述が書かれていることもあります。
Data APIを使ったAjaxの呼び出しもMovableTypeサーバー本体に負荷がかかりやすく重くなることが多いです。
そうした中で記事内のドキュメントデータを構造把握しやすく高負荷でも耐えるヘッドレスCMSの仕組みをMovableTypeに導入していきましょうというのが本記事です。
ヘッドレスCMS化としてまずMovableTypeに以下の機能をつけます。
- MovableTypeから画像をアップロードするとS3にアップロード
- 記事の再構築・静的ファイルの生成を止める
- 記事・カテゴリ・サイトの投稿、変更、削除が行われたらWebhook URLに通知する
MovableTypeから画像がS3にアップロードされて、静的ファイルの生成を止めれば
あとはMovableTypeにあるData APIを使えばHeadlessCMSとして使えます。
ただ、Data APIをそのまま使うとMovableTypeサーバー負荷となり投稿そのものができなくなることがあります。
そこでWebhookを利用してAWSのDynamoDBに取得した記事データを入れDynamoDBから参照するように変更します。
カテゴリ、サイト情報も同様にDynamoDB経由で取得できるようにします。
DynamoDBに入れておけばAppSyncでGraphQL経由で使うようにすることもできますし、負荷にも強くなりオススメです。。
これで記事投稿するまでがMovableTypeの仕事になり、SSRや静的生成の依存がMovableTypeからDynamoDBとS3となる骨格が出来上がります。
実際に開発環境を作ってDynamoDBからSSRするまでをやってみましょう。
開発環境をつくる
Dockerを使って開発環境を作りましょう。
既存の運用中の開発環境をいきなり用意しようとするとそれなりに大掛かりになります、まずは初期状態のMT環境を用意しましょう。
今回は古いMTを改修するということでMT6.7系のMovableTypeの環境を作成します。
Dockerのインストールは
などを参照してみてください。
Dockerの準備
Docker Composeを使ってローカルで動く雛形をGithubに用意しましたのでこれを使います。
git clone git@github.com:mozquito/mt_docker.git
下記のようなファイルが展開されます。
├── docker
│ ├── httpd
│ │ ├── Dockerfile
│ │ ├── default.conf
│ │ └── docker-php-entrypoint
│ └── movabletype
│ ├── Dockerfile
│ └── docker-mt-entrypoint
├── README.txt
├── docker-compose.yml
├── mt-config.cgi
└── volumes
├── plugins
└── web1
MovableTypeはライセンスの関係で事前にソースをダウンロードしたMovableTypeをもとにDockerをビルドする必要があります。
- ダウンロードしたMovableTypeをmtとリネーム
- 直下にあるmt-config.cgiをmtフォルダに移動
-
docker/movabletype
の中にmtフォルダを移動
これで一旦準備が整いましたのでdocker-compose.ymlがあるディレクトリに移動します。
docker-compose up
としhttp://localhost/mt/mt.cgi にアクセスしてみてください。
MovableTypeが立ち上がります。
docker-compose.ymlの構成では
- MovableTypeをPSGIで起動
- Apache,Mysqlを起動
- enviromentsのパラメータでmt-config.cgiの設定書き換え
- localhostがDocker Apache上の
/var/www/web1
を見るように設定 - Reverse Proxyを使いhttp://localhost/mt/mt.cgi でMovabletypeが動くように設定
- Apache,MovableTypeの
/var/www/web1
をvolumes設定でローカル上の./volumes/web1
と同期
などの設定が行われています。
MovableTypeの初期設定
ユーザーを作成します
サイトを設定します
ウェブサイトパスを/var/www/web1
として後はそのままにしてインストールします。
インストールが終わったら一旦全再構築をして表示されるか確かめます。
ローカル側の./volumes/web1
にもファイルが生成されていたらインストール完了です。
Pluginをインストール
- MovableTypeから画像をアップロードするとS3に同期
- 記事の再構築と静的ファイルの生成を止める
- 記事・カテゴリ・サイトの投稿、変更、削除が行われたらWebhookで通知する
この3つの機能をPlugin化してでっち上げていますので今回はこれを使います。
こちらをクリックしてダウンロードします。
ダウンロードして解凍したフォルダをHeadlessCMSPack(適当です)とリネームし./volumes/plugins
にディレクトリを移動させます。
システムのプラグイン設定に移動しプラグインを利用しないボタンを押してプラグインを無効化します。
するとプラグインが全てOffになるとともにHeadlessCMSPackという項目が出てきます(反映されない場合は再読み込みしてください)。
プラグインを利用するボタンを押しプラグインを有効化させたあと、HeadlessCMSPackも有効にします(有効ボタンを押した後、再読み込みする後に反映されることが多いです)。
まだ詳細設定がありますがこれでHeadlessCMSPackプラグインのインストールができました。
静的ファイルが生成されないか確認
HeadlessCMSPackプラグインをインストールしてプラグインを有効にすると、再構築やテンプレートからファイルが書き出されなくなります。ローカルの./volumes/web1
内のファイルやディレクトリを全て削除しFirst Websiteを全再構築してみます。全再構築後./volumes/web1
内には何もファイルが作成されてないはずです。First Websiteのデザイン(テンプレート)に移動し
インデックステンプレートを全て公開にしてもファイルが作成されないことを確認します。
これで見かけ上はHeadlessCMSとなりました。
S3に画像を同期
次にS3に画像がアップロードされる仕組みを作ります。
docker-composer.ymlのあるディレクトリに移動し
serverless create --template aws-nodejs --path mt-sls
とコマンドを叩き、ServerlessFrameworkの雛形を作成します。mt-sls
は適当につけたServerlessFramework上での名前になります。
cd ./mt-sls
と移動しserverless.ymlを開き下記のように修正します。
service: mt-sls
frameworkVersion: '2'
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
region: ap-northeast-1
resources:
Resources:
MtUpload:
Type: AWS::S3::Bucket
Properties:
BucketName: movabletype-xxxx # xxxxにはS3上でBucketNameが一意となる文字を入れる
編集し終えたら
sls deploy
とコマンドを叩きS3のバケットを作成します。
今回はlocalhost上からS3にアップロードするため、AmazonS3FullAccess権限のあるユーザーを作成し アクセスキーとシークレットアクセスキーを用意します。
用意ができたらサイト、First Website側のプラグイン設定にいきHeadlessCMSPackの詳細設定を開きます。
アクセスキーとシークレットキーと順に入れ、
Regionにはap-northeast-1
Bucektにはserverless.ymlのBucketNameに入れた値を入力して保存してください。
保存し終えたらFirst Websiteのアイテム(新規)に移動します。
ここからテストとして適当な画像をアップロードしてS3上にアップロードされていたら設定完了です。
DynamoDBを作成
MovableTypeからDynamoDBに記事を保存・更新・削除される仕組みを作ります。
- テスト投稿する
- DataAPIからどのようなJSONが取得できるか確認する
- DynamoDBを設計する
という手順を行います。
テスト投稿する
どのようなデータが取れるのかテスト投稿します。
- カテゴリをtest1、test2と作成しチェックボックスをつけます。
- タグも@page,tag1,tag2,タグ3と入れてみます。
- 本文に画像と適当な文字列
- 続きにも適当な文字列
- 記事アイテムにも画像をいくつかセットします。
以上を入力して投稿してみましょう。
記事はプラグインの機能により出力されないのでData APIで内容を確認します。
Data APIの確認
まずFirst Websiteの管理画面の左メニューから設定→Webサービスと移動します。
Data API の設定にチェックが入ってるかどうかを確認します。
チェックが入ってなければチェックして保存します。
記事データ
http://localhost/mt/mt-data-api.cgi/v3/sites/{site_id}/entries/{entry_id}
サイト情報
http://localhost/mt/mt-data-api.cgi/v3/sites/{site_id}
カテゴリ情報
http://localhost/mt/mt-data-api.cgi/v3/sites/{site_id}/categories/{category_id}
でアクセスできます。
初投稿の記事は初期値のままだとsite_idが1でentry_idが3となります(初期値ではウェブページ用の記事が2本下書投稿されてます)。
http://localhost/mt/mt-data-api.cgi/v3/sites/1/entries/3
にアクセスして値を確認します。
実際の記事データJSON
{
"id": 3,
"class": "entry",
"title": "test",
"permalink": "http://localhost/2021/01/test.html",
"body": "<p>test</p>\n<p>test</p>",
"more": "<p>test<img alt=\"ByiHDionTA4.jpeg\" src=\"http://localhost/ByiHDionTA4.jpeg\" width=\"1080\" height=\"809\" class=\"mt-image-none\" /></p>",
"excerpt": "test test...",
"author": {
"displayName": "admin",
"userpicUrl": null
},
"keywords": "",
"tags": [
"@page",
"tag1",
"tag2",
"タグ3"
],
"status": "Publish",
"basename": "test",
"customFields": [],
"categories": [
{
"parent": "0",
"id": 2,
"label": "test1"
},
{
"id": 3,
"parent": "0",
"label": "test2"
}
],
"blog": {
"id": "1"
},
"createdDate": "2021-01-14T04:03:11+09:00",
"modifiedDate": "2021-01-14T04:09:55+09:00",
"date": "2021-01-14T03:55:42+09:00"
"assets": [
{
"mimeType": "image/jpeg",
"fileExtension": "jpeg",
"blog": {
"id": "1"
},
"label": "ByiHDionTA4.jpeg",
"parent": null,
"tags": [],
"id": "21",
"description": null,
"createdBy": {
"displayName": "admin",
"userpicUrl": null
},
"updatable": false,
"class": "image",
"url": "http://localhost/ByiHDionTA4.jpeg",
"customFields": [
{
"value": null,
"basename": "assets"
}
],
"createdDate": "2021-01-14T03:57:39+09:00",
"meta": {
"height": "809",
"fileSize": 52536,
"width": "1080"
},
"type": "画像",
"filename": "ByiHDionTA4.jpeg",
"modifiedDate": "2021-01-14T03:57:39+09:00"
},
{
"description": null,
"id": "22",
"parent": null,
"tags": [],
"updatable": false,
"class": "image",
"createdBy": {
"userpicUrl": null,
"displayName": "admin"
},
"blog": {
"id": "1"
},
"fileExtension": "jpeg",
"mimeType": "image/jpeg",
"label": "B-HOT1JnVHi.jpeg",
"meta": {
"height": "1350",
"fileSize": 91264,
"width": "1080"
},
"type": "画像",
"modifiedDate": "2021-01-14T04:09:44+09:00",
"filename": "B-HOT1JnVHi.jpeg",
"customFields": [
{
"value": null,
"basename": "assets"
}
],
"createdDate": "2021-01-14T04:09:44+09:00",
"url": "http://localhost/B-HOT1JnVHi.jpeg"
},
{
"tags": [],
"parent": null,
"id": "23",
"description": null,
"createdBy": {
"userpicUrl": null,
"displayName": "admin"
},
"updatable": false,
"class": "image",
"mimeType": "image/jpeg",
"fileExtension": "jpeg",
"blog": {
"id": "1"
},
"label": "B-HOT1JHjS8.jpeg",
"meta": {
"height": "1350",
"width": "1080",
"fileSize": 88421
},
"type": "画像",
"modifiedDate": "2021-01-14T04:09:44+09:00",
"filename": "B-HOT1JHjS8.jpeg",
"url": "http://localhost/B-HOT1JHjS8.jpeg",
"customFields": [
{
"basename": "assets",
"value": null
}
],
"createdDate": "2021-01-14T04:09:44+09:00"
},
{
"blog": {
"id": "1"
},
"fileExtension": "jpeg",
"mimeType": "image/jpeg",
"label": "B-KSEmmn2cw.jpeg",
"description": null,
"id": "24",
"tags": [],
"parent": null,
"updatable": false,
"createdBy": {
"userpicUrl": null,
"displayName": "admin"
},
"class": "image",
"customFields": [
{
"basename": "assets",
"value": null
}
],
"createdDate": "2021-01-14T04:09:45+09:00",
"url": "http://localhost/B-KSEmmn2cw.jpeg",
"type": "画像",
"meta": {
"height": "1350",
"width": "1080",
"fileSize": 112175
},
"modifiedDate": "2021-01-14T04:09:45+09:00",
"filename": "B-KSEmmn2cw.jpeg"
}
],
"pingsSentUrl": [],
"trackbackCount": "0",
"allowTrackbacks": false,
"updatable": false,
"comments": [],
"allowComments": true,
"trackbacks": [],
"commentCount": "0",
}
"pingsSentUrl": [],
"trackbackCount": "0",
"allowTrackbacks": false,
"updatable": false,
"comments": [],
"allowComments": true,
"trackbacks": [],
"commentCount": "0",
まず上記の値は、今回はトラックバックやコメント機能は使わないので考えません。
記事のカテゴリは
"categories": [
{
"parent": "0",
"id": 2,
"label": "test1"
},
{
"id": 3,
"parent": "0",
"label": "test2"
}
],
となっておりカスタムフィールドやディスクリプションなどの値は取れておらずでラベルと親子のidのみの出力となっています。記事のデザインや構成によっては別途DataAPIでカテゴリ情報を取りに行く必要があります。
"tags": [
"@page",
"tag1",
"tag2",
"タグ3"
],
タグについては列挙されているだけでidなども無い単純な仕組みの様です。
@から始まるタグはシステムタグとしてテンプレートなどで使われるタグなので、記事に表示させないようにする必要があります。
あと大きなところではAssets、画像となります。
"assets": [
{
"id": "21",
"mimeType": "image/jpeg",
"fileExtension": "jpeg",
"blog": {
"id": "1"
},
"label": "ByiHDionTA4.jpeg",
"parent": null,
"tags": [],
"description": null,
"createdBy": {
"displayName": "admin",
"userpicUrl": null
},
"updatable": false,
"class": "image",
"url": "http://localhost/ByiHDionTA4.jpeg",
"customFields": [
],
"createdDate": "2021-01-14T03:57:39+09:00",
"meta": {
"height": "809",
"fileSize": 52536,
"width": "1080"
},
"type": "画像",
"filename": "ByiHDionTA4.jpeg",
"modifiedDate": "2021-01-14T03:57:39+09:00"
},
...
],
assetsのJSONを見るとasset情報自体は完全な形であるようです。
サムネイル画像は作成されていますがこちらには含まれていないので扱いをどうするか決める必要があります。
続いてサイト情報を見てみます。
http://localhost/mt/mt-data-api.cgi/v3/sites/1
{
"parent": null,
"id": "1",
"class": "website",
"name": "First Website",
"host": "localhost",
"url": "http://localhost/",
"relativeUrl": "/",
"language": "ja",
"description": null,
"archiveUrl": "http://localhost/",
"timezone": "+09:00",
"modifiedDate": "2021-01-13T17:55:56+09:00",
"createdDate": "2021-01-13T17:55:08+09:00",
"customFields": [],
"ccLicenseUrl": "",
"updatable": false,
"dateLanguage": "ja",
"pingOthers": [],
"serverOffset": "9",
"createdBy": {
"displayName": "admin",
"userpicUrl": null
},
"modifiedBy": {
"userpicUrl": null,
"displayName": "admin"
},
"ccLicenseImage": ""
}
取り敢えずタイトルとURLとDescriptionとカスタムフィールドあたりを抑えておけばよさそうです。
カテゴリ情報も見てみます。
http://localhost/mt/mt-data-api.cgi/v3/sites/1/categories/2
{
"modifiedDate": "2021-01-14T03:55:50+09:00",
"createdDate": "2021-01-14T03:55:50+09:00",
"customFields": [
],
"updatable": false,
"createdBy": {
"displayName": "admin",
"userpicUrl": null
},
"class": "category",
"description": null,
"id": 2,
"archiveLink": "http://localhost/test1/index.html",
"parent": "0",
"label": "test1",
"blog": {
"id": "1"
},
"basename": "test1"
}
archiveLinkのURLをどうするか以外はAssetと同じ様な構成の様です。
DynamoDBの設計
先ほどのData APIから取れる情報を元にDynamoDBの設計をしていきます。
テーブル数とインデックスを決めます。
Entry Table
まず記事とページですがこちらはもともとmt_entryというテーブルにあるのでEntryテーブルを作って入れることにします。
インデックスについてはentry_idとpermalinkをどちらからでも取得できるように作ります。
一覧についてはカテゴリやタグなど複数設定できるので一覧用のテーブルを作ります。
また、一覧から記事データをgetBatchされるのを考えentry_id(id)をプライマリキー、permalinkをGSIで設定します(GSIはscanとqueryでしか使えない)。
Hash Key |
---|
id(Number) |
permalink-index(GSI)
Hash Key |
---|
Permalink(String) |
ArchiveList Table
次に一覧ですがこちらはタグ、カテゴリ、月別アーカイブと3種類のアーカイブが考えられます。
Hash KeyとRange Keyの組み合わせで1つのテーブルにまとめれそうなので
MTのテンプレートタグの名前に合わせてArchiveListテーブルとして一つにまとめます。
Hash Key | Range Key | attribute |
---|---|---|
archive_key(String) | date | id(entry_id |
archive_keyには下記のような値が入ります。
archive_key |
---|
tag_{blog_id}_{tagname} |
category_{blog_id}_{category_id} |
folder_{folder_id} |
month_{YYYY-MM} |
month_{blog_id}_{YYYY-MM} |
month_{blog_id}{category_id}{YYYY-MM} |
entry-index(GSI)
Hash Key | Range Key |
---|---|
id | date |
Asset Table
AssetですがAssetテーブルとします。今回では使わない。
Hash Key |
---|
id(asset_id) (Number) |
Meta Table
残りのカテゴリ情報とサイト情報はKey-Valueとしてしか使わない予定なのでMetaテーブルとして1つにまとめます。そのほかに必要なKey-Valueデータがあればこちらに入れるようにします。
Hash Key |
---|
meta_key (String) |
meta_keyの値は下記のような形にします。 |
meta_key |
------------------- |
site_{site_id} |
category_{catgory_id} |
その他あれば拡張する |
mt-sls/serverless.ymlを開きDynamoDBの構成を編集します。
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
region: ap-northeast-1
resources:
Resources:
MtUpload:
Type: AWS::S3::Bucket
Properties:
BucketName: movabletype-xxxx # xxxxにはS3上でBucketNameが一意となる文字を入れる
EntryTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Entry
AttributeDefinitions:
- AttributeName: permalink
AttributeType: S
- AttributeName: id
AttributeType: N
KeySchema:
- AttributeName: id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: Entry
KeySchema:
- AttributeName: permalink
KeyType: HASH
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
ArchiveListTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ArchiveList
AttributeDefinitions:
-
AttributeName: archive_key
AttributeType: S
-
AttributeName: date
AttributeType: S
KeySchema:
-
AttributeName: archive_key
KeyType: HASH
-
AttributeName: date
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
AssetTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Asset
AttributeDefinitions:
-
AttributeName: id
AttributeType: N
KeySchema:
-
AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
MetaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Meta
AttributeDefinitions:
-
AttributeName: meta_key
AttributeType: S
KeySchema:
-
AttributeName: meta_key
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
Serverless Frameworkからdeployします。
sls deploy
これでDynamoDBのTableが完成しました。
DynamoDBとMovableTypeの連携
次にMovableTypeに投稿したらDynamoDBに記事データが挿入される仕組みを作ります。まずWebhook用URL作成しましょう。
Webhook URLを作成する
Webhook URLをAPI GatewayとLambdaを使ってURLを作成します。
URL | 機能 |
---|---|
/entry/update | DynamoDBに記事データを作成・アップデート 一覧更新 |
/entry/delete | DynamoDBの記事データを削除 一覧から削除 |
/category/update | DynamoDBにカテゴリデータを作成・アップデート |
/category/delete | DynamoDBにカテゴリデータを削除 |
/site/update | DynamoDBにサイトデータを作成・アップデート |
/site/delete | DynamoDBにサイトデータを削除 |
/asset/update | DynamoDBにAssetデータを作成・アップデート |
/asset/delete | DynamoDBにAssetデータを削除、S3の画像も削除 |
以上のようなwebhook urlを作ります。
まずはURLを作成してみましょう。
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
region: ap-northeast-1
この下にLambdaの記述とeventの設定を書きます。
functions:
webhook:
handler: handler.hello
events:
- httpApi:
path: /entry/update
method: POST
- httpApi:
path: /entry/delete
method: POST
- httpApi:
path: /category/update
method: POST
- httpApi:
path: /category/delete
method: POST
- httpApi:
path: /site/update
method: POST
- httpApi:
path: /site/delete
method: POST
- httpApi:
path: /asset/update
method: POST
- httpApi:
path: /asset/delete
method: POST
まずは一通りのURLを作成します。機能は後で付加していきます。
この状態で一旦deployします。
sls deploy
すると以下のような感じでAPI GatewayのURLが作成されます。
(**********には作成したAPI GatewayのIDが入ります)
endpoints:
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/entry/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/entry/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/category/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/category/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/site/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/site/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/asset/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/asset/delete
URLができたのでMovableTypeのwebhookを設定しにいきます。
Webhookを設定
MovableTypeにアクセスしFirst Website内のPlugin設定→HeadlessCMSPackに移動します。
ここで先ほどのWebhook URLを設定します。
動作確認
handler.jsにconsole.log(event)を仕込んで内容を確認します。
module.exports.hello = async (event) => {
console.log(event)
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
},
null,
2
),
};
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// return { message: 'Go Serverless v1.0! Your function executed successfully!', event };
};
sls deploy
lambdaをアップデートしてから、
記事を更新しCloudWatchで/aws/lambda/mt-sls-dev-webhook
のログを見ます。
...
body: '{"pingsSentUrl":[],"keywords":"","allowTrackbacks":false,"trackbackCount":"0",...',
isBase64Encoded: false
}
Webhook URLが動作していることとbodyにentryのJSONデータが入ってることが確認できます。
DynamoDBの保存・削除を実装
Webhook URLが動作していることが確認できましたのでDynamoDBの保存・削除を実装を実装します。
まずはLambdaに4つのテーブル(Entry,ArchiveList,Asset,Meta)で読み書きと検索のscanとqueryができるように権限をにつけます。
serverless.ymlのprovider欄を編集します。
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
region: ap-northeast-1
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- { "Fn::GetAtt": ["EntryTable", "Arn"] }
- { "Fn::GetAtt": ["ArchiveListTable", "Arn"] }
- { "Fn::GetAtt": ["AssetTable", "Arn"] }
- { "Fn::GetAtt": ["MetaTable", "Arn"] }
{ "Fn::GetAtt": ["EntryTable", "Arn"] }
はserverless.yml内に書かれたResorcesに指定されたIDのARNを書き出すCloudformationの組み込み関数を利用して書いています。これを使うとDynamoDBのarnの値を知らなくてもResourceを指定できます。。
endpoints:
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/entry/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/entry/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/category/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/category/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/site/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/site/delete
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/asset/update
POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/asset/delete
次にこの8つのURLを実装していきしょう。
まずアーカイブの月別アーカイブを実装するため
mt-sls
ディレクトリに移動し
npm init
とします。コマンド上で色々聞かれますがデフォルトのままで進みます。
npm install dayjs --save
としてdayjsをインストールします。
/entry/update
/entry/update
は
- 記事statusがPublishの時にEntryに記事データを挿入
- ArchiveListの記事と一致するアーカイブの削除(初期化)
- ArchiveListにタグアーカイブ、カテゴリーアーカイブ、月アーカイブの参照データを作成
- 記事statusがPublish意外だった場合、Entryの該当記事データを削除、ArchiveList内のentry_idが一致するアーカイブを削除
以上を実装します。
'webhookEntryUpdate'というlambda関数を追加し
- httpApi:
path: /entry/update
method: POST
上のwebhookにあったevent設定をwebhookEntryUpdateに移動します。
functions:
webhookEntryUpdate:
handler: handler.entryUpdate
events:
- httpApi:
path: /entry/update
method: POST
handler.jsに以下のコードを追記します。。
module.exports.entryUpdate = async (event) => {
const data = JSON.parse(event.body);
const day = dayjs(data.date)
if (data.status === 'Publish') {
await docClient.put(
{
TableName: "Entry",
Item: data
}
).promise();
// 該当アーカイブをリセット
const deleteItems = await docClient.query(
{
TableName: "ArchiveList",
IndexName: 'entryid-index',
ExpressionAttributeNames: { '#id': 'id' },
ExpressionAttributeValues: { ':id': data.id },
KeyConditionExpression: '#id = :id'
}
).promise();
const batchDeleteRequest = []
deleteItems.Items.forEach(item => {
batchDeleteRequest.push({
DeleteRequest: {
Key: {
archive_key: item.archive_key
}
}
})
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
const batchRequest = []
// カテゴリアーカイブ
data.categories.forEach(cat => {
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'category_' + data.blog.id + '_' + cat.id,
date: data.date
}
})
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + cat.id + '_' + day.format('YYYY-MM'),
date: data.date
}
})
})
// タグアーカイブ
data.tags.forEach(tagname => {
if (!tagname.match(/^@/)) {
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'tag_' + data.blog.id + '_' + tagname,
date: data.date
}
})
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + tagname + '_' + day.format('YYYY-MM'),
date: data.date
}
})
}
})
// 月アーカイブ
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + day.format('YYYY-MM'),
date: data.date
}
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
} else {
await docClient.delete(
{
TableName: "Entry",
Key: {
id: data.id
}
}
).promise();
// 該当アーカイブをリセット
const deleteItems = await docClient.query(
{
TableName: "ArchiveList",
IndexName: 'entryid-index',
ExpressionAttributeNames: { '#id': 'id' },
ExpressionAttributeValues: { ':id': data.id },
KeyConditionExpression: '#id = :id'
}
).promise();
const batchDeleteRequest = []
deleteItems.Items.forEach(item => {
batchDeleteRequest.push({
DeleteRequest: {
Key: {
archive_key: item.archive_key
}
}
})
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
}
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
その他URL
- /entry/delete
- /category/update
- /category/delete
- /site/update
- /site/delete
- /asset/update
- /asset/delete
上記は特定のテーブルデータを追加・更新。削除です。
serverless.ymlとhandler.jsは以下のようになります。
service: mt-sls
frameworkVersion: '2'
provider:
name: aws
runtime: nodejs12.x
region: ap-northeast-1
lambdaHashingVersion: 20201221
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- { "Fn::GetAtt": ["EntryTable", "Arn"] }
- { "Fn::GetAtt": ["ArchiveListTable", "Arn"] }
- { "Fn::GetAtt": ["AssetTable", "Arn"] }
- { "Fn::GetAtt": ["MetaTable", "Arn"] }
functions:
webhookEntryUpdate:
handler: handler.entryUpdate
events:
- httpApi:
path: /entry/update
method: POST
webhookEntryDelete:
handler: handler.entryDelete
events:
- httpApi:
path: /entry/delete
method: POST
webhookCategoryUpdate:
handler: handler.categoryUpdate
events:
- httpApi:
path: /category/update
method: POST
webhookCategoryDelete:
handler: handler.categoryDelete
events:
- httpApi:
path: /category/delete
method: POST
webhookAssetUpdate:
handler: handler.assetUpdate
events:
- httpApi:
path: /asset/update
method: POST
webhookAssetDelete:
handler: handler.assetDelete
events:
- httpApi:
path: /asset/delete
method: POST
webhookSiteUpdate:
handler: handler.siteUpdate
events:
- httpApi:
path: /site/update
method: POST
webhookSiteDelete:
handler: handler.siteDelete
events:
- httpApi:
path: /site/delete
method: POST
resources:
Resources:
MtUpload:
Type: AWS::S3::Bucket
Properties:
BucketName: movabletype-xxxx # xxxxにはS3上でBucketNameが一意となる文字を入れる
EntryTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Entry
AttributeDefinitions:
- AttributeName: permalink
AttributeType: S
- AttributeName: id
AttributeType: N
KeySchema:
- AttributeName: id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: permalink-index
KeySchema:
- AttributeName: permalink
KeyType: HASH
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
ArchiveListTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ArchiveList
AttributeDefinitions:
-
AttributeName: archive_key
AttributeType: S
-
AttributeName: date
AttributeType: S
-
AttributeName: id
AttributeType: N
KeySchema:
-
AttributeName: archive_key
KeyType: HASH
-
AttributeName: date
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: entry-index
KeySchema:
- AttributeName: id
KeyType: HASH
- AttributeName: date
KeyType: RANGE
Projection:
ProjectionType: ALL
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
AssetTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Asset
AttributeDefinitions:
-
AttributeName: id
AttributeType: N
KeySchema:
-
AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
MetaTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Meta
AttributeDefinitions:
-
AttributeName: meta_key
AttributeType: S
KeySchema:
-
AttributeName: meta_key
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
'use strict';
const AWS = require('aws-sdk')
const dayjs = require('dayjs')
const docClient = new AWS.DynamoDB.DocumentClient({ convertEmptyValues: true });
module.exports.entryUpdate = async (event) => {
const data = JSON.parse(event.body);
const day = dayjs(data.date)
if (data.status === 'Publish') {
await docClient.put(
{
TableName: "Entry",
Item: data
}
).promise();
// 該当アーカイブをリセット
const deleteItems = await docClient.query(
{
TableName: "ArchiveList",
IndexName: 'entry-index',
ExpressionAttributeNames: { '#id': 'id' },
ExpressionAttributeValues: { ':id': data.id },
KeyConditionExpression: '#id = :id'
}
).promise();
const batchDeleteRequest = []
deleteItems.Items.forEach(item => {
batchDeleteRequest.push({
DeleteRequest: {
Key: {
archive_key: item.archive_key
}
}
})
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
const batchRequest = []
if (data.categories) {
// カテゴリアーカイブ
data.categories.forEach(cat => {
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'category_' + data.blog.id + '_' + cat.id,
date: data.date
}
})
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + cat.id + '_' + day.format('YYYY-MM'),
date: data.date
}
})
})
}
// タグアーカイブ
if( data.tags ) {
data.tags.forEach(tagname => {
if (!tagname.match(/^@/)) {
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'tag_' + data.blog.id + '_' + tagname,
date: data.date
}
})
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + tagname + '_' + day.format('YYYY-MM'),
date: data.date
}
})
}
})
}
// 月アーカイブ
batchRequest.push({
PutRequest: {
id: data.id,
archive_key: 'month_' + data.blog.id + '_' + day.format('YYYY-MM'),
date: data.date
}
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
} else {
await docClient.delete(
{
TableName: "Entry",
Key: {
id: data.id
}
}
).promise();
// 該当アーカイブをリセット
const deleteItems = await docClient.query(
{
TableName: "ArchiveList",
IndexName: 'entryid-index',
ExpressionAttributeNames: { '#id': 'id' },
ExpressionAttributeValues: { ':id': data.id },
KeyConditionExpression: '#id = :id'
}
).promise();
const batchDeleteRequest = []
deleteItems.Items.forEach(item => {
batchDeleteRequest.push({
DeleteRequest: {
Key: {
archive_key: item.archive_key
}
}
})
})
await docClient.batchWirte({
RequestItems: {
ArchiveList: batchRequest
}
}).promise()
}
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.entryDelete = async (event) => {
const data = JSON.parse(event.body);
await docClient.delete(
{
TableName: "Entry",
Key: {
id: data.id
}
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.categoryUpdate = async (event) => {
const data = JSON.parse(event.body);
data.meta_key = 'category_' + data.id
await docClient.put(
{
TableName: "Meta",
Item: data
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.categoryDelete = async (event) => {
const data = JSON.parse(event.body);
await docClient.delete(
{
TableName: "Meta",
Key: {
meta_key: 'category_' + data.id
}
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.assetUpdate = async (event) => {
const data = JSON.parse(event.body);
data.meta_key = 'asset_' + data.id
await docClient.put(
{
TableName: "Asset",
Item: data
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.assetDelete = async (event) => {
const data = JSON.parse(event.body);
await docClient.delete(
{
TableName: "Meta",
Key: {
meta_key: 'asset_' + data.id
}
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.siteUpdate = async (event) => {
const data = JSON.parse(event.body);
data.meta_key = 'site_' + data.id
await docClient.put(
{
TableName: "Meta",
Item: data
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
module.exports.siteDelete = async (event) => {
const data = JSON.parse(event.body);
await docClient.delete(
{
TableName: "Meta",
Key: {
meta_key: 'site_' + data.id
}
}
).promise();
return {
statusCode: 200,
body: JSON.stringify(
{
message: 'successfully!',
input: event,
},
null,
2
),
};
};
変更後
sls deploy
してMovableTypeを更新するとDynamoDBとS3に反映される仕組みが出来上がりました。
ようやく登り始めたばかりのはてしないHeadless CMSですが長くなったので一旦終わります。次回にDynamoDBを使ったSSRの仕組みを作っていきます。
Discussion