ほたてメモ

日々学んだことをメモメモ

EventBridge Pipe の containerOverrides の設定について

経緯

同じ ECS コンテナを使って、コマンドだけ差し替えて複数の ECS タスクを実行したいという場面があった。

SQS → EventBridge Pipe → ECS という構成で、SQS のキューにコマンドを渡せば ECS タスクを起動できるという形にしたが、EventBridge Pipe の containerOverrides の設定でかなりはまったのでメモ。

詳細

SQS を経由した理由は、ECS タスクをアプリケーション側から起動する際に、直接 ECS タスクを実行すると、ネットワーク設定やタスク定義もアプリケーション側で管理する必要が出てくるので、できたらもう少しシンプルな使い勝手にしたかったため。

EventBridge Pipe から起動するとその辺りの設定を、EventBridge Pipe 側に持たせられるので、アプリケーション側はコマンドだけ渡せばよくなる。 SQS を介しているのでキューの URL だけはアプリケーション側に持つ必要はあるが、最低限にできる。

今回は、コンテナ名とコマンドだけを渡す形にした。

はまったポイント

構成は CDK で管理しているが、EventBridge Pipe のターゲットに ECS を指定する箇所だけを抜粋すると以下のような設定になる。

new CfnPipe(this, 'Pipe', {
  ...
  targetParameters: {
    ecsTaskParameters: {
      taskDefinitionArn: props.taskDefinitionArn,
      taskCount: 1,
      launchType: 'FARGATE',
      enableExecuteCommand: false,
      networkConfiguration: {
        awsvpcConfiguration: {
          subnets: props.subnetIds,
          securityGroups: [ecsSecurityGroup.securityGroupId],
          assignPublicIp: 'DISABLED',
        },
      },
      overrides: {
        containerOverrides: [
          {
            name: 'xxxx'
            command: ['command', 'xxxx']
          },
        ],
      },
    },
  },
  ...
});

また、SQS から渡ってくるイベントは以下のようなフォーマット(サンプル)

{
  "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
  "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...",
  "body": {
    "name": "xxxx",
    "command": ["command", "xxxx"]
  },
  "attributes": {
    "ApproximateReceiveCount": "1",
    "SentTimestamp": "1545082649183",
    "SenderId": "AIDAIENQZJOLO23YVJ4VO",
    "ApproximateFirstReceiveTimestamp": "1545082649185"
  },
  "messageAttributes": {},
  "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3",
  "eventSource": "aws:sqs",
  "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue",
  "awsRegion": "us-east-2"
}

containerOverrides に固定値を書けば、それは機能したが、SQS から受け取ったイベントの内容に含まれる値に置き換える方法が、公式のドキュメントにも具体的な例がなくて困った。

というのも、以下のようにさらっと書かれているだけで詳細が不明だった。

すべての Amazon ECS runTask パラメータは、EcsParameters を使用して明示的に設定されます。これらのパラメータは、すべてのパイプのパラメータと同様に、受信イベントペイロードへの JSON パスを使用するとき、動的に設定できます。

また、途中 inputTemplate を使うという例をいくつか見つけたが、これは全く機能しなかった。

inputTemplate: JSON.stringify({
  containerOverrides: [
    {
      name: '<$.body.name>',
      command: '<$.body.command>',
    },
  ],
}),

最終的に、以下の Issue がかなり役に立ったのだが、固定値で動いてた ecsTaskParameters の設定を変更することで、動的にコマンドを埋め込むことができた。

overrides: {
  containerOverrides: [
    {
      name: EventField.fromPath('$.body.name'),
      command: [EventField.fromPath('$.body.command')],
    },
  ],
},

この設定で、AWS コンソール上で EventBridge Pipe の設定を見ると以下のように表示された。

ContainerOverrides: [{"Command":["$.body.command"],"Name":"$.body.name"}]

$.body.command は配列で渡しているので、文字列の形で大丈夫なのかと思ったが、ECS タスク側には配列で渡されていて正常に動作した。

PHP8.2 で DOM操作をする際のエンティティの変換について

PHP7.4 で HTML を DOMDocument を使って変換する処理をしていたが PHP8.2 に上げたところ、 mb_convert_encoding の Deprecated エラーが出るようになった。

PHP Deprecated:  mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity 

エラーを解消するのに手間取ったので記録として残しておく。

従来の方法

これまでは以下のような変換をしていた。

<?php

// 1. DOMDocument を使いたいが、単独の & を渡すと warning が出るので事前に &amp; に変換する
$escaped = str_replace('&', '&amp;', $html);

// 2. DOMDocument に utf-8 の文字列を渡すとそのままでは文字化けするので、mb_convert_encoding でエンティティに変換する
$encoded = mb_convert_encoding($escaped, 'HTML-ENTITIES', 'utf-8');

// 3. DOM操作をして変換後の HTML を取得する
$doc = new DOMDocument();
$doc->loadHTML($encoded);
$savedHtml = $doc->saveHTML();

// 4. 変換後のHTMLはエンティティのままなので、再度 mb_convert_encoding で元に戻す
$result = mb_convert_encoding($savedHtml, 'utf-8', 'HTML-ENTITIES');

新しい方法

最終的に以下の形にした。

<?php

// 1.事前に & → &amp; に変換する処理は同じ
$escaped = str_replace('&', '&amp;', $html);

// 2. mb_convert_encoding の代わりに mb_encode_numericentity を使う
$map = [0x80, 0x10ffff, 0, 0x1fffff];  // ascii を除くユニコード文字の範囲
$encoded = mb_encode_numericentity($escaped, $map, 'utf-8');

// 3. DOM操作をして変換後の HTML を取得する
$doc = new DOMDocument();
$doc->loadHTML($encoded);
$savedHtml = $doc->saveHTML();

// 4. html_entity_decode でエンティティを元の文字に戻す
$result = html_entity_decode($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'utf-8');

変換処理について

mb_convert_encoding の代わりに、 mb_encode_numericentityhtml_entity_decode を使用したが、それぞれどのように変換されるか実験した。

例として、元の HTML が以下の形だったとする。

<p>&</p>

<p> タグの中は以下のように変化した。

初期状態 あ & ♥ あ & ♥
1. & に変換 あ &amp; ♥ あ &amp; ♥
2. エンティティに変換 &#12354; &amp; &hearts; &#12354; &amp; &#9829;
3. DOM操作 &#12354; &amp; &hearts; &#12354; &amp; &hearts;
4. デコード あ & ♥ あ & ♥
  1. &&amp; の置き換えは DOMDocument に渡すために必要なので、どちらの方式も同じ
  2. mb_convert_encoding では「♥」が名前付きエンティティ (&hearts;) に変換されていたが、 mb_encode_numericentityでは数値エンティティに置き換わっている点が異なる
  3. DOMDocument にエンティティに変換した文字列を渡すと、名前付きエンティティを持っている数値エンティティは名前付きに変換されるため、新方式ではこのタイミングで &hearts; に置き換わっている
  4. 旧方式 (mb_convert_encoding) では名前付き・数値エンティティのどちらも変換されるので、元の文字列に戻る。
    新方式は 2. で使った mb_encode_numericentity の対になる関数は mb_decode_numericentity だが、数値エンティティのみデコードするため、&amp;&hearts; は残ってしまう。 html_entity_decode であればどちらも変換するので、今回はデコードに html_entity_decode を使う形にした。

Devcontainerで立てたAPIサーバに接続できなくなった問題の調査

VSCode で Dev Container を使用して API サーバを立てていたのだが、とあるタイミングでフロントエンドの開発環境から接続できなくなっていた。

構成は以下の通り。

  • バックエンド (Dev Container) : PHP + Nginx
  • フロントエンド : React + Next.js

ローカルからはポート 8000 番でコンテナ側の Nginx の 80 番に接続できるようにしていた。

つまりローカルからは

http://localhost:8000/

で接続できていた。

現象としては、React から fetch() で接続できなくなっていた。

試したところ、コマンドラインから curl で接続しても取得できないが、なぜか wget だと取得できる。

さらに調べたところ、IPv6 (http://[::1]:8000/) だと接続できるが、IPv4だと接続できないという状態になっていた。

そこで LISTEN しているポートを調べたところ以下のようになっていた。

$ lsof -P -i TCP -s TCP:LISTEN
COMMAND     PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Code\x20H  1461 xxxxxxxxxxxxxx   34u  IPv4 0x31fa87a050a0f887      0t0  TCP localhost:8000 (LISTEN)
com.docke 16144 xxxxxxxxxxxxxx  181u  IPv6 0x31fa87a50aea79df      0t0  TCP *:8000 (LISTEN)

2行目は docker なので想定通りのもの。TYPE が IPv6 になっているが Docker はIPv4 でも接続できるみたい。

参考)

1行目が怪しいのでプロセスの詳細を確認。

$ ps -p 1461 -ww
  PID TTY           TIME CMD
 1461 ??         0:12.68 /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper --type=utility --utility-sub-type=node.mojom.NodeService --lang=ja --service-sandbox-type=none --user-data-dir=/Users/xxxxxxxxxxxxxx/Library/Application Support/Code --standard-schemes=vscode-webview,vscode-file --secure-schemes=vscode-webview,vscode-file --bypasscsp-schemes --cors-schemes=vscode-webview,vscode-file --fetch-schemes=vscode-webview,vscode-file --service-worker-schemes=vscode-webview --streaming-schemes --shared-files --field-trial-handle=1718379636,r,6655308202130409097,12078965550584404602,131072 --disable-features=CalculateNativeWinOcclusion,SpareRendererForSitePerProcess

VSCode の Code Helper が 8000 番を握っているように見える。

実際に、Devcontainer を使用しないで、docker-compose を直接実行したら 8000 番で正常に接続できた。

試しに上記のプロセスを kill してみたところ、接続できるようにはなったが、VSCode が不安定になった。 何度か立ち上げ直しているとまたプロセスが復活したので根本解決にはならない。

リモート設定をいじっても変化がなかったが、最終的に以下の設定で解決した。

ターミナルを表示すると、タブに「ポート」がある。 そこに docker ではない 8000 番の設定があるので右クリック→「ポートの転送を停止する」を選択。

これでプロセスも消えて、API が接続できるようになった。

この設定を自分でした記憶がないのだが、したのかなぁ。。

Amazon EventBridge Scheduler ユニバーサルターゲットの設定

TerraformでEventBridge Schedulerを利用してECSを操作しようと思ったときに困ったのでメモ。

targetのarnについて

ユニバーサルターゲットの場合、targetのarnはService ARNにする必要がある。

ドキュメントにも書いてあったが、最初ECS Serviceのarnを設定していて間違えた。

registry.terraform.io

Service ARNについて

Service ARNの一覧は下記に記載がある。

docs.aws.amazon.com

ECSの場合は

arn:aws:scheduler:::aws-sdk:ecs:[apiAction]

となっている。

apiActionのフォーマットが不明だったが、例えば UpdateService の場合は、updateService と最初を小文字にする必要があった。

今回は

arn:aws:scheduler:::aws-sdk:ecs:updateService

で良かった。

inputに渡すjson

jsonは、最初 aws ecs update-service --generate-cli-skeleton で生成したjsonを加工して設定してみたが、この場合キーの先頭は小文字になる。

{
  "cluster" : xxxx,
  "service" : xxxx,
  "desiredCount" : 0
}

これではダメで、実際には下記のように先頭を大文字にする必要があった。

{
  "Cluster" : xxxx,
  "Service" : xxxx,
  "DesiredCount" : 0
}

on_content_end のエラーについて

Chromeのconsoleに以下のログが出ていた。

on_content_end ==>response to loadPagePattern, href=xxxx, pattern.id=undefined

調べても全く情報が出てこなくて困っていたのだが、ウイルスバスターを入れたときに追加された拡張機能が出していると分かった。 拡張機能をOFFにしたら表示されなくなった。

入っていた拡張機能はこちら。

Macでタイムスタンプと日付を相互変換する

いつも忘れるのでメモ

タイムスタンプ→日付

$ date -r 1666230165
2022年 10月20日 木曜日 10時42分45秒 JST

現在時刻のタイムスタンプ

$ date +%s
1666230333

指定した日時のタイムスタンプ

$ date -j -f '%Y-%m-%d %H:%M:%S' '2022-10-01 12:34:56' +%s
1664595296

AWS Client VPNを使って固定IPでインターネットに接続する

試しで、AWS Client VPNを利用してVPN環境を構築した際の記録。

構成

VPN構成

  • 固定IPでインターネットに接続するために、プライベートサブネットからパブリックサブネットにあるNAT Gateway経由でアクセスできる経路を作成する。

必要なリソース

  • VPC
  • サブネット (Public / Private)
  • Internet Gateway
  • NAT Gateway
  • ルートテーブル
  • セキュリティグループ (クライアント VPN エンドポイント用)
  • ACM
  • クライアント VPN エンドポイント

VPCの作成

  • VPN用にVPCを作成する。
  • 今回は、IPv4 CIDR を 172.19.0.0/16 とした。

サブネット

  • Public / Privateそれぞれのサブネットを作成する
  • 各設定は下記のようにした
    • Public
      • CIDR : 172.19.0.0/21
      • パブリック IPv4 アドレスを自動割り当ては「はい」
      • ネットワークACLはデフォルト(全てのトラフィックを通す)
    • Private
      • CIDR : 172.19.16.0/21
      • パブリック IPv4 アドレスを自動割り当ては「いいえ」
      • ネットワークACLはデフォルト(全てのトラフィックを通す)

Internet Gateway

  • インターネットに接続するために Internet Gateway を作成する
  • 上記で作成したVPC上に作成する。

Nat Gateway

  • Privateサブネットからインターネットに接続するために NAT Gatewayを作成する。
  • 各設定
    • サブネット : Publicサブネット
    • 接続タイプ : Public

ルートテーブル

  • こちらも、Public / Private用にそれぞれ作成する。
  • 作成後、サブネットの関連付けで、各サブネットと紐付ける
  • また、ルートを下記のように設定する
Public

Publicのルートテーブルには、全てのトラフィックのターゲットをInternet Gawateyにする。

Private

Privateは全てのトラフィックのターゲットをNAT Gatewayにする。


セキュリティグループ

  • クライアント VPN エンドポイント用のセキュリティグループを作成する。
  • インバウンドルールはなし(空)
  • アウトバウンドルールは全てのトラフィックを許可する。

ACM

  • 今回は相互認証を利用する。
  • クライアント VPN エンドポイントを作成する際に、サーバー証明書が必要なので、先に作成してACMに登録しておく。
証明書の作成

証明書の作成は、公式記載の方法に従った。

docs.aws.amazon.com

  • 作成した証明書をACMのインポートから登録を行う。

  • 上から順に対応するファイルは下記の通り。

    • 証明書本文: server.crt
    • 証明書のプライベートキー: server.key
    • 証明書チェーン; ca.crt
  • 証明書やプライベートキーは BEGINEND の内容を貼り付ける。

クライアント VPN エンドポイント

  • これまで作成したリソースを元にクライアント VPN エンドポイントを作成する。
  • 設定
  • 作成後、「ターゲットネットワークの関連付け」タブからPrivateサブネットを紐付ける
  • また、「セキュリティグループ」で事前に作成しておいたセキュリティグループを設定する
  • さらに、「承認ルール」のタブで、全ての送信先(0.0.0.0/0)を追加する

接続

ここまでの設定で、一度VPNに接続する。

クライアントソフトのダウンロード

  • 以下から、AWS Client VPN for Desktop (AWS VPN Client)をダウンロード&インストールする。

aws.amazon.com

  • また、AWSコンソールのクライアント VPN エンドポイントの画面から、「クライアント設定をダウンロード」のボタンでovpnファイルをダウンロードする。

プロファイルの設定

  • VPN Clientを起動し、プロファイルの追加でダウンロードしたovpnファイルを指定する。
  • ただ、そのままだと下記のエラーが表示される。

  • エラーを解消するためには、ovpnファイルにクライアント証明書と鍵を追加する

  • 変更前

...
-----END CERTIFICATE-----

</ca>


reneg-sec 0
  • 変更後
...
-----END CERTIFICATE-----

</ca>

<cert>
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
xxxx
-----END PRIVATE KEY-----
</key>

reneg-sec 0

証明書と鍵を公式の手順( クライアント認証 - AWS クライアント VPN )で作成していた場合は、<cert />client1.domain.tld.crt<key />client1.domain.tld.keyを設定する形になる。

  • ovpn更新後再度プロファイル設定を行うと、VPNに接続できる。