ぴろログ

Output Driven

GitHub Actions を使って Jamf Pro にスクリプトを自動デプロイしてみた

Jamf Pro でのスクリプトの管理(コード管理)のお悩みツイートを目にしました。
自社ではまだ使い込めていないので想像ですが、運用規模が大きくなると以下が悩みどころになるのかなと思います。

  • コード管理
    • バージョン管理
    • 変更管理
  • 構成管理

解決につながりそうな事例を探したところ、 JNUC 2018 の発表スライドで「 GitHub でコードを管理し Jenkins で自動デプロイ」する仕組みを見つけました。

Jenkins を構築するのは避けたかったので、 GitHub Actions を利用して GitHub → Jamf Pro へのスクリプトの自動デプロイ環境を作成・検証してみました。(今回は自動デプロイにスコープを絞って確認しています。 ポリシーとの紐付けの可視化は別途やってみたいと思います。)

なお、検証にあたって Sandbox を払い出していただけました。 Jamf Pro を契約している場合は Jamf の担当営業の方に相談すると対応していただけるようです。担当営業が誰だかわからない場合は @yoshifin さんにコンタクトをとると何とかしてくれます!感謝です!!

検証の概要

全体の構成です。

f:id:pirox07:20210319215646p:plain

GitHub 上での main ブランチへの push をトリガに GitHub Actions が自動実行され、 API 経由で Jamf Pro にスクリプトがデプロイされます。スクリプト本体だけでなく [ 情報 ] や [ 優先度 ] 等の各属性もあわせてデプロイできます。

その他に確認できたことです。

  • スクリプトの更新前に(なんちゃって)バックアップを取得
  • Jamf Pro API へのアクセスに利用するユーザの credential の隠蔽
  • Jamf Pro API に対するアクセス権のコントロール(操作可能なリソースや権限を最小限に制限)
  • Jamf Pro 上のスクリプトGitHub リポジトリの紐付けを自動記録

設定作業

環境構築のための作業メモです。

Jamf Pro

Jamf Pro API へのアクセス(認証)に利用するユーザを作成します。

ユーザ script-manager を作成して、[ スクリプト ] の作成・読み取り・更新の権限セットを付与しました。対象のオブジェクトが細かく指定できるので、必要最小限の権限に留められて良いですね。

f:id:pirox07:20210319194121p:plain

f:id:pirox07:20210319194154p:plain

GitHub

GitHubリポジトリを作成し、以下のディレクトリ構成でファイルを作成しました。

sample_deploy_jamf-pro_scripts/
├.github/
 │  └ workflows/
 │            └ deploy.yml  # GitHub Actions の設定ファイル
├ deploy.sh # デプロイ用スクリプト
└ script/
    ├echo-hello.sh # デプロイ対象のスクリプト
    └config.json # デプロイモードの制御情報(新規作成・更新)、各種属性情報

.github/workflow/deploy.yml

YAML ファイルで GitHub Actions の設定を定義します。

name: Deploy script to Jamf pro
on:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: macos-10.15
    steps:
    - uses: actions/checkout@v2
    - name: install GNU sed
      run: brew install gnu-sed
    - name: Grant execute permission
      run: chmod +x deploy.sh
    - name: Deploy
      env:
        JAMF_URL: "https://xxxxxxsandbox.jamfcloud.com"
        JAMF_USER: "script-manager"
        JAMF_PASSWORD: ${{ secrets.JAMF_PASSWORD }}
      run: sh deploy.sh

on:GitHub Actions の実行トリガを main ブランチへの push に限定しています。他のブランチへの push では実行されません。

runs-on: で CI/CD の実行環境(コンテナ)を指定します。手元の環境にあわせて macOS にしてみました。

デプロイ用スクリプト( deploy.sh )のなかで GNU sed を使いたかったので、実行環境にインストールしています( brew install gnu-sed )。

Jamf Pro の URL やユーザの credential は環境変数として扱っています( env: )。 パスワードに関しては GitHubSecrets で管理しています。

f:id:pirox07:20210319194904p:plain

script/config.json

スクリプトの属性情報や、 デプロイの動作(新規作成・更新)を定義しています。デプロイ用スクリプト( deploy.sh )に読み込ませます。

{
  "mode": "NEW",   <- "NEW" or "UPDATE"
  "id": "",
  "name": "< Generate dynamically >",
  "info": "say Hello.",
  "notes": "< Generate dynamically >",
  "priority": "AFTER",
  "categoryId": "0",
  "categoryName": "",
  "parameter4": "arg-1",
  "parameter5": "arg-2",
  "parameter6": "arg-3",
  "parameter7": "arg-4",
  "parameter8": "arg-5",
  "parameter9": "arg-6",
  "parameter10": "arg-7",
  "parameter11": "arg-8",
  "osRequirements": "10.10.x",
  "scriptContents": "< Generate dynamically >"
}

mode の値によってデプロイの挙動を切り替えています。

スクリプトの ID は 新規作成時の HTTP Response を確認するか、設定画面の URL で確認できます

f:id:pirox07:20210319195223p:plain

その他の key とダッシュボード上での表記との紐付けは以下の通りです

  • Jamf Pro API Reference - scripts

  • name ... [ 一般 ] - [ 表示名 ]

  • info ... [ 一般 ] - [ 情報 ]
  • notes ... [ 一般 ] - [ 注記 ]
  • priority ... [ オプション ] - [ 優先度 ]
  • categoryId ... 表示なし ※[ カテゴリ] の番号と対応
  • categoryName ... [ 一般 ] - [ カテゴリ ]
  • parameter4~11 ... [ オプション ] - [ Parameter Labels ] - [ パラメータ 4 ~ 11 ]
  • osRequirements ... [ 制限 ] - [ オペレーティングシステム要件 ]
  • scriptContents ... [ スクリプト ]

今回の検証では name, scriptContens, notes の値が以下となるようデプロイ時に自動生成しています。

script/echo-hello.sh

デプロイするスクリプトです。

#!/bin/bash

echo "Hello, Jamf!! from GitHub."

deploy.sh

デプロイ処理のためのスクリプトです。 GitHub Actions の CI/CD 環境( macOS コンテナ)で実行されます。

script-manager の credential (ユーザ名、パスワード)を Base64 形式でエンコードした文字列を使って API へのアクセスに必要なトークンを取得し( /uapi/auth/tokens )、 config.json, echo-hello.sh からデータを読み込みこんで API に送信しています( /uapi/v1/scripts/, /uapi/v1/scripts/{id} )。

冗長な部分はいつか直します...

#!/bin/bash

# server connection information
URL=$JAMF_URL
username=$JAMF_USER
password=$JAMF_PASSWORD

# created base64-encoded credentials
encodedCredentials=$( printf "${username}:${password}" | /usr/bin/iconv -t ISO-8859-1 | /usr/bin/base64 -i - )

# generate an auth token
authToken=$( /usr/bin/curl "$URL/uapi/auth/tokens" \
--silent \
--request POST \
--header "Authorization: Basic $encodedCredentials" )

# parse authToken for token, omit expiration
token=$( awk -F \" '{ print $4 }' <<< "$authToken" | xargs )

# set config
FILENAME=$(basename ./script/*.sh)
CONTENTS=$(cat ./script/$FILENAME | sed -e 's/"/\\"/g' | gsed -z 's/\n/\\n/g' )
conf_info=$(cat ./script/config.json | jq .info | sed -e 's/^"//' -e 's/"$//' )

#conf_notes=$(cat ./script/config.json | jq .notes | sed -e 's/^"//' -e 's/"$//' )
conf_notes="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA} \n- GitHub Actions Run Number: #${GITHUB_RUN_NUMBER}"
conf_priority=$(cat ./script/config.json | jq .priority | sed -e 's/^"//' -e 's/"$//' )
conf_categoryId=$(cat ./script/config.json | jq .categoryId | sed -e 's/^"//' -e 's/"$//' )
conf_categoryName=$(cat ./script/config.json | jq .categoryName | sed -e 's/^"//' -e 's/"$//' )
conf_param4=$(cat ./script/config.json | jq .parameter4 | sed -e 's/^"//' -e 's/"$//' )
conf_param5=$(cat ./script/config.json | jq .parameter5 | sed -e 's/^"//' -e 's/"$//' )
conf_param6=$(cat ./script/config.json | jq .parameter6 | sed -e 's/^"//' -e 's/"$//' )
conf_param7=$(cat ./script/config.json | jq .parameter7 | sed -e 's/^"//' -e 's/"$//' )
conf_param8=$(cat ./script/config.json | jq .parameter8 | sed -e 's/^"//' -e 's/"$//' )
conf_param9=$(cat ./script/config.json | jq .parameter9 | sed -e 's/^"//' -e 's/"$//' )
conf_param10=$(cat ./script/config.json | jq .parameter10 | sed -e 's/^"//' -e 's/"$//' )
conf_param11=$(cat ./script/config.json | jq .parameter11 | sed -e 's/^"//' -e 's/"$//' )
conf_osRequirements=$(cat ./script/config.json | jq .osRequirements | sed -e 's/^"//' -e 's/"$//' )

# create json data
jsonData=$(cat <<EOS
{
  "name": "${FILENAME}",
  "info": "${conf_info}",
  "notes": "${conf_notes}",
  "priority": "${conf_priority}",
  "categoryId": "${conf_categoryId}",
  "categoryName": "${conf_categoryName}",
  "parameter4": "${conf_param4}",
  "parameter5": "${conf_param5}",
  "parameter6": "${conf_param6}",
  "parameter7": "${conf_param7}",
  "parameter8": "${conf_param8}",
  "parameter9": "${conf_param9}",
  "parameter10": "${conf_param10}",
  "parameter11": "${conf_param11}",
  "osRequirements": "${conf_osRequirements}",
  "scriptContents": "${CONTENTS}"
}
EOS
)

echo "=== jsonData ==="
echo $jsonData

# deploy

MODE=$( cat ./script/config.json | jq .mode | sed -e 's/^"//' -e 's/"$//' )
case "$MODE" in
  "NEW")
    httpResponse=$( curl "$URL/uapi/v1/scripts" \
    --silent \
    --request POST \
    --header "Authorization: Bearer $token" \
    --header "Accept: application/json" \
    --header "Content-Type: application/json" \
    --data "$jsonData" )

    echo "=== POST /uapi/v1/scripts ==="
    echo $httpResponse
  ;;
  "UPDATE")
    scriptId=$( cat ./script/config.json | jq .id | sed -e 's/^"//' -e 's/"$//' )

    # back up
    httpResponse=$( curl "$URL/uapi/v1/scripts/$scriptId" \
    --silent \
    --request GET \
    --header "Authorization: Bearer $token" )

    echo "=== GET /uapi/v1/scripts/$scriptId ==="
    echo $httpResponse

    httpResponse=$( curl "$URL/uapi/v1/scripts/$scriptId" \
    --silent \
    --request PUT \
    --header "Authorization: Bearer $token" \
    --header "Accept: application/json" \
    --header "Content-Type: application/json" \
    --data "$jsonData" )

    echo "=== PUT /uapi/v1/scripts/$scriptId ==="
    echo $httpResponse
  ;;
  *)
    echo "\$MODE is invalid value."
    exit 1
  ;;
esac

動作確認

上記ファイルを main ブランチに push すると GitHub Actions が自動実行されました。 Jamf Pro にデプロイされたスクリプトの ID は 18 のようです。

f:id:pirox07:20210319200359p:plain

ダッシュボードを確認すると、 echo-hello.sh が登録されていました。

f:id:pirox07:20210319200443p:plain

[ 注記 ]( note )には、GitHubリポジトリやコミットのハッシュ値に対応した URL と、デプロイした GitHub Actions の Run Number が記録されています。

f:id:pirox07:20210319200526p:plain

その他項目の反映も問題ないようです。

f:id:pirox07:20210319200646p:plain

f:id:pirox07:20210319200709p:plain

f:id:pirox07:20210319200724p:plain

次はスクリプトを更新してみます。

dev ブランチを作成してファイルを更新し、 push します。

% git checkout -b dev
Switched to a new branch 'dev'
xxxxx@xxxxx sample_deploy_japf-pro_scripts % git branch
* dev
  main
% git diff
diff --git a/script/config.json b/script/config.json
 ~
-  "mode": "NEW",
-  "id": "",
+  "mode": "UPDATE",     <- mode を "UPDATE" に変更
+  "id": "18",           <- 更新対象のスクリプトの ID を設定
 ~
-  "osRequirements": "10.10.x",
+  "osRequirements": "10.15.x",    <- 実行制限の OS バージョンを変更
 ~

diff --git a/script/echo-hello.sh b/script/echo-hello.sh
 ~ 
-echo "Hello, Jamf!! from GitHub."
\ No newline at end of file
+echo "Hello, Jamf!! from GitHub. (update)" <- 末尾に "(update)" を追記
\ No newline at end of file
% git add script/*
% git commit -m "[update] update script."
[dev a679517] [update] update script.
 2 files changed, 4 insertions(+), 4 deletions(-)

% git push --set-upstream origin dev

main ブランチ以外への push では GitHub Actions は実行されないため、 main ブランチへ Pull Request -> merge して発火させます。(実運用ではここでコードレビューが入る形になると思います。)

更新前の状態と更新後の設定を出力しています。なんちゃってバックアップです。

f:id:pirox07:20210319222234p:plain

ダッシュボード上でも実際に値が更新されていることを確認できました。

f:id:pirox07:20210319201237p:plain

f:id:pirox07:20210319201257p:plain

ポリシーに紐付けて Mac に配信したログです。ちゃんと動いたようです。

f:id:pirox07:20210319201331p:plain

まとめ

GitHub から Jamf Pro へスクリプトの自動デプロイができました。これでコード管理を GitHub に集約することができそうです。 GitHub Actions を採用することで構成コンポーネントを少なくできるのもメリットだと感じます。

Clasic API ではポリシーの操作が可能なようなので、スクリプトの配信対象も含めてコードで構成管理ができるかもしれません。

今回は勉強がてらシェルスクリプトでデプロイしてみましたが、エラー処理等も考えると別の言語で書くほうが楽だと思いました。。

参考情報

Jamf Nation - Creating an Authorization Token With JAMF Pro API - Help would be greatly appreciated

As of Jamf Pro 10.14, the Jamf Pro API (/uapi) allows access to create and update scopes for computer PreStage Enrollments. Edit the information at the top and include a list of computer serial numbers for the COMPLETE scope. (The script replaces the scope list; it doesn't update.) Be sure to leave the opening and closing parentheses. · GitHub

github.com

knowledge.sakura.ad.jp

blog.kondoumh.com

dev.classmethod.jp