@raindancers/raindancers-crew 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.jsii +1026 -3
- package/API.md +1459 -326
- package/README.md +58 -0
- package/docs/spec-ecs-crew-construct.md +200 -0
- package/lib/crew-backup-bucket.js +1 -1
- package/lib/crew-webhook-ingress.d.ts +104 -0
- package/lib/crew-webhook-ingress.js +111 -0
- package/lib/ecs-crew-host.d.ts +247 -0
- package/lib/ecs-crew-host.js +383 -0
- package/lib/fargate-crew-base.js +6 -2
- package/lib/fargate-crew.js +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -1
- package/lib/remote-crew-instance.js +1 -1
- package/package.json +2 -2
- package/src/assets/ecs-host-bootstrap.sh +205 -0
- package/src/crew-webhook-ingress.ts +175 -0
- package/src/ecs-crew-host.ts +616 -0
- package/src/fargate-crew-base.ts +4 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# KiroCrew ECS-on-EC2 host bootstrap (cloud-init, first boot, root).
|
|
3
|
+
#
|
|
4
|
+
# This host runs the ECS agent (the AMI is ECS-optimized Amazon Linux 2023, so
|
|
5
|
+
# the agent is preinstalled) and does the NAT for the crew tasks, which run in
|
|
6
|
+
# awsvpc mode on their own private ENIs. It joins the cluster, enables IP
|
|
7
|
+
# forwarding + an iptables MASQUERADE rule, and mounts each per-crew durable
|
|
8
|
+
# EBS volume resolved by a STABLE FILESYSTEM LABEL (never /dev/sdf, which the
|
|
9
|
+
# Nitro NVMe layer renames unpredictably).
|
|
10
|
+
#
|
|
11
|
+
# The header the EcsCrewHost construct prepends defines, before this body runs:
|
|
12
|
+
# ECS_CLUSTER name of the cluster to register with
|
|
13
|
+
# CREW_COUNT number of crews (informational)
|
|
14
|
+
# CREW_VOLUME_LABELS space-separated filesystem labels, one per crew volume
|
|
15
|
+
# CREW_MOUNT_PATHS space-separated host mount paths, positionally matched
|
|
16
|
+
# to CREW_VOLUME_LABELS
|
|
17
|
+
#
|
|
18
|
+
# Security-load-bearing choices preserved from the EC2 bootstrap: IMDSv2 is
|
|
19
|
+
# enforced at the instance (construct side), and the bootstrap is resumable
|
|
20
|
+
# across a first-boot reboot via a systemd oneshot.
|
|
21
|
+
set -u
|
|
22
|
+
|
|
23
|
+
BOOTSTRAP_SCRIPT=/usr/local/sbin/kirocrew-ecs-host-bootstrap
|
|
24
|
+
BOOTSTRAP_STATE=/var/lib/kirocrew-ecs-host-bootstrap
|
|
25
|
+
SETUP_DONE=$BOOTSTRAP_STATE/setup-complete
|
|
26
|
+
|
|
27
|
+
# cloud-init runs UserData once. Persist the rendered script and hand off to a
|
|
28
|
+
# systemd oneshot so a managed patch reboot can resume it on boot.
|
|
29
|
+
if [ "$#" -eq 0 ]; then
|
|
30
|
+
install -m 0700 "$0" "$BOOTSTRAP_SCRIPT"
|
|
31
|
+
cat > /etc/systemd/system/kirocrew-ecs-host-bootstrap.service <<'UNIT'
|
|
32
|
+
[Unit]
|
|
33
|
+
Description=Kiro Crew ECS-host resumable bootstrap
|
|
34
|
+
After=network-online.target
|
|
35
|
+
Wants=network-online.target
|
|
36
|
+
|
|
37
|
+
[Service]
|
|
38
|
+
Type=oneshot
|
|
39
|
+
ExecStart=/usr/local/sbin/kirocrew-ecs-host-bootstrap --resume
|
|
40
|
+
RemainAfterExit=yes
|
|
41
|
+
|
|
42
|
+
[Install]
|
|
43
|
+
WantedBy=multi-user.target
|
|
44
|
+
UNIT
|
|
45
|
+
systemctl daemon-reload
|
|
46
|
+
systemctl enable kirocrew-ecs-host-bootstrap.service
|
|
47
|
+
systemctl start --no-block kirocrew-ecs-host-bootstrap.service
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
mkdir -p "$BOOTSTRAP_STATE"
|
|
52
|
+
[ -f "$SETUP_DONE" ] && exit 0
|
|
53
|
+
exec >>/var/log/kirocrew-ecs-host-setup.log 2>&1
|
|
54
|
+
echo "=== KiroCrew ECS-host bootstrap starting $(date -u) ==="
|
|
55
|
+
|
|
56
|
+
# --- 1. Per-crew durable EBS: resolve by LABEL, format on first use only,
|
|
57
|
+
# mount, and persist in fstab keyed by LABEL so a Nitro NVMe rename cannot
|
|
58
|
+
# mount the wrong crew's state.
|
|
59
|
+
echo "--- mounting per-crew data volumes (resolve-by-label) ---"
|
|
60
|
+
# Positional pairing of labels and mount paths.
|
|
61
|
+
read -r -a LABELS <<< "${CREW_VOLUME_LABELS:-}"
|
|
62
|
+
read -r -a MOUNTS <<< "${CREW_MOUNT_PATHS:-}"
|
|
63
|
+
i=0
|
|
64
|
+
while [ "$i" -lt "${#LABELS[@]}" ]; do
|
|
65
|
+
label="${LABELS[$i]}"
|
|
66
|
+
mount_path="${MOUNTS[$i]}"
|
|
67
|
+
i=$((i + 1))
|
|
68
|
+
[ -n "$label" ] || continue
|
|
69
|
+
[ -n "$mount_path" ] || continue
|
|
70
|
+
mkdir -p "$mount_path"
|
|
71
|
+
|
|
72
|
+
# Wait for the raw device to attach. On Nitro the device is /dev/nvmeXn1;
|
|
73
|
+
# match it by the EBS volume mapping rather than a guessed name. We find any
|
|
74
|
+
# unmounted, unformatted-or-labelled block device and label it on first use.
|
|
75
|
+
dev=""
|
|
76
|
+
for _ in $(seq 1 30); do
|
|
77
|
+
# A volume already carrying our label (instance replacement / reboot).
|
|
78
|
+
existing=$(blkid -L "$label" 2>/dev/null || true)
|
|
79
|
+
if [ -n "$existing" ]; then
|
|
80
|
+
dev="$existing"
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
# Otherwise pick a data disk that is not the root device and has no fs yet.
|
|
84
|
+
while read -r cand; do
|
|
85
|
+
[ -n "$cand" ] || continue
|
|
86
|
+
# Skip the root device and any partition/child.
|
|
87
|
+
root_src=$(findmnt -n -o SOURCE / 2>/dev/null | sed 's/[0-9]*p\?[0-9]*$//')
|
|
88
|
+
case "/dev/$cand" in
|
|
89
|
+
"$root_src"*) continue ;;
|
|
90
|
+
esac
|
|
91
|
+
# Skip anything already carrying a filesystem or mounted.
|
|
92
|
+
if [ -z "$(blkid "/dev/$cand" 2>/dev/null)" ] && ! findmnt -S "/dev/$cand" >/dev/null 2>&1; then
|
|
93
|
+
dev="/dev/$cand"
|
|
94
|
+
break
|
|
95
|
+
fi
|
|
96
|
+
done < <(lsblk -dn -o NAME 2>/dev/null)
|
|
97
|
+
[ -n "$dev" ] && break
|
|
98
|
+
sleep 5
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
if [ -z "$dev" ]; then
|
|
102
|
+
echo "WARNING: could not resolve a device for crew volume label '$label'; skipping"
|
|
103
|
+
continue
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Format only if the device has no filesystem yet (first provision). An
|
|
107
|
+
# already-labelled device (reattached) is never reformatted.
|
|
108
|
+
if [ -z "$(blkid "$dev" 2>/dev/null)" ]; then
|
|
109
|
+
echo "formatting $dev as ext4 with label '$label' (first provision)"
|
|
110
|
+
mkfs.ext4 -F -L "$label" "$dev" || { echo "WARNING: mkfs failed for $dev"; continue; }
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Persist by LABEL (stable), not by device name (renamed on Nitro).
|
|
114
|
+
if ! grep -q "LABEL=$label " /etc/fstab; then
|
|
115
|
+
echo "LABEL=$label $mount_path ext4 defaults,nofail 0 2" >> /etc/fstab
|
|
116
|
+
fi
|
|
117
|
+
mount "$mount_path" 2>/dev/null || mount -L "$label" "$mount_path" || \
|
|
118
|
+
echo "WARNING: could not mount label '$label' at $mount_path"
|
|
119
|
+
echo "crew volume '$label' -> $mount_path"
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
# --- 2. Host NAT for the crew task ENIs: enable IP forwarding and MASQUERADE
|
|
123
|
+
# egress out of the primary interface. The construct disables source/dest check
|
|
124
|
+
# on the host ENI so the kernel may forward packets not addressed to the host.
|
|
125
|
+
echo "--- enabling host NAT (ip_forward + iptables MASQUERADE) ---"
|
|
126
|
+
cat > /etc/sysctl.d/99-kirocrew-nat.conf <<'SYSCTL'
|
|
127
|
+
net.ipv4.ip_forward = 1
|
|
128
|
+
SYSCTL
|
|
129
|
+
sysctl --system >/dev/null 2>&1 || sysctl -w net.ipv4.ip_forward=1 || true
|
|
130
|
+
|
|
131
|
+
# Resolve the primary egress interface (the one carrying the default route).
|
|
132
|
+
EGRESS_IF=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
|
|
133
|
+
[ -n "$EGRESS_IF" ] || EGRESS_IF=$(ls /sys/class/net | grep -E '^(eth0|ens|enp)' | head -1)
|
|
134
|
+
echo "egress interface: ${EGRESS_IF:-unknown}"
|
|
135
|
+
|
|
136
|
+
if [ -n "$EGRESS_IF" ]; then
|
|
137
|
+
# Idempotent: only add the MASQUERADE rule if it is not already present.
|
|
138
|
+
if ! iptables -t nat -C POSTROUTING -o "$EGRESS_IF" -j MASQUERADE 2>/dev/null; then
|
|
139
|
+
iptables -t nat -A POSTROUTING -o "$EGRESS_IF" -j MASQUERADE || true
|
|
140
|
+
fi
|
|
141
|
+
# Persist the rules across reboot. iptables-services provides the save path;
|
|
142
|
+
# fall back to a boot-time restore unit if the package is unavailable.
|
|
143
|
+
if command -v iptables-save >/dev/null 2>&1; then
|
|
144
|
+
mkdir -p /etc/kirocrew
|
|
145
|
+
iptables-save > /etc/kirocrew/nat.rules || true
|
|
146
|
+
cat > /etc/systemd/system/kirocrew-nat-restore.service <<'UNIT'
|
|
147
|
+
[Unit]
|
|
148
|
+
Description=Restore KiroCrew host NAT iptables rules
|
|
149
|
+
After=network-online.target
|
|
150
|
+
Wants=network-online.target
|
|
151
|
+
Before=ecs.service
|
|
152
|
+
|
|
153
|
+
[Service]
|
|
154
|
+
Type=oneshot
|
|
155
|
+
ExecStart=/bin/sh -c '/sbin/iptables-restore < /etc/kirocrew/nat.rules'
|
|
156
|
+
RemainAfterExit=yes
|
|
157
|
+
|
|
158
|
+
[Install]
|
|
159
|
+
WantedBy=multi-user.target
|
|
160
|
+
UNIT
|
|
161
|
+
systemctl daemon-reload
|
|
162
|
+
systemctl enable kirocrew-nat-restore.service || true
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# --- 2b. Disable source/dest check on THIS instance so the kernel forwards
|
|
167
|
+
# packets not addressed to the host (the ENI-level half of NAT; ip_forward is
|
|
168
|
+
# the kernel half). CloudFormation cannot set this on an ASG-launched instance,
|
|
169
|
+
# so the host does it to itself via IMDSv2 identity + modify-instance-attribute
|
|
170
|
+
# (the host role is scoped to KiroCrew ECS hosts by a tag condition).
|
|
171
|
+
echo "--- disabling source/dest check on the host ENI ---"
|
|
172
|
+
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
|
|
173
|
+
-H "X-aws-ec2-metadata-token-ttl-seconds: 300" 2>/dev/null || true)
|
|
174
|
+
IID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
|
|
175
|
+
http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || true)
|
|
176
|
+
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
|
|
177
|
+
http://169.254.169.254/latest/meta-data/placement/availability-zone 2>/dev/null || true)
|
|
178
|
+
REGION="${AZ%[a-z]}"
|
|
179
|
+
if [ -n "$IID" ] && [ -n "$REGION" ]; then
|
|
180
|
+
aws ec2 modify-instance-attribute --no-source-dest-check \
|
|
181
|
+
--instance-id "$IID" --region "$REGION" \
|
|
182
|
+
|| echo "WARNING: could not disable source/dest check (NAT forwarding may drop task egress)"
|
|
183
|
+
else
|
|
184
|
+
echo "WARNING: could not resolve instance-id/region from IMDS; source/dest check left unchanged"
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# --- 3. Join the ECS cluster: write /etc/ecs/ecs.config with the cluster name
|
|
188
|
+
# and enable awsvpc task networking (ENI trunking raises the per-instance task
|
|
189
|
+
# ENI budget on smaller sizes). The ECS agent (preinstalled on the ECS AMI)
|
|
190
|
+
# reads this on start.
|
|
191
|
+
echo "--- joining ECS cluster '$ECS_CLUSTER' ---"
|
|
192
|
+
mkdir -p /etc/ecs
|
|
193
|
+
# Append only the keys we own; do not clobber an AMI-provided config.
|
|
194
|
+
{
|
|
195
|
+
echo "ECS_CLUSTER=$ECS_CLUSTER"
|
|
196
|
+
echo "ECS_ENABLE_TASK_ENI=true"
|
|
197
|
+
echo "ECS_AWSVPC_BLOCK_IMDS=true"
|
|
198
|
+
echo "ECS_ENABLE_AWSVPC_TRUNKING=true"
|
|
199
|
+
} >> /etc/ecs/ecs.config
|
|
200
|
+
|
|
201
|
+
# Start (or restart) the agent so it picks up the config.
|
|
202
|
+
systemctl enable --now ecs.service 2>/dev/null || systemctl restart ecs 2>/dev/null || true
|
|
203
|
+
|
|
204
|
+
echo "=== KiroCrew ECS-host bootstrap complete $(date -u) ==="
|
|
205
|
+
touch "$SETUP_DONE"
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Duration } from 'aws-cdk-lib';
|
|
2
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
3
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
4
|
+
import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
5
|
+
import * as sns from 'aws-cdk-lib/aws-sns';
|
|
6
|
+
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';
|
|
7
|
+
import { Construct } from 'constructs';
|
|
8
|
+
import { EcsCrewHost } from './ecs-crew-host';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CREW_PORT = 5476;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Properties for {@link CrewWebhookIngress}.
|
|
14
|
+
*/
|
|
15
|
+
export interface CrewWebhookIngressProps {
|
|
16
|
+
/**
|
|
17
|
+
* VPC to place the ingress Lambda in. Must be the same VPC as the crew tasks
|
|
18
|
+
* so the Lambda can reach a task's private ENI IP directly with NO VPC
|
|
19
|
+
* endpoint.
|
|
20
|
+
*/
|
|
21
|
+
readonly vpc: ec2.IVpc;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The crew host whose tasks receive webhooks. The task security group is
|
|
25
|
+
* granted a scoped inbound rule from this ingress Lambda's SG on the crew
|
|
26
|
+
* port only — the one controlled inbound exception to the egress-only model.
|
|
27
|
+
*/
|
|
28
|
+
readonly host: EcsCrewHost;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ARN of an IAM permissions boundary applied to the Lambda's execution role.
|
|
32
|
+
* The ingress Lambda runs consumer-triggered code, so it carries a boundary
|
|
33
|
+
* matching the rest of the construct.
|
|
34
|
+
*/
|
|
35
|
+
readonly permissionsBoundaryArn: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The code the ingress Lambda runs. The consumer supplies it: this construct
|
|
39
|
+
* wires the plumbing (SNS -> in-VPC Lambda -> task private IP) but does not
|
|
40
|
+
* ship an opinion about how a webhook payload maps to a crew.
|
|
41
|
+
*/
|
|
42
|
+
readonly code: lambda.Code;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Lambda handler entry point.
|
|
46
|
+
*
|
|
47
|
+
* @default 'index.handler'
|
|
48
|
+
*/
|
|
49
|
+
readonly handler?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The Lambda runtime. Must be an arm64-compatible runtime to match the
|
|
53
|
+
* arm64 default host.
|
|
54
|
+
*
|
|
55
|
+
* @default lambda.Runtime.NODEJS_20_X
|
|
56
|
+
*/
|
|
57
|
+
readonly runtime?: lambda.Runtime;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lambda architecture. Defaults to arm64 to match the Graviton host.
|
|
61
|
+
*
|
|
62
|
+
* @default lambda.Architecture.ARM_64
|
|
63
|
+
*/
|
|
64
|
+
readonly architecture?: lambda.Architecture;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subnets to place the ingress Lambda's ENIs in. Should be the same private
|
|
68
|
+
* subnet the crew tasks run in so the Lambda reaches task IPs directly.
|
|
69
|
+
*
|
|
70
|
+
* @default - the VPC's private-with-egress subnets
|
|
71
|
+
*/
|
|
72
|
+
readonly vpcSubnets?: ec2.SubnetSelection;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* TCP port on the crew task the Lambda reaches (the gateway/webhook port).
|
|
76
|
+
*
|
|
77
|
+
* @default 5476
|
|
78
|
+
*/
|
|
79
|
+
readonly crewPort?: number;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* An existing SNS topic to subscribe the Lambda to. Omit to create one.
|
|
83
|
+
*
|
|
84
|
+
* @default - a new topic is created
|
|
85
|
+
*/
|
|
86
|
+
readonly topic?: sns.ITopic;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Lambda timeout.
|
|
90
|
+
*
|
|
91
|
+
* @default Duration.seconds(30)
|
|
92
|
+
*/
|
|
93
|
+
readonly timeout?: Duration;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* OPTIONAL, composable webhook-ingress path for {@link EcsCrewHost}, mirroring
|
|
98
|
+
* the optional {@link CrewBackupBucket} shape. Nothing is created unless the
|
|
99
|
+
* consumer instantiates it, so an existing consumer gains no new resources.
|
|
100
|
+
*
|
|
101
|
+
* Because each crew task runs in awsvpc mode with a real private VPC IP, an
|
|
102
|
+
* in-VPC Lambda can POST to a crew's `POST /api/hooks/agent` DIRECTLY — no VPC
|
|
103
|
+
* endpoint, no PrivateLink, no ALB. This construct wires: an SNS topic, an
|
|
104
|
+
* in-VPC Lambda under the permissions boundary, and a scoped ingress rule on
|
|
105
|
+
* the crew task security group from the Lambda's SG on the crew port ONLY.
|
|
106
|
+
* That ingress rule is the single controlled inbound exception to the
|
|
107
|
+
* egress-only task model.
|
|
108
|
+
*
|
|
109
|
+
* Greenfield: the construct wires the plumbing but ships no webhook-to-crew
|
|
110
|
+
* routing opinion — the consumer supplies the Lambda {@link CrewWebhookIngressProps.code}.
|
|
111
|
+
*/
|
|
112
|
+
export class CrewWebhookIngress extends Construct {
|
|
113
|
+
/** The SNS topic that fans events into the ingress Lambda. */
|
|
114
|
+
public readonly topic: sns.ITopic;
|
|
115
|
+
/** The in-VPC ingress Lambda. */
|
|
116
|
+
public readonly function: lambda.Function;
|
|
117
|
+
/** The ingress Lambda's security group (the source of the scoped task ingress rule). */
|
|
118
|
+
public readonly securityGroup: ec2.SecurityGroup;
|
|
119
|
+
|
|
120
|
+
constructor(scope: Construct, id: string, props: CrewWebhookIngressProps) {
|
|
121
|
+
super(scope, id);
|
|
122
|
+
|
|
123
|
+
const crewPort = props.crewPort ?? DEFAULT_CREW_PORT;
|
|
124
|
+
|
|
125
|
+
this.securityGroup = new ec2.SecurityGroup(this, 'IngressLambdaSg', {
|
|
126
|
+
vpc: props.vpc,
|
|
127
|
+
description: 'KiroCrew webhook-ingress Lambda - reaches crew task private IPs',
|
|
128
|
+
allowAllOutbound: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const executionRole = new iam.Role(this, 'IngressLambdaRole', {
|
|
132
|
+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
|
|
133
|
+
permissionsBoundary: iam.ManagedPolicy.fromManagedPolicyArn(
|
|
134
|
+
this,
|
|
135
|
+
'Boundary',
|
|
136
|
+
props.permissionsBoundaryArn,
|
|
137
|
+
),
|
|
138
|
+
managedPolicies: [
|
|
139
|
+
// The Lambda runs in a VPC, so it needs the ENI-management actions.
|
|
140
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
|
141
|
+
'service-role/AWSLambdaVPCAccessExecutionRole',
|
|
142
|
+
),
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
this.function = new lambda.Function(this, 'IngressLambda', {
|
|
147
|
+
runtime: props.runtime ?? lambda.Runtime.NODEJS_20_X,
|
|
148
|
+
architecture: props.architecture ?? lambda.Architecture.ARM_64,
|
|
149
|
+
handler: props.handler ?? 'index.handler',
|
|
150
|
+
code: props.code,
|
|
151
|
+
role: executionRole,
|
|
152
|
+
vpc: props.vpc,
|
|
153
|
+
vpcSubnets: props.vpcSubnets ?? {
|
|
154
|
+
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
|
155
|
+
},
|
|
156
|
+
securityGroups: [this.securityGroup],
|
|
157
|
+
timeout: props.timeout ?? Duration.seconds(30),
|
|
158
|
+
environment: {
|
|
159
|
+
CREW_PORT: String(crewPort),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.topic = props.topic ?? new sns.Topic(this, 'IngressTopic');
|
|
164
|
+
this.topic.addSubscription(new subs.LambdaSubscription(this.function));
|
|
165
|
+
|
|
166
|
+
// The one controlled inbound exception to the egress-only task model: the
|
|
167
|
+
// crew task SG accepts the crew port FROM the ingress Lambda SG only, never
|
|
168
|
+
// a CIDR. Documented in the README.
|
|
169
|
+
props.host.base.securityGroup.addIngressRule(
|
|
170
|
+
ec2.Peer.securityGroupId(this.securityGroup.securityGroupId),
|
|
171
|
+
ec2.Port.tcp(crewPort),
|
|
172
|
+
'Webhook ingress from the in-VPC ingress Lambda SG only (no CIDR)',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|