AmplifyとVueを使ってS3をラップしたアプリケーションを作ってみた

Aさん「S3でファイル共有したいね」
Bさん「いいねー、じゃあIAMユーザーを、、」
Aさん「おっといけない、うちのルールではIAMユーザーを払い出すことはできないんだ」
Bさん「じゃあ、S3のバケットポリシーでIPアドレスせいげ、、」
Aさん「おっといけない、認証機能は必須なんだ」

Bさん「じゃあ、Amplifyで簡単なアプリケーションを作るか」

というのが本ブログです。こんな感じのアプリを作成します。

それではやってみましょう!!

プロジェクト作成

AWS Amplify Vue Starterをベースにアプリケーションを作成します。

対象のプロジェクトを取得し必要なライブラリをインストールします。

$ git clone https://github.com/aws-samples/aws-amplify-vue.git
$ cd aws-amplify-vue
$ yarn install

UIのコンポーネントも合わせてインストールします。

$ yarn add element-ui

Amplify Frameworkをインストール

Amplify Frameworkをインストールします。

$ npm install -g @aws-amplify/cli
$ amplify version
3.0.0

AmplifyでAWSリソースを作成

Amplifyをセットアップします。対話形式で設定を行います。

$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project s3-app
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script serve
Using default provider  awscloudformation

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

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use hoge
? Enter the MFA token code: 
⠏ Initializing project in the cloud...

認証(Cognito)機能を追加します。

$ amplify add auth
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added resource s3app0a645faf 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

ストレージ(S3)機能を追加します。S3を操作できるユーザーは認証済ユーザーのみとします。

$ amplify add storage
? Please select from one of the below mentioned services Content (Images, audio, video, etc.)
? Please provide a friendly name for your resource that will be used to label this category in the project: s329f8afc1
? Please provide bucket name: s3-appd2834b29a3a946e0a85699ad2dc7633e
? Who should have access: Auth users only
? What kind of access do you want for Authenticated users? create/update, read, delete
? Do you want to add a Lambda Trigger for your S3 Bucket? No
Successfully added resource s329f8afc1 locally

Some next steps:
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

リソースをデプロイします。これによりCognito、S3などのリソースがAWS上に作成されます。

$ amplify push

Current Environment: dev

| Category | Resource name | Operation | Provider plugin   |
| -------- | ------------- | --------- | ----------------- |
| Auth     | XXXXXXXXXXXXX | Create    | awscloudformation |
| Storage  | XXXXXXXXXX    | Create    | awscloudformation |
? Are you sure you want to continue? Yes

✔ All resources are updated in the cloud

フロント部分を修正

src/Home.vueを以下のように変更します。UploadDownloadDeleteに対応したファンクションをそれぞれ作成します。

<template>
  <div class="container shifted">
    <h1 class="h1">
      S3 Objects
    </h1>
    <el-button>
      <label for="file">
        Upload
        <input type="file" @change="upload" id="file" style="display:none;">
      </label>
    </el-button>
    <el-button @click="refresh" class="el-icon-refresh-left"></el-button>
    <el-table 
      :data="s3Data"
      style="width: 100%">
      <el-table-column
        prop="key"
        label="Key"
        sortable>
      </el-table-column>
      <el-table-column
        prop='lastModified'
        label="LastModified"
        sortable>
      </el-table-column>
      <el-table-column
        prop="size"
        label="Size"
        sortable>
      </el-table-column>
      <el-table-column>
        <template slot-scope="scope">
          <el-button
            @click="download(scope.row)">Download</el-button>
          <el-button type="danger" @click="openDeleteDialog(scope.row)">Delete</el-button>
        </template>
      </el-table-column>    
    </el-table>
    <el-dialog
      title="Delete objects"
      :visible.sync="dialogVisible"
      width="30%"
      :before-close="handleClose">
      <span>{{ deleteObject }} Objects will be deleted</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">Cancel</el-button>
        <el-button type="primary" @click="deleteOK()">Confirm</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import Vue from 'vue'
import Amplify, { API,Storage } from 'aws-amplify';
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en'

Vue.use(ElementUI, { locale })

export default {
  name: 'Home',
  data () {
    return {
      s3Data: [],
      dialogVisible: false,
      selectedRow: "",
      deleteObject: "",
      uploadFile: ""
    }
  },
  created () {
    this.refresh()  
  },
  methods: {
    refresh () {
      Storage.list('')
        .then(result => this.s3Data = JSON.parse(JSON.stringify(result)))
        .catch(err => console.log(err));
    },
    upload(e) {
      var files = e.target.files || e.dataTransfer.files;
      console.log(files)
      Storage.put(files[0].name, files[0])
        .then(result => {
          console.log(result)
          this.refresh() 
          })
        .catch(err => console.log(err));
    },
    download (row) {
      Storage.get(row['key'], { download: true })
        .then(result => {
          console.log(result)
          const url = URL.createObjectURL(new Blob([result.Body]));
          const link = document.createElement('a')
          link.href = url
          link.download = row['key']
          console.log(link)
          link.click()
        })
        .catch(err => console.log(err));
    },
    openDeleteDialog(row) {
      this.selectedRow = row;
      console.log(row)
      this.deleteObject = row['key'];
      this.dialogVisible = true
    },
    deleteOK () {
      this.dialogVisible = false
      Storage.remove(this.selectedRow['key'])
        .then(result => {
          console.log(result)
          this.refresh() 
          }
        )
        .catch(err => console.log(err));
    },      
    handleClose(done) {
        this.$confirm('Are you sure to close this dialog?')
          .then(_ => {
            done();
          })
          .catch(_ => {});
    }
  }
}

</script>

<style>
label {
  color: #606266;  
  background-color:white;
  padding: 10px;
}
</style>

ローカルでの動作確認

ローカルで動作を確認してみます。

$ yarn run dev  
Your application is running here: http://localhost:8080 

上記のURLに接続するとログイン画面が表示されます。Create accountよりユーザーを作成しログインしてみましょう。

するとこのような画面が表示されます。

アップロード、ダウンロード、削除も問題なく実行できます。

ファイルの実態はamplify pushにて作成したS3に保存されています。

S3でホスティング

ローカルで動作確認ができたのでS3にアップロードしてインターネットから接続できる状態にしてみましょう。

$ amplify hosting add
? Select the environment setup: DEV (S3 only with HTTP)
? hosting bucket name aws-amplify-vue-20190905141756-hostingbucket
? index doc for the website index.html
? error doc for the website index.html

You can now publish your app using the following command:
Command: amplify publish

アプリケーションをビルドし静的ファイルを出力します。

$ yarn build    
$ ll ./dist/
total 8
-rw-r--r--  1 jogan.naoki  staff   524B  9  5 14:22 index.html
drwxr-xr-x  6 jogan.naoki  staff   192B  9  5 14:22 static 

S3にアップロードし画面を確認してみます。

$ amplify publish

frontend build command exited with code 0
✔ Uploaded files successfully.
Your app is published successfully.
http://XXXXXXXXXXXXX.s3-website-ap-northeast-1.amazonaws.com

問題なさそうです。

また、不特定のユーザのアクセスを禁止する場合は、Cognitoの管理画面よりサインアップの設定を変更しておきましょう。

IP制限を追加したい場合はバケットポリシーで制限すれば良さそうですね。

さいごに

Amplify+Vueを使ってS3を操作できる簡単なアプリケーションを作成してみました。ユースケースはあまりないかもしれませんがハマる場合は使ってみてください。(アクセスログを取得する場合はAPIGateway経由でS3のPre-signedURL発行する必要がありそうです)