全力で怠けたい

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

シェルで小数点以下を四捨五入して整数にするやり方のメモ。

シェルで小数点以下を四捨五入して整数にするやり方のメモ。

シェルで四捨五入するやり方

シェルでたまに小数点以下を四捨五入して整数にするけどたまにすぎて四捨五入のやり方を忘れて毎回調べてる気がするのでやり方をメモしておく。

awk を使って四捨五入する

awk を使ってこんな感じに四捨五入する。

$ echo 1.4 | awk '{printf("%d\n", $1 + 0.5)}'
1

$ echo 1.5 | awk '{printf("%d\n", $1 + 0.5)}'
2

printf 関数を使って四捨五入する

Bashawk を使わないでも組み込みの printf 関数でこんな感じで小数点以下を四捨五入して整数にする。

$ printf '%.0f\n' 1.4
1

$ printf '%.0f\n' 1.5
2

参考サイト

ECS の Fargate で動かしてるコンテナが socket: too many open files で外部のサーバーに接続できないときにやったこと。

ECS の Fargate で動かしてるコンテナが socket: too many open files で外部のサーバーに接続できないときにやったことのメモ。

ECS で動かしてるコンテナが外部のサーバーに接続できなくなる

やったこと

ことの始まりはとある ECS の Fargate で動かしてるコンテナが突然外部のサーバーに接続できなくなるのに気が付いたときだった。

問題のコンテナは DB とかキャッシュで使ってるサーバーに接続していろいろやるのだけど、どうも問題のコンテナから DB サーバーとかキャッシュサーバーに接続できなくなることがあるようだった。 問題のコンテナからは常に DB とかの外部サーバーに接続できないわけではなくてちゃんとそのへんの外部サーバーに接続できている時間帯があるようなのだけど、接続できたり接続できなかったりという感じで時間帯と接続可否の状態には関連性はないっぽい。 問題のコンテナはいろいろと動作検証に使ってるけど外部には公開していないので、おそらく動作検証のなにかが影響して外部のサーバーに接続できなくなっている可能性が高い。

結論

コンテナのログを見たら一発で原因が分かった。

コンテナのログは外部サーバーに接続できない時間帯は socket: too many open files が大量に出力されていて、たぶんこれはコンテナ使うファイル数がファイルディスクリプタの制限に引っかかっていそう。

コンテナで設定する ulimits のリスト。このパラメータは、Docker Remote API の コンテナを作成する セクションの Ulimits と、docker run の --ulimit オプションにマッピングされます。

Fargate タスクは、デフォルトのリソース制限値を使用します。ただし、Fargate がオーバーライドする nofile リソース制限パラメータを除きます。nofile リソース制限は、コンテナが使用できるオープンファイルの数の制限を設定します。デフォルトの nofile ソフト制限は 1024、ハード制限は Fargate タスク用で 4096 です。これらの制限は、タスクでより多くのファイルを処理する必要がある場合は、タスク定義で調整できます。

コンテナは CDK で構築してるので ContainerDefinition#addUlimits()UlimitName.NOFILEsoftLimithardLimit が大きくなるようにこんな感じで設定した。

このあとコンテナを ECS にデプロイしたら socket: too many open files がログに出力することがなくなって問題のコンテナからちゃんと外部のサーバーに接続できるようになった。

container.addUlimits({
  name: UlimitName.NOFILE,
  softLimit: 6144,
  hardLimit: 12288,
});

参考サイト

Lambda を定期実行する CloudWatch イベントルールを作る CDK のスニペット。

Lambda を定期実行する CloudWatch イベントルールを作る CDK のスニペットを書いておく。

Lambda を定期実行する CloudWatch イベントルールを作る CDK のスニペット

Lambda を定期実行する CloudWatch イベントルールを作る CDK のスニペットを書いておく。

CDK のバージョン。

$ cdk --version
1.62.0 (build 8c2d7fc)

CDK のコード

CDK アプリはこんな感じのディレクトリ構成を想定してる。

├── bin
│   └── cdk-event-rule.ts
├── cdk.json
├── cdk.out
├── lib
│   └── cdk-event-rule-stack.ts
│
(省略)

bin/cdk-event-rule.ts はこんな感じに書いていく。

import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { CdkEventRuleStack } from '../lib/cdk-event-rule-stack';

const app = new cdk.App();
new CdkEventRuleStack(app, 'CdkEventRuleStack');

lib/cdk-event-rule-stack.ts はこんな感じに書いていく。

Lambda 関数は1分毎に Hello World! をログに出力するだけのやつ。 あと Lambda 関数の構築で deprecated な lambda.Code.inline() を使ってるけど実際に使うときはこのへんは適宜 deprecated じゃないメソッドを使うようにしていく。

import * as cdk from '@aws-cdk/core';
import * as events from '@aws-cdk/aws-events';
import * as targets from '@aws-cdk/aws-events-targets';
import * as lambda from '@aws-cdk/aws-lambda';

export class CdkEventRuleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const fn = new lambda.Function(this, `my-lambda-function`, {
      functionName: "hello-world",
      code: lambda.Code.inline(`

def lambda_handler(event, context):
    print("Hello World!")
      `),
      handler: "index.lambda_handler",
      runtime: lambda.Runtime.PYTHON_3_7,
    });

    const target = new targets.LambdaFunction(fn);
    new events.Rule(this, `my-event-rule`, {
      ruleName: 'every-minute',      schedule: events.Schedule.cron({minute: '*/1'}),
      targets: [target],
    });
  }
}

構築したリソースと Lambda 関数の実行ログの確認

CDK アプリをデプロイしたら Lambda 関数と CloudWatch イベントルールが作られてるのを確認してく => ちゃんと作られてそう。

# Lambda 関数
$ aws lambda get-function-configuration --function-name='hello-world' | jq -r '.FunctionName'
hello-world

# CloudWatch イベントルール
$ aws events describe-rule --name='every-minute'
{
    "Name": "every-minute",
    "Arn": "arn:aws:events:<region>:<account-id>:rule/every-minute",
    "ScheduleExpression": "cron(*/1 * * * ? *)",
    "State": "ENABLED",
    "EventBusName": "default"
}

Lambda 関数と CloudWatch イベントルールをデプロイして2, 3分くらい待ったら Lambda 関数の実行ログが CloudWatch に出力してるか確認していく。 こんな感じで Hello World! が出力してて Lambda 関数はちゃんと動いてそう。

$ LOG_STREAM=$(aws logs describe-log-streams --log-group-name='/aws/lambda/hello-world' | jq -r '.logStreams[0].logStreamName')
$ aws logs get-log-events --log-group-name='/aws/lambda/hello-world' --log-stream-name="${LOG_STREAM}" | jq -r '.events[].message' | grep 'Hello World!'
Hello World!
Hello World!
# 省略

以上。

シェルの case でよく使うような気がするやつを軽くメモ。

シェルの case でよく使うような気がするやつを軽くメモしておく。

シェルの case でよく使うような気がするやつ

シェルは case を使うと Java とか Go の switch みたいなことができる。

シェルの case でよく使うような気がするやつを軽くメモしておく。

case の基本的な書き方

case の基本的な書き方を書いておく。

case は case で始めて esac で終わる。

case 文字列 in
    パターン1) 文字列がパターン1に合致するときのコマンド ;;
    パターン2) 文字列がパターン2に合致するときのコマンド ;;
    *) 文字列がどのパターンにも合致しないときのコマンド ;;
esac

こんな感じのシェルスクリプトを用意して実際に動かしてみる。

case $1 in
    hoge) echo 'Hello hoge!' ;;
    fuga) echo 'Hello fuga!' ;;
    *) echo 'Hello *' ;;
esac
$ ./my-case.sh hoge
Hello hoge!

$ ./my-case.sh fuga
Hello fuga!

$ ./my-case.sh piyo
Hello *

パターンは大文字と小文字を区別するので HOGE という文字列は hoge というパターンには合致しない。

$ ./my-case.sh HOGE
Hello *

パターンとパターンに合致するときのコマンドの間には改行を入れることができる。

case $1 in
    hoge)
        echo 'Hello hoge!' ;;
    fuga)
        echo 'Hello fuga!' ;;
    *)
        echo 'Hello *' ;;
esac

パターンに合致するときのコマンドは1つだけじゃなくて複数のコマンドが書ける。

case $1 in
    hoge)
        echo 'hello hoge!'
        echo 'hello hoge!'
        ;;
    fuga)
        echo 'hello fuga!'
        echo 'hello fuga!'
        ;;
    *)
        echo 'hello *'
        echo 'Hello *'
        ;;
esac

パターンに合致するときのコマンドが複数のコマンドのときは ; を使うとまとめて1行に書ける。

case $1 in
    hoge)
        echo 'hello hoge!' ; echo 'hello hoge!'
        ;;
    fuga)
        echo 'hello fuga!' ; echo 'hello fuga!'
        ;;
    *)
        echo 'hello *' ; echo 'Hello *'
        ;;
esac

一応、こんな感じに1行にまとめて書ける。

case $1 in
    hoge) echo 'hello hoge!' ; echo 'hello hoge!' ;;
    fuga) echo 'hello fuga!' ; echo 'hello fuga!' ;;
    *) echo 'hello *' ; echo 'Hello *' ;;
esac

?: 任意の1文字に合致

パターンは ? を使うと任意の1文字に合致する。

case $1 in
    a?) echo "a で始まる2文字" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh aa
a で始まる2文字

$ ./my-case.sh ab
a で始まる2文字

$ ./my-case.sh a
*

$ ./my-case.sh abc
*

*: 任意の複数文字に合致

パターンは * を使うと任意の複数文字に合致する。

case $1 in
    a*a) echo "a で始って a で終わる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh aba
a で始って a で終わる文字列

$ ./my-case.sh aa
a で始って a で終わる文字列

$ ./my-case.sh aaaaa
a で始って a で終わる文字列

$ ./my-case.sh a
*

[]: [] で囲んでいる任意の文字に合致

[] で文字を囲むと [] で囲んでいる任意の文字に合致する。

case $1 in
    [aA]*[aA]) echo "a か A で始って a か A で終わる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh aa
a か A で始って a か A で終わる文字列

$ ./my-case.sh Aa
a か A で始って a か A で終わる文字列

$ ./my-case.sh aA
a か A で始って a か A で終わる文字列

$ ./my-case.sh AA
a か A で始って a か A で終わる文字列

[a-c] みたいに書くと a から c までの範囲の任意の文字に合致する。

case $1 in
    [a-c]*[a-c]) echo "a か b か c で始って a か b か c で終わる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh ab
a か b か c で始って a か b か c で終わる文字列

$ ./my-case.sh bc
a か b か c で始って a か b か c で終わる文字列

$ ./my-case.sh ca
a か b か c で始って a か b か c で終わる文字列

[a-zA-Z] みたいに書くと任意のアルファベットに合致する。

case $1 in
    [a-zA-Z]*[a-zA-Z]) echo "アルファベットで始ってアルファベットで終わる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh aZ
アルファベットで始ってアルファベットで終わる文字列

[!]: [] で囲んでいない任意の文字に合致

[!] で文字を囲むと [!] で囲んでいない任意の文字に合致する。

case $1 in
    [!a]*) echo "a 以外で始まる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh b
a 以外で始まる文字列

$ ./my-case.sh ba
a 以外で始まる文字列

$ ./my-case.sh a
*

$ ./my-case.sh ab
*

[!a-c] みたいに書くと a から c までの範囲以外の任意の文字に合致する。

case $1 in
    [!a-c]*) echo "a と b と c 以外で始まる文字列" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh a
*

$ ./my-case.sh b
*

$ ./my-case.sh c
*

$ ./my-case.sh d
a と b と c 以外で始まる文字列

|: 複数のパターンのいずれかに合致

abc|xyz みたいに書くと abcxyz のいずれかに合致する。

case $1 in
    abc|xyz) echo "abc か xyz" ;;
    *) echo "*" ;;
esac
$ ./my-case.sh abc
abc か xyz

$ ./my-case.sh xyz
abc か xyz

$ ./my-case.sh aaa
*

[ab]* みたいなパターンは | を使って a*|b* みたいに書くことができる。

case $1 in
    a*|b* ) echo "a か b で始まる文字列" ;;
    *) echo "*" ;;
esac

$ ./my-case.sh b
a か b で始まる文字列

$ ./my-case.sh ab
a か b で始まる文字列

$ ./my-case.sh bc
a か b で始まる文字列

$ ./my-case.sh ca
*

特定の 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