Javaで複数のRange検索を比較的高速に行うコード

  • このエントリーをはてなブックマークに追加

複数のCIDR形式で記されたネットワークに該当するか、複数の定義された時間帯に現時刻が該当するか、などなどRange検索をしたい事がそれなりにあります。

単純なRange検索であれば、from <= v && v <= toのような単純なコードで判定できますが、Rangeの定義が複数ある場合、愚直に実装するとO(N)になります。それはとても嫌なのでO(log N)で判定できるようにします。全部自前で書いてやろうと思ったのですが、ライブラリに任せることにしました。

Guavaのコードを見ると、追加するたびにRangeをMergeするという方法を取っていました。あるクエリにHitする定義はどれだみたいな事は出来ないのですが、今回は含まれるか(contains)を判定できれば良いので妥協します。

実際のコード その1

ライブラリはデータ構造としてguavaRangeSet、CIDRの扱いとしてcommons-netSubnetUtilsを使います。コードの記述を減らすためにLombok、JSONの処理にはgsonを使いました。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>at.orz</groupId>
<artifactId>sample-range-checker</artifactId>
<version>1.0.0-SNAPSHOT</version>

<name>sample-range-checker</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.1-jre</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
App.java
public class App {

@Data
public static class AWSIpRanges implements Serializable {
private String syncToken;
private String createDate;
private List<IPv4Prefix> prefixes;
private List<IPv6Prefix> ipv6Prefixes;
@Data
public static class IPv4Prefix implements Serializable {
private String ipPrefix;
private String region;
private String service;
}
@Data
public static class IPv6Prefix implements Serializable {
private String ipv6Prefix;
private String region;
private String service;
}
}

public static class ImmutableIntRangeChecker {
@Delegate
private final ImmutableRangeSet<Integer> rangeSet;
public ImmutableIntRangeChecker(List<Range<Integer>> ranges) {
this.rangeSet = ImmutableRangeSet.unionOf(ranges);
}
public boolean isIPAddrContains(String ipAddr) {
int query = InetAddresses.coerceToInteger(InetAddresses.forString(ipAddr));
return rangeSet.contains(query);
}
}

public static void main(String[] args) throws IOException, InterruptedException {

URI awsIpRangesURI = URI.create("https://ip-ranges.amazonaws.com/ip-ranges.json");

// get aws-ip json file
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(awsIpRangesURI).build();
String json = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).body();

// parse json
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
AWSIpRanges ipRanges = gson.fromJson(json, AWSIpRanges.class);

List<Range<Integer>> ranges = ipRanges.getPrefixes().stream()
.map(AWSIpRanges.IPv4Prefix::getIpPrefix) // List<IP-String>にする
.map(ip -> ip.contains("/") ? ip : ip + "/32") // SubnetUtilsでエラーにならないように
.map(ip -> {
// CIDR形式をパースし、ネットワークアドレスの範囲を求め、そのIPアドレスを32ビット変数に変換
SubnetUtils subnet = new SubnetUtils(ip);
subnet.setInclusiveHostCount(true);
SubnetUtils.SubnetInfo info = subnet.getInfo();
return Range.closed(info.asInteger(info.getLowAddress()), info.asInteger(info.getHighAddress()));
})
.collect(Collectors.toList());

ImmutableIntRangeChecker checker = new ImmutableIntRangeChecker(ranges);
System.out.println(checker.isIPAddrContains("35.172.155.127")); // -> true
System.out.println(checker.isIPAddrContains("8.8.8.8")); // -> false

}
}

実際のコード その2

Intで扱わない場合です。RangeクラスはComparatorの実装を要求しますがInetAddrクラスは実装していないので、ラッパークラスを作成します。

またCheckerクラスを汎用的に実装し、FunctionクラスによってKeyの変換も定義します。今回はStringからComparableInetAddrに変換する関数を定義します。

App2.java

public static class ComparableInetAddr implements Serializable, Comparable<ComparableInetAddr> {
@Delegate
private final InetAddress inetAddress;
private final int intIP;
private ComparableInetAddr(InetAddress inetAddress) {
this.inetAddress = inetAddress;
this.intIP = InetAddresses.coerceToInteger(inetAddress);
}
public ComparableInetAddr(String ipAddr) {
this(InetAddresses.forString(ipAddr));
}
@Override
public int compareTo(ComparableInetAddr o) {
return Integer.compare(this.intIP, o.intIP);
}
}

public static class ImmutableRangeChecker<T, C extends Comparable> {
@Delegate
private final ImmutableRangeSet<C> rangeSet;
private final Function<T, C> queryTransformer;
public ImmutableRangeChecker(List<Range<C>> ranges, Function<T, C> queryTransformer) {
this.rangeSet = ImmutableRangeSet.unionOf(ranges);
this.queryTransformer = queryTransformer;
}
public boolean contains(T value) {
C query = queryTransformer.apply(value);
return rangeSet.contains(query);
}
}

差分のコードだけ。ComparableIntAddrをRangeクラスに入れるようにした

List<Range<ComparableInetAddr>> ranges = ipRanges.getPrefixes().stream()
.map(AWSIpRanges.IPv4Prefix::getIpPrefix)
.map(ip -> ip.contains("/") ? ip : ip + "/32")
.map(ip -> {
SubnetUtils subnet = new SubnetUtils(ip);
subnet.setInclusiveHostCount(true);
SubnetUtils.SubnetInfo info = subnet.getInfo();
// ここを変更した
return Range.closed(new ComparableInetAddr(info.getLowAddress()), new ComparableInetAddr(info.getHighAddress()));
})
.collect(Collectors.toList());

ImmutableRangeChecker<String, ComparableInetAddr> checker =
new ImmutableRangeChecker<>(
ranges,
ipString -> new ComparableInetAddr(InetAddresses.forString(ipString)));

System.out.println(checker.contains("35.172.155.127")); // false
System.out.println(checker.contains("8.8.8.8")); // true

時間の範囲を判定する場合

例えば、1日のうちの10時から12時と14時から16時。とか。1週間のうちの月曜日の午前中と火曜日の午前中とか。前者の場合は0から86400の整数として扱えば良いし、後者は月曜始まりの0時を0とした整数として扱うとかすれば良いと思います。

もしくは上記のコードでComparableが実装されているDate系のクラスを使えば良いと思います。

その他

今回はImmutableRangeSetを使いましたが、他にもTreeRangeSetの実装があります。こっちはInstanceを生成後にRangeを追加することができます。個人的にはIntegerではなくPrimitiveのintで扱いたいです。

RangeがMergeされるのを確認したコード

@Test
public void rangeSample1() {
ImmutableList<Range<Integer>> x = ImmutableList.of(Range.open(1, 100), Range.open(200, 300)).asList();
TreeRangeSet<Integer> im = TreeRangeSet.create();
im.addAll(x);
im.add(Range.open(50, 250));
System.out.println(im); // [(1..300)]
}
@Test
public void rangeSample2() {
ImmutableList<Range<Integer>> x = ImmutableList.of(Range.closed(1, 100), Range.closed(200, 300)).asList();
TreeRangeSet<Integer> im = TreeRangeSet.create();
im.addAll(x);
im.add(Range.open(50, 150)); // [[1..150), [200..300]]
System.out.println(im);
}

参考

CIDRに限ってしまえばもっと高速にできるみたいです。

AWSで特定のサーバの時刻が急激にずれていく

  • このエントリーをはてなブックマークに追加

NTPDの設定をしているにも関わらず、急激に時間がずれていった時の話。

ntpqで確認したらoffsetが大きくずれていたが、**dateでみると時間はずれていなかった。**正確には私が見る前に時間が大きくずれていたので1回ntpdateをして時間を合わせたのだが、offsetはそのままでまた時間がすごい勢いでずれていくという現象が発生しました。

ntpqで状態を確認。

ntpq -p
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
*169.254.169.123 10.66.1.54 3 u 47 64 377 0.098 531.046 12.220

driftファイル(/var/lib/ntp/drift)を見ると、ほかのサーバは-0.403,0.013,-0.090といった小さい数字(正しくは0に近い値)なのに対して、このサーバは-499.822と極端に大きな値(0から大きくかけ離れた値)が記載されていた。

解決方法

  1. ntpd stop
  2. driftファイルを削除(実際にはmove)
  3. ntpdate -b (大きくずれていたのでslewモードではなくstepモードで一気に設定)
  4. ntpd start

これはVMのImage(AMI)を作るときには削除しておくのが良いファイルの1つです。

この操作によって大きく時間がずれていくことがなくなりました。

ntpq -p
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
*169.254.169.123 10.66.1.54 3 u 23 256 377 0.117 0.890 1.268

参考

Macで自分のHostNameの解決が遅い

  • このエントリーをはてなブックマークに追加

Macで開発していたらHostNameの参照が遅いという事がありました。
例えばJavaのLoggerのLogbackではHostnameを自動で取得します。

HOSTNAME変数とCONTEXT_NAME変数はよく使われるので、コンテキストスコープを持つ変数として自動的に定義されます。

自分のHostNameを正しく解決できないと10秒ほどBlockされるので、テストケースが100個あるとその都度待たされてしまうわけです。

実際に詰まっているのを確認したら、IPv6のアドレスを求めるところで詰まってました。

"pool-1-thread-1-ScalaTest-running-FormDefinitionsTest" #12 prio=5 os_prio=31 tid=0x00007fc13882d800 nid=0xa903 runnable
[0x00007000026bb000]
java.lang.Thread.State: RUNNABLE
at java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method)
at java.net.InetAddress$2.lookupAllHostAddr(InetAddress.java:928)
at java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1323)
at java.net.InetAddress.getLocalHost(InetAddress.java:1500)
- locked <0x0000000740866718> (a java.lang.Object)
at ch.qos.logback.core.util.ContextUtil.getLocalHostName(ContextUtil.java:37)
at ch.qos.logback.core.util.ContextUtil.addHostNameAsProperty(ContextUtil.java:67)
at ch.qos.logback.classic.joran.action.ConfigurationAction.begin(ConfigurationAction.java:59)
at ch.qos.logback.core.joran.spi.Interpreter.callBeginAction(Interpreter.java:269)
at ch.qos.logback.core.joran.spi.Interpreter.startElement(Interpreter.java:145)
at ch.qos.logback.core.joran.spi.Interpreter.startElement(Interpreter.java:128)
at ch.qos.logback.core.joran.spi.EventPlayer.play(EventPlayer.java:50)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:148)
- locked <0x0000000740f7f370> (a ch.qos.logback.core.spi.LogbackLock)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:135)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:100)
at ch.qos.logback.core.joran.GenericConfigurator.doConfigure(GenericConfigurator.java:50)
at play.api.Logger$$anonfun$configure$3.apply(Logger.scala:256)
at play.api.Logger$$anonfun$configure$3.apply(Logger.scala:256)
at scala.Option.foreach(Option.scala:257)
....

jshellで確認できます。

jshell> InetAddress localhost = InetAddress.getLocalHost();
localhost ==> SN0298.local/192.168.11.4

原因と解決策

次のコマンドを実行した時に、scutil --get HostNameの結果が空なのが原因です。

scutil --get HostName       #
scutil --get LocalHostName # SN0298
scutil --get ComputerName # SN0298

次のコマンドで設定する事で対応できます。

sudo scutil --set HostName $(scutil --get LocalHostName)

jshell> InetAddress localhost = InetAddress.getLocalHost();
localhost ==> SN0298/192.168.11.4

.localが無くなりますが、まぁ気にしない方向で。

検索すると/etc/hostsSN0298 127.0.0.1のようにする解決策を見かけます。この方法でも良いのですが、それだと上記のコードでDHCPのアドレスが取得できなくなります。

Maven RepositoryをGCSに構築する

  • このエントリーをはてなブックマークに追加

Maven Repositoryを作るとき、一般的にはNexusやJFrogを使いますが、APIサーバが必要なわけでもないのでGCPのCloud Storageに構築します。

これを使います。こちらのサイトを参考にします。

まずGCSにBucketを作る

適当な名前で作ります。Private Repositoryとして作成するのでPublicアクセスはOFFのままにしておきます。

pom.xmlにplugin(extension)の設定を追記

<build>
<extensions>
<extension>
<groupId>com.gkatzioura.maven.cloud</groupId>
<artifactId>google-storage-wagon</artifactId>
<version>1.7</version>
</extension>
</extensions>
</build>

バージョンは1.7を指定します。今時点では2.0がリリースされていますが、1.8からバグっていて、GCSにはアップロードできません。おそらくこのコミットで動かなくなったのだと思います。

成果物のアップロード先を指定します。

<distributionManagement>
<snapshotRepository>
<id>hoge-fuga-snapshot</id>
<url>gs://hogefuga-bucket/snapshot</url>
</snapshotRepository>
<repository>
<id>hoge-fuga-release</id>
<url>gs://hogefuga-bucket/release</url>
</repository>
</distributionManagement>

GCSの認証

通常はsetting.xmlに記述しますが、このPluginはその設定情報を使いません。

gcloud auth login –brief で認証するか、環境変数のGOOGLE_APPLICATION_CREDENTIALSを設定する必要があります。

WSLでSSHの鍵を転送するとBSoDで死ぬ件について

  • このエントリーをはてなブックマークに追加

関連Issue: https://github.com/Microsoft/WSL/issues/3916

KB4489868 を削除して再起動したら直りました。いや、回避しただけで直ってはいないわけですが。

追記 2019/04/08

Windows10のパッチバージョン1809をインストールするとBSoDが発生しなくなりました。

HttpClientはデフォルトでAccept Headerを付けない

  • このエントリーをはてなブックマークに追加

ApacheのHttpClinetはデフォルトでAcceptヘッダを付与しません。
例えば、簡単なサンプルを次に示します。

SSLContextConnectionManager,RequestConfigを設定していませんが、そこは本質的ではないので省略しています。

HttpClientBuilder clientBuilder = HttpClientBuilder.create();
try (CloseableHttpClient client = clientBuilder.build()) {
HttpGet getMethod = new HttpGet();
getMethod.setURI(URI.create("https://httpstat.us/200"));
try (CloseableHttpResponse resp = client.execute(getMethod)) {
String body = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
logger.info("header:{}", resp.getStatusLine());
logger.info("body:{}", body);
}
}

HttpClientのDebugLogをみたいので、LogレベルをDEBUGに設定していまいます。あとコピペしやすいようにPatternをちょっとだけ修正。

Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.setLevel(Level.DEBUG);

PatternLayoutEncoder ple = new PatternLayoutEncoder();
ple.setContext(rootLogger.getLoggerContext());
ple.setPattern("%-50class{1} %msg%n");
ple.start();

ConsoleAppender consoleAppender = (ConsoleAppender) rootLogger.getAppender("console");
consoleAppender.setEncoder(ple);

実行結果です。必要なところだけを抜き出しています。

RunningLog
o.a.h.i.e.MainClientExec                           Executing request GET /200 HTTP/1.1
o.a.h.i.e.MainClientExec Target auth state: UNCHALLENGED
o.a.h.i.e.MainClientExec Proxy auth state: UNCHALLENGED

o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 >> GET /200 HTTP/1.1
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 >> Host: httpstat.us
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 >> Connection: Keep-Alive
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.4 (Java/11.0.2)
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 >> Accept-Encoding: gzip,deflate
o.a.h.i.c.Wire http-outgoing-0 >> "GET /200 HTTP/1.1[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 >> "Host: httpstat.us[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.4 (Java/11.0.2)[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 >> "[\r][\n]"

o.a.h.i.c.Wire http-outgoing-0 << "HTTP/1.1 200 OK[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Cache-Control: private[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Server: Microsoft-IIS/10.0[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "X-AspNetMvc-Version: 5.1[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Access-Control-Allow-Origin: *[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "X-AspNet-Version: 4.0.30319[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "X-Powered-By: ASP.NET[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Set-Cookie: ARRAffinity=beec3692495883b8df6195e900c12f49514e054d865a22ad2951c84f51dbaf93;Path=/;HttpOnly;Domain=httpstat.us[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Date: Sun, 17 Mar 2019 08:21:05 GMT[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "Content-Length: 0[\r][\n]"
o.a.h.i.c.Wire http-outgoing-0 << "[\r][\n]"

o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << HTTP/1.1 200 OK
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Cache-Control: private
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Server: Microsoft-IIS/10.0
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << X-AspNetMvc-Version: 5.1
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Access-Control-Allow-Origin: *
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << X-AspNet-Version: 4.0.30319
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << X-Powered-By: ASP.NET
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Set-Cookie: ARRAffinity=beec3692495883b8df6195e900c12f49514e054d865a22ad2951c84f51dbaf93;Path=/;HttpOnly;Domain=httpstat.us
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Date: Sun, 17 Mar 2019 08:21:05 GMT
o.a.h.i.c.LoggingManagedHttpClientConnection http-outgoing-0 << Content-Length: 0
o.a.h.i.e.MainClientExec Connection can be kept alive indefinitely

o.a.HttpClientSample header:HTTP/1.1 200 OK
o.a.HttpClientSample body:

これを見るとAcceptヘッダを送信していません。

何が問題か?

ブラウザやcurlはデフォルトでAcceptを送信しており、そこには*/*が含まれています。
参照: https://developer.mozilla.org/ja/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values

サンプルに利用したhttpstatでAcceptヘッダがある場合と無い場合とで確認してみましょう。

include_accept_header
$ curl http://httpstat.us/200 -v
* Trying 23.99.0.12...
* TCP_NODELAY set
* Connected to httpstat.us (23.99.0.12) port 80 (#0)
> GET /200 HTTP/1.1
> Host: httpstat.us
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Cache-Control: private
< Content-Length: 6
< Content-Type: text/plain; charset=utf-8
< Server: Microsoft-IIS/10.0
< X-AspNetMvc-Version: 5.1
< Access-Control-Allow-Origin: *
< X-AspNet-Version: 4.0.30319
< X-Powered-By: ASP.NET
< Set-Cookie: ARRAffinity=beec3692495883b8df6195e900c12f49514e054d865a22ad2951c84f51dbaf93;Path=/;HttpOnly;Domain=httpstat.us
< Date: Sun, 17 Mar 2019 08:33:52 GMT
<
* Connection #0 to host httpstat.us left intact
200 OK
exclude_accept_header
$ curl -H "Accept:"  http://httpstat.us/200 -v
* Trying 23.99.0.12...
* TCP_NODELAY set
* Connected to httpstat.us (23.99.0.12) port 80 (#0)
> GET /200 HTTP/1.1
> Host: httpstat.us
> User-Agent: curl/7.58.0
>
< HTTP/1.1 200 OK
< Cache-Control: private
< Server: Microsoft-IIS/10.0
< X-AspNetMvc-Version: 5.1
< Access-Control-Allow-Origin: *
< X-AspNet-Version: 4.0.30319
< X-Powered-By: ASP.NET
< Set-Cookie: ARRAffinity=beec3692495883b8df6195e900c12f49514e054d865a22ad2951c84f51dbaf93;Path=/;HttpOnly;Domain=httpstat.us
< Date: Sun, 17 Mar 2019 08:36:54 GMT
< Content-Length: 0
<
* Connection #0 to host httpstat.us left intact

Accpetヘッダを送信しないと、Content-Lengthヘッダが0になってBODYが空のレスポンスが返ってきました。

このサイトに限らず、例えばAcceptヘッダでapplication/jsonだとJSON形式で返すようなサイトでは、Frameworkによってはこういう挙動になるかもしれません。

どう書けばいいのか

いくつかやり方がありますがDefaultHeaderを設定するのが楽だと思います。

List<BasicHeader> defaultHeaders = Arrays.asList(new BasicHeader("Accept", "*/*"));
HttpClientBuilder clientBuilder = HttpClientBuilder.create()
.setDefaultHeaders(defaultHeaders);

もしくはGetMethodに設定します。

getMethod.setHeader("Accept", "*/*");

GCPのプロジェクトを組織なしで作成する方法

  • このエントリーをはてなブックマークに追加

GMAILアカウントでGCPを利用している場合は組織という概念がない(?)ので、常に「組織なし」の状態で作られます。
GSuiteで管理しているドメイン(会社のドメイン)でプロジェクトを作成すると、設定にもよりますがそのGSuiteの組織の下にプロジェクトが作られます。

組織の下にプロジェクトを作成すると、組織そのものにIAMを追加しておけば配下の全てのプロジェクトに引き継がれるので毎回オーナーを設定すると言った煩わしい作業が無くなるので楽なのですが、組織の下に置くとプロジェクトを組織から移動させる事が大変になります。

例えば、会社はサポートしているんだけどOSSとしてやっているプロジェクトなんかは組織に紐付けない方が良いものがあります。もしくは将来別組織に移動する予定があるプロジェクトとか。

どう作るか?

  1. 個人のアカウントでプロジェクトを作成する
  2. 課金アカウントを紐付けている場合は課金アカウントを外す
  3. そのプロジェクトに組織ドメインのユーザーをオーナーとして追加する
  4. そのプロジェクトの課金アカウントを組織ドメインの課金アカウントに紐付ける
  5. 個人のユーザーをそのプロジェクトから外す

普通はこんな事やりませんが、ちょっと諸事情で必要になったので調べたのでした。

ハマったこと

会社のアカウントでやる前に個人のGSuiteアカウントで予行練習しようとしたら、次の事にハマりました。マニュアル読めばきっと書いてある内容ではあると思う。

  • 最初GSuiteで管理しているGCPのアカウントを作ったら、プロジェクトが作れなかった。組織のIAM設定でユーザーを追加する必要があった
  • 組織の下にはフォルダを作ることができる。しかし組織の管理者でもオーナーでもこのフォルダを作ることが出来なかった。フォルダ作成者といった権限が必要だった。なんでもかんでもオーナーだからと言って設定出来るわけではないらしい。組織の管理者ではだめだった。

rsyslogのTCP転送のConnectionを定期的にRefreshする

  • このエントリーをはてなブックマークに追加

rsyslogでリモートサーバにログを転送する時、転送先を冗長化するためにLBを使うことがあると思います。AWSだとNLBを使うことになります。

rsyslogはログを転送するたびにsocketを貼るわけではなく、貼りっぱなしの状態になります。そのため、NLB配下に複数サーバがあっても均等に割り当てられるとは限らないため、負荷が均等に分散されません。解決策として、Client側で定期的にSocketを張り替える必要があります。

設定方法

こちらのマニュアルにRebindIntervalの項目があるため、これを利用します。TCPなので$ActionSendTCPRebindIntervalを使います。

Permits to specify an interval at which the current connection is broken and re-established. This setting is primarily an aid to load balancers. After the configured number of messages has been transmitted, the current connection is terminated and a new one started. Note that this setting applies to both TCP and UDP traffic. For UDP, the new connection'' uses a different source port (ports are cycled and not reused too frequently). This usually is perceived as a new connection’’ by load balancers, which in turn forward messages to another physical target system.

この設定項目は、公式サイトより、バージョン4.5.1からサポートされています。しかしながら、同じく公式サイトにはバージョン6.3.6からと書いてあります。

ソースコードを見ると4.5.1からサポートされていますし、AmazonLinuxでは5.8.10ですが当然このバージョンでもサポートされています。

指定する単位はメッセージ数です。秒ではないです。
例えば以下のように設定すると、10000メッセージを送ったタイミングで再接続します。数値は実際のログの流量によって調整すると良いです。

rsyslog.conf
$ActionSendTCPRebindInterval 10000 # reconnect per message count

ssh forwardで転送している鍵をdocker containerの中で使う方法

  • このエントリーをはてなブックマークに追加

docker containerの中でAgent転送した鍵を使いたいことがあります。
次のような場合です。
bastionにagent forwardで鍵を転送して、その鍵をdockerの中に持ち込みたい。

mac -> bastion
docker run fabric
[in container]
fabricでsshの設定が必要

AgentForwardしていると環境変数$SSH_AUTH_SOCKが設定され、そこにsocketファイルのPATHが入っています。これをdockerの中に持ち込めれば良いので、次のようにします。

docker run -it --rm \
-v $SSH_AUTH_SOCK:/ssh-agent \
-e SSH_AUTH_SOCK=/ssh-agent \
image command

もしbastionに存在する鍵や、SSHの設定も一緒にdocker containerの中で使いたい場合は、.sshもmountします。

-v ${HOME}/.ssh:/root/.ssh -v $SSH_AUTH_SOCK:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent

なおmountする時に、mount元が存在しない場合はrootでディレクトリが作られてしまうので注意が必要です。-v /home/user/.m2:/root/.m2とした場合、/home/user/.m2というディレクトリが存在しないとrootユーザーの.m2ディレクトリが作成されてしまいます。

参考

datadogのauto mutingとlimit metrics collectionについて

  • このエントリーをはてなブックマークに追加

9月11日(火)の早朝から、DatadogのAutomutingが効かなくなりました。その原因を探ったのでその記録です。

AutoMutingとは?

ASGを使って、ScaleOut/ScaleInを頻繁にするようなシステムの場合、ScaleInで縮退する時(Instanceの削除)にはそのInstanceは監視対象外にしたいです。でないと、Instanceが削除されるたびにMetricsが来ないNODATAやdatadog.agent.upを見ている場合にアラートが発生します。

それを解決するためにDatadogではAutomutingをサポートしており、CloudWatchの情報を使ってInstanceが死んだら自動でInstanceのMuteをするようにしています。
これはDatadogのAWSとの連携設定の中にあります。

何が原因なのか?

ホストベースの簡単なMonitorを作成すると、次のようなクエリの監視ができあがります。

"datadog.agent.up".over("*").by("host").last(2).count_by_status()

これは通常、InstanceがTerminatedされた時にはDatadog側でそのホストは自動的にMuteされるのですが、この機能が動かずアラートが鳴りまくるという事象が発生しました。

原因について

結論から言えば、Datadog側の挙動が変わりました。サポートに連絡したところ、いくつかのIssueがあって解決中とのことなので、また元の挙動に戻るかもしれません。
ひとまず、現時点での挙動を記録に残しておきます。

原因判明について

こちらが解決のヒントになりました。
ありがたや、ありがたや。

このスレッドに、様々なスクリーンショットがありますのでご参考まで。

現時点の挙動について

Datadogは課金対象のInstanceを、Agentが動いている もしくは Datadogが認識したEC2インスタンスとしています。Agentを動かしていなくても、DatadogがEC2 Instanceを認識すればCloudWatch経由でMetricsを取ってきます。これも課金対象になります。
そのため、開発用にささっと立てたInstanceにDatadogのAgentを入れていなくても課金対象になってしまいます。

それを回避するため、limit metrics collectionの設定をします(下図参照)。例えば、ここにdatadog:enabledと入れるとAWSのタグにdatadog:enabledと設定されているInstanceが対象になります。(※当然ですがタグの設定がなくてもAgentが動いていれば課金対象です)

これらがどのように変わったのか、下の図にまとめました。
端的に言えば、limit metrics collectionの設定がある場合に、EC2にそのTagを設定していなくてもAgentの起動をしていればAutomutingの対象でしたが、該当日から対象外になりました。

Before

Agent起動 Tag有り 課金対象 Automutingが動作するか
F F F F
F T T T
T F T T
T T T T

Aftre

Agent起動 Tag有り 課金対象 Automutingが動作するか
F F F F
F T T T
T F T F
T T T T

サポートに連絡したところ、最終的には次の回答をもらいました。

I found out that we do have some issues with the EC2 automuting feature and the autoscaling groups. Our engineers are currently working on it.
We’re sorry for the inconvenience and will keep you updated.

しばらく様子を見ます。

なお、タグの設定を一回外すということをすると、このように課金対象ホストが増えてしまうので注意です。(これは実験のために意図して外したので真似はしないように。。)