とーますメモ

Ruby on Rails / Goなどの学習メモ

【Ansible】Githubのプライベート・リポジトリからGit cloneができない!!!

結論から言うと以下の流れで解決。

1)ssh-agentの設定
2)ansible.cfgの設定
3)ansible taskの設定

1) ssh-agentの設定

以下のサイトにまとまっているので、そのとおりに対応。
qiita.com

この設定を行うことで、ssh接続時のパスフレーズ入力を省略することができる。
ただし「ssh-agent」を使用すると、パスフレーズの入力を省略できるため非常に便利だが、その分セキュリティレベルは当然低下する。
作業端末から離れる場合は「ssh-agent」を停止させたり、重要なサーバ(本番サーバ等)への接続には使用しないなどのルールが推奨される。

[参考]
laboradian.com
https://32imuf.com/ssh/command/ssh-add/
https://qiita.com/naoki_mochizuki/items/93ee2643a4c6ab0a20f5

2)ansible.cfgの設定

以下をansible.cfgに追記する

[ssh_connection]
ssh_args = -o ForwardAgent=yes

[ssh_connection]ヘッダの下で、ssh_argsを設定することで、特定のsshオプションを設定できる。大体は以下のように渡すオブション毎に、「-o」を設定する

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

上記の設定では、ssh-agentのForwardAgent(エージェント転送機能)をonにしている。

3)ansible taskの設定

- name: Git clone project
  git:
    repo: "{{ project_git_repo }}"
    dest: "{{ working_direcory }}"
    version: master
    accept_hostkey: yes
  become_flags: '-E'
  tags: git


すごく大事なのは「become_flags: '-E'」

'-E'オプションの意味は「現在の環境変数を保持してコマンドを実行する」という意味になる。
【 sudo 】コマンド――スーパーユーザー(rootユーザー)の権限でコマンドを実行する:Linux基本コマンドTips(68) - @IT
Githubからcloneするため、SSHエージェント転送機能を使用する時に、サーバに環境変数として保存される「SSH_AUTH_SOCK」を
使用する必要があるためと見られる。

The agent outputs environment variable settings that this puts in place. The SSH_AUTH_SOCK environment variable is set to point to a unix-domain socket used for communicating with the agent

ssh-agent - How to configure, forwarding, protocol.

This fails if you do

> sudo git clone git@github.com:my-github-account/my-repo.git

because your environment variables are not available to the
commands running under sudo.

However, you can set the SSH_AUTH_SOCK variable for the command by
passing it on the command line like so

> sudo SSH_AUTH_SOCK=$SSH_AUTH_SOCK git clone git@github.com:my-github-account/my-repo.git

and all is well.

Git clone using ssh agent forwarding and sudo · GitHub

※1 ancible.cfg内の[privilege_escalation]ヘッダ内で以下の設定を入れても良い。

[privilege_escalation]
become_flags = -E

※2 昔の記事を探すと「sudo_flags = -E」を設定する記事も出てくるが
これは新しいバージョンでは廃止になるものなので、上記でやったほうが良い。

[参考]
Deprecation warning when using "sudo_flags" in ansible.cfg · Issue #47006 · ansible/ansible · GitHub
How do I use remote machine's SSH keys in ansible git module - Stack Overflow
Ansible and Git Permission denied (publickey) at Git Clone - Stack Overflow
Configuration file — Ansible Documentation
Understanding privilege escalation: become — Ansible Documentation

【Ansible】Nginx + Gunicornの設定

Nginx + Gunicorn + Flaskの環境構築を行った際のメモ。

流れは、Client => Nginx => Gunicorn => Flask

NginxとGunicornの接続については以下のサイトが参考になった。
Nginx + Gunicorn + Django + Aurora (MySQL) の本番環境をAnsible Playbookで構成する - Qiita
NginxとGunicornの接続をソケットからHTTPに変更した - Qiita
Gunicorn用のSystemdソケットとサービスファイルの作成 - Qiita

Socket接続とHTTP接続があるが、
NginxとGunicornを別々のサーバ上に分けて設定しない限りは、Socket接続のほうが早いので、Socket接続を採用する。

Nginxの設定

やり方は省略するが、nginx用のユーザである「nginx」ユーザを作成する。
そして以下のページと同じ設定を行い、

thoames.hatenadiary.jp

upstreamの箇所を以下のように変更するだけ。

upstream app_server {
  server unix:/run/gunicorn/socket fail_timeout=0;
}

Gunicornの設定

Pythonの環境は以下のページを参考に「pyenv + pipenv」で構築
thoames.hatenadiary.jp

defaults/main.yml (※gunicorn_app_pathは任意)

---

gunicorn_version: "20.0.4"
gunicorn_bin: "{{ working_direcory }}/.venv/bin/gunicorn"

gunicorn_app_name: "gunicorn"
gunicorn_app_path: "run:app"  # app module in run.py

gunicorn_run_dir: "/run/gunicorn"
gunicorn_config_dir: "/etc/gunicorn"
gunicorn_socket: "gunicorn.socket"
gunicorn_bind: "unix:/run/{{gunicorn_app_name}}/socket"
gunicorn_user: "{{ admin_user }}"
gunicorn_dynamic_workers: False
gunicorn_cpu_coefficient: 2
gunicorn_workers: 1

# [Log]: use nginx log functions
gunicorn_loglevel: False
gunicorn_errorlog: False
gunicorn_accesslog: False
# gunicorn_loglevel: "info"
# gunicorn_errorlog: "/var/log/gunicorn/error.log"
# gunicorn_accesslog: "/var/log/gunicorn/access.log"

gunicorn_reload: False    # only for 'development'
gunicorn_timeout: 600   # because of scraping

tasks/main.yml

---

- name: Check exist pipenv command
  shell: type pipenv
  register: exist_pipenv
  failed_when: false
  tags: gunicorn

- name: Install Gunicorn
  shell: bash -lc "pipenv install gunicorn=={{ gunicorn_version }}"
  args:
    chdir: "{{ working_direcory }}"
  when: exist_pipenv is success
  tags: gunicorn

- name: Make gunicorn config directory
  file: path={{ gunicorn_config_dir }} state=directory
  tags: [gunicorn, gunicorn_config]

- name: Configure gunicorn
  template: src=gunicorn.py.j2 dest={{ gunicorn_config_dir }}/{{ gunicorn_app_name }}.py
  notify:
    - restart socket
    - restart gunicorn
  tags: [gunicorn, gunicorn_config]

- name: Configure gunicorn socket
  template: src=gunicorn.socket.j2 dest=/etc/systemd/system/{{ gunicorn_socket }}
  notify:
    - restart socket
    - restart gunicorn
  tags: [gunicorn, gunicorn_config]

- name: Configure systemd service
  template: src=systemd.conf.j2 dest=/etc/systemd/system/{{ gunicorn_app_name }}.service mode="0755"
  notify:
    - restart socket
    - restart gunicorn
  tags: [gunicorn, gunicorn_config]

- name: Enable gunicorn service
  service: name="{{ gunicorn_app_name }}.service" enabled=yes
  tags: [gunicorn, gunicorn_config]

handlers/main.yml

---
  
- name: restart gunicorn
  service: name={{ gunicorn_app_name }} state=restarted daemon_reload=yes

- name: restart socket
  systemd: name={{gunicorn_app_name}}.socket enabled=yes state=restarted daemon_reload=yes

gunicorn.socket

[Unit]
Description=gunicorn socket

[Socket]
ListenStream={{ gunicorn_run_dir }}/socket

[Install]
WantedBy=sockets.target

gunicorn.py (gunicorn用の設定ファイル)

import multiprocessing

bind = '{{ gunicorn_bind }}'
timeout = {{gunicorn_timeout}}

{% if gunicorn_loglevel %}
loglevel = '{{ gunicorn_loglevel }}'
{% endif %}
{% if gunicorn_accesslog %}
accesslog = '{{ gunicorn_accesslog }}'
{% endif %}
{% if gunicorn_errorlog %}
errorlog = '{{ gunicorn_errorlog }}'
{% endif %}

reload = {% if gunicorn_reload %}True{% else %}False{% endif %}

{% if gunicorn_dynamic_workers %}
workers = ({{gunicorn_cpu_coefficient}} * multiprocessing.cpu_count()) + 1
{% else %}
workers = {{gunicorn_workers}}
{% endif %}

gunicorn.service (systemd用のサービス設定)

[Unit]
Description = {{ gunicorn_app_name }}
Requires = {{ gunicorn_socket }}
After = network.target

[Service]
PermissionsStartOnly = true
User = {{ gunicorn_user }}
Group = {{ gunicorn_user }}
RuntimeDirectory = {{ gunicorn_app_name }}
PIDFile = {{ gunicorn_run_dir }}/{{ gunicorn_app_name }}.pid
WorkingDirectory = {{ working_direcory }}

ExecStart = {{ gunicorn_bin }} --pid {{ gunicorn_run_dir }}/{{ gunicorn_app_name }}.pid \
                               --config {{ gunicorn_config_dir }}/{{ gunicorn_app_name }}.py \
                               {{ gunicorn_app_path }}

ExecReload = /bin/kill -s HUP $MAINPID
ExecStop = /bin/kill -s TERM $MAINPID
PrivateTmp = true

[Install]
WantedBy = multi-user.target


[その他参考]
GitHub - azavea/ansible-gunicorn: An Ansible role for installing and configuring gunicorn.
Django + Nginx + Gunicorn でアプリケーションを立ち上げる | WEBカーテンコール
ansible/gunicorn.service.j2 at master · UTNkar/ansible · GitHub
Deploying Gunicorn — Gunicorn 20.0.4 documentation

【SSH】VPS上でGithubからPullするときの設定

CI/CDの設定とかは、とりあえず忘れて単純にGithubにSSH経由で接続して、リポジトリをPullするまでの設定
ちなみに以下の環境があり、ローカルでの秘密鍵(id_rsa)と公開鍵(id_rsa.pub)がある前提。

[開発マシン(Mac)] <=> [本番サーバ] <=> [Github]

いろんなサイトを見ると、本番サーバ上に秘密鍵を置くのは危険度Maxということなので
開発マシン上の秘密鍵を用いてGithubにアクセスできる方法の「ssh-agent」という機能を使うことにする。

1)Githubに公開鍵を登録する。

id_rsa.pubの中身をコピーして、以下のサイトで公開鍵を登録するだけ。
Sign in to GitHub · GitHub
「New SSH Key」ボタンを押し、Titleには何のSSH接続のための公開鍵なのか入力し、Keyには公開鍵の中身を貼り付けて登録

ちなみにMac上で公開鍵の中身をコピーするコマンドは以下。

$ pbcopy < ~/.ssh/id_rsa.pub

2)ssh-agentへ秘密鍵の設定

鍵をssh-agentに登録

$ ssh-add -K [鍵の場所]

※「-K」 をつけると電源を落としても消えない。利便性は上がるが、セキュリティ度は下がる。

現在登録されている鍵

$ ssh-add -l

ssh-agentに鍵を登録しておけば、ssh接続時に毎回パスフレーズを入力することも回避できる。

3)ssh-agentに登録した鍵を使ってログイン

通常だと以下のコマンドでOK

$ ssh -A [鍵の名前]

ただ毎回、「-A」を叩くのがめんどくさいので「~/.ssh/config」を以下のように設定する。

$ vi ~/.ssh/config
Host prod
    ...
    ForwardAgent yes

※prodは(任意のホスト名)

以上の設定を行えば、以下のコマンドだけでssh-agentを有効にし、サーバにログインできる。

$ ssh prod

サーバのコンソールから以下のコマンドを叩けば、Githubと疎通ができることが確認できる

$ ssh -T git@github.com

【Flask】HTTPS環境で、Ajaxを使用するページを開いたら、エラーが出た。

件名の通り、以下のようなエラーが出た

The page at 'URL' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'URL'. This request has been blocked; the content must be served over HTTPS.

原因はスラッシュ漏れ。FlaskもDjangoと同じ構造みたい。
How to Make an AJAX HTTPS GET Request Using jQuery - Stack Overflow

間違い

url  : '/search'

正解

url  : '/search/'

【MySQL】SQLAlchemy内でMySQLの全文検索を試めしてみた

MySQLの設定は以下を参考
thoames.hatenadiary.jp

検索対象のインデックスがつけられる単語の最小長と最大長の設定

InnoDBなら「innodb_ft_min_token_size」、MyISAMなら「ft_min_word_len」で最小長を設定
InnoDBなら「innodb_ft_max_token_size」、MyISAMなら「ft_max_word_len」で最大長を設定

自分が使用するのはInnoDBなので、InnoDB用の設定を行う。

innodb_ft_min_token_sizeのデフォルトは「3」、innodb_ft_max_token_sizeは「84」
MySQL :: MySQL 5.7 Reference Manual :: 14.15 InnoDB Startup Options and System Variables
MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.9.6 MySQL の全文検索の微調整

/usr/local/etc/my.cnf

[mysqld]
innodb_ft_min_token_size=2
ft_min_word_len=2

設定の確認

mysql> SHOW VARIABLES like 'innodb_ft_min_token_size';

変更を有効にするためには、mysqldを再起動し、INDEXを再作成する必要がある

検索の仕方

ヒット率50%に準じた検索(全体の50%を超えたら出力されない)

SELECT * FROM products
WHERE 
 MATCH(search)
     against(ngram('protein'));

ヒット率を無視した検索

SELECT * FROM products
WHERE 
 MATCH(search)
     against(ngram('protein') in boolean mode );

検索語での結果に加えて、類似・関連語の結果も返す検索

SELECT * FROM products
WHERE 
 MATCH(search)
     against(ngram('protein') with query expansion );

以下の記事を参考
MySQLで全文検索2 - SQLer 生島勘富 のブログ
MySQLで全文検索 - FULLTEXTインデックスの基礎知識|blog|たたみラボ

Flask内で使用する

customers = session.query(Customer).filter(Customer.name.match("anomaly")).all()

現在は単一テーブルの単一カラムを対象にした検索で「BOOLEAN mode」のみしかデフォルトだと使用できない。
もし複数テーブルの複数カラム且つ「with query expansion」などを対象にした検索がしたい場合は
「ClauseElement」を使用し、自らクエリを組み立てる必要があるとのこと。
stackoverflow.com

将来的にはSQLAlchemyでサポートされるっぽい。

以下の記事を参考
Python SQLAlchemy + MySQL で 複数フィールド に対する 全文検索:MATCH AGAINST 文 を実行する - Qiita
MySQL full-text searches and SQLAlchemy; the present and a proposed future | Anomaly
MySQL — SQLAlchemy 1.3 Documentation



[他参考]
LIKE検索より50倍速い!?MySQLでラクラク高速な日本語全文検索 - bitA Tech Blog
innodb - MySQL FullText search on string shorter than 3 chars returns no rows - Database Administrators Stack Exchange

【Ruby】MacでRailsが突然動かなくなった話

2021年にまたやらかした。
thoames.hatenadiary.jp

=============

原因が何だったのか定かではないが、
恐らくXcode関係かbrew関係のコマンドを弄ったかが原因で
Railsを起動しようとしたところ以下のようなエラーが表示された。

$ bundle exec rails s

...
LoadError - library not found for class Digest::SHA1 -- digest/sha1
...

ruby on rails - LoadError: dlopen(digest/sha1.bundle): Symbol not found: _rb_Digest_SHA1_Finish - Stack Overflow
ios - Ruby: LoadError - library not found for class Digest::SHA1 -- digest/sha1 - Stack Overflow

どうやらOpenSSLが直接的な原因であったようで
/usr/local/opt/opensslのシンボリックリンクが「../Cellar/openssl@1.1/1.1.1d」を指していたのが原因っぽい。(あくまでも予想)

そしてこの問題が発生したときは、情報があまりなかったので
安易にRubyを再インストールすればよいと思ってしまい、使用していたRuby 2.3.1を消してしまったことから悲劇が始まった。

Rbenvで古いバージョン(2.3.1)がインストールできない

Ruby環境の構築にはrbenv及びbundlerを使用している。
そしてRubyを再インストールするために、以下のコマンドで2.3.1が以下のようなビルドエラーになるという状態になった

$ rbenv install 2.3.1

...
The Ruby openssl extension was not compiled.
ERROR: Ruby install aborted due to missing extensions
...

調べてみると自分の環境が、opensslのバージョンが1.1系になっており
2.4より古いRubyは、openssl@1.1がインストールされた環境ではビルドが失敗するとのこと。
なので古いopenssl経由でビルドする必要があるみたい。

自分の場合は/usr/local/opt/opensslのシンボリックリンクが、openssl@1.1を向いていたので
これを古いバージョン(以前使用していたのは1.0.2q)へ向け直す必要があったので、brew switchと組み合わせて以下のようなコマンドで、なんとか2.3.1がインストールできた。

ちなみに古いバージョンのopensslは「/usr/local/Cellar/openssl」に入っている。

$ brew switch openssl 1.0.2q
$ RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/opt/openssl" rbenv install 2.3.1
$ rbenv rehash
$ rbenv global 2.3.1
$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin18]

[参考]
Mac環境でopenssl@1.1があると古いRubyのインストールに失敗する - Qiita
Cannot install Ruby versions < 2.4 because of openssl@1.1 dependency · Issue #1353 · rbenv/ruby-build · GitHub
[Ruby] rbenv で「The Ruby openssl extension was not compiled.」が発生したときの対処 - Qiita

Bundlerが原因でRailsが動かない。

bundle exec rails sを行うために、bundlerのインストールが必要だが、後々2系だと自分が使用しているgemと相性が悪いので
古いバージョンのbundlerをインストールした

$ gem install bundler -v 1.17.1
$ gem list

...
bundler (1.17.1)
...

Nokogiri(1.6.6.2)がインストールできない

どうやらmacOS SDK headerなるものが関係あるらしいので、以下の記事を参考にmacOS_SDK_headersをインストールしようとしたが
バージョンが古い?とのことでインストールされなかった。
mojaveでbundle installできない - Qiita

$ sudo open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg

どうやら最新のものがAppleのサイト(https://developer.apple.com/download/more/)からダウンロードできるらしい。
サイトに接続し、デベロッパーアカウントを作り、最新のpkgをダウンロードすることに成功し、無事にmacOK_SDK_headersのインストールに成功。
【macOS Mojave】macOS MojaveにアプデしたらCのヘッダーファイルが読み込まれなくなった件 - 空飛ぶセロ弾き

その後、以下のコマンドを叩いて、nokogiriをインストールすることに成功した。

$ bundle config build.nokogiri --use-system-libraries=true --with-xml2-include="$(xcrun --show-sdk-path)"/usr/include/libxml2
$ bundle install

[参考]
https://www.it-swarm.net/ja/ruby/mac-os-sierra-1012%E3%81%ABnokogiri%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/827764805/
ruby on rails - An error occurred while installing nokogiri (1.6.6.2), and Bundler cannot continue - Stack Overflow

lib8とtherubyracerがインストールできない

以下の記事と同じことを行ったらインストールできた。

$ brew install v8-315
$ bundle config --local build.libv8 --with-system-v8
You are replacing the current local value of build.libv8, which is currently nil
$ bundle config --local build.therubyracer --with-v8-dir=$(brew --prefix v8-315)
You are replacing the current local value of build.therubyracer, which is currently nil
$ bundle install

mysql2がインストールできない

以下の記事と同じことを行ったらインストールできた。

$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include"
$ bundle install

[参考]
[MacOS Mojave]mysql2のbundle installに失敗してハマった件 - Qiita
macOS Mojave環境でmysql2(0.5.2)のgemインストールが失敗した - Qiita

bundle exec rails serverでエラーがでる。

上述したエラーを全て回避し、bundle installは全て成功したのに、rails sでまさかのエラー。
以下の記事の参考にして、なんとか対応できた。

bundle updateするしかなかったのにで以下のコマンドで対応

$ bundle update therubyracer

【Ruby on Rails】dyld: lazy symbol binding failed: Symbol not found: というエラーが出たので - ハイパーニートプログラマーへの道

そしてついにサーバが立ち上がった!と思ったら・・・

最後に、Ignoring・・・という変なログがサーバを立ち上げたら出てきた。。。
サーバ自体はちゃんと立ち上がるようになったら、起動する度にこのエラーを見たくないので
gemがインストールされているフォルダを見に行き、ignoring・・・のgemのgemspecを削除することで
このメッセージを強制的に出ないようにした。

※色んなサイトには「gem pristine --all」しろと書いてあるが、だめだった。

場所: /vendor/bundle/ruby/2.3.0/specifications

[参考]
ruby - Ignoring GEM because its extensions are not built - Stack Overflow