【Developer Hub 実践|第8回】Golden Pathの実装をマスターしよう - 後編

こんにちは、Red HatでOpenShift関連のプリセールスをしている北村です。

前回の記事ではGolden Path実装の前段として、SonarQubeの導入やDeveloper Hubとの連携、そしてAWS ECRへの認証設定などを行いました。

この記事ではいよいよGolden Pathの中身について解説していきます。

今回実装する環境(おさらい)

今回は以下のような環境を構築していきます。

  1. 利用者はDeveloper Hubの画面で必要なパラメータを入力してTemplateを実行する
  2. Skeleton Repoからソースコードを取得し、ユーザーが入力したパラメータを代入する
  3. 新しいGitHubリポジトリ(app用リポジトリとmanifest用リポジトリ)にPushする
  4. Hello worldアプリ用のK8sマニフェストをデプロイするためのApplication CRを作成する
  5. リポジトリの作成を契機にアプリをビルド→デプロイするGitHub Actionsを実行する
  6. ビルドされたコンテナイメージはAWS ECRに保管する
  7. Application CRが新しいGitHubリポジトリ上のK8sマニフェストを検知してアプリをデプロイする

Golden Path実行の準備

今回実行するGolden Pathはこちらのリポジトリを利用します。このリポジトリを自分の環境にForkしてきてください。

Golden Pathの登録

ForkしてきたGolden Pathリポジトリにあるtemplate.yamlをDeveloper Hubに登録していきます。

app-config-rhdh.yamlを修正して、このファイルを常に読み込む状態にします。

app-config-rhdh.yaml

kind: ConfigMap
apiVersion: v1
metadata:
  name: app-config-rhdh
  namespace: rhdh
  annotations:
    rhdh.redhat.com/backstage-name: developer-hub
data:
  app-config-rhdh.yaml: |
    app:
    ...omit...
    backend:
    ...omit...
    auth:
    ...omit...
    integrations:
    ...omit...
    signInPage: github
    catalog:
    ...omit...
      locations:
      - type: url
        target:  https://github.com/<GitHub Organization名>/helloworld-skeleton/blob/main/template.yaml
        schedule:
            frequency: PT60S
            initialDelay: PT30S
            timeout: PT120S
      # ここから下を追記
      - type: url
        target: https://github.com/<GitHub Organization名>/helloworld-goldenpath/blob/main/template.yaml   # 先ほどForkしたリポジトリのtempalte.yamlを指定
        schedule:
            frequency: PT60S
            initialDelay: PT30S
            timeout: PT120S

この設定を反映します。

oc apply -f app-config-rhdh.yaml 

Podが再起動するのでアクセスすると、Templateが1つ追加されていることがわかります。このHello world app with CICDが今回のGolden Pathになります。

まずは実行してみよう

中身の説明は後回しにして、一回このGolden Pathを実行してみましょう。

実際に選択して必要項目を入力していきますが、入力する内容は第5回で作成したものと全く一緒のものになります。

実際にデプロイ後、コンポーネント画面に移動すると、これまでのものとはいくつか異なる点があるかと思います。

  • LinkにApp RepositoryManifest Repositoryの2つが存在する
  • CIというタブが増えている
  • Sonarqubeのスキャン結果を確認できるカードが増えている

Linkを選択すると、ソースコードを管理する<repo-name>-appとK8sマニフェストを管理する<repo-name>-manifestの2種類のリポジトリに遷移できます。これらはGolden Pathの実行によって自動生成されました。

次にCIタブに移動すると、2つのパイプラインが実行されていることが確認できます。ひとつ目がコンテナをビルド・デプロイするためのCICDパイプラインworkflowです。ふたつ目が第6回で作成したTechdocsを作成・登録するためのworkflowです。

Messageの部分のリンクを選択すると、該当のworkflowの詳細画面が表示されます。LinksにあるWorkflow runs on GitHubを選択すると、GitHubの画面にも遷移できます。

実際に遷移してみましょう。すると4つのjobが実行されています。

  • sonar_scan : ソースコードに対してSonarqubeによるスキャンを実行します。
  • build_and_push : コンテナをビルドしてECRにPushします。
  • update_manifests : manifestリポジトリ側のk8sマニフェスト内のイメージタグをビルドしたイメージのものに書き換えます。
  • deploy_application : ArgoCDのSyncを実行してアプリケーションを更新します。

つまり、開発者がこのGolden Pathを実行しただけで、自分たち専用のGitリポジトリが払い出され、その中にサンプルコードが格納されており、かつCIパイプラインが実行され、静的解析が走り、コンテナがビルド・デプロイされることになります。さらにその周辺のツールへもこのコンポーネント画面から簡単にアクセスできるようになりました。便利でしょう?

Golden Pathの中身を確認してみよう

ではこのGolden Pathがどのように実装されているか、中身を見てみましょう。

$ tree -L 1
.
├── README.md
├── app-skeleton
├── manifest-skeleton
└── template.yaml

ディレクトリ直下を見てみると、app-skeletonmanifest-skeleton、そしてtemplate.yamlが存在します。このskeletonリポジトリがGolden Pathを実行し、リポジトリを払い出すときの元となります。

template.yaml

まずはじめにtemplate.yamlの中身を上から順に確認していきます。なお、パラメータ入力の部分は前回のものと同様のため割愛し、Stepの部分から見ていきます。

Fetch App skeleton

  steps:
    - id: fetch-app
      name: Fetch App skeleton
      action: fetch:template
      input:
        url: ./app-skeleton
        values:
          app_name: ${{ parameters.app_name }}
          owner: ${{ parameters.owner }}
          git_repo_name: ${{ parameters.git_repo_name }}
          git_host_url: ${{ parameters.git_host_url }}
          git_owner_name: ${{ parameters.git_owner_name }}
        targetPath: ./app-tenant

    - id: fetch-manifest
      name: Fetch Manifest skeleton
      action: fetch:template
      input:
        url: ./manifest-skeleton
        values:
          app_name: ${{ parameters.app_name }}
          owner: ${{ parameters.owner }}
          git_repo_name: ${{ parameters.git_repo_name }}
          git_host_url: ${{ parameters.git_host_url }}
          git_owner_name: ${{ parameters.git_owner_name }}
        targetPath: ./manifest-tenant

ここは前回のTemplateでも行っていたfetch:templateというactionです。前回ではmanifest-skeletonだけでしたが、今回は開発者が実際にコーディングを行うアプリ用のリポジトリを生成すべく、app-skeletonからもfetchを行っています。

Publish GitHub

    - id: publish-app
      name: Push App Repo to GitHub
      action: publish:github
      input:
        repoUrl: ${{ parameters.git_host_url }}?owner=${{ parameters.git_owner_name }}&repo=${{ parameters.git_repo_name }}-app
        repoVisibility: public
        sourcePath: ./app-tenant
        defaultBranch: develop
        protectDefaultBranch: false

    - id: publish-manifest
      name: Push Manifest Repo to GitHub
      action: publish:github
      input:
        repoUrl: ${{ parameters.git_host_url }}?owner=${{ parameters.git_owner_name }}&repo=${{ parameters.git_repo_name }}-manifest
        repoVisibility: public
        sourcePath: ./manifest-tenant
        defaultBranch: develop
        protectDefaultBranch: false

ここでGitHubにPushを行います。repoUrlの末尾にそれぞれ-app-manifestがついており、この命名規則に従ってリポジトリが生成されます。

ArgoCD Deploy

    - id: argocd
      name: Deploy with ArgoCD
      action: argocd:create-resources
      input:
        appName: ${{ parameters.app_name }}-init
        argoInstance: main
        namespace: openshift-gitops
        repoUrl: https://${{ parameters.git_host_url }}/${{ parameters.git_owner_name }}/${{ parameters.git_repo_name }}-manifest.git
        path: 'argocd/'

ここは前回とほぼ同じく、ArgoCDのapplicationリソースをデプロイするActionです。repoUrlだけmanifestリポジトリをちゃんと参照するように変更されています。

Register Component

    - id: register
      name: Register Catalog into Developer Hub
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish-app'].output.repoContentsUrl }}
        catalogInfoPath: "/catalog-info.yaml"

ここも前回同様、新しく生成されたcatalog-info.yamlをDeveloper Hubに登録しているActionです。今回のcatalog-info.yamlはappリポジトリ側に存在するため、repoContentsUrlの部分だけstepの参照先がpublish-appに変わっています。

Outputs

  output:
    links:
      - title: Componentを開く
        icon: catalog
        entityRef: ${{ steps['register'].output.entityRef }}
      - title: Application Gitリポジトリを開く
        url: ${{ steps['publish-app'].output.remoteUrl }}
        icon: github
      - title: Manifest Gitリポジトリを開く
        url: ${{ steps['publish-manifest'].output.remoteUrl }}
        icon: github

最後にoutputの部分ですが、ここではGolden Pathの完了後にComponent、Application Gitリポジトリ、Manifest Gitリポジトリに遷移できるリンクを生成しています。

以上がtemplate.yamlの全貌です。意外と前回までのTemplateと大差がないことがわかったかと思います。実はこのSofeware Template機能で実行すること事態はそれほどバリエーションが多くなく、結局はいかにSkeletonリポジトリを作り込むかが現時点のDeveloper Hubにおける重要なポイントになります。ただこれから様々なActionが追加され、このSoftware Templateからもさらにいろんな作業ができるようになっていきます(多分)ので、Developer Hubの進化を楽しみしてください。

Manifestリポジトリ

次にManifest用のリポジトリを見ていきましょう。こちらも多少ディレクトリ構成が変わっていますが、大きな変化はありません。

$ tree manifest-skeleton
manifest-skeleton
├── argocd
│   └── application.yaml
└── kustomize
    ├── base
    │   ├── app-sa.yaml
    │   ├── deployment.yaml
    │   ├── ecr-secret.yaml
    │   ├── kustomization.yaml
    │   ├── route.yaml
    │   └── service.yaml
    └── overlays
        └── dev
            └── kustomization.yaml

マニフェストを格納するディレクトリを名をkustomizeとし、その下にbaseoverlaysの階層を設けています。今回の環境ではdevelopブランチのみなので、overlays配下にはdevのみを切っています。

前回と大きく変わった点としては、overlays/dev配下にあるkustomization.yamlです。

kustomize/overlays/dev/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../base

images:
- name: ___IMAGE_URL___@___IMAGE_DIGEST___

このimagesの部分にname: ___IMAGE_URL___@___IMAGE_DIGEST___と記載しています。これはのちほどCIパイプラインの中で新しいイメージタグに書き換えるためにあります。

このために、deployment.yamlにも同様のimage名が設定されています。

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
...omit...
spec:
  ...omit...
  template:
    ...omit...
    spec:
      containers:
      - image: ___IMAGE_URL___@___IMAGE_DIGEST___
    ...omit...

また、base配下にECR Secret OperatorのSecretカスタムリソースをデプロイするecr-secret.yamlと、サービスアカウントをデプロイするためのapp-sa.yamlが追加されています。

ecr-secret.yaml

apiVersion: ecr.mobb.redhat.com/v1alpha1
kind: Secret
metadata:
  name: ecr-secret
spec:
  generated_secret_name: ecr-auto-generated-secret
  ecr_registry: ___AWS_ACCOUNT_ID___.dkr.ecr.___AWS_REGION___.amazonaws.com
  frequency: 10h
  region: ___AWS_REGION___

specにECRの情報や名前を記入することで、ECRの認証情報を持つSecretを自動生成します。

___AWS_ACCOUNT_ID______AWS_REGION___という記載がありますが、こちらはGitHub Actions内で実際の値に置換されるのでこのままでOKです。

app-sa.yaml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ${{ values.app_name }}-sa
imagePullSecrets:
  - name: ecr-auto-generated-secret
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ${{ values.app_name }}-sa-admin
subjects:
- kind: ServiceAccount
  name: ${{ values.app_name }}-sa
  namespace: ${{ values.app_name }}
roleRef:
  kind: ClusterRole
  name: admin
  apiGroup: rbac.authorization.k8s.io

こちらはアプリケーションのデプロイ、すなわち該当イメージのPullに使用されるサービスアカウントです。imagePullSecretsに自動生成されるECR認証情報のSecret名を記入しておくことで、その認証情報を使ってECRからイメージをPullしてきます。

以上がManifestリポジトリの主な変更点です。

Appリポジトリ

次にAppリポジトリについてです。

$ tree app-skeleton -L 1 -a
app-skeleton
├── .dockerignore
├── .github
├── .gitignore
├── Dockerfile
├── README.md
├── catalog-info.yaml
├── docs
├── mkdocs.yml
├── package-lock.json
├── package.json
└── src

今回は簡単なHello Worldアプリのため、ディレクトリ構成もシンプルです。Techdocs生成に必要なdocsmkdocs.yml、そしてコンポーネント画面用のcatalog-info.yamlはこちらのリポジトリに移してあります。

このディレクトリで重要なのはやはりGitHub Action用のworkflowファイルです。今回は新たにビルド・デプロイ用のパイプラインファイルbuild-deploy.yamlが追加されています。早速中身を見てみましょう。

.github/workflows/build-deploy.yaml

SonarQube Scan
jobs:
  sonar_scan:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: SonarQube Scan
        uses: sonarsource/sonarqube-scan-action@master
        env:
          SONAR_TOKEN: ${{ "${{" }} secrets.SONARQUBE_USER_TOKEN }}
          SONAR_HOST_URL: ${{ "${{" }} vars.SONARQUBE_URL }}
        with:
          args: >
            -Dsonar.projectKey="${{ values.app_name }}"

まず最初のJobはSonarqubeによるスキャンです。sonarsource/sonarqube-scan-action@masterアクションを使って実行します。必要な変数はGitHub Organization Secrets/Variablesから取ってきたり、Developer HubのGolden Path Tempalte実行時のユーザー入力値から持ってきます

GitHubから取得する場合の記載方法が${{ "${{" }} secrets.SONARQUBE_USER_TOKEN }}と不思議な記載になっていますが、これはScaffolderのfetch:templateアクションでの変更を防ぐためになります。

奇しくもScaffolderのfetch:templateアクションとGitHubの変数の代入ロジックがともに${{...}}となっており、これをそのまま${{ secrets.SONARQUBE_USER_TOKEN }}と記述してしまうとScaffolderのfetch:templateアクション側で代入が走り、結果としてnull値となってしまいます。今回のように${{ "${{" }} secrets.変数名 }}とすることで、fetch:template${{ "${{" }}${{という変換をしてもらいます。

Build and Push
  build_and_push:
    runs-on: ubuntu-latest
    needs: [sonar_scan]
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v3
        with:
          role-to-assume: arn:aws:iam::${{ "${{" }} secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsPushECRRole
          role-session-name: GitHubActionsSession
          aws-region: ${{ "${{" }} secrets.AWS_REGION }}

      - name: Login to Amazon ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v1
        with:
          mask-password: 'true'

      - name: Create ECR repository if not exists
        run: |
          REPOSITORY_NAME="${{ values.app_name }}"
          if ! aws ecr describe-repositories --repository-names "$REPOSITORY_NAME" > /dev/null 2>&1; then
            aws ecr create-repository --repository-name "$REPOSITORY_NAME"
          fi

      - name: Build Docker image
        run: |
          docker build -t ${{ "${{" }} steps.ecr-login.outputs.registry }}/${{ values.app_name }}:${{ "${{" }} github.sha }} .

      - name: Push Docker image
        run: |
          docker push ${{ "${{" }} steps.ecr-login.outputs.registry }}/${{ values.app_name }}:${{ "${{" }} github.sha }}

次に行っているのがコンテナのビルドとECRへのイメージのPush処理になります。

  • Configure AWS Credentials : 第7回で設定したGitHub Actions用のIAMロールを指定し、認証情報を取得しています。
  • Login to Amazon ECR : ECRへログインします。
  • Create ECR repository if not exists : 該当のECRリポジトリが存在しない場合にECRの作成処理を行います。
  • Build Docker image : コンテナイメージをビルドします
  • Push Docker image : ECRへイメージをPushします。
Update Manifests
  update_manifests:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    needs: [build_and_push]
    steps:
      - name: Create token for manifest repository
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ "${{" }} secrets.GITHUBAPP_ID }}
          private-key: ${{ "${{" }} secrets.GITHUBAPP_PRIVATE_KEY }}
          owner: ${{ values.git_owner_name }}
          repositories: ${{ values.git_repo_name }}-manifest

      - name: Checkout manifest repository
        uses: actions/checkout@v4
        with:
          repository: ${{ values.git_owner_name }}/${{ values.git_repo_name }}-manifest
          path: ${{ values.git_repo_name }}-manifest
          token: ${{ "${{" }} steps.app-token.outputs.token }}

      - name: Install kustomize
        run: |
          curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
          sudo mv kustomize /usr/local/bin

      - name: Update Manifests
        run: |
          cd ${{ values.git_repo_name }}-manifest/kustomize/overlays/dev
          kustomize edit set image ___IMAGE_URL___@___IMAGE_DIGEST___=${{ "${{" }} secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ "${{" }} secrets.AWS_REGION }}.amazonaws.com/${{ values.app_name }}:${{ "${{" }} github.sha }}
          sed -i "s|___AWS_ACCOUNT_ID___|${{ "${{" }} secrets.AWS_ACCOUNT_ID }}|g" ../../base/ecr-secret.yaml
          sed -i "s|___AWS_REGION___|${{ "${{" }} secrets.AWS_REGION }}|g" ../../base/ecr-secret.yaml

      - name: Commit and push changes
        run: |
          cd ${{ values.git_repo_name }}-manifest

          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          git add .
          git diff-index --quiet HEAD || git commit -m "Update kustomize manifests for commit ${{ github.sha }}"

          git push origin HEAD:develop

次に行っているupdate_manifestsJobでは、パイプライン上でmanifestリポジトリをチェックアウトしてきて、今回ビルドしたコンテナイメージの情報をもとにマニフェストを書き換える処理を行っています。

  • Create token for manifest repository : GitHub Appの認証情報を利用して、対象となるManifestリポジトリにアクセスするためのアクセストークンを生成します。
  • Checkout manifest repository : 前のステップで生成したトークンを利用して、対象のManifestリポジトリをチェックアウトします。
  • Install kustomize : 次ステップで利用するkustomizeコマンドをインストールします。
  • Update Manifests : kustomize edit set imageコマンドで、プレースホルダーになっている___IMAGE_URL___@___IMAGE_DIGEST___を実際のECRのURLとgithub.shaで表されるコミット識別子に更新しています。また、sed コマンドで base/ecr-secret.yaml内の AWS アカウントIDおよびリージョンのプレースホルダーを実際の値に置換します。
  • Commit and push changes : 変更内容をコミットし、develop ブランチにプッシュします。
Deploy Application
  deploy_application:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    needs: [update_manifests]
    steps:
      - name: Install ArgoCD CLI
        run: |
          curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
          chmod +x argocd
          sudo mv argocd /usr/local/bin/argocd

      - name: Sync and wait for deployment
        run: |
          argocd login "${{ "${{" }} vars.ARGOCD_INSTANCE_URL }}" --username "${{ "${{" }} secrets.ARGOCD_USERNAME }}" --password "${{ "${{" }} secrets.ARGOCD_PASSWORD }}" --skip-test-tls --grpc-web
          argocd app sync ${{ values.app_name }} --revision develop
          argocd app wait ${{ values.app_name }} --health

最後のJobはArgoCDにSyncコマンドを送信するためのものです。これにより、前のJobで変更したマニフェストの内容を実環境に反映させます。

  • Install ArgoCD CLI : ArgoCD CLIをインストールします。
  • Sync and wait for deployment : ROSA上のArgoCDにログインして、syncを実行します。

以上が今回のCICDパイプラインの全貌です。

おわりに

前回と今回の記事で、簡単ですが実践的なGolden Pathの準備と実行を実現してきました。

これはコンテナ環境におけるCICDのベースとなるものになります。これから様々なJob(例えばイメージのスキャンやSBOMの作成など)を追加して、自分なりのGolden Pathにブラッシュアップしていっていただけばと思います。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。