ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Visual Studio CodeのExtensionを作り、ローカルのWSLで実行する

Visual Studio Code拡張機能(Extension)を作り、ローカルで実行してみました。
手順については公式や様々なサイトに書かれていますが、自分がやったことを纏めておきます。

やったこと

  • Extensionをローカルで作成する
  • ローカルで動かしてみる(Windows)
  • packageを作り、WSLで動かしてみる

Extensionをローカルで作成する

こちらは公式のチュートリアル通りに雛形をつくり、任意の機能を追加しました。
Your First Extension | Visual Studio Code Extension API

ただ「generator-code」はグローバル領域ではなくローカルにインストールしたので、いくつかコマンドが異なりました。

インストール

$ npm install yo generator-code

プロジェクト作成

$ npx yo code

ローカルで動かしてみる(Windows)

私はWindowsで開発したのですが、ローカルで動かす場合は作成したプロジェクトのフォルダ毎
「%USERPROFILE%.vscode\extensions」にコピーすることで動きました。

MacLinuxであれば「$HOME/.vscode/extensions」にコピーすれば動くようです。

※参考
Visual Studio Code の拡張機能を作ってみた - Qiita

packageを作り、WSLで動かしてみる

Visual Studio CodeWindows上で実行する際には、先に書いたようにプロジェクトフォルダをコピーすることで動かすことはできたのですが
WSL上で動かすにはフォルダのコピーでは出来ませんでした。。。

「vsce」というVisual Studio Code Extensions のコマンドラインツールを使い、公開用のファイル(.vsix)を作り、Visual Studio CodeのWSLモードでExtensionsとしてインストールする必要があります。

以下、簡単な作業の流れとなります。

Azure DevOpsにアカウントを作り「Personal Access Token」を取得する。

Create a new organization, project collection - Azure DevOps | Microsoft Docs
Publishing Extensions | Visual Studio Code Extension API

この辺りを参考に行う必要があります。注意点としては「Personal Access Token」の「Organization」を「All accessible organizations」とする必要があります。
(これをしないと次の「$ npx vsce create-publisher」で401エラーなどになります)

vsceのインストール

これもグローバル領域ではなくローカルにインストールします。

$ npm install vsce
$ npx vsce --version

packageを作り、インストール

vsceを使いpackageを作ります。package作成時に作られる公開用ファイル(.vsix)をVisual Studio CodeのExtensionとしてインストールし、WSL上で使ってみます。

Publisherの登録

Extensionを公開しなくても必要なようです。

$ npx vsce create-publisher YOUR_PUBLISHER_NAME
package.jsonの「publisher」を定義

package.jsonの「publisher」項目を追加し、先のPublisherの登録時に使ったPublisher名を記載します。

packageの作成

以下を実行すると公開用ファイル(.vsix)が作成され、フルパスが表示されます。

$ npx vsce package
Extensionのインストール

Visual Studio CodeをWSLモードで起動し、 Extensionsパネルの右上の「...」より「Install from VSIX...」から、先に作成した公開用ファイル(.vsix)を読み込みます。

aws-cdkを触ってみた

少し(かなり?)遅れた感はありますが、aws-cdkを触ってみました。

https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/getting_started.html#hello_world_tutorial
こちらの公式の「Hello World Tutorial」をほぼなぞっただけですが、いくつか独自にアレンジしたところもあるので、メモ書き程度に残しておきます。

aws-cdkのインストールはローカルにインストールからnpxを使ったインストールに変更しました

変更したところ

  • (TypeScriptでやったのだが) aws-cdkのモジュールはnpxにインストールした
  • Package.jsonの「scripts」にコマンドを用意してaws-cdkを各操作を出来るようにした
  • プログラムで構築できることを検証するため、動的にS3のバケット名を生成してみた

以下、変更したところの詳細です。

aws-cdkを使ってのプロジェクト作成

Hello World Tutorial」ではaws-cdkをグローバル領域にインストールしていますが、バージョンアップなどされることも考慮して、今回はnpxでプロジェクトを作成することにしました。
プロジェクト名は「hello-cdk-npx」とし、プロジェクト名と同名のフォルダを作成して以下のように実行します。

$ mkdir hello-cdk-npx
$ cd hello-cdk-npx
$ npx cdk init app --language=typescript
$ npx cdk --version

package.json

良く使うコマンドは全てpackage.jsonの「Scripts」に纏めました。
こうすれば「$ npm run deploy」のように「$ npm run ~」で統一して実行できます。以下、今回作成したpackage.jsonの「scripts」です。

  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk",
    "synth": "npx cdk synth",
    "diff": "npx cdk diff",
    "deploy": "npx cdk deploy",
    "destroy": "npx cdk destroy"
  },

動的にS3のバケット名を生成

TypeScriptのプログラムで、日付を動的に取得してバケット名として指定してみました。
lib/hello-cdk-npx-stack.ts は以下のようになりました。

import core = require('@aws-cdk/core');
import s3 = require('@aws-cdk/aws-s3');

export class HelloCdkNpxStack extends core.Stack {
  constructor(scope: core.Construct, id: string, props?: core.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    const now = new Date();
    const yyyymmdd = now.getFullYear().toString() + (now.getMonth()+1).toString() + now.getDate().toString();
    const myBucketName = "hello-cdk-" + yyyymmdd;

    new s3.Bucket(this, 'MyFirstBucket', {
      versioned: true,
      bucketName: myBucketName
    });
  }
}

バケット名を指定している以外は、以下の「Hello World Tutorial」から変わっていないです。
https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/getting_started.html#hello_world_tutorial

実行

ここまで作成すれば、Makefileを使用して以下のコマンドでビルド、デプロイ、破棄を行うことができます。

$ npm run build
$ npm run synth
$ npm run deploy
$ npm run destroy

以上です。

AnsibleでEC2にSSH接続してファイルをアップロードする

仕事でAnsibleを少し触る機会があったのですが、初見でさっぱり分からなかったので
復習を兼ねてインストールからやってみました。

タイトルにも書きましたが、やったことは

  • Ansibleにて既存のEC2にSSH接続する
  • SSH経由でEC2内にフォルダを作る
  • 同じくSSH経由でファイルをアップロードする

です。

以下、やったことのメモとなります。

Ansibleのインストール

ローカルマシンにAnsibleをインストールします。私はWSLのUbuntuを使ったので、公式の以下の手順を参考にしました。
https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#latest-releases-via-apt-ubuntu

Ansibleはエージェントレスで動き、先にも書いたようにSSHで接続して操作を行うため、EC2に何かをインストールする必要はありません。

プロジェクト作成

ローカルの任意のフォルダをプロジェクトフォルダとし、以下のファイルを用意します。

  • ssh_config ・・・ SSHでの接続先を定義
  • inventory.ini ・・・ ssh_configで定義した接続先を指定
  • ansible.cnf ・・・ SSHの接続オプションを指定
  • sample.yml ・・・ 実行するタスク(今回はフォルダの作成、ファイルのアップロード)を定義
  • test.txt ・・・ アップロードするファイル

以下、各ファイルについてです。

ssh_config

SSHの接続先を定義します。今回はEC2にキーペアで接続しました。
こんな感じとなります。(~/.ssh/configを使ったSSH接続と同じ内容です)

Host my-ec2
  HostName ec2-xx-xxx-xxx-xxx.ap-northeast-1.compute.amazonaws.com
  User ec2-user
  IdentityFile your_pem_fullpath.pem
  ServerAliveInterval 15

用意したらこのファイル単体でSSH接続できるかを確認します。

$ ssh -F ssh_config my-ec2

inventory.ini

ssh_configで定義した接続先を指定します。

[mysection]
my-ec2

ansible.cnf

SSHの接続オプションを指定します。

ssh_args = -F ssh_config

sample.yml

実行するタスクを定義します。今回は

  • /home/ec2-user に workフォルダを作成
  • workフォルダにローカルのtext.txtをアップロード

を行っています。

- hosts: my-ec2
  tasks:
    - name: make dir
      file:
        path: /home/ec2-user/work
        state: directory
        mode: 0755
    - name: file copy
      copy:
        src: ./test.txt
        dest: /home/ec2-user/work/
        mode: 0755

test.txt

特にこだわりはないです。適当なテキストファイルを用意します。

実行

ここまで用意したら実行してみます。
AnsibleはDry Runが可能なので、まずはDry Runで確認してみます。

$ ansible-playbook ./sample.yml -i inventory.ini --check

「--check」を付けることでDry Runとなります。
本番実行は以下のようになります。

$ ansible-playbook ./sample.yml -i inventory.ini

「Go言語によるWebアプリケーション開発」の感想など

O'Reilly Japan - Go言語によるWebアプリケーション開発をのんびりと写経してきて、一通り終わりました。
全ての章のものではないですが、写経と読書中にメモした感想などを備忘録として残しておきたいと思います。

全体の感想

「Go言語によるWebアプリケーション開発」というタイトルですが、
本書に書かれているプログラムはWebアプリケーションとコンソールアプリケーションの両方になります。

どちらもGo言語での開発に即、実践可能なノウハウが書かれているので
「Webアプリケーション開発」に限らず、Go言語でのあらゆるアプリケーション開発に従事する人におススメできるかと思います。
(例え小さなツールを作るだけの場合などでも。)

また、ある程度Go言語が分かった状態で始めた方が良さそうだとも感じました。
goroutineやスライスなど、Go言語での開発でよく使う機能が取り上げられていますが、これらを知らないで本書を読むと、唐突に出来てたような印象を受けそうだと思います。

各章の「まとめ」には、書かれている内容が「まと」められているので、後日必要になった時に読み返す箇所を探すのに役立ちそうです。

3章

  • インターフェースを使った実装の切り替え
  • 型の埋め込み(type embedding)とインターフェース

について取り扱われており、大変参考になりました。

インターフェースの定義と具象の実装と切り替え、これに型の埋め込みを組み合わせて
どのように使うかの具体例が示されてていることが参考になります。

4章

一転してコンソールアプリで、小さなアプリを組み合わせ、それぞれのアウトプットを標準出力で繋ぐことで目的を達成している具体例です。
小さなアプリの組み合わせているところは、「UNIXという考え方」で書かれていた考えに近い感じがします。
この章も、ツールやバッチを作る時などにアプリ構成を考えるのに参考になるかと思います。

5章

分散システム。システムを複数のサブシステム(今でいうマイクロサービスに近い?)に分割することによって、サブシステムごとにスケールアウトできるメリットについて言及しています。
また複数のgoroutineの使用例、mapとlockを使って複数goroutineでmapを破壊しない方法、チャネルを使ったシグナルの送受信、Graceful Shutdownについても記載されています。

6章

5章で作ったシステムのデータをWebで公開する仕組みを作成します。
独立したAPIと、Webサイトをそれぞれ分ける仕組みです。
APIの方は実案件では何らかのフレームワーク使うかな、という感じはしました。

7章

WebAPIの作成。ここまでやってきたことの応用例という感じです。

8章

コンソールアプリでファイルのバックアップシステムを作成します。
これもここまでやってきたことの応用例という感じです。


以上です。

watermillを触ってみた

前回に引き続きメッセージを扱うライブラリを触ってみました。
github.com

「watermill」というもので、メッセージストリームを効率的に扱うためことを目的としているようです。
手始めにREADMEにある「Getting Started Guide」(https://watermill.io/docs/getting-started/)から始めてみたのですが、一番最初にあるサンプルを改変してみたので上げておきたいと思います。

ポイントとしては

  • メッセージを受信するためのChannelを複数(2つ)最初に定義する(messagesA、messagesB)
  • それぞれのChannel(messagesA、messagesB)にはトピック名を定義する
  • メッセージを受信するreceiveMessages()をgoroutineとして実行する
  • publishMessages()でトピック名を指定してメッセージを送信する
  • 最後に終了のログ(finish)も出している

点です。ソースは以下のようになります。
GitHub - SrcHndWng/go-watermill-simple-sample

package main

import (
	"context"
	"log"
	"time"

	"github.com/ThreeDotsLabs/watermill"
	"github.com/ThreeDotsLabs/watermill/message"
	"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
)

func main() {
	pubSub := gochannel.NewGoChannel(
		gochannel.Config{},
		watermill.NewStdLogger(false, false),
	)

	messagesA, err := pubSub.Subscribe(context.Background(), "example.topic.A")
	if err != nil {
		panic(err)
	}
	messagesB, err := pubSub.Subscribe(context.Background(), "example.topic.B")
	if err != nil {
		panic(err)
	}

	go receiveMessages(messagesA)
	go receiveMessages(messagesB)

	publishMessages(pubSub)

	log.Println("finish!")
}

func publishMessages(pubSub *gochannel.GoChannel) error {
	publishMessage := func(publisher message.Publisher, topic string, msgStr string) error {
		msg := message.NewMessage(watermill.NewUUID(), []byte(msgStr))
		return publisher.Publish(topic, msg)
	}

	for i := 0; i < 5; i++ {
		if err := publishMessage(pubSub, "example.topic.A", "Hello, message A!"); err != nil {
			return err
		}
		if err := publishMessage(pubSub, "example.topic.B", "Hello, message B!"); err != nil {
			return err
		}
		time.Sleep(time.Second)
	}
	return nil
}

func receiveMessages(messages <-chan *message.Message) {
	for msg := range messages {
		log.Printf("received message: %s, payload: %s", msg.UUID, string(msg.Payload))
		msg.Ack() // Ack sends message's acknowledgement.
	}
}

動かしてみると分かりますが、A・Bのメッセージが受信される順番は、送信した順になるとは限らないようです。

メッセージを受信したら、「Ack()」で受信済である旨を通知することもポイントかと思います。

これ以外は、goroutineを使っていること、pushish~、subscribe~など一般的な用語がそのままメソッド名になっているため、分かりやすいライブラリかなと思います。

RxGoを触ってみた

RxGoというReactiveをGolangで実現するためのライブラリを触ってみました。
READMEのサンプルが分かりやすく、それを見るのがいいとは思いますが、自分の理解のために多少改変してみたりもしたのでサンプルとして上げておきます。

GitHub - SrcHndWng/go-rx-sample

helloworld

一番シンプルなサンプルで、READMEの"Simple Usage"を改修したものになります。
READMEのサンプルとの違いは、time.Sleepを使って1秒毎にメッセージを送信するようにした点です。
実案件ではtime.Sleepの他、何らかのイベントを切っ掛けにメッセージを送信することもあるかと思いますが、そのような使用を想定しました。

コメントに書いたように "not error raise. print 'Done!'"の行を実行すると全てを実行して"Done!"の表示まで実行します。
コメントアウトしてある"error sample"以下のif~else文を実行するようにし、 "not error raise.・・・"をコメントアウトするとobserverのErrHandlerにてエラーがhandleされます。

package main

import (
	"fmt"
	"time"

	"github.com/reactivex/rxgo/observable"
	"github.com/reactivex/rxgo/observer"
)

func main() {
	watcher := observer.Observer{
		NextHandler: func(item interface{}) {
			fmt.Printf("handled: %v\n", item)
		},

		ErrHandler: func(err error) {
			fmt.Printf("error: %v\n", err)
		},

		DoneHandler: func() {
			fmt.Println("Done!")
		},
	}

	message := make(chan interface{})
	source := observable.Observable(message)
	sub := source.Subscribe(watcher)

	go func() {
		cnt := 0
		for {
			if cnt > 4 {
				break
			}
			// error sample
			// if cnt == 3 {
			// 	message <- errors.New("some error")
			// } else {
			// 	message <- fmt.Sprintf("Hello, cnt = %d", cnt)
			// }
			message <- fmt.Sprintf("Hello, cnt = %d", cnt) // not error raise. print 'Done!'
			cnt++
			time.Sleep(1 * time.Second)
		}
		close(message)
	}()

	<-sub
}

grouping

READMEの"Simple Usage"の次のサンプルを改修したものになり、handlerを observer.Newでグループ化したものです。
ただ処理概要は先の"helloworld"と同じにしてあるので、比較してみてください。

package main

import (
	"fmt"
	"time"

	"github.com/reactivex/rxgo/handlers"
	"github.com/reactivex/rxgo/observable"
	"github.com/reactivex/rxgo/observer"
)

func main() {
	onNext := handlers.NextFunc(func(item interface{}) {
		fmt.Printf("handled: %v\n", item)
	})

	onError := handlers.ErrFunc(func(err error) {
		fmt.Printf("error: %v\n", err)
	})

	onDone := handlers.DoneFunc(func() {
		fmt.Println("Done!")
	})

	watcher := observer.New(onNext, onError, onDone)

	message := make(chan interface{})
	source := observable.Observable(message)
	sub := source.Subscribe(watcher)

	go func() {
		cnt := 0
		for {
			if cnt > 4 {
				break
			}
			// error sample
			// if cnt == 3 {
			// 	message <- errors.New("some error")
			// } else {
			// 	message <- fmt.Sprintf("Hello, cnt = %d", cnt)
			// }
			message <- fmt.Sprintf("Hello, cnt = %d", cnt) // not error raise. print 'Done!'
			cnt++
			time.Sleep(1 * time.Second)
		}
		close(message)
	}()

	<-sub
}

observable

READMEの"Recap"欄に「In RxGo, it's useful to think of Observable and Connectable as channels with additional ability to Subscribe handlers.」とあります。
これもREADMEのサンプルを改修したものですが

  • DBへの登録などの代わりに標準出力するようにした
  • エラーについても実装した

などを変えています。

実行してみると"f1"と"f2"の出力順序が逆になりますが、このことからも先に引用した"Recap"欄の旨が分かるかと思います。

package main

import (
	"errors"
	"fmt"
	"time"

	"github.com/reactivex/rxgo/handlers"
	"github.com/reactivex/rxgo/observable"
)

func main() {
	f1 := func() interface{} {

		// Simulate a blocking I/O
		time.Sleep(2 * time.Second)
		return 1
	}

	f2 := func() interface{} {

		// Simulate a blocking I/O
		time.Sleep(time.Second)
		return 2
	}

	f3 := func() interface{} {

		// Simulate a blocking I/O
		time.Sleep(3 * time.Second)
		return 3
	}

	f4 := func() interface{} {

		// Simulate a blocking I/O
		time.Sleep(4 * time.Second)
		return errors.New("some error")
	}

	onNext := handlers.NextFunc(func(v interface{}) {
		fmt.Printf("handled, v = %v\n", v)
	})

	wait := observable.Start(f1, f2, f3, f4).Subscribe(onNext)
	sub := <-wait

	if err := sub.Err(); err != nil {
		fmt.Printf("error raise. err = %v\n", err)
	}

	fmt.Println("finish")
}

GolangでWebAssemblyの復習をした

GolangでWebAssemblyの復習するため、いくつかサイトを見て写経してみました。
一年以上ぶり、かつ実行環境が異なったので、改めて気づいたことがあったので、メモとして残しておきます。

実行環境について

先に書いておくと、goenv、Visual Studio Code 固有のポイントに気づいた感じです。

気づいたポイント

ビルドコマンドはGOOS、GOARCHの指定も含めて一行で行う

https://github.com/golang/go/wiki/WebAssembly#getting-started
にあるように、以下のように一行のコマンドでビルドしないと、ブラウザからの実行時にエラーとなりました。
正しいwasm形式にビルドできないようです。

$ GOOS=js GOARCH=wasm go build -o main.wasm

wasm_exec.js、テンプレートのHTMLのコピー

サイトによってwasm_exec.jsのコピーのコマンドや、テンプレートのHTMLは自作するなどの違いがありましたが、
両方とも「GOROOT」環境変数を使ってコピーし、HTMLはファイル名を変更するのが効率がいいと思いました。

https://blog.gopheracademy.com/advent-2018/go-in-the-browser/
からの引用です。

$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.html" .
$ mv wasm_exec.html index.html

goexecの実行について

ローカルでWebサーバを動かすのに「goexec」を使う例がありました。
https://github.com/shurcooL/goexec

「go get」で取得して「goexec」コマンドをそのまま実行しても、私の環境ではgoexecが見つからないでエラーとなりました。
goenvで複数のバージョンのgoを入れているためかと思われます。
以下のようにパスを含めたコマンドで実行できました。

$GOPATH/bin/goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'

could not import syscall/js

syscall/jsのimportでVisual Studio Code上で「could not import syscall/js」というエラーとなりました。

https://github.com/Microsoft/vscode-go/issues/1874
こちらのissueにあるように、Visual Studio CodeのWorkspaceのsettings.jsonを以下のようにしたら解決しました。

{
    "go.toolsEnvVars": {
        "GOOS": "js",
        "GOARCH": "wasm",
        "GOROOT": "/usr/local/go",
        "GOPATH": "/home/corey/src/goprojects"
    }
}

以上です。