0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SalesforceのCI/CDを構築してみた

Posted at

はじめに

Salesforceの組織間のメタデータの送信には「送信/受信セット」が多く使われております。これは「どれとどれをリリースする」というのを画面からポチポチと選択するだけで良いのでとても簡単です。しかし、リリース対象が多くなってくると

  • リリースしたいコンポーネントが見つからない
  • ApexClassやカスタム項目多すぎてページ送りが大変
  • 他の人が作業したものを上書きしてしまう

など、とにかくフラストレーションがたまります。
そこで、メタデータを全てソースとしてgit管理しソース駆動型開発を行うことでこれらの問題の解決をし、より早く、より安全なリリースサイクルを実現したいと考え、github Actionを用いてこのソース駆動型開発を支えるCI/CD(継続的インテグレーション/継続的デリバリー)環境を作成しました。

CI/CDには前提知識がとにかく必要になります。まずは基本知識から勉強していきましょう。

Salesforceの組織について

送信/受信セットを使った開発を行っている場合、よく使う組織として

  • 本番組織
  • Sandbox組織

があるかと思います。開発者がたくさんいる場合は、開発者ごとにSandboxを作り、そこで開発した結果を本番環境に送信セットを使って送り出しているような運用がほとんどかと思います。

ソース駆動型開発をする際に、Salesforce公式は「スクラッチ組織」を使って開発をすることが想定しています。

DevHub組織とスクラッチ組織

  • DevHub組織: あなたのSalesforce開発における「司令本部」です。全ての開発環境(スクラッチ組織)は、ここから生み出され、管理されます。本番組織やDeveloper組織で「Dev Hub機能を有効化」することができます。
  • スクラッチ組織: 開発者が使う、使い捨ての「クリーンな作業場」です。他の開発者の影響を受けないクリーンな環境で、安全に開発を進めることができます。今までのSandboxに代わる開発用の組織です。

理想の開発サイクルと「環境」の役割

CI/CDパイプラインを設計する上で、最も重要なのが「環境戦略」です。これは、コードが本番に届くまでの「関所」をどう設計するか、という問題です。

  1. 開発環境 (スクラッチ組織): 開発者個人の作業場。
  2. 統合環境 (共有サンドボックス or スクラッチ組織): 複数の開発者の変更を初めて統合し、技術的なレビュー(機能間の干渉、フローの動作確認など)を行う、開発チーム内の品質保証の場。
  3. Staging/UAT環境 (共有サンドボックス): 開発が完了したバージョンを、お客様やビジネスユーザーが受け入れテスト(UAT)を行う場所。「この機能は本当に要件を満たしているか?」を検証する、公式な検収の場。
  4. 本番組織 (DevHub組織): 実際のビジネスが動く最終ゴール。

コラム:「統合環境」と「Staging環境」は一緒じゃないの?

答えは「開発のビジネスコンテキストによる」です。受託開発など、お客様との明確な合意形成が必要な場面では、開発チーム内の「統合環境」と、お客様向けの「Staging環境」を厳密に分離することが、手戻りを防ぎ、プロジェクトを成功に導く鍵となります。

DevOps Center vs CI/CD構築:なぜ自前でパイプラインを組む意義があるのか?

Salesforceから公式に「DevOps Center」というツールが提供されています。この記事のタイトルと見たときに、「DevOps Centerを使えばいいのでは?なぜ、CI/CDパイプラインを自前で構築する必要があるのか?」と思われた方もいらっしゃるかと思います。

結論から言うと、両者はターゲットとするユーザーと思想が異なり、どちらが良いというわけではありません。しかし、自前でCI/CDを構築することには、DevOps Centerだけでは得られない、非常に大きな意義があります。

DevOps Center:標準化された高速道路を走る「全自動ツアーバス」

DevOps Centerは、Gitの複雑な操作を完全に隠蔽し、Adminなどの非エンジニア層にもソース管理の恩恵をもたらす、素晴らしいツールです。

  • Gitの知識が不要: UIのクリック操作だけで、裏側では自動的にGitへのコミットが行われます。
  • 標準化されたプロセス: Salesforceが推奨する優良な開発プロセスに、チーム全体を乗せることができます。

これは、決まったルートを安全・快適に走る「ツアーバス」のようなものです。誰でも簡単に乗車でき、目的地まで連れて行ってくれます。

CI/CD構築:あらゆる道を走破できる「カスタムビルドのオフローダー」

一方で、SFDXとGitHub Actions(またはCircleCIなど)を使って自前でパイプラインを構築することは、自分たちのチームに最適な「オフロード車」をカスタムビルドするようなものです。

DevOps Centerという「舗装された道路」しか走れないツアーバスとは違い、以下のような道なき道を進むことができます。

1. 無限の拡張性

パイプラインに、好きなツールを自由に組み込めます。

  1. 静的コード解析 (Apex PMD): コードの品質を自動でチェック。
  2. LWCの単体テスト (Jest): フロントエンドの品質を担保。
  3. 外部ツール連携: デプロイ結果をSlackやteamsに通知したり。

2. プロセスの完全な所有権:

チームの開発プロセスを、ツールに縛られることなく、自分たちで100%コントロールできます。これは、チームの成熟度に合わせてプロセスを継続的に改善していく上で不可欠です。

3. 深い学びとチームの成長:

パイプラインを自前で構築する過程は、今回私が経験したように、多くの失敗と発見に満ちています。この苦しい旅路を経ることで、チームは自分たちの開発プロセスの課題を深く理解し、それを解決するための技術力を身につけ、真のDevOps文化を醸成することができるのです。

結論として、DevOps Centerは「手軽に標準的なプロセスを導入する」ための最良の選択肢の一つです。しかし、チームの品質基準をさらに高め、開発プロセスを継続的に最適化していきたいのであれば、自前でCI/CDパイプラインを構築するという挑戦には、計り知れない価値があります。

package.xml とは

組織、としてデプロイ方法について解説しました。
実際のデプロイには、package.xmlと呼ばれるマニフェストファイルを使ってデプロイを行います。
また、このマニフェストファイルは組織からのデータの取得(retrieve)にも使用される「どのメタデータを受信/送信するか」を決める重要なものです。

CI/CDの構築には、このpackage.xmlでデプロイを制御するが故のテクニックが必要となります。次のCI/CD構築で順に見ていきましょう。

CI/CD構築

1. シンプルにpackage.xmlを使ってデプロイを行う

一番最初に提示するこのパターンでは、github Actionは次の動作をします。

  1. Salesforce CLIをインストール
  2. デプロイ先の組織にCLIからログイン
  3. package.xmlを使ってデプロイ
name: Step 1 - Basic Full Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v4

      #  Salesforce CLIをインストール
      - name: Install Salesforce CLI
        run: npm install --global @salesforce/cli

      # デプロイ先の組織にCLIからログイン
      - name: Create JWT key file
        run: |
          mkdir -p ./assets
          echo "${{ secrets.SF_JWT_KEY }}" > ./assets/server.key

      - name: Authenticate to Org
        run: |
          sf org login jwt \
            --username ${{ secrets.SF_USERNAME_PROD }} \
            --client-id ${{ secrets.SF_CLIENT_ID }} \
            --jwt-key-file assets/server.key \
            --alias deployOrg

      # package.xmlを使ってデプロイ
      - name: Deploy all metadata using package.xml
        run: |
          sf project deploy start \
            --manifest ./manifest/package.xml \
            --test-level RunLocalTests \
            --target-org deployOrg

このデプロイでは、package.xmlがretrieveの兼ねているため大量のメタデータがデプロイ対象になってきます。そのためデプロイが遅くなるという欠点があります
また、上記のデプロイコマンドはメタデータの作成・更新用のためApexクラスなどの削除に未対応という欠点もあります。

この問題を解消するために、「差分デプロイ」を行いましょう

差分デプロイを行う

name: Step 2 - Delta Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source with full history
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Salesforce CLIのインストール
      - name: Install Salesforce CLI
        run: npm install --global @salesforce/cli

      # 差分デプロイ用のプラグインをインストールするために、
      # ホワイトリストに追加
      - name: Trust and Install git-delta plugin
        run: |
          mkdir -p $HOME/.config/sf
          echo '["sfdx-git-delta"]' > $HOME/.config/sf/unsignedPluginAllowList.json
          sf plugins install sfdx-git-delta

      # 差分対象をわかりやすく表示するためのミドルウェアをインストール
      - name: Install helper tools (yq for xml parsing)
        run: pip install yq

      # デプロイ対象の組織にログイン
      - name: Create JWT key file
        run: |
          mkdir -p ./assets
          echo "${{ secrets.SF_JWT_KEY }}" > ./assets/server.key

      - name: Authenticate to Org
        run: |
          sf org login jwt \
            --username ${{ secrets.SF_USERNAME_PROD }} \
            --client-id ${{ secrets.SF_CLIENT_ID }} \
            --jwt-key-file assets/server.key \
            --alias deployOrg

    # 差分のpackage.xmlが作られるディレクトリを用意
      - name: Create output directory
        run: mkdir -p ./output

      # 差分のpackage.xmlを作成
      - name: Generate package from Git Diff
        run: |
          sf sgd source delta \
            --to HEAD \
            --from HEAD~1 \
            --output-dir ./output

      # 差分ファイルの状態によって、追加・変更や削除があったかどうかを管理
      # 同時にデプロイ対象を表示させる
      - name: Check for changes and log them
        id: check_changes
        run: |
          ADD_MANIFEST="./output/package/package.xml"
          DELETE_MANIFEST="./output/destructiveChanges/destructiveChanges.xml"
          ADD_CHANGES=false
          DELETE_CHANGES=false
          # ... (スクリプトの詳細は最終版を参照) ...
          if [ -f "$ADD_MANIFEST" ] && grep -q '<types>' "$ADD_MANIFEST"; then ADD_CHANGES=true; fi
          if [ -f "$DELETE_MANIFEST" ] && grep -q '<types>' "$DELETE_MANIFEST"; then DELETE_CHANGES=true; fi
          echo "add_changes=$ADD_CHANGES" >> $GITHUB_OUTPUT
          echo "delete_changes=$DELETE_CHANGES" >> $GITHUB_OUTPUT

      # 3つのデプロイシナリオに応じたデプロイコマンド
      # 追加・変更のみ
      # 削除のみ
      # 追加・変更と削除の両方
      - name: Deploy Additions and Deletions
        if: steps.check_changes.outputs.add_changes == 'true' && steps.check_changes.outputs.delete_changes == 'true'
        run: |
          sf project deploy start \
            --manifest ./output/package/package.xml \
            --post-destructive-changes ./output/destructiveChanges/destructiveChanges.xml \
            --test-level RunLocalTests \
            --target-org deployOrg
      # ... (他の2つのデプロイステップもあるが省略) ...

一気に増えましたね。
ただ追加されたのはシンプルなものと比べて

  1. 差分取得用のプラグインのインストール
  2. 差分の生成
  3. 差分の状態に応じてデプロイ方法を変える

という内容です。

しかし、まだこのパイプラインでも問題があります。
それは、「デプロイが失敗した際に環境ドリフトが発生してしまう」という問題です。

環境ドリフトとは?

「環境ドリフト」とは、Gitリポジトリ(コードの正史)と、Salesforce組織(実際の環境)の状態が、意図せずズレてしまう現象を指します。このパイプラインでは、以下の最悪のシナリオで発生します。

  1. 開発者がmainブランチにコードをマージします。
  2. CI/CDが起動し、差分デプロイを実行しますが、テストの失敗などで【デプロイが失敗】します。
  3. この時点で、Gitの歴史はマージによって先に進んでいますが、Salesforce組織の状態は古いままです。
  4. 開発者が失敗に気づき、コードを修正して再度マージします。
  5. CI/CDが再び起動しますが、差分計算の基準は「前回失敗したコミット」になるため、「Salesforceメタデータに関する差分はゼロ」と誤判断し、デプロイをスキップしてしまいます。結果として、Gitと組織の状態が食い違ったまま、誰もそれに気づけない危険な状態に陥ります。

そして、この問題を解決するために次の「自己修復機能(revert)」を組み込みます。

差分デプロイと自己修復機能

今回は追加分を載せます

deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source with full history
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      # ...
      # ... (差分生成、変更検知、3パターンのデプロイ)
      # ...

  # === JOB 2: デプロイが失敗した場合にのみ実行されるジョブ ===
  revert_on_failure:
    runs-on: ubuntu-latest
    needs: deploy # deployジョブの完了を待つ
    if: failure()  # deployジョブが失敗した場合にのみ実行

    steps:
      - name: Checkout source code with full history
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Revert merge on failure
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git revert -m 1 HEAD --no-edit
          # 無限ループを防ぐための [skip ci] を追加
          git commit --amend -m "$(git log -1 --pretty=%B) [skip ci]"
          git push

これを入れることで、マージに失敗した場合でも「一歩先に進んだが、一歩前に戻る」とすることで環境ドリフトを防ぎます。

次に完成形を載せておきます。

完成形

name: Deploy Changes to Staging and Prod Orgs

on:
  push:
    branches:
      - main     # 本番環境 (Production)
      - staging  # 検証環境 (Staging / UAT)

permissions:
  contents: write

jobs:
  # === JOB 1: デプロイを実行するジョブ ===
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source and submodules
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          submodules: 'recursive'

      - name: Install Salesforce CLI
        run: npm install --global @salesforce/cli

      - name: Trust sfdx-git-delta plugin
        run: |
          mkdir -p $HOME/.config/sf
          echo '["sfdx-git-delta"]' > $HOME/.config/sf/unsignedPluginAllowList.json

      - name: Install git-delta plugin
        run: sf plugins install sfdx-git-delta

      - name: Install helper tools (yq for xml parsing)
        run: |
          pip install yq

      - name: Create JWT key file
        run: |
          mkdir -p ./assets
          echo "${{ secrets.SF_JWT_KEY }}" > ./assets/server.key

      - name: Authenticate to Org
        run: |
          USERNAME_SECRET=""
          if [ "${{ github.ref_name }}" == "staging" ]; then
            USERNAME_SECRET="${{ secrets.SF_USERNAME_STAGING }}"
          elif [ "${{ github.ref_name }}" == "main" ]; then
            USERNAME_SECRET="${{ secrets.SF_USERNAME_PROD }}"
          fi
          sf org login jwt \
            --username "$USERNAME_SECRET" \
            --client-id ${{ secrets.SF_CLIENT_ID }} \
            --jwt-key-file assets/server.key \
            --alias deployOrg

      - name: Create output directory
        run: mkdir -p ./output

      - name: Generate package from Git Diff
        id: delta_generator
        run: |
          sf sgd source delta \
            --to HEAD \
            --from HEAD~1 \
            --output-dir ./output
      
      - name: Check for changes to deploy
        id: check_changes
        run: |
          ADD_MANIFEST="./output/package/package.xml"
          DELETE_MANIFEST="./output/destructiveChanges/destructiveChanges.xml"

          ADD_CHANGES=false
          DELETE_CHANGES=false
          echo "--- Checking for metadata changes ---"
          echo "Checking for additions/updates at: $ADD_MANIFEST"
          echo "Checking for deletions at: $DELETE_MANIFEST"

          if [ -f "$ADD_MANIFEST" ] && grep -q '<types>' "$ADD_MANIFEST"; then
            ADD_CHANGES=true
            echo "✅ Found additions/updates."
            echo "--- Added/Modified Components ---"
            xq . < "$ADD_MANIFEST" | jq -r '.Package.types | if type == "array" then .[] else . end | .name as $name | .members | if type == "array" then .[] else . end | "  - \($name): \(.)"'
          else
            echo "No additions/updates found."
          fi

          if [ -f "$DELETE_MANIFEST" ] && grep -q '<types>' "$DELETE_MANIFEST"; then
            DELETE_CHANGES=true
            echo "✅ Found deletions."
            echo "--- Deleted Components ---"
            xq . < "$DELETE_MANIFEST" | jq -r '.Package.types | if type == "array" then .[] else . end | .name as $name | .members | if type == "array" then .[] else . end | "  - \($name): \(.)"'
          else
            echo "No deletions found."
          fi
          
          echo "--- Setting outputs for next steps ---"
          echo "add_changes=$ADD_CHANGES" >> $GITHUB_OUTPUT
          echo "delete_changes=$DELETE_CHANGES" >> $GITHUB_OUTPUT
          echo "Result: add_changes=$ADD_CHANGES, delete_changes=$DELETE_CHANGES"
          
          if [ "$ADD_CHANGES" = true ] || [ "$DELETE_CHANGES" = true ]; then
            echo "changes_exist=true" >> $GITHUB_OUTPUT
            echo "Conclusion: Changes exist, deployment will proceed."
          else
            echo "changes_exist=false" >> $GITHUB_OUTPUT
            echo "Conclusion: No changes exist, deployment will be skipped."
          fi

      - name: Deploy Additions and Deletions
        if: steps.check_changes.outputs.add_changes == 'true' && steps.check_changes.outputs.delete_changes == 'true'
        run: |
          echo "Deploying additions and deletions..."
          sf project deploy start \
            --manifest ./output/package/package.xml \
            --post-destructive-changes ./output/destructiveChanges/destructiveChanges.xml \
            --test-level RunLocalTests \
            --target-org deployOrg

      - name: Deploy Additions/Updates Only
        if: steps.check_changes.outputs.add_changes == 'true' && steps.check_changes.outputs.delete_changes == 'false'
        run: |
          echo "Deploying additions/updates only..."
          sf project deploy start \
            --manifest ./output/package/package.xml \
            --test-level RunLocalTests \
            --target-org deployOrg

      - name: Deploy Deletions Only
        if: steps.check_changes.outputs.delete_changes == 'true' && steps.check_changes.outputs.add_changes == 'false'
        run: |
          echo "Deploying deletions only..."
          sf project deploy start \
            --manifest ./output/package/package.xml \
            --post-destructive-changes ./output/destructiveChanges/destructiveChanges.xml \
            --test-level RunLocalTests \
            --target-org deployOrg
      
      - name: No changes to deploy
        if: steps.check_changes.outputs.changes_exist == 'false'
        run: echo "No metadata changes detected. Deployment step was correctly skipped."

  # === JOB 2: デプロイが失敗した場合にのみ実行されるジョブ ===
  revert_on_failure:
    runs-on: ubuntu-latest
    needs: deploy
    if: failure()

    steps:
      - name: Checkout source code with full history
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          fetch-depth: 0

      - name: Revert merge on failure
        run: |
          git config user.name "GitHub Actions"
          git config user.email "actions@github.com"
          git revert -m 1 HEAD --no-edit
          git commit --amend -m "$(git log -1 --pretty=%B) [skip ci]"
          git push

最後に

これは、始まりのパイプラインに過ぎません。
ここからさまざまなツール連携を入れたり、差分のテストのみをしてデプロイできるようにしたり、解析を入れてエラーを起こしてコード品質を保ったり、いろいろなことを追加することができます。

SalesforceのCI/CDパイプライン構築はgithubと組織という複数にあるコードの統一を、マニフェストファイルを通じて同期させるため複雑になってしまいます。しかし、私を含めCI/CDパイプラインの構築は非常に勉強になるため、ぜひ皆さんもチャレンジしてみてください。そして、その便利さに酔いしれてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?