12破壊されたアプリとDocker

長年にわたり、マニフェスト12ファクターアプリをサポートする人々が増え、そこに記載されている規定の実装を開始してきました。 これにより、アプリケーションの展開と管理が大幅に簡素化されました。 ただし、これらの12の要因の実際の適用例は、インターネット上では非常にまれでした。





Dockerでの作業中に、12 Factor App(12FA)の利点がより具体的になりました。 たとえば、12FAでは、標準出力用にロギングを構成し、一般的なイベントストリームとして処理することを推奨しています。 docker logs



使用したことがありますか? これは実際の12FAです!



12FAは、環境変数を使用してアプリケーションを構成することも推奨しています。 Dockerはこれを簡単に行い、コンテナーを作成するときに環境変数をプログラムで設定する機能を提供します。



Dockerと12 Factor Appは、将来のアプリケーションの設計と展開の概要をすばやく提供する素晴らしい組み合わせです。


Dockerはまた、レガシーアプリケーションをコンテナに簡単に移動できるようにします。 最終的にDockerコンテナーをわずかに編集する必要があり、その結果、完全なLinuxディストリビューションの上に2 GBのコンテナーイメージが作成されるため、「一部」と言います。



残念ながら、現在使用しているレガシーアプリケーションには、特にスタートアッププロセスに関して多くの欠点があります。 アプリケーションは、最新のものであっても、依存関係が多すぎるため、クリーンスタートを提供できません。 通常、外部データベースへのアクセスを必要とするアプリケーションは、起動時にデータベースへの接続を開始します。 ただし、このデータベースが利用できなかった場合、または一時的に利用できなかった場合、アプリケーションの多くは起動しません。 運がよければ、トラブルシューティングに役立つエラーメッセージと詳細が表示される場合があります。



Dockerにパッケージ化されている多くのアプリケーションには、いくつかの小さな欠陥があります。 これは、マイクロクラックのようなものです。アプリケーションは引き続き動作しますが、それらを操作するときに地獄のような苦痛を引き起こす可能性があります。


このアプリケーションの動作により、複雑な展開プロセスに頼らざるを得なくなり、PuppetやAnsibleなどのツールの開発に貢献します。 構成管理ツールは、データベースへのアクセス不能など、さまざまな問題の解決に役立ちます。 アプリケーション自体を起動する前に、このアプリケーションが依存するデータベースを起動します。 ほとんどの場合、これは傷口にバンドエイドを貼るのと似ています。 アプリケーションは、返されたエラーの分類と、もちろんエラーログを使用して、データベースへの接続を再試行する必要があります。 この場合、2つの選択肢があります。データベースをオンラインに戻すか、会社が破産するかです。



Dockerに移動するアプリケーションのもう1つの問題は、構成ファイルです。 多くのアプリケーションは、最新のものであっても、ディスク上のローカルにある構成ファイルに依存しています。 最も一般的に使用されるソリューションは、構成ファイルをコンテナーイメージにリンクする新しいコンテナーをさらに展開することです。



これをしないでください。



このソリューションを選択すると、次のような名前のコンテナイメージが無限になります。





すぐに、非常に多くの画像を管理するツールを探す必要があります。



Dockerに移行すると、人々は、どのような形式の構成でも管理する必要がなくなったという誤った意見を得ることができました。 私はこれに同意する傾向があります。画像を作成するときにPuppet、Chef、またはAnsibleを使用する必要はありませんが、作業中に構成設定を制御する必要があります。



同様のロジックを使用して、構成管理システムの頻繁な使用を終了し、 initシステムがdocker run



優先するのを回避します



構成管理ツールと堅牢なinitシステムの不足を補うために、Dockerユーザーはシェルスクリプトを使用して、ブートストラップおよび起動プロセスに関するアプリケーションの欠陥を隠します。



すべてをDockerに転送し、Dockerロゴのないツールの使用を拒否するとすぐに、絶望的な状態に陥ります。


アプリ



次に、典型的なアプリケーションを起動する際のいくつかの一般的なタスクを示すために、サンプルアプリケーションに移りましょう。 この例では、起動時に次のタスクを実行します。





 package main import ( "database/sql" "encoding/json" "fmt" "io/ioutil" "log" "net" "os" _ "github.com/go-sql-driver/mysql" ) var ( config Config db *sql.DB ) type Config struct { DataDir string `json:"datadir"` // Database settings. Host string `json:"host"` Port string `json:"port"` Username string `json:"username"` Password string `json:"password"` Database string `json:"database"` } func main() { log.Println("Starting application...") // Load configuration settings. data, err := ioutil.ReadFile("/etc/config.json") if err != nil { log.Fatal(err) } if err := json.Unmarshal(data, &config); err != nil { log.Fatal(err) } // Use working directory. _, err = os.Stat(config.DataDir) if err != nil { log.Fatal(err) } // Connect to database. hostPort := net.JoinHostPort(config.Host, config.Port) dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?timeout=30s", config.Username, config.Password, hostPort, config.Database) db, err = sql.Open("mysql", dsn) if err != nil { log.Fatal(err) } if err := db.Ping(); err != nil { log.Fatal(err) } }
      
      





完全なソースコードはGitHubで入手できます


ご覧のとおり、ここには特別なものはありませんが、注意深く見ると、このアプリケーションは特定の条件下でのみ自動的にロードされることがわかります。 構成ファイルまたは作業ディレクトリが利用できない場合、または起動時にデータベースにアクセスできない場合、上記のアプリケーションは起動しません。



Dockerを使用してサンプルアプリケーションをデプロイし、調査してみましょう。



docker build



を使用してアプリケーションをビルドします。



 $ GOOS=linux go build -o app
      
      





次に、 docker run



を使用して、アプリからコンテナを作成します:v1のDockerイメージ:



 FROM scratch MAINTAINER Kelsey Hightower <kelsey.hightower@gmail.com> COPY app /app ENTRYPOINT ["/app"]
      
      





ここで行っているのは、アプリケーションバイナリを適切な場所にコピーすることだけです。 このコンテナイメージは、ベースイメージスクリプトを使用して、アプリケーションの展開に適した最小限のDockerコンテナイメージを作成します。



docker build



を使用してイメージを作成します。



 $ docker build -t app:v1 .
      
      





最後に、 docker run



を使用して、アプリからコンテナを作成します:v1イメージ:



 $ docker run --rm app:v1 2015/12/13 04:00:34 Starting application... 2015/12/13 04:00:34 open /etc/config.json: no such file or directory
      
      





痛みを始めましょう! 開始当初、私は最初の起動の問題に遭遇しました。 /etc/config.json



構成ファイルがないため、アプリケーションが起動しないことに注意してください。 実行時に構成ファイルをマウントすることでこれを修正できます。



 $ docker run --rm \ -v /etc/config.json:/etc/config.json \ app:v1 2015/12/13 07:36:27 Starting application... 2015/12/13 07:36:27 stat /var/lib/data: no such file or directory
      
      





もう一つの間違い! 今回は、 /var/lib/data



ディレクトリが存在しないため、アプリケーションを起動できません。 コンテナに別のホストディレクトリをマウントすることで、不足しているディレクトリを簡単にバイパスできます。



 $ docker run --rm \ -v /etc/config.json:/etc/config.json \ -v /var/lib/data:/var/lib/data \ app:v1 2015/12/13 07:44:18 Starting application... 2015/12/13 07:44:48 dial tcp 203.0.113.10:3306: i/o timeout
      
      





進行中ですが、このインスタンスのデータベースアクセスをセットアップするのを忘れました。



これは、一部の人々が構成管理ツールの使用を開始して、これらの依存関係のすべてがアプリケーションの起動前に実行されるようにするポイントです。 これは機能しますが、それでもある程度は過剰であり、アプリケーションレベルの問題を解決するための間違ったアプローチです。



ブートの問題を解決するためにカスタムDockerエントリポイントを使用することを提案することを熱望している「sys管理者」ヒップスターから静かな叫び声が聞こえます。




ユーザーレスキューエントリポイント。



起動時の問題を解決する1つの方法は、シェルスクリプトを作成し、実際のアプリケーションの代わりにDockerエントリポイントとして使用することです。 ここに、シェルスクリプトをエントリポイントとして使用して達成できることの短いリストを示します。







次のシェルスクリプトは最初の2つの要素を使用し、環境変数を/etc/config.json



構成ファイルと共に使用する機能を追加し、起動プロセス中に欠落している/var/lib/data



ディレクトリを作成します。 スクリプトは、サンプルアプリケーションを最終段階として実行し、デフォルトでアプリケーションが起動したときの初期動作を保持します。



 #!/bin/sh set -e datadir=${APP_DATADIR:="/var/lib/data"} host=${APP_HOST:="127.0.0.1"} port=${APP_PORT:="3306"} username=${APP_USERNAME:=""} password=${APP_PASSWORD:=""} database=${APP_DATABASE:=""} cat <<EOF > /etc/config.json { "datadir": "${datadir}", "host": "${host}", "port": "${port}", "username": "${username}", "password": "${password}", "database": "${database}" } EOF mkdir -p ${APP_DATADIR} exec "/app"
      
      





これで、次のDockerファイルを使用してイメージを復元できます。



 FROM alpine:3.1 MAINTAINER Kelsey Hightower <kelsey.hightower@gmail.com> COPY app /app COPY docker-entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]
      
      





カスタムシェルスクリプトはDockerイメージにコピーされ、アプリケーションバイナリの代わりにエントリポイントとして使用されることに注意してください。


docker buildコマンドを使用してapp:v2イメージを作成します。



 $ docker build -t app:v2 .
      
      





次の手順に従ってください:



 $ docker run --rm \ -e "APP_DATADIR=/var/lib/data" \ -e "APP_HOST=203.0.113.10" \ -e "APP_PORT=3306" \ -e "APP_USERNAME=user" \ -e "APP_PASSWORD=password" \ -e "APP_DATABASE=test" \ app:v2 2015/12/13 04:44:29 Starting application...
      
      





ユーザーエントリポイントが機能しています。 環境変数のみを使用して、アプリケーションを構成および実行できます。



しかし、なぜこれを行うのでしょうか?


なぜこのような複雑なラッパースクリプトを使用する必要があるのですか? 一部の人は、この機能をアプリケーションに実装するよりも、シェルにこの機能を書く方がはるかに簡単だと言うでしょう。 ただし、シェルスクリプトの管理だけではありません。 v1ファイルとv2ファイルの別の違いに注目してください。



 FROM alpine:3.1
      
      





v2ファイルは、アルパインベースイメージを使用してスクリプト環境を提供しますが、Dockerイメージのサイズは2倍になります。



 $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE app v2 1b47f1fbc7dd 2 hours ago 10.99 MB app v1 42273e8664d5 2 hours ago 5.952 MB
      
      





このアプローチのもう1つの欠点は、イメージで構成ファイルを使用できないことです。 スクリプトの記述を続け、構成ファイルと環境変数のサポートを追加できますが、ラッパースクリプトがアプリケーションと同期しなくなると、これらすべてが機能を失うだけです。 しかし、この問題を解決する別の方法があります。



プログラミングは全員を救います。



はい、古き良きプログラミング。 Dockerエントリポイントのシェルスクリプトの各タスクは、アプリケーションで直接処理できます。



誤解しないでください。エントリポイントスクリプトの使用は、管理していないアプリケーションに適しています。 ただし、アプリケーションのエントリポイントスクリプトに依存している場合、アプリケーション展開プロセスに別のレベルの複雑さが不当に追加されます。



構成ファイルはオプションである必要があります



90年代後半以降、構成ファイルを使用する理由はまったくないと思います。 構成ファイルが存在し、デフォルト設定にロールバックする場合は、ロードすることをお勧めします。 次のコードスニペットはそれを行います。



 // Load configuration settings. data, err := ioutil.ReadFile("/etc/config.json") // Fallback to default values. switch { case os.IsNotExist(err): log.Println("Config file missing using defaults") config = Config{ DataDir: "/var/lib/data", Host: "127.0.0.1", Port: "3306", Database: "test", } case err == nil: if err := json.Unmarshal(data, &config); err != nil { log.Fatal(err) } default: log.Println(err) }
      
      





構成に環境変数を使用します。


これは、アプリケーションで直接実行できる最も単純なものの1つです。 次のコードスニペットは、環境変数を使用して構成設定をオーバーライドします。



 log.Println("Overriding configuration from env vars.") if os.Getenv("APP_DATADIR") != "" { config.DataDir = os.Getenv("APP_DATADIR") } if os.Getenv("APP_HOST") != "" { config.Host = os.Getenv("APP_HOST") } if os.Getenv("APP_PORT") != "" { config.Port = os.Getenv("APP_PORT") } if os.Getenv("APP_USERNAME") != "" { config.Username = os.Getenv("APP_USERNAME") } if os.Getenv("APP_PASSWORD") != "" { config.Password = os.Getenv("APP_PASSWORD") } if os.Getenv("APP_DATABASE") != "" { config.Database = os.Getenv("APP_DATABASE") }
      
      





アプリケーションの作業ディレクトリを管理します。


作業の責任とディレクトリとの接続を外部ツールまたはエントリポイントスクリプトに変更する代わりに、アプリケーションで直接管理する必要があります。 何らかの理由で何かが機能しない場合は、詳細を含むエラーロギングを設定することを忘れないでください。



 // Use working directory. _, err = os.Stat(config.DataDir) if os.IsNotExist(err) { log.Println("Creating missing data directory", config.DataDir) err = os.MkdirAll(config.DataDir, 0755) } if err != nil { log.Fatal(err) }
      
      





特定の順序でサービスを開始する必要性を排除


特定の順序でアプリケーションのデプロイメント要件を削除します。 さまざまなアプリケーションの多くの展開ガイドには、データベースの起動後にアプリケーションを起動する指示があることがわかりました。そうでない場合、結果はnullになります。



この要件は、次のようにして取り除くことができます。



 $ docker run --rm \ -e "APP_DATADIR=/var/lib/data" \ -e "APP_HOST=203.0.113.10" \ -e "APP_PORT=3306" \ -e "APP_USERNAME=user" \ -e "APP_PASSWORD=password" \ -e "APP_DATABASE=test" \ app:v3 2015/12/13 05:36:10 Starting application... 2015/12/13 05:36:10 Config file missing using defaults 2015/12/13 05:36:10 Overriding configuration from env vars. 2015/12/13 05:36:10 Creating missing data directory /var/lib/data 2015/12/13 05:36:10 Connecting to database at 203.0.113.10:3306 2015/12/13 05:36:40 dial tcp 203.0.113.10:3306: i/o timeout 2015/12/13 05:37:11 dial tcp 203.0.113.10:3306: i/o timeout
      
      





上記の出力では、203.0.113.10にある作業ターゲットデータベースに接続できないことに注意してください。


次のコマンドを実行して、MySQLデータベースへのアクセスを許可します。



 $ gcloud sql instances patch mysql \ --authorized-networks "203.0.113.20/32"
      
      





アプリケーションはデータベースに接続し、起動プロセスを完了することができます。



 2015/12/13 05:37:43 dial tcp 203.0.113.10:3306: i/o timeout 2015/12/13 05:37:46 Application started successfully.
      
      





実行するコードは次のようになります。



 // Connect to database. hostPort := net.JoinHostPort(config.Host, config.Port) log.Println("Connecting to database at", hostPort) dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?timeout=30s", config.Username, config.Password, hostPort, config.Database) db, err = sql.Open("mysql", dsn) if err != nil { log.Println(err) } var dbError error maxAttempts := 20 for attempts := 1; attempts <= maxAttempts; attempts++ { dbError = db.Ping() if dbError == nil { break } log.Println(dbError) time.Sleep(time.Duration(attempts) * time.Second) } if dbError != nil { log.Fatal(dbError) }
      
      





ここでは特別なことは何もありません。 データベース接続を繰り返し、各試行間の時間を増やします。



すばらしいことに、アプリケーションが正常に起動したというわかりやすいメッセージがログに記録された起動プロセスが得られました。



 log.Println("Application started successfully.")
      
      





私を信じて、あなたのシステム管理者はあなたに感謝します。



ここから元のソースへのリンクを見つけることができます



All Articles