الو، Docker؟ (ارتباط GoLang با api داکر)

اگه توی این صفحه اومدین احتمال زیاد میدونین داکر چیه، ولی طبق رسوم معمول برای اینجور مقاله ها یه مقدمه و تعریف اولیه ببینیم (نگاهی بندازیم به سایت ویکی پدیا و تعریفش از داکر):

Docker is a set of platform as a service (PaaS) products that use OS-level virtualization to deliver software in packages called containers.[6] Containers are isolated from one another and bundle their own software, libraries and configuration files; they can communicate with each other through well-defined channels.[7] All containers are run by a single operating-system kernel and are thus more lightweight than virtual machines.[8]The service has both free and premium tiers. The software that hosts the containers is called Docker Engine.[8] It was first started in 2013 and is developed by Docker, Inc.[9]
داکر (انگلیسی: Docker) یک برنامه رایانه‌ای متن باز است که از شبیه‌سازی سطح سیستم‌عامل برای توسعه و منتشر کردن پکیج ها که به عنوان کانتینر(container) شناخته می‌شوند استفاده می‌کند. داکر استقرار(deployment) نرم‌افزارهای کاربردی را درون کانتینر به وسیلهٔ فراهم کردن لایهٔ انتزاعی اضافه‌ای فراهم می‌کند. نرم افزاری که میزبانی کانتینر های داکر را به عهده دارد موتور داکر(docker engine) نام دارد.
داکر در سال ۲۰۱۳ شروع به کار کرد و توسط شرکت داکر توسعه داده می‌شود. این سرویس به دو نوع رایگان و پولی در دسترس است.
کانتینرهای داکر قسمتی از نرم‌افزار را در یک سیستم فایل کامل تعبیه می‌کند. به صورتی که شامل هر آنچه جهت اجرا شدن (مانند کد زمان اجرا، ابزارهای سیستم و کتابخانه سیستم) لازم است و هر آنچه که می‌تواند بر روی یک سرور نصب شود. این امر اجرای برنامه را به صورت ثابت در هر نوع محیطی تضمین می‌کند.

توی این مقاله میخوایم ببینیم چجوری با api داکر شروع به کار کنیم (به کمک زبان GoLang).

نصب داکر خیلی راحته ولی بعضی روشا با بعضی سیستم عاملا یکم اذیت میکنن، من توی تهیه این مقاله از این لینک برای نصبش روی CentOS Linux release 7.7.1908 استفاده کردم، ورژن داکر:

Docker version 19.03.5, build 633a0ea

خب حالا که داکر رو نصب کردیم، اول باید بدونیم چه کامندایی داره کلا، ولی این مقاله برای آشنایی با داکر نیست برای همین زیاد وارد جزئیات و یادگیری داکر نمیشیم (گرچه دید نسبتا خوبی پیدا میکنیم). ما میخوایم بتونیم از راه دور و به کمک api داکر باهاش ارتباط برقرار کنیم. با مراجعه به لینک زیر

https://docs.docker.com/develop/sdk/

و پیدا کردن ورژن داکرتون، میتونید ورژن api مرتبط با ورژن داکرتون رو ببینید، در حال حاضر بصورت زیر هست:

پس ما میخوایم از api ورژن 1.40 استفاده کنیم که باید به این لینک مراجعه کنیم:

https://docs.docker.com/engine/api/v1.40/

اما خبر خوب اینه که (توی دو تا لینک بالاتر مشخصه که) بصورت پیشفرض کلاینتهایی برای خیلی از زبانهای زبان های برنامه نویسی در نظر گرفته شدن (البته بصورت رسمی GoLang و پایتون). و ما چون میخوایم فعلا از زبان گو استفاده کنیم با کامند زیر میتونیم SDK کلاینت رو اضافه کنیم:

go get github.com/docker/docker/client

خب تا اینجا داکر روی سیستممون داریم و میخوایم از بیرون باهاش ارتباط برقرار کنیم.
این نکته مشخصه ولی بعضی وقتا شاید آدم حواسش نباشه، قبل از هر چیزی از ارتباط با api داکر مطمئن بشید و اگه لازمه تنظیمات فایروال و ... رو انجام بدین که از بیرون بتونید باهاش ارتباط برقرار کنید.

برای تست ارتباط کافیه آدرس زیر رو صدا بزنید:

http://(server-ip):(docker port)
http://123.456.789.000:1234 :مثال

خب حالا یه سوال مطرحه، من از کجا بدونم پورت داکر چیه؟

جواب اینه که برای راه اندازی api داکر اول باید به فایل زیر مراجعه کنیم:

vi /usr/lib/systemd/system/docker.service

خطی که با کلمه ی ExecStart شروع میشه رو پیدا کنید و بصورت زیر ویرایشش کنید (کلش یه خط هست):

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 --containerd=/run/containerd/containerd.sock

توضیح مختصرش اینه که با tcp://0.0.0.0:2375 میگیم روی همه ی آی پی ها این پورت رو برای داکر در نظر بگیر. خب حالا کافیه دستورات زیر رو بزنیم و سرویس داکر رو ری استارت کنیم:

systemctl daemon-reload
systemctl restart docker.service

اگه همچی خوب پیش رفته باشه، وقتی از بیرون به سرورمون این ریکوئست رو بدیم:

http://(server-ip):(docker port)

این جواب رو میگیریم:


و تمام. حالا که api داکر آمادس، برای پیاده سازی کد سمت کلاینت و اینکه ببینیم چجوری میتونیم از راه دور به داکرمون فرمان بدیم باید به داکیومنت ها و مثالا رجوع کنیم، مثل لینک زیر:

https://docs.docker.com/develop/sdk/examples/

برای نمونه کد زیر (از همین لینک بالایی) برای دریافت لیست کانتینرهاست:

package main

import (
	&quotfmt&quot

	&quotgithub.com/docker/docker/api/types&quot
	&quotgithub.com/docker/docker/client&quot
	&quotgolang.org/x/net/context&quot
)

func main() {
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		panic(err)
	}

	containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
	if err != nil {
		panic(err)
	}

	for _, container := range containers {
		fmt.Println(container.ID)
	}
}

و من برای اینکه بتونم کد رو اجرا کنم بصورت زیر تغییرش دادم:

package main

import (
    &quotfmt&quot
    &quotnet&quot
    &quotnet/http&quot
    &quottime&quot
    &quotgithub.com/docker/docker/api/types&quot
    &quotgithub.com/docker/docker/client&quot
    &quotgolang.org/x/net/context&quot
    )
    
// Docker constants
const (
    DockerHost = &quothttps://ip:port&quot
    DockerAPIVersion = &quot1.40&quot
    DockerRequestRimeout = time.Second * 20
    )
        
func main() {
    ctx := context.Background()
    cliPtr, err := createNewClient()
    
    if err != nil {
        panic(err)
    }
    
    showAllContainersList(ctx, *cliPtr)
}

func createNewClient() (*client.Client, error) {
    var netTransport = &http.Transport{
    Dial: (&net.Dialer{
        Timeout: DockerRequestRimeout,
        }).Dial,
        TLSHandshakeTimeout: DockerRequestRimeout,
    }
    var netClient = &http.Client{
        Timeout:   DockerRequestRimeout,
        Transport: netTransport,
    }
    return client.NewClient(DockerHost, DockerAPIVersion, netClient, nil)
}

func showAllContainersList(ctx context.Context, cli client.Client) {
    containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
        All: true,
    })
    if err != nil {
        panic(err)
    }
    for _, container := range containers {
        fmt.Println(container)
    }
}

توضیح مختصر این کد اینه که تابع createNewClient با ساختاری که میبینید یه کلاینت جدید میسازه، تابع showAllContainersList قراره لیست کانتینرهارو بگیره، وقتی داریم cli.ContainerList رو صدا میزنیم میتونیم پارامترهایی بهش بدیم برای مثال چون من میخوام کل کانتینرهارو بگیرم بولین All رو true میفرستم. موارد زیر رو میتونید به عنوان آپشن استفاده کنید:

type ContainerListOptions struct {
    Quiet   bool
    Size    bool
    All     bool
    Latest  bool
    Since   string
    Before  string
    Limit   int
    Filters filters.Args
}

خروجی این درخواست لیستی از کانتیرنهاست که هر کدوم شامل موارد زیر میشن:

type Container struct {
    ID         string `json:&quotId&quot`
    Names      []string
    Image      string
    ImageID    string
    Command    string
    Created    int64
    Ports      []Port
    SizeRw     int64 `json:&quot,omitempty&quot`
    SizeRootFs int64 `json:&quot,omitempty&quot`
    Labels     map[string]string
    State      string
    Status     string
    HostConfig struct {
        NetworkMode string `json:&quot,omitempty&quot`
    }
    NetworkSettings *SummaryNetworkSettings
    Mounts          []MountPoint
}

کد بالا عملکردی شبیه دستور زیر توی سیستمی که داکر روش نصبه داره:

docker ps --all

یا برای یه مثال دیگه من میخوام یه کانتینر جدید تعریف کنم برای این کار اول میام یه ایمیج (روی سروری که داکر روشه) رو با دستور زیر دریافت میکنم (برای مثال من jrottenberg/ffmpeg رو میخوام استفاده کنم):

docker pull jrottenberg/ffmpeg

و همونطور که میدونین با زدن کامند زیر میتونیم لیست ایمیج هامون رو ببینیم:

docker images

بیاین برای جذابتر شدن ماجرا این کار رو توی خود کلاینت انجام بدیم. با تابع pullAnImage همین ایمیج رو توسط کلاینت روی سرور دریافت میکنیم:

const (
    ...
    DockerRequestRimeout = time.Second * 60
    FfmpegImageName = &quotjrottenberg/ffmpeg&quot
    )
...
func main() {
    ...
    pullAnImage(ctx, *cliPtr, FfmpegImageName)
}
...
func pullAnImage(ctx context.Context, cli client.Client, imageName string) {
    out, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }
    fmt.Println(out)
    
    defer out.Close()
    io.Copy(os.Stdout, out)
}

* به تایم-اوتمون توجه کنیم، چون سرور قراره چیزی رو دانلود کنه بسته به سرعت نت سرورتون دقت کنین چه تایم-اوتی روی درخواستتون میذارین. اگه مشکلی توی روند درخواستتون پیش نیاد عکس زیر خروجی قابل قبول رو نشون میده (خط آخر میگه که آخرین ورژن ایمیج jrottenberg/ffmpeg رو دریافت کردم)

و اگه بخوایم توی کلاینتمون لیست ایمیج ها رو ببینیم میتونیم از تابع زیر استفاده کنیم:

func imageList(ctx context.Context, cli client.Client) {
    images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
    if err != nil {
        panic(err)
    }
    for _, image := range images {
        fmt.Println(image)
    }
}

که خروجی ای شبیه زیر میتونیم ازش انتظار داشته باشیم:

به این نکته هم توجه کنیم که خروجی تابع cli.ImageList در صورت خطا نداشتن لیستی از ImageSummary ها هست که ساختارشون به شکل زیره:

type ImageSummary struct {
    // containers
    // Required: true
    Containers int64 `json:&quotContainers&quot`
    
    // created
    // Required: true
    Created int64 `json:&quotCreated&quot`
    
    // Id
    // Required: true
    ID string `json:&quotId&quot`
    
    // labels
    // Required: true
    Labels map[string]string `json:&quotLabels&quot`
    
    // parent Id
    // Required: true
    ParentID string `json:&quotParentId&quot`
    
    // repo digests
    // Required: true
    RepoDigests []string `json:&quotRepoDigests&quot`
    
    // repo tags
    // Required: true
    RepoTags []string `json:&quotRepoTags&quot`
    
    // shared size
    // Required: true
    SharedSize int64 `json:&quotSharedSize&quot`
    
    // size
    // Required: true
    Size int64 `json:&quotSize&quot`
    
    // virtual size
    // Required: true
    VirtualSize int64 `json:&quotVirtualSize&quot`
}

خب قبل از اینکه ادامه بدیم یه مرور کوتاهی داشته باشیم. تا اینجا داکر رو روی سیستم نصب کردیم، فایل docker.service رو ویرایش کردیم تا بتونیم از api استفاده کنیم، sdk مرتبط با کلاینت GoLang رو دریافت کردیم، کلاسی نوشتیم که داخلش به کمک api، اول لیست کانتینرهای موجود رو گرفتیم، بعد درخواست pull کردن یه ایمیج رو فرستادیم و در نهایت تونستیم لیستی از ایمیج های داکرمون رو ببینیم.

به عنوان آخرین مثال هم ببینیم که چجوری میتونیم از یه ایمیج که روی سرور داریم، یه کانتینر ایجاد کنیم:

func startNewContainer(ctx context.Context, cli client.Client, imageName string) {
    resp, err := cli.ContainerCreate(ctx, &container.Config{
        Image: imageName,
        Tty: true,
    }, nil, nil, &quot&quot)
    if err != nil {
        panic(err)
    }
    if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
        panic(err)
    }
    
    statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
        select {
            case err := <-errCh:
            if err != nil {
                panic(err)
           }
           case <-statusCh:
    }
       
    out, err := cli.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
    if err != nil {
        panic(err)
    }
       
    defer out.Close()
    io.Copy(os.Stdout, out)
}

از اسمشون مشخصه که تابع ContainerCreate یه کانتینر جدید میسازه، تابع ContainerStart اون کانتینر رو start میکنه، تابع ContainerWait منتظر میمونه که کانتینر کارش رو انجام بده، و در نهایت با تابع ContainerLogs میتونیم استریم لاگای کانتینرمون رو داشته باشیم و توی خطای بعدش میندازیمش روی استریم خروجی لاگ خودمون.

پس با اجرای دستور زیر:

startNewContainer(ctx, *cliPtr, FfmpegImageName)

هم یه کانتینر ایجاد کردیم هم اجراش کردیم هم لاگش رو میبینیم:

فقط حواسمون باشه که هربار تابع startNewContainer رو صدا بزنیم داریم یه کانتینر جدید ایجاد میکنیم، عملا هربار که نمیخوایم مثلا یه کانتینر ffmpeg جدید داشته باشیم الزاما. پس یبار میسازیمش و از اون به بعدش میتونیم از همون استفاده کنیم. یا زمانهایی که لازم شد جدید میسازیم و ...

برای این هم که هر وقت خواستیم بتونیم از کانتینرهایی که ایجاد کردیم استفاده کنیم بد نیست نگاهی به توابع cli.ContainerExecStart یا cli.ContainerStart بندازیم و برای ری استارت کردن به کانتینر از cli.ContainerRestart استفاده کنیم. کلا توابع موجود توی sdk کلاینت، خیلی متنوعن و اکثر کارارو راحت انجام میدن، داکیومنت خیلی خوب و قوی ای هم دارن، هم سایت داکر هم قسمت sdk کلاینتاش.


از اونجایی که ظاهرا ویرگول با gist مشکل داره، کد کامل توابعی که دیدیم رو میتونید توی لینک زیر ببینید:

https://gist.github.com/MohammadGhodsian/ff565b9dcb49c27a3582c752d03b3579


منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian

https://virgool.io/@mohammad.ghodsian/%D8%A7%D9%84%D9%88-docker-%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-golang-%D8%A8%D8%A7-api-%D8%AF%D8%A7%DA%A9%D8%B1-sm7qpweoe7zm