Karakuri.com

ベンチャー企業で働くソフトウェアエンジニアの技術録

CloudFormationでAmazon SQS metricsのDimensionsは何を書けばいいのか?

Serverless FrameworkでSQSのCloudWatch Alarmを作成しようとしたときに情報が全然なかったので整理します。Serverless FrameworkではSQSはサポートしていないため、CloudFormationで記述することになります。そしてSQS CloudWatchメトリクスについてはここにまとまっています。

docs.aws.amazon.com

各パラメータは上記記事のdescriptionを読めば大体分かるのですが、Dementionsだけがよく分かりません。

The only dimension that Amazon SQS sends to CloudWatch is QueueName. This means that all available statistics are filtered by QueueName.

困ってAWSコンソールを見たり試行錯誤してみたのですが、結論を言うとこういうことでした。

続きを読む

TerraformでAurora MySQLに対してEC2のみアクセスを許可するセキュリティグループを作成する

Aurora MySQLとその周りのインフラをTerraformで構築していたのですが、VPC内のEC2からのみAuroraへのアクセスを許可するセキュリティグループの作成に少し手間取ったのでメモを残しておきます。

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 3.0"
  ...
}

module "ec2_security_group" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 4.0"

  name        = "ec2-security-group"
  description = ""
  vpc_id      = module.vpc.vpc_id

  ingress_cidr_blocks = ["0.0.0.0/0"]
  ingress_rules       = ["all-icmp", "ssh-tcp"]
  egress_rules        = ["all-all"]
}

module "aurora_security_group" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 4.0"

  name        = "aurora-security-group"
  description = ""
  vpc_id      = module.vpc.vpc_id

  ingress_with_source_security_group_id = [
    { 
      rule = "mysql-tcp", 
      source_security_group_id = module.ec2_security_group.security_group_id 
    },
  ]
  egress_rules        = ["all-all"]
}

Terrafoem Registryに登録されている terraform-aws-modules を使って構築しました。
github.com

これによってAuroraにはVPC内のEC2にSSH接続する以外の方法でアクセスできないようになります。

Serverless Framework+Lambda+Node.js+TypeScriptで別のAWSアカウントのDynamoDBにアクセスする

いわゆるAWSのクロスアカウントでリソースにアクセスする方法についてのメモです。方法はいくつかあるのですが、STSを使った方法が汎用的に見えたので採用しました。

  const sts = new STS();
  const roleResponse = await sts
    .assumeRole({
      RoleArn: ROLE_ARN,
      RoleSessionName: "TEST",
    })
    .promise();

  const dynamoDB = new DynamoDB({
    credentials: new AWS.Credentials(
      roleResponse.Credentials.AccessKeyId,
      roleResponse.Credentials.SecretAccessKey,
      roleResponse.Credentials.SessionToken
    ),
  });

STSでロール情報を取得し、それをリソースのインスタンス生成時に渡してあげるだけです。AWS SDKの構文は同じなので、他のリソースに関しても同様の方法でクロスアカウントでアクセスできます。

iamRoleStatements: [
  {
    Effect: "Allow",
    Action: ["sts:AssumeRole"],
    Resource: "*",
  },

serverless.tsにsts:AssumeRoleを許可するのを忘れずに。あとはAWSコンソールでIAMにDynamoDBへアクセスするためのロールを作成すればOKです。AWS SDKのSTSインスタンスに対して作成したロールのarnをassumeRoleメソッドで渡してあげればクレデンシャル情報を取得できます。このarnをどこに保管しておくかも方法は色々ありますが、自分はSSMに保管してみました。

  const ssm = new SSM();
  const parameter = await ssm
    .getParameter({
      Name: SSM_PATH,
      WithDecryption: false,
    })
    .promise();

  const sts = new STS();
  const roleResponse = await sts
    .assumeRole({
      RoleArn: parameter.Parameter.Value,
      RoleSessionName: "TEST",
    })
    .promise();

こんな感じですね。

WPFと.Net FrameworkでPasswordBoxを簡単に自分で作ってみる

WPFに標準で用意されているPasswordBoxはWinForms時代のものなので機能が貧弱です。一番イケてないのはPasswordBoxに入力したパスワードをViewModelにBindingできないこと。実際にはEventTriggerなどを使えばBindingできることはできるのですが、MVVMパターンが推奨されているWPFの標準UIコンポーネントとしてはあまりにお粗末です。また、デザインのカスタマイズ性もイマイチなので、これはもう自分で作った方が早いということになりました。

ViewModelでPasswordBoxの処理を作りこむ

どうやって作るかというと、普通のTextBoxを使いながらViewModel側で上手くPasswordBoxの振る舞いを作りこみます。TextBoxは特に目立った設定は行いません。

<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"/>

このTextBoxにBindingしているプロパティがこちらです。

public string Password
{
    get => new string('●', plainPassword.Length);
    set
    {
        if (value.Length < plainPassword.Length)
        {
            plainPassword= plainPassword.Substring(0, value.Length);
        }
        else if (value.Length > plainPassword.Length)
        {
            plainPassword+= value.Substring(plainPassword.Length, value.Length - plainPassword.Length);
        }
        else
        {
            plainPassword= value;
        }
                
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Password)));
    }
}

string plainPassword = string.Empty;

setterで入力値の差分を見て平文のパスワードを更新しています。getterでは平文のパスワードを全てマスキングして返すことで、View上では入力したパスワードが見えないというカラクリです。

続きを読む

WPFのTextBoxがソフトウェアキーボードと被ってしまわないようにXAMLだけで自動で最上位に移動させてみる

TextBoxがソフトウェアキーボードと被ると入力ができなくなってしまうので、ソフトウェアキーボードが表示されたら自動でTextBoxは画面の最上位に移動して欲しいケースはタッチパネルPC向けのアプリケーション開発では必ず出てくるかと思います。ただこの自動で画面最上位に移動するコードをViewModelに書くのは面倒ですし、イベントでxaml.csに書くのも気が引けます。今回はこの要件をXAMLだけで実現してみます。

EventTriggerとStoryboadでMarginを変える

作戦としてはEventTriggerでStoryboardを発動させてパネルのMarginを変えてしまうというものです。パネル全体を上に移動させてしまうことで、あたかもTextBoxが画面最上位に移動したかのように振る舞います。

<TextBox>
    <TextBox.Triggers>
        <EventTrigger RoutedEvent="GotFocus">
            <BeginStoryboard>
                <Storyboard>
                    <ThicknessAnimation Storyboard.TargetName="Panel"
                                        Storyboard.TargetProperty="Margin"
                                        To="0,-93,0,0" Duration="0:0:0.2"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
        <EventTrigger RoutedEvent="LostFocus">
            <BeginStoryboard>
                <Storyboard>
                    <ThicknessAnimation Storyboard.TargetName="Panel"
                                        Storyboard.TargetProperty="Margin"
                                        To="0" Duration="0:0:0.2"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </TextBox.Triggers>
</TextBox>

また、LostFocusでMarginを元に戻してあげるのも忘れないようにしてください。TextBoxの領域外を触るとフォーカスが外れてMarginが元に戻ることで、画面の表示位置が戻るような振る舞いをします。コード全体はGithubで公開しているので、触ってみたい方はこちらからどうぞ。
github.com

実際に動かしてみたキャプチャ動画

youtu.be

AWS OrganizationsのAWSメンバーアカウントを別のOrganizationに引っ越ししました

会社が大きくなった結果、1つのAWS Organizationsに複数の組織のAWSメンバーアカウントが同居する状況が発生し、管理部側で請求を整理するのが大変になってきました。このため、新たな組織(Organizations)を作成してAWSメンバーアカウントを移動することにしました。

組織を移動するための手順

手順は至ってシンプルです。

  1. AWSメンバーアカウントのルートアカウントで既存組織を抜ける
  2. 新組織からAWSメンバーアカウントに招待を投げる
  3. AWSメンバーアカウントで招待を受ける

ただ、1番目と2番目にそれぞれ条件があり、そこにハマると時間を溶かすことになります。

続きを読む

AWS Organizationsで作成したAWSアカウントのルートアカウントを取得する方法

AWS OrganizationsからAWSアカウントを作成することができますが、このときルートアカウントは自動生成されたパスワードが設定された状態になっているそうです。このパスワードは特に通知されないため、通常はログインすることができません。ただ何等かの理由でルートアカウントにログインが必要となった場合、取得する方法はあるので書き残しておきます。

AWSコンソールからログインを試みる

AWS OrganizationsからAWSアカウントを作成する際にメールアドレスが必要となります。AWSコンソールからこのメールアドレスを使ってログインを試みてください。するとパスワード入力画面で「パスワードをお忘れですか?」というリンクが表示されます。ここからメールアドレスにパスワード再設定メールを送ることができます。あとはメールに記載されているURLからパスワードを再設定すると、ルートアカウントでログインできるようになります。
f:id:hazakurakeita:20201020003105p:plain

ルートアカウントを放置するリスク

上記から分かるように、AWS Organizationsから作成したAWSアカウントはメールアドレスのセキュリティ強度で守られていることになります。もしAWSアカウントのメールアドレスが社内で複数の人間が利用できるものだった場合、その人たちはルートアカウントを手にすることができることとなります。これは権限管理の視点でも好ましくないため、ルートアカウントはAWS Organizationsで作成したままにせず、パスワードを再設定して2段階認証を設定しておくべきかと思います。この2段階認証を適切な管理者だけで管理しておけばルートアカウントは権限管理されている状態になるのではないかと思います。

CloudFlareで2段階認証もバックアップコードも紛失してしまったので無効化・復旧した話

先日メンバーからCloudFlareにログインできないと相談を受けました。アカウントの2FAを強制オンにしていたのですが、2FAを紛失してしまったとのこと。バックアップコードも保管した記憶がないということで、メンバーのアカウントの復旧を試みました。

CloudFlareの2FAとは

Super Administratorで2FAの強制をオンにしていると、メンバーを招待したときにパスワード設定と同様に2FAの設定も強制されます。また、この際にバックアップコードも表示されます。次回ログイン時にはこの2FAが要求されますが、IPをCloudFlareが見ているのか、何らかの条件を満たすと2FAはログイン時には要求されません。

CloudFlareの2FAの無効化を試みる

Super Administratorからメンバーの2FAは無効化できない

まずCloudFlareのアカウント管理からメンバーの2FAの無効化ができないかメニューを探してみました。1Passwordなどはそういう機能があるのですが、どうやらCloudFlareにはないようです(プランによってはあるのかもしれない)。

アカウントを削除しても2FAは残る

そこで一旦メンバーのアカウントを削除して、再招待して再登録を実行してもらいました。しかし、パスワードを再設定したあとに2FAが求められます。なんと、アカウントを削除しても2FAは残っており、同じメールアドレスだと2FAは維持されるようです。セキュリティ的には強固ですが、こういう仕様は初めて見ました。

メールアドレスのエイリアスもチェックしている

それならばとGmailのエイリアス機能を使ってメールアドレスを変えて再招待してみました。例えば、momotaro@gmail.comで登録していたのであれば、momotaro+cf@gmail.comで再招待するということです。するとCloudFlareからエラーが…。
f:id:hazakurakeita:20200328142035p:plain
エイリアスを認識してNGと言ってきました。CloudFlareのセキュリティは本当にすごいですね。

挫折してCloudFlareに連絡する

CloudFlareのサポートはまずはコミュニティ投稿

CloudFlareのサポートは基本的にコミュニティ投稿のようです。直接サポートに連絡する手段は明確には公開されていません。例えば2FAとバックアップコードの紛失についても書き込みはあります。
community.cloudflare.com

CloudFlareのサポートに連絡する

上の記事を見てもらうと分かりますが、2FAとバックアップコードを紛失した際はユーザーで復旧する手段はありません。上の記事に書かれているメールアドレスに連絡するしかないので、連絡しました。

CloudFlareサポートに2FAの無効化をしてもらう

CloudFlareから認証を求められる

これだけ2FAの無効化が厳しいので、サポートに連絡するだけで無効化を解除はしてくれません。回答としては追加の認証行為を求められました。公開情報ではないので具体的には書きませんが、Google AnalyticsのWebサイトの認証とか、そういう方法に似ています。正直、CloudFlareを使っているサーバーが多数あって中には本番のWebサーバーもあったので目がくらみました。

他に認証方法はないのか?

本番のWebサーバーまで変更するのは骨が折れるので他に方法がないかどうかサポートに質問しました。結論を言うと、他の認証方法でメンバーの2FAは無効化してもらえました。この他の方法も言っていいのか分からないので伏せておきます。もし困った際はサポートに相談すれば良いと思います。

CloudFlareのサポートはとても親切だった

先日CloudFlareは新型コロナウイルス対策のため在宅勤務を基本としており、サポート等のレスポンスが送れる可能性があるというメールを送っていました。が、今回その影響を感じさせない早さで対応いただいたのでとても感謝しています。ありがとうございました。

言語設定によって小数点や桁区切り文字が違って海外で不具合が起きる話

前職は世界中で開発したソフトウェアを販売しており、同じWindowsでも日英中韓台独仏伊と8か国くらいの言語版でテストしてたりしました。するとドイツ版で変な挙動をするという報告を受けました。どうも数値がおかしくなるんだそうです。それで当時調べてみました。

値が1000分の1になったりクラッシュしたりする

問題の挙動

前職で開発していたソフトウェアは産業機械関連なので設定値がたくさんあります。例えば距離でメートルの設定はたくさん出てきますし、ミリメートルだとそれなりの桁数になるので、桁区切り文字の「,」を自動で入力するようにしていました。

1,000 mm

こんな感じで保存されます。しかし保存して再度設定画面を開くとドイツ版だと下記のようになってしまうとのこと。

1 mm

なんということでしょう。何が起こっているんだ。日本語や英語では起きないのに…。

原因は国や地域の桁区切り文字設定

桁区切り文字が国や地域によって違う

これが衝撃的だったのですが、実は桁区切り文字が国や地域によって違います。国際規格では小数点にピリオド「.」を使うことになっているようですが、少なくともWindowsは汎用的なOSなので国や地域の慣習を優先して使います。例えばドイツの場合は桁区切り文字がピリオド「.」で小数点が「,」で日本と逆です。
coliss.com
中には桁区切り文字がスペースな国もありますが、Windowsの標準設定はどうなっているんですかね。考えるだけでも恐ろしいです。

コードでは何が起きていたのか

桁区切り文字を追加するとき

例えばWindowsの場合は,Net Frameworkを使って数値に桁区切り文字を付けてString型にすることができます。

string.Format("{0:N0}", value);

これがWindowsの設定によって挙動が変わります。例えば「1000」を渡したとき桁区切り文字が「,」の場合は「1,000」ですし、「.」のときは「1.000」になります。

桁区切り文字を除去するとき

先ほど桁区切り文字を追加した文字列を再び数値に変換してDBなどに保存する場合を考えます。桁区切り文字を含む文字列を数値に素直に変換するメソッドが.Net Frameworkにはないので、簡易的に下記のように書くことが多いのではないかと思います。

int.Parse(valueString.Replace(",", ""));

ここでvalueStringが「1.000」だとクラッシュします。実際はint.TryParseを使ってエラーハンドリングで0とか無害な数値になるかもしれませんが、いずれにせよ正常には動きませんね。ちなみに最初に「1,000mm」が「1mm」になった理由はドイツ版で問題が起きた環境を開発陣の日本版環境で検証したからとかそんな感じだった気がします。もう環境が混ざるとカオスです。

どのように対策すれば良いのか

運用で回避してしまう

前職は割と規模が大きくソフトウェアの改修コストが大変なことになってしまうので、各国でも桁区切り文字に「,」を使うように運用で回避してもらうことにしました。Windowsの設定で桁区切り文字を変えることができます。
www.wannko.net

ちゃんと対応するには?

やってないので調べてないのですが、例えばここら辺とか使って何とかできそうな気もします。
docs.microsoft.com
これってAndroidやiOSだとどうなんでしょうね。

Ruby on RailsでTimeとDateTimeとPostgreSQLのTimestampの精度の差でRspecが失敗した話

言わずもがな保守運用をやってるレガシーシステムで時刻で使っている型がバラバラでして、ちょっと不具合修正をしたらジェンガのように音を立ててRspecが失敗するようになってしまいました。原因を調べていくと、DBで使用しているPostgreSQLのTimestamp型まで含めた精度の話に帰着したのでまとめておきます。

なにが起きたのか

先にも書いたのですが、意図が分からない to_datetime がソースコードに散らばっていて、ここら辺の不具合を修正したときに to_datetime するかどうかでテスト結果が変わるという謎の状態に陥ってしまったんですね。to_datetime する意図が分からないので、とりあえず全部消してみたら更にテスト失敗数が増えてどんどんシステムが壊れていきました。

expected: 2019-12-07 23:59:59.999999999 +0900
     got: 2019-12-07 23:59:59.999999000 +0900

こんな失敗が頻発してたんですね…。どういうこと…。

それぞれの型の精度を調べてみる

結果から察するとナノ秒の情報が欠落しているのが原因みたいです。それでRspecのコードを調べてみると、

  • expected: Time型
  • got: PostgreSQLのTimestamp型(ActiveRecordで取ってきた)

ということが分かりました。

Time型

Time型はナノ秒までの精度があるみたいです。文字列変換するとデフォルトでナノ秒まで含んだ文字列になります。
class Time (Ruby 2.6.0)

PostgreSQLのTimestamp型

Timestamp型はマイクロ秒までの精度があるようです。
https://www.postgresql.jp/document/9.4/html/datatype-datetime.html

ということで問題の原因はこの精度の差でナノ秒の情報が欠落していたからでした。

なぜ今までテストが通っていたのか?

ここで1つ疑問が残ります。なぜ今までテストが通っていたのか?

DateTime型の精度は?

先に書きましたが、意図が分からない to_datetime が散らばっていました。もしかしてこいつが原因かもしれません。DateTime型の精度を調べてみると、、、
docs.ruby-lang.org
精度が記載されていません( ノД`)シクシク…
ただ、文字列にして表示してみるとナノ秒の精度があるっぽいです。もしかすると今まではActiveRecordで取ってきても to_datetime されてナノ秒を復旧させていたのかな…。いずれにせよ to_datetime したらテスト通るからという理由で to_datetime していたんじゃないか疑惑が浮上しました。レガシーシステムは闇が深いですね。そもそも end_of_day で保持して比較している時点で…以下略