こんにちは、インフラエンジニアの 高山 です。
この記事は Enigmo Advent Calendar 2022 の25日目の記事です。
はじめに
エニグモ入社後に AWSを徐々に触れ始め 数年経過しました。
最初はWebコンソールからポチポチしていたのですが、CloudFormationでコード化を進めたり AWSCLIやSDKを使うようになり、最近は Rubyで AWSリソースの情報取得や操作をすることが多くなってきました。
(以下、RubyでAWSリソースの情報取得や操作をする
ことをAWSをRubyで操作する
と表現します)
- そもそもWebコンソールは苦手(というより怖い)で、CLIで操作したい派である
- 構造的なデータを扱うのは Shellスクリプトだと厳しい(jqが嫌い)
- Ruby on Rails(以下、Railsと表記します)を使ったことがあるので、Rubyに少しは慣れている
- BUYMAではRailsを採用しているので、社内で聞ける
AWSを操作するには python(+boto3)を使っているユーザの方が圧倒的に多そうな気がしますが、pythonと比較しているわけではないので、片手落ち感は否めない記事になってます。
相性が良いと思う理由
AWSとRubyは相性が良いと思う理由、それはデータのシリアライズが簡単にできるからです。
この一点に尽きるので、ここで記事終了でも良いくらい。
シリアライズとは
シリアライズというのは データやオブジェクトをファイルに書き込みできるように変換することです。
反対はデシリアライズ。 言いにくいですね。
やってみよう
シリアライズは yamlライブラリを使い、オブジェクトをyaml化して、ファイルに保存するだけです。
$ export AWS_REGION="ap-northeast-1" $ pry [1] pry(main)> require 'aws-sdk-ec2' => true [2] pry(main)> require 'yaml' => true [3] pry(main)> ec2_client = Aws::EC2::Client.new(); [4] pry(main)> ec2_data_list = ec2_client.describe_instances(); [5] pry(main)> File.open("ec2_data_list.yml",'w'){|h| h.write ec2_data_list.to_yaml} => 896272 [6] pry(main)> quit $ ls -l ec2_data_list.yml -rw-rw-r-- 1 ec2-user ec2-user 896272 Dec 21 15:49 ec2_data_list.yml
デシリアライズも同じように yamlライブラリを使ってloadするだけです
※ aws-sdk-ec2
をrequire
する必要はあります
$ pry [1] pry(main)> require 'aws-sdk-ec2' => true [2] pry(main)> require 'yaml' => true [3] pry(main)> ec2_data_list = YAML.load_file("ec2_data_list.yml"); [4] pry(main)> p ec2_data_list.reservations.count; 149
簡単ですね。
シリアライズできることの何が嬉しいのか
簡単にシリアライズできることは わかっていただけたと思いますが、それの何が嬉しいのか?
データ構造を そのままファイルにして、それを読み込むことができるので 以下を分離することができます。
- 時間がかかるデータ取得の処理
- いろいろ試行錯誤したいデータ整形の処理
👆が嬉しいことなのですが、伝わりませんね 実際にやってみましょう。
スクリプトの例
データを取得するスクリプト
まずデータを取得するスクリプトです。
EC2インスタンスが多い場合はnext_token
がnil
になるまでループする必要があります。
(データが少なければ不要です)
ここは できるだけフィルターした方が良いですが、今回は検証なのでしていません。
データ取得は数秒程度ですが、検証のたびに取得し直すは地味にストレスですよね。
$ cat get_all_ec2_data.rb #!/usr/bin/env ruby require 'aws-sdk-ec2' require 'yaml' ec2_client = Aws::EC2::Client.new() all_ec2_data_list = [] token = nil loop do ec2_data_list = ec2_client.describe_instances({next_token: token}) token = ec2_data_list.next_token all_ec2_data_list += ec2_data_list.reservations break if token.nil? end File.open("all_ec2_data_list.yml",'w'){|h| h.write all_ec2_data_list.to_yaml}
インタラクティブシェル(pry)で確認
次はデータ構造や、データをどうやって整形するか確認しましょう。
以下は Nameタグの取得方法を確認した時のログです。
[1] pry(main)> require 'aws-sdk-ec2' => true [2] pry(main)> require 'yaml' => true [3] pry(main)> [4] pry(main)> all_ec2_data_list = YAML.load_file("all_ec2_data_list.yml"); [5] pry(main)> all_ec2_data_list[6].instances.first.tags => #<struct Aws::EC2::Types::Tag key="Name", value="Test-BM-Rails-19">, #<struct Aws::EC2::Types::Tag key="role", value="Railsweb">, #<struct Aws::EC2::Types::Tag key="Service", value="Buyma">, #<struct Aws::EC2::Types::Tag key="CreateUser", value="takayama">, [6] pry(main)> all_ec2_data_list[6].instances.first.tags.class => Array [7] pry(main)> all_ec2_data_list[6].instances.first.tags.find{|tag|tag["key"]=="Name"} => #<struct Aws::EC2::Types::Tag key="Name", value="Test-BM-Rails-19"> [8] pry(main)> all_ec2_data_list[6].instances.first.tags.find{|tag|tag["key"]=="Name"}["value"] => "Test-BM-Rails-19"
各EC2インスタンスのena_supportが有効になっているどうかをチェックするスクリプト
各EC2インスタンスのena_support
が有効になっているどうか チェックするスクリプトを書いてみました。
(例として良いものが思いつかなかったです。。)
$ cat check_all_ec2_ena_support.rb #!/usr/bin/env ruby require 'aws-sdk-ec2' require 'yaml' all_ec2_data_list = YAML.load_file("all_ec2_data_list.yml") all_ec2_data_list.each do |ec2| name = ec2.instances.first.tags.find{|tag|tag["key"]=="Name"}["value"] puts "#{name}: #{ec2.instances.first.ena_support}" end
以下のような感じで出力されます。
$ ./check_all_ec2_ena_support.rb | head -5 Test-Railsweb-Server: true Test-Phpweb-Server: true Test-EC2-Instance: false Test-Airflow-Server: false Test-Redash-Server: false
EC2のタグだけ取ってファイルに保存するスクリプト
次は EC2のタグだけ取ってファイルに保存するスクリプトを作成してみました。
$ cat check_all_ec2_tags.rb #!/usr/bin/env ruby require 'aws-sdk-ec2' require 'yaml' def convert_tags_to_hash(tags) hash_tags = {} tags.each{|e| hash_tags[e["key"]] = e["value"]} hash_tags end all_ec2_data_list = YAML.load_file("all_ec2_data_list.yml") all_ec2_tag_info_list = {} all_ec2_data_list.each do |ec2| tags = convert_tags_to_hash(ec2.instances.first.tags) all_ec2_tag_info_list[tags["Name"]] = tags end File.open("all_ec2_tag_info_list.yml",'w'){|h| h.write all_ec2_tag_info_list.to_yaml}
実行してみます。
$ ./check_all_ec2_tags.rb $ head all_ec2_tag_info_list.yml --- Test-Railsweb-Server: Name: Test-Railsweb-Server role: Railsweb CreateUser: takayama Test-Phpweb-Server: Name: Test-Phpweb-Server role: Phpweb CreateUser: takayama Test-EC2-Instance:
yamlになっていると、可読性が高いところもメリットです!
いったん、結論
ということで、シリアライズの簡単さと メリットがわかっていただけたのではないかと思います。
メリットまとめ
Rubyでの認証情報の取得
これまでの検証では わかりやすくするため Read権限のあるEC2上で実行していたので 認証情報の取得は不要でしたが、PC上など他の場所でAWSのSDKを使用する場合は 認証情報の取得が必要です。
$ ls -l ~/.aws/cli/cache/ total 24 -rw------- 1 takayama staff 1421 12 8 20:16 3a9ab49ce67fe5806c5af3d5a378fbbb470561d9.json -rw------- 1 takayama staff 1445 10 24 15:24 65d08609cd764dc442d836e7665fbbd663992aea.json -rw------- 1 takayama staff 1433 8 25 16:07 6c11869d293d59bf5a50ceef707e37b36524415d.json
AWSCLIだと、一時認証情報が保存され再利用できますが、RubyのSDKを使ってスクリプトを作っている場合は再利用できず、スクリプトを実行するたびに認証情報を取得し直さないといけないのが難点でした。
しかし、それも シリアライズで解決できます。 さすがシリアライズさん!
以下のような感じです。
role_credentials = Aws::AssumeRoleCredentials.new(client: sts_client, ...) role_credentials.client.config.retry_backoff = nil role_credentials.client.config.defaults_mode_config_resolver = nil File.open("role_credentials.yml",'w'){|h| h.write role_credentials.to_yaml}
そのまま保存すると読み込む時にエラーになるので 一部の不要なデータを削除してから保存する必要がありました。
aws-sdk-core
のバージョンによって削除する必要のあるデータが変わるようです。 古いバージョンだとretry_backoff
のみ削除で大丈夫でしたが 最新バージョンだとdefaults_mode_config_resolver
も削除する必要がありました。 削除しても動作には問題なさそうでした。
認証周りの機能はクラスにして それぞれのスクリプトで簡単に使えるようにしているのですが、 AWSCLIも併用して使う場合 認証情報の取得が2回になってしまう問題があります。
AWSCLIを併用する場合の認証
ということで、AWSCLIの一時認証情報ファイルをRubyでも使う方法を考えました。
簡単に書くと、以下のような感じで 一時認証情報ファイル(json)をパースして 環境変数として設定するだけです。
jq嫌いだけど、このくらいはね うん。
awscli_credentials_cache=~/.aws/cli/cache/xxxxx.json export AWS_ACCESS_KEY_ID=$(jq -r ".Credentials.AccessKeyId" < $awscli_credentials_cache) export AWS_SECRET_ACCESS_KEY=$(jq -r ".Credentials.SecretAccessKey" < $awscli_credentials_cache) export AWS_SESSION_TOKEN=$(jq -r ".Credentials.SessionToken" < $awscli_credentials_cache) export AWS_REGION=ap-northeast-1
実際に試してみましょう
$ awscli_credentials_cache=~/.aws/cli/cache/3a9ab49ce67fe5806c5af3d5a378fbbb470561d9.json $ export AWS_ACCESS_KEY_ID=$(jq -r ".Credentials.AccessKeyId" < $awscli_credentials_cache) $ export AWS_SECRET_ACCESS_KEY=$(jq -r ".Credentials.SecretAccessKey" < $awscli_credentials_cache) $ export AWS_SESSION_TOKEN=$(jq -r ".Credentials.SessionToken" < $awscli_credentials_cache) $ export AWS_REGION=ap-northeast-1 $ pry [1] pry(main)> require 'aws-sdk-ec2' => true [2] pry(main)> ec2_client = Aws::EC2::Client.new(); => #<Aws::EC2::Client>
新たに認証情報を取得せずに実行できました。
スクリプト
スクリプトにしたものはこちらです。(macを使っているので、dataがgdateになっています)
有効期限が1分以内に切れる場合は認証情報を再取得するようにしてあります。 テキトーに作ったスクリプトで 一時認証情報のファイルを全消ししているのがイケてないですが、参考にしてみてください。
この方法はRubyスクリプト以外でも、AWSの環境変数で認証情報取得できる機能なら使えます。
例えば Serverless Framework
とか、Terraform
とか (最近 触っていないので、 確認できてませんが)
実際
実際には クラスやモジュールを作成してDRYな感じでスクリプトを作っています。
また、例えば 以下のような機能でRubyを使用しています。
- CloudFormationスタック作成や変更セット作成、変更セットの確認
- ELBへのインスタスの組み込み、切り離し
- セキュリティグループへの追加更新用Webアプリ
- 各種調査
- etc
この中から2つ紹介します。
変更セットの確認機能
CloudFormationスタックの変更セットは 以下を確認してから適応するようにしています。
- テンプレートの差分
- パラメータの変更
- タグの変更
- リソースが作り直されるか
以下が確認時の内容です。
$ ./scripts/check_change_set.sh Lb-Instance-01.yml === Stack Name: Test-Lb-Instance-01 === Diff Check ================================================================ --- [Info]: Lb-Instance-01.yml Template Difference Exist !!! Instance: Instance: InstanceType: { Type: String } InstanceType: { Type: String } Role: { Type: String } Role: { Type: String } Instance: Instance: Type: AWS::EC2::Instance Type: AWS::EC2::Instance > - { Key: addtag, Value: dummy } - | - | RecordSet: RecordSet: Type: AWS::Route53::RecordSet Type: AWS::Route53::RecordSet === Change-Set Check ================================================================ --- - :logical_resource_id: Instance ------------------------- :resource_type: AWS::EC2::Instance :action: Modify :replacement: Conditional !!!!!!!!!!!!!!!!!!!!!!!!! :details: - :attribute: Tags :name: :change_source: - :attribute: Properties :name: InstanceType :change_source: ParameterReference - :attribute: Properties :name: InstanceType :change_source: DirectModification - :logical_resource_id: RecordSet ------------------------- :resource_type: AWS::Route53::RecordSet :action: Modify :replacement: False :details: - :attribute: Properties :name: ResourceRecords :change_source: ResourceAttribute === Change-Set Params Check ================================================================ NAME | CURRENT | NEW | STATUS -------------|----------|-----------|---------- InstanceType | t3.small | t3.medium | change Role | lb | lb | no change === Change-Set Tags Check ================================================================ NAME | CURRENT | NEW | STATUS -----------|---------------------|---------------------|---------- Name | Test-Lb-Instance-01 | Test-Lb-Instance-01 | no change Role | lb | lb | no change CreateUser | dummy | takayama | change === Validation Check ================================================================ --- [Info]: Lb-Instance-01.yml Validation Check OK === Lint Check ====================================================================== --- [Info]: Lb-Instance-01.yml Lint Check OK
補足
- Shellスクリプトを実行してますが、複数のShellスクリプトとRubyスクリプトをラッピングしています。
- テンプレートの差分は 差分と項目名のみ抽出しているのでわかりづらいですが、今回は
- { Key: addtag, Value: dummy }
の行だけが追加になったという結果です。
セキュリティグループへの追加更新用Webアプリ
こちらはWebアプリなのですが、リモートワークが始まった時に 簡易的にセキュリティグループへ各メンバーのIPアドレスを追加したいと思って作成した機能です。
AWSをRubyで操作することに慣れていたので、Railsアプリも わりと簡単に作成することができました。
(この機能自体は とりあえずで作成したものです。 最近 他の施策で ほぼ不要になり 役目を終えました。)
最後に
Rubyのスクリプトって あまり書いている人がいないイメージがあるのですが、けっこう書きやすいのではないかと思います。AWSで情報取得すると複雑なデータ構造で返ってくることが多いので シリアライズが簡単にできるRubyも選択肢に入れても良いかなと思いました。
ただ インフラエンジニアでは さらに書いている人が少ないと思うので、可読性に気を使ったり コメントで説明をしっかり書いたり 他の人の迷惑にならないようにした方が良さそうだなとは思います。(自戒を込めて)
どなたかの参考になれれば幸いです。
こちらでEnigmo Advent Calendar 2022は以上となります。 2023年もよろしくお願いします!