ふり返る暇なんて無いね

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

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プログラミングのハウツー」で入手できます。

terraformでDataDogリソースを管理したかったのに403 Forbiddenと言われたとき

現象

terraformでdatadogプロバイダーを使ってdatadogリソースを管理しようとしていたのですが、apiキーもappキーも正しいものを使ってるはずなのに、403が出てこまりました。

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: 403 Forbidden
│ 
│   with module.datadog.provider["registry.terraform.io/datadog/datadog"],
│   on modules/datadog/provider.tf line 19, in provider "datadog":
│   19: provider "datadog" {
│ 
╵

原因

当初こんな感じに設定していました。api_urlの指定をしてませんでした。

provider "datadog" {
  api_key = var.datadog_api_key
  app_key = var.datadog_app_key
}

providerのソースコードを読んでもデフォルト値が分からないのですが、おそらく https://app.datadoghq.com の値がセットされてしまってるのかなと推定しています。 今回東京リージョン(AP1)を使っています。東京リージョンのURLは https://ap1.datadoghq.com なのでこの値api_urlを明示的にセットしてあげることで、planが通るようになりました。

variable "datadog_api_url" {
  type    = string
  default = "https://ap1.datadoghq.com"
}


provider "datadog" {
  api_key = var.datadog_api_key
  app_key = var.datadog_app_key
  api_url = var.datadog_api_url
}

参考

CloudSQL(PostgreSQL)のmax_connectionsを上げたい

割り当てと上限  |  Cloud SQL ドキュメント  |  Google Cloud にありますが、max_connectionsフラグを設定することで設定変更できます。

MySQL max_connections フラグを使用すると、接続数の上限を構成できます。MySQL で最大 100,000 件の接続が可能です。データベースに接続して次のコマンドを実行すると、インスタンスの接続数上限を確認できます。 SHOW VARIABLES LIKE "max_connections";

PostgreSQL max_connections フラグを使用すると、接続数の上限を構成できます。Cloud SQL for PostgreSQL インスタンスを作成すると、マシンタイプ構成設定により、選択したコア数に基づき、自動的に利用可能なメモリサイズの範囲が調整されます。これにより、インスタンスに設定される当初のデフォルトの接続数上限も設定されます。

データベースに接続して次のコマンドを実行すると、インスタンスの接続数上限を確認できます。SELECT * FROM pg_settings WHERE name = 'max_connections';

terraform で書くとするとこんな感じでしょうか。

resource "google_sql_database_instance" "main" {
  name             = "db_instance"
  database_version = "POSTGRES_14"
  region           = "asia-northeast1"

  settings {
    tier = "db-f1-micro"
    database_flags {
      name  = "max_connections"
      value = 100
    }
  }
}

なお、デフォルトのmax_connectionsはメモリーの容量によって自動的に設定されます。db-f1-microならメモリ0.6GBなのでmax_connectionsは25となります。

DB接続回りでほかに注意する点としてはCloud Runインスタンス当たりの接続数は100までに制限されています。

Cloud Run に関する上限 Cloud Run サービスでは、Cloud SQL データベースに対する接続数が 100 に制限されています。この上限はサービス インスタンスごとに適用されます。つまり、Cloud Run サービスの各インスタンスはデータベースに対して 100 接続を保持できるため、スケールした場合にデプロイあたりの接続の合計数が増加する可能性があります。

EventBridgeで受け取ったパラメータを後続のターゲットに渡したい

忘れがちな脳への覚え書きです

正確にはここを読んでください。

Amazon EventBridge input transformation - Amazon EventBridge

サンプルとして、S3バケットのCreate Objectを拾って、バケット名とオブジェクトをECSタスクに環境変数として引き渡す例を記載します。

locals {
  name                    = "eventbridge-input-test"
  ecs_task_definition_arn = "タスク定義ARN"
  ecs_cluster_arn         = "ECS Cluster ARN"
  security_group_id       = "ECSタスクのセキュリティグループ"
  subnets                 = ["ECSタスクのサブネット"]
}

module "source_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "3.14.0"

  bucket = "${local.name}-source"

  versioning = {
    enabled = true
  }
}

# XXX: s3モジュール単体では通知が対応していないので、ここで別途設定する
resource "aws_s3_bucket_notification" "source_bucket" {

  bucket      = module.source_bucket.s3_bucket_id
  eventbridge = true
}

module "collect_event_csv" {
  source  = "terraform-aws-modules/eventbridge/aws"
  version = "1.17.3"

  create_bus        = false
  role_name         = "${local.name}-event"
  attach_ecs_policy = true
  ecs_target_arns   = [local.ecs_task_definition_arn]

  rules = {
    "${local.name}-target-ecs" = {
      description = "create object event"
      event_pattern = jsonencode({
        "source" : ["aws.s3"],
        "detail-type" : ["Object Created"]
        "detail" : {
          "bucket" : {
            "name" : ["${module.source_bucket.s3_bucket_id}"]
          }
        }
      })
    }
  }

  targets = {
    "${local.name}-target-ecs" = [
      {
        name            = "${local.name}-target-ecs"
        arn             = local.ecs_cluster_arn
        attach_role_arn = true
        ecs_target = {
          launch_type         = "FARGATE"
          task_count          = 1
          task_definition_arn = local.ecs_task_definition_arn

          network_configuration = {
            assign_public_ip    = true
            subnets             = local.subnets
            aws_security_groups = [local.security_group_id]
          }
        }
        input_transformer = {
          input_paths = {
            # $のあとにeventに存在する目的のパスを指定する。この場合S3のオブジェクト作成Eventからバケット名が取得出来る
            source_bucket = "$.detail.bucket.name" 
            target_object = "$.detail.object.key"
          }
          input_template = <<TEMPLATE
{
  "containerOverrides": [
    {
      "name": "${local.container_name}, # 書き換えるコンテナ名を指定
      "environment": [
        # input_transformer.input_pathsで指定したsource_bucketの値が実行時に変換される
        { "name": "SOURCE_BUCKET", "value": <source_bucket> }, 
        { "name": "TARGET_OBJECT", "value": <target_object> }
      ]
    }
  ]
}
TEMPLATE
        }
      }
    ]
  }
}