インフラエンジニアとして働いているTIGの原木です。
cfn-guardを使用してTerraformをポリシーチェックしようとした話をします。
cfn-guardのcfnとはAWSのCloudFormation(AWSのIaCソリューションのこと)の略称です。
このツールはCloudFormationを使ってAWSのリソースをデプロイするときにその内容をチェックするポリシーチェックツールとしてよく使われています。
しかし、cfn-guardはその名前に反して、CloudFormationに限らず、JSON/YAMLファイルに対する汎用的なポリシーチェックツールとしても使用することができます。
READMEの記載にも、次の通り説明があります。
Guard offers a policy-as-code domain-specific language (DSL) to write rules and validate JSON- and YAML-formatted data such as CloudFormation Templates, K8s configurations, and Terraform JSON plans/configurations against those rules.
Guardは、CloudFormationテンプレート、K8sコンフィグレーション、TerraformのJSONプラン/コンフィグレーションなどのJSONやYAMLフォーマットのデータに対して、ルールを記述し検証するためのPolicy as Codeなドメイン固有言語(DSL)を提供します。
AWS Certified Securityの勉強をしていて本ツールの名前を知り、READMEを見て、自分は興味を持ちました。
Terraformのポリシーチェックとしては、過去にFuture技術ブログで紹介したtflintやterraform validator1、tfsec2等すでに様々なツールがあります。
その中でなぜあえて、cfn-guardをTerraform planをチェックしようとしたのか?
それはcfn-guardに読み込ませるルール表となるCFn Guard DSLとAWSマネージドサービスの力を借りてインフラをデプロイする前から後まで一貫したポリシーチェックができるのではないか。と考えたためです。
一度CFn Guard DSL(ポリシールール)を書くことで二度おいしいメリットがあると考えました。
CloudFormation Guard で Policy as Code! 実際どうよ? / Policy as Code with CloudFormation Guardのスライドをお借りすると次のようなイメージです。
このような 青写真 を描きました。
このブログでは、cfn-guardを検証し…そして、思ってたのと違った!!という話をしたいと思います。
一番基礎的な使い方として、S3ファイルをデプロイするterraformのファイルをチェックする方法について書きたいと思います。
前提として、cfn-guardはHCLファイル(要は.tfファイル)を直接チェックできず、JSON/YAMLファイルを読み込ませないといけないので、 terraform plan
の実行結果からJSONファイルを作成する必要があります。
下記のようにS3バケットを作成するHCLファイルがあったとしましょう。
# provider等は省略します |
このファイルがまだ未作成の場合、次のようにコマンドを実行することで
作成後に想定されるリソース構成をJSONファイルで出力することができます。
terraform plan -out tfplan.bin |
tfplan.json
のファイル構造を分解して中身を見てみましょう。
※そのままだと見づらいのでJSONファイルをサブセットであるYAMLファイルに変換して表示します。
※YAMLファイルでも素のJSONファイルでもcfn-guardは動かすことができます。
format_version: "1.2" |
上記リソースをチェックするためのリソースポリシーを書いてみます。
よくあるリソースポリシーとして
をチェックしたいと思います。
let aws_s3_bucket_resources = planned_values.root_module.resources[type == "aws_s3_bucket"] |
このルールに従っているか実際に cfn-guard
を動かし、チェックしてみましょう。
cfn-guard validate -r s3_template_example.guard -d infrastructure/tfplan1.json -o yaml |
エラーになりました。contextを確認すると、 context: ' %aws_s3_bucket_resources[*].values.bucket EQUALS "/^test-.*/"'
とあるようにバケットの命名規則がルールに従っていないことがわかります。
そこでバケット名を修正し、再度 cfn-guard
にかけてみます。
修正したs3.tf及びyamlファイルは割愛 |
今度は通りました。
序の口ではありますが、cfn-guardを使ったチェック方法について、雰囲気は掴めたのではないかと思います。しかし、ここから先、cfn-guardを深掘りするうちにギャップが広がっていくことに気づきました。
当初の自分の妄想では、cfn-guardとは、AWSのリソースAPIにアクセスしていい感じにチェックするツールなのかなとふわっと思ってました。ですが、実践例にあるように実態はどうでしょうか?
カンの良い方はすぐに気づかれたかもしれません。
CloudFormationを例に先ほどと同じことをしてみましょう。
AWSTemplateFormatVersion: 2010-09-09 |
というファイルに対してS3のバケット名の命名規則をチェックします。
let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ] |
別物やんけ。
その通りです。なぜなら、CloudFormationとTerraform planではファイルの構造が全然異なりますので。
cfn-guardの実態は、CFn Guard DSLに基づきJSONやYAMLなどの構造型データを検査する、ある意味シンプルな構文解析ツールです。
したがって、ポリシーファイルについてCloudFormation向けはCloudFormation向け、Terraform plan向けはTerraform plan向けに書く必要があります。そして後者のTerraform plan向けのポリシーファイルは、当然AWS Config上で動きません。
ここに当初の構想はからくも崩れたのでした。
ECSのTask Definitionのようにインラインで文字列化したJSONファイルをいい感じにパースする方法が見つからなかった、tagチェックでtagsとtags_allを別々にチェックする必要があった、そもそも構文エラー時説明してるようで何も説明してくれないエラーログ等、細かいことを言い出すときりがない不満があり、最終的に自分はおとなしくtflintに戻りました。
Terraformユーザーには、cfn-guardの扱いは少々難しいところがあるという話でした。
ポリシールール等の設定について最近はChatGPT先生に下書きをお願いすることが多いのですが、彼女に自由に書かせたら、明らかにAWS CloudFormationテンプレート向けのguardファイルをTerraformと言い張ったのは悲しかったです。
WHY?と聞いたら次の通り開き直った回答が返ってきました。
しかし、AWS CDKを使ってCloufFormationのテンプレートファイルを生成し、AWSリソースのデプロイを行っているユーザーにとって強力なポリシーチェックツールなのは間違いありません。
CFn Guard Rules Registryには、ルールの実装例が多数掲載されております。
Amazon Web Services’ Well-Architected Framework Reliability Pillar等、インフラエンジニアが非機能要件を考える時のベストプラクティスを実装したポリシーファイル等もあり、痒い所に手が届く例となっています。
以上、参考になれば幸いです。
]]>Terraform連載2024の5日目です。
IaCを利用してインフラを構成することで、構築忘れや設定ミスといったイージーなミスが減らせるようになりました。とはいえ、展開していく範囲が増えれば増えるほどコードの量も増えていくので、このリソースはどこで作ったっけ…みたいなことが起きてしまいます。
現在の業務ではサービスの海外展開に携わっており、まさに多国展開絶賛実施中という状態です。その際、スペックは同じでもリージョンのみが異なるリソースを作成することが多々あり、環境の管理方法って大切だなーと実感しております。
そこで本記事では、Terraformを利用してシステムを他リージョンへロールアウトする場合のリソース管理・展開方法を3つ挙げてみました。もちろん、他にもたくさんあると思いますので、本記事が参考になると幸いです。
また、本連載の1日目でも伊藤さんがマルチリージョンによるDR(Disaster Recovery)戦略についての記事を書かれているので、こちらも参考にしてください。
以下のような前提で考えてみます。
以下のように環境、リージョン1をそれぞれディレクトリ分けして管理します。
envs |
各環境の中にcommonとリージョン毎のディレクトリを持ちます。commonにはVPCやプロジェクトといった共通となるリソースを置き、リージョン毎に必要なリソースはリージョンディレクトリに配置します。
また、各ディレクトリでbackendを持ち、tfstateの管理を行う形となるためterraform
コマンドは各ディレクトリに対して行う必要があります。
単純にディレクトリをコピペして展開していけるので、リージョンの追加があった場合でも共通リソースの展開であれば容易に実行できます。視覚的にもしっかり分かれているので、新規参画者などにも認知負荷が高くないです。
ただ、共通リソースを参照する場合はdata
として用意する必要があり、冗長な感じは否めません。
data "google_project" "my_project" { |
以下のように環境をそれぞれディレクトリ分けして管理します。
envs |
先ほどとは異なり、各環境でbackendを1つとしてWorkspaceによってリージョンを区別していきます。
$ terraform workspace list |
また、plan/apply時にtfvarsを利用することによって各環境ワンリソースで管理することが可能となります。
$ terraform plan -var-file config/sydney.tfvars |
resource "google_storage_bucket" "bucket" { |
Terraform Workspaceは機能として存在するものの、開発環境を区別するのには非推奨2など、中々使いどころの難しい存在でしたがリージョンを区別するのには使えそうです。この構成であれば、他リージョン展開時に新しいtfvars
ファイルを作成するだけでよいので、ロールアウト時の作業が激減します。
ただ、新しいリソース・変数を定義する場合には全tfvars
ファイルに値の追加が必要なので注意が必要です。
ちょっとした亜種ですが、Workspace名をそのまま変数として持ってきてリソースに適用することも可能です。
※この場合は、Workspace名にasia-northeast1
やaustralia-southeast1
を使う必要があります。
resource "google_storage_bucket" "bucket" { |
以下のようなディレクトリ構成で管理します。
envs |
各種リソースはワンリソースとして、Plan/Apply時に各tfvars
ファイルで変数を渡す形となります。
環境やリージョンを変更する場合のbackendの変更はどうするのか?という部分ですが、terraform_init.sh
というbashスクリプトを介して、各環境ごとでbackendを構成し直します。
|
そのため、リージョン毎のbackend変更に関しては都度スクリプトを実行する運用でカバーしていく形となります。
以下の記事で紹介されているようにplan/applyなどTerraformのコマンドもラッピングすることで、Terraformの操作を全てシェルスクリプト経由にしてしまったほうが誤ったbackendでのplan/applyが起きないかもしれません。
参考:Terraformでmoduleを使わずに複数環境を構築する
上記3つの方法をまとめると以下のようになります。
案 | リージョン展開方法 | リージョン毎の運用 | 新しいリソースの追加方法 | 認知負荷 |
---|---|---|---|---|
①ディレクトリ分けのみ | ディレクトリを追加 | ディレクトリ毎でApply | 各環境・リージョンでファイル追加 | 低 |
②ディレクトリ分けとworkspace | 各環境でtfvarsとWorkspaceを追加 | Workspace毎でApply | 各環境でファイル追加 | 中 |
③ワンリソースにしてtfvars | 各環境でtfvarsを追加 | シェルスクリプトを実行してからApply | 1ファイル追加 | 高 |
展開するリージョンの数が少なければ①が楽そうですが、どんどん増えていくのであれば②と③のどちらかな気がします。
実務上では参画当初から③の方法で運用を回しており、CIでデプロイの自動化をしている部分もあるのでシェルスクリプトの実行に関してはそこまで負担に感じておりません。ただ、個人的には②のやり方がよりシンプルになるので好みです。
本記事ではインフラの多国展開時におけるTerraformの管理方法を紹介してみました。これらが正解というわけではなく、運用を回していく中で強み・弱みが見えてくると思いますので、せひチームに適した形を探してみてください。
アイキャッチ画像のアイコンは以下から引用させて頂いております。
HashiCorp Brand
Google Cloud - アーキテクチャ図用のプロダクト アイコン
はじめまして。Technology Inovation Group (TIG) の原田と申します。
私が所属しているチームではTerraformでAWSリソースを管理しており、tfstateを統合する機会があったので、その経験をもとに記事を書きます。
Terraform バージョン 1.5.7
私が所属しているチームでは、サービスごとに複数のtfstateでリソースを管理していました。
ただ、プロダクトが成熟してきて、リリース頻度が非常に低い&リリースをしたとしても複数サービスが同じタイミングでリリースされることが多くなりました。
そういった理由から、成熟しきったStateを統合しようという機運が高まりました。
ゴールは、移行元のtfstate(a.tfstate)を移行先のtfstate(b.tfstate)にマージしてb.tfstateですべてのAWSリソースを管理できる状態です。
そのための作業パターンとして大きく2つの案を考えました。
手順:
remove
コマンドimport
コマンドplan
にて差分が発生しないことを確認お手軽ですね。Terraform直々に提供している機能ということもあって、安心感もあります。
1. 統合元のStateで remove
コマンド
# 統合元のStateにて |
2. 統合先のStateで import
コマンド
# 統合先のStateにて |
Terraformのバージョンが1.5以上の場合はimport ブロックを使用するのもよいでしょう。
https://developer.hashicorp.com/terraform/language/import
3. Terraform のコードを統合
コピペでいいですね。リソースが多い場合はかなり根気が要ります。
4. plan
にて差分が発生しないことを確認
$ terraform plan |
お疲れ様でした。
お気づきの方もいらっしゃるかと思いますが、管理リソースが多い場合はかなり作業コストが高いです。リソースごとに、import・removed・コードの修正が必要になります。
私のチームでは100を優に超えるAWSリソースを管理していたため、この方法は現実的ではありませんでした。
Stateを直接操作することはあまり推奨されないですが、今回はこの選択をとりました。
操作イメージとしては以下のような手順です
手順:
plan
にて差分が発生しないことを確認パターン①よりステップが一つ多くなっているように見えますが、ちょっと待ってください。詳しく見ていきましょう。
1. ローカルにtfstateをローカルに取得
# 統合元のStateにて |
# 統合先のStateにて |
2. 二つのStateファイルをマージ
ここが今回の作業のキモです。頑張って手作業でStateファイルをマージしていきましょう。
…というのは危険なので、今回はfujiwaraさんが開発された、便利なツールを利用させていただきましょう。
https://github.com/fujiwara/tfstate-merge
コマンド一撃でtfstateファイルをマージしてくれる、何ともありがたいツールです。
ある程度のバリデーション機能も実装されているので、手動でマージするより、格段に安心して作業できます。
こちらで作者であるfujiwaraさんの解説があるので、詳細に関してはそちらをぜひご覧ください。
https://sfujiwara.hatenablog.com/entry/tfstate-merge
$ tfstate-merge b.tfstate a.tfstate > b'.tfstate |
3. マージしたtfstateファイルを統合先にpush
terraform state push b'.tfstate |
4. Terraform のコードを統合
ここはパターン①と同様、根気よく作業していきましょう。
git の 差分の増減などを確認するもよいでしょう。
5. plan
にて差分が発生しないことを確認
$ terraform plan |
お疲れ様でした。
こちらのパターンの利点は作業の負担が圧倒的に少ないです。ただ、terraform state push
というかなり危険なコマンドを使用するので実行には細心の注意を払いましょう。
tfstateを統合した経験について書きました。
Stateの統廃合や、moduleへの移行など、Terraformのリファクタには様々な手札がありますが、エンジニアたるもの日常的により良いコードを目指していきたいですね。これは自身への戒めでもあります。
新卒でIT業界に飛び込み、もうすぐ2年が過ぎようとしていますが、自身の経験を発信していくことは良い刺激になりました。Terraform 連載2024の一環として執筆させていただきましたが、非常に興味深いトピックが目白押しなので、ぜひ他の記事もご覧ください!
]]>Terraform 連載2024の3本目です。
はじめまして。フューチャーキャリア入社1年半の森と申します。
Terraformにおける変数を、構築するインフラの要件に合わせてどのように制御していくかについてまとめます。
Terraformにおける変数制御は、構築するインフラの要件を明確化させる上で重要です。そのため、どのような変数制御があるか、ユースケースを踏まえ見ていきます。
Terraformには型による制約の他に、下記3種類の変数の制御方法があります。
静的・動的の可不可や、事前・事後のチェックなどの違いがありますが、図にするとこのような感じです。
動的チェック | 事前チェック | 事後チェック | |
---|---|---|---|
validation | 不可 | 可 | 不可 |
precondition | 可 | 可 | 不可 |
postcondition | 可 | 不可 | 可 |
具体的に1つずつ確認していきましょう。
validation
は3つの変数制御の中でも最もシンプルで簡単な制御の方法です
例えば、AWSのEC2でEBSを暗号化したい場合、以下のようにvalidation
ブロックを暗号化の有無に関する変数に追加します。
このブロックのcondition
の内容で真偽を判定し、偽の場合error_message
で指定のエラーメッセージを出力できます。
resource "aws_instance" "example" { |
ebs_encryption
にfalse
を指定した場合、terraform plan
時にvalidation
ブロックで指定した下記のエラーメッセージが出されます。
╷ |
このようにcondition
に条件式を書いておくことで、誤ってEBSの暗号化を無効にする事のないように事前にチェックをしてくれるのがvalidation
の特徴です。
構築したいインフラに対して、あらかじめ変数の条件をハードコードすることで制約を課したい場合にvalidation
ブロックが使えます。
上述のvalidation
で課すことのできる制約は静的な条件に限りました。つまり、全て条件式にハードコードして制御する必要があり、動的にAWSから情報を取得して条件を絞ることは不可能でした。
これを解決したのがTerraform v1.2から追加されたprecondition
です。例えば、EC2のインスタンスタイプを無料利用枠のみに制限したい場合、aws_ec2_instance_type
データソースから最新の無料利用枠の情報をAWSから取得し、インスタンス構築の際の条件に課すことが可能です。
lifecycle
ブロック内にprecondition
ブロックを作成し、condition
の条件で真偽を判定し、偽の場合にerror_message
に記載されているメッセージを出力できます。
data "aws_ec2_instance_type" "example" { |
上記のようにインスタンスタイプをc5.xlarge
(無料利用枠でないインスタンスタイプ)でplanを流してみるとどうなるでしょうか?
╷ |
指定のエラーメッセージとともに無事?planが通らなくなりました。(当然、applyもできません。)
このようにvalidation
と違って動的な変数の制御を可能にするのがprecondition
の特徴です。
validation
でハードコーディングするのが難しい場合に使うのが良いでしょう。
最新のAWSからの情報が常に反映されるため、より安定的なチェックが実施できます。
varidation
やprecondition
はapply前に変数をチェックする事前チェックでした。一方でpostcondition
は、applyの後にエラーを追跡する事後チェックが可能です。
例としてオートスケーリンググループを作成する際、AZが2つ以上あるかどうかをチェックしたい場合を考えます。書き方はprecondition
と同様で、postcondition
ブロックのcondition
でAZの数が1よりも大きくない場合に指定のエラーメッセージを出力するようにします。
resource "aws_launch_configuration" "example" { |
planをしてみましょう。この時点でAZはknown after apply
とあるので、エラーは捕捉されずにplan自体は通ります。
Terraform will perform the following actions: |
この状態でapplyすると完了後に下記のエラーが出ます。
╷ |
エラーは出力されていますが、precondition
と異なり、planはちゃんと通ってリソースまで作成されているのが分かります。
これがpostcondition
の事後チェックというもので、apply後へのリソースの変数に対するチェックを実施することが可能になっています。
validation
やprecondition
などの事前チェックのみで補足しきれない条件を課すのが良いでしょう。
上記のような例だと、apply時にリソースが作成されてしまうので、事前チェックのようにリソース作成前に制限を課すような強い制約ができないことには注意が必要です。
インスタンスタイプの選定でprecondition
を使いましたが、precondition
→postcondition
と単純に置き換えてみたらどうなるでしょうか?
data "aws_ec2_instance_type" "example" { |
上記はpreconditionについてにおけるコードでprecondition
がpostcondition
に置き換わっているだけです。
planで下記エラーが出てきます。
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: |
エラーメッセージはprecondition
とほぼ変わらないのですが、postcondition
では plan によるリソース出力が成功します(precondition
ではplanが失敗してリソース出力がされませんでした)。
これはpostcondition
が事後チェックであることを反映している例で、データソースやリソースが取得された後で値が評価されていることを如実に表しています。
こうなると postcondition
で全部チェックしてしまっても良い気もしますが、事後チェックであるか事後チェックであるかを明示するため、precondition
で制御できる箇所はprecondition
で制御するようにしましょう。
多くの変数に対して制限を課すのはなかなか大変で工数を取る上、可読性にも影響します。
修正の際にupdate in placeなどで気軽にアップデート出来ない変数(EBS暗号化の有無など)に対する制約から優先的に実施するのが個人的には良いと思います。
TIG真野です。
Terraformファイルをコード生成するため、hclwriteというGoパッケージの使い方を調べました。
ある複数のリソースをセットで定義する設計開発ルールがあったとします。AWSの例ですが、以下のようにDynamoDBとその監視をCloudwatch Metricsを用いてセットで行いたいとします。
# DynamoDB |
ここではスロットリング数だけを監視していますが、もう1~5個くらい監視したい項目があったとします。この例のように、あるリソースの追加に合わせて整合性を保ちつつ別のリソースを追加することは難しく、抜け漏れがちです。
正攻法だとTerraform module化でしょう。しかし、このケースではモジュール化するにしてはリソース数が少なく、モジュール化すること自体が新規参画した開発者にとって認知負荷が高いことを考えると、もう少しプロダクトが成長して、関連するリソースが増えるかどうかを待ってから対応を考えたいケースもあるでしょう。もちろんチームの方針としてこれくらいでもすぐにモジュール化に取り掛かる場合もあるかと思いますが、チームのTerraform習熟度にバラツキがあり設計パターンを抑えたいなど、様々な背景があったとします。
こうした場面で、あるTerraformで定義したリソース(ここではDynamoDB)をインプットに別のコード(Cloudwatch Metrics)を生成し、モジュール化しなくとも不整合が生じにくい開発フローを整備したいと思います。これでモジュール化の判断を先送りにできますね。
ちなみに、通常、DynamoDBはそこまで数が増えない(ほいほい増えるようであればおそらくDynamoDBを使うべきではない)し、監視項目もそう変更しないだろうから、コード生成もモジュール化しなくても良いんじゃないか?という意見もあるかと思いますが、それはそれとします。
この回のケースではRego(Conftest)を使って整合性チェックを入れるのも有効でしょう。しかし開発チームにRego経験者はほとんど供給され無いと思うので、学習コストが多少なりとも掛かります。また、不整合を検知できるのであれば自動でFixしてくれた方が開発者フレンドリーです。
そのため、この記事では .tf ファイルの自動生成に注目します。
Terraformの .tf コードをパース、生成する方法として有名なのは、hashicorp/hcl を用いることです。Go言語でインポートしてライブラリとして使えます。HCLはTerraformの.tfファイルが利用するファイルフォーマットのことです。
管理されているパッケージはいくつかあり、以下のようなものが含まれていて、いい感じに使い分けるリテラシーが求められます。
パッケージの概略はthaimさんのZenn記事のHCLファイルを hashicorp/hcl で読み書きするが実装もありイメージしやすいです。
ここでも簡単に一覧を載せます。
Name | Memo |
---|---|
hclsimple | HCLをGoの構造体にマッピングする、encoding/json 的な高レベルなパッケージです。しかし、拡張子.tf には対応しておらず、全ての.tfファイルに対応していないことを言外に伝えています。 |
hclparse | HCLファイルをパースして、結果を独自のStructで取得できます |
hclwrite | HCLファイルを加工するのに適したペッケージです。元のHCLファイルの構造を壊さず、リソースの追加/削除、コメントや属性などの編集を行えます |
hclsyntax | HCLを解析してASTを作るパッケージです。hclparseなどにも使われています |
今回はTerraformコードの細かい解析は不要であるため、hclwriteパッケージを利用します。
バージョンは hashicorp/hcl/v2 v2.20.0
を利用します。
まずは.tfファイルを読み込みます。
hclwrite.ParseConfig()
でパースしたいファイルを指定します。
package main |
結果の取得は tfFile.Body().Blocks()
という部分で取得できます。
Blockとはなにかですが、 resource
、module
、locals
のようなTerraform上でインデントをともなうような塊を指します。例えば、resource "aws_dynamodb_table" "table1" {...}
といった定義が10あれば、for文が10呼ばれます。
次にわかりにくいのが b.Labels()
の部分です。これは aws_dynamodb_table
, table1
といったブロックを開くときに設定されるTerraformのリソースタイプ、リソース名が入ります。
今回はTerraformのリソース名を取得して表示するとします。
次のようなファイルがあるとします。
resource "aws_dynamodb_table" "myproduct_read" { |
さきほどのコードを実行すると、Terraformリソース名が取れています。
$ go run . ../example/dynamodb_table.tf |
属性を取得するためには、b.Body().GetAttribute()
などで取得できますので、目的に応じて条件を追加することができます。
次に.tfファイルを生成します。
hclwrite.NewFile()
で初期化し、そこにブロック(Terraformリソース)を追加していきます。
今回は新規コード生成なので、先頭に // DO NOT EDIT
コメントを追加しましょう。
hclwriteを用いるとコメントを追加する便利な関数は(おそらく)存在しないので、いきなりですがトークンレベルの操作となる、AppendUnstructedTokens()
を用います。
func main() { |
for 文の中にある、 AppendNewBlock()
が今回出力したい本丸の、aws_cloudwatch_metric_alarm
リソースを追加する部分です。
実行すると次のような空リソースが生成されます。
$ go run . ../example/dynamodb_table_one.tf |
SetAttributeRaw()
を用いて各属性ごとに項目を追加していきます。 SetAttributeRaw()
は低レベルのAPIで、トークンを直接追加します。今回、"${aws_dynamodb_table.myproduct_read.name}-throttledrequests"
といったリファレンスを追加したいため利用しています。SetAttributeValue()
を使う方法だと、 $
がエスケープされて、 $$
と出力されてしまうためです。
また、特記したいことはzclconf/go-cty というライブラリの型で値を競ってしないとならないことです。ここでさらに別のライブラリ?と一瞬焦る気持ちがありますが、慣れていきましょう。
属性の型がオブジェクトであり、その中にリファレンスが入ると、再び AppendNewBlock()
を呼び出す必要があるなど、生成したい定義によっては試行錯誤する必要があるので、注意してください。
func main() { |
これを実行すると次のようにTerraformコードが生成されます。
$ go run . example/dynamodb_table_one.tf |
hclwriteパッケージ自体は文法チェックを行いません。そのため alarm_actions = aws_sns_topic.myproduct_alert.arn
としれっと存在しないリソースを参照してもエラーにはなりません。
説明を省きましたが hclwrite.Format()
でフォーマットをかけられるので、お手軽です。
今回使用したコードの全量は↓のリポジトリにコミットしています。
https://github.com/ma91n/hclwrite-dynamodb
究極的には生成したいTerraformコードがどのようなものであるかに依存しますが、あるTerraformコードを読み取って別のファイルを生成するだけであれば、hclwriteパッケージを用いてもそれほど難しくはありません。
今回の内容であれば、パース部分もHCLの構造を無視し、スクラッチで解析しても良さそうなレベルではあります。しかし、複数のリソースタイプが混ざったり、ある属性の条件でのみを対象としたいといった拡張はしばしばありえるので、こういったライブラリを用いてパースすると良いでしょう。
一方で生成側です。今回の用途だと、既存ファイルの更新ではなく新規生成です。しかも成果物の構造のシンプル。この場合は、hclwrite
の仕様を学んでゴリッと出力するより、Go Templateなどお好きなてプレートエンジンで生成するほうが遥かにメンテナンスがしやすいいと思います。HCLのフォーマットだけは使っても良いかもしれませんが、全てをhclwriteで閉じて生成するのは、出力がこれだけであれば割に合わない気がしました。
hclwrite
ですが主な用途はすでに存在するTerraformのコードを破壊せず、一律でタグを付けたり属性を変えたりといった用途に向いているパッケージのようです。
hclwriteというパッケージを用いてあるTerraformリソースから、別のリソースを生成しました。新規生成については別のテンプレートエンジンを利用する方が良いかなと個人的には思います。
こういった開発フローは少し特殊で、通常はTerraformモジュール化などを試みると思いますが、モジュール化するにしてはリソース対象が少なく、ちょっと抽象度が弱いんだよな~といった場面では、コード生成案も考えてみても良いのではないでしょうか。
]]>こんにちは。技術ブログ運営の伊藤です。
本日、3/11よりTerraform連載を開始します。
昨年はTerraform v1.4がリリースされたことをトリガーとして技術ブログでは初となるTerraform連載2023を開催しました。その時の募集形態は以下です。
- v1.4のリリース内容
- これまでTerraformを触ってきたノウハウ、Tips
- エコシステムについての調査、学習
今年の連載については上記の中から1点目を除き、社内に募集をかけました。すると、募集開始後数日であっという間に10人強集まり、社内でも利用者が増えていること、ナレッジが蓄積されてきていることを感じています。
さて、目を世の中に向けてみて、どれくらいTerraformに対しての興味関心があるのかと思い、Google Trendsで調べたところ、以下のグラフとなりました。
注) 青: Terraform, 赤: CloudFormationTerraformとパブリッククラウドの中では利用比率が最も高い、AWSのCloudFormationとの比較ですが、大きく差が開いており、その興味関心度を伺うことができます。ちなみに、v1.0のリリースが2021年の夏頃でしたが、その半年後の2022年に入って以降が伸びているようです。
今回の連載は10人を超すメンバーでお送りします。まだテーマ未定のところもありますが、公開時までのお楽しみということでしばらくお待ちください。
また、テーマが決まっているものも変更になったり順番が前後する可能性もありますが、ご了承ください。
日付 | 投稿者 | テーマ |
---|---|---|
3/11(月) | 伊藤太斉 | 本インデックス記事 & TerraformにおけるDR戦略を考える |
3/12(火) | 真野隼記 | hclwriteを用いたtfコード生成入門 |
3/13(水) | 森大作 | Terraformにおける変数の制御について |
3/14(木) | 原田達也 | Stateを統合してみる] |
3/15(金) | 岸下優介 | サービスの多国展開を支えるTerraform構成 |
3/18(月) | 原木翔 | cfn-guardを使ってTerraformをポリシーチェックしようとした話 |
3/19(火) | 棚井龍之介 | Terraformの実装を読む |
3/20(水) | 大岩潤矢 | TBD |
3/21(木) | 前原応光 | TerraformのMock |
3/22(金) | 小林弘樹 | TBD |
3/25(月) | 真鍋優 | TBD |
Terraform、もといIaCのメリット、思想として謳われるものの中には可搬性、再利用性があります。再利用性を上げておくことで、同じ構成のインフラ、サービス群を一括して作成することができ、手作業と比べた時の再現性、信頼度が大きくなることはいうまでもありません。
具体、業務を前提としたITインフラ環境を考えたときに、社会インフラとして稼働しているサービスや、どんな状況であろうとも24時間365日稼働していないといけないサービス、企業があります。このような企業では災害対策環境として、地理的に大きく離れたエリアにDCを構えることで、ある1点で甚大な災害が発生してサービス断になったとしても、別のDCで継続することが可能になります(もちろん一定時間のサービス断は発生しますが、数日、数ヶ月単位になることはほぼないでしょう)。
この災害対策環境(この後はDR環境と記載します)については…
…のいくつかのパターンが考えられます。昨今のクラウドインフラを鑑みると、コストメリット、移行の容易性を考えると2が妥当と考えられるケースが多いかと思います。事前にDR環境へ作成するリソースについても本環境と同等、同様のものを作ることは少なく、このハンドリングについてはTerraformの記載に依存するものがあります。本記事ではいくつかのパターンに分けつつ、今私自身が実践している方法について説明します。
今回のTerraformの構成はモジュールを利用して説明します。採用理由はモジュールという形でパッキングしておくことで、特定の単位のリソースを一括して起動することができることにあります。本環境とDR環境では基本的には同一のインフラができることが望ましいので、モジュールであれば任意の単位で横展開できることを考えています。
また、本環境のリージョンを東京(ap-northeast-1)、DR環境を大阪リージョン(ap-northeast-3)で構成することを前提とします。
いくつか、分割する単位にも考える余地があるかと思います。これは、私自身が構築した所感ですが、1つのモジュールでDR環境まで表現することはせず、本環境向けで構築をした上で、DRにも展開できる様に手を入れるのが良いのではないかと考えています。これには
があげられます。前者についてはさらに3点に場合分けして説明しますが、後者については肥大化することで可読性や重複した表現でリソースの適用漏れなどヒューマンエラーの元になります。また、肥大化したとしても本環境、DR環境にフォーカスするのであればあまり大きな問題にはなりません。ただ、実際にはDR環境まで作らない環境(開発環境など)にも同じモジュールを適用する場合、DR向けとして定義したリソースのハンドリングが煩雑になることが容易に想像されます。いきなりDR環境も同じモジュールで構築する、というのは難しいですが、本環境向けのみに作成の上、少々のテコ入れをすることが望ましいです。
前者について、さらに場合分けすると…
…の3つが挙げられます。これらについてさらに説明します。
まずは純粋な複製で済む場合です、ちょっと極端ですが、VPCのみをラップしているモジュールがあると仮定します。
# vpc.tf |
provider "aws" { |
モジュールに、作成するリソースごと重複を許容できない場合には変数を切り出し、モジュールの呼び出し側で適切なパラメータを渡してあげるイメージになります。また、Providerがクラウドのエンドポイントをさし示しているので、モジュールごとエンドポイントの向く先も変わるでしょう。
これは本環境、DR環境両方で常時稼働させるパターンのもの、かつ、本環境のリソースを引用、および継承してDRを作る場合がここに当てはまります。そもそもDR環境で稼働させるものは何かと考えたとき、迅速な回復、喪失を避けて通りたいのはDBやバケットなどのストレージ系が当てはまるかと思います。その中でRDSを例とします。
resource "aws_db_instance" "postgres" { |
provider "aws" { |
ポイントだと考えているのは…
…だと考えています。変数としてdb_primary_arn
を書き出し、この変数に値が入っているかどうかを判定することで、プライマリのインスタンスになるか、DR向けのレプリカインスタンスかを判定して作成することが可能になります。
三項演算子を多く使っていることはもう少し改善ポイントですが、少ない変数でリソースのハンドリングができることで、使いやすさを意識しています。そして、default = null
にしておくことで、不要な変数を定義を回避しています。
コストの観点から、DRでは構築せず、有事の際に作成するリソースもあり得るでしょう。その際にはcountやfor_eachといったTerraformにおけるループ構文を用いてハンドリングしています。例ではEC2インスタンスを記載しています。
resource "aws_instance" "instance" { |
provider "aws" { |
instance_ip_address
の配列に対して、IPがあればその分だけリソースを作成し、空の配列(デフォルト値)が入ったときはcount = 0
になるので作成されないことになります。
Terraformを使ったマルチリージョン構築は、IaC自体のメリットを最大限かせるものだと考えています。その中で再利用性、可搬性を意識していくと、私はここまでに記載したような方法でそれなりにスマートに実装ができたと思います。
途中にも記載しましが、まずは本環境向けの構築を中心におこない、その上でDR環境に向けて何を切り出さないといけないか、微修正を重ねながら実装していくことが1つの方法でしょう。
本記事以外の構成も気になるので、ぜひリアクションいただけると幸いです。
最後になりますが、今日から約10記事、Terraformのネタが続きますので、この連載や技術ブログにこれまで上がっているTerraformの記事もぜひご覧ください。
本記事では触れていませんが、1つのモジュールに対して2つのリージョンに投げる場合(CloudFrontとALB)の構成の場合には、configuration_aliases
を使うことで対応できます。
(configuration_aliases
の使い方は「かゆいところに手が届く、Terraformの書き方 (configuration_aliasesの使い方)」を参照ください。)
The Gopher character is based on the Go mascot designed by Renée French
TIG真野です。
フューチャーでは2021年の2月に公開されたGo 1.16から、Goのリリースノートを読んで気になったところをブログにまとめるというブログリレーを続けています。
単なる翻訳ではなく自分たちならではの付加価値を提供するための執筆のフローや秘訣を、初心者向けにまとめます。社内外あるいはGoだけのとどまらず、次の新しいソフトウェアや技術のリリース時に技術ブログが増えると良いなと思っています。
Release Historyを見るとわかりやすいですが、Goは年2回のペースでメジャーアップデートを繰り返しています。だいたい、2月と8月です。
その1, 2ヶ月ほど前になるとRelease Candidate(RC)版が公開され、先立って機能を試すことができます。RC版は、 Go1.x.rc1
, Go1.x.rc2
などというサフィックスで公開され、rc1
とか rc2
が出るともうそろそろメジャーリリースが出そうだなと個人的にはワイワイな気持ちになります。
フューチャーでは、この RC
版が公開されてから、実際にメジャーリリースされる期間(1ヶ月程度)の間に触ってみてブログを公開していくという流れを取ることが多いです。
リリース状況は以下のXのアカウントをフォローしておくと便利だなと思います。
リリースノートはRC版が公開されるころにはDraft版が公開されていますので、ざっと見て、各人の興味がある部分を選定します。テーマ選定が個人的に一番悩ましいポイントです。
Go 1.22を例にします。「私見」ですがリリースノートは大きく6つのブロックに分割できます。
それぞれ、ブログに取り上げるテーマとして「私見」を述べます。
go test
のようなコマンドのアップデート系ですnet/http
など利用頻度が高いメジャーなパッケージの変更は、役立つことが多いので調べると多くの人にとっても学びになります「お勧め」と書いた部分も、メジャーバージョンごとに差異が大きく、実際にテーマ選定時には当然リリースノートを確認する必要があります。面白そうなアップデートだと思ったけど、Windows OS特有の修正で、検証が難しくて途中で諦めたケースなどが過去の連載でもありました。
※改めて強調しますが、全て個人の意見です。初心者だろうがベテランだろうが、気になったところを興味が赴くままに触ってみることが大事だと思っています。一方で、ピンと来る内容が無いことも初心者だと多いと思います。その時に、あえて絞り込むならという視点で書いています。
初心者の場合は、どのテーマを深掘りすべきか決め手にかけるということも多いと思います。
その場合は、有識者に「書きやすそうなネタリスト」を出してもらって、そこから選ぶというのも良い進め方だと思います。
過去、わたしも社内で執筆メンバーを募集したときは以下のネタが書きやすいのでは?という内容をリストアップして告知しました。
改めて見ると、このリストアップもどうなんだ…とも思いますが、これを見てテーマを決めたという人もいました。
難易度を判定できる人に教えてもらう、というのは最初の取っ掛かりとして悪くないため、周囲に頼れる人がいれば頼り、テックリード的な人は補助線を引いて上げると良いかなと思います。
お手軽な手法として、 go install
を使う方法があります。例えば go 1.22rc2
をインストールする場合はこのページからバージョンをたどり、次のコマンドが用意されています。
$ go install golang.org/dl/go1.22rc2@latest |
これで go
コマンドのように go1.22rc2
コマンドを利用することができます。 go1.22rc2 run main.go
といった形でいち早く新バージョンを動かせ、かつ既存のGoのバージョンを変えなくても済むので便利です。
GoLandを使っている人はさらに楽ができます。
設定>Go>GOROOT>+記号>ダウンロードで、GoLand側でバージョンリストを出してくれ、指定したいバージョンをクリックするとダウンロードからIDE内でPATHまで諸々設定してくれます。GoLandのターミナルで実行する go
コマンドまで指定のバージョンを利用してくれるため便利です。
リリースノートからブログを書くと言いましたが、リリースノートの内容は簡略化した内容ですので、加えてGoDocやプロポーサルを確認して、背景や議論のやり取りを確認すると内容が深まります。
プロポーサル(Issue)の探し方ですが、一番簡単なのは、ページのHTMLを開き(Chromeであれば ctrl + U)、そこにコメントされているURLを取得する方法です。おそらくこの手法が最速です。
次のように range overが議論されていたIssueが見つかります。
それを開くとGitHub Issueに飛べると思います。
時には長い議論になっていることも多いですので、サマリにまとめるだけでも良いとっかかりな記事にしやすいと思います。
社内メンバーは次のようなポイントを拾うことが多いです。
辻さんのHTTP ResponseController記事では、互換性の立場からこっちのメソッドを拡張できずといった説明があります。ある断面だけだと不自然に思えることも、歴史的経緯を知ると理解しやすくなりますよね。
IssueからはコードレビューのURLがリンクされていることが多いので、どのようなファイルの修正があったかも確認できます。コードの修正内容まで踏み込めるとオリジナリティ溢れること間違いないです。
実際にサンプルコードを書いて動かすと理解が深まります。
もし、標準パッケージの追加/更新であればテストコードが存在するため、GoDocだけみて動かし方がわからない場合も、標準パッケージのテストコードを参考にするとよいです。それらをデバック実行してみて、どのような実装になっているか内部のコードを見ても面白いですね。
棚井さんは encoding/base32
をテーマに、Base32そのものの変換処理の流れを解説する記事を書いていました。こうした派生は面白いですね。
Bug Fix系であれば、新バージョンと旧バージョンで比較して動作検証してみても良いかもしれません。
また、性能が気になればベンチマークを取ってみるというのも、知見を増やしやすいポイントです。例えば、武田さんの記事では、標準のnet/httpとサードパーティ製ライブラリでベンチマークを取っていて、使えそうかそうじゃないかの1つの材料を提供しています。
私もnet/httpの記事では、CPUプロファイルや、strace
を用いて、システムコールの発行具合を調査した記事を書きました。Goは標準に様々なツールがあり分析できるので、引き出しを増やすという意味でも学びになります。
触ってみて得た知見や疑問点も、記事に書いたり周囲のGopherな人に雑談で話すと、ネタが広がることも多いので、記事を書く前にもどんどん発信してフィードバックをもらいましょう。
コードリーディングのサポートとしてGitHub Copilotをうまく活用しているよというメンバーもいます。
途中で難易度の高い処理にぶつかっても、エディタ上でCopilotに質問すれば、処理内容を解説して頂けます。
VSCodeプラグインもあります。
https://marketplace.visualstudio.com/items?itemName=GitHub.copilot
自分のレベルと興味にあったテーマの選定が最も難しく、決まればプロポーサルはリリースノートからすぐにたどり着けますので、あとはテストコードやデバック、ベンチマーク、プロファイルなどのツールを駆使していろいろ触ってみて得た知見を記事にしましょう、という内容でした。
リリースを記念に祝う目的でブログを書くと、良い感じの緩さ加減な期日感もあり機会としてはちょうどいいですし、適度に学びになります。
もっと良い方法があればXでぜひ教えてください。
]]>タイトルで言いたいことは言ってしまっているのですが、2017年ぐらいからNext.jsを使ってきて、最新の情報のキャッチアップとかもそんなに苦労はなくて、こだわりがないならNext.jsでいいのでは?という記事です。
フレームワークが大きいのはたいていそうで、提供されているすべての機能を使うわけではなく、その一部だけを使います。そのサブセット自体がシンプルであればフレームワークはどれだけ大きくても問題はないはずです。JavaとかPythonとかGoのコード書いてもごく一部のライブラリしか使わないわけで、でもそれに対して「ライブラリがでかすぎる」とは言わないですよね。
Next.jsは「より高速にする」機能がたくさんありますが、別にそんなの最初から使う必要はないですし、サービスによってはそもそもその機能が合わない、というのもあります。ユーザープロフィール画面にISGとかSSGは使わないでしょう。
最低限の部分を取り出せば、ファイルシステムベースのルーターがあって、SSRしてくれる、ぐらいでしょう。それ以外は素のReactをそのまま使う、という作戦でいいかと思います。データのフェッチもgetServerSideProps
とか使わずにuseEffect
とかuseSWR
で取得しちゃえばいい。そうなれば、ルーター周りのちょっとしたコードぐらいが差分で、あとはReactそのものになります。
静的なJSファイルを出したい、という場合はVite+Reactでも良いと思いますが、ルーターすらないので、「EasyよりもSimple!」派でないなら、ここもNext.jsでもいいかなって思っています。
Vercelを売るための機能が多い、みたいなのも見かけましたが、オプショナルな機能でいくつかあるだけかなって思います。ぱっと見つけたのはビデオファイルをVercel Blobにもおけるよ、とかOpenTelemetryとか。別に不要であれば使わなければ良い機能ぐらいかと思いますし。GoのPlan9対応みたいなものですよね。
Next.js 4の時代からするとコードの書き方は大きく変わりました。クラスコンポーネントが関数コンポーネントになってhooksが登場し、TypeScriptが当たり前になって、Reduxでがんばってconnectの定義を書いていたのが、今では嘘のようです。
とはいえ、これってNext.js側の変化ではなくて、Reactの変化ですよね。Server Actionsは派手なNext.jsの機能に見えて、これもReact本家の方の機能だったりします。
Next.js側も変化はありますが、機能追加はあっても機能の削減で困ることはいまのところなかったように思います。app routerの登場は大きかったですが、これはむしろ、React本家が導入したReact Server Componentを実現するためのNext.js側が用意した解、みたいな感じです。
ビルド周りとか設定周りは大きく改善されました。ビルドもかなり高速になりました。自前でかなり手を入れていた人には厳しいでしょうが、なるべく設定を変更せずに使う、というのが今時のウェブフロントエンドの渡世術かな、と思います。少ないコストでエコシステムの進化の恩恵を享受できるコツです。
Next.jsの一番の価値は、JS界のさまざまなライブラリとの繋ぎ込み方法がexamplesというフォルダにあって、動作検証されていることかなって思います。JS界隈はライブラリが多いので組み合わせではまったり、とかはあるのですが、すくなくともNext.jsとこのバージョンのこのライブラリならつながる、という情報があるのは大変ありがたいことです。
https://github.com/vercel/next.js/tree/canary/examples
それだけではなく、たとえば認証どうしよう、とか、実現したいことに対する情報がたくさんあって、それを支えるライブラリもたくさんあって・・・というところですね。ようするにエコシステム。仕事で使う場合に、何かトラブルがあってもすぐに解決したい!というのであれば、ないよりはあった方が良いものですよね。
Reactの開発は、それ自身に閉じるのではなく、サーバー側との連携機能などは外部のフレームワークともやっていくぞ!というのはReact Server Componentのときに語られています。Next.jsはReactフレームワークの中では旗手ではありますし、新機能が最初に使える環境となっています。React Compiler楽しみですよね?そういうReact側の機能を早く試してみたい人にはNext.jsが一番良いとも言えるかと思います。
強い意志を持ってAstro.js使いたい!とか、トラブルはむしろ大歓迎!みたいな人とか、趣味でやっているしむしろ人がやっていないことに価値がある!みたいに思っている人は、そちらを選べば良いと思います。
逆に、そういう強い思いがない人であれば、とりあえず朝くNext.jsを触るのがいいんじゃないかな、という記事でした。
]]>多くの企業では、セキュリティ対策の一環として認証プロキシを導入しています。これは、社内ネットワークからインターネットへのアクセスを制御し、不正なアクセスや情報漏洩を防ぐための重要な手段です。しかし、この認証プロキシを利用する際には、ユーザ名とパスワードの入力が必要であり、ソフトウェアごとにプロキシ設定する必要があります。これが、日々の業務において非常に煩わしい作業となっています。
今回は、この問題を解決するために、自分専用のローカルプロキシをセットアップし、認証プロキシの設定を一元管理する方法についてご紹介します。
認証プロキシを利用する際の主な課題は以下の通りです。
これらの課題を解決する方法として、手元のPCにローカルプロキシを立てて認証を肩代わりする方法をご紹介します。
ローカルプロキシをセットアップすることで、認証プロキシの設定を一元管理し、上記の課題を解決できます。多くのオープンソースソフトウェアが利用可能で、例として「Squid」や「CNTLM」、「stone」などが挙げられます。ここでは例として「mitmproxy」を取り上げます。具体的な手順は以下の通りです。
mitmproxy は Python プログラムです。インストール方法は複数あるので公式サイトを参照してください。
LinuxやWSL環境において最短でインストールするならパッケージリポジトリを(コミュニティ運営のため公式は積極的に推奨していませんが)使用して下記になります。
aptでインストール |
sudo apt update |
GitHub から clone して実行する手もあります。
Docker を使った簡単な導入方法もありますが起動方法込みになるので次章にて纏めて記述します。
導入形態によって変わりますので以下に列挙します。
※社内向け認証プロキシにありがちな独自証明書対策としてオプションに --ssl-insecure
を使用したいケースもあります。
mitmweb --mode upstream:http://[認証プロキシ]:[ポート] |
.\venv\Scripts\activate |
source ./venv/bin/activate |
docker run --rm -it -v ~/.mitmproxy:/home/mitmproxy/.mitmproxy -p 127.0.0.1:8081:8081 -p 8080:8080 mitmproxy/mitmproxy mitmweb --web-host 0.0.0.0 --mode upstream:http://[認証プロキシ]:[ポート] |
コンテナの /home/mitmproxy/.mitmproxy
には mitmproxy が生成する証明書が格納されます。run するたびに新たに生成されるとその都度証明書のインポートが必要になるのでこの例では永続化させていますが、一度 run した後はコンテナを使い回して start する運用とする場合にはこの考慮は不要です。
http://localhost:8081/ を開いて、 Options -> Edit Options -> upstream_auth に本来の認証情報を [user]:[pass]
形式で記入します。保存ボタンは無く、フォーカスを外せば設定完了です。
mitmproxy の起動コマンドに認証情報を書く事が気にならない方は、起動時のオプションに --upstream-auth [user]:[pass]
を加える事でこのステップは無視できます。またその場合は起動コマンドを mitmweb
ではなく mitmdump
にすればWeb UIが起動しないので省リソースです。
各ソフトウェアのプロキシ設定を、ローカルプロキシのアドレス(通常はlocalhost:8080
)に変更します。これにより、すべてのソフトウェアで共通のプロキシ設定を利用できるようになります。
HTTPS通信は mitmproxy を通る際に一旦復号され独自の証明書により再暗号化されるようになります。よって mitmproxy が使用する証明書のインポートが必要になります。ブラウザのプロキシを mitmproxy に設定した状態で http://mitm.it/ を開くと証明書をダウンロードする画面が開くので、プロキシのクライアントとなるOSごとに証明書のインポートを行ってください。
Windowsの場合は下記の手順になります。
他にも証明書の参照先が異なる物は対応が必要です。
WSL |
host.docker.internal
のようなホスト名も無いので名前解決方法を実装する必要がある)認証プロキシの設定に伴う煩雑さは、多くの企業で共通の課題です。ローカルプロキシを導入することで、これらの課題を効果的に解決し、よりスムーズで安全なネットワーク環境を構築できます。ただし、導入にあたってはセキュリティ対策やネットワークポリシーの確認が必要ですので、注意が必要です。
今回ご紹介した方法が、認証プロキシの煩わしさに悩む多くの方々にとって、有効な解決策となることを願います。
TIG真野です。
2023年末にAWS Lambda界隈で話題だった「AWS LambdaのGo 1.xランタイムのサポートが2023年12月31日で終了する」への対応を、あまりネットに無い特殊なやり方を採用して行ったので、考え方や実施メモを残します。
すでに大半のAWS LambdaのGoユーザの方は対応している時期かと思いますが、ご容赦ください。
この影響で、AWS Lambdaにおいて Go 1.x
のランタイムから al2023
などに変更し、zipで固めるバイナリ名も bootstrap
にする必要があります。
以下の記事が参考になります。
ちなみに、更新先は provided.al2
と provided.al2023
とで少し悩みましたが、より新しく保守期間も長い、provided.al2023
を選択しています。作業を年末のギリギリまで引っ張ったメリットかもしれません。AWSのドキュメントも気がつけば provided.al2023
推しに変わっていました。
この更新作業の内容自体は通常、 lambda
(今回のプロダクトで使っていたバイナリ名) を bootstrap
という名称に変えて、TerraformのLambdaリソースの設定値を書き換えておしまいであるため、さほど難しく無いでしょう。
しかし私が担当していたプロダクトでは、数十のLambdaが、Kinesis Data Streams、DynamoDB Streamsなどのイベント着火し、それもそれぞれが24/365で停止タイムがないというものです。
本来であればイベントソースマッピングを無効にし、その間にLambdaランタイムを更新し、イベントソースマッピング再び有効に戻すという手順が必要です。それをせず、直接アプリデプロイ or ランタイム更新すると、そのタイミングでリクエストが来た場合に処理が失敗し、データロストを誘発してしまいかねません。
しかし、上記の方法では以下の面倒臭さがありました。
幸い、対象のLambdaリソースを一括デプロイするための、Makefileをテンプレートベースで生成するツールが整えられていたため、これを改修して、make deploy-prod
するだけでLambdaランタイムのアップデートを行えるようにすることを目指しました。
今回思いついた手段ですが、 Go 1.x で動くlambda
と provided.al2023
で動く bootstrap
という2種類のバイナリをzipで同梱するという方法を取ることにしました。
手はずとしては、次のとおりです。
lambda
とうバイナリだけで動くlambda
, bootstrap
の2つのバイナリをデプロイするGo 1.x
のままであるlambda
のままであり、 bootstarp
は呼ばれないprovided.al2023
に更新するbootstrap
になり切り替わるlambda
というバイナリはなくして、 bootstrap
のみのzipに絞る。これは後々の対応で問題ないポイントとして、zipに2つのバイナリを同梱しちゃっても、Lambdaとしては問題なく動く(zip時の50MBサイズ上限はありますが、利用するハンドラ以外のバイナリを渡しても問題ありませんでした)。それにより、Lambdaランタイム更新をコマンドで行っても、上モノのzipには新旧両方のランタイムで動くバイナリが存在するため、ダウンタイム無しで切り替え可能になったということです。
今回は上記の方針を、Makefileで実施するようにしました。
ビルド、デプロイの流れは次のような流れです。
SHELL := /bin/bash |
Makefile中のコメント通りですが、いくつか補足します。
build
ターゲットgo build
で lambda
, bootstrap
の2種類のバイナリを作成します-tags lambda.norpc
は provided.al2
などで動かす場合にビルドサイズを下げることができるオプションです。このオプションを無視して、 go build
を1度だけ呼び出し、コピー+リネームで対応しても良かったかもしれませんzip
ターゲットlambda.zip
を作っていますtouch
でバイナリの最終更新日時を固定して、zipのコードハッシュが変化しないようにしていますdeploy-prod
ターゲットlambda.zip
をデプロイしています上記の切り替えは1度だけ動かせば、Lambdaランタイムが切り替わるので、その後は次のように記述を戻してOKです。
SHELL := /bin/bash |
作業手順としては make deploy-prod
を実行するだけ(※数十のLambdaリソースを一括デプロイするラッパーのようなツールがあったため)ですので、非常に楽でした。慣れた手順で、ダウンタイム無しで切り替えられるため安心感があり、この手順を採用して良かったと思いました。
なお本題ではないですが、Go 1.x からのランタイム切り替えに際して、次のコード書き換えが1点必要でした。
一部のコードで次のようにタイムゾーンを読み込んでいる処理がありました。
jst, _ = time.LoadLocation("Asia/Tokyo") |
これが次のようなエラーでてしまいました。
{"level":"error", |
provided.al2023
ですとタイムゾーン情報をファイルから取れないのですね。対応としては、利用しているタイムゾーンがJSTのみだったため、jst := time.FixedZone("Asia/Tokyo", 9*60*60)
と単純に書き換えてしのぎました。
この事象がなぜ発生したかは本記事のテーマとは少し外れるため、詳細は 辻さんの記事 などを参照ください。
それ以外、これといって課題は出ておらず安定しています。
切り替え作業ですが、慣れた手順をそのままで中身のみを拡張するような方式だと、作業中のプレッシャーが格段に減り手順書も作る必要がないほどでしたので非常に楽ができました。zipに2つのバイナリを同梱するのはトリッキーであり、あまり聞かないやり方な気がしますが、覚えておくと今後もなにかの役に立つかもしれません。
本当はamd64
から arm64
に切り替えたかったのですが、これについては持ち越し(今回の手順だと対応もできないです)となりました。その場合は、イベントマッピングを無効化するといった手順が必要そうで、少し大変だと感じています。良い切り替えアイデアは絶賛募集中です。
こんにちは。最近自宅チェアをバランスボールにして体幹を鍛えている、HealthCare Innovation Group(HIG)所属の山本です。
私用PCはLinux、会社PCはプロジェクトによってWindowsとMacのどちらかを使っている生活をしており、かつ自宅のモニターやキーボードは外付けで1つのものを使用しています。
その日々を過ごす中で、キーバインディングが異なるPCで混乱することがあり、ツール・操作感をできるだけ統一したいという願望がありました。
今回はMacを使用する際に、キーバインディングや操作感を私用PCに寄せるため「Hammerspoon」を使用した、個人的な環境構築集を書きます。
macOSの操作をLua言語で行うことができるツールです。
アプリケーション、ウィンドウ、マウス ポインター、クリップボードなどできることは多岐に及びます。
キーバインド操作ツールとしては「Karabiner-Elements」や画面分割では「Shiftit」など、類似の操作ができるアプリはいくつかあります。
これらのツールのようにGUIで設定できない代わりに、スクリプトをゴリゴリ書くことでかなり自由度の高い操作をできることが特徴です。
今回はキーバインディングの操作や、ウィンドウの操作に主に使用させていただきました。
今回の操作環境としては、上記のようなものです。
英字キーボードを外付けしている&複数の外部ディスプレイを接続している、ということが記事の背景となります。
Macを初めて使うユーザーのお悩みとして、以下のようなものがあるのではないでしょうか?
これらのうちの多くは、Macのキーバインディングやツールに慣れたり活用することで解決する、もしくはより効率的な操作できると思います。
思い…ますが、私はなるべくお手軽に単一のツールで解決し、かつ使用感はLinuxやWindowsに寄せたいです。
その頑固な意思が背景にありましたが、Hammerspoonであれば、Luaで設定ファイルを記載することで自在に管理できました。
今回紹介することはシンプルな3つの設定です。
1.キーバインディングの変更:
※上記キーの名称はキーボードの刻印見たまま。
2.ウィンドウ操作の変更:
3.パスの相互変換:
特に、Windowsユーザーから共有されるパスの相互変換機能は、メニューバーにアイコンを常駐させることで、操作によりクリップボード上のパスを変換する方法としました。これにより、\\ホスト名\フォルダ名
(Windows) と smb://ホスト名/フォルダ名
(Mac) の間での変換を簡単に行うことができます。
すべての変更をHammerspoonで管理しています!と言いたいところですが、試行錯誤の結果、自然な操作感とするためModifier Keysについては以下のようにスワップしています。
上記設定とHammerspoon側の設定を合わせることで、キー操作を定義する形です。
さて、ここからが本題です。実際にHammerspoonの設定例を紹介していきます。
Hammerspoonの設定ファイルは~/.hammerspoon/init.lua
に配置され、Lua言語で記載します。
また、メニューバー上からもOpen Config
にて開くことができる親切仕様です。
設定ファイルの記載内容としては、ドキュメントがかなり整備されており下記を参照することで一通り記載されています。
https://www.hammerspoon.org/docs/index.html
上記のドキュメントの中でも、今回紹介するものは、以下の4つです。
以降のセクションでは実際の実装例として紹介していきます。
※1つのファイルに記載すると煩雑であったため、init.lua
から各設定のLuaファイルを読み込むように分割して構成しています
https://www.hammerspoon.org/docs/hs.eventtap.html
キーバインディングについては、hs.eventtap
の章に記載されている各関数使用することで、入力キーの入れ替えなどの操作ができます。
今回はCtrl/Cmdキーの入れ替えをいい感じにしたかったので、以下のように実装しています。
標準設定でModifier Keysを一部入れ替えていることを前提に、スクリプトで一部操作を上書きするイメージですね。
local key_bindings = {} |
-- keyBindigsを有効化する |
ちょっぴり複雑な背景としては、ターミナル系アプリおよびエディタの操作時にはいくつか例外としたいものがあったためです。
他のOSから持ってきた設定ファイルをそのまま使いたかったことや、Ctrl + C
で処理を中止(SIGINTシグナルを送信)できるようにしておきたかったことがあります。
素朴なキー入れ替えだとCtrl + Spaceでの文字入力変換や、他OSで定義したVimのショートカットをそのまま使うことができませんでした。
上記の設定にすることで、他OSで設定した設定ファイルをそのまま持ち込めています。
ウィンドウ操作としては、Windowsのキーバインディングに寄せたいと思ってました。
画面の半分に移動したり、
最大化したり、
ディスプレイ間を移動したり、
などの操作ですね。Windowsではこれらはデフォルトのショートカットキーとなっていますが、Macでも再現をしたいといったことがモチベーションです。
https://www.hammerspoon.org/docs/hs.window.html#moveToUnit
Hammerspoonでは、hs.window
に記載されている各関数でウィンドウの操作が可能になっています。
今回は、以下のように実装しました。
local window_management = {} |
hs.hotkey.bind({"option"}, "Left", window_management.moveWindowLeft) |
上記の設定により、Windowsライクなウィンドウの移動・サイズ変更操作ができています。
最後に、パス変換の紹介です。
WindowsとMacのパス表記の間には、小さくそして大きな差異があることには度々苦しめられると思います。
区切り文字が違ったり、ファイルサーバーのパスが異なったりですね。
(\\ホスト名\フォルダ名
<-> smb://ホスト名/フォルダ名
のようなもの)
この変換については濁点の扱いであったりUTF-8の扱いなど闇が深い部分も多いので詳細は触れず、一部簡単にした例を紹介します。
パス変換の実施方法はいろいろあると思いますが、今回はメニューバーに変換を常駐させておき、操作によりクリップボード上のパスを変換する方法としました。
https://www.hammerspoon.org/docs/hs.menubar.html
Hammerspoonでは、hs.menubar
に記載された各関数を使用することで、Mac上のメニューバーにドロップダウンメニューを表示することができます。
https://www.hammerspoon.org/docs/hs.pasteboard.html
また、クリップボード操作についてはhs.pasteboard
に記載された各関数を使用することで実施できます。
今回は、メニュー上に表示した変換メニューを押下することで、クリップボードからテキストを取得して変換し、クリップボードに返すような挙動としました。
実装例としては、以下のようになります。
-- MacのファイルパスをWindows形式に変換する |
local path_converter = require "modules.path_converter" |
上記の設定により、メニューバーからパス変換をし、変換内容についてはダイアログ表示できました。
実行時のイメージは以下のようなものです。
クリップボードにパスをコピーした上で、Macのメニューバー上に作成したプルダウンから、作成した関数をトリガーすることで画像のようなアラートを表示する&クリップボードに変換後パスを保存できます。
Luaスクリプトをより煮詰めることで、さらにいろんなパスケースや文字列変換にも対応させることが可能です。
本記事では、Hammerspoonを導入して実現したMacのキーバインディング・ウィンドウ操作・クリップボードの文字列としてのパス変換の3つを紹介しました。
こういったHammerspoonの設定を通して、Macの操作にいまだに不慣れな私でも、個人的にはWindows/Linuxライクな操作ができるようになっています。
正直、今回記事で実施した操作は他のアプリの組み合わせでも実行できるとは思いますが、こういった設定はして暫く経つとどのツールを組み合わせているのか、どこに設定ファイルがあるのか、また記載内容が読めなくなっているのが私の常です。
そういった意味で、HammerspoonではLuaファイル一本で設定できるので振り返りやすく、また自由度も高いという意味で個人的には良かったです。
(NeovimのLuaファイル設定と同じ感覚でできました)
Macを使う、けど操作に慣れない…!スクリプトでゴリゴリいじりたい…!といった方におすすめしたいとおもいます。
こんにちは。最近スギ花粉耐性がないことを実感しつつある山本です。
2024/2/1、GDG Tokyo主催の「【Firebase】GDG Tokyo Monthly Online Tech Talks」に「Flutter×Firebaseサービス達で高速でモバイルアプリを開発した話」というタイトルで登壇してきたので、その事後レポートです。
登壇資料:
Google Developers Group (GDG) Tokyo は主にGoogleのテクノロジーに興味のある人たちで情報を共有しあう集いです
GDG Tokyoが毎月開催する「GDG Tokyo Monthly Online Tech Talks」は、Googleの技術に関心を持つ人々が集うオンラインMeetupです。このイベントは、Android、Google Cloud、Web、Firebase、Machine Learning(ML)、Flutter、Goなど、多様なGoogleの技術に焦点を当てています。参加者は、技術情報をキャッチアップし、エンジニア同士のコミュニケーションと交流の場としてご活用いただけます。
(※GDG Tokyoのconnpassページより引用)
「GDG Tokyo」および「GDG Tokyo Monthly Online Tech Talks」についてはGDG Tokyoのグループの説明として上記が記載されています。
今回のイベントのテーマは「Firebase」に関わることで、15分枠 or 5分枠の登壇枠で開催されていました。
最近お仕事でFirebaseに携わっていたこともあり、社内の人におすすめされたので登壇を申し込んでみました。(会社アカウントとしては初の登壇です…!)
登壇内容としては、新規事業プロジェクト周りでFlutterとFirebaseを活用していたのでそのユースケースの紹介をしてみました。
以下内容を一部抜粋して紹介していきます。
今回紹介したユースケースとしてはモバイルアプリ側はFlutter、バックエンドサービス側をFirebaseのサービスを活用したものです。
どちらもGoogle製ということもあり、親和性がある&ドキュメントが豊富ということでこのアーキテクチャが選定されています。
使用したサービスを一覧で並べてみると、Firebaseを主としたかなりモダンな構成と言えるのではないでしょうか?
自分は開発チームにあとから参画したのですが、なかなか良い経験となっています。
Firebaseでログ・アラート周りに使用できるサービスとしては以下の2つがあります。
構成としてはいろいろなものが考えられますが、今回はCrashlyticsについては主にクラッシュor致命的なエラーを通知するように、Google Analyticsにはユーザーログを出したりBigQueryに連携したりしてユーザーの操作の追跡や広告効果の測定といったことを行っていました。
※BigQueryへの連携については松井さんが以下記事で解説してます。
Firebaseで取得したログをBigQueryに連携してユーザー操作をトラッキングする
その他に活用していてユニークだったサービスとしては、Remote Configがあります。
https://firebase.google.com/docs/remote-config?hl=ja
詳しくは上記の公式ページを見ていただきたいですが、Remote Config Serverでパラメータ値などを管理することで、アプリストアへのリリースすることなくユーザーアプリの画面切り替えや強制アップデートなども行えます。
まだ導入できていませんが、A/B testingなども行うことができるようなので将来的には触ってみたいですね。
今回はGDG Tokyoにて、最近触ったFirebaseサービスとFlutterの活用事例について登壇してきました!
会社アカウントで登壇することは初めてだったのですが、登壇になれた方やチームメンバーに手厚くフォロー頂き楽しく発表してくることができました。
今後もどんどん登壇やアウトプットできるようにがんばります。
※スギ花粉とも頑張って生きていこうと思います。
TIG 真野です。
2023年3月31日にリリースされたLocalStack v2.0.0から、LocalStackのイメージ構成に変更が入りました。利用する環境によってはKinesis Data Streamsなど一部のサービスを利用するときにカスタムCA証明書をダウンロードする必要がありました(後述する通り、Kinesis Data Streamsに関しては現在のバージョンでは対応不要ですので安心ください)。
この記事では、DockerのマルチステージビルドでOpenSSLを使って証明書をダウンロードして、LocalStackのカスタムイメージを作成する流れをまとめます。
#8782のIssueを見つけて対応を考えている人や、installation of kinesis-mock failed
といったエラーログが出ていて困っている場合、おそらくこの記事が参考になります。
ただし、少なくてもKinesis Data Streamsに関しては、v2.3.0
からアップデートが入り本記事の対応が不要になりました。LocalStackのその他サービスでハマった場合にこの記事を確認いただくと良いかなと思います。エラーログでこの記事を見つけた方は、LocalStackのバージョンを上げることで解決することもあるようですので、まずバージョンアップを試してみることを推奨します。
LocalStackは様々なAWSサービスをローカルやCI環境で再現してくれるエミュレータです。こういったサービスの難しいポイントの1つは、AWSのサービスや機能はどんどん増え豊富になっていくため、追随するためにはイメージサイズが肥大化しいくことでしょう。
そのため、v2.0.0からは起動時に一度だけ外部からサービスが必要とするパッケージ読み込みキャッシュ。それにより、開発者の利用しないサービスが依存するパッケージは元のイメージから取り除き、容量削減を狙う方式になりました。
少しばかり複雑な手順を踏んでいる気がしますが、イメージサイズと利用勝手のバランスを取った賢いやり方に思えます。CIで利用するユーザにとってはイメージのpull時間の節約、しいては費用削減となるため嬉しい施策出ると思います。
一方でこれにより、DynammoDBなどでは色々と問題が多かったらしく、利用頻度が高いサービス(Issueではトップ15と書かれていますが今のところは数種類)が再びプリインストールする方向にするよという話も出ていました。
v3.1.0では、Dockerfileを見る限り、DynamoDBとLambdaはプリインストール方式に戻っていました(DynamoDBは(多分)DynamoDB LocalのJAR増加で、47MB程度イメージサイズが増えたようです)。
# Install packages which should be shipped by default |
Kinesis Data StreamsなどもDynamoDBと同じようにプリインストールできないかという要望も#8300で上げられましたが、やはりイメージサイズとのバランス問題で棄却されています。何かしらプリインストールしないと困るユースケースが無いと追加はされないような雰囲気があります。
LocalStackでKinesis Data Streamsのストリームを作成しようとした場合に、Installation of kinesis-mock failed
というエラーが出るケースについて話します。ログ内容としては次のようなものです。
localstack-1 | SERVICES variable is ignored if EAGER_SERVICE_LOADING=0. |
公式ドキュメントCustom TLS certificatesにも触れられています。非標準の TLS 証明書を使用するプロキシサーバーを利用する場合に発生するようです。原因はプロキシ、慣れたものです。
ドキュメントにDockerfileを拡張して証明書をインストールする手順があり、Installation of kinesis-mock failed in LocalStackの記事では、https://api.github.com
のCA 証明書にアクセスして取得する例が書かれています。
しかし、チームメンバー全員にこの手順を行ってもらうのは手間ですし、証明書をGit管理にもしたくないでしょう。ファイルサーバやGoogle Driveのようなコラボレーションツール上にも、こういった手順は廃れがちであるため、あまり配備したくない場合が多いでしょう。
サーバからTLS証明書をダウンロードするために、OpenSSLを利用します。
Get SSL Certificate from Server (Site URL) – Export & Download という記事が参考になります。
さきほどの https://api.github.com
から証明書を取得するのであれば次のようなコマンドです。
echo | openssl s_client -servername api.github.com -connect api.github.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > custom-ca.cer |
取得した custom-ca.cer
を公式ドキュメント通り、 CURL_CA_BUNDLE
、 REQUESTS_CA_BUNDLE
、NODE_EXTRA_CA_CERTS
の環境変数にセットしたイメージを作れば対応完了です。
Dockerfileのマルチステージビルドを利用すると次のようになると思います。
FROM alpine/openssl:3.1.3 AS build |
docker-compose経由で起動したいので、次のようなYAMLファイルを準備します。
version: "3.8" |
docker-compose up localstack
などで起動すると、Kinesis Data Streamsのリソース作成ができるようになっていると思います。3つのほどストリームを作成してみたときのログです。
localstack-1 | |
動的にパッケージを取得し、Kinesisのストリームが上手く作成されていることが分かります。
この記事を書くにあたり、元のエラーログを発生させようとしていて気がついたのですが、v2.3.0
でアップデートが入ったようで、Kinesis Data Streamsについては対応が不要です。
v2.0.2
→✘ エラー発生v2.1.0
→✘v2.2.0
→✘v2.3.0
→✅ 正常動作v3.0.0
→✅v3.1.0
→✅おそらく、Kinesisのモックをバイナリからscala.js版に入れ替えた Use scala.js for executable and docker image #531 で解消されたのかなと予測しますが、詳細は未調査です。
LocalStack v2からイメージの構成が変わって、起動時に動的にパッケージをインストールするケースがあります。その場合にネットワーク環境によっては外部リソースの取得に失敗するため、CA証明書の設定が必要。OpenSSLで自動化すると楽になるかもしれない、という記事でした。
みんなハマっていないのかな?と思っていましたが、NGだった期間は v2.0.0
が公開されたときから、v2.3.0
が公開された 2023.3.31
~ 2023.9.29
と半年足らずだったので、レアな経験だったのかもしれません。
2024年に計画している、ブログリレーの企画スケジュールをを取り上げます。
2024年にはじめて開催する連載は初心者マーク(🔰)を付けてみました。
フューチャーで最も利用頻度が高い重要技術要素である、JavaやVue.jsを今回始めてテーマに加えたことが変わったポイントです。実はフューチャーはJavaやVue.jsの会社でもあります。
アドカレリバイバルは、QiitaアドベントカレンダーでQiita側に書かれているか、フューチャー技術ブログ側で書かれているかたまに混乱することがあるという意見を某メンバーからもらい、せっかくだからリバイバルと称してアップデートしてこちらのブログに転記してもらったらどうか?ということで企画しました。
Month | Title | Memo | Link |
---|---|---|---|
1月 | - | - | |
2月 | Go 1.22 | Go 1.22リリース記念 | Go1.22 |
3月 | Terraform | Terraform全般をテーマ | 2024 2023 |
4月 | 春の入門祭り | 初心者向けに入門記事を書いてみよう | 2024 2023 2022、2021 2020 |
5月 | 🔰Cloudflare | Cloudflare | 2024 |
6月 | 🔰アドカレリバイバル | 過去のアドベントカレンダー記事をリバイバル | 2024 |
7月 | Go1.23 | Go 1.23リリース記念 | Go1.23 |
8月 | 夏休み自由研究 | 夏休みに行った自由研究と銘うって、個人的に関心があることを深堀りして調べる連載 | 2024 2023 2022 2021 2020 |
🔰Python | Pythonリリース記念 | 2024 | |
9月 | 🔰Java | Java23記念/Java全般 | 2024 |
10月 | 秋のブログ週間 | 秋の夜長に楽しめるように、いつもより文章が多めな読み物 | 2024 2023 2022 2021 2020 |
11月 | 🔰Vue.js | Vue.js全般 | 2024 |
12月 | アドベントカレンダー | Qiitaさんのアドベントカレンダーのイベントに乗っかる連載 | 2024 2023 2022 2021 2020 2019 2018 2017 2016 2015 |
新しい風を入れつつ、春・夏・秋などの定番企画は引き続き開催しようと思っています。
これまでの経緯を踏まえテーマをブラッシュアップしています。2024年も皆様にとって有益な情報発信を活発にしていきますので、応援よろしくおねがいします!
]]>本記事では、負荷テストツールであるLocustとGoogle Kubernetes Engine(GKE)と組み合わせて負荷テストを体感します。Kubernetesの柔軟なスケールアップ・ダウン能力によって、負荷の大きさを変えながらテストを行うことが可能となります。
参考:Google Kubernetes Engineを使用した負荷分散テスト
LocustはPythonベースで書かれたオープンソースの負荷テストツールとなります。
GitHub: https://github.com/locustio/locust
公式ページにも載っておりますが、特徴としては以下の3つになります。
では早速、検証を行うための環境を構築していきます。必要なものは以下です。
インフラ側は全てTerraformを利用して構築しようと思います。
※ProjectやVPCの構築、gcloud、kubectlのインストールは割愛します。
GKEのTerraformコードは量が多いため、以下のリポジトリに配置しました。
https://github.com/bigface0202/terraform-useful-modules/tree/main/google-cloud
また、GKEを利用した検証に関するTIPSになりますが、GKEは立ち上げるまでに約20分少々かかるため、一番最初にGKEを構築しておくと検証がスムーズになります。
resource "google_app_engine_application" "app" { |
resource "google_artifact_registry_repository" "my-repo" { |
ここからはターミナルでの作業がメインとなるため、頻繁に利用する定数などを定義しておきます。適宜、自身で定義した内容に変更してください。
export PROJECT=test-project |
AppEngineにデプロイするアプリケーションはGoogle Cloudが提供するサンプルアプリを利用します。
# Clone the repository |
App Engineへのデプロイが完了後、表示されたURLへ移動すると以下のような画面が表示されます。
次にGKEにLocustと負荷テスト用のタスクをデプロイしたいので、まずはLocustのイメージをビルドします。
各エンドポイント/login
と/metrics
に対して、1:999の割合で呼び出すようなタスクが定義されております。詳細は以下を参照してください。
docker-image/locust-tasks/tasks.py
gcloud builds submit \ |
Cloud Buildを利用することでイメージのビルドとプッシュがgcloudコマンド1回でできるので便利ですね。
イメージがちゃんとビルドできているかどうかをコンソールから確認してみましょう。
イメージのビルドができたのでデプロイしていきます。
# Get cluster's credential |
初めてenvsubst
コマンドを知ったのですが、ターミナル上で定義済みの環境変数を代入出来て便利ですね。
無事にLocustをデプロイすることができたので、ポートフォワードして画面に接続してみます。
kubectl port-forward svc/locust-master-web -n default 8080:8089 |
http://127.0.0.1:8080/
にアクセスして、以下の画面が表示されることを確認します。
“Number of users”は負荷テストに利用するユーザーの同時接続数、”Spawn rate”は1秒当たりに何人のユーザーがリクエストを開始するかの数、Hostは接続先になります。
“Start swarming”を押すことでテストが開始されます。
以下の条件でテストを開始したときの画面が次のようになります。
右上のSTATUSの部分では現在接続中のユーザー数(5 Users)が表示されており、10Usersまで増えていきます。RPSはRequest Per Secondで、秒間のリクエスト数を表しております。
また、各種タブを切り替えることでテストに関する情報を見ることができます。
シンプルなUIの作りになっているため、直観的でわかりやすいです。
また、”Download Data”にてレポートを出力することができるのですが、テスト結果に対して自動でサマリした状態で出力してくれるので非常に便利です。
また、かなり大きめの負荷をかけたい場合は、Podの数を増やすことで対応可能です。Kubernetesならではですね。
Podを増やしたい場合は以下のコマンドで増やします。
kubectl scale deployment/locust-worker --replicas=10 |
本記事では、GKEとLocustを利用した分散型の負荷テストをハンズオン形式で紹介させていただきました。
Locustはシンプルな作りになっているため、特別なキャッチアップも必要なくサクッと使うことができます。
もしアプリのローンチを計画している方は、ローンチ前にLocustを利用した負荷テストをやってみてはいかがでしょうか?
]]>こちらにその講演の詳細なレポートがこちらにあります。
https://www.famitsu.com/news/202009/11205564.html
その8の発売前に龍が如くスタジオの技術責任者の方がXのアカウントを開設して、C++のコードを投稿されていたのですが、それに対してエンプラ開発目線で意見しているようなツイートを見かけて、「いや、システムの特性全然違うから」と思い筆を取った次第です。
大学時代、アジャイルソフトウェア開発というかエクストリームプログラミング(XP)が日本に来た時に、僕はその本に熱狂しました。こんな開発がしてみたいと。そして自分が社会人になるころにはアジャイル開発できる会社が増えたらいいなと日本XPユーザーグループの運営委員に入ったり本を書いたり翻訳したりしたわけです。ですが、学生の身分では、実際お客さんのいる開発などはできなく、できることといえば、テストとリファクタリングのコーディング周りのプラクティスぐらいだったので、ユニットテスト周りで自分でテスティングフレームワークを作ったりしてました。
そんなときに、サークルの先輩から言われたことがひとことがありました。
「テストファーストプログラミングを見て見たけど、研究のプログラムには使えなさそうだよね」
先輩がやられていたのはニューラルネットワークか何かだったか詳細は忘れてしまいましたが、確かに、1つ1つのニューロンの動きはテストできたとしても、システム全体だと荒すぎます。精度が70%以上みたいなユニットテストはナンセンスです。あくまでも入力に対して出力もすぱっと決まる、小さいモジュールの集合体としてプログラムが作れる場合にしかテストファーストプログラミングは適用できません。そんでもって、内部のロジックを大幅に書き換えました!というときにはテストも結局捨てて作り直すことが多くなります。業務システムにありがちな「内部のロジックは変わってもインタフェースは保とう」という方針が取れるのはかなり限定的な状況といえます。
一方、テストファーストプログラミング、現代的な言葉で言うと(といってもこっちももう20年以上歴史がある)テスト駆動開発が対象としているのは業務システムです。業務システムは状態を持ちません。
「データベースがあるじゃないか」と思われるかもしれませんが、業務的なシステムからするとデータベースはあくまでも外部システムです。業務システムのユニットテストは、DB含め、観測可能な外部の状態を固定し(Arrange)、中のロジックを実行し(Act)、結果を検証する(Assertion)というのがとても浅いシステムということになります。データベースが外部の状態ということはシステム自身は状態を持たず、イミュータブルであるとみなせます。
Wikipediaより引用
一方でゲームのコードというのはどういうものかというと、エンプラ系のシステムと比較すると大きな特徴は2つあるかと思います
1つ目は、ゲーム開発で一番使うツールはExcelと言われるぐらい、データ中心ということです。エンプラ開発でもマスターデータはありますが、キャラクターの絵やモデル、アニメーション、音は専用のツールで作りますが、それをどのタイミングでどう使うか、ユーザーが探索するマップと倒す敵の組み合わせとか出現頻度、アニメーション、セリフなど、ユーザーが触れる体験のほとんどはデータで駆動されます。
プログラムだけ単体テストしてもあまり意味はなく、データとの組み合わせで作品ができてきます。データ側に不具合というのありえます。データ単体もバリデーションでチェックをしたりも行われたりはしますが、データとプログラムを組み合わせて初めて発現するエラーも当然あります。Rustとかの型チェックの強い言語を使って言語側だけ強化しても限界があります。
あとは積分ですね。プログラムはフレーム単位でちょっとずつ変数を加工していくのですが、どんなに単体テストで1フレームの処理を「正しいだろう」と検証しても、プレイしつづけるとおかしくなったりします。物理エンジンで、ぶつかり方によってすごい勢いでふっとんでいくのをゲームで実感したことがある人はいると思います。
フレーム単位の小さいテストを書いても全体は見えないし、10秒間600フレーム回した後にテスト、みたいなテストを書いても、結局問題の発見には程遠い品質の低いテストにしかなりません。問題発見に2時間回し続けるバグがあったとして、他の全部のテストケースのループ数を7200秒x60フレームにするのか、というとそんなことはしないですよね。自動テストの限界として、一度作られてパスしたテストは、その後新しいバグを発見することは少ないというものがあります。時間をかければかけるほどリターンは小さくなります。研究の単体テストがうまく書けないのと同じ感じかと思います。
公開された情報からの推測でしかないですが、あえて別の言い方をすると、「複数のテストケースを並列実行する耐久E2Eテスト」なんじゃないかと思います。ビルド周りとの連携や、チケット管理システムへの自動起票などCI/CDとの連携周りがここ最近では強化されていそうですが、コア部分を見てみると、おそらくC/C++時代にはよくお世話になった(ユーザーがMFCとかでよくはまったと思われる)ASSERTが活躍しているのではないかと思います。
スライドにもゼロ除算の例がありました。これは単純にクラッシュする例ですが、マップでは入れてはいけない建物の中にすり抜けて入ってしまった!とかはおそらく地面がないので奈落に落ちることになると思うのでZ座標がマップ中に存在する最低点よりも低いというASSERTにできると思います。変な状態を検知したらクラッシュするようなASSERTを大量に埋め込んだプログラムにしておくことで、1つの「歩く」というテストケースの中に、たくさんのテスト条件を同居させているということなのかな、と。
もちろん、「期待した目的が達成できたかどうか」というのを表現する上では大事です。「A地点に行け」といったら、数秒以内にその地点に付くはずだ、というものです。おそらくこれはPythonで書かれているというテストケースで、このPythonのテストコードとC++レイヤーのASSERTの組み合わせで、状態が積分されていく&データ駆動という、業務システムとは毛色の異なるシステムの検証が行われているのではないかと想像しています。
テスト技法は、爆発する入力の組み合わせを減らして少ないケース数で効率よくテストを行う手法と言い換えられます。境界値テストは、同じ結果になる範囲のテストケースをいくら増やしても利得は少ないよね、じゃあ減らそうとか。悪く言えば怒られない程度に手を抜く手法と言えます。
ゲームの状態の組み合わせ数は業務システムの比ではなく膨大なので、それに対応した方法になっているのではないかと思います。業務システムが扱うテストケース数なんて、それと比べたらたかが知れてますよね。
システムの特性が大きく変われば求められるテストの性質も大きく変わります。ゲームにはゲームならではの事情はあります。基本的にはそのままこの手法を取り込む必要のない業務システムが多いのではと思います。
ただ、ASSERTを活用(しているというのは僕の予想でしかないのですが)というのはいろいろ応用できそうな気がしています、業務システムのステートフルなコンポーネントのテストとか、研究用のプログラムとか、そういうところに応用が効くのではないかと思います。数理最適化案件なんかはゲームとかとだいぶ近そうですね。AIとかもいけるかもしれん。例えば、キャッシュを持つシステムで、キャッシュがあたたまった状態で実行したときのパフォーマンスが期待値よりも上になるはず、みたいなコードは極めてステートフルといえます。HPをMAX100として、キャッシュヒットしたらHPが回復、ヒットしなかったらダメージを受けて、HPがゼロになったらエラー、みたいなのとか良さそうです。今でもスロークエリーでエラーログを出すシステムとかは多いと思いますが、同一ユーザーに対して連続で起きなければOKとか、遅さ加減を見て、すごく遅ければ一発KOとかそういうのもありな気がしますね。あんまり厳しい条件で1つでも出たらエラーとか出しすぎてもオオカミ少年になりそうですし、そういうちょっと踏ん張るテストケースは面白そうです。
龍が如くスタジオは、完全新作タイトルである7外伝を11月末、8を1月末と、通常あり得ない2ヶ月スパンで発売するという離れ技をやってのけたので、今年のCEDECではまたすごいテストの発表があるのではないかと期待しています。
]]>The Gopher character is based on the Go mascot designed by Renée French
TIG 真野です。Go1.22連載の8本目です。
Go 1.22 ライブラリのマイナーアップデートである net
, net/http
, net/netip
を取り上げます。
TCPConn
からUnixConn
へのio.Copy()
で、Linux’s splice(2)システムコールが使われ性能改善 #58808-tags=netgo
付きでビルドすると、DNSクエリの前に%SystemRoot%\System32\drivers\etc\hosts
から検索するようになる [#57757]https://github.com/golang/go/issues/57757)ServeFileFS()
, FileServerFS()
, NewFileTransportFS()
が新規追加 #51971Content-Length
ヘッダを拒否するようになった #61679Request.PathValue()
が新規追加 #61410AddrPort.Compare()
が新規追加 #61642前提知識となる、 splice(2)
ですが、入力用と出力用のファイルディスクリプタを繋ぎ、カーネル空間とユーザー空間でのデータコピーを行わず(ゼロコピーと言われる所以です)、データ転送を行うシステムコールです。(2)
の 2
は引数ではなく、システムコールを指す番号です。
例えば静的ファイルをホストしているGoのHTTPサーバを構築するとします。極めて素朴に実装すると、ファイルの要求に対して、os.Open()
でファイルを開き、 io.ReadAll()
で[]byte
を取得し、http.ResponseWriter
に Write()
で実現できます(※実際は http.FileServer()
を使うでしょうが)。このとき io.ReadAll()
するとカーネル空間から、ユーザー空間にデータコピーが行われます。また、読み取った値を Write()
で書き込みHTTP応答する際に、再びユーザー空間からカーネル空間にデータコピーが行われます。
これを splice(2)
を用いて、ユーザー空間にメモリコピーせず、カーネル空間上に閉じてやり取りをさせたいよね、というのは背景となるモチベーションです。順序的には pipe(2)
のシステムコールを呼び、次に左のsplice(2)
でパイプに書き込み、最後に右側のsplice(2)
を呼びパイプから読み取りネットワークインターフェースに書き込ませます。
splice(2)
を利用するためには、2つのファイルディスクリプタのうち、1つがパイプである必要があるそうです。そのため pipe(2)
を呼び出しています。パイプにコピーしているからゼロコピーじゃないじゃん!って思いましたが、多分カーネル空間に閉じていればノーカンなんだと思います。多分。
参考:
Go1.21以前のステータスでは、以下のケースは splice(2)
を用いてゼロコピーになるように io.Copy()
が実装されていました。
先ほど例に上げた静的ファイルをHTTP応答で返すケースは、ファイル→TCPソケットで対応済み、例えば、http.FileServer()
は内部で io.Copy()
を使っているのですでに最適化されています。
#58808
ではこの対応を以下の2つにも広げようというものです。
そレを実現するため、Go1.22では、net.TCPConn
と os.File
に WriteTo(io.Writer)
を追加されました。それらの内部で、 splice(2)
や sendfile(2)
を可能であれば利用する実装になっています。
io.Copy() ですが、引数に io.Writer
, io.Reader
を取りますが、 GoDocにも書かれている通り io.WriterTo
が実装されていれば src.WriteTo(dst)
が、io.ReaderFrom
が実装されていれば dst.ReadFrom(src)
が呼ばれます。io.WriterTo
で条件が揃えばシステムコールのsendfile(2)
や splice(2)
を呼び、無理であれば genericWriteTo()
というio.Writer
とio.Reader
をfor分でループさせて転送する処理にフォールバックします。
例として、ファイル→Unixドメインソケットにデータをコピーし、どのように呼び出し階層が変わるか go tool
で可視化します。
まずはサーバ側の実装です。こちらはUnixドメインソケットに書き込まれた内容を標準出力するだけで、今回は特に何もしません。
package main |
続いてUnixドメインソケットのクライアント側です。
メソッド呼び出しのコールグラフを作りたかったので、ベンチマーク形式で作っています。
package unixdomainclient |
これを実行し、可視化します。
$ go test -v -cpuprofile cpu.prof -memprofile mem.prof -bench . |
そうすると、io.Copy()
から システムコール sendfile(2)
が呼ばれているのが分かります。
比較のため、Go1.21.7 で動かしてみます。
$ go version |
そうすると、今度は sendfile(2)
ではなく read(2)
が呼ばれていることが分かります。
今回は go tool
でシステムコールがどのような流れ呼び出されているか確認しました。
Goならわかるシステムプログラミング 第5回 Goから見たシステムコール に書かれている通り、 strace
を見て確認するのも良いかと思います。
先ほどとほぼ類似の main.go
を作ります。
package main |
stace
でシステムコールの発行状況を確認します。 Go1.22
の場合は sendfile(2)
を利用しています。
$ go version |
Go1.21
の場合は read(2)
, write(2)
を用いていることが分かります。
$ go version |
私の業務範囲だとUnixドメインソケットを使う場面はあまり考えられないのですが、Linuxの機能を上手く活用した改善が入るのは嬉しいですね。
-tags=netgo
をつけてビルドした場合、Windowsで %SystemRoot%\System32\drivers\etc\hosts
のhosts
を参照しない不備があったようです。TODO が残っていたとのこと。
netgo
ってなんだ?という方も多いかと思いますが、golangの名前解決について - okzkメモに説明されている通り、GoではDNS名前解決の方法が2種類あり、pure Go実装版を利用するためには、 CGO_ENABLED=0
か -tags=netgo
を付けてビルドする必要があります。
今回はpure Go版かつWindowsで hosts
ファイルを見る実装が漏れていたので修正したということです。Windowsサーバ上もそうですが、GoでCLIツールを開発して展開している人なんかは、ちゃんと hosts
を見るようになって嬉しいかもしれませんね。
net/http
には ServeFile()、ServeContent() など静的ファイルをホストするような便利関数が存在します。しかし、これらは io/fs
パッケージが登場したGo 1.16 以前に開発されていたもので、互換性のため io.FS
で動作する版を追加しようという提案です。
サーバ側には2つ追加されました。
func ServeFileFS(w ResponseWriter, r *Request, fsys fs.FS, name string) |
クライアント側には1つ追加されました。
func NewFileTransportFS(fsys fs.FS) RoundTripper |
http.NewFileTransportFS()
を取り上げます。
最初に検証用のファイルを作成します。
echo -e "test\ntest\ntest" > ~/example.txt |
続いて RegisterProtocol()
で file
スキーマに http.NewFileTransportFS()
登録します。アクセス先は先ほど作成した example.txt
です。
package main |
そうすると実行結果は次のように、先ほど作成した example.txt
の結果が表示されます。
test |
従来ですと次のように FS
がついていない http.NewFileTransport()
を使っていました。
package main |
挙動としては同じですし、deprecatedという訳でもないですが、今後は FS
がついている方を利用する方が拡張性などの観点で良いでしょう。
Content-Length: 0
ではなく、 Content-Length:
をサーバにHTTPリクエストした場合、従来ですと200が返ってきた(400ではない)ことが、RFC 9110 のセクション 8.6などに反しているということで、修正されました。この拒否する挙動は、ApacheやNginxと同じらしく、影響を受けるユーザーはほぼゼロだろうということも話されていました。
..にも関わらず、従来の挙動で動かしたい場合 GODEBUG
に httplaxcontentlength=1
を設定すれば切り戻し可能だそうです。芸が細かい..!!
Go1.22リリースの目玉機能の1つで、HTTPサーバのルーティングが大幅に強化されました。
武田さんのGo1.22 リリース連載 HTTPルーティングの強化 を参照ください。機能面の紹介だけではなく性能面もベンチマークを取っており、参考になります。
func (p AddrPort) Compare(p2 AddrPort) int
が追加され、AddrPort
の比較ができるようになりました。 time.Compare()
などと同様、 p<p2 → -1
、p=p2 → 0
、ip>ip2 → 1
を返します。
背景としては、Go1.21で追加された slices.SortFunc()
と組み合わせたいとのことです。
slices.SortFunc()
について → Go1.21:slicesパッケージのチートシートどのような感じになるか slices.SortFunc()
に netip.Compare()
を組み合わせてみます。
package main |
実行すると次のような並び順になります。まずIP部分でソート、その後ポート番号でソートといった形で、直感的だと思います。IPv4とIPv6を混ぜた場合は、v4→v6の順になるようです。
1.2.3.4:80 |
ちなみに、元のIssueでは netip.Prefix
にも Compare()
を追加しようという提案でしたが、次回以降に持ち越しとなりました。理由として 10.0.0.0/8
→ 0.0.0.0/32
のような違和感ある並び順となる実装になっていたようで、既存の標準順序があればそれに合わせようということで、取り下げられました。
Compare()
1つ追加するにしても、どのような順序が一般的か(慣習に乗っ取っていて、利用者の驚きが最小化されるか)、周辺知識も深めていかないと駄目だなと感じました。
Go1.22のnet
, net/http
, net/netip
の3パッケージのアップデートについて取り上げました。
最近、低レイヤーについてどこまで抑えておくべきか、といった言説をXで見かけた気がしており、私が新人のときの研修リーダー的な先輩に、「自分の業務で用いる1つ下まで抑えるべき。2つ下まで深掘りできたら相当差がつく、凄い」と言われたことを思い出しました。
リリースノートの内容も、「Goならわかるシステムプログラミング」を理解していればもっと解像度が高く理解できていたなと反省/痛感しています。引き続き理解できる範囲も広げていこうと思います。最後まで読んでいただき、ありがとうございます。
]]>たとえば、ウェブブラウザのJavaScriptから呼べるalert()
やconfirm()
は、関数を呼び出せばダイアログが表示されますし、ダイアログが閉じたら処理が戻ってきます。confirm()
ならユーザーが選択したものと一緒に返ってきます。標準の<dialog>
タグが今時ですが、このタグはDOMインスタンスのshowModal()
やshow()
メソッドを呼ぶ必要があります。命令志向ですね。
一方、Reactでダイアログを実装する場合を考えます。メソッド呼び出しが直接扱えればシンプルですが、Reactでは基本的にステート管理でやりましょう、というのが流儀です。useImperativeHandle()
を使うとか、forwardRef()
を使うとか、いろいろ手はありますが、できることならrefは複雑化して利用者が動きを理解するのが難しくなりがちなので、呼び元でref
を使わなくても良い方法を考えました。
親コンポーネントの立場で見れば、関心があることは次の2つです。
この操作のためにrefで子コンポーネントの参照を取得するとか、ダイアログの開閉状態の管理をする、というのは本来やりたくない仕事のはずです。
一方で子供の方を見て見ましょう。
これは違和感はないと思いますが、Reactでは複数のコンポーネントが関心を持つステートは、共通の先祖かそれよりも上位のコンポーネントが持つことになります。これはReduxとかJotaiとかRecoilを使ってもそうです。親は子ダイアログを開きたいので、開閉ステートの管理は親が持つことになります。
そうなると親側でステートをuseState()
で作成し、それを変更したりというのも必要になりますが、そういうのはカスタムフックでまとめれば良さそうです。
以前のブログ記事でも紹介したようなDFD風の図で、親の関心が最小になるような構成を考えて見ました。
ダイアログの開閉状態はカスタムフックの中に閉じ込められたので、親コンポーネントと子コンポーネントはそれぞれ必要最低限の関心ごとにのみ触れれば良い状況がつくれそうです。
useOpener
というカスタムフックを作ってみます。呼び出しもとのイメージはこんな感じです。変更はカスタムフックに渡すコールバックで受け取ります。今回は確認ダイアログなので、booleanの値を受け取っていますが、ここは呼び出すダイアログによってはテキストかもしれないし、はジェネリクスの型パラメータにしたいですね。
ダイアログを開くボタンに渡すコールバックや、子コンポーネントで必要な情報一式がカスタムフックのレスポンスには含まれています。これをこのまま子コンポーネントに渡します。
import { useOpener } from "./opener" |
カスタムフックは次のような実装です。
import { useCallback, useState } from "react" |
最後にダイアログの実装です。カスタムフックの情報からダイアログのオープンが必要であれば<dialog>
のshowModal()
を呼び出してモーダルを開きます。ダイアログ操作でダイアログを閉じた場合は<dialog>
を閉じつつ、再度呼べるようにカスタムフックのステートを閉じるに設定します。また、カスタムフック作成時に渡されたコールバックを呼びます。
カスタムフックを媒介させることで、親と子の結合はだいぶ弱くできました。すくなくとも、内部実装を知らないと使いにくいref
のようなものを親コンポーネントから除外できたのは大きいでしょう。
import { useRef, useEffect, useCallback } from "react" |
実際に表示してみたのが次のものになります。daisyUIを使っています。
今回のカスタムフック本体は単にbooleanの開閉状態を持っているだけでした。つまり、子コンポーネントはダイアログ以外にも、ドロワーやアラートなんかの表示にも使えます。
実際にアラート表示としてそのまま使ってみましょう。アラートは表示されたら勝手に消えるものなので、終了のコールバックを受ける必要はありません。
import { useOpener } from "./opener" |
実装してみたアラートがこんな感じです。表示されたらタイマーで5秒後にクローズしています。
import { useEffect, ReactNode } from "react" |
かんたんですね。
Reactは状態管理を複雑にしようとおもえば結構複雑にできてしまいますが、それぞれのコンポーネントで必要な関心ごとはどれか、というのを考えて、それらのみに触れれば良い状況を作ることで、かなりシンプルにできます。パズルみたいで楽しいですよね。スクリーンリーダー等を考えれば、ダイアログはネイティブなタグの<dialog>
を使うべきですが、このAPIが命令的でReactとの相性が良くない(コードが長くなりがち)というのも回避できました。
状態を整理してお互いの依存のないカスタムフックにできたので、当初の予定のダイアログ以外のアラートにも応用ができました。
こんにちは。CSIG 所属の棚井です。
タイトルの内容が気になる方は、先に「こちら」をご覧ください。
本ブログは、Go 1.22 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.22 リリース連載」7本目の記事です。
Go 1.22 では、「澁川さんの記事」や「こちらの記事」にて取り上げられているように for ループのアップデート が入りました。アップデート分のうち、range over integer
と range over function
はこちらの issue(spec: add range over int, range over func #61405)に Proposal が記載されています。
本記事ではこのうち、range over integer
にフォーカスして取り上げていきます。
Go の言語仕様が記載された For statements with range clause にて、for-range ループ
は以下のように説明されています。
A “for” statement with a “range” clause iterates through all entries of an array, slice, string or map, values received on a channel, or integer values from zero to an upper limit [Go 1.22].
Range expression
に渡す値のデータ型により、ループ変数にどのような値が渡されるのかが整理されています。例えば、Range
に渡したデータ型が配列やスライスであれば、1st value と 2nd value に渡される値は「int型のインデックス、インデックスに対応する値」であり、マップ型であれば「キー、キーに対応する値」という言語仕様が説明されています。
Range expression 1st value 2nd value |
Range の説明に see below と記載された項目のうち、integer については以下の説明が Go 1.22 から追記されています。
- For an integer value n, the iteration values 0 through n-1 are produced in increasing order. If n <= 0, the loop does not run any iterations.
range over integer
の挙動としては
n
が integer型の場合はとの説明があります。
実際に Go 1.22(release-branch.go1.22)を動かしてみると、仕様通りの挙動が得られました。
package main |
package main |
Range expression
に自然数を渡した場合は、0からその数分だけ+1インクリメントしながらループが繰り返されていること、また、0以下の整数を渡した場合にはループ自体がスキップされていることが分かります。
Go 1.21 以前に「指定の回数だけループを繰り返したい」場合には以下のような「C言語から続く伝統的なループ処理」で実装する必要がありました。
package main |
「伝統的なループ処理」のコードレビューでは「あー、for で回数指定のループね。ここの処理は配列やマップに依存していないから、拡張for文は使えないのか。ループの開始値は x で、終了条件は x < y だから、ループ回数は z になる。よし、テストコード側も確認しよう。」のように「境界条件を脳内でイメージした上、ループ回数を演算して、妥当性を確認する」必要がありました。今後は「伝統的なループの記述が不要」になりましたので、Go 1.22 からは少しだけレビューが楽になりました。予めループの回数が決まっている処理は range over integer
で書き換えられると思いますので、発見次第リファクタリングの Pull Request を作成してみてはいかがでしょうか。
Go 1.22 連載での「渋川さんの記事」にある C言語からの伝統のループ とのワードを読み
との感情が湧き出てきました。
さらに、ちょうど今読み進めている「達人プログラマー」の「達人の哲学」には
毎年少なくとも一つの言語を習得する。
言語が異なると、同じ問題でも違った解決方法が採用されています。つまり、いくつかの異なったアプローチを学習することにより、幅広い思考が可能になるわけです。
と書かれていることにインスパイアされましたので、
30種類のプログラミング言語で、同じ出力が得られるループ処理
を書いてみたら何か学びがあるのかなと思い、調べてみました。
これまでの業務で利用した言語もあれば、名前を聞いたことはある程度の言語、せっかくの機会なので「(私にとっては)伝説の言語」までピックアップしています。
肝心のループ処理内容は、Go 1.22 Release Notes に example として提示されている以下コードをベースとしています。
package main |
また、言語選定には、以下サイトを参考にしています。
それでは、30種類のプログラミング言語でのループ処理を見ていきましょう。
10 |
本ブログでは「実装の正しさ」よりも「各言語ごとのループ処理の雰囲気を味わうこと」を優先してます。
「その実装だと、ループ回数が多いよ!」などの粗が見つかるかもしれませんが、温かい目で見守っていただけると幸いです。
for i in range(10): |
for i in range(0, 10): |
10.times do |i| |
for i in 0..9 do |
public class HelloWorld { |
void main() { |
for (let i = 0; i < 10; i++) { |
for (let i: number = 0; i < 10; i++) { |
|
|
|
|
fn main() { |
fun main() { |
import Foundation |
|
using System; |
for (int i = 0; i < 10; i++) |
void main() { |
#!/usr/bin/perl |
object Countdown { |
main :: IO () |
start_countdown :- |
for (i in 0:9) { |
for i in 0:9 |
program Countdown |
(dotimes (i 10) |
for i = 0:9 |
for ($i = 0; $i -lt 10; $i++) { |
for i in range(0, 9, 1) |
(dotimes (i 10) |
Imports System |
IDENTIFICATION DIVISION. |
**FREE |
1 to: 10 do: [:i | |
30種類の言語を書く予定でしたが、ブログ前半の Go
と最後の Smalltalk
を合わせて、合計 32 個のループ処理となりました。
普段は Go で開発しており、久しぶりに見た Java の文法で パブリックスタティックヴォイドメイン
を回避する方法が出てきた、というのが一番の発見でした。
JEP 445: Unnamed Classes and Instance Main Methods (Preview) で「Hello World!」のコードが
There is too much clutter here — too much code, too many concepts, too many constructs — for what the program does.
…
Educators often offer the admonition, “don’t worry about that, you’ll understand it later.” This is unsatisfying to them and their students alike, and leaves students with the enduring impression that Java is complicated.
と言われているのは、「それはそう」と久しぶりに仕様書で笑いました。
ここまで読んでいただけた方であれば、本記事冒頭の「Go 1.22 で追加された range over integer
の仕様紹介」はメイントピックではなく、「色々なプログラミング言語を書いてみること」が執筆モチベーションになっていることをご理解いただけると思います。Go 1.22 のマイナーアップデートという「きっかけ」を利用して、多種多様なプログラミング言語を眺めてみました。
また、執筆後に クジラ飛行机 さんの書かれた「プログラミング言語大全」を見つけまして、早速購入させていただきました。
私自身の開発言語経験と比較しますと、今回取り上げたプログラミング言語の殆どは初見です。
達人プログラマーの「毎年少なくとも一つの言語を習得する。」には「習得する」とあります。そのレベルまで達成している言語を増やせるように、これからも頑張らねばと思いました。
TIG 真野です。Go1.22連載の6本目です。
Go 1.22のアップデートのツーリングのうち Vet
と、ライブラリのマイナーアップデートである log/slog
, testing/slogtest
を取り上げて紹介します。
Vet
でループ変数の変数キャプチャを検知しなくなった #63888Vet
で slice1 = append(slice1)
の操作を検知するようになりました #63888Vet
で defer
で time.Since()
が呼ぶ操作を検知するようになりました #60448Vet
で不正な log/slog
なキーと値のペアを検知するようになりました #59407log/slog
で SetLogLoggerLevel()
が追加されました #62418testing/slogtest
で Run()
関数が追加されサブテストの制御がしやすくなりました #61758Go 1.22リリース連載始まります & ループの変化とTinyGo 0.31で説明されている通り、Goのループ変数は単なる参照だったためgoroutineにループ変数を渡すときにはいくつかのお作法がありました。go vetではありがちなミスを検知してくれていました。
func main() { |
go1.21以前ですと、次のように0から4の値を出力するはずが、全く直感と反する挙動です。go vetコマンドでその間違いを検知してくれました。
>go run main.go |
go1.22以降では、goroutine起動なので順序制御はされないものの(これは想定通りですね)、ループ変数が1つずつgoroutineに渡っていることが分かります。
go run vetloop.go |
正しく動いたので、go vetも検知しないようになりました。
go vet |
…ってあれ、変わらないですね。なんででしょう。試したのは go1.22rc2
です。まだ作業中なのかも知れません。後で調査したいと思います。
みなさんは知っていましたでしょうか? append()
が引数を取らなくても文法上は有効なことを。例えば次のようなコードはコンパイルもでき実行できます。
package main |
本来の想定は sli2 = append(sli2, v)
だと思いますが、手違いで上記のようなコードが生まれると、良くて混乱の元、通常はバグの元なので、go vetで検知する事になりました。
さて、Vetに追加するにはポリシーに沿っているかが重要です。ポリシーは次のようなものです。
今回のチェック内容ですが、約 276,000 の Go プロジェクトのうち650 のプロジェクトで 727 の問題を検知したようです。これは数が少ないので基準を満たしていない?といったコメントもありました。検知した内容でモジュールの利用頻度でソートしたトップ20を提示して、どうするかをディスカッションしています。
これで流れが決まり、Vetに含まれることになりました。空中戦にならず集計調査を実施しファクトから判断する流れは気持ちが良いですね。
ちなみにですが、このチェック内容、StaticCheckにはすでに入っている ということで、それを使っている方や、StaticCheckはgolangci-lintのデフォルトリンターでもあるので、golangci-lintをカスタマイズせずに使っている方は、この変更で追加で検知されることは多分無いです。ええええ、残念。
https://github.com/golang/go/blob/go1.21.6/src/cmd/vet/README#L18-L23
time.Since()
のユースケースは何かしらの時間を計測するときに用いられます。例えば、処理性能をロギングしたい場合は次のように書きがちです。
package main |
これを実行すると 0s
が出力されます。
正しくは以下です。
package main |
実行すると 1.0081377s
などと想定通りの結果となります。
defer
へ渡された関数の呼び出しは、元の関数がreturnするタイミングですが、その関数に渡した 引数は即時評価されるためです。 defer\ [^{]*time.Since
などで検索すると、GoogleCloudPlatform/prometheus
など大物リポジトリでもヒット、他にも多数該当するリポジトリがあるということで、Vet入りが決まりました。
検知されると次のようなメッセージが出力されます。分かりやすいですね。
>go vet |
slogのキーと値のペアのうち、以下の2条件で検知してくれるようになりました。
簡単な例です。この実装では 2
が本来キー名に当たるのですが、数値型となり不正です。
package main |
run
と vet
の結果は次の通りです。vetでは2
が string
か slog.Attr
にしろと教えてくれますね。便利。
>go run main.go |
しかし、...
を利用したスプレッド構文で渡した場合は検知できません。
package main |
上記は次のように、 run
では正しく無いレイアウトでありますが、 vet
では検知されません。
>go run main.go |
この点はプロポーサルでも触れられており、スプレッド構文は一律NGとしてはどうか、という話もありましたが、こちらは誤検知が考えられ、vetのポリシーから外れるとして棄却されていました。方針と対応が一貫しており、有無を言わさない感じがGoぽいと思いました。
slog繋がりで log/slog
アップデートです。
SetLogLoggerLevel()
関数の新規追加されました。SetLogLoggerLevel()
でデフォルトlogパッケージ側のログレベルを変更することができます(現状はINFO固定だと思います)。
本当にレベルを変えて出力したい場合は、標準のlog.Printf()
などではなく、slog.Warn()
などを使えば良いので、悪くないデフォルト値だと思いますが、プロポーサルでは、デフォルト log
パッケージはデフォルトで stderr
に出力するため、ログレベルを変更できたほうが便利だろう、という提案です。
func SetLogLoggerLevel(level Level) (oldLevel Level) |
この関数ですが、slog.SetDefault()
が呼ばれる前と後で、slog.Debug()
など、slog経由のロギングにも影響します。業務などでは slog.SetDefault()
をおそらくmainのエントリーポイントに近い場所で呼び出すと思うため、通常は細かい挙動の差を意識しなくても良いと思います。繰り返しますが本来の意図としては、標準のlogパッケージのログレベルを変更するためのものです。
…と言いながらもも slog.SetDefault()
を呼び出さず、SetLogLoggerLevel()
を呼んで、slogと標準のlogパッケージで挙動がどう変わるか試します。
package main |
>go run main.go |
動きを見ると、slog.SetLogLoggerLevel(slog.LevelDebug)
を呼び出すと、slog.Debug()
が出力されるようになったことが分かります。slog.SetDefault()
を呼び出さない前提だと、slog
パッケージ経由のログ出力を、ログレベルでフィルターするような挙動となります。
次に、 slog.SetDefault()
でlogパッケージで用いるロガーをslog形式に変更した場合に、slog.SetLogLoggerLevel()
がどう影響するか試します。
package main |
これを実行すると次のように、logパッケージ経由の出力も、ログレベルがERRORで出力されることがわかります。このケースですと、ややこしいですがslog側のERROR未満のログは抑制されません。
>go run main2.go |
少しややこしいですが、 slog.SetLogLoggerLevel()
は slog.SetDefault()
の呼び出し有無で、 slog.Debug()
やslog.Info()
などを制御するモードが変わると思った方が理解しやすいです。
ちなみに、次のように、 slog.SetLogLoggerLevel()
ではERRORレベル、 slog.SetDefault()
にわたすHandlerのログレベルにWARNを渡しても、 標準のlog
パッケージの出力はERRORレベルで出力されます。
package main |
> go run main.go |
逆に、slog.SetLogLoggerLevel()
ではINFOレベルに返ると出力されません。logパッケージのログレベルがINFO、デフォルトロガーのレベルがWARNのため、WARN未満である log.Print()
の内容は出力されません。
package main |
>go run main.go |
理にかなった挙動に見えますが、それぞれにしたログレベル値によっては、log
, slog
どちらも併用すると混乱しそうですね。
slogは slog.Handler
インターフェースを満たすことで、最終的な出力レイアウトを切り替える仕組みがあります。さきほどの slog.NewJSONHandler()
はJSONで出力するHandlerでしたよね。調べるとgo-slog/awesome-slog のようなAwesomeなリポジトリも見つかるほど、多くのHandlerが存在することが分かります。
Go1.21のslogの登場とともに、Handlerをテストする testing/slogtest
というパッケージが追加されましたが、そこにRun()関数が追加されました。
まずslogtestパッケージってなに?ってことですが、簡単にいうと予め準備されたslogのテストケースを呼び出すテストヘルパーです。どのようなケースかと言いますと、slogtest.go のcases変数を見ると分かります。
var cases = []testCase{ |
GoDocのExampleに書いてある使い方としては、テスト対象のslog.Handlerを引数に渡して、 slogtest.TestHandler(h, results)
のように呼び出すと、上記であげたテストケースが実行されます。
import ( |
この課題ですが、プロポーサルに記載されているように、TestSlogHandler という1つのフラットな関数でテストが実行されるため、どこか1ケースが落ちた際に切り分けがしにくいということが上げられていました。そこでgo test -run TestSlogHandler/ignore.an.empty.Attr
のように各1ケースごとサブテストで動かすことを行う Run()
関数が追加されました。
slog.Handlerを作る人は要チェックだと思います。作り方はガイドラインもあるようなので、併せて確認すると良さそうです。
実はGo1.22の2本目です。プロポーサルやそのやり取りを見るのが楽しくて2枠いただきました。
Vetは好きで、go vet に含まれないスタンドアロンな静的解析ツールたち 記事を書いたり、日々golangci-lintのenableとするLinterを増やす活動を行い徳を積んでいます。
slogは実践導入ができていないですが、これを機会にチャレンジしていきたいです。
引き続きフューチャー技術ブログをよろしくお願いします。Go言語で開発できる仲間を募集しています。キャリア採用での応募を待っています。
]]>Go1.22リリース連載 の5本目です。
本記事ではGoの標準ライブラリである net/http の ServeMux におけるルーティング周りの強化について取り上げます。
関連する Release Note と Issue はこちらを参照してください。
https://tip.golang.org/doc/go1.22#enhanced_routing_patterns
https://github.com/golang/go/issues/61410
ServeMux.Handle や ServeMux.HandleFunc を使用してハンドラを登録する際に GET /xxx
のようにHTTPメソッド指定して、ハンドラを呼び分けることができるようになりました。
mux := http.NewServeMux() |
従来はハンドラの中で、自前でHTTPメソッドによって処理を呼び分ける(もしくは chi のようなHTTPメソッドの呼び分けに対応したルーティングライブラリを利用する)必要がありました。
mux := http.NewServeMux() |
HTTPメソッドの指定について、3点ほど詳細を補足しておきます。
GET /hello
)とメソッドを指定しないパターン(/hello
)の両方が登録された場合は、メソッド指定のハンドラが優先されます。"HOGE /"
を指定した場合 curl -X HOGE ...
で呼び出すことが可能です。)HTTPメソッドが指定できるようになっただけでなく、/items/{id}
のようにワイルドカードが使用できるようになりました。ワイルドカードにマッチしたパスセグメントの値については Request.PathValue でアクセスできます。
mux := http.NewServeMux() |
また、/files/{path...}
のように ...
で終わるワイルドカードを指定することで、特定のパスに続く全てのセグメントにマッチさせることができます。
mux := http.NewServeMux() |
従来は /items/{id}
のようなパスパラメータに対するワイルドカードマッチを実現するためには、/items/
のように末尾スラッシュをつけることで、/items/
で始まる全てのリクエストをハンドリングし、ハンドラの中で、自前でパスパラメータをパースする必要がありました。
mux := http.NewServeMux() |
Go1.22においても末尾にスラッシュを指定した場合の挙動(指定したパスで始まる全てのエンドポイントがハンドリングされる挙動)は従来と変わりませんが、末尾スラッシュで終わるパスのみに完全にマッチさせたい場合は /items/{$}
のように {$}
を付与することで完全なパターンマッチを実現できます。
mux := http.NewServeMux() |
https://pkg.go.dev/net/http@go1.22rc2#hdr-Compatibility
ほとんどのユースケースにおいて問題になることはないと思いますが、パスに {}
を使用していたケースなどでは問題が起きるかもしれません。
例えば、ワイルドカードが存在しなかった従来のバージョンでは下記のように /items/{hoge}
と items/{fuga}
は別々のパスとして認識され、異なるハンドラを登録し、呼び分けることができました。
mux := http.NewServeMux() |
しかしながらGo1.22では {}
で囲んだ部分はワイルドカードとなるため、/items/{hoge}
と items/{fuga}
は同一のパターンと認識され、起動時に panic となります。
panic: pattern "/items/{fuga}" (registered at xxxxx) conflicts with pattern "/items/{hoge}" (registered at xxxxx): |
もしワイルドカードなどの新しい仕様が許容できない場合は、環境変数に GODEBUG=httpmuxgo121=1
を設定することで、Go1.22を使いつつ以前の動作のまま動かすことが可能です。
Go標準のベンチマーク機能を用いて ServeMux とルーティング機能を提供する主要ライブラリである chi と Gorilla の性能を比較してみます。
各ライブラリを利用して以下の4パターンのAPIを定義し、それぞれ ServeHTTP をテストする形で性能を計測します。
ソースコードはこちらで公開してますが、各ハンドラ内ではパスパラメータ値の取得まで行っております。
実行結果に表示されている各ケースの命名については、Benchmark${ライブラリ名}${パスパラメータ数}
(例. BenchmarkServeMux0)となります。
結果を見ると、パスパラメータが存在しない or 1個のみの場合は ServeMux > chi > Gorilla となり、パスパラメータが5個、10個と増えた場合は chi > ServeMux > Gorilla となっています。
go test -bench . -benchmem |
一般的にAPIのパスパラメータはそこまで多くならない(通常は0 ~ 2個)よう設計されることが多いと思います。
ルーティングライブラリを導入せず、標準の ServeMux だけで十分というケースが増えそうですね。
CSIGの棚井です。
本ブログは、Go 1.22 Release Notes の内容を紹介する「フューチャー技術ブログ Go 1.22リリース連載」の4本目の記事です。
今回は encoding
のアップデートを取り上げます。
また、本ブログは release-branch.go1.22 での動作をベースとしています。
$ go version |
Go 1.22 のリリースノート原文には、以下の説明があります。
The new methods AppendEncode and AppendDecode added to each of the Encoding types in the packages encoding/base32, encoding/base64, and encoding/hex simplify encoding and decoding from and to byte slices by taking care of byte slice buffer management.
該当する issue は「encoding: provide append-like variants #53693」です。issue には、以下のような説明があります。
ちなみに、今回追加された Append ライクなエンコード関数とデコード関数の追加は、こちらのコミット(encoding: add AppendEncode and AppendDecode)にて確認できます。
base32
, base64
, hex
のそれぞれに同じ機能の関数が追加されているので、今回は base32
での処理内容をメインに、AppendEncode
と AppendDecode
を見ていきます。
// AppendEncode appends the base32 encoded src to dst |
// AppendDecode appends the base32 decoded src to dst |
Go での base32
のエンコード・デコード処理は、もちろんソースコード内に実装されています。
私の場合、実装コードとテストコードを眺めるだけでは、いまいち処理内容がつかめなかったので、base32 のエンコード・デコード処理を「手計算」で実施してみました。
以下、「Hello」という文字列を、base32 の値にエンコードするまでの流れです。
まず、「Hello」の文字列を、1つずつ Asciiコード に変換します。
H | e | l | l | o | |
---|---|---|---|---|---|
10進数 | 72 | 101 | 108 | 108 | 111 |
16進数 | 0x48 | 0x65 | 0x6c | 0x6c | 0x6f |
続いて、IP アドレスやサブネットマスクで見慣れた「2進数、バイナリ値」に変換します。
H | e | l | l | o | |
---|---|---|---|---|---|
10進数 | 72 | 101 | 108 | 108 | 111 |
16進数 | 0x48 | 0x65 | 0x6c | 0x6c | 0x6f |
binary | 01001000 | 01100101 | 01101100 | 01101100 | 01101111 |
②で変換したバイナリ値を連結すると、8 × 5 = 40 ビットのバイナリストリームができあがります。
H | e | l | l | o | |
---|---|---|---|---|---|
binary | 01001000 | 01100101 | 01101100 | 01101100 | 01101111 |
↓
0100100001100101011011000110110001101111
0と1で連結された文字列を、5つごとに区切ります。
0100100001100101011011000110110001101111
↓
01001, 00001, 10010, 10110, 11000, 11011, 00011, 01111
2進数の値を10進数に変換します。
binary per 5 bits | 01001 | 00001 | 10010 | 10110 | 11000 | 11011 | 00011 | 01111 |
---|---|---|---|---|---|---|---|---|
10進数 | 9 | 1 | 18 | 22 | 24 | 27 | 3 | 31 |
10進数の値それぞれを、1つずつ base32 で変換(エンコード)します。
binary per 5 bits | 01001 | 00001 | 10010 | 10110 | 11000 | 11011 | 00011 | 01111 |
---|---|---|---|---|---|---|---|---|
10進数 | 9 | 1 | 18 | 22 | 24 | 27 | 3 | 31 |
base32 | J | B | S | W | Y | 3 | D | P |
エンコードされたそれぞれの文字を結合してできる文字列が、最終的な base32変換された値となります。
# 元の文字列 -> base32エンコード後の文字列 |
検証用に、以下の Go コードを動かしてみると、base32 変換後の値が一致することも確認できます。
package main |
$ go run base32_encode.go "Hello" |
また、base32は「5バイト(40ビット)」ごとに分割して、5バイトからの不足分は「=
」によりパディングするルールがあります。
=
」を1つパディング=
」を3つパディング=
」を4つパディング=
」を6つパディング例えば「Golang」の文字列であれば、
8bit × 6文字
= 48 bit
= 6 byte
= 5 byte + 1 byte
= 5 byte + (5 byte - 4 byte) ← 4バイト不足
なので、「=
」は6つ追加されます。
$ go run base32_encode.go "Golang" |
ちなみに、「Hello」の文字列はちょうど5バイトなので、パディングは発生しません。
それでは、今回追加されたエンコード関数とデコード関数を動かしてみます。
package main |
提案元の issue に記載されているように「Append ライク」な動作ということなので、関数の呼び出し側でバッファを意識せずとも「Append」が可能です。
以下のサンプルコードは、こちらの「テストコード」をもとに作成しました。
package main |
こちらのコミット(encoding: require unique alphabet for base32 and base64)で修正された「パディングの受け取る値」について、以前のバージョンでの挙動と比較しながら確認してみます。
base32 エンコードでは、5バイトごとに分割した際の「不足分」が、「=
」によりパディングされるというルールがありました。
Go の実装では、パディングの値は「こちら」で定義されています。
const ( |
パディングの値を「$
」に指定すると、base32エンコードの結果が以下のようになります。
package main |
リリースノートには以下の記載があります。
The methods base32.Encoding.WithPadding and base64.Encoding.WithPadding now panic if the padding argument is a negative value other than NoPadding.
試しに WithPadding
へ -2
を代入して動かしてみます。
package main |
$ go build -trimpath -o invalid_padding invalid_padding.go |
Go 1.22 で動かすと、リリースノートの記載通り panic が起きました。
比較検証として、Go 1.21 で動かしてみると、panic にはならず「パディングの値が文字化けして」表示されました。
このバグを踏むケースがあまりイメージできませんが、1.22 で回避されるようになりました。
$ go run invalid_padding.go |
'\b'
と '\f'
はそれぞれ、\u0008
と \u000c
に変換されていました'\b'
は \b
に、'\f'
は \f
に変換されるようになりましたRFC 8259
で定義された5つの制御文字全てへの対応が完了しましたリリースノートには、1行だけ説明があります。
Marshaling and encoding functionality now escapes ‘\b’ and ‘\f’ characters as \b and \f instead of \u0008 and \u000c.
本アップデートは、こちらの「コミットログ(encoding/json: encode \b and \f as ‘\b’ and ‘\f’ in JSON strings)」に詳しい説明があります。
EFC 8259
には5つの制御文字があり、
\r
と n
に対応\t
に対応する中で、残りの \b
と \f
に対応したのが今回のアップデートのようです。
また、コミットログには以下の記載があります。
This change is to prepare the path forward for a potential v2 “json” package, which has more consistent encoding of JSON strings.
「v2 json package への準備」とのことなので、今回のリリースにて math/rand/v2 が追加されたこともり、今後のアップデートが楽しみだなと思いました。
本ブログでは、encoding
パッケージへの追加機能、bugfix 内容を紹介しました。
今回の連載記事の内容を調べるなかで「アップデートの背景を、issue を通して知る」ことの面白さに気付きました。
コードを読む中で、以外と小さな単位のコミットがマージされているケースも見つかりましたので、私もできるところから OSSにコミットしていきたいなと思いました。
]]>The Gopher character is based on the Go mascot designed by Renée French
TIG 真野です。Go1.22連載の3本目です。
Go 1.22のマイナーアップデートのうち、ファイルなどの入出力に関連しそうな archive/tar
・archive/zip
・bufio
・io
を取り上げて紹介します。
Writer.AddFS
が追加された #54898Writer.AddFS
が追加された #58000SplitFunc
が ErrFinalToken
を返すときに即時停するようになった。従来は []byte{}
を返していた #56381SectionReader.Outer()
メソッドが追加された #61870archive/zipパッケージにWriter.AddFS というメソッドが追加されました。処理としては、FS、つまりファイルシステムを入力として、ルートからディレクトリツリーを辿ってフォルダ構成を維持しながら全ファイルをzip化します。便利ですね。
これが登場する以前は、Stack Overflowなど複数の記事でいくつか実装例を参考にしながら各自が実装していたようで、揺れていたり実装ミスが発生したようです。zip.NewWriter()でzipに追加したいファイルを1つ1つ追加する必要がありました。
Stack Overflowの例もFileWalkerなどを使って(それなりの量を)実装する必要があります。また、w.Create()の前に書いてあるコメント通り、指定されたパスが相対パスにする必要があったり、Windowsでも動作するようにするためには、一工夫がさらに必要です。
package main |
これが次のように書き換わります。
package main |
とても楽ですし、直感的ですね!ちなみに、空フォルダはzip化されないようです。
また、何かしらのファイルを除去したいなどのフィルター処理をしたい場合は、それを行う fs.FS
を作成して回避するといった考えのようです。
fs.FS
を引数に取るということは、別の応用も効かせられます。jszwec/s3fs はS3の指定されたバケットに対してfs.FS
インターフェースを満たすライブラリです。例えばこれを用いると、S3バケットがそのままzip化されます。
package main |
ビルディングブロック的に、zip化ができるようになったのは画期的だと思います。
【注意】上記のコードはS3バケットまるごとダウンロードするので、バケットのデータ量によっては利用を控えたほうが無難です。また、s3fs側の実装とAddFS()の組み合わせが悪いのか、S3に空フォルダオブジェクト(キー名が/
で終わるオブエジェクト)が含まれる場合は、上手くzip化されないようです(実行時エラーとなります)。ご注意ください。
archive/tarパッケージにWriter.AddFS というメソッドが追加されました。背景や内容については、archive/zip
と全く同じでしたので割愛します。
Scannerが、bufio.SplitFunc を受け取り、 ErrFinalToken
を返した場合は停止するようになりました。従来は []byte
を返していました。
まずScannerにはSplit() という関数があり、Split()
は SplitFunc
を引数に取ります。
func (s *Scanner) Split(split SplitFunc) |
例としてIssueにあったScannerの実装を上げます。
func main() { |
上記で、scanner.Scan() ですが、inputは1行ですが、ループはカンマごとにSplitFunc() で分割され、またSTOPという文字列で停止するために2回ループが実行されるのが想定だと思います。
go1.21以前では、これが3回実行されていましたが、go1.22以降では2回の実行となります。
go1.21以前:
Got a token "1" |
go1.22以降:
Got a token "1" |
変更理由は、このデータを取りたいケースは存在しないだろうということで、どちらかといえばあるべき動きに訂正されたようです。
io.SectionReader
に以下のメソッドが追加されました。
func (s *SectionReader) Outer() (r ReaderAt, off int64, n int64) |
SectionReader自体はRead(), Seek(), ReadAt() を実装する、入力を指定された オフセット~長さに区切ったReaderです。GoDocのExampleを見ると何をするようなものか一目瞭然です。
package main |
これに Outer()
を追加します。
func main() { |
実行すると、引数で渡したstrings.Reader, offset=5, length=17
が取得できます。
さて、機能はわかったところで、これが追加された理由です。io
側のコメントを追っていくと、net: support zero-copy send on TCPConn when reading from File via SectionReader #61727がモチベーションのようです。
#61727
の内容は私が理解しきれた範囲だと以下です(補足、訂正大歓迎です)
net.TCPConn
でファイル送信する場合に、ゼロコピーになるのは現在、LimitedReader
のみSectionReader
もゼロコピー対応したいこの対応が入れば、GoのHTTPサーバの応答性能がさらに上がりそう、というのがわかります。今後に期待ですね。
派手さはないですが、こういった細かいアップデートを確認していくと、Stack Overflowなどでコミュニティ側が混乱していそうな点を標準パッケージ側で吸収しGoアプリケーションとしての品質向上に努めたり、性能観点など、確実にGoが良くなっているのが感じられます。
個人的には、fs.FSをインターフェースにzip化できるのは、便利で応用力が高くて良い設計だな感嘆しました。私が設計者なら、普通にディレクリパスを渡して、あるフォルダごとzip化するようなインターフェースを考えてしまいそうです。
ライブラリのAPI設計の勉強にもなり学びでした。次のバージョンでもこのブログ連載に参加しようと思います。
]]>The Gopher character is based on the Go mascot designed by Renée French
TIGの辻です。Go1.22連載の2本目です。
この記事では、マイナーアップデートから slices パッケージを取り上げて紹介します。
Concat()
API が追加になった(#56353)Insert()
で引数が範囲外の場合に、常に panic させる(#63913)Concat()
API が追加になった(#56353)以下の API が追加になりました。渡された slice を連結して新しい slice を返却します
func Concat[S ~[]E, E any](slices ...S) S |
コード例です
package main |
ちなみにGo 1.21ですと appned() では複数の slice を連結できず、slice を unpack して以下のように実装する必要がありました。
package main |
以下のAPIの機能改善です。slice の長さが小さくなる関数で、破棄すべき slice の要素をゼロ値でクリアするようになっています
Delete()
のコード例です
package main |
package main |
横道にそれますが Delete()
に関して CL#541477 からコードの変更内容を見てみました。
https://go-review.googlesource.com/c/go/+/541477
変更前は要素を除いた値を appned()
しているだけですが、変更後は明示的に clear()
しています
func Delete[S ~[]E, E any](s S, i, j int) S { |
func Delete[S ~[]E, E any](s S, i, j int) S { |
Insert()
で引数が範囲外の場合に、常に panic させる(#63913)Insert()
APIの機能改善です。
Go1.21では範囲外の Index が渡されても、挿入する要素が存在しない場合は panic しませんでした。
ただドキュメント上での説明では Insert panics if i is out of range
とあり動きにドキュメントと動作に乖離がありました。
https://pkg.go.dev/slices@go1.21.0#Insert
Go1.21では以下のコードが実行できます
package main |
ただし範囲外の Index に値を追加する場合はGo.1.21でも panic になります
package main |
Go1.22では範囲外の Index が指定されている場合は要素の有無に関わらず、panic するようになります
package main |
slices パッケージの機能追加/更新を紹介しました。
slices パッケージはGo1.21で標準ライブラリに追加されましたが、ユーザーに見える/見えにくい内容含めて、まだまだ進化しそうでこれからも楽しみですね
The Gopher character is based on the Go mascot designed by Renée French
Future Tech Blog恒例のGoリリース連載が始まります。
Date | Title | Author |
---|---|---|
1/29 | インデックス & ループの変化とtinygo 1.31 | 渋川 |
1/30 | slicesのマイナーアップデート | 辻大志郎 |
1/31 | archive/tar, archive/zip, bufio, io | 真野隼記 |
2/1 | encoding, encoding/json | 棚井龍之介 |
2/2 | HTTPルーティング強化 | 武田大輝 |
2/5 | vet, log/slog, testing/slogtest | 真野隼記 |
2/6 | 30種類のプログラミング言語で、ループ処理を書いてみた | 棚井龍之介 |
2/14 | net, net/http, net/netip | 真野隼記 |
Go 1.22のトピックとしては以下のようなものがあります。だいぶ安定版になってきたからか、言語もライブラリもこつぶなものが多くなってきたかな、という印象です。
個人的に注目しているHTTP/3やQUICへの対応は、準標準ライブラリのgolang.org/x/net/internal/quicの中で進行中。将来的にはinternalが外れたgolang.org/x/net/quicができて、そののちに標準ライブラリ化される予定のようですが、まだ時間かかりそうですね。
https://github.com/golang/net/tree/master/internal/quic
2/1追記
database/sqlのNullの追加は以下のブログエントリーが詳しいです。コミットした本人によるブログ記事なので日本だけでなく海外含めてもこれよりも詳しい説明は存在しないでしょう。
methaneのブログ: sql.Null[T] をGo 1.22に追加しました
Go 1.22は言語の変化としてはループ変数の扱いが変わったり、固定回数のループが描きやすくなったり、今後のバージョンで入る予定のrange over functionという機能が GOEXPERIMENT=rangefunc
という環境変数のスイッチで有効になったりします。まずはループ変数の扱いの変化から説明します。
Goのループ変数は単なる参照でした。で、goroutineの起動には時間がかかります。次のようなコードを1.21までのGoで実行するとgoroutineが起動するころには呼び出し元のループは終わっていました。iは単なる参照で、iの最終の値は9なので、だいたい9が出力されます。
package main |
これに対処するために、わざわざ新しいメモリ領域が確保されるようにする必要がありました。主に2つの方法がありました。しかし、
// ループの中で変数を定義して代入 |
Go 1.22からは、ループ変数が内部のクロージャなどから参照されたりした場合はi := i
を自動で差し込むようになり、呼び出したタイミングでのループ変数の値が中でも利用できます。
Goでgoroutineをループであつかう場合のよくある落とし穴が塞がれました。
range over intというのが入りました。今まで、10回繰り返したい!みたいなループはC言語からの伝統のループを書く必要がありました。
// C言語からの伝統のループ |
これがこのように書けるようなります。
// 1.22からの新しいループ |
Go 1.23以降に入る予定だが、1.22で実験的に実装されているものがrange over functionです。
古の書のデザインパターンを読んだことがある人もいるかもしれません。その中で「イテレーターパターン」というものがありました。繰り返し処理を自前で実装する方法を表現したものです。Goの標準ライブラリの中にもいくつかあります。イテレーターパターンはその実装方法から、内部と外部と2種類の分類が知られています
// 内部イテレータ |
だいたいの言語にはforループがあり、うまく外部イテレータを言語が定めるプロトコル通りに実装することで言語標準の文法の中で使えるような言語がいくつもあります。C++やJavaScriptのfor ofループや、Pythonの__iter__メソッドなどです。range over functionも、この仲間です。
使い方とかはすでに詳しくわかりやすく解説してくれているブログ記事があったりするのでそちらを見てもらえればよいかな、と思います。
これらがリリースされると、そのうち、次のように書けるようになるかと思います。まだExperimentalで変更もありえるので詳しくは説明しません。
// メソッド名は適当です |
Go本体のリリースしかあまり見てなかったのですが、以下のtakasagoさんの投稿を見て、TinyGoもリリースを予定していることを知りました。
Go 1.22 リリースに合わせて TinyGo 0.31 がリリースされる予定です。 Go 1.22 の encoding/json で新たに必要となった reflect.TypeFor の実装を足せばリリースできそうな感じ。
— takasago (@sago35tk) January 17, 2024
大きな変更として net package が TinyGo 内に用意され大幅に更新されました。このあたりは、別途。
ここ数年のGoのリリースと、それらのバージョンにあわせたTinyGoのバージョンを表にしたものが次の通りです。READMEの説明を見ると、昔は「完全ではない」という表現もあったりしたのですが、おおむね、2-3週間のラグで対応するバージョンが出ているようです。
リリース日 | 対応するTinyGoバージョン(リリース日) | |
---|---|---|
Go 1.21 | 2023/8/8 | 0.29 (2023/8/26) |
Go 1.20 | 2023/2/1 | 0.27 (2023/2/12) |
Go 1.19 | 2022/8/2 | 0.24 (2022/7/1、ただしベータ扱い), 0.25? (2022/8/3) |
Go 1.18 | 2022/3/15 | 0.23 (2022/4/29、ただし全機能ではない) |
Go 1.17 | 2021/8/16 | 0.20 (2021/9/22) |
せっかくなので、Go 1.22サポートを目指している開発版のTinyGoをインストールしてみます。手順はTinyGoのウェブサイトのBuild from sourceのページにあります。
$ git clone --recursive https://github.com/tinygo-org/tinygo.git |
さっそく実行してみようとすると、Go 1.22がないぞ、とのエラー。Go 1.22 rc 1をインストールして以下のrange over intのコードを動かしてみます。もう最新の言語サーバーのgoplsはrange over intを正しく解釈してくれるのですね。
package main |
以下のように実行すると、Go 1.22のコードをコンパイルして実行できました。小さいバイナリができてWASMでの利用もしやすいし、今後はちょくちょく触ってみようと思います。
$ tinygo run main.go |