EMoshU Blog
2023/08/30
2023-08-31
kohei

Goで簡単インフラ構築

もくじ

 

 

 

はじめに

こんにちは。

サーバーサイドエンジニアのkoheiです。

今回は新プロダクトのAPIサーバー構築にCDKを採用してみたので、基本的な使い方と感想をお伝えします。

 

 

 

CDKについて

CDKは正式名称をAWS Cloud Development Kitといいます。

TypeScriptやPythonなどのプログラミング言語を用いてAWSリソースを定義・操作することができるIaCツールとなります。

Terraformで採用されているHCLなどの独自言語を使用していないため、アプリケーションエンジニアでも気軽に入門することができます。

 

今回CDKを選定した理由としては以下の2点です。

  1. 使用するクラウドサービスがAWSに限られている。
  2. インフラを専門にしていないアプリケーションエンジニアでも構築できる。

 

また採用するプログラミング言語としては普段の業務で使用しているGoを選択しました。

一般的にはTypeScriptを採用することが多いそうですが、弊社では自社プロダクトのバックエンド開発にはGoをメインに採用しており、統一感をだすためにGoにしてみました。

 

それでは実際にコードを書いていきましょう!

 

 

 

インフラ構成について

実装する前に今回構築するインフラ構成を簡単に説明します。

  • エンドユーザーはHTTP通信でエンドポイントにアクセスし、ALBがAPIサーバーであるECS(Fargate)に振り分けます。
  • APIサーバーは二つ用意し、どちらも異なるAZのPublic Subnetに配置します。
  • RDSはAuroraを使用し、WriterとReaderを異なるAZのPrivate Subnetに配置します。

 

構成図としては以下のようになります。

 

mariaインフラ構成図.drawio.png

 

 

実装!

 

CDKのインストール方法は公式のチュートリアルを参考にしました。

日本語版はないですが、読みやすいので環境は簡単に整います。

https://aws.amazon.com/jp/getting-started/guides/setup-cdk/

 

それでは実際の実装が以下となります。

(※ブログ用に一部実装を変更しています。)

main.go


package main

import (
    cdk "github.com/aws/aws-cdk-go/awscdk/v2"
    ec2 "github.com/aws/aws-cdk-go/awscdk/v2/awsec2"
    ecr "github.com/aws/aws-cdk-go/awscdk/v2/awsecr"
    ecs "github.com/aws/aws-cdk-go/awscdk/v2/awsecs"
    ecs_patterns "github.com/aws/aws-cdk-go/awscdk/v2/awsecspatterns"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsrds"
    secretmanager "github.com/aws/aws-cdk-go/awscdk/v2/awssecretsmanager"
    "github.com/aws/constructs-go/constructs/v10"
    "github.com/aws/jsii-runtime-go"
)

type EMoshUStackProps struct {
    cdk.StackProps
}

func NewEMoshUFargateWithALBStack(scope constructs.Construct, id string, props *EMoshUStackProps) cdk.Stack {
    var sprops cdk.StackProps
    if props != nil {
        sprops = props.StackProps
    }
    stack := cdk.NewStack(scope, &id, &sprops)

    // Create VPC
    vpc := ec2.NewVpc(stack, jsii.String("EMoshUVpc"), &ec2.VpcProps{
        MaxAzs: jsii.Number(2),
        // FargateをPublicSubnetに配置するため、NatGatewayは作成しない
        NatGateways: jsii.Number(0),
        SubnetConfiguration: &[]*ec2.SubnetConfiguration{
            {
                Name:       jsii.String("application"),
                SubnetType: ec2.SubnetType_PUBLIC,
            },
            {
                Name:       jsii.String("rds"),
                SubnetType: ec2.SubnetType_PRIVATE_ISOLATED,
            },
        },
    })

    // セキュリティグループの作成
    ecsSecurityGroup := ec2.NewSecurityGroup(stack, jsii.String("EMoshU-ecs-security-group"), &ec2.SecurityGroupProps{
        Vpc: vpc,
    })

    rdsSecurityGroup := ec2.NewSecurityGroup(stack, jsii.String("EMoshU-rds-security-group"), &ec2.SecurityGroupProps{
        Vpc:              vpc,
        AllowAllOutbound: jsii.Bool(true),
    })

    rdsSecurityGroup.Connections().AllowFrom(ecsSecurityGroup.Connections(), ec2.Port_Tcp(jsii.Number(3306)), jsii.String("allow from ecs"))

    // RDSのマスタユーザーとパスワード設定
    databaseCredential := secretmanager.NewSecret(stack, jsii.String("EMoshU-secret"), &secretmanager.SecretProps{
        RemovalPolicy: cdk.RemovalPolicy_DESTROY,
        SecretName:    jsii.String("mysql"),
        GenerateSecretString: &secretmanager.SecretStringGenerator{
            IncludeSpace:         jsii.Bool(false),
            ExcludePunctuation:   jsii.Bool(true),
            GenerateStringKey:    jsii.String("password"),
            SecretStringTemplate: jsii.String(`{"username": "EMoshU"}`),
        },
    })

    // RDSクラスターの作成
    rdsCluster := awsrds.NewDatabaseCluster(stack, jsii.String("EMoshU-rds-cluster"), &awsrds.DatabaseClusterProps{
        Engine:         awsrds.DatabaseClusterEngine_AuroraMysql(&awsrds.AuroraMysqlClusterEngineProps{Version: awsrds.AuroraMysqlEngineVersion_VER_3_01_1()}),
        Vpc:            vpc,
        VpcSubnets:     &ec2.SubnetSelection{SubnetType: ec2.SubnetType_PRIVATE_ISOLATED},
        SecurityGroups: &[]ec2.ISecurityGroup{rdsSecurityGroup},
        RemovalPolicy:  cdk.RemovalPolicy_DESTROY,
        Port:           jsii.Number(3306),
        Credentials: awsrds.Credentials_FromPassword(
            databaseCredential.SecretValueFromJson(jsii.String("username")).UnsafeUnwrap(),
            cdk.SecretValue_UnsafePlainText(databaseCredential.SecretValueFromJson(jsii.String("password")).UnsafeUnwrap()),
        ),
        DefaultDatabaseName: jsii.String("EMoshU"),
        Writer: awsrds.ClusterInstance_Provisioned(jsii.String("writer"), &awsrds.ProvisionedClusterInstanceProps{
            InstanceType: ec2.InstanceType_Of(ec2.InstanceClass_T3, ec2.InstanceSize_MEDIUM),
        }),
        Readers: &[]awsrds.IClusterInstance{
            awsrds.ClusterInstance_Provisioned(jsii.String("reader"), &awsrds.ProvisionedClusterInstanceProps{
                InstanceType: ec2.InstanceType_Of(ec2.InstanceClass_T3, ec2.InstanceSize_MEDIUM),
            }),
        },
    })

    // ECSクラスター作成
    cluster := ecs.NewCluster(stack, jsii.String("EMoshUECSCluster"), &ecs.ClusterProps{
        Vpc:         vpc,
        ClusterName: jsii.String("EMoshU-ecs-cluster"),
    })

    // タスク定義の作成
    taskDef := ecs.NewFargateTaskDefinition(stack, jsii.String("EMoshUTaskDefinition"), &ecs.FargateTaskDefinitionProps{
        // 最小構成に設定する
        MemoryLimitMiB: jsii.Number(512),
        Cpu:            jsii.Number(256),
    })

    // ECRのリポジトリを取得
    repository := ecr.Repository_FromRepositoryName(stack, jsii.String("EMoshU-repository"), jsii.String("EMoshU-mvp"))

    // タスク定義にコンテナを追加
    container := taskDef.AddContainer(jsii.String("EMoshUFargateContainer"), &ecs.ContainerDefinitionOptions{
        Image: ecs.ContainerImage_FromEcrRepository(repository, jsii.String("latest")),
        Environment: &map[string]*string{
            "DB_ADDRESS":  rdsCluster.ClusterEndpoint().Hostname(),
            "DATABASE":    jsii.String("EMoshU"),
            "DB_USER":     databaseCredential.SecretValueFromJson(jsii.String("username")).UnsafeUnwrap(),
            "DB_PASSWORD": databaseCredential.SecretValueFromJson(jsii.String("password")).UnsafeUnwrap(),
        },
        Logging: ecs.AwsLogDriver_AwsLogs(&ecs.AwsLogDriverProps{
            StreamPrefix: jsii.String("EMoshU-mvp"),
        }),
    })

    // コンテナとのポートマッピングを設定
    container.AddPortMappings(&ecs.PortMapping{
        ContainerPort: jsii.Number(8080),
        Protocol:      ecs.Protocol_TCP,
    })

    // サービスを作成
    ecs_patterns.NewApplicationLoadBalancedFargateService(stack, jsii.String("ALBFargoService"), &ecs_patterns.ApplicationLoadBalancedFargateServiceProps{
        Cluster:        cluster,
        TaskDefinition: taskDef,
        TaskSubnets: &ec2.SubnetSelection{
            Subnets: vpc.PublicSubnets(),
        },
        DesiredCount:           jsii.Number(2),
        ListenerPort:           jsii.Number(80),
        PublicLoadBalancer:     jsii.Bool(true),
        AssignPublicIp:         jsii.Bool(true),
        SecurityGroups:         &[]ec2.ISecurityGroup{ecsSecurityGroup},
        HealthCheckGracePeriod: cdk.Duration_Seconds(jsii.Number(3600)),
    })

    return stack
}

func main() {
    // CDKアプリケーションの初期化
    app := cdk.NewApp(nil)

    // ECSのサービスとタスク定義
    NewEMoshUFargateWithALBStack(app, "EMoshUFargateWithALBStack", &EMoshUStackProps{
        cdk.StackProps{},
    })

    app.Synth(nil)
}

VPCやDBなどの細かい説明は省略しますが、公式のサンプルコードが書き方などの参考になります。

https://github.com/aws-samples/aws-cdk-examples/tree/master/go/ecs

 

以上のファイルが配置されたディレクトリで


$ cdk deploy

を行うと、上記の構成のインフラが整います。

 

 

 

まとめ

一旦APIを動かす環境を作りたかったので、上記のコードでは以下のことが対応できていません。

  • HTTPSとの通信
  • 独自ドメインの設定

ここら辺はサービスリリースまでに対応したいところです。

ただインフラ未経験の私でも構築することができたので、CDKは便利なツールだと思います(Goだと情報量が少なかったので、情報量が多いTypeScriptにすれば良かったと途中で後悔していました。)。

 

ここまで読んでいただきありがとうございました。

EMoshUでは一緒に開発できる仲間を募集しております(インフラに強い人求む)。

ご興味を持たれましたら、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております!

 

 

 

参考文献

 

 

募集要項

 

 

 

 

まずは話を聞いてみたいという方へ