Kaip įdiegti „Grafto“ naudojant „Pulumi“: trumpas vadovas
Dėl klientų darbo, kai turėjau konsoliduoti jų infrastruktūrą AWS, man kilo klausimas, ar turėčiau naudoti IaC įrankį ir, jei taip, kokį? Apie sprendimo priėmimo procesą šiek tiek parašiau čia. Nuolat bandydamas atskleisti savo sprendimus ir sprendimus realiame pasaulyje, maniau, kad būtų įdomu sužinoti, kaip būtų galima priimti „Grafto“ prieglobą naudojant „Pulumi“ AWS. „Grafto“ yra mažas mano pradinio šablono projektas, kuris yra konteineriuose, todėl naudotis tokiomis paslaugomis kaip ECS ir Fargate tampa paprasta.
Tai gana naivus požiūris, kuriame neišnaudojama daug „Pulumi“ privalumų, pavyzdžiui, leidžiant mums naudoti dizaino modelius kuriant infrastruktūrą. Bet, tikėkimės, turėtų parodyti infrastruktūros naudą kaip kodą kode, kurį iš tikrųjų skaitote ir rašote kiekvieną dieną.
Kodo siena
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6
7 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2"
8 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ecs"
9 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam"
10 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lb"
11 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/rds"
12 "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
13)
14
15func main() {
16 pulumi.Run(func(ctx *pulumi.Context) error {
17 availabilityZones := []string{"us-east-1a", "us-east-1b"}
18
19 // VPC
20 vpc, err := ec2.NewVpc(ctx, "grafto-vpc", &ec2.VpcArgs{
21 CidrBlock: pulumi.String("10.0.0.0/16"),
22 EnableDnsHostnames: pulumi.Bool(true),
23 EnableDnsSupport: pulumi.Bool(true),
24 })
25 if err != nil {
26 return err
27 }
28
29 startingSubnetCidrRange := "10.0.0.0/20"
30
31 // SUBNETS
32 subnets := make(map[string][]*ec2.Subnet, len(availabilityZones))
33 for i, az := range availabilityZones {
34 var cidrRangePublic string
35 var cidrRangePrivate string
36 if i == 0 {
37 cidrRangePublic = startingSubnetCidrRange
38 cidrRangePrivate = fmt.Sprintf("10.0.%v.0/20", 16)
39 } else {
40 cidrRangePublic = fmt.Sprintf("10.0.%v.0/20", 16*(i+1))
41 cidrRangePrivate = fmt.Sprintf("10.0.%v.0/20", 16*(i+2))
42 }
43
44 publicSubnet, err := ec2.NewSubnet(
45 ctx,
46 fmt.Sprintf("grafto-%s-subnet-%v", "public", i+1),
47 &ec2.SubnetArgs{
48 VpcId: vpc.ID(),
49 CidrBlock: pulumi.String(cidrRangePublic),
50 AvailabilityZone: pulumi.String(az),
51 },
52 )
53 if err != nil {
54 return err
55 }
56
57 subnets["public"] = append(subnets["public"], publicSubnet)
58
59 privateSubnet, err := ec2.NewSubnet(
60 ctx,
61 fmt.Sprintf("grafto-%s-subnet-%v", "private", i+1),
62 &ec2.SubnetArgs{
63 VpcId: vpc.ID(),
64 CidrBlock: pulumi.String(cidrRangePrivate),
65 AvailabilityZone: pulumi.String(az),
66 },
67 )
68 if err != nil {
69 return err
70 }
71
72 subnets["private"] = append(subnets["private"], privateSubnet)
73 }
74
75 // INTERNET GATEWAY
76 internetGateway, err := ec2.NewInternetGateway(
77 ctx,
78 "grafto-internet-gateway",
79 &ec2.InternetGatewayArgs{
80 VpcId: vpc.ID(),
81 },
82 )
83 if err != nil {
84 return err
85 }
86
87 publicRouteTable, err := ec2.NewRouteTable(
88 ctx,
89 "grafto-public-route-table",
90 &ec2.RouteTableArgs{
91 VpcId: vpc.ID(),
92 },
93 )
94 if err != nil {
95 return err
96 }
97
98 _, err = ec2.NewRoute(ctx, "grafto-public-route", &ec2.RouteArgs{
99 DestinationCidrBlock: pulumi.String("0.0.0.0/0"),
100 GatewayId: internetGateway.ID(),
101 RouteTableId: publicRouteTable.ID(),
102 })
103 if err != nil {
104 return err
105 }
106
107 _, err = ec2.NewRouteTableAssociation(
108 ctx,
109 "grafto-public-route-ass-1",
110 &ec2.RouteTableAssociationArgs{
111 RouteTableId: publicRouteTable.ID(),
112 SubnetId: subnets["public"][0].ID(),
113 },
114 )
115 if err != nil {
116 return err
117 }
118
119 _, err = ec2.NewRouteTableAssociation(
120 ctx,
121 "grafto-public-route-ass-2",
122 &ec2.RouteTableAssociationArgs{
123 RouteTableId: publicRouteTable.ID(),
124 SubnetId: subnets["public"][1].ID(),
125 },
126 )
127 if err != nil {
128 return err
129 }
130
131 // NATGATEWAY
132 elasticIP, err := ec2.NewEip(ctx, "grafto-elastic-ip", &ec2.EipArgs{})
133 if err != nil {
134 return err
135 }
136
137 natGateway, err := ec2.NewNatGateway(ctx, "grafto-nat-gateway", &ec2.NatGatewayArgs{
138 AllocationId: elasticIP.ID(),
139 SubnetId: subnets["public"][0].ID(),
140 })
141 if err != nil {
142 return err
143 }
144
145 privateRouteTable, err := ec2.NewRouteTable(
146 ctx,
147 "grafto-private-route-table",
148 &ec2.RouteTableArgs{
149 VpcId: vpc.ID(),
150 },
151 )
152 if err != nil {
153 return err
154 }
155
156 _, err = ec2.NewRoute(ctx, "grafto-private-route", &ec2.RouteArgs{
157 DestinationCidrBlock: pulumi.String("0.0.0.0/0"),
158 NatGatewayId: natGateway.ID(),
159 RouteTableId: privateRouteTable.ID(),
160 })
161 if err != nil {
162 return err
163 }
164
165 _, err = ec2.NewRouteTableAssociation(
166 ctx,
167 "grafto-private-route-ass-1",
168 &ec2.RouteTableAssociationArgs{
169 RouteTableId: privateRouteTable.ID(),
170 SubnetId: subnets["private"][0].ID(),
171 },
172 )
173 if err != nil {
174 return err
175 }
176
177 _, err = ec2.NewRouteTableAssociation(
178 ctx,
179 "grafto-private-route-ass-2",
180 &ec2.RouteTableAssociationArgs{
181 RouteTableId: privateRouteTable.ID(),
182 SubnetId: subnets["private"][1].ID(),
183 },
184 )
185 if err != nil {
186 return err
187 }
188
189 // SECURITY GROUP
190 applicationLoadBalancer, err := ec2.NewSecurityGroup(
191 ctx,
192 "grafto-alb-sg",
193 &ec2.SecurityGroupArgs{
194 VpcId: vpc.ID(),
195 Ingress: ec2.SecurityGroupIngressArray{
196 &ec2.SecurityGroupIngressArgs{
197 CidrBlocks: pulumi.StringArray{
198 pulumi.String("0.0.0.0/0"),
199 },
200 FromPort: pulumi.Int(80),
201 ToPort: pulumi.Int(80),
202 Protocol: pulumi.String("tcp"),
203 },
204 },
205 Egress: ec2.SecurityGroupEgressArray{
206 &ec2.SecurityGroupEgressArgs{
207 CidrBlocks: pulumi.StringArray{
208 pulumi.String("0.0.0.0/0"),
209 },
210 FromPort: pulumi.Int(0),
211 ToPort: pulumi.Int(0),
212 Protocol: pulumi.String("-1"),
213 },
214 },
215 },
216 )
217 if err != nil {
218 return err
219 }
220
221 ecsSG, err := ec2.NewSecurityGroup(
222 ctx,
223 "grafto-ecs-sg",
224 &ec2.SecurityGroupArgs{
225 VpcId: vpc.ID(),
226 Ingress: ec2.SecurityGroupIngressArray{
227 &ec2.SecurityGroupIngressArgs{
228 CidrBlocks: pulumi.StringArray{
229 pulumi.String("0.0.0.0/0"),
230 },
231 FromPort: pulumi.Int(0),
232 ToPort: pulumi.Int(0),
233 Protocol: pulumi.String("-1"),
234 },
235 },
236 Egress: ec2.SecurityGroupEgressArray{
237 &ec2.SecurityGroupEgressArgs{
238 CidrBlocks: pulumi.StringArray{
239 pulumi.String("0.0.0.0/0"),
240 },
241 FromPort: pulumi.Int(0),
242 ToPort: pulumi.Int(0),
243 Protocol: pulumi.String("-1"),
244 },
245 },
246 },
247 )
248 if err != nil {
249 return err
250 }
251
252 rdsSGG, err := ec2.NewSecurityGroup(
253 ctx,
254 "grafto-rds-sgg",
255 &ec2.SecurityGroupArgs{
256 VpcId: vpc.ID(),
257 Ingress: ec2.SecurityGroupIngressArray{
258 &ec2.SecurityGroupIngressArgs{
259 CidrBlocks: pulumi.StringArray{
260 pulumi.String("0.0.0.0/0"),
261 },
262 FromPort: pulumi.Int(0),
263 ToPort: pulumi.Int(0),
264 Protocol: pulumi.String("-1"),
265 },
266 },
267 Egress: ec2.SecurityGroupEgressArray{
268 &ec2.SecurityGroupEgressArgs{
269 CidrBlocks: pulumi.StringArray{
270 pulumi.String("0.0.0.0/0"),
271 },
272 FromPort: pulumi.Int(0),
273 ToPort: pulumi.Int(0),
274 Protocol: pulumi.String("-1"),
275 },
276 },
277 },
278 )
279 if err != nil {
280 return err
281 }
282
283 rdsSg, err := rds.NewSubnetGroup(ctx, "grafto-rds-sg", &rds.SubnetGroupArgs{
284 SubnetIds: pulumi.StringArray{
285 subnets["private"][0].ID(),
286 subnets["private"][1].ID(),
287 },
288 })
289 if err != nil {
290 return err
291 }
292
293 database, err := rds.NewInstance(ctx, "grafto-rds-psql", &rds.InstanceArgs{
294 AllocatedStorage: pulumi.Int(10),
295 DbName: pulumi.String("grafto"),
296 Password: pulumi.String("password"),
297 Username: pulumi.String("grafto"),
298 Engine: pulumi.String("postgres"),
299 EngineVersion: pulumi.String("16.3"),
300 InstanceClass: pulumi.String("db.t3.micro"),
301 ParameterGroupName: pulumi.String("default.postgres16"),
302 DbSubnetGroupName: rdsSg.Name,
303 VpcSecurityGroupIds: pulumi.StringArray{
304 rdsSGG.ID(),
305 },
306 SkipFinalSnapshot: pulumi.Bool(true),
307 PubliclyAccessible: pulumi.Bool(false),
308 })
309 if err != nil {
310 return err
311 }
312
313 loadBalancer, err := lb.NewLoadBalancer(ctx, "grafto-load-balancer", &lb.LoadBalancerArgs{
314 Internal: pulumi.Bool(false),
315 LoadBalancerType: pulumi.String("application"),
316 SecurityGroups: pulumi.StringArray{
317 applicationLoadBalancer.ID(),
318 },
319 Subnets: pulumi.StringArray{
320 subnets["public"][0].ID(),
321 subnets["public"][1].ID(),
322 },
323 EnableDeletionProtection: pulumi.Bool(false),
324 })
325 if err != nil {
326 return err
327 }
328 ctx.Export("url", pulumi.Sprintf("http://%s", loadBalancer.DnsName))
329
330 targetGroup, err := lb.NewTargetGroup(ctx, "grafto-alb-target-group", &lb.TargetGroupArgs{
331 HealthCheck: &lb.TargetGroupHealthCheckArgs{
332 Path: pulumi.String("/api/health"),
333 Protocol: pulumi.String("HTTP"),
334 },
335 Name: pulumi.String("grafto-app-tg"),
336 Port: pulumi.Int(80),
337 Protocol: pulumi.String("HTTP"),
338 TargetType: pulumi.String("ip"),
339 VpcId: vpc.ID(),
340 })
341 if err != nil {
342 return err
343 }
344
345 _, err = lb.NewListener(ctx, "grafto-alb-listener", &lb.ListenerArgs{
346 DefaultActions: lb.ListenerDefaultActionArray{
347 lb.ListenerDefaultActionArgs{
348 TargetGroupArn: targetGroup.Arn,
349 Type: pulumi.String("forward"),
350 },
351 },
352 LoadBalancerArn: loadBalancer.Arn,
353 Port: pulumi.Int(80),
354 Protocol: pulumi.String("HTTP"),
355 })
356 if err != nil {
357 return err
358 }
359
360 // IAM RELATED STUFF
361 _, err = iam.NewServiceLinkedRole(
362 ctx,
363 "elastic-container-service",
364 &iam.ServiceLinkedRoleArgs{
365 AwsServiceName: pulumi.String("ecs.amazonaws.com"),
366 Description: pulumi.String("Role to enable Amazon ECS to manage your cluster."),
367 },
368 )
369 if err != nil {
370 return err
371 }
372
373 _, err = iam.NewServiceLinkedRole(ctx, "rds", &iam.ServiceLinkedRoleArgs{
374 AwsServiceName: pulumi.String("rds.amazonaws.com"),
375 Description: pulumi.String("Role to enable Amazon RDS to manage your cluster."),
376 })
377 if err != nil {
378 return err
379 }
380
381 _, err = iam.NewServiceLinkedRole(ctx, "elastic-load-balancer", &iam.ServiceLinkedRoleArgs{
382 AwsServiceName: pulumi.String("elasticloadbalancing.amazonaws.com"),
383 Description: pulumi.String("Allows ELB to call AWS services on your behalf"),
384 })
385 if err != nil {
386 return err
387 }
388
389 _, err = iam.NewServiceLinkedRole(
390 ctx,
391 "application-autoscaling",
392 &iam.ServiceLinkedRoleArgs{
393 AwsServiceName: pulumi.String("ecs.application-autoscaling.amazonaws.com"),
394 Description: pulumi.String(
395 "Allows application autoscaling to call AWS services on your behalf",
396 ),
397 },
398 )
399 if err != nil {
400 return err
401 }
402
403 roleJson, err := json.Marshal(map[string]interface{}{
404 "Version": "2012-10-17",
405 "Statement": []map[string]interface{}{
406 {
407 "Action": []string{
408 "sts:AssumeRole",
409 },
410 "Principal": map[string]string{"Service": "ecs-tasks.amazonaws.com"},
411 "Effect": "Allow",
412 },
413 },
414 })
415 if err != nil {
416 return err
417 }
418 role, err := iam.NewRole(ctx, "grafto-iam-role", &iam.RoleArgs{
419 Name: pulumi.String("grafto-iam-role"),
420 AssumeRolePolicy: pulumi.String(string(roleJson)),
421 })
422 if err != nil {
423 return err
424 }
425
426 rolePolicyJson, err := json.Marshal(map[string]interface{}{
427 "Version": "2012-10-17",
428 "Statement": []map[string]interface{}{
429 {
430 "Action": []string{
431 "ecr:*",
432 },
433 "Effect": "Allow",
434 "Resource": "*",
435 },
436 },
437 })
438 if err != nil {
439 return err
440 }
441 _, err = iam.NewRolePolicy(ctx, "grafto-iam-role-policy", &iam.RolePolicyArgs{
442 Name: pulumi.String("grafto-iam-role"),
443 Role: role.Name,
444 Policy: pulumi.String(string(rolePolicyJson)),
445 })
446 if err != nil {
447 return err
448 }
449
450 // ELASTIC CONTAINER SERVICE
451 cluster, err := ecs.NewCluster(ctx, "grafto-ecs-cluster", &ecs.ClusterArgs{
452 Name: pulumi.String("grafto"),
453 })
454 if err != nil {
455 return err
456 }
457
458 taskContainerDefinition := pulumi.JSONMarshal([]map[string]interface{}{
459 {
460 "name": "grafto-task",
461 "image": "docker.io/mbvofdocker/grafto:pulumi-blog",
462 "portMappings": []map[string]interface{}{
463 {
464 "containerPort": 8080,
465 "hostPort": 8080,
466 "protocol": "HTTP",
467 },
468 },
469 "essential": true,
470 "command": []string{"./app"},
471 "environment": []map[string]interface{}{
472 {
473 "name": "ENVIRONMENT",
474 "value": "production",
475 },
476 {
477 "name": "SERVER_HOST",
478 "value": "0.0.0.0",
479 },
480 {
481 "name": "SERVER_PORT",
482 "value": "8080",
483 },
484 {
485 "name": "DEFAULT_SENDER_SIGNATURE",
486 "value": "[email protected]",
487 },
488 {
489 "name": "POSTMARK_API_TOKEN",
490 "value": "insert-valid-token-here",
491 },
492 {
493 "name": "DB_KIND",
494 "value": "postgres",
495 },
496 {
497 "name": "DB_PORT",
498 "value": "5432",
499 },
500 {
501 "name": "DB_HOST",
502 "value": database.Address.ApplyT(
503 func(addr string) string {
504 return addr
505 },
506 ).(pulumi.StringOutput),
507 },
508 {
509 "name": "DB_NAME",
510 "value": database.DbName.ApplyT(
511 func(name string) string {
512 return name
513 },
514 ).(pulumi.StringOutput),
515 },
516 {
517 "name": "DB_USER",
518 "value": database.Username.ApplyT(
519 func(name string) string {
520 return name
521 },
522 ).(pulumi.StringOutput),
523 },
524 {
525 "name": "DB_PASSWORD",
526 "value": database.Password.ApplyT(
527 func(pass *string) string {
528 return *pass
529 },
530 ).(pulumi.StringOutput),
531 },
532 {
533 "name": "DB_SSL_MODE",
534 "value": "require",
535 },
536 {
537 "name": "PASSWORD_PEPPER",
538 "value": "lotsandlotsofrandomcharshere",
539 },
540 {
541 "name": "PROJECT_NAME",
542 "value": "Pulumi Grafto BLog Post",
543 },
544 {
545 "name": "APP_HOST",
546 "value": loadBalancer.DnsName.ApplyT(func(url string) string {
547 return url
548 }),
549 },
550 {
551 "name": "APP_SCHEME",
552 "value": "http",
553 },
554 {
555 "name": "CSRF_TOKEN",
556 "value": "lotsandlotsofrandomcharshere",
557 },
558 {
559 "name": "SESSION_KEY",
560 "value": "lotsandlotsofrandomcharshere",
561 },
562 {
563 "name": "SESSION_ENCRYPTION_KEY",
564 "value": "lotsandlotsofrandomcharshere",
565 },
566 {
567 "name": "TOKEN_SIGNING_KEY",
568 "value": "lotsandlotsofrandomcharshere",
569 },
570 },
571 },
572 })
573 taskDefinition, err := ecs.NewTaskDefinition(ctx, "grafto-task", &ecs.TaskDefinitionArgs{
574 ContainerDefinitions: taskContainerDefinition,
575 Cpu: pulumi.String("256"),
576 ExecutionRoleArn: role.Arn,
577 Family: pulumi.String("grafto"),
578 Memory: pulumi.String("512"),
579 NetworkMode: pulumi.String("awsvpc"),
580 TaskRoleArn: role.Arn,
581 })
582 if err != nil {
583 return err
584 }
585
586 _, err = ecs.NewService(ctx, "grafto-service", &ecs.ServiceArgs{
587 Cluster: cluster.Arn,
588 DeploymentMaximumPercent: pulumi.IntPtr(200),
589 DeploymentMinimumHealthyPercent: pulumi.IntPtr(50),
590 DesiredCount: pulumi.IntPtr(1),
591 ForceNewDeployment: pulumi.Bool(true),
592 LoadBalancers: ecs.ServiceLoadBalancerArray{
593 &ecs.ServiceLoadBalancerArgs{
594 TargetGroupArn: targetGroup.Arn,
595 ContainerName: pulumi.String("grafto-task"),
596 ContainerPort: pulumi.Int(8080),
597 },
598 },
599 NetworkConfiguration: ecs.ServiceNetworkConfigurationArgs{
600 Subnets: pulumi.StringArray{
601 subnets["private"][0].ID(),
602 subnets["private"][1].ID(),
603 },
604 SecurityGroups: pulumi.StringArray{
605 ecsSG.ID(),
606 },
607 },
608 Name: pulumi.String("grafto-ecs-service"),
609 LaunchType: pulumi.String("FARGATE"),
610 PlatformVersion: pulumi.String("1.4.0"),
611 TaskDefinition: taskDefinition.Arn,
612 })
613 if err != nil {
614 return err
615 }
616
617 return nil
618 })
619}
Patobulinimai
Akivaizdus pirmiau pateikto patobulinimas būtų įjungti HTTPS; jei patikrinsite apkrovos balansavimo priemonės saugos grupę, pamatysite, kad leidžiame įvesties srautą per 80 prievadą. Tai yra vienintelis įėjimo taškas, nes visos mūsų Fargate užduotys yra privačiuose tinkluose, todėl pridėjus sertifikatą jis būtų apribotas iki 443 prievado. ilgas kelias.
Pažvelkite į CIDR diapazonų skaičiavimus. Jei pridėsime per daug pasiekiamumo zonų, tai nepavyks, o tai taip pat yra kažkas, ką reikia tvarkyti. Gali būti paprastas patikrinimas, kiek AZ reikia, ir apriboti jį iki tam tikro lygio, bet vis tiek turėtų būti pataisytas.
Taip pat būtų naudinga saugoti aplinkos kintamuosius kažkur, pavyzdžiui, AWS parametrų saugykloje, o ne tiesiogiai kode.
Tikriausiai taip pat pastebėjote daugybę galimybių pakartotinai naudoti kodą per sąrankos funkcijas arba, šiuo atveju, mano asmeninį mėgstamiausią, statybininkus.
Kūrėjai gali labai supaprastinti kodą, ypač jei padidėja ECS paslaugos užduočių skaičius. Būsimame straipsnyje mes tai patobulinsime, kad galėtume lengvai išplėsti savo infrastruktūrą.
Įdomus palyginimas būtų padaryti tą patį su „Terraform“ ir pamatyti, kiek jie skiriasi, ir ar pastangos, kad infrastruktūros kodas būtų pakartotinai naudojamas naudojant skirtingus dizaino modelius, būtų prasmingos.
Bet kol kas ir viskas. Laimingas įsilaužimas!