出力を入力へ

プログラミングに関する自分が考えた事を中心にまとめます

SimpleCovを並列実行した結果をマージする

CircleCI実践入門で紹介されていたRubyアプリケーションのCI事例について、 SimpleCovでカバレッジを計測するとき、CircleCIを並列実行すると結果が分かれてしまうので 並列実行した結果をマージする必要がある。 このマージの実装が古くなっており動作しなかったのでその修正。

マージエラー

サンプルとなるコードはこちらのリポジトリで公開されている。 ただし、この設定で単にCIを実行してもエラーで動作しない。

原因は mimemagicのライセンス問題により過去のバージョンのgemが非公開されたこと。 これにより bundle installが失敗するようになった。

...
Install missing gems with `bundle install`
Fetching gem metadata from https://rubygems.org/............
Your bundle is locked to mimemagic (0.3.5), but that version could not be found
in any of the sources listed in your Gemfile. If you haven't changed sources,
that means the author of mimemagic (0.3.5) has removed it. You'll need to update
your bundle to a version other than mimemagic (0.3.5) that hasn't been removed
in order to install.
...

ちなみに、mimemagicのライセンス問題は以下が詳しい。

hackmd.io

では単に bundle updateしてgemを更新すればよいかというと、それでも上手くいかない。 当然アプリは動作するようになり、rspecも正常に動作するが、 標記のSimpleCovの並列実行結果をマージする処理で失敗する。

#!/bin/bash -eo pipefail
bundle exec rake simplecov:merge_results

rake aborted!
TypeError: no implicit conversion of Array into String
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/simplecov-0.21.2/lib/simplecov/result_merger.rb:57:in `exist?'
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/simplecov-0.21.2/lib/simplecov/result_merger.rb:57:in `read_file'
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/simplecov-0.21.2/lib/simplecov/result_merger.rb:52:in `parse_file'
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/simplecov-0.21.2/lib/simplecov/result_merger.rb:47:in `valid_results'
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/simplecov-0.21.2/lib/simplecov/result_merger.rb:37:in `merge_results'
/home/circleci/project/lib/tasks/simplecov.rake:14:in `store_result'
/home/circleci/project/lib/tasks/simplecov.rake:9:in `block (2 levels) in <main>'
/home/circleci/project/vendor/bundle/ruby/2.6.0/gems/rake-13.0.3/exe/rake:27:in `<top (required)>'
/usr/local/bin/bundle:23:in `load'
/usr/local/bin/bundle:23:in `<main>'
Tasks: TOP => simplecov:merge_results
(See full trace by running task with --trace)
Coverage report generated for RSpec to /home/circleci/project/coverage. 0 / 33 LOC (0.0%) covered.
Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected

Exited with code exit status 1
CircleCI received exit code 1

原因は SimpleCov 0.19.0より SimpleCov::Result.from_hashの挙動が変更になったことである。 このため、マージ処理の実装の修正が必要である。

github.com

解決策1: SimpleCovのバージョンを古いままにする

一番わかりやすい解決策としてSimpleCovのバージョンを0.18のままにすればよい。 以下の通り修正した上で bundle updateすればmimemagicの問題を解決しつつSimpleCovのカバレッジ結果のマージも問題なく動作する。

--- a/Gemfile
+++ b/Gemfile
@@ -52,7 +52,7 @@ group :test do
   gem 'selenium-webdriver'
   # Easy installation and use of web drivers to run system tests with browsers
   gem 'webdrivers'
-  gem 'simplecov', require: false
+  gem 'simplecov', '< 0.19.0', require: false
   gem 'rspec_junit_formatter'
 end

ただし、当然SimpleCovのアップデートができなくなるので、できれば別の方法で解決したい。

解決策2: Simplecov.collateでマージする

公式のREADMEにも並列環境での結果をマージする方法について記載がある。 公式には Simplecov.collateメソッドを利用する方法が紹介されている。 すなわち、自分でマージ処理を実装する必要はない。 (Simplecov.collate 自体は 0.18.0から実装されていた)

github.com

require "simplecov"

namespace :simplecov do
  desc "merge_results"
  task merge_results: :environment do
    SimpleCov.start 'rails' do
      merge_timeout(3600)
    end

    SimpleCov.collate Dir["coverage_results/.resultset-*.json"]
  end
end

これにより、最新のSimpleCovを利用しつつ、マージ処理も簡潔に実装できる。

railsアプリでdatadog logレベルをhttpのステータスコード依存にする

datadogに転送したアクセスログのログレベルを変更したいだけだったのに かなり時間を取られたのでそのまとめ。

やりたいこと

現在、railsアプリのログをdatadogに転送して可視化しているが、 このうち、webアクセスログの内容に応じてログレベルを変更したい。

現在はrailsログのうち例外等はエラーログとして扱ってくれるが、 webアクセスログは500系エラーであってもINFOレベルになってしまう。 200系300系はOKレベル、400系はWARNレベル、500系はERRORレベルに設定したい。

解決策

当然ながらdatadog logs pipelineで設定すればよい。 基本的には公式FAQのHow to remap custom severity values to the official log status? に従えばよい。

注意が必要なのは、 category processor で http.status_code ではなく payload.status を参照すること、 nested pipelien が必要なこと。 この解決に時間が掛かり、datadogサポートを通して何度もやりとりを行う必要が出てしまった。

はまったポイント

railsログは http.status_codeを持たない

自分たちのrailsアプリでは Semantic Logger を利用してログの構造化を行っている。 少なくともこのロガーの場合、アクセスログのHTTPステータスコードが payload.statusに記載される。 このため、 datadog logs configuration では http.status_code ではなく payload.status を参照する必要があったが これを認識できておらず、存在しない http.status_codeを参照しようとして失敗していた。

なぜ http.status_code を参照しようとしていたかというと、 datadogでは standard attribute という機能があり、 すべてのアクセスログは透過的に http.status_codeで参照できるような仕組みがあるからである。 この機能を利用していたため、無意識に http.status_codeにHTTPステータスコードがあるものだと思い込んでいた。 実際、以下のような設定が有効になっており、 http.status_code を参照すればよい状況だった。

f:id:thaim:20210403200742p:plain

standard attribute機能は pipeline を通過したログに対して payload.status の値を http.status_codeにコピーする。 ログ分析画面やconfigurationのpipeline preview画面では pipeline通過後のログを参照するため http.status_code が参照できるが pipelineの設定ではまだ属性がコピーされていないので参照できない、という仕組みだった。 結果として、以下のように payload.status を参照して処理するように設定すればよい。

f:id:thaim:20210403201035p:plain f:id:thaim:20210403201107p:plain

http status codeを持たないログが含まれている

Webのアクセスログ(rack log)は http status codeを持つが、 自分で埋め込んだログなどはhttp status codeを持たない。 このため、すべてのログが http status codeを持つという仮定は誤りである。

解決策としては、 nested pipeline によりhttp status codeを持つログを絞り込む必要がある。 これにより、status codeを持つログのみ処理対象とし、status codeを持たないログはログレベルを他の方法で決定することができる。 自分の場合は基本的に level という属性にログレベルを格納しており、datadogもデフォルトで level属性を参照してくれるので上手く動作する。 nested pipelineを用いたログのフィルタイングは以下の通り。

f:id:thaim:20210403195503p:plain

始めはstatus codeを持たないログが処理できずエラーになることを認識しておらず nested pipelineを作らなかった。 このため、pipelineが処理できる場合と処理できない場合が混在していた。 datadog ではpipelineが正常にログを処理できたか、エラーが発生したかを知る術はないため、これに気付くことができない。 このあたりdatadogにどうにかして欲しいところ。

TerraformでAWS ELBリスナーに登録されているターゲットグループを再生成する

やっとのことALBロードバランサをTerraform管理しようとしたら 設定変更や再生成まわりで時間を取られたのでそのまとめ。

ターゲットグループの設定変更の問題

AWSロードバランサにおけるターゲットグループがリスナーとしてロードバランサに登録されていると、 一度リスナー登録を削除しない限りターゲットグループを削除できない。 ターゲットグループの基本的な設定として、ターゲットの種類やプロトコル、ポートなどは AWSの仕様上、設定変更できないので再作成し直す必要がある。

特にTerraformでロードバランサを管理していると (もちろん基本的な設定を変更する機会というのはほとんどないのだが) ターゲットグループを変更したくても変更できず手詰まりになる。 具体的には以下のようなエラーログが出力される。

Error: Error deleting Target Group: ResourceInUse: Target group 'arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxx' is currently in use by a listener or a rule
    status code: 400, request id: 65dda169-3495-4d43-8aab-70721d2122ea

このときだけ手作業で修正するというのも1つの手ではあるけれど、 できればTerraformでもいい感じに対応したい。

ちなみに、以降の解決策は issueに記載の解決策そのままであるが ぴんとこなかったので追試した内容であり、同じ結論に辿りついている。

解決ステップ1: create_before_destroy

上記のようなリソースの依存関係により削除できないケースは ターゲットグループにも複数存在する。 こういったケースでの主な解決策は lifecycle で create_before_destroy を指定する方法である。

この挙動について、公式ドキュメント に全部記載されているのだが、 通常はリソースを削除してから作成するのだが、このオプションを有効にすることで 先にリソースを作成してから古いリソースを削除する。 具体的には以下のように設定する。

resource "aws_lb_target_group" "sample" {
  name = "sample-alb-target-group"

  port = 80
  protocol = "HTTP"
  target_type = "ip"
  vpc_id = var.vpc_id

  lifecycle {
    create_before_destroy = true
  }
}

ただし、この方法には注意点があり、これも公式ドキュメントに記載さているのだが、 同一の名前でリソースが作成できない場合に上手く動作しない。 ターゲットグループは名前がユニークである必要があるので、 プロトコルやポートを変更しようとして create_before_destroy を有効にしていても 同名のリソースが作成できないのでエラーになる。

Error: Error creating LB Target Group: DuplicateTargetGroupName: A target group with the same name 'sample-alb-target-group' exists, but with different settings
    status code: 400, request id: 29d9468f-6665-4b7f-b404-3f12ca0fb8b3

解決ステップ2: name_prefixを利用する

ではこのような課題にどう対処すればよいかというと、 名前の先頭だけ指定し、末尾に名前がユニークになるようなランダムな文字列を付与すればよい。 terraformがこのような動作をサポートしており、nameの替わりにname_prefixを指定することで 勝手に名前がユニークとなることが保証される。

resource "aws_lb_target_group" "sample" {
  name_prefix = "sample-alb-target-group"

  port = 80
  protocol = "HTTP"
  target_type = "ip"
  vpc_id = var.vpc_id

  lifecycle {
    create_before_destroy = true
  }
}

しかし、この方法では ターゲットグループの場合のみ上手くいかない場合があり、 それは name_prefix は6文字以下にする必要があるという制限である。 これは ドキュメント にも記載がある。

"name_prefix" cannot be longer than 6 characters

Cannot be longer than 6 characters.

しかも、ターゲットグループには descriptionのように説明を記載する項目がないので、 名前を6文字に制限するとそのターゲットグループが何なのか一目で判断するのが難しくなる。 そのためのterraformコード管理ではあるのだが、できればAWSリソースの名前でも目的等が 上手く識別できるように命名したい。

実際、GitHubでも名前が6文字以下では役に立たないよね、という趣旨のissueが登録されている。

解決ステップ3: 自前でname_prefix相当機能を実装する

name_prefix では解決できないとわかったので、 自前で name_prefix 相当の機能を実装することで回避する。 具体的には 名前の末尾にランダムな文字列を付与する。

resource "aws_lb_target_group" "sample" {
  name = "sample-alb-target-group-${substr(uuid(), 0, 6)}"

  port = 80
  protocol = "HTTP"
  target_type = "ip"
  vpc_id = var.vpc_id

  lifecycle {
    create_before_destroy = true
  }
}

上記の例ではUUIDで生成した文字列の先頭6文字をターゲットグループの名前の末尾に追記している。 もちろん、このランダムな文字列6文字が偶然重複するとエラーになるのだが、 その可能性はかなり低いし心配なら文字数を増やせばよい。 AWSの仕様上、名前は最大32文字までらしいので、 32文字になる最大の文字数だけ付与すればよい (上記例では sample-alb-target-group- が24文字なので8文字のランダムな文字列を付与できる)。

なお、名前にランダムな文字列を付与しようとすると terraform planを実行する度に差分が発生してしまうので、 この差分は無視する必要がある。 具体的には、lifecycleで名前を ignore_changes に追加すればよい。

resource "aws_lb_target_group" "sample" {
  name = "sample-alb-target-group-${substr(uuid(), 0, 6)}"

  port = 80
  protocol = "HTTP"
  target_type = "ip"
  vpc_id = var.vpc_id

  lifecycle {
    create_before_destroy = true
    ignore_changes = [name]
  }
}

以上により、ターゲットグループがリスナーとして登録されている場合に 設定変更で再生成が必要でも上手く対処できるようになった。

2020年のふりかえりと2021年の抱負

2020年のふりかえり

2020年は転職して(転職自体は2019年だけど)さまざまな経験を積めた1年だった。 コロナ過によるフルリモート体制への移行やこれに伴う開発・コミュニケーション方法の試行錯誤など なかなか上手くいかない点も多かったけど、それも含めていろいろな学びがあった。

コロナ過による生活の大きな変化のあるなかで、 運動機会が減ったこと、そもそも外出が減ったことは健康面であまりよろしくなかった。 まだコロナが終息する気配はないので、できることは限られているけれど できる範囲で改善していきたい。

できたこと

本番サービスの構築・運用

転職の主目的であった本番サービスの構築と運用に深く携われることは非常によかった。 特にスタートアップにありがちと言われている事業の拡大と開発の混乱を経験できたことは 今後の技術者としてのキャリアでも大きく活かしていくことができそう。

運用上必要だけど価値を生まないトイルな作業もまだまだたくさんあって、 トイルの撲滅はまだまだ道半ばであまり抜本的な改善にまで着手できなかったことは反省点。

ログ・リソースの可視化

インフラの改善の中で一番インパクトが大きかったのがログやサーバリソースの可視化。 従来はログ基盤が整備されておらずログの可視化が上手くできていなかったので、この改善が急務だった。 railsアプリケーションログの出力を構造化(json化)し、ログ分析基盤への投入・可視化を実現することで 開発者のだれもが簡単にログを参照できるようになったのはインパクトのある改善だった。

まだまだ十分なログ出力できないケースやログの監視が十分ではないので 2021年も引き続き取組まなくてはいけないけれど、 はじめの1歩としては大きな効果を上げることができた。

IaCの実践

terraformやansibleを用いた Infrastructure as Codeの実践も インフラの開発・管理の面では大きな効果があった。 従来の最新にアップデートされていないインフラの構築手順書を破棄し、 コードだけでインフラが構築できるようになったのはインフラの品質面でも管理面でも大きな効果があった。

一方でansibleやterraformを利用できる人が少なく、インフラ構築作業の属人化が進んでしまったので チームのインフラ技術力の強化という意味でも、だれでもansibleやterraformを触れるように推進して行きたい。

コンテナの導入

新規開発したサービスのインフラにDockerおよびECSを導入することができた。 継続的な改善が必要であること、どれだけ利用されるかわからないこと、 研究チームの成果なのでインフラを意識せずにリリースできるようにしたいといった要件から オートスケールなECS基盤を採用して構築・リリースした。 これらの取組みは非常によかったと思う。

転職前からDockerは積極的に利用していたのでその知見が活かせて技術展開に取組めたことはよかった。 一方でECSのオートスケールについてはもう少し改善したいし、 メインのwebアプリへのコンテナ技術の導入はこれからなので、まだまだ課題は残る。 引き続きコンテナ化やコンテナ技術の共有は進めていきたい。

インフラのセキュア化

地味だしあまり成果を外部に出せない部分ではあるけれど インフラのセキュリティ向上に向けた取組みも、内部的には大きな意味を持つ内容ではあった。 初めてインフラの構成を見たときにはびっくりするような状態だったけど もうそろそろ外に公開しても恥かしくない構成になったのかなとは思う。

セキュリティは今まで積極的に取り組んだことはなかったので、なんとなくの理解の分野が多かったけど、 実際にシステムをセキュアにするために取組むことで、改めて学び直すことも多かった。

OKRやアジャイルへの取組み

技術面ではないけれど、会社としてOKRやアジャイル開発の取組みを本格化した1年であり、 これを踏まえていろいろと勉強した1年だった。 特にOKRは自分としても初めての取組みで、いろいろ勉強しながらではあった。 その甲斐あって、一部メンバのみではあるけれどツールの導入や振り返りを実践するようになり、 社内でのOKRの理解推進に一役買うことができたかなと思う。

自分が1人インフラチームで開発に取り組んでいて アジャイル開発の観点からは課題が多かったり、1on1などは上手く実践できていないなどの課題もあるので もう少し自分たちにあったやり方を模索していきたい。

できなかったこと

成果のアウトプット

一番心残りなのが成果を社外にアウトプットできなかったこと。 これはセキュリティ面の取組みのように、社外に公開しにくい内容も多かったので 単純にアウトプットに適さなかったという面もあるけれど、 日々の開発に追われてアウトプットをサボってしまったという面も強い。

社内への知見共有として、社内記事としてのアウトプットはなんとか確保できたけれど もう少し社外へのアウトプットも取組みたかった。 なんとか会社のテックブログも始まったけど、あまり他の人はアウトプットに積極的ではなく 上手く運用できるか難しいので、少なくとも個人レベルでは社外アウトプットを強化したい。

VRの知見の深化

せっかくVR技術という面白い分野を扱っているのにもかかわらず、 このあたりの技術の理解を進めることができなかった。

もちろん、VR技術にもいろいろあって、自分たちが扱うVR技術はそのほんの一部でしかないのだけれど 自分たちが扱うVRにも、その周辺技術にも、世の中のトレンドとしてのVR技術にも ほとんど触れる機会がなかった。 インフラとの間で上手く抽象化されてアプリケーションを考慮する必要がないといえばその通りだけれど、 やはり自分たちが扱う技術くらいはきちんと把握しておきたい。

機械学習

機械学習を用いた画像処理なんかは、まさに自分が学生のときに取り組んでいたテーマであり、 さらには機械学習を用いたシステムの構築の機会があったにもかかわらず その理論や実装をほとんど深掘りすることができなかった。 また、MLOpsといった自分にぴったりなテーマもあるにもかかわらず これらを実践することができなかった。

今後機械学習システムの運用は対応が必須になってくるし、 研究チームとのやりとりも、むしろwebチームより頻度高く行っているくらいなので、 ぜひともこのあたりの知見は強化していきたい。

論文

学会レベルでの最新技術動向の収集も大きく遅れてしまった。 論文を読むどころか学会誌すら確認できておらず、学会のスケジュール把握もままならない状況。 コロナ過で無料でカンファレンスを公開しているケースも多かったみたいだけれど、それらを活かすことなくスルーしてしまった。

転職前から投稿はできていなかったので、論文投稿まで行うことは想定していなかったけど せっかく研究出身なので、自分の扱う分野くらいは動向ウォッチしていきたい。

競技プログラミング

これは業務で扱う技術ではないけれど、 趣味としての競技プログラミングもあまり時間を割いて取組むことができなかった。 年末に半年ぶりの復帰をしたのはいいけれど、当然普段の精進も行っていなかったので レートは落ちる一方だった。

世の中のレベルの上昇もあるので最高レート更新は難しいかもだけど、 なんとか現状のレートを維持したかった。

2021年の抱負

2020年にできなかったことをやるのはもちろん。 それに加え、2020年は目の前のタスクに追われていたので、 2021年は計画的・戦略的にやることを進めていきたい。

抱負としては盛り沢山の欲張りセットになったけど 今年もいろいろと取り組んでいきたい。

成果のアウトプット

まずは2020年にできなかった・一番の心残りであった成果のアウトプットについて取り組んでいきたい。 具体的にはブログ記事(またQiita/Zenn記事)の執筆、OSSへのコントリビュート、カンファレンス発表に取組みたい。 特にブログ記事を中心に、日々取り組んだことをアウトプットしたい。

また2021年は2020年に取り組んだ内容の深化・高度化を図っていきたい。 その中でOSSの積極的な活用を通してコントリビュートしたり、 ユーザカンファレンス等で取組みをアウトプットできるようにしたい。

運用・監視の深化

社内唯一のインフラエンジニアとして、SREの取組みをしている身として、 運用は自分がメインで取り組んでいる内容なのでここをしっかりと頑張りたい。 2020年に自分のシステム運用の取組みが始まったばかりなので、この高度化に取り組んでいきたい。 障害まわりの対応はもちろん、リソースの監視や脆弱性まわりの対応など、 やることはいろいろとあるけれど、1つ1つ深掘りしていきたい。

一方で1人で運用するのは限界があるので 自分がボトルネックにならないよう、燃え尽きないようにできる範囲で頑張りたい。 インフラの構築まわりも当然に求められるので、 開発の遅れにならないよう、なんでも抱え込まないように気をつけたい。

DB知見の深化

運用・監視を行う上で対象の知見が一番不足しているのがDBであり DBの知見を特に深めていきたい。 DBとしてはRDBMSにMySQL、NoSQLにRedisとMongoDBを利用しているので どれも欠けることなく知見を蓄積していく必要がある。

MySQLはパフォーマンスの問題が発生しているので SQLを中心としたパフォーマンスチューニングできるように知見を蓄積したい。 Redisはシステムがレガシー化しつつあるので、 最新バージョンへの追従に向けた知見を蓄積したい。 MongoDBは機械学習などで活用が見えているので、 新しいシステムを構築するために必要な知見や、機械学習とどのように使っていくか、知見を深めていきたい。

スケーラブルなインフラ

ユーザビリティの向上やコスト最適化に向けてスケーラブルなインフラを実現したい。 オートスケールはECSまわりの新しいシステムでしか実現できていないので、 既存のシステムにおけるオートスケールも実現したい。

これを実現するためにはメインのwebアプリもコンテナ化したり、 そもそもオートスケールを実現するために監視の仕組みを構築しないといけない。 これが単にリソースの利用状況を見るだけでなく、 アプリの仕様を踏まえた監視の仕組みを作り込む必要がある。 redisまわりの仕組みもきちんと理解する必要があるので 監視やDBの知見深化と合わせて取り組んでいく。

CI/CDの強化

CI/CDは転職前は自分の好きな技術領域としていろいろ取り組んでいたはずが 現職ではなぜか技術を発揮することなく放置されている残念なところとなっている。 特にCDまわりの仕組みは自社の弱いところなので、このあたりを整備していきたい。

メインで利用しているCircleCIに加え、 利用を開始したGitHub Actionsや今後利用を想定するCodeBuildといった 各種CIサービスについての知見を蓄積することも必要。 それ以上にCIを上手く活用するために各種静的解析の導入やCIの高速化、 およびCDとしてのリリースの高度化のための知識を取り入れていきたい。 インフラのCIなんかもできていないのでやりたいところ。

マネジメント

OKRや1on1およびアジャイルなど 仕組みについては広く公開されているものの、組織によって適切なやり方が異なるものについて どうすればの適切な方法になるか取組みを模索していく必要がある。 特にリードエンジニアの立場として、自分の作業を効率化するだけでなく 組織自体の強化と組織全体の効率化ができるよう 若手を引っ張って・盛り上げていきたい。

正直なところ、ピープルマネジメントは苦手な分野ではあるけれど そういった苦手な面も積極的に取組みたい。

フロントエンド技術入門

ここまで挙げてきたテーマは自分の得意分野を伸ばす取組みだけど 苦手な分野の強化としてフロントエンドまわりの基礎を身に付けたい。 自社で採用しているVue.jsの知見を深めるのはもちろん、 TypeScriptやNext.jsのような新しい技術についても、 少なくとも世の中の流れが把握できる程度には理解を深めていきたい。

また、VRにおけるフロントエンドは面白い分野だと思うので 自分が新しい取組みを行うまではいかなくても、会社の取組みに取り残されないようにしたい。

Terraform Cloudをterraformで管理する

Terraform CloudにおけるWorkspaceの管理

Terraform Cloudはtfstateの管理だけでなくterraformの実行を管理してくれる. このため,自前でTerraformのCI環境を構築する必要なしに簡単にTerraformを導入できる.

一方で,tfstateの分割単位であるworkspaceが増えるとその管理が大変になる. 具体的にはworkspace間で共通の変数(AWSのクレデンシャル情報など)の管理などが煩雑である. CircleCIのコンテキストやGitHub ActionsのOrganization Secretsのように 組織内で共通して変数を参照する仕組みがないので, workspaceごと変数を設定する必要がある. このため,AWSのクレデンシャル情報をローテートするときには, workspaceの数だけ人手で更新する必要があり,かなり面倒である.

Terraform Cloud 管理の自動化

人手で管理するのは面倒なので自動化したい. Terraform CloudにはAPIが提供されており,HashiCorpが公式にメンテするSDKとして go-tfeがある. このSDKやAPIを用いたCLIも多数存在する.

workspaceの変数管理という単目的であれば上記のCLIを利用すれば済む話ではあるが, 変数管理だけでなく通知の設定や 利用するTerraformのバージョンなども管理したくなる. まさに,Terraform Cloudの IaCがやりたい.

こうなるとCLIで操作するだけでは不十分となり, Terraform CloudをTerraformで管理したくなる.

Terraform CloudをTerraformで管理する

Terraform CloudはTerraform Enterpriseのマネージドサービスであり, Terraform Enterprise Providerが利用できる. これを利用してTerraform Cloud上のリソースをTerraformで管理する.

主な注意事項は以下の通り.

認証トークンの設定

Terraform Enterprise Providerにおける認証トークンとしては, ユーザートークンなどが利用できるが, 今回のように複数workspaceを管理するためにはチームAPIトークンの利用がよさそう. ユーザートークンや組織トークンとはアクセスレベルが大きく異なるので目的に応じて検討が必要.

チームトークンはOrganization SettingsのTeamsから発行できる.

f:id:thaim:20201025170239p:plain
チームトークンの発行

発行したトークンは tfeプロバイダに設定する.

provider "tfe" {
  token = var.token
}

variable "token" {}

リソースのインポート

多くの場合 workspaceのリソースなどは既に作成されていると思うので, これをterraform管理下におくためにインポートする. WorkspaceのインポートにはIDを指定する. IDはWorkspaceのGeneral Settingsから確認できる.

Terraform Cloudでリソースのインポートを行う場合, 特にRemote Execution Modeを利用している場合でもインポートの処理はローカルで動作する点に注意. すなわち,Terraform変数や環境変数はローカルでも設定しておく必要がある.

WorkspaceのVCS設定

WorkspaceのVCS連携を利用することで リポジトリを契機としたTerraformの実行や,プルリク画面でのplan結果の表示などができるようになる. このVCS連携は複数の選択肢があり,通常はあまり気にすることがないものの, Terraform管理しようとすると考慮する必要がある. 結論から言うと personal access token を利用する方法のみが上手くいく.

多くの人が利用するであろうGitHub.comとの連携については Configuration-Free GitHubがある. これは Terraform Cloud GitHub App を利用する方法で,細かい認証の設定なしにGitHub.comとTerraform Cloudを連携できる. この方法は通常Terraform Cloudを利用する場合の方法である一方で,tfe プロバイダを用いたterraform管理にはこの方法は利用できない.

もう1つの方法として OAuth App を登録する方法がある. OAuth Appを利用すればGitHub Organizationレベルで接続設定を登録できる. 一方で,こちらもAPIには対応していないので自動化できない.

ということで,唯一の選択肢が personal access token を利用する方法である. personal access token はAPIに対応しているので今回実現したいTerraform CloudのIaCが実現できる. personal access token で連携させる課題としては,接続設定が個人のアカウントに紐付くことである. botアカウントを利用すれば解決できる?かもしれないが自分では試していないのでわからない. APIがpersonal access token以外にも対応して欲しいというissue は挙がっているのでいずれは解決する?

具体的なコードは以下のような感じ. ちなみにGitLabでもほぼ同じような設定で連携できる.

resource "tfe_workspace" "my_workspace" {
  name = "my-workspace"
  organization = tfe_organization.my_organization.name

  file_triggers_enabled = false
  queue_all_runs = false

  vcs_repo {
    identifier = "thaim/my_workspace"
    ingress_submodules = false
    oauth_token_id = tfe_oauth_client.github.oauth_token_id
  }
}

resource "tfe_oauth_client" "github" {
  organization = tfe_organization.my_organization.name

  api_url = "https://api.github.com"
  http_url = "https://github.com"
  service_provider = "github"
  oauth_token = var.github_personal_access_token
}

resource "tfe_notification_configuration" "slack_my_workspace" {
  name = "slack-my-workspace"
  enabled = true
  workspace_id = tfe_workspace.my_workspace.id

  destination_type = "slack"
  url = var.slack_webhook_url
  triggers = ["run:needs_attention", "run:completed", "run:errored"]
}

変数の管理

Terraform CloudのIacを実現したい動機の1つであった変数の管理について. 1つはVCS設定のような共通設定の管理がある. 例えばSlack連携ではwebhook_urlを,リポジトリとの連携ではpersonal access tokenをそれぞれ各リポジトリに設定する必要がある. terraformでTerraform Cloudを管理することで,このworkspaceにおける変数として管理することができる.

例えば上記WorkspaceのVCS設定では, personal access token は変数 var.github_personal_access_token として, Slackのwebhook URLは var.slack_webhook_url として参照している. このため,このTerraform Cloudを管理するworkspaceのvariableとして設定すれば, 変更したいときも変数を更新してapplyするだけでよい.

もう1つが各workspaceの環境変数やTerraform変数の設定である. よくある例がAWSのクレデンシャル情報(アクセスキーIDおよびシークレットアクセスキー)で, 複数のworkspaceで同じ値を設定したい.

環境変数を設定するTerraformリソースの定義例は以下の通り. これは環境変数の例だがTerraform変数も同様に設定できる. 対象となるworkspaceが1つか設定できず,workspaceごとにリソースを定義しないといけないのが若干面倒.

resource "tfe_variable" "aws_access_key_id" {
  workspace_id = tfe_workspace.my_workspace.id

  key = "AWS_ACCESS_KEY"
  value = var.aws_access_key_id
  category = "env"
  sensitive = false
}

resource "tfe_variable" "aws_secret_access_key" {
  workspace_id = tfe_workspace.my_workspace.id

  key = "AWS_SECRET_KEY"
  value = var.aws_secret_access_key
  category = "env"
  sensitive = true
}

Rails+Vuetify+ActiveAdminでボタンのスタイルが崩れる

Vuetifyにおけるスタイル崩れ

Rails6 + Vue + Vuetify でVuetify入門してた. Vuetifyのレイアウトでいろいろ試してみたところ, ハンバーガーメニューのスタイル崩れが発生した.

style collapsed huberger menu
スタイル崩れが発生したハンバーガーメニュー

Vuetify App Barsのナビゲーションアイコンに 意図しないグレーの円が表示されている. また,ハンバーガーメニュー以外にもナビゲーションメニューのうち ラベルのEDITボタンなども表示崩れが発生している.

意図しないスタイル反映の解除

スタイル崩れの共通点として,いずれもボタンタグなので ボタンタグに関するCSSがおかしいのだろうということはわかったが, その原因をすぐに見付けることができなかった.

Rails,特にCSSなどのフロントエンドは詳しくない上に Vuetifyもよく理解していなかったので(VuetifyのGetting Startedはあまり親切ではないと思う) その設定等がおかしいのかと思って試行錯誤していた.

結果, app/assets/stylesheets/application.css の反映の有無により スタイル崩れが生じることがわかった. これは同ディレクトリに active_admin.scss があり, application.cssがこのファイルを読み込んでいるため ActiveAdmin以外のページにもActiveAdminのスタイルが適用されていた.

...
  *
  *= require_tree .
  *= require_self
  */

このため, require_tree . を削除することで解決した.

require_treeの設定

この require_tree . でいろんなスタイルが反映されて表示崩れが発生するのは Railsあるあるな挙動らしい.

app/assets/stylesheets/ にscssファイルを生成すれば (generatorでビューを作成すれば勝手に作成される) 自動的に application.cssが読み込んでくれる.

逆にページごとに異なるCSSを適用したいケースがある場合は この require_tree . の記述を削除し, 共通ファイルは個別に明示的に指定した上で 各ページのビューファイル (*.html.erbなど)にてstylesheet_link_tag で個別に読み込むCSSを指定すればよい.

なるほど?

GitHub Secretsの更新をgithub-secrets-writerで自動化する

GitHub Actionsでクレデンシャル情報を利用するためにGitHub Secretsを利用しているわけだけど, この値の設定や更新を自動化したい.

具体的には AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY の更新を自動化したい. また,現状は単一のbotユーザの権限で,複数リポジトリに同じアクセスキーを設定しているので アクセスキーを更新したら複数リポジトリをまとめて更新したい.

リポジトリが少ないうちは手動対応でもよかったけれど リポジトリ数が増えるにつれて設定忘れや更新もれが発生しそうなこと, そもそも面倒なので自動化する.

自動更新の仕組み

おおまかな手順としては以下の通り

  1. アクセスキーを更新してダウンロード
  2. cliツールを利用して特定リポジトリのsecretsを更新
  3. すべてのリポジトリに対して 2 を繰り返す

cliによるsecretesの更新

secretsの更新に githubの公式クライアントツールの cli を利用したい. ところが,cliはsecretsの更新APIには対応しておらず,要望は上がっているが まだすぐに実装されそうというものではない.

その代替としては, github-secrets-writer というのがあるのでこれを利用する. その名前の通り,github secretsの更新に特化したcliツールである.

github-secrets-writerを利用する

手順としてはREADMEにある通り.

GITHUB_TOKENはGitHub公式ヘルプなどを参考に作成する. 権限について,パブリックリポジトリのsecretesのみを更新するのであれば public_repo だけで, プライベートリポジトリのsecretsも更新するのであれば repo のフルコントロールが必要になる.

あとは更新するリポジトリとsecrets名および値を指定して実行するだけ. key=value 形式で設定値が記載されているファイルがあれば --from-file で簡単に指定できるが, 残念ながらAWSアクセスキーはcsv形式なので--from-fileで簡単に利用できる形式ではないので 諦めて --from-literal で直接指定して実行する.

出力が少しわかりにくく, secretName1: 204 No Content のように表示されていれば上手く更新されている. 新しくsecretsの項目を作った場合は secretName1: 201 Created のようになる. 一方で,権限不足等であれば以下のように表示される.

secretname3: GET https://api.github.com/repos/thaim/samplerepo/actions/secrets/public-key: 404 Not Found []
ERROR: encountered some failures, see above

github-secrets-writerによる自動更新を自動化する

アクセスキーの更新は90日で更新してねという推奨があるので, アクセスキーの発行や上記cliの操作自体も自動化したい.

アクセスキーを発行するAPI自体は存在するので lambdaでアクセスキーを発行してそのままsecretsを更新するような処理を実装して CloudWatchで定期的に呼び出してあげればよい.

が,そこそこ面倒なので止めた. まずは手元で自動化できればよさそう.

Organizationレベルで共通のSecrets

ここまで手順を整理したところで, GitHubが最近 SecretsをOrganizations レベルで共有できる Organizations Secretsを発表している ことに気が付いた.

これを設定・利用すればそもそも,リポジトリ毎のSecretsを更新してまわるなんて不要になる.