Oracle Cloud Free Tier で Autonomous Database に接続する PHP アプリケーションを構築する

Oracle Cloud Free Tier が無料で色々使えるとのことだったので、これまで使っていた PHPレンタルサーバを解約して、Oracle Cloud に移行する際のメモ。

Oracle Cloud Free Tier でインスタンスと DB を作成

今回利用する Oracle Cloud Free Tier の無料枠

  • Compute: Arm ベースプロセッサ Ampere 4 OCPU(= 8 vCPU) と 24GB メモリのリソース(4インスタンスまで分割利用可)
  • Database: 1 OCPU と 20 GBのストレージのデーターベース2つ
  • Flexible Load Balancer:1インスタンス、10 Mbps。

(詳細)Oracle Cloud Free Tier | Oracle 日本

システム構成

  • 開発環境:Compute 1台 + Autonomous Database(Transaction Processing)
  • 本番環境:Compute 2台 + Autonomous Database(Transaction Processing) + Flexible Load Balancer

Compute

開発環境・本番環境もインスタンスのスペックは同一

Autonomus Database

Oracle 19c の Transaction Processing Type のデータベースを作成する。

ADMIN アカウントのパスワードを入力

アクセスを 許可するインスタンスの Public IP を指定する。

サービスゲートウェイ経由でのアクセスにする場合、vcn や Private IP の指定も可能だがここでは割愛する。

(参考)Provision Autonomous Database

PHPOracle Autonomous Database に接続する

Oracle Cloud の手順 Build a PHP Application に沿って進める。

流れとしては以下のような感じ。

  1. PHP のダウンロード&インストール
  2. Oracle Instant Client のダウンロード&インストール
  3. PHP OCI8 のダウンロード&インストール
  4. Database のクライアント証明書のダウンロード

PHP のダウンロード&インストール

RHEL8 系から導入されたモジュール方式のパッケージ管理を使って PHP-7.4 をインストールする。

モジュールには複数の Stream(バージョン)があり、希望するStreamを指定することで好きなバージョンのパッケージをインストールすることができる。

# php の利用可能 Stream(バージョン)を確認
$ dnf module list php
Oracle Linux 8 Application Stream (aarch64)
Name                            Stream                                Profiles                                             Summary
php                             7.2 [d][e]                            common [d], devel, minimal                           PHP scripting language
php                             7.3                                   common [d], devel, minimal                           PHP scripting language
php                             7.4                                   common [d], devel, minimal                           PHP scripting language

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled

# php:7.4 を enable にする
$ sudo dnf module enable php:7.4 -y

# php:7.4 が enable になっているか確認する
$ dnf module list php
Oracle Linux 8 Application Stream (aarch64)
Name                             Stream                             Profiles                                              Summary
php                              7.2 [d]                            common [d], devel, minimal                            PHP scripting language
php                              7.3                                common [d], devel, minimal                            PHP scripting language
php                              7.4 [e]                            common [d], devel, minimal                            PHP scripting language

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled

# php をインストールする
$ sudo dnf install php -y

PHP extention を install する際に必要な pecl コマンドが使えるよう「php-pear」と、PHP extention を build するときに必要な phpize コマンドが使えるよう「php-devel」もインストールしておく。

$ sudo dnf install php-pear php-devel -y

(参考)

Oracle Instant Client のダウンロード&インストール

Oracle Instant Client は Oracle Database に接続するためのライブラリ(クライアント)。

# Oracle Instant Client を配布する yum repository を使うためのパッケージ
$ sudo dnf install oracle-release-el8 -y
# Oracle Instant Client
$ sudo dnf install oracle-instantclient19.10-basic oracle-instantclient19.10-devel -y

以上で Oracle Instant Client のセットアップは完了。

PHP OCI8 のダウンロード&インストール

PHP OCI8 は PHP から Oracle Instant Client を使ってデータベースに接続するための PHP のライブラリ。

途中で instantclient のライブラリのパスを求められるが autodetect に任せるのでそのまま Enter で OK。

# 最新の oci8 は php 8.1.0 以上を求められるのでバージョンを指定(oci8-2.2.0)する
# pecl/oci8 requires PHP (version >= 8.1.0)
$ sudo PHP_DTRACE=yes pecl install oci8-2.2.0
Please provide the path to the ORACLE_HOME directory. Use 'instantclient,/path/to/instant/client/lib' if you're compiling with Oracle Instant Client [autodetect] : # autodetect に任せるのでそのまま Enter
...

# oci8 拡張の有効化と設定
$ sudo sh -c 'cat <<EOF > /etc/php.d/20-oci8.ini
extension=oci8.so
oci8.events=On
EOF'

Database のクライアント証明書のダウンロード

作成した Autonomus Database の詳細ページの「DB Connection」タブを開いて、Wallet (クライアント証明書) を作成し、zip 形式でダウンロードする。

ダウンロードした Wallet は Oracle インスタンスに scp する。

$ scp ~/Downloads/Wallet_testdb.zip dev:

以降 Oracle インスタンス側の作業。

# 接続するデータベースの設定ファイルの場所として TNS_ADMIN 環境変数を設定する
# インストールする instantclient のバージョンによって ORACLE_HOME のパスが異なるので注意
$ export TNS_ADMIN=/usr/lib/oracle/19.10/client64/lib/network/admin

$ sudo mv Wallet_testdb.zip $TNS_ADMIN
$ cd $TNS_ADMIN
$ sudo unzip Wallet_testdb.zip 
Archive:  Wallet_devariuedb.zip
replace README? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
...
$ sudo rm Wallet_testdb.zip


# 設定ファイルの修正(zip を展開したディレクトリのパスを記載する。今回は opc ユーザーを使っている。)
$ sudo cp sqlnet.ora{,.bak}
$ sudo vim sqlnet.ora # 以下の diff になるように修正する

# 環境変数を参照する設定にすることで、アプリケーションごとに異なる Oracle Database へ接続することができる
$ diff sqlnet.ora{.bak,}
1,2c1,2
< WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="?/network/admin")))
< SSL_SERVER_DN_MATCH=yes
\ No newline at end of file
---
> WALLET_LOCATION = (SOURCE = (METHOD = file) (METHOD_DATA = (DIRECTORY="$TNS_ADMIN")))
> SSL_SERVER_DN_MATCH=yes
$

(参考)104: クレデンシャル・ウォレットを利用して接続してみよう | Oracle Cloud Infrastructure チュートリアル

PHP から Oracle DB への接続確認テスト

テスト用のテーブルを作成

コマンドラインから Oracle DB に接続するツールの sqlplus を使ってテスト用のテーブルを作成する(接続確認テストを行わない場合はインストール不要)

# sqlplus のインストール
$ sudo dnf install oracle-instantclient19.10-sqlplus -y

# sqlplus を使った接続コマンド
$ sqlplus <user_name>@<database_name($TNS_ADMIN/tnsnames.ora で定義されている名前)>
Enter password: <password>

SQL> CREATE TABLE Persons (
    PersonID int,
    LastName varchar(255),
    FirstName varchar(255),
    Address varchar(255),
    City varchar(255)
);
SQL> INSERT INTO Persons VALUES (1, 'Tanaka', 'Taro', 'Tokyo', 'Minato-ku');
SQL> SELECT * from Persons;
<結果が帰ってきたらOK>

PHPOracle DB に接続するサンプルコード:test.php, , <database名> は適宜変えてください)

<?php

$conn = oci_pconnect('<user>', '<password>', '<database名(ex. hogehoge_low)>');
if (!$conn) {
    $e = oci_error();
    trigger_error(htmlentities($e['message'], ENT_QUOTES), E_USER_ERROR);
}

$stid = oci_parse($conn, 'SELECT * FROM Persons');
oci_execute($stid);

echo "<table border='1'>\n";
while ($row = oci_fetch_array($stid, OCI_ASSOC+OCI_RETURN_NULLS)) {
    echo "<tr>\n";
    foreach ($row as $item) {
        echo "    <td>" . ($item !== null ? htmlentities($item, ENT_QUOTES) : "&nbsp;") . "</td>\n";
    }
    echo "</tr>\n";
}
echo "</table>\n";

?>

実行して正しく出力されれば PHP からの DB 接続は OK。

$ php test.php
<table border='1'>
<tr>
    <td>1</td>
    <td>Tanaka</td>
    <td>Taro</td>
    <td>Tokyo</td>
    <td>Minato-ku</td>
</tr>
</table>

動作確認後、不要なテーブルは削除する

$ sqlplus <user_name>@<database_name>
SQL> DROP TABLE Persons;

nginx, php-fpm を使って PHP アプリケーションサーバーをたてる

単発実行の PHP プログラムで Autonomous Database への接続ができることを確認したら、実際に外部にWebページを公開できるようにする。

80 ポートの外部公開

VCN(Virtual Cloud Network)のサブネットのデフォルトのセキュリティリストには SSH くらいしか許可しないルールが設定されているので、 80 ポートへのアクセスを許可するために VCN のサブネットのデフォルトセキュリティリストを更新する。

該当インスタンスを含むサブネットのセキュリティリストの Ingress Rule (= in-traffic rule)に以下を許可するルールを追加するか、新規セキュリティリストを作ってサブネットに紐付ける。

  • ソース・タイプ: CIDR
  • ソースCIDR: 0.0.0.0/0(全てに公開)
  • IPプロトコル: TCP
  • 宛先ポート: 80 , 443(https で通信する場合)

(参考)ロード・バランシングの開始

Oracle Linux 8 で外部から HTTP 通信するためには firewalld の設定追加も必要。

$ sudo firewall-cmd --permanent --zone=public --add-service=http 

# https を有効化する場合
$ sudo firewall-cmd --permanent --zone=public --add-service=https

$ sudo firewall-cmd --reload

SELinux の無効化

SELinux が有効のままだと後ほどインストールする php-fpm から OCI8 の動的ライブラリの読み込みに失敗したので、無効にしておく。

# 現在の設定値の確認
$ sudo getenforce
Enforcing
$ sudo vim /etc/selinux/config
SELINUX=disabled
$ sudo reboot

# reboot 後確認
$ sudo getenforce
Disabled

nginx, php-fpm のインストール

$ sudo dnf install nginx php-fpm -y

# configure php-fpm
* 変更点1: user, group を apache → nginx に変える(php-fpm と nginx は別のユーザーで実行することがセキュリティ的には望ましいがここでは簡易化して進める)
* 変更点2: clear_env=no のコメントアウトを外して php-fpm worker プロセスが環境変数を読み込めるようにする
* 変更点3: OCI8 を使うための各種環境変数を設定
$ sudo cp /etc/php-fpm.d/www.conf{,.bak}
$ sudo vim /etc/php-fpm.d/www.conf # 以下の diff になるように修正する
$ diff /etc/php-fpm.d/www.conf{.bak,}
24c24
< user = apache
---
> user = nginx
26c26
< group = apache
---
> group = nginx
379c379
< ;clear_env = no
---
> clear_env = no
396a397,400
> env[TNS_ADMIN] = /usr/lib/oracle/19.10/client64/lib/network/admin
> env[NLS_LANG]=Japanese_Japan.AL32UTF8
> env[LD_LIBRARY_PATH]=/usr/lib/oracle/19.10/client64/lib:$LD_LIBRARY_PATH
> env[ORA_SDTZ]=Japan

nginx, php-fpm の起動と動作確認

$ sudo systemctl start nginx php-fpm
$ sudo systemctl enable nginx php-fpm
$ curl '<public_ip>'
# nginx のデフォルトページが返ってきたら OK 
# うまくいかない場合、セキュリティリストや Firewalld の設定を見直す

必要に応じて、DB 接続テスト用のプログラムを使って、php-fpm から Oracle DB に接続できることを確認する。

$ sudo cp test.php /usr/share/nginx/html/test.php
$ curl 'http://<public_ip>/test.php' #
<table border='1'>
<tr>
    <td>1</td>
    <td>Tanaka</td>
    <td>Taro</td>
    <td>Tokyo</td>
    <td>Minato-ku</td>
</tr>
</table>

うまくいかない場合、phpinfo で OCI8 が enable になっているか、php-fpm の journal にエラーや警告が出ていないかを nginx の error.log 等で確認する。

PDO を使った DB アクセスをする場合

PDO を使って DB 接続する場合は、pdo_oci のインストールが必要。

pdo_oci に必要な依存パッケージを事前にインストールする。

$ sudo dnf install sqlite-devel unixODBC-devel php-pdo -y
$ sudo pecl install sqlsrv pdo_sqlsrv

pdo_oci のインストールは、インスタンスが利用している php バージョンのソースコードから pdo_oci のみをコンパイルしてインストールする。

$ wget https://museum.php.net/php7/php-7.4.19.tar.gz
$ tar xzvf php-7.4.19.tar.gz
$ cd php-7.4.19/ext/pdo_oci/
$ phpize
$ ./configure --with-pdo-oci=instantclient,/usr/lib/oracle/19.10/client64/lib/,19.10
$ make
$ sudo make install
$ sudo sh -c "echo extension=pdo_oci.so > /etc/php.d/20-pdo_oci.ini"
$ sudo systemctl restart php-fpm

(参考) PHP: Oracle (PDO) - Manual

PDO を使ったサンプルプログラム:pdo_test.php

<?php
$db['dbname']='oci:dbname=<databasename>;charset=AL32UTF8';
$db['user']=<user>;
$db['pass']=<pass>;

try {
    $pdo = new PDO ( $db['dbname'], $db['user'], $db['pass'], array (   
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    ) );
    $statement = $pdo->query ( 'SELECT * FROM Persons' );
    while ( $row = $statement->fetch ( PDO::FETCH_ASSOC ) ) {
        var_dump($row);
    }
    $statement->closeCursor();
} catch ( PDOException $e ) {
    exit ( 'Failed to connect database: ' . $e->getMessage () );
}

(参考) PHP: PDO_OCI DSN - Manual

正常に実行できれば PDO を使って Oracle DB に接続ができている

$ php pdo_test.php
array(5) {
  ["PERSONID"]=>
  string(1) "1"
  ["LASTNAME"]=>
  string(6) "Tanaka"
  ["FIRSTNAME"]=>
  string(4) "Taro"
  ["ADDRESS"]=>
  string(5) "Tokyo"
  ["CITY"]=>
  string(9) "Minato-ku"
}
$

実アプリケーションの配置

/usr/share/nginx/html/ に PHP アプリケーションを配置して動作することを確認する。

本番用 Load Balancer の利用

冗長化、負荷分散のために本番環境は LB を使ってバックエンドサーバ2台にアクセスが振られるようにする。

バックエンドサーバのインスタンスセットアップ後、LB を作成する。

(適当に設定)

LB 作成後、バックエンドリスナーにバックエンドサーバを追加する。

作成後、LB 経由でバックエンドサーバーにアクセスできることを確認する。

$ curl 'http://<LB の public ip>'

不要なアクセスを遮断する

セットアップ後は SSH や ICMP など不要なアクセスを遮断するため、不要な Ingress Rule を削除する。

注:以降の設定を実施すると SSH 接続もできなくなり、LB 経由での 80 アクセスしかできなくなるのでアプリケーションのセットアップが完全に終わってから実施すること。

サブネットのデフォルトセキュリティリストの Ingress(in-traffic) で不要なものを削除する

ingress_rule

なお、本当は Egress Rule やルート表(参考:ロード・バランシングの開始)で Autonomous Database 以外の public network へのアクセスを遮断したかったが、Autonomous Database の ip は変わる可能性があるので制限が難しかった。

(Free Tier では使えないが、「Private Endpoint Access to Autonomous Database」を使うと VCN にでプライベートなエンドポイントを生やせるよう)

通常、80 ポートのみ公開していれば Web サーバー(nginx)に致命的な脆弱性が見つからない限りは大丈夫だろうが、その辺は要件次第で検討。

備考

今回はあまり触れられなかったが、実際には以下の点を考慮に入れる。