Atlantisを使ったオペレーションの検証(あるいはPoor man's Bytebase)

BytebaseのようにSQLだけでなく任意のワンショットのコマンドの実行をAtlantisのワークフローに乗せられるのではないか…ということを思いついたので検証してみた。


まずワンショットのコマンドのterraform providerを作成。

github.com

resource "oneshot_run" "hello" {
  command      = "echo 'hello, oneshot'"
  plan_command = "echo \"hello, oneshot (plan=$ONESHOT_PLAN)\""
}

terraform planplan_commandで実行され、terraform applycommandが実行される(plan_commandはapply時には実行されない)。

plan_commandのログがplan-stdout.log plan-stderr.log に、commandのログが stdout.log stderr.log に出力される。

terraformなので、一度apply(実行)されたら二度目は実行されない。


Atlantisのワークフローの設定は以下の通り。

workflows:
  operation:
    plan:
      steps:
        - init
        - run: echo "[WARN] Plan command was not executed" > plan-stdout.log
        - run: touch plan-stderr.log
        - run:
            command: terraform${ATLANTIS_TERRAFORM_VERSION} plan -input=false -refresh -out $PLANFILE
            output: hide
        - run: cat plan-stdout.log
        - run: cat plan-stderr.log
    apply:
      steps:
        - run: echo "[WARN] Apply command was not executed" > stdout.log
        - run: touch stderr.log
        - run:
            command: terraform${ATLANTIS_TERRAFORM_VERSION} apply $PLANFILE
            output: hide
        - run: cat stdout.log
        - run: cat stderr.log

GitHubのコメントが冗長なのでテンプレートを上書き。

  • プロジェクトごとのapply/delete/planコマンドのコメントを削除
  • <details>...</details>の折りたたみを削除

  • plan_success_unwrapped.tmpl
{{ define "planSuccessUnwrapped" -}}

{{ if .EnableDiffMarkdownFormat }}{{ .DiffMarkdownFormattedTerraformOutput }}{{ else }}{{ .TerraformOutput }}{{ end }}

{{ if .PlanWasDeleted -}}
This plan was not saved because one or more projects failed and automerge requires all plans pass.
{{ end -}}
{{ template "mergedAgain" . -}}
{{ end -}}
  • plan_success_wrapped.tmpl
{{ define "planSuccessWrapped" -}}

{{ if .EnableDiffMarkdownFormat }}{{ .DiffMarkdownFormattedTerraformOutput }}{{ else }}{{ .TerraformOutput }}{{ end }}

{{ if .PlanWasDeleted -}}
This plan was not saved because one or more projects failed and automerge requires all plans pass.
{{ end -}}
{{ .PlanSummary -}}
{{ template "mergedAgain" . -}}
{{ end -}}


任意のコマンドを実行するPRを作成してみる。

resource "oneshot_run" "any_command" {
  command      = <<-EOT
    echo 'これはapplyに実行されるコマンドの出力だよ'
  EOT
  plan_command = <<-EOT
    echo -e "これはplanで実行されるコマンドの出力だよ\n(plan=$ONESHOT_PLAN)"
  EOT
}

Atlantisでplan_commandが実行されて結果がコメントとして追記される。

plan_commandを修正してpush。

diff --git a/project/hello/terraform.tf b/project/hello/terraform.tf
index f1f2851..b88f327 100644
--- a/project/hello/terraform.tf
+++ b/project/hello/terraform.tf
@@ -27,6 +27,6 @@ resource "oneshot_run" "any_command" {
     echo 'これはapplyに実行されるコマンドの出力だよ'
   EOT
   plan_command = <<-EOT
-    echo -e "これはplanで実行されるコマンドの出力だよ\n(plan=$ONESHOT_PLAN)"
+    echo -e "これはplanで実行されるコマンドの出力だよ\n※修正したよ※\n(plan=$ONESHOT_PLAN)"
   EOT
 }

修正したコマンドが実行される。


atlantis applyとコメントするとcommandが実行される。

メモ

resource "example_thing" "example" {
  for_each = fileset("scripts", "**/main.sh")

  command      = each.value
  plan_command = each.value
}
# main.sh
if [ "$ONESHOT_PLAN" = "1" ] ; then
  # (planの処理)
else
  # (applyの処理)
fi
  • atlantis planで検証用のplan_commandが実行されるのは悪くない、ような気がする
  • 任意のコマンドを実行できるのでセキュリティまわりが大変かも
    • 入出力に秘匿情報がある場合はどうしたらよいか…
  • plan/apply時にコマンドではなくterraform自体の出力を出す必要があるかも知れない

AWS Secrets Managerから秘匿値を取得して環境変数を設定しコマンドを実行するツールを作った

envchainのバックエンドにAWS Secrets Managerを使ったようなツールを作った。

github.com

使い方

Secrets Managerに秘匿値を設定した上で

$ aws secretsmanager get-secret-value --secret-id foo/bar
{
  ...
  "SecretString": "BAZ",
  ...

$ aws secretsmanager get-secret-value --secret-id foo/zoo # JSON secret
{
  ...
  "SecretString": "{\"TOKEN\":\"AAA\",\"SECRET\":\"BBB\"}",
  ...

sevの設定ファル(~/.sev.toml)を作成し

[hello]
BAR = "secretsmanager://foo/bar"
ZOO = "secretsmanager://foo/zoo:TOKEN"
BAZ = "BAZBAZBAZ"

[world]
HOGE = "secretsmanager://foo/zoo:SECRET"
FOGA = "secretsmanager://foo/bar"
PIYO = "PIYOPIYOPIYO"

sevでラップしてプロファイル名を指定してコマンドを実行すると、秘匿値が環境変数経由でコマンドに渡される。

$ sev hello -- env
FOO=BAZ
ZOO=AAA
BAZ=BAZBAZBAZ

$ sev world -- env
HOGE=BBB
FUGA=BAZ
PIYO=PIYOPIYOPIYO

設定ファイルにAWS_PROFILEを指定しておくと、デフォルトではそのAWS_PROFILEを使ってSecrets Manager APIを呼び出す。

[hello]
AWS_PROFILE = "my-profile"
BAR = "secretsmanager://foo/bar"
# foo/barの値の取得にmy-profileが使われる

aws_iam_policy_documentを使うとplanの差分が大きくなるパターン

aws_iam_policy_documentを使っていると、terraform planを実行したときに差分が大きくて変更がわかりにくくなることがあったので検証してみた。

具体的には以下のようなパターンで差分が大きくなった。


まず以下のようなtfがあったとして

resource "aws_cloudwatch_log_group" "my-group" {
  name = "my-group"
}

resource "aws_iam_role" "log-writer" {
  name = "log-writer"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      },
    ]
  })
}

data "aws_iam_policy_document" "put-log" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:DescribeLogStreams",
      "logs:PutLogEvents",
      "logs:GetLogEvents",
    ]

    resources = [
      # 普通、aws_cloudwatch_log_group.my-group.arn を使うが、説明のために便宜上
      "arn:aws:logs:ap-northeast-1:XXX:log-group:${aws_cloudwatch_log_group.my-group.name}:*",
    ]
  }
}

resource "aws_iam_role_policy" "put-log" {
  role = aws_iam_role.log-writer.name
  name = "put-log"
  policy = data.aws_iam_policy_document.put-log.json
}

aws_cloudwatch_log_group.my-groupのname属性を変更する。

diff --git a/terraform.tf b/terraform.tf
index cea67e4..d806f57 100644
--- a/terraform.tf
+++ b/terraform.tf
@@ -14,7 +14,7 @@ provider "aws" {
 }

 resource "aws_cloudwatch_log_group" "my-group" {
-  name = "my-group"
+  name = "my-group2"
 }

 resource "aws_iam_role" "log-writer" {

terraform planの差分は以下のようになる。

  # aws_iam_role_policy.put-log will be updated in-place
  ~ resource "aws_iam_role_policy" "put-log" {
        id          = "log-writer:put-log"
        name        = "put-log"
      ~ policy      = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = [
                          - "logs:CreateLogStream",
                          - "logs:DescribeLogStreams",
                          - "logs:PutLogEvents",
                          - "logs:GetLogEvents",
                        ]
                      - Effect   = "Allow"
                      - Resource = "arn:aws:logs:ap-northeast-1:XXX:log-group:my-group:*"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        # (2 unchanged attributes hidden)
    }

これをjsonencodeで直接ポリシーを指定するとポリシーの差分だけ表示される。

resource "aws_iam_role_policy" "put-log" {
  role = aws_iam_role.log-writer.name
  name = "put-log"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogStream",
          "logs:DescribeLogStreams",
          "logs:PutLogEvents",
          "logs:GetLogEvents",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:ap-northeast-1:XXX:log-group:${aws_cloudwatch_log_group.my-group.name}:*"
      },
    ]
  })
}

  # aws_iam_role_policy.put-log will be updated in-place
  ~ resource "aws_iam_role_policy" "put-log" {
        id          = "log-writer:put-log"
        name        = "put-log"
      ~ policy      = jsonencode(
          ~ {
              ~ Statement = [
                  ~ {
                      ~ Resource = "arn:aws:logs:ap-northeast-1:XXX:log-group:my-group:*" -> "arn:aws:logs:ap-northeast-1:XXX:log-group:my-group2:*"
                        # (2 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (2 unchanged attributes hidden)
    }

name属性のようにリソースの変更前に値がわかっているようなものだと、同様の現象が発生しそう。

追記

id:MIZZY さんにコメントをいただいたので、aws_cloudwatch_log_group.my-group.name をlocal変数に格納してみた。

resource "aws_cloudwatch_log_group" "my-group" {
  name = "my-group2"
}

locals {
  my_group_name = aws_cloudwatch_log_group.my-group.name
}

data "aws_iam_policy_document" "put-log" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:DescribeLogStreams",
      "logs:PutLogEvents",
      "logs:GetLogEvents",
    ]

    resources = [
      "arn:aws:logs:ap-northeast-1:822997939312:log-group:${local.my_group_name}:*",
    ]
  }
}

いったんlocal変数に格納すると、差分が最小限になる。