Docker & Makefile | X-Ops — sharing infra-as-code parts
Docker と Makefile | X-Ops — infra-as-code partsを共有すること
X-Ops、例えばDevOps, CloudOps, GitOps, SysOpsのようなもの)としての人生の中において、私たちはinfra-as-codeを構築することに集中しています。構築とテスト、レポジトリからやレポジトリへのコードのプルの実行などのためにとても頻繁にDockerを使います。そしてすべてのテストにパスしたら、インフラは製品へ移行します。このすべては完全に自動化されています。
現在ではX-Opsチームのスケーリングと統合が大きなチャンスとなっています。 その候補リストの2つの目標は次のとおりです。 1.スタート時に障壁が低い(できればゼロが望ましい)ことで、より多くの仲間を得られます。2.ビルドとテストのという流れを理解しやすくし、部品をより速く生産し、再利用を改善します。
この記事では、いつくかの簡単な技術的なプラクティスを主に紹介していきたいと思います。つまり、Docker、Makefile、正しいUIDsなどのことです。このようなプラクティスで自分の経験から上に挙げた目標により近づけるようにしていきたいと思います。
Using a Makefile
Makefileの使用についてです。
Dockerのおかげでシステムをより早く効果的に自動的に構築できるようになります。そして、docker-composeを伴ったサービスの実行を強化することもできます。
しかし、製作中の多くのケースにおいては、別々に分かれたコンテナのプラットフォームが存在します。自分たちがローカルで構築、テスト、実行するそのコンテナは巨大なコンテナの一部なのです。ここでよく問題に上がるのは、チームや世界中の人々とdockerコードを共有する場合、自分のコンテナの実装方法を説明する必要があるということです。
個人的には、私は自分のすべてのプロジェクトがこのような機能を持ってほしいと思っています。
git pull && make test && make build && make deploy
このフォーマットのone-linerについて話し合うことはとてもいいことです。同僚に製品(-コンポーネント)を試してもらい、長いREADMEを読まなくても簡単に依頼できます。機能するかしないかのどちらかですから。
優れた古いMakefileを利用するようにしてください。40年前前の古いものですが、まだまだ引退するには早すぎるMakefileは自動化プロジェクトの内部で生まれ変わりました。(詳しくはこちらを読んでみてください。怠け者の開発者のための makefile)
これはDockerととても相性のよい、私だけのMakefile です。
# --------------------------------------------------------------------
# Copyright (c) 2019 LINKIT, The Netherlands. All Rights Reserved.
# Author(s): Anthony Potappel
#
# This software may be modified and distributed under the terms of the
# MIT license. See the LICENSE file for details.
# --------------------------------------------------------------------# If you see pwd_unknown showing up, this is why. Re-calibrate your system.
PWD ?= pwd_unknown# PROJECT_NAME defaults to name of the current directory.
# should not to be changed if you follow GitOps operating procedures.
PROJECT_NAME = $(notdir $(PWD))# Note. If you change this, you also need to update docker-compose.yml.
# only useful in a setting with multiple services/ makefiles.
SERVICE_TARGET := main# if vars not set specifially: try default to environment, else fixed value.
# strip to ensure spaces are removed in future editorial mistakes.
# tested to work consistently on popular Linux flavors and Mac.
ifeq ($(user),)
# USER retrieved from env, UID from shell.
HOST_USER ?= $(strip $(if $(USER),$(USER),nodummy))
HOST_UID ?= $(strip $(if $(shell id -u),$(shell id -u),4000))
else
# allow override by adding user= and/ or uid= (lowercase!).
# uid= defaults to 0 if user= set (i.e. root).
HOST_USER = $(user)
HOST_UID = $(strip $(if $(uid),$(uid),0))
endifTHIS_FILE := $(lastword $(MAKEFILE_LIST))
CMD_ARGUMENTS ?= $(cmd)# export such that its passed to shell functions for Docker to pick up.
export PROJECT_NAME
export HOST_USER
export HOST_UID# all our targets are phony (no files to check).
.PHONY: shell help build rebuild service login test clean prune# suppress makes own output
#.SILENT:# shell is the first target. So instead of: make shell cmd="whoami", we can type: make cmd="whoami".
# more examples: make shell cmd="whoami && env", make shell cmd="echo hello container space".
# leave the double quotes to prevent commands overflowing in makefile (things like && would break)
# special chars: '',"",|,&&,||,*,^,[], should all work. Except "$" and "`", if someone knows how, please let me know!).
# escaping (\) does work on most chars, except double quotes (if someone knows how, please let me know)
# i.e. works on most cases. For everything else perhaps more useful to upload a script and execute that.
shell:
ifeq ($(CMD_ARGUMENTS),)
# no command is given, default to shell
docker-compose -p $(PROJECT_NAME)_$(HOST_UID) run --rm $(SERVICE_TARGET) sh
else
# run the command
docker-compose -p $(PROJECT_NAME)_$(HOST_UID) run --rm $(SERVICE_TARGET) sh -c "$(CMD_ARGUMENTS)"
endif# Regular Makefile part for buildpypi itself
help:
@echo ''
@echo 'Usage: make [TARGET] [EXTRA_ARGUMENTS]'
@echo 'Targets:'
@echo ' build build docker --image-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' rebuild rebuild docker --image-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' test test docker --container-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' service run as service --container-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' login run as service and login --container-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' clean remove docker --image-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ' prune shortcut for docker system prune -af. Cleanup inactive containers and cache.'
@echo ' shell run docker --container-- for current user: $(HOST_USER)(uid=$(HOST_UID))'
@echo ''
@echo 'Extra arguments:'
@echo 'cmd=: make cmd="whoami"'
@echo '# user= and uid= allows to override current user. Might require additional privileges.'
@echo 'user=: make shell user=root (no need to set uid=0)'
@echo 'uid=: make shell user=dummy uid=4000 (defaults to 0 if user= set)'rebuild:
# force a rebuild by passing --no-cache
docker-compose build --no-cache $(SERVICE_TARGET)service:
# run as a (background) service
docker-compose -p $(PROJECT_NAME)_$(HOST_UID) up -d $(SERVICE_TARGET)login: service
# run as a service and attach to it
docker exec -it $(PROJECT_NAME)_$(HOST_UID) shbuild:
# only build the container. Note, docker does this also if you apply other targets.
docker-compose build $(SERVICE_TARGET)clean:
# remove created images
@docker-compose -p $(PROJECT_NAME)_$(HOST_UID) down --remove-orphans --rmi all 2>/dev/null \
&& echo 'Image(s) for "$(PROJECT_NAME):$(HOST_USER)" removed.' \
|| echo 'Image(s) for "$(PROJECT_NAME):$(HOST_USER)" already removed.'prune:
# clean all that is not actively used
docker system prune -aftest:
# here it is useful to add your own customised tests
docker-compose -p $(PROJECT_NAME)_$(HOST_UID) run --rm $(SERVICE_TARGET) sh -c '\
echo "I am `whoami`. My uid is `id -u`." && echo "Docker runs!"' \
&& echo success
もしMakefileに詳しくないというのであれば、これはすこし仰々しく思うかもしれません。スタートする前に、Dockerが必要です。Dockerのインストール手順はこちらです。Ubuntu, Mac ,Windows. またシェルを開いてgitクライアントをインストールし、makeを実行する方法も知っていますよね。Linuxディストリビューションをおすすめしますが、Macでもテストしています。もし準備できたのであれば、ここにコマンドがあるのでトライしてみてください。
# download our filesgit clone https://github.com/LINKIT-Group/dockerbuild# enter directorycd dockerbuild# build, test and run a commandmake build test shell cmd="whoami"# my favorite for container explorationmake shell# shell-target is the default (first item), so this also works:make cmd="whoami"make cmd="ls /"# force a rebuild, test and cleanupmake rebuild test clean
私がどこにも“&&”-charsを使っていないことに気づきましたか?ターゲットリスト(例えばbuild, test, clean)の単一のmake-commandを使うこともできますし、エラーが起きるたびにストップしてもいいです。
Makefileの中では、特定のターゲットに対してone-linersを実行するようにカスタマイズすることができます。個人的には、いつもラン制御文に対して“ — rm”を付け加えて、破棄されたコンテナのトレイルで終わることを防ぐようにします。隅々までMakefileを読めば、試してみたくなるような興味深いことがたくさん出てくると思います。
テスト・ターゲットをカスタマイズしたいと思っているでしょうし、テストを基本的なプラクティスとしたいと思っているはずです。自分の構築したパートがCI/CDパイプラインの中で統合される(将来的にはやがて統合されます)ときに、「テストを実施する」ためにものすごく役に立ちます。
ここからデプロイターゲットを追加し、Docker-hubかそれに似たようなレポジトリにコンテナをプッシュするには残すところあと1つのステップのみとなりました。継続的なデプロイメントは物事がとても面白くなる場面です。悲しいことに、これもまたより多くのライティングスペースが必要です。これをセーブする必要性があるのですが、その理由は後で説明します。
デプロイメントに移る前に、もう1つの基本的な構成プラクティスを説明する必要があります。
MakefileのHOST_USER変数とHOST_UID変数に気づきましたか? これらはランタイム時に書き込まれ、docker-composeが読み取るために再エクスポートされます。 それでは、UIDに関する次の章で説明します。
もっと基本的なコンフィギュレーションプラクティスをカバーする必要があります。Makefile の中のHOST_USER と HOST_UID変数に気づきましたか?
Dealing with UIDs
UserID (UID)についてです。
DockerはUserID (UID)がコンフィギュアされていない時に、コンテナをユーザールートにデフォルト設定します。プロダクションシステムを処理しないといけない場合、ユーザーを正しくコンフィギュアする必要があります。これについてはDockers’ best-practices-listや this piece from K8s などの記事を読んでみてください。分かりやすく説明してくれています。
Macでテストをローカルに走らせるとき、ルートは自分自身のユーザーにマップされています。そのためすべてが問題なく機能します。プロダクションのプラットフォームは、user-isolation(ユーザー分離)のためにコンフィギュアされるべきです。しかし、それが常にデフォルトであるとは限りません。例えばもし、Linuxシステム上で(再マッピングすることなく)テストを実行したとしたら、Dockerによって生成されたファイルがルートによって所有されているという問題につまづくこともあるでしょう。
一つのシステムにたくさんのユーザーがいる場合にも問題に直面するでしょう。システムを共有していなくても、パラレルテストを実行するにはセパレーション(分離)も必要です。では、適切なUIDセットアップもなくどのように承認を受けるのでしょうか(例えば、~/.sshや~/.awsのようなものにリンクをする)。後者はインフラのデプロイメントにおいては一般的なパターンです。
もっともっとdockerを利用するようになると、チームが成長し、事態が複雑になります。最終的には、すべてのプロジェクトにUIDセパレーション(分離)を組み込む必要が出てきます。
幸いなことに、UIDの設定は早い段階においては(比較的)簡単に設定可能です。上手にセットアップを行うには多少の練習が必要でしたが、(Makefileの使用を想定して)今ではすべて自動的に行うテンプレートがあり、とても簡単にできるようになりました。
下のものはdocker-composeファイルのコピーです。
version: '3.4'services:main:# Makefile fills PROJECT_NAME to current directory name.# add UID to allow multiple users run this in parallelcontainer_name: ${PROJECT_NAME}_${HOST_UID:-4000}hostname: ${PROJECT_NAME}# These variables are passed into the container.environment:- UID=${HOST_UID:-4000}# Run with user priviliges by default.user: ${HOST_USER:-nodummy}image: ${PROJECT_NAME}:${HOST_USER:-nodummy}build:context: .# Build for current user.target: userdockerfile: Dockerfile# These variables are passed to Dockerfile.args:- HOST_UID=${HOST_UID:-4000}- HOST_USER=${HOST_USER:-nodummy}# Run container as a service. Replace with something useful.command: ["tail", "-f", "/dev/null"]# Copy current (git-) project into container.volumes:- ${PWD:-.}:/home/${HOST_USER}/${PROJECT_NAME}
変数ステートメントにはちょっとした魔法かけられています。
${HOST_USER:-nodummy}${HOST_UID:-4000}
これは、自分のランタイムから変数をコピーし、それが存在していなければデフォルトでそれぞれ「nodummy」や「4000」となります。デフォルトの設定があまり気に入らないのであれば、こちらをどうぞ。
${HOST_USER:?You forgot to set HOST_USER in .env!}${HOST_UID:?You forgot to set HOST_UID in .env!}
「HOST_」prefixについてです。私はUSERとUIDを直接使用することを避けるようにしています。それらの変数はランタイムで利用可能だという保証がされていないからです。USERはシェルの中では利用可能な場合が多くありますが、UIDはほとんどが環境変数で、Dockerが拾うことがありません。アクシデントを避けるために別々のネーミングスキームを持ち、フレキシビルにオートメーションパイプラインをコンフィギュアするようにしましょう。
Dockerfile はこのような感じになります。
FROM alpine as baseRUN apk update \&& apk add --no-cache \bashFROM scratch as userCOPY --from=base . .ARG HOST_UID=${HOST_UID:-4000}ARG HOST_USER=${HOST_USER:-nodummy}RUN [ "${HOST_USER}" == "root" ] || \(adduser -h /home/${HOST_USER} -D -u ${HOST_UID} ${HOST_USER} \&& chown -R "${HOST_UID}:${HOST_UID}" /home/${HOST_USER})USER ${HOST_USER}WORKDIR /home/${HOST_USER}
ここに面白いかなというのと練習のためにバッシュを加えた小さなAlpinコンテナを構築しました。いわゆるstaged builds のコンセプトを適用しており、それはベースイメージ(再利用可能なビルドコンポーネント)をユーザーイメージ(特定の実装のために準備されたイメージ)とずっと切り離しておくためです。
先ほど説明したMakefileを使って、全ての変数は自動的にセットされます。このDockerfileはMakefileがなくても問題なく機能しますが、ユーザーはまだ自分のランタイムの中の変数か、別々のenv-fileをコンフィギュアすることも可能です。個人的には、私はMakefilesの方を使っています。その方が断然楽だからです。
最後にもう一つ。もしあなたが開発モードにあるなら、おそらくユーザールートとしてトラブルシューティングする必要がある特定の問題に直面することもあるでしょう。シンプルに以下を入力してください。
make shell user=root
まとめ
この記事を読んで役に立ててもらえると嬉しいと思っています。完全なコードは githubを参照してください。
さて、新しいサービスを構築するとき、私たちはシンプルに以下のようなファイル―Makefile, Dockerfile ,
docker-compose.yml など — をただコピー/ペーストして自分たちのプロジェクトを始めることができます。そしてそれらが必要な場所を変え、新しいサービスをすぐに起動して実行できます。
Docker + Makefileプロセスを使用すると、パーツがポータブルになるだけでなく、チーム(または世界)で再利用できるようになり、テストルールを添付して出荷できるようになります。 X-Opsの世界へようこそ!
コーディングを楽しんでください!また次の記事でお会いしましょう。
Orangesys.ioでは、kuberneteの運用、DevOps、監視のお手伝いをさせていただいています。ぜひ私たちにおまかせください。