🐶

こんにちわSymfony7 with Flyio

2023/12/16に公開

Symfony Advent Calendar 2023 の14日目の記事です!
(遅くなってしまってすいません...😇)

はじめに

久しぶりにSymfonyを触っているんですが、気づいたらSymfony7になっていました...。
自分の知識はSymfony5.4で止まっています。
そこで、Symfony7を使ってflyioにデプロイするまでの諸々を書いていきたいと思います。

何が変わったのか?

最初にSymfony7を触った感想としては「あれ??5.4と比べると、ほとんど何も変わってない?」でした。

というか細かい実装は変わっているだろうけど、、Symfonyってメジャーバージョンアップしても、コントローラ作って、フォーム作って、DoctrineでEntity作ってみたいなのところはあんまり変わってなくてキャッチアップしやすいですね。
(これがタダで使えるとかまじでいいの?って気持ちになります。)

そもそも6.4と7.0の違いは?

https://x.com/ttskch/status/1735168534790156792?s=20

雑に呟いたら@ttskchさんが教えてくました。

つまり機能としてはSymfony6.4と変わらないわけです。
ちなみに型定義が結構追加されているのは個人的に嬉しいです。

https://x.com/ippey_s/status/1699313820349673585?s=20

DoctrineのCollectionもこんな感じで書けるのでphpstanに怒られないように頑張ってコード書くみたいな本末転倒なことが起きにくくなっています。

    /**
     * @var Collection<int, Item>
     */
    #[ORM\OneToMany(mappedBy: 'brand', targetEntity: Item::class, fetch: 'EXTRA_LAZY')]
    private Collection $items;

使う側としては5.4のころからほとんど変わってない印象が強いです。

fly.ioにSymfonyをデプロイする

私の大好きなfly.ioにSymfony7をデプロイするまでの手順を書きます。

fly.ioにアカウントを登録済みで、クレジットカードも登録済みだという想定からはじめます。

まずはSymfonyのプロジェクトを作る

Symfony CLIを使うことでサクッとSymfonyプロジェクトが用意できます。

$ symfony new --webapp sf7-flyio-example
$ cd sf7-flyio-example

flyctl コマンドをインストールする

公式ドキュメントの通りにインストールします。
Install flyctl

$ curl -L https://fly.io/install.sh | sh

インストールしたらログインしておきましょう。

$ flyctl auth login

上記のコマンドを実行すると、ブラウザが立ち上がってflyioのページが表示されるのでそのままログインすれば大丈夫です。

無事にログインできたかを確認するには以下のコマンドを実行してください。

$ flyctl auth whoami

ログインしたアカウントのメールアドレスが表示されます。

flyctl launchを実行する

flyctl launch コマンドでflyioにデプロイするための準備をします。
launchコマンドを実行すると言語・フレームワークをみていい感じにDockerfileやfly.tomlなどを生成してくれます。
あとは flyctl deploy をすればすぐにデプロイできます。

ただし、サポートされている言語・フレームワークに限るわけです。
fly.ioの場合Laravelはサポートされているんですが、Symfonyはサポートされていないんですよね...

でも、大丈夫です公式ドキュメントにSymfonyをデプロイするための手順が書かれています。

Using Symfony

ここからはlaunchするための手順を紹介します。

artisanを用意する

公式ドキュメントに書かれていますがartisanを用意します。
そうすることでlaunchコマンドはLaravelプロジェクトであると認識してくれるみたいです...w

We’ll start by tricking flyctl into thinking we have a Laravel application. The fly launch command just searches for a file named artisan. If it finds that, it will assume we have a Laravel application.

touch artisan
fly launch

用意できたらlaunchコマンドを実行します。

$ fly launch

Scanning source code
Detected a Laravel app
Creating app in /Users/polidog/s/github.com/polidog/sf7-flyio-example
We're about to launch your Laravel app on Fly.io. Here's what you're getting:

Organization: polidog        (fly launch defaults to the personal org)
Name:         sf7-flyio-example      (derived from your directory name)
Region:       Hong Kong, Hong Kong   (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres:     <none>                 (not requested)
Redis:        <none>                 (not requested)

? Do you want to tweak these settings before proceeding? (y/N)

「Do you want to tweak these settings before proceeding?」と聞かれるでyを入力するとブラウザが立ち上がり、リージョンやマシンスペックを変更できます。

今回はリージョンを東京にしてメモリを1GBから256MBに変更しています。

上記の設定が完了すると勝手にDocker imageのビルドが始まりますが、失敗に終わります。
これは.envが存在しないことを起因としたエラーです。

#0 15.03  [KO]
#0 15.03 Script cache:clear returned with error code 255
#0 15.03 !!  PHP Fatal error:  Uncaught Symfony\Component\Dotenv\Exception\PathException: Unable to read the "/var/www/html/.env" environment file. in /var/www/html/vendor/symfony/dotenv/Dotenv.php:553
#0 15.03 !!  Stack trace:
#0 15.03 !!  #0 /var/www/html/vendor/symfony/dotenv/Dotenv.php(106): Symfony\Component\Dotenv\Dotenv->doLoad()
#0 15.03 !!  #1 /var/www/html/vendor/symfony/dotenv/Dotenv.php(149): Symfony\Component\Dotenv\Dotenv->loadEnv()
#0 15.03 !!  #2 /var/www/html/vendor/symfony/runtime/SymfonyRuntime.php(107): Symfony\Component\Dotenv\Dotenv->bootEnv()
#0 15.03 !!  #3 /var/www/html/vendor/autoload_runtime.php(17): Symfony\Component\Runtime\SymfonyRuntime->__construct()
#0 15.03 !!  #4 /var/www/html/bin/console(11): require_once('...')
#0 15.03 !!  #5 {main}
#0 15.03 !!    thrown in /var/www/html/vendor/symfony/dotenv/Dotenv.php on line 553
#0 15.03 !!
#0 15.03 Script @auto-scripts was called via post-install-cmd

環境変数の問題について

Symfonyの場合デフォルトではcomposer install時に .env あることを前提しています。
しかしflyioでは.envは含めない方針となっています。
そのため、dockerignoreに.envが記述されています。

# 1. Ignore Laravel-specific files we don't need
bootstrap/cache/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
*.env*
.rr.yml
rr
vendor

# 2. Ignore common files/directories we don't need
fly.toml
.vscode
.idea
**/*node_modules
**.git
**.gitignore
**.gitattributes
**.sass-cache
**/*~
**/*.log
**/.DS_Store
**/Thumbs.db
public/hot

回避策として公式ではSymfonyRuntimeを書き換えて.envがなくてもcomposer installできるようにする方法が書かれています。

<?php

namespace App;

use Symfony\Component\Runtime\SymfonyRuntime;

class FlySymfonyRuntime extends SymfonyRuntime
{
    public function __construct(array $options = [])
    {
        if (isset($_ENV['FLY_APP_NAME'])) {
            // production環境でflyio上で動いている状態なので.envを読み込まない
            $options['disable_dotenv'] = true;
        } else {
            if (false === file_exists(__DIR__.'/../.env')) {
                // .envがないのでflyio上のDocker Build時になるので.envを読み込まない
                $options['disable_dotenv'] = true;
            }
        }
        parent::__construct($options);
    }
}

上記のコードを.fly/FlySymfonyRuntime.phpとして用意してください。

そして、Dockerfileを修正します。

# copy application code, skipping files based on .dockerignore
COPY . /var/www/html
COPY ./.fly/FlySymfonyRuntime.php /var/www/html/src/FlySymfonyRuntime.php

最後にfly.tomlを修正してAPP_RUNTIMEの環境変数の指定をします。

[env]
  APP_RUNTIME = '\App\FlySymfonyRuntime'

PHPのバージョンの問題について

2023/12/16現在だとflyctl lunchで生成されたfly.tomlで指定されているphpのバージョンが8.1になっています。
今回はphp8.3を使いたいので変更します。

[build]
  [build.args]
    NODE_VERSION = "18"
    PHP_VERSION = "8.3"

修正が完了したので fly deploy を実行します。
fly launch はfly.tomlやDockerfileを用意してくれるコマンドなので実際にデプロイする場合はfly deploy コマンドを使います。

$ fly deploy

しかし、公式ドキュメントの方法で設定したはずなのにデプロイに失敗してしまいました。
composer installの後でエラーメッセージが表示されていたのでcomposer関連で何かしらの問題があったようです。

composerのpost-install-cmdでエラーになる

エラーメッセージを確認するとcomposer install後に実行される bin/console cache:clear 実行時にエラーが起きていました。

#0 12.79
#0 12.79 Run composer recipes at any time to see the status of your Symfony recipes.
#0 12.79
#0 12.80 Executing script cache:clear [KO]
#0 12.86  [KO]
#0 12.86 Script cache:clear returned with error code 255
#0 12.86 !!  PHP Fatal error:  Uncaught Symfony\Component\Dotenv\Exception\PathException: Unable to read the "/var/www/html/.env" environment file. in /var/www/html/vendor/symfony/dotenv/Dotenv.php:553
#0 12.86 !!  Stack trace:
#0 12.86 !!  #0 /var/www/html/vendor/symfony/dotenv/Dotenv.php(106): Symfony\Component\Dotenv\Dotenv->doLoad()
#0 12.86 !!  #1 /var/www/html/vendor/symfony/dotenv/Dotenv.php(149): Symfony\Component\Dotenv\Dotenv->loadEnv()
#0 12.86 !!  #2 /var/www/html/vendor/symfony/runtime/SymfonyRuntime.php(107): Symfony\Component\Dotenv\Dotenv->bootEnv()
#0 12.86 !!  #3 /var/www/html/vendor/autoload_runtime.php(16): Symfony\Component\Runtime\SymfonyRuntime->__construct()
#0 12.86 !!  #4 /var/www/html/bin/console(11): require_once('...')
#0 12.86 !!  #5 {main}
#0 12.86 !!    thrown in /var/www/html/vendor/symfony/dotenv/Dotenv.php on line 553
#0 12.86 !!
#0 12.86 Script @auto-scripts was called via post-install-cmd

ここの原因は簡単で cache:clearを実行するが.envがないからエラーになっていました。

FlySymfonyRuntimeに差し替えてるはずなのになぜエラーが起きるのか?

vendor/autoroad_runtime.php の中のソースコードを確認してみると理解でます。

bin/console実行時にvendor/autoroad_runtime.phpがrequireされています。

// /bin/consle

#!/usr/bin/env php
<?php

use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;


if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
    throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $context) {
    $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);

    return new Application($kernel);
};

vendoer/autoroad_runtime.phpの中でどのSymfonyRuntimeが実行されるの決定されるのですが、その部分が以下のコードになります。

$runtime = $_SERVER['APP_RUNTIME'] ?? $_ENV['APP_RUNTIME'] ?? 'Symfony\\Component\\Runtime\\SymfonyRuntime';

$_SERVER['APP_RUNTIME']に値がなくてデフォルトの Symfony\Compnent\Runtime\SymfonyRuntimeが呼び出されて .env が存在しないのでエラーになります。

実行時はfly.tomlのenvの情報が使われると思いますが、dockerビルド時はenvがセットされないためエラーになるということだと思います。

composer.jsonを使ってSymfonyRuntimeを切り替える

The Runtime Component #Selecting Runtimes

SymfonyRuntimeのドキュメントを読んでいたらcomposer.jsonでRuntimeを切り替えるということが書かれていました。

    "extra": {
        "symfony": {
            "allow-contrib": false,
            "require": "6.4.*"
        },
        "runtime": {
            "class": "App\\FlySymfonyRuntime"
        }
    },

このように指定するとfly deployFlySymfonyRuntime で実行されていました。
しかしこれではローカル環境にエラーが出てしまいます。が、とりあえず一旦この方法で進めます。

Dockerfileの修正

再び flyctl deployするとまたエラーになります。

#0 15.88 sed: can't read app/Http/Middleware/TrustProxies.php: No such file or directory

このエラーはDockerfileがLaravel向けになっているためですね。。。

以下のようにDockerfileを修正します。

# syntax = docker/dockerfile:experimental

# Default to PHP 8.2, but we attempt to match
# the PHP version from the user (wherever `flyctl launch` is run)
# Valid version values are PHP 7.4+
ARG PHP_VERSION=8.2
ARG NODE_VERSION=18
FROM fideloper/fly-laravel:${PHP_VERSION} as base

# PHP_VERSION needs to be repeated here
# See https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact
ARG PHP_VERSION

LABEL fly_launch_runtime="laravel"

# copy application code, skipping files based on .dockerignore
COPY . /var/www/html
COPY ./.fly/FlySymfonyRuntime.php /var/www/html/src/FlySymfonyRuntime.php

RUN composer install --optimize-autoloader --no-dev \
    && chown -R www-data:www-data /var/www/html \
    && cp .fly/entrypoint.sh /entrypoint \
    && chmod +x /entrypoint

EXPOSE 8080

ENTRYPOINT ["/entrypoint"]

そしてもう一度deployします。

$ flyctl deploy

...
-------
Updating existing machines in 'sf7-flyio-example' with rolling strategy

-------
 ✔ Machine xxxxxxxxxxxxx [app] update succeeded
-------

ちなみにクレジットカード登録していないとエラーになるので気をつけてください😇

✖ Failed: error creating a new machine: failed to launch VM: To create more than 1 machine per app please add a payment method.

環境変数の設定について

無事にデプロイできたのでサイトにアクセスしてみたら、デバッグモードの画面が表示されてました...。

とうことでfly.tomlのenvを修正します。

[env]
  APP_RUNTIME = '\App\FlySymfonyRuntime'
  APP_ENV = "prod"

これでデバッグモードは表示されなくなります。

DATABASE_URLなどのクレデンシャルな環境変数について

fly.tomlにDBの接続情報などは記載できないですよね。
そういうときは flyctl secrets set コマンドで設定するか、もしくはGUIからも設定ができます。

 flyctl secrets set DATABASE_URL="postgresql://app:hoge@127.0.0.1:5432/app?serverVersion=15&charset=utf8"

おまけ

そもそもSymfonyRuntimeってなんだ?

Symfonyのpublic/index.phpはなぜ"無名関数"をreturnしているだけなのか

起動ロジックをRunnerに隠蔽し、Runnerを使ってアプリケーション起動する仕組みを使うことで、Symfony側のロジックを一切変更せず、SwooleやRoadrunnerなどでの実行を用意に切り替えれるようにするため

これはなかなかおもしろい仕組みですね。
いつの間にかこういう仕掛けになってし、ユーザーランドには影響ないのがまたすごいなぁと思いました。

まとめ

  • Symfony7は機能的には6.4からそんなに変わってない
  • flyioにデプロイするときはいろいろと工夫がいる
  • SymfonyRuntimeすげー

最後に

今日のコードはリポジトリに上げてあります。
polidog/sf7-flyio-example

パーティーハード株式会社

Discussion