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