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 が出るので事前に & に変換する $escaped = str_replace('&', '&', $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.事前に & → & に変換する処理は同じ $escaped = str_replace('&', '&', $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_numericentity と html_entity_decode を使用したが、それぞれどのように変換されるか実験した。
例として、元の HTML が以下の形だったとする。
<p>あ & ♥</p>
<p> タグの中は以下のように変化した。
| 旧 | 新 | |
|---|---|---|
| 初期状態 | あ & ♥ |
あ & ♥ |
| 1. & に変換 | あ & ♥ |
あ & ♥ |
| 2. エンティティに変換 | あ & ♥ |
あ & ♥ |
| 3. DOM操作 | あ & ♥ |
あ & ♥ |
| 4. デコード | あ & ♥ |
あ & ♥ |
&→&の置き換えは DOMDocument に渡すために必要なので、どちらの方式も同じmb_convert_encodingでは「♥」が名前付きエンティティ (♥) に変換されていたが、mb_encode_numericentityでは数値エンティティに置き換わっている点が異なる- DOMDocument にエンティティに変換した文字列を渡すと、名前付きエンティティを持っている数値エンティティは名前付きに変換されるため、新方式ではこのタイミングで
♥に置き換わっている - 旧方式 (
mb_convert_encoding) では名前付き・数値エンティティのどちらも変換されるので、元の文字列に戻る。
新方式は 2. で使ったmb_encode_numericentityの対になる関数はmb_decode_numericentityだが、数値エンティティのみデコードするため、&や♥は残ってしまう。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 でも接続できるみたい。
参考)
- Docker と IPv6 — Docker-docs-ja 1.11.0 ドキュメント
- netstatやlsofでLISTENしているアドレスポートが IPv6と表示されてもIPv4でアクセスできる罠 - Qiita
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を設定していて間違えた。
Service ARNについて
Service ARNの一覧は下記に記載がある。
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
}
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環境を構築した際の記録。
構成

- 固定IPでインターネットに接続するために、プライベートサブネットからパブリックサブネットにあるNAT Gateway経由でアクセスできる経路を作成する。
必要なリソース
- VPC
- サブネット (Public / Private)
- Internet Gateway
- NAT Gateway
- ルートテーブル
- セキュリティグループ (クライアント VPN エンドポイント用)
- ACM
- クライアント VPN エンドポイント
VPCの作成
サブネット
- Public / Privateそれぞれのサブネットを作成する
- 各設定は下記のようにした
Internet Gateway
Nat Gateway
- Privateサブネットからインターネットに接続するために NAT Gatewayを作成する。
- 各設定
- サブネット : Publicサブネット
- 接続タイプ : Public
ルートテーブル
- こちらも、Public / Private用にそれぞれ作成する。
- 作成後、サブネットの関連付けで、各サブネットと紐付ける
- また、ルートを下記のように設定する
Public
Publicのルートテーブルには、全てのトラフィックのターゲットをInternet Gawateyにする。

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

セキュリティグループ
ACM
証明書の作成
証明書の作成は、公式記載の方法に従った。
作成した証明書をACMのインポートから登録を行う。
上から順に対応するファイルは下記の通り。
- 証明書本文:
server.crt - 証明書のプライベートキー:
server.key - 証明書チェーン;
ca.crt
- 証明書本文:
証明書やプライベートキーは
BEGIN〜ENDの内容を貼り付ける。
クライアント VPN エンドポイント
- これまで作成したリソースを元にクライアント VPN エンドポイントを作成する。
- 設定
- 作成後、「ターゲットネットワークの関連付け」タブからPrivateサブネットを紐付ける
- また、「セキュリティグループ」で事前に作成しておいたセキュリティグループを設定する
- さらに、「承認ルール」のタブで、全ての送信先(
0.0.0.0/0)を追加する
接続
ここまでの設定で、一度VPNに接続する。
クライアントソフトのダウンロード
プロファイルの設定
- 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に接続できる。
