AWSアカウントを取り違えないための試み

SREチームの長田です。

皆さんは操作するAWSアカウントを取り違えたことはありますか? 私はあります

カヤックのSREは複数のプロダクトを担当することも多く、 ひとつのプロダクトでも環境(本番、ステージング、開発、etc.)ごとにAWSアカウントを分ける場合があり、 扱わなければならないAWSアカウントが多くなる傾向にあります *1

今回はうっかり別のアカウントのリソースを削除してしまったーといったオペレーションミスを減らすために個人的に行っている、 「気をつける」以外の対策を紹介します。

間違いに気づくための対策

対象のアカウントが操作の対象として正しいかどうかは、結局は操作している本人にしか分かりません *2。 そのため、「アカウント取り違え自体をなくす」のではなく、 「アカウントを取り違えていることに気づきやすくする」ための対策をしています。

AWSコンソール用の対策

AWSコンソールには、ヘッダーにはアカウントIDしか表示されず、それも常時表示されているわけではないので、 現在どのアカウントでコンソールを開いているのかがわからない場合があります。

もちろんアカウント詳細ページまでたどれば確認はできるのですが、ページ遷移を伴うのは手間です。 面倒なことは人間サボりがちなので、できるだけ簡単に確認できるようにしなければなりません。

AWSコンソールのデフォルト表示

なお、上記のスクリーンショットはAWSコンソールにSSOでログインした場合の表示です。 Switch Roleを利用してログインした場合は、Switche Role時に設定したRole名が表示されるので多少認識しやすくなりますが、 最大5 Roleまでしか履歴が保存されないという制約があります*3。 6 Role以上Switchした場合は、古い設定は消えてしまいます。

ブラウザ拡張の利用

"Display AWS Account Name" というChorem拡張を導入しています。

AWSコンソールに現在選択中のアカウント情報を表示する拡張はいくつかありますが、 ヘッダーの色とアカウント情報を別々にハイライトすることができるという点で気に入っています。

Display AWS Account Nameの利用例

以下のスクリーンショットのように、「プロダクトごとの色」「環境の種類ごとの色」を組み合わせています。

Display AWS Account Nameの設定画面

AWS CLI用の対策

コマンドラインからAWSの操作を行う場合は、AWS Command Line Interface (AWS CLI)を使うことが多いでしょう。

https://docs.aws.amazon.com/cli/

AWS CLIを使う場合、AWSアカウントの指定は環境変数 AWS_PROFILE をセットするか、 コマンドライン引数 --profile を指定することで行います。

$ AWS_PROFILE=foo-production aws s3 ls
$ aws s3 ls --profile foo-production

なお、アカウント認証のためのprofile定義については、今回は割愛させていただきます。 AWS CLIのドキュメントに説明がありますので、 そちらを参照してください。

AWS CLIにおいてAWSアカウントを取り違えるということは、このprofile選択を取り違えるということです。 これに気づくための対策を3つほど紹介します。

環境変数を自動設定する

ディレクトリごとに環境変数を自動セットするツールを使っています。 有名なものに direnv があります。

私も長らく direnv を使っていましたが、 最近は mise の環境変数セット機能を使っています。

プロダクトの関連ディレクトリごとに AWS_PROFILE を設定することで、 「profileを選択する」という操作自体を省略してしまうのです。 操作しなければオペミスは発生しません。

例えば mise を使っている場合に、対象ディレクトリに以下の内容で .mise.toml ファイルを配置すれば、

[env]
AWS_PROFILE = 'foo-dev'

そのディレクトリ及びその配下で環境変数 AWS_PROFILE がセットされます。

$ mise set
key          value    source
AWS_PROFILE  foo-dev ~/src/github.com/kayac/test-foo/.mise.toml

$ printenv AWS_PROFILE
foo-dev

環境変数の自動設定は、AWS CLIだけでなく、TeraformCloudFormationなどのIaCツールにも有効です。

ターミナルのプロンプトにprofileを表示する

環境変数を自動セットするようにしたとして、その結果どのprofileが設定されているのかを確認する方法が欲しくなります。

私はshellとして fish を使っているので、 そのプラグインである "tide" でプロンプトをカスタマイズしています。

tideを導入するとデフォルトでプロンプトにprofile名が表示されるようになります。

プロンプト

もちろんfish以外のshellでも同様にプロンプトのカスタマイズが可能で、 例えばzshの場合は oh my zshのawsプラグインを使用すれば実現できるようです。

こだわる方はプロンプトを自作するのもいいでしょう。

AWS CLI実行時にprofileを表示する

「その場限りのprofileしたつもりができていなかった〜」という場合もあるでしょう。 私はあります

これを防ぐために、実行したAWS CLIコマンドがどのprofileを使ったのかを表示するツールを作りました。

awsc は、AWS CLIが使用するprofile情報を出力したうえで、すべての引数をそのまま使って exec aws します。

こんな設定を書くと、

rules:
  - expression: '-(production|pro?d)$'
    color: red
  - expression: '-(staging|stg)$'
    color: yellow
  - expression: '-(development|dev)$'
    color: blue
extra_info: true

以下のように色付きでprofileとアカウント情報を出力します。

awscの出力

出力行が多すぎるということであれば、テンプレートを定義することで出力をカスタマイズすることができます。 設定ファイルに以下のように書けば、

template: |-
  AWS_PROFILE={{ .Profile }}

このように出力される内容を抑制できます。

awscのテンプレートカスタマイズ

また、リソースの参照以外の操作を行った場合に、確認を求めるようにすることもできます。

実行確認

詳しくは awscREADME を参照してください。

まとめ

人間が操作する限り、オペレーションミスをゼロにすることはできません。 しかし、そのたびに対策をしていくことで、オペレーションミスを減らすことはできます。 失敗を糧に少しずつ改善していきましょう。

カヤックではオペレーションの改善が好きなエンジニアを募集しています!

*1:ちなみに、アカウント管理を受け持っている社内情シスチームでは社内で使用しているすべてのAWSアカウントを扱う可能性があるため、 100個以上のアカウントを適切に切り替える必要があります。 カヤックではクライアントワークを行っていることもあり、アカウント数が非常に多いのです。 そのあたりの話も聞いてみたいところです。

*2:このへんはAIに何とかしてほしいですね。

*3:執筆時点の制約です。

Perlbatross in YAPC::Hakodate 2024の結果発表と解説 #yapcjapan

こんにちは! 技術部の谷脇です。

去る10/5にYAPC::Hakodate 2024が開催されました。いかがでしたか? 

yapcjapan.org

以前に告知したように今回のYAPCもコードゴルフコンテストPerlbatrossを開催しました。

techblog.kayac.com

このエントリでは結果発表と、事前解答チームの川添(@acidlemon)より社内最短解の紹介と解説をお届けします。

Perlbatrossは現在コードの投稿や検証はできるものの、ランキングに載らないモードになっております。あと1週間程度はこのようにしておきますので、やりそびれた方や、ちょっと試してみたいなという方は是非コードゴルフに挑戦してみてください。

ここで宣伝

JavaScript版コードゴルフコンテスト JS体操はまだまだやっております!

hubspot.kayac.com

こちらも是非ご参加ください。

これはPerlbatrossの裏話ですが、JS体操の問題で生成したアスキーアートをPerlbatrossのHole2のテストに使っていました。

さらにランダム性を持たせるために回転もしていました。

回転するYAPCロゴ

結果発表

今回のPerlbatrossの優勝は「こーのいけ」さんでした!

Hole1が-36(64bytes), Hole2が-35(70bytes)でした。前回も参加していただいていたsugyanさんは惜しくも2位、そして続く3位にはkobakenさんが入りました。

全体の順位はPerlbatrossのサイトからご覧ください。

参加していただいた皆様には感謝を申し上げます。

では、ここからは@acidlemonさんにバトンタッチです。

社内最短解の紹介と解説

こんにちは @acidlemon です! 今回もPerlbatrossをお楽しみいただきありがとうございました。

前回はPerlbatrossのシステムを作るところにそこそこ時間がかかったのと、4問も作ってしまったので社内の事前解答があんまり進まないままParを決定したのでなかなかスコアが激しいことになっていましたが、今回は2問、事前解答の時間もまぁまぁ取れたということで大体2問とも60〜70byteくらいになりそうということがわかりPar100とPar105に設定しました。

それでは、社内の事前解答の最短解を簡単に解説していきます。この解答は私だけでなく @fujiwara@macopy の3人で短くしていったものです(偶然ですが、fujiwara組です)。

Hole 1: Portalbless Gramana

事前解答の最短解

  • スコア: -39 (61バイト)
map{%s=map{+"@{[sort/\w|\W{3}/g]}",1}split;print%s==1|0,$/}<>

参加者のみなさんの最短解はshebangを書いてPerlのオプションを設定していい感じにやるものでしたが、なんと社内ではshebang書いたら効くことに気付いていなかったということでただのワンライナーとしての最短を追求しています。一応、「perlコマンドのオプションで -a というのを使うと @F ってっていう変数に1行ずつ突っ込まれた状態でスクリプトが実行される仕様があるかとおもうんですが、これをPerlのスクリプト内で有効にする方法ってないですよね?」などといった話は社内でもしていたのですが… 組長に「shebangで指定するぐらいしかない気がする…」と言われてじゃあ無理だな! と諦めてしまっていました。

というのも、前回のYAPC::Hiroshima 2024のPerlbatrossは入力されたスクリプトをdoブロックにいれて実行する方式だったのでshebangが効かなかったのです。今回はチート抑止みたいなところの意図でperlコマンドを実行する方式になったので、ちゃんとshebangを書けば効くようになっていたのでした。

短縮の過程

アナグラムの判定は、文字ごとにソートしてHashのキーにして、最後にハッシュのキーを数える方法でやりました。この辺からスタートです。

for(<>){
  utf8::decode($_);                    # 文字列をUTF-8として扱う
  my %h;                               # ハッシュ %h を初期化しておく
  for(split /\s/, $_){                 # $_をスペースでsplitして、各単語ごとにループを回す
    $h{join("", sort(split //, $_))}++ # 単語を1文字ずつバラしてソートし結合したものを %hのキーにして値を設定
  }
  print %h==1 ? 1 : 0, "\n"            # ハッシュキーの個数が1個なら、アナグラム成立
}

可読性のためにコメント、改行、インデントが入ってますが、それを全部詰めるとちょうどParになります(my %hなども my%hと書けます)。

  • for(<>){ ... }map{ ... }<> に書き換える
  • 省略できる $_ は全て省略する
  • 余計なカッコ()を削る
  • printのところの三項演算子のところは真なら1、偽なら空になるのでOR演算子を使って偽のときだけ0にする

などをやっていくと、-19くらいまで短くなります。

map{utf8::decode$_;my%h;map{$h{join"",sort/./g}++}split/\s/;print%h==1||0,"\n"}<>

まだまだ削っていけるので、いろいろ削っていきます。

  • myは不要
  • mapで(key,1)のリストを返して直接 %h に代入できる
  • utf8::decodeutf8'decodeにできる(deprecated記法)
  • splitは第一引数を省略すると /\s+/ 相当になるので省略可能
  • %h==1||0 は Vertical Bar を1個省いて %h==1|0 にできる
  • "\n"$/ に置換可能
map{utf8'decode$_;%h=map{(join"",sort/./g),1}split;print%h==1|0,$/}<>

これで -31 です。ここで、同じバイト数の別解として内側のmapのところをjoinを使わずに書く方法もありました。

map{utf8'decode$_;%h=map{("@{[sort/./g]}",1)}split;print%h==1|0,$/}<>

今回の問題には utf8::decode をサボるとFailするようにするテストが入っています(test2の固定テスト、test3のランダムテスト両方)。UTF-8シーケンスとして評価するとアナグラムではないが、バイト列として評価するとアナグラム判定になるというものです。たとえば「あヵ」と「ヂふ」は見るからにアナグラムではないのですが、それぞれ6バイトのバイト列として1バイトずつアナグラム判定をしてしまうと構成バイトが同じ! となってしまいます。utf8::decode をサボるとこのテストが通らなくなるようになっています。

同じバイト列で構成されている違う文字

ということで、utf8::decode は簡単に外せないようになっています。ただ、問題文に単語の文字種は「英数・ひらがな・全角カタカナ」とかいてあるので、UTF-8的にはASCII範囲の1バイト or ひらがなカタカナの3バイトが来るということになります。これはうまいこと正規表現を書くと1バイトor3バイトのシーケンスで取り出していくことができます。

  • utf8::decodeを取る
  • sort/./gsort/\w|\W{3}/gに変更
map{%h=map{("@{[sort/\w|\W{3}/g]}",1)}split;print%h==1|0,$/}<>

これで -38になりました。最後に、内側のmapのところに記号がやたら固まっているのでこれなんとかできないかとperldocをよく読みました。 map{ ... } の内側の最初にカッコ()があるのはPerlインタプリタがmap EXPR,LISTmap BLOCK LIST を間違えてしまうのを防ぐためにカッコをつけている(BLOCKとして認識させている)のですが、これはPerldocによると + とかの単項演算子をおくだけでOKとのことだったのでカッコを無くしてプラスに置換しました。

map{%h=map{+"@{[sort/\w|\W{3}/g]}",1}split;print%h==1|0,$/}<>

…というのがカヤック社内最短解の解説でした。

なお、あとでまとめてリンクをご紹介しますが、参加いただいた方のWriteUpを読んで上のコードは %h==1%h<2 に出来ることに気付いたので、そこまでやると -40 となります。

別のアルゴリズムパターン

アナグラム判定でハッシュを使わず配列でやっていく方法も試したのですがやはりハッシュのほうが短くなりますね。配列でやる場合は、ソートした文字列を詰めた配列と、配列の1個目の要素を配列の個数分リピートした配列をスマートマッチで完全一致判定をみるみたいな感じでしょうか。配列同士のスマートマッチで書くならこんな感じです(これでももっと短くはなりそうです)。

map{utf8'decode$_;@a=map{join"",sort/./g}split;@b=($a[0])x@a;print@a~~@b|0,$/}<>

これで -20 です。

Hole 2: QuAAterPix

事前解答の最短解

  • スコア: -33 (72バイト)
for my($a,$b)(<>){print map{$a=~s/(.)(.)?//s;$2x2eq$_?$2:$1}$b=~/..?/sg}

for my($a,$b)(<>) という記法は Perl 5.36から利用できるようになった for my でリスト受け取りができるようになったという文法を利用しています。これ自体はwhile($a=<>,$b=<>) より1バイト短く書けます。ただ、この記法の弱点は myなので $_ に書き込むことが出来ないことです。

事前解答中には while($a=<>,$b=<>) 記法に気付いてなかったのですが、これを使って書き直すと$_に書き込むことが出来るのと、ループの中身が単文だと後置にできること、カッコも省略できること…がありまして

print(map{$a=~s/(.)(.)?//s;$2x2eq$_?$2:$1}/..?/sg)while$a=<>,$_=<>

これで -39になるので、こちらのほうが短かったですね。

短縮の過程

4つの文字を1文字にするアルゴリズムは、ルールに着目すると

  • 基本的に左上の文字を採用してよい(左上の文字が2回以上出現するなら左上の文字で確定)
  • ただし、左上文字が1回、残りの3文字が別の文字のときだけは残りの文字を採用する

という感じになります。ここからの説明では、左上、右上、左下、右下の順に1文字目〜4文字目と呼んでいます。

まずこのぐらいのコードからスタートします。コメントと改行空白を潰して+15のコードです。

for my($a,$b)(<>){
  while ($a =~ /(.)(.)/g) { # $a を2文字ずつ切り出してループを回す 
    # $1が1文字目, $2が2文字目
    # $cが3文字目と4文字目の2文字
    $c = substr($b, 0 , 2, ''); # substrで$bを2文字削っている
    
    if ($c eq $2.$2){
      # $2(1文字)と$c(2文字)がすべて同じ場合はその3文字を採用
      push @r, $2
    } else {
      # それ以外は最初に出現した文字を採用
      push @r, $1
    }
  }
  push @r, "\n" # 改行を追加
}
print @r

/gつきの正規表現を while で回すとマッチし続ける限りループが回るので、それを利用して1行目($a)を2文字ずつ切り出しながら2行目($b)もsubstrで切り出していきます。2行目を正規表現で切り出していくと$1$2が変わってしまうので、あえて正規表現を使わず切り出しています。

最初に整理したとおり、基本は1文字目(左上)を採用すればOKですが、の2〜4文字目が3文字とも同じときだけはそれを採用しないといけないというのを「2文字目を2回繰り返したものと3文字目4文字目が等しいかどうか」で確認しています。

ひとまず改行空白コメントを潰したものを短くしていきます。

for my($a,$b)(<>){while($a=~/(.)(.)/g){$c=substr($b,0,2,'');if($c eq$2.$2){push@r,$2}else{push@r,$1}}push@r,"\n"}print@r
  • $cは1回しか使わないので変数に受ける必要が無い
  • if文は三項演算子にできて、push @r の引数に$2 or $1を選び取る三項演算子を直接書ける
for my($a,$b)(<>){while($a=~/(.)(.)/g){push@r,substr($b,0,2,'')eq$2.$2?$2:$1}push@r,"\n"}print@r

これで-9になり、Parを切ることができました。

  • whileの中が文1個になったので、後置whileにできる
for my($a,$b)(<>){push@r,substr($b,0,2,'')eq$2.$2?$2:$1while$a=~/(.)(.)/g;push@r,"\n"}print@r

-12になりました。

  • 入力の最後に改行が入ってて、それを捨てるなんてもったいない! ということで正規表現を/s(シングルラインモード)にする
    • 行末の最後の改行1個だけ拾えるように、2文字目は任意(?つき)にする
    • 改行は $1 に入ってくるので三項演算子の条件を満たさず勝手に$1が入る
  • するとpushが1個になるので、もはやpushする必要が無い。その場でprintする
for my($a,$b)(<>){print substr($b,0,2,'')eq$2.$2?$2:$1while$a=~/(.)(.)?/sg}

-30になりました。

ここから風雲急なゴリッとした改造になりますが

  • substrを正規表現に置換したいので、$aでループを回すのではなく、$bで回すようにする
    • $b=~/..?/sg で回すことができて、3文字目4文字目として$_ を利用できる
  • ループの中に入れた $a は s///s で置換ループにして2文字ずつ削る
  • whileの中が1文じゃなくなるので、mapに変える
  • $2.$2$2x2 にすると1文字削れる
for my($a,$b)(<>){print map{$a=~s/(.)(.)?//s;$2x2eq$_?$2:$1}$b=~/..?/sg}

…ということで最後に大改造になりましたがこれで -33になりました。

別パターン

4文字を1文字にするところは、別パターンとして

  • 左上の優先度が高いので重み2で評価して出現が過半になっているものを採用する
    • [0,0], [0,0], [0,1], [1,0], [1,1] の5文字を連結して文字数カウントして3文字以上あるものを採用

とかもあります。そのパターンだと-24くらいになりました。

for my($a,$b)(<>){CORE::say map{$a=~s/(.).//;"$1$&$_"=~y/#//>2?"#":"`"}$b=~/../g}

社内最短解の紹介と解説は以上です。いかがでしたでしょうか?

謝辞

最後に、参加いただいた方々への謝辞を込めまして、皆様のWriteUpもこちらで紹介いたします。

広島に引き続いての2回目ではありますが、こういったゲリラ的な企画はみなさまの参加無しには成り立ちません。また、YAPCに参加できなかった方もPerlbatrossのようなオンラインで参加できる企画に来ていただいて、我々としてもこの企画をやる意義を感じました。みなさまご参加いただき本当にありがとうございました。