t.marcusの外部記憶装置

忘備録とかちょっとした考えとかをつらつらと...

やりかけメモ

func injectEnv(c AppConfig) AppConfig {
	injectEnv2(&c)
	return c
}

func injectEnv2(c interface{}) {
	fmt.Println(">>")
	tmpC := c
	if reflect.TypeOf(c).Kind() == reflect.Ptr {
		tmpC = reflect.Indirect(reflect.ValueOf(c)).Interface()
	}
	t := reflect.TypeOf(tmpC)
	v := reflect.ValueOf(tmpC)
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fv := v.Field(i)

		fmt.Println("===")

		fmt.Printf("Name: %s\n", f.Name)
		fmt.Printf("Type: %s(%s)\n", f.Type, f.Type.Kind())

		switch f.Type.Kind() {
		case reflect.Struct:
			injectEnv2(fv.Interface())
			break
		default:
			break
		}
	}
	fmt.Println("<<")
}

gRPC over SSL/TLS with ClientCertification

gRPCを利用したサービスを作ることになったので、その際に調べた認証・認可周りのメモ

キーワード:grpc, grpcs, client認証

gRPCにおける認証方法

gRPCの認証は公式サイトによると以下の方式がビルトインされてるらしい

また、認証情報は以下の方式で設定できる

  • Channel credential
    • SSL証明書などのように gRPC Channel に紐付ける方式
  • Call credential
    • Request時にClientContextに対して設定する方式

今回はgRPCServiceをグローバルに公開する予定があったのと通信を暗号化したかったので、クライアントに自己署名証明書(通称、オレオレ証明書)を払い出して、その証明書のCommonNameで認可を行う SSL/TLS + Channel credential の方式で試してみた。

[利用方法のイメージ]
               +------- Internet ---------+               
+---------+    |                          |
| Client  |  -------(gRPC over SSL/TLS)---------> [gRPCService]
+---------+    |    ( with ClientCert )   | :443
               +--------------------------+

準備

オレオレ認証局

クライアントに払い出す証明書を生成する必要があるので、オレオレ認証局オレオレ証明書の発行はこちらのQiita記事を参考にキーの出力フォーマットをなどを調整したものをGithubに上げた

https://github.com/tmarcus87/grpc-training-self-signed-ca

クライアントコード

Serverが1台の場合

以下のような感じでSSLで待ち受けるサーバを実装して起動すると動くはず

Serverが複数台の場合

公式のドキュメントによるとLoadBalancingは以下の3種の方法が提案されている

  • Proxy方式
  • 負荷分散に対応したClientを利用する方式 GoとJavaだとそういう対応クライアントがあるらしい(未検証)
  • 外部のLoadBalcningServiceを利用する方式
    • ちゃんと読んでないけど、EtcdとかZooKeeperとかEurekaとか使ってDNS応答を変化させてやる方式?

Proxy以外の方式では1IP:1Serverである必要があるっぽい?

Proxy方式でnginxを利用する場合以下の選択肢がある

  • L4 load balancing
    • TCPセッション単位のバランシング
    • TCPセッション単位のバランシングになるので、クライアント数が少ないと負荷分散が難しそう
    • SSL終端とクライアント認証はgRPCServerで行う
  • L7 load balancing
    • gRPCリクエスト単位のバランシングになる(設定次第?)
    • SSL終端とクライアント認証はnginxで行う
    • nginxのconfigでクライアント証明書の情報をアプリ側にmetadata(http header)として流す
    • nginx <--> gRPCサーバ間も暗号化しようと思えば可能ぽっい(あくまでnginx<--> grpcServer間の暗号化でclient<-->nginx間をのデータをそのままバイパスできないっぽい)

// todo Plain接続の場合のアプリ側のコードを書く

// todo L4 Load balancingの場合のnginx.confを貼り付ける

L7 Load balancingの場合のnginx.confは以下の通り

[nginxの設定]
$ cat /etc/nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log debug;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    upstream grpc_servers {
        server grpc_server1:5000;
        server grpc_server2:5000;
    }

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''        close;
    }

    server {
        listen 443 ssl http2;

        ssl_certificate /opt/server/dev-gateway-api.example.com.crt;
        ssl_certificate_key /opt/server/dev-gateway-api.example.com.pem;

        ssl_client_certificate /opt/ca/ca.crt;
        ssl_verify_client on;

        proxy_set_header ssl-client-cert        $ssl_client_escaped_cert;
        proxy_set_header ssl-client-verify      $ssl_client_verify;
        proxy_set_header ssl-client-subject-dn  $ssl_client_s_dn;
        proxy_set_header ssl-client-issuer-dn   $ssl_client_i_dn;
        grpc_set_header  ssl-client-cert        $ssl_client_escaped_cert;
        grpc_set_header  ssl-client-verify      $ssl_client_verify;
        grpc_set_header  ssl-client-subject-dn  $ssl_client_s_dn;
        grpc_set_header  ssl-client-issuer-dn   $ssl_client_i_dn;

        location /error502grpc {
            internal;
            default_type application/grpc;
            add_header grpc-status 14;
            add_header grpc-message "unavailable";
            return 204;
        }

        location / {
            grpc_pass grpc://grpc_servers;
            
            error_page 502 = /error502grpc;
        }
    }
}

サーバ側で以下のデータがmetadataから取れる

こんな感じの関数をUnaryServerInterceptorに差し込めば
func getMetaFromHeader(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    if md, ok := metadata.FromIncomingContext(ctx); ok {
        json, _ := json.Marshal(md)
        fmt.Println(string(json))
    }
    return handler(ctx, req)
}

こんなデータが取れるはず

{
  ":authority": [
    "grpc_servers"
  ],
  "content-type": [
    "application/grpc"
  ],
  "ssl-client-cert": [
    "-----BEGIN%20CERTIFICATE-----なんやかんや証明書の中身%0A-----END%20CERTIFICATE-----%0A"
  ],
  "ssl-client-issuer-dn": [
    "emailAddress=grpc@example.com,CN=gateway-gateway.example.com,OU=DevelopmentHQ,O=Hoge\\,Inc.,L=Sumida-ku,ST=Tokyo,C=JP"
  ],
  "ssl-client-subject-dn": [
    "emailAddress=service1@example.com,CN=service1,OU=DevelopmentHQ,O=Hoge\\,Inc.,L=Sumida-ku,ST=Tokyo,C=JP"
  ],
  "ssl-client-verify": [
    "SUCCESS"
  ],
  "user-agent": [
    "grpc-go/1.15.0"
  ]
}

MySQL8を入れて、memcached pluginを使うための忘備録

$ yum install -y https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
$ yum install -y mysql-community-server mysql-community-client
$ systemctl start mysqld
$ grep "password" /var/log/mysqld.log
$ mysql -uroot -p


rootで入ったらパスワードリセット

mysql> set password = 'P4$$w0rd';

mysql 5.7.6でpassword()でラップしなくて良くなったので



設定投入

mysql> create database innodb_memcache;
mysql> use innodb_memcache;


-- キャッシュポリシーの設定

mysql> CREATE TABLE IF NOT EXISTS `cache_policies` (
    `policy_name` VARCHAR(40) PRIMARY KEY,
    `get_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled') NOT NULL ,
    `set_policy` ENUM('innodb_only', 'cache_only','caching','disabled') NOT NULL ,
    `delete_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled') NOT NULL,
    `flush_policy` ENUM('innodb_only', 'cache_only', 'caching','disabled') NOT NULL
  ) ENGINE = innodb;

mysql> INSERT INTO cache_policies
VALUES("cache_policy", "innodb_only", "innodb_only", "innodb_only", "innodb_only");


-- テーブル/カラムとのマッピングを追加

mysql> CREATE  TABLE IF NOT EXISTS `containers` (
    `name` varchar(50) not null primary key,
    `db_schema` VARCHAR(250) NOT NULL,
    `db_table` VARCHAR(250) NOT NULL,
    `key_columns` VARCHAR(250) NOT NULL,
    `value_columns` VARCHAR(250),
    `flags` VARCHAR(250) NOT NULL DEFAULT "0",
    `cas_column` VARCHAR(250),
    `expire_time_column` VARCHAR(250),
    `unique_idx_name_on_key` VARCHAR(250) NOT NULL
  ) ENGINE = InnoDB;

mysql> INSERT INTO containers
  VALUES ("aaa", "test", "demo_test", "key", "value",  "flags", "cas", "expire", "PRIMARY");


-- 設定を追加

mysql> CREATE  TABLE IF NOT EXISTS `config_options` (
    `name` varchar(50) not null primary key,
    `value` varchar(50)
  ) ENGINE = InnoDB;

mysql> INSERT INTO config_options VALUES("separator", "|");
mysql> INSERT INTO config_options VALUES("table_map_delimiter", ".");

テストデータの投入

mysql> create database test;
mysql> use test;

mysql> CREATE TABLE demo_test (
    `key` VARCHAR(32) not null primary key,
    `value` VARCHAR(1024),
    `flags` INT,
    `cas` BIGINT UNSIGNED,
    `expire` INT
  ) ENGINE = InnoDB;

mysql> INSERT INTO demo_test VALUES ("AA", "HELLO, HELLO", 8, 0, 0);

memcached pluginのインストール

mysql> install plugin daemon_memcached soname "libmemcached.so";


さすれば、11211でLISTENしてるので

$ netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:11211           0.0.0.0:*               LISTEN
:
:
:

telnetでアクセスして試してみる

$ telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get AA
VALUE AA 8 12
HELLO, HELLO
END
quit
Connection closed by foreign host.

Spring + JUnitでParameterized実行する

qiita.com

上記のQiita記事通りにやっても動かなかったので…

以下のような感じで、@ClassRuleと@Ruleを宣言してやれば無事動いた

package org.tmarcus

import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;

@ActiveProfiles("test")
@SPringApplicationConfiguration(ParameterizedTestBase.class)
@EnableAutoConfiguration
@ComponentScan(basePackages = {
  "org.tmarcus"
})
@RunWith(Parameterized.class)
public ParameterizedTestBase extends TestBase {
  
  @ClassRule
  public static final SpringClassRule SCR = new SpringClassRule();

  @Rule
  public final SpringMethodRule springMethodRule = new SpringMethodRule();

}
package org.tmarcus

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runners.Parameterized;
import org.tmarcus.service.MathmaticsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestContextManager;

import java.util.Collection;
import java.util.List;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@Slf4j
public class SampleTest extends ParameterizedTestBase {

  private MathmaticsService service;

  @Autowired
  public void inject(
      MathmaticsService service) {
    this.service = service;
  }

  @Parameterized.Parameters
  public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][] {
      { 0, 1 },
      { 1, 1 },
      { 2, 2 },
      { 3, 6 },
      { 4, 24 },
      { 5, 120 }
    });
  }

  @Parameterized.Parameter(0)  // <- index=0のパラメータを代入する
  public int parameter;

  @Parameterized.Parameter(1)  // <- index=1のパラメータを代入する
  public int factorialExpected;

  @Test
  public void test() {
    log.info("{} -> {}", parameter, expected);
    assertThat(service.factorial(parameter), is(factorialExpected));
  }
}

RabbitMQでHAを利用したときにClientで例外を吐いてしまう問題

RabbitMQでClusterを組み、ミラーリングを設定して、いざクライアントからデータを流そうとしたら、下記のようなエラーが出てキューを開けなかったので、その対応方法の覚書。

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-ha-policy'for queue 'queue_name01ha' in vhost '/': received none but current is the value 'all' of type 'longstr', class-id=50, method-id=10)
        at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:67)
        at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:33)
        at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:361)
        at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:226)
        at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:118)
        ... 67 common frames omitted
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-ha-policy'for queue 'queue_name01ha' in vhost '/': received none but current is the value 'all' of type 'longstr', class-id=50, method-id=10)
        at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:484)
        at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:321)
        at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:144)
        at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:91)
        at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:560)
        ... 1 common frames omitted

Channelでキューを宣言するときに、このメソッドの第六引数で、以下の通りHAを使うという設定を入れてやればOK。

Connection connection = connectionFactory.getConnection();
Channel channel = connection.createChannel();
Map<String, Object> arguments = Maps.newHashMap();
arguments.put("x-ha-policy", "all");
channel.queueDeclare("queue_name01ha", true, true, false, false, arguments);

SpringMVCのMockMvcでurlVariablesを使う方法

SpringMVCのテストを書くときによく使うMockMvcで指定するgetやらpostやらでurlTemplate内でurlVarsをどう指定すればドキュメントが見当たらなかったので、ソースに潜った結果、どうすればいいかの覚書

第2引数をindex=0として{index}という形で書いてやれば置換される模様

例)

server.perform(get("/user/{0}/{1}/setting/categories", idType.getCode(), id)
		.header(HttpHeaders.CONTENT_TYPE, MEDIA_TYPE_APPLICATION_JSON))
		.andExpect(status().isOk())

server.perform(post("/user/{0}/{1}/setting/category/{2}", idType.getCode(), id, CATEGORY1)
		.header(HttpHeaders.CONTENT_TYPE, MEDIA_TYPE_APPLICATION_JSON))
		.andExpect(status().isOk())