CloudKitのAPIを使ってみる

面倒な繰り返し作業はスクリプトにお任せ!

Soudai Sasada

Soudai Sasada

CloudKit の Public Database にあるデータは Apple が提供しているダッシュボードで管理することが可能です。が、ダッシュボートではレコードの登録や更新は1件ずつしか行えず一括で操作が行える削除もページ単位でしか実行できないため大量のデータを一括で操作したいといった用途には不向きです。

特に CloudKit ではProduction環境に反映した Record Type は削除できなくなるといった特性があるため開発時はDevelopment環境を使って試行錯誤するのが一般的であり、また、Development環境で生成したデータはProduction環境に反映させることはできないため、例えば次のバージョンのリリースに合わせて大量のマスターデータのようなものをCloudKit上に置きたい場合にはダッシュボードから1件1件登録しなくてはならず人力でやるには骨が折れます。

CloudKit にはこうした問題に対処できるサーバ間( Server to Server )APIが提供されており、これを使えば簡単なスクリプトを用意するだけでデータの一括更新ができるようになります。なおサーバ間APIはドキュメントを見たところ Public Database 以外はリクエスト可能なユーザーがリクエスト対象となるデータの所有者でなければならずこれは開発者においても例外ではありません。そのため今回は Public Database を前提とした話とさせていただきます。

それでは早速ですがサーバ間APIの実装例を紹介します。

サーバ間APIの準備

大まかな手順は次のとおりです。

  1. 鍵ペアを用意し CloudKit へ公開鍵を登録する
  2. 秘密鍵を使ってリクエストする

鍵ペアを用意し CloudKit へ公開鍵を登録する

サーバ間APIではデジタル署名を用いておりこれにより予め CloudKit 上に登録された公開鍵と対となる秘密鍵を知っている送信者がリクエストしたこと、また、リクエスト内容が改ざんされていないことを保証します。

デジタル署名には公開鍵と秘密鍵のキーペアが必要となるためまずはその準備をしましょう。 Cloud Kit のダッシュボード上で鍵の設定画面へ進みます。

cloudkit-tokens-and-keys

add-server-to-server-api-key-on-cloudkit

それぞれのフィールドの意味は次のとおりです。

フィールド名 意味
Name 開発者が任意に設定可能な鍵を識別するための名前
Public Key 署名に使用する秘密鍵の対となる公開鍵
Notes 開発者が任意に設定可能な自由入力欄

Public Key の欄には鍵ペアの作成方法が書いてあるのでそのとおりに作成します。

$ openssl ecparam -name prime256v1 -genkey -noout -out eckey.pem
$ openssl ec -in eckey.pem -pubout
read EC key
writing EC key
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBqZEEN331hFTl64Jrw8ZI0EFP/E8
GwEwQCC4Lmht007A9F28B1xqeIp45GjpBfhldFGDnzrE0F+5QtUfKFuPsg==
-----END PUBLIC KEY-----

コマンドを叩いて出力された内容の BEGIN PUBLIC KEY から END PUBLIC KEY の間の文字列を画面上の Public Key の欄に入力し、 NameNotes をそれぞれ任意の値で埋めて保存します。なおこのときに入力した全ての内容は後で編集することができず値を書き換えるには一度削除し再度新たに登録するしかありませんので注意しましょう。

公開鍵を保存するとCloudKitでその公開鍵に対応したAPIキーが作成され Key ID という値が振られます。

server-to-server-api-key-details-on-cloudkit

これをリクエスト時にヘッダーへ付加することで「どのAPIキーを使うか」を宣言し、そのAPIキーに対応した秘密鍵を使ってリクエスト内容に署名しリクエスト時には内容に加え署名を送信します。

秘密鍵を使ってリクエストする

次はリクエストする部分です。今回は Ruby で簡単なスクリプトを作ります。

依存なしでも作れますが楽をしたいので今回は faraday だけ使うこととします。ということでまずは依存をインストールしスクリプトファイルを用意します。

$ bundle init
$ echo "gem 'faraday'" >> Gemfile
$ bundle install --path vendor/bundle
$ touch api.rb

次にスクリプトを書いていきましょう。具体的なコードは次のようになります。

api.rb
# frozen_string_literal: true

require 'time'
require 'json'
require 'base64'
require 'openssl'
require 'csv'

require 'faraday'

HOST = 'https://api.apple-cloudkit.com'
API_VERSION = 1
CONTAINER = 'iCloud.com.soudai-s.nekotango'
ENVIRONMENT = 'development'
DATABASE = 'public'

BASE_ENDPOINT = "#{HOST}/database/#{API_VERSION}/#{CONTAINER}/#{ENVIRONMENT}/#{DATABASE}"

SERVER_TO_SERVER_KEY_ID = 'XXX' # 公開鍵登録後に画面上に表示されるKey ID
PRIVATE_KEY_FILE_PATH = 'eckey.pem' # 秘密鍵の相対パス
DEFAULT_HEADERS = {
  'Content-Type' => 'text/plain',
  'X-Apple-CloudKit-Request-KeyID' => SERVER_TO_SERVER_KEY_ID
}

HOW_MANY_ACCESS_AT_ONE_TIME = 200 # records
HOW_MANY_SLEEP_AFTER_EACH_REQUESET = 3 # seconds

# recordType や fieldName 、 desiredKeys は実際の内容に合わせる必要があります
request_body = {
  zoneID: { zoneName: :_defaultZone },
  resultsLimit: HOW_MANY_UPLOAD_AT_ONE_TIME,
  query: {
    recordType: 'CD_Word',
    "sortBy": [
      {
        "fieldName": "CD_number",
        "ascending": true
      }
    ]
  },
  desiredKeys: [:CD_number, :CD_en]
}.to_json
url = "#{BASE_ENDPOINT}/records/query"
now = Time.now.utc.iso8601
digest = Digest::SHA256.base64digest(request_body)
path = url.sub(HOST, '')
message = "#{now}:#{digest}:#{path}"
ec = OpenSSL::PKey::EC.new(open(PRIVATE_KEY_FILE_PATH).read)
signed_message = ec.dsa_sign_asn1(OpenSSL::Digest::SHA256.digest(message))
signature = Base64.strict_encode64(signed_message)
headers = DEFAULT_HEADERS.merge(
  'X-Apple-CloudKit-Request-ISO8601Date' => now,
  'X-Apple-CloudKit-Request-SignatureV1' => signature
)
res = Faraday.post(url, request_body, headers)
puts res.body

最低限これで動くかと思います。では早速試してみましょう。

$ bundle exec ruby api.rb
{
  "records" : [ ],
  "continuationMarker" : "QW9KNkJqOW1hVU5zYjNWa0xtTnZiUzV6YjNWa1lXa3RjeTV1Wld0dmRHRnVaMjhrTXpBa2MyRnVaR0p2ZUNSa05qVTRPRGRpTVMwNVlqWXdMVFJqWlRRdE9UaGtZUzFqTkdVeVpUSXpOak5oTW1VaFVGVkNURWxESVU1RVRUQk5hbEpDVDBSbmRFOUVWVEZOZVRBd1RXcEJla3hVWjNoUFJWbDBVVEJSZWxGVlZURk9WRTAwVVhwUmVBPT0="
}

無事に動きましたね :)

まとめ

ということで今回は CloudKit にサーバ間APIを用いてリクエストする方法を紹介しました。この例ではデータの取得を行なっていますが当然データの作成や更新、削除といったことも可能ですので、例えば予め用意されたCSVファイルの内容をアップロードするようなこともできます。詳しくはドキュメントをご参照ください。

この記事がお役に立てたら一杯のコーヒーを恵んで頂けると励みになります 😊

Buy Me A Coffee
© 2020, Soudai Sasada