Contentfulのブランチ運用(環境の切り替え)

12 min read読了の目安(約11600字

Contentfulを使ったWebアプリの開発を行っています。アプリのリリースを行うにあたって本番・開発環境の分離や運用方法を検討しましたが、Contentfulに登場する色々な概念について結構曖昧な理解をしていて苦しんだので調査、検討した内容についてまとめます。

概念の整理やContentfulでの動作の検証がメインで、実際の運用についてはまだそれほど深く検討できていない部分がある点はご了承ください。(参考情報等シェアしていただけると非常に助かります)

なお、この記事の内容はContenfulのFreeプランでの運用を前提にしています。
有料プランの場合には全く異なる方法があるかもしれませんので、その点についてもご了承ください。

環境とコンテンツのセットアップ

環境の切り替えを確認するために必要な前準備を行っていきます。
Contentfulに登録したらOrganization, Spaceの順に作成を行い、その次に実際のコンテンツを作成します。

Organizationの作成

まず、Organizationの作成を行います。

Spaceの作成

次にコンテンツを登録するためのSpaceを作成します。
プランはFreeを選択します。

Contentの登録

実際にコンテンツの登録を行って行きます。

まず、実際にコンテンツを登録する前にContent modelの登録が必要です。
Content modelとはコンテンツのデータ形式を定義したもの(例えばTodoならtitleという項目とdescriptionという項目があるなど)です。

今回の記事の内容とはあまり関係しないので詳細は省きますが、サンプルとしてTodoのコンテンツを作成しました。

Contenfulの基本的な概念

ここからが本題で、まず環境の切り替えを行うにあたって必要な概念について簡単にまとめます。

APIの種類

配信用と更新用で以下2種類のAPIがあります。

  • コンテンツの配信用
    • Content Delivery API
      • その名の通りコンテンツの配信用APIでRead Only。
      • CDNが利用可能で、実際の配信時にはユーザにもっとも近いサーバからコンテンツが配信される
    • Content Preview API
      • コンテンツの確認用(Read Only)
      • Previewとついている通り後述するDraft状態のコンテンツの内容にアクセスできる
      • 本番での利用は想定されていない(CDNは使用されず、本番での配信用に最適化されていない)
  • コンテンツの更新用

この記事では主にコンテンツの配信用に絞って説明を行い、更新用については触れません(ブラウザ上からの操作を前提としています)

コンテンツの状態

作成するコンテンツの内容について、以下2つの状態があります。

  • Draft
    • 編集すると自動でこの状態になる
    • 名前の通りまだ正式なコンテンツとしてリリースされていない扱い
    • Preview APIでアクセスが可能
  • Published
    • 正式なコンテンツとしてリリースされたもの

環境に関連したContenfulの概念について

以下3つの概念が本番環境、開発環境等の運用を行うにあたって重要になります。

SpaceとEnvironmentについて

SpaceとEnvironmentに関しては公式ドキュメントの以下の図がわかりやすいです。


Managing multiple environments

ポイントとして以下を押さえておけば大丈夫だと思います。

  • Spaceの配下にEnvironmentを作成できる(記載時点でFreeの場合には4つまで)
  • デフォルトではenvironmentとしてmasterが作成されている
  • コンテンツ(content model, content, media)の内容はenvironmentごとに独立して管理可能
  • Environmentを作成するときは作成元のEnvironmentを指定して、作成元のデータがそのままコピーされる

特に最後の点についてはGitのブランチと同じイメージです。(ただし、次の項で説明するaliasの概念がGitのブランチ運用と異なる点です)

Aliasについて

aliasの役割は本番環境がどのEnvironmentであるかを指し示すためにあり、デフォルトではmaster environmentを指しています。

ポイントとしては、どの環境が本番であるかを決めるのはEnvironmentではなく、aliasであるということです。

つまり、master environment=本番環境という状態であり、コンテンツの更新が直に本番環境に反映されることになります。

また、新しいEnvironmentを作成してもaliasは自動で更新されないので、APIでアクセスしても返される内容は変化しません。(aliasの向き先となっているEnvironmentのコンテンツを更新しない限り)

以下のように明示的にaliasを変更することで初めてデータの取得元が更新されます。

Aliasの更新自体はContentful上であれば、Environemtの設定から行えます。

SDKでのアクセス

この後実際に環境の切り替え等を実際に行うにあたってSDKを使用していきます。
ContentfulではPlatformsに記載の通り記事執筆時点で8種類のSDKが提供されています。

今回はPythonを使いますが、基本的に言語が変わっても基本的な使い方は変わらないので、使用する言語に合わせて指定する項目名等を変更してください。

Pythonの場合、以下2つのライブラリが提供されています。

APIキーの発行

APIでコンテンツにアクセスするには(SDKの使用有無にかかわらず)APIキーの発行が必要です。
APIキーの発行時にアクセス権を与える対象として以下の2つを選べます。

  • ENVIRONMENT ALIASES
  • ENVIRONMENT

ポイントとなるのはENVIRONMENT ALIASESの方で、こちらにチェックを入れておけばEnvironmentが変更しても本番環境に対するアクセス権を付与できます。

インストール

インストール自体は通常のpythonライブラリと同様にpipで行えます。

pip install contentful contentful_management

基本的な使い方

基本的な使い方は以下の通り、クライアントを設定して、コンテンツを取得する流れになります。

import contentful
# クライアントの設定
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx'
)

# コンテンツの取得
res = client.entries({'content_type': 'toDo'})

環境(Environment)を指定

アクセス先の環境を変更するには、クライアントの設定時にenvironemtを指定します。
特に指定をしないとデフォルト値であるmasterが設定されます。

# stagingという環境を指定
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
  environment='staging'
)

# 環境を指定しない場合デフォルトでmasterが指定される
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
)

Previewを指定

api_urlを指定することでアクセス先が変わり、Preview(Draftのコンテンツを含んだ)状態のコンテンツを取得できます。

# PreviewAPIを指定
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
  api_url='preview.contentful.com'
)

# Environmentの指定を組み合わせる
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
  api_url='preview.contentful.com'
  environment='staging'
)

Environment aliasへのアクセス

To request data using the environment alias, specify the alias ID instead of the ID of the target environment.
For example, if the master alias targets an environment with ID target-environment, you can access its data from /spaces/<id>/environments/master/....
Environments

上記に記載の通り対象environmentの名前を指定するのではなく、aliasのID(masterなど)を指定することが可能です。

例として以下のenvironmentが存在する場合を考えてみます。

  • master-20210412(masterのaliasとして設定)
  • master-20210413
  • staging

masterというenvironmentは存在しませんが、master aliasを指定すればaliasが指し示すenvironmentに対してアクセスが行われます。

# master aliasを指定
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
  environment='master' # aliasを指定可能
)

以下の図のイメージになります。

また、前述の通りEnvironemntを直接指定してアクセスすることも可能です。
繰り返しになりますが、staging Environmentを指定してアクセスする場合には以下のようにclientの設定時にenvironmentを指定すればOKです。

# stagingという環境を指定
client = contentful.Client(
  space_id='xxxxxxxxxxx',
  access_token='xxxxxxxxxxx',
  environment='staging'
)

イメージとしては以下の通りになります。

環境の分離

では実際に上記でまとめた内容を元に実際にAPIのリクエストを行って確認しながら環境の切り替えを試してみます。

切り替えを試すにあたって以下のような環境の分け方と運用を想定して進めます

まず、環境として以下4つの分類が存在するとします。

  • staging
    • 開発中のコンテンツを管理する環境
    • コンテンツの更新作業自体は常にここに対して行う
    • アプリケーションのステージング環境からアクセスすることを想定
  • temp
    • 本番環境の切り替えに伴い一時的に存在する環境
  • master
    • 本番中のコンテンツの内容を保持する環境
    • 何が本番環境かはmaster aliasの紐付けで決定する
    • アプリケーションの本番環境からアクセスすることを想定
  • released
    • 過去の本番にリリースしたコンテンツの内容を保持する環境
    • 無料プランではEnvironmentの数に制約があるため、1世代前の情報のみを保持する

現在以下のような状態だとします。

staging environmentでコンテンツの更新を行い、本番環境に更新したコンテンツを反映させるために新しいEnvironmentとしてmaster-20210417の作成を行います。(作成元のEnvironmentはstagingです)

master aliasをmaster-20210417に切り替えることで本番への適用が行われます。(本番環境で適用されるコンテンツの内容がstagingと同様になる)

それでは、実際にContentfulで環境の切り替えを実施しながら上記の運用を確認してみます。

環境の作成

まず、stagingというenvironmentを作成します。

新しいenvironmentを作成したことにより、Contentful上で切り替えが可能になっています。

(stagingに切り替えた後の表示)

ステージングのPreview

staging environmentでコンテンツの追加後(Draft状態)のAPIリクエスト結果を確認してみます。
サンプルとして「洗濯物を干す」というデータを追加しました。

実際にPreviewAPIをコールしてみると、以下の通り新しく追加したコンテンツが取得できていることが確認できます。

import contentful

# staging preview
client = contentful.Client(
  space_id='',
  access_token='',
  api_url='preview.contentful.com', # PreviewAPIをコールするため指定
  environment='staging' # stagingを指定
)
>>> 
{'title': '洗濯物を干す'}
{'title': '牛乳を買う', 'due_date': datetime.datetime(2021, 4, 23, 0, 0, tzinfo=tzoffset(None, 32400)), 'description': 'スーパーで一番値段の高い牛乳を買う'}

一方、Content Delivery APIを呼んだときには新しく追加したコンテンツは取得されません。

# staging
client = contentful.Client(
  space_id='',
  access_token='',
  environment='staging' # stagingを指定。api_urlの指定は行わない
)
res = client.entries({'content_type': 'toDo'})
for item in res.items:
    print(item.fields())
>>> 
{'title': '牛乳を買う', 'due_date': datetime.datetime(2021, 4, 23, 0, 0, tzinfo=tzoffset(None, 32400)), 'description': 'スーパーで一番値段の高い牛乳を買う'}

Publish後

新しく追加したコンテンツの状態を[Published]に変更して、Content Delivery APIのリクエスト結果を確認してみます。

Publishされたため、想定通りコンテンツが追加されていることが確認できました。

# staging
client = contentful.Client(
  ...
  environment='staging' # stagingを指定。api_urlの指定は行わない
)
res = client.entries({'content_type': 'toDo'})
for item in res.items:
    print(item.fields())
>>>
{'title': '洗濯物を干す'}
{'title': '牛乳を買う', 'due_date': datetime.datetime(2021, 4, 23, 0, 0, tzinfo=tzoffset(None, 32400)), 'description': 'スーパーで一番値段の高い牛乳を買う'}

masterへの反映

masterへはまだ反映されていないので、新しく追加したTodoはレスポンスに含まれません。

# master
client = contentful.Client(
  space_id='',
  access_token='',
  environment='master' # デフォルトでmasterになるので省略可能
)
>>>
{'title': '牛乳を買う', 'due_date': datetime.datetime(2021, 4, 23, 0, 0, tzinfo=tzoffset(None, 32400)), 'description': 'スーパーで一番値段の高い牛乳を買う'}

新しいEnvironmentの作成

本番のaliasを設定するために新しいenvironmentを作成します。

ここで1点注意点として、キャプチャを撮り忘れてしまったたため、environment名が一部以下の通り変わっていますが、ご了承ください。

  • staging
    • 開発用
    • まだ本番に反映していないコンテンツが追加されている
  • master-2021-04-13
    • master aliasが設定されている
    • stagingに追加した新しいコンテンツは反映されていない
    • 元々のmaster environmentに相当

新しくEnvironmentを追加します。
Environmentの作成時に作成元のEnvironmentを選べるので、先ほどTodoの追加を行ったstagingを選択します。

この時点ではmasterが指し示しているもの(master environmentを指定してリクエストを送った時にアクセスする対象)はmaster-2021-04-13なので、新しく追加したTodoはまだmasterに反映されていません。

aliasの切り替え(本番環境の切り替え)

masterのAliasを新しく追加したEnvironmentに指定します。

これで本番環境のコンテンツが切り替わったはずなので、実際にAPIリクエストを行って確認してみます。

# master
client = contentful.Client(
  space_id='',
  access_token='',
  environment='master'
)
res = client.entries({'content_type': 'toDo'})
for item in res.items:
    print(item.fields())
>>> 
{'title': '洗濯物を干す'}
{'title': '牛乳を買う', 'due_date': datetime.datetime(2021, 4, 23, 0, 0, tzinfo=tzoffset(None, 32400)), 'description': 'スーパーで一番値段の高い牛乳を買う'}

上記の通り追加したコンテンツにアクセスできることが確認できました。

参考資料