Lambdaを使ってリポジトリ内のファイルを取得しGitHub Issueを作成する
はじめに
こんにちは、M-Yamashitaです。
業務でAWS Lambda(以降、Lambda)を使うことになったので、お試しとしてLambdaを使ってリポジトリ内のファイルを取得しGitHub Issueを作成することをやってみることにしました。業務で活かせることを考え、GitHub上の個人で作成した組織を使用し、その組織にあるリポジトリからマニフェストを取得することを考えます。
なお今回の記事は、いわゆるやってみた記事となるため、作成手順に加えて時折発生したエラーとその原因・解決策も載せています。そのため脱線が多い内容となりますのでご注意ください。
前提
使用バージョン
ローカル環境のMacOS: Sequoia 15.3.2
Ruby: 3.3
Terraform: 1.9.5
本記事の前に実施済みなこと
- ローカル環境へのTerraformのインストール
- GitHubとAWSとのOIDC連携
構成図
今回作成しようとする構成図は次のとおりです。
AWS上でLambda、Amazon ECR(以降、ECR)、AWS Secrets Manager(以降、Secrets Manager)を使い、GitHubのリポジトリ内にあるマニフェストファイルからnamespace一覧を取得できる構成となります。またLambdaはRubyのコンテナを使用し、GitHub Appsを使用してリポジトリにアクセスするようにします。
なお、AWSの各サービスの構築に関してはTerraformを使用します。さらにCI/CD環境を構築し、GitHub Actionsを使用してECRにイメージをプッシュし、Lambdaをデプロイするようにします。
システムの構築
GitHub Appsの作成および対象の組織へのインストール
まずはGitHub Appsを作ります。GitHubの公式ドキュメントやZennの記事をもとに、組織向けのGitHub Appsを作成します。
基本的には公式ドキュメントに沿って必要項目を埋めておきます。Expire user authorization tokens
についてはGitHubがチェックしたままを推奨しているのでそのままにしておきます。またPermissions項目については、今回はRepositoryからのコンテンツ取得、およびissueを作成するので、Repository permissions
を開き、以下2つの項目の権限を変更します。
-
Contents
:Read-only
-
Issues
:Read and write
実際に設定した画面はこちらです。
これで保存すると、次のような画面が表示されます。
この画面にて、Client ID
の値があるので控えておきます。また、この画面下部にあるGenerate a private key
を押下し、private keyを生成しておきます。これも後ほど必要になりますので、忘れずに控えておきます。
左サイドメニューのInstall App
を選択すると、次のような画面が表示されますので、GitHub Appsを対象組織にインストールすることでGitHub Appsの設定は完了となります。
GitHub AppsのクレデンシャルをSecrets Managerに登録する
Secrets Managerを開き、シークレットを新しく保存します。シークレットタイプについては、今回GitHub Appsの秘匿情報を追加するためその他のシークレットのタイプ
を選択します。
キー/値のペア
項目では、先ほどのClient ID
とprivate key
の値をそれぞれGITHUB_APPS_CLIENT_ID
、GITHUB_APPS_PRIVATE_KEY
として登録します。
Client ID
やprivate key
を設定後、Secrets Manager上でのシークレットの名前を指定します。ここではgithub_apps_secrets
と指定しました。出来上がったシークレットは以下画像のようになります。
Lambda上のRubyでの実装およびテスト環境の整備
Rubyでの実装
次にLambdaで実行するRubyのコードを準備していきます。コードで実現したいことは以下のとおりです。
- GitHub Appsの
Client ID
とprivate key
をSecretsManagerから取得する - octokitとjwtのgemを使い、GitHub Appsを使ったoctokitのクライアントを作成する
- 2のクライアントを使い、GitHubのリポジトリからマニフェストファイルを取得する
- マニフェストファイルからnamespaceを取得し、2のクライアントを使ってissueとして投稿する
AWSの公式ドキュメントにて、以下のようなサンプルコードが用意されていますのでそれを元に作成していきます。
module LambdaFunctions
class Handler
def self.process(event:,context:)
"Hello!"
end
end
end
また先ほどの公式ドキュメントにはてベストプラクティスも記載してあります。それによると、次のようなコアロジックの分離での実装をすすめられているので、実装ではそこの部分も考慮した形としました。
- Lambda ハンドラーをコアロジックから分離します。
def lambda_handler(event:, context:) foo = event['foo'] bar = event['bar'] result = my_lambda_function(foo:, bar:) end def my_lambda_function(foo:, bar:) // MyLambdaFunction logic here end
この実装を進めるうえでのポイントの1つとして、gemのインストールをどこで実施するかという点があります。方法としては2つあり、1つはLambdaを実行した際にgemをリアルタイムでインストールする方法、もう1つはコードをコンテナ化しておき、Gemfileにgemを定義してビルド時にインストールしておく方法があります。
当初コンテナ化するほどでもないかなと思い、リアルタイムでgemをインストールする方法を試してみました。ですがgemの依存ライブラリがNative extensionのgemとなっており、そのインストールが毎回エラーとなってしまっていました。このエラーに対してこれといった解決策が見つからず長い間つまづいてしまっており、これ以上の時間を投資しても解決できるか不明な状態でした。
そのためリアルタイムでのインストールはいったん諦めて、コンテナ化する方向に舵を切りました。この時のdockerfileとしては以下の通りです。
FROM ruby:3.3.0-slim-bookworm AS build
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libxml2-dev \
libxslt1-dev \
libffi-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN gem install bundler:2.5.3
WORKDIR /app
# Copy Gemfile and Gemfile.lock
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local path 'vendor/bundle'
RUN bundle install
FROM public.ecr.aws/lambda/ruby:3.3
COPY /app/vendor/bundle vendor/bundle
# Copy function code
COPY lib/*.rb ${LAMBDA_TASK_ROOT}/
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.LambdaFunction::Handler.process" ]
このdockerfileではマルチステージビルドを採用し、ビルド後のイメージを軽量化するようにしました。この軽量化の目的として、不要なファイルやライブラリを含めないことによるセキュリティリスクの低減や、後述するECRにアップロードした際の課金額を減らす狙いがあります。
これで実装は完了です。
テスト環境の整備
コードが出来上がったのでテスト環境を整えます。今回はコード量が少なくやりたいこともシンプルだったのでコードを先に作った後にテスト環境を整えることにしました。
今回のテスト環境の整備においてテストフレームワークには手慣れたRSpecを採用しました。またテスト実施にあたってテスト用のprivate keyが必要と思われたので、octokitリポジトリからテスト用のprivate keyを取得しました。
加えて、Secrets Managerからの取得をテストできるようにLocalStackを導入します。LocalStackの導入が初めてだったこともありいろいろとつまづいてしまったので、導入手順も含めて記載します。LocalStackの導入
LocalStackの構築に関しては、以下公式ドキュメントとZennの記事を参考に構築します。
構築にはCLIやdocker compposeを使った方法があります。後述するCI環境でRSpecを使ったテストを実行する際、docker composeでLocalStackを起動しておく方法がシンプルで分かりやすいと考えたので、 docker composeの使用を選択しました。公式ドキュメントにcomposeファイルのテンプレートがあるので、これを元にSERVICES=secretsmanager
を指定します。作成したcomposeファイルは次のとおりです。
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
image: localstack/localstack
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
environment:
- SERVICES=secretsmanager
- DEBUG=${DEBUG:-0}
volumes:
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
次に、LocalStack上のsecretsmanagerに値が登録されるようにします。この値の登録に関して調べたところ、LocalStackにはライフサイクルのフェーズがあるようです。
LocalStack has four well-known lifecycle phases or stages:
- BOOT: the container is running but the LocalStack runtime has not been started
- START: the Python process is running and the LocalStack runtime is starting
- READY: LocalStack is ready to serve requests
- SHUTDOWN: LocalStack is shutting down
You can hook into each of these lifecycle phases using custom shell or Python scripts. Each lifecycle phase has its own directory in /etc/localstack/init. You can mount individual files, stage directories, or the entire init directory from your host into the container./etc └── localstack └── init ├── boot.d <-- executed in the container before localstack starts ├── ready.d <-- executed when localstack becomes ready ├── shutdown.d <-- executed when localstack shuts down └── start.d <-- executed when localstack starts up
この仕様を見ると、各フェーズにおいてそのディレクトリに配置したファイルが実行されるようです。そのため、secretsmanagerへの登録処理を記載したinit.shとシークレット情報を持ったsecrets.jsonを作成し、etc/localstack/init/boot.dディレクトリに配置すると、LocalStackの起動時にシークレットが登録されるようになります。値の登録処理ではawslocal
コマンドを使用します。これはLocalStackのCLIコマンドであり、aws
コマンドのラッパーとなります。そのためawslocal
コマンドの使用方法はaws
コマンドに似た使用方法となります。
AWSの公式ドキュメントを見てみるとシークレットの登録例が掲載されています。この処理を元に、init.shの中身を以下のようにします。
#!/bin/bash
awslocal secretsmanager create-secret \
--name github_apps_secrets\
--description "LocalStack Secret" \
--secret-string file:///etc/localstack/init/ready.d/secrets.json
次にsecrets.jsonを作ります。secrets.jsonはコード管理することもあり本番用のシークレット情報は載せられません。そのためテスト用の値をセットしておきます。
{
"GITHUB_APPS_PRIVATE_KEY": "private_key",
"GITHUB_APPS_CLIENT_ID": "client_id"
}
また、init.shやsecrets.jsonをLocalStackの起動前に配置しておく必要があるので、LocalStackをコンテナ化します。
FROM localstack/localstack
COPY ./init.sh /etc/localstack/init/ready.d/init.sh
COPY ./secrets.json /etc/localstack/init/ready.d/secrets.json
RUN apt-get update && apt-get install -y jq
RUN chmod u+x /etc/localstack/init/ready.d/init.sh
補足としてjq
を入れたのはデバッグ用のためです。またLocalStackをコンテナ化したので先ほどのcomposeファイルも修正しておきます。
docker composeを使ってコンテナを起動後シークレットが登録されているかどうか確認します。この確認においてはコンテナ外からのアクセスでチェックします。この理由としては後述するCI環境において、RSpecの実行環境はコンテナではないため、コンテナ外からコンテナのLocalStackにアクセスした際に問題なく値を取れるようにしておくためです。
確認する際のコマンドには、ローカル環境にインストールしていたaws
コマンドを使います。なおaws
コマンド実行において、~/.aws/config
や~/.aws/credentials
に以下のようなdefault
設定があったので、profileの引数指定は不要でした。
[default]
region = ap-northeast-1
[default]
aws_access_key_id = xxx
aws_secret_access_key = xxx
この前提でaws secretsmanager list-secrets
を実行します。endpoint
の引数にはLocalStackのURL、region
の引数にはLocalStackのデフォルトリージョンであるus-east-1
を指定します。すると以下のようなレスポンスが返ってきて、シークレットが登録されていることが確認できました。
❯ aws secretsmanager list-secrets --endpoint-url=http://localhost:4566 --region us-east-1
{
"SecretList": [
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:github_apps_secrets-FsVdXB",
"Name": "github_apps_secrets",
"Description": "LocalStack Secret",
"LastChangedDate": "2025-04-01T22:53:02.256303+09:00",
"LastAccessedDate": "2025-04-01T09:00:00+09:00",
"SecretVersionsToStages": {
"3d8e19ce-887b-4e3c-9bea-9bacf32278c1": [
"AWSCURRENT"
]
},
"CreatedDate": "2025-04-01T22:53:02.256303+09:00"
}
]
}
念の為、aws secretsmanager get-secret-value
を使って--secret-id
の引数にシークレット名を指定し、シークレットの値を取得してみます。すると以下のようにシークレットの値が取得できました。
❯ aws secretsmanager get-secret-value --secret-id github_apps_secrets --endpoint-url=http://localhost:4566 --region us-east-1
{
"ARN": "arn:aws:secretsmanager:us-east-1:000000000000:secret:github_apps_secrets-FsVdXB",
"Name": "github_apps_secrets",
"VersionId": "3d8e19ce-887b-4e3c-9bea-9bacf32278c1",
"SecretString": "{\n \"GITHUB_APPS_PRIVATE_KEY\": \"private_key\",\n \"GITHUB_APPS_CLIENT_ID\": \"client_id\"\n}",
"VersionStages": [
"AWSCURRENT"
],
"CreatedDate": "2025-04-01T22:53:02+09:00"
}
これでLocalStackの導入は完了です。
CI環境の整備
コードとテスト環境が整ったので次にCI環境を整えます。CI環境には手慣れたGitHub Actionsを使用します。CIの流れとしてはLocalStackを起動しその後にテストを実行する形にしました。LocalStackの起動にはdocker composeを使用しているため、RSpecの実行時にLocalStackが起動している必要があります。そのためdocker composeでの起動後に、LocalStackの起動完了に必要な時間を見積もったsleep
コマンドを入れ、その後にRSpecを実行するようにしました。
コード例は以下のようになります。
name: RSpec
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run tests
run: |
docker compose -f compose.yaml up -d
sleep 10
bundle exec rspec
docker compose -f compose.yaml down
ワークフローはできたものの、CI実行時のエラーと改善点がそれぞれ1つずつ見つかりました。
Aws::InstanceProfileCredentials::TokenRetrivalError
CI実行時のエラー: ワークフローでのRSpec実行時に以下のエラーが発生しました。
Error retrieving instance profile credentials: Aws::InstanceProfileCredentials::TokenRetrivalError
・・・略・・・
Aws::Errors::MissingCredentialsError:
unable to sign request without credentials set
・・・略・・・
上記エラーが発生したときの対象のコードは以下の通りです。
params = { region: 'ap-northeast-1' }
# if localstack's endpoint is set, use it and set region to us-east-1 for localstack
endpoint = ENV.fetch('AWS_SECRET_MANGER_ENDPOINT', nil)
if endpoint
params[:endpoint] = endpoint
params[:region] = 'us-east-1'
end
@client = Aws::SecretsManager::Client.new(params)
原因と解決策
エラーの最初の部分だけ見るとわかりませんが、よく読んでみるとcredentialsが見つからないというエラーのようです。一応それ以外にも問題がないか調べるためGitHub Actionsのコードにデバッグコードを仕込んでみましたが、LocalStack用のendpoint
の環境変数、接続コード自体は問題なさそうでした。
credentialsの読み込みについてはAWS SDK for Rubyのgemが管理しているようでした。そのREADMEを読んでみると次のような記載があります。
- The shared credentials ini file at ~/.aws/credentials. The location used can be changed with the AWS_CREDENTIALS_FILE ENV variable.
- Unless ENV['AWS_SDK_CONFIG_OPT_OUT'] is set, the shared configuration ini file at ~/.aws/config will also be parsed for credentials.
これを今の状況に当てはめて考えると、ローカル環境上ではすでに~/.aws/config
や~/.aws/credentials
が存在しているので、コード上でAws::SecretsManager::Client.new(params)
を実行した際、new
の引数にcredentials
がなくてもデフォルトでローカルの設定を読み込んでくれます。
一方、GitHub Actions上では~/.aws/config
や~/.aws/credentials
は存在しないため、credentials
の引数を指定しないとエラーが発生してしまうようでした。
そのためLocalStackを使うケースでは、以下のようにAws::SecretsManager::Client
の初期化時にcredentials
の引数も与える形にします。
params = { region: 'ap-northeast-1' }
# if localstack's endpoint is set, use it and set region to us-east-1 for localstack
endpoint = ENV.fetch('AWS_SECRET_MANGER_ENDPOINT', nil)
if endpoint
params[:endpoint] = endpoint
params[:region] = 'us-east-1'
params[:credentials] = Aws::Credentials.new('dummy', 'dummy') # これを追加
end
@client = Aws::SecretsManager::Client.new(params)
この修正によりCI実行時のエラーを解決できました。
改善ポイント: docker composeのヘルスチェック追加
RSpec実行前にsleep
を入れていますが、これはLocalStackの起動に必要な時間を見積もっているだけなので、LocalStackが起動完了したかどうかは確認できていません。そのため、docker composeによる起動が想定以上に時間がかかってsleep
の時間をオーバーしてしまい、LocalStack起動完了前にRSpecが実行されて失敗することも考えられます。加えてsleep
を使う方法はスマートではないと感じました。
これを改善すべく何か良い方法がないかと調べてみたところ、docker composeにはヘルスチェック機能が存在するようです。またそのヘルスチェックが通るまで待つ--wait
オプションもあるようです。
公式ドキュメントにも記載があります。
- ヘルスチェックの書き方:
- ヘルスチェックの各項目について
- waitオプション
このヘルスチェックを使うにあたり、LocalStackにおけるヘルスチェックのエンドポイントを調べたところ、公式ドキュメントに記載がありました。
The API path for the LocalStack internal resources is /_localstack. Several endpoints are available under this path. For instance, /_localstack/health checks the available and running AWS services in LocalStack while /_localstack/diagnose (enable with the DEBUG=1 configuration variable), reports extensive and sensitive data from the LocalStack instance.
https://docs.localstack.cloud/references/internal-endpoints/
この文章を読むと、LocalStackのヘルスチェックのエンドポイントは/_localstack/health
となるようです。そのため、http://localhost:4566/_localstack/health
で確認ができそうでした。試しにcurlで叩いて、jq
でレスポンスを整形すると以下のようなレスポンスが返ってきました。
❯ curl -s http://localhost:4566/_localstack/health | jq
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "disabled",
"dynamodbstreams": "disabled",
"ec2": "disabled",
"es": "disabled",
"events": "disabled",
"firehose": "disabled",
"iam": "disabled",
"kinesis": "disabled",
"kms": "available",
"lambda": "available",
"logs": "disabled",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "running",
"ses": "disabled",
"sns": "disabled",
"sqs": "disabled",
"ssm": "disabled",
"stepfunctions": "disabled",
"sts": "available",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled"
},
"edition": "community",
"version": "4.2.1.dev7"
}
このレスポンスを見ると、ヘルスチェックのエンドポイントはLocalStackで使用可能なサービスの一覧とそのステータスを返してくれるようです。今回使用するsecretsmanager
の状態は"secretsmanager": "running",
となっていました。
そのため、running
になっていることをヘルスチェックの条件に追加したcomposeファイルに修正します。
version: "3.6"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
build: ./localstack
ports:
- "127.0.0.1:4566:4566"
environment:
- SERVICES=secretsmanager
- DEBUG=${DEBUG:-0}
healthcheck: # このhealthcheck項目を追加
test: [ "CMD-SHELL", "curl -s http://localhost:4566/_localstack/health | jq '.services.secretsmanager' | grep running" ]
interval: 1s
timeout: 30s
retries: 10
start_period: 1s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
このようにすることで、LocalStackの起動後にヘルスチェックが実行され、secretsmanager
の状態がrunning
になるまで待機してくれるようになります。
これでsleep
コマンドを外すことができます。スマートにしたワークフローは以下の通りです。
name: RSpec
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run tests
run: |
docker compose -f compose.yaml up -d --wait
bundle exec rspec
docker compose -f compose.yaml down
これでCI環境の整備は完了です。次はTerraformでのAWS上の各サービス構築を行います。
Terraformの初期設定およびIAMユーザーやECRの作成
ここではTerraformで初期設定した後にIAMユーザー、ECRをそれぞれ構築します。
初期設定: S3をbackendにする
初期設定に関する記事がありましたので、それに沿ってTerraform用のIAMユーザー(lambda-terraformer
)を作成し、アクセスキーとシークレットキーを取得します。なお、IAMユーザーのポリシーにはAmazonS3FullAccess
を付与しています。
aws configure
を実行し、先ほどのキーを入力します。
❯ aws configure --profile lambda-terraformer
AWS Access Key ID [None]: xxx
AWS Secret Access Key [None]: xxx
Default region name [None]: ap-northeast-1
Default output format [None]:
プロファイルが正常に追加されたことを確認します。
❯ aws s3 ls --profile lambda-terraformer
エラーが出ないので問題ないようです。
次にTerraformのbackendをS3に設定します。先ほどの記事を参考に作成したコードは次のとおりです。
provider "aws" {
region = "ap-northeast-1"
profile = "lambda-terraformer"
}
terraform {
required_version = "~> 1.9.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.91.0"
}
}
backend "s3" {
bucket = "sample-rb-lambda-tfstate"
region = "ap-northeast-1"
profile = "lambda-terraformer"
key = "production.tfstate"
encrypt = true
}
}
resource "aws_s3_bucket" "sample_rb_lambda_bucket" {
bucket = "sample-rb-lambda-tfstate"
}
このコードを使ってterraform init
を実行するとbucket作成とbackend設定を同時に行ってしまいエラーが発生します。
❯ terraform init
Initializing the backend...
╷
│ Error: Failed to get existing workspaces: S3 bucket "sample-rb-lambda-tfstate" does not exist.
│
│ The referenced S3 bucket must have been previously created. If the S3 bucket
│ was created within the last minute, please wait for a minute or two and try
│ again.
│
│ Error: operation error S3: ListObjectsV2, https response error StatusCode: 404, RequestID: YJ3AEJS6FNMM5SHS, HostID: 0KcfwnKlVjIoExcXtAizkxK0czzYaIPeibZT6IrRXe2vFBMt1zC5NU8QMMElAlsAjoTh1tXt2NY=, NoSuchBucket:
│
調べてみると同様のエラーに出会っている人がいるようでした。
解決策として、一旦ackend設定をコメントアウトしてterraform apply
まで完了させ、コメントアウトを外し再度terraform init
を実行すると良いようです。
そのためまずはbackend設定をコメントアウトしterraform apply
まで実行します。
❯ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.91.0"...
- Installing hashicorp/aws v5.91.0...
- Installed hashicorp/aws v5.91.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
❯ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
+ create
## 以下省略
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
backend設定のコメントアウトを元に戻し、改めてterraform init
を実行します。ローカル上にあるterraform.tfstate
をコピーしますか?と聞かれるのでyesを選択します。
❯ terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "s3" backend. No existing state was found in the newly
configured "s3" backend. Do you want to copy this state to the new "s3"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.91.0
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
これで初期設定は完了です。
ECR作成
次はECRの作成です。ただし、いきなりECRを作成することはできないので、その前にECR作成用のIAMポリシーを作成します。
ECR作成用のIAMポリシーを作成
Terraform用のIAMユーザーlambda-terraformer
に、次のようなECR用のインラインポリシーを追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Action": [
"ecr:CreateRepository",
"ecr:DescribeRepositories",
"ecr:DeleteLifecyclePolicy",
"ecr:DeleteRepository",
"ecr:GetLifecyclePolicy",
"ecr:ListTagsForResource",
"ecr:PutLifecyclePolicy"
],
"Resource": "*"
}
]
}
これでECRを作成する準備が整いました。
ECR作成
ECR用のポリシーがIAMユーザーにセットされたのでECRを作成します。今回のケースでは、Lambdaに使用するイメージはlatest運用としたいのでMUTABLE
をセットしておきます。またイメージは最新の1つだけあれば良いので、untaggedとなった不要なイメージは早めに削除し課金を防ぐようにライフサイクルを設定します。
公式ドキュメントのaws_ecr_repository
やaws_ecr_lifecycle_policy
のサンプルコードを見ると、実現したいことがほぼそのまま書いてあるようでした。そのため、そのサンプルコードをベースに作成します。作成したコードは次のとおりです。
resource "aws_ecr_repository" "sample_rb_lambda_ecr_repository" {
name = "sample-rb-lambda"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
resource "aws_ecr_lifecycle_policy" "sample_rb_lambda_ecr_lifecycle_policy" {
repository = aws_ecr_repository.sample_rb_lambda_ecr_repository.name
policy = <<EOF
{
"rules": [
{
"rulePriority": 1,
"description": "Expire images older than 1 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 1
},
"action": {
"type": "expire"
}
}
]
}
EOF
}
ECRへイメージをプッシュする環境の整備
前述の段階でECRの作成まで完了しました。次はLambda構築に移りたいところですが、ECRにイメージがない状態でLambdaを作成しても動作できないので意味がありません。
そのためECRにイメージをプッシュできる環境を作成します。この作成において、同じようなことをしている人がいたので、その方のコードを参考にさせていただきました。
この記事をベースに以下3つを実施していきます。
- Terraform用のIAMユーザー
lambda-terraformer
にIAM Roleを作成する権限を付与 - GitHub ActionsがECRにプッシュするためのIAMロールの作成
- GitHub ActionsでECRにプッシュするためのワークフロー作成
なお、AWS上でOIDCの設定はすでに完了している前提で進めます。
Terraform用のIAMユーザーにIAM Roleを作成する権限を付与
ECRにプッシュするためのIAM Roleを作成するためには、IAM Roleを作成する権限が必要です。そこでTerraform用のIAMユーザーlambda-terraformer
に、インラインポリシーとしてiam-role-manage-policy
を作成しました。付与した権限は以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:UpdateAssumeRolePolicy",
"iam:ListInstanceProfilesForRole",
"iam:DeleteRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:CreateRole",
"iam:DeleteRole",
"iam:PutRolePolicy",
"iam:ListRolePolicies",
"iam:GetRolePolicy"
],
"Resource": "arn:aws:iam::<aws account id>:role/*"
}
]
}
GitHub ActionsがECRにプッシュするためのIAM Roleの作成
GitHub ActionsがECRにプッシュするためのIAM Roleを作成します。このIAM Roleの信頼ポリシーには、ECRにプッシュできるリポジトリやブランチ名も指定することができるようでした。
そのため、リポジトリ名はコードを置いているリポジトリ(M-Yamashita01/sample-rb-lambda
)とし、ブランチ名に関してはmain
ブランチを指定しました。これにより、GitHub Actionsでmain
ブランチにpushした際のみECRにプッシュできるようになります。
作成したコードは次のとおりです。
resource "aws_iam_role" "github_actions_ecr_push_role" {
name = "github-actions-ecr-push-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Federated = "arn:aws:iam::<aws account id>:oidc-provider/token.actions.githubusercontent.com"
},
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:sub" : "repo:M-Yamashita01/sample-rb-lambda:ref:refs/heads/main"
}
}
}
]
})
}
resource "aws_iam_role_policy" "ecr_push_image_policy" {
name = "ecr-push-image-policy"
role = aws_iam_role.github_actions_ecr_push_role.name
policy = data.aws_iam_policy_document.ecr_push_image_poclicy_document.json
}
data "aws_iam_policy_document" "ecr_push_image_poclicy_document" {
# ECRログインに必要
statement {
effect = "Allow"
actions = ["ecr:GetAuthorizationToken"]
resources = ["*"]
}
# `docker push`に必要。resourcesにはECRリポジトリのARNを指定
statement {
effect = "Allow"
actions = [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
]
resources = ["arn:aws:ecr:ap-northeast-1:<aws account id>:repository/sample-rb-lambda"]
}
}
GitHub ActionsでECRにプッシュするためのワークフロー作成
GitHub Actions用のIAM Roleが作成されたので、そのIAM Roleを使用したGitHub Actionsのワークフローを作成します。
ワークフローでの秘匿情報のハードコードを避けるため、先ほどのIAM RoleのARNはECR_PUSH_AWS_ROLE_ARN
という名前でGitHubのシークレットに登録しておきます。またそれ以外のAWSアカウントやリージョン、リポジトリ名も同様に登録しておきます。
作成したGitHub Actionsのワークフローは次のとおりです。
name: Build and Push
on:
push:
branches: [ "main" ]
permissions:
contents: read
id-token: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.ECR_PUSH_AWS_ROLE_ARN }}
- name: Log in to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build Docker image
run: docker build -t ${{ secrets.ECR_REPOSITORY }}:latest .
- name: Tag Docker image
run: docker tag ${{ secrets.ECR_REPOSITORY }}:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:latest
- name: Push Docker image to ECR
run: docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/${{ secrets.ECR_REPOSITORY }}:latest
main
ブランチに適当な修正コードをpushしてGitHub Actionsを実行することで、ECRにイメージがプッシュされました。
TerraformでLambdaを作成する
Lambda作成
aws_lambda_function
を参考に、LambdaをTerraformで作成します。このLambdaはコンテナを使って起動するため、package_type
にはImage
を指定します。また、image_uri
には前述でECRにプッシュしたイメージのURIを指定します。コンソール上で見ると以下のようなURIになっていると思います。
このURIをTerraform上のコードを使って表すと"${aws_ecr_repository.sample_rb_lambda_ecr_repository.repository_url}:latest"
となります。そのため、LambdaのTerraformコードは次のようになります。
data "aws_iam_policy_document" "sample_rb_lambda_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role" "sample_rb_lambda_iam_role" {
name = "sample-rb-lambda-iam-role"
assume_role_policy = data.aws_iam_policy_document.sample_rb_lambda_assume_role.json
}
resource "aws_lambda_function" "sample_rb_lambda_function" {
function_name = "sample-rb-lambda"
package_type = "Image"
image_uri = "${aws_ecr_repository.sample_rb_lambda_ecr_repository.repository_url}:latest"
role = aws_iam_role.sample_rb_lambda_iam_role.arn
timeout = 60
}
LambdaをapplyするためのIAMインラインポリシーを作成
Terraform用のIAMユーザーlambda-terraformer
にLambdaを作成するためには、IAMのインラインポリシーが必要です。一通り必要な項目を埋めてterraform apply
を実行すると、PassRoleが付与されてないので実行できないというエラーが発生しました。
│ Error: creating Lambda Function (sample-rb-lambda): operation error Lambda: CreateFunction, https response error StatusCode: 403, RequestID: 610eb31c-ace0-468b-8407-2225d1a70f76, api error AccessDeniedException: User: arn:aws:iam::<aws account id>:user/lambda-terraformer is not authorized to perform: iam:PassRole on resource: arn:aws:iam::<aws account id>:role/sample-rb-lambda-iam-role because no identity-based policy allows the iam:PassRole action│
PassRoleについてよく分からなかったので、クラスメソッドさんの記事を参考にしました。
理解が難しかったので何回か読み直し、まだ完璧ではないものの以下の結論を咀嚼して飲み込みました。
・IAM ロールはお面のようなもの
・PassRole は人間以外にもお面を与えること
このことを踏まえたうえで、PassRole権限の追加におけるベストな方法を検討しました。今回のケースではTerraform用のIAMユーザーlambda-terraformer
にPassRoleの権限を追加し、Resourcesに指定されたロールを渡すことができるようになります。ただResourceを*
にすると全てのIAM Roleを渡せることになってしまい、セキュリティ的に問題があります。そのためResourceにはTerraformコード上に書いたsample-rb-lambda-iam-role
のIAM RoleをPassRoleできるようにします。さらにこのPassRoleできるサービスをLambdaだけに絞ります。
この結果、Terraform用のIAMユーザーlambda-terraformer
に追加するインラインポリシー(lambda-manage-policy
)は以下のようになります。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ecr:SetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy",
"ecr:GetRepositoryPolicy"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::<aws account id>:role/sample-rb-lambda-iam-role",
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:ListVersionsByFunction",
"lambda:GetFunction",
"lambda:UpdateFunctionConfiguration",
"lambda:DeleteFunction"
],
"Resource": "arn:aws:lambda:*:<aws account id>:function:*"
}
]
}
この状態でterraform apply
を実行すると成功し、Lambdaが作成されました。
CD環境の整備
AWS上にLambdaが出来上がったので、次はLambdaのデプロイを自動化するためにCD環境を整備します。Lambdaのデプロイに使うツールは色々とありますが、今回はlambroll
を選択しました。
lambrollを使った初期化
まずはlambrollを使うための初期化を行います。初期化については以下の記事を参考にしました。
関数名については、先ほど作ったLambdaの関数名を指定します。次のようにコマンドを実行すると、function.json
と.lambdaignore
が作成されます。
❯ lambroll init --function-name sample-rb-lambda
2025/03/15 18:15:54 [info] lambroll v1.2.1
2025/03/15 18:15:54 [info] function sample-rb-lambda found
2025/03/15 18:15:54 [info] creating .lambdaignore
2025/03/15 18:15:54 [info] creating function.json
作成されたfunction.json
にて、Timeoutをデフォルトの3秒から60秒に変更しておきます。これはマネジメントコンソール上でLambdaをお試しで動かしたところ3秒以上かかることがわかったので、長めに設定しておきます。
出来上がったfunction.json
は以下の通りです。
{
"Architectures": [
"x86_64"
],
"Code": {
"ImageUri": "<aws account id>.dkr.ecr.ap-northeast-1.amazonaws.com/sample-rb-lambda:latest"
},
"EphemeralStorage": {
"Size": 512
},
"FunctionName": "sample-rb-lambda",
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/sample-rb-lambda"
},
"MemorySize": 128,
"PackageType": "Image",
"Role": "arn:aws:iam::<aws account id>:role/sample-rb-lambda-iam-role",
"SnapStart": {
"ApplyOn": "None"
},
"Timeout": 60,
"TracingConfig": {
"Mode": "PassThrough"
}
}
これでlambrollを使った初期化は完了です。
lambrollを使うためのIAM Role整備
GitHub Actionsでlambrollを実行するために、lambroll用のIAM Roleを作る必要があります。ですが今回のケースでのlambroll実行に必要な権限がわかりませんでした。
そのため、次の2ステップで必要な権限を把握しIAM Roleを作成します。
- AWSマネジメントコンソール上でテスト用のIAMユーザーとIAM Roleを作り、ローカル上でlambrollを使いトライ&エラーしながら必要な権限を確認する
- 1のステップで把握した権限をもとにTerraformでlambroll用のIAM Roleを作成する
テスト用のIAMユーザーとIAM Roleの作成およびローカル上でlambrollを使ったトライ&エラー
まずはlambroll用のIAMユーザー(lambroll-for-lambda-deploy
)を作ります。加えてそのIAMユーザーにインラインポリシーを追加します。インラインポリシーの権限がわからなかったので、Go未経験ではあるものの、もしかしたらlambrollのデプロイ用コードに何かヒントがありそうかと思いコードを眺めてみました。
しばらく見ていると、AWSへの操作と思われるコードにおいて権限名と一致するようなコードがあることが分かりました。例としてはapp.lambda.UpdateFunctionConfiguration
のようなコードです。これをもとに、以下のようなデプロイに必要な権限のポリシーを作成し、IAMユーザー(lambroll-for-lambda-deploy
)に追加します。
{
"Statement": [
{
"Action": [
"lambda:UpdateFunctionConfiguration",
"lambda:UpdateFunctionCode",
"lambda:UpdateAlias",
"lambda:GetFunction",
"lambda:ListVersionsByFunction",
"lambda:CreateAlias"
],
"Effect": "Allow",
"Resource": "*",
"Sid": ""
}
],
"Version": "2012-10-17"
}
IAMユーザーへのポリシー追加完了後、ローカル上でlambroll deploy
を実行してみたところ以下のようなエラーが出ました。
❯ lambroll deploy --profile=lambroll-for-lambda-deploy
2025/03/21 00:04:20 [info] lambroll v1.2.1
2025/03/21 00:04:20 [info] loading Function from function.json
2025/03/21 00:04:20 [info] starting deploy function sample-rb-lambda
2025/03/21 00:04:21 [info] using docker image <aws account id>.dkr.ecr.ap-northeast-1.amazonaws.com/sample-rb-lambda:latest
2025/03/21 00:04:21 [info] updating function configuration
2025/03/21 00:04:21 [info] updating function configuration ...
2025/03/21 00:04:21 [info] State:Active LastUpdateStatus:Successful
2025/03/21 00:04:21 [error] FAILED. failed to update function configuration: failed to update function configuration: operation error Lambda: UpdateFunctionConfiguration, https response error StatusCode: 403, RequestID: 1ce05657-7e5e-4044-b80f-c5589fe1e669, api error AccessDeniedException: User: arn:aws:iam::<aws account id>:user/lambroll-for-lambda-deploy is not authorized to perform: iam:PassRole on resource: arn:aws:iam::<aws account id>:role/sample-rb-lambda-iam-role because no identity-based policy allows the iam:PassRole action
LambdaをTerraformで作成する時に出てきたPassRole
権限が、今回も不足しているとのことです。そのため先ほどのIAMユーザー(lambroll-for-lambda-deploy
)に以下のようなiam:PassRole
の権限を追加します。
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": "arn:aws:iam::<aws account id>:role/sample-rb-lambda-iam-role",
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
}
この権限追加でlambrollを使ってLambdaをデプロイできるようになりました。
Terraformでlambroll用のIAM Roleを作成する
次にTerraformを使いIAM Roleを作成します。信頼ポリシーにはGitHub OIDCの設定が必要なので、以下GitHubのドキュメントを参考します。
このドキュメントのロールと信頼ポリシーの構成
項目にある"token.actions.githubusercontent.com:sub"
の値をリポジトリ名とブランチ名に修正する必要があります。この信頼ポリシーと、前述したlambrollで必要な権限をもとに作成したIAM Roleのコードは以下のようになります。
# Deploy IAM Role for Lambda
resource "aws_iam_role" "sample_rb_lambda_iam_deploy_role" {
name = "sample-rb-lambda-iam-deploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
"Federated" = "arn:aws:iam::<aws account id>:oidc-provider/token.actions.githubusercontent.com"
},
Action = "sts:AssumeRoleWithWebIdentity",
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" : "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub" : "repo:M-Yamashita01/sample-rb-lambda:ref:refs/heads/main"
}
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "sample_rb_lambda_policy_attachment" {
role = aws_iam_role.sample_rb_lambda_iam_deploy_role.name
policy_arn = aws_iam_policy.sample_rb_lambda_deploy_policy.arn
}
resource "aws_iam_policy" "sample_rb_lambda_deploy_policy" {
name = "sample-rb-lambda-deploy-policy"
description = "Deploy Policy for sample-rb-lambda"
policy = data.aws_iam_policy_document.sample_rb_lambda_policy_document.json
}
data "aws_iam_policy_document" "sample_rb_lambda_policy_document" {
statement {
actions = [
"lambda:UpdateFunctionConfiguration",
"lambda:UpdateFunctionCode",
"lambda:UpdateAlias",
"lambda:GetFunction",
"lambda:ListVersionsByFunction",
"lambda:CreateAlias"
]
resources = ["*"]
}
statement {
actions = ["iam:PassRole"]
resources = [aws_iam_role.sample_rb_lambda_iam_role.arn]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["lambda.amazonaws.com"]
}
}
}
このコードを使ってterraform apply
しましたがエラーとなってしまいました。
│ Error: creating IAM Policy (sample-rb-lambda-deploy-policy): operation error IAM: CreatePolicy, https response error StatusCode: 403, RequestID: e50bac94-e02c-48f9-9f4b-1b741c5d9a24, api error AccessDenied: User: arn:aws:iam::<aws account id>:user/lambda-terraformer is not authorized to perform: iam:CreatePolicy on resource: policy sample-rb-lambda-deploy-policy because no identity-based policy allows the iam:CreatePolicy action
Terraform用のIAMユーザーlambda-terraformer
にiam:CreatePolicy
の権限が必要とのことです。iam:CreatePolicy
が必要ということはGetやDeleteなども必要になると思われます。そのため必要な権限を調べたあと、IAMユーザーlambda-terraformer
にすでにアタッチされていたインラインポリシーiam-role-manage-policy
に追加することにしました。修正後のiam-role-manege-policy
は以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:GetRole",
"iam:UpdateAssumeRolePolicy",
"iam:ListInstanceProfilesForRole",
"iam:DetachRolePolicy",
"iam:DeleteRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:CreateRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"iam:ListRolePolicies",
"iam:GetRolePolicy"
],
"Resource": "arn:aws:iam::<aws account id>:role/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"iam:CreatePolicy",
"iam:GetPolicyVersion",
"iam:GetPolicy",
"iam:ListPolicyVersions",
"iam:DeletePolicy"
],
"Resource": "arn:aws:iam::<aws account id>:policy/*"
}
]
}
改めてterraform apply
を実行すると成功しました。これでlambroll用のIAM Roleが作成済みとなります。
lambrollを使ったGitHub Actionsワークフロー作成
権限が整ったので、GitHub Actionsでのデプロイワークフローを作成します。この作成において注意点が2つあります。
1つはARNをGitHub Secretに登録してそれを参照するようにすることです。先ほど作成したlambroll用のデプロイIAM Role(sample-rb-lambda-iam-deploy-role
)をsecretに設定しておきます。
2つ目はワークフローの実行タイミングです。今の時点で、コンテナをビルドしECRにプッシュするワークフローが存在します。そのためこのワークフローが完了したあとにlambrollでデプロイする必要があります。もしECRにプッシュするワークフローが完了する前にデプロイのワークフローを実行した場合、最新から1つ前のイメージでLambdaがデプロイされてしまいます。そのためGitHub Actionsの機能であるworkflow_run
を活用し、指定のワークフローが終了次第このデプロイワークフローを実行できるようにしました。
作成したワークフローは以下の通りです。
name: Deploy to AWS Lambda
on:
workflow_run:
workflows: ["Build and Push"]
types: [completed]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.DEPLOY_LAMBDA_AWS_ROLE_ARN }}
- name: Deploy to Lambda using Lambroll
uses: fujiwara/lambroll@febaa8df8bc65284e4b196b8359cb065e82d63cb # v1
- run: |
lambroll deploy
このワークフローを使ってGitHub Actionsを実行すると、ECRにプッシュしたイメージを使ってLambdaがデプロイされました。
Lambda実行
Lambdaのデプロイが完了したので実行してみます。AWSマネジメントコンソール上でLambdaのテストタブを開き、適当なイベント名を指定し、イベント共有の設定はプライベートとし、Lambdaに渡すパラメータのJSONはデフォルトのままにして、テストボタンで実行します。
証明書読み取りエラー
Lambdaを実行すると、OpenSSL::PKey::RSA
のインスタンス作成時に以下のようなエラーが発生しているようでした。
{
"errorMessage": "Neither PUB key nor PRIV key",
"errorType": "Function<OpenSSL::PKey::RSAError>",
・・・
}
エラーメッセージの通り、読み込んだ証明書が公開鍵、秘密鍵のどちらでもないというエラーです。このときのSecretManager上の秘密鍵は以下のようになっていました。
-----BEGIN RSA PRIVATE KEY-----\nMIIxxxxxxx=\n-----END RSA PRIVATE KEY-----
LocalStackを使って同じ秘密鍵を設定し、ローカルでテストしたところテストは全てパスしてしまいました。どうやらAWS上だけで起きる問題のようです。
調査を進めたところ、秘密鍵を読み込んだ際に改行コードが\\n
になってしまう記事を見つけました。
おそらく同様の問題が今回でも起きていそうと想定し、Lambda内で秘密鍵に対してgsub("\\n", "\n"))
した後、OpenSSL::PKey::RSA.new
にその秘密鍵を渡すようにしました。実際のコードは以下の通りです。
JWT.encode(payload, OpenSSL::PKey::RSA.new(@secrets_manager_client.private_key.gsub("\\n", "\n")), "RS256")
この修正で先ほどのエラーを解決できました。
contents読み出し後のsplitでエラー
証明書は正常に読み込めるようになったものの、octokitのgemを経由して取得したファイルのcontentsをsplitしようとした際、文字コードに関する以下のようなエラーが発生しました。
{
"errorMessage": "invalid byte sequence in US-ASCII",
"errorType": "Function<ArgumentError>",
・・・
}
このエラーメッセージを使って調べていたところ、次の記事と同じ現象が発生していると思われました。
public.ecr.aws/lambda/ruby:3.3 を使用した場合の実行結果は以下のようになりました。ひらがなを含む2行目で Invalid byte sequence が発生していることがわかります。
実際にAWSドキュメントを見てみると、私の使用しているAWSのベースイメージはAmazon Linux 2023を使っているようでした。
このイメージ上でlocaleと環境変数を出力して確認してみました。
bash-5.2# ruby -v
ruby 3.3.7 (2025-01-15 revision be31f993d7) [aarch64-linux]
bash-5.2#
bash-5.2# locale --all-locales
C
C.utf8
POSIX
bash-5.2#
bash-5.2# env
HOSTNAME=4da30dd148f0
PWD=/var/task
TZ=:/etc/localtime
LAMBDA_TASK_ROOT=/var/task
HOME=/root
LANG=en_US.UTF-8
LAMBDA_RUNTIME_DIR=/var/runtime
TERM=xterm
SHLVL=1
LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib
PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin
_=/usr/bin/env
この結果から、ロケールがCロケールしかないにも関わらず、LANGがen_US.UTF-8
になっていることが分かります。そのため上記テックブログの記事と同じ結論となりますが、enロケールが使えず今回の文字コードの問題が出ていると思われます。
この問題の対処として記事内で言及されていたように、環境変数のLANG
をC.UTF-8
に変更することで解決できるようです。具体的には、Dockerfileの中で以下のように環境変数を設定します。
FROM public.ecr.aws/lambda/ruby:3.3
ENV LANG=C.UTF-8
・・・
この指定により文字コードのエラーがなくなり、Lambdaが問題なく動作するようになりました。
動かした結果
このシステムを使用して、リポジトリ内のマニフェストを取得し、namespaceの一覧をGitHubのissueに出力した結果は次のとおりとなります。
元のマニフェストファイル
# xxx bbb triggered by M-Yamashita01.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app
namespace: argocd1
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: installs #子Applicationのmanifestがあるディレクトリを指定する
repoURL: https://github.com/xxx/argocd-sample-app.git
targetRevision: main
syncPolicy:
automated: {}
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: apps2
namespace: argocd2
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: installs #子Applicationのmanifestがあるディレクトリを指定する
repoURL: https://github.com/xxx/argocd-sample-app.git
targetRevision: main
syncPolicy:
automated: {}
---
# xxx bbb triggered
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: apps3
namespace: argocd3
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: installs #子Applicationのmanifestがあるディレクトリを指定する
repoURL: https://github.com/xxx/argocd-sample-app.git
targetRevision: main
syncPolicy:
automated: {}
---
# xxx bbb triggered by M-Yamashita01.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: apps4
namespace: argocd4
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: installs #子Applicationのmanifestがあるディレクトリを指定する
repoURL: https://github.com/xxx/argocd-sample-app.git
targetRevision: main
syncPolicy:
automated: {}
GitHub Issueに出力した結果は以下の通りです。namespace一覧が取得できていることが分かります。
おわりに
今回の記事では、Lambdaを使ってリポジトリのファイルにアクセスしてGitHub Issueに投稿するシステムの作成手順と結果について説明しました。
エラーとその解決のための脱線話や、CIやCDなどいろいろ盛り込んだシステムにしたのでかなりボリュームのある記事となりました。綺麗にスマートにシステムを作れたらそれも良い形とは思いますが、泥臭くエラーに向き合って脱線を繰り返す姿勢も良い形だと思っています。そうすることで自身の守備範囲を広めたり、あの時調べた内容が別の何かとつながったりすることもあります。
この記事が誰かのお役に立てれば幸いです。
Discussion