一つのdocker-compose.ymlで2つの環境用の設定をする

あまり真剣にdocker-composeを使うことがなかったけど、フロントエンドとバックエンドとDBを同じdocker-composeで立ててるとき、リクエストが遅くなります。
そのため、フロントはnginxで動かそうかなとか考えるのですが、いちいちビルドする必要あるし、めんどくさいなーと思ってました。
ローカルを汚したくないからとりあえず実行環境はdockerで考えてる人は多いかと思います。
ビルドとかするときはどうするかまだ迷ってますが、profilesを書くことで、普段はprofilesをセットすることで、

$ docker-compose --profile dev up --build 

のように --profile オプションをつけることで自動で切り替えられます。
今回はdevとprodと2つのprofilesを設定しています。  

docker-compse.ymlも以下のような書き方で起動するコンテナを設定できます。

services:
  front:
    profiles: ["dev"]
    container_name: "front"
    depends_on:
      - api 
    volumes:
      - ./front:/app/front
      - ./scripts:/app/scripts:cached
    build:
      context: ./docker/front
      dockerfile: Dockerfile
    working_dir: /app/scripts
    command: bash -c "sh front.sh"
    ports:
      - 5173:5173
    env_file:
      - .env_front
  nginx:
    profiles: ["prod"]
    container_name: "nginx"
    depends_on:
      - api 
    volumes:
      - ./front/dist:/var/www/html/
      - ./docker/nginx/server.conf:/etc/nginx/nginx.conf
    build:
      context: ./docker/nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
  api:
    profiles: ["dev","prod"]
    container_name: "api"
    depends_on:
      - db
    volumes:
      - ./app:/workspace/app
      - ./scripts:/workspace/scripts:cached
    build:
      context: ./docker/api
      dockerfile: Dockerfile
    working_dir: /workspace/scripts
    command: bash -c "sh run.sh"
    ports:
      - 8080:8080
    networks:
      - fastapi_network
    environment:
      APP_ENV: "development"
      TZ: "Asia/Tokyo"
    env_file:
      - .env

中学受験 ~算数の逆算~

小4の息子が中学受験したいと言い出した(それ自体は良いこと)ので、塾を探して塾に入って勉強を始めた。
夏休み明けからということで、たぶん受験勉強開始したのは遅い方だろう。それは仕方ないが本人のやる気もあり、それを止める理由もそこまでないなと思っている。

そして、塾に入って宿題がたくさん出てたのでと、中学生以降の知識をフル活用すればさほど難しくない計算も小学生の知識でどうやって解くか?を親として考えてしまう問題も多々ある。そんな感じなので、子供が勉強してる横で算数についてはどう解くのか勉強することにしたのでやったことをまとめます。

逆算

逆算ってなに?って思うかともいるかもしれませんが、   □が入った計算を四則演算の優先度を逆から順番にやっていくものです。

例えば、5+(10-□)×2=21 とあった場合、
最初に ()内の 10-□が優先度的に最初で、次に(10-□)×2、最後に5を足すという四則演算がありますが、□があるので計算ができません。
ついでに、小学生は左辺を右辺に移すという概念ではないので、
- 四則演算の優先順を逆からやっていく
- 記号は反転(✖️↔︎➗, ➕↔︎マイナス)と置き換える

ここでは、
1.21-5=16
2.16÷2=8
3 10-8=2
となります。さて、ここで疑問に持たれる方もいますが、
引き算についても基本的には➖をプラスにしますが、そうでないケースがあります。
それは、 10-□=8 のようなケースです。これは 10-2=8となるのですが、□を求めるだけなら
10-8=2と簡単にできます。
しかし、ここでルールにのっとってやろうとすると、
10+8=18となります。
これでは間違った答えが出てしまいます。
なので、引き算で引かれる数がわかっているときに限り、□と=の右にある数字を入れ替えます。
また、□-2=8の引かれる数がわからないケースは、8+2=10と、基本のルールに則った計算をします。

ルール
- 引かれる数がわかってる時は、□と答えを入れ替える
- 引かれる数がわからない時は、➖を➕にする。

これらのルールを守れば基本的に間違わないはずです。
子供と算数の復習しつつ、勉強するのは面白いですね。

それはそうと、最近の小学校の算数の比例・反比例でxを使うようになったそうですね。
□もxに置き換えて色々できると便利そうですが、そうはなってないので、縛りがあって面白いなと思いました。

dockerにmysqlclientを入れようとするとエラーになる

fastapiとmysqlをdocker-composeで起動しようとしてたら以下のようなエラーがでた。 mysqlclientをインストールする時のエラーだ。

5.468 Collecting sqlalchemy_utils (from -r requirements.txt (line 6))
5.475   Downloading sqlalchemy_utils-0.42.0-py3-none-any.whl.metadata (4.6 kB)
5.494 Collecting mysqlclient (from -r requirements.txt (line 7))
5.500   Downloading mysqlclient-2.2.7.tar.gz (91 kB)
5.513   Installing build dependencies: started
7.912   Installing build dependencies: finished with status 'done'
7.912   Getting requirements to build wheel: started
8.046   Getting requirements to build wheel: finished with status 'error'
8.048   error: subprocess-exited-with-error
8.048   
8.048   × Getting requirements to build wheel did not run successfully.
8.048   │ exit code: 1
8.048   ╰─> [35 lines of output]
8.048       /bin/sh: 1: pkg-config: not found
8.048       /bin/sh: 1: pkg-config: not found
8.048       /bin/sh: 1: pkg-config: not found
8.048       /bin/sh: 1: pkg-config: not found
8.048       Trying pkg-config --exists mysqlclient
8.048       Command 'pkg-config --exists mysqlclient' returned non-zero exit status 127.
8.048       Trying pkg-config --exists mariadb
8.048       Command 'pkg-config --exists mariadb' returned non-zero exit status 127.
8.048       Trying pkg-config --exists libmariadb
8.048       Command 'pkg-config --exists libmariadb' returned non-zero exit status 127.
8.048       Trying pkg-config --exists perconaserverclient
8.048       Command 'pkg-config --exists perconaserverclient' returned non-zero exit status 127.
8.048       Traceback (most recent call last):
8.048         File "/usr/local/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 389, in <module>
8.048           main()
8.048           ~~~~^^
8.048         File "/usr/local/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 373, in main
8.048           json_out["return_val"] = hook(**hook_input["kwargs"])
8.048                                    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^
8.048         File "/usr/local/lib/python3.13/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 143, in get_requires_for_build_wheel
8.048           return hook(config_settings)
8.048         File "/tmp/pip-build-env-bl45y4e9/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 331, in get_requires_for_build_wheel
8.048           return self._get_build_requires(config_settings, requirements=[])
8.048                  ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8.048         File "/tmp/pip-build-env-bl45y4e9/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 301, in _get_build_requires
8.048           self.run_setup()
8.048           ~~~~~~~~~~~~~~^^
8.048         File "/tmp/pip-build-env-bl45y4e9/overlay/lib/python3.13/site-packages/setuptools/build_meta.py", line 317, in run_setup
8.048           exec(code, locals())
8.048           ~~~~^^^^^^^^^^^^^^^^
8.048         File "<string>", line 156, in <module>
8.048         File "<string>", line 49, in get_config_posix
8.048         File "<string>", line 28, in find_package_name
8.048       Exception: Can not find valid pkg-config name.
8.048       Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually
8.048       [end of output]
8.048   
8.048   note: This error originates from a subprocess, and is likely not a problem with pip.
9.171 error: subprocess-exited-with-error
9.171 
9.171 × Getting requirements to build wheel did not run successfully.
9.171 │ exit code: 1
9.171 ╰─> See above for output.

とエラーでてた。 原因調べると、パッケージが足りないようなので、

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    pkg-config \
    default-libmysqlclient-dev

mysqlは以上のようなパッケージが必要になる。

alembicで生成したマイグレーションファイルにsqlmodelがimportされてなかった

alembic で マイグレーションファイルを作成した後、データベースを反映させようとしたところ、以下のようなエラーが出た。 fastapi + alembic + sqlmodel + sqlalchemyと使っていますが、sqlmodelがimportされず、実行時にエラーが出てた。

# python -m alembic upgrade head
mysql+mysqldb://{username}:{password@{hostname}:3306/{database}?charset=utf8mb4
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 88e53880a27c, create todos table
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/root/.local/lib/python3.13/site-packages/alembic/__main__.py", line 4, in <module>
    main(prog="alembic")
    ~~~~^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.13/site-packages/alembic/config.py", line 1022, in main
    CommandLine(prog=prog).main(argv=argv)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/root/.local/lib/python3.13/site-packages/alembic/config.py", line 1012, in main
    self.run_cmd(cfg, options)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.13/site-packages/alembic/config.py", line 946, in run_cmd
    fn(
    ~~^
        config,
        ^^^^^^^
        *[getattr(options, k, None) for k in positional],
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        **{k: getattr(options, k, None) for k in kwarg},
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/root/.local/lib/python3.13/site-packages/alembic/command.py", line 483, in upgrade
    script.run_env()
    ~~~~~~~~~~~~~~^^
  File "/root/.local/lib/python3.13/site-packages/alembic/script/base.py", line 549, in run_env
    util.load_python_file(self.dir, "env.py")
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/root/.local/lib/python3.13/site-packages/alembic/util/pyfiles.py", line 116, in load_python_file
    module = load_module_py(module_id, path)
  File "/root/.local/lib/python3.13/site-packages/alembic/util/pyfiles.py", line 136, in load_module_py
    spec.loader.exec_module(module)  # type: ignore
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/workspace/app/db/migration/env.py", line 68, in <module>
    run_migrations_online()
    ~~~~~~~~~~~~~~~~~~~~~^^
  File "/workspace/app/db/migration/env.py", line 63, in run_migrations_online
    context.run_migrations()
    ~~~~~~~~~~~~~~~~~~~~~~^^
  File "<string>", line 8, in run_migrations
  File "/root/.local/lib/python3.13/site-packages/alembic/runtime/environment.py", line 946, in run_migrations
    self.get_context().run_migrations(**kw)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/root/.local/lib/python3.13/site-packages/alembic/runtime/migration.py", line 627, in run_migrations
    step.migration_fn(**kw)
    ~~~~~~~~~~~~~~~~~^^^^^^
  File "/workspace/app/db/migration/versions/88e53880a27c_create_todos_table.py", line 26, in upgrade
    sa.Column('title', sqlmodel.sql.sqltypes.AutoString(length=200), nullable=False),
                       ^^^^^^^^
NameError: name 'sqlmodel' is not defined

結局そういうこういうエラーなので、importしてあげるしかないので以下のような修正を行なった

# diff -u app/db/migration/versions/88e53880a27c_create_todos_table.py.org app/db/migration/versions/88e53880a27c_create_todos_table.py
--- app/db/migration/versions/88e53880a27c_create_todos_table.py.org    2025-09-09 21:54:23
+++ app/db/migration/versions/88e53880a27c_create_todos_table.py        2025-09-09 21:50:55
@@ -9,6 +9,7 @@
 
 from alembic import op
 import sqlalchemy as sa
+import sqlmodel
 
 
 # revision identifiers, used by Alembic.

Ansibleのfirewalldで特定ホストのIPをセットする

firewalldで制限をかける設定をするとき、addressはhostnameに対応してません。(ufwは確認してないですが、hostname対応してないっぽいので同じ方法で対応できるかと思います。)

送信元アドレスを指定することにより、接続試行の発信元を送信元アドレスに制限できます。ソースアドレスまたはアドレス範囲は、IPv4 または IPv6 のマスクを持つ IP アドレスまたはネットワーク IP アドレスのいずれかです。IPv4 の場合、マスクはネットワークマスクまたは単純な番号になります。IPv6 の場合、マスクは単純な番号です。ホスト名の使用はサポートされていません。NOT キーワードを追加することで、source address コマンドの意味を反転させることができます。指定されたアドレス以外はすべて一致します。

引用元: docs.redhat.com

環境が1つだったらIPを直書きできますが、複数環境ある場合はそうはいきません。

そのため、digを使用します。digは手元で動くので、dnspythonをインストールしておく必要があります。 その上で以下のように書くとhostnameでもセットできるようになります。

- name: host AとB からsshを許可
  ansible.posix.firewalld:
    zone: public
    rich_rule: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  with_items:
    - rule family="ipv4" source address="{{ lookup('dig', 'hostA') }}" port port="22" protocol="tcp" accept
    - rule family="ipv4" source address="{{ lookup('dig', 'hostB') }}" port port="22" protocol="tcp" accept

このhostAやhostBは {{ host_something }} のように変数を入れることも可能です。 これでhostnameに環境依存のところを環境の部分だけ変数にしたりして対応してかけますね。

PDF パスワード解除スクリプトの作成

最近パスワードのかかったpdfファイルをよく受け取るので、pdfのパスワードを削除するスクリプトを書いた。

import pypdf

SRC_PDF = 'password.pdf'  #パスワードがかかったPDF
DST_PDF = 'non_password.pdf' #パスワードを解除したPDF
PASSWORD = '' #PDFのパスワード

def delete_password(src_pdf, dst_pdf, src_password):
    src_pdf_file = pypdf.PdfReader(src_pdf)
    src_pdf_file.decrypt(src_password)

    dst_pdf_file = pypdf.PdfWriter()
    dst_pdf_file.clone_reader_document_root(src_pdf_file)

    d = {key: src_pdf_file.metadata[key] for key in src_pdf_file.metadata.keys()}
    dst_pdf_file.add_metadata(d)

    dst_pdf_file.write(dst_pdf)


delete_password(SRC_PDF, DST_PDF, PASSWORD)

pypdfでパスワードのかかったファイルのパスワードを解除します。
パスワードのかかったpdfのファイル名をSRC_PDFにセットし、
新しいファイル名をDST_PDF、
パスワードをPASSWORDにセットすれば、パスワードのかかってないファイルがDST_PDFにセットしたファイル名で出力されます。

フロントエンドでVueを勉強するときにChatGPTにエラー対応させたらだいぶ楽だった

フロントエンドはとくに流れが早く、全然わからん!という感じだったので、とりあえずなんか昔同僚が触ってたVue.jsで使って何か作ろうと思いました。
最近はChatGPTなど便利なツールもあるので、やらない理由がどんどんなくなってきていて素晴らしいなーと思っています。
怠け者の自分でも、めんどくささがなくなったなーと感心してます。

それでも新しいこと始めるのはめんどくさいのでガンガンChatGPTを使っていきました。新しいこと始めるコストはないに等しいなと思います。 でも何かしらのプログラミング言語をやったことない人が頼ると少し壁はあるのかな?という感じです。
トラップを回避してく感があるぐらいの人ならガンガンできるでしょう。
言語とかのセットアップも最近はめんどくさいので Dockerに乗っけてガンガン動かしてます。
言語のバージョンとかパッケージの管理とか毎回スクラップアンドビルドで綺麗にしていけばいいんじゃないかーと思ってます。

qiita.com

今回勉強にはこのブログを参考にさせていただきました。
ただ、実際 フロント動かすに、axiosをfetchで書き換えたり、element plusをvuetifyに書き換えたりしてます。
axiosは脆弱性など報告が多く最近の流れ的にもfetchの方が良いかな?と思ったり、
vuetifyはレスポンシブデザインっぽいこと書かれてたので、今後もフロントエンドを自分で書くときは極力手間を減らしたいのでなんとなく良い感じになるよう選んでます。
(エラーが出たらガンガンChatGPTに聞きましょう。ググる時間がもったいないです。) あと、バックエンドはflaskに書き換えてます。modelかけば falsk db init, migrate, upgradeとさっとできるよう flask-migrateを使うのが楽な気がしてるからです。

fetchの書き換えで、以下のような違いがあってだいぶ最初エラーが出てたけど、ChatGPT先生に聞いて、とりあえず動く状態まで持ってくと「じゃぁなんでこれがこうなってるの?」を調べたり色々試すのが簡単でした。

axios

  getAll(): Promise<any> {
    return http.get("/api/item");
  }

  create(name: string, price: number): Promise<any> {
    return http.post(
        `/api/item`, 
        { 
            name: `${name}`,
            price: `${price}`
        }
    );
  }

fetch

  async getAll(): Promise<any> {
    return httpClient("/api/item", { method: "GET" });
  }

  async create(name: string, price: number): Promise<any> {
    const res =  httpClient(
      "/api/item",
      {
        method: "POST",
        body: JSON.stringify({name: name, price: price})
    });
    return res
  }

そして、コードを検証するため、docker-composeを準備しました。

version: '3.8'

services:
  frontend:
    build: ./frontend
    ports:
      - "8080:80"
    depends_on:
      - backend
    networks:
      - app-network

  backend:
    build: ./backend
    ports:
      - "5000:5000"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

とdocker-composeを準備して、めでたく起動はdocker-compose upだけで動くようになりました。

たまに npm run buildでエラーが出るから調べる 

 > [frontend build-stage 6/6] RUN npm run build:
0.146
0.146 > front-sample@0.0.0 build
0.146 > vue-tsc -b && vite build
0.146
0.148 sh: 1: vue-tsc: Permission denied
------
failed to solve: process "/bin/sh -c npm run build" did not complete successfully: exit code: 127

と権限んがなさそうなので、
対処としては、

$ chmod 755 frontend/node_modules/vue-tsc/bin/vue-tsc.js
chmod: Invalid file mode: 755

と対処してあげれば動きます。

実際のコード

github.com