$30 off During Our Annual Pro Sale. View Details »

Elixir on Containers

Elixir on Containers

Know-how that ops Elixir on Docker containers.

さっちゃん

May 19, 2019
Tweet

More Decks by さっちゃん

Other Decks in Programming

Transcript

  1. Elixir
    on Containers

    View Slide

  2. .。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

    View Slide

  3. Container is good, because:
    It gives an abstract of processes with environments.
    It's easy to deploy.

    View Slide

  4. I'm using container for Elixir server @ dev, test & prod.
    A little bit stateful WebSocket servers.
    Umbrella
    Related tools: Distillery, Dialyzer, docker‑compose, Kubernetes.

    View Slide

  5. Containers for local dev

    View Slide

  6. Why use containers for local dev?
    Use the same env between Alchemist team members.
    Use the same env between non‑Alchemist team members. (Rubyist, frontend
    enginier, etc.)
    Use the near env between dev, test & prod.

    View Slide

  7. I maintain Docker images that fixes both Erlang & Elixir ver.

    View Slide

  8. Work with the team & CI (Continuous Integration).

    View Slide

  9. My application have some umbrellas.
    apps/
    app_a/
    app_b/

    View Slide

  10. Dockerfile
    (deps.compile & dialyzer ‑‑plt)
    FROM nesachirou/elixir:1.8_erl21
    SHELL ["/bin/ash", "-ex", "-o", "pipefail", "-c"]
    RUN apk add --no-cache -t .build-deps \
    ~ \
    git \
    && rm -rf /var/cache/apk/*
    WORKDIR /var/www
    VOLUME /var/www
    COPY mix.exs mix.lock ./
    COPY apps/app_a/mix.exs apps/app_a/
    COPY apps/app_b/mix.exs apps/app_b/
    RUN mix "do" deps.get, deps.compile \
    && MIX_ENV="test" mix deps.compile \
    && mix dialyzer --plt \
    && mv _build deps /tmp
    RUN apk add --no-cache -t .runtime-deps \
    inotify-tools \
    rsync \
    && rm -rf /var/cache/apk/*
    EXPOSE 4000 4001
    CMD ["./entrypoint.sh"]

    View Slide

  11. entrypoint.sh
    #!/bin/sh -eux
    # shellcheck shell=dash
    if test "$(stat /.dockerenv > /dev/null || echo $?)" ; then
    : "Outside Docker"
    mix "do" deps.get, compile
    else
    : "Inside Docker"
    rsync -au /tmp/_build /tmp/deps .
    mix "do" deps.get, compile
    fi
    elixir --name [email protected] --cookie example -S mix phx.server

    View Slide

  12. docker-compose.yml
    ---
    version: "3"
    services:
    example:
    image: example.docker.registry/example/example:latest
    depends_on: [example-src]
    logging:
    options:
    max-size: "100m"
    max-file: "2"
    ports:
    - 4000:4000
    - 4001:4001
    volumes:
    - example-src:/var/www

    View Slide

  13. docker-compose.override.yml
    ---
    version: "3"
    services:
    example:
    build:
    context: .
    dockerfile: Dockerfiles/Dockerfile.dev
    docker-compose build --pull --force-rm
    docker-compose push

    View Slide

  14. docker‑sync
    http://docker‑sync.io/
    Docker (for MacOS) is too slow in it's file IO between containers & the host.
    docker‑sync solves this, but troublesome.
    I do a little bit different way (unison + rsync).

    View Slide

  15. docker-compose.yml

    example-src:
    image: eugenmayer/unison:2.51.2.1
    environment:
    APP_VOLUME: /app_sync
    HOST_VOLUME: /host_sync
    TZ: Asia/Tokyo
    UNISON_ARGS: |
    -debug default -ignore 'Name .git/*' -ignore 'Name _build/*' \
    -ignore 'Name deps/*' -prefer /host_sync -numericids -auto -batch
    UNISON_DEST: /app_sync
    UNISON_SRC: /host_sync
    UNISON_WATCH_ARGS: -repeat watch
    volumes:
    - ../example:/host_sync:cached
    - example-src:/app_sync
    volumes:
    example-src:

    View Slide

  16. precopy_appsync
    #!/bin/bash -eux
    rsync -auv \
    --delete \
    --exclude='.git/*' --exclude='_build/*' --exclude='deps/*' \
    /host_sync/ /app_sync
    docker-compose -f example/docker-compose.yml stop example-src
    docker-compose -f example/docker-compose.yml run --rm --no-deps example-src \
    /host_sync/tools/precopy_appsync
    docker-compose -f example/docker-compose.yml start example-src

    View Slide

  17. Testing container image
    yamllint https://github.com/adrienverge/yamllint
    hadolint https://github.com/hadolint/hadolint
    container‑structure‑test https://github.com/GoogleContainerTools/container‑
    structure‑test

    View Slide

  18. Containers for prod

    View Slide

  19. Work with CD (Continuous Delivery).

    View Slide

  20. Umbrella
    apps/
    app_a/
    app_b/
    common/

    View Slide

  21. Distillery https://github.com/bitwalker/distillery
    {:distillery, "~> 2.0", runtime: false},
    No hot code swapping, RollingUpdate.

    View Slide

  22. rel/config.exs

    environment :prod do
    set(include_erts: true)
    set(include_src: false)
    set(cookie: :"
    ~")
    set(vm_args: Path.join("rel", "vm.args.prod"))
    end
    release :app_a do
    set(version: "0.0.1")
    set(applications: [:app_a, :common, :runtime_tools])
    set(commands: [app_terminate: "rel/commands/app_terminate.sh"])
    set(overlay_vars: [
    ~])
    end

    View Slide

  23. rel/commands/app_terminate.sh
    → ./bin/app_a app_terminate
    (PreStop hook)
    #!/bin/bash -eu
    COOKIE=$(awk '/-setcookie/{print$2}' releases/0.0.1/vm.args)
    export COOKIE
    bin/app_a command Elixir.AppA.Command app_terminate
    AppA.Command
    defmodule AppA.Command do
    def app_terminate do
    :ok = do_rpc(:"[email protected]", :"[email protected]",
    AppA.AppTerminator, :request_terminate, [],
    1_800_000)
    end
    defp do_rpc(target_node, node_name, module, fun, args, timeout) do
    Node.start(node_name)
    Node.set_cookie(String.to_atom(System.get_env("COOKIE")))
    :rpc.call(target_node, module, fun, args, timeout)
    end
    end

    View Slide

  24. Dockerfile
    for app_a (multi stage build)
    FROM nesachirou/elixir:1.8_erl21 as builder
    SHELL ["/bin/ash", "-ex", "-o", "pipefail", "-c"]
    RUN apk add --no-cache -t .build-deps \
    build-base \
    ~ \
    git
    ARG REVISION
    ENV MIX_ENV=prod
    WORKDIR /var/www
    COPY mix.exs mix.lock ./
    COPY apps/app_a/mix.exs apps/app_a/
    COPY apps/common/mix.exs apps/common/
    RUN mix "do" deps.get --only prod, deps.compile
    COPY apps/app_a apps/app_a
    COPY apps/common apps/common
    COPY config config
    COPY rel rel
    # hadolint ignore=DL3003
    RUN mix compile \
    && mix release --verbose --name=app_a

    View Slide

  25. Dockerfile
    for app_a (multi stage build)

    FROM alpine:3.9
    SHELL ["/bin/ash", "-ex", "-o", "pipefail", "-c"]
    RUN apk add --no-cache -t .runtime-deps \
    bash \
    inotify-tools \
    openssl \
    && rm -rf /var/cache/apk/*
    WORKDIR /var/www
    # hadolint ignore=DL3010
    COPY --from=builder /var/www/_build/prod/rel/app_a/releases/0.0.1/app_a.tar.gz ./
    RUN tar xzf app_a.tar.gz \
    && rm -rf app_a.tar.gz
    EXPOSE 80 4369
    CMD ["bin/app_a", "foreground"]

    View Slide

  26. container-structured-test.yml
    ---
    schemaVersion: "2.0.0"
    commandTests:
    - name: Application meta data
    command: bin/example
    args: [describe]
    expectedOutput:
    - example-0.0.1
    - "Name: [email protected]"
    - name: libcrypto
    command: bin/example
    args: [eval, ":crypto.strong_rand_bytes(8)"]
    metadataTest:
    cmd: [bin/example, foreground]
    exposedPorts: ["80", "4369"]
    workdir: /var/www

    View Slide