zaki work log

zaki work log

作業ログやら生活ログやらなんやら

[Hahicorp Vault] K3sにデプロイするときの公開設定のHelmチャート (Ingress/LoadBalancer)

K3sは標準でIngressもLoadBalancer Serviceもどちらも使える。
VaultデプロイのHelmチャートのパラメタはそれぞれ異なる場所にあるのでメモ。

Ingress

K3sはTraefik Ingress Controllerが標準でインストールされる。 Vaultデプロイ時にIngressを使った外部に公開する最小の設定は以下の通りでserver以下に記述。

yourhost.example.orgに使いたいFQDNを指定する。

server:
  ingress:
    enabled: true
    hosts:
      - host: yourhost.example.org

デプロイするとこんな感じ

$ helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace -f values.yaml 
Release "my-vault" does not exist. Installing it now.
NAME: my-vault
LAST DEPLOYED: Tue Nov 19 09:14:17 2024
NAMESPACE: vault
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Vault!

Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:

https://developer.hashicorp.com/vault/docs


Your release is named my-vault. To learn more about the release, try:

  $ helm status my-vault
  $ helm get manifest my-vault
$
$ kubectl get pod,svc,ing -n vault
NAME                                           READY   STATUS    RESTARTS   AGE
pod/my-vault-0                                 0/1     Running   0          13s
pod/my-vault-agent-injector-5fb64f8dfb-rzd7q   1/1     Running   0          13s

NAME                                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/my-vault                      ClusterIP   10.43.125.114   <none>        8200/TCP,8201/TCP   13s
service/my-vault-agent-injector-svc   ClusterIP   10.43.233.51    <none>        443/TCP             13s
service/my-vault-internal             ClusterIP   None            <none>        8200/TCP,8201/TCP   13s

NAME                                 CLASS     HOSTS                  ADDRESS        PORTS   AGE
ingress.networking.k8s.io/my-vault   traefik   yourhost.example.org   192.168.0.12   80      13s

LoadBalancer Service

K3sは標準でKlipper Load Balancerが含まれているので,type: LoadBalancerの指定で簡単に使用できる。
Vaultデプロイ時にLoadBalancer Serviceを使うための最小の定義は以下の通りでui以下に記述。

externalPortはListenするポート番号で、省略時は8200/TCPになる。

ui:
  enabled: true
  serviceType: "LoadBalancer"
  externalPort: 25080

デプロイするとこの通り。

$ helm upgrade --install my-vault hashicorp/vault -n vault --create-namespace -f value
s.yaml 
Release "my-vault" does not exist. Installing it now.
NAME: my-vault
LAST DEPLOYED: Tue Nov 19 09:34:24 2024
NAMESPACE: vault
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Vault!

Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:

https://developer.hashicorp.com/vault/docs


Your release is named my-vault. To learn more about the release, try:

  $ helm status my-vault
  $ helm get manifest my-vault
$
$ kubectl get pod,svc,ing -n vault
NAME                                           READY   STATUS    RESTARTS   AGE
pod/my-vault-0                                 0/1     Running   0          45s
pod/my-vault-agent-injector-5fb64f8dfb-kzrhv   1/1     Running   0          45s

NAME                                  TYPE           CLUSTER-IP    EXTERNAL-IP    PORT(S)             AGE
service/my-vault                      ClusterIP      10.43.16.34   <none>         8200/TCP,8201/TCP   45s
service/my-vault-agent-injector-svc   ClusterIP      10.43.68.2    <none>         443/TCP             45s
service/my-vault-internal             ClusterIP      None          <none>         8200/TCP,8201/TCP   45s
service/my-vault-ui                   LoadBalancer   10.43.88.2    192.168.0.12   25080:30507/TCP     45s

[html/css] 使用するCSSファイルをJavaScriptを使って動的に切り替える

WebアプリのスタイルをテーマごとにCSSファイルで定義し、設定画面のフォーム操作などで使用するCSSファイルを切り替えてスタイルを更新する、というのをできるか試してみたところDOM操作で普通にできたので、そのやり方について。

通常のCSSファイル指定

通常CSSファイルを指定する場合はこんな感じ。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>sample</title>
        <link href="style1.css" rel="stylesheet" type="text/css" media="all" id="style"/>

        <!-- snip -->

ここで<link href="style.css">の部分をストレートにスクリプトから変更してやればOK

JavaScriptCSSファイルの指定を更新

以下は、IDの値(上記例ではid="style")を使って要素を探し、href属性を使ったCSSファイルの指定をstyle2.cssで上書きするコード。

document.getElementById("style").setAttribute("href", "style2.css");

実装例

styleの値によって使用するCSSファイルを変更している。

const switch_app_style = (style = null) => {
    switch (style) {
        case "njgk":
            document.getElementById("style").setAttribute("href", "style/njgk.css");
            break;
        case "sgbt":
        default:
            document.getElementById("style").setAttribute("href", "style/sgbt.css");
            break;
    }
}

https://github.com/zaki-lknr/swarm-sgbt/blob/0.10.0/swarm.js#L879-L896

swarm.jp-z.jp

このWebアプリの設定画面の「Style」のとこで、チェックボックスを切り替えるとスタイルが更新される。

[JavaScript / Python] 配列内に指定の要素が含まれるかどうかをチェック (備忘録)

よく使うけとすぐ忘れるので。

JavaScript

includesを使う。

developer.mozilla.org

基本構文

array.include(item);

例:httpステータスチェック

httpレスポンスコードが200/201/202/203だったら処理をする、の場合

status = get_http_status();
if ([200, 201, 202, 203].includes(status)) {
    ...
}

Python

inを使う。

docs.python.org

基本構文

item in list

例:httpステータスチェック

JavaScriptの例と同様、httpレスポンスコードが200/201/202/203だったら処理をする、の場合

status = get_http_status()
if status in (200, 201, 202, 203):
    ...

※ リストでなくタプルでもOK

辞書の場合

JavaScriptは配列限定(だよね?)だが、Pythonだと辞書などのシーケンス型全てに使える(とドキュメントに書かれている)
辞書の場合は、キーがあるかテストできる。

key = 'host'
data = {
    'host': '192.168.0.10',
    'user': 'zaki',
    'home': '/home/zaki'
}

if key in data:
    # dataにはキー`host`を含むのでTrueとなる

集合(追記)

素数が多く重複もしないのであれば(というか要素チェック時に重複の有無は無視できるはず)、集合に変換すればパフォーマンスが改善される。
nikkieさんコメントありがとうございます!

前述コードをsetを使って集合に変換してinを使う。

status = get_http_status()
data_set = set((200, 201, 202, 203))
if status in data_set:
    ...

[Terraform / AWS] externalデータソースを使った外部コマンド実行でセキュリティグループに自分のIPアドレスをセットする

Terraformで環境を作成する際のアクセス元IPを設定したい場合に、AWSのwebポータルだと「マイIP」でアクセス元グローバルIPを簡単にセットできるけどTerraformなどは機能として提供されてなさそうだっため、事前にアドレスを取得してそれをセットするという手順を実現できないか調べてみた。
IPアドレスを知るにはcurlでそういう値を返すサーバーに問い合わせるのが手軽なので、そのコマンド結果をTerraformコード内で使う方法についてお試し。

external_external | Data Sources | hashicorp/external | Terraform | Terraform Registry

externalを使ったIPアドレスの取得とエラー

コマンドを実行してIPアドレスを得るにはいくつかあると思うが、慣れているcurl -sS https://ifconfig.ioを実行。

# ダメな書き方
data "external" "src_ip" {
  program = program = ["curl", "-sS", "https://ifconfig.io/"]
}

ただし↑のように書いてもエラーになる。

│ Error: Unexpected External Program Results
│ 
│   with module.dev_online.data.external.src_ip,
│   on modules/online/main.tf line 55, in data "external" "src_ip":
│   55:   program = ["curl", "-sS", "https://ifconfig.io/"]
│ 
│ The data source received unexpected results after executing the program.
│ 
│ Program output must be a JSON encoded map of string keys and string values.
│ 
│ If the error is unclear, the output can be viewed by enabling Terraform's logging at TRACE level. Terraform documentation
│ on logging: https://www.terraform.io/internals/debugging
│ 
│ Program: /usr/bin/curl
│ Result Error: invalid character '.' after top-level value

エラーやドキュメントをよく見るとコマンドの出力はJSON形式である必要がある。(後述するけどstringであるとも書いてある)

Program output must be a JSON encoded map of string keys and string values.

externalのドキュメントの「External Program Protocol」の項は以下。

The program must read all of the data passed to it on stdin, and parse it as a JSON object.

https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external#external-program-protocol

ということで、JSON形式のレスポンスを返すようにオプションのパスを追加してみると

│ Error: Unexpected External Program Results
│ 
│   with module.dev_online.data.external.src_ip,
│   on modules/online/main.tf line 56, in data "external" "src_ip":
│   56:   program = ["curl", "-sS", "https://ifconfig.io/all.json"]
│ 
│ The data source received unexpected results after executing the program.
│ 
│ Program output must be a JSON encoded map of string keys and string values.
│ 
│ If the error is unclear, the output can be viewed by enabling Terraform's logging at TRACE level. Terraform documentation
│ on logging: https://www.terraform.io/internals/debugging
│ 
│ Program: /usr/bin/bash
│ Result Error: json: cannot unmarshal number into Go value of type string

またエラー。
エラーを見るとnumberをstringにできないとある。
いろいろ試行錯誤の末、以下の出力の通りportがnumberになっているのがエラーの原因で、externalでは「すべて文字列であること」が条件。

$ curl -sS https://ifconfig.io/all.json | python3 -m json.tool 
{
    "country_code": "JP",
    "encoding": "gzip, br",
    "forwarded": "*.*.*.*",
    "host": "*.*.*.*",
    "ifconfig_cmd_hostname": "ifconfig.io",
    "ifconfig_hostname": "ifconfig.io",
    "ip": "*.*.*.*",
    "lang": "",
    "method": "GET",
    "mime": "*/*",
    "port": 23814,
    "referer": "",
    "ua": "curl/8.5.0"
}

まぁこの文字列であるという条件のことも、エラーとドキュメントにも書いてあるんだけどね。

The JSON object contains the contents of the query argument and its values will always be strings.

https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external#external-program-protocol

数値型を除外してエラー回避

試しに以下の内容でportの出力を除外して実行すると期待通りに動作する。

data "external" "src_ip" {
  program = ["bash", "-c", "curl https://ifconfig.io/all.json | python3 -m json.tool | grep -v port"]
}

さすがにこれだと不格好すぎるなので、もう少しスマートに書き直す。
ここからはもはやTerraform関係なく単にLinuxコマンドの文字列制御だけど。

エスケープがどうしても多くなるけど基本コマンドで書くなら、/ipでなく/ip.jsonにするとクォートを付けてくれるのでそれを使って

program = ["bash", "-c", "echo {\\\"ip\\\":$(curl ifconfig.io/ip.json)}"]

もしくは、jqが使えるならこんな感じで

program = ["bash", "-c", "curl -sS https://ifconfig.io/all.json | jq '{ip}'"]

あるいはjqでエラーになるnumber型であるportを除外

program = ["bash", "-c", "curl -sS https://ifconfig.io/all.json | jq 'del(.port)'"]

あまりスマートじゃないけど、-nで入力なしの実行からIPアドレスJSON形式の出力を作成するのでも動く。

program = ["bash", "-c", "jq -n \".ip = $(curl -s https://ifconfig.io/ip.json)\""]

セキュリティグループ設定

データソースで取得した結果は、.resultで参照できる。
前述のようにキーipであれば、.result.ipになる。

セキュリティグループのIPアドレスに使用するならこんな感じ。

resource "aws_security_group" "sg" {
  name   = ...
  vpc_id = ...

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["${data.external.src_ip.result.ip}/32"]
  }

  ...
}

補足

使用にあたってはexternalプロバイダーが必要なので、初回であればterraform initでプロバイダー更新を行う。

╷
│ Error: Inconsistent dependency lock file
│ 
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│   - provider registry.terraform.io/hashicorp/external: required by this configuration but no version is selected
│ 
│ To update the locked dependency selections to match a changed configuration, run:
│   terraform init -upgrade
╵

ifconfig.ioのレスポンスを得るだけなら

あとから検索したら実はhttpデータソースがあるようなので、これなら「HTTPアクセスして結果を得る」ところまでをTerraformにさせることができるみたい。

まとめ

  • コマンドの実行結果をデータソースにするにはexternalを使うよ
  • コマンドの実行結果はJSON形式になってる必要があるよ
  • JSON内のデータの型はすべてstringである必要があるよ
  • curlを使ったHTTPアクセスの結果をどうにかしたい場合はhttpというデータソースもあるよ
  • エラーとドキュメントよく読もうね

[ブラウザJavaScript] クリップボード操作 (テキストのコピー/ペースト)

developer.mozilla.org

基本的にサンプルコード通りで動作した。

クリップボードへのテキストコピー

テキストをクリップボードへコピーするのは簡単で、以下のコードで実現できる。

navigator.clipboard.writeText("コピーしたいテキスト")

フォームに入力されているテキストをコピーするには以下。

const input_text = document.getElementById("sample_form");
navigator.clipboard.writeText(input_text.value);

HTTPの場合

上記のコードのnavigator.clipboardは、開いてるhtmlがHTTPの場合はundefinedとなり使用できない。
HTTPの場合は現在deprecatedになっているdocument.execCommand('copy')を使用する。

前述のフォームテキストのコピーをHTTP版に書き直すと以下。

const input_text = document.getElementById("sample_form");
input_text.select();
document.execCommand('copy');

ちなみにHTTPSだけでなく、file://の場合もnavigator.clipboard.writeTextは使用可能。

動的に動作を変えるには

navigator.clipboardundefinedかどうかをハンドリングすればOK

const input_text = document.getElementById("sample_form");
if (navigator.clipboard) {
    navigator.clipboard.writeText(input_text.value);
}
else {
    input_text.select();
    document.execCommand('copy');
}

ペーストの場合

クリップボードのテキストをJavaScriptで参照するにはnavigator.clipboard.readText()を使用する。
ただし非同期で動作するので、then()でメソッドチェーンで内容を取得する。

navigator.clipboard.readText().then((clipText) => {
    console.log(clipText);
    input_text.value = clipText;
})

なお、HTTPの場合はやっぱりnavigator.clipboardが使えないためdocument.execCommand("paste")で代用することになると思うが、手元の環境では以下のコードでは動作しなかった。

// HTTPの場合・ただし動作せず
input_text.focus();
document.execCommand("paste");

フォーカスからペーストまでの時間が速すぎるのかとインターバルを入れてみても動作は変わらず。。

// インターバルを入れてみても動作せず
setTimeout(() => {
    document.execCommand("paste");
}, 20);

動作環境

Chrome 129.0.6668.90 (for Windows x64)

[Terraform / AWS] Data Sourcesでデフォルト(メイン)のルートテーブルを参照する

VPCの作成を行うコードからそのままデフォルトのルートテーブルを使いたい場合は何も問題ないけど、VPCは作成済みで、そのデフォルトのルートテーブルを使いたい場合にプチはまりしたのでその備忘録。

Resourcesで実装する元のコード

TerraoformコードでVPCを作成し、それを使ってルートテーブルを参照するには以下でOK

resource "aws_vpc" "example_dev_vpc" {
  cidr_block           = "10.1.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "example-vpc"
  }
}

resource "aws_vpc_endpoint" "example_dev_endpoint_to_s3" {
  vpc_id = aws_vpc.example_dev_vpc.id
  service_name = "com.amazonaws.${var.region_name}.s3"
  vpc_endpoint_type = "Gateway"

  # これはオフラインのサブネットはデフォルトのルートテーブルを使用してる場合
  route_table_ids = [aws_vpc.example_dev_vpc.default_route_table_id]

  tags = {
    Name = "example-endpoint-s3"
  }
}

元コードはこちらで、今回モジュール分割したくてData Sources経由で実装しようとしたらVPCのData Sourcesが使えなかった、という話。

zaki-hmkc.hatenablog.com

Data Sourcesの場合

データソースを使って「別で作成済みのVPCのデフォルトのルートテーブルを参照したい」場合、データソースのaws_vpcにはdefault_route_table_id属性が無いため、同じ要領では実装できない。

※ ダメな実装

data "aws_vpc" "example_dev_vpc" {
  filter {
    name = "..."
    values = [...]
  }
}

resource "aws_vpc_endpoint" "example_dev_endpoint_to_s3" {
  vpc_id            = data.aws_vpc.example_dev_vpc.id
  service_name      = "com.amazonaws.${var.region_name}.s3"
  vpc_endpoint_type = "Gateway"

  # オフラインのサブネットはデフォルトのルートテーブルを使用
  route_table_ids = [data.aws_vpc.example_dev_vpc.default_route_table_id]  ### NG

  tags = {
    Name = "example-endpoint-s3"
  }
}

デフォルトのルートテーブルが欲しい場合はvpcからたどるのでなく、aws_route_tableのデータソースを使って対象のルートテーブルを探す。

data "aws_vpc" "example_dev_vpc" {
  filter {
    name = "..."
    values = [...]
  }
}

# ルートテーブルのデータソースで対象を定義
data "aws_route_table" "default_route_table" {
  vpc_id = data.aws_vpc.example_dev_vpc.id
  filter {
    name = "association.main"
    values = ["true"]
  }
}

resource "aws_vpc_endpoint" "3s_vpc_endpt" {
  vpc_id            = data.aws_vpc.example_dev_vpc.id
  service_name      = "com.amazonaws.${var.region_name}.s3"
  vpc_endpoint_type = "Gateway"

  # オフラインのサブネットはデフォルトのルートテーブルを使用
  route_table_ids = [data.aws_route_table.default_route_table.id]

  tags = {
    Name = "example-endpoint-s3"
  }
}

解決策は以下で見つけた。

github.com

association.main

このassociation.mainが何かというと、メインのルートテーブルを表すキーワード。

docs.aws.amazon.com

dev.classmethod.jp

association.main - Indicates whether the route table is the main route table for the VPC (true | false ). Route tables that do not have an association ID are not returned in the response.

https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-route-tables.html#options

環境

$ terraform --version
Terraform v1.9.2
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.43.0

Your version of Terraform is out of date! The latest version
is 1.9.7. You can update by downloading from https://www.terraform.io/downloads.html

もしかすると「デフォルトのルートテーブル」と「メインのルートテーブル」は実は別でごちゃまぜにしてるかもしれないけど、とりあえず期待する動作はしたよというメモです。

リファレンス

[Ansible] password_hashフィルター使用時はpasslibパッケージを入れておく

Ansibleクックブックの「2-3-3 パスワードをハッシュ化する」にも載っているpassword_hashフィルターで発生したエラーについて簡単にまとめ。

docs.ansible.com

Ansible10 (ansible-core 2.17)の場合

password_hashフィルターを使っているタスクを、Ansible10 (ansible-core 2.17)で実行すると発生したエラーが以下。

fatal: [hostname]: FAILED! => 
  msg: Unable to encrypt nor hash, passlib must be installed. No module named 'passlib'. Unable to encrypt nor hash, passlib must be installed. No module named 'passlib'

メッセージに従ってpasslibを追加すれば解決。

$ pip install passlib
Collecting passlib
  Using cached passlib-1.7.4-py2.py3-none-any.whl.metadata (1.7 kB)
Using cached passlib-1.7.4-py2.py3-none-any.whl (525 kB)
Installing collected packages: passlib
Successfully installed passlib-1.7.4

Ansible9 (ansible-core 2.16)の場合

passlibが無い場合、1つ前のバージョンだと以下の警告が出力され、2.17で使えなくなる旨が確認できていて、これもpasslibをインストールしていれば解決する。

[DEPRECATION WARNING]: Encryption using the Python crypt module is deprecated. The Python crypt module is 
deprecated and will be removed from Python 3.13. Install the passlib library for continued encryption 
functionality. This feature will be removed in version 2.17. Deprecation warnings can be disabled by setting 
deprecation_warnings=False in ansible.cfg.

どういうこと?

password_hashフィルターのページには記載はないが、フィルタープラグインそのもののページには以下の記載がある。

Hash types available depend on the control system running Ansible, ansible.builtin.hash depends on hashlib, ansible.builtin.password_hash depends on passlib. The crypt is used as a fallback if passlib is not installed.

https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_filters.html#hashing-and-encrypting-strings-and-passwords

つまり、元々はpasslibが無ければcryptが使われる動作だったが、Pythonのcryptはdeprecatedになっている(3.13で無くなる)ためansible-core 2.17ではcryptを使わずpasslib必須になったよ、ということ。

環境

$ python --version
Python 3.12.3
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=24.04
DISTRIB_CODENAME=noble
DISTRIB_DESCRIPTION="Ubuntu 24.04 LTS"

Python 3.13でも試そうかと思ったけど、まだリリース前だった