Amazon Copilot を使ってシステム移行（アカウントまたぎ RDS → Aurora の移行を含む）

# 承前

ほぼ同じタイトルで、以前 [やってみた記事](https://qiita.com/sixpetals/items/608f0ae6877747e52aa6) を書きました。同じようなDocker Compose for Amazon ECS を使っていた別のアプリも移し替えたのでそちらのメモを残しておきます。前回はAPIでしたが今回はアプリケーションです。前回とはスタックが少し違います。

* アプリケーション：Flask
* Webサーバ：Nignx
* データストア：PostgreSQL

この記事はその1で振れていなかった

* Aurora
* セキュアな変数
* Pipline
* 環境による切り替え

などについて触れていきたいと思います。前回の記事ではいろいろ準備のことも書いてありますが、今回はそのへんはすっとばしています。

copilot のバージョンだけ記載しておきます。

```
$ copilot -v
copilot version: v1.33.2
```

# app 作成

app を作成します。

```shell
copilot app init hawk-api --domain 'ai-hawk.com'
```

# env 作成

env を作成します。今回は dev と prod を両方いっぺんに作っておきました。VPCも作ってもらいたいので質問にはデフォルトとして進めます。

```shell
copilot env init --app hawk-basic --name dev --profile zenk_dev
copilot env deploy --name dev
```
```shell
copilot env init --app hawk-basic --name prod --profile zenk_prd
copilot env deploy --name prod
```

# svc 作成

webapp というディレクトリ以下に flask のアプリケーションがあり Dockerfile がここにあるのでこうします。

```
copilot svc init --app hawk-basic --name webapp --dockerfile ./webapp/Dockerfile --svc-type "Load Balanced Web Service"
```

ここで作成されたmanifestは後で書き換えるので後述。


# storage 作成

前回の記事では DyanmoDB でしたが、今回はもともと RDS（PostgreSQL） だったので Aurora に乗り換えます（乗り換えなのでデータ移行が必要になるのですがこれは余談なので後述）

-w でワークロード（svc でつけた名前のことです）を指定、　-l は lifecycle で何を削除したときに一緒に削除されてほしいかを記述します。ここではアプリケーションの削除と連動してほしいので workload と指定します。

```
copilot storage init -n hawk-basic-dbcluster -t Aurora -w webapp -l workload --engine PostgreSQL --initial-db hawk_basic_db 
```
作るとこんな結果が来ます。

```
const {username, host, dbname, password, port} = JSON.parse(process.env.HAWKBASICDBCLUSTER_SECRET)
```

このDBへのアクセス情報は HAWKBASICDBCLUSTER_SECRET という名前の環境変数から JSON で取得できるから、それを展開しましょうということが書いてあります。この名前は使いにくいので変更します。

`copilot/webapp/addons`というフォルダの下に hawk-basic-dbcluster.yml というファイルが出来ています。

これはCloudFormationのファイルなのですが、ここの

```diff
Outputs:
-  hawkbasicdbclusterSecret: # injected as HAWKBASICDBCLUSTER_SECRET environment variable by Copilot.
+  databaseSecret: # injected as DATABASE_SECRET environment variable by Copilot.
    Description: "The JSON secret that holds the database username and password. Fields are 'host', 'port', 'dbname', 'username', 'password', 'dbClusterIdentifier' and 'engine'"
    Value: !Ref hawkbasicdbclusterAuroraSecret
  hawkbasicdbclusterSecurityGroup:
    Description: "The security group to attach to the workload."
    Value: !Ref hawkbasicdbclusterSecurityGroup
```

という箇所の hawkbasicdbclusterSecret を databaseSecret に変更します（混乱するのでコメント部分も書き換えておきます）
これで、DATABASE_SECRET という環境変数が自動的に作成されるはずです。

さて、さすがにコードの修正が必要になります。

もともと

```python
    SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{user}:{password}@{host}/{name}'.format(**{
        'user': os.environ['DB_USER'],
        'password': os.environ['DB_PASSWORD'],
        'host': os.environ['DB_HOST'],
        'name': os.environ['DB_NAME']
    })
```

こうなっていたので

```python
    db_secret = env.json('DATABASE_SECRET', default=None)
    if db_secret is not None:
        SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{user}:{password}@{host}:{port}/{name}'.format(**{
            'user': db_secret['username'],
            'password': db_secret['password'],
            'host': db_secret['host'],
            'name': db_secret['dbname'],
            'port': db_secret['port']
        })
    else:
        SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{user}:{password}@{host}/{name}'.format(**{
        'user': os.environ['DB_USER'],
        'password': os.environ['DB_PASSWORD'],
        'host': os.environ['DB_HOST'],
        'name': os.environ['DB_NAME']
    })
```

こう書き換えました。ローカルで動かすときや別環境に持っていくときは今までの環境変数が使えるように元の記述も残した形での書き換えです。

さて、この CloudFromation Stack の設定ファイルにはもう一工夫必要です。今回は、 **もともとRDSで動いていたアプリケーションをCopilotでデプロイできるようにする必要があります** 。Copilot では RDS はなく Aurora になりますが、普通に作ってしまうと当然ですがデータが空っぽの DB ができてしまいます。かといってスナップショットから主導で作ってしまうと Copilot のエコシステムから外れてしまいます。

ということで、strage init で作成された addons 内の CloudFormation Stack のマニフェストファイルを書き換えて、**初期 Aurora データベースを予め用意したスナップショットから作る必要** があります。ここでは自分のアカウント内のスナップショットがすでに作成されているものとします。

今回RDSが13.13で作成されていたので、Auroraの方を下げました。バージョンが一致していれば特に変える必要は無いかと思いますし、元々のデータベース側で一致させてから移行しても良いと思います。

```diff
Mappings:
  (中略)
+  hawkbasicdbclusterSnapshotsMap: 
+    dev:
+      ARN: 'arn:aws:rds:ap-northeast-1:*******:snapshot:hawk-basic-dev-old-snapshot'
+    prod:
+      ARN: 'arn:aws:rds:ap-northeast-1:*******:snapshot:hawk-basic-prod-old-snapshot'
```

```diff
Resources:
  (中略）
  hawkbasicdbclusterDBClusterParameterGroup:
    Metadata:
      'aws:copilot:description': 'A DB parameter group for engine configuration values'
    Type: 'AWS::RDS::DBClusterParameterGroup'
    Properties:
      Description: !Ref 'AWS::StackName'
-      Family: 'aurora-postgresql14'
+      Family: 'aurora-postgresql13'      
      Parameters:
        client_encoding: 'UTF8'
  hawkbasicdbclusterDBCluster:
    Metadata:
      'aws:copilot:description': 'The hawkbasicdbcluster Aurora Serverless v2 database cluster'
    Type: 'AWS::RDS::DBCluster'
    Properties:
      MasterUsername:
        !Join [ "",  [ '{{resolve:secretsmanager:', !Ref hawkbasicdbclusterAuroraSecret, ":SecretString:username}}" ]]
      MasterUserPassword:
        !Join [ "",  [ '{{resolve:secretsmanager:', !Ref hawkbasicdbclusterAuroraSecret, ":SecretString:password}}" ]]
      DatabaseName: !Ref hawkbasicdbclusterDBName
      Engine: 'aurora-postgresql'
-      EngineVersion: '14.4'
+      EngineVersion: '13.13'      
      DBClusterParameterGroupName: !Ref hawkbasicdbclusterDBClusterParameterGroup
      DBSubnetGroupName: !Ref hawkbasicdbclusterDBSubnetGroup
      Port: 5432
      VpcSecurityGroupIds:
        - !Ref hawkbasicdbclusterDBClusterSecurityGroup
      ServerlessV2ScalingConfiguration:
        # Replace "All" below with "!Ref Env" to set different autoscaling limits per environment.
        MinCapacity: !FindInMap [hawkbasicdbclusterEnvScalingConfigurationMap, All, DBMinCapacity]
        MaxCapacity: !FindInMap [hawkbasicdbclusterEnvScalingConfigurationMap, All, DBMaxCapacity]
+      SnapshotIdentifier: !FindInMap [hawkbasicdbclusterSnapshotsMap, !Ref Env, ARN]
```

修正した箇所は


# secret 作成

```
copilot secret init --name hawk_api_token
```

各 Environment ごとに聞かれるので入力します。

```shell
Dev secret value: 
Prod secret value: 
...Put secret hawk_api_token to environment dev and prod
✔ Successfully put secret hawk_api_token in environment dev as /copilot/hawk-basic/dev/secrets/hawk_api_token.
✔ Successfully put secret hawk_api_token in environment prod as /copilot/hawk-basic/prod/secrets/hawk_api_token.
You can refer to these secrets from your manifest file by editing the `secrets` section.
```
ほかにもあれば、都度作ります。

# svc デプロイ

いくつかエラーがあり回避するのがなかなか大変でした。

Copilotはエラーを見るのが大変です。なおAuroraの作成にかなり時間がかかっているようです。タスクはこのAuroraの作成の後にしか行われないので、タスクのエラーを見たい場合はこのAuroraの作成を待つ必要があります。10分以上かかりました。


## ProstgreSQL バージョン問題

もともとのRDSで使っていたPostgreSQLは13.13というバージョンでしたが、Copilotがデフォルトで作成するバージョンが14.3でした。これが違っているせいでデプロイに失敗してました。

## dbname 見付からない問題

なんとdbnameがありません……！

```json
{
    "dbClusterIdentifier": "hawk-basic-dev-webapp-add-hawkbasicdbclusterdbclus-******",
    "password": "******",
    "engine": "postgres",
    "port": 5432,
    "host": "hawk-basic-dev-webapp-add-hawkbasicdbclusterdbclus-******.cluster-cigkhswdpwb7.ap-northeast-1.rds.amazonaws.com",
    "username": "postgres"
}
```

解決しなかったので、dbnameはプログラム上で解決することにしました。

自分でゼロから作らないと取得できないんですね。