Septeni Engineer's Blog

セプテーニ・オリジナルのエンジニアが綴る技術ブログ

Terraformを使って複数のAWSアカウントにAWS Integrationを導入する

これは Datadog Advent Calendar 2019 13日目の記事です。

門脇(@blac_k_ey)です。

PYXISのデータ基盤チームでSREっぽいことを行っている傍ら、インフラチームとして横断的なインフラの管理・運用を行っています。

モチベーション

最近PYXISチームではモニタリングツールとしてDatadogの採用が進んでいます。
PYXISのインフラは主にAWSを利用しており、DatadogのAWS Integrationを導入することで各種サービスのメトリクスを取得することが可能になります。

ここで課題になるのが弊社のAWSのアカウント構成です。
PYXISチームのAWSアカウント管理は以下のような構成になっています。

f:id:Nomad_Blacky:20191212214328p:plain

このように、基幹となるAWSアカウントからAssumeRoleを使って、各プロジェクトで利用しているAWSアカウントにログインするという形を取っています。

Terraformで各プロジェクトアカウントに対してAWS Integrationを導入しようとしたとき、アカウントひとつひとつにTerraform用のユーザを用意することを考えました。
しかし、この場合だと各アカウントにユーザを用意する必要があるためcredentialの管理が大変ですし、stateファイルも各プロジェクトに散らばってしまうので管理が難しくなってしまいます。
なるべく最小の手間でAWS Integrationを導入したいため、これでは骨が折れます。

f:id:Nomad_Blacky:20191212215148p:plain

そこで、今回は以下のような構成を取りました。

f:id:Nomad_Blacky:20191212215348p:plain

基幹アカウントにTerraform用ユーザ、各プロジェクトのアカウントにTerraformを利用できる権限を持ったRoleを用意し、AssumeRoleできるようにします。
これにより、credentialの管理は基幹アカウントのユーザだけになり、stateファイルもいちアカウントにまとめることができました。
必要なものはプロジェクトアカウントのRoleと、基幹→プロジェクトアカウントにAssumeRoleする権限だけになりますが、この部分は弊社の情シスがAWSアカウント作成時に用意してくれるものです。
AWS Integrationの導入のスピードも早まり、Credential管理などの運用の負担もかなり減りました。

以下から、実際のTerraformを見ていきます。

Terraformの準備

表題の通り、Terraformを使ってDatadogのAWS Integrationを設定していきます。

結論から入ってしまいますが、以下は今回の成果物となるTerraformです。

main.tf

terraform {
  required_version = "= 0.12.12"

  backend "s3" {
    bucket               = "account-a-terraform"
    workspace_key_prefix = "datadog_aws_integration_terraform"
    key                  = "terraform.tfstate"
    region               = "ap-northeast-1"
    role_arn             = "arn:aws:iam::111111111111:role/TerraformLoginRole"
  }
}

provider "aws" {
  version = "~> 2.24.0"
  region  = local.aws_region

  assume_role {
    role_arn = var.workspace_iam_role_arn[terraform.workspace]
  }
}

provider "datadog" {
  version = "~> 2.4.0"
  api_key = var.datadog_api_key
  app_key = var.datadog_app_key
}

##### AWS #####

data "aws_iam_policy_document" "datadog_integration_iam_policy_document" {
  # https://docs.datadoghq.com/integrations/amazon_web_services/?tab=allpermissions#datadog-aws-iam-policy
  statement {
    sid = "all"

    effect = "Allow"

    actions = [
      "apigateway:GET",
      "autoscaling:Describe*",
      "budgets:ViewBudget",
      "cloudfront:GetDistributionConfig",
      "cloudfront:ListDistributions",
      "cloudtrail:DescribeTrails",
      "cloudtrail:GetTrailStatus",
      "cloudwatch:Describe*",
      "cloudwatch:Get*",
      "cloudwatch:List*",
      "codedeploy:List*",
      "codedeploy:BatchGet*",
      "directconnect:Describe*",
      "dynamodb:List*",
      "dynamodb:Describe*",
      "ec2:Describe*",
      "ecs:Describe*",
      "ecs:List*",
      "elasticache:Describe*",
      "elasticache:List*",
      "elasticfilesystem:DescribeFileSystems",
      "elasticfilesystem:DescribeTags",
      "elasticloadbalancing:Describe*",
      "elasticmapreduce:List*",
      "elasticmapreduce:Describe*",
      "es:ListTags",
      "es:ListDomainNames",
      "es:DescribeElasticsearchDomains",
      "health:DescribeEvents",
      "health:DescribeEventDetails",
      "health:DescribeAffectedEntities",
      "kinesis:List*",
      "kinesis:Describe*",
      "lambda:AddPermission",
      "lambda:GetPolicy",
      "lambda:List*",
      "lambda:RemovePermission",
      "logs:Get*",
      "logs:Describe*",
      "logs:FilterLogEvents",
      "logs:TestMetricFilter",
      "logs:PutSubscriptionFilter",
      "logs:DeleteSubscriptionFilter",
      "logs:DescribeSubscriptionFilters",
      "rds:Describe*",
      "rds:List*",
      "redshift:DescribeClusters",
      "redshift:DescribeLoggingStatus",
      "route53:List*",
      "s3:GetBucketLogging",
      "s3:GetBucketLocation",
      "s3:GetBucketNotification",
      "s3:GetBucketTagging",
      "s3:ListAllMyBuckets",
      "s3:PutBucketNotification",
      "ses:Get*",
      "sns:List*",
      "sns:Publish",
      "sqs:ListQueues",
      "support:*",
      "tag:GetResources",
      "tag:GetTagKeys",
      "tag:GetTagValues",
      "xray:BatchGetTraces",
      "xray:GetTraceSummaries",
    ]

    resources = [
      "*",
    ]
  }
}

resource "aws_iam_policy" "datadog_integration_iam_policy" {
  name_prefix = "DatadogAWSIntegrationPolicy"
  policy      = data.aws_iam_policy_document.datadog_integration_iam_policy_document.json
}

resource "aws_iam_role_policy_attachment" "datadog_integration_iam_role_policy_attachment" {
  policy_arn = aws_iam_policy.datadog_integration_iam_policy.arn
  role       = aws_iam_role.datadog_integration_iam_role.name
}

data "aws_iam_policy_document" "datadog_integration_assume_role_iam_policy_document" {
  statement {
    actions = [
      "sts:AssumeRole",
    ]

    principals {
      identifiers = [
        "arn:aws:iam::${local.datadog_account_id}:root",
      ]

      type = "AWS"
    }

    condition {
      test = "StringEquals"

      values = [
        datadog_integration_aws.aws_integration.external_id,
      ]

      variable = "sts:ExternalId"
    }
  }
}

resource "aws_iam_role" "datadog_integration_iam_role" {
  name               = "DatadogAWSIntegrationRole"
  assume_role_policy = data.aws_iam_policy_document.datadog_integration_assume_role_iam_policy_document.json
}

##### Datadog #####

resource "datadog_integration_aws" "aws_integration" {
  account_id                       = var.aws_account_id[terraform.workspace]
  account_specific_namespace_rules = {}
  filter_tags = [
    "datadog-enabled:true",
  ]
  host_tags = [
    "aws_account_name:${terraform.workspace}",
  ]
  role_name = local.datadog_aws_integration_role_name
}

variables.tf

locals {
  aws_region         = "ap-northeast-1"
  datadog_account_id = "464622532012" # DatadogのアカウントID
}

variable "workspace_iam_role_arn" {
  type = map(string)
  default = {
    account-a = "arn:aws:iam::111111111111:role/TerraformLoginRole"
    account-b = "arn:aws:iam::222222222222:role/TerraformLoginRole"
  }
}

variable "aws_account_id" {
  type = map(string)
  default = {
    account-a = "111111111111"
    account-b = "222222222222"
  }
}

variable "datadog_api_key" {
  type = string
}

variable "datadog_app_key" {
  type = string
}

重要な部分をかいつまんで説明します。

AWS Integrationから参照されるIAMロールを設定

AWS Integrationで各サービスのメトリクスを取得するために必要な権限を追加していきます。

必要な権限はDatadogのドキュメントに記載されており、AWSのサービス追加やDatadog側のIntegrationの追加などで時々変更がありますので、監視したいサービスの権限が含まれているかよく確認しておきましょう。

data "aws_iam_policy_document" "datadog_integration_iam_policy_document" {
  # https://docs.datadoghq.com/integrations/amazon_web_services/?tab=allpermissions#datadog-aws-iam-policy
  statement {
    sid = "all"
    effect = "Allow"
    actions = [
      "apigateway:GET",
      "autoscaling:Describe*",
      "budgets:ViewBudget",
      ...
    ]
    resources = ["*"]
  }
}

また、Datadogが管理しているAWSのアカウントからAssumeRoleできる権限を追加します。

data "aws_iam_policy_document" "datadog_integration_assume_role_iam_policy_document" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      identifiers = ["arn:aws:iam::${local.datadog_account_id}:root"]
      type = "AWS"
    }

    condition {
      test = "StringEquals"
      values = [datadog_integration_aws.aws_integration.external_id]
      variable = "sts:ExternalId"
    }
  }
}

DatadogにAWS Integrationを設定

TerraformのDatadog Providerを使ってAWS Integrationを設定します。

resource "datadog_integration_aws" "aws_integration" {
  account_id                       = var.aws_account_id[terraform.workspace]
  account_specific_namespace_rules = {}
  filter_tags = [
    "datadog-enabled:true",
  ]
  host_tags = [
    "aws_account_name:${terraform.workspace}",
  ]
  role_name = local.datadog_aws_integration_role_name
}

filter_tags を使うことで、指定のタグを持ったEC2インスタンスのみを監視下に置くことが可能です。
これにより必要以上にHost単位の課金がかかることを抑えられます。

host_tags はこのAWS Integrationから送信されたメトリクスにタグを振ることができます。
ここではアカウント名を追加することで、どのアカウントから送られてきたメトリクスか分かりやすくしています。

また、ここで重要になるのが role_name をローカル変数から取ってきているという点です。
前項でIAMロールの定義を行っているのでそこからロール名を引っ張りたいところです。
しかし、IAMロールがこの aws_integrationexternal_id を参照しているため、循環参照となってしまうため無効な定義となってしまいます。

Workspacesでアカウントごとの差分とStateを管理

TerraformにはWorkspacesという機能があります。
これを使うことで、本番・ステージング…といった環境ごとにState管理できるようになります。

今回はAWSアカウント名をworkspace名として管理していきます。

$ terraform workspace new account-a
$ terraform workspace new account-b

workspaceごとに設定値を変更したい場合があります。
その場合はvariablesを map とし、workspace名をキーとしてデフォルト値をもたせます。

variable "workspace_iam_role_arn" {
  type = map(string)
  default = {
    account-a = "arn:aws:iam::111111111111:role/TerraformLoginRole"
    account-b = "arn:aws:iam::222222222222:role/TerraformLoginRole"
  }
}

あとは変数の利用側を var.mapの変数名[terraform.workspace] とすればよいです。

provider "aws" {
  version = "~> 2.24.0"
  region  = local.aws_region

  assume_role {
    role_arn = var.workspace_iam_role_arn[terraform.workspace]
  }
}

上記の設定により、ひとつのIAMユーザから複数のアカウントにAssumeRoleし、Terraformを適用できます。

また、

terraform {
  required_version = "= 0.12.12"

  backend "s3" {
    bucket               = "account-a-terraform"
    workspace_key_prefix = "datadog_aws_integration_terraform"
    key                  = "terraform.tfstate"
    region               = "ap-northeast-1"
    role_arn             = "arn:aws:iam::111111111111:role/TerraformLoginRole"
  }
}

GitLab CI で terraform play/apply を自動化する

以上でTerraformの準備は整いました。
GitLab CIを使ってデプロイを自動化していきましょう。

stages:
  - plan
  - apply

image:
  name: hashicorp/terraform:0.12.12
  entrypoint: [""]

## 環境変数の設定
.env_account-a: &env_account-a
  variables:
    AWS_ACCOUNT_NAME: account-a

.env_account-b: &env_account-b
  variables:
    AWS_ACCOUNT_NAME: account-b

## terraform plan
.plan: &plan
  stage: plan
  script:
    - terraform fmt -check=true -diff=true
    - terraform init
    - terraform workspace new $AWS_ACCOUNT_NAME || true
    - terraform workspace select $AWS_ACCOUNT_NAME
    - terraform validate
    - terraform plan
  artifacts:
    paths:
      - '.terraform'

plan account-a:
  <<: *env_account-a
  <<: *plan

plan account-b:
  <<: *env_account-b
  <<: *plan

## terraform apply
.apply: &apply
  stage: apply
  script:
    - terraform apply -auto-approve
  when: manual
  only:
    - master

apply account-a:
  <<: *env_account-a
  <<: *apply
  dependencies:
    - plan account-a

apply account-b:
  <<: *env_account-b
  <<: *apply
  dependencies:
    - plan account-b

CI設定のVariablesにTerraform用ユーザのcredentialを追加しておきましょう。

これで、新しいアカウントでAWS Integrationを導入したい場合は、TerraformのvariablesにRoleのARNなどを追加し、GitLab CIのジョブを追加すればOKですね。

デプロイしてみる

あとはGitLab CIのジョブを走らせるだけです!

f:id:Nomad_Blacky:20191212210956p:plain



f:id:Nomad_Blacky:20191212211145p:plain

これで、AWS Integrationを複数のアカウントに展開することができました!

まとめ

  • DatadogのAWS IntegrationをTerraformでデプロイするには
    • AWSのIAM Roleを設定して、
    • Datadog Providerの aws_integration を設定する
  • 複数のアカウントにAWS Integrationを展開したい場合は
    • 各アカウントにAssumeRoleできるユーザを用意するとcredentialとstateファイルの管理が楽になる

このほか、TerraformのDatadog ProviderではMonitorやDashboardの設定も可能なので、
各アカウント共通で使えるアラートや、ダッシュボードなどを用意するのも面白そうだなと思いました。

まだまだDatadogの活用が始まったばかりなので、色々いじって知見をためていきたいです。

お気に入りのScalaライブラリ・ツールをだらだら紹介する

この記事は Scala Advent Calendar 2019 の6日目です。

こんにちは。最近はPYXISのデータ基盤チームでSREっぽいことしている 門脇(@blac_k_ey)です。
TETRIS99の次はポケモンに進捗を奪われる日々を送っています。


締め切りギリギリまで ポケモンで忙しかった 「これ書きたい!」というテーマが思いつかなかったので、個人的に最近のScala開発でお世話になっているライブラリやツールなどを紹介していこうと思います。

詳細はあまり書かず、「ここが好き!」ぐらいのお気持ち表明ぐらいの文章でしかないので、ゆるく暖かい気持ちで読んでくれたら幸いです。

Ammonite

リッチなScalaREPL、Scalaスクリプト

自分のScala生活が豊かになっている大きな要因のひとつ。

Magic Importsを使って、ライブラリをちょっと試したいときにササッと使えるし、

$ amm
Loading...
Welcome to the Ammonite Repl 1.7.1
(Scala 2.12.10 Java 1.8.0_181)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import $ivy.`com.typesafe.play::play-json:2.7.4`
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4.pom
  100.0% [##########] 4.2 KiB (1.0 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4.pom
  100.0% [##########] 1.5 KiB (12.0 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4-sources.jar
  100.0% [##########] 9.7 KiB (79.4 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-functional_2.12/2.7.4/play-functional_2.12-2.7.4.jar
  100.0% [##########] 175.5 KiB (389.2 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4-sources.jar
  100.0% [##########] 52.5 KiB (94.5 KiB / s)
https://repo1.maven.org/maven2/com/typesafe/play/play-json_2.12/2.7.4/play-json_2.12-2.7.4.jar
  100.0% [##########] 706.6 KiB (705.2 KiB / s)
import $ivy.$

@ import play.api.libs.json._
import play.api.libs.json._

@ val json = Json.parse("""{"language": "Scala", "repl": "Ammonite"}""")
json: JsValue = JsObject(Map("language" -> JsString("Scala"), "repl" -> JsString("Ammonite")))

スケジュール実行に対応したCIツールを使えば、手元で書いたちょっとしたスクリプトでささっと自動化できるし、

(esa.ioのメトリクスをDatadogに送信する例)

# .gitlab-ci.yml

image: openjdk:8

scheduled_post_stats:
  only:
    refs:
      - schedules
    variables:
      - $SCHEDULED_JOB_POST_STATS
  script:
    - ./amm esa-stats-to-datadog.sc --team septeni-original

f:id:Nomad_Blacky:20191205234130p:plain

f:id:Nomad_Blacky:20191205234756p:plain

何度閉じてもダイアログが開くという犯罪臭のするスクリプトだって書けちゃう!!(?)

import java.awt._, event._
import javax.swing._

import scala.util.Random

val (screenCenterW, screenCenterH) = {
  val ss = Toolkit.getDefaultToolkit().getScreenSize
  (ss.getWidth.toInt / 2, ss.getHeight.toInt / 2)
}

val label = new JLabel("何回閉じても無駄ですよ~ww")

def randomLocation(): (Int, Int) =
  (screenCenterW + Random.nextInt(200) - 200, screenCenterH + Random.nextInt(200) - 200)

def newDialog(): Unit = {
  val dialog = new JDialog()
  dialog.setSize(300, 200)
  val (x, y) = randomLocation()
  dialog.setLocation(x, y)
  dialog.add(label)
  dialog.setVisible(true)
  dialog.addWindowListener(new WindowAdapter {
    override def windowClosing(e: WindowEvent): Unit = {
      newDialog()
    }
  })
}

newDialog()

f:id:Nomad_Blacky:20191206000910g:plain

こうしてAmmoniteは自分のScala生活に欠かせないものになっていきました。

気になった人は以下のコマンドを実行して早速インストール! (執筆時点の最新版)

$ sudo sh -c '(echo "#!/usr/bin/env sh" && curl -L https://github.com/lihaoyi/Ammonite/releases/download/1.8.2/2.13-1.8.2) > /usr/local/bin/amm && chmod +x /usr/local/bin/amm' && amm

Ammoniteに関してはいくつか資料を公開していますので、そちらもご覧いただけると幸いです。

upickle

JSON, MessagePack に対応したシリアライゼーションライブラリ。
Ammoniteの依存に含まれるため、上記のコマンドでインストールした方はすぐお試しできます。

@ import upickle.default._
import upickle.default._

@ case class Post(id: Int, name: String, tags: Seq[String])
defined class Post

@ implicit val postRW: ReadWriter[Post] = macroRW
postRW: ReadWriter[Post] = upickle.core.Types$ReadWriter$$anon$3@73fb1d7f

@ val json = """
    [
      {"id":1, "name":"post1", "tags":["Java"]},
      {"id":2, "name":"post2", "tags":["Scala", "Ammonite"]}
    ]
    """
json: String = """
  [
    {"id":1, "name":"post1", "tags":["Java"]},
    {"id":2, "name":"post2", "tags":["Scala", "Ammonite"]}
  ]
  """

@ val posts = read[Seq[Post]](json)
posts: Seq[Post] = Vector(Post(1, "post1", Vector("Java")), Post(2, "post2", Vector("Scala", "Ammonite")))

@ write(posts)
res5: String = "[{\"id\":1,\"name\":\"post1\",\"tags\":[\"Java\"]},{\"id\":2,\"name\":\"post2\",\"tags\":[\"Scala\",\"Ammonite\"]}]"

macroによって、最低限のボイラープレートでJSONの読み書きを実現できていますね。

他のJSONライブラリと比べて好きなところとしては、自身以外のライブラリに依存していないところでしょうか。

$ coursier resolve --tree com.lihaoyi::upickle:0.8.0
  Result:
└─ com.lihaoyi:upickle_2.13:0.8.0
   ├─ com.lihaoyi:ujson_2.13:0.8.0
   │  └─ com.lihaoyi:upickle-core_2.13:0.8.0
   │     └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
   │        └─ org.scala-lang:scala-library:2.13.0
   ├─ com.lihaoyi:upack_2.13:0.8.0
   │  └─ com.lihaoyi:upickle-core_2.13:0.8.0
   │     └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
   │        └─ org.scala-lang:scala-library:2.13.0
   └─ com.lihaoyi:upickle-implicits_2.13:0.8.0
      └─ com.lihaoyi:upickle-core_2.13:0.8.0
         └─ org.scala-lang.modules:scala-collection-compat_2.13:2.0.0
            └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree com.typesafe.play::play-json:2.7.4
  Result:
└─ com.typesafe.play:play-json_2.13:2.7.4
   ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.8
   ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │     ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │     └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │  ├─ com.fasterxml.jackson.core:jackson-core:2.9.8
   │  └─ com.fasterxml.jackson.core:jackson-databind:2.9.8
   │     ├─ com.fasterxml.jackson.core:jackson-annotations:2.9.0 -> 2.9.8
   │     └─ com.fasterxml.jackson.core:jackson-core:2.9.8
   ├─ com.typesafe.play:play-functional_2.13:2.7.4
   │  └─ org.scala-lang:scala-library:2.13.0
   ├─ joda-time:joda-time:2.10.1
   ├─ org.scala-lang:scala-library:2.13.0
   └─ org.scala-lang:scala-reflect:2.13.0
      └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree org.json4s::json4s-native:3.6.7
  Result:
└─ org.json4s:json4s-native_2.13:3.6.7
   ├─ org.json4s:json4s-core_2.13:3.6.7
   │  ├─ com.thoughtworks.paranamer:paranamer:2.8
   │  ├─ org.json4s:json4s-ast_2.13:3.6.7
   │  │  └─ org.scala-lang:scala-library:2.13.0
   │  ├─ org.json4s:json4s-scalap_2.13:3.6.7
   │  │  └─ org.scala-lang:scala-library:2.13.0
   │  └─ org.scala-lang:scala-library:2.13.0
   └─ org.scala-lang:scala-library:2.13.0

$ coursier resolve --tree io.circe::circe-core:0.11.1
  Result:
└─ io.circe:circe-core_2.12:0.11.1
   ├─ io.circe:circe-numbers_2.12:0.11.1
   │  └─ org.scala-lang:scala-library:2.12.8
   ├─ org.scala-lang:scala-library:2.12.8
   └─ org.typelevel:cats-core_2.12:1.5.0
      ├─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      ├─ org.typelevel:cats-kernel_2.12:1.5.0
      │  └─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      ├─ org.typelevel:cats-macros_2.12:1.5.0
      │  ├─ org.scala-lang:scala-library:2.12.7 -> 2.12.8
      │  └─ org.typelevel:machinist_2.12:0.6.6
      │     ├─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
      │     └─ org.scala-lang:scala-reflect:2.12.6
      │        └─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
      └─ org.typelevel:machinist_2.12:0.6.6
         ├─ org.scala-lang:scala-library:2.12.6 -> 2.12.8
         └─ org.scala-lang:scala-reflect:2.12.6
            └─ org.scala-lang:scala-library:2.12.6 -> 2.12.8

Scala.js対応している点も人によっては嬉しいかもしれません。

あと、他のJSONライブラリと比較してパフォーマンスが良いそうですが、自身で試してないので深くは言及しません。

requests

とてもシンプルなHTTPクライアント。
PythonのRequestsから影響を受けているみたいです。

これもAmmoniteの依存に含まれます。

@ val response = requests.get("https://google.com")
response: requests.Response = Response(
  "https://www.google.com/",
  200,
  "OK",
  Map(
    "expires" -> Buffer("-1"),
    "server" -> Buffer("gws"),
    "p3p" -> Buffer("CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\""),
    "x-xss-protection" -> Buffer("0"),
    "cache-control" -> Buffer("private, max-age=0"),
    "date" -> Buffer("Thu, 05 Dec 2019 12:54:14 GMT"),
    "content-type" -> Buffer("text/html; charset=ISO-8859-1"),
    "transfer-encoding" -> Buffer("chunked"),
    "x-frame-options" -> Buffer("SAMEORIGIN"),
    "alt-svc" -> Buffer(
      "quic=\":443\"; ma=2592000; v=\"46,43\",h3-Q050=\":443\"; ma=2592000,h3-Q049=\":443\"; ma=2592000,h3-Q048=\":443\...

@ response.text.take(100)
res1: String = "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/WebPage\" lang=\"ja\"><head><meta content"

import文なしにそのまま書ける点が地味ながらすき。

前述のupickleと合わせればWebAPIを叩くスクリプトも組めますね。

sbt-explicit-dependencies

コンパイル時に不要なライブラリや、暗黙的に依存しているライブラリを見つけてくれるsbtプラグインです。

ここでの「暗黙的に依存している」とは、ライブラリAが依存するライブラリBをアプリケーションコードが直接利用していることを指します。 (例: Circeの依存を追加して、ソースコード上でcatsを利用している)

↓インストール方法↓

// 執筆時点の最新版
addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.11")

たとえば、以下のようなプロジェクトがあったとします。

// build.sbt
lazy val root = (project in file("."))
  .settings(
    name := "explicitdeps",
    libraryDependencies ++= Seq(
      "com.lihaoyi" % "ammonite" % "1.8.2" cross CrossVersion.full
    )
  )
// Hello.scala
object Hello extends App {
  println(ujson.Obj("key" -> "value", "foo" -> "bar").render(indent = 2))
}

sbtを起動して、 unusedCompileDependencies, undeclaredCompileDependencies を実行します。

sbt:explicitdeps> unusedCompileDependencies
[warn] explicitdeps >>> The following libraries are declared in libraryDependencies but are not needed for compilation:
[warn]  - "com.lihaoyi" % "ammonite" % "1.8.2"
[success] Total time: 0 s, completed Dec 5, 2019 11:28:04 PM

sbt:explicitdeps> undeclaredCompileDependencies
[warn] explicitdeps >>> The project depends on the following libraries for compilation but they are not declared in libraryDependencies:
[warn]  - "com.lihaoyi" %% "ujson" % "0.8.0"
[success] Total time: 0 s, completed Dec 5, 2019 11:28:17 PM

前者でライブラリ依存に加えているのに使われていない ammonite の依存が検出され、
後者でアプリケーションコードで使われているのに明示的に依存していない ujson が検出されました。

これで、闇のライブラリ依存に対する防衛術が手に入りましたね。べんり!

不正な依存があった場合にエラーとなる unusedCompileDependenciesTest, undeclaredCompileDependencies コマンドもあるので、CIに組み込むといい感じになります。

例えば、GitHub Actionsで使う場合はこんな感じ

check_unused_compile_dependencies:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: '1.8'
    - name: Check unused compile dependencies
      run: sbt test/unusedCompileDependenciesTest

check_undeclared_compile_dependencies:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v1
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: '1.8'
    - name: Check undeclared compile dependencies
      run: sbt test/undeclaredCompileDependenciesTest

プロジェクト導入初期からぜひ入れたいプラグインですね!

scala-steward

依存ライブラリのアップデートを探してリポジトリにプルリクエストを投げてくれるとてもかしこいbot
似たものとしてはdependabotなどがあります。
(こちらはsbtに非対応)

自分で動かすことも可能ですが、今回はコミュニティで定期的に動かしてくれているものを使いましょう。

このファイルに自身が公開しているGitHubリポジトリを加えて、プルリクエストを出します。

f:id:Nomad_Blacky:20191205235415p:plain

そのうち筆者のfthomasさんがマージしてくれます。(感謝)

すると、1日1回ぐらい(?)の頻度でライブラリのアップデートを確認して、自身のリポジトリプルリクエストを出してくれます。

f:id:Nomad_Blacky:20191205235445p:plain

f:id:Nomad_Blacky:20191205235548p:plain

自分でライブラリのアップデートを確認する作業は骨が折れるので、とても便利ですね!
便利と感じたら、ぜひ感謝の気持ちとしてBadgeを付けましょう。

f:id:Nomad_Blacky:20191205235626p:plain

まとめ

Scalaすき…

関数型ニキ達にインスパイアされた最近の Scala 開発の取り組み

こんにちは、中途三年目の堀越です。

近頃、Scalaのコミュニティにおいて Functional Programming による実装テクニックを紹介する記事や発表を見たり聞いたりすることは珍しいことではなくなってきました。弊社にもたくさんの関数型ニキ*1が在籍しており、わたしも日々影響を受けています。

ということで、本日はわたしが所属するチームでの日々の Scala 開発における取組みや戦略をサンプルコード*2と合わせて紹介していきます。

高カインド型によるEffect型の抽象化

私達はドメイン駆動設計を実践しています。なのでドメインロジックはドメインの関心事に集中できるのが理想です。ドメイン層を抽象化し、特定の実行環境や技術的関心事に依存しない戦略として 高カインド型 を用いてEffect型を抽象化します。

*1:関数型つよつよお兄さん

*2:import 文とか一部端折ってる部分あり。

続きを読む

Scala秋祭り開催しました!

こんにちは。セプテーニ・オリジナルの池田です。

先日サイバーエージェントさん、ビズリーチさん、チャットワークさん、そしてセプテーニ・オリジナルの4社で、「Scala秋祭り」を開催しました!!

f:id:taketor:20190917175728p:plain

【増席】Scala秋祭り - connpass

今年6月に開催されたScalaMatsuri2019にて、各社ブースが近く一緒にイベントしましょうとのことで話しをしまして、今回開催する運びになりました。

続きを読む

業務十倍効率化計画 Sentry API ft. Shell Script

みなさんこんにちは、2 年目を迎えました清水です。Scala 全然書かずに TypeScript やら Shell Script ばっか書いてます。今回はエラートラッキングツール Sentry の API を使って業務を効率化したよという内容で書かせていただきました。

概要をざっくりと説明すると、Sentry API と Shell Script でこれまで 100 分くらいかかっていたミーティングが 10 分にまで短縮できたよというような内容です(Sentry について全然知らなくても読めます)。

ちなみに、GitLab API で業務改善した同じ毛色のブログも書いてますので、この記事を読んでみて面白いと思ったらぜひ読んでみてください(http://labs.septeni.co.jp/entry/2019/03/05/120000)。

続きを読む