/dev/null

脳みそのL1キャッシュ

Lambda 関数から ECS Task を起動する

プロジェクト構造

成果物は https://github.com/d2verb/lambda-golang-terraform/tree/run-ecs-task にあります。前回の記事のものをベースに使っています。AWS リソースは Terraform で管理しています。

$ tree .
.
├── ecs.tf
├── golang
│   ├── artifacts
│   │   ├── sample.exe
│   │   └── sample.zip
│   └── src
│       ├── go.mod
│       ├── go.sum
│       └── main.go
├── lambda.tf
├── provider.tf
└── terraform.tfstate

RunTask する

AWSAPI には RunTask というものがあります。今回はこの API を Lambda 関数から叩いて ECS Task を実行します。

func HandleRequest() error {
    awsConfig, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        return err
    }

    client := ecs.NewFromConfig(awsConfig)

    var count int32 = 1
    cluster := "sample-cluster"
    taskDef := "sample"
    containerName := "sample"

    _, err = client.RunTask(
        context.TODO(),
        &ecs.RunTaskInput{
            Cluster:        &cluster,
            TaskDefinition: &taskDef,
            Count:          &count,
            LaunchType:     ecsTypes.LaunchTypeFargate,
            NetworkConfiguration: &ecsTypes.NetworkConfiguration{
                AwsvpcConfiguration: &ecsTypes.AwsVpcConfiguration{
                    Subnets:        []string{"subnet-02ad4be365c5f37f6"},
                    SecurityGroups: []string{"sg-0501b0a3793ddabc7"},
                    AssignPublicIp: ecsTypes.AssignPublicIpEnabled,
                },
            },
            Overrides: &ecsTypes.TaskOverride{
                ContainerOverrides: []ecsTypes.ContainerOverride{
                    ecsTypes.ContainerOverride{
                        Name: &containerName,
                    },
                },
            },
        },
    )

    if err != nil {
        fmt.Printf("ecs run task: error: %s\n", err.Error())
    } else {
        fmt.Println("ecs run task: ok")
    }

    return err
}

特筆すべきものはないのですが、前準備として

  • ECS Cluster
  • ECS Task Definition

を準備しておく必要があります。(ECS Service は必要ないです)

ポリシー周り

ここが重要です。作成した Lambda 関数から RunTask するためには以下のポリシーが必要です。

data "aws_iam_policy_document" "lambda" {
  statement {
    effect = "Allow"
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "ecs:RunTask"
    ]
    resources = ["*"]
  }

  statement {
    effect = "Allow"
    actions = [
      "iam:PassRole",
    ]
    resources = ["*"]
    condition {
      test     = "StringLike"
      variable = "iam:PassedToService"
      values = [
        "ecs-tasks.amazonaws.com"
      ]
    }
  }
}

ecs:RunTask が必要なのはもちろんですが、iam:PassRole も必要です。ECS Task の実行には Execution Role が必要で、Lambda 側から ECS Task を生成し実行するには、Execution Role を渡してあげる必要があります。

Pass Role に関しては以下の資料を参考にしてください。

https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html https://blog.rowanudell.com/iam-passrole-explained/

動作確認

terraform apply でリソースを作成して、マネジメントコンソールから Lambda 関数を叩きます。

ECS Cluster 側で Task が走っており、ログに以下が出力されていれば成功です。

Go 言語で書かれた Lambda 関数を Terraform で管理する

プロジェクト構造

成果物は https://github.com/d2verb/lambda-golang-terraform にあります。

$ tree .
.
├── golang
│   ├── artifacts
│   │   ├── sample.exe
│   │   └── sample.zip
│   └── src
│       ├── go.mod
│       ├── go.sum
│       └── main.go
├── lambda.tf
├── provider.tf
└── terraform.tfstate

Go 言語の場合は Python や Node.js などのスクリプト型言語と違って、実行バイナリをデプロイする必要があることに気をつけてください。

ビルド & アーカイブ

lambda.tf 内の以下の部分で実行ファイルのビルドとアーカイブを行っています。

resource "null_resource" "go_build" {
  triggers = {
    always_run = "${timestamp()}"
  }

  provisioner "local-exec" {
    command = "cd golang/src && GOOS=linux GOARCH=amd64 go build -o ../artifacts/sample.exe main.go"
  }
}

data "archive_file" "go" {
  depends_on  = [null_resource.go_build]
  type        = "zip"
  source_file = "golang/artifacts/sample.exe"
  output_path = "golang/artifacts/sample.zip"
}

気をつける点として、まず以下があります。null_resource は一度生成されるとその後変化しません。これでは Go 言語のソースファイルを変更しても反映されないので、以下で timestamp が変わるたびに null_resource を再生成するようにしています。

  triggers = {
    always_run = "${timestamp()}"
  }

次に、気をつける点として、以下があります。特に GOOS=linux GOARCH=amd64 の箇所です。Go 言語はコンパイル型言語で、実行バイナリを用意する必要があります。生成された実行バイナリが Lambda の実行環境とマッチしない場合、実行が失敗してしまいますので、実行バイナリの形式を指定する必要があります。

    command = "cd golang/src && GOOS=linux GOARCH=amd64 go build -o ../artifacts/sample.exe main.go"

デプロイ & 動作確認

まずは、terraform apply を実行してリソースを作成します。

$ terraform apply
...
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
...

後は、マネジメントコンソールでテキトーなテストを実行し、Hello World とログに出力されれば動作確認完了です。

MySQL で ENUM 型のカラムに無効な値を入れたときの動き

はじめに

MySQLENUM 型を使う機会があったのですが、ENUM 型のカラムに無効な値をインサートするとなんとそのままデータを投入できたということがあったので、どうしてそんなことが起こったのか調べてみました。

実験

MySQLのバージョンに関してですが、今回は 5.7.34 を利用しました。

まず、こんな感じのテーブルを作ってみます。

CREATE TABLE members(
    id INT PRIMARY KEY AUTO_INCREMENT,
    gender ENUM('MALE', 'FEMALE', 'OTHERS'),
    name VARCHAR(255)
);

そして、データを投入してみます。これはいいですね。

INSERT INTO members(gender, name) VALUES('MALE', 'Bob');
Query 1 OK: 1 row affected

gender に無効な値を入れてみます。なぜか投入できます。

INSERT INTO members(gender, name) VALUES('UNKNOWN', 'Alice');
Query 1 OK: 1 row affected

そして、SELECT COUNT(*) FROM members WHERE gender = ''; が 1 を返すことも確認できました。つまり、UNKNOWN は空文字列へと変換されて gender カラムに設定されたのです。

エラーにしたいのだが

実はこの動作は MySQL のドキュメントにしっかりと載っていました。

An enumeration value can also be the empty string ('') or NULL under certain circumstances:

If you insert an invalid value into an ENUM (that is, a string not present in the list of permitted values), the empty string is inserted instead as a special error value. This string can be distinguished from a “normal” empty string by the fact that this string has the numeric value 0. See Index Values for Enumeration Literals for details about the numeric indexes for the enumeration values.

If strict SQL mode is enabled, attempts to insert invalid ENUM values result in an error.

ドキュメントによると strict SQL mode が有効であれば、エラーを吐いてくれるそうです。

strict SQL mode に関してはMySQL のドキュメントで以下のように述べています。

Strict SQL mode is in effect if either STRICT_ALL_TABLES or STRICT_TRANS_TABLES is enabled, although the effects of these modes differ somewhat:

For transactional tables, an error occurs for invalid or missing values in a data-change statement when either STRICT_ALL_TABLES or STRICT_TRANS_TABLES is enabled. The statement is aborted and rolled back.

トランザクションテーブルなら STRICT_ALL_TABLES or STRICT_TRANS_TABLES が有効であれば strict SQL mode になるようです。MySQL 5.5.5 以降はデフォルトのストレージエンジンが InnoDB なので、ENGINE を指定せずにテーブルを作ればトランザクションテーブルになります。

なので、STRICT_ALL_TABLES を sql_mode に設定すればインサートが失敗するはずです。やってみましょう。

SET SESSION sql_mode = 'STRICT_ALL_TABLES';

そして、インサート

INSERT INTO members(gender, name) VALUES('UNKNOWN', 'Alice');
Query 1 ERROR: Data truncated for column 'gender' at row 1

今度はエラーになりましたね。

WebAssembly を触ってみる

WebAssembly (wasm) とは

MDN によると

WebAssembly は最近のウェブブラウザーで動作し、新たな機能と大幅なパフォーマンス向上を提供する新しい種類のコードです。基本的に直接記述ではなく、C、C++、Rust 等の低水準の言語にとって効果的なコンパイル対象となるように設計されています。

つまり、ブラウザ内に(JS の処理系とは別に?)wasm の処理系があり、その処理系は C、C++、Rust などの低水準言語がコンパイルし吐き出したバイナリを解釈実行する。そして、パフォーマンス面でも JS に勝るものがある…てな感じですかね。

webassembly.org を見てみると、どうやら以下の言語から wasm のバイナリを生成できるようです。

Zig で wasm

最近勉強している Zig で wasm ファイルを生成してみます。以下のコードを用意し、

export fn add(i: i32, j: i32) i32 {
    return i + j;
}

以下のコマンドでコンパイルします。

$ zig build-lib math.zig -O ReleaseFast -target wasm32-freestanding -dynamic

するとカレントディレクトリに math.wasm というファイルが出来上がるかと思います。これを以下のように JS 側から読み込めば、Zig 側で定義した関数を呼び出すことができます。

<script>
   fetch("math.wasm")
       .then(response => response.arrayBuffer())
       .then(bytes => WebAssembly.instantiate(bytes))
       .then(results => results.instance)
       .then(i => {
           console.log(i.exports.add(1, 2));
       });
</script>

正しく動作すれば、コンソールに 3 が表示されるはずです。

wasm の中身を見てみる

wasm2wat を使えば wasm ファイルを wat ファイルに変換できます。これを使って、math.wasm の中を見てみましょう。

(module
  (type (;0;) (func))
  (type (;1;) (func (param i32 i32) (result i32)))
  (func $__wasm_call_ctors (type 0))
  (func $add (type 1) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add)
  (memory (;0;) 2)
  (global (;0;) (mut i32) (i32.const 66560))
  (global (;1;) i32 (i32.const 1024))
  (global (;2;) i32 (i32.const 1024))
  (global (;3;) i32 (i32.const 1024))
  (global (;4;) i32 (i32.const 66560))
  (global (;5;) i32 (i32.const 0))
  (global (;6;) i32 (i32.const 1))
  (export "memory" (memory 0))
  (export "__wasm_call_ctors" (func $__wasm_call_ctors))
  (export "add" (func $add))
  (export "__dso_handle" (global 1))
  (export "__data_end" (global 2))
  (export "__global_base" (global 3))
  (export "__heap_base" (global 4))
  (export "__memory_base" (global 5))
  (export "__table_base" (global 6)))

Lisp みたいに S 式で構成されているんですね。こんなところで S 式を目にするとは結構意外でした。

さて、add 関数に関連する箇所だけを抽出してみます。

(module
  (func $add (type 1) (param i32 i32) (result i32)
    local.get 1
    local.get 0
    i32.add)
  (export "add" (func $add)))

MDN によれば、Module という単位が wasm のバイナリに対応するとのことなので、ルートノードが module になっています。module ノードの子ノードとして func ノードと export ノードがあり、func ノードで関数を定義し、export ノードで関数を JS で認識できるようにしています。

もう一つ注目する箇所があって、それが以下です。

    local.get 1
    local.get 0
    i32.add

MDN によると wasm のスタックマシン上で実行されるとのことなので、上記の命令をすべてスタックを操作します。local.get 1 は第 2 引数をスタックにプッシュ、local.get 0 は第 1 引数をスタックにプッシュ、i32.add はスタックから 2 値をポップし、加算してからスタックにプッシュします。

JS と wasm で簡単なパフォーマンス比較

JS と wasm の両方で選択ソートを実装してみて実行速度を計測してみます。

Zig 側の実装例は以下の通り。

export fn sort(head: [*]i32, len: i32) void {
    var i: usize = 0;
    while (i < len) : (i += 1) {
        var j = i;
        var k = j;
        while (j < len) : (j += 1) {
            if (head[j] < head[k]) {
                k = j;
            }
        }
        var tmp = head[i];
        head[i] = head[k];
        head[k] = tmp;
    }
}

JS 側の実装は以下の通り。今回は要素数15000の乱数列に対して10回ソートした合計処理時間を計測しました。

<script>
   var data = undefined;

   fetch("sort.wasm")
       .then(response => response.arrayBuffer())
       .then(bytes => WebAssembly.instantiate(bytes))
       .then(results => results.instance)
       .then(i => {
           // array to sort
           data = new Int32Array(i.exports.memory.buffer, 0, 15000);

           // js
           console.log(measure(() => {
               sort(data);
           }, 10) + " ms");

           // wasm
           console.log(measure(() => {
               i.exports.sort(data.byteOffset, 15000);
           }, 10) + " ms");
       });

   function sort(data) {
       for (let i = 0; i < data.length; i++) {
           let k = i;
           for (let j = i; j < data.length; j++) {
               if (data[j] < data[k]) {
                   k = j;
               }
           }
           [data[i], data[k]] = [data[k], data[i]];
       }
   }

   function measure(callback, times = 1) {
       let acc = 0;
       for (let n = 0; n < times; n++) {
           window.crypto.getRandomValues(data);

           const start = performance.now();
           callback();
           const end = performance.now();

           acc += end - start;
       }
       return acc;
   }
</script>

結果は以下の通り。wasm の方がおおよそ 2 倍のパフォーマンス向上が見られますね。

wasm : 1974.2000000029802 ms
js   : 3934.10000000149 ms

wasm は CPU intensive な処理に向いているとのことなのでこの結果も納得です。暗号処理、画像処理あたりにも向いているような気がしますので、これらは別の機会に検証したいです。

参考サイト

developer.mozilla.org

webassembly.org

NX bit てなんぞや

はじめに

訳あって NX bit について調べる必要があったので、その際に残したメモをまとめました。

NX bit とは

メモリ領域に付与する実行不可属性です。NX bit が立っているメモリ領域上でコードを実行することはできません。なお、NX(No eXecute) bit は AMD の用語で、Intel では XD (eXecute Disable) bit、ARM では XN(eXecute Never) bit、MIPS では XI(eXecute Inhibit) bit だったりします。今回は NX bit で統一します。

動作確認

Linux 上で NX bit の動作確認をします。以下のようなC言語のコードを用意します。シェルを立ち上げるシェルコードをスタック上において、それを実行するコードになります。

#include <stdio.h>

typedef void (*fn)();

int main(void) {
    char code[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05";
    ((fn)code)();
    return 0;
}

NX bit 有効時

これを NX bit disabled の状態でコンパイルして実行してみましょう。新しいシェルが立ち上がるはずです。なお、-z はリンカにキーワードを渡すオプションです。(ということは、NX bit 関連の設定をするのはリンカ、ということになりますね)

┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ gcc -z execstack -o nx-off nx.c
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ checksec ./nx-off
[*] '/home/kali/Workspace/misc/nxbit/nx-off'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ ./nx-off
$ whoami
kali

NX bit 無効時

次に NX bit enabled の状態でコンパイルして実行してみましょう。今度はセグフォになりましたね。

┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ gcc -o nx-on nx.c
┌──(kali㉿kali)-[~/Workspace/misc/nxbit]
└─$ ./nx-on
zsh: segmentation fault  ./nx-on

PT_GNU_STACK と p_flags

さて、さきほどから checksec というコマンドを使って NX bit enabled/disabled の判定をしていましたが、具体的に checksec は何を根拠に判定しているのでしょうか。

checksec のソースコードを覗いてみましょう。

    def nx(self):
        """:class:`bool`: Whether the current binary uses NX 
        ...
            case PT_GNU_STACK:
                if (elf_ppnt->p_flags & PF_X)
                    executable_stack = EXSTACK_ENABLE_X;
                else
                    executable_stack = EXSTACK_DISABLE_X;
                break;
        ...
        """
        if not self.executable:
            return True

        for seg in self.iter_segments_by_type('GNU_STACK'):
            return not bool(seg.header.p_flags & P_FLAGS.PF_X)

        # If you NULL out the PT_GNU_STACK section via ELF.disable_nx(),
        # everything is executable.
        return False

ELF 形式の実行ファイルのメタデータを使って判断していますね。

セグメントタイプが PT_GNU_STACK と設定されているプログラムヘッダが存在し、かつ、p_flags で PF_X フラグが立っている場合に NX bit enabled と判断しているようです。

PT_GNU_STACK と p_flags に関しては、下記で述べられている通り、スタックが存在するセグメントの属性を指定しているようです、PF_X の X は eXecute の X なので、このフラグが立っているとスタックは実行可能ということになるのでしょう。

PT_GNU_STACK The p_flags member specifies the permissions on the segment containing the stack and is used to indicate wether the stack should be executable. The absense of this header indicates that the stack will be executable.

引用元: https://refspecs.linuxbase.org/LSB_3.0.0/LSB-PDA/LSB-PDA/progheader.html

カーネル

このプログラムを実行する際に、プログラムのバイナリはカーネルによってメモリ上にロードされますが、その際に、PT_GNU_STACK はどう処理されるのか見てみましょう。訳あって、Linux 2.6.12 という古めのバージョンのコードを読んでいきます。

まずは、ELF 形式のプログラムをロードする処理です。

// https://elixir.bootlin.com/linux/v2.6.12/source/fs/binfmt_elf.c#L511

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
...
    elf_ppnt = elf_phdata;
    for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
        if (elf_ppnt->p_type == PT_GNU_STACK) {
            if (elf_ppnt->p_flags & PF_X)
                executable_stack = EXSTACK_ENABLE_X;
...
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
                 executable_stack);

案の定 PT_GNU_STACK と p_flags でスタックが実行可能か判断しているっぽいですね。executable_stack 変数に値が設定されたあと、setup_arg_pages 関数に渡ります。

では、次に setup_arg_pages 関数を見てみましょう。スタック領域の設定をしているところです。

// https://elixir.bootlin.com/linux/v2.6.12/source/fs/exec.c#L349

int setup_arg_pages(struct linux_binprm *bprm,
            unsigned long stack_top,
            int executable_stack)
{
...
        /* Adjust stack execute permissions; explicitly enable
        * for EXSTACK_ENABLE_X, disable for EXSTACK_DISABLE_X
        * and leave alone (arch default) otherwise. */
        if (unlikely(executable_stack == EXSTACK_ENABLE_X))
            mpnt->vm_flags = VM_STACK_FLAGS |  VM_EXEC;
        else if (executable_stack == EXSTACK_DISABLE_X)
            mpnt->vm_flags = VM_STACK_FLAGS & ~VM_EXEC;
        else
            mpnt->vm_flags = VM_STACK_FLAGS;
        mpnt->vm_flags |= mm->def_flags;
        mpnt->vm_page_prot = protection_map[mpnt->vm_flags & 0x7];
...
    for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
        struct page *page = bprm->page[i];
        if (page) {
            bprm->page[i] = NULL;
            install_arg_page(mpnt, page, stack_base);
        }
        stack_base += PAGE_SIZE;
    }
    up_write(&mm->mmap_sem);
    
    return 0;
}

EXSTACK_ENABLE_X なら vm_flags に VM_STACK_FLAGS と VM_EXEC を設定し、それが protection_map 配列によって別の値に変換され、vm_page_prot に設定されているのがわかります。つまり、スタックの実行可否の情報は vm_page_prot に設定されたということですね。

この vm_page_prot には一体どんな情報が設定されるのでしょうか。protection_map 配列を見てみると

// https://elixir.bootlin.com/linux/v2.6.12/source/mm/mmap.c#L57

pgprot_t protection_map[16] = {
    __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111,
    __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111
};

と定義されています。では、この配列の中身は何でしょうか。これらの値の定義は CPU ごとに異なります。今回は x86_64 の実装を見ます。

// https://elixir.bootlin.com/linux/v2.6.12/source/include/asm-x86_64/pgtable.h#L191

...

#define _PAGE_BIT_NX           63

...

#define _PAGE_NX        (1UL<<_PAGE_BIT_NX)

...

#define PAGE_NONE  __pgprot(_PAGE_PROTNONE | _PAGE_ACCESSED)
#define PAGE_SHARED    __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_SHARED_EXEC __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | _PAGE_ACCESSED)
#define PAGE_COPY_NOEXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_COPY PAGE_COPY_NOEXEC
#define PAGE_COPY_EXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED)
#define PAGE_READONLY  __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED | _PAGE_NX)
#define PAGE_READONLY_EXEC __pgprot(_PAGE_PRESENT | _PAGE_USER | _PAGE_ACCESSED)

...

#define __P000 PAGE_NONE
#define __P001 PAGE_READONLY
#define __P010 PAGE_COPY
#define __P011 PAGE_COPY
#define __P100 PAGE_READONLY_EXEC
#define __P101 PAGE_READONLY_EXEC
#define __P110 PAGE_COPY_EXEC
#define __P111 PAGE_COPY_EXEC

#define __S000 PAGE_NONE
#define __S001 PAGE_READONLY
#define __S010 PAGE_SHARED
#define __S011 PAGE_SHARED
#define __S100 PAGE_READONLY_EXEC
#define __S101 PAGE_READONLY_EXEC
#define __S110 PAGE_SHARED_EXEC
#define __S111 PAGE_SHARED_EXEC

NX の文字が見えましたね。_PAGE_NX の値が 1 << 63 と設定されていますが、これは一体なんでしょうか。Intel SDM を見てみましょう。

出典: https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

(0 based indexing で) 63 bit 目に XD と書いてありますね。つまり、1 << 63 は NX bit を指してるわけです。

おわりに

gcc -z execstack が指定されるとどうなるのか、カーネルレベルまで追ってみました。頭ではたぶんこうなっているなと思っていましたが、実際にコードを追うと、自分の考えに自信を持てるようになりますね。

PHP で未定義の定数は文字列として解釈されるよという話

動作環境

$ php -v
PHP 7.3.28 (cli) (built: Apr 29 2021 18:47:43) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.28, Copyright (c) 1998-2018 Zend Technologies
    with Xdebug v3.0.4, Copyright (c) 2002-2021, by Derick Rethans

本題

タイトル通り。今日の仕事中に出会った PHP の不思議動作です。

PHP では、$がついてない識別子は定数として解釈されます。更に、その定数が未定義の場合、識別子がそのまま文字列として解釈されます。

なので、下記のコードにおいて $msg には文字列 "helloworld" が格納されます。

<?php
$msg = hello . world;
echo $msg; /* "helloworld" */

また、文字列として解釈されるにはされますが、以下の warning が出力されます。 warning なので動作自体は停止しませんね。

<warning>PHP Warning:  Use of undefined constant hello - assumed 'hello' (this will throw an Error in a future version of PHP) in Psy Shell code on line 1</warning>
<warning>PHP Warning:  Use of undefined constant world - assumed 'world' (this will throw an Error in a future version of PHP) in Psy Shell code on line 1</warning>

このことについては、PHP のドキュメントの User Contributed Notes でも言及されています。

Lets expand comment of 'storm' about usage of undefined constants. His claim that 'An undefined constant evaluates as true...' is wrong and right at same time. As said further in documentation ' If you use an undefined constant, PHP assumes that you mean the name of the constant itself, just as if you called it as a string...'. So yeah, undefined global constant when accessed directly will be resolved as string equal to name of sought constant (as thought PHP supposes that programmer had forgot apostrophes and autofixes it) and non-zero non-empty string converts to True.

引用元: https://www.php.net/manual/ja/language.constants.php

この動作は一体誰得なんですかねぇ…

ちなみに、PHP8 では以下のように warning ではなく error になるようです。

Fatal error: Uncaught Error: Undefined constant "hello" in /tmp/15newekq82bbl0/tester.php:3 Stack trace: #0 {main} thrown in /tmp/15newekq82bbl0/tester.php on line 3

Docker コンテナ内で Apache と Laravel Queue Worker を同時に動かす

はじめに

仕事で Supervisor を使って Docker コンテナ内で Apache と Laravel Queue Worker を同時に動かすことがあったので、その作業記録としてこの記事を残します。 Supervisor ってのはこいつですね。複数のプロセスを監視、制御できるプログラムです。

supervisord.org

設定

Dockerfile

まずは Dockerfile の設定ですね。

FROM php:7.3-apache
...
RUN apt-get install supervisor
...
COPY ./docker/app/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
CMD ["/usr/bin/supervisord"]

ちなみに、php:7.3-apache の Docker イメージは以下のように ENTRYPOINT に docker-php-entrypoint、CMD に apache2-foreground を設定します。ENTRYPOINT と CMD を同時に指定するとENTRYPOINT 側を実行ファイル、CMD 側を引数にしてプログラムを実行します。

ENTRYPOINT ["docker-php-entrypoint"]
...
CMD ["apache2-foreground"]

docs.docker.jp

docker-php-entrypoint は以下のようになっていて、- で始まる引数を渡せば apache2-forground にその引数を渡して実行し、それ以外は引数をそのまま実行します。

#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
        set -- apache2-foreground "$@"
fi

exec "$@"

なので、CMD をこちら側で再定義してやれば、docker-php-entrypoint に実行させるコマンドを上書きできるというわけですね。

supervisord.conf

次に、supervisord.conf です。

[supervisord]
nodaemon=true

[program:apache]
command=/usr/local/bin/apache2-foreground
redirect_stderr=true
stdout_logfile=/dev/stdout

[program:laravel-worker]
command=php /var/www/html/artisan queue:work --tries=3
redirect_stderr=true
stdout_logfile=/dev/stdout

ポイントとしては

  • supervisord を nodaemon モードで動かす(daemon で動かすとコンテナがすぐ終了してしまうので)
  • stdout と stderr を /dev/stdout に流して docker-compose logs コマンドでログを見れるようにしている

とこくらいですかね。

動作確認

上記の設定をした上で Docker コンテナを立ち上げ、コンテナ内に入ります。以下のように RUNNING 状態になっていると成功です。

$ supervisorctl status
apache                           RUNNING   pid 10, uptime 6:31:07
laravel-worker                   RUNNING   pid 11, uptime 6:31:07

あまりに薄い Laravel 要素

Laravel の要素があまりにも薄いので Job と Queue を使うときのテストの tips でも書いておきます。Queue に Job が push されていることを確認し、さらに Job の処理を実行するには以下のように書けばいいです。

Queue::fake();
...
// Job を Queue に突っ込む処理
...
Queue::assertPushed(SomethingJob::class, function ($job) {
    $job->handle();
    return <<check something>>;
});
...
// Job の処理結果に依存する処理

おわりに

あまりにも Laravel 要素が薄いなこの記事