TFLintを使ったterraformのチェックとカスタムルールの設定

インフラエンジニアの菅原です。

カンムはサービスの運用にAWSを使用し、そのリソースの管理にterraformを使用しています。 リソースの定義はGitHub上でコードとして管理されているので、何かリソースを追加する場合はプルリクエストを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。

しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで機械的にチェックして欲しいところです。

そこでカンムではtflintを導入してチェックの自動化を行うようにしました。

TFLintの導入

github.com

TFLintはterraform用のlinterで、非推奨な書式に警告を出してくれたり、ベストプラクティスを強制することができたりします。 メジャーなプロバイダー(AWS/Azure/GCP)のルールセットはすでに存在しており、カンムではtflint-ruleset-awsを利用しています。

tflintを導入するにはまず対象のtfファイルが置かれているフォルダに .tflint.hcl を作成し tflint --init を実行します。

plugin "aws" {
  enabled = true
  version = "0.12.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}
$ tflint --init
Installing `aws` plugin...
Installed `aws` (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.12.0)

tflintを実行するとルールに違反した箇所を表示してくれます。

$ tflint
3 issue(s) found:

Warning: resource `aws_acm_certificate` needs to contain `create_before_destroy = true` in `lifecycle` block (aws_acm_certificate_lifecycle)

  on route53.tf line 25:
  25: resource "aws_acm_certificate" "stg_example_com" {

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_acm_certificate_lifecycle.md

Notice: "default.redis6.x" is default parameter group. You cannot edit it. (aws_elasticache_replication_group_default_parameter_group)

  on redis.tf line 123:
 123:   parameter_group_name          = "default.redis6.x"

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_elasticache_replication_group_default_parameter_group.md

Error: "t1.2xlarge" is an invalid value as instance_type (aws_instance_invalid_type)

  on ec2.tf line 40:
  40:   instance_type           = "t1.2xlarge"

AWSのルールセットの場合、デフォルトで有効になっているルールはここにあるとおりです。

.tflint.hcl で個々のルールの有効・無効を指定することもできます。

rule  "aws_elasticache_replication_group_default_parameter_group" {
  enabled = false
}

また、tflint-ignore というコメントをつけることで特定の箇所のチェックを無視することもできます。

resource "aws_instance" "my-instance" {
  # tflint-ignore: aws_instance_invalid_type
  instance_type           = "t1.2xlarge"

GitHub Actionsへの組み込み

terraform-lintersからsetup-tflintアクションが提供されているので、簡単にGitHub Actionsに組み込むことができます。

      - uses: actions/checkout@v2
      - run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secret.GITHUB_TOKEN }}
      - run: tflint

reviewdogというツールを使ったアノテーションを入れてくれるアクション reviewdog/action-tflintもありますが、そちらを試したことはないです。

カスタムルールの作成

カンムではRDSの暗号化など必須にしたいポリシーがいくつかあるため、カスタムルールを作成しています。

カスタムルールを作成する場合、まず tflint-ruleset-template をテンプレートとしてGitHubリポジトリを作成します。

たとえば aws_rds_cluster リソースの storage_encrypted をチェックするルールを作成する場合、チェックを書いたGoのソースコードを作成します。

  • rules/aws_rds_cluster_must_be_encrypted.go
package rules

import (
    "fmt"

    hcl "github.com/hashicorp/hcl/v2"
    "github.com/terraform-linters/tflint-plugin-sdk/terraform/configs"
    "github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

type AwsRdsClusterMustBeEncryptedRule struct{}

func NewAwsRdsClusterMustBeEncryptedRule() *AwsRdsClusterMustBeEncryptedRule {
    return &AwsRdsClusterMustBeEncryptedRule{}
}

func (r *AwsRdsClusterMustBeEncryptedRule) Name() string {
    // ルール名
    return "aws_rds_cluster_must_be_encrypted"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Enabled() bool {
    return true
}

func (r *AwsRdsClusterMustBeEncryptedRule) Severity() string {
    return tflint.ERROR
}

func (r *AwsRdsClusterMustBeEncryptedRule) Link() string {
    // ルールのドキュメントのリンク先
    return "https://rule-document.example.com/posts/12345"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Check(runner tflint.Runner) error {
    err := runner.WalkResources("aws_rds_cluster", func(resource *configs.Resource) error {
        content, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
            Attributes: []hcl.AttributeSchema{
                {Name: "storage_encrypted"},
            },
        })

        if diags.HasErrors() {
            return diags
        }

        // storage_encryptedが存在しない場合は違反
        if _, exists := content.Attributes["storage_encrypted"]; !exists {
            return runner.EmitIssue(r, "`storage_encrypted` attribute not found", resource.DeclRange)
        }

        return nil
    })

    if err != nil {
        return err
    }

    return runner.WalkResourceAttributes("aws_rds_cluster", "storage_encrypted", func(attribute *hcl.Attribute) error {
        var storageRncrypted string
        err := runner.EvaluateExpr(attribute.Expr, &storageRncrypted, nil)

        if err != nil {
            return err
        }

        if storageRncrypted == "true" {
            return nil
        }

        // storage_encryptedがtrueでない場合は違反
        return runner.EmitIssueOnExpr(
            r,
            fmt.Sprintf("`storage_encrypted` is %s", storageRncrypted),
            attribute.Expr,
        )
    })
}

そして main.go にルールを追加します。

  • main.go
func main() {
    plugin.Serve(&plugin.ServeOpts{
        RuleSet: &tflint.BuiltinRuleSet{
            Name:    "kanmu",
            Version: "0.2.0",
            Rules: []tflint.Rule{
                rules.NewAwsRdsClusterMustBeEncryptedRule(),
            },
        },
    })
}

これで aws_rds_cluster リソースが storage_encrypted = true でない場合はエラーになります。

Error: `storage_encrypted` is false (aws_rds_cluster_must_be_encrypted)

  on rds.tf line 70:
  70:   storage_encrypted                   = false

Reference: https://rule-document.example.com/posts/12345

カスタムルールセットの配布

カスタムルールをプラグインとして tflint --init でインストールする場合、terraform providerと同様にgpgでの署名が必要になります。

基本的にはterraformのドキュメントに従ってgpgの鍵を作成し、terraform-provider-scaffoldingのGoReleaserの設定を編集して使えば、ルールセットのCIで自動的にインストール可能なアーカイブファイルが作成できます。

また.tflint.hcl にはカスタムルールセットの設定を追加しておきます。

plugin "kanmu" {
  enabled = true
  version = "0.1.0"
  source  = "github.com/xxx/tflint-ruleset-kanmu"

  signing_key = <<-KEY
  -----BEGIN PGP PUBLIC KEY BLOCK-----
  ...
  -----END PGP PUBLIC KEY BLOCK-----
  KEY
}

まとめ

tflintを使うことで、単純なミスをチェックしたりベストプラクティスを強制することができるようになります。 また、独自のルールセットを作成することで、RDSの暗号化のような「作成後に変更できない」設定をCIでチェックできるようになりました。

カンムではインフラの自動化に興味のあるインフラエンジニアを絶賛募集中です。

open.talentio.com