AWS App RunnerのランタイムにPHPが追加されたので使ってみる

# TL;DR

- App RunnerでPHPが使えるようになったらしい
- 触った事ないので触ってみた
- どの場面で使うの?となった

# はじめに

昨今お手軽にWEBアプリを動かす環境が増えて来ました。
一昔前はどんな小さなアプリでもある程度ポチポチカタカタsshなんてしていたものです。
便利になったものだ。

便利 = 面倒な事を抽象化してユーザに意識させない
という事なんですかね？

知らなくてもできる。

便利と怖さはセットなようです。

とりあえず、
「え？まだEC2とか使ってんの？ んなもんRunner一発でエルボーっしょ」
って言いたいがためにApp Runner触ってみた話です.

# やること

- symfonyで簡単なWebAppを作る
- DockerImageにする
- App Runnerに必要な一式をcdkで作る
- 動かしてみる

# My環境

- PC: MAC Apple M1
- Node: 18.1.0
- PHP: 8.1

# App Runner is 何

わからないのでITに詳しい私の親友に連絡してみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/2c6d587c-7ded-d968-c981-584eacd24696.png" width=50%>


よしこの顔になったところで自分で調べます。最後に頼れるのは自分のみです。

[https://aws.amazon.com/jp/apprunner/](https://aws.amazon.com/jp/apprunner/)
>AWS App Runner は、コンテナ化されたウェブアプリケーションや API を開発者が簡単かつ迅速にデプロイできるフルマネージド型サービスです。大規模に、しかも事前のインフラ経験を必要とせずにデプロイすることができます。ソースコードからでも、コンテナイメージからでも始められます。


あー、なるほどなるほど。（5%程理解したの意）


まあ色々みてみると、要はポイッとコードかイメージをpushすると、エイッとビルドしてデプロイしてくれるみたいです。

ざっくり構成図にしてみるとこんな感じです

![qiita20221101.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/c466227d-b812-32a1-e44b-8cc528e230a5.png)

App Runnerの知識も十分得た所で次に進みます。


・・。

このままじゃウチの社長にエルボーくらいそうなので、もう少し、得た知識を書きます。

特徴はざっくり以下です。

## 単一のイメージをスケールさせて起動できる

App Runnerは単一のイメージをスケールさせて稼働できるようです。

Appの元ネタは、`github` or `ECR` の2択です。

githubなら指定リポジトリの特定ブランチに更新が入るとビルド+デプロイが走る感じですね。
ECRならdockerImageの更新がトリガーです。今回は後者を使います。

![Screenshot 2022-11-01 0.23.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/0a4ebb31-dd95-808e-4365-ec57048e67a3.png)


## スケーリング簡単に設定できる

同時リクエスト数、というメトリクスのみでオートスケールさせる事が可能なようです。
あくまで単一イメージなので、複数コンテナで1つのサービスを提供する形の場合は、ECSとか使わねばなりません。

![Screenshot 2022-11-01 0.26.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/51dad9c4-d720-1981-5ac8-495a1d1128c6.png)


## ネットワーク周りを勝手に作ってくれる

オートスケールを簡単にしてくれるという事は、、、そうALBやVPCもよしなに作ってくれます。

隠蔽する形で裏っかわで作ってくれてる訳ですね。VPCとALB。

コンテナがスケールした時にそちらをよしなに、えいや、してくれてる訳です。

発展途上のサービスなので、開始直後はVPCにアクセスできなかった（←これほんとに何に使うんだよ）らしいです。

いまはVPCコネクタを用意して、既存のVPC内のサービス(RDSとか)にアクセスできるようになりました。

モロち..もちろん、SGも適用できます。

## CD簡単に実現

デプロイ、という話をしましたが、コードorイメージをpushした時勝手にデプロイしてくれます。

App Runnerは実際は裏でfargateが動いてるみたいなので、結局codebuild&deployが走ってるわけですね。ALB+VPCと同じく、隠蔽(抽象化?)してくれているわけです。

## 改めて概念図

先程の図を改めます。
ヒゲのおじさんが隠してた部分はこんな感じという事です。

![qiita20221101.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/48e260d4-9ed3-d843-7a1d-d8b103f99237.png)

ではさっそくどれほどのものか、さわってみる事にします。

# symfonyで簡単なWebAppを作る

まずはディレクトリ作ります.

```bash
mkdir sample-app-runner; cd $_
symfony new app --version="6.1.*" --webapp
cd app
symfony server:start -d
open -a "google chrome" https://127.0.0.1:8000
```

![Screenshot 2022-10-31 18.01.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/10544fc6-126b-9a45-9a98-506344283074.png)

ok.　立ち上がりましたね。

テキトーなhtmlを当て込みます。

今回は ↓↓ から拝借した `tpl_089.zip` のhtmlをあてます
https://f-tpl.com/category/HTML%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88/

```bash
unzip ~/Downloads/tpl_089.zip
mv ./tpl_089/index.html {path_to_sample-app-runner}/templates/index.html.twig
mv ./tpl_089/js {path_to_sample-app-runner}/public
mv ./tpl_089/css {path_to_sample-app-runner}/public
mv ./tpl_089/images {path_to_sample-app-runner}/public
```

次にコントローラーの雛形を作ります。

```bash
symfony console make:controller IndexController

> created: src/Controller/IndexController.php
> created: templates/index/index.html.twig ← こいつは不要ので消す
```

ちょいと修正します。

```diff_php
--- a/src/Controller/IndexController.php
+++ b/src/Controller/IndexController.php
 class IndexController extends AbstractController
 {
-    #[Route('/index', name: 'app_index')]
+    #[Route('/', name: 'index')]
     public function index(): Response
     {
-        return $this->render('index/index.html.twig', [
-            'controller_name' => 'IndexController',
-        ]);
+        return $this->render('index.html.twig');
     }
 }
```

リロードすると。

![Screenshot 2022-11-01 1.11.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/297357bb-c0dd-4600-bcf8-fa504d0f208b.png)

はい、とりあえずWebApp（とは名ばかりの静的サイト）完成。

# DockerImageにする

こいつを載せるイメージ作りましょう。

```bash
cd {path_to_sample-app-runner}
mkdir docker; cd $_
```

まずは、Dockerfile。
今回はPHP8.1 + apache のイメージを使います。
何もinstallするものがないので、ファイル配置してcomposer叩いてるだけです。
リポジトリルートからビルドする前提で書いてます。

```dockerfile:docker/Dockerfile
# リポジトリルートからビルドする事!!
FROM php:8.1-apache

# confの配置
COPY ./docker/phpConf/php.ini /usr/local/etc/php/
COPY ./docker/apacheConf/000-default.conf /etc/apache2/sites-enabled/000-default.conf

# appの配置
COPY ./app /var/www/project

WORKDIR /var/www/project

# composer 持ってきて叩く
COPY --from=composer:2.3.5 /usr/bin/composer /usr/bin/composer
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_NO_INTERACTION 1
RUN composer install --no-dev --no-scripts --no-suggest --no-interaction
```

次にphpのconf。
タイムゾーンの変更だけです

```ini:docker/phpConf/php.ini
[date]
date.timezone = "Asia/Tokyo"
```

そしてapache。
symfonyのDocに記載のやつをそのまま使ってます。
逆言うとこれに合わせてファイル配置しています。

VirtualHost切ってますが、 `000-` と最初にロードされるようにする + servernameを指定しない事で、
どのdomainでアクセスしてもこのディレクティブに到達するようにしてます。

```apache:docker/apacheConf/000-default.conf
<VirtualHost *:80>
    DocumentRoot /var/www/project/public
    DirectoryIndex /index.php

    <Directory /var/www/project/public>
        AllowOverride None
        Order Allow,Deny
        Allow from All

        FallbackResource /index.php
    </Directory>

    # uncomment the following lines if you install assets as symlinks
    # or run into problems when compiling LESS/Sass/CoffeeScript assets
    # <Directory /var/www/project>
    #     Options FollowSymlinks
    # </Directory>

    # optionally disable the fallback resource for the asset directories
    # which will allow Apache to return a 404 error when files are
    # not found instead of passing the request to Symfony
    <Directory /var/www/project/public/bundles>
        DirectoryIndex disabled
        FallbackResource disabled
    </Directory>
    ErrorLog /var/log/apache2/project_error.log
    CustomLog /var/log/apache2/project_access.log combined

    # optionally set the value of the environment variables used in the application
    #SetEnv APP_ENV prod
    #SetEnv APP_SECRET <app-secret-id>
    #SetEnv DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name"
</VirtualHost>
```

## ローカルで起動する

起動してみましょう。
appと同改装にdockerディレクトリを作ったので、リポジトリのルートからDockerfileを指定してビルドします。
（docker配下にapp持ってくればそんな事しなくてよかったのですが）

```bash
cd {path_to_sample-app-runner}
docker build -t sample-app-runner -f docker/Dockerfile .
docker run --publish 8001:80 -d --name sample-app-runner-test sample-app-runner
```

8001ポートにリンクしました。ブラウザで `http://127.0.0.1:80001` を開いてみましょう。

![Screenshot 2022-11-01 2.41.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/0bd5275e-f151-e4b8-d318-97bd23c8754d.png)

okok,これでイメージは作成完了です。

今ディレクトリ構成はこんな感じ

```dir
sample-app-runner
  ├─ app (symfonyApp)
  └─ docker
      ├─ phpConf
      │    └─ php.ini
      ├─ apacheConf
      │    └─ 000-default.conf
      └─ Dockerfile
```

# App Runnerに必要な一式をcdkで作る

せっかくなのでワンポチで環境つくれるようにcdkでApp Runner立ち上げます。
今回はtypescriptを使う事にします。

```bash
cd {path_to_sample-app-runner}
mkdir cdk; cd $_
npx cdk init --language typescript
```

入りました
```
> tree -L 1                                                                       
.
├── README.md
├── bin
├── cdk.json
├── jest.config.js
├── lib
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json
```

まずはイメージをpushする先、ECRを作成します。

作るのは2つだけ。

- ECR
- App Runner 本体

です。

本当はECRのリポジトリをfetchできるRoleをApp Runner用に作成してあげなければなりませんが、
ここは、CDK。勝手に作ってくれます。

```diff_typescript
--- a/cdk/lib/cdk-stack.ts
+++ b/cdk/lib/cdk-stack.ts

-import * as cdk from 'aws-cdk-lib';
+import { Stack, StackProps } from 'aws-cdk-lib';
 import { Construct } from 'constructs';
+import * as Ecr from 'aws-cdk-lib/aws-ecr';
+import * as AppRunner from '@aws-cdk/aws-apprunner-alpha';

-export class ExampleAppRunnerStack extends cdk.Stack {
-  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
+export class ExampleAppRunnerStack extends Stack {
+  constructor(scope: Construct, id: string, props?: StackProps) {
     super(scope, id, props);

     // ECRの作成
-    const repository = new cdk.aws_ecr.Repository(this, 'ExampleAppRunnerRepository');
+    const Repository = new Ecr.Repository(this, 'ExampleAppRunnerRepository', {
+      repositoryName: 'app-runner'
+    })
+
+    // AppRunnerの作成
+    new AppRunner.Service(this, 'ExampleAppRunner', {
+      source: AppRunner.Source.fromEcr({
+        repository: Repository,
+        imageConfiguration: {
+          port: 80
+        }
+      })
+    })
   }
 }
```

初回であればbootstrap叩いて、

```bash
npx cdk bootstrap
```

さあ準備は整いました。
いざゆかん。

エンターを押す指を高らかに掲げ、
大きな声でせ〜の、

「よろしくおねがいしまーす!」 ッターン!!

```bash
npx cdk deploy
```

↓↓

![Screenshot 2022-11-01 4.42.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/3393020c-6382-3a7e-7ded-cc02718f0ee3.png)

クッそエラーでました。


ちょっと見てみると、

App Runnerが初回起動してECRからimageをpullしようとする → imgaeまだない → 待つ

みたいな感じでぐるぐるした後 CFnが根をあげて

```
Resource handler returned message: "null" (RequestToken: xxxxxxx, HandlerErrorCode: null)
```

となっている模様。


うーんなんか腑に落ちませんが、
ECR作ってイメージを用意してから、AppRunnerをデプロイしてみれば解決するように見える。

という事でスタックをわけます。

```typescript:cdk/lib/ecr-stack.ts
import {Stack, StackProps} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import * as Ecr from 'aws-cdk-lib/aws-ecr';

export class EcrStack extends Stack {
  public readonly repository: Ecr.Repository;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // ECRの作成
    this.repository = new Ecr.Repository(this, 'ExampleAppRunnerRepository', {
      repositoryName: 'app-runner'
    });
  }
}
```

```typescript:cdk/lib/apprunner-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as AppRunner from '@aws-cdk/aws-apprunner-alpha';
import * as Ecr from 'aws-cdk-lib/aws-ecr';

interface AppRunnerStackProps extends StackProps {
  repository: Ecr.Repository;
}

export class ExampleAppRunnerStack extends Stack {
  constructor(scope: Construct, id: string, props: AppRunnerStackProps) {
    super(scope, id, props);

    // AppRunnerの作成
    new AppRunner.Service(this, 'ExampleAppRunner', {
      source: AppRunner.Source.fromEcr({
        repository: props.repository,
        imageConfiguration: {
          port: 80
        }
      })
    })
  }
}
```

repositoryは専用のinterfaceを用意してコンストラクタで渡します。

ちなみにメインはこう

```typescript:cdk/bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { ExampleAppRunnerStack } from '../lib/apprunner-stack';
import {EcrStack} from "../lib/ecr-stack";

const app = new cdk.App();
const ecrStack = new EcrStack(app, 'EcrStack', {});
new ExampleAppRunnerStack(app, 'ExampleAppRunnerStack', {
    repository: ecrStack.repository
});
```

もはやスタック管理する意味が皆無な形となりました。

分けたので、ECRだけ先に作成

```bash
npx cdk deploy EcrStack
```

いざイメージをpushします。

```bash
# ECRログイン
AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

# ビルド
docker build --platform amd64 -t app-runner -f docker/Dockerfile .

# タグづけ
docker tag app-runner:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/app-runner:latest

# push
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/app-runner:latest
```

次にAppRunnerを作ります。

```bash
npx cdk deploy ExampleAppRunnerStack
```

ふむふむ、今度は行けそう。

![Screenshot 2022-11-01 5.23.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/110a7c93-bb9a-a422-17e6-6f22cd02b548.png)

いけました。

![Screenshot 2022-11-01 5.29.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/95fbc5b7-2958-0501-efca-4237b47d3f09.png)

やはり色々リソース作ってるので遅い。

さて出来上がったのでブラウザでアクセスしてみます。

![Screenshot 2022-11-01 5.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/bb9ee3a3-8e03-de6f-3959-a0cc055c4ea3.png)


![Screenshot 2022-11-01 5.30.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/9ebdb1c1-e946-15e8-4f3c-d3b2873d0a2c.png)


よし見える。


デプロイを試してみましょう。

トップの美女の画像をエガちゃんに変えます。


![Screenshot 2022-11-01 5.41.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/ffa13cb6-5feb-4b80-3773-c56ffd4e5066.png)


先程同様。ビルドし、pushします。

```bash

## 略 ##

docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/app-runner:latest
```

しばらくするとAppRunnerが `Operation in progress` に変わります。

![Screenshot 2022-11-01 6.00.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/00be647f-301a-de00-b65b-5c2501a452ad.png)

5~6分経過して、Runningに変わりました。

モロち..もちろん、その間WebAppにはアクセス可能です。

どうやら、ローリングアップデートではなく、ブルーグリーンデプロイされるのだそう。
今回はタスク1つなので関係ないですが。

![Screenshot 2022-11-01 6.10.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/988e48d5-a657-d2dd-b5e3-4cfd674ee25f.png)

そしてブラウザをリロード

![Screenshot 2022-11-01 6.10.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240041/09f8d7f9-740b-0158-4297-847329897dea.png)

いい位置に文字が来てくれました。
無事にエガちゃんが見れた所で終了です。

# さいごに

振り返ってみますが、まず料金。

コンテナ起動時間で課金され、 `0.064 USD/vCPU 時` だそうです。
また、起動最小数として確保しておくメモリにも課金され `0.007 USD/GB 時` だそうです。
あと一応ビルドも分単位で料金がかかり `0.005 USD/ビルド時間 (分)` との事。

例でいくと 80リクエストさばけるような 1 vCPU、2 GB を 通常1インスタンスで構えていたとすると、
大体 月25.50USD くらいなわけです。


はい、そして大事な所、cloudfront & waf はさせません。 （2022/11/1現在）

静的サイトはS3+cloudfrontでいいよな。。
ちょっとしたお問い合わせとかもさばきたいなら、LightsailにGitflowとかが最安値を叩き出す気がします。

今このAppRunnerがドハマリする要件って何.....?

どの場面で使うの..? となった所でおしまいです。

# 参考

https://dev.classmethod.jp/articles/re-introduction-2022-app-runner/

https://dev.classmethod.jp/articles/update-aws-app-runner-support-go/

https://memo.morelents.com/kaisou-henkan/