LattePandaを購入

秋月電子でLattePandaを買いました。おすすめされていたACアダプターとヒートシンクも一緒に購入。

f:id:LocaQ:20171118160553p:plain:w700

LattePandaは5V/2Aの電流を流せる電源が必要ですが、一緒に買ったACアダプターは5V/3Aのもので余裕があります。ヒートシンクはLattePanda公式のもののようです。

以下、セットアップでやったことを書きます。セットアップで必要なマウス・キーボード・HDMIケーブルは持っていたものを使います。

起動

付属のマニュアルに書いてある起動方法が少し変わっていました。

  1. ACアダプターのUSB端子をLattePandaに接続する
  2. 赤いLEDが光った後、消えるまで待つ
  3. LattePandaの電源スイッチを押す

1ではLattePandaの初期化を行うようです。

3で電源スイッチを押すと再び赤いLEDが光ってWindowsの起動が始まります。

Windows Update

まずはWindows 10のPCと同様にWindows Updateを実行しました。何回か実行するので結構時間がかかります。

日本語化

初期状態のLattePandaのWindows 10は英語なので下のサイトを見て日本語化しました。

外国語版 Windows 10 を日本語化する

TightVNCのインストール

Windows 10 HomeはWindowsリモートデスクトップのホストになれないので、別の方法でリモートデスクトップをできるようにする必要があります。私はLattePanda公式のドキュメントに書かれている通りにTightVNCをインストールしました。

Drivers - LattePanda Docs

クライアントのPCにもTightVNCのビューワーをインストールします。

Wi-Fiのセットアップ

LattePandaはWi-Fiに対応していますが2.4GHz帯を使うものです。しかし通信速度が欲しかったので、標準のWi-Fiアダプターではなく持っていたELECOMの5GHz専用のWi-Fiアダプターを使います。

11ac 867Mbps USB3.0小型無線LANアダプター(子機) - WDC-867SU3SBK

LattePandaの3つのUSBの内1つがUSB3.0に対応しているのでそこに接続します。

ここで注意するのがドライバーのセットアップです。

ELECOMのサイトでWindows 10用のものがダウンロードできますが、Windows 10標準のドライバーで接続できるのでインストールは不要です。インストールするとWindowsに認識されなくなって使えなくなります。なので、ELECOMのサイトのドライバーはインストールせずにアダプターをLattePandaに接続します。接続すると自動でドライバーがインストールされます。

インストールが完了したら、Windows 10標準の方法でWifiのセットアップをします。

不要ソフトのアンインストール

32GBしかストレージがないのでゲームなどの不要なソフトをアンインストールします。

ヒートシンク

LattePandaは結構熱くなるというのは聞いていたのでヒートシンクを載せる前にアップデート中のLattePandaの金属部分に指で触ってみたら、確かにずっと触っていられない程度には熱かったです。

で、ヒートシンクを載せる時に最初は表面の一番面積の広い部分(LattePandaのキャラクターが印刷されている所)に5個載せようとしてたんですが、一応公式サイトで調べたら表面の小さい金属部分に1個、裏面に4個載せてました。

f:id:LocaQ:20171118172722j:plain f:id:LocaQ:20171118172730j:plain

(2枚の画像は右のサイトから引用:Pure Copper Heatsink Pack for LattePanda (5 pcs) – LattePanda

思い込みで載せなくてよかったです。

でもヒートシンクなしの状態でどこが熱いか触って確かめてみたら、表面は公式通りの場所より広い方が熱かった気がしたのでそっちに載せました。

f:id:LocaQ:20171118160741p:plain:w700 f:id:LocaQ:20171118160710p:plain:w700

載せた後に触ってみたら表面のヒートシンクより横の狭い金属部分の方が熱かったので公式通りに載せた方がよかったかもしれません。

v6プラスとPPPoE(IPv4)の併用

PPPoE(IPv4)の併用

前回の記事のように設定すればPCやスマートフォンからはインターネットに問題なく接続できますが、PS4ボイスチャットが使えなくなりました。フレッツはマルチセッションができるので、もう1台ルーターを使ってPPPoE(IPv4)を使った接続を作成して回避します。

ネットワーク構成は下の図のようにします。中央のWXY-1750DHP2が今回購入したv6プラス対応のルーターで、PCやスマートフォンなどはこれに接続します。そして右のWSR-600DHPは同じくBUFFALOのルーターで、v6プラス利用前まで使っていたものです。これとPS4を接続します。

また、WXR-1750DHP2のLANポートとWSR-600DHPのWANポートをLANケーブルでつなぎます。

f:id:LocaQ:20170828004612p:plain:w800

WXR-1750DHP2の設定

v6プラス対応ルーターの設定です。「PPPoEパススルー」の「使用する」にチェックします。

f:id:LocaQ:20170828004943p:plain:w800

また、このルーターのネットワークは仮に192.168.11.0/24とします。

WSR-600DHPの設定

IPv4接続用のルーターの設定です。

1.「IPアドレス取得方法」で「PPPoEクライアント機能を使用する」を選択する。

f:id:LocaQ:20170828005552p:plain:w800

2.「PPPoE」で@niftyから送付されたIDとパスワードで接続する設定を作成する。

f:id:LocaQ:20170828005604p:plain:w800

また、このルーターはネットワークを分けます。ネットワークは仮に192.168.12.0/24とします。

まとめ

これでPS4はv6プラスを使わずにインターネットに接続できるのでボイスチャットも使えるようになりました。インターネット接続の速度はv6プラス利用前と同じ状況になりますが、今のところゲームをしている時に問題は出ていません。また、プロバイダが変わった影響かボイスチャットで相手の音声が途切れ途切れになる問題が解決しました。

f:id:LocaQ:20170828010041j:plain:w800

v6プラスを使ってインターネット接続速度を改善する

v6プラス利用前に起こっていた問題

家のインターネット回線の速度が夜に数百kbpsと劇的に遅くなり、ニコニコ動画Youtubeの動画を再生するのに動画時間の何倍も待ったりWebサイトやツイッターの画像の表示も数十秒待つような状況になっていました。業を煮やして調べると、NURO光やv6プラスにすると回線速度の低下がなくなるらしいので対策をすることにしました。

フレッツ光 2017年 急激に遅い評判増える その原因は? | 実際の速度は800M以上! NURO光の実測や評判は?

夜インターネット回線が遅いのを改善してみた(@nifty/フレッツ光/v6プラス/WXR-1900DHP): わかぶろぐ

私は賃貸マンションに住んでいて、インターネット回線はフレッツ光のマンションタイプ(100Mbps)、プロバイダはYahoo! BBでした。NURO光にするには回線を引けるかどうか大家の方と相談が必要だったり時間と手間がかかりそうだったので、自分だけで手続きができるv6プラスを申し込んでみることにしました。

フレッツの回線タイプによってはv6プラスは使えないようですが、自分のマンションが対応しているタイプ(書類によるとNマンション1Bですが、下のサイトのどれに該当するかはよく分かりません。)は対応してました。

サービス内容|フレッツ・v6オプション|フレッツ光公式|NTT東日本|インターネット接続ならフレッツ光

v6プラスについて

フレッツのIPv4は契約者数が多い上に装置で最大200Mbpsに制限されていて通信量が多くなる夜に速度低下が起こるようで、それがv6プラスで回避できるようです。

【v6プラスとは?】遅いネット回線が高速に!対応プロバイダと市販ルーター機器のまとめ - 踊るびあほりっく

v6プラス | サービス紹介 | JPNE | 日本ネットワークイネイブラー株式会社

また、v6プラスにすると固定IPアドレスを使えなくなったり特定のポートを使う通信ができなくなるので自宅サーバーや特定ポートを使う一部のゲームに影響があるようです。とは言え1契約でIPv4のセッションも同時に張ることができるので、v6プラスを使っても大丈夫だと思います。私はPS4IPv4を使ってインターネットに接続できるようにしました。

プロバイダとの契約

v6プラスに対応しているプロバイダはいくつかありますが私は@niftyにしました。v6プラスオプションは無料で利用できます。

v6プラスのご案内 : @nifty

7/10に申し込んで7/12には書類が郵送されてきました。ただ、よく分からないのは7/12にはルーターの設定をしてIPv4が使えるようになったと思ってたんですが、@niftyの課金開始日が8/1でした。もしかしたらルーターYahoo! BBの設定が残っていてインターネットに接続できていたのかもしれません。

8/3に「IPv6接続オプション」の申し込みが完了したというメールが来て、v6プラスが使えるようになりました。

v6プラス対応のルーターの準備

v6プラスを使うには対応するルーターが必要です。v6プラス対応のホームゲートウェイひかり電話対応ルーター)か、市販のv6プラス対応ルーターを用意する必要があります。私はホームゲートウェイを使っていなかったので、BUFFALOのWXR-1750DHP2というルーターを購入しました。

buffalo.jp

BUFFALOのv6プラス対応ルーターはサイトに一覧があります。

IPv6接続動作確認済みサービス/機器一覧 | BUFFALO バッファロー

ルーターの設定

v6プラスを使ってインターネットに接続するようにルーターの設定をします。ブラウザでルーターの設定画面にログインした後、

1.「IPアドレス取得方法」で「インターネット@スタートを行う」を選択する。

f:id:LocaQ:20170827220918p:plain:w800

2.PPPoEの設定がもしあれば削除する。

f:id:LocaQ:20170827220928p:plain:w800

3.「IPv6接続方法」で「NDプロキシを使用する」を選択する。

f:id:LocaQ:20170828232509p:plain:w800

当初は1と2だけ行っていましたが深夜0:30頃に回線が数分間切断されるという現象が発生していました。対策として3の設定を行うと解決します。

※2017/8/28追記:当初は3で「IPv6パススルーを使用する」を選択していましたが、セキュリティ上危険だと指摘を頂いたので「NDプロキシを使用する」を選択するように変更しました。

ルーターの設定ができたらv6プラスがちゃんと使えるか下のサイトで確認します。

IPv4/IPv6接続判定ツール

「v6プラスのインターネットアクセス(v6プラス用)」の試験でOKが表示されていれば成功です。

f:id:LocaQ:20170827230617p:plain:w600

感想

v6プラスにする前は速くて20Mbps程度、夜は最悪1Mbpsを下回る状況でしたが、v6プラスを使い始めたら夜でも60Mbps以上出るようになり最初に書いた問題も解決しました。最大100Mbpsの回線契約でこれだけ速度が出るのは驚きです。

また、これくらい速度が出るとYoutube4K解像度・60FPSの動画も、ページを表示してすぐに再生を開始しても止まることなく視聴できます。

プロバイダを変更する手間とルーターの購入が必要でしたが、快適にインターネットを使えるようになったので満足してます。

Dockerのビルドのレスポンスを整形して出力する

F#でDockerイメージをビルドする - locabloではレスポンスをそのまま出力していたのでパースして出力できるようにしました。

F#っぽいコードにならなかったのが残念…

let formatResponse line =
    let root = JObject.Parse(line)
    let hasProp key =
        root.Properties()
        |> Seq.map (fun x -> x.Name)
        |> Seq.contains key
    let valueOf (key : string) = string root.[key]

    let newLine = System.Environment.NewLine
    let builder = new System.Text.StringBuilder()
    if hasProp "id" then
        builder.AppendFormat("{0}: ", valueOf "id") |> ignore
    if hasProp "progress" then
        builder.AppendFormat("{0} {1}{2}", valueOf "status", valueOf "progress", newLine) |> ignore
    elif hasProp "stream" then
        builder.AppendFormat("{0}", valueOf "stream") |> ignore
    elif hasProp "status" then
        builder.AppendFormat("{0}{1}", valueOf "status", newLine) |> ignore
    else
        builder.Append(line) |> ignore

    builder.ToString()

let outputBuildResponse (responseStream : Stream) : unit =
    seq {
        use reader = new StreamReader(responseStream, Text.Encoding.UTF8)
        while not reader.EndOfStream do
            yield reader.ReadLine()
    }
    |> Seq.map formatResponse
    |> Seq.iter (printf "%s")

前回の記事の出力がこれで、

{"stream":"Step 1/2 : FROM busybox\n"}
{"status":"Pulling from library/busybox","id":"latest"}
{"status":"Pulling fs layer","progressDetail":{},"id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":16384,"total":699311},"progress":"[=\u003e                                                 ] 16.38 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":260087,"total":699311},"progress":"[==================\u003e                                ] 260.1 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":424839,"total":699311},"progress":"[==============================\u003e                    ] 424.8 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":490375,"total":699311},"progress":"[===================================\u003e               ] 490.4 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":539527,"total":699311},"progress":"[======================================\u003e            ] 539.5 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":621447,"total":699311},"progress":"[============================================\u003e      ] 621.4 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Verifying Checksum","progressDetail":{},"id":"1cae461a1479"}
{"status":"Download complete","progressDetail":{},"id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":32768,"total":699311},"progress":"[==\u003e                                                ] 32.77 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Pull complete","progressDetail":{},"id":"1cae461a1479"}
{"status":"Digest: sha256:c79345819a6882c31b41bc771d9a94fc52872fa651b36771fbe0c8461d7ee558"}
{"status":"Status: Downloaded newer image for busybox:latest"}
{"stream":" ---\u003e c75bebcdd211\n"}
{"stream":"Step 2/2 : RUN ls\n"}
{"stream":" ---\u003e Running in 74350655bd58\n"}
{"stream":"bin\ndev\netc\nhome\nproc\nroot\nsys\ntmp\nusr\nvar\n"}
{"stream":" ---\u003e 7fc898365619\n"}
{"stream":"Removing intermediate container 74350655bd58\n"}
{"stream":"Successfully built 7fc898365619\n"}

formatResponse関数を使ってパースして出力するとこうなります。

Step 1/2 : FROM busybox
latest: Pulling from library/busybox
1cae461a1479: Pulling fs layer
1cae461a1479: Downloading [=>                                                 ] 15.93 kB/699.3 kB
1cae461a1479: Downloading [===============>                                   ] 211.8 kB/699.3 kB
1cae461a1479: Downloading [=======================>                           ] 326.5 kB/699.3 kB
1cae461a1479: Downloading [=========================>                         ] 359.3 kB/699.3 kB
1cae461a1479: Downloading [============================>                      ] 392.1 kB/699.3 kB
1cae461a1479: Downloading [==============================>                    ] 424.8 kB/699.3 kB
1cae461a1479: Downloading [=================================>                 ]   474 kB/699.3 kB
1cae461a1479: Downloading [=====================================>             ] 523.1 kB/699.3 kB
1cae461a1479: Downloading [========================================>          ] 572.3 kB/699.3 kB
1cae461a1479: Downloading [============================================>      ] 621.4 kB/699.3 kB
1cae461a1479: Downloading [==============================================>    ] 654.2 kB/699.3 kB
1cae461a1479: Downloading [=================================================> ]   687 kB/699.3 kB
1cae461a1479: Downloading [==================================================>] 699.3 kB/699.3 kB
1cae461a1479: Verifying Checksum
1cae461a1479: Download complete
1cae461a1479: Extracting [==>                                                ] 32.77 kB/699.3 kB
1cae461a1479: Extracting [=======================================>           ] 557.1 kB/699.3 kB
1cae461a1479: Extracting [==================================================>] 699.3 kB/699.3 kB
1cae461a1479: Extracting [==================================================>] 699.3 kB/699.3 kB
1cae461a1479: Pull complete
Digest: sha256:c79345819a6882c31b41bc771d9a94fc52872fa651b36771fbe0c8461d7ee558
Status: Downloaded newer image for busybox:latest
 ---> c75bebcdd211
Step 2/2 : RUN ls
 ---> Running in 5c8584208f21
bin
dev
etc
home
proc
root
sys
tmp
usr
var
 ---> b95a8e54a8f1
Removing intermediate container 5c8584208f21
Successfully built b95a8e54a8f1

F#でDockerイメージをビルドする

概要

DockerにはDocker Engine APIがあり、HTTPを使ってアクセスできます。

Docker Engine API and SDKs | Docker Documentation

また、各言語向けにSDKがあります。

SDKs for Docker Engine API | Docker Documentation

今回はDocker for Windowsを使ってF#からDockerイメージのビルドを実行します。

環境は次の通りです。

Docker for Windowsの設定

Docker for WindowsをインストールしてDockerが使えるようになったら、ローカルのDocker EngineにアクセスするためにTLSを無効にします。有効な状態で使うこともできると思いますが今回はそこまで調べられてませんので、無効にした状態でアクセスします。

やり方は、まずタスクトレイのDockerアイコンを右クリックして(なければまずはDockerを起動します)、"Settings…“をクリックします。

f:id:LocaQ:20170606234230p:plain:w250

ウィンドウが開いたら、左のエリアで"General"を選び、右側のエリアの"Expose daemon on tcp://localhost:2375 without TLS"のチェックを外します。

f:id:LocaQ:20170606235242p:plain:w500

Dockerイメージのビルド

Docker.DotNet

.NET向けのSDKとしてDocker.DotNetがあるのでこれを使います。

GitHub - Microsoft/Docker.DotNet: .NET (C#) Client Library for Docker API

ただし、記事作成時点でNuGetで公開されているバージョン(2.124.3)にはまだDockerイメージをビルドするAPIは含まれていません。GithubのmasterブランチにはAPIが存在するので、自分でライブラリをビルドして使います。

ビルドは簡単で、Visual Studioで開いて"Docker.DotNet"というプロジェクトをビルドするだけです。他のプロジェクトは今回は使いません。

ビルド後、自分のプロジェクトの参照に、

を追加します。

コード

DockerイメージのビルドをするF#のコードです。

let buildDockerImage (tar : byte[]) : Stream =
    use client = (new DockerClientConfiguration(new Uri("http://localhost:2375"))).CreateClient()
    use dockerfileTarStream = new MemoryStream(tar)

    let imageBuildParams = new ImageBuildParameters()
    imageBuildParams.Tags <- new List<string>()
    imageBuildParams.Tags.Add("build-test")

    client.Images.BuildImageFromDockerfileAsync(dockerfileTarStream, imageBuildParams)
    |> Async.AwaitTask
    |> Async.RunSynchronously

let outputBuildResponse (responseStream : Stream) : unit =
    seq {
        use reader = new StreamReader(responseStream, Text.Encoding.UTF8)
        while not reader.EndOfStream do
            yield reader.ReadLine()
    }
    |> Seq.iter (printfn "%s")

buildDockerImage関数はdockerのコマンドだと

docker build -t build-test .

と同じことをします。

buildDockerImage関数ではまずDockerClientオブジェクトを作成します。

use client = (new DockerClientConfiguration(new Uri("http://localhost:2375"))).CreateClient()

作成するときのURIには上の"Docker for Windowsの設定"でチェックをつけたところに書かれていた"http://localhost:2375"を指定します。

次にビルドパラメータ(ImageBuildParameters)のオブジェクトを作成します。

let imageBuildParams = new ImageBuildParameters()
imageBuildParams.Tags <- new List<string>()
imageBuildParams.Tags.Add("build-test")

imageBuildParams.Tagsにイメージ名である"build-test"を指定します。これは、"docker build"コマンドの"-t"オプションに指定する値に該当します。

最後にDocker Engine APIにDockerイメージのビルドをリクエストします。

client.Images.BuildImageFromDockerfileAsync(dockerfileTarStream, imageBuildParams)
|> Async.AwaitTask
|> Async.RunSynchronously

1番目の引数はDockerfileをTARデータを持つSystem.Streamオブジェクトで、この後でデータの作成方法を説明します。2番目の引数はビルドパラメータです。

buildDockerImage関数を実行してリクエストに成功するとHTTPレスポンスのデータを読み取れるStreamオブジェクトが返ります。

outputBuildResponse関数はbuildDockerImage関数で返すHTTPレスポンスのStreamオブジェクトを読み込んで標準出力に出力する関数です。F#のシーケンスとyieldを使って1行受信するごとに出力します。

TARデータの作成

Docker Engine APIEngine API v1.24 | Docker Documentation)の"BUILD IMAGE FROM A DOCKERFILE"の項目を読むと、リクエスト例が

POST /v1.24/build HTTP/1.1

{{ TAR STREAM }}

となっています。先ほどのbuildDockerImage関数の引数"tar"にTARフォーマットのデータを表すバイト配列を渡すと、Docker.DotNetのBuildImageFromDockerfileAsync()の第一引数に渡されてこのHTTPリクエストのボディになります。

SharpZipLib

TARデータを作るのに今回はSharpZipLibというライブラリを使います。バージョンは0.86.0です。

GitHub - icsharpcode/SharpZipLib: #ziplib is a Zip, GZip, Tar and BZip2 library written entirely in C# for the .NET platform.

これはNuGetでプロジェクトに追加します。

コード

Dockerfileを読み込んでTARデータを作る関数のコードです。

/// "dockerfileDirPath"はDockerfileがあるディレクトリのパス。
let createDockerfileTar (dockerfileDirPath : string) : byte[] =
    let generateTar (outputStream : Stream) : unit =
        use archive = TarArchive.CreateOutputTarArchive(outputStream)
        let dockerFileEntry = TarEntry.CreateEntryFromFile("Dockerfile")
        archive.WriteEntry(dockerFileEntry, false)

    let currentDirectory = Directory.GetCurrentDirectory()
    try
        use memoryStream = new MemoryStream()

        Directory.SetCurrentDirectory(dockerfileDirPath)
        generateTar memoryStream

        memoryStream.ToArray()
    finally
        Directory.SetCurrentDirectory(currentDirectory)

createDockerfileTar関数はDockerfileを読み込んでTARデータを作成、バイト配列にして返します。

内部のgenerateTar関数でDockerfileを読み込んでTARデータを作っています。TarArchiveやTarEntryがSharpZipLibで定義されている型で、

use archive = TarArchive.CreateOutputTarArchive(outputStream)

で引数に渡されたStreamオブジェクトにデータを書き込むようなアーカイブオブジェクトを作り、

let dockerFileEntry = TarEntry.CreateEntryFromFile("Dockerfile")
archive.WriteEntry(dockerFileEntry, false)

でDockerfileをTARに追加しています。

archiveはDisposeメソッドが呼ばれないとデータの書き込みが行われないようなので、関数にして関数終了時にデータがStreamオブジェクト(outputStream)に書き込まれるようにしています。

また、カレントディレクトリをDockerfileがあるディレクトリにしないとDocker Engine APIにリクエストを投げた時に失敗する(理由はわかりませんが)ので、TARデータの作成前後でカレントディレクトリを変更しています。

実行

アプリケーション全体のコードです。

open Docker.DotNet
open Docker.DotNet.Models
open ICSharpCode.SharpZipLib.Tar
open System
open System.Collections.Generic
open System.IO

let createDockerfileTar (dockerfileDirPath : string) : byte[] =
    let generateTar (outputStream : Stream) : unit =
        use archive = TarArchive.CreateOutputTarArchive(outputStream)
        let dockerFileEntry = TarEntry.CreateEntryFromFile("Dockerfile")
        archive.WriteEntry(dockerFileEntry, false)

    let currentDirectory = Directory.GetCurrentDirectory()
    try
        use memoryStream = new MemoryStream()

        Directory.SetCurrentDirectory(dockerfileDirPath)
        generateTar memoryStream

        memoryStream.ToArray()
    finally
        Directory.SetCurrentDirectory(currentDirectory)

let buildDockerImage (tar : byte[]) : Stream =
    use client = (new DockerClientConfiguration(new Uri("http://localhost:2375"))).CreateClient()
    use dockerfileTarStream = new MemoryStream(tar)

    let imageBuildParams = new ImageBuildParameters()
    imageBuildParams.Tags <- new List<string>()
    imageBuildParams.Tags.Add("build-test")

    client.Images.BuildImageFromDockerfileAsync(dockerfileTarStream, imageBuildParams)
    |> Async.AwaitTask
    |> Async.RunSynchronously

let outputBuildResponse (responseStream : Stream) : unit =
    seq {
        use reader = new StreamReader(responseStream, Text.Encoding.UTF8)
        while not reader.EndOfStream do
            yield reader.ReadLine()
    }
    |> Seq.iter (printfn "%s")

[<EntryPoint>]
let main argv = 
    let dockerfilePath = argv.[0]
    let dockerfileDir = Directory.GetParent(dockerfilePath).FullName

    createDockerfileTar dockerfileDir
    |> buildDockerImage
    |> outputBuildResponse
    0

main関数はコマンドライン引数でDockerfileのパスを受け取って親ディレクトリのパスを取得しています。その後、TARデータ作成→Dockerイメージのビルドリクエストの送信→レスポンスの出力、という処理をしています。

また、次のような内容のDockerfileを作って保存し、

FROM busybox
RUN ls

先ほどのアプリケーションの引数にDockerfileのパスを指定して実行すると次のように出力されてDockerイメージがビルドされます。

> .\DockerBuild.exe ..\..\..\Dockerfile
{"stream":"Step 1/2 : FROM busybox\n"}
{"status":"Pulling from library/busybox","id":"latest"}
{"status":"Pulling fs layer","progressDetail":{},"id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":16384,"total":699311},"progress":"[=\u003e                                                 ] 16.38 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":260087,"total":699311},"progress":"[==================\u003e                                ] 260.1 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":424839,"total":699311},"progress":"[==============================\u003e                    ] 424.8 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":490375,"total":699311},"progress":"[===================================\u003e               ] 490.4 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":539527,"total":699311},"progress":"[======================================\u003e            ] 539.5 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":621447,"total":699311},"progress":"[============================================\u003e      ] 621.4 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Downloading","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Verifying Checksum","progressDetail":{},"id":"1cae461a1479"}
{"status":"Download complete","progressDetail":{},"id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":32768,"total":699311},"progress":"[==\u003e                                                ] 32.77 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Extracting","progressDetail":{"current":699311,"total":699311},"progress":"[==================================================\u003e] 699.3 kB/699.3 kB","id":"1cae461a1479"}
{"status":"Pull complete","progressDetail":{},"id":"1cae461a1479"}
{"status":"Digest: sha256:c79345819a6882c31b41bc771d9a94fc52872fa651b36771fbe0c8461d7ee558"}
{"status":"Status: Downloaded newer image for busybox:latest"}
{"stream":" ---\u003e c75bebcdd211\n"}
{"stream":"Step 2/2 : RUN ls\n"}
{"stream":" ---\u003e Running in 74350655bd58\n"}
{"stream":"bin\ndev\netc\nhome\nproc\nroot\nsys\ntmp\nusr\nvar\n"}
{"stream":" ---\u003e 7fc898365619\n"}
{"stream":"Removing intermediate container 74350655bd58\n"}
{"stream":"Successfully built 7fc898365619\n"}

> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
build-test          latest              68c39e9a6aa8        37 seconds ago      1.11 MB
busybox             latest              c75bebcdd211        3 weeks ago         1.11 MB

HTTPレスポンスの出力の1行1行はJSONで、Dockerビルド中に中で標準出力に出力された文字列が返ってきます。標準出力の内容なので中に改行コード("\n")が含まれています。また、">“が”\u003e"にエスケープされています。

2017/06/13追記:レスポンスを整形して出力できるようにしました。

Dockerのビルドのレスポンスを整形して出力する - locablo