20200630のdockerに関する記事は17件です。

Kabaneroを使ったアプリケーションの開発フローを確認してみた。

はじめに

勉強用にKabaneroを使ったアプリケーションの開発フロー(ローカル環境はDocker、本番環境はOpenShift)を作りながら動作確認した時の内容をまとめてみました。

Kabaneroとは

Kabaneroは、マイクロサービスベースのフレームワークで、開発者、アーキテクト、および運用チームがより迅速に連携できるしくみを作るIBM発のOSSのプロジェクトです。
image.png
Kabaneroは IBM Cloud Pak for ApplicationsにAccelerator for Teamsという名称で同梱され主要な機能になっています。
Kabanero がアップストリームプロジェクトで Kabanero → Cloud Pak for Applications(Accelerator for Teams)の順番でリリースされます。

Appsody

image.png

KabaneroのアンブレラプロジェクトであるappsodyがDockerの開発ライフサイクルを抽象化しています。
そのため、開発者はdockerのコマンドやDockerfileなど知らなくても開発することができます。

例えば、

  • appsody cliコマンドを使ってDockerを使ったローカル環境での開発からKubernetes、OpenShiftへのデプロイまで制御します。
  • stackというNode.js、Eclipse Microprofile、Quarkus、Spring Boot などのオープン・ソースのランタイムとフレームワークに対応する事前構成されたテンプレート(ベースコンテナイメージはRHEL8などのUBIイメージ)用意されています。

image.png

Kabaneroを使った開発者のワークフロー

では、Kabaneroを使うと開発者のワークフローはどのように変わるのでしょうか
フロー図に沿って、手順を確認していきたいと思います。

  • 前提
    • appsodyがインストールされていること
    • ローカル環境にDockerがインストールされていること
    • (本番環境のリリース用)OpenShiftに Kabaneroがインストールされていること

Appsody Hubから stackの一覧を取得

image.png

開発者のJaneは appsody listコマンドを使用して、利用できる stackの一覧を取得します。

appsody list 

REPO        ID                              VERSION     TEMPLATES                   DESCRIPTION
kabanero    java-microprofile [Deprecated]  0.2.27      *default                    Eclipse MicroProfile on Open Liberty & OpenJ9 using Maven
kabanero    java-openliberty                0.2.3       *default                    Open Liberty & OpenJ9 using Maven
kabanero    java-spring-boot2               0.3.29      *default, kafka, kotlin     Spring Boot using OpenJ9 and Maven
kabanero    nodejs                          0.3.6       *simple                     Runtime for Node.js applications
kabanero    nodejs-express                  0.4.8       kafka, scaffold, *simple    Express web framework for Node.js

REPO列がリポジトリ名、IDが stack名、TEMPLATESはアプリケーションのソースコードのテンプレート名です。

appsodyのプロジェクトの作成

image.png

appsody listコマンドで出力されたstackの中から、今回は java-openliberty stackを使って開発したいと思います。

まず、プロジェクトの保存先のディレクトリを作成します。

mkdir sample-application
cd sample-application

そして、appsody init "stack名"を実行して、開発環境を作成します。

# appsody init kabanero/java-openliberty
Checking stack requirements...
Docker requirements met
Appsody requirements met
Running appsody init...
Downloading java-openliberty template project from https://github.com/appsody/stacks/releases/download/java-openliberty-v0.2.12/incubator.java-openlibert                                                       y.v0.2.12.templates.default.tar.gz
Download complete. Extracting files from /root/sample-application/java-openliberty.tar.gz
Setting up the development environment
Your Appsody project name has been set to sample-application
Pulling docker image docker.io/appsody/java-openliberty:0.2
Running command: docker pull docker.io/appsody/java-openliberty:0.2
0.2: Pulling from appsody/java-openliberty
ee2244abc66f: Pulling fs layer
befb03b11956: Pulling fs layer
4d3fb84d4d60: Pulling fs layer
d0aba8033c85: Pull complete
e7e883ba65bb: Pull complete
Digest: sha256:4dff3ca3d27b910b8dacf84895f5763d96df9671b7bbd5694ae14b4624d47dbd
Status: Downloaded newer image for appsody/java-openliberty:0.2
docker.io/appsody/java-openliberty:0.2
[Warning] The stack image does not contain APPSODY_PROJECT_DIR. Using /project
Running command: docker run --rm --entrypoint /bin/bash docker.io/appsody/java-openliberty:0.2 -c "find /project -type f -name .appsody-init.sh"
Extracting project from development environment
[Warning] The stack image does not contain APPSODY_PROJECT_DIR. Using /project
Running command: docker create --name sample-application-extract -v /root/.m2/repository:/mvn/repository -v /root/sample-application/:/project/user-app docker.io/appsody/java-openliberty:0.2
Running command: docker cp sample-application-extract:/project /root/.appsody/extract/sample-application
Project extracted to /root/sample-application/.appsody_init
Running command: docker rm sample-application-extract -f
Running command: ./.appsody-init.sh
[InitScript] [INFO] Scanning for projects...
[InitScript] [INFO]
[InitScript] [INFO] --------------------< dev.appsody:java-openliberty >--------------------
[InitScript] [INFO] Building java-openliberty 0.2.14
[InitScript] [INFO] --------------------------------[ pom ]---------------------------------
[InitScript] [INFO]
[InitScript] [INFO] --- maven-enforcer-plugin:3.0.0-M3:enforce (default-cli) @ java-openliberty ---
[InitScript] [INFO] Skipping Rule Enforcement.
[InitScript] [INFO]
[InitScript] [INFO] --- maven-install-plugin:2.4:install (default-install) @ java-openliberty ---
[InitScript] [INFO] Installing /root/sample-application/.appsody_init/pom.xml to /root/.m2/repository/dev/appsody/java-openliberty/0.2.14/java-openliberty-0.2.14.pom
[InitScript] [INFO] ------------------------------------------------------------------------
[InitScript] [INFO] BUILD SUCCESS
[InitScript] [INFO] ------------------------------------------------------------------------
[InitScript] [INFO] Total time:  1.354 s
[InitScript] [INFO] Finished at: 2020-07-01T03:24:37+09:00
[InitScript] [INFO] ------------------------------------------------------------------------
Successfully added your project to /root/.appsody/project.yaml
Your Appsody project ID has been set to 20200701032437.87801097
Successfully initialized Appsody project with the kabanero/java-openliberty stack and the default template.

initが終了した後、sample-applicationディレクトリ構成は以下の通りです。

.
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── dev
    │   │       └── appsody
    │   │           └── starter
    │   │               ├── health
    │   │               │   ├── StarterLivenessCheck.java
    │   │               │   └── StarterReadinessCheck.java
    │   │               ├── StarterApplication.java
    │   │               └── StarterResource.java
    │   ├── liberty
    │   │   └── config
    │   │       ├── configDropins
    │   │       │   └── defaults
    │   │       │       └── quick-start-security.xml
    │   │       └── server.xml
    │   └── webapp
    │       ├── index.html
    │       └── WEB-INF
    │           └── beans.xml
    └── test
        └── java
            └── it
                └── dev
                    └── appsody
                        └── starter
                            ├── EndpointTest.java
                            └── HealthEndpointTest.java

開発者Janeは、この構成をベースに開発を進めます。

ローカル環境でのアプリケーションの動作確認

開発者Janeは、アプリケーションの開発の途中、任意のタイミングでアプリケーションを起動して動作確認するのには
appsody では run / test / debug コマンドを使用てJaneのアプリケーションを起動します。

image.png

では、 appsody run コマンドを実行して、アプリケーションを起動します。

# appsody run
Running development environment...
Pulling docker image docker.io/appsody/java-openliberty:0.2
Running command: docker pull docker.io/appsody/java-openliberty:0.2
0.2: Pulling from appsody/java-openliberty
Digest: sha256:4dff3ca3d27b910b8dacf84895f5763d96df9671b7bbd5694ae14b4624d47dbd
Status: Image is up to date for appsody/java-openliberty:0.2
docker.io/appsody/java-openliberty:0.2
[Warning] The stack image does not contain APPSODY_DEPS
Running command: docker run --rm -p 7777:7777 -p 9080:9080 -p 9443:9443 --name sample-application -u 0:0 -e "APPSODY_USER=0" -e "APPSODY_GROUP=0" -v /root/.m2/repository:/mvn/repository -v /root/sample-application/:/project/user-app -v appsody-controller-0.3.5:/.appsody -t --entrypoint /.appsody/appsody-controller docker.io/appsody/java-openliberty:0.2 "--mode=run"
[Container] Running APPSODY_PREP command: /project/run-stack.sh prep
[Container] Installing parent dev.appsody:java-openliberty:0.2.14
[Container] [INFO] Scanning for projects...
[Container] [INFO]
[Container] [INFO] --------------------< dev.appsody:java-openliberty >--------------------
[Container] [INFO] Building java-openliberty 0.2.14
[Container] [INFO] --------------------------------[ pom ]---------------------------------
[Container] [INFO]
[Container] [INFO] --- maven-enforcer-plugin:3.0.0-M3:enforce (default-cli) @ java-openliberty ---
[Container] [INFO] Skipping Rule Enforcement.
[Container] [INFO]
[Container] [INFO] --- maven-install-plugin:2.4:install (default-install) @ java-openliberty ---
[Container] [INFO] Installing /project/user-app/../pom.xml to /mvn/repository/dev/appsody/java-openliberty/0.2.14/java-openliberty-0.2.14.pom
[Container] [INFO] ------------------------------------------------------------------------
[Container] [INFO] BUILD SUCCESS
[Container] [INFO] ------------------------------------------------------------------------
[Container] [INFO] Total time:  0.932 s
(省略)
[Container] [INFO] [AUDIT   ] CWWKE0001I: The server defaultServer has been launched.
[Container] [INFO] [AUDIT   ] CWWKG0093A: Processing configuration drop-ins resource: /opt/ol/wlp/usr/servers/defaultServer/configDropins/defaults/quick-start-security.xml
[Container] [INFO] [AUDIT   ] CWWKZ0058I: Monitoring dropins for applications.
[Container] [INFO] [AUDIT   ] CWPKI0820A: The default keystore has been created using the 'keystore_password' environment variable.
[Container] [INFO] [AUDIT   ] CWWKS4104A: LTPA keys created in 3.284 seconds. LTPA key file: /opt/ol/wlp/usr/servers/defaultServer/resources/security/ltpa.keys
[Container] [INFO] [AUDIT   ] CWPKI0803A: SSL certificate created in 4.596 seconds. SSL key file: /opt/ol/wlp/usr/servers/defaultServer/resources/security/key.p12
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/health/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/ibm/api/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/jwt/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/openapi/ui/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/metrics/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/openapi/
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/
[Container] [INFO] [AUDIT   ] CWWKZ0001I: Application starter-app started in 18.902 seconds.
[Container] [INFO] [AUDIT   ] CWWKF0012I: The server installed the following features: [appSecurity-2.0, cdi-2.0, concurrent-1.0, distributedMap-1.0, jaxrs-2.1, jaxrsClient-2.1, jndi-1.0, json-1.0, jsonb-1.0, jsonp-1.1, jwt-1.0, microProfile-3.2, mpConfig-1.3, mpFaultTolerance-2.0, mpHealth-2.1, mpJwt-1.1, mpMetrics-2.2, mpOpenAPI-1.1, mpOpenTracing-1.3, mpRestClient-1.3, opentracing-1.3, servlet-4.0, ssl-1.0].
[Container] [INFO] [AUDIT   ] CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 49.562 seconds.
[Container] [INFO] CWWKM2015I: Match number: 1 is [6/30/20 21:47:36:813 UTC] 0000002e com.ibm.ws.kernel.feature.internal.FeatureManager            A CWWKF0011I: The defaultServer server is ready to run a smarter planet. The defaultServer server started in 49.562 seconds..
[Container] [INFO] Tests will run automatically when changes are detected.
[Container] [INFO] Source compilation was successful.
[Container] [INFO] Tests compilation was successful.
[Container] [INFO] [AUDIT   ] CWWKT0017I: Web application removed (default_host): http://023d8b3431cd:9080/
[Container] [INFO] [AUDIT   ] CWWKZ0009I: The application starter-app has stopped successfully.
[Container] [INFO] Running unit tests...
[Container] [INFO] Unit tests finished.
[Container] [INFO] Waiting up to 30 seconds to find the application start up or update message...
[Container] [INFO] CWWKM2010I: Searching for (CWWKZ0001I.*|CWWKZ0003I.*starter-app) in /opt/ol/wlp/usr/servers/defaultServer/logs/messages.log. This search will timeout after 30 seconds.
[Container] [INFO] CWWKM2015I: Match number: 1 is [6/30/20 21:47:36:038 UTC] 00000039 com.ibm.ws.app.manager.AppMessageHelper                      A CWWKZ0001I: Application starter-app started in 18.902 seconds..
[Container] [INFO] Running integration tests...
[Container] [INFO] [AUDIT   ] CWWKT0016I: Web application available (default_host): http://023d8b3431cd:9080/
[Container] [INFO] [AUDIT   ] CWWKZ0003I: The application starter-app updated in 2.628 seconds.
[Container] [INFO]
[Container] [INFO] -------------------------------------------------------
[Container] [INFO]  T E S T S
[Container] [INFO] -------------------------------------------------------
[Container] [INFO] Running it.dev.appsody.starter.HealthEndpointTest
[Container] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.048 s - in it.dev.appsody.starter.HealthEndpointTest
[Container] [INFO] Running it.dev.appsody.starter.EndpointTest
[Container] [INFO] [WARNING ] CWMOT0010W: OpenTracing cannot track JAX-RS requests because an OpentracingTracerFactory class was not provided or client libraries for tracing backend are not in the class path.
[Container] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.056 s - in it.dev.appsody.starter.EndpointTest
[Container] [INFO]
[Container] [INFO] Results:
[Container] [INFO]
[Container] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[Container] [INFO]
[Container] [INFO] Integration tests finished.

ログからは、docker pull、maven、OpenLibertyの起動、IntegrationTestの実行されているのがわかりますが、
OpenLibertyが起動したので、 起動確認として、healthチェックがupになっているのを確認します。

# curl http://localhost:9080/health
{"checks":[{"data":{},"name":"StarterLivenessCheck","status":"UP"},{"data":{},"name":"StarterReadinessCheck","status":"UP"}],"status":"UP"}

これで、ローカル環境でのアプリケーションの起動を確認することができました。
Janeは開発が終了まで、ソースコードの作成とappsody runコマンドで動作確認を繰り返すことになります。

OpenShiftへリリースするためのアプリケーションのビルド

image.png

Janeはアプリケーションの開発が終了したので、OpenShiftへデプロイします。
まずは、appsody buildコマンドを実行して、本番環境用のコンテナイメージをビルドします。
appsody runの時はローカル環境用のbuilderを使って、コンテナイメージを生成していましたが、appsody buildの場合は本番環境のbuilderを使ってコンテナイメージを生成します。

# appsody build
Extracting project from development environment
Pulling docker image docker.io/appsody/java-openliberty:0.2
Running command: docker pull docker.io/appsody/java-openliberty:0.2
0.2: Pulling from appsody/java-openliberty
Digest: sha256:4dff3ca3d27b910b8dacf84895f5763d96df9671b7bbd5694ae14b4624d47dbd
Status: Image is up to date for appsody/java-openliberty:0.2
docker.io/appsody/java-openliberty:0.2
[Warning] The stack image does not contain APPSODY_PROJECT_DIR. Using /project
Running command: docker create --name sample-application-extract -v /root/.m2/repository:/mvn/repository -v /root/sample-application/:/project/user-app docker.io/appsody/java-openliberty:0.2
Running command: docker cp sample-application-extract:/project /root/.appsody/extract/sample-application
Project extracted to /root/.appsody/extract/sample-application
Running command: docker rm sample-application-extract -f
(省略)
[Docker] Sending build context to Docker daemon    706kB
[Docker] Step 1/62 : FROM kabanero/ubi8-maven:0.9.0 as compile
[Docker] 0.9.0: Pulling from kabanero/ubi8-maven
(省略)
[Docker] Removing intermediate container 587544680ab9
[Docker]  ---> 6cb69d5ff953
[Docker] Successfully built 6cb69d5ff953
[Docker] Successfully tagged dev.local/sample-application:latest
Built docker image dev.local/sample-application
Running command: docker create --name sample-application-extract docker.io/appsody/java-openliberty:0.2
Running command: docker cp sample-application-extract:/config/app-deploy.yaml /root/sample-application/app-deploy.yaml
Running command: docker rm sample-application-extract -f
Created deployment manifest: /root/sample-application/app-deploy.yaml

OpenShiftへのデプロイは OpenShiftにインストールされた Appsody Operatorがアプリケーションをデプロイします。
最後に作成されたapp-deploy.yamlは、Appsody Operatorに渡すためのmanifestです。

  • app-deploy.yaml
apiVersion: openliberty.io/v1beta1
kind: OpenLibertyApplication
metadata:
  annotations:
    architecture: x86_64
    authoritative-source-url: registry.access.redhat.com
    build-date: 2020-03-31T14:54:13.907559
    commit.stack.appsody.dev/contextDir: /incubator/java-openliberty
    commit.stack.appsody.dev/date: Thu Jun 4 13:29:00 2020 +0100
    commit.stack.appsody.dev/message: 'java-openliberty: define APPSODY_DEBUG_PORT
      (#818)'
    description: Eclipse MicroProfile & Jakarta EE on Open Liberty & OpenJ9 using
      Maven
    distribution-scope: public
    image.opencontainers.org/created: "2020-07-01T07:13:29+09:00"
    k8s.io/description: The Universal Base Image is designed and engineered to be
      the base layer for all of your containerized applications, middleware and utilities.
      This base image is freely redistributable, but Red Hat only supports Red Hat
      technologies through subscriptions for Red Hat products. This image is maintained
      by Red Hat and updated regularly.
    k8s.io/display-name: Red Hat Universal Base Image 8
    maintainer: Red Hat, Inc.
    name: java-openliberty
    openshift.io/expose-services: ""
    openshift.io/tags: base rhel8
    redhat.com/build-host: cpt-1007.osbs.prod.upshift.rdu2.redhat.com
    redhat.com/component: ubi8-container
    redhat.com/license_terms: https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI
    release: "408"
    stack.appsody.dev/authors: Mike Andrasak <uberskigeek>, Andy Mauer <ajm01>, Scott
      Kurz <scottkurz>, Adam Wisniewski <awisniew90>
    stack.appsody.dev/configured: docker.io/appsody/java-openliberty:0.2
    stack.appsody.dev/created: "2020-06-04T12:31:59Z"
    stack.appsody.dev/description: Eclipse MicroProfile & Jakarta EE on Open Liberty
      & OpenJ9 using Maven
    stack.appsody.dev/digest: sha256:4dff3ca3d27b910b8dacf84895f5763d96df9671b7bbd5694ae14b4624d47dbd
    stack.appsody.dev/documentation: https://github.com/appsody/stacks/tree/master/incubator/java-openliberty/README.md
    stack.appsody.dev/licenses: Apache-2.0
    stack.appsody.dev/revision: 1b105b4891a3edef718d668271da30086214dd84
    stack.appsody.dev/source: https://github.com/appsody/stacks/tree/master/incubator/java-openliberty/image
    stack.appsody.dev/tag: docker.io/appsody/java-openliberty:0.2.14
    stack.appsody.dev/title: Open Liberty
    stack.appsody.dev/url: https://github.com/appsody/stacks/tree/master/incubator/java-openliberty
    summary: Open Liberty
    url: https://access.redhat.com/containers/#/registry.access.redhat.com/ubi8/images/8.1-408
    vcs-ref: 26f36bfa3e3a04c8c866b250924c1aefc34f01c9
    vcs-type: git
    vendor: Open Liberty
    version: 0.2.14
  creationTimestamp: null
  labels:
    image.opencontainers.org/title: sample-application
    stack.appsody.dev/id: java-openliberty
    stack.appsody.dev/version: 0.2.14
  name: sample-application
  namespace: sampleapps1
spec:
  applicationImage: dev.local/sample-application
  createKnativeService: false
  expose: true
  livenessProbe:
    failureThreshold: 12
    httpGet:
      path: /health/live
      port: 9080
    initialDelaySeconds: 5
    periodSeconds: 2
  readinessProbe:
    failureThreshold: 12
    httpGet:
      path: /health/ready
      port: 9080
    initialDelaySeconds: 5
    periodSeconds: 2
    timeoutSeconds: 1
  service:
    annotations:
      prometheus.io/scrape: "true"
    port: 9080
    type: NodePort
  version: 1.0.0

アプリケーションのOpenShiftへのデプロイ

image.png

buildが終了すると本番環境のコンテナイメージが生成されます。このイメージをOpenShiftにデプロイするためには appsody deployコマンドでデプロイします。

その前に、OpenShiftの環境を準備します。

プロジェクト(namespace)の作成

アプリケーションをデプロイするためのプロジェクト( sampleapps )を作成します。

# oc new-project sampleapps
Now using project "sampleapps" on server "https://api.lab.example.com:6443".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app ruby~https://github.com/sclorg/ruby-ex.git

to build a new example application in Python. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=gcr.io/hello-minikube-zero-install/hello-node

現時点では、 sampleappsプロジェクトには何もリソースはありません。

# oc get all -n sampleapps
No resources found in sampleapps namespace.

次に"oc get csv"コマンドを実行すると、sampleappsプロジェクトでは以下のOperatorが使えるようになっていますが、
このOperatorは、KabaneroをInstallしたときに導入されたOperatorが、sampleappsプロジェクトにも展開されています。

# oc get csv -n sampleapps
NAME                                         DISPLAY                          VERSION               REPLACES                                     PHASE
appsody-operator.v0.5.1                      Appsody Operator                 0.5.1                                                              Succeeded
elasticsearch-operator.4.3.26-202006160135   Elasticsearch Operator           4.3.26-202006160135   elasticsearch-operator.4.3.25-202006081335   Succeeded
jaeger-operator.v1.17.3                      Red Hat OpenShift Jaeger         1.17.3                jaeger-operator.v1.17.2                      Succeeded
kiali-operator.v1.12.13                      Kiali Operator                   1.12.13               kiali-operator.v1.12.12                      Succeeded
open-liberty-operator.v0.5.1                 Open Liberty Operator            0.5.1                                                              Succeeded
openshift-pipelines-operator.v0.11.2         OpenShift Pipelines Operator     0.11.2                                                             Succeeded
serverless-operator.v1.7.1                   OpenShift Serverless Operator    1.7.1                                                              Succeeded
servicemeshoperator.v1.1.3                   Red Hat OpenShift Service Mesh   1.1.3                 servicemeshoperator.v1.1.2.3                 Succeeded

次に、OpenShiftの内部レジストリに本番環境用のコンテナイメージpushするために、内部レジストリにログインします。

ログインするために必要な内部レジストリのURLを変数HOSTに格納します。

HOST=$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}')
# echo ${HOST}
default-route-openshift-image-registry.apps.lab.example.com

そして、内部レジストリにログインして、ローカル環境からOpenShiftにコンテナイメージをpushできるようにします。

# docker login -u kubeadmin -p $(oc whoami -t) $HOST
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

最後に appsody deployコマンドを使用して、OpenShiftにデプロイします。

# appsody deploy --tag sampleapps/sample-application:v1 --push-url $HOST --push --pull-url docker-registry.default.svc:5000 --namespace sampleapps

-- tag プロジェクト名/コンテナイメージ名:tag

--push-url $HOST --push は ローカル環境からpushするためにアドレスを記述
--pull-url は OpenShift内部で、デプロイするのに pullするために必要なURLを記述

デプロイが終了して、"oc get all -n sampleapps" コマンドを実行すると podが起動しているのが確認できます。

# oc get all -n sampleapps
NAME                                      READY   STATUS    RESTARTS   AGE
pod/sample-application-6d56c87cdf-fhrs2   0/1     Running   0          42s

NAME                         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
service/sample-application   NodePort   172.30.145.68   <none>        9080:30957/TCP   43s

NAME                                 READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sample-application   0/1     1            0           42s

NAME                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/sample-application-6d56c87cdf   1         1         0       42s

NAME                                                IMAGE REPOSITORY                                                                          TAGS     UPDATED
imagestream.image.openshift.io/sample-application   default-route-openshift-image-registry.apps.lab.example.com/sampleapps/sample-application   latest   2 minutes ago

NAME                                          HOST/PORT                                          PATH   SERVICES             PORT       TERMINATION   WILDCARD
route.route.openshift.io/sample-application   sample-application-sampleapps.apps.lab.example.com          sample-application   9080-tcp                 None

外部からcurlでhealthチェックのURLにアクセスすると statusがupになってるのが確認できました。

# curl http://sample-application-sampleapps.apps.lab.example.com/health
{"checks":[{"data":{},"name":"StarterLivenessCheck","status":"UP"},{"data":{},"name":"StarterReadinessCheck","status":"UP"}],"status":"UP"}

Kabaneroを使った開発フローのまとめ

これまでのkabaneroを使ったアプリケーションの開発フローをまとめると下図の様ながれになります。
image.png

これまでの流れから、appsody cliのコマンドを使うとdockerコマンドが抽象化されているので、開発者はdockerの作法知らなくても、開発できそうです。
また、stackをカスタマイズすると、それぞれの組織にあったstackもできそうです。
そして、5. deployでは、appsody operatorがアプリケーションをOpenShiftにデプロイしているのは初見でした。
もう少し勉強をすすめていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Docker/Kubernetes 実践コンテナ開発入門」の7章をHelm v3でやってみた

はじめに

「Docker/Kubernetes 実践コンテナ開発入門」を読みながらKubernetesを勉強していたのですが、コンテナ技術の発展が早く、出版が2018年8月ということで内容が少し古くなってきました。特に7章3節のHelmの解説では、Helmのバージョンがv31に上がったことによるコマンド変更があり、Helm v3を使用する場合には手順がそのまま使えなくなっています。この記事ではHelm v3を使用した場合の手順について解説します。

前提

環境

  • mac OS Catalina 10.15.5
  • プロセッサ: 2.3 GHz デュアルコアIntel Core i5
  • メモリ: 8 GB 2133 MHz LPDDR3

dockerはdocker for macを使用してローカルで実行しました。

$ docker --version
Docker version 19.03.8, build afacb8b
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.4", GitCommit:"c96aede7b5205121079932896c4ad89bb93260af", GitTreeState:"clean", BuildDate:"2020-06-18T02:59:13Z", GoVersion:"go1.14.3", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"16+", GitVersion:"v1.16.6-beta.0", GitCommit:"e7f962ba86f4ce7033828210ca3556393c377bcc", GitTreeState:"clean", BuildDate:"2020-01-15T08:18:29Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}

また、VS Codeでyamlを編集していたのですが、自動整形をした際の中括弧の挙動がおかしかったので、VS Code拡張機能のvscode-helmをインストールしました。

手順

Helmのセットアップ

kubectlをローカルkubernetesクラスタに切り替えておきます。
HomebrewでHelmをインストールします。

$ brew install helm
$ helm version
version.BuildInfo{Version:"v3.2.4", GitCommit:"0ad800ef43d3b826f31a5ad8dfbb4fe05d143688", GitTreeState:"dirty", GoVersion:"go1.14.3"}

v3でTillerサーバが廃止されたので、helm initは不要です。
v2ではkubernetesクラスタ上にTillerサーバがデプロイされ、クライアントのhelmコマンドでTillerサーバのAPIを呼び出し、命令を受けたTillerサーバがkubernetesクラスタのAPIを呼び出すという流れになっていました。拡張性のある構成だとは思いますが、Tillerサーバ周りのサービス・アカウントの設定が複雑になるということで使い勝手が悪かったようです。v3 ではTillerサーバが廃止され、クライアントから直接kubernetesクラスタのAPIを呼び出すようになりました。マニフェストの生成もクライアントで行われるようになっています。詳細は下記のサイトをご覧ください。

Helmの概念

v2とは違いstableリポジトリはデフォルトでは利用できなくなっています。次のようにすればstableリポジトリを追加できます。

$ helm repo add stable https://kubernetes-charts.storage.googleapis.com/

stableリポジトリは将来廃止される予定で、分散型のリポジトリに移行するようです。

The stable repository is no longer added by default. This repository will be deprecated during the life of Helm v3 and we are now moving to a distributed model of repositories that can be searched by the Helm Hub.
Ref: https://helm.sh/blog/helm-v3-beta/

リポジトリの検索はhelm searchで行いますが、repohubの2つで検索を指定します。hubの検索は一部のリポジトリのみが表示されます。そのリポジトリからChartを使用するには、関連するリポジトリをHelmリポジトリリストに追加する必要があります。

$ helm search -h

Search provides the ability to search for Helm charts in the various places
they can be stored including the Helm Hub and repositories you have added. Use
search subcommands to search different locations for charts.

Usage:
  helm search [command]

Available Commands:
  hub         search for charts in the Helm Hub or an instance of Monocular
  repo        search repositories for a keyword in charts

Flags:
...

Chartをインストールする

redmineをインストールします。bitnamiリポジトリを追加します。一応リポジトリの情報を最新化しておきます。

$ helm repo add bitnami https://charts.bitnami.com/bitnami
$ helm repo update

インストールするChartの情報です。

$ helm show chart bitnami/redmine
helm show chart bitnami/redmine
apiVersion: v1
appVersion: 4.1.1
dependencies:
- condition: mariadb.enabled
  name: mariadb
  repository: https://charts.bitnami.com/bitnami
  version: 7.x.x
- condition: postgresql.enabled
  name: postgresql
  repository: https://charts.bitnami.com/bitnami
  version: 8.x.x
description: A flexible project management web application.
home: http://www.redmine.org/
icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png
keywords:
- redmine
- project management
- www
- http
- web
- application
- ruby
- rails
maintainers:
- email: containers@bitnami.com
  name: Bitnami
name: redmine
sources:
- https://github.com/bitnami/bitnami-docker-redmine
version: 14.2.4

インストールします。パラメータをredmine.yamlで定義します。

redmine.yaml
redmineUsername: abeT
redminePassword: abeT
redmineLanguage: ja

service:
  type: NodePort

リリース名は--nameで指定せず、helm installの直後に指定します。

$ helm install -f redmine.yaml redmine bitnami/redmine
NAME: redmine
LAST DEPLOYED: Fri Jun 26 15:42:19 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the Redmine URL:

  export NODE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].nodePort}" services redmine)
  export NODE_IP=$(kubectl get nodes --namespace default -o jsonpath="{.items[0].status.addresses[0].address}")
  echo "Redmine URL: http://$NODE_IP:$NODE_PORT/"

2. Login with the following credentials

  echo Username: abeT
  echo Password: $(kubectl get secret --namespace default redmine -o jsonpath="{.data.redmine-password}" | base64 --decode)

インストールされたものの一覧を表示します。

$ helm ls
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART           APP VERSION
redmine default     1           2020-06-26 13:55:26.228257 +0900 JST    deployed    redmine-14.2.4  4.1.1 

作成されたリソースを確認します。

$ kubectl get service,deployment
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/kubernetes        ClusterIP   10.96.0.1        <none>        443/TCP        24m
service/redmine           NodePort    10.97.203.207    <none>        80:30677/TCP   114s
service/redmine-mariadb   ClusterIP   10.110.204.194   <none>        3306/TCP       114s

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/redmine   1/1     1            1           114s

service/redmineNodePortになっており、http://localhost:30677でredmineにアクセスできるようになっています。

※ deployment.apps/redmineがREADYにならず、止まってしまう事象が発生しました。原因は不明ですが(多分メモリの不足?)、Reset Kubernetes Clusterでクラスターを全削除してから実行するとうまくいく場合があります。

スクリーンショット 2020-06-26 16.09.34.png

アンインストール

次のコマンドでアンインストールを実行できます。v3ではデフォルトではリビジョンの記録が残りません。--keep-historyを付与することで、リビジョンを残しておくことができます。

$ helm uninstall redmine --keep-history
release "redmine" uninstalled

リビジョンを確認できます。

$ helm ls --all
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART           APP VERSION
redmine default     1           2020-06-26 16:14:29.972481 +0900 JST    uninstalled redmine-14.2.4  4.1.1   

ロールバックを実行します。

$ helm rollback redmine 1
Rollback was a success! Happy Helming!

削除がロールバックされて、実行状態になっています。

$ helm ls
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART           APP VERSION
redmine default     2           2020-06-29 14:21:46.552373 +0900 JST    deployed    redmine-14.2.4  4.1.1  

リビジョンの記録を残さないで削除するには--keep-historyなしで削除します。

$ helm uninstall redmine
release "redmine" uninstalled
$ helm ls --all
NAME    NAMESPACE   REVISION    UPDATED STATUS  CHART   APP VERSION

Chartを自作する

Chartのテンプレートを作成します。

$ helm create echo
Creating echo
$ tree .
.
└── echo
    ├── Chart.yaml
    ├── charts
    ├── templates
    │   ├── NOTES.txt
    │   ├── _helpers.tpl
    │   ├── deployment.yaml
    │   ├── hpa.yaml
    │   ├── ingress.yaml
    │   ├── service.yaml
    │   ├── serviceaccount.yaml
    │   └── tests
    │       └── test-connection.yaml
    └── values.yaml

4 directories, 10 files

Macには標準でtreeコマンドは入っていないので、Homebrewでインストールしました。

$ brew install tree

v2と比べると、次の3つファイルが増えています。これらのファイルは今回は使用しません。

  • hpa.yaml # HorizontalPodAutoscalerのマニフェスト
  • serviceaccount.yaml # ServiceAccountのマニフェスト
  • test-connection.yaml # Chartをテストするのに使用する

Chartのテンプレートの構成はこちらを参照してください。

生成されたdeployment.yamlの中身は次のようになっています。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
{{- end }}
  selector:
    matchLabels:
      {{- include "echo.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "echo.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "echo.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

あらかじめ様々なパラメータが抽象化されています。開発者が編集するのはspec.template.specの部分です。次のように変更します。

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}

spec:
{{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
{{- end }}
  selector:
    matchLabels:
      {{- include "echo.selectorLabels" . | nindent 6 }}
  template:
    metadata:
    {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      labels:
        {{- include "echo.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "echo.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: nginx
          image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}"
          imagePullPolicy: {{ .Values.nginx.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
          livenessProbe:
            httpGet:
              path: {{ .Values.nginx.healthCheck }}
              port: http
          readinessProbe:
            httpGet:
              path: {{ .Values.nginx.healthCheck }}
              port: http
          env:
            - name: BACKEND_HOST
              value: {{ .Values.nginx.backendHost | quote }}
        - name: echo
          image: "{{ .Values.echo.image.repository }}:{{ .Values.echo.image.tag }}"
          imagePullPolicy: {{ .Values.echo.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.echo.httpPort }}
          env:
            - name: HTTP_PORT
              value: {{ .Values.echo.httpPort | quote }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

パラメータはvalues.yamlで指定します。生成されたvalues.yamlの中身は次のようになっています。

values.yaml
# Default values for echo.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: true
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

これを次のように書き換えます。

values.yaml
replicaCount: 1
nginx:
  image:
    repository: gihyodocker/nginx
    pullPolicy: Always
    tag: latest
  healthCheck: /
  backendHost: localhost:8080

echo:
  image:
    repository: gihyodocker/echo
    pullPolicy: Always
    tag: latest
  httpPort: 8080

Serviceも同様に書き換えます。生成されたServiceの中身は次のようになっています。

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "echo.fullname" . }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "echo.selectorLabels" . | nindent 4 }}

これを次のようにします。

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "echo.fullname" . }}
  labels: 
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: {{ .Values.service.name }}
  selector: {{- include "echo.selectorLabels" . | nindent 4 }}

values.yamlに次の設定を追記します。

values.yaml
service:
  name: nginx
  type: ClusterIP
  port: 80

次はIngressです。生成されたIngressの中身は次のようになっています。

ingress.yaml
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "echo.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
  name: {{ $fullName }}
  labels:
    {{- include "echo.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ . }}
            backend:
              serviceName: {{ $fullName }}
              servicePort: {{ $svcPort }}
          {{- end }}
    {{- end }}
  {{- end }}

metadata.labelsを書き換えます。

ingress.yaml
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "echo.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
  name: {{ $fullName }}
  labels:
    app: {{ include "echo.name" . }}
    chart: {{ include "echo.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ . }}
            backend:
              serviceName: {{ $fullName }}
              servicePort: {{ $svcPort }}
          {{- end }}
    {{- end }}
  {{- end }}

values.yamlに次の設定を追記します。これはvalues.yaml生成時のデフォルトの値です。

values.yaml
ingress:
  enabled: false
  annotations:
    {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

また、ServiceAccountは今回作成しないので、serviceAccount.createfalseとします。

values.yaml
serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

values.yaml全体は次のようになります。

values.yaml
replicaCount: 1
nginx:
  image:
    repository: gihyodocker/nginx
    pullPolicy: Always
    tag: latest
  healthCheck: /
  backendHost: localhost:8080

echo:
  image:
    repository: gihyodocker/echo
    pullPolicy: Always
    tag: latest
  httpPort: 8080

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext:
  {}
  # fsGroup: 2000

securityContext:
  {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  name: nginx
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations:
    {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths: []
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources:
  {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

Chart.yamlにパッケージングに必要な情報を記載します。

Chart.yaml
apiVersion: v2
name: echo
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: 0.1.0

helm lintで文法チェックができます。

$ helm lint echo
==> Linting echo
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Chartをインストールする

作成したChartをインストールします。helm install --debug --dry-run リリース名 Chart名コマンドでインストールのdry runを実行できます。dry runでは生成されたマニフェストを確認することができます。

$ helm install --debug --dry-run echo echo
install.go:159: [debug] Original chart version: ""
install.go:176: [debug] CHART PATH: /hogehoge/echo

NAME: echo
LAST DEPLOYED: Mon Jun 29 16:58:44 2020
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
echo:
  httpPort: 8080
  image:
    pullPolicy: Always
    repository: gihyodocker/echo
    tag: latest
fullnameOverride: ""
imagePullSecrets: []
ingress:
  annotations: {}
  enabled: false
  hosts:
  - host: chart-example.local
    paths: []
  tls: []
nameOverride: ""
nginx:
  backendHost: localhost:8080
  healthCheck: /
  image:
    pullPolicy: Always
    repository: gihyodocker/nginx
    tag: latest
nodeSelector: {}
podAnnotations: {}
podSecurityContext: {}
replicaCount: 1
resources: {}
securityContext: {}
service:
  name: nginx
  port: 80
  type: ClusterIP
serviceAccount:
  annotations: {}
  create: false
  name: ""
tolerations: []

HOOKS:
---
# Source: echo/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "echo-test-connection"
  labels:
    helm.sh/chart: echo-0.1.0
    app.kubernetes.io/name: echo
    app.kubernetes.io/instance: echo
    app.kubernetes.io/version: "0.1.0"
    app.kubernetes.io/managed-by: Helm
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args: ['echo:80']
  restartPolicy: Never
MANIFEST:
---
# Source: echo/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: echo
  labels: 
    app: echo
    chart: echo-0.1.0
    release: echo
    heritage: Helm
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: nginx
  selector:
    app.kubernetes.io/name: echo
    app.kubernetes.io/instance: echo
---
# Source: echo/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
  labels:
    app: echo
    chart: echo-0.1.0
    release: echo
    heritage: Helm

spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: echo
      app.kubernetes.io/instance: echo
  template:
    metadata:
      labels:
        app.kubernetes.io/name: echo
        app.kubernetes.io/instance: echo
    spec:
      serviceAccountName: default
      securityContext:
        {}
      containers:
        - name: nginx
          image: "gihyodocker/nginx:latest"
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 80
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          env:
            - name: BACKEND_HOST
              value: "localhost:8080"
        - name: echo
          image: "gihyodocker/echo:latest"
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
          env:
            - name: HTTP_PORT
              value: "8080"
          resources:
            {}

NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=echo,app.kubernetes.io/instance=echo" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80

dry runでパラメータに問題ないか確認できたらインストールします。

$ helm install echo echo
NAME: echo
LAST DEPLOYED: Mon Jun 29 16:59:48 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=echo,app.kubernetes.io/instance=echo" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:80

アプリが立ち上がっていることがわかります。

$ helm ls
NAME    NAMESPACE   REVISION    UPDATED                                 STATUS      CHART       APP VERSION
echo    default     1           2020-06-29 16:59:48.534198 +0900 JST    deployed    echo-0.1.0  0.1.0    
$ kubectl get deployment,service,ingress --selector app=echo
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/echo   1/1     1            1           2m10s

NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/echo   ClusterIP   10.111.40.236   <none>        80/TCP    2m10s

Ingressの使用がfalseになっていたので、これをtrueにして実行してみます。デプロイ済みのアプリは削除します。

$ helm uninstall echo

ローカルにはL7ロードバランサがないので、NGINX Ingress Controllerを使用します。インストール方法はこちらを参照してください。

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
$ kubectl -n ingress-nginx get service,pod
NAME                                         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.98.29.212    localhost     80:32012/TCP,443:30306/TCP   3m46s
service/ingress-nginx-controller-admission   ClusterIP      10.107.104.62   <none>        443/TCP                      3m46s

NAME                                            READY   STATUS      RESTARTS   AGE
pod/ingress-nginx-admission-create-6plnl        0/1     Completed   0          3m36s
pod/ingress-nginx-admission-patch-csb5s         0/1     Completed   0          3m36s
pod/ingress-nginx-controller-579fddb54f-d47d7   1/1     Running     0  

次に、カスタムValueファイルを作成します。

$ vi echo.yaml
ingress:
  enabled: true
  hosts:
    - host: "ch06-echo.gihyo.local"
      paths: ["/"]

カスタムValueファイルを使用してアプリをインストールします。

$ helm install -f echo.yaml echo echo
NAME: echo
LAST DEPLOYED: Mon Jun 29 17:32:19 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  http://ch06-echo.gihyo.local/

curlでアクセスしてみます。

$ curl http://localhost -H 'Host:ch06-echo.gihyo.local'
Hello Docker!!

これで成功です!

パッケージをつくる

helm packageコマンドでパッケージングできます。

$ helm package echo
$ ls
echo/   echo-0.1.0.tgz  echo.yaml

v2では開発目的のために、ローカルのチャートリポジトリをローカルマシン上で実行していましたが、開発ツールとしてはあまり受け入れられず、その設計には多くの問題があったため、v3ではhelm serveが無くなり、プラグインになりました。

おわりに

今回は「Docker/Kubernetes 実践コンテナ開発入門」をHelm v3で行いました。コンテナまわりの技術は進歩が速くて追いかけるのが大変です。


  1. Helm v3 は2019年11月13日にリリースされました。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.jsをDockerで動かした時のメモ

環境

$ cat /etc/linuxmint/info 
RELEASE=19.3
CODENAME=tricia
EDITION="Cinnamon"
DESCRIPTION="Linux Mint 19.3 Tricia"
DESKTOP=Gnome
TOOLKIT=GTK
NEW_FEATURES_URL=https://www.linuxmint.com/rel_tricia_cinnamon_whatsnew.php
RELEASE_NOTES_URL=https://www.linuxmint.com/rel_tricia_cinnamon.php
USER_GUIDE_URL=https://www.linuxmint.com/documentation.php
GRUB_TITLE=Linux Mint 19.3 Cinnamon

Dockerのインストール

$ apt update
$ apt install docker-ce docker-ce-cli docker-compose

node.jsとcreate-react-appのインストール

$ apt install nodejs
$ npm install create-react-app yarn

サンプルReact.jsアプリを作る

$ create-react-app docker-test
$ cd docker-test

とりあえず普通に起動してみる。

yarn start

ブラウザで http://localhost:3000 にアクセスしReact.jsが動作していることを確認。
確認できたらCtrl-Cで落とす。

Dockerfileとdocker-compose.ymlを作成

作成したReactアプリのルートディレクトリに以下のような内容で Dockerfiledocker-compose.yml を作る。

Dockerfile
FROM node:14.4.0-buster-slim
WORKDIR /usr/src/app
RUN npm install --save prop-types
RUN npm install -g create-react-app
docker-compose
version: '3'
services:
  node:
    build:
      context: .
    tty: true
    environment:
      - NODE_ENV=production
    volumes:
      - ./:/usr/src/app
    command: sh -c "yarn start"
    ports:
      - "3000:3000"

上記のような内容だと、作成したReactアプリのルートディレクトリがDocker環境内では /usr/src/app にマウントされるため、 WORKDIRを /usr/src/app に指定してあれば、実行するスクリプトは sh -c "yarn start" だけとなるため、これを command で指定している。

docker-hub nodeを見に行くと、alpine buster stretch などの名前がバージョンについています。なんのことかと思ったらLinuxディストリビューションですね。

slim とついているものは、Debianディストリビューションでエクストラパッケージが含まれないものです。いずれにしても今回はDockerでnodeが動けばいいので node:14.4.0-buster-slim を選択しました。
ちなみにDebianの現在のバージョンは buster(10.0) であり、それ以前は以下のようになります。

Debian 10 (buster) — 現在の安定版リリース
Debian 9 (stretch) — 過去の安定版リリース
Debian 8 (jessie) — 過去の安定版リリース
Debian 7 (wheezy) — 過去の安定版リリース

alpine linuxはパワーユーザー向けの軽量Linuxのようです。軽量だけにイメージが小さく抑えられるかもしれませんが、私はDebianやUbuntuしか使ったこと無いので alpine の使用はやめておきました。

Dockerイメージをビルド

$ docker-compose build

DockerでReact.jsアプリを実行する

起動

$ docker-compose up

ブラウザで http://localhost:3000 にアクセスしReact.jsが動作していることを確認。
Ctrl-C で停止。

バックグラウンドで起動

$ docker-compose up -d

停止

$ docker-compose down

イメージ情報を確認

$ docker images | grep dockertest
dockertest_node     latest               daf22d9465c7        26 minutes ago      194MB

おまけ

すべてのDockerイメージを停止

$ docker stop $(docker ps -q)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ラズパイに向けてRustをクロスコンパイル!

想定読者

  • Rustが好きだ!Rustが使いたいんだ!でも、ラズパイに向けたクロスコンパイル方法がわからない!
  • クロスコンパイルのやり方は知らなくても、どんなのかは知っている
  • Dockerにアレルギー反応を起こさない
  • アーキテクチャの名前にアナフィラキシー・ショックを起こさない
  • ツールチェイン、リンカという言葉に拒絶反応を起こさない

という人向けの、初心者による初心者のための記事です。
(というか私の備忘録です。)

環境

開発環境
OS:Ubuntu 20.04 LTS
アーキテクチャ:x86_64

実行環境
ハードウェア:RaspberryPi4 typeB 4GB
OS:Raspbian(Version: May 2020)
アーキテクチャ:ARMv7

目標

rust-pushordを用いたRust製GUIアプリケーションをRaspberryPi用にクロスコンパイルして、Raspbian上でも動かすこと。

流れ

  1. rustの公式Dockerイメージを使って開発環境を整える
  2. コンテナ内に「Hello World」を用意する。
  3. CargoへARMv7向けにコンパイルするように設定する
  4. ツールチェインとリンカを用意する
  5. コンパイルの確認
  6. rust-pushordの依存ライブラリを用意する(SDL2)
  7. rust-pushordのexampleをコンパイルする
  8. RaspberryPi上で実行する
  9. 喜びの舞を踊る(option

1. rustの公式Dockerイメージを使って開発環境を整える

Dockerのインストール等は割愛させていただきます。

インストールが終わったら早速、Rustの公式イメージゲットだぜ!

bash
docker container run -v ホスト:コンテナ -it --rm rust:latest /bin/bash

(注:もしかしたらsudoがいるかもね。)

将来的なことを考えるとDockerfileを用意したほうがいい気がするけど、ここでは「初心者による初心者のための」ということで作りません。

2. コンテナ内に「Hello World」を用意する。

初めに、コンテナ内で「Hello World」のコードを作り、確認のため普通(開発環境アーキテクチャ向け)に実行します。

bash
root@abcd12345:/# export USER=root
root@abcd12345:~# cd
root@abcd12345:~# cargo new hello
root@abcd12345:~# cd hello
root@abcd12345:~/hello# cargo run

これで「Hello World」が帰ってきたらOK。

3. CargoへARMv7向けにコンパイルするように設定する

次に「.cargo/config」を作成しクロスコンパイルをするための設定を記述します。

bash
root@abcd12345:~/hello# mkdir .cargo
root@abcd12345:~/hello# cat <<EOF > .cargo/config
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
EOF

これは
「cargoでtargetにarmv7-unknown-linux-gnueabihfが指定されたら、リンカにarm-linux-gnueabihf-gccを使ってね!」
とcargoにお願いするためのものです。

4. ツールチェインとリンカを用意する

さて、次に用意しないと行けないのが「ARMv7用の、ツールチェインとリンカ」です。

bash
# ツールチェインの追加
rustup target add armv7-unknown-linux-gnueabihf

# 備考:用意されているツールチェインは次のコマンドで表示できます
# rustup target list

# リンカの用意
apt install gcc-arm-linux-gnueabihf

5. コンパイルの確認

さぁ、準備ができたので、コンパイルがちゃんと通るか確かめて見ましょう!

bash
root@abcd12345:~/hello# cargo build --target=armv7-unknown-linux-gnueabihf

実際にARMv7上で動くかは、RaspbianのDockerイメージを使ってみたり、実機でテストしてください。

6. rust-pushordの依存ライブラリを用意する(SDL2)

CUIだと寂しいのでGUIアプリをコンパイルしてみましょう!

クロスプラットフォームでさくっとできるのにrust-pushordというのがあるので、これを使います。

任意の場所にGitHubからクローンして、exampleをコンパイルしましょう。

bash
root@abcd12345:~# git clone https://github.com/KenSuenobu/rust-pushrod.git
root@abcd12345:~# cd rust-pushrod
root@abcd12345:~/rust-pushrod# cargo build --target=armv7-unknown-linux-gnueabihf --example timer

残念なことに、失敗しますね。。かなしみ。

... "-ldl" "-lutil"
  = note: /usr/lib/gcc-cross/arm-linux-gnueabihf/8/../../../../arm-linux-gnueabihf/bin/ld: cannot find -lSDL2
          /usr/lib/gcc-cross/arm-linux-gnueabihf/8/../../../../arm-linux-gnueabihf/bin/ld: cannot find -lSDL2_image
          /usr/lib/gcc-cross/arm-linux-gnueabihf/8/../../../../arm-linux-gnueabihf/bin/ld: cannot find -lSDL2_ttf
          collect2: error: ld returned 1 exit status


error: aborting due to previous error

error: could not compile `rust-pushrod`.

To learn more, run the command again with --verbose.

どうやらrust-pushordに必要なSDL2が足りていないようです。

では「依存関係を解決するためにapt install libsdl2-devすればいいのか」というと、そうは問屋がおろしません。

なぜなら、必要としているSDL2は「ARMv7向け」だからです。

ということで、次のようにしないといけません。

bash
# マルチアーキテクチャでarmhfを有効にする
dpkg --add-architecture armhf

# 本当は、armhf向けのパッケージが必要とする必須の
# 基本的なarmhf向けのアプリをインストールする必要があります。
# しかし、Rustの公式Dockerイメージには、もうすでに入ってるのでスキップします。
# apt install crossbuild-essential-armhf

# 必要なSDL2のライブラリをインストール
apt install libsdl2-dev:armhf libsdl2-image-dev:armhf libsdl2-ttf-dev:armhf

これで依存ライブラリも揃いました。もう一度コンパイルして、うまく行くことを確認してみてください。

bash
root@abcd12345:~/rust-pushrod# cargo build --target=armv7-unknown-linux-gnueabihf --example timer

8. RaspberryPi上で実行する

コンパイルしてできたものを共有フォルダに移し、ホスト側に取り出し、RaspberryPiに持っていってください。

そして実行ししっかりと動くか確認してみましょう。
ついに、RustがRaspberryPi上で動くはずです。

(画像は明日くらいに載せます。(現在:2020/6/30))

9. 喜びの舞を踊る(option

♪(((^-^)八(^∇^)))♪

お疲れ様でした。今回、私自身が「Rustで組み込み!初クロスコンパイル!」ということで、情報を漁るも、簡単には見つからなかったため、記事にさせていただきました。(特にライブラリの依存関係を解消するあたり)

誰かの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerの不要コンテナを一括削除する

最近AWSのEC2(AmazonLinux2)上でDockerを入れて色々と遊んで勉強しているのですが、
centosのコンテナを作成する → ごにょごにょする → 終わる、mysqlのコンテナを作る → ごにょごにょする → 終わるなどを繰り返し行い、たまにdocker psコマンドでコンテナの稼働状況を確認してたんですが、ある時docker ps -aを実行したところなんじゃこりゃぁ!!となりました。
そこで「Dockerコンテナは終わっても、Dockerの事は嫌いにならないでください!」と
◯田敦子さんが言ったかどうかは定かではないですが、少なくともDockerコンテナは終わってもDockerコンテナは残っているという事を学んだわけです。

無駄な前置きが長くなりましたが、この溜まりに溜まったゴミDockerコンテナを削除するための
コマンドの覚書です。

awkを利用した不要コンテナの削除

削除前の状態

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS                    NAMES
9ec50bb2ac79        mysql               "docker-entrypoint.s…"   4 days ago          Up 2 days                 3306/tcp, 33060/tcp      MYSQL
20cabc9db638        centos              "/bin/bash"              4 days ago          Up 2 days                 0.0.0.0:9989->8080/tcp   centos_container
149e47fc7182        mysql               "docker-entrypoint.s…"   4 days ago          Exited (0) 4 days ago                              silly_haibt
6d0b42666db1        mysql               "docker-entrypoint.s…"   4 days ago          Exited (0) 4 days ago                              stoic_panini
cc75432e283b        mysql               "docker-entrypoint.s…"   4 days ago          Exited (127) 4 days ago                            serene_merkle
e75ea014088a        mysql               "docker-entrypoint.s…"   4 days ago          Exited (0) 4 days ago                              laughing_allen
7f6d0144717d        centos              "/bin/bash"              4 days ago          Exited (0) 4 days ago                              zealous_haibt
b78d2f0c9b93        centos              "/bin/bash"              4 days ago          Exited (0) 4 days ago                              centos_1
a1adb31a4719        mysql               "docker-entrypoint.s…"   4 days ago          Exited (0) 4 days ago                              mysql_container
b977c75c9dd7        mysql               "docker-entrypoint.s…"   4 days ago          Exited (1) 4 days ago                              bold_nobel
6c590752cab8        mysql               "docker-entrypoint.s…"   4 days ago          Exited (1) 4 days ago                              gifted_nightingale

上記のうち、稼働中の上2つ以外を削除する。
※今回はdocker ps -f status=<ステータス>で"Exited"のコンテナのみを削除

docker ps -f status=exited | awk '{print $1}' | xargs docker rm

実行結果

149e47fc7182
6d0b42666db1
cc75432e283b
e75ea014088a
7f6d0144717d
b78d2f0c9b93
a1adb31a4719
b977c75c9dd7
6c590752cab8
Error: No such container: CONTAINER
[ ~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
9ec50bb2ac79        mysql               "docker-entrypoint.s…"   4 days ago          Up 2 days           3306/tcp, 33060/tcp      MYSQL
20cabc9db638        centos              "/bin/bash"              4 days ago          Up 2 days           0.0.0.0:9989->8080/tcp   centos_container

無事不要なコンテナは消えましたが、ヘッダー行の"CONTAINER"でdocker rmをしようとしてエラーとなってますね。。(ま、いっか)

Docker公式ドキュメント方式(-_-) ※こちらおすすめしております!

上記コマンド作成後、Docker公式マニュアルに稼働していないコンテナ削除方法が載ってました。。
上のしょーもないawk方式なんかより先に載せろよと言われるかもしれませんが...

実行前状態(もう一度不要なコンテナを増産しました)

docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                    NAMES
109839ada027        centos              "/bin/bash"              35 seconds ago      Exited (0) 34 seconds ago                            vibrant_bohr
cf2e8c41b2ee        centos              "/bin/bash"              36 seconds ago      Exited (0) 36 seconds ago                            brave_mestorf
fcfdaf35235d        centos              "/bin/bash"              37 seconds ago      Exited (0) 37 seconds ago                            gallant_faraday
bf7b9fa96291        centos              "/bin/bash"              38 seconds ago      Exited (0) 38 seconds ago                            zealous_morse
9ec50bb2ac79        mysql               "docker-entrypoint.s…"   4 days ago          Up 2 days                   3306/tcp, 33060/tcp      MYSQL
20cabc9db638        centos              "/bin/bash"              4 days ago          Up 2 days                   0.0.0.0:9989->8080/tcp   centos_container

削除コマンド

コマンド説明(公式マニュアルより)

このコマンドは停止しているコンテナを全て削除します。コマンド docker ps -a -q は終了した全てのコンテナ ID を rm コマンドに渡し、全て削除するものです。実行中のコンテナは削除されません。

docker rm $(docker ps -a -q)

実行結果

109839ada027
cf2e8c41b2ee
fcfdaf35235d
bf7b9fa96291
Error response from daemon: You cannot remove a running container 9ec50bb2ac797f3cfdbe827aa2793ba135f6f29e9fd2c1b179afa09d6be4b5ba. Stop the container before attempting removal or force remove
Error response from daemon: You cannot remove a running container 20cabc9db6380304c3c0c3b8a14f04e0bd60fd58c810ee4c02d6539fd5b4f3f3. Stop the container before attempting removal or force remove

実行中のコンテナは削除しようとしてエラーとなります。
試してはいませんが、docker rm -f $(docker ps -a -q)とするとコンテナ皆殺しの刑に処す!
となるはずですし、ここまで読んだ殆どの方はお気づきかと思いますが、上記awk方式で実施した
内容も下記コマンドで実現できます...

docker rm $(docker ps -f status=exited -q)

それでは、お後がよろしいようで。。。テケテン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2(AmazonLinux2)にJenkinsを構築する

EC2 に Jenkins を構築したのでその作業のメモです。

鍵に権限を付与します。

$ chmod 600 KEY_NAME.pem

ログインします。

$ ssh -i KEY_NAME.pem ec2-user@{IP_ADDRES}

アップデートします。

$ sudo su -
$ yum -y update
$ yum -y upgrade

Nginx をインストールします。(必要な場合)

$ amazon-linux-extras install nginx1.12
$ yum -y install nginx
$ systemctl enable nginx.service
$ systemctl start nginx.service
$ systemctl status nginx.service

Docker をインストールします。

$ yum -y install docker
$ systemctl enable docker.service
$ systemctl start docker.service

マウントするディレクトリを作成します。

$ mkdir /home/jenkins_home
$ chown -R 1000:1000 /home/jenkins_home

Jenkins を起動します。

docker run \
  --name jenkins \
  --detach \
  --volume /home/jenkins_home:/var/jenkins_home \
  --publish 8080:8080 \
  --publish 50000:50000 \
  jenkins/jenkins:lts

Nginx の設定を追加します。(必要な場合)

nginx.conf
user root;
worker_processes  4;

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}
jenkins.conf
server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    location / {
        proxy_set_header Host $http_host;
        proxy_pass http://localhost:8080;
    }
}

Nginxを再起動します。

$ systemctl restart nginx.service

参考

Installing Jenkins

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerでローカル開発環境のhttps化

はじめに

ローカル開発環境の https 化
https://blog.jxck.io/entries/2020-06-29/https-for-localhost.html
この記事を読ませて頂き、自分の環境ではこうやってるよというのをシェアしようと思いました。
Let's Encryptによる証明書ではなくて、自己署名証明書を使う方法になります。
また、記事で指摘されているlocalhostの特殊性を無視して粗があっても手早くhttpsで開発したい人向けです

https化の動機

localhostとの通信を暗号化しても無意味な気がしますが、そうせざるをえない時もあります

  • mixed contentでリソースがブロックされる
  • FacebookのOAuthで設定するredirect URIはhttpsでないと動かない
  • Geolocation APIで位置情報を取得する時にhttpsでないと動かない

そのような時のためにlocalhostをhttpsで使う手順があると便利です。
ngrokなどもありますが、手元で完結するタイプの方法になります。

steps

https-portal imageを使う

こちらのイメージを使わせて頂きます。
https://github.com/SteveLTN/https-portal

docker-compose.yml
version: "3"
services:
  https-portal3000:
  image: steveltn/https-portal:1
  ports:
    - '3443:443'
  environment:
    STAGE: local
    DOMAINS: 'localhost -> http://host.docker.internal:3000'

このコンテナは、 https://localhost:3443 でリクエストを受けて http://localhost:3000 にフォワーディングしてくれる設定となります。
(localhost:3000 で開発中のアプリケーションが動いていることを想定)
STAGE=local この環境変数を渡すとコンテナの中で自己署名証明書を作ってくれます(便利!)
Docker Desktop for Macを使っている場合はこれでOK。他の環境では extra_hosts の設定が必要でしょう。
例:

docker-compose.yml
version: "3"
services:
  https-portal3000:
  image: steveltn/https-portal:1
  ports:
    - '3443:443'
  environment:
    STAGE: local
    DOMAINS: 'localhost -> http://host.docker.internal:3000'
  extra_hosts:
    - "host.docker.internal:172.17.0.1"

Dockerでアプリケーションを動かしている場合は host.docker.internal でなくて直接コンテナに繋いでも動くと思います。

Chromeで警告を無視する

これをEnabledにすると localhost に限り自己署名証明書であっても警告が出なくなります。

chrome://flags/#allow-insecure-localhost

セキュリティを下げる設定ですが対象がlocalhostのみなので許容します。
以上で https://localhost:3443/ へhttpsアクセスできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cx_Oracle が使える Docker イメージ

Python アプリから Oracle データベースに繋ぐ必要があり Python クライアントを入れる Dockerfile を書いたのだが、やたら苦労したので備忘録として書いておく。

Dockerfile
FROM python:3.7

RUN pip install cx_Oracle

# Install Oracle Client
ENV ORACLE_HOME=/opt/oracle
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME/lib
RUN apt-get update && apt-get install -y libaio1 && rm -rf /var/lib/apt/lists/* \
 && wget -q https://download.oracle.com/otn_software/linux/instantclient/19600/instantclient-basic-linux.x64-19.6.0.0.0dbru.zip \
 && unzip instantclient-*.zip \
 && mkdir -p $ORACLE_HOME \
 && mv instantclient_19_6 $ORACLE_HOME/lib \
 && rm -f instantclient-*.zip

cx_Oracle 8 Initialization — cx_Oracle 8.0.0 documentation

公式ドキュメントによると、cx_Oracle を利用するためには Oracle Client ライブラリがインストールされている必要があり、cx_Oracle は次の順序でライブラリの読み込みを試行する。

  1. cx_Oracle.init_oracle_client(lib_dir="...") で指定したパス
  2. OS のライブラリパス ($LD_LIBRARY_PATH)
  3. $ORACLE_HOME/lib

このうち 1.3. の方法で試したときに、他のライブラリは読み込めるのに libnnz19.so だけ「そんなファイルは無い」などとエラーが出て大変困った。
原因はいまだにわからない。

2. の方法を試したところ問題なく読み込まれた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerで作ったDjangoアプリケーションをHerokuにデプロイする

はじめに

Container Registryを使ってHerokuにデプロイする記事が、Railsが多い一方でDjangoではそんなにヒットしなかったので、アウトプットがてら書いていきます。まだ勉強中の身なので、もし間違えていたら指摘お願いします。

Heroku CLIをダウンロード

始めにこのリンクからHeroku CLIをダウンロードします。僕はwindows 10を使っているので、64ビットインストーラを使ってダウンロードしました。
他のOSを使っている人でも、やり方は詳しく(英語で)書かれているので、その通りにダウンロードしてください。

Heroku上でアプリを作成、アドオン等設定

Herokuにログインしてアプリを作成し、OverviewのところからHeroku PostgresをAdd-onsとして設定します。このとき、Hobby Devを選択することで無料で使えるっぽい。(ただし遅いとかなんとか…)
Herokuではvolumeが使えないため、データの永続化をするためにこのようにDBを利用しています。

requirement.txtに追記

requirement.txt
gunicorn
dj-database-url

setting.pyに追記

以下のようにsetting.pyの下の方にstatic_rootとデータベースの設定を書いていきます。
STATIC_ROOTを設定することで本番環境でも静的ファイルを扱えるようになります。

setting.py
DEBUG = False
ALLOWED_HOSTS = ['自分のドメイン']
()
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
()
import dj_database_url
DATABASES['default'] = dj_database_url.config(conn_max_age=600, ssl_require=True)

公式参考

Procfileを用意

Dockerfileと同じディレクトリにProcfileという名前のファイルを準備し、以下のように記述します。

Procfile
web: gunicorn [プロジェクト名].wsgi

プロジェクト名はご自身のプロジェクト名に置き換えてください。

Dockerfileに追記

以下のようにDockerfileの一番下に追記します。

Dockerfile
CMD gunicorn --bind 0.0.0.0:$PORT [プロジェクト名].wsgi

こちらも同様にプロジェクト名を置き換えてください。

コマンドを実行

プロジェクトがあるディレクトリ上でコマンドを実行していきます。

$ heroku login
$ heroku container:login
$ heroku container:push web -a [アプリ名]
$ heroku container:release web -a [アプリ名]
$ heroku open -a [アプリ名]

No webprocess runningエラーが出たら以下のコマンドを実行しましょう。

$ heroku ps:scale web=1 -a [アプリ名]

公式参考

コンテナとHeroku Postgresを結び付ける

$ heroku addons:attach [postgresコンテナ名] -a [アプリ名]

postgresコンテナ名はHerokuのアプリ管理サイトのOverviewのInstalled add-onsから確認することができます。また、最初以外にもマイグレーションが失敗するときなどにこのコマンドを再度実行し、再び結び付け直すことで上手くいくようになることがあります。

マイグレーションの実行

以下のコマンドでマイグレーションを実行します。

$ heroku run python manage.py migrate -a [アプリ名]

collectstaticの実行

collectstaticを実行します。whitenoiseを使うとここら辺が省略できるらしい…?

$ heroku run python manage.py collectstatic -a [アプリ名]

おわりに

とりあえずDockerを利用してデプロイは出来たものの、なぜかdjango-herokuを使うと500エラーが出たり、静的ファイルが適用出来ていなかったりするので、もうちょっと触りながら勉強します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goの実行環境をDockerで構築しようと際に発生した「no Go files in /go/src/github.com/go-playground/localesエラー」を解決

はじめに

root@xxxxxxxxxxxx:/go# go get github.com/go-playground/locales

いつものようにパッケージをインストールしていたら次のエラーが発生。

can't load package: package github.com/go-playground/locales: no Go files in /go/src/github.com/go-playground/locales

スクリーンショット 2020-06-30 14.49.04.png

ここのlocalesフォルダの中に何もファイルなんてねーよ!って言われてるんですね。そんなこと言われても。。。

解決方法

go getをしても同じエラーが吐かれるだけだったので、試しにlocalesフォルダを削除して、再度以下コマンドを叩いてみました。

root@xxxxxxxxxxxx:/go# go get github.com/go-playground/locales

すると2分程度でパッケージのインストールが終わりました。

スクリーンショット 2020-06-30 14.51.55.png

入った!

無事コンパイルが終了し、GoをDocker環境で構築することができました。Docker便利です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker基本コマンド

mono-horizontal.png

はじめに

Dockerについての記事は沢山ありますが
自分用の忘備録です。
覚えたコマンドは随時追加

コンテナの起動/再起動

docker start [コンテナ識別子]

コンテナの停止

docker stop [コンテナ識別子]

コンテナの削除

docker rm [コンテナ識別子]

コンテナを作って中に入る

docker run -it [イメージ名] bin/sh

コンテナの名前を変えるコマンド

docker rename [古いコンテナ名][新しいコンテナ名]

Docker-compose build

docker-compose build

imageを作成・Pullするコマンド

docker-compose run

docker-compose run (サービス名) (コマンド)
example: docker-compose run --rm app bash 

コンテナを起動して、コマンドを実行することができる。
-rm を追加することによって、コマンドを実行した後に、
コンテナを削除します。

docker-compose up する前などに、一時的にコンテナ内に入り
コマンドを実行したい時などに便利です。

docker-compose exec

docker-compose exec (サービス名) (コマンド名)
example: docker-compose exec app bash

これは、稼働中のコンテナに入りコマンドを実行するための物です。

docker-compose run

docker-compose up

このコマンドを実行すると、
docker-compose.ymlで記述した通りにコンテナを作成し、
コンテナ同士を繋げて起動をします。

-d をオプションとしてつけることで、バックグラウンドでコンテナを起動します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GithubActionsでのDocker BuildでCacheを効かせる方法メモ

はじめに

GithubActionsでDockerのコンテナをBuildするとデフォルトだとLayerのCache?がされないため、毎回Dockerfileの先頭から実行することになります(何も工夫をしないと)。
「LocalでBuildするときは(Cacheが効いて)速いんだけど、GithubActionsだと遅い」というのは、Buildに時間がかかる場合結構しんどいです(Twitterなどが捗ってしまう)。

色々方法があるようですが、Dockerのマルチステージビルドを使っていないなら、割と簡単にCacheを効かせられるようなので、そのメモです(主に自分用)。

Docker BuildでCacheを効かせる方法

ポイントは docker buildコマンド--cache-from--build-arg BUILDKIT_INLINE_CACHE=1 になります。また、このOptionを指定するには、BuildKitを有効にしないといけないので、環境変数 DOCKER_BUILDKIT=1 を指定しておくことが必要になります(buildx でも良いようですが)。

上記リンクのドキュメントにも書いてありますが、それぞれ次のような意味になります。

  • --cache-from: CacheSourceを指定するコマンドで、外部リポジトリを指定すると適宜アクセスしてくれるようです。
  • --build-arg BUILDKIT_INLINE_CACHE=1: Build時にMeta情報を埋め込み、--cache-from で使えるようにします。これをリポジトリにPushしておけば、前回Build時のLayerを再利用できるようになります。
  • 環境変数 DOCKER_BUILDKIT=1: こう設定しておくとBuildKitが有効になるようです。GithubActionsでも使えるようです。

サンプル

例えば、AWSのECRにPushする場合は、以下のように書くことができます。

aws.yml
on:
  push:
    tags: release-*  # トリガーは `release-****` というTagがPushされたとき

name: Push to Amazon ECR

jobs:
  push:
    name: BuildAndPush
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v2

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build, tag, and push image to Amazon ECR
      id: build-image
      env:
        DOCKER_BUILDKIT: 1  # これ大事
        ECR_REGISTRY: 999999999999.dkr.ecr.ap-northeast-1.amazonaws.com  # TODO: 要変更
        ECR_REPOSITORY: my-awesome-container  # TODO: 要変更
        IMAGE_TAG: ${{ github.sha }}
      run: |
        # latest と github.sha でタグをふる場合
        docker build \
            --cache-from=$ECR_REGISTRY/$ECR_REPOSITORY:latest --build-arg BUILDKIT_INLINE_CACHE=1 \
            -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

さいごに

actions/cache@v2 とか使うともっと良い(速い)のかもしれないですが、使わなくてもできるんだな、と思ったのでメモしておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[mysql,docker,ESET]Lost connection to MySQL server at 'reading initial communication packet', system error: 0の解決例

どんなエラー?

接続者がデータベースに対して権限を持っていない状態で接続を試みると出るエラーのようです

スクリーンショット 2020-06-30 8.49.16.png

何をしたら出た?

docker-compose upで予め用意したdocker-compose.ymlに沿って正しく立ち上げで、ローカルでの接続で権限をいじった記憶もないのに出現しました

試行錯誤

この記事を見て関係ありそうな部分をすべて試すも空振りしました。

原因

会社のPCということもありセキュリティに厳しくしているようでESET Endpoint SecurityというファイアウォールをMacのデフォルト以外に取り入れており、そのセキュリティが、データベースへの直接の接続はローカルであっても妨げているようでした。(それだけ保存されたデータは取り扱い注意であるのだとわかりますね。)

解決方法

ESETのパーソナルファイアーウォールをオフにすることで接続が可能になります

(手順は、画面左にある保護の状態をクリック→画面中央にあるパーソナルファイアーウォールをクリック→画面中央のパーソナルファイアーウォールのオンオフスイッチをクリックしてオフにする)

いくら取り扱い注意とはいえ、ローカルでSQL文の挙動の実験などをする必要がある場合がありますので、必要に応じて操作できる・できないの状態を変更できる方法を覚えておけるといいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeでNode.js(Express)とPostgresSQLを連携したサービスを作る勉強会

概要

先日行われた勉強会にて簡易ではありますが表題のサービスを作りました。Dockerの勉強の延長で取り組みました。この記事ではそのときの勉強会の内容を記載します。

どんなモノをつくるのか?

  • チャットのようなアプリケーション
  • docker-composeによるデータベースとの連携
    • Node.js(Express)でフロントエンド&バックエンド
    • データベースはPostgresSQL

完成したソースコード

https://gitlab.com/tamoco-mocomoco/study-docker-compose

参考にさせていただいた記事

https://qiita.com/ryo-ohnishi/items/b54e649b14b51694ef77

作ったモノの説明

docker-compose

サービス:app

  app:
    build:
      context: ./      # Dockerfile保存場所
    depends_on: 
      - database
    image: n-app                  # イメージ名
    container_name: n-app         # コンテナ名
    ports:                          # ポート接続
      - 3000:5000
    environment:
      PORT: 5000
      DB_USER: postgres
      DB_HOST: database
      DB_PORT: 5432
      DB_NAME: test_db

appNode.js(Express)がメインとなるサービスになります。ここでは学習のために敢えてデフォルトのポートを5000に変更しています。これは参考にさせていただいたExpressの環境変数でPORTが使用されており、それをdocker-compose5000に上書きしています。コンテナ内の5000をホスト側の3000に転送しています。ちなみにExpressで環境変数が使用されている箇所はapp/bin/wwwというファイルで確認できます。

app/bin/www

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('myapp:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
~以下省略~

サービス:database

  database:
    image: postgres:12.3
    volumes:
      - ./init-sql:/docker-entrypoint-initdb.d:ro
    environment:
      POSTGRES_DB: test_db
      TZ: "Asia/Tokyo"
      POSTGRES_HOST_AUTH_METHOD: trust

databasePostgresSQLを使用しています。docker-composeで環境変数であるPOSTGRES_DBtest_dbに設定することで自動的にtest_dbという名前のデータベースが作成されます。

volumesの設定はinit-dbというフォルダにSQLファイルを格納しておくことで、SQLファイルに書かれているクエリを自動的に実行してくれます。
初期スキーマの設定(テーブル構造の作成)やダミーデータなどを挿入するときに利用します。

実行順序はファイル名の昇順で実行されるので、ファイル名に番号などをつけるとよさそうです。

今回、作成したデータベースにはpostgresというユーザーが作成されますが、パスワードが設定されていません。そのためPOSTGRES_HOST_AUTH_METHODtrustに設定する必要があります。

あと、TZでタイムゾーンを設定しています。タイムゾーンの変更については以下のクエリでも可能です。

ALTER DATABASE test_db SET timezone TO 'Asia/Tokyo';
SELECT pg_reload_conf();

init-dbにクエリをファイルとして格納しておけばタイムゾーンも変更可能です。ただし、どこでタイムゾーンが設定されているか後追いしづらいので、環境変数で設定できるなら環境変数を使った方が良いと思います。

Dockerfile:Node.js(Express)

今回のアプリケーションの部分(サービス:app)はappフォルダ配下
Expressのソースが格納されています。それらのソースを元にDockerfileでコンテナを作成します。

Dockerfile

# ベースイメージを指定
FROM node:10.12

# 環境変数設定
ENV NODE_ENV="development"

# 作業ディレクトリ作成&設定
WORKDIR /src

COPY ./app /src

RUN npm install

CMD npm run start

ここで重要なのでコンテナの中にsrcというフォルダを作成し、そこにホスト(自分の端末)のappフォルダの中身をコピーします。srcWORKDIRに設定されているのでsrcでコピーしたソースファイルを元にnpm installなどを実行できます。

今回はDockerfileでソースをコピーしましたがdocker-composeでも同じようなことが可能です。

Node.js(Express)

ここは別記事にしてもよいくらい説明する箇所が多いので抜粋して記載します。今回のExpressのソースではapp/routesフォルダのファイルでエンドポイントを設定して、表示内容をapp/viewsフォルダのテンプレートのファイルに渡してコンテンツを表示する流れになっています。

処理の流れ
app/routes → app/views

localhost:3000 にアクセス

app/routes/index.js
↓
app/views/index.ejs

index.jsではPostgresSQLchatテーブルをセレクトした結果をindex.ejsに渡しています。

localhost:3000/insert?msg=SEND_TEST

app/routes/insert.js
↓
app/views/index.ejs

insert.jsではURLパラメーターでmsgを受け取ることができます。msgの値をchatテーブルにインサートします。もしパラメーターがない場合は「ふにょふにょ」という値がインサートされます。

表示する内容はindex.ejsと同じですがインサートされた後のテーブルの情報を表示します。app/routesのファイルとapp/viewsのファイルは必ず1対1にする必要はなく、同じテンプレートでも渡す内容で表示内容を切り替えるなどの制御も可能だったりします。

データベース(PostgresSQL)への接続:db_client.js

module.exports = {
    pg_client: function () {
        const { Client } = require('pg')

        const client = new Client({
            user: process.env.DB_USER,
            host: process.env.DB_HOST,
            port: process.env.DB_PORT,
            database: process.env.DB_NAME
        })
        return client
    },
    pupupu: function () {
      console.log('pupupupu')
    }
  };

db_client.jsでデータベースの接続処理を共通で使用できるようにしています。PostgresSQLに必要なユーザー情報などはdocker-composeから環境変数で受け取りそれを元に接続を行います。

総評

ある程度コンパクトにまとまっており、構成もそこまで複雑ではないかなと思いました。ただ開発するときに少し難がありExpressは個別でnpm installなどでpackage.jsonなどを更新する必要があります。これはdocker-compose buildappフォルダをコピーしているので、アプリのnode_moduleなどは手動で更新しないとコンテナで動作してくれません。逆にnpm startなどで開発すればよい話になりますが、データベースとの接続ができないので困ったところです。
しかしながら、今回の勉強会ではいろいろとdocker-composeについて理解を深めることができたのでよかったかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンテナイメージサイズ削減手法の効果

サービスインにかかる時間短縮等のため、コンテナイメージサイズはできるだけ小さくする必要があります。Dockerではイメージレイヤ数削減が、コンテナイメージサイズ削減の常套手段です。

しかし、マルチステージビルドが利用可能になったこともあり、コンテナイメージサイズ削減の手法ごとの効果を改めて調べてみました。

対象

  • Dockerの概要は知っている人
  • コンテナイメージサイズ削減手法の効果が知りたい人

サンプル

以下の条件でコンテナイメージサイズを比較します。

  • ベースイメージ:nginx
  • トップページ:ngixのLicenseページ

Debian

比較元となるコンテナイメージです。以下のDockerfileを使用します。

FROM nginx:latest
WORKDIR /usr/share/nginx/html

RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y wget
RUN wget https://nginx.org/LICENSE -O index.html

Alpine

ディストリビューションの変更によりコンテナイメージサイズ削減を図ります。以下のDockerfileを使用します。

FROM nginx:alpine
WORKDIR /usr/share/nginx/html

RUN apk update
RUN apk upgrade
RUN apk add wget
RUN wget https://nginx.org/LICENSE -O index.html

Alpine + &&結合

&&結合によりイメージレイヤ数削減を図ります。以下のDockerfileを使用します。

FROM nginx:alpine
WORKDIR /usr/share/nginx/html

RUN apk update &&\
  apk upgrade &&\
  apk add wget  &&\
  wget https://nginx.org/LICENSE -O index.html

Alpine + マルチステージビルド

マルチステージビルドの利用により、イメージレイヤ数削減を図ります。マルチステージビルドはDocker ver 17.05以降で利用可能です。以下のDockerfileを使用します。

FROM alpine:latest AS build # .............................. A
WORKDIR /
RUN apk update
RUN apk upgrade
RUN apk add wget # ...................................... 1
RUN wget https://nginx.org/LICENSE -O index.html # ...... 2

FROM nginx:alpine # ........................................ B
COPY --from=build /index.html /usr/share/nginx/html # ... 3

上記のDockerfileの処理を解説します。

A. ビルド用コンテナ

B. 実行コンテナ

  1. ビルド用コンテナにwegetをインストールする
  2. ngixのLicenseページをindex.htmlとして保存する
  3. ビルド用コンテナのindex.htmlを実行コンテナのトップページとする
  • ビルド用コンテナはイメージのビルドが終われば削除される
  • 実行コンテナはアプリケーションの実行に必要なパッケージのみインストールすればよい
    • よって、コンテナイメージサイズの削減が図れる

結果

Debian Alpine Alpine + &&結合 Alpine + マルチステージビルド
ディストリビューション Debian Alpine Alpine Alpine
メモリ使用量(MB) 152 25.7 25.6 21.3

結論

  • ディストリビューションの変更によるコンテナイメージサイズ削減が最も効果が大きかった
  • &&結合はイメージレイヤ数が多くないとコンテナイメージサイズ削減の効果を発揮しにくい
  • イメージ削減の代償として、可読性も悪くなっている
    • 本番用実行コンテナをローカル環境でテストできることが重要で、開発用実行コンテナと本番用実行コンテナとの構成は異なってよいのでは?
    • ディストリビューションは共通で、開発時は可読性優先、本番用はコンテナイメージサイズ削減優先とすればよいのでは?

終わりに

コンテナイメージサイズ削減の手法と、手法ごとのコンテナのサイズをまとめました。可読性の問題も絡むため、コンテナイメージサイズ削減の手法はケースバイケースでよいのではないでしょうか。

参考

マルチステージ・ビルドを使う

Docker/Kubernetes 実践コンテナ開発入門

Docker multi stage buildで変わるDockerfileの常識

Dockerのマルチステージビルドを使う

今更ながらDocker マルチステージビルド

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub * TravisCI * Docker Repository(DockerHub, ECR)の連携

GitHub * TravisCI * Docker Repository の連携

TravisCI を使うことになりちょっと勉強しました。
CircleCIやGitLab CIとそんなに変わらないdockerが使えるCIです。

ソースは github にあげました。

ファイル

Djangoの雛形作成

一応、Djangoの雛形作成した手順です。
pyenv, Pipenvはセットアップ済です。

$ pipenv --python 3.6
$ pipenv install django==2.2.3
$ pipenv shell
(travis_ci) $ django-admin startproject travis_ci
(travis_ci) $ cd travis_ci
(travis_ci) $ python manage.py migrate

.travis.yml

GitHubでタグを打ったら、Docker Hub, ECRにDjango雛形が入ったdockerイメージをプッシュします。

.travis.yml
language: python

services:
  - docker

env:
  global:
    - APP_NAME=sample_travis_ci
    # awscli
    - PATH=$HOME/.local/bin:$PATH
    # Docker Hub
    - DOCKER_HUB_ACCOUNT=va034600
    # ECR
    - AWS_DEFAULT_REGION=ap-northeast-1

before_install:
  # ECR
  - pip install -U pip
  - pip install awscli

script:
  - echo "script"

after_success:
  - docker --version
  - docker build -t $APP_NAME:latest -f docker/Dockerfile .
  - docker images

  # Docker Hub
  - echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_ACCOUNT" --password-stdin
  # Docker Hub public
  - docker tag $APP_NAME:latest $DOCKER_HUB_ACCOUNT/${APP_NAME}_pub:${TRAVIS_TAG}
  - docker push $DOCKER_HUB_ACCOUNT/${APP_NAME}_pub:${TRAVIS_TAG}
  # Docker Hub private
  - docker tag $APP_NAME:latest $DOCKER_HUB_ACCOUNT/$APP_NAME:${TRAVIS_TAG}
  - docker push $DOCKER_HUB_ACCOUNT/$APP_NAME:${TRAVIS_TAG}

  # ECR
  - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
  - docker tag $APP_NAME:latest $AWS_ECR_ACCOUNT.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$APP_NAME:${TRAVIS_TAG}
  - docker push $AWS_ECR_ACCOUNT.dkr.ecr.ap-northeast-1.amazonaws.com/$APP_NAME:${TRAVIS_TAG}

branches:
  only:
  - "/^v?[0-9\\.]+/"

Dockerfile

FROM python:3.6.10

WORKDIR /usr/src/app

RUN pip install --upgrade pip && pip install pipenv

COPY ./Pipfile /usr/src/app/Pipfile
COPY ./Pipfile.lock /usr/src/app/Pipfile.lock

RUN pipenv install --system

COPY . /usr/src/app/

CMD tail -f /dev/null

各種サービスの設定

TravisCI

TravisCI の Environment Variablesの設定です。
スクリーンショット 2020-06-30 7.51.17.png

Docker Repository(DockerHub)

DockerHubにrepositoryを作成しました。
スクリーンショット 2020-06-30 7.53.14.png

Docker Repository(ECR)

ECR -> リポジトリ -> リポジトリ でrepositoryを作成しました。

起動

最後にDocker Hubに登録したdockerイメージをpullして起動確認しました。

docker-compose.yml

docker-compose.yml
version: '3'
services:
  api:
    image: va034600/sample_travis_ci_pub:v2.0.10
    working_dir: /usr/src/app/travis_ci
    command: python manage.py runserver 0.0.0.0:8000
    ports:
      - "8000:8000"
$ docker-compose up

http://127.0.0.1:8000

ブラウザで確認できたら終了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

k8s-the-hard-way in Docker ~ Dockerコンテナ内でkubernetes-the-hard-wayしてみた

概要

kubernetes-the-hard-way を実践しようと思い、自宅環境に手軽に構築したいためにdockerコンテナ上に構築することを考えました。クラスタ構築の勉強方法の一つと捉えていただければ。
事実誤認や別の効率的なやり方等存じていたら教えていただければ幸いです。

TL:DR

本稿は、k8sクラスタを手で構築しようとした筆者が詰まったポイントの備忘、もしくは同じくクラスタを構築したいという人がもしかしたら悩むかもしれないポイントの共有を目的としています。
そのため主に詰まったポイントと対処方法、リファレンスについて下記にまとめておきます。
詳細な内容については各章で触れていきます。

Issue Proposal Reference
各コンポーネントバイナリの実行時に”Operation not permitted”のエラー コンテナ実行時にcap_addする DockerCapability LinuxCapability
SSL証明書関連で実行権がないと怒られる 署名時に該当のhostnameを追加し直す 本稿参照
/procや/sysへの書き込み権限がないと怒られる write権限を付与して該当のFSをマウント、もしくは再マウント 本稿参照
containerdバイナリ実行時にcontained Not Found 依存パッケージ(libseccomp-dev libc6-compat)のインストール ref1, ref2
コンテナイメージをPullする際にx509: certificate signed by unknown authority ca-certificatesをインストール project page
servicesリソースへのアクセスができない ルーティングを追加 本稿参照
kube-proxyがwrite /sys/module/nf_conntrack/parameters/hashsize: operation not supported --conntrack-max=0を付加 ref
kubelet起動時の権限不足 tmpfsをマウント 本稿参照

はじめに

基本的にgcpの機能は使えないということと、dockerの機能で抑制されている部分を変更する必要がありました。
以下、k8s-the-hard-wayのチュートリアル(以降:本家)に沿ってそれぞれ解説していきます。
構築環境は、VirtualBOX上にCPU:1, RAM:4Gi, Disk:32GiのCentos8をminimalインストールした状態です。centos7でも同様の手順で問題なく動くこと実験済みです。

環境情報
$ cat /etc/redhat-release 
CentOS Linux release 8.1.1911 (Core) 
$ grep cpu.cores /proc/cpuinfo
cpu cores       : 1
$ lsblk | grep disk
sda           8:0    0   32G  0 disk
$ grep MemTotal /proc/meminfo 
MemTotal:        3871792 kB

$ docker --version
Docker version 19.03.8, build afacb8b

01-prerequisites

GoogleクラウドのCLI導入およびアカウント登録。
本稿では不要なので割愛。

02-client-tools

SSL証明書系の作成ツール(cfssl, cfssljson)およびkubernetes操作用のCLIツール(kubectl)の導入。
証明書一連の作業はその他のコマンドでも可能ですが、cfsslは公開鍵通信用の鍵作成から署名要求(CSR: Certificate Signing Request)、CAによる署名までを一気に行うことが可能なので、導入しておくことが好ましいです、素直にインストラクションに従います。
ちなみにcfsslは鍵のjson形式での出力、cfssljsonで鍵ファイルの保存を行います。

$ {
curl -O https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/linux/cfssl
curl -O https://storage.googleapis.com/kubernetes-the-hard-way/cfssl/linux/cfssljson
chmod +x cfssl cfssljson
mv cfssl cfssljson /usr/local/bin/
}

# 1.3.4 >= であることを確認
$ cfssl version
Version: 1.3.4
Revision: dev
Runtime: go1.13
$ cfssljson --version
Version: 1.3.4
Revision: dev
Runtime: go1.13

次にkubectlを同様にしてインストール。
こちらはkubernetesのいわば神経系とも言うべきkube-apiサーバに対しリクエストをなげるためのバイナリです。
k8sを扱う際、基本全てkube-api経由で管理することになりますが、例えばクラスタ内のワーカーノードの情報を取得したいといった場合に
curl -X GET --cacert ../ca/ca.pem --cert admin.pem --key admin-key.pem https://balancer:443/api/v1/nodes
とGETリクエストを出し、帰ってきたJSON形式の文字列を処理して・・・といったような面倒くさいことになります。しかしこちらのバイナリを使えば
kubectl get no
と、3単語入れるだけで情報の取得から人間に読みやすい形への整形・出力を勝手にしてくれます。
素のAPI経由での操作が気になればReferenceを読むといいと思います(丸投げ
とてもいい感じにAPIサーバとのやりとりが書かれている記事もありました
 →転職したらKubernetesだった件

kubectl-install
$ {
curl -O https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
# 1.15.3 >=
kubectl version --client
}
output
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.2", GitCommit:"52c56ce7a8272c798dbc29846288d7cd9fbae032", GitTreeState:"clean", BuildDate:"2020-04-16T11:56:40Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}

03-compute-resources

本家ではGCP上にVPCの作成とインスタンスの作成を行なっているが、本稿ではここをDockerコンテナに置き換えます。
なお、ここで作成されるネットワークは以下のような構成になっています。

  +---------------------------------------------------+ host machine +----------------------------------------------+
  |                                                                                                                 |
  |                                                                                                                 |
  |                                                 docker network(k8s)                                             |
  |    +-------------------------------------------+ 172.16.10.0/24 +------------------------------------------+    |
  |    |                                                                                                       |    |
  |    |                                                                                                       |    |
  |    |                                                balancer                                               |    |
  |    |                                          +-+ 172.16.10.10 +-+                                         |    |
  |    |                                          |                  |                                         |    |
  |    |                                          +------------------+                                         |    |
  |    |                                                                                                       |    |
  |    |                    ctl-0                         ctl-1                         ctl-2                  |    |
  |    |            +-+ 172.16.10.20 +-+          +-+ 172.16.10.21 +-+          +-+ 172.16.10.22 +-+           |    |
  |    |            |                  |          |                  |          |                  |           |    |
  |    |            +------------------+          +------------------+          +------------------+           |    |
  |    |                                                                                                       |    |
  |    |                  worker-0                      worker-1                      worker-2                 |    |
  |    |           +--+ 172.16.10.30 +--+        +--+ 172.16.10.31 +--+        +--+ 172.16.10.32 +--+          |    |
  |    |           |                    |        |                    |        |                    |          |    |
  |    |           |      podCIDR       |        |      podCIDR       |        |      podCIDR       |          |    |
  |    |           | + 10.200.0.0/24 +  |        | + 10.200.1.0/24 +  |        | + 10.200.2.0/24 +  |          |    |
  |    |           | |               |  |        | |               |  |        | |               |  |          |    |
  |    |           | +---------------+  |        | +---------------+  |        | +---------------+  |          |    |
  |    |           |                    |        |                    |        |                    |          |    |
  |    |           |                    |        |                    |        |                    |          |    |
  |    |           |                    |        | serviceClusterCIDR |        |                    |          |    |
  |    |          +-------------------------------+ 10.100.0.0/16 +----------------------------------+         |    |
  |    |          |                                                                                  |         |    |
  |    |          +----------------------------------------------------------------------------------+         |    |
  |    |           |                    |        |                    |        |                    |          |    |
  |    |           +--------------------+        +--------------------+        +--------------------+          |    |
  |    |                                                                                                       |    |
  |    +-------------------------------------------------------------------------------------------------------+    |
  +-----------------------------------------------------------------------------------------------------------------+

Networking

最初にNetworkingについて説明があるが、ひとまず本チュートリアルの操作に関係のある部分は存在しないので読み流す。
内容的にはkubernetesのネットワーキングの思想は「クラスタ内のpodは一意のIPを持ち、それを通じてノードを問わず互いに認識できる」ことである、というようなことが書いてある。
なお、これもチュートリアルの粋を超えている内容ではあるが、クラスタ内でアクセスコントロールする際はNetworkPolicyリソースを用いる(対応しているPluginをkubelet起動時に指定している必要がある。有名どころはCalicoとかでしょうか)

Firewall Rules

今回CentOS8で構築したため、OSインストールした初期状態ではコンテナ間の通信を許可するために下記の設定がホストマシンで必要です。
CentOS7では特に設定は必要ありませんでした。

firewall_for_centos8
# コンテナからWANへの通信を許可(apk利用のため)
$ {
nft add rule inet firewalld filter_FWDI_public_allow udp dport 53 accept
nft add rule inet firewalld filter_FWDI_public_allow tcp dport { 80,443 } accept
# コンテナ内部での通信を許可
nft add rule inet firewalld filter_FWDI_public_allow ip saddr 172.16.10.0/24 ip daddr 172.16.10.0/24 accept
}

これに関連して格闘した形跡をこちらに記しているので、気が向いたら読んでいただければと思います。

Kubernetes Public IP Address

Load Balancerに紐づけるIPの取得。
本稿ではコンテナでまとめて作るので省略

Compute Instances

いよいよ一つの山場。
若干長くなるのと、本家のチュートリアルから外れる内容ではあるので面倒な方はこちらをcloneしていただければそのまま次の04-certificate-authorityに進めます。

ここではLoadBalancer、ControlPlane、WorkerNodeの3種類のイメージを作成し、Dockerサービスを作成していきます。
まずDockerイメージをビルドする際無駄にデーモンが重くならないようにディレクトリを分けます。
テスト目的なのでほとんどの実行バイナリをホスト上から共有するようにしています。

$ mkdir -p build \
           bin/ctl/bin bin/ctl/exec bin/worker/bin bin/worker/exec bin/worker/opt \
           lib/balancer lib/ctl lib/worker/cni lib/worker/containerd lib/worker/kubelet lib/worker/kube-proxy \
           lib/certs/ca lib/certs/ca_sec lib/certs/ctl lib/certs/worker \
           env/common \
           cert_configs
# こんな感じのディレクトリ構成になります
# .
# |-- bin 
# |   |-- ctl 
# |   |   |-- bin 
# |   |   `-- exec
# |   `-- worker
# |       |-- bin 
# |       |-- exec
# |       `-- opt 
# |-- build
# |-- env 
# |   `-- common
# |-- lib 
# |   |-- balancer
# |   |-- certs
# |   |   |-- ca
# |   |   |-- ca_sec
# |   |   |-- ctl 
# |   |   `-- worker
# |   |       |-- kubelet
# |   |       `-- kube-proxy
# |   |-- ctl 
# |   `-- worker
# |       |-- etccontainerd
# |       |-- kubelet
# |       |-- kube-proxy
# |       `-- varcni
# `-- cert_utils

以下の内容でDockerfile、entrypoint.sh(サービス起動用のシェルスクリプト)、docker-composeファイルを作成。インスタンス数は本家と同じくLoadBalancer=1, ControlPlane=3, WorkerNode=3で構成しています。
DockerFiledocker-compose.ymlの各Instructionは本家のページを参考ください。

build/Dockerfile_ctl
FROM alpine:3.11

RUN apk --update --no-cache --no-progress add \
    bash \
    && rm -rf /var/cache/apk/*

#for ETCD runtime
RUN mkdir -p /etc/etcd /var/lib/etcd 

#for kubernetes control
#such as api-server, conroller-manager, scheduler
RUN mkdir -p /var/lib/kubernetes/

#
COPY entrypoint.sh /
CMD [ "/bin/bash", "/entrypoint.sh" ]
build/Dockerfile_worker
FROM alpine:3.11

RUN apk --update --no-cache --no-progress add \
    bash \
    socat ipset conntrack-tools \
    libseccomp-dev libc6-compat \
    iptables \
    ca-certificates \
    && rm -rf /var/cache/apk/*


# for provisioning worker node (kubelet, proxy)
RUN mkdir -p /etc/cni/net.d \
            /opt/cni/bin \
            /var/lib/kubelet \
            /var/lib/kube-proxy \
            /var/lib/kubernetes \
            /var/run/kubernetes

#
# ENV PATH ${C_HOME}/bin
COPY entrypoint.sh /
CMD [ "/bin/bash", "/entrypoint.sh" ]
build/Dockerfile_loadbalancer
FROM nginx:alpine
RUN printf 'stream {\n\tinclude /etc/nginx/conf.d/*.conf.stream;\n}\n'  >> /etc/nginx/nginx.conf

見ての通り全てalpineベースで構成している。
重点だけ記しておくと

  • socat ipset conntrack-tools はチュートリアル本編に沿って入れる、ネットワーク関連
  • iptables はkube-proxy動作のために必要だがalpineにデフォルトで入っていないので改めてインストール
  • ca-certificatesは証明書インストールで、これがないとcontainerdがイメージをpullする際にx509: certificate signed by unknown authorityとなってpullできない
  • LoadBalancerはnginxのTCPロードバランサー機能を利用します。そのためnginx.confに、別の設定ファイルをhttpブロック外でincludeする行を記載

続けてentrypoint.sh。
単純にkubernetes関連のサービス起動用に作成するシェルスクリプトをスリープを含んでキックしていくだけの代物です。
いろいろ試すためにスクリプトを修正してすぐに反映して、という進め方を想定しているので、openrcなどsysinit系は特に仕込みませんでした(本家からずれてるけど、、)

build/entrypoint.sh
#!/bin/bash
while read svc sleepSec; do
[ -f ${svc} ] && {
echo " service '$(basename ${svc%.sh})' starting..."
/bin/bash ${svc} &
sleep ${sleepSec:-5}
echo "done."
}
done<<EOF
/exec/ETCD_SERVICE.sh 10
/exec/KUBE-API_SERVICE.sh 20
/exec/KUBE-CONTROLLER-MANAGER_SERVICE.sh
/exec/KUBE-SCHEDULER_SERVICE.sh
/exec/RBAC_AUTH.sh
/exec/CONTAINERD_SERVICE.sh 30
/exec/KUBELET_SERVICE.sh 20
/exec/KUBE-PROXY_SERVICE.sh
EOF

echo "container Ready"
tail -f /dev/null

最後にdocker-composeファイル。LinuxCapability について勉強する機会となったので最小限のCapabilityとなるように気をつけましたが、ワーカーノードは結局--privilrgedをつけないと起動できなかった、、

docker-compose.yml
version: "3.7"                                                                                                                                                                                              
x-services:                                                                                                                                                                                                 
  &default-ctl                                                                                                                                                                                              
    build:                                                                                                                                                                                                  
      context: ./build                                                                                                                                                                                      
      dockerfile: Dockerfile_ctl                                                                                                                                                                            
    image: k8snodes-ctl:0.1                                                                                                                                                                          
    init: true                                                                                                                                                                                              
    networks:                                                                                                                                                                                               
      k8s:                                                                                                                                                                                                  
    volumes:                                                                                                                                                                                                
      - "/sys/fs/cgroup:/sys/fs/cgroup:rw"                                                                                                                                                                                                                                                                                                                                            
      - "./bin/ctl/exec:/exec:ro"                                                                                                                                                                           
      - "./bin/ctl/bin:/usr/local/bin:ro"                                                                                                                                                                   
      - "./lib/certs/ctl:/var/lib/kubernetes:ro"                                                                                                                                                            
      - "./lib/certs/ca:/var/lib/ca:ro"                                                                                                                                                                     
      - "./lib/certs/ca_sec:/var/lib/ca_sec:ro"                                                                                                                                                             
    cap_drop:                                                                                                                                                                                               
      - ALL                                                                                                                                                                                                 
    cap_add:                                                                                                                                                                                                
      - SYS_CHROOT                                                                                                                                                                                          
      - SETGID                                                                                                                                                                                              
      - SETUID                                                                                                                                                                                              
      - CHOWN                                                                                                                                                                                               
      - NET_RAW
      - NET_ADMIN                                                                                                                                                                                          
      - DAC_OVERRIDE                                                                                                                                                                                                                                                                                                                                                                       
    dns_search:
      - k8s
    environment:
      - KUBECONFIG=/var/lib/kubernetes/admin.kubeconfig

x-services:          
  &default-worker        
    build:         
      context: ./build
      dockerfile: Dockerfile_worker
    image: k8snodes-worker:0.1
    init: true     
    networks:      
      k8s:  
    volumes:                                 
      - "/lib/modules:/lib/modules:ro"
      - "./bin/worker/exec:/exec:ro"
      - "./bin/worker/bin:/usr/local/bin:ro"
      - "./bin/worker/opt:/opt/cni/bin:ro"
      - "./lib/worker/etccontainerd:/etc/containerd:ro"
      - "./lib/worker/varcni:/var/cni:ro"
      - "./lib/worker/kubelet:/var/lib/kubelet_config:ro"
      - "./lib/worker/kube-proxy:/var/lib/kube-proxy_config:ro"
      - "./lib/certs/worker/kubelet:/var/lib/kubelet_certs:ro"
      - "./lib/certs/worker/kube-proxy:/var/lib/kube-proxy:ro"
      - "./lib/certs/ca:/var/lib/ca:ro"
      - type: tmpfs
        target: /var/lib/containerd
        tmpfs:              
          size: 500M  
    privileged: true            
    dns_search:                                
      - k8s

x-services:
  &default-balancer
    build:
      context: ./build
      dockerfile: Dockerfile_lb
    image: nginx_lb:alpine
    ports:
      - "10443:443"
    volumes:
      - "./lib/balancer:/etc/nginx/conf.d:ro"
    networks:
      k8s:
    cap_drop:
      - ALL
    cap_add:
      - NET_RAW
      - NET_BIND_SERVICE
      - CHOWN
      - SETGID
      - SETUID

services:
  balancer:
    << : *default-balancer
    container_name: balancer
    hostname: balancer
    networks:
      k8s:
        ipv4_address: 172.16.10.10
  ctl-0:
    << : *default-ctl
    container_name: ctl-0
    hostname: ctl-0
    networks:
      k8s:
        ipv4_address: 172.16.10.20
  ctl-1:
    << : *default-ctl
    container_name: ctl-1
    hostname: ctl-1
    networks:
      k8s:
        ipv4_address: 172.16.10.21
  ctl-2:
    << : *default-ctl
    container_name: ctl-2
    hostname: ctl-2
    networks:
      k8s:
        ipv4_address: 172.16.10.22

  worker-0:
    << : *default-worker
    container_name: worker-0
    hostname: worker-0
    networks:
      k8s:
        ipv4_address: 172.16.10.30
  worker-1:
    << : *default-worker
    container_name: worker-1
    hostname: worker-1
    networks:
      k8s:
        ipv4_address: 172.16.10.31
  worker-2:
    << : *default-worker
    container_name: worker-2
    hostname: worker-2
    networks:
      k8s:
        ipv4_address: 172.16.10.32

networks:
  k8s:
    name: k8s 
    ipam:
      config:
        - subnet: 172.16.10.0/24
    driver_opts:
          com.docker.network.bridge.name: k8s_br

これも要点だけまとめると

  • x-接頭辞はExtensionフィールドであり、記述はできるが解釈されなくなるので、yamlのアンカー・エイリアス構文を用いることができるようになる
  • /sys/fs/cgroup:/sys/fs/cgroup:rwの権限でのマウントは必須だが、workerではprivilegedオプションをつけているので省略可
  • workerノードの/lib/modules:/lib/modules:roはkube-proxyに求められるが、無視しても問題ないとは言われるので必須ではない
  • workerノードのcontainerdのルートディレクトリにtmpfsをマウントは、デフォルトのoverlayfsではエラーとなるため必須
  • dns-searchは互いのノードIPを調べる際に必要となる。<dokcer networkの名前>というドメインに設定されるので、そこに合わせるようにする。
  • docker-composeのバージョン3系を用いる場合、init: trueの部分に関しては3.7>=となっている。ただしこのオプションを付けなくても特に問題はない。ただしExtensionフィールドは3.4>= となっているのでそこだけ注意

以上です。少々長くなりましたが、これでkubernetesの各コンポーネントを動かす準備ができました。
一応本家ではsshで各インスタンスにアクセスするようになっていますが、ここではdockerなのでプロセスのアタッチで各ノードに入ります。

ノードへのアクセス方法
$ docker exec -ti <container name> sh

04-certificate-authority

kubernetesの各コンポーネントはクラウド基板上で稼働することが前提であり、互いの接続をTLS認証で行います。
このセクションでは自分用PKIを作成して自己署名した証明書を作ります。
まずはCAを作ります。

CA
$ cd cert_util
$ {
cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Tokyo",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Asia"
    }
  ]
}
EOF
# C  : Country
# L  : Locality or City
# O  : Organization
# OU : Organization Unit
# ST : State


cfssl gencert -initca ca-csr.json | cfssljson -bare ca

}

CAの公開鍵認証用のペアができます。

ca-key.pem
ca.pem

ここからはkubernetesクラスタのサーバの各コンポーネントと、kubectlを扱うクライアントのための証明書を作成し、このCAによって署名していきます。
各証明書の役割は以下の通りです

  • adminユーザ用 (controlプレーン&クライアント)
  • kubelet用 (workerノード)
  • kube-proxy用 (workerノード)
  • Controller-Manager用 (controlプレーン)
  • Scheduler用 (controlプレーン)
  • Service-Account-Key用 (controlプレーン)
  • kubernetes-API-server(兼etcd)用 (controlプレーン)

Service Account Keyに関してはmanaging service accountsに記載ありますが、Contoroller Managerとkube-api-serverコンポーネントで用いられ、各podのプロセスがデフォルトの状態でkube-api-serverにアクセスする権限を持たせるために使われる。ということらしいです。
まずはadminユーザ用。これはcontrolプレーン、及び自分がクラスタと更新する際にkubectlから呼び出して使う用の証明書になります。

adminユーザ用
# in ./cert_util
$ sed '
/"CN"/s/Kubernetes/admin/
/"O"/s/Kubernetes/system:masters/
/"OU"/s/CA/Kubernetes The Hard Way/
' ca-csr.json | tee admin-csr.json

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  admin-csr.json | cfssljson -bare admin

上と同じく、adminと頭についた鍵ペアができます。
*.csrのファイルはcaで署名される前の署名要求なので、基本鍵ペア以外は不要です。
次にworkerノードの各コンポーネント用の証明書ペアを作成していきます。

kubeletではNode Authorizerという認証モードを用いてkubeletからのAPIリクエストを認証するようです。これを用いるためには 「system:nodes グループ」に属し、「system:node:<nodeName>というユーザ名」を持った証明書をkubeletが用いる必要があるようです。公式のドキュメントにもしっかりと記載がありました。
x509公開鍵認証ではCommonName(CN)フィールドの値が「ユーザ名」、Organization(O)フィールドの値が「グループ」となるようなのでここの整合性がちゃんと取れるように気をつけます。ちなみにnodeNameはworkerノード内でhostnameで帰ってくる値と一致している必要がありました。

kubelet用
# in ./cert_util

$ for instance in worker-0 worker-1 worker-2
do sed '
/"CN"/s/admin/system:node:'${instance}'/
/"O"/s/system:masters/system:nodes/
' admin-csr.json | tee ${instance}-csr.json

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${instance} \
  -profile=kubernetes \
  ${instance}-csr.json | cfssljson -bare ${instance}
done

-hostnameで証明書のSAN (Subject Altenative Name)を設定できます。この証明書を使って接続するリモート側のDNS名、もしくはアドレスが、CNもしくはSANに設定されている値に一致しないと証明書エラーとなってしまいます。
実際に試したところkubectlを用いてpodにアタッチする際、kubeletは自身に対してAPIリクエストを出しており、ここで-hostnameを指定しなければkubectl execkubectl logsを実行しようとする際に証明書エラーとなりました。
本家ではworkerノード自体のInternal・External IPを指定していましたが、特に指定しなくでも(今のところは)動かせたので確認できたケースではDNS名しか要求していない模様。
問題生じたらアップデートしようかと思います。

長くなりましたが次にkube-proxy用の証明書。
ここからは特に意識することはなく本家通り粛々と作成していきます。

kube-proxy用
# in ./cert_util

$ sed '
/"CN"/s/admin/system:kube-proxy/
/"O"/s/system:masters/system:node-proxier/
' admin-csr.json | tee kube-proxy-csr.json

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-proxy-csr.json | cfssljson -bare kube-proxy

次にkube-controller-manager用の証明書。一応、ここからはcontrolプレーンの各コンポーネントになります。

kube-controller-manager用
# in ./cert_util

$ sed '
/"CN"/s/admin/system:kube-controller-manager/
/"O"/s/system:masters/system:kube-controller-manager/
' admin-csr.json | tee kube-controller-manager-csr.json

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager

kube-scheduler用

kube-scheduler用
# in ./cert_util

$ sed '
/"CN"/s/admin/system:kube-scheduler/
/"O"/s/system:masters/system:kube-scheduler/
' admin-csr.json | tee kube-scheduler-csr.json

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-scheduler-csr.json | cfssljson -bare kube-scheduler

service-account用

service-account用
# in ./cert_util

$ sed '
/"CN"/s/admin/service-accounts/
/"O"/s/system:masters/Kubernetes/
' admin-csr.json | tee service-account-csr.json

$ cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  service-account-csr.json | cfssljson -bare service-account

最後にkubernetes-api-server用なのですが、こいつは最低限etcd達とLoad Balancer、あとkubernetesが自動的に作成するサービスリソースのClusterIP(後ほど割り当てるサービスCIDRの、一番最初の序列のIP。CIDRが10.100.0.0/16なら10.100.0.1となる)につながれば動作できたので、そこにSANを限定していきます(本家ではデフォルトで作成されるサービスリソース(kubernetes)のいろいろな形でのドメイン含めた名前が加えられており、そこから外れているが最低限の設定の方が不具合発生時した際に勉強になる部分が多いと感じるので、、)

API_&_etcd用
# in ./cert_util

$ sed '
/"CN"/s/admin/kubernetes/
/"O"/s/system:masters/Kubernetes/
' admin-csr.json | tee kubernetes-csr.json

$ {
LOs='localhost,127.0.0.1'
# Control Plane
CTLs='172.16.10.20,172.16.10.21,172.16.10.22'
# kubernetes services
SVC='10.100.0.1,kubernetes,balancer'
# 外部のクライアントからアクセスする用の、dockerホスト
EXTERNAL='172.16.10.1'


cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${CTLs},${LOs},${SVC},${EXTERNAL} \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes
}

最後にしかるべき場所にこれらの証明書を配置します。

kube-proxy, kube-controller-manager, kube-scheduler, and kubelet


次の段階でkubeconfigファイルに含めて用いるので(あと一応、adminも)、これらは動かしません。

# in ./cert_util
$ {
cp -p ca.pem ../lib/certs/ca
mv ca-key.pem ../lib/certs/ca_sec
mv service-account*.pem ../lib/certs/ctl
mv kubernetes*.pem ../lib/certs/ctl
}

05-kubernetes-configuration-files

ここではkubernetes APIのクライアントとなる

controller manager, kubelet, kube-proxy, and scheduler clients and the admin user.

のための、接続のための認証情報と接続先の情報をkubeconfigファイルという物の中に埋め込んでいきます。
クライアント達の接続先はローカルホスト、及びLoad Balancerとなるので、Load BalancerのIPを本家では取得しています。本稿ではdocker-compose.ymlの中で定めた172.16.10.10です。

$ KUBERNETES_PUBLIC_ADDRESS='balancer'

それでは作成していきます。

kubelet用
# in ./cert_util
$ {
for instance in worker-0 worker-1 worker-2; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:443 \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-credentials system:node:${instance} \
    --client-certificate=${instance}.pem \
    --client-key=${instance}-key.pem \
    --embed-certs=true \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

  kubectl config use-context default --kubeconfig=${instance}.kubeconfig
done
}
kube-proxy用
# in ./cert_util
$ {
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:443 \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config set-credentials system:kube-proxy \
    --client-certificate=kube-proxy.pem \
    --client-key=kube-proxy-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-proxy \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig
}
kube-controller-manager用
# in ./cert_util
$ {
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-credentials system:kube-controller-manager \
    --client-certificate=kube-controller-manager.pem \
    --client-key=kube-controller-manager-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-controller-manager \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig
}
kube-scheduler用
# in ./cert_util
$ {
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-credentials system:kube-scheduler \
    --client-certificate=kube-scheduler.pem \
    --client-key=kube-scheduler-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-scheduler \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig
}
adminユーザ用
# in ./cert_util
$ {
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=admin.kubeconfig

  kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem \
    --embed-certs=true \
    --kubeconfig=admin.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=admin \
    --kubeconfig=admin.kubeconfig

  kubectl config use-context default --kubeconfig=admin.kubeconfig
}

最後にこれらもしかるべき場所に配置します。一応、kubelet用のworker-?.kubeconfigは本来それぞれのノードにあるべきですが特にそこまで拘らなくてもいいかと思いひとところにまとめています。

# in ./cert_util
$ {
mv admin.kubeconfig kube-scheduler.kubeconfig kube-controller-manager.kubeconfig ../lib/certs/ctl/

mv kube-proxy.kubeconfig ../lib/certs/worker/kube-proxy/
mv worker-?.kubeconfig ../lib/certs/worker/kubelet/
mv worker-*.pem ../lib/certs/worker/kubelet/
}

06-data-encryption-keys

ここではKubernetes上で扱われるデータ(APIリソース)の暗号化を行う設定ファイルを作成します。
ここで作成するファイルに関して言えば、APIリソースの中でもsecretsリソースに対して暗号化するようにしています。13-smoke-testで動作確認します。

$ cd ..
# in .
$ cat | tee lib/certs/ctl/encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: $(head -c 32 /dev/urandom | base64)
      - identity: {}
EOF

07-bootstrapping-etcd

ここではconrolプレーンのデータストアであるetcdを起動させます。
各controlプレーン内で起動させ、3台でクラスタ構成を組みます。
まず、etcdetcdctlのバイナリを入手します

# in .
$ {
curl -LO "https://github.com/etcd-io/etcd/releases/download/v3.4.0/etcd-v3.4.0-linux-amd64.tar.gz"
# 最新版は https://github.com/etcd-io/etcd/releases をチェック
tar -xf etcd-v3.4.0-linux-amd64.tar.gz

mv etcd-v3.4.0-linux-amd64/etcd* bin/ctl/bin/
# controlプレーンで/usr/local/binにマウントする領域。移動したもの以外は削除してよし
rm -fr etcd-v3.4.0-linux-amd64*
ls -lh bin/ctl/bin/
}

etcd起動用のシェルを書きます。
それぞれのホスト名とdockerから割り当てられているIPを動的に入手するようにしています。
それと、証明書類について前章で作成したkubernetes*.pemが必要になりますが、本編での設定と合わせるためシンボリックリンク貼るようにしています。

bin/ctl/exec/ETCD_SERVICE.sh
#!/bin/bash
ETCD_NAME=$(hostname -s)
INTERNAL_IP=$(awk '/'${ETCD_NAME}'/ {print $1}' /etc/hosts)
CTLs=$(IFS=',';for i in {0..2};do CTLs[$i]=ctl-${i}=https://172.16.10.2${i}:2380;done;echo "${CTLs[*]}"; )

for link_target in /var/lib/kubernetes/kubernetes.pem /var/lib/kubernetes/kubernetes-key.pem /var/lib/ca/ca.pem; do                                                                                        
  dest="/etc/etcd/$(basename ${link_target})"                                                                                                                                                                
  [ -L  ${dest} ] || ln -s ${link_target} ${dest}                                                                                                                                                            
done

/usr/local/bin/etcd \
  --name ${ETCD_NAME} \
  --cert-file=/etc/etcd/kubernetes.pem \
  --key-file=/etc/etcd/kubernetes-key.pem \
  --peer-cert-file=/etc/etcd/kubernetes.pem \
  --peer-key-file=/etc/etcd/kubernetes-key.pem \
  --trusted-ca-file=/etc/etcd/ca.pem \
  --peer-trusted-ca-file=/etc/etcd/ca.pem \
  --peer-client-cert-auth \
  --client-cert-auth \
  --initial-advertise-peer-urls https://${INTERNAL_IP}:2380 \
  --listen-peer-urls https://${INTERNAL_IP}:2380 \
  --listen-client-urls https://${INTERNAL_IP}:2379,https://127.0.0.1:2379 \
  --advertise-client-urls https://${INTERNAL_IP}:2379 \
  --initial-cluster-token etcd-cluster-0 \
  --initial-cluster ${CTLs} \
  --initial-cluster-state new \
  --log-outputs '/etc/etcd/etcd.log' \
  --logger=zap \
  --data-dir=/var/lib/etcd

オプション名でなんとなく判別できますが、一応要点だけまとめると

  • etcdはデフォルトでクラスタ間用の通信にポート番号2380を、クライアント用に2379を用いる(もちろん変更可)
  • peerでクラスタ間の通信について定義
  • clientで外部からのリクエストを受け付ける通信について定義。ローカルを入れてるのはデバッグ時にetcdctlを用いるが、そのデフォルトの向き先がそうなっているため
  • clientからの通信の際は--key--certで指定した証明書類と適合するものが必要になる
  • --log-outputsは本家では指定されていない。おそらくsysinitで起動するためと思われるが、ここでは明示的に指定して出力させておく

次に動作確認です。
これだけ作成した状態でいったんサービスを起動させます。

# in .
$ docker-compose up -d ctl-{0..2}
Creating network "k8s" with the default driver
Creating ctl-2 ... done
Creating ctl-0 ... done
Creating ctl-1 ... done

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
ed27920de960        k8snodes-ctl:0.1    "/bin/bash /entrypoi…"   47 seconds ago      Up 44 seconds                           ctl-1
3e39308fc506        k8snodes-ctl:0.1    "/bin/bash /entrypoi…"   47 seconds ago      Up 44 seconds                           ctl-0
5ca113b3596e        k8snodes-ctl:0.1    "/bin/bash /entrypoi…"   47 seconds ago      Up 44 seconds                           ctl-2

$ docker exec -ti ctl-0 /bin/sh -c "ETCDCTL_API=3 etcdctl member list \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/kubernetes.pem \
  --key=/etc/etcd/kubernetes-key.pem -w table"

output
+------------------+---------+-------+---------------------------+---------------------------+------------+
|        ID        | STATUS  | NAME  |        PEER ADDRS         |       CLIENT ADDRS        | IS LEARNER |
+------------------+---------+-------+---------------------------+---------------------------+------------+
| 61c4f7534361ad48 | started | ctl-1 | https://172.16.10.21:2380 | https://172.16.10.21:2379 |      false |
| b167b173b818a05c | started | ctl-0 | https://172.16.10.20:2380 | https://172.16.10.20:2379 |      false |
| f6d332cd022ef5f8 | started | ctl-2 | https://172.16.10.22:2380 | https://172.16.10.22:2379 |      false |
+------------------+---------+-------+---------------------------+---------------------------+------------+

ちゃんと表示されば成功です。
失敗したら、大抵の場合はdockerホスト側のNetfilterの設定がうまくできていない可能性が高いです。

ここまで確認できたらいったん止めます。

$ docker-compose stop

08-bootstrapping-kubernetes-controllers

次にcontrolプレーンの他のコンポーネントを起動していきます。
まずコンポーネント用のバイナリを入手し、共有領域に置いてあげます。

# in .
$ {
curl -L \
-O  "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-apiserver" \
-O  "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-controller-manager" \
-O  "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-scheduler" \
-O  "https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl"

chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl
mv kube-apiserver kube-controller-manager kube-scheduler kubectl bin/ctl/bin
}

続けてそれぞれのサービスを起動するためのスクリプトを書いていきます。

APIサーバ

bin/ctl/exec/KUBE-API_SERVICE.sh
#!/bin/bash

INTERNAL_IP=$(awk '/'$(hostname -s)'/ {print $1}' /etc/hosts)
CTLs=$(IFS=',';for i in {0..2};do CTLs[$i]=https://172.16.10.2${i}:2380;done;echo "${CTLs[*]}"; )

# add route to other node's pods
# this config required in "11-pod-network-routes" section
for ((i=0;i<3;i++));do
  ip r add to 10.200.$i.0/24 via 172.16.10.3$i
done

LOGFILE='/var/log/kube-api.log'

/usr/local/bin/kube-apiserver \
  --advertise-address=${INTERNAL_IP} \
  --allow-privileged=true \
  --apiserver-count=3 \
  --audit-log-maxage=30 \
  --audit-log-maxbackup=3 \
  --audit-log-maxsize=100 \
  --audit-log-path=/var/log/audit.log \
  --authorization-mode=Node,RBAC \
  --bind-address=0.0.0.0 \
  --client-ca-file=/var/lib/ca/ca.pem \
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
  --etcd-cafile=/var/lib/ca/ca.pem \
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \
  --etcd-servers=${CTLs} \
  --event-ttl=1h \
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \
  --kubelet-certificate-authority=/var/lib/ca/ca.pem \
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \
  --kubelet-https=true \
  --runtime-config=api/all=true \
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \
  --service-cluster-ip-range=10.100.0.0/16 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --log-file=${LOGFILE} \
  --logtostderr=false \
  --v=2
  • ここでもログファイルを明示的に指定する部分を追加しています。
  • APIリソースの中のserviceリソースが割り当てられるCIDRもここで指定しています。Controller-Managerの起動オプションの中でも指定できますが、こちらで設定した内容の方が優先されました

Controller-Manager

bin/ctl/exec/KUBE-CONTROLLER-MANAGER_SERVICE.sh
#!/bin/bash
LOGFILE="/var/log/kube-controller-manager.log"

/usr/local/bin/kube-controller-manager \
  --bind-address=0.0.0.0 \
  --cluster-cidr=10.200.0.0/16 \
  --cluster-name=kubernetes \
  --cluster-signing-cert-file=/var/lib/ca/ca.pem \
  --cluster-signing-key-file=/var/lib/ca_sec/ca-key.pem \
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
  --leader-elect=true \
  --root-ca-file=/var/lib/ca/ca.pem \
  --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \
  --use-service-account-credentials=true \
  --log-file=${LOGFILE} \
  --logtostderr=false \
  --v=2

kube-Scheduler
ここだけConfigファイルでパラメータを渡します。
基本k8sではConfigファイルを経由してオプション類を渡すことが推奨されています。
APIとController-ManagerだけなぜかConfigファイルを渡すためのオプション等がなくベタ書きという感じでした。コアモジュールだからかわかりませんが、そのうち設定できるようになるのかもしれません。

kube-scheduler.yaml
# in .
$ cat <<EOF | sudo tee lib/certs/ctl/kube-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true
EOF
bin/ctl/exec/KUBE-SCHEDULER_SERVICE.sh
#!/bin/bash
CONFIGFILE='/var/lib/kubernetes/kube-scheduler.yaml'
LOGFILE='/var/log/kube-scheduler.log'

/usr/local/bin/kube-scheduler \
  --config=${CONFIGFILE} \
  --log-file=${LOGFILE} \
  --logtostderr=false \
  --v=2

本家ではここで外部のロードバランサーが行うヘルスチェックのため、contarolプレーンの80番ポートに対するリクエストを、kube-api-serverが公開するhttps://127.0.0.1:6443/healthzへと中継させるようにnginxのproxy機能を活用する設定をしています。
が、本稿で用いているロードバランサーはフリー版のnginxで代用しており、製品バージョンでしかActiveヘルスチェック機能は提供されないようでしたので、やるとしたらPassiveですがここまでマネなくてもいいかと思い割愛しました。
最後にkube-api-serverから各workerノードのkubeletにRBACでアクセスするための設定をします。
これはpodのログを取得する際やコマンドを実行する際に用いられます。
後ほどpodは起動するけど上記のような操作ができないといった場合にはこちらのClusterRoleリソースがちゃんと作られているかどうか確認するといいと思います。
こちらはAPIサーバで管理されるリソース作りなので、一度作成すればクラスタ全体で有効になりますが、特に何度適用しても問題はないのでサービス起動時のスクリプトに含めています。
ちなみにこの際にadminユーザのkubeconfigが用いられるようになっています。

bin/ctl/exec/RBAC_AUTH.sh
#!/bin/bash
CONFIGFILE='/var/lib/kubernetes/admin.kubeconfig'

cat <<EOF | kubectl apply --kubeconfig ${CONFIGFILE} -f -
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdata: "true"
  labels:
    kubernetes.io/bootstrapping: rbac.defaults
  name: system:kube-apiserver-to-kubelet
rules:
  - apiGroups:
      - ""
    resources:
      - nodes/proxy
      - nodes/stats
      - nodes/log
      - nodes/spec
      - nodes/metrics
    verbs:
      - "*"
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: system:kube-apiserver
  namespace: ""
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-apiserver-to-kubelet
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: kubernetes
...
EOF
  • kube-api-serverは--kubelet-client-certificateで指定した証明書で持って、kubernetesユーザとしてkubeletにアクセスしにいく
  • そのため作成したClusterRoleリソースをkubernetesユーザに紐づけるClusterRoleBindingリソースを作成している

API起動時に上記のオプションで指定した証明書(kubernetes.pem)のCNはkubernetesでした。
それをユーザ名としてリクエストが正当かどうか(resourceに対してverbを行っても良いか)Authorizeするための設定ということですね。
ここまできたところで、Controlプレーンのコンポーネントは全て起動するようになっているはずなので、いったんサービスを起動しヘルスチェックを行います。

$ docker-compose up --force-recreate -d ctl-{0..2}
$ docker-compose logs -f
# 'container Ready'が表示されるまで待ちます。<C-c>
$ docker exec -ti ctl-0 sh

# in ctl-0 container
$ kubectl get cs
NAME                 STATUS    MESSAGE             ERROR
controller-manager   Healthy   ok                  
scheduler            Healthy   ok                  
etcd-0               Healthy   {"health":"true"}   
etcd-2               Healthy   {"health":"true"}   
etcd-1               Healthy   {"health":"true"} 

$ kubectl get clusterRole | grep apiserver-to-
system:kube-apiserver-to-kubelet                                       2020-06-14T05:51:06Z
$ kubectl get clusterRoleBinding -o wide| grep apiserver-to-
system:kube-apiserver                                  10m   ClusterRole/system:kube-apiserver-to-kubelet                       kubernetes  
# 上記のような出力があれば正常に動作している
# 'get cs'の結果は帰ってくるけどclusterRole関連が帰ってこない場合はリソースの関係で起動が遅れているので、下記のコマンドで改めて登録し直す
$ bash /exec/RBAC_AUTH.sh
# 確認できたらホストマシンに戻ります。
$ exit

このセクションの最後にロードバランサーにルールを追加し、正常にバランシングが行われているか確かめています。
本稿ではここをnginx用の設定に置き換えています。

# in .
$ cat <<EOF | tee lib/balancer/lb.conf.stream
upstream k8s_ctl {
  server ctl-0:6443;
  server ctl-1:6443;
  server ctl-2:6443;
}
server {
  listen 443;
  proxy_pass k8s_ctl;
}
EOF

$ docker-compose up -d balancer
Creating balancer ... done

# dockerのホストマシンの10443番ポートに紐づけられていることを確認
$ docker port balancer
# 疎通確認
$ curl --cacert lib/certs/ca/ca.pem https://127.0.0.1:10443/version
{                                                                                                                                                                                                           
  "major": "1",
  "minor": "18",
  "gitVersion": "v1.18.1",
  "gitCommit": "7879fc12a63337efff607952a323df90cdc7a335",
  "gitTreeState": "clean",
  "buildDate": "2020-04-08T17:30:47Z",
  "goVersion": "go1.13.9",
  "compiler": "gc",
  "platform": "linux/amd64"
}

証明書類を指定しないアクセスはsystem:anonymousユーザと解釈されAuthorizationで弾かれますが、/versionは取得できるようです。
上記のような出力が帰ってくれば正常にバランシングできていることになります。

長かったですがこれでやっとcontrolプレーンが動くようになりました。
次のセクションでworkerノードを動かせばひとまずクラスタとしては動くようになりますのでもうひと頑張りです。

09-bootstrapping-kubernetes-workers

ここではWorkerノードを作成していきます。
最初に依存パッケージを入れていますが、これはDockerfileで入れているため特に操作は不要です。
次にswappingをオフにすることがデフォルトで求められます。
Dockerの場合はホストマシンのシステムファイルを共有するため、ホストマシン(か、--priviledgedしているのでworkerからでもいいですが)にて swapoffしないとデフォルト設定のkubeletの起動に失敗します。
が、本稿ではそれほど本格的に運用することを目的としているわけではなく、もしswappingがが起きてもそれほど深刻なことにはならないので、swapoffにせずにkubeletの設定の方を変えます。

まず、Workerノード用のバイナリをダウンロードし、展開します。
kubectlも再度ダウンロードしているのですが、workerノードからの使いどころがよくわからなかったので省略しています。

# in .
$ curl -L \
-O  https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.18.0/crictl-v1.18.0-linux-amd64.tar.gz \
-O  https://github.com/opencontainers/runc/releases/download/v1.0.0-rc8/runc.amd64 \
-O  https://github.com/containernetworking/plugins/releases/download/v0.8.2/cni-plugins-linux-amd64-v0.8.2.tgz \
-O  https://github.com/containerd/containerd/releases/download/v1.2.13/containerd-1.2.13.linux-amd64.tar.gz \
-O  https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kube-proxy \
-O  https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubelet

$ {
tar -xf containerd-1.2.13.linux-amd64.tar.gz -C bin/worker/
tar -xf cni-plugins-linux-amd64-v0.8.2.tgz -C bin/worker/opt/
tar -xf crictl-v1.18.0-linux-amd64.tar.gz -C bin/worker/bin/
mv runc.amd64 runc
chmod +x kube-proxy kubelet runc
mv kube-proxy kubelet runc bin/worker/bin
rm -f cni-plugins-linux-amd64-v0.8.2.tgz crictl-v1.18.0-linux-amd64.tar.gz containerd-1.2.13.linux-amd64.tar.gz
}

次にcniの設定を行います。
このチュートリアルではコンテナランタイムとしてcontainerdを直接用いていますが、こいつがpodを作る際にこちらのcniを用いてネットワークデバイスを割り当てます。
kubeletではなく自分の手でcniを使ってネットワークデバイスを作成するといった試みはネットワーク上探すと結構出てきますが、大元の使い方はこちらにありますので、やってみると余計kubeletのありがたみを感じるかもしれません(知らんけど)
設定にあたり、ipamhost-localでは、ここで定義したレンジからpodにアドレスを割り当てます。あくまでkubeletが管理しており、kube-api-serverは「こういうpod作ったよ」情報を保持するのみです。
つまり、ここで渡すCIDRが被ってしまうと同じIPを持つpodがたくさんできてしまいアクセスできなくなるので、workerノードごとにCIDRレンジを変更する必要があります。
そのように動的に設定するため、まずは各workerノードコンテナ内の/var/cniに一時的に設定ファイルを起き、containerd起動のスクリプト内で置換して本来の場所に移すように作ります。

# in .
$ cat <<EOF | sudo tee lib/worker/varcni/10-bridge.conf
{
    "cniVersion": "0.3.1",
    "name": "bridge",
    "type": "bridge",
    "bridge": "cnio0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "\${POD_CIDR}"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
    }
}
EOF

$ cat <<EOF | sudo tee lib/worker/varcni/99-loopback.conf
{
    "cniVersion": "0.3.1",
    "name": "lo",
    "type": "loopback"
}
EOF

次にcontainerdを起動するスクリプトを書いていきます。
containerdは外部から設定ファイルを読み込みます。
ファイルにて設定できる項目はcontainerd config defaultコマンドで確認できます。

$ cat << EOF | tee lib/worker/etccontainerd/config.toml
[plugins]
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runc"
      runtime_root = ""
EOF
bin/worker/exec/CONTAINERD_SERVICE.sh
#!/bin/bash
LOGFILE="/var/log/containerd.log" 
cp -p /var/cni/* /etc/cni/net.d/
POD_CIDR="10.200.${HOSTNAME#worker-}.0/24"
CNICONF='/etc/cni/net.d/10-bridge.conf'

sed -i 's|${POD_CIDR}|'${POD_CIDR}'|' ${CNICONF}
/usr/local/bin/containerd \
  --config /etc/containerd/config.toml >>${LOGFILE} 2>&1

次にkubeletの起動スクリプト
workerノード内のバイナリは基本設定ファイルを読み込ませる形で起動します。
まずは設定ファイルの中身です。ここでもノードごとに動的に変えたい部分が存在しますので、kubelet起動時に置き換えるようにしています。
本家ではここでもPODCIDRを指定していますが、これはstandaloneモードでのみ用いられるもののようです。今回はクラスタモードで起動するので特に指定する必要はありません。さらに言えばhost-localなcniを用いるのでMasterノードで行うべき設定すらも関係ありません。
それと、各ノードで実行されるpodへの通信を確保するためにルーティングテーブルを作成する必要があります。そのルーティングも動的に実行するように設定しています。
まずは設定ファイルです。

# in .
$ cat <<EOF |  tee lib/worker/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: "/var/lib/ca/ca.pem"
authorization:
  mode: Webhook
clusterDomain: "cluster.local"
clusterDNS:
  - "10.100.0.10"
# resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet_certs/\${HOSTNAME}.pem"
tlsPrivateKeyFile: "/var/lib/kubelet_certs/\${HOSTNAME}-key.pem"
failSwapOn: false
EOF

起動スクリプト

bin/worker/exec/KUBELET_SERVICE.sh
#!/bin/bash
CONFIGFILE="/var/lib/kubelet-config.yaml"                                                                                                                                                    
LOGFILE="/var/log/kubelet.log"

NodeNum=${HOSTNAME#worker-}

sed -e '
  s|${HOSTNAME}|'${HOSTNAME}'|g
' /var/lib/kubelet_config/kubelet-config.yaml > ${CONFIGFILE}

# add route to other node's pods
# this config required in "11-pod-network-routes" section
for ((i=0;i<3;i++));do
 [ $i -ne ${NodeNum} ] && ip r add to 10.200.$i.0/24 via 172.16.10.3$i 
done

/usr/local/bin/kubelet \
  --config=${CONFIGFILE} \
  --container-runtime=remote \
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
  --image-pull-progress-deadline=2m \
  --kubeconfig=/var/lib/kubelet_certs/${HOSTNAME}.kubeconfig \
  --network-plugin=cni \
  --register-node=true \
  --logtostderr=false \
  --log-file=${LOGFILE} \
  --v=2

最後にkube-proxyです。
こいつはノードごとのネットワーク設定を、主にiptables(可変だけども)を用いて操作する担当です。
なのでServiceリソースを作った時なんかに各ノードにてiptablesでみてみると、確率でトラフィックを振り分けているServiceの実態が見えたりします。
それと、dockerコンテナの方に--priviledgedをつけているにもかかわらずデフォルトで/proc/sys以下をReadOnlyモードでマウントするらしく、ここをReadWriteでリマウントしてあげないとこれもまたエラーになります。Workerノードは何もかも捧げているといった感じですね。
ここでもCIDRを指定していますが、ここでの意味合いは指定したCIDRレンジ以外へのトラフィックをマスカレードするという設定になるらしいです。

# in .
cat <<EOF | sudo tee lib/worker/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kube-proxy.kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"
conntrack:
  maxPerCore: 0
EOF
bin/worker/exec/KUBE-PROXY_SERVICE.sh
CONFIGFILE="/var/lib/kube-proxy_config/kube-proxy-config.yaml"                                                                                                                                              
LOGFILE="/var/log/kube-proxy.log" 

#re-mount the Required File System if that was as ReadOnly mode
RFS='/proc/sys'
[[ $(awk -v RFS=${RFS} '/^proc/ {if($2==RFS){split($4, mode, ",")}}END{print mode[1]}' /proc/mounts) == 'ro' ]] && \
mount -o remount,rw,bind ${RFS} ${RFS}

/usr/local/bin/kube-proxy \
  --config=${CONFIGFILE} \
  --logtostderr=false \
  --log-file=${LOGFILE} \
  --v=2

以上で k8sクラスタコンポーネントの起動設定は完了です。
Workerも起動し、無事ノードとして認識されるか確かめて次のセクションに移動します。

# in .
$ docker-compose up -d worker-{0..2}

# 'container Ready'が表示されるくらいまで待ちます
$ docker exec -it ctl-0 kubectl get no
NAME       STATUS     ROLES    AGE   VERSION
worker-0   NotReady   <none>   7s    v1.18.1
worker-1   NotReady   <none>   7s    v1.18.1
worker-2   NotReady   <none>   7s    v1.18.1

10-configuring-kubectl

このセクションでは、自分作成したクラスタに対して任意の場所からアクセスできるようにします。
そのためにはkubeconfigという設定ファイルに接続先と認証情報を書き込み、kubectlを打つときにこの設定ファイルを参照するようにしてあげます。
まず接続先はbalancer(つまりdockerホストのネットワークデバイス(172.16.10.1)の10443ポート)になります。

# in .
$ (
  cd cert_util/

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://172.16.10.1:10443

  kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem \
    --embed-certs=true


  kubectl config set-context kubernetes-the-hard-way \
    --cluster=kubernetes-the-hard-way \
    --user=admin

  kubectl config use-context kubernetes-the-hard-way
)

kubectlが用いるconfigファイルは
1. --kubeconfigで指定した値
2. $KUBECONFIGの値
3. ${HOME}/.kube/config

の順で参照されますので、特に何もいじっていないこの場合は${HOME}/.kube/configにここで設定した値が保存されています。

最後に疎通確認です。

# from docker host machine
$ kubectl get cs
NAME                 STATUS    MESSAGE             ERROR
scheduler            Healthy   ok                  
controller-manager   Healthy   ok                  
etcd-2               Healthy   {"health":"true"}   
etcd-0               Healthy   {"health":"true"}   
etcd-1               Healthy   {"health":"true"} 

11-pod-network-routes

ここではあるpodから別のworkerノードで動いているpodに対しても疎通ができるようにルーティング設定を行います。具体的にはpodからのトラフィックを、宛先podが動いているworkerノードのethデバイスにルーティングするようにします。
本来GWであるdockerホストにおいて、これように作成されるデバイス用に下記のように設定を行うべきです、が、実は本チュートリアルにおいて、この設定自体は08-bootstrapping-kubernetes-controllers及び09-bootstrapping-kubernetes-workers内のkubelet起動スクリプトの中に組み込んでいます ので、pod宛の通信は各ノードが直接ルーティングするようになっています。
これによりdockerコンテナの方をdownさせると自動的に設定自体も削除されるので、個人的にはよりクリーンかなと。
ということでこのセクションも割愛します。

一応解説だけしておくと、キモの部分は自分以外のpodへのトラフィックのみルーティングを行うという部分です。
今回workerノードとなっているdockerコンテナは、基本vethデバイスをdokcerホストマシン内に作成されるbridgeデバイスに差し込んで各種通信を行います。そのため各種通信は必ずホストマシンのbridgeデバイスを介して行われます。詳しくは別で記事にしています。
同様のことがworkerとpodの間でも行われます。つまりpodからの通信はworker内に自動で生成されるbridgeデバイスを介して行われるので、もしここで自分の中で動いているpodへのトラフィックを自分のethデバイスにむけてルーティングするとそこでループとなりトラフィックが消失します(最初手探りで構築していたので、途中でトラフィックが消えていたことをコンテナのバグかと思い込みすごくハマりました、、、)。

12-dns-addon

ここではpod及びserviceリソースに対する名前解決を行えるようにするaddonをデプロイします。
kubernetesで使用されるDNSは、現在ではkube-dnsというのがレガシーでcorednsがモダンのようです。
本家でも corednsを用いているので素直にその通りにします。
ただ当チュートリアルでは手作り感を出すためにserviceリソース用のCIDRを変更しており、本家で行っているデプロイ用のYAMLをそのまま用いることはできないので少しだけ変更を加えます。
まず、元となるYAMLをダウンロードします。

# in .
$ curl -O https://storage.googleapis.com/kubernetes-the-hard-way/coredns.yaml
# お好みのテキストエディタか下記コマンドで該当箇所修正します。
$ sed -i '/clusterIP/s/32/100/' coredns.yaml

$ kubectl apply -f coredns.yaml
serviceaccount/coredns created
clusterrole.rbac.authorization.k8s.io/system:coredns created
clusterrolebinding.rbac.authorization.k8s.io/system:coredns created
configmap/coredns created
deployment.apps/coredns created
service/kube-dns created

# ちゃんと作成され、DNSサービスが機能するか確かめます
$ kubectl get pods -l k8s-app=kube-dns -n kube-system
NAME                       READY   STATUS    RESTARTS   AGE
coredns-589fff4ffc-d8745   1/1     Running   0          21s
coredns-589fff4ffc-k6zwl   1/1     Running   0          22s

$ kubectl run busy --image=busybox --rm -ti -- sh

# もしここでPermissionDeniedになったらkubeapi-to-kubeletのrbacが正常に作られているかcontrolプレーンの中で確認してみてください。
# もしくはとりあえず下記コマンド打ってみてください
# docker exec ctl-0 bash /exec/RBAC_AUTH.sh
#
# APIサーバを起動したときにデフォルトで作成されている'kubernetes'という名前のサービスリソースを名前解決します。
# in pod
$ nslookup kubernetes
Server:         10.100.0.10
Address:        10.100.0.10:53

Name:   kubernetes.default.svc.cluster.local
Address: 10.100.0.1

$ exit

13-smoke-test

ここではkubernetesリソース関連の各種操作を行なっていきます。
ここに記載の範囲の操作は可能なことは確認できています。

secrets

ここでは06-data-encryption-keysで実施したsecretsリソースがちゃんと暗号化されていることを確認します。

# ctl-0にattach
$ docker exec -ti ctl-0 sh


$ kubectl create secret generic kubernetes-the-hard-way --from-literal="mykey=mydata"
$ etcdctl get \
  --cacert=/etc/etcd/ca.pem \
  --key=/etc/etcd/kubernetes-key.pem \
  --cert=/etc/etcd/kubernetes.pem \
  /registry/secrets/default/kubernetes-the-hard-way  | hexdump -C

00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|                                                                                                                              
00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|                                                                                                                              
00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
00000040  3a 76 31 3a 6b 65 79 31  3a 84 82 28 36 9a 28 ef  |:v1:key1:..(6.(.|
#---<省略>---

aescbcという文字列が、このアルゴリズムがそれ以降文字列の暗号化に使われたということを示すようです。
試しにそれ以外のリソースについて暗号化されていないのか確かめてみます。

# still in ctl-0
# 'default' namespaceに関連して保存されているリソースの表示
$ etcdctl get \
  --cacert=/etc/etcd/ca.pem \
  --key=/etc/etcd/kubernetes-key.pem \
  --cert=/etc/etcd/kubernetes.pem  "" --prefix --keys-only | grep default

# kubernetesサービスリソースの情報確認
$ etcdctl get \
  --cacert=/etc/etcd/ca.pem \
  --key=/etc/etcd/kubernetes-key.pem \
  --cert=/etc/etcd/kubernetes.pem  /registry/services/specs/default/kubernetes | hexdump -C

outputs

00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 72 76 69 63  |/registry/servic|
00000010  65 73 2f 73 70 65 63 73  2f 64 65 66 61 75 6c 74  |es/specs/default|
00000020  2f 6b 75 62 65 72 6e 65  74 65 73 0a 6b 38 73 00  |/kubernetes.k8s.|
00000030  0a 0d 0a 02 76 31 12 07  53 65 72 76 69 63 65 12  |....v1..Service.|
00000040  87 04 0a bb 03 0a 0a 6b  75 62 65 72 6e 65 74 65  |.......kubernete|
00000050  73 12 00 1a 07 64 65 66  61 75 6c 74 22 00 2a 24  |s....default".*$|
00000060  35 39 66 35 32 61 62 62  2d 34 61 39 61 2d 34 63  |59f52abb-4a9a-4c|
00000070  30 34 2d 39 64 35 61 2d  35 33 36 30 32 34 32 34  |04-9d5a-53602424|
00000080  36 63 63 38 32 00 38 00  42 08 08 82 e7 9f f7 05  |6cc82.8.B.......|
00000090  10 00 5a 16 0a 09 63 6f  6d 70 6f 6e 65 6e 74 12  |..Z...component.|
000000a0  09 61 70 69 73 65 72 76  65 72 5a 16 0a 08 70 72  |.apiserverZ...pr|
000000b0  6f 76 69 64 65 72 12 0a  6b 75 62 65 72 6e 65 74  |ovider..kubernet|
000000c0  65 73 7a 00 8a 01 b8 02  0a 0e 6b 75 62 65 2d 61  |esz.......kube-a|
000000d0  70 69 73 65 72 76 65 72  12 06 55 70 64 61 74 65  |piserver..Update|
000000e0  1a 02 76 31 22 08 08 82  e7 9f f7 05 10 00 32 08  |..v1".........2.|
000000f0  46 69 65 6c 64 73 56 31  3a 85 02 0a 82 02 7b 22  |FieldsV1:.....{"|
00000100  66 3a 6d 65 74 61 64 61  74 61 22 3a 7b 22 66 3a  |f:metadata":{"f:|
00000110  6c 61 62 65 6c 73 22 3a  7b 22 2e 22 3a 7b 7d 2c  |labels":{".":{},|
00000120  22 66 3a 63 6f 6d 70 6f  6e 65 6e 74 22 3a 7b 7d  |"f:component":{}|
00000130  2c 22 66 3a 70 72 6f 76  69 64 65 72 22 3a 7b 7d  |,"f:provider":{}|
00000140  7d 7d 2c 22 66 3a 73 70  65 63 22 3a 7b 22 66 3a  |}},"f:spec":{"f:|
00000150  63 6c 75 73 74 65 72 49  50 22 3a 7b 7d 2c 22 66  |clusterIP":{},"f|
00000160  3a 70 6f 72 74 73 22 3a  7b 22 2e 22 3a 7b 7d 2c  |:ports":{".":{},|
00000170  22 6b 3a 7b 5c 22 70 6f  72 74 5c 22 3a 34 34 33  |"k:{\"port\":443|
00000180  2c 5c 22 70 72 6f 74 6f  63 6f 6c 5c 22 3a 5c 22  |,\"protocol\":\"|
00000190  54 43 50 5c 22 7d 22 3a  7b 22 2e 22 3a 7b 7d 2c  |TCP\"}":{".":{},|
000001a0  22 66 3a 6e 61 6d 65 22  3a 7b 7d 2c 22 66 3a 70  |"f:name":{},"f:p|
000001b0  6f 72 74 22 3a 7b 7d 2c  22 66 3a 70 72 6f 74 6f  |ort":{},"f:proto|
000001c0  63 6f 6c 22 3a 7b 7d 2c  22 66 3a 74 61 72 67 65  |col":{},"f:targe|
000001d0  74 50 6f 72 74 22 3a 7b  7d 7d 7d 2c 22 66 3a 73  |tPort":{}}},"f:s|
000001e0  65 73 73 69 6f 6e 41 66  66 69 6e 69 74 79 22 3a  |essionAffinity":|
000001f0  7b 7d 2c 22 66 3a 74 79  70 65 22 3a 7b 7d 7d 7d  |{},"f:type":{}}}|
00000200  12 43 0a 1a 0a 05 68 74  74 70 73 12 03 54 43 50  |.C....https..TCP|
00000210  18 bb 03 22 07 08 00 10  ab 32 1a 00 28 00 1a 0a  |...".....2..(...|
00000220  31 30 2e 31 30 30 2e 30  2e 31 22 09 43 6c 75 73  |10.100.0.1".Clus|
00000230  74 65 72 49 50 3a 04 4e  6f 6e 65 42 00 52 00 5a  |terIP:.NoneB.R.Z|
00000240  00 60 00 68 00 1a 02 0a  00 1a 00 22 00 0a        |.`.h......."..|
0000024e
"

情報が平文で保存されていることが確認できます。

deployment

ここではアプリケーションを運用していく上で主に使われることになるdeploymentリソースの作成です。

$ kubectl create deployment nginx --image=nginx
$ kubectl get po -w
# コンテナがRunningになるまで待ちます。確認できたらCtl-Cで抜けます。

Port Forwarding

ここではkubectlを用いるホスト(ここではdockerホスト)に、podのポートへとforwardingを行う操作の確認を行います。
主にデバッグ目的で行われる操作になります。

# 上のdeploymentで表示したpodの名前をコピーします。
$ POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
$ kubectl port-forward $POD_NAME 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80


# in new terminal of docker host
$ curl --head 127.0.0.1:8080
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Mon, 15 Jun 2020 10:11:53 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 May 2020 15:00:20 GMT
Connection: keep-alive
ETag: "5ecd2f04-264"
Accept-Ranges: bytes

Logs

podのログを表示します。デバッグの基本となる、個人的には重要なコマンドです。
先ほどのアクセスログが表示されるはずです。

$ kubectl logs $POD_NAME
127.0.0.1 - - [15/Jun/2020:10:11:53 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.61.1" "-"

Exec

podの中でコマンドを実行します。
上記のログ同様、デバッグを行う上で結構用います。

kubectl exec -ti $POD_NAME -- nginx -v

Services

ここでは先ほど作成したpodに、その付加されたタグに該当すればトラフィックをフォワードするというサービスリソースを作成します。
kube-proxyを作成する段でも少し触れましたが、iptabesなどNetfilter操作系のコマンドで実装されているようです。
どのタグがどのpodについて、、とかを考えて作ると割と面倒なのですが、kubectlには一括でpodやdeploymentなどのリソースとサービスを紐づけるためのコマンドがあります。それがexposeになります。
ここでは完全に外部からアクセスするため、NodePort型のサービスリソースを作成しています。
ClusterIPではあくまでpodへの通信をforwardするため、サービスクラスタのネットワークが見える環境でなければいけませんが(つまり今回の環境においてはホスト上で10.100.0.0/16への通信をどこかのノードへルーティングする必要があります)、NodePortはpodがデプロイされるworkerノードのポートからpodにフォワードするため、何もしなくでもdockerホストからpodへと疎通できるようになります。

$ kubectl expose deployment nginx --port 80 --type NodePort
$ kubectl get svc 
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE                                                                                                                                   
kubernetes   ClusterIP   10.100.0.1      <none>        443/TCP        162m                                                                                                                                  
nginx        NodePort    10.100.211.41   <none>        80:31867/TCP   7s 

$ NODE_PORT=$(kubectl get svc nginx \
  --output=jsonpath='{range .spec.ports[0]}{.nodePort}')
# 上で表示したportの値となる

# どこか適当なworkerノードのポートに疎通確認
$ curl -I 172.16.10.30:${NODE_PORT}
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Mon, 15 Jun 2020 10:47:25 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 26 May 2020 15:00:20 GMT
Connection: keep-alive
ETag: "5ecd2f04-264"
Accept-Ranges: bytes

14-cleanup

ここまでに構築した環境を一掃します。
特にカスタマイズしてなければ下記コマンド一発です。

# in .
$ docker-compose down

ちなみにまたサービスをupすれば再度クリーンなkubernetesクラスタの実行環境が立ち上がります。

$ docker-compose up

おわりに

以上、docker環境上でhard-wayするための方法と、つまづいたところの共有になります。

実際にやってみようとなっても、ここで行うテストの範囲ならかなり低リソース(再喝しますがdockerホストはCPU: 1コア、RAM: 4GB、ディスク: 32GBです)でも十分動きました。全てフリーの範囲なのでお金の心配もありません。
意外とネットワーク周りの理解に手間取り結構時間かかってしまいましたが、コンテナ技術を理解するための入り口としてとてもいいチュートリアルでした。

hard-wayしたいけどパブリッククラウドに手を出すと後片付け忘れそうで不安、サインアップが面倒くさい、自前の環境で済ませたい、コンテナの制限を知りたいなど、自分と似た境遇の方の何かの助けになれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む