全力で怠けたい

怠けるために全力を尽くしたいブログ。

特定の EC2 インスタンスの情報を AWS CLI で取得する方法のメモ。

特定の EC2 インスタンスの情報を AWS CLI で取得することがちょいちょいあるのでコマンドをメモしておく。

特定の EC2 インスタンスの情報を AWS CLI で取得する

特定の EC2 インスタンスの情報を AWS CLI で取得することがちょいちょいある。

多くの場合は EC2 インスタンスはたくさんあると思うので特定の EC2 インスタンスの情報を取得するには EC2 インスタンスの属性で絞り込むけど、自分はたいがいは EC2 インスタンスにタグを付けてタグで EC2 インスタンスを絞り込んでる。

そのへんをメモしていく。

EC2 インスタンスをタグで特定する

MyFavoriteInstance みたいなタグを付けてる EC2 インスタンスはこんな感じのコマンドで取得してる。

$ aws ec2 describe-instances | \
    jq -r '.Reservations[].Instances[]' | \
    jq -r 'select(.Tags[].Value | contains("MyFavoriteInstance"))'

タグの絞り込みは jq を使ってて複数のフィルターを指定するときは jq コマンド自体をパイプで接続して取得してるけど、そのへんは jq のフィルターでパイプを使って jq -r '.Reservations[].Instances[] | select(.Tags[].Value | contains("MyFavoriteInstance"))' みたいに書けるので jq コマンド自体をパイプで接続しなくてもいいのだけど、紙面の都合上 jq の複数のフィルターを指定するときは jq コマンド自体をパイプで接続するやり方で書いていく。

タグ以外の属性も使って絞り込む

CFn とかで EC2 インスタンスを作ってると EC2 インスタンスが置換することがある。

EC2 インスタンスが置換するとしばらくの間は同じタグを持ってる EC2 インスタンスが複数存在したりするのでタグ以外の属性を使って絞りこむようにしてる。 たとえば MyFavoriteInstance みたいなタグを付けてる EC2 インスタンスのパブリック IP アドレスはこんな感じのコマンドで取得するけど、EC2 インスタンスが置換して MyFavoriteInstance タグが複数の EC2 インスタンスに付いてるときは古い EC2 インスタンスのパブリック IP アドレスも取得しちゃう。

$ aws ec2 describe-instances | \
    jq -r '.Reservations[].Instances[]' | \
    jq -r 'select(.Tags[].Value | contains("MyFavoriteInstance"))' | \
    jq -r '.PublicIpAddress'

null
<EC2 インスタンスのパブリック IP アドレス>

古い EC2 インスタンスのパブリック IP アドレスは通常解放しているので null が取得するけどこれは取得したい情報ではないので jq で select(.PublicIpAddress != null) みたいにして置換後の新しい EC2 インスタンスのパブリック IP だけ取得してる。

$ aws ec2 describe-instances | \
    jq -r '.Reservations[].Instances[]' | \
    jq -r 'select(.Tags[].Value | contains("MyFavoriteInstance"))' | \
    jq -r 'select(.PublicIpAddress != null)' | \
    jq -r '.PublicIpAddress'

<EC2 インスタンスのパブリック IP アドレス>

ECS がログを Firelens で出力するときにタスクが止まってハマったときのメモ。

ECS がログを Firelens で出力するときにタスクが止まってハマったときのメモ。

ECS がログを Firelens で出力するときにタスクが止まった

ECS は CDK で構築していてログは awslogs ログドライバーで CloudWatch に出力してるんだけど、awsfirelens ログドライバーで S3 に出力するように変えて CFn スタックを更新したらタスクが止まって上がらなくなったときのことをメモしておく。

事象

ECS は CDK で構築していてログは awslogs ログドライバーで CloudWatch に出力してるのを awsfirelens ログドライバーで S3 に出力するように変えて CFn スタックを更新したら AWS::ECS::ServiceUPDATE_PROGRESS のままでしばらく待っても更新が終わらない。

ECS のサービスとタスクの状態を見てみるとタスクが止まって上がらなくなっているよう。さらにタスクの状態を見てみるとタスクが止まる => 新しくタスクを作成 => 新しく作成したタスクが止まる => さらに新しくタスクを作成 => ... みたいな感じでタスクの作成と止まるのが繰り返している。

なにが駄目だったか

Unable to generate firelens config file: unable to generate firelens config: unable to apply log options of container <コンテナ名> to firelens config: missing output key Name which is required for firelens configuration of type fluentbit

STOPPED になってるタスクの停止理由を確認したらこんな感じになってるんだけど Firelens でログを出力するのは初めてじゃないし Name フィールドは指定してるはず……と思ったけどコードを見たらたしかに Name フィールドは指定してなかった。

そのときのコードはこうなってた。 Name を指定するところが nametypo してたのが原因だった。

logging: ecs.LogDrivers.firelens({
  options: {
    name: "firehose",
    region: this.region,
    delivery_stream: "my-stream",
  },
}),

name => Name にして CFn スタックを更新したらちゃんと ECS のタスクとサービスが上がってきてログが S3 に流れるようになった。

結論: typo ダメ絶対。

logging: ecs.LogDrivers.firelens({
  options: {
    Name: "firehose",
    region: this.region,
    delivery_stream: "my-stream",
  },
}),

typo がエラーにならなかったのはなぜか

CDK は TypeScript で書いててフィールド名を typo したらコンパイルエラーになるんだけど FireLensLogDriverProps.optionsName じゃなくて name と書いてもコンパイルエラーにはならない。

ドキュメント を見直したら FireLensLogDriverProps.options の型は普通に Map<string, string> になってて typo しようがなかった。

ちなみに FireLensLogDriverProps.options のコードはこうなってる。 Name は必須のフィールドだから明示的に定義しといてもいいんじゃないかって気はする。

結論: ドキュメントはちゃんと読む。

/**
 * Specifies the firelens log driver configuration options.
 */
export interface FireLensLogDriverProps extends BaseLogDriverProps {
    /**
     * The configuration options to send to the log driver.
     * @default - the log driver options
     */
    readonly options?: {
        [key: string]: string;
    };
}

参考サイト

テキストを指定行以降だけ出力する方法のメモ。

ときどきテキストの指定行以降だけを出力することがあるんだけど tail コマンドとか sed コマンドを使うと簡単にできるのでメモしておく。

指定行以降を出力する方法

ときどきテキストの指定行以降だけを出力することがあるんだけど tail コマンドとか sed コマンドを使うと簡単にできるのでメモしておく。

tail -n オプションを + 付きで指定する

テキストが10行あるんだけど1〜3行目はヘッダーが入ってて実際のデータが3行目から始まるときとかは3行目以降のデータだけを出力したいことがある。 そういうときは tail コマンドを tail -n +3 のように -n オプションを + 記号付きで指定すると3行目以降のテキストだけが出力する。

$ seq 10 | tail -n +3
3
4
5
6
7
8
9
10

sed を使う

sed コマンドで 1,2d みたいに指定して1〜2行を削除するでも同じことができる。

$ seq 10 | sed -e '1,2d'
3
4
5
6
7
8
9
10

awk を使う

awk コマンドで NR>=3 みたいに指定して3行目以降だけ処理するでも同じことができる。

$ seq 10 | awk 'NR>=3'
3
4
5
6
7
8
9
10

Firehose で GZIP 圧縮して S3 に流したデータをダウンロードすると GZIP 圧縮されてない?と勘違いしたときのメモ。

AWS Kinesis Data Firehose で GZIP 圧縮して S3 に流したデータをブラウザーでダウンロードすると GZIP 圧縮されてない? と勘違いしたときのメモ。

Firehose で GZIP 圧縮して S3 に流したデータをダウンロードすると GZIP 圧縮されてない?

サーバーのログは Firehose で S3 に流してて Firehose のデータ配信ストリームは S3 のデータ使用量を抑えるためにサーバーのログを GZIP 圧縮してから S3 に保存するように設定してるんだけど、ブラウザーで S3 に保存してるサーバーのログをダウンロードしてファイルタイプを確認したら GZIP じゃなくて JSON だった。 サーバーのログは JSON 形式で出力しているんだけど Firehose がサーバーのログを GZIP 圧縮してなさそうに思ったので「Firehose の配信ストリームの設定を間違えてるのかな?」と少し調べたときのメモ。

結論としては Firehose の配信ストリームの設定は間違ってなくて Firehose のデータ配信ストリームから S3 に流すときにちゃんと GZIP 圧縮してる。

事象

サーバーのログは JSON 形式で出力してて Firehose の配信ストリーム経由で S3 に保存している。

Firehose のデータ配信ストリームは S3 のデータ使用量を抑えるためにサーバーのログを GZIP 圧縮してから S3 に保存するように設定してるんだけど、ブラウザーAWS コンソールを表示して保存してるサーバーのログをダウンロードしてファイルタイプを確認したら GZIP じゃなくて JSON だった。 テキストエディターでログ (data.gz) を表示してみると普通に表示できてやっぱり JSON になってる。

$ file data.gz
data.gz: JSON data

一応、AWS CLI で同じログを S3 からダウンロードすると今度はファイルタイプが GZIP になってる。 やっぱり Firehose の配信ストリームの設定は合ってるっぽい。

$ aws s3 cp s3://hoge-bucket/data.gz data-2.gz
download: s3://hoge-bucket/data.gz to ./data-2.gz

$ file data-2.gz
data-2.gz: gzip compressed data, from FAT filesystem (MS-DOS, OS/2, NT), original size modulo 2^32 3115

原因

S3 に保存してるサーバーのログを AWS コンソールでいろいろ見たりググったりしたら、どうも原因は Firehose がデータを S3 に保存するときに Content-Encoding: gzip メタデータをサーバーのログに付与してからっぽい。

AWS コンソールでサーバーのログのメタデータを表示すると Content-Encoding: gzip が付いてる。

f:id:ebc_2in2crc:20200801152631p:plain:w500

Content-Encoding: gzip がサーバーのログに付いてるのでブラウザーでアクセスすると Content-Encoding: gzip ヘッダーがレスポンスヘッダーに付く。 ブラウザーはレスポンスに Content-Encoding: gzip ヘッダーが付いてると body を自動的に GZIP 展開するのでダウンロードして保存したサーバーのログは GZIP 圧縮前の JSON になっているということのようだった。

f:id:ebc_2in2crc:20200801152646p:plain:w400

結論

サーバーのログサイズはかなり大きいのでローカルにダウンロードすることはめったにないのと、あとたぶん実際にサーバーのログをローカルにダウンロードするときは AWS CLI でダウンロードすると思うので結論としては特に気にしないでよさそう。

参考サイト

AWS CLI v1 => v2 でバイナリ入力パラメータのデフォルトの解釈方法が変わっているメモ。

AWS CLI v1 => v2 で file:// とか使うときのバイナリ入力パラメータのデフォルトの解釈方法が変わっている。v2 を使うときにバイナリ入力パラメータのデフォルトの解釈方法を v1 互換にする方法とかを調べたメモ。

バイナリ入力パラメータのデフォルトの解釈方法

AWS CLI v1 => v2 でバイナリ入力パラメータのデフォルトの解釈方法が変わっている。 AWS CLI v2 を使うときにバイナリ入力パラメータのデフォルトの解釈方法を v1 互換にする方法をメモしておく。

AWS CLI のバージョン。

$ aws --version
aws-cli/2.0.35 Python/3.7.4 Darwin/19.5.0 botocore/2.0.0dev39

AWS CLI v2 はデフォルトで base64エンコードされたテキストに解釈する

AWS CLI v1 は file:// とか使うときのバイナリ入力パラメータをデフォルトで raw データつまりエンコードされていないテキストに解釈したけど、AWS CLI v2 は file:// とか使うときのバイナリ入力パラメータをデフォルトで base64エンコードされたテキストに解釈する。

以下のコードは AWS CLI v1 では意図するとおりに動くけど AWS CLI v2 はエラーになって Invalid base64: "Hello" が表示する。

$ aws kms encrypt --key-id=${KEY_ID} --plaintext="Hello" --query=CiphertextBlob --output=text

Invalid base64: "Hello"

file:// プレフィクスを使っても同じ結果になる。

$ aws kms encrypt --key-id=${KEY_ID} --plaintext=file://<(echo -n "Hello") --query=CiphertextBlob --output=text

Invalid base64: "Hello"

AWS CLI v1 => v2 で file:// とか使うときのバイナリ入力パラメータのデフォルトの解釈方法が変わってしまっているので、v1 のときに作ったツールとかシェルスクリプトを v2 を使うとエラーになったり、意図しない挙動になったりする。 エラーになるのは困るけど意図しない挙動になるともっと困ることが多いと思うので、v1 のときに作ったツールとかを作ったときに意図していた挙動で動かす方法を書いていく。

バイナリ入力パラメータを v1 互換の解釈方法にする

AWS CLI v2 はバイナリ入力パラメータの解釈方法をコマンドのフラグとか config ファイルで指定できる。

コマンドのフラグでバイナリ入力パラメータの解釈方法を指定する

コマンドのフラグでバイナリ入力パラメータの解釈方法を指定するには --cli-binary-format オプションを指定していく。 --cli-binary-format=raw-in-base64-out を指定すると AWS CLI v1 互換の解釈方法になる。

$ aws kms encrypt --key-id=${KEY_ID} --plaintext="Hello" --query=CiphertextBlob --output=text --cli-binary-format=raw-in-base64-out
# 暗号化したテキストが出力

file:// プレフィクスを使っても同じ結果になる。

$ aws kms encrypt --key-id=${KEY_ID} --plaintext=file://<(echo -n "Hello") --query=CiphertextBlob --output=text --cli-binary-format=raw-in-base64-out
# 暗号化したテキストが出力

cofig ファイルでバイナリ入力パラメータの解釈方法を指定する

AWS CLI v2 はバイナリ入力パラメータの解釈方法を config ファイルの cli_binary_format オプションに設定しておくとその解釈方法でバイナリ入力パラメータを解釈する。 cli_binary_format オプションが raw-in-base64-out のときは AWS CLI v2 はバイナリ入力パラメータを raw データとして解釈して、 cli_binary_format オプションが config ファイルにないか cli_binary_format オプションが base64 のときはバイナリ入力パラメータを base64エンコードされたテキストとして解釈する。

[default]
cli_binary_format = raw-in-base64-out

config ファイルを直接いじるかこんな感じのコマンドを実行すればいい。

$ aws configure set cli_binary_format raw-in-base64-out

$ cat ~/.aws/config
[default]
cli_binary_format = raw-in-base64-out
# 省略

$ aws kms encrypt --key-id=${KEY_ID} --plaintext="Hello" --query=CiphertextBlob --output=text
# 暗号化したテキストが出力

$  aws kms encrypt --key-id=${KEY_ID} --plaintext=file://<(echo -n "Hello") --query=CiphertextBlob --output=text
# 暗号化したテキストが出力

自分が普段使ってる環境は config を設定しておくのが手軽だけどシェルスクリプトを作って他の人に使ってもらったり CI で動かすときはバイナリ入力パラメータの解釈方法は config の設定に依存したくないと思うので --cli-binary-format フラグでバイナリ入力パラメータの解釈方法を指定するのがよさそう。

fileb:// プレフィクスを使うと常に raw データとして解釈する

AWS CLI v2 は fileb:// プレフィクスを使うとバイナリ入力パラメータを常に raw データとして解釈する。

$ cat ~/.aws/config
[default]
cli_binary_format = base64

$  aws kms encrypt --key-id=${KEY_ID} --plaintext=fileb://<(echo -n "Hello") --query=CiphertextBlob --output=text --cli-binary-format=base64
# 暗号化したテキストが出力

fileb:// プレフィクスは --cli-binary-format フラグと config ファイルの cli_binary_format オプションで base64 を指定してもバイナリ入力パラメータを常に raw データとして解釈するので、AWS CLI v1 のときに作ったツールとかシェルスクリプトAWS CLI v2 で同じ挙動にするのは fileb:// プレフィクスを使うのが一番楽なような気がする。

環境変数でバイナリ入力パラメータの解釈方法は指定できない

バイナリ入力パラメータの解釈方法は環境変数では指定できないので AWS CLI v2 でバイナリ入力パラメータの解釈方法を指定するのは --cli-binary-format フラグ、config ファイルの cli_binary_format オプションを指定するか、あるいは fileb:// プレフィクスを使っていく。

このエントリには、同等の環境変数はありません。

参考サイト

banner コマンドを Dockerfile にしたメモ。

banner) コマンドを Dockerfile にしたのでメモしておく。

banner コマンドの Dockerfile

仕事をしてるとたまに banner コマンドを使うときがある。 普段は Mac で作業してて Mac に入ってる banner コマンドは BSD 版で縦出力なんだけど、自分は縦出力じゃなくて横出力したいことが多くて banner コマンドを使うときは適当な Linux 機に入って banner コマンドを使ってる。Linux 機に入って banner コマンドを打つのはまあまあ面倒くさいので Dockerfile にした。

Dockerfile

Dockerfile はこんな感じにしてる。

FROM centos:7

RUN yum install -y epel-release && \
        yum install -y banner

COPY docker-entrypoint.sh /usr/local/bin

ENTRYPOINT ["docker-entrypoint.sh"]

docker-entrypoint.sh はこんな感じ。

#!/bin/sh

banner "$@"

使い方

こんな感じで使ってる。

$ docker image build --tag docker-banner .

$ docker container run --rm -it docker-banner Hello

#     #  #######  #        #        #######
#     #  #        #        #        #     #
#     #  #        #        #        #     #
#######  #####    #        #        #     #
#     #  #        #        #        #     #
#     #  #        #        #        #     #
#     #  #######  #######  #######  #######

banner コマンドの入力が複数あると改行する。改行したくなかったら "aaa bbb" みたいにする。

$ docker container run --rm -it docker-banner aaa bbb

   #        #        #
  # #      # #      # #
 #   #    #   #    #   #
#     #  #     #  #     #
#######  #######  #######
#     #  #     #  #     #
#     #  #     #  #     #


######   ######   ######
#     #  #     #  #     #
#     #  #     #  #     #
######   ######   ######
#     #  #     #  #     #
#     #  #     #  #     #
######   ######   ######

$ docker container run --rm -it docker-banner "aaa bbb"

   #        #        #           ######   ######   ######
  # #      # #      # #          #     #  #     #  #     #
 #   #    #   #    #   #         #     #  #     #  #     #
#     #  #     #  #     #        ######   ######   ######
#######  #######  #######        #     #  #     #  #     #
#     #  #     #  #     #        #     #  #     #  #     #
#     #  #     #  #     #        ######   ######   ######

入力が長くて途中で切れちゃうときは --env COLUMNS=100 みたいにして COLUMNS 環境変数を設定して実行すると途中で切れなくなる。

$ docker container run --rm -it docker-banner "Hello World"

#     #  #######  #        #        #######        #     #  #######  ######
#     #  #        #        #        #     #        #  #  #  #     #  #     #
#     #  #        #        #        #     #        #  #  #  #     #  #     #
#######  #####    #        #        #     #        #  #  #  #     #  ######
#     #  #        #        #        #     #        #  #  #  #     #  #   #
#     #  #        #        #        #     #        #  #  #  #     #  #    #
#     #  #######  #######  #######  #######         ## ##   #######  #     #

$ docker container run --rm -it --env COLUMNS=100 docker-banner "Hello World"

#     #  #######  #        #        #######        #     #  #######  ######   #        ######
#     #  #        #        #        #     #        #  #  #  #     #  #     #  #        #     #
#     #  #        #        #        #     #        #  #  #  #     #  #     #  #        #     #
#######  #####    #        #        #     #        #  #  #  #     #  ######   #        #     #
#     #  #        #        #        #     #        #  #  #  #     #  #   #    #        #     #
#     #  #        #        #        #     #        #  #  #  #     #  #    #   #        #     #
#     #  #######  #######  #######  #######         ## ##   #######  #     #  #######  ######

参考サイト

シェルのリダイレクトでわりと使うやつのメモ。

シェルのリダイレクトでわりと使うやつをメモしておく。

シェルのリダイレクト

シェルスクリプトを書くときにわりと使うリダイレクトのやつを書いていく。

/bin/sh のバージョン。普通に bash.

$ /bin/sh --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
Copyright (C) 2007 Free Software Foundation, Inc.

このへんは man sh すると sh は /private/var/select/shシンボリックリンクが実行すると書いてあるので /private/var/select/sh を見たら /bin/bash へのシンボリックリンクになってた。 /bin/sh を実行すると /bin/bash が sh 互換で動く感じ。

It is implemented by re-execing as either bash(1), dash(1), or zsh(1) as determined by the symbolic link located at /private/var/select/sh.

標準出力のリダイレクト

標準出力のリダレクトは > を使う。

# 標準出力をファイルにリダイレクトする
$ echo Hello > stdout.txt
$ cat stdout.txt
Hello

> はリダイレクト先のファイルの中身を上書きする。

$ echo Hello > stdout.txt
$ echo World > stdout.txt
$ cat stdout.txt
World

リダイレクト先のファイルを上書きじゃなくて追記するときは >> を使う。

$ echo Hello > stdout.txt
$ echo World >> stdout.txt
$ cat stdout.txt
Hello
World

標準エラー出力のリダイレクト

標準エラー出力> でリダイレクトするけど >標準エラー出力はリダイレクトしない。 ls /hoge/not/exists みたいに存在しないファイルを ls するとメッセージがプロンプトに表示するしリダイレクト先の stderr.txt は空っぽになる。

$ ls /hoge/not/exists
ls: /hoge/not/exists: No such file or directory
$ cat stderr.txt

標準エラー出力のリダイレクトは 2> を使う。 ls /hoge/not/exists みたいに存在しないファイルを ls するとメッセージがプロンプトに表示されなくてちゃんとリダイレクト先の stderr.txt に入る。

$ ls /hoge/not/exists 2> stderr.txt

$ cat stderr.txt
ls: /hoge/not/exists: No such file or directory

2> はリダイレクト先のファイルを上書きする。リダイレクト先のファイルを上書きじゃなくて追記するときは 2>> を使う。

$ ls /hoge/not/exists 2> stderr.txt
$ ls /fuga/not/exists 2> stderr.txt
$ cat stderr.txt
ls: /fuga/not/exists: No such file or directory

$ ls /hoge/not/exists 2> stderr.txt
$ ls /fuga/not/exists 2>> stderr.txt
$ cat stderr.txt
ls: /hoge/not/exists: No such file or directory
ls: /fuga/not/exists: No such file or directory

標準入力とかのリダイレクトで使うファイルディスクリプタ

標準エラーのリダイレクトで 2> とか 2>> みたいにしたときの 2 はファイルディスクリプタを指定してる。

標準出力とか標準エラー出力のファイルディスクリプタは一般的に以下のようになってるので 2>標準エラー出力をリダイレクトするという意味になる。

ファイルディスクリプタ 対象
0 標準入力
1 標準出力
2 標準エラー出力

あと標準出力のリダイレクトは > とか >> みたいに書いたけど標準出力のファイルディスクリプタ1 なので 1> とか 1>> みたいにしても同じ動きになる。

$ echo Hello 1> stdout.txt
$ echo World 1>> stdout.txt
$ cat stdout.txt
Hello
World

標準入力のリダイレクト

標準入力のリダレクトは < を使う。

$ cat stdout.txt
Hello
World

$ grep Hello < stdout.txt
Hello

ヒアドキュメント

標準入力はヒアドキュメント << をリダイレクトすることができる。

$ cat << EOF
Hello
World
EOF
Hello
World

$ grep Hello << EOF
Hello
World
EOF
Hello

標準出力をファイルにリダイレクトして空っぽのファイルを作る

ファイルを空っぽにするときに echo > stdout.txt みたいにすると stdout.txt の内容は空っぽにならない。

$ echo > stdout.txt
$ ls -l stdout.txt
-rw-r--r--  1 shrimp  staff  1  7 24 14:48 stdout.txt

$ od stdout.txt
0000000    000012
0000001

これ、bash なら echo -n > stdout.txt でファイルを空っぽにできるけど sh で echo -n > stdout.txt をやると -n というファイルになっちゃう。

rm stdout.txt; touch stdout.txt でファイルを削除してから空のファイルを作ってもいいけど :> みたいにするほうが楽なので :> を使うことが多い気がする。

$ :> stdout.txt
$ ls -l stdout.txt
-rw-r--r--  1 shrimp  staff  0  7 24 14:49 stdout.txt

$ od stdout.txt

ちなみに ubuntu/bin/sh が dash のシンボリックリンクになってるんだけど echo -n > stdout.txt で普通にファイルを空っぽに出来ると Twitter で教えてもらった。

docker container run --rm -it ubuntu /bin/sh コマンドで ubuntu の Docker コンテナを起動していろいろ試してみたけどちゃんと echo -n > stdout.txt でファイルを空っぽにできる……

# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Aug  7  2019 /bin/sh -> dash

# echo -n > stdout.txt
# ls -l stdout.txt
-rw-r--r-- 1 root root 0 Jul 24 10:31 stdout.txt

# cat stdout.txt

参考サイト