ArgoCD Pull Request Generatorを使ってプルリクエスト毎に環境を構築する

概要

ArgoCDを使ってプルリクエスト毎にインフラ環境を起動したのでメモ
argo-cd.readthedocs.io

前提

使用ツール

  • Kubernets
  • Istio
  • ArgoCD
  • Helm
  - name: istio/istio/istioctl@1.17.2
  - name: kubernetes/kubectl@v1.26.3
  - name: argoproj/argo-cd@v2.6.7
  - name: helm/helm@v3.11.2

設計

  • Pull Request毎にService,Deployment etcを構築する
  • Pull Request毎にRoute53レコードを作成する
  • アクセス制御はIstioのVirtual Serviceを使う
  • DB、キャッシュは共通のものを使う
  • アプリケーションリポジトリはモノレポ構成であり、複数のアプリケーションを起動する

ワークフロー

  1. アプリケーションリポジトリでPull Requesetを作成する
  2. 変更されたアプリケーションコードを検知して自動でラベルを付与する
    例:foo-api,bar-apiなど
  3. 手動でpreviewラベルを貼るとArgoCDが検知してインフラリソースを作成する
  4. リソースが作成されたらPull ReqeustにURLがコメントされる

Application Setの実装イメージ

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: preview
spec:
  goTemplate: true
  generators:
    - pullRequest:
        github:
          owner: hogeOrg
          repo: fugaRepo
          labels:
            - preview
        requeueAfterSeconds: 1800
  template:
    metadata:
      name: "preview-{{ .number }}"
    spec:
      ignoreDifferences:
        - group: "*"
          kind: ConfigMap
          namespace: hoge
          jsonPointers:
            - /data
      sources:
        - repoURL: "https://github.com/hogeOrg/fugaRepo.git"
          targetRevision: main
          ref: repo
        - repoURL: "ghcr.io/hogeOrg/fugaRepo"
          targetRevision: 0.0.1
          chart: app-a
          helm:
            releaseName: app-a-{{ .number }}
            valueFiles:
              - $repo/clusters/dev/apps/app-a/values.yaml
              - secrets://https://$${GITHUB_TOKEN}@raw.githubusercontent.com/hogeOrg/fugaRepo/main/clusters/dev/apps/app-a/values.secrets.yaml
            parameters:
              - name: nameOverride
                value: "app-a-{{ .number }}"
              - name: "image.tag"
                value: >-
                  {{if has "app-a" .labels -}}
                  sha-{{ substr 0 7 .head_short_sha }}
                  {{- else -}}
                  main
                  {{- end -}}
              - name: "image.pullPolicy"
                value: Always
              - name: "app.datadogServiceName"
                value: "app-a-{{ .number }}"
            values: |
              resources:
                requests:
                  cpu: 500m

        - repoURL: "ghcr.io/hogeOrg/fugaRepo"
          targetRevision: 0.0.1
          chart: virtual-service
          helm:
            releaseName: virtual-service-{{ .number }}
            parameters:
              - name: nameOverride
                value: api-virtual-service-{{ .number }}
            values: |
              annotations:
                external-dns.alpha.kubernetes.io/target: gateway.hoge.fuga.com
                external-dns.alpha.kubernetes.io/alias: "true"
              hosts:
                - api-{{ .number }}.hoge.fuga.com
              httpRoutes:
                - match:
                    - uri:
                        prefix: /app-a
                  route:
                    - destination:
                        host: app-a-{{ .number }}.hoge.svc.cluster.local
                        port:
                          number: 8000
                - match:
                    - uri:
                        prefix: /
                  route:
                    - destination:
                        host: api.hoge.fuga.com
                        port:
                          number: 443

        - chart: app-b
          repoURL: "ghcr.io/hogeOrg/fugaRepo"
          targetRevision: 0.0.1
          helm:
            releaseName: app-b-{{ .number }}
            parameters:
              - name: "nameOverride"
                value: app-b-{{ .number }}
              - name: "image.tag"
                value: >-
                  {{if has "app-b" .labels -}}
                  sha-{{ substr 0 7 .head_short_sha }}
                  {{- else -}}
                  main
                  {{- end -}}
              - name: "app.datadogServiceName"
                value: "app-b-{{ .number }}"
            valueFiles:
              - $repo/clusters/dev/apps/app-b/values.yaml
              - secrets://https://$${GITHUB_TOKEN}@raw.githubusercontent.com/hogeOrg/fugaRepo/main/clusters/dev/apps/app-b/values.secrets.yaml
            values: |
              resources:
                requests:
                  cpu: 500m

      project: "hoge"
      destination:
        server: https://kubernetes.default.svc
        namespace: hoge

      syncPolicy:
        syncOptions:
          - CreateNamespace=true
          - RespectIgnoreDifferences=true
        automated:
          selfHeal: true
          prune: true

ポイント

一意なリソースを作成する

        hosts:
                - api-{{ .number }}.hoge.fuga.com

識別子にプルリクエスト番号を入れておくことで独立した環境を構築する

イメージの動的な更新

              - name: nameOverride
                value: "app-a-{{ .number }}"
              - name: "image.tag"
                value: >-
                  {{if has "app-a" .labels -}}
                  sha-{{ substr 0 7 .head_short_sha }}
                  {{- else -}}
                  main
                  {{- end -}}

このように書いておくことで、

  • アプリケーションコードが変更されている場合はプルリクエスト内で変更されたイメージタグ
  • アプリケーションコードが変更されていない場合はmainタグ

を利用できる。
ArgoCDのApplicationSetはGo templateを利用できる。

https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/GoTemplate/

Virtual Serviceによるリクエスト制御

IstioのVirtual Serviceを使うことでインフラリソースを短い時間で作成できる。
PRを立ち上げたときに時間がかかるリソースは作らないような設計にする。(ロードバランサー、データベースなどなど)

まとめ

過去やったことをざっくりまとめた。 K8sはエコシステムが充実しているので楽。

GitHub ActionsでPull Requestのコメントを更新する

概要

GitHub Actionsでコメントを更新する方法をまとめる
サードパーティアクションはなるべく使わない

一つのコメントを更新する

gh pr commentの--edit-lastを使う

gh --version
gh version 2.49.2 (2024-05-13)
https://github.com/cli/cli/releases/tag/v2.49.2

https://cli.github.com/manual/gh_pr_comment

--edit-last Edit the last comment of the same author

実装例

GitHub Actionsで下記のように書けば

  • 初回はコメントを書く
  • すでにコメントがあったらアップデートする

という処理がかける

      - name: Update comment
        continue-on-error: true
        id: update-comment
        run: gh pr comment ${{ github.event.pull_request.number }} -b "updated" --edit-last
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Add comment
        if: steps.update-comment.outcome == 'failure'
        run: gh pr comment ${{ github.event.pull_request.number }} -b "first comment"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

制限事項

Edit the last comment of the same author

なので、github-actions botなどで処理をすると1つのコメントしか更新できない。
matrix strategy内で実行するとコメントが上書きされる

複数のコメントを更新する

https://github.com/actions/github-script を使う

実装例

      - name: add or update comment
        uses: actions/github-script@v7.0.1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const comment = `${{ matrix.value }}`
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            })
            const botComment = comments.find(comment => {
              return comment.user.type === 'Bot' && comment.body.includes("${{ matrix.value }}")
            })
            if (botComment) {
            github.rest.issues.updateComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: botComment.id,
              body: output
            })
            } else {
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
            }

これによりMatrix strategyを利用しているときもコメントを更新できる

参考

https://github.com/orgs/community/discussions/38895

TerraformのStateやModuleに関する設計

概要

TerraformのStateやModuleに関する設計に関する考慮点をざっくり書く

名前付け

  • ユビキタス言語とディレクトリ名や変数名を合わせて認知負荷を下げる
  • リソース名でなく役割・機能ベースのディレクトリ名にして認知負荷を下げる
    プロダクトのコードネームはユビキタス言語なのでディレクトリ名にしても問題ない
  • ModuleやStateのREADME.mdを書く
    • https://github.com/terraform-docs/terraform-docs を使う
    • うまく説明が書けない場合は設計に問題がある可能性がある
    • チームメンバーが利用できるように考慮する
      いつどのようなときに使えばいいかわからないものを作らない

抽象度

  • 抽象度が低いModuleを作らない
    例:modules/s3など

参考: https://developer.hashicorp.com/terraform/language/modules/develop#when-to-write-a-module

  • 大きく作ってから小さくしていく
    小さいものをまとめるほうがコストが大きいため
    stateやmoduleが小さいときは上記のように抽象化を間違えて不要に小さいパターンが多いため

インフラアーキテクチャ

アーキテクチャ図を円で囲ってstate、moduleの単位を作る

チーム

自分が把握していない変更が出るとapplyしていいかわからずコミュニケーションが発生するためチーム単位で分ける

認証情報

CI実行時などを考慮して認証情報の粒度でディレクトリを分ける

変更(デプロイ)のライフサイクル

DRYの誤用

  • 不要な依存関係につながるので偶発的凝集をModuleにしない

コードの検索性

Webコンソールからリソース名やタグでコード検索したときに探しやすいように記述する

モジュール間の依存

依存関係の把握や管理が困難になるのでモジュール間の依存を作らない
特にモジュールの中で別のモジュールをローカルパス参照しない

参考: https://developer.hashicorp.com/terraform/language/modules/develop/composition

アプリケーションレイヤーとの違い

アプリケーションレイヤーと異なりライフサイクルが長く作り直しの難易度が高く、作り直しに時間がかかることを考慮する

まとめ

  • 適切に名前付けできないのは設計に問題があるサイン
  • なぜその抽象度で設計するか説明できるようになる
  • 公式ドキュメントやAWSGCP公式のModuleを読む

プレビュー環境に必要な要件を整理する

概要

プレビュー環境、マルチステージング環境などと呼ばれる環境について要件を整理する。

要件

  • プルリクエスト(任意のブランチ)のコードをデプロイできる
    • 他の開発者と分離された環境をデプロイできる
  • プルリクエストにラベルを貼るとインフラが作成される
    • プレビュー環境が必要ないプルリクエストを考慮する(Renovateなど)
  • 短い時間で起動できる
    • 3分以上待つのは厳しい
    • 起動するリソースの種類を考慮する
    • ロードバランサーやストレージは起動に時間がかかるので起動しなくて済む設計にする
  • 環境変数を変更できる
  • プルリクエストごとにURLが発行される
    • リクエストはパスベースでルーティングする
  • デプロイされたら通知が来る
    • リポジトリ
    • アプリケーション名
    • コミットハッシュ
    • URL
  • プルリクエストがクローズされたらインフラリソースが削除される

まとめ

プレビュー環境に必要な要件を整理した。
他にも検証用に開発メンバー個人専用の環境を用意する方法もある。

最近の仕事まとめ2023冬

概要

今年もお疲れ様でした。

新規プロジェクトインフラ基盤構築・レビュー

ecspresso利用

既存プロダクトではTerraformでECSをデプロイしていたが、新規プロダクトではecspressoを導入するように推進した。
https://github.com/kayac/ecspresso

Atlantis導入

AWS、Azureのマルチクラウドインフラ基盤をAtlantisで構築した。
https://www.runatlantis.io/

AWSアカウント移行

AWSのアカウント移行作業を進めた。

ECS->EKS移行

ECSからEKS移行を進めた。
https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Generators-Pull-Request/
dev00~10のような運用をしており、動作確認をしたいときに使えないという課題を解決するため、ArgoCDのPull request generatorを使ってPRを作成したらコンテナが立ち上がるようにした。
プロジェクトは諸事情で一時中断させた。  

勉強会

新メンバーに対して、Terraform勉強会とDevOps、CI/CD勉強会を開催した。
分野を増やしつつ毎年継続的にやっていきたい。
講師も交代できるレベルにしていきたい。

登壇系

SRE勉強会のLTと社内カンファレンスのLT枠で登壇した。
プロジェクトの失敗要因やSREの実践について話した。

プロジェクト管理

ロールは決まってないが無いが適切なタイミングでプロジェクト管理について、提案をしている。
チケットのテンプレートを作成したり、KPTしたり、タスクの優先順位を変更したり、DesignDoc書いたりなど。

その他

  • チームや人事の課題に対して適切にエスカレーションし、他部署のメンバーを巻き込んで仕事を進めた。
  • 静的解析の追加
  • Renovateの更新とバージョンアップ作業
  • self hosted runnerの追加
  • チームメンバーの育休に備えてドキュメントの整備、育休中の対応

などコツコツやっている

まとめ

戦略面のカバーをできたのが大きかった。
来年も引き続き組織の再現性、スケーラビリティを意識して仕事していきたい。

Amazon ECRでイメージを保護する

概要

Amazon ECRでイメージを保護したいが保護のアクションがないので対応策

結論

保護したいイメージの削除期間やイメージの個数を大きな数にする

下のようなJSONにすると

  • vから始まるタグ(v1.1.0など)は1年間削除されない
  • sha-から始めるタグは1日で削除される
  • v1.1.0とsha-xxxxxxxがついていた場合は優先順で365日削除されない

という挙動になる

{
  "rules": [
    {
      "action": {
        "type": "expire"
      },
      "selection": {
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 365,
        "tagStatus": "tagged",
        "tagPrefixList": [
          "v"
        ]
      },
      "description": "keep released images",
      "rulePriority": 1
    },
    {
      "action": {
        "type": "expire"
      },
      "selection": {
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1,
        "tagStatus": "tagged",
        "tagPrefixList": [
          "sha-"
        ]
      },
      "description": "remove development images",
      "rulePriority": 2
    }
  ]
}

参考

GCPとAzureにはあった。

https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy#create

https://learn.microsoft.com/en-ca/azure/container-registry/container-registry-image-lock#protect-an-image-from-deletion

Terraformの改善活動まとめ

概要

最近のやっていたTerraformリポジトリの改善活動をまとめる

課題点

リポジトリの分割粒度

prd,stgなどの実行環境ごとにリポジトリが別れており、AWSアカウントとリポジトリが1対1でない。また、明確なルールがない環境のため

  • どのリポジトリにコードを書けばいいかわからない
  • どのmoduleを使えばいいかわからない(terraform-moduleリポジトリがあるのにdev-terraformリポジトリにもモジュールがある)
  • リソースを書く場合リポジトリを間違える、間違いを指摘する手間が増える
  • カスタムスクリプトやドキュメントが重複しており管理コストが高い

stateの粒度

1terraform resourceにつき1stateになっていたため

  • apply作業を何度も実行する必要がある
  • 定期的にapplyされないのでコードとリソースの差分が発生する
  • インフラアーキテクチャの全体像が分かりづらい

カスタムスクリプトによる開発体験の低下

  • メンテナンスできるメンバーが少ない
  • 拡張性の低下
    linterなどの周辺ツールを導入するためにカスタムスクリプトに大幅な変更が必要
    CI変更時も同様
  • terraform applyする対象をファイルで管理するためPR作成時にコンフリクトする

Terraform, AWS providerのバージョンが古い

リソースが細かいことにも相まってバージョンアップ作業が大変

やったこと

リポジトリの統合

リポジトリをmonorepo構成に変更

カスタムスクリプトの脱却

  • カスタムスクリプトを削除しterraformコマンドを実行するように変更
  • apply対象をファイル管理していたものを、変更差分をCIで検出するように変更
    カスタムスクリプトをなくしたことで、

CIの拡張

CodeBuildからGitHub Actionsに変更

  • Plan結果をPull Requestのコメントに表示するように変更
  • apply結果をSlackに通知
  • DynamoDBによるstateのlockの追加

静的解析の追加

  • tfsec、tflint、conftestなどの静的解析を導入

Terraform, AWS providerのバージョンアップ

  • スクリプトを書いて最新版に更新
    Terraformバージョン...0.13,0.14系から1.3系に
    AWSプロバイダー...3系から4系に
  • Renovateでバージョンアップに追従できるように

やれなかったこと

  • stateの統合
  • drift detection

学んだこと

やったことは基本的なことなので、そこから抽象化した学びをメモする  

  • 守れないルールは必要ないので、ルールと仕組み化はセットにする
  • チームとして負債をハンドリングできないと返却に大幅な時間がかかる
  • 負債とは正面から向き合う必要があるか考える
  • 開発組織の人数規模が増えてくると基礎的なフォローアップや、同じことを繰り返し言う必要がある