Amplify CLIでコンテナをデプロイする

# はじめに
コンテナを触る一環で、AmplifyのAPIにコンテナを使う方法をやってみました。
参考にしたサイトが古い情報なので、指定する項目が少し異なっていました。新しく触る方はご参考にしてください。

# 参考
https://aws.amazon.com/jp/blogs/news/amplify-cli-enables-serverless-container-deployments-using-aws-fargate-jp/


# やってみた

## 事前準備
以下を用意しました。
- Cloud9
    - t2.microでギリギリかも
- IAMユーザー
    - AdministratorAccessを付与
    - 後ほど使うので、アクセスキーを発行

まずはamplify cliをインストールして、プロジェクト用のディレクトリを作成します。
```bash
npm install -g @aws-amplify/cli

mkdir amplify-containerized && cd amplify-containerized
```

## amilify init
まずは初期設定です。作成したユーザはここで指定します。

```
$ amplify init
Note: It is recommended to run this command from the root of your app directory

#　プロジェクト名を指定します。私はデフォルトで作りました。
? Enter a name for the project (amplifycontainerized) 

Project information
| Name: amplifycontainerized
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: none
| Source Directory Path: src
| Distribution Directory Path: dist
| Build Command: npm run-script build
| Start Command: npm run-script start

# Yを入力
? Initialize the project with the above configuration? (Y/n) 

# 以下はAWS access keysを選びました。
? Select the authentication method you want to use: (Use arrow keys)
❯ AWS profile 
  AWS access keys

# 最初に作成したユーザのアクセスキー、シークレットキーを入力します。リージョンはap-northeast-1を選びました。
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
? region:  (Use arrow keys)

Deployment completed.
Deploying root stack amplifycontainerized [ ---------------------------------------- ] 0/4
        amplify-amplifycontainerized-… AWS::CloudFormation::Stack     CREATE_IN_PROGRESS             Mon May 08 2023 23:22:27…     
        DeploymentBucket               AWS::S3::Bucket                CREATE_IN_PROGRESS             Mon May 08 2023 23:22:30…     
        UnauthRole                     AWS::IAM::Role                 CREATE_IN_PROGRESS             Mon May 08 2023 23:22:30…     
        AuthRole                       AWS::IAM::Role                 CREATE_IN_PROGRESS             Mon May 08 2023 23:22:30…     

# yを入力
? Help improve Amplify CLI by sharing non sensitive configurations on failures (y/N) ‣ 
```
## amplify configure
ここで、コンテナを使う設定を行います。
```
$ amplify configure project

Project information
| Name: amplifycontainerized
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: none
| Source Directory Path: src
| Distribution Directory Path: dist
| Build Command: npm run-script build
| Start Command: npm run-script start

Advanced: Container-based deployments
| Leverage container-based deployments: No

# "Advanced: Container-based deployments"を選びます
? Which setting do you want to configure? (Use arrow keys)
❯ Project information 
  AWS Profile setting 
  Advanced: Container-based deployments 

Using default provider  awscloudformation
# yを選択
? Do you want to enable container-based deployments? (y/N) 

Browserslist: caniuse-lite is outdated. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme

Successfully made configuration changes to your project.
```

`Browserslist: caniuse-lite is outdated.`については、以下にありました。

https://dev.classmethod.jp/articles/asked-to-update-the-browserslist-when-building-react-app/#toc-2

参考にし、以下コマンドを実行。
```
$ npx update-browserslist-db@latest
```

## amplify add api

Amplifyに戻って、コンテナのAPIを追加します。
```
$ amplify add api

# "REST"を選択
? Select from one of the below mentioned services: (Use arrow keys)
❯ GraphQL 
  REST 

# "API Gateway + AWS Fargate (Container-based) "を選択
? Which service would you like to use (Use arrow keys)
❯ API Gateway + Lambda 
  API Gateway + AWS Fargate (Container-based) 

# プロジェクトのラベルを指定。デフォルトの値を指定しました。
? Provide a friendly name for your resource to be used as a label for this category in the project: (container8f709be3) 

# "ExpressJS - REST template"を選択
? What image would you like to use (Use arrow keys)
❯ ExpressJS - REST template 
  Docker Compose - ExpressJS + Flask template 
  Custom (bring your own Dockerfile or docker-compose.yml) 
  Learn More 

# "On every "amplify push" (Fully managed container source)"を選択
? When do you want to build & deploy the Fargate task (Use arrow keys)
❯ On every "amplify push" (Fully managed container source) 
  Advanced: Self-managed (Learn more: docs.amplify.aws/cli/usage/containers) 

# "n"を入力
? Do you want to restrict API access (Y/n) 
✅ Successfully added resource container8f709be3 locally.

✅ Next steps:
- Place your Dockerfile, docker-compose.yml and any related container source files in "amplify/backend/api/container8f709be3/src"
- Amplify CLI infers many configuration settings from the "docker-compose.yaml" file. Learn more: docs.amplify.aws/cli/usage/containers
- To access AWS resources outside of this Amplify app, edit the /home/ec2-user/environment/amplify-containerized/amplify/backend/api/container8f709be3/custom-policies.json
- Run "amplify push" to build and deploy your image
✅ Successfully added resource container8f709be3 locally

✅ Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
```
Dockerfileの場所は結構深いです。
`/amplify-containerized/amplify/backend/api/[プロジェクトのラベル]/src/Dockerfile`

## amplify push
公式の説明そのまま修正します。
```js:index.js
const express = require("express");
const bodyParser = require('body-parser');
const port = process.env.PORT || 3001;

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// すべてのメソッドに対しCORSを有効にする
app.use(function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*")
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
    next()
});

app.get("/", async (req, res, next) => {

    try {
        res.contentType("application/json").send({ 
            "randomNumber": Math.floor(Math.random() * 101) 
        })
    } catch (err) {
        next(err);
    }
});

app.listen(port, () => {
    console.log('Example app listening at http://localhost:' + port);
});
```

（変更不要）Dockerfilの中身は以下でした。
```dockerfile:Dockerfile
FROM public.ecr.aws/bitnami/node:14-prod-debian-10

ENV PORT=8080
EXPOSE 8080

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install
COPY . .

CMD [ "node", "index.js" ]
```
ではPUSHしてデプロイします。
```
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

    Current Environment: dev
    
┌──────────┬───────────────────┬───────────┬───────────────────┐
│ Category │ Resource name     │ Operation │ Provider plugin   │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ container8f709be3 │ Create    │ awscloudformation │
└──────────┴───────────────────┴───────────┴───────────────────┘

# "Y"を入力
? Are you sure you want to continue? (Y/n) ‣ 

(中略)（10分ほどかかります）

Deployment state saved successfully.

REST API endpoint: https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com

Browserslist: caniuse-lite is outdated. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
```

作成されたエンドポイントにCURLコマンドでアクセスしてみます。
```
$ curl https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com
{"randomNumber":100}
$ curl https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com
{"randomNumber":48}
$ curl https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com
{"randomNumber":16}
```


## APIを追加する
公式をなぞり、別のAPIを追加します。
```
$ amplify add api

# RESTを選択
? Select from one of the below mentioned services: (Use arrow keys)
❯ GraphQL 
  REST 

# API Gateway + AWS Fargate (Container-based) を選択
? Which service would you like to use (Use arrow keys)
❯ API Gateway + Lambda 
  API Gateway + AWS Fargate (Container-based) 

# プロジェクト名。デフォルトで
? Provide a friendly name for your resource to be used as a label for this category in the project: (containerbcfeafbd) 

# "Docker Compose - ExpressJS + Flask template"を選択。
? What image would you like to use (Use arrow keys)
❯ ExpressJS - REST template 
  Docker Compose - ExpressJS + Flask template 
  Custom (bring your own Dockerfile or docker-compose.yml) 
  Learn More 

# "On every "amplify push" (Fully managed container source) "を選択
? When do you want to build & deploy the Fargate task (Use arrow keys)
❯ On every "amplify push" (Fully managed container source) 
  Advanced: Self-managed (Learn more: docs.amplify.aws/cli/usage/containers) 

# nを選択
? Do you want to restrict API access (Y/n) 

# expressを選択
? Select which container is the entrypoint (Use arrow keys)
❯ express 
  python 
```
ディレクトリがもう一個作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/1f275b4f-425b-5c3f-d938-7ec04bee5697.png)

公式をなぞり、ファイルを修正します。
```python:src/python/src/server.py
from flask import Flask
from random import randrange

server = Flask(__name__)

@server.route('/random')
def hello():
    return str(randrange(100))

if __name__ == "__main__":
   server.run(host='0.0.0.0')

```
公式通り、Javascriptを何回か修正します。最終的に以下になりました。
```javascript:src/express/index.js
const express = require("express");
const bodyParser = require('body-parser');
const http = require('http');
const port = process.env.PORT || 3000;

const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// すべてのメソッドに対し CORS を有効にする
app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*")
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
  next()
});

app.get("/fizzbuzz", (req, res, next) => {
    // ここでレスポンスの処理を追加します
const options = {
    port: 5000,
    host: 'localhost', // ローカル開発の場合は 'python' に置き換えます
    method: 'GET',
    path: '/random'
  };

  http.get(options, data => {
    var body = '';
    data.on('data', (chunk) => {
      body += chunk;
    });
    data.on('end', () =>{
      console.log(body);
      const randomNumber = body
      let fizzOrBuzz = ''
      // FizzBuzz ロジックコードをここに追加
      if (randomNumber % 15 === 0) {
        fizzOrBuzz = 'FizzBuzz'
      }
      else if (randomNumber % 3 === 0) {
        fizzOrBuzz = 'Fizz'
      }
      else if (randomNumber % 5 === 0) {
        fizzOrBuzz = 'Buzz'
      }
      else {
        fizzOrBuzz = randomNumber
      }
      try {
        res.contentType("application/json").send({
         "newRandomNumber": body,
         "fizzOrBuzz": fizzOrBuzz
        });
      } catch (err){
        console.log(err);
        next(err);
      }
    }).on('error', (error) => {
      console.log(error);
    });
  })
});

app.listen(port, () => {
  console.log('Example app listening at http://localhost:' + port);
});
```
PUSHして追加します。既存のAPIに変更がないことを確認して進めます。
```
$ amplify push

✔ Successfully pulled backend environment dev from the cloud.

    Current Environment: dev
    
┌──────────┬───────────────────┬───────────┬───────────────────┐
│ Category │ Resource name     │ Operation │ Provider plugin   │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ containerbcfeafbd │ Create    │ awscloudformation │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ container8f709be3 │ No Change │ awscloudformation │
└──────────┴───────────────────┴───────────┴───────────────────┘
# Yを入力
? Are you sure you want to continue? (Y/n) ‣ 

(中略)

REST API endpoint: https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com

Browserslist: caniuse-lite is outdated. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme

```
先ほどと同様に、CURLコマンドでエンドポイントを叩き、動くことを確認します。passに`fizzbuzz`を入れることを忘れずに。
```
$ https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com/fizzbuzz
{"newRandomNumber":"87","fizzOrBuzz":"Fizz"}
$ https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com/fizzbuzz
{"newRandomNumber":"74","fizzOrBuzz":"74"}
$ https://[ランダム文字列].execute-api.ap-northeast-1.amazonaws.com/fizzbuzz
{"newRandomNumber":"15","fizzOrBuzz":"FizzBuzz"}

```

# 削除
フォルダごと消しますので、必要であればバックアップを取っておいてください。
## 片方削除
```
$ amplify remove api

# 先に作成した、上のものを削除
? Choose the resource you would want to remove (Use arrow keys)
❯ container8f709be3  
  containerbcfeafbd  

# Yを選択
? Are you sure you want to delete the resource? This action deletes all files related to this resource from the backend directory. (Y/n) 

✅ Successfully removed resource

# 削除した状態をPUSH
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

    Current Environment: dev
    
┌──────────┬───────────────────┬───────────┬───────────────────┐
│ Category │ Resource name     │ Operation │ Provider plugin   │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ container8f709be3 │ Delete    │ awscloudformation │
├──────────┼───────────────────┼───────────┼───────────────────┤
│ Api      │ containerbcfeafbd │ No Change │ awscloudformation │
└──────────┴───────────────────┴───────────┴───────────────────┘
? Are you sure you want to continue? (Y/n) ‣ 

```

## 全部削除

```
$ amplify delete
? Are you sure you want to continue? This CANNOT be undone. (This will delete all the environments of the project from the cloud and wipe out all the local files created by Amplify CLI) (y/N) ‣ 

```

なおECRは残っているので、コンソールから削除しておきます。
また、最初に作ったIAMユーザも不要であれば削除します。

# おわりに
今回はバックエンドをコンテナにするAmplify CLIをやってみました。
apiを追加する際のCloudFormationテンプレートを見てみると、かなり多くのリソースが作られていますが、それらを意識することなく構築できたことに驚きでした。
この記事がどなたかのお役に立てれば幸いです。