🪄

TypeScript初心者のAWS CDKをProjenで使うメモ

2024/03/30に公開

はじめに

AWS CDKからTypeScriptを始めたが、npmとかごちゃごちゃしていたり、どうすれば正解なのかがわからなくなってきてProjenを試してみようとしているメモ。
ホントに初心者なので間違っている箇所はあると思う。

作業メモなので使っていくうちに更新していく。
この記事を書き始めた時点での感想としては、.projenrc.tsだけを変更するという制約があるので迷わなくてすむ。いい感触。

Projen

参考URL

https://aws.amazon.com/jp/blogs/news/getting-started-with-projen-and-aws-cdk/
https://qiita.com/hayao_k/items/194dfb051f18a38b6dbd#unit-test-build-release

インストール

と思ったら普通に実行するだけらしい。npx初心者すぎる。

mkdir test_project && cd test_project
npx projen new awscdk-app-ts
$ ll
total 292
drwxr-xr-x   8 raiha raiha   4096 Mar 30 00:59 ./
drwxr-xr-x   3 raiha raiha   4096 Mar 30 00:57 ../
-r--r--r--   1 raiha raiha   4303 Mar 30 00:58 .eslintrc.json
drwxr-xr-x   8 raiha raiha   4096 Mar 30 00:59 .git/
-r--r--r--   1 raiha raiha    818 Mar 30 00:58 .gitattributes
drwxr-xr-x   3 raiha raiha   4096 Mar 30 00:58 .github/
-r--r--r--   1 raiha raiha    817 Mar 30 00:58 .gitignore
-r--r--r--   1 raiha raiha    629 Mar 30 00:58 .mergify.yml
-r--r--r--   1 raiha raiha    391 Mar 30 00:58 .npmignore
drwxr-xr-x   2 raiha raiha   4096 Mar 30 00:58 .projen/
-rw-r--r--   1 raiha raiha    533 Mar 30 00:59 .projenrc.ts
-r--r--r--   1 raiha raiha  11358 Mar 30 00:58 LICENSE
-rw-r--r--   1 raiha raiha     14 Mar 30 00:58 README.md
-r--r--r--   1 raiha raiha    483 Mar 30 00:58 cdk.json
drwxr-xr-x 392 raiha raiha  16384 Mar 30 00:59 node_modules/
-rw-r--r--   1 raiha raiha   2348 Mar 30 00:59 package.json
drwxr-xr-x   2 raiha raiha   4096 Mar 30 00:58 src/
drwxr-xr-x   2 raiha raiha   4096 Mar 30 00:58 test/
-r--r--r--   1 raiha raiha    870 Mar 30 00:58 tsconfig.dev.json
-r--r--r--   1 raiha raiha    844 Mar 30 00:58 tsconfig.json
-rw-r--r--   1 raiha raiha 189043 Mar 30 00:59 yarn.lock

.projenrc.tsの編集

.gitignoreを含めて読み取り権限のみが与えられており、これらのファイルの編集は.projenrc.tsから行う。

awscdk.AwsCdkTypeScriptAppのプロパティにgitignoredepsおよびdevDepsを追加してみます。

Constructを作成するプロジェクトになりますが、以下もイメージをつかむ参考になりそうです。
https://projen.io/docs/project-types/aws-cdk-construct-library/

.projenrc.ts
import { awscdk } from 'projen';
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'CloudTFd',
  projenrcTs: true,

  // deps: [],                /* Runtime dependencies of this module. */
  // description: undefined,  /* The description is just a string that helps people understand the purpose of the package. */
  // devDeps: [],             /* Build dependencies for this module. */
  // packageName: undefined,  /* The "name" in package.json. */
  gitignore: [
    'cdk.context.json',
    'lib/config/config.ts',
  ],
  deps: ['cdk-monitoring-constructs'],
  devDeps: ['@types/aws-lambda'],
});
project.synth();

反映には再度以下を実施すると、追加が確認できる。

npx projen
.gitignore
// snip
.cache
cdk.context.json
lib/config/config.ts
/test-reports/
// snip
package.json
{ // snip
  "devDependencies": {
    "@types/aws-lambda": "^8.10.136",
    "@types/jest": "^29.5.12",
    "@types/node": "^18",
    "@typescript-eslint/eslint-plugin": "^6",
    "@typescript-eslint/parser": "^6",
    "aws-cdk": "^2.1.0",
    "esbuild": "^0.20.2",
    "eslint": "^8",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.29.1",
    "jest": "^29.7.0",
    "jest-junit": "^15",
    "projen": "^0.80.15",
    "ts-jest": "^29.1.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.4.3"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.1.0",
    "cdk-monitoring-constructs": "^7.8.0",
    "constructs": "^10.0.5"
  },
  // snip
}

yarn/npx projen

.projen/tasks.json配下に実行コマンドが記載されている。

yarn build
yarn eslint
npx projen build
npx projen eslint

eslint


build

yarn buildするとテストも行われる。

Successfully synthesized to /projen/CloudTFd/cdk.out
Supply a stack id (CloudTFdStack, GlobalStack) to display its template.

👾 build » test | jest --passWithNoTests --updateSnapshot
 PASS  test/main.test.ts (5.725 s)
  ✓ Snapshot (1614 ms)

----------------------------------|---------|----------|---------|---------|----------------------------
File                              | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s          
----------------------------------|---------|----------|---------|---------|----------------------------
All files                         |   96.52 |    62.96 |   95.74 |   96.52 |                            
 lib                              |     100 |      100 |     100 |     100 |                            
  cloudtfd-stack.ts               |     100 |      100 |     100 |     100 |                            
  global-stack.ts                 |     100 |      100 |     100 |     100 |                            
 lib/config                       |     100 |      100 |     100 |     100 |                            
  config.ts                       |     100 |      100 |     100 |     100 |                            
 lib/constructs                   |   97.05 |    61.53 |   95.23 |   97.05 |                            
  application-patterns.ts         |     100 |      100 |     100 |     100 |                            
  base.ts                         |     100 |      100 |     100 |     100 |                            
  basic-auth.ts                   |     100 |      100 |     100 |     100 |                            
  bucket.ts                       |     100 |      100 |     100 |     100 |                            
  cdn.ts                          |     100 |       50 |     100 |     100 | 84                         
  database.ts                     |     100 |      100 |     100 |     100 |                            
  mail.ts                         |     100 |      100 |     100 |     100 |                            
  monitor.ts                      |     100 |      100 |     100 |     100 |                            
  redis.ts                        |     100 |      100 |     100 |     100 |                            
  waf.ts                          |   89.23 |    63.63 |      90 |   89.23 | 36,107-111,163-165,183,244 
 lib/constructs/utils             |   94.17 |    64.28 |   95.83 |   94.17 |                            
  aws-managed-prefix-list.ts      |     100 |      100 |     100 |     100 |                            
  cloudfront-keypair-generator.ts |     100 |      100 |     100 |     100 |                            
  default-security-group.ts       |     100 |      100 |     100 |     100 |                            
  domain.ts                       |     100 |      100 |     100 |     100 |                            
  smtp-credentials-generator.ts   |     100 |      100 |     100 |     100 |                            
  waf-ipsets.ts                   |   83.33 |    83.33 |     100 |   83.33 | 22-28                      
  waf-statement.ts                |   87.09 |       50 |   94.44 |   87.09 | 26,62,204,221              
----------------------------------|---------|----------|---------|---------|----------------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   2 passed, 2 total
Time:        5.852 s, estimated 7 s
Ran all test suites.
👾 build » test » eslint | eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern  src test build-tools projenrc .projenrc.ts

deploy/destroy

cdk deploy/destroyと同じ

yarn deploy
yarn deploy --all
yarn destroy
yarn destroy --all

サブプロジェクトの追加

AWS CDKでLambdaをデプロイしたい場合に、そのLambdaを(CDKのProjenを親とした)Projenでプロジェクトを生成できる。
https://projen.io/docs/project-types/aws-cdk-construct-library/

以下を実行すると./src/lib/lambda/typescriptにProjenで生成される設定ファイルが生成されるが、.projenrc.tsだけは親側にしかない。
つまりサブプロジェクトであっても、親プロジェクトの.projenrc.tsを編集したらnpx projenという流れは変わらない。

.projenrc.ts
import { awscdk, typescript } from 'projen';
const cdkProject = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.116.0',
  defaultReleaseBranch: 'main',
  name: 'CloudTFd',
  projenrcTs: true,

  gitignore: [
    'cdk.context.json',
    'config.ts',
  ],
  deps: [
    'cdk-monitoring-constructs',
    '@aws-cdk/aws-redshift-alpha',
    'constructs',
  ],
  devDeps: ['@types/aws-lambda'],
});

const lambdaTS = new typescript.TypeScriptProject({
  defaultReleaseBranch: 'main',
  name: 'lambdaTS',
  parent: cdkProject,
  outdir: './src/lib/lambda/typescript',
});

cdkProject.synth();
lambdaTS.synth();

サブプロジェクトのよくわかっていないところ

  1. サブプロジェクトが参照するdepsLockFilePath
    親と子を両方ともTypeScriptで作ったせい(?)なのかNodejsFuntionで指定するdepsLockFilePathはプロジェクトのルート配下にある./package-lock.jsonを指定したら動いた。
    そもそもNodejsFunctionを使ったのも初めてなのでいろいろ勘違いしている場所かも。

  2. 親プロジェクトがTypeScriptでサブプロジェクトがPythonで作成したときのテスト
    サブプロジェクトもTypescriptだったらJestが動いているっぽいけど、サブプロジェクトがPythonだったときにテストが動いてない気がする。

    親が作られたときにできるtestのタスクがjestなのでそれはそうなのだが、Projen的に自分でタスクに追加するものなのかがよくわかっていない。(未調査)

タスクの追加

.projenrc.tsにtaskを追加することで、yarnで実行できるコマンドを増やせる。
execに実行したいコマンド
conditionにexecを実行する条件を書く。conditionを実行した終了コードが0であれば実行される。
spawnで別のタスクを呼び出す

.projenrc.ts
const cdkProject = new awscdk.AwsCdkTypeScriptApp({
  ...
});

cdkProject.addTask('initial-ctfd-repos', {
  steps: [
    {
      name: 'add-ctfd',
      exec: 'git submodule add https://github.com/CTFd/CTFd.git CTFd',
      condition: '! git submodule status CTFd >/dev/null',
    },
    {
      name: 'add-ctfd-cloudfornt-plugin',
      exec: 'git submodule add https://github.com/raihalea/CTFd-CloudFront-signed-url.git CTFd-Plugins/cloudfront',
      condition: '! git submodule status CTFd-Plugins/cloudfront >/dev/null',
    },
    {
      name: 'copy-plugins',
      spawn: 'copy-plugins',
    },
  ],
});

cdkProject.addTask('copy-plugins', {
  exec: 'cp -r ./CTFd-Plugins/* ./CTFd/CTFd/plugins/',
});

cdkProject.addTask('pull-submodules', {
  exec: 'git submodule update --init --recursive',
});

以下のようなものが生成される。

tasks.json
    "initial-ctfd-repos": {
      "name": "initial-ctfd-repos",
      "steps": [
        {
          "name": "add-ctfd",
          "exec": "git submodule add https://github.com/CTFd/CTFd.git CTFd",
          "condition": "! git submodule status CTFd >/dev/null"
        },
        {
          "name": "add-ctfd-cloudfornt-plugin",
          "exec": "git submodule add https://github.com/raihalea/CTFd-CloudFront-signed-url.git CTFd-Plugins/cloudfront",
          "condition": "! git submodule status CTFd-Plugins/cloudfront >/dev/null"
        },
        {
          "name": "copy-plugins",
          "spawn": "copy-plugins"
        }
      ]
    },

Github Actionsの対応

projenはデフォルトでいくつかのWorkflows(./github/workflows/)を展開するが、何も設定しないとpersonal access tokenを前提に作成される。

PATを使用する場合

PATを使用したい場合はPROJEN_GITHUB_TOKENをRepository secretsに入れる
https://github.com/raihalea/CDK-AutoScalingNatInstance/blob/f3af1b4e408d49c98563ccf51b41dbe2c178087a/.github/workflows/upgrade.yml#L57-L61

      - name: Create Pull Request
        id: create-pr
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.PROJEN_GITHUB_TOKEN }}

GitHub Appsを使いたい場合にはprojenCredentialsを追記する。

.projen.ts
import { awscdk, github } from 'projen';
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'BottleNat',
  projenrcTs: true,
  repository: 'https://github.com/raihalea/CDK-AutoScalingNatInstance.git',
  gitignore: [
    'cdk.context.json',
    'test/__snapshots__',
  ],
  deps: [
    'constructs',
  ],
  devDeps: ['@types/aws-lambda'],
  projenCredentials: github.GithubCredentials.fromApp({
    appIdSecret: 'APP_ID',
    privateKeySecret: 'PRIVATE_KEY',
  }),
});
project.synth();

GitHub Appsの作成

Projenの話ではないので簡単に
Applicationsの作成と、それを該当リポジトリからアクセスできるようにする
画面が現時点(2024/04/07)と異なるけどこれが参考になる

権限とアクセス

リポジトリから利用する

上記のコードの場合はAPP_IDPRIVATE_KEYをリポジトリのRepository secretsに作成する。(Secrets and variables -> Actions)

Workflowの追加

参考になりそうなURL
https://subaud.io/blog/github-action-workflows-in-projen

git submoduleの更新を月1でPRするAction
(長くなったけど、これをいい感じに省略できるものはProjenで提供されてるのだろうか?別ファイルで管理したくなってくる)

.projenrc.ts
if (project.github) {
  const updateSubmoduels = project.github.addWorkflow('update-submodules');
  updateSubmoduels.on({ schedule: [{ cron: '0 0 1 * *' }], workflowDispatch: {} });
  updateSubmoduels.addJobs({
    updateCtfd: {
      runsOn: ['ubuntu-latest'],
      name: 'update-submoduels',
      permissions: {
        contents: JobPermission.READ,
      },
      steps: [
        {
          name: 'Generate token',
          id: 'generate_token',
          uses: 'actions/create-github-app-token@v1',
          with: {
            app_id: '${{ secrets.APP_ID }}',
            private_key: '${{ secrets.PRIVATE_KEY }}',
          },
        },
        {
          name: 'check out',
          uses: 'actions/checkout@v4',
          with: {
            submodules: 'recursive',
          },
        },
        {
          name: 'update submodules',
          run: 'git submodule update --remote --recursive',
        },
        {
          name: 'git status',
          id: 'status',
          run: 'echo "status=$(git status -s)" >> $GITHUB_OUTPUT',
        },
        {
          name: 'Create Pull Request',
          uses: 'peter-evans/create-pull-request@v6',
          if: '${{ steps.status.outputs.status }}',
          with: {
            token: '${{ steps.generate_token.outputs.token }}',
            message: 'chore(deps): upgrade submodules',
            branch: 'github-actions/upgrade-submodules',
            title: 'chore(deps): upgrade submodules',
            author: 'github-actions <github-actions@github.com>',
            comitter: 'github-actions <github-actions@github.com>',
          },
        },
      ],
    },
  });
}
一応.projenrc.tsの全文
.projenrc.ts
import { awscdk, github } from 'projen';
import { JobPermission } from 'projen/lib/github/workflows-model';
const project = new awscdk.AwsCdkTypeScriptApp({
  cdkVersion: '2.1.0',
  defaultReleaseBranch: 'main',
  name: 'BottleNat',
  projenrcTs: true,
  repository: 'https://github.com/raihalea/CDK-AutoScalingNatInstance.git',
  gitignore: [
    'cdk.context.json',
    'test/__snapshots__',
  ],
  deps: [
    'constructs',
  ],
  devDeps: ['@types/aws-lambda'],
  projenCredentials: github.GithubCredentials.fromApp({
    appIdSecret: 'APP_ID',
    privateKeySecret: 'PRIVATE_KEY',
  }),
});

if (project.github) {
  const updateSubmoduels = project.github.addWorkflow('update-submodules');
  updateSubmoduels.on({ schedule: [{ cron: '0 0 1 * *' }], workflowDispatch: {} });
  updateSubmoduels.addJobs({
    updateCtfd: {
      runsOn: ['ubuntu-latest'],
      name: 'update-submoduels',
      permissions: {
        contents: JobPermission.READ,
      },
      steps: [
        {
          name: 'Generate token',
          id: 'generate_token',
          uses: 'actions/create-github-app-token@v1',
          with: {
            app_id: '${{ secrets.APP_ID }}',
            private_key: '${{ secrets.PRIVATE_KEY }}',
          },
        },
        {
          name: 'check out',
          uses: 'actions/checkout@v4',
          with: {
            submodules: 'recursive',
          },
        },
        {
          name: 'update submodules',
          run: 'git submodule update --remote --recursive',
        },
        {
          name: 'git status',
          id: 'status',
          run: 'echo "status=$(git status -s)" >> $GITHUB_OUTPUT',
        },
        {
          name: 'Create Pull Request',
          uses: 'peter-evans/create-pull-request@v6',
          if: '${{ steps.status.outputs.status }}',
          with: {
            token: '${{ steps.generate_token.outputs.token }}',
            message: 'chore(deps): upgrade submodules',
            branch: 'github-actions/upgrade-submodules',
            title: 'chore(deps): upgrade submodules',
            author: 'github-actions <github-actions@github.com>',
            comitter: 'github-actions <github-actions@github.com>',
          },
        },
      ],
    },
  });
}

project.synth();

Discussion