Blog

  • Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 1

    Python Deep Dive: Hiểu closures, decorators và các ứng dụng của chúng – Phần 1

    Trong lập trình với Python thì Functional Programming đóng một vai trò vô cùng quan trọng và các functions trong Python là các first-class citizens. Điều đó có nghĩa là chúng ta có thể vận hành các functions giống như các objects khác:

    • Truyền các function giống như các đối số.
    • Gán một function cho một biến số.
    • Return một function từ một function khác.

    Dựa trên những điều này, Python hỗ trợ một kỹ thuật vô cùng mạnh mẽ: closures. Sau khi hiểu closures, chúng ta sẽ đi đến tiếp cận một khái niệm rất quan trọng khác – decorators. Đây là 2 khái niệm/kỹ thuật mà bất kỳ lập trình viên Python chuyên nghiệp nào cũng cần phải nắm vững.

    Table of contents

    Global and Local Scopes

    Scopes and Namespaces

    Khi một đối tượng được gán cho một biến (ví dụ: a = 100) thì biến đó trỏ đến một object nào đó và chúng ta nói rằng biến (name) đó được liên kết với đối tượng đó. Khi đó, object có thể được truy cập từ một số nơi trong code của chúng ta, sử dụng name (tên biến) nói trên.
    Tuy nhiên, hãy nhớ rằng tên biến và binding của nó (name và object) chỉ tồn tại trong một phần cụ thể của mã nguồn của chúng ta; phần mã nguồn mà ở đó name/binding được xác định – được gọi là lexical scope của các biến. Các bindings này được lưu trữ trong namespaces (mỗi scope có namespace riêng của nó).

    The Global Scope

    Global scope về cơ bản là module scope. Nó chỉ nằm trong một file .py duy nhất. Trong Python thì KHÔNG có khái niệm global scope (qua tất cả các mô đun trong toàn bộ ứng dụng) thực sự. Chỉ có một số ngoại lệ đó là có một số đối tượng built-in, được sử dụng toàn cục, chẳng hạn như: True, False, None, dict, print.

    Các biến built-in và global có thể được sử dụng bất kỳ đâu trong mô đun của chúng ta, kể cả trong các hàm.

    Global scopes được nested bên trong built-in scope.

    Nếu bạn tham chiếu một tên biến bên trong một scope và Python không tìm thấy nó trong không gian tên của scope đó –> Python sẽ tìm nó bên trong không gian tên của enclosing scope. Ví dụ trong Module1 bạn sử dụng đến nhãn True, Python sẽ tìm True bên trong không gian tên của built-in scope.

    The Local Scope

    Khi chúng ta tạo một function, chúng ta có thể tạo các tên biến bên trong function đó (sử dụng các đối số là ví dụ). Các biến được tạo bên trong function sẽ không được tạo cho đến khi function được gọi.

    Lưu ý: Giả sử chúng ta có function func1 bên trong module. Thì khi load module, Python sẽ compile mọi thứ và func1 sẽ nằm trong namespace của module đang được load. Tuy nhiên mọi thứ bên trong function sẽ chưa được tạo cho tới tận khi chúng được gọi bởi lời gọi hàm.

    def func1(a, b):
        # do something
        pass

    Mỗi khi function được gọi thì một scope mới sẽ được tạo. Và các biến được xác định bên trong function sẽ được gán cho scope đó – được gọi là function local scope. Lưu ý rằng đối tượng thực sự được tạo ra mà một biến trong hàm tham chiếu đến có thể khác nhau trong các lần hàm được gọi (đây là lý do tại sao đệ quy hoạt động!).

    Nested Scopes

    Các scope thường được nested trong các scope khác.
    Khi yêu cầu truy cập vào một object mà một biến tham chiếu đến. Ví dụ:

    print(a)

    Python sẽ tìm object được bound tới biến a như sau:

    • Đầu tiên, tìm trong local scope hiện tại. Nếu không thấy,
    • Lần lượt tìm tiếp lên các scope ‘bao bọc’ scope đang tìm.

    Thêm một ví dụ:

    # module1.py
    a = 10 # a thuộc global scope
    def myFunc(b):
        print(True) # print và True thuộc built-in scope
        print(a) # a thuộc global scope
        print(b) # b thuộc local scope
    
    myFunc(300) # một local scope mới được tạo, b trỏ đến đối tượng lưu trữ 300
    myFunc('a') # thêm một local scope nữa được tạo, b trỏ đến đối tượng lưu trữ 'a'

    The global keyword

    Khi chúng ta truy xuất một giá trị của một biến global bên trong một function. Python sẽ tìm kiến nó theo chuỗi các không gian tên tăng giần: local -> global -> built-in

    Điều gì sẽ xảy ra nếu như chúng ta sửa giá trị của một biến global bên trong một function ?

    a = 0
    def myFunc():
        a = 100 # Python sẽ hiểu rằng đây là biến local tại compile-time
        print(a)
    myFunc() ## 100
    print(a) ## 0

    Chúng ta có thể bảo Python rằng 1 biến được scoped bên trong global scope khi sử dụng nó bên trong một local scope (hàm) bằng cách sử dụng từ khóa global.

    a = 0
    def myFunc():
        global a
        a = 100
        print(a)
    myFunc() ## 100
    print(a) ## 100

    Global and Local Scoping

    Khi Python gặp một định nghĩa hàm tại compile-time, nó sẽ scan bất kỳ nhãn (biến) nào có giá trị assigned cho chúng (anywhere trong function). Nếu nhãn đó không được chỉ định là global thì nó sẽ là local. Các biến được tham chiếu nhưng not assigned một giá trị ở bất kỳ đâu trong function sẽ not be local, và Python sẽ, tại run-time, tìm kiếm chúng trong enclosing scopes.
    Ví dụ 1:

    # vidu1.py
    a = 10
    def func1():
        print(a) ## a chỉ được tham chiếu đến trong function chứ không được gán -> tại compile-time, a là non-local
    def func2():
        a = 100 ## a được gán trong function -> tại compile-time, a là local
    
    def func3():
        global a
        a = 100 ## a được gán và được chỉ định global -> tại compile-time, a là global

    Ví dụ 2:

    # vidu2.py
    def func4():
        print(a)
        a = 100 # tại compile-time, a là local
    
    # khi gọi hàm func4()
    # print(a) sẽ dẫn đến một run-time error bởi vì a là local,
    # và chúng ta đang tham chiếu đến nó trước khi chúng ta gán một
    # giá trị cho nó.
    func4()

    Code

    Tham khảo thêm các ví dụ về Global, local scopes tại:
    Click here

    Nonlocal Scopes

    Inner Functions

    Chúng ta có thể định nghĩa các hàm bên trong hàm khác, như sau:

    def outer_func():
        # some code here
        def inner_func():
            # some code here
    
        inner_func()
    outer_func()

    Cả hai hàm đều có quyền truy cập vào global scope, built-in scope và local scope tương ứng của chúng. Nhưng inner function cũng có thể truy cập vào enclosing scope của nó – ở đây là outer function.
    Scope mà không là local, cũng không là global, thì được gọi là nonlocal scope

    Modifying nonlocal variables

    Xét ví dụ sau:

    # vd.py
    def outer_func():
        x = 'TuAnh'
        def inner_func():
            x = 'HuuLinh'
        inner_func()
        print(x)
    outer_func() ## TuAnh

    Khi inner_func được compiled, python nhìn vào phép gán tới biến x -> nó xác định được biến x là local của hàm inner_func. Đứng ở góc độ inner_func thì biến x trong outer_func là nonlocal. Hai biến này trỏ đến 2 đối tượng khác nhau, vì vậy khi gọi outer_func() sẽ in ra màn hình chuỗi ‘TuAnh’ thay vì ‘HuuLinh’.

    Vậy làm cách nào để có thể sửa được biến x của hàm outer_func bên trong hàm inner_func. Rất đơn giản, giống như các biến global, chúng ta cần khai báo rõ ràng nonlocal với biến x bên trong hàm inner_func, như sau:

    def outer_func():
        x = 'TuAnh'
        def inner_func():
            nonlocal x
            x = 'HuuLinh'
        inner_func()
        print(x)
    outer_func() ## HuuLinh

    Nonlocal Variables

    Bất cứ khi nào Python nói rằng một biến là nonlocal, nó sẽ tìm kiếm trong enclosing local scopes lần lượt từ trong ra ngoài, cho tới khi bắt gặp lần đầu tên biến được chỉ định.
    Lưu ý: Python chỉ tìm trong local scopes, nó sẽ KHÔNG tìm trong global scope.

    Xem xét các ví dụ sau đây:

    # vd1.py
    def outer():
        x = 'tuanh'
        def inner1():
            x = 'python'
            def inner2():
                nonlocal x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # huulinh
        inner1()
        print('outer: ', x)              # tuanh
    outer()
    # vd2.py
    def outer():
        x = 'tuanh'
        def inner1():
            nonlocal x
            x = 'python'
            def inner2():
                nonlocal x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # huulinh
        inner1()
        print('outer: ', x)              # huulinh
    outer()
    # vd3.py
    x = 1000
    def outer():
        x = 'tuanh'
        def inner1():
            nonlocal x
            x = 'python'
            def inner2():
                global x
                x = 'huulinh'
            print('inner(before): ', x)  # python
            inner2()
            print('inner(after): ', x)   # python
        inner1()
        print('outer: ', x)              # python
    outer()
    print(x)                             # huulinh

    Authors

    [email protected]

  • TaskGroup Swift

    TaskGroup Swift

    Trong sự kiện WWDC21, apple đã giới thiệu với công chúng swift 5.5 với nhiều cải tiến, trong đó có async/await. Đây là một cập nhật lớn về cách chúng ta làm việc với bất đồng bộ, chúng dùng để viết những đoạn code async dễ hiểu và dễ đọc.
    Tuy nhiên, async/await bản thân nó không cho phép chúng ta chạy tất cả mọi thứ một cách đồng thời, ngay cả khi với nhiều CPU cores cùng hoạt động, async/ await code vẫn sẽ thực thi tuần tự.
    Để giải quyết vấn đề này, vẫn ở swift 5.5, apple giới thiệu với chúng ta Task và TaskGroups. Chúng là 1 trong những phần quan trọng trong concurrency framework của Swift.

       

    1-Bản chất taskgroup

    Đúng như tên gọi, TaskGroup là 1 tập hợp những task con thực thi đồng thời, nói cách khác, TaskGroup sẽ giúp chúng ta chia 1 công việc thành nhiều concurrent operations.
    Taskgroup hoạt động tốt nhất khi các task con của nó trả về cùng 1 kiểu data, tuy nhiên, ta cũng có thể ép chúng hỗ trợ kiểu data khác nhau.
    Để dể hiểu hơn, mình có 5 gạch đầu dòng về Taskgroup:

    – Một taskgroup là 1 tập hợp các task async mà trong tập hợp đó, các task này hoạt động độc lập với nhau.

    – Tất cả các task con sẽ thực thi 1 cách đồng thời và gần như ngay lập tức sau khi được add vào Taskgroup.

    – Chúng ta không thể kiểm soát khi nào các task con hoàn tất việc thực thi của chúng. Vì thế, chúng ta không nên sử dụng taskgroup nếu muốn các task con hoàn thành theo một thứ tự nào đó.

    – Một taskgroup chỉ return khi và chỉ khi tất các task con của nó hoàn tất. Nói cách khác, tất cả các task của Taskgroup chỉ tồn tại trong chính TaskGroup đó còn đang hoạt động.

    – Có thể dừng một taskgroup bằng cách return 1 giá trị, hoặc return void, hoặc throw error.

       

    2-Mức độ ưu tiên

    TaskGroup có thể được tạo với 1 trong 4 độ ưu tiên: High là độ ưu tiên cao nhất, tiếp đó là Medium, Low, và Background.
    Mức độ ưu tiên task cho phép hệ thống sắp xếp task nào thực thi trước.
    Nếu so sánh với các mức độ của DispatchQueue, userInitiated và utility sẽ là High và low. Taskgroup không có mức độ tương đương với userInteractive, vì với mức độ đó, nó dành riêng cho user interface.

       

    3-Khởi tạo taskGroup

    Để tạo một taskgroup, ta có thể sử dụng withTaskGroup(of:returning:body:) hoặc withThrowingTaskGroup(of:returning:body:). Ở bài viết này, mình không sử dụng taskgroup có throw error, nên nếu muốn tìm hiểu thêm về withThrowingTaskGroup(of:returning:body:), mọi người có thể xem thêm ở document của apple.

       

    4-Làm việc với taskgroup

    Mình có tạo ra 1 struct demoChildTask việc nhân 2 số với nhau, và trong đó có một khoảng nghỉ để tiện cho việc control taskgroup. Ở đây mình có tạo ra một mảng demoChildTask:

    let demoOperations = [
        demoChildTask(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
        demoChildTask(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
        demoChildTask(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
    ]
    

    Sau đó add các task con vào taskgroup bằng cách chạy vòng lặp array demoChildTask

    let demoResult = await withTaskGroup(of: (String, Double).self,
                                             returning: [String: Double].self,
                                             body: { taskGroup in
            
            // Loop through demoOperations array
            for operation in demoOperations {
                
                // Add child task to task group
                taskGroup.addTask {
                    
                    // Execute slow operation
                    let value = await operation.slowMulti()
                    
                    // Return child task result
                    print("Quang Huy -: \(operation.name)")
                    return (operation.name, value)
                }
                
            }
            
            // Collect child task result...
        })
    

    Lưu ý, kiểu dữ liệu task con trả về phải đúng là kiểu dữ liệu của task con mà ta đã khai báo khi khởi tạo TaskGroup

    Như đã đề cập, tất cả các task con đều thực thi đồng thời với nhau, nên ta không thể control việc khi nào chúng hoàn tất. Vì thế để nhận result của từng task con, ta phải loop qua taskgroup:

    // Collect results of all child task in a dictionary
    var demoChildTaskResults = [String: Double]()
    for await result in taskGroup {
        print("Quang Huy 1 - \(result.0)")
        // Set operation name as key and operation result as value
        demoChildTaskResults[result.0] = result.1
    }
            
    // All child tasks finish running, return task group result
    return demoChildTaskResults
    

    Ở đoạn code trên, mình sử dụng await keyword, keyword này có ý nghĩa là vòng lặp có thể dừng lại để đợi task con thực thi xong. Mỗi khi task con xong, vòng lặp lại tiếp tục và update giá trị cho childTaskResults.
    Sau khi xử lý result của tất cả các task con hoàn tất, ta return về result của taskgroup. Giống như task con, return type của taskgroup cũng cần phải trùng với kiểu return khi khởi tạo nó:

    Khi chạy đoạn code trên, ta nhận được đoạn log như này:

    Như có thể thấy, task group cần ~ 5s để hoàn thành, 5s cũng là khoảng nghỉ dài nhất mà mình khởi tạo.

       

    5-Tổng kết

    Với sự ra mắt của TaskGroup, việc quản lý và sử dụng các concurrent task chưa bao giờ đơn giản đến thế. Đồng thời cũng là sự kết hợp hoàn hảo với async/await, thứ cũng là 1 cập nhật lớn ở WWDC21.


    REFERENCE

    https://www.hackingwithswift.com/quick-start/concurrency/what-are-tasks-and-task-groups

    https://developer.apple.com/documentation/swift/taskgroup

  • SSL Pinning & Signature checking(SecureCoding – P2)

    SSL Pinning & Signature checking(SecureCoding – P2)

    SecureCoding – P2

    Hi, lại là mình, rambler coder đây

    Trong phần trước chúng ta đã tìm hiểu về MITM attack và một số thủ thuật đi kèm

    Trước khi đi vào detail rule/coding tips ở những phần sau, phần thứ 2 này mình muốn chia sẻ thêm về 1 số rule config project, runtime application để hạn chế việc application bị tấn công, sửa đổi.

    Table of contents

    Ssl Pinning

    Hành động này gần như là bắt buộc với tất các ứng dụng sử dụng giao thức HTTPS. Tuy nhiên trên Google developer page chúng ta có thể thấy một đoạn warning như sau

    Caution: Certificate Pinning is not recommended for Android applications due to the high risk of future server configuration changes, such as changing to another Certificate Authority, rendering the application unable to connect to the server without receiving a client software update.
    

    Sự lo lắng này hoàn toàn hợp lý, khi certificate hết hạn, khi thay đổi certificate..etc – không có cách nào chắc chắn người dùng sẽ cập nhật phần mềm, đồng nghĩa với việc người dùng không thể kết nối tới máy chủ, và có thể chúng ta sẽ mất một lượng user rất lớn. Nên nếu sử dụng SSL pinning, chúng ta phải handle được các case liên quan đến certification và phải tính đến khả năng force update đối với client khi có exception liên quan đến ssl certificate

    Có rất nhiều cách để triển khai SSL pinning, tuỳ thuộc cách thức mà ứng dụng kết nối với server mà chúng ta lựa chọn item phù hợp

    1. NetworkSecurityConfig

    Step 1 Create networkSecurityconfig file

    res/xml/network_security_config.xml
    

    với nội dung sau

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config>
            <domain includeSubdomains="true">example.com</domain> // your domain
            <pin-set expiration="2018-01-01">
                <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> // your hash key 
                <!-- backup pin -->
                <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin> // your hash key 
            </pin-set>
        </domain-config>
    </network-security-config>
    

    Step2

    Khai báo sử dụng trong AndroidManifest

    <?xml version="1.0" encoding="utf-8"?>
    <manifest ... >
        <application android:networkSecurityConfig="@xml/network_security_config"
                        ... >
            ...
        </application>
    </manifest>
    

    Chi tiết vui lòng tham khảo https://developer.android.com/training/articles/security-config#CertificatePinning

    1. OKHttpClient

    Nếu đang triển khai kết nối bằng OKHttpClient thì có thể tham khảo link bên dưới https://github.com/baka3k/CleanArchitecture/blob/main/data/src/main/java/com/baka3k/architecture/data/service/base/Service.kt

    private fun buildCertificatePinner(): CertificatePinner {
            return if (pinning != null && pinning.isNotEmpty()) {
                val builder = CertificatePinner.Builder()
                for (item in pinning) {
                    builder.add(item.value, item.key)
                }
                builder.build()
            } else {
                CertificatePinner.Builder()
                    .add("api.themoviedb.org", "sha256/+vqZVAzTqUP8BGkfl88yU7SQ3C8J2uNEa55B7RZjEg0=") // your hash key (pinning SSL)
                    .build()
            }
        }
    }
    

    Sử dụng

    val pining = buildCertificatePinner()
    val okHttpClient = OkHttpClient.Builder()
                .certificatePinner(pining)
                .build()
    
    1. Hash Key? – sha256 lấy ở đâu?

    Trong 2 ví dụ ở trên chúng ta đều thấy sự xuất hiện của chuỗi ký tự sha256, có nhiều cách để lấy chuỗi này, ví dụ dùng open-ssl hoặc cách đơn giản nhất là truy cập https://www.ssllabs.com/ – Điền link muốn check vào và thông tin sẽ hiện ra như sau

    pinning

    Self signature

    Đây là một kỹ thuật mình thấy khá hiếm người sử dụng trong ứng dụng(mặc dù nó rất dễ, mất vài dòng code thôi) – dùng để ngăn chặn(phần nào đó) việc APK bị sửa đổi, thêm mã độc.

    Hầu hết lập trình viên Android đều từng ít nhất một lần tải APK từ nguồn ko chính thống, ví dụ apkpure, apkresult hoặc các game trên appstorevn

    Điều gì đảm bảo các apk này là apk sạch? ko có bị sửa đổi với mục đích thu thập thông tin người dùng, chèn quảng cáo hoặc tệ hơn là Phishing, hoặc tạo ra các attack vector để tấn công vào một hệ thống nào đó – ở đây có thể là hệ thống của chính chúng ta? – nếu đó là APK của chúng ta?

    Ví dụ, attacker có thể modify một file APK cho app ngân hàng/ví điện tử nào đó, ở giao diện đăng nhập thay vì việc gửi thông tin đăng nhập thì attacker sẽ log lại thông tin đăng nhập này, bao gồm mật khẩu, password và gửi về máy chủ của attacker chả hạn.

    Một file APK có thể được sửa đổi và sign đi sign lại nhiều lần, cách nhanh nhất để kiểm tra một file APK có bị sửa đổi rồi sign lại hay ko là check signature của file APK, signature là duy nhất, nó chỉ được gen ra bởi key dùng để sign apk và ko có cách nào từ signature trong apk sinh ngược được ra sign key cả.

    1. Dùng Keytool để lấy ra SHA của file APK release
    keytool -printcert -jarfile app-release.apk
    ... 
    MD5:  B3:4F:BE:07:AA:78:24:DC:CA:92:36:FF:AE:8C:17:DB
    SHA1: 16:59:E7:E3:0C:AA:7A:0D:F2:0D:05:20:12:A8:85:0B:32:C5:4F:68XXXX
    SHA256: 1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX1XXXXXXXXXXXX
    
    1. Trên source code ứng dụng, khi vào một màn hình nào đó chúng ta có thể check SHA dựa vào đoạn code đơn giản bên dưới
    Signature[] sigs = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
    for (Signature sig : sigs)
    {
        Trace.i("MyApp", "Signature hashcode : " + sig.hashCode());
    }
    

    Compare các mã sha này => có thể phán đoán được ứng dụng có bị sửa đổi hay ko, tuỳ thuộc business, bạn có thể quyết định stop hẳn ứng dụng hoặc disable các tính năng tương ứng.

    API Key

    Đây là rule liên quan đến việc lưu trữ các API key/ User/Password cần thiết khi buid ứng dụng

    1. API Key Thông thường khi phát triển ứng dụng, developer hay có xu hướng hardcode API key trực tiếp vào source code, ví dụ thế này
    object MovieDBServer {
        const val MOVIE_DB_ACCESS_KEY = "xxxxxxxxxxxxx"
    }
    
     @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = MovieDBServer.MOVIE_DB_ACCESS_KEY
        ): MovieResult
    

    Giả sử chúng ta có 2 API key dùng cho test server và production server thì sao?

    object MovieDBServer {
        const val MOVIE_DB_ACCESS_KEY_DEBUG = "xxxxxxxxxxxxx"
        const val MOVIE_DB_ACCESS_KEY_PRODUCTION = "xxxxxxxxxxxxx"
    }
    

    Tất nhiên khi query thì phải check như thế này

    @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = if (BuildConfig.DEBUG)
                MovieDBServer.MOVIE_DB_ACCESS_KEY_DEBUG
                else MovieDBServer.MOVIE_DB_ACCESS_KEY_PRODUCTION
        ): MovieResult
    

    Có 2 issue ở đây

    • Có source code là có key, key không được quản lý tập trung
    • Nếu thêm nhiều loại key, vd stagging, production, develop..etc thì phải sửa source để build

    Để giải quyết vấn đề này, người ta sẽ đẩy việc define các key ra file – ngoài source code và sẽ inject vào trong quá trình build time, các job build khác nhau cho các môi trường khác nhau sẽ inject các API key khác nhau. Tức là thế này

    @GET("popular")
        suspend fun getPopular(
            @Query("api_key") clientId: String = BuildConfig.MOVIE_DB_ACCESS_KEY // chú ý dòng này BuildConfig.
        ): MovieResult
    

    MOVIE_DB_ACCESS_KEY được inject vào tại build time, nên có thể tuỳ chỉnh, thay đổi cho các job build khác nhau.

    Một ví dụ đơn giản, mình chọn lưu ra gradle.properties

    moviedb_access_key="PLACE YOUR KEY IN HERE"
    

    Trên file build.gradle

      defaultConfig {
            ...
            buildConfigField("String", "MOVIE_DB_ACCESS_KEY", "\"" + getMovieDBAccessKey() + "\"")
        }
    .....
    def getMovieDBAccessKey() {
        return project.findProperty("moviedb_access_key")
    }
    

    Cụ thể, nếu các bạn cần sample, thì có thể refer tại https://github.com/baka3k/CleanArchitecture/blob/main/data/build.gradle

    Tương tự đối với các sensitive information như user/password của private repository, pass của keystore..etc chúng ta cũng có thể inject trong build time như vậy

    Authors

    [email protected]

  • Robustness or Resilience?

    Robustness or Resilience?

    Migrated to https://athena.wingadium.space/writing/robustness_or_resilience

    Nếu phần mềm là một vật thể bạn mong muốn chúng như một chai thủy tinh rất cứng hay một sợi dây cao su có khả năng co giãn?

    Đầu tiên nói đề cập thế này có vẻ hơi khó hiểu, hãy quay trở lại với bài viết: Software Architecture: Bắt đầu từ đâu? – Part 3 Soft Skills – Continuous Delivery. Chúng ta có một vài metric liên quan đến Continuous Delivery, hãy để ý đến 2 cái Mean Time Between Failures (MTBF) và Mean Time To Repair (MTTR).

    Robustness, một quan niệm khá xưa trong thế giới phần mềm, người ta muốn quan tâm đến việc phần mềm ít xảy ra sự cố hơn là thời gian khắc phục nó (ngay cả ở môi trường thương mại – production), tức là MTBF dài hơn được coi trọng hơn MTTR ngắn. Cách tiếp cận này đôi khi được coi là truyền thống, và thường các phần mềm này sẽ không được apply Continuous Delivery trong quy trình phát triển.

    Resilience, khả năng phục hồi, thì ngược lại MTTR sẽ được coi trọng hơn, một cách tiếp cận để hạn chế tác động của sự cố phần mềm và phần mềm có thể hoạt động trong các điều kiện khác nhau về phần cứng cũng như cơ sở hạ tầng.

    Một cách suy nghĩ đơn giản như này, với Robustness phần mềm khó có cơ hội apply Continuous Delivery, vì khi một phần mềm đòi hỏi ít sự cố, tức là số người trong chuỗi ra quyết định sẽ dài hơn (quan liêu), các stage cuối cùng của CD Pipeline thường ít được sử dụng do cần thời gian dài để xử lý qua các stage trước đó.

    Khi một phần mềm có xu hướng chuyển dần sang Resilience thì chúng ta có thể giảm effort để quản lý rủi ro (vì sự cố và lỗi được xử lý liên tục), và dần chuyển sang CD.

    Robustness – Truyền thống

    Dễ thấy là trong 1 2 chục năm trước, phần mềm hoàn hảo luôn là xu thế, các tổ chức luôn muốn phần mềm có độ tin cậy cao, tức là MTBF/MTTR khá lớn. Các dự án phần mềm theo kiểu này luôn muốn duy trì một môi trường production không có sự cố, với một niềm tin rằng môi trường production luôn được xử lý theo các ẩn số đã biết, trong đó các quá trình tương tác với môi trường luôn đồng nhất và có thể dự đoán được. Sự cố trong các phần mềm này luôn được cho là do các thay đổi (source code, biến môi trường, các thông số phần cứng), và có thể dự đoán được.

    Trong một vài dự án với các khách hàng mang xu thế cổ điển, họ thường đòi hỏi chúng tôi chỉ ra tất cả các kịch bản có thể có với hệ thống phần mềm mới được xây dựng???

    Trong trường hợp đó, chúng ta chỉ có thể xử lý theo cách như sau:

    image

    • End-to-end testing để xác minh chức năng trong phiên bản mới mới cùng với các servioce/component phụ thuộc.
    • Change Boards: một bộ sậu các phòng ban để quyết định đưa phiên bản lên môi trường production
    • Freeze: đóng băn phần mềm để hạn chế thay đổi trong một khoảng thời gian.

    Đầu tiên chúng ta có thể nghĩ rằng, tại sao việc chú trọng vào phần mềm không lỗi lại là vấn đề trong khi nó là một yêu cầu khá hợp lý, nhưng hãy xem xét, 3 practice trên đều chậm, đó là vấn đề của việc quản lý rủi ro trong Continuous Delivery. End-to-end testing đòi hỏi chi phí và thời gian dài để vét cạn các lỗi, đồng thời khi đến giai đoạn System test thường sẽ cần thời gian down time đáng kể để cô lập các vấn đề ở môi trường production. Change Boards sẽ dẫn đến một cơ chế quan liêu để đưa ra được quyết định cho phiên bản mới. cả 2 practice đều chậm và vẫn có khả năng sự cố, tức là chúng ta không thể thấy được các ẩn số chưa biết trong hệ thống phức tạp (Complex domain – Cynefin framework, còn chúng ta mới chỉ để ý đến Complex Domain)

    image

    Freeze, đương nhiên rồi, lead time quá dài.

    Thời gian trong việc phát triển phần mềm hiện nay luôn là yếu tố quan trọng, chậm trễ vài tháng thậm chí chỉ là vài tuần sẽ làm lỡ các phiên bản update của library, phần cứng hay ngôn ngữ, việc đó sẽ gia tăng rủi ro khi chúng biến thành các yêu cầu thay đổi về công nghệ, phiên bản vì có quá nhiều việc phải xử lý trong một lần update phần mềm.

    Từ đó có thể thây rằng, Robustness hoàn toàn không thể đáp ứng nhu cầu về mặt kinh doanh của phần mềm, vì sự hạn chế về sự ổn định và số lượng phiên bản, điều đó lý giải tại sao nhiêu dự án phần mềm không thể xử lý được vấn đề Continuous Delivery.

    Khi sự cố là điều không thể tránh khỏi

    Vấn đề là thường khi chúng ta quá chú ý đến Robustness, phần mềm thường quá chặt chẽ và dẫn tới việc phần mềm đó gần như không có khả năng xử lý khi có failure, tức là cả hệ thống sụp đổ, thay vì một phần nhỏ.

    Chúng ta có thể thấy như này khi một dự án chú ý đến Robustness, nhưng họ lại bị thúc ép bởi các nhu cầu mang tính ngắn hạn (nhanh, rẻ, tốt) thì các nhu cầu cơ bản về an toàn, high availability sẽ bị bỏ qua. Dự án sẽ tập trung rất nhiều nguồn lực vào môi trường production, các non-functional requirement thường sẽ bị bỏ qua, không có các dự tính dài hạn cho môi trường thực tế khi user bắt đầu sử dụng, hạ tầng thường dễ bị sụp đổ (fragile) bởi các yếu tố bất ngờ về người dùng (số lượng người dùng gia tăng đột biến). Nhưng ngược lại họ và đối tác thường chấp nhận vì hệ thống hiếm khi xảy ra sự cố.

    Tuy nhiên, thật là production environment không hẳn là một hệ thống phức tạp mà chúng ta có thể dự đoán được. Production environment theo kinh nghiệm hiện tại cho thấy, thường sẽ phải xử lý khối lượng lớn các request không đồng nhất, với các điều kiện không thể lặp lại.

    Ví dụ phần mềm sẽ xử lý thế nào nếu AWS bị sự cố ở cả 1 datacenter (Availability Zone) Nhìn chung các hệ thống này thường sẽ tiềm ẩn nhiều sai sót nhỏ lẻ, nhưng không tạo thành sự cố. Mà thường các sự cố này được tạo ra trong một điều kiện nhiều sai sót nhỏ lẻ kết hợp với nhau.

    Vậy khi có sự cố chúng ta mất những gì

    • Đầu tiên là doanh thu = doanh thu trên một đơn vị thời gian x thời gian để xử lý
    • Chi phí mất mát vì sự cố đến khi chúng được phát hiện: tỉ như việc nhà mạng không thể xử lý kịp khi tài khoản hết tiền mà vẫn nhắn được hàng trăm, hàng chục tin trong vòng vài phút, sự cố huyền thoại này có lẽ mất đến hàng năm để phát hiện. Rõ ràng chi phí cho sự cố tiềm ẩn này quá lớn.
    • Chi phí về cơ hội tiếp cận khách hàng, giả sử như Tiki, họ không thể xử lý được các hot deals trong các đợt sale trong thời gian thực (ref) thì họ sẽ mất đi khá nhiều khách hàng vì niềm tin, hơn nữa với kiến trúc dễ vỡ, disaster blast radius có thể lớn, ảnh hưởng đến các vùng khác trong hệ thống.

    Hãy xem xét bài toán giả thuyết như này:

    Chúng ta có một trang thương mại điện tử, vì một lý do nào đó, database của service Cart bị quá tải, mọi thứ vẫn hoạt động, nhưng người dùng không thể đặt hàng. Đội dự án không phát hiện ra sự cố này cho đến ngày thứ 4, mất 2 ngày để hot fix, và mất 1 ngày tiếp theo để checkout. Mỗi ngày chúng ta mất 80 triệu doanh thu, vậy là tổng cộng chúng ta mầt 560 triệu , trong đó có 240 triệu chi phí cơ hội và 320 triệu chi ví chìm.

    image

    Tại sao sự cố lại xảy ra trong khi tất cả mọi việc đã được tính toán trước? Con người, tất nhiên rồi. Có một lý thuyết có tên gọi là Bad Apple – Quả táo hỏng, trong trường hợp này có thể hiểu là, không phải tất cả các thành viên trong dự án đều hoàn hảo mọi lúc (về công việc của họ trong dự án), hệ thống tin cậy nhưng con người thì không, khi có sự cố xảy ra nó kết hợp với việc tất cả mọi thứ đều được xem như là đã tính toán trước (các ẩn số đã biết), dễ dàng trở thành một kiểu đổ lỗi cho các cá nhân có liên quan, từ đó giảm độ hợp tác và chia sẻ kiến thức chung trong dự án.

    Việc tách biệt các nhiệm vụ đóng vai trò như một rào cản đối với việc chia sẻ kiến ​​thức, phản hồi và cộng tác, đồng thời làm giảm nhận thức về tình huống vốn là yếu tố cần thiết để có phản ứng hiệu quả trong trường hợp xảy ra sự cố. Hơn nữa, thường trong develop team sẽ còn có các hoạt động review code, điều này cũng làm giảm hiệu suất cho việc xử lý/phản hồi về sự cố, và đổ lỗi cho hoạt động review.

    Với đội dự án trong trường hợp thương mại điện tử ở trên, họ sẽ xử lý như nào, có lẽ để xây dựng một phần mềm theo hướng Robustness, họ đã mất cả tháng (Lead Time), để qua tất cả các practice. Thế nhưng họ đã mất 4 ngày doanh thu cho chi phí chìm vì phát hiện chậm, vậy nên 320 triệu đã mất đi khiến họ phải chạy đua với thời gian để giảm ngay lập tức chi phí cơ hội, ở trường hợp này họ sẵn sàng hy sinh chính Robustness để đảm bảo tiến độ, trong đó tất cả các practice của Robustness bị giảm xuống còn giờ hoặc ngày. Vấn đề có thể thấy ngay, cùng một quy trình nhưng đã bị kéo từ hàng tuần xuống hàng ngày hoặc thậm chí vài giờ.

    Continuous Delivery có thể cải thiện độ ổn định của hệ thống và khối lượng release, nhưng để xử lý rất khó. Continuous Delivery cho hệ thống Robustness sẽ gặp vấn đề ngay lập tức khi dự án quá chú trọng vào khối lượng release (đơn giản là khối lượng lớn, nhưng thời gian release quá dài để thông qua các practice). Rõ ràng thời gian development (coding, design, architect, unit-test) khó có thể giảm vậy 3 practice rõ ràng là khả dĩ nhất để thay đổi và giản lược, nhưng End-to-end testing gần như đã ăn sâu vào tiềm thức và gần như mọi người đều công nhận nó. Và Continuous Delivery sẽ bị đổ lỗi vì làm thay đổi văn hóa của dự án ngay lập tức khi có lỗi đầu tiên ở môi trường production sau khi áp dụng, và dự án sẽ quay trở lại với hiện trạng.

    Nếu hệ thống có khả năng phục hồi thì sao

    Hoặc chí ít là phục hồi nhanh, nếu việc này được chú trọng (Resilience), tức là chúng ta muốn có MTTR thấp hơn là việc MTBF cao, bằng cách tối ưu hệ thống để có thể hotfix trên production khi có sự cố. Nhìn chung lỗi có thể phân loại, một số sẽ không bao giờ xảy ra, một số lỗi gây ra sự cố nghiêm trọng hơn các lỗi khác và các lỗi về an toàn thì không được phép xuất hiện, nhưng rõ ràng chúng ta nên có thể nhanh chóng đưa hệ thống trở lại hơn là cố gắng ít sự cố hơn.

    Nếu giả sử trang thương mại điện tử bên trên có khả năng phục hồi (tự động hoặc manual), chúng ta có thể dễ dàng thay đổi hệ thống về phiên bản ổn định trước đó, ngoài ra khi hệ thống gặp vấn đề về performance, việc thay đổi phần cứng tốt hơn sẽ dễ dàng hơn và chi phí phần cứng đôi khi chưa bao giờ là vấn đề khi so sánh với mất mát về doanh thu.

    image

    Vậy xây dựng hệ thống Resilience cần gì, theo Erik Hollnagel trong cuốn Resilience Engineering in Practice có 4 yếu tố:

    • Anticipation: Hiểu hệ thống sẽ đối mặt với những thứ gì: số lượng người dùng đột biến khi có notification mới, một API Get cho hàng triệu người dùng mà không có biện pháp cache sẽ gây quá tải database và web server…
    • Monitoring: biết được cần theo dõi thứ gì trong hệ thống, khi nào hệ thống bất thường
    • Response: sử dụng các hiểu biết và các nguyên tắc cũng như nhận thức chung của cả dự án khi có sự cố để giảm thiểu tác động.
    • Learn: Hiểu các trường hợp sự cố là gì, các trường hợp có cảnh báo, và chia sẻ trong dự án.

    Ngoài ra sẽ có các yếu tố về văn hóa và tổ chức hỗ trợ. Trong một dự án, rõ ràng với hệ thống phức tạp, dự án cần một khối lượng kiến thức và nguồn lực đủ nhiều để ngăn ngừa các lỗi tiềm ẩn hoặc để xử lý sự cố, thường thì các team scrum sẽ có các phương thức hotfix để xử lý sự cố nhỏ hơn là thực hiện theo các operation guideline, và tất cả đều dựa trên sự giao tiếp thuận tiện giữa tất cả các thành viên, clear and transparent information.

    Vậy Resilience system cần gì, chúng ta sẽ có một môi trường production mà trong đó các service/component độc lập, mở rộng theo chiều ngang và dọc tốt để ứng phó các thay đổi bất ngờ trong thực tế. Nhìn chung ngưỡng chịu đựng của các hệ thống Resilience sẽ tốt hơn: ví dụ nếu hệ thống phân tán và phần cart có khả năng mở rộng thì có thể hệ thống thương mại điện tử bên trên có thể serve được đến 10.000 CCU mới xảy ra sự cố thay vì 3.000 CCU, như vậy thời gian down time và chi phí mất đi rõ ràng giảm.

    Vậy thay vì quá chú trọng vào phần DEV, phần OPS trong dự án được đầu tư nguồn lực tốt hơn để giữ cho hệ thống ở trong điều kiện hoạt động an toàn và tin cậy. Tuy nhiên việc xây dựng hệ thống có khả năng chịu đựng tốt với các điều kiện, thường dựa trên các practice sau đây:

    1. Đối với phần developer:
    • Adaptive architecture hay hiểu theo kiểu defensive architecture cũng được: code những gì cần thiết, tính toán các trường hợp, có một bức tranh tổng thể, code dễ hiểu, dễ maintain và có thể mở rộng
    • Feature On/Off, feature không gắn liền với phần kết cấu lõi của phần mềm, ví dụ trong một hệ thống IoT, timeseries có thể lưu trữ bằng nhiều loại DB khác nhau: Timeseries Database ~ Cassandra, hoặc đơn giản hơn lưu trữ vào thẳng một DB quan hệ với index phù hợp, khi Cassandra down, rõ ràng việc dựng lại cả hệ thống DB Cassandra mất nhiều thời gian hơn cho việc chuyển sang SQL, nhưng việc đó cần được chuẩn bị về mặt feature
    1. Test, mình muốn nói đến việc test tình trạng hệ thống nhé, còn feature đương nhiên phải pass all test rồi.
    • Smoke test kết hợp với Chaos Engineering sẽ hiệu quả trong việc kiểm thử về tình trạng sơ bộ của hệ thống hoặc tìm ra các lỗi tiềm ẩn như trong các điều kiện server bất ngờ dừng hoạt động hay ổ cứng bị lỗi, kết nối mạng bị ngắt…
    1. Hạ tầng – Infrastructure: Infrastructure as a service và Auto Recovery, IaaS sẽ đảm bảo việc hạ tầng ở các môi trường giống nhau, giảm tác động của con người với hạ tầng, trong khi đó Auto Recovery sẽ phục hồi hệ thống khi có service bị fail, tất nhiên ở mức độ nào đó, source code của developer sẽ cần đáp ứng được một số yêu cầu.

    Những ai đã tiếp xúc với các kỹ thuật IaaS như terraform, pulimi, cloudformation, k8s yaml đều khá tự tin khi deploy một môi trường tương tự, độ chính xác của các phần code hạ tầng này đều rất cao, công việc còn lại chỉ là đánh giá lại kết quả.

    1. Timeseries/Telemetry: Log, Monitor luôn là yêu cầu bắt buộc, kết hợp với alarm để mọi người có thể được thông báo khi có các sự cố hay lỗi runtime xảy ra. Cuối cùng là User Analytics (có thể lấy từ Firebase analytic, …) dành cho việc theo dõi user experience.
    2. Con người: phần luôn là khó nhất và để nói về văn hóa trong dự án là chính, nhưng có nguyên tắc “You Built It, You Run It”: thông tin trong dự án sẽ cần được transparency, mọi người cần làm mọi việc có thể làm (có thể không thuộc trách nhiệm của mình) để đảm bảo rằng mọi thứ ổn ngay từ đầu, việc minh bạch thông tin sẽ giúp tìm ra nguyên nhân của các vấn đề nhanh hơn nhiêu.

    1 trong 2 yếu tố nữa trong phần con người đó là Blameless PostMortems, mọi người rất dễ mắc sai lầm nhất là khi họ cần cân bằng giữa khả năng và trách nhiệm, khi đó không nên điều tra nguyên nhân và quy kết cho sự cố xuất phát từ sai lầm cá nhân, điều chỉ nên được xem xét như là một nguy cơ tiềm ẩn trong tổ chức, vấn đề này gần như luôn luôn song hành với yếu tố còn lại – Hindsight bias. Thành kiến ​​nhận thức muộn – Khi mà mọi người vẫn tin rằng sau một sự cố trong quá khứ, họ có thể dự đoán được với độ chắc chắn cao về các sự cố có thể xảy ra sau này, điều đó có nghĩa là khi bạn để xảy ra lại những sự cố đó thì đó là thảm họa. Nhìn chung nên nhìn nhận những vấn đề kiểu như Hindsight Bias là không thể tránh khỏi và hãy làm tất cả để tránh cùng với việc hãy để những người mắc sai lầm truyền đạt lại kinh nghiệm của họ để tránh sự cố cho những người khác.

    Hơi lan man một chút, hãy quay trở lại bài toán thương mại điện tử nêu trên. Trong trường hợp build hệ thống đó theo hướng Adaptive và Feature On/Off, rõ ràng phần cart luôn là phần quan trọng trong hệ thống, nó nên được quản lý bởi một Service Registry bên cạnh đó Circuit Breaker sẽ đóng vài trò kiểm tra service có được hoạt động hay không, vậy sẽ không tốn đến 5 phút để phát hiện ra vấn đề, một bản vá có thể được fix trong vòng 3 tiếng, thâm chí là 1 ngày nhưng sẽ được deploy ngay sau đó. Hãy tính toán lại chi phí: như vậy sẽ tốn ~ 80 triệu, trong đó chi phí chìm vì phát hiện ra vấn đề chậm không đáng kể, còn chi phí fix cũng giảm do việc deploy nhanh chóng và thuận tiện hơn.

    Khả năng phục hồi được tối ưu hóa gần như là nhu cầu tất yếu cho một phần mềm hay một tổ chức có thể đổi mới và phát triển, khi đó chúng ta có thể đầu tư vào con người và công nghệ để đạt được Robustness: khả năng thích ứng với các điều kiện bất ngờ có thể xảy đến trong tuơng lai, khi đó các doanh nghiệp, tổ chức sẽ có lợi thế lớn khi đưa ra sản phẩm nhanh hơn các đối thủ cạnh tranh.

    Continuous Delivery hỗ trợ bởi Resilience

    Mọi người sẽ có xu hướng tìm một cách để xử lý Continuous Delivery cho mọi dự án, tổ chức tuy nhiên việc đó quá phức tạp. Bản thân việc xây dựng theo hướng adaptive architect như đã nói ở trên cũng sinh ra các hoàn cảnh và ràng buộc.

    Tuy nhiên nếu một dự án đã tối ưu hóa Robustness và đang không có Continuous Delivery chúng ta có thể xử lý theo các bước sau.

    1. Version hóa mọi thứ: Deployment sẽ ổn định hơn, không có phiên bản nào bất ngờ được deploy mà mọi người không biết.

    Có một vài tình huống như bạn thực hiện hàng loạt thay đổi trong K8S Yaml, nhưng ngày hôm sau sự cố xảy ra, bạn không thể biết được chính xác vấn đề nằm ở đâu khi không versioning các config mà bạn đã làm.

    1. Đo đạc: mục đích đảm bảo việc delivery ổn định hơn khi chúng ta đã biết rõ chúng ta phát triển thứ gì và delpoy/deliery những phần nào.
    2. tăng độ tin cậy cho Production environment: có 2 hoạt động cần làm rearchitect, và thiết lập các hệ thống monitoring. Rearchitect cần làm dần dần và theo hướng adaptive để giảm rủi ro do vẫn là hệ thống production.
    3. Bước cuối cùng, thực ra đây không hẳn là một bước mà khi đến bước này hệ thống và dự án đã đủ các điều kiện cần thiết, việc deployment đã ổn định production environment đã phần nào có khả năng tự phục hồi, việc còn lại là áp dụng như một hệ thống Resilience.

    Sau đó thì sao

    Nhìn chung sau các bước bên trên, điều còn lại cuối cùng là văn hóa. Các tổ chức có xu hướng vẫn giữ lại các practice của robustness để đề phòng rủi ro, việc thay thế nó cần thời gian và một câu chuyện hợp lý bao gồm các lập luận về chi phí, thời gian và tỉ lệ lỗi giảm.

    Nhưng việc đầu tiên đó là thay đổi nhận thức về các vấn đề quản lý rủi ro và robustness. Các bước thay đổi bên trên sẽ cho chúng ta một loạt các thông số về độ ổn định, khối lượng delivery và minh họa chi phí có thể được cải tiến cũng như MTTR và thời gian triển khai. Ngoài ra Chaos Engineering được xem xét cẩn thận sẽ cho chúng ta các practice giúp giảm MTTR xuống vài giờ thậm chí vài phút, bằng cách giả lập và chỉ ra các lỗi có thể xảy ra trong môi trường production.

    Các practice của robustness khi đó sẽ dần được thay thế bằng sự kết hợp giữa Continuous Delivery và các practice trong quá trình vận hành. End-to-end testing sẽ được thay thế dần bởi Test Pyramid, khối lượng test, tần suất test sẽ được phân phối một cách hợp lý để giảm thời gian test và chi phí. Trong đó số UnitTest sẽ được tiến hành thường xuyên và cho môi bản build, Acceptance Test sẽ được pick up (khoảng 100 cases) và cũng được tiến hành như vậy, trong khí đó các loại smoke test sẽ chỉ khoảng 10 case, các kỹ thuật telemetry thì gần như tiến hành hằng ngày hằng giờ.

    Change Boards và Change Freezes hiện tại sẽ trở nên lỗi thời với các kỹ thuật Blue Green Deployments và Canary Deployment, trong đó các phiên bản mới sẽ được triển khai và thay thế dần dần phiên bản cũ, việc deploy sẽ được rollback khi có sự cố. Ngoài ra Facebook cũng đưa ra một kỹ thuật khác – Dark Launch, khi đó Facebook sẽ đưa các tùy chọn cho người dùng có bật hay tắt các tính năng mới hay không. Hiện nay Google hay AWS cũng làm theo cách này, cũng là một cách khá tốt để thử nghiệm tính năng trên môi trường thực tế – điều mà mọi dự án đều mong muốn.

    Kết luận

    Tối ưu hóa cho Robustness có vẻ đã lỗi thời và không thể đáp ứng được nhu cầu công nghệ thông tin hiện nay, cùng với đó việc không áp dụng Continuous Delivery trong một thời gian dài rất dễ dẫn đến những sự cố lớn. Robustness rõ ràng vẫn có giá trị, điều đó là mong muốn tự nhiên của các lập trình viên và dự án, tuy vậy sự ưu tiên nên được thay đổi để hệ thống có khả năng thích ứng cao hơn cả về nghiệp vụ và các yêu cầu từ phía người dùng.

    Mặt khác tối ưu hóa cho khả năng phục hồi của hệ thống có vẻ là một chiến lược có độ tin cậy cao hơn, cho phép phần mềm và dự án có thể mở rộng, tránh tác động của các lỗi và tìm cách làm cho phần mềm có khả năng chịu đựng và thích ứng tốt hơn. Việc này rõ ràng là một sự thay đổi về mô hình tổ chức và văn hóa, trong đó cần mọi người nhận thức rằng hệ thống phần mềm rõ ràng luôn phức tạp và nhiều khi sự cố là không thể tránh khỏi, việc tối ưu khả năng phục hồi sẽ làm giảm chi phí hay tổn thất cho sự cố mà thôi.

    Ngoài ra việc tối ưu hóa cho Robustness sẽ làm giảm hiệu quả của Continuous Delivery, Continuous Delivery có thể cho ta thấy hiệu quả rõ ràng với các hệ thông phục hồi tốt, điều đó giúp chúng ta tăng độ tin cậy từ source code, deployment, delivery và môi trường production ngay từ đầu.

  • Giả lập môi trường phát triển AWS với LocalStack

    Giả lập môi trường phát triển AWS với LocalStack

    Khi chúng ta xây dựng các ứng dụng với AWS, chúng ta truy cập các dịch vụ AWS khác nhau cho nhiều mục đích: lưu trữ tệp trong S3, lưu một số dữ liệu trong DynamoDB, gửi tin nhắn tới SQS, viết trình xử lý sự kiện bằng các hàm lambda và nhiều mục đích khác.

    Tuy nhiên, trong những ngày đầu phát triển, chúng ta thích tập trung vào việc viết mã ứng dụng thay vì dành thời gian cho việc thiết lập môi trường để truy cập các dịch vụ AWS. Việc thiết lập môi trường phát triển để sử dụng các dịch vụ này tốn nhiều thời gian và phát sinh chi phí không mong muốn với AWS.

    Để tránh bị sa lầy bởi những tác vụ thông thường này, chúng ta có thể sử dụng LocalStack để phát triển và kiểm tra các ứng dụng của mình với các triển khai mô phỏng của các dịch vụ này.


    Tại sao nên dùng LocalStack?

    Chúng ta thường hay sử dụng phương thức dummy như mock, fake, proxy object để chạy Unit test cho ứng dụng với việc sử dụng các liên kết bên ngoài như database, aws, …

    Với LocalStack, chúng ta sẽ giả lâp môi trường trường AWS phía local. LocalStack hỗ trợ:

    • Chạy các ứng dụng tương tác với các dịch vụ AWS ở phía local mà không cần kết nối môi trường thật.

    • Tránh sự phức tạp của cấu hình AWS và tập trung vào phát triển.

    • Tích hợp chạy thử nghiệm với CI / CD pipeline.

    • Cấu hình và thử nghiệm các kịch bản lỗi.


    Cách sử dụng LocalStack?

    Việc sử dụng LocalStack của chúng tôi tập trung vào hai nhiệm vụ:

    • Chạy LocalStack.
    • Ghi đè URL điểm cuối AWS bằng URL của LocalStack.

    LocalStack thường chạy bên trong vùng chứa Docker, nhưng thay vào đó chúng ta cũng có thể chạy nó dưới dạng ứng dụng Python.


    Chạy Localstack với Python

    Trước tiên, chúng ta cài đặt LocalStack bằng pip:

    pip install localstack
    

    Sau đó, chúng ta bắt đầu localstack bằng lệnh start:

    localstack start
    

    Thao tác này sẽ khởi động LocalStack bên trong vùng chứa Docker.


    Chạy Localstack với Docker

    Bạn cũng có thể sử dụng tệp docker-compose.yml từ Github và sử dụng lệnh này (hiện yêu cầu docker-compile phiên bản 1.9.0+):

    docker-compose up
    

    Tùy chỉnh LocalStack

    Mặc định của LocalStack là tạo ra tất cả các dịch vụ được hỗ trợ với mỗi dịch vụ trong số chúng đang lắng nghe trên cổng 4566. Chúng ta có thể ghi đè hành vi này của LocalStack bằng cách thiết lập một vài biến môi trường.

    Port mặc định 4566 có thể được ghi đè bằng cách đặt biến môi trường EDGE_PORT. Chúng tôi cũng có thể định cấu hình LocalStack để tạo ra một nhóm dịch vụ hạn chế bằng cách đặt danh sách tên dịch vụ được phân tách bằng dấu phẩy làm giá trị cho biến môi trường SERVICES:

    version: '2.1'
    
    services:
      localstack:
        container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
        image: localstack/localstack
        ports:
          - "4566-4599:4566-4599"
          - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}"
        environment:
          - SERVICES=s3,dynamodb
    

    Trong file docker-compose.yml này, chúng ta đặt biến môi trường SERVICES thành tên của dịch vụ chúng ta muốn sử dụng trong ứng dụng của mình (S3 và DynamoDB).


    Kết nối với LocalStack

    Chúng ta truy cập các dịch vụ AWS thông qua AWS CLI hoặc từ các ứng dụng của chúng ta bằng AWS SDK.

    AWS SDK và CLI là một phần không thể thiếu trong bộ công cụ của chúng ta để xây dựng ứng dụng với các dịch vụ AWS. SDK cung cấp các thư viện máy khách bằng tất cả các ngôn ngữ lập trình phổ biến như Java, Node js hoặc Python để truy cập các dịch vụ AWS khác nhau.

    Cả AWS SDK và CLI đều cung cấp tùy chọn ghi đè URL của AWS API. Chúng ta thường sử dụng điều này để chỉ định URL của máy chủ proxy của chúng ta khi kết nối với các dịch vụ AWS từ phía sau máy chủ proxy của công ty. Chúng ta sẽ sử dụng tính năng tương tự này trong môi trường local của chúng tôi để kết nối với LocalStack.

    Chúng ta thực hiện việc này trong AWS CLI bằng các lệnh như sau:

    aws --endpointurl http://localhost:4566 s3 ls
    

    Việc thực thi lệnh này sẽ gửi các yêu cầu đến URL của LocalStack được chỉ định làm giá trị của tham số dòng lệnh URL điểm cuối (localhost trên port 4566) thay vì đến AWS thật.

    Chúng ta sử dụng cách tiếp cận tương tự khi sử dụng SDK:

    URI endpointOverride = new URI("http://localhost:4566");
    S3Client s3 = S3Client.builder()
      .endpointOverride(endpointOverride )  // <-- Overriding the endpoint
      .region(region)
      .build();
    

    Ở đây, chúng ta đã ghi đè điểm cuối AWS của S3 bằng cách cung cấp giá trị của URL của LocalStack làm tham số cho phương thức endpointOverride trong lớp S3ClientBuilder.

    Toàn bộ bài viết là giới thiệu về framework LocalStack. Trong bài viết sau, mình sẽ hướng dẫn mọi người cách tích hợp nó vào source code như nào nhé.

  • Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Upload file dung lượng lớn tới S3 với Multipart và Presign-url

    Nếu như bình thường ta thực hiện single upload tới s3 thì sẽ có 2 cách sau:

    • Upload file lên s3 qua server của chúng ta: Cái này thì reject vì chúng ta phải tốn thời gian upload lên server của mình rồi từ đó mới upload lên S3. Với large file thì gần như là không nên.

    • Upload từ client bằng cách sử dụng presign-url. Thay vì phải qua server trung gian thì chúng ta upload thẳng lên S3. Tốc độ cải thiện rất nhiều vì cách 1 đa phần thời gian tốn ở khâu upload lên server của chúng ta.

    Nhưng nói gì thì nói single upload to S3 thì sẽ có những hạn chế sau dưới Maximum size chỉ là 5GB . Điều này thực sự hạn chế. Hơn nữa AWS cũng suggest chúng ta là file >100MB thì nên sử dụng Multipart Upload . Vậy ưu điểm chính của nó là:

    • Maximum size là 5TB
    • Tốc độ upload tốt hơn

    Điều đó mình lựa chọn multipart và presign-url cho bài toán upload file dung lượng lớn. Server ở đây mình sử dụng là python. Client thì dùng angular.

    part1

    Server

    Phần server này mình sử dụng kiến trúc serverless để triển khai với ngôn ngữ Python Flask trên môi trường AWS Cloud

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Phần Server chủ yếu sẽ có 3 api chính:

    • Đầu tiên là api start-upload, với logic là request tới s3 để lấy uploadId

      @app.route("/start-upload", methods=["GET"])
      def start_upload():
          file_name = request.args.get('file_name')
          response = s3.create_multipart_upload(
              Bucket=BUCKET_NAME, 
              Key=file_name
          )
      
          return jsonify({
              'upload_id': response['UploadId']
          })
      
    • Tiếp theo là api get-upload-url để lấy presignurl cho từng part của file khi upload

      @app.route("/get-upload-url", methods=["GET"])
      def get_upload_url():
           file_name = request.args.get('file_name')
           upload_id = request.args.get('upload_id')
           part_no = request.args.get('part_no')
           signed_url = s3.generate_presigned_url(
               ClientMethod ='upload_part',
               Params = {
                   'Bucket': BUCKET_NAME,
                   'Key': file_name, 
                   'UploadId': upload_id, 
                   'PartNumber': int(part_no)
               }
           )
      
           return jsonify({
               'upload_signed_url': signed_url
           })
      
    • Cuối cùng đây là api để kiểm tra xem việc upload đã hoàn thành chưa.

       @app.route("/complete-upload", methods=["POST"])
       def complete_upload():
           file_name = request.json.get('file_name')
           upload_id = request.json.get('upload_id')
           print(request.json)
           parts = request.json.get('parts')
           response = s3.complete_multipart_upload(
               Bucket = BUCKET_NAME,
               Key = file_name,
               MultipartUpload = {'Parts': parts},
               UploadId= upload_id
           )
           
           return jsonify({
               'data': response
           })
      

    Client

    Phần client này mình dùng Angular để triển khai 1 trang web upload file đơn giản.

    Các bạn tham khảo project trên Github của mình để hiểu cách setup nhé.

    Khi có action upload đầu tiên ta sẽ gọi tới function uploadMultipartFile. Function uploadMultipartFile có chức năng với các step sau:

    • Call api start-upload để lấy được uploadId.

      const uploadStartResponse = await this.startUpload({
          fileName: file.name,
          fileType: file.type
      });
      
    • Split file upload thành các chunks, ở đây chia thành 10MB/chunk nhé. Ở đây mình chia là 10MB vì minimum size của Multipart Upload là 10MB.

    • Thực hiện call api get-upload-url để lấy preSignurl cho từng chunk ở trên.

    • Upload các chunks bằng signurl.

    • Khi tất cả các chunks upload xong sẽ thực hiện call api complete-upload để xác nhận với S3 mình đã upload đầy đủ các part. Done.

      try {
          const FILE_CHUNK_SIZE = 10000000; // 10MB
          const fileSize = file.size;
          const NUM_CHUNKS = Math.floor(fileSize / FILE_CHUNK_SIZE) + 1;
          let start, end, blob;
      
          let uploadPartsArray = [];
          let countParts = 0;
      
          let orderData = [];
      
          for (let index = 1; index < NUM_CHUNKS + 1; index++) {
            start = (index - 1) * FILE_CHUNK_SIZE;
            end = (index) * FILE_CHUNK_SIZE;
            blob = (index < NUM_CHUNKS) ? file.slice(start, end) : file.slice(start);
      
            // (1) Generate presigned URL for each part
            const uploadUrlPresigned = await this.getPresignUrl({
              fileName: file.name,
              fileType: file.type,
              partNo: index.toString(),
              uploadId: uploadStartResponse.upload_id
            });
      
            // (2) Puts each file part into the storage server
      
            orderData.push({
              presignedUrl: uploadUrlPresigned.upload_signed_url,
              index: index
            });
      
            const req = new HttpRequest('PUT', uploadUrlPresigned.upload_signed_url, blob, {
              reportProgress: true
            });
      
            this.httpClient
              .request(req)
              .subscribe((event: HttpEvent<any>) => {
                switch (event.type) {
                  case HttpEventType.UploadProgress:
                    const percentDone = Math.round(100 * event.loaded / FILE_CHUNK_SIZE);
                    this.uploadProgress$.emit({
                      progress: file.size < FILE_CHUNK_SIZE ? 100 : percentDone,
                      token: tokenEmit
                    });
                    break;
                  case HttpEventType.Response:
                    console.log('Done!');
                }
      
                // (3) Calls the CompleteMultipartUpload endpoint in the backend server
      
                if (event instanceof HttpResponse) {
                  const currentPresigned = orderData.find(item => item.presignedUrl === event.url);
      
                  countParts++;
                  uploadPartsArray.push({
                    ETag: event.headers.get('ETag').replace(/[|&;$%@"<>()+,]/g, ''),
                    PartNumber: currentPresigned.index
                  });
                  if (uploadPartsArray.length === NUM_CHUNKS) {
                    console.log(file.name)
                    console.log(uploadPartsArray)
                    console.log(uploadStartResponse.upload_id)
                    this.httpClient.post(`${this.url}/complete-upload`, {
                      file_name: encodeURIComponent(file.name),
                      parts: uploadPartsArray.sort((a, b) => {
                        return a.PartNumber - b.PartNumber;
                      }),
                      upload_id: uploadStartResponse.upload_id
                    }).toPromise()
                      .then(res => {
                        this.finishedProgress$.emit({
                          data: res
                        });
                      });
                  }
                }
              });
          }
        } catch (e) {
          console.log('error: ', e);
        }
      

    Có những chú ý sau.

    • Minimum size của Multipart Upload là 10MB. Maximum là 5TB
    • Được upload tối đã 10000 chunks cho 1 file thôi. Ví dụ ở đây mình chia 1 chunk là 10MB thì mình chỉ upload tối đa 200GB. Tương tự 1 chunk là 5GB => upload tối đa là 5TB.

    Demo

    • Cài đặt server với serverless. Vào project serverless và gõ lệnh

      sls deploy
      

      Có những chú ý sau:

      • Sau khi cài đặt NodeJS thì cài Serverless framework

        npm i -g serverless
        
      • Cần có account AWS

      • Config ở file serverless đang sử dụng aws profile mà mình đã setup ở local(khanhcd92). Nếu bạn dùng default hoặc cấu hình profile riêng thì hãy thay đổi nó nhé. Cách cấu hình aws profile.

        provider:
          name: aws
          runtime: python3.6
          stage: dev
          region: ap-southeast-1
          profile: khanhcd92
        
    • Thay đường dẫn API endpoint từ output của cài đặt server cho url trong class UploadService của project web

    • Cài đặt thư viện trong project web

      npm i
      
    • Chạy web angular ở local với lệnh

      npm start
      

      part2

    • Kéo file cần upload vào trình duyệt. Chỗ này mình sẽ dùng 1 file có dụng lượng khoảng 75M để upload.

      part3

    • Kiểm tra kết quả trên S3 part4

    Chi tiết source code các bạn xem ở đây nhé.

  • [Flutter] Xuất bản flutter web lên Github

    [Flutter] Xuất bản flutter web lên Github

    [Flutter] Xuất bản flutter web lên Github

    Giới thiệu

    Github có chức năng rất nay là giúp các bạn đưa trang web của mình lên hoàn toàn miễn phí. Hôm mình mình sẽ hướng dẫn các bạn cách deploy 1 trang web Flutter lên Github nhé. Cùng bắt đầu thôi nào!

    Các bước thực hiện

    Để deploy 1 trang web flutter lên github. Trước tiên chúng ta cần tạo 1 repository trên github, sau đó tạo 1 dự án flutter và đẩy nó lên github.

    Tạo repository Github

    • Các bạn vào link github.com, đăng nhập vào và nhấn New nhé

    • Ở Repository name mình sẽ điền đại 1 tên là demo_flutter_web và nhấn Create repository

    • Sau đó mình sẽ được dẫn tới link repository https://github.com/tiendung01023/demo_flutter_web. Các bạn nhớ link github này nha, tý mình sẽ dùng tới

    Xong phần khởi tạo repository git. Giờ mình đến phần flutter thôi.

    Tạo dự án flutter

    • Các bạn mở Terminal lên và tạo 1 project nha. Ở đây mình sẽ đặt tên là demo_web
    flutter create demo_web
    
    • Giờ tiếp tục tại terminal, mình cd vô dự án
    cd demo_web
    
    • Vì giờ mình quan tâm đến web, tạm thời bỏ qua mobile. Nên mình sẽ chạy lệnh build trên chrome xem trang web chạy trên local sẽ như thế nào ha

    • Chạy demo xong các bạn nhấn Ctrl + C trên Windows hoặc control + C trên MacOS để dừng demo.

    • Lưu ý: khi bạn muốn deploy web của mình lên github. Bạn cần thêm tên repository vào file như sau:

    -> Bạn mở file demo_web/web/index.html bằng bất kì trình chỉnh sửa nào

    -> Dự án github mình đặt là demo_flutter_web. Tại thẻ base bạn sửa lại thành <base href="/demo_flutter_web/"> và nhấn lưu lại

    • Tiếp theo tại terminal mình sẽ chạy câu lệnh build flutter bản web
    flutter build web
    
    • Sau khi lệnh chạy xong bạn sẽ thấy thêm thư mục demo_web/build/web. Bạn cd terminal đến thư mục này. Mình sẽ chỉ publish thư mục này lên git thôi nha.
    cd build/web
    
    • Giờ mình upload thư mục này lên github. Các bạn mở lại link github repository sẽ có phần hướng dẫn nha, hoặc các bạn chạy từng dòng lệnh như mình làm dưới đây cũng được
    git init
    git add .
    git commit -m "first commit"
    git branch -M master
    git remote add origin [email protected]:tiendung01023/demo_flutter_web.git
    git push -u origin master
    
    • Sau khi chạy hết các bạn lên repo github xem các file đã được upload lên chưa nha

    Thế là đã xong phần tạo và upload dự án flutter_web lên github. Về sau khi bạn viết thêm chức năng cho dự án, bạn cần chạy lại câu lệnh build web và push data mới lên github nha.

    Tạo page demo trang web trên github

    • Vào link github. Chọn tab Settings, xuống mục Pages

    • Trong Github Pages, bạn chọn nhánh mà muốn hiển thị trang web, hiện tại của mình thì chọn master nha. Sau đó nhấn Save

    Như hình trên Github báo đã tạo thành công trang web Your site is published at https://tiendung01023.github.io/demo_flutter_web/. Bạn bấm vào link đó để xem thành quả nha

    Kết thúc

    Hi vọng qua bài viết của mình sẽ giúp bạn sáng tạo thêm 1 số trò với flutter web này nha

    Cảm ơn các bạn đã xem bài viết.

    Tác giả

    Phạm Tiến Dũng [email protected]

  • [Flutter] Hướng dẫn tăng điểm trên pub.dev

    [Flutter] Hướng dẫn tăng điểm trên pub.dev

    Giới thiệu

    Ở bài trước mình đã giới thiệu về cách upload plugin lên pub.dev (Xem lại tại đây). Hôm nay mình sẽ nói về cách tăng điểm pub points trên pub.dev nhé. Cùng bắt đầu thôi nào!

    Phân tích cách tính điểm trên pub.dev

    Trên pub.dev sẽ có 6 mục lớn để tính điểm cho plugin của bạn

    Tuân thủ quy ước của Dart (20 điểm)

    • 10 điểm: Cung cấp file pubspec.yaml hợp lệ
    • 5 điểm: Cung cấp file README.md hợp lệ
    • 5 điểm: Cung cấp file CHANGELOG.md hợp lệ

    Các bạn có thể tham khảo bài viết trước của mình nha

    Cung cấp hướng dẫn (20 điểm)

    • 10 điểm: Có ví dụ. Pub.dev sẽ lấy file example/lib/main.dart để làm ví dụ cho người dùng. Bạn nên cung cấp ví dụ đầy đủ trong 1 file này. Vì người khác sẽ ngại việc lấy project của bạn để hiểu được plugin của bạn dùng như thế nào
    • 10 điểm: Cung cấp hướng dẫn cho từng API. Theo yêu cầu của pub.dev thì bạn phải cung cấp ít nhất 20% các api. Tức ở mỗi biến, hàm, lớp được tạo ra, bạn phải thêm hướng dẫn bằng ///

    Hỗ trợ nhiều nền tảng (20 điểm)

    • 20 điểm: Pub.dev cần bạn hỗ trợ android, ios và web. Nếu chỉ hỗ trợ android và ios thôi thì bạn sẽ chỉ được 10 điểm. Sau khi code android và ios xong thì bạn nên nghiên cứu thêm cho cả bản web nữa nha

    Vượt qua bộ phân tích (30 điểm)

    • 30 điểm: Bạn cần fix hết các lỗi,cảnh báo, vấn đề. Hiện tại mình không chắc pub.dev dùng bộ check nào, hiện tại mình dùng bộ này và đã đạt được 30/30 điểm. Các bạn thử xem, nếu có bộ nào tốt hơn các bạn gợi ý cho mình dưới comment nha.

    Các gói phụ thuộc phải được cập nhật (10 điểm)

    • 10 điểm: Khi bạn thêm gói vào dependencies và dev-dependencies. Bạn cần cập nhật bản mới nhất cho những gói đó### Các gói phụ thuộc phải được cập nhật

    Hỗ trợ null-safety (20 điểm)

    • 20 điểm: Plugin của bạn cần nâng cấp lên hỗ trợ null-safety. Bởi vì từ bản flutter 2.12 trở về sau khi tạo dự án sẽ mặc định là có null-safety, thì những dự án này sẽ không thể thêm plugin của bạn nếu bạn có hỗ trợ null-safety

    Công cụ kiểm tra điểm

    Thật là khó chịu khi mà upload plugin xong mới biết bạn được bao nhiêu điểm đúng không?

    Vậy thì sẽ có cách bạn tự kiểm tra trước xem mình được bao nhiêu điểm, và bị mất điểm ở phần nào luôn nha.

    Công cụ đó chính là pana

    Các bạn cài pana vào máy tính bằng dòng lệnh sau:

    pub global activate pana
    

    Sau đó bạn vào terminal của dự án và gõ lệnh:

    flutter pub global run pana
    

    Đây là mình sau khi chạy lệnh được tool báo được chưa hỗ trợ nền tảng web nên chỉ được 110 điểm thôi.

    Kết thúc

    Hi vọng qua bài viết của mình sẽ giúp được các bạn đang gặp khó khăn với việc upload plugin lên pub.dev nha.

    Nguồn tham khảo

    Cảm ơn các bạn đã xem bài viết.

    Tác giả

    Phạm Tiến Dũng [email protected]

  • Thiết kế khả năng phục hồi cho một trang web tĩnh trên AWS Cloud

    Thiết kế khả năng phục hồi cho một trang web tĩnh trên AWS Cloud

    Khi bạn thiết kế một hệ thống thì bạn nên có suy nghĩ rằng không hệ thống nào có thể đảm bảo vận hành trơn chu mà không có vấn đề gì xảy ra. Hệ thống có thể sập vì rất nhiều lý do. Chúng ta luôn phải có phương án có thể khôi phục lại ngay tức thì để độ tổn thất cho khách hàng và công ty ở mức thấp nhất. Đặc biệt là các hệ thống trên Cloud.

    Werner Vogels, CTO & VP, Amazon says:

    Everything fails all the time.

    Bài viết hôm nay mình sẽ giới thiệu về cách thiết kế 1 trang web tĩnh trên S3 mà đảm bảo khả năng hồi phục(Resiliency).

    Khả năng phục hồi (Resiliency) là gì?

    Khả năng phục hồi là khả năng hệ thống phục hồi sau lỗi do hệ thống quá tải, bị tấn công, các lỗi liên quan phần cứng hoặc phần mềm.

    Amazon S3 rất khả dụng và có khả năng phục hồi cao đối với sự cố theo vùng nhưng liệu trang web có khả năng chống chịu với thảm họa khu vực như nguồn điện, thời tiết.,? đám mây có những cách tốt hơn để giúp cung cấp tính liên tục cho hoạt động kinh doanh và đây là một trong những cách.

    Thiết kế khả năng phục hồi cho một trang web tĩnh trên AWS Cloud

    s3dr

    Các dịch vụ sử dụng

    • Route 53
    • CloudFront
    • S3
    • IAM

    Triển khai

    Do chưa có domain có sẵn nên bước triển khai này mình sẽ chỉ sử dụng các dịch vụ:

    • CloudFront
    • S3
    • IAM

    Bước 1: Tạo S3 bucket name static-web-sample-s3 với region Singapore và enable tính năng Versioning. Chi tiết

    Bước 2: Tạo S3 bucket name static-web-sample-dr-s3 với region Tokyo và enable tính năng Versioning Chi tiết

    Ghi chú: Ở hướng dẫn này, web chính của mình triển khai lưu trên S3 với region Singapore, và web khôi phục khi xảy ra thảm hoạ sẽ được lưu trên S3 với region Tokyo.

    Bước 3: Cấu hình replication giữa 2 bucket name

    • Ở bucket name static-web-sample-s3 chọn Managemet tab. Tiếp theo, ở phần Replication rules chọn Create replication rule.

      • Nhập rule name rep1

      • Phần rule scope thì chọn option thứ 2 rep2

      • Ấn Browse S3 xong chọn đến bucket name static-web-sample-dr-s3 rep3

      • Phần IAM role, chọn Create new role rep4

      • Phần Additional replication options thì không chọn gì cả rồi ấn Save rep5

      • Kết quả sau khi cấu hình xong rep6

    Bước4: Tạo 1 trang page index.html đơn giản rồi upload nó lên S3 bucket name

    <!DOCTYPE html>
    <html lang='en'>
    
    <head>
        <meta charset='UTF-8'>
        <title>Static HTML</title>
    </head>
    
    <body>
        <div class="container">
            <h1>Hey there</h1>
        </div>
    </body>
    
    </html>
    

    Bước 5: Tạo CloudFront liên kết với S3 bucket name static-web-sample-s3. Chi tiết

    origin

    Ghi chú: Sử dụng OAI cho CloudFront access đến S3 và chọn Yes, update the bucket policy

    Bước 6: Tạo thêm origin của CloudFront liên kết tới S3 bucket name static-web-sample-dr-s3 dro2

    Ghi chú: Sử dụng OAI cho CloudFront access đến S3 và chọn Yes, update the bucket policy

    Bước 7: Tạo Origin Group của CloudFront. Nhập thông như hình bên dưới.

    • Chọn Create origin group og1
    • Chọn các origin cần sử dụng rồi thêm vào. Chú ý origin web chính sẽ được đặt lên đầu tiên. og2

    Bước 8: Cập nhật Behavior default của CloudFront Vì behavior default của CloudFront đang liên kết với origin của S3 bucket name static-web-sample-s3, nên chúng ta sẽ cần cập nhật sang sử dụng origin group.

    • Chọn Behavior Default rồi chọn Edit beha1
    • Ở màn hình Edit, cập nhật origin sang origin group (DR-Group) beha2

    Bước 9: Đợi CloudFront Deploy re1

    Bước 10: Sau khi CloudFront đã deploy xong, thì chúng ta vào brownser và nhập DNS link của CloudFront. web

    Bước 11: Xoá bucket name static-web-sample-s3 hoặc chỉ xoá file index.html trong bucket.

    Bước 12: Bạn vào lại trình duyệt reload lại trang web nhé. Bạn sẽ thấy trang web vẫn hoạt động bình thường dù đã xoá file hoặc S3 bucket chưa source chính. web

  • [Flutter] Hướng dẫn đưa plugin lên pub.dev

    [Flutter] Hướng dẫn đưa plugin lên pub.dev

    [Flutter] Hướng dẫn đưa plugin lên pub.dev

    Giới thiệu

    Ở bài trước mình đã giới thiệu về cách tạo plugin flutter và cách kết nối xuống native (Xem lại tại đây). Hôm nay mình sẽ nói về cách đưa plugin bạn đã viết lên trang chia sẻ của flutter là pub.dev. Cùng bắt đầu thôi nào!

    Lưu ý: Khi bạn đã xuất bản plugin lên pub.dev rồi thì không thể gỡ xuống được nữa. Vậy nên các bạn hãy xuất bản những gói ít nhất là có thể sử dụng được, tránh việc xuất bản chơi/thử nghiệm.

    Chuẩn bị plugin

    Khi xuất bản plugin lên pub.dev sẽ cần có những tiêu chuẩn cần tuân theo:

    • LICENSE: file này mô tả giấy phép cho plugin của bạn, quy định cách người khác sử dụng plugin của bạn như thế nào. Một số giấy phép phổ biến như MIT, Apache, BSD. Bạn có thể tham khảo cách viết một số license tại đây.
    • Dung lượng plugin: dung lượng sau khi nén gzip của bạn phải nhỏ hơn 100MB. Nếu lớn hơn bạn có thể chia ra nhiều plugin hoặc giảm các thành phần không cần thiết trong thư mục example.
    • Phụ thuộc: phần dependencies chỉ được phụ thuộc vào các plugin có sẵn trên pub.dev
    • Tài khoản: bạn cần có 1 tài khoản google đăng nhập sẵn trên pub.dev

    Tệp quan trọng

    Trang pub.dev sẽ dùng 1 số file để tạo nội dung cho plugin của bạn, bạn nên chăm chút các file này để plugin trông đẹp hơn

    • README.md: Đây là file mô tả về nội dung plugin của bạn, cách người khác sử dụng plugin. File này theo định dạng markdown. Bạn có thể tham khảo cách viết README của pub.dev tại đây
    • CHANGELOG.md: File này mô tả sự thay đổi trong plugin của bạn, mỗi khi bạn cập nhật 1 bản mới, bạn cần viết mô tả rõ ràng về sự thay đổi đó. File này cũng định dạng markdown
    • pubspec.yaml: File này điền các thông tin chi tiết của plugin

    name: Tên plugin

    description: Mô tả của gói

    version: phiên bản plugin

    homepage: đường dẫn đến repository git

    Chạy thử nghiệm

    Việc này giúp bạn biết gói của mình đã đủ thông tin chưa, sẽ còn cần bổ sung thêm những gì.

    Gọi lệnh trên terminal:

    dart pub publish --dry-run
    

    hoặc

    flutter packages pub publish --dry-run
    

    Sau khi chạy lệnh, nếu bạn thấy báo Package has 0 warnings. là đã ok rồi.

    Xuất bản

    Khi đã sẵn sàng cho việc xuất bản. Hãy gọi lệnh:

    dart pub publish
    

    hoặc

    flutter packages pub publish
    

    Flutter sẽ hỏi bạn có sẵn sàng để upload chưa, bạn điền y và nhấn Enter.

    Tiếp đến nếu bạn chưa upload lần nào lên pub.dev, terminal sẽ hiện lên 1 link, bạn click vào link này để mở trình duyệt và chọn tài khoản google bạn muốn quản lý thư viện

    Sau đó bạn trở về đợi terminal báo thành công thôi.

    Sau khi upload thành công thì bạn đợi khoảng 1 tiếng để pub.dev quét plugin của bạn và tính điểm thì plugin của bạn mới được hiển thị với mọi người.

    Kết thúc

    Hi vọng qua bài viết của mình sẽ giúp được các bạn đang gặp khó khăn với việc upload plugin lên pub.dev nha.

    Nguồn tham khảo

    Cảm ơn các bạn đã xem bài viết.

    Tác giả

    Phạm Tiến Dũng [email protected]