Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made it possible to deploy multiple instances into single AWS account by adding prefix to each deploys #9

Merged
merged 2 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 118 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ Query parameters
* tag_name: Tag name in metadata.
* coverage_type: SORACOM coverage type. "g" or "jp".

Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracomAirMetadataSource=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}"
```

* deploySoracomAirMetadataSource - A flag to deploy a component
*
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.

### SoraCam(Soracom Cloud Camera Service)

A source webhook which uploads SorCam image to Soracom Harvest Files.
Expand Down Expand Up @@ -61,6 +77,23 @@ You will need SAM user with permissions below
}
```

Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracamImageSourceSink=1 \
--context harvestFilesPath="${HARVEST_FILES_DIR_PATH}" \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}"
```

* deploySoracamImageSourceSink - A flag to deploy a component
*
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.

### SORACOM Harvest Data

A source webhook which returns a series of data entries from Soracom Harvest Data.
Expand All @@ -77,6 +110,21 @@ Query parameters
* resource_id: If you are using SORACOM SIM, please specify your SIM's IMSI.
* coverage_type: SORACOM coverage type. "g" or "jp".

Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracomHarvestDataSource=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}"
```

* deploySoracomHarvestDataSource - A flag to deploy a component
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.

## Sinks

Sinks are outlet components to send data from Soracom Flux.
Expand All @@ -102,6 +150,21 @@ Query parameters
* tag_name: Tag name in metadata.
* coverage_type: SORACOM coverage type. "g" or "jp".

Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracomAirMetadataSink=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}"
```

* deploySoracomAirMetadataSink - A flag to deploy a component
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.

### SORACOM SMS API

This sink sends a SMS with posted text.
Expand All @@ -120,6 +183,22 @@ curl \
Query parameters
* sim_id: Target SIM ID

Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracomAirSmsSink=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}"
```

* deploySoracomAirSmsSink - A flag to deploy a component
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.


### Phone call

This sink makes a phone call with posted text.
Expand All @@ -138,20 +217,26 @@ curl \
Query Parameters
* to_number: URL encoded phone number with country code. For example, when you call to U.S. number, you have to start with '%2B1' while %2B represents '+' in url safe characters, which means '+1'.


### AWS IoT Core(To be implemented)

This sink publish a message to AWS IoT Core with posted topic.
Deploy

```
curl \
-XPOST \
-H 'Content-Type:application/json' \
-H "x-api-key:${apikey}" \
-d '{"topic":"${topic}","${message}"} \
https://${hostname}/v1/sink/aws_iot_core
npm run installAll
npm run build
npx cdk deploy \
--context deployPhoneCallSink=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}" \
--context twilioSecretname="${AWS_SECRETS_MANAGER_NAME_FOR_TWILIO}"
```

* deployPhoneCallSink - A flag to deploy a component
* twilioSecretname - A secret name of AWS Secrets Manager. The value should be like
```
{"accountSid":"YOUR TWILIO ACCOUNT SID STARTING FROM 'AC'","authToken":"YOUR AUTH TOKEN","myPhoneNumber":"YOUR FROM PHONE NUMBER"}
```
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.

### Google Sheets

This sink emits a row to Google Spreadsheet. Each rows should be consists of ordered items like CSV.
Expand All @@ -173,26 +258,39 @@ Query parameters
* sheed_id: Google Sheet ID. It is included in the URL of your sheet.
* sheet_name: Sheet name in the spread sheet.

## Deploy
Deploy

```
npm run installAll
npm run build
npx cdk deploy \
--context deploySoracomAirMetadataSink=1 \
--context deploySoracomAirMetadataSource=1 \
--context deploySoracomAirSmsSink=1 \
--context deploySoracomHarvestDataSource=1 \
--context deploySoracamImageSource=1 \
--context deployGoogleSheetsSink=1 \
--context deployPhoneCallSink=1 \
--context soracomAuthKeyId="${SORACOM_AUTH_KEY_ID}" \
--context soracomAuthKey="${SORACOM_AUTH_KEY}" \
--context harvestFilesPath="${HARVEST_FILES_DIR_PATH}" \
--context googleSecretname="${AWS_SECRETS_MANAGER_NAME_FOR_GOOGLESHEETS}" \
--context twilioSecretname="${AWS_SECRETS_MANAGER_NAME_FOR_TWILIO}"
--context googleSecretname="${AWS_SECRETS_MANAGER_NAME_FOR_GOOGLE}"
```

* deployGoogleSheetsSink - A flag to deploy a component
* googleSecretname - A secret name of AWS Secrets Manager. The value should be like below. It's a Goole service account's credentials.
```
{
"type": "service_account",
"project_id": "factory-personal",
"private_key_id": "xxxx",
"private_key": "-----BEGIN PRIVATE KEY-----\nxxxx----\n",
"client_email": "xxxx",
"client_id": "xxxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/xxx",
"universe_domain": "googleapis.com"
}
```
* soracomAuthKeyId - Your Soracom credentials AuthKeyId. It is required everywhere as of now.
* soracomAuthKey - Your Soracom credentials AuthKeyId. It is required everywhere as of now.


### You will need IAM permission below

```
Expand Down
9 changes: 8 additions & 1 deletion bin/flux-toys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { FluxToysStack, FluxToysStackProps } from "../lib/flux-toys-stack";

const app = new cdk.App();
Expand Down Expand Up @@ -39,4 +41,9 @@ const props: FluxToysStackProps = {
: false,
};

new FluxToysStack(app, "FluxToysStack", props);
const stackName = app.node.tryGetContext("stackName") + "-flux-toys-stack";

console.log("Stack Name: ", stackName);
console.log("Props: ", props);

new FluxToysStack(app, stackName, props);
11 changes: 7 additions & 4 deletions lambda/soracam-image-source.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from "axios";
import dayjs from "dayjs";
import path from "path";
import { getSoracomClient, setGetSoracomClient } from "./lib/utils";
export { setGetSoracomClient };

Expand Down Expand Up @@ -47,12 +48,14 @@ export const handler = async (event: any = {}): Promise<any> => {
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
let path = `${harvestfilesPath}/${uploadDirectory}/${deviceId}-${dayjs().format(
"YYYYMMDDHHmmss"
)}.jpg`;
let filePath = path.join(
harvestfilesPath,
uploadDirectory,
`${deviceId}-${dayjs().format("YYYYMMDDHHmmss")}.jpg`
);

try {
await soracomClient.putFile(path, buffer, "image/jpeg");
await soracomClient.putFile(filePath, buffer, "image/jpeg");
} catch (e) {
console.error(e);
return {
Expand Down
15 changes: 15 additions & 0 deletions lib/base-construct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Construct, IConstruct } from "constructs";

export abstract class BaseConstruct extends Construct implements IConstruct {
constructor(scope: Construct, id: string) {
super(scope, id);
}

protected addPrefixToId(id: string): string {
return `${this.node.id}-${id}`;
}

protected functionName(): string {
return `${this.node.id}-function`;
}
}
116 changes: 96 additions & 20 deletions lib/flux-toys-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,109 @@ import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { FluxToysStack, FluxToysStackProps } from "../lib/flux-toys-stack";

test("FluxToys Stack Test", () => {
const app = new cdk.App();

const props: FluxToysStackProps = {
harvestFilesPath: "/test/path",
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
googleSecretname: "testGoogleSecret",
deploySoracamImageSource: true,
describe("FluxToysStack", () => {
const createStack = (props: FluxToysStackProps) => {
const app = new cdk.App();
return new FluxToysStack(app, "TestStack", props);
};

const stack = new FluxToysStack(app, "TestStack", props);
test("should create SoracamImageSourceConstruct when deploySoracamImageSource is true", () => {
const props: FluxToysStackProps = {
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
harvestFilesPath: "/test/path",
deploySoracamImageSource: true,
};

const template = Template.fromStack(stack);
const stack = createStack(props);
const template = Template.fromStack(stack);

// API Gatewayのリソースが作成されていることを確認
template.hasResourceProperties("AWS::ApiGateway::RestApi", {
Name: "FluxToysCollection",
template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "soracam-image-source.handler",
Runtime: "nodejs20.x",
Environment: {
Variables: {
HARVEST_FILES_PATH: "/test/path",
},
},
});
});

// API Gatewayのキーが作成されていることを確認
template.hasResourceProperties("AWS::ApiGateway::ApiKey", {
Name: "FluxToysCollectionApiKey",
test("should create PhoneCallSinkConstruct when deployPhoneCallSink is true", () => {
const props: FluxToysStackProps = {
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
twilioSecretname: "testTwilioSecret",
deployPhoneCallSink: true,
};

const stack = createStack(props);
const template = Template.fromStack(stack);

template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "phone-call-sink.handler",
FunctionName: "TestStack-phone-call-sink-function",
Runtime: "nodejs20.x",
Environment: {
Variables: {
TWILIO_SECRET_NAME: "testTwilioSecret",
},
},
});

template.hasResourceProperties("AWS::SecretsManager::Secret", {
Name: "TestStack-soracom-api-credentials",
});
});

test("should create GoogleSheetsSinkConstruct when deployGoogleSheetsSink is true", () => {
const props: FluxToysStackProps = {
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
googleSecretname: "testGoogleSecret",
deployGoogleSheetsSink: true,
};

const stack = createStack(props);
const template = Template.fromStack(stack);

template.hasResourceProperties("AWS::Lambda::Function", {
Handler: "google-sheets-sink.handler",
FunctionName: "TestStack-google-sheets-sink-function",
Runtime: "nodejs20.x",
Environment: {
Variables: {
GOOGLE_SECRET_NAME: "testGoogleSecret",
},
},
});

template.hasResourceProperties("AWS::SecretsManager::Secret", {
Name: "TestStack-soracom-api-credentials",
});
});

test("should throw error when deploySoracamImageSource is true but harvestFilesPath is not provided", () => {
const props: FluxToysStackProps = {
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
deploySoracamImageSource: true,
};

expect(() => createStack(props)).toThrow(
"harvestFilesPath is required for SoracamImageSourceConstruct"
);
});

// Secrets Managerのシークレットが作成されていることを確認
template.hasResourceProperties("AWS::SecretsManager::Secret", {
Name: "SoracomAPICredentials",
test("should throw error when deployPhoneCallSink is true but twilioSecretname is not provided", () => {
const props: FluxToysStackProps = {
soracomAuthKeyId: "testKeyId",
soracomAuthKey: "testKey",
deployPhoneCallSink: true,
};

expect(() => createStack(props)).toThrow(
"twilioSecretname is required for PhoneCallSinkConstruct"
);
});
});
Loading