SAML2.0のSP metadataにXML署名するのが面倒臭かった
なにをやったか、備忘録です。正確には記載しませんよ。
無駄なことしてる箇所もあるかもしれない。
<作りたいもの一覧>
①XML署名されたSAML2.0のSP metadata XML
②Spring Securityに渡す秘密鍵
③Spring Securityに渡す証明書
<使ったもの>
keytool (jdk付属品、今回openjdk17使ってます)
openssl (linuxだったら使えるでしょ)
xmlsectool (v3.0)
STEP 1: keytoolで鍵ペアを作る (JKS形式キーストア)
注意点は1つ。鍵パスワードとキーストアパスワードを同じものにしておくこと。
ちなみに、このJKS形式のキーストアは、あとでxmlsectoolで使う。
だいたいこんな感じだったかな。
keytool -genkeypair -alias sp-metadata -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -dname "CN=hoge, O=company, L=ota-ku, ST=Tokyo, C=JP" -validity 365 -keystore ./keystore.jks -storetype JKS -storepass password -keypass password
STEP 2: keytoolでJKS形式をPKCS#12形式に変換する
PKCS#12形式は、鍵パスワードとキーストアパスワードが同じという制約があるらしい。
変換後の形式なんだから、変換前のJKSは違ってもいいと思っていたら、
Given final block not properly padded
こういう感じのエラーメッセージがでてきてたと思う。
だいたいこんな感じだったかな
keytool -importkeystore -srckeystore ./keystore.jks -destkeystore ./keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass password -deststorepass password -srcalias sp-metadata -srckeypass password
STEP 3: openssl pkcs12でPKCS#12をいったんPEM形式のキーストアにしとく。
だいたいこんな感じだったかな
openssl pkcs12 -in ./keystore.p12 -out ./keystore.pem -passin pass:password -passout pass:password
STEP 4: openssl pkcs8でPEM形式キーストアを読み込み、秘密鍵をPEM形式で出力。
これは、spring securityに渡すものです。
RelyingPartyRegistrationsBeanDefinitionParserあたりを読み進めれば、pkcs8で読み込んでるのが追えます。
だいたいこんな感じだったかな
openssl pkcs8 -topk8 -inform PEM -outform PEM -in ./keystore.pem -out ./sp-metadata-pkcs8.pem.key -passin pass:password -passout pass:password
うーむ、パスワードついてていいのかな、Spring Securityが開けないのでは? 未確認。
STEP 5: openssl x509でPEM形式キーストアを読み込み、証明書をPEM形式で出力。
これも、spring securityに渡すものです。
だいたいこんな感じだったかな
openssl x509 -in ./keystore.pem -out ./sp-metadata-x509.pem.crt -outform PEM -passin pass:password
STEP 6: PEM形式の証明書をsedとtrでワンラインにしてしまって、Base64エンコーディングされたDER部分を抜きとる。
これは、SP metadata xmlのSAML Requestとかで署名するときに検証してもらうための証明書のデータです。
だいたいこんな感じだったかな
sedでヘッターとフッターを削ったあとに、trで改行コードを削る。
cert_txt=$(cat ./sp-metadata-x509.pem.crt | sed -e "1d" -e "$d" | tr -d "\n")
STEP 7: ある程度作っておいたSP metadata xmlに、証明書データを埋め込む。
これはSTEP 6で作ってる値を、置換しやすいように書いておいた文字列と置換する話です。
sedを使いましょう。しかし注意してください。Base64にはスラッシュも含まれます。
sedを使う際には、スラッシュ以外の区切りでやる必要があります。
これで、XML署名前のSP metadata xmlができました
だいたいこんな感じだったかな
sed -e "s|##TODO##|${cert_txt}|" ./sp-metadata-template.xml > ./sp-metadata-signee.xml
STEP 8: xmlsectoolでXML署名する
だいたいこんな感じだったかな
./xmlsectool.sh --sign --inFile ./sp-metadata-signee.xml --outFile ./sp-metadta-signed.xml --referenceIdAttributeName root --keystore ./keystore.jks --keystoreType JKS --keystorePassword password --keyAlias sp-metadata --keyPassword password
AWS KMSはXML署名なんてできないだろうし、Apache SantuarioでJavaコード書くしかないかなと思っていたが、xmlsectoolがApache Santuarioを裏で使っているようで、安心して使える。
CloudWatch Logsのエクスポートタスクのエラー
先日、create_export_task呼び出し時の例外を調べた。
InvalidParameterExceptionだったはず。
例外メッセージを読むと、
fromとtoの指定が時間的に逆か、
ロググループの保持期間を過ぎているかのようだ。
fromとtoの時間は正しい順序、
保持期間は30日でとくに問題ない。
では何が悪いかを調べた。
toのunix timestampが、
ロググループのCreationTimeより前だった。
検証してみると、CreationTimeときっかり同じ時間でも例外になった。1ミリ秒後なら例外にならなかった。
ロググループがいつ作られたか調べずに
create_export_taskを使うと
例外が起きることがあるよ。
その例外が原因で、他のロググループをエクスポートしないで止まってしまったら目も当てられない。
日次バックアップ目的でfrom,toは時間固定で、新しいロググループが作られた日は例外が発生するということ。
create_export_taskは1アカウントで1つしか動かないから、いつか致命傷になる。
firehoseでやったほうがきっと良い。
PySparkでの最頻値mode
pandasにはあるけど、pysparkにはない代表格と言っても良いかもしれない。
modeでピンとこない人は、most frequentだったり、most commonなどでも検索します。
ネットで探すといくつか実装例がある。
少しは参考にした。
自分が先日書いたコードだとどんな感じになったかを思い出しながら書いてみる
実現したい最頻値の取り方は、
「あるグループの中での、あるカラムの最頻値を複数カラム分」というものです。
都道府県ごとに知りたい最頻値がたくさんあるというようなことです。
最頻値を取らないといけないカラムが多すぎて、ループで回して書いてたら鈍足になってしまっていたので、できるだけ速度的にズバッといける書き方を模索した。
from pyspark.sql import functions as f from pyspark.sql import Window as w df = something keys = ["key1", "key2"] # グループキー cols_mode = ["col1", "col2"] expression_count = [f.col(c) for c in keys] + [f.struct( f.count(c).over( w.partitionBy(keys + [c]) ), c).alias(f"{c}_struct") for c in cols_mode ] df_count = df.select(*expression_count) # key1, key2, col1_struct, col2_struct expression_max = [f.max(f"{c}_struct").getItem(c).alias(f"mode({c})") for c in cols_mode] df_mode = df_count.groupBy(*keys).agg(*expression_max) # key1, key2, mode(col1), mode(col2)
いくつかのポイント。
Windowを用いたcount集約を使う。
最頻値の性質として、件数カウントがどうしても必要になってくるが、df.groupBy(...)で書き始めてしまうと沼にハマる。
key1 | key2 | col1_struct | col2_struct | ||
---|---|---|---|---|---|
count | col1 | count | col2 |
structを活用する。
ネット上の例でもstructを使ったものがあり、非常に参考になった。
structに対してmaxを取ると良い感じに動く。
orderbyやlimitは書かなくていい。
注意点1 Null
このコードでは最頻値てして、Nullはカウントされてない。Nullをカウントする場合は、whenを用いる必要がある。NaNも。
注意点2
最頻値に同じ件数の出現があった場合、カウント対象の値が大きい方が使われる。これはstructにmaxを使ってるのでそうなる。異なる基準で選ぶ必要があるなら、structの2番目にそれを入れ込む必要がある。
PySparkのadd_monthsでカラムを使う
EMRのバージョンの関係で今、PySpark 2.4.5を使っている。
Pythonという言語にはオーバーロードがないためなのか、Scalaでは用意されてるメソッドが呼び出せないなんてことが稀にある。
PySparkのadd_months(start, months)の
docstringの例で、startはColumnでmonthsは数値リテラルになっていて、monthsにColumnを渡すと、「TypeError: Column is not iterable」が発生してしまう。
解決策はexprを使うことが一番楽だった。
Is there an add_months with data driven number of months? - Databricks Community Forum
PySparkのコードを読む限りでは、
monthsをjavaカラムに変換してない。
Columnが来たら変換してくれればいいのに。
そのため、現状では絶対に数値リテラルしか無理だと言える。(litで包んでも意味がない)
Scala側は、add_months(Colum, int)の他に、
add_months(Column, Column)を持っている。
というわけで、PySpark→Py4Jのルートを一部迂回するのにexprを使うのが一番早い。
そもそもなんで「Column is not iterable」なんかになる理由は、深くは追えてないけど、だいたいこんな理由だろう。
py4jのpython側は、コマンドを作って、java側と通信する。コマンドを作る際、すでにJavaObjectになってるもの(javaカラムになっているもの)はコンバートしないが、そうではないものは複数あるコンバーターをループさせて変換をかける。コンバーターはコンバート可能かをチェックする関数があり、ListConverterは__iter__を持っていることをチェックしている。python側のColumnは__iter__を持っているので、ListConverterが適用されるのだが、いざ__iter__を呼び出してみるとTypeErrorが即座に返される。
私の使い方では、日付に対してたくさん計算するので、数値リテラルで実現するのは困難なので方法を調べていた。
日付 |
2020-01 |
みたいなデータに一旦arrayで加算したい月数を作る。リスト内包表記とlitを使って、arrayに渡す形。
日付 | 加算月 |
2020-01 | [1,2,3,4,...] |
ここからexplodeで行を増やす。
日付 | 加算月 |
2020-01 | 1 |
2020-01 | 2 |
2020-01 | 3 |
2020-01 | 4 |
2020-01 | ... |
で、これらを加算するのにadd_monthsを使う。
日本語を含むCloudFormtionテンプレート
Windows上でAWS CLI V2を使って
aws cloudformation create-stackする時に
日本語を含むテンプレートが読み込めずにエラーになると相談されたので、夜中まで調べた。
相談の際に引用されたURLはこれだった。
CloudFormationに日本語コメントを含めるとエラーになる場合の解決方法 | LaptrinhX
ちなみに例外の箇所はここだ。
aws-cli/paramfile.py at 96359d999dffc0357eaba4d150f2ffe4d2a68ce7 · aws/aws-cli · GitHub
こんなメッセージがでる。
Unable to load paramfile (%s), text contents could not be decoded. If this is a binary file, please use the fileb:// prefix instead of the file:// prefix.
それでは私の結論を言おう。
AWS CLI V2の新しめのやつなら出来るよ。
AWS CLI を設定する環境変数 - AWS Command Line Interface
確かに、PyIntallerで作ってると、sysがfrozenになる(変更できない)とかで、PYTHONUTF8などは効かなくなるようだ。素のままのgithub上のコードであれば、bin/aws.cmdを書き換えれば、-X utf8を与えられるだろうが、PyInstallerで作られたものでは、この手のスクリプト類がなくなって出来ない。PyInstallerのspecでbin/awsを指定してる箇所があるので、もしかしたらLC_CTYPEは効くかなと試したがダメだった。
AWS_CLI_FILE_ENCODING環境変数が出来たのは、2.0.13かららしいが、CloudFormationで使えるようになったのは2.0.24かららしい。(2020年6月19日リリース)
Unable to load file:// with non-English char in UTF-8 encoding on Windows · Issue #5086 · aws/aws-cli · GitHub
まあ、日本語で登録できるけど、AWSコンソールで日本語が化けるところは多い。化けないところもある。AWSコンソールがUTF-8に対応できてないってだけ。
Building AWS Glue Data Catalog Client for Apache Hive Metastore
長い道のりを経て、なんとかCodeBuildで、Sparkを動かすためのなんちゃってEMR(without EMRFS)を用意したときの記録です。
最終的に、CodeBuildからpytestを動かす。
テストケースでpysparkが動いて、spark.sqlからGlueデータカタログに繋がって、ロケーション情報からS3のデータを拾ってデータフレームを得られるようになる。
初めの頃に目指した構成は断念
初めはSparkとHadoopを別々に用意するつもりだった。
without Hadoopには、Hadoopだけではなく、Hiveも入ってない。これが最後の方でクラスパス地獄になったので諦めた。
最終的にはSparkのディストリビューション用のシェルに頼った部分がある。
- Spark with Hadoop
- Glue Metastore client
こう見ると、簡単に思うかもしれないが、そんなことはない。
Hiveを2回ビルドして、Glue Clientをビルドして、Sparkもビルドする。
1回通すのに40分は覚悟しないといけない。
CodeBuildはレイヤーのキャッシュが効かないので、まじで辛い。
もしも同じことをやるなら、EC2にdocker入れて、ある程度動くDockerfileを作ってからCodeBuildに持っていった方が試行錯誤の時間を短縮できる。
javaとscalaとpythonを知っていて、mavenのpomやapache ivyを知っていて、gitも軽く使えて、dockerも知っていて、AWSのCodeBuildとECRとIAMの知識があれば同じことが出来る。
今回のビルドに使う主なバージョン
Spark 2.4.5
Hadoop 2.8.5
Scala(binary version) 2.11
Hive 2.3.5
Hive 1.2.1-spark2
使うEMRのバージョンに合わせて、Spark/Hadoopのバージョンを決めた形。
Hive 2.3系のビルド
Hiveのbranch-2.3でビルドする手順になっているが、Hiveの2.3.6で入った変更で、Glue Client側でコンパイルエラーが発生する。
そのため、2.3.5でビルドした。
Hiveにはパッチを当てる。
はっきり言って、欲しいのはSpark用のGlue Clientなので、Hive 2.3.5はビルドしたくない。しかし、Glue Clientの手順を通すのために、渋々とビルドしている。
hadoop.versionも変更。
junitが見つからないエラーがでたりするが、pomの変更で対応可能、HiveのJIRAにパッチがある。
Hive 1.2.1-spark2のビルド
手順では何も言及がないが、forkされたHiveを使う。
GitHub - JoshRosen/hive at release-1.2.1-spark2
SparkはforkしたHiveに本当に依存している。
これにもパッチを当てる。
gpgは-Dgpg.skip=trueで省略。
hadoop-23.versionも変更。
JDK差異の対応
Hiveのビルドで、java8でのコンパイルが失敗する場合、コンパイルオプションに-Dignore.symbol.fileを付けるようにsedを書いた。
sedは最長一致になるので書き方に気を付ける。
Glue Clientのビルド
手順ではHiveのバージョンを書き換えることになってるが、普通に-Dで渡せば良い。
そのとき、xmllintを使ったりする。
xmllintでpom.xmlのようにスキーマ情報のあるエレメントの抽出はググればでてくる。
後で収集するので、installまでしておく。
spark.hive.metastore.jars用のjarの収集
いまだにこれが正解とは言い切れない。
spark-project:hive-execのruntimeを収集し、Glue Client側からはSpark Clientのjar(shaded済)を収集して、1つのディレクトリに集める。
除外しないといけないJarもあって、SparkのHiveUtilsとIsolatedClientLoaderあたりを読まないと分からない。
maven-dependency-pluginに悩まされる
古いPOMが多く、当然プラグインもバージョンが古い。何がどうなってるか分からないので、githubでプラグインのコード(Mojo)も読みながら対応していった。
バージョン2.1でrepositoryUrlの指定についてメッセージが出るようなら、新しいバージョンを使って、remoteRepositoriesを使うのが良い。
Sparkのビルド
dev/change-scala-version.sh
dev/make-distribution.sh
を使った。
自分が使わないRなどは除外。
最終的にPython3で色々準備したいので、
make-distribution.shもpython3を使うようにsedで置換。
setuptoolsは先に入れておかないといけない。
zincの問題
alpineでSparkのビルドをやっていたとき、zinc周りの問題が解消出来なかった。installされたはずのzincがいつの間にか消えてしまう、no such file or directoryな問題。
このせいでalpineを諦めた。
javac 137の対応
Sparkのビルドでjavacが137を返していた。
特にspark-sqlの箇所でよく発生。
Hiveではjava8の問題があったが、同じようなログはなく、これは関係していなくて、137に気付かない限り解決に至らない。
メモリが足りてないのでCodeBuildの設定で、7GBメモリに変更。
sparkのインストール
tgzを作ったので、好きなところに展開して、シンボリックリンクを貼ったりした。
$SPARK_HOME/spark-defaults.confの作成
spark.master
spark.submit.deployMode
spark.pyspark.python
spark.pyspark.driver.python
spark.sql.hive.metastore.version
spark.sql.hive.metastore.jars
spark.sql.catalogImplementation
spark.sql.hive.metastore.jarsには、一箇所に集めておいたjarに対して、lsコマンドとtrコマンド(改行コードをコロンに置換)でクラスパスを作成したものを与える。
$SPARK_HOME/hive-site.xmlの作成
hive.metastore.client.factory.class
aws.region
aws.glue.endpoint
regionとendpointは、EC2だったら設定しなくても動くはずだが、CodeBuildのようなコンテナ環境では設定しないと動かない。
今回は、docker build中に埋め込んだが、CodeBuildでAWS_REGION環境変数が使えるなら、そのタイミングで作成した方が良い。
hadoop-awsの追加
実際に動作確認をしていたら、
s3のファイルシステムが見つからない問題が発生。
sparkのdistributionには、hadoop-awsは含まれていないのだった。
mvn dependey:getでhadoop-awsとaws-java-sdk-bundleを各々単品で入手して、$SPARK_HOME/jarsに配置。
$SPARK_HOME/core-site.xmlの作成
fs.s3.impl
fs.s3a.aws.credential.provider
Glueのデータカタログにはs3で登録してあるため、s3スキーマが来た時に、S3Aの実装が使われるように設定。
CodeBuildで動かす場合、
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI環境変数が渡ってくるので、今回はEC2ContainerCredentialsProviderWrapperを使ってみた。このクラスの話は、hadoop awsのページには載ってない。最新版のhadoopにはこのクラスを使用したコードがあるが、今回使う2.8.5にはそれは含まれていない。
このクラスを使うと、アクセスキーなどは設定ファイルに書かなくても動く。
pysparkのインストール
Sparkの資材の中にあるものを利用して、python3 setup.py installした。
これはjarsにあるものをコピーしたりするので、もろもろ整ったあとに実施する。
aws-cliはインストールしないこと
Glue Clientだったり、hadoop-awsだったりの認証プロバイダの探索順の関係で干渉したりするので、入れないこと。hadoop-awsの方は認証プロバイダを変更できる。Glue Clientの方も変更できないことはないが、aws sdkのものを直接指定できないので、DefaultAWSCredentialsProviderChainが使われると考えると、~/.aws/credentialsが読み込まれないようにインストールしない方が良い。
HiveSessionStateBuilderが見つからない
without hadoopでやってたときに発生。
Sparkのspark-hiveが足りてない。
HiveConfが見つからない
Hiveのhive-commonが足りてない。
これは、パッチが当たってないといけないため、簡単に入手できるSparkのディストリビューションはそのまま使えない。
Py4jの問題
without hadoopでやってたときに発生。
バージョンの異なるPy4jが混ざると発生。
GatewayServerBuilderが、GatewayServerの staticメソッドを呼ぶ箇所だったはず。
異なるpomでjarを集めるのはやはり無理が多い。
S3アクセス時に403エラー
クレデンシャルの設定がおかしいのか、
普通にIAMロールに権限が不足している。
Install Python36 on Amazon Linux2 based container
いまは2020年12月ですが、
なかなかどうして、上手くPython36を入れる方法というのがありません。
過去にうまく入れてる例は多いですが、それらの情報は古く、整理しないといけません。
最終的に、アーカイブされてるFedoraのEPELリポジトリから入れることにしました。
動機
EMRで使用するPythonバージョンが3.6と分かっているので、単体試験用の環境として、諸々インストールしたdockerイメージを準備したくCodeBuildでamazoncorretto:8をベースにDockerfileを作ってるところ。
今のCodeBuildの機能ではPython 3.7か3.8しか選べない。
古いFedoraのArchiveされたEPELから入れる
Index of /pub/archive/epel/7.2019-05-29/x86_64/Packages/p
Python 3.6.8がある。
自分でrepoファイルを作らないといけない。
yum install -y https://archives.fedoraproject.org/pub/archive/epel/7.2019-05-29/x86_64/Packages/e/epel-release-7-11.noarch.rpm sed -i "s/metalink=/#metalink/g/" /etc/yum.repos.d/epel.repo sed -i "#baseurl=http:\/\/download\.fedoraproject\.org\/pub\/epel\/7\//baseurl=https:\/\/archives\.fedoraproject\.org\/pub\/archive\/epel\/7\.2019-05-29\//g" /etc/yum.repos.d/epel.repo yum clean all yum install -y --enablerepo=epel python36
先にepel-releaseを入れる。
epel.repoファイルは出来るが、アーカイブにあったとしてもurlは本物。
metalinkを無効化して、baseurlでアーカイブに向ける。repomd.xmlの問題(更新時刻が古い)がでたりするのでcleanする。/var/yum/cacheを消すまではやらなくても動いた。
enablerepoは付けなくてもいけたかも??
amazon-linux-extrasの方法
この方法は、python3のトピックがdeprecatedになっており、使えなくなるはず。amazon-linux-extras listでpython3のトピックはもう表示されておらずpython3.8が見えてます。
隠れてるのを無理やり使ってます。
amazon-linux-extras enable python3 yum install -y python3-3.6
この方法ではPython 3.6.2が入りました。
amazon-linux-extras enable python3 yum install -y --disablerepo=amzn2-core python3
python3の指定でやるときは念のため、amzn2-coreの方にならないようにした方が良い。さもなくばpython 3.7とかが入ることになったりする。
どんなトピックがあるのか調べてみたときにdeprecatedだと気付いた。
http://amazonlinux.default.amazonaws.com/2/extras-catalog-x86_64.json
amazon-linux-extrasでEPELからはもう無理
amazon-linux-extras install epel -y yum install -y --enablerepo=epel python36
この方法は、fedora 7のEPELを見るrepoが追加される。
しかし、fedoraのEPELにpython36はもういない。
Index of /pub/epel/7/x86_64/Packages/p
1739804 – Retired python36 breaks CentOS
簡単に言うと、Centos 7.7で、Python3.6はメインのリポジトリに移動して、EPELから消えた。2019年8月頃の話です。
IUS版から入れる方法
Amazon Linuxでは推奨しない。
IUS - FAQ
yum install -y https://centos7.iuscommunity.org/ius-release.rpm yum install -y python36u
iusのrepoはinstall時に有効化されてるタイプ。
Fedora 32等からは入らない
yum install -y https://dl.fedoraproject.org/pub/fedora/linux/releases/32/Everything/x86_64/os/Packages/f/fedora-repos-32-1.noarch.rpm yum install -y python36
Fedora 32にはPython 3.6.10がある。
Fedora 33にはPython 3.6.12がある。
インストール失敗。
Python36がEPELにいないので、fedora-reposを入れてみようとしましたが、
system-release(32)が必要と怒られた。
まあ、そうだわな。
うちらFedoraの7がベースだもの。
これは仕方ない。
rpmを直指定すれば入るかもしれないが依存関係とかの問題に当たりそうで諦める。
ソースからビルド
ここでは説明しない。
依存ライブラリのインストールなど大変だ。