nikkie-ftnextの日記

イベントレポートや読書メモを発信

S3にオブジェクト(ファイル)が存在するか否かシェルスクリプトで確認する 〜提案実装と、行き着くまでの回り道〜

はじめに

YAPC::Kyotoのチケット、2/3(金)までの販売に延長1 nikkieです。

S3にオブジェクトがあるかどうかをシェルスクリプトで確認する方法を考えてみました。
現時点の暫定解としてアウトプットします。
シェルスクリプトAWSに関しては伸びしろ豊富と思いますので、フィードバックは大歓迎です。

目次

実現したいこと

S3のオブジェクト(今回はファイルに限定します)のURIが手元にあります(s3://で始まるURIです)。
このURIが指すオブジェクト(ファイル)があるかどうかシェルスクリプトで知りたいという状況です。
ここでは詳しく立ち入りませんが、存在有無に応じて後く処理を分岐させることを考えていました。

素振り準備

LocalStackで環境構築

Dockerを使ってローカル環境にS3を用意します。
以下の記事のPythonスクリプトを使って、s3://awesome/foo/bar.txtを置きました。

docker runの出力には以下のバージョン記載がありました。

LocalStack version: 1.3.2.dev
LocalStack build date: 2023-01-28
LocalStack build git hash: 63e357fb

AWS CLIの設定

コマンドラインからAWSのサービスを操作するのにAWS CLIを使います。
インストールはこちらを参考にしました。

aws configureでLocalStack用のprofileを用意します2

高速セットアップ - AWS Command Line Interface

$ aws configure --profile localstack
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: us-east-1
Default output format [None]: json

使ったCLIのバージョンは以下です。

$ aws --version
aws-cli/2.1.30 Python/3.8.8 Darwin/18.7.0 exe/x86_64 prompt/off

準備できました!

$ aws --profile localstack --endpoint-url http://localhost:4566 s3 ls s3://awesome/foo/
2023-01-31 19:53:33         11 bar.txt

参考実装とその読み解き

以下を参考にしました。

  1. aws s3 ls --sumの結果を取得
  2. grepでオブジェクトの数を確認
    • grep -sq 'Total Objects: 0'URIで指定したディレクトリにオブジェクトがないことを検出
    • -qは見つかった行を標準出力に書き出さない指定3
    • -sはエラーメッセージを表示しない指定

提案実装(S3にオブジェクトがあるかどうかを確認する方法 暫定解)

exist_or_not.sh

#!/usr/bin/env bash
set -euo pipefail

file_uri=$1
set +e
aws --profile localstack --endpoint-url http://localhost:4566 s3 ls ${file_uri} > /dev/null
ls_code=$?
set -e

if [ ${ls_code} -eq 0 ]; then
  echo "${file_uri} はあります"
else
  echo "${file_uri} はありません"
fi

オブジェクトが存在するURIs3://awesome/foo/bar.txtと、オブジェクトが存在しないURIs3://awesome/foo/baz.txtを渡して実行してみます。

$ ./exist_or_not.sh s3://awesome/foo/bar.txt
s3://awesome/foo/bar.txt はあります

$ ./exist_or_not.sh s3://awesome/foo/baz.txt
s3://awesome/foo/baz.txt はありません

aws s3 lsの終了コード

ドキュメントで見つけられなかったのですが、URIで指定したオブジェクトがない場合、aws s3 lsの終了コードは1になるようです4

$ aws --profile localstack --endpoint-url http://localhost:4566 s3 ls s3://awesome/foo/bar.txt
2023-01-31 19:53:33         11 bar.txt
$ echo $?
0

$ aws --profile localstack --endpoint-url http://localhost:4566 s3 ls s3://awesome/foo/baz.txt
$ echo $?
1

これを利用して、aws s3 ls終了コードで処理を分岐しました。

終了コードの扱い(set +eの理由)

単にaws s3 lsだけを使って実装したスクリプトは、ファイルが存在するときは期待通り動きますが、ファイルが存在しないときにはechoされません。
set -eにより、コマンドの終了コードが0以外だと、そこでスクリプトが終了する5ためです。

aws s3 lsスクリプトが終了しないよう、set +eと書いて、set -eを一時的に無効にしています。

供養したい実装

実はこのエントリを書き始めた時点でのexist_or_not.shは以下の実装でした。
diffのシンタックスハイライトを利用して、大きく違うところを強調しています。

#!/usr/bin/env bash
set -euo pipefail

file_uri=$1
set +e
+ls_output=$(aws --profile localstack --endpoint-url http://localhost:4566 s3 ls ${file_uri} --sum)
+echo "${ls_output}" | grep -sq 'Total Objects: 1'
+grep_code=$?
set -e

if [ ${grep_code} -eq 0 ]; then
  echo "${file_uri} はあります"
else
  echo "${file_uri} はありません"
fi

エントリを書いているうちにaws s3 lsの終了コードだけで判定できることに気付き、その方が実装が簡潔になるので差し替えました。
この実装に至るまでに調べたことを以下にアウトプットしておきます。

aws s3 ls --sum

--sum--summarizeオプションです。

Displays summary information (number of objects, total size). (aws s3 lsのドキュメントより引用)

summary information(要約情報。「Total Objects」「Total Size」のこと)が表示されるようになります。

$ aws --profile localstack --endpoint-url http://localhost:4566 s3 ls s3://awesome/foo/bar.txt --sum
2023-01-31 19:53:33         11 bar.txt

Total Objects: 1
   Total Size: 11

ドキュメントの「Example 5: Summarizing all prefixes and objects in a bucket」も参考になりました。

grep

aws s3 ls --sumの結果の文字列をgrepで検索します。
存在しないオブジェクトのURIを指定したときも--sumオプションにより要約情報は表示されます。

$ aws --profile localstack --endpoint-url http://localhost:4566 s3 ls s3://awesome/foo/baz.txt --sum

Total Objects: 0
   Total Size: 0

そこで(ここではオブジェクトはファイルの想定なので)、aws s3 ls --sumの結果に

  • 「Total Objects: 1」があればURIで指定したオブジェクト(ファイル)が存在する
  • なければ、URIで指定したオブジェクトが存在しない

と判定することにしました。

set +e

ここまでの要素で実装したスクリプトは、ファイルが存在するときは期待通り動きますが、ファイルが存在しないときにはechoされませんでした。
これはaws s3 lsgrepの終了コードによります。

URIに対応するオブジェクトが存在しないときaws s3 lsは終了コード1となります。
set -eによりエラーが送出されたらスクリプトを終了します。

また、grepも検索語句が見つからなかったときに終了コードが1となります。

選択される行が見つかったときの終了ステータスは 0 であり、 見つからなかったときは 1 であり、エラーが起きた場合は 2 です。

Man page of GREP

grepの終了コードは行が見つかったか否かを表すことを知り、これを判定ロジックで使いたいと考えました。

終了コードをまとめると以下のようになります6

ファイル有無 aws s3 lsの終了コード grepの終了コード
あり 0 0
なし 1 1

ファイルがない場合、aws s3 lsgrepも終了コードは0でありません。
aws s3 lsgrepスクリプトが終了しないよう、set +eを使っています。

終わりに

S3にオブジェクト(ファイル)が存在するか否か確認するスクリプトについてアウトプットしました。
aws s3 lsの終了コードを使う考えです。
またこの実装に至るまでにaws s3 ls --sumgrepを調べた回り道もアウトプットしました。

(2023/05/02 追記)set +eが実は不要なことを教えていただく

そうか、if文の条件の部分では終了コードしか見ない(シェルスクリプトが落ちることはない)のか!
arterminalさん、ありがとうございます!


  1. リンク先にありますが、売り切れも近いそうです。お早めに!
  2. LocalStackを使ってローカル環境でS3を立ち上げ、Lambdaでファイルをアップロードしてみた - Qiita を参考にしました
  3. マスタリングLinuxシェルスクリプト 第2版』8.1によると> /dev/nullと同じです
  4. ドキュメントに記載がない情報を使うのは反対という立場も全然あると思います。私は「lsコマンドの終了コードと似ているし、突然実装も変わりづらいだろうから利用しちゃえ」と考えました
  5. set -eエラーが送出されたらそれをハンドルしてから終了するシェルスクリプトの書き方を理解しました(set -e, trap, exit) - nikkie-ftnextの日記で完全に理解しました
  6. この表を書いたのが簡潔な実装に気付くきっかけとなりました