2022年ももう5ヶ月が終わったんですね…。
かつて大人に言われた「年を取ると1年があっという間」という言葉を痛感しています。

さて、今回はとある OSS ツールに修正 PR を投げ、それがマージされた話を書きます。

はじめに

皆さんは AWS お使いでしょうか。
最近は個人でも使っている方が多い印象で、かくいう私もその一人です。
従量課金という仕組みから中々手が出しづらいのも事実ですが、無料利用枠(期間限定/半永続的)もあるので気になる方・使ってみたいと思っていた方はぜひ使ってみることをお勧めします。
※AWS に限った話ではありませんが、ちゃんとドキュメントを読んでおかないと意図しない課金が発生するのでご注意ください。

そんな AWS には、ECS というコンテナオーケストレーションサービスがあります。
昨今何かと話題のコンテナ技術を利用し、Web サービスやバッチ処理を実行することができます。
設定については AWS マネジメントコンソールから操作することも可能なのですが、AWS CLI を利用することでコマンドベースで操作することも可能です。
この「コマンドベースで操作できる」というのはサービスを運用していくにあたりかなり重要で、これによって再現性の高い手順で自動化でき、改修~リリースという流れを継続的かつスピーディーに実行していくことが可能です。

前提

私もいくつかの仕事で ECS を利用しているのですが、AWS リソースの作成とアプリケーションのデプロイについては以下のような方針を取ることが多いです。

  • リソースの作成: Terraform
  • デプロイ: ecspresso/ecschedule(それぞれ別の OSS ツールです)

Terraform は IaC ツールの1つで、インフラリソースの構築手順をコードに落とし込むことができます。
以前の記事でも紹介していますので、興味がある方は以下も合わせてご覧ください。

Terraform ではリソースの状態を tfstate というファイルで管理します。
上記ファイルと実際の作成状態を比較して、登録・更新・削除などを判断しています。

ecspresso/ecschedule は OSS ツールで、ECS へのデプロイに特化しています。
ecspresso はタスク定義の登録やサービスの更新、ecschedule はスケジュールタスク(バッチ処理)の登録・更新ができます。
デプロイについては Terraform で実施することもできるのですが、以下のような事情で分けています。

  • リソースの作成とアプリケーションのデプロイは異なるライフサイクルになることが多い
  • アプリケーションエンジニアに Terraform を覚えてもらうのはコストがかかる

何をやったのか

ecspresso/ecschedule ともに、利用するタスク定義ファイルや設定ファイルで AWS リソースの id や arn (Amazon Resource Name) を参照することがあります。
とはいえ、インフラ都合でこれらの値が変更された場合に、上記設定ファイルを都度修正するのは結構手間です。
ecspresso では前述の tfstate を参照できるので、リソースの id や arn を設定ファイルにべた書きしなくて済みます。
例えば

{
  "executionRoleArn": "arn:aws:iam::123456789012:role/myapp-ecs-task-execution-role",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/app:latest"
    }
  ],
  "portMappings": [
    {
      "hostPort": 80,
      "protocol": "tcp",
      "containerPort": 80
    }
  ],
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "myapp",
      "awslogs-region": "ap-northeast-1",
      "awslogs-stream-prefix": "app"
    }
  },
  "essential": true,
  "memory": "512",
  "taskRoleArn": "arn:aws:iam::123456789012:role/myapp-ecs-task-role",
  "family": "myapp",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "256"
}

というように書いてあるタスク定義ファイルを

{
  "executionRoleArn": "{{ tfstate `identity.aws_iam_role.ecs_task_execution_role.arn` }}",
  "containerDefinitions": [
    {
      "name": "app",
      "image": "{{ tfstate `application.aws_ecr_repository.app.repository_url` }}:latest"
    }
  ],
  "portMappings": [
    {
      "hostPort": 80,
      "protocol": "tcp",
      "containerPort": 80
    }
  ],
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "{{ tfstate `log.aws_cloudwatch_log_group.app.name` }}",
      "awslogs-region": "ap-northeast-1",
      "awslogs-stream-prefix": "app"
    }
  },
  "essential": true,
  "memory": "512",
  "taskRoleArn": "{{ tfstate `identity.aws_iam_role.ecs_task_role.arn` }}",
  "family": "myapp",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "256"
}

というように書くことができます。
上記ファイルをバージョン管理しておき、デプロイ時は tfstate を参照する権限があるユーザ・ロールを利用することで、インフラ都合によらずデプロイすることが可能になります。
ただ、この機能は ecschedule では利用できなかったので、ecschedule でもこの機能が使えるように対応しました。

どのように修正したのか

修正 PR はこちらです。
ecspresso の処理を参考に実装しました。
大きくは以下2点です。

  • ecschedule の設定ファイル (yaml) の解析時に tfstate を読み込む機能を追加
  • yaml 内で tfstate が定義されている箇所を実際の値に置換

この修正がマージされたことにより、ecschedule バージョン 0.5.0 以降を利用することで、これまで

region: ap-northeast-1
cluster: app
rules:
- name: daily_batch
  description: 日次バッチ
  scheduleExpression: cron(0 15 * * ? *)
  taskDefinition: app
  launch_type: FARGATE
  platform_version: LATEST
  network_configuration:
    aws_vpc_configuration:
      subnets:
      - subnet-12345678
      - subnet-01234567
      security_groups:
      - sg-11111111
      assign_public_ip: DISABLED
plugins:
- name: tfstate
  config:
    url: ./myapp.tfstate

というように書いていた設定ファイルを

region: ap-northeast-1
cluster: app
rules:
- name: daily_batch
  description: 日次バッチ
  scheduleExpression: cron(0 15 * * ? *)
  taskDefinition: app
  launch_type: FARGATE
  platform_version: LATEST
  network_configuration:
    aws_vpc_configuration:
      subnets:
      - {{ tfstate `network.aws_subnet.private['1a'].id` }}
      - {{ tfstate `network.aws_subnet.private['1c'].id` }}
      security_groups:
      - {{ tfstate `security_group.aws_security_group.app.id` }}
      assign_public_ip: DISABLED
plugins:
- name: tfstate
  config:
    url: ./myapp.tfstate

というように書けるようになります。

tfstate の読み込みには、ecspresso でも使われているtfstate-lookupというライブラリを使いました。
そこで読み込んだ値をメモリ上に持ち、yaml から読み込んだ {{ tfstate `hoge` }} を実際の値に変換してスケジュールタスクを登録します。

苦労したのは、yaml ファイルの読み込み時点では tfstate を参照できない問題をどうするかでした。
当然ですが、yaml ファイルを読み込んだ時点では tfstate の格納場所はわかっても実際の値は読み込んでいません。
そのため、yaml 読み込み後には変換対象を一時変数に格納しておき、tfstate から値を読み込んだ後で実際の値に変換するという手順を踏んでいます。

まとめ

普段お世話になっているツールには何かしら貢献していきたいと常々思っているので、マージされた時の喜びも一入でした。
今回は Go のコードに関する具体的な説明は割愛しているので、機会があれば細かく書こうと思います。