オリジナルのgit-syncでnginxの設定を動的に変更する

概要

運用要件の変更で、コンテナの中のnginx.confを開発チームが触りたいとなったので、構成変更を行った際の備忘録です。

今までは、kubernetesのkostomizeで、configMapGeneratorを利用したconfigの生成を行なっていた為、別の方法でnginx.configを管理するように変更する方法を考える必要がありました。

要件は以下のようになります。

1. nginx.configだけを管理するGitリポジトリを作成
2. サイドカーとしてGitリポジトリを一定間隔でpullするコンテナを起動する
3. 更新があった場合、nginxプロセスをHUPシグナルを送信する

最初は公式のgit-syncを使い検証を行なっていました。

このイメージで上手く行けば良かったのですが、残念ながら
3. 更新があった場合、nginxプロセスをHUPシグナルを送信する
を行う為には各種パッケージを追加する必要があり、
それなら勉強の為に自前で同じ動きをさせてみよう!となりました。

app.py

アプリケーションの動きは、公式のgit-syncに合わせて、
GIT_SYNC_REPO       ・・・ 対象リポジトリ
GIT_SYNC_BRANCH   ・・・ 対象ブランチ
GIT_SYNC_DEST        ・・・ 保存場所
を、環境変数から読み込むようにしました。

GIT_SYNC_DESTは、nginxコンテナからも読み込ませるので、emptyDirをvolumeとしてマウントしたPATHにします。

import os
import time
import git
import json
import subprocess
from datetime import datetime

def main():

  while True:
    dt = datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')
    log = {}

    if os.path.exists(os.getenv("GIT_SYNC_DEST")):
        repo = git.Repo(os.getenv("GIT_SYNC_DEST"))
        repo.git.checkout(os.getenv("GIT_SYNC_BRANCH"))

        git_result = repo.git.pull()
        log["timestamp"] = dt
        log["message"]   = f'git pull from {os.getenv("GIT_SYNC_DEST")}'
        print(json.dumps(log))

        change_flg = 0
        for log in git_result.splitlines():
            if 'Updating' in log:
                change_flg = 1

        if change_flg == 1:
            log = {}
            dt = datetime.strftime(datetime.now(), '%Y/%m/%d %H:%M:%S')
            cmd = 'pkill -HUP -f "nginx: master process"'
            subprocess.call(cmd, shell=True)
            log["timestamp"] = dt
            log["message"]   = 'exec HUP nginx master process'
            print(json.dumps(log))

    else:
        git_result = git.Repo.clone_from(
            os.getenv("GIT_SYNC_REPO"),
            os.getenv("GIT_SYNC_DEST")
        )
        log["timestamp"] = dt
        log["message"]   = f'git clone {os.getenv("GIT_SYNC_REPO")} to {os.getenv("GIT_SYNC_DEST")}'
        print(json.dumps(log))

    time.sleep(60)

if __name__=="__main__":
    main()

requirements

GitPython

Dockerfile

本来であればgitアクセスの際の利用するSSHの設定は、secretなどを使ってpodsに渡した方が良いですが、今回は割愛しています。

FROM ubuntu:latest

ENV TZ Asia/Tokyo
ENV DEBIAN_FRONTEND=noninteractive

USER root

RUN apt-get -y update && apt-get -y install tzdata procps python3 pip git\
    && apt-get clean && rm -rf /var/lib/apt/lists/*

ENV HOME=/tmp
WORKDIR /tmp

COPY .ssh /root/.ssh
COPY app /app
RUN pip3 install --upgrade pip
RUN pip3 install -r /app/requirements.txt

ENTRYPOINT [ "/bin/python3 /app/app.py" ]

deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx

spec:
  template:
    spec:
      # サイドカーコンテナからnginxのプロセスを操作出来るようにする
      shareProcessNamespace: true

      containers:
        # nginxコンテナ
        - name: nginx

          # 共有のからディレクトリをマウント
          volumeMounts:
          - name: git-sync-volume
            mountPath: /tmp/git

          env:
          - name: NGINX_CONF
            value: /tmp/git/nginx-config/conf.d

          # nginxの起動前にconf.dディレクトリを/etc/nginx/conf.dに
          # シンボリックリンクを貼ります。
          # sleep を入れているのは、git pullでファイルの同期を待つ為に入れています
          command: ["/bin/sh", "-c", "--"]
          args: ["sleep 5 && ln -sf ${NGINX_CONF} /etc/nginx/conf.d && nginx -g \"daemon off;\""]


        # git-sync コンテナ
        - name: git-sync
          image: git-sync:latest
          imagePullPolicy: Always
          volumeMounts:
          - name: git-sync-volume
            mountPath: /tmp/git

          # 環境変数でリポジトリの情報を渡します
          env:
          - name: GIT_SYNC_REPO
            value: "git@github.com:xxx/nginx-config.git"
          - name: GIT_SYNC_BRANCH
            value: master
          - name: GIT_SYNC_DEST
            value: "/tmp/git/nginx-config"

          # git-syncコンテナから、nginxプロセスのPIDを取得する際に必要
          securityContext:
            capabilities:
              add:
              - SYS_PTRACE

      restartPolicy: Always

      volumes:
        - name: git-sync-volume
          emptyDir: {}

nginx-configリポジトリがmasterブランチが更新された場合、
git-syncコンテナが1分間隔でgit pullを行い
nginxコンテナの親プロセスにHUPを送信します。

これであれば、HUPの前にconfigtestを行い失敗したらSlackで通知を飛ばすとかも容易ですね。

Adventカレンダー 社内Docker環境の話

会社でやっているAdventカレンダー 3日目の記事です。

ここ1年で蓄積したDockerのナレッジを書きます。

弊社では、ローカルの開発環境でDockerを利用しています。

決して安定している開発環境とは言えず、日々ブラッシュアップしている状況ですが、培ったナレッジを書き残します。

dockerネットワークの話

コンテナは起動する事に、排他的な独自のネットワークを作ります。
単純にコンテナを起動するだけであればあまり気にする事はありませんが、IPアドレスによる制限を行っている場合は起動の度にコンテナのIPアドレスが変わる為、ネットワーク設計を考える必要があります。

弊社の環境ではL7のPATHルーティングを必要としますので、以下のような構成になります。

127.0.0.1:80
 ↓
Nginxロードバランサコンテナ
 ↓     ↓
コンテナA  コンテナB

上記の構成の場合、
① NginxロードバランサーコンテナのIPアドレスを固定する。
② X-Forwardedに127.0.0.1を含む場合アクセスを許可する。

の2つの許可方法が考えられますが、①で行うように設定しようと考えました。

docker-composeでコンテナを起動する場合、単一のコンテナだけではなく全てのコンテナにIPアドレスを固定指定する必要がありますが、
上記のようにコンテナが3つだけであれば良いのですが、実際は20ぐらいのコンテナが動いています。

同一ネットワークでNginxロードバランサコンテナだけIPアドレスを固定してみましたが、DHCPレンジとぶつかるケースがコンテナ側の起動が出来ない事がありました。
その為、ネットワークの設定、DHCPプールの設定を行う必要があります。

docker network create --subnet=172.19.0.0/16 --gateway=172.19.0.1 --ip-range=172.19.1.0/24 ネットワーク名

・Dockerネットワーク全体のネットワークは172.19.0.0/16
・ゲートウェイは172.19.0.1
・DHCPレンジは172.19.1.0/24の範囲で設定する。

Dockerネットワーク作成後、172.19.1.0/24 以外を固定IPアドレス設定可能ネットワークとして、通常通りコンテナにIPアドレスを固定しても、
DHCPレンジとのバッティングも発生しなくなりました。

    networks: 
      ネットワーク名:
        ipv4_address: 172.19.0.5

node ビルドの話

nodeのbuild(npm run dev等)がとにかく遅い。
ソースコードをそのままvolumesとしてマウントを行いビルドを行うと、node_modules配下が作られるが、
node_modules配下を”別物として”マウントする事で、ビルドの速度改善が見られた。

services:
  app:
    build:
      context: ./app
    volumes:
      - app/src:/var/www/DocmentRoot
      - node_modules_volume:/var/www/DocmentRoot/node_modules

volumes:
  node_modules_volume:

快適に開発が行える環境までもう一歩。
現在は社内からクレームが多い 「自分の業務とは関係ないコンテナを立ち上げたくない(重い)」の対応を進めているので、
動き始めたらその事も記事に書く予定です。

GitHub Actionsの使い方

Github謹製のCD/CIツールとしてGitHub Actionsを使ってみましたのでメモを残します。

今回テストしたGithubは、
①githubのソースコードを含めたコンテナイメージ化
②コンテナイメージをGCR(Google Container Registry)にアップロード
③GCRよりGKE上のkubenetesにデプロイ
④部分的にチャットワークにメッセージ送信
の4つです。

GCPのcredential.jsonをGithubのSercretに登録する

GCPを触るので、事前に取得した認証ファイルを使うことになりますが、リポジトリ内に認証ファイルを上げずに、githubのsercretに登録します。
jsonファイルの中身をそのまま貼り付けるのではなく、base64エンコード形式で貼る必要があります。

bast64 xxxxx.json #認証ファイル

GithubActionファイルの作成

Actionメニューより、NewWorkFlowを選択します。

以前の情報を見ると、初期の頃はビジュアライズなインターフェースで作成が出来たようですが、今はYAML形式での記載になります。
今回は、Docker imageのテンプレートを選びます。

リポジトリの/.github/workflows/dockerimage.ymlとしてテンプレートが挿入されるので、以下のようにカスタマイズします。

name: Docker Image CI

# CI/CDのトリガーになる動作を指定
# 以下はmasterブランチが変更されたら...
on:
  push:
    branches:
      - master

jobs:
  build:
    # CIで使うベースイメージ?
    runs-on: ubuntu-latest

    steps:
    # ベースイメージにリポジトリをcloneする 
    - uses: actions/checkout@v1

    # チャットワークにgit logの内容を送信する
    - name: Send cahtwork from gitlog
      env:
        ROOM_ID: xxxxxxxx
        API_TOKEN: xxxxxxxxxxxxxxx
        REPO_NAME: 'test repo'
      run: |
        git log --numstat -m -1 --date=iso --pretty='[%ad] %h %an : %s' > gitlog.txt
        curl -s -X POST -H "X-ChatWorkToken: ${API_TOKEN}" -d "body=[info][title]${REPO_NAME}[/title]`cat gitlog.txt`[/info]" "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages"

    # Docker buildを実行
    - name: Build the Docker image
      run: docker build -f Dockerfile . -t {タグ}  #リポジトリ直下にDockerfileを置いています

    # イメージにタグを付ける
    - name: Tagging Docker
      run: docker tag {タグ} gcr.io/{プロジェクトID}/{タグ}:latest

    # 
    - name: gcloud auth
      uses: actions/gcloud/auth@master
      env:
        GCLOUD_AUTH: ${{ secrets.GCR_KEY }}

    - name: Upload to GCS
      env:
        PROJECT_ID: skyticket-devel-161401
        GCLOUD_AUTH: ${{ secrets.GCR_KEY }}
        GOOGLE_APPLICATION_CREDENTIALS: ./service-account.json
      run: |
        export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
        echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
        curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
        sudo apt-get update
        sudo apt-get install google-cloud-sdk
        echo ${GCLOUD_AUTH} | base64 --decode > ${GOOGLE_APPLICATION_CREDENTIALS}
        cat $GOOGLE_APPLICATION_CREDENTIALS | docker login -u _json_key --password-stdin https://gcr.io
        gcloud auth activate-service-account --key-file $GOOGLE_APPLICATION_CREDENTIALS
        gcloud config set project ${PROJECT_ID}
        gcloud docker -- push gcr.io/${PROJECT_ID}/sky_scrayping/sky_scrayping:latest

    # GKEにデプロイ
    - name: Deploy Kubenetes
      env:
        cluster_name: test-cluster
        cluster_ip: xx.xx.xx.xx
      run: |
        kubectl config set-cluster ${cluster_name} --server=http://${cluster_ip}
        kubectl config set-context ${cluster_name}
        gcloud container clusters get-credentials ${cluster_name} --zone=us-west1-a

        # kubenetest のマニフェストはmanifest/dev.ymlに置いており、
        # deployを強制する為にマニフェストの一部を置換しています。
        sed -i -e "s/annotations_reloaded-at/`date  +%s`/g" manifest/dev.yml
        kubectl apply -f manifest/dev.yml

    # チャットワークにデプロイ完了の通知する
    - name: Send cahtwork from Deploy
      env:
        ROOM_ID: xxxxxxxx
        API_TOKEN: xxxxxxxxxxxxxxx
        REPO_NAME: 'test repo'
        BODY: '開発環境のデプロイが完了しました'
      run: |
        curl -s -X POST -H "X-ChatWorkToken: ${API_TOKEN}" -d "body=[info][title]${REPO_NAME}[/title]`echo -e ${BODY}`[/info]" "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages"

    # Actionが失敗した時はチャットワークにデプロイ失敗の通知する
    - name: error notification
      if: failure()   # これ重要
      env:
        ROOM_ID: xxxxxxxx
        API_TOKEN: xxxxxxxxxxxxxxx
        REPO_NAME: 'test repo'
        BODY: '開発環境のデプロイが失敗しました\n詳細はgithubActionのステータスを参照してください'
      run: |
        curl -s -X POST -H "X-ChatWorkToken: ${API_TOKEN}" -d "body=[info][title]${REPO_NAME}[/title]`echo -e ${BODY}`[/info]" "https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages"

一度定義したenvは他の場所でも参照出来ると思います。

dkronを触ってみた

crondで動くジョブスケジューラーがSPOFになっているサービスは多いと思う。
自社サービスも例に漏れずに、そうなっているので良さげなジョブスケジューラーが無いのか探していたところ、dkron(https://dkron.io/)というOSSがシンプルで良さげだったので、触ってみた。

まだ発展途上なのか、GUIの表示で部分部分がおかしい箇所もあるが、crondよりは十分と思える動きです。

バックグラウンドサービス

昨日作ったetcdを使います。
etcdのクラスタ設定でハマった事まとめ

インストール

cat << _EOF > /etc/yum.repos.d/dkron.repo
[dkron]
name=Dkron Pro Private Repo
baseurl=https://yum.fury.io/victorcoder/
enabled=1
gpgcheck=0
_EOF

yum -y install dkron

設定

サーバー3台でクラスタを作成する為、それぞれ設定します。

vim /etc/dkron/dkron.yml

# Dkron example configuration file
backend: etcd
advertise-addr: 192.168.33.10   # bindするアドレス
backend-machine: 127.0.0.1:2379

# 3台ともserver: trueとすると、1台だけはStatusがleavingの状態のままだった為、
# node01 , node02 はserver:true、 node03はfalseとします。
server: true
log-level: info
tags:
  role: batch
#   datacenter: east
# keyspace: dkron
# encrypt: a-valid-key-generated-with-dkron-keygen
join:
  - 192.168.33.10
  - 192.168.33.20
  - 192.168.33.30
# webhook-url: https://hooks.slack.com/services/XXXXXX/XXXXXXX/XXXXXXXXXXXXXXXXXXXX
# webhook-payload: "payload={\"text\": \"{{.Report}}\", \"channel\": \"#foo\"}"
# webhook-headers: Content-Type:application/x-www-form-urlencoded
# mail-host: email-smtp.eu-west-1.amazonaws.com
# mail-port: 25
# mail-username": mailuser
# mail-password": mailpassword
# mail-from": cron@example.com
# mail-subject-prefix: [Dkron]

設定後にdkronを起動します。

systemctl enable dkron
systemctl start dkron

VirtualBoxのポートフォワードの設定後、http://localhost:8080にアクセスすると、dkronのダッシュボードが表示されます。

ジョブは以下のようにjsonで登録します。

{
  "name": "echo-hostname",

  # timezone指定
  "timezone": "Asia/Tokyo",

  # crondと近い書き方。違うのは、[ 秒 分 時 日 月 曜日] と、秒が加わります
  "schedule": "0 * * * * *",
  "owner": "root",
  "owner_email": "", 

  # 多分、単発実行のjobの場合にtrueにするのかな
  "disabled": false, 

  # batch:xは、batchロールの中で何台のノードでジョブを実行するか
  # batch:2であれば、3台中2台のノードで実行する。
  "tags": {
    "role": "batch:2"  
  },
  "retries": 0,
  "parent_job": "",
  "processors": null,

  # 単一ノードで、ジョブの重複実行を許可するなら[allow]、許可しないなら[forbid]
  "concurrency": "forbid",
  "executor": "shell",
  "executor_config": {
    "command": "echo `date +\"%Y/%m/%d %H:%M:%S\"` `hostname`  >> /tmp/host.log",
    "shell": "true"
  },
  "status": "success"
}

確認したこと

フェイルーバーは、特に何も設定しなくても生きているノードで実行されますが、server: true としているノードが存在していないと全体が停止しますので、本番環境であればserver: trueを2台は必須と思われます。

ec2のsquidでグローバルIPを分散するProxyを作る

これはAdventure Advent Calendar 2018の13日目の記事です。(後追い)

プロキシサーバーと言えばsquidと思い浮かべるように、どメジャーなミドルウェアですが、今回はsquidを使って「複数のグローバルIPでインターネットにアクセスするプロキシサーバー」を作ってみました。

複数人でプロキシサーバーを使ったアクセスを行うと、すぐに接続元IPアドレスによるアクセス拒否が行われてしまうサイトにアクセスする際にどうぞ。

ec2を立てる

インスタンスのサイズによって、インターフェイスあたりの IPv4 アドレスの最大数が異なりますので以下を参考にして下さい。

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html

今回はt2.largeにし、
eth0の18080ポートで、外部からの通信を受け入れ、
eth1に9個のセカンダリプライベートIPをアサインして、それぞれにElasticIPを付け、計10個のIPアドレスからインターネットに出るようにしたいと思います。

インスタンスを作る上で注意する点は、最初からeth1を作成する事ぐらいです。
(後からでも追加出来ますが、インスタンスを停止してeth1をアタッチし、また起動するという手間があります)

なお、今回はAmazon Linux 2 AMI (HVM), SSD Volume Typeで作成しています。

インスタンスが起動したら、メニューからIPアドレスの管理を選択します。

eth1側に必要な数分、プライベートIPを割り当てます。
ここでは、DHCPでアドレスが割り当たりませんので、手動で入力していく必要があります。
(当たり前ですが、eth1が所属するサブネットの範囲で割り当てて下さい)

なお、eth1のeni-で始まる管理番号は、次で利用しますので控えておいて下さい。

忘れないように、セキュリティグループの穴あけもやっておきましょう。

ElasticIPを発行する

ElasticIPを発行します。
ここで注意する点は、
eth1のセカンダリIPアドレスに割り当てるElasticIPは、ネットワークインターフェイスでのみ検索、表示出来る。
という点です。
ラジオボタンがインスタンスのままだと表示されません。

複数のグローバルIPアドレス分、この割り当て作業を繰り返します。

squidを立てる

最後にsquidの設定です

sudo yum -y install squid

以下は/etc/squid/squid.confの抜粋です

 # proxyを使う接続元IP
acl mynetwork src xxx.xxx.xxx.xxx/32

 # 今回10個のIPをランダムで使う
acl balance random 1/10
balance_on_multiple_ip on

# LISTENポート
http_port 18080

# 帯域を潰されたくないので、特定の動画サイトのドメインは見せないようにする
acl blacklist dstdomain "/etc/squid/blacklist"
# 非読化
visible_hostname unkown
forwarded_for off
request_header_access X-FORWARDED-FOR deny all
request_header_access Via deny all
request_header_access Cache-Control deny all

# ファイルディスクリプタの上限変更
max_filedesc 65535

# ElasticIPに紐付けたセカンダリIP
tcp_outgoing_address 172.30.2.211 balance 
tcp_outgoing_address 172.30.2.212 balance 
tcp_outgoing_address 172.30.2.213 balance 
tcp_outgoing_address 172.30.2.214 balance 
tcp_outgoing_address 172.30.2.215 balance 
tcp_outgoing_address 172.30.2.216 balance 
tcp_outgoing_address 172.30.2.217 balance 
tcp_outgoing_address 172.30.2.218 balance 
tcp_outgoing_address 172.30.2.219 balance 
tcp_outgoing_address 172.30.2.220 balance 

/etc/squid/blacklistには主要な動画サイトのドメインを書いておきます

www.youtube.com
www.nicovideo.jp
gyao.yahoo.co.jp
www.happyon.jp
www.netflix.com

squidの自動起動と開始

sudo systemctl enable squid
sudo systemctl start squid

エラーが無く起動が出来れば、eth0に割り当てたElasticIPの18080をブラウザのプロキシに設定し、IP確認くんなどで通信確認とeth1のセカンダリIPアドレスに割り当てたElasticIPで通信が行えているか確認をしてみて下さい。

terraform の 変数ファイルの読み込み方法

以前は、variables.tfに

variable "project" {
  default = {
    id          = "zuu-infra"
    name        = "zuu-infra"
    region      = "asia-northeast1"
    description = "create from terraform"
  }
}

variable "domain" {
  default = {
    step       = "common-step.zuuonline.com"
    zabbix-web = "zbx.zuuonline.com"
    relay      = "relay.zuuonline.com"
    redash     = "redash.zuuonline.com"
    zabbix     = "zbx.zuuonline.com"
    grafana    = "grafana.zuuonline.com"
    vuls       = "vuls.zuudev.com"
  }
}

と書いて読み込ませていた為、実行する対象が変わる度にvariables.tfファイルを差し替えしていた。

実ファイルを動かすのは宜しくないので、調べたところ

-vars-file=xxxx.tfvars

実行時とオプションで、ファイルを読み込ませられるようだ。

調べて、色々勘違いしていた事もあり勉強になったので書いておく。

ファイルは以下の3つ

vpc.tf
  └ 実際のリソース定義

variables.tf
  └ -vars-file=c.tfvars で渡された内容を「変数」として定義する。tfファイルと同じ場所に置く

c.tfvars
  └ -vars-file=c.tfvars で渡す。 場所はどこでも良い(tfファイルからの相対パスか絶対パス)

variables.tfは、「変数」として定義するだけなので、

variable "credential" {
    type = "map"
    // c.tfvarsで情報が設定されていない場合のデフォルト値
    default = {
        "auth_file"   = "~/.ssh/dev-project.json",
        "project"     = "dev-project",
        "zones"       = "us-east-1a",
        "description" = "create from terraform"
    }
}
// VPC
variable "vpc" {
    type = "map"
}

このような感じで定義する。

c.tfvars は、variables.tf の typeに合うように変数を書く。
書き方は、以下のどちらでも認識した。

// procject
credential = {
    "auth_file" = "~/.ssh/hoge.json",
    "project" = "stg-project",
    "zones"   = "us-east-1a",
    "description" = "create from terraform"
}

// vpc
vpc = {
    vpc_name = "stg-project-network"
    network  = "172.24.0.0/22"
    autoscale_network_cidr = "172.168.0.0/24"
    autoscale_network_name    = "subnet1"
    autoscale_network_region  = "us-east1"
    none_autoscale_network_cidr = "172.168.2.0/24"
    none_autoscale_name    = "subnet2"
    none_autoscale_region  = "us-east1"
}

最後に、a.tf
これはリソースの定義なので、以前から書いている内容から変更は無いが、念の為に残しておく。

resource "google_compute_network" "vpc" {
  name         = "${lookup(var.vpc, "vpc_name")}"
  description  = "${lookup(var.credential, "description")}"
  auto_create_subnetworks = "false"
}

resource "google_compute_subnetwork" "subnet1" {
  depends_on    = ["google_compute_network.vpc"]
  name          = "${lookup(var.vpc, "autoscale_network_name")}"
  network       = "${lookup(var.vpc, "vpc_name")}"
  ip_cidr_range = "${lookup(var.vpc, "autoscale_network_cidr")}"
  region        = "${lookup(var.vpc, "autoscale_network_region")}"
  private_ip_google_access = true
}

resource "google_compute_subnetwork" "subnet2" {
  depends_on    = ["google_compute_network.vpc"]
  name          = "${lookup(var.vpc, "none_autoscale_network_name")}"
  network       = "${lookup(var.vpc, "vpc_name")}"
  ip_cidr_range = "${lookup(var.vpc, "none_autoscale_network_cidr")}"
  region        = "${lookup(var.vpc, "none_autoscale_network_region")}"
  private_ip_google_access = true
}

GCPのstartupスクリプトでどハマりしたメモ

VM インスタンスの特別な構成これ通りにNATゲートウェイを構築しようとして、ハマった箇所のメモ

実行したコマンドは以下の通り
開発環境なので、一部は手動でコマンドを置き換えています

# startup.sh取得
gsutil cp gs://nat-gw-template/startup.sh .

# インスタンステンプレート作成(nat-1)
gcloud compute instance-templates create nat-1 \
    --image-family=centos-7  --tags natgw \
    --image-project=centos-cloud \
    --machine-type n1-standard-1 --can-ip-forward \
    --metadata-from-file=startup-script=startup.sh --address グローバルIPアドレス1

# インスタンステンプレート作成(nat-2)
gcloud compute instance-templates create nat-2 \
    --image-family=centos-7 --tags natgw \
    --image-project=centos-cloud \
    --machine-type n1-standard-1 --can-ip-forward \
    --metadata-from-file=startup-script=startup.sh --address グローバルIPアドレス2

# ヘルスチェック作成
gcloud compute health-checks create http nat-health-check --check-interval 2 \
    --timeout 1 \
    --healthy-threshold 1 --unhealthy-threshold 2 --request-path /health-check

# インスタンスグループ作成(nat-1)
gcloud compute instance-groups managed create nat-1 \
    --size=1 --template=nat-1 --zone=asia-northeast1-a

# インスタンスグループ作成(nat-2)
gcloud compute instance-groups managed create nat-2 \
    --size=1 --template=nat-2 --zone=asia-northeast1-a

# ルート作成(nat-1)
gcloud compute routes create nat-1 --destination-range 0.0.0.0/0 \
    --tags noip --priority 800 --next-hop-instance-zone asia-northeast1-a \
    --next-hop-instance nat-1-5h46 \
    --network=default

# ルート作成(nat-2)
gcloud compute routes create nat-2 --destination-range 0.0.0.0/0 \
    --tags noip --priority 800 --next-hop-instance-zone asia-northeast1-a \
    --next-hop-instance nat-2-r3kn \
    --network=default

これで、ネットワークタグ「noip」を付けたインスタンスのルートがnat-1 / nat-2 を通って出て行くはず

 while :; do sleep 1 ;date; curl httpbin.org/ip --connect-timeout 1 ; done

2018年 11月 12日 月曜日 18:50:03 JST
curl: (28) Connection timed out after 1005 milliseconds
2018年 11月 12日 月曜日 18:50:05 JST
curl: (28) Connection timed out after 1005 milliseconds
2018年 11月 12日 月曜日 18:50:07 JST
curl: (28) Connection timed out after 1005 milliseconds

ダメじゃん

内容を確認すると、startup.shに記載されている

echo 1 > /proc/sys/net/ipv4/ip_forward

が実行されていない。

何これ?何で?と思いつつ数時間調べたけど、結局原因わからない。

理由は分からないけど、startup.shに

#!/bin/bash
timedatectl set-timezone Asia/Tokyo (追加)
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

したらecho 1 > /proc/sys/net/ipv4/ip_forwardも動くようになった。
改行コードか?

時間を無駄にした

grafanaでワールドマップを利用する


grafanaでworld map プラグインを使ってみたので備忘録

マッピングデータは、MaxMaindさんが提供してくれているものを使います。
https://dev.maxmind.com/ja/geolite2/

ElasticSearchを使う方法が一般的みたいですが、ざっくり国別データを集計したいだけなので、MySQLで行きます。
また、アクセスログもInfluxDBではなくMySQLです。

【環境】
python3.4
Mysql 5.7
maxminddb 1.4.1

まずはモジュールインストールと、マッピングデータの取得

pip3 install maxminddb
wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz -P /usr/local/src

MySQLにテーブルを作る

CREATE TABLE `geo2` (
  `ip_address` varchar(15) DEFAULT NULL,
  `country_name` varchar(50) DEFAULT NULL,
  UNIQUE KEY `ip_address` (`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

pythonスクリプトを作る。
vim /usr/local/bin/insert_get2.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import MySQLdb
import os
import maxminddb
import datetime

DBFILE = '/usr/local/src/GeoLite2-City.mmdb'
geo2db = maxminddb.open_database(DBFILE)

# 直近1時間前のログからデータ集計を行う
todaydetail = datetime.datetime.today()
starttime = (todaydetail-datetime.timedelta(hours=1)).strftime("%Y-%m-%d %H:00")
endtime   = todaydetail.strftime("%Y-%m-%d %H:00")

def geo2(ips):
  val_list = []
  for ip in ips:
    try:
      location = geo2db.get(ip)
      # 国
      country = location["registered_country"]["iso_code"]

      # 経度と緯度も取得出来るけど今回は使わない
      # 緯度
      # latitude = location['location']['latitude']
      # 経度
      # longitude = location['location']['longitude']

      val_list.append([ip,country])
    except:
      pass

  return val_list

def db_select():
  con = MySQLdb.connect(
    host='localhost',
    db='log',
    user='DBユーザー',
    passwd='パスワード',
    charset='utf8'
  )

  cur = con.cursor()
  sql = "select distinct remote_ip from log where time between '{start}' and '{end}' and remote_ip != '::1'".format(start=starttime,end=endtime)

  result = cur.execute(sql)
  ips = []
  for ip in cur:
    ips.append(ip[0])

  cur.close()
  con.close()
  return ips


def db_bulk_insert(rows):
  con = MySQLdb.connect(
    host='localhost',
    db='log',
    user='DBユーザー',
    passwd='パスワード',
    charset='utf8'
  )

  cur = con.cursor()
  sql = "INSERT IGNORE geo2(ip_address,country_name) VALUES(%s, %s)"
  cur.executemany(sql,rows)
  con.commit()
  cur.close()
  con.close()

ips  = db_select()
rows = geo2(ips)
db_bulk_insert(rows)

あとは、/usr/local/bin/insert_get2.pyを1時間毎に実行する。

GCEインスタンスでpostgresqlをフェイルオーバーさせる

GCEでは、単一インスタンスに同一ネットワークのIPアドレスを複数持たせる事ができない。
複数のネットワーク インターフェースの概要と例

つまり、Virtual IPを同一セグメントに持たせた構成は出来ない。

クラスタを組みたかったけど、今回の要件としては

1. Active/Standbyの構成にする
2. Activeのpostgresが止まった場合、Standbyを昇格する
3. フェイルバックは行わない。
4. フェイルオーバー後、App側で検知。
   必要な設定変更後、動的にサービスの再起動を行う。
5. 1分以内にサービスが復旧される。

とシンプルな構成にする。

PostgreSQL

要件1、2には、pg_keeperを使う。
クラスタでは無いのでスプリットブレインの検知などは出来ない。
その為、アプリケーション側に今のActive機の情報を教える必要がある。

スプリットブレイン対策用のDB、Table作成

postgres=#  create database pg_state;
postgres=#  create table failover_log (unixtime int, host varchar(10));

インストール(Actice/Standby共に)

cd /usr/local/src

git clone https://github.com/MasahikoSawada/pg_keeper.git

export PATH=/usr/pgsql-9.6/bin/:$PATH

make USE_PGXS=1

make USE_PGXS=1 install

postgresql.conf書き換え(Actice/Standby共に)

vim postgresql.conf

shared_preload_libraries = 'pg_keeper'
pg_keeper.my_conninfo = 'host=10.0.0.10 port=5432 dbname=postgres'
pg_keeper.partner_conninfo = 'host=10.0.0.11 port=5432 dbname=postgres'
pg_keeper.keepalive_time = 2
pg_keeper.keepalive_count = 3
pg_keeper.after_command = 'sleep 1 ; psql -d pg_state -c "insert into failover_log values(`date +%s`, \'`hostname`\');" -x'

Activeが止まった場合、pg_keeper.keepalive_time秒 × pg_keeper.keepalive_count回 チェックを行い、全てNGの場合にフェイルオーバーを実行し、最後にpg_keeper.after_commandの内容が実行される。
今回は、フェイルオーバー後に[unixtimestamp, hostname]を pg_state.failover_logに入れている。

app側

これはアプリケーションのよるので参考まで。
monitor_master_db.pyというモニタリングスクリプトを作成し、root権限で動かす事にした。
動きとしては、Active/Standby両機のDBのpg_state.failover_logをチェックし、タイムスタンプが若い方をDB接続先として、設定ファイル(yaml)を書き換えデーモンの再起動を行う。

#!/bin/env python3

import os,sys
import yaml
import psycopg2
import codecs
import subprocess

yaml_file = '/PATH/TO/env.yaml'
dbs = ['postgresql://postgres@db01:5432/pg_state'
         ,'postgresql://postgres@db02:5432/pg_state']

def get_item():
    arr = []
    for db in dbs :
        try:
            dbcon = psycopg2.connect(db)
            cur = dbcon.cursor()
            cur.execute('select * from failover_log order by unixtime desc limit 1')
            result = cur.fetchone()
            cur.close()
            dbcon.close()
            arr.append(result)
        except :
            pass
    if len(dbs) == len(arr):    # Active/Standby共にデータ取得成功
        if arr[0][0] > arr[1][0]:
            return arr[0][1]
        else :
            return arr[1][1]

    else :                             # 片系が停止している
        return arr[0][1]


def overwrite(db_name):
    with codecs.open(yaml_file, 'r', 'utf-8') as read :
        env_dict = yaml.load(read)

        if env_dict['db_master'][0]['address'] != '{}:5432'.format(db_name) or env_dict['db_slave'][0]['address'] != '{}:5432'.format(db_name):
            env_dict['db_master'][0]['address'] = '{}:5432'.format(db_name)
            env_dict['db_slave'][0]['address'] = '{}:5432'.format(db_name)

            with codecs.open(yaml_file, 'w', 'utf-8') as write :
                yaml.dump(env_dict, write, encoding='utf8', allow_unicode=True, default_flow_style=False)

            try:
                subprocess.check_call(["systemctl", "restart", "デーモン"])
            except :
                pass

作成したmonitor_master_db.pyをcronで動かす。
cronは普通に書くと1分が最小の実行単位だが、以下のように書くと5秒単位でスクリプトを実行してくれる。

# 5秒間隔
* * * * * for i in `seq 1 12`;do sleep 5; python3 /usr/local/bin/monitor_master_db.py; done

# 10秒間隔の場合
* * * * * for i in `seq 1 6`;do sleep 10; python3 /usr/local/bin/monitor_master_db.py; done

この状態で、Active側のDBを落として、フェイルオーバーされApp側の接続先も変更される事を確認する。
Slave側が昇格前にfailover_logへのinsertが実行される場合、pg_keeper.after_commandのsleepを大きくする。

pg_keeper.after_command = 'sleep 5 ; psql -d pg_state -c "insert into failover_log values(`date +%s`, \'`hostname`\');" -x'

ドメイン移管のエラー対応

他サービスで取得したドメインを、お名前.comに移管する際、一筋縄ではいかない。

まず拒否理由の内容

審査による拒否
詳細は下記内容をご確認ください。
拒否理由:[Whois情報に正確な情報の記載がございませんため、弊社審査において不受理とさせていただきました。  現ドメイン管理会社にて、Whois情報を正確な情報へご変更のうえ、再度お手続きをお願いいたします。]

正しい情報を住所をローマ字読みで入力しているのに、なんで!?

と、なる事が多いのでまとめておく。
先に言うと、『日本語の解釈と違う内容を求められる』事が最大の原因だと思う。

例として、東京都港区の区役所の住所「〒105-8511 東京都港区芝公園1丁目5番25号」を展開してみる。

項目 日本語 英語 説明
国(Country) 日本 JP ここは迷わない
郵便番号(ZIP/Postal code) 105-8511 105-8511 ハイフンあり、なしはどちらでもいい
都道府県(state) 東京都 Tokyo ここも迷わない
市区町村(city) 港区 Minato-ku 「市区町村」なので、「Minato-ku Shibakouen」と描きたいが、ここは英語でcityの場所の為、「Minato-ku」まで
番地(street) 芝公園1丁目5番25号 1-5-25, Shibakouen 番地って「1-5-25」だけじゃないの?と思うが、書き方は「番地 ,(カンマ) 町名」になる

他に電話番号やメールアドレスも登録する必要があるが、
それは迷わないと思うから省略。

これを適切に理解していれば、一発でドメイン移管が行える!