Unverified Commit 064467c9 authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Enable deploying a minimal set of resources for running the canary (#1306)

parent 102f3042
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -7,3 +7,4 @@ build
# CDK asset staging directory
.cdk.staging
cdk.out
cdk-outputs.json
+24 −0
Original line number Diff line number Diff line
@@ -5,6 +5,30 @@ continuous integration.

The `cdk.json` file tells the CDK Toolkit how to synthesize the infrastructure.

## Canary local development

Sometimes it's useful to only deploy the the canary resources to a test AWS account to iterate
on the `canary-runner` and `canary-lambda`. To do this, run the following:

```bash
npm install
npm run build
npx cdk --app "node build/bin/canary-only.js" synth
npx cdk --app "node build/bin/canary-only.js" deploy --outputs-file cdk-outputs.json
```

From there, you can just point the `canary-runner` to the `cdk-outputs.json` to run it:

```bash
cd canary-runner
cargo run -- --sdk-version <version> --musl --cdk-outputs ../cdk-outputs.json
```

__NOTE:__ You may want to add a `--profile` to the deploy command to select a specific credential
profile to deploy to if you don't want to use the default.

Also, if this is a new test AWS account, be sure it CDK bootstrap it before attempting to deploy.

## Useful commands

-   `npm run lint`: lint code
+16 −0
Original line number Diff line number Diff line
#!/usr/bin/env node
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

// This CDK app sets up the absolute minimum set of resources to succesfully
// execute the canary with.

import "source-map-support/register";
import { App } from "aws-cdk-lib";
import { CanaryStack } from "../lib/aws-sdk-rust/canary-stack";

const app = new App();

new CanaryStack(app, "aws-sdk-rust-canary-stack", {});
+77 −17
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ use clap::Parser;
use cloudwatch::model::StandardUnit;
use s3::ByteStream;
use semver::Version;
use serde::Deserialize;
use smithy_rs_tool_common::git;
use smithy_rs_tool_common::macros::here;
use smithy_rs_tool_common::shell::ShellOperation;
@@ -62,23 +63,82 @@ pub struct RunOpt {
    #[clap(long)]
    musl: bool,

    /// The name of the S3 bucket to upload the canary binary bundle to
    /// File path to a CDK outputs JSON file. This can be used instead
    /// of all the --lambda... args.
    #[clap(long)]
    lambda_code_s3_bucket_name: String,
    cdk_output: Option<PathBuf>,

    /// The name of the S3 bucket to upload the canary binary bundle to
    #[clap(long, required_unless_present = "cdk-output")]
    lambda_code_s3_bucket_name: Option<String>,

    /// The name of the S3 bucket for the canary Lambda to interact with
    #[clap(long)]
    lambda_test_s3_bucket_name: String,
    #[clap(long, required_unless_present = "cdk-output")]
    lambda_test_s3_bucket_name: Option<String>,

    /// The ARN of the role that the Lambda will execute as
    #[clap(long)]
    #[clap(long, required_unless_present = "cdk-output")]
    lambda_execution_role_arn: Option<String>,
}

#[derive(Debug)]
struct Options {
    sdk_version: Option<String>,
    sdk_path: Option<PathBuf>,
    musl: bool,
    lambda_code_s3_bucket_name: String,
    lambda_test_s3_bucket_name: String,
    lambda_execution_role_arn: String,
}

impl Options {
    fn load_from(run_opt: RunOpt) -> Result<Options> {
        if let Some(cdk_output) = &run_opt.cdk_output {
            #[derive(Deserialize)]
            struct Inner {
                #[serde(rename = "canarycodebucketname")]
                lambda_code_s3_bucket_name: String,
                #[serde(rename = "canarytestbucketname")]
                lambda_test_s3_bucket_name: String,
                #[serde(rename = "lambdaexecutionrolearn")]
                lambda_execution_role_arn: String,
            }
            #[derive(Deserialize)]
            struct Outer {
                #[serde(rename = "aws-sdk-rust-canary-stack")]
                inner: Inner,
            }

            let value: Outer = serde_json::from_reader(
                std::fs::File::open(cdk_output).context("open cdk output")?,
            )
            .context("read cdk output")?;
            Ok(Options {
                sdk_version: run_opt.sdk_version,
                sdk_path: run_opt.sdk_path,
                musl: run_opt.musl,
                lambda_code_s3_bucket_name: value.inner.lambda_code_s3_bucket_name,
                lambda_test_s3_bucket_name: value.inner.lambda_test_s3_bucket_name,
                lambda_execution_role_arn: value.inner.lambda_execution_role_arn,
            })
        } else {
            Ok(Options {
                sdk_version: run_opt.sdk_version,
                sdk_path: run_opt.sdk_path,
                musl: run_opt.musl,
                lambda_code_s3_bucket_name: run_opt.lambda_code_s3_bucket_name.expect("required"),
                lambda_test_s3_bucket_name: run_opt.lambda_test_s3_bucket_name.expect("required"),
                lambda_execution_role_arn: run_opt.lambda_execution_role_arn.expect("required"),
            })
        }
    }
}

pub async fn run(opt: RunOpt) -> Result<()> {
    let options = Options::load_from(opt)?;
    let start_time = SystemTime::now();
    let config = aws_config::load_from_env().await;
    let result = run_canary(opt, &config).await;
    let result = run_canary(&options, &config).await;

    let mut metrics = vec![
        (
@@ -129,19 +189,19 @@ pub async fn run(opt: RunOpt) -> Result<()> {
    result.map(|_| ())
}

async fn run_canary(opt: RunOpt, config: &aws_config::Config) -> Result<Duration> {
async fn run_canary(options: &Options, config: &aws_config::Config) -> Result<Duration> {
    let repo_root = git_root().await?;
    env::set_current_dir(repo_root.join("tools/ci-cdk/canary-lambda"))
        .context("failed to change working directory")?;

    if let Some(sdk_version) = &opt.sdk_version {
    if let Some(sdk_version) = &options.sdk_version {
        use_correct_revision(sdk_version)
            .await
            .context(here!("failed to select correct revision of smithy-rs"))?;
    }

    info!("Building the canary...");
    let bundle_path = build_bundle(&opt).await?;
    let bundle_path = build_bundle(options).await?;
    let bundle_file_name = bundle_path.file_name().unwrap().to_str().unwrap();
    let bundle_name = bundle_path.file_stem().unwrap().to_str().unwrap();

@@ -151,7 +211,7 @@ async fn run_canary(opt: RunOpt, config: &aws_config::Config) -> Result<Duration
    info!("Uploading Lambda code bundle to S3...");
    upload_bundle(
        s3_client,
        &opt.lambda_code_s3_bucket_name,
        &options.lambda_code_s3_bucket_name,
        bundle_file_name,
        &bundle_path,
    )
@@ -166,9 +226,9 @@ async fn run_canary(opt: RunOpt, config: &aws_config::Config) -> Result<Duration
        lambda_client.clone(),
        bundle_name,
        bundle_file_name,
        &opt.lambda_execution_role_arn,
        &opt.lambda_code_s3_bucket_name,
        &opt.lambda_test_s3_bucket_name,
        &options.lambda_execution_role_arn,
        &options.lambda_code_s3_bucket_name,
        &options.lambda_test_s3_bucket_name,
    )
    .await
    .context(here!())?;
@@ -208,15 +268,15 @@ async fn use_correct_revision(sdk_version: &str) -> Result<()> {
}

/// Returns the path to the compiled bundle zip file
async fn build_bundle(opt: &RunOpt) -> Result<PathBuf> {
async fn build_bundle(options: &Options) -> Result<PathBuf> {
    let mut builder = Command::new("./build-bundle");
    if let Some(sdk_version) = &opt.sdk_version {
    if let Some(sdk_version) = &options.sdk_version {
        builder.arg("--sdk-version").arg(sdk_version);
    }
    if let Some(sdk_path) = &opt.sdk_path {
    if let Some(sdk_path) = &options.sdk_path {
        builder.arg("--sdk-path").arg(sdk_path);
    }
    if opt.musl {
    if options.musl {
        builder.arg("--musl");
    }
    let output = builder
+79 −48
Original line number Diff line number Diff line
@@ -11,26 +11,31 @@ import {
    ServicePrincipal,
} from "aws-cdk-lib/aws-iam";
import { BlockPublicAccess, Bucket, BucketEncryption } from "aws-cdk-lib/aws-s3";
import { StackProps, Stack, Tags, RemovalPolicy, Duration } from "aws-cdk-lib";
import { StackProps, Stack, Tags, RemovalPolicy, Duration, CfnOutput } from "aws-cdk-lib";
import { Construct } from "constructs";
import { GitHubOidcRole } from "../constructs/github-oidc-role";

export interface Properties extends StackProps {
    githubActionsOidcProvider: OpenIdConnectProvider;
    githubActionsOidcProvider?: OpenIdConnectProvider;
}

export class CanaryStack extends Stack {
    public readonly awsSdkRustOidcRole: GitHubOidcRole;
    public readonly awsSdkRustOidcRole?: GitHubOidcRole;
    public readonly lambdaExecutionRole: Role;
    public readonly canaryCodeBucket: Bucket;
    public readonly canaryTestBucket: Bucket;

    public readonly lambdaExecutionRoleArn: CfnOutput;
    public readonly canaryCodeBucketName: CfnOutput;
    public readonly canaryTestBucketName: CfnOutput;

    constructor(scope: Construct, id: string, props: Properties) {
        super(scope, id, props);

        // Tag the resources created by this stack to make identifying resources easier
        Tags.of(this).add("stack", id);

        if (props.githubActionsOidcProvider) {
            this.awsSdkRustOidcRole = new GitHubOidcRole(this, "aws-sdk-rust", {
                name: "aws-sdk-rust-canary",
                githubOrg: "awslabs",
@@ -61,6 +66,7 @@ export class CanaryStack extends Stack {
                    resources: ["*"],
                }),
            );
        }

        // Create S3 bucket to upload canary Lambda code into
        this.canaryCodeBucket = new Bucket(this, "canary-code-bucket", {
@@ -77,9 +83,18 @@ export class CanaryStack extends Stack {
            removalPolicy: RemovalPolicy.DESTROY,
        });

        // Output the bucket name to make it easier to invoke the canary runner
        this.canaryCodeBucketName = new CfnOutput(this, "canary-code-bucket-name", {
            value: this.canaryCodeBucket.bucketName,
            description: "Name of the canary code bucket",
            exportName: "canaryCodeBucket",
        });

        // Allow the OIDC role to GetObject and PutObject to the code bucket
        if (this.awsSdkRustOidcRole) {
            this.canaryCodeBucket.grantRead(this.awsSdkRustOidcRole.oidcRole);
            this.canaryCodeBucket.grantWrite(this.awsSdkRustOidcRole.oidcRole);
        }

        // Create S3 bucket for the canaries to talk to
        this.canaryTestBucket = new Bucket(this, "canary-test-bucket", {
@@ -96,12 +111,26 @@ export class CanaryStack extends Stack {
            removalPolicy: RemovalPolicy.DESTROY,
        });

        // Output the bucket name to make it easier to invoke the canary runner
        this.canaryTestBucketName = new CfnOutput(this, "canary-test-bucket-name", {
            value: this.canaryTestBucket.bucketName,
            description: "Name of the canary test bucket",
            exportName: "canaryTestBucket",
        });

        // Create a role for the canary Lambdas to assume
        this.lambdaExecutionRole = new Role(this, "lambda-execution-role", {
            roleName: "aws-sdk-rust-canary-lambda-exec-role",
            assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
        });

        // Output the Lambda execution role ARN to make it easier to invoke the canary runner
        this.lambdaExecutionRoleArn = new CfnOutput(this, "lambda-execution-role-arn", {
            value: this.lambdaExecutionRole.roleArn,
            description: "Canary Lambda execution role ARN",
            exportName: "canaryLambdaExecutionRoleArn",
        });

        // Allow canaries to write logs to CloudWatch
        this.lambdaExecutionRole.addToPolicy(
            new PolicyStatement({
@@ -124,6 +153,7 @@ export class CanaryStack extends Stack {
        );

        // Allow the OIDC role to pass the Lambda execution role to Lambda
        if (this.awsSdkRustOidcRole) {
            this.awsSdkRustOidcRole.oidcRole.addToPolicy(
                new PolicyStatement({
                    actions: ["iam:PassRole"],
@@ -140,3 +170,4 @@ export class CanaryStack extends Stack {
            );
        }
    }
}