LoginSignup
9
8

More than 1 year has passed since last update.

aws-cdkのL1・L2・L3とは aws-cdk#3

Posted at

概要

前回の記事でのこの宣言通り
cdkのL1とL2(そしてなんとL3も)について書いていきます。

もう少し深く(cdkのL1とL2について)書こうと思いましたが
予想より文量が多くなってしまったので次回に回そうと思います。
主にCfnとcdkの関係の話になります。

結論

一口にaws-cdkと言ってもその中には3つのレイヤーが存在します。

  • L1:ほとんどそのままCfnのテンプレートを記述するもの→書いたものがそのままリソースとなる
  • L2:設定しなくてもデフォルトで設定をいい感じにしてくれる
  • L3:単一のリソースではなくAWSが設定したタスク群を設定できる。

レイヤーが高くなるほど便利になる(コード量や設定箇所)一方、設定の自由度が下がると言ったイメージ。

環境

操作する環境は以下の通りです。

% node -v //Node.jsのバージョン
v14.16.0

% tsc -v //TypeScriptのバージョン
Version 4.5.5

% cdk --version //aws-cdkのバージョン
1.142.0

L1

AWSの説明はこちら

beginning with low-level constructs, which we call CFN Resources (or L1, short for "layer 1") or Cfn (short for CloudFormation) resources. These constructs directly represent all resources available in AWS CloudFormation. CFN Resources are periodically generated from the AWS CloudFormation Resource Specification. They are named CfnXyz, where Xyz is name of the resource.

「頭にCfnとついているクラス(L1のクラス)はCfnを直接表現でき、Cfnで扱えるものは全てこのレイヤーで扱える。」とあります。
cdkの中でも低レイヤーのクラスであるということのようです。

実践

実際にやってみましょう。
CfnVPCクラスを例にしてみます。リファレンスはこちら

//lib/cdk-stack.ts

+import { CfnVPC } from "@aws-cdk/aws-ec2";
import * as cdk from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    new CfnVPC(this, "VPC", {
+      cidrBlock: "10.0.0.0/16",
    });
  }
}

必須要素のcidrBlockのみ指定してみます。
cdk synthにて出力したCfnのテンプレートはこちら。

% cdk synth CdkStack --profile test
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
    Metadata:
      aws:cdk:path: CdkStack/VPC
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/yWMQQqAIBAA39Jdt4zoHPiBKOge6wYWKbhaB/HvFZ0GhmEUqK6FphrWmyWao87oA0Ge44qH0N5xDAmj0JubiH0KSEV8LWELr1xGXYTzhmDn+lIdqP697WytDMlFexJMPx/x8RwRagAAAA==
    Metadata:
      aws:cdk:path: CdkStack/CDKMetadata/Default

指定した通り、CidrBlockが10.0.0.0/16のAWS::EC2::VPCが作成されてます。(deployはスキップします。)
指定した分だけ反映されるというのがミソですね。

L2

少し長いですが、こちらも公式ドキュメントから

The next level of constructs, L2, also represent AWS resources, but with a higher-level, intent-based API. They provide similar functionality, but provide the defaults, boilerplate, and glue logic you'd be writing yourself with a CFN Resource construct. AWS constructs offer convenient defaults and reduce the need to know all the details about the AWS resources they represent, while providing convenience methods that make it simpler to work with the resource.

「L1と同様AWSリソースを表現するが、AWSリソースの全ての知識がなくても詳細まで表現できる」とのことですね。
細かい設定は知らなくでも勝手にやってくれるイメージでしょうか。

実践

ピンと来なければやってみればいいんです。

//lib/cdk-stack.ts

+import { Vpc } from "@aws-cdk/aws-ec2";
import * as cdk from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    new Vpc(this, "VPC", {
+      cidr: "10.0.0.0/16",
    });
  }
}

今度はVpcクラスで構築してみます。
リファレンス置いときます。
必須ではないですが(この時点でL1と違いますね)先ほどと同様にcidrを設定します。

さあ、どうなるんでしょうか。

% cdk synth CdkStack --profile test
Resources:
  VPCB9E5F0B4:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: CdkStack/VPC
    Metadata:
      aws:cdk:path: CdkStack/VPC/Resource
  VPCPublicSubnet1SubnetB4246D30:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/Subnet
  VPCPublicSubnet1RouteTableFEE4B781:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/RouteTable
  VPCPublicSubnet1RouteTableAssociation0B0896DC:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet1RouteTableFEE4B781
      SubnetId:
        Ref: VPCPublicSubnet1SubnetB4246D30
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/RouteTableAssociation
  VPCPublicSubnet1DefaultRoute91CEF279:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet1RouteTableFEE4B781
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: VPCIGWB7E252D3
    DependsOn:
      - VPCVPCGW99B986DC
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/DefaultRoute
  VPCPublicSubnet1EIP6AD938E8:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/EIP
  VPCPublicSubnet1NATGatewayE0556630:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId:
        Ref: VPCPublicSubnet1SubnetB4246D30
      AllocationId:
        Fn::GetAtt:
          - VPCPublicSubnet1EIP6AD938E8
          - AllocationId
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet1/NATGateway
  VPCPublicSubnet2Subnet74179F39:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.32.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/Subnet
  VPCPublicSubnet2RouteTable6F1A15F1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/RouteTable
  VPCPublicSubnet2RouteTableAssociation5A808732:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet2RouteTable6F1A15F1
      SubnetId:
        Ref: VPCPublicSubnet2Subnet74179F39
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/RouteTableAssociation
  VPCPublicSubnet2DefaultRouteB7481BBA:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet2RouteTable6F1A15F1
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: VPCIGWB7E252D3
    DependsOn:
      - VPCVPCGW99B986DC
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/DefaultRoute
  VPCPublicSubnet2EIP4947BC00:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/EIP
  VPCPublicSubnet2NATGateway3C070193:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId:
        Ref: VPCPublicSubnet2Subnet74179F39
      AllocationId:
        Fn::GetAtt:
          - VPCPublicSubnet2EIP4947BC00
          - AllocationId
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet2/NATGateway
  VPCPublicSubnet3Subnet631C5E25:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.64.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1d
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/Subnet
  VPCPublicSubnet3RouteTable98AE0E14:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/RouteTable
  VPCPublicSubnet3RouteTableAssociation427FE0C6:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet3RouteTable98AE0E14
      SubnetId:
        Ref: VPCPublicSubnet3Subnet631C5E25
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/RouteTableAssociation
  VPCPublicSubnet3DefaultRouteA0D29D46:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPublicSubnet3RouteTable98AE0E14
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: VPCIGWB7E252D3
    DependsOn:
      - VPCVPCGW99B986DC
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/DefaultRoute
  VPCPublicSubnet3EIPAD4BC883:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/EIP
  VPCPublicSubnet3NATGatewayD3048F5C:
    Type: AWS::EC2::NatGateway
    Properties:
      SubnetId:
        Ref: VPCPublicSubnet3Subnet631C5E25
      AllocationId:
        Fn::GetAtt:
          - VPCPublicSubnet3EIPAD4BC883
          - AllocationId
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PublicSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PublicSubnet3/NATGateway
  VPCPrivateSubnet1Subnet8BCA10E0:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.96.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet1/Subnet
  VPCPrivateSubnet1RouteTableBE8A6027:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet1
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet1/RouteTable
  VPCPrivateSubnet1RouteTableAssociation347902D1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet1RouteTableBE8A6027
      SubnetId:
        Ref: VPCPrivateSubnet1Subnet8BCA10E0
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet1/RouteTableAssociation
  VPCPrivateSubnet1DefaultRouteAE1D6490:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet1RouteTableBE8A6027
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: VPCPublicSubnet1NATGatewayE0556630
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet1/DefaultRoute
  VPCPrivateSubnet2SubnetCFCDAA7A:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.128.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet2/Subnet
  VPCPrivateSubnet2RouteTable0A19E10E:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet2
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet2/RouteTable
  VPCPrivateSubnet2RouteTableAssociation0C73D413:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet2RouteTable0A19E10E
      SubnetId:
        Ref: VPCPrivateSubnet2SubnetCFCDAA7A
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet2/RouteTableAssociation
  VPCPrivateSubnet2DefaultRouteF4F5CFD2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet2RouteTable0A19E10E
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: VPCPublicSubnet2NATGateway3C070193
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet2/DefaultRoute
  VPCPrivateSubnet3Subnet3EDCD457:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.160.0/19
      VpcId:
        Ref: VPCB9E5F0B4
      AvailabilityZone: ap-northeast-1d
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Private
        - Key: aws-cdk:subnet-type
          Value: Private
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet3/Subnet
  VPCPrivateSubnet3RouteTable192186F8:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      Tags:
        - Key: Name
          Value: CdkStack/VPC/PrivateSubnet3
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet3/RouteTable
  VPCPrivateSubnet3RouteTableAssociationC28D144E:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet3RouteTable192186F8
      SubnetId:
        Ref: VPCPrivateSubnet3Subnet3EDCD457
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet3/RouteTableAssociation
  VPCPrivateSubnet3DefaultRoute27F311AE:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: VPCPrivateSubnet3RouteTable192186F8
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: VPCPublicSubnet3NATGatewayD3048F5C
    Metadata:
      aws:cdk:path: CdkStack/VPC/PrivateSubnet3/DefaultRoute
  VPCIGWB7E252D3:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: CdkStack/VPC
    Metadata:
      aws:cdk:path: CdkStack/VPC/IGW
  VPCVPCGW99B986DC:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: VPCB9E5F0B4
      InternetGatewayId:
        Ref: VPCIGWB7E252D3
    Metadata:
      aws:cdk:path: CdkStack/VPC/VPCGW
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/0WPTw+CMAzFP4v3MYQQzxJiDBdDwHAfpcbxZzNbpzGE7y6IyKm/vr42fQEPopDvd0fxsh7UrT+ANsiHggS0LEernQFkiVaWjANiyU2t6sjmJYSQD+UD5kmZJSxzVSehcJXCr3ujXDvCq6g63PRNi63VIAVJrf7mGU5pNpeLoLMgfIk3y4x8TrgdThWhmXg1LJ/8upimKPceFY0jU7pG3lj/GUQ8OEy5GyulZ5wi2SPPl/oBpmTsORQBAAA=
    Metadata:
      aws:cdk:path: CdkStack/CDKMetadata/Default

な、長い。。。
よく見てみると10.0.0.0/16のAWS::EC2::VPCだけでなく
Subnet、IGW、NATGW、RouteTableまでできてますね。

これが

but provide the defaults, boilerplate, and glue logic you'd be writing yourself with a CFN Resource construct.

ということなんでしょうね。
設定をしていなくても「いい感じに」設定を入れてくれるってとこでしょうか。

確かに勝手にサービスを作られているのは「えっ?」って感じですが、
いざEC2を立てようとするときVPCだけじゃうまくいかないですもんね。

EC2に詳しければ、Subnetを立てて、IGWとRTを設定して、、とやっていけますがそこまで詳しくないと厳しいかも。。

それをAWSにそこまで詳しくなくても
「取り合えす動く状態」にしてくれるのがL2の良さってことでしょうね。
(ちなみにL1だと全て自分で設定が必要です)
※もちろん詳細な設定はL2でも可能です。「詳細な設定をしなくても動く」というのが強みですね

L3

こちらより引用

the AWS Construct Library includes L3 constructs, which we call patterns. These constructs are designed to help you complete common tasks in AWS, often involving multiple kinds of resources.

L2までと違い
リソース単体ではなく、よく使われるリソースの組み合わせを提供してくれるようです。
これは便利そう。
一つのクラスでよく使う構成が作れるということですね。

実践

ということで実践。
ドキュメントにあったこちらのクラスを構築していこうと思います。
よくあるLambda+APIGatewayの組み合わせを構築できるということのようです。

が、結論から言うとこのL3はあまりイケてない。。
書きたいこと(L1とL2の抽象度の違い)から外れるのと
L1→L2の時にあった「自動で勝手に補完してくれる」と言う感動もないので軽く触れて終わります。

//lib/cdk-stack.ts

+import { LambdaRestApi } from "@aws-cdk/aws-apigateway";
import * as cdk from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    new LambdaRestApi(this, "LambdaRestApi", {
+      handler: ,
+    });
  }
}

handlerは必須パラメータなのですが、このhandlerの型がIFunction型。つまりlambda functionを割り当てないといけないわけです。
早い話が「自分でlambda」を作らないといけないってことです。
まあ、いい感じに勝手に作れ!ってのも無茶な話ですが。。。

作りました。

//lib/cdk-stack.ts

import { LambdaRestApi } from "@aws-cdk/aws-apigateway";
+import { Code, Function, Runtime } from "@aws-cdk/aws-lambda";
import * as cdk from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
+    const func = new Function(this, "Function", {
+      code: Code.fromAsset(`${__dirname}/../lambdas/src`),
+      handler: "index.handler",
+      runtime: Runtime.PYTHON_3_8,
    });

    new LambdaRestApi(this, "LambdaRestApi", {
-      handler:
+      handler: func,
    });
  }
}

Lambda+APIGatewayの組み合わせを構築するのにL2でlambdaとAPIGatewayを作りました、
と言う話です。
(連携は自動でしてくれますが。。。)

L3を使うメリットがあまり感じないというより、
やっていることはL2の構築なのでL3を気にする必要はないのかなという印象です。

まとめ

  • L1
    • メリット
      • 基本的にCfnと同じなので記述した以外のリソースを勝手に作成されない。
      • Cfnに対応しているリソースであれば全てこのレイヤーで扱える。
    • デメリット
      • リソースの深い知識や理解がないと稼働するリソースを作成するハードルが高い。
      • 必然的にコード量が多くなる。
  • L2(&L3)
    • メリット
      • 必須プロパティさえ設定すれば、自動で稼働するリソースを作成してくれる
      • 少ないコード量でリソースを定義できる
    • デメリット
      • 対応していないリソースがある
      • 思わぬリソースや意図しない設定で構築されることに注意しないといけない。

あるあるですが、メリットとデメリットが逆になっている感じですね。

L2(L3)でも詳細な設定ができないわけではないので、
基本はL2で構築が良いかなと思います。

ただ、
「設定項目がない」だとか、「そもそもリソースが対応していない」って場合はL1で構築をチャレンジということになるかなと思います。

補足

同一スタック内でもL1とL2、L3を混在させて記述することは可能です。
が、propsは共有できないので連携させる際には注意が必要です。

さいごに

L1とL2を整理することで
構築で利用するクラスを意識しながら構築を進められるといいなと思います。
特に、
L2を利用しているときに、どういった設定が自動でされているのか意識することでより詳細な設計に活かせる気がします。

次回はまあ単純にEC2でも立ててみます。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8