ICPにMySQLに接続するLibertyアプリケーションをデプロイ

お勉強のため、MySQLコンテナにアクセスするWebアプリケーション（WebSphere Liberty）コンテナを作成し、IBM Cloud Private CEにデプロイしてみたメモ。

IBM Cloud Private CEのインストールは以下の記事を参照。Kubernetes環境であればIKS（IBM Cloud Kubernetes Service）でもMinikubeでもDocker for Macでも何でもよいはず。

[AWSにIBM Cloud Private-CE 2.1.0.3をインストールする](https://qiita.com/sotoiwa/items/fe6e4466dfd0290a5a2f)

[AWSにIBM Cloud Private-CE 3.1をインストールする](https://qiita.com/sotoiwa/items/9cab9497d9753e1984ed)

## ClusterImagePolicyの作成

ICP 3.1ではデフォルトではイメージをPullできるレジストリーが制限されているため、DockerHubからのイメージのPullを許可する。ICP 2.1.0.3では不要。

[コンテナー・イメージ・セキュリティーの適用](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.0/manage_images/image_security.html)

```yaml:allow-dockerhub-image-policy.yaml
apiVersion: securityenforcement.admission.cloud.ibm.com/v1beta1
kind: ClusterImagePolicy
metadata:
  name: allow-dockerhub-image-policy
spec:
  repositories:
  # Docker hub Container Registry
  - name: "docker.io/*"
    policy:
```

## Namespaceの作成

今回のアプリケーションは`sugi`というNamespaceに作成することにする。そのNamespaceを作成する。

```shell
kubectl create ns sugi
```

（オプション）
kubectlが操作するデフォルトのNamespaceを作成したNamespaceに変えておく。

```shell-session
ubuntu@icp31-single:~$ kubectl config current-context
mycluster-context
ubuntu@icp31-single:~$ kubectl config set-context mycluster-context --namespace=sugi
Context "mycluster-context" modified.
ubuntu@icp31-single:~$
```

## MySQLコンテナのデプロイ

以下を参考にする。

[Run a Single-Instance Stateful Application](https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/)

### PV/PVCの作成

今回は簡単に試すためPVとしてはhostPathで使用するので、ディレクトリーを用意する。NFSが利用できればその方が望ましい。

```shell-session
root@icp31-single:~# mkdir -p /data/mysql
root@icp31-single:~# ls -l /data
total 4
drwxr-xr-x 2 root root 4096 Oct 23 10:45 mysql
root@icp31-single:~#
```

MySQLから使用するPesistentVolumeを作成する。

```yaml:mysql-pv.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
  name: mysql-pv
spec:
  storageClassName: mysql
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/data/mysql"
```

```shell
kubectl apply -f mysql-pv.yaml
```

PesistentVolumeClaimを作成する。なお、PVは全てのNamespaceで共通のリソースであるが、PVCはNamespace毎に作成するリソースである。

```yaml:mysql-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  storageClassName: mysql
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
```

```shell
kubectl apply -f mysql-pvc.yaml -n sugi
```

PVCがBoundになったことを確認しておく。

```shell-session
ubuntu@icp31-single:~$ kubectl get pvc -n sugi
NAME        STATUS    VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-pvc   Bound     mysql-pv   10Gi       RWO            mysql          1m
ubuntu@icp31-single:~$
```

### Secretの作成

MySQLのコンテナはrootパスワードを環境変数として渡す必要があるので、パスワードを保管するSecretを作成する。

パスワードをBase64でエンコードする。

```shell-session
ubuntu@icp31-single:~$ echo -n "password" | base64
cGFzc3dvcmQ=
ubuntu@icp31-single:~$
```

Secretを作成する。

```yaml:mysql-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mysql-secret
type: Opaque
data:
  root-password: cGFzc3dvcmQ=
```

```shell
kubectl apply -f mysql-secret.yaml -n sugi
```

### Deploymentの作成

MySQLのデプロイメントを作成する。先ほど作成したSecretから環境変数を定義している。

```yaml:mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:5.7
        name: mysql
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pvc
```

```shell
kubectl apply -f mysql-deployment.yaml -n sugi
```

Podが稼働したことを確認する。

```shell-session
ubuntu@icp31-single:~$ kubectl get po -n sugi
NAME                     READY     STATUS    RESTARTS   AGE
mysql-7c47657796-b7x48   1/1       Running   0          41s
ubuntu@icp31-single:~$
```

### Serviceの作成

MySQLのServiceを作成する。アプリケーションはこのServiceに接続する。

```yaml:mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
```

```shell-session
kubectl apply -f mysql-service.yaml -n sugi
```

### MySQLコンテナの初期化

MySQLコンテナにユーザーを作成し、テーブルを作成し、初期データをロードする。
本来はJobで実行するのが望ましいかもしれない。ここでは`kubectl exec`で実施する。

（追記）
Jobでやる場合は以下の記事を参照。
[Kubernetes JobでMySQLへデータをロードする](https://qiita.com/sotoiwa/items/6c80606366020b35bb8c)

（補足）
環境変数`MYSQL_DATABASE`や、`MYSQL_USER`と`MYSQL_PASSWORD`により、コンテナの初回起動時にデータベースを1つ作成したり、ユーザーをひとり作ることも可能。また、`/docker-entrypoint-initdb.d/`ディレクトリーにスクリプトを配置して、コンテナの初回起動時に初期化処理を行う事も可能。

[2.5.8.2 More Topics on Deploying MySQL Server with Docker](https://dev.mysql.com/doc/refman/5.6/en/docker-mysql-more-topics.html)

MySQLのPodを確認する。

```shell
kubectl get po -n sugi
```

Podにログインする。

```shell
kubectl exec -it -n sugi <Pod名> bash
```

mysqlクライアントを実行する。パスワードは環境変数から取得。

```shell
mysql -uroot -p${MYSQL_ROOT_PASSWORD}
```

mydbデータベースを作成する。

```sql
create database mydb;
```

libertyユーザーを作成する。

```sql
create user 'liberty' identified by 'liberty';
```

libertyユーザーにmydbへの全権限を付与する。

```sql
grant all privileges on mydb.* to 'liberty';
```

一度抜けてlibertyユーザーで入り直す。パスワードは`liberty`。

```shell
quit;
mysql -u liberty -p
```

mydbにmemberテーブルを作成する。

```sql
use mydb;
DROP TABLE IF EXISTS member;
CREATE TABLE `member` (
  id int NOT NULL AUTO_INCREMENT,
  name varchar(255) NOT NULL,
  created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
```

テストデータを挿入する。

```sql
INSERT INTO member (name) VALUES ('user1'),('user2'),('user3');
INSERT INTO member (name) VALUES ('user4'),('user5'),('user6');
```

データを確認。

```sql
SELECT * from member;
```

（実行例）

```sql
mysql> SELECT * from member;
+----+-------+---------------------+---------------------+
| id | name  | created             | updated             |
+----+-------+---------------------+---------------------+
|  1 | user1 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
|  2 | user2 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
|  3 | user3 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
|  4 | user4 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
|  5 | user5 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
|  6 | user6 | 2018-10-23 11:18:48 | 2018-10-23 11:18:48 |
+----+-------+---------------------+---------------------+
6 rows in set (0.00 sec)

mysql>
```

終了する。

```shell
quit;
exit
```

## Libertyコンテナのデプロイ

MySQLに接続して取得したデータを表示するアプリケーションを乗せたLibertyコンテナを作成する。

### アプリケーションの作成

簡単なWebアプリケーションを作成する。以下のコマンドでWarファイルを抽出して先に進んでもよい。

```
docker run --rm -v $(pwd):/data sotoiwa540/hellomysql:1.0 cp /config/dropins/hellomysql.war /data/
```

アプリケーションの作成は本筋から外れるため、ここでは要点のみ記載。

Eclipseで「Dynamic Web Project」を作成し、以下のServletを作成する。

```java:HelloServlet.java
package com.example;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

/**
 * Servlet implementation class HelloServlet
 */
@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	/**
	 * Default constructor.
	 */
	public HelloServlet() {
		// TODO Auto-generated method stub
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		response.setContentType("text/html; charset=SJIS");
		PrintWriter writer = response.getWriter();

		DataSource dataSource = null;
		Connection conn = null;

		try {
			Context context = new InitialContext();
			dataSource = (DataSource) context.lookup("jdbc/mydb");
		} catch (NamingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new ServletException(e);
		}

		try {
			conn = dataSource.getConnection();

			PreparedStatement statement = conn.prepareStatement("select name from member");
			ResultSet result = statement.executeQuery();
			while (result.next()) {
				writer.println(result.getString(1));
				System.out.println(result.getString(1));
			}

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			try {
				if (conn != null)
					conn.close();
			} catch (SQLException e) {
			}
		}

		writer.append("Served at: ").append(request.getContextPath());
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
	 *      response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}
```

MySQLのJDBCドライバーのjarファイルをダウンロードして、Libertyの`${server.config.dir}/resources/mysql`ディレクトリーに配置する。

https://dev.mysql.com/downloads/connector/j/

Libertyの`server.xml`は以下のようになる。MySQLへの接続情報は環境変数で渡すようにする。ローカルで稼働確認する際は`server.env`を使って環境変数を渡すとよい。

```xml:server.xml
<server description="new server">
  <!-- Enable features -->
  <featureManager>
    <feature>webProfile-7.0</feature>
    <feature>localConnector-1.0</feature>
  </featureManager>
  <!-- To access this server from a remote client add a host attribute to 
		the following element, e.g. host="*" -->
  <httpEndpoint host="*" httpPort="9080" httpsPort="9443" id="defaultHttpEndpoint" />
  <library id="MySQLLib">
    <fileset dir="${server.config.dir}/resources/mysql" includes="*.jar" />
  </library>
  <dataSource jndiName="jdbc/mydb" transactional="false">
    <jdbcDriver libraryRef="MySQLLib" />
    <properties databaseName="mydb" serverName="${env.MYSQL_SERVERNAME}" portNumber="${env.MYSQL_PORTNUMBER}" user="${env.MYSQL_USER}" password="${env.MYSQL_PASSWORD}" />
  </dataSource>
  <!-- Automatically expand WAR files and EAR files -->
  <applicationManager autoExpand="true" />
  <applicationMonitor updateTrigger="mbean" />
</server>
```

ローカルで稼働確認する場合、以下のコマンドでDockerでMySQLを起動することができる。

```
docker run --name mysql -e MYSQL_ROOT_PASSWORD=password -d -p 3306:3306 mysql:5.7
```

起動後は以下のコマンドでコンテナの中に入り、先ほどと同様にユーザーやデータを作る。

```
docker exec -it mysql bash
```

### Libertyイメージのビルド

Dockerfileを作成する。

```dockerfile:Dockerfile
FROM websphere-liberty:webProfile7
COPY server.xml /config/
RUN installUtility install --acceptLicense defaultServer
COPY mysql-connector-java-8.0.13.jar /config/resources/mysql/
COPY hellomysql.war /config/dropins/
```

同じディレクトリーに必要なファイルを配置する。

```
$ tree .
.
├── Dockerfile
├── hellomysql.war
├── mysql-connector-java-8.0.13.jar
└── server.xml

0 directories, 4 files
$
```

このディレクトリーでDockerビルドを実行し、DockerHubにPushする。

```
docker build -t sotoiwa540/hellomysql:1.0 .
docker push sotoiwa540/hellomysql:1.0
```

### Secretの作成

MySQLに接続するユーザーのパスワードを格納するためのSecretを作成する。

パスワードをBase64でエンコードする。

```
$ echo -n "liberty" | base64
bGliZXJ0eQ==
```

Secretを作成する。

```yaml:liberty-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: liberty-secret
type: Opaque
data:
  liberty-password: bGliZXJ0eQ==
```

```
kubectl apply -f liberty-secret.yaml -n sugi
```

### ConfigMapの作成

パスワード以外のMySQLへの接続情報を格納したConfigMapを作成する。

```yaml:liberty-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: liberty-config
data:
  mysql-servername: "mysql"
  mysql-portnumber: "3306"
  mysql-user: "liberty"
```

```
kubectl apply -f liberty-config.yaml -n sugi
```

### Libertyコンテナのデプロイ

Libertyコンテナをデプロイする。

```yaml:liberty-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: liberty
spec:
  selector:
    matchLabels:
      app: liberty
  replicas: 1
  template:
    metadata:
      labels:
        app: liberty
    spec:
      containers:
      - name: liberty
        image: sotoiwa540/hellomysql:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 9080
        env:
        - name: MYSQL_SERVERNAME
          valueFrom:
            configMapKeyRef:
              name: liberty-config
              key: mysql-servername
        - name: MYSQL_PORTNUMBER
          valueFrom:
            configMapKeyRef:
              name: liberty-config
              key: mysql-portnumber
        - name: MYSQL_USER
          valueFrom:
            configMapKeyRef:
              name: liberty-config
              key: mysql-user
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: liberty-secret
              key: liberty-password
```

```
kubectl apply -f liberty-deployment.yaml -n sugi
```

Podが稼働したことを確認する。

```shell-session
ubuntu@icp31-single:~/hellomysql$ kubectl get po -n sugi
NAME                       READY     STATUS      RESTARTS   AGE
liberty-74fd798747-pwghp   1/1       Running     0          11s
mysql-7c47657796-bkc67     1/1       Running     0          4m
ubuntu@icp31-single:~/hellomysql$
```

### Serviceの作成

Serviceを作成する。

```yaml:liberty-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: liberty
spec:
  type: ClusterIP
  selector:
    app: liberty
  ports:
  - protocol: TCP
    port: 9080
```

```
kubectl apply -f liberty-service.yaml -n sugi
```

### Ingressの作成

Ingressを作成する。
Minikubeの場合はアノテーションが`nginx.ingress.kubernetes.io/rewrite-target:`となるかもしれない。

```yaml:liberty-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: liberty
  annotations:
    ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host:
    http:
      paths:
      - path: /liberty
        backend:
          serviceName: liberty
          servicePort: 9080
```

```
kubectl apply -f liberty-ingress.yaml -n sugi
```

## 稼働確認

ブラウザで以下のURLにアクセスし、アプリケーションがMySQL内のデータを取得して表示することを確認する。

`https://<ICPのProxyのVIP>/liberty/hellomysql/HelloServlet`

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/47d86d31-bff6-e90a-5de0-b6df9814f27d.png)

ログを確認する。

```shell-session
ubuntu@icp31-single:~/hellomysql$ kubectl logs liberty-74fd798747-pwghp -n sugi
Launching defaultServer (WebSphere Application Server 18.0.0.3/wlp-1.0.22.cl180320180905-2337) on IBM J9 VM, version 8.0.5.22 - pxa6480sr5fp22-20180919_01(SR5 FP22) (en_US)
[AUDIT   ] CWWKE0001I: The server defaultServer has been launched.
[AUDIT   ] CWWKE0100I: This product is licensed for development, and limited production use. The full license terms can be viewed here: https://public.dhe.ibm.com/ibmdl/export/pub/software/websphere/wasdev/license/base_ilan/ilan/18.0.0.3/lafiles/en.html
[AUDIT   ] CWWKG0093A: Processing configuration drop-ins resource: /opt/ibm/wlp/usr/servers/defaultServer/configDropins/defaults/keystore.xml
[AUDIT   ] CWWKZ0058I: Monitoring dropins for applications.
[AUDIT   ] CWWKS4104A: LTPA keys created in 3.087 seconds. LTPA key file: /opt/ibm/wlp/output/defaultServer/resources/security/ltpa.keys
[AUDIT   ] CWPKI0803A: SSL certificate created in 7.370 seconds. SSL key file: /opt/ibm/wlp/output/defaultServer/resources/security/key.jks
[AUDIT   ] CWWKT0016I: Web application available (default_host): http://liberty-74fd798747-pwghp:9080/hellomysql/
[AUDIT   ] CWWKZ0001I: Application hellomysql started in 0.994 seconds.
[AUDIT   ] CWWKF0012I: The server installed the following features: [localConnector-1.0].
[AUDIT   ] CWWKF0011I: The server defaultServer is ready to run a smarter planet.
user1
user2
user3
user4
user5
user6
ubuntu@icp31-single:~/hellomysql$
```