FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

AWS CDKで新規プロダクトを構築して感じた嬉しいポイント(について社内発表をしたよ)

はじめに

あけましておめでとうございます。ピクトリンク事業部開発部サーバサイド開発課のkitajimaです。2023年もよろしくお願いいたします。
今年の抱負は「鼻うがいを続ける」です。

昨年末に「技術交流会」※という社内の発表会で、
「AWS CDKを利用したピクトリンク連携用プロダクトのインフラ構築及びデプロイの自動化」
(外部発信に合わせ一部表記を変更)と題して私が発表しました。 せっかくなので外部にも同内容を発信したく、社外秘など一部改変して紹介させていただきます。

※技術交流会とは
弊社で年に一度行われる社内の発表会です。異なる事業部の合計4~5名ほどがそれぞれの業務に取り入れた技術や獲得した知見、その他発信したいことを発表する場です。
普段あまり見ることのできない他事業部の取り組みや技術を知れる機会として、とても楽しい行事になっています。

概要

ピクトリンク連携という新機能に向けた新規プロダクト(以下「連携サーバ」)について、AWS CDKを用いてインフラ構築およびデプロイの自動化を行いました。

ピクトリンク連携とは

プリントシール機(以下、プリ機)でプレイする際にピクトリンクアプリ(iOS版, Android版)と連携することで、プレイ後にプリが通知として届くようになるという機能です。 プリ機に"ログインする"というイメージを持ってもらうとわかりやすいかもしれません。

詳しくは、弊社ニュースをご覧ください。

連携サーバの概要

連携サーバは以下のような構成になっています(一部改変&簡略化)。

プリ機に向けて、連携用のAPIを提供しています。 ご覧のようにAPI Gateway + Lambdaのサーバレス構成を採用しています。

AWS CDK(Cloud Development Kit)

概要

CDKは、環境構築がコードで管理できるIaC(Infrastructure as Code)の一種です。
プログラミング言語を使用してコードを管理します。2019年登場で、弊チームでは2021年ごろから利用しています。

開始方法

詳しくは公式ドキュメントを参照いただきたいですが、大まかに流れを説明します。

プロジェクト作成

cdk initを叩いてプロジェクトを作成します。

$ cdk init --language typescript

以下のようなディレクトリ構成が作成されます。

sample
├── README.md
├── bin
│   └── sample.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── sample-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── sample.test.ts
└── tsconfig.json

ライブラリを用いてインフラ定義

CDKでインフラ構築する際はCDK Construct Libraryを使用します。 作成されたlib/sample-stack.tsに書いていきます。

// sample-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export class SampleStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    const function1 = new NodejsFunction(this, 'function', {
        entry: './lambda/hello-world.ts',
        runtime: Runtime.NODEJS_16_X,
    });
  }
}

ここではLambda関数を1つ作成しました。

デプロイ

cdk deployでデプロイできます。

$ cdk deploy

また、事前にcdk diffを叩くことでコードとデプロイされているインフラとの差分を確認できます。

$ cdk diff

破棄

cdk destroyを叩くことでデプロイされているインフラをスタック単位で破棄できます。

$ cdk destroy

CDK嬉しいポイント

プログラミング言語を使える

CDKではJavaScript、TypeScript、Python、Java、C#(プレビュー版としてGo)がサポートされています。
従来使用していたAWS CloudFormation(yaml/jsonを使用するIaC)に比べ、弊チームのようなサーバサイドの人間と親和性が高く、学習コストも低いです。

しかもLambda実装と同じ言語を使える

また、CDKのサポート言語はLambdaのサポート言語(Java、Go、PowerShell、Node.js、C#、Python、Ruby)と共通しているものが多いです。
AWS Cloud Development Kit のよくある質問
AWS Lambda の特徴
そのため、インフラ構築とLambdaの実装で同じ言語を使用すればスイッチングコストが減るのです!
なお、今回はCDK、LambdaともにTypeScriptで書くことにしました。ドキュメントやサンプル、参考記事が充実しているためです。

実装に時間をかけられる

例として連携サーバに改修が入り、提供するエンドポイントを1つ増やしたいケースを想定します。

1. Lambdaの実装を書く

CDKの範囲からは外れる工程ですが、まずLambdaの実装を書きます。 (例として、ランダムなULIDを返す関数)

// hoge.ts
import { ulid } from "ulid";

exports.handler = async () => {
    return {
        statusCode: 200,
        headers: {
            'Content-Type': 'text/html',
        },
        body: `random ID: ${ulid()}`,
    };
};
2. aws_lambda.Functionをもう一個newする

TypeScriptで実装しているため、今回はaws_lambda.Functionを拡張したaws_lambda.NodejsFunctionを使用しています。

const hogeFunction = new NodejsFunction(this, 'function', {
    entry: './lambda/hoge.ts',
    runtime: Runtime.NODEJS_16_X,
});
3. 既存のAPIとFunctionを統合

既存のaws_apigateway.RestApiにリソースを追加し、そのリソースにLambda関数を統合します。 以下のようにメソッドを呼んであげるだけです。

existingApi.root
    .addResource("/hoge")
    .addMethod("GET", new LambdaIntegration(hogeFunction));

このように、インフラ部分(2, 3)はさっと作れて肝心の実装(1)に時間をかけられることが大きかったです。

インフラと実装の関係が分かりやすい

本プロダクトでは実装とインフラを同一のリポジトリでgit管理しています。以下のように、プロジェクトルート以下のcdkディレクトリでcdk initしたのち、Lambdaの実装を置くためのlambdaディレクトリを掘りました。

federation-servers
├── cdk
│   ├── bin
│   │   └── federation_servers.ts
│   ├── lib
│   │   └── federation_servers-stack.ts
│   ├── lambda
│   │   └── hoge.ts
│   ├── package.json
│   ├── package-lock.json
│   (略)
(略)

こうすると、API Gateway⇔Lambda⇔実装の関係性をコードを追うことで理解できます。
また、gitのコミットログから変更の理由も読み取りやすくなります。

以上のように、自分達が作ったインフラを実装との関係性も含め把握できる点も非常に嬉しかったです。

実装も含めてデプロイできる

インフラと実装の関係が分かりやすいに関連して、Lambdaの実装とインフラを同一のCDKプロジェクトに配置していることで受けられる恩恵があります。
cdk deployさえ叩けば、インフラだけでなく実装の差分も含めてデプロイできるのです。

なお、AWS公式ブログ(AWS CDK による AWS Lambda コードの管理)にもありますように、CDKでLambdaコードの管理を行う何らかの手法を選択する必要があります。
弊チームでどの方法を採ったかの経緯はこちらの記事で詳しく紹介しておりますので、是非併せてご覧ください。

この恩恵を生かし、デプロイをGitHub Actionsで自動化しました。mainにPRがマージされたタイミングでcdk deployを叩いてくれるActionsを定義しました。

# 一部改変
name: prod-deploy-from-main

on:
  push:
    branches:
      - main

env:
  AWS_ROLE_ARN: xxxxxx
  STACK_NAMES: hoge-stack fuga-stack

jobs:
  aws-cdk-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
          cache: npm
          cache-dependency-path: cdk/package-lock.json
      - name: Setup AWS CDK
        run: npm -g install aws-cdk
      - name: Setup dependencies
        run: npm ci
        working-directory: ./cdk
      - name: Build
        run: npm run build
        working-directory: ./cdk
      - name: CDK Diff Check
        run: cdk diff ${{ env.STACK_NAMES }}
        working-directory: ./cdk
      - name: CDK deploy
        run: cdk deploy ${{ env.STACK_NAMES }}
        working-directory: ./cdk

インフラのテストを自動化できる

aws-cdk-lib.assertionsというテスト用モジュールを用いるとインフラのテストコードが書けます。

// 設定値の検証
template.hasResourceProperties('AWS::Lambda::Function', {
  Runtime: "nodejs16.x",
});

CDKは便利な反面インフラ構築が抽象化されており、実際に作成されるリソースたちを把握することが大変だと私は感じました。
今後はこのようにインフラのテストも自動化していきたいです。

(番外編) 抽象度の高いConstructを使うと、数行で環境ができる

CDK Construct Libraryには異なる種類の"抽象度"を持った Construct (基本的な構成要素)が含まれます。 - L1 Construct(CFN Resources)
CFnと1対1に対応したConstructです。CFnと同様に全てのプロパティを明記するため、後述のL2 Constructより記述が長くなります。L2 Constructがまだ提供されていない新しめのAWSサービスを構築する際には使えそうです。
例:CfnFunction

  • L2 Construct
    単一のリソースをいい感じに構築するために抽象化されたConstructです。
    プロパティを設定しない場合はいい感じのデフォルト値を使用してくれたり、リソースの操作を行う直感的なメソッドが提供されていたりします。基本的にこちらを使用することが多いです。
    例:Function

  • L3 Construct(Patterns)
    複数のリソースからなる、一般的なユースケースに最適な環境をネットワーク周りも含めて構築してくれるConstructです。 執筆時点でそこまで種類はなく、大きく分けて以下の2つが提供されています。

L3 Constructを使用すると、たった数行で特定のユースケースに最適な環境をネットワーク周りも含めて構築してくれるのです。

const albFargateService = new ApplicationLoadBalancedFargateService(this, 'hoge', {
    taskImageOptions: {
        image : ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
    },
});

技術交流会では、このコードを実際にデプロイして動作させるところまで実演してみました。

だいたいこんな感じの環境ができあがります。

この全能感がたまりません。
連携サーバではL3 Constructは採用しませんでしたが、気軽にインフラ構築できるという点でAWS新規利用時や個人開発で役立ちそうだと感じたため番外編として挙げております。

最後に

本施策以外にも、最近の新規プロダクトではAWSの知見は必要不可欠です。今後もCDKでサクッとインフラ構築して、実装に集中することでよりよいサービスを届け続けられたらと思っております!