プログラマ行進曲第二章

主にソフトウェア関連の技術をネタにした記事を執筆するためのブログ

文字を書くリハビリをする

前回の記事から158日空いてしまっている。

やはり年初の思いを抱きながらちゃんと技術記事を書くのは2月で終わってしまうのだなあと思いながら適当にこの文章を書いています。

仕事では多少文章を書くものの、その他ではSNS含めてめっきり減ってしまい、そもそも文字を書く練習をしないとダメなんじゃないかと思って、文字を書くという行為を思い出すためだけにこの文章を書いています。

本当は「terraform plan の -generate-config-out オプションがとても使える」とかそういう、ちゃんとした技術記事を定期的にアウトプットできているといいんでしょうけどね。

取りあえず最近は相変わらずTerraformとAnsibleとYAMLと戯れている日々です。仕事的には。

何とか生きています。

1行に収めるには長いJSONをCLIの引数に与える場合に見やすくする書き方の例

世の中には設定をJSON形式で与えるソフトウェアがたくさんあります。

GUIで適当に設定できる、あるいはしてもいい場合は適当にやればいいのですが、変更する対象のリソースが大量にある場合、CLIを介して複数同時にオペレーションした方がいい場合もあるでしょう。

その際、与える設定値であるJSONの形式がそこまで複雑ではない場合、CLIの引数に与えるJSONを1行に収めてそのまま与えるでしょう。

たとえばawscliのsecretsmanager tag-resourceコマンドのドキュメントにはこんな例が書いてあります。

aws secretsmanager tag-resource \
    --secret-id MyTestSecret \
    --tags '[{"Key": "FirstTag", "Value": "FirstValue"}, {"Key": "SecondTag", "Value": "SecondValue"}]'

この場合、引数として与えられているJSONはたかだか要素数が2のリストなので、まだ素直に引数としてそのまま与えればいいと思います。

しかし、これが以下の様になったらもう見づらくてしょうがないと思います。

aws secretsmanager tag-resource \
    --secret-id MyTestSecret \
    --tags '[{"Key":"id","Value":"$id"},{"Key":"environment","Value":"$environment"},{"Key":"role","Value":"$role"},{"Key":"owner","Value":"$owner"},{"Key":"description","Value":"description"}]'

正直私の目ではもうこの時点で辛いです。

しかも書くのが一回だけでなく、手順書やらドキュメントに残す場合、明らかに読みづらいので何かしら手を打ちたいところです。

何かうまい方法はないものかとChatGPTに聞いてみたら、中々いいなと思った返答が返ってきました。

具体的にはヒアドキュメントを活用して改行とインデントを含むJSON文字列を変数に格納し、それをCLI実行時にブレース展開する形で与えてやればいいという感じです。

具体的には以下の通りです。

tags=$(cat <<EOF
[
  {"Key": "id", "Value": "$id"},
  {"Key": "environment", "Value": "$environment"},
  {"Key": "role", "Value": "$role"},
  {"Key": "owner", "Value": "$owner"},
  {"Key": "description", "Value": "description"}
]
EOF
)

aws secretsmanager tag-resource \
    --secret-id MyTestSecret \
    --tags "$tags"

シェルスクリプトとかbashマスターな人には別に当たり前の使い方なのかもしれませんが、私としてはこういう書き方を学んだことも思いついたこともなかったので非常に勉強になりました。

(2024/02/19 追記)

以前に似たような問題で記事にしていたことを書き上げた後に思い出しました。

takuan-osho.hatenablog.com

この時はただ1行に収めるだけのやり方でしたが、今回のやり方の方が応用できるケースが多そうでいいのかな、とは思います。

Amazon VPC CNIプラグインの設定をTerraformで行う時にENIConfigの設定で詰まった

前回の記事でAmazon EKSアドオンの設定の際のスキーマについて書きました。

takuan-osho.hatenablog.com

今回は具体的にAmazon VPC CNIプラグインの設定をする際、ENIConfigの設定で初見では中々気づきづらいエラーが出て解消するのに難儀したという話をしようと思います。

今回も前回と同じく、以下の記事の内容を理解しておくとすんなり分かるタイプの話が多くなるでしょう。

aws.amazon.com

Amazon VPC CNIプラグインでカスタムネットワークの設定をする

Amazon EKSを使っている際、たとえば「普段使っているVPC内のprivate subnetが確保しているIPv4アドレスよりも多い数が確保できるsubnetとIPv4アドレスを使用して、pod用にIPアドレスを割り振りたい」など諸々要件があってカスタムネットワークの設定が必要になったとします。

そういうときにはAmazon VPC CNIプラグインに設定を追加して対応してやる必要があります。詳しくは以下AWS公式ドキュメントを参照。

docs.aws.amazon.com

機能の概要

Amazon EKS アドオン API に新しい configurationValues パラメータが追加されました。ローンチ時点では、このパラメータは、JSON BLOB として設定を受け入れます。YAML のサポートも近日中に開始される予定です。JSON BLOB は、作成または更新しようとしているアドオンの特定バージョンごとの JSON Schema に準拠する必要があります。スキーマは、新しい API である Amazon EKS DescribeAddonConfiguration API を通じて利用可能です。以下で説明するように、 configurationValues パラメータは aws eks create-addon および aws eks update-addon AWS CLI コマンドで使用できます。

ここで書かれているように、 configurationValues というパラメータに設定を書いてやります。

Terraformでここに設定を追加したい場合、ドキュメントのこの箇所のように、 aws_eks_addon リソースの configuration_values argument にjsonencode関数でエンコードしたJSON形式のObjectを入れてやればいいです。

ただ、ここでENIConfigの設定を入れようとすると結構はまりポイントがありました。

ENIConfigカスタムリソースのスキーマのはまりポイント

前回の記事にも掲載した、Amazon VPC CNIプラグインの設定のスキーマから、ENIConfigに関わる部分だけ取り出してみます。

{
  "$ref": "#/definitions/VpcCni",
  "$schema": "http://json-schema.org/draft-06/schema#",
  "definitions": {
    "Affinity": {
      "type": [
        "object",
        "null"
      ]
    },
    "EniConfig": {
      "additionalProperties": false,
      "properties": {
        "create": {
          "type": "boolean"
        },
        "region": {
          "type": "string"
        },
        "subnets": {
          "additionalProperties": {
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string"
              },
              "securityGroups": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "id"
            ],
            "type": "object"
          },
          "minProperties": 1,
          "type": "object"
        }
      },
      "required": [
        "create",
        "region",
        "subnets"
      ],
      "type": "object"
    },
# 省略
  },
  "description": "vpc-cni"
}

これはawscliで返ってきた値をjqでパースしたjsonなので要素の並び方がjq由来なのも手伝っているのでしょうが、このスキーマをパッと見てどんな感じか分かるでしょうか?

特に $.eniConfig.subnets の箇所の定義に自信を持って設定できるでしょうか?

私はJSONスキーマに慣れていないこともあり、この定義をちゃんと見ていたはずなのですが、結果的に読み切れて無くてTerraformの設定時によく分からないエラーに遭遇して時間を溶かしてしまいました。

具体的にはterraform plan時に出た、以下の様なエラーです。

module.eks_addon.aws_eks_addon.vpc_cni: Creating...
â•·
│ Error: creating EKS Add-On (dev-cluster:vpc-cni): InvalidParameterException: ConfigurationValue provided in request is not supported: Json schema validation failed with error: [$.eniConfig.subnets: array found, object expected]
│ {
│   RespMetadata: {
│     StatusCode: 400,
│     RequestID: "my-request-id"
│   },
│   AddonName: "vpc-cni",
│   ClusterName: "dev-cluster",
│   Message_: "ConfigurationValue provided in request is not supported: Json schema validation failed with error: [$.eniConfig.subnets: array found, object expected]"
│ }
│
│   with module.eks_addon.aws_eks_addon.vpc_cni,
│   on ../modules/eks_addon.tf line 4, in resource "aws_eks_addon" "vpc_cni":
│    4: resource "aws_eks_addon" "vpc_cni" {
│
╵

$.eniConfig.subnets: array found, object expected というエラーが表示され、「え?ここはarrayじゃないの?」となってしまいました。

定義のほうをちゃんと見ると $.eniConfig.subnets はobjectが型として指定されているのは分かるのですが、 "type": "object" までの記述が離れていて大分見分けが付きづらい状態です。

「ではどういう風に書けばいいのか?」という問いに対する答えを書いてしまうと、eniConfigの箇所には以下の様にavailability zoneの値をkeyにし、subnetの情報をそこに紐付ければいいです。

  eniConfig = {
    create  = true
    region  = "ap-northeast-1"
    subnets = {
      "ap-northeast-1a" = {
          id = "subnet-aaaaa",
          securityGroups = ["sg-yyyyy"]
    },
  }

aws_eks_addon リソースの configuration_values 引数にjsonencode関数で適切に変換しつつ上記のeniConfig要素を入れると、以下の様なCustom Resourceを作るのと同等になります。

  apiVersion: crd.k8s.amazonaws.com/v1alpha1
  kind: ENIConfig
  metadata:
    name: ap-northeast-1a
  spec:
    securityGroups:
      - "sg-yyyyy"
    subnet: "subnet-aaaaa"

$.eniConfig.subnets に設定したobjectのkeyにあたる ap-northeast-1a がENIConfig Custom Resourceの .metadata.name に設定されています。

ここがAmazon VPC CNIプラグインの設定をTerraformで行う時に一番分かりづらかったところです。

この形に適合するように値を変更したら、 Json schema validation failed with error というエラーは消え、実際にterraform applyすると必要な設定・リソースが入ることを確認できました。

お金がなくなってきたので一旦はてなブログProの更新をやめてみた

タイトルの通り。

機能に不満があるかとかそういうことよりかは、単純に最近お金を遊び用のものに使いすぎて金欠気味なこと&いうほど広告非表示とかの機能に依存して無さそうと感じたのが原因。

脳味噌使わない状態で更新しててもお金を溶かしているだけだしな、と自分を納得させて、取りあえず一旦更新をやめてみました。

一応Googleアナリティクス4を拙いながらも入れていてアクセス解析していますが、明らかにアクセス数とかに影響出てブログ更新の意欲が削がれるとか悪影響が出ているなと感じたら再度契約しようかなと思います。

...うーん、でもやっぱり無料プランで広告が挿入される位置とか久し振りに見るとだいぶウザいなって感じたので、悩みどころですね。

もっとはてなブログProにしたことで得られるメリットが2年間14400円に見合うものだったらいいんだけどなあ、とか何となく感じました。

Amazon EKSアドオンの設定のスキーマをAWSのAPIから知る方法

Amazon EKSを使う際、お世話になるのがEKSアドオンという仕組みです。今回はこのEKSアドオンの設定をする際のパラメータをどのように書けばいいのかということについて記録します。

仕事を進めている際、このEKS アドオンの設定を書いているときに色々詰まったので備忘録代わりのメモです。

なお、この記事で書いていることはAWS公式の以下の記事を読み通せば分かる内容なので、深い内容が知りたい人はこの記事を丹念に読むといいと思います。

aws.amazon.com

DescribeAddonConfiguration APIを呼び出すとよい

さっそく答えですが、Amazon EKSに用意されている新しめのAPI、DescribeAddonConfiguration APIを呼び出せばconfigurationValuesパラメータに入れるべき設定のスキーマをJSONスキーマの形で知ることができます。

docs.aws.amazon.com

もちろん、APIだけでなく、AWSCLIにも同APIを呼び出せるeks describe-addon-configurationコマンドが用意されています。各自で使いやすい方を使うといいでしょう。

awscli.amazonaws.com

どちらも、アドオンの名前とバージョンを指定して呼び出す必要があります。

例えば、記事執筆時点で最新バージョンであるAmazon VPC CNIプラグインのバージョンv1.16.2-eksbuild.1の設定のスキーマを知りたいときはこんな感じでAWSCLIを実行し、jqでパースすれば、望みの設定のスキーマを得られます。

aws eks describe-addon-configuration \
  --addon-name vpc-cni \
  --addon-version v1.16.2-eksbuild.1 | \
  jq -r '.configurationSchema' | \
  jq -r '.'

返ってくる値は以下の様な感じです。

{
  "$ref": "#/definitions/VpcCni",
  "$schema": "http://json-schema.org/draft-06/schema#",
  "definitions": {
    "Affinity": {
      "type": [
        "object",
        "null"
      ]
    },
    "EniConfig": {
      "additionalProperties": false,
      "properties": {
        "create": {
          "type": "boolean"
        },
        "region": {
          "type": "string"
        },
        "subnets": {
          "additionalProperties": {
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string"
              },
              "securityGroups": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "id"
            ],
            "type": "object"
          },
          "minProperties": 1,
          "type": "object"
        }
      },
      "required": [
        "create",
        "region",
        "subnets"
      ],
      "type": "object"
    },
    "Env": {
      "additionalProperties": false,
      "properties": {
        "ADDITIONAL_ENI_TAGS": {
          "type": "string"
        },
        "ANNOTATE_POD_IP": {
          "format": "boolean",
          "type": "string"
        },
        "AWS_EC2_ENDPOINT": {
          "type": "string"
        },
        "AWS_EXTERNAL_SERVICE_CIDRS": {
          "type": "string"
        },
        "AWS_MANAGE_ENIS_NON_SCHEDULABLE": {
          "format": "boolean",
          "type": "string"
        },
        "AWS_VPC_CNI_NODE_PORT_SUPPORT": {
          "format": "boolean",
          "type": "string"
        },
        "AWS_VPC_ENI_MTU": {
          "format": "integer",
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG": {
          "format": "boolean",
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS": {
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_EXTERNALSNAT": {
          "format": "boolean",
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_LOGLEVEL": {
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_LOG_FILE": {
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_RANDOMIZESNAT": {
          "type": "string"
        },
        "AWS_VPC_K8S_CNI_VETHPREFIX": {
          "type": "string"
        },
        "AWS_VPC_K8S_PLUGIN_LOG_FILE": {
          "type": "string"
        },
        "AWS_VPC_K8S_PLUGIN_LOG_LEVEL": {
          "type": "string"
        },
        "CLUSTER_ENDPOINT": {
          "type": "string"
        },
        "DISABLE_INTROSPECTION": {
          "format": "boolean",
          "type": "string"
        },
        "DISABLE_LEAKED_ENI_CLEANUP": {
          "format": "boolean",
          "type": "string"
        },
        "DISABLE_METRICS": {
          "format": "boolean",
          "type": "string"
        },
        "DISABLE_NETWORK_RESOURCE_PROVISIONING": {
          "format": "boolean",
          "type": "string"
        },
        "DISABLE_POD_V6": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_BANDWIDTH_PLUGIN": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_POD_ENI": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_PREFIX_DELEGATION": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_V4_EGRESS": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_V6_EGRESS": {
          "format": "boolean",
          "type": "string"
        },
        "ENI_CONFIG_ANNOTATION_DEF": {
          "type": "string"
        },
        "ENI_CONFIG_LABEL_DEF": {
          "type": "string"
        },
        "INTROSPECTION_BIND_ADDRESS": {
          "type": "string"
        },
        "IP_COOLDOWN_PERIOD": {
          "format": "integer",
          "type": "string"
        },
        "MAX_ENI": {
          "format": "integer",
          "type": "string"
        },
        "MINIMUM_IP_TARGET": {
          "format": "integer",
          "type": "string"
        },
        "POD_SECURITY_GROUP_ENFORCING_MODE": {
          "type": "string"
        },
        "WARM_ENI_TARGET": {
          "format": "integer",
          "type": "string"
        },
        "WARM_IP_TARGET": {
          "format": "integer",
          "type": "string"
        },
        "WARM_PREFIX_TARGET": {
          "format": "integer",
          "type": "string"
        }
      },
      "title": "Env",
      "type": "object"
    },
    "Init": {
      "additionalProperties": false,
      "properties": {
        "env": {
          "$ref": "#/definitions/InitEnv"
        },
        "resources": {
          "$ref": "#/definitions/Resources"
        }
      },
      "title": "Init",
      "type": "object"
    },
    "InitEnv": {
      "additionalProperties": false,
      "properties": {
        "DISABLE_TCP_EARLY_DEMUX": {
          "format": "boolean",
          "type": "string"
        },
        "ENABLE_V6_EGRESS": {
          "format": "boolean",
          "type": "string"
        }
      },
      "title": "InitEnv",
      "type": "object"
    },
    "Limits": {
      "additionalProperties": false,
      "properties": {
        "cpu": {
          "type": "string"
        },
        "memory": {
          "type": "string"
        }
      },
      "title": "Limits",
      "type": "object"
    },
    "NodeAgent": {
      "additionalProperties": false,
      "properties": {
        "conntrackCacheCleanupPeriod": {
          "format": "integer",
          "type": "integer"
        },
        "enableCloudWatchLogs": {
          "format": "boolean",
          "type": "string"
        },
        "enablePolicyEventLogs": {
          "format": "boolean",
          "type": "string"
        },
        "enabled": {
          "type": "boolean"
        },
        "healthProbeBindAddr": {
          "format": "integer",
          "type": "string"
        },
        "metricsBindAddr": {
          "format": "integer",
          "type": "string"
        },
        "resources": {
          "$ref": "#/definitions/Resources"
        }
      },
      "title": "NodeAgent",
      "type": "object"
    },
    "Resources": {
      "additionalProperties": false,
      "properties": {
        "limits": {
          "$ref": "#/definitions/Limits"
        },
        "requests": {
          "$ref": "#/definitions/Limits"
        }
      },
      "title": "Resources",
      "type": "object"
    },
    "Tolerations": {
      "additionalProperties": false,
      "items": {
        "type": "object"
      },
      "type": "array"
    },
    "VpcCni": {
      "additionalProperties": false,
      "properties": {
        "affinity": {
          "$ref": "#/definitions/Affinity"
        },
        "branchENICooldown": {
          "type": "integer"
        },
        "enableNetworkPolicy": {
          "format": "boolean",
          "type": "string"
        },
        "enableWindowsIpam": {
          "format": "boolean",
          "type": "string"
        },
        "enableWindowsPrefixDelegation": {
          "format": "boolean",
          "type": "string"
        },
        "eniConfig": {
          "$ref": "#/definitions/EniConfig"
        },
        "env": {
          "$ref": "#/definitions/Env"
        },
        "init": {
          "$ref": "#/definitions/Init"
        },
        "livenessProbeTimeoutSeconds": {
          "type": "integer"
        },
        "minimumWindowsIPTarget": {
          "type": "integer"
        },
        "nodeAgent": {
          "$ref": "#/definitions/NodeAgent"
        },
        "podAnnotations": {
          "additionalProperties": {
            "not": {
              "type": [
                "object",
                "array",
                "null"
              ]
            }
          },
          "type": [
            "object",
            "null"
          ]
        },
        "podLabels": {
          "additionalProperties": {
            "not": {
              "type": [
                "object",
                "array",
                "null"
              ]
            }
          },
          "type": [
            "object",
            "null"
          ]
        },
        "readinessProbeTimeoutSeconds": {
          "type": "integer"
        },
        "resources": {
          "$ref": "#/definitions/Resources"
        },
        "tolerations": {
          "$ref": "#/definitions/Tolerations"
        },
        "warmWindowsIPTarget": {
          "type": "integer"
        },
        "warmWindowsPrefixTarget": {
          "type": "integer"
        }
      },
      "title": "VpcCni",
      "type": "object"
    }
  },
  "description": "vpc-cni"
}

JSONスキーマに慣れてない私からすると大分読みにくいのですが、これでconfigurationValuesに書くべき値がどんなものかを調べながら書けるようになりました。

このENIConfig絡みの設定でまた色々と詰まったのですが、それはまた別記事にして書こうと思います。

元々技術書は電子書籍派だったけど最近は紙の本も買っている

はてなブログの購読リストを眺めていたら、id:magnoliakさんのとある記事が目にとまりました。

blog.magnolia.tech

ここで私の目にとまったのは以下の箇所。

  • 一度に読む本を限定する

机の上に無印良品の仕切りスタンドを置いてあって、ここに収まる本以外は同時に読まないようにしている 読み終わったら、本棚の本と入れ替え

これは自分にはなかった発想でなるほどなあと思いました。

似たこととして、机の上に平積みしていた紙の本を立てるようにして一冊ずつ読みやすくするためにブックエンドを購入して整理した、ということをしてはいましたが、magnoliakさんほど意図的にはやってなかったですね。

その時に購入したブックエンドです。コクヨさんのこのブックエンド、結構重みがありしっかりしているので、本が倒れず重宝しました。

せっかくなので、自分の現時点の状態の記録保存のため、最近は技術書を買う際に電子書籍ではなくて紙の本を選ぶことが多くなっているということについて書こうと思います。

単純に目が疲れやすくなったので紙の本を選ぶ

見出しの通りですが、年を取ったせいなのか、はたまた元々近視で目が悪いからなのか、最近は昔と比べて特に目が疲れやすくなり、iPadやモニターで電子書籍を見ているとすぐに肩まで含めて目が疲れてしまい、疲弊することが多くなりました。

個人的な感覚ではありますが、液晶モニターよりも紙の本の方が目への負担が小さく、読んでいて疲れにくいと感じるので、サッと読み通しやすそうな技術書や分量が多い技術書は敢えて紙の本を選んだりしています。

たとえば『詳解システム・パフォーマンス第2版』だったり『GitLabに学ぶ世界最先端のリモート組織のつくりかた』などがそうです。

全部が全部紙の本に切り替えているわけではなく、目への負担と持ち運びするかどうかなど、実際に読み通す負担が小さくなりそうな点を考慮して電子書籍で買うか、紙の本で買うかを決めています。

なので、お金がかかりはしますが、同じ書籍を紙と電子両方で買うこともたまにではありますが、出てきました。

ラムダノートさんみたく、紙と電子書籍がセットでお得に買えるところならそれを選ぶんですけどね。中々現実は難しい。

第1回 AWSコスト削減 天下一武道会にオンラインで参加しました

タイトルの通りで、実際に開催される前から大分話題になっていた第1回 AWSコスト削減 天下一武道会にオンラインで参加しました。

no1.connpass.com

当日オンラインで放送されたもののアーカイブが撮って出しで出されているので、興味のある方はこちらからオンデマンドで見直すことができます。

www.youtube.com

仕事終わりの時間からオンラインで見つつ、途中お風呂に入り*1、最後まで楽しく見ることができました。

この勉強会を見るまでにコスト最適化に関わるような仕事をしたことはないので、生きた知識があるわけではなかったですが、事前に知っていた知識やコストがかかる事例と似た事例が何度も発表ででてくるのを見て、コストがかかりやすいポイントはある程度絞られてくるのかなと見ていて思いました。具体的には以下の様なポイントです

  • まずCost explorerを確認して、一番料金がかかっているところ、ボトルネックを潰していく
  • NAT gateway経由の転送量が多いと料金がすごいことになるのでVPC Endpointを活用する
  • CloudWatchは料金が高いので、用途と運用に応じてログの保存先をS3にし、可視化はAthenaでやるという構成で運用上問題ないか検討する

発表で印象的だったのは、「AWS SAP試験の対策をしていたら似た事例があったからコスト削減ポイントに気づけた」という話です。

AWS Well-Architectedの6つの柱の中に「コスト最適化」が含まれるようになったというのは話として聞いていましたが、実際にこうやって効力を発揮しているんだろうなあということを感じられたのは初なので印象深かったです。

まだまだコスト最適化・削減については自分の中で消化し切れていないので、今回の動画を見直したり、以下の様な書籍を読んで勉強していきたいと思います。

後余談ですが、ライブを見ながらTwitterに流されていて確認できる限りの登壇者の資料をconnpassの街頭イベントの資料ページにペタペタ貼るということをしていました。後で見る人の資料探しを楽にするという意味で、多少は貢献できたかなと思います。


このAWSコスト削減天下一武道会の主催者である西田さんがイベント終了後に記事を書いていたようです。

www.keisuke69.net

*1:入浴中にイベントの動画を見るの、初体験。それくらいには見逃したくなかったのです。ライブの時はアーカイブが残るかどうか分からなかったので。