【AWS/Serverless】 S3rver & Serverless Offineで構築するローカルのS3 bucketライクなストレージ環境の導入する方法


2020/03/29

私事ですがお仕事でもプライベートでも
Dropboxで様々なファイルを共有しているのですが、ふとS3を用いて自分専用Dropboxみたいなサービスを構築したくなってきました。

以前に本ブログでも取り上げた、
DynamoDB Local を単独のDockerコンテナとして別のコンテナから呼び出すでは、Serverless Offlineを利用して、ローカルのDockerコンテナから疑似DynamoDBのサービスを構築する方法を取り上げていました。

今回はそれをそのまま使えるところを再利用して、ローカルで
AWS S3を運用テストできるようにしてみます。


ローカルS3ストレージサービス/S3rver

当然ながらAWS S3に限った話ではないのですが、オンラインストレージサービスを利用する際には、利用料金を気にしながらファイル容量やデータ転送量を日々モニターする必要があり、いくらでもデータ使いたい放題というわけにはいきません。

そこで
S3rverというAWS S3の模擬サーバーを起動できるアプリケーションを利用すると、ローカル環境になんの縛りもないS3バケット使いたい放題の環境が手に入ります。

RESTフルなアプリケーションから
S3rverにアクセスしようとする場合、Serverless-Offlineを経由してから、リソースに接続する方式になりますので今回はDockerコンテナ越しに以下のような構成にして利用を検討してみます。

合同会社タコスキングダム|蛸壺の技術ブログ

以前に特集した
DynamoDB Local を単独のDockerコンテナとして別のコンテナから呼び出すで、Serverless Offlineを利用したDynamoDBのローカルのREST APIを構築していたときと同様、S3単体のホストするサービスを起動しておくDockerコンテナと、RESTfulな機能を寄せ硬めたApi Gateway + LambdaのDockerコンテナの2段構成となっています。

通常、S3のリソースを利用するユースケースとしては、AWS CloudFrontを介してそのままファイルを読みにいく所謂
Cache Distribution パターン がもっともオーソドックスなものです。

S3でもRESTなサービスにしておくことで、後々で
Auth0によるM2M認証も追加で拡張しやすいため、今回のような構成にしておきます。


ローカルなS3で使えるDockerコンテナ 〜 s3rverサービスの環境構築

DynamoDB Local を単独のDockerコンテナとして別のコンテナから呼び出すの記事では、DynamoDB LocalをベースにDockerコンテナを構築し、ローカルでも動くDynamoDBの疑似サービスを説明しましたが、今回はS3をローカルに擬似ホスティングさせるためのバックエンドアプリケーションが必要です。

選択肢は色々あるようですが、本ブログでは
S3rverをDockerで利用させていただこうと思います。

Dockerfileの作成

まずは取っ掛かりのDockerイメージをDocker Hubから探したものの、ちょっとどれも古臭い感じです。

ただ、
nodeさえ扱えれば十分ですので、以下のようにDockerfileを自前でこさえます。

            
            FROM node:10.18-alpine
ENV NODE_ENV development
WORKDIR /usr/src/app

#awscli on dokcer alpine
ARG pip_installer="https://bootstrap.pypa.io/get-pip.py"
ARG awscli_version="1.16.307"

#Install dependent packages
RUN apk update && apk upgrade && apk add --no-cache \
    bash git openssh \
    python \
    curl \
    groff \
    jq \
    less

#Install awscli
RUN curl ${pip_installer} | python && pip install awscli==${awscli_version}

#Completing input
RUN bash -c 'echo complete -C '/usr/bin/aws_completer' aws >> $HOME/.bashrc'
ENV PS1="[\u@\h:\w]$"
        
今回はローカルでMocking S3を常駐させるだけですので、nodeのDocker Imageさえあれば十分動くのです。

今後のカスタマイズするかもしれないので、
awscli一式を念の為入れておきます。

docker-compose.ymlの作成 (インタラクティブモード用)

現在ローカルで常駐可動しているサービスとの競合がいやなので、サービスポート番号を6122へ変更しております(お好きな番号を利用ください)。

docker-compose.ymlを以下のように作成します。

            
            version: '3'

services:
    app: # appというサービス名で構築
        image: my/s3rver_dckr # コンテナー名
        build: .
        environment:
            NODE_ENV: development
            AWS_ACCESS_KEY_ID: "ABCDEFGHIJKLMNOPQRST" # IAMのユーザーID (ローカルだとなんでも動きます)
            AWS_SECRET_ACCESS_KEY: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # IAMのユーザーパスワード (ローカルだとなんでも動きます)
            AWS_DEFAULT_REGION: "us-east-1"
        ports:
            - "6122:6122" # S3rverのサービス用に6122番を指定
        volumes:
            - ./:/usr/src/app
        tty: true
        
内容を確認したら、docker-compose buildして、インタラクティブモードで開発用のコンテナができます。

S3rverのパッケージインストール

では、ここから先程作成したコンテナへインタラクティブモードに入り、コンテナ中で作業します。プロジェクトのルートディレクトリで、

            
            % docker-compose up -d && docker-compose exec app bash
        
でインタラクティブモードに入り、

            
            % yarn add s3rver -D
        
それでは次にs3rverのパッケージをdevDependenciesにインストールしましょう。

これだけで
s3rverの機能全部が利用の準備完了ですので、とてもお手軽です。

次に手っ取り早くnpmスクリプトで
s3rverが立ち上がるように、package.jsonに以下のようなスクリプトを追加します。

            
            {
    "name": "s3rver_dckr",
    "version": "0.0.1",
    "scripts": {
        "s3:start": "s3rver --address 0.0.0.0 --port 6122 --directory /usr/src/app/tmp"
    },
    // ...
    "devDependencies": {
        "s3rver": "^3.5.0"
    }
}
        
注意点としては、今回のプロジェクトルートディレクトリにtmpという名前のフォルダを作り、そこにS3のデータが入るようにしますが、--directoryオプションは絶対パスを指定しますので、現在のDockerコンテナのworkingディレクトリ(/usr/src/app)を考慮してコマンドを作成ください。

s3rverコンテナの起動

コンテナ常駐化させる前に、ちゃんと動きのかテストしてみます。

とりあえず
testのフォルダを作成し、test.txttest2.textの2つを入れておきます。

空ファイルだと読み出したときにすこし物悲しいので、なにか中身をいれておきます。

            
            $ cat test/test.txt
1234
$ cat test/test2.txt
5678
        
さらにS3のローカルBucketの格納用のフォルダtmpも作成しましょう。

ちなみに
tmpという名前でなくとも、--directoryオプションで正しく指定出来ていればどんな名前でも動きます。

上のような手順を踏んでいただくと、だいたい以下のようなプロジェクト構造になっているかと思います。

            
            $ tree -I node_modules
.
├── Dockerfile
├── README.md
├── docker-compose.yml
├── package.json
├── test
│   ├── test.txt
│   └── test2.txt
├── tmp
└── yarn.lock
        
それでは、s3rverを立ち上げてみましょう。起動には先程追加したスクリプトを用います。

            
            $ yarn s3:start
        
s3rverサービスがdockerコンテナ上で起動します。

curlで外部から動作確認

別のコンソールを立ち上げて、curlで動作確認してみます。

S3rverのReadmeにはこれと言って明記してありませんが、AWS S3 REST APIの機能の一部をサポートしているようです。

具体的な実装は、
lib/controllersのソースコード内に書かれているので、興味があれば覗いてみるとよいでしょう。

ひとまずは基本的な操作のメソッドを以下のようにご紹介します。

            
            #バケットの作成
#例) hogeという名前の空バケットを作成
$ curl -XPUT 'http://localhost:6122/hoge'

#バケットの中身リスト表示
#例) hogeという名前のバケットの中身を表示
#(応答はXML形式のため、xmllintなどでフォーマット表示するのがよろしいかと思います)
$ curl -XGET 'http://localhost:6122/hoge/' | xmllint --format -

#バケットの削除 (※空バケットに限る)
#例) hogeという名前の空バケットを削除
$ curl -XDELETE 'http://localhost:6122/hoge'

#ファイルのアップロード
#例) test.txtをhogeにアップロード
$ curl -XPUT -T test/test.txt 'http://localhost:6122/hoge/'
#ただし、同時に二つ以上のファイルはアップロードできない
$ curl -XPUT -T test/test.txt -T test/test2.txt 'http://localhost:6122/hoge/'

#ファイルの取得
#例) test2.txtをhogeから取得(応答はファイルの中身)
$ curl -XGET 'http://localhost:6122/hoge/test2.txt'
5678

#ファイルの削除
#例) test.txtをhogeから削除
$ curl -XDELETE 'http://localhost:6122/hoge/test.txt'
        
お手元の環境でも動作していれば、正常に機能していると思います。

Dockerコンテナ常駐化してみる

最後に、今回のDockerコンテナをバックグラウンドで永続化させてみましょう。

インタラクティブモードから抜けて、
docker-compose downでコンテナを停止させておきます。追加で、inject-command.ymlを新規作成します。

            
            $ touch inject-command.yml
$ tree -I node_modules
.
├── Dockerfile
├── README.md
├── docker-compose.yml
├── inject-command.yml
├── package.json
├── test
├── tmp
└── yarn.lock
        
このinject-command.ymlを以下のように編集します。

            
            version: '3'

services:
    app:
        entrypoint: yarn s3:start
        restart: always
        
その後、docker-composeの複数のymlファイルを合成してバックグラウンドで起動しましょう。

            
            $ docker-compose -f docker-compose.yml \
    -f inject-command.yml \
    up -d
        
とすることでDockerコンテナは永続化されていると思います。

ここまでで
S3rverを用いてお手軽にS3をローカル環境で利用できるようになりました。

これだけだと、自分専用の
Dropboxみたいなアプリケーションとはまだまだ程遠いのですが、最初の足がかりとしてはまずまず良いのかと思います。

後半では、
Serverless-OfflineのDockerkコンテナを別で起動することでもっと気の利いたRESTサービスをなるように実装を解説して行きましょう。


Serverless-Offlineコンテナの構築とRest API

ここから話の後半戦に突入しましょう。

Serverless-OfflineのDockerコンテナに関しての内容をやっていきます。

前回・
DynamoDB Local を単独のDockerコンテナとして別のコンテナから呼び出す で解説したServerless-OfflineのDockerコンテナと基本的に全く同じ構築手順です。

Serverless Offlineのベースプロジェクトの作成(前準備)

それでは引き続きローカルでs3rverを利用するAPI側の環境を作成していきます。

つまりは
Serverless Offlineのサービスコンテナの作成手順の解説になります。

上記の内容では、ローカルな
S3rをDockerコンテナ化して、独立した常駐サービスになったところまで確認しましたので、以降はServerless-offlineを利用してRest APIを仕上げていきます。

まずは取っ掛かりとして、以前紹介した
Serverless Offineの基本プロジェクトの作成手順: DynamoDB Local を単独のDockerコンテナとして別のコンテナから呼び出す を同様の内容とほぼほぼ被るので、興味があれなそちらの記事のほうも参照の上、手順のほうを比較ください。

さてその時と同じ構成のプロジェクトフォルダ構造を流用しますが、

            
            $ tree -I 'node_modules'
.
├── Dockerfile
├── docker-compose.yml
├── package.json
├── app
│   ├── handler.js
│   ├── package.json
│   ├── serverless.yml
│   └── webpack.config.js
└── yarn.lock
        
こんな感じになっているところから今回の解説をスタートしましょう。

今回の
Serverless Offlineアプリケーションでは、以下のようにDockerfiledocker-compose.ymlを修正して使います。

Dockerfile作成

ではserverless-offline用のDockerfileを以下のように作成します。

            
            FROM node:10.18-alpine
ENV NODE_ENV development
WORKDIR /usr/src/app

#awscli on dokcer alpine
ARG pip_installer="https://bootstrap.pypa.io/get-pip.py"
ARG awscli_version="1.18.31" #👈最新バージョンを手動で書き換えください

#Install dependent packages
RUN apk update && apk upgrade && apk add --no-cache \
    bash git openssh \
    python \
    curl \
    groff \
    jq \
    less

#Install awscli
RUN curl ${pip_installer} | python && pip install awscli==${awscli_version}

#Completing input
RUN bash -c 'echo complete -C '/usr/bin/aws_completer' aws >> $HOME/.bashrc'
ENV PS1="[\u@\h:\w]$"
        
なお、pipインストール番のaws-cliは頻繁にアップデートがきますので、pipインストラー公式のサイトで最新バージョンをチェックしましょう。

docker-compose.yml

今回のコンテナのサービスポートは6121番を開けておきます。ご自身の好ましいポートで変更することも可能です。

            
            version: '3'

services:
    app:
        image: tm/sls_dckr_s3_local
        build: .
        environment:
            NODE_ENV: development
            AWS_ACCESS_KEY_ID: "ABCDEFGHIJKLMNOPQRST" # IAMのユーザーID (本番環境Deploy用)
            AWS_SECRET_ACCESS_KEY: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # IAMのユーザーパスワード (本番環境Deploy用)
            AWS_DEFAULT_REGION: "us-east-1"
            LOCAL_SLS_SERVICE_NAME: "my-sls-local-s3"
        ports:
            - "6121:6121"
        volumes:
            - ./:/usr/src/app
        tty: true
        

外部のS3rver用のDockerコンテナを呼び出す

以前の記事に記載していたように、DynamoDB LocalのDockerコンテナのIPの取得の方法で、S3rverを常駐させているコンテナを呼び出します。

現在動かしている著者のPC上で起動しているIPを調べると、

            
            #S3rver のコンテナIDをチェック
% docker ps -a
CONTAINER ID        IMAGE                          COMMAND                  CREATED             STATUS                      PORTS                                                      NAMES
736ba12e4d4b        tm/s3rver_dckr                 "yarn s3:start"          24 hours ago        Up 5 hours                  0.0.0.0:6122->6122/tcp                                     s3rver-local-dckr_app_1

#コンテナIDからIPを取得
% docker inspect --format='{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' 736
172.26.0.1
        
よって現在のターゲットのIPは172.26.0.1であることがわかります。IPが判明したら、プロジェクトにenv-s3-local.ymlという設定ファイルを追加しましょう。

            
            % touch env-s3-local.yml
        
以下のように編集します。

            
            version: '3'

services:
  app:
    environment:
      OUTER_S3_ID: "172.26.0.1" # PLEASE MAKE SURE OF IT EVERYTIME
        
こうすることで、コンテナ内部のnodejsのソースコードからprocess.env.OUTER_S3_IDで値を参照できます。

インタラクティブモードでコンテナ内部での一時作業

ここからコード実装〜動作確認までインタラクティブモードで作業を進めてみましょう。

            
            % docker-compose -f docker-compose.yml \
    -f env-s3-local.yml up -d \
    && docker-compose exec app bash
        
ここから、serverless.ymlを設計しますが、先に今回のLambda用のハンドラ関数のjsコードと、分割したymlファイルをフォルダごとに作成しておきます。

ハンドラ関数のリソースは
app/handlersフォルダ、serverless.ymlの分割設定用ymlはapp/configフォルダを配置します。

            
            % tree -I 'node_modules'
.
├── Dockerfile
├── README.md
├── app
│   ├── config
│   │   ├── bucket_method.yml
│   │   └── obj_method.yml
│   ├── handlers
│   │   ├── bucket_ctrl.js
│   │   └── obj_ctrl.js
│   ├── package.json
│   ├── serverless.yml
│   ├── webpack.config.js
│   └── yarn.lock
├── docker-compose.yml
├── env-s3-local.yml
├── package.json
└── yarn.lock
        

ハンドラ関数の実装

バケット操作のメソッドをまとめたbucket_ctrl.jsとオブジェクト操作のメソッドをまとめたobj_ctrl.jsにハンドラ関数を実装していきます。基本的には、AWS S3 API Referenceを参照しながら、欲しい機能を盛り込んでいきますが、今回の内容は最も簡単な操作だけ実装します。

bucket_ctrl.js (バケット操作)

S3 APIの全ての関数を解説するのは難しいですが、ここでは最も基本的なlistObjectsのみを例にあげます。これは名前の通り、指定したバケット名からそのバケットの中身を取得します。

            
            "use strict";

const AWS = require('aws-sdk');

module.exports.showBucket = (event, context, callback) => {
    const bn = event.pathParameters.bucketname; // Name of bucket

    const s3 = new AWS.S3({
        apiVersion: '2006-03-01',
        s3ForcePathStyle: true,
        accessKeyId: 'S3RVER', // This specific id is required when working offline
        secretAccessKey: 'S3RVER', // This specific key is required when working offline
        endpoint: new AWS.Endpoint(`http://${process.env.OUTER_S3_ID}:6122`)
    });

    const params = {
        Bucket: bn
    };
    s3.listObjects(params, (err, data) => {
        let response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = JSON.stringify({code: 500, message: "ListObject Error"});
        } else {
            response.statusCode = 200;
            response.body = JSON.stringify({data: data});
        }
        callback(null, response);
    });
};
        
ちなみに、ローカルでS3rverを利用する場合、accessKeyIdsecretAccessKeyにはS3RVERを指定する必要があります。

obj_ctrl.js (オブジェクト操作)

オブジェクト操作もかなり豊富なAPI関数が用意してあります。ここでは、例としてgetObjectを利用します。指定したバケット内から、特定のファイルを取得します。ちなみに、S3rverへアップロードしたファイルは、Content-Typeを指定していなかったので、MIME Typeはデフォルトのbinary/octet-streamになっています。ということで、この状態のままgetObjectで取得した場合には、全てバイナリ形式でダウンロードされます。適切にダウンロードさせたい場合には、ファイルのアップロード時のMIME Typeを情報として指定しておく必要があります。

            
            "use strict";

const AWS = require('aws-sdk');

module.exports.fetchObj = (event, context, callback) => {
    const bn = event.pathParameters.bucketname; // Name of bucket
    const on = event.pathParameters.objname; // Name of object
    const s3 = new AWS.S3({
        apiVersion: '2006-03-01',
        s3ForcePathStyle: true,
        accessKeyId: 'S3RVER', // This specific key is required when working offline
        secretAccessKey: 'S3RVER', // This specific key is required when working offline
        endpoint: new AWS.Endpoint(`http://${process.env.OUTER_S3_ID}:6122`)
    });
    const params = {
        Bucket: bn,
        Key: on,
    };
    s3.getObject(params, (err, data) => {
        let response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = JSON.stringify({code: 500, message: "GetObject Error"});
        } else {
            response.statusCode = 200;
            response.body = JSON.stringify({data: data});
        }
        callback(null, response);
    });
};
        

ymlファイルを分割して設定する

正直今回のプロジェクト規模でserverless.ymlをファイル分割しておくかどうか迷いましたが、serverless.ymlファイルは肥大化しやすいので、早い段階で分割しておくのが良いと思います。

bucket_method.yml

bucket_ctrl.js(バケット操作)用の設定は以下のようにします。

            
            show_bucket:
    - http:
        path: /{bucketname}
        method: get
        
パス名にプレイスホルダー{*}を設定することで、Lambdaのコードでevent.pathParameters.bucketnameからバケット名を引き出すことができます。

obj_method.yml

obj_ctrl.js(オブジェクト操作)用の設定は以下のようにします。

            
            fetch_obj:
    - http:
        path: /{bucketname}/{objname}
        method: get
        
パス名にプレイスホルダー{*}を設定することで、Lambdaのコードでevent.pathParameters.bucketnameからバケット名、event.pathParameters.objnameからオブジェクト名、をそれぞれ引き出すことができます。

serverless.yml (最終版)

以上で最終的なserverless.ymlは以下のようになります。

            
            service:
    name: my-sls-local-s3

plugins:
    - serverless-webpack
    - serverless-offline

provider:
    name: aws
    runtime: nodejs10.x
    region: us-east-1
    stage: dev

custom:
    serverless-offline:
        host: 0.0.0.0
        port: 6121

functions:
    showBucket:
        handler: handlers/bucket_ctrl.showBucket
        events: ${file(./config/bucket_method.yml):show_bucket}
    fetchObj:
        handler: handlers/obj_ctrl.fetchObj
        events: ${file(./config/obj_method.yml):fetch_obj}
        

serverless-offlineコンテナのRest Api動作テスト

それでは、Serverless Offlineアプリケーションが動作するか起動してみます。とりあえずルートのpackage.jsonに起動させるスクリプトを追加します。

            
            {
    "name": "dckr_sls_s3_local",
    "version": "0.0.1",
    "scripts": {
        "start": "cd ./app && sls offline" // 👈 追加
    },
    "devDependencies": {
        "serverless": "^1.59.0"
    }
}
        
サービスを起動してみます。

            
            % yarn start
yarn run v1.21.1
$ cd ./app && sls offline
Serverless: Load command interactiveCli

...(中略)

[offline] fetchObj runtime nodejs10.x
Serverless: Routes for fetchObj:
Serverless: GET /{bucketname}/{objname}
[offline] Response Content-Type application/json
Serverless: POST /{apiVersion}/functions/my-sls-local-s3-dev-fetchObj/invocations

Serverless: Offline [HTTP] listening on http://0.0.0.0:6121
Serverless: Enter "rp" to replay the last request
        
となった状態で、別のコンソールでこのAPIをCurlで叩いてみます。

            
            #バケット'hoge'の中身を表示
% curl -XGET 'http://localhost:6121/hoge' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   311  100   311    0     0    711      0 --:--:-- --:--:-- --:--:--   710
{
  "data": {
    "IsTruncated": false,
    "Marker": "",
    "Contents": [
      {
        "Key": "test.txt", # 👈 test.txtというファイルがあるのを確認
        "LastModified": "2019-12-20T16:34:26.000Z",
        "ETag": "\"e7df7cd2ca07f4f1ab415d457a6e1c13\"",
        "Size": 5,
        "StorageClass": "STANDARD",
        "Owner": {
          "DisplayName": "S3rver",
          "ID": "123456789000"
        }
      }
    ],
    "Name": "hoge",
    "Prefix": "",
    "MaxKeys": 1000,
    "CommonPrefixes": []
  }
}
        
ということで、バケットのリスト表示が機能しております。続いて、このバケットからtest.txtを取得しましょう。

            
            #'hoge'バケットからファイルを取得
% curl -XGET 'http://localhost:6121/hoge/test.txt' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   238  100   238    0     0    746      0 --:--:-- --:--:-- --:--:--   746
{
  "data": {
    "AcceptRanges": "bytes",
    "LastModified": "2019-12-20T16:34:26.000Z",
    "ContentLength": 5,
    "ETag": "\"e7df7cd2ca07f4f1ab415d457a6e1c13\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {},
    "Body": {
      "type": "Buffer",
      "data": [
        49,
        50,
        51,
        52,
        10
      ]
    }
  }
}
        
MIME-Typeはデフォルトのままなので、生バイナリが取得されていますが、1234という文字列が得られていることがわかります。きちんと期待した通りのRESTfulな感じに動作をしているようです。

ついでにコンテナサービスの永続化

それではserverless-offline側のコンテナも動作確認できましたので、このコンテナを常時バックグラウンドで常駐するようにしましょう。

まずインタラクティブモードから抜け、コンテナを停止させます。そして
inject-command.ymlという名前のymlファイルを追加します。

            
            % touch inject-command.yml
        
中身を以下のように編集します。

            
            version: '3'

services:
    app:
        entrypoint: yarn start
        restart: always # 👈コンテナサービスの永続化
        
できたら以下のコマンドでサービスを起動することで、バックグラウンドでサービスを永続的に常駐します。

            
            % docker-compose -f docker-compose.yml \
    -f env-s3-local.yml \
    -f inject-command.yml \
    up -d
        


まとめ

お疲れ様でした。

以上で、ローカルにS3ライクな開発環境を構築することが可能になりました。

ここまで長々とS3rver & Serverless Offineの環境構築の手順を読んでいただきありがとうございます。

本来はこの内容は2~3回に分けようかと思っていましたが、結局一つにまとめてしまいボリューミーな記事になってしまいました。

とにかくこれでファイル容量やデータ転送量も気にすることなく、様々なテストがローカルの中で可能となりました。

自分専用
Dropboxっぽいアプリケーションにはまだまだ程遠いですが、これからバリバリとローカル環境で開発を進めて行こうと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。