AWS Amplify + Ionic(Angular) で、AWSにデータを簡単に保存・取得する

# 概要
最近はブラウザ上で動作するWebアプリ（フロントエンド）だけでも色々できます。
特に、ローカルストレージは便利で、必要なデータはローカルストレージに保存しておけば、
同じブラウザで同じWebアプリを開く限りは、データが引き継がれます。


ただ、時々、ローカルストレージの内容を、別の端末（別のブラウザ）と共有したいなぁと思うことがあります。
そんな時、
「バックエンドは、単にデータの保存だけしてくれれば十分なので、超簡単に作れないかなー」
と思う人もいるのではないでしょうか。

ということで、
Ionic と　AWS Amplify を使って、簡単にAWS上にデータをアップロード/ダウンロードするサンプルを作りました。

# 考え方
* Webアプリケーションで使用しているデータを、JSON形式の文字列にしてAWS上に保存します。
AWSとの連携部分は、AWS Amplifyを使用して作成します。
* 具体的には、DynamoDB＋AppSync＋S3の構成となりますが、すべてAWS Amplifyが自動で生成してくれるので、こちらとしては意識する必要はありません。。（今回はCognitoでの認証などは省略）

# 開発環境
* Ionic 6.15.0
* Angular 13.0.0
* AWS Amplify 7.6.9


# 参考ページ
* https://docs.amplify.aws/start/getting-started/setup/q/integration/ionic/
* https://qiita.com/t_okkan/items/38aca98993bf06598af6
* https://docs.amplify.aws/start/getting-started/installation/q/integration/ionic/#option-2-follow-the-instructions

# 手順
以下、順を追ってサンプルの作成手順をまとめます。

## 1.準備
AWS Amplify をインストールしていない場合は、下記にてインストールしてください。

インストール

```console
sudo npm install -g @aws-amplify/cli
```



## 2.ブランクプロジェクトを作る
下記を実行して、空のプロジェクトを作成します。

```console
ionic start AWSStorageTest blank --type=angular 
```

次に、下記を実行して、 aws-amplify と　@aws-amplify/ui-angular　をインストールします。

```
npm install aws-amplify @aws-amplify/ui-angular@1.x.x
```

そして、下記を実行して、AWSStorageServiceという名前で空のサービスを生成します。

```console
 ionic g service AWSStorageService
```

この段階で、一度ビルドして確認してみます。

```
ionic serve
```

下記のようにブランクページが表示されたら、まずは成功です。

![スクリーンショット 2022-02-03 7.55.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/d16b944f-2a46-2dc0-fc70-35e0cd9be74c.png)


## 3.AWS Amplifyの設定
続いて、AWS Amplifyの設定をしていきます。下記コマンドを実行して、amplifyの設定を行います。


```console:amplifyの設定
amplify configure
```

まずは、下記が表示されます。

```
Sign in to your AWS administrator account:
https://console.aws.amazon.com/
Press Enter to continue
```

ブラウザが起動しますので、AWSにログインします。


続いて、コンソール上での作業を進めます。
ここで実施する内容は下記となります。

1. リージョンを選択する　（特に希望がなければ、東京リージョン（ap-northeast-1)を選択）
2. IAMユーザーを作成する（ユーザー名はデフォルトで良い）


```
Specify the AWS Region
? region:  ap-northeast-1
Specify the username of the new IAM user:
? user name:  amplify-＊＊＊＊＊
Complete the user creation using the AWS console
https://console.aws.amazon.com/iam/home?region=ap-northeast-1#/users$new?step=final&accessKey&userNames=amplify-****&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess-Amplify
Press Enter to continue

```

再び、ブラウザが開いて、ブラウザ上での作業となります。
一通りブラウザで作業が完了した後は、再度、コンソール画面の内容に回答していきます。
accessKeyIdとsecretAccessKeyは、先ほどのブラウザ上に表示されたIAMユーザーのものをコピー＆ペーストとします。


```

Enter the access key of the newly created user:
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
This would update/create the AWS Profile in your local machine
? Profile Name:  default

Successfully set up the new user.
```
以上で、amplifyの設定は完了です。

## 4.AWS Amplifyの初期化

続いて、プロジェクトのAmplify の初期化を行います。
コンソールにて、下記を実行します。

```
amplify init
```

すると次のように聞かれますので、順に回答していきます。

```
? Enter a name for the project AWSStorageTest
The following configuration will be applied:

Project information
| Name: AWSStorageTest
| Environment: dev
| Default editor: Visual Studio Code
| App type: javascript
| Javascript framework: angular
| Source Directory Path: src
| Distribution Directory Path: www
| Build Command: npm run-script build
| Start Command: ng serve

? Initialize the project with the above configuration? Yes
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html

? Please choose the profile you want to use default
```

CloudFormationが実行され、AWS上に必要なリソースが自動的に生成されていきます。

```
Adding backend environment dev to AWS Amplify app: ************
⠸ Initializing project in the cloud...

CREATE_IN_PROGRESS amplify-awsstoragetest-dev-****** AWS::CloudFormation::Stack Sat Feb 05 2022 07:28:41 GMT+0900 (日本標準時) User Initiated
CREATE_IN_PROGRESS UnauthRole                       AWS::IAM::Role             Sat Feb 05 2022 07:28:45 GMT+0900 (日本標準時)               
CREATE_IN_PROGRESS AuthRole                         AWS::IAM::Role             Sat Feb 05 2022 07:28:45 GMT+0900 (日本標準時)               
CREATE_IN_PROGRESS DeploymentBucket                 AWS::S3::Bucket            Sat Feb 05 2022 07:28:45 GMT+0900 (日本標準時)               
⠹ Initializing project in the cloud...

CREATE_IN_PROGRESS UnauthRole       AWS::IAM::Role  Sat Feb 05 2022 07:28:47 GMT+0900 (日本標準時) Resource creation Initiated
CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Sat Feb 05 2022 07:28:47 GMT+0900 (日本標準時) Resource creation Initiated
CREATE_IN_PROGRESS AuthRole         AWS::IAM::Role  Sat Feb 05 2022 07:28:47 GMT+0900 (日本標準時) Resource creation Initiated
⠙ Initializing project in the cloud...

CREATE_COMPLETE UnauthRole       AWS::IAM::Role  Sat Feb 05 2022 07:29:08 GMT+0900 (日本標準時) 
CREATE_COMPLETE DeploymentBucket AWS::S3::Bucket Sat Feb 05 2022 07:29:09 GMT+0900 (日本標準時) 
CREATE_COMPLETE AuthRole         AWS::IAM::Role  Sat Feb 05 2022 07:29:09 GMT+0900 (日本標準時) 
✔ Successfully created initial AWS cloud resources for deployments.
✔ Initialized provider successfully.
Initialized your environment successfully.
```

これでAWS上のリソースが生成されました。
実行結果を見ると、このinitの段階では、IAMロールとS3だけが生成されたようです。

最後に、プロジェクトの生成が成功したことを示す表示が出ます。
また、次のステップとして実行すべきコマンドについての簡単な説明が出てきます。

```
Your project has been successfully initialized and connected to the cloud!

Some next steps:
"amplify status" will show you what you've added already and if it's locally configured or deployed
"amplify add <category>" will allow you to add features like user login or a backend API
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify console" to open the Amplify Console and view your project status
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

Pro tip:
Try "amplify add api" to create a backend API and then "amplify push" to deploy everything

```

さて、上記を実行したことにより、プロジェクト内に下記の変更が行われています。

* プロジェクトのトップフォルダに、```amplify```というフォルダが生成され、amplify関連のファイルが追加されています。amplify関係の変更を行うと、このフォルダの下のファイルが変更されます。
* srcフォルダ内に、```aws-exports.js```　という名前のファイルが追加されています。 APIキーなど、amplify 関連のサービスを利用するための設定が記載されます。自分のソースコードからamplify関連の情報を取得する際には、このファイルを使用します。このファイルはgit等にはコミットせず、ローカルで保存しておきます。
* ```.gitignore```が変更されています。

実際に変更・追加されたソースコードは下記となります。
(aws-export.jsは .gitignoreに登録されてるため、ここには表示されてません。）

![スクリーンショット 2022-02-05 7.45.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/2149aece-6ef9-291e-7416-aee4ff4eeca2.png)

## 5.AWSのリソースを利用するためのAPIを作成する
AWSのリソースにアクセスするためのAPIを作成します。
作成するWEBアプリからAWS上のリソースを利用する場合、このAPIを使用します。

今回は、GraphQLのAPIを作成します。
下記を実行します。

```console:実行コマンド
amplify add api
```
下記の通り、色々と聞かれますが、基本、デフォルトでOKです。

```console:設定内容
? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
```

続いて、作成されたGraphQLのスキーマーファイルを編集します。
```/amplify/backend/api/awsstoragetest/schema.graphql```　を開きます。


初期に作成されたファイルは下記の様になっています。

```graphql:初期状態
type Todo @model {
  id: ID!
  name: String!
  description: String
}
```

以下の様に変更します。

```graphql:変更後
type Storage @model {
  id: ID!
  data: String!
}
```
今回は最小限の機能とするため、idとdataのみとしました。

idは、Createすると自動的に割り付けられますので、
データを新規にCreateした際に割り付けられたidをWebアプリ側で取得して保持します。
データの更新および取得時には、このidを使用します。
dataの部分には、Webアプリで使用するデータをJSON形式にしたものを入れて使用します。


スキーマーを修正した後、下記を実行します。

```
amplify push
```

いくつか聞かれますので、下記のように回答します。（基本的にすべてデフォルトでOK）

```
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target angular
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.graphql
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/app/API.service.ts
```

```amplify push``` の実行が完了すると、
AWS上でCloudFormationが実行され、下記が生成されます。

* AWS::AppSync
* AWS::IAM
* AWS::DynamoDB

```amplify init``` 実行時とは異なり、色々なリソースが生成されています。
なお、CloudFormation のスタックとしてまとめられているため、
作成したリソースは後でまとめて消すことができるようになっています。


最後に、GraphQLのendpointとAPI KEYが表示されます。
これらの情報は、 aws-export.jsに追記されています。
（これらの情報は、```amplify status``` コマンドを実行することで確認することもできます。）

```
GraphQL endpoint: https://********.appsync-api.ap-northeast-1.amazonaws.com/graphql
GraphQL API KEY: *************
```

プロジェクトフォルダ内に下記ファイルが生成されます。
これは、AWS上のリソースにアクセスするためのAPIが自動的に生成されたものになります。

![スクリーンショット 2022-02-05 18.47.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/5a33583a-52c8-8ee6-f199-3cdd8aa875e7.png)


## 6.Webアプリ作成の準備

続いて、Webアプリの準備を行います。
まず、```main.ts```に、下記を追記します。

```typescript:src/main.ts
import Amplify from "aws-amplify";
import aws_exports from "./aws-exports";

Amplify.configure(aws_exports);
```

続いて、　```src/tsconfig.app.json```の ```compilerOptions```に下記を追記します。

```json:src/tsconfig.app.json
"compilerOptions": {
    "types" : ["node"]
}
```

最後に、```src/polyfills.ts``` に下記を追記します。

```typescript:src/polyfills.ts

/***************************************************************************************************
 * APPLICATION IMPORTS
 */

 (window as any).global = window;

 (window as any).process = {
   env: { DEBUG: undefined },
 };
```

以上で準備は完了です。

## 7.AWSStorageServiceの作成
データをAWS上に保存（upload)、取得(download)するためのサービスを実装します。

なお、インターネット経由でAWS上のリソースへアクセスするため、
基本的にはAPIの呼び出し結果などは非同期で返ってきます。
これらを適切に処理するには、RxJSの Observableを使って、
Subscriberに完了を通知するという流れが基本です。


しかし、今回は、シンプルに作るため、あえてObservableを使わない状態とします。
この点はご了承ください。



まずは、状態を定義します。
例えば、インターネットにつながってない時はAWSへデータをアップロードすることも、
AWSからダウンロードすることもできません。
サービス内の状態管理をしておかないと、今がどういう状態なのかわからなくなってしまいますので、
サービスの内部状態を定義します。

``` awsstorageservice.ts``` の先頭(import文の直下)に下記の状態を定義してください。

```typescript:awsstorageservice.ts
export const AWSSTORAGE_STATUS = {
  READY: 'ready',                       // データアップロード可能状態
  NOT_INITIALIZED: "not_initialized",   // 未初期化状態
  UPLOADING: "uploading",               // アップロード中
  DOWNLOADING: "downloading",           // ダウンロード中
  DOWNLOADERROR: "download_error",      // ダウンロードエラー
  UPLOADERROR: "upload_error"           // アップロードエラー
} as const;

```

状態遷移図で表すと下記となります。
今回は、ダウンロードエラー、アップロードエラーとなった場合は、手動で初期化メソッド ```initAWSStorage```を呼び出す必要があります。

![SoWkIImgAStDuIhEpimhI2nAp5N8pS_BJyueoizDLIWfAatbYjQALT3LjLFmym-9z_Jp3F5qzF6CSdNXGkCh09K4kJgwH6AJSF4Fz_FnTtJnz7FdGc8ZIxa61Va78KiY30IJG6ve0CmC60kX25K8o84RoOLYWmAcJu1QyKfZUCXAGUdPd5LFE-O-cpF_QSyq9MHN0-KUDpSyRbhsk7dpu-QrZvltF6xkVB9ZhHB85eIIfaMF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/e40ddbdb-592a-39aa-b053-b1d10f2a9ac0.png)

続いて、``` class AWSStorageServiceService``` の中に、下記を追加します。

```typescript:awsstorageservice.ts
// AWSStorageのID
  private AWSStorageID: string = "";          // idフィールドの内容
  private AWSStorageData: string = "";        // detaフィールドの内容
  private AWSStorageStatus: string = AWSSTORAGE_STATUS.NOT_INITIALIZED;
```

初期化するためのメソッドを追加します。

```typescript:awsstorageservice.ts
/// AWSStorageServiceを初期化する
  public initAWSStorage(id: string){
    if(id === ""){
      // ID無指定の場合は、初期化完了（Upload待ち)
      this.AWSStorageStatus = AWSSTORAGE_STATUS.READY;
    }else{
      // ID指定の場合は、ダウンロード開始
      this.AWSStorageID = id;
      this.downloadAWSStorage();
    }
  }

```

データと状態変数のset/getのメソッドを追加します。

```typescript:awsstorageservice.ts
  /// AWSStorageにデータをセットする
  public setAWSStorage(data: string):boolean{
    if(this.AWSStorageStatus !==AWSSTORAGE_STATUS.READY){
      return false;
    }

    this.AWSStorageData = data;
    this.uploadAWSStorage(data);
    return true;

  }

  /// AWSStorageのデータを渡す
  public getAWSStorage():string{
    return this.AWSStorageData;
  }

  /// AWSStorageのIDを取得する
  public getAWSStorageID():string{
    return this.AWSStorageID;
  }

  /// AWSStorageの状態を取得する
  public getAWSStorageStatus(): string{
    return this.AWSStorageStatus;
  }

```

続いて、AWSと通信を行う部分を追加します。
ここでは、アップロードとダウンロードのメソッドを追加します。

```typescript:awsstorageservice.ts

  /// upload処理を行う
  public uploadAWSStorage(data: string){
    this.AWSStorageStatus = AWSSTORAGE_STATUS.UPLOADING;

    if(this.AWSStorageID === ""){
      //新規の場合
      this.apiService.CreateStorage({
        data: data
      }).then( (evt) => {
          this.AWSStorageID = evt.id;
          this.AWSStorageStatus = AWSSTORAGE_STATUS.READY;
      }).catch( (error) =>{
        console.log(error)
        this.AWSStorageStatus = AWSSTORAGE_STATUS.UPLOADERROR;
      });
    }else{
      //アップデートの場合
      this.apiService.UpdateStorage({
        id: this.AWSStorageID,
        data: data
      }).then( (evt) =>{
        this.AWSStorageStatus = AWSSTORAGE_STATUS.READY;
      }).catch( (error) =>{
        console.log(error)
        this.AWSStorageStatus = AWSSTORAGE_STATUS.UPLOADERROR;
      });
    }
  }

  /// download処理を行う
  public downloadAWSStorage(){
    this.AWSStorageStatus = AWSSTORAGE_STATUS.DOWNLOADING;
    this.apiService.GetStorage(this.AWSStorageID).then((evt) => {
      this.AWSStorageData = evt.data;
      this.AWSStorageStatus = AWSSTORAGE_STATUS.READY;
    }).catch( (error) =>{
      console.log(error)
      this.AWSStorageStatus = AWSSTORAGE_STATUS.DOWNLOADERROR;
    });
  }

```
以上です。

## 8.テスト用のWebページの作成
動作テストのため、home.pageを編集します。
```FormsModule```を使用したいため、```app.module.ts```に 下記を記載します。

```typescript:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, FormsModule],
  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
  bootstrap: [AppComponent],
})
export class AppModule {}

```

続いて、HTMLファイルを編集します。

```html:home.page.html
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      AWSStorageData TEST PAGE
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-button (click)="init()">初期化</ion-button>

  <ion-item>
    <ion-button (click)="upload()">アップロード</ion-button>
    <ion-input [(ngModel)]="uploaddata"></ion-input>
  </ion-item>

  <ion-item>
    <ion-button (click)="getid()">ID取得</ion-button>
    <ion-input name="id" [(ngModel)]="id"></ion-input>
  </ion-item>

  <ion-item>
    <ion-button (click)="download()">ダウンロード</ion-button>
    <ion-input [(ngModel)]="downloaddata"></ion-input>
  </ion-item>

  <ion-item>
    <ion-button (click)="updatestatus()">状態取得</ion-button>
    <ion-input [(ngModel)]="status"></ion-input>
  </ion-item>

</ion-content>
```

最後に、Typescriptを編集します。
ボタンを押した時の処理などを追加します。

```typescript:home.page.ts

export class HomePage {
  public id: string = "";
  public downloaddata: string = "";
  public uploaddata: string = "";
  public status: string = "";

  constructor(private awsstoragedataservice:AWSStorageServiceService) {

  }

  public init(){
    this.awsstoragedataservice.initAWSStorage(this.id);
  }

  public getid(){
    this.id = this.awsstoragedataservice.getAWSStorageID();
  }

  public download(){
    this.downloaddata = this.awsstoragedataservice.getAWSStorage();
  }


  public upload(){
    this.awsstoragedataservice.setAWSStorage(this.uploaddata);
  }

  public updatestatus(){
    this.status = this.awsstoragedataservice.getAWSStorageStatus();
  }

}

```

## 9.デプロイ＆ホスティングする
作成したWebアプリを、AWS上にデプロイ＆ホスティングします。
まず、下記を実行します。

```
amplify add hosting
```

下記の様に回答します。

```
? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment)
? Choose a type Manual deployment
```

続いて、下記を実行します。

```
amplify publish
```

自動的に　PC上でng build が実行され、ビルドされたWebアプリがAWS上にアップロードされます。
完了すると、下記の様に、URLが表示されます。

```
✔ Zipping artifacts completed.
✔ Deployment complete!
https://dev.********.amplifyapp.com
```


## 10.動作確認
先ほどのURLにアクセスします。
まずはデータのアップロードの確認です。

1. 初期化
2. データをアップロード。
3. 状態取得→アップロードに成功したことを確認
4. アップロードしたデータのIDを取得

![upload_test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/27e0a993-ef13-d072-fd4d-e58878c41765.gif)

続いて、ダウンロードの確認です。

1. 先ほどのIDだけをコピーする
2. ブラウザの別のタブを開いて、同じURLにアクセス
3. IDをペーストする
4. 初期化
5. 状態取得→ダウンロードに成功したことを確認
6. ダウンロードしたデータを表示


![downloadtest.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017303/ba4b0c7f-72b9-99fa-f26a-b75c1fe10382.gif)

前のページでアップロードしたデータが取得できていることが確認できます。
（AWSコンソールにアクセスして確認すると、DynamoDBに上記のデータが追加されていることがわかります。）


## 11.まとめ
今回は、AWS Amplifyを使って、Webアプリ内のデータを、簡単にAWS上にアップロード/ダウンロードするサンプルを作ってみました。


とても簡単に、AWS上にデータをアップロードしたり、ダウンロードしたりできました。
機会があれば、また別のサンプルも作ってみたいと思います。


以上です。