はじめに
CodeCommitで管理しているリポジトリにサーバーレスの定期実行でPushする、ということをやってみたのでそのやり方を残したいと思います。
本記事では、試しにYAHOO!ニュース(ITカテゴリ)のRSSを取得して、JSON形式で保存する。というユースケースでやり方の紹介をします。
目次
前提
1. 以下のツールがインストールされていること。
node18
yarn
aws-cli
2. Push先のリポジトリの作成と、initial commitを済ませておく。
準備
今回Lambda使用するのはNode.js 18.xです。
yarnでプロジェクトを管理します。
yarn init
パッケージのインストールをします。
yarn add @aws-sdk/client-codecommit axios typescript xml2js
yarn add -D @types/aws-lambda @types/node @types/xml2js esbuild
今回はTypescriptで型定義をしつつコードを書いていきます。
RSSはxmlで取得されるので、xmlをjsonに変換するためにxml2jsを使用します。
esbuildはTypeScriptのコードをJavaScriptにトランスパイルするために使用します。
権限の設定
ロール
iamフォルダを作成し、その中にrole.jsonを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
ロールを作成します。
aws iam create-role --role-name lambda-codecommit --assume-role-policy-document file://iam/role.json
ポリシー
iamフォルダ下にpolicy.jsonを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"codecommit:ListRepositories",
"codecommit:ListBranches",
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:PutFile"
],
"Resource": "<リポジトリのARN>"
}
]
}
ポリシーを作成します。
aws iam create-policy --policy-name put-file-to-codecommit --policy-document file://iam/policy.json
ロールにアタッチします。
aws iam attach-role-policy --role-name lambda-codecommit --policy-arn <先ほど作成したポリシーのARN>
Amazon CloudWatch Logsへの書き込み等を許可するポリシーをアタッチします。
aws iam attach-role-policy --role-name lambda-codecommit --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Lambda関数作成
各種設定
Typescriptを使用しているので、tsconfig.jsonを作成します。
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"strict": true,
"incremental": true,
"outDir": "./.ts-cache/"
}
}
簡潔に上記の設定だけ記述しました。
次に、esbuildの実行ファイルを作成します。
import esbuild from 'esbuild';
esbuild.build({
entryPoints: [
'./index.ts',
],
outdir: 'dist',
bundle: true,
minify: true,
platform: 'node',
sourcemap: false,
target: ['es2020'],
}).catch(() => process.exit(1));
CLIからパラメータを指定すれば実行できますが、今回はファイルで管理してみました。
次にpackage.jsonのscriptsセクションを定義します。
"scripts": {
"prebuild": "rm -rf dist",
"build": "node esbuild.config.mjs",
"postbuild": "cd dist && zip -r index.zip index.js*"
}
esbuildコマンドの定義をしています。
Lambda関数
今回は役割ごとに複数ファイルに分けてみました。
index.tsがエントリーポイントになります。
import { Handler, APIGatewayProxyResult } from 'aws-lambda';
import { putFile } from "./lib/codeCommitUtil";
import { CodeCommitClient } from '@aws-sdk/client-codecommit';
export const handler: Handler = async (): Promise<APIGatewayProxyResult> => {
try {
const REGION = process.env['REGION'];
if (!REGION) throw new Error("REGION is null.");
const client = new CodeCommitClient({ region: REGION });
const result: string = await putFile(client);
return {
statusCode: 200,
body: result
};
} catch (e) {
console.log(e)
if (e instanceof Error) {
return {
statusCode: 500,
body: e.message
};
} else {
return {
statusCode: 500,
body: 'An unknown error occurred'
};
}
}
};
次にCodeCommitに関するコードを記述したファイルです。(libフォルダ下に作っています。)
import { getData } from "./fetchDataUtil";
import {
CodeCommitClient,
GetBranchCommand,
GetBranchCommandOutput,
PutFileCommand,
PutFileCommandOutput,
SameFileContentException,
GetBranchCommandInput,
PutFileCommandInput
} from "@aws-sdk/client-codecommit";
export const putFile = async (client: CodeCommitClient): Promise<string> => {
try {
const REPOSITORY = process.env['REPOSITORY'];
if (!REPOSITORY) throw new Error("REPOSITORY is null.");
const BRANCH = process.env['BRANCH'];
if (!BRANCH) throw new Error("BRANCH is null.");
const data: Uint8Array = await getData();
const latestCommitId: string = await getLatestCommitId(REPOSITORY, BRANCH, client);
const params: PutFileCommandInput = {
repositoryName: REPOSITORY,
branchName: BRANCH,
fileContent: data,
filePath: 'test.json',
commitMessage: 'Update file from Lambda function',
parentCommitId: latestCommitId
};
const command: PutFileCommand = new PutFileCommand(params);
const response: PutFileCommandOutput = await client.send(command);
return 'Successfully put file. commitId = "' + response.commitId + '"';
} catch (e) {
if (e instanceof SameFileContentException) {
return 'No changes in the data file';
}
throw e;
}
};
const getLatestCommitId = async (repositoryName: string, branchName: string, client: CodeCommitClient): Promise<string> => {
const params: GetBranchCommandInput = {
repositoryName: repositoryName,
branchName: branchName,
};
const command: GetBranchCommand = new GetBranchCommand(params);
const response: GetBranchCommandOutput = await client.send(command);
if (response.branch && typeof response.branch.commitId === 'string') {
return response.branch.commitId;
} else {
throw new Error(`Failed to retrieve commit id.`);
}
};
基本的にドキュメントを参考にパラメータ等を作っていきます。
今回は最小限必須パラメータのみで構成していますが、他にもパラメータは存在するので参照してみてください。
注意点:
- PutFileCommandInputのparentCommitIdは、必須パラメータではありませんが、リポジトリが空ではない場合は必須になります。
- fileContentの型は
undefined | Uint8Array
なので、パラメータはUint8Arrayに変換しておく必要があります。(下記ファイル参照)
次にデータを取得するファイルです。
import axios, { AxiosResponse, AxiosError } from "axios";
import { parseStringPromise } from "xml2js";
export const getData = async (): Promise<Uint8Array> => {
const response = await fetchRSSData();
const xmlData = response.data;
const jsonData = await xmlToJson(xmlData);
const extractedJsonData = JSON.stringify(jsonData, null, 2);
return new TextEncoder().encode(extractedJsonData);
};
async function fetchRSSData(): Promise<AxiosResponse<string>> {
const DATA_URL = process.env['DATA_URL'];
if (!DATA_URL) throw new Error("DATA_URL is null.");
try {
const response = await axios.get<string>(DATA_URL);
return response;
} catch (error) {
console.log(error);
throw Error(handleApiError(error as AxiosError));
}
}
function handleApiError(error: AxiosError): string {
if (error.response) {
return `API Error: ${error.response.status} - ${error.response.statusText}`;
} else if (error.request) {
return 'Request failed. No response received.';
} else {
return 'An error occurred while processing the request.';
}
}
async function xmlToJson(xmlData: string) {
try {
const jsonData = await parseStringPromise(xmlData, { trim: true });
return jsonData;
} catch (error) {
console.log(error);
throw Error('An error occurred while converting xml to json.');
}
}
axiosでRSSを取得し、xml2jsでxmlをjsonに変換し、Uint8Arrayに変換したものを返しています。
Lambda関数の登録
コードが完成したので、Lambda関数をデプロイしていきます。
まず、yarn build
でTypescriptで書いたコードをJavascriptにトランスパイルし、zipに圧縮します。
distフォルダ下にトランスパイルされたjsファイルとzipファイルが生成されます。
下記CLIコマンドでLambda関数の登録を行います。
aws lambda create-function --function-name putFileToCodecommit --role <作成したロールのARN> --region ap-northeast-1 --zip-file fileb://dist/index.zip --runtime nodejs18.x --handler index.handler --timeout 10
次に環境変数を登録します。
極力CLIで済ませたいので、環境変数もファイルで定義しアップロードするようにします。
{
"Variables":{
"REGION": "ap-northeast-1",
"REPOSITORY": "fetch-by-lambda",
"BRANCH": "main",
"DATA_URL": "https://news.yahoo.co.jp/rss/categories/it.xml"
}
}
下記コマンドで環境変数を登録します。
aws lambda create-function-configuration --function-name putFileToCodecommit --environment file://config/env.json
下記コマンドでLambda関数のテストができます。
aws lambda invoke --function-name putFileToCodecommit out
EventBridge スケジュールを設定すれば、簡単にLambda関数を定期実行することができます。
おわりに
以上でCodeCommitで管理しているリポジトリにサーバーレスの定期実行でPushすることができました。
静的コンテンツをAmplify等でデプロイしていて、定期的にリポジトリを更新したい際に、Lambdaを使用すれば全てサーバーレスで完結できます。
Lambdaを使用した各種AWSサービスの操作はとても便利なので、ぜひ使いこなせるようになりたいです。