Bài viết thuộc series “Chinh phục CDK”
Giới thiệu
Trong bài này chúng ta sẽ tìm hiểu sâu hơn về Stack và cách sử dụng tài nguyên ở một Stack này cho các Stack khác.
Stack
Như ta đã tìm hiểu ở các bài trước, Stack là tập hợp các tài nguyên có liên quan với nhau, mục đích của Stack là ta giúp dễ dàng trong việc quản lý và cấu trúc mã nguồn.
Trong một ứng dụng AWS CDK có thể có một hay nhiều Stack, ví dụ:
app := awscdk.NewApp(nil)
MyFirstStack(app, "stack1")
MySecondStack(app, "stack2")
app.Synth(nil)
Để liệt kê toàn bộ Stack ta sử dụng câu lệnh cdk ls
:
stack1
stack2
Khi ta chạy câu lệnh synth
để tạo AWS CloudFormation cho ứng dụng nhiều Stack, truyền tên của Stack vào:
cdk synth stack1
Với AWS CDK và Stack ta có thể dễ dàng cấu trúc các tài nguyên cần thiết cho các môi trường khác nhau. Ví dụ ta có một ứng dụng và cần triển khai hạ tầng cho môi trường dev
và prod
. Hạ tầng của ta gồm ba Stack sau: Application Stack, Monitoring Stack và CI/CD Stack.
Ở môi trường dev
thông thường ta sẽ không cần Monitoring Stack để tiết kiệm chi phí. Để tạo được hạ tầng cho hai môi trường với yêu cầu trên khá dễ dàng trong CDK. Ví dụ như sau, ta tạo 3 hàm cho 3 Stack:
package main
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/constructs-go/constructs/v10"
)
func NewMonitoringStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
func NewAppStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
func NewCICDStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
func main() {
app := awscdk.NewApp(nil)
app.Synth(nil)
}
func env() *awscdk.Environment {
return nil
}
Tiếp theo ta viết thêm một hàm để kết hợp 3 Stack ở trên và dùng câu lệnh if
để so sánh nếu là prod
thì ta mới tạo Monitoring Stack:
func NewService(scope constructs.Construct, id string, props *ServiceProps) {
stack := awscdk.NewStage(scope, &id, &awscdk.StageProps{
Env: env(),
})
if props != nil && props.Prod {
NewMonitoringStack(stack, "monitoring")
}
NewAppStack(stack, "app")
NewCICDStack(stack, "cicd")
}
Cập nhật lại hàm main()
:
package main
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/constructs-go/constructs/v10"
)
func NewMonitoringStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
func NewAppStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
func NewCICDStack(scope constructs.Construct, id string) {
awscdk.NewStack(scope, &id, &awscdk.StackProps{})
}
type ServiceProps struct {
Prod bool `json:"prod"`
}
type Service struct {
constructs.Construct
}
func NewService(scope constructs.Construct, id string, props *ServiceProps) {
stack := awscdk.NewStage(scope, &id, &awscdk.StageProps{
Env: env(),
})
if props != nil && props.Prod {
NewMonitoringStack(stack, "monitoring")
}
NewAppStack(stack, "app")
NewCICDStack(stack, "cicd")
}
func main() {
app := awscdk.NewApp(nil)
NewService(app, "dev", nil)
NewService(app, "prod", &ServiceProps{Prod: true})
app.Synth(nil)
}
func env() *awscdk.Environment {
return nil
}
Chạy câu lệnh ls
để xem các Stack được tạo ra:
cdk ls
dev/app
dev/cicd
prod/app
prod/cicd
prod/monitoring
Như ta thấy với CDK việc xây dựng hạ tầng cho các môi trường khác nhau rất dễ dàng.
Sử dụng Construct giữa các Stack
Một vấn đề khó về mặt tổ chức hay xảy ra khi ta sử dụng các công cụ IaC để tạo hạ tầng là: định nghĩa được các tài nguyên chung và sử dụng cho các hạ tầng khác nhau.
Ví dụ ta có một trường hợp sau: dự án của ta phát triển trên AWS với cấu trúc Microservices và ta quyết định sử dụng Terraform để làm công cụ tạo hạ tầng. Ta tạo một source code cho mỗi Service và viết code để tạo hạ tầng. Thông thường ta sẽ tạo AWS VPC (Virtual Private Cloud) trước và sau đó tạo các tài nguyên khác nằm trong VPC đó, mọi thứ đều ổn.
Tiếp theo dự án của ta mở rộng thêm và nó cần thêm Service. Ta tạo thêm một source code và viết code, nhưng ta phát hiện Service mới này ta cần nó nằm trong VPC cùng với Service trước đó. Lúc này vấn đề đã xảy ra, làm sao ta sử dụng được VPC ta đã tạo trước đó?
Với Terraform ta làm như sau:
data "aws_vpc" "vpc" {
id = var.vpc_id // Giá trị ID của VPC ta đã tạo
}
Điều này không có gì sai, nhưng nó làm dự án của ta khó về mặt tổ chức source code và quản lý tài nguyên. Tài nguyên của source code này lại được để cứng với giá trị của source code khác. Mình sẽ không đề cập cách giải quyết vấn đề với Terraform ở bài này.
Với CDK ta có thể giải quyết vấn đề này khá đơn giản bằng cách truyền Construct giữa các Stack. Để hiểu rõ hơn ta làm một ví dụ tạo hạ tầng cho hệ thống Microservices gồm hai Service là User và Post.
Áp dụng
Tạo thư mục và khởi tạo ứng dụng
mkdir referencing && cd referencing
cdk init --language go && go get
Tạo thêm thư mục stack
với 3 tệp tin như sau:
└── stack
├── global.go
├── post-service-stack.go
└── user-service-stack.go
Tất cả các tài nguyên chung ta để trong tệp tin global.go
:
package stack
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type GlobalStackProps struct {
awscdk.StackProps
}
type GlobalStackResource struct {
Vpc awsec2.IVpc
}
func NewGlobalStack(scope constructs.Construct, id string, props *GlobalStackProps) *GlobalStackResource {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
vpc := awsec2.NewVpc(stack, jsii.String("VPC"), &awsec2.VpcProps{})
return &GlobalStackResource{
Vpc: vpc,
}
}
Hai tệp tin còn lại ta viết code cho User Service và Post Service. Tệp tin user-service-stack.go
:
package stack
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type UserServiceStackProps struct {
StackProps awscdk.StackProps
Vpc awsec2.IVpc
}
func NewUserServiceStack(scope constructs.Construct, id string, props *UserServiceStackProps) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
// EC2 Construct
awsec2.NewInstance(stack, jsii.String("Server"), &awsec2.InstanceProps{
InstanceType: awsec2.NewInstanceType(jsii.String("t3.micro")),
MachineImage: awsec2.NewAmazonLinuxImage(&awsec2.AmazonLinuxImageProps{
Generation: awsec2.AmazonLinuxGeneration_AMAZON_LINUX_2,
}),
Vpc: props.Vpc,
})
return stack
}
Tệp tin post-service-stack.go
:
package stack
import (
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
)
type PostServiceStackProps struct {
StackProps awscdk.StackProps
Vpc awsec2.IVpc
}
func NewPostServiceStack(scope constructs.Construct, id string, props *PostServiceStackProps) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
stack := awscdk.NewStack(scope, &id, &sprops)
// EC2 Construct
awsec2.NewInstance(stack, jsii.String("Server"), &awsec2.InstanceProps{
InstanceType: awsec2.NewInstanceType(jsii.String("t3.micro")),
MachineImage: awsec2.NewAmazonLinuxImage(&awsec2.AmazonLinuxImageProps{
Generation: awsec2.AmazonLinuxGeneration_AMAZON_LINUX_2,
}),
Vpc: props.Vpc,
})
return stack
}
Ở tệp tin referencing.go
ta tạo Global Stack và truyền các tài nguyên liên quan cho các Stack còn lại.
package main
import (
"referencing/stack"
"github.com/aws/aws-cdk-go/awscdk/v2"
"github.com/aws/jsii-runtime-go"
)
func main() {
defer jsii.Close()
// App
app := awscdk.NewApp(nil)
// Global Resource
resource := stack.NewGlobalStack(app, "GlobalStack", &stack.GlobalStackProps{
StackProps: awscdk.StackProps{
Env: env(),
},
})
// User Service Stack
stack.NewUserServiceStack(app, "UserServiceStack", &stack.UserServiceStackProps{
StackProps: awscdk.StackProps{
Env: env(),
},
Vpc: resource.Vpc,
})
// Post Service Stack
stack.NewPostServiceStack(app, "PostServiceStack", &stack.PostServiceStackProps{
StackProps: awscdk.StackProps{
Env: env(),
},
Vpc: resource.Vpc,
})
app.Synth(nil)
}
func env() *awscdk.Environment {
return nil
}
Chạy câu lệnh cdk ls
để liệt kê Stack:
GlobalStack
PostServiceStack
UserServiceStack
Tiếp theo chạy câu lệnh deploy
để tạo tài nguyên. Lưu ý là vì UserServiceStack
và PostServiceStack
đều phụ thuộc vào tài nguyên của GlobalStack
nên ta cần tạo GlobalStack
trước.
Ta có thể chạy cdk --all
để CDK tự động tạo hạ tầng theo thứ tự cho ta, nhưng trong môi trường thực tế ta nên chỉ định từng Stack cụ thể khi triển khai cho chắc ăn.
cdk deploy GlobalStack
cdk deploy PostServiceStack
cdk deploy UserServiceStack
Các bạn nhớ xóa tài nguyên khi hoàn thành để tránh mất phí.
Kết luận
Vậy là ta đã tìm hiểu kĩ hơn về cách sử dụng Stack trong CDK. Với CDK thì việc tổ chức source code đơn giản hơn khá nhiều so với các công cụ IaC khác.
Tác giả Quân Huỳnh
Nếu bài viết có gì sai hoặc cần cập nhật thì liên hệ Admin.
Tham gia nhóm chat của DevOps VN tại Telegram.
Kém tiếng Anh và cần nâng cao trình độ giao tiếp: Tại sao bạn học không hiệu quả?