F#でDockerイメージをビルドする
概要
DockerにはDocker Engine APIがあり、HTTPを使ってアクセスできます。
Docker Engine API v1.39 Reference
また、各言語向けにSDKがあります。
Develop with Docker Engine SDKs and API | Docker Documentation
今回はDocker for Windowsを使ってF#からDockerイメージのビルドを実行します。
環境は次の通りです。
- Windows 10 Pro (1703)
- Docker for Windows (Version 17.03.1-ce-win12 (12058), Channel: stable)
- F# 4.1
- Visual Studio Community 2017
Docker for Windowsの設定
Docker for WindowsをインストールしてDockerが使えるようになったら、ローカルのDocker EngineにアクセスするためにTLSを無効にします。有効な状態で使うこともできると思いますが今回はそこまで調べられてませんので、無効にした状態でアクセスします。
やり方は、まずタスクトレイのDockerアイコンを右クリックして(なければまずはDockerを起動します)、"Settings..."をクリックします。
ウィンドウが開いたら、左のエリアで"General"を選び、右側のエリアの"Expose daemon on tcp://localhost:2375 without TLS"のチェックを外します。
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 API(Engine 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です。
これは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追記:レスポンスを整形して出力できるようにしました。