iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0
DevOps

CDK 從 0 開始打造靈活自如的 IaC系列 第 7

07 - 參數化 CDK Stacks

  • 分享至 

  • xImage
  •  

本篇文章內有:

  • 使用參數使 Stack 更靈活
  • 多個 Stack
  • 動態指定 Stack 的參數

使用參數使 Stack 更靈活

讓我們回到程式碼的懷抱,先前所寫的 AWS CDK ,其實很多設定是可以變成參數,讓我們可以簡單新增類似,但是卻有點微妙地不同的 Stack 們。

首先,我們來把 cdk.StackProps 擴充一下:

interface AppStackProps extends cdk.StackProps {
  readonly s3BucketExpiration?: cdk.Duration;
  readonly ec2Arch?: "x86_64" | "arm64";
  readonly lambdaValue?: string;
  readonly lambdaIsReturn?: boolean;
}

就做幾個簡單的開關:

  • s3BucketExpiration :控制 Amazon S3 儲存貯體的物件過期時間。
  • ec2Arch :使用 ADM64 或是 ARM64 平台。
  • lambdaValue :指定 AWS Lambda 函數使用的字串。
  • lambdaIsReturn :是否讓 AWS Lambda 函數回傳字串。

再來,將擴充好的 AppStackProps 指定在 AppStack 的建構子中:

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

這樣就完成讓 AppStack 可以接收外部的參數了,但是還沒結束,接收是一回事,要讓 AppStack 針對這些參數作動是一件事。

我們來把 s3BucketExpiration 指定給 Amazon S3 儲存貯體,並且將原本的 7 天做為預設值:

bucket.addLifecycleRule({
  expiration: props?.s3BucketExpiration ?? cdk.Duration.days(7),
});

在這個例子中,我們只會在有明確指示的情況下,將 Amazon EC2 執行個體更換成 x86_64

new cdk.aws_ec2.Instance(this, "instance", {
  vpc,
  instanceType:
    props?.ec2Arch === "x86_64"
      ? cdk.aws_ec2.InstanceType.of(
          cdk.aws_ec2.InstanceClass.BURSTABLE2,
          cdk.aws_ec2.InstanceSize.MICRO
        )
      : cdk.aws_ec2.InstanceType.of(
          cdk.aws_ec2.InstanceClass.BURSTABLE4_GRAVITON,
          cdk.aws_ec2.InstanceSize.SMALL
        ),
  machineImage: new cdk.aws_ec2.AmazonLinuxImage({
    generation: cdk.aws_ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
    cpuType:
      props?.ec2Arch === "x86_64"
        ? cdk.aws_ec2.AmazonLinuxCpuType.X86_64
        : cdk.aws_ec2.AmazonLinuxCpuType.ARM_64,
  }),
}).role.addManagedPolicy(
  cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
    "AmazonSSMManagedInstanceCore"
  )
);

最後,我們將嵌入的程式碼做調整,直接在樣板字面值 (template literals or template strings) 中把變數帶入:

new cdk.aws_lambda.Function(this, "function", {
  vpc,
  runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
  code: cdk.aws_lambda.Code.fromInline(`
    exports.greeting = async function () {
      const value = '${props?.lambdaValue ?? "Hello AWS CDK"}';
      console.log(value);
      ${props?.lambdaIsReturn ? "return value;" : ""}
    };
  `),
  handler: "index.greeting",
});

到這邊為止,對 AppStack 的變更就結束了,可以先部署上去,如此一來晚些再變更參數時會有比較乾淨的差異性更新可以觀察。

接著,讓我們來去給 AppStack 不同的參數,開啟 bin/app.ts ,並在建立 AppStack 的第三個參數中,加上想要的數值,例如:

{
  s3BucketExpiration: cdk.Duration.days(30),
  ec2Arch: "arm64",
  lambdaValue: "Value from the outside",
  lambdaIsReturn: true,
}

然後,看一下是不是有成功的傳入。

Resources
[~] AWS::S3::Bucket bucket bucket43879C71
 └─ [~] LifecycleConfiguration
     └─ [~] .Rules:
         └─ @@ -1,6 +1,6 @@
            [ ] [
            [ ]   {
            [-]     "ExpirationInDays": 7,
            [+]     "ExpirationInDays": 30,
            [ ]     "Status": "Enabled"
            [ ]   }
            [ ] ]
[~] AWS::Lambda::Function function functionF19B1A04
 └─ [~] Code
     └─ [~] .ZipFile:
         ├─ [-]
        exports.greeting = async function () {
          const value = 'Hello AWS CDK';
          console.log(value);

        };

         └─ [+]
        exports.greeting = async function () {
          const value = 'Value from the outside';
          console.log(value);
          return value;
        };

太好了,的確如我們所指定的參數一致。

多個 Stack

現在讓我們建立多個 AppStack ,直接複製貼上就好了,接著看看現在的 Stack 有哪些。

npm run cdk -- list

哇, AWS CDK CLI 生氣了。

/app/node_modules/constructs/src/construct.ts:428
      throw new Error(`There is already a Construct with name '${childName}' in ${typeName}${name.length > 0 ? ' [' + name + ']' : ''}`);
            ^
Error: There is already a Construct with name 'AppStack' in App
    at Node.addChild (/app/node_modules/constructs/src/construct.ts:428:13)
    at new Node (/app/node_modules/constructs/src/construct.ts:71:17)
    at new Construct (/app/node_modules/constructs/src/construct.ts:480:17)
    at new Stack (/app/node_modules/aws-cdk-lib/core/lib/stack.js:1:2457)
    at new AppStack (/app/lib/app-stack.ts:14:5)
    at Object.<anonymous> (/app/bin/app.ts:28:1)
    at Module._compile (node:internal/modules/cjs/loader:1256:14)
    at Module.m._compile (/app/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)
    at Object.require.extensions.<computed> [as .ts] (/app/node_modules/ts-node/src/index.ts:1621:12)

Subprocess exited with error 1

還記得我們之前說的嗎?無所不在的 Construct 會利用人與人的連結,建立起唯一路徑 (path) 。
也就是說,在同一個 scope (Construct) 之下的 id 必須為唯一,不可以重複。

來在後面加個後墜快速處理一下。

new AppStack(app, "AppStack-2", {
  s3BucketExpiration: cdk.Duration.days(30),
  ec2Arch: "arm64",
  lambdaValue: "Value from the outside",
  lambdaIsReturn: true,
});

接著再來跑一次剛才的指令,會看到 AWS CDK CLI 列出了我們所建立的兩個 Stack 。

AppStack
AppStack-2

但是在我們要來部署的時候,卻又看到另一個錯誤訊息:

Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`
Stacks: AppStack · AppStack-2

原來是之前我們一直在走捷徑,當 AWS CDK App 只有一個 Stack 的時候,可以不需要指定 Stack ,而現在有多個 Stack ,則需要跟 AWS CDK CLI 說明我們要針對哪些 Stack 操作。

可以一一列舉,像是:

npm run cdk -- deploy AppStack AppStack-2

或是使用星號:

npm run cdk -- deploy AppStack*

也可以全選:

npm run cdk -- deploy --all

動態指定 Stack 的參數

平安地把第二個 Stack 部署上 AWS 了,那我們來試著用不同的方式指定參數。

new AppStack(app, "AppStack-2", {
  s3BucketExpiration: cdk.Duration.days(30),
  ec2Arch: process.arch.startsWith("arm") ? "arm64" : "x86_64",
  lambdaValue: `Value from env ${
    process.env.AWS_LAMBDA_VALUE
  } at ${new Date().toISOString()}`,
  lambdaIsReturn: true,
});

上面用了 process 以及 Date 來取用 Node.js 本身提供的資訊,事不宜遲,馬上來看看是不是有正確地取用了。
macOS:

AWS_LAMBDA_VALUE='here i am' npm run cdk -- diff AppStack-2

Windows:

$env:AWS_LAMBDA_VALUE='here i am'
npm run cdk -- diff AppStack-2

不只是環境變數,連時間日期與機器架構都有拿到:

Parameters
[-] Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2"}
[+] Parameter SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn2-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter: {"Type":"AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>","Default":"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"}

Resources
[~] AWS::EC2::Instance instance instanceB7CCE687 replace
 ├─ [~] ImageId (requires replacement)
 │   └─ [~] .Ref:
 │       ├─ [-] SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter
 │       └─ [+] SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter
 └─ [~] InstanceType (may cause replacement)
     ├─ [-] t4g.small
     └─ [+] t2.micro
[~] AWS::Lambda::Function function functionF19B1A04
 └─ [~] Code
     └─ [~] .ZipFile:
         ├─ [-]
        exports.greeting = async function () {
          const value = 'Value from the outside';
          console.log(value);
          return value;
        };

         └─ [+]
        exports.greeting = async function () {
          const value = 'Value from env here i am at 2023-09-07T08:00:02.695Z';
          console.log(value);
          return value;
        };

只是取用 Node.js 的資訊肯定無法滿足日益複雜的雲端架構,所以我們要來讓取用不同 Stack 的資訊。

首先,將想要取用的屬性建立出來:

export class AppStack extends cdk.Stack {
  public readonly lambdaFunctionName: string;

  constructor(scope: Construct, id: string, props?: AppStackProps) {

再來,將值指定進去,這邊我們使用 AWS Lambda 函數名稱,因為我們在建立AWS Lambda 函數時沒有指定,所以他會是由 AWS CDK 與 AWS CloudFormation 的亂數組合而成:

const func = new cdk.aws_lambda.Function(this, "function", {
  vpc,
  runtime: cdk.aws_lambda.Runtime.NODEJS_18_X,
  code: cdk.aws_lambda.Code.fromInline(`
	exports.greeting = async function () {
	  const value = '${props?.lambdaValue ?? "Hello AWS CDK"}';
	  console.log(value);
	  ${props?.lambdaIsReturn ? "return value;" : ""}
	};
  `),
  handler: "index.greeting",
});
this.lambdaFunctionName = func.functionName;

接著,把我們要參照的 AppStack 存成變數:

const appStack = new AppStack(app, "AppStack", {

最後,直接使用就可以了:

new AppStack(app, "AppStack-2", {
  s3BucketExpiration: cdk.Duration.days(30),
  ec2Arch: process.arch.startsWith("arm") ? "arm64" : "x86_64",
  lambdaValue: `Value from env ${
    process.env.AWS_LAMBDA_VALUE
  } at ${new Date().toISOString()} and AWS Lambda Function from AppStack is ${
    appStack.lambdaFunctionName
  }`,
  lambdaIsReturn: true,
});

來看一下現在 AWS CDK 跟 AWS 的差異,是不是數值都上去了呢?

Stack AppStack
Outputs
[+] Output Exports/Output{"Ref":"functionF19B1A04"} ExportsOutputReffunctionF19B1A04ABA385C1: {"Value":{"Ref":"functionF19B1A04"},"Export":{"Name":"AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"}}

Stack AppStack-2
Resources
[~] AWS::Lambda::Function function functionF19B1A04
 └─ [~] Code
     └─ [~] .ZipFile:
         └─ @@ -1,1 +1,12 @@
            [-] "\n        exports.greeting = async function () {\n          const value = 'Value from env here i am at 2023-09-07T08:00:02.695Z';\n          console.log(value);\n          return value;\n        };\n      "
            [+] {
            [+]   "Fn::Join": [
            [+]     "",
            [+]     [
            [+]       "\n        exports.greeting = async function () {\n          const value = 'Value from env here i am at 2023-09-07T08:00:05.654Z and AWS Lambda Function from AppStack is ",
            [+]       {
            [+]         "Fn::ImportValue": "AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"
            [+]       },
            [+]       "';\n          console.log(value);\n          return value;\n        };\n      "
            [+]     ]
            [+]   ]
            [+] }

怎麼原本的 AppStack 多了一個資源?

[+] Output Exports/Output{"Ref":"functionF19B1A04"} ExportsOutputReffunctionF19B1A04ABA385C1: {"Value":{"Ref":"functionF19B1A04"},"Export":{"Name":"AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"}}

而且,原本應該要出現 AWS Lambda 函數名稱的地方,怎麼會是這個詭異的字串呢?

[+]       {
[+]         "Fn::ImportValue": "AppStack:ExportsOutputReffunctionF19B1A04ABA385C1"
[+]       },

原來是 AWS CDK 幫忙做的處理。
在 AWS CloudFormation 中,如果要做跨 Stack 的參照,就必須要把想要參照的對象產成輸出 (output) ,而 AWS CDK 已經自動地幫忙做完這件事情了。


我們成功的將 Stack 給予不同的參數讓他可以被重複利用,也學會了如何將不同 Stack 中的動態值做參照,既然開啟了增加複雜度的可能性,接下來就試著編寫 AWS CDK 的測試。


上一篇
06 - CDK 的架構
系列文
CDK 從 0 開始打造靈活自如的 IaC7
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言