TerraformのCIツールであるAtlantisを導入しました

こんにちは、今年の春に新卒でリクルートに入社し、『スタディサプリENGLISH』SREグループ所属となった巻田です。

他の記事でも書かれていますが、『スタディサプリENGLISH』ではTerraformを使って主にAWS上のインフラ管理を行っています。この記事ではTerraformの動作の自動化のためにAtlantisを導入した経緯やその際の設定、使い方などに関して解説します。

導入に至った経緯

Terraformはインフラ管理のために非常に便利なツールです。このチームでは1つのRepository内にdev、staging、productionのように複数のTerraform Working Directoryを配置して各環境のインフラ構築を行なってきました。

今まではインフラの構成を変更する際には以下のような作業を行なっていました。

  1. Terraformのコードを編集
  2. 手元でplanしてみる
  3. (動作確認したい場合は)手元でapplyする
  4. Pull Requestを出してレビューしてもらう
  5. Pull Requestをマージする (この前後で必要に応じてapplyもする)

このように、今まではそれぞれの環境ごとに手元のPC上でterraform planterraform applyを実行していました。しかし、この方法ではチームの人数が増えていくに従って、複数人が同じ環境に同時にapplyしてしまい、他の人が作成したリソースを消してしまうなどの競合が発生していました。

このような問題を解消するために何らかのCIツールを使いたいということでAtlantisを導入するに至りました。このツールを選んだ理由は、変更のPull Requestを出した後に比較的自由なタイミングでapplyの操作が行えるなど今までの運用フローに比較的近い使い方ができると考えたためです。

Atlantisの機能

AtlantisはGitHub Appsとして動作し、主にPull Request上のコメントを使ってコマンドを実行します。Pull Requestでの操作以外にもWeb GUIでも一部操作が可能で、ロックの状態の確認やロックの解除などが行えます(詳細は後述)。また、PlanやApplyの際のコンソール出力を見ることもできます。
(GitHub以外のVCSと連携させることも可能なようです。)

Atlantisの機能として代表的なのは以下のものが挙げられます。

  • Pull Requestにおける自動plan
  • Pull Requestのコメントからの操作
  • 複数人で同じ環境を操作しないためのロック

Pull Requestにおける自動plan

Terraformのファイルを編集してPull Requestを出すと自動的にterraform planを実行して結果をコメントする機能です。

このような感じでplan結果をコメントしてくれます。Diffを見ることもできます。
(このスクリーンショットにあるようなDiffを表示する際には--enable-diff-markdown-formatのオプション1)https://www.runatlantis.io/docs/server-configuration.html#enable-diff-markdown-formatを有効にすると見やすくなります。)

Pull Requestのコメントからの操作

この画像のような感じでatlantisでコマンドを実行して、plan, applyを実行することができます。

複数人で同じ環境を操作しないためのロック

Atlantisには各Working Directoryごとにロックを掛ける機能があり2)https://www.runatlantis.io/docs/locking.html、これによって複数人が同時に同じTerraform Working Directoryに対して変更を加えるのを防止することができます。また、AtlantisのWeb GUIにはロックの一覧を表示する機能があり、誰がどの環境をどのPull Request上で変更しようとしているかを簡単に見ることができます。
(このロックはTerraformの機能であるState Lockとは別物です。)

Conftestを用いたPolicy Checking

Atlantisによるplanの終了後、自動的にConftestを実行する機能です。Conftest3)https://www.conftest.dev/とはyamlやjsonといったデータに対してRegoという言語で書かれたポリシーを満たしているかを確認することができるツールです。

Atlantisによるplanではその結果をファイルに保存し、それをもとにapplyを実行します。このファイルにはplanの結果としてインフラ構成をどのように変更するのか、最終的にどのような構成になるのかといった情報が含まれます。また、terraform showコマンドを使用することでこのファイルをjsonの形式で書き出すことができます。ConftestをTerraformと組み合わせて使用する際にはこのjsonファイルに対してポリシーを満たしているかを確認することが一般的です。

このようなjsonファイルの具体例を以下に示します。これは、既にenglish-log-devという名前のS3 Bucketが既に存在して、それに加えてimage_bucketという名前のS3 Bucketを新規作成しようとしている状態です。

{
    "format_version": "1.1",
    "terraform_version": "1.2.0",
    "planned_values": {...},
    "resource_changes": [
        {
            "address": "aws_s3_bucket.english_log",
            "mode": "managed",
            "type": "aws_s3_bucket",
            "name": "english_log",
            "provider_name": "registry.terraform.io/hashicorp/aws",
            "change": {
                "actions": [
                    "no-op"
                ],
                "before": {
                    "arn": "arn:aws:s3:::english-log-dev",
                    "bucket": "english-log-dev",
                    ...
                },
                "after": {
                    "arn": "arn:aws:s3:::english-log-dev",
                    "bucket": "english-log-dev",
                    ...
                },
                "after_unknown": {},
                ...
            }
        },
        {
            "address": "aws_s3_bucket.image_bucket",
            "mode": "managed",
            "type": "aws_s3_bucket",
            "name": "image_bucket",
            "provider_name": "registry.terraform.io/hashicorp/aws",
            "change": {
                "actions": [
                    "create"
                ],
                "before": null,
                "after": {
                    "bucket": "image_bucket",
                    ...
                },
                "after_unknown": {
                    "arn": true,
                    "bucket_domain_name": true,
                    "bucket_regional_domain_name": true,
                    ...
                },
                ...
            }
        }
    ],
    "prior_state": {...},
    "configuration": {...}
}

このjsonに対して以下のようなポリシーを使ったチェックを行うと新しく作成するS3 Bucketはポリシーに違反していることが分かります。(このポリシーではS3 Bucketの名称にアンダーバーを含んではいけないことを意味しています。)

package main
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    contains(resource.change.after.bucket, "_")
    msg := sprintf("Bucket name MUST NOT contain \"_\". resource: %s", [resource.address])
}

AtlantisではConftestのポリシーはTerraformのコードとは別のRepositoryにて管理することが推奨されています4)https://www.runatlantis.io/docs/policy-checking.htmlが、ポリシーが同じRepositorに含まれていた方が運用が楽になることから、標準的なワークフローを少し変更してRepository内のポリシーを使えるようにしています。(実際の設定は後述)

導入・各種設定

Atlantisは自分達でサーバーを立てて運用する必要があります。『スタディサプリENGLISH』では開発用のツールを動かしているEKSクラスターがあり、Atlantisもそのクラスタ上で動かしています。公式でKustomizeやHelm Chartなどが配布されているのでインストール自体は簡単です5)https://www.runatlantis.io/docs/deployment.html。Kustomizeで配布されたファイルをベースにして一部設定を上書きして使用しています。

Atlantisの設定には大きく分けて

  • 起動時のオプション・環境変数による設定
  • サーバー側に配置する設定ファイルによる設定
  • Repository内に配置する設定ファイル

の3種類の方法があります。

起動時のオプション・環境変数による設定の中にはGitHubの認証情報などの設定が入っています6)https://www.runatlantis.io/docs/server-configuration.html

サーバー側に配置する設定ファイルとRepository内に配置する設定ファイルにはほぼ同じ内容を設定可能であるため、サーバー側に配置する設定ファイルには最低限の項目のみを書き、Repository内の設定ファイルほとんどの設定を記述しています。このファイルには主にplanやapplyの際に実行される処理に関する設定を入れています。

Repository内の設定ファイルの一部を以下に示します。(一部実際のものから修正しています)

version: 3
projects:
- name: dev
  dir: aws/development/terraform
  terraform_version: v1.2.1
  workflow: dev
  autoplan:
  when_modified:
  - "./**"
  - "../../../module/**"
- name: stg
  dir: aws/staging/terraform
  terraform_version: v1.2.1
  workflow: stg
  autoplan:
  when_modified:
  - "./**"
  - "../../../module/**"
workflows:
  dev:
    plan:
      steps:
      - env:
        name: AWS_ROLE_ARN
        value: "arn:aws:iam::123456789012:role/atlantis-plan-role-dev"
      - init
      - plan
    apply:
      steps:
      - env:
        name: AWS_ROLE_ARN
        value: "arn:aws:iam::123456789012:role/atlantis-apply-role-dev"
      - apply
    policy_check:
      steps:
      - show
      - run: conftest test -p ../../../conftest-policies/aws $SHOWFILE
  stg:
    plan:
      steps:
      - env:
        name: AWS_ROLE_ARN
        value: "arn:aws:iam::123456789012:role/atlantis-plan-role-stg"
      - init
      - plan
    apply:
      steps:
      - env:
        name: AWS_ROLE_ARN
        value: "arn:aws:iam::123456789012:role/atlantis-apply-role-stg"
      - apply
    policy_check:
      steps:
      - show
      - run: conftest test -p ../../../conftest-policies/aws $SHOWFILE

また、このRepositoryの大まかなディレクトリ構成は以下のような感じです。

.
├── aws
│   ├── development
│   └── staging
├── module
│   ├── module1
│   └── module2
└── conftest-policies
    └── aws

このように、planやapplyの際の動作をかなり柔軟に設定できるのがAtlantisの特徴の一つです。

EKSにはIRSA7)https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.htmlという機能があり、これを使うことでPodを実行するService Accountに対してAWSのIAM Roleを紐づけることができます。この設定ファイルにおいても各Working Directoryのplan、applyごとにIAM Roleを作成してそれを使用するようにしています。通常1つのService Accountに対しては1つのIAM Roleを紐づけて使用することが多いですが、自分達でAWS_ROLE_ARNの環境変数を設定することで複数のIAM Roleを使い分けることができます8)https://aws.amazon.com/jp/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts/

先程も紹介したConftestはpolicy_checkの設定に記載しています。terraform showを実行した後にconftestコマンドが実行されます。

Atlantis導入後のインフラ構築の流れ

Atlantis導入後のTerraformコードの修正は以下のような手順になりました。

  1. Terraformのコードを編集
  2. コミットしてPull Requestを出す
  3. Atlantisが自動的にplan及びconftestを実行して結果をPull Requestにコメントする
  4. レビュー前に動作確認が必要であればこの時点でPR上でapplyする
  5. コードレビューでApproveをもらう
  6. まだapplyしていなければapplyする

このようにTerraformに関する操作は全てGitHubのPull Request上で完結させることができます。また、従来からの方法のように複数人の変更が競合することも防止でき、誰がどこを変更しようとしているかを簡単に見ることができるようになりました。

苦労した点

構築の際にいくつかハマったポイントがあったので解決方法とともに紹介します。

モジュールの更新時に全てのWorking Directoryでplanが走ってしまう

先程掲載したRepository内の設定ファイルではmodule/ディレクトリ内のコードが編集されると全てのTerraform Working Directoryでplanが走ってしまいます。それにより、全てのTerraform Working Directoryがロックされてしまい、他の作業に影響が出る場合があります。
特にrenovateによるTerraform Providerのバージョン更新によってこのような現象が頻繁に起こっていました。

そのため、同じRepository内においてあるモジュールに関してはrequired_providersでの指定からバージョン指定をしないようにしました。モジュールの呼び出し元ではバージョン指定が行われているためバージョンの指定がなくてもそれほど問題にならなさそうだという判断です。

EKSのトークン取得

TerraformでEKSを構築するためにAWS公式のモジュールを使用しています。このモジュールを使用することでクラスターの構築だけでなくクラスターのOIDC ProviderをIAMに登録したり、クラスターのアクセス権を管理したりすることができます。EKSにおいてクラスターのアクセス権はkube-systemnamespace内のaws-authというConfigMapの内容を用いてIAM UserやIAM RoleとEKS上のGroupとをマッピングすることで管理されています9)https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html

そのため、内部的にこのモジュールはKubernetes Providerを用いてConfigMapを作成、更新しています。従って、planを行うRoleにはConfigMapの読み取り権限、applyを行うRoleにはConfigMapの書き込み権限を与える必要があります。

Kubernetes Providerに与える情報として、EKSクラスターでの認証のためのトークンを取得する必要があります。従来は以下のような方法でトークンを取得していました。(Terraformコードは一部省略しています。)

data "aws_eks_cluster_auth" "english_cluster_auth {
  name = module.support-tools-es-cluster-v1-21.cluster_id
}
provider "kubernetes" {
  token                  = data.aws_eks_cluster_auth.english_cluster_auth.token
  ....
}

Atlantisではplanの際にその結果としてどのリソースをどのように追加、変更、削除するかなどを全てファイルに保存し、applyの際にそれを読み込んで実際に操作を行います。しかし、上記の方法でEKSのトークンを取得してしまうとplanの際に取得したトークンがそのまま使われてしまい、aws-authのConfigMapの変更が権限不足により行えないという問題が起こりました。

そのため、以下のコードのように毎回awscliを実行してトークンを取得するように変更しました。(公式で配布されているAtlantisのコンテナイメージにはawscliが入っていないため公式のイメージをベースとしてawscliをインストールしたイメージを作成してそれを使用しています。)

provider "kubernetes" {
  exec {
    api_version = "client.authentication.k8s.io/v1alpha1"
    args        = ["eks", "get-token", "--cluster-name", "english-cluster"]
    command     = "aws"
  }
  ...
}

導入途中に意図しないplanが行われる

Atlantisを導入後、Repository内の設定ファイルが存在しない状態の時にはAtlantisは特定の条件に当てはまるディレクトリをTerraform Working Directoryとして自動的に認識し、Terraformのコードを変更したPull Requestに対して自動的にplanを行います。単にplanが行われるだけで実害はありませんが、厄介に感じる際には以下のような空の設定ファイルをあらかじめRepositoryに追加しておくと良いでしょう。

version: 3
projects: []

まとめ

この記事では『スタディサプリENGLISH』においてAtlantisを導入した経緯やその際の使い方などに関して解説しました。よく「日本語の記事が少ない」などと書かれているAtlantisですが、設定次第で非常に便利に使うことができます。TeraformのCI化にお困りの際には一度検討してみることをお勧めします。