AWSとRubyは相性が良いと思う話

こんにちは、インフラエンジニアの 高山 です。

この記事は Enigmo Advent Calendar 2022 の25日目の記事です。

はじめに

エニグモ入社後に AWSを徐々に触れ始め 数年経過しました。 最初はWebコンソールからポチポチしていたのですが、CloudFormationでコード化を進めたり AWSCLIやSDKを使うようになり、最近は RubyAWSリソースの情報取得や操作をすることが多くなってきました。 (以下、RubyでAWSリソースの情報取得や操作をすることをAWSをRubyで操作すると表現します)

AWSRubyで操作するようになった理由は以下です。

  • そもそもWebコンソールは苦手(というより怖い)で、CLIで操作したい派である
  • 構造的なデータを扱うのは Shellスクリプトだと厳しい(jqが嫌い)
  • Ruby on Rails(以下、Railsと表記します)を使ったことがあるので、Rubyに少しは慣れている
  • BUYMAではRailsを採用しているので、社内で聞ける

AWSを操作するには python(+boto3)を使っているユーザの方が圧倒的に多そうな気がしますが、pythonと比較しているわけではないので、片手落ち感は否めない記事になってます。

相性が良いと思う理由

AWSRubyは相性が良いと思う理由、それはデータのシリアライズが簡単にできるからです。

この一点に尽きるので、ここで記事終了でも良いくらい。

シリアライズとは

シリアライズというのは データやオブジェクトをファイルに書き込みできるように変換することです。

反対はデシリアライズ。 言いにくいですね。

やってみよう

シリアライズ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-ec2requireする必要はあります

$ 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_tokennilになるまでループする必要があります。 (データが少なければ不要です)

ここは できるだけフィルターした方が良いですが、今回は検証なのでしていません。
データ取得は数秒程度ですが、検証のたびに取得し直すは地味にストレスですよね。

$ 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になっていると、可読性が高いところもメリットです!

いったん、結論

ということで、シリアライズの簡単さと メリットがわかっていただけたのではないかと思います。

メリットまとめ

  • そのままのデータ構造で保存できる
  • インタラクティブシェルでの確認や検証がやりやすい
  • データの取得とデータ整形などの処理を分けやすい
  • yamlで保存するので 可読性が高い

Rubyでの認証情報の取得

これまでの検証では わかりやすくするため Read権限のあるEC2上で実行していたので 認証情報の取得は不要でしたが、PC上など他の場所でAWSSDKを使用する場合は 認証情報の取得が必要です。

$ 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だと、一時認証情報が保存され再利用できますが、RubySDKを使ってスクリプトを作っている場合は再利用できず、スクリプトを実行するたびに認証情報を取得し直さないといけないのが難点でした。

しかし、それも シリアライズで解決できます。 さすがシリアライズさん!

以下のような感じです。

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アドレスを追加したいと思って作成した機能です。 AWSRubyで操作することに慣れていたので、Railsアプリも わりと簡単に作成することができました。
(この機能自体は とりあえずで作成したものです。 最近 他の施策で ほぼ不要になり 役目を終えました。)

セキュリティグループへの追加更新用Webアプリ

最後に

今回は Rubyスクリプトの話をしました。

Rubyスクリプトって あまり書いている人がいないイメージがあるのですが、けっこう書きやすいのではないかと思います。AWSで情報取得すると複雑なデータ構造で返ってくることが多いので シリアライズが簡単にできるRubyも選択肢に入れても良いかなと思いました。

ただ インフラエンジニアでは さらに書いている人が少ないと思うので、可読性に気を使ったり コメントで説明をしっかり書いたり 他の人の迷惑にならないようにした方が良さそうだなとは思います。(自戒を込めて)

どなたかの参考になれれば幸いです。

こちらでEnigmo Advent Calendar 2022は以上となります。 2023年もよろしくお願いします!