のべラボ.blog

Tech Blog | AWS や サーバーレスやコンテナ などなど

AWS Compute Optimizer で Lambda 関数の推奨事項を提示させてみた!

本記事は「AWS LambdaとServerless Advent Calendar 2023」3 日目の記事です。


今回は、AWS Compute Optimizer を使って Lambda 関数のメモリ設定に関する推奨事項を確認してみます!

目次


AWS Lambda 関数のメモリ設定の最適値導出について

AWS Lambda の Lambda 関数には使用するメモリ容量を設定できますが、この設定されたメモリ容量に比例して CPU パワーも割り当てられます。

docs.aws.amazon.com

また、メモリ容量の設定は、Lambda 関数のコストにも影響します。

aws.amazon.com

よって、Lambda 関数のメモリ容量を設定する場合は、パフォーマンスとコストの両面から最適値を導き出すことが重要となります。

そんな時には、下記のサービスやツールの使用を検討してみて下さい。

AWS Compute Optimizer

AWS Compute Optimizer は、Lambda 関数や、EC2 インスタンス、EC2 AutoScaling Group、EBS ボリュームなどの最適化について推奨事項を提示してくれるサービスです。

docs.aws.amazon.com

AWS Lambda Power Tuning

AWS Lambda Power Tuning は、Lambda 関数を対象にさまざまなメモリ割り当てを選択し、実行時間とコストを測定することで最適値の分析を支援してくれるツールです。 具体的には、AWS Step Functions のステートマシンを使用し、測定対象の Lambda 関数を実行して分析用のデータを収集します。

docs.aws.amazon.com

AWS Lambda Power Tuning は、筆者も AWS のサーバーレスのトレーニング登壇時によくデモを実施しているのですが、この記事では、AWS Compute Optimizer に着目していきたいと思います!


AWS Compute Optimizer における Lambda 関数の要件

以下のドキュメントにも記載がありますが、AWS Compute Optimizer で Lambda 関数の推奨を提示させるにはいくつか要件があります。

docs.aws.amazon.com

要件

  • Lambda 関数に設定されているメモリは 1,792 MB 以下であること

  • Lambda 関数は過去 14 日間に少なくとも 50 回呼び出されていること

2023 年 12 月現在、Lambda 関数には最大 10,240 MB のメモリを設定できるので、「1,782 MB 以下であること」という要件には少し疑問が残りましたが、今回はこの要件にマッチする Lambda 関数を用意して、推奨事項を提示してくれるか確認してみることにしました。

この要件をみると、AWS Compute Optimizer は、AWS Lambda Power Tuning のように、その場で Lambda 関数を実行してすぐに結果を出すのではなく、ある程度の期間、実行された Lambda 関数の情報から推奨事項を出してくれるので、現在、運用中の本番環境のワークロードを対象にしやすいというメリットがありますね。(本番環境で推奨事項を出すために AWS Lambda Power Tuning で実際に Lambda 関数を実行するのは、一般的には避けたいですよね。)


測定対象の Lambda 関数

今回は、測定対象として以下の Lambda 関数を Python で実装しました。

名前 設定メモリ(MB) 処理内容
demo-memory-under-provisioned 128 設定値である 128 MB ギリギリまでメモリを使用する(メモリ割り当てが十分でない状況にする)
demo-OOM 128 メモリ不足でエラーが発生する処理をする(関数エラーが発生する状況にする)
My-CPU-eater-function 128 1 回の呼出しで CPU を 5 秒間使用し続ける

上の表のうち、demo-memory-under-provisioned は、Lambda 関数で使用するメモリが設定値ギリギリまで使用するコードになっていることは、ログで確認できています。 下記はこの Lambda 関数実行時の REPORT ログですが、Memory Size の値と Max Memory Used の値が同じ になっていますね。ただ、メモリ不足エラーが起こらないように、あくまでギリギリにしています。

REPORT RequestId: xxx    Duration: 2229.80 ms    Billed Duration: 2230 ms      Memory Size: 128 MB   Max Memory Used: 128 MB

これらの Lambda 関数を Amazon Event Bridge のスケジューラを使用して定期的に実行して、14 日以上経過した後に AWS Compute Optimizer で推奨事項が表示されるか確認してみます。

(追記:メモリ割り当てが過剰な Lambda 関数についてもテストしようとしたのですが、なぜか今回の検証では Compute Optimizer でうまく検知させることができませんでしたので、これはあらためて調査したいと思います!mm)


結果

結論からすると、メモリ割り当てが不十分なもの、CPU を 5 秒間消費するものについては、推奨事項が表示されました。 ただ、メモリ不足でエラーが発生したものについては、推奨事項は表示されませんでした。

AWS マネジメントコンソールで AWS Compute Optimizer のダッシュボードでは、次のように表示されました。

レコメンデーションの表示

上の図のダッシュボードで、「レコメンデーションを表示」をクリックすると、次のように各関数毎の情報が表示されます。(下記は抜粋です。)

  • Lambda 関数のバージョンや検出理由

  • Lambda 関数の現在設定されているメモリと推奨設定メモリ

推奨値を出してくれるのは、ありがたいですね!

  • Lambda 関数の現在のコストと推奨コストや、そのコスト差

推奨値を適用した場合のコスト予測も出してくれるので、パフォーマンスだけでなくコストも含めて分析できます。


まとめ

AWS Compute Optimizer が、以下のような Lambda 関数について推奨事項を提示してくれることを確認できました。

  • メモリの割り当てが不十分な Lambda 関数
  • CPUを多く消費している Lambda 関数

ただ、今回のテストケースでは、メモリ不足で関数エラーが発生した Lambda 関数については推奨事項が提示されることはありませんでした。ただこのケースでは、関数エラーが発生しているのですぐにログで問題を検出できるため、Compute Optimizer で検出させるまでもない、と考えることもできますね。


所感

普段は、デモの実施のしやすさから AWS Lambda Power Tuning を説明する機会が多いのですが、今回、AWS Compute Optimizer について検証し、新たな気づきを得ることができたので、AWS Compute Optimizer についても積極的に情報発信していこうと思います!


Python 3エンジニア認定 基礎試験の勉強記録:関数の引数編

先週から引き続き、あらためて Python を体系的に勉強してみて学べたことや、メモとして残しておきたいことなどを記載していきます。今回は 関数の引数編 です。

学習のゴールとして、Python 3エンジニア認定 基礎試験 の合格ですが、Python というプログラミング言語の仕様の全てや、試験合格のコツを解説するのではなく、あくまで自分が気づけたことのメモであることはご了承ください!

この試験ですが、先日受験して合格できました! 今回の記事では、Python の学習や試験を受けて合格するまで振返ってみた所感も記述します。


目次


引数のデフォルト値

まず、次のコードをみてみましょう。

a = 1

def hello(x = a):
    return x

a = 2
res = hello()
print(res)

このコードを実行した結果はどうなるでしょうか? 2 と表示される?それとも 1 と表示される?

正解は、1 と表示されます。

a という変数には、最終的に 2 が代入されていて、その後に hello 関数を呼び出しているから 2 が表示されるんじゃないの?と考えてしまうかもしれません。

ただ、コードを最初から順番にみていきましょう。最初は、 a = 1 で a に 1 を代入しています。

次に、hello 関数を定義しています。この定義の段階で、仮引数 x にデフォルト値として 変数の a の値を設定しています。この時の a の値は 1 なので、x には 1が代入されています。

その後、a に 2 を再代入したとしても、hello 関数の仮引数 x の値は変わらないので、hello 関数を引数を設定せずに呼び出した場合は、デフォルトの値 1 が表示されます。

仮引数のデフォルト値が決定するタイミングの理解が重要ですね!


引数の位置とキーワード

次のような関数があったとします。

def print_param(x,y,z):
    print(x,y,z)

この関数に引数を設定して呼び出す方法として、次の a から e のうち 誤っているもの2 つ あります。どれとどれでしょうか?

  1. print_param(1, y=2, z=3)
  2. print_param(1, 2, z=3)
  3. print_param(x=1, y=2, 3)
  4. print_param(x =1, 2, z=3)
  5. print_param(y=2, z=3, x=1)

いかがでしょうか?

どの選択肢も、引数を 3つ指定しています。また、キーワード引数、つまり x=1 と引数の値を指定してるものもありますが、 仮引数名 (x, y, z) を間違えているわけではないですよね。

ただし、キーワード引数と、位置引数1 のように引数の値だけを記載するもの)を混在して使っているものがあります。

この問題を解くための重要なポイントは、キーワード引数の後に位置引数を指定できない というルールです。

  • 選択肢 A では、キーワード引数 は、y=2z=3 ですが、これらは 1 という 位置引数の後 に指定されているので、正しい指定となります。
  • 選択肢 B も、同様ですね。
  • 選択肢 C はどうでしょうか? キーワード引数 x=1y=2 は、位置引数 3 の前に指定されています。これは誤った指定となります。
  • 選択肢 D も、選択肢C と同様で、キーワード引数が 位置引数の前に指定されているので誤りです。
  • 選択肢 E は、位置引数がないので問題ありません。

ということで、誤っている選択肢は、CD となります。


可変長引数

次はメモとして記載しています。仮引数に * をつけることで、複数の値を タプル として受け取ることができます。

def print_as_tapple(*args):
    print(args)
    
print_as_tapple("a","b","c")

このコードを実行すると、次のように a, b, c の値をもつ タプルとして 表示されます。

('a', 'b', 'c')

また、仮引数に ** をつけることで、複数の値を 辞書型 として受け取ることができます。

def print_as_dict(**args):
    print(args)
    
print_as_dict(a="1",b="2",c="3")

このコードを実行すると、次のように 辞書型として 表示されます。

{'a': '1', 'b': '2', 'c': '3'}

引数のアンパック

次のコードをみてみましょう。

def greeting(name):
    print("Hello!" + name)

【A】
greeting(**me)

このコードで、Hello!Alex という結果を得るには、【A】にはどのようなコードを挿入すればいいでしょうか?

次の選択肢の中から 1 つ選択して下さい。

  1. me = "name=Alex"
  2. me = ("name","Alex")
  3. me = {"name" : "Alex"}
  4. me = "["name","Alex"]"

これは、引数のアンパック に関する問題です。コードの 4 行目で greeting 関数の引数にアスタリスクを 2 つつけて **me と指定しています。

これにより、me の辞書型のキーを、キーワード引数の名前として指定して関数に値を渡すことができます。

つまり辞書型を指定する必要があるので、選択肢 C が正解になります。

greeting 関数の仮引数名は name なので、辞書型のキーの名前と一致する必要があります。

選択肢 C では、辞書型のキーは name で、値が Alex です。キーの name は 仮引数名と一致してますよね。

ちなみに、引数が {"name" : "Alex", "address" : "Kyoto"} の場合は、name 以外のキーも含んでいるので、実行時に次のエラーが発生します。

TypeError: greeting() got an unexpected keyword argument 'address'

参考までに、次のコードは問題なく動作して、Hello!John Doe と表示されます。

def greeting_fullname(fname,lname):
    print("Hello!" + fname + " " + lname)

me = {"fname" : "John", "lname" : "Doe"}
greeting_fullname(**me)

勉強開始から合格までの振り返り

さて、3 回にわたり Python 3エンジニア認定 基礎試験の勉強記録を投稿してきました。

振返ってみると、もともとは、「Python を独学ではなく、体系的に学んでスキルを強化したい!」という思いから始めた取組みでした。

これまで、サンプルなどを参考に Python のコードを書く機会は多々ありましたが、わからないことがあれば、その都度、その部分だけ調べるという取り組みをしてきたので、体系的な学習ができていませんでした。

今回、体系的に Python を学習することで、中途半端に理解していた部分(まさにブログに書いてきた部分)を正確に理解することができたので、とても収穫が大きかったと思ってます。

試験については、市販の問題集や Web から利用できる模擬試験を活用して勉強しました。

そこでわからなかった問題や、間違えた問題は、実際にコードを書いてみて動作を確認するようにしました。また、その際、「もし、コードをこう変えたらどうなるだろう?」というアレンジも積極的に行いました。そのアレンジが、このブログで紹介したサンプルコードになっています。

こういった取り組みはけっこう時間はかかりますが、その分、自分の理解をより深めることができす。

試験本番の直前に受けた模擬試験では、自分にとって難しい問題が多く点数が 730 (合格点は 700) とギリギリだったので、「もしかしたら、危ないかな」ともあせりましたが、実際に試験を受けてみると、難易度は模擬試験より低かったように感じました。(あくまで個人の感想です。たまたま自分にとって簡単だった問題が多かっただけかもしれません。)

そのため、20分ほどで試験を終了して、875 点で合格することができました。

合格してホッとしましたが、同時に「これ以降、Python を積極的に勉強するモチベーションを維持できるかな」という懸念も出てきました。

他にも Dive Deep したいプログラミング言語はあるのですが、せっかくなので Python の勉強は継続したいなと思ってます。また何か Python における テーマをみつけて、そのテーマに沿って学んで、ブログに書いていきます!

そして、今回のチャレンジであらためて感じたことは、プログラミングは楽しい! コードを書くのは楽しい! ということです。

いろんな仕事に忙殺されてコードを書くことが少なくなっているのですが、もっと積極的にコードを書きたい!それが楽しい!と気づけたことも大きな収穫でした!

今後もプログラミングして、コードを書くことを楽しみたいです!


Python 3エンジニア認定 基礎試験の勉強記録:式の評価編

先週から引き続き、あらためて Python を体系的に勉強してみて学べたことや、メモとして残しておきたいことなどを記載していきます。今回は 式の評価編 です。

さしあたってのゴールは、Python 3エンジニア認定 基礎試験 の合格ですが、Python というプログラミング言語の仕様の全てや、試験合格のコツを解説するのではなく、あくまで自分が気づけたことのメモであることはご了承ください!


目次


整数の真偽評価

整数を対象にした真偽評価は、ふだん使うことがないので練習問題ではひっかかってしまいました 💦 よってメモしておきます。

次のサンプルコードを実行すると、どのような結果になるでしょう?

result1 = 0 and 2 and -1
result2 = 0 or  2 or  -1
print(result1)
print(result2)

これを理解するには 2 つのポイントがあります。

まず 1 つは、Python において 整数を真偽評価する場合、0 が 偽(False) であり、0以外が 真 (True) であるということです。

そしてもう1つは、andor をつかった真偽評価では、短絡評価となるという点です。

例えば、x and y の場合、まず x の評価を行い、結果が偽 (False) であれば、それ以上の評価は行わず、結果は x となります。 つまり、y の評価はパスされるわけです。もし x の評価が真 (True) の場合、y の評価も行われます。

ではそれをふまえて サンプル result1 = 0 and 2 and -1 を考えてみるとどうなるでしょう?

  • まず、0 and 20 を評価します。

  • 0 は、偽 (False) なので、そこで評価は終わり、結果は 0 となります。

  • つまり、print(result1) の結果は、0 となるわけです。

次に、or ですが、x or y の場合、まず x の評価を行い、結果が真 (True) であれば、それ以上の評価は行わず、結果は x となります。 つまり、y の評価はパスされるわけです。もし x の評価が偽 (False) の場合、y の評価も行われます。

では サンプル result2 = 0 or 2 or -1 を考えてみるとどうなるでしょう?

  • まず、0 or 20 を評価します。

  • 0 は、偽 (False) なので、次に 2 を評価します。

  • 2 は、真 (True) なので、そこで評価は終わり、結果は 2 となります。

  • つまり、print(result2) の結果は、2 となるわけです。

よって、サンプルコードの結果は、下記のようになります。

0
2

Python 以外のプログラミング言語の知識があると、0 は False か True かの判定で混乱するかもしれませんね。気をつけたいです。


set を使用した集合演算

set はコレクションの一種で、下記のような特性をもちます。

  • 要素を重複して保持しない
  • 要素の挿入時の順序や位置を記録しない(順不同)
  • 要素を追加する時は、add( ) を使う
    • list 型の場合は append ( ) を使うので、間違えて覚えないように気をつけましょう!

また set を使用した集合演算も可能です。次のサンプルコードがどのような結果になるか考えてみましょう。

myset1 = {'1','2','3','4','5','6','7','8','9'}
myset2 = {'2','4','6','8','a','b','c'}

print(myset1 - myset2)
print(myset1 & myset2)
print(myset1 | myset2)
print(myset1 ^ myset2) 

set を使用した集合演算では、演算子の意味を理解したうえでベン図で考えるとわかりやすいです。

3 行目の print(myset1 - myset2) ですが、これは 差集合 の演算を行っています。

ベン図で書くと、背景色がついている部分が結果となります。

4 行目の print(myset1 & myset2)積集合 の演算を行っています。

ベン図で書くと、背景色がついている部分、つまり myset1 と myset2 の両方に重複して存在している要素が結果となります。

5 行目の print(myset1 & myset2)和集合 の演算を行っています。

ベン図で書くと、背景色がついている部分、つまり myset1 と myset2 のすべての要素が結果となります。

6 行目の print(myset1 ^ myset2)対称差集合 の演算を行っています。

ベン図で書くと、背景色がついている部分、つまり 積集合を除く部分が結果となります。

以上、まとめるとサンプルコードの実行結果(例)は下記になります。

{'9', '5', '1', '7', '3'}
{'2', '4', '6', '8'}
{'4', '9', '5', 'c', '6', 'b', 'a', '8', '1', '2', '7', '3'}
{'5', 'c', 'a', '1', '7', '9', 'b', '3'}

上記はあくまで例です。set では要素の順序は維持されないので出力する時も要素の順は不確定ですが、要素の値としてはベン図の通りになっていますね。


先日、Python 3エンジニア認定 基礎試験 の申し込みを完了して受験日が確定しました!

普段、よく受けている AWS の認定とは申し込み方法が違い、慣れていないので少し苦労しましたが受験日が決まったので計画的に勉強を進めて合格を目指します!


Python 3エンジニア認定 基礎試験の勉強記録:基本編

Python はこれまで長い間、独学で なんとなく コードを書いてきたんですが、体系的に学んで理解を深めたいと思い至りました。

ただ、目標設定をせずにプログラミング言語を学ぶと途中で飽きてしまいそうなので、Python 3エンジニア認定 基礎試験 の合格を目標することにしました。

このブログでは、あらためて Python を体系的に勉強してみて学べたことや、メモとして残しておきたいことなどを記載していきます。今回は 基本編 です。

Python というプログラミング言語の仕様の全てや、試験合格のコツを解説するのではなく、あくまで自分が気づけたことのメモであることはご了承ください!

なお、Python 3 エンジニア認定 基礎試験 の試験情報については下記を参照してください。

cbt.odyssey-com.co.jp


目次


Python プログラミングのスタイルガイド

Python では、プログラミングにおける推奨事項をまとめたスタイルガイド PEP (Python Enhancement Proposals) があります。この PEP の 8番目である通称 PEP 8 で最低限、覚えておこうと思ったものを列記します。

peps.python.org

  • インデント レベルごとに 4 つのスペースを使用する
  • インデントではタブよりもスペースの方がのぞましい
  • 1行の長さを最大79文字までに制限する
  • 関数や変数の名前は小文字にする。読みやすくするために必要に応じて単語をアンダースコアで区切る

PEP 8 を読んでみると、何より重視しているのは 可読性 であることがわかります。スタイルガイドでありながら、ガイドラインに従うと可読性を損なわれる場合は敢えてガイドラインに準拠しないことも検討すべしと記載されています。実プロジェクトではチームでルールを決めることもありますし、可読性の判断基準も人によって異なる場合があるので、特定のガイドラインだけでガチガチに縛らない、というのは重要なことですね。


除算演算子 / による演算結果

試験の練習問題でひっかかってしまったので、シンプルな例でメモしておきます。

result = 5 / 5
print(result)

上記のコードで、print 関数の実行結果はどうなるでしょう? 正解は、1.0 になります。つまり、除算の対象が整数同士であっても結果は浮動小数点型になります。 1 ではないので注意したいです。


文字列のスライス

次のサンプルコードをみてみましょう。

message = "Hello"

sliced_message1 = message[2:4]
print(sliced_message1)

sliced_message2 = message[-3:]
print(sliced_message2)

このサンプルのように、文字列をスライスする場合は、下図のように 0 から番号をつけた区切り線をいれる とわかりやすいです。

サンプルコードだと、sliced_message1 には、2 番の区切り線から 4 番の区切り線の間の文字が代入されるので、ll になります。 スライスする数字がマイナスの場合は、文字列の最後から 1文字目に -1 の区切り線とします。そうすると、sliced_message2 には、-3番の区切り線からすべての文字が代入されるので、llo になるわけです。

文字列のスライスにおいて、リストのインデックスのように 1 つ 1 つの文字に番号をつけて考えた場合だとどうでしょう? その場合、サンプルでは、H がインデックス 0で、e が インデックス 1となり、[2:4] の指定なら インデックス 2 の l を含んで インデックス 4 の o は含まないことになります。 よって、[2:4] なんだけど、対象になるのは 2 から 4 までではなく 2 から 3 までと覚える必要があります。 それよりも、上図のように番号をつけた区切り線をいれて、その番号で考えたほうがシンプルといえます。


format メソッド

文字列のフォーマット指定に使用できる format メソッドのパターンをメモしておきます。

num1 = 10
num2 = 20

# これは OK
print("num1は {0} です。num2は {1} です。".format(num1,num2))
print("num1は {} です。num2は {} です。".format(num1,num2))
print("num1は {val1} です。num2は {val2} です。".format(val1=num1,val2=num2))
print("num1は {num1} です。num2は {num2} です。".format(num1=num1,num2=num2))

# これは NG
print("num1は {num1} です。num2は {num2} です。".format(num1,num2))

上記のサンプルコードの 4 行目から 7行目はすべて num1は 10 です。num2は 20 です。 と表示されますが、 9 行目では KeyError になります。 { } (フォーマットフィールド) には、引数の変数名は、そのまま記述できないということですね。その点は f 文字列を使用したフォーマットとは違うので、ごっちゃにしないように注意したいです。

num1 = 10
num2 = 20

#  f 文字列を使ったフォーマット
print(f"num1は {num1} です。num2は {num2} です。")

やはり 体系的に学ぶ ことで、これまでは正確に理解できていなかったことを学べてよい刺激になりますね! 今回は基本編でしたが、次回も引き続き Python 3エンジニア認定 基礎試験の勉強記録を書く予定です!


Amazon API Gateway のリクエスト本文検証時のエラーメッセージを変更してみる

Amazon API Gateway のリクエスト検証機能は広く知られているかもしれませんが、今回はリクエスト本文検証時のエラーメッセージを変更して詳細な情報を表示する方法を紹介します。

これは、数ヵ月前に私が登壇したトレーニングの受講者の質問から気づけた Tips です。(整理してブログで紹介しようと思っていながら遅れに遅れてしまいました 🙇‍♂️)

なお、この記事の内容は 2023 年 5 月に検証した内容に基づきます。


目次


Amazon API Gateway のリクエスト検証

Amazon API GatewayREST API では、API のリクエスト内容を検証できます。

docs.aws.amazon.com

例えば、特定のクエリ文字列パラメータや HTTP ヘッダ を必須とした場合、それがリクエストに含まれているかを検証したり、リクエスト本文が特定のフォーマットになっているかを検証できます。

今回は Amazon API GatewayAPI のサンプルとして使用できる PetStore API を使用して、リクエスト検証の機能についてみていきましょう。その後、リクエスト本文の検証時のエラーメッセージを変更していきます!


PetStore API の作成

AWS マネジメントコンソールから Amazon API Gateway のコンソールを表示し、API の作成を開始します。その際、API タイプとして [ REST API ] を選択して作成します。( [ REST API プライベート] ではないのでご注意ください。)

その後、下図のように [ API の例 ] を選択して ページ右下の[インポート] を選択します。

これで、サンプルである PetStore API が作成できました。


リクエストのクエリ文字列パラメータの検証

この PetStore API に必須のクエリ文字列を設定してみます。(もちろん、これはリクエストの検証機能を試すための設定で、PetStore API としては本来は必要ないものです。)

下図のように、[リソース] 、/pets の POST メソッド、[メソッドリクエス] の順で選択します。

そして、[リクエストの検証] で鉛筆アイコンをクリックして、 [本文、クエリ文字列パラメータ、およびヘッダーの検証] を選択し、右横のチェックマークをクリックします。これを選択したのは、あとで本文の検証も確認するためです。

[URL クエリ文字列パラメータ ] を展開表示して、[クエリ文字列の追加] を選択します。

鉛筆アイコンをクリックした後に表示される入力エリアに、クエリ文字列パラメータ名を入力します。今回はテスト用なのでどんな名前でもいいのですが、ここでは プレースホルダーに表示されている通り myQueryStringにしておきます。その後チェックマークをクリックして、[必須] にチェックをしておきましょう。

この設定により、PetStore API で /pets のパスで POST メソッドの API を発行する時は、クエリ文字列パラメータ名 として myQueryString が必須になりました。リクエストは検証され、もしこのパラメータが無い場合は、エラーとなるように設定できました。

この後、[アクション] から [API のデプロイ] を選択して、[デプロイされるステージ] として [新しいステージ] を選択します。[ステージ名] は任意ですがここでは [dev] と指定して [デプロイ] を選択します。

その後表示される [ URL の呼び出し:] の URL をメモしておきます。東京リージョンを使用している場合は、下記のような URL になっているはずです。

https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

では curl コマンドを使用して API を実行してみます。

まず、myQueryString クエリ文字列パラメータを含めたリクエストを発行してみます。

curl -X POST  "<メモしておいたAmazon API Gateway のURL>/pets/?myQueryString=dummy" -H "Content-Type: application/json" -d '{ "type": "dog", "price": 50000  }' 

リクエストは正常に処理され、下記のようなレスポンスが表示されます。

  "pet": {
    "type": "dog",
    "price": 50000
  },
  "message": "success"
}

次にmyQueryString クエリ文字列パラメータなしでリクエストを発行してみます。

curl -X POST "<メモしておいたAmazon API Gateway のURL>/pets" -H "Content-Type: application/json" -d '{ "type": "dog", "price": 50000  }'  

下記のようなレスポンスが表示されます。メッセージをみると、必須パラメータが指定されていないということがわかります。

message": "Missing required request parameters: [myQueryString]"}

Amazon API Gateway のリクエスト検証が期待通りに動作していることが確認できました。


リクエストの本文の検証

次はリクエスト本文の検証を試しますが、PetStore API は、下図のようにすでに本文の検証が設定されています。

上図では [モデル名] として [NewPet] が指定されており、この NewPet モデルで定義されたスキーマに基づき本文の内容が検証されます。

このモデルの定義は、Amazon API Gateway のコンソールの左側のメニューから [モデル] を選択することで確認できます。

このモデルが参照している Type として PetType がありますが、これもコンソールから参照できます。

つまり、リクエスト本文のコンテンツタイプは application/json で、type キーと price キーが必要、また type の値は 文字列で "dog", "cat", "fish", "bird", "gecko" のいずれかでなくてはいけません。さらに price は数値でなくてはいけない、ということになります。

では、curl コマンドで確認します。正常処理はさきほどのクエリ文字列パラメータのテストで確認できているので、ここではスキーマに合致しない本文データを指定してみます。

curl -X POST "<メモしておいたAmazon API Gateway のURL>/pets/?myQueryString=dummy" -H "Content-Type: application/json" -d '{ "type": "tiger" ,"price": 50000  }' 

リクエストは検証され、スキーマに合致しないのでエラーになりました。

{"message": "Invalid request body"}

ただし、なぜエラーになったのか知りたい場合はどうすればいいでしょうか?この Invalid request body というエラーメッセージだけでは、本文のどこが正しくないのか判別できません。 もちろん、要件的にそれで問題ない場合もありますが、開発やテストフェーズでは検証エラーの理由を確認できた方が便利な場合もあります。

そこで、このデフォルトのエラーメッセージを変更してみます!


リクエストの本文の検証時のエラーメッセージの変更

下図のように、Amazon API Gateway のコンソールの左側のメニューで [ゲートウェイのレスポンス] 、[リクエスト本文が不正です] をクリックして、ページ右上の [編集] をクリックします。

[レスポンステンプレート] で [application/json] を選択して、[レスポンステンプレートの本文] を {"message":$context.error.validationErrorString} に変更して [保存] します。

$context.error.validationErrorString は、$context 変数とよばれるものの一種で、これを使用することで詳細な検証エラーメッセージを表示できます。

docs.aws.amazon.com

この変更後、Amazon API Gateway のコンソールの左側のメニューで [リソース] を選択して、もう一度 dev ステージを指定して API のデプロイを行ってください。

では、さきほどと同じスキーマに合致しないリクエストを発行してみます。

curl -X POST "<メモしておいたAmazon API Gateway のURL>/dev/pets/?myQueryString=dummy" -H "Content-Type: application/json" -d '{ "type": "tiger" ,"price": 50000  }' 

今回は下記のように、詳細なエラーメッセージを確認することができました。

message":[instance value (\"tiger\") not found in enum (possible values: [\"dog\",\"cat\",\"fish\",\"bird\",\"gecko\"])]}

その他、下記のようなパターンでリクエスト検証を試してみましたが、スキーマに合致しないリクエストはすべて詳細なエラーメッセージを表示することができました。

price に数値以外を指定した場合

curl -X POST "<メモしておいたAmazon API Gateway のURL>/pets/?myQueryString=dummy" -H "Content-Type: application/json" -d '{ "type": "dog", "price": "a"  }' 
message":[instance type (string) does not match any allowed primitive type (allowed: [\"integer\",\"number\"])]}

必要なキー( この例では price )がない場合

curl -X POST "<メモしておいたAmazon API Gateway のURL>/pets/?myQueryString=dummy" -H "Content-Type: application/json" -d '{ "type": "dog" }' 
  "errors": [
    {
      "key": "Pet2.price",
      "message": "Missing required field"
    }
  ]
}

今回の所感

これまで Amazon API Gateway のリクエスト検証の機能は触ったことがありましたが、ゲートウェイのレスポンス$ コンテキスト変数 についてはあまり触ったことがないため、よい気づきを得ることができました。 シンプルに Amazon API Gateway のリクエスト検証を使うだけでなく、さらに一歩進んだ Tips として活用していきたいと思います!


Amazon EKS に Container Insights を導入する手順をまとめてみた

Amazon EKS には、Amazon CloudWatch Container Insights (以降、Container Insights )を導入できます。 Container Insights を導入すると、Amazon EKS クラスターやノード、Pod などといった単位でメトリクスを収集し、Amazon CloudWatch アラームを設定できます。またログを収集して Amazon CloudWatch Logs で参照・分析することもできるので、AWS に慣れた人には Amazon EKS のモニタリングやロギングを行う仕組みとして取り組みやすいのではないでしょうか。

docs.aws.amazon.com

Amazon EKS クラスターに Container Insights を導入する手順は下記の AWS のドキュメントに記載されています。

docs.aws.amazon.com

ただこのドキュメントに目を通していて、個人的に「おや?」と思いました。 というのも、メトリクスやログを収集する Agent に Amazon CloudWatch へのアクセスを許可するという重要な部分の設定については他のドキュメントを読んで実施してね、という流れになっているからです。

個人的には、何かを操作する手順をドキュメントに記載するときには、そのページに必要な説明や手順をすべて掲載し、読者は 1つのページを上から下に読めば必要な手順を理解・実行できるようにすべきと思っています。 なので、自分のメモ用に、もしくは他の人への説明用に、具体的な手順をまとめておこうと思い立ちました。

なお、今回まとめた手順は、2023年4月時点の検証に基づいています。 Amazon EKS クラスターと、Amazon EC2 インスタンスを使用するマネージドノードグループも作成済です。また、メトリクスやログを収集する Agent として CloudWatch AgentFluent Bit の Agent を使用する前提とします。


目次


Agent への Amazon CloudWatch へのアクセスの許可について

Amazon EKS では、 CloudWatch AgentFluent Bit の AgentKubernetes の DaemonSet で管理される Pod として実行されます。これらの Agentに Amazon CloudWatch へのアクセスを許可する方法は 2種類です。 どちらも AWS IAM の管理ポリシーである CloudWatchAgentServerPolicy を設定した IAM ロールを使用しますが、その IAM ロールの適用方法が異なります。

1. ノードである EC2 インスタンスに IAM のロールを設定する。

この方法は設定自体はシンプルですが、Agent 以外の Pod にも Amazon CloudWatch へのアクセスを許可してしまうことになります。これは最小権限の原則の観点から望ましくありません。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/neob/20230410/20230410083759.png

2. Agent の Pod に IAM ロール と関連付けた Service Account を設定する。

この方法の場合、Service Account を設定した Pod だけに Amazon CloudWatch へのアクセスを許可できます。よって今回は、こちらの方法を使用する前提とします。

https://cdn-ak.f.st-hatena.com/images/fotolife/n/neob/20230410/20230410083840.png


手順 1. クイックスタートセットアップの実施

この手順は、次のドキュメント通りに実施することで完了できます。

docs.aws.amazon.com

下記は、クラスター名が test-cluster という東京リージョンの Amazon EKS クラスターにクイックスタートセットアップを実施する例です。

ClusterName=test-cluster
RegionName=ap-northeast-1
FluentBitHttpPort='2020'
FluentBitReadFromHead='Off'
[[ ${FluentBitReadFromHead} = 'On' ]] && FluentBitReadFromTail='Off'|| FluentBitReadFromTail='On'
[[ -z ${FluentBitHttpPort} ]] && FluentBitHttpServer='Off' || FluentBitHttpServer='On'
curl https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed 's/{{cluster_name}}/'${ClusterName}'/;s/{{region_name}}/'${RegionName}'/;s/{{http_server_toggle}}/"'${FluentBitHttpServer}'"/;s/{{http_server_port}}/"'${FluentBitHttpPort}'"/;s/{{read_from_head}}/"'${FluentBitReadFromHead}'"/;s/{{read_from_tail}}/"'${FluentBitReadFromTail}'"/' | kubectl apply -f - 

この手順により amazon-cloudwatch という名前の Namespace が作成され、主要なオブジェクトが作成されます。

Agent となる Pod の起動を次のコマンドで確認します。

kubectl get pods -n amazon-cloudwatch

Pod は DaemonSet で管理されているので、ノード数分の Pod が表示されれば OK です。

NAME                     READY   STATUS    RESTARTS   AGE
cloudwatch-agent-5b4hb   1/1     Running   0          2m22s
cloudwatch-agent-6n5qc   1/1     Running   0          2m22s
fluent-bit-d2gfh         1/1     Running   0          2m22s
fluent-bit-hncw9         1/1     Running   0          2m22s

手順 2. IRSA(IAM Roles for Service Accounts)による既存の Service Accountのオーバーライド

次に Agent の Pod に Amazon CloudWatch へのアクセスを許可します。

手順 1. が完了した段階で、CloudWatch Agent の Pod には、cloudwatch-agent という Service Accountが、Fluent Bit の Agent には fluent-bit という Service Account が設定されています。

これらの Service Account に、AWS IAM のロールを関連付ける必要があります。 これは、Amazon EKS でサポートされている IRSA(IAM Roles for Service Accounts) という仕組みを使用することで実現できます。

IRSA を使用するには、まず Amazon EKS クラスターの OpenID Connect プロバイダ (OIDC プロバイダ)を AWS IAM に登録します。 これは次のコマンドにて行えますが、クラスター毎に 1 回実施すれば OK です。

eksctl utils associate-iam-oidc-provider --cluster ${ClusterName} --approve

次に Service Account cloudwatch-agent に CloudWatchAgentServerPolicy を設定した IAM ロールを設定します。

eksctl create iamserviceaccount \
  --name cloudwatch-agent \
  --namespace amazon-cloudwatch \
  --role-name ${ClusterName}-cloudwatch-agent-role  \
  --cluster ${ClusterName}  \
  --attach-policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy \
  --override-existing-serviceaccounts \
  --approve

Service Account fluent-bit にも同じように CloudWatchAgentServerPolicy を設定した IAM ロールを設定します。

eksctl create iamserviceaccount \
  --name fluent-bit \
  --namespace amazon-cloudwatch \
  --role-name ${ClusterName}-fluent-bit-role  \
  --cluster ${ClusterName}  \
  --attach-policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy \
  --override-existing-serviceaccounts \
  --approve

ここで ポイント になるのは、7 行目の --override-existing-serviceaccounts オプションです。 手順 1. のクイックスタートセットアップですでに Service Account は作成されています。--override-existing-serviceaccounts オプションを付けることで作成済の Service Account に AWS IAM のロールを設定できます。

この後、Agent の Pod の再起動が必要になるので、DaemonSet をリスタートしておきましょう。

kubectl rollout restart ds cloudwatch-agent -n amazon-cloudwatch
kubectl rollout restart ds fluent-bit -n amazon-cloudwatch

手順 3. 動作確認

この後 1~2分ほど待って、Container Insights が使用できるか確認してみます。

AWS マネジメントコンソール で Amazon CloudWatch のページ左側のメニューから [ インサイト ] - [ Container Insights ] を選択します。 下図では、[ パフォーマンスのモニタリング ] で test-cluster のメトリクスの表示を確認しています。

他にも、様々な切り口でメトリクスを確認してみて下さい。

また、ログの収集も確認してみます。Amazon CloudWatch のページ左側のメニューから [ ログ ] - [ ロググループ ] を選択します。 下図では、test-cluster のロググループが作成されていることを確認しています。


今回の所感

今回紹介した手順でポイントになるのは、手順 2. です。手順 2. については、AWS の Container Insights のドキュメントには具体的な記載がありません。ただ、手順 2.の実施においては、Kubernetes の RBAC や Service Account、Amazon EKS の IRSA の仕組みの理解が必要になるので、AWS のドキュメントのように他のページで確認して下さい、という記述になるのは、仕方がない部分もあるのかもしれません。

とはいうものの、Container Insights を導入する具体的な手順は明確に示しておくことは必要かと思いますので、この記事でそれをまとめることができてよかったと思ってます!


kubesec で AWS KMS のキーを使って Secret を暗号化・復号してみる

Kubernetes の Secret リソースでマニフェストで作成するとき、Secret として秘匿したいデータを Base 64 でエンコードして指定します。

ただ、Base 64は単なるエンコードなので、そのままでマニフェストを保存することは、セキュリティ上避けたいですよね。そのため、Secret のデータの暗号化や復号を行う kubesecSealedSecrets などのツールが提供されています。

これまで「kubesecとか使えば暗号化や復号はできるよね」と頭でしか理解してなかったんですが、kubesec を実際に触ってみようと思い立ったので、そのときのメモをここに書いておきたいと思います。

なお、今回は 下図のように AWS KMS のキーを使用して暗号化・復号する前提とします。


目次

  1. AWS KMS のキーの作成
  2. kubesec のインストール
  3. Secret のマニフェストの暗号化
  4. Secret のマニフェストの復号
  5. 今回の所感

1. AWS KMS のキーの作成

今回、暗号化や復号に必要なキーを AWS KMS を使用して生成しますので、AWS KMSにアクセスできるようにする必要があります。そのため、使用しているAWS IAM のユーザーまたはロールに、AWS IAM のポリシーを設定します。

今回は、使用している AWS IAM のユーザーに AWSKeyManagementServicePowerUser という管理ポリシーを設定する前提としますが、実際に使用する際は最小権限の原則に基づいて設定して下さい。

AWS KMS を操作する上での AWS IAM のポリシーについては次のドキュメントに例があるので参考にしましょう。

docs.aws.amazon.com

AWS KMS キーの作成は AWS マネジメントコンソールからでも行えますが、今回は AWS CLI のコマンドで作成してみます。

aws kms create-key  --description "demo for kubesec"

--description オプションで指定する文字列の値は任意です。

キー作成が完了すると、次のような 出力が表示されるので、この中の Arn の値をメモしておきましょう。

{
    "KeyMetadata": {
        "AWSAccountId": "000000000000",
        "KeyId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "Arn": "arn:aws:kms:ap-northeast-1:000000000000:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "CreationDate": "2023-03-25T09:31:57.061000+00:00",
        "Enabled": true,
        "Description": "demo for kubesec",
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
        "KeySpec": "SYMMETRIC_DEFAULT",
        "EncryptionAlgorithms": [
            "SYMMETRIC_DEFAULT"
        ],
        "MultiRegion": false
    }
}

2. kubesec のインストール

kubesec のインストール方法などについては、下記を参考にしました。

github.com

今回は Linux の環境にインストールしますので、次のコマンドを実行します。

curl -sSL https://github.com/shyiko/kubesec/releases/download/0.9.2/kubesec-0.9.2-linux-amd64 \
  -o kubesec && chmod a+x kubesec && sudo mv kubesec /usr/local/bin/  

その後、確認のため次のコマンドを実行します。

kubesec --version

0.9.2 のようにバージョン ID が出力されれば OK です。


3. Secret のマニフェストの暗号化

ではいよいよ、Secret のマニフェストを暗号化してみます。

今回は、次の内容を secret.yaml として保存して使用します。

apiVersion: v1
kind: Secret
metadata:
  name: my-secret
type: Opaque
data:
  username: YWRtaW4=
  password: YWRtaW4xMjM0

kubesec で AWS KMS のキーを使用して暗号化するときは、--key オプションに キーの ARN を指定します。

次の例では、secret.yaml を暗号化したマニフェスト標準出力 に表示します。

kubesec encrypt --key=aws:<ARN of AWS KMS key> secret.yaml

実行すると次のような内容が表示されます。

apiVersion: v1
data:
  password: (暗号化された値)
  username:  (暗号化された値)
kind: Secret
metadata:
  name: my-secret
type: Opaque
# kubesec:v:3
# kubesec:aws:arn:aws:kms:ap-northeast-1:0000000000001:key/xxxxx
# kubesec:mac:xxxxx

data: 以下の usernamepassword の値が暗号化されていることを確認できました。

また、-i オプションを付けると、指定したマニフェストファイルに暗号化した値を直接変更します。

kubesec encrypt -i  --key=aws:<ARN of AWS KMS key> secret.yaml

上記を実行すると、secret.yamldata: 以下の usernamepassword の値が直接変更されたことが確認できます。


4. Secret のマニフェストの復号

暗号化されたマニフェストを復号する場合は、decrypt を使用します。

次の例では、暗号化されている secret.yaml を復号して標準出力に表示します。

kubesec decrypt secret.yaml 

実行すると次のような内容が表示されます。

apiVersion: v1
data:
  password: YWRtaW4xMjM0
  username: YWRtaW4=
kind: Secret
metadata:
  name: my-secret
type: Opaque

また、暗号化と同じように -i オプションを付けると、暗号化されたファイルに復号した値を直接変更します。

kubesec decrypt -i  secret.yaml 

上記を実行すると、暗号化された secret.yamldata: 以下の usernamepassword の値が直接変更されたことが確認できます。


5.今回の所感

kubesec と AWS KMS のキーを使用して Secret の値を暗号化・復号する仕組み自体は、とてもシンプルです。

GitOps のように Kubernetesマニフェストを Git リポジトリに保存・管理してデプロイする場合は、Secret は kubesec のようなツールを使い、暗号化して保存し、Kubernetes クラスタに適用する際に自動的に復号するような仕組みを考えたいと思いました。

一方で、AWS KMS のキーの権限の管理は厳格に行う必要があります。キーを作成できる権限、キーを使用して暗号化・復号できる権限などは IAMのユーザーまたはロールごとに最小権限の原則に基づいて設定する必要があるので、その管理や運用を漏れなく実施する必要性を強く感じました。

今回は 暗号化・復号のツールとして kubesec を使ってみましたが、他にも SealedSecrets といった便利なツールもあるので、また別の機会に触ってみたいです!


/* -----codeの行番号----- */