Skip to main content

· 4 min read
Shunsuke Suzuki

GitHub Actions の勉強がてら Pull Request (以下 PR) で変更されたファイルや PR Label に応じて Matrix build を実行する Github Actions の Workflow のサンプルを書いてみました。

https://github.com/suzuki-shunsuke/example-github-actions-dynamic-matrix

Monorepo で同じ Job を PR で変更されたものに対してだけ実行したい、 けど workflow をサービスごとに定義するのはめんどいみたいな場合に使えるかもしれません。

勉強がてらちょっと書いてみて軽く動作確認しただけなので、バグってる、あるいは実用的ではないかもしれません。

ここでは Monorepo の CI を GitHub Actions で実行する場合を考えます。

GitHub Actions では path filter を用いて workflow の実行有無を制御することができます。 そこでサービスごとに workflow を作成し、 path filter を設定することでそのサービスが更新されたときのみそのサービスの CI を実行するということが簡単にできます。

しかし多くのサービスが含まれる Monorepo で各サービスに同じ Job を実行したい場合を考えてみましょう。 その場合サービスを追加するたびに workflow を追加していく必要があります。 まぁ .github/workflows 配下に 1 つ YAML をコピペで作成するだけといえばそれまでなのですが、それすらも省略したいとしましょう。

Terraform の CI/CD を CodeBuild に移行した話では CodeBuild の Batch Build の buildspec を PR で変更されたファイルおよび PR Label に応じて動的に生成しています。 これの良いところは、サービスを追加したり、リネームしたり、削除したりしても CI をイジる必要がまったくないところです。

それと似たようなことを GitHub Actions でもやってみました。

foo と bar という 2 つのサービス(Go のアプリケーション)があり、 CI では setup job で CI を実行するサービスのリストを動的に生成し、後続の build job で対象サービスの build を実行しています。 例えばリポジトリ直下の README.md だけを更新した場合、どのサービスのビルドも実行されません。 サービス foo の main.go だけを更新した場合、サービス foo のビルドだけ実行され、 bar のビルドは実行されません。 target/<サービス名> という PR Label をつけて CI を実行すると指定したサービスのコードが変更されていなくてもビルドを実行できます。

主に以下のツールを使いつつ、シェルスクリプトで実装しています。

詳細は https://github.com/suzuki-shunsuke/example-github-actions-dynamic-matrix のコードと Demo 用の PR を見てください。

· 4 min read
Shunsuke Suzuki

Terraform v0.14 で local で terraform init すると lock ファイルが更新されてしまう問題に対応しました。

結論を最初に言うと、 100 以上の Terraform 環境をいい感じに v0.14 に upgrade した方法で紹介している方法で Renovate で Terraform Provider を update する際に terraform init -upgrade を実行して lock ファイルを更新してコミット・プッシュしているのですが、 その際に terraform providers lock -platform=darwin_amd64 を実行するようにしました。

Terraform v0.14 で lock ファイル .terraform.lock.hcl が導入されました。 Renovate で Terraform Provider を update する際にも lock ファイルを更新する必要があるので、 terraform init -upgrade を実行して lock ファイルを更新してコミット・プッシュしています。 なのですが、ローカルで terraform init を実行するとなんか lock ファイルが更新されることが良くありました。しばらく放置していたのですが、 developer から「なんかファイル更新されたんだけど、これコミットしていいの?」と聞かれ、このまま放っておいて困惑させたりもやっとさせたりするのは良くないなと思い、調べてみました。

lock ファイルについて .terraform.lock.hcl 完全に理解したで詳しく解説されていたので大変助かりました。

  • lock ファイルには provider の hash 値が記録されている
  • lock ファイルは terraform init で自動的に更新される
  • hash 値は platform (Mac, Linux, etc) によって違う
  • terraform init 実行時に、その platform の hash 値が lock ファイルになければ追加される
    • デフォルトでは実行環境以外の Platform の hash 値は追加されない
  • CI は Linux 上で実行しているので、 Linux の hash 値だけが記録される
  • ローカルで Mac 上で terraform init すると Mac の hash 値が追加され、 lock ファイルに差分が生じる

なので差分が出てしまった場合はコミットするで良いとは思いますが、そもそも CI で lock ファイルを更新する際に Mac の hash 値も追加してしまえばローカルで Mac 上で terraform init しても差分が出なくなります。ちなみに Windows 上で terraform init する人は自分の周りにはいなさそうなので、 Windows は対応しないことにしました。

100 以上の Terraform 環境をいい感じに v0.14 に upgrade した方法で紹介しているようにすでに lock ファイルを更新してコミット・プッシュする仕組みはあるので、変更としては 1 (正確にはコードコメント入れて4)行追加するだけでした。

github-comment exec -- terraform providers lock -platform=darwin_amd64

· 6 min read
Shunsuke Suzuki

Terraform Module の使い方として Terraform Module のテンプレートをコピペして使うというアプローチを紹介します。

Terraform の設定ファイル(以下 tfファイル) を書く際、毎回一から書くのは大変です。 多くの場合、既存のコードを再利用したほうが楽でしょう。

Terraform のコードの再利用の仕組みとして、 Module があります。 Module は勿論便利なのですが、使い方には注意が必要で、「安易に Module 化するな。使うな」というふうな考え方もあるでしょう。 自分も基本的に同意見で、 Module を共用するようになると Module への変更がしづらくなったり、パラメータがどんどん増えて複雑になったりします。

例えば次のように共用の local Module を作成するアプローチがあります。

modules/
lambda-base/
README.md
main.tf
variables.tf
outputs.tf
services/
foo/
staging/
main.tf # リポジトリ直下の modules/lambda-base を参照
production/
main.tf # リポジトリ直下の modules/lambda-base を参照

こうすると modules 配下の Module を変更した際にその Module を使っているすべてのサービスに影響が出てしまい、 サービスのオーナーが様々だったり、曖昧だったり不在だったりすると変更が難しいですし、どんどん Module が複雑になったりします。

Module を別のリポジトリでバージョニングして管理し、バージョンを指定するようにするというやり方もありますが、 結構複雑というか考えることが多いアプローチだとは思います。

Terraform にそこまで詳しくない developer にも書いてもらうとなると、シンプルなアプローチにするのが望ましいでしょう(当然これは組織によりますが)。

そこで Module のテンプレートを用意し、 Module を使いたくなったらそれをコピペして使うというアプローチがあります。 例えば lambda-base という Module の Template を foo というサービスの staging 環境と production 環境で使う場合、次のような感じになります。

services/
foo/
staging/
modules/
lambda-base/ # templates からコピー
main.tf
production/
modules/
lambda-base/ # templates からコピー
main.tf
templates/ # Module のテンプレートを置いておく
lambda-base/ # 中身は普通の Module
README.md
main.tf
variables.tf
outputs.tf

こうすると 2 つの Module はそれぞれ独立しているため、変更がしやすくなりますし、シンプルに保つことが出来ます。

テンプレートエンジンとかは使わない

Module の Template をコピペする際に、コードに変数を埋め込んでコピペする際に置換したりとか、高度なテンプレートエンジンを使って動的に内容を変えたりといったことも考えられますが、 個人的には複雑度が上がるのでやらないほうが良いかなと思っています。 変数であれば Module の input variable として渡せばよいし、テンプレートエンジン使いたいのであれば、テンプレートを分けたり、 HCL で表現したりすればよいのではという気がします。

考えられるデメリット

通常の Module と比べて、デメリットとしては以下のようなことが考えられます。

  • コード量が増える(DRY じゃない)
  • Module を一箇所で管理できない
    • 一括して変更を加えることが出来ない
    • 設定を強制することができない

しかし、個人的にはこれは大した問題じゃないと思っています。

コードが増えることに関しては、それはそうとしか言いようがありません。 一括して変更を加えることが出来ないのは、トレードオフだと思っていて、むしろ対処のサービスを限定しながら Module に変更を加えられるというメリットのほうが大きいと思っています。 設定を強制することができないのは、たしかにそれはあると思っていて、コピペした Module に一括して変更を加えたり Conftest などでテストする仕組みが必要かなと思います。

· One min read
Shunsuke Suzuki

· One min read
Shunsuke Suzuki

· One min read
Shunsuke Suzuki

· 2 min read
Shunsuke Suzuki

2020-12-01 から 2020-12-31 にかけて仕事でやったことを書ける範囲で書きます。

  • AWS SAM Application の開発
  • Renovate の PR にリンクを追加
  • Terraform
    • Terraform の CI に関して日々行っている改善点・変更点をチームにシェア
    • Docker Compose を用いてローカルで開発しやすいように改善
    • ドキュメント・コードコメントの追加
    • リファクタリング
      • 不要なコードの削除
      • 不要な secret を削除
      • 不要な変数の削除
      • data.terraform_remote_state を local values に置換
      • なぜか環境変数でパラメータを渡していた箇所を、 local value に置換
    • CI に tflint の導入
    • 対象の build が 1 つの場合 batch build を実行しないようにする
    • master の HEAD じゃなくても apply できるようにする
    • plan file を S3 に保存
    • refactor: tfsec で設定ファイルを使うようにする
    • Renovate の PR が多すぎて鬱陶しい問題の対応
      • automerge されるものは reviewer を設定しないようにした
      • prConcurrentLimit を 1 にした
      • branch protection Require branches to be up to date before merging を無効化
  • kube-linter
    • Rule に基づいて manifest の修正
  • miam でリソースが削除されそうなときに警告をするようにした
  • ブログの執筆

· 13 min read
Shunsuke Suzuki

terraformer で雑に生成した Terraform の設定ファイル (以下 tf ファイル) と state を分割したくてツールを書きました。

tfmigrator

経緯

miam から Terraform へ移行したい

miam というツールで管理されている大量のリソースを Terraform で管理したくなりました。 多くの AWS Resource は Terraform で管理されていますが、 IAM に関しては miam で管理されています。 なぜ Terraform ではなく miam で管理されているかというと、当時のことは自分には分かりませんが、歴史的な経緯もあると思います。 昔は今よりも Terraform の表現力が豊かではなく、 Ruby で自由にかける miam のほうが扱いやすかったとか、 miam だと miam でリソースを管理することを強制できるため、権限管理を厳格にやるという観点では都合が良いという点もあるかと思います。

ではなぜ Terraform で管理したくなったかというと、 一番大きな理由は miam で頻繁に rate limit に引っかかるようになったからです。 Terraform にしろ miam にしろ CI/CD で test, apply が実行されるようになっています。 miam では毎回全部のリソースを対象に処理が実行されるため、リソースの数が増えるにつれて rate limit に引っかかりやすくなります。 CI を rerun すれば成功するのですが、悪いときは 3 回連続で rate limit に引っかかり、 4 回目でようやく成功するということもありました。

Terraform ではサービスや環境単位で State が分割されており、 CI も PR でファイルが変更された state に対してのみ実行されるため、 rate limit に引っかかることは基本ないようになっています。

他にも色々理由はあるのですが、本題からそれるのでやめておきます。 rate limit だけなら miam でも exclude する機能があるので頑張ればなんとかなる気はします。

やりたかったこと

  • Terraform で既存のリソースを管理したい
    • tf ファイル と state を生成したい
  • Terraform の設定ファイル と state はサービス・環境ごとに分割したい
  • Terraform のリソースパスはヒューマンリーダブルにしたい
    • これは難しければ諦めるのもありだが、できればやりたい

terraformer で自動生成するも色々問題があった

まずは terraformer で雑に tf ファイル と state を生成しました。 今回 terraformer を使うのは初めてで、 terraformer で万事解決なら話は簡単だったのですが、話はそんな簡単ではありませんでした。 これに関しては、自分の問題(使い方を間違っている、ドキュメントをちゃんと読んでいない)なのか、 あるいは terraformer 側の問題なのかよく分かってない部分もあります。

まずドキュメントを読んでもいまいちリソースのフィルタリングの仕方が分かりませんでした。 試しに IAM role の名前を指定してそれだけ import しようとしましたが、なぜか全 IAM リソースが import されてしまいました。

良くわからないので、これは全 IAM リソースを雑に import してからサービス・環境ごとに分割するしか無いかなぁと思いました。

加えて、リソースパスが全然ヒューマンリーダブルではありませんでした。 terraformer としてはこれは仕方ないと思いますが、なんとかリネームしたいと思いました。

最初手作業で始めるも、自動化が必要と悟る

最初特定のサービスに関して手作業で tf ファイル, state を移行する作業を行ってみました。 簡単なシェルスクリプトを書いて半自動化してみました。 tf ファイルの操作には hcledit が便利です。

以下のようなコマンドを使いました。

  • terraform state mv
  • hcledit block get
  • hcledit block mv
  • hcledit block list

で、やってみたものの、なにせ対象が多いので、これを一つ一つ手作業でやるのは大変だし、ヒューマンエラーは避けられないと感じました。 そこでちょっとしたツールを作ることにしました。 手作業で一回やった分手順はイメージできているので、割と簡単にできるだろうと思いました。

tfmigrator

そこで作ったのが tfmigrator です。 今回は AWS の IAM リソースを扱いますが、 tfmigrator は特定の provider などには依存しないツールです。 Terraform CLI と hcledit が必要です。

まず terraformer で IAM リソースを全部 import してきます。

$ terraformer import aws -r iam --compact --path-pattern .

こうすると resources.tf と terraform.tfstate が作られます。

tfmigrator の設定ファイル tfmigrator.yaml を書きます。

---
items:
# 既に Terraform で管理されているものは無視
- rule: '"tags" in Values && Values.tags.ManagedBy == "Terraform"'
exclude: true
- rule: '"name" not in Values'
exclude: true
# `name` に "foo" が含まれているものはサービス foo のリソースとみなして分割
- rule: 'Values.name contains "foo"'
state_out: foo/terraform.tfstate
resource_name: "{{.Values.name}}"
tf_path: foo/resource.tf
# 以下略

tfmigrator の処理の流れをなんとなくそれっぽい擬似言語で表現します。 実際の処理の流れとは若干異なりますが、雰囲気が伝わればと思います。

for resource in state
for item config.items
if item.rule.match(resource)
if item.exclude
# このリソースは処理せず次のリソースの処理に移る
break
# tf の migration (note: 元の tf はそのまま)
hcledit block get resource.path < tf | hcledit block mv resource.path "${resource.type}.${item.resource_name(resource)}" >> item.tf_path
terraform state mv
# 次のリソースの処理に移る
break

各 item の設定の意味はこんな感じです。

  • rule: expr の expression 。この条件にマッチしたリソースをこの item で処理する
  • exclude: true の場合、この item にマッチしたリソースは無視する
  • state_out: terraform state mv-state-out
  • resource_name: 新しいリソース名。デフォルトでは名前はそのまま。 Go の text/template で処理されます
  • tf_path: Terraform の設定ファイルの出力先

設定ファイルを書いたら tfmigrator を実行します。 いきなりマイグレーションをするというよりは、まずは dry run てきなことをして動作を確認したいですね。 -skip-state をつけると terraform state mv を skip し、分割される tf ファイル を新しいファイルに出力だけします。

$ cat *.tf | tfmigrator run -skip-state

生成された tf ファイル を眺めて、良さそうなら -skip-state をとって実行します。

tf ファイル生成時の注意点

tf ファイルの生成は現状追記モードで実行されます。なので -skip-state をつけて複数回実行すると同じ設定が重複して書き込まれることになります。 それが困る場合は実行前に対象ファイルを消してから実行してください。

また、tf ファイルの移行は既存の tf ファイルに対して hcledit block get, hcledit block mv を実行して行われるため、 元の tf ファイルはそのまま残ること、また expression は評価されないことに注意が必要です。 例えば name = var.name のように変数を参照している場合、それもそのまま評価されずに残ります。 とりあえず自分がやりたかったのは terraformer で生成した tf ファイルの移行だったので、そんなに問題にはならないだろうと思っています。

エラーハンドリング

あるリソースの処理でエラーが起こったら即座に異常終了するようにしています。 (当然ですが)ロールバックとかはしません(し、できません)。 エラー出力しつつ次のリソースの処理に移る、というのも考えられますが、間違って terraform state mv されると面倒なので、現状即座に終了するようにしています。 問題のあるリソースを無視したい場合は、 tfmigrator の設定でそのリソースにマッチする item を追加し exclude: true とすればよいでしょう。

このツールの便利なところ

  • expr を用いてリソースを分類できる
  • 設定ファイルに記述し、ワン・コマンドで実行できる
    • レビューできる
    • 後で見返せる

ローカルで試行錯誤しながら複数のコマンドを実行していると、後でなにやったかわからなくなりがちですし、途中で作業を中断したりすると、あとで今どういう状態なのか分からなくなったりします。 ワン・コマンドで実行できるとそういう問題がなくて便利ですね。

肝心の移行はできたのか

まだできていません。移行するために tfmigrator を作ったので、これから移行していこうという段階です。 なので tfmigrator はまだ全然使い込んでないですし、使ってく中で機能修正したりすることもあると思います。

最後に

terraformer で雑に import してきた tf ファイルと state をいい感じに分割するために tfmigrator というツールを作りました。 tfmigrator が役に立つケースは割と限られているというか、日常的に使うようなツールでもないですが、 terraformer で雑に import してきたのは良いが、扱いに困っているなんて人には役に立つかもしれません。

· 5 min read
Shunsuke Suzuki

skaffold を用いてマニフェストを動的に生成しつつ GitOps する方法を考えたので紹介します。 なお、現時点ではあくまで考えてみただけで実際に導入したりはしていません。

GitOps はマニフェストを Git リポジトリにコミットしないといけないわけですが、 Docker image をビルド、プッシュし、マニフェストの image tag を書き換えるという一連の処理をどうやってやるのがいいのか 個人的に考えていました。

自分は FluxCD には詳しくないのですが、 FluxCD では registry をポーリングして自動で最新のタグに書き換える機能があるそうですね。

https://toolkit.fluxcd.io/guides/image-update/

ただし、まだ alpha であることと、 semver に従っていないといけないようです。 これだと master branch が update されるたびに image をビルドして sha でタグを付与するみたいな運用は難しそうです。

Skaffold だとマニフェストの image tag を自動で書き換えてくれる機能があります。 加えて skaffold render コマンドを使うと manifest の apply はせずにファイルへの出力だけやってくれます。 出力された manifest を Git リポジトリに commit, push すれば GitOps が実現できそうです。

How

リポジトリを 2 つ用意します。

  • app: アプリケーションのコードとマニフェストを管理するリポジトリ
  • manifest: GitOps が連携するマニフェストを管理するリポジトリ

app は Monorepo になっているとします。ディレクトリ構成は次のような感じをイメージしています。

services/
foo/
skaffold.yaml
Dockerfile
kubernetes/
base/
kustomization.yaml
deployment.yaml
overlays/
develop/
kustomization.yaml
production/
kustomization.yaml

skaffold.yaml

---
apiVersion: skaffold/v2beta10
kind: Config
build:
tagPolicy:
gitCommit:
prefix: develop-
artifacts:
- image: foo
deploy:
kustomize:
paths:
- kubernetes/overlays/develop
profiles:
- name: production
build:
tagPolicy:
gitCommit:
prefix: production-
deploy:
kustomize:
paths:
- kubernetes/overlays/production

profile によって develop と production を区別しています。

manifest では tag を指定しないようにします。

    spec:
containers:
- name: foo
image: foo

skaffold render で manifest を生成します。

$ skaffold render \
-p production \
--default-repo xxx.dkr.ecr.ap-northeast-1.amazonaws.com \
--offline=true \
--loud=true \
--output manifest.yaml

--loud=true をつけないとなにも出力しないのでなにが起こってるのかわかりません。

生成されたマニフェストをみると tag が自動で付与されています。

      - image: xxx.dkr.ecr.ap-northeast-1.amazonaws.com/foo:production-e3a42e0@sha256:7032af912c511ab0c8353c28604461d8960833144953fb50853f087db55ffdd0

生成した manifest を manifest リポジトリに commit, push します。 manifest リポジトリを checkout してきて git コマンドでやることもできますが、 ghcp を使うとコマンド1つでできるので便利です。

manifest リポジトリは対象のサービス及び環境ごとにブランチを分けることにします。 そうしたほうが push の際に失敗しにくいと思います(多分。分からないけど)。

$ ghcp commit -u suzuki-shunsuke -r manifest -b "foo/production" -m "message" manifest.yaml

message は実際にはもっと詳細なメッセージにすべきでしょう。 CI で実行することになると思うので、 build URL を含めたりすると良いでしょう。

こうすると foo/production ブランチに manifest.yaml が push されます。 あとは ArgoCD や FluxCD のような GitOps ツール で foo/production の manifest.yaml をデプロイすればよいはずです。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: foo
namespace: argocd
spec:
source:
repoURL: https://github.com/suzuki-shunsuke/manifest.git
targetRevision: foo/production
path: manifest.yaml
destination:
server: https://kubernetes.default.svc
namespace: production

さいごに

以上、 Skaffold を使っていい感じに tag を書き換えて GitOps する仕組みを考えてみました。 Skaffold 使うと Docker image の build, push といった一連の流れも全部やってくれるので楽で良いなと感じました。

GitOps じゃなくても CIOps でも Skaffold でデプロイすると便利そうだなと思いました。 その場合は skaffold render ではなく、 skaffold run になりそうですね。

https://skaffold.dev/docs/workflows/ci-cd/

· 2 min read
Shunsuke Suzuki

mercari/tfnotify を Fork して 2 つほど OSS を作りました。

開発の経緯

これまで tfnotify を便利に使わせてもらってたのですが、幾つか改善したいと思うところがあり、本家に PR を投げました。 しかし残念ながらこれまでのところ反応がなく、そこまで本家が活発ではないこと、また他にも色々改修したいところがあったことから、自分でフォークしてメンテすることにしました。 最初は互換性を維持しながら suzuki-shunsuke/tfnotify を開発していました(今もしています)。 しかし、開発を進めるに連れ、自分にとって必要のないプラットフォームなどに関するコードが邪魔であると感じ、それらを消したバージョンを別に開発することにしました。 互換性がなくなることから、名前も変えて tfcmt としました。

https://github.com/suzuki-shunsuke/tfcmt

こういった経緯から、 tfcmt のほうを優先的に開発していますが、 tfcmt で実装した機能を後から suzuki-shunsuke/tfnotify にも実装してたりもします。

Fork 元のバージョン

suzuki-shunsuke/tfnotifymercari/tfnotify v0.7.0 fb178d8 をフォークしました。 一方 tfcmt は suzuki-shunsuke/tfnotify v1.3.3 をフォークしました。

mercari/tfnotify との違い

本家との違いは Release Note とドキュメントを参照してください。