インフラエンジニアの菅原です。
カンムはサービスの運用にAWSを使用し、そのリソースの管理にterraformを使用しています。 リソースの定義はGitHub上でコードとして管理されているので、何かリソースを追加する場合はプルリクエストを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。
しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで機械的にチェックして欲しいところです。
そこでカンムではtflintを導入してチェックの自動化を行うようにしました。
TFLintの導入
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でチェックできるようになりました。
カンムではインフラの自動化に興味のあるインフラエンジニアを絶賛募集中です。