ふり返る暇なんて無いね

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

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

GCSのバケットをすべてコピーしたい(同期したい)

Google Cloud Storageのバケットのリージョンは作成後に変更できません。 そのため、シングルリージョンで作ったバケットをデュアルリージョンまたはマルチリージョンに変えるためには新規で作成し、オブジェクトを全部新しいバケットにコピーした上で切り替えないといけません。 そのためのオブジェクト同期の方法について考えます。

ドキュメントで紹介されている例は転送ジョブとgcloud storage cp gsutil cp ですが、これだとコピーはできますが、バケットの中身を同じにはできません。ここで別解として gsutil rsync を使います。

コマンド例としては以下の通りになります。

gsutil -m rsync -r -d gs://${転送元バケット名} gs://${転送先バケット名}

The -m option typically will provide a large performance boost if either the source or destination (or both) is a cloud URL. If both source and destination are file URLs the -m option will typically thrash the disk and slow synchronization down.

-mオプションはパフォーマンスの向上のため

The rsync -d option is very useful and commonly used, because it provides a means of making the contents of a destination bucket or directory match those of a source bucket or directory. This is done by copying all data from the source to the destination and deleting all other data in the destination that is not in the source. Please exercise caution when you use this option: It's possible to delete large amounts of data accidentally if, for example, you erroneously reverse source and destination.

-rオプションはディレクトリ再帰

The -R and -r options are synonymous. Causes directories, buckets, and bucket subdirectories to be synchronized recursively. If you neglect to use this option gsutil will make only the top-level directory in the source and destination URLs match, skipping any sub-directories.

-dは削除オプションとなり転送元に存在しないオブジェクトが転送先にある場合削除されてしまいます。これをつけることで転送元と転送先が同期できますが、バケットを間違えると大変なことになるので注意が必要です。

google-github-actions/deploy-cloudrunでlabelを付け替えるのをやめさせたい

v0からv2にバージョンアップした際にdeploy-cloudrunでlabelを更新する挙動に変わったので、これをやめさせたいといのが趣旨です。 Cloud Run Serviceのlabelはterraform側で制御したいので、GitHub Actions側で変更されると困るのです。

結論から言うと、 skip_default_labelsを trueにしてあげると良いです。

# (省略)
      - name: Deploy to Cloud Run
        uses: 'google-github-actions/deploy-cloudrun@v2'
        with:
          service: ${{ inputs.SERVICE_NAME }}
          image: ${{ inputs.REPOSITORY }}:${{ github.sha }}
          project_id: ${{ inputs.PROJECT_ID }}
          region: ${{ inputs.REGION }}
          skip_default_labels: true  # <<<<<  これ

ドキュメントの該当箇所は以下の通りです。

skip_default_labels: (Optional) Skip applying the special annotation labels that indicate the deployment came from GitHub Actions. The GitHub Action will automatically apply the following labels which Cloud Run uses to enhance the user experience:

managed-by: github-actions
commit-sha: <sha>

Setting this to true will skip adding these special labels. The default value is false.

EC2インスタンスのuserdataにシェバンがないと動かない

EC2インスタンスのuserdataの実行履歴を見たい - ふり返る暇なんて無いね でログを確認したところ、以下のように言われてしまった。

2023-09-21 02:01:06,809 - __init__.py[WARNING]: Unhandled non-multipart (text/x-not-multipart) userdata: 'b''...'

どうもシェバンがないのでうまくbashスクリプトとして認識してくれなかったようだ。下記1行を先頭に加えて再度起動したところうまく動いた。

#!/bin/bash 

参考 起動時に Linux インスタンスでコマンドを実行する - Amazon Elastic Compute Cloud

ユーザーデータシェルスクリプトは、#! 文字と、スクリプトの読み取り先であるインタープリタのパス (通常は /bin/bash)) で開始する必要があります。シェルスクリプティングに関する有用な紹介文は、Linux ドキュメントプロジェクト (tldp.org) の「BASHプログラミングのハウツー」で入手できます。