AWS Lambda を開発する際のユニットテストでは、TZ=UTC にしておこう

時刻判定周りでのローカルタイムゾーンの違いは、うっかりバグの元になりやすいものですが、AWS Lambda の場合、以下のように環境変数 TZ が予約済み環境変数として変更不可能となっているようです。

docs.aws.amazon.com

  • TZ – 環境のタイムゾーン (UTC)。実行環境は、システムクロックを同期するために NTP を使用します。

つまり、AWS Lambda の実行環境ではローカルタイムゾーンUTC 固定になっているということですね。わかり易くはあるのですが、開発PC 上でや CI でユニットテストを走らせるときにローカルタイムゾーンが現地 (JSTとか) に設定されていて、テスト通ったけど本番環境でバグる、ということもありそうです。

テストを実行するときは 環境変数 TZ に UTC を設定しておくようにするのがよさそうですね。Go 言語なら Makefile に書いてしまうのもいいかもしれません。

AWS SDK for Go V2 をちょっとだけ触ってみた

AWS SDK for Go V2 が正式版となったので、ちょっとだけ触ってみました

aws.github.io

Pagination の変更

V1 から V2 に移行するにあたって、コードを書く上で最も影響がありそうな変更が、リスト系の API で使用する Pagination の変更です。

V1 では、個々のページの処理はコールバック関数で行う形でした。

   input := &s3.ListObjectsV2Input{Bucket: aws.String("test-bucket.go-aws-sdk.sample")}
    err := s3Client.ListObjectsV2Pages(input, func(output *s3.ListObjectsV2Output, lastPage bool) bool {
        for _, content := range output.Contents {
            fmt.Println(*content.Key)
        }
        return true
    })
    if err != nil {
        fmt.Printf("%v\n", err)
        panic(err)
    }

V2 ではイテレータを返す形になっていて、単純に同じコンテキストの中で for ループで回すことができます。

   input := &s3.ListObjectsV2Input{Bucket: aws.String("test-bucket.go-aws-sdk.sample")}
    paginator := s3.NewListObjectsV2Paginator(s3Client, input)

    for paginator.HasMorePages() {
        output, err := paginator.NextPage(ctx)
        if err != nil {
            fmt.Printf("%v\n", err)
            panic(err)
        }
        for _, content := range output.Contents {
            fmt.Println(*content.Key)
        }
    }

V1 だとコールバック関数内のエラーを外に引き回したりするのが面倒だったので、シンプルになるのは嬉しい変更ですが、これまでの Pagination 系関数をバッサリ消してしまったのは見切りが良すぎるというか…

モジュール化

V1 では github.com/aws/aws-sdk-go という一塊のモジュールになっていましたが、 V2 ではサービス API ごとに分割されていますね。ビルド後のバイナリサイズも相応に小さくなるようです。

上記の2つのサンプルコードを (最低限動作する形にして) それぞれ go build -ldflags="-s -w" でビルドしたところ、バイナリのサイズは 8,216,5766,545,408 と 1.7MB ほどの削減となりました。

ほか

Waiter 系は、非同期で実行される API の処理が完了するまで、状況を ポーリングしつつスレッドをブロックしてくれる機能ですね。例えば ECS のタスクが起動するまで待ってくれる TaskRunningWaiter などは、デプロイツールのようなものを作るのに便利そうです。

気になったところ

Paginator もそうですが、 session や config の呼び出し周り、あるいは DynamoDB の AttributeValue 系が別モジュールに移動しているなど、破壊的な変更が結構あるので、単純な移行は結構難しそうです。わざわざモジュール名を変えているのでしばらくは併存させるのでしょうが、いつ公開が止まるかわからんので、既存の資産をいつ、どうやって移行していくべきかが悩みどころですね…

Ubuntu18.04 に PHP7.4.8 をインストールしたときのメモ

PHP インストールのたびに前提パッケージで悩んでる気がするのでメモ。

(多分、後々バージョンが上がったらまた変わってくると思うけど)

1. anyenv 入れる

$ git clone https://github.com/anyenv/anyenv ~/.anyenv
$ echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(anyenv init -)"' >> ~/.bashrc
$ exec $SHELL -l

2. phpenv 入れる

$ anyenv install phpenv
$ exec $SHELL -l

3. 前提パッケージを apt 経由で入れる

$ sudo apt update
$ sudo apt -y install build-essential
$ sudo apt -y install \
  libxml2-dev \
  libssl-dev \
  libbz2-dev \
  libcurl4-openssl-dev \
  libjpeg-dev \
  libpng-dev \
  libmcrypt-dev \
  libreadline-dev \
  libtidy-dev \
  libxslt-dev \
  autoconf \
  libkrb5-dev \
  sqlite3 \
  libsqlite3-dev \
  libonig-dev \
  libzip-dev

4. PHPを phpenv 経由で入れる

$ PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 phpenv install 7.4.8
$ phpenv global 7.4.8
$ exec $SHELL -l

PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 はビルドの並列処理数。CPUスレッド数に合わせて調整する。

AWS ECR を使う開発に便利な Vagrantfile

概要

ECS で稼働させるアプリケーションを開発していますが、Docker for Mac は何かと遅いので、Docker/ECR まわりの作業だけ Vagrant上でやるための Vagrantfile を作りました。その内容を残しておきます。

Vagrantfile

# Vagrant環境に予めインストールしておきたいものを記述する
# ここでは、AWS CLI をインストールし、 ECR へ Push するための docker login コマンドを alias として追加している
$provision_script = <<SCRIPT
apt-get update
apt-get install -y zip unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --update
rm awscliv2.zip
echo 'alias ecr-docker-login="docker login -u AWS -p $(aws ecr get-login-password) https://$(aws sts get-caller-identity --query Account --output text).dkr.ecr.${AWS_REGION}.amazonaws.com"' >> .bashrc
SCRIPT

Vagrant.configure("2") do |config|
  # お好みの Distro の Box を以下サイトから検索して指定。ここでは Ubuntu 18.04 を使用。
  # https://app.vagrantup.com/boxes/search
  config.vm.box = "bento/ubuntu-18.04"

  # メモリを増やしておく
  config.vm.provider "virtualbox" do |v|
    v.memory = 4096
    v.cpus = 2
  end

  # Windows + Hyper-V の場合
  # config.vm.provider "hyperv" do |v|
  #   v.memory = 4096
  #   v.cpus = 2
  # end

  # VM 上で初めから Docker を使えるようにしておく
  config.vm.provision "docker"
  # 初期インストールスクリプト実行
  config.vm.provision "shell", inline: $provision_script

  # Dockerコンテナでサービスするポートにホストからアクセスできるようにする
  config.vm.network "private_network", ip: "172.12.8.150"
  config.vm.network "forwarded_port", host: 8080, guest: 80

  # ホスト側のソースコード VM 内で参照するため共有しておく
  # (gitwork の下に、 github から clone したリポジトリがある想定)
  config.vm.synced_folder "#{ENV['HOME']}/gitwork", "/home/vagrant/gitwork", type: "nfs"

  # Windows の場合
  # config.vm.synced_folder "#{ENV['HOMEDRIVE']}\\#{ENV['HOMEPATH']}\\gitwork", "/home/vagrant/gitwork", type: "nfs"

  # ホスト(ローカル)での AWS Credential を共有したい場合
  # config.vm.synced_folder "#{ENV['HOME']}/.aws", "/home/vagrant/.aws", type: "nfs"
end

使用方法

vagrant up して、VMが起動したら vagrant ssh し、上記でマウントしたソースコードディレクトリに移動して docker build します。

ECR に Push するときは

$ AWS_REGION=ap-northeast-1 ecr-docker-login     # AWS_REGION には使用する ECRリポジトリがあるリージョンを指定

とした後に、ドキュメント通り、タグ付けと docker push を行います。

終わりに

今回は、Mac上に開発環境が出来上がっている状態で作成したので、ソースコードなどのリソースはホスト(Mac) のファイルシステムをマウントする形にしていますが、ゼロから作るのであれば、VagrantVMファイルシステム上に git clone して開発環境を作ったほうが何かと幸せかもしれません。

VS Code なら SSH Remote Connect を使えば開発作業もある程度シームレスにできるでしょう。

参考

qiita.com

AWS CLI で取得した情報をシェルスクリプトで使うときの Tips

はじめに

AWS 上のリソースの操作を自動化したいとき、ちょっとしたことなら AWS CLIシェルスクリプトで書くのが一番お手軽だと思います。ただ、きちんとソースコード管理されるようなスクリプトであれば、例えば EC2 の Instance ID のような自動採番値はスクリプトに仕込みたくないので、list や describe 系のコマンドで情報を取得したうえでゴニョゴニョしたくなります。

これらの参照系コマンドは実にたくさんの情報を返してくるので、欲しい情報だけ抜き出さないといけませんが、そのあたりのやり方を書いておきます。

基本: --query オプション

AWS CLI の出力から抽出・加工する方法として、 --query オプション を使う方法と、JSON 出力して jq で加工する方法をよく見かけますが、jq はデフォルトインストールされていない環境も多いので、個人的には --query を好んで用います。

例えば、 Name タグが hoge-instanceインスタンスIDを取りたい場合を考えます。以下のコマンドを叩くと、インスタンスの細かい情報が取得できます。

$ aws ec2 describe-instances \
    --filter "Name=tag:Name,Values=hoge-instance" --output json
{
    "Reservations": [
        {
            "Groups": [],
            "Instances": [
                {
                    "AmiLaunchIndex": 14,
                    "ImageId": "ami-gej3kjfo9",
                    "InstanceId": "i-0f4f44a816fexample",
                    "InstanceType": "c4.xlarge",

...

}

ここから InstanceId だけ取り出したいので、出力の階層構造を踏まえ、以下のように --query を設定します。

$ aws ec2 describe-instances \
    --filter 'Name=tag:Name,Values=hoge-instance' \
    --query 'Reservations[0].Instances[0].InstanceId' --output text

i-0f4f44a816fexample

--output text としていますが、 json のままだと、出力に引用符が残ります。 --output の設定値を変えても、クエリの抽出結果には影響しません。

シェルスクリプト内では、こうして必要な情報を抽出できるコマンドを書き、 $() で変数に代入して扱う形が多くなると思います。

一度のコマンドで複数の値を取りたい場合

一度のコマンド実行結果から複数の値を取りたい場合は、 set コマンドが便利です。例えば、上記の結果から InstanceIdImageId を取りたいというような場合は以下のように書きます。

set $(aws ec2 describe-instances \
    --filter 'Name=tag:Name,Values=hoge-instance' \
    --query 'Reservations[0].Instances[0].[InstanceId,ImageId]' \
    --output text)

INSTANCE_ID=${1}
IMAGE_ID=${2}

query で [InstanceId, ImageId] のように配列に括るようにして text 形式で出力すると、各値がスペース区切りで出力されます。これを set に与えることで位置パラメータ $1, $2, ... に代入されて、個別に取り出すことができるようになります。

query の JMESPath ?演算子 を組み合わせると、順不同で帰ってくるリストから、特定の条件にマッチするものを複数一度に取得することができます。以下の例は、 ALB に複数設定されたリスナのリストから、http と https の各リスナの ARN を抽出する例です。

set $(aws elbv2 describe-listeners --load-balancer-arn $load_balancer_arn \
      --query '[Listeners[?Port==`80`].ListenerArn,Listeners[?Port==`443`].ListenerArn]' \
      --output text)

local listener_arn_http=${1}
local listener_arn_https=${2}

カンマ区切りリストとして取得する

CloudFormation などは、複数値をとるパラメータの形式として、カンマ区切りリストが使えます。このようなカンマ区切りリストを手軽に作る方法です。

例えば、ある ECS Service のタスクが展開される Subnet のリストを取りたいときは以下のようなコマンドになります。

$ aws ecs describe-services --cluster the-cluster --services the-service \
       --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \
       --output text

subnet-xxxxxxxx subnet-yyyyyyyy

これをカンマ区切りリストにするには printf コマンドが使えます。

$ printf '%s,' \
   $(aws ecs describe-services --cluster the-cluster --services the-service \
       --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \
       --output text)

subnet-xxxxxxxx,subnet-yyyyyyyy,

ただしこれでは、末尾にもカンマが付加されてしまうため、変数展開を使って末尾のカンマを取り除きます。 スクリプトではこんな感じ。

SUBNET_LIST=$(printf '%s,' \
   $(aws ecs describe-services --cluster the-cluster --services the-service \
       --query 'services[0].networkConfiguration.awsvpcConfiguration.subnets' \
       --output text))

echo ${SUBNET_LIST%,}

# 出力結果
# subnet-xxxxxxxx,subnet-yyyyyyyy

その他細かいネタ

よく必要になる AWSアカウントID を取得するスニペットです。

ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)

おわりに

だいたいのケースは、ここに書いたように、AWS CLI とシェルの組み込みコマンドで何とかなってしまうので、生の ID をスクリプトに埋め込まないようにしましょう。

( あんまり複雑になるようなら他のプログラミング言語SDKを使ったほうがいいと思いますが )

Mac の iTerm2 上で Home/End キーを使いたい

MacBook に PC 用キーボードをつないで使っていますが、Terminal での行頭/行末移動が不便。なので、Windows 同様、Home/End キーで行頭/行末移動ができるようになりたい。

というのを実現したのでその手順を。

なお、 以下手順は iTerm2 で確認しています。標準のターミナルでは使えないようです。

  1. Home/End キーの Escape Sequence を確認
    iTerm2 上で Ctrl+V の直後に特殊キーを押すと、そのキーのエスケープコードが表示されます。例えば Ctrl+V Home と押すと、^[[HCtrl+V End^[[F が出力されます。

  2. bindkey コマンドでエスケープコードと動作の関連付けを行う
    (zshの場合 )1 で確認した Escape Sequence にカーソル移動の動作を紐つけます。(.zshrc などに追記しておくといいでしょう)

bindkey "^[[H" beginning-of-line
bindkey "^[[F" end-of-line     

※試してませんが、bash なら bind -x でも同様のことができるかも知れません。

以上で、 iTerm2 上で Home/End キーによる行頭/行末移動ができるようになります。

anyenv で tfenv を入れると init でエラーが出るので回避する

数多ある *env 系のツール導入をぐっと簡単にしてくれる anyenv ですが、現行バージョン (1.1.1) では、tfenv を導入すると、シェルログイン時にエラーが出るようになります。

No such command 'init'
Usage: tfenv <command> [<options>]

Commands:
   install       Install a specific version of Terraform
   use           Switch a version to use
   uninstall     Uninstall a specific version of Terraform
   list          List all installed versions
   list-remote   List all installable versions

anyenv はログイン時、導入されている *env すべての init サブコマンドを順に実行するのですが、 tfenv には init サブコマンドがないのでエラーとなるようです。

修正 PR も上がっているようなのでそのうち取り込まれると思いますが、当面の回避方法のメモ。

ここでは、Mac で homebrew を使って anyenv をインストールした前提で書きます。

anyenv のインストール先を確認

% brew ls anyenv
/usr/local/Cellar/anyenv/1.1.1/bin/anyenv
/usr/local/Cellar/anyenv/1.1.1/completions/ (3 files)
/usr/local/Cellar/anyenv/1.1.1/libexec/ (14 files)

で、 libexec 以下の anyenv-init を修正します。

% cp /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup
% vim /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init

修正内容は以下のような感じ。

% diff -up /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init
--- /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init.backup 2020-01-26 22:57:10.000000000 +0900
+++ /usr/local/Cellar/anyenv/1.1.1/libexec/anyenv-init    2020-01-26 22:59:09.000000000 +0900
@@ -140,5 +140,5 @@ for env in $(anyenv-envs); do
     ;;
   esac

-  echo "$(${ENV_ROOT}/bin/${env} init - ${no_rehash_arg}${shell})"
+  [ "${env}" != 'tfenv' ] && echo "$(${ENV_ROOT}/bin/${env} init - ${no_rehash_arg}${shell})"
 done

homebrew を使わず git clone でインストールした場合は、 .anyenv/libexec/anyenv-init を同じように修正すればOKです。

本当はちゃんと 「init のない *env」を判定したいところなんですが、まあ一時的な回避策ということで。