|
| 1 | +// Copyright 2016-2025, Pulumi Corporation. All rights reserved. |
| 2 | + |
| 3 | +import * as aws from "@pulumi/aws"; |
| 4 | +import * as awsx from "@pulumi/awsx"; |
| 5 | +import * as pulumi from "@pulumi/pulumi"; |
| 6 | + |
| 7 | + |
| 8 | +const config = new pulumi.Config(); |
| 9 | +const projectName = "zookeeper"; |
| 10 | +const environment = config.require("environment"); |
| 11 | + |
| 12 | +// VPC Configuration |
| 13 | +const vpc = new awsx.ec2.Vpc(`${projectName}-vpc`, { |
| 14 | + numberOfAvailabilityZones: 3, |
| 15 | + natGateways: { |
| 16 | + strategy: environment === "production" ? "OnePerAz" : "Single", |
| 17 | + }, |
| 18 | + tags: { |
| 19 | + Name: `${projectName}-vpc`, |
| 20 | + Environment: environment, |
| 21 | + }, |
| 22 | +}); |
| 23 | + |
| 24 | +// Security Groups |
| 25 | +const zookeeperSG = new aws.ec2.SecurityGroup(`${projectName}-sg`, { |
| 26 | + vpcId: vpc.vpcId, |
| 27 | + description: "Security group for ZooKeeper nodes", |
| 28 | + ingress: [ |
| 29 | + { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["10.0.0.0/8"] }, // SSH |
| 30 | + { protocol: "tcp", fromPort: 2181, toPort: 2181, cidrBlocks: ["10.0.0.0/8"] }, // Client port |
| 31 | + { protocol: "tcp", fromPort: 2888, toPort: 2888, cidrBlocks: ["10.0.0.0/8"] }, // Follower port |
| 32 | + { protocol: "tcp", fromPort: 3888, toPort: 3888, cidrBlocks: ["10.0.0.0/8"] }, // Election port |
| 33 | + ], |
| 34 | + egress: [{ |
| 35 | + protocol: "-1", |
| 36 | + fromPort: 0, |
| 37 | + toPort: 0, |
| 38 | + cidrBlocks: ["0.0.0.0/0"], |
| 39 | + }], |
| 40 | + tags: { |
| 41 | + Name: `${projectName}-sg`, |
| 42 | + Environment: environment, |
| 43 | + }, |
| 44 | +}); |
| 45 | + |
| 46 | +// IAM Role and Instance Profile |
| 47 | +const zookeeperRole = new aws.iam.Role(`${projectName}-role`, { |
| 48 | + assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ |
| 49 | + Service: "ec2.amazonaws.com", |
| 50 | + }), |
| 51 | +}); |
| 52 | + |
| 53 | +const zookeeperInstanceProfile = new aws.iam.InstanceProfile(`${projectName}-instance-profile`, { |
| 54 | + role: zookeeperRole.name, |
| 55 | +}); |
| 56 | + |
| 57 | +// SSM Policy Attachment |
| 58 | +const ssmPolicy = new aws.iam.RolePolicyAttachment(`${projectName}-ssm-policy`, { |
| 59 | + role: zookeeperRole.name, |
| 60 | + policyArn: "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore", |
| 61 | +}); |
| 62 | + |
| 63 | +// CloudWatch Policy |
| 64 | +const cloudwatchPolicy = new aws.iam.RolePolicy(`${projectName}-cloudwatch-policy`, { |
| 65 | + role: zookeeperRole.name, |
| 66 | + policy: JSON.stringify({ |
| 67 | + Version: "2012-10-17", |
| 68 | + Statement: [{ |
| 69 | + Effect: "Allow", |
| 70 | + Action: [ |
| 71 | + "cloudwatch:PutMetricData", |
| 72 | + "cloudwatch:GetMetricData", |
| 73 | + "cloudwatch:ListMetrics", |
| 74 | + ], |
| 75 | + Resource: "*", |
| 76 | + }], |
| 77 | + }), |
| 78 | +}); |
| 79 | + |
| 80 | +// Launch Template User Data Script |
| 81 | +const getUserData = (id: number) => `#!/bin/bash |
| 82 | +apt-get update |
| 83 | +apt-get install -y openjdk-11-jdk |
| 84 | +
|
| 85 | +# Install ZooKeeper |
| 86 | +ZOOKEEPER_VERSION="3.9.3" |
| 87 | +wget https://dlcdn.apache.org/zookeeper/zookeeper-$ZOOKEEPER_VERSION/apache-zookeeper-$ZOOKEEPER_VERSION-bin.tar.gz |
| 88 | +tar -xzf apache-zookeeper-$ZOOKEEPER_VERSION-bin.tar.gz |
| 89 | +mv apache-zookeeper-$ZOOKEEPER_VERSION-bin /opt/zookeeper |
| 90 | +
|
| 91 | +# Configure ZooKeeper |
| 92 | +cat > /opt/zookeeper/conf/zoo.cfg << EOF |
| 93 | +tickTime=2000 |
| 94 | +initLimit=10 |
| 95 | +syncLimit=5 |
| 96 | +dataDir=/var/lib/zookeeper |
| 97 | +clientPort=2181 |
| 98 | +server.1=zk1.internal:2888:3888 |
| 99 | +server.2=zk2.internal:2888:3888 |
| 100 | +server.3=zk3.internal:2888:3888 |
| 101 | +EOF |
| 102 | +
|
| 103 | +mkdir -p /var/lib/zookeeper |
| 104 | +echo "${id}" > /var/lib/zookeeper/myid |
| 105 | +
|
| 106 | +# Create systemd service |
| 107 | +cat > /etc/systemd/system/zookeeper.service << EOF |
| 108 | +[Unit] |
| 109 | +Description=ZooKeeper Service |
| 110 | +After=network.target |
| 111 | +
|
| 112 | +[Service] |
| 113 | +Type=forking |
| 114 | +User=root |
| 115 | +Group=root |
| 116 | +ExecStart=/opt/zookeeper/bin/zkServer.sh start |
| 117 | +ExecStop=/opt/zookeeper/bin/zkServer.sh stop |
| 118 | +ExecReload=/opt/zookeeper/bin/zkServer.sh restart |
| 119 | +WorkingDirectory=/opt/zookeeper |
| 120 | +
|
| 121 | +[Install] |
| 122 | +WantedBy=multi-user.target |
| 123 | +EOF |
| 124 | +
|
| 125 | +# Start ZooKeeper |
| 126 | +systemctl daemon-reload |
| 127 | +systemctl enable zookeeper |
| 128 | +systemctl start zookeeper |
| 129 | +
|
| 130 | +# Install CloudWatch agent |
| 131 | +wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb |
| 132 | +dpkg -i amazon-cloudwatch-agent.deb |
| 133 | +
|
| 134 | +# Configure CloudWatch agent |
| 135 | +cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json << EOF |
| 136 | +{ |
| 137 | + "metrics": { |
| 138 | + "metrics_collected": { |
| 139 | + "mem": { |
| 140 | + "measurement": ["mem_used_percent"] |
| 141 | + }, |
| 142 | + "disk": { |
| 143 | + "measurement": ["disk_used_percent"], |
| 144 | + "resources": ["/"] |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +EOF |
| 150 | +
|
| 151 | +systemctl enable amazon-cloudwatch-agent |
| 152 | +systemctl start amazon-cloudwatch-agent`; |
| 153 | + |
| 154 | +// Launch Template |
| 155 | +const launchTemplate = new aws.ec2.LaunchTemplate(`${projectName}-launch-template`, { |
| 156 | + namePrefix: `${projectName}-`, |
| 157 | + imageId: "ami-0c7217cdde317cfec", // Ubuntu 24.04 LTS AMI ID |
| 158 | + instanceType: "t3.medium", |
| 159 | + userData: Buffer.from(getUserData(1)).toString("base64"), |
| 160 | + vpcSecurityGroupIds: [zookeeperSG.id], |
| 161 | + iamInstanceProfile: { |
| 162 | + name: zookeeperInstanceProfile.name, |
| 163 | + }, |
| 164 | + blockDeviceMappings: [{ |
| 165 | + deviceName: "/dev/sda1", |
| 166 | + ebs: { |
| 167 | + volumeSize: 50, |
| 168 | + volumeType: "gp3", |
| 169 | + deleteOnTermination: "true", |
| 170 | + encrypted: "true", |
| 171 | + }, |
| 172 | + }], |
| 173 | + tags: { |
| 174 | + Name: `${projectName}-launch-template`, |
| 175 | + Environment: environment, |
| 176 | + }, |
| 177 | + metadataOptions: { |
| 178 | + httpEndpoint: "enabled", |
| 179 | + httpTokens: "required", |
| 180 | + httpPutResponseHopLimit: 1, |
| 181 | + }, |
| 182 | +}); |
| 183 | + |
| 184 | +// Auto Scaling Group |
| 185 | +const asg = new aws.autoscaling.Group(`${projectName}-asg`, { |
| 186 | + vpcZoneIdentifiers: vpc.privateSubnetIds, |
| 187 | + desiredCapacity: 3, |
| 188 | + maxSize: 3, |
| 189 | + minSize: 3, |
| 190 | + healthCheckType: "ELB", |
| 191 | + healthCheckGracePeriod: 300, |
| 192 | + launchTemplate: { |
| 193 | + id: launchTemplate.id, |
| 194 | + version: "$Latest", |
| 195 | + }, |
| 196 | + tags: [{ |
| 197 | + key: "Name", |
| 198 | + value: `${projectName}-node`, |
| 199 | + propagateAtLaunch: true, |
| 200 | + }, { |
| 201 | + key: "Environment", |
| 202 | + value: environment, |
| 203 | + propagateAtLaunch: true, |
| 204 | + }], |
| 205 | +}); |
| 206 | + |
| 207 | +// Application Load Balancer |
| 208 | +const alb = new aws.lb.LoadBalancer(`${projectName}-alb`, { |
| 209 | + internal: true, |
| 210 | + loadBalancerType: "application", |
| 211 | + securityGroups: [zookeeperSG.id], |
| 212 | + subnets: vpc.privateSubnetIds, |
| 213 | + tags: { |
| 214 | + Name: `${projectName}-alb`, |
| 215 | + Environment: environment, |
| 216 | + }, |
| 217 | +}); |
| 218 | + |
| 219 | +// Target Group |
| 220 | +const targetGroup = new aws.lb.TargetGroup(`${projectName}-tg`, { |
| 221 | + port: 2181, |
| 222 | + protocol: "HTTP", |
| 223 | + vpcId: vpc.vpcId, |
| 224 | + targetType: "instance", |
| 225 | + healthCheck: { |
| 226 | + enabled: true, |
| 227 | + path: "/", |
| 228 | + port: "2181", |
| 229 | + protocol: "HTTP", |
| 230 | + healthyThreshold: 2, |
| 231 | + unhealthyThreshold: 3, |
| 232 | + timeout: 5, |
| 233 | + interval: 30, |
| 234 | + }, |
| 235 | + tags: { |
| 236 | + Name: `${projectName}-tg`, |
| 237 | + Environment: environment, |
| 238 | + }, |
| 239 | +}); |
| 240 | + |
| 241 | +// ALB Listener |
| 242 | +const listener = new aws.lb.Listener(`${projectName}-listener`, { |
| 243 | + loadBalancerArn: alb.arn, |
| 244 | + port: 2181, |
| 245 | + protocol: "HTTP", |
| 246 | + defaultActions: [{ |
| 247 | + type: "forward", |
| 248 | + targetGroupArn: targetGroup.arn, |
| 249 | + }], |
| 250 | +}); |
| 251 | + |
| 252 | +// Attach ASG to Target Group |
| 253 | +const asgAttachment = new aws.autoscaling.Attachment(`${projectName}-asg-attachment`, { |
| 254 | + autoscalingGroupName: asg.name, |
| 255 | + lbTargetGroupArn: targetGroup.arn, |
| 256 | +}); |
| 257 | + |
| 258 | +// CloudWatch Alarms |
| 259 | +const cpuAlarm = new aws.cloudwatch.MetricAlarm(`${projectName}-cpu-alarm`, { |
| 260 | + comparisonOperator: "GreaterThanThreshold", |
| 261 | + evaluationPeriods: 2, |
| 262 | + metricName: "CPUUtilization", |
| 263 | + namespace: "AWS/EC2", |
| 264 | + period: 300, |
| 265 | + statistic: "Average", |
| 266 | + threshold: 80, |
| 267 | + alarmDescription: "This metric monitors ec2 cpu utilization", |
| 268 | + dimensions: { |
| 269 | + AutoScalingGroupName: asg.name, |
| 270 | + }, |
| 271 | +}); |
| 272 | + |
| 273 | +// Export values |
| 274 | +export const vpcId = vpc.vpcId; |
| 275 | +export const albDnsName = alb.dnsName; |
| 276 | +export const asgName = asg.name; |
0 commit comments