MySQL GTID レプリケーション素振り

従来のレプリケーションとの違いをざっくりと。

  • ノードに UUID が付与されて「UUID+連番」ですべてのトランザクションに一意なID(GTID)がつく
  • 各ノードが「適用済の GTID」を持っているので循環レプリケーションやマスター切り替えが容易にできる
  • 「適用済の GTID」はバイナリログに書かれているのでスレーブでもバイナリログ必須(log-slave-updates
    • 追記 2019-11-15 というのは 5.6 だけで 5.7 以降はマスターに昇格するつもりのないスレーブならバイナリログ無効でも OK のようです
  • GTID はトランザクション単位で付与されるのでトランザクションテーブル必須(MyISAM 不可)

GTID レプリケーションをセットアップ

docker-compose.yml

version: '3.4'

services:
  sv01:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.11
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=1

  sv02:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.12
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=2

  sv03:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.13
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=3

networks:
  mysql:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.88.0/24

up してコンテナの中へ。

docker-compose up
docker-compose exec sv01 bash
docker-compose exec sv02 bash
docker-compose exec sv03 bash

すべてのノードにレプリケーション用のユーザーを作る。--binlog-do-db=test してるのでこれはレプリケーションされません。

# [sv01/sv02/sv03]
mysql mysql -v <<'SQL'
CREATE USER 'repl'@'%' IDENTIFIED BY 'pass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SQL

レプリケーションを設定する。sv01 -> sv02 -> sv03 -> sv01 のような循環レプリケーションします。

# [sv01]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv02]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.11',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv03]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.12',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

適当なノードでテーブルを作ったり行を挿入したりすると、すべてのノードにレプリケーションされます。

# [sv01/sv02/sv03]
mysql test
/* [sv03] */
create table t (id int not null primary key, no int not null);

/* [sv01] */
insert into t values (1, 111);

/* [sv02] */
insert into t values (2, 222);

/* [sv01/sv02/sv03] */
select * from t;

slave-exec-mode=IDEMPOTENT

(GTID とは関係ないですが)slave-exec-mode=IDEMPOTENT の効果により、行ベースレプリケーションで行の競合がエラーにならなくなっています。

試しに行の挿入で競合を発生させてみます。

/* [sv01/sv02/sv03] */
stop slave;

/* [sv01] */
insert into t values (9, 901);

/* [sv02] */
insert into t values (9, 902);

/* [sv03] */
insert into t values (9, 903);

/* [sv01/sv02/sv03] */
start slave;
select * from t;
show slave status \G

普通ならSQLスレッドでエラーになってレプリケーションが止まるところですが slave-exec-mode=IDEMPOTENT の効果によってエラーになりません。

ちなみにノードごとに行の値は次のようになっていました。

# [sv01]
+----+-----+
| id | no  |
+----+-----+
|  9 | 902 |
+----+-----+

# [sv02]
+----+-----+
| id | no  |
+----+-----+
|  9 | 903 |
+----+-----+

# [sv03]
+----+-----+
| id | no  |
+----+-----+
|  9 | 901 |
+----+-----+

この結果から推測するに、競合した場合は後勝ちで上書きされるようです。

なので例えば2台で双方向レプリケーションしているときに両方に同じ主キーのレコードを書き込むと、ノード1にはノード2に書き込んだ内容が、ノード2にはノード1に書き込んだ内容がのこることになりますね、うーん?

マスターの切り替え

GTID ならマスターの切り替えも簡単です。試しに sv02 が死んだと仮定して sv03 のマスターを sv01 に変えてみます。

/* [sv02] */
stop slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.11';
start slave;

sv01 を更新すると sv03 にレプリケーションされます。

/* [sv01] */
insert into t values (4, 444);

/* [sv03] */
select * from t;
show slave status \G

従来のレプリケーションだとマスターが変わればログポジションも変わるので mysqldump で再同期が必要なケースです(MHA なんかはこの辺うまくやってた気がする)。

戻すのも簡単です。

/* [sv02] */
start slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.12';
start slave;

競合したときのリカバリ

slave-exec-mode=IDEMPOTENT の効果によって行ベースの競合はエラーになりませんが、DDL なら簡単に競合によってレプリケーションが停止します。

/* [sv01/sv02] */
stop slave;

/* [sv01] */
create table x (sv01 int not null primary key);

/* [sv02] */
create table x (sv02 int not null primary key);

/* [sv01/sv02] */
start slave;

/* [sv01/sv02/sv03] */
show slave status \G

sv01 や sv02 で次のようなエラーでレプリケーションが止まっています。

Error 'Table 'x' already exists' on query. Default database: 'test'. Query: 'create table x (sv02 int not null primary key)'

従来のレプリケーションなら SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1 でスキップするステートメントの数を指定するのですが、GTID レプリケーションではこれはできません。

GTID の場合はまず show slave statusRetrieved_Gtid_SetExecuted_Gtid_Set でエラーになっている GTID を特定します。例えば次のようにでした。

Retrieved_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-4,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3
Executed_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-3,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
  c75314e4-72d0-11e8-a322-0242c0a8580b:1-5

Retrieved_Gtid_Set にあって Executed_Gtid_Set に無いものがレプリケーションされていない GTID です。比較すると UUID c7399d3d-72d0-11e8-a858-0242c0a8580c について Retrieved_Gtid_Set1-4Executed_Gtid_Set1-3 なので、エラーになっているのは c7399d3d-72d0-11e8-a858-0242c0a8580c:4 だということがわかります。

このエラーの原因となっている GTID を次の手順で空のトランザクションで上書きします。

SET GTID_NEXT='c7399d3d-72d0-11e8-a858-0242c0a8580c:4';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';

この時点でスレーブを開始すればレプリケーションは復帰しますが、この手順で復帰させたときは同じ GTID のバイナリログがノードによって異なる内容になっているため、誤動作の元になるかもしれないので次のようにバイナリログを削除しておくのが良いらしいです。

/* バイナリログの一覧を表示して最後のログをメモる */
show master logs;

/* バイナリログをローテート */
FLUSH LOGS;

/* バイナリログの削除 */
PURGE BINARY LOGS TO 'mysql-bin.000003';

この後、スレーブを開始できます。

start slave;
show slave status \G

もちろん、テーブルがノードによって異なる定義になってしまっている問題は残ったままなので、その問題の解決はまた別に行う必要があります。

スレーブの追加や再同期

スレーブの追加や再同期は mysqldump で流し込んだ後に change master tostart slave するだけで良いです。

# [sv04]
mysqldump -h sv03 --all-databases --triggers --routines --events | mysql

# [sv04]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

GTID が有効なサーバからの mysqldump では GTID レプリケーションに必要な↓みたいなのがデフォで含まれています。

--
-- GTID state at the beginning of the backup
--

SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ 'c1ea378a-72d5-11e8-a345-0242c0a8580d:1,
c7399d3d-72d0-11e8-a858-0242c0a8580c:1-6,
c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
c75314e4-72d0-11e8-a322-0242c0a8580b:1-6';

「これらの GTID のバイナリログは削除されてるので無いです、でもかつてあったということは覚えていて」みたいなニュアンスかと思います。

その他のメモ

binlog-do-db とか replicate-do-db とか

binlog-do-dbbinlog-ignore-db はマスターのバイナリログの出力に作用するオプションなので、マスターでバイナリログへの出力の段階でフィルタされ、スレーブへ転送されるログやスレーブで適用されるログは、マスターでフィルタされたバイナリログのみになる。

一方で replication-do-dbreplicate-ignore-db はスレーブのSQLスレッドの実行に作用するオプションなので、マスターでのバイナリログの出力やスレーブへ転送されるバイナリログの量には影響せず、マスターから転送されたバイナリログをスレーブがどれだけ実行するかの制御にしかならない。

なので、マスター~スレーブの転送量の削減のために replication-do-db は使えない。その目的なら binlog-do-db を使わなければならない。

ただし binlog-do-db だとその設定をおこなったマスタからレプリケーションするすべてのスレーブに設定が影響する。replication-do-db ならスレーブごとにレプリケーションするDBを選択できるのでフレキシブル。

ステートメントベースレプリケーションと行ベースレプリケーション

binlog-do-db とか replicate-do-db が作用するデータベースについて、ステートメントベースなら USE で選択されているカレントのデータベースによって制御される。

例えば binlog-do-db=test の場合、カレントデータベースが test であるクエリのみがレプリケーションの対象となる。実際に作用されたデータベースではないことに注意。例えば下記のようになる。

/* カレントが test なのでレプリケーションされる */
use test
insert into hoge.t_user values (1);

/* カレントが hoge なのでレプリケーションされない */
use hoge
insert into test.t_user values (1);

一方で、行ベースでは、カレントのデータベースではなく実際に作用するデータベースで制御される。そのため↑の例だと逆になる。

なお binlog-format=row と設定したからといって必ず行ベースになるわけではない。そもそも行ベースではないもの、例えば DDL は行ベースにならない(しようがない)のでステートメントベースになる。

なので binlog-format=row かつ replicate-do-db=test しているとき次のようになる。

use test
/* 行ベースなのでレプリケーションされない */
insert into hoge.t_user values (1);
/* ステートメントベースなのでレプリケーションされる */
create table hoge.t (id int not nul primary key);

use hoge
/* 行ベースなのでレプリケーションされる */
insert into test.t_user values (1);
/* ステートメントベースなのでレプリケーションされない */
create table test.t (id int not nul primary key);

一方で create database drop database alter databasereplicate-do-db で指定の通りに動く。つまりカレントデータベースではなく作用されるデータベースでフィルタされる( https://dev.mysql.com/doc/refman/8.0/en/replication-rules-db-options.html の下の方の Important の説明)。

さいごに

GTID レプリケーションではいくつか制限があります。

が、(既存の非GTIDからのマイグレーションとかはともかく)新規にやる分には問題なさそうな気がするので、今後 MySQL でレプリケーションするなら GTID は常に有効で良いと思う。

AWS Code Deploy を雑に触った

AWS Code Deploy で EC2 インスタンスにコードを雑にデプロイしてみた。

IAM ロールの準備

Code Deploy がインスタンスとかを操作するために必要なロール(サービスロール)を作成します。

# ロールを作成
aws iam create-role --role-name CodeDeployServiceRole --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "codedeploy.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}'

# ロールに Code Deploy のためのポリシーをアタッチ
aws iam attach-role-policy --role-name CodeDeployServiceRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole

EC2 インスタンスに付与するインスタンスプロファイルを作成します。

# ロールを作成
aws iam create-role --role-name CodeDeployDemo-EC2-Instance-Profile --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}'

# ロールにインラインポリシーを設定
aws iam put-role-policy --role-name CodeDeployDemo-EC2-Instance-Profile \
    --policy-name CodeDeployDemo-EC2-Permissions --policy-document '{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}'

# インスタンスプロファイルを作成
aws iam create-instance-profile --instance-profile-name CodeDeployDemo-EC2-Instance-Profile

# インスタンスプロファイルにロールを追加
aws iam add-role-to-instance-profile --instance-profile-name CodeDeployDemo-EC2-Instance-Profile \
    --role-name CodeDeployDemo-EC2-Instance-Profile

Code Deploy の設定とインスタンスの作成

Code Deploy のアプリケーションを作成します。 これは後述のデプロイグループをまとめるだけのものっぽいです。

aws deploy create-application --application-name myapp

次にデプロイグループを作成します。どのインスタンスにどのようにデプロイするかの設定です。

他にもいろいろ指定できて、インプレースではなく Blue/Green にしたり、ロードバランサへのデタッチ/アタッチを自動化したりもできます。今回は何も指定していないので一番シンプルなインプレースでロードバランサ無しです。

--service-role-arn で指定しているのは↑で作成したサービスロールです。--ec2-tag-filters で対象となる EC2 インスタンスのタグを指定します。

aws deploy create-deployment-group \
    --application-name myapp \
    --deployment-group-name mydep \
    --service-role-arn arn:aws:iam::999999999999:role/CodeDeployServiceRole \
    --ec2-tag-filters Key=Name,Value=mydep,Type=KEY_AND_VALUE

インスタンスを作成します。↑で作成したインスタンスプロファイルを指定したり、↑で指定した通りにタグを付与したりします。ついでにユーザーデータで Code Deploy のエージェントをインストールしたり Apache をインストールしたりします。

aws ec2 run-instances \
  --image-id "ami-91c4d3ed" \
  --key-name "oreore" \
  --instance-type "t2.nano" \
  --block-device-mappings "DeviceName=/dev/sda1,Ebs={DeleteOnTermination=true,VolumeType=gp2}" \
  --associate-public-ip-address \
  --iam-instance-profile "Name=CodeDeployDemo-EC2-Instance-Profile" \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=mydep}]" \
  --user-data '#!/bin/bash
set -eu

yum -y install ruby
yum -y install wget
wget https://aws-codedeploy-ap-northeast-1.s3.amazonaws.com/latest/install
chmod +x ./install
./install auto

yum -y install httpd
systemctl start httpd
systemctl enable httpd
'

aws ec2 describe-instances --filters Name=instance-state-name,Values=pending \
  --query "Reservations[].Instances[].PublicIpAddress" \
  --output text
#=> 192.0.2.123

デプロイ

appspec.yml ファイルでインスタンス内にコードをどのようにデプロイするか指定します。 この例だと index.html をドキュメントルートにおいて、デプロイ後に after.sh を実行します。

version: 0.0
os: linux
files:
  - source: /index.html
    destination: /var/www/html/

hooks:
  AfterInstall:
    - location: /after.sh
      timeout: 300
      runas: root

index.htmlafter.sh も適当に作ってアーカイブにまとめて S3 にアップします。

tar czvf app.tar.gz appspec.yml index.html after.sh
aws s3 cp app.tar.gz s3://oreore-deploy/v1.tar.gz

デプロイを開始します。

aws deploy create-deployment --application-name myapp --deployment-group-name mydep \
    --s3-location bucket=oreore-deploy,bundleType=tgz,key=v1.tar.gz
#=> d-111111111

終わるのを待って、結果を確認します。

aws deploy wait deployment-successful --deployment-id d-111111111
aws deploy get-deployment --deployment-id d-111111111

ブラウザで見ると・・・ index.html の内容が見えます、デプロイ成功です。

適当にファイルを編集してもう一度デプロイすると・・・

tar czvf app.tar.gz appspec.yml index.html after.sh
aws s3 cp app.tar.gz s3://oreore-deploy/v2.tar.gz

aws deploy create-deployment --application-name myapp --deployment-group-name mydep \
    --s3-location bucket=oreore-deploy,bundleType=tgz,key=v2.tar.gz
#=> d-222222222

aws deploy wait deployment-successful --deployment-id d-222222222
aws deploy get-deployment --deployment-id d-222222222

ブラウザで見ると index.html への編集内容が反映されています。

さいごに

(試していないけど)Blue/Green デプロイがわりと簡単にできるのが良さそう。自前でやろうとするとわりと面倒だろうので。

ただ、インプレースでデプロイする分にはどうかな・・サーバでスクリプト1本走らせればいいだけなので、わざわざ Code Deploy を使うまでも無いような気もする。

例えば AWS Systems Manager の Run Command で次のようにしても似たようなことはできそう。

aws ssm send-command \
  --region ap-northeast-1 \
  --document-name AWS-RunShellScript \
  --targets Key=tag:Name,Values=mydep \
  --timeout-seconds 30 \
  --max-concurrency 1 \
  --max-errors 1 \
  --parameters '{
    "commands":[
        "aws s3 cp s3://oreore-deploy/v2.tar.gz /tmp",
        "sudo tar xvzf v2.tar.gz -C /var/www/html/",
        "rm -f v2.tar.gz"
    ],
    "workingDirectory":["/tmp"],
    "executionTimeout":["3600"]
  }'

ただ、Code Deploy ならどのバージョンがデプロイされているか分かるし、ロールバックもマネジメントコンソールから簡単にできたりと、メリットは多い。

AWS Systems Manager を雑に触った

かつて EC2 Systems Manager(SSM) と呼ばれていたものの機能拡張版。 いろいろ機能はあるもののざっと見た感じ、下記あたりは使えなくもないような気がしました。

  • Run Command
  • State Manager
  • Parameter Store

Run Command

SSM でいちばん有名なやつ、EC2 インスタンスにエージェントを入れておけばマネジメントコンソールや AWS CLI 経由でコマンドが実行できます。

aws ssm send-command \
  --region ap-northeast-1 \
  --document-name AWS-RunShellScript \
  --targets Key=tag:Env,Values=test \
  --timeout-seconds 600 \
  --max-concurrency 50 \
  --max-errors 0 \
  --parameters '{
    "commands":["yum -y install awscli"],
    "workingDirectory":["/tmp"],
    "executionTimeout":["3600"]
  }'

aws ssm list-commands --region ap-northeast-1 --max-items 1 | jq '.Commands[].CommandId' -r
#=> 63842a22-71a4-4018-bc80-eba3120ea7c3

aws ssm list-command-invocations --region ap-northeast-1 --command-id 63842a22-71a4-4018-bc80-eba3120ea7c3
aws ssm list-command-invocations --region ap-northeast-1 --details --command-id 63842a22-71a4-4018-bc80-eba3120ea7c3 |
  jq -r '.CommandInvocations[] | "### " + .InstanceId + "\n" + .CommandPlugins[].Output'
#=> ### i-07c712d6fbcb5a9e1
#=> Loaded plugins: fastestmirror
#=> Loading mirror speeds from cached hostfile
#=>  * base: ftp.iij.ad.jp
#=>  * epel: ftp.iij.ad.jp
#=>  * extras: ftp.iij.ad.jp
#=>  * updates: ftp.iij.ad.jp
#=> Package awscli-1.11.133-1.el7.noarch already installed and latest version
#=> Nothing to do

RunCommand を使ってシェルっぽくコマンドを実行するアイデアもあるようです。

State Manager

本来の用途は、インスタンスをあるべき状態に維持するために定期的な処理を行なう、ものらしいですが簡易 cron として使えそうです。

aws ssm create-association \
  --region ap-northeast-1 \
  --name AWS-RunShellScript \
  --association-name oreore-job \
  --targets Key=tag:Env,Values=test \
  --schedule-expression 'rate(30 minutes)' \
  --parameters '{
    "commands":["logger oreore-job"],
    "workingDirectory":["/tmp"],
    "executionTimeout":["60"]
  }'
sudo tail -n +1 -f /var/log/messages | grep oreore-job
#=> Mar  7 04:34:03 localhost logger: oreore-job
#=> Mar  7 04:34:04 localhost amazon-ssm-agent: "logger oreore-job"
#=> Mar  7 05:04:03 localhost amazon-ssm-agent: "logger oreore-job"
#=> Mar  7 05:04:03 localhost logger: oreore-job

Parameter Store

それなりにセキュアな KVS です。類似のプロダクトとしては HashiCorp Vault とかでしょうか。

aws ssm put-parameter \
  --region ap-northeast-1 \
  --type SecureString \
  --overwrite \
  --name /oreore/himitu \
  --value naisyo

type で SecureString を指定すると自動的に KMS の暗号化キーで暗号化されます。

aws ssm get-parameter \
  --region ap-northeast-1 \
  --name /oreore/himitu \
  --query Parameter.Value \
  --output text
#=> ABCDEF0123456789...

aws ssm get-parameter \
  --region ap-northeast-1 \
  --name /oreore/himitu \
  --with-decryption \
  --query Parameter.Value \
  --output text
#=> naisyo

単に aws ssm get-parameter で値を取り出して使えるだけではなく、Run Command でパラメータの値を参照できたりします。 ただし、その場合は SecureString は使えません。

aws ssm put-parameter \
  --region ap-northeast-1 \
  --type String \
  --overwrite \
  --name /oreore/koukai \
  --value yametokyayokatta

aws ssm send-command \
  --region ap-northeast-1 \
  --document-name AWS-RunShellScript \
  --targets Key=tag:Env,Values=test \
  --parameters '{"commands":["echo {{ssm:/oreore/koukai}}"]}'

aws ssm list-command-invocations --region ap-northeast-1 --details --max-items 1 |
  jq -r '.CommandInvocations[].CommandPlugins[].Output'
#=> yametokyayokatta

さいごに

その他の機能はよくわからない&使わ無さそうに思いました。

SSM エージェント自体は Amazon Linux なら最初から入っている(らしい)し、CentOS でも rpm からサッと入れられるので、とりあえず入れておくだけ入れておいてもいいかもしれません。

GitLab CI 8.17.2 でジョブが並列に実行されるときの cache の動き

8.17.2 ではデフォルトではジョブごとブランチごとに有効になっている。つまり・・・

  • 異なるジョブ同士では共有されない
  • 異なるブランチでは共有されない
  • 同じブランチの同じジョブでのみパイプラインに跨って共有される

9.0 からはデフォルトがブランチごとに変わっている(たぶん)。

.gitlab-ci.ymlcache:key を指定すればキャッシュの共有の範囲を変更できます。 例えば次のように固定値にすれば、すべてのジョブのすべてのブランチでパイプラインに跨って共有されます。

cache:
  key: xxx

のはずですが、試してみたところうまく共有されませんでした。

例えば次のようなジョブ設定だったときに、

  • build
    • build_01
    • build_02
  • test
    • test_1
    • test_2
  • deploy
    • deploy_1
    • deploy_2

build_01 -> test_1 -> deploy_1 でキャッシュが共有され、かつ、build_01 -> test_1 -> deploy_1 でキャッシュが共有されました。つまり、同じステージで並列実行されるプロセスの順序が一致するものだけが共有されています。

Gitlab CI のキャッシュは Runner のローカルで持っているので、共有されるはずのキャッシュでも Runner が異なれば共有されない(複数の AP サーバのローカルに保持されるキャッシュが共有されないイメージ)。

がしかし、上の実験では同じひとつのホストの Runner で実行しているので、すべて共有されることを期待していました。

どうやら 8.17.2 の gitlab-ci-multi-runner だと並列に実行される Runner の順序数ごとに異なる Runner と扱われて、キャッシュも別に記録されるようだった(並列数 4 なら 4 つの Runner が別々に存在するようなイメージ)。

試しに並列数 1 で試してみたところ、上記と同じ設定ですべてのジョブでキャッシュが共有されました。


最近の版だとどうなっているかは試していないので不明。同時に実行されるジョブ間で共有されないのはともかく、ステージが異なれば共有できても良さそうなものな気がするので、もしかしたら改善されているかもしれない(ChangeLog にそれっぽいものは見つからなかった)。

git で無視したいディレクトリの中に .gitignore で * するのダメ

例えばプロジェクト直下の ore-no-special/ ディレクトリを無視したい場合、リポジトリルートの .gitignore で下記のように記述すれば無視できますが、

/ore-no-special/

次のような .gitignoreore-no-special/ の中に入れておくだけでも無視できます。

*

つまり次のような構成です。

/path/to/project/
    ore-no-special/
        .gitignore # *

自分にだけ必要で他の誰にも必要の無いディレクトリの無視設定をリポジトリに含まれる .gitignore に書くのははばかられますが、この方法ならリポジトリに含まれるファイルは汚れません。

.git/info/exclude でも似たようなことはできますが、ディレクトリまるごと無視するのならそのディレクトリの中に .gitignore を作るほうが * だけで手早いので(echo \* > ore-no-special/.gitignore)多用していました。

.

.

.

と思っていたんですが、この方法で無視したディレクトリは git clean -d -f で消し飛びます。

-x を指定しなければ無視されたディレクトリは消えないと思っていたんですが、そもそもこの方法はディレクトリの中身を無視しているのであってディレクトリは無視できていないので git clean -d に対して無視されません。

ので、ore-no-special/ みたいなディレクトリを無視するときはやっぱり普通に .git/info/exclude に下記のようにするのが正解でした。

/ore-no-special/

なお、プロジェクトルートの .gitignore でも /ore-no-special/* と書いてしまうと git clean -d -f で中身ごと消し飛びます。普通しないけど。

MD や LVM 使っている CentOS 6 に Kickstart で CentOS 7 を上書き

例えば CentOS 6 で下記のようなディスク構成だった場合、

zerombr
bootloader --location=mbr --driveorder=vda,vdb
clearpart --all --initlabel

part raid.11 --ondrive=sda --asprimary --size 500
part raid.12 --ondrive=sdb --asprimary --size 500

part raid.21 --ondrive=sda --asprimary --size 1 --grow
part raid.22 --ondrive=sdb --asprimary --size 1 --grow

raid /boot --fstype=ext4 --device=md0 --level=1 raid.11 raid.12
raid pv.01 --fstype=ext4 --device=md1 --level=1 raid.21 raid.22

volgroup vg0 pv.01

logvol /     --vgname=vg0 --size=4096 --name=lv_centos6 --fstype=ext4
logvol swap  --vgname=vg0 --size=1024 --name=lv_swap
logvol /data --vgname=vg0 --size=1024 --name=lv_data --fstype=ext4

下記のように Kickstart すれば、既存のボリュームグループに CentOS 7 のルートボリュームを作成してセットアップできる。

bootloader --location=mbr --driveorder=vda,vdb
clearpart --none

# 作成済のパーティションを RAID で使う
part raid.11 --onpart=sda1
part raid.12 --onpart=sdb1

# /boot を RAID1 で作る
raid /boot --fstype=xfs --device=md0 --level=1 raid.11 raid.12

# 作成済のボリュームグループに論理ボリュームを作成
logvol /     --vgname=vg0 --size=4096 --name=lv_centos7 --fstype=xfs

# 作成済の論理ボリュームを使う
logvol swap  --vgname=vg0 --name=lv_swap --useexisting
logvol /data --vgname=vg0 --name=lv_data --useexisting --noformat

ただしこれだと /boot は上書きされるので CentOS 6 のルートボリュームは残っているものの CentOS 6 を起動することはできない。


1年ちょいぐらい前にこのようなことを行ったメモを発掘したのだけど、一体なんのためにこんなことしたのだろう・・/data を維持したまま CentOS 6 -> 7 にリプレースしたかったのだと思うけど。

Prometheus -> InfluxDB でダウンサンプリングしてみたメモ

半年くらい前に社内の勉強会っぽい何かで話すために書いていたけど結局やらずにお蔵入りしたメモ。

半年くらい前なのでわりと古いです


Prometheus は単体だとダウンサンプリングができないので長期のデータ保持には向いていません。デフォルトだと 15 日しか保持しないし、もっと長期に渡って保持するように設定すると凄まじい量のストレージを喰います。

他のリソース監視・可視化ツールであれば、5 分毎のデータを 1 日保持、30 分毎のデータを 1 週間保持、2 時間毎のデータを 1 ヶ月保持、1 日ごとのデータを 1 年間保持、のように段階的に粒度を荒くしたデータを長期に保持できるようになっていたりします。

Prometheus の場合、データの粒度と保持期間が異なる別の Prometheus を用意して Prometheus -> Prometheus とデータを流したり(Federation)、Prometheus から別の時系列データベースにデータを書き込んだりすることで(Remote write)、ダウンサンプリングっぽいことができます。

そこで、試しに社内のいくつかのサーバを監視している Prometheus のメトリクスを InfluxDB に流してダウンサンプリングしてみました。

remote_storage_adapter (remote_storage_bridge) のインストール

以前セットアップしたときは remote_storage_bridge というものが必要だったんですが、今は remote_storage_adapter という名前に変わってました。たぶん次のような感じでビルドできます(別にビルドに Docker を使う必要は無いですが)。

docker run --rm -v /usr/local/bin:/go/bin:rw golang \
  go get github.com/prometheus/prometheus/documentation/examples/remote_storage/remote_storage_adapter

remote_storage_bridge の起動やら Prometheus の設定とか InfluxDB のセットアップとかは別記事に簡単に書いています。

グラフ

1 時間毎のデータを 1 ヶ月保持したものです。

f:id:ngyuki:20171114204706p:plain

ダウンサンプリングの前後の比較です。上のグラフがダウンサンプリング前の 1 分毎のデータ、下のグラフがダウンサンプリング後の 1 時間毎のグラフです。ダウンサンプリングには平均値を使っています。メトリクスの種類によっては最大値とかのが良いかも?

f:id:ngyuki:20171114204639p:plain

さいごに

大抵の場合は Prometheus のデフォルトの 15 日で十分な気がするのですが、たまに長期の傾向が知りたいことあります(1 年間を通して見て、あーこの時期はあれがあったわー、とか)。

今は、短期グラフのために Prometheus を、長期グラフのために Cacti を併用していたりするのですが・・・

Prometheus -> InfluxDB でダウンサンプリングする場合、InfluxDB の保持期間ごとに Grafana で別々にグラフを作る必要があってちょっと面倒な感じもしました(Cacti・・というか RRDTool ならデータを取得する期間に応じて自動的に適切な粒度を拾ってきてくれる)。が、そもそも短期と長期でグラフをみるきっかけ違うし、そんなには困らないかな?

長期グラフを見ているときに特定の期間にドリルダウンしても短期グラフに変わらないのが不便かも・・

テンプレート変数でどうにかするものなのだろうか。