AWS CDK書き始め用の個人的メモ

CDK自体はいじったことがあったのだが、APIリファレンスの追い方をちょっと前に整理したものをここに保存。
後は、サンプルがどうもVPCを新規に作るものが多いような気がするので「既存のサブネットを使う」コードを保存しておきたかったという思いもある。
数年後に忘れても思い出せますように。
自分のためのメモだが、ついでに初学者の人のヒントになれば嬉しい。

## 記事の目的
- APIリファレンスの読み方
- そのほかサンプルやテスト方法などについて参考リンクのメモ
- ALB + EC2 を既存サブネットに起動するコードメモ


## APIリファレンスの見方
### importの書き方
リファレンスの細かい見方はここが参考になった。

https://zenn.dev/proyogram/articles/937b43d370fbc7

例として、[ApplicationLoadBalancer](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer.html)の場合には以下のようにタイプが書いてある。

> aws-cdk-lib » aws_elasticloadbalancingv2 » ApplicationLoadBalancer

この場合importの仕方と使い方はアンダーバー(_)をハイフン(-)に変えて以下のようになる。

```typescript
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
...
    const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", { 
    ...
```
このimportの書き方は以下のページなどで使われている書き方である。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2-readme.html

他にもimportの書き方は複数あり、以下のページでは*Classic import*と*Barrel import*の2通りを紹介している。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.html

それぞれ以下。
**Classic import**
```typescript
import { Stack, App, aws_s3 as s3 } from 'aws-cdk-lib';
...
new s3.Bucket(stack, 'TestBucket');
```

**Barrel import**
```typescript
import { Bucket } from 'aws-cdk-lib/aws-s3';
...
new Bucket(stack, 'TestBucket');
```

また、上記含めた色々なimport方法を比較してくれている記事もあった。好きな方法で書けば良い。

https://zenn.dev/keyamin/articles/1617db66ca6644


### リファレンスの追い方
「ALB作成時に既存のサブネットを指定したい」という状況を例にとってみていく。
[ApplicationLoadBalancer](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer.html)のページを見る

vpcSubnetsプロパティは[SubnetSelection](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SubnetSelection.html)というタイプなので、SubnetSelectionタイプでどうやってサブネットを指定できるのか、プロパティを見る。
SubnetSelectionでサブネットを指定できそうなのはsubnetsプロパティで、これは[ISubnet[]](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.ISubnet.html)タイプである。そこでさらにISubnetのリファレンスを見ると以下のようなメソッドから取得できることがわかる。
> Obtainable from Subnet.fromSubnetAttributes(), Subnet.fromSubnetId()

なので、こんな感じで既存サブネットを指定したSubnetSelectionを取得できる(fromSubnetAttributesとfromSubnetIdの使い分けはパッとわからなかったので置いておく)
```typescript
import * as ec2 from "aws-cdk-lib/aws-ec2";

const subnetIds = [
      {
        "subnetId":"subnet-xxxxx",
        "az":"ap-northeast-1a"
        
      },
      {
        "subnetId":"subnet-yyyyy",
        "az":"ap-northeast-1b"
      }
    ]

        const subnets: ec2.SubnetSelection = {
      subnets:subnetIds.map(subnet => {
        // fromSubnetAttributesはISubnetタイプを返す
        return ec2.Subnet.fromSubnetAttributes(this, "subnet-"+subnet.subnetId, {
          "subnetId":subnet.subnetId,
          "availabilityZone": subnet.az
        })
      })
    }
```

## バージョン管理について
CDK固有のものではない情報も多いがこれを機にメモ。
- CDKバージョンを上げたいときは*aws-cdk*だけではなく、`package.json`の*aws-cdk-lib*を見る必要がある。
- グローバルでcdkをインストールするとバージョンの不整合が起こりうるので、`npm install aws-cdk`でローカルインストールして`npx cdk`で使うのがバージョンの管理はしやすそう
- 以下のコマンドで、全部のパッケージを最新にできる。この記事がバージョン管理について考察してくれている


https://sotoiwa.hatenablog.com/entry/2021/12/16/182533
```
npm install npm-check-updates
npx ncm -u
npm install
```

## コンストラクタの作り方
この辺りが参考になりそう(読んだわけではない)。
- [AWS CDK Immersion Day ワークショップ](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/ja-JP/2000-typescript-workshop/400-hit-counter/410-api)

- [AWS CDK の3種類の Construct を使ってデプロイしてみた](https://dev.classmethod.jp/articles/aws-cdk-construct-explanation/) scopeでthisを指定する時以外の参考になりそう

## L2コンストラクトで対応していないプロパティを変えるエスケープハッチ
L1コンストラクトを取り出してそのプロパティを変更する。

https://docs.aws.amazon.com/cdk/v2/guide/cfn_layer.html#develop-customize-escape

```typescript
// Get the CloudFormation resource
const cfnBucket = bucket.node.defaultChild as s3.CfnBucket;

// Change its properties
cfnBucket.analyticsConfiguration = [
  { 
    id: 'Config',
    // ...        
  } 
];
```

## テスト方法
`aws-cdk-lib/assertions`ライブラリを使って単体テストができそう

[AWS CDK Immersion Day ワークショップ](https://catalog.us-east-1.prod.workshops.aws/workshops/10141411-0192-4021-afa8-2436f3c66bd8/ja-JP/2000-typescript-workshop/600-advanced-topics/650-construct-testing)

## サンプル
- https://github.com/aws-samples
色々なサンプルが置いてあるレポジトリだが、cdk + 使いたいサービス名で検索するとCDKアプリケーションもあったりする

- [AWS Solutions Constructs](https://docs.aws.amazon.com/solutions/latest/constructs/api-reference.html)
CDKチームではないがAWSの人が開発しているらしい。「API Gateway + Lambda」とか「Fargate + SQS」とか、2サービスくらいを組み合わせてまとめたものが多そう。単品でこれを作るのは面倒だよね、というレベルのものはないが、シンプルが故にパーツとしては使いやすそう。

- https://cdkpatterns.com/
CDKサンプルをオープンソースで公開するプロジェクトのよう
例えば以下のLambda + RDS (RDS Proxy)など、しっかりした構成のサンプル
https://github.com/cdk-patterns/serverless/tree/main/the-rds-proxy/typescript

## ALB + EC2 を既存サブネットに起動するコード例

やはりどこかに雛形を置いておきたいためこの記事に入れる。
あとデフォルトでALB作ろうとすると同じAZにノード二つ配置しようとしてエラーになる気がする。

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';


import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as elbv2target from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as asg from 'aws-cdk-lib/aws-autoscaling';

// 既存のVPCとサブネットを指定する
// ALBはinternet facingで80ポートフルオープン
// **EC2インスタンスはSSHポートを開けていないのでそこは追記する**
// ALBにアクセスすると固定文字列が返されるだけのシンプルなApacheサーバー

//既存VPCを選択
const VPCID = "vpc-aaa";

// 既存のパブリックサブネットとそのID。AZは別のサブネットを選ぶこと。
const subnetIds = [
  {
    "subnetId": "subnet-xxxxx",
    "az": "ap-northeast-1a"

  },
  {
    "subnetId": "subnet-yyyyy",
    "az": "ap-northeast-1b"
  }
]

//既存キーペア名
const ec2KeyPairName = "<EC2インスタンス接続用の既存キーペアの名前>";

export class Albwithec2Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 既存のVPC, subnetを取得
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: VPCID })

    const subnets: ec2.SubnetSelection = {
      subnets: subnetIds.map(subnet => {
        return ec2.Subnet.fromSubnetAttributes(this, "subnet-" + subnet.subnetId, {
          "subnetId": subnet.subnetId,
          "availabilityZone": subnet.az
        })
      })
    }

    // ALBを作成
    //// SG for ALB
    const sgForAlb = new ec2.SecurityGroup(this, "vpcForAlb", {
      vpc,
      description: "SG for ALB. created by CDK"
    })

    sgForAlb.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80))

    //// ALB
    const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
      vpc,
      vpcSubnets: subnets,
      internetFacing: true,
      securityGroup: sgForAlb
    });

    // EC2インスタンス用のSG
    const sgForEc2 = new ec2.SecurityGroup(this, "vpcForEc2", {
      vpc,
      description: "SG for EC2. created by CDK"
    })

    sgForEc2.addIngressRule(ec2.Peer.securityGroupId(sgForAlb.securityGroupId), ec2.Port.tcp(80))
    // SSH用に22番ポートを開けるルールも書く
    // sgForEc2.addIngressRule(ec2.Peer.prefixList(prefixListId), ec2.Port.tcp(22))
    // sgForEc2.addIngressRule(ec2.Peer.ipv4("172.0.0.0/8"), ec2.Port.tcp(22))

    // ターゲットグループ作成
    const targetGroup = new elbv2.ApplicationTargetGroup(this, "TargetGroup", {
      vpc,
      port: 80,
      targetType: elbv2.TargetType.INSTANCE,
    });

    // リスナーを作成してALBにアタッチ
    alb.addListener("Listener", {
      port: 80,
      defaultTargetGroups: [targetGroup]
    });


    //autoscaling
    ////起動テンプレート
    const userData = ec2.UserData.forLinux();
    userData.addCommands(
      "sudo dnf install httpd -y",
      'sudo echo "test page" |sudo tee /var/www/html/index.html',
      "sudo service httpd restart"
    );

    const launchTemplate = new ec2.LaunchTemplate(this, 'LaunchTemplate', {
      launchTemplateName: "ec2LaunchTemplateByCdk",
      machineImage: ec2.MachineImage.latestAmazonLinux2023(),
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      securityGroup: sgForEc2,
      keyPair: ec2.KeyPair.fromKeyPairName(this, "keyPair", ec2KeyPairName),
      userData
    });

    //起動テンプレートを指定してオートスケーリンググループ作成 & ターゲットグループにアタッチ
    const autoScalingGroup = new asg.AutoScalingGroup(this, "AutoScalingGroup", {
      vpc,
      autoScalingGroupName: "asgByCdk",
      launchTemplate,
      vpcSubnets: subnets
    });
    autoScalingGroup.attachToApplicationTargetGroup(targetGroup)
  }
}

```