ニフクラ ブログ

ニフクラ/FJ Cloud-Vやクラウドの技術について、エンジニアが語るブログです。

Let's EncryptのSSL証明書発行とロードバランサーへの設定をSDKで自動化してみた

こんにちは!ニフクラエンジニアb:id:nifcloud-developersです!

以前、ニフクラ SDK for Goを公開した際に書いたニフクラ SDK for Go のご紹介 というブログ記事では、簡単なサンプルコードを載せていましたが、今回は実運用フェーズ向けのニフクラ SDK for Goを使ったプログラムをご紹介したいと思います。

概要

まず本記事のタイトルにあるLet's Encryptについて、軽く触れておきます。別のブログ記事になりますが、Let's EncryptのDNS-01方式をニフクラDNSで認証して無料のSSL証明書を取得し自動更新するというものがあります。 この記事では、ニフクラDNSで管理しているFQDNのLet's Encrypt証明書を取得する方法が紹介されています。

このブログ記事に記載された方法で発行した証明書をニフクラ ロードバランサーに設定したい場合は、 発行した証明書をコントロールパネルよりアップロードした上でロードバランサーに設定する という追加の 手順 が発生してしまします。

そこで今回はニフクラ SDK for Goを利用し、Go言語のプログラムから Let's Encrypt証明書の発行ロードバランサーの設定 までを 完全自動化 するプログラムを作成してみました。

f:id:nifcloud-developers:20210305104106p:plain

Go言語のプログラムからLet's Encrypt証明書を発行する方法

Go言語で書かれたACMEクライアントlegoというオープンソースソフトウェアを利用します。
legoならば、DNS-01で認証する場合ニフクラ DNSを含む様々なDNSサービスが対応しているので、Let's Encryptの証明書を簡単に発行できます。

証明書を発行しロードバランサーに適用する

以下の手順で証明書の発行とロードバランサーへの適用を実施します。

Step1. DNSゾーン登録

まずはニフクラDNSを利用するため、こちらを参考に証明書を取得したいドメインをゾーン登録しておきます。

Step2. 証明書を発行するプログラムコード作成

Go言語のプログラムからlegoのニフクラDNSで認証するプロバイダーを利用し、証明書を発行します。
legoをライブラリとして利用するには、サンプルコードニフクラDNSプロバイダーのドキュメントを参考に実装します。

Step3. 証明書をニフクラへアップロードするプログラムコード作成

ニフクラ SDK for Goを利用してStep2 で発行した証明書をニフクラにアップロードします。

ニフクラへのアップロードは以下のサンプルのようにUploadSslCertificateを呼び出すことで実行できます。

ニフクラ SDK for Go を利用して証明書をアップロードするサンプル
cfg := nifcloud.NewConfig(
        "YOUR_ACCESS_KEY_ID",
        "YOUR_SECRET_ACCESS_KEY",
        "jp-east-1",
)

svc := computing.New(cfg)
req := svc.UploadSslCertificateRequest(
    &computing.UploadSslCertificateInput{
        Certificate:          "CERTIFICATE",
        Key:                  "KEY",
        CertificateAuthority: "CA",
    },
)

resp, err := req.Send(context.TODO())

Step4. 証明書をロードバランサーに適用するプログラムコード作成

ニフクラ SDK for Goを利用して Step3 でアップロードした証明書をロードバランサーに適用します。

ロードバランサーへの適用は以下のサンプルのようにSetLoadBalancerListenerSSLCertificateを呼び出すことで実行できます。
リクエストパラメータに指定するSSLCertificateIdは、 Step3 のレスポンスのFqdnIdから取得できます。

ニフクラ SDK for Go を利用して証明書をロードバランサーへ適用するサンプル
cfg := nifcloud.NewConfig(
        "YOUR_ACCESS_KEY_ID",
        "YOUR_SECRET_ACCESS_KEY",
        "jp-east-1",
)

svc := computing.New(cfg)
req := svc.SetLoadBalancerListenerSSLCertificateRequest(
        &computing.SetLoadBalancerListenerSSLCertificateInput{
            LoadBalancerName:  "LB",
            LoadBalancerPort:  443,
            InstancePort:      80,
            SSLCertificateId:  "FqdnID",
    },
)

resp, err := req.Send(context.TODO())

Step5. 作成したプログラムを実行する

プログラムを実行すると、証明書が発行されロードバランサーへの適用まで実施されます。

あとはこのプログラムをsystemdのtimerなどで2か月毎に実行させることで、
Let's Encryptの証明書の有効期限を迎える前に、ロードバランサーの証明書更新が自動で実施されます。

まとめ

今回はニフクラSDK for Goの実用的な使い方として、 証明書のアップロードロードバランサーへの適用 のサンプルコードを紹介しました。
ニフクラSDK for Goはロードバランサーだけでなくマルチロードバランサーにも対応しているので、 マルチロードバランサーに証明書を適用したい場合も同じように自動化できます。

是非ニフクラSDK for Goを利用して様々なニフクラの操作を自動化してみてください。

最後に今回作成したプログラムコードの全体を載せておきます。

Let's Encryptの証明書発行とロードバランサーへの設定を自動化するプログラム
package main

import (
    "context"
    "crypto"
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "log"
    "time"

    "github.com/go-acme/lego/v4/certcrypto"
    "github.com/go-acme/lego/v4/certificate"
    "github.com/go-acme/lego/v4/lego"
    "github.com/go-acme/lego/v4/providers/dns/nifcloud"
    "github.com/go-acme/lego/v4/registration"
    sdk "github.com/nifcloud/nifcloud-sdk-go/nifcloud"
    "github.com/nifcloud/nifcloud-sdk-go/service/computing"
    kingpin "gopkg.in/alecthomas/kingpin.v2"
)

var (
    email              = kingpin.Flag("email", "Email address for let's encrypt account").Envar("NEOSKIVE_EMAIL").String()
    domain             = kingpin.Flag("domain", "Domain of the certificate").Envar("NEOSKIVE_DOMAIN").String()
    lbName             = kingpin.Flag("lb-name", "Name of the LB that sets the certificate").Envar("NEOSKIVE_LB_NAME").String()
    lbPort             = kingpin.Flag("lb-port", "LB Port of the LB that sets the certificate").Envar("NEOSKIVE_LB_PORT").Int64()
    instancePort       = kingpin.Flag("instance-port", "Instance Port of the LB that sets the certificate").Envar("NEOSKIVE_INSTANCE_PORT").Int64()
    region             = kingpin.Flag("region", "Region of the LB that sets the certificate").Envar("NEOSKIVE_REGION").String()
    computingAccessKey = kingpin.Flag("computing-access-key", "NIFCLOUD API AccessKey for computing").Envar("NEOSKIVE_COMPUTING_NIFCLOUD_ACCESS_KEY_ID").String()
    computingSecretKey = kingpin.Flag("computing-secret-key", "NIFCLOUD API SecretKey for computing").Envar("NEOSKIVE_COMPUTING_NIFCLOUD_SECRET_ACCESS_KEY").String()
    dnsAccessKey       = kingpin.Flag("dns-access-key", "NIFCLOUD API AccessKey for dns").Envar("NEOSKIVE_DNS_NIFCLOUD_ACCESS_KEY_ID").String()
    dnsSecretKey       = kingpin.Flag("dns-secret-key", "NIFCLOUD API SecretKey for dns").Envar("NEOSKIVE_DNS_NIFCLOUD_SECRET_ACCESS_KEY").String()

    svc *computing.Client
)

type User struct {
    Email        string
    Registration *registration.Resource
    key          crypto.PrivateKey
}

func (u *User) GetEmail() string {
    return u.Email
}
func (u User) GetRegistration() *registration.Resource {
    return u.Registration
}
func (u *User) GetPrivateKey() crypto.PrivateKey {
    return u.key
}

func main() {
    kingpin.Parse()

    svc = computing.New(
        sdk.NewConfig(
            sdk.StringValue(computingAccessKey),
            sdk.StringValue(computingSecretKey),
            sdk.StringValue(region),
        ),
    )

    ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second)
    defer cancelFn()

    certificates, err := requestCertificate()
    if err != nil {
        log.Fatalf("[ERROR] request certificate failed: %s", err)
    }

    fqdnID, err := uploadCertificate(ctx, certificates)
    if err != nil {
        log.Fatalf("[ERROR] upload certificate failed: %s", err)
    }

    err = setCertificate(ctx, fqdnID)
    if err != nil {
        log.Fatalf("[ERROR] set certificate failed: %s", err)
    }
}

func requestCertificate() (*certificate.Resource, error) {
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return nil, err
    }

    user := User{
        Email: sdk.StringValue(email),
        key:   privateKey,
    }

    config := lego.NewConfig(&user)
    config.Certificate.KeyType = certcrypto.RSA2048

    client, err := lego.NewClient(config)
    if err != nil {
        return nil, err
    }

    providerConfig := nifcloud.NewDefaultConfig()
    providerConfig.AccessKey = sdk.StringValue(dnsAccessKey)
    providerConfig.SecretKey = sdk.StringValue(dnsSecretKey)

    provider, err := nifcloud.NewDNSProviderConfig(providerConfig)
    if err != nil {
        return nil, err
    }

    err = client.Challenge.SetDNS01Provider(provider)
    if err != nil {
        return nil, err
    }

    reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
    if err != nil {
        return nil, err
    }
    user.Registration = reg

    request := certificate.ObtainRequest{
        Domains: []string{sdk.StringValue(domain)},
        Bundle:  true,
    }
    certificates, err := client.Certificate.Obtain(request)
    if err != nil {
        return nil, err
    }

    return certificates, nil
}

func uploadCertificate(ctx context.Context, certificates *certificate.Resource) (*string, error) {
    resp, err := svc.UploadSslCertificateRequest(
        &computing.UploadSslCertificateInput{
            Certificate:          sdk.String(string(certificates.Certificate)),
            Key:                  sdk.String(string(certificates.PrivateKey)),
            CertificateAuthority: sdk.String(string(certificates.IssuerCertificate)),
        },
    ).Send(ctx)
    if err != nil {
        return nil, err
    }
    return resp.FqdnId, nil
}

func setCertificate(ctx context.Context, fqdnID *string) error {
    _, err := svc.SetLoadBalancerListenerSSLCertificateRequest(
        &computing.SetLoadBalancerListenerSSLCertificateInput{
            LoadBalancerName: lbName,
            LoadBalancerPort: lbPort,
            InstancePort:     instancePort,
            SSLCertificateId: fqdnID,
        },
    ).Send(ctx)
    return err
}