今日こそやるぞ

本気を出せば今日できる

VS Code Remote Development に乗り遅れるな

tl;dr

  • 実行環境からエディタ拡張インストールまで全てDocker環境に収まり、つよい
  • もっとシンプルになると更に楽かなー

(この記事ではSSHを捨象してDockerの話ばかりしていますので適宜補完してください)

それは何なの

これです(説明放棄)

crieit.net

世間の声です(感想放棄)

b.hatena.ne.jp

おいしいところ

この機能は「Docker上にプログラムの実行環境を用意する機能」に留まらず、「Docker上にVSCodeエクステンションをインストールして、実質すべてDockerで編集作業をする機能」と言うべきもの、みたいです。

f:id:stakme:20190519145443p:plain
VS Code Remote Development

code.visualstudio.com

この「全部」というのが喜びポイント。

シンプルに「コードの実行環境をDockerに分離する」だけなら、これまでも全員やってきた話だと思います。Compose書いてRails + MySQL立てっぱなしとか、テストするたびdocker-compose run app yarn testとか。Dockerfile書けば済む話ですし、それだけなら言うほど難しくはない1

ただ、それで解決できないタイプの問題もありました。その典型は「IDE自身が実行環境を必要とする場合」であって、たとえば「IDE拡張機能がnpmモジュールに依存していて、Docker上でコンパイルされたコマンドをローカルで叩こうとして死ぬ」みたいな事故が起こってくるとか。

「ローカルにPerl v5.20が入っているけど、リモートにあるPerl v5.24で開発したい」というときには、実行もデバッグもリントもすべて別環境(Docker)でやってほしいわけですが、そこでVSCode拡張機能だけがローカルでperlコマンドを叩き始めたりすると非常に困るわけですね。

こういった問題を一挙に解決するためのアプローチはある意味シンプルで、全部とにかくリモートに突っ込んでしまえば済む話です。

Remote Development概念図が意味するところは、そういう意味です。たぶん。

使い方

例によって細かい話は公式を読んだ方が早いです。

aka.ms

そのうえでユーザー側でやることは「このディレクトリをリモートに送りたいな〜」という場所に.devcontainer/devcontainer.jsonを設置するだけです。超簡単!

その設定内容は、ちゃんとしたリファレンスもありますが、テンプレートを眺めるのが一番わかりやすいでしょう。

github.com

Docker Hubにあるイメージを使う場合だと、相当シンプルに書けますねー。ここでは検証ということで、Composeくんを噛ませてみます。

docker-compose.yml

適当にそれっぽくでっち上げてみます。Dockerfileはさらに適当なので掲載略。

version: "3.7"
services:
  perl:
    build: ./
    image: vsc-perl:5.28
    tty: true
    volumes:
      - ..:/workspace
      - ~/.gitconfig:/root/.gitconfig
    depends_on:
      - db
      - redis
  redis:
    image: redis:5-alpine
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: password

devcontainer.json

Composeを採用する場合、以下のような感じです。エクステンションを刺しまくりましょう2

{
  "name": "Perl 5",
  "dockerComposeFile": "docker-compose.yml",
  "service": "perl",
  "workspaceFolder": "/workspace",
  "extensions": [
    // perl
    "richterger.perl",
    "d9705996.perl-toolbox",
    "sfodje.perltidy",
    "mortenhenriksen.perl-debug",

    // YAML
    "redhat.vscode-yaml",

    // EditorConfig
    "editorconfig.editorconfig",

    // Perttier
    "esbenp.prettier-vscode"
  ]
}

serviceはComposeのサービス名です。ここで作業をします。

"workspaceFolder": "/workspace"VSCodeが開くべきDocker上のパスを指しています。/workspaceはローカルにあるデータのマウント先なので、結果として「このディレクトリをリモートに送りたいな〜」というディレクトリがworkspaceFolderに来ています。

参考までに、ファイルはここにあります。

gitlab.com

その他のメモ

VSCodeの問題ではないのですが、人間サイドに欠陥があるため、やや戸惑うことがあります。

たとえば「拡張機能はリモート環境でインストールする」という概念自体を頭でわかっていても、うっかりローカルにインストールしたり、リモートにインストールしたけど設定ファイル更新を忘れたりします。

今後あるいは「リモートにインストールするのか、ローカルにインストールするのかよく分からん」という事件も起こりそうですし、ある程度VSCode側で管理してもらって「リモートにインストールするとdevcontainer.jsonが自動更新される」「リモート有効なら、インストール先を自動で判別してくれる」みたいな機能がついたら幸せかなーと思います。それこそpackage.jsonみたいな感じで。

現状でも相当便利に使えるものだなーという感じでみています。開発中なのでまだちょっと手間かかりますけど、試してみるのオススメですよ。


  1. 面倒だし、間違えてローカルにnpmインストールするかもしれないけど…

  2. デバッガーのみ挙動がちょっと怪しいですが、他はちゃんと動きます

祝 React-DnD 型定義ファイル理解

理解できたので、TypeScriptで関数でReact DnDです。

本来はチュートリアルを読めばいいと思いますが「動く現物がポーンってあると何かと楽〜」という人(筆者です)のために、とりあえず置いておくやつ。

注意: このコンポーネントは、ユーザーがドラッグする対象と、それがドロップされる対象を兼ねています。

// 追記: なんかめっちゃ間違ってたので超直した
import React, { FunctionComponent } from "react";
import {
  ConnectDragSource,
  DragSource,
  DragSourceCollector,
  DragSourceSpec,
  DropTargetSpec,
  DropTargetCollector,
  DropTarget,
  ConnectDropTarget
} from "react-dnd";

export type Props = {
  title: string;
};

type DragProps = {
  connectDrag: ConnectDragSource;
};

type DropProps = {
  connectDrop: ConnectDropTarget;
};

type DraggableProps = Props & DragProps & DropProps;
type Component = FunctionComponent<DraggableProps>;

const ToDoItem: Component = ({
  title,
  connectDrag,
  connectDrop
}) => {
  return connectDrop(
    connectDrag(
      <li>
        {title}
      </li>
    )
  );
};

/*
    drag
 */

const dragSource: DragSourceSpec<Props, {}> = {
  beginDrag: (p: Props) => ({}),
  endDrag: (props, monitor, component: Component) => {
    const result = monitor.getDropResult();
    console.log({ result });
  }
};

const dragCollector: DragSourceCollector<DragProps, Props> = (
  connect,
  monitor,
  props
) => {
  return { connectDrag: connect.dragSource() };
};

/*
    drop
 */

const dropTarget: DropTargetSpec<Props> = {
  drop: (props, monitor, component: Component): any => {
    console.log({ props, monitor, component });
    return {};
  }
};

const dropCollector: DropTargetCollector<DropProps, Props> = (
  connect,
  monitor,
  props
) => {
  return { connectDrop: connect.dropTarget() };
};

export default DropTarget("DnD-item", dropTarget, dropCollector)(
  DragSource("DnD-item", dragSource, dragCollector)(ToDoItem)
) as FunctionComponent<Props>

そしてアプリ全体でD&Dを有効化して参ります。

import React, { FunctionComponent } from "react";
import DefaultLayout from "./layouts/default";
import HTML5Backend from "react-dnd-html5-backend";
import { DragDropContextProvider } from "react-dnd";

const App: FunctionComponent = () => {
  return (
    <DragDropContextProvider backend={HTML5Backend}>
      <DefaultLayout />
    </DragDropContextProvider>
  );
};
export default App;

ちゃんとした記事は、アプリをちゃんと作ったら書くかもしれない。この記事はこれでおしまい。

ちゃんちゃん。

ちなみに

「とりあえずドラッグしたい」という場合であっても、いきなりDnDをゴリゴリ書く必要はないかもしれません。使えるケースなら素直にソータブルリストを使うほうがいい気がします。D&D UI自作は結構めんどそう…

React Router + Firebase Auth認証ってどうやればいいんだろうね

tl;dr

謎だね!ということで捻り出した実装がこれ↓

<Router>
  <Switch>
    {authed && <Route exact path="/" component={RootIndex} />}
    <Route component={NotFound} />
  </Switch>
</Router>

詳しく

firebaseの使い方については、地球人なら全員知っているという前提にて省略します。

firebase.ts

まず最初に、Firebaseを初期化する処理をどこかに用意します。

import Firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

Firebase.initializeApp({
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_Id,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID
});

export const firebase = Firebase.app();

Firebaseを扱う場合、本番・ステージング環境を分離するために環境変数を弄り倒すことが多いですね。CIの都合もありますし、ファイルより環境変数のほうが大体いいです。

ちなみにcreate-react-appはビルド時に.env.productionしか見ないらしいので1、ステージング向けデプロイ設定を.env.developmentに書いたり、NODE_ENV=development npm run buildとかコマンド打ったりしても無駄なので注意しましょう。

1時間くらい無駄になります。なりました。

layout.tsx

ついでReact Routerに中身を詰めるコンポーネントを書きます。ファイル名はなんでもいいです。

import React, { FunctionComponent, useState } from "react";
import { Route, Switch } from "react-router-dom";
import { firebase } from "../firebase";
import AppBar from "../components/AppBar";

import loadable from "@loadable/component";
const RootIndex = loadable(() => import("../pages/index"));
const NotFound = loadable(() => import("../pages/notFound"));

const defaultLayout: FunctionComponent = () => {
  const [authed, setAuthed] = useState<boolean>();

  useEffect(() => {
    firebase.auth().onAuthStateChanged(u => {
      setAuthed(!!u);
    });
  }, []);

  return (
    <div>
      <header>
        <AppBar />
      </header>

      {authed !== undefined && (
        <Switch>
          {authed && <Route exact path="/" component={RootIndex} />}
          <Route component={NotFound} />
        </Switch>
      )}
    </div>
  );
};
export default defaultLayout;

コンポーネント内部でfirebase.auth().onAuthStateChangedを直接いじって、認証の状況authedを取得しています。密結合で最高ですね!

  • undefined: 初期化中
  • true: ログイン状態
  • false: ログアウト状態

onAuthStateChangedは登録されたコールバックをすべて1回ずつ、登録された順に呼んでくれるようです。そのおかげで、こんな雑な感じでも動いてしまいますが、ちゃんとしたい場合はReduxを噛ませましょう。

~ Happy End ~

でも公式ドキュメントにはRedirectって書いてあるけど?

はい、実を言うと公式ドキュメントに例があります。

ドキュメントでは、ルーティング用のコンポーネントを作り、そこで認証状態をみて、認証されていなければリダイレクトという構成が示されています。まあ言われてみりゃそうで、クライアント側で完結する分にはリダイレクトでよさそうです(読み飛ばしていて後から気づいた)。

ただ、SSRするときには401やら404やらをサクッと返したい場合もあるだろうなーということで、上記のような書き方もあるなということを一応メモ。

https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/server-rendering.md

おしまい。


  1. when you run npm run build to make a production bundle, it is always equal to ‘production’. You cannot override NODE_ENV manually. (link)

AWS Amplify / GraphQL Transform 紹介と雑感

tl;dr

AppSyncするならAmplify

突然ですが皆さん、AWSのAppSyncをご存知ですか?

AppSyncは、AWSRDBやDynamoDBなどをデータソースとし、そこから情報を取る処理も、新しく書き込む処理も、なんでもGraphQLリゾルバー経由で書けちゃうすごいやつです。API Gateway + Lambda構成でよくやるパターンをDSLでサクサク書けてしまうんですよね。

すばらしい、時代はGraphQLですね。

が、しかしAppSyncのドキュメントを見ると「AWSのWebコンソールから試してみよう!うわっ便利!うひょー!」というテンション高い話に終始していて、具体的にどうやって開発を進めたらいいのか、曖昧なままになっています。

「とりあえずDynamoDBとAppSyncを使ってみたけど、リポジトリで管理する方法がわからん」みたいな状態になりがちですよね。全員なりがちだと思うんですが、私は先週なりました。

そこを解決してくれるのがAWS Amplifyです。

そもそもはモバイル向け開発をやるためのフレームワークですが、その1機能としてAPI管理ができます。具体的には、AmplifyのなかでGraphQLスキーマを書くだけで、AWSの関連リソースがCloudFormation経由でデプロイされます。えっ何それすごくない…?

ということで、Amplifyを経由してAppSyncしましょう。

お試し

やっぱり公式ドキュメントを見るのが一番いいです。この辺ですな。

とりあえずWebアプリを作って配信するだけなら、create-react-appでも使いましょう。ただ今回、そこは本題でないので index.html 手書きでもいいです。

# Amplify CLIインストール
npm install -g @aws-amplify/cli
amplify configure

# 適当にアプリ作る
npx create-react-app my-app --typescript
cd my-app

# 配信できる!!!
amplify add hosting
amplify publish

# API生えた!!!
amplify add api
amplify push

なんと、上記のコマンドを叩いていけば、もうそれだけでS3から配信できてしまうのです1。いやこれ簡単ですね。

この調子で順次、コマンドから auth api analytics… と足していけば、それだけでWebアプリが作れちゃいます。SDKに必要な設定ファイルなども全部CLIが吐き出してくれます。これは快適です。

GraphQLスキーマのサンプルもついでに載せておきます。

type Blog @model {
  id: ID!
  name: String!
  posts: [Post] @connection(name: "BlogPosts")
}

type Post @model {
  id: ID!
  title: String!
  blog: Blog @connection(name: "BlogPosts")
  comments: [Comment] @connection(name: "PostComments")
}

type Comment @model {
  id: ID!
  content: String
  post: Post @connection(name: "PostComments")
}

こういう風に書くだけで、AppSyncで利用できる詳細なスキーマとリゾルバーが自動生成されちゃうのです。ストレージはDynamoDBにあって、モデル同士のリレーション処理とか、ユーザーベースの権限認証とかもそれなりにやってくれるんです。すごっ。

まだ使い込んでいない段階ですが、今のところFirebaseくんの生産性と殴り合えそうなアウラを感じます。アウラがすごいですよ、アウラが。

もっと真っ当な詳細を知りたい人はこちらのスライドを見ましょう。

speakerdeck.com

Amplify最高!

Amplifyコンソールもあるぞ

ややこしいんですが、名前が同じで微妙に関係ないAmplifyコンソールもあります。ドキュメントの紹介テキストが一番いいのでそれを読んで欲しいですが、

  • Amplify CLIでバックエンド書いてAmplifyコンソールに突っ込むと、つよいぞ
    • フロント・バックエンドをセットで、ブランチ単位の環境を作れるぞ
    • セットだからデプロイ工程が堅牢だぞ
  • ブランチ名サブドメインとか使えちゃうぞ
  • パスワード保護とかもできちゃうぞ

というような話が手短に書いてあります。つまり最高ってことなんですね。なお今月あたりから東京でも使えるようになっています。

aws.amazon.com

時代はAWS Amplify!!!

とまあそれで終わればよかったんですが

根本的なプロダクトの方向性として、とにかく「分かってる人」が楽をするツールとして作られているような節があり、このドキュメントを1から読むのは相当骨が折れるな… という気分です。実際に読んでみると、裏にあるのは普通のAWSサービス群ということもあって、既視感のある記述がほとんどではあるのですが。

例えばバックエンドのAPIを書こう、とりあえずデータ保存するくらいでいいから動くもの作ろう、となると、この辺を読むわけじゃないですか。

いやちょっと長いな… 長くない?

考えてみれば当然ですが、Amplifyを使ってバックエンドを書くということはつまり、DynamoDBとAppSyncとCognitoのドキュメントを全部読んでから出直してこいということなんですよね。そして本体のドキュメントを知っている前提なので、例えば.vtlという拡張子が何の説明もなく出てきます。この辺ちゃんと読んでなかったので無事死亡しました。

それに加えて、GraphQLスキーマから自動生成されるロジックにも今ひとつ馴染めないでいます。具体的には下記。

謎1. DynamoDBテーブルにプライマリソートキーがない

設定次第では無い、ということではなく、常に絶対にないようです。

まだまだDynamoDBに詳しくないのでアレですが、絶対にソートキーがないテーブルでは相当しんどいのでは…?

議論は進んでいるので、いずれ対応されるとは思いますが、現状では「手でテーブルを作るしかない」「既存のテーブルにつなげ」というような話になっています。

「デプロイが容易で確実」「環境構築が楽」という話とまったく噛み合わないので、ここはちょっと調べたいというか、救いがほしいというか…

謎2. アイテムのIDがUUID

デフォルトで生成されるロジックでは、DynamoDBに新しいアイテムを生成する際、mutation引数の1つとしてIDを渡すことができます。渡さないでおくとUUIDになります。

DynamoDB素人の的外れな感想かもしれませんが、私の理解する限り、プライマリパーティションキーを全部UUIDで埋められては使い物にならない気がします。

キーがUUIDになると、クライアント側としてはスキャンを要求するしかないはずです。実際に自動生成されたlist処理をみても、やはりスキャンをかけています。

#set( $limit = $util.defaultIfNull($context.args.limit, 10) )
{
  "version": "2017-02-28",
  "operation": "Scan",
  "filter":   #if( $context.args.filter )
$util.transform.toDynamoDBFilterExpression($ctx.args.filter)
  #else
null
  #end,
  "limit": $limit,
  "nextToken":   #if( $context.args.nextToken )
"$context.args.nextToken"
  #else
null
  #end
}

早すぎる最適化サムライになっても仕方ないので、デフォルト実装がスキャンでも全く問題ないかなとは思うのですが。しかし、これを後からベストプラクティスにしたがってクエリに変えようと思うと、それは中々しんどそうですよね。IDの設計からやり直しになるわけですし…

アイテム生成時にIDを指定してやればよい、とはいえ、どうせ手で書くならサーバー側にまとめて記述したいです。ただそうすると自動生成コードのコピペ修正が必須になってしまい、自動生成による旨味が激減しそう。

基本的なロジックが自動生成されるのは本当に嬉しいんですが、それをそのまんま使っていいのか悩ましい(素人にはわからない)ので、ちょっと戸惑いがありますですね。

謎3. ユーザー名で権限管理している (解決)

説明しずらいのですが、要するにこうです。

f:id:stakme:20190423184242p:plain

owner という属性にユーザー名が保存されています。この文字列を使って権限管理をしよう、ということです。

最初にこのデータを見たとき、「ユーザー名は変わるかもしれないのだから、文字列として保存しておくと壊れるのでは?」と心配になりました。なりますよね?

ところが実際のところは、Cognito User Poolに登録されたusernameは変更できないという仕様があります。ですから、文字列として保存しておいて問題ありません2

一事が万事この調子なので、AWSの細かい挙動を「分かっている」人でないと五里霧中という感じが強いです。

謎4. 認証の世界観が謎

Amplifyでは、認証周りをシンプルに扱えるReactコンポーネントが提供されています。TypeScript定義がまだないようですが、それは大した問題ではないので置きます。

使うときはこんな感じです。

import { withAuthenticator } from "aws-amplify-react";
import App from "./App.tsx";
export default withAuthenticator(App)

これで「ログインしていないと常にログインメニューだけが表示されるアプリ」が完成します。この簡単さには目を見張るものがあります。

ただ、業務用のアプリなら「このサイトはログイン強制です」の一言で済むと思いますが、一般のWebアプリで「ログインしないと何も見えないサイト」というのはむしろレアケースです。ログインしないユーザーを想定しているからこそ、Cognito IDプールもUnauthロールとか何とか用意してくれているわけです。

そういった「普通のサイト」をどう作るんでしたっけ、という部分について、実はまだ全く理解できていません。ドキュメントを読みつつ手を動かしてみようという段階です。

AppSyncのエンドポイントを叩くためにはCognito認証を通す必要があるはずなので、なんかあるんだろうとは思います。たぶんね。

とりあえずAmplify推していくぞ

現時点でAWS Amplifyの印象としては面白そうだが、良くも悪くもAWSであり、とりあえず独自ドキュメントが長いといったところです。

ドキュメントくらい読めよ!という話は本当にそうなんですが、実際のところ人間はREADMEを読まないものなので、この取っ付きにくさで流行るのか心配だなーというところがあります。

他方、GraphQLスキーマをいじるとAppSyncにエンドポイントが生え、DynamoDBが用意され、IAMもよしなに処理される(今回は触れていませんが画像用S3なども利用できる)という仕組み全体はかなり面白いですし、もちろん実用的でもあります。背後にいるのが普通のAWSサービス群なので、最悪そこに手を突っ込めばなんとかなるだろうという思惑 / 安心感も働きます。

全体としてみると、まあこれは流行るだろうな… というお気持ち表明記事でした。


  1. CloudFrontも噛ませられる。実際にはCLIが「開発用ならS3 + httpでいいんじゃないかと思うが?」などと提案してくれる。親切最高〜

  2. どうしてもユーザー名を保存したくなければ @auth(identityField: "sub")