lacolaco

唐揚げとアニメとプログラミングが大好きです

日記 20181018

voluntasさんのツイートを見て、自分がここでいう優秀な若者だという自惚れではないんだけど、自分はどういう風に働きたいかなと振り返ったログ

死亡前死因分析 | Basic Werk

法が人を守るんじゃない 人が法を守るんです

秩序が組織を守るんじゃなくて、組織が秩序を守る、そうありたいなと思った20181018。ところで20181018ってなんかキレイだ。

Google Developer Expert (Angular) になりました

f:id:lacolaco:20181004192231p:plain

タイトルのとおり、AngularのGoogle Developer Expertsになりました!

GDEの個人ページは作成が遅れていてまだ存在していません。あしからず

Angular GDEは世界では82人目になるようですが、日本では僕が初めてです。やったぜ!

github.com

まだ高専生だったころ、2013年にGDGのGoogle I/O 報告会九州会場に参加して、そこではじめてGDEを知りました。 それからずっとGDEに憧れていたので、5年越しにひとつ夢が叶いました。大好きなAngularでGDEになれて本当に嬉しいです。 プログラムの進行をサポートしてくれたGoogleのえーじさんやたくおさん、面接に協力してくれたGDEのFilipさんとAngular TeamのStephenには大感謝です。

Angular GDEはWeb GDEのグループにも含まれるようなので、Angularに限らずWeb技術のオープンコミュニティへも微力ながら貢献していくつもりです。 GDEになったからといってこれまでの活動が変わることはほとんどないと思いますが、マウンテンビューから遠く離れた日本でAngularのコミュニティを発展させていくための旗印として、この新しい立場を有効活用していく、あるいはコミュニティが僕を利用するとよいと考えています。 そして、自分がそうだったように、僕を含めた日本のGDEというモデルが、学生たちがエキスパートを目指すモチベーションとなれるように頑張ります。

というわけで、ただのAngular好きからGoogle Developer Expertにランクアップしたlacolacoを今後ともよろしくおねがいします。 Angularのエキスパートが必要なときには気軽にお声がけください!

らこらこファンクラブ会員も募集中です!月額$1から支援できます!

www.patreon.com

GCE上に立てたMastodonインスタンスの定期バックアップを構築したメモ

lacolaco.hatenablog.com

この記事を書いてから2週間くらい、Mastodonインスタンスは何の問題もなく動き続けている。 そろそろバックアップを取るようにして、いつ落ちても泣かなくて良いようにしようと思い、GCP上でバックアップフローを構築した。 Docker構成の都合上、GCEのVMインスタンス上にDBも存在しているので、DBのバックアップではなくそれを含むディスクごとスナップショットを取ることにした。

全体像はこんな感じ。枠線で囲った部分が新たに追加したところ。

f:id:lacolaco:20180920224248p:plain

Cloud Schedulerはまだアルファなので詳細は伏せる。ざっくりcron on GCPのようなもの。 週に1回のサイクルでCloud PubSub経由でCloud Functionsを起動し、呼び出される関数でGCEのクライアントライブラリを使ってスナップショットを作成する。 関数はこのような感じ。

// Imports the Google Cloud client library
const Compute = require('@google-cloud/compute');
const format = require('date-fns/format');
 
// Creates a client
const compute = new Compute();
 
const zone = compute.zone('<zone>');
const disk = zone.disk('<disk name>');

function generateSnapshotName(date) {
    return 'snapshot-' + format(date, 'YYYY-MM-DD');
}

/**
 * Triggered from a message on a Cloud Pub/Sub topic.
 *
 * @param {!Object} event Event payload.
 * @param {!Object} context Metadata for the event.
 */
exports.run = async (event, context) => {
  console.log('== begin job');
  const snapshotName = generateSnapshotName(new Date());
  console.log('snapshotName: ', snapshotName);
  const snapshotData = await disk.createSnapshot(snapshotName);
  console.log(snapshotData);
  console.log('== finish job');
};

いまのところうまく動いている。スナップショットが溜まると金を食うけども、そもそもディスクが10GBだししばらくは気にならない金額のはずなので気にしない。

Google Compute Engine上にMastodonインスタンスを立てた記録

表題のとおり、GCE上にMastodonインスタンスを立てたけど、なんだかんだ試行錯誤で4日くらいかかったので自分のために記録を残す。

立てたインスタンスhttps://activitypub.lacolaco.net。ひとり用に作ったので新規ユーザーは作成できない。 アカウントは https://activitypub.lacolaco.net/@lacolaco。mstdn.jpからのお引越し。

参考にしたのは以下のドキュメント。Googleでいろいろ調べると2017年ごろの記事が多く、いまではやらなくていい無駄やいまだとやらないといけない不足があったので苦労した

Mastodonのバージョンは v2.5.0

GCP準備

インスタンス作成

あんまりよく考えずにus-east1-bにg1-small、Ubuntu 16系で立てた。f1-microではセットアップするのにスワップが必要になって面倒なのでRAM 1.7GBは用意した。 スペックを上げろと案内されてるけどg1-small(vCPU x 1、メモリ 1.7 GB)で今の所動いている。 たぶんリモートフォローが増えていくともっと処理能力が必要になりそう。

ファイアウォール

HTTPとHTTPSさえ空いていれば特にファイアウォールの設定は必要なかった。 v2.5.0時点だとLOCAL_HTTPSの設定がなくなっているので、3000番ポートで接続確認するステップがない。

DNS設定

GCEインスタンスに固定の外部IPを割り当てた後、好きなDNSで好きなドメインにAレコードを設定する。 今回は activitypub.lacolaco.net とした。

ストレージ

GCSに適当なBucketを作る。Regionalにしておく。

設定で相互運用性の設定をして、APIキーと秘密鍵を生成する。これでS3の代わりにGCSが使える。

f:id:lacolaco:20180908234014p:plain

Mailgun準備

おひとり用だとどういうプランでも普通に無料枠なので、適当に登録する。 カスタムドメインも設定して、次の状態にした。

f:id:lacolaco:20180908233149p:plain

あとでここの情報を使う。

VM内作業

インスタンスが出来上がったら、SSHでログインして作業を始める。

Docker準備

UbuntuにDockerとDocker Composeをインストールする。変に日本語記事を当てにせず、普通に公式ドキュメントを読んだほうがよかった。

Get Docker CE for Ubuntu | Docker Documentation

Docker CEをインストールしたあと、dockerコマンドをsudo以外で呼び出せるようにする。

Post-installation steps for Linux | Docker Documentation

$ docker run hello-world が実行できればゴール

次にDocker Composeも公式ドキュメントのとおりでよい。

Install Docker Compose | Docker Documentation

nginx準備

Ubuntuにnginxをインストールする。apt-getで適当に入れる。

$ apt-get update
$ apt-get upgrade
$ sudo apt-get install nginx
// 一旦止める
$ sudo nginx -s stop
// 再開
$ sudo nginx

この時点で、http://activitypub.lacolaco.net にアクセスすればnginxのデフォルト画面に到達できるはず。

証明書準備

あらかじめnginxを停止して、80番ポートを開放する。sudo lsof -i:80 でnginxが出ないことを確認する。

$ sudo apt install letsencrypt
$ sudo letsencrypt certonly

メールアドレスと activitypub.lacolaco.net を入力してうまくいくはず。

続いて、継続的に証明書を更新するためのcron設定をする。

# これが通るかどうかをまずチェックする
$ sudo letsencrypt renew --force-renewal

$ sudo crontab -e

次の1行を追加する

# 毎月1日の朝5時にSSL証明書を自動更新する

00 05 01 * * sudo nginx -s stop; sudo letsencrypt renew --force-renewal; sudo nginx

これで証明書が毎月更新されるようになる

Mastodon準備

いよいよMastodonの準備。基本的に公式ガイドどおりにやるだけ。

Gitのクローン。 liveである意味は特にないんだけど、後続のいろんな設定ファイルが ~/live 前提なのでいじらなくていいようにそのままにする。

# Clone mastodon to ~/live directory
git clone https://github.com/tootsuite/mastodon.git live
# Change directory to ~/live
cd ~/live

docker-compose.ymlの修正

まずデータの永続化を有効化する。 volumesプロパティがコメントアウトされているのをアンコメントする。

### Uncomment to enable DB persistance
    volumes:
      - ./postgres:/var/lib/postgresql/data

  redis:
    restart: always
    image: redis:alpine
### Uncomment to enable REDIS persistance
    volumes:
      - ./redis:/data

Dockerイメージの準備

今回は特にMastodonをカスタマイズしたい気持ちはないのでprebuiltイメージを使った。完全に公式ガイドのとおり。 バージョンは image: tootsuite/mastodon:v2.5.0とした。

f:id:lacolaco:20180908233348p:plain

Mastodonセットアップ

次のコマンドを実行する。

docker-compose run --rm web bundle exec rake mastodon:setup

いろいろ聞かれる。聞かれるままにそのまま進めていけば、あとで .env.production にペーストする用の設定が出力される。 注意が必要なのは、メール送信のポート番号は587だとGCPにブロックされるので2525を使うこと。

最終的に、 .env.production は次のようになった。redisとかパスワードかけてなくて危ない気もするけど、そもそもGCEのファイアウォールで外から見えないので今回は無視している。 GCSを使うために手で編集する必要がある部分は2017年ごろの記事には書かれていないことが多い。

# Generated with mastodon:setup on 2018-09-07 00:46:28 UTC

LOCAL_DOMAIN=activitypub.lacolaco.net
SINGLE_USER_MODE=true
SECRET_KEY_BASE=(mastodon:setupで生成される)
OTP_SECRET=(mastodon:setupで生成される)
VAPID_PRIVATE_KEY=(mastodon:setupで生成される)
VAPID_PUBLIC_KEY=(mastodon:setupで生成される)
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
S3_ENABLED=true
S3_PROTOCOL=https
S3_BUCKET=(自分で作ったGCSのBucket)
S3_REGION=(自分で作ったGCSのBucketのリージョン)
S3_HOSTNAME=storage.googleapis.com
AWS_ACCESS_KEY_ID=(自分で作ったGCSの相互運用性APIキー)
AWS_SECRET_ACCESS_KEY=(自分で作ったGCSの相互運用性秘密鍵)
SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=2525
SMTP_LOGIN=(MailgunのDefault SMTP Login)
SMTP_PASSWORD=(MailgunのDefault Password)
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS=Mastodon <notifications@activitypub.lacolaco.net>

# 自動セットアップ後に手で追加する
S3_ENDPOINT=https://storage.googleapis.com/
S3_SIGNATURE_VERSION=s3

ここまでできたら一旦立ち上げる。

$ docker-compose up

エラーにならなければOK。Ctrl+cで一旦終了し、改めて $ docker-compose up -dでデーモン実行する。

nginx設定の追加

Mastodonの公式設定にしたがってnginxの設定をおこない、3000番ポートでホストされているMastodonを外部からアクセス可能にする。

documentation/Production-guide.md at master · tootsuite/documentation · GitHub

以上で https://activitypub.lacolaco.netdocker-compose up -dで動くMastodonに接続された。

ざっくりと構成図はこういう感じだと思う

f:id:lacolaco:20180909001617p:plain

苦しんだところ

  • 外部IPがエフェメラルになっていることに気づくのに時間がかかって、全然nginxが確認できなくてつらかった
  • 上記理由でLet's Encrypt失敗しまくるとレートリミットに引っかかった。最初は--stagingオプションをつけるとレートリミットがゆるくなるので実験しやすい
  • S3_ENDPOINTS3_SIGNATURE_VERSION に気づくのに時間がかかった。Mastodon自体は立ち上がるけどアイコン画像アップロードしようとしたら死ぬので気づくのが遅れた
  • アセットのプリコンパイルは長い

改善できそうなところ

  • StackDriverと繋いでみたのでモニタリングとアラートとかやってみたい
  • 1ヶ月どれくらいのコストになるかわかったらまたいろいろ調整したい
  • Mastodonのアップロードにもできるだけ追従していきたいので必要になったら手順を追う

Angular CDK drag-and-drop の紹介

こんにちは。

この記事ではAngular CDKの次期アップデートで提供される、 drag-and-drop 機能を紹介します。 執筆時点ではまだnpmパッケージとして公開されていないので、一般に利用できるまでにはもうしばらくかかりますが、 もし早く使いたい方は、次のコマンドで開発版ビルドをインストールしましょう。 なお、開発版ビルドですので自己責任でお願いします。

$ yarn add angular/cdk-builds

CDK drag-and-drop

drag-and-dropはその名のとおり、UI上でのドラッグアンドドロップ操作をサポートするものです。

@angular/cdk/drag-drop パッケージから提供される DragDropModule をインポートすると、次の2つのディレクティブ、コンポーネントが利用できます。

cdkDrag ディレクティブ

cdkDragディレクティブは、ドラッグされる要素を指定するディレクティブです。このディレクティブを付けられた要素は画面上で自由に位置を変えられます。

たとえば ng new 直後のテンプレートHTMLで、 li要素に cdkDragディレクティブを付与すると、次のようになります。 (わかりやすさのために li要素にCSSでスタイルを付与しています)

<ul>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
  </li>
</ul>

f:id:lacolaco:20180829200010g:plain

cdkDrag ディレクティブだけを使うと、何の制約もなく自由に移動することができました。

cdk-dropコンポーネント

このままでは動いて面白い以上の意味がないので、cdk-dropコンポーネントを使います。 cdk-dropコンポーネントは、cdkDragディレクティブをグルーピングし、動きに制限をつけて、限られた領域内でだけ移動できるようにします。

たとえば、ul要素の外側に <cdk-drop> コンポーネントを配置すると、ulの内部でだけ移動できるようになり、移動中は並び替えが行われるようになります。

<cdk-drop>
<ul>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
  </li>
  <li cdkDrag>
    <h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
  </li>
</ul>
</cdk-drop>

f:id:lacolaco:20180829200735g:plain

見てのとおり、 <cdk-drop>タグの内側でだけ並べ替えが行われるようになりましたが、ドロップしてしまうともとの状態に戻ります。 これはcdk-dropコンポーネントの仕様で、ドラッグアンドドロップが終了すると、その内部のcdkDragの順序は復元されます。 ただし、ドラッグアンドドロップ終了時にはdroppedイベントが発行されていて、このイベントをもとにコンポーネント側からデータモデルを更新することで、 ドラッグアンドドロップによる並べ替えを実現できます。

並べ替え

並べ替えをおこなうためには、リストをコンポーネント側で管理する必要があります。これまでは適当なli要素を使っていましたが、AppComponentに次のようなlistプロパティをもたせます。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  list = ['まぐろ', 'サーモン', 'えび'];
}

そしてテンプレートを次のように変更し、listプロパティの要素を繰り返し表示します。先程までと同じように、<cdk-drop>タグのなかで繰り返される並べ替えの対象にcdkDragディレクティブを付与します。

<h2>好きなネタ</h2>
<cdk-drop [data]="list" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of list" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

ポイントは <cdk-drop [data]="list" (dropped)="drop($event)"> です。[data]プロパティには並べ替えの対象となるデータモデルを渡します。 次に、(dropped)="drop($event)"では、droppedイベントハンドラdropメソッドを呼び出しています。 dropメソッドは次のように記述します。

import {
  CdkDragDrop,
  moveItemInArray,
} from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  list = ['まぐろ', 'サーモン', 'えび'];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

CdkDragDrop<string[]>は、dropイベントの引数の型です。ジェネリックstring[]は並べ替え対象の配列の型を表しています。

moveItemInArray関数は、基本的な配列の並べ替えを行ってくれるCDKの機能です。中身は単なるJavaScriptの配列の並べ替えですが、Angularチームによる実装にまかせておくのが安心だと思います。

export function moveItemInArray<T = any>(array: T[], fromIndex: number, toIndex: number): void {
  const from = clamp(fromIndex, array.length - 1);
  const to = clamp(toIndex, array.length - 1);

  if (from === to) {
    return;
  }

  const target = array[from];
  const delta = to < from ? -1 : 1;

  for (let i = from; i !== to; i += delta) {
    array[i] = array[i + delta];
  }

  array[to] = target;
}

f:id:lacolaco:20180829210528g:plain

これで、dropイベントによって配列を並べ替えられるようになりました。

複数のcdk-dropでグルーピングをおこなう

複数のグループを跨いだ並べ替えも可能です。先程のAppComponentを次のように変更します。listプロパティをlikeプロパティに改名し、新しくunlikeプロパティを追加します。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  like = ['まぐろ', 'サーモン', 'えび'];
  unlike = ['数の子', 'たくあん'];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(
      event.container.data,
      event.previousIndex,
      event.currentIndex
    );
  }
}

テンプレートでは、likeプロパティとunlikeプロパティの両方で同じようにcdk-dropによる並べ替えができるようにします。

<h2>好きなネタ</h2>
<cdk-drop [data]="like" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of like" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

<h2>好きじゃないネタ</h2>
<cdk-drop [data]="unlike" (dropped)="drop($event)">
  <ul>
    <li *ngFor="let item of unlike" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

ここまでは先程と変わりません。ここから、この2つのグループを結合します。 並べ替えグループを結合するには、cdk-dropconnectToプロパティを使います。このプロパティに結合の対象となるグループの参照を渡します。

<h2>好きなネタ</h2>
<cdk-drop #dropLike [data]="like" (dropped)="drop($event)" [connectedTo]="[dropUnlike]">
  <ul>
    <li *ngFor="let item of like" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

<h2>好きじゃないネタ</h2>
<cdk-drop #dropUnlike [data]="unlike" (dropped)="drop($event)" [connectedTo]="[dropLike]">
  <ul>
    <li *ngFor="let item of unlike" cdkDrag>
      <h2>{{item}}</h2>
    </li>
  </ul>
</cdk-drop>

さらに、AppComponentのdropメソッドで、グループを跨いでいた場合の処理を追加します。 この場合も汎用的なグループ移動機能をサポートするtransferArrayItem関数が提供されているので、それを使います。

グループを跨いだ移動かどうかは event.previousContainerevent.container を比較して判定できます。 次のように書けば、一致する場合は配列内での移動を、一致しない場合はグループを越えた移動をおこないます。

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    }
  }

f:id:lacolaco:20180829212028g:plain

これで複数のグループを跨いだドラッグアンドドロップによる並べ替えができるようになりました。

CSSによるスタイリング

最後に、CDKのdrag-dropが提供するスタイリングのためのCSSクラスを紹介します。

.cdk-drag.placeholder

.cdk-drag.placeholderクラスは、ドラッグされている要素のプレースホルダ部分につけられるCSSクラスです。たとえばここを次のように見えなくすることで自然な挿入を演出できます。

.cdk-drag-placeholder {
  opacity: 0;
}

f:id:lacolaco:20180829212513g:plain

.cdk-drag-preview

.cdk-drag-previewクラスは、ドラッグされている要素のプレビュー部分(動かしている部分)につけられるCSSクラスです。たとえば次のように半透明にすることで自然な挿入を演出できます。

.cdk-drag-preview {
  box-sizing: border-box;
  opacity: 0.5;
}

f:id:lacolaco:20180829212904g:plain

この他にもいくつかCSSクラスがあります。詳しくはスタイリングに関するドキュメントを参照してください。

まとめ

CDKのアップデートはAngular v7のリリースと合わせておこなわれるだろうと見られています。 楽しみに待ちましょう。