AWS CLI で取得した情報をシェルスクリプトで使うときの Tips
はじめに
AWS 上のリソースの操作を自動化したいとき、ちょっとしたことなら AWS CLI とシェルスクリプトで書くのが一番お手軽だと思います。ただ、きちんとソースコード管理されるようなスクリプトであれば、例えば EC2 の Instance ID のような自動採番値はスクリプトに仕込みたくないので、list や describe 系のコマンドで情報を取得したうえでゴニョゴニョしたくなります。
これらの参照系コマンドは実にたくさんの情報を返してくるので、欲しい情報だけ抜き出さないといけませんが、そのあたりのやり方を書いておきます。
基本: --query オプション
AWS CLI の出力から抽出・加工する方法として、 --query オプション を使う方法と、JSON 出力して jq
で加工する方法をよく見かけますが、jq
はデフォルトインストールされていない環境も多いので、個人的には --query
を好んで用います。
例えば、 Name タグが hoge-instance
の インスタンスIDを取りたい場合を考えます。以下のコマンドを叩くと、インスタンスの細かい情報が取得できます。
$ aws ec2 describe-instances \ --filter "Name=tag:Name,Values=hoge-instance" --output json { "Reservations": [ { "Groups": [], "Instances": [ { "AmiLaunchIndex": 14, "ImageId": "ami-gej3kjfo9", "InstanceId": "i-0f4f44a816fexample", "InstanceType": "c4.xlarge", ... }
ここから InstanceId
だけ取り出したいので、出力の階層構造を踏まえ、以下のように --query
を設定します。
$ aws ec2 describe-instances \ --filter 'Name=tag:Name,Values=hoge-instance' \ --query 'Reservations[0].Instances[0].InstanceId' --output text i-0f4f44a816fexample
--output text
としていますが、 json
のままだと、出力に引用符が残ります。 --output
の設定値を変えても、クエリの抽出結果には影響しません。
シェルスクリプト内では、こうして必要な情報を抽出できるコマンドを書き、 $()
で変数に代入して扱う形が多くなると思います。
一度のコマンドで複数の値を取りたい場合
一度のコマンド実行結果から複数の値を取りたい場合は、 set
コマンドが便利です。例えば、上記の結果から InstanceId
と ImageId
を取りたいというような場合は以下のように書きます。
set $(aws ec2 describe-instances \ --filter 'Name=tag:Name,Values=hoge-instance' \ --query 'Reservations[0].Instances[0].[InstanceId,ImageId]' \ --output text) INSTANCE_ID=${1} IMAGE_ID=${2}
query で [InstanceId, ImageId]
のように配列に括るようにして text 形式で出力すると、各値がスペース区切りで出力されます。これを set
に与えることで位置パラメータ $1, $2, ...
に代入されて、個別に取り出すことができるようになります。
query の JMESPath ?演算子 を組み合わせると、順不同で帰ってくるリストから、特定の条件にマッチするものを複数一度に取得することができます。以下の例は、 ALB に複数設定されたリスナのリストから、http と https の各リスナの ARN を抽出する例です。
set $(aws elbv2 describe-listeners --load-balancer-arn $load_balancer_arn \ --query '[Listeners[?Port==`80`].ListenerArn,Listeners[?Port==`443`].ListenerArn]' \ --output text) local listener_arn_http=${1} local listener_arn_https=${2}
カンマ区切りリストとして取得する
CloudFormation などは、複数値をとるパラメータの形式として、カンマ区切りリストが使えます。このようなカンマ区切りリストを手軽に作る方法です。
例えば、ある ECS Service のタスクが展開される Subnet のリストを取りたいときは以下のようなコマンドになります。
$ aws ecs describe-services --cluster the-cluster --services the-service \ --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \ --output text subnet-xxxxxxxx subnet-yyyyyyyy
これをカンマ区切りリストにするには printf
コマンドが使えます。
$ printf '%s,' \ $(aws ecs describe-services --cluster the-cluster --services the-service \ --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \ --output text) subnet-xxxxxxxx,subnet-yyyyyyyy,
ただしこれでは、末尾にもカンマが付加されてしまうため、変数展開を使って末尾のカンマを取り除きます。 スクリプトではこんな感じ。
SUBNET_LIST=$(printf '%s,' \ $(aws ecs describe-services --cluster the-cluster --services the-service \ --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \ --output text)) echo ${SUBNET_LIST%,} # 出力結果 # subnet-xxxxxxxx,subnet-yyyyyyyy
その他細かいネタ
よく必要になる AWSアカウントID を取得するスニペットです。
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
おわりに
だいたいのケースは、ここに書いたように、AWS CLI とシェルの組み込みコマンドで何とかなってしまうので、生の ID をスクリプトに埋め込まないようにしましょう。
Mac の iTerm2 上で Home/End キーを使いたい
MacBook に PC 用キーボードをつないで使っていますが、Terminal での行頭/行末移動が不便。なので、Windows 同様、Home/End キーで行頭/行末移動ができるようになりたい。
というのを実現したのでその手順を。
なお、 以下手順は iTerm2 で確認しています。標準のターミナルでは使えないようです。
Home/End キーの Escape Sequence を確認
iTerm2 上でCtrl+V
の直後に特殊キーを押すと、そのキーのエスケープコードが表示されます。例えばCtrl+V
Home
と押すと、^[[H
、Ctrl+V
End
で^[[F
が出力されます。bindkey コマンドでエスケープコードと動作の関連付けを行う
(zshの場合 )1 で確認した Escape Sequence にカーソル移動の動作を紐つけます。(.zshrc
などに追記しておくといいでしょう)
bindkey "^[[H" beginning-of-line bindkey "^[[F" end-of-line
※試してませんが、bash なら bind -x
でも同様のことができるかも知れません。
以上で、 iTerm2 上で Home/End キーによる行頭/行末移動ができるようになります。
anyenv で tfenv を入れると init でエラーが出るので回避する
数多ある *env 系のツール導入をぐっと簡単にしてくれる anyenv ですが、現行バージョン (1.1.1) では、tfenv を導入すると、シェルログイン時にエラーが出るようになります。
No such command 'init' Usage: tfenv <command> [<options>] Commands: install Install a specific version of Terraform use Switch a version to use uninstall Uninstall a specific version of Terraform list List all installed versions list-remote List all installable versions
anyenv はログイン時、導入されている *env すべての init
サブコマンドを順に実行するのですが、 tfenv には init
サブコマンドがないのでエラーとなるようです。
修正 PR も上がっているようなのでそのうち取り込まれると思いますが、当面の回避方法のメモ。
ここでは、Mac で homebrew を使って anyenv をインストールした前提で書きます。
anyenv のインストール先を確認
% brew ls anyenv /usr/local/Cellar/anyenv/1.1.1/bin/anyenv /usr/local/Cellar/anyenv/1.1.1/completions/ (3 files) /usr/local/Cellar/anyenv/1.1.1/libexec/ (14 files)
で、 libexec
以下の anyenv-init
を修正します。
% cp /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup % vim /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init
修正内容は以下のような感じ。
% diff -up /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init --- /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup 2020-01-26 22:57:10.000000000 +0900 +++ /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init 2020-01-26 22:59:09.000000000 +0900 @@ -140,5 +140,5 @@ for env in $(anyenv-envs); do ;; esac - echo "$(${ENV_ROOT}/bin/${env} init - ${no_rehash_arg}${shell})" + [ "${env}" != 'tfenv' ] && echo "$(${ENV_ROOT}/bin/${env} init - ${no_rehash_arg}${shell})" done
homebrew を使わず git clone
でインストールした場合は、 .anyenv/libexec/anyenv-init
を同じように修正すればOKです。
本当はちゃんと 「init のない *env」を判定したいところなんですが、まあ一時的な回避策ということで。
CircleCI で CIRCLE_SHA1 を使う場合の注意
過去に自分がミスった内容と似た事例を他にも見かけたので、今更ながらメモ。
TL; DR
- CircleCI の ECS/ECR へのデプロイに関するドキュメントで、コンテナイメージのタグに
CIRCLE_SHA1
の値が設定されているが、これには要注意 - CircleCI における
CIRCLE_SHA1
の値は、最終コミットを識別するハッシュ値だよ - Git上のブランチやタグに依って変わるわけではないので、ブランチ・タグによって異なるものの識別には使えないよ
- イメージタグのような識別子を作成する場合、それが必要なユニーク性を持っているか確認することが必要だよ
CircleCI で ECR/ECS にデプロイするときのコンテナイメージタグ
CircleCI 公式のドキュメントとして、以下の記事が公開されています。 (2019/12/15現在)
上記記事より引用
version: 2.1 orbs: aws-ecr: circleci/aws-ecr@0.0.2 aws-ecs: circleci/aws-ecs@0.0.3 workflows: build-and-deploy: jobs: - aws-ecr/build_and_push_image: account-url: "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" repo: "${AWS_RESOURCE_NAME_PREFIX}" region: ${AWS_DEFAULT_REGION} tag: "${CIRCLE_SHA1}" - ...
ビルドの結果出来上がる個々の コンテナイメージを識別するものとしてタグを設定します(上記例の tag
)が、ここでは、タグに設定する値は "${CIRCLE_SHA1}"
となっています。
(以前のドキュメントでは、普通に docker
コマンドを使用して docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com:${CIRCLE_SHA1}
という感じになっていたはず…)
CIRCLE_SHA1 って?
さて、この CIRCLE_SHA1
に設定される値はどのようなものでしょうか。こちらも CircleCI の公式ドキュメントで説明されています。
VCS が Git であれば、 Git のコミットハッシュと同一の値になります。CircleCI でビルドを開始する際に Git リポジトリから取得した内容 (バージョン断面) を識別するものと考えていいと思います。
コンテナイメージタグに CIRCLE_SHA1 を設定すると
この CIRCLE_SHA1
をコンテナイメージタグに使うと、どうなるでしょう?
「Git リポジトリの内容が同一ならば、ビルドされるコンテナイメージも同一である」という条件が成り立つのであれば、これで問題ありません。CIRCLE_SHA1
がリポジトリの内容ごとにユニークであるので、そこと 1:1 に対応付けられるコンテナイメージも CIRCLE_SHA1
で識別することができます。
しかし、上記の条件が必ずしも成り立たない場合、つまりGitリポジトリが同一であっても、できあがるコンテナイメージが異なる場合がある(異なるイメージそれぞれを管理する必要がある)場合は注意が必要です。
例えば、よく見かける方式として、本番環境やステージング環境にリリースを行うために、Git 上の master から特定のブランチにマージし、それをトリガーとして CircleCI で各環境向けのビルドおよびデプロイを行う、という方式を考えます。
コンテナイメージ内に環境の情報を含める場合は、当然、リポジトリの内容が同一であっても、本番環境向けとステージング環境向けでできあがるコンテナイメージに差異が発生します。しかし、 CIRCLE_SHA1
の値は、トリガーとなったブランチに依らず。あくまでも最終コミットのハッシュ値によって決定されるため、それぞれのコンテナイメージに同一のタグが設定されることになります。
Docker および ECR の仕様上、すでに存在するタグと同一のタグを付与して Push した場合、古い方のイメージからはタグが剥がされてしまうため、例えば ステージング環境へのデプロイ後に本番環境へのデプロイを行った場合、ステージング環境の ECS クラスタにも本番環境用のコンテナイメージがデリバリされてしまうことになります。また、並行して複数環境のビルド・デプロイを行って、完了する順番が不定となるような場合、見かけ上ランダムにコンテナイメージが切り替わるため、再現検証の難しい障害となることがあります。
回避方法
この問題をするには、以下のやり方が考えられます
インフラ構成や実装の手間、ユニーク性の要件、費用等に合わせた方法を取るとよいでしょう。
まとめ
CIRCLE_SHA1
(に限りませんが) を何かの識別子として使用する場合、そのユニーク性が識別する対象のユニーク性と合致するかどうか、毎回きちんと検証しましょうというお話でした。
必要最低限のrequirements.txtを作成する (virtualenv版)
2017年に書いた、「必要最低限のrequirements.txtを作成する」という記事に未だに多少のアクセスがありまして、今現在でこれを参考にされてしまうとちょっとアレだなあと思いましたので。
(当時は Docker 憶えたてでイキっていました。反省)
クリーンな Python 環境を得るには virtualenv がおすすめ
virtualenv は、システムの Python 環境から、任意のディレクトリ以下に最低限のライブラリや実行ファイルをコピーし、クリーンな仮想 Python 環境を構成できるツールです。
通常は、開発したいプロジェクトやプロダクトごとに仮想環境を構成し、そのプロダクトに必要なライブラリだけ仮想環境にインストールして使用する、という使い方になると思いますが、開発で試行錯誤した結果、開発環境がプロダクションに不要なライブラリで汚染されていて整理したい、なんてこともあると思います。
そんなときは慌てず、別のディレクトリに改めてクリーンな仮想環境を作って、作成したソースコードをコピーした上で、改めて必要最低限のライブラリを pip install
してテストし、問題なければ pip freeze
してあげれば良いです。
使い方
virtualenv が未インストールであれば、インストールします。 pip install virtualenv
でインストールできますが、特定のバージョンを入れたいなどオプションについてはドキュメントを参照。
仮想環境を作りたいディレクトリに移動し、環境を作成します。
$ mkdir -p ~/path/to/project $ cd ~/path/to/project $ virtualenv . Using base prefix '/home/user/.pyenv/versions/3.7.5' New python executable in /home/user/path/to/project/bin/python3.7 Also creating executable in /home/user/path/to/project/bin/python Installing setuptools, pip, wheel... done.
すると、このディレクトリの下に bin
, include
, lib
というディレクトリができます。bin
以下にある activate
というスクリプトを source すると、仮想環境を使用するモードになります。
$ source bin/activate (project) $
プロンプトに (ディレクトリ名)
が付加されていれば、そのディレクトリの仮想環境に入っている状態です。
この状態で、必要なライブラリを pip install
し、 pip freeze
すれば、明示的にインストールしたライブラリと依存ライブラリだけのリストが出力されます。
仮想環境から出たい場合は deactivate
コマンドを実行する ( activate 時に alias が設定されている ) か、シェルからログアウトしてください。
細かすぎて伝わらないJava10のvar仕様
いろいろ試したのでメモ。
初期化時に型が決まる
まあ当たり前ですが。
public class Main { public static void main(String... args) { var hoge = 1; // hoge は int型になる hoge = 2147483648; // Integer.MAX_VALUE + 1 System.out.println(hoge); } }
> javac Main.java Main.java:4: エラー: 整数2147483648が大きすぎます hoge = 2147483648; // Integer.MAX_VALUE + 1 ^ エラー1個
以下のようににすると、iはint型になるので桁溢れして無限ループになります。
for (var i = 0; i < 2147483648; i++) { ....
プリミティブ型も推論してくれる
public class Main { public static void main(String... args) { var i = 1; i.getClass().getName(); } }
>javac Main.java Main.java:4: エラー: intは間接参照できません i.getClass().getName(); ^ エラー1個
余計な Boxing/Unboxing は発生しないようで一安心。
匿名クラスのインスタンスで初期化すると匿名クラス型の変数になる
var hoge = new Object() { int a = 1; String s = "Hello!"; int func(int a, int b) { return a + b; } };
のようにすると、hoge
は Object 型ではなくここで定義された匿名クラスの型 (Main$1とか) になります。
従って、この同じメソッドの中であれば、 hoge.a
や hoge.func(1, 2);
のように匿名クラス内のメンバを Interface定義なしで 呼び出すことができます。おもしろいけど使い所あるかな?
以降思いついたら順次追加
無限Streamに終了条件を設定する (Java9版)
というのを以前書きましたが、Java9では Stream#takeWhile
というメソッドが追加されてこういうケースが簡単に書けるようになりました。
public static String getTreePathTakeWhile(Node node, String delimiter) { List<String> names = Stream.iterate(node, Node::getParent) .takeWhile(n -> n != null) .map(n -> n.getName()) .collect(Collectors.toList()); Collections.reverse(names); return delimiter + String.join(delimiter, names); }
これでSupplierを使った無限Streamがとても使いやすくなりますね。