Google AppEngineのJava8サポートが終了するので対処してみた

:::note
ご注意
この記事は2024年1月時点での内容です。賞味期限は短いと思われるので注意してください。おなかを下したりします。
あと途中のテキストは9割がた与太なので、適当に読み流してください。
:::

:::note
2024-03-02
記事内の一部を編集しました。
フレキシブル環境 -> スタンダード環境
フレキシブルだと無料枠使えないからね！
:::

## これまでのあらすじ

はるか昔にWebのサイトつくってねって依頼されて、当時（ドメイン以外は）無料でどうにかできるAppEngineでサイト作ってお渡ししたところから、なんかもう使えなくなるみたいなんだけれど、どうにかできる？期限は2024年の1月30日なんだけれどってさっきお願いされました。まじかやべえ。なんも覚えてねえ。期限までの時間もねえ。

## 現状GAE/Jのそこらへんってどうなってるん？

ちょい調べたところ、Java8サポートの終了っていうのは小さい問題で、Javaのランタイム環境がサーブレットコンテナじゃなくなっておるってところが大問題。WARのデプロイじゃだめってことなんね。まじどうしよう。

## じゃあどうしようか？

まず参照したのはこれ。

https://cloud.google.com/appengine/docs/standard/java-gen2/runtime?hl=ja

主なJavaフレームワークのサンプルはあるよって書いてあるけれど、最初にあげられてるSpringBotとか重いだけで超いらない。ちなみに私はJavaのフレームワークだったらWicketが好きです。愛してます。Wicket最高！ヒャッハー！！SpringなんてもんはせせこましくDIだけしてりゃあいいんだよ！

いやちがうそうじゃない。話がそれた。

まあ他に並んでいるのもだいたいJetty抱え込んだアレなので、勉強がてら組みなおすってのはナシじゃないんだけれど、オーバーヘットすぎる。軽量っていうけど素のAPIにくらべりゃあ軽量じゃないんです（あたりまえ）。今はシンプルなServlet＋JSPでいいのに。
今どきのハヤリはこうなんだろうけれど、旧世代のロートルにもやさしい世界を望んでしまうのは悪なのですか神よ！

と絶望していたら、ありました。これでいいじゃん。神は居た！

https://cloud.google.com/appengine/docs/flexible/java/war-packaging?hl=ja

Jettyをパッケージングして実行時にWARファイルを指定して立ち上げちゃおうっていう荒業です。これなら既存リソースの変更最低限で環境が維持でいるっぽい。
まあ、App Engine APIは使えなくなるので、使ってたらそこの書き換えは必要だけれど（私の場合はメール関連とか）、まあこれならいけるはず。これでいこう。そうしよう。そういうことになった。

## その１．Jettyをなんとかする

まあこれはGoogleさんの提供してくれたとおりの手順です。

```cmd
.
├─src
│  └─main
│      └─java
│          └─jetty
│              └─Main.java
└─pom.xml
```
まず、起動のフックになるMainクラスをつくる。

```java:WshShell.java
package jetty;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration.ClassList;
import org.eclipse.jetty.webapp.WebAppContext;

/** WARファイルを指定してJettyを起動したりするやつ */
public class Main {
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("相対パスでWARファイル指定してちょうだい！");
      System.exit(1);
    }
    System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StrErrLog");
    System.setProperty("org.eclipse.jetty.LEVEL", "INFO");
    // Jettyサーバーオブジェクトの生成。
    // PORT環境変数が指定されてればそっち、なければ8080で起動するカンジ。
    int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8080"));
    Server server = new Server(port);
    // コンテキストパスを"/"（ルート）に設定したりするなにか。
    WebAppContext webapp = new WebAppContext();
    webapp.setContextPath("/");
    webapp.setWar(args[0]);
    ClassList classlist = ClassList.setServerDefault(server);
    // アノテーションとか使えるようにするなにか。
    classlist.addBefore(
        "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
        "org.eclipse.jetty.annotations.AnnotationConfiguration");
    // コンテキストの設定をサーバーに反映するなにか。
    server.setHandler(webapp);
    // サーバーの起動とか。
    server.start();
    server.join();
  }
}
```

んでもって、JARファイルにする。全部サンプルどおりだとあじけないので、必要なJARは固めてひとつにして、実行クラスも指定してみました。
pom.xmlはこんな感じですね。私はMaven弱者なので、怪しいところがあってもご勘弁。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>info.yamanee.gcloud</groupId>
  <artifactId>jetty-main</artifactId>
  <name>simplejettymain-j17</name>
  <version>0</version>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <jetty.version>9.4.53.v20231009</jetty.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-server</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-webapp</artifactId>
      <version>${jetty.version}</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-util</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>jetty-annotations</artifactId>
      <version>${jetty.version}</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jetty</groupId>
      <artifactId>apache-jsp</artifactId>
      <version>${jetty.version}</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.0.0</version>
        <configuration>
          <finalName>jetty</finalName>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>jetty.Main</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
```
んで、「mvn install」。
これで「jetty-jar-with-dependencies.jar」ってファイルができる筈。これで勝つる。

## その２．WARファイルつくる

これはApp Engine API使ってた部分をオミットするくらいでわざわざコード記述するまでもないんだけれど、検証用にせっかく作ったので晒しておきます。素のServletとかJSPなんて10年以上ぶりに書いた気がする。

<details><summary>warファイルの中身</summary>


```
.
└─src
    └─main
        ├─java
        │  └─test
        │      ├─TestFilter.java
        │      └─TestServlet.java
        └─webapp
            ├─WEB-INF
            │    ├─lib
            │    │  ├─taglibs-standard-impl-1.2.5.jar
            │    │  └─taglibs-standard-spec-1.2.5.jar
            │    ├─result.jsp
            │    └─weeb.xml
            └─index.jsp
```

```java:TestFilter.java
package test;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

@WebFilter("/*")
public class TestFilter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
	}
	
	@Override
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		req.setCharacterEncoding("utf-8");
		chain.doFilter(req, res);
	}
}
```

```java:TestServlet.java
package test;

import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/test")
public class TestServlet extends HttpServlet {

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		process(req, resp);
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		process(req, resp);
	}
	
	private void process(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		String message = req.getParameter("message");
		ServletContext context = getServletContext();
		if (message!= null &&  message.trim().length() != 0) {
			req.setAttribute("message", message);
			context.getRequestDispatcher("/WEB-INF/result.jsp").forward(req, resp);
		} else {
			req.setAttribute("error", "なにか入力して！");
			context.getRequestDispatcher("/index.jsp").forward(req, resp);
		}
	}
}
```

```jsp:index.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page isELIgnored="false"  %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
	<h3>メッセージを入力</h3>
	<form action="${pageContext.request.contextPath}/test" method="post">
		<input type="text" name="message" />
		<button type="submit">送信</button>
		<c:if test="${!empty error}">
		  <p style="color:red;">${fn:escapeXml(error)}</p>
		</c:if>
	</form>
</body>
</html>
```

```jsp:result.jsp
<%@ page contentType="text/html; charset=UTF-8"%>
<%@ page isELIgnored="false"  %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<h3>入力されたメッセージ：${fn:escapeXml(message)}</h3>
<a href="${pageContext.request.contextPath}/">戻る</a>
</body>
</html>
```

```xml:web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">

	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>

</web-app>
```
</details>

わたしはIDEAが良いっよてまわりに言われ続けてもEclipseを使い続けるリリース時からのEclipse狂信者（布教はしない）なので、普通にEclipseでつくってExportsして「sample.war」ってファイルに固めました。pom.xml書くのめんどい。

## その３．動作確認してみる

ここまできたら本当にこれ動くのかい？っていう動作確認です。
作成した２つのファイル、「jetty-jar-with-dependencies.jar」と「sample.war」を同じディレクトリに置いて実行。

```cmd
java -jar jetty-jar-with-dependencies.jar sample.war
```
http://localhost:8080 にアクセス。
おおちゃんとうごいた。すごい。これは勝ったんじゃないかな。

## その４．デプロイする

以前AppEngine触ったときはEclipseのプラグイン経由だったから、ここらが正直ぜんぜん知識がなかった。でももう使えないので調べるしかないのん。めんどい。

### まずは「Google Cloud CLI インストーラ」のインストール
これはリンク先のドキュメント読めば迷うことはないですね。

https://cloud.google.com/sdk/docs/install-sdk?hl=ja

途中でリージョンの指定とか聞かれるところがあるので、「asia-northeast」のどれか選ぶといいんじゃないかと思います。

### 次に「app.yaml」の作成
これがデプロイしたファイルはこう使ってね、ってAppEngineにお願いするための設定です。appengine-web.xmlは死んだので、ここに全部書きます。
どーせJava11もそのうちダメっていわれそうなので17を指定。
あと「entrypoint」のところに、さっき検証したコマンドを、こう実行しろって記述します。
ほかの部分はまあそのうち調べる。

```yaml
runtime: java17
 
env: standard

entrypoint: 'java -jar jetty-jar-with-dependencies.jar sample.war'

handlers:
- url: /.*
  script: this field is required, but ignored

manual_scaling:
  min_instances: 1
  max_instances: 1
```

### 最後にデプロイ

「jetty-jar-with-dependencies.jar」、「sample.war」、「app.yaml」の3つのファイルを同じディレクトリに置いたらあとはそこで

```cmd
gcloud app deploy
```
これでおわり。
なんか色々メッセージが流れたり、少々待たされたりするけれど、そのうちおわります。

```cmd
gcloud app deploy --version VERSION_ID --no-promote
```

とかすると、既存のものを残して別バージョンとしてデプロイして勝手に切り替わらないとかできます。ちょっと本当に切り替えて大丈夫？なんて不安になった場合はこちらなど。

これでデプロイ完了です。おつかれさまでした。

一度デプロイしたJettyのJARとかそのままでいいはずなので、コンテンツ更新の際にはWARだけデプロイすればいいんじゃないかな？

```cmd
gcloud app deploy sample.war
```



私としてはこの後、プログラマじゃないWEBサイト運用担当者にもメンテできるようにコマンドとかスクリプトとかドキュメントとか準備する作業が残ってるけれど、まあ動いてしまえばあとはどうにかなるもんです。担当者嫁だし。奴はIT屋じゃないけど幼少時はベーマガのMSXプログラム打ち込んでた強者だし。

## 実はいちばんハマったのはデプロイ権限の部分

と、ここまでなら超すっきりさっぱりだったんですが、実際のところはデプロイの部分で試行錯誤してました。
既存環境では最後のデプロイ時に「権限たりないからダメ！」をなんどもくらってたりします。この記事書くためにあたらしくプロジェクト作成して作業したらすんなり通ってしまったので困惑してます。この作業でもうすこし原因がクリアになると思ったんだけどなあ。

素のGoolgeアカウントとGoogleWorkspace経由だとなにか違うのか、はるか昔に設定した情報だとなんかまずいところがあるのか、原因はいまのところ闇の中です。

いろいろググって権限いじくりながら何度もトライしたんですが、そも「サービスアカウント」という概念がイマイチ理解できてないのが原因っぽい。
いまだ理解できていないので、こうやったらうまくいったの実例のみ記述します。

プロジェクトに紐づくユーザーの権限管理は、「IAMと管理」っていうメニューから行います。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21475/feeddd39-bc02-6736-f034-313f0bd3f11e.png)

で、いろいろ調べたところデプロイに必要な権限はこんな。

![無題2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21475/14ceaf99-6d62-3939-9ced-dacf01995af3.png)

・App Engine サービス管理者
・App Engine デプロイ担当者
・Cloud Build サービス アカウント
・Compute ストレージ管理者
・Storage オブジェクト閲覧者
・サービス アカウント ユーザー

識者の提供してくれている情報からも、これでいいハズ。なのになぜか毎回おこられる。

で、最終的にどうしたかというと、「App Engine default service account」に同様の権限を設定しました。
これ上記の画面だとリストに出てきてるんですが、私の環境だと最初は出てきてなかった。どこでアカウント名を調べたかというとこっち。

![無題3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21475/7c79062d-17bc-c8b0-a9d6-3476e4038e0c.png)

App Engine のバージョンメニューにあるこの記述です。
ここに記述されているアカウントに上記と権限を与えてやっとデプロイが上手くいきました。

正直いまだ気持ち悪いままなんですが、もしかしたら誰かの役にたつかもしれないと考えて、酷い内容だと理解しつつ、ひとつの例として情報を残します。

## 参考ページ

https://qiita.com/_masaokb/items/7bb7354d2b22cb90fdc5

https://cloud.google.com/appengine/docs/standard/java-gen2/runtime?hl=ja

https://cloud.google.com/appengine/docs/flexible/java/war-packaging?hl=ja

https://cloud.google.com/sdk/docs/install-sdk?hl=ja