PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

あまり知られていない AWS S3 CLI の便利な使い方と注意すべきポイント 4 選

こんにちは、SaaS 事業部の髙橋と申します。

AWS のサービスのひとつにファイルをクラウド上に保存できる Amazon S3 というサービスがあります。この S3 は AWS CLI を使ってコンソール上で操作することもできます。今回は AWS CLI を使って S3 を操作する方法を解説したいのですが、基本的な使い方は様々なサイトでもよく解説されていますので、この記事ではあまり解説されない仕様や使い方をピックアップして解説していきます。

標準入力と標準出力

aws s3 cpコマンドではファイル名に-を指定することで標準入力と標準出力を使用できます。

例を示します。まずは簡単なテキストを標準入力からアップロードし、アップロードした S3 上のテキストファイルを標準出力してみます。

$ echo 'test message.' | aws s3 cp - s3://s3-bucket/blog/text.txt

$ aws s3 cp s3://s3-bucket/blog/text.txt -
test message.

例えば CloudFront のアクセスログは gzip 形式で圧縮されているので、標準出力をパイプで渡しzcatコマンド(Mac ではgzcat)やzlessコマンドを使用することで、ファイルを保存せずに閲覧することができます。

$ aws s3 cp s3://cloudfront-log-bucket/prefix/TESTTEST12345.2023-02-20-01.1234abcd.gz - | zless
省略

tarコマンドを使用すればディレクトリを tar 形式にして標準入力から直接アップロードができます。ダウンロードも標準出力からtarコマンドに渡すことで tar ファイルをローカルに生成せずに展開できます。

$ find output/ | wc -l
310

$ time tar c output | aws s3 cp - s3://s3-bucket/output.tar --quiet
real    0m2.642s
user    0m1.984s
sys     0m0.956s

$ cd directory/

$ time aws s3 cp s3://s3-bucket/output.tar - --quiet | tar x
real    0m7.351s
user    0m3.328s
sys     0m2.109s

$ find output/ | wc -l
310

# 一応 --recursive でアップロードするよりも早いという結果になった
$ time aws s3 cp output/ s3://s3-bucket/output/ --quiet --recursive
real    0m4.619s
user    0m3.734s
sys     0m1.308s

$ time aws s3 cp s3://s3-bucket/output/ output1 --quiet --recursive
real    0m7.375s
user    0m4.867s
sys     0m1.859s

Content-Type の自動設定

S3 のオブジェクトはメタデータに Content-Type を持つことができます。aws s3 cpなどのコマンドを使ってファイルをアップロードすると、AWS CLI がファイル名の拡張子から Content-Type を自動的に設定してくれます(--no-guess-mime-type--content-typeのオプションを指定した場合は自動的に設定しません)。例えば AWS CLI で video.mp4 というファイル名のファイルをアップロードすると、Content-Type には video/mp4 が自動的に設定されます。

この AWS CLI がファイル名の拡張子から Content-Type をどう判定しているのか、それは Python の mimetypes モジュールから guess_type() を呼び出して判定しています*1

mimetypes モジュールがどこから MIME の一覧を持ってきているかは以下のように確認できます。(AWS CLI v2 は Python を内蔵していますが、その Python を実行することはできなかったので、別途 Python をインストールしています)

>>> import mimetypes
>>> mimetypes.knownfiles
['/etc/mime.types', '/etc/httpd/mime.types', '/etc/httpd/conf/mime.types', '/etc/apache/mime.types', '/etc/apache2/mime.types', '/usr/local/etc/httpd/conf/mime.types', '/usr/local/lib/netscape/mime.types', '/usr/local/etc/httpd/conf/mime.types', '/usr/local/etc/mime.types']

Windows ではさらにレジストリの HKEY_CLASSES_ROOT からも一覧を持ってきています。

逆に上記のファイル一覧の中に自分で MIME を定義することで、この自動判定に追加することができます。自分で MIME を定義することは、あるディレクトリ内の様々な拡張子のファイルをまとめてアップロードするときに便利です。

例を示します。3 つの動画ファイルが入ったディレクトリを S3 にアップロードし、それぞれの Content-Type を取得してみます。

$ ls
movie.mov  mpeg2.ts  test.mp4

$ aws s3 cp . s3://s3-bucket --recursive
upload: ./test.mp4 to s3://s3-bucket/test.mp4
upload: ./mpeg2.ts to s3://s3-bucket/mpeg2.ts
upload: ./movie.mov to s3://s3-bucket/movie.mov

$ aws s3api head-object --bucket s3-bucket --key test.mp4 --query 'ContentType'
"video/mp4"

$ aws s3api head-object --bucket s3-bucket --key mpeg2.ts --query 'ContentType'
"text/vnd.trolltech.linguist"

$ aws s3api head-object --bucket s3-bucket --key movie.mov --query 'ContentType'
"video/quicktime"

拡張子が mp4, mov のファイルについては想定通りの Content-Type が付きました。しかし拡張子が ts のファイルには想定外の Content-Type(text/vnd.trolltech.linguist)が付いてしまいました。

拡張子が ts のファイルにも想定される Content-Type が付くように MIME を定義して、もう一度 S3 にアップロードしてみます。

$ echo -e 'video/MP2T\tts' >> /usr/local/etc/mime.types

$ cat /usr/local/etc/mime.types
video/MP2T      ts

$ aws s3 cp . s3://s3-bucket --recursive
upload: ./test.mp4 to s3://s3-bucket/test.mp4
upload: ./mpeg2.ts to s3://s3-bucket/mpeg2.ts
upload: ./movie.mov to s3://s3-bucket/movie.mov

$ aws s3api head-object --bucket s3-bucket --key mpeg2.ts --query 'ContentType'
"video/MP2T"

拡張子が ts のファイルに自分で定義した Content-Type が付きました。 一応、S3 にアップロードするときに--content-typeオプションでも Content-Type を指定できますが、1 つしか指定できないため、ファイル数が多いときや他の拡張子のファイルもまとめてアップロードするときには今回の方法のほうが楽です。

sync コマンドでファイルが変更されたと判定される条件

aws s3 syncは S3 からローカルに、またはローカルから S3 に変更が加えられたファイルだけをコピーすることでファイルの同期を行えるコマンドです。このファイルが変更されたと判定される条件は「コピー先の更新日時がコピー元の更新日時よりも新しい」または「ファイルサイズが異なる」となっています。

しかしこの挙動ですと S3 からローカルにダウンロードして同期した後に、S3 のファイルが変更(ファイルサイズは同じ)されてしまうと困ってしまいます。

例を示してみます。まずは古い内容のファイル(text_old.txt)と新しい内容のファイル(text_new.txt)をローカル上に用意します。

$ echo 'test message old.' > text_old.txt

$ echo 'test message new.' > text_new.txt

$ ls -l
total 8
-rw-rw-r-- 1 root root 18 Feb 27 15:25 text_new.txt
-rw-rw-r-- 1 root root 18 Feb 27 15:24 text_old.txt

次に古い内容のファイルを S3 上に text.txt としてアップロードします。さらにローカルに同期します。

$ aws s3 cp text_old.txt s3://s3-bucket/blog/text.txt
upload: ./text_old.txt to s3://s3-bucket/blog/text.txt

$ aws s3 sync s3://s3-bucket/blog ./
download: s3://s3-bucket/blog/text.txt to ./text.txt

$ ls -l
total 12
-rw-rw-r-- 1 root root 18 Feb 27 15:25 text_new.txt
-rw-rw-r-- 1 root root 18 Feb 27 15:24 text_old.txt
-rw-rw-r-- 1 root root 18 Feb 27 15:25 text.txt

$ cat text.txt
test message old.

同期のときに text.txt がダウンロードされ、text_old.txt と同じ内容となっています。

次に新しい内容のファイルをS3上に text.txt としてアップロードして上書きします。さらにローカルに再び同期します。

$ aws s3 cp text_new.txt s3://s3-bucket/blog/text.txt
upload: ./text_new.txt to s3://s3-bucket/blog/text.txt

$ aws s3 sync s3://s3-bucket/blog ./

$ ls -l
total 12
-rw-rw-r-- 1 root root 18 Feb 27 15:25 text_new.txt
-rw-rw-r-- 1 root root 18 Feb 27 15:24 text_old.txt
-rw-rw-r-- 1 root root 18 Feb 27 15:25 text.txt

$ cat text.txt
test message old.

$ aws s3 ls s3://s3-bucket/blog/text.txt
2023-02-27 15:26:02         18 text.txt

同期のときに text.txt はダウンロードされず、text_new.txt と同じ内容になりませんでした。

同期のときに text.txt がダウンロードされるようにするには、--exact-timestamps というオプションを使用します。このオプションを使用するとファイルが変更されたと判定される条件が「アップロード先の更新日時がアップロード元の更新日時よりも新しい」から「アップロード先の更新日時がアップロード元の更新日時と異なる」に変更されます。なおこのオプションは S3 からローカルにファイルをダウンロードして同期するときにしか使用できません。

$ aws s3 sync s3://s3-bucket/blog ./ --exact-timestamps
download: s3://s3-bucket/blog/text.txt to ./text.txt

$ cat text.txt
test message new.

同期のときに text.txt がダウンロードされ、text_old.txt と同じ内容になりました。

他のアカウントのバケットにアップロードするときの注意点

他のアカウントの S3 バケットにアップロードする方法はいくつか存在します。その中でバケットポリシーを利用してかつアクセスコントロールリスト(ACL)を有効にしている場合は注意が必要です。これは AWS CLI に限った話ではなく S3 そのものの仕様なのですが、アクセス権を適切に設定しないと、S3 バケットの所有者がオブジェクトを読み取れないといったことが起こります。(もちろんその仕様でオブジェクトを管理している場合は別です)

例を示します。この例では account-b が account-a の持つ S3 バケットにファイルをアップロードし、account-a がアップロードされたファイルをダウンロードするという例を示しています。なおバケットポリシーは設定済みとします。

$ aws s3 cp text.txt s3://account-a-bucket --profile account-b
upload: ./text.txt to s3://account-a-bucket/text.txt

$ aws s3 ls s3://account-a-bucket/text.txt --profile account-b
2023-02-27 18:29:02         18 text.txt

$ aws s3 ls s3://account-a-bucket/text.txt --profile account-a
2023-02-27 18:29:02         18 text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-b
download: s3://account-a-bucket/text.txt to ./text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-a
fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden

account-b は自らがアップロードしたファイルをダウンロードできましたが、S3 バケットの所有者である account-a はファイルをダウンロードできませんでした。

account-a がファイルをダウンロードできるようにするには、account-b がファイルのアップロード時に --acl bucket-owner-full-control オプションを使用してバケット所有者にアクセスの許可を与える必要があります。

$ aws s3 cp text.txt s3://account-a-bucket --acl bucket-owner-full-control --profile account-b
upload: ./text.txt to s3://account-a-bucket/text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-b
download: s3://account-a-bucket/text.txt to ./text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-a
download: s3://account-a-bucket/text.txt to ./text.txt

account-a がファイルをダウンロードできるようになりました。

--grants オプションに account-a の正規 ID (canonical user ID) を指定する方法もあります。正規 ID はバケット所有者が aws s3api get-bucket-acl を実行すると簡単に取得できます。この方法ではバケット所有者以外のアカウントにもアクセス許可を与えることができます。

$ CANONICAL_USER_ID=$(aws s3api get-bucket-acl --bucket account-a-bucket --query 'Owner.ID' --output text --profile account-a)

$ aws s3 cp text.txt s3://account-a-bucket --grants full=id=${CANONICAL_USER_ID} --profile account-b
upload: ./text.txt to s3://account-a-bucket/text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-b
download: s3://account-a-bucket/text.txt to ./text.txt

$ aws s3 cp s3://account-a-bucket/text.txt . --profile account-a
download: s3://account-a-bucket/text.txt to ./text.txt

正規 ID を指定する方法でも account-a がファイルをダウンロードできるようになりました。

終わりに

今回は AWS S3 を AWS CLI で操作する際の小ネタをまとめてみました。何かのお役に立てれば幸いです。

S3 向けのコマンドにはまだここで紹介していない機能やオプションがありますので、色々と試してみると何か発見があるかもしれません。

参考

cp — AWS CLI 2.11.8 Command Reference

sync — AWS CLI 2.11.8 Command Reference