Blog

Verwendung von Slack zur automatischen Bereitstellung von Softwareprojekten mit Vorlagen

Greg Fogelberg
April 28, 2020

Hier bei FloQast verwenden wir eine Kombination aus GitHub, Jenkins, Terraform und AWS für die Wartung, Bereitstellung, Erstellung und Unterbringung unserer Infrastruktur und Softwareanwendungen. Eines der Projekte, die wir entwickeln, ist eine Sammlung von React-Anwendungen, die in einem S3-Bucket hinter einem Cloudfront Content Delivery Network gespeichert sind. Da all diese React-Anwendungen im Grunde mit einem gemeinsamen Satz von Attributen ins Leben gerufen werden, dachten wir: „Was wäre, wenn wir eine neue React-Anwendung erstellen könnten, ohne dass ein Entwickler etwas über die Infrastruktur oder unsere CI/CD-Pipeline wissen müsste. Und was wäre, wenn sie das alles von Slack aus tun könnten?“ Mit dieser Frage wurde unser GRAIL-Projekt gegründet.

Das Ziel dieses Projekts ist es, schnell und automatisiert neue GitHub-Repositorys erstellen zu können, die auf einer Reihe von Templates basieren. Unsere Entwickler können dann schneller iterieren, ohne sich um die Einrichtung einer Backend-Infrastruktur kümmern zu müssen. Mit ein bisschen Backronymisierung haben wir uns für „(G) GitHub (R) Repository (A) Automation to (I) Increase (L) Egerity“ für GRAIL entschieden. Wie bei den meisten Projekten begann GRAIL als Whiteboard-Sequenzdiagramm:

Grail Whiteboard

Schließlich sah das Sequenzdiagramm wie folgt aus:

GRAIL-Sequenzdiagramm

GitHub Terraform

Die Basis unseres Stacks ist eine Reihe von Terraform-Skripten, die so eingerichtet sind, dass sie gegen den GitHub Terraform Provider ausgeführt werden. Dieser Workflow kann auch unabhängig vom bevorstehenden Slash-Befehl von Slack eingerichtet und durch eine beliebige Anzahl von Methoden ausgelöst werden. Hier bei FloQast haben wir uns dafür entschieden, Jenkins Pipeline-Jobs für die Bewerbung unserer Terraform zu verwenden.Wir haben zwei Klassen von GitHub-Repositorys, die wir mit diesem Job erstellen können: Template Repositories oder Blank Repositories.

Vorlagen-Repositorys

Für jedes unserer Standardprojekte haben wir zunächst eine erstellt GitHub-Vorlagen-Repository. Dieses Vorlagen-Repository enthält den Mindestcode oder das Gerüst, das für die Erstellung einer Basisanwendung erforderlich ist. Dies kann eine React js-Anwendung, ein einfaches Lambda oder sogar ein System zur Unterbringung mehrerer Lambdas sein. Unsere Terraform erstellt dann eine Kopie dieses Repositorys mit dem Vorlagenparameterbeim Erstellen eines neuen Repositorys.

1# Create Templatized GitHub Repository
2resource "github_repository" "template_repo" {
3  name        = local.repository_name
4  description = "Repository for ${var.repository_name}"
5  private     = true
6  template {
7    owner      = var.github_organization
8    repository = var.template_repository
9  }
10  count = var.templatized_repo == "template" ? 1 : 0
11}

Nachdem das gerüstete Repository erstellt wurde, fügen wir dem Master-Branch Branch Branch-Schutz hinzu. Wir setzen Statusüberprüfungen für Repo-Administratoren durch, lehnen veraltete Reviews ab, verlangen eine Überprüfung des Code-Inhabers, mindestens zwei Reviews, und setzen durch, dass nur unser QE-Team zu „Master“ fusionieren kann.

resource "github_branch_protection" "branch_protection" {
  repository = github_repository.template_repo[count.index].name
  branch     = "master"
  enforce_admins = true
  required_pull_request_reviews {
    dismiss_stale_reviews           = true
    require_code_owner_reviews      = true
    required_approving_review_count = 2
  }
  depends_on = [
    github_team_repository.qe_production
  ]
  restrictions {
    teams = [
      data.github_team.qe_production.slug
    ]
  }
  count = var.templatized_repo == "template" ? 1 : 0
}

Als Nächstes fügen wir GitHub-Webhooks hinzu, um Validierungs- und Build-Jobs auf unserem Jenkins-Server auszulösen. Mehr dazu folgt weiter unten.

# Add Webhooks to the repository
resource "github_repository_webhook" "jenkins_push" {
  repository = github_repository.template_repo[count.index].name
  configuration {
    url          = local.webhook_url
    content_type = "form"
    insecure_ssl = false
  }
  events = ["push"]
  count = var.templatized_repo == "template" ? 1 : 0
}

Leere Repositorys

Manchmal wollen wir nur ein leeres Repository, also erlauben wir das auch.

# Create Bare Bones GitHub Repository
resource "github_repository" "stock_repo" {
  name        = local.repository_name
  description = "Repository for ${var.repository_name}"
  private     = true
  auto_init   = true
  count       = var.templatized_repo == "stock" ? 1 : 0
}

Um zwischen den beiden Arten von Repositorys zu wechseln, übergeben wir eine Variable namens `templatized_repo`, die in unserem Jenkins-Job definiert ist:

stage('Set Template Parameters') {
    steps {
        script {
            env.TEMPLATIZED_REPO = 'template'
            if (params.TEMPLATE_REPO == 'template-client') {
                env.BUILD_JOB = 'Validate_client'
            } else if (params.TEMPLATE_REPO == 'custom') {
                env.TEMPLATIZED_REPO = 'stock'
                env.BUILD_JOB = 'notapplicable'
            }
        }
    }
}

Diese Variable wird dann zusammen mit mehreren anderen an unseren Terraform-Befehl übergeben:

terraform plan -refresh=true -input=false 
-var=repository_name=${params.REPO_NAME} 
-var=jenkins_credentials=${JENKINS_CREDENTIALS} 
-var=build_job=${BUILD_JOB} 
-var=template_repository=${params.TEMPLATE_REPO} 
-var=templatized_repo=${TEMPLATIZED_REPO} 
-out=tfplan.plan-${ENVIRONMENT}-${BUILD_NUMBER}

GitHub-Teams

Unabhängig von der Art des GitHub-Repositorys, das erstellt wird, werden wir dann unsere GitHub-Teams mit „Push“ -Berechtigungen (dh: „Schreiben“) zum Repository hinzufügen.

# Add teams to repository
resource "github_team_repository" "engineering_repo" {
  team_id    = data.github_team.engineering.id
  repository = data.github_repository.repo.name
  permission = "push"
}

Jenkins

Damit das oben Genannte in einer Jenkins-Umgebung funktioniert, müssen einige Anmeldeinformationen eingerichtet werden. Es gibt mehrere Möglichkeiten, Anmeldeinformationen in Jenkins zu erstellen. Bei FloQast verwenden wir am besten die Jenkins-Konfiguration als Code Plugin in Verbindung mit dem Konfiguration als Code AWS SSMPlugin.

Der erste erforderliche Berechtigungsnachweis ist ein GitHub-Token mit dem Geltungsbereich „Repo“ daran befestigt. Diese Anmeldeinformationen werden gespeichert als Art: Geheimer Text die wir benannt haben github_token. Per der Terraform-DokumentationDiese Anmeldeinformationen werden als Umgebungsvariable geladen und an die Terraform Plan- und Apply- Stufen der zugehörigen Jenkins-Pipeline-Datei übergeben.

withCredentials([string(credentialsId: 'github_token', variable: 'GITHUB_TOKEN')]) {
    terraformApply()
}

Bei FloQast verwenden wir GitHub-Webhooks, um Deployment-Jobs auszulösen, wenn Code entweder übertragen oder zusammengeführt wird. Da die Webhooks einen Benutzernamen und ein Token benötigen, um unsere Okta-Authentifizierung zu durchlaufen, müssen wir dies als sichere Zeichenfolge an unseren Terraform-Code übergeben. Diese Anmeldeinformationen werden gespeichert als Art: Nutzername mit Passwortdie wir benannt haben github.com/webhook_id. In diesem Fall ist der Benutzername die Unternehmens-E-Mail eines Dienstkontos und das Passwort ist ein API-Token, das an das Jenkins-Konto dieses Benutzers angehängt ist.

Diese Anmeldeinformationen werden dann als Umgebungsvariable in unsere Jenkins-Pipeline importiert:

environment {
    JENKINS_CREDENTIALS = credentials('github_webhook_id')
}

Die Anmeldeinformationen können dann als Variable verwendet werden, wenn sie an Terraform übergeben werden:

terraform plan -refresh=true -input=false 
-var=jenkins_credentials=${JENKINS_CREDENTIALS} 
... 
-out=tfplan.plan-${ENVIRONMENT}-${BUILD_NUMBER}

Unsere Terraform erstellt dann eine lokale Variable Webhook_URL was an die weitergegeben wird github.com/repository_webhook oben erwähnte Ressource:

locals {
  webhook_url = "https://${var.jenkins_credentials}@${var.alb_dns}/job/${var.build_job}/buildWithParameters"
}

Der letzte Schritt besteht darin, eine Jenkins-Pipeline-Datei zu haben, die die Anmeldeinformationen und alle zusätzlichen Variablen (wie den Repository-Namen) verbraucht und dann die Terraform-Schritte ausführt.

Der GRAIL AWS Lambda

Das GRAIL-Projekt besteht aus vier Hauptkomponenten: Terraform für die Erstellung der Ressourcen und das Hochladen des AWS-Lambda-Quellcodes, eine Authn/Authz-Lambda-Ebene, den Lambda-Funktionsquellcode für die Verarbeitung der Logik und einen Jenkins-Pipeline-Job für die Bereitstellung der Terraform in AWS.

Terraform

Die Terraform besteht aus mehreren Teilen, aber die wichtigsten Bestandteile sind das Erstellen von SSM-Parameter-Store-Platzhaltern, das Erstellen eines API-Gateways sowie das Verpacken und Hochladen des Lambda-Src-Verzeichnisses. Verwenden der Terraform aws_ssm_parameter Ressource Wir erstellen Platzhalterwerte für alle Werte, die geheim sind oder sich zwischen unserer Produktions- und unserer Entwicklungsumgebung unterscheiden können. Diese Werte werden zur Laufzeit in unser Lambda geladen, sodass sie für unsere Funktion verfügbar sind, wenn der Slack-Befehl aufgerufen wird. Hier ist ein Beispiel für eine solche Ressource:

resource "aws_ssm_parameter" "slack_signing_secret" {
  name   = "/slack/ops/signingsecret"
  type   = "SecureString"
  value  = "terraform"
  description = "Slack Integration Secret"
  overwrite = true // Needed for the lifecycle change
  lifecycle {
    ignore_changes = ["value"] //Don't overwrite our manually entered value
  }
}

Die Parameter, die wir setzen, lauten wie folgt:

  • slack_signing_secret: Wird verwendet, um zu authentifizieren, dass die Anfrage von Slack stammt
  • slack_authorized_channel: Channel-ID für alle Slack-Channels, die autorisiert sind, den Slack-Befehl auszuführen, eingegeben als JSON-Wert, also: {„Kanalname“: „KANALID“}
  • jenkins_username: Benutzername für einen Jenkins-Benutzer mit Zugriff auf die Ausführung der erforderlichen Jobs
  • jenkins_user_token: Jenkins-API-Token, das zur Authentifizierung mit der Jenkins Okta-Integration für den obigen jenkins_username verwendet wird
  • jenkins_job_token: Job-Token zur Verwendung mit dem Build-Token-Root-Jenkins-Plugin
  • jenkins_environment: Wir führen mehrere Jenkins Master-Instanzen aus; eine für die Entwicklung und eine für die Produktion. Die Agenten, die einem dieser Master zugeordnet sind, haben keinen Zugriff auf die Umgebung des anderen Masters. Diese Variable wird als Jobparameter verwendet, um dem Agenten mitzuteilen, in welcher Umgebung die Bereitstellung erfolgen soll.
  • jenkins_protocol: 'http' oder 'https', wir empfehlen, diesen Wert auf 'https' zu setzen
  • jenkins_internal_url: DNS-Eintrag für die Jenkins-Instanz
  • jenkins_job_path: Der Pfad zum Jenkins-Job, wie vom Build-Token-Root-Plugin beschrieben, dh: Mein Ordner/Mein Job

Wir haben eine API-Gateway-Rest-API als Einstiegspunkt zu unserem Lambda eingerichtet. Dieses API-Gateway fungiert als Proxy und leitet alle Anfragen, die an diesen API-Endpunkt gerichtet sind, an unser GRAIL-Lambda weiter. Das API Gateway wird derzeit mit einer Lambda-Integration mit Proxy als Integrationstyp bereitgestellt. Dies ermöglicht eine schnellere Bearbeitung von Code, ohne dass Sie sich Gedanken über die Komplexität der Konfiguration des API-Gateways mit Coderevisionen machen müssen (z. B. neue Endpunkte, neue/geänderte/gelöschte Methoden). Für die Bereitstellung des Lambda-Codes selbst verwenden wir archive_datei Datenquelle, um eine Zip-Datei zu erstellen, die den Projektcode enthält.

data "archive_file" "lambda_zip" {
  type        = "zip"
  output_path = "${local.archive_path}"
  source_dir  = "../src/slack-jenkins-auth"
}

Wir verwenden dann die aws_lambda_functionRessource, um das Lambda zu erstellen, ihm Zugriff auf die Jenkins-VPC zu gewähren und den Quellcode hochzuladen.

resource "aws_lambda_function" "lambda_function" {
  function_name    = "${var.function_name}"
  filename         = "${local.archive_path}"
  source_code_hash = "${data.archive_file.lambda_zip.output_base64sha256}"
  handler          = "lambda_function.lambda_handler"
  runtime          = "${var.runtime}"
  role             = "${aws_iam_role.lambda_role.arn}"
  vpc_config {
    security_group_ids = ["${data.aws_security_group.jenkins_master.id}"]
    subnet_ids         = ["${data.aws_subnet.ops_private_subnet_a.id}"]
  }
}

Darüber hinaus verwenden wir ein IAM-Modul, um eine Lambda-Rolle mit dem zu verknüpfen AWSLAMBDAVPcAccessExecution-Rolle AWS-Richtlinie und eine zusätzliche benutzerdefinierte Richtliniengewährung SSM: getParameter für die erforderlichen SSM-Ressourcen.

resource "aws_iam_role" "lambda_role" {
  name               = "${var.function_name}"
  path               = "/service-role/"
  assume_role_policy = "${data.template_file.lambda-roles-file.rendered}"
}
resource "aws_iam_role_policy" "ssm_parameters_policy" {
  name   = "${var.name}-ssm-parameters-policy"
  role   = "${aws_iam_role.lambda_role.name}"l
  policy = "${data.template_file.ssm_parameters_policy_file.rendered}"
}
resource "aws_iam_role_policy_attachment" "lambda_vpc_access" {
  role       = "${aws_iam_role.lambda_role.name}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

AuthN- und AuthZ-Lambda-Layer

Um sicherzustellen, dass jedes Ereignis, das unser Lambda empfängt, von einem geeigneten Agenten gesendet wird, führen wir eine Autorisierungs- und Authentifizierungsvalidierung durch: Authentifizierung: Following dieses Wiki von Slack wir bestätigen, dass der Event-Body, den wir erhalten, von Slack stammt.

authenticated = fa.authenticate(event, SLACK_SIGNING_SECRET)
 ...
def authenticate(event, slack_signing_secret):
    """ Authenticate the request came from Slack """
    request_body = base64.b64decode(event['body']).decode('ascii') if event['isBase64Encoded'] else event['body']
    timestamp = event['headers']['X-Slack-Request-Timestamp']
    sig_basestring = f'v0:{timestamp}:{request_body}'.encode('utf-8')
    slack_signature = event['headers']['X-Slack-Signature']
    if abs(time.time() - float(timestamp)) > 60*5:
        return False
    hmac_key = hmac.new(slack_signing_secret, sig_basestring, hashlib.sha256)
    my_signature = f'v0={hmac_key.hexdigest()}'
    return hmac.compare_digest(my_signature, slack_signature)

Autorisierung: Wir wollten sicherstellen, dass wir einschränken können, wer den GRAIL-Befehl ausführen kann, und einschränken können, von wo aus der Befehl gestartet werden kann. Wir haben uns dazu entschlossen, einen privaten Channel einzurichten, in dem nur bestimmte Personen Zugriff auf Beiträge haben. Wie oben erwähnt, wird die Kanal-ID dann als SSM-Parameter gespeichert, in dem wir überprüfen, ob der Befehl tatsächlich von unserem angegebenen Kanal stammt. Außerdem protokollieren wir den Benutzer, der den Befehl initiiert hat. Wenn der Befehl von einem anderen Kanal stammt, melden wir den Benutzer als „nicht autorisiert“ an.

authorized = fa.authorize(event, json.loads(AUTHORIZED_CHANNELS_DICT))
...
def authorize(event, authorized_channels_dict):
    """ Authorize the requestor is in the authorized_users_dict """
    request_body = base64.b64decode(event['body']).decode('ascii') if event['isBase64Encoded'] else event['body']
    body_dict = dict(x.split("=") for x in request_body.split("&"))
    user_id = body_dict['user_id']
    channel_id = body_dict['channel_id']
    if channel_id in authorized_channels_dict.values():
        logging.warning('user_id: %s is authorized', user_id)
        return True
    logging.warning('user_id: %s is not an authorized_user', user_id)
    return False

Lambda

Zum Zeitpunkt der Erstellung dieses Artikels besteht unsere JSON-Datei Lambda Function.configuration aus 8 Hauptabschnitten: Wir verwenden eine JSON-Datei zum Speichern der SSM-Parameterspeicherpfade, die umgebungsspezifische Geheimnisse, Anmeldeinformationen und Parameter enthalten. Diese Datei wird in das Lambda geladen, das dann zum Setzen der unten genannten globalen Variablen verwendet wird. Diese Werte entsprechen denen, die im oben beschriebenen Terraform ssm-Setup eingerichtet wurden:

{
  "slack_secret_ssm_loc": "/slack/ops/signingsecret",
  "slack_channel_ssm_loc": "/slack/channel_id",
  "jenkins_environment_ssm_loc": "/jenkins/environment",
  "jenkins_protocol_ssm_loc": "/jenkins/protocol",
  "jenkins_username_ssm_loc": "/jenkins/username",
  "jenkins_user_token_ssm_loc": "/slack/ops/jenkinsusertoken",
  "jenkins_url_ssm_loc": "/jenkins/internalurl",
  "jenkins_job_path_ssm_loc": "/slack/ops/jobpath",
  "jenkins_job_token_ssm_loc": "/slack/ops/jenkinsjobtoken"
}

Globale Variablen: Durch die Verwendung globaler Variablen müssen wir den Parameter Store nur einmal aufrufen, wenn das Lambda geladen wird, und nicht jedes Mal, wenn der Slack-Befehl ausgeführt wird. Wir verwenden Boto 3 um Parameter Store aufzurufen, um die Werte abzurufen und sie für die spätere Verwendung zu speichern:

SSM_CLIENT = boto3.client('ssm')
AUTHORIZED_CHANNELS_DICT_PARAMETER = SSM_CLIENT.get_parameter(Name=CONFIG['slack_channel_ssm_loc'], WithDecryption=True)
AUTHORIZED_CHANNELS_DICT = AUTHORIZED_CHANNELS_DICT_PARAMETER['Parameter']['Value']

Authentifizierung und Autorisierung: Wenn ein Ereignis vom Lambda empfangen wird, besteht der erste Schritt darin, AuthN und AuthZ zu überprüfen. Dies wird von unserer Lambda-Ebene wie oben beschrieben behandelt.Parsing the Command: Wenn unser Ereignis empfangen wird, möchten wir den Befehl aus den restlichen Metadaten herausfiltern. Wir beginnen damit, das Ereignis zu dekodieren und dann den Textteil des Befehls herauszufiltern, den wir dann in ein Wörterbuch für die zukünftige Verwendung umwandeln:

command = parse_command(event)
...
def parse_command(event):
    request_body = base64.b64decode(event['body']).decode('ascii') if event['isBase64Encoded'] else event['body']
    body_dict = dict(x.split("=") for x in request_body.split("&"))
    return body_dict['text'].split('+')

Hilfe: Sobald ein Befehl authentifiziert und der Benutzer autorisiert ist, erlauben wir ihm, Parameter zu übergeben. Der erste Parameter, den wir initiiert haben, war der Hilfe Befehl. Indem du rennst/grail hilfe Wir stellen dem Benutzer die verfügbaren Befehlsoptionen sowie deren Beschreibungen und schließlich einen Beispielbefehl zur Verfügung. Wir bieten auch die zusätzliche Option, detailliertere Hilfe zu erhalten, indem wir Argumente in das Hilfe Befehl wie /grail hilfeclient:

if command[0] == 'help':
    return give_help()
...
def give_help(command):
    text = "Github Repository Automation to Increase Legerity (GRAIL)"
    attachment_text = ('• Choose your service: `client`, `system`, `lambda`, `custom` n'
                       '• Enter your desired repository name: `new_repo_name`n'
                       '• Optionally pass the following flag: n'
                       '`--debug` will run Terraform in debug mode n'
                       'example: `/grail client repo_name --debug`n'
                       'use `/grail help {service}` for additional help')
    if len(command) > 1:
        required_params = ('• Required parameters: n'
                           '`repository_name`')
        optional_params = ('• Optional parameters: n `--debug` will run Terraform in debug mode')
        if command[1] == "custom":
            text = "Creates a blank GitHub respository and attaches Teams with write permissions"
        attachment_text = required_params + 'n' + optional_params
    return {
        'statusCode': 200,
        'body': json.dumps({
            "response_type": "in_channel",
            "text": text,
            "attachments": [
                {
                    "color": "#36a64f",
                    "text": attachment_text
                }
            ]
        })
    }

Bearbeitung des Befehls: Es gibt mehrere Befehle, die an den GRAIL-Befehl übergeben werden können. Der Typ des zu erstellenden Repositorys, der Name des zu erstellenden Repositorys und dann gibt es eine Reihe von Flags, die ebenfalls übergeben werden können.

params = handle_command(command)

Repository-Typ: Der erste Parameter, der an den übergeben wird /Gral Der Befehl bestimmt, welcher Repository-Typ erstellt werden soll. Es kann eine beliebige Anzahl von Arten von Repositorys geben. Derzeit unterstützt der Code 3 Template-Repositorys und 1 leeres Repository. Je nachdem, um welchen Dienst es sich bei diesem ersten Parameter handelt, verwenden wir ein Wörterbuch, um diesen Wert dem zugehörigen Vorlagen-Repository zuzuordnen:

template_repo = {'client': 'template-react-client',
                 'lambda': 'template-nodejs-lambda',
                 'system': 'template-lambda-system',
                 'custom': 'custom'}

Repository-Name: Der zweite Parameter, der an den übergeben wird /Gral command ist der Name, der dem neu erstellten GitHub-Repositorium gegeben wird.Optionale Flags: Es gibt ein optionales Flag, das an den Befehl übergeben werden kann.

  • --debug: Führt Terraform im Debug-Modus aus

Rückgabeanweisung: Anschließend geben wir eine JSON-Zuordnung von Parametern und Werten zur Übergabe an den Downstream-Jenkins-Job zurück

def handle_command(command):
    """ Handle the command """
    logging.warning('command: %s', command)
    template_repo = {'client': 'template-client',
                     'lambda': 'template-lambda',
                     'system': 'template-system',
                     'custom': 'custom'}

    debug = False
    if '--debug' in command:
        debug = True

    if command[0] in template_repo:
        return {
            'TEMPLATE_REPO': template_repo[command[0]],
            'ENVIRONMENT': JENKINS_ENVIRONMENT,
            'REPO_NAME': command[1],
            'Debug': debug
        }

    return {
        'status': f'failure: {command[0]} is not a recognized service. Please run `/grail help` for a list of options'
    }

Jenkins aufrufen: Nachdem wir alle unsere Parameter gesetzt haben, formen wir sie zu einer Zeichenfolge und hängen sie an die URL an. Wenn wir die Payload an Jenkins senden, werden die Parameter nativ behandelt.

success = call_jenkins(URL, params)
...
def call_jenkins(url, params):
    """ Call Jenkins """
    for param in params:
        formatted_param = f'&{param}={params[param]}'
        url += formatted_param
    try:
        response = HTTP.request('POST', url)
        logging.warning('response: %s', {str(response.status)})
        return response.status in [200, 201]
    except Exception as e:
        logging.error(e)
        return False

Jenkins-Pipeline:

Die vierte und letzte Komponente ist eine Jenkins-Pipeline-Datei, die den Terraform-Plan verarbeitet und Schritte zum Ausführen der Terraform und zum Erstellen der erforderlichen Repositorys anwendet.

Slack

Schließlich erstellen wir mithilfe der Slack-GUI einen Slash-Befehl und verweisen ihn auf das API-Gateway, das sich vor unserem GRAIL Lambda befindet:

In unserem aktuellen Setup haben wir 2 Slack-Apps; Ops-Development und Ops-Production.

Ops-Entwicklung

Ops-Development ist die Slack-Anwendung, mit der wir alle Änderungen vor der Bereitstellung in der Produktion testen. Ops-Development verweist auf ein API-Gateway, das in unserem AWS-Entwicklungskonto existiert, auf einen privaten Slack-Kanal beschränkt ist und Jobs in unserer Dev-Jenkins-Umgebung auslöst. Wenn wir Terraform in unserer Dev-Jenkins-Umgebung ausführen, beenden wir den Vorgang nach der Terraform-Plan-Phase, um keine tatsächlichen GitHub-Repositorien zu erstellen. Hier ist ein Beispiel für den Betrieb eines /grail-devBefehl und die Antwort, die von Jenkins empfangen wird.

Ops-Produktion

Ops-Production ist unsere Produktions-Slack-Anwendung. Ops-Production verweist auf unser Produktions-API-Gateway in unserem AWS-Produktionskonto und löst Jobs in unserer Prod-Jenkins-Umgebung aus. Die Ausführung dieses Befehls ist derzeit auf einen öffentlichen Slack-Kanal beschränkt, für den Einschränkungen gelten, wer in dem Channel posten kann. Aus Gründen der Transparenz senden wir Slack-Benachrichtigungen zurück an den öffentlichen Channel, wenn die Terraform-Jobs von Jenkins ausgelöst werden, sowie das Ergebnis der Jobs.

Die nächsten Schritte

Für dieses Projekt sind noch mehrere Verbesserungen geplant:

  • Verschieben Sie die Verwaltung der globalen Variablen aus unserem Lambda in eine separate Ebene.
  • Fügen Sie einen Unterbefehl zum Erstellen eines neuen Template-Repositorys hinzu.
  • Möglichkeit hinzugefügt, GitHub nach einer Liste aller Template-Repositorys zu durchsuchen
  • Klonen Sie ein Templated-Repo auf der Grundlage des Repository-Namens, wodurch die Notwendigkeit entfällt Template_Repo Wörterbuch in der /handle_command () wirken