💨

github actions経由でAWS Lambda,APIGatewayにdeployしてAPIを作成してみた

2023/07/16に公開

やりたかったこと

github actionsにpushをすることでAWSにdelopyされ、APIを作成したかった

やったこと

前提条件

ファイル構造(ddlは無視してもらって大丈夫です)

Lambda-stackの作成

vpcは使用していません

import { Duration, Environment, Stack, StackProps } from "aws-cdk-lib";
import { DeploySetting } from "../deploy-list";
import { Construct } from "constructs";
import { SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { RetentionDays } from "aws-cdk-lib/aws-logs";
import * as IAM from "aws-cdk-lib/aws-iam";

export interface LambdaProps extends StackProps {
  env?: Environment;
  name: string;
  stackNameSuffix?: string;
  secretJson?: any;
  vpc?: string;
  securityGroup: string;
  params?: DeploySetting;
}

export class LambdaStack extends Stack {
  public readonly lambdas: any = {};

  constructor(scope: Construct, id: string, props: LambdaProps) {
    super(scope, id, props);
  }
  deploy(props: LambdaProps) {
    const parsedJson = props.secretJson;
    const lambda = new NodejsFunction(
      this,
      `bff-${props.name}-${props.stackNameSuffix}`,
      {
        functionName: `bff-${props.name}-${props.stackNameSuffix}`,
        entry: `./src/action/${props.name}.ts`,
        handler: "handler",
        memorySize: 256,
        timeout: Duration.seconds(180),
        environment: {
          TZ: "Asia/Tokyo",
          database: parsedJson.host,
          user: parsedJson.username,
          password: parsedJson.password,
          host: parsedJson.host,
        },
        securityGroups: [
          SecurityGroup.fromSecurityGroupId(
            this,
            `${props.name}_SG`,
            props.securityGroup
          ),
        ],
        logRetention: RetentionDays.ONE_WEEK,
      }
    );
    if (props.name && props.params?.lambdaRole) {
      const lambdaPolicy = new IAM.PolicyStatement({
        ...props.params.lambdaRole,
      });

      lambda.role?.attachInlinePolicy(
        new IAM.Policy(this, `LambdaRole-bff-${props.name}`, {
          statements: [lambdaPolicy],
        })
      );
    }
    this.lambdas[props.name] = lambda;
  }
}

APIGateway-stackの作成

import { CfnOutput, Stack, StackProps } from "aws-cdk-lib";
import {
  CognitoUserPoolsAuthorizer,
  Cors,
  LambdaIntegration,
  RestApi,
} from "aws-cdk-lib/aws-apigateway";
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { UserPool } from "aws-cdk-lib/aws-cognito";
import { Construct } from "constructs";
import { DeploySetting } from "../deploy-list";

export interface ApiProps extends StackProps {
  stage: string;
  stackNameSuffix?: string;
  lambdas: any;
  isUseCognito?: boolean;
  setting: DeploySetting;
}

interface Obj {
  [props: string]: any;
}

export class ApiStack extends Stack {
  private restApi: any = {};
  private cognitoAuthorizer: any;
  constructor(scope: Construct, id: string, props: ApiProps) {
    super(scope, id, props);
    this.restApi = new RestApi(this, "production-pool-bff", {
      restApiName: `bff-${props.stackNameSuffix}`,
      description: "priduction_pool_bff by CDK",
      deployOptions: {
        stageName: props.stage,
      },
      defaultCorsPreflightOptions: {
        allowOrigins: Cors.ALL_ORIGINS,
        allowMethods: Cors.ALL_METHODS,
        allowHeaders: Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },
    });
    new CfnOutput(this, "OutputApiUrl", { value: this.restApi.url! });
    //Cognitoへのアクセスに関する処理
    if (props.isUseCognito) {
      const userPool = UserPool.fromUserPoolArn(
        this,
        "cognitoのID",
        "自分の環境でのarn"
      );
      this.cognitoAuthorizer = new CognitoUserPoolsAuthorizer(
        this,
        "cognitoAuthorizer",
        {
          authorizerName: "CognitoAuthorizer",
          cognitoUserPools: [userPool],
        }
      );
    }
  }
  deploy(props: ApiProps) {
    const accountId = Stack.of(this).account;
    const region = Stack.of(this).region;

    if (!props.setting.urls) return;

    //リソースの作成
    let resorce = this.restApi.root.addResource(props.setting.urls[0]);
    props.setting.urls.shift();
    props.setting.urls.forEach((resorceItem) => {
      resorce = resorce.addResource(resorceItem);
    });

    const stackDevideFunction = props.lambdas[props.setting.name];

    const cognitoSwitch: Obj = {
      requestParameter: props.setting.required,
      validateRequestParameters: props.setting.required ? true : false,
    };
    if (props.isUseCognito) cognitoSwitch.authorizer = this.cognitoAuthorizer;
    resorce.addMethod(
      props.setting.method,
      new LambdaIntegration(stackDevideFunction),
      cognitoSwitch
    );

    stackDevideFunction.addPermission("myFunctionPermission", {
      principal: new ServicePrincipal("apigateway.amazonaws.com"),
      action: "lambda:InvokeFunction",
      sourceArn: `arn:aws:execute-api:${region}:${accountId}:${this.restApi.restApiName}/*/*/*`,
    });
  }
}

deplory-listの作成

ここはApiGatewayの入り口です

export const deploylist: DeploySetting[] = [
  {
    name: "bff_test",
    urls: ["bff_test"],
    auth: false,
    method: "GET",
    required: {},
  },
  {
    name: "userSignUp",
    urls: ["userSignUp"],
    auth: false,
    method: "POST",
    lambdaRole: {
      actions: [
        "cognito-idp:AdminCreateUser",
        "cognito-idp:AdminSetUserPassword",
        "ses:SendEmail"
      ],
      resources: ["*"],
    },
    required: {},
  },
];

export interface Keys {
  [key: string]: boolean;
}

export interface LambdaRoleType {
  [key: string]: Array<string>;
}

export interface DeploySetting {
  name: string;
  urls: Array<string>;
  auth: boolean;
  method: string;
  required?: Keys;
  lambdaRole?: LambdaRoleType;
}

deployments-stackの作成

import * as cdk from "aws-cdk-lib";
import { execSync } from "child_process";
import { deploylist } from "./deploy-list";
import { ApiStack } from "./serviceStack/apigateway-stack";
import { LambdaStack } from "./serviceStack/lambda-stack";
const { STAGE = "local" } = process.env;
const { STACK_NAME_SUFFIX = "local" } = process.env;
const { SECURITY_GROUP_ID = "" } = process.env;

const createApp = async (lambdaJson: any): Promise<cdk.App> => {
  const app = new cdk.App();
  cdk.Tags.of(app).add("Name", "PRODUCTION-POOL-BACKEND-CDK");
  cdk.Tags.of(app).add("Price", "production-pool-backend");
  cdk.Tags.of(app).add("CmVillingGroup", "production-pool-backend");
  cdk.Tags.of(app).add("Maiking", "miyashita-hiroki");
  cdk.Tags.of(app).add("Sys-group", "production-pool-backend");

  const lambda = new LambdaStack(app, `lambda-bff-${STACK_NAME_SUFFIX}`, {
    env:
      STAGE === "local"
        ? undefined
        : {
            account: process.env.CDK_DEFAULT_ACCOUNT,
            region: process.env.CDK_DEFAULT_REGION,
          },
    name: deploylist[0].name,
    securityGroup: SECURITY_GROUP_ID,
  });

  const apiGateway = new ApiStack(app, `api-bff-${STACK_NAME_SUFFIX}`, {
    stage: STAGE,
    stackNameSuffix: STACK_NAME_SUFFIX,
    lambdas: lambda.lambdas,
    setting: {
      name: "",
      urls: [],
      auth: false,
      method: "",
      required: {},
    },
  });
  apiGateway.addDependency(lambda);

  deploylist.forEach(async (deployListItem) => {
    await lambda.deploy({
      stackNameSuffix: STACK_NAME_SUFFIX,
      name: deployListItem.name,
      securityGroup: SECURITY_GROUP_ID,
      secretJson: lambdaJson,
      params: deployListItem,
    });

    await apiGateway.deploy({
      stage: STAGE,
      lambdas: lambda.lambdas,
      isUseCognito: STAGE != "local" && deployListItem.auth,
      setting: deployListItem,
    });
  });

  return app;
};

let lambdaJson = {};
const get_command = `aws secretsmanager get-secret-value --secret-id pool/postgres`;
const result = execSync(get_command);
const secretsManagerJson = JSON.parse(result.toString());
lambdaJson = JSON.parse(secretsManagerJson.SecretString);

createApp(lambdaJson);

ymlの作成

# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
    - run: npm install -g aws-cdk
    - run: npm install
    - run: npm run build --if-present
    - run: cdk bootstrap
    - run: cdk deploy --all
    # - run: npm test

まとめ

上記のことをやることでgitにpushした際にAWS上にdeployされる仕組みができあがります。
ただし、branchによってapiの名前などを変更したりはしていないのでそれぞれ自分の環境、仕様で
カスタムをしていってもらえればいいと思います

Discussion