Rubyのサーバレスフレームワーク「Ruby on Jets」を使ってAWSへアプリケーションをデプロイする

## はじめに

[Ruby on Jets](https://rubyonjets.com/#page-top)とはRubyでサーバレスアプリケーションを構築するためのフレームワークです。

<img width="1440" alt="Ruby_on_Jets___The_Ruby_Serverless_Framework.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/f225a2d3-c6e3-59f6-7224-2e542bb05377.png">

CLIを利用して簡単に、AWS上へサーバレスなアプリケーションを構築できます。
CLIやディレクトリ構成や設定ファイルの記述方法など、Ruby on Railsと共通する点も多いので
これまでRailsでの開発経験があれば学習コストも低く済みそうです。

<img width="985" alt="Ruby_on_Jets___The_Ruby_Serverless_Framework.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/606cd724-a8ce-b6d6-1f2d-d1e838f95acc.png">

今回、サンプルアプリケーションを作成して、デプロイするところまで試してみたので手順をまとめます。
尚、諸々の事情により、フルサーバレスではなく、RDS(PostgreSQL)も存在していますのでご留意ください。

## サンプルアプリケーションを作成

Jetsの [Quick Start](https://rubyonjets.com/quick-start/) を参考にサンプルアプリケーションを作成しました。

RDS(PostgreSQL)を使うので `--database=postgresql` を追加しています。
オプションの指定方法がRailsと同じなのでわかりやすいですね。

```bash
$ jets new jets_rds_example --database=postgresql
$ cd jets_rds_example
$ bundle exec jets generate scaffold post title:string body:text
```

環境変数にDBの情報を設定します。

```.env.development
DB_HOST=(DBのホスト名)
DB_USER=(DBのユーザ名)
DB_PASS=(DBのパスワード)
```

DBの生成、マイグレーションを実行し、ローカルでサーバ起動します。

```bash
$ bundle exec jets db:create db:migrate
$ bundle exec jets server
```

http://localhost:8888/ へアクセスすると以下のような画面が表示されました。

<img width="1439" alt="Ruby_on_Jets.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/ff80bc1a-6054-2398-5b2d-1b189ad37c41.png">

http://localhost:8888/posts へアクセスしてscaffoldで作成したPostsの一覧画面も閲覧できます。

<img width="642" alt="Jets_App.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/7913e885-10c9-b789-6719-690033be9240.png">

## デプロイする

### デプロイ用のIAMユーザを作成

全権限を付与したIAMユーザを用意するのがてっとり早いのですが、当然セキュリティの観点で問題があるので、デプロイに必要な権限のみを付与したIAMユーザを作成します。

[Minimal Deploy IAM Policy](https://rubyonjets.com/docs/extras/minimal-deploy-iam/) に手順が公開されているので参考にします。

※以降の手順は、IAMグループ・ユーザの作成権限を持つアクセスキーをaws-cliへ設定していることを前提とします。

```bash
# IAMグループを作成
$ aws iam create-group --group-name (任意のグループ名)

# IAMグループのポリシーをtmpファイルへ記述
$ cat << 'EOF' > /tmp/jets-iam-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "apigateway:*",
                "cloudformation:*",
                "dynamodb:*",
                "events:*",
                "iam:*",
                "lambda:*",
                "logs:*",
                "route53:*",
                "s3:*"
             ],
            "Resource": [
                "*"
            ]
        }
    ]
}
EOF

# IAMグループへポリシーを適用
$ aws iam put-group-policy --group-name Jets --policy-name JetsPolicy --policy-document file:///tmp/jets-iam-policy.json

# IAMユーザを作成
$ aws iam create-user --user-name (任意のユーザ名)

# IAMユーザをグループに追加
$ aws iam add-user-to-group --user-name (任意のユーザ名) --group-name (任意のグループ名)
```

## aws-cliへprofile追加

IAMユーザを作成したらアクセスキーを発行して、aws-cliへprofile追加します。

```
# IAMユーザのアクセスキーを発行する(アクセスキーが出力されるので控えておくこと)
$ aws iam create-access-key --user-name (任意のユーザ名)
{
    "AccessKey": {
        "UserName": "(任意のユーザ名)",
        "AccessKeyId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "Status": "Active",
        "SecretAccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "CreateDate": "2019-09-23T21:04:07Z"
    }
}

# aws-cliへprofile追加
$ aws configure --profile (任意のプロファイル名)
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

### リモートの環境変数設定

デプロイ先環境(リモート)の環境変数を定義します。`.env.development` と同様にDBの情報を設定します。

```.env.remote.development
DB_HOST=(DBのホスト名)
DB_USER=(DBのユーザ名)
DB_PASS=(DBのパスワード)
```

### デプロイする

ここまでの準備が終わったらデプロイします。
たった1行のコマンドでCloudFormationを利用して簡単にデプロイ処理が走ります。

```
$ AWS_PROFILE=(任意のプロファイル名) bundle exec jets deploy
```

このとき、以下のようなエラーが表示される場合、手元の環境のRubyのバージョンがLambdaで使われているバージョン(2.5.x)と異なっています。

```
Deploying to Lambda jets-rds-example-dev environment...
Building CloudFormation templates.
Deploying CloudFormation stack with jets app!
06:09:34AM CREATE_IN_PROGRESS AWS::CloudFormation::Stack jets-rds-example-dev User Initiated
06:09:38AM CREATE_IN_PROGRESS AWS::S3::Bucket S3Bucket 
06:09:39AM CREATE_IN_PROGRESS AWS::S3::Bucket S3Bucket Resource creation Initiated
06:10:00AM CREATE_COMPLETE AWS::S3::Bucket S3Bucket 
06:10:02AM CREATE_COMPLETE AWS::CloudFormation::Stack jets-rds-example-dev 
Stack success status: CREATE_COMPLETE
Time took for stack deployment: 30s.
You are using Ruby version 2.6.4 which is not supported by Jets.
Jets uses Ruby 2.5.3.  You should use a variant of Ruby 2.5.x
```

Rubyのバージョンを切り替えて再度、`gem install jets` することでエラーを回避できます。
`rbenv` を利用している場合は以下のようにバージョンを切り替えてみてください。

```bash
$ rbenv install 2.5.4
$ rbenv global 2.5.4
$ bundle install
```

デプロイに成功すると以下のようなログが出力されます。

```
Stack success status: UPDATE_COMPLETE
Time took for stack deployment: 2m 26s.
Prewarming application.
API Gateway Endpoint: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/
```

## 動作確認

デプロイ時に出力されたURL [https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/](https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/) へアクセスしてみましょう。
local環境で動作確認したときと同様に、Jetsのロゴが表示されます。

![Ruby_on_Jets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/5f3a05a4-fff1-78c6-d667-49d61e9ff2ef.png)


次にscaffoldで作ったCRUDのURL [https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts](https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/posts) へアクセスしてみます。
local環境で動作確認したときと同様に、CRUDの画面が表示されました

![Jets_App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/8e20f60d-9a1b-509f-7dcb-83aac4f18a2f.png)

## 今後調べたいこと

とりあえず動かすことはできたものの、以下のような気になる点が残っているので追って調査したいと思います。

- Lambdaがスケールアウトした場合、DBのコネクションが枯渇することはないか？
- Lambda on VPCすることで、RDSのインスタンスをprivate subnetへ移せるか？
- Cognitoと連携してユーザ認証をアプリケーションを構築できるか？
- Railsで使えたgemをどこまで使えるか？

## まとめ

Jetsを使うことでRuby x AWSでサーバレスアプリケーションを作れました。
(ここまでRubyのソースコードを1行も書いてませんね)

今回はHTMLをレンダリングしていますが、JSONを返すAPIサーバの構築としても活用できそうですね。