ふり返る暇なんて無いね

日々のメモ書きをつらつらと。メインブログに書くほどでもないことを流してます

豆: Terraformのcurlプロバイダーよりhttpプロバイダーを使ったほうが良さそう

diary.masasuzu.net

前回 curl プロバイダーを紹介したのですが、 httpプロバイダーを使ったほうが望ましいので前回のサンプルコードを書き換えてみます。パラメータが多少変わるくらいですね。

ドキュメントを見ればわかるのですが、プロバイダー名はcurlなのですが、書き方としてAzure ADを前提にしたような書き方をされていて、品質に不安を覚えます。

それに対して、httpプロバイダーは汎用的にhttpリクエストを送るために書かれており、またhashicorp公式が出しているという安心感があります。

data "http" "github_meta" {
  method = "GET"
  url    = "https://api.github.com/meta"
}

locals {
  github_meta              = jsondecode(data.http.github_meta.response_body)
  github_webhook_cidr      = local.github_meta.hooks
  github_webhook_cidr_ipv4 = [for x in local.github_meta.hooks : x if length(split(".", x)) == 4]
  github_webhook_cidr_ipv6 = [for x in local.github_meta.hooks : x if length(split(":", x)) > 1]

}

resource "aws_ec2_managed_prefix_list" "main" {
  name           = "GitHub Hooks"
  address_family = "IPv4"
  max_entries    = length(local.github_webhook_cidr_ipv4)

  dynamic "entry" {
    for_each = local.github_webhook_cidr_ipv4
    content {
      cidr = entry.value
    }
  }
}

resource "aws_security_group" "main" {
  name   = "Allow GitHub Webhooks"
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "main" {
  security_group_id = aws_security_group.main.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  prefix_list_id    = aws_ec2_managed_prefix_list.main.id
}

output "github_webhook_cidr_ipv4" {
  value = local.github_webhook_cidr_ipv4
}

output "github_webhook_cidr_ipv6" {
  value = local.github_webhook_cidr_ipv6
}

豆: TerraformでAPIから取得した値をリソースのパラメータに使用したい

追記: 豆: Terraformのcurlプロバイダーよりhttpプロバイダーを使ったほうが良さそう - ふり返る暇なんて無いね こっちに書いたようにhttpを使ったほうが良さそうです

それcurlが使えるよ。

curl_curl | Data Sources | anschoewe/curl | Terraform | Terraform Registry

前回マネージドプリフィクスリストを使用する例を載せてましたが、Local値にGitHub WebHookのCIDRを直書きしていたため、GitHub側でCIDRが変更されたときに対応が面倒くさいという欠点がありました。

diary.masasuzu.net

今回はこの値を直書きでなくcurlでAPIから取得してそれを利用する例を紹介します。

IPv4アドレスのものだけフィルターするコードが適当なので、このあたりもっと良い手があれば教えてください。

data "curl" "github_meta" {
  http_method = "GET"
  uri         = "https://api.github.com/meta"
}

locals {
  github_meta              = jsondecode(data.curl.github_meta.response)
  github_webhook_cidr      = local.github_meta.hooks
  github_webhook_cidr_ipv4 = [for x in local.github_meta.hooks : x if length(split(".", x)) == 4]
  github_webhook_cidr_ipv6 = [for x in local.github_meta.hooks : x if length(split(":", x)) > 1]

}

resource "aws_ec2_managed_prefix_list" "main" {
  name           = "GitHub Hooks"
  address_family = "IPv4"
  max_entries    = length(local.github_webhook_cidr_ipv4)

  dynamic "entry" {
    for_each = local.github_webhook_cidr_ipv4
    content {
      cidr = entry.value
    }
  }
}

resource "aws_security_group" "main" {
  name   = "Allow GitHub Webhooks"
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "main" {
  security_group_id = aws_security_group.main.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  prefix_list_id    = aws_ec2_managed_prefix_list.main.id
}

output "github_webhook_cidr_ipv4" {
  value = local.github_webhook_cidr_ipv4
}

output "github_webhook_cidr_ipv6" {
  value = local.github_webhook_cidr_ipv6
}

豆: セキュリティグループに複数のCIDRブロックを指定したいときはマネージドプリフィクスリストを使うといい。

タイトルオンリーです。

GItHub HooksのCIDRから来たhttpsアクセスのみ許可するセキュリティグループを考えます。

マネージドプリフィクス使わない例。CIDRブロックの数だけ、ingress ruleをfor_eachで回して作成する形になります。

locals {
  # https://api.github.com/meta
  github_webhook_cidr = [
    "192.30.252.0/22",
    "185.199.108.0/22",
    "140.82.112.0/20",
    "143.55.64.0/20",
  ]
}

resource "aws_security_group" "main" {
  name   = "no prefix list"
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "main" {
  for_each = toset(local.github_webhook_cidr)

  security_group_id = aws_security_group.main.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  cidr_ipv4         = each.value
}

マネージドプリフィクス使う例。マネージドプリフィクスを指定したingress rule1つだけ作成される形になります。

locals {
  # https://api.github.com/meta
  github_webhook_cidr = [
    "192.30.252.0/22",
    "185.199.108.0/22",
    "140.82.112.0/20",
    "143.55.64.0/20",
  ]
}
resource "aws_ec2_managed_prefix_list" "main" {
  name           = "GitHub Hooks"
  address_family = "IPv4"
  max_entries    = length(local.github_webhook_cidr)

  dynamic "entry" {
    for_each = local.github_webhook_cidr
    content {
      cidr = entry.value
    }
  }
}


resource "aws_security_group" "main" {
  name   = "prefix list"
  vpc_id = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "main" {
  security_group_id = aws_security_group.main.id
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
  prefix_list_id    = aws_ec2_managed_prefix_list.main.id
}

何が嬉しいかというと、CIDRの変更する際に aws_ec2_managed_prefix_list.main のリソースの変更だけで済むということです。ingress ruleに直接CIDRを指定してる場合だと、それぞれのルールの作成削除が行われて、変更差分が見にくくなります。

Artifatct Registryのクリーンアップポリシーを設定する

参考: クリーンアップ ポリシーを構成する  |  Artifact Registry のドキュメント  |  Google Cloud

Artifatct Registryに保存するコンテナイメージが地味にコストになっていたので、不要な古いバージョンのイメージを消したいと思いクリーンアップポリシーを設定しました。

Terraformで書くとこんな感じです。 cleanup_policies ブロックでそれぞれ定義できます。

resource "google_artifact_registry_repository" "this" {
  project       = var.project_id
  location      = var.region
  repository_id = var.repository_id
  format        = "DOCKER"

  # 30日は保持する
  # 5世代は保持する
  # それ以外は削除する
  cleanup_policies {
    action = "KEEP"
    id     = "keep-30d"

    condition {
      newer_than = "2592000s" # 30 days
    }
  }
  cleanup_policies {
    action = "KEEP"
    id     = "keep-5gen"

    most_recent_versions {
      keep_count = 5
    }
  }
  cleanup_policies {
    action = "DELETE"
    id     = "delete-any"

    condition {
    }
  }
}

action="KEEP"になっているものが保持ポリシー、aciont="DELETE"となっているものが削除ポリシーです。

自分は最初、5世代残す保持ポリシーだけ設定していて、いつまで経っても5世代より古いものが削除されずなぜだ?となってました。削除ポリシーを設定しない限り削除されることはないという大ポカでした。 なお、設定後すぐ削除されるわけではなく、1日1回任意のタイミングで削除ジョブが実行されるようです。

削除ポリシーよりも保持ポリシーが優先されます。上記では以下の条件となっています。

  • 5世代は保持する
  • 作成されて30日までは保持する
  • 全て削除する。

この場合、5世代は必ず保持されて、5世代超えていても30日以内なら保持されて、それ以外のものは削除されるものとなります。

そんな感じで、ポリシー設定する前は100GBあったレジストリのサイズを9GBまで節約しました。地味に節約できて満足です。

Terraformの教科書を読んでた

構成としては以下の通り。

[目次]
Part 1:基礎知識
1章 IaCを知る
2章 Terraformのインストール

Part 2:コア・コンセプト
3章 Terraformをはじめよう
4章 Terraformへのディープダイブ
5章 Terraform CLI
6章 Terraformのワークフロー
7章 Terraformのモジュール

Part 3: Terraformによるインフラストラクチャの管理
8章 Terraformの構成ファイル
9章 Terraformスタックを理解する
10章 Terraform CloudとTerraform Enterprise

付録 Terraform用語集/解答と解説

教科書と銘打つ通り、基本的な内容を体系的に説明しているので、これ一冊読めばTerraformをだいたい理解することは出来ると思います。 ただ、Terraformに限らずIaCツールを使いこなすにはクラウドリソースの理解とツール特有の癖を理解する必要があり、ただ書き方を会得しただけでは使いこなすことはむつかしいです。 実際にTerraformで構築して少しずつなれていくしか近道はないです。

自分自身なんとなくで使っていたので、いくつか知らなかったトピックがあったり、理解の確認をすることができ参考になりました。章ごとに問題があり、理解確認できるのはいいですね。 印象に残ったトピックをいくつかメモ代わりに残して起きます。

プロビジョナー

プロビジョナーとはshellやssh、Circle CIなどのサードパーティーツールを通して任意の処理を実行する機能です。本書やドキュメントでも記載されていますが、プロビジョナーは最後の手段であり、Terraformの宣言的記述で解決できないときだけ使うものとなっています。

自分自身そういう場面がなかったので、存在を知らなかったのですが、いざというときの手段として参考になりました。

スタック

このスタックが著者の独自概念なのかは分からないのですが、使い方は面白いとは思いましたが、用途が絞られるかなという印象を受けました。

github上にサンプルコードがあるので見てみると早いのですが、ディレクトリ構成として以下のようになっています。

  • modules
    • モジュールを定義
  • stacks
    • main.tfから各種モジュールを呼び出す
  • stacks_of_stacks
    • stacksを呼び出す

stacks_of_stacksの存在意義を本書を読んだ限りで理解できなかったです。stacks_of_stacksがルートモジュールの扱いならひとつ上のディレクトリにmain.tfを置けば良いのでは?と思いました。

stacksの直下にmain.tfを置くのではなく、さらにディレクトリを切ってstackを複数作り、複数のstackを呼び出す役割としてstacks_of_stacksを作るなら一定の理解はできるかなと思いました。

また、複数のstackを利用する規模になると依存関係や実行時間の問題でstate分割をしたくなる頃なので、概念としては参考にはなりますがあまり実用的には感じない印象です。

まとめ

体系的にTerraformの概要を学ぶには良い本です。ただ、実践的にやっていくには足りないので、各種ドキュメントを読みつつ、実際に手を動かして体得していく必要があります。

この本を読んだ後に必要となるのが下記の詳細Terraformになるんじゃないかなと思って、読み始めてます。

ALBのヘルスチェック間隔でどれくらいだっけ?

HealthCheckIntervalSecondsの設定次第で、デフォルト30秒。5秒から300秒の範囲で指定ができる。

背景

LambdaをALBのバックエンドにしたときにヘルスチェックを設定すると無料の範囲内からはみ出ないか、というところが気になりました。なお、Lambdaの無料枠は1ヶ月あたり100万回となります。

デフォルトの30秒間隔であれば、1分間に2回呼び出されることとなる。1ヶ月を30日とすると以下のような計算式になります。

2(/minute)*60(minutes)*24(hours)*30(days)=86400回

結論としては8万6千回強なので、100万回には程遠い。他のLambdaの使われ方次第ではあるが、気にしなくても良いという結論となりました。

CDKTFでリソースのアトリビュートに対して文字列処理がうまくできなかった件

リソースのアトリビュートを文字列処理しようとしたらうまく動かなかったという現象に出会ったのですが、 結論としては、リソースのアトリビュートはStringではなくTokensという型なので、Stringのメソッドは使えないという話しです。

Tokens - CDK for Terraform | Terraform | HashiCorp Developer

現象

こんな感じでIAM Policyを定義しようとしていました。 タスク定義ARNの末尾には :${リビジョン番号} が付与されてるので、これを削除するためにreplace()を使ってるつもりですが、これはうまく動きません。 意図通りreplaceされることはなく、タスク定義ARNがそのままの値でResourceに渡されてしまいます。

    const taskDefinition = new EcsTaskDefinition(this, "ecs_task_definition", {
      // 省略
    });

    new IamPolicy(this, "ecs_deploy_policy", {
      name: getResourceName("ecs-deploy-policy"),
      policy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [
          {
            Sid: "RegisterTaskDefinitionWithTag",
            Effect: "Allow",
            Action: ["ecs:TagResource"],
            Condition: {
              StringEquals: {
                "ecs:CreateAction": "RegisterTaskDefinition",
              },
            },
            Resource: taskDefinition.arn.replace("/:[1-9][0-9]*$/", ""),
          },
        ],
      })

原因

Tokens - CDK for Terraform | Terraform | HashiCorp Developer

タスク定義ARNの型はStringではなく、Tokens型です。Tokensはモジュールのoutputやリソースのアトリビュートに使われる型で、terraformがapplyされるまで不定の値を示します。

Tokensを文字列として扱うためにはFnを利用してterraformの組み込み関数を呼び出すか、asString()メソッドを呼び出して、Stringに変換してから文字列処理をしてあげる必要があります。

解としては以下の通りになります。

    import { Fn } from "cdktf";

    new IamPolicy(this, "ecs_deploy_policy", {
      name: getResourceName("ecs-deploy-policy"),
      policy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [
          {
            Sid: "RegisterTaskDefinitionWithTag",
            Effect: "Allow",
            Action: ["ecs:TagResource"],
            Condition: {
              StringEquals: {
                "ecs:CreateAction": "RegisterTaskDefinition",
              },
            },
            Resource: Fn.replace(taskDefinition.arn, "/:[1-9][0-9]*$/", ""),
          },
        ],
      }),
    });

Spannerを触りたい。(Spanner構築と接続確認まで)

Cloud SQLでPostgreSQLを使っていたのですが、諸事情でSpannerを検証する必要があるので、ざっくり構築して接続確認するまでのメモです。

Spanner構築

Terraformでざっくり作ります。マルチリージョン構成で、スペックは最低で作ります。 database_dialectをPOSTGRESQLにすることでPostgreSQLと互換が取れます。

###########################################
# Service API
###########################################
resource "google_project_service" "main" {
  for_each = {
    for service in [
      "spanner.googleapis.com",
    ] : service => service
  }
  service = each.key

  disable_dependent_services = true
  disable_on_destroy         = false
}

###########################################
# Spanner
###########################################
resource "google_spanner_instance" "main" {
  config           = "asia1"
  name             = "poc-spanner"
  display_name     = "poc-spanner"
  processing_units = 100

  depends_on = [
    google_project_service.main
  ]
}

resource "google_spanner_database" "database" {
  instance         = google_spanner_instance.main.name
  name             = "poc"
  database_dialect = "POSTGRESQL" 
}

Spannerに接続

SpannerにはPostgreSQLのインターフェースがあるが、これに接続するために、PGAdapterを使用する必要があります。 PGAdapterはローカルで起動して、Spannerに接続するためのプロキシとして働きます。

今回はMac上にPGAdapterをインストールして、接続確認します。

とりあえず、javaのランタイムとpsqlクライアントをインストールします。

brew install java
echo 'export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"' >> ~/.zshrc
sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk
brew install libpq
echo 'export PATH="/opt/homebrew/opt/libpq/bin:$PATH"' >> ~/.zshrc
exec ${SHELL} -l

PGAdapterをダウンロードして、起動します

wget https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz && tar -xzvf pgadapter.tar.gz
java -jar pgadapter.jar -p ${PROJECT_ID} -i ${INSTANCE_NAME} -d ${DATABASE}

ターミナルを別窓で開いてpsqlで接続して雑にテーブルを作ってみます。

% psql -h localhost
psql (16.1, server 14.1)
Type "help" for help.

poc=> CREATE TABLE Staff (
poc-> id    INTEGER    NOT NULL,
poc(> name   TEXT       NOT NULL,
poc(> age    INTEGER    ,
poc(> PRIMARY KEY (id));

気持ちテーブル作成に時間がかかる気がします。

コンソールからテーブルを確認すると作成されていることが確認できます。

いったん接続出来るところまで確認できました。 ここから既存のデータを入れたり、Cloud Run上のアプリケーションから接続したりを確認していきます。

TerraformでEventBridgeのターゲットにCloudWatch Logsにするリソースを構築するとうまく動かない件

現象

Terraformで以下のようにECSイベントが発生した際にそれをCloudWatch Logsに出力するEventBridge ruleとtargetを作成したところ、うまくCloudWatch Logsにイベントが出力されませんでした。

resource "aws_cloudwatch_log_group" "main" {
  name              = "/aws/events/masasuzu/test/ecs/event"
  retention_in_days = 3
}

module "eventbridge" {
  source = "terraform-aws-modules/eventbridge/aws"

  create_bus = false
  create_role = false

  rules = {
    "masasuzu-test-ecs-event-log" = {
      event_pattern = jsonencode({
        "source" : ["aws.ecs"],
      })
      enabled = true
    }
  }

  targets = {
    "masasuzu-test-ecs-event-log" = [
      {
        name = "masasuzu-test-ecs-event-log"
        arn  = aws_cloudwatch_log_group.main.arn
      }
    ]
  }
}

試しにコンソールから同様のリソースを作成したところうまくCloudWatch Logsに出力され、Terraformで作ったEventBridgeも正しく動くようになりました。

ここからわかることはコンソールでEventBridgeの設定をした際に裏側で暗黙的になにかリソースが作られたということです。

原因

EventBridgeからCloudWatch Logsへの出力を許可するためにCloudWatch Logsのリソースポリシーを設定する必要があります。

まっさらなAWSアカウントでは以下のようにCloudWatch Logsのリソースポリシーが設定されています。何も設定されていません。

% aws logs describe-resource-policies --no-cli-pager
{
    "resourcePolicies": []
}

コンソールからEventBridgeの設定をすると以下のような設定が追加されます。これにより、EventBridgeからCloudWatch LogsへのPutLogEventsやCreateLogStreamが許可され、無事イベントがログに出力されるようになります。

% aws logs describe-resource-policies --no-cli-pager
{
    "resourcePolicies": [
        {
            "policyName": "TrustEventsToStoreLogEvents",
            "policyDocument": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Sid\":\"TrustEventsToStoreLogEvent\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":[\"delivery.logs.amazonaws.com\",\"events.amazonaws.com\"]},\"Action\":[\"logs:CreateLogStream\",\"logs:PutLogEvents\"],\"Resource\":\"arn:aws:logs:ap-northeast-1:xxxxxxxxxx:log-group:/aws/events/*:*\"}]}",
            "lastUpdatedTime": 1705025982872
        }
    ]
}

対策

リソースポリシーを設定するために、以下のような記述を追加してあげると良いでしょう。

data "aws_iam_policy_document" "main" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = ["arn:aws:logs:ap-northeast-1:${var.account_id}:log-group:${var.log_group_path}:*"]

    principals {
      identifiers = ["events.amazonaws.com"]
      type        = "Service"
    }
  }
}

resource "aws_cloudwatch_log_resource_policy" "main" {
  policy_document = data.aws_iam_policy_document.main.json
  policy_name     = "EventsToLog"
}

参考: CloudWatch Logs リソースへの許可の管理の概要 - Amazon CloudWatch Logs

参考: aws_cloudwatch_log_resource_policy | Resources | hashicorp/aws | Terraform | Terraform Registry