※ 今はGitHub ActionsでOIDCが使えるので、本記事の内容は少し古いです。*1 現場のルール等で「インフラを触るワークロードはオンプレでしか動かしてはならない」みたいなルールがある場合には多少参考になるかと思います。
SREのたっち(@TatchNicolas)です。
JX通信社では「インフラチーム」のようなものは存在せず、開発したチームが運用までやるFull-cycleなスタイルを取っています。AWS・GCPリソースの管理も特定のメンバーが担当するのではなく、必要とする人が必要な時に作成・修正等を行います。すると、terraformなどIaCのツールを利用する場合に「今リポジトリにあるコードは実態を正しく反映しているのか」「誰かが矛盾する変更を加えていないか」という問題が発生します。
CIツール上でterraformを実行することで、問題の一部は回避できるかもしれませんが、CIにSaaSを利用している場合、クラウド上のリソースを操作する強い権限を持ったAPIキーを外部に預けることになってしまいます。
GCPのCloudBuildを使った解決策 *2、AWSのCodeBuildを使った例*3はそれぞれあるのですが、せっかくterraformをつかうならできるだけ同じ仕組みに載せたいところです。
そこで、「CIの仕組みとしてはSaaS(今回はGitHub Actions)を利用しつつも、実行場所はクラウド内にとどめることでGitOpsの考え方を一部取り入れたterraform運用のしくみ」を作ってみました。
TL;DR;
- Self-hosted runnerを使えば、GitHubにクラウドの認証情報を渡さなくともGitHub Actionsでterraformを実行できる
- Pull Request作成でplan、masterへのマージでapplyして、また定期的にmaster最新コミットでplanして乖離をチェック することでGitOps-likeに運用できる *4
- 発想としては単純で「GCP Service AccountまたはAWS IAM Roleを使ってCIのRunnerにterraform実行に必要な権限を与える」なので、GitLab.comでも同じようにできるはず *5
やってみる
GKE(GCP)・EKS(AWS)にて、実際にやってみます。 AWS・GCPの権限を引き受けることができれば良いので、k8sではなくEC2・GCEでも実現可能なのですが、できるだけクラウドベンダごとの違いがでないようにk8s上のPodとして動かしました。
- GKE・EKSクラスタは構築済みの前提
- RunnerにGCP・AWSの強い権限を持ったAPIキーを渡さない
- GKE・EKSの機能を使ってAPIキー自体を発行しないで権限をRunnerに付与する
- 定期的にplanを実行して、masterの最新commitと食い違いを検知する
1. Runnerをつくる
今回は下記の記事の中で紹介されている gh-runners/gke at master · bharathkkb/gh-runners · GitHub を参考にしました
気を付けるポイントとしては、
- あとで hachicorp/setup-terraform を使うために、unzipをDockerイメージにインストールする必要がある
- 権限付与の動作確認用にgcloudやaws cliなども追加しておくと便利かもしれません
Podが削除されるときにGitHub上のからRunnerを除名する処理をシェルスクリプトで書いて、preStop
のlifecycleで実行するようにしても良いでしょう。
apiVersion: apps/v1 kind: Deployment metadata: namespace: github-runner name: github-runner labels: app: github-runner spec: replicas: 1 selector: matchLabels: app: github-runner template: metadata: labels: app: github-runner spec: serviceAccount: github-runner containers: - name: runner image: gcr.io/your-project-name/github-runner env: - name: REPO_OWNER value: "your_github_name" - name: REPO_NAME value: "your_repo_name" - name: GITHUB_TOKEN valueFrom: secretKeyRef: name: name-of-secret key: GITHUB_TOKEN lifecycle: preStop: exec: command: ["sh", "-c", "'./remove.sh'"]
ちなみにPodが削除されたまま放っておいても、GitHub側で30日経てば自動で取り除いてくれるようです。しかしお片付けはきちんとしたほうが気持ちが良いでしょう。
A self-hosted runner is automatically removed from GitHub if it has not connected to GitHub Actions for more than 30 days.
About self-hosted runners - GitHub Docs
また、今回は使いませんでしたがRunnerの登録/削除などを良い感じに管理してくれる素敵なControllerもありました。
2. Actionを定義する
HashiCorpのチュートリアルにGitHub Actionsを使った自動化の例 があるので、そちらを参考にすれば環境やチームのリポジトリ構成に合わせてすんなり書けると思います。
また、追加で定期的にplanを実行して、tfファイルと実態の乖離を検知するworkflowを定義してみます。
検知の方法は色々あるとおもいますが、planをファイルに書き出して、jsonとしてterraform show
したものをjqでパースして判断してみます。今回はついでに例として、乖離があったらIssueを作成してみます。
下記の例の通りやると乖離が解決されない限り重複するIssueを作成しつづけてしまうので、cronの頻度を落としたり通知先をGitHub IssueではなくSlackにする、復帰のためのapplyまで実行してしまうなど、チームの方針に合わせて調整してみてください。
name: check-drift on: push: schedule: - cron: '*/10 * * * *' jobs: check-drift: runs-on: - self-hosted steps: - uses: actions/checkout@v2 - uses: hashicorp/setup-terraform@v1 with: terraform_version: 0.13.2 - name: Terraform Init id: init run: terraform init - name: Terraform Plan id: plan run: terraform plan -out tfplan - name: Print the result of terraform plan as json id: print_json run: terraform show -json tfplan - name: Check configuration drift id: check env: PLAN_JSON: ${{ steps.print_json.outputs.stdout }} run: | DIFF=$(echo "$PLAN_JSON"| jq -c '.resource_changes[] | select(.change.actions!=["no-op"])') if [ -z "$DIFF" ]; then echo 'No configuration drift detected' echo '::set-output name=STATUS::synced' else echo 'Configuration drift detected!' echo '::set-output name=STATUS::drifted' terraform show tfplan -no-color fi - uses: actions/github-script@0.9.0 id: show_plan if: steps.check.outputs.STATUS == 'drifted' env: PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const output = `\`\`\`${process.env.PLAN}\`\`\``; github.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title: "Configuration drift detected", body: output });
3. Runnerに権限を渡す
GCPのservice accountまたはAWSのIAM roleと、k8s service account(KSA)の紐付けをするterraformのサンプルコードを示しておきます。どちらも github-runner
というk8s namespaceにrunnerをデプロイすることを前提としています。 *6
GCPの場合
GKE上のPodにGCPリソースの操作権限を渡すには、Workload Identityを使います。
resource "google_service_account" "github_runner" { account_id = "github-runner" display_name = "github-runner" } resource "google_project_iam_member" "github_runner_owner" { role = "roles/owner" member = "serviceAccount:${google_service_account.github_runner.email}" } resource "google_service_account_iam_member" "github_runner_wi" { service_account_id = google_service_account.github_runner.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${var.project_id}.svc.id.goog[github-runner/github-runner]" }
紐付けるKSA
apiVersion: v1 kind: ServiceAccount metadata: name: github-runner namespace: github-runner labels: app: github-runner annotations: iam.gke.io/gcp-service-account: github-runner@your-project-id.iam.gserviceaccount.com
AWSの場合
EKS上のPodにAWSリソースの操作権限を渡すには、こちらのドキュメントを参考にします。
resource "aws_iam_role" "github_runner" { name = "GitHubRunnerRole" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "${aws_iam_openid_connect_provider.this.arn}" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "${replace(aws_iam_openid_connect_provider.this.url, "https://", "")}:sub": "system:serviceaccount:github-runner:github-runner" } } } ] } EOF } resource "aws_iam_role_policy_attachment" "github_runner" { role = aws_iam_role.github_runner.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" }
紐付けるKSA
apiVersion: v1 kind: ServiceAccount metadata: name: github-runner namespace: github-runner labels: app: github-runner annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/GitHubRunnerRole
動かしてみる
実際にPull Requestを作成してみると、下記のようなコメントが自動でつきます。
show plan
のところをクリックすれば terraform plan
した内容が確認できます。
確認できたらマージすると、applyが実行されます。
また、定期的にplanして定義と実態の乖離を検知すると、Issueが作成されます。
まとめ
得られたメリット
- GCP・AWSの強い権限をもったAPIキーをGitHubに預けなくてよくなった
- AWSでもGCPでも、仕組みの作り方がほぼ共通で利用・メンテナンスがしやすい
- クラウドベンダごとの違いはterraformとk8sで吸収
- terraformを運用者の手元で実行する必要がなくなった
- 「これって反映済み?どれが実態?」「planが食い違ってしまった、今誰か作業してる?」が発生しなくなる
- tfenvに頼らなくてもterraformのバージョンを指定して実行できる
- terraformの定義と実態の乖離を検知できるようになった
注意点・残った課題
- GitHubのself-hosted runnerはリポジトリ単位か組織単位でしか設定できない
- 組織でterraform用runnerを共有すると、組織以下のすべてのリポジトリがCIでAWS・GCPの強い権限を利用できてしまう
- GitLabにはgroup単位のrunnerを設定できるようですが、GitHubにはそのあたりを細かく設定できなさそうなのでもうひと工夫する必要がありそう
- masterへpushできる人は(アプリケーションでもそうですが)しっかり管理する必要がある
- masterへpushできる人は(与えた権限の範囲で)Runnerを通じて何でも出来てしまうため
- 同じ理由でrunnerを動かしているk8sクラスタ上でのPodへの権限も、きちんと管理する必要がある
- HashiCorpのチュートリアルにあったworkflow定義では、Pull Request作成時にplanした分はファイルへ書き出さず、マージ時に改めてapplyしているので、Pull Requestコメントに書き出したplanと実際に行われるapplyの内容が異なる可能性がある
- planした結果を何処かへ保存して、その内容を参照するようにworkflowを改善できるかも
チームの体制やポリシーによって、持たせる権限やRunnerの運用方法には工夫が必要ですが、「credentialを外に出さない」「GitリポジトリをSingle Source of Truthにする」「乖離を検知する」というGitOpsの特徴を持ったterraform運用が実現できました。少しでも参考になれば幸いです。
*1:https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/
*2:https://cloud.google.com/solutions/managing-infrastructure-as-code
*3:https://medium.com/mixi-developers/terraform-on-aws-codebuild-44dda951fead
*4:GitOpsの定義は https://www.weave.works/technologies/gitops/ や https://www.gitlab.jp/blog/2020/09/03/is-gitops-the-next-big-thing-in-automation/ など揺らぎがあるので、今回はその一部である「権限を外に出さない」「GitリポジトリをSingle Source of Truthとする」を取り入れつつもsyncはCI上で行っていることからGitOps-"like"としました
*5:調べてみると、事例がありました 絶対に止められない超重要サービスをGitOpsで安全に開発できるようにしている話
*6:本記事ではroles/ownerやAdministratorAccessといった強い権限を渡していますが、実際にはチームの方針にあわせて調整してください。