[セッションレポート]Advanced Usage of the AWS CLI #reinvent

2014.11.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、虎塚です。

今日は、re:Invent 2014のセッション「DEV301: Advanced Usage of the AWS CLI」(クラスメソッド訳: AWS CLIの一歩進んだ使い方)をタイムシフト聴講したのでレポートします。前回のAWS Black Belt Tech Webinarでも、ソリューションアーキテクトの皆さんオススメのセッションの1本として取り上げられていましたね。

テーマは、AWS CLIの応用的な使い方の紹介です。講演は、AWSのソフトウェア開発者であるJames Saryerwinnieさんです。

アジェンダ

発表は3つのカテゴリから構成される。

  • 早わかり - AWS CLIの紹介
  • 基本 - 主要機能の説明
  • 応用シナリオ - AWS CLIの応用機能に注目する

早わかり - AWS CLIの紹介

AWS Command Line Interfaceは、AWSサービスを管理するための統合ツール。

  • 3種類のインストール形式をサポート
    • MSI (Windows)
    • Bundled (クロスプラットフォーム)
    • pip (クロスプラットフォーム)

aws configure

対話的に質問に答えることで設定が可能なコマンド。

すべてのAWS CLIコマンドは同じ構造を持っている。"aws"から始まり、コマンド、サブコマンドが続く。たとえば、aws ec2 describe-instancesというコマンドなら、ec2がコマンド(たいていはサービス名)、describe-instancesがサブコマンド(オペレーション)である。

  • コマンド結果のアウトプット形式は3種類
    • JSON形式: プログラムアクセスに便利
    • Text形式: grepやsedに連携しやすい
    • Table形式: 視覚的にわかりやすい

JSON形式だと、たとえば次のようになる。(Text形式とTable形式の見た目については、スライド資料を参照ください)

{
  "Places": [
    {
      "City": "Seattle",
      "State": "WA"
    },
    {
      "City": "Las Vegas",
      "State": "NV"
    }
  ]
}

基本のAWS CLIの使い方デモ

まず、AWS CLIのインストールから設定まで。

$ aws --version
$ pip install awscli
$ aws configure

次に、aws ec2 describe-regionsの実行結果を、前述の3つのアウトプット形式で見てみる。

# JSON形式(今回はデフォルトのoutput形式をJSONにしているので、指定なし)
$ aws ec2 describe-regions
# Text形式
$ aws ec2 describe-regions --output text
# Text形式でリージョン名だけを表示
$ aws ec2 describe-regions --output text | cut -f 3
# Table形式
$ aws ec2 describe-regions --output table

それから、helpコマンドの紹介。

$ aws ec2 run-instances help

ヘルプには、オプション引数の説明やサンプル、いくつかのシナリオが記述されている。

最後に、AWS外部で生成したキー(公開鍵)をAWSにキーペアとしてインポートする方法について。

$ aws ec2 import-key-pair --region us-west-2 --key-name foo --public-key-material file:///...

ローカルのファイルを指定するには、file:///...形式で記述する。

基本 - 主要機能の説明

Configuration

aws configureコマンドで設定できるのは、次の4つの要素。

  • AWS Access Key ID
  • AWS Secret Access Key
  • Default region name
  • Default output format

aws configure で使えるサブコマンドには、次のものがある。

  • list : 一般的な設定項目を一覧表示する
  • get: 設定された値を1つ取得する
  • set: 値を1つ設定する

たとえば、次のコマンドでデフォルトのリージョン名を取得する。

$ aws configure get region

同様に、次のコマンドで、prodグループのリージョンをus-west-2に設定する。

$ aws configure set profile.prod.region us-west-2

profileとは、設定値のセット(グループ)のこと。Productionアカウントのprofile, Developmentアカウントのprofileなどというふうに使い分けると便利。

たとえば、次のコマンドで、"prod"グループを特定して設定をすることができる。

$ aws configure --profile prod

Configuration Files

aws configureコマンドで編集される設定ファイルは2種類ある。

~/.aws/credentials
すべてのAWS SDKによってサポートされる設定ファイル
クレデンシャルだけで構成される(Access Key ID、Secret Access Key、オプションのトークン)
~/.aws/config
CLIだけから利用される設定ファイル
クレデンシャルを含めることもできる(が、デフォルトでは含まれない)

たとえば、次のコマンドを実行すると、2種類の設定ファイルにはどのように値が書き込まれるか。

$ aws configure set profile.prod.aws_access_key_id foo
$ aws configure set profile.prod.aws_secret_access_key bar
$ aws configure set profile.prod.region us-west-2
$ aws configure set profile.prod.output text

まず、prodグループのセクションがなければ作成する。次に、credentialsファイルにaccess_key_idとsecret_access_keyを書き込む。このとき、まだ同じキーがなければ単に行が追加されるが、すでに同じキーの値があるとその行は削除される(同じキーで新しい値が上書きされる)ことに注意。最後に、configファイルにregionとoutputを書き込む。

~/.aws/credentials

[prod]
aws_access_key_id = foo
aws_secret_access_key = bar

~/.aws/config

[profile prod]
region = us-west-2
output = text

configファイル側のセクション定義では、profileプレフィックスが必要なことに注意する。

新規IAMユーザを作成する簡単なシェルスクリプトを例に説明する。

create-new-user.sh

#!/bin/bash

# ユーザreinvent-userを作成
aws iam create-user --user-name reinvent-user

# reinvent-userのアクセスキーを作成
credentials=$(aws iam create-access-key --user-name reinvent-user \
    --query 'AccessKey.[AccessKeyId,SecretAccessKey]' \
    --output text)

# 変数credentialsの値を空白区切りで取得し、access_key_idとsecret_access_keyに設定
access_key_id=$(echo $credentials | cut -d' ' -f 1)
secret_access_key=$(echo $credentials | cut -d' ' -f 2)

# ~/.aws/configにreinventグループとして値を設定
aws configure set profile.reinvent.aws_access_key_id "$access_key_id"
aws configure set profile.reinvent.aws_secret_access_key "$secret_access_key"

aws configureのサブコマンドを活用しよう。

Query

--query (string)
レスポンスデータをフィルタリングするのに使うJMESPathクエリー

--query Processingの実装詳細

  1. コマンドを入力する
  2. パラメータをパースする
  3. HTTPリクエストを投げる
  4. レスポンスをパースする(3.に戻る)
  5. レスポンスをフォーマットする
  6. レスポンスを表示する(ページネーションのために3.に戻る)

レスポンスをパースする箇所に着目してみる。ここでは、レスポンスbodyをパースした後に、--queryを適用する。その後、結果をTableやTextの形式にレンダリングする。

例として、次のようなレスポンスがあるとする。

{
  "Users": [
    {
      "Arn": "arn:aws:iam::...:user/james",
      "USerId": "userid",
      "CreateDate": "2013-03-09T23:36:32Z",
      "Path": "/",
      "UserName": "james"
    }
  ]
}

上のデータに--query Users[0].[UserName,Path,UserId]を適用すると、次のように表示されるだろう。

[
  [
    "james", "/", "id"
  ],
]

JMESPathの紹介

上記サイトのTutorialで、簡単な操作を試せる。JSON用クエリ言語を使って、入力されたJSONからシンプルなアウトプットを作成する。

続いてjmespath/jmespath.terminalを利用して、次のようなデモが行われた。

  • インデックスアクセスによって配列のn個目や-n個目(後ろから数えてn個目)の要素にアクセス
  • []を最後につけることで出力をフラットに変換
  • sum(Contents[].Size)関数でレスポンスに含まれる値のサイズ合計を算出
  • sort_by関数で値を並べ替え

(このデモは結構長かったので省略します。詳細は、動画13:00あたりからのデモを参照ください)

Waiters

Amazon EC2インスタンスの状態遷移

起動されたEC2は、まずpending状態になり、その後running状態になる。running状態のEC2は、Stopされるとstopping状態になり、その後stopped状態になる。

従来のコード例

EC2を起動した後、インスタンスの状態を取得して、running状態に遷移したことを条件式でチェックして(遷移するまで状態をチェックし続けて)、次の処理に移行していた。

#!/bin/bash

# EC2インスタンスを起動してインスタンスIDを取得
instance_id=$(aws ec2 run-instances --image-id ami-12345 \
    --query Reservations[].Instances[].InstanceId \
    --output text)

# 起動したインスタンスの状態を取得
instance_state=$(aws ec2 describe-instances --instance-ids $instance_id \
    --query 'Reservations[].Instances[].State.Name')

# 状態がrunningになるまで待つ
while [ "$instance_state" != "running" ]
do
  sleep 1
  instance_state=$(aws ec2 describe-instances --instance-ids $instance_id \
      --query 'Reservations[].Instances[].State.Name') 
done

上のコードには、いくつかの問題があった。リクエストがタイムアウトした場合を考慮していないために無限ループが発生しうること、失敗ステート(running移行前にterminateされた場合など)を扱っていないこと、多くのコードを手で書かねばならないことなど。

Waiterを使ったコード例

Waiterを使うことで、上のコードを次のように書ける。

ec2-waiters.sh

#!/bin/bash
instance_id=$(aws ec2 run-instances --image-id ami-12345 \
    --query 'Reservations[].Instances[].InstanceId' \
    --output text)
aws ec2 wait instance-running --instance-ids $instance_id

ここで、waitはサブコマンド、instance-runningはwaiter name、--instance-ids $instance_idはdescribe-instancesのオプションである。

waitコマンドによって、インスタンスの状態がrunningになるまで以降の操作をブロックできる。たとえば、instance-idsに5個のインスタンスIDを渡していた場合、5個のインスタンスすべてがrunningになるまで処理を待つ。

応用シナリオ - AWS CLIの応用機能に注目する

Templates

AWS CLIはデータドリブン。インプットもアウトプットも。

JSONモデルの実装詳細

たとえば、インスタンス起動リクエストのデータ構造は、コマンドオプションと次のように対応している。(実行時に解析される)

"RunInstancesRequest": {
  "type": "structure",
  "required": [
    "ImageId",
    "MinCount",
    "MaxCount"
  ],
  "members": {
    "ImageId": {"shape": "String"},    // --image-id
    "MinCount": {"shape": "Integer"},  // --min-count
    "MaxCount": {"shape": "Integer"},  // --max-count
    "KeyName": {"shape": "String"},    // --key-name 
    "SecurityGroups": {                // --security-groups
      "shape": "SecurityGroupStringList",
      "locationName": "SecurityGroup"
    },
  }
},

これらのオプションは、たとえばarguments.jsonとして、単独のテンプレートファイルに切り出すことができる。

arguments.json

{
  "ImageId": "ami-12345",
  "MinCount": 1,
  "MaxCount": 1,
  "KeyName": "id_rsa",
  "SecurityGroups": ["SSH", "web"],
}

切り出したオプションファイルは、コマンド実行時に次のように使用する。

$ aws ec2 run-instances --cli-input-json file://arguments.json

じつは、テンプレートに記述するべきオプションをまとめたJSONスケルトンを、次のコマンドで作成できる。

$ aws ec2 run-instances --generate-cli-skelton

コマンド実行時にオプションを1つ1つ指定する方法以外に、このようにスケルトンを編集してテンプレートとして使う方法がある。生成されたスケルトンをファイルとして保存して、値を編集する。自分にとって必要なオプションだけを残すとよい。

JSONテンプレートを作成・使用するデモ

テンプレートファイルを利用して、Dynamoのテーブルを作成するデモ。

まず、スケルトンを作成して、テンプレートファイルtable.jsonとして保存する。次に、table.jsonを開いてオプションの値を編集し、保存する。(TableNameにjames-test-1という名前を設定する)

$ aws dynamodb create-table --generate-cli-skeleten > /tmp/table.json
$ vim !$

それから、dynamodb create-tableコマンドのオプションに、上のファイルを与えて実行する。

$ aws dynamodb create-table --cli-inpu-json file:///tmp/table.json

最後に、作成されたテーブルに対してdescribe tableコマンドを実行する。テンプレートの値どおりにテーブルが作成されたことを確認する。

$ aws dynamodb describe-table --table-name james-test-1 --output table

さらに、同じコマンドを再実行してみる。この時、table.jsonにはテーブル名が直書きされているので、そのまま実行すると同名のテーブルを作成しようとしてエラーになってしまう。そこで、(テンプレートは書き替えずに)コマンドオプション引数でテーブル名としてjames-test-2を指定する。

$ aws dynamodb create-table --cli-inpu-json file:///tmp/table.json --table-name james-test-2

コマンドオプション引数で与えた値が、テンプレートファイルの値を上書きする。最後に、新しいテーブル名で、同じ内容のテーブルが作成されたことを確認する。

$ aws dynamodb describe-table --table-name james-test-2 --output table

Credential Providers

通常、CredentialLocatorは、~/.aws/credentialsに書かれた値を参照して、認証する。同様に、AssumeRole Providerを実行することもできる。

AWSリソースへのアクセスをデリゲートする(IAMロールの利用)

前提として、ProductionアカウントにS3バケットがあるとする。また、DevelopmentアカウントにIAMユーザがあるとする。このとき、DevelopmentアカウントのIAMユーザに、ProductionアカウントのS3バケットへのアクセスを許可する設定について考える。

まず、ProductionアカウントのS3バケットへのアクセス権限を持つIAM Roleを、Productionアカウント側で定義する。次に、アクセス権限を付与するIAMユーザを指定したTrust Policyを、Productionアカウント側で定義する。

それから、DevelopmentアカウントのIAMユーザが、AWS STSのAssumeRoleをリクエストし、一時的な認証情報を受け取る。最後に、そのIAMユーザは、受け取った認証情報でProductionアカウントのS3バケットにアクセスする。

一時的な認証情報が期限切れしたら、新規にAssumeRoleをリクエストする必要がある。

上記は、次のようなコマンドで実行できる。

$ aws configure set profile.prodrole.role_arn arn:aws:iam...
$ aws configure set profile.prodrole.source_profile dev

リソースprofileは、別のprofileを参照できる。

~/.aws/credentials

[dev]
aws_access_key_id = foo
aws_secret_access_key = bar

~/.aws/config

[profile prodrole]
role_arn = arn:aws:iam...
source_profile = dev

上のconfigファイルで、source_profileの値として記述されている「dev」は、credentialsのdevを参照している。

AWS CLIでロールを利用するデモ

まず、IAM Roleを作成する。この時、AssumeRoleリクエストのためのポリシーを与えておく。Principalで指定するのは、権限を付与する対象のユーザになる。

trust-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-id>:user/james"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
$ aws iam create-role --role-name ReinventProdAccess \
    --assume-role-policy-document file://trust-policy.json \
    --query Role.Arn

次に、上のIAM Roleにポリシーを追加する。S3バケットへのアクセスを許可する。

role-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListAllMyBuckets"],
      "Resource": "arn:aws:s3:::*"

    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::jamesls-reinvent-role"

    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": "arn:aws:s3:::jamesls-reinvent-role/*"
    }
  ]
}
$ aws iam put-role-policy --role-name ReinventProdAccess \
    --policy-name S3BucketAccess --policy-document file://role-policy.json

この時、~/.aws/configは次のようになっているものとする(下半分については後で触れる)。

~/.aws/config

[profile prod-access]
role_arn = arn:aws:iam::<user-account>:role/ReinventProdAccess
source_profile=development

[profile prod-full-s3-access]
role_arn = arn:aws:iam::<user-account>:role/FullS3Access
source_profile=development
mfa_serial=arn:aws:iam::<user-account>:mfa/james

prod-access profileを認証に利用して、s3の特定バケットの一覧を表示してみる。

$ aws s3 ls s3://jamesls-reinvent-role/ --profile prod-access --debug 2>/tmp/debug.log

debugオプションは、トラブルシューティングの情報を確認するのに便利。ここでは、STSを利用して認証情報を得ている様子を見るために使う。

上のコマンドを実行した後、debug.logを確認する。

$ egrep '(assumerole|credentials|endpoint' /tmp/debug.log | less -S

debug.logによると、まず環境変数から認証情報を探し、次にassume-roleを見に行き、認証情報を見つけたことが分かる(「Retrieving credentials via AssumeRole」と出力されている。詳細は動画を参照ください)。

また、同じコマンドを再実行してからdebugログを見ると、「Credentials for role retrieved from cache.」と出力されている。2回目のAPIコールではキャッシュから認証情報を取得したことが分かる。

なお、prod-full-s3-access profileを認証に利用して次のコマンドを実行すると、(profileの記述どおり)MFAを要求される。

$ aws s3 ls --profile prod-full-s3-access
Enter MFA code:
2014-11-07 15:34:17 jamesls-other-bucket
2014-11-06 22:15:15 jamesls-reinvent-role

その直後に、一覧表示されたバケットの1つに対してさらにAPIを叩いても、MFAのプロンプトは表示されない。

$ aws s3 ls s3://jamesls-other-bucket/ --profile prod-full-s3-access

この時も、キャッシュ内の認証情報が利用されていることが分かる。

Amazon S3 Streaming

aws s3 cp

S3からS3へファイルをコピーする際に、ディスクへの書き込みを避けたい。そういう時は、ファイルをストリーミングで扱えばよい。特定のファイル名を記述する代わりに、ダッシュを記述する。

$ aws s3 cp - s3://bucket/key
$ aws s3 cp s3://bucket/key -

これは、S3からファイルをダウンロードして、圧縮し、再アップロードするようなユースケースで便利だ。次のようにパイプを併用して記述するとよい。

$ aws s3 cp s3://bucket/key - | \
    bzip2 --best | \
    aws s3 cp - s3://bucket/key

これによって、ディスクに一時ファイルを書き込まずに済む。

Amazon S3 Streamingデモ

(動画を見る限り、時間の都合で省略された模様)

botocore

アーキテクチャ

AWS CLIがbotocore(Pythonライブラリ)を呼び出すと、botocoreの内部にあるProviders, Serializers, ParsersがHTTPリクエストを作成し、送出する。boto3(AWSのPython版SDK)もAWS CLIと同じ位置にあり、botocoreを外から呼び出す。

サンプルコード

PythonでどのようにAWSにアクセスするかを紹介する。botocoreはpipでインストールできる。

list_buckets.py

import botocore

s = botocore.session.get_session()
s3 = s.create_client('s3', region_name='us-west-2')

for bucket in s3.list_buckets()['Buckets']:
    print("Bucket: {bucket}".format(bucket=bucket['Name']))

python REPLでbotocoreを使用するデモ

ターミナルでpythonを実行して、セッションを取得し、DynamoDBのクライアントにテーブルを問い合わせた。

>>> import botocore.session
>>> s = botocore.session.get_session()
>>> ddb = s.create_client('dynamodb', 'us-west-2')
>>> ddb.list_tables()
# JSON形式のレスポンス(省略)
>>> response = _
>>> response['TableNames']
[u'james-test-1', u'james-test-2']
>>> response['TableNames'][0]
u'james-test-1'
>>>

まとめ

  • Configuration: configureコマンドで何ができるか
  • Waiters
  • Query: Arrayアクセス
  • Templates: プログラマティックに入力を与える方法
  • Credential Providers: クロスアカウントのアクセス
  • Amaozn S3 Streaming
  • boto3の紹介

参考資料

感想

AWS CLIの応用的な使い方ということで、デモも交えて様々な話を聴くことができて良かったです。セッションの途中で知りましたが、講演されたJames SaryerwinnieさんはJMESPathを開発された方なんですね。どおりでJMESPathのデモに力が入っていたわけです。レポートではその部分を少し省略してしまったので、気になる方は動画をご覧ください。

個人的には、public-key-importと、S3 Streamingに驚きました。恥ずかしながら、こんな機能があったとは知りませんでした。これから使っていこうと思います(もっとも、Key Management Serviceが来たおかげで、前者は今後それほど使わないかもしれませんが)。

ちなみに、Waitersについては、大瀧さんの記事で使い方が分かりやすく解説されていますので、気になる方はご覧ください。

それでは、また。