This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
exec-aide/lib/constructs/socket-mode.ts
Adam Moussa 21ad13943c Pin SocketMode image asset to LINUX_ARM64
The task runs ARM64, but the image asset had no explicit platform, so
the amd64 CD runner built an amd64 image (QEMU alone does not change
the default target). Revision :8 crash-looped with "exec format
error" (50 failed task starts); CloudFormation hung on service
stabilization until cancelled. Local arm64 builds masked this -
deploys from this Mac always produced the right image.

Same fix pattern as the org-wide QEMU+platform rule for arm64 Lambda
bundling.
2026-06-03 16:02:39 -04:00

137 lines
4.7 KiB
TypeScript

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as path from 'path';
export interface SocketModeProps {
table: dynamodb.ITable;
conversationFnArn: string;
}
export class SocketModeConstruct extends Construct {
constructor(scope: Construct, id: string, props: SocketModeProps) {
super(scope, id);
const account = cdk.Stack.of(this).account;
const region = cdk.Stack.of(this).region;
// ── VPC ────────────────────────────────────────────────────
const vpc = new ec2.Vpc(this, 'Vpc', {
vpcName: 'exec-aide',
ipAddresses: ec2.IpAddresses.cidr('10.30.0.0/16'),
maxAzs: 1,
natGateways: 0,
subnetConfiguration: [
{
cidrMask: 24,
name: 'exec-aide-listener',
subnetType: ec2.SubnetType.PUBLIC,
},
],
});
const sg = new ec2.SecurityGroup(this, 'ListenerSG', {
vpc,
description: 'exec-aide listener - outbound only',
allowAllOutbound: true,
});
// ── ECR ────────────────────────────────────────────────────
new ecr.Repository(this, 'ListenerRepo', {
repositoryName: 'exec-aide-listener',
imageScanOnPush: true,
lifecycleRules: [
{
maxImageCount: 5,
description: 'Keep last 5 images',
},
],
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// ── ECS Cluster + Task Definition ──────────────────────────
const cluster = new ecs.Cluster(this, 'Cluster', {
clusterName: 'exec-aide',
vpc,
});
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', {
family: 'exec-aide-listener',
cpu: 256,
memoryLimitMiB: 512,
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64,
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
},
});
taskDef.addContainer('listener', {
image: ecs.ContainerImage.fromAsset(
path.join(__dirname, '../../listener'),
// Explicit platform: the task is ARM64, and without this an amd64 CI
// runner builds an amd64 image even with QEMU enabled — tasks then
// crash-loop with "exec format error" (50 failed starts on 2026-06-03).
{ platform: ecrAssets.Platform.LINUX_ARM64 },
),
essential: true,
environment: {
TABLE_NAME: props.table.tableName,
SECRET_SLACK: 'exec-aide/slack-credentials',
SSM_PREFIX: '/exec-aide',
CONVERSATION_FN_ARN: props.conversationFnArn,
},
logging: ecs.LogDrivers.awsLogs({
streamPrefix: 'listener',
logGroup: new logs.LogGroup(this, 'ListenerLogGroup', {
logGroupName: '/ecs/exec-aide-listener',
retention: logs.RetentionDays.TWO_MONTHS,
}),
}),
});
// ── Task role IAM ──────────────────────────────────────────
props.table.grantReadWriteData(taskDef.taskRole);
taskDef.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [
`arn:aws:secretsmanager:${region}:${account}:secret:exec-aide/slack-credentials-*`,
],
}));
taskDef.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['ssm:GetParametersByPath', 'ssm:GetParameter'],
resources: [
`arn:aws:ssm:${region}:${account}:parameter/exec-aide`,
`arn:aws:ssm:${region}:${account}:parameter/exec-aide/*`,
],
}));
taskDef.taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['lambda:InvokeFunction'],
resources: [props.conversationFnArn],
}));
// ── Fargate Service ────────────────────────────────────────
new ecs.FargateService(this, 'Service', {
serviceName: 'exec-aide-listener',
cluster,
taskDefinition: taskDef,
desiredCount: 1,
assignPublicIp: true,
securityGroups: [sg],
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});
}
}