くりにっき

フルスタックキュアエンジニアです

GitLab CIでTerraformを動かす

GitLab CIでTerraformをいくつか動かしてるのですがGitLab CI独自の機能をうまく使うといい感じにCIができるようになったのでメモ

前提

  • AWSの各アカウントをTerraformで管理している
  • アカウントごとにTerraformのリポジトリを作成
  • masterブランチ以外では terraform plan を実行し、masterブランチで terraform apply を実行
    • terraform plan があまり信用できないから別アカウントへの terraform apply のパターンもあるかもしれないですが、 terraform plan の方が基本形かなと思ってます

ベースになるジョブをincludeで別リポジトリから読み込むと便利

includeとはGitLab 11.4にcoreに入った機能で、外部のCI設定を .gitlab-ci.yml に取り込む機能です。

https://docs.gitlab.com/ee/ci/yaml/#include

他のCIサービスでいうとCircleCIのorbsが一番近いです。

include導入前

includeを使う前は各リポジトリに下記のような .gitlab-ci.yml が点在していました。

.terraform: &terraform
  image:
    name: hashicorp/terraform:0.12.5
    entrypoint: [""]

  before_script:
    - terraform init -input=false

  tags:
    - terraform

stages:
  - test
  - deploy

cache:
  key: "${CI_JOB_NAME}"
  untracked: true
  paths:
    - .terraform/

tflint:
  <<: *terraform

  stage: test

  variables:
    TFLINT_VERSION: "0.9.2"

  script:
    # tflintのDockerイメージを使うとterraform initできなくてmoduleが読めないためバイナリをDLする
    - cd /tmp
    - wget https://github.com/wata727/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip
    - unzip tflint_linux_amd64.zip

    - ./tflint

  tags:
    - docker

terraform-plan:
  <<: *terraform

  stage: test

  script:
    - terraform plan -input=false

  except:
    - master

terraform-apply:
  <<: *terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  only:
    - master

terraform-apply-manual:
  <<: *terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  except:
    - master

  when: manual

2箇所くらいならまだ許せたのですが4つ超えたあたりで厳しい感じになりました

include導入後

先日会社のGitLabをバージョンアップしたので includeを使ってリファクタリングしました。

まずは下記のようにテンプレートファイルのみを用意したリポジトリを用意

variables:
  TERRAFORM_VERSION: ""
  TFLINT_VERSION:    ""

.terraform:
  image:
    name: hashicorp/terraform:${TERRAFORM_VERSION}
    entrypoint: [""]

  before_script:
    - terraform init -input=false

  tags:
    - terraform

stages:
  - test
  - deploy

cache:
  key: "${CI_JOB_NAME}"
  untracked: true
  paths:
    - .terraform/

tflint:
  extends: .terraform

  stage: test

  script:
    # tflintのDockerイメージを使うとterraform initできなくてmoduleが読めないためバイナリをDLする
    - cd /tmp
    - wget https://github.com/wata727/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip
    - unzip tflint_linux_amd64.zip

    - ./tflint

  except:
    - master

  tags:
    - docker

terraform-plan:
  extends: .terraform

  stage: test

  script:
    - terraform plan -input=false

  except:
    - master

terraform-apply:
  extends: .terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  only:
    - master

terraform-apply-manual:
  extends: .terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  except:
    - master

  when: manual

Terraformリポジトリでは下記のように別リポジトリのtemplateを呼び出すだけで使えるようになります

include:
  - project: "group/gitlabci-terraform-template"
    file:    "/template.yml"

variables:
  TERRAFORM_VERSION: "0.12.5"
  TFLINT_VERSION:    "0.9.2"

メリット

  • CIの設定を一箇所に集約したことにより、利用側ではTerraformとtflintのバージョンだけ渡すだけで使えるようになる

上記templateでやってること

masterブランチ以外では terraform plantflint を自動実行しつつ、必要なら terraform apply を手動実行

f:id:sue445:20190920235810p:plain

masterブランチでは terraform apply を自動実行

f:id:sue445:20190920235819p:plain

Terraform実行専用のRunnerを作る

前述の設定で tagsterraform を書いてたやつです。

このTerraform専用のRunnerですが、下記のようにlimitを1にしたdocker executorを作っています。

[[runners]]
  name = "terraform"
  executor = "docker"
  limit = 1

https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section

limit = 1 にする理由

AWSでTerraformを使う場合にはbackendをs3にしつつ、DynamoDBで排他ロックをかけるのがデファクトになってます。

www.terraform.io

しかしこれだと同時に2つのブランチをpushした場合に後から実行された方が排他ロックエラーになってジョブが失敗します。

Terraformを触る人数が多いとこの手のエラーはしょっちゅう起こるし、1人しか触ってない場合でもトピックブランチをpush後にミスに気づいてforce pushしたような場合にもエラーになります。

それを防ぐためにRunnerを limit = 1 にする(つまり複数のジョブの同時実行をさせない)ことでジョブが確実に1つずつ実行されるようになり、さっきのような排他ロックエラーを回避できます。(planの時だけロックかけないってのも頑張ればできそうだけど怖いのでやったことはない)

ちなみにジョブの並列実行数を意図的に制限するというのは僕が知る限りGitLab CIでしか実現できない機能です。(少なくともTravis CI, CircleCI, Wercker, GitHub Actionsには存在しないです)

このTerraform専用Runnerはここ1年くらいずっと使っていますが、CircleCIでTerraformを動かしてた時によく起こってた排他ロックエラーが一切発生しなくなったのでおすすめです。

CIマニアから見たGitHub Actions(Beta)の使い所

1ヶ月くらい使って勘所が見えてきたのでメモ

メリット

1リポジトリ辺り20並列までジョブを並列実行できる

これに尽きる。

CircleCIにしろTravis CIにしろorganization(user) *1単位で並列数が縛られているため、例えば同じuserの他のリポジトリでジョブが詰まっていると別リポジトリではqueueが詰まってジョブが実行されません。

しかしGitHub Actionsの場合リポジトリ単位で並列数が縛られているので、あるリポジトリで20並列ジョブ実行中だからといって他のリポジトリでジョブが詰まることがありません。

後の方でデメリットについて色々書いてるけど、このメリット1つだけで他のデメリットを帳消しにしてお釣りがくるくらいの破壊力はあります。(偏見)

ジョブ実行時はアクセストークンが勝手に設定されている

CIのジョブの中でGitHubAPIを使いたい場合、パーソナルアクセストークンを発行してsecretsとかにセットすると思うのですが、GitHub Actionsだと secrets.GITHUB_TOKEN に勝手にセットされているので発行する必要がないです。

特に会社系のorganizationの場合、誰かがジョブ実行用のパーソナルアクセストークンを発行してCIを設定してその後その人が退職してorganizationから外された後にCIが動かなくなるってことが割とよくあると思うのですが(僕は過去2~3回見てきました)、その心配がなくなるのは地味に嬉しいところ。

マトリクステストがやりやすい

Travis CIライクな記法が使えるのでマトリクスの軸が増えてもyamlが長大になりづらいというメリットがあります。

jobs:
  build:
    strategy:
      matrix:
        ruby:
          - v2.3.x
          - v2.4.x
          - v2.5.x
          - v2.6.x
        gemfile:
          - gemfiles/rails_4_0.gemfile
          - gemfiles/rails_4_1.gemfile
          - gemfiles/rails_4_2.gemfile
          - gemfiles/rails_5_0.gemfile
          - gemfiles/rails_5_1.gemfile
          - gemfiles/rails_5_2.gemfile
          - gemfiles/rails_6_0.gemfile

最初に書いた1リポジトリ辺り20並列の仕様もあるのでライブラリ作者的に嬉しい。

実際にGitHub Actionsに移行したプロダクト

時系列順に紹介

zatsu_monitor

github.com

元々はTravis CIを使ってたんですが、僕の他リポジトリで採用してる「gitのtagをpushしたらCI上でバイナリをビルドしてReleasesに自動アップロードするお手軽リリース」がなくて割と面倒でした。

そのうちTravis CIで実装しようかと思ってた矢先にGitHub Actionsが使えるようになったので移行してお手軽リリースを実装しました

activerecord-compatible_legacy_migration

github.com

僕がメンテしてるgemの中で2番目にマトリクス数がヤバかったやつ。(Travis CIで40個)

並列数は多いものの後述のindex_shotgunと違ってMySQLPostgreSQLのようなDBを使ってないのでGitHub Actionsで初マトリクステストやるには丁度よさそうな規模感と難易度と踏んで移行

index_shotgun

github.com

僕がメンテしてるgemの中で1番マトリクス数がヤバいやつ。

マトリクス数はTravis CIだけで50個くらいあって、歴史的経緯でCircleCIも併用してるのでそれも合わせると55個くらいあります。

なんでマトリクス数がこんなにヤバいのかというと「各Rubyのバージョン x 各activerecordのバージョン x 各DB(sqlite3, MySQL, PostgreSQL, Oracle)」でテストしてるため。

複数のCIを併用してる弊害として .circleci/config.yml だけ修正したのにTravis CI側でもジョブが実行されて30分くらい待たされるという事象が度々あったので、CIを1つにまとめたいとは思ってました。

実際にGitHub Actions対応を行ったのが下記PRですが、Railsで使えるdatabase.ymlをテストコードで使っているので普通のRailsアプリをGitHub ActionsでCIする時の設定は参考になると思います。

github.com

ちなみにGitHub Actions移行後はマトリクス数60個くらいですが、全部実行し終わるための所要時間は18分前後です。

デメリット

Betaクオリティなので大目に見てますが、大目に見た上でもここはつらいというところだけ書いてます

yamlのanchorが使えない

Railsのdatabase.ymlでよく見る

default: &default

development:
  <<: *default

的なやつ。

yamlファイルが一定規模以上になるとanchorとmergeを使って上記のようなリファクタリングをしてDRYにしたくなるのですが、GitHub Actionsだと「Anchors are not currently supported」というエラーになって使えなくてつらい。

f:id:sue445:20190909225037p:plain

anchorとmergeでリファクタリングできるのがYAMLの特徴だと思うんだけど、それが(まだ?)使えないのが残念。

マトリクステストだとSlack通知がつらい

GitHub ActionsにSlack通知機能が搭載されていなくてstepの中で自分でSlack APIを叩くかmarketplaceにあるSlack通知用のCheckを使うことになります。

しかしマトリクステストのstepsの中で直接Slack通知を行うと通知が大量にきてノイズになるので、全てのマトリクステストが終わった後に1回だけ成功か失敗かの通知を投げてほしいと思うのが人情ですが、それを実装するのが今のGitHub Actionsの仕様だと非常に面倒です。

上記要件を真面目に満たそうとするとこんな感じになります。

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        # 略

    steps:
      # 略

      - name: Slack Notification (not success)
        uses: homoluctus/slatify@v1.5
        if: "! success()"
        with:
          job_name: '*build*'
          type: ${{ job.status }}
          icon_emoji: ":octocat:"
          url: ${{ secrets.SLACK_WEBHOOK }}

  notify:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Slack Notification (success)
        uses: homoluctus/slatify@v1.5
        if: always()
        with:
          job_name: '*build*'
          type: ${{ job.status }}
          icon_emoji: ":octocat:"
          url: ${{ secrets.SLACK_WEBHOOK }}

needs を使えば前のジョブが全部終わった時に次のジョブを実行するというworkflowが組めるのでこれを使えばマトリクステストが全て終わった時にnotify ジョブで1回だけ通知を飛ばすということが簡単にできそうに見えます。

しかしneedsに書いたジョブが全て成功した時にしか実行されないので notify ジョブだけだとジョブ成功時の通知しか飛ばせません。

そのためマトリクステストの中で失敗時の通知を飛ばすような実装が必要になります。

GitHub ActionsのマトリクステストはTravis CIと違ってfail-fast(マトリクステストがどれか1つでも失敗したら他はスキップする)という仕様なのでジョブ失敗時の通知は1回しかこないように見えますが、同時に20並列もテストを実行してるとだいたい同時にテストが失敗するため失敗時の通知は複数飛んできます。

ジョブ失敗時はだいたい10個くらい同時に通知が飛るのですが、他にやりようがないので今は我慢してます、、、

f:id:sue445:20190909231009p:plain

合わせて読みたい

sue445.hatenablog.com

*1:ここで言ってるuserというのはジョブの実行者ではなくリポジトリの所有者の方です

zatsu_monitor v1.0.0をリリースした

リリースノート

https://github.com/sue445/zatsu_monitor/releases/tag/v1.0.0

Breaking changes

zatsu_monitorは今まで 0.3.3 のようなtagをつけていたのですが、Go modulesがprefix付きのtagを推奨していたので *1 、それに準拠して v1.0.0 のようなtag名に変更しました

アプリ的な機能変更は特にない(強いて言うならGo 1.13へのアップデートとライブラリのアップデートくらい)のですが、プロビジョニングツールとかからReleasesのバイナリをDLしてるケースもあるので *2 念の為Breaking changes扱いにしています。

*1:https://github.com/golang/go/wiki/Modules#modules

*2:自分のItamaeレシピでやってる

よいコミットメッセージを書くために心がけていること

よいコミットメッセージとはどういうものだろう? - chiastolite’s blog に対するアンサーソングです

chiastolite.hatenablog.com

必要であればWhyを補足するためのContextを書く

元エントリだとコミットメッセージの話だったけど僕はPRで実践してもいいと思います。*1

起点となるURLを明記する

  • レビューで指摘されたのであればPRのコメントのURL
  • issueのURL
  • アプリケーションエラーであればSentryなどのエラートラッキングシステムのURL
  • GitHub外のタスク管理ツール(例:redmineやBacklogやTrelloなど)に修正概要が書かれているのであればそのURL
  • チャットのやり取りが起因ならSlackなどのパーマリンク
  • 公式ドキュメントのURL
  • CIがコケているのであればCIのビルドのURL

チーム開発していればコミットやPRを行う起点となるURLがだいたいの場合存在するので、僕はそれを書くようにしています。

理由

  • コミットメッセージやPRにWhyを全て書くのは難しいのでそれに付随するContextは別URLを参照させた方が楽
  • 特にチャットのやり取りの場合、その当時のリアルなやり取りをコミットメッセージやPRの概要で読み取るのは難しいのでSlackのURLが貼られていると後々他の人が見た時の情報が増える

別に他人に強制するようなものでもないし *2即効性がない(1~2年後の誰かが嬉しくなる可能性があるだけ)のですが、自分がこうされていると嬉しいので日頃からやっています

アプリケーションエラーやCIのビルドエラーであればバックトレースも(全部じゃないけど)書く

これは場合によりけりかな。

SaaSは過去ログが全部残ってることが多いので1年後とかにそのURLを見返すことができます。

しかしSentryやGitLabなどを自前でホスティングしてるような場合に容量的な問題で過去ログを定期的に消していることがあるので、そういった場合にURLだけだとリンク先が消えて詳細な情報が追えなくなるので僕は念の為書くようにしています。

*1:僕は両方やったことある

*2:さすがにコミットメッセージの書き方まで細かく指摘はしない

condo3と地域.rbカレンダーを作った

夏の進捗です。

condo3

condo3について

connpassやDoorkeeper のグループのイベント情報をiCalendarやAtomで取得するためのサイトです

https://condo3.appspot.com/

f:id:sue445:20190811162205p:plain

モチベーション

connpassやDoorkeeperで特定のグループ(コミュニティ)の勉強会をウォッチするのに現状だと開催通知のメールしかないのが不便という問題がありました。(個人的にはSlackでウォッチする手段がほしかった)

本当は公式でこの手の機能(グループ単位のフィード)を提供してほしかったんですが、connpassには(Doorkeeperだったかも?)過去に要望投げたものの特に音沙汰がなかったので結局自分で作りました。

ユースケース

  • AtomのURLをSlackの特定のチャンネルで /feed subscribe 〜 すれば新しいイベントが作られた時にSlackに通知される
  • iCalendarのURLをGoogleカレンダーに登録すれば自分のカレンダーに表示できる。

ここではSlackとGoogleカレンダーのみ取り上げていますが、AtomもiCalendarも汎用的な規格なのでこれらの形式に対応している他のツールと連携させることもできます。

技術的なこと

リポジトリ

github.com

余談ですが個人開発だとCloud KMS, TypeScript, webpack, Bulma辺りは初採用です

技術の選択理由

  • Google App Engine/Go
    • アプリの性質上無限にAPIを叩かれることが容易に想像できたので、Herokuはあまり使いたくなかった
      • Herokuはアカウント単位でDyno hour(インスタンス起動時間)の無料枠を使うため。(既にHerokuで個人アプリ10個以上動いてるのでリクエストがガンガンくるアプリは立てたくない)
    • 業務的な話になるけど今期の個人目標が「GCPのスキルをつける」だったのでせっかくだからappengineを使ってみるかってことに
      • appengineは昔使ってたことがあるので全然知らないわけではなかった
    • appengineではいくつか言語が選択できるけど、爆速でリクエストさばくにはGoが一番向いてた
  • フロントエンド
    • 自分の中ではReactかVue.jsの二択だったけど、APIがメインで画面はトップページしか必要なかったのでVue.jsを選択
      • Reactは大規模開発のイメージがあった。(異論は認める)
    • 型安全が好きなのでTypeScript選択
  • CSSフレームワーク
    • Bootstrap以外思いつかなかったので社のSlackで募集したうちの1つがBulma
    • 実際に社のSlackに投げた時のスクショが下記

f:id:sue445:20190811165356p:plain

開発時に苦労したこと

  • 8〜9年ぶりにGoogle App Engineを触ったので浦島太郎状態
  • 特にappengine/GoはGo 1.11(1st generation)と1.12(2nd generation)で作り方が全然違う *1 のを知らずに最新版って理由だけで1.12で作り始めたので、1.12のドキュメントも少なくググった時に1.11以前のドキュメントが出てきて動かなくてそこでハマってた
    • 2nd genだとローカル開発時に dev_appserver.py が不要になってたり、appengine本体にmemcachedが入ってないのが一番のハマりでした...
  • フロントエンド赤ちゃんの状態で雰囲気でVue.jsやTypeScriptを書いたのでハマりしかなかった

名前の由来

connpassの con とDoorkeeperの do を足してcondo3です。

末尾の3に関しては諸説ありますが語呂が良かった説が一番有力です。(他にもappengineで作った3つ目のwebアプリだったり、appengine/Javaで使ってたSlim3というフレームワークにあやかったという理由もある(今思いついた))

地域.rbカレンダー

condo3の副産物です。日本各地の地域.rb情報がひと目で分かるカレンダーです

https://sue445.github.io/regional-rb-calendar/

f:id:sue445:20190815120012p:plain

github.com

技術的なこと

condo3のiCalendarのURLをGoogleカレンダーにインポートして表示させただけ。

僕が見つけた地域.rbは全部登録したけど漏れがあると思うのでその場合は issue で教えてください。

メリット

  • 一度iCalendarのURLをGoogleカレンダーに登録してしまえばあとは勝手に最新の情報に追従され続けるのでメンテが楽

8/15 16:50追記

知らなかった...

開発時にソース内検索してrssatomが無かった気がしたんだけど今見たら普通に埋め込まれてた(つらい)

f:id:sue445:20190815165516p:plain

AWS Lambda CI/CD俺的ベストプラクティス

Lambdaで動くアプリやフレームワークの事例はよく見るのですが、LambdaのCIやCDにしやすさに主眼をおいた紹介はあんまり見ないので現時点での自分のベストプラクティスのメモです

tl;dr;

  • ライブラリインストール時にmakeが走る言語(例:Ruby, Python, nodejsなど)の場合はServerless Framework使うのが楽
  • それ以外はServerless FrameworkかAWS SAMのどっちかならまず困らない

このエントリで書いていること

  • 自分が過去に試した手法
  • 逆に言えば自分がやったことないことは書いてないので誰かいい感じに紹介してほしいw

Lambdaをデプロイするのに肝になること

  • AWSAPIを実行するために必要なライブラリ(Rubyならaws-sdk gem)は実行環境に含まれているのでインストールする必要はないが、それ以外のライブラリを使いたい場合にはインストール済の状態でzipに固めてアップロードする
  • さらにライブラリインストール時にmakeが走る場合(Rubyのgemだとnative extensionと呼ばれるもの)、Lambdaと同じ実行環境(Amazon Linux)上でコンパイルをする必要がある。(以降、native extension問題と呼称)
    • ローカルのMacUbuntuとかで bundle install してzipに固めてデプロイするのは絶対ダメ
    • Rubyに限らずPythonとかでも同じことが言える
      • LambdaのPythonからMySQLdb(PythonMySQLを使うためのライブラリ)を使おうとするとAmazon Linuxでビルド済の mysqlclient.so もzipに含める必要がある
  • Lambdaを使いたい場合、大抵の場合Lambda以外のリソースも必要になるのでそれらを含めたデプロイを考える
    • 例えばLambdaでHTTPリクエストを受けたい場合にはAPI Gatewayが必要だし、cronみたいに定期実行させたい場合にはCloudWatchが必要だし、Lambdaでこれらのリソースを操作するにはIAMの権限を追加する必要がある
    • 純粋にLambda(とLambdaの実行しかできないIAMロール)だけで済むケースは見たことないです

デプロイしやすさに着眼したフレームワーク紹介

論外

コンソールからアップロードする

軽くお試し利用するくらいならコンソール上で直接編集してもいいんだけど、各種ライブラリを使いたい場合にはzipで固めてアップロードする必要があります。

問題点

  • Vim のような使い慣れたエディタが使えない
  • gitによる履歴管理できない
  • CIとの連携ができない
  • 手でzipを固めてアップロードするのは時代遅れ感
  • 冒頭で書いたように真面目にやるならEC2にAmazon Linuxインスタンスたててからインストールしてzipに固める必要があるので面倒
    • ライブラリを使わないんならそこまで気にしなくてもいいんだけど、一定規模以上のアプリ作る場合にはライブラリは必須だし、そのライブラリのインストール時にmakeが必要かを依存してるライブラリのその先の依存まで人間が全部気にかける必要はないと思ってます。(それはbundlerやnpmなどのパッケージマネージャの役割)

できなくはないがかなり厳しい

Terraform

www.terraform.io

AWS全般のオーケストレーションといえばTerraformが一番有名ですが、Lambdaのデプロイには向かないです

理由

  • Terraformだけだとライブラリのインストール込のzip圧縮ができない
    • ファイル名直打ちやディレクトリ丸ごと圧縮とかはできる *1
    • Terraform外でzipに固めてそれをTerraformに渡せばまあできないことはなさそうだけど、そこを自分で作り込むくらいなら他の出来るツールを使った方がいい
  • 仮にTerraformでzipを作った場合、zip上は差分が全くないのにTerraform上で差分が出ることがある
    • ローカルだと差分がないのにCIだと差分が出ることが割とよくある(その逆もよくある)
  • Terraform使えばAWSに関することはだいたいなんでもできるんだけど、AWSAPIをそのまま使うのとそんなに変わらないので記述が割と冗長になる
    • 後述のServerless FrameworkやSAM(実質Cloud Formation)の方が多少楽

Apex

メリット

デメリット

  • Lambdaの実行にLambda以外のものが必要な場合、結局Terraformが必要になる
    • Lambda実行用のIAMロールは自動で作ってくれるんだけど、それに新しくポリシーを追加したい時はTerraformにimportする必要がある
    • Apex自体はTerraform連携機能があるんだけど、最初からそれ用のディレクトリ構成にする必要があるので移行コストがある
    • 上でも書いたけどTerraformだと記述が冗長になりがち

8/12 17:20追記

リポジトリトップに

This software is no longer being maintainted and should not be chosen for new projects.

って書いてるので新規に採用しない方がよさそう。

実用レベル

Serverless Framework

メリット

  • Terraformに比べたらAWSAPIを意識しなくていい
  • プラグインが充実してる(後述)
  • Lambda以外のリソースも作れるので、Serverlessのリポジトリだけで完結する

デメリット

  • npmなので実行環境にnodejsが必要
  • デプロイにCloudFormationを利用してるので、デプロイ時にエラーになったらCloudFormationのjsonを読む必要があって大変

AWS SAM

native extension問題と戦う

Amazon LinuxのEC2インスタンス内でビルドする

AWS Lambda RubyでNative Extensionsを使用するgemを使うには?serverlessも使ってみた! - GA technologies Tech Blog に書かれているやり方

ただしこれだとCircleCIなどとの連携がやりづらいという欠点があります。(GitLab CIであれば自分でRunnerを立ててshell executorで動かせば使えないことはない)

Amazon Linux互換のDockerイメージを使う

  • https://hub.docker.com/r/lambci/lambda/
  • AWS公式ではないけど、活発にメンテされていて信用できる
  • LambdaのCIをしようとするとだいたいこのDockerイメージにいきつくくらいにはメジャー

昨今のCIサービスはDockerイメージを使うのがデファクトなので、 lambci/lambda を使うのがいいでしょう

ただし注意点として lambci/lambda のコンテナ内ではrootユーザではない&sudoもないので権限がだいぶ制限されてます。

Serverless Frameworkのプラグインを使う

Serverless Frameworkはプラグインが充実しています。その中にライブラリのインストール時に任意のDockerイメージからコンテナを起動して、コンテナ中でライブラリのインストールを行ってデプロイしてくれるプラグインがあります。

僕が把握しているのは下記です

これらを使えばCIで使うのも容易でしょう。

ただしCIが起動したDockerコンテナの上でさらに lambci/lambda のコンテナを起動する構図になるのでちょっと特殊な構成になります。

また、そのためにはDockerのprivilegedオプションを有効にしてdind(Docker in Docker)が必要になるので、CIサービスによってはセキュリティ的な観点で使えないケースもあるかもしれません。(GitLab CIであればprivilegedオプションを有効化したRunnerを自分で作れば可、CircleCIはやったことないので不明)

ライブラリをインストールするジョブとデプロイするジョブを分ける

AWS SAMのクライアントはnpmなので実行環境にnodejsが必要になります。

例えばSAMでデプロイするためにlambci/lambda 内でbundle installすることを考えると、もし1つのイメージで行おうとするとlambci/lambda 内にnodejsとRubyのランタイムをインストールする必要があるのですが、先に書いたようにlambci/lambda 内では権限が制限されているためインストールができません。*2

そのため、「ライブラリのインストールを行うジョブ」と「デプロイを行うジョブ」をわけて、それぞれのジョブで使うDockerイメージを別々にするのが現実的な解決方法だと思います。

CircleCIであれば persist_to_workspaceattach_workspace を使えば同一ワークフローの複数のジョブ間でファイルの受け渡しが可能です。

https://circleci.com/docs/2.0/configuration-reference/

もしCircleCIでRubyのアプリをSAMでデプロイしようとするなら

  1. lambci/lambda:ruby2.5 を使ったジョブでbundle installして persist_to_workspace する
    • ここでzip圧縮をしないのは lambci/lambda:ruby2.5 にはzipがインストールされていない&権限の関係でインストールもできないため
  2. 後続のジョブで attach_workspace してインストール済のライブラリを受け取り、zip圧縮してSAMでデプロイする

という構成になるでしょう。

Serverless Frameworkを使う場合に比べたらジョブを分ける必要があって若干手間ですが、個人的にはギリギリ許せる範囲かなと思っています。

【9/4 0:00追記】sam build --use-containerを使う

sam build--use-container があるという知り合い情報

docs.aws.amazon.com

If your functions depend on packages that have natively compiled dependencies, use this flag to build your function inside an AWS Lambda-like Docker container.

なるほどー

aws.amazon.com

If we include dependencies with native extensions, such as nokogiri, we want to make sure we’re building on an Amazon Linux-compatible image. With SAM CLI, you can do that in a single command as well.

トノコト

native extension問題と無縁な言語

Go言語はクロスコンパイルに対応しているのでlinux_amd64でビルドしていればどの環境でビルドしてもLambdaで動くバイナリが作れて楽です。(Javaもnative extension問題と無縁だと思うけど自信ない)

この手の言語で開発するのであればServerless FrameworkとAWS SAMのどっちで作ったとしてもまず困ることはないと思います。

8/12 01:20追記

ブコメレス

id:asuka0801

Serverless Framework使うためだけにnodejs入れるのが嫌

ローカルでの話なのかCIでの話なのか分からないけど(まぁこのエントリではCIのことしか書いてないので後者と予想)、公式じゃないもののserverlessインストール済のDockerイメージもあるのでCIでnodejs入れる必要はないです。(公式じゃないから嫌とかDocker入れるのが嫌とかいうのはさすがに面倒見きれない)

https://hub.docker.com/r/amaysim/serverless

とはいえ amaysim/serverless だとdockerが入っていなくてdindができないので実際のところ docker:dind にnodejs入れてさらに npm ci が現実解だと思います。(社のserverlessリポジトリだとそうやってた。deployジョブの実行時間見たら諸々のインストールの時間含めて1分半前後)

8/12 11:00追記

ブコメレス

id:esodov

そもそも例であげてるlambdaでmysqlって構成どうなのという気も。そこ主題じゃないか。

重箱の隅をつつくような指摘ありがとうございます。

一応補足しておくと、RDSやAuroraのMySQLの監視を行うスクリプトpythonで、そいつをLambdaで動かしていたのです。

8/12 17:20追記

id:YonmanHasse

Apexは"This software is no longer being maintainted and should not be chosen for new projects."なので挙げるにしてもそこを書いた方が

あー、元々は半年以上前に会社のesaに書いてたやつなので、社外向けに書き直した時にリンク先のリポジトリは見てなかったです。追記しました。

*1:https://www.terraform.io/docs/providers/archive/d/archive_file.html

*2:むっちゃ頑張ればできるかもしれないけど未確認

go-doorkeeperを作った

github.com

go-doorkeeperについて

golangDoorkeeper API を利用するためのAPIクライアントです

最近golangでDoorkeeperのAPIを叩きたい需要が発生したんですが誰も作ってなさそうだったので作りました。*1

使い方

READMEからコピペ

package main

import (
    "github.com/sue445/go-doorkeeper"
    "os"
)

func main() {
    accessToken := os.Getenv("DOORKEEPER_ACCESS_TOKEN")
    if accessToken == "" {
        panic("DOORKEEPER_ACCESS_TOKEN is required")
    }

    client := doorkeeper.NewClient(accessToken)

    // List all featured events
    events, rateLimit, err := client.GetEvents(&doorkeeper.GetEventsParams{})
    // more options
    events, rateLimit, err := client.GetEvents(&doorkeeper.GetEventsParams{Query: "golang", Sort: doorkeeper.SortByPublishedAt()})

    // List a community's events
    groupName := "trbmeetup"
    events, rateLimit, err := client.GetGroupEvents(groupName, &doorkeeper.GetEventsParams{})

    // Show a specific event
    eventID := 28319
    event, rateLimit, err := client.GetEvent(eventID)
    // or
    event, rateLimit, err := client.GetEvent(eventID, doorkeeper.WithLocale("en"))

    // Show a specific group
    groupName := "trbmeetup"
    group, rateLimit, err := client.GetGroup(groupName)
    // or
    group, rateLimit, err := client.GetGroup(groupName, doorkeeper.WithLocale("en"))
}

*1:「doorkeeper api」でググると全く関係ない方のdoorkeeper gemしか見つからなかったのでそいつがノイズになって見つけられなかった説はある