2021年1月29日金曜日

docker registry を LB 配下で動作させる場合は nginx を挟まなければならない

概要

例えば ELB や F5BIG-IP などのロードバランサを使って docker registry を動作させようとします
その場合にロードバランサ側で SSL アクセラレータを使って 443 -> 5000 などのバランシングをすると思います
その場合には間に nginx を挟んでヘッダの調整をしないとうまく動作しないことがあるようです

環境

  • Ubuntu 18.04
  • docker registry 2.7.2
  • nginx 1.14.0
  • docker 20.10.2

自分が遭遇した現象

docker push 時に止まり何度かリトライしたあとで 503 が返却されました
docker registry のログを見ても成功のログしか出ておらずおそらくロードバランサとの相性が悪いのだと思います

docker registry の起動

  • mkdir -p /mnt/registry
  • docker run -d -p 5000:5000 --restart=always --name registry -v /mnt/registry:/var/lib/registry registry:2

nginx の起動

  • apt -y install nginx
  • vim /etc/nginx/sites-enabled/default
server {
  listen *:80;
  server_name your.domain.com;
  server_tokens off;

  client_max_body_size 0;
  chunked_transfer_encoding on;

  location / {

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Ssl on;

    proxy_read_timeout 900;
    proxy_cache off;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_http_version 1.1;

    proxy_pass          http://localhost:5000;
  }
}

ロードバランサ側の設定

  • SSL アクセラレータを ON にする
  • 443 -> 80 にバランシングする
  • ロードバランサの IP などに対して A レコードなどを DNS に設定する

動作確認

docker login して問題なく push できるか確認してみましょう

最後に

proxy_set_header などの設定がないと registry 側が接続を中断しているのかなと思います

2021年1月28日木曜日

Ubuntu でクライアント証明書を発行する方法

概要

過去 に Mac で作成しました
今回は Ubuntu で作成してみたいと思います

環境

  • Ubuntu 18.04
  • openssl 1.1.1i

CA 用ディレクトリの作成

ここに鍵やクライアント証明書を作成していきます

  • sudo mkdir /CertificateAuthCA

ルートCAの作成

鍵を作成し鍵から証明書を作成します
CSR の情報は適当に入力します
今回の方法では鍵にパスフレーズの設定が必要になるので入力しましょう

  • sudo openssl genrsa -des3 -out ca.key 4096
  • sudo openssl req -new -x509 -days 3650 -key ca.key -out ca.crt

クライアント証明書の作成

こちらもまずは鍵を作成します
次に CSR を先に作成してその CSR と先ほど作成してルートCA の鍵と証明書を使ってクライアント証明書を作成します

  • sudo openssl genrsa -des3 -out client.key 2048
  • sudo openssl req -new -key client.key -out client.csr
  • sudo openssl x509 -req -days 365 -in client.csr -CA myca.crt -CAkey myca.key -set_serial 01 -out client.crt

nginx で動作確認するので pem ファイルも作成しておきます

  • sudo openssl x509 -in client.crt -outform PEM -out client.pem

Optional: サーバ証明書の作成

nginx で動作確認します
クライアント証明書は https が必須なので https で nginx が動作するためにサーバ証明書も作成しておきます

  • sudo apt -y install nginx
  • sudo mkdir /etc/nginx/certificates
  • sudo chown -R www-data:www-data /etc/nginx/certificates
  • cd /etc/nginx/certificates

サーバ証明書配置ディレクトリを作成してそこに作成します
鍵、CSR、証明書を作成します

  • sudo openssl genrsa -out domain.com.key 2048
  • sudo openssl req -new -sha256 -key domain.com.key -out domain.com.csr
  • sudo openssl x509 -in domain.com.csr -out domain.com.crt -req -signkey domain.com.key -days 3650

あとは nginx の設定をします

  • cd /etc/nginx/sites-available
  • sudo touch proxy.conf
  • sudo vim proxy.conf
ssl_client_certificate /CertificateAuthCA/ca.pem;
ssl_verify_client on;
  • sudo ln -s /etc/nginx/sites-available/proxy.conf /etc/nginx/sites-enabled/proxy.conf
  • sudo systemctl restart nginx
  • systemctl status nginx

動作確認

curl で動作確認します
必要になるのはクライアント証明書と鍵になります
また curl の場合は --key で指定する鍵情報に証明書の情報も含まれている必要があるので注意してください

  • cat client.crt client.key > client_curl_key.pem
  • curl -k --cert ./client.crt --key ./client_curl_key.pem https://localhost

ちなみに Ruby では以下のスクリプトで動作します

  • vim test.rb
require 'net/https'

https = Net::HTTP.new('localhost', 443)
https.use_ssl = true
https.cert = OpenSSL::X509::Certificate.new(File.read('./client.crt'))
https.key = OpenSSL::PKey::RSA.new(File.read('./client.key'), 'pass')
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
https.verify_depth = 5
https.start {
  response = https.get('/')
  puts response.body
}
  • ruby test.rb

最後に

中間CA がある場合にはまた作業や挙動が異なると思います
証明書はクライアントごとに発行できるます
もし複数のクライアントで証明書を作成した場合は nginx 側では ca.pem の後ろに結合していけば OK です

参考サイト

2021年1月27日水曜日

Prometheus の blackbox_exporter を使ってみた

概要

blackbox_exporter はインスタンスの死活監視を行うためのエクスポータです
今回はインストール方法と簡単な使い方について紹介します

環境

  • Ubuntu 18.04
  • docker 20.10.2
  • Prometheus 2.24.1

nginx 起動

動作確認用の nginx を起動します
192.168.100.11 という IP のインスタンスで起動しています

  • sudo systemctl start nginx

blackbox.yml の作成

使用するモジュールやルールを設定します
実際に監視を行うサイトなどの情報は prometheus.yml 側に記載します
今回は Ping チェックとターゲットのサイトから 200 番系のレスポンスが返却されるかのチェックをします
ただこちらにはターゲットのサイトなどは記載しません

  • vim blackbox.yml
modules:
  icmp:
    prober: icmp
    timeout: 5s
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: []
      method: GET
      preferred_ip_protocol: "ip4"

blackbox_exporter の起動

先程作成した blackbox.yml を使ってコンテナを起動します
blackbox_exporter は 192.168.100.10 で起動しています

  • docker run --rm -d -p 9115:9115 --name blackbox_exporter -vpwd:/config prom/blackbox-exporter:master --config.file=/config/blackbox.yml

prometheus.yml の作成

次に prometheus.yml を定義します
blackbox_exporter の場所と実際に死活監視するターゲットサイトの情報はこちらに記載します

  • vim prometheus.yml
global:
  scrape_interval:     15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'nginx_check'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - http://192.168.100.11 # nginx が起動しているインスタンス
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 192.168.100.10:9115 # blackbox_exporter が起動しているインスタンス

Prometheus の起動

prometheus.yml を使って起動します

  • docker run -d -p 9090:9090 -v $(pwd):/prometheus-data prom/prometheus --config.file=/prometheus-data/prometheus.yml

動作確認

まず blackbox_exporter が起動しているノードにアクセスするとチェック結果が確認できます
今回であれば http://192.168.100.10:9115/ にアクセスすると以下のような結果が確認できます

また Prometheus にアクセスすると probe から始まるメトリックが取得できているのが確認できます

nginx を停止して probe_success の値が 0 になることを確認しましょう

おまけ: Ping チェックをする場合の prometheus.yml

job_name を追加すれば OK です
あとは Prometheus コンテナを再起動しましょう
メトリックは probe_icmp_duration_seconds あたりが追加になっています
probe_successjob="icmp_check" も追加になっているのでそれを見ても OK です

global:
  scrape_interval:     15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'nginx_check'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - http://192.168.100.10
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 192.168.100.10:9115
  - job_name: 'icmp_check'
    metrics_path: /probe
    params:
      module: [icmp]
    static_configs:
      - targets:
        - 192.168.100.11
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 192.168.100.10:9115

最後に

blackbox_exporter を試してみました
ターゲットのサイトは prometheus.yml に書きますが実際にターゲットのサイトにアクセスしているのは blackbox_exporter のはずなのでネットワークのリーチャビリティなどには注意しましょう

参考サイト

2021年1月26日火曜日

docker-compose で nginx を使って proxy_pass をコンテナ名でアクセスする方法

概要

nginx の proxy_pass にコンテナ名を指定する方法と注意事項を紹介します
docker-compose で nginx と proxy_pass するアプリを定義します

環境

  • macOS 11.1
  • docker 20.10.2

ポイント

  • upstream は使わない
  • proxy_pass するアプリ側は 0.0.0.0 でバインドするようにする

Sinatra app

  • bundle init
  • vim Gemfile
gem "sinatra"
gem "thin"
  • bundle install
  • vim app.rb
require 'sinatra'

get '/' do
  'ok'
end

Dockerfile

  • vim Dockerfile
FROM ruby

ADD . /app
WORKDIR /app

RUN bundle install

CMD ["bundle", "exec", "ruby", "app.rb", "-o", "0.0.0.0"]

nginx default.conf

  • vim default.conf
server {
    listen 80;
    location / {
        proxy_pass http://app:4567/;
    }
}

Dockerfile-web

  • vim Dockerfile-web
FROM nginx

ADD ./default.conf /etc/nginx/conf.d/default.conf

docker-compose

  • vim docker-compose.yml
version: '3.8'
services:
  web:
    image: web
    ports:
      - 80:80
    depends_on:
      - app
  app:
    image: app:latest

ビルド

  • docker build -t app .
  • docker build -f Dockerfile-web -t web .

動作確認

  • docker-compose up -d
  • curl localhsot

2021年1月25日月曜日

APISpec の to_yaml で日本語が文字化けする場合の対処方法

概要

定義した spec 内で日本語を使っている場合は to_yaml を使わずに yaml.dump を使いましょう
to_yaml は内部的には yaml.dump を使っていますがオプションが使えないようになっています

環境

  • macOS 11.1
  • Python 3.8.7
    • apispec 4.0.0

文字化けするコード

with open(file_path, mode='w') as f:
    f.write(spec.to_yaml())

yaml.dump を使う

import yaml

with open(file_path, mode='w') as f:
    yaml.dump(spec.to_dict(), f, allow_unicode=True)


allow_unicode=True を忘れずに設定してください

おまけ: OrderedDict が入っている場合

to_dict した dict 内に OrderedDict が入っている場合以下の処理を追加しましょう

import yaml
from collections import OrderedDict

with open(file_path, mode='w') as f:
    represent_dict_order = lambda self, data:  self.represent_mapping("tag:yaml.org,2002:map", data.items())
    yaml.add_representer(OrderedDict, represent_dict_order)
    yaml.dump(spec.to_dict(), f, allow_unicode=True)

参考サイト

2021年1月24日日曜日

flask + apispec を使って OpenAPI3 の定義ファイルを自動生成する

概要

過去に flasgger を使って Swagger2.0 の定義ファイルを作成する方法を紹介しました
しかし flasgger では OpenAPI3 に完全に対応しておらず components.schame など自動で出力してくれません
今回は apispec を使って OpenAPI3 の定義ファイルを自動生成してみました

環境

  • macOS 11.1
  • Python 3.8.7
    • apispec 4.0.0
    • apispec-webframeworks 0.5.2
    • flask 1.1.2
    • marshmallow 3.10.0

インストール

  • pipenv install apispec apispec-webframeworks flask marshmallow

サンプルコード

  • vim app.py
import uuid

from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flask import Flask
from marshmallow import Schema, fields


spec = APISpec(
    title="Swagger Petstore",
    version="1.0.0",
    openapi_version="3.0.2",
    plugins=[FlaskPlugin(), MarshmallowPlugin()],
)

class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    categories = fields.List(fields.Nested(CategorySchema))
    name = fields.Str()


api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
spec.components.security_scheme("ApiKeyAuth", api_key_scheme)


app = Flask(__name__)


@app.route("/random")
def random_pet():
    """A cute furry animal endpoint.
    ---
    get:
      description: Get a random pet
      security:
        - ApiKeyAuth: []
      responses:
        200:
          description: Return a pet
          content:
            application/json:
              schema: PetSchema
    """
    pet_data = {
        "name": "sample_pet_" + str(uuid.uuid1()),
        "categories": [{"id": 1, "name": "sample_category"}],
    }
    return PetSchema().dump(pet_data)


with app.test_request_context():
    spec.path(view=random_pet)


if __name__ == "__main__":
    import json
    print(json.dumps(spec.to_dict(), indent=2))
    print(print(spec.to_yaml()))
    app.run(debug=True)
  • pipenv run python app.py

or

  • FLASK_APP=app.py pipenv run flask run

説明

基本は APISpec を作成してこれに必要な属性を追加していく感じです
今回は Flask + Marshmallow と連携するので plugins でそれぞれのプラグインを追加しています
components.schemas はデフォルトで表示してくれますが components.securitySchemes は表示してくれないので spec.components.security_scheme で追加しています

Flask のルーティングのコメント部分に各パスのパラメータやレスポンスの定義を直接記載します

MethodView を使う

MethodView を使うとルーティングをクラスとして管理することができます
こちらの方が管理しやすくなると思います

  • vim app.py
import uuid

from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec_webframeworks.flask import FlaskPlugin
from flask import Flask
from flask.views import MethodView
from marshmallow import Schema, fields


spec = APISpec(
    title="Swagger Petstore",
    version="1.0.0",
    openapi_version="3.0.2",
    plugins=[FlaskPlugin(), MarshmallowPlugin()],
)

class CategorySchema(Schema):
    id = fields.Int()
    name = fields.Str(required=True)


class PetSchema(Schema):
    categories = fields.List(fields.Nested(CategorySchema))
    name = fields.Str()


api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"}
spec.components.security_scheme("ApiKeyAuth", api_key_scheme)


app = Flask(__name__)


class RandomPet(MethodView):
    def get(self):
        """A cute furry animal endpoint.
        ---
        description: Get a random pet
        security:
        - ApiKeyAuth: []
        responses:
          200:
            description: Return a pet
            content:
              application/json:
                schema: PetSchema
        """
        pet_data = {
            "name": "sample_pet_" + str(uuid.uuid1()),
            "categories": [{"id": 1, "name": "sample_category"}],
            }
        return PetSchema().dump(pet_data)


random_pet_view = RandomPet.as_view("random")
app.add_url_rule(
    "/random",
    view_func=random_pet_view
)
with app.test_request_context():
    spec.path(view=random_pet_view)


if __name__ == "__main__":
    import json
    print(json.dumps(spec.to_dict(), indent=2))
    print(print(spec.to_yaml()))
    app.run(debug=True)
  • pipenv run python app.py

最後に

apispec と flask を組み合わせて OpenAPI3 のドキュメントを flask 内で定義し自動生成する方法を紹介しました
今回はすべて 1 つのファイルに記載しましたが MVC は分割して管理したほうが良いかなと思います
OpenAPI3 の定義ファイルも今回は標準出力に出しているだけなので、別のメインファイルを作成してそちらで spec の情報をファイルに出力するようにしても良いかなと思います

参考サイト

2021年1月23日土曜日

Gitlab で特定のブランチだけ CI を実行する方法

概要

only を使います
正規表現が使えるほか refs などと組み合わせるとブランチ名を直接指定することもできます

環境

  • Gitlab-ee 13.7.3

gitlab runner の準備

この記事を参考に構築してください

ベースの .gitlab-ci.yml

  • vim .gitlab-ci.yml
stages:
    - build
    - test

build_job:
  stage: build
  script:
    - echo "build"

test_job:
  stage: test
  script:
    - echo "test"

master ブランチだけ CI する

only.refs を使います

  • vim .gitlab-ci.yml
stages:
    - build
    - test

build_job:
  stage: build
  script:
    - echo "build"
  only:
    refs:
      - master

test_job:
  stage: test
  script:
    - echo "test"

feature/* ブランチだけ CI する

only と正規表現を組み合わせます

  • vim .gitlab-ci.yml
stages:
    - build
    - test

build_job:
  stage: build
  script:
    - echo "build"
  only:
    refs:
      - master

test_job:
  stage: test
  script:
    - echo "test"
  only:
    - /^feature\/.*$/

参考サイト